前言
大家好,我是唐某人~ 我非常乐意与大家分享我在AI领域的经验和心得。我的目标是帮助更多的前端工程师以及其他开发岗位的朋友,能够以一种轻松易懂的方式掌握AI应用开发的技能。希望我们能一起学习,共同进步!
1.1 回顾
在《教你如何用 JS 实现 Agent 系统(2)------ 开发 ReAct 版本的"深度搜索"》中,我分享了什么是深度搜索,什么是 ReAct 设计模式以及 ReAct 版本深度搜索的实现思路。
1.2 目标
不知道你是否发现,现在的 Cursor 或者 Trae,它们在解决问题前,会先列一个 To-dos 任务清单,然后一步步的处理这些任务。
相比之前的处理方式,这种"先规划再行动"的模式可以让 LLM 对未来要做的事情有一个大体的规划,避免了方向跑偏。同时也可以让用户知道 AI 工具的解决思路,而不是等着一个"黑盒"给你最终的结果。

所以,这篇我们就一起来学习这种"先规划,再行动"的设计模式,然后用这种模式来实现深度搜索。我们的目标是:
- 学习 ReAct 模式有哪些不足
- 了解 Plan And Execute 设计模式是怎么工作的
- 用 Plan And Execute 的模式实现深度搜索
二、什么是 Plan And Execute
2.1 ReAct 缺点
在上一篇文章中,我们提到ReAct通过多轮"思考→行动→观察"的循环逐步接近目标。这种方法存在几个问题:
- 缺乏整体规划:采用走一步看一步的方式,可能导致推理方向不确定且难以控制,随着步骤增加可能偏离目标。
- 效率低下:由于是按顺序执行任务,复杂的问题需要更多的推理步骤,从而增加了完成时间。
- 上下文限制:所有工具的结果都会累积到一个模型的上下文中,因为没有拆分和规划,很容易超出上下文容量限制。
2.2 由来和概念
ReAct 模式有如上的缺点,所以后来人们设计了很多改进的方法。今天我们来讲讲其中的一种叫 Plan And Execute 的模式。这个方法受到了 Plan-and-Solve 论文和 Baby-AGI 项目的启发。主要分为三个步骤:
- 规划阶段:先制定一个详细的多步骤行动计划。
- 执行阶段:按照计划一步步执行,并返回每一步的结果。
- 重规划阶段:根据执行结果调整计划。
这种模式的好处就是它会先制定一个整体计划,然后把每个具体任务分配给另一个独立的Agent来完成。这样做的好处有两点:
- 有一个总管全局的Agent,确保解决问题的方向不会跑偏。
- 每个任务的执行,交给独立的 Agent。这样任务范围小、职责单一、上下文不容易撑爆。
为了让大家对这个流程有一个更直观的理解,我准备了一张流程图供大家参考。

首先,我们从主体来看:
- 用户:提问题、发起对话的人
- Plan And Exectue Agents:一个由多 Agent 组成的系统。Plan Agent 复杂制定初始化计划、Execute Agent 负责执行任务、RePlan Agent 负责分析结果并调整计划状态。
然后再来看流程细节:
- 用户提出问题
- Plan Agent 根据问题制定任务计划
- 根据任务列表开始循环
- 每次取出第一个任务,交给 Exectue Agent 执行并记录结果
- Replan 拿到所有任务的执行结果 ,加上当前任务计划 ,判断是否已经解决用户问题
- 未解决,生成新的执行计划(去掉执行完的任务,也可能补充新的任务),继续循环
- 解决了,直接回复用户
三、代码实现
接下来,我们就用这个模式来实现一个新版本的深度搜索。它主要由三个逻辑部分组成。
3.1 Plan
演示
首先是实现一个 Plan 规划器。它的主要任务就是像一个主管一样,先分析用户提出的问题,然后拆分成一步步可以执行的小任务,最后分配给"小弟"们执行。例如,下面我问的是"特斯拉和英伟达,谁的股价更高",它于是给出一个执行计划:
- 搜索特斯拉股价
- 搜索英伟达股价
- 比较两者股价

