第5章 ReAct-让Agent学会思考
来源:https://ai-agent-guide.xiaofuge.cn/chapters/ch04-react-pattern.html 所属:第二篇-Agent的大脑
让 Agent 学会"边想边做"的经典范式
🔗 从上一章延续的问题
第3章的天气 Agent 能查天气、能规划行程,但它不会思考为什么要查——用户问"明天适合跑步吗",它直接调 get_weather,却没有先推理"适合跑步需要看天气+空气质量"。这导致了两个问题:① 可能漏掉该调的工具,② 可能调了不必要的工具浪费 token。ReAct 就是为了解决这两个问题而诞生的——让 Agent 在行动前先思考,观察后再反思。 🎯 本章先掌握 3 件事
这一章内容很多,但如果你是第一次接触 ReAct,只要先搞懂三件事就够了:为什么要先想再做、Thought / Action / Observation 如何形成闭环、它和上一章工具调用有什么关系。
你可以先读 4.1 到 4.4,把 ReAct 的核心循环吃透;4.5 之后的 ReWOO、LLM Compiler、Reflexion 和生产级主循环更适合作为进阶阅读,不需要一次全吞下去。
5.1 从一个问题开始
假设用户问 Agent:"2024年诺贝尔物理学奖得主是谁?他们的研究领域是什么?"
一个没有规划能力的 Agent 可能直接让 LLM 回答——但 LLM 的知识截止到训练日期,可能不知道 2024 年的诺奖。它需要搜索。但什么时候搜?搜什么?搜完之后做什么?
这就是 ReAct 要解决的问题。
ReAct = Reasoning + Acting
让 LLM 交替进行"推理(Thought)"和"行动(Action)",通过"观察(Observation)"获取反馈,循环直到得出最终答案。
5.2 ReAct 的诞生背景
在 ReAct 之前,Agent 面临两难:
只推理(Reasoning-only)
LLM 用 CoT 一步步推理
✓ 推理能力强
✗ 无法获取外部信息
✗ 可能产生幻觉
→ 想得到,做不到
只行动(Acting-only)
LLM 直接调用工具
✓ 能获取外部信息
✗ 没有推理规划
✗ 不知道下一步做什么
→ 做得到,想不到
2022 年,Yao 等人在论文 "ReAct: Synergizing Reasoning and Acting in Language Models" 中提出了 ReAct 范式,将两者结合: 💡 ReAct 的核心洞察
**推理指导行动,行动反馈推理。**LLM 先"想"该做什么(Thought),再"做"(Action),然后"看"结果(Observation),基于结果再"想"下一步——形成闭环。
5.3 ReAct 循环:Thought → Action → Observation
ReAct 的核心是三个角色的循环:
逐步演示:一个完整的 ReAct 执行
1
Thought #1
我需要先搜索2024年诺贝尔物理学奖得主。
← 1 / 7 → 重播
5.4 ReAct 的 Prompt 模板
ReAct 的实现核心是 System Prompt。来看一个标准的 ReAct prompt:
ReAct System Prompt
尽可能回答以下问题。你可以使用以下工具:
可用工具:
- search(query): 搜索互联网获取信息
- lookup(keyword): 在上一次搜索结果中查找关键词
- finish(answer): 提交最终答案
格式要求:
每一步必须使用以下格式:
Thought: 你对下一步该做什么的推理
Action: 工具名称(参数)
Observation: 工具返回的结果
... (Thought/Action/Observation 可以重复多次)
Thought: 我现在知道答案了
Action: finish(最终答案)
开始!
问题: {user_question}
关键设计点
1. 强制结构化输出 用 "Thought:" "Action:" "Observation:" 标记,让 LLM 的输出可被程序解析。
2. 工具结果由框架注入 "Observation:" 不是 LLM 生成的,是框架执行工具后拼接上去的。
3. 循环终止条件 当 LLM 输出 finish(answer) 时结束循环。也可设置最大步数防止死循环。
4. 推理可见性 Thought 部分是 LLM 的"内心独白",让推理过程可审计、可调试。
5.5 ReWOO 模式(Reasoning Without Observation)
ReAct 的核心循环是"边想边做"——每一步 Thought 之后立刻 Action,获得 Observation 后再想下一步。这很直观,但有一个严重的问题:每一步的 Observation 都会被塞入上下文,随着步数增加,token 消耗线性膨胀。
ReWOO 提出了一个反直觉的思路:先想好所有步骤,再统一执行。
ReWOO = Reasoning Without Observation
2023 年由 Xu 等人提出。核心思想:让 LLM 在没有 Observation 的情况下,先规划出完整的推理链(Planner),然后由 Worker 统一执行所有工具调用,最后由 Solver 综合所有结果给出答案。
Planner → Worker → Solver 三阶段
ReWOO 把 ReAct 的循环拆成了三个独立阶段: 🔍 三阶段详解
Planner(规划器)
LLM 一次性生成完整的推理链,每一步用 #E 标记期望的工具调用。因为还没有 Observation,Planner 必须靠自身推理决定步骤——这就是 "Without Observation" 的含义。
Worker(执行器)
逐个执行 Planner 规划的工具调用,将结果填入 #E 占位符。Worker 是程序化的,不需要 LLM 参与——纯工具执行。
Solver(求解器) LLM 接收已填入结果的完整推理链,综合所有信息生成最终答案。此时 LLM 看到的是"规划+真实结果"的完整视图。
ReAct vs ReWOO 流程对比
ReAct:边想边做
- 每一步 Thought 都需要携带完整历史上下文
- Observation 逐步累积,token 随步数线性增长
- LLM 调用次数 = 步数 × 2(Thought + 解析)
- 优势:灵活调整,每步可基于 Observation 改变策略
ReWOO:先想后做
- Planner 只调用 LLM 1次,生成完整推理链
- Worker 执行工具,不消耗 LLM token
- Solver 只调用 LLM 1次,综合答案
- 总 LLM 调用 = 2次,远少于 ReAct 的 N 次
ReWOO 代码示例
from openai import OpenAI
client = OpenAI()
TOOLS = {"search": search_web, "weather": get_weather, "calc": calculate}
# ===== Phase 1: Planner =====
PLANNER_PROMPT = """你是一个规划器。根据用户问题,生成一系列步骤来解决问题。
每一步用 #E 标记需要执行的工具调用。
格式:
Plan: 第一步的推理
#E1 = tool_name("参数")
Plan: 第二步的推理(可引用 #E1 的结果)
#E2 = tool_name("参数")
...
问题: {question}"""
def plan_steps(question):
"""Phase 1: LLM 一次性规划所有步骤"""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": PLANNER_PROMPT},
{"role": "user", "content": question}]
)
return resp.choices[0].message.content
# ===== Phase 2: Worker =====
def execute_steps(plan_text):
"""Phase 2: 解析并执行所有 #E 工具调用"""
import re
results = {}
# 查找所有 #E 标记
steps = re.findall(r'#E(\d+)\s*=\s*(\w+)\((".*?"|\'.*?\'|.*?)\)', plan_text)
for step_id, tool_name, tool_input in steps:
tool_input = tool_input.strip('"\'')
if tool_name in TOOLS:
results[f"#E{step_id}"] = TOOLStool_name
else:
results[f"#E{step_id}"] = f"Error: {tool_name} not found"
return results
# ===== Phase 3: Solver =====
SOLVER_PROMPT = """根据以下规划和执行结果,给出最终答案。
规划:
{plan}
执行结果:
{results}
请综合以上信息,给出完整答案。"""
def solve_answer(plan_text, results):
"""Phase 3: LLM 综合所有结果生成答案"""
# 将结果填入规划的 #E 占位符
filled_plan = plan_text
for key, value in results.items():
filled_plan = filled_plan.replace(key, f"[结果: {value}]")
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": SOLVER_PROMPT},
{"role": "user", "content":
f"规划:\n{plan_text}\n\n执行结果:\n{filled_plan}"}]
)
return resp.choices[0].message.content
# ===== 完整流程 =====
def rewoo_agent(question):
"""ReWOO: Planner → Worker → Solver"""
# 1. 规划
plan = plan_steps(question)
# 2. 执行
results = execute_steps(plan)
# 3. 求解
answer = solve_answer(plan, results)
return answer
# 使用
answer = rewoo_agent("北京今天天气如何?适合户外活动吗?")
import OpenAI from 'openai';
const client = new OpenAI();
const TOOLS: Record string> = {
search: search_web,
weather: get_weather,
calc: calculate
};
// ===== Phase 1: Planner =====
const PLANNER_PROMPT = `你是一个规划器。根据用户问题,生成一系列步骤来解决问题。
每一步用 #E 标记需要执行的工具调用。
格式:
Plan: 第一步的推理
#E1 = tool_name("参数")
Plan: 第二步的推理(可引用 #E1 的结果)
#E2 = tool_name("参数")
...
问题: {question}`;
async function plan_steps(question: string): Promise {
// Phase 1: LLM 一次性规划所有步骤
const resp = await client.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: PLANNER_PROMPT },
{ role: 'user', content: question }
]
});
return resp.choices[0].message.content!;
}
// ===== Phase 2: Worker =====
function execute_steps(planText: string): Record {
// Phase 2: 解析并执行所有 #E 工具调用
const results: Record = {};
const regex = /#E(\d+)\s*=\s*(\w+)\(["']?(.*?)["']?\)/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(planText)) !== null) {
const stepId = match[1];
const toolName = match[2];
const toolInput = match[3];
if (toolName in TOOLS) {
results[`#E${stepId}`] = TOOLStoolName;
} else {
results[`#E${stepId}`] = `Error: ${toolName} not found`;
}
}
return results;
}
// ===== Phase 3: Solver =====
const SOLVER_PROMPT = `根据以下规划和执行结果,给出最终答案。
规划:
{plan}
执行结果:
{results}
请综合以上信息,给出完整答案。`;
async function solve_answer(
planText: string,
results: Record
): Promise {
// Phase 3: LLM 综合所有结果生成答案
let filledPlan = planText;
for (const [key, value] of Object.entries(results)) {
filledPlan = filledPlan.replaceAll(key, `[结果: ${value}]`);
}
const resp = await client.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: SOLVER_PROMPT },
{ role: 'user', content: `规划:\n${planText}\n\n执行结果:\n${filledPlan}` }
]
});
return resp.choices[0].message.content!;
}
// ===== 完整流程 =====
async function rewoo_agent(question: string): Promise {
// ReWOO: Planner → Worker → Solver
const plan = await plan_steps(question); // 1. 规划
const results = execute_steps(plan); // 2. 执行
const answer = await solve_answer(plan, results); // 3. 求解
return answer;
}
// 使用
const answer = await rewoo_agent('北京今天天气如何?适合户外活动吗?');
package main
import (
"context"
"fmt"
"regexp"
"strings"
openai "github.com/sashabaranov/go-openai"
)
var client = openai.NewClient()
func searchWeb(input string) string { return "search: " + input }
func getWeather(input string) string { return "weather: " + input }
func calculate(input string) string { return "calc: " + input }
var tools = map[string]func(string) string{
"search": searchWeb,
"weather": getWeather,
"calc": calculate,
}
// ===== Phase 1: Planner =====
const plannerPrompt = `你是一个规划器。根据用户问题,生成一系列步骤来解决问题。
每一步用 #E 标记需要执行的工具调用。
格式:
Plan: 第一步的推理
#E1 = tool_name("参数")
Plan: 第二步的推理(可引用 #E1 的结果)
#E2 = tool_name("参数")
...
问题: %s`
func planSteps(question string) (string, error) {
// Phase 1: LLM 一次性规划所有步骤
resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
Model: openai.GPT4o,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: fmt.Sprintf(plannerPrompt, "{question}")},
{Role: openai.ChatMessageRoleUser, Content: question},
},
})
if err != nil {
return "", err
}
return resp.Choices[0].Message.Content, nil
}
// ===== Phase 2: Worker =====
func executeSteps(planText string) map[string]string {
// Phase 2: 解析并执行所有 #E 工具调用
results := make(map[string]string)
re := regexp.MustCompile(`#E(\d+)\s*=\s*(\w+)\(["']?(.*?)["']?\)`)
matches := re.FindAllStringSubmatch(planText, -1)
for _, m := range matches {
stepID, toolName, toolInput := m[1], m[2], m[3]
if fn, ok := tools[toolName]; ok {
results["#E"+stepID] = fn(toolInput)
} else {
results["#E"+stepID] = fmt.Sprintf("Error: %s not found", toolName)
}
}
return results
}
// ===== Phase 3: Solver =====
const solverPrompt = `根据以下规划和执行结果,给出最终答案。
规划:
%s
执行结果:
%s
请综合以上信息,给出完整答案。`
func solveAnswer(planText string, results map[string]string) (string, error) {
// Phase 3: LLM 综合所有结果生成答案
filledPlan := planText
for key, value := range results {
filledPlan = strings.ReplaceAll(filledPlan, key, fmt.Sprintf("[结果: %s]", value))
}
resultStr := ""
for k, v := range results {
resultStr += fmt.Sprintf("%s: %s\n", k, v)
}
resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
Model: openai.GPT4o,
Messages: []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleSystem, Content: solverPrompt},
{Role: openai.ChatMessageRoleUser, Content: fmt.Sprintf("规划:\n%s\n\n执行结果:\n%s", planText, filledPlan)},
},
})
if err != nil {
return "", err
}
return resp.Choices[0].Message.Content, nil
}
// ===== 完整流程 =====
func rewooAgent(question string) (string, error) {
// ReWOO: Planner → Worker → Solver
plan, err := planSteps(question) // 1. 规划
if err != nil {
return "", err
}
results := executeSteps(plan) // 2. 执行
answer, err := solveAnswer(plan, results) // 3. 求解
if err != nil {
return "", err
}
return answer, nil
}
// 使用
func main() {
answer, _ := rewooAgent("北京今天天气如何?适合户外活动吗?")
fmt.Println(answer)
}
import com.openai.client.OpenAIClient;
import com.openai.models.*;
import java.util.*;
import java.util.regex.*;
public class AgentCode {
static OpenAIClient client = new OpenAIClient();
static Map> tools = new HashMap<>();
static {
tools.put("search", AgentCode::searchWeb);
tools.put("weather", AgentCode::getWeather);
tools.put("calc", AgentCode::calculate);
}
static String searchWeb(String input) { return "search: " + input; }
static String getWeather(String input) { return "weather: " + input; }
static String calculate(String input) { return "calc: " + input; }
// ===== Phase 1: Planner =====
static final String PLANNER_PROMPT = """
你是一个规划器。根据用户问题,生成一系列步骤来解决问题。
每一步用 #E 标记需要执行的工具调用。
格式:
Plan: 第一步的推理
#E1 = tool_name("参数")
Plan: 第二步的推理(可引用 #E1 的结果)
#E2 = tool_name("参数")
...
问题: %s""";
static String planSteps(String question) {
// Phase 1: LLM 一次性规划所有步骤
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model("gpt-4o")
.addMessage(ChatCompletionMessage.builder()
.role(ChatCompletionMessage.Role.SYSTEM)
.content(String.format(PLANNER_PROMPT, "{question}"))
.build())
.addMessage(ChatCompletionMessage.builder()
.role(ChatCompletionMessage.Role.USER)
.content(question)
.build())
.build();
ChatCompletion resp = client.chat().completions().create(params);
return resp.choices().get(0).message().content().orElse("");
}
// ===== Phase 2: Worker =====
static Map executeSteps(String planText) {
// Phase 2: 解析并执行所有 #E 工具调用
Map results = new LinkedHashMap<>();
Pattern pattern = Pattern.compile("#E(\\\\d+)\\\\s*=\\\\s*(\\\\w+)\\\\([\"']?(.*?)[\"']?\\\\)");
Matcher matcher = pattern.matcher(planText);
while (matcher.find()) {
String stepId = matcher.group(1);
String toolName = matcher.group(2);
String toolInput = matcher.group(3);
if (tools.containsKey(toolName)) {
results.put("#E" + stepId, tools.get(toolName).apply(toolInput));
} else {
results.put("#E" + stepId, "Error: " + toolName + " not found");
}
}
return results;
}
// ===== Phase 3: Solver =====
static final String SOLVER_PROMPT = """
根据以下规划和执行结果,给出最终答案。
规划:
%s
执行结果:
%s
请综合以上信息,给出完整答案。""";
static String solveAnswer(String planText, Map results) {
// Phase 3: LLM 综合所有结果生成答案
String filledPlan = planText;
for (Map.Entry entry : results.entrySet()) {
filledPlan = filledPlan.replace(entry.getKey(),
"[结果: " + entry.getValue() + "]");
}
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model("gpt-4o")
.addMessage(ChatCompletionMessage.builder()
.role(ChatCompletionMessage.Role.SYSTEM)
.content(SOLVER_PROMPT)
.build())
.addMessage(ChatCompletionMessage.builder()
.role(ChatCompletionMessage.Role.USER)
.content("规划:\\n" + planText + "\\n\\n执行结果:\\n" + filledPlan)
.build())
.build();
ChatCompletion resp = client.chat().completions().create(params);
return resp.choices().get(0).message().content().orElse("");
}
// ===== 完整流程 =====
static String rewooAgent(String question) {
// ReWOO: Planner → Worker → Solver
String plan = planSteps(question); // 1. 规划
Map results = executeSteps(plan); // 2. 执行
String answer = solveAnswer(plan, results); // 3. 求解
return answer;
}
// 使用
public static void main(String[] args) {
String answer = rewooAgent("北京今天天气如何?适合户外活动吗?");
System.out.println(answer);
}
}
⚠️ ReWOO 的局限
ReWOO 的 Planner 必须在没有 Observation 的条件下规划所有步骤,这意味着它无法根据中间结果调整策略。如果第一步的搜索结果出乎意料,Planner 无法"转向"——它只能沿着预设路径走到底。这是 ReWOO 相比 ReAct 的核心劣势。
5.6 LLM Compiler 模式
ReAct 是串行的:Thought₁ → Action₁ → Observation₁ → Thought₂ → Action₂ → …。如果 Agent 需要调用 5 个独立的工具,即使它们之间没有依赖关系,也必须一个一个等。
LLM Compiler 提出了并行执行的思路:让 LLM 一次性输出多个独立的工具调用,然后并行执行它们。
LLM Compiler
2023 年由 Kim 等人提出。核心思想:将 Agent 的执行计划编译成依赖图(DAG),识别可并行的步骤,同时执行没有依赖关系的工具调用,显著减少总执行时间。
依赖图分析:识别可并行的步骤
关键洞察:很多工具调用之间没有数据依赖。比如:
串行(ReAct)
Thought → search("北京天气") → Obs₁
Thought → search("上海天气") → Obs₂
Thought → search("广州天气") → Obs₃
Thought → 比较三城市 → Answer
总计:4轮LLM + 3次搜索 = ~12s
并行(LLM Compiler)
Planner → 并行:
search("北京天气") ─┐
search("上海天气") ─┤→ Solver → Answer
search("广州天气") ─┘
总计:1轮LLM + 1次并行搜索 = ~4s
并行调用多个工具:代码示例
import asyncio
from openai import OpenAI
client = OpenAI()
# ===== Step 1: 编译器生成并行计划 =====
COMPILER_PROMPT = """根据用户问题,生成工具调用计划。
标记依赖关系:如果一个调用需要另一个的结果,标注 depends_on。
输出JSON格式:
[
{"id": 1, "tool": "search", "args": {"query": "北京天气"},
"depends_on": []},
{"id": 2, "tool": "search", "args": {"query": "上海天气"},
"depends_on": []},
{"id": 3, "tool": "compare", "args": {"data_from": [1, 2]},
"depends_on": [1, 2]}
]
问题: {question}"""
async def compile_plan(question):
"""LLM 编译:生成带依赖关系的工具调用计划"""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "system", "content": COMPILER_PROMPT},
{"role": "user", "content": question}],
response_format={"type": "json_object"}
)
import json
return json.loads(resp.choices[0].message.content)
# ===== Step 2: 并行执行独立工具 =====
async def execute_parallel(plan):
"""按依赖图并行执行:无依赖的步骤同时运行"""
results = {}
completed = set()
while len(completed) = 0.8: # 质量达标
return result
# 3. 反思推理过程
reflection = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": REFLECT_PROMPT},
{"role": "user", "content":
f"之前的尝试:\n{result['trajectory']}\n\n"
f"评估反馈:\n{evaluation['feedback']}"}
]
).choices[0].message.content
reflections.append(reflection)
print(f"Trial {trial+1}: score={evaluation['score']:.2f}")
print(f"Reflection: {reflection[:100]}...")
return result # 达到最大尝试次数,返回最后结果
def evaluate_result(question, result):
"""LLM 评估答案质量"""
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content":
"评估以下答案的质量。输出JSON: {\"score\": 0-1, \"feedback\": \"...\"}"},
{"role": "user", "content":
f"问题: {question}\n答案: {result['answer']}"}
],
response_format={"type": "json_object"}
)
import json
return json.loads(resp.choices[0].message.content)
# 使用
answer = reflexion_agent(
"解释量子纠缠的原理,并说明它与经典关联的区别"
)
import OpenAI from 'openai';
const client = OpenAI();
const REFLECT_PROMPT = """你刚刚完成了一个任务,但结果不够好。;
// 回顾你的推理过程和执行结果,找出问题所在。
// 之前的尝试:
// {trajectory}
// 评估反馈:
// {evaluation}
// 请反思:
// 1. 哪一步推理出了问题?
// 2. 应该怎么做才更好?
// 3. 下次尝试时需要注意什么?
// 输出格式:
// Reflection: 你的反思内容"""
function react_agent_with_context(question, context="") {
/** docstring */
const messages = [{"role": "system", "content": REACT_PROMPT + context}];
// messages.append({"role": "user", "content": question})
const trajectory = [];
for (const step of range(10)) {
const resp = client.chat.completions.create(model="gpt-4o", messages=messages);
const thought = resp.choices[0].message.content;
// trajectory.append(thought)
if ("finish" in thought.lower()) {
return {"answer": thought, "trajectory": "\n".join(trajectory)};
// messages.append({"role": "assistant", "content": thought})
return {"answer": thought, "trajectory": "\n".join(trajectory)};
function reflexion_agent(question, max_trials=3) {
/** docstring */
const reflections = [] # 累积的反思经验;
for (const trial of range(max_trials)) {
// 1. 执行任务(使用 ReAct + 历史反思作为额外上下文)
const context = "";
if (reflections) {
const context = `\n之前的反思经验:\n" + "\n`;
const result = react_agent_with_context(question, context);
// 2. 评估结果质量
const evaluation = evaluate_result(question, result);
if (evaluation["score"] >= 0.8) {
return result;
// 3. 反思推理过程
const reflection = client.chat.completions.create(;
const model = "gpt-4o",;
const messages = [;
// {"role": "system", "content": REFLECT_PROMPT},
// {"role": "user", "content":
// f"之前的尝试:\n{result['trajectory']}\n\n"
// f"评估反馈:\n{evaluation['feedback']}"}
// ]
// ).choices[0].message.content
// reflections.append(reflection)
console.log(`Trial {trial+1}: score={evaluation['score']:.2f}`);
console.log(`Reflection: {reflection[:100]}...`);
return result # 达到最大尝试次数,返回最后结果;
function evaluate_result(question, result) {
/** docstring */
const resp = client.chat.completions.create(;
const model = "gpt-4o",;
const messages = [;
// {"role": "system", "content":
// "评估以下答案的质量。输出JSON: {\"score\": 0-1, \"feedback\": \"...\"}"},
// {"role": "user", "content":
// f"问题: {question}\n答案: {result['answer']}"}
// ],
const response_format = {"type": "json_object"};
// )
// Node.js built-in or npm package for: json
return json.loads(resp.choices[0].message.content);
// 使用
const answer = reflexion_agent(;
// "解释量子纠缠的原理,并说明它与经典关联的区别"
// )
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// from openai import OpenAI
// Python: client = OpenAI()
// Python: REFLECT_PROMPT = """你刚刚完成了一个任务,但结果不够好。
// Python: 回顾你的推理过程和执行结果,找出问题所在。
// Python: 之前的尝试:
// Python: {trajectory}
// Python: 评估反馈:
// Python: {evaluation}
// Python: 请反思:
// Python: 1. 哪一步推理出了问题?
// Python: 2. 应该怎么做才更好?
// Python: 3. 下次尝试时需要注意什么?
// Python: 输出格式:
// Python: Reflection: 你的反思内容"""
func react_agent_with_context() {
// Python: messages = [{"role": "system", "content": REACT_PROMPT + context}]
// Python: messages.append({"role": "user", "content": question})
// Python: trajectory = []
for _, step := range range(10) {
// Python: resp = client.chat.completions.create(model="gpt-4o", messages=messages)
// Python: thought = resp.choices[0].message.content
// Python: trajectory.append(thought)
if "finish" in thought.lower() {
return {"answer": thought, "trajectory": "\n".join(trajectory)}
// Python: messages.append({"role": "assistant", "content": thought})
return {"answer": thought, "trajectory": "\n".join(trajectory)}
func reflexion_agent() {
// Python: reflections = [] # 累积的反思经验
for _, trial := range range(max_trials) {
// 1. 执行任务(使用 ReAct + 历史反思作为额外上下文)
// Python: context = ""
if reflections {
// Python: context = f"\n之前的反思经验:\n" + "\n".join(reflections)
// Python: result = react_agent_with_context(question, context)
// 2. 评估结果质量
// Python: evaluation = evaluate_result(question, result)
if evaluation["score"] >= 0.8 {
return result
// 3. 反思推理过程
// Python: reflection = client.chat.completions.create(
// Python: model="gpt-4o",
// Python: messages=[
// Python: {"role": "system", "content": REFLECT_PROMPT},
// Python: {"role": "user", "content":
// Python: f"之前的尝试:\n{result['trajectory']}\n\n"
// Python: f"评估反馈:\n{evaluation['feedback']}"}
// Python: ]
// Python: ).choices[0].message.content
// Python: reflections.append(reflection)
fmt.Println(f"Trial {trial+1}: score={evaluation['score']:.2f}")
fmt.Println(f"Reflection: {reflection[:100]}...")
return result # 达到最大尝试次数,返回最后结果
func evaluate_result() {
// Python: resp = client.chat.completions.create(
// Python: model="gpt-4o",
// Python: messages=[
// Python: {"role": "system", "content":
// Python: "评估以下答案的质量。输出JSON: {\"score\": 0-1, \"feedback\": \"...\"}"},
// Python: {"role": "user", "content":
// Python: f"问题: {question}\n答案: {result['answer']}"}
// Python: ],
// Python: response_format={"type": "json_object"}
// Python: )
// import json
return json.loads(resp.choices[0].message.content)
// 使用
// Python: answer = reflexion_agent(
// Python: "解释量子纠缠的原理,并说明它与经典关联的区别"
// Python: )
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// from openai import OpenAI
// Python: client = OpenAI()
// Python: REFLECT_PROMPT = """你刚刚完成了一个任务,但结果不够好。
// Python: 回顾你的推理过程和执行结果,找出问题所在。
// Python: 之前的尝试:
// Python: {trajectory}
// Python: 评估反馈:
// Python: {evaluation}
// Python: 请反思:
// Python: 1. 哪一步推理出了问题?
// Python: 2. 应该怎么做才更好?
// Python: 3. 下次尝试时需要注意什么?
// Python: 输出格式:
// Python: Reflection: 你的反思内容"""
public static void react_agent_with_context() {
// Python: messages = [{"role": "system", "content": REACT_PROMPT + context}]
// Python: messages.append({"role": "user", "content": question})
// Python: trajectory = []
for (var step : range(10)) {
// Python: resp = client.chat.completions.create(model="gpt-4o", messages=messages)
// Python: thought = resp.choices[0].message.content
// Python: trajectory.append(thought)
if ("finish" in thought.lower()) {
return {"answer": thought, "trajectory": "\n".join(trajectory)};
// Python: messages.append({"role": "assistant", "content": thought})
return {"answer": thought, "trajectory": "\n".join(trajectory)};
public static void reflexion_agent() {
// Python: reflections = [] # 累积的反思经验
for (var trial : range(max_trials)) {
// 1. 执行任务(使用 ReAct + 历史反思作为额外上下文)
// Python: context = ""
if (reflections) {
// Python: context = f"\n之前的反思经验:\n" + "\n".join(reflections)
// Python: result = react_agent_with_context(question, context)
// 2. 评估结果质量
// Python: evaluation = evaluate_result(question, result)
if (evaluation["score"] >= 0.8) {
return result;
// 3. 反思推理过程
// Python: reflection = client.chat.completions.create(
// Python: model="gpt-4o",
// Python: messages=[
// Python: {"role": "system", "content": REFLECT_PROMPT},
// Python: {"role": "user", "content":
// Python: f"之前的尝试:\n{result['trajectory']}\n\n"
// Python: f"评估反馈:\n{evaluation['feedback']}"}
// Python: ]
// Python: ).choices[0].message.content
// Python: reflections.append(reflection)
System.out.println(String.format("$1"));
System.out.println(String.format("$1"));
return result # 达到最大尝试次数,返回最后结果;
public static void evaluate_result() {
// Python: resp = client.chat.completions.create(
// Python: model="gpt-4o",
// Python: messages=[
// Python: {"role": "system", "content":
// Python: "评估以下��案的质量。输出JSON: {\"score\": 0-1, \"feedback\": \"...\"}"},
// Python: {"role": "user", "content":
// Python: f"问题: {question}\n答案: {result['answer']}"}
// Python: ],
// Python: response_format={"type": "json_object"}
// Python: )
// import json
return json.loads(resp.choices[0].message.content);
// 使用
// Python: answer = reflexion_agent(
// Python: "解释量子纠缠的原理,并说明它与经典关联的区别"
// Python: )
}
💡 Reflexion 的核心价值
Reflexion 不只是"多做几遍"。它的关键是反思内容会累积——每轮尝试的反思都被保存下来,作为下一轮的额外上下文。这意味着 Agent 在"从失败中学习",而不是盲目重试。这就像人类做题:第一次做错,反思原因,第二次带着反思去做,大概率比第一次好。
5.8 Structured Output(结构化输出)
ReAct 用 "Thought:" "Action:" 标记强制 LLM 输出特定格式,但文本标记解析极不稳定——LLM 可能输出 "Thought: ..." 或 "思考: ..." 或干脆不写标记。这是 Agent 开发中最头疼的问题之一。
结构化输出(Structured Output)就是为了彻底解决 LLM 输出不可靠的问题。
Structured Output
通过 JSON Schema 约束,让 LLM 的输出严格符合预定义的结构——字段名、类型、必填项、枚举值都被锁定。LLM 不再"自由发挥",而是像填表一样输出。
三种方式对比
📊 JSON Mode vs Function Calling vs Structured Output | 方式 | 约束强度 | 字段类型保证 | 必填项保证 | 适用场景 | | --- | --- | --- | --- | --- | | JSON Mode | 弱(只保证是合法JSON) | ✗ 不保证 | ✗ 不保证 | 简单JSON输出 | | Function Calling | 中(参数有Schema) | △ 部分保证 | △ 部分保证 | 工具调用 | | Structured Output | 强(完整Schema约束) | ✓ 严格保证 | ✓ 严格保证 | Agent结构化推理 | ### OpenAI 结构化输出 + Pydantic 验证
from pydantic import BaseModel, Field
from openai import OpenAI
from typing import List, Optional
client = OpenAI()
# ===== 1. 用 Pydantic 定义输出 Schema =====
class AgentStep(BaseModel):
"""Agent 单步推理的结构化输出"""
thought: str = Field(
description="对当前状态的推理分析"
)
action: str = Field(
description="要调用的工具名称",
enum=["search", "lookup", "calculate", "finish"]
)
action_input: str = Field(
description="工具的输入参数"
)
confidence: float = Field(
description="对当前推理的置信度 0-1",
ge=0, le=1
)
class AgentResponse(BaseModel):
"""Agent 完整响应的结构化输出"""
steps: List[AgentStep] = Field(
description="推理步骤列表"
)
final_answer: Optional[str] = Field(
description="最终答案(仅当 action=finish 时提供)",
default=None
)
needs_more_info: bool = Field(
description="是否需要更多信息才能回答"
)
# ===== 2. 用 OpenAI Structured Output 调用 =====
def structured_agent(question):
"""使用 Structured Output 的 Agent"""
response = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content":
"你是一个研究助手。分析问题并给出推理步骤。"},
{"role": "user", "content": question}
],
response_format=AgentResponse # Pydantic 模型直接作为 Schema
)
# 3. 返回的是已验证的 Pydantic 对象
result = response.choices[0].message.parsed
# 类型安全!result.thought 是 str,result.confidence 是 float
for step in result.steps:
print(f"Thought: {step.thought}")
print(f"Action: {step.action}({step.action_input})")
print(f"Confidence: {step.confidence:.2f}")
if result.final_answer:
print(f"Answer: {result.final_answer}")
return result
# ===== 3. Pydantic 自动验证 =====
# 如果 LLM 输出的 confidence 超出 0-1 范围,Pydantic 会自动拒绝
# 如果 action 不是枚举值之一,Pydantic 会自动拒绝
# 如果 steps 是空列表,可以根据业务规则添加额外验证
def validate_agent_response(response: AgentResponse):
"""业务层面的额外验证"""
if not response.steps:
raise ValueError("Agent 必须至少输出一个推理步骤")
if response.needs_more_info and response.final_answer:
raise ValueError("标注需要更多信息时不应有最终答案")
if response.steps[-1].action == "finish" and not response.final_answer:
raise ValueError("finish 动作必须提供最终答案")
return True
# 使用
result = structured_agent("量子计算和经典计算的本质区别是什么?")
validate_agent_response(result)
import {BaseModel, Field} from 'pydantic';
import OpenAI from 'openai';
// TypeScript has built-in types, no import needed for List, Optional
const client = OpenAI();
// ===== 1. 用 Pydantic 定义输出 Schema =====
class AgentStep {
/** docstring */
// thought: str = Field(
// description = "对当前状态的推理分析"
// )
// action: str = Field(
// description = "要调用的工具名称",
// enum = ["search", "lookup", "calculate", "finish"]
// )
// action_input: str = Field(
// description = "工具的输入参数"
// )
// confidence: float = Field(
// description = "对当前推理的置信度 0-1",
// ge = 0, le=1
// )
class AgentResponse {
/** docstring */
// steps: List[AgentStep] = Field(
// description = "推理步骤列表"
// )
// final_answer: Optional[str] = Field(
// description = "最终答案(仅当 action=finish 时提供)",
// default = None
// )
// needs_more_info: bool = Field(
// description = "是否需要更多信息才能回答"
// )
// ===== 2. 用 OpenAI Structured Output 调用 =====
function structured_agent(question) {
/** docstring */
// response = client.beta.chat.completions.parse(
// model = "gpt-4o-2024-08-06",
// messages = [
// {"role": "system", "content":
// "你是一个研究助手。分析问题并给出推理步骤。"},
// {"role": "user", "content": question}
// ],
// response_format = AgentResponse # Pydantic 模型直接作为 Schema
// )
// 3. 返回的是已验证的 Pydantic 对象
// result = response.choices[0].message.parsed
// 类型安全!result.thought 是 str,result.confidence 是 float
for (const step of result.steps) {
console.log(`Thought: {step.thought}`);
console.log(`Action: {step.action}({step.action_input})`);
console.log(`Confidence: {step.confidence:.2f}`);
if (result.final_answer) {
console.log(`Answer: {result.final_answer}`);
return result;
// ===== 3. Pydantic 自动验证 =====
// 如果 LLM 输出的 confidence 超出 0-1 范围,Pydantic 会自动拒绝
// 如果 action 不是枚举值之一,Pydantic 会自动拒绝
// 如果 steps 是空列表,可以根据业务规则添加额外验证
function validate_agent_response(response: AgentResponse) {
/** docstring */
if (!response.steps) {
// raise ValueError("Agent 必须至少输出一个推理步骤")
if (response.needs_more_info && response.final_answer) {
// raise ValueError("标注需要更多信息时不应有最终答案")
if (response.steps[-1].action == "finish" && !response.final_answer) {
// raise ValueError("finish 动作必须提供最终答案")
return true;
// 使用
// result = structured_agent("量子计算和经典计算的本质区别是什么?")
// validate_agent_response(result)
}
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// from pydantic import BaseModel, Field
// from openai import OpenAI
// from typing import List, Optional
// Python: client = OpenAI()
// ===== 1. 用 Pydantic 定义输出 Schema =====
// AgentStep - CLI Agent class
type AgentStep struct {
// Python: thought: str = Field(
// Python: description="对当前状态的推理分析"
// Python: )
// Python: action: str = Field(
// Python: description="要调用的工具名称",
// Python: enum=["search", "lookup", "calculate", "finish"]
// Python: )
// Python: action_input: str = Field(
// Python: description="工具的输入参数"
// Python: )
// Python: confidence: float = Field(
// Python: description="对当前推理的置信度 0-1",
// Python: ge=0, le=1
// Python: )
// AgentResponse - CLI Agent class
type AgentResponse struct {
// Python: steps: List[AgentStep] = Field(
// Python: description="推理步骤列表"
// Python: )
// Python: final_answer: Optional[str] = Field(
// Python: description="最终答案(仅当 action=finish 时提供)",
// Python: default=None
// Python: )
// Python: needs_more_info: bool = Field(
// Python: description="是否需要更多信息才能回答"
// Python: )
// ===== 2. 用 OpenAI Structured Output 调用 =====
func structured_agent() {
// Python: response = client.beta.chat.completions.parse(
// Python: model="gpt-4o-2024-08-06",
// Python: messages=[
// Python: {"role": "system", "content":
// Python: "你是一个研究助手。分析问题并给出推理步骤。"},
// Python: {"role": "user", "content": question}
// Python: ],
// Python: response_format=AgentResponse # Pydantic 模型直接作为 Schema
// Python: )
// 3. 返回的是已验证的 Pydantic 对象
// Python: result = response.choices[0].message.parsed
// 类型安全!result.thought 是 str,result.confidence 是 float
for _, step := range result.steps {
fmt.Println(f"Thought: {step.thought}")
fmt.Println(f"Action: {step.action}({step.action_input})")
fmt.Println(f"Confidence: {step.confidence:.2f}")
if result.final_answer {
fmt.Println(f"Answer: {result.final_answer}")
return result
// ===== 3. Pydantic 自动验证 =====
// 如果 LLM 输出的 confidence 超出 0-1 范围,Pydantic 会自动拒绝
// 如果 action 不是枚举值之一,Pydantic 会自动拒绝
// 如果 steps 是空列表,可以根据业务规则添加额外验证
func validate_agent_response() {
if not response.steps {
// Python: raise ValueError("Agent 必须至少输出一个推理步骤")
if response.needs_more_info and response.final_answer {
// Python: raise ValueError("标注需要更多信息时不应有最终答案")
if response.steps[-1].action == "finish" and not response.final_answer {
// Python: raise ValueError("finish 动作必须提供最终答案")
return true
// 使用
// Python: result = structured_agent("量子计算和经典计算的本质区别是什么?")
// Python: validate_agent_response(result)
}
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// from pydantic import BaseModel, Field
// from openai import OpenAI
// from typing import List, Optional
// Python: client = OpenAI()
// ===== 1. 用 Pydantic 定义输出 Schema =====
public class AgentStep {
// Python: thought: str = Field(
// Python: description="对当前状态的推理分析"
// Python: )
// Python: action: str = Field(
// Python: description="要调用的工具名称",
// Python: enum=["search", "lookup", "calculate", "finish"]
// Python: )
// Python: action_input: str = Field(
// Python: description="工具的输入参数"
// Python: )
// Python: confidence: float = Field(
// Python: description="对当前推理的置信度 0-1",
// Python: ge=0, le=1
// Python: )
public class AgentResponse {
// Python: steps: List[AgentStep] = Field(
// Python: description="推理步骤列表"
// Python: )
// Python: final_answer: Optional[str] = Field(
// Python: description="最终答案(仅当 action=finish 时提供)",
// Python: default=None
// Python: )
// Python: needs_more_info: bool = Field(
// Python: description="是否需要更多信息才能回答"
// Python: )
// ===== 2. 用 OpenAI Structured Output 调用 =====
public static void structured_agent() {
// Python: response = client.beta.chat.completions.parse(
// Python: model="gpt-4o-2024-08-06",
// Python: messages=[
// Python: {"role": "system", "content":
// Python: "你是一个研究助手。分析问题并给出推理步骤。"},
// Python: {"role": "user", "content": question}
// Python: ],
// Python: response_format=AgentResponse # Pydantic 模型直接作为 Schema
// Python: )
// 3. 返回的是已验证的 Pydantic 对象
// Python: result = response.choices[0].message.parsed
// 类型安全!result.thought 是 str,result.confidence 是 float
for (var step : result.steps) {
System.out.println(String.format("$1"));
System.out.println(String.format("$1"));
System.out.println(String.format("$1"));
if (result.final_answer) {
System.out.println(String.format("$1"));
return result;
// ===== 3. Pydantic 自动验证 =====
// 如果 LLM 输出的 confidence 超出 0-1 范围,Pydantic 会自动拒绝
// 如果 action 不是枚举值之一,Pydantic 会自动拒绝
// 如果 steps 是空列表,可以根据业务规则添加额外验证
public static void validate_agent_response() {
if (!response.steps) {
// Python: raise ValueError("Agent 必须至少输出一个推理步骤")
if (response.needs_more_info && response.final_answer) {
// Python: raise ValueError("标注需要更多信息时不应有最终答案")
if (response.steps[-1].action == "finish" && !response.final_answer) {
// Python: raise ValueError("finish 动作必须提供最终答案")
return true;
// 使用
// Python: result = structured_agent("量子计算和经典计算的本质区别是什么?")
// Python: validate_agent_response(result)
}
}
✓ Structured Output 的核心优势
- 100% 格式保证:LLM 输出严格符合 Schema,无需正则解析
- 类型安全:字段类型、枚举值、范围约束全部锁定
- 可验证:Pydantic 自动验证 + 业务规则二次校验
- 开发效率:不用写解析代码,直接用 Pydantic 模型
5.9 上下文压缩实战
ReAct 的致命问题是上下文膨胀。每一步 Thought + Action + Observation 都被追加到消息历史中,10步任务可能积累上千 token 的上下文。到第20步,LLM 可能因为上下文太长而"遗忘"最初的问题。 ⚠️ 上下文膨胀的真实数据
Step 1: ~200 tokens (system + question + thought₁ + action₁ + obs₁)
Step 5: ~1,000 tokens (累积5轮)
Step 10: ~2,000 tokens
Step 20: ~4,000 tokens → LLM 开始遗忘早期信息
Step 50: ~10,000 tokens → 接近模型上下文窗口上限
三种压缩策略
1. 截断(Truncation) 最粗暴:直接丢弃最早的 N 条消息。优点是简单,缺点是可能丢失关键信息。
messages = messages[-K:]
只保留最近K条
2. 摘要(Summarization) 用 LLM 将早期对话压缩成摘要。保留语义,但消耗额外 LLM 调用。
summary = llm.summarize(old_msgs)
messages = [summary] + recent_msgs
3. 语义保护型压缩 最精细:锁定核心指令,只压缩中间步骤。确保关键信息不丢失。
locked = [system, question]
compressed = compress(middle)
messages = locked + compressed + recent
语义保护型压缩:锁定核心指令
压缩前后 Token 对比
from openai import OpenAI
client = OpenAI()
def semantic_compress(messages, keep_recent=3):
"""语义保护型压缩:锁定核心指令,压缩中间步骤"""
# 1. 锁定:System Prompt 和原始问题
locked = messages[:2] # system + user question
# 2. 保留:最近K步(完整不压缩)
recent = messages[-(keep_recent * 3):] # 每步3条(thought+action+obs)
# 3. 压缩:中间所有步骤 → 一段摘要
middle = messages[2:-(keep_recent * 3)]
if len(middle) > 6: # 只有中间步骤足够多才压缩
summary = client.chat.completions.create(
model="gpt-4o-mini", # 用小模型压缩,节省成本
messages=[
{"role": "system", "content":
"将以下对话历史压缩为简短摘要,保留关键决策和发现。"
"忽略冗余的推理过程,只保留结论性信息。"},
{"role": "user", "content":
f"对话历史:\n{format_messages(middle)}"}
]
).choices[0].message.content
compressed_middle = [
{"role": "system", "content": f"[历史摘要] {summary}"}
]
else:
compressed_middle = middle
# 4. 组合:锁定 + 压缩 + 保留
return locked + compressed_middle + recent
def format_messages(messages):
"""格式化消息列表为可读文本"""
text = ""
for msg in messages:
role = msg["role"]
content = msg["content"][:200] # 每条截断到200字
text += f"[{role}] {content}\n"
return text
# ===== Token 对比示例 =====
def compare_compression(original_msgs, compressed_msgs):
"""压缩前后 token 对比"""
def count_tokens(msgs):
# 粗略估算:1个中文字≈1.5token,1个英文词≈1token
total = 0
for msg in msgs:
content = msg["content"]
chinese_chars = sum(1 for c in content if '\u4e00' 6) {
const summary = client.chat.completions.create(;
const model = "gpt-4o-mini", # 用小模型压缩,节省成本;
const messages = [;
// {"role": "system", "content":
// "将以下对话历史压缩为简短摘要,保留关键决策和发现。"
// "忽略冗余的推理过程,只保留结论性信息。"},
// {"role": "user", "content":
// f"对话历史:\n{format_messages(middle)}"}
// ]
// ).choices[0].message.content
const compressed_middle = [;
// {"role": "system", "content": f"[历史摘要] {summary}"}
// ]
} else {
const compressed_middle = middle;
// 4. 组合:锁定 + 压缩 + 保留
return locked + compressed_middle + recent;
function format_messages(messages) {
/** docstring */
const text = "";
for (const msg of messages) {
const role = msg["role"];
const content = msg["content"][:200] # 每条截断到200字;
// text += f"[{role}] {content}\n"
return text;
// ===== Token 对比示例 =====
function compare_compression(original_msgs, compressed_msgs) {
/** docstring */
function count_tokens(msgs) {
// 粗略估算:1个中文字≈1.5token,1个英文词≈1token
const total = 0;
for (const msg of msgs) {
const content = msg["content"];
const chinese_chars = sum(1 for c in content if '\u4e00' 6 {
// Python: summary = client.chat.completions.create(
// Python: model="gpt-4o-mini", # 用小模型压缩,节省成本
// Python: messages=[
// Python: {"role": "system", "content":
// Python: "将以下对话历史压缩为简短摘要,保留关键决策和发现。"
// Python: "忽略冗余的推理过程,只保留结论性信息。"},
// Python: {"role": "user", "content":
// Python: f"对话历史:\n{format_messages(middle)}"}
// Python: ]
// Python: ).choices[0].message.content
// Python: compressed_middle = [
// Python: {"role": "system", "content": f"[历史摘要] {summary}"}
// Python: ]
} else {
// Python: compressed_middle = middle
// 4. 组合:锁定 + 压缩 + 保留
return locked + compressed_middle + recent
func format_messages() {
// Python: text = ""
for _, msg := range messages {
// Python: role = msg["role"]
// Python: content = msg["content"][:200] # 每条截断到200字
// Python: text += f"[{role}] {content}\n"
return text
// ===== Token 对比示例 =====
func compare_compression() {
func count_tokens() {
// 粗略估算:1个中文字≈1.5token,1个英文词≈1token
// Python: total = 0
for _, msg := range msgs {
// Python: content = msg["content"]
// Python: chinese_chars = sum(1 for c in content if '\u4e00' 6) {
// Python: summary = client.chat.completions.create(
// Python: model="gpt-4o-mini", # 用小模型压缩,节省成本
// Python: messages=[
// Python: {"role": "system", "content":
// Python: "将以下对话历史压缩为简短摘要,保留关键决策和发现。"
// Python: "忽略冗余的推理过程,只保留结论性信息。"},
// Python: {"role": "user", "content":
// Python: f"对话历史:\n{format_messages(middle)}"}
// Python: ]
// Python: ).choices[0].message.content
// Python: compressed_middle = [
// Python: {"role": "system", "content": f"[历史摘要] {summary}"}
// Python: ]
} else {
// Python: compressed_middle = middle
// 4. 组合:锁定 + 压缩 + 保留
return locked + compressed_middle + recent;
public static void format_messages() {
// Python: text = ""
for (var msg : messages) {
// Python: role = msg["role"]
// Python: content = msg["content"][:200] # 每条截断到200字
// Python: text += f"[{role}] {content}\n"
return text;
// ===== Token 对比示例 =====
public static void compare_compression() {
public static void count_tokens() {
// 粗略估算:1个中文字≈1.5token,1个英文词≈1token
// Python: total = 0
for (var msg : msgs) {
// Python: content = msg["content"]
// Python: chinese_chars = sum(1 for c in content if '\u4e00' string> = {
search: searchWeb,
lookup: lookupKeyword,
};
const REACT_PROMPT = `...`; // ReAct 系统提示词
async function reactAgent(question: string, maxSteps = 10): Promise {
const messages: any[] = [
{ role: "system", content: REACT_PROMPT },
{ role: "user", content: question },
];
for (let step = 0; step > tools = new HashMap<>();
static {
tools.put("search", ReActAgent::searchWeb);
tools.put("lookup", ReActAgent::lookupKeyword);
}
static final String REACT_PROMPT = "..."; // ReAct 系统提示词
public static String reactAgent(String question, int maxSteps) {
List messages = new ArrayList<>();
messages.add(ChatCompletionMessageParam.ofSystem(
ChatCompletionSystemMessageParam.builder()
.systemMessage(REACT_PROMPT).build()));
messages.add(ChatCompletionMessageParam.ofUser(
ChatCompletionUserMessageParam.builder()
.userMessage(question).build()));
Pattern actionRe = Pattern.compile("Action: (\\w+)\\((.*?)\\)");
for (int step = 0; step tool = tools.get(toolName);
String result = tool != null ? tool.apply(toolInput)
: "Error: tool " + toolName + " not found";
// 5. 注入 Observation
messages.add(ChatCompletionMessageParam.ofUser(
ChatCompletionUserMessageParam.builder()
.userMessage("Observation: " + result).build()));
}
return "达到最大步数限制";
}
}
核心就这几步:LLM 生成 → 解析 Action → 执行工具 → 注入 Observation → 循环。
5.11 ReAct 的优势与局限
✓ 优势
- 简单直观:Thought-Action-Observation 三段式,易于理解和实现
- 推理可审计:Thought 让每步推理可见,方便调试
- 错误可恢复:Observation 反馈让 Agent 能发现错误并调整
- 工具可组合:多步推理中可调用不同工具
- 通用性强:不依赖特定模型,任何 LLM 都能用
✗ 局限
- Token 消耗大:每步都带完整历史,长任务 token 消耗线性增长
- 串行执行:Thought→Action→Observation 严格串行,无法并行
- 依赖 LLM 质量:推理质量直接取决于 LLM 能力
- 格式不稳定:LLM 可能输出错误格式导致解析失败
- 容易循环:某些情况下 Agent 会陷入循环不收敛
5.12 ReAct 的演进与变体
本章介绍了 ReAct 的多个演进变体,下面是完整的 ReAct 家族演进图谱: 🔄 ReAct 家族演进 | 变体 | 改进点 | 核心机制 | 年份 | | --- | --- | --- | --- | | ReAct | 原始版:Thought-Action-Observation | 推理+行动交替循环 | 2022 | | ReWOO | 先规划再执行,减少token消耗 | Planner→Worker→Solver | 2023 | | LLM Compiler | 并行执行独立工具调用 | 依赖图(DAG)编译+并行 | 2023 | | Reflexion | 加入自我反思,从失败中学习 | 执行→评估→反思→重试循环 | 2023 | | LATS | 结合蒙特卡洛树搜索+ReAct | 树搜索+推理行动 | 2023 | | Function Calling ReAct | 用原生 Function Calling 替代文本解析 | 结构化工具调用 | 2023+ | ## 5.13 工程深度:生产级 Agent 主循环设计
前面我们学习了 ReAct、ReWOO、Reflexion 等推理模式,但它们都是概念模型。生产环境中,Agent 主循环需要解决一系列工程问题:烧钱怎么控制?死循环怎么检测?AI 输出被截断怎么办?接口超时怎么重试?本节基于 WaLiCode 项目的 streamingAgent.ts 真实实现,讲解生产级 Agent 主循环的六大工程机制。
5.13.1 双模式设计:Chat vs Agent
WaLiCode 的 Agent 主循环支持两种模式,用对比表格展示差异: | 维度 | Chat 模式 | Agent 模式 | | --- | --- | --- | | 交互方式 | 一问一答,不自动续写 | 自动多轮,直到任务完成 | | 最大轮次 | 1 轮 | 20 轮(可配置) | | 工具调用 | 每轮最多 5 次 | 每轮最多 5 次,总计最多 200 次 | | 重试次数 | 3 次 | 5 次(更宽容) | | 适用场景 | 简单问答、闲聊 | 编码任务、文件操作、复杂工作流 | | Token 预算 | 无全局限制 | 200k token 全局预算 | 为什么需要两种模式?如果用 Agent 模式处理简单问答,AI 可能会"过度思考"——明明一句话能回答的问题,它非要调几个工具验证一下,浪费 token 和时间。反过来,用 Chat 模式处理编码任务,AI 无法自动连续执行多步操作,用户体验很差。
// 伪代码:双模式入口
interface AgentLoopConfig {
mode: 'chat' | 'agent';
maxRounds: number; // chat: 1, agent: 20
maxToolCallsPerRound: number; // 默认 5
maxTotalToolCalls: number; // agent: 200
maxRetries: number; // chat: 3, agent: 5
tokenBudget: number; // agent: 200000
}
async function runAgentLoop(
messages: Message[],
config: AgentLoopConfig,
callbacks: StreamingCallbacks
) {
let round = 0;
let totalToolCalls = 0;
let totalTokens = 0;
while (round = GLOBAL_TOKEN_BUDGET) {
callbacks.onText('\n⚠️ 已达到全局 Token 预算限制,自动停止。');
break;
}
// 流式接收 AI 输出
for await (const event of ai.stream(currentMessages)) {
if (event.type === 'text') {
const deltaTokens = countTokens(event.content);
totalTokensAcrossRounds += deltaTokens;
// ... 实时累加
}
}
round++;
}
// // 伪代码:Token Budget 管理
// const GLOBAL_TOKEN_BUDGET = 200_000; // 20万 token 全局预算
// let totalTokensAcrossRounds = 0;
// while (round = GLOBAL_TOKEN_BUDGET) {
// callbacks.onText('\n⚠️ 已达到全局 Token 预算限制,自动停止。');
// break;
// }
// // 流式接收 AI 输出
// for await (const event of ai.stream(currentMessages)) {
// if (event.type === 'text') {
// const deltaTokens = countTokens(event.content);
// totalTokensAcrossRounds += deltaTokens;
// // ... 实时累加
// }
// }
// round++;
// }
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// Python: // 伪代码:Token Budget 管理
// Python: const GLOBAL_TOKEN_BUDGET = 200_000; // 20万 token 全局预算
// Python: let totalTokensAcrossRounds = 0;
// Python: while (round = GLOBAL_TOKEN_BUDGET) {
// Python: callbacks.onText('\n⚠️ 已达到全局 Token 预算限制,自动停止。');
// Python: break;
// Python: }
// Python: // 流式接收 AI 输出
// Python: for await (const event of ai.stream(currentMessages)) {
// Python: if (event.type === 'text') {
// Python: const deltaTokens = countTokens(event.content);
// Python: totalTokensAcrossRounds += deltaTokens;
// Python: // ... 实时累加
// Python: }
// Python: }
// Python: round++;
// Python: }
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// Python: // 伪代码:Token Budget 管理
// Python: const GLOBAL_TOKEN_BUDGET = 200_000; // 20万 token 全局预算
// Python: let totalTokensAcrossRounds = 0;
// Python: while (round = GLOBAL_TOKEN_BUDGET) {
// Python: callbacks.onText('\n⚠️ 已达到全局 Token 预算限制,自动停止。');
// Python: break;
// Python: }
// Python: // 流式接收 AI 输出
// Python: for await (const event of ai.stream(currentMessages)) {
// Python: if (event.type === 'text') {
// Python: const deltaTokens = countTokens(event.content);
// Python: totalTokensAcrossRounds += deltaTokens;
// Python: // ... 实时累加
// Python: }
// Python: }
// Python: round++;
// Python: }
}
关键设计点:
- 全局预算:不是单轮限制,而是整个会话的总量限制。20 万 token 约等于 15 万字中文,足够完成大部分编码任务
- 实时累加:每收到一段流式输出就立即计数,不等一轮结束才统计。这样即使单轮输出就超预算也能及时停止
- 优雅停止:超预算时不直接中断,而是注入提示让 AI 自然收尾,避免输出半截代码
5.13.3 死循环保护(Diminishing Returns Detection)
Agent 模式下 AI 可能陷入死循环——反复调用同一个工具、传完全相同的参数、得到完全相同的错误结果。这不罕见:AI 看到 FileNotFoundError,于是去 read_file,文件确实不存在,于是又去 write_file,写错了路径,又看到 FileNotFoundError……
WaLiCode 的检测策略:连续 2 轮执行完全相同的失败工具调用时自动终止。
// 伪代码:死循环检测
let lastRoundToolCalls: string[] = [];
let lastRoundErrors: string[] = [];
while (round 0
&& currentRoundErrors.every(e => lastRoundErrors.includes(e))
&& currentRoundErrors.length === lastRoundErrors.length;
if (sameErrors) {
callbacks.onText('\n⚠️ 检测到重复失败,自动停止以避免死循环。');
break;
}
lastRoundToolCalls = currentRoundToolCalls;
lastRoundErrors = currentRoundErrors;
round++;
}
为什么阈值设为 2 而不是 1?因为有些问题需要重试一次才能确认——比如网络抖动导致的临时失败,第二次重试可能就成功了。设为 2 是在"尽早发现死循环"和"允许合理重试"之间的平衡。
5.13.4 max_tokens 截断恢复
AI 模型有输出长度限制(如 4096 token)。当输出被截断时,finish_reason 会变成 'length' 而不是 'stop'。如果直接忽略,用户会看到半截代码或不完整的回答。WaLiCode 的方案是注入 "please continue" 自动恢复:
// 伪代码:截断恢复
for await (const event of ai.stream(currentMessages)) {
if (event.type === 'text') {
assistantContent += event.content;
}
if (event.type === 'finish') {
if (event.finish_reason === 'length') {
// 输出被 max_tokens 截断
wasTruncated = true;
}
}
}
if (wasTruncated && round IDLE_TIMEOUT_MS) {
callbacks.onText('\n⚠️ 空闲超时,自动停止。');
break;
}
// 每次收到事件时更新
for await (const event of ai.stream(...)) {
lastActivityTime = Date.now();
// ...
}
// const IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
// let lastActivityTime = Date.now();
// // 在主循环中检查
// if (Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) {
// callbacks.onText('\n⚠️ 空闲超时,自动停止。');
// break;
// }
// // 每次收到事件时更新
// for await (const event of ai.stream(...)) {
const lastActivityTime = Date.now();;
// // ...
// }
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// Python: const IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
// Python: let lastActivityTime = Date.now();
// Python: // 在主循环中检查
// Python: if (Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) {
// Python: callbacks.onText('\n⚠️ 空闲超时,自动停止。');
// Python: break;
// Python: }
// Python: // 每次收到事件时更新
// Python: for await (const event of ai.stream(...)) {
// Python: lastActivityTime = Date.now();
// Python: // ...
// Python: }
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// Python: const IDLE_TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
// Python: let lastActivityTime = Date.now();
// Python: // 在主循环中检查
// Python: if (Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) {
// Python: callbacks.onText('\n⚠️ 空闲超时,自动停止。');
// Python: break;
// Python: }
// Python: // 每次收到事件时更新
// Python: for await (const event of ai.stream(...)) {
// Python: lastActivityTime = Date.now();
// Python: // ...
// Python: }
}
413 反应式压缩:当 API 返回 413(请求体过大)时,紧急压缩上下文后重试。
try {
for await (const event of ai.stream(currentMessages)) {
// ...
}
} catch (err) {
if (err instanceof HTTPError && err.status === 413) {
// 请求体过大,紧急压缩上下文
const summary = await generateAiSummary(currentMessages);
currentMessages = [
{ role: 'system', content: '之前的对话已压缩:' + summary },
...recentMessages // 保留最近的几轮
];
// 用压缩后的上下文重试
continue;
}
throw err;
}
// try {
// for await (const event of ai.stream(currentMessages)) {
// // ...
// }
// } catch (err) {
// if (err instanceof HTTPError && err.status === 413) {
// // 请求体过大,紧急压缩上下文
// const summary = await generateAiSummary(currentMessages);
const currentMessages = [;
// { role: 'system', content: '之前的对话已压缩:' + summary },
// ...recentMessages // 保留最近的几轮
// ];
// // 用压缩后的上下文重试
// continue;
// }
// throw err;
// }
package main
import (
"fmt"
"os"
"os/exec"
"strings"
)
// Python: try {
// Python: for await (const event of ai.stream(currentMessages)) {
// Python: // ...
// Python: }
// Python: } catch (err) {
// Python: if (err instanceof HTTPError && err.status === 413) {
// Python: // 请求体过大,紧急压缩上下文
// Python: const summary = await generateAiSummary(currentMessages);
// Python: currentMessages = [
// Python: { role: 'system', content: '之前的对话已压缩:' + summary },
// Python: ...recentMessages // 保留最近的几轮
// Python: ];
// Python: // 用压缩后的上下文重试
// Python: continue;
// Python: }
// Python: throw err;
// Python: }
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;
// Python: try {
// Python: for await (const event of ai.stream(currentMessages)) {
// Python: // ...
// Python: }
// Python: } catch (err) {
// Python: if (err instanceof HTTPError && err.status === 413) {
// Python: // 请求体过大,紧急压缩上下文
// Python: const summary = await generateAiSummary(currentMessages);
// Python: currentMessages = [
// Python: { role: 'system', content: '之前的对话已压缩:' + summary },
// Python: ...recentMessages // 保留最近的几轮
// Python: ];
// Python: // 用压缩后的上下文重试
// Python: continue;
// Python: }
// Python: throw err;
// Python: }
}
413 压缩与正常压缩的区别:
- 触发时机:正常压缩在 token 达到 80% 时主动触发;413 压缩在请求被拒绝后被动触发
- 压缩力度:正常压缩保留近期 3-5 轮;413 压缩可能只保留最近 1-2 轮,更激进
- 用户感知:正常压缩用户无感;413 压缩可能丢失重要上下文,影响回答质量 🔗 4.13 核心要点
双模式:Chat 简单高效,Agent 自动多轮,按场景选择避免浪费。
Token Budget:全局预算防止烧钱,实时累加及时停止。
死循环保护:连续 2 轮相同失败自动终止,阈值 2 平衡了灵敏度和容错。
截断恢复:max_tokens 截断时注入 continue 自动续写,有限次恢复避免无限循环。
指数退避:临时错误自动重试,2^n 秒退避避免雪崩,区分可重试和不可重试错误。
413 压缩:请求过大时紧急压缩重试,是正常压缩的兜底方案。 📋 八股总结 — 面试高频考点
Q1: 什么是 ReAct 模式?它解决了什么问题?
ReAct = Reasoning + Acting,让 LLM 交替进行**推理(Thought)、行动(Action)、观察(Observation)**的循环,直到得出最终答案。
解决的问题:纯推理模型(CoT)无法获取外部信息,容易幻觉;纯行动模型直接调用工具但缺乏规划。ReAct 将推理和行动结合,推理指导行动,行动反馈推理。
Q2: ReAct 循环中 Thought、Action、Observation 各自的作用?
Thought(推理):LLM 的"内心独白",分析当前状态,决定下一步该做什么。是推理过程的核心,也是可审计性的基础。
Action(行动):基于 Thought 的决策,调用具体工具。格式为 Action: tool_name(input)。由 Agent 框架解析和执行。
Observation(观察):工具执行后返回的结果。注意:这不是 LLM 生成的,而是框架执行工具后注入到对话中的。LLM 基于 Observation 进行下一轮 Thought。
Q3: ReAct 的 Prompt 是怎么设计的?为什么需要强制格式?
Prompt 通过 "Thought:" "Action:" "Observation:" 标记强制结构化输出。
需要强制格式的原因:Agent 框架需要从 LLM 输出中解析出"要调用什么工具"和"传什么参数"。如果 LLM 输出自然语言,解析极其困难且不可靠。强制格式让输出可程序化解析,同时保持推理过程的可读性。
Q4: ReAct 模式有哪些局限性?后续如何改进?
局限:① Token 消耗线性增长(每步带完整历史);② 严格串行无法并行;③ 格式解析不稳定;④ 可能陷入死循环。
改进方向:① ReWOO 先规划后执行减少token;② LLM Compiler 并行执行独立步骤;③ Reflexion 加入自我反思从失败学习;④ Structured Output 解决格式不稳定;⑤ 上下文压缩控制 token 消耗。
Q5: ReAct 中的 Observation 是 LLM 生成的吗?为什么?
不是。 Observation 是 Agent 框架执行工具后,将真实结果注入到对话历史中的。
这是最容易踩坑的点。如果让 LLM 自己"想象" Observation,就失去了与真实世界交互的能力,变成纯粹的推理游戏。ReAct 的核心价值就在于 Action 真实执行、Observation 真实反馈。这也是 ReAct 相比纯 CoT 的根本优势。
Q6: ReWOO 与 ReAct 的核心区别是什么?
ReAct 是边思考边执行(Thought→Action→Observation 循环),每步都需要 LLM 参与,token 消耗随步数线性增长。
ReWOO 是先规划后执行(Planner→Worker→Solver),LLM 只调用2次(规划+求解),Worker 执行工具不消耗 LLM token。
ReWOO 的优势是 token 消耗大幅减少;劣势是 Planner 无法根据中间结果调整策略,如果第一步规划错误,后续只能沿着错误路径走到底。
Q7: LLM Compiler 如何实现并行执行?它的适用条件是什么?
LLM Compiler 让 LLM 一次性输出多个工具调用,构建依赖图(DAG),识别没有数据依赖的步骤同时执行。
适用条件:工具调用之间确实没有依赖关系(如并行搜索多个城市天气)。如果有依赖(如先搜结果再基于结果做计算),则依赖步骤必须等前置步骤完成。并行加速效果取决于独立步骤的占比。
Q8: Structured Output 相比文本解析(ReAct的 "Action:" 标记)有什么本质优势?
文本解析:LLM 输出 "Action: search("query")",用正则解析。不稳定——LLM 可能输出格式偏差(少冒号、多空格、换行位置不同),导致解析失败。
Structured Output:通过 JSON Schema + Pydantic 约束,LLM 输出严格符合预定义结构。字段名、类型、枚举值、范围全部锁定,100% 可靠解析,无需正则。
本质区别:文本解析是"尽力而为",Structured Output 是"强制保证"。
Q9: 上下文压缩的三种策略分别是什么?语义保护型压缩的核心思想?
截断:直接丢弃最早的消息,简单但可能丢失关键信息。
摘要:用 LLM 将早期对话压缩成摘要,保留语义但消耗额外调用。
语义保护型压缩:将消息分为三层——锁定层(System Prompt + 原始问题,不压缩)、压缩层(中间步骤,摘要压缩)、保留层(最近K步,原样保留)。核心思想是确保关键指令永远不丢失,只压缩可牺牲的中间推理过程。