Omnilude 的 ai-service 中,Agent 是这样运行的
开始之前
这篇文章是我之前写的 Omnilude 的 ai-service 为什么采用 Assistant / Thread / Run 结构 的下一篇。上一篇主要讲了 assistant、thread、run 分别扮演什么角色,这次我想继续往下讲,说明 agent 在这套结构之上究竟是怎么运行的。
在解释这套结构时,我想先去掉一个误解。Omnilude 的 AiAgent 并不是什么巨大的魔法黑盒。它也不是把 assistant 变成另一种东西的额外层。恰恰相反,AiAgent 更接近一种把多个 assistant 按节点组织起来,并把执行顺序保存为 workflow 的结构。
换一种更容易理解的说法:
如果
AiAssistant是单个 LLM 的预设,那么AiAgent就是把这些预设连接起来的工作流。
这篇文章会尽量用不难懂的方式,说明这条执行链在代码里是如何实现的。
Agent 不是另一个 AI,而是 workflow
首先要看的就是 AiAgent 实体。这里面当然也有名称、描述、类型,但真正的核心是 workflow 字段。这个值不是存在独立表里,而是以 JSONB 的形式保存。
也就是说,在 Omnilude 里,agent 与其说是「一个更聪明地调用模型的对象」,不如说更接近一个由节点和边表示的执行图。
例如,一个 agent 可以长这样:
- 从起始节点接收输入。
- 在
generate-text节点里用一个 assistant 生成文本。 - 在
router节点里决定下一条路径。 - 最后由
finish节点整理结果。
看到这里,assistant 和 agent 的区别就会清晰很多。
- assistant 是一次推理用的预设。
- agent 是决定这些推理按什么顺序、在什么条件下连接起来的 workflow。
Agent 不是另一个 AI,而是 workflow(文本参考)
AiAssistant
- 单次 LLM 调用的预设
- 定义要使用哪个 model/provider/instructions
AiAgent
- 连接多个节点的 workflow
- 每个节点都可以使用 assistant,也可以像 tool 一样工作
- 关键不在于「一次调用」,而在于「执行顺序和连接关系」
请求不会立刻执行
在 Omnilude 中,AiAgent 的执行大多数时候不会以一次同步函数调用结束。这里真正重要的角色是 DTE(Distributed Task Executor)。
整体流程大致如下:
- 客户端或后台发起 agent 执行请求。
- 服务端不会立刻处理,而是把它转换成一个 DTE job。
WorkflowTaskHandler从队列中取出这个 job。- 接着由
SingleWorkflowExecutor负责真正的 workflow 执行。 - 在内部,
BasicWorkflowEngine会逐个推进节点。
这套结构重要的原因其实很简单。agent 执行往往比想象中更长,可能带流式输出,需要记录中间状态,有时还必须放到后台跑。如果把这些都硬塞进普通的 request-response 里,稳定性会很差。把执行本身当成 job 来管理,反而更合适。
请求不会立刻执行(文本参考)
Controller
-> AiAgentExecutor
-> DistributedTaskQueue
-> WorkflowTaskHandler
-> SingleWorkflowExecutor
-> BasicWorkflowEngine
关键点
- 执行被当成 job 来处理
- 流式输出和后台处理会更容易
- 长时间执行也能在同一套结构里管理
起点只有一个 start node
有 workflow 并不意味着引擎会从头到尾按列表顺序遍历。真实的 BasicWorkflowEngine 会先找到 startNode=true 且类型以 -input 结尾的节点,然后把初始输入注入进去,从这里开始执行。
我很喜欢这一点。因为它并不是把 agent 看成「一条命令」,而是看成一个入口明确的执行图。
比如 text-input 节点会先准备用户输入的字符串,并把这个值传给下一个节点。接着 generate-text 节点收到这个值,触发一次 LLM 调用。之后 output 会沿着 edge 继续传给下一个节点。
换句话说,agent 不是一个巨大的函数,而是一串由小执行单元组成的流程。
真正执行节点的是 NodeExecutor
这里最关键的一层就是 NodeExecutor。每个节点都有自己的类型字符串,NodeExecutorFactory 会根据这个类型找到对应的执行器。同时,NodeExecutorRegistry 会扫描 @NodeType 注解,自动注册「哪个执行器处理哪个类型」。
正因为有这层结构,新增节点时不需要重写整个引擎。只要实现一个节点、声明它的类型、挂上 provider,它就能进入运行时。
这篇文章里最值得先理解的代表性节点有四个。
TextInputNode: 准备起始输入。GenerateTextNode: 使用 assistant 生成实际文本。RouterNode: 用 LLM 判断下一条路径。FinishNode: 汇总最终结果并结束执行。
只要理解这四个节点,就已经能大致看懂 Omnilude 的 agent 结构是怎么运转的。
即使在 Agent 内部,真正的推理仍然由 assistant 完成
这里就和上一篇文章重新连起来了。
即便有了 AiAgent,也不是 agent 自己直接去调用模型。真正的 LLM 调用发生在节点内部,而在那个时刻,AiAssistantService 会再次出现。
例如 GenerateTextNode 会从配置中读取 aiAssistantId。然后它通过这个 id 找到 assistant,再把 AiModel、AiProvider、AiApiKey 组合起来,构造成一个 RunnableAiAssistant。到这里之后,真正的 chat model 调用才会发生。
所以,理解这套结构最自然的方式是:
- assistant 是推理用的组件。
- agent 是决定这些组件按什么顺序运行的 workflow。
一旦理解了这一点,也就不需要再把 assistant 和 agent 看成彼此竞争的概念。它们不是替代关系,而是层级关系。
即使在 Agent 内部,真正的推理仍然由 assistant 完成(文本参考)
AiAgent
- 拥有 workflow 的上层执行单元
Node
- 在需要时选择并调用 assistant
RunnableAiAssistant
- 由 assistant + model + provider + key 组合成的真实执行对象
在 /ai-runs 路径上,agent 是叠在对话上下文之上的
还有一个很有意思的点是 /ai-runs 这条路径。它并不是一个单纯把已保存的 agent 直接执行掉的 API。它会先读取 thread 中的 message,再基于这些上下文去执行某个特定的 AiAgent workflow。
在当前实现里,ChatAgentTaskHandler 会挑选像 AiAgentType.USING_WEB_SEARCH_TOOL 这样的 agent 类型,并把它交给 SingleWorkflowExecutor。也就是说,在这个阶段,run 已经不只是一次简单的 LLM 调用,而是一个在对话上下文之上启动 agent workflow 的命令。
这也是 Omnilude 的 ai-service 看起来更像平台而不是轻量模型封装的原因之一。同样的推理结构,一旦放到 thread/message/run 这套接口之上,它的产品意义就完全不同了。
我刻意没有把 multiagent 放进这篇文章
继续往下读代码时,你也会看到 multiagent 包。但它和这里正在说明的 AiAgent workflow 方向并不完全一样。它更像是一个单独的编排层,用来处理多 agent 协作、动态路由、重试、人类审核这类更大的问题。
更重要的是,它现在还有一些进行中的部分,所以我并不想把它和当前 AiAgent runtime 放在完全同一层来解释。因此这篇文章里我刻意把它拿掉了。这里的范围始终只聚焦于 如何通过组合 assistants 来运行一个 agent workflow。
范围收窄之后,文章会更容易理解,说明也不会发散。
我认为这套结构相当现实
重新看一遍就会发现,Omnilude 的 ai-service 里的 agent 并不是什么名字很大的另一种 AI,也不是套在 assistant 外面的又一层 wrapper。相反,它其实更务实。
- 执行流程保存在
AiAgent.workflow - 实际执行通过 DTE job 交出去
- 引擎从起始节点开始沿着 edge 前进
- 每个节点在需要时调用 assistant 来使用 LLM
我觉得这套结构好用,是因为它没有夸大扩展性。assistant、thread、run、agent 各自负责不同的角色,而且各自的边界相对清晰。
我越来越觉得,当 AI 功能真正进入产品之后,这种结构才是更重要的部分。比起「把模型调用好一次」,更能留下来的其实是:以什么单位保存、通过什么接口运行、又通过什么流程连接起来。
结尾
上一篇文章里,assistant / thread / run 是构成 Omnilude 执行平台的基础概念。这一篇里的 agent 则是叠在其上的组合层。AiAgent 不是另一个 AI,而是一个保存 workflow 的实体,它的执行会经过 DTE -> WorkflowExecutor -> NodeExecutor 这条路径。
而真正的推理依然由 assistant 完成。归根结底,Omnilude 的 ai-service 并不是让 assistant 和 agent 对立起来,而是通过把 assistant 当成零件来组装 agent 的方式不断扩展。
下一篇我会再往实战走一步,用更具体的例子来说明,我究竟是怎么设计实际的 workflow JSON 和节点结构的。