编排优先——Go 语言开发 AI 智能体的设计与实现

0 阅读21分钟

2025 年 1 月,基于 Go 语言的大模型应用综合开发框架 Eino 在 CloudWeGo 正式开源。Eino 是字节跳动开源的大模型应用开发框架,具备稳定的内核、灵活的扩展性以及完善的工具生态,可靠且易于维护,依托豆包、抖音等应用所积累的丰富实践经验。本文由 CloudWeGo 深圳站技术沙龙的演讲实录整理而成。

Eino 项目的定位为一个 AI 应用的开发框架。在深入探讨之前,我们首先需要明确 AI 应用的核心概念。显然,大模型是构建任何 AI 应用的关键组成部分。本次介绍将分为三个部分:首先将解释 Eino 如何帮助我们在 AI 应用开发过程中解决各种挑战;第二,通过一个具体案例,展示如何利用 Eino 进行智能体开发,以便大家直观理解其应用方式;最后,会介绍一下Eino的现状。

AI 应用开发的挑战与 Eino 的解法

AI 应用:围绕大模型的信息流

从图中可见,AI 应用的核心为“ChatModel”,即大模型。大模型的主要特性是其信息生成能力。例如,它能提供我们之前未知的问题解决方案,或者将杂乱无章的信息整理成有序的内容。此外,大模型还能进行文本润色或风格转换,实现信息的深度加工。因此,大模型无疑是AI应用的核心所在。

有了这个核心,AI 应用的整体架构便围绕大模型展开,构成一个明确的信息流程,包括信息的输入、输出及中间的处理步骤。这种架构确保了 AI 应用能够有效地处理和转化信息,实现其功能目标。

从图中可以看到,“ChatModel”左侧有几个框。最上方的框名为“ChatTemplate”,它作为提示模板的角色,主要负责信息的格式化与增强处理;而位于下方的“Retriever”框则负责从知识库中召回信息,进行必要的信息检索。在“ChatModel”的右侧,我们有一个“Tool”框,这是用于进一步的信息处理。

要构建这样一个结构的AI应用,我们需要完成几个关键步骤。首先,我们需要为每个框定义具体的实现方式,即选择合适的组件来充实这些框架,这是第一个关键的组件问题。其次,我们需要解决这些组件之间的连接问题,确保信息能够有效流动。这种信息流的组织方式,即我们所说的拓扑结构,需要通过精心的 编排来实现,以保证信息流转的高效和有序。

可枚举的组件

接下来,我们将重新审视之前提到的组件问题。作为研发人员,开发AI应用前需要了解两个关键方面。首先,必须清楚哪些类型的组件可用于构建 AI 应用;其次,在熟悉了组件类型的清单后,需要确定每种类型的组件都有哪些具体的组件实现可供选择。这两个清单对研发人员至关重要。

例如,图中列出了三种主要的组件类型:大模型“ChatModel”,知识库“Retriever”,以及工具“Tool”。每种组件都有明确的接口定义,包括输入输出规范和流式处理范式,这些都会在各自的接口文档中详细说明。图右侧展示了针对每种组件接口的示例,Eino框架中提供了一些通用、实用且常用的组件实现案例。

因此,这两个清单是 Eino 首先向用户提供的重要信息。开发者只需接入 Eino 一次,了解 Eino 对这三种组件的抽象定义,然后可以依靠 Eino 框架来选择和对接具体的组件实现。这样的流程简化了开发者的工作,使得快速开发和部署AI应用成为可能。

信息流编排

在前文中,我们已经讨论了组件问题,即作为基本构成部分的组件。接下来,我们将探讨编排问题——如何将这些组件根据业务逻辑需求有效地串联起来。以图中为例,整个信息流展示了以下特点:

首先,每个框所对应的组件类型是预定义的。例如,图中的“ChatModel”框必须由符合该接口类型的组件填充,无法用其他类型的组件替换。这说明我们的编排基于组件类型的抽象层面,而非针对具体的组件实现。每个节点的输入和输出因此是固定的。

其次,组件间的连接方式是根据业务需求设定的,具有明确的业务特定性。例如,图的最左侧从起始节点到“ChatTemplate”和“Retriever”之间呈现并行扇出的结构,这是一种一对多的拓扑结构;从“ChatTemplate”到“Retrieval”再汇聚到“ChatModel”则形成了多对一的信息合并。此外,还包括分支判断以选择不同的执行路径,以及从“Tool”返回“ChatModel”的回环结构,这些都是相对复杂的结构。当然,也包括最简单的直线连接。

通过这些分支、扇出、扇入、回环和判断等结构,我们可以构建出适应不同业务场景的AI应用。这个示例较为典型,涵盖了常见的拓扑结构,并且根据这些不同的组件,我们还能实现更多类型的控制流效果。这种灵活的编排能力是搭建高效、响应业务需求的AI应用的关键。

此外,图的外围包含了一些被称为“external”的元素,例如外部存储或外部工具的API等。这些元素允许不同的组件采取各种实践方式,并处理状态保存、数据存储以及API交互逻辑等问题。在框架内部,由这些核心部分组成的数据流或信息流是无状态的。所谓“无状态”,意味着不保留跨请求的状态依赖,每次请求所需的数据完全来源于外部,不依赖于内部的状态。这种设计使得我们的信息流可以实现水平扩展,不会由于依赖特定的缓存或内部状态而限制后续请求只能由特定实例处理。即便在单次请求内存在状态,也应在该请求内解决,确保状态的影响范围限定于单个请求内部。例如,在一次智能体调用过程中,多次循环间的本地内存可以看作是本地状态,而所有外部状态则都是可插拔的,可以随时被替换。

此前,我们主要讨论了控制流的过程,即节点和信息在图中的流动方式。除此之外,由于AI应用是围绕大模型构建的,大模型的输出通常具有流式特性,即逐渐输出信息而非一次性提供完整答案。因此,处理这种连续的数据流行为对于AI应用的设计和实现至关重要。

数据流处理

在下图中,左侧的输入可以是一个数据流,也可以是非流式数据。如果输入为数据流,则由多帧数据组成一个序列。当数据流进入到编排系统后,首先进行流复制操作,即将一个数据流复制成多个流。这是因为数据流的消费通常是一次性的,一旦被消费可能就无法再被重复使用。

随后,进行流合并,即将多个独立的数据流合并成一个单一的数据流,以便传递给中间处理节点。接下来是流的输出过程。在图中,红色节点表示非流节点,例如“Tool”,这类节点只能处理完整的数据集,而不是流式数据。因此,当数据流到达这种节点时,需要进行流拼接,即将流式数据中的各帧数据组合成一个完整的数据集。

当这类节点需要输出数据时,它输出的是完整的数据集。如果这些数据需要再次被中间的圆形节点处理成数据流,这就涉及到流的转换处理,包括流复制、流合并、流拼接和流装箱等步骤。这些过程都由Eino框架自动完成,为用户提供了一种无缝的解决方案,以便高效处理各种数据流转换需求。

此外,该框架采用事件驱动的设计。在这种设计中,不仅输入和输出节点可以被视为事件,整个图的执行过程中的每一个步骤,甚至每一帧数据都被视作独立的事件。以图中的事件示例来看,对于“feed”节点,系统可以生成多种事件,如节点的启动、完成以及错误事件;而对于图上方的事件,我们可以捕获到每个流节点的数据输入和输出事件。

因此,这表明该框架具备强大的流式事件流处理能力,能够在数据流动、节点活动等各个方面实时响应并处理事件。这种事件驱动的机制不仅增强了框架的灵活性和响应能力,也使得其更适合处理复杂且动态变化的数据流场景。

Eino的整体结构

接下来,我们将介绍系统的整体结构。整个架构中包含两个核心库:左侧的Eino和右侧的Eino-Ext,此外还有一个名为Eino examples的示例库。

左侧的核心库Eino包括以下几个关键部分:

  • 核心Schema:位于最底层,这是一个结构体,用于定义和抽象AI应用领域的领域模型。它包括处理数据流的功能,如流的读取和写入。

  • Components:位于上一层,这是一个组件类型的抽象列表,目前定义了8种组件类型,包括它们各自的输入输出规范、流式处理范式和回调事件信息。

  • Compose:这是核心的编排能力部分,包含编排的基本组成元素和能力。

  • 引擎层:这一层包括两个不同的引擎,它们的核心区别在于节点间触发下一个节点的方式。

  • API层:这一层有三套API:

    • Chain:一个简单的线性链式结构。
    • Graph:一个灵活的数据流和控制流合一的图引擎。
    • Workflow:这是一种新型结构,能够解耦数据流和控制流,允许灵活配置数据流节点间的数据映射关系。
  • Flow:最顶层包含一些预置的编排产物,如 ReAct Agent 和 Multi Agent 等,这些都是基于compose builder能力构建的。

右侧的 Eino-Ext 包含不同组件的具体实现,这包括回调事件处理器的实现及一些开发工具,例如 IDE 插件。

通过这种结构设计,Eino 框架不仅提供了强大的核心功能和灵活的拓展能力,还能通过不同的API和工具支持开发者高效地构建和管理AI应用。

实战:“计划——执行”多智能体

下面我们将通过一个实际案例来展示如何开发一个多智能体系统,即根据用户偏好制定主题乐园的一日行程安排。若要开发一个应用程序(APP)来解决这一问题,通常的第一步是进行领域建模或需求分析。