实现
这个 Plan Agent 的实现很简单,我们只需要定义清楚它的提示词即可。
typescript
import { Block } from '../base/block';
const prompt = `
你是一个研究主管。你的任务是针对给定目标,制定一个简单的分步计划。这些任务会分配给研究助理,所以不要过分细化。
该计划应包含各项独立任务,这些任务若执行正确,就能得出正确答案。请勿添加任何多余步骤。
最后一步的结果须为最终答案。确保每一步都包含所需的全部信息 ------ 不要省略步骤。
当前时间是:${new Date().toLocaleString()}
输出的计划必须是一个 JSON 格式的内容,示例如下:
\`\`\`json
["1.步骤", "2.步骤", "3.步骤"]
\`\`\`
`;
export const planer = new Block({
instruction: prompt,
responseFormat: {
type: 'json_object',
},
name: 'planer',
});
基于现有的Block类(详情见GitHub),创建Agent时,提示词应包括:
- 确定主管身份
- 明确职责:分析问题、拆解任务
- 规定JSON返回格式,便于后续处理
注意:因为我演示的案例使用的是 DeepSeek 的模型,你可以使用设置 json_object 来更加强制的要求的模型必须返回 JSON 的格式。
然后需要实现一个 PlanAndExecuteAgent 的类,来组合这些子 Agent
typescript
import { UserMessage } from '../base/message';
import { planer } from './plan';
export class PlanondExecutorAgent {
async invoke(query: string) {
const planMessage = await planer.invoke([new UserMessage(query)]);
console.log('planMessage', planMessage.content);
// 当前计划
let plans = JSON.parse(planMessage.content) as string[];
// 是否完成
let isComplete = false;
// 最终结果
let finalResult;
// 已执行计划的结果
const pastSteps: string[] = [];
console.table(plans.map((plan, index) => ({ 步骤: index + 1, 计划: plan })));
}
}
一切顺利的话,输入问题后,它应该会返回一个任务的 JSON 数组给你。
3.2 Executer
演示
Executer 执行器负责接收子任务并利用现有工具解决问题。比如,当它接到"获取特斯拉当前股价"的任务时,会通过调用搜索工具来实时查询股价。

最后得出结论

实现
这里我们直接采用 ReAct 的模式来实现执行任务的子 Agent。让它收到任务后,会按照"思考-行动-观察"的模式,解决子任务的问题。
如果你有看过上一篇《教你如何用 JS 实现 Agent 系统(2)------ 开发 ReAct 版本的"深度搜索"》,那你应该很清楚如何实现 ReAct 的 Agent 了,思路就是设定提示词、设定解决这类的问题具体工具。
typescript
import { Block } from '../base/block';
import { Tools } from '../base/tool';
import { thoughtTool } from '../tools/thought';
import { tavilySearchTool } from '../tools/tavily-search';
export const createExecuter = (name: string) => {
return new Block({
instruction: `
你是一个研究助理,你的职责是解决研究主管委派给你的任务。
当前时间是:${new Date().toLocaleString()}
你有如下核心工具:
- thought: 用于思考和决策。注意,在调用 tavilySearch 之前,你必须先调用 thought 分析这么做的原因。在调用 tavilySearch 之后,你必须调用 thought 观察上下文并思考分析后续步骤。
- tavilySearch: 用于搜索互联网
`,
tools: new Tools([thoughtTool, tavilySearchTool]),
name,
debug: false,
});
};
这里我们采用函数调用的方式,这样每次执行一个子任务,就创建一个新的 Agent。工具的话还是复用之前准备的思考工具和查询工具。
组合
设定好 Executer Agent 以后,接下就是让它和 Plan Agent 组合起来工作。思路就是构建一个循环,每一次循环都取出任务列表中的第一个任务执行,并且需要记录执行的结果。
typescript
import { UserMessage } from '../base/message';
import { planer } from './plan';
import { createExecuter } from './executer';
import { getNewPlans } from './replan';
export class PlanondExecutorAgent {
async invoke(query: string) {
const planMessage = await planer.invoke([new UserMessage(query)]);
console.log('planMessage', planMessage.content);
// 当前计划
let plans = JSON.parse(planMessage.content) as string[];
// 是否完成
let isComplete = false;
// 最终结果
let finalResult;
// 已执行计划的结果
const pastSteps: string[] = [];
console.table(plans.map((plan, index) => ({ 步骤: index + 1, 计划: plan })));
// 构建循环
while (plans.length > 0) {
// 取出第一个任务
const step = plans[0];
const prompt = `你的任务是:${step}`;
// 分配任务
console.log(`正在处理任务:${step}`);
const exectuerMessage = await createExecuter('executer').invoke([new UserMessage(prompt)]);
console.log('exectuerMessage', exectuerMessage.content);
// 记录结果
pastSteps.push(exectuerMessage.content);
}
}
}
3.3 RePlan
最后是 RePlan 重规划器。它的主要工作就是分析用户的问题、当前的计划、已经执行的步骤和结果,判断问题是否已经解决了。如果解决了,它会终止循环,直接给出答案;如果没有解决,它会继续循环,并更新任务的状态。
演示
例如,当 "获取特斯拉(Tesla)当前股价" 任务执行完毕后。它会收到问题、计划、执行结果等信息,发现还需完成剩余两步才可以解决用户问题,于是它更新了当前计划的进度,并再次循环。

第二次循环中,Executer Agent 取到的第一个任务是 "获取英伟达(NVIDIA)当前股价",于是它开始查询英伟达股价,并最后结果也记录起来。

现在有了特斯拉和英伟达的股价了,当 RePlan 有了这些信息后,就可以给出最终答案了。

