多智能体(Agent)不靠运气 ,一个受控协作系统的设计复盘

本文基于 AI Mind 项目的真实实现整理。

GitHub:github.com/HWYD/ai-min...

对应代码版本:v0.4.11 线上体验:ai.hwyblog.cloud/instant-min...

AI Mind 是一个持续迭代中的 Next.js AI Chat 项目。从最基础的本地聊天开始,逐步加入流式协议、工具调用、MCP、Skill 和 Agent 能力。

如果你对这个项目感兴趣,或者这篇文章对你有一点帮助,也欢迎顺手到 GitHub 帮 AI Mind 点个 Star⭐,这会是对我继续更新很大的鼓励。

名词速览(第一次读也能跟得上)

  • 调度者(Supervisor)/ 评审 Agent(Reviewer)/ 方案 Agent(Plan Agent)/ 任务 Agent(Task Agent):分别是总指挥、挑问题的、写方案的、拆任务的。下文统称"智能体(Agent)"。
  • 运行时(Runtime):系统的"硬核裁判",用写死的代码逻辑做最终裁决,不被模型的措辞左右。
  • 大模型(LLM):指底层的大语言模型,负责生成内容、做出判断------但它说的话不一定稳定。
  • 契约(Contract)与结构定义(schema):契约是给每个智能体规定的"输出格式";结构定义(schema)是一种机器可校验的格式约定------字段类型、可选值、长度都被写死,且禁止出现未声明的字段。

系统概览:一条协作链路,一个核心难题

AI Mind 的"生成交付计划"功能接收一个需求描述,由多个智能体(Agent)协作产出一份交付计划(它包含两部分:可落地的「实现方案」与拆解后的「任务清单」)。整条链路是固定的:

  1. **调度者(Supervisor)**先判断输入是否充分;
  2. **方案 Agent(Plan Agent)**生成实现方案;
  3. **任务 Agent(Task Agent)**把方案拆成具体任务;
  4. **三个评审 Agent(Reviewer)**分别从方案一致性、风险、边界三个角度并行挑问题;
  5. 若评审发现必须修改的问题,对应的方案或任务会做一次返修;
  6. 最后生成一份完整的交付报告。

这条链上,每个环节的输出都是大模型生成的。而大模型的输出天然不稳定------同样的输入,两次调用可能得到措辞不同、结构不同甚至结论不同的结果。当多个智能体串联协作时,上游的输出就是下游的输入,这种不确定性会在链路上被层层放大。

本文要讨论的核心问题是:在这个前提下,怎么设计一套机制,让多个智能体协作产出的结果是可以信任的?

v0.4.11 的答案是:在智能体(Agent)的输出和系统决策之间,建立一套由运行时掌握的确定性控制层。这套控制层由三个递进的设计决策组成------先让每个智能体(Agent)的输出可被机器验证,再让关键安全规则不被绕过,最后让反馈闭环在可控范围内收敛。


决策一:让智能体(Agent)的输出可被机器验证

问题

智能体(Agent)输出的是自然语言,但这些文字里嵌入了业务结论------风险等级、边界状态、是否需要返修。如果直接用正则或关键词提取这些结论,智能体(Agent)换一种措辞,系统就可能做出不同的判断。

举个例子:风险评审 Agent 在正文中写"整体风险可控",和"存在中高风险",可能被映射到不同的风险等级,但实际上它想表达的是同一个结论。更糟的是,如果评审 Agent 在正文中额外加了一段说明,恰好触发了某个关键词匹配,系统就会误判。

方案

每个智能体(Agent)的输出不直接进入业务判断,而是先通过一个角色专属的严格结构定义(strict schema)校验。校验通过的结构化数据,才是系统决策的唯一事实来源。Markdown 正文保留,但只用于展示,不参与任何机器判断。

在代码中,这个设计由 agent-contracts.ts(智能体契约定义)和 contract-invocation.ts(契约调用边界)两个模块共同实现。

关键实现

