第一部|最小閉環 · 第 1 章
從 LLM 到 Agent
拆開 LLM、Agent 與 coding agent,建立 tiny-agent 採用的 model → tool → result 教學閉環。
一句話掌握本章核心:在 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) |
|---|---|---|
| 計算本質 | 可近似為推論函式 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 再次呼叫模型
}
}
-
messages
模型對話的主要延續狀態
- model(messages)
- 有 tool_calls? 否 → 回傳 final answer,結束 是 → 執行 tool
- 依名稱找到 tool 並執行
- 把 tool result 寫回 messages
- 以更新後的 messages 再呼叫 model(messages)
- user 輸入 messages = [user]
- model(messages) 此偽碼每次重送 messages。完整 Runtime 另有 Session、設定與 Operation 狀態
-
assistant 回覆
tool_calls: [read README.md]
messages = [userassistant] - 執行 read result = "…檔案內容…"
- result 寫回 messages messages = [userassistanttool]
- 再次 model(messages) 這次看得到 tool result,回 final answer
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。