③Agent 简史,从 ReAct 到 Claude Code

我们回到 2022 年,看看在还没有 function calling 的年代,人们是怎么用纯文本让模型"思考—行动—观察"的;AutoGPT 为什么火得那么快、凉得也那么快;function calling 和 MCP 又分别解决了什么问题。我们还会用 Go 复刻一个 2022 年风格的 ReAct Agent,亲眼看看"格式全靠模型自觉"有多脆。

outlier 15 分钟 5733 字 1 0
系列教程Agent从入门到放弃 第 4 / 5 章
  1. 01Agent 从入门到放弃:教程大纲
  2. 02①大模型、Chatbot、Agent,一次把名字理清楚
  3. 03②Agent 的最小定义,一个 while 循环
  4. 04③Agent 简史,从 ReAct 到 Claude Code
  5. 05④Workflow 还是 Agent?大部分场景你不需要 Agent
查看教程目录

开场:我也给 AutoGPT 交过学费

2023 年 4 月,朋友圈突然被一个叫 AutoGPT 的项目刷屏。演示视频里,有人给它一个目标:"帮我调研一下市场,然后做一个网站。"它就自己开始搜索、写文件、执行命令,一步接一步,看得人热血沸腾。

我当晚就装上了,给它的目标很朴素:"写一个命令行的 todo 小工具。"

接下来的二十分钟,我眼看着它:

  • 搜了一遍"how to build a todo app";
  • 把搜索结果总结成一段话,存进了"长期记忆";
  • 又搜了一遍"best practices for todo app";
  • 觉得需要先调研一下竞品,又搜了几遍;
  • 写了一个 plan.txt;
  • 读了一遍 plan.txt,觉得计划需要完善,又搜了一遍……

二十分钟后,一行代码都没写出来,API 账单上多了几美元。我关掉终端,心想:这东西也就这样了。

两年后,我每天都在用编码 Agent 改代码、修 bug、写测试。回头看,今天的 Agent 和当年的 AutoGPT,核心几乎是同一个循环,就是上一篇那二十行代码。那中间到底发生了什么?

这一篇我们讲点历史。不是为了怀旧,而是因为每一次"Agent 突然能用了",背后都是循环里的某个零件被换掉了。读完本篇,你应该能回答这几个问题:

  • 2022 年的 ReAct 到底提出了什么?为什么今天的 Agent 还在用它的思路?
  • Toolformer 和 function calling 分别解决了什么问题?
  • AutoGPT 为什么火得那么快、凉得也那么快?
  • MCP 是在什么背景下出现的?
  • 为什么偏偏是 2025 年,编码 Agent 全面爆发?

一、先看全景:一条时间线

flowchart LR A["2022.1
思维链 CoT
只会想"] --> B["2022.10
ReAct
边想边做"] B --> C["2023.2
Toolformer
学会用工具"] C --> D["2023.3
AutoGPT
全民狂热"] D --> E["2023.6
function calling
进入官方 API"] E --> F["2024
长上下文
为工具使用训练"] F --> G["2024.11
MCP 发布"] G --> H["2025
编码 Agent
全面爆发"] style B fill:#e3f2fd,stroke:#1565c0 style D fill:#ffcdd2,stroke:#b71c1c style E fill:#e8f5e9,stroke:#2e7d32 style H fill:#fff3e0,stroke:#e65100

这条线上有四个关键转折:ReAct 定下了循环的形状,function calling 把"约定"变成了"协议",MCP 统一了工具的接口,而 2025 年,模型终于强到能把这个循环转起来。 下面一个一个说。

二、2022:ReAct,让模型边想边做

2.1 在 ReAct 之前:要么只想,要么只做

2022 年初,有一篇很有名的论文叫"思维链"(Chain-of-Thought):在 prompt 里给几个"一步一步推理"的示例,模型就会先写推理过程、再给答案,复杂题的正确率明显提高。第 07 篇会讲为什么"先想再答"有用。

但思维链有个硬伤:它只能在脑子里想。 模型不知道的事实,想得再认真也想不出来,反而会一本正经地编。问它"埃菲尔铁塔比自由女神像高多少",它可能会把两个都记错的数字认真地相减。

另一条路是让模型"只做":给它搜索之类的工具,让它直接输出动作。可是没有中间的思考,它不知道为什么要做这一步,也不知道做完之后离目标还有多远。

2.2 ReAct:Thought、Action、Observation 交替进行

2022 年 10 月,Yao 等人发表了 ReAct 论文,名字取自 Reasoning + Acting。思路简单得惊人:让模型交替地"想一步、做一步、看一眼结果",全部用纯文本写出来:

Question: 埃菲尔铁塔比自由女神像高多少米?
Thought 1: 我需要先查埃菲尔铁塔的高度。
Action 1: Search[埃菲尔铁塔]
Observation 1: 埃菲尔铁塔位于巴黎,建成于 1889 年,含天线总高约 330 米。
Thought 2: 埃菲尔铁塔约 330 米。接下来查自由女神像的高度。
Action 2: Search[自由女神]
Observation 2: 没有找到相关词条。
Thought 3: 关键词不对,换成全称再查一次。
Action 3: Search[自由女神像]
Observation 3: 自由女神像位于纽约,1886 年落成,从地面到火炬顶端约 93 米。
Thought 4: 两个高度都有了,算一下差值。
Action 4: Calculate[330-93]
Observation 4: 237
Thought 5: 埃菲尔铁塔比自由女神像高约 237 米。
Action 5: Finish[约 237 米]

这段 trace 是我仿照论文风格写的,放在配套仓库的 examples/ch03-react/sample-trace.txt。注意第 2、3 步:第一次搜索失败了,模型在 Thought 3 里意识到关键词不对,换了个词再搜。这就是"想"和"做"交替的威力:

  • Thought 让行动有方向:每一步做之前先说清楚为什么做,做完之后还记得下一步要什么;
  • Observation 让思考接地气:事实不靠记忆,靠查;查错了,结果会告诉它。
sequenceDiagram participant M as 模型 participant P as 程序 participant W as 小百科 / 计算器 P->>M: 问题 + 格式说明 M-->>P: Thought: 先查埃菲尔铁塔
Action: Search[埃菲尔铁塔] P->>W: 查词条 W-->>P: 约 330 米 P->>M: 原文 + Observation: 约 330 米 M-->>P: Thought: 再查自由女神像
Action: Search[自由女神像] P->>W: 查词条 W-->>P: 约 93 米 P->>M: 原文 + Observation: 约 93 米 M-->>P: Action: Calculate[330-93] P->>W: 计算 W-->>P: 237 P->>M: 原文 + Observation: 237 M-->>P: Action: Finish[约 237 米]

眼熟吗?这就是上一篇的那个循环。ReAct 第一次把"模型想 → 程序做 → 结果喂回去"这个形状写进了论文,今天所有 Agent 都还在用它。

2.3 那个年代怎么"调用工具":正则表达式

关键问题来了:2022 年的 API 里没有 tool_use 这种结构化输出,模型只会吐文字。程序怎么知道它想调用什么?

答案朴素得让人心疼:在 prompt 里约定格式,然后用正则表达式去抠。 配套仓库用 Go 复刻了一个 2022 年风格的 ReAct Agent,examples/ch03-react/main.go 里的 prompt 是这样写的:

可用的 Action 只有三种:
- Search[关键词]:在一个小百科里查找词条,返回词条内容
- Calculate[算式]:计算一个只含数字和 + - * / ( ) 的算式
- Finish[答案]:给出最终答案并结束

严格按下面的格式输出,每次只输出一组 Thought 和 Action,然后停下来等待 Observation:
Thought: 你的思考
Action: Search[...] 或 Calculate[...] 或 Finish[...]

程序这边,用一个正则把 Action 抠出来:

var actionRe = regexp.MustCompile(`Action:\s*(Search|Calculate|Finish)\[(.*)\]`)

还有一个特别关键的小技巧:

resp, err := m.Create(ctx, &llm.Request{
    MaxTokens:     2048,
    Messages:      []llm.Message{llm.UserText(transcript)},
    StopSequences: []string{"Observation:"},
})

StopSequences 的意思是:模型一写出 Observation: 就立刻停下。 为什么要这样?因为模型是个文字接龙高手,它看过无数这种格式的文本,写完 Action 之后会顺手把 Observation 也"编"出来。观察结果必须由程序来填,不能让模型自己编,否则整个"接地气"的设计就白费了。

2.4 脆,太脆了

这套做法能跑,但你能想象它有多脆:

  • 模型多写了一句客套话,正则匹配不上;
  • 模型把方括号写成了中文的【】,匹配不上;
  • 模型一次输出了两组 Action,你只抠到第一个;
  • 参数里有方括号,(.*) 贪婪匹配把后面的内容一起吞了。

配套代码里专门有一段处理"格式不对"的情况:

match := actionRe.FindStringSubmatch(out)
if match == nil {
    // 2023 年最常见的翻车现场:模型没按格式来。
    fmt.Println("!!格式解析失败,提醒模型重新按格式输出")
    transcript += out + "\nObservation: 你的输出不符合格式,请严格输出 Thought: ... 和 Action: ...\n"
    continue
}

每转一圈都可能有一次"格式解析失败",一个任务转二十圈,总有一圈会翻车。格式全靠模型自觉,这是那个年代所有 Agent 的阿喀琉斯之踵。

三、2023 年初:Toolformer,工具可以"训练"进模型

ReAct 是靠 prompt 教模型用工具的。2023 年 2 月,Meta 的 Toolformer 换了个思路:能不能让模型在训练时就学会用工具?

它的做法大致是:让模型在大量文本里尝试插入 API 调用(计算器、问答、搜索、翻译、日历),比如在"今年是 [Calendar()→2023] 年"这样的位置;然后只保留那些确实让后文更好预测的调用,用这些数据微调模型。模型就这样学会了"什么时候该调一下计算器"。

Toolformer 本身没有变成大家天天用的产品,但它传递了一个重要信号:工具使用可以是模型的内置能力,而不只是 prompt 里的约定。 几个月后,各家大模型厂商就在 API 层面把这件事做成了标配。

四、2023 年春:AutoGPT 的狂热与幻灭

4.1 为什么火

2023 年 3 月,GPT-4 发布,大家第一次觉得"这模型好像真的能推理"。几乎同时,AutoGPT、BabyAGI 这类项目登场,在 GitHub 上以惊人的速度涨星。

它们卖的是一个梦:给 AI 一个目标,它自己拆任务、自己执行、自己反思,直到完成。 配套的概念一个比一个响亮:长期记忆(用向量数据库存经验)、自我反思、任务队列、多 Agent 协作……那段时间,好像每个周末都有一个新的"全自动 AI 员工"项目。

4.2 为什么凉

然后,大家发现它们基本干不成事。上一篇从零件的角度分析过原因(模型能力、上下文长度、工具协议),这里补两个当年亲身体会到的:

  1. 成本失控:每一圈都要把越来越长的历史发一遍,而 GPT-4 当时很贵。一个任务转几十圈,跑一次就是几美元,还经常跑不出结果;
  2. 死循环和原地打转:就像我开头那个 todo 工具,它不停地"调研",因为调研永远可以更充分。没有一个外部信号告诉它"够了,该写代码了"。

更深层的原因,是这些任务没法验证。"调研市场""写商业计划"做得好不好,没有编译器,没有测试,Agent 自己评价自己,当然永远觉得"还可以更好"。

flowchart TB G["目标:做一个网站"] --> P["拆成子任务"] P --> S["搜索、总结、存进记忆"] S --> R["自我反思:还不够完善"] R --> P R -.->|"什么时候停?没人知道"| X["账单"] style R fill:#fff9c4 style X fill:#ffcdd2,stroke:#b71c1c

第 39 篇会算一笔账:哪怕每一步有 95% 的成功率,连续 20 步全对的概率也只剩三分之一左右。2023 年的模型,单步成功率远没有 95%。

4.3 留下了什么

AutoGPT 没做成,但它让一整代开发者第一次亲手跑了 Agent 循环,也让大家看清了瓶颈在哪里。之后几年的进展,几乎就是在逐条修复那些瓶颈。另外它也留下了一个教训:"长期记忆""自我反思"这些听起来很高级的模块,解决不了模型能力和任务验证的问题。 第 17 篇讲项目记忆时,我们会看到今天的编码 Agent 选了一条朴素得多的路:一个 Markdown 文件。

五、2023 年 6 月:function calling 把"约定"变成"协议"

5.1 一个 API 字段,终结了正则

2023 年 6 月,OpenAI 在 API 里推出了 function calling:你在请求里用 JSON Schema 描述有哪些函数可调用,模型如果想调用,就不再输出一段自由文本,而是输出一个结构化的调用请求,包括函数名和符合 schema 的 JSON 参数。随后各家厂商陆续跟进,形式略有不同,思路完全一样。

对比一下两个时代:

问题ReAct 文本协议(2022)function calling(2023 之后)
模型怎么表达"我要调用工具"输出约定格式的文字,程序用正则抠结构化的 tool_use / tool_calls
参数格式Search[...] 里随便写JSON Schema 约束
什么时候停下来等结果靠 stop sequence 截断 Observation:stop_reason: tool_use
格式出错怎么办自己检测、自己提醒、再来一次基本不会出错
一次调多个工具很难一条回复里多个调用块

模型那边也变了:它们被专门训练过如何输出这种结构化调用。格式这件事,从"靠模型自觉"变成了"由协议保证"。

5.2 ReAct 并没有过时

有意思的是,function calling 之后,ReAct 的三件套并没有消失,只是换了一身衣服:

ReAct 里的今天的 API 里的
Thought模型回复里的 text 块,或者推理模型的 thinking(第 12 篇)
Actiontool_use 块(DeepSeek 叫 tool_calls)
Observationtool_result 块(DeepSeek 叫 role: "tool" 的消息)
Finish[...]stop_reason: end_turn

形式从文本变成了结构,思想一点没变。 第 19 篇会把这套协议拆到每一个字段。

六、2024:零件一个个到位

2024 年没有某个单一的"爆点",但 Agent 需要的零件在这一年基本凑齐了:

  • 上下文变长了:从几千、几万 token,变成了几十万甚至上百万。读几十个文件不再是问题;
  • 模型开始专门为多步工具使用而训练:不只是"能调工具",而是"能连着调几十次还不忘目标";
  • 有了衡量标准:像 SWE-bench 这样的评测集(用真实开源项目里的 issue 让 Agent 去修,用项目自己的测试判分),让"Agent 写代码到底行不行"有了可以比较的数字。各家的分数在这两年里一路往上涨;
  • 大家开始认真做 harness:工具怎么设计、上下文怎么管理、权限怎么控制,从"随便写写"变成了一门手艺。

这一年还有很多"会自己干活的 AI 程序员"的演示,有的惊艳,有的被质疑演示效果和实际使用差距太大。但方向已经很明确了:编码是 Agent 最先能落地的场景,原因第 05 篇细讲。

七、2024 年 11 月:MCP,工具界的 USB-C

工具调用有了统一的思路,但每个 Agent 产品对接每个工具,还是要各写一套适配代码。想让你的 Agent 能查 GitHub、读 Notion、连数据库?每个都得单独写。N 个 Agent 对接 M 个工具,就是 N×M 套适配。

2024 年 11 月,Anthropic 开源了 MCP(Model Context Protocol):一个让"工具提供方"和"Agent"之间说同一种话的协议。工具方写一个 MCP server,任何支持 MCP 的 Agent 都能直接接上。

flowchart LR subgraph 之前["之前:N × M"] A1["Agent A"] --- T1["GitHub"] A1 --- T2["数据库"] A2["Agent B"] --- T1 A2 --- T2 end subgraph 之后["之后:N + M"] B1["Agent A"] --- MCP(("MCP")) B2["Agent B"] --- MCP MCP --- S1["GitHub server"] MCP --- S2["数据库 server"] end style MCP fill:#fff3e0,stroke:#e65100

后来,其他厂商和工具也陆续支持了 MCP,它逐渐成了事实上的标准。第 21 到 23 篇会专门讲 MCP,还会用 Go 手写一个 MCP server。

八、2025:编码 Agent 全面爆发

到了 2025 年,所有零件都齐了:

  • 2 月,Anthropic 发布了 Claude Code 的研究预览,一个跑在终端里的编码 Agent,5 月正式发布;
  • 4 月,OpenAI 发布了开源的 Codex CLI,Codex 这个名字以 Agent 产品的身份复活(第 01 篇讲过它的身世);
  • 各家 IDE 也纷纷加上 Agent 模式,可以在编辑器里让它自己读代码、改文件、跑命令。

这一次,大家不再只是看演示视频。很多开发者开始每天用它们干活:修 bug、写测试、做重构、读陌生的代码库。

回到开头的问题:今天的 Agent 和 AutoGPT 用的是同一个循环,为什么这次成了? 把这篇讲的串起来就是:

flowchart TB L["同一个循环"] --> M["模型:为多步工具使用训练过"] L --> P["协议:function calling + MCP"] L --> C["上下文:几十万到上百万 token"] L --> H["Harness:工具、权限、压缩都认真做了"] L --> V["任务:代码可以验证"] M & P & C & H & V --> OK["2025:真的能用了"] style OK fill:#fff3e0,stroke:#e65100

没有一个魔法时刻,是循环里的每个零件都被换成了更好的。 这也是第五部分的思路:我们会从最朴素的循环开始,一个零件一个零件地往上加,亲眼看着它从"能跑"变成"能用"。

九、动手:回到 2022 年

实验 1:数一数 ReAct trace(不调用模型)

大纲里的动手作业是"读 ReAct 论文里的示例 trace,数一数 Thought、Action、Observation 各出现了几轮"。配套仓库写了个小工具帮你数:

go run ./examples/ch03-react count examples/ch03-react/sample-trace.txt

输出:

Thought      5
Action       5
Observation  4

想一想:为什么 Observation 比 Action 少一个? 因为最后一个 Action 是 Finish,它不调用任何工具,也就没有观察结果,循环在这里结束。这和上一篇里"最后一轮 stop_reason 是 end_turn"是一回事。

你也可以把 ReAct 论文附录里的真实 trace 复制到一个文本文件里来数。论文里的写法是 Thought 1:、Act 1:、Obs 1:,这个小工具都认得。这个实验也能在 Playground 的第 03 篇里直接跑。

实验 2:跑一个 2022 年风格的 Agent

go run ./examples/ch03-react run "埃菲尔铁塔比自由女神像高多少米?"

不设 Key 的时候,用的是仓库内置的离线演示模型。它认得 ReAct 格式,套路很固定:先把整个问题拿去 Search,拿到观察结果就直接 Finish。输出是这样的:

Thought: 我不知道答案,先查一下资料。
Action: Search[埃菲尔铁塔比自由女神像高多少米?]
Observation: 埃菲尔铁塔位于巴黎,建成于 1889 年,含天线总高约 330 米。
Thought: 资料查到了,直接据此作答。
Action: Finish[埃菲尔铁塔位于巴黎,建成于 1889 年,含天线总高约 330 米。]

最终答案:埃菲尔铁塔位于巴黎,建成于 1889 年,含天线总高约 330 米。(共 2 轮)

答非所问,对吧?演示模型不会推理,它只是演示了格式:程序怎么用 stop sequence 截住、怎么用正则抠出 Action、怎么把 Observation 拼回 transcript。

想看真正的 ReAct,要接真实模型,在线 Playground 演示不了:先 export DEEPSEEK_API_KEY=sk-... 再跑同一条命令。

接上 DeepSeek 之后,你多半会看到它像 sample-trace 那样,分别查两座建筑的高度,再调用 Calculate 算差值。多跑几次,换几个问题(比如"东方明珠比埃菲尔铁塔高多少米?"),留意有没有出现"格式解析失败"。

实验 3:亲手把格式弄坏

打开 examples/ch03-react/main.go,试着做这些修改,每改一处,用真实模型跑一次:

  1. 删掉 StopSequences:观察模型会不会自己编 Observation。它编出来的数字,可能和小百科里的不一样;
  2. 把 prompt 里"严格按下面的格式输出"那一段删掉:看格式解析失败的次数会不会变多;
  3. 把正则改严一点,比如要求 Action: 后面只能有一个空格:体会一下"正则写宽了吞内容,写窄了匹配不上"的两难。

做完这三个实验,你会真正理解第 19 篇那句话:function calling 把"约定"变成了"协议"。

实验 4:对比一下今天的写法

回头看第 01 篇的 agent 子命令,两个程序干的是同一类事情:

go run ./examples/ch01-three-layers agent

在 examples/ch01-three-layers/main.go 里,工具是用 JSON Schema 定义的,模型的调用意图从 resp.ToolUses() 里直接拿,没有一行正则。对比着读两份代码,大概十分钟,这是理解这几年进步最直观的方式。

十、踩坑记录:复刻 ReAct 时翻的车

坑 1:没设 stop sequence,模型自己编了观察结果。
我第一版忘了加 StopSequences,模型一口气写完了 Thought、Action、Observation、再一轮 Thought……整段推理一气呵成,看起来特别聪明,可惜里面的"观察结果"全是它编的。凡是应该由外部世界提供的信息,都不能让模型自己写。 这条原则放到今天的 Agent 里同样成立。

坑 2:正则写得太宽,吞掉了后面的内容。
\[(.*)\] 是贪婪匹配,如果模型在同一行后面又写了一个方括号,参数就会把中间的东西全吞进去。改成非贪婪的 (.*?) 又会在参数本身含方括号时截断。正则解析自然语言,本来就没有完美解。

坑 3:把整段 transcript 塞进一条 user 消息。
2022 年的 API 很多是"补全"接口,只有一段文本。配套代码为了还原当年的感觉,也是把整个 transcript 拼成一条消息发出去。这样做的问题是:模型分不清哪些是它自己说的,哪些是程序填的。 今天的 API 用 assistant / user / tool 几种角色把它们分开,模型看得清清楚楚。

坑 4:让工具直接执行模型给的代码。
Calculate 工具最省事的写法,是把模型给的算式丢给某个 eval 去算。千万别这么干。配套代码里的 calc.go 专门手写了一个只认数字和加减乘除的解析器,开头的注释写得很直白:"Agent 的工具永远不该直接执行模型给的代码。"今天的 Agent 能跑命令,所以第 27 篇会花一整篇讲权限。

坑 5:以为"历史"没用。
很多人觉得 ReAct、AutoGPT 都是老黄历了,没必要了解。但你会发现,今天 Agent 产品里遇到的问题,比如死循环、原地打转、模型编造工具结果、上下文爆掉,当年全都遇到过。知道它们是怎么被解决的,你才知道哪些问题已经被解决了,哪些还没有。

一句话总结

ReAct 定下了"想—做—看"的循环,function calling 把格式从约定变成协议,MCP 统一了工具的接口;这个循环 2022 年就有了,2025 年它之所以真的能用,是因为循环里的每个零件都被换成了更好的。

flowchart LR R["ReAct
循环的形状"] --> F["function calling
结构化的调用"] F --> M["MCP
统一的工具接口"] M --> A["编码 Agent
能干活了"] style R fill:#e3f2fd,stroke:#1565c0 style F fill:#e8f5e9,stroke:#2e7d32 style M fill:#fff9c4 style A fill:#fff3e0,stroke:#e65100

评论

还没有评论,来说点什么吧。

发表评论

Ctrl + Enter 快速提交
Enter 搜索Esc 关闭