在这一过程中,首先需要明确核心的数据结构。以“Activity”为例,这个结构体用于定义乐园中的各种活动。在定义这些结构体时,有两个重要方面需要考虑:

  1. 命名清晰性:结构体及其字段的命名需要足够清晰和表意明确,以便于模型能够轻松理解。这有助于后续的数据处理和逻辑实现。
  2. 模型辅助标签:使用如jsonschema之类的标签来为模型提供额外信息,这些标签帮助模型更好地理解每个字段的用途和位置。

通过这样的方法,我们不仅定义了数据的结构,还增强了模型对数据的理解能力,从而为开发高效且功能强大的多智能体系统奠定了基础。这些初步的步骤是至关重要的,因为它们直接影响到后续开发和系统的整体性能。

接下来的步骤是开发一些专用工具或服务,这些服务将作为APP的基础工具。举一个例子,我们可以创建一个用于获取游乐设施信息的函数,该函数未来将被封装成一个工具函数。在将服务函数转换为工具时,需要特别注意以下两点:

  1. 函数签名的标准化:根据框架的要求,每个工具函数的签名必须固定为包含contextrequest的输入参数,以及包含responseerror的输出。这样的规定确保了工具函数的一致性和可预测性,便于集成和维护。
  2. 入参校验与异常处理:由于这些函数可能会被模型直接调用,而模型生成的输入参数可能不完全可控,因此进行严格的入参校验和异常处理变得尤为重要。这不仅保护了系统的稳定性,也确保了数据的准确性和安全性。

通过实施这些指导原则,我们可以确保开发的工具不仅符合框架的技术要求,还能在实际应用中提供稳定可靠的服务。这些工具和服务是构建高效、功能丰富的应用程序的关键组成部分。

信息流建模

在完成了传统的领域建模和领域服务实现后,我们接下来将进入AI应用开发的阶段。这部分开发的核心是对信息流进行建模,确立信息流的结构和编排方式。

具体来说,该APP的开发包含两个核心步骤,目的是输出一个精心规划的一日行程安排,这是我们期望的最终信息输出结果。为了实现这一输出,一个关键的前置步骤是收集关于乐园活动时间和其他相关信息的数据,这些都是直接影响行程规划的实际信息。此外,还有一个可选的步骤,即在前置阶段制定一个初步的解决方案或计划。这个计划可以指导我们在后续阶段中逐步收集必要的实际信息并进行行程规划。选择是否采用这一步骤将取决于我们解决问题的具体方法和风格。

通过这样的信息流建模和步骤安排,我们能够确保AI应用在处理和输出最终结果时的效率和准确性,同时也提供了灵活性,允许根据具体情况调整解决方案的策略。

基于前述的应用开发需求,我们需要明确模型在整个系统中扮演的角色。模型的主要责任可以分为三个部分:

  1. 行程规划:模型需要制定详细的行程安排,这是最终的输出目标。
  2. 函数调用:模型负责决定调用哪些工具函数,以及调用的方式和顺序。
  3. 解决方案制定:如果选择了先制定计划再执行的策略,模型还需要提出一个初步的解决问题的计划。

鉴于这些任务的特性和要求,我们需要精心选择能够有效承担这些功能的模型。在此,我们选用了Deepseek R1来进行推理和规划任务,而选择豆包的 1.5 Pro 32K来处理函数调用任务,因为 Deepseek R1目前不支持函数调用功能。这一选择表明,我们的信息流结构需要至少两个模型的协作来解决问题,形成一个多智能体系统。

通过这样的模型配置,我们确保了各个任务能够由最适合的技术来执行,从而提高整个应用的效率和准确性。这种多智能体的合作方式也为我们的系统提供了更大的灵活性和扩展性。

“计划——执行”多智能体范式

因此,我们倾向于采用前置规划步骤,构建一个以计划和执行为基础的多智能体结构。整个流程如下所示:

  1. 任务输入:接收初始的任务需求。
  2. 计划智能体处理:该智能体负责输出一个详细的任务列表,明确各项任务的执行策略和步骤。
  3. 执行智能体处理:此智能体根据计划智能体提供的任务列表,通过函数调用获取所需的信息。
  4. 重新计划智能体检查:该智能体检查执行过程中是否存在任何问题或遗漏。如果需要更多的规划或调整,任务将被送回执行智能体,形成一个闭环处理过程。
  5. 最终输出:一旦所有任务都按照计划顺利完成,最终结果将被输出。

这种多智能体结构不仅保证了任务执行的系统性和条理性,还允许在执行过程中进行动态调整和优化,从而提升了处理复杂情况的灵活性和效率。借助于多智能体的协作,我们可以有效地管理和执行复杂的任务流程。

