第2章:Agent 的工作原理 —— 拆开看看里面有什么
一句话:理解 Agent 内部的四大组件是怎么协作的。
上一章我们聊了 Agent 是什么,知道了它是一个"能想能干"的 AI 助手。但知道"是什么"远远不够,我们还得知道"怎么运作的"。
这就好比你知道汽车能把你从 A 点送到 B 点,但如果你想自己造一辆车,你就得知道发动机怎么工作、轮子怎么转、方向盘怎么控制转向。
这一章,我们就来把 Agent 这台"机器"拆开,看看里面到底有哪些零件,以及这些零件是怎么配合工作的。
本章目标
读完这一章,你将能够:
- 理解 Agentic Loop(代理循环)的工作机制
- 理解 Tools(工具)在 Agent 中的角色
- 理解 Context(上下文)的管理方式
- 理解 LLM 在 Agent 中扮演的角色
- 能画出一次完整的 Agent 执行流程
前置知识
- 第1章:AI Agent 到底是个啥?
Agent 的四大组件总览
在我们深入每个部分之前,先来个全景图。Agent 内部有四个核心组件,就像一个人一样:
┌─────────────────────────────────────────────┐
│ Agent │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ 大脑 │ │ 记忆 │ │
│ │ (LLM) │ │(Context) │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ┌────┴──────────────┴────┐ │
│ │ 心脏 │ │
│ │ (Agentic Loop) │ │
│ └────────────┬───────────┘ │
│ │ │
│ ┌────────────┴───────────┐ │
│ │ 双手 │ │
│ │ (Tools) │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────┘
- 心脏(Agentic Loop):整个系统的运转核心,不停地循环:思考 → 行动 → 观察
- 双手(Tools):Agent 用来干活的工具,比如读文件、搜索、执行命令
- 记忆(Context):Agent 记住了哪些信息,怎么管理这些信息
- 大脑(LLM):做出所有决策的"指挥官"
接下来我们一个一个来聊。
2.1 Agent 的心脏:Agentic Loop(代理循环)
先用做饭来理解
假设你今天要做一道番茄炒蛋。你不是一下子就做好的,而是一步一步来:
- 看菜谱(理解任务):哦,需要番茄、鸡蛋、盐、油
- 切菜(调用工具):把番茄切块
- 尝一口(检查结果):嗯,番茄切好了
- 下一步?(做决定):还没完,接着打鸡蛋
- 打鸡蛋(调用工具):把鸡蛋打到碗里搅拌
- 检查(检查结果):鸡蛋打好了
- 下一步?(做决定):开火炒
- 炒菜(调用工具):热油,倒蛋液,炒番茄
- 尝一口(检查结果):味道差点意思
- 加盐(调用工具):撒点盐
- 再尝(检查结果):完美!
- 出锅!(任务完成):菜做好了,可以端上桌了
看到了吗?你在做饭的时候,其实一直在重复一个循环:想一想 → 干一步 → 看看结果 → 决定下一步。这个循环一直转,直到菜做好为止。
Agent 的工作方式跟这个一模一样。这个不断重复的循环,就叫做 Agentic Loop(代理循环)。
循环的伪代码
用代码来表示,Agentic Loop 长这样:
def agentic_loop(user_task):
# 把用户的任务放进对话上下文
messages = [{"role": "user", "content": user_task}]
while True:
# 第1步:让大脑(LLM)思考
response = llm.think(messages)
# 第2步:检查 —— 大脑说任务完成了吗?
if response.is_final_answer():
# 菜做好了!端上桌
return response.text
# 第3步:大脑说还要干活 —— 调用工具
tool_name = response.tool_to_use # 比如 "切菜"
tool_params = response.tool_parameters # 比如 "番茄, 切块"
# 第4步:执行工具,拿到结果
result = execute_tool(tool_name, tool_params)
# 第5步:把结果放回对话上下文,让大脑知道
messages.append({"role": "assistant", "content": response})
messages.append({"role": "tool", "content": result})
# 回到 while True 的开头,继续循环
这段代码虽然简化了很多细节,但核心逻辑就是这样。你会发现,整个流程就是一个 while True 循环,只有当 LLM 判断"任务完成了"的时候才会跳出来。
循环的流程图
用图来画更直观:
用户输入任务
│
▼
┌──────────┐
│ LLM 思考 │ ◄──────────────────┐
│ (大脑) │ │
└────┬─────┘ │
│ │
▼ │
┌────────┐ 是 │
│任务完成?├──────► 返回最终结果 │
└────┬───┘ │
│ 否 │
▼ │
┌──────────┐ │
│ 选择工具 │ │
│ 生成参数 │ │
└────┬─────┘ │
│ │
▼ │
┌──────────┐ │
│ 执行工具 │ │
│ (双手干活) │ │
└────┬─────┘ │
│ │
▼ │
┌──────────┐ │
│ 拿到结果 │ │
│ 放入记忆 │ ─────────────────┘
└──────────┘
每转一圈,就叫一个 turn(轮次)。一个简单的任务可能只需要 1-2 个轮次,而一个复杂的任务可能需要 10 几个甚至几十个轮次。
跟普通函数调用有什么区别?
你可能会问:这跟我写一个普通函数有什么区别?区别可大了。
普通函数调用:
# 人类程序员预先写好了每一步
def process_files():
files = list_files("./src") # 第1步写死了
for f in files: # 第2步写死了
content = read_file(f) # 第3步写死了
if "TODO" in content: # 第4步写死了
print(f) # 第5步写死了
这段代码在你写的时候,每一步做什么就已经定死了。程序不会临场发挥,不会根据情况变通。
Agentic Loop:
用户:帮我找出项目里有 TODO 注释的文件
Agent 思考:我需要先看看项目结构...
Agent 调用:list_files("./")
Agent 观察:有 src/, tests/, docs/ 三个目录
Agent 思考:src 目录可能是主要代码,先搜这里
Agent 调用:grep("TODO", "./src")
Agent 观察:找到了 5 个 TODO
Agent 思考:tests 目录也可能有,查一下
Agent 调用:grep("TODO", "./tests")
Agent 观察:找到了 2 个 TODO
Agent 思考:docs 目录一般没有 TODO,但还是查一下
Agent 调用:grep("TODO", "./docs")
Agent 观察:没有找到
Agent 思考:好了,我已经搜索完所有目录了,把结果整理一下
Agent 回复:我找到了 7 个 TODO 注释,分布在以下文件中...
看到区别了吗?Agent 的每一步都是 动态决定 的。它会根据上一步的结果来决定下一步做什么。如果某个目录搜不到结果,它会自动换个方向。如果某个工具报错了,它还会想办法换个方式尝试。
这就像一个真正的人在干活 —— 遇到什么情况就应对什么情况,而不是像机器人一样死板地执行预设的步骤。
循环什么时候停?
一个很重要的问题:循环什么时候停下来?
一般有三种情况:
- 任务完成:LLM 判断任务已经完成了,可以给用户答案了
- 达到最大轮次:为了防止 Agent 无限循环,通常会设置一个最大轮次(比如 20 轮)
- 遇到不可恢复的错误:比如所有工具都失败了,Agent 无法继续
MAX_TURNS = 20
turn_count = 0
while turn_count < MAX_TURNS:
response = llm.think(messages)
if response.is_final_answer():
return response.text
# ... 执行工具 ...
turn_count += 1
# 如果超过了最大轮次
return "抱歉,我尝试了很多次但没能完成任务"
设置最大轮次非常重要,不然如果 Agent "犯迷糊"了(比如一直在两个工具之间来回跳),它会无限消耗你的 API 费用。就像你在厨房里一直在"加盐 → 尝一口 → 太淡了 → 加盐 → 尝一口 → 太淡了...",那这道菜永远也做不好,还浪费了一堆盐。
2.2 Agent 的双手:Tools(工具)
什么是工具?
工具就是 Agent 能调用的函数。
你可以把 Agent 想象成一个很聪明的人,但他被关在一个房间里。房间里有一张桌子,桌子上放着各种工具:锤子、螺丝刀、计算器、电话、电脑。这个人自己不能穿墙出去,但他可以用桌子上的工具来完成各种任务。
LLM(大脑)本身只能思考和说话,它不能直接去读一个文件、不能直接去执行一条命令、不能直接去查数据库。但如果你给它提供了"读文件"这个工具,它就可以通过调用这个工具来读取文件内容。
没有工具的 LLM:
用户:项目根目录有哪些文件?
LLM:抱歉,我无法直接查看你的文件系统。你可以在终端运行 ls 命令查看。
有工具的 Agent:
用户:项目根目录有哪些文件?
Agent:(调用 list_files 工具)
Agent:项目根目录下有以下文件和目录:
- src/
- tests/
- package.json
- tsconfig.json
- README.md
看到区别了吗?工具让 Agent 从"动嘴"变成了"动手"。
常见的工具类型
工具可以分为两大类:
内置工具(Agent SDK 自带的)
这些是 Agent 框架自带的、开箱即用的工具:
| 工具名 | 干什么用的 | 举个例子 |
|---|---|---|
| Read | 读取文件内容 | 读取 package.json 的内容 |
| Write | 写入文件 | 创建一个新的 config.js |
| Edit | 编辑文件 | 把文件里的某段代码替换掉 |
| Bash | 执行终端命令 | 运行 npm install |
| Glob | 按模式搜索文件 | 找到所有 *.ts 文件 |
| Grep | 在文件内容中搜索 | 找到包含 "TODO" 的行 |
| WebSearch | 搜索网页 | 搜索 "React 最新版本" |
| WebFetch | 获取网页内容 | 读取一个网页的内容 |
外部工具(你自己定义的)
这些是你根据业务需求自己编写的工具:
| 工具名 | 干什么用的 | 举个例子 |
|---|---|---|
| send_email | 发送邮件 | 给同事发一封汇报邮件 |
| query_database | 查询数据库 | 查询用户表有多少条记录 |
| deploy_service | 部署服务 | 把代码部署到生产环境 |
| create_jira_ticket | 创建 Jira 工单 | 创建一个 bug 修复工单 |
| slack_message | 发送 Slack 消息 | 在频道里发个通知 |
你可以根据需要给 Agent 配备各种各样的工具,就像给一个工人配备不同的工具箱一样。工具越多,Agent 能干的事情就越多。
Agent 怎么知道该用哪个工具?
这是一个好问题。Agent 不是随机选工具的,它是根据 工具描述(tool description) 来选择的。
每个工具在注册到 Agent 的时候,都需要提供一份"说明书"。这份说明书包括:
- 名字:这个工具叫什么
- 描述:这个工具是干什么的
- 参数:使用这个工具需要提供什么参数
- 返回值:工具执行完会返回什么
LLM 会读这些说明书,然后根据当前任务的需要,选择最合适的工具。
这就像你去五金店,你跟店员说"我要把两块木板钉在一起",店员会根据你的需求,从货架上给你拿锤子和钉子,而不是给你拿螺丝刀或者油漆桶。LLM 就是那个"聪明的店员"。
工具的定义格式
来看一个具体的例子。假设我们要定义一个"计算器"工具:
{
"name": "calculator",
"description": "执行基础数学运算。支持加减乘除。当需要进行数学计算时使用此工具。",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "要计算的数学表达式,比如 '2 + 3 * 4'"
}
},
"required": ["expression"]
}
}
用 TypeScript 代码来写的话,大概是这样:
const calculatorTool = {
name: "calculator",
description: "执行基础数学运算。支持加减乘除。当需要进行数学计算时使用此工具。",
inputSchema: {
type: "object",
properties: {
expression: {
type: "string",
description: "要计算的数学表达式,比如 '2 + 3 * 4'"
}
},
required: ["expression"]
},
// 这个函数才是真正干活的
execute: async (params) => {
const result = eval(params.expression); // 实际代码不要用 eval,这里只是示意
return { result: result };
}
};
当 Agent 遇到一个数学计算的需求时,对话过程会是这样的:
用户:23 乘以 47 等于多少?
LLM 内心独白:这是一个数学计算问题,我应该用 calculator 工具
LLM 输出:
{
"tool": "calculator",
"parameters": {
"expression": "23 * 47"
}
}
系统执行工具,返回结果:{ "result": 1081 }
LLM 收到结果后回复:23 乘以 47 等于 1081。
核心洞见:工具就是 JSON Schema
这里有一个非常重要的洞见,值得你花时间理解:
工具本质上就是一个 JSON Schema(JSON 模式描述),LLM 的工作就是根据这个 Schema 填入正确的参数值。
打个比方。工具定义就像是一张"表格模板":
┌──────────────────────────────────┐
│ 表格名:calculator(计算器) │
│ │
│ 用途:执行数学运算 │
│ │
│ 需要填写: │
│ ┌─────────────────────────────┐ │
│ │ expression: ______________ │ │
│ │ (请填写数学表达式) │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────┘
LLM 做的事情就是看着这张表格,然后根据用户的需求把空白处填好:
┌──────────────────────────────────┐
│ 表格名:calculator(计算器) │
│ │
│ 用途:执行数学运算 │
│ │
│ 需要填写: │
│ ┌─────────────────────────────┐ │
│ │ expression: "23 * 47" │ │
│ │ (请填写数学表达式) │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────┘
填好之后,系统就把这张"填好的表格"交给对应的函数去执行。整个过程就是这么回事。
所以你看,LLM 并不需要"理解"锤子是怎么工作的,它只需要知道锤子的使用说明(JSON Schema),然后正确地填写"要锤哪里"(参数)就行了。真正干活的是工具本身的代码。
工具调用失败怎么办?
工具不是每次都能成功的。文件可能不存在,网络可能超时,命令可能报错。一个优秀的 Agent 需要能处理这些失败情况。
用户:读取 config.yaml 文件
Agent 调用:read_file("config.yaml")
工具返回:错误!文件不存在
Agent 思考:config.yaml 不存在,可能是 config.yml?
Agent 调用:read_file("config.yml")
工具返回:错误!文件也不存在
Agent 思考:两个都不存在,那可能叫别的名字,搜一下
Agent 调用:glob("config.*")
工具返回:找到 config.json
Agent 思考:原来是 JSON 格式的
Agent 调用:read_file("config.json")
工具返回:{"port": 3000, "host": "localhost"}
Agent 回复:我找到了配置文件 config.json,内容是...
看到了吗?Agent 遇到错误时不会直接放弃,它会像一个有经验的人一样,换个方法尝试。这种"灵活应变"的能力,正是 Agentic Loop 带来的好处。
2.3 Agent 的记忆:Context(上下文)
什么是 Context?
Context(上下文)就是 Agent 的记忆。它包含了 Agent 当前知道的所有信息。
想象你在跟一个朋友聊天。你说了一句话,朋友回了一句,你又说了一句...整个对话过程中,你们两个人都记得之前说过什么。你不需要每次说话都重复一遍前面的内容,因为你们脑子里都有这段对话的"记忆"。
Agent 的 Context 就是这段"记忆"。每当你跟 Agent 说一句话,这句话就被加入 Context;Agent 回复的内容也会加入 Context;Agent 调用工具的参数和结果也会加入 Context。
Context(上下文)的内容,像一个不断增长的列表:
[
{ role: "user", content: "帮我找项目里的 TODO" },
{ role: "assistant", content: "好的,我先搜索一下", tool_call: grep("TODO") },
{ role: "tool", content: "src/app.ts:23: // TODO: 添加错误处理" },
{ role: "assistant", content: "我找到了一个 TODO..." },
{ role: "user", content: "还有别的目录吗?" },
{ role: "assistant", content: "我再搜搜其他目录", tool_call: grep("TODO", "tests/") },
{ role: "tool", content: "tests/api.test.ts:11: // TODO: 添加边界测试" },
{ role: "assistant", content: "在 tests 目录也找到了一个..." },
...
]
每次 LLM 要做决策的时候,它看到的就是这个完整的列表。这样它才知道之前发生了什么,才能做出合理的下一步决策。
短期记忆 vs 长期记忆
Agent 的记忆分两种:
短期记忆(Context Window)
就是当前对话的内容。只要对话还在进行,这些信息就一直在。但一旦对话结束(或者被关闭),这些信息就没了。
就像你跟别人面对面聊天 —— 聊天过程中你记得所有内容,但聊完各自回家后,很多细节你就忘了。
特点:
- 信息量有限(受 Context Window 大小限制)
- 即时可用,不需要额外操作
- 对话结束就消失
长期记忆(Persistent Memory)
保存到文件、数据库或者其他持久化存储中的信息。就算对话结束了,这些信息还在。
就像你把重要的事情写到笔记本上 —— 哪怕过了一个月,翻开笔记本还能看到。
特点:
- 信息量基本不受限制
- 需要主动存取(写入和读取)
- 可以跨对话保留
在实际的 Agent 应用中,通常两种记忆会配合使用:
短期记忆(Context Window)
├── 用户刚才说了什么
├── Agent 刚才做了什么
├── 工具返回了什么结果
└── 当前的对话历史
长期记忆(文件/数据库)
├── 用户的偏好设置
├── 之前对话的摘要
├── 项目的关键信息(比如 CLAUDE.md)
└── 知识库内容
举个例子:Claude Code 里有一个 CLAUDE.md 文件,这其实就是一种长期记忆。每次你启动 Claude Code 的时候,它会自动读取这个文件,把里面的内容加载到短期记忆中。这样 Agent 就"记起"了你之前告诉它的那些项目信息。
Context Window 的大小限制
这是一个非常重要的概念。Context Window 不是无限大的,它有一个上限。
对于 Claude 模型来说:
| 模型 | Context Window 大小 |
|---|---|
| Claude Opus | 200K tokens |
| Claude Sonnet | 200K tokens |
| Claude Haiku | 200K tokens |
Token 是什么? 简单说,1 个 token 大约等于一个英文单词,或者半个到一个中文字。200K tokens 大约等于 15 万个英文单词,或者 10 万个中文字左右。
这听起来很多,但在实际使用中,Context 会被消耗得很快:
一次典型的 Agent 对话中 Context 的消耗:
用户输入: 约 100 tokens
系统提示词: 约 2,000 tokens
工具定义(10 个工具): 约 3,000 tokens
第1轮 - LLM 思考 + 工具调用: 约 500 tokens
第1轮 - 工具返回结果: 约 2,000 tokens
第2轮 - LLM 思考 + 工具调用: 约 500 tokens
第2轮 - 工具返回结果: 约 5,000 tokens
...
第10轮: 约 X,000 tokens
─────────────────────────
已经用了几万 tokens 了!
特别是当工具返回大量内容的时候(比如读取一个大文件),Context 会被迅速填满。
"金鱼问题":为什么 Agent 会"忘事"?
你有没有遇到过这种情况:跟 AI 聊了很久之后,它突然"忘了"你之前说过的话?
这就是所谓的"金鱼问题"。金鱼的记忆据说只有 7 秒(虽然这是个谣言),但 LLM 的"记忆"确实是有限的。
当对话内容超过 Context Window 的大小时,最早的内容就会被"挤出去"。就像一个固定大小的水杯 —— 水满了之后再倒水进去,最早的水就会溢出来。
Context Window(固定大小的"杯子"):
时间 1: [消息1] [消息2] [消息3] [ 空间 ]
时间 2: [消息1] [消息2] [消息3] [消息4] [空间]
时间 3: [消息1] [消息2] [消息3] [消息4] [消息5] ← 满了!
时间 4: [消息2] [消息3] [消息4] [消息5] [消息6] ← 消息1被挤掉了
时间 5: [消息3] [消息4] [消息5] [消息6] [消息7] ← 消息2也被挤掉了
Agent SDK 怎么处理 Context?
Claude Agent SDK 提供了一些自动化的 Context 管理策略:
- 自动截断:当 Context 快满的时候,自动删掉最早的消息
- 摘要压缩:把很长的对话历史压缩成一段摘要
- 工具结果截断:如果工具返回的内容太长,自动截断
原始 Context(太长了):
[消息1][消息2][消息3]...[消息50][消息51]...[消息100]
压缩后的 Context:
[摘要: 用户让我处理一个 TypeScript 项目,我已经读取了配置文件并安装了依赖]
[消息90][消息91]...[消息100]
这种方式虽然会丢失一些细节,但能保证 Agent 始终记住最近发生的事情和之前的大致情况。
关于 Context 的一些实用建议
- 明确指令,减少来回:一次把需求说清楚,减少不必要的对话轮次
- 大文件分块处理:不要一次性读一个几千行的文件,分段读
- 重要信息写入文件:把关键信息写到 CLAUDE.md 这样的文件里,作为长期记忆
- 适时开启新对话:如果对话太长了,可以把关键信息总结一下,开个新对话继续
2.4 Agent 的大脑:LLM(大语言模型)
LLM 在 Agent 中扮演什么角色?
如果说 Agentic Loop 是心脏,Tools 是双手,Context 是记忆,那么 LLM 就是 大脑 —— 整个系统的决策中心。
LLM 在 Agent 中承担四个核心职责:
┌──────────────────────────────────────────┐
│ LLM 的四大职责 │
│ │
│ 1. 理解意图 ── 用户到底想干什么? │
│ 2. 选择工具 ── 该用哪个工具来完成? │
│ 3. 生成参数 ── 工具需要什么输入? │
│ 4. 判断结果 ── 工具返回的结果对不对? │
│ ── 任务完成了吗? │
│ ── 下一步该干什么? │
└──────────────────────────────────────────┘
来看一个具体的例子,感受一下 LLM 是怎么"思考"的:
用户说:"帮我把 src/utils.ts 里的 calculateTotal 函数改成异步的"
LLM 的内心活动:
第1步 - 理解意图:
"用户想把一个函数改成 async 版本。
文件是 src/utils.ts,函数名是 calculateTotal。"
第2步 - 选择工具:
"我得先看看这个函数现在长什么样,用 Read 工具。"
第3步 - 生成参数:
Read(file_path="src/utils.ts")
(工具执行后返回了文件内容)
第4步 - 判断结果:
"好的,我看到了这个函数:
function calculateTotal(items: Item[]): number { ... }
现在我需要用 Edit 工具把它改成 async 版本。"
第5步 - 选择工具(第二轮):
Edit(file_path="src/utils.ts",
old_string="function calculateTotal(items: Item[]): number",
new_string="async function calculateTotal(items: Item[]): Promise<number>")
(工具执行后返回成功)
第6步 - 判断结果(第二轮):
"修改成功了。但等等,函数体内部如果有同步代码,
也需要相应调整。让我再看看..."
你看到了吗?LLM 不是在"执行"一个预设的程序,它是在"思考"。它会理解你的意图,制定计划,执行步骤,检查结果,甚至想到你没说的细节(比如函数体内部也需要改)。
一个重要的点:LLM 不"运行"工具
这一点必须强调:LLM 只是决定用哪个工具以及传什么参数,它并不真正运行工具。
打个比方。LLM 就像一个建筑师,它画图纸(决定做什么),但不亲自搬砖(执行操作)。搬砖的是工人(工具的实际代码)。
LLM 输出:
"我要使用 Bash 工具,执行 npm install express"
│
│ 这段话传给了 Agent 系统
▼
Agent 系统解析 LLM 的输出:
"哦,它想用 Bash 工具,参数是 'npm install express'"
│
│ Agent 系统调用 Bash 工具的实际代码
▼
Bash 工具的代码在你的电脑上真正执行:
$ npm install express
added 57 packages in 2.3s
│
│ 执行结果返回给 Agent 系统
▼
Agent 系统把结果放入 Context,让 LLM 看到
整个过程中,LLM 只做了"想"和"说"两件事,"做"的部分是 Agent 系统和工具代码负责的。
不同模型的选择
Anthropic 提供了三个级别的 Claude 模型,就像三档变速箱:
Claude Opus —— "最强大脑"
- 特点:最聪明,推理能力最强,上下文理解最深
- 适合场景:
- 复杂的多步骤推理任务
- 需要深度理解代码架构的任务
- 需要精细判断的任务(比如代码审查)
- 处理模糊或复杂的用户需求
- 缺点:最贵,速度较慢
- 比喻:像一个资深的高级工程师,什么难题都能解决,但请他要花大价钱
Claude Sonnet —— "性价比之王"
- 特点:聪明程度很高,速度和价格都适中
- 适合场景:
- 日常编程任务(写代码、改 bug、重构)
- 一般的文件处理和搜索
- 大多数 Agent 应用
- 缺点:在特别复杂的推理上略逊于 Opus
- 比喻:像一个靠谱的中级工程师,95% 的任务都能搞定,性价比最高
Claude Haiku —— "极速闪电"
- 特点:速度最快,价格最低
- 适合场景:
- 简单的分类和判断
- 快速的文本处理
- 对延迟敏感的场景
- 需要大量调用但每次任务都很简单的场景
- 缺点:在复杂任务上能力有限
- 比喻:像一个刚入行的初级程序员,简单任务又快又好,但复杂的搞不定
怎么选?
一个实用的选择策略:
你的任务 ──► 简单吗? ──► 是 ──► 用 Haiku
│
▼ 否
──► 普通难度? ──► 是 ──► 用 Sonnet
│
▼ 否
──► 很复杂? ──► 是 ──► 用 Opus
Anthropic 官方的建议是:大多数情况下用 Sonnet 就够了。只有在需要最强推理能力的时候才上 Opus,在追求速度和低成本的时候才用 Haiku。
在 Agent 应用中,甚至可以混合使用不同的模型。比如:
- 用 Haiku 来做初步的意图分类("用户是想写代码还是查资料?")
- 用 Sonnet 来执行具体的编码任务
- 用 Opus 来做最后的代码审查
这样既保证了质量,又控制了成本。
2.5 把它们串起来:一次完整的 Agent 执行流程
现在我们已经了解了四个组件,让我们来看它们是怎么协作完成一个真实任务的。
示例任务
用户说:"找出项目中所有导入了 React 的 TypeScript 文件,列出来"
完整执行流程
让我们一步一步来看整个过程:
═══════════════════════════════════════════════════════════════
完整执行流程图
═══════════════════════════════════════════════════════════════
用户输入: "找出项目中所有导入了 React 的 TypeScript 文件"
│
▼
┌─────────────────────────────────────────────────────────┐
│ 第 1 轮 (Turn 1) │
│ │
│ Context: [用户消息] │
│ │
│ LLM 思考: │
│ "用户想找 TypeScript 文件中导入 React 的文件。 │
│ 我应该先找到所有 TypeScript 文件。 │
│ 用 Glob 工具,搜索模式为 **/*.ts 和 **/*.tsx" │
│ │
│ LLM 决定: 调用 Glob 工具 │
│ 参数: pattern = "**/*.{ts,tsx}" │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Glob 工具执行 │ │
│ │ 搜索所有 .ts 和 .tsx 文件 │ │
│ │ 返回: │ │
│ │ src/App.tsx │ │
│ │ src/index.ts │ │
│ │ src/components/Header.tsx │ │
│ │ src/components/Footer.tsx │ │
│ │ src/utils/helpers.ts │ │
│ │ src/utils/api.ts │ │
│ │ src/types/index.ts │ │
│ └─────────────────────────────────┘ │
│ │
│ Context: [用户消息, LLM调用Glob, Glob结果] │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 第 2 轮 (Turn 2) │
│ │
│ Context: [用户消息, LLM调用Glob, Glob结果] │
│ │
│ LLM 思考: │
│ "好的,找到了 7 个 TypeScript 文件。 │
│ 现在我需要看哪些文件导入了 React。 │
│ 用 Grep 工具搜索 import 语句中包含 React 的。" │
│ │
│ LLM 决定: 调用 Grep 工具 │
│ 参数: pattern = "import.*React", glob = "*.{ts,tsx}" │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ Grep 工具执行 │ │
│ │ 在所有 .ts/.tsx 文件中搜索 "import.*React" │ │
│ │ 返回: │ │
│ │ src/App.tsx:1: │ │
│ │ import React from 'react'; │ │
│ │ src/components/Header.tsx:1: │ │
│ │ import React, { useState } from 'react';│ │
│ │ src/components/Footer.tsx:1: │ │
│ │ import React from 'react'; │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Context: [用户消息, Glob调用+结果, LLM调用Grep, Grep结果]│
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 第 3 轮 (Turn 3) │
│ │
│ Context: [全部历史消息] │
│ │
│ LLM 思考: │
│ "搜索完成了。有 3 个文件导入了 React: │
│ 1. src/App.tsx │
│ 2. src/components/Header.tsx │
│ 3. src/components/Footer.tsx │
│ 信息齐全了,可以给用户最终答案了。" │
│ │
│ LLM 决定: 返回最终结果(不再调用工具) │
│ │
│ 最终回复: │
│ "项目中有 3 个 TypeScript 文件导入了 React: │
│ │
│ 1. src/App.tsx │
│ import React from 'react' │
│ │
│ 2. src/components/Header.tsx │
│ import React, { useState } from 'react' │
│ │
│ 3. src/components/Footer.tsx │
│ import React from 'react' │
│ │
│ 其余 4 个 TypeScript 文件 │
│ (index.ts, helpers.ts, api.ts, types/index.ts) │
│ 没有导入 React。" │
└─────────────────────────────────────────────────────────┘
消息流的详细时序
让我们用另一种方式来看整个消息流:
时间线
│
│ ① 用户 → Agent
│ "找出项目中所有导入了 React 的 TypeScript 文件"
│
│ ② Agent → LLM
│ [系统提示 + 工具定义 + 用户消息]
│
│ ③ LLM → Agent
│ "使用 Glob 工具, pattern=**/*.{ts,tsx}"
│
│ ④ Agent → Glob 工具
│ 执行文件搜索
│
│ ⑤ Glob 工具 → Agent
│ 返回 7 个文件路径
│
│ ⑥ Agent → LLM
│ [之前的消息 + Glob 结果]
│
│ ⑦ LLM → Agent
│ "使用 Grep 工具, pattern=import.*React"
│
│ ⑧ Agent → Grep 工具
│ 执行内容搜索
│
│ ⑨ Grep 工具 → Agent
│ 返回 3 个匹配结果
│
│ ⑩ Agent → LLM
│ [之前的消息 + Grep 结果]
│
│ ⑪ LLM → Agent
│ 最终回复文本(不包含工具调用)
│
│ ⑫ Agent → 用户
│ 展示最终结果
│
▼
注意看这个流程中的规律:
- 偶数步(②⑥⑩)都是 Agent 把信息送给 LLM 思考
- 奇数步(③⑦⑪)都是 LLM 返回决策结果
- ④⑤⑧⑨ 是工具的执行和返回
- 每一组"LLM 思考 → 工具执行"就是一个 turn(轮次)
四大组件各自的贡献
在这个例子中:
| 组件 | 做了什么 |
|---|---|
| Agentic Loop | 驱动了 3 轮循环,协调 LLM 和工具的交互 |
| LLM(大脑) | 决定了先用 Glob 再用 Grep 的策略,最后整理了结果 |
| Tools(工具) | Glob 找到了文件列表,Grep 找到了匹配内容 |
| Context(记忆) | 保存了每一轮的结果,让 LLM 能"记住"前面的信息 |
四个组件缺一不可:
- 没有 Agentic Loop → 不能多步骤执行
- 没有 LLM → 不知道该干什么
- 没有 Tools → 空有想法但没法执行
- 没有 Context → 每一轮都忘了上一轮做过什么
2.6 Agent 和 Workflow 有什么区别?
在 AI 应用开发中,你经常会听到两个词:Agent 和 Workflow。它们有什么区别?
Workflow:预设的流水线
Workflow 就像工厂里的流水线 —— 每一步做什么都是提前设计好的。
Workflow(工作流)的执行方式:
用户输入
│
▼
步骤1: 调用 LLM 提取关键词 ← 开发时就写死了
│
▼
步骤2: 根据关键词搜索数据库 ← 开发时就写死了
│
▼
步骤3: 把结果发给 LLM 做总结 ← 开发时就写死了
│
▼
步骤4: 返回给用户 ← 开发时就写死了
用代码来表示就是:
def workflow(user_input):
# 步骤1:提取关键词(写死的)
keywords = llm.extract_keywords(user_input)
# 步骤2:搜索数据库(写死的)
results = database.search(keywords)
# 步骤3:生成摘要(写死的)
summary = llm.summarize(results)
# 步骤4:返回结果(写死的)
return summary
每一步做什么、什么顺序、用什么工具,都是你在写代码的时候就决定了的。不管用户输入什么,它都按照这个固定流程走。
Agent:临场发挥的自由人
Agent 不一样。它的执行步骤不是预设的,而是 运行时由 LLM 动态决定 的。
Agent 的执行方式:
用户输入
│
▼
LLM 思考: "我该怎么做?" ← 每次可能不一样!
│
├──► 可能先搜索文件
│ │
│ ▼
│ LLM 再想: "搜到了,下一步呢?" ← 根据结果决定
│ │
│ ├──► 可能去读文件内容
│ ├──► 可能去搜索网页
│ └──► 可能直接给出答案
│
├──► 也可能先查数据库
│
└──► 也可能直接回答(如果它已经知道答案)
同一个任务,Agent 每次执行的路径可能都不一样,因为它会根据实际情况临场发挥。
一个直观的对比
| 方面 | Workflow | Agent |
|---|---|---|
| 执行路径 | 固定的,开发时确定 | 动态的,运行时确定 |
| 决策者 | 程序员(写代码时决定) | LLM(运行时决定) |
| 灵活性 | 低,只能处理预期的场景 | 高,能处理意料之外的情况 |
| 可预测性 | 高,每次结果一致 | 较低,每次路径可能不同 |
| 复杂度 | 低,容易理解和调试 | 高,行为不太可控 |
| 成本 | 低,LLM 调用次数固定 | 高,LLM 调用次数不确定 |
| 适用场景 | 流程明确的任务 | 需要灵活应变的任务 |
再打一个比方
Workflow 就像自动售货机:
- 你投币 → 选饮料 → 出饮料 → 完事
- 流程固定,不会变
- 但如果你想买的东西里面没有(比如你想买热汤),它就没辙了
Agent 就像一个便利店店员:
- 你说"我想喝点热的"
- 店员想了想:"今天有热咖啡、热茶、热豆浆"
- 你说"来杯热咖啡"
- 店员:"要加糖吗?"
- 你说"加一点"
- 店员根据你的要求现场操作
- 如果咖啡机坏了,他还会推荐你喝热茶
这就是两者最本质的区别:Workflow 的路径是开发阶段决定的,Agent 的路径是运行时决定的。
什么时候用 Workflow?什么时候用 Agent?
Anthropic 官方给出的建议非常实用:先从简单的开始,只在必要的时候才增加复杂度。
你的任务 ──► 流程固定且明确? ──► 是 ──► 用 Workflow
│
▼ 否
──► 需要灵活决策? ──► 是 ──► 用 Agent
│
▼ 否
──► 不确定? ──► 先用 Workflow,不够再升级为 Agent
适合 Workflow 的场景:
- 每次都是固定三步:输入 → 处理 → 输出
- 客服机器人的意图分类(总是先分类再路由再回复)
- 文档翻译(总是先分段再翻译再合并)
- 数据处理管道(提取 → 转换 → 加载)
适合 Agent 的场景:
- 用户需求不确定,可能需要多种工具配合
- 需要根据中间结果调整策略
- 任务步骤数不固定
- 需要处理错误并重试
Anthropic 原话:
"Workflows are code paths determined at development time; agents determine their own paths at runtime." (工作流是开发阶段确定的代码路径;代理在运行时决定自己的路径。)
可以混合使用
在实际应用中,Workflow 和 Agent 不是非此即彼的。很多时候会混合使用:
大的 Agent 框架
│
├── Agent 决定: "这是一个翻译任务"
│ │
│ └── 调用翻译 Workflow(固定流程:分段→翻译→合并)
│
├── Agent 决定: "这是一个代码审查任务"
│ │
│ └── Agent 自由发挥(读代码→分析→提建议→可能再读更多代码)
│
└── Agent 决定: "这是一个数据分析任务"
│
└── 调用数据分析 Workflow(固定流程:取数→清洗→分析→出图)
这种"Agent 做高层决策,Workflow 做具体执行"的模式,在实践中非常常见。
动手练习
练习 1:画出 Agentic Loop
假设用户对 Agent 说:"帮我创建一个 Python 函数来计算斐波那契数列,再写一个测试"
请你在纸上(或脑子里)画出整个 Agentic Loop 的执行过程。思考:
- Agent 需要几轮循环?
- 每轮分别做了什么?
- 什么时候循环结束?
参考答案(展开查看):
点击查看参考答案
第 1 轮:
LLM 思考: 需要先创建一个 Python 文件写斐波那契函数
调用工具: Write(file_path="fibonacci.py", content="def fibonacci(n): ...")
结果: 文件创建成功
第 2 轮:
LLM 思考: 函数写好了,现在需要写测试
调用工具: Write(file_path="test_fibonacci.py", content="import unittest...")
结果: 测试文件创建成功
第 3 轮:
LLM 思考: 应该运行测试确认一下能通过
调用工具: Bash(command="python -m pytest test_fibonacci.py")
结果: 3 tests passed
第 4 轮:
LLM 思考: 函数写好了,测试也通过了,任务完成!
返回最终结果: "我已经创建了 fibonacci.py 和 test_fibonacci.py,
所有测试都通过了。"
总共 4 轮,循环在 LLM 判断任务完成后结束。
练习 2:列出需要的工具
还是上面那个任务,请列出 Agent 完成这个任务需要哪些工具?
点击查看参考答案
至少需要以下工具:
- Write(写文件):创建 fibonacci.py 和 test_fibonacci.py
- Bash(执行命令):运行 pytest 来测试
- Read(读文件):可能需要读取文件来检查内容(可选)
- Edit(编辑文件):如果测试失败,需要修改代码(可选)
最少只需要 Write 和 Bash 两个工具就够了。但为了处理可能的错误情况(比如测试失败需要改代码),Read 和 Edit 也很有用。
练习 3:工具调用失败怎么办?
思考这个场景:Agent 在执行上面的任务时,运行测试发现有一个测试失败了。这时候 Agentic Loop 应该怎么处理?
点击查看参考答案
第 3 轮:
调用工具: Bash(command="python -m pytest test_fibonacci.py")
结果: 1 test FAILED - fibonacci(0) 应返回 0 但返回了 1
此时循环不会结束!而是继续:
第 4 轮:
LLM 思考: 测试失败了,fibonacci(0) 的处理有问题,
需要修改函数的边界条件
调用工具: Edit(file_path="fibonacci.py",
old_string="if n <= 1: return 1",
new_string="if n <= 0: return 0\n if n == 1: return 1")
结果: 编辑成功
第 5 轮:
LLM 思考: 改好了,再跑一次测试
调用工具: Bash(command="python -m pytest test_fibonacci.py")
结果: 3 tests passed
第 6 轮:
LLM 思考: 所有测试通过了,任务完成!
返回最终结果: "我发现斐波那契函数的边界条件有误,
已经修复并通过了所有测试。"
关键点:
- 失败不等于结束,Agent 会尝试修复
- LLM 会分析错误原因,而不是盲目重试
- 修复后会再次验证,确保问题真正解决了
- 这就是 Agentic Loop 的强大之处:它能自我纠错
本章小结
让我们回顾一下这一章的核心内容:
四大组件
┌──────────────┬───────────────────────────────────────┐
│ 组件 │ 作用 │
├──────────────┼───────────────────────────────────────┤
│ Agentic Loop│ 心脏 —— 驱动整个系统不断循环运转 │
│ (代理循环) │ 思考 → 行动 → 观察 → 思考 → ... │
├──────────────┼───────────────────────────────────────┤
│ Tools │ 双手 —— 让 Agent 能真正干活 │
│ (工具) │ 读文件、写文件、执行命令、搜索... │
├──────────────┼───────────────────────────────────────┤
│ Context │ 记忆 —— 记住对话历史和中间结果 │
│ (上下文) │ 短期记忆 + 长期记忆 │
├──────────────┼───────────────────────────────────────┤
│ LLM │ 大脑 —— 理解意图、做出决策 │
│ (大语言模型)│ 选工具、填参数、判断结果 │
└──────────────┴───────────────────────────────────────┘
核心要点
- Agentic Loop 是一个 while 循环,一直转到任务完成或达到上限
- 工具本质上是 JSON Schema,LLM 负责填参数,系统负责执行
- Context 有大小限制,需要合理管理,否则 Agent 会"失忆"
- LLM 是决策者不是执行者,它只负责想和说,不负责做
- Agent 和 Workflow 的区别:Agent 运行时动态决策,Workflow 开发时预设路径
一句话总结
Agent = Agentic Loop(心脏) + Tools(双手) + Context(记忆) + LLM(大脑)
心脏不停跳动,大脑不停思考,双手不停干活,记忆不断积累 —— 这就是 Agent 的工作原理。
下一章预告
现在你已经明白了 Agent 是什么(第1章)以及它怎么运作的(第2章),是时候动手了!
第3章:环境准备 —— 把工具都装好
我们将会:
- 安装 Node.js、Python 等开发工具
- 获取 Claude API Key
- 安装 Claude Code CLI
- 安装 Claude Agent SDK
- 运行你人生中第一个 Agent 程序
理论学够了,下一章开始写代码!