一、背景:为什么 Agent 项目更需要统一错误日志?

在传统 Java 后端项目里,一次请求通常都在同一个技术栈内完成:Controller 接收请求,Service 处理业务,DAO 操作数据库,最后返回结果。即使中间出了问题,也可以通过 traceId、日志关键字、异常栈快速定位。

但 Agent 项目不一样。

在实际业务中,Java 端往往负责业务编排、数据入库、风控同步、告警触发等生产链路;Agent 端则可能由 Python 实现,负责调用大模型、执行 Browser Agent、处理 RAG 检索、管理 memory、运行固定 Skill 等能力。

以风险信息监控场景为例,整体链路大致是:

Java 风控服务
 -> 调用 Python Agent 接口
 -> Python 侧执行固定 Skill 或 Browser Agent
 -> 浏览器自动化查询外部风险信息
 -> 返回结构化结果
 -> Java 解析、去重、入库、触发告警

这个链路功能上并不复杂,真正麻烦的是:一旦失败,到底是 Java 端的问题,还是 Agent 端的问题?是参数校验失败,还是浏览器初始化失败?是验证码失败,还是 RAG 检索失败?是 Agent 执行慢,还是 Java 入库慢?

如果没有统一的错误日志和可观测性设计,最后就会变成:

Java 说:我调用 Agent 超时了。
Python 说:我这边好像执行失败了。
浏览器工具说:某一步点不动了。
业务方说:为什么这条企业风险信息没同步成功?

每一层都有日志,但每一层都只能看到自己的一小段,整条链路串不起来。这也是 Agent 工程化从“能跑”走向“可生产”时必须解决的问题。


二、原来的问题:不是没有日志,而是日志没有统一语义

很多项目早期都会有日志,但这些日志往往只是“局部可读”,不具备生产排障能力。

原先链路主要有几个问题。

1. 异常返回不统一

Agent 端失败时,可能直接返回:

{
  "ok": false,
  "error": "browser run failed"
}

或者内部直接抛出 RuntimeError

这种方式的问题是:Java 调用方只能靠字符串猜错误原因。

比如:

captcha failed
validation failed
browser timeout
result empty

这些字符串对人来说能看懂,但对 Java 业务系统来说很难稳定处理。因为它不知道:

  • 这个错误发生在哪个阶段?

  • 是否可以重试?

  • 是用户输入问题,还是系统执行问题?

  • 是固定 Skill 失败,还是开放式 Agent 失败?

  • 是否应该告警,还是直接降级?

所以第一个核心问题不是“日志不够多”,而是“错误没有标准语义”。


2. Java 和 Agent 链路断裂

Java 端本来可能已经有 requestIdtraceId,但如果调用 Python Agent 时没有透传过去,那么两边日志就是断的。

Java 看到的是:

requestId=java-xxx 调用 Agent 失败

Python 看到的是:

requestId=python-yyy browser skill failed

这两个 ID 不一致,排查时只能靠时间、参数、企业名称去人工匹配,效率非常低。

在生产环境里,这种问题会被放大。尤其是高并发或者批量同步任务中,同一时间可能有大量企业风险查询请求,如果没有统一 requestId,失败链路基本很难快速定位。


3. memory、RAG 等非核心依赖会影响主流程

Agent 链路里经常会接入 memory、RAG、模型、浏览器工具等组件。

但并不是所有组件都应该和主链路强绑定。

比如风险信息查询的核心目标是:查询外部风险信息并返回结构化结果。memory 和 RAG 可以增强对话体验,但如果 memory 读取失败就直接打断整个查询链路,就会导致非核心依赖拖垮主业务。

所以 Agent 工程化里必须区分:

核心依赖:失败后当前任务无法继续
非核心依赖:失败后可以降级,不影响主流程

这也是后面 fail-open 设计的基础。


4. 缺少阶段耗时,无法判断慢在哪

原先可能只有总耗时:

agent elapsed = 12000ms

但 12 秒到底慢在哪?

可能是:

memoryLoadMs = 200ms
ragSearchMs = 600ms
browserInitMs = 4000ms
skillExecMs = 6500ms
memoryWriteMs = 300ms

也可能是 Java 端慢:

agentCallMs = 8000ms
parseMs = 100ms
dbPersistMs = 3500ms
totalMs = 11600ms

如果只看总耗时,就无法判断是模型慢、浏览器慢、RAG 慢,还是 Java 入库慢。没有阶段耗时,后续做 P95、P99 优化也没有依据。


三、核心思想:统一的不是“日志格式”,而是“错误语义 + 链路标识 + 阶段观测”

Java 端和 Agent 端统一错误日志,不应该只理解为大家都打印 JSON 日志,或者大家都带上一个 requestId。

更准确地说,应该统一四件事:

