传统分布式追踪基于已知请求树,但Agent是模型驱动的循环,运行时结构未知,需以工具调用为基本观测单元重新设计。
让一个 Hermes agent 运行起来大约需要三十分钟。但要获得真正的可观测性,需要的时间要长得多。你选择一个后端,部署一个 OTel 收集器,配置导出器,在 agent 运行的所有地方注入环境变量。然后你需要搞清楚 agent 自己会产生哪些 span,哪些又需要手动插桩,部署,再等着看实际是否有数据到达。
这个差距不是工具层面的差距。工具本身都很优秀。差距存在的原因在于,分布式追踪底层的模型是为一种不同形态的程序设计的,而不是你正在运行的这个。
分布式追踪假设的是一个请求树。一个调用进来,它分发到一组已知的后端服务,每一跳就是一个 span,树最终闭合。请求开始前结构就已经确定了——你作为插桩人员的工作就是给已经存在的边打标签。
但 agent 不是请求树。它是一个循环,中间是一个决定下一步做什么的模型。运行的结行结构在运行开始前是未知的,而且每次运行之间也不稳定。由此产生四个问题,每个都会让你花上一整天。
当一次 agent 运行出问题时,问题几乎从来不是哪个服务变慢了。而是它决定做什么,用什么参数,以及返回了什么。这些都是语义事件。HTTP 插桩不会免费捕获这些,所以你最终只能手动对你真正关心的事物进行插桩——这是代价最高的那种插桩。
两次运行同一个任务可能走不同的路径,调用不同数量的工具,但都成功。聚合 span 统计数据假设拓扑是稳定的,所以一个 span 在一次运行中出现三次而在另一次运行中出现零次,那么它的 p95 延迟是一个几乎没有意义的数字。你需要的是比较运行结果,而不是聚合 span,但大多数后端并不是围绕这个设计的。
这一点让很多人意外。一次工具调用失败了,agent 读取错误,判断它可以绕过,然后继续执行。运行完成了,最终消息说任务完成了。每个 span 都是绿的,你的错误率是零,但工作结果是错的。没有什么可以让阈值告警触发的——因为在传输层一切正常。失败出在推理层,而那已经是你测量的东西往上游两步的地方了。
提示词、工具参数和工具结果才是你需要看到的。Span 属性并不是为承载千字节级别的文本块设计的,而一旦你开始截断它们,你就抛弃了你最初收集的证据。
OpenTelemetry 正在制定 GenAI 语义约定,这会有所帮助。但约定只是标准化字段名称。它们不会改变你正在用一个请求树模型来描述一个决策循环的事实,也不会缩短你在查看第一次运行之前花在架设收集器上的那个下午。
我们在 Failproof 中围绕工具调用边界来设计,而不是网络边界,因为那是 agent 行为变得可理解的地方。Agent 执行的每个动作都是一个事件:调用、参数、结果,以及它在运行中的位置。安装它只需要一个包和一条命令:
npm i -g failproofai
failproofai config
config 设置好二进制文件和環境,这样你的 Hermes 日志就会被转发到你的仪表盘。当提示时选择推荐的选项,它会要求你提供 API key。
要获取一个,打开你的仪表盘,进入 Keys 部分,创建一个带有 events.add 权限的新 key。这个权限允许 CLI 发送日志事件。用它所属的 agent 来命名它,这样以后你有多个的时候会更方便。把 key 粘贴到 CLI 中。整体设置就完成了——不需要运行收集器,不需要配置导出器,也不需要修改你的 agent 代码。
事件会在 agent 工作的同时开始出现在仪表盘中。如果你之前已经在运行 agent 了,你可以拉取当时没有捕获的历史记录:
failproofai backfill --since 7d
一旦数据开始流动,有意义的行动不是建仪表盘。而是打开三四个真实的运行记录,从头读到尾。三样东西通常会在第一轮检查中出现。
重复调用。 同一个文件在一次运行中被读取三次,或者同一个查询在循环的每次迭代中都触发。通常对正确性无害,但对延迟和 token 消耗很贵。一旦你能看到就很容易修复,但在那之前是隐形的。
被吞掉的错误。 找一次成功了的运行,扫描里面的失败工具调用。在 agent 遇到错误并继续的地方,问自己继续执行是否真的正确。这才是有趣 bug 所在的地方。
分歧。 运行同一个任务两次,把两条追踪记录并排放置。它们不同的地方就是你的 agent 正在做一个你从未明确指定的判断。有时候这是可以的,有时候则是一个需要更严格约束的提示词。
这些都不会产生异常。也不会触发告警。它们是你部署的 agent 和你理解的 agent 之间的区别,唯一能找到它们的方法是看实际发生了什么。
CLI 是开源的。文档和仪表盘在 befailproof.ai。