第一,结构定义不做妥协。 每个角色的 schema 全部使用 .strict() 模式。如果智能体(Agent)输出包含了结构定义未声明的字段,整个结果被拒绝------不是删除未知字段后继续。因为未知字段意味着智能体(Agent)越过了自己的角色边界,静默删除会掩盖这种越权。

以评审 Agent 发现一个问题时的结构化输出为例:

typescript 复制代码
// agent-contracts.ts - 评审发现的结构定义
export const reviewFindingDraftSchema = z
    .object({
        description: boundedText(),           // 问题描述
        evidence: boundedTextList(10),         // 证据列表,最多 10 条
        findingType: z.enum(['issue', 'observation']),
        // 只有 issue 类型才会触发后续返修
        requirement: z.enum(['required', 'advisory']),
        // 只有 required 级别才影响最终状态
        severity: z.enum(['blocker', 'high', 'medium', 'low', 'info']),
        suggestedAction: boundedText(),        // 建议的处理动作
        targetArtifacts: z.array(revisionTargetSchema).min(1).max(2),
        // 指明需要修改哪个产物:plan 或 tasks
    })
    .strict()

上面这段代码用 Zod(一个常见的结构校验库)来描述格式。你不必纠结具体语法------只需抓住关键点:每个字段的类型、可选值和长度都被写死,且禁止出现未声明的字段。

注意 findingTyperequirement 这两个字段。它们不是随便定义的枚举------findingType 区分了"需要处理的问题"和"仅供参考的观察",requirement 区分了"必须修改"和"建议修改"。这两个字段直接决定了后续是否触发返修、以及最终状态是什么。如果这些信息藏在 Markdown 里靠关键词猜测,任何措辞变化都可能导致误判。

第二,一次修复,不是无限重试。 contract-invocation.ts 中的 invokeBusinessAgentContract 函数实现了这样的逻辑:首次校验失败时,给模型一次自我纠正的机会,但反馈只包含 {path, code} 摘要(比如 ["findings.0.severity", "invalid_enum_value"]),不暴露原始输出。第二次还失败就按阶段失败收口,不再继续尝试。如果两次都失败,说明问题不是格式而是能力,继续重试没有意义。

第三,生成和编码用不同模型。 业务判断由用户选择的模型完成("这个需求的风险等级应该是什么"),结构化编码固定使用 deepseek/deepseek-v4-pro。两者的职责和可靠性约束不同------业务模型负责创意和判断,编码模型负责严格遵守格式。用户换业务模型不会影响系统判断的稳定性。

取舍

每个智能体(Agent)阶段额外增加了一次模型调用来做结构化编码,用成本换可靠性。.strict() 的严格拒绝意味着一个合法字段的拼写错误就会导致整个结果失败------但这是故意的。对多智能体(Agent)协作系统来说,"静默接受一个意外字段然后做出错误判断"比"安全失败让用户重试"危险得多。


决策二:安全规则写在代码里,不写在提示词里

问题

这个系统有一些绝对不能违反的规则:三个评审 Agent(方案一致性、风险、边界)必须各执行一次,不能少也不能多;任何一个评审 Agent 判定为阻断(blocked)时,整个交付链都必须停止,不能被其他智能体(Agent)的"通过"结论覆盖;调度者(Supervisor)不能跳过方案或任务阶段直接进入评审。

如果把这些规则写在提示词里------"请务必调用三个评审 Agent""请尊重风险评审 Agent 的阻断结论"------智能体(Agent)可以忽略,可以误解,也可以被精心构造的输入诱导绕过。提示词是软约束,而安全规则需要硬保证。

方案

把所有安全规则从提示词层面移到运行时的硬编码逻辑中。智能体(Agent)只负责"建议",运行时负责"裁决"。在代码中,这个设计由 delegation-policy.ts(委派策略)和 report-synthesis.ts(报告合成)两个模块共同实现。

关键实现

