找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4623

积分

0

好友

639

主题
发表于 6 小时前 | 查看: 5| 回复: 0

上周有位同学复盘他在字节跳动的大模型面试经历,面试官提的几个问题让他印象非常深刻,也直接点出了很多人在设计 Multi-Agent 系统时的常见误区。

面试官问:“你简历写了做过 Multi-Agent 系统,讲一下你们的 Supervisor 是怎么分配任务的?”

他答:“就是 Supervisor 把任务拆分成几个子任务,分别交给不同的 Agent 去做。”

面试官点头,继续追:“那子 Agent 挂了怎么处理?比如网络超时,或者 LLM 调用失败?”

他答:“……会重试。”

面试官又追:“重试几次?等待多久?重试还失败了整个任务就失败了吗?”

他开始语塞。

面试官最后一个问题:“三个 Agent 同时在分析同一家公司,ResearchAgent 说这家公司营收增长,AnalysisAgent 说财务数据显示营收下滑,两个结论矛盾,WriterAgent 写报告的时候怎么合并?”

沉默。

面试官放下笔,说了一句话:“你说的 Multi-Agent,只是把几个 LLM 调用摆在一起,不是真正的 Multi-Agent 系统。”

这句话值得我们认真思考。今天,我们就来把 Multi-Agent 从设计模式到工程实现全部拆开讲清楚。

为什么需要 Multi-Agent——单 Agent 的根本局限

在深入 Multi-Agent 之前,我们必须诚实地承认:单 Agent 不是万能的

以一个真实的银行对公客户智能咨询助手项目“拓业智询”为例。任务是这样的:客户经理输入一家中小企业的名称,系统需要生成一份完整的贷款风险评估报告,内容涵盖企业工商信息、财务状况、行业竞争格局、宏观市场趋势、历史贷款记录这五个维度。

最初尝试使用单 Agent 实现:一个 ReAct Agent,配上搜索工具、SQL 查询工具、财务数据接口,让它自己规划步骤,顺序执行。但这很快就暴露了三个根本性的问题:

第一个问题:上下文过长,精度下降。

顺序执行五个维度的分析,每个维度检索出来的内容都要塞进上下文。分析到第四个维度时,上下文长度常常超过 30000 token。在这种超长上下文中,LLM 开始出现“中间遗忘”现象:报告的前两个维度分析得很详细,后两个维度的分析却越来越粗糙,甚至出现前后矛盾的结论。这并非模型能力不足,而是长上下文本身的注意力分散问题,学术上称之为“lost in the middle”现象。

第二个问题:任务天然可以并行,但单 Agent 只能顺序执行。

企业财务分析、竞争对手分析、市场趋势分析,这三件事之间没有强依赖关系,完全可以同时进行。但单 Agent 的 ReAct 循环是顺序的:先做 A,再做 B,再做 C。五个维度串行下来,整个任务耗时约 45 秒,用户体验极差。

第三个问题:一个 Agent 很难在所有任务上都表现最优。

检索任务需要 Agent 懂得如何拆解查询词、构造召回策略;数据分析任务需要 Agent 理解财务指标、识别异常;报告生成任务则需要 Agent 具备良好的文字组织能力和结构化输出能力。这三种能力在 Prompt 层面的要求是不同的,甚至可能相互冲突——一个 Prompt 很难同时把三件事都优化到极致。

因此,Multi-Agent 的出发点变得非常清晰:分工、并行、独立上下文

Supervisor 模式:Multi-Agent 的核心架构

Multi-Agent 有多种架构模式,但在生产环境中应用最广泛的是 Supervisor 模式

其基本结构是:一个 Supervisor Agent 负责任务分解和全局协调,多个子 Agent 各司其职执行具体任务,最后由一个 WriterAgent(或类似的聚合器)汇总生成最终结果。

Multi-Agent Supervisor 模式架构图

在“拓业智询”项目中,我们使用 LangGraph 实现了这个架构。首先,来看核心的状态定义和 Supervisor 节点:

from langgraph.graph import StateGraph, END
from typing import TypedDict, Literal

class SupervisorState(TypedDict):
    task: str                    # 总任务
    subtasks: list[str]          # 分解的子任务
    agent_results: dict          # 各Agent的结果
    final_report: str            # 最终报告

def supervisor_node(state: SupervisorState) -> SupervisorState:
    """Supervisor:任务分解与分配"""
    task = state["task"]
    # LLM分解任务为子任务
    subtasks = decompose_task(task)
    # 返回决策:下一步执行哪个Agent
    return {**state, "subtasks": subtasks}

def route_to_agents(state: SupervisorState) -> Literal["research", "analysis", "writer", "end"]:
    """根据状态决定下一个执行的Agent"""
    if not state.get("agent_results", {}).get("research"):
        return "research"
    elif not state.get("agent_results", {}).get("analysis"):
        return "analysis"
    elif not state.get("final_report"):
        return "writer"
    else:
        return "end"

这里有几个关键的设计决策:

  • 为什么用 TypedDict 定义状态? LangGraph 的核心是围绕状态流转来设计工作流的。所有节点(Agent)共享同一个状态对象,每个节点读取自己需要的字段,更新自己负责的字段,然后把更新后的状态传递给下一个节点。使用 TypedDict 的好处在于类型检查(开发阶段就能发现字段拼写错误)和自文档化(State 的结构本身就是一份清晰的文档)。
  • route_to_agents 函数的作用是什么? 这是 LangGraph 中的“条件边”。工作流通过这个路由函数来决定下一步该走哪条分支。它的返回值对应该流向的下游节点名称。这样,Supervisor 就实现了动态调度:根据当前状态(哪些 Agent 已经完成了任务),决定下一步执行哪个 Agent。

完整的 Graph 构建如下:

def build_supervisor_graph():
    graph = StateGraph(SupervisorState)

    # 添加所有节点
    graph.add_node("supervisor", supervisor_node)
    graph.add_node("research", research_node)
    graph.add_node("analysis", analysis_node)
    graph.add_node("writer", writer_node)

    # 设置入口
    graph.set_entry_point("supervisor")

    # Supervisor根据状态路由到子Agent
    graph.add_conditional_edges(
        "supervisor",
        route_to_agents,
        {
            "research": "research",
            "analysis": "analysis",
            "writer": "writer",
            "end": END
        }
    )

    # 子Agent执行完毕后回到Supervisor进行下一步决策
    graph.add_edge("research", "supervisor")
    graph.add_edge("analysis", "supervisor")
    graph.add_edge("writer", "supervisor")

    return graph.compile()

请注意这里的结构:每个子 Agent 执行完毕后,都会返回 Supervisor,由 Supervisor 再次判断下一步动作。 这正是 Supervisor 模式的核心——决策权始终在 Supervisor 手中,子 Agent 只负责执行,不负责决定下一步做什么。

并行执行:让 ResearchAgent 同时检索多个维度

既然知道了顺序执行的弊端,接下来看并行执行的实现。

在“拓业智询”的场景中,对一家企业的贷款风险评估,ResearchAgent 需要同时检索三个维度:企业工商信息、行业竞争格局、宏观政策环境。这三个检索任务之间没有依赖,天然适合并行。

import asyncio

async def parallel_research(queries: list[str]) -> list[dict]:
    """并行执行多个Research子任务"""
    tasks = [research_agent.ainvoke({"query": q}) for q in queries]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    # 处理失败的子任务
    final_results = []
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            print(f"子任务{i}失败: {result},使用降级方案")
            final_results.append({"query": queries[i], "result": "检索失败,跳过此部分"})
        else:
            final_results.append(result)
    return final_results

