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

Canonical source: https://maguyva.ai/zh-hans/blog/agent-observability-hooks-alloy-grafana

License: text under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/); code samples under [MIT-0](https://opensource.org/license/mit-0); the Maguyva name, logos, brand assets and other code are not covered. Details: [maguyva.ai/license](https://maguyva.ai/license).

Date: 2026-03-24 · Author: Maguyva Team

> 我们用 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 数据，也需要语义事件。

## 我们最终采用的架构

我们并行运行两条遥测路径。

```text
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、bash
- `mcp_server`：哪个 MCP 后端处理了这次调用
- `bash_cli`:Shell 命令的类别
- `subagent_type`：调度出去的是哪种子智能体
- `agent_tool`：事件来源是 Claude Code 还是 Codex

这意味着我们可以提出真正关乎运维的问题：

```text
{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 技术栈上。我们可以按来源智能体和工具族群筛选，跨运行时查看，而不必为每个系统都另外发明一套可观测性方案。

<figure class="feature-figure feature-figure--wide">
  <a
    href="/blog/agent-observability-dashboard.png"
    target="_blank"
    rel="noreferrer"
    aria-label="以完整分辨率打开可观测性仪表盘截图"
  >
    <img
      src="/blog/agent-observability-dashboard.png"
      alt="Grafana 仪表盘展示了智能体工作流的活动流、工具调用次数、失败情况、提示词，以及按智能体、内置工具、MCP 使用情况、CLI 命令和技能划分的明细。"
    />
  </a>
  <figcaption>
    这是我们智能体工作流背后实时仪表盘中的一个。这张截图只是共享 Grafana 技术栈中的一个切片，该技术栈还接收着来自我们其他智能体运行时和系统的遥测数据。点击图片可查看完整分辨率版本。
  </figcaption>
</figure>

## 为什么 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 的核心，是为智能体提供更好的代码智能。但一旦智能体真正开始做有用的工作，一个新的需求就会立刻浮现：你需要能看清它们的行为方式。

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

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

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