回顾
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
主图这里主要是流程的变化,使用两个子图代替原来具体的子图功能。
Python
# 子图节点
builder.add_node(RESEARCH_AGENT_NODE, research_agent_graph)
builder.add_node(WRITER_AGENT_NODE, writer_agent_graph)
# 边定义
builder.add_edge(START, GENERATE_PLAN_NODE)
# 条件边:人类干预是否认可研究计划
builder.add_conditional_edges(GENERATE_PLAN_NODE, evaluate_plan, [RESEARCH_AGENT_NODE, SEARCH_REPLAN, AWAITING_PLAN_CONFIRMATION])
# 重新生成计划
builder.add_edge(SEARCH_REPLAN, GENERATE_PLAN_NODE)
# 子话题并行搜索
builder.add_edge(RESEARCH_AGENT_NODE, WRITER_AGENT_NODE)
builder.add_edge(WRITER_AGENT_NODE, END)

reasearch_graph

研究子图这块,同样是将原 graph 研究相关代码抽离到这。其实,代码都是原来写好的,只是重新组成了新的 Graph。
Python
_builder = StateGraph(OverallState, context_schema=Configuration)
_builder.add_node(GENERATE_SEARCH_NODE, _generate_search)
_builder.add_node(WEB_SEARCH_NODE, _web_search)
_builder.add_node(CRITIQUE_NODE, _critique)
_builder.add_edge(START, GENERATE_SEARCH_NODE)
_builder.add_conditional_edges(GENERATE_SEARCH_NODE, _fan_out_to_web_search, [WEB_SEARCH_NODE])
_builder.add_edge(WEB_SEARCH_NODE, CRITIQUE_NODE)
_builder.add_conditional_edges(CRITIQUE_NODE, _route_after_critique, [WEB_SEARCH_NODE, END])
research_agent_graph = _builder.compile(name=SUB_RESEARCH_AGENT)
writer_graph

