回顾
DeepRearchSystem 已经具备了 HITL 机制。现在可以说是基本链路已经跑通了,那我们还能做哪些优化呢?
之前我是做移动端开发的,在写代码之前的设计总会提前考虑到一些编程的设计模式和原则,像六大设计原则:
- 单一职责原则
- 开闭原则
- 里氏替换原则
- 接口隔离
- 依赖倒置
- 迪米特法则
这里感觉 单一职责 和 迪米特法则 可以复用。再来回顾下 DeepRearchSystem 的整体流程:
用户输入 → 研究计划 → 查询规划 → 并行搜索 → 反思评估 → 迭代深化 → 报告生成
但是我们所有的功能都集中在了一个 Graph 里,这显然是违背了 单一职责 和 迪米特法则。那我们是否可以像传统类对象一样,将不同的功能分散到 不同的Graph ?
解决
MAS
其实,MAS(Multi-Agent System) 就是解决上述问题的。它是由多个具备自主性、专用能力的 AI 智能体组成的团队,通过协同工作解决复杂问题,每个智能体扮演特定角色,共同致力于实现共同目标。
除了设计原则里的职责分离,完备的 Agent 编排系统需要 MAS 的理由还有哪些?
1. 上下文窗口与注意力瓶颈
当任务需要跨角色、跨工具持续协调时,单 Agent 容易在长时间交互中失去连贯性,难以稳定协调多步行动。把不同职责拆分到不同 Agent,各自维护局部上下文,再由编排层汇总,能显著缓解这一问题。
2. 安全与合规的边界隔离
金融等行业常要求"一个 Agent 准备交易、另一个 Agent 校验交易",通过架构本身强制执行职责分离。这种 "最小权限设计" 能把安全事件的爆炸半径限制在单个 Agent 边界内——这是单 Agent 做不到的。
3. 多团队、多领域的知识边界
当不同团队管理各自的知识领域、需要独立的开发周期和数据源时,解耦的多 Agent 架构让团队能并行开发、独立部署更新,显式接口降低集成风险。
DeepResearch-MAS化
现在,我们把 DeepResearch 实现 MAS 化。
- 角色拆分:三个核心 Agent
- 主 Agent(
graph): 负责计划 - 资料搜索 Agent(
reasearch_graph):负责 查询-搜索-评估 Loop - 报告生成 Agent(
writer_graph):负责 提纲 → 草稿 → 终稿
- 编排模式:Supervisor + SubGraph 监督者模式
- Supervisor(监督者):主图(
graph.py),不直接处理复杂的业务逻辑:- 状态路由:根据
plan_status决定流向 Research 还是 Replan。 - 子图调度:调用
research_agent_graph和writer_agent_graph。 - 生命周期管理:管理 HITL 中断和恢复。
- 状态路由:根据
- 状态管理:中心化共享 + 局部隔离
- 中心化:
OverallState贯穿全程,存储plan、web_search_result、sources_gathered等全局数据。 - 局部隔离:子图内部有自己的细化状态(如
QueryGenerationState、DraftModel),主图不关心子图内部如何实现,只消费其结果。
- 中心化:
和单 Agent对比
| 维度 | MAS 架构(当前设计) | 原 graph(单 Agent 架构) |
|---|---|---|
| 代码组织 | 高内聚低耦合。按业务域拆分为子图,文件物理隔离。 | 强耦合。所有节点逻辑堆积在一个文件中,代码膨胀快。graph 单文件代码达近500行。 |
| 测试策略 | 单元测试友好。research_graph.py 可独立 Mock 测试,无需启动主图。 | 集成测试为主。节点间依赖深,Mock 成本高,常需 E2E 测试。 |
| 复用性 | 极高。ResearchAgent 可被复用于客服问答、竞品分析等场景;WriterAgent 可被复用于邮件撰写。 | 极低。逻辑绑定在 DeepResearch 流程中,难以抽离。 |
| 调试难度 | 可视化友好。LangSmith 中能看到清晰的 Subgraph 层级,Trace 结构清晰。 | 扁平化 Trace。所有节点在一个平面展开,长链条下难以定位问题。 |
Coding
Talk is cheap,show you the code...
graph
主图这里主要是流程的变化,使用两个子图代替原来具体的子图功能。
1# 子图节点 2builder.add_node(RESEARCH_AGENT_NODE, research_agent_graph) 3builder.add_node(WRITER_AGENT_NODE, writer_agent_graph) 4 5 6# 边定义 7builder.add_edge(START, GENERATE_PLAN_NODE) 8# 条件边:人类干预是否认可研究计划 9builder.add_conditional_edges(GENERATE_PLAN_NODE, evaluate_plan, [RESEARCH_AGENT_NODE, SEARCH_REPLAN, AWAITING_PLAN_CONFIRMATION]) 10# 重新生成计划 11builder.add_edge(SEARCH_REPLAN, GENERATE_PLAN_NODE) 12# 子话题并行搜索 13builder.add_edge(RESEARCH_AGENT_NODE, WRITER_AGENT_NODE) 14builder.add_edge(WRITER_AGENT_NODE, END) 15
reasearch_graph
研究子图这块,同样是将原 graph 研究相关代码抽离到这。其实,代码都是原来写好的,只是重新组成了新的 Graph。
1_builder = StateGraph(OverallState, context_schema=Configuration) 2 3_builder.add_node(GENERATE_SEARCH_NODE, _generate_search) 4_builder.add_node(WEB_SEARCH_NODE, _web_search) 5_builder.add_node(CRITIQUE_NODE, _critique) 6 7_builder.add_edge(START, GENERATE_SEARCH_NODE) 8_builder.add_conditional_edges(GENERATE_SEARCH_NODE, _fan_out_to_web_search, [WEB_SEARCH_NODE]) 9_builder.add_edge(WEB_SEARCH_NODE, CRITIQUE_NODE) 10_builder.add_conditional_edges(CRITIQUE_NODE, _route_after_critique, [WEB_SEARCH_NODE, END]) 11 12research_agent_graph = _builder.compile(name=SUB_RESEARCH_AGENT) 13
writer_graph
报告生成这块我们在原来的基础上做了进一步的细化:
- 原流程:搜索内容 → 报告生成
- 优化流程:搜索内容 → 提纲撰写 → 草稿撰写 → 草稿评审 → 报告生成 这样,让我们的报告撰写阶段更加专业。
outline
1def _outline(state: OverallState, config: RunnableConfig) -> dict: 2 """ 3 根据研究主题和计划,生成大纲 4 :param state: 5 :param config: 6 :return: 7 """ 8 9 configuration = Configuration.runnable_config(config) 10 reasoning_model = state.get("reasoning_model") or configuration.answer_model 11 logger.info(f"[WriterAgent] outline using model={reasoning_model}") 12 13 agent = Agent(model_id=reasoning_model) 14 agent.step_prompt(outline_instructions) 15 raw = agent.step( 16 research_topic=get_research_topic(state["messages"]), 17 research_proposal=state.get("plan", ""), 18 summaries="\n---\n\n".join(state["web_search_result"]) 19 ) 20 outline =- JsonUtils.extract_pattern(raw, pattern="markdown") 21 logger.info(f"[WriterAgent] outline 已生成 ({len(outline)} 字)") 22 23 return { 24 "report_outline": outline, 25 "revision_count": 0, 26 "max_revisions": DEFAULT_MAX_REVISIONS, 27 } 28
draft
1def _draft(state: OverallState, config: RunnableConfig) -> DraftModel: 2 """ 3 根据大纲撰写正文草稿 4 """ 5 6 configurable = Configuration.from_runnable_config(config) 7 reasoning_model = state.get("reasoning_model") or configurable.answer_model 8 logger.info(f"[WriterAgent] drafting using model={reasoning_model}") 9 10 feedback = state.get("critic_feedback", "") 11 outline = state.get("report_outline", "") 12 is_revision = bool(feedback) 13 revision_count = state.get("revision_count", 0) + (1 if is_revision else 0) 14 15 if is_revision: 16 logger.info(f"[WriterAgent] 修改稿 (revision {revision_count})") 17 revision_context = ( 18 f"\n# 修订说明 (第 {revision_count} 次修订)\n" 19 f"请根据以下审稿建议修改草稿:\n\n" 20 f"{feedback}\n\n" 21 f"请逐条处理上述问题,优先修复 critical 和 major 级别的问题。" 22 f"请保留上版草稿中审稿人没有异议的内容。\n" 23 ) 24 return_update = {"revision_count": revision_count, "critic_feedback": ""} 25 else: 26 logger.info(f"[WriterAgent] 从零开始撰写草稿") 27 revision_context = "" 28 return_update = {} 29 30 agent = Agent(model_id=reasoning_model) 31 agent.set_step_prompt(draft_instructions) 32 raw = agent.step( 33 current_date=get_current_date(), 34 research_topic=get_research_topic(state["messages"]), 35 research_proposal=state.get("plan", ""), 36 outline=outline, 37 summaries="\n---\n\n".join(state["web_search_result"]), 38 revision_context=revision_context, 39 ) 40 draft = JsonUtils.extract_pattern(raw, pattern="markdown") 41 logger.info(f"[WriterAgent] draft 已生成 ({len(draft)} 字)") 42 43 return {**return_update, "report_draft": draft} 44
review
1def _review(state: OverallState, config: RunnableConfig) -> ReviewModel: 2 """ 3 评论员审阅草稿并提供结构化反馈。 4 使用 JsonAgent 和 CritiqueResult 模式生成结构化输出。 5 """ 6 7 configurable = Configuration.from_runnable_config(config) 8 reasoning_model = state.get("reasoning_model") or configurable.answer_model 9 logger.info(f"[WriterAgent] critic reviewing draft using model={reasoning_model}") 10 11 draft = state.get("report_draft", "") 12 agent = JsonAgent(model_id=reasoning_model, keys=ReviewModel) 13 agent.set_step_prompt(review_instructions) 14 result: ReviewModel = agent.step( 15 research_topic=get_research_topic(state["messages"]), 16 research_proposal=state.get("plan", ""), 17 summaries="\n---\n\n".join(state["web_search_result"]), 18 draft=draft, 19 ) 20 21 # 针对修改稿的点评反馈 22 if result.issues: 23 issues_text = "\n".join( 24 f"- [{iss.severity.upper()}] {iss.location}: {iss.problem}\n" 25 f" 建议: {iss.suggestion}" 26 for iss in result.issues 27 ) 28 else: 29 issues_text = "无明显问题。" 30 31 feedback = ( 32 f"## 审稿评分: {result.overall_rating}/10\n" 33 f"## 综合评价: {result.summary}\n\n" 34 f"## 具体问题:\n{issues_text}" 35 ) 36 37 logger.info( 38 f"[WriterAgent] 审稿评分={result.overall_rating}/10, " 39 f"issues={len(result.issues)} " 40 f"(critical={sum(1 for i in result.issues if i.severity == 'critical')}, " 41 f"主要的={sum(1 for i in result.issues if i.severity == 'major')}, " 42 f"次要的={sum(1 for i in result.issues if i.severity == 'minor')}), " 43 f"准备润色={result.ready_for_polish}" 44 ) 45 46 return { 47 "critic_feedback": feedback, 48 "critic_score": result.overall_rating, 49 "ready_for_polish": result.ready_for_polish, 50 } 51
after_review
1def _route_after_review(state: OverallState, config: RunnableConfig) -> str: 2 """ 3 决定:继续修改或进入终审润色。 4 进入润色的条件: 5 - Critic 明确标记 ready_for_polish,或 6 - revision_count >= max_revisions(安全兜底) 7 否则回到 draft 继续修改。 8 """ 9 10 # 获取当前状态 11 revision = state.get("revision_count", 0) 12 max_rev = state.get("max_revisions", DEFAULT_MAX_REVISIONS) 13 ready = state.get("ready_for_polish", False) 14 15 if ready: 16 logger.info(f"[WriterAgent] Critic ready_for_polish → polish") 17 return _POLISHNODE 18 19 if revision >= max_rev: 20 logger.info(f"[WriterAgent] 已达到最大修改次数 ({revision}/{max_rev}) → polish") 21 return _POLISHNODE 22 23 logger.info(f"[WriterAgent] 需要重新撰写草稿 (rev={revision}/{max_rev}) → draft") 24 return _DRAFTNODE 25
polish
1def _polish(state: OverallState, config: RunnableConfig) -> PolishModel: 2 """ 3 将短链接替换为真实链接,删除重复来源,润色语言。 4 """ 5 configurable = Configuration.from_runnable_config(config) 6 reasoning_model = state.get("reasoning_model") or configurable.answer_model 7 logger.info(f"[WriterAgent] polishing using model={reasoning_model}") 8 9 draft = state.get("report_draft", "") 10 11 # Step A — LLM polish pass 12 agent = Agent(model_id=reasoning_model) 13 agent.set_step_prompt(polish_instructions) 14 raw = agent.step( 15 research_topic=get_research_topic(state["messages"]), 16 draft=draft, 17 summaries="\n---\n\n".join(state["web_search_result"]), 18 ) 19 polished = JsonUtils.extract_pattern(raw, pattern="markdown") 20 21 unique_sources = [] 22 for source in state.get("sources_gathered", []): 23 if source["short_url"] in polished: 24 polished = polished.replace(source["short_url"], source["value"]) 25 unique_sources.append(source) 26 27 logger.info(f"[WriterAgent] 已润色 ({len(polished)} 字), {len(unique_sources)} 个引用来源") 28 return { 29 "messages": [AIMessage(content=polished)], 30 "sources_gathered": unique_sources, 31 } 32
compile
1# 写大纲 2_builder.add_node(_OUTLINENODE, _outline) 3# 写草稿 4_builder.add_node(_DRAFTNODE, _draft) 5# 草稿审核 6_builder.add_node(_REVIEWNODE, _review) 7# 润色出终稿 8_builder.add_node(_POLISHNODE, _polish) 9 10_builder.add_edge(START, _OUTLINENODE) 11_builder.add_edge(_OUTLINENODE, _DRAFTNODE) 12_builder.add_edge(_DRAFTNODE, _REVIEWNODE) 13_builder.add_conditional_edges(_REVIEWNODE, _route_after_review, [_DRAFTNODE, _POLISHNODE]) 14_builder.add_edge(_POLISHNODE, END) 15 16writer_agent_graph = _builder.compile(name=SUB_WRITER_AGENT) 17
Prompts
新增了几个节点,当然也需要撰写对应的提示词。这里可以按照之前写 Prompt的方式自行补齐各个提示词。这里以写大纲(outline)为🌰给出示例。
1outline_instructions = """# 角色定义 2你是一个科研报告架构师。 3# 任务说明 4基于研究课题和已收集的材料,你需要为最终报告设计一个清晰的章节结构。 5 6# Instruction 7- 分析研究课题和已有材料,确定报告应该包含哪些核心章节 8- 每个章节是一个二级标题(##),如果内容复杂可以规划到三级标题(###) 9- 章节顺序应有逻辑递进:概述 → 分维度分析 → 综合讨论 → 结论/展望 10- 控制在 3-6 个主章节,每个主章节下列出 2-3 个子章节 11- 思考每个章节应该覆盖的关键信息点 12 13# Output Format 14请输出在 ```markdown 和 ``` 之间的纯 markdown 格式的大纲。例如: 15 16\```markdown 17## 1. 市场概述 18### 1.1 行业背景与现状 19### 1.2 市场规模与增长趋势 20 21## 2. 主要玩家分析 22### 2.1 领先企业对比 23### 2.2 新兴竞争者 24... 25\``` 26 27# 研究课题 28{research_topic} 29 30# 研究计划 31{research_proposal} 32 33# 已收集的材料摘要 34{summaries} 35 36# 输出""" 37
拓展🤔🤔🤔
MAS 历史
其实, MAS 并不是一个新概念,为什么之前没有得到广泛应用。大家搞 LLM 特别是做 Agent 的一定对论文:《Why Do Multi-Agent LLM Systems Fail?》不陌生,论文指出了 MAS 14种失败模式和3大致命陷阱。
| 类别 | 典型失败模式 |
|---|---|
| 系统设计问题 | 智能体职责定义不清、编排拓扑不匹配任务、缺少退出条件 |
| 智能体间失对齐 | 重复作业、相互冲突的变更、信息传递失真 |
| 任务验证问题 | 缺少终止判断、错误在协作中传播、无有效回溯 |
MAS 是美好的,论文结论是扎心的😭😭😭:多 Agent 和 Single Agent相比带来的提效,常常被多 Agent之间的协调开销所抵消。而且,单纯堆 Agent 数量,性能提升很快饱和。
MAS 还能用吗?
答案当然是肯定的啦,不然今天的活不就白干了嘛。我个人的理解是用一定能用,但是需要一些手段去克服以上论文聊到的问题。而且很大部分不是技术角度,而是从工程实践去克服。
- 协调与冲突 → 统一编排层(中央"指挥"智能体或结构化层级)负责路由、强制共享上下文、解决冲突。采用 Supervisor + SubGraph、Orchestrator + Worker 等模式
- 可观测性黑洞 → 为 Agent 每一个操作步骤实施监测、追踪与评估,引入护栏智能体监控中间状态,端到端决策日志记录
- 安全治理 → 专用安全与策略智能体 + 沙盒化 + 统一治理框架
怎么选型
其实,我的理解就一句话:能用 Single Agent 就使用Single Agent;要使用 MAS,就必须说清楚为什么。 以下是参考 Microsoft Learn-Single agent or multiple agents 总结的选型考量。
Single Agent
- 低复杂度任务:任务复杂度低、可分解性有限、统一领域知识
- 协调开销敏感:低协调开销、资源受限
- MVP 验证阶段:先用单 Agent 建立性能基线,度量它到底在哪失败,再决定要不要拆
MAS
- 任务本身需要:可分解为独立子任务、需多样领域专长、受益于并行处理
- 未来增长规划:解决方案多样化功能、数据源或业务单元,预期超出 3-5
- 涉及多模块/多团队:不同模块/团队管理独立知识领域,需要解耦的独立开发周期
- 跨安全与合规边界:法规或政策要求严格的数据隔离,单 Agent 无法提供独立处理环境
SOP
实际工程实践中,往往是 “先单后拆,从单到多” 的步骤,就像我们 DeepRearchSystem 最开始也是单 Agent,开发到一定阶段才进行的拆分。
- 建立基线:根据需求实现,部署单 Agent 处理核心用例,建立准确性、延迟、成本的基线
- 识别不足:单 Agent瓶颈到底是上下文限制?专精知识不足?还是需要并行?
- 多 Agent 拆分:针对性扩展,一个瓶颈对应加一个 Agent,增量构建编排
- MAS 观测性优化 :跨 Agent 监控性能,该合并的合并,该扩的扩
《DeepResearchSystem 0x04:MAS 进阶》 是转载文章,点击查看原文。