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

第一部|最小閉環 · 第 1 章

從 LLM 到 Agent

拆開 LLM、Agent 與 coding agent,建立 tiny-agent 採用的 model → tool → result 教學閉環。

約 14 分鐘 2 / 10

一句話掌握本章核心:在 tiny-agent 的工程模型中,單次模型推論不自行保存應用程式對話。Agent Runtime 則以「接收目標 → 呼叫模型 → 執行工具 → 寫回觀測 → 再判斷」形成閉環。本流程與 OODA 的概念相近,但不是 OODA 原典的四階段定義。OODA 正確順序是 Observe → Orient → Decide → Act。

LLM 不是 Agent

在計算機模型中,單次 LLM 呼叫可以高度抽象為純函式:output = model(messages)。

對 tiny-agent 使用的 OpenRouter Chat Completions 配接器而言,模型推論不自行記住上一次呼叫,也不直接具備讀檔、編譯或連網的 OS 權限。Host 會在下一次 Request 重送選入的 Messages。其他 Endpoint 也可能提供 Server-managed Conversation State,因此這是本專案的配接模式,不是所有 Model API 的普遍限制。

LLM 純函式與 Agent 閉環控制系統對比圖
圖 1:LLM 與 Agent 的工程邊界——模型負責根據有效 Context 產生輸出。Agent 則以 Transcript、Session 狀態與 Tool 副作用形成閉環系統。
比較維度純大型語言模型 (LLM)智能代理 (Agent)
計算本質可近似為推論函式 f(context) -> distribution/output以狀態機為核心的閉環控制系統 (Control Loop)
狀態延續模型不自行保存 tiny-agent 的對話。Endpoint 也可能提供 Server-managed State本專案由 Host 的 Messages、Durable Session 與 Operation State 維持
環境副作用零(僅產生字串或 JSON 資料)有(透過 Tool Adapter 執行真實 I/O 與 Shell 命令)
終止條件產生 Stop Token、達到長度上限或 Provider 中止正常情況是沒有 Tool Calls 且有非空答案。也可能因取消、截斷、Provider Error 或 Runtime Failure 結束

教學用核心閉環 (Canonical Agent Loop)

以下是教學用的 Canonical Loop 偽碼,保留「模型提出下一步、Host 執行工具並回填觀測」的核心資料流。生產實作仍需加入持久化、錯誤處理、取消與輸出邊界,Tool Calls 也可依協定與安全需求選擇循序或平行執行:

async function runAgentLoop(userText: string): Promise<string> {
    // 1. 初始化對話歷史,寫入使用者目標
    messages.push({ role: "user", content: userText });

    for (;;) {
        // 2. 呼叫模型,注入當前累積的所有 messages 與可用 tools 定義
        const answer = await callModel(messages, toolDefinitions);
        messages.push(answer);

        // 3. 終止判斷:如果模型沒有要求呼叫 Tool,代表推論完畢,回傳最終文字
        if (!answer.tool_calls?.length) {
            return answer.content;
        }

        // 4. 副作用執行:循序執行模型要求的 Tool,並將結果以 role: "tool" 寫回
        for (const call of answer.tool_calls) {
            const result = await tools[call.name].execute(call.arguments);
            messages.push({
                role: "tool",
                tool_call_id: call.id,
                content: result,
            });
        }
        // 5. 回到迴圈頂端,攜帶包含 Tool Results 的最新 messages 再次呼叫模型
    }
}
教學用 Agent 核心閉環流程圖
圖 2:最小 Agent 閉環的狀態機流程——推論、工具派發、執行守衛與 Append-only 寫回。
  1. messages

    模型對話的主要延續狀態

  2. model(messages)
  3. 有 tool_calls? 否 → 回傳 final answer,結束 是 → 執行 tool
  4. 依名稱找到 tool 並執行
  5. 把 tool result 寫回 messages
  6. 以更新後的 messages 再呼叫 model(messages)
圖 2:Canonical Agent Loop——上面教學偽碼的控制流程。
  1. user 輸入 messages = [user]
  2. model(messages) 此偽碼每次重送 messages。完整 Runtime 另有 Session、設定與 Operation 狀態
  3. assistant 回覆 tool_calls: [read README.md]
    messages = [userassistant]
  4. 執行 read result = "…檔案內容…"
  5. result 寫回 messages messages = [userassistanttool]
  6. 再次 model(messages) 這次看得到 tool result,回 final answer
