1. Agent 核心实现 - Agent Loop
Agent 和 Chat 的区别
很多人对 Agent 的第一反应是:“不就是 Chat 加了个插件系统吗?” 你跟 Chat 说话,它回你文本;你跟 Agent 说话,它不光回你文本,中间还可能弹几个”调用工具中”——看起来确实像 Chat 多了几个按钮。
但如果你这样想,可以换一个角度:
- Chat 是一个百科全书编辑。你问,它查,它写答案。工作结束。
- Agent 是一个刚入行的实习生。你交代任务,他得自己在工位上琢磨、翻文件、跑命令、看结果、判断”搞定了没”,搞不定就再来一圈。
| 维度 | Chat | Agent |
|---|---|---|
| 交互模式 | 问答,用户驱动 | 自主,目标驱动 |
| 能力边界 | 生成文本、图片、视频 | 可以通过调用工具影响真实世界 |
| 执行流程 | 用户提问 → 模型回答 | 用户下达任务 → 思考 → 调用工具 → 观察 → 思考 → … → 返回答案 |
| 状态管理 | 每轮对话独立 | 维护完整的消息历史,包含工具调用与返回结果 |
| 自主性 | 无 | 模型自己决定下一步干什么、用什么工具、什么时候算完 |
Chat 不会在你没要求的情况下去读你的文件。Agent 会。Chat 不会自己判断”我已经回答完了”。Agent 需要一条明确的终止条件,否则它能一直跑下去。
Agent 的核心就四个零件
1. LLM 客户端初始化 // 怎么跟大模型说话
2. 工具定义(Tool Schema) // 告诉大模型它能用哪些工具
3. 工具实现(Tool Functions) // 工具背后的实际代码
4. Agent 循环(Core Loop) // 把上面几个零件串起来的调度逻辑没有数据库,没有向量检索,没有多 Agent 编排。任何一个成熟的 Agent 框架——Claude Code、OpenCode、OpenClaw——核心循环跟这四个零件没有本质区别。
Agent 循环在做什么
把用户任务发给大模型 → 大模型说”我要调某个工具” → 你在本地执行 → 把执行结果塞回对话 → 再发给大模型 → 它看了结果说”再调一个工具” → 你执行,再塞回去 → … 直到大模型觉得任务完成了,返回纯文本,退出。
拿一个具体例子串一遍:
- 用户输入”统计当前文件夹的文件数,然后写入 count.txt”
- [第 1 轮] 把这句话发给 LLM。LLM 没返回文字,返回了一段 JSON:
{ "function": "bash", "arguments": { "command": "ls | wc -l" } }。它在说:“我不知道文件数,查一下” - 你在本地跑
ls | wc -l,拿到结果42 - 把
42追加到对话历史 - [第 2 轮] 把完整对话(用户任务 + 上一轮的 tool_call + 执行结果
42)再次发给 LLM。LLM 看了结果是 42,判断下一步该写文件:{ "function": "write", "arguments": { "path": "count.txt", "content": "42" } } - 你在本地执行写文件
- 把写入成功的信息追加进去
- [第 3 轮] 再次发给 LLM。LLM 看到写入成功,判断任务完成,返回纯文本:“已创建 count.txt,内容为 42”
- 没有 tool_call 了,退出,用户收到消息
注意:LLM 从头到尾没有”执行”过任何东西。 它只是输出 JSON,说”我想调这个函数,参数是这些”。真正的 bash 命令是你的 Node.js 进程跑的。LLM 是大脑,你的代码是手脚。
直接看代码
import { OpenAI } from "openai";
import { tools, toolsMap } from "./tools";
type ChatMessage = OpenAI.Chat.Completions.ChatCompletionMessageParam;
async function callLLM(messages: ChatMessage[]) {
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL: process.env.OPENAI_BASE_URL });
const resp = await client.chat.completions.create({ model: "gpt-4o", messages, tools });
return resp.choices[0]?.message;
}
async function agentLoop(userInput: string, maxIterations = 20) {
const messages: ChatMessage[] = [
{ role: "system", content: "You are a helpful assistant. Use bash commands when needed." },
{ role: "user", content: userInput },
];
for (let i = 0; i < maxIterations; i++) {
const msg = await callLLM(messages);
messages.push(msg);
if (!msg?.tool_calls?.length) break; // LLM 觉得任务完成了
for (const toolCall of msg.tool_calls) {
const { name, arguments: rawArgs } = toolCall.function;
const output = await toolsMap[name](JSON.parse(rawArgs || "{}"));
messages.push({ role: "tool", tool_call_id: toolCall.id, content: output });
}
}
return messages;
}
// 一行启动
agentLoop(process.argv.slice(2).join(" ") || "List files in current directory");代码里几个点
tools 怎么传?
tools 是一个 JSON Schema 数组,跟 messages 一起传给 LLM:
const tools = [
{
type: "function",
function: {
name: "bash",
description: "Execute a bash command",
parameters: {
type: "object",
properties: {
command: { type: "string", description: "The command to execute" }
},
required: ["command"]
}
}
},
// read, write, edit ...
];LLM 收到这个 Schema,在需要时输出 JSON 告诉你调哪个函数、传什么参数。你的进程解析 JSON 并真正执行,然后拼回 messages。
为什么是 for 不是 while?
maxIterations 是硬上限。LLM 有时候会反复调同一个工具、拿同样的错误、不知道停。这个上限等于在一个撞墙的机器人面前放了一堵更厚的墙——撞到这里,停。
生产环境里 maxIterations 通常会设更大(50、100),而且会叠更细的终止条件:连续 N 轮返回同样的 tool_call 就掐掉,token 消耗超阈值就告警。
核心工具
理论上你只给一个 bash,LLM 就能干任何事。但实际用起来不是这回事——工具越通用,LLM 行为越发散。不给 Read,它用 cat 读;不给 Write,它用 echo 写。所以工具设计的原则是:每个工具只做一件事,这件事 LLM 能稳定用对。
| 工具 | 作用 | 备注 |
|---|---|---|
bash | 执行 shell 命令 | 万能,但需要其他工具来约束发散 |
read | 读取文件内容 | 编程 Agent 的起点——不给读文件,就是蒙眼写代码 |
write | 写入文件(全量覆盖) | read + write 就是一个 code agent 原型。文件大了之后全量覆盖会让 context 炸 |
edit | 精确编辑(增量修改) | 解决 write 的 context 膨胀问题。配合 diff 做版本对比 |
两个常见问题
为什么设置 maxIterations?
见过 Agent 调一个命令失败了,下一轮调同样的命令,再失败,再调……死循环。maxIterations 就是来喊停的。
LLM 没有”我已经试了三次该换个办法”这种反思能力。它只看当前的 messages 快照,不知道自己在同一个坑里转了多久。maxIterations 是一种外挂——在 LLM 反应过来之前,替它停下来。
LLM 真的在”执行”工具吗?
没有。LLM 做的事全程只有一件:接收文本,输出文本。
它”调用工具”的时候,只是输出了一段 JSON 说我想调什么。你的代码读到这段 JSON,在本地执行,再把结果追加回去。LLM 永远碰不到你的系统。它怎么”行动”,完全由你的代码决定。
回头看:Agent 到底是什么
Agent 的底层就是三个步骤的循环:
- 感知 — 通过工具获取外部信息(读文件、跑命令、查 API)
- 决策 — LLM 根据已有信息,决定下一步
- 行动 — 通过工具影响外部环境(写文件、发请求、创建目录)
然后回到 1,再来一圈。直到 LLM 说:“我做完了。”
用 Claude Code 的时候,它在创建文件、执行测试、读项目结构——背后就是这个循环在转。你让它”修这个 bug”,它可能绕十几圈。每一圈你看到的 tool_call,就是这个 for 循环跑了一轮。