第一,精确集合校验。 delegation-policy.ts 中的 validateExactReviewerRoles 函数在任何一个评审 Agent 启动之前,对调度者(Supervisor)声明的评审角色集合做精确计数:恰好三个角色各一次。不补齐缺失的、不删除多余的、不规范化顺序。

typescript 复制代码
// delegation-policy.ts - 精确集合校验
const REQUIRED_REVIEWER_ROLES = ['general', 'risk', 'boundary']

export function validateExactReviewerRoles(reviewerRoles) {
    if (reviewerRoles.length !== REQUIRED_REVIEWER_ROLES.length) {
        return { message: '...', summary: '调度者声明的评审角色集合不完整。' }
    }
    const counts = new Map()
    for (const role of reviewerRoles) {
        counts.set(role, (counts.get(role) ?? 0) + 1)
    }
    if (REQUIRED_REVIEWER_ROLES.some(role => counts.get(role) !== 1)) {
        return { message: '...', summary: '调度者声明的评审角色集合不完整或重复。' }
    }
    return null  // 校验通过
}

这个函数看起来很简单------一个 Map 计数,三次判断。但它的存在本身就是设计决策:调度者(Supervisor)在提示词中声明了评审角色集合,但运行时不信任这个声明,必须再次校验。声明是"意图",校验是"执法"。如果调度者(Supervisor)的声明不合法,整个评审调度被拒绝,三个评审 Agent 都不会被启动。

第二,纯函数状态矩阵。 report-synthesis.ts 中的 resolveReviewBundleStatus 函数用纯函数(同样的输入永远得到同样的输出,没有任何随机性)计算最终状态。输入只有结构化覆盖率(三个评审 Agent 各自是否执行成功)和结构化评审结果,不依赖任何大模型文本。

typescript 复制代码
// report-synthesis.ts - 确定性状态计算
export function resolveReviewBundleStatus(bundle): RunStatus {
    const coverage = Object.values(bundle.coverage)
    const completed = coverage.filter(state => state === 'completed').length
    if (completed === 0) return 'failed'
    // 三个评审 Agent 全部执行失败 → 系统失败

    const hardBlocked =
        (general?.disposition === 'blocked') ||
        (risk?.severity === 'blocker') ||
        (boundary?.boundaryStatus === 'blocked')
    if (hardBlocked) return 'blocked'
    // 任一硬阻断 → 阻断

    if (completed !== 3) return 'needs_review'
    // 覆盖不完整 → 需要人工复核

    if (general?.disposition === 'needs_changes' ||
        general?.planTaskAlignment === 'misaligned' ||
        bundle.findings.some(f => f.findingType === 'issue' && f.requirement === 'required'))
        return 'needs_changes'
    // 有必改问题 → 需要修改

    return 'pass'
}

优先级链清晰且不可变:执行失败 > 硬阻断 > 覆盖不完整 > 有必改问题 > 通过。这个函数不读任何 Markdown,不调任何正则,不依赖任何模型输出。相同的输入永远产生相同的输出。

第三,硬规则不可覆盖。 风险评审 Agent 返回 blocker(致命阻断级)时,无论其他评审 Agent 给出什么结论,系统状态必须是 blocked(阻断)。边界评审 Agent 返回阻断同理,方案一致性评审 Agent 返回阻断同理。调度者(Supervisor)不能降级这个结论,报告生成过程不能忽略它,任何后续智能体(Agent)的输出都不能覆盖它。

取舍

硬编码的关卡(Gate)意味着灵活性受限------不能根据场景动态决定"今天少审一个边界检查"。但安全关卡的完整性不应该是"灵活"的。在这个设计里,我们选择了"确定但受限"而不是"灵活但不可验证"。


决策三:反馈闭环的收敛控制

问题

评审发现的问题需要修复------这是多智能体(Agent)协作的核心价值。但问题来了:谁来决定修什么?修完了谁来判断修得对不对?如果修完再审、审完再修,这个循环在哪停下?

