一个多智能体系统的完整设计

本文是一份多智能体系统设计的结构性还原,只讨论通用架构方法、模块划分与协议设计,不涉及任何具体业务、内部平台、数据与效果指标。文中的协议定义为示意性简化版本。

一、背景

垂直业务领域——CRM、交易、履约这类——有两个共同特征:业务逻辑极其复杂,且大量事务依赖人工处理。人要在多个系统之间查数据、判断状态、执行操作,效率和 ROI 都很难有显著提升。

LLM 的出现让"用智能体系统处理复杂业务流程"变得可能。但从"能用 LLM 回答问题"到"能用多个智能体协同完成业务任务",中间隔着一整套系统设计。

多智能体系统(Multi-Agent Systems, MAS)是由多个自主智能体组成的分布式系统,通过协作或竞争解决复杂问题。本文记录这样一个系统从算法策略角度的完整设计——目标是可扩展、高效、鲁棒。

三个核心设计目标贯穿全文:

  1. 任务规划及回复准确,任务与问题解决率高;
  2. 不同业务方可以灵活接入
  3. 减少定制开发——包括定制 workflow、定制工具范围、定制代码。

第三点是最容易被忽略、也最决定长期成本的。一个需要为每个新业务写一遍 workflow 的框架,本质上只是把复杂度从业务侧搬到了框架侧。

二、概要设计

2.1 单 Agent 架构

配置与能力层离线:训练与评估闭环上下文与配置 QueryRetriever · 工具 / Agent 召回CoordinatorRetriever · 知识 / 场景规划PlannerReflectorActor(Executor)ToolsSub-AgentsResponseMemory 拒绝 / 澄清 / 闲聊 / 快慢分流Agent ProfileScenario MgmtTools MgmtKnowledge日志标注训练评估模型服务(Coordinator / Planner / Actor 共用)ObservationEnvironmentPrompt MgmtDemos Mgmt Fast Call反思循环读写注册 / 配置

单 Agent 整体架构:中间为主链路,两侧为支撑设施;离线闭环产出的模型供主链路各模块使用

主链路是一条清晰的流水线,每一跳都有明确职责:

Query → Retriever(工具/Agent 召回)→ Coordinator → Retriever(知识/场景召回)→ Planner ⇄ Reflector → Executor(Actor) → Tools / Sub-Agents → Response

两次 Retriever 调用是有意为之,它们检索的东西完全不同:

  • 第一次在 Coordinator 之前,从几百个候选中粗筛出可能相关的 Tools 和 Agents,为分流决策提供候选集;
  • 第二次在 Planner 之前,检索业务知识和已有的场景规划(Scenario Plan),为规划提供背景。

围绕主链路的是四组支撑设施:

分组 内容
配置侧 Agent Profile(Agent 注册与管理)、Scenario Mgmt(场景名 / 描述 / 规划 / 示例)
能力侧 Tools(QA、工作流管理、工具管理)、Knowledge(结构化 + 非结构化)
上下文侧 Memory(Observation + Environment)、Prompt Mgmt、Demos Mgmt
模型侧 在线的模型服务,离线的日志管理 → 数据标注 → 训练 → 评估 → benchmark 闭环

值得注意的是主链路上那条 Fast Call 旁路:Coordinator 判定为高频简单场景时,可以跳过 Planner 直接调用工具或子 Agent 返回。这条旁路是延迟优化的主要来源,后面 Coordinator 一节会详细讲。

2.2 多 Agent 架构

先看业界的四种编排模式:

编排模式 主要用例 示例
分层监督者 将任务路由到专业代理 客户支持请求被路由到"计费代理"或"技术支持代理"
静态流程(DAG) 结构化的多步骤数据处理或生成 生成研究报告:大纲 → 搜索 → 撰写 → 编辑
对话协作 通过辩论、头脑风暴或迭代评审解决问题 "编码代理"提出代码,"评审代理"循环反馈
涌现式(Blackboard) 智能体需对不断变化的状态做出反应 各代理将信号发布到共享空间并相互响应

本设计采用的是分层监督者模式——一个 Master Agent 加若干 Sub Agent。原因是业务场景里任务边界相对清晰,需要的是可靠的路由和可追溯的执行,而不是涌现式的探索。

UserQueryResponseAgent ServiceCoordinatorPlannerActorTools(所有 Agent 共享) Master AgentSub Agent 层 领域 Agent A(白盒接入)领域 Agent B(白盒接入)黑盒接入黑盒接入其他 Agent CoordinatorPlannerActorCoordinatorPlannerActorLLMLLMLLM / … FastSlowSlow:子 Agent 返回Fast:子 Agent 直接返回

多 Agent 主从架构:Fast 路径由 Coordinator 直接分流,Slow 路径经 Planner / Actor 编排

架构特性上要满足三点:

  • 可扩展性:支持动态添加和移除智能体,横向扩展;
  • 容错性:单点故障不影响整体系统运行;
  • 异构支持:支持不同类型和能力的智能体协作。

"异构"是关键。Sub Agent 分两种接入形态:

  • 白盒接入:基于主 Agent 框架构建,内部同样是 Coordinator → Planner → Actor 结构,主 Agent 能看到它的中间过程;
  • 黑盒接入:对主 Agent 完全不透明,内部可能只是一个 LLM 调用,只保证输入输出协议一致。

一个成熟的系统里这两种必然共存——不可能要求所有团队都用同一套框架重写。

2.3 Sub Agent 接入方案的三选一

这是多 Agent 设计里第一个真正的分岔口。三个候选方案:

方案一:把 Sub Agent 拆成 N 个独立 Tool 接入。 最省事,但每接入一个新的 Sub Agent 都要重复一遍拆分工作,没有从根本上解决问题。

