当前位置: 首页 > news >正文

VecDB第五篇:从问题到答案:一个完整的RAG系统是如何工作的

前言

前面我们学了:

  • 第一篇:用Prompt把知识库塞给LLM
  • 第二篇:把文字变成数字(文本向量化)
  • 第三篇:用余弦相似度判断"像不像"
  • 第四篇:用向量数据库存储和检索

今天,我们把所有知识点串起来,构建一个完整的RAG系统。

读完这篇,你就会明白:从用户提问到系统回答,中间到底发生了什么。


一、先说一个故事

1.1 用户问了一个问题

假设你是一个智能客服,用户问:

用户:月球任务什么时候发射?

知识库里有8句话:

句子1:阿尔忒弥斯2号是美国宇航局计划中的载人月球轨道飞行任务... 句子2:该任务将使用太空发射系统火箭和猎户座飞船... 句子3:阿尔忒弥斯2号的主要目标是测试生命支持系统... 句子4:任务计划在2025年执行,将是自阿波罗17号以来首次载人离开地球轨道的任务... 句子5:此次飞行将为后续的阿尔忒弥斯3号月球着陆任务提供关键数据和技术验证... 句子6:宇航员将在飞行过程中进行科学实验,并测试深空通信和导航系统... 句子7:任务将持续约10天,飞行距离将达到27万英里... 句子8:阿尔忒弥斯2号的成功将为建立月球基地和未来火星探索奠定基础...

问题来了:系统怎么知道哪句话最相关?

1.2 第一篇的方案:全部塞进Prompt

系统:你是智能客服,请根据以下知识库回答问题: 知识库: - 句子1... - 句子2... - 句子3... - ...(全部8句话) 用户问:月球任务什么时候发射?

问题:知识库如果有一万句话,prompt就爆了。

1.3 更聪明的方案:只给最相关的

系统:你是智能客服,请根据以下知识库回答问题: 知识库: - 句子4:任务计划在2025年执行... 用户问:月球任务什么时候发射?

妙处:只给最相关的1-2句话,prompt精简,答案准确。

这就是RAG的核心思想:检索增强生成。


二、完整流程图

┌─────────────┐ │ 用户提问 │ │ "月球任务什么时候发射?" │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ 第一步:向量化 │ │ 把问题转成向量 │ │ [0, 1, 1, 0, ...] │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ 第二步:检索 │ │ 在向量数据库中找最相似的 │ │ 返回Top-K个结果 │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ 第三步:组装Prompt │ │ 把检索结果塞进prompt │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ 第四步:调用LLM │ │ 让大模型生成答案 │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ 返回答案 │ │ "任务计划在2025年执行" │ └─────────────┘

三、一步步实现

3.0 准备工作:定义知识库和词汇表

直接使用前面文章的常量,简单明了:

packagemainimport("fmt""math""strings")// 知识库:关于阿尔忒弥斯2号的8句话varknowledgeBase=[]string{"阿尔忒弥斯2号是美国宇航局计划中的载人月球轨道飞行任务,标志着人类重返月球的重要一步","该任务将使用太空发射系统火箭和猎户座飞船,搭载宇航员进行绕月飞行","阿尔忒弥斯2号的主要目标是测试生命支持系统和宇航员在深空环境中的生存能力","任务计划在2025年执行,将是自阿波罗17号以来首次载人离开地球轨道的任务","此次飞行将为后续的阿尔忒弥斯3号月球着陆任务提供关键数据和技术验证","宇航员将在飞行过程中进行科学实验,并测试深空通信和导航系统","任务将持续约10天,飞行距离将达到27万英里,是地球到月球距离的10倍以上","阿尔忒弥斯2号的成功将为建立月球基地和未来火星探索奠定基础",}// 词汇表:我们关心的10个关键词varvocab=[]string{"阿尔忒弥斯","月球","任务","宇航员","火箭","飞船","测试","系统","飞行","轨道"}

3.1 第一步:向量化(复用第二篇的代码)

把文字变成数字:

// 文本清洗funccleanText(textstring)string{varcleaned strings.Builderfor_,r:=rangetext{ifr>=0x4e00&&r<=0x9fff{cleaned.WriteRune(r)}}returncleaned.String()}// 分词处理functokenize(textstring,vocab[]string)[]string{vartokens[]stringfor_,vocabWord:=rangevocab{count:=strings.Count(text,vocabWord)fori:=0;i<count;i++{tokens=append(tokens,vocabWord)}}returntokens}// 词频统计funccountFrequency(tokens[]string)map[string]int{freq:=make(map[string]int)for_,token:=rangetokens{freq[token]++}returnfreq}// 构建向量funcbuildVector(freqmap[string]int,vocab[]string)[]float64{vector:=make([]float64,len(vocab))fori,word:=rangevocab{vector[i]=float64(freq[word])}returnvector}// 向量归一化funcnormalize(vector[]float64){sum:=0.0for_,v:=rangevector{sum+=v*v}ifsum==0{return}magnitude:=math.Sqrt(sum)fori:=rangevector{vector[i]/=magnitude}}// 把文本转成向量(五步合一)functextToVector(textstring)[]float64{cleaned:=cleanText(text)tokens:=tokenize(cleaned,vocab)freq:=countFrequency(tokens)vector:=buildVector(freq,vocab)normalize(vector)returnvector}

测试一下

// 用户问题query:="月球任务什么时候发射"queryVector:=textToVector(query)fmt.Printf("问题向量: %v\n",queryVector)// 输出: [0, 0.707, 0.707, 0, 0, 0, 0, 0, 0, 0]// ↑ 只有"月球"和"任务"两个词

3.2 第二步:检索(复用第三篇的余弦相似度)

在知识库中找到最相似的句子:

// 计算余弦相似度funccosineSimilarity(a,b[]float64)float64{// 第一步:算点积dotProduct:=0.0fori:=rangea{dotProduct+=a[i]*b[i]}// 第二步:算各自的长度lengthA:=0.0lengthB:=0.0fori:=rangea{lengthA+=a[i]*a[i]lengthB+=b[i]*b[i]}lengthA=math.Sqrt(lengthA)lengthB=math.Sqrt(lengthB)// 第三步:点积除以长度乘积iflengthA==0||lengthB==0{return0}returndotProduct/(lengthA*lengthB)}// 搜索最相似的知识funcsearchKnowledge(querystring,topKint)[]SearchResult{// 把问题转成向量queryVector:=textToVector(query)// 计算每句话的相似度varresults[]SearchResultfori,content:=rangeknowledgeBase{contentVector:=textToVector(content)similarity:=cosineSimilarity(queryVector,contentVector)results=append(results,SearchResult{ID:i,Content:content,Similarity:similarity,})}// 按相似度排序(从高到低)sort.Slice(results,func(i,jint)bool{returnresults[i].Similarity>results[j].Similarity})// 返回Top-K个结果iflen(results)>topK{results=results[:topK]}returnresults}

测试一下

results:=searchKnowledge("月球任务什么时候发射",3)fori,r:=rangeresults{fmt.Printf("第%d名: 相似度=%.2f, 内容=%s\n",i+1,r.Similarity,r.Content)}

输出

第1名: 相似度=0.71, 内容=任务计划在2025年执行,将是自阿波罗17号以来首次载人离开地球轨道的任务 第2名: 相似度=0.50, 内容=阿尔忒弥斯2号是美国宇航局计划中的载人月球轨道飞行任务,标志着人类重返月球的重要一步 第3名: 相似度=0.41, 内容=此次飞行将为后续的阿尔忒弥斯3号月球着陆任务提供关键数据和技术验证

看到了吗?第一名正好是回答"什么时候发射"的那句话!


3.3 第三步:组装Prompt

把检索结果塞进prompt:

