Agent 最小内核:模型、循环与工具
Agent 看起来很神秘:它能读代码、改文件、跑命令,遇到错误还能换一种方式重试。但把最小实现拆开,核心并不复杂——一个能工作的 Agent,最小内核只有三件事:模型、循环、工具。
这篇文章不停留在概念层面。读完以后,你应该能看懂任何 Agent 框架的核心循环,能判断一个 SDK 帮你封装了什么、省掉了什么、藏了什么坑。
1. 从聊天机器人到 Agent:差了什么
最简单的大模型应用就是命令行聊天程序:用户输入一句话,程序把它连同历史会话发给模型,模型返回文本,程序打印出来。
这里有个容易忽略的事实:大模型服务不替你保存会话状态。模型之所以”记得”之前聊过什么,是因为你的程序每次把完整的历史消息重新发过去。模型本身是无状态函数——输入消息列表,输出下一条消息。
1 | 用户输入 |
但这还只是聊天机器人。它可以说”你应该打开 main.go 看一下”,但它自己看不到 main.go;它可以说”把这个函数改掉”,但它自己不能写入文件。
Agent 和聊天机器人的分界线是工具。一旦模型能通过工具触达真实世界——读文件、写文件、跑命令、搜索网页——它就从”会说话”变成了”会做事”。
那么,Agent 的结构到底长什么样?
2. Agent 的最小公式
1 | Agent = LLM + Loop + Tools |
三个组件,职责边界清晰:
| 组件 | 职责 | 不负责 |
|---|---|---|
| LLM(模型) | 理解任务、判断下一步、生成工具调用参数 | 不执行任何真实操作,不持久化状态 |
| Loop(循环) | 持续把消息送给模型、分发工具调用、收集结果 | 不做任何”智能”判断,不替模型决策 |
| Tools(工具) | 执行具体动作:读文件、写文件、跑命令 | 不判断该不该执行,不决定执行顺序 |
运行时的数据流如下:
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#3B82F6', 'primaryTextColor': '#1E3A5F', 'primaryBorderColor': '#2563EB', 'lineColor': '#60A5FA', 'secondaryColor': '#10B981', 'tertiaryColor': '#F59E0B'}}}%%
flowchart LR
U["用户输入"] --> Loop["Loop(循环)"]
Loop -->|"发送 conversation"| LLM["LLM(模型)"]
LLM -->|"tool_use"| Loop
Loop -->|"分发调用"| Tools["Tools(工具)"]
Tools -->|"返回结果"| Loop
LLM -->|"end_turn"| Loop
Loop -->|"最终回答"| U常见误解有两个:
误解一:”Agent 的聪明来自外层代码。”不是。外层程序几乎没有业务逻辑,它只是忠实地执行模型的指令。Agent 之所以让人觉得”聪明”,是因为模型本身具备了很强的任务理解和规划能力。
误解二:”工具越多,Agent 越强。”也不是。工具太多,模型选择困难,反而容易出错。好的 Agent 设计用最少的工具覆盖最多的场景。Claude Code 的核心工具也就十来个。
理解了公式中三个组件的职责后,接下来看它们之间的通信协议——工具调用的具体过程。
3. 工具调用的协议细节
工具调用不是”约定”那么模糊——它是模型 API 里的一套结构化协议。整个过程分四步。
3.1 注册工具定义
发起 API 请求时,把可用工具的定义传给模型。定义包含工具名、功能描述、参数的 JSON Schema:
1 | { |
这段定义会被注入模型的上下文。模型看到它,就知道自己有 read_file 这个能力可用。
工具描述直接影响模型行为。描述写得清楚,模型更容易在正确时机调用;描述写得含糊,模型就可能乱用或不用。这不是文档,是给模型看的指令。
3.2 模型返回工具调用
当模型判断需要使用工具时,返回的不是普通文本,而是结构化的工具调用请求:
1 | { |
注意 stop_reason 这时候是 tool_use 而不是 end_turn。这是程序判断”模型想用工具”还是”模型说完了”的关键信号。
3.3 程序执行工具
程序收到工具调用请求后,在本地找到对应的函数并执行,拿到结果。
这一步发生在你的程序里,不在模型侧。模型只是”提出请求”,真正碰文件、跑命令的是你的代码。
3.4 结果回注上下文
把工具执行结果作为 tool_result 消息追加到 conversation,再次发给模型:
1 | { |
模型拿到文件内容后继续推理——可能直接回答用户,也可能再发起下一个工具调用。
整个流程画出来:
sequenceDiagram
participant U as 用户
participant P as 程序 Agent Loop
participant M as 模型 LLM
participant T as 工具
U->>P: 用户输入任务
P->>M: 发送 conversation + 工具定义
M->>P: 返回 tool_use read_file
P->>T: 执行 read_file main.go
T->>P: 返回文件内容
P->>M: 发送 tool_result + 历史上下文
M->>P: 返回 tool_use edit_file
P->>T: 执行 edit_file ...
T->>P: 返回执行结果
P->>M: 发送 tool_result + 历史上下文
M->>P: 返回 end_turn 最终回答
P->>U: 展示结果协议本身不复杂,但模型到底需要调用哪些工具才能完成任务?接下来用三个最小工具说明。
4. 三个核心工具:从观察到行动
一个代码编辑 Agent 的最小工具集只需要三个:list_files、read_file、edit_file。
4.1 list_files:知道环境里有什么
人进入一个陌生项目,第一件事是 ls 看目录结构。Agent 也一样。
1 | func ListFiles(dir string) ([]string, error) { |
4.2 read_file:看到真实内容
有了目录结构,模型还需要能读取文件内容,才能基于事实推理,而不是靠训练记忆猜。
1 | func ReadFile(path string) (string, error) { |
4.3 edit_file:把想法变成改动
最小实现可以很朴素——字符串替换:
1 | func EditFile(path, oldStr, newStr string) error { |
三个工具组合在一起,模型就能完成完整的代码编辑流程。比如用户问”这个项目用的 Go 版本是多少”:
- 调用
list_files("."),看到有go.mod。 - 调用
read_file("go.mod"),读取内容。 - 从内容中找到 Go 版本,直接回答。
关键不在于工具多复杂,而在于模型能自己决定调用顺序。程序没有硬编码”如果用户问 Go 版本,就读取 go.mod”。程序只提供工具,模型自己选择。
工具定义好了,接下来看把它们串起来的 Agent Loop 怎么写。
5. 用 Go 实现一个最小 Agent Loop
下面是一个可以跑起来的 Agent Loop 骨架,完整展示”循环 + 工具分发”的核心逻辑:
1 | const maxIterations = 20 |
核心逻辑只有一个 for 循环和一个 if:
- 模型说
end_turn→ 循环结束,返回结果。 - 模型说
tool_use→ 执行工具,把结果塞回 conversation,继续循环。
Claude Code、Cursor、Cline 这些产品的核心循环,和上面这段代码在结构上没有本质区别。区别在循环之外:UI 怎么展示、权限怎么控制、上下文怎么压缩。
理解了代码层面的实现,再回到日常使用体验——你在 Claude Code 里看到的每个行为,都能映射到这套内核。
6. 你在 Claude Code 里看到的,对应内核里的什么
如果你每天都在用 Claude Code 或 Cursor,下面这些日常体验可以直接映射到内核组件:
| 你看到的现象 | 内核里的对应 |
|---|---|
Claude 先 list_dir 再 read_file,然后才回答 | 模型在 Loop 里连续发起多轮工具调用 |
| 修改文件前弹出确认框 | Loop 在执行写入类工具前加了权限拦截 |
| 上下文太长时 Claude 说”让我总结一下之前的对话” | Loop 做了上下文压缩,把旧消息摘要后替换 |
| 执行命令报错后 Claude 自动换了一种方式重试 | 工具返回了 IsError: true,模型基于错误信息重新规划 |
Claude 说”我需要先安装依赖”然后跑 npm install | 模型判断当前任务依赖前置步骤,主动拆解子任务 |
| Cursor 的 Tab 补全 vs Agent 模式 | 补全是单次推理,Agent 模式才有 Loop |
这张表的价值在于:一旦理解了内核,产品行为就不再是黑盒。你知道它为什么这么做,也知道它在什么情况下会出问题。
从日常体验映射到内核之后,再来看把最小 Agent 推向生产环境时,必然会遇到哪些工程痛点。
7. 生产级 Agent 的核心痛点
最小内核简单,不代表生产级 Agent 简单。以下是 80% 场景必然遇到的三个核心问题。
7.1 上下文窗口管理
这是最难的问题。
模型的上下文窗口有硬上限(Claude 是 200K tokens)。一个真实编码任务可能涉及几十个文件、多轮工具调用,上下文很快就会爆。
常见策略分三级:
第一级:控制输入。工具返回的内容做截断——文件太长只返回前 N 行,目录太深只列前两层。大部分 Agent 框架在工具层就做了限制。
第二级:滑动窗口。当 conversation 超过阈值时,保留 system prompt 和最近 N 轮消息,中间部分丢弃或压缩。
第三级:摘要压缩。把早期的多轮对话调用另一次模型生成摘要,用摘要替换原始消息。Claude Code 在上下文接近上限时会做这一步。
每一级都有代价:截断可能丢关键信息,滑动窗口可能让模型忘记早期决策,摘要压缩引入延迟和额外成本。没有完美方案,只有根据场景选择的 trade-off。
7.2 错误反馈闭环
工具调用失败是常态,不是异常。文件不存在、命令执行报错、权限不够——这些都是正常情况。
关键在于:错误信息的质量直接决定模型能否自我修正。
做得好的 Agent 把完整的错误信息(包括 stderr、错误码、堆栈)原样喂给模型。模型看到 permission denied 就知道要换路径或提权,看到 file not found 就知道要先 list_files 确认路径。做得差的只返回一个 "failed",模型只能盲目重试或放弃。
回顾前面的 Go 代码,ToolResult 里的 IsError 字段告诉模型”这不是正常结果,是一个错误”。模型的行为模式会从”继续推进”切换到”分析错误原因并修正”。这个字段看起来不起眼,但它是 Agent 遇错自修复的底层机制。
真正成熟的验证闭环还会在修改完代码后自动跑测试,把测试结果回注上下文。如果测试失败就继续修复,构成完整的”改代码 → 跑测试 → 看结果 → 再改”自动迭代循环。
7.3 工具权限与人机确认
read_file 只是读取,风险可控。但 edit_file 会改文件,run_command 能执行任意命令——这些需要权限控制。
生产级 Agent 通常把工具分成三个安全等级:
| 等级 | 典型工具 | 处理方式 |
|---|---|---|
| 只读 | list_files、read_file、search | 自动执行,无需确认 |
| 有限写入 | edit_file(已有文件的小改动) | 展示 diff,用户可选自动或手动确认 |
| 高危 | run_command、delete_file | 每次弹窗确认,或在沙箱中执行 |
Claude Code 的做法:读取类工具直接跑,写入类工具默认弹确认,用户也可以设置白名单实现自动确认。这个分级逻辑不在模型里,而是在 Loop 层实现——Loop 在收到模型的工具调用请求后,先判断权限等级,决定直接执行还是先问用户。
解决了生产级痛点之后,回到全局视角——Agent 最小内核和生态里其他概念是什么关系?
8. 和 MCP、Skills、Agent SDK 的关系
这篇文章讲的是 Agent 的最小内核。MCP、Skills、Agent SDK 是在这个内核之上的工程化封装:
- MCP 解决工具如何标准化暴露——让不同应用通过统一协议连接不同工具源。
- Skills 解决能力如何按需加载——模型先看简短描述,需要时再加载完整指令和资源。
- Agent SDK 解决运行时——把 Loop、工具执行、上下文管理、会话控制封装起来。
无论封装多复杂,底层仍然是同一条主线:
1 | 模型判断下一步 -> 请求工具 -> 程序执行工具 -> 结果回到上下文 -> 模型继续判断 |
这些概念的详细解析,参见本系列的 MCP 协议详解、Claude Agent Skills 深度解析 和 Agent 与 Subagent 从概念到实践。
9. 小结
读懂 Agent,先不要从框架开始。先抓住最小内核:
1 | Agent = 模型 + 循环 + 工具 |
- 模型负责判断下一步,但不执行任何真实操作。
- 循环负责持续推进,但不做任何智能决策。
- 工具负责接触真实世界,但不决定何时该用。
普通聊天机器人只能生成文本。Agent 在循环里调用工具,把”想法”变成”动作”。
一旦看懂了这个内核,所有 Agent 产品的行为——Claude Code 的多步推理、Cursor 的自动修复、任何 Agent SDK 的 API 设计——都不再是黑盒。你能看到它在循环的哪一步,用的哪个工具,为什么这样决策。