实现
RePlan 的实现跟 Plan 差异不大,本质都是一个纯提示词工程。
typescript
import { Block } from '../base/block';
import { UserMessage } from '../base/message';
const generateReplanPrompt = (input: string, plan: string, past_steps: string) => {
return `
你是一个研究主管。针对给定目标,制定一个简单的分步计划。该计划应包含各项独立任务,这些任务若执行正确,就能得出正确答案。请勿添加任何多余步骤。
最后一步的结果须为最终答案。注意,必须确保每一步都包含所需的全部信息------------不要跳过步骤。
当前时间是:${new Date().toLocaleString()}
你的目标如下:
${input}
你最初的计划如下:
${plan}
你目前已完成以下步骤:
${past_steps}
请据此更新你的计划。若无需再执行其他步骤,且可以向用户反馈最终的结果,则直接回复该结论;若仍需执行步骤,请完善计划内容。仅添加仍需完成的步骤,切勿将已完成的步骤纳入更新后的计划中。
注意,计划必须是一个 JSON 格式的内容,示例如下:
["1.步骤", "2.步骤", "3.步骤"]
注意,如果返回的是最终结果,那么答案也必须是一个 JSON 格式的内容,示例如下:
{"answer": "答案"}
`;
};
export const getNewPlans = async (input: string, plan: string, past_steps: string) => {
const prompt = generateReplanPrompt(input, plan, past_steps);
const replanner = new Block({
instruction: prompt,
name: 'replan',
responseFormat: {
type: 'json_object',
},
});
const res = await replanner.invoke([new UserMessage(prompt)]);
console.log('replan', res.content);
return JSON.parse(res.content);
};
这个提示词核心的逻辑有这三个:
- 确定身份和职责
- 接受目标、计划、已经执行的计划和结果
- 更新计划:一是去掉已执行的任务,二是可能补充新的任务
- 返回内容:判断执行进度。未解决,更新一个新的任务数组;已解决,回复一个包含结果的对象
但是,这里需要重点讲一下更新计划的这个过程。为什么会出现追加任务的场景呢?
这个是 Plan And Execute 这种模式比较有意思的地方。因为一开始的规划,可能会是不完善的,随着子任务执行,上下文信息逐步全面,LLM 就会补充一些新任务来让这个问题解决的更好。本质是模拟人类不断完善方案的一个过程。
组合
接着就是把这几个 Agent 进行一个完整的组成,组成一个真正的 Agentic System。
typescript
import { UserMessage } from '../base/message';
import { planer } from './plan';
import { createExecuter } from './executer';
import { getNewPlans } from './replan';
export class PlanondExecutorAgent {
async invoke(query: string) {
const planMessage = await planer.invoke([new UserMessage(query)]);
console.log('planMessage', planMessage.content);
// 当前计划
let plans = JSON.parse(planMessage.content) as string[];
// 是否完成
let isComplete = false;
// 最终结果
let finalResult;
// 已执行计划的结果
const pastSteps: string[] = [];
console.table(plans.map((plan, index) => ({ 步骤: index + 1, 计划: plan })));
while (plans.length > 0) {
const step = plans[0];
const prompt = `你的任务是:${step}`;
console.log(`正在处理任务:${step}`);
const exectuerMessage = await createExecuter('executer').invoke([new UserMessage(prompt)]);
console.log('exectuerMessage', exectuerMessage.content);
pastSteps.push(exectuerMessage.content);
const plansStr = plans.join('\n');
const pastStepsStr = pastSteps.join('\n');
const result = await getNewPlans(query, plansStr, pastStepsStr);
if (result?.answer) {
isComplete = true;
finalResult = result?.answer;
// 返回结果,终止循环
return finalResult;
} else {
// 更新计划
plans = result as string[];
console.table(plans.map((plan, index) => ({ 步骤: index + 1, 计划: plan })));
}
}
}
}
四、优缺点
4.1 优点
可以发现,相比于 ReAct 模式,Plan And Execute 的模式,具备两个明显的优点:
- 动态规划能力
- 避免上下文限制
ReAct 模式上下文容易撑爆;随着上下文内容增多,LLM 的注意力能力会下降。

Plan And Execute 模式会先规划任务,确定方向,然后把子任务分给不同的 Agent。因为子 Agent 关注的问题更新,所以需要步骤相对更少,上下文就不容易受限制。

4.2 缺点
但是 Plan And Execute 模式任然有他的缺点。这种模式执行任务的效率并不高,哪怕拆分了任务,但是它任然是串行的执行。例如我们上面的例子中,其实查询特斯拉股票和查询英伟达股票,其实是可以并行进行的,因为它们并不是相互依赖的任务。
五、最后
5.1 思考
在上一篇文章中,提出了一个问题,就是如果 LLM 一直不停的调用工具怎么办?其实最好的解决办法就是:
- 告诉 LLM 有限的迭代次
- 在执行工具的时候,统计迭代次数,如果超出了次数,就强制 LLM 直接答复。
然后这次再抛一个问题给大家,既然 Plan And Execute 不能并行执行任务,如果是你,你该怎么设计和优化呢?
5.2 结语
下一遍,我们会继续分享 Agent 的设计模式------如何用 ReWOO 模式实现深度搜索。如果你觉得内容对你有帮助,请关注我,我会持续更新~
最后,关于"深度搜索"实现的完整的代码内容,我都放在这个仓库 github.com/zixingtangm... 了,大家可以直接查看。
原创不易,转载请私信我。