跳至主要內容
cd /blog

代理人可觀測性:Hooks、Alloy 與 Grafana

[可觀測性][Grafana][OpenTelemetry][架構]

> 我們將 Claude Code 與 Codex 接入同一套 Grafana 技術堆疊,搭配 OpenTelemetry 與 Alloy,再運用追蹤與日誌,從源頭找出並修正代理人的行為問題。

代理人系統會以各種奇怪的方式失敗。

有時問題出在模型本身,有時出在工具,有時你的 MCP 伺服器一切正常,但代理人選錯了專家、或是把半個工作階段都花在你意料之外的殼層操作上、又或是在一個表面上看起來很有生產力的迴圈中悄悄燒掉成本。

如果你看不出這些差異,你就不是真的在「操作」一套代理人系統,你只是在瞎猜。

因此我們為自己的工作流程打造了一套可觀測性技術堆疊: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 介面,因此我們會將較單薄的回合完成事件正規化進同一套日誌結構描述,而不是假裝兩個執行環境提供了相同的控制能力。

原生 OTEL 提供了基準串流:來自執行環境本身的日誌與 trace,以及該環境確實會發出的指標。

Hook 與 notify 事件 提供了語意層:像是 PreToolUsePostToolUsePostToolUseFailureUserPromptSubmitSubagentStopSkillActivated,以及我們在除錯代理人行為時真正在乎的分類後中繼資料。這部分較豐富的事件串流由 Claude Code 提供,Codex 則提供較單薄、但仍然有用的正規化串流。

Hook 存在的理由

我們的 hook 管線會在事件抵達 Loki 之前先加以擴充。

我們不會只說「有一個工具執行了」,而是會將事件分類到以下這些欄位:

  • tool_type:builtin、mcp、skill、agent、bash
  • mcp_server:由哪個 MCP 後端處理這次呼叫
  • bash_cli:殼層指令的類型
  • 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"

這些都不是花俏的欄位,它們正是「這個代理人感覺變慢了」與「這個代理人過去十分鐘都花在殼層密集的 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 使用情形、殼層指令與技能所做的細分。

真正有用的地方,在於這個儀表板與其餘代理人遙測資料,共用同一套 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 結構描述

這讓我們能在同一條日誌串流中,使用像 agent_tool="codex-cli" 這樣的統一篩選條件。

誠實地說,Codex 目前的 notify 酬載,內容仍比 Claude Code 的 hook 酬載單薄,因為它並非同一種整合介面。以我們目前的設定來說,Codex 的回合完成事件可以被正規化進共用的結構描述,但逐一工具層級的豐富擷取,目前仍然是原生 OTEL 串流做得比 notify 橋接更好。

這並不是要迴避這篇文章要談的重點,反而正是重點所在:真正的可觀測性系統,是由不完美的訊號拼組而成的。

透過 MCP 使用 Grafana,改變了整個局面

更大的轉變在於,Grafana 不再只是人類用瀏覽器造訪的地方。

在這個儲存庫中,我們同時透過 MCP 公開了 Grafana。這代表代理人可以直接查詢 Loki、Prometheus 與 Tempo,而不必等人類先手動檢視儀表板。

這讓可觀測性從被動的回報介面,變成了工作流程的主動輸入。

代理人可以提出這樣的問題:

  • 過去一小時裡,哪些工具類別失敗最多?
  • 哪個 MCP 伺服器主導了某個工作階段?
  • 近期的變更究竟減少了工具失敗,還是只是把同樣的錯誤,搬到了更依賴殼層的路徑上?
  • 哪些 trace 顯示出最高的延遲,或反覆重試?

一旦擁有這些能力,你就已經非常接近一套自我改進迴圈了。

從儀表板到回饋迴圈

這是我們認為最有意思的部分。

一旦可觀測性技術堆疊可以從代理人層級被查詢,遙測資料就不再只是被動的回報介面,而會變成一種控制訊號。