这里有两点值得单独说明:

  • *`asyncio.gather(tasks, return_exceptions=True)的用法。** 如果不加return_exceptions=True,任何一个子任务抛出异常,gather会立刻终止所有子任务并把异常向上抛出。这在生产环境中是不可接受的——一个子任务的失败不应导致所有任务失败。加上该参数后,异常会被当作正常返回值放进results列表,我们可以在后续逐一检查每个结果是否为Exception` 实例,并分别处理。
  • ainvoke 而不是 invoke LangGraph 和 LangChain 的 Agent 都提供了异步调用接口 ainvoke。只有使用异步接口,asyncio.gather 才能真正并发地执行多个任务。如果使用同步的 invoke,即使放入 gather,实际上仍是顺序执行。

实测数据显示,在该项目中,三个检索维度的任务,顺序执行约需 24 秒,并行执行约需 9 秒,提速约 2.7 倍(并非 3 倍,因为有协调开销和资源竞争)。

解决了速度问题,接下来是另一个关键:子 Agent 失败时,整个流程该如何处理?

子 Agent 错误处理:重试 + 降级,缺一不可

这是面试官追问的第二个核心问题。很多人的回答停留在“失败了就重试”,但重试策略本身也需要精心设计。

我们的错误处理策略分为两层:重试降级

子Agent错误处理策略流程图

完整实现如下:

import asyncio
from typing import Optional

async def execute_agent_with_retry(
    agent,
    input_data: dict,
    agent_name: str,
    max_retries: int = 2,
    retry_delay: float = 1.0
) -> dict:
    """
    带重试和降级的Agent执行器
    - max_retries: 最多重试次数(不含第一次执行)
    - retry_delay: 每次重试前等待的秒数
    """
    last_exception: Optional[Exception] = None

    for attempt in range(max_retries + 1):  # 0, 1, 2 共三次机会
        try:
            result = await agent.ainvoke(input_data)
            return {"status": "success", "data": result, "agent": agent_name}
        except Exception as e:
            last_exception = e
            if attempt < max_retries:
                print(f"[{agent_name}] 第{attempt + 1}次失败: {e},{retry_delay}秒后重试...")
                await asyncio.sleep(retry_delay)
            else:
                print(f"[{agent_name}] 已重试{max_retries}次,全部失败,触发降级")

    # 降级处理:标记数据缺失,不抛出异常,允许流程继续
    return {
        "status": "degraded",
        "data": None,
        "agent": agent_name,
        "error": str(last_exception),
        "message": f"{agent_name}数据获取失败,该部分分析结果缺失"
    }

在 Supervisor 层,需要能够感知哪些 Agent 发生了降级:

def supervisor_node(state: SupervisorState) -> SupervisorState:
    agent_results = state.get("agent_results", {})

    # 检查是否有降级的Agent
    degraded_agents = [
        name for name, result in agent_results.items()
        if isinstance(result, dict) and result.get("status") == "degraded"
    ]

    if degraded_agents:
        print(f"警告:以下Agent数据缺失,将继续生成报告但需标注: {degraded_agents}")
        # 在state中记录缺失信息,WriterAgent生成报告时需要标注
        return {**state, "data_gaps": degraded_agents}

    return state

这里最重要的设计原则是:一个子 Agent 的失败,绝不能阻塞整个任务。在银行的业务场景中,宁可生成一份带有“部分数据缺失”标注的报告,也不能让整个系统因为某一个数据源超时而完全报错。客户经理拿到有标注的报告,至少还能参考其他维度做出判断;若只拿到一个系统错误,则什么都做不了。

这也是降级策略的业务逻辑依据——降级不是技术妥协,而是对用户体验的必要保护

错误处理有了兜底方案,还有一个更棘手的问题:多个 Agent 并行分析得出了相互矛盾的结论,WriterAgent 该如何处理?

结果合并:WriterAgent 怎么处理矛盾的结论

这是面试官的最后一个问题,也是最容易被忽视的工程细节。

