本文基于
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)协作产出一份交付计划(它包含两部分:可落地的「实现方案」与拆解后的「任务清单」)。整条链路是固定的:
- **调度者(Supervisor)**先判断输入是否充分;
- **方案 Agent(Plan Agent)**生成实现方案;
- **任务 Agent(Task Agent)**把方案拆成具体任务;
- **三个评审 Agent(Reviewer)**分别从方案一致性、风险、边界三个角度并行挑问题;
- 若评审发现必须修改的问题,对应的方案或任务会做一次返修;
- 最后生成一份完整的交付报告。
这条链上,每个环节的输出都是大模型生成的。而大模型的输出天然不稳定——同样的输入,两次调用可能得到措辞不同、结构不同甚至结论不同的结果。当多个智能体串联协作时,上游的输出就是下游的输入,这种不确定性会在链路上被层层放大。
本文要讨论的核心问题是:在这个前提下,怎么设计一套机制,让多个智能体协作产出的结果是可以信任的?
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 发现一个问题时的结构化输出为例:
// 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(一个常见的结构校验库)来描述格式。你不必纠结具体语法——只需抓住关键点:每个字段的类型、可选值和长度都被写死,且禁止出现未声明的字段。
注意 findingType 和 requirement 这两个字段。它们不是随便定义的枚举——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)声明的评审角色集合做精确计数:恰好三个角色各一次。不补齐缺失的、不删除多余的、不规范化顺序。
// 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 各自是否执行成功)和结构化评审结果,不依赖任何大模型文本。
// 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 字段(指明需要修改方案还是任务)分组,生成对应的返修请求。
// 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(返修说明),但它只能影响返修请求中的 requiredActions 和 summary 文字内容,不能改变"是否返修"和"返修什么"。这两个决策完全由运行时从结构化数据中派生——没有必改问题就不返修,发现说改方案就只改方案,发现说改任务就只改任务,两者都涉及就方案先改、任务再对齐。
第二,一次返修,不复审。 返修路径中方案先于任务执行(保持依赖顺序),返修产物保留稳定的标识符和递增的版本号,返修结果(RevisionOutcome)通过 findingId 追溯到原始评审发现,但不声明"问题已解决"。最关键的是返修后的收口逻辑:
// 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⭐。这个支持对我来说很重要,也会让我更有动力继续整理后续版本的实现过程、设计取舍和踩坑复盘。