
Deep-Research 多智能体开发是当前 AI Agent 应用里讨论度最高的实战方向之一。它要解决的问题不是“写一个能聊天的机器人”而是让多个拥有独立职责的 Agent 协作完成一次深度调研拆解用户问题、分头检索资料、提取关键信息、交叉验证内容、最终生成带引用来源的报告。许多开发者第一次接触“多智能体”时以为只是把多个 Prompt 放进一个循环里实际写起来才发现任务拆解、工具调用、状态传递、上下文截断、循环终止和结果校验每一个环节都会影响最终质量。这篇文章围绕“从零上手 Deep-Research 多智能体开发”这条主线展开尽量还原一套实战课的核心路径。先解释多智能体系统的核心概念和四种常见交互模式再给出一个可以在本地运行的最小项目包含规划 Agent、检索 Agent 和综述 Agent 三类角色接着分析关键参数、验证方式、常见问题最后补充从实验到生产环境落地的工程化建议。示例代码基于 Python 和 LangGraph 生态但重点讲的是可以迁移到其他框架的设计思路。1. 先理解 Deep-Research 多智能体到底在解决什么问题1.1 单 Agent 做深度调研的四个瓶颈在正式写代码之前建议先想清楚一个问题为什么深度调研需要多智能体而不是一个更强的模型先看单 Agent 直接处理深度调研时会遇到什么。用户提出“2025 年多智能体框架的主要演进方向”这类开放性问题模型需要规划、检索、阅读、综合、引用全在一轮对话里完成。对这个过程做拆解至少存在四个明显瓶颈。第一个是上下文窗口有限。深度调研通常要阅读几十篇甚至上百篇资料把这些内容全部塞进一个模型会话里很快会超出模型上下文限制。即使超出后还能继续模型也会因为信息过载而丢失早期内容导致报告前后矛盾。第二个是任务复杂度高。规划检索策略、判断资料可信度、提炼技术观点、生成结构化报告这些任务对提示词、参数和工具的要求并不相同。把不同能力要求集中到一个 Agent 里往往导致哪个任务都做不精细。第三个是并发效率低。调研的多个子问题之间往往是相对独立的单 Agent 只能串行处理整体耗时随子任务数量线性增长。多 Agent 并行后耗时可以明显下降。第四个是错误定位困难。单 Agent 全权处理时如果最终报告质量差很难判断是检索环节漏了资料还是综合环节理解偏了。多 Agent 把阶段拆开之后每个节点都有输入输出排查粒度可以细化到具体环节。这就是 Deep-Research 类产品采用多智能体架构的核心原因不是为了“用多个 Agent”这个形式而是为了在有限上下文里完成更大规模的信息处理并通过角色分工提升质量和可维护性。1.2 Deep-Research 的经典任务拆解把一次深度调研拆开看通常包含五个阶段。规划把用户问题拆解成若干子问题明确检索方向和范围。检索针对每个子问题从网页、文档、数据库或企业知识库中获取材料。提取从原始材料中提炼关键信息去除广告、导航和无用内容。综合把各子问题的结果整理成有逻辑、有引用、有结论的报告。校验检查报告是否有信息缺口、引用是否真实、结论是否与材料一致。这五个阶段对应到多智能体系统里就是五类 Agent 角色。最小可运行项目可以只保留三个节点规划 Agent、检索 Agent、综述 Agent。提取和校验可以先合并进检索与综述等链路跑通后再单独拆出来。这种“先少后多”的拆法能避免一开始就把状态设计和图编排做得过于复杂。1.3 多智能体的四种常见交互模式多智能体系统的开发者经常讨论“交互模式”简单说就是 Agent 之间以什么方式协作。常见的有四种它们不是互斥关系一个复杂系统里可以同时出现多种模式。交互模式协作方式典型场景优点注意点串行流水线Agent 按固定顺序执行前一个输出是后一个输入规划到检索再到综述流程清晰容易排查链路慢某个节点失败会中断整条流程并行扇出/扇入控制器把任务分给多个 Agent 并行处理再汇总同时检索多个子主题速度快利用多路资源需要设计汇总逻辑注意结果去重层级主从主 Agent 根据情况决定是否调用子 Agent动态规划、按需深挖灵活节省资源主 Agent 的决策质量很关键协商/辩论多个 Agent 对同一问题发表观点并互相质疑观点综合、结果校验能暴露单一视角的盲区成本高容易陷入无结论循环Deep-Research 的典型场景是“串行 并行”的组合规划 Agent 先串行产出子任务检索 Agent 对多个子任务并行扇出最后综述 Agent 汇总。理解这四种模式可以帮助你在设计图结构时判断这个节点之间是顺序依赖还是可以并发又或者需要回退和重试。2. 环境准备与依赖选型2.1 Python 版本与虚拟环境示例项目使用 Python 3.10 及以上版本。新版 LangGraph 对类型注解和异步支持做得更好Python 3.9 虽然也能跑但遇到状态泛型和异步工具时容易踩版本坑。先检查本机 Python 版本然后创建独立虚拟环境。python3 --version python3 -m venv .venv source .venv/bin/activate激活虚拟环境后命令行提示符前会出现(.venv)。这一步的目的是隔离项目依赖避免多个项目之间出现包版本冲突。实际开发中经常遇到“在我电脑上能跑换环境就报错”绝大多数是全局依赖混乱造成的。2.2 核心依赖选型LangGraph 编排、LangChain 模型调用、MCP 接入工具当前做多智能体开发比较主流的技术栈组合如下。包名作用说明langgraph多 Agent 图编排用状态图管理节点、边、状态和条件分支langchain-core基础抽象提供消息、工具、输出解析等通用结构langchain-openaiOpenAI 兼容模型接入可以通过 base_url 切换不同模型服务mcp工具接入协议把外部搜索、数据库、代码执行等工具标准化接入python-dotenv环境变量管理读取 .env 文件避免密钥写进代码LangGraph 与其他方案的差异在于它把多 Agent 协作建模成一张“图”节点是 Agent 或普通函数边是执行顺序整个图共享一份状态对象。这种模型很适合 Deep-Research因为调研流程天然是分阶段、有状态、需要中间结果累积的。MCP 指的是 Model Context Protocol它解决的是“每个 Agent 接入工具都要写一套自定义调用”的问题。通过 MCP搜索服务、数据库、文件系统可以暴露成统一格式的工具Agent 只需要按协议调用即可。下面代码里先用本地假工具跑通再说明如何接入 MCP。2.3 项目目录结构与配置文件最小项目建议按下面的目录组织把 Agent 按角色拆分图编排单独放一个文件入口单独放一个文件。deep_research_lab/ ├── .env ├── requirements.txt ├── agents/ │ ├── __init__.py │ ├── planner.py │ ├── retriever.py │ └── summarizer.py ├── graph.py └── main.pyrequirements.txt内容如下。注意这里的版本号是示例区间安装前先查看当前最新稳定版避免直接复制过期版本。langgraph0.2.0 langchain-core0.3.0 langchain-openai0.2.0 mcp1.0.0 python-dotenv1.0.0.env文件用于保存模型服务的密钥和地址。OPENAI_API_KEYsk-xxx OPENAI_BASE_URLhttps://api.example.com/v1 OPENAI_MODELdeepseek-chat这种写法比较稳妥模型服务地址和模型名称都通过环境变量注入切换服务商时不需要改业务代码。实际项目中密钥应该由配置中心或环境管理平台下发不要提交到 Git 仓库。3. 最小可运行实现规划、检索、综述三个 Agent 协作3.1 定义全局状态在 LangGraph 中全局状态是多个节点共享的数据结构。每个节点从状态里读取自己需要的字段执行完后返回一个字典框架会自动把返回的字段合并回状态。先定义 Deep-Research 需要共享的状态。from typing import Dict, List, TypedDict class ResearchState(TypedDict): question: str # 用户原始问题 sub_tasks: List[str] # 规划 Agent 拆解出的子任务 materials: List[Dict[str, str]] # 检索 Agent 收集的材料 report: str # 综述 Agent 生成的报告 errors: List[str] # 各节点产生的错误信息这里要注意TypedDict的字段是“建议类型”运行时并不会强制校验。真正驱动状态合并的是字典的键。如果某个节点返回了状态里没有声明的键LangGraph 也会把它写入状态只是后续代码可能因为类型不匹配而报错。所以状态字段要提前约定清楚不要随手加新字段。3.2 规划 Agent把问题拆成子任务规划 Agent 的职责是把开放性问题拆成若干可检索的子任务。它只负责“拆”不负责“答”。import json import os from langchain_openai import ChatOpenAI llm ChatOpenAI( base_urlos.getenv(OPENAI_BASE_URL), api_keyos.getenv(OPENAI_API_KEY), modelos.getenv(OPENAI_MODEL, deepseek-chat), temperature0.2, timeout60, ) def plan_node(state: ResearchState) - dict: prompt \n.join([ 你是一名研究规划 Agent。, f用户问题{state[question]}, 请把问题拆成 3 到 5 个相互独立、便于检索的子任务。, 只输出 JSON 数组不要输出任何解释文字。, 示例[\子任务一\, \子任务二\], ]) resp llm.invoke(prompt) try: sub_tasks json.loads(resp.content) sub_tasks [str(t) for t in sub_tasks] if not sub_tasks: sub_tasks [state[question]] except json.JSONDecodeError: sub_tasks [state[question]] return {sub_tasks: sub_tasks}这里有两个关键设计。第一个是设置temperature0.2规划任务要求稳定输出随机性不能太高。第二个是 JSON 解析要做兜底模型偶尔会输出带解释前缀的内容解析失败时直接把原问题当成唯一子任务保证流程继续。实际项目中这种解析兜底非常重要因为模型输出格式不会 100% 稳定。3.3 检索 Agent获取每个子任务的资料为了让读者在没有搜索 API 的情况下也能运行整个项目先实现一个本地假搜索工具用来验证链路。class StubSearchTool: 本地联调用的假搜索工具接入真实搜索服务前先用它跑通流程。 def invoke(self, query: str, max_results: int 5) - List[Dict[str, str]]: return [ { title: f{query} - 示例结果 {i 1}, url: fhttps://example.com/{i 1}, snippet: f这是为「{query}」准备的示例摘要用于验证多智能体链路。, } for i in range(max_results) ]检索节点遍历子任务调用搜索工具把结果统一存入materials。search_tool StubSearchTool() def retrieve_node(state: ResearchState) - dict: materials [] for task in state[sub_tasks]: docs search_tool.invoke(querytask, max_results5) for doc in docs: materials.append({ task: task, title: doc[title], url: doc[url], snippet: doc[snippet], }) return {materials: materials}这段代码有两个地方值得注意。第一检索节点返回的是materials的完整新列表而不是只返回新增的几条。因为当前状态里materials初始是空列表直接覆盖是安全的。如果希望多轮检索累积结果就需要在节点内部先读取已有材料再追加。第二真实项目中搜索工具替换起来很简单只要让替换对象也实现invoke(query, max_results)方法返回相同结构即可。常见的选择是 Tavily 搜索接口、SerpAPI或者是企业内部搜索服务。接入真实服务后假工具可以保留在测试代码里用于单元测试和离线验证。3.4 综述 Agent基于材料生成深度报告综述 Agent 读取所有材料把它们拼进提示词要求模型生成带结构、带引用的报告。def summarize_node(state: ResearchState) - dict: material_text \n\n.join( f标题{m[title]}\n链接{m[url]}\n摘要{m[snippet]} for m in state[materials] ) prompt \n.join([ 你是一名综述 Agent。请基于检索材料生成深度调研报告。, f原始问题{state[question]}, 要求, 1. 报告结构清晰使用小标题分节, 2. 关键结论必须引用对应材料链接, 3. 如果材料不足以支撑结论明确写出信息缺口不要编造, 4. 最后给出参考资料列表。, 检索材料如下, material_text, ]) resp llm.invoke(prompt) return {report: resp.content}这里的提示词里明确写了“不要编造”。原因是综述模型在材料不足时倾向于用自己的知识补全内容这在深度调研场景下是严重错误。宁可让报告承认信息不足也不能生成无来源的结论。实际项目中还会在校验节点里做引用一致性检查确保报告中出现的链接确实存在于检索材料集合中。3.5 图编排与运行入口有了三个节点函数接下来把它们组装成一张图。from langgraph.graph import END, START, StateGraph builder StateGraph(ResearchState) builder.add_node(planner, plan_node) builder.add_node(retriever, retrieve_node) builder.add_node(summarizer, summarize_node) builder.add_edge(START, planner) builder.add_edge(planner, retriever) builder.add_edge(retriever, summarizer) builder.add_edge(summarizer, END) app builder.compile()运行入口放在main.py。from dotenv import load_dotenv from graph import app load_dotenv() def main(): # 注意这里需要先加载 .env再执行 app.invoke。 # 因为模型实例是在导入 graph 模块时创建的所以建议初始化逻辑放在模块内部函数中。 question 多智能体系统在企业知识库调研中的应用有哪些 result app.invoke({ question: question, sub_tasks: [], materials: [], report: , errors: [], }) print(result[report]) if __name__ __main__: main()执行命令如下。pip install -r requirements.txt python main.py这里要提醒一个容易遇到的问题ChatOpenAI实例是模块加载时创建的如果load_dotenv()放在导入之后调用环境变量还没有加载模型服务地址会变成空值。稳妥做法是把load_dotenv()放在入口文件最前面或者在模型初始化之前显式加载。这也是很多新手跑不起来项目的第一原因。4. 关键机制与参数调优4.1 状态更新与 Agent 角色边界运行上面代码时LangGraph 会按照planner - retriever - summarizer的顺序执行。每个节点返回的字典会以“局部更新”的方式合并进全局状态。比如plan_node只返回sub_tasks框架会把sub_tasks写入状态其他字段保持不变。这个机制决定了 Agent 角色的边界设计。规划 Agent 不应该去检索资料检索 Agent 不应该去生成最终结论。职责越单一提示词越简单输出越稳定。如果发现某个 Agent 的提示词超过 500 字并且包含多个不相关的任务就应该考虑再拆一个 Agent 出来。4.2 工具注册与 MCP 集成方式上面示例里检索 Agent 直接调用了一个自定义搜索对象。真实项目中工具数量会变多搜索、网页抓取、数据库查询、代码执行、文档解析等。如果每个工具都写一套自定义调用维护成本会很高。MCP 的思路是把工具标准化。检索 Agent 只需要知道工具的名称、参数结构和返回格式就能调用任意已注册的 MCP 工具。下面是一个检索节点接入 MCP 后的示意结构。class MCPRetriever: def __init__(self, session): self.session session def invoke(self, query: str, max_results: int 5): # 不同版本 MCP SDK 的 call_tool 方法签名存在差异以官方文档为准 result self.session.call_tool( web_search, arguments{query: query, count: max_results}, ) return parse_mcp_result(result)这种封装的好处是检索节点内部不关心工具是本地函数还是远程服务只要MCPRetriever提供统一的invoke方法业务流程就不需要改动。学习 MCP 时重点理解三个概念工具声明、调用参数、返回格式。其他细节都可以之后再看。4.3 关键参数说明与调优多智能体系统里真正影响运行结果和成本的参数并不多但每一个都要理解清楚。参数含义建议初始值调大的影响调小的表现适用场景temperature模型采样随机性0.2回答发散引用不稳定回答保守可能重复规划、综述用低值头脑风暴可用低中值timeout单次模型调用超时60 秒容忍慢接口更快暴露故障检索节点可适当调大max_results每个子任务检索条数5材料更全但上下文变大速度快可能漏信息子任务多时适当减小recursion_limit图最大执行步数25允许长链路和重试提前终止节省 token有循环节点时加大recursion_limit是 LangGraph 里的一个保护参数。如果图里存在条件循环比如“综述质量不过关就重新检索”不加限制会导致无限循环。设置一个合理上限既保证循环有空间执行又避免异常情况下调用费失控。4.4 上下文管理与截断策略综述节点把大量材料拼进提示词很容易触发模型上下文上限。常见的解决办法有三种。第一控制单条材料长度。检索结果通常只保留标题、URL 和摘要不把整篇文章塞进去。摘要可以截断到 200 到 300 字。第二按相关度筛选。真实系统中检索接口往往返回很多条结果不能直接全量塞给模型。先做一轮粗筛按标题和摘要相关性排序只保留前 N 条。第三分段综合。如果材料实在太多可以先用“提取 Agent”把每篇材料压缩成要点再用综述 Agent 综合这些要点。这样既能保留关键信息又能控制最终提示词体积。5. 运行验证与结果分析5.1 示例问题与预期输出使用假搜索工具运行时输入以下问题。多智能体系统在企业知识库调研中的应用有哪些正常情况下规划 Agent 会输出一个包含三到五个子任务的 JSON 数组检索 Agent 会为每个子任务生成若干条示例材料综述 Agent 最终输出一份分节报告。报告里会包含“应用场景”“技术架构”“挑战”等小节并列出参考资料。如果看到的是空报告或者报告里没有任何引用说明链路中某个节点没有按预期工作。不要急着改模型先按下面的过程定位问题。5.2 通过流式输出观察中间状态只打印最终报告很难判断每个 Agent 是否正常。调试阶段建议使用app.stream()它会逐节点输出每一步的状态快照。from dotenv import load_dotenv load_dotenv() from graph import app initial_state { question: 多智能体系统在企业知识库调研中的应用有哪些, sub_tasks: [], materials: [], report: , errors: [], } for step in app.stream(initial_state, config{recursion_limit: 25}): for node_name, state in step.items(): print(f--- 节点{node_name} ---) print(state)观察重点有三个。第一个是retriever节点执行后materials列表是否有数据。第二个是planner节点输出了几条子任务是否与问题相关。第三个是summarizer节点的报告内容是否覆盖了所有子任务并且包含链接。5.3 结果质量评估清单多智能体系统的质量评估不能只看“有没有生成报告”。建议从以下维度检查。评估维度检查方式达标标准子任务覆盖度对比规划输出与原始问题没有遗漏明显子主题检索相关性抽查几条材料的标题与摘要内容与对应子任务相关引用真实性报告中的链接是否存在于材料集不存在无来源结论信息缺口处理检查报告是否承认材料不足不编造数据与事实结构完整性是否有分节、小结和参考列表读者能按结构快速定位这个清单可以作为后续开发校验节点的参考标准。生产环境中可以把评估规则交给一个独立的“评审 Agent”它不参与生成只负责对报告打分并指出问题。6. 常见问题与排查路径6.1 模型调用失败或一直超时现象运行python main.py后长时间没有输出最终报超时或连接错误。排查顺序先检查.env是否被正确加载再确认apikey是否有效然后单独执行一次模型调用排除框架问题。python -c import os from dotenv import load_dotenv load_dotenv() print(BASE_URL:, os.getenv(OPENAI_BASE_URL)) print(MODEL:, os.getenv(OPENAI_MODEL)) 如果环境变量打印为空说明.env文件路径不对或者程序里load_dotenv()的执行时机晚于模型初始化。6.2 规划 Agent 输出的 JSON 解析失败现象sub_tasks为空或者内容是一整段解释文字。原因模型在 JSON 前后加了注释或说明文字导致json.loads直接抛异常。解决解析失败时保留原始问题作为唯一子任务同时把原始输出写入errors字段方便事后分析。也可以使用 LangChain 的JsonOutputParser做更强的容错解析。6.3 检索结果为空综述 Agent 开始编造现象报告里有大量内容但没有任何材料链接。原因检索节点没有返回数据同时综述节点的提示词没有强制“材料不足时明确说明”。解决首先在检索节点里对空结果做日志告警其次把综述节点提示词里的“禁止编造”写得更具体最好加一句“如果材料为空请直接输出未检索到可用材料”。6.4 图循环不终止调用费用暴涨现象使用条件边后程序在节点之间反复执行直到超时或费用超限。原因缺少终止条件或终止条件判断依赖的字段没有正确传递。解决给图配置recursion_limit同时在循环节点里增加最大重试次数。比如“综合质量不达标最多重试两次”超过次数后强制进入结束分支。6.5 状态字段丢失现象某个节点读取state[materials]时抛出KeyError。原因前一个节点返回的字典键名与状态声明不一致或者节点在异常分支里没有返回对应字段。解决统一字段命名并在节点开头做防御性读取。推荐写法是用state.get(materials, [])代替直接取下标这样即使字段缺失也不会中断整条流程。问题现象可能原因检查方式解决方案调用超时环境变量未加载、密钥无效单独执行模型调用检查 .env 路径与加载时机输出 JSON 解析失败模型输出带解释文字打印模型原始输出增加容错解析和兜底值报告无引用检索结果为空或提示词约束不足检查 materials 列表长度增加空结果告警强化提示词循环不停缺少终止条件打印每次节点进入日志设置 recursion_limit 与重试上限状态 KeyError字段名不统一检查各节点返回值使用 get 默认值统一字段命名7. 从实验到生产的工程化建议7.1 学习环境与生产环境的差异本地跑通最小 Demo只完成了 20% 的工作。生产环境的多智能体系统要考虑的维度完全不同。维度学习环境生产环境配置写死在代码或 .env配置中心动态下发支持灰度日志print 打印结构化日志链路追踪模型单一模型多模型路由、降级、限流工具本地假工具MCP 标准协议权限隔离数据示例输入敏感信息脱敏访问审计运行直接 invoke异步任务队列超时与重试7.2 可观测性设计多智能体系统比普通 API 服务更难排查因为一次请求会经过多个模型调用和工具调用。生产环境必须做到三点。第一每个节点输出 token 数、耗时和状态字段变更。第二整条链路的唯一 request_id 贯穿所有日志。第三工具调用结果要记录来源 URL 和调用参数这样后续审计报告引用时才有依据。7.3 安全、权限与合规检索工具在本地跑无所谓进入生产后要严格控制工具权限。搜索哪些域名、数据库能查哪些表、代码执行工具要不要开放、敏感词如何过滤这些都需要在工具层做权限隔离不能把全部能力暴露给模型。另外深度调研报告可能引用外部资料生产环境需要在报告中保留原始来源并在展示页面上提示“内容由 AI 生成可能存在偏差”。涉及企业内部数据时还要按数据等级做脱敏处理。7.4 Deep-Research 多智能体落地检查清单把前面所有内容整理成一份可复用的检查清单发布前逐项确认。[ ] 环境变量通过配置中心或 .env 管理密钥不进代码仓库。[ ] 状态字段命名统一所有读取都带默认值处理。[ ] 规划、检索、综述节点职责单一提示词不混用。[ ] 模型输出解析带容错和兜底逻辑。[ ] 检索空结果有日志告警综述提示词明确禁止编造。[ ] 所有循环节点设置了最大重试次数图配置了 recursion_limit。[ ] 每个节点输出结构化日志包含请求 ID、耗时和 token 数。[ ] 工具接入统一走 MCP 协议调用前有权限校验。[ ] 报告引用必须来自材料集输出前做一致性校验。[ ] 生产环境配置了限流、降级、超时和重试机制。多智能体开发真正的难点不在写代码而在把一次复杂调研拆分成边界清晰、状态可控、结果可验证的协作流程。先把最小链路跑通再逐步加入并行、校验、MCP 工具和可观测性这种方式比一开始就设计一个庞大的 Agent 网络可靠得多。如果把这个领域继续深入下一步可以研究 LangGraph 的条件边和子图机制也可以对比 LangChain4j 在 Java 服务里的实现方式。无论选择哪条路线前面这份“先理解任务拆解再设计状态最后补工程能力”的顺序都是通用的。