方案二:采用 A2A 协议接入。 标准、灵活,支持输出中间过程(如 plan、当前步骤)。但需要额外开发一整套 A2A 接入逻辑,开发成本高。

方案三:采用 MCP 协议接入。 兼容现有的工具接入模式,开发成本最低。选它。

选择方案三之后,剩下两个必须专门优化的点:

  1. Sub Agent 的功能描述——描述写不好,主 Agent 就不知道什么时候该调它;
  2. 主 Agent 传给 Sub Agent 的指令准确度——这决定了协作的下限。

入参形式也有取舍:

入参形式 优点 缺点
直接传 query 通用性好,子工具变化不影响主 Agent 需要 Sub Agent 自己做意图理解
抽取所有子工具的入参 参数明确 子工具一变,Sub Agent 描述和参数都要改,维护成本高

结论是传 query。这是一个典型的"把不确定性留在边界内部"的选择:让 Sub Agent 自己消化参数解析的复杂度,而不是把它暴露到协作协议里。

2.4 混合编排:让主 LLM 分不清工具和 Agent

同一个主 LLM 既要调度工具函数,又要调度子智能体。这两种执行单元本质上完全不同:

  • Tools:无状态原子能力,函数调用、单次执行、返回结果即结束;
  • Sub-Agent:有状态独立智能体,自带上下文、规划、反思、多轮推理。

核心难点是把「工具调用」和「子 Agent 调用」做成统一协议、统一解析、统一调度入口,让上层 LLM 无感兼容。

解法分三层:

统一抽象。 把 Sub-Agent 包装成一种特殊的 Tool——给每个 Sub-Agent 定义标准的 namedescriptionparameters,格式和普通函数工具完全一致。主 LLM 视角里分不清是普通工具还是子 Agent,都是"传入参数、等待返回结果"。调度层内部做路由:识别是普通函数就执行函数,识别是 Sub-Agent 就拉起子 Agent 会话并托管执行。

统一输出格式。 强制主 LLM 输出统一结构:

{"call_type": "tool|sub_agent", "name": "xxx", "params": {}}

解析层只靠 name 路由,上层逻辑完全不用改。

统一上下文与消息格式。 工具返回和子 Agent 返回都封装成同一种消息结构(role=tool,content 为结果文本或结构化 JSON)。主 Agent 的上下文只追加统一格式消息,不区分来源;Sub-Agent 内部有自己的独立上下文,与主 Agent 物理隔离,只通过标准入参 / 出参通信。

最后这条隔离很重要:如果子 Agent 的中间推理过程全部回灌到主 Agent 上下文里,多跑几轮上下文就爆了,而且主 Agent 会被子 Agent 的思考过程干扰。隔离既是为了成本,也是为了质量。

三、详细设计

以下按模块展开。每个模块给出关键能力协议设计问答——设计问答是原始设计文档里最有价值的部分,它记录的不是"做成了什么",而是"为什么这么做"。

3.1 Agent Profile

关键能力:Sub Agent 既可以依赖 Master Agent 的公共基建,也可以自建。架构图中的 Coordinator、Retriever、Planner、Actor 等子模块,既提供单独的原子能力,也提供多模块组合的能力——业务方按需选用。

这是"减少定制开发"目标的具体落地:框架提供的不是一个必须整体接受的黑盒,而是一组可以按需取用的原子能力。

Agent 注册协议(参照 A2A 的 Agent Card 设计):

struct Agent {
    1: required i64 id,                        // 唯一标识
    2: required string name,                   // 名称
    3: required string desc,                   // 功能描述
    4: optional list<string> demos,            // 示例
    5: optional string url,                    // 服务端点
    6: optional string version,
    7: optional list<string> defaultInputModes,   // 默认输入媒体类型
    8: optional list<string> defaultOutputModes,
    9: optional list<AgentSkill> skills,       // 该 Agent 提供的技能列表
}

struct AgentSkill {
    1: required i64 id,
    2: required string name,                   // 可读名称
    3: required string description,            // 技能的详细说明
    4: optional list<string> tags,             // 用于分类与发现
    5: optional list<string> examples,         // 示例 prompt 或用例
    6: optional list<string> inputModes,
    7: optional list<string> outputModes,      // 如 "text/plain"、"application/json"
}

Agent 间通信协议

// 调用 Agent 的请求
struct AgentCall {
    1: required string name,
    // 与 ToolCall 不同,这里不带参数的 meta info;
    // 每个 Agent 定义自己需要的入参,默认传近 N 轮对话
    2: optional list<Message> conversationHistory,
    3: optional i64 id,
    4: required Message currentMessage,
    5: optional string environment,   // 必要的环境信息:用户标识、请求来源、请求时间
    6: optional string extra,         // 预留扩展字段
}

// Agent 返回
struct AgentResponse {
    1: required string execStatus,   // success / failed / partial
    2: optional string resultData,   // 任务结果(JSON,需含必要的字段描述)
    3: optional string errorInfo,    // failed 时必填,含错误码和描述
    4: optional string execTime,     // 执行耗时(毫秒)
}

设计问答

Q:Agent 返回什么格式? A:端到端的回复文本,不返回函数调用结果等中间结果。这条约束保证了调用方不需要理解被调方的内部实现。

Q:A2A 与 MCP 的区别? A:A2A 支持更灵活的输入输出,例如输出中间过程(plan、当前步骤等)。这也是为什么在需要展示"思考过程"的场景里,A2A 更合适;而在只需要结果的场景里,MCP 的低成本优势更明显。

Q:怎么快速构建一个新的 Multi-Agent(比如报告生成型)? A:在 planning 入口函数的入参里增加 agent 字段。注意不要复用已有的业务线 ID 字段——一个字段承担两种语义,是后期维护灾难的开始。