选择采用这种计划执行的多智能体范式的主要原因包括:

  1. 专业化模型的应用:不同模型根据各自的专长进行特定任务,如让Deepseek R1负责推理和规划,而豆包大模型负责函数调用,这样可以最大化每个模型的效能。
  2. 单一职责原则:这种结构符合智能体层面或模型层面的单一职责原则,每个智能体的职责都是明确并且解耦的,这有助于进行更精准的调优和评测。
  3. 模拟人类解决问题的方式:这种方式符合人类解决问题的通用模式——首先制定计划,然后执行计划,并在必要时对计划进行调整和优化。

通过这种结构,整个过程的每个步骤都得到了明确的定义和优化,确保了任务处理的高效性和可靠性。这也使得整个系统能够更好地适应不断变化的需求和情况,从而提高整体的执行效率和成功率。

释放编排的力量

现在让我们回到Eino框架,探讨如何利用此框架实现前面描述的信息流程。在Eino框架中,图中的每个方框节点代表一个框架组件。具体来说,Planner、Executor和 Reviser这三个核心组件都实现了ChatModel接口。尽管这些组件使用的是不同的模型,它们都遵循同一套接口标准,确保了组件间的一致性和互操作性。

除了这些核心组件,框架中还包括其他一些辅助组件,例如“ToList”,这里我们不作详细介绍。在框架的右上方,有一个名为“Tools_Node”的组件,也被称为工具执行器。这是Eino框架中的一个特定类型的组件,专门用于处理模型输出的函数调用消息。工具执行器能够解析这些消息,并执行相应的本地或远程函数。完成函数调用后,它会将结果以消息形式返回给大模型。

这种设计使得Eino框架能够灵活应对各种任务,有效地将复杂的任务分解并通过各个专门的组件协同工作,从而实现高效的信息处理和任务执行。

再来详细讨论三个“ToList”节点。这些节点实际上是lambda节点,即在Eino框架中可以定义的本地自定义函数。通过lambda节点,我们可以将业务所需的任何类型的输入输出函数集成到信息流程中。在Eino框架中,模型的输出通常是单个消息,而模型的输入往往需要一个消息列表以便提供充分的上下文信息。因此,当需要将两个模型的输出与输入相连接时,使用“ToList”节点可以将单一输出消息转换为适用于输入的消息列表,确保数据类型之间的匹配。

这一点引出了Eino框架的一个重要特性:强类型系统。Eino中每个节点之间的数据类型都是明确定义的,输入与输出之间需要严格对应。这种设计在图的编译阶段允许及时发现任何类型不匹配的问题,相对于如Python这样的动态类型语言,Eino的这种强类型特性使得框架更加健壮和可靠。具体的图代码在此不详细展示,但它包含约两页纸,近100行代码。

从函数到工具

下面我们将探讨如何将之前编写的领域函数和服务(例如查询乐园信息列表的服务函数)封装为工具。在 Eino 框架中,我们可以利用一些快捷方式,将本地函数的输入和输出结构体直接转换为模型所需的工具信息。此外,我们也可以选择手动编写转换代码,或者使用 OpenAPI 的schema来进行转换。

在完成整个信息流图的编排之后,实际上我们所做的是对组件类型的接口进行了抽象的编排。而在实际运行时,需要将具体的组件实现填充到相应的节点中,这一步骤是必需的,以确保系统能够正常运行。在这个过程中,我们实例化了两个模型:一个是 Deepseek 模型,另一个是豆包模型。这些模型使用的是Eino EXT中提供的具体组件实现。

通过这样的方法,我们不仅确保了系统的灵活性和可扩展性,还能够有效地将现有的服务和功能集成到一个统一的、高效的Eino框架中。这种框架的结构化设计也便于后续的维护和升级。

完整示例代码仓库:github.com/cloudwego/e…

实际运行结果

现在我们来观察一下实际运行的效果。首先,第一个智能体 Planner 会进行流式输出,这不仅包括推理过程,还包括实际的答案。这一输出过程利用了 Eino 的回调机制。我们可以监听 ChatModel 节点的流式事件输出,并将捕获的信息输出到控制台。

接下来,第二个智能体 Executor 根据 Planner 提供的计划步骤进行函数调用,并逐步处理这些步骤。最终,Reviser 负责输出最终的答案。

通过这种方式,每个智能体都在其专责的领域内进行操作,确保了整个流程的高效和准确。这不仅展示了 Eino 框架的动态调用和事件处理能力,也体现了其在实际应用中的强大功能和灵活性。

Eino:现状

目前,该项目于 1 月份开源,截止发稿已经在 GitHub 上获得了 3.5 k 的 star。它有三个核心仓库,即前面提到的核心库 eino、扩展库 eino-ext 和示例库 eino-examples。目前,字节内部有较多业务线在使用该框架,约有 60 多个业务线或多或少会用到它。对于外部企业用户,目前虽未特意进行商业化推广,但已知至少有 8 家外部企业在使用该框架。下方是 Eino 用户群飞书二维码,欢迎扫码加入,共同交流 AI 应用落地的经验。

相关链接: