1. AI 系统为什么更难调试
传统应用的调试线索通常比较明确。因为往往是单个请求节点发生故障,所以很容易找到问题所在。这类问题往往:较容易复现、能够追溯、便于验证。当然,有的场景下,我们也会遇到竞态、网络抖动等不稳定因素,但只要拿到错误堆栈、请求参数和依赖状态,通常就能沿着调用链缩小范围
AI Agent 的情况就不同。假设我们收到一条用户反馈:“AI 回复不对,我明明告诉过它我养了一只猫,它却说我没有养宠物。”
表面看是回复内容错了,问题却可能出在整条处理管线的任何一环:
- 记忆写入失败:用户说了“我养了一只猫”,但异步后处理没有把这条事实真正写入数据库
- 记忆召回失败:事实已经写入了,但向量检索的时候没有把它召回来
- 查询理解错误:查询计划解析偏了,导致检索方向不对,根本没有搜索“宠物”相关的记忆
- Prompt 组装遗漏:记忆检索到了,但在拼 Prompt 时因为 token 预算不足被截断了
- 情绪路由偏差:当前使用的回复模板更强调情绪表达,弱化了事实引用
- LLM 幻觉:上下文里明明有“养了一只猫”,但模型生成回复时还是忽略了这个事实
同一个“回复不对”,背后可能对应 6 种完全不同的根因,而且它们分布在处理管线的不同阶段。只看最终回复,很难判断问题究竟发生在哪里。
LLM 的输出还会受到采样参数、模型版本、供应商实现和上下文细节影响,同样的输入再次执行,得到的结果可能并不相同。即使降低 temperature,也不能把整个远程模型调用视为严格确定的函数。用同一条消息重新测试时,AI 这次答对了,并不代表上次答错的原因已经消失。
因此,我们不能把定位问题完全寄托在事后复现上
Agent 系统难调试,并不只是因为代码更多,而是因为结果不稳定,问题也未必能够可靠复现。既然无法保证重新触发同一个结果,就需要在问题发生的那一刻,把关键的处理现场记录下来。
这正是可观测性要解决的问题:系统在运行过程中留下足够的诊断信息,让我们可以在事后尽可能还原当时发生了什么。这里的“还原”指查看当时的输入摘要、配置、调用顺序和输出结果,并不等于重新执行后一定得到相同答案。
2. 从日志到可观测性
提到可观测性,很容易先想到多写几条日志。console.log 式的日志当然有用,但仅靠它还不足以还原一条完整的 AI 请求,其核心原因在于:
我们无法从日志中,还原一次请求的完整过程
可观测性并不是简单地增加日志数量,而是让系统在运行时记录足够多的、结构化且能够相互关联的信息。
Logs、Metrics 和 Traces 是可观测性中最常用的三类信号。它们记录的对象不同,解决的问题也不同。
Logs(日志)记录离散事件。系统里每当发生一件值得关注的事情,就可以写入一条日志,例如用户发送消息、安全检查通过,或者 LLM 调用超时。日志既可以是自由文本,也可以是结构化记录,适合补充上下文和排查单点问题。把 traceId 和 spanId 写入日志后,还可以把日志与对应 Trace 关联起来。
Metrics(指标)用数值描述系统状态,通常按照时间序列存储。过去 5 分钟的平均响应时间、每小时错误率和当前活跃会话数,都属于指标。它们经过聚合后更容易观察趋势,也适合设置报警规则。
Traces(追踪)关注单次请求经过的调用链路。它会把安全检查、记忆检索、Prompt 组装和 LLM 调用等节点串联起来,并记录每一步的耗时和必要属性。输入与输出是否记录、记录到什么粒度,需要由隐私、成本和排障需求共同决定。
我们要额外学习的重点是 Traces
3. Trace
Trace 是可观测性中的核心概念。理解它之前,我们先区分 Trace 和 Span:
| 概念 | 含义 |
|---|---|
| Trace | 一次完整的调用链路,从消息进入系统开始,串联本轮请求涉及的各项操作 |
| Span | 请求中的一个操作,例如安全检查、记忆检索或 LLM 调用 |
一个 Trace 由多个 Span 组成。Span 之间可以平级,也可以形成父子关系,表示一个操作内部还包含更细的子操作。它们既可能顺序执行,也可能在时间上重叠;是否并行需要结合开始时间和持续时间判断,不能只看树形层级。
映射到 AI 管线中,一条用户消息从进入系统到生成回复,就是一个 Trace;安全检查、查询理解、记忆检索、Prompt 组装和 LLM 生成,则分别对应不同的 Span。
下面是一条经过简化的 Trace 展示,名称、耗时和结果均为示意数据。每一行都遵循 节点名称 [耗时] → 结果摘要 的结构,缩进则用来表示父子关系。例如,vector_search 和 structured_query 都是 memory_retrieval 的子操作。真实系统还会记录各 Span 的开始时间、属性、事件和状态。Span 的记录内容如下: