核心摘要

Eino 框架的核心设计思想是组件即接口——通过 Go interface 表达能力边界和输入输出契约;只有行为兼容时,底层实现才可以替换。本文拆解 9 个组件方向,并以一个示意性的 ChatModel → Tool 集成骨架说明边界。

本文是 Eino 框架全景:为什么用 Go 构建 AI 应用 的系列第二篇。上篇可用于了解本文接口示例的背景。


目录

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

核心要点

  • 接口即契约:每个组件通过 Go interface 定义能力边界,实现可自由替换
  • ChatModel 边界GenerateStream 和工具绑定覆盖常见交互路径,但能力与 Option 仍取决于提供商和 revision
  • Tool = ToolInfo + 执行逻辑:通过 JSON Schema 描述参数,让模型理解何时调用
  • Retriever 抽象:不同后端可以复用常见检索调用,但过滤、评分、元数据和运维语义必须通过契约测试确认
  • Document Pipeline:Loader → Transformer → Indexer 三级流水线覆盖知识入库全流程
  • Lambda 粘合剂:具备明确 context、取消和错误契约的 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 的耦合,但提供商能力、错误、Schema、过滤器和运维策略仍需适配器与契约测试。

完整组件全景表

组件 职责 可用实现
ChatModel 与 LLM 交互:输入 Message[],输出 Message 以核对过的 Eino revision 中的提供商 package 为准
Tool 基于模型输出执行动作 Google Search, DuckDuckGo, 自定义
Retriever 获取上下文用于 Grounding 以核对过的 Eino revision 中的后端适配器为准
ChatTemplate 将外部输入转换为 Prompt Messages DefaultChatTemplate
Document Loader 从数据源加载文本 WebURL, Amazon S3, File
Document Transformer 文本转换/分割 HTMLSplitter, ScoreReranker
Indexer 文档存储与索引 ElasticSearch, Volc VikingDB
Embedding 文本 → 向量 OpenAI, Ark
Lambda 自定义函数节点 符合核对过的节点契约的函数

ChatModel 详解

ChatModel 是模型交互的组件边界。具体接口和 Option 类型会随 revision 变化,以下代码是示意,复制前应核对已锁定的 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:  "PROVIDER_MODEL@REVIEWED_REVISION",
    APIKey: os.Getenv("OPENAI_API_KEY"),
})

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

// Ark (字节火山引擎)
model, _ := ark.NewChatModel(ctx, &ark.ChatModelConfig{
    Model:  "ARK_ENDPOINT@REVIEWED_REVISION",
    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 输出
}

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 格式。执行器仍必须重新校验模型生成的参数;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)
}

该接口背后可能是向量、词法、混合或元数据检索;Embedding、ANN、排序、过滤和授权均取决于后端与应用。

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)
}

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 接口支持多种模型",
})
// 向量维度由所选模型决定,不应硬编码。

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 之间的信任边界,并不是完整的搜索集成:具体 revision 的提供商 API、参数解析、授权、配额和结果来源链路仍需实现。

go
package main

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

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

// 定义搜索工具的执行逻辑
func executeSearch(argumentsJSON string) (string, error) {
    // 解析并校验参数、执行授权,再调用真实搜索 API。
    // 模型生成的参数不能直接作为授权或 URL。
    return fmt.Sprintf("示意结果:%s", argumentsJSON), nil
}

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

    // 1. 初始化 ChatModel
    model, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        Model:  "PROVIDER_MODEL@REVIEWED_REVISION",
        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. 执行工具
            if call.Function.Name != searchTool.Name {
                log.Printf("拒绝未允许的工具:%s", call.Function.Name)
                continue
            }
            result, err := executeSearch(call.Function.Arguments)
            if err != nil {
                log.Printf("搜索失败:%v", err)
                continue
            }

            // 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)
    }
}

最佳实践

组件评估方向

场景 评估方向
快速原型 使用 Ollama 本地模型 + 文件 Loader
生产 RAG 先按质量、授权、运维和成本评测 Embedding 与检索后端
Agent 工具链 只绑定允许列表中的工具,并校验参数、授权、预算和审计记录
大文档处理 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:结合提供商限制、延迟、重试和成本实测批量收益
  2. 流式输出:只有客户端协议和审核链路支持增量输出时才使用 Stream
  3. Retriever 预热:仅在后端明确支持空查询时预热连接
  4. Lambda 有界化:避免隐藏 IO,执行取消、预算和可观测截止时间

常见问题

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 接口。是否适合生产仍取决于核对过的 revision、提供商契约、授权、评测和运维:

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

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


相关资源