你可能听过 ReAct(Reason + Act),也可能在很多 Agent 框架里“被动使用过它”——模型先推理(Reason),再行动(Act),用工具结果(Observation)来纠错并继续推进。
这篇文章只做一件事:把 ReAct 讲清楚、讲到能落地。我会刻意用 ReAct 的表达方式来写:用 Thought / Action / Observation / Reflection 推进叙事,就像在读一段“可执行的工程推理”。(说明:这里的 Thought 是工程判断点,不展开冗长推演。)
ReAct Step 1:ReAct 到底解决什么问题
1
2
3
4
5
6
7
| Thought: 为什么我需要 ReAct,而不是让模型一次性给“最终答案”?
Action: 列出单次生成在工程里的典型失败方式。
Observation:
- 问题复杂时,模型容易“讲得像对的”,但过程不可控、不可验证
- 一旦涉及外部事实(网页/DB/文件),模型需要 grounding,否则就是幻觉风险
- 真正的工程任务需要执行:查数据、改文件、跑命令、读日志,而不是只写说明
Reflection: ReAct 的价值是把“生成文本”变成“可执行的循环”,并用 Observation 把模型拉回现实。
|
ReAct Step 2:ReAct 的最小闭环长什么样
1
2
3
4
5
6
7
| Thought: 我需要一个足够稳定的步骤格式,让系统能驱动工具调用。
Action: 规定输出协议(协议比 prompt 更重要)。
Observation: 最小闭环通常是:
- Thought(思考):决定下一步(建议只在内部记录/不对用户展示)
- Action(行动):选择工具或 NONE
- Observation(观察):工具结果
Reflection: ReAct 不是“让模型多想想”,而是给系统一个可解析、可编排的执行接口。
|
ReAct Step 3:实践(Ruby 实现最小可用 ReAct 循环)
这个例子刻意“笨一点”:不追求框架化,只把 ReAct 的最小闭环做出来,便于你迁移到任何语言/框架。
关键点很简单:
@scratchpad 记录每一步 Thought/Action/Observation,让模型“看见自己刚刚做过什么”- 每次调用 LLM 时,把
@scratchpad 塞回 prompt 里,形成多轮闭环
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
| require "openai"
class MiniReAct
def initialize(model: "gpt-4.1-mini")
@client = OpenAI::Client.new
@model = model
@scratchpad = "" # ReAct 轨迹(Thought/Action/Observation)
end
def append(text)
@scratchpad << text
end
def step(user_input)
prompt = <<~PROMPT
You are a ReAct agent.
Follow the format:
Thought: <your reasoning> # keep it short; internal notes
Action: <tool name or NONE>
Observation: <result of action>
Current scratchpad:
#{@scratchpad}
User input:
#{user_input}
Continue the next step.
PROMPT
res = @client.chat(
parameters: {
model: @model,
messages: [{ role: "user", content: prompt }]
}
)
output = res["choices"][0]["message"]["content"]
append(output + "\n")
output
end
def run(user_input)
3.times do |i|
puts "\n=== STEP #{i + 1} ==="
out = step(user_input)
puts out
# 如果模型说 Action: NONE → 停止
break if out.include?("Action: NONE")
end
puts "\n=== FINAL SCRATCHPAD ==="
puts @scratchpad
end
end
# 使用示例
# agent = MiniReAct.new
# agent.run("What is 12 * 13?")
# agent.run("明天是周几?")
|
1
2
3
4
| Thought: 这个“最小循环”到底让系统多了什么能力?
Action: 对比“单次回答”与“循环 + Observation”。
Observation: 你现在可以把外部信息(工具结果)写回轨迹,再让模型基于新事实继续规划。
Reflection: 这就是 ReAct 的核心:把不确定性留给下一轮,用 Observation 驱动纠错。
|
ReAct Step 4:实践进阶(加入工具调用:Action → 真执行)
更接近真实系统时,你会加入:
- tools:把行动从“文本”变成“可执行的函数调用”
- memory:把长期信息从“长文本”变成“结构化对象”
但 ReAct 的闭环不变:模型先产出 Action,你去执行,再把 Observation 写回去。
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
| require "openai"
require "json"
module Reactor
class Agent
attr_reader :scratchpad, :memory
def initialize(model: "gpt-4.1-mini", tools: [])
@client = OpenAI::Client.new
@model = model
@tools = tools
@scratchpad = ""
@memory = {}
end
# 写入 scratchpad
def append(text)
@scratchpad << text
end
# 执行一步
def step(user_input)
prompt = build_prompt(user_input)
res = @client.chat(
parameters: {
model: @model,
messages: [{ role: "user", content: prompt }]
}
)
content = res["choices"][0]["message"]["content"]
append(content + "\n")
# 解析 Action
action = content.match(/Action:\s*(.+)/)&.captures&.first&.strip
if action && action != "NONE" && tool = @tools.find { |t| t.name == action }
args_json = content.match(/Arguments:\s*(\{.*\})/)&.captures&.first
args = args_json ? JSON.parse(args_json) : {}
result = tool.call(args)
append("Observation: #{result}\n")
end
content
end
# 循环执行
def run(user_input, max_steps: 10)
max_steps.times do |i|
puts "\n=== STEP #{i + 1} ==="
output = step(user_input)
puts output
break if output.include?("Answer:") || output.include?("Action: NONE")
end
puts "\n=== FINAL SCRATCHPAD ==="
puts @scratchpad
end
private
# 构建 prompt,包含 scratchpad + memory
def build_prompt(user_input)
<<~PROMPT
You are a ReAct agent. Follow this extended format:
Thought: <your reasoning>
Action: <tool name or NONE>
Arguments: <JSON arguments if tool>
Observation: <result of action>
Reflection: <self critique or check>
Fix: <correction if needed>
Plan: <next subgoal or step>
Answer: <final answer when ready>
Current memory:
#{@memory.to_json}
Current scratchpad:
#{@scratchpad}
User input:
#{user_input}
Continue the next step.
PROMPT
end
end
# 工具抽象
class Tool
attr_reader :name
def initialize(name, &block)
@name = name
@block = block
end
def call(args)
@block.call(args)
end
end
end
|
1
2
3
4
| Thought: 为什么需要先写入模型输出,再执行工具?
Action: 把“可审计性”当成一等公民:先把 Action 记录下来再执行。
Observation: 线上排障时你能回答“模型当时想做什么、传了什么参数、工具返回了什么”。
Reflection: ReAct 的工程价值不止是准确率,更是可观测、可复现、可治理。
|
这段代码里,ReAct 的执行顺序很清晰:
- LLM 输出一段计划(含 Action / Arguments)→ 你把它写入轨迹(scratchpad/trace)
- 你执行工具 → 把 Observation 也写回轨迹
- 下一轮规划时,模型看到的是“完整的已发生轨迹”,因此能稳定地连续推理
ReAct Step 5:踩坑点 / 常见误区(ReAct 很容易“跑偏”)
- 把 Thought 当作“给用户看的解释”:不要。Thought 更像内部控制信号;对用户展示建议只给结论/依据/引用,避免暴露内部草稿与敏感信息。
- 用正则解析 Action/Arguments 太脆弱:LLM 输出很容易偏格式。更稳的做法是让模型产出严格 JSON(或使用框架的结构化工具调用),并做 schema 校验。
- Observation 不做截断/摘要:工具返回可能很大(网页、日志、SQL 结果集)。如果 Observation 不治理,上下文会被“结果噪声”淹没,模型会变笨。
- 工具调用没做安全边界:需要 allowlist、超时、重试、权限隔离、以及 prompt injection 防护(尤其是 Web 工具)。
- 循环停不下来:必须有
max_steps、明确的停止条件(Answer / Action: NONE)、以及失败兜底策略(降级为解释 + 下一步建议)。
ReAct Step 6:生产环境建议(让 ReAct “可控地跑”)
- 优先用结构化工具调用:让
Action/Arguments 变成 schema(JSON Schema / typed tool calling),并做严格校验与拒绝策略。 - 把轨迹做成结构化事件:每一步
{thought?, action, args, observation, timestamp},方便检索、评估与回放。 - 对 Observation 做预算:限制 token/字节数,必要时摘要、分页、或只保留 top-k 片段。
- 隔离“用户输出”和“内部轨迹”:线上日志脱敏,最小化保留;前端只展示可解释信息(引用、证据、关键步骤),不要直接 dump scratchpad。
- 把失败当成常态设计:工具超时/限流/失败要有 retry/backoff 与降级路径(例如只给出可执行的下一步指引)。
ReAct Step 7:总结 / Takeaways
- ReAct 的本质是闭环:
Thought → Action → Observation 让模型在外部事实与执行结果的约束下前进。 - 工程上,协议比 prompt 更重要:Action/Arguments 必须可解析、可校验、可拒绝。
- 落地时别只追求“能跑”:要考虑可观测性(轨迹/指标)、可控性(预算/停止条件)、以及安全边界(工具权限/注入防护/脱敏)。