Q:框架如何灵活适配不同类型的任务? A:以报告生成类 Agent 为例,需要能快速新建 Agent,支持灵活配置 prompt / tool / 模型;同时增强 Actor 与 Reflector 的能力,基于 Planner 生成的 MetaPlan 细化出具体步骤,才能生成长篇报告。

3.2 Task Management(Skill / Scenario)

任务描述有三种来源,成本递减、灵活性递增:

  1. 纯文本描述——产品/运营编写;
  2. 带占位符的描述——技术侧编写,描述中直接引用 Tools;
  3. 从知识库检索任务描述——没有预置描述的场景,由 LLM 生成。

场景的协议定义:

struct Scenario {
    1: required i64 id,
    2: required string name,
    3: required string description,
    4: required string configRef,        // 场景执行步骤的配置引用
    5: optional list<string> demos,      // 提问示例
    6: optional bool needReflector,      // Actor 执行后是否需要再次调用 Reflector,默认 true
}

needReflector 这个开关看起来很小,但它是延迟优化的一个重要旋钮——不是所有场景都需要反思,简单查询走反思是纯粹的延迟浪费。

3.3 Tools

工具注册协议参照 MCP:

struct Tool {
    1: required i64 Id,
    2: required string Name,
    3: required string Description,
    4: optional list<string> Demos,       // 提问示例
    5: optional string InParam,           // 入参 meta info,JSON Schema
    6: optional string OutParam,          // 出参 meta info,JSON
}

// 工具调用。两处使用:1) Plan 的结果指令,给到工程侧执行;
//                    2) Message 中的一条返回消息,作为 Memory 的一部分
struct ToolCall {
    1: required string Name,
    2: optional string InParam,           // 实际参数值,不带 meta
    3: optional string OutParam,          // 返回结果
    4: optional string ToolCallId,        // 全局唯一,区分同一工具被多次调用
}

入参用标准 JSON Schema 描述:

{
    "type": "object",
    "properties": {
        "param_name1": {"type": "string",  "description": ""},
        "param_name2": {"type": "integer", "description": ""},
        "param_name3": {"type": "array", "items": {"type": "string"}, "description": ""}
    },
    "required": ["param_name1", "param_name2"]
}

设计问答

Q:function call 协议用 JSON 串还是转成 IDL? A:用 MCP 协议的 JSON 串,入参出参都用这个格式。跟随生态标准,比自造一套省下的长期成本远超短期的转换开销。

Q:工具重名怎么办? A:配置平台限制 name 唯一,不允许创建同名 Tool。听起来是小事,但在几百个工具、多团队并行注册的规模下,这是必须在平台层强制的约束——靠约定一定会失效。

通用工具集:除业务工具外,框架预置一组通用工具,让智能体能与外部环境交互:

  • bash_tool:执行 bash 命令
  • browser_tool:浏览网页
  • crawl_tool:爬取网页内容
  • python_repl_tool:执行 Python 代码
  • search_tool:搜索信息

3.4 Knowledge

关键能力

  1. 可以看作 long term memory 的一种形式;
  2. 知识录入:支持通用文档和 FAQ 两种形式;
  3. 知识检索:作为 RAG 和 Planning 的共同输入。非结构化数据走向量化,结构化数据走 text-to-SQL。

知识片段的协议:

struct Knowledge {
    1: required i64 id,
    2: optional string content,          // 知识片段
    3: optional string ref,              // 原始文档链接
    4: optional string metaData,         // JSON,如权限控制字段,用于拼接检索 DSL
    5: optional double score,            // 召回得分
    6: optional KnowledgeType type,      // doc / faq
}

知识条目的元信息则包含:所属业务、类型、格式(PDF/DOC/TXT/MD)、owner生效时间失效时间

后三个字段值得单独说:owner 决定了这条知识过期时谁来负责,生效/失效时间让"知识过期"变成系统可判定的状态而不是靠人发现。没有这三个字段,知识库的腐坏就只能靠事后治理。

设计问答

Q:不同 Agent 的知识范围不同,如何做权限管理? A:简单逻辑(如按业务线过滤)在策略侧实现;复杂权限管理需要后端统一支持,策略侧在召回阶段改写检索 DSL 实现过滤。

Q:同时支持结构化和非结构化检索吗? A:是。非结构化走向量化,结构化走 text-to-SQL。这两条路径的召回质量评估方式完全不同,需要分别建评测集。

3.5 Memory(Context Engineering)

关键能力分五类:

  1. 信息存储:沟通历史(历史消息、槽位)、历史工具调用返回、用户相关的环境信息;
  2. 信息检索
  3. 信息压缩
  4. 信息管理:更新与遗忘;
  5. 信息共享:多 Agent 之间共享 memory(如环境信息),并区分私有记忆和共享记忆

存储信息按必要性分级:

数据类型 必要性 用途
历史意图 P0 维护对话上下文、支持多轮任务、预测用户长期目标、消息引用
历史槽位值 P0 避免重复询问、动态更新参数、处理多源信息冲突
历史动作 P1 行为追踪、策略优化(如强化学习)、错误诊断与回溯

历史动作被列为 P1 而非 P0,是个有意思的判断:它对当前这轮对话的正确性帮助有限,但它是训练数据和问题排查的基础。换句话说,它服务的是系统的长期演进而非单次交互。

设计问答

Q:memory 检索策略? A:三条并行——近 N 轮消息(按时间截断)、按同一个 goal 取 memory(注意 goal 可能变化频繁)、以及 memory retrieve(语义检索)。

Q:memory 过长、或某次工具调用的 response 过长怎么办? A:信息压缩。工具返回超长是实践中最常见的上下文爆炸来源——尤其是搜索类工具。

