核心摘要

Eino 框架的核心设计思想是组件即接口——每个能力单元(ChatModel、Tool、Retriever 等)都由一个 Go interface 定义清晰的输入、输出和方法签名,底层实现可自由替换。本文将逐一拆解 9 大核心组件的设计意图与实战用法,并通过一个完整的带搜索工具的问答 Bot 示例,串联 ChatModel → Tool → Retriever 的协作全流程。

本文是 Eino 框架全景:为什么用 Go 构建 AI 应用 的系列第二篇。建议先阅读上篇了解 Eino 整体架构。


目录

  1. 核心要点
  2. 组件体系设计哲学
  3. ChatModel 详解
  4. Tool 系统
  5. Retriever 与向量检索
  6. 文档处理链路
  7. Embedding 与 ChatTemplate
  8. Lambda 自定义节点
  9. 实战:构建带搜索工具的问答 Bot
  10. 最佳实践
  11. 常见问题
  12. 总结
  13. 相关资源

核心要点

  • 接口即契约:每个组件通过 Go interface 定义能力边界,实现可自由替换
  • ChatModel 三件套Generate(同步)、Stream(流式)、BindTools(工具注册)覆盖 LLM 交互全场景
  • Tool = ToolInfo + 执行逻辑:通过 JSON Schema 描述参数,让模型理解何时调用
  • Retriever 统一抽象:ElasticSearch / VikingDB 实现可互换,上层代码零修改
  • Document Pipeline:Loader → Transformer → Indexer 三级流水线覆盖知识入库全流程
  • Lambda 粘合剂:任意 Go 函数均可包装为编排节点

组件体系设计哲学

Eino 的组件设计遵循三层原则:

graph TB A["接口定义层 (Interface)"] --> B["实现层 (Implementation)"] B --> C["可替换层 (Replaceable)"] A --> D["ChatModel Interface"] A --> E["Tool Interface"] A --> F["Retriever Interface"] D --> G["OpenAI"] D --> H["Claude"] D --> I["Ollama"] E --> J["Google Search"] E --> K["Custom Tool"] F --> L["ElasticSearch"] F --> M["VikingDB"]

设计原则

原则 说明 实际效果
接口优先 每个组件先定义 interface,再提供实现 编译时类型安全
输入输出明确 严格定义参数类型和返回值 减少运行时错误
Option 模式 通过变长参数 ...Option 支持运行时配置 灵活但不臃肿
零耦合实现 接口与实现在不同 package 按需引入依赖

这种设计使得在 AI Agent 开发中,切换模型提供商或检索引擎只需更换初始化代码,业务逻辑完全不变。

完整组件全景表

组件 职责 可用实现
ChatModel 与 LLM 交互:输入 Message[],输出 Message OpenAI, Claude, Gemini, Ark, Ollama
Tool 基于模型输出执行动作 Google Search, DuckDuckGo, 自定义
Retriever 获取上下文用于 Grounding ElasticSearch, Volc VikingDB
ChatTemplate 将外部输入转换为 Prompt Messages DefaultChatTemplate
Document Loader 从数据源加载文本 WebURL, Amazon S3, File
Document Transformer 文本转换/分割 HTMLSplitter, ScoreReranker
Indexer 文档存储与索引 ElasticSearch, Volc VikingDB
Embedding 文本 → 向量 OpenAI, Ark
Lambda 自定义函数节点 任意 Go 函数

ChatModel 详解

ChatModel 是 Eino 中最核心的组件,负责与大语言模型的所有交互。

接口定义

go
type ChatModel interface {
    Generate(ctx context.Context, input []*schema.Message, opts ...Option) (*schema.Message, error)
    Stream(ctx context.Context, input []*schema.Message, opts ...Option) (*schema.StreamReader[*schema.Message], error)
    BindTools(tools []*schema.ToolInfo) error
}

三个方法各司其职:

方法 用途 典型场景
Generate 同步生成完整响应 一次性问答、批处理
Stream 流式返回 Token 实时聊天界面、低首字延迟
BindTools 注册可用工具 Function Calling、Agent 工具调度

多模型提供商支持

go
// OpenAI
model, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
    Model:  "gpt-4o",
    APIKey: os.Getenv("OPENAI_API_KEY"),
})

// Ollama 本地模型
model, _ := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{
    Model:   "llama3:70b",
    BaseURL: "http://localhost:11434",
})

// Ark (字节火山引擎)
model, _ := ark.NewChatModel(ctx, &ark.ChatModelConfig{
    Model:  "ep-xxx-endpoint",
    APIKey: os.Getenv("ARK_API_KEY"),
})

Generate vs Stream 使用示例

go
// 同步调用 - 适用于后台处理
message, err := model.Generate(ctx, []*schema.Message{
    schema.SystemMessage("you are a helpful assistant."),
    schema.UserMessage("what does the future AI App look like?"),
})
if err != nil {
    log.Fatal(err)
}
fmt.Println(message.Content)

// 流式调用 - 适用于实时 UI
reader, err := model.Stream(ctx, messages)
if err != nil {
    log.Fatal(err)
}
defer reader.Close()

for {
    chunk, err := reader.Recv()
    if err == io.EOF {
        break
    }
    fmt.Print(chunk.Content) // 逐 token 输出
}

开发时如果需要快速验证 LLM 返回的 JSON 格式,可以使用 JSON 格式化工具 进行可视化检查。


Tool 系统

Tool 是 Eino 中连接模型「思考」与「行动」的桥梁。当模型判断需要外部信息或执行操作时,通过 Function Calling 机制调用 Tool。

ToolInfo 定义

每个 Tool 通过 ToolInfo 结构体向模型描述自己的能力:

go
type ToolInfo struct {
    Name        string          // 工具名称
    Description string          // 功能描述(模型据此判断何时调用)
    Parameters  *schema.Schema  // JSON Schema 格式的参数定义
}

自定义 Tool 实现

go
// 定义搜索工具
searchTool := &schema.ToolInfo{
    Name:        "web_search",
    Description: "Search the web for current information about a topic",
    Parameters: &schema.Schema{
        Type: "object",
        Properties: map[string]*schema.Schema{
            "query": {
                Type:        "string",
                Description: "The search query",
            },
            "max_results": {
                Type:        "integer",
                Description: "Maximum number of results to return",
            },
        },
        Required: []string{"query"},
    },
}

// 注册到模型
err := model.BindTools([]*schema.ToolInfo{searchTool})

参数定义使用 JSON Schema 格式——如果你对该格式不熟悉,可以先借助 JSON Schema 验证工具 检验格式正确性。

Tool 与 Function Calling 的关系

sequenceDiagram participant User as 用户 participant Model as ChatModel participant Tool as Tool Executor User->>Model: 发送消息 Model->>Model: 判断是否需要工具 Model-->>Tool: 返回 tool_call (name + args) Tool->>Tool: 执行工具逻辑 Tool-->>Model: 返回工具结果 Model->>User: 生成最终回答

Eino 的 Tool 系统与模型的 Function Calling 能力协同工作:

  1. 开发者通过 BindTools 注册工具列表
  2. 模型根据对话上下文决定是否调用工具
  3. 框架解析模型返回的 tool_call,执行对应 Tool
  4. 将执行结果作为 ToolMessage 回传模型
  5. 模型基于工具结果生成最终回答

Retriever 与向量检索

Retriever 组件为 RAG(检索增强生成)应用提供标准化的文档检索能力。

接口抽象

go
type Retriever interface {
    Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error)
}

简洁的接口背后隐藏了复杂的向量检索逻辑:Query → Embedding → ANN Search → Document Ranking。

ElasticSearch 实现

go
retriever, _ := elasticsearch.NewRetriever(ctx, &elasticsearch.RetrieverConfig{
    Addresses: []string{"http://localhost:9200"},
    Index:     "knowledge_base",
    TopK:      5,
    // 支持混合检索:向量 + 关键词
    SearchMode: elasticsearch.HybridSearch,
})

docs, err := retriever.Retrieve(ctx, "Eino 框架的核心组件有哪些?")
for _, doc := range docs {
    fmt.Printf("Score: %.3f | Content: %s\n", doc.Score, doc.Content[:100])
}

VikingDB 实现

go
retriever, _ := vikingdb.NewRetriever(ctx, &vikingdb.RetrieverConfig{
    Collection: "my_knowledge_base",
    TopK:       5,
    Region:     "cn-beijing",
})

// 接口完全一致,上层代码无需修改
docs, err := retriever.Retrieve(ctx, "向量数据库的选型建议")

实现对比

特性 ElasticSearch Volc VikingDB
部署方式 自建 / 托管 火山引擎云服务
混合检索 ✅ BM25 + Vector ✅ 原生支持
适用规模 百万级 亿级
运维复杂度 低(全托管)
成本模型 资源型 按量计费

文档处理链路

在向量检索之前,原始文档需要经过标准化处理流水线:

graph LR A["Document Loader"] --> B["Document Transformer"] B --> C["Embedding"] C --> D["Indexer"] A1["WebURL / S3 / File"] --> A B1["HTMLSplitter / Reranker"] --> B C1["OpenAI / Ark Embedding"] --> C D1["ES / VikingDB"] --> D

Document Loader — 数据加载

go
// 从 Web URL 加载
loader, _ := weburl.NewLoader(&weburl.Config{
    URL:     "https://example.com/docs",
    Timeout: 30 * time.Second,
})
docs, _ := loader.Load(ctx)

// 从本地文件加载
loader, _ := file.NewLoader(&file.Config{
    Path: "/data/knowledge/*.md",
})
docs, _ := loader.Load(ctx)

Document Transformer — 文本处理

go
// HTML 分割器:按语义切片
splitter, _ := htmlsplitter.NewTransformer(&htmlsplitter.Config{
    ChunkSize:    512,
    ChunkOverlap: 64,
})
chunks, _ := splitter.Transform(ctx, docs)

// Score Reranker:按相关性重排
reranker, _ := scorereranker.NewTransformer(&scorereranker.Config{
    Model: "bge-reranker-v2",
    TopN:  3,
})
ranked, _ := reranker.Transform(ctx, chunks)

Indexer — 存储索引

go
indexer, _ := elasticsearch.NewIndexer(ctx, &elasticsearch.IndexerConfig{
    Addresses: []string{"http://localhost:9200"},
    Index:     "knowledge_base",
})

// 批量索引文档
err := indexer.Store(ctx, chunks)

Embedding 与 ChatTemplate

Embedding — 文本向量化

Embedding 组件将文本转化为高维向量表示,是向量检索的基础:

go
embedder, _ := openai.NewEmbedding(ctx, &openai.EmbeddingConfig{
    Model: "text-embedding-3-small",
})

vectors, err := embedder.EmbedStrings(ctx, []string{
    "Eino 是一个 Go 语言 AI 框架",
    "ChatModel 接口支持多种模型",
})
// vectors[0] = []float64{0.012, -0.034, ...} (1536维)

ChatTemplate — 提示词模板

ChatTemplate 将外部输入(用户问题、检索结果等)组装成标准的 Message 列表:

go
template := chattemplate.New(&chattemplate.Config{
    Templates: []*schema.Message{
        schema.SystemMessage("你是一个专业的技术助手。\n\n参考资料:\n{{.context}}"),
        schema.UserMessage("{{.question}}"),
    },
})

messages, _ := template.Format(ctx, map[string]interface{}{
    "context":  retrievedDocs,
    "question": "Eino 的组件设计原则是什么?",
})
// 输出标准 []*schema.Message,直接传给 ChatModel

Lambda 自定义节点

Lambda 是 Eino 编排系统的「瑞士军刀」,将任意 Go 函数包装为可编排的节点:

go
// 数据格式化 Lambda
formatNode := lambda.New(func(ctx context.Context, docs []*schema.Document) (string, error) {
    var sb strings.Builder
    for i, doc := range docs {
        sb.WriteString(fmt.Sprintf("[%d] %s\n", i+1, doc.Content))
    }
    return sb.String(), nil
})

// 过滤 Lambda
filterNode := lambda.New(func(ctx context.Context, msg *schema.Message) (*schema.Message, error) {
    if len(msg.Content) > 10000 {
        msg.Content = msg.Content[:10000] + "...(truncated)"
    }
    return msg, nil
})

Lambda 在编排图中可以串联在任何两个节点之间,用于数据转换、验证、日志记录等轻量操作。


实战:构建带搜索工具的问答 Bot

以下是一个完整的示例,整合 ChatModel、Tool 和 Retriever 构建一个具备网络搜索能力的问答 Bot:

go
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/cloudwego/eino/components/model/openai"
    "github.com/cloudwego/eino/schema"
)

// 定义搜索工具的执行逻辑
func executeSearch(query string) string {
    // 实际项目中对接搜索 API
    return fmt.Sprintf("搜索结果:关于'%s'的最新信息...", query)
}

func main() {
    ctx := context.Background()

    // 1. 初始化 ChatModel
    model, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        Model:  "gpt-4o",
        APIKey: os.Getenv("OPENAI_API_KEY"),
    })
    if err != nil {
        log.Fatal(err)
    }

    // 2. 定义并注册 Tool
    searchTool := &schema.ToolInfo{
        Name:        "web_search",
        Description: "Search the internet for current information",
        Parameters: &schema.Schema{
            Type: "object",
            Properties: map[string]*schema.Schema{
                "query": {
                    Type:        "string",
                    Description: "Search query keywords",
                },
            },
            Required: []string{"query"},
        },
    }

    if err := model.BindTools([]*schema.ToolInfo{searchTool}); err != nil {
        log.Fatal(err)
    }

    // 3. 第一轮对话:模型决定是否调用工具
    messages := []*schema.Message{
        schema.SystemMessage("你是一个有用的助手,可以通过搜索获取最新信息。"),
        schema.UserMessage("Eino 框架最新版本是什么?"),
    }

    response, err := model.Generate(ctx, messages)
    if err != nil {
        log.Fatal(err)
    }

    // 4. 检查模型是否请求调用工具
    if len(response.ToolCalls) > 0 {
        for _, call := range response.ToolCalls {
            fmt.Printf("模型请求调用工具: %s, 参数: %s\n", call.Function.Name, call.Function.Arguments)

            // 5. 执行工具
            result := executeSearch(call.Function.Arguments)

            // 6. 将工具结果回传
            messages = append(messages, response) // 助手消息(含 tool_call)
            messages = append(messages, schema.ToolMessage(result, call.ID))
        }

        // 7. 模型基于工具结果生成最终回答
        finalResponse, err := model.Generate(ctx, messages)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println("最终回答:", finalResponse.Content)
    } else {
        fmt.Println("直接回答:", response.Content)
    }
}

如果你在调试过程中需要将 Go struct 转换为 JSON 格式查看,推荐使用 JSON to Go 工具 进行双向转换。


最佳实践

组件选型原则

场景 推荐方案
快速原型 使用 Ollama 本地模型 + 文件 Loader
生产 RAG Ark/OpenAI Embedding + VikingDB Retriever
Agent 工具链 ChatModel.BindTools + 自定义 Tool 组合
大文档处理 WebURL Loader → HTMLSplitter → 批量 Indexer

错误处理模式

go
// 为 ChatModel 调用添加超时和重试
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

var response *schema.Message
for retries := 0; retries < 3; retries++ {
    response, err = model.Generate(ctx, messages)
    if err == nil {
        break
    }
    time.Sleep(time.Duration(retries+1) * time.Second)
}

性能优化要点

  1. 批量 Embedding:一次请求处理多段文本,减少 API 调用次数
  2. Stream 优先:面向用户的场景始终使用 Stream 降低首字延迟
  3. Retriever 预热:启动时执行一次空查询预热连接池
  4. Lambda 轻量化:Lambda 节点避免重 IO 操作,保持 < 10ms 执行时间

常见问题

Q: 如何实现模型热切换(不重启服务)?

A: 利用 Go 的 interface 特性,在上层维护一个 ChatModel 变量,通过配置中心动态替换实现:

go
var currentModel ChatModel // 接口变量

func switchModel(provider string) {
    switch provider {
    case "openai":
        currentModel, _ = openai.NewChatModel(ctx, openaiConfig)
    case "ollama":
        currentModel, _ = ollama.NewChatModel(ctx, ollamaConfig)
    }
}

Q: Tool 执行超时如何处理?

A: 通过 context 控制超时,并在 Tool 执行层添加 fallback:

go
toolCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

result, err := executeTool(toolCtx, toolCall)
if err != nil {
    result = "工具执行超时,请尝试其他方式回答"
}

Q: 多个 Retriever 如何融合结果?

A: 使用 Lambda 节点做结果聚合和去重:

go
mergeNode := lambda.New(func(ctx context.Context, results [][]*schema.Document) ([]*schema.Document, error) {
    seen := make(map[string]bool)
    var merged []*schema.Document
    for _, docs := range results {
        for _, doc := range docs {
            if !seen[doc.ID] {
                seen[doc.ID] = true
                merged = append(merged, doc)
            }
        }
    }
    return merged, nil
})

总结

Eino 的组件体系是一套经过字节跳动大规模生产验证的 Go AI 基础设施。其核心设计可归纳为:

  • ChatModel 统一了多模型对接复杂性,一个 interface 覆盖同步、流式和工具绑定
  • Tool 标准化了 Function Calling 工作流,让 Agent 获得「行动」能力
  • Retriever 抽象了向量检索实现差异,为 RAG 应用提供可插拔后端
  • Document Pipeline 覆盖了从数据加载到索引的完整知识入库流程
  • Lambda 作为粘合剂让任意逻辑融入编排图

下一篇文章将深入探讨如何使用 Eino 的编排引擎(Chain、Graph 与 Workflow) 将这些组件组装成复杂的 AI 应用。


相关资源