1. 统一错误模型:失败必须有 errorCode、errorStage、retryable
2. 统一链路标识:Java 和 Agent 使用同一个 requestId / traceId
3. 统一阶段耗时:能拆出 Agent 内部和 Java 业务侧耗时
4. 统一降级策略:区分核心失败和非核心依赖失败

这样一来,日志才不是散点,而是一条完整链路。


四、第一步:定义统一错误模型

Agent 端需要先定义一个标准错误对象,例如:

AgentExecutionError

核心字段包括:

errorCode    错误码,表示具体错误类型
errorStage   错误阶段,表示失败发生在哪一层
retryable    是否可重试
message      面向开发者的错误描述
details      扩展信息

返回给 Java 的失败结构可以统一成:

{
  "ok": false,
  "requestId": "req-20260702-001",
  "errorCode": "AGENT_BROWSER_EXEC_FAILED",
  "errorStage": "BROWSER_EXEC",
  "retryable": true,
  "message": "browser tool execution timeout",
  "details": {
    "toolName": "click",
    "pageUrl": "https://example.com",
    "elapsedMs": 10000
  }
}

这样 Java 端就不需要再解析字符串,而是可以直接根据字段处理。

例如:

errorCode = AGENT_CREDITCHINA_CAPTCHA_FAILED
说明是验证码失败,可以记录为外部站点挑战失败

errorCode = AGENT_RESULT_VALIDATION_FAILED
说明结果校验失败,不应该直接入库

errorStage = INPUT_VALIDATION
说明请求参数本身不合法,通常不应该重试

retryable = true
说明可以进入重试队列或等待下次调度

这一步的价值是:把“人肉看日志”变成“系统可识别的错误语义”。


五、第二步:Java 和 Agent 贯通 requestId / traceId

统一错误模型解决的是“失败是什么”,requestId 解决的是“这次失败属于哪一次请求”。

Java 端作为业务入口,应该负责生成或透传 requestId。

调用 Agent 时,在请求头里带上:

X-Request-Id: req-20260702-001
X-Trace-Id: trace-20260702-001

Python Agent 入口统一读取这两个字段,并在后续所有日志、响应、Skill 返回、工具执行日志中都带上。

链路变成:

Java request start
 requestId=req-001

Java call Python Agent
 requestId=req-001

Python request start
 requestId=req-001

Skill lifecycle start
 requestId=req-001

Browser tool start
 requestId=req-001

Browser tool failed
 requestId=req-001

Python response failed
 requestId=req-001

Java parse Agent response
 requestId=req-001

这样排查时只需要搜索一个 requestId,就能把 Java 业务日志、Python Agent 日志、Browser Tool 日志全部串起来。

这一步看似简单,但对跨语言系统非常关键。因为 Java 和 Python 是两个运行时、两套日志系统、两套异常栈,如果没有统一 requestId,就很难形成完整调用视角。


六、第三步:按阶段拆分日志,而不是只打印开始和结束

统一日志不代表所有地方都打印一堆内容,而是要按链路关键阶段打点。

可以拆成四层。

1. HTTP 入口层

记录请求进入和返回:

agent.request.start
agent.request.finish
agent.request.fail

关键字段:

requestId
path
method
elapsedMs
ok
errorCode
errorStage

2. Skill 生命周期层

记录 Skill 执行阶段:

skill.lifecycle.start
skill.lifecycle.finish
skill.lifecycle.fail

关键字段:

requestId
skillName
inputValid
outputValid
elapsedMs
errorCode
errorStage

3. Browser Tool 层

Browser Agent 最容易失控,所以工具调用必须单独记录:

agent.tool.start
agent.tool.finish
agent.tool.fail

关键字段:

requestId
toolName
pageUrl
elapsedMs
resultSummary
failureReason

比如一次点击失败,可以记录:

{
  "event": "agent.tool.fail",
  "requestId": "req-001",
  "toolName": "click",
  "pageUrl": "https://example.com/search",
  "elapsedMs": 10000,
  "failureReason": "tool timeout"
}

这样就能明确知道:这次不是 Java 入库失败,也不是结果解析失败,而是 Browser Agent 某个工具步骤超时。

4. Java 业务消费层

Java 端也需要拆分耗时:

agentCallMs
parseMs
dbPersistMs
totalMs

比如:

{
  "requestId": "req-001",
  "agentCallMs": 8500,
  "parseMs": 120,
  "dbPersistMs": 300,
  "totalMs": 9000
}

如果 agentCallMs 很高,说明慢在 Agent;如果 dbPersistMs 很高,说明慢在 Java 数据库持久化;如果 parseMs 高,说明结构化结果可能过大或解析逻辑有问题。


七、第四步:区分核心失败和非核心依赖失败

Agent 链路里不是所有失败都应该中断主流程。

比如:

memory 读取失败
RAG 检索失败
memory 写回失败