Q:为什么要存快照? A:三个用途——构建训练样本、排查线上问题、构造线上请求的输入(这个只需要最新快照)。

核心数据结构:

// 单条消息对应的观察
struct Observation {
    1: Message Message,                        // 用户 query 或 Agent 返回
    2: list<string> Goals,                     // 当前消息对应的目标(可能多个)
    3: optional list<string> Slots,            // 当前消息提取的槽位
    4: optional list<ActorResult> ActorResults,// 当前消息应执行的动作
    5: optional list<string> Plans,            // 采用的 plan
    6: optional list<Scenario> Scenarios,      // 命中场景,与 plan 一一对应
}

struct Memory {
    1: string DialogueId,
    2: optional list<Observation> Observ,      // intents / slots / actions / 对话历史
    3: optional list<string> Environment,      // 环境信息,如客户信息、时区
}

消息类型的枚举设计值得单独看一眼,它体现了真实产品的复杂度:

enum MessageType {
    TEXT, IMG, VOICE, EMOJI, PIC_EMOJI, HYPERLINK,
    SHORT_CUT,    // 快捷命令,直接映射到功能:透传工具 id,无需提槽
    CARD_EVENT,   // 卡片事件:同上
    TOOL_RES,     // 工具调用结果。对话历史过模型时可选择排除这部分
    HINT,         // 用户点击[关联推荐问题]发送,自带意图信息但不带槽位
    OTHER,
}

SHORT_CUT / CARD_EVENT / HINT 这三类的存在说明了一件事:真实的 Agent 输入远不止自然语言。用户点一个卡片按钮、选一个推荐问题,携带的意图信息比一句话精确得多——识别出这些类型并跳过意图理解环节,既省钱又更准。而 TOOL_RES 单独成类,是为了在拼上下文时能精确地把它排除掉。

3.6 Coordinator

关键能力

  1. 支持快慢分流——简单任务快分流直接执行,复杂任务转交 Planner 多步处理;
  2. 拒绝不在能力范围内的请求;
  3. 处理闲聊等通用对话场景;
  4. 引导用户提供足够上下文以澄清意图。

输出协议:

enum CoordinatorType {
    REJECT = 1,      // 拒绝响应
    CLARIFY = 2,     // 澄清意图
    SMALL_TALK = 3,  // 闲聊等通用对话
    PLAN = 4,        // 需要 Planner 生成规划
    OTHER = 1000,
}

struct CoordinatorResult {
    1: optional string text,             // CLARIFY / SMALL_TALK 时生成的回复文本
    2: optional string goal,             // 改写后的完整用户意图
    3: optional CoordinatorType type,
}

设计问答

Q:快慢分流之后的处理逻辑如何设计? A:快分流入参是 history,不做改写,特殊场景打补丁;慢分流由 Actor 按步骤改写当前步的 query。 这个结论看似随意,其实是延迟与准确率的权衡——快分流的价值就在于省掉一次改写调用,如果为了统一而给它加上改写,它就不快了。

Q:意图和 Agent 是什么关系? A:三条判断——

  1. 意图不固定范围
  2. 用户意图需要由多个 Agent 协同解决;
  3. 意图与 Agent 不再强绑定,由 planning 决定调用哪个 Agent。

第三条是整个多 Agent 设计的基础。如果意图和 Agent 一一绑定,那系统就退化成了一个意图分类器 + 路由表——新增一个 Agent 就要重新划分意图边界,且无法处理需要多个 Agent 协作的意图。

意图识别模型的演进路径:前期基于通用大模型积累线上数据,后续微调自建模型。自研模型的预期收益有四个维度:

  1. 准确性更高——领域知识注入模型;
  2. 延迟更低——参数量更小;
  3. 成本——降低模型调用开销;
  4. 稳定性——外部模型出问题只能等 oncall,自研模型可控性更强。

第四点在设计文档里通常不会被写进"技术收益",但它往往是推动自研的真正原因。

3.7 Planner

关键能力

  1. 生成任务规划,支持两种模式: - FullPlan(Plan-and-Execute,全局规划):一次生成完整任务规划; - ReAct(局部规划):根据每一步执行结果动态生成下一步;
  2. 根据执行结果和用户最新输入动态更新任务规划;
  3. FullPlan 的两种生成方式:LLM 基于业务背景生成(可微调),或人工编写;
  4. 模式选择:尽量执行,而不是先追问全部参数
  5. 支持开放意图——意图转化为 Goal。

第 4 点是个重要的产品判断。两种策略:执行优先 vs 追问澄清优先。选执行优先的理由是:用户对"先做起来再问"的容忍度,远高于"上来先问三个问题"。

设计问答

Q:多步 Plan 执行过程中,用户输入了新 query 怎么办? A:生成新 goal → 生成新 plan → Actor 根据新 plan 判断下一步执行什么。已执行的步骤可以复用。

Q:Planner 需要在每次循环时按需 Update Plan 吗? A:Planner 输出的 Plan 定位是 MetaPlan,并非详细 steps。用户有新 query 输入时走大循环尝试 Update Plan;而每一轮 Actor 指令执行后,由 Reflector 或 Actor 按需更新详细执行步骤。

这个分层很关键:MetaPlan 稳定,详细步骤易变。如果 Planner 直接输出详细步骤,那么每一次工具返回都可能让整个 plan 失效,Planner 就会被迫高频重算。

Q:Plan 中要不要指定具体的工具名称? A:这是 Planner 与 Actor 的职责边界问题——Planner 生成带占位符的抽象 plan,Actor 生成工程侧可执行的下一步(具体 tool 和参数)。抽象与具体分离,让 plan 可以复用、可以被检索、也更容易被人工编写。

