前言

后端开发中,很多问题并不是“代码写不出来”,而是“问题不好定位”。

尤其是接口联调、线上告警、偶发异常这类场景,经常会遇到下面这些情况:

  • 日志很多,但关键信息分散;
  • 异常堆栈能看到报错位置,但看不出根因;
  • 本地无法稳定复现;
  • 测试环境正常,线上偶尔出问题;
  • 看起来是空指针,实际可能是鉴权、并发、上下文传递或数据状态问题。

这类问题如果只靠人工一点点翻日志,效率并不高。现在很多开发者会尝试把 ChatGPT 5.5 这类大模型工具引入到开发流程中,用来辅助整理日志、归纳排查方向、生成 checklist、补充测试用例和复盘文档。

需要说明的是,大模型不能替代真实排查,也不能直接作为线上故障结论。它更适合做的是:把混乱的信息结构化,把可能遗漏的方向列出来,帮助开发者更快进入有效排查状态。

本文就以一个比较常见的 Java 后端接口异常为例,看看如何用 ChatGPT 5.5 辅助分析问题。


一、问题场景:订单创建接口偶发 500

假设有一个订单创建接口:

http

POST /api/order/create

接口逻辑比较常规:

  1. 获取当前登录用户;
  2. 查询商品信息;
  3. 判断库存;
  4. 创建订单;
  5. 扣减库存;
  6. 返回订单编号。

最近在联调和压测时发现,这个接口偶尔返回 500。

前端拿到的响应类似:

json

{  "code": 500,  "message": "Internal Server Error",  "data": null}

服务端日志如下:

text

2026-01-15 10:23:41.231 ERROR [order-service]c.e.order.controller.OrderController : create order failed
java.lang.NullPointerException: Cannot invoke "com.example.user.UserInfo.getUserId()"because "userInfo" is null    at com.example.order.service.OrderService.createOrder(OrderService.java:86)    at com.example.order.controller.OrderController.create(OrderController.java:42)

相关代码简化如下:

java

@PostMapping("/create")public Result<String> create(@RequestBody CreateOrderRequest request) {    String orderNo = orderService.createOrder(request);    return Result.success(orderNo);}

java

public String createOrder(CreateOrderRequest request) {    UserInfo userInfo = userContext.getCurrentUser();
    Long userId = userInfo.getUserId();
    Product product = productMapper.selectById(request.getProductId());    if (product.getStock() <= 0) {        throw new BizException("库存不足");    }
    Order order = new Order();    order.setUserId(userId);    order.setProductId(product.getId());    order.setAmount(product.getPrice());    orderMapper.insert(order);
    productMapper.decreaseStock(product.getId());
    return order.getOrderNo();}

从堆栈看,问题很明显:userInfo 是 null,调用 getUserId() 时抛出了空指针。

但真实排查时,不能只停留在“加个判空”这一层。
真正要搞清楚的是:为什么用户上下文会为空?


二、不要只问“这段代码哪里错了”

很多人在使用大模型分析代码时,会直接这样问:

text

下面代码报空指针,帮我看看怎么改。

这样问当然也能得到答案,但通常比较浅。模型大概率会给出:

  • userInfo 可能为空;
  • 需要增加判空;
  • 登录状态可能失效;
  • 检查 token;
  • 加全局异常处理。

这些建议没错,但对定位根因帮助有限。

更好的方式是,把问题背景、异常日志、代码片段和期望输出格式一起给 ChatGPT 5.5。

例如可以这样提问:

text

你是一名 Java 后端问题排查助手。下面是一个订单创建接口的异常日志和简化代码。
请你帮我完成以下任务:1. 只基于我提供的信息整理问题现象;2. 区分“已确认事实”和“可能原因”;3. 给出排查优先级;4. 不要直接下最终结论;5. 输出适合研发排查记录使用。
【异常日志】粘贴异常日志
【相关代码】粘贴代码
【补充背景】接口在并发稍高时更容易出现 500,单次手动调用不一定复现。

这样输入后,模型输出的内容通常会更接近“排查记录”,而不是简单的代码修复建议。


三、先让 ChatGPT 5.5 整理已知信息

一个比较有价值的输出应该类似下面这样:

markdown

