Agent 最小内核:模型、循环与工具

Agent 看起来很神秘:它能读代码、改文件、跑命令,遇到错误还能换一种方式重试。但把最小实现拆开,核心并不复杂——一个能工作的 Agent,最小内核只有三件事:模型、循环、工具。

这篇文章不停留在概念层面。读完以后,你应该能看懂任何 Agent 框架的核心循环,能判断一个 SDK 帮你封装了什么、省掉了什么、藏了什么坑。

1. 从聊天机器人到 Agent:差了什么

最简单的大模型应用就是命令行聊天程序:用户输入一句话,程序把它连同历史会话发给模型,模型返回文本,程序打印出来。

这里有个容易忽略的事实:大模型服务不替你保存会话状态。模型之所以”记得”之前聊过什么,是因为你的程序每次把完整的历史消息重新发过去。模型本身是无状态函数——输入消息列表,输出下一条消息。

1
2
3
4
5
6
用户输入
-> 加入 conversation
-> 发给模型
-> 模型回复
-> 回复也加入 conversation
-> 等下一轮输入

但这还只是聊天机器人。它可以说”你应该打开 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
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "read_file",
"description": "读取指定路径的文件内容。用于查看代码、配置文件或任何文本文件。",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要读取的文件路径"
}
},
"required": ["path"]
}
}

这段定义会被注入模型的上下文。模型看到它,就知道自己有 read_file 这个能力可用。

工具描述直接影响模型行为。描述写得清楚,模型更容易在正确时机调用;描述写得含糊,模型就可能乱用或不用。这不是文档,是给模型看的指令。

3.2 模型返回工具调用

当模型判断需要使用工具时,返回的不是普通文本,而是结构化的工具调用请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lqs136",
"name": "read_file",
"input": {
"path": "main.go"
}
}
]
}

注意 stop_reason 这时候是 tool_use 而不是 end_turn。这是程序判断”模型想用工具”还是”模型说完了”的关键信号。

3.3 程序执行工具

程序收到工具调用请求后,在本地找到对应的函数并执行,拿到结果。

这一步发生在你的程序里,不在模型侧。模型只是”提出请求”,真正碰文件、跑命令的是你的代码。

3.4 结果回注上下文

把工具执行结果作为 tool_result 消息追加到 conversation,再次发给模型:

1
2
3
4
5
6
7
8
9
10
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lqs136",
"content": "package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"hello\")\n}"
}
]
}

模型拿到文件内容后继续推理——可能直接回答用户,也可能再发起下一个工具调用。

整个流程画出来:

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_filesread_fileedit_file

4.1 list_files:知道环境里有什么

人进入一个陌生项目,第一件事是 ls 看目录结构。Agent 也一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func ListFiles(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
var names []string
for _, e := range entries {
name := e.Name()
if e.IsDir() {
name += "/"
}
names = append(names, name)
}
return names, nil
}

4.2 read_file:看到真实内容

有了目录结构,模型还需要能读取文件内容,才能基于事实推理,而不是靠训练记忆猜。

1
2
3
4
5
6
7
func ReadFile(path string) (string, error) {
content, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(content), nil
}

4.3 edit_file:把想法变成改动

最小实现可以很朴素——字符串替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func EditFile(path, oldStr, newStr string) error {
content, err := os.ReadFile(path)
if err != nil {
if oldStr == "" {
// 文件不存在且 oldStr 为空,创建新文件
return os.WriteFile(path, []byte(newStr), 0644)
}
return err
}
s := string(content)
if !strings.Contains(s, oldStr) {
return fmt.Errorf("未找到要替换的内容")
}
s = strings.Replace(s, oldStr, newStr, 1)
return os.WriteFile(path, []byte(s), 0644)
}

三个工具组合在一起,模型就能完成完整的代码编辑流程。比如用户问”这个项目用的 Go 版本是多少”:

  1. 调用 list_files("."),看到有 go.mod
  2. 调用 read_file("go.mod"),读取内容。
  3. 从内容中找到 Go 版本,直接回答。

关键不在于工具多复杂,而在于模型能自己决定调用顺序。程序没有硬编码”如果用户问 Go 版本,就读取 go.mod”。程序只提供工具,模型自己选择。

工具定义好了,接下来看把它们串起来的 Agent Loop 怎么写。

5. 用 Go 实现一个最小 Agent Loop

下面是一个可以跑起来的 Agent Loop 骨架,完整展示”循环 + 工具分发”的核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const maxIterations = 20

func RunAgent(client *anthropic.Client, userTask string, tools []Tool) (string, error) {
// 1. 初始化 conversation
conversation := []Message{
{Role: "user", Content: userTask},
}

// 2. 构建工具定义(传给模型)
toolDefs := buildToolDefinitions(tools)
toolMap := buildToolMap(tools) // name -> 执行函数

// 3. Agent Loop
for i := 0; i < maxIterations; i++ {
// 发给模型
resp, err := client.CreateMessage(conversation, toolDefs)
if err != nil {
return "", fmt.Errorf("模型调用失败: %w", err)
}

// 把模型回复加入上下文
conversation = append(conversation, resp.AsMessage())

// 判断是否结束
if resp.StopReason == "end_turn" {
return resp.TextContent(), nil
}

// 模型请求了工具调用
if resp.StopReason == "tool_use" {
var toolResults []ToolResult
for _, block := range resp.ToolUseBlocks() {
fn, ok := toolMap[block.Name]
if !ok {
toolResults = append(toolResults, ToolResult{
ID: block.ID,
Content: fmt.Sprintf("未知工具: %s", block.Name),
IsError: true,
})
continue
}

// 执行工具
result, execErr := fn(block.Input)
if execErr != nil {
toolResults = append(toolResults, ToolResult{
ID: block.ID,
Content: fmt.Sprintf("执行失败: %s", execErr.Error()),
IsError: true,
})
} else {
toolResults = append(toolResults, ToolResult{
ID: block.ID,
Content: result,
})
}
}

// 工具结果加入 conversation
conversation = append(conversation, toolResultsMessage(toolResults))
}
}

return "", fmt.Errorf("超过最大迭代次数 %d", maxIterations)
}

核心逻辑只有一个 for 循环和一个 if

  • 模型说 end_turn → 循环结束,返回结果。
  • 模型说 tool_use → 执行工具,把结果塞回 conversation,继续循环。

Claude Code、Cursor、Cline 这些产品的核心循环,和上面这段代码在结构上没有本质区别。区别在循环之外:UI 怎么展示、权限怎么控制、上下文怎么压缩。

理解了代码层面的实现,再回到日常使用体验——你在 Claude Code 里看到的每个行为,都能映射到这套内核。

6. 你在 Claude Code 里看到的,对应内核里的什么

如果你每天都在用 Claude Code 或 Cursor,下面这些日常体验可以直接映射到内核组件:

你看到的现象内核里的对应
Claude 先 list_dirread_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_filesread_filesearch自动执行,无需确认
有限写入edit_file(已有文件的小改动)展示 diff,用户可选自动或手动确认
高危run_commanddelete_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 设计——都不再是黑盒。你能看到它在循环的哪一步,用的哪个工具,为什么这样决策。

参考文章:How to Build an Agent