输入输出协议

struct PlanningRequest {
    1: required string bizId,
    2: optional string dialogId,
    3: optional Memory memory,
    4: optional Message curMessage,
    5: optional list<Tool> tools,             // 可用工具列表
    6: optional list<Agent> agents,           // 可用 Agent 列表
    7: optional list<Knowledge> knowledges,   // 可参考知识
    8: optional list<string> reflects,        // 反思结果
    9: optional string recallBackground,      // 召回的背景知识
    10: optional string agentId,
    11: optional Agent currentAgent,
}

struct PlanningResponse {
    1: required list<string> plans,           // 规划结果
    2: optional list<Scenario> scenarios,     // 命中场景
}

注意 toolsagents作为入参传入的,而不是 Planner 自己去查。这让同一个 Planner 可以服务不同 Agent、不同业务线——可用能力集合由调用方决定。这是"减少定制开发"的又一处体现。

任务管理层的两个组件(框架侧抽象):

class TaskDecomposer:
    """将复杂任务分解为子任务"""
    async def decompose(self, task, context=None): ...
    def _build_decomposition_prompt(self, task, context): ...
    def _parse_subtasks(self, llm_response): ...

class TaskAllocator:
    """将任务分配给合适的智能体"""
    async def allocate(self, task, constraints=None): ...
    async def optimize(self, task, constraints=None):   # 动态更新任务分配
    def _analyze_task_requirements(self, task): ...
    async def _find_matching_agents(self, requirements, constraints): ...
    async def _select_best_agent(self, candidates, task): ...
    async def _select_best_tool(self): ...
    async def _ask(self):                                # 生成追问话术

3.8 Actor(Executor)

关键能力

  1. 根据 plan 和当前 context,生成具体可执行的下一步,调用 tools 或 sub agents;
  2. 生成中间步骤 summary;
  3. 判断任务是否完成——若 FINISH 则生成回复文本;
  4. 提取工具依赖的参数;
  5. Planner 生成带占位符的抽象 plan,Actor 生成工程侧可执行的下一步

Actor 的决策类型枚举,本质上是整个 Agent 的"动作空间":

enum ActorType {
    TEXT = 1,            // 生成话术,等待用户输入
    NO_RESPONSE = 2,     // 不响应
    SMALL_TALK = 3,      // 闲聊等兜底回复
    CLARIFY = 4,         // 澄清意图
    FINISH = 5,          // 结束,输出最终回复
    TOOL_FULLPLAN = 6,   // 调用工具,调用后继续循环
    AGENT = 7,           // 调用 Agent
    OTHER = 1000,
}

struct ActorResult {
    1: optional list<string> TextList,     // 生成话术
    2: optional list<ToolCall> Tools,      // 下一步要执行的工具
    3: optional list<AgentCall> Agents,    // 下一步要调用的 Agent
    4: optional ActorType ActorType,       // 决策类型
    5: optional string DialogId,
    6: optional string MessageId,
    7: optional i64 SceneId,
    8: optional string FullPlan,           // Planner 生成的 plan,用于前端展示
}

ActorResult 里同时带 ToolsAgents 两个字段,正是 2.4 节"混合编排"在数据结构上的落地——一次决策可以输出工具调用,也可以输出 Agent 调用,上层用同一套循环处理。

FullPlan 字段的作用是让前端能同时展示完整计划当前步骤。这是 Agent 产品体验的一个关键点:用户需要知道系统打算做什么、现在做到哪一步,否则长任务的等待过程就是纯粹的焦虑。

3.9 Retriever

核心能力(按优先级):

  1. P0 根据用户意图(Goal)检索相关知识片段;
  2. P1 从大量 Tools 候选中粗筛可用工具——例如从几百量级缩到几十;
  3. P2 从候选 Agents 中选出相关 Agent;
  4. 根据 query 从现有 MetaPlan 中召回 plan。

第 2 点是很多 Agent 系统在规模化时才遇到的问题:工具数量一旦上百,全部塞进 prompt 既超上下文又降准确率。工具召回本身就是一个检索问题,需要独立的召回模块和评测。

第 4 点则是一个被低估的优化:已经验证过的 plan 是可以复用的资产。相似 query 直接召回历史 plan,比每次让 LLM 重新规划既快又稳。

enum SpeechType {
    CHAT = 1,           // 文本问答
    TOOL_RECALL = 2,    // 工具召回
    PLAN_RETRIEVE = 3,  // 抽取 plan 相关的业务文档信息
    OTHER = 255,
}

struct RAGRequest {
    1: required string content,
    2: required SpeechType type,
    3: optional list<Message> chatHistory,
    4: optional string userID,
    5: optional list<string> bizIdList,
    6: optional list<Tool> Tools,
    7: optional list<Agent> Agents,
    8: optional list<Scenario> Scenarios,
    9: optional string rawDsl,                 // filter 条件
    10: optional map<string,string> userInfo,
    11: optional string rankPriority,          // 排序逻辑 prompt
    254: optional bool isDebug,
}

struct DebugInfo {
    1: optional string stopNode,                    // 流程终止节点
    2: optional map<string,string> nodesOutput,     // 终止前各节点的输出
}

DebugInfo 这个结构值得一提:它记录流程在哪个节点终止、以及终止前每个节点的输出。多模块串联的系统里,没有这个东西就没法定位问题——你只知道最终答案不对,不知道是召回错了、规划错了还是执行错了。可观测性必须设计进协议,而不是事后加日志。

3.10 RAG

核心能力:作为通用 Tool,可以被各个 Agent 调用。

设计问答

