1. 为什么智能体之间需要一套自己的通信语言
过去一年我接触了不少智能体项目,从扣子、Dify 这类平台搭建的,到用 Python 从零手写的 ReAct 智能体,几乎每个团队都会在某个阶段撞上同一堵墙:单个智能体跑得挺好,一旦想让两个或多个智能体协作,整个系统就开始变得脆弱、混乱、难以调试。Agent Network Protocol(ANP)这份技术白皮书草案想解决的,正是这堵墙背后的根本问题——智能体之间到底该怎么"说话"。
先把结论摆在前面:ANP 要做的不是又一个 RPC 框架,也不是又一个消息队列协议,而是一套面向"开放网络中的异构智能体"的通信与身份层。它要回答三个问题:我是谁(身份)、我怎么找到你(发现)、我们用什么格式交换信息(消息与语义)。这三个问题听起来简单,但每一个在真实工程里都能把人折磨到怀疑人生。
为什么不能用现成的 HTTP + JSON 直接搞定?我一开始也是这么想的。实测下来,问题出在几个地方。第一,智能体是"活的",它的能力会动态变化,今天能查天气明天可能接入了新的工具,静态的接口定义跟不上。第二,智能体可能由不同厂商、不同框架构建,扣子搭的和 Python 手写的语义理解方式完全不同,需要一个双方都能接受的中间语义层。第三,也是最容易被忽略的——身份。当 A 智能体收到 B 智能体的请求时,它凭什么相信 B 就是 B,而不是某个冒充者?这在多智能体协同、智能体行为审计场景里是绕不过去的。
ANP 的关键词里出现了 DID,这不是偶然。DID(去中心化标识符)提供了一种不依赖中心化账号体系的身份表达方式,每个智能体可以拥有一个可验证的、自主控制的身份标识。把 DID 和通信协议结合,意味着智能体的身份和它的消息可以绑定验证,这对构建可信的多智能体网络是基础性的。
这篇文章我会从协议设计的核心动机讲起,拆解 ANP 的几层结构,重点讲清楚身份层、发现层、消息层各自解决什么问题,然后落到实操——如果你现在就想在自己的项目里引入类似 ANP 的思路,该怎么起步,有哪些坑我踩过。适合正在做智能体开发、多智能体协同、或者对智能体架构感兴趣的读者,不管你是用平台还是手写代码,都能从中找到可复用的部分。
2. ANP 的分层设计:身份、发现、消息三件事分开做
2.1 为什么不能把三件事揉在一起
很多团队做智能体通信时,习惯性地把身份认证、服务发现、消息格式全塞进一个自定义的 JSON 结构里,比如{ "from": "agentA", "to": "agentB", "auth": "xxx", "payload": {...} }。这种"一锅炖"的做法在只有两三个智能体、且都是自己写的时候没问题,但一旦规模上去、参与方变多,就会变成灾难。
原因很简单:这三件事的变化频率完全不同。身份体系一旦确定,可能几个月都不动;服务发现会随着智能体上下线频繁变化;消息格式则可能因为业务需求每周都在调整。把它们耦合在一起,改任何一层都要动全部,测试成本指数级上升。
ANP 把这三层拆开,各自有独立的抽象和演进节奏。这是我认为这份草案最值得借鉴的设计决策,哪怕你最后不用 ANP,这个分层思路也值得抄。
2.2 身份层:DID 到底解决了什么
先说身份层。传统做法是给每个智能体分配一个 ID,存在数据库里,通信时带上这个 ID。问题在于,这个 ID 是中心化系统发的,跨系统就不认了。你的智能体在 A 平台有个 ID,到了 B 平台就是陌生人。
DID 的思路是让标识符本身携带可验证性。一个 DID 通常长这样:did:example:123456789abcdefghi,它是一串自描述的字符串,配套一份 DID Document,里面包含公钥、服务端点等信息。验证方拿到 DID 后,可以通过解析 Document 拿到公钥,进而验证对方签名。
落到智能体场景,这意味着:智能体 A 用自己的私钥对消息签名,智能体 B 通过 A 的 DID 解析出公钥来验签。整个过程不需要任何中心化机构介入,也不需要 B 事先认识 A。这就是"开放网络"的前提。
我在实际项目里试过用简单的 HMAC 共享密钥做智能体认证,小规模能用,但密钥分发本身就是个麻烦事,而且一旦某个智能体被攻破,所有共享密钥的智能体都受影响。DID 这种非对称的方式,每个智能体独立持有私钥,风险隔离得好得多。
注意:DID 的解析在真实网络里是有成本的,尤其是跨链或跨方法(did:web、did:key 等)解析时。如果你的智能体通信是高频低延迟场景,别每条消息都去解析 DID,应该做本地缓存,并设置合理的过期策略。
2.3 发现层:智能体怎么找到彼此
身份解决了"你是谁",接下来是"我怎么找到你"。发现层要处理的是:给定一个能力需求(比如"我需要一个能做图像识别的智能体"),如何找到具备这个能力的智能体。
ANP 草案里这块的思路偏向于"能力描述 + 注册/查询"。每个智能体在 DID Document 或独立的描述文档里声明自己具备哪些能力、支持哪些消息格式、服务端点在哪儿。查询方通过某种目录服务或点对点方式获取这些描述。
这里有个工程上的取舍:中心化目录 vs 去中心化发现。中心化目录实现简单、查询快,但单点故障和信任问题明显;去中心化发现更符合开放网络的理念,但实现复杂、查询效率低。我的经验是,早期项目用中心化目录快速跑通,等规模上来再逐步引入去中心化机制,不要一上来就追求理想架构。
2.4 消息层:语义互操作才是真难点
消息层是最容易被低估的一层。很多人以为定义个 JSON schema 就完事了,但真正的难点在语义。智能体 A 说"帮我订一张明天去北京的票",智能体 B 理解成"订机票",智能体 C 理解成"订火车票",这就是语义鸿沟。
ANP 在消息层需要处理的不只是传输格式,还有意图表达、上下文传递、错误语义等。这部分草案里还在演进,但从关键词"通信协议""服务通信协议层"来看,它试图在传输之上建立一个语义协商机制。
我的实践体会是:语义互操作没有银弹,最务实的做法是定义一套核心意图词汇表(类似对话行为理论里的 speech act),复杂语义通过组合核心意图来表达。这样既保证了互操作性,又留了扩展空间。
3. 从零跑通一个最小 ANP 风格通信:我的实操路径
3.1 环境与依赖准备
假设你现在想在自己的项目里落地一套 ANP 风格的通信,我建议从最小可用版本开始。下面是我实际用过的一套技术选型,你可以直接参考。
- 语言:Python 3.10+,生态成熟,智能体相关库多
- 身份:先用
did:key方法,实现最简单,不需要网络解析 - 消息传输:HTTP + JSON,别一上来就上消息队列
- 签名:Ed25519,性能好、密钥短
- 序列化:JSON,可读性优先,调试方便
安装依赖:
pip install cryptography httpx pydanticcryptography负责 Ed25519 签名验签,httpx做 HTTP 通信,pydantic做消息结构的校验。这三个库足够跑通最小版本。
3.2 生成智能体身份并构造 DID
第一步是给智能体生成身份。用 Ed25519 生成密钥对,然后基于公钥构造一个did:key标识符。
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization import base58 private_key = Ed25519PrivateKey.generate() public_key = private_key.public_key() public_bytes = public_key.public_bytes( encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw ) did = "did:key:z" + base58.b58encode(public_bytes).decode() print(did)这里用 base58 编码是为了让 DID 字符串更紧凑、更适合在 URL 里传递。did:key的特点是公钥直接编码在 DID 里,解析时不需要任何网络请求,这对早期开发非常友好。
提示:生产环境如果要用
did:web或更复杂的方法,解析逻辑会不一样,但签名验签的核心流程是一致的。先把did:key跑通,理解原理后再换。
3.3 消息签名与验签的完整流程
有了身份,接下来是消息签名。每条发出的消息都要带上发送方的 DID 和签名,接收方验签后才处理。
import json, time def sign_message(private_key, did, payload): message = { "from": did, "timestamp": int(time.time()), "payload": payload } canonical = json.dumps(message, sort_keys=True, separators=(',', ':')) signature = private_key.sign(canonical.encode()) message["signature"] = base58.b58encode(signature).decode() return message注意这里用了sort_keys=True和紧凑分隔符,目的是让序列化结果确定化。如果签名和验签两边的 JSON 序列化方式不一致,验签必然失败。这是我在实际项目里踩过的坑——不同语言的 JSON 库默认行为不同,跨语言通信时一定要约定好规范化方式。
验签方拿到消息后,从from字段解析出公钥,重新序列化去掉签名的部分,然后验签:
def verify_message(message): signature = base58.b58decode(message.pop("signature")) did = message["from"] public_bytes = base58.b58decode(did.replace("did:key:z", "")) public_key = Ed25519PublicKey.from_public_bytes(public_bytes) canonical = json.dumps(message, sort_keys=True, separators=(',', ':')) public_key.verify(signature, canonical.encode())验签通过才继续处理 payload,否则直接拒绝。这一步看似简单,但它是整个可信通信的基石。
3.4 能力声明与发现的最小实现
发现层的最小实现可以很简单:每个智能体暴露一个/agent-card端点,返回自己的能力描述。
agent_card = { "did": did, "name": "image-recognizer", "capabilities": ["image.classify", "image.describe"], "endpoint": "http://localhost:8001", "protocol_version": "anp-draft-0.1" }查询方通过已知的目录服务或直接访问对方的 card 端点来获取能力信息。早期不需要复杂的目录,维护一个静态的智能体列表就够了。
3.5 跑通第一个跨智能体调用
把上面几块拼起来,就能跑通一个最小调用链:智能体 A 发现智能体 B 的能力,构造请求消息,签名后通过 HTTP 发给 B,B 验签、处理、返回结果。
import httpx def call_agent(target_card, payload, private_key, my_did): message = sign_message(private_key, my_did, payload) resp = httpx.post(f"{target_card['endpoint']}/invoke", json=message) return resp.json()实测下来,这套最小实现大概两百行代码就能跑通,足够验证你的多智能体协作想法。等验证通过,再考虑引入更复杂的发现机制、消息队列、错误重试等。
4. 那些文档不会告诉你的坑
4.1 时间戳与重放攻击
签名能保证消息没被篡改,但保证不了消息没被重放。攻击者可以把一条合法消息原样再发一次。解决办法是在消息里加时间戳和 nonce,接收方维护一个近期 nonce 集合,重复的直接拒绝。
我一开始没做这个,测试时发现同一个请求被处理了两次,排查了半天才意识到是重试机制导致的重复投递。加上 nonce 校验后问题解决。时间戳窗口别设太大,一般 5 分钟足够,设太大等于没设。
4.2 跨语言序列化的规范化陷阱
前面提过 JSON 序列化的问题,这里展开说。Python 的json.dumps默认会在,和:后加空格,JavaScript 的JSON.stringify不加。如果签名方用 Python、验签方用 Node.js,不做规范化处理必然失败。
解决方案有两种:一是约定统一的规范化算法(类似 JCS,JSON Canonicalization Scheme),二是干脆用二进制序列化如 CBOR。我倾向第一种,因为可读性对调试太重要了。跨语言项目里,规范化这块一定要写测试用例覆盖,别靠人肉检查。
4.3 DID 解析的性能与缓存
did:key不需要网络解析,但did:web需要。如果你的智能体网络里有大量did:web身份,每次验签都去解析会拖垮性能。必须做缓存,而且要处理缓存失效。
我的做法是本地维护一个 DID Document 缓存,带 TTL,默认 1 小时。同时提供一个强制刷新的接口,当验签失败且怀疑是公钥轮换导致时,主动刷新一次再验。这个"失败后刷新重试"的逻辑很实用,能覆盖大部分密钥轮换场景。
4.4 错误语义的统一
智能体调用失败时,返回什么?HTTP 500 加一段文字?还是结构化的错误对象?ANP 风格的做法是定义统一的错误语义,让调用方能程序化地处理。
我用的错误结构:
{ "error": { "code": "CAPABILITY_NOT_SUPPORTED", "message": "requested capability image.segment not available", "retryable": false } }code用枚举,retryable告诉调用方要不要重试。这个字段看似小,但在多智能体协同里能省掉大量猜测逻辑。踩过的坑是早期没定义retryable,调用方对所有错误都重试,结果把不可恢复的错误重试到超时。
4.5 智能体行为审计的埋点
关键词里出现了"智能体行为审计",这在多智能体系统里非常重要。每个智能体的每次调用、每次决策都应该留下可追溯的记录。我的做法是在消息层统一埋点,记录消息 ID、发送方、接收方、时间戳、处理结果,写入独立的审计日志。
审计日志和业务日志要分开存,因为审计日志的保留周期通常更长,且不能被业务逻辑修改。这块早期不做,后期补起来会很痛苦,因为历史数据已经丢了。
5. 平台智能体与手写智能体在 ANP 下的差异
5.1 平台智能体的通信封闭性
用扣子、Dify 这类平台搭建的智能体,通信方式通常由平台定义,对外暴露的接口有限。想让平台智能体参与 ANP 网络,往往需要一层适配:把平台的调用接口包装成 ANP 消息格式,把平台的身份体系映射到 DID。
我做过一个扣子智能体接入自建 ANP 网络的适配层,核心工作是把平台的 webhook 回调转换成 ANP 消息,并给平台智能体分配一个代理 DID。代理 DID 的私钥由适配层持有,平台智能体本身不感知 DID 的存在。这样既接入了网络,又不用改平台。
5.2 手写智能体的灵活性优势
用 Python 手写的智能体在 ANP 接入上灵活得多,因为你可以完全控制消息的构造和签名流程。缺点是所有基础设施都要自己搭,身份管理、发现服务、审计日志都得从零写。
我的建议是混合架构:核心的、需要深度定制的智能体手写,外围的、功能标准的智能体用平台搭,中间用适配层打通。这样既保证了关键路径的可控性,又利用了平台的开发效率。
5.3 两种智能体协同时的语义对齐
平台智能体和手写智能体协同时,最大的问题是语义对齐。平台智能体的意图理解由平台模型决定,手写智能体的意图理解由你自己的 prompt 或逻辑决定,两者对同一句话的理解可能不同。
解决办法是在 ANP 消息层之上再定义一层"意图契约",双方都按契约表达和解析意图,不直接传递自然语言。这增加了前期设计成本,但大幅降低了协同的不确定性。我在一个客服场景的多智能体项目里用了这个思路,把意图收敛到二十几个标准动作后,协同成功率从七成提升到九成以上。
6. 我对 ANP 落地节奏的一点个人判断
ANP 现在还处于草案阶段,很多细节还在演进,现在就想找一个"标准实现"直接用是不现实的。但这不影响它作为设计参考的价值。我的建议是:把 ANP 的分层思想、DID 身份、消息签名验签这几块先在自己的项目里落地,哪怕用的是自定义的消息格式,只要分层清晰、身份可验证,就已经比大多数"一锅炖"的实现强很多。
等 ANP 规范稳定后,再把自定义部分替换成标准实现,迁移成本主要在消息格式的适配,核心的身份和签名逻辑基本不用动。这个渐进路径是我认为最务实的。
最后分享一个小技巧:在项目早期就引入一个"协议版本"字段,每条消息都带上。这样将来协议升级时,你可以根据版本号做兼容处理,而不是被迫一次性全量迁移。这个字段现在加进去几乎零成本,但将来能救你一命。