Administrator
发布于 2025-12-31 / 4126 阅读
76

多智能体协作:任务分解与结果汇总

多智能体上线两周,成本涨了 6 倍

十一月份我们把合同审查从单 Agent 改成了多智能体——一个主管 Agent 负责任务分解,下面挂了四个专职 Agent(条款抽取、风险识别、合规比对、历史案例检索)。理由是单 Agent 的表现遇到瓶颈:一份 40 页的合同要塞进一次上下文,模型经常顾此失彼,审查项召回率卡在 82% 上不去。

改造后召回率确实提到了 93%。但上线第二周,财务把账单发过来了:单份合同的审查成本从 ¥0.34 涨到 ¥2.17,涨了 6.4 倍。日均 900 份合同,月成本从 9,180 元变成 58,590 元。

这篇是完整的复盘,包括架构、成本失控的原因,以及我们怎么把成本压回到 ¥0.71。

架构:Supervisor 模式

我们用的是最经典的 Supervisor(主管-执行者)模式。选择它的原因很实际——层级结构最容易被人类理解和干预。出问题的时候能明确说是哪个子 Agent 的责任。

                    ┌──────────────────┐
                    │   Supervisor     │  强模型,负责分解+汇总
                    │   qwen-max       │
                    └────────┬─────────┘
                             │ 委派任务
        ┌────────────┬───────┴────┬────────────┐
        ▼            ▼            ▼            ▼
  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
  │ 条款抽取  │ │ 风险识别  │ │ 合规比对  │ │ 历史案例  │
  │ qwen-plus│ │ qwen-max │ │ qwen-plus│ │qwen-turbo│
  └──────────┘ └──────────┘ └──────────┘ └──────────┘
        │            │            │            │
        └────────────┴────────────┴────────────┘
                     ▼ 结果汇总回 Supervisor

Supervisor 拿到结果后做汇总,如果发现结果不充分,可以再派发一轮任务(最多 3 轮)。

任务委派:结构化的任务描述

委派这块我们踩过坑。第一版是让 Supervisor 用自然语言描述任务,子 Agent 自己去理解:

// v1:自然语言委派,问题很多
"请审查这份合同的付款条款,看看有没有风险"

// 子 Agent 的返回五花八门:
//   - 有时候返回一大段分析文字
//   - 有时候返回一个列表
//   - 有时候只说"没发现问题"(其实没审)

问题在于 Supervisor 无法可靠地解析子 Agent 的自由文本输出。汇总阶段经常丢失信息,或者重复计算。

改成结构化任务契约之后稳定多了:

public record SubTask(
    String taskId,
    String agentName,
    String instruction,        // 仍然是自然语言,给模型看
    List<String> inputs,       // 明确输入:需要哪些数据
    OutputSpec outputSpec,     // 明确输出:字段、类型、必填
    int maxTokens,
    String model
) {}

public record OutputSpec(List<Field> fields, String format) {
    public record Field(String name, String type, String desc, boolean required) {}
}

// 实际派发的任务
new SubTask("t-01", "clause-extractor",
    "从合同原文中抽取所有涉及付款的条款,包含金额、时间节点、前置条件",
    List.of("contract_text#full"),
    new OutputSpec(List.of(
        new Field("clauses", "array", "条款列表", true),
        new Field("clause_no", "string", "条款编号", true),
        new Field("amount", "number", "金额,单位元,无则 null", false),
        new Field("deadline_days", "integer", "付款期限天数", false)
    ), "json"),
    4000, "qwen-plus");

关键是 OutputSpec——子 Agent 被强制按 schema 输出,Supervisor 能可靠解析。这一项改动把汇总失败率从 12% 降到 1.4%。

另外 inputs 字段明确指定了输入范围。contract_text#full 是全部原文,contract_text#clause_3 是指定条款。这个设计是为了控制 token——不要让每个子 Agent 都拿到全量原文。

成本失控的四个原因

拿到账单后我们逐条分析,成本暴涨不是单一原因。

原因成本占比说明
子 Agent 重复读取全量原文41%四个 Agent 各自把 40 页合同都读了一遍
Supervisor 汇总时上下文过大27%把四个 Agent 的完整输出都塞进汇总
无效的重试轮次19%3 轮里平均有 1.7 轮是没必要的
子 Agent 用小模型返工13%输出不达标,Supervisor 让它重做

原因一:全量原文的重复传递

一份 40 页合同约 2.8 万 token。四个子 Agent 都拿到全量原文,光这一项就是 11.2 万输入 token。而且这还没算 Supervisor 自己读的那一次。

改造分成两步。第一步是预处理一次,切片共享:由一个专门的预处理 Agent(用最便宜的模型)先把合同切成结构化片段,后续 Agent 只拿自己需要的片段。

// 预处理:只做一次,结果共享
ContractSections sections = preprocessor.extract(contractText);
// {
//   payment: [第3条, 第7条, 第12条],       // 约 3,200 token
//   liability: [第8条, 第9条],              // 约 1,800 token
//   termination: [第15条],                 // 约 900 token
//   ...
// }