動圖 1:同一個迴圈換成資料視角——messages 陣列如何隨時序單調遞增。

這段程式碼完全不包含任何「修復 Bug」或「分析商業營收」的特定領域規則。它只專注於機械式的狀態排程:呼叫模型 → 派發能力 → 保存結果 → 決定是否迭代。

Coding Agent 的四個基本原子能力

tiny-agent 選擇以下四個 Built-in Tools 作為常用的 Coding 能力。這是本專案的介面設計,而不是可證明的最小集合:單一 Shell Tool 也可能完成多種工作,較完整的產品則可能另需 Search、VCS 或遠端工具。

核心工具名稱本質功能系統對應操作安全性考量
read讀取 UTF-8 檔案內容Node readFile() 後分頁回傳限制回傳為 2,000 行 / 50KB。目前會先讀完整檔案,且不做 CWD 路徑限制
write建立新檔或覆寫檔案Node writeFile()會直接覆寫且可操作 CWD 外路徑,呼叫者與部署層須提供授權與隔離
edit針對指定字串區段精準置換精確字串比對與取代 (Exact Match)要求每個目標在原檔恰好出現一次且 Edit 不重疊
bash執行外部指令、編譯與測試Node exec() 經 Shell 啟動子行程 (Child Process)有 Timeout 與輸出截斷,但不是 OS Sandbox

工程真實面:撰寫這個幾十行的 Loop 很容易,真正的挑戰在於生產環境的可靠性——當遇到網路錯誤、使用者中途中斷、Token 暴增、程式 Crash 崩潰時,系統如何保持 Transcript 合法且能安全復原?這正是後續章節要解決的核心問題。

第一性原則的責任邊界分配

在設計 Agent 架構時,模組權責劃分必須極度清晰:

系統層級 唯一職責 (Must Do) 嚴格禁止 (Must Not Do)
Model (大腦) 根據目前上下文判斷下一步,產出自然語言或 Tool Call 提議 直接取得系統憑證、直接操作 OS 硬體
Agent Runtime (心臟) 調度迴圈、管理 Message 狀態機、捕捉例外 不應無意間耦合本可由 Tool / Workflow 模組承擔的領域政策。這是 tiny-agent 的設計取捨
Tool Adapter (手腳) 驗證參數、執行具體 I/O、捕獲錯誤並格式化回傳 超越 Host 授權邊界發動未定義的副作用
Session / WAL (記憶) Append-only 持久化記錄事件,支援 Crash 復原 在復原時盲目猜測未記錄的外部副作用狀態
CLI / Host (外殼) 提供使用者互動介面與 Plugin Allowlist。生產部署另須配置 OS Sandbox、資源與網路邊界 將部署安全責任推卸給模型。tiny-agent CLI 本身不建立 Container 或 VM Sandbox

動手驗證 (Hands-on Lab)

請在終端機中實際執行以下指令,觀察 Tool 邊界如何約束 Agent 的行為(需配置 OPENROUTER_API_KEY):

# 1. 賦予 read 權限:Agent 能夠讀取 README 並正常回答
tiny-ts --plugin read "讀取 README.md,說明 agent loop"

# 2. 在拋棄式目錄中只賦予 write。Request 不會出現 read Tool Schema
scratch=$(mktemp -d)
printf '# disposable fixture\n' > "$scratch/README.md"
(cd "$scratch" && tiny-ts --plugin write "不要修改檔案。若無法讀取 README.md,說明缺少哪個工具")

核心收穫:Agent 的可派發能力由本次 Request 中 Host 實際注入並支援的 Tool Schemas 決定。第二個命令可驗證 Runtime 不會派發未啟用的 read。模型是否以特定措辭拒絕仍具非確定性,不能把自然語言反應當成權限測試。

上方教學偽碼若只有 Heap 中的 messages,Process 結束便會失去狀態。但本 repo 的 TypeScript reference implementation 已要求 SessionStore,並在 Run、Model Attempt、Assistant Message 與 Tool Result 等邊界 Append JSONL Facts。下一章先探討 Message 的規範化 (Normalization) 與 Provider Adapter,再於後續章節完整說明 Durable Session。