
做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字符串不是对象应用层必须自己解析。第二个是解析完后要把执行结果按照原始调用idtool_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。**MCPModel Context Protocol**解决的是“Agent怎么统一插工具”的问题。你可以把每个业务系统想象成不同的插头以前每个项目都要写适配器硬接现在按MCP标准暴露能力所以应用和工具之间真正做到了“插上就能用”。MCP的核心角色是Client和ServerServer把工具、资源、提示词整理成标准接口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之间的通信格式决定这个天花板能不能被够到。