Q:数据传入格式? A:支持 PDF / TXT / DOC / MD。知识中心提供知识的元信息(通过消息队列传给策略侧)以及搜索接口,作为一路召回。

Q:知识权限如何控制? A:知识中心统一做权限控制并提供接口,策略侧获取权限配置后,在召回阶段改写 DSL 实现过滤。

Q:个性化 tagging 引入答案排序? A:本期不做。(设计文档里明确写下"本期不做",比不写要好得多——它记录了这个选项被考虑过并被主动推迟。)

Q:问答作为 tool 调用完之后还要走 Reflector,流式怎么处理? A:见 Reflector 一节。

3.11 Reflector(反思与异常处理)

关键能力

  1. 反思 plan 步骤的执行结果是否正确: - 调用 tool:根据返回结果判断是否正确; - 调用 agent:上游 Agent 对下游 Agent 的返回结果做置信度判断; - 生成 text:改写生成的文本;
  2. 多结果判别:对多个返回结果排序;
  3. 异常处理:Solver 模块在执行结束时综合 Context 生成最终回复;
  4. Deep Research 策略:反思(Reflect)、重规划(Replan)、重执行(Re-Act)。

设计问答

Q:Reflector 如何支持卡片定制和流式输出? A:三条应对——

  1. 卡片定制:反思模块的返回里带上 tool 信息,前端据此映射定制卡片;
  2. 流式:中间结果流式展示在"思考过程"区域。如果反思模块认为需要重新生成,正式结果也流式输出;如果认为可以直接返回,就直接返回,延迟更低。流式的本质目的是降低感知延迟
  3. 某些渲染端(如不支持展示思考区域的卡片场景)需要配置为无 Reflector。注意这个开关不能配到 tool 粒度——同一个 tool 在网页端有思考区域、在卡片端没有,粒度必须是渲染端而非工具。

Q:异常处理有哪些场景? A:

  1. 尝试多种解决思路并输出尝试过程。例如用户问"近 8 天的数据",而系统只有 7 天和 15 天粒度——那就返回近 7 天和近 15 天,并说明原因。这比直接回答"无法查询"有用得多;
  2. 兜底:异常情况返回兜底话术,并对兜底话术做润色;
  3. 工具返回超长(如搜索类工具):做 summary。

Q:多 Agent 结果不一致怎么办? A:上游 Agent 转发给下游后,下游可能拒绝回复。这时需要一个中央决策者角色:如果还有其他 Agent 可以处理就继续转发,如果没有则终止,并记录问题转人工 review

最后这半句很重要——系统必须有一个明确的"我搞不定"出口,并且这个出口要留下可供改进的记录。

Q:如何避免陷入多轮循环? A:设定最大循环轮次。朴素但必要。

Q:Reflector 的完整流程? A:

  • 判断工具调用正确:若是 FINISH,生成 answer(放进 actorResults);若不是 FINISH,继续进入 Actor 规划下一步;
  • 判断工具调用错误:生成错误原因(放入 result 字段),传给 Actor 重新规划下一步;
  • 判断为异常情况:生成异常描述和建议的解决方向,传给 Actor 重新规划。

Q:Reflector 的返回改写会破坏下游的定制输出格式吗? A:增加一个 tool 粒度的配置项——有定制格式的 tool,Reflector 不改写其返回;没有的才可以改写。

Q:为什么 Reflector 从 Actor 循环调整到 Planner 循环? A:Actor 循环粒度过细,调用频繁,延迟增加。

这是全文我最喜欢的一条设计记录。它记录了一次架构调整,以及调整的唯一原因——延迟。反思放在每一个 Actor 动作之后当然更"正确",但正确性的边际收益扛不住延迟的线性增长。

3.12 智能体协作机制

核心能力

  1. 上游 Agent 通过 planning 调用下游 Agent 解决对应问题;
  2. 下游 Agent 返回最终文本;
  3. 消息传递须遵循主从模式——请求时由上游逐层调用到下游,返回时由下游逐层返回上游;
  4. 支持多种协作模式,如发布-订阅(P1)。

第 3 条是一条纪律性约束。允许下游直接跨层返回或横向通信,短期看省了一跳,长期看会让调用链无法追踪、错误无法归因。

框架侧提供两个抽象:WorkflowEngine(基于图的工作流定义与执行,节点逐个执行并根据结果决定下一节点,直到 __end__)和 CollaborationStrategy(协作模式:层级 hierarchical / 对等 peer / 市场 market)。

3.13 策略配置

  • Prompt Management:统一管理各模块使用的 prompt,托管在 prompt 管理平台;
  • Demos Management:管理各模块的 few-shot 示例,用于提升模型效果。

把 prompt 和 demos 从代码里抽出来做成配置,是 Agent 系统的基础设施要求——它们的迭代频率比代码高一个数量级。

3.14 对外接口

整合 Coordinator、Planner 等模块,对外只暴露两个接口:

service DialogManagementService {
    RAGResponse commonRAG(1: RAGRequest req),                    // 通用问答 / 召回
    AgentPlanningResponse agentPlanning(1: AgentPlanningRequest req),  // Agent 规划
}

内部十几个模块,对外两个接口。接口收敛是框架可用性的前提。

3.15 代码结构

├── README.md
├── idls/
└── agent/
    ├── actor/
    ├── agent/
    ├── common/
    ├── config/
    ├── coordinator/
    ├── planner/
    ├── prompt/
    ├── rag/
    ├── reflector/
    ├── retriever/
    ├── solver/
    └── utils/

目录结构和架构图一一对应。这不是巧合,而是一个健康信号:当代码结构需要一张"翻译表"才能对应到架构图时,通常意味着其中一个已经腐坏了。

四、模型训练

自建模型的预期收益,四个维度:准确性更高、延迟更低、稳定性更高、成本更低

