跳到主要内容

智能体间通信协议:让多智能体系统具备可调试性的接口契约

· 阅读需 13 分钟
Tian Pan
Software Engineer

当多智能体流水线(multi-agent pipeline)开始输出垃圾内容时,人们的直觉往往是归咎于模型。推理能力差、上下文错误、幻觉。但在实践中,很大一部分多智能体系统的失败源于更乏味的原因:智能体之间无法进行可靠的通信。格式错误的 JSON 虽然通过了语法验证,但无法通过语义解析。编排器(orchestrator)发送了一个状态为 "partial" 的任务,而下游智能体将其理解为已完成。由于缺少幂等键(idempotency key),重试操作触发了两次。

这些不是模型故障,而是接口故障。它们比模型故障更难调试,因为日志中没有任何信息会告诉你序列化契约(serialization contract)已经断裂。

对生产环境下多智能体系统故障的研究一致发现,智能体间通信崩溃是主要的故障类别之一——智能体在没有请求澄清的情况下处理模糊数据,智能体拥有相关信息却不分享,或者推理逻辑与执行动作相矛盾。这些问题大多是伪装下的协议问题。LLM 完全按照指令行事,但指令是以一种导致隐性错误行为的形式传达的。

本文旨在讨论如何设计消息契约来防止这种情况。我们不谈论理论上的协议,而是讨论决定你的多智能体系统在生产环境中是否具备可调试性的实际选择。

消息信封:真正关键的字段

每个智能体间的消息除了负载(payload)之外,还需要一个结构化的信封(envelope)。跳过这一步的团队在凌晨 2 点试图重放失败的工作流时,就会明白这有多重要。

以下是始终证明其价值的字段:

  • transaction_id:由发起智能体生成的 UUID,它会随请求在每一跳中传递。这是你关联跨智能体日志、在重试时检测重复项以及将故障追溯到源头的方式。没有它,你的分布式追踪就是死路一条。
  • sender_id:哪个智能体发送了此消息。这不仅是为了记录日志——下游智能体有时需要根据来源调整行为。相比 user_input 消息,researcher_agent 的输出需要不同的信任校准。
  • message_type:一个明确的意图字段(TASK_REQUESTTASK_RESULTCLARIFICATION_REQUESTESCALATION)。这使得接收端智能体无需解析完整负载即可进行路由。
  • protocol_version:基于日期的字符串,例如 2025-06-01。非破坏性更改不会增加此版本号。破坏性的架构更改则会。这个单一字段可以防止滚动部署导致部分智能体无法解析来自已升级对等智能体的消息。
  • status:与 message_type 不同。任务结果消息可能具有 COMPLETEPARTIALFAILEDNEEDS_CLARIFICATION 状态。将此作为一个显式的枚举字段(而不是埋在正文中),可以使编排器具备可编程性,而不需要另一个 LLM 来解释响应。
  • confidence:一个 0 到 1 之间的浮点数。在大多数系统设计中,这个字段往往被忽略,直到团队发现布尔值的成功/失败无法捕捉到足够的信号。置信度为 0.4 的智能体输出应与置信度为 0.95 的输出区别对待。

容易被遗漏并导致后续问题的包括:时间戳(用于排序和 TTL 强制执行)、区分并行子智能体调用中同一事务的相关 ID(correlation IDs),以及独立于协议版本的架构版本(你的负载架构可以独立于信封格式演进)。

错误信号:超越二元的成功与失败

在智能体间契约中,最难处理好的是故障通信。二元的成功/失败无法模拟智能体的真实经历。调研智能体可能会找到五个请求源中的三个,置信度为 0.6。代码智能体可能会生成一个通过单元测试但存在无法解决的类型错误(type error)的解决方案。这些都不能简单归类为“失败”。

生产系统至少需要四种不同的故障信号:

NEEDS_CLARIFICATION:智能体信息不足或含义模糊,无法继续。这并非故障,而是一个请求。响应契约应包含具体哪些内容不明确。如果没有这个信号,智能体要么会出现幻觉(在不标记不确定性的情况下选择一种解释),要么会默默失败。