這個迴圈的樣貌如下:

  1. 代理人活動產生 trace、指標,以及擴充後的 hook 日誌。
  2. Grafana 將這些證據儲存在 Loki、Tempo,以及有指標存在的 Prometheus 中。
  3. 代理人透過 Grafana MCP 查詢這些證據。
  4. 系統辨識出不良的工具組合、脆弱的技能、薄弱的路由,或不斷產生可避免錯誤的殼層密集工作流程。
  5. 代理人或操作者調整提示、代理人設定、技能說明、路由規則,或工具存取權限。
  6. 下一個工作階段會產生新的遙測樣貌,循環繼續進行。

這正是從「有趣的儀表板」邁向「可衡量的改進系統」的方式。

目標並不是把某一類工具的使用量最大化,而是為實際進行的工作,找出 CLI、內建工具、MCP 呼叫與技能之間正確的比例組合。

這讓我們能回答哪些問題

一旦兩種執行環境都落在同一套 Grafana 技術堆疊中,我們就能更快回答營運面的問題:

  • 失敗是否集中在某一個工具類別?
  • 依賴殼層的工作流程,是否正在造成本可避免的錯誤,而其實該有更高階的工具存在?
  • 哪些 MCP 伺服器承擔了大部分工作量?
  • 我們是否正在為沒有產生實質進展的代理人活動付費?
  • 一個工作階段不健康,究竟是因為模型、工具,還是協調層本身?

這在多代理人工作流程中格外有用,因為「代理人很忙」這句話幾乎什麼都沒說明。

如果某一位專家不斷被派出去、卻不斷產生高失敗率,那就是路由或提示設計上的問題。

如果某個 MCP 伺服器主導了所有呼叫,這可能代表架構良好,也可能代表其他一切都是死重。

如果工具失敗率飆升、成本卻居高不下,那就是營運面的問題,而不是品質問題。

如果殼層工作持續以可預期、本可避免的方式失敗,而其實該有更高階的工具存在,那就是一個產品訊號。

如果某個技能不斷被觸發、卻沒有改善結果,那就是提示或路由訊號。

真正的教訓

這裡更深層的教訓在於:代理人可觀測性同時需要執行環境遙測工作流程遙測

執行環境遙測告訴你系統做了什麼。

工作流程遙測告訴你代理人「以為」自己在做什麼。

兩者我們都需要。

如果你只保留 trace 與計數器,你就會錯過語意層;如果你只保留 hook 事件,你就會錯過延遲、span,以及更完整的執行環境全貌。

而如果你兩者都保留,卻從未將其回饋給代理人層,你得到的只是監控,而不是調適。

正是這樣的組合,讓整套系統既足夠可解釋以供操作,也足夠可調整以持續改進。

仍不完美之處

仍有一些粗糙的邊角。

  • 並非每個 hook 事件都包含我們希望有的耗時與 token 資料。
  • 有些最好的時間分析視角,仍來自 Tempo trace,而不是 hook 日誌。
  • 在擴充後的事件串流中,Codex 目前在語意豐富度上仍不及 Claude Code。
  • 這張儀表板截圖,是一個真實的營運介面,而不是一件精心打磨的行銷素材。

最後這一點是刻意為之的。我們寧可展示真實的儀表面板,也不願假裝代理人系統會神奇地自我解釋一切。

這對 Maguyva 為何重要

Maguyva 的核心,是要為代理人提供更好的程式碼智慧。但一旦代理人真正開始做有用的工作,一項新的需求會立刻浮現:你需要看見它們的行為表現。

搜尋品質、路由品質、工具選擇,以及情境使用效率,全都會變成可觀測的問題。

這正是我們認為值得寫下這篇文章的原因。未來的代理人技術堆疊,不會只是提示與工具,而是提示、工具,再加上告訴你這整套機制是否真正運作正常的儀器層。

如果你正在打造認真對待的代理人工作流程,可觀測性就不是可有可無的基礎設施,它就是產品本身的一部分。

延伸閱讀

更多來自 Maguyva 開發日誌的內容