训练覆盖四类模型,各自的方案高度相似但目标不同:

模型 目标 方案
意图识别模型 快慢分流判断准确 构建训练集与评测集 → 中小尺寸开源模型 → SFT / RL
Planner 模型 生成合理且完整的 MetaPlan 同上,另加流式 RL 训练
Actor 模型 工具编排与参数提取准确 全参量 SFT
RAG 模型 Recall / Rank / MRC 三段分别优化 训练 embedding 模型提升召回;微调提升生成准确性、降低延迟、减少幻觉

算法演进路径是一条清晰的阶梯:

Prompt → RAG / GraphRAG → SFT → MetaPlan(RL)→ Agent + RL

这条路径的意义在于:每一级都能独立产出价值,且后一级依赖前一级积累的数据。Prompt 阶段积累线上 query 与结果,成为 SFT 的训练数据;SFT 阶段的模型上线后产生的交互轨迹,又成为 RL 的素材。不能跳级,跳级就没有数据。

微调方案除基础的领域知识注入外,多智能体场景还有四类特殊训练:

  1. 多智能体交互数据生成:用 LLM 生成智能体之间的对话与决策过程数据;
  2. 一致性训练:确保各智能体行为决策一致——例如 Coordinator 与 Planner 的判断要一致,上游 Planner 与下游 Coordinator 的理解要一致;
  3. 协作性训练:设计需要多智能体共同完成的任务,让它们学会分配与配合;
  4. 对抗训练:让智能体与对抗性智能体或环境交互,提升鲁棒性。

第 2 点"一致性"是多 Agent 系统特有的问题:单个模块各自最优,不代表串起来最优。上游认为该转给 A Agent,下游 A Agent 却认为这不是自己该管的——这种不一致在单 Agent 系统里根本不存在。

训练方法上,SFT 之后走 RL(DPO、GRPO 等),并支持流式 RL 训练——即用线上真实交互持续更新模型。

五、效果评估

业务指标:问题解决率。 技术指标:各模块的输出准确率。

评估体系分三条腿走路。

5.1 人工标注

用于效果评估(测试集)和模型训练(训练集)。需要明确的标注 SOP 和标注标准,这是整个评估体系的地基——标注标准不清晰,后面所有自动化都是在放大噪声

5.2 自动化标注(无 Ground Truth)—— LLM as a Judge

两个目的:一是产出训练数据,二是快速验证模型迭代效果、提高迭代效率。做法是多模型结果投票

端到端自动化标注流程:

  1. 获取线上 query-answer pairs;
  2. 提供背景知识和 tools;
  3. 给出明确的标注标准;
  4. LLM 基于上述背景和标准,标注 answer 是否合理;
  5. 人工质检部分 case
  6. 质检准确率达标(如 95% 以上)才用于训练。

各模块自动化标注同理,只是把 answer 换成中间模块的输出。

第 5、6 步是这套方法能否成立的关键。LLM as a Judge 本身有误差,如果不做人工质检,你无法知道这个误差有多大——没有质检环节的自动标注,等于把不知深浅的噪声直接灌进训练集。

5.3 自动化评测(有 Ground Truth)

  1. 获取线上 query-answer pairs;
  2. 人工 review,保留正确的 pairs;
  3. 构建千条量级的 Ground Truth 测试集,根据用户 query 定期更新,覆盖主要场景;
  4. 模型迭代后、上线前必须在此测试集上测试,由 LLM 判断生成结果与 GT 是否一致,有提升才允许上线

各模块的评测按输出类型分三类处理:

  • 可枚举输出类模块:LLM 判断输出是否与 GT 一致;
  • 灵活输出类模块:LLM 判断输出是否合理;
  • 混合类:两者结合。

"定期更新测试集"这一条容易被忽略但极其重要:用户 query 的分布是漂移的,一个半年不更新的测试集,测的是半年前的产品。

5.4 评估体系

完整指标体系分三个一级维度:

一级 二级 描述 相关模块
效果 回复合理性 端到端回复是否解决用户问题 端到端
规划合理性 plan 的正确性与完整性 Planner
工具调用 工具编排准确性、参数提取准确性 Actor
意图识别 快慢分流结果是否正确 Coordinator / Actor
检索能力 tools / plan / knowledge 检索的正确性与完整性 Retriever
长短期记忆 上下文理解与信息提取是否准确(如新值覆盖旧值) Memory
反思能力 反思结果是否正确 Reflector
延迟 端到端延迟 从 query 到最终回复 端到端
首 Token 延迟 从 query 到首 token
单步延迟 单步骤耗时
交互轮次 端到端处理轮次(越低越好)
合规 幻觉 生成的幻觉占比
合规 生成内容是否合规

这张表最值得学习的是它的结构:每个二级指标都明确绑定到具体模块。这让评估结果可以直接指导优化——端到端回复合理性下降时,能顺着表往下查是规划出了问题、工具调用出了问题,还是检索出了问题。

另外有一列在原表里叫「兼容 Reward Model」——即这个指标能否直接用作强化学习的奖励信号。在设计评估体系时就考虑它未来能否变成训练信号,这是一个很有前瞻性的设计动作:评估和训练用的是同一套标准,模型优化的方向才和评估的方向一致。

LLM Judge 本身也有验收标准:双盲一致率 90%+、人机一致率 90%+。评估工具自己也要被评估。

六、Case Study

6.1 通用场景与业务场景的区别

  1. LLM 在公开语料上训练,具备一定的通用任务规划能力,但研究表明规划成功率仍有待提高(见 PlanGEN 等相关工作);
  2. 垂域业务场景的特有知识往往是 LLM 不具备的,因此难以直接规划,需要通过 prompt 或训练把领域知识注入模型。

