基于 Eino 框架从零搭建一个 RAG
基于 CloudWeGo Eino 从零搭建 Go 语言 RAG 检索增强系统
前言
大语言模型(LLM)在落地企业场景时,普遍面临知识时效性差、专业领域内容缺失、生成幻觉等问题。检索增强生成(RAG)通过「先检索外部知识库、再基于参考内容生成答案」的思路,低成本且高效地解决了上述痛点,已成为构建企业级 AI 应用的核心技术方案。
CloudWeGo 推出的Eino是一款 Go 生态的大模型应用开发框架,它对主流大模型服务、向量数据库、文档处理工具做了统一抽象与封装,开发者无需对接各类异构 SDK,仅通过几行代码即可组装出完整的 RAG 链路。本文将基于 Eino 框架,从组件拆解到完整落地,一步步搭建一个可运行的 RAG 知识库系统。
一、RAG 核心组件拆解
在 Eino 的设计中,一个完整的 RAG 系统由以下核心组件构成,各司其职且可灵活插拔:
| 组件 | 核心作用 | 通俗类比 |
|---|---|---|
| ChatModel | 与大语言模型直接交互,负责最终答案生成 | 答题的「考生」 |
| ChatTemplate | 结构化 Prompt 模板,统一管理系统提示、上下文、检索结果 | 标准化答题卡 + 答题规则 |
| Embedding | 将文本转换为高维向量,是语义检索的基础 | 把文字翻译成「向量语言」 |
| Indexer | 将文档向量化后存入向量数据库,建立索引 | 整理并装订「小抄本」 |
| Retriever | 根据用户问题从向量库召回最相关的文档片段 | 从「小抄本」里翻找对应知识点 |
| Transformer | 文档预处理:分割、过滤、格式转换、合并等 | 把长文档裁成适合翻阅的小片段 |
| Tool | 大模型可调用的外部工具(扩展能力) | 考生可以使用的计算器、词典 |
整条 RAG 的核心链路为:
原始文档 → Transformer 切片 → Embedding 向量化 → Indexer 入库 → 用户提问 → Retriever 检索 → ChatTemplate 组装 Prompt → ChatModel 生成答案
二、前置准备
2.1 环境与依赖
- Go 1.21 及以上版本
- 已部署的 Milvus 向量数据库(本文使用 Milvus 2.x 独立部署)
- 火山方舟(字节跳动大模型平台)API Key(用于调用豆包大模型与 Embedding 服务)
2.2 项目初始化
创建项目并安装核心依赖:
mkdirEino_frame&&cdEino_frame go mod init Eino_frame# 安装 Eino 核心与扩展包go get github.com/cloudwego/eino go get github.com/cloudwego/eino-ext go get github.com/milvus-io/milvus-sdk-go/v2 go get github.com/joho/godotenv2.3 环境变量配置
在项目根目录创建.env文件,统一管理密钥与模型配置:
# .env ARK_API_KEY=your_ark_api_key_here MODEL=doubao-pro-32k # 对话模型名称 EMBEDDER=doubao-embedding-text-240515 # 向量嵌入模型名称2.4 项目结构
最终项目目录结构如下:
Eino_frame/ ├── main.go # 主程序入口,串联全流程 ├── .env # 环境变量 ├── RAG_introduction.md # 测试知识库文档 └── rag/ ├── milvus_client.go # Milvus 客户端初始化 ├── embedding.go # 向量嵌入组件 ├── transformer.go # 文档分割转换组件 ├── indexer.go # 索引存储组件 ├── retriever.go # 检索组件 ├── chat_model.go # 大模型对话组件 └── prompt.go # ChatTemplate 提示词模板三、核心组件代码实现
我们将所有 RAG 能力封装在rag包下,保证职责单一、可复用。
3.1 Milvus 客户端初始化
所有向量操作都依赖 Milvus 客户端,做全局单例初始化。
rag/milvus_client.go
packageragimport("context""github.com/milvus-io/milvus-sdk-go/v2/client""log")// MilvusCli 全局 Milvus 客户端实例varMilvusCli client.Client// InitMilvusClient 初始化 Milvus 连接funcInitMilvusClient(){ctx:=context.Background()cli,err:=client.NewClient(ctx,client.Config{Address:"39.105.40.22:19530",// Milvus 服务地址DBName:"EinoFrame",// 目标数据库名})iferr!=nil{log.Fatalf("初始化 Milvus 客户端失败: %v",err)}MilvusCli=cli log.Println("Milvus 客户端初始化成功")}💡 注意:正式创建 Indexer 前,必须确保 Milvus 服务已启动,且对应数据库已提前创建。
3.2 Embedding 文本向量化组件
Embedding 负责将文本转换为高维向量,是实现语义相似度匹配的基础。这里我们使用火山方舟的 Embedding 服务。
rag/embedding.go
packageragimport("context""github.com/cloudwego/eino-ext/components/embedding/ark""os""log")// NewArkEmbedding 创建火山方舟向量嵌入器funcNewArkEmbedding(ctx context.Context)*ark.Embedder{apiType:=ark.APITypeMultiModal embedder,err:=ark.NewEmbedder(ctx,&ark.EmbeddingConfig{APIKey:os.Getenv("ARK_API_KEY"),Model:os.Getenv("EMBEDDER"),APIType:&apiType,})iferr!=nil{log.Printf("创建 Embedding 失败: %v",err)returnnil}returnembedder}3.3 Transformer 文档分割组件
长文档无法直接向量化和精准检索,需要先按语义切分为多个小块。Eino 内置了 Markdown 标题分割器,非常适合技术文档、博客等结构化内容。
rag/transformer.go
packageragimport("context""github.com/cloudwego/eino-ext/components/document/transformer/splitter/markdown""github.com/cloudwego/eino/components/document""log")// NewArkTransformer 创建 Markdown 标题分割器// 按 #、##、### 标题层级切割文档,保留标题结构funcNewArkTransformer(ctx context.Context)document.Transformer{splitter,err:=markdown.NewHeaderSplitter(ctx,&markdown.HeaderConfig{Headers:map[string]string{"#":"h1","##":"h2","###":"h3",},TrimHeaders:false,// 保留标题在片段内容中})iferr!=nil{log.Printf("创建文档分割器失败: %v",err)returnnil}returnsplitter}3.4 Indexer 索引存储组件
Indexer 是「做小抄」的核心组件:它自动调用 Embedding 将文档片段向量化,然后写入 Milvus 集合中。
⚠️ 重要:正式创建 Indexer 之前,一定要先定义好集合的字段结构,字段类型、维度必须与 Embedding 输出严格匹配。
rag/indexer.go
packageragimport("context""github.com/cloudwego/eino-ext/components/embedding/ark""github.com/cloudwego/eino-ext/components/indexer/milvus""github.com/milvus-io/milvus-sdk-go/v2/entity""log")// NewArkIndexer 创建 Milvus 索引器funcNewArkIndexer(ctx context.Context,embedder*ark.Embedder)*milvus.Indexer{collectionName:="test"// 定义集合字段结构fields:=[]*entity.Field{{Name:"id",DataType:entity.FieldTypeVarChar,TypeParams:map[string]string{"max_length":"255"},PrimaryKey:true,},{Name:"vector",// 向量字段名,需与 Retriever 配置一致DataType:entity.FieldTypeFloatVector,// 普通浮点向量;二值向量使用 BinaryVectorTypeParams:map[string]string{"dim":"1536"},// 维度必须与 Embedding 模型输出匹配},{Name:"content",DataType:entity.FieldTypeVarChar,TypeParams:map[string]string{"max_length":"8192"},},{Name:"metadata",DataType:entity.FieldTypeJSON,},}indexer,err:=milvus.NewIndexer(ctx,&milvus.IndexerConfig{Client:MilvusCli,Collection:collectionName,Fields:fields,Embedding:embedder,// 绑定嵌入器,入库时自动完成向量化})iferr!=nil{log.Printf("创建 Indexer 失败: %v",err)returnnil}log.Println("Indexer 创建成功")returnindexer}3.5 Retriever 检索组件
Retriever 是「翻小抄」的组件:将用户问题向量化后,在 Milvus 中做相似度搜索,召回最相关的 Top K 个文档片段。
rag/retriever.go
packageragimport("context""github.com/cloudwego/eino-ext/components/embedding/ark""github.com/cloudwego/eino-ext/components/retriever/milvus""log")// NewArkRetriever 创建 Milvus 检索器funcNewArkRetriever(ctx context.Context,embedder*ark.Embedder)*milvus.Retriever{retriever,err:=milvus.NewRetriever(ctx,&milvus.RetrieverConfig{Client:MilvusCli,Collection:"test",VectorField:"vector",// 指定用于相似度计算的向量字段OutputFields:[]string{// 检索返回的字段"id","content","metadata",},TopK:3,// 召回最相关的 3 个片段Embedding:embedder,})iferr!=nil{log.Printf("创建 Retriever 失败: %v",err)returnnil}returnretriever}3.6 ChatModel 对话模型组件
ChatModel 封装了与大模型的交互逻辑,负责基于组装好的 Prompt 生成最终答案。
rag/chat_model.go
packageragimport("context""github.com/cloudwego/eino-ext/components/model/ark""os""log")// NewArkModel 创建火山方舟对话模型funcNewArkModel(ctx context.Context)*ark.ChatModel{model,err:=ark.NewChatModel(ctx,&ark.ChatModelConfig{APIKey:os.Getenv("ARK_API_KEY"),Model:os.Getenv("MODEL"),})iferr!=nil{log.Printf("创建 ChatModel 失败: %v",err)returnnil}returnmodel}3.7 ChatTemplate 提示词模板
ChatTemplate 用于结构化地管理和组装 Prompt。我们将检索到的知识库内容注入到系统提示词中,约束模型仅基于参考内容回答,从源头减少幻觉。
新增rag/prompt.go:
packageragimport("github.com/cloudwego/eino/components/prompt""github.com/cloudwego/eino/schema")// BuildRAGPrompt 构建 RAG 对话模板// 将检索到的参考资料注入 system prompt,随用户问题一起喂给大模型funcBuildRAGPrompt()prompt.ChatTemplate{systemPrompt:=`你是一个专业的知识问答助手,请严格基于下面的参考资料回答用户的问题。 如果参考资料中没有相关信息,请如实回答"暂未找到相关资料",不要编造内容。 参考资料: {{.references}}`returnprompt.FromMessages(schema.SystemMessage(systemPrompt),schema.UserMessage("{{.question}}"),).WithVariables("references","question")}四、完整 RAG 流程串联
现在我们在main.go中把所有组件串起来,实现两个核心流程:
- 文档入库:加载 Markdown 文档 → 分割 → 向量化 → 存入 Milvus
- 检索问答:接收用户问题 → 检索相关片段 → 组装 Prompt → 调用大模型生成答案
main.go
packagemainimport("Eino_frame/rag""context""fmt""github.com/cloudwego/eino/schema""github.com/joho/godotenv""log""os""strconv""strings")funcmain(){ctx:=context.Background()// 加载环境变量(全局加载一次即可)err:=godotenv.Load(".env")iferr!=nil{log.Fatalf("加载 .env 文件失败: %v",err)}// 1. 初始化 Milvus 客户端rag.InitMilvusClient()// 2. 初始化所有基础组件embedder:=rag.NewArkEmbedding(ctx)indexer:=rag.NewArkIndexer(ctx,embedder)retriever:=rag.NewArkRetriever(ctx,embedder)splitter:=rag.NewArkTransformer(ctx)chatModel:=rag.NewArkModel(ctx)promptTemplate:=rag.BuildRAGPrompt()// ========== 流程一:文档入库 ==========log.Println("===== 开始文档入库 =====")// 读取本地 Markdown 知识库文档bs,err:=os.ReadFile("./RAG_introduction.md")iferr!=nil{log.Fatalf("读取文档失败: %v",err)}// 构造 Eino 标准文档对象docs:=[]*schema.Document{{ID:"doc_rag_intro",Content:string(bs),},}// 按标题分割文档splitDocs,err:=splitter.Transform(ctx,docs)iferr!=nil{log.Fatalf("文档分割失败: %v",err)}log.Printf("文档分割完成,共 %d 个片段",len(splitDocs))// 为每个片段设置唯一 IDfori,doc:=rangesplitDocs{doc.ID=docs[0].ID+"_chunk_"+strconv.Itoa(i)}// 自动向量化并写入 Milvusids,err:=indexer.Store(ctx,splitDocs)iferr!=nil{log.Fatalf("文档入库失败: %v",err)}log.Printf("文档入库成功,ID 列表: %v",ids)// ========== 流程二:RAG 检索问答 ==========log.Println("\n===== 开始 RAG 问答 =====")userQuestion:="RAG 技术主要解决了大语言模型的哪些问题?"log.Printf("用户问题: %s",userQuestion)// 1. 从向量库检索相关文档片段results,err:=retriever.Retrieve(ctx,userQuestion)iferr!=nil{log.Fatalf("检索失败: %v",err)}log.Printf("检索到 %d 条相关片段",len(results))// 2. 拼接参考资料文本varrefBuilder strings.Builderfori,res:=rangeresults{refBuilder.WriteString(fmt.Sprintf("[片段%d]\n%s\n\n",i+1,res.Content))}references:=refBuilder.String()// 3. 用模板格式化最终 Promptmessages,err:=promptTemplate.Format(ctx,map[string]any{"references":references,"question":userQuestion,})iferr!=nil{log.Fatalf("格式化 Prompt 失败: %v",err)}// 4. 调用大模型生成答案resp,err:=chatModel.Generate(ctx,messages)iferr!=nil{log.Fatalf("调用模型失败: %v",err)}log.Println("\n===== 最终答案 =====")fmt.Println(resp.Content)}五、运行与验证
- 在项目根目录准备测试文档
RAG_introduction.md,写入 RAG 相关的技术介绍内容 - 执行主程序:
go run main.go- 预期输出顺序:
- Milvus 客户端初始化成功
- 文档分割完成,输出片段数量
- 文档入库成功,输出所有片段 ID
- 输出检索到的相关片段数量
- 打印大模型基于参考资料生成的最终答案
六、优化与扩展方向
- 增量入库与去重:生产环境增加文档指纹校验,避免重复写入;支持批量入库提升性能
- 检索后重排序(Rerank):在向量粗召回后增加重排序模型,进一步提升检索准确率
- 多格式文档支持:接入 PDF、Word、网页、PPT 等格式的 Transformer,扩展知识库来源
- 多轮对话记忆:增加会话上下文管理,实现带历史记忆的 RAG 对话
- 工具调用能力:基于 Eino 的 Tool 组件,让大模型可调用数据库、外部 API 等工具
七、总结
Eino 框架通过统一的组件抽象,极大降低了 Go 语言下 RAG 系统的开发门槛。开发者只需关注业务逻辑本身,无需花费大量精力对接不同厂商的 SDK、处理向量数据库的底层细节。从文档切片到最终生成答案,整条链路清晰可扩展,非常适合快速落地企业内部知识库、智能客服、技术问答等场景。