三个 Agent 并行分析同一家企业,完全可能得出矛盾的结论。在“拓业智询”中确实出现过:ResearchAgent 通过新闻检索发现某企业最近签了几个大合同,判断营收向好;AnalysisAgent 通过分析企业提交的财务报表,发现应收账款周转率下降,实际现金流紧张。两个结论方向相反。

WriterAgent 的设计必须能处理这类矛盾。核心代码如下:

def writer_node(state: SupervisorState) -> SupervisorState:
    """WriterAgent:合并各Agent结果生成最终报告"""
    research_result = state["agent_results"].get("research", {})
    analysis_result = state["agent_results"].get("analysis", {})
    data_gaps = state.get("data_gaps", [])

    # 构建降级说明
    gap_notice = ""
    if data_gaps:
        gap_notice = f"\n\n注意:以下分析模块数据缺失,相关结论请谨慎参考:{', '.join(data_gaps)}"

    merge_prompt = f"""
    你是报告生成专家。请基于以下各分析模块的结果,生成一份结构化的综合报告:

    ## 研究发现
    {research_result.get('data', '数据缺失')}

    ## 数据分析
    {analysis_result.get('data', '数据缺失')}
    {gap_notice}

    要求:
    1. 合并重复内容,保留关键信息
    2. 如有数据矛盾,优先采用数据分析模块的结论(财务数据比新闻检索更客观)
    3. 对矛盾点必须明确标注:“研究发现与数据分析存在分歧,建议进一步核实”
    4. 生成执行摘要(3-5条核心结论)
    5. 如有数据缺失,在对应章节标注“[数据缺失,仅供参考]”
    """
    final_report = llm.invoke(merge_prompt).content
    return {**state, "final_report": final_report}

这里有一个关键的业务规则:数据分析模块的结论优先于检索模块。这是我们基于具体业务场景制定的优先级——企业提交的财务报表(数据分析的来源)比公开新闻(检索的来源)更具法律效力和客观性,在贷款风险评估场景下应赋予更高权重。

当然,不同业务场景下的优先级规则可能不同。关键在于:优先级规则必须显式地写入 WriterAgent 的 Prompt 中,而不能让 LLM 自行决定。让 LLM 自行决定意味着每次合并的结果可能不一致,这在金融等严谨场景中是不可接受的。

架构细节讲完了,让我们用真实数据来看一下单 Agent 和 Multi-Agent 的差距究竟有多大。

单 Agent vs Multi-Agent:拓业智询的真实数据

单Agent与Multi-Agent性能对比表格

在“拓业智询”项目中,我们对同一批 50 个企业评估任务做了 A/B 对比测试,结果如下:

  • 总耗时:单 Agent 顺序执行平均 45 秒,Multi-Agent 并行执行平均 18 秒,提速 60%。
  • 上下文长度:单 Agent 到后期分析维度时上下文超过 32000 token,Multi-Agent 各子 Agent 保持独立上下文,最长不超过 8000 token。
  • 报告质量评分:邀请 5 名有经验的客户经理对报告进行盲评(1-100 分),单 Agent 平均 72 分,Multi-Agent 平均 89 分,提升 23.6%。
  • 适用场景:单 Agent 在简单的单维度查询任务(如“查一下这家公司的注册资本”)上反而更快,Multi-Agent 的优势在多维度复杂分析任务中才能充分体现。

但并行不是越多越好。 这一点值得单独强调。

实验中,我们尝试了 2、3、4、5、6 个子 Agent 并行的方案:

  • 2-3 个子 Agent 并行:性能提升最明显,协调开销小。
  • 4-5 个子 Agent 并行:性能提升趋于平缓,开始出现 LLM API 的并发限速问题。
  • 6 个子 Agent 并行:总耗时反而比 3 个并行的方案更长,原因是 API 限速导致多个请求排队等待。

另外,任务有强顺序依赖时,不能并行。在“拓业智询”中,WriterAgent 必须在所有分析 Agent 完成之后才能运行,因为它需要所有分析结果作为输入——这是典型的顺序依赖,强行并行没有意义。

