tiny-agent 從第一性原理打造可靠 Agent

第四部|走向 Production · 第 9 章

Committed Lifecycle 與 Phoenix Tracing

從 Session committed facts 投影跨語言 lifecycle,並用 OpenTelemetry、OpenInference 與 Phoenix 安全觀察 Agent trace。

約 22 分鐘 10 / 10

一句話掌握本章核心:tiny-agent 不直接把 scattered callbacks 當成 trace,而是先讓 append-only Session 提交事實,再由同一個 committed lifecycle projector 產生 JSON 與 OpenTelemetry/OpenInference 投影。Phoenix 只是讀取這份投影的 backend,不是執行結果或 recovery 的權威。

為什麼一般 callback tracing 不夠?

Agent 的一次工作不只是一個 HTTP request。它可能先建立或開啟 Session、連接 MCP、呼叫 model、接受多個 tool intent、執行外部 effect、再次呼叫 model,最後才完成 operation。過程也可能被 Esc 取消或在 process crash 後恢復。若每個 branch 各自發出 started/completed,就容易出現一個危險反轉:trace 已宣稱 tool 成功,但保存結果的 Session append 尚未成功。

tiny-agent 因此把「發生過什麼」分成三層。每層回答不同問題,不能互相取代:

層次回答的問題例子
Durable truthCrash 後仍可相信哪些 logical facts?runStarted、stepAttempt、toolStarted、result entry、operationFinished
Physical observation這個 process 此刻真的開始了哪個 effect?CLI/MCP startup、實際 tool execution attempt、recovery attach
Projection人與 backend 要如何觀察上述事實?--json lifecycle events、OTLP spans、Phoenix trace tree

Committed lifecycle projector 的資料流

四個語言版本使用同一個小型架構:Session append 成功後,把本次 committed facts 交給 lifecycle projector。只有沒有合適 durable terminal 的即時事件,才走 observation seam。Projector 再 fan out 到 JSON sink 與 OpenTelemetry sink。

CLI / Agent / Recovery
        │
        ├── startup、recovery attach、physical tool start ── observe(...)
        │
        └── Session append/write 成功 ──────────────────── committed(facts)
                                                              │
                                                Execution Lifecycle Projector
                                                   │                    │
                                                   ▼                    ▼
                                             JSON lifecycle       OTLP/OpenInference
                                                   │                    │
                                                stdout          Collector / Phoenix

Session JSONL + reducer ─────────────── recovery authority(不由右側反向修改)

這不是第二個 reducer。Projector 只辨識 lifecycle 需要的 fact subset、關聯 durable IDs、計算 duration 與確保 terminal 至多一次。transcript reconstruction、replay safety、recovery planning 仍留在 Session reducer 與 planner。

從 commit 推導 canonical ordering

一次包含 read tool 的正常 one-shot run,canonical lifecycle 順序如下。Startup 與 Agent operation 分開,因為 MCP setup 失敗時可能根本還沒有 committed operation:

startup.started
session.attached
startup.completed
operation.started          ← runStarted / compactionStarted 已提交
model.started              ← stepAttempt 已提交,model effect 才開始
 model.completed           ← assistant tool-call entry 已提交
tool.admitted              ← durable toolStarted intent 已提交
tool.started               ← 這個 process 的 physical execution attempt
 tool.completed            ← tool result entry 已提交
model.started
 model.completed           ← final assistant entry 已提交
operation.completed        ← operationFinished 已提交

tool.admitted 與 tool.started 刻意分開。前者代表 logical tool intent,使用可跨 crash 保留的 toolStartedId。後者代表某次 physical attempt,使用新的 attemptId。Safe replay 會沿用 logical identity,但不能假裝 crash 前後是同一次 effect。

Identity生命週期用途
sessionId跨多個 prompts 與 processConversation / Session correlation
operationId一次 run 或 compactionAgent root 與 durable operation
stepId、model attemptId一次 logical model attemptLLM span 與 retry correlation
toolStartedId一次 durable tool intent正常執行與 replay 共用的 logical identity
tool physical attemptId單一 process 內的一次 effect區分原執行與 crash 後 replay
toolCallIdProvider transcript 中的 tool call協定關聯,不當作全域 span identity

Cancellation、crash 與 recovery 如何呈現?

cancel.requested 是 observation event,不是 operation terminal。Esc 會先 durable append abort request,再 signal 目前的 model 或 tool。只有 operationFinished 提交後,projector 才發布 operation.completed(outcome=cancelled)。

Process crash 後,新的 process 無法可靠延續舊的 in-memory span context,也不應重播舊的 operation.started。它會發布 operation.recovered,沿用相同 durable operationId,但建立新的 recovery trace segment。若發現 crash 前未 reconcile 的 model attempt,會用 model.reconciled 描述恢復判斷,而不是虛構一個跨 process model span。

Shutdown 時仍開啟的 physical span 可以用 effect_unknown 收束,意思是「本 process 無法觀察 terminal」,不是替 Session 合成成功或失敗。這個區分讓 trace 誠實保留不確定性。

Phoenix 看到的 OpenInference trace tree

OTLP sink 將 canonical lifecycle 映射成兩棵不同用途的樹:startup root 包含 MCP connection spans。每個 committed run 或 compaction 則有自己的 Agent operation root。Interactive Session 的多個 prompts 是以 Session ID 關聯的 sibling operations,不建立一個長時間不結束的 Session span。

tiny.startup
└── tiny.mcp.connect

