Multi-Agent Handoff 合同工程:别让 Agent 交接变成甩锅现场

很多团队第一次把单 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 协作中最模糊的一段变成明确协议。它不会让模型突然变聪明,但会让系统在输入不完整、权限不对、证据不足、状态非法时及时停下来。很多时候,可靠系统和不可靠系统的差距,不在于前者永远不犯错,而在于前者知道什么时候应该拒绝继续。

相关推荐
苏灿烤鱼2 小时前
14MB 模型,凭什么跟 270M 对打?
javascript·python·agent
mCell10 小时前
DeepSeek Harness 速览:“一切皆插件”意味着什么
typescript·agent·deepseek
__zRainy__10 小时前
ClaudeCode 源码深度剖析:从零读懂 Agent 架构与 MVP 最小骨架实现
架构·agent·源码解读·claude code
To_OC11 小时前
别死磕 Prompt 了!我用 Harness 流水线,让大模型自动产出高质量代码
人工智能·llm·agent
怕浪猫12 小时前
DeepSeek Harness 开发者预览版:一切皆插件
aigc·agent
破烂pan14 小时前
AI-Agent-Book第一章思考题
人工智能·agent
赵大仁15 小时前
Agent 安全:沙箱、权限、Prompt 注入与审计
ai·大模型·agent·ai安全·合规
Loveyourself16 小时前
Claude Code Memory 总体系核心代码逐行解析
面试·agent
demo007x16 小时前
CoT(Chain-of-Thought)
程序员·llm·agent