智能体协商无需大语言模型:用合同网协议完成任务分配
在会协商的智能体中,协商过程通常最终会落到一个语言模型里。你用提示词描述情境,模型给出回答,然后你解析返回的内容。这能行,但每个决策都要付出一次网络往返和账单上的一行费用。
对于把工作交给应做之人这一狭窄场景,存在一个早于这一切的协议。它叫 Contract Net(合同网),小到可以用一个下午读完。Agenor 内置了它。下面的示例中,一个 manager 在单个 JVM 内把一项工作交给三个 worker 之一,而它的 import 中不包含任何模型提供方的东西。
它的结构
一个智能体有工作需要分发出去。它向可能承接这项工作的智能体广播一份提案征集。每个智能体要么给出一个报价,要么拒绝。调用方选出一个报价,告诉那个智能体它被接受了,然后等待结果。
这些消息类型都有名字,借自这一切所源自的智能体通信语言:先是 CFP,然后是 PROPOSE 或 REFUSE,接着是 AGREE,工作结束后是 INFORM 或 FAILURE。Agenor 在 Performative 中定义了十种。Contract Net 使用上述六种,再加上 CANCEL,调用方在仍在收集响应时可以发送它。
投标的一方
这个注解注册了处理器,没有其他注册代码。方法如何处理这次调用由你自己的代码决定,这里就是两行算术:worker 根据自己的效率给工作定价,再加一点噪声。决定一项工作对一个智能体值多少的规则也放在同一个地方,用普通 Java 写出来。
选择的一方
responses 来自 callForProposals(workerIds, task, Duration.ofSeconds(10)),这是一个 future,会在超时之前以已到达的内容完成。一个下线的 worker 什么也不贡献,调用方会在没有它的情况下继续。
运行这个示例会得到三个报价和一个赢家:
这些数字在每次运行之间会变化,因为报价里有一个随机因子。排名保持不变:三个 worker 的效率是固定的,所以每次都是 worker-2 胜出。
框架贡献了什么
这个协议是一个状态机,写在 ContractNetProtocol 中。PROPOSE 或 REFUSE 会让调用方留在 AWAITING_RESPONSE,继续收集响应。AGREE 会把它移到 AGREED。INFORM 会以 COMPLETED 结束它,FAILURE 则以 FAILED 结束。
它还定义了 AGREE 之后由谁来做工作的规则:在 Contract Net 下,就是发出报价的那个智能体,而 Agenor 会查询 Protocol.senderPerforms,因为这个答案会随协议不同而变化。
它的边界在哪里
对于当前状态来说顺序不对的消息,比如对话已经处于 AGREED 之后又来了一个 PROPOSE,不会阻塞任何东西:它仍会进入历史记录,状态机也仍会执行它的转换,而对这些协议来说,转换会让状态保持不变。唯一的影响是一条 WARN 日志,其中会写明对话、发送方和状态。如果在那里抛出异常,反而会破坏所有已经存在、但并未严格遵循协议的对等方。对于无视规则的对等方来说,实际结果和完全没有检查一样:只有那条日志会有所不同。
对话状态存在于内存中,位于每个智能体自己的进程里。没有中央的对话注册表。每个智能体只保留自己参与的那些对话。
重启会抹掉这些状态。智能体会失去它正在跟踪的对话以及它记录过的承诺,而对等方不会收到通知。剩下的只有对等方在调用时设置的超时,而这个超时就是故障转移机制。所以不要把任何需要持久化的东西建立在 Conversation 上。如果业务流程必须经受重启,就把它保存在智能体自己的持久状态(@Persist)中,并用对话来承载各个步骤。
还剩一个序列化陷阱。contentAs() 会转换消息内容,这就是示例调用它、而不是把 msg.content() 强制转换为 Task 的原因:只要每个智能体共享同一个 JVM,这种强制转换就能工作;但如果在底层放一个会序列化的传输层,负载到达时就会变成 Map,于是强制转换会抛异常。
模型该放在哪里
模型该用在决策无法塞进成本函数的地方:阅读一份以散文形式到达的工作,或者权衡那些无法归结为单个数字的报价。智能体可以在同一个处理器内部为那个决策调用模型。上面的示例从提案征集走到工作完成,全程没有调用模型。
上面的代码是 ContractNetExample,位于 examples 模块中,标签为 v0.35.0。