不可观测的系统等于黑盒;黑盒不能优化,也不能被信任。
Agent 可观测性与传统 Web 可观测性的根本区别
普通 Web 应用的可观测性相对简单:请求进来,处理,响应出去,记录请求日志。请求和响应之间的因果链是线性的、短暂的、确定的。一个 HTTP 请求从进入到返回,通常在几百毫秒内完成,中间经过的链路可以用一条 trace 清晰地串起来。
Agent 系统的可观测性面对的是一个完全不同的世界。
执行非线性
Web 服务的调用链是树状的:一个请求触发几个下游调用,每个下游调用可能再触发几个,形成一棵有限深度的调用树。Agent 的执行路径是图状的、甚至是环状的:AI 调用了一个工具,工具的结果让 AI 决定调用另一个工具,那个工具又可能触发子 Agent,子 Agent 内部又是一个完整的工具调用循环。更复杂的是,这些调用之间不一定有严格的先后顺序,多个工具可能并发执行,子 Agent 可能在后台运行。
传统的 trace 模型(每个 span 有唯一的父 span,形成树结构)可以勉强适配 Agent 的调用链,但会丢失很多语义信息。比如:AI 在第 3 轮调用了文件搜索工具,搜索结果影响了第 7 轮的代码修改决策。这两个 span 之间有因果关系,但在 trace 树里它们是平行的兄弟节点,看不出因果联系。
因果链长且隐蔽
在 Web 服务中,一个 bug 的原因通常可以在同一次请求的调用链中找到。在 Agent 系统中,一个错误的行为可能源于几轮之前的一个决策,而那个决策的依据可能是更早时候一次上下文压缩丢失了关键信息。
这意味着 Agent 的可观测性不只是记录发生了什么,还需要记录为什么。每次 LLM 调用需要知道自己是为什么目的发起的;每次工具调用需要知道 AI 为什么选择了这个工具而非另一个;每次上下文压缩需要记录压缩前后丢失了哪些信息。
归因困难
出了问题(Agent 做了意外的操作、任务没有完成),原因的排查空间比 Web 服务大得多:
- 是工具描述误导了模型?(工具层问题)
- 是上下文压缩丢失了关键信息?(上下文管理问题)
- 是权限规则阻止了正确的操作?(权限层问题)
- 是 Skill 的指令和当前任务不匹配?(Skill 层问题)
- 还是模型本身的推理出了错?(模型层问题)
在 Web 服务中,bug 通常集中在某一层(数据库查询错了、业务逻辑写错了)。在 Agent 系统中,问题可能跨越多个层级,而且各层之间的交互使得单独分析任何一层都可能得出错误的结论。
时间跨度大
一次 Web 请求通常在秒级完成。一次 Agent 任务可能持续几分钟甚至更长,中间经历了十几轮 LLM 调用、几十次工具执行、可能还有多个子 Agent 的完整生命周期。如果没有结构化的记录,事后几乎无法还原当时发生了什么。
OTel 三信号在 Agent 场景下的适配
OpenTelemetry 定义了三种可观测信号:Traces、Metrics、Logs。这三种信号在 Agent 场景下都需要适配,不能直接照搬 Web 服务的做法。
Traces:追踪
记录一次完整操作的调用链。Agent 场景下的 trace 结构比 Web 服务复杂得多:一次用户对话作为顶层 span,下面嵌套着 LLM 请求 span(含模型与 token 统计)、工具执行 span(含工具名与执行耗时)、压缩 span(含压缩级别与 token 变化)、子 Agent span(内部嵌套完整的 LLM 请求和工具执行 trace)。
一个重要的设计细节是等待用户确认的时间与系统执行时间的分离。如果不区分这两段时间,你会在日志里看到工具执行花了很长时间,但实际上其中大部分是在等用户点击确认按钮。分开记录才能准确分析系统性能。这个区分在权限系统中尤其重要:如果用户配置了需要确认的工具,确认等待时间不应该计入系统性能指标,否则会误导性能优化方向。
OpenTelemetry 在 Agent 场景下的适配挑战
OpenTelemetry 是为微服务架构设计的,Agent 系统使用 OTel 时会遇到几个不匹配的地方:
Span 的生命周期模型不匹配。OTel 的 span 假设操作有明确的开始和结束时间。但 Agent 循环中的 LLM 调用是流式的:第一个 token 返回的时间和最后一个 token 返回的时间可能差几秒甚至十几秒。是用第一个 token 的时间作为 span 结束,还是最后一个 token?两种选择对性能分析的含义完全不同。我们选择了最后一个 token,因为在此之前 AI 的决策(是否调用工具、调用哪个工具)尚未确定。
Trace 的上下文传播模型不匹配。在微服务中,trace context 通过 HTTP header 在服务间传递。在 Agent 系统中,子 Agent 和父 Agent 之间的关系是同一进程内的函数调用或 IPC 通信,并非 HTTP 调用。OTel 的上下文传播器(Propagator)需要自定义,才能正确地把父 Agent 的 trace ID 传递给子 Agent。
指标的基数(Cardinality)问题。传统的 OTel 指标用 HTTP 方法、路由、状态码作为维度,维度组合是有限的。Agent 的指标如果用工具名、Skill 名、模型名作为维度,当用户自定义的 Skill 和工具数量增长后,维度组合可能爆炸。这在使用 Prometheus 等指标存储时会导致内存问题。我们的做法是:只对内置工具和 Skill 做细粒度维度,用户自定义的统一归入通用类别。
采样策略。传统微服务可以对 trace 做比例采样(比如只记录少量请求),因为请求是大量同质的。Agent 的每次对话都是独特的,采样会导致某些重要的调试信息丢失。我们选择了全量记录元数据(时间、token 数、工具名),按需记录详细内容(prompt、response),兼顾存储成本和调试需求。
Metrics:指标
关键指标:
- LLM 请求总数 / prompt tokens 总量 / completion tokens 总量
- 工具调用次数(按工具分类)
- 压缩触发次数(按压缩级别分类)
- 会话持续时长分布
Logs:结构化日志
每个关键事件的结构化记录,按日期落盘,支持归档和分析。
调用来源分类的意义
每次 LLM 调用都需要知道它是为什么目的发起的。这个看似简单的字段,是 Agent 可观测性区别于传统 Web 可观测性最重要的设计之一。
为什么这个分类如此重要
没有调用来源字段时,你看到的日志是这样的:过去 24 小时,一共发生了大量 LLM 调用,消耗了大量 token。这个数字告诉你花了多少钱,但不告诉你钱花在哪里。
有了调用来源字段,你能回答真正有价值的问题:
成本归因:这次对话花了这么多钱,是因为 AI 调用了太多工具(主对话的调用次数异常多),还是因为上下文压缩太频繁(压缩类调用占比高),还是因为子 Agent 消耗了大量 token?
性能诊断:对话响应变慢了,是主对话的 LLM 调用变慢了(延迟上升),还是后台任务抢占了资源(记忆提取和 Skill 分析的并发数高)?
行为异常检测:压缩类调用的频率突然升高,可能说明用户的对话变长了,或者某个工具的输出变大了;分类器调用的频率升高,可能说明用户在频繁触发需要权限判断的操作。
这些分析在没有调用来源字段的情况下,需要人工从日志中逐条推断,效率极低且容易遗漏。
隐私保护:LLM 日志的边界
可观测性和隐私保护之间有天然张力:记录越详细,越容易调试;但记录越详细,用户隐私暴露越多。
展示多少信息
Alice 的原则是:隐私保护是默认值,详细记录是选择项。
默认情况下:
- 不记录 Prompt 内容
- 不记录工具的具体输入参数
- 只记录元数据(时间、模型、token 数、工具名、耗时)
需要调试时,用户显式开启详细日志模式,记录完整的请求和响应。
这和权限系统的设计哲学一致:默认保守,显式放宽。
给谁看
LLM 日志的可见性需要区分三个受众:
用户自己:可以看到自己的对话摘要、token 消耗、工具调用记录。这些信息帮助用户理解 AI 在做什么、为什么这么做、花了多少成本。
开发者(Debug 模式):可以看到完整的 Prompt、Response、工具输入输出、上下文压缩前后的差异。这些信息对调试 Agent 行为至关重要,但包含用户的对话内容,不应该对所有人可见。
系统管理员:只能看到聚合指标(总调用次数、总 token 消耗、错误率),不能看到任何具体的对话内容。
这个三级可见性的设计,在本地桌面应用中看起来多此一举(用户自己的设备上,谁都可以看)。但我们之所以在架构层面做这个区分,是为将来可能的多用户或云端部署场景做准备。如果将来 Alice 支持团队共享,日志的可见性控制就是必须的。
Debug 模式的门控设计
Debug 信息包含了完整的 LLM 调用详情:System Prompt 全文、用户消息、AI 回复、工具输入输出。这些信息对排查问题极其有价值,但也意味着系统的内部实现(比如 System Prompt 的内容)对外可见。
为什么不是所有用户都能看 Debug 信息
两个原因:
产品体验:大多数用户不需要也不想看 Debug 信息。把技术细节暴露给普通用户,只会增加困惑。就像大多数人使用手机时不需要看系统日志一样。
知识产权保护:System Prompt 是 Agent 产品的核心竞争力之一。如果所有用户都能看到完整的 System Prompt,等于公开了产品的核心设计。对于开源项目来说这不是问题,但对于商业产品来说,这是一个需要考虑的因素。
激活方式
Alice 的 Debug 模式需要激活码才能开启。激活码与设备绑定,防止被随意分享。
这个方案的设计思考:
为什么用激活码而非用户角色(比如管理员角色):Alice 是桌面应用,没有中心化的用户管理系统。角色系统需要一个服务端来管理,激活码则可以离线验证。
为什么要设备绑定:防止激活码被分享。如果一个激活码可以在任意设备上使用,发给一个开发者的激活码可能被传播到用户群里。设备绑定把影响范围控制在有限的设备上。
这个方案的局限:设备标识不是完美的(虚拟机、硬件更换都可能改变标识),过于严格的绑定会带来用户摩擦(换电脑后需要重新申请)。目前的实现允许一个激活码绑定少量设备,作为折中。
启动性能追踪
Agent 应用的启动速度影响用户体验。在关键初始化阶段打点:进程启动、模块加载完成、数据库初始化完成、MCP 连接就绪、用户看到界面。每个 mark 记录相对进程启动的耗时。
这类数据帮助识别:哪个初始化步骤最慢?某次版本更新后启动变慢了,是因为什么?
我们在实际开发中发现,启动性能的最大瓶颈是 MCP 连接建立,而非代码执行。某些 MCP Server 的启动需要几秒钟(特别是需要启动子进程的 Server),如果在主线程同步等待,用户会看到几秒钟的白屏。解决方案是 MCP 连接异步化:先展示界面,MCP 在后台连接,连接完成后再更新工具列表。这个优化让启动体验改善明显,但代价是用户在前几秒可能看到「MCP 工具加载中」的提示。
多 Agent 的可观测性
单个 Agent 出问题好定位;多个 Agent 并发时,日志混在一起很难区分。
解决方案:每个 Agent 实例有唯一 ID,所有日志和事件都携带这个 ID。
在 OTel Trace 里,子 Agent 的 Span 是父 Agent Span 的子节点,形成完整的树状调用链。在 UI 里,子 Agent 的事件通过 ID 字段对应展示在各自的卡片里。
但多 Agent 的可观测性还有一个更深层的挑战:跨 Agent 的因果追踪。父 Agent 把任务分配给子 Agent,子 Agent 的执行结果影响了父 Agent 的后续决策。在日志中,这个因果链表现为:父 Agent 的某个 span 触发了子 Agent 的创建,子 Agent 的结果 span 完成后,父 Agent 的下一个 span 开始。OTel 的 span link 机制可以表达这种因果关系,但大多数可视化工具(Jaeger、Zipkin)对 span link 的展示支持有限,实际使用中往往退化为纯文本日志分析。
PlayGround 的可观测性调试工具
可观测性不只是线上监控,也是开发时的调试工具。
PlayGround 是专门的调试面板,包含:
- 实时查看当前上下文大小和压缩状态
- 模拟不同的权限模式,观察系统行为
- 查看 MCP 连接状态
- 测试不同的提示词,观察模型响应
在开发新功能时,PlayGround 是在不改生产代码的情况下验证想法的地方。
可观测性的工程价值
可观测性不是运维需求,是工程质量的标志。
没有可观测性的 Agent 系统的典型表现:出了问题只能靠重现和猜测;性能优化无从下手(不知道瓶颈在哪);用户报告偶发问题无法排查。
有可观测性的 Agent 系统:问题出现时,日志告诉你发生了什么、为什么;可以用数据驱动优化(某个工具被调用太频繁、压缩触发太早);监控关键指标,提前发现异常。
对于 Agent 系统,可观测性尤其重要。执行路径太复杂,人工推理几乎不可能,只有靠数据才能理解系统的行为。
一个真实的例子:我们曾经收到用户反馈说「Alice 有时候会忘记之前说过的话」。没有可观测性的话,这种反馈几乎无法排查。有了调用来源字段和压缩日志后,我们发现问题出在上下文压缩环节:某些情况下,压缩算法把用户的关键需求描述当作了低价值内容压缩掉了。修复后,这类问题明显下降。如果没有把压缩调用单独标记出来,我们可能需要花几天时间在日志里人工搜索才能定位到压缩环节。