项目地址 :gitee.com/guoxuezha/e...
1. 单服务诊断的局限性
前面的博客聚焦于"单服务"的日志诊断:给定一个服务编码和发生时间,抓取该服务的日志并分析。但在真实的微服务架构中,一个用户请求往往会穿越多个服务:
scss
用户请求 → Gateway → Doctor-Service → (Feign) Order-Service → (MQ) Nurse-Service
如果异常发生在 Order-Service,但用户只看到了 Doctor-Service 的超时错误,仅分析入口服务的日志只能看到"表象"(超时),看不到"根因"(Order-Service 的 SQL 慢查询)。
全链路诊断 Agent(FullChainDiagnosisAgent) 的目标就是:自动追踪请求在各个服务间的传递过程,把所有相关服务的日志串联起来,让 AI 进行综合分析。
2. 核心思路:request_id 是钥匙
我们的日志格式中,每行日志都携带 request_id(通过 MDC 注入):
ini
[abc-123-def] [admin] 2026-07-07 10:30:15.123 [http-nio-8080-exec-1] ERROR ...
同一个 HTTP 请求,在入口服务和下游服务中,共享同一个 request_id。这意味着:
- 从入口服务的错误日志中提取
request_id。 - 以
request_id为关键字,去下游服务的 INFO 级别日志中搜索。 - 将所有匹配结果按服务分组,就能还原完整的请求链路。
3. FullChainDiagnosisAgent 的 Action 编排
全链路 Agent 包含 7 个 Action,形成一个清晰的线性 + 分支流程:
scss
parseRequest
│ [hasParsedInput]
▼
fetchEntryServiceLog
│ [entryLogFetched]
▼
assessEntryLogQuality
├── [chainQualityPass] ──→ queryDownstreamServices
│ ├── [hasDownstreamServices] ──→ extractRequestIds
│ │ ├── [hasValidRequestIds] ──→ fetchDownstreamLogs
│ │ │ │ [downstreamLogsFetched]
│ │ │ ▼
│ │ │ generateChainReport (全链路报告)
│ │ │
│ │ └── [noValidRequestIds]
│ │ ▼
│ │ generateNoRequestIdReport (降级报告)
│ │
│ └── [noDownstreamServices]
│ ▼
│ generateSingleServiceReport (单服务报告)
│
└── [chainQualityFail]
▼
generateFailReport (失败报告)
可以看到,Agent 在不同条件下有 4 种终点:
- 全链路报告(COMPLETE):成功追踪到下游服务日志。
- 单服务报告(COMPLETE):没有下游服务,仅分析入口服务。
- 降级报告(PARTIAL):有下游服务但未能提取 request_id。
- 失败报告(FAILED):入口服务日志质量不达标。
这种"渐进降级"的设计,确保系统在任何情况下都能给出尽可能有价值的诊断结果。
4. 关键 Action 详解
4.1 请求解析
java
@Action(
description = "处理用户输入,解析为全链路诊断请求参数",
post = {"hasParsedInput"}
)
public FullChainDiagnosisInput parseRequest(UserInput userInput, OperationContext ctx) {
FullChainDiagnosisInput input = ctx.ai()
.withDefaultLlm()
.createObject(PromptBuilder.buildFullChainInputPrompt(userInput), FullChainDiagnosisInput.class);
input.setParsed(true);
return input;
}
与单服务 Agent 一样,用户输入通过 LLM 解析为结构化对象。全链路版本需要额外的必填字段:url(接口路径)和 problemDescription(问题描述)。
4.2 入口服务日志抓取
java
@Action(
description = "通过 SSH 远程抓取入口服务日志",
pre = {"hasParsedInput"},
post = {"entryLogFetched"},
canRerun = true
)
public SshLogResult fetchEntryServiceLog(FullChainDiagnosisInput input, OperationContext ctx) {
List<ServiceResource> resources = knowledgeBaseService.findServiceResources(serviceCode);
SshLogResult sshResult = sshService.firstLogCatch(resources, occurredTime);
// ...
}
这里只抓取 error 级别日志,因为入口服务的目标是定位异常并提取 request_id。
4.3 质量评估
全链路版本的质量评估标准与单服务版本略有不同:评分 ≥ 6 分即视为达标 (而非单服务版本的 8 分)。这是因为全链路诊断的入口日志主要用于提取 request_id,对日志完整性的要求相对宽松。
4.4 下游服务查询
java
@Action(
description = "根据 serviceCode 和 url 查询异常请求的下游服务信息",
pre = {"chainQualityPass"},
post = {"hasDownstreamServices", "noDownstreamServices"}
)
public DownstreamServiceInfo queryDownstreamServices(FullChainDiagnosisInput input) {
HashSet<String> downstream = knowledgeBaseService.getDownStreamService(
input.getServiceCode(), input.getUrl());
return DownstreamServiceInfo.builder()
.entryServiceCode(input.getServiceCode())
.url(input.getUrl())
.downstreamServiceCodes(downstream)
.build();
}
下游服务查询复用了知识库的 getDownStreamService 方法,它同时考虑了 Feign 调用 和 MQ 消费者 两种关联关系:
java
public HashSet<String> getDownStreamService(String serviceCode, String url) {
HashSet<String> relations = new HashSet<>();
ApiRelationInfo relationInfo = apiRelationMap.get(serviceCode).get(url);
// Feign 调用:直接获取目标服务编码
if (relationInfo.getHasFeignCall()) {
relationInfo.getFeignCalls().forEach(f -> relations.add(f.getServiceCode()));
}
// MQ 消费者:通过 topic#tag 查找消费方
if (relationInfo.getHasMqSend()) {
for (MqSendInfo mqSend : relationInfo.getMqSends()) {
String topicTag = mqSend.getTopic() + "#" + mqSend.getTag();
relations.addAll(topicConsumerMap.getOrDefault(topicTag, List.of()));
}
}
return relations;
}
4.5 request_id 提取
这是全链路追踪最关键的一步。我们采用了双重提取策略:
java
@Action(
description = "根据日志行格式识别关键日志的 request_id",
pre = {"hasDownstreamServices"},
post = {"hasValidRequestIds", "noValidRequestIds"}
)
public RequestIdExtractOutput extractRequestIds(SshLogResult logResult,
FullChainDiagnosisInput input,
OperationContext ctx) {
// 方式一: 正则提取(快,精准)
List<String> requestIds = extractRequestIdsFromLog(logContent);
// 方式二: LLM 辅助提取(正则失败时的备选方案)
if (CollUtil.isEmpty(requestIds)) {
requestIds = extractRequestIdsByLlm(logContent, ctx);
}
return RequestIdExtractOutput.builder()
.requestIds(requestIds)
.serviceCode(input.getServiceCode())
.build();
}
正则提取:匹配每行日志第一个方括号内的内容:
java
private static final Pattern REQUEST_ID_PATTERN = Pattern.compile("^\[([^\]]+)]");
private List<String> extractRequestIdsFromLog(String logContent) {
Set<String> requestIds = new LinkedHashSet<>();
for (String line : logContent.split("\n")) {
Matcher matcher = REQUEST_ID_PATTERN.matcher(line.trim());
if (matcher.find()) {
String requestId = matcher.group(1).trim();
if (!"EMPTY".equalsIgnoreCase(requestId) && StrUtil.isNotEmpty(requestId)) {
requestIds.add(requestId);
}
}
}
return new ArrayList<>(requestIds);
}
LLM 辅助提取:当日志格式非标准或 request_id 位置不规则时,让 LLM 理解日志结构并提取。这种"规则优先 + AI 兜底"的策略,在准确性和鲁棒性之间取得了良好的平衡。
4.6 下游服务日志抓取
拿到 request_id 后,逐一访问每个下游服务,以 request_id 为关键字抓取 INFO 级别以上的日志:
java
@Action(
description = "以 request_id 为关键字,获取下游服务完整的请求链路日志",
pre = {"hasValidRequestIds"},
post = {"downstreamLogsFetched"}
)
public FullChainLogResult fetchDownstreamLogs(RequestIdExtractOutput requestIdOutput,
DownstreamServiceInfo downstreamInfo,
SshLogResult entryLogResult,
FullChainDiagnosisInput input,
OperationContext ctx) {
List<ServiceLogGroup> allGroups = new ArrayList<>();
// 首先加入入口服务日志
allGroups.add(ServiceLogGroup.builder()
.serviceCode(input.getServiceCode())
.logLevel("error")
.logContent(entryLogResult.getOutput())
.build());
// 选取第一个有效的 request_id
String requestId = requestIdOutput.getRequestIds().getFirst();
// 逐一抓取下游服务日志
for (String downstreamCode : downstreamInfo.getDownstreamServiceCodes()) {
List<ServiceResource> resources = knowledgeBaseService.findServiceResources(downstreamCode);
// 用 request_id 作为关键字抓取 INFO 级别以上的日志
SshLogResult infoResult = sshService.logSectionCatch(
resources, input.getOccurredTime(), requestId, null, null, null);
allGroups.add(ServiceLogGroup.builder()
.serviceCode(downstreamCode)
.logLevel("info")
.logContent(infoResult.getOutput())
.build());
}
return FullChainLogResult.builder()
.serviceLogGroups(allGroups)
.entryServiceCode(input.getServiceCode())
.requestId(requestId)
.build();
}
5. 全链路诊断报告
最终的报告包含所有服务的日志和 LLM 的综合分析:
java
@AchievesGoal(description = "完成全链路日志整合分析并生成诊断报告")
@Action(
description = "基于全链路日志生成综合诊断报告",
pre = {"downstreamLogsFetched"}
)
public FullChainDiagnosisReport generateChainReport(FullChainLogResult chainResult,
FullChainDiagnosisInput input,
DownstreamServiceInfo downstreamInfo,
OperationContext ctx) {
// 构建全链路日志文本(按服务分组展示)
String chainLogsText = buildChainLogsText(chainResult.getServiceLogGroups());
// 调用 LLM 进行综合分析
String analysis = ctx.ai()
.withDefaultLlm()
.generateText(PromptBuilder.buildFullChainAnalysisPrompt(
input.getProblemDescription(),
input.getServiceCode(),
chainResult.getEntryLogContent(),
chainLogsText
));
return FullChainDiagnosisReport.builder()
.entryServiceCode(input.getServiceCode())
.serviceLogGroups(chainResult.getServiceLogGroups())
.requestId(chainResult.getRequestId())
.analysisConclusion(analysis)
.diagnosisStatus("COMPLETE")
.build();
}
6. 全链路日志的呈现格式
buildChainLogsText 方法将各服务的日志按清晰的格式组织:
ini
========== [his-doctor-service] ==========
日志级别: error
来源主机: 192.168.0.52
日志文件: doctor-error.log
日志内容:
[abc-123] [admin] 2026-07-07 10:30:15.123 ... ERROR ... 查询超时
========== [his-order-service] ==========
日志级别: info
来源主机: 192.168.0.53
日志文件: order-info.log
日志内容:
[abc-123] [admin] 2026-07-07 10:30:14.890 ... INFO ... 开始处理订单查询
[abc-123] [admin] 2026-07-07 10:30:14.950 ... INFO ... 执行SQL: SELECT ...
[abc-123] [admin] 2026-07-07 10:30:15.100 ... WARN ... SQL执行耗时210ms
LLM 基于这份全链路日志,能够清晰地还原事件时间线,并精确定位问题发生在哪个服务的哪个环节。
7. LlmTool:供 LLM 自主调用的诊断工具集
除了 Agent 的 Action 编排外,我们还提供了一组 @LlmTool 注解的工具方法,让 LLM 在推理过程中可以自主调用:
| 工具名 | 功能 | 典型使用场景 |
|---|---|---|
search-log-by-time |
按时间搜索日志 | 首次抓取目标服务日志 |
search-log-by-keyword |
按关键字搜索日志 | 用 request_id 追踪链路 |
search-log-from-file |
从指定文件抓取 | 精确控制抓取的文件和范围 |
query-feign-calls |
查询 Feign 调用关系 | 确定下游服务 |
query-mq-consumers |
查询 MQ 消费者 | 确定消息消费方 |
query-downStream-service |
查询下游服务 | 一站式获取所有下游 |
lookup-service-resource |
查询服务资源 | 获取主机、端口等信息 |
这些工具通过 @LlmTool 注解注册,LLM 可以在 createObject 推理过程中自主决定调用哪些工具、传什么参数。这赋予了 Agent 极大的灵活性------LLM 不再只是"被动接收输入并输出结果",而是可以"主动探索"。
8. 渐进降级:四种终点的价值
全链路 Agent 的四种终点不是"失败",而是"不同程度的成功":
场景一:完美诊断
"Doctor-Service 的
/consult/query接口超时。追踪 request_id=abc-123 发现:Order-Service 在处理订单查询时执行了一条全表扫描 SQL,耗时 3.2 秒。建议在order表的patient_id字段上添加索引。"
场景二:无下游服务
"Doctor-Service 的
/doctor/list接口报 NullPointerException。该接口没有调用任何下游服务。日志显示DoctorDTO.getDepartment()返回 null,建议添加空值检查。"
场景三:request_id 提取失败(降级)
"Doctor-Service 的日志中未找到有效的 request_id(可能日志格式不标准)。仅分析了入口服务的日志,初步判断为数据库连接池耗尽。建议进一步人工排查 Order-Service。"
场景四:入口日志质量不达标(失败)
"Doctor-Service 的日志内容为空或与描述的问题无关(评分 2/10)。可能原因:发生时间不准确、服务编码错误、或日志已被轮转清理。建议确认参数后重试。"
即使是最差的情况,系统也给出了有价值的反馈(诊断过程摘要 + 失败原因 + 下一步建议),而不是简单地报一个"500 错误"。
9. 实际诊断案例
案例:某医院 HIS 系统报告"医生工作站查询患者信息偶发超时"。
- 用户输入:
serviceCode=his-doctor-service, occurredTime=2026-07-07 10:30:15, url=/patient/query, problemDescription=查询患者信息超时 - Agent 抓取入口服务 error.log,评分 8/10(发现超时异常堆栈)。
- 查询知识库,发现
/patient/query通过 Feign 调用了his-patient-service。 - 从日志中提取 request_id=
req-7f8a9b。 - 以
req-7f8a9b为关键字搜索his-patient-service的 INFO 日志。 - LLM 综合分析发现:
his-patient-service在 10:30:14 执行了一条SELECT * FROM patient_info WHERE ...查询,耗时 4.5 秒(正常应 < 100ms)。 - 报告建议:检查
patient_info表索引是否缺失,并排查是否存在锁等待。
整个诊断过程约 25 秒,而人工排查通常需要 15-30 分钟。
10. 小结
全链路追踪 Agent 通过 request_id 这一"线索",将分散在多个微服务中的日志串联起来,让 AI 能够从全局视角分析问题。结合知识库的服务关联信息(Feign + MQ),系统能自动发现下游依赖,无需用户手动指定。渐进降级机制确保了在任何场景下都能给出有价值的诊断结果。