这些在很多业务场景里都不是核心依赖。它们失败了,可以记录日志,但不一定要让主请求失败。

可以采用 fail-open 策略:

memory 读失败 -> 降级为空 memory,继续执行
RAG 检索失败 -> 降级为空检索结果,继续执行
memory 写失败 -> 记录 session error,不影响主结果返回

但像下面这些错误,就应该直接失败:

输入参数非法
浏览器初始化失败
固定 Skill 查询失败
结构化结果缺失
主体一致性校验失败

这种区分很重要。

因为生产系统追求的不是“任何组件失败都立刻报错”,而是“核心链路可靠,非核心能力可降级”。


八、第五步:固定 Skill 的错误要比开放式 Agent 更标准

开放式 Browser Agent 具有不确定性,可能会因为页面变化、工具调用、模型决策等因素失败。

但固定 Skill 不一样。固定 Skill 本质上是面向确定业务场景的生产能力,例如信用中国查询、企业风险查询等。因此固定 Skill 的错误应该更加标准化。

creditchina_query 为例,可以设计这些错误码:

AGENT_CREDITCHINA_INPUT_INVALID
AGENT_CREDITCHINA_CAPTCHA_FAILED
AGENT_RESULT_VALIDATION_FAILED
AGENT_BROWSER_EXEC_FAILED
AGENT_CREDITCHINA_QUERY_FAILED

这些错误码对应不同处理方式:

输入无效:Java 端不重试,直接记录参数问题
验证码失败:可重试,也可能触发外部站点异常告警
主体不一致:不能入库,必须拦截
浏览器执行失败:可根据 retryable 判断是否重试
查询失败:记录失败原因,等待下次调度

固定 Skill 的价值在于稳定、可控、可校验。所以固定 Skill 不应该只返回一句“查询失败”,而应该把失败原因明确暴露给 Java 业务端。


九、完整链路可以这样设计

最终优化后的链路可以抽象成:

Java / 前端请求
 -> Java 生成或透传 requestId
 -> Java 调用 Python Agent,并透传 X-Request-Id
 -> Python Flask 入口记录 request start
 -> 参数解析
 -> 判断进入 chat / direct skill / fixed skill
 -> 如果是 chat:
      -> memory 读取,失败则 fail-open
      -> RAG 检索,失败则 fail-open
      -> skill 分发
 -> 进入 SkillLifecycleRunner
      -> input schema 校验
      -> browser init
      -> runtime 构造
      -> 执行固定 Skill 或 browser_react
 -> browser runtime 执行
      -> tool step 日志
      -> tool budget / timeout 控制
      -> 固定 skill 结果校验
 -> output schema 校验
 -> memory 写回
 -> Python 返回标准响应
      -> ok
      -> requestId
      -> errorCode
      -> errorStage
      -> retryable
      -> result
 -> Java 消费端解析
      -> 识别标准错误
      -> 成功则入库、去重、触发事件
      -> 记录 agentCallMs / parseMs / dbPersistMs / totalMs

这条链路的关键不是多打印日志,而是让每个阶段都具备明确边界。


十、统一错误日志后的收益

统一 Java 端和 Agent 端错误日志之后,收益主要体现在四点。

1. 失败可分类

原来只知道“Agent 调用失败”,现在可以知道:

是输入失败
是 RAG 失败
是 memory 失败
是 browser init 失败
是 tool timeout
是结果校验失败
还是 Java 入库失败

2. 链路可追踪

通过同一个 requestId,可以从 Java 查到 Python,再查到 Browser Tool,最后定位到具体失败步骤。

3. 慢请求可定位

通过阶段耗时,可以判断慢在:

Java 调 Agent
Python memory
Python RAG
Browser 初始化
Skill 执行
Tool 调用
Java 解析
Java 入库

这为后续 P95、P99 优化提供了基础数据。

4. 系统更适合生产消费

Agent 不再是一个“黑盒能力”,而是变成 Java 业务系统可以理解、可以重试、可以降级、可以告警的生产组件。


十一、总结

Java 端和 Agent 端统一错误日志,本质上不是日志格式问题,而是跨语言、跨运行时、跨组件的工程化治理问题。

真正应该统一的是:

错误语义:errorCode、errorStage、retryable
链路标识:requestId、traceId
阶段耗时:Agent 内部耗时 + Java 业务耗时
降级策略:核心链路失败,非核心依赖 fail-open
执行边界:tool budget、tool timeout、健康检查

对于 Agent 项目来说,功能跑通只是第一步。真正进入生产环境后,更重要的是让系统在失败时可解释、在变慢时可定位、在依赖异常时可降级、在跨语言调用时可追踪。

一句话总结就是:

不要只把 Agent 当成一个接口调用,而要把它当成一条可观测、可治理、可兜底的生产链路来设计。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