核心摘要
Eino 框架的核心设计思想是组件即接口——通过 Go interface 表达能力边界和输入输出契约;只有行为兼容时,底层实现才可以替换。本文拆解 9 个组件方向,并以一个示意性的 ChatModel → Tool 集成骨架说明边界。
本文是 Eino 框架全景:为什么用 Go 构建 AI 应用 的系列第二篇。上篇可用于了解本文接口示例的背景。
目录
- 核心要点
- 组件体系设计哲学
- ChatModel 详解
- Tool 系统
- Retriever 与向量检索
- 文档处理链路
- Embedding 与 ChatTemplate
- Lambda 自定义节点
- 实战:构建带搜索工具的问答 Bot
- 最佳实践
- 常见问题
- 总结
- 相关资源
核心要点
- 接口即契约:每个组件通过 Go interface 定义能力边界,实现可自由替换
- ChatModel 边界:
Generate、Stream和工具绑定覆盖常见交互路径,但能力与 Option 仍取决于提供商和 revision - Tool = ToolInfo + 执行逻辑:通过 JSON Schema 描述参数,让模型理解何时调用
- Retriever 抽象:不同后端可以复用常见检索调用,但过滤、评分、元数据和运维语义必须通过契约测试确认
- Document Pipeline:Loader → Transformer → Indexer 三级流水线覆盖知识入库全流程
- Lambda 粘合剂:具备明确 context、取消和错误契约的 Go 函数可包装为编排节点
组件体系设计哲学
Eino 的组件设计遵循三层原则:
设计原则:
| 原则 | 说明 | 实际效果 |
|---|---|---|
| 接口优先 | 每个组件先定义 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 源码。
接口定义
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: "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 使用示例
// 同步调用 - 适用于后台处理
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 结构体向模型描述自己的能力:
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 格式。执行器仍必须重新校验模型生成的参数;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)
}
该接口背后可能是向量、词法、混合或元数据检索;Embedding、ANN、排序、过滤和授权均取决于后端与应用。
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)
}
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 接口支持多种模型",
})
// 向量维度由所选模型决定,不应硬编码。
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 之间的信任边界,并不是完整的搜索集成:具体 revision 的提供商 API、参数解析、授权、配额和结果来源链路仍需实现。
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 |
错误处理模式
// 为 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:结合提供商限制、延迟、重试和成本实测批量收益
- 流式输出:只有客户端协议和审核链路支持增量输出时才使用
Stream - Retriever 预热:仅在后端明确支持空查询时预热连接
- Lambda 有界化:避免隐藏 IO,执行取消、预算和可观测截止时间
常见问题
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 接口。是否适合生产仍取决于核对过的 revision、提供商契约、授权、评测和运维:
- ChatModel 统一了多模型对接复杂性,一个 interface 覆盖同步、流式和工具绑定
- Tool 标准化了 Function Calling 工作流,让 Agent 获得「行动」能力
- Retriever 抽象了向量检索实现差异,为 RAG 应用提供可插拔后端
- Document Pipeline 覆盖了从数据加载到索引的完整知识入库流程
- Lambda 作为粘合剂让任意逻辑融入编排图
下一篇文章将深入探讨如何使用 Eino 的编排引擎(Chain、Graph 与 Workflow) 将这些组件组装成复杂的 AI 应用。