实战建议:2-3 个子 Agent 并行是大多数场景下的最佳平衡点,既能获得显著的提速效果,又不会因协调开销和 API 限速把收益全部抵消。

面试怎么答 Multi-Agent 架构?

这是本文最实用的部分。当面试官问“你们的 Multi-Agent 架构是怎么设计的”时,不要一上来就讲技术细节。应该先给出一个整体框架,再按层次展开:

第一层:为什么用 Multi-Agent(动机)
“我们做的是银行对公客户风险评估,单 Agent 顺序分析五个维度,上下文过长导致精度下降,且45秒的响应时间用户体验很差。Multi-Agent 让各维度分析并行执行,各 Agent 保持独立上下文,总耗时降到 18 秒。”

第二层:架构是什么样的(设计)
“我们用 LangGraph 实现 Supervisor 模式。Supervisor 负责任务分解和状态管理,三个子 Agent(ResearchAgent、AnalysisAgent、SQLAgent)并行执行,最后由 WriterAgent 汇总生成报告。每个子 Agent 执行完回到 Supervisor,由 Supervisor 决定下一步——决策权始终在 Supervisor 手里。”

第三层:错误处理怎么做的(健壮性)
“每个子 Agent 有独立的重试机制:失败后最多重试 2 次,每次等待 1 秒。重试全部失败则触发降级——标记该模块数据缺失,继续执行其他 Agent。WriterAgent 生成报告时会在缺失部分标注说明。核心原则是一个子 Agent 的失败不能阻塞整个任务。”

第四层:结果合并怎么处理矛盾(细节)
“如果检索结果和数据分析结论矛盾,我们规定数据分析模块优先,因为财务报表比公开新闻更具法律效力。矛盾点会在报告中明确标注,建议人工核实。这个优先级规则显式地写在 WriterAgent 的 Prompt 里,不让 LLM 自行决定,以保证结果的一致性。”

第五层:有什么局限(自我批判,加分项)
“并行不是越多越好。我们测试发现超过 5 个子 Agent 并行时,因 API 限速,总耗时反而比 3 个并行更长。另外,强顺序依赖的任务不能并行,WriterAgent 必须等所有分析完成才能运行。实战下来,2-3 个子 Agent 并行是最佳平衡点。”

能清晰地说到第五层,这道面试题基本就稳了。大多数候选人只能说到第一或第二层,能深入阐述错误处理和结果合并细节的,面试官会认为你真正在生产环境里跑过这个系统,而不只是看过几篇论文。

总结

Multi-Agent 远非简单地把几个 LLM 调用摆在一起。它需要系统性地解决一系列工程问题:任务如何分解、子任务如何调度、子 Agent 失败如何处理、并行结果如何合并、矛盾结论如何裁定。

Supervisor 模式是生产环境中最主流的 Multi-Agent 架构:一个中心化的 Supervisor 掌控全局决策,子 Agent 只负责执行,实现了决策权与执行权的分离。

错误处理的核心是“不阻塞”原则:重试 + 降级,确保一个子 Agent 的失败止步于自身,不会传染给整个系统。

结果合并的核心是“规则显式化”:数据矛盾时谁优先、矛盾如何标注、缺失如何处理,这些规则必须明确写入 Prompt,不能让 LLM 自由发挥。

并行是有上限的:2-3 个子 Agent 并行是大多数场景下的最优解,更多的并行带来的往往是协调开销和 API 限速,而非更快的速度。

希望这篇从实战出发的剖析,能帮助你更好地理解和设计真正的 Multi-Agent 系统。如果你想了解更多关于系统架构或 大模型面试 的实战经验,欢迎在 云栈社区 交流探讨。




上一篇:掌握5种AI Agent技能设计模式,提升ADK开发效率与代码质量
下一篇:C语言指针从零到一:用形象比喻理解内存的地图
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-29 10:30 , Processed in 0.510245 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表