山竹智阅畅读app
81.75MB · 2025-10-03
大家好,我是唐某人~ 我非常乐意与大家分享我在AI领域的经验和心得。我的目标是帮助更多的前端工程师以及其他开发岗位的朋友,能够以一种轻松易懂的方式掌握AI应用开发的技能。希望我们能一起学习,共同进步!
在《教你如何用 JS 实现 Agent 系统(2)—— 开发 ReAct 版本的“深度搜索”》中,我分享了什么是深度搜索,什么是 ReAct 设计模式以及 ReAct 版本深度搜索的实现思路。
不知道你是否发现,现在的 Cursor 或者 Trae,它们在解决问题前,会先列一个 To-dos 任务清单,然后一步步的处理这些任务。
相比之前的处理方式,这种“先规划再行动”的模式可以让 LLM 对未来要做的事情有一个大体的规划,避免了方向跑偏。同时也可以让用户知道 AI 工具的解决思路,而不是等着一个“黑盒”给你最终的结果。
所以,这篇我们就一起来学习这种“先规划,再行动”的设计模式,然后用这种模式来实现深度搜索。我们的目标是:
在上一篇文章中,我们提到ReAct通过多轮“思考→行动→观察”的循环逐步接近目标。这种方法存在几个问题:
ReAct 模式有如上的缺点,所以后来人们设计了很多改进的方法。今天我们来讲讲其中的一种叫 Plan And Execute 的模式。这个方法受到了 Plan-and-Solve 论文和 Baby-AGI 项目的启发。主要分为三个步骤:
这种模式的好处就是它会先制定一个整体计划,然后把每个具体任务分配给另一个独立的Agent来完成。这样做的好处有两点:
为了让大家对这个流程有一个更直观的理解,我准备了一张流程图供大家参考。
首先,我们从主体来看:
然后再来看流程细节:
接下来,我们就用这个模式来实现一个新版本的深度搜索。它主要由三个逻辑部分组成。
首先是实现一个 Plan 规划器。它的主要任务就是像一个主管一样,先分析用户提出的问题,然后拆分成一步步可以执行的小任务,最后分配给“小弟”们执行。例如,下面我问的是“特斯拉和英伟达,谁的股价更高”,它于是给出一个执行计划:
这个 Plan Agent 的实现很简单,我们只需要定义清楚它的提示词即可。
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时,提示词应包括:
然后需要实现一个 PlanAndExecuteAgent 的类,来组合这些子 Agent
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 数组给你。
Executer 执行器负责接收子任务并利用现有工具解决问题。比如,当它接到“获取特斯拉当前股价”的任务时,会通过调用搜索工具来实时查询股价。
最后得出结论
这里我们直接采用 ReAct 的模式来实现执行任务的子 Agent。让它收到任务后,会按照“思考-行动-观察”的模式,解决子任务的问题。
如果你有看过上一篇《教你如何用 JS 实现 Agent 系统(2)—— 开发 ReAct 版本的“深度搜索”》,那你应该很清楚如何实现 ReAct 的 Agent 了,思路就是设定提示词、设定解决这类的问题具体工具。
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 组合起来工作。思路就是构建一个循环,每一次循环都取出任务列表中的第一个任务执行,并且需要记录执行的结果。
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);
}
}
}
最后是 RePlan 重规划器。它的主要工作就是分析用户的问题、当前的计划、已经执行的步骤和结果,判断问题是否已经解决了。如果解决了,它会终止循环,直接给出答案;如果没有解决,它会继续循环,并更新任务的状态。
例如,当 “获取特斯拉(Tesla)当前股价” 任务执行完毕后。它会收到问题、计划、执行结果等信息,发现还需完成剩余两步才可以解决用户问题,于是它更新了当前计划的进度,并再次循环。
第二次循环中,Executer Agent 取到的第一个任务是 “获取英伟达(NVIDIA)当前股价”,于是它开始查询英伟达股价,并最后结果也记录起来。
现在有了特斯拉和英伟达的股价了,当 RePlan 有了这些信息后,就可以给出最终答案了。
RePlan 的实现跟 Plan 差异不大,本质都是一个纯提示词工程。
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。
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 })));
}
}
}
}
可以发现,相比于 ReAct 模式,Plan And Execute 的模式,具备两个明显的优点:
ReAct 模式上下文容易撑爆;随着上下文内容增多,LLM 的注意力能力会下降。
Plan And Execute 模式会先规划任务,确定方向,然后把子任务分给不同的 Agent。因为子 Agent 关注的问题更新,所以需要步骤相对更少,上下文就不容易受限制。
但是 Plan And Execute 模式任然有他的缺点。这种模式执行任务的效率并不高,哪怕拆分了任务,但是它任然是串行的执行。例如我们上面的例子中,其实查询特斯拉股票和查询英伟达股票,其实是可以并行进行的,因为它们并不是相互依赖的任务。
在上一篇文章中,提出了一个问题,就是如果 LLM 一直不停的调用工具怎么办?其实最好的解决办法就是:
然后这次再抛一个问题给大家,既然 Plan And Execute 不能并行执行任务,如果是你,你该怎么设计和优化呢?
下一遍,我们会继续分享 Agent 的设计模式——如何用 ReWOO 模式实现深度搜索。如果你觉得内容对你有帮助,请关注我,我会持续更新~
最后,关于“深度搜索”实现的完整的代码内容,我都放在这个仓库 github.com/zixingtangm… 了,大家可以直接查看。
原创不易,转载请私信我。