☰
多Agent通信格式设计:从Function Calling到MCP/A2A
2026/10/7 17:47:11 网站建设 项目流程

做Agent开发这几年,我的一个核心感受是:单Agent跑demo拼的是提示词,多Agent跑业务拼的是通信格式。见过太多项目前期顺风顺水,一进入多模块联调就翻车——不是模型智商不够,不是提示词写得不好,而是LLM和Agent实例之间传消息的方式没设计好。所谓应用层通信格式,指的就是LLM、Agent、工具服务之间交换的结构化数据契约:字段怎么定、JSON怎么校验、错误怎么处理、权限怎么控制、版本怎么兼容。很多人以为这只是个“格式问题”,实际上它是整个Agent系统的架构地基。

我会把实际项目里沉淀下来的格式设计思路、选型过程、踩过的坑,以及当前业界主流的Function Calling、MCP、A2A协议方案完整梳理一遍。这篇文章适合正在从单Agent Demo走向多Agent工程化的开发者,尤其适合那些已经感觉到“接口越来越乱、消息越来越看不懂、模型输出越来越不可控”的人。

1. 为什么应用层通信格式成了Agent工程的命门

先说结论:Agent系统本质上是一个分布式系统。分布式系统的每个节点之间必须有一套明确的通信协议,否则节点之间无法协同。只是这套协议里的“节点”不是服务器,而是LLM API、Agent实例、工具服务和人机交互入口。

1.1 Agent之间究竟在传什么

实际运行起来,Agent之间传递的载荷其实就是三类东西。

第一类是指令与意图。比如“查询一下用户订单状态”“把这份合同总结成三句话”“继续执行上一轮未完成的退款流程”。这类消息部分来自用户,部分来自上游Agent的决策结果。它的特点是语义密度高、结构松散,通常需要携带少量结构化参数。

第二类是工具调用与结果。模型决定调用哪个函数、传入什么参数,然后把调用结果回传。这是目前LLM应用里最成熟、也是出错率最高的通信链路,因为模型输出的参数列表是文本,下游解析一旦不兼容就直接断裂。

第三类是状态与上下文上下文。包括会话ID、用户ID、当前任务进度、历史消息摘要、token消耗统计等。这类消息最容易被新手忽略,但它决定了整个系统能不能断点续跑、能不能排查问题。

这三类消息的格式诉求完全不同:指令重语义,需要留自由文本空间;工具调用重结构,必须严格校验;状态传递重精炼,过度冗余就是烧钱。

1.2 通信格式设计上的三个核心矛盾

设计Agent间通信格式时,大家都会撞上三堵墙。

**灵活性与可校验性的矛盾。**字段定得太死,模型在边界场景下容易生成失败;字段放得太开,下游解析方就要面对大量脏数据。我目前的工程平衡点是“强约束核心字段 + 宽松扩展字段”,也就是信封结构严格校验,业务payload只约束关键字段。

**表达力与Token成本的矛盾。**消息越详细,模型理解越准确,可每个token都是实打实的成本,长上下文还会拉高响应延迟。这个矛盾在长链路的多Agent流水线里尤其突出,后面我专门讲怎么压缩。

**开放性与安全性的矛盾。**通信格式本质上是把模型输出当作可执行指令来路由,格式不设限意味着下游Agent或工具可能收到攻击性内容。通过消息Schema限制字段,是Agent系统安全的第一道闸门,比事后过滤可靠得多。

这三大矛盾决定了:通信格式设计不是写几个JSON字段那么简单,而是要在多个目标之间做显式的取舍。

2. 主流的三种通信格式形态

现阶段成熟可用的格式形态有三类,分别面向不同层级。很多新人把这三类混为一谈,其实它们的定位、抽象层次和适用场景差异很大。

2.1 Function Calling / Tool Use:模型与工具之间的契约

主流大模型API普遍支持的Function Calling机制,是LLM应用最底层的通信格式。它的核心逻辑是:应用在请求里声明一批工具,每个工具用JSON Schema描述名字、参数类型、必填项和约束;模型在生成过程中判断需要调用哪个工具时,不再输出自然语言,而是输出一个结构化的调用请求。