报告生成这块我们在原来的基础上做了进一步的细化:
- 原流程:搜索内容 → 报告生成
- 优化流程:搜索内容 → 提纲撰写 → 草稿撰写 → 草稿评审 → 报告生成 这样,让我们的报告撰写阶段更加专业。
outline
Python
def _outline(state: OverallState, config: RunnableConfig) -> dict:
"""
根据研究主题和计划,生成大纲
:param state:
:param config:
:return:
"""
configuration = Configuration.runnable_config(config)
reasoning_model = state.get("reasoning_model") or configuration.answer_model
logger.info(f"[WriterAgent] outline using model={reasoning_model}")
agent = Agent(model_id=reasoning_model)
agent.step_prompt(outline_instructions)
raw = agent.step(
research_topic=get_research_topic(state["messages"]),
research_proposal=state.get("plan", ""),
summaries="\n---\n\n".join(state["web_search_result"])
)
outline =- JsonUtils.extract_pattern(raw, pattern="markdown")
logger.info(f"[WriterAgent] outline 已生成 ({len(outline)} 字)")
return {
"report_outline": outline,
"revision_count": 0,
"max_revisions": DEFAULT_MAX_REVISIONS,
}
draft
Python
def _draft(state: OverallState, config: RunnableConfig) -> DraftModel:
"""
根据大纲撰写正文草稿
"""
configurable = Configuration.from_runnable_config(config)
reasoning_model = state.get("reasoning_model") or configurable.answer_model
logger.info(f"[WriterAgent] drafting using model={reasoning_model}")
feedback = state.get("critic_feedback", "")
outline = state.get("report_outline", "")
is_revision = bool(feedback)
revision_count = state.get("revision_count", 0) + (1 if is_revision else 0)
if is_revision:
logger.info(f"[WriterAgent] 修改稿 (revision {revision_count})")
revision_context = (
f"\n# 修订说明 (第 {revision_count} 次修订)\n"
f"请根据以下审稿建议修改草稿:\n\n"
f"{feedback}\n\n"
f"请逐条处理上述问题,优先修复 critical 和 major 级别的问题。"
f"请保留上版草稿中审稿人没有异议的内容。\n"
)
return_update = {"revision_count": revision_count, "critic_feedback": ""}
else:
logger.info(f"[WriterAgent] 从零开始撰写草稿")
revision_context = ""
return_update = {}
agent = Agent(model_id=reasoning_model)
agent.set_step_prompt(draft_instructions)
raw = agent.step(
current_date=get_current_date(),
research_topic=get_research_topic(state["messages"]),
research_proposal=state.get("plan", ""),
outline=outline,
summaries="\n---\n\n".join(state["web_search_result"]),
revision_context=revision_context,
)
draft = JsonUtils.extract_pattern(raw, pattern="markdown")
logger.info(f"[WriterAgent] draft 已生成 ({len(draft)} 字)")
return {**return_update, "report_draft": draft}
review
Python
def _review(state: OverallState, config: RunnableConfig) -> ReviewModel:
"""
评论员审阅草稿并提供结构化反馈。
使用 JsonAgent 和 CritiqueResult 模式生成结构化输出。
"""
configurable = Configuration.from_runnable_config(config)
reasoning_model = state.get("reasoning_model") or configurable.answer_model
logger.info(f"[WriterAgent] critic reviewing draft using model={reasoning_model}")
draft = state.get("report_draft", "")
agent = JsonAgent(model_id=reasoning_model, keys=ReviewModel)
agent.set_step_prompt(review_instructions)
result: ReviewModel = agent.step(
research_topic=get_research_topic(state["messages"]),
research_proposal=state.get("plan", ""),
summaries="\n---\n\n".join(state["web_search_result"]),
draft=draft,
)
# 针对修改稿的点评反馈
if result.issues:
issues_text = "\n".join(
f"- [{iss.severity.upper()}] {iss.location}: {iss.problem}\n"
f" 建议: {iss.suggestion}"
for iss in result.issues
)
else:
issues_text = "无明显问题。"
feedback = (
f"## 审稿评分: {result.overall_rating}/10\n"
f"## 综合评价: {result.summary}\n\n"
f"## 具体问题:\n{issues_text}"
)
logger.info(
f"[WriterAgent] 审稿评分={result.overall_rating}/10, "
f"issues={len(result.issues)} "
f"(critical={sum(1 for i in result.issues if i.severity == 'critical')}, "
f"主要的={sum(1 for i in result.issues if i.severity == 'major')}, "
f"次要的={sum(1 for i in result.issues if i.severity == 'minor')}), "
f"准备润色={result.ready_for_polish}"
)
return {
"critic_feedback": feedback,
"critic_score": result.overall_rating,
"ready_for_polish": result.ready_for_polish,
}
after_review
python
def _route_after_review(state: OverallState, config: RunnableConfig) -> str:
"""
决定:继续修改或进入终审润色。
进入润色的条件:
- Critic 明确标记 ready_for_polish,或
- revision_count >= max_revisions(安全兜底)
否则回到 draft 继续修改。
"""
# 获取当前状态
revision = state.get("revision_count", 0)
max_rev = state.get("max_revisions", DEFAULT_MAX_REVISIONS)
ready = state.get("ready_for_polish", False)
if ready:
logger.info(f"[WriterAgent] Critic ready_for_polish → polish")
return _POLISHNODE
if revision >= max_rev:
logger.info(f"[WriterAgent] 已达到最大修改次数 ({revision}/{max_rev}) → polish")
return _POLISHNODE
logger.info(f"[WriterAgent] 需要重新撰写草稿 (rev={revision}/{max_rev}) → draft")
return _DRAFTNODE
polish
css
def _polish(state: OverallState, config: RunnableConfig) -> PolishModel:
"""
将短链接替换为真实链接,删除重复来源,润色语言。
"""
configurable = Configuration.from_runnable_config(config)
reasoning_model = state.get("reasoning_model") or configurable.answer_model
logger.info(f"[WriterAgent] polishing using model={reasoning_model}")
draft = state.get("report_draft", "")
# Step A --- LLM polish pass
agent = Agent(model_id=reasoning_model)
agent.set_step_prompt(polish_instructions)
raw = agent.step(
research_topic=get_research_topic(state["messages"]),
draft=draft,
summaries="\n---\n\n".join(state["web_search_result"]),
)
polished = JsonUtils.extract_pattern(raw, pattern="markdown")
unique_sources = []
for source in state.get("sources_gathered", []):
if source["short_url"] in polished:
polished = polished.replace(source["short_url"], source["value"])
unique_sources.append(source)
logger.info(f"[WriterAgent] 已润色 ({len(polished)} 字), {len(unique_sources)} 个引用来源")
return {
"messages": [AIMessage(content=polished)],
"sources_gathered": unique_sources,
}
compile
Python
# 写大纲
_builder.add_node(_OUTLINENODE, _outline)
# 写草稿
_builder.add_node(_DRAFTNODE, _draft)
# 草稿审核
_builder.add_node(_REVIEWNODE, _review)
# 润色出终稿
_builder.add_node(_POLISHNODE, _polish)
_builder.add_edge(START, _OUTLINENODE)
_builder.add_edge(_OUTLINENODE, _DRAFTNODE)
_builder.add_edge(_DRAFTNODE, _REVIEWNODE)
_builder.add_conditional_edges(_REVIEWNODE, _route_after_review, [_DRAFTNODE, _POLISHNODE])
_builder.add_edge(_POLISHNODE, END)
writer_agent_graph = _builder.compile(name=SUB_WRITER_AGENT)
Prompts
新增了几个节点,当然也需要撰写对应的提示词。这里可以按照之前写 Prompt的方式自行补齐各个提示词。这里以写大纲(outline)为🌰给出示例。
Python
outline_instructions = """# 角色定义
你是一个科研报告架构师。
# 任务说明
基于研究课题和已收集的材料,你需要为最终报告设计一个清晰的章节结构。
# Instruction
- 分析研究课题和已有材料,确定报告应该包含哪些核心章节
- 每个章节是一个二级标题(##),如果内容复杂可以规划到三级标题(###)
- 章节顺序应有逻辑递进:概述 → 分维度分析 → 综合讨论 → 结论/展望
- 控制在 3-6 个主章节,每个主章节下列出 2-3 个子章节
- 思考每个章节应该覆盖的关键信息点
# Output Format
请输出在 ```markdown 和 ``` 之间的纯 markdown 格式的大纲。例如:
\```markdown
## 1. 市场概述
### 1.1 行业背景与现状
### 1.2 市场规模与增长趋势
## 2. 主要玩家分析
### 2.1 领先企业对比
### 2.2 新兴竞争者
...
\```
# 研究课题
{research_topic}
# 研究计划
{research_proposal}
# 已收集的材料摘要
{summaries}
# 输出"""
拓展🤔🤔🤔
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 监控性能,该合并的合并,该扩的扩