A 扁平的日誌可以告訴你有五件事發生了。它通常無法告訴你是哪個操作導致了下一個操作、哪個失敗觸發了回退,或是三次工具呼叫到底是某個規劃步驟的子項,還是彼此無關的工作。
這種區別對 AI 代理很重要,因為路徑本身就是行為的一部分。
我維護 AgentInspect,這是一個用於在本機檢視代理執行流程的開源 TypeScript 工具包。本文說明我為什麼選擇執行樹作為主要除錯模型,並使用經 [email protected] 驗證的合成測試樣板來說明。
想像一個支援代理執行了以下操作:
09:00:00.000 plan started
09:00:00.020 inventory request started
09:00:00.060 inventory request failed: 503
09:00:00.061 inventory request started
09:00:00.120 inventory request succeeded
09:00:00.150 answer completed
這足以重建一個簡單故事,但真正的重建其實是在你的腦中完成的。當加入巢狀代理、平行工具、重複使用的操作名稱,以及交錯的應用程式日誌後,時間戳就不再是可靠的因果關係圖像。
執行樹會把關係明確呈現出來:
support-agent
├── plan
├── fetch-inventory (failed: 503)
├── fetch-inventory (success)
└── draft-answer
這個樹狀圖不會取代原始事件資料。它只是針對開發者通常最先會問的問題,對這些資料做出的投影:這次執行走了哪條路徑?
AgentInspect 提供了用於整個執行與命名步驟的包裝器。以下是一個刻意簡化的範例:
import { inspectRun, step } from "agent-inspect";
await inspectRun(
"travel-planner",
async () => {
const plan = await step("plan", async () => ({
destinations: ["SFO", "SEA"],
}));
const [flights, hotels] = await Promise.all([
step.tool("search-flights", async () => [
{ id: "F-101", price: 220 },
]),
step.tool("search-hotels", async () => [
{ id: "H-202", nightly: 180 },
]),
]);
return step.llm("rank-options", async () => ({
plan,
flights,
hotels,
}));
},
{ traceDir: "./.agent-inspect" },
);
這是手動加上監測。它並不是在宣稱某個包裝器能自動找出框架內部的每一個操作。其目的,是記錄你在意的邊界:整個執行、規劃步驟、兩個平行的工具呼叫,以及最後面向模型的步驟。
接著就可以在本機檢視這次執行:
npx agent-inspect view travel-planner \
--dir .agent-inspect \
--summary
三層深的合成測試樣板會渲染成這樣:
Execution Tree:
✔ outer (120ms)
✔ middle (80ms)
✔ inner (50ms)
那兩個空格不是裝飾。它們告訴我們 inner 隸屬於 middle,而 middle 又隸屬於 outer。如果 inner 失敗了,我們就知道是哪個高階操作擁有它。若只看扁平日誌,就得靠匹配 ID 或前後時間戳來推測同樣的結構。
巢狀結構在以下情境特別有用:一個代理委派給另一個代理、一個工具會執行多個子操作,或一個檢索步驟同時擁有查詢重寫與向量搜尋。
現在來看一個錯誤復原樣板:
Execution Tree:
✖ tool:primary-search (100ms)
Error: primary search unavailable
✔ tool:fallback-search (200ms)
✔ handle-recovered-result (50ms)
最終執行可能仍然成功。如果我們只看答案,失敗的主要搜尋可能就會從除錯故事中消失。樹狀圖保留了兩件事:
這個區別可能會改變工程決策。由回退產生的成功答案也許可以接受,但回退使用率突然上升,仍可能表示某個依賴退化,或是路由策略變得昂貴。
重試也值得有自己清楚可見的形狀:
Execution Tree:
✖ tool:fetch-inventory (40ms)
Error: synthetic 503 from upstream
✖ tool:fetch-inventory (45ms)
Error: synthetic 503 from upstream
✔ tool:fetch-inventory (60ms)
✔ handle-recovered-result (30ms)
如果只看最後成功的狀態,就會看不到達成成功所花的成本。重複的工具名稱讓重試序列變得可見。它也讓某個確定性檢查有具體內容可評估,例如 fetch-inventory 是否超過允許的呼叫次數。
樹本身並不能告訴我們重試策略是否正確,但它提供了證據,證明這個策略確實被觸發了。
平行樣板會渲染成同層的操作:
Execution Tree:
✔ tool:search-hotels (300ms)
✔ tool:search-flights (200ms)
✔ tool:search-cars (100ms)
這些持續時間不是要相加的。這些步驟是兄弟節點,而且可能彼此重疊。這能避免一個常見的時間線錯誤:以為每個有時間戳的操作都在前一個操作之後才開始。
樹無法證明並行性是否以最佳方式實作,但它能準確保留用來調查並行性的結構關係。
人們很容易想把可讀的樹變成唯一儲存的工件。但我避免這麼做,因為人類可讀的視圖必然會壓縮資訊。
底層追蹤資料可能包含辨識碼、時間戳、狀態、輸入或輸出(依擷取政策而定)、觀察結果,以及中繼資料。不同問題需要不同的投影:
structured trace
├── tree -> 走了哪條路徑?
├── check -> 是否滿足不變式?
├── diff -> 兩次執行之間變了什麼?
├── report -> 審查者應該讀什麼?
└── bundle -> 可以分享哪些證據?
執行樹是最快的入口點,但不是取代檢查或分析的東西。
假設重試樹顯示某個庫存工具可以執行三次。如果預期政策最多只允許兩次呼叫,就應該把這個預期編碼進去,而不是依賴之後人工目視檢查。
在 CLI 層級,可以用 trajectory 檢查來要求工具並在記錄到觀察結果時失敗:
npx agent-inspect check travel-planner \
--dir .agent-inspect \
--preset trajectory \
--required-tool search-flights \
--fail-on-observation failed
若需要更豐富的規則,AgentInspect 提供了一個實驗性的 TraceContract API,可表達工具需求、禁止使用的工具、最大呼叫次數、順序、執行狀態、持續時間、模型允許清單,以及 token 上限。由於該 API 在本文引用的版本中仍屬 beta,請先鎖定版本,並在把它當作 CI 閘門前,測試其精確語意。
真正重要的工作流程不只是一個 API:
一棵乾淨的樹並不能證明答案正確。必要的檢索步驟可能回傳無關文件。模型呼叫可能產生沒有根據的主張。某個工具在技術上成功了,卻可能回傳過時資料。
執行樹最擅長的是結構性問題:
內容品質則應交給語意評估器、領域測試與人工審查。最可靠的代理除錯流程,是把這些層次結合起來,而不是指望某一種視覺化解答所有問題。
最終回應是使用者看得到的東西,但執行路徑才是工程師可以改進的部分。樹把這條路徑從推測出的敘事,變成了具體的工件。
這就是 AgentInspect 本機檢視的設計原則:保留因果結構、即使復原成功也要暴露失敗工作,並讓可疑模式容易轉成可重複的檢查。
你可以在 GitHub 上探索本文使用的確切版本。如果你要嘗試,建議先從一個「失敗與回退」的合成樣板開始。完美的 happy path,是對除錯器最沒意思的測試。