新手刚开始用 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。