DeepResearchSystem 0x04:MAS 进阶

回顾

  1. DeepRearchSystem 0x00:初识
  2. DeepRearchSystem 0x01:Agent 基础
  3. DeepRearchSystem 0x02:Graph 构建

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 化。

  1. 角色拆分:三个核心 Agent
  • 主 Agent(graph): 负责计划
  • 资料搜索 Agent(reasearch_graph):负责 查询-搜索-评估 Loop
  • 报告生成 Agent(writer_graph):负责 提纲 → 草稿 → 终稿
  1. 编排模式:Supervisor + SubGraph 监督者模式
  • Supervisor(监督者) :主图(graph.py),不直接处理复杂的业务逻辑:
    • 状态路由 :根据 plan_status 决定流向 Research 还是 Replan。
    • 子图调度 :调用 research_agent_graphwriter_agent_graph
    • 生命周期管理:管理 HITL 中断和恢复。
  1. 状态管理:中心化共享 + 局部隔离
    • 中心化OverallState 贯穿全程,存储 planweb_search_resultsources_gathered 等全局数据。
    • 局部隔离 :子图内部有自己的细化状态(如 QueryGenerationStateDraftModel),主图不关心子图内部如何实现,只消费其结果。

和单 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,开发到一定阶段才进行的拆分。

  1. 建立基线:根据需求实现,部署单 Agent 处理核心用例,建立准确性、延迟、成本的基线
  2. 识别不足 :单 Agent瓶颈到底是上下文限制?专精知识不足?还是需要并行?
  3. 多 Agent 拆分:针对性扩展,一个瓶颈对应加一个 Agent,增量构建编排
  4. MAS 观测性优化 :跨 Agent 监控性能,该合并的合并,该扩的扩
相关推荐
_Jimmy_5 小时前
Agent 溯源精度提升方案
人工智能·python·langchain
魔术师Grace6 小时前
Vibe Coding:一场由自己坐庄的“必赢赌局”
ai编程
kyriewen7 小时前
AI Agent 9秒删光了生产数据库——我给自己的项目做了5个紧急检查
前端·ai编程·claude
颜酱7 小时前
09 | 重构项目结构
人工智能·python·langchain
做前端的娜娜子7 小时前
前端必看!我把一段"能跑不敢动"的报表代码用 AI 重构成了组件(附完整 Prompt)
前端·ai编程
妙码生花8 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十三):前后端数据验证
后端·go·ai编程
阿里云大数据AI技术8 小时前
DataWorks Data Agent 实战课堂(三):对话式完成数据同步与智能诊断
人工智能·agent
阿里云云原生8 小时前
一个多 Agent 零人工运维系统的设计复盘:4 个 Agent、7 个 Skill 与 9 条工程判断
agent