Eino中的两种应用模式:“单独使用”和“在编排中使用”

"单独使用" vs "在编排中使用"

这是一个关于软件架构和组件化思想的核心问题。简单来说,"单独使用"和"在编排中使用"描述了同一个组件(比如 ChatModel)在不同复杂度场景下的两种应用模式。


单独使用 (Standalone Use)

"单独使用"指的是直接调用一个组件来完成一个单一、明确的任务 。这通常是点对点的交互

示例:

项目中的 main.go 文件就是典型的"单独使用"。它的逻辑非常直接:

  1. 初始化 ChatModel
  2. 准备一组消息。
  3. 调用 model.Generate()model.Stream() 来获取一个回复。
  4. 打印回复。

整个过程的目标就是完成一次对话。ChatModel 是这个程序的核心和唯一的功能模块。

特点:

  • 目标单一:就是为了和模型进行一次对话。
  • 逻辑简单:没有复杂的流程控制,一条直线走到底。
  • 自包含:程序的功能几乎完全由这一个组件提供。
go 复制代码
package main

import (
	"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 Key
		Model:   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 调用完成。编排流程可能是这样的:

  1. 接收用户问题:(例如:用户输入 "Eino 的 ChatModel 和 LangChain 的 ChatModel 有什么区别?")
  2. 调用【文档检索器】:编排器首先调用一个"文档检索"组件(Retriever),在你的知识库(比如一堆 Markdown 文档或一个向量数据库)中搜索与问题最相关的内容片段。
  3. 构建提示词 (Prompt) :编排器将用户原始问题和上一步检索到的内容片段,组合成一个新的、更详细的提示词。
    • 例如:请根据以下背景信息:<检索到的内容>... 来回答这个问题:Eino 的 ChatModel 和 LangChain 的 ChatModel 有什么区别?
  4. 调用【ChatModel】 :编排器现在才调用 ChatModel,并将这个精心构建的提示词发给它。ChatModel 在这里的作用是基于给定的上下文进行"总结和推理",而不是凭空回答。
  5. 返回最终答案 :将 ChatModel 生成的答案返回给用户。

在这个流程中,ChatModel 只是"棋盘"上的一颗"棋子",它被编排器在合适的时机调用,与其他组件(如文档检索器)协同工作,最终完成一个远超其自身能力的复杂任务。

特点:

  • 目标复杂:需要多个步骤和多个不同能力的组件协作。
  • 流程驱动:有一个明确的、可能带有条件分支和循环的执行流程。
  • 模块化ChatModel 只是其中一个模块,可以被替换或升级,而不影响整个流程的其他部分。
go 复制代码
package main

import (
	"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 *Retriever
	model     *ark.ChatModel
}

// NewOrchestrator 创建并初始化编排器及其所有依赖的组件。
func NewOrchestrator(ctx context.Context) (*Orchestrator, error) {
	// 初始化检索器
	retriever := NewRetriever()

	// 初始化 ChatModel
	timeout := 30 * time.Second
	model, 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 应用。