下面是工具定义的典型写法:

{ "type": "function", "function": { "name": "query_order_status", "description": "查询用户订单的当前状态", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,例如 OD20250101" }, "include_detail": { "type": "boolean", "description": "是否返回详细物流信息", "default": false } }, "required": ["order_id"] } } }

模型如果真的决定调用,它的返回里就会出现tool_calls字段:

{ "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "query_order_status", "arguments": "{\"order_id\": \"OD20250101\", \"include_detail\": true}" } } ] }

这里有两个非常值得注意的细节。第一个是arguments是一个JSON字符串,不是对象,应用层必须自己解析。第二个是解析完后要把执行结果,按照原始调用id(tool_call_id)回传给模型,模型才能继续生成最终回复。这套“请求–工具调用–回传结果–继续生成”的循环,本质上就是LLM应用层通信的原始形态。

我自己的实战心得是:工具的parameters描述绝对不能写“任意字符串”或者“object,内容随意”这种话。模型对工具描述的理解能力很强,但描述的模糊也会让参数生成的随机性变大。对必填字段必须显式声明required,对取值受限的字段就加enum。与此同时,也不能把一个参数限制得太死,比如让模型只从五个枚举值里选,实际场景跑起来总会有第六种情况。合理的做法是宽松约束加描述引导,通过提示词告知模型“如果拿不准枚举值,就填入最接近默认真”。

2.2 结构化输出:把JSON当成通信语言

Function Calling解决的是“模型怎么调用工具”,但Agent之间传业务数据时,往往需要模型直接输出结构化结果。比如信息抽取、摘要整理、计划生成,这时候你用JSON Mode或Structured Output会更顺手。

结构化输出的思路是:在请求中声明响应格式必须遵循某个JSON Schema,模型输出的就是可直接解析的JSON,而不是夹在自然语言里的“顺便附上数据”。

{ "name": "extract_order_info", "strict": true, "schema": { "type": "object", "properties": { "order_id": { "type": "string" }, "status": { "type": "string", "enum": ["pending", "paid", "shipped", "completed", "cancelled"] }, "total_amount": { "type": "number" } }, "required": ["order_id", "status", "total_amount"], "additionalProperties": false } }

我用下来最大的感受是:别指望模型在没有Schema约束时稳定输出合法JSON。给提示词写“请输出JSON”的前提是模型足够强,强到能够自我约束。在实际工程里,不同模型对响应格式的遵守能力差异非常大,有些模型必须靠平台级约束才能可靠输出。所以能用API的结构化输出参数就用,不能用的情况下,也要把Schema同时写进System Prompt,并且在模型回答后面明确标注“只输出JSON,不要解释,不要Markdown代码块”。

结构化输出最大的价值是让Agent链上的每个节点都吃同一份契约。下游Agent拿到上游Agent输出的JSON后,直接校验、直接消费,不需要再做文本解析。这条链路做通了,多Agent之间的消息才谈得上“格式”二字。

2.3 协议化:MCP与A2A解决不同层次的问题

当工具数量一多、Agent数量一多,格式就不再是自定义JSON能收场的事了。业界逐渐形成两类标准化协议:MCP和A2A。

**MCP(Model Context Protocol)**解决的是“Agent怎么统一插工具”的问题。你可以把每个业务系统想象成不同的插头,以前每个项目都要写适配器硬接,现在按MCP标准暴露能力,所以应用和工具之间真正做到了“插上就能用”。MCP的核心角色是Client和Server:Server把工具、资源、提示词整理成标准接口,Client统一消费。传输层支持本地stdio,也支持远程HTTP,非常适合做企业内部工具接入层。

这里放下我常用的对比表:

对比维度MCPA2A
解决问题Agent与工具/数据源之间的标准化接入Agent与Agent之间的发现、协商与协作
核心角色Client / ServerAgent Card + Task
通信方式通过Client调用Server暴露的工具和资源基于JSON-RPC 2.0交换任务和产物
抽象层次工具层、资源层任务层、协作层
典型场景把企业内部API包成标准工具给Agent调用多个Agent分工完成一个跨系统任务

之前有人问我“MCP和A2A是不是二选一”,其实根本不是。实际落地的架构里,这两者是互补的:A2A负责Agent之间发现对方、传递任务,Agent拿到任务后,各自通过MCP去调用自己背后的工具和数据源。有点像一个销售团队里,销售员之间靠工单系统协作(A2A),每个销售员用统一的工作台查询订单系统(MCP)。

A2A里的Agent Card值得多说一句。它是一个JSON文件,描述Agent的名字、技能、输入输出格式、endpoint地址。其他Agent可以通过发现机制拿到这个卡片,然后决定要不要把任务派发过来。任务本身有生命周期,包括提交、执行、等待输入、完成等状态,任务结果以Artifacts形式返回。这套设计让Agent之间的通信从“接口到处乱飞”变成了“有注册、有状态、有反馈”的正规协作。

3. 实操:设计一套可落地的Agent消息格式

如果你暂时上不了MCP和A2A这种重型协议,只是在自建Agent系统,那么一套经过验证的消息格式同样能扛住多数场景。我给出我在项目里实际用过的信封式消息设计,你直接抄也能用。

3.1 最小消息结构字段设计

核心消息结构如下:

{ "messageId": "msg_20250101120000_001", "type": "intent", "sender": "agent.assistant.customer_service", "recipient": "agent.order_processor", "timestamp": "2025-01-01T12:00:00Z", "threadId": "thread_78211", "traceId": "trace_9f821ac0", "payload": { "action": "query_order", "params": { "order_id": "OD20250101" } }, "metadata": { "priority": "high", "maxTimeout": 30000, "tokenBudget": 1200 } }

这里每个字段的存在都是有原因的,不是堆砌。

messageId用于去重和日志关联。消息服务重试时,接收方靠messageId判断是否已处理过。type是枚举值,我一般只留intent、event、result、error四类,避免语义泛滥。sender和recipient是Agent的路由依据,必须支持正则匹配,不能只写死单个名字。threadId把同一会话的多轮消息串起来,traceId则跨线程追踪全链路,这两者一结合,排查问题时能省下大把时间。

payload是业务数据的载体,action作为子命令,params作为参数,这样一层intent消息就能驱动不同工具。metadata里放路由和资源相关的信息,比如优先级、超时、token预算,这对接下来的并发控制很关键。

3.2 用JSON Schema把消息契约“钉死”

有了结构,接下来必须用JSON Schema把它变成计算机可执行的语言:

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "messageId": { "type": "string", "pattern": "^msg_" }, "type": { "type": "string", "enum": ["intent", "event", "result", "error"] }, "sender": { "type": "string" }, "recipient": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" }, "threadId": { "type": "string" }, "traceId": { "type": "string" }, "payload": { "type": "object", "properties": { "action": { "type": "string" }, "params": { "type": "object" } }, "required": ["action"] }, "metadata": { "type": "object" } }, "required": ["messageId", "type", "sender", "recipient", "timestamp", "threadId", "payload"], "additionalProperties": false }

这份Schema只有一条硬性要求:信封字段必须齐,payload里的业务参数可以放宽。这样既保证路由可靠,又给业务提供空间。

实际落地时你要注意校验的层级。我习惯在网关层做严格校验,凡是信封字段不合规的消息直接拒收,并返回带messageId的错误消息,方便发送方定位。在消费方做宽松校验,尤其是payload层,接收方要容忍未知字段的存在。这种“上游严格、下游宽容”的分布能够把问题拦在系统边缘,不至于让脏数据一路流到最深处。

3.3 消息格式的版本与兼容策略

通信格式一定会演进,关键是演进方式要可控。