假设让调度者(Supervisor)来决定"修什么"------调度者(Supervisor)理解评审意见,然后告诉方案 Agent(Plan Agent)和任务 Agent(Task Agent)要怎么改。这看起来合理,但调度者(Supervisor)自己也是大模型,它可能误解评审意见,可能遗漏关键问题,可能把不相关的东西也塞进返修范围。如果返修后再让三个评审 Agent 重新评审一次,又会发现新问题,然后又返修,然后又评审......循环次数和成本都无法控制。

方案

返修目标由运行时从已验证的结构化评审发现中直接派生,不依赖调度者(Supervisor)的判断。最多一次返修,返修后不执行第二轮评审,把最终判断交还给人。

在代码中,这个设计由 structured-delivery-manager.ts(结构化交付管理器)中的 derivePostReviewDecision 函数和返修编排逻辑共同实现。

关键实现

第一,返修由运行时派生,不是调度者决定。 derivePostReviewDecision 函数的核心逻辑是:从已验证的评审发现中过滤出 findingType === 'issue'requirement === 'required' 的条目,然后根据每个条目的 targetArtifacts 字段(指明需要修改方案还是任务)分组,生成对应的返修请求。

typescript 复制代码
// structured-delivery-manager.ts - 运行时派生返修决策
export function derivePostReviewDecision(bundle, guidance?) {
    const actionableFindings = bundle.findings.filter(
        f => f.findingType === 'issue' && f.requirement === 'required'
    )
    if (actionableFindings.length === 0) return { action: 'finalize' }
    // 没有必改问题 → 直接定稿

    const requests = (['plan', 'tasks']).flatMap(target => {
        const findings = actionableFindings.filter(
            f => f.targetArtifacts.includes(target)
        )
        if (findings.length === 0) return []
        return [{
            requestKey: `runtime-${target}-revision`,
            sourceFindingIds: findings.map(f => f.findingId),
            targets: [target],
            // 调度者的 guidance 只影响说明文字,不改变动作
        }]
    })
    return { action: 'revise', requests, revisionTargets: requests.map(r => r.targets[0]) }
}

调度者(Supervisor)在评审后会提供一个可选的 guidance(返修说明),但它只能影响返修请求中的 requiredActionssummary 文字内容,不能改变"是否返修"和"返修什么"。这两个决策完全由运行时从结构化数据中派生------没有必改问题就不返修,发现说改方案就只改方案,发现说改任务就只改任务,两者都涉及就方案先改、任务再对齐。

第二,一次返修,不复审。 返修路径中方案先于任务执行(保持依赖顺序),返修产物保留稳定的标识符和递增的版本号,返修结果(RevisionOutcome)通过 findingId 追溯到原始评审发现,但不声明"问题已解决"。最关键的是返修后的收口逻辑:

typescript 复制代码
// structured-delivery-manager.ts - 返修后的硬编码收口
// 本版本在一次受控返修后结束;没有独立复评时不得宣称通过。
runStatus = 'needs_review'

系统不会启动第二轮评审组,不会产生第二次返修。内部状态固定为 needs_review(需要人工复核),不会标记为 pass(通过)。

第三,把最终判断交还给人。 最终报告中,返修后的下一步始终是"请人工确认本次返修后的方案与任务"。系统负责发现问题、派生返修、执行修改,但不负责判断修改是否正确。这个判断交还给人。

取舍

这是整个系统设计中最核心的取舍。我们承认了两件事:第一,大模型自评不可靠------让评审 Agent 重新评审自己刚改过的产物,和让学生自己批改自己的作业没有本质区别;第二,自动循环不可控------多轮"评审→返修→评审"的收敛条件难以定义,成本也无法预测。

所以系统的定位是:系统负责"改",人负责"判断改得对不对"。 这不是功能的缺失,而是对系统边界的清醒认知。


当前边界:刻意不做的事

