教你如何用 JS 实现 Agent 系统(3)—— 借鉴 Cursor 的设计模式实现深度搜索

前言

大家好,我是唐某人~ 我非常乐意与大家分享我在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通过多轮"思考→行动→观察"的循环逐步接近目标。这种方法存在几个问题:

  1. 缺乏整体规划:采用走一步看一步的方式,可能导致推理方向不确定且难以控制,随着步骤增加可能偏离目标。
  2. 效率低下:由于是按顺序执行任务,复杂的问题需要更多的推理步骤,从而增加了完成时间。
  3. 上下文限制:所有工具的结果都会累积到一个模型的上下文中,因为没有拆分和规划,很容易超出上下文容量限制。

2.2 由来和概念

ReAct 模式有如上的缺点,所以后来人们设计了很多改进的方法。今天我们来讲讲其中的一种叫 Plan And Execute 的模式。这个方法受到了 Plan-and-Solve 论文和 Baby-AGI 项目的启发。主要分为三个步骤:

  1. 规划阶段:先制定一个详细的多步骤行动计划。
  2. 执行阶段:按照计划一步步执行,并返回每一步的结果。
  3. 重规划阶段:根据执行结果调整计划。

这种模式的好处就是它会先制定一个整体计划,然后把每个具体任务分配给另一个独立的Agent来完成。这样做的好处有两点:

  1. 有一个总管全局的Agent,确保解决问题的方向不会跑偏。
  2. 每个任务的执行,交给独立的 Agent。这样任务范围小、职责单一、上下文不容易撑爆。

为了让大家对这个流程有一个更直观的理解,我准备了一张流程图供大家参考。

首先,我们从主体来看:

  1. 用户:提问题、发起对话的人
  2. Plan And Exectue Agents:一个由多 Agent 组成的系统。Plan Agent 复杂制定初始化计划、Execute Agent 负责执行任务、RePlan Agent 负责分析结果并调整计划状态。

然后再来看流程细节:

  1. 用户提出问题
  2. Plan Agent 根据问题制定任务计划
  3. 根据任务列表开始循环
  4. 每次取出第一个任务,交给 Exectue Agent 执行并记录结果
  5. Replan 拿到所有任务的执行结果 ,加上当前任务计划判断是否已经解决用户问题
  6. 未解决,生成新的执行计划(去掉执行完的任务,也可能补充新的任务),继续循环
  7. 解决了,直接回复用户

三、代码实现

接下来,我们就用这个模式来实现一个新版本的深度搜索。它主要由三个逻辑部分组成。

3.1 Plan

演示

首先是实现一个 Plan 规划器。它的主要任务就是像一个主管一样,先分析用户提出的问题,然后拆分成一步步可以执行的小任务,最后分配给"小弟"们执行。例如,下面我问的是"特斯拉和英伟达,谁的股价更高",它于是给出一个执行计划:

  1. 搜索特斯拉股价
  2. 搜索英伟达股价
  3. 比较两者股价

实现

这个 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时,提示词应包括:

  1. 确定主管身份
  2. 明确职责:分析问题、拆解任务
  3. 规定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);
};

这个提示词核心的逻辑有这三个:

  1. 确定身份和职责
  2. 接受目标、计划、已经执行的计划和结果
  3. 更新计划:一是去掉已执行的任务,二是可能补充新的任务
  4. 返回内容:判断执行进度。未解决,更新一个新的任务数组;已解决,回复一个包含结果的对象

但是,这里需要重点讲一下更新计划的这个过程。为什么会出现追加任务的场景呢?

这个是 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 一直不停的调用工具怎么办?其实最好的解决办法就是:

  1. 告诉 LLM 有限的迭代次
  2. 在执行工具的时候,统计迭代次数,如果超出了次数,就强制 LLM 直接答复。

然后这次再抛一个问题给大家,既然 Plan And Execute 不能并行执行任务,如果是你,你该怎么设计和优化呢?

5.2 结语

下一遍,我们会继续分享 Agent 的设计模式------如何用 ReWOO 模式实现深度搜索。如果你觉得内容对你有帮助,请关注我,我会持续更新~

最后,关于"深度搜索"实现的完整的代码内容,我都放在这个仓库 github.com/zixingtangm... 了,大家可以直接查看。

原创不易,转载请私信我。

相关推荐
weixin_457340212 小时前
RTX5060 Ti显卡安装cuda版本PyTorch踩坑记录
人工智能·pytorch·python
Stanford_11062 小时前
关于物联网的基础知识(四)——国内有代表性的物联网平台都有哪些?
人工智能·物联网·微信·微信公众平台·twitter·微信开放平台
偶尔贪玩的骑士2 小时前
Machine Learning HW4 report: 语者识别 (Hongyi Lee)
人工智能·深度学习·机器学习·self-attention
看到我请叫我铁锤2 小时前
vue3使用leaflet的时候高亮显示省市区
前端·javascript·vue.js
柯南二号2 小时前
【AI】【Java后端】RAG 实战示例:SpringBoot + 向量检索 + LLM 问答系统
java·人工智能·spring boot
南囝coding2 小时前
Vercel 发布 AI Gateway 神器!可一键访问数百个模型,助力零门槛开发 AI 应用
前端·后端
民乐团扒谱机2 小时前
【微实验】激光测径系列(六)MATLAB 实现 CCD 图像像素与实际距离标定
人工智能·计算机视觉
AI大模型2 小时前
前端学 AI 不用愁!手把手教你用 LangGraph 实现 ReAct 智能体(附完整流程 + 代码)
前端·llm·agent
算家计算2 小时前
阿里最新开源Wan2.2-Animate-14B 本地部署教程:统一双模态框架,MoE架构赋能电影级角色动画与替换
人工智能·开源