RAG 一张图讲清楚:检索增强生成的完整流程

发布时间:2026/6/4 11:34:20

RAG 一张图讲清楚:检索增强生成的完整流程 一只用 AI Agent 搭副业产线的程序员如果你用过 ChatGPT 问「我们公司那个支付接口怎么调的」它只能老老实实说「抱歉我不知道你们公司的内部文档」。不是 AI 不行。是它没见过你的私有数据。RAG 解决的就是这个问题——让 LLM 在回答之前先去你的文档库里翻答案翻到了再回答。全称 Retrieval-Augmented Generation检索增强生成。名字很唬人。拆开看就是 5 步。一张图RAG 的 5 个环节用户提问: 支付接口超时怎么办 │ ▼ [1] 问题 → Embedding 向量 │ 调用 Embedding API 把问题变成一串数字 │ 代码位置internal/embed/embedder.go │ ▼ [2] 向量 → 向量数据库检索 │ 拿这串数字去向量数据库里找最相似的 Top-K 个文档片段 │ 代码位置internal/retriever/qdrant.go │ ▼ [3] 检索结果 用户问题 → 组装 Prompt │ 把找到的文档片段塞进 Prompt 的上下文 │ 代码位置internal/assembler/prompt.go │ ▼ [4] Prompt → LLM 推理 │ 调用 DeepSeek带上下文的完整 Prompt 发给模型 │ 代码位置internal/llm/client.go │ ▼ [5] LLM → 返回答案 根据文档支付接口超时常见原因有三1.……就这么简单。拆开每一步你会发现都是模块一和模块二学过的积木。第一步把问题变成向量这不就是模块一第 8 篇讲的 Embedding 吗。// internal/embed/embedder.gopackageembedtypeEmbedderstruct{apiKeystringbaseURLstringmodelstring}funcNewEmbedder(apiKeystring)*Embedder{returnEmbedder{apiKey:apiKey,baseURL:https://api.deepseek.com/anthropic,model:deepseek-v4-pro,}}func(e*Embedder)Embed(textstring)([]float64,error){reqBody:map[string]any{model:e.model,input:text,}body,_:json.Marshal(reqBody)req,_:http.NewRequest(POST,e.baseURL/v1/embeddings,bytes.NewReader(body))req.Header.Set(x-api-key,e.apiKey)req.Header.Set(Content-Type,application/json)resp,err:http.DefaultClient.Do(req)iferr!nil{returnnil,fmt.Errorf(embedding 请求失败: %w,err)}deferresp.Body.Close()varresultstruct{Data[]struct{Embedding[]float64json:embedding}json:data}json.NewDecoder(resp.Body).Decode(result)iflen(result.Data)0{returnnil,fmt.Errorf(返回了空 embedding)}returnresult.Data[0].Embedding,nil}用户问「支付接口超时怎么办」→ 1536 个浮点数。这一步完成了。第二步向量数据库检索 Top-K向量数据库就是一个专门存向量 做最近邻搜索的数据库。你不用自己写余弦相似度算 N 个文档——它帮你搞定。// internal/retriever/qdrant.gopackageretrieverimport(bytesencoding/jsonfmtnet/http)typeQdrantRetrieverstruct{baseURLstringcollectionstringhttpClient*http.Client}funcNewQdrantRetriever(url,collectionstring)*QdrantRetriever{returnQdrantRetriever{baseURL:url,collection:collection,httpClient:http.Client{},}}func(r*QdrantRetriever)Search(vector[]float64,topKint,)([]SearchResult,error){reqBody:map[string]any{vector:vector,limit:topK,with_payload:true,}body,_:json.Marshal(reqBody)url:fmt.Sprintf(%s/collections/%s/points/search,r.baseURL,r.collection)req,_:http.NewRequest(POST,url,bytes.NewReader(body))req.Header.Set(Content-Type,application/json)resp,err:r.httpClient.Do(req)iferr!nil{returnnil,fmt.Errorf(Qdrant 检索失败: %w,err)}deferresp.Body.Close()varresultstruct{Result[]struct{Scorefloat64json:scorePayloadstruct{Textstringjson:textDocstringjson:doc_name}json:payload}json:result}json.NewDecoder(resp.Body).Decode(result)vardocs[]SearchResultfor_,r:rangeresult.Result{docsappend(docs,SearchResult{Score:r.Score,Text:r.Payload.Text,DocName:r.Payload.Doc,})}returndocs,nil}typeSearchResultstruct{Scorefloat64TextstringDocNamestring}Top-K 参数一般设 3-5。太小可能漏掉相关内容太大给 LLM 塞太多噪音反而干扰判断。第三步组装 Prompt——把文档塞进上下文这是 RAG 最关键的手工活。文档碎片怎么拼、位置放哪、怎么告诉 LLM「请基于以下文档回答」——每个选择都影响最终效果。// internal/assembler/prompt.gopackageassemblerimport(fmtstrings)funcBuildRAGPrompt(querystring,docs[]retriever.SearchResult,)[]llm.Message{vardocBlocks[]stringfori,doc:rangedocs{docBlocksappend(docBlocks,fmt.Sprintf([文档%d] 来源%s\n%s,i1,doc.DocName,doc.Text,))}systemPrompt:你是技术文档助手。只根据提供的文档回答。文档中没有提到的内容直接说「文档中未提及」。不要编造。userPrompt:fmt.Sprintf(参考文档\n\n%s\n\n问题%s\n\n请基于以上文档回答。如果文档信息不足请明确说明。,strings.Join(docBlocks,\n\n),query,)return[]llm.Message{{Role:system,Content:systemPrompt},{Role:user,Content:userPrompt},}}这段代码看着简单但 4 个坑都在里面文档编号LLM 回答时可以引用「根据文档 2」方便你回溯验证防幻觉指令「文档中未提及」——这句话能减少 70% 的编造System Prompt 定边界告诉 LLM 你的角色和约束引用位置文档放问题前面因为 LLM 对前面的内容注意力更高第四步 第五步调用 LLM 返回答案这步用的就是模块一的 LLM 客户端没什么新鲜的。// internal/llm/client.go复用模块一的封装func(c*Client)Chat(messages[]Message,tempfloat64,maxTokensint)(string,error){// ... 标准 HTTP 调用跟前面文章一样}核心区别只有一个Prompt 里带了你的私有文档内容。LLM 还是那个 LLM但 Prompt 不一样了答案就不一样了。端到端跑通把 5 步串起来funcmain(){apiKey:os.Getenv(DEEPSEEK_API_KEY)embedder:embed.NewEmbedder(apiKey)retriever:retriever.NewQdrantRetriever(http://localhost:6333,tech_docs)llmClient:llm.NewDeepSeekClient(apiKey)query:支付接口超时怎么排查// Step 1: Query → EmbeddingqueryVec,_:embedder.Embed(query)// Step 2: Vector → Top-K documentsdocs,_:retriever.Search(queryVec,3)// Step 3: Assemble Promptmessages:assembler.BuildRAGPrompt(query,docs)// Step 4-5: LLM → Answeranswer,_:llmClient.Chat(messages,0.1,500)fmt.Println(answer)}从用户提问到 AI 基于你的文档回答就这 5 步。每一步都有明确的代码位置。RAG 能做什么不能做什么能做基于私有文档的问答技术文档、制度、合同客服机器人产品手册 FAQ 作为知识库项目知识库设计方案、会议纪要全灌进去不能做跨文档的逻辑推理文档 A 说 X 依赖 Y文档 B 说 Y 已废弃——RAG 推不出 X 也废了实时数据查询RAG 基于离线索引不是实时库深度分析/总结整本文档RAG 返回的是片段不是全书理解本篇核心收获RAG 不是一个新技术。它是三个老技术的组合Embedding模块一 Prompt模块二 向量数据库本模块。组合起来恰好解决了「私有数据问答」这个刚需。下一篇我们拆 RAG 最容易踩的坑——文档分块。切多大怎么切同一份文档用 3 种策略切实测检索效果用数据说话。关注我别错过。 一只用 AI Agent 搭副业产线的程序员全平台同名虾哥不加班需要定制 AI 工具来聊聊 → lob_ai源码GitHub - lobster-bujiaban/rag-from-scratch

相关新闻