Ohhnews

分类导航

$ cd ..
foojay原文

智能体协商无需大语言模型:用合同网协议完成任务分配

#多智能体#合同网协议#协商机制#java#agenor

在会协商的智能体中,协商过程通常最终会落到一个语言模型里。你用提示词描述情境,模型给出回答,然后你解析返回的内容。这能行,但每个决策都要付出一次网络往返和账单上的一行费用。

对于把工作交给应做之人这一狭窄场景,存在一个早于这一切的协议。它叫 Contract Net(合同网),小到可以用一个下午读完。Agenor 内置了它。下面的示例中,一个 manager 在单个 JVM 内把一项工作交给三个 worker 之一,而它的 import 中不包含任何模型提供方的东西。

它的结构

一个智能体有工作需要分发出去。它向可能承接这项工作的智能体广播一份提案征集。每个智能体要么给出一个报价,要么拒绝。调用方选出一个报价,告诉那个智能体它被接受了,然后等待结果。

这些消息类型都有名字,借自这一切所源自的智能体通信语言:先是 CFP,然后是 PROPOSE 或 REFUSE,接着是 AGREE,工作结束后是 INFORM 或 FAILURE。Agenor 在 Performative 中定义了十种。Contract Net 使用上述六种,再加上 CANCEL,调用方在仍在收集响应时可以发送它。

投标的一方

$ java
@DialogueHandler(performatives = Performative.CFP)
public void handleCFP(DialogueMessage msg) {
    Task task;
    try {
        task = msg.contentAs(Task.class);
    } catch (IllegalArgumentException e) {
        task = null;
    }
    if (task == null || task.type() == null) {
        dialogue.refuse(msg, "Invalid task");
        return;
    }

    double cost = task.complexity() / efficiency * (0.9 + random.nextDouble() * 0.2);
    int time = (int) (task.complexity() / 10 / efficiency);
    dialogue.propose(msg, new Bid(cost, time));
}

这个注解注册了处理器,没有其他注册代码。方法如何处理这次调用由你自己的代码决定,这里就是两行算术:worker 根据自己的效率给工作定价,再加一点噪声。决定一项工作对一个智能体值多少的规则也放在同一个地方,用普通 Java 写出来。

选择的一方

$ java
List<DialogueMessage> proposals = responses.stream()
    .filter(r -> r.performative() == Performative.PROPOSE)
    .toList();

if (proposals.isEmpty()) {
    return CompletableFuture.completedFuture("NO_PROPOSALS");
}

DialogueMessage best = proposals.stream()
    .min(Comparator.comparingDouble(p -> p.contentAs(Bid.class).cost()))
    .orElseThrow();

dialogue.reply(best, Performative.AGREE, task);

responses 来自 callForProposals(workerIds, task, Duration.ofSeconds(10)),这是一个 future,会在超时之前以已到达的内容完成。一个下线的 worker 什么也不贡献,调用方会在没有它的情况下继续。

运行这个示例会得到三个报价和一个赢家:

[Manager] Broadcasting CFP to 3 workers
[worker-1] PROPOSE: cost=179.16
[worker-3] PROPOSE: cost=235.86
[worker-2] PROPOSE: cost=102.97
[Manager] Proposals:
  - worker-2: cost=102.97, time=11s
  - worker-1: cost=179.16, time=16s
  - worker-3: cost=235.86, time=25s
[Manager] Selected: worker-2
[worker-2] Task done!

这些数字在每次运行之间会变化,因为报价里有一个随机因子。排名保持不变:三个 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。