AI 辅助调试——定位生产 Bug 的新姿势
AI 辅助调试——定位生产 Bug 的新姿势
适读人群:Java工程师、后端开发者 | 阅读时长:约13分钟 | 核心价值:AI辅助定位生产Bug的完整工作流,不是"把报错粘给AI"
凌晨两点,手机报警响了。
生产环境某个核心接口的错误率突然从 0.1% 涨到 8%,告警触发。我爬起来,开始排查。
以前这种情况我会先查日志,然后打开 IDE 看代码,对着堆栈一行一行看,在脑子里构建调用链,通常一两个小时能找到问题。
那次我换了个方式——把 AI 拉进来。从发现问题到定位根因,花了 27 分钟。
这篇文章写的就是那套工作流,以及我后来把它系统化的过程。
声明:不是把报错粘给 AI
先把这件事说清楚。
"AI 辅助调试"不是打开对话框,把异常堆栈粘进去,问"这是什么问题"。那个用法只能处理最简单的场景,遇到稍微复杂点的生产 Bug 基本没用。
我说的是一套有节奏的工作流:日志分析 → 假设生成 → 假设验证 → 修复方案对比。AI 在每个节点都有明确的任务,而不是一个全知全能的黑盒。
真实案例(已脱敏)
背景
一个订单查询接口,正常情况下 P99 延迟在 200ms 以内,错误率 < 0.2%。
凌晨两点,错误率涨到 8%,主要错误类型是 DataAccessException,不是全量报错,而是某些特定的请求在报错。
第一步:日志分析
不是把所有日志丢给 AI,而是做一个有结构的预处理:
# 日志分析辅助脚本
import re
from collections import Counter
from datetime import datetime
def parse_error_logs(log_file: str, time_window_minutes: int = 30) -> dict:
"""
从日志文件中提取错误模式
返回结构化的分析结果
"""
errors = []
pattern = re.compile(
r'(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})'
r'.*?ERROR.*?'
r'(?P<exception>[A-Za-z.]+Exception[^\n]*)'
r'(?P<trace>(?:\n\tat [^\n]+)*)',
re.DOTALL
)
with open(log_file) as f:
content = f.read()
for match in pattern.finditer(content):
errors.append({
"timestamp": match.group("timestamp"),
"exception": match.group("exception").strip(),
"trace_lines": [
line.strip()
for line in match.group("trace").strip().split("\n")
if line.strip() and "at " in line
][:5] # 只取前5行堆栈
})
# 统计错误分布
exception_counter = Counter(e["exception"][:100] for e in errors)
# 找出最常见的堆栈模式
trace_patterns = Counter(
tuple(e["trace_lines"][:3]) for e in errors if e["trace_lines"]
)
return {
"total_errors": len(errors),
"exception_distribution": dict(exception_counter.most_common(5)),
"top_stack_patterns": [
{"pattern": list(k), "count": v}
for k, v in trace_patterns.most_common(3)
],
"time_distribution": _time_distribution(errors)
}
def _time_distribution(errors: list) -> dict:
"""错误在时间上的分布"""
if not errors:
return {}
minutes = Counter(e["timestamp"][:16] for e in errors)
return dict(minutes.most_common(10))运行这个脚本,得到结构化的分析结果,然后把这个结果给 AI 分析,而不是原始日志。
第二步:给 AI 的 Prompt 结构
我在排查一个生产环境的异常,以下是日志分析结果:
【错误概要】
- 时间范围:2:10 - 2:30(UTC+8)
- 总计错误:147条
- 主要异常:DataAccessException: could not execute statement; SQL [n/a] (占92%)
- 错误触发时间:集中在 02:11-02:18,之后逐渐减少
【最频繁的堆栈模式(出现83次)】
at org.hibernate.exception.SQLExceptionHelper.convert(SQLExceptionHelper.java:126)
at com.example.order.repository.OrderRepository.findByUserId(OrderRepository.java:45)
at com.example.order.service.OrderQueryService.getOrdersByUser(OrderQueryService.java:78)
at com.example.order.controller.OrderController.queryOrders(OrderController.java:32)
【相关代码片段】
OrderRepository.java:45:
return jdbcTemplate.query(
"SELECT * FROM orders WHERE user_id = ? AND status IN (?)",
new Object[]{userId, statusList}, // statusList 是 List<String>
orderRowMapper
);
【业务背景】
- 这是一个订单查询接口,根据用户ID和状态列表查询订单
- 错误期间没有发布新版本
- 错误只发生在部分请求(约8%),不是全部
请分析:
1. 最可能的根因是什么(列出前3个假设,按可能性排序)
2. 每个假设对应的验证方法
3. 有没有需要补充的日志或数据才能确认AI 的回答(核心部分):
基于日志分析,列出三个假设:
假设1(最可能,约65%置信度):IN 子句参数列表为空或包含 null
- DataAccessException 在 IN 查询时很常见的触发场景是:
statusList 为空列表,SQL 变成 `WHERE status IN ()`,这在 MySQL 里是语法错误
- 另一种情况:statusList 里有 null 值,导致 IN 查询返回非预期结果或报错
- 只影响部分请求(statusList 为空的那些)符合你描述的8%错误率
验证方法:
a. 在 OrderRepository.java:45 前面加日志,打印 statusList 的内容和大小
b. 搜索调用 getOrdersByUser 的地方,看 statusList 是否可能为空
c. 在日志里搜索同一时间段里 statusList 相关的请求参数
假设2(约25%置信度):数据库连接池耗尽导致超时
- 集中在8分钟内的错误模式有时候是连接池问题
- 但这通常是全量报错,不会只有8%,优先级降低
验证方法:检查 HikariCP(或你用的连接池)的监控,看那段时间连接等待队列情况
假设3(约10%置信度):MySQL 版本或配置变更
- 如果运维在那段时间做了什么 MySQL 参数调整,某些查询行为会变化
验证方法:和运维确认那段时间有无数据库操作
建议先从假设1入手,验证成本最低,可能性也最大。第三步:验证假设
根据 AI 的建议,我去看了调用方:
// OrderController.java
@GetMapping("/orders")
public ResponseEntity<List<Order>> queryOrders(
@RequestParam String userId,
@RequestParam(required = false) List<String> statuses) {
// 这里!statuses 可以是 null(required=false)
return ResponseEntity.ok(orderQueryService.getOrdersByUser(userId, statuses));
}
// OrderQueryService.java
public List<Order> getOrdersByUser(String userId, List<String> statuses) {
// 这里没有对 null 做处理,直接传下去了
return orderRepository.findByUserId(userId, statuses);
}问题找到了。当 statuses 参数不传时,值是 null,传到 JDBC 查询里,IN 子句处理 null 列表时 MySQL 报错。
那个 8% 的错误率,正好对应了不传 statuses 参数的请求比例。
第四步:修复方案对比
找到根因之后,我让 AI 给出几个修复方案,然后评估取舍:
根因确认:statuses 参数为 null 时传入 IN 查询导致 DataAccessException。
请给出3种修复方案,并分析每种方案的优缺点(考虑:代码侵入性、是否有潜在副作用、长期可维护性)。AI 的回答:
方案1:在 Repository 层加 null 检查
优点:修改最小,只改一处
缺点:把业务决策(null statuses 意味着查全部?还是返回空?)放在了数据访问层,职责不清
方案2:在 Service 层处理 null
public List<Order> getOrdersByUser(String userId, List<String> statuses) {
if (statuses == null || statuses.isEmpty()) {
return orderRepository.findAllByUserId(userId); // 不加 status 过滤
}
return orderRepository.findByUserIdAndStatuses(userId, statuses);
}
优点:业务逻辑集中在 Service,职责清晰
缺点:需要在 Repository 加一个新方法 findAllByUserId
方案3:在 Controller 层设置默认值
@RequestParam(required = false) List<String> statuses
改为
@RequestParam(required = false, defaultValue = "PENDING,PAID,SHIPPED") List<String> statuses
优点:接口行为明确,有默认语义
缺点:把业务默认值硬编码在 Controller,如果需要变更影响面大;
而且"不传 statuses = 查所有状态"可能是正确的业务语义,被这个默认值覆盖了
综合建议:方案2,业务语义最明确,修改范围可控。我选了方案2,凌晨2点37分推了热修复,错误率回到正常水平。
工作流总结
1. 日志预处理(用脚本)
- 提取错误分布、时间分布、堆栈模式
- 目的:给 AI 结构化数据,不是原始日志
2. 假设生成(AI + 代码上下文)
- 给出错误摘要 + 相关代码 + 业务背景
- 让 AI 给出有置信度排序的假设
- 让 AI 告诉你怎么验证每个假设
3. 假设验证(自己做)
- 按 AI 建议的验证方法,自己动手确认
- 这一步 AI 帮不上太多,你需要实际看代码和数据
4. 修复方案评估(AI + 自己判断)
- 让 AI 列举方案和取舍
- 你来做最终决策(AI 不了解你团队的约定和上下文)每个节点 AI 的作用不同。假设生成 AI 帮我省了最多时间——它能在几秒内列出几个有价值的方向,而我一个人摸索可能要 20 分钟才能想到同样的假设。
一个额外的技巧:让 AI 帮你写调试用的日志
找到了根因方向但还不确定时,可以让 AI 帮你写临时调试日志。比如:
在这个方法里加入调试日志,帮助确认 statuses 参数在运行时的实际值。
要求:
1. 日志级别用 DEBUG(不影响生产)
2. 加上请求 ID(MDC 里有 requestId)
3. 脱敏:userId 只打印后4位AI 生成的代码可以直接用,而且不会忘记加脱敏。
什么时候 AI 帮不上忙
- 你没有足够的信息:如果日志太少,或者日志格式混乱,AI 能做的推理也很有限。这时候要先收集更多信息,再来问 AI。
- 问题和运行时状态强相关:比如内存泄漏,必须用 Heap Dump、GC 日志、Profiler,这些是工具的活,AI 只能辅助分析结果,不能替代工具。
- 涉及你特有的内部协议或框架:如果你们有高度定制的内部框架,AI 对它的理解可能完全错误,这时候要提供更多上下文,或者换用熟悉这个框架的人来分析。
