Eino中的两种应用模式:“单独使用”和“在编排中使用”
“单独使用” vs “在编排中使用”
这是一个关于软件架构和组件化思想的核心问题。简单来说,“单独使用”和“在编排中使用”描述了同一个组件(比如 ChatModel
)在不同复杂度场景下的两种应用模式。
单独使用 (Standalone Use)
“单独使用”指的是直接调用一个组件来完成一个单一、明确的任务。这通常是点对点的交互。
示例:
项目中的 main.go
文件就是典型的“单独使用”。它的逻辑非常直接:
- 初始化
ChatModel
。 - 准备一组消息。
- 调用
model.Generate()
或model.Stream()
来获取一个回复。 - 打印回复。
整个过程的目标就是完成一次对话。ChatModel
是这个程序的核心和唯一的功能模块。
特点:
- 目标单一:就是为了和模型进行一次对话。
- 逻辑简单:没有复杂的流程控制,一条直线走到底。
- 自包含:程序的功能几乎完全由这一个组件提供。
package mainimport ("context""fmt""time""github.com/cloudwego/eino-ext/components/model/ark""github.com/cloudwego/eino/schema""github.com/spf13/viper"
)// runStandaloneExample 展示了如何“单独使用”ChatModel。
// 它的功能是直接与大模型进行一次完整的对话交互。
func runStandaloneExample() {// --- 这部分代码在编排示例中会重复,因此可以考虑重构 ---viper.SetConfigName("config")viper.SetConfigType("yaml")viper.AddConfigPath(".") // 路径已更正为当前目录err := viper.ReadInConfig()if err != nil {panic(fmt.Errorf("fatal error config file: %w", err))}// ----------------------------------------------------// 创建一个上下文,用于控制请求的生命周期ctx := context.Background()// 设置请求超时时间timeout := 30 * time.Second// --- 初始化 ChatModel ---// 使用 ark.NewChatModel 创建一个模型实例。// 配置信息(如 API Key 和模型名称)从 viper 加载的 config.yaml 文件中读取。model, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{APIKey: viper.GetString("ARK_API_KEY"), // 从配置中获取 API KeyModel: viper.GetString("ARK_MODEL"), // 从配置中获取模型名称Timeout: &timeout, // 设置超时})if err != nil {panic(fmt.Errorf("初始化 ChatModel 失败: %w", err))}// --- 准备对话消息 ---// 消息列表是一个对话历史,可以包含多种角色。messages := []*schema.Message{schema.SystemMessage("你是一个助手"), // 系统消息,用于设定模型的角色和行为schema.UserMessage("你好"), // 用户消息,代表用户的输入}// --- 方式一: 标准生成 (Generate) ---// 一次性获取完整的模型回复。println("--- 标准生成 (Standalone) ---")response, err := model.Generate(ctx, messages)if err != nil {panic(fmt.Errorf("标准生成失败: %w", err))}// 打印模型生成的完整内容println(response.Content)// 打印本次调用的 Token 使用情况if usage := response.ResponseMeta.Usage; usage != nil {println("提示 Tokens:", usage.PromptTokens)println("生成 Tokens:", usage.CompletionTokens)println("总 Tokens:", usage.TotalTokens)}// --- 方式二: 流式生成 (Stream) ---// 逐块接收模型返回的内容,适用于打字机效果或长文本生成。println("\n--- 流式生成 (Standalone) ---")stream, err := model.Stream(ctx, messages)if err != nil {panic(fmt.Errorf("流式生成失败: %w", err))}// 确保在函数结束时关闭流defer stream.Close()// 循环接收数据块,直到流结束for {chunk, err := stream.Recv()if err != nil {// 当流结束时,stream.Recv() 会返回 io.EOF 错误,这是正常结束的标志。break}// 实时打印每个数据块的内容print(chunk.Content)}println() // 确保在流式输出后换行
}
在编排中使用 (Use in Orchestration)
“在编排中使用”指的是将一个组件作为构建块(Building Block),与其他多个组件或服务组合起来,共同完成一个更复杂、多步骤的任务。这时,会有一个“编排器”(Orchestrator)来负责协调和调度这些组件。
示例:构建一个能回答专业文档问题的问答机器人。
这个任务无法通过一次 ChatModel
调用完成。编排流程可能是这样的:
- 接收用户问题:(例如:用户输入 “Eino 的 ChatModel 和 LangChain 的 ChatModel 有什么区别?”)
- 调用【文档检索器】:编排器首先调用一个“文档检索”组件(Retriever),在你的知识库(比如一堆 Markdown 文档或一个向量数据库)中搜索与问题最相关的内容片段。
- 构建提示词 (Prompt):编排器将用户原始问题和上一步检索到的内容片段,组合成一个新的、更详细的提示词。
- 例如:
请根据以下背景信息:<检索到的内容>... 来回答这个问题:Eino 的 ChatModel 和 LangChain 的 ChatModel 有什么区别?
- 例如:
- 调用【ChatModel】:编排器现在才调用
ChatModel
,并将这个精心构建的提示词发给它。ChatModel
在这里的作用是基于给定的上下文进行“总结和推理”,而不是凭空回答。 - 返回最终答案:将
ChatModel
生成的答案返回给用户。
在这个流程中,ChatModel
只是“棋盘”上的一颗“棋子”,它被编排器在合适的时机调用,与其他组件(如文档检索器)协同工作,最终完成一个远超其自身能力的复杂任务。
特点:
- 目标复杂:需要多个步骤和多个不同能力的组件协作。
- 流程驱动:有一个明确的、可能带有条件分支和循环的执行流程。
- 模块化:
ChatModel
只是其中一个模块,可以被替换或升级,而不影响整个流程的其他部分。
package mainimport ("context""fmt""strings""time""github.com/cloudwego/eino-ext/components/model/ark""github.com/cloudwego/eino/schema""github.com/spf13/viper"
)// =============================================================================
//
// 本文件演示了如何“在编排中使用”ChatModel。
// 我们构建一个简单的“检索增强生成”(RAG)流程,它由多个组件协作完成。
//
// =============================================================================// -----------------------------------------------------------------------------
// 组件 1: 文档检索器 (Retriever)
// -----------------------------------------------------------------------------
// Retriever 的作用是从知识库中查找与用户问题相关的信息。
// 在真实场景中,这可能是一个连接到向量数据库(如 Milvus, Pinecone)的复杂组件。
// 这里我们用一个简单的 map 来模拟一个小型知识库。
type Retriever struct {knowledgeBase map[string]string
}// NewRetriever 创建并初始化一个检索器实例。
func NewRetriever() *Retriever {return &Retriever{knowledgeBase: map[string]string{"eino": "Eino 是一个云原生的大模型应用开发框架,旨在简化和加速大模型应用的构建。","ark": "火山方舟(Ark)是字节跳动推出的一个模型即服务(MaaS)平台,提供了多种先进的AI模型。",},}
}// Retrieve 根据查询从知识库中查找相关文档。
// 它通过简单的关键字匹配来模拟检索过程。
func (r *Retriever) Retrieve(ctx context.Context, query string) (string, error) {for keyword, doc := range r.knowledgeBase {// 如果查询中包含知识库中的关键字,则返回对应的文档if strings.Contains(strings.ToLower(query), keyword) {return doc, nil}}return "", fmt.Errorf("在知识库中未找到与 '%s' 相关的信息", query)
}// -----------------------------------------------------------------------------
// 组件 2: ChatModel (我们已经熟悉)
// -----------------------------------------------------------------------------
// ChatModel 在这个编排流程中扮演“大脑”的角色,负责根据提供的信息进行推理和生成文本。
// 为了保持编排器的整洁,我们将模型初始化放在编排器的构造函数中。// -----------------------------------------------------------------------------
// 组件 3: 编排器 (Orchestrator)
// -----------------------------------------------------------------------------
// Orchestrator 是整个流程的核心,负责协调和调用其他组件。
type Orchestrator struct {retriever *Retrievermodel *ark.ChatModel
}// NewOrchestrator 创建并初始化编排器及其所有依赖的组件。
func NewOrchestrator(ctx context.Context) (*Orchestrator, error) {// 初始化检索器retriever := NewRetriever()// 初始化 ChatModeltimeout := 30 * time.Secondmodel, err := ark.NewChatModel(ctx, &ark.ChatModelConfig{APIKey: viper.GetString("ARK_API_KEY"),Model: viper.GetString("ARK_MODEL"),Timeout: &timeout,})if err != nil {return nil, fmt.Errorf("初始化 ChatModel 失败: %w", err)}// 返回一个包含所有已初始化组件的编排器实例return &Orchestrator{retriever: retriever,model: model,}, nil
}// Run 方法执行 RAG (Retrieval-Augmented Generation) 流程。
// 这是编排器定义的核心业务逻辑。
func (o *Orchestrator) Run(ctx context.Context, userQuery string) (string, error) {fmt.Printf("编排流程开始,用户问题: \"%s\"\n", userQuery)// 步骤 1: 调用【文档检索器】获取相关上下文fmt.Println("步骤 1: 调用【文档检索器】...")contextDoc, err := o.retriever.Retrieve(ctx, userQuery)if err != nil {// 如果在知识库中找不到相关信息,可以选择直接让模型回答,或返回错误。// 这里我们选择让模型在没有额外上下文的情况下尝试回答。fmt.Printf("检索失败: %v。将直接由模型回答。\n", err)contextDoc = "无相关背景知识" // 提供一个明确的“无信息”信号}fmt.Printf("检索到的上下文: \"%s\"\n", contextDoc)// 步骤 2: 动态构建包含上下文的提示词 (Prompt)// 这是 RAG 的核心思想:将检索到的知识注入到提示词中,为模型提供回答问题的依据。fmt.Println("步骤 2: 构建提示词...")prompt := fmt.Sprintf("请根据以下背景知识回答问题。\n\n背景知识:%s\n\n问题:%s", contextDoc, userQuery)fmt.Printf("构建的提示词: \"%s\"\n", prompt)// 步骤 3: 调用【ChatModel】进行推理和生成// 模型将基于我们提供的、包含上下文的提示词来生成答案。fmt.Println("步骤 3: 调用【ChatModel】...")messages := []*schema.Message{schema.SystemMessage("你是一个智能问答助手,请严格基于提供的背景知识来回答问题。如果背景知识没有提供相关信息,请直接说不知道。"),schema.UserMessage(prompt),}response, err := o.model.Generate(ctx, messages)if err != nil {return "", fmt.Errorf("ChatModel 生成失败: %w", err)}fmt.Println("编排流程结束。")return response.Content, nil
}// main 函数是程序的入口,它现在负责驱动编排器。
func main() {// --- 统一的配置加载 ---viper.SetConfigName("config")viper.SetConfigType("yaml")viper.AddConfigPath(".") // 确保在项目根目录运行err := viper.ReadInConfig()if err != nil {panic(fmt.Errorf("fatal error config file: %w", err))}ctx := context.Background()// --- 初始化并运行编排器 ---fmt.Println("--- 正在初始化编排器... ---")orchestrator, err := NewOrchestrator(ctx)if err != nil {panic(err)}fmt.Println("--- 编排器初始化完成 ---")// 定义一个用户问题来驱动 RAG 流程userQuery := "请问 Eino 是什么?它和 Ark 有什么关系吗?"finalAnswer, err := orchestrator.Run(ctx, userQuery)if err != nil {panic(err)}// 打印由整个编排流程生成的最终答案fmt.Println("\n--- 最终答案 ---")fmt.Println(finalAnswer)// (可选)可以调用之前的独立使用示例// fmt.Println("\n\n--- 现在运行独立使用示例 ---")// runStandaloneExample()
}
总结
特性 | 单独使用 | 在编排中使用 |
---|---|---|
角色 | 核心功能,主角 | 构建块,流程中的一个环节 |
场景 | 简单的聊天机器人、文本生成工具 | 复杂的 RAG(检索增强生成)、Agent、多工具协作系统 |
交互 | 用户 -> ChatModel | 用户 -> 编排器 -> (组件A, ChatModel, 组件B…) |
价值 | 提供基础的 AI 对话能力 | 作为 AI 大脑,驱动复杂业务逻辑的实现 |
Eino 框架的设计正是为了同时支持这两种模式。您可以像现在这样“单独使用”它,也可以轻松地将 ChatModel
集成到一个更宏大的“编排”流程中,去构建更强大的 AI 应用。