1. 项目概述:为AI Agent打造一个“通用语言”
如果你和我一样,在尝试构建多AI Agent协作系统时,被它们之间混乱、随意的“对话”搞得焦头烂额,那么这个项目就是为你准备的。bot-protocol,或者我更喜欢叫它“Bot间通信协议”,本质上是在为不同的AI Agent定义一套它们都能理解的、结构化的“普通话”。
想象一下,在一个团队里,每个成员都用自己家乡的方言汇报工作,那会是多么低效和混乱。在OpenClaw这样的多Agent框架里,当Lotbot需要Mantis去检查服务器状态,或者Clawcos需要将一项复杂任务分解并分派给其他Agent时,如果没有一个统一的沟通标准,信息就很容易丢失、误解,甚至导致任务陷入死循环。这个协议就是为了解决这个问题而生的:它定义了一套清晰的消息格式、状态跟踪和交互规则,让Agent之间的协作像调用一个设计良好的API一样可靠。
这套协议的核心价值在于通道无关性和结构化。它不关心你的Agent是跑在Discord的聊天频道里,还是Telegram的私聊窗口,抑或是Slack的工作区。只要是个能传输文本的地方,它就能工作。它把一次Agent间的请求与响应,封装成一个包含发送者、接收者、任务描述、上下文、优先级和深度限制的完整数据包。这不仅仅是让消息“看起来整齐”,更是为了实现可靠的对话状态跟踪、深度的防无限循环以及自动化的超时处理。接下来,我会带你深入这套协议的内部,看看它是如何被设计出来的,以及在实际中如何驾驭它。
2. 协议核心设计思路拆解
2.1 为何需要结构化,而不仅仅是自然语言?
很多初涉多Agent系统的开发者会想:既然Agent本身就能理解自然语言,为什么不让它们直接用自然语言对话呢?这个想法很自然,但经不起实践的考验。自然语言充满歧义、省略和上下文依赖。对于一个任务“检查版本号”,Agent A可能回复“版本是v2.1”,而Agent B可能回复“一切正常,版本最新”。对于人类来说,我们或许能推断,但对于需要精确判断“是否过时”的另一个Agent来说,这种模糊的回复是无法处理的。
bot-protocol采用结构化消息,强制要求关键信息必须以明确的字段呈现。比如,一个REQUEST消息必须包含RequestId(用于唯一追踪)、Task(清晰的任务描述)、Depth(当前对话深度)。这就好比我们要求所有工作邮件必须包含“主题”、“发件人”、“任务编号”和“紧急程度”,极大地降低了沟通成本和处理复杂度。解析器(parser.js)会严格校验这些字段,格式不对的消息会被直接拒绝(返回null),从源头保证了消息质量。
2.2 深度限制:防止“踢皮球”与无限递归
这是协议中最精妙也最必要的设计之一。在多Agent协作中,一个经典陷阱是:Agent A把任务交给B,B发现自己搞不定,又交给C,C可能又甩回给A……如此循环,形成死锁或无限递归,消耗大量资源却毫无进展。
bot-protocol引入了**深度(Depth)**机制来根治这个问题。每个消息都携带一个深度计数器,格式为{ current: 1, max: 5 }。每当一个REQUEST、CLARIFY或HANDOFF消息被发出时,current值就会递增。当深度达到最大值(默认为5)时,协议强制规定:接收方必须返回一个RESPONSE,结束这条对话链,而不能继续向下传递或请求澄清。构建器(builder.js)会强制执行这一规则,试图在深度已满时构建上述消息会直接抛出错误。
这个设计模拟了人类项目管理中的“ escalation path”(升级路径)。它承认Agent的能力是有限的,并将“无法解决”本身作为一种明确的状态(通过RESPONSE返回失败或需要人工介入),而不是让问题在系统中无限期游荡。
2.3 状态持久化:给对话安上“记忆”
无状态的通信是脆弱的。如果Agent重启,或者系统崩溃,之前的任务进行到哪一步就全丢失了。bot-protocol通过state.js模块实现了对话状态的持久化。所有正在进行的对话(Open Requests)、历史记录、时间戳和状态(进行中、等待澄清、已完成、失败、超时)都会被保存到本地文件(~/.openclaw/workspace/bot-protocol-state.json)。
这个状态文件是整个系统的“中央看板”。它使得:
- 断点续传:Agent重启后,可以读取状态文件,恢复未完成的任务。
- 超时检测:通过定期运行
state.checkTimeouts(),系统可以自动发现那些长时间没有进展的对话(例如,一个REQUEST发出后30分钟没有收到RESPONSE),并将其标记为超时,触发清理或告警流程。 - 全局视图:运维人员可以通过查看这个状态文件,一目了然地掌握所有Agent间的协作情况。
实操心得:状态文件的路径是硬编码的。在生产环境中,我强烈建议将这个路径改为可配置项,或者直接使用Redis这样的外部存储服务,以便支持多节点部署的Agent系统。文件锁也是需要考虑的,如果多个进程同时读写同一个JSON文件,可能会造成数据损坏。
3. 五大消息类型详解与实战应用
协议定义了五种核心消息类型,覆盖了Agent协作的所有基本场景。理解每一种的用途和时机,是正确使用协议的关键。
3.1 REQUEST:发起一次任务委托
这是最常用的消息类型,用于一个Agent(发起方)请求另一个Agent(接收方)执行某个任务。
核心字段解析:
to: 接收方Agent的名称。这是消息路由的依据。from: 发送方Agent的名称。task:必须清晰、无歧义的任务描述。这是协作成功的基础。好的task描述应包含“做什么”和“交付物标准”。例如,“检查 /home/user/app 目录下 latest.log 文件的大小,如果超过100MB则返回文件路径和大小,否则返回 ‘OK’。”就比“看看日志文件”要好得多。context: 可选字段,提供任务背景。例如,“用户报告系统卡顿,怀疑是日志膨胀所致。”这能帮助接收方Agent更好地理解任务优先级和排查方向。depth: 当前深度。由构建器自动计算,通常发送方无需手动设置。priority: 优先级 (low,normal,high,critical)。接收方Agent可以根据此字段调整任务处理队列。
构建示例:
const { buildRequest } = require('./lib/builder.js'); const requestMsg = buildRequest({ to: 'Mantis', from: 'Lotbot', task: '获取当前系统负载(1分钟、5分钟、15分钟),并检查是否有进程占用CPU超过70%,如有则列出进程名和PID。', context: '夜间批处理作业启动前例行检查。', priority: 'normal' }); console.log(requestMsg); // 输出类似: // [REQUEST → @Mantis] // From: Lotbot // RequestId: lotbot-8f2d4e1a // Task: 获取当前系统负载... // Context: 夜间批处理作业启动前例行检查。 // Depth: 1/5 // Priority: normal3.2 RESPONSE:给出任务结果或最终答复
这是对REQUEST、CLARIFY或HANDOFF的回应,标志着当前对话轮次的结束。
核心字段解析:
to/from: 与原始请求对应。requestId:必须与原始消息的RequestId一致,这是状态跟踪器进行匹配的关键。status: 执行状态 (success,partial_success,failed,cancelled)。这比一个模糊的“完成”或“没完成”包含更多信息。body: 任务执行的结果内容。在成功时,这里应包含请求方需要的数据;在失败时,应包含错误信息或原因。nextAction: 可选字段,建议下一步操作。例如,“失败原因为权限不足,建议以更高权限角色重试。”
构建示例(成功响应):
const { buildResponse } = require('./lib/builder.js'); const responseMsg = buildResponse({ to: 'Lotbot', from: 'Mantis', requestId: 'lotbot-8f2d4e1a', // 必须匹配请求的ID status: 'success', body: '系统负载:1min 0.2, 5min 0.15, 15min 0.1。未发现CPU占用超过70%的进程。', nextAction: null });3.3 CLARIFY:请求澄清与信息补充
当接收方Agent无法理解或无法执行任务时,不应猜测或直接失败,而应主动发起CLARIFY消息,向请求方询问更多细节。
使用场景:
- 任务描述模糊(如“处理那个文件”)。
- 缺少必要参数(如“发送邮件”,但未指定收件人)。
- 遇到歧义(如“最新版本”,是指稳定版还是开发版?)。
构建示例:
const { buildClarify } = require('./lib/builder.js'); // 假设builder.js导出了此方法 const clarifyMsg = buildClarify({ to: 'Lotbot', from: 'Mantis', originalRequestId: 'lotbot-8f2d4e1a', question: '您提到的“检查版本号”,具体是指检查哪个应用程序或服务的版本号?请提供名称或路径。' });发起CLARIFY后,原始请求的状态会变为clarifying,并启动一个独立的超时计时器(默认10分钟)。请求方需要对此CLARIFY作出RESPONSE来提供补充信息。
3.4 HANDOFF:任务转交与责任传递
当一个Agent认为自己不是处理某个任务的最佳人选,或者任务的一部分需要特定专家处理时,可以使用HANDOFF将任务(或子任务)转交给另一个Agent。
与REQUEST的区别:HANDOFF通常伴随着上下文和责任的完全转移。原始请求方可能不再关注后续过程,而由接手方负责最终向原始请求方或另一个目标汇报。HANDOFF消息需要包含完整的原始任务上下文。
构建示例:
const { buildHandoff } = require('./lib/builder.js'); // 假设导出 const handoffMsg = buildHandoff({ to: 'SecurityBot', // 转交给安全专家 from: 'Mantis', originalRequestId: 'lotbot-8f2d4e1a', task: '深度分析服务器登录日志 /var/log/auth.log,识别可疑登录尝试。', context: '来自Lotbot的常规安全审计请求。原始上下文:夜间批处理作业启动前例行检查。', reason: '此任务涉及安全日志分析,超出我的常规监控范围。' });3.5 BROADCAST:发布公告与协同感知
用于一个Agent向所有(或一组)Agent广播信息,例如系统状态通知、配置更新、协同开始的信号等。
特点:
to字段可以是“all”或特定的组名。- 通常不期待直接的、结构化的
RESPONSE,但其他Agent可以据此更新自己的内部状态或触发相应动作。 - 超时时间最短(默认5分钟),因为广播消息的生命周期较短。
构建示例:
const { buildBroadcast } = require('./lib/builder.js'); const broadcastMsg = buildBroadcast({ from: 'SystemMonitor', to: 'all', subject: '数据库主节点计划维护', body: '将于北京时间明日02:00-04:00进行数据库主节点维护,期间写入操作可能有短暂延迟。请各Agent调整重试策略。', urgency: 'high' });4. 完整集成与实操流程
理解了消息类型,我们来看如何将一个Agent真正接入bot-protocol。这里我以集成到一个基于Node.js的简易Agent为例。
4.1 环境准备与协议库安装
首先,你需要将bot-protocol作为依赖引入你的Agent项目。假设你的Agent技能目录结构类似于OpenClaw。
# 进入你的Agent技能目录 cd /path/to/your-agent/skills # 克隆或复制bot-protocol项目 git clone <repository-url> bot-protocol cd bot-protocol # 安装依赖 npm install确保你的Agent主程序或技能管理器能够加载这个模块。
4.2 消息接收与处理循环
你的Agent需要在一个消息监听循环中集成协议解析器。以下是一个高度简化的示例:
// your-agent/main.js 或某个技能处理器中 const { parse } = require('./skills/bot-protocol/lib/parser.js'); const state = require('./skills/bot-protocol/lib/state.js'); const { buildResponse, buildClarify } = require('./skills/bot-protocol/lib/builder.js'); // 假设这是一个从某个频道(如Discord)获取新消息的函数 async function onNewMessage(rawTextMessage, channelInfo) { // 1. 尝试解析为协议消息 const parsedMessage = parse(rawTextMessage); // 2. 如果不是协议消息,按原有逻辑处理(可能是用户指令) if (!parsedMessage) { return handleUserCommand(rawTextMessage, channelInfo); } // 3. 如果是协议消息,但接收者不是我,则忽略 if (parsedMessage.to !== 'MyAgentName') { console.log(`Ignoring message for ${parsedMessage.to}`); return; } // 4. 状态跟踪:登记或更新此对话 await state.track(parsedMessage); // 5. 根据消息类型分发处理 switch (parsedMessage.type) { case 'REQUEST': await handleRequest(parsedMessage, channelInfo); break; case 'CLARIFY': await handleClarify(parsedMessage, channelInfo); break; case 'HANDOFF': await handleHandoff(parsedMessage, channelInfo); break; case 'BROADCAST': await handleBroadcast(parsedMessage, channelInfo); break; // RESPONSE 通常由我们发起的请求接收,这里也可能需要处理(如更新UI) case 'RESPONSE': await handleResponse(parsedMessage, channelInfo); break; default: console.warn(`Unknown message type: ${parsedMessage.type}`); } // 6. 定期检查超时(可以放在一个独立的定时任务中) // setInterval(() => state.checkTimeouts(), 60000); // 每分钟检查一次 } async function handleRequest(request, channel) { console.log(`处理来自 ${request.from} 的请求: ${request.task}`); // 示例:解析任务,这里可以根据task字段的内容进行复杂的匹配和路由 if (request.task.includes('检查') && request.task.includes('日志')) { // 模拟执行一个检查日志的任务 const logResult = await checkLogFile('/var/log/syslog'); const response = buildResponse({ to: request.from, from: 'MyAgentName', requestId: request.requestId, status: 'success', body: `日志检查完成。最后一条错误信息:${logResult.lastError}` }); await sendToChannel(response, channel); // 假设的发送函数 await state.updateStatus(request.requestId, 'done'); // 更新状态为完成 } else if (request.task.includes('执行') && request.task.includes('脚本')) { // 如果任务不明确,发起澄清 const clarify = buildClarify({ to: request.from, from: 'MyAgentName', originalRequestId: request.requestId, question: '请提供需要执行的脚本的完整路径或内容。' }); await sendToChannel(clarify, channel); // 状态会在state.track中自动变为‘clarifying’ } else { // 无法处理,返回失败 const response = buildResponse({ to: request.from, from: 'MyAgentName', requestId: request.requestId, status: 'failed', body: `无法理解或处理此任务:${request.task}` }); await sendToChannel(response, channel); await state.updateStatus(request.requestId, 'failed'); } }4.3 消息发送与构建封装
为了便于使用,最好将消息构建和发送封装成更友好的函数。
// lib/protocol-helper.js const { buildRequest, buildResponse, buildClarify, buildHandoff, buildBroadcast } = require('./skills/bot-protocol/lib/builder.js'); class ProtocolHelper { constructor(agentName) { this.agentName = agentName; } async sendRequest({ to, task, context, priority = 'normal' }) { const message = buildRequest({ to, from: this.agentName, task, context, priority }); // 这里调用你的频道发送逻辑 await this._sendToDestination(message, to); console.log(`已发送REQUEST给 ${to}: ${task.substring(0, 50)}...`); } async sendResponse({ to, requestId, status, body, nextAction }) { const message = buildResponse({ to, from: this.agentName, requestId, status, body, nextAction }); await this._sendToDestination(message, to); } // ... 类似地封装 sendClarify, sendHandoff, sendBroadcast async _sendToDestination(protocolMessage, target) { // 这里是具体的发送实现,取决于你的通信通道 // 例如,如果是Discord: // await discordChannel.send(`\`\`\`\n${protocolMessage}\n\`\`\``); // 如果是WebSocket: // wsClient.send(JSON.stringify({ type: 'protocol', data: protocolMessage })); console.log(`[发送至 ${target}]:\n${protocolMessage}`); } } // 在你的Agent中初始化并使用 const helper = new ProtocolHelper('MyAgentName'); await helper.sendRequest({ to: 'Mantis', task: '监控API服务 /api/health 端点,未来5分钟内若连续两次返回非200状态码则告警。', context: '用户报告服务间歇性不可用。', priority: 'high' });5. 高级议题与避坑指南
在实际部署和深度使用bot-protocol时,你会遇到一些设计决策和边缘情况。以下是我在实践中总结的经验和解决方案。
5.1 状态管理的扩展与挑战
项目自带的state.js使用本地JSON文件,这在单机、单进程的Agent场景下是可行的。但在分布式、多实例部署时,就会遇到状态同步的问题。
解决方案建议:
- 使用外部存储:将状态持久化层抽象出来,替换为Redis、PostgreSQL或任何分布式数据库。实现一个
StateStore接口,然后提供FileStore和RedisStore等不同实现。 - 实现乐观锁或分布式锁:在对同一个
requestId的状态进行更新时,使用锁机制防止并发写冲突。在Redis中,这可以通过SETNX命令实现。 - 定期清理:状态文件或数据库表会不断增长。需要实现一个清理任务,定期归档或删除已完成(
done,failed,timeout)且超过一定时间(如7天)的旧状态记录。
5.2 协议版本化与向前兼容
bot-protocol目前是v0.1,随着发展,字段可能会增加或修改。协议设计中的metadata字段(用于存放未知字段)提供了基本的向前兼容性。但更系统的版本化管理是必要的。
最佳实践:
- 在每个协议消息中引入一个明确的
version字段(如Protocol-Version: 0.1)。 - 在解析器(
parser.js)中,根据版本号应用不同的解析规则。对于无法识别的高版本消息,可以降级处理或返回一个CLARIFY消息要求对方使用兼容版本。 - 建立简单的版本协商机制。例如,在首次交互时,Agent可以通过一个特殊的
BROADCAST或握手消息来声明自己支持的协议版本。
5.3 安全性考量
当前协议是明文、无认证的。在开放或不完全受信任的通道(如公共Slack频道)中使用时,存在风险。
加固措施:
- 消息签名:使用非对称加密(如Ed25519)为每条消息生成签名。接收方验证签名以确保消息来源真实且未被篡改。可以在
from字段旁增加一个signature字段。 - 通道加密:确保传输层本身是加密的(如Discord/Slack的TLS,或使用端到端加密的通信层)。
- 权限控制:在解析消息后,增加一层权限检查。例如,一个名为
LogBot的Agent可能只被允许接收task字段包含“日志”关键词的REQUEST,防止越权操作。
5.4 调试与监控
当多个Agent通过协议频繁交互时,调试会变得复杂。一个请求可能经过多个Agent的HANDOFF,最终才得到RESPONSE。
构建可观测性:
- 生成唯一的TraceId:在第一个
REQUEST生成时,除了RequestId,再生成一个全局唯一的TraceId,并在所有后续相关消息(CLARIFY,HANDOFF,RESPONSE)中传递。这样,你可以通过TraceId在日志中串联起一次完整任务的整个生命周期。 - 结构化日志:将协议消息的关键字段(
type,from,to,requestId,traceId,depth)记录到结构化日志系统(如JSON格式)。便于使用ELK Stack或类似工具进行聚合查询和链路追踪。 - 可视化看板:基于状态存储的数据,开发一个简单的Web看板,实时展示所有进行中的对话、它们的当前状态、深度、耗时等信息。这对于运维和调试有巨大帮助。
5.5 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 消息被对方忽略,无任何反应。 | 1.to字段名称拼写错误。2. 对方Agent未运行或未加载协议处理器。 3. 消息格式错误,被解析器返回 null。 | 1. 检查发送消息的to字段与接收方Agent注册的名称是否完全一致(大小写敏感)。2. 确认接收方Agent进程存活,并检查其日志是否有消息接收记录。 3. 在发送前,用 parse()函数试解析一下你构建的消息,确保不为null。 |
收到“Depth limit reached”错误。 | 当前对话深度已达到最大值(默认5),协议禁止继续发起REQUEST等消息。 | 1. 检查当前depth值。这通常意味着任务链过长,可能需要重新设计任务分解逻辑。2.必须发送一个 RESPONSE来结束当前链,即使结果是failed或partial_success。3. 考虑是否应该使用 BROADCAST寻找其他解决方案,而不是线性传递。 |
状态文件 (bot-protocol-state.json) 损坏或为空。 | 多进程同时写入导致JSON格式破坏,或程序在写入时异常退出。 | 1. 实现文件锁或迁移到数据库。 2. 定期备份状态文件。 3. 在读取状态文件时,使用 try...catch,如果解析失败,则尝试从备份恢复或初始化一个空状态。 |
CLARIFY消息发出后石沉大海。 | 1. 请求方Agent崩溃或离线。 2. 请求方未正确处理 CLARIFY消息类型。3. 网络或通道问题。 | 1. 依赖state.checkTimeouts(),CLARIFY默认10分钟超时,超时后状态会变更为timeout,可触发告警。2. 在请求方Agent中,确保 CLARIFY消息类型有对应的处理逻辑,并能生成包含澄清信息的RESPONSE。3. 建立心跳或存活检测机制。 |
| 任务被重复执行。 | 相同的RequestId被意外重复使用,或状态未正确更新为done。 | 1. 确保RequestId生成算法具有足够的随机性和唯一性(如UUID v4)。2. 在执行任务前,检查状态文件中该 requestId的状态是否为open,避免重复处理。3. 任务完成后,立即调用 state.updateStatus(requestId, 'done')。 |
这套bot-protocol为AI Agent的协作提供了一个坚实、可扩展的基础。从我自己的使用经验来看,初期引入它会增加一些开发复杂度,但一旦跑通,整个多Agent系统的可靠性和可维护性会有质的提升。它迫使你以结构化的方式思考Agent之间的交互,而这种思考本身就能帮你发现系统设计中的模糊点和脆弱环节。