PARTIAL_SUCCESS:智能体完成了部分任务。响应应包括已完成的内容、未完成的内容、每个已完成项的置信度评分,以及是否可以继续。这让编排器能够做出明智的决定:重试未完成的部分、升级给人工处理,或接受部分结果。

FAILED_RETRIABLE:瞬态故障。网络超时、速率限制、临时不可用。接收端智能体应在退避(backoff)后重试。

FAILED_PERMANENT:任务本身存在问题。输入无效、能力不匹配、违反策略。重试会产生相同的结果;需要升级或重新设计任务。

将这些信号混为一谈会对运行产生重大影响。如果编排器将 NEEDS_CLARIFICATION 视为 FAILED_PERMANENT,它就会放弃可恢复的任务。如果它将 FAILED_PERMANENT 视为 FAILED_RETRIABLE,它就会陷入无限重试循环。大多数智能体框架的默认实现只有一个故障状态,导致团队在生产环境的异常行为迫使问题浮现时,才不得不后期补救。

置信度阈值在这里非常重要。设置一个最低可接受置信度(例如 0.3)意味着低于该阈值的智能体会发出 NEEDS_CLARIFICATION 或 PARTIAL_SUCCESS,而不是编造一个高置信度的答案。这是一项系统级策略,而非单个智能体的决定——它需要在消息验证层中强制执行。

看起来像模型错误的序列化陷阱

在智能体间系统中,最隐蔽的失败是那些表面上看起来像模型输出错误的序列化问题。你在下一步看到了垃圾信息,并假设上游智能体的推理出了错。而实际问题在于输出本身是正确的,但其呈现形式让下游解析器无法处理。

Markdown 包裹。 当要求 LLM 返回 JSON 时,它经常会用代码栅栏将其包裹起来:```json\n{...}\n```。如果你的解析器期望得到的是原始 JSON,它就会失败。解决方法要么是在消息处理器中剥离栅栏(这很脆弱——LLM 在这方面的表现并不一致),要么是使用结构化输出 / 函数调用来强制生成原始 JSON。后者才是正确答案。当你可以通过结构化方式强制执行时,千万不要依赖提示词指令来正确格式化输出。

双重序列化。 当工具结果在转发给另一个智能体之前先传回给 LLM 时,LLM 有时会重新序列化已经序列化过的 JSON 字符串。接收智能体随后看到的是一个经过字符串转义的 JSON 对象,而不是解析后的对象。这在编排模式中尤为常见,即主管智能体在移交之前重新表述工具结果。解决方法是将工具结果作为类型化对象传递,而不是作为流经提示词上下文的字符串负载。

不可序列化类型。 Python 的 datetime 对象、字节流(bytes)、函数引用和 Pydantic 模型实例除非经过显式处理,否则都无法进行 JSON 序列化。如果一个智能体直接在消息中嵌入 datetime.now(),会导致任何调用 json.loads() 的接收端崩溃。解决方法是在出口处捕获这些异常的序列化中间件,而不是在入口处——你希望尽可能在靠近生成智能体的地方捕获错误。

部分部署下的模式偏移(Schema drift)。 当你发布新的智能体版本时,某些智能体将运行旧代码,而其他智能体运行新代码。如果你在版本中添加了一个必填字段,发送不带该字段消息的旧智能体会导致新的接收器失败。行之有效的模式是:始终将新字段添加为带有默认值的可选字段。永远不要删除字段——而是弃用它们。仅在必须破坏向后兼容性时才增加 protocol_version,并在滚动发布期间同时支持 N 和 N-1 版本。

所有这些问题的诊断迹象是:如果故障仅在跨越智能体边界的消息上显现,但在单个智能体内处理相同数据却运行正常,那么你遇到了序列化契约问题。

在不破坏运行流的情况下对智能体间契约进行版本控制

多智能体系统加剧了 API 版本控制问题。在微服务架构中,你对端点进行版本控制并更新客户端。在多智能体系统中,你正在对 LLM 的认知行为进行版本控制——而这种行为不仅通过你的部署发生变化,还会通过你未启动的供应商模型更新而变化。