第一,消息体里必须带version字段,不要依赖接口路径版本号。路径版本号管的是HTTP,管不了消息内部兼容。第二,新增字段一律用optional,实现向后兼容。老节点收到新字段要么忽略,要么存日志,不能因为多了一个字段就解析失败。第三,破坏性变更必须换version,并在升级时同时保留新旧两套处理逻辑跑一段时间。

我踩过的最疼的坑就是在消息里加了一个required字段,结果线上老版本Agent全部接口报错。从那以后我定了一条死规矩:线上消息格式只增加字段,不删除字段,不改字段类型,不改字段语义。数据类型修改永远创建一个新字段,把老字段标记为deprecated,等所有消费者迁移完再清理。

4. 从单Agent到多Agent的通信架构

格式搞定后,真正难的是把它放到架构里用起来。多Agent场景下,你一定会遇到几个绕不开的问题:哪些通信走直连,哪些走异步消息;上下文到底传多少;并发怎么控。

4.1 什么时候直接函数调用,什么时候走消息协议

很多团队在这个选择上比较随意,结果就是架构混乱。我给出的判断标准很简单:看信任边界和部署边界。

同一个进程内、同一个信任边界内,比如一个工作流里主Agent调用一个验证工具,直接用本地函数调用就行,完全没必要套协议。跨进程、跨服务、跨信任边界时,比如订单Agent要调用库存Agent,就必须走消息协议。如果两个服务还要异步解耦,就直接上消息队列。

我见过最头疼的是在单体应用里强行引入一套A2A协议,结果一套纯Java方法调用变成了走HTTP的远程调用,延迟翻了几倍,出错排查还更难。记住,协议的引入是为了解耦和独立演进,不是为了好看。单体应用内部老老实实用函数签名当契约,接口文档本来就够用了。

4.2 上下文、状态与Trace的三层设计

多Agent协作中最容易炸的就是“上下文”。新手常见的做法是每个Agent发消息时都带上完整的会话上下文,觉得这样模型理解最充分。实际这样跑一两个任务还行,跑十来个任务后上下文体积爆炸,token成本直线上升,响应时间翻了数倍。

正解是把上下文和状态分开治理。会话上下文放在threadId对应的共享存储里,每次消息只传增量摘要,而不是全量历史。高频业务数据放进参数里传,低频数据靠接收方主动拉取。更重要的是状态管理:不要依赖模型从历史里推断“当前任务到哪一步了”,而是在消息里显式传递stage数字或者state枚举。模型推断状态的不确定性太高,工程上必须用确定性字段管理状态机。

traceId的设计同样关键。每个任务进入系统时生成一个traceId,后续所有Agent消息、API调用、工具执行都带上这个id。日志系统按traceId聚合,你就能看到任务从入口到出口的全部路径。真出问题时,你能顺着路径快速定位是哪个Agent丢了消息,哪个步骤返了错误。

4.3 并发控制与Token成本:一次通信送多少内容才划算

多Agent系统一旦开放给真实用户,并发问题立刻暴露。我的实操经验是:消息信封在网关层做限流,按recipient维度区分处理和计划,保证下游Agent不会被同时打爆。对于同一条threadId,在同一时间只允许一个Agent处于执行状态,避免多个Agent对同一任务状态做互相覆盖的写操作。任务队列的引入不可避免,它也能让“Agent执行中”的状态变得透明。

token成本方面,其实核心策略只有一个:能少送就少送。发送方在组装消息时,先把大段文本抽取出关键字段,必要的时候启发模型生成摘要,再用结构化参数放进去。这就像两个人通信,你没必要把整本小说发过去,发一页纸的重点摘要就够了。摘要生成本身也要做缓存,同一个threadId的高频任务摘要可以复用。

5. 经验与踩坑实录

最后这部分,我整理一下这几年实际踩过的坑。每一条都是真金白银换来的,挨个说。

5.1 解析失败的元凶:裸JSON与转义陷阱

第一坑:模型返回的结果里自带Markdown代码块。你可能也遇到过,模型明明要输出JSON,结果返回的是:

```json {"action": "query_order", "params": {"order_id": "OD20250101"}} ```