// 子 Agent 只拿相关片段
new SubTask("t-01", "payment-risk", "...",
    List.of("sections#payment"),     // 3,200 token 而不是 28,000
    ...);

第二步是给每个 Agent 的 inputs 做精确匹配。我们统计了各 Agent 实际用到的条款分布,发现「合规比对」Agent 只需要 4 类条款,之前却拿到了全文。

效果:输入 token 从 11.2 万降到 2.9 万,降了 74%。

原因二:汇总阶段的上下文爆炸

Supervisor 汇总时,原来的做法是把四个子 Agent 的完整输出(平均每个 2,100 token)全部放进上下文,再加自己的指令。四轮下来累计 8,400+ token。

问题是这些输出里大量内容是过程描述,比如「我首先检查了第 3 条,发现……然后我又看了第 7 条」。Supervisor 真正需要的只是结论和依据。

我们强制子 Agent 分两部分输出:

public record AgentOutput(
    List<Finding> findings,      // 结构化结论,进汇总上下文
    String reasoning,            // 过程描述,不进汇总上下文,只存审计日志
    double confidence
) {}

// 汇总时只用 findings,序列化后约 380 token/agent
String buildSummaryContext(List<AgentOutput> outputs) {
    return outputs.stream()
        .map(o -> Json.toJson(o.findings()))      // 不含 reasoning
        .collect(joining("\n---\n"));
}

汇总上下文从 8,400 token 降到 1,520 token。reasoning 我们仍然保留——它存在审计日志里,复盘时能看,只是不占 token。

原因三:无意义的重试轮次

Supervisor 的重试逻辑一开始写得太宽松:

// 问题版本:只要觉得"不够充分"就再来一轮
while (round < 3 && !supervisor.isSatisfied(results)) {
    results.addAll(dispatchMoreTasks(...));
    round++;
}

isSatisfied 是让模型自己判断,结果它几乎永远觉得「还能再查查」。统计下来平均 2.7 轮,其中 1.7 轮是没必要的。

改成基于明确缺口的判断:Supervisor 不说「满不满意」,而是必须列出「还缺什么具体信息」,列不出来就结束。

public record RoundDecision(
    boolean needAnotherRound,
    List<InformationGap> gaps,     // 必须具体:「缺少 2024 年同类合同的实际赔付金额」
    String justification
) {}

boolean shouldContinue(RoundDecision d) {
    // 硬约束:拿不出具体缺口就必须停
    if (!d.needAnotherRound()) return false;
    if (d.gaps().isEmpty()) {
        log.warn("supervisor requested another round without specific gaps, stopping");
        return false;
    }
    // 缺口必须能映射到某个 Agent
    return d.gaps().stream().anyMatch(g -> router.canHandle(g));
}

这一改,平均轮次从 2.7 降到 1.2,成本降了 19%。而且召回率只掉了 0.4pp——说明那些多出来的轮次基本没产生有效信息。

原因四:小模型输出不达标导致返工

我们把「条款抽取」和「合规比对」配了 qwen-plus 省钱,结果它们的输出经常不符合 schema,Supervisor 要求重做。平均 1.4 次返工/任务,等于付了 2.4 倍的钱。

这个账要重新算:

方案单次成本返工率期望成本
qwen-plus¥0.0958%¥0.09 × 2.4 = ¥0.216
qwen-max¥0.317%¥0.31 × 1.07 = ¥0.332

看起来 plus 还是便宜。但返工不只是钱的问题——每轮返工增加 3~5 秒延迟,而且返工后质量仍不稳定。

我们最后的方案不是换模型,是加校验和修复:子 Agent 输出后先用代码校验 schema,不合格的走一次廉价的修复调用(只发错误信息让模型改,不发原文):

String fixOutput(String badOutput, List<Violation> violations) {
    // 只发错误和输出,不重发原文,成本极低
    return cheapModel.call("""
        以下 JSON 不符合要求,错误是:
        %s

        原始输出:
        %s

        只输出修正后的 JSON,不要解释。
        """.formatted(describe(violations), badOutput));
}

修复调用平均 420 个 token,成本 ¥0.001。用这个替代整轮返工后,期望成本降到 ¥0.09 × 1.58 + ¥0.001 × 0.58 ≈ ¥0.143,比换 qwen-max 还便宜。

冲突解决:子 Agent 结论不一致怎么办

这是多智能体特有的、单 Agent 不会遇到的问题。我们统计了 1,200 份合同的处理记录,23% 存在至少一处子 Agent 结论冲突

典型例子:风险识别 Agent 说「第 8 条违约责任过重,高风险」,合规比对 Agent 说「第 8 条符合行业标准,无异常」。

处理策略我们按冲突类型分了三种:

冲突类型检测方式处理占比
同一条款风险等级不同按 (条款号, 风险等级) 分组比对交给仲裁 Agent,用强模型重审64%
事实性矛盾(金额、日期)规则抽取数值比对回原文重新抽取,取证据充分的一方28%
表述冲突但结论一致语义相似度 > 0.8 但措辞相反忽略,由 Supervisor 统一表述8%
// 冲突检测
List<Conflict> detect(List<AgentOutput> outputs) {
    Map<String, List<Finding>> byClause = outputs.stream()
        .flatMap(o -> o.findings().stream())
        .collect(groupingBy(Finding::clauseNo));

    return byClause.entrySet().stream()
        .filter(e -> e.getValue().stream()
                       .map(Finding::riskLevel).distinct().count() > 1)
        .map(e -> new Conflict(e.getKey(), e.getValue()))
        .toList();
}

// 仲裁:只把冲突的那几条送给强模型,不重发全文
Finding arbitrate(Conflict c, String clauseText) {
    return strongModel.call(ARBITRATION_PROMPT, c.candidates(), clauseText);
}

仲裁 Agent 的调用成本很低(平均 1,100 token),因为只处理冲突的那几条。34% 的仲裁结果推翻了原结论,说明这一步是必要的——不是走过场。

另外我们把冲突率做成了一个监控指标。某个子 Agent 的结论被推翻的比例如果持续偏高(我们设的阈值是 40%),说明它的 prompt 有问题,需要调优。这个功能帮我们发现了「合规比对」Agent 的一个系统性错误(它把「行业标准」理解成了错误的版本)。

成本控制的其他手段

除了上面四项,还加了两个通用的:

public class CostGuard {

    private static final BigDecimal TASK_BUDGET = new BigDecimal("1.20");

    // 1. 单任务预算:超了就降级(换小模型)或中止
    void check(String taskId) {
        BigDecimal spent = costTracker.current(taskId);
        if (spent.compareTo(TASK_BUDGET.multiply(BigDecimal.valueOf(0.7))) > 0) {
            degradeToCheaperModel(taskId);      // 70% 预算:降级
        }
        if (spent.compareTo(TASK_BUDGET) > 0) {
            abort(taskId, "超出成本预算,转人工");   // 100% 预算:中止
        }
    }

    // 2. 并发限制:最多 3 个子 Agent 并行
    ExecutorService executor = Executors.newFixedThreadPool(3);
}

并发限制这一项是为了避免「同时开 8 个 Agent 各自跑」的情况。我们测过,并发 4 个以上时,单个 Agent 的响应时间会因为 GPU 排队显著变长,总耗时反而增加。

预算触发率:降级 8.3%,中止 1.1%。中止的那部分全部转人工,这是我们必须付出的成本——但 1.1% 的比例在可接受范围。

最终结果

指标单 Agent多 Agent(初版)多 Agent(优化后)
审查项召回率82.1%93.4%92.8%
误报率7.2%5.1%5.4%
单份成本¥0.34¥2.17¥0.71
平均耗时42s187s94s
月成本(900 份/天)¥9,180¥58,590¥19,170

相对单 Agent,多智能体版本的召回率提升了 10.7 个百分点,成本是 2.1 倍。我们认为是划算的——一份合同漏审一个风险条款的代价,远高于多付的 0.37 元。

耗时还是比单 Agent 慢一倍(94s vs 42s)。这个我们接受了,因为合同审查是异步批处理场景,用户提交后等通知,不阻塞。

什么时候不该用多智能体

这轮做下来,我对多智能体的适用边界有了更清楚的认识。以下情况不建议上:

  • 单 Agent 上下文装得下。如果一份文档 8,000 token 就能塞进去,拆成四个 Agent 纯属浪费。我们的合同是 2.8 万 token,确实装不下,这是拆分的正当理由。
  • 子任务之间高度耦合。我们试过把「风险识别」拆成「财务风险」和「法务风险」两个 Agent,发现两者经常需要互相的信息,来回传递的成本超过了并行收益,后来合并了。
  • 对延迟敏感。多轮委派 + 冲突仲裁,耗时大概率翻倍以上。
  • 团队还没有单 Agent 的完整监控。多智能体的调试难度是数量级的提升。如果没有 token 追踪、决策日志、效果评测这些基础设施,出了问题根本无从下手。

最后一条是我最想强调的。我们这次能快速定位成本问题,是因为一上线就有按 Agent 维度的 token 统计。如果没有这个,我们可能要花一个月才发现「子 Agent 重复读全文」这个占了 41% 成本的问题。

小结

多智能体不是「更强的 Agent」,它是用成本换效果的一种架构选择。在我们的案例里,2.1 倍成本换 10.7 个百分点的召回率提升,这笔账算得过来。但如果当初没有做那轮成本优化(6.4 倍成本),这个项目大概率会被砍掉。

如果要我给一个最重要的建议,是这一条:多智能体的成本控制,必须在架构设计阶段就做,不能等上线后优化。重复传原文、汇总上下文爆炸、无意义重试——这三个占了 87% 的浪费,全都是可以在设计时避免的,只需要在写第一行代码前问一句「每个 Agent 真正需要看到什么」。

顺带说一句,今年 Java 的 Agent 框架生态变化挺大,Embabel、AgentScope Java 这些都冒出来了,Spring AI 也在往这个方向演进。我们这版是自己手写编排逻辑的(大约 2,300 行),如果有机会重做,我会先认真评估一下现成框架。不过话说回来,自己写的好处是成本控制的每一步都在自己手里,用框架的话,这些优化点未必都能改得到。

参考