## 已确认事实
1. `/api/order/create` 接口偶发返回 500。2. 异常类型为 `NullPointerException`。3. 空指针发生在 `OrderService.createOrder` 方法中。4. 具体位置是调用 `userInfo.getUserId()` 时。5. `userInfo` 来源于 `userContext.getCurrentUser()`。6. 问题在并发稍高时更容易出现,单次调用不一定复现。
## 可能原因
1. 请求未携带有效登录 token。2. token 解析失败,导致用户信息没有写入上下文。3. 鉴权拦截器没有被执行。4. 用户上下文基于 ThreadLocal,但在异步线程中丢失。5. ThreadLocal 设置或清理时机不正确。6. 并发测试时部分请求复用了错误的请求头或压测脚本未正确传参。7. 某些白名单路径配置错误,导致接口绕过鉴权。
## 暂不能确认的部分
1. 不能仅凭 NPE 判断一定是前端没传 token。2. 不能确认一定是 ThreadLocal 问题。3. 不能确认是否和异步调用有关。4. 不能确认是否所有失败请求都缺少用户信息。

这个过程看似简单,但实际很有帮助。

排查问题最怕的是把“可能原因”直接当成“最终结论”。
ChatGPT 5.5 在这里的作用,是帮我们先把信息分层:

  • 哪些是日志已经证明的;
  • 哪些只是合理猜测;
  • 哪些还需要继续补证据。

这一步做好之后,后续排查会清晰很多。


四、生成一条更靠谱的排查路径

接下来可以继续让模型基于已有信息生成排查步骤:

text

请基于上面的事实和可能原因,给出一个后端接口排查路径。
要求:1. 从最容易验证、最高优先级开始;2. 每一步说明要看什么;3. 每一步说明目的;4. 不超过 8 步;5. 覆盖 token、鉴权拦截器、ThreadLocal、并发复现、日志补充几个方向。

可能得到如下结果:

markdown

## 排查路径
### 1. 确认失败请求是否携带 token
- 看什么:网关日志、access log、请求 Header。- 目的:确认失败请求是否缺少 Authorization 信息。
### 2. 检查 token 解析是否成功
- 看什么:token 是否过期、格式是否正确、解析出的 userId 是否为空。- 目的:判断用户信息是否在鉴权阶段已经丢失。
### 3. 确认鉴权拦截器是否执行
- 看什么:Filter、Interceptor、网关鉴权日志。- 目的:确认该接口是否经过统一鉴权流程。
### 4. 检查 UserContext 的实现方式
- 看什么:是否使用 ThreadLocal 保存当前用户。- 目的:判断用户信息是否依赖当前请求线程。
### 5. 检查 ThreadLocal 设置和清理时机
- 看什么:是否在请求开始时 set,请求结束后 remove。- 目的:排查上下文未写入、提前清理或线程复用污染问题。
### 6. 检查是否存在异步调用
- 看什么:是否使用 @Async、线程池、CompletableFuture。- 目的:判断用户上下文是否在跨线程时丢失。
### 7. 并发压测复现
- 看什么:并发请求下 userInfo 为 null 的比例、失败请求特征。- 目的:确认问题是否与并发、线程复用或压测脚本有关。
### 8. 增加业务层防御和关键日志
- 看什么:userInfo 为空时返回明确错误码,同时记录 requestId。- 目的:避免 NPE 暴露为 500,并提升后续定位效率。

这类 checklist 很适合放到排查文档里,也方便团队成员同步进展。


五、代码层面先做防御,而不是直接 NPE

从代码质量角度看,下面这行确实有风险:

java

Long userId = userInfo.getUserId();

如果 userInfo 为空,接口会直接抛 NPE,并返回 500。
更合理的做法是把“未登录”或“登录状态异常”和“系统异常”区分开。

可以改成:

java

public String createOrder(CreateOrderRequest request) {    UserInfo userInfo = userContext.getCurrentUser();    if (userInfo == null || userInfo.getUserId() == null) {        throw new UnauthorizedException("用户未登录或登录状态已失效");    }
    if (request == null || request.getProductId() == null) {        throw new BizException("商品参数不能为空");    }
    Long userId = userInfo.getUserId();
    Product product = productMapper.selectById(request.getProductId());    if (product == null) {        throw new BizException("商品不存在");    }
    if (product.getStock() <= 0) {        throw new BizException("库存不足");    }
    Order order = new Order();    order.setUserId(userId);    order.setProductId(product.getId());    order.setAmount(product.getPrice());
    orderMapper.insert(order);
    productMapper.decreaseStock(product.getId());
    return order.getOrderNo();}

再配合全局异常处理:

java