tiny.agent.operation                  openinference.span.kind = AGENT
├── tiny.model.request                openinference.span.kind = LLM
├── tiny.tool.execute                 openinference.span.kind = TOOL
└── tiny.model.request                openinference.span.kind = LLM

為了讓 Phoenix 與其他 AI observability backend 可直接分組,resource 同時設定 service.name 與 openinference.project.name。operation、model 與 tool spans 帶有 AGENT、LLM、TOOL kind。session.id 與 gen_ai.conversation.id 對應 durable Session,tiny.* attributes 則保留 operation、step、attempt 與 recovery identity。

Model name 只放在 LLM span,不放在 startup 或 tool span。Token 也保留兩種語意:OpenTelemetry/OpenInference 的 prompt tokens 是 uncached input 加上 cache read 與 cache write。tiny.usage.input_tokens 仍只代表 provider 回報的 uncached input,方便診斷 cache 行為而不混淆口徑。

Metadata-first 的隱私邊界

OTLP 預設只輸出名稱、IDs、duration、usage、outcome、recovery 與有限的 error type。它不輸出 prompt、model answer、tool arguments、tool results、error message、MCP/OTLP endpoint、request headers 或 credentials。這能降低意外外洩,但不等於 backend 已具備完整的資料治理。

動手使用 Phoenix backend

以下範例會在本機啟動 Phoenix,並讓 tiny-agent 直接以 OTLP/HTTP 匯出 traces。先在第一個 terminal 啟動 backend:

uvx --from arize-phoenix phoenix serve

Phoenix UI 與本機 OTLP endpoint 預設使用 http://localhost:6006。在第二個 terminal 設定 OpenRouter key 與標準 OpenTelemetry deployment environment,再執行任一語言版本:

export OPENROUTER_API_KEY=sk-or-...
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:6006
export OTEL_SERVICE_NAME=tiny-ts

tiny-ts "讀取 README 並摘要"

將 service name 與 CLI 一起替換,即可分別觀察四個 runtime:

OTEL_SERVICE_NAME=tiny-ts tiny-ts "讀取 README 並摘要"
OTEL_SERVICE_NAME=tiny-go tiny-go "讀取 README 並摘要"
OTEL_SERVICE_NAME=tiny-py tiny-py "讀取 README 並摘要"
OTEL_SERVICE_NAME=tiny-rs tiny-rs "讀取 README 並摘要"

開啟 Phoenix UI 後,以 project/service、Session ID 或 operation ID 篩選。應能看到 Agent root、LLM children、Tool children、duration、outcome 與 token usage。不應看到 prompt、answer 或 tool payload。若同一 Session crash 後恢復,應看到帶相同 durable operation identity 的新 recovery segment,而不是一條偽造的跨 process span。

Phoenix UI 顯示 tiny-ts Agent、模型與工具 Span 的實際追蹤畫面
圖 9.1:實際啟動 Phoenix 並執行 tiny-ts --plugin read 後的 Trace。中央 Trace Tree 依序顯示 tiny.agent.operation、兩次 tiny.model.request 與一次 tiny.tool.execute;右側則顯示由 committed lifecycle 投影而來的 durable identity 與 OpenInference attributes。

如何閱讀這張 Trace?

  1. 先看根節點:tiny.agent.operation 代表一次已提交的 Run,狀態為 OK,總延遲約 5.5 秒。
  2. 再看子節點順序:第一次 Model Request 提出 read Tool Call,Tool 執行後回填結果,第二次 Model Request 才產生最終回答。
  3. 最後核對 Attributes:tiny.operation.id、tiny.session.id、session.id 與 gen_ai.conversation.id 用來跨 Trace 關聯 durable Session;畫面中不應出現 README 內容、完整 Prompt 或模型答案。

如何驗證四語言沒有漂移?

Cross-language parity 不是比較 class 名稱,而是比較 observable contract。固定 fixture 應讓四個 CLI 產生相同的 event types、ordering、required fields、outcomes 與 durable identity 關係。各語言再以 in-memory exporter 或 loopback OTLP server 驗證 span tree 與 privacy allowlist。

  1. Lifecycle contract:正常 run 的 ordering 符合 v2 schema。logical terminal 只在 committed settlement 後出現。
  2. Recovery contract:發布 operation.recovered/model.reconciled,不重播歷史 start,也不合成跨 process context。
  3. Identity contract:durable IDs 可跨 recovery 關聯,physical tool attempt ID 每次 effect 都不同。
  4. Span contract:AGENT、LLM、TOOL parentage、Session attributes、model placement 與 token arithmetic 一致。
  5. Privacy contract:以 sentinel prompt、answer、arguments、result、error message、endpoint 與 header 驗證 exported attributes 中完全不存在它們。
  6. Failure isolation:sink throw、exporter failure 與 flush failure 不改變 CLI result或 durable Session。
# Repository 的離線驗證入口
make test
make check

# Book build 也會檢查章節 metadata、HTML 與 assets
npm --prefix book run build

最後的心智模型

從左到右記住這條單向資料流即可:effect intent 先 durable commit,執行 effect,settlement 再 durable commit,lifecycle 投影 committed truth,backend 最後才顯示它。Realtime 的正確定義不是「搶在 Session append 完成前宣稱成功」,而是 start 在 effect 前可見,terminal 在 authoritative settlement 寫入成功後立刻可見。

本章收束:Phoenix 讓 Agent 執行變得容易閱讀。committed lifecycle 則確保容易閱讀的畫面仍忠於可恢復的事實。前者回答「我們看見什麼」,後者守住「系統真正承諾了什麼」。