智能体可观测性:Hooks、Alloy 与 Grafana
> 我们用 OpenTelemetry 和 Alloy,把 Claude Code 与 Codex 接入同一套 Grafana 技术栈,再借助 Trace 和日志从源头定位并修复智能体的行为问题。
智能体系统会以各种古怪的方式出问题。
有时候是模型的问题,有时候是工具的问题。有时候你的 MCP 服务器明明一切正常,但智能体选错了专家子智能体,或者花了半个会话去做你根本没料到的 Shell 操作,又或者在一个表面看起来很“高产”的循环里悄悄烧掉了成本。
如果你看不出这些差异,你其实并不是在运维一个智能体系统,你只是在瞎猜。
所以我们为自己的工作流搭建了一套可观测性技术栈:Claude Code、Codex、Claude 的 Hook 事件、Codex 的 notify 事件、原生 OpenTelemetry,另一端则是 Grafana Alloy 和 Grafana Cloud。
有意思的地方不在于“我们做了一个仪表盘”,而在于我们不得不把遥测数据拆分成两条不同的数据流——因为没有任何一条单一的数据源能给我们完整的图景。
问题所在:智能体遥测数据是碎片化的
现代编码智能体本身已经会发出一些遥测数据,这有帮助,但还不够。
原生 OTEL 很擅长回答这类问题:
- 我们发出了多少次请求?
- 一个会话花费了多少成本?
- Span 和 Trace 在哪里?
- 延迟有没有出现峰值?
但它在回答这类问题时就差得多了:
- 智能体主要依赖了哪个 MCP 服务器?
- 这次失败发生在
Bash、某个内置文件工具,还是某次 MCP 调用中? - 究竟哪个技能被激活了?
- 调度出去的是哪种子智能体类型?
- 这个会话到底是在做有用的工作,还是在空转?
第二类问题,离 Hook 更近,离 Trace 更远。
但反过来也一样成立:一些最重要的性能问题,反而离 Trace 更近,离 Hook 更远。
如果你想知道延迟究竟积累在哪里、哪些 Span 慢了,或者这个会话把时间花在了模型调用上还是工具执行上,你既需要 Trace 数据,也需要语义事件。
我们最终采用的架构
我们并行运行两条遥测路径。
Claude Code
native OTEL -> Alloy -> Grafana Cloud
hooks -> send_event.py -> Grafana Cloud Loki
Codex
native OTEL -> Alloy -> Grafana Cloud
notify hook -> codex_notify.py -> shared Loki schema
这种拆分是刻意为之的。
它同时也是不对称的。Claude Code 给了我们丰富得多的生命周期 Hook 接口;Codex 给了我们原生 OTEL 加上一个 notify 接口,所以我们把 Codex 那些较为单薄的“轮次完成”事件规范化进同一套日志 Schema,而不是假装两个运行时暴露出的是同一套控制能力。
原生 OTEL 为我们提供了基线数据流:来自运行时本身的日志和 Trace,以及运行时实际发出的指标。
Hook 与 notify 事件 为我们提供了语义层:诸如 PreToolUse、PostToolUse、PostToolUseFailure、UserPromptSubmit、SubagentStop、SkillActivated,以及我们在调试智能体行为时真正关心的那些分类元数据。Claude Code 在这里贡献了更丰富的事件流,Codex 贡献的是一条更单薄、但依然有用的规范化数据流。
Hook 究竟为什么存在
我们的 Hook 流水线会在事件进入 Loki 之前,先对它们进行增强处理。
我们不会只说“某个工具运行了”,而是把事件分类到诸如以下字段中:
tool_type:builtin、mcp、skill、agent、bashmcp_server:哪个 MCP 后端处理了这次调用bash_cli:Shell 命令的类别subagent_type:调度出去的是哪种子智能体agent_tool:事件来源是 Claude Code 还是 Codex
这意味着我们可以提出真正关乎运维的问题:
{service_name="claude-code-hooks"} | agent_tool="codex-cli"
{service_name="claude-code-hooks"} | tool_type="mcp"
{service_name="claude-code-hooks"} | json | bash_cli="git"
这些都不是花架子字段。它们是“感觉智能体有点慢”和“智能体在过去十分钟里都耗在了工具失败率很高的、大量使用 Shell 的 Git 操作上”这两种说法之间的真正差异。
有一个看起来比实际更奇怪的实现细节:即便事件来自 Codex,共享的 Loki 数据流仍然使用 service_name="claude-code-hooks" 作为标签。运行时之间真正的区分发生在 agent_tool 上。
为什么 Alloy 处在中间位置
在这套设置里,Grafana Alloy 不只是一个转发器,它是策略边界。
我们把来自 Claude Code 和 Codex 的原生 OTEL 数据流,指向 localhost:4318 上的本地 Alloy 代理,让 Alloy 在把负载转发给 Grafana Cloud 之前先清理一遍。
这一点很重要,因为原始的智能体遥测数据里,充斥着大量对分析很有用、但作为索引标签却很糟糕的高基数字段:
session_idprompt_id- Token 计数
- 耗时
- 工具参数数据块
如果你把一切都拿去建索引,就会遇到标签爆炸,然后迎来糟糕的一天。
所以 Alloy 为我们做了三件事:
- 只保留一小撮低基数标签进行索引。
- 把嘈杂但有用的字段挪进结构化元数据里。
- 把纯粹的噪声彻底丢弃。
核心理念很简单:多观测,少索引。
为什么 Hook 数据流会绕开 Alloy
Hook 数据流本身已经是为 Loki 量身打造的了。
在 send_event.py 推送一个事件之前,我们早就已经决定好了哪些字段值得作为标签处理、哪些字段应该留在结构化的 JSON 主体里。这条数据流会直接发往 Grafana Cloud 的 OTLP 网关,而不会再经过 Alloy 处理一遍。
所以整个系统的分工非常清晰:
- Alloy 驯服原始的原生 OTEL 数据流。
- Hook 增强 让语义事件变得可查询。
这比试图把一切都硬塞进同一条路径,要简单得多。
仪表盘实际展示了什么
下面的截图来自我们智能体工作流背后的一个可观测性仪表盘。这不是基准测试,这些数字也只是某个时间点的切片。重点在于数据的形态:活动流、工具调用、失败情况、提示词,以及按智能体、内置工具、MCP 使用情况、Shell 命令和技能划分的明细。
有用的地方在于,这个仪表盘和其余的智能体遥测数据位于同一套 Grafana 技术栈上。我们可以按来源智能体和工具族群筛选,跨运行时查看,而不必为每个系统都另外发明一套可观测性方案。
为什么 Trace 比乍看之下更重要
日志告诉我们发生了哪一类工作,Trace 告诉我们这项工作是如何随时间展开的。
这个区别在智能体系统中很重要,因为“慢”这个词太笼统,没什么实际用处。
一条 Trace 可以告诉我们,痛点究竟来自:
- 模型延迟
- 工具执行耗时
- 反复重试
- 某一次格外昂贵的 MCP 交互
- 一长串单独看起来都无伤大雅的小操作
实际操作中,我们会把 Hook 数据流和 Tempo 的 Trace 结合起来使用。
- Hook 日志 回答的是:发生了什么类型的事情?
- Trace 回答的是:时间都花到哪里去了?
正是这种组合,才把可观测性从一个仪表盘变成了一份解释。
Codex 处在什么位置
Codex 是同一套技术栈的一部分,但它和 Claude Code 并不完全相同。
对于 Codex,我们接入了两个部分:
- 来自 Codex 的原生 OTEL 数据接入 Alloy
- 一个 notify Webhook 接入
codex_notify.py,把轮次完成事件映射进我们用于 Hook 事件的那套 Loki Schema
这让我们能在同一条日志流内,使用像 agent_tool="codex-cli" 这样的统一筛选条件。
诚实地说有个但书:Codex 的 notify 负载目前比 Claude Code 的 Hook 负载要单薄,因为二者本就不是同一种集成接口。在我们今天的设置里,Codex 的轮次完成事件可以被规范化进共享 Schema,但要做到逐工具的丰富提取,原生 OTEL 数据流依然比 notify 桥接更胜一筹。
这不是回避这篇文章要说的问题的理由,恰恰相反,这正是这篇文章想说的重点:真正的可观测性系统,本就是由不完美的信号拼凑而成的。
通过 MCP 使用 Grafana,彻底改变了游戏规则
更大的转变在于,Grafana 不再只是人类在浏览器里访问的地方。
在这个代码仓库中,我们还通过 MCP 把 Grafana 暴露了出来。这意味着智能体可以直接查询 Loki、Prometheus 和 Tempo,而不必等人类先手动查看一遍仪表盘。
这让可观测性变成了工作流中一项主动的输入,而不只是被动的报告。
智能体可以提问:
- 过去一小时里,哪个工具族群失败次数最多?
- 哪个 MCP 服务器在某个会话中占据了主导?
- 最近的改动是减少了工具失败,还是只是把工作挪到了更依赖 Shell 的路径上、却犯着同样的错误?
- 哪些 Trace 显示出最高的延迟或反复的重试?
一旦你能做到这些,你就已经非常接近一个自我提升循环了。
从仪表盘到反馈循环
这是我们觉得最有意思的部分。
一旦可观测性技术栈可以从智能体层被查询,遥测数据就不再是一个被动的报告界面,而是变成了一个控制信号。
这个循环大致是这样的:
- 智能体活动产生 Trace、指标,以及经过增强处理的 Hook 日志。
- Grafana 把证据存储在 Loki、Tempo,以及存在指标的地方——Prometheus。
- 智能体通过 Grafana MCP 查询这些证据。
- 系统识别出糟糕的工具组合、脆弱的技能、薄弱的路由,或是那些不断产生可避免错误的、大量依赖 Shell 的工作流。
- 智能体或运维人员调整提示词、智能体配置、技能描述、路由规则或工具访问权限。
- 下一个会话产生新的遥测形态,循环再次开始。
这就是从“有意思的仪表盘”走向“可衡量的改进系统”的方式。
目标不是把某一类工具的使用量最大化,而是针对实际正在进行的工作,找到 CLI、内置工具、MCP 调用和技能之间恰到好处的组合。
这让我们能够回答什么问题
一旦两种运行时都汇入同一套 Grafana 技术栈,我们就能更快地回答运维层面的问题:
- 失败是否集中在某一个工具族群里?
- 大量依赖 Shell 的工作流,是否正在制造本可以用更高层级工具避免的错误?
- 哪些 MCP 服务器承担了主要工作量?
- 我们是否在为那些没有产生实质进展的智能体活动买单?
- 一个会话状态不健康,是因为模型、工具,还是编排层出了问题?
这在多智能体工作流中格外有用,因为“智能体一直很忙”这句话几乎什么都说明不了。
如果某个专家子智能体不断被调度、却产生很高的失败率,那就是路由或提示词设计的问题。
如果某个 MCP 服务器主导了所有调用,这可能是良好的架构,也可能说明其他一切都是摆设。
如果工具失败率飙升、成本却居高不下,那是运维问题,不是质量问题。
如果 Shell 相关工作总是以可预测、本可避免的方式失败、而本该有更高层级的工具存在,那就是一个产品信号。
如果某个技能持续被激活、却没有改善结果,那就是一个提示词或路由信号。
真正的教训
这里更深一层的教训是:智能体可观测性同时需要运行时遥测和工作流遥测。
运行时遥测告诉你系统做了什么。
工作流遥测告诉你智能体当时以为自己在做什么。
两者我们都需要。
如果你只保留 Trace 和计数器,就会错过语义层;如果你只保留 Hook 事件,就会错过延迟、Span 以及更宏观的运行时全貌。
而如果你两者都保留了、却从不把它们反馈回智能体层,那你拥有的只是监控,而不是自适应能力。
正是这种组合,才让系统既足够可解释、能够运维,又足够可调优、能够改进。
目前仍不完美之处
仍然存在一些粗糙的边缘。
- 并非每个 Hook 事件都包含我们想要的耗时和 Token 数据。
- 一些最好的时序视图,依然来自 Tempo 的 Trace,而非 Hook 日志。
- 目前在增强事件流的语义丰富度上,Codex 仍不及 Claude Code。
- 仪表盘截图是一个真实的运维界面,而不是一件打磨过的营销作品。
最后一点是有意为之的:比起假装智能体系统能神奇地自我解释清楚,我们更愿意展示真实的仪表盘本身。
为什么这对 Maguyva 很重要
Maguyva 的核心,是为智能体提供更好的代码智能。但一旦智能体真正开始做有用的工作,一个新的需求就会立刻浮现:你需要能看清它们的行为方式。
搜索质量、路由质量、工具选择和上下文效率,统统会变成可观测的问题。
这正是我们认为这个话题值得写一写的原因。未来的智能体技术栈不会只是提示词加工具,而是提示词、工具,再加上一层能告诉你整套系统是否真正在正常运转的仪表层。
如果你正在构建认真严肃的智能体工作流,可观测性就不是可有可无的基础设施,而是产品本身的一部分。
相关阅读
更多来自 Maguyva 开发日志的内容
我们为什么把代码搜索升级到了 voyage-4-large_
我们把代码 Embedding 迁移到了 voyage-4-large——目前公开的 RTEB 代码检索排行榜上排名第一的模型。诚实的版本是这样的:我们做出的取舍、我们实际索引的内容,以及我们为什么愿意为高端 Embedding 付费。
语言递归自我提升:在约 280 种语言上打磨代码智能_
我们为约 280 种语言提供代码智能支持,没有任何人能靠人工逐一审核这个规模。于是我们搭建了一套语言递归自我提升循环——抽查、LLM 担任评审、修一处、重新验证——并用一支相互隔离的智能体舰队运行它,直到提取结果真正正确,而不只是“绿灯通过”。
多模态融合搜索:为每一次查询挑选正确的检索器_
像“parseConfig 是在哪里定义的”这样的查询,和“身份验证是怎么工作的”所需要的搜索方式截然不同。Maguyva 会对查询意图进行分类,据此为四种检索模态分配权重,再用加权的 Reciprocal Rank Fusion(倒数排序融合)把结果融合起来。