这两条决定了业务 Agent 不能直接照搬通用 Agent 的做法。

6.2 一个多步任务的典型形态

以"为某个客户准备跟进材料"这类任务为例,一次完整执行大致是:

  1. 获取当前需要跟进的对象;
  2. 根据 ID 或名称获取对象的基础信息;
  3. 用搜索工具获取相关的外部信息;
  4. 判断外部信息的相关性
  5. 对相关链接调用网页读取工具;
  6. 基于内外部信息汇总;
  7. 生成后续建议;
  8. 生成报告。

八个步骤里,只有第 4 步是纯粹的判断节点,其余都是"调工具 + 处理结果"。这解释了为什么 Actor 的工具编排准确率是最核心的技术指标——多步任务的成功率是每一步成功率的连乘

6.3 结构化任务描述

对于流程明确的任务,与其让 LLM 自由规划,不如直接给一份结构化的任务描述。它的格式是这样的:

1. 获取待查询的用户标识
    - 前置条件:[user_input, no user_id]
    - 执行操作:
        1.1 如果用户输入 {user_input} 中包含用户标识,提取 {user_id}
        1.2 若没有,调用 Function get_user_id,解析返回值
        1.3 调用后向用户确认:"是否要查询 {user_id} 的权限?若不是请提供正确标识。"
    - 后置操作:[保存变量 {user_id}]

2. 获取待排查的资源编号
    - 前置条件:[user_input, no resource_id]
    - 执行操作:
        2.1 从用户输入中提取 {resource_id}
        2.2 若没有则向用户询问,然后解析用户输入
    - 后置操作:[保存变量 {resource_id}]

3. 调用权限系统检查
    - 前置条件:[user_id, resource_id, operation_type]
    - 执行操作:
        3.1 调用 Function check_permission,输入上述三个变量,
            解析返回中的 {has_permission}, {reason}
    - 后置操作:[保存变量 {has_permission} {reason}]

4. 返回诊断结果
    - 前置条件:[resource_id, operation_type, has_permission, reason]
    - 执行操作:
        4.1 若 {has_permission} 为 true,回复"你拥有该资源的权限……" [end]
        4.2 若为 false,回复"你没有该资源的权限,原因:{reason}" [end]

这个格式有三个要素:前置条件(什么时候该执行这一步)、执行操作(具体做什么,含分支)、后置操作(产出什么变量)。

它本质上是一个带槽位状态机,但用自然语言写成,因此既能被 LLM 理解和灵活执行,又保留了流程的确定性。这是"确定性流程"和"LLM 灵活性"之间一个很实用的折中——比纯 prompt 可控,比硬编码 workflow 灵活。

三种实现方式的对比也很说明问题:

方式 特点
通用 LLM 直接规划 灵活,但业务流程正确性不可控
任务描述 + 通用 LLM 流程可控,但受限于通用模型的指令遵循能力与延迟
SFT 后的专用模型 把流程知识注入模型,指令遵循更准、prompt 更短、延迟更低

演进方向是从左到右——但左边两种是右边的前提,因为它们负责产出训练数据。

6.4 多 Agent 协作

最终形态是若干领域子 Agent(数据查询型、诊断型、报告生成型、对话型等)挂载在主 Agent 之下,由主 Agent 统一调度。这也回到了 2.2 节的架构:分层监督者 + 混合编排

七、几点观察

通读这套设计,有几个判断值得单独拎出来。

1. 这套架构的核心是"分层稳定性"。

从 MetaPlan / 详细步骤的分离,到 Planner / Actor 的抽象与具体分工,再到主 Agent / 子 Agent 的上下文隔离——同一个模式反复出现:把变化频率不同的东西分开。MetaPlan 变化慢,详细步骤变化快;业务知识变化快,框架能力变化慢;prompt 变化快,代码变化慢。分层的位置基本都落在变化频率的断层上。

2. 延迟是贯穿始终的隐藏主线。

Fast Call 旁路、needReflector 开关、Reflector 从 Actor 循环上移到 Planner 循环、流式输出、自研小模型——设计文档里至少五处调整的唯一动因是延迟。这是 Agent 系统和传统 LLM 应用最大的区别:Agent 的每一次"更聪明",几乎都要用延迟来买单,而多步任务会把单步延迟线性放大。

3. 最难的部分不在架构图上。

架构图上的模块都是可以照着实现的。真正难的是那些藏在 Q&A 里的东西:工具描述怎么写才能让主 LLM 正确选择、传给 Sub Agent 的指令如何保证准确、标注标准如何定义才不会让 LLM Judge 放大噪声。这些都是"文本质量"问题,没有架构图能解决它们。

4. 评估体系应该和架构同时设计,而不是之后补。

这份设计里,评估的每个二级指标都绑定到具体模块,且预留了"能否作为 Reward Model 信号"的判断。这意味着从第一天起,系统就是可度量、可优化、可训练的。相比之下,很多 Agent 项目是先把功能做完,再回头想怎么评估——那时候模块边界已经模糊,中间结果没有留存,想评估也无从下手。

5. 一个仍待验证的问题。

整套设计里,Planner 输出 MetaPlan、Actor 细化执行、Reflector 反思纠错,这三者形成了一个闭环。但闭环意味着误差也会在环里传播:Planner 给了一个次优的 MetaPlan,Actor 忠实执行,Reflector 判断"每一步都对"——最终结果不好,却没有任何一个模块会报错。

单模块准确率再高,也无法保证组合起来的端到端正确。这也正是端到端评估必须独立存在、且优先级最高的原因;更进一步,也是端到端强化学习(用最终结果的 reward 去优化中间模块)真正的价值所在。

关键词: agent LLM 架构