funcbuildPrompt(querystring,results[]SearchResult)string{// 组装知识库部分knowledge:=""fori,r:=rangeresults{knowledge+=fmt.Sprintf("%d. %s\n",i+1,r.Content)}// 组装完整promptprompt:=fmt.Sprintf(`你是一个智能客服,请根据以下知识库回答用户的问题。 知识库: %s 用户问题:%s 请简洁准确地回答:`,knowledge,query)returnprompt}

测试一下

query:="月球任务什么时候发射"results:=searchKnowledge(query,2)prompt:=buildPrompt(query,results)fmt.Println(prompt)

输出

你是一个智能客服,请根据以下知识库回答用户的问题。 知识库: 1. 任务计划在2025年执行,将是自阿波罗17号以来首次载人离开地球轨道的任务 2. 阿尔忒弥斯2号是美国宇航局计划中的载人月球轨道飞行任务,标志着人类重返月球的重要一步 用户问题:月球任务什么时候发射? 请简洁准确地回答:

3.4 第四步:调用LLM(复用第一篇的代码)

把这个prompt发给大模型:

// 这里的代码与第一篇相同,调用Ollama + DeepSeekfunccallLLM(promptstring)string{// ... 调用Ollama的代码 ...// 参考第一篇文章的 talkToOllama 函数}

LLM返回

根据知识库,阿尔忒弥斯2号任务计划在2025年执行。

四、完整代码

packagemainimport("fmt""math""sort""strings")// ==================== 数据定义 ====================// 知识库:关于阿尔忒弥斯2号的8句话varknowledgeBase=[]string{"阿尔忒弥斯2号是美国宇航局计划中的载人月球轨道飞行任务,标志着人类重返月球的重要一步","该任务将使用太空发射系统火箭和猎户座飞船,搭载宇航员进行绕月飞行","阿尔忒弥斯2号的主要目标是测试生命支持系统和宇航员在深空环境中的生存能力","任务计划在2025年执行,将是自阿波罗17号以来首次载人离开地球轨道的任务","此次飞行将为后续的阿尔忒弥斯3号月球着陆任务提供关键数据和技术验证","宇航员将在飞行过程中进行科学实验,并测试深空通信和导航系统","任务将持续约10天,飞行距离将达到27万英里,是地球到月球距离的10倍以上","阿尔忒弥斯2号的成功将为建立月球基地和未来火星探索奠定基础",}// 词汇表:我们关心的10个关键词varvocab=[]string{"阿尔忒弥斯","月球","任务","宇航员","火箭","飞船","测试","系统","飞行","轨道"}// 搜索结果typeSearchResultstruct{IDintContentstringSimilarityfloat64}// ==================== 向量化函数(复用第二篇)====================funccleanText(textstring)string{varcleaned strings.Builderfor_,r:=rangetext{ifr>=0x4e00&&r<=0x9fff{cleaned.WriteRune(r)}}returncleaned.String()}functokenize(textstring,vocab[]string)[]string{vartokens[]stringfor_,vocabWord:=rangevocab{count:=strings.Count(text,vocabWord)fori:=0;i<count;i++{tokens=append(tokens,vocabWord)}}returntokens}funccountFrequency(tokens[]string)map[string]int{freq:=make(map[string]int)for_,token:=rangetokens{freq[token]++}returnfreq}funcbuildVector(freqmap[string]int,vocab[]string)[]float64{vector:=make([]float64,len(vocab))fori,word:=rangevocab{vector[i]=float64(freq[word])}returnvector}funcnormalize(vector[]float64){sum:=0.0for_,v:=rangevector{sum+=v*v}ifsum==0{return}magnitude:=math.Sqrt(sum)fori:=rangevector{vector[i]/=magnitude}}functextToVector(textstring)[]float64{cleaned:=cleanText(text)tokens:=tokenize(cleaned,vocab)freq:=countFrequency(tokens)vector:=buildVector(freq,vocab)normalize(vector)returnvector}// ==================== 相似度函数(复用第三篇)====================funccosineSimilarity(a,b[]float64)float64{dotProduct:=0.0fori:=rangea{dotProduct+=a[i]*b[i]}lengthA:=0.0lengthB:=0.0fori:=rangea{lengthA+=a[i]*a[i]lengthB+=b[i]*b[i]}lengthA=math.Sqrt(lengthA)lengthB=math.Sqrt(lengthB)iflengthA==0||lengthB==0{return0}returndotProduct/(lengthA*lengthB)}// ==================== RAG核心函数 ====================// 搜索最相似的知识funcsearchKnowledge(querystring,topKint)[]SearchResult{queryVector:=textToVector(query)varresults[]SearchResultfori,content:=rangeknowledgeBase{contentVector:=textToVector(content)similarity:=cosineSimilarity(queryVector,contentVector)results=append(results,SearchResult{ID:i,Content:content,Similarity:similarity,})}sort.Slice(results,func(i,jint)bool{returnresults[i].Similarity>results[j].Similarity})iflen(results)>topK{results=results[:topK]}returnresults}// 组装PromptfuncbuildPrompt(querystring,results[]SearchResult)string{knowledge:=""fori,r:=rangeresults{knowledge+=fmt.Sprintf("%d. %s\n",i+1,r.Content)}prompt:=fmt.Sprintf(`你是一个智能客服,请根据以下知识库回答用户的问题。 知识库: %s 用户问题:%s 请简洁准确地回答:`,knowledge,query)returnprompt}// ==================== 主函数 ====================funcmain(){fmt.Println("========== RAG系统演示 ==========")fmt.Println()// 用户问题query:="月球任务什么时候发射"fmt.Printf("用户问题: %s\n",query)fmt.Println()// 第一步:向量化fmt.Println("【第一步:向量化】")queryVector:=textToVector(query)fmt.Printf("问题向量: %v\n",queryVector)fmt.Println()// 第二步:检索fmt.Println("【第二步:检索】")results:=searchKnowledge(query,3)fmt.Println("检索结果:")fori,r:=rangeresults{fmt.Printf(" 第%d名: 相似度=%.2f\n",i+1,r.Similarity)fmt.Printf(" 内容: %s\n",r.Content)fmt.Println()}// 第三步:组装Promptfmt.Println("【第三步:组装Prompt】")prompt:=buildPrompt(query,results)fmt.Println(prompt)fmt.Println()// 第四步:调用LLM(这里用模拟输出)fmt.Println("【第四步:调用LLM】")fmt.Println("LLM返回: 根据知识库,阿尔忒弥斯2号任务计划在2025年执行。")fmt.Println()fmt.Println("========== 演示结束 ==========")}

运行结果

========== RAG系统演示 ========== 用户问题: 月球任务什么时候发射 【第一步:向量化】 问题向量: [0 0.707 0.707 0 0 0 0 0 0 0] 【第二步:检索】 检索结果: 第1名: 相似度=0.71 内容: 任务计划在2025年执行,将是自阿波罗17号以来首次载人离开地球轨道的任务 第2名: 相似度=0.50 内容: 阿尔忒弥斯2号是美国宇航局计划中的载人月球轨道飞行任务,标志着人类重返月球的重要一步 第3名: 相似度=0.41 内容: 此次飞行将为后续的阿尔忒弥斯3号月球着陆任务提供关键数据和技术验证 【第三步:组装Prompt】 你是一个智能客服,请根据以下知识库回答用户的问题。 知识库: 1. 任务计划在2025年执行,将是自阿波罗17号以来首次载人离开地球轨道的任务 2. 阿尔忒弥斯2号是美国宇航局计划中的载人月球轨道飞行任务,标志着人类重返月球的重要一步 3. 此次飞行将为后续的阿尔忒弥斯3号月球着陆任务提供关键数据和技术验证 用户问题:月球任务什么时候发射? 请简洁准确地回答: 【第四步:调用LLM】 LLM返回: 根据知识库,阿尔忒弥斯2号任务计划在2025年执行。 ========== 演示结束 ==========

五、与第一篇方案的对比

5.1 第一篇:Prompt RAG

优点: - 简单,容易理解 - 适合小知识库 缺点: - 知识库大了,prompt就爆了 - 每次都要传所有知识,成本高

5.2 第五篇:向量检索RAG

优点: - 只传最相关的知识,prompt精简 - 知识库可以很大 - 检索速度快 缺点: - 实现稍复杂 - 需要理解向量化

5.3 用表格说清楚

特性Prompt RAG向量检索RAG
实现难度✅ 简单⚠️ 中等
知识库规模❌ 受限(几千字)✅ 不受限
Prompt长度❌ 长(全传)✅ 短(只传相关)
API成本❌ 高✅ 低
准确性✅ 全面✅ 智能

六、总结

6.1 RAG的四个步骤

1. 向量化:把文字变成数字 2. 检索:在知识库中找到最相似的内容 3. 组装Prompt:把检索结果塞进prompt 4. 调用LLM:让大模型生成答案

6.2 为什么这样做?

- 文字 → 向量:电脑不认识字,但认识数字 - 余弦相似度:判断"像不像",不受长度影响 - 只传相关的:prompt精简,成本降低

6.3 系列文章回顾

第一篇:Prompt RAG - 最简单的方式 第二篇:文本向量化 - 把文字变成数字 第三篇:余弦相似度 - 判断"像不像" 第四篇:向量数据库 - 存储和检索 第五篇:完整RAG系统 - 把所有知识串起来

后记

恭喜你读完了整个系列!

从第一篇的"把知识库塞进prompt",到第五篇的"智能检索",你已经理解了RAG的核心原理。

RAG不神秘,就是:

  1. 把文字变成数字
  2. 用数学方法找到最相似的内容
  3. 把相关内容给大模型,让它生成答案

下一步

  • 如果想深入了解,可以学习:TF-IDF、Word2Vec、BERT等更高级的向量化方法
  • 如果想实际应用,可以学习:Faiss、Milvus、Pinecone等向量数据库
  • 如果想构建产品,可以学习:LangChain、LlamaIndex等RAG框架

系列文章

  1. Go + Ollama 构建tinyRAG应用:Prompt提示工程
  2. 用阿尔忒弥斯2号讲透文本向量化
  3. 一眼看穿相似度:余弦相似度原理详解
  4. 手把手实现文本向量数据库
  5. 本文:从问题到答案:一个完整的RAG系统是如何工作的
http://www.cnnetsun.cn/news/4247038.html

相关文章:

  • 【TDengine】 如何将 Kafka 中的时序数据实时写入 TDengine?
  • 紧凑型电源调节器:如何兼顾FPGA、GPU与ASIC的供电需求
  • 【零基础速领】全套AI大模型入门指南(学习路线+PDF文档+全套视频+面试)
  • 免安装LabelImg工具:VOC/YOLO格式标注与计算机视觉数据集构建实战
  • NXP与Widex联手:助听器无线音频流技术深度解析
  • EZTools3.0如何切换简洁版和专业版
  • 解密prompt系列5. APE+SELF=自动化指令集构建代码实现
  • PSoC 6+Wi-Fi组合芯片:Cypress与Arrow的IoT开发平台实战解析
  • Java基础 - Maven的基础使用
  • 计算机单片机毕设实战-基于单片机的多级权限密码门锁与蓝牙远程开锁系统设计 基于 STM32 或 51 单片机的密码错误报警智能门禁系统设计(025804)
  • 跨境电商账号为什么会被关联?2026 年 6 大风险点排查
  • 鸿蒙开发入门:deviceConfig内部结构
  • 抖助手第077个开关:隐藏相关搜索的位置、验证方法与检索边界
  • 抖助手第111个开关:移除经验的位置、验证方法与未知顶栏边界
  • 技术立身,进阶Android,成为行业领跑者!
  • 程序员那么卷,就业那么难,为什么你还当一名程序员
  • 最新29刷网课平台系统源码+爱学习+搭建教程
  • FloodFill算法
  • 想降AI率不用愁?2026年这些免费AI工具助你高效写作
  • VecDB第十篇:除了HNSW,向量索引还有什么?——IVFFlat与PQ量化原理
  • RecyclerView实现混合布局
  • 【es报错】request [/xx] contains unrecognized parameter: [include_type_name]
  • Chunk标记功能说明
  • 整流器控制器反向保护设计:P-MOS防反接与比较器检测实战
  • 工业级Arm Mini-PC选型与开发实践:从加固设计到IIoT边缘部署
  • Linux 上设置 Nginx 开机自启
  • 【单片机课程设计/毕业设计】基于 STM32 单片机多传感器空气环境监控终端设计 基于 STM32 的室内 PM2.5 温湿度烟雾综合监测系统设计(010305)
  • 2026 年指纹浏览器深度横评:6 款主流产品防关联原理与性能对比
  • 公司注销在哪里登报?一篇讲透,少走弯路不花冤枉钱!
  • Java 大厂面试实录:Spring Boot + Kafka + Redis + Spring Security + AI 的电商直播中台实战问答