新手剛開始用 LLM 蓋東西時,通常一上來就抓 agent 框架 —— LangGraph、CrewAI、Mastra、AutoGen —— 然後立刻被 nodes、edges、state schema、各種抽象層搞到迷路。半年後才發現,框架在解的是它自己的問題,而你真正的問題它沒解。
從零寫一次 agent loop 會讓你真的搞懂「agent」是什麼。寫完一次以後,你才會知道你需不需要框架。劇透:80% 的產品不需要。
agent 真實的樣子
單一 LLM 呼叫回答一個問題就停。agent 就是把 LLM 放在一個 loop 裡,給它工具。就這樣。虛擬碼:
while not done:
response = llm.generate(messages, tools)
if response.is_final:
return response.text
for tool_call in response.tool_calls:
result = execute(tool_call)
messages.append(tool_result(result))
其他東西 —— 多 agent 編排、routing、supervisor —— 都是這個 loop 外面包一層。把 loop 寫對,其他都是裝飾。
一個能跑的範例:研究型 agent
來蓋一個小 agent,能用搜尋回答事實問題。兩個工具:search(query) 跟 fetch(url)。我們用 Anthropic 的 TypeScript SDK,但 OpenAI / Gemini 形狀基本上一樣。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const tools = [
{
name: "search",
description: "搜尋網路。回傳前 5 筆結果,含標題、URL、片段。",
input_schema: {
type: "object",
properties: { query: { type: "string" } },
required: ["query"],
},
},
{
name: "fetch",
description: "抓一個網頁的全文,最多 5000 字。",
input_schema: {
type: "object",
properties: { url: { type: "string" } },
required: ["url"],
},
},
];
async function executeTool(name: string, input: any): Promise<string> {
if (name === "search") return await webSearch(input.query);
if (name === "fetch") return await fetchPage(input.url);
return `Unknown tool: ${name}`;
}
async function runAgent(question: string, maxSteps = 10) {
const messages: any[] = [{ role: "user", content: question }];
for (let step = 0; step < maxSteps; step++) {
const response = await client.messages.create({
model: "claude-sonnet-4-7",
max_tokens: 4096,
tools,
messages,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason === "end_turn") {
const textBlock = response.content.find((b) => b.type === "text");
return textBlock?.text ?? "";
}
if (response.stop_reason === "tool_use") {
const toolResults = [];
for (const block of response.content) {
if (block.type !== "tool_use") continue;
const result = await executeTool(block.name, block.input);
toolResults.push({
type: "tool_result",
tool_use_id: block.id,
content: result,
});
}
messages.push({ role: "user", content: toolResults });
continue;
}
throw new Error(`Unexpected stop_reason: ${response.stop_reason}`);
}
throw new Error(`Agent exceeded ${maxSteps} steps`);
}
這就是整個 agent。42 行。執行 runAgent("Anthropic 在 2026 年三月公佈了什麼?"),看模型自己搜尋、讀網頁、整合答案。
loop 一步一步在幹嘛
- 把使用者問題加上工具 schema 送給模型。
- 模型決定:資訊夠了直接回答,還是要呼叫工具?
- 如果
stop_reason === "end_turn"就是答完了,return。 - 如果
stop_reason === "tool_use"就是它要呼叫一個或多個工具。執行每一個,結果 append 進 message 歷史。 - 繼續迴圈。下一輪模型會看到自己之前的呼叫跟結果。
模型沒有「呼叫」工具 —— 是你的程式在呼叫。模型只是吐出一個結構化請求,你跑真正的函式,把結果 append 回去讓它下一輪看到。
五件會出包的事
自己寫一次會體驗到所有框架在抽象掉的失敗模式:
- 無限迴圈。 沒有
maxSteps,一個一直要呼叫工具的 agent(或一直撞同一個失敗工具)會把你的預算燒光。永遠要有上限。 - token 膨脹。 每一輪 message 歷史都會長,跑久了會撞 context window。解法:摘要舊輪次、把不需要的工具結果丟掉、或用 prompt caching。
- 工具錯誤不能拋例外。
fetch(url)404 的時候,要把錯誤訊息當 tool result 回傳,不能 throw。throw 會殺掉整個 loop;模型其實能從錯誤訊息復原。 - JSON schema 漂移。 工具的 input schema 寫得鬆,模型會傳怪輸入進來。schema 寫嚴格、用 enum、server 端再驗一次。
- 停止條件。 「答完了」比想像中難。模型有時候會想問澄清問題(沒 tool_use 但也沒答完)。仔細讀
stop_reason。
真的需要再加上的東西
基本 loop 跑起來之後,你會慢慢想要:
- Streaming。 邊跑邊把模型的思考顯示出來。Anthropic 的 stream API 直接給你,buffer text delta 就好。
- System prompt。 角色設定、輸出格式、拒答規則。放在
system參數,不是messages裡。 - 記憶。 長期對話需要摘要或 vector recall。第一天不需要拿記憶函式庫 —— 把
messages存到檔案再讀回來就夠了。 - 追蹤 / log。 印出每次模型輸出、每次工具呼叫、每次結果。你會讀這些 log 幾百次。
- 成本追蹤。 每次
response.usage有 token 數,加總起來。
什麼時候不要自己寫
- 多 agent 協同共享狀態。 五個 agent 要透過 graph 協同,有重試、平行分支、human-in-the-loop,LangGraph 真的在解真問題。先自己寫一次,等真的痛了再升級。
- 你想要現成的 observability。 LangSmith / Langfuse 直接給你 UI。如果你不打算自己蓋 dashboard,值得用。
- >5 人團隊一起寫 agent 程式碼。 共用框架可以給你新人 onboarding 文件、type hint、共同詞彙。
單一產品有一兩個專業 agent,從零寫的 loop 出貨快、debug 容易、換模型不用重寫整個堆疊。
你真正學到的東西
寫完這個 loop 之後,「agent」這整件事就不神祕了。多 agent 系統?就是一個 loop 把另一個 loop 當工具呼叫。Planning agent?同一個 loop 加 planner_step 跟 executor_step 工具。自我修正?同一個 loop 加一個叫 critic 的工具。
框架不是錯的,只是比你以為的早。先寫 50 行版本。
下一步
- Anthropic 官方 tool use 文件。
- Building effective agents —— Anthropic 部落格,2024 年 12 月。讀兩遍。
- 查這些詞:ReAct prompting、function calling、MCP for tool sharing、agent observability。