很多团队第一次把单 Agent 改成 Multi-Agent,代码看起来会突然"高级"很多:Planner 负责拆任务,Researcher 负责查资料,Coder 负责改代码,Reviewer 负责挑错,最后再交给 Publisher 输出结果。Demo 阶段这套东西很顺,甚至会给人一种错觉:只要把职责拆细,系统就自然更可靠。
真上生产后,最容易坏的地方恰恰不是某一个 Agent 的 prompt,而是 Agent 和 Agent 之间的交接。上游说"我已经完成了",下游接到的是一坨聊天记录;Reviewer 说"缺少证据",Coder 说"证据在前面上下文里";Publisher 发现封面、摘要、标签都没有,却又不知道应该退回给谁。最后日志里只剩一句很无辜的话:handoff failed。
这篇文章讨论一个很窄但很关键的问题:Multi-Agent 系统里的 handoff,不能只是把上一轮消息塞给下一个 Agent,而应该像后端服务之间的 API 一样,有一份明确的 Handoff Contract。
我会把它拆成五层:上下文预算、状态机、证据包、权限边界、验收回执。文章里的代码是一个可运行的 TypeScript 最小实现,重点不是框架炫技,而是把"交接"这件事从玄学 prompt 变成可检查、可回放、可拒收的工程协议。
一、为什么 Multi-Agent 最先坏在交接处
单 Agent 的失败通常比较直接。用户给一个任务,Agent 输出错了,你能围绕 prompt、工具调用、检索结果和模型能力排查。Multi-Agent 的失败更麻烦,因为错误会沿着交接链路传播。上游漏了一个约束,下游可能会基于错误状态继续加工;中间 Agent 误解了任务边界,最后输出看起来完整,但其实已经偏离原始目标。
近期很多 Agent SDK 和编排文档都会强调几个能力:规划、工具调用、专家协作、状态保持、可观测性、评估与人工审核。这些能力都重要,但它们中间缺了一层很容易被低估的东西:协作双方到底交接了什么。
在真实项目里,我更愿意把 Multi-Agent 失败分成五类。
第一类是上下文丢失。Planner 把任务拆成三个子任务,但只把当前子任务标题传给 Researcher,没有传原始用户目标、约束条件和禁止事项。Researcher 完成了局部最优,整体却错了。
第二类是状态污染。某个 Agent 在中间修改了任务定义,比如把"生成草稿"变成"发布文章",下游拿到的状态已经包含危险动作,却没有任何地方记录这个动作是谁授权的。
第三类是权限漂移。上游只被允许读文件,下游却继承了发布权限;或者 Coder 可以执行命令,Reviewer 也莫名其妙拿到了执行命令的能力。
第四类是证据断链。Researcher 引用了来源,Coder 或 Writer 在改写时删掉了来源映射。最终 Reviewer 发现文章里有结论,但找不到哪条证据支撑。
第五类是验收缺失。下游接到任务后默认继续执行,即使输入不完整也硬着头皮补。系统表面上"自动化程度很高",实际是在把错误悄悄吞下去。
这些问题不是靠"请你认真交接"能解决的。Agent 不是公司同事,不会天然理解组织流程;它只会根据当前输入做下一步。你要让它可靠交接,就必须把交接协议写进数据结构、状态机和运行时检查里。
二、Handoff Contract 的最小定义
一个生产可用的 Handoff Contract 至少要包含八类字段。
- task:这次交接要完成什么,不是整个项目愿景,而是可执行任务。
- owner:上游是谁,下游是谁,谁对交接输入负责。
- input bundle:下游必须读取的输入包,包括原始目标、当前子任务、约束、已完成产物。
- state cursor:任务状态游标,说明这是 planned、researched、drafted、reviewed 还是 ready_to_publish。
- permission scope:下游允许做什么,不允许做什么。
- evidence:支撑当前结论的证据包,可以是 URL、文件路径、日志片段、测试结果、截图。
- acceptance criteria:下游接收这次交接前必须检查什么。
- receipt:下游接收、拒收或要求补件的回执。
注意,这不是把聊天记录结构化一下那么简单。聊天记录是过程,合同是边界。聊天记录可以很长、很乱、包含废话;合同必须短、明确、可验证。
LangGraph 的 memory 文档把短期记忆定义为 thread-scoped,把长期记忆定义为跨 thread 的 namespace store。Mem0 这类记忆层也强调 Add、Extract、Recall。它们解决的是"系统如何记得东西",不是"两个 Agent 交接时谁对输入质量负责"。所以不要把 memory 当 handoff contract。Memory 可以是证据来源,contract 才是交接协议。
三、一个可运行的 TypeScript 合同模型
下面是一个最小实现。它没有依赖复杂框架,只表达三件事:交接输入必须过 schema;状态流转必须合法;接收方可以拒收。
ts
type AgentRole = 'planner' | 'researcher' | 'writer' | 'reviewer' | 'publisher';
type HandoffState =
| 'planned'
| 'researched'
| 'drafted'
| 'reviewed'
| 'ready_to_publish';
type Permission =
| 'read_sources'
| 'write_artifact'
| 'run_tests'
| 'publish_external';
interface EvidenceItem {
id: string;
kind: 'url' | 'file' | 'log' | 'test' | 'screenshot';
ref: string;
claim: string;
capturedAt: string;
}
interface HandoffContract {
id: string;
task: string;
from: AgentRole;
to: AgentRole;
state: HandoffState;
originalGoal: string;
constraints: string[];
artifacts: string[];
permissions: Permission[];
evidence: EvidenceItem[];
acceptanceCriteria: string[];
contextBudget: {
maxTokens: number;
summaryRequired: boolean;
mustInclude: string[];
};
}
interface HandoffReceipt {
handoffId: string;
receiver: AgentRole;
status: 'accepted' | 'rejected' | 'needs_more_input';
missing: string[];
acceptedAt: string;
}
这份结构里最容易被忽略的是 contextBudget。很多 Multi-Agent 系统会把"上下文窗口足够大"误解成"可以不做上下文治理"。上下文越大,越需要预算。因为下游 Agent 不只是读上下文,它还会被上下文里的旧任务、废弃结论、未确认假设干扰。
我的经验是,handoff 输入要分三层。第一层是 10 行以内的任务合同,下游必须优先读。第二层是结构化证据包,下游按需展开。第三层才是完整过程日志,只用于排障和回放,不应该直接塞进主 prompt。
四、状态机:禁止"看起来完成了"
Handoff Contract 必须绑定状态机,否则它只是一份漂亮 JSON。状态机的价值在于,它能禁止一些危险捷径。比如没有 research evidence 的草稿不能进入 reviewed;没有 review receipt 的文章不能进入 ready_to_publish;没有 publish 权限的 Agent 不能触发外部发布。
ts
const allowedTransitions: Record<HandoffState, HandoffState[]> = {
planned: ['researched'],
researched: ['drafted'],
drafted: ['reviewed'],
reviewed: ['ready_to_publish'],
ready_to_publish: [],
};
function canMove(from: HandoffState, to: HandoffState): boolean {
return allowedTransitions[from]?.includes(to) ?? false;
}
function assertTransition(prev: HandoffContract, next: HandoffContract) {
if (!canMove(prev.state, next.state)) {
throw new Error(`illegal handoff transition: ${prev.state} -> ${next.state}`);
}
if (next.permissions.includes('publish_external') && next.to !== 'publisher') {
throw new Error('publish_external can only be granted to publisher');
}
if (next.state === 'reviewed' && next.evidence.length === 0) {
throw new Error('reviewed handoff requires evidence');
}
}
这段代码很朴素,但它能挡住一类高频事故:Agent 自己宣布"我已经 review 过了"。在生产系统里,状态不是自然语言声明,而是运行时认可的结果。
如果你用工作流引擎,状态机可以放在 Temporal、Step Functions、LangGraph 或自研 orchestration 层里。如果你只用普通队列,也可以把状态机放在消费者入口。关键不是选哪个工具,而是不要把状态流转交给模型自由发挥。
五、接收方必须有权拒收
很多 Agent 编排失败,是因为下游没有拒收机制。只要上游输出了,下游就继续。这样看似自动化,实际上是在制造"沉默失败"。
接收方应该在执行前先做 acceptance check。
ts
function receiveHandoff(contract: HandoffContract): HandoffReceipt {
const missing: string[] = [];
if (!contract.originalGoal.trim()) missing.push('originalGoal');
if (contract.constraints.length === 0) missing.push('constraints');
if (contract.acceptanceCriteria.length === 0) missing.push('acceptanceCriteria');
for (const required of contract.contextBudget.mustInclude) {
const inArtifacts = contract.artifacts.some(a => a.includes(required));
const inEvidence = contract.evidence.some(e => e.claim.includes(required));
if (!inArtifacts && !inEvidence) missing.push(`mustInclude:${required}`);
}
if (contract.state === 'researched' && contract.evidence.length < 2) {
missing.push('at_least_two_evidence_items');
}
return {
handoffId: contract.id,
receiver: contract.to,
status: missing.length ? 'needs_more_input' : 'accepted',
missing,
acceptedAt: new Date().toISOString(),
};
}
拒收机制会让系统短期显得"不够丝滑",但长期会显著减少脏数据传播。生产系统最怕的不是失败,而是不知道什么时候已经失败。一个明确的 needs_more_input,比一个看似完整但实际错误的下游结果便宜得多。
六、证据包:不要让结论裸奔
Multi-Agent 系统里,证据包比普通 RAG 引用更重要。RAG 引用通常服务于回答质量,handoff evidence 还承担责任追踪。
一个好的 evidence item 不应该只是 URL。它至少要说明这条证据支撑哪一个 claim、什么时候捕获、来自哪个产物。否则 Reviewer 看到一堆链接,仍然不知道该检查什么。
ts
const evidence: EvidenceItem = {
id: 'ev-langgraph-memory-001',
kind: 'url',
ref: 'https://docs.langchain.com/oss/python/concepts/memory',
claim: '短期记忆是 thread-scoped,长期记忆可跨 thread namespace 召回',
capturedAt: '2026-08-14T08:05:00+08:00',
};
在文章写作、代码生成、数据分析这类链路里,我建议把 evidence 分成三类。
第一类是 source evidence,证明事实来源。第二类是 execution evidence,证明某个命令、测试或脚本确实跑过。第三类是 decision evidence,记录为什么选择 A 而不是 B。
很多团队只存第一类,所以 Review 阶段只能检查"有没有来源"。但生产事故常常来自第三类:当时为什么跳过安全检查?为什么把 publish 权限交给这个 Agent?为什么没有等人工审批?如果没有 decision evidence,事后只能翻聊天记录,排障成本会非常高。
七、权限边界:handoff 不能继承一切
Multi-Agent handoff 里最危险的默认值,是"下游继承上游全部能力"。这在内部 demo 里方便,在生产里很危险。
Publisher 需要 publish_external,Writer 不需要;Researcher 需要 read_sources,不一定需要 write_artifact;Reviewer 需要读证据和产物,不应该拥有发布权限。权限应该跟任务阶段绑定,而不是跟整个会话绑定。
可以用一个简单的权限矩阵做第一层防护。
ts
const rolePermissions: Record<AgentRole, Permission[]> = {
planner: ['write_artifact'],
researcher: ['read_sources', 'write_artifact'],
writer: ['write_artifact'],
reviewer: ['read_sources', 'run_tests'],
publisher: ['read_sources', 'write_artifact', 'publish_external'],
};
function assertPermissionScope(contract: HandoffContract) {
const allowed = new Set(rolePermissions[contract.to]);
const illegal = contract.permissions.filter(p => !allowed.has(p));
if (illegal.length) {
throw new Error(`${contract.to} received illegal permissions: ${illegal.join(',')}`);
}
}
更稳的做法是运行时也隔离权限。比如不同 Agent 使用不同工具白名单,不同外部 API key,不同文件系统 mount。Handoff Contract 只能声明权限,真正的权限收敛必须由 runtime 执行。
这点尤其适合发布、支付、删除、发消息、改配置这类外部动作。只要动作会离开本机或影响真实用户,就不能因为"上游说可以"而直接执行。
八、上下文预算:交接不是塞满窗口
上下文工程最近很热,但在 handoff 场景里,我最看重的不是"如何塞更多",而是"如何少塞且不丢关键约束"。
一个实用规则是:handoff prompt 的前 20% 放合同,后 80% 才放资料。合同里必须包含原始目标、禁止事项、当前状态、验收标准。资料可以被截断,合同不能被截断。
你可以在生成下游 prompt 前做一次预算检查。
ts
function buildHandoffPrompt(contract: HandoffContract, rawContext: string) {
const contractBlock = JSON.stringify({
task: contract.task,
originalGoal: contract.originalGoal,
constraints: contract.constraints,
state: contract.state,
permissions: contract.permissions,
acceptanceCriteria: contract.acceptanceCriteria,
}, null, 2);
const maxContextChars = Math.floor(contract.contextBudget.maxTokens * 3);
const trimmedContext = rawContext.slice(0, maxContextChars);
return [
'你正在接收一个上游 Agent 的交接。先检查合同,再决定是否继续。',
'如果合同缺字段或证据不足,输出 needs_more_input,不要自行脑补。',
'HANDOFF_CONTRACT:',
contractBlock,
'SUPPORTING_CONTEXT:',
trimmedContext,
].join('
');
}
这段代码故意很保守。它没有让模型自己判断哪些上下文重要,而是先把合同字段固定在最前面。实际项目可以再加摘要器、reranker、semantic cache,但这些都是优化层,不应该替代合同层。
九、5 个生产陷阱与修复办法
第一个陷阱是把 handoff 写成自然语言。比如"我已经完成调研,请你继续写作"。这句话没有状态、没有证据、没有验收标准。修复办法是:自然语言可以保留给人看,但机器执行必须读结构化合同。
第二个陷阱是让下游自动补缺。下游发现缺约束,却自己猜一个约束继续。修复办法是:缺关键字段必须 needs_more_input;只有非关键字段可以使用默认值,而且默认值要写进 receipt。
第三个陷阱是证据只进正文,不进合同。Writer 写文章时引用了来源,但 Reviewer 的输入里只有成稿,没有 source map。修复办法是:evidence 应该独立存在,正文引用只是 evidence 的消费结果。
第四个陷阱是权限跟会话走。一个会话里曾经授权过发布,后续所有 Agent 都能发。修复办法是:权限跟 handoff state 和 receiver role 绑定,每次交接重新计算。
第五个陷阱是没有拒收指标。团队只统计任务完成率,不统计 handoff reject rate。修复办法是:把 accepted、needs_more_input、rejected 都做成指标。handoff reject rate 短期升高不一定是坏事,可能说明系统终于开始暴露脏输入。
十、Handoff Contract 和 Memory、Trace、Workflow 的关系
这四个东西经常被混在一起。
Memory 负责记住信息。它可以告诉系统用户偏好、历史选择、上次任务结果。但 memory 不负责证明这次交接完整。
Trace 负责记录发生过什么。它适合排障、审计、回放。但 trace 往往太长,不能直接作为下游执行输入。
Workflow 负责控制任务怎么流转。它能保证 A 后面是 B,B 后面是 C。但如果 A 给 B 的输入质量很差,workflow 也只是在稳定地传递垃圾。
Handoff Contract 负责定义一次交接是否合格。它站在 Memory、Trace、Workflow 中间,把"记得什么""发生了什么""下一步去哪"连接起来。
如果只能先做一件事,我建议从合同字段和拒收机制开始,而不是先搭复杂平台。因为很多 Multi-Agent 项目不是死于缺平台,而是死于每一步都在默认输入没问题。
十一、一个落地 checklist
最后给一个工程落地清单。你可以直接拿去改自己的 Agent 编排。
- 每个 Agent 输出必须包含结构化 handoff,不只输出自然语言。
- 每次 handoff 都要有 from、to、state、task、originalGoal。
- 原始用户目标和禁止事项必须进入合同前部,不能只留在聊天历史里。
- evidence 要独立成包,至少包含 ref、claim、capturedAt。
- 下游执行前必须做 acceptance check。
- 缺关键字段时必须拒收,不能自动脑补。
- publish、delete、payment、message、config change 等外部动作必须单独授权。
- 状态流转由 runtime 检查,不由模型声明。
- handoff receipt 要持久化,便于审计和重放。
- 指标里加入 handoff accept rate、reject reason、missing field topN。
结语
Multi-Agent 系统真正难的地方,不是把几个 Agent 串起来。串起来很容易,难的是让每一次交接都有边界、有证据、有验收、有责任。
如果你现在的 Agent 系统还停留在"上游输出一段话,下游继续接着做",那它更像一个聊天接龙,而不是一个工程系统。聊天接龙能做 demo,不能扛生产。
Handoff Contract 的价值,就是把 Agent 协作中最模糊的一段变成明确协议。它不会让模型突然变聪明,但会让系统在输入不完整、权限不对、证据不足、状态非法时及时停下来。很多时候,可靠系统和不可靠系统的差距,不在于前者永远不犯错,而在于前者知道什么时候应该拒绝继续。