@RestControllerAdvicepublic class GlobalExceptionHandler {
    @ExceptionHandler(UnauthorizedException.class)    public Result<Void> handleUnauthorized(UnauthorizedException e) {        return Result.fail(401, e.getMessage());    }
    @ExceptionHandler(BizException.class)    public Result<Void> handleBizException(BizException e) {        return Result.fail(400, e.getMessage());    }
    @ExceptionHandler(Exception.class)    public Result<Void> handleException(Exception e) {        // 实际项目中应记录完整异常堆栈        return Result.fail(500, "系统异常,请稍后重试");    }}

这样改完以后,至少能保证:

  • 未登录问题返回 401;
  • 业务参数问题返回 400;
  • 真正未知异常才返回 500;
  • 前端和后端都能更清楚地区分问题类型。

不过要注意,这只是防御性改造。
如果按业务逻辑该接口必须登录,那么 userInfo 为空仍然是一个需要继续追查的问题。


六、重点关注 ThreadLocal 用户上下文

很多 Java Web 项目会使用 ThreadLocal 保存当前用户信息,例如:

java

public class UserContext {
    private static final ThreadLocal<UserInfo> USER_HOLDER = new ThreadLocal<>();
    public static void set(UserInfo userInfo) {        USER_HOLDER.set(userInfo);    }
    public static UserInfo getCurrentUser() {        return USER_HOLDER.get();    }
    public static void clear() {        USER_HOLDER.remove();    }}

在拦截器中设置用户信息:

java

@Componentpublic class AuthInterceptor implements HandlerInterceptor {
    @Override    public boolean preHandle(HttpServletRequest request,                             HttpServletResponse response,                             Object handler) {        String token = request.getHeader("Authorization");
        UserInfo userInfo = tokenService.parseToken(token);        UserContext.set(userInfo);
        return true;    }
    @Override    public void afterCompletion(HttpServletRequest request,                                HttpServletResponse response,                                Object handler,                                Exception ex) {        UserContext.clear();    }}

这种写法比较常见,但需要注意几个问题。

1. 请求结束后必须 clear

Web 容器通常使用线程池处理请求。
如果 ThreadLocal 不及时清理,线程被复用后,可能出现上下文残留。

正确做法是在请求结束后执行:

java

UserContext.clear();

如果项目中有多个 Filter、Interceptor,还要确认异常情况下也能清理。

2. 异步线程拿不到原线程上下文

例如:

java

CompletableFuture.runAsync(() -> {    UserInfo userInfo = UserContext.getCurrentUser();    // 这里很可能拿到 null});

因为 ThreadLocal 默认只在当前线程有效,切换线程后不会自动传递。
如果业务确实需要异步使用用户信息,更推荐显式传参:

java

Long userId = UserContext.getCurrentUser().getUserId();
CompletableFuture.runAsync(() -> {    orderLogService.saveCreateLog(userId);});

不要在异步线程中继续依赖当前请求线程的 ThreadLocal。

3. 拦截器路径可能没有覆盖接口

需要检查拦截器配置:

java

@Overridepublic void addInterceptors(InterceptorRegistry registry) {    registry.addInterceptor(authInterceptor)            .addPathPatterns("/api/**")            .excludePathPatterns("/api/login", "/api/public/**");}

如果路径配置不准确,可能会导致某些接口绕过鉴权。

比如订单接口实际路径是:

text

/order/api/create

但拦截器只拦截:

tex

/api/**

就可能出现鉴权没有执行的情况。


七、让 ChatGPT 5.5 帮忙设计日志点

偶发问题最怕日志不够。
如果日志里只有一行 NPE,那么下次再出现问题,还是很难继续分析。

可以让 ChatGPT 5.5 帮你设计日志点:

text

针对订单创建接口偶发 userInfo 为空的问题,请帮我设计需要补充的关键日志。
要求:1. 不打印完整 token;2. 能关联一次请求;3. 覆盖网关、拦截器、业务层;4. 避免泄露敏感信息;5. 输出字段清单。

可以整理出如下日志方案。

1. 网关层日志

建议记录:

text

requestIdpathmethodclientIphasAuthorizationHeadertokenHashtargetService

注意不要打印完整 token。
如果要关联 token,可以只打印 hash 后的短标识。

2. 鉴权层日志

建议记录:

text

requestIdpathtokenExiststokenParseSuccessuserIdauthResult

这样可以确认:

  • token 是否存在;
  • token 是否解析成功;
  • 是否拿到了 userId;
  • 是否放行请求。

3. 业务入口日志

建议记录:

text

requestIduserInfoExistsuserIdproductId

当 userInfo 为空时,可以记录一条 warn 日志:

java

if (userInfo == null) {    log.warn("create order userInfo is null, requestId={}, productId={}",            RequestIdHolder.get(),            request.getProductId());    throw new UnauthorizedException("用户未登录或登录状态已失效");}

4. 异常日志

异常日志需要保留完整堆栈,同时带上 requestId:

java

log.error("create order failed, requestId={}, productId={}",        requestId,        request.getProductId(),        e);

这样后续就可以通过 requestId 串起网关、鉴权、业务层日志。


八、并发场景下还要检查库存扣减

虽然当前日志显示问题是 userInfo 为空,但订单创建接口通常还会有另一个高频问题:库存超卖。

原代码里是:

java

Product product = productMapper.selectById(request.getProductId());if (product.getStock() <= 0) {    throw new BizException("库存不足");}
orderMapper.insert(order);
productMapper.decreaseStock(product.getId());

如果多个请求同时进来,可能都读到 stock > 0,然后都去扣减库存。

更稳妥的方式是使用带条件的库存扣减:

sql

UPDATE productSET stock = stock - 1WHERE id = #{productId}  AND stock > 0

Java 代码中判断影响行数:

java

int rows = productMapper.decreaseStockIfEnough(product.getId());if (rows == 0) {    throw new BizException("库存不足");}

这样可以避免并发下库存被扣成负数。

为什么这里也要提库存?
因为接口偶发 500、并发下异常这类问题,经常不止一个原因。排查 userInfo 的同时,也应该顺手检查核心业务链路中的并发安全点。


九、让 ChatGPT 5.5 生成测试用例清单

除了排查路径,大模型还适合帮忙补测试用例。

可以输入:

text

请根据订单创建接口的逻辑,生成一份测试用例清单。
要求覆盖:1. 正常创建订单;2. 未登录;3. token 过期;4. 商品不存在;5. 库存不足;6. 并发扣减库存;7. userInfo 为空;8. productId 为空。

输出可以整理为:

用例 输入条件 预期结果
正常创建订单 token 有效,productId 有效,库存充足 返回订单号
未登录 不携带 token 返回 401
token 过期 携带过期 token 返回 401
商品不存在 productId 不存在 返回 400
库存不足 商品库存为 0 返回 400
并发扣减库存 多线程同时下单 不出现超卖
userInfo 为空 模拟上下文未设置 返回 401,不抛 NPE
productId 为空 请求参数缺失 返回 400

这类内容手写也不难,但模型可以更快生成第一版,然后开发者再根据项目实际情况补充。


十、一个比较实用的 Prompt 模板

如果想用 ChatGPT 5.5 辅助排查后端接口问题,可以参考下面这个模板:

text

你是一名有经验的 Java 后端问题排查助手。请根据我提供的信息,帮我分析接口异常。
【接口背景】接口名称:接口路径:主要逻辑:
【异常现象】1.2.3.
【异常日志】粘贴日志
【相关代码】粘贴核心代码
【补充信息】是否偶发:是否与并发有关:是否本地可复现:最近是否改动过:
【输出要求】1. 先整理已确认事实;2. 再列出可能原因;3. 区分事实和推测;4. 给出排查优先级;5. 每一步说明要看什么、为什么看;6. 不要直接下最终结论;7. 如信息不足,请列出还需要补充的数据。

这个模板的关键点是:不要让模型直接“猜原因”,而是让它参与“整理和拆解问题”。


十一、使用大模型排查问题时的注意事项

1. 不要把模型结论当最终结论

模型能根据上下文给出合理猜测,但线上问题必须靠证据闭环。

比如它说“可能是 ThreadLocal 丢失”,你还需要验证:

  • 是否真的使用 ThreadLocal;
  • 是否存在异步线程;
  • 是否在失败请求中没有设置上下文;
  • 是否有日志证明这一点。

2. 不要粘贴敏感信息

排查接口问题时,不建议直接粘贴:

  • 完整 token;
  • 用户手机号;
  • 身份证号;
  • 数据库密码;
  • 内部密钥;
  • 真实生产用户数据。

必要时应先脱敏。

3. 给模型的信息越结构化,输出越有用

相比这样问:

text

接口报错了,帮我看看。

更推荐这样问:

text

这是异常日志,这是代码,这是复现条件,这是我已经排查过的内容,请帮我整理下一步排查路径。

信息越完整,模型越不容易泛泛而谈。

4. 让模型输出 checklist,而不是直接输出结论

对于复杂问题,最有用的不是一句“根因是 XXX”,而是一份可以逐项验证的 checklist。


总结

ChatGPT 5.5 用在后端开发中,比较适合处理这类事情:

  • 整理异常日志;
  • 分析堆栈信息;
  • 区分事实和推测;
  • 生成排查路径;
  • 补充日志点;
  • 设计测试用例;
  • 整理复盘文档。

但它不适合直接替代开发者做最终判断。

以本文的订单创建接口为例,日志表面上是 userInfo 为空导致 NPE,但真正排查时需要继续确认 token、鉴权拦截器、ThreadLocal、异步线程、并发压测和日志链路等多个方向。

合理的使用方式是:让 ChatGPT 5.5 帮你把混乱信息整理成结构化排查清单,然后由开发者结合真实日志、监控和复现实验逐步验证。这样既能提高排查效率,也能避免因为过早下结论而走偏。

更多推荐