跳转到内容
cd /blog

智能体可观测性:Hooks、Alloy 与 Grafana

[可观测性][Grafana][OpenTelemetry][架构]

> 我们用 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 事件 为我们提供了语义层:诸如 PreToolUsePostToolUsePostToolUseFailureUserPromptSubmitSubagentStopSkillActivated,以及我们在调试智能体行为时真正关心的那些分类元数据。Claude Code 在这里贡献了更丰富的事件流,Codex 贡献的是一条更单薄、但依然有用的规范化数据流。

Hook 究竟为什么存在

我们的 Hook 流水线会在事件进入 Loki 之前,先对它们进行增强处理。

我们不会只说“某个工具运行了”,而是把事件分类到诸如以下字段中:

  • tool_type:builtin、mcp、skill、agent、bash
  • mcp_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_id
  • prompt_id
  • Token 计数
  • 耗时
  • 工具参数数据块

如果你把一切都拿去建索引,就会遇到标签爆炸,然后迎来糟糕的一天。

所以 Alloy 为我们做了三件事:

  1. 只保留一小撮低基数标签进行索引。
  2. 把嘈杂但有用的字段挪进结构化元数据里。
  3. 把纯粹的噪声彻底丢弃。

核心理念很简单:多观测,少索引

为什么 Hook 数据流会绕开 Alloy

Hook 数据流本身已经是为 Loki 量身打造的了。

send_event.py 推送一个事件之前,我们早就已经决定好了哪些字段值得作为标签处理、哪些字段应该留在结构化的 JSON 主体里。这条数据流会直接发往 Grafana Cloud 的 OTLP 网关,而不会再经过 Alloy 处理一遍。

所以整个系统的分工非常清晰:

  • Alloy 驯服原始的原生 OTEL 数据流。
  • Hook 增强 让语义事件变得可查询。

这比试图把一切都硬塞进同一条路径,要简单得多。

仪表盘实际展示了什么

下面的截图来自我们智能体工作流背后的一个可观测性仪表盘。这不是基准测试,这些数字也只是某个时间点的切片。重点在于数据的形态:活动流、工具调用、失败情况、提示词,以及按智能体、内置工具、MCP 使用情况、Shell 命令和技能划分的明细。

有用的地方在于,这个仪表盘和其余的智能体遥测数据位于同一套 Grafana 技术栈上。我们可以按来源智能体和工具族群筛选,跨运行时查看,而不必为每个系统都另外发明一套可观测性方案。

Grafana 仪表盘展示了智能体工作流的活动流、工具调用次数、失败情况、提示词,以及按智能体、内置工具、MCP 使用情况、CLI 命令和技能划分的明细。
这是我们智能体工作流背后实时仪表盘中的一个。这张截图只是共享 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 显示出最高的延迟或反复的重试?

一旦你能做到这些,你就已经非常接近一个自我提升循环了。

从仪表盘到反馈循环

这是我们觉得最有意思的部分。

一旦可观测性技术栈可以从智能体层被查询,遥测数据就不再是一个被动的报告界面,而是变成了一个控制信号。

这个循环大致是这样的:

  1. 智能体活动产生 Trace、指标,以及经过增强处理的 Hook 日志。
  2. Grafana 把证据存储在 Loki、Tempo,以及存在指标的地方——Prometheus。
  3. 智能体通过 Grafana MCP 查询这些证据。
  4. 系统识别出糟糕的工具组合、脆弱的技能、薄弱的路由,或是那些不断产生可避免错误的、大量依赖 Shell 的工作流。
  5. 智能体或运维人员调整提示词、智能体配置、技能描述、路由规则或工具访问权限。
  6. 下一个会话产生新的遥测形态,循环再次开始。

这就是从“有意思的仪表盘”走向“可衡量的改进系统”的方式。

目标不是把某一类工具的使用量最大化,而是针对实际正在进行的工作,找到 CLI、内置工具、MCP 调用和技能之间恰到好处的组合。

这让我们能够回答什么问题

一旦两种运行时都汇入同一套 Grafana 技术栈,我们就能更快地回答运维层面的问题:

  • 失败是否集中在某一个工具族群里?
  • 大量依赖 Shell 的工作流,是否正在制造本可以用更高层级工具避免的错误?
  • 哪些 MCP 服务器承担了主要工作量?
  • 我们是否在为那些没有产生实质进展的智能体活动买单?
  • 一个会话状态不健康,是因为模型、工具,还是编排层出了问题?

这在多智能体工作流中格外有用,因为“智能体一直很忙”这句话几乎什么都说明不了。

如果某个专家子智能体不断被调度、却产生很高的失败率,那就是路由或提示词设计的问题。

如果某个 MCP 服务器主导了所有调用,这可能是良好的架构,也可能说明其他一切都是摆设。

如果工具失败率飙升、成本却居高不下,那是运维问题,不是质量问题。

如果 Shell 相关工作总是以可预测、本可避免的方式失败、而本该有更高层级的工具存在,那就是一个产品信号。

如果某个技能持续被激活、却没有改善结果,那就是一个提示词或路由信号。

真正的教训

这里更深一层的教训是:智能体可观测性同时需要运行时遥测工作流遥测

运行时遥测告诉你系统做了什么。

工作流遥测告诉你智能体当时以为自己在做什么。

两者我们都需要。

如果你只保留 Trace 和计数器,就会错过语义层;如果你只保留 Hook 事件,就会错过延迟、Span 以及更宏观的运行时全貌。

而如果你两者都保留了、却从不把它们反馈回智能体层,那你拥有的只是监控,而不是自适应能力。

正是这种组合,才让系统既足够可解释、能够运维,又足够可调优、能够改进。

目前仍不完美之处

仍然存在一些粗糙的边缘。

  • 并非每个 Hook 事件都包含我们想要的耗时和 Token 数据。
  • 一些最好的时序视图,依然来自 Tempo 的 Trace,而非 Hook 日志。
  • 目前在增强事件流的语义丰富度上,Codex 仍不及 Claude Code。
  • 仪表盘截图是一个真实的运维界面,而不是一件打磨过的营销作品。

最后一点是有意为之的:比起假装智能体系统能神奇地自我解释清楚,我们更愿意展示真实的仪表盘本身。

为什么这对 Maguyva 很重要

Maguyva 的核心,是为智能体提供更好的代码智能。但一旦智能体真正开始做有用的工作,一个新的需求就会立刻浮现:你需要能看清它们的行为方式。

搜索质量、路由质量、工具选择和上下文效率,统统会变成可观测的问题。

这正是我们认为这个话题值得写一写的原因。未来的智能体技术栈不会只是提示词加工具,而是提示词、工具,再加上一层能告诉你整套系统是否真正在正常运转的仪表层。

如果你正在构建认真严肃的智能体工作流,可观测性就不是可有可无的基础设施,而是产品本身的一部分。

相关阅读

更多来自 Maguyva 开发日志的内容