经典范式手写
经典范式手写
第 07 章我们写出了最小 Agent Loop:while (iterations < maxIterations) 里跑 think → act → observe,模型要工具就执行回填,不要就把文本当答案。那一刻你可能有种「我已经会写 Agent 了」的错觉:循环有了,但循环的行为不可预期。
同一个 50 行的 loop,塞给同一个模型,你能得到完全不同的走法:它可能查完天气就收工、完全忘了你还让它看景点;可能在同一个失败结果上反复重试同一把工具;可能把一段明显不合格的文案直接交付给你。循环保证了「它会一直跑」,但没保证「它跑得像样」。这一章给「像样」补上三种业界沉淀下来的控制结构模板:范式(paradigm),ReAct、Plan-and-Execute、Reflection。它们都是第 07 章那个 loop 的变体,不需要任何新依赖,只改控制流。
本章目标
读完本章并做完配套练习后,你应该能够:
- 说出「范式」是什么:同样的循环 + 注入
callLlm,控制结构不同,行为就不同; - 手写 ReAct:最小 loop + 显式思考轨迹,说清「为什么原生 tool calling 时代 ReAct 是免费的」;
- 手写 Plan-and-Execute:先让模型产出计划当数据,再机械执行、一次综合,说清它与 ReAct 的取舍(模型调用数 vs 灵活性);
- 手写 Reflection:draft → judge → revise 外环,评审不过就修订,说清它的成本与盲区;
- 面对一个任务能选型:什么时候用哪个范式,什么时候一个范式都不用;
- 说出三种范式各自的典型失败模式与解药(保险丝、评审标准、计划过期)。
开场反例:裸 loop 的行为不可预期
回到第 07 章的天气助手,任务升级成:
苏州周末适合户外活动吗?帮我查一下苏州周末的天气,再看两个值得去的景点,最后给一份简短建议。
把任务丢进裸 loop,会发生什么?取决于模型当时的发挥,它可能:
- 查完天气就收工:「适合,记得防晒」,景点的事完全忘了;
- 查到一半开始纠结:景点 A 和景点 B 先查哪个?要不要再查第三遍天气?
- 干脆不查工具,凭知识库编一段「拙政园周末晴」(幻觉)。
你无法预测它会走哪条路,因为**「下一步做什么」完全交给每一轮模型的临场发挥**。三个问题悬而未决:
- 多步任务怎么不跑偏? 走一步看一步缺乏全局方向感:要不要先想清楚「总共几步、每步做什么」再动手?
- 决策质量谁来把关? 输出有没有错、够不够好,循环里没有任何检查点。
- 中间出了状况怎么调整? 工具报错、信息不足时,有没有结构化的「修正」路径?
业界沉淀出的三种经典范式,恰好是这三个问题的三个答案。它们共享同一个内核,第 07 章的 loop,只是控制流模板不同:
flowchart LR
base["第 07 章最小 loop<br/>think → act → observe"] --> react["ReAct<br/>把思考轨迹显式化"]
base --> pne["Plan-and-Execute<br/>先计划(数据)再执行"]
base --> refl["Reflection<br/>外层套 draft → judge → revise"]
datawhalechina/hello-agents 的第四章用一整章逐一实现过这三者(第四章 智能体经典范式构建),我们这一章用 TypeScript 把它们压进三个函数,下面逐个来。
范式一:ReAct——边想边做
动机:思考与行动分开都会坏
在 ReAct 之前,让模型解决多步任务有两条路,各坏一半:
- 纯思维链(Chain-of-Thought):让模型一口气把推理写出来再给结论,能推理,但无法与外部世界交互,纯靠记忆容易在事实性问题上幻觉(第 11 章讲过知识边界);
- 纯行动(Act):让模型直接输出动作序列,能执行,但没有推理轨迹,动作之间缺乏关联与纠错依据。
ReAct(Yao et al., 2022,arXiv:2210.03629)把两者交替编排:先思考再行动,行动结果成为下一次思考的输入。这个「思考-行动-观察」的循环,正是第 07 章我们手写的那个 loop,ReAct 就是加了思考轨迹的 Agent Loop:
flowchart TD
start(["任务进入 messages"]) --> think["Thought · 模型分析现状与下一步<br/>输出文本(思考)+ 结构化 tool_calls"]
think --> hasTool{"要调用工具?"}
hasTool -- "是" --> act["Action · harness 执行工具<br/>模型只决定,不执行"]
act --> obs["Observation · 结果作为 tool 消息回填<br/>成为下一轮思考的输入"]
obs --> think
hasTool -- "否" --> final["思考轨迹 + 文本即最终答案"]
最小实现:loop + 思考轨迹
第 07 章的 runAgent 只差一件事:把每一轮模型的文本记下来,这就是 ReAct 的「Thought 轨迹」,它让每一步行动的原因可以被看见、被调试。其余一字不改:
export async function runReact(
initialMessages: Message[],
opts: { callLlm: CallLlm; tools: Tool[]; maxIterations?: number },
): Promise<{ messages: Message[]; finalText: string; iterations: number; thoughts: string[] }> {
const { callLlm, tools, maxIterations = 5 } = opts;
const messages = [...initialMessages];
const thoughts: string[] = []; // 思考轨迹:一次模型调用记一条
let iterations = 0;
while (iterations < maxIterations) {
const response = await callLlm(messages, tools);
thoughts.push(response.content); // 这一轮模型「想」了什么
const assistant: Message = { role: 'assistant', content: response.content };
if (response.toolCalls.length > 0) assistant.tool_calls = response.toolCalls;
messages.push(assistant);
if (response.toolCalls.length === 0) {
return { messages, finalText: response.content, iterations: iterations + 1, thoughts };
}
for (const call of response.toolCalls) {
const tool = tools.find((t) => t.name === call.name);
if (!tool) throw new Error(`Unknown tool: ${call.name}`);
const output = await tool.execute(call.arguments);
messages.push({ role: 'tool', tool_call_id: call.id, content: output });
}
iterations += 1;
}
const lastAssistant = [...messages].reverse().find((m) => m.role === 'assistant');
return { messages, finalText: lastAssistant?.content ?? '', iterations, thoughts };
}
注意 thoughts 里存的是模型输出的原始文本,不是我们臆测的「意图」。有了它,你可以事后回答「它为什么调了这把工具」「它在哪一步跑偏了」,这是调试一切 Agent 行为的第一手材料,也是第 13 章评估里「失败模式分类」的输入。
适用场景
ReAct 是默认范式,没有特殊理由就用它:
- 需要实时/外部信息的任务:天气、搜索、数据库查询,模型不知道的,查了才知道;
- 走一步看一步的动态任务:方向要随观察调整,上一步的结果可能改变下一步的选择;
- 可解释性敏感的场景:思考轨迹直接可展示(很多产品把「思考过程」折叠给用户看,就是从
thoughts渲染的)。
一句话:ReAct 买的是灵活性,每步都被最新观察锚定,代价是每步一次模型往返。
范式二:Plan-and-Execute——先谋后动
动机:ReAct 的「走一步看一步」在复杂任务上会跑偏
ReAct 的弱点从它的优点长出来:决策粒度是一步。任务步骤一多,模型可能在中途迷失:查了 A 忘了 B、在某个分支上反复打转。Plan-and-Solve(Wang et al., 2023,arXiv:2305.04091)给出的方案朴素而有效:先整体规划,再执行。规划模型先生成完整步骤计划,执行模型再逐步完成,「三思而后行」:
flowchart TD
start(["任务"]) --> phase1
subgraph phase1["规划阶段 · 1 次模型调用"]
p1["模型产出计划<br/>{ steps: [...] }——计划是数据,不是散文"]
end
phase1 --> phase2
subgraph phase2["执行阶段 · harness 机械执行"]
e1["step1 查天气 → 记录结果"]
e2["step2 查景点 → 记录结果"]
e3["... 其余工具步骤,按序执行"]
end
phase2 --> phase3
subgraph phase3["综合阶段 · 1 次模型调用"]
s1["模型收到任务 + 全部观察<br/>产出最终答案"]
end
phase3 --> final(["finalText"])
两个阶段的分工值得看清:规划是模型的事,执行是 harness 的事。规划阶段产出的是数据(一个 steps 数组),不是散文,harness 才能直接遍历执行、校验、甚至展示给人看。
最小实现:计划当数据 + 机械执行 + 一次综合
export async function runPlanAndExecute(
task: string,
opts: { callLlm: CallLlm; tools: Tool[] },
): Promise<{ plan: PlanStep[]; results: Record<string, string>; finalText: string; iterations: number }> {
const { callLlm, tools } = opts;
// 阶段一:规划。模型第一轮的 content 是 JSON 序列化的计划。
const planResponse = await callLlm([{ role: 'user', content: task }], tools);
const plan = parsePlan(planResponse.content);
// 阶段二:执行。除最后一步外,每步都必须带 tool,harness 机械执行。
const results: Record<string, string> = {};
for (let i = 0; i < plan.steps.length - 1; i++) {
const step = plan.steps[i];
if (!step.tool) throw new Error(`step ${step.id} has no tool but is not the final synthesis step`);
const tool = tools.find((t) => t.name === step.tool);
if (!tool) throw new Error(`Unknown tool: ${step.tool}`);
results[step.id] = await tool.execute(step.args ?? {});
}
// 阶段三:综合。最后一次调用收到任务 + 全部观察,产出最终答案。
const observations = plan.steps
.filter((s) => results[s.id] !== undefined)
.map((s) => `- ${s.description}: ${results[s.id]}`)
.join('\n');
const solveResponse = await callLlm(
[
{ role: 'user', content: task },
{ role: 'user', content: `已收集到的信息:\n${observations}` },
],
[],
);
return { plan: plan.steps, results, finalText: solveResponse.content, iterations: 2 };
}
对照 ReAct 看取舍:同一个「查天气 → 查景点 → 出建议」任务,ReAct 要 3 次模型调用(每步决策一次),Plan-and-Execute 只要 2 次(规划 + 综合),中间的工具步骤由 harness 机械执行、零模型调用。省下来的调用是拿灵活性换的:计划一旦生成,执行阶段不会再根据观察调整:计划之外的情况一概不管。
learn-claude-code 的 s05(TodoWrite)在生产里的形态就是它:先列任务清单、逐项打勾执行,作者实测「有计划 vs 无计划的完成率翻倍」(s05 TodoWrite)。pguso 的 Lesson 08 则从另一个角度印证:计划应该当数据存、当数据展示,而不是埋在思考里(Lesson 08: Planning)。
适用场景
- 步骤可预见、依赖关系清晰的多步任务,规划能提前列出,执行不需要临场决策;
- 想压低模型调用数/成本:中间步骤不需要模型参与;
- 计划要给人看/给人批:数据化的计划可以渲染成清单,甚至可以交给人确认后再执行(第 18 章 HITL 的天然接口,计划即审批对象)。
一句话:Plan-and-Execute 买的是效率与可控性,模型调用少、计划可见可批;代价是环境若在计划后变化,它会执行一份过期计划。
范式三:Reflection——写完回头改
动机:一次生成的输出,质量没人把关
ReAct 和 Plan-and-Execute 都在解决「怎么做事」,Reflection 解决「做得怎么样」。生成式任务的输出是概率的:模型第一版文案可能漏了关键信息、代码可能藏着边界 bug,而且它自己没意识到。Reflexion(Shinn et al., 2023,arXiv:2303.11366)的思想是给 Agent 加一条自我批判回路:生成 → 评审 → 不通过就带着评审意见修订,直到通过或达到轮数上限。注意它的名字叫 Reflexion 而非 Reflection。论文强调这是语言形式的强化学习:不改权重,只把「哪里错了、怎么改」写进上下文,下一轮自然改好:
flowchart TD
gen["生成一版草稿"] --> judge{"评审通过?"}
judge -- "是" --> done["草稿即最终答案"]
judge -- "否" --> fb["草稿 + 评审意见追加进消息历史<br/>要求模型修订"]
fb --> gen
最小实现:draft → judge → revise 外环
关键设计:评审(judge)是注入的。它可以是规则检查器(「必须包含防晒建议」「长度 ≥ 40 字」),也可以是一次评审 LLM 调用,练习里注入脚本化的确定性评审,演示里走模型通道。生成器与评审解耦,Reflection 环才能离线可测:
export async function runReflect(
task: string,
opts: { callLlm: CallLlm; judge: (candidate: string) => JudgeVerdict | Promise<JudgeVerdict>; maxRounds?: number },
): Promise<{ finalText: string; drafts: string[]; feedbacks: string[]; rounds: number }> {
const { callLlm, judge, maxRounds = 3 } = opts;
const messages: Message[] = [{ role: 'user', content: task }];
const drafts: string[] = [];
const feedbacks: string[] = [];
let rounds = 0;
while (rounds < maxRounds) {
const response = await callLlm(messages, []);
const draft = response.content;
drafts.push(draft);
const verdict = await judge(draft);
rounds += 1;
if (verdict.passed) {
return { finalText: draft, drafts, feedbacks, rounds };
}
feedbacks.push(verdict.feedback);
// 把草稿与评审意见追加进历史:下一轮修订能看到「自己上一版为什么被否」
messages.push({ role: 'assistant', content: draft });
messages.push({ role: 'user', content: `评审意见:${verdict.feedback}。请根据意见修改,重新输出。` });
}
// 保险丝:评审一直不过,用最后一版草稿兜底,绝不让用户拿到空字符串
return { finalText: drafts[drafts.length - 1] ?? '', drafts, feedbacks, rounds };
}
注意消息历史的追加顺序:task → assistant(草稿) → user(评审意见)。这与第 07 章 loop 的「只追加」精神一致:修订轮模型看到的是完整过程,而不是孤立的一句话。rounds 是评审次数,drafts 记录每一版,如果修订越改越差,你能从 drafts 里挑出最好的一版(best-of-n 是 Reflection 的常见加固,见下文坑四)。
适用场景
- 输出质量敏感的生成任务:文案、翻译、代码审查、SQL 生成,一次过不了关就改到过关;
- 有明确可判的评审标准:标准越客观(规则检查、单元测试、长度/关键词),judge 越可靠;
- 反复打磨值得付费的场景:评审与修订都是模型调用,质量收益要能覆盖成本。
一句话:Reflection 买的是质量,输出要过评审关;代价是生成 + 评审的调用都算成本,且评审质量依赖评审者(可能和生成者是同一个模型)。
三范式对照与选择
| 维度 | ReAct | Plan-and-Execute | Reflection |
|---|---|---|---|
| 决策时机 | 每一步 | 开头一次(计划) | 生成后(评审) |
| 计划 | 隐式(散在每步思考里) | 显式(steps 数据) | 无 |
| 模型调用数 | 每步 1 次,最多 | 少(规划 + 综合) | 生成 + 评审,翻倍 |
| 适应性 | 高(每步随观察调整) | 低(计划提交后不再调整) | 中(对自己的评审意见调整) |
| 典型场景 | 实时信息、探索式任务 | 可预见多步任务、计划要给人看 | 文案/代码等质量敏感生成 |
| 主要失败模式 | 思考与行动脱节、死循环 | 计划过期、计划当散文 | 评审含糊、同源盲区、修订倒退 |
选型不是非此即彼,更常见的是组合:ReAct 里嵌一步「先给个计划」(混合范式,下一节讲);Plan-and-Execute 的规划阶段可以用 Reflection 把计划评审一遍;Reflection 的评审者可以换成另一个模型。但默认从 ReAct 起步,pguso 的课程顺序也是「先把 loop 跑稳,再谈规划与反思」(Lesson 08: Planning)。只有任务明确暴露了 ReAct 的问题(跑偏、太贵、质量差),才值得上更重的结构。
native tool calling 时代,哪些范式「免费」了
这一节回答 PLAN 里 TP11 的最后一个问题:协议把哪些机制内化了,导致你什么都不用写?
| 范式 | 免费吗 | 为什么 |
|---|---|---|
| ReAct | ✅ 基本免费 | 原生 tool calling 把 Thought/Action/Observation 结构化为协议:思考就是每轮的 content,行动就是结构化 tool_calls,观察就是 tool 消息回填。第 07 章那个 loop 就是 ReAct。对比 ReAct 文本协议时代(2022):你不仅要写正则解析 Action: {...} 行,还要用 stop=["Observation:"] 截断生成防模型幻觉 Observation。这些在原生路线下全部消失(第 07 章对比表讲过)。 |
| Reflection | 🔶 半免费 | 代码量确实很小:多一个评审 prompt 或注入一个 judge,十行以内。但成本不免费:每轮评审都是一次模型调用,质量收益要覆盖翻倍的调用量;且评审质量依赖模型,同一个模型的自我评审有同源盲区。协议没有把「自我批判」内化给你。 |
| Plan-and-Execute | ❌ 不免费 | 计划结构是 harness 的控制流设计,协议不提供任何「计划」原语。规划 → 机械执行 → 综合的三段式必须你自己写;计划当数据存、给人看、甚至交给人批,全是应用层的事。reasoning 模型在内部「想得更多」不等于产出可展示、可执行、可审批的计划数据。 |
判断「免费与否」有个好用标准:这个机制的载体是协议还是 harness。协议把机制内化了(tool calling 之于 ReAct),你就免费;机制要靠控制流结构实现(计划之于 PnE、评审回路之于 Reflection),你就得自己写。顺便说一句,ReAct 的「免费」有一个隐含前提:模型厂商要支持原生 tool calling(近两年主流模型均已支持);在只有文本模型的场景(本地小模型),ReAct 文本协议仍是那条后备路线,第 07 章的对比表就是为这种时刻准备的。
常见坑与失败模式
坑一(ReAct):思考轨迹不落地。 模型 Thought 说「先查天气」,Action 却去调了搜索,思考与行动脱节。原因多在 prompt:工具描述不清、思考格式约束太松。把 thoughts 打出来看一遍,通常立刻能定位。
坑二(ReAct):死循环。 和 ch07 一样,模型每轮都要工具时 maxIterations 是唯一的救星。反思一下:只有 ReAct 会死循环吗? 不——Plan-and-Execute 的规划阶段若模型反复产出非法计划、Reflection 的 judge 若永远不过,一样会转圈。每个循环变体都要有自己的保险丝(maxIterations / maxRounds)。
坑三(Plan-and-Execute):计划过期。 计划提交后环境变了(要查的城市下雨了、数据库 schema 换了),执行阶段仍按旧计划跑。最小实现不做重规划;生产里常见的加固是混合范式:在计划里显式安排一个「复查」步骤,或执行若干步后带着新观察重新规划一次,这本质上又回到 ReAct 的灵活性,只是把「重规划」变成计划中的一等公民。
坑四(Plan-and-Execute):计划当散文不当数据。 模型把计划写成自然语言段落,harness 只能靠正则和 indexOf 去捞步骤,第 05 章的教训在这里重演。让模型输出 { steps: [...] } JSON,解析、校验、展示、审批全都顺了;解析失败就抛错重试,别猜。
坑五(Reflection):评审标准含糊。 judge 的标准越主观(「写得不错」「有点干」),评审结果越随机,修订越像碰运气。把评审做成可判的检查点:「必须包含具体温度」「代码必须能通过这组单元测试」「输出 ≤ 100 字」。Hello-Agents 第四章在实现反思时也提醒:评审意见要具体到能指导修改(第四章 智能体经典范式构建)。
坑六(Reflection):同源盲区与修订倒退。 同一个模型既当生成者又当评审者,它看不出自己的系统性错误(「中文就这个风格」),这叫同源盲区:对策是换模型/换视角当评审。而修订可能越改越差:保留历史最佳版本(drafts 里挑评分最高的,best-of-n)是廉价且有效的加固。
坑七(共通):把范式当银弹。 「加了 Reflection 就高级」「不上 Planning 就不专业」,范式是控制结构,不是信仰。判断该不该用哪个范式,唯一可靠的手段是第 13 章要讲的评估:定义任务集、跑出失败模式、用数据决定。这也是本章练习之外最该带走的心态。
动手练习
配套练习要求你在 starter/src/paradigms.ts 里实现三个函数,把第 07 章的 loop 改造成三种范式,并通过三个 stage 的测试:
- stage 1 · ReAct:
runReact,loop + 思考轨迹thoughts(行为与 ch07 一致,多记录每轮模型文本); - stage 2 · Plan-and-Execute:
runPlanAndExecute,解析计划 JSON、校验每步(只有最后一步可不带工具)、按序执行、把观察喂给综合调用; - stage 3 · Reflection:
runReflect,draft → judge → revise,评审不过就修订,maxRounds保险丝兜底。
练习全部用注入式的脚本化 mock 模型判题,无需 API key、结果确定可复现。
小结
- 范式 = 控制结构模板:同一套「循环 + 注入
callLlm」,只改控制流,就得到 ReAct / Plan-and-Execute / Reflection 三种行为; - ReAct:loop + 思考轨迹,边想边做、每步被观察锚定,默认范式,买的是灵活性;
- Plan-and-Execute:先出计划(数据,不是散文)→ 机械执行 → 一次综合,买的是效率与可控性,代价是不再随环境调整;
- Reflection:draft → judge → revise 外环,评审注入式、不过就改,买的是质量,代价是调用翻倍与同源盲区;
- 免费与否看载体:原生 tool calling 让 ReAct 免费(协议内化机制);Reflection 半免费(代码少但调用要付费);Plan-and-Execute 不免费(计划结构是 harness 的控制流设计);
- 每个循环变体都要保险丝(
maxIterations/maxRounds),截断后必须有兜底文本; - 默认从 ReAct 起步,任务暴露了明确问题再上更重的结构;选型判断交给评估,不靠信仰。
下一章(ch13)做评估入门:任务集、grader、失败模式分类,你会发现第 13 章的三类失败模式(planning / tool / efficiency)恰好对应本章三种范式的软肋:计划跑偏、工具用错、调用过贵。评估是把「我觉得这个范式好」变成「数据说这个范式好」的唯一途径。
延伸阅读
- Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models, arXiv:2210.03629:ReAct 原论文,思考-行动-观察循环的出处。
- Wang et al., Plan-and-Solve Prompting, arXiv:2305.04091:Plan-and-Solve 原论文,「先整体规划再执行」的出处。
- Shinn et al., Reflexion: Language Agents with Verbal Reinforcement Learning, arXiv:2303.11366:Reflexion 原论文,「语言形式的强化学习」:不改权重,把评审意见写进上下文。
- datawhalechina/hello-agents · 第四章 智能体经典范式构建:三范式(ReAct / Plan-and-Solve / Reflection)的完整手写教程,含提示词模板与调试技巧。
- pguso/agents-from-scratch · Lesson 08: Planning:「计划当数据,而不是当思考」:规划的数据化形态与原子动作。
- shareAI-lab/learn-claude-code · s05 TodoWrite:生产形态的 Plan-and-Execute:任务清单 + 逐项打勾,实测完成率翻倍。