前言1. 为什么需要 RAG2. RAG 的整体流程(1) 先跑通 ChatModel(2) 使用 Prompt 模板控制模型输入(3) Embedding让文本可以被语义检索(4) Indexer把知识写入向量数据库(5) Retriever从向量数据库检索相关知识(6) Transformer让文档更适合入库和生成完整 RAG 问答流程3. 总结前言最近在学习Eino这类 AI 应用开发框架, 刚开始只是跑一些简单 demo比如调用大模型、流式输出、使用Prompt模板。往后学习自然接触到Embedding、Indexer、Retriever、Transform这些概念。这些组件单独看都不复杂但如果把它们串起来其实就是一个最小RAG应用的核心流程。这篇文章不是从理论角度完整介绍RAG而是基于我学习Eino的几个小demo回顾一下自己是如何一步步理解 RAG 的。1. 为什么需要 RAGRAGRetrieval Augmented Generation检索增强生成, 通过将外部知识库与大模型能力结合在回答问题前先进行知识检索再基于检索结果生成答案从而有效提升回答的准确性、时效性和可解释性。平常我们使用通用大模型时, 其都是通过获取互联网已有的内容进行回复生成所以针对一些私有非公开的数据我们需要提供足够的上下文告诉大模型, 它才能生成我们想要的答案。将大模型应用于实际业务场景时会发现存在以下几个方面问题知识的局限性大模型自身的知识完全源于训练数据而现有的主流大模型deepseek、gpt的训练集基本都是构建于网络公开的数据对于一些实时性的、非公开的或私域的数据是没有。幻觉问题所有的深度学习模型的底层原理都是基于数学概率模型输出实质上是一系列数值运算大模型也不例外所以它经常会一本正经地胡说八道尤其是在大模型自身不具备某一方面的知识或不擅长的任务场景。数据安全性对于企业来说数据安全至关重要没有企业愿意承担数据泄露的风险尤其是大公司没有人将私域数据上传第三方平台进行训练会推理。这也导致完全依赖通用大模型自身能力的应用方案不得不在数据安全和效果方面进行取舍。当我们希望模型回答公司内部文档、项目知识库、用户手册里的内容单纯依赖模型本身是不够的。而RAG就是解决上述问题的有效方案。RAG 的思路是不要直接让模型凭空回答而是先检索相关资料再让模型基于资料回答它的核心不是让大模型变得无所不知而是在生成答案之前先给它补充一批和问题相关的外部知识。2. RAG 的整体流程一个最基础的 RAG 系统通常可以拆成两条链路。第一条是知识入库链路原始文档 - Transformer 清洗/切分 - Embedding 向量化 - Indexer 写入向量数据库第二条是问答检索链路用户问题 - Retriever 检索文档 - Transformer 整理检索结果 - Prompt 拼接上下文 - ChatModel 生成回答如果对应到 Eino 里的组件可以大概这样理解组件作用ChatModel调用大模型生成回答Prompt Template组织模型输入Embedding把文本转换成向量Indexer把文档写入向量数据库Retriever从向量数据库检索相关文档Transformer对文档进行切分、转换或清洗我目前的学习路径也基本是沿着这个顺序展开的。(1) 先跑通 ChatModel学习 Eino 的第一步是先跑通大模型调用model, err : ark.NewChatModel(ctx, ark.ChatModelConfig{ APIKey: os.Getenv(ARK_API_KEY), Model: os.Getenv(MODEL), Timeout: timeout, }) messages : []*schema.Message{ schema.SystemMessage(你是一个助手), schema.UserMessage(请介绍一下你自己), } response, err : model.Generate(ctx, messages)这一步是先确认模型可以正常被调用。在 RAG 里ChatModel 是最后负责生成答案的组件。前面的检索、向量化、文档召回最终都是为了给模型提供更可靠的上下文。除了普通生成外还有流式输出reader, err : model.Stream(ctx, messages)流式输出在实际问答产品中很常见因为用户不用等完整答案生成完模型可以边生成边返回体验更接近我们平时使用的大模型应用。(2) 使用 Prompt 模板控制模型输入在能调用模型之后下一步就是学习如何组织 Prompt。如果只是简单问答可以直接构造 schema.UserMessage。但在真实应用里Prompt 往往不是固定文本而是由多个变量动态组成。比如用户角色用户问题检索到的上下文回答规则输出格式要求Eino 提供了 prompt.FromMessages 来定义模板template : prompt.FromMessages(schema.FString, schema.SystemMessage(你是一个{role}), schema.Message{ Role: schema.User, Content: {task}, }, ) params : map[string]any{ role: 问答助手, task: 请根据资料回答问题, } message, err : template.Format(ctx, params)一个典型的RAG Prompt可能长这样你是一个严谨的问答助手。 请只根据下面的参考资料回答用户问题。 如果参考资料中没有答案请说明无法从资料中得到结论。 参考资料 {context} 用户问题 {question}这里的 {context} 占位符就是 Retriever 检索出来的文档内容{question} 是用户的原始问题。所以 Prompt模板在 RAG 中就是把用户问题和外部知识组合成稳定的模型输入。(3) Embedding让文本可以被语义检索理解 RAG 时Embedding向量化是绕不开的一步。传统搜索更多依赖关键词匹配比如用户搜iPhone手机系统可能根据iPhone:和手机这两个词去匹配内容。但语义搜索更关注文本含义。即使两个句子没有完全相同的关键词只要语义接近也应该能被检索出来。Embedding 组件是一个用于将文本转换为向量表示的组件。它的主要作用是将文本内容映射到向量空间使得语义相似的文本在向量空间中的距离较近。Embedding 的作用就是把文本转换成向量文本 - 一组浮点数语义相近的文本在向量空间中的距离也会更近。embedder, err : ark.NewEmbedder(ctx, ark.EmbeddingConfig{ APIKey: os.Getenv(ARK_API_KEY), Model: os.Getenv(EMBEDDER), Timeout: timeout, }) texts : []string{ 第一段文本, 第二段文本, } embeddings, err : embedder.EmbedStrings(ctx, texts)然后可以打印每个向量的维度for i, embedding : range embeddings { fmt.Println(文本, i1, 的向量维度, len(embedding)) }(4) Indexer把知识写入向量数据库有了 Embedding 之后下一步就是把文档存起来, 这就需要向量数据库。我这里使用的是 Milvus。在 Milvus 里需要先定义 Collection 的字段结构。比如fields : []*entity.Field{ { Name: id, DataType: entity.FieldTypeVarChar, PrimaryKey: true, }, { Name: vector, DataType: entity.FieldTypeFloatVector, TypeParams: map[string]string{ dim: 1024, }, }, { Name: content, DataType: entity.FieldTypeVarChar, }, { Name: metadata, DataType: entity.FieldTypeJSON, }, }这几个字段分别表示字段含义id文档唯一 IDvector文档内容对应的向量content原始文本内容metadata额外信息比如作者、来源、分类然后使用 Eino 的 Milvus Indexerindexer, err : milvus.NewIndexer(ctx, milvus.IndexerConfig{ Client: milvusCli, Collection: collection, Fields: fields, Embedding: embedder, })接着构造文档并写入docs : []*schema.Document{ { ID: 1, Content: 这里是一段需要入库的知识内容, MetaData: map[string]any{ author: Ryne, }, }, } ids, err : indexer.Store(ctx, docs)Indexer 实际做了两件事1. 调用 Embedding把文档内容转换成向量2. 把文档内容、向量和元数据写入 Milvus这就完成了 RAG 的知识入库阶段。(5) Retriever从向量数据库检索相关知识Indexer 负责把知识放进去Retriever 则负责把知识取出来。当用户提出一个问题时Retriever 的流程通常是用户问题 - 问题向量化 - 向量数据库相似度搜索 - 返回相关文档比如用户问原神是什么系统会先把这个问题转换成向量然后去 Milvus 里查找语义最接近的文档内容。返回的结果可能是原神是一款二次元开放世界冒险游戏...这个过程不是简单的关键词搜索而是基于向量相似度的语义搜索。下面是检索的基础案例, milvus客户端Client需要自己配置, 这里不占篇幅了retriever, err : milvus.NewRetriever(ctx, milvus.RetrieverConfig{ Client: MilvusCli, Collection: test, Partition: nil, VectorField: vector, OutputFields: []string{ id, content, metadata, }, TopK: 1, Embedding: embedder, })这一部分补齐之后RAG 的核心闭环就完整了。(6) Transformer让文档更适合入库和生成这一节重点写Transformer 并不只属于入库阶段它的本质是对 Document 做转换处理可以分两种情况入库前 原始文档 - Transformer - Embedding - Indexer检索后 Retriever - Transformer - Prompt - ChatModel入库前主要做文档切分文本清洗格式统一metadata 补充检索后主要做合并多个文档片段截断过长内容去重过滤低质量结果转成适合 Prompt 的上下文文本splitter, err : markdown.NewHeaderSplitter(ctx, markdown.HeaderConfig{ Headers: map[string]string{ #: h1, ##: h2, ###: h3, }, TrimHeaders: false, }) if err ! nil { panic(err) } // 准备要分割的文档 content, err : os.OpenFile(./document.md, os.O_CREATE|os.O_RDWR, 0755) if err ! nil { panic(err) } defer content.Close() bs, err : os.ReadFile(./document.md) if err ! nil { panic(err) } docs : []*schema.Document{ { ID: doc1, Content: string(bs), }, } // 执行分割 results, err : splitter.Transform(ctx, docs)完整 RAG 问答流程当 ChatModel、Prompt、Embedding、Indexer、Retriever Transform都理解之后就可以把它们串成完整 RAG 流程。整体逻辑可以概括为用户提问 ↓ Retriever 检索相关文档 ↓ Transformer 整理检索结果 ↓ Prompt 拼接上下文 ↓ ChatModel 基于上下文生成回答 ↓ 返回最终答案用伪代码表示question : 用户的问题 docs, err : retriever.Retrieve(ctx, question) message, err : template.Format(ctx, map[string]any{ context: docs, question: question, }) answer, err : chatModel.Generate(ctx, message)所以RAG不是某一个单独组件而是一条由多个组件组成的工程链路,其中Embedding 解决如何表示语义Indexer 解决如何存储知识Retriever 解决如何找回知识Transform 解决文本如何转换处理Prompt 解决如何组织上下文ChatModel 解决如何生成自然语言答案只有这些组件配合起来才是一个完整的 RAG 应用。3. 总结第一: RAG 的重点不只是大模型。刚开始很容易把注意力都放在模型调用上但真正做 RAG 时检索链路同样重要。模型生成答案只是最后一步前面能不能找到正确资料直接决定回答质量。第二: Embedding 是语义检索的基础。如果没有 Embedding系统很难理解“意思相近但表达不同”的文本。向量化之后语义搜索才成为可能。第三: Indexer 和 Retriever 是一进一出。Indexer 负责把文档写入向量数据库Retriever 负责根据问题从数据库中找回相关文档。一个负责存一个负责取。第四: Prompt 决定模型如何使用检索结果。即使检索到了正确资料如果 Prompt 写得不好模型也可能没有正确引用上下文甚至继续自由发挥。第五: 最小 RAG 系统并不复杂。真正复杂的是后续优化比如文档切分、召回准确率、重排序、多轮上下文、权限控制、引用来源等。通过这几个 Eino demo可以简单总结为先把知识向量化并存入数据库 用户提问时再检索相关知识 最后把知识交给大模型生成回答。对应Eino: Embedding Indexer 负责知识入库 Retriever 负责知识召回 Prompt 负责组织上下文 ChatModel 负责生成答案RAG 的核心价值不是替代大模型而是为大模型补充可靠、可更新、可追溯的外部知识。对我来说AI 应用开发并不是只会调用模型接口就够了更重要的是理解模型周围的工程链路。RAG 正是一个很好的切入点它把模型调用、Prompt、向量化、向量数据库和检索流程都串在了一起。