译者 | 核子可乐
审校 | 重楼
2025年将成为AI智能体之年。在本文的场景中,AI智能体是一套能够利用AI通过一系列步骤实现目标的系统,且具备就结果进行推理及更正的能力。在实践中,智能体遵循的步骤可总结成图表形式。
我们将构建一款响应式应用(对来自用户的输入做出响应),帮助人们规划自己的完美假期。此智能体将根据用户指定的餐食、海滨和活动需求,在指定的国家/地区内推荐最佳城市。
智能体基本架构如下:
在第一阶段,智能体将并行收集信息,根据单一特征对各城市进行排名。最后一步代表根据信息选出的最佳城市。
本用例仅使用ChatGPT执行所有步骤,大家也可根据需求配合搜索引擎。这里使用Fibry中手动添加的Actor系统以显示图形并细化控制并行性。
Fibry是一款轻量化Actor系统,允许参与者轻松简化多线程代码,且不涉及任何依赖项。Fibry还提供有限状态机,这里我们将对其扩展以实现Java编程。
这里建议大家使用Fibry 3.0.2,如:
设定提示词
第一步是设定大模型所需要的揭示词:
设定状态
一般我们会在四个步骤中各设定一个状态。但由于分支往来比较常见,因此这里专门添加功能来仅使用一个状态处理此问题。因此,我们只需要用到两个状态:CITIES,即收集信息的城市,以及CHOICE,即我们选定的城市。
设定上下文
智能体中的各步骤将收集存储在他处的信息,我们称之为上下文。理想情况下,每个步骤最好各自独立,且尽可能少触及其他步骤。但既要保持实现简单、使用的代码量不大,同时保持尽可能多的类型安全性与线程安全,显然不是件容易的事。
因此这里选择强制上下文记录,提供部分功能以更新记录的值(使用下面列出的反射),同时等待JEP 468(创建派生记录)的实现。
设定节点
现在我们可以设定智能体的逻辑。本用例允许用户使用两种不同的大语言模型,如用于搜索的“普通”模型和用于选择步骤的“推理”模型。
到这里开始上难度了,因为信息密度很大:
大家肯定已经猜到,modelSearch代表用于搜索的模型(如ChatGPT 40),
modelThink代表“推理模型”(如ChatGPT o1)。Fibry提供一个简单的大模型接口和一个简单的ChatGPT实现,并通过ChatGpt类进行公开。
请注意,调用ChatGPT API需要相应的API密钥,你需要使用“-DOPENAI_API_KEY=xxxx” JVM参数来定义此密钥。
还有一个跟Fibry理念相关的小问题,因为其不涉及任何依赖项,所以这在JSON中会比较麻烦。这里Fibry可以通过两种方式运行:
- 若检测到Jackson,Fibry将使用它进行反射以解析JSON。
- 若未检测到Jackson,则使用简单的自定义解析器(似乎可与ChatGPT输出搭配使用)。但这种方法仅适用于快速测试,不推荐在生产环境下使用。
- 或者,你也可以提供自己的JSON处理器实现并调用JsonUtils.setProcessor(),也可查看JacksonProcessor以获取灵感。
- replaceField() 和 replaceAllFields()方法由RecordUtils 定义,且只是替换提示词中文本内容的便捷方法,以便我们将数据提供给大模型。 setAttribute()函数用于设置状态中属性的值,而无需手动重新创建记录或定义“withers”方法。大家也可以使用其他方法,例如 mergeAttribute(), addToList(), addToSet()和 addToMap()。
构建智能体
逻辑已经有了,接下来需要描述各状态间的依赖关系图并指定希望实现的并行性。对于生产运行状态下的大型多功能体系统,最重要的就是既通过并行性实现性能最大化,又不致耗尽资源、达到速率限制或者超过外部系统的承载上限。这就是Fibry的意义所在,它能让整个设计思路非常明确,而且设置难度也不算高。
首先创建智能体builder:
其中参数autoGuards 用于对状态设置自动保护,其以AND逻辑执行,且仅在处理完所有传入状态后才会执行该状态。
若参数为false,则每个传入状态调用一次该状态。
在以上示例中,若目标是执行两次D,分别在A和C之后,则autoGuards应当为false。若希望在A和C之后再执行一次D,则autoGuards应为true。
这里继续说回咱们的度假智能体。
让我们从addState()方法开始。它用于指定某个状态应跟踪另一状态并执行某个逻辑。此外,大家还可以指定并行性(后文具体介绍)和guards。
在本示例中:
- 状态为CHOICE
- 无默认的后续状态
- 并行性为1
- 无guard
下一状态仅为默认状态,因为节点可能会覆盖下一状态,因此上图可以在运行时动态变更,特别是可以执行循环。例如需要重复某些步骤以收集更多或更好的信息这类高级用例。
并行性在这里没有涵盖,因为智能体的单次运行不太涉及这个问题,但在大规模生产中却非常重要。
在Fibry中,每个节点都由一个actor支持——所谓actor,其实就是一个包含待处理消息列表的线程。每条消息都代表一个执行步骤。因此,并行度指可以一次执行的消息数。具体来讲:
- parallelism == 1 代表只有一个线程管理该步骤,因此每次只能执行一条。
- parallelism > 1 代表有一个线程池支持该actor,线程数由用户指定。默认情况下使用虚拟线程。
- parallelism == 0 代表每条消息都会创建一个由虚拟线程支持的新actor,因此并行度可根据需求尽量调高。
每个步骤均可独立配置,因此大家可以灵活配置性能和资源使用情况。请注意,如果parallelism != 1则可能存在多线程,因为与actor相关的线程限制经常会丢失。
状态压缩
如前所述,多个状态彼此关联也是常见情况,比如需要并行执行和加入,而后才能转向公共状态。这时候我们不需要设定多个状态,而只使用其一:
在这种情况下,我们看到CITIES 状态由三个节点定义,其中addStateParallel()负责并行执行各节点并等待所有节点执行完成。这时候应该在每个节点上应用并行性,借此获取三个单线程actor。
请注意,如果不使用autoGuards,则可将OR 与 AND逻辑混合起来。
如果希望合并一些处于相同状态的节点,但要求其按顺序执行(例如需要使用前一个节点生成的信息),则可使用 addStateSerial()方法。
AI智能体的创建很简单,但需要指定相关参数:
- 初始状态
- 最终状态(可以为null)
- 尽量并行执行的状态标记
现在我们已经有了智能体,调用进程即可使用:
此版本的 process() 需要两个参数:
- 初始状态,其中包含智能体执行操作所需要的信息
- 可选监听器,支持如打印各步骤输出等需求
若需要启动操作并检查其后续返回值,可以使用 processAsync()。
如果大家关注并行选项的更多信息,建议各位查看单元测试 TestAIAgent。它会模拟节点休眠一段时间后的智能体,借此查看各选项的实际影响:
扩展至多智能体
我们刚刚创建的是一个actor智能体,它会在自己的线程上(加上各节点使用的所有线程)运行,并实现了Function接口以备不时之需。
多智能体其实没什么特别,基本逻辑就是一个智能体的一个或多个节点要求另一智能体执行操作。我们可以构建一套智能体库以将它们良好组合起来,从而简化整个系统。
接下来,我们要利用之前的智能体输出计算度假费用,以便用户判断是否符合需求。到这里,是不是就跟真正的旅行社很像了?
下图为构建流程:
首先用提示词来提取目的地并计算成本。
这里只需两个状态,一个用于研究城市(由上一智能体完成),另一个用于计算费用。
我们还需要上下文,此上下文负责保存上一智能体的提议。
之后可以定义智能体逻辑,该逻辑需要另一智能体作为参数。首节点调用上一智能体以获取提议。
第二节点负责计算成本:
之后是定义图表并构建智能体:
最终输出
假设我们说自己想跳萨尔萨舞和巴恰塔舞,得到的长输出如下:
目的地
提议
费用
内容着实不少,而且这还只是两个“推理”模型的输出!
但结果非常有趣,那不勒斯也确实是个不错的选项。接下来我们检查一下中间结果,发现得出结论的过程相当合理。
中间输出
如果感兴趣,大家还可以查看中间结果。
餐食
海滨
活动
有趣的是,那不勒斯在各个分段排名上都没登顶,但综合下来却是最优选项。
许可细节
这里再聊几句关于Fibry许可证的情况。FIbry目前已经不再以纯MIT许可证的形式发布。最大的变更是,如果大家想要急雨 套系统来为第三方(如软件工程师智能体)大规模生成代码,则需要申请商业许可证。此外,它还禁止用户将其作为数据集来训练系统生成代码(例如ChatGPT不得在Fibry的源代码上进行训练)。除此之外,所有用途都不受影响。
总结
希望这篇文章能帮助大家了解如何使用Fibry编写AI智能体。其实对于分布在多个节点上的多智能体系统,Fibry也不在话下!但受篇幅所限,这里不过多展开。
在Fibry中,通过网络的消息发送和接收会被抽象出来,因此无需修改智能体逻辑即可实现分发。这使得Fibry能够轻松实现跨节点扩展,核心逻辑完全不受影响。
祝大家编码愉快!
原文标题:Designing AI Multi-Agent Systems in Java,作者:Luca Venturi