"单独使用" vs "在编排中使用"
这是一个关于软件架构和组件化思想的核心问题。简单来说,"单独使用"和"在编排中使用"描述了同一个组件(比如 ChatModel
)在不同复杂度场景下的两种应用模式。
单独使用 (Standalone Use)
"单独使用"指的是直接调用一个组件来完成一个单一、明确的任务 。这通常是点对点的交互。
示例:
项目中的 main.go
文件就是典型的"单独使用"。它的逻辑非常直接:
- 初始化
ChatModel
。 - 准备一组消息。
- 调用
model.Generate()
或model.Stream()
来获取一个回复。 - 打印回复。
整个过程的目标就是完成一次对话。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
调用完成。编排流程可能是这样的:
- 接收用户问题:(例如:用户输入 "Eino 的 ChatModel 和 LangChain 的 ChatModel 有什么区别?")
- 调用【文档检索器】:编排器首先调用一个"文档检索"组件(Retriever),在你的知识库(比如一堆 Markdown 文档或一个向量数据库)中搜索与问题最相关的内容片段。
- 构建提示词 (Prompt) :编排器将用户原始问题和上一步检索到的内容片段,组合成一个新的、更详细的提示词。
- 例如:
请根据以下背景信息:<检索到的内容>... 来回答这个问题:Eino 的 ChatModel 和 LangChain 的 ChatModel 有什么区别?
- 例如:
- 调用【ChatModel】 :编排器现在才调用
ChatModel
,并将这个精心构建的提示词发给它。ChatModel
在这里的作用是基于给定的上下文进行"总结和推理",而不是凭空回答。 - 返回最终答案 :将
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 应用。