【AI Agent 与 Super Agent 构建实战】第 07 篇:ReAct 模式详解 --- 推理与行动的交替循环
本系列定位:用 Go 语言从零构建各类 AI Agent,覆盖 ReAct、Plan-and-Execute、Multi-Agent、Super Agent 等核心设计模式的完整工程实现。
模块 2 开篇。从本篇起,我们进入「单 Agent 架构与工具系统」。ReAct 是所有 Agent 范式的鼻祖,后续的 Plan-and-Execute、Tree-of-Thoughts、多智能体编排,本质上都是对 ReAct 循环的变体与扩展。把这一篇吃透,等于拿到了整座 Agent 大厦的地基。
本篇你将学到
- ReAct 论文的核心思想:Reasoning(推理)+ Acting(行动)交错进行
- Thought → Action → Observation 三段式循环的结构与意义
- 用 Go 从零实现一个可运行的 ReAct 循环引擎
- ReAct 与 Function Calling 的本质区别:Prompt 驱动 vs. API 原生
- 停止条件设计:最大迭代、完成信号、异常熔断
一句话总结:ReAct 让 Agent 学会「先想后做」------把 LLM 的语言推理能力和外部工具的执行能力编织成一条紧密交替的决策链。
一、为什么需要 ReAct:纯推理与纯行动的局限
在 ReAct 出现之前(2022 年),让 LLM 解决复杂任务有两条路,但都有明显短板。
路线 A:纯推理(Chain-of-Thought)。模型只在脑子里想,不接触外部世界。问题在于:它的「知识」冻结在训练截止日,算错一步就一路错到底,无法查证、无法纠偏。
路线 B:纯行动(早期 Function Calling)。模型直接吐出一个工具调用,没有中间推理。问题在于:没有「思考」环节,模型选错工具、传错参数时,系统无从知道它为什么这么选,也无法在失败后自我修正。
ReAct(论文 ReAct: Synergizing Reasoning and Acting in Language Models , Yao et al., 2022)的核心贡献是:让推理和行动交替进行。
#mermaid-svg-8pVXFXozK5hASVL3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8pVXFXozK5hASVL3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8pVXFXozK5hASVL3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8pVXFXozK5hASVL3 .error-icon{fill:#552222;}#mermaid-svg-8pVXFXozK5hASVL3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8pVXFXozK5hASVL3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8pVXFXozK5hASVL3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8pVXFXozK5hASVL3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8pVXFXozK5hASVL3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8pVXFXozK5hASVL3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8pVXFXozK5hASVL3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8pVXFXozK5hASVL3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8pVXFXozK5hASVL3 .marker.cross{stroke:#333333;}#mermaid-svg-8pVXFXozK5hASVL3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8pVXFXozK5hASVL3 p{margin:0;}#mermaid-svg-8pVXFXozK5hASVL3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8pVXFXozK5hASVL3 .cluster-label text{fill:#333;}#mermaid-svg-8pVXFXozK5hASVL3 .cluster-label span{color:#333;}#mermaid-svg-8pVXFXozK5hASVL3 .cluster-label span p{background-color:transparent;}#mermaid-svg-8pVXFXozK5hASVL3 .label text,#mermaid-svg-8pVXFXozK5hASVL3 span{fill:#333;color:#333;}#mermaid-svg-8pVXFXozK5hASVL3 .node rect,#mermaid-svg-8pVXFXozK5hASVL3 .node circle,#mermaid-svg-8pVXFXozK5hASVL3 .node ellipse,#mermaid-svg-8pVXFXozK5hASVL3 .node polygon,#mermaid-svg-8pVXFXozK5hASVL3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8pVXFXozK5hASVL3 .rough-node .label text,#mermaid-svg-8pVXFXozK5hASVL3 .node .label text,#mermaid-svg-8pVXFXozK5hASVL3 .image-shape .label,#mermaid-svg-8pVXFXozK5hASVL3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-8pVXFXozK5hASVL3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8pVXFXozK5hASVL3 .rough-node .label,#mermaid-svg-8pVXFXozK5hASVL3 .node .label,#mermaid-svg-8pVXFXozK5hASVL3 .image-shape .label,#mermaid-svg-8pVXFXozK5hASVL3 .icon-shape .label{text-align:center;}#mermaid-svg-8pVXFXozK5hASVL3 .node.clickable{cursor:pointer;}#mermaid-svg-8pVXFXozK5hASVL3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8pVXFXozK5hASVL3 .arrowheadPath{fill:#333333;}#mermaid-svg-8pVXFXozK5hASVL3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8pVXFXozK5hASVL3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8pVXFXozK5hASVL3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8pVXFXozK5hASVL3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8pVXFXozK5hASVL3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8pVXFXozK5hASVL3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8pVXFXozK5hASVL3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8pVXFXozK5hASVL3 .cluster text{fill:#333;}#mermaid-svg-8pVXFXozK5hASVL3 .cluster span{color:#333;}#mermaid-svg-8pVXFXozK5hASVL3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8pVXFXozK5hASVL3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8pVXFXozK5hASVL3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-8pVXFXozK5hASVL3 .icon-shape,#mermaid-svg-8pVXFXozK5hASVL3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8pVXFXozK5hASVL3 .icon-shape p,#mermaid-svg-8pVXFXozK5hASVL3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8pVXFXozK5hASVL3 .icon-shape .label rect,#mermaid-svg-8pVXFXozK5hASVL3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8pVXFXozK5hASVL3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8pVXFXozK5hASVL3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8pVXFXozK5hASVL3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户问题
Thought 1
我需要先查...
Action 1
调用搜索工具
Observation 1
搜索结果
Thought 2
根据结果,还需要...
Action 2
调用计算工具
Observation 2
计算结果
Thought 3
信息齐全,可以回答
Final Answer
上图就是 ReAct 的灵魂。每一次 Thought 都是一次显式推理,每一步 Observation 都给下一步推理注入新的、来自真实世界的信息。这正是「Agent」区别于「Chatbot」的根本所在------Agent 的决策基于环境反馈,而非一次性预测。
1.1 ReAct 的三大收益
| 收益 | 说明 | 示例 |
|---|---|---|
| 可纠错 | 观察到错误结果后,下一轮 Thought 能调整策略 | 搜索无果 → 换关键词重搜 |
| 可追溯 | 每一步思考都写进上下文,便于调试与审计 | 日志里能看到「为什么调了这个工具」 |
| 可组合 | 复杂任务被自然拆解为多步小任务 | 「查 A、算 B、再综合 C」自动分步 |
二、Thought-Action-Observation 循环详解
ReAct 把 Agent 的一次完整推理过程拆成三个反复出现的阶段,业内称为 TAO 循环。
2.1 三阶段的职责
Thought(思考):模型用自然语言写出当前这一步的推理。关键点是「写出来」------不是隐式的内部状态,而是显式的、进入上下文窗口的文本。这种「显式化」让推理可观测、可纠偏,也让模型在长链路中不容易「失忆」。
Action(行动) :模型决定调用哪个工具、传什么参数。在 ReAct 的原始论文里,Action 是一段结构化文本(如 Search[Go 1.23 release]);在现代实现里,Action 通常以 JSON 形式表达,便于程序解析。
Observation(观察):工具执行后返回的结果,被原样(或截断后)追加进上下文。模型在下一轮看到它,作为新的推理依据。
2.2 一个完整的 ReAct 轨迹
假设用户问:「Go 1.23 引入了哪些新特性?举一个代码例子。」
Thought: 用户想知道 Go 1.23 的新特性。我需要先搜索相关信息,然后找一个具体的代码示例。
Action: {"tool": "search_web", "args": {"query": "Go 1.23 release notes new features"}}
Observation: Go 1.23 于 2024 年 8 月发布。主要新特性:(1) iter 包提供范围遍历函数; (2) slices 和 maps 包新增迭代器; (3) timer 行为变更...
Thought: 搜索结果显示核心是 range-over-func 和新的 iter 包。我应该再查一下 range-over-func 的具体语法示例。
Action: {"tool": "search_web", "args": {"query": "Go 1.23 range over function example"}}
Observation: 示例: for v := range slices.Values(s) {...} 可以遍历切片。Custom iterator 需实现 func(yield func(K,V) bool)。
Thought: 现在信息齐全,我可以给出包含新特性的总结和一个 range-over-func 代码示例。
Final Answer: Go 1.23 的主要新特性包括... [此处给出完整回答和代码]
注意三个细节:第一,每一步 Thought 都引用了上一步的 Observation,这就是「基于环境的推理」。第二,Action 是 JSON 结构,方便我们的 Go 程序解析。第三,最后一轮用 Final Answer 作为完成信号,跳出循环。
2.3 用序列图看清交互时序
工具 LLM ReAct 引擎 用户 工具 LLM ReAct 引擎 用户 #mermaid-svg-uqQSI3de1m9h4w7L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uqQSI3de1m9h4w7L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uqQSI3de1m9h4w7L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uqQSI3de1m9h4w7L .error-icon{fill:#552222;}#mermaid-svg-uqQSI3de1m9h4w7L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uqQSI3de1m9h4w7L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uqQSI3de1m9h4w7L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uqQSI3de1m9h4w7L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uqQSI3de1m9h4w7L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uqQSI3de1m9h4w7L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uqQSI3de1m9h4w7L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uqQSI3de1m9h4w7L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uqQSI3de1m9h4w7L .marker.cross{stroke:#333333;}#mermaid-svg-uqQSI3de1m9h4w7L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uqQSI3de1m9h4w7L p{margin:0;}#mermaid-svg-uqQSI3de1m9h4w7L .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uqQSI3de1m9h4w7L text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-uqQSI3de1m9h4w7L .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uqQSI3de1m9h4w7L .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-uqQSI3de1m9h4w7L .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-uqQSI3de1m9h4w7L .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-uqQSI3de1m9h4w7L #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-uqQSI3de1m9h4w7L .sequenceNumber{fill:white;}#mermaid-svg-uqQSI3de1m9h4w7L #sequencenumber{fill:#333;}#mermaid-svg-uqQSI3de1m9h4w7L #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-uqQSI3de1m9h4w7L .messageText{fill:#333;stroke:none;}#mermaid-svg-uqQSI3de1m9h4w7L .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uqQSI3de1m9h4w7L .labelText,#mermaid-svg-uqQSI3de1m9h4w7L .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-uqQSI3de1m9h4w7L .loopText,#mermaid-svg-uqQSI3de1m9h4w7L .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-uqQSI3de1m9h4w7L .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-uqQSI3de1m9h4w7L .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-uqQSI3de1m9h4w7L .noteText,#mermaid-svg-uqQSI3de1m9h4w7L .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-uqQSI3de1m9h4w7L .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uqQSI3de1m9h4w7L .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uqQSI3de1m9h4w7L .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-uqQSI3de1m9h4w7L .actorPopupMenu{position:absolute;}#mermaid-svg-uqQSI3de1m9h4w7L .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-uqQSI3de1m9h4w7L .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-uqQSI3de1m9h4w7L .actor-man circle,#mermaid-svg-uqQSI3de1m9h4w7L line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-uqQSI3de1m9h4w7L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} loop 直到 Final Answer 或达到 max_iter "Go 1.23 新特性?" 发送上下文(含历史 Thought/Action/Obs) Thought + Action(JSON) 解析 Action, 调用工具 Observation 把 Thought+Action+Obs 追加进上下文 最后一轮(若仍未完成) Final Answer 返回最终答案 + 完整轨迹
这张图是本篇最重要的一张图。ReAct 引擎本质上是一个 while 循环 :每轮把累积的上下文发给 LLM,解析出 Action,执行工具,把 Observation 塞回上下文,直到模型给出 Final Answer 或触发停止条件。
三、ReAct 与 Function Calling 的本质区别
第 04 篇我们已经实现了 Function Calling。很多初学者会问:Function Calling 不也是「调工具」吗?它和 ReAct 到底什么关系?
答案是:它们是两个不同层面的东西。
| 维度 | Function Calling | ReAct |
|---|---|---|
| 本质 | LLM 厂商提供的 API 能力 | 一种 Agent 设计模式 |
| 推理过程 | 隐式(模型内部,不可见) | 显式(Thought 写进上下文,可见) |
| 谁来驱动循环 | SDK / Agent 代码 | ReAct 引擎(你自己写) |
| 多步任务 | 需要你自己写 while 循环拼装 | 循环结构是模式的一部分 |
| 可调试性 | 较弱(不知道模型「为什么」选这个工具) | 强(每步 Thought 都是解释) |
| 依赖 | 必须用支持 FC 的模型 | 任何能对话的模型都行 |
一句话:Function Calling 是「执行器」,ReAct 是「指挥执行器的策略」。事实上,现代 ReAct 实现常常在 Action 环节用 Function Calling 来执行工具(因为 FC 的参数解析更可靠),但顶层循环和 Thought 显式化仍然由 ReAct 模式来组织。第 12 篇的 AgentForge v0.2 就是这种「ReAct 外壳 + FC 执行器」的混合架构。
#mermaid-svg-HAGcuy12xEFIiG8H{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HAGcuy12xEFIiG8H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HAGcuy12xEFIiG8H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HAGcuy12xEFIiG8H .error-icon{fill:#552222;}#mermaid-svg-HAGcuy12xEFIiG8H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HAGcuy12xEFIiG8H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HAGcuy12xEFIiG8H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HAGcuy12xEFIiG8H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HAGcuy12xEFIiG8H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HAGcuy12xEFIiG8H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HAGcuy12xEFIiG8H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HAGcuy12xEFIiG8H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HAGcuy12xEFIiG8H .marker.cross{stroke:#333333;}#mermaid-svg-HAGcuy12xEFIiG8H svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HAGcuy12xEFIiG8H p{margin:0;}#mermaid-svg-HAGcuy12xEFIiG8H .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HAGcuy12xEFIiG8H .cluster-label text{fill:#333;}#mermaid-svg-HAGcuy12xEFIiG8H .cluster-label span{color:#333;}#mermaid-svg-HAGcuy12xEFIiG8H .cluster-label span p{background-color:transparent;}#mermaid-svg-HAGcuy12xEFIiG8H .label text,#mermaid-svg-HAGcuy12xEFIiG8H span{fill:#333;color:#333;}#mermaid-svg-HAGcuy12xEFIiG8H .node rect,#mermaid-svg-HAGcuy12xEFIiG8H .node circle,#mermaid-svg-HAGcuy12xEFIiG8H .node ellipse,#mermaid-svg-HAGcuy12xEFIiG8H .node polygon,#mermaid-svg-HAGcuy12xEFIiG8H .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HAGcuy12xEFIiG8H .rough-node .label text,#mermaid-svg-HAGcuy12xEFIiG8H .node .label text,#mermaid-svg-HAGcuy12xEFIiG8H .image-shape .label,#mermaid-svg-HAGcuy12xEFIiG8H .icon-shape .label{text-anchor:middle;}#mermaid-svg-HAGcuy12xEFIiG8H .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HAGcuy12xEFIiG8H .rough-node .label,#mermaid-svg-HAGcuy12xEFIiG8H .node .label,#mermaid-svg-HAGcuy12xEFIiG8H .image-shape .label,#mermaid-svg-HAGcuy12xEFIiG8H .icon-shape .label{text-align:center;}#mermaid-svg-HAGcuy12xEFIiG8H .node.clickable{cursor:pointer;}#mermaid-svg-HAGcuy12xEFIiG8H .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HAGcuy12xEFIiG8H .arrowheadPath{fill:#333333;}#mermaid-svg-HAGcuy12xEFIiG8H .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HAGcuy12xEFIiG8H .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HAGcuy12xEFIiG8H .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HAGcuy12xEFIiG8H .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HAGcuy12xEFIiG8H .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HAGcuy12xEFIiG8H .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HAGcuy12xEFIiG8H .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HAGcuy12xEFIiG8H .cluster text{fill:#333;}#mermaid-svg-HAGcuy12xEFIiG8H .cluster span{color:#333;}#mermaid-svg-HAGcuy12xEFIiG8H div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HAGcuy12xEFIiG8H .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HAGcuy12xEFIiG8H rect.text{fill:none;stroke-width:0;}#mermaid-svg-HAGcuy12xEFIiG8H .icon-shape,#mermaid-svg-HAGcuy12xEFIiG8H .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HAGcuy12xEFIiG8H .icon-shape p,#mermaid-svg-HAGcuy12xEFIiG8H .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HAGcuy12xEFIiG8H .icon-shape .label rect,#mermaid-svg-HAGcuy12xEFIiG8H .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HAGcuy12xEFIiG8H .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HAGcuy12xEFIiG8H .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HAGcuy12xEFIiG8H :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 基础设施
执行层
ReAct 层
Thought 显式推理
循环控制
停止条件判断
Function Calling
参数生成与调用
工具执行
LLM Provider
工具注册表
四、用 Go 实现 ReAct 循环引擎
现在动手。我们要实现一个不依赖特定厂商 FC 能力的「纯 Prompt 版」ReAct 引擎------这样它能在任何模型上跑(包括 Ollama 本地模型)。
4.1 设计 Action 的 JSON 协议
先约定模型输出的 Action 格式。我们用一个简单的 JSON:
json
{
"thought": "我需要先搜索 Go 1.23 的信息",
"action": "search_web",
"args": {"query": "Go 1.23 release notes"},
"final": false
}
thought:本轮推理(必须显式写出)action:要调用的工具名;若final=true则可省略args:工具参数final:是否给出最终答案(完成信号)
当 final=true 时,我们要求 thought 字段直接放最终答案。
4.2 ReAct 引擎核心结构
go
// internal/agent/react.go
package agent
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/yourname/agentforge/internal/provider"
)
// ReActEngine ReAct 循环引擎
type ReActEngine struct {
llm provider.LLMProvider // LLM 提供者
tools map[string]Tool // 工具注册表
model string // 模型名
maxIterations int // 最大迭代次数(停止条件 1)
systemPrompt string // 系统 Prompt(含 ReAct 指令)
}
// Tool 工具接口(第 08 篇会升级为完整 Registry)
type Tool interface {
Name() string
Description() string
Execute(args map[string]interface{}) (string, error)
}
// reactAction 模型输出的结构化动作
type reactAction struct {
Thought string `json:"thought"`
Action string `json:"action"`
Args map[string]interface{} `json:"args"`
Final bool `json:"final"`
}
// NewReActEngine 创建引擎
func NewReActEngine(llm provider.LLMProvider, model string) *ReActEngine {
return &ReActEngine{
llm: llm,
tools: make(map[string]Tool),
model: model,
maxIterations: 8, // 默认上限
systemPrompt: defaultReActSystemPrompt,
}
}
// RegisterTool 注册工具
func (e *ReActEngine) RegisterTool(t Tool) {
e.tools[t.Name()] = t
}
// SetMaxIterations 设置最大迭代(停止条件可调)
func (e *ReActEngine) SetMaxIterations(n int) {
if n > 0 {
e.maxIterations = n
}
}
4.3 ReAct System Prompt 设计
Prompt 是纯 Prompt 版 ReAct 的灵魂。它必须明确告诉模型:输出 JSON、按 TAO 循环、何时给出 Final。
go
const defaultReActSystemPrompt = `你是一个遵循 ReAct(Reasoning + Acting)模式的智能助手。
面对用户的任务,你需要交替进行「思考」和「行动」,直到能给出最终答案。
## 工作流程
每一轮你必须输出一个 JSON 对象,格式严格如下:
{
"thought": "你对当前情况的推理:我已知什么、还缺什么、下一步该做什么",
"action": "要调用的工具名称(给出最终答案时留空)",
"args": {"参数名": "参数值"},
"final": false
}
## 规则
1. 每一轮都要先在 "thought" 里写出推理过程,禁止跳过。
2. 当你认为信息足够回答用户时,设 "final": true,并把最终答案写在 "thought" 字段。
3. "action" 必须是下列可用工具之一,不要编造工具名。
4. "args" 必须符合该工具的参数要求。
5. 只输出 JSON,不要输出任何其他文字。
## 可用工具
{{TOOLS}}
`
注意 {``{TOOLS}} 占位符------运行时我们会把注册的工具名和描述拼接进去。
4.4 主循环:Run 方法
这是引擎的心脏。对照前面的序列图阅读:
go
// ReactStep 记录一步轨迹(用于展示和调试)
type ReactStep struct {
Iteration int
Thought string
Action string
Args map[string]interface{}
Observation string
IsFinal bool
}
// Run 执行 ReAct 循环
func (e *ReActEngine) Run(ctx context.Context, userInput string) (answer string, trace []ReactStep, err error) {
// 1. 拼装 System Prompt(注入工具描述)
sysPrompt := e.buildSystemPrompt()
// 2. 初始化上下文消息
messages := []provider.Message{
{Role: "system", Content: sysPrompt},
{Role: "user", Content: userInput},
}
// 3. 进入 TAO 循环
for i := 1; i <= e.maxIterations; i++ {
// 3.1 调用 LLM 获取本轮 Thought+Action
resp, err := e.callLLM(ctx, messages)
if err != nil {
return "", trace, fmt.Errorf("第 %d 轮 LLM 调用失败: %w", i, err)
}
// 3.2 解析 JSON(带容错)
action, perr := parseReactJSON(resp)
if perr != nil {
// 解析失败 → 把错误作为 Observation 喂回去,让模型自我修正
messages = append(messages, provider.Message{
Role: "assistant",
Content: resp,
}, provider.Message{
Role: "user",
Content: "[系统提示] 上一轮输出不是合法 JSON,请严格按格式重新输出。" + perr.Error(),
})
trace = append(trace, ReactStep{
Iteration: i, Thought: "(解析失败)", Observation: perr.Error(),
})
continue
}
// 3.3 完成信号 → 跳出循环
if action.Final {
trace = append(trace, ReactStep{
Iteration: i, Thought: action.Thought, IsFinal: true,
})
return action.Thought, trace, nil
}
// 3.4 执行工具,得到 Observation
observation := e.executeAction(ctx, action)
step := ReactStep{
Iteration: i, Thought: action.Thought, Action: action.Action,
Args: action.Args, Observation: observation,
}
trace = append(trace, step)
// 3.5 把本轮 Thought+Action 和 Observation 追加进上下文
messages = append(messages, provider.Message{
Role: "assistant",
Content: fmt.Sprintf(`{"thought":%q,"action":%q,"args":%s,"final":false}`,
action.Thought, action.Action, mustJSON(action.Args)),
}, provider.Message{
Role: "user",
Content: "Observation: " + observation,
})
}
// 4. 达到最大迭代仍未完成(停止条件 1 触发)
return "", trace, fmt.Errorf("ReAct 达到最大迭代次数 %d 仍未给出最终答案", e.maxIterations)
}
这里的循环结构和第二节的理论完全对应:调 LLM → 解析 → 判断是否完成 → 执行工具 → 把 Observation 塞回上下文。
4.5 工具执行与错误熔断
go
// executeAction 执行 Action,返回 Observation 字符串
func (e *ReActEngine) executeAction(ctx context.Context, a reactAction) string {
tool, ok := e.tools[a.Action]
if !ok {
// 停止条件 2:模型编造了不存在的工具
return fmt.Sprintf("[错误] 未知工具 '%s'。可用工具: %s",
a.Action, e.availableToolNames())
}
// 工具执行超时保护(停止条件 3 的前置)
done := make(chan string, 1)
go func() {
result, err := tool.Execute(a.Args)
if err != nil {
done <- fmt.Sprintf("[工具执行错误] %s: %v", a.Action, err)
return
}
done <- result
}()
select {
case r := <-done:
return r
case <-time.After(30 * time.Second):
return fmt.Sprintf("[超时] 工具 %s 执行超过 30 秒", a.Action)
case <-ctx.Done():
return "[中断] 上下文已取消"
}
}
func (e *ReActEngine) availableToolNames() string {
names := make([]string, 0, len(e.tools))
for n := range e.tools {
names = append(names, n)
}
return strings.Join(names, ", ")
}
关键设计:工具执行错误不会让整个循环崩溃,而是变成 Observation 喂给模型。这样模型能在下一轮 Thought 里说「上个工具报错了,我换个思路」------这正是 ReAct 可纠错的来源。
4.6 JSON 解析容错(实战必备)
模型偶尔会输出带 markdown 代码块包裹的 JSON,或者前后带废话。我们需要一个容错解析器:
go
// parseReactJSON 容错地解析模型输出为 reactAction
func parseReactJSON(raw string) (reactAction, error) {
var act reactAction
s := strings.TrimSpace(raw)
// 去除可能的 ```json ... ```包裹
if strings.HasPrefix(s, "```") {
s = strings.TrimPrefix(s, "```json")
s = strings.TrimPrefix(s, "```")
s = strings.TrimSuffix(s, "```")
s = strings.TrimSpace(s)
}
// 截取第一个 { 到最后一个 }
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start < 0 || end < 0 || end <= start {
return act, fmt.Errorf("未找到合法 JSON 边界")
}
s = s[start : end+1]
if err := json.Unmarshal([]byte(s), &act); err != nil {
return act, fmt.Errorf("JSON 反序列化失败: %w", err)
}
return act, nil
}
func mustJSON(v interface{}) string {
b, _ := json.Marshal(v)
return string(b)
}
4.7 构建 System Prompt(注入工具描述)
go
func (e *ReActEngine) buildSystemPrompt() string {
var b strings.Builder
b.WriteString(strings.Replace(e.systemPrompt, "{{TOOLS}}", e.toolDescriptions(), 1))
return b.String()
}
func (e *ReActEngine) toolDescriptions() string {
var lines []string
for _, t := range e.tools {
lines = append(lines, fmt.Sprintf("- %s: %s", t.Name(), t.Description()))
}
return strings.Join(lines, "\n")
}
五、停止条件设计
ReAct 循环如果不设停止条件,要么死循环,要么 token 烧爆。我们设计了三层停止条件,前面的代码已经用到,这里系统总结。
#mermaid-svg-5JiUeolgx5QCyQE9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5JiUeolgx5QCyQE9 .error-icon{fill:#552222;}#mermaid-svg-5JiUeolgx5QCyQE9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5JiUeolgx5QCyQE9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5JiUeolgx5QCyQE9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5JiUeolgx5QCyQE9 .marker.cross{stroke:#333333;}#mermaid-svg-5JiUeolgx5QCyQE9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5JiUeolgx5QCyQE9 p{margin:0;}#mermaid-svg-5JiUeolgx5QCyQE9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5JiUeolgx5QCyQE9 .cluster-label text{fill:#333;}#mermaid-svg-5JiUeolgx5QCyQE9 .cluster-label span{color:#333;}#mermaid-svg-5JiUeolgx5QCyQE9 .cluster-label span p{background-color:transparent;}#mermaid-svg-5JiUeolgx5QCyQE9 .label text,#mermaid-svg-5JiUeolgx5QCyQE9 span{fill:#333;color:#333;}#mermaid-svg-5JiUeolgx5QCyQE9 .node rect,#mermaid-svg-5JiUeolgx5QCyQE9 .node circle,#mermaid-svg-5JiUeolgx5QCyQE9 .node ellipse,#mermaid-svg-5JiUeolgx5QCyQE9 .node polygon,#mermaid-svg-5JiUeolgx5QCyQE9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5JiUeolgx5QCyQE9 .rough-node .label text,#mermaid-svg-5JiUeolgx5QCyQE9 .node .label text,#mermaid-svg-5JiUeolgx5QCyQE9 .image-shape .label,#mermaid-svg-5JiUeolgx5QCyQE9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-5JiUeolgx5QCyQE9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5JiUeolgx5QCyQE9 .rough-node .label,#mermaid-svg-5JiUeolgx5QCyQE9 .node .label,#mermaid-svg-5JiUeolgx5QCyQE9 .image-shape .label,#mermaid-svg-5JiUeolgx5QCyQE9 .icon-shape .label{text-align:center;}#mermaid-svg-5JiUeolgx5QCyQE9 .node.clickable{cursor:pointer;}#mermaid-svg-5JiUeolgx5QCyQE9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5JiUeolgx5QCyQE9 .arrowheadPath{fill:#333333;}#mermaid-svg-5JiUeolgx5QCyQE9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5JiUeolgx5QCyQE9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5JiUeolgx5QCyQE9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5JiUeolgx5QCyQE9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5JiUeolgx5QCyQE9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5JiUeolgx5QCyQE9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5JiUeolgx5QCyQE9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5JiUeolgx5QCyQE9 .cluster text{fill:#333;}#mermaid-svg-5JiUeolgx5QCyQE9 .cluster span{color:#333;}#mermaid-svg-5JiUeolgx5QCyQE9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5JiUeolgx5QCyQE9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5JiUeolgx5QCyQE9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-5JiUeolgx5QCyQE9 .icon-shape,#mermaid-svg-5JiUeolgx5QCyQE9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5JiUeolgx5QCyQE9 .icon-shape p,#mermaid-svg-5JiUeolgx5QCyQE9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5JiUeolgx5QCyQE9 .icon-shape .label rect,#mermaid-svg-5JiUeolgx5QCyQE9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5JiUeolgx5QCyQE9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5JiUeolgx5QCyQE9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5JiUeolgx5QCyQE9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是
否
是
否
每轮开始
达到 max_iter?
停止: 迭代上限
模型给 Final?
停止: 自然完成
执行工具
连续 N 轮无进展?
相同 Action+相同 Args
停止: 死循环熔断
进入下一轮
| 停止条件 | 触发场景 | 处理方式 |
|---|---|---|
| 最大迭代 | 默认 8 轮,复杂任务可调到 15-20 | 返回已收集的部分信息 + 错误 |
| 完成信号 | 模型输出 final: true |
返回 thought 字段作为答案 |
| 死循环熔断 | 连续 2-3 轮 Action+Args 完全相同 | 提前终止,避免无效 token 消耗 |
死循环熔断的代码可以作为练习补充进 Run 方法:维护一个 lastActionSignature 字符串,若本轮和上一轮的 action+args 拼接后相同计数加一,达到阈值就退出。
下面是补全熔断逻辑的片段:
go
// 在 Run 方法循环体内,执行工具之前加入:
sig := action.Action + "|" + mustJSON(action.Args)
if sig == lastSig {
repeatCount++
if repeatCount >= 2 {
return action.Thought, trace,
fmt.Errorf("ReAct 检测到死循环: 连续重复 Action '%s'", action.Action)
}
} else {
repeatCount = 0
}
lastSig = sig
记得在循环外声明 var lastSig string 和 repeatCount := 0。
六、运行示例
把上面的引擎和一个简单的搜索工具组合起来(工具实现沿用第 04 篇风格):
go
// 一个演示用工具
type mockSearchTool struct{}
func (m *mockSearchTool) Name() string { return "search_web" }
func (m *mockSearchTool) Description() string { return "搜索互联网,返回结果摘要。args: {query: string}" }
func (m *mockSearchTool) Execute(args map[string]interface{}) (string, error) {
q, _ := args["query"].(string)
return fmt.Sprintf("搜索 '%s' 的结果: [模拟] Go 1.23 于 2024-08 发布,含 iter 包、range-over-func", q), nil
}
func main() {
llm := provider.New(...) // 沿用第 02 篇的 Provider
engine := agent.NewReActEngine(llm, "qwen2.5:14b")
engine.RegisterTool(&mockSearchTool{})
engine.SetMaxIterations(6)
answer, trace, err := engine.Run(context.Background(),
"Go 1.23 有什么新特性?给我一个简短总结。")
if err != nil {
fmt.Println("错误:", err)
}
fmt.Println("=== 最终答案 ===")
fmt.Println(answer)
fmt.Println("=== 推理轨迹 ===")
for _, s := range trace {
fmt.Printf("[轮 %d] 思考: %s\n", s.Iteration, truncate(s.Thought, 60))
if !s.IsFinal {
fmt.Printf(" 动作: %s(%v)\n", s.Action, s.Args)
fmt.Printf(" 观察: %s\n", truncate(s.Observation, 60))
}
}
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}
预期输出(不同模型会有差异):
=== 最终答案 ===
Go 1.23 于 2024 年 8 月发布,主要新特性包括:1) 新增 iter 包,提供...
=== 推理轨迹 ===
[轮 1] 思考: 用户想知道 Go 1.23 新特性,我需要先搜索。
动作: search_web(map[query:Go 1.23 release notes])
观察: 搜索 'Go 1.23 release notes' 的结果: [模拟] Go 1.23 于 2024-08...
[轮 2] 思考: 信息已足够,可以给出总结。
看到没?第 1 轮的 Thought 明确引用了「我需要先搜索」,第 2 轮判断「信息已足够」------这就是 ReAct 的可追溯性,每一决策都有据可查。
本篇小结
| 知识点 | 核心内容 |
|---|---|
| ReAct 核心思想 | Reasoning(Thought)与 Acting(Action)交替,Observation 反馈驱动下一步推理 |
| TAO 循环 | Thought → Action → Observation,反复直到 Final Answer |
| 与 Function Calling 的关系 | FC 是执行器(API 能力),ReAct 是指挥策略(设计模式);可混合使用 |
| Go 实现 | ReActEngine + JSON Action 协议 + 容错解析 + 错误变 Observation |
| 停止条件 | 最大迭代、完成信号、死循环熔断,三者缺一不可 |
| 可纠错性来源 | 工具错误/解析失败被当 Observation 回注,模型可在下一轮 Thought 调整 |
下篇预告
ReAct 引擎跑起来了,但工具还是一个一个手动塞进去的。下一篇我们把
map[string]Tool升级为完整的 Tool Registry:支持元数据描述、自动发现、分组权限、描述自动生成,让 AgentForge 能装下几十上百个工具而不混乱。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。