生产环境下的智能体有四个独立的版本控制轴:

  1. 智能体逻辑版本:编排智能体行为的代码
  2. 提示词和策略版本:指令和护栏
  3. 模型运行时版本:底层的基座模型
  4. 工具和 API 接口版本:智能体调用的外部系统

大多数团队只管理 (1) 和 (4)。这就是为什么模型偏移导致了 40% 的生产环境下智能体故障——供应商在更新模型时没有宣布会破坏下游假设的行为变化。在智能体配置中显式锁定模型版本,并在 CI 中针对锁定版本进行测试是强制性的,而非可选。

专门针对智能体间的消息模式(schemas):

对于快速更迭的系统,基于日期的版本控制比语义化版本控制(semver)更有效。schema_version: "2025-09-01" 字段既传达了“这是生效的规范”,也传达了“我们在该日期前后进行了更改”——这在调试某条特定消息是否由早于模式更改的智能体产生时非常有用。

作为一种纪律的向后兼容性:添加字段,永不删除。当你需要删除一个字段时,在模式文档中将其标记为弃用,停止在新的智能体中读取它,但在迁移窗口期内继续生成它。这让你可以先于发送端升级接收端,这是安全的滚动发布顺序。

消息层的兼容性适配器:一个小的转换函数,在路由之前将旧格式的消息标准化为新格式。这能将兼容性逻辑集中在一处,而不是分散在每个接收智能体中。

经得起生产考验的接口模式

显式状态枚举优于自然语言。 一个返回 {"status": "done with some caveats"} 的智能体无法从代码中调用。而一个返回 {"status": "PARTIAL", "items_completed": 3, "items_total": 5, "confidence": 0.72} 的智能体则是可以调用的。一旦你需要另一个 LLM 来解释任务是否成功,你就失去了构建可靠编排的能力。

在每个改变状态的调用中使用幂等键。 每条导致写入的消息——发送电子邮件、创建记录、触发外部动作——都需要一个唯一的幂等键。编排器会在超时时重试,网络会分区,智能体会在操作中途崩溃。如果没有幂等性,你的重试逻辑就会变成一个重复动作生成器。该键应由 transaction_id 加上操作类型派生而来,从而为你提供事务内的每操作去重。

在入口处进行 Pydantic(或同等工具)验证,而不是在提示词中。 “请使用符合此模式的有效 JSON 进行响应”是一个软约束。带严格模式的 Pydantic 则是一个硬约束。在每个智能体的边界处验证消息,而不是在推理链的中间。快速失败并返回结构化的 FAILED_PERMANENT 错误,而不是尝试继续处理格式错误输入。

将模式与自然语言分离。 当智能体交流任务描述时,将结构化字段(状态、置信度、transaction_id、结果类型)保留在消息信封中,而将易于理解的解释放在单独的 summary 字段中。编排器读取结构化字段;人类调试系统时读取摘要。混淆这两者会迫使编排器解析自然语言——这既慢、又贵且脆弱。

记录完整的消息信封,而不仅仅是有效负载。 当你调试多智能体故障时,有效负载内容通常不如元数据提供的信息多:哪个智能体发送的、什么时候发送的、属于哪个事务、置信度是多少。如果你的日志设置剥离了信封而只保留有效负载,你将花费大量时间重新构建原本就已经存在的上下文。

更深层次的观点

智能体间协议故障之所以诊断不足,是因为它们在单智能体评估中是不可见的。你的评估之所以能通过,是因为每个智能体在接受格式良好的输入并进行隔离测试时,其表现都是正确的。只有当智能体相互协作时,故障才会显现 —— 即当一个智能体的输出变成另一个智能体的输入,且它们之间的隐性契约被证明是脆弱的时候。

在显式消息契约上的投入,在大规模应用时会获得不成比例的高额回报。一个由两个智能体组成的系统或许能容忍草率的接口;但一个拥有十个智能体且接口草率的系统简直就是一场调试噩梦。定义封包,在代码中强制执行 Schema,明确错误状态,并以对待公开 API 的严谨态度来处理智能体间的消息设计。LLM 会给你各种理由让你将故障归因于模型行为。不要接受这种借口。

References:Let's stay in touch and Follow me for more thoughts and updates