AI Agent 教程

第2章:Agent 的工作原理 —— 拆开看看里面有什么

一句话:理解 Agent 内部的四大组件是怎么协作的。

上一章我们聊了 Agent 是什么,知道了它是一个"能想能干"的 AI 助手。但知道"是什么"远远不够,我们还得知道"怎么运作的"。

这就好比你知道汽车能把你从 A 点送到 B 点,但如果你想自己造一辆车,你就得知道发动机怎么工作、轮子怎么转、方向盘怎么控制转向。

这一章,我们就来把 Agent 这台"机器"拆开,看看里面到底有哪些零件,以及这些零件是怎么配合工作的。


本章目标

读完这一章,你将能够:

前置知识


Agent 的四大组件总览

在我们深入每个部分之前,先来个全景图。Agent 内部有四个核心组件,就像一个人一样:

┌─────────────────────────────────────────────┐
│                   Agent                      │
│                                              │
│   ┌─────────┐    ┌─────────┐                │
│   │  大脑    │    │  记忆    │                │
│   │  (LLM)  │    │(Context) │                │
│   └────┬────┘    └────┬────┘                │
│        │              │                      │
│   ┌────┴──────────────┴────┐                │
│   │       心脏              │                │
│   │   (Agentic Loop)       │                │
│   └────────────┬───────────┘                │
│                │                             │
│   ┌────────────┴───────────┐                │
│   │       双手              │                │
│   │     (Tools)            │                │
│   └────────────────────────┘                │
└─────────────────────────────────────────────┘

接下来我们一个一个来聊。


2.1 Agent 的心脏:Agentic Loop(代理循环)

先用做饭来理解

假设你今天要做一道番茄炒蛋。你不是一下子就做好的,而是一步一步来:

  1. 看菜谱(理解任务):哦,需要番茄、鸡蛋、盐、油
  2. 切菜(调用工具):把番茄切块
  3. 尝一口(检查结果):嗯,番茄切好了
  4. 下一步?(做决定):还没完,接着打鸡蛋
  5. 打鸡蛋(调用工具):把鸡蛋打到碗里搅拌
  6. 检查(检查结果):鸡蛋打好了
  7. 下一步?(做决定):开火炒
  8. 炒菜(调用工具):热油,倒蛋液,炒番茄
  9. 尝一口(检查结果):味道差点意思
  10. 加盐(调用工具):撒点盐
  11. 再尝(检查结果):完美!
  12. 出锅!(任务完成):菜做好了,可以端上桌了

看到了吗?你在做饭的时候,其实一直在重复一个循环:想一想 → 干一步 → 看看结果 → 决定下一步。这个循环一直转,直到菜做好为止。

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 的每一步都是 动态决定 的。它会根据上一步的结果来决定下一步做什么。如果某个目录搜不到结果,它会自动换个方向。如果某个工具报错了,它还会想办法换个方式尝试。

这就像一个真正的人在干活 —— 遇到什么情况就应对什么情况,而不是像机器人一样死板地执行预设的步骤。

循环什么时候停?

一个很重要的问题:循环什么时候停下来?

一般有三种情况:

  1. 任务完成:LLM 判断任务已经完成了,可以给用户答案了
  2. 达到最大轮次:为了防止 Agent 无限循环,通常会设置一个最大轮次(比如 20 轮)
  3. 遇到不可恢复的错误:比如所有工具都失败了,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)

就是当前对话的内容。只要对话还在进行,这些信息就一直在。但一旦对话结束(或者被关闭),这些信息就没了。

就像你跟别人面对面聊天 —— 聊天过程中你记得所有内容,但聊完各自回家后,很多细节你就忘了。

特点:

长期记忆(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 管理策略:

  1. 自动截断:当 Context 快满的时候,自动删掉最早的消息
  2. 摘要压缩:把很长的对话历史压缩成一段摘要
  3. 工具结果截断:如果工具返回的内容太长,自动截断
原始 Context(太长了):
[消息1][消息2][消息3]...[消息50][消息51]...[消息100]

压缩后的 Context:
[摘要: 用户让我处理一个 TypeScript 项目,我已经读取了配置文件并安装了依赖]
[消息90][消息91]...[消息100]

这种方式虽然会丢失一些细节,但能保证 Agent 始终记住最近发生的事情和之前的大致情况。

关于 Context 的一些实用建议

  1. 明确指令,减少来回:一次把需求说清楚,减少不必要的对话轮次
  2. 大文件分块处理:不要一次性读一个几千行的文件,分段读
  3. 重要信息写入文件:把关键信息写到 CLAUDE.md 这样的文件里,作为长期记忆
  4. 适时开启新对话:如果对话太长了,可以把关键信息总结一下,开个新对话继续

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 —— "性价比之王"

Claude Haiku —— "极速闪电"

怎么选?

一个实用的选择策略:

你的任务 ──► 简单吗? ──► 是 ──► 用 Haiku
              │
              ▼ 否
        ──► 普通难度? ──► 是 ──► 用 Sonnet
              │
              ▼ 否
        ──► 很复杂? ──► 是 ──► 用 Opus

Anthropic 官方的建议是:大多数情况下用 Sonnet 就够了。只有在需要最强推理能力的时候才上 Opus,在追求速度和低成本的时候才用 Haiku。

在 Agent 应用中,甚至可以混合使用不同的模型。比如:

这样既保证了质量,又控制了成本。


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 → 用户
  │     展示最终结果
  │
  ▼

注意看这个流程中的规律:

四大组件各自的贡献

在这个例子中:

组件 做了什么
Agentic Loop 驱动了 3 轮循环,协调 LLM 和工具的交互
LLM(大脑) 决定了先用 Glob 再用 Grep 的策略,最后整理了结果
Tools(工具) Glob 找到了文件列表,Grep 找到了匹配内容
Context(记忆) 保存了每一轮的结果,让 LLM 能"记住"前面的信息

四个组件缺一不可:


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 的执行过程。思考:

参考答案(展开查看):

点击查看参考答案
第 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 完成这个任务需要哪些工具?

点击查看参考答案

至少需要以下工具:

  1. Write(写文件):创建 fibonacci.py 和 test_fibonacci.py
  2. Bash(执行命令):运行 pytest 来测试
  3. Read(读文件):可能需要读取文件来检查内容(可选)
  4. 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         │  大脑 —— 理解意图、做出决策             │
│  (大语言模型)│  选工具、填参数、判断结果               │
└──────────────┴───────────────────────────────────────┘

核心要点

  1. Agentic Loop 是一个 while 循环,一直转到任务完成或达到上限
  2. 工具本质上是 JSON Schema,LLM 负责填参数,系统负责执行
  3. Context 有大小限制,需要合理管理,否则 Agent 会"失忆"
  4. LLM 是决策者不是执行者,它只负责想和说,不负责做
  5. Agent 和 Workflow 的区别:Agent 运行时动态决策,Workflow 开发时预设路径

一句话总结

Agent = Agentic Loop(心脏) + Tools(双手) + Context(记忆) + LLM(大脑)

心脏不停跳动,大脑不停思考,双手不停干活,记忆不断积累 —— 这就是 Agent 的工作原理。


下一章预告

现在你已经明白了 Agent 是什么(第1章)以及它怎么运作的(第2章),是时候动手了!

第3章:环境准备 —— 把工具都装好

我们将会:

理论学够了,下一章开始写代码!

← 上一章1. Agent 是什么