我的解析代码第一版只做了JSON.parse,下班前测试全绿,上线第一个晚上全是解析错误。后来我写了一个三层容错函数:先trim,再剥掉代码块标记,最后用正则找到第一个完整JSON对象,再交给JSON.parse。这一套下来,解析成功率高了一大截。

第二坑:字符串转义。特别是工具参数需要拼接大量路径、正则、编码文本时,反斜杠和引号极易出问题。为此我统一规定:所有进入消息体的字符串字段必须做JSON转义,所有从消息体取出的字段必须做JSON反转义,同时使用JSON校验器在解析阶段直接暴露非法转义。

5.2 Schema校验的严与松怎么平衡

这个平衡我前面提到过。补充一个细节:强约束模式下,小模型偶尔会无法生成字段,导致整体请求失败。我做过一个项目,模型从某个轻量模型换到旗舰模型后,同样的Schema约束表现完全不同,轻量模型更容易漏掉required字段,而旗舰模型几乎从不漏。

线上跑下来比较合适的策略是:校验严格度分成“容错模式”和“严格模式”。容错模式下,校验失败的消息自动补默认值,并打上警告日志,同时抄送一份到离线任务做样本分析,隔周排期修复。严格模式只用于核心交易链路,比如支付、退款相关Agent之间的消息,必须全字段校验通过才继续执行。

5.3 工具调用安全:别让Agent权限裸奔

这是一个容易被忽视的地方。通信格式是Agent的“嘴”,嘴能说什么决定了Agent能做什么。我遇到过一次工具调用事故:Agent调用了一个删除函数,参数里被塞进了一个不是我方系统生成的路径,导致系统侧误删了一份缓存数据。排查后发现,上游Agent把外部用户输入的文本原封不动传进了工具参数,触发了路径拼接漏洞。

从那以后,我定了三条硬性安全规则。

第一,工具Schema里必须声明参数的白名单,凡是涉及文件路径、外部URL、命令行的参数,不能让模型自由填写。能枚举的枚举,不能枚举的也要做正则校验。第二,危险工具(删除、写库、转账、推送)的设置必须有二次确认机制:Agent先把调用意图发到确认队列,由人工或者值班脚本确认后再执行。第三,所有工具调用日志必须包含完整tool_calls、参数、执行结果和messageId,便于事后审计。

安全这块建议尽量前置。等出了事故再补格式层面的校验,往往已经晚了。

5.4 一次切换协议的兼容性教训

最后讲一个关于格式演进的教训。我们曾经有一个多Agent系统,早期各Agent之间用的是自定义裸JSON消息。后来团队决定切换到A2A协议,方向是对的,但切换方式出了问题:我们把消息网关的协议全部切到A2A,没有做新旧兼容,结果存量Agent和新协议Agent在网关处互不相认,消息全堵在转换层,整整折腾了一个通宵。

从那以后我明确了一个原则:协议切换必须经过兼容层。没有完美的新旧协议无缝切换,最好的方式是先引入一个网关层做协议转换,让老Agent发旧格式、新Agent发新格式,网关负责解析、转换、再投递,等确认新协议跑稳后逐步下线老格式。这个网关层的存在时间通常不会短,但它能保命。

再补充一点:如果某天你发现自己要维护的消息格式又多又乱,不要想着靠增加代码分支解决。分支多了代码就是定时炸弹。反过来想,多Agent系统的消息就是一个企业内部多个系统间的接口清单,接口越乱,协作越差,治理越重要。花时间把消息格式标准化、版本化、可观测化,绝对是一本万利的投入。

做Agent工程这几年,我最真实的体会是:格式是沟通的信仰。你前期懒得定义的字段,后期都会变成线上事故来找你。别怕格式复杂,怕的是没有格式、没有校验、没有版本意识。给Agent之间定一套清晰的通信格式,就是给整个系统装上了一条靠谱的神经系统。单Agent的能力决定系统的天花板,而Agent之间的通信格式决定这个天花板能不能被够到。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询