这个版本明确划定了能力边界,以下事情刻意不碰:

  • 不做 ReAct(推理---行动)循环。 工作节点没有探索工具,问题和资源边界已知。引入"思考---行动---观察"循环只会增加调用成本和不可控状态,不产生本版所需价值。
  • 不做多轮"评审→返修→评审"循环。 最多一次返修,返修后不进第二轮评审。让大模型反复评审自己的输出是收敛不了的循环。
  • 不做通用 DAG(有向无环图)调度器。 拓扑是固定的:调度者(Supervisor) → 方案 Agent(Plan Agent) → 任务 Agent(Task Agent) → 三个评审 Agent(Reviewer)并行 → 可选返修。不引入动态依赖图或任意并行组。
  • 不做持久化。 所有调度计划、产物、评审包和评审发现只存在于单次运行内,运行结束即丢弃。
  • 不做大模型最终润色。 最终报告由已验证的结构化数据确定性渲染,不依赖额外的模型调用来"美化"。

这些边界不是"还没做",而是"判断后决定不做"。它们定义了系统的安全半径,也定义了下一版本可以扩展的方向。


总结

回到最开始的问题:多个智能体(Agent)协作时,怎么保证最终结果可信?

v0.4.11 给出的不是"让模型更准"的答案,而是"让系统更可靠"的答案。三层设计各自承担不同的职责:

  • 结构化信任确保每个智能体(Agent)的结论是机器可验证的,不会因为措辞变化被误判;
  • 硬性关卡确保安全规则不被绕过,大模型的建议不会变成系统的裁决;
  • 有限反馈确保反馈闭环在可控范围内收敛,不陷入"自动修复→自动复评"的不可靠循环。

这三个设计决策将控制权从大模型手中拿回,交给运行时的确定性逻辑。大模型仍然是协作者------它负责生成内容、做出判断、提供建议------但最终的决定权在运行时手里。

为了验证这套设计,我们用同一套固定评测样本跑了三条基线对比:①单智能体(Agent)直接生成(完全不协作);②固定多智能体(Agent)、但评审发现问题后不回流修改;③v0.4.11 的受控反馈闭环(即本文方案)。结果证明,结构化契约和硬性关卡在成本增加可控的前提下,显著提升了状态判断的一致性和安全规则的可靠性。这不是一个完美的系统,但它的可靠性是可以被验证的。

后续版本将继续扩展智能体(Agent)的边界------但起点是当前已经落地的确定性控制层。只有运行时能可靠地管理智能体(Agent)的输出边界,我们才敢让智能体(Agent)做更多的事。


项目地址

👉 GitHub:github.com/HWYD/ai-min...

👉 线上体验:ai.hwyblog.cloud/instant-min...

如果这篇文章或者 AI Mind 项目对你有所帮助,也欢迎顺手帮项目点个 Star⭐。这个支持对我来说很重要,也会让我更有动力继续整理后续版本的实现过程、设计取舍和踩坑复盘。

相关推荐
明明如月学长1 小时前
6 个 WorkBuddy 高阶功能,搞定复杂的办公任务
aigc·agent
枫叶V1 小时前
Prompt Injection 防不住怎么办?从 Source-Sink 模型设计 Agent 安全边界
后端·agent
5pat54OfCg1 小时前
OpenCode 源码拆解(二):Token 怎么省?——上下文管理的 5 个设计模式
agent
晨曦中的暮雨1 小时前
Learn Claude Code:CodeAgent 与后端程序大搭配——定时任务、Git Worktree 和MCP 服务
git·ai·llm·agent
chaors3 小时前
DeepResearchSystem 0x07:搜索缓存
llm·agent·ai编程
一个处女座的程序猿3 小时前
AI之Interview:Claude Code之父Boris Cherny深度访谈—删除80%提示词、产品悬余、解缚思维与AI编程的范式转移
agent·claude·harness
星栈14 小时前
oh-my-pi工程级AI编码工使用体验
人工智能·后端·agent
周末程序猿14 小时前
LLM智能路由实践:通过 Harness 工程节约模型成本
人工智能·agent