核心摘要
Eino 框架的核心设计思想是组件即接口——每个能力单元(ChatModel、Tool、Retriever 等)都由一个 Go interface 定义清晰的输入、输出和方法签名,底层实现可自由替换。本文将逐一拆解 9 大核心组件的设计意图与实战用法,并通过一个完整的带搜索工具的问答 Bot 示例,串联 ChatModel → Tool → Retriever 的协作全流程。
本文是 Eino 框架全景:为什么用 Go 构建 AI 应用 的系列第二篇。建议先阅读上篇了解 Eino 整体架构。
目录
- 核心要点
- 组件体系设计哲学
- ChatModel 详解
- Tool 系统
- Retriever 与向量检索
- 文档处理链路
- Embedding 与 ChatTemplate
- Lambda 自定义节点
- 实战:构建带搜索工具的问答 Bot
- 最佳实践
- 常见问题
- 总结
- 相关资源
核心要点
- 接口即契约:每个组件通过 Go interface 定义能力边界,实现可自由替换
- ChatModel 三件套:
Generate(同步)、Stream(流式)、BindTools(工具注册)覆盖 LLM 交互全场景 - Tool = ToolInfo + 执行逻辑:通过 JSON Schema 描述参数,让模型理解何时调用
- Retriever 统一抽象:ElasticSearch / VikingDB 实现可互换,上层代码零修改
- Document Pipeline:Loader → Transformer → Indexer 三级流水线覆盖知识入库全流程
- Lambda 粘合剂:任意 Go 函数均可包装为编排节点
组件体系设计哲学
Eino 的组件设计遵循三层原则:
设计原则:
| 原则 | 说明 | 实际效果 |
|---|---|---|
| 接口优先 | 每个组件先定义 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 中最核心的组件,负责与大语言模型的所有交互。
接口定义
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 工具调度 |
多模型提供商支持
// 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 使用示例
// 同步调用 - 适用于后台处理
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 结构体向模型描述自己的能力:
type ToolInfo struct {
Name string // 工具名称
Description string // 功能描述(模型据此判断何时调用)
Parameters *schema.Schema // JSON Schema 格式的参数定义
}
自定义 Tool 实现
// 定义搜索工具
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 的关系
Eino 的 Tool 系统与模型的 Function Calling 能力协同工作:
- 开发者通过
BindTools注册工具列表 - 模型根据对话上下文决定是否调用工具
- 框架解析模型返回的
tool_call,执行对应 Tool - 将执行结果作为
ToolMessage回传模型 - 模型基于工具结果生成最终回答
Retriever 与向量检索
Retriever 组件为 RAG(检索增强生成)应用提供标准化的文档检索能力。
接口抽象
type Retriever interface {
Retrieve(ctx context.Context, query string, opts ...Option) ([]*schema.Document, error)
}
简洁的接口背后隐藏了复杂的向量检索逻辑:Query → Embedding → ANN Search → Document Ranking。
ElasticSearch 实现
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 实现
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 | ✅ 原生支持 |
| 适用规模 | 百万级 | 亿级 |
| 运维复杂度 | 高 | 低(全托管) |
| 成本模型 | 资源型 | 按量计费 |
文档处理链路
在向量检索之前,原始文档需要经过标准化处理流水线:
Document Loader — 数据加载
// 从 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 — 文本处理
// 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 — 存储索引
indexer, _ := elasticsearch.NewIndexer(ctx, &elasticsearch.IndexerConfig{
Addresses: []string{"http://localhost:9200"},
Index: "knowledge_base",
})
// 批量索引文档
err := indexer.Store(ctx, chunks)
Embedding 与 ChatTemplate
Embedding — 文本向量化
Embedding 组件将文本转化为高维向量表示,是向量检索的基础:
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 列表:
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 函数包装为可编排的节点:
// 数据格式化 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:
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 |
错误处理模式
// 为 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)
}
性能优化要点
- 批量 Embedding:一次请求处理多段文本,减少 API 调用次数
- Stream 优先:面向用户的场景始终使用
Stream降低首字延迟 - Retriever 预热:启动时执行一次空查询预热连接池
- Lambda 轻量化:Lambda 节点避免重 IO 操作,保持 < 10ms 执行时间
常见问题
Q: 如何实现模型热切换(不重启服务)?
A: 利用 Go 的 interface 特性,在上层维护一个 ChatModel 变量,通过配置中心动态替换实现:
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:
toolCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result, err := executeTool(toolCtx, toolCall)
if err != nil {
result = "工具执行超时,请尝试其他方式回答"
}
Q: 多个 Retriever 如何融合结果?
A: 使用 Lambda 节点做结果聚合和去重:
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 应用。