代理人可觀測性:Hooks、Alloy 與 Grafana
> 我們將 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 事件 提供了語意層:像是 PreToolUse、PostToolUse、PostToolUseFailure、UserPromptSubmit、SubagentStop、SkillActivated,以及我們在除錯代理人行為時真正在乎的分類後中繼資料。這部分較豐富的事件串流由 Claude Code 提供,Codex 則提供較單薄、但仍然有用的正規化串流。
Hook 存在的理由
我們的 hook 管線會在事件抵達 Loki 之前先加以擴充。
我們不會只說「有一個工具執行了」,而是會將事件分類到以下這些欄位:
tool_type:builtin、mcp、skill、agent、bashmcp_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_idprompt_id- token 數量
- 耗時
- 工具參數區塊
如果你把每樣東西都索引起來,就會遇上標籤爆炸,然後迎來悲慘的一天。
所以 Alloy 為我們做了三件事:
- 只讓一小組低基數標籤維持索引狀態。
- 把雜訊多、但仍有用的欄位移進結構化中繼資料。
- 徹底捨棄純粹的雜訊。
背後的核心理念很簡單:觀察得更多,索引得更少。
為何 Hook 串流會繞過 Alloy
Hook 串流本身早已針對 Loki 塑形完成。
在 send_event.py 推送一筆事件之前,我們早已決定好哪些欄位值得當作標籤處理、哪些該歸入結構化的 JSON 內容主體。這條串流會直接送往 Grafana Cloud 的 OTLP 閘道,不會再經過 Alloy。
因此整套系統的分工非常清楚:
- Alloy 馴服原始的原生 OTEL 串流。
- Hook 擴充 讓語意事件變得可查詢。
這讓整體架構,比起硬要把所有東西塞進同一條路徑,簡單得多。
儀表板實際呈現了什麼
下方的截圖,取自支撐我們代理人工作流程的其中一個可觀測性儀表板。它不是一次基準測試,這些數字也只是某個時間點的切片。重點在於資料的樣貌:活動動態、工具呼叫、失敗紀錄、提示內容,以及依代理人、內建工具、MCP 使用情形、殼層指令與技能所做的細分。
真正有用的地方,在於這個儀表板與其餘代理人遙測資料,共用同一套 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 顯示出最高的延遲,或反覆重試?
一旦擁有這些能力,你就已經非常接近一套自我改進迴圈了。
從儀表板到回饋迴圈
這是我們認為最有意思的部分。
一旦可觀測性技術堆疊可以從代理人層級被查詢,遙測資料就不再只是被動的回報介面,而會變成一種控制訊號。
這個迴圈的樣貌如下:
- 代理人活動產生 trace、指標,以及擴充後的 hook 日誌。
- Grafana 將這些證據儲存在 Loki、Tempo,以及有指標存在的 Prometheus 中。
- 代理人透過 Grafana MCP 查詢這些證據。
- 系統辨識出不良的工具組合、脆弱的技能、薄弱的路由,或不斷產生可避免錯誤的殼層密集工作流程。
- 代理人或操作者調整提示、代理人設定、技能說明、路由規則,或工具存取權限。
- 下一個工作階段會產生新的遙測樣貌,循環繼續進行。
這正是從「有趣的儀表板」邁向「可衡量的改進系統」的方式。
目標並不是把某一類工具的使用量最大化,而是為實際進行的工作,找出 CLI、內建工具、MCP 呼叫與技能之間正確的比例組合。
這讓我們能回答哪些問題
一旦兩種執行環境都落在同一套 Grafana 技術堆疊中,我們就能更快回答營運面的問題:
- 失敗是否集中在某一個工具類別?
- 依賴殼層的工作流程,是否正在造成本可避免的錯誤,而其實該有更高階的工具存在?
- 哪些 MCP 伺服器承擔了大部分工作量?
- 我們是否正在為沒有產生實質進展的代理人活動付費?
- 一個工作階段不健康,究竟是因為模型、工具,還是協調層本身?
這在多代理人工作流程中格外有用,因為「代理人很忙」這句話幾乎什麼都沒說明。
如果某一位專家不斷被派出去、卻不斷產生高失敗率,那就是路由或提示設計上的問題。
如果某個 MCP 伺服器主導了所有呼叫,這可能代表架構良好,也可能代表其他一切都是死重。
如果工具失敗率飆升、成本卻居高不下,那就是營運面的問題,而不是品質問題。
如果殼層工作持續以可預期、本可避免的方式失敗,而其實該有更高階的工具存在,那就是一個產品訊號。
如果某個技能不斷被觸發、卻沒有改善結果,那就是提示或路由訊號。
真正的教訓
這裡更深層的教訓在於:代理人可觀測性同時需要執行環境遙測與工作流程遙測。
執行環境遙測告訴你系統做了什麼。
工作流程遙測告訴你代理人「以為」自己在做什麼。
兩者我們都需要。
如果你只保留 trace 與計數器,你就會錯過語意層;如果你只保留 hook 事件,你就會錯過延遲、span,以及更完整的執行環境全貌。
而如果你兩者都保留,卻從未將其回饋給代理人層,你得到的只是監控,而不是調適。
正是這樣的組合,讓整套系統既足夠可解釋以供操作,也足夠可調整以持續改進。
仍不完美之處
仍有一些粗糙的邊角。
- 並非每個 hook 事件都包含我們希望有的耗時與 token 資料。
- 有些最好的時間分析視角,仍來自 Tempo trace,而不是 hook 日誌。
- 在擴充後的事件串流中,Codex 目前在語意豐富度上仍不及 Claude Code。
- 這張儀表板截圖,是一個真實的營運介面,而不是一件精心打磨的行銷素材。
最後這一點是刻意為之的。我們寧可展示真實的儀表面板,也不願假裝代理人系統會神奇地自我解釋一切。
這對 Maguyva 為何重要
Maguyva 的核心,是要為代理人提供更好的程式碼智慧。但一旦代理人真正開始做有用的工作,一項新的需求會立刻浮現:你需要看見它們的行為表現。
搜尋品質、路由品質、工具選擇,以及情境使用效率,全都會變成可觀測的問題。
這正是我們認為值得寫下這篇文章的原因。未來的代理人技術堆疊,不會只是提示與工具,而是提示、工具,再加上告訴你這整套機制是否真正運作正常的儀器層。
如果你正在打造認真對待的代理人工作流程,可觀測性就不是可有可無的基礎設施,它就是產品本身的一部分。
延伸閱讀
更多來自 Maguyva 開發日誌的內容
我們為何將程式碼搜尋升級至 voyage-4-large_
我們將程式碼嵌入模型換成了 voyage-4-large — 目前在公開的 RTEB 程式碼檢索排行榜上排名第一。這是誠實的版本:我們做了什麼取捨、我們實際索引的是什麼,以及我們為何願意為高階嵌入模型付費。
語言遞迴自我改進:在近 280 種語言中打磨程式碼智慧_
我們為近 280 種語言提供程式碼智慧支援,沒有人力能逐一手動稽核。因此我們打造了一套語言遞迴自我改進迴圈 — 抽查、LLM 擔任評審、修正單一問題、重新驗證 — 並以一支隔離代理人艦隊持續執行,直到擷取結果真正正確,而不只是綠燈通過。
多模態融合搜尋:為每個查詢挑選正確的檢索方式_
像「parseConfig 定義在哪裡」這樣的查詢,需要的搜尋方式和「auth 是如何運作的」截然不同。Maguyva 會先分類查詢意圖,據此為四種檢索模式加權,再以加權版的 Reciprocal Rank Fusion(倒數排名融合)演算法整合結果。