ai agent - RAG 汇总

69 阅读6分钟

一. RAG 是什么?

RAG = Retrieval(检索)+ Augmented(增强)+ Generation(生成)

image.png RAG技术实现流程

image.png

二. RAG 的作用

image.png

三. RAG 的好处

image.png

四.RAG 的完整工作流程(核心知识点)

image.png

这张图把之前那段纯文本流程拆成了两个清晰的区域:

  • 上半区(蓝色 · 离线构建 Indexing) :文档准备 → 切片 → 向量化 → 存入向量库。这是在离线状态下提前一次性做好的工作,把你的资料变成可检索的向量。
  • 下半区(绿色 · 在线问答 Query) :用户提问 → 去向量库检索相关片段 → 组装成 Prompt → 大模型生成回答。这是每次用户提问时实时走的通路。
  • 虚线箭头表示"检索"这一步要去查上半区已经建好的向量库
  • 底部汇总了 RAG 的五大核心价值: 减少幻觉,接入私有文档,知识实时更新,可溯源引用,成本低。

在每一个步骤下面都有具体的api如下:

环节技术选型(图中已标注)
文档准备PyPDFLoader / WebBaseLoader / UnstructuredLoader
切片RecursiveCharacterTextSplitter(按语义边界切,500字/段+重叠50字)
向量化OpenAIEmbeddings + text-embedding-v3(或 bge-large-zh
存储FAISS(本地)/ Chroma / Milvus / Pinecone / Qdrant
检索similarity_search + MMR / Rerank
Prompt 组装ChatPromptTemplate(System + Context + Question)
生成ChatOpenAI(可接 qwen-plus / gpt-4o
进阶能力一句话作用
Rerank 重排序向量粗筛 20 条 → 精排模型挑最准的 3 条,效果提升巨大
HyDE先让 LLM 假设一个回答,用"假设回答"去检索,比原问题更准
Query Rewriting把口语化问题改写成更适合检索的查询,多轮可拆子问题
Hybrid Search向量搜索 + BM25 关键词结合,召回更全
GraphRAG建知识图谱,适合复杂关系推理(微软开源)
Agentic RAGAgent 自主决定何时检索、检索什么,可多轮追问补全

api汇总:

image.png

五.使用案例

1.基础案例

准备嵌入模型

image.png 在下面代码里面,文档我们是我们定义好的,跑一下基础版的RAG流程。 代码如下

import 'dotenv/config';
import {ChatOpenAI, OpenAIEmbeddings} from '@langchain/openai';
import {Document} from '@langchain/core/documents';
import {MemoryVectorStore} from '@langchain/classic/vectorstores/memory';

//OpenAIEmbeddings 是嵌入模型

const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  temperature: 0,
  apiKey: process.env.OPENAI_API_KEY,
  confuguration:{
    baseURL: process.env.OPENAI_BASE_UR,
  }
});

//创建一个嵌入模型,用于将文档转换为向量
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDINGS_MODEL_NAME,
  confuguration:{
    baseURL: process.env.OPENAI_BASE_UR,
  }
});

const docments =[
  new Document({
    pageContent: '明明是一个活泼可爱的小朋友,他喜欢踢足球和唱歌。他总是充满活力和好奇心,喜欢探索新事物和新朋友。他今年9岁,就读于双塔小学,学习成绩也很好。',
    metadata: {
      chapter:1,
      character:'明明',
      type:"角色介绍",
      mood:'活泼可爱'
    }
  }),
  new Document({
    pageContent: `在遥远的“赛博山谷”里,住着一位年轻的魔法师,名叫小链(LangChain)。小链有一个烦恼:虽然山谷里的智慧老人(大模型 LLM)学识渊博,但只要问题稍微涉及山谷外的新鲜事(私有知识/PDF文档),老人就会因为“知识未更新”而胡编乱造(幻觉)。
为了解决这个问题,小链决定踏上“RAG(检索增强生成)”的修行之旅。
有一天,小链在山洞里发现了一本古老的秘籍(PDF文档)。但这本秘籍太厚了,老人一次性根本看不完。于是,小链请来了裁缝师傅(Text Splitter 文本切片器),把秘籍裁成了一页页的小纸条(Documents 文档片段),方便后续处理。`,
    metadata: {
      chapter:1,
      character:'花花和明明',
      type:"尾声",
      mood:'温馨'
    }
  }),
    new Document({
    pageContent: `接着,小链找到了三位聪明的“翻译官”——它们就是Embeddings(向量化模型,比如 text-embedding-v3)。小链把每一张小纸条都交给翻译官,翻译官唰唰几笔,把文字变成了一串串神奇的数字密码(向量 Vector)。
小链把这些数字密码和对应的原文纸条,统统存进了一个神奇的“记忆宝盒”里(MemoryVectorStore 内存向量库)。这个宝盒就住在小链的随身背包里,虽然进程一重启就会清空(易失性),但在日常修炼中非常方便快捷。`,
    metadata: {
      chapter:1,
      character:'花花和明明',
      type:"尾声",
      mood:'温馨'
    }
  }),
  new Document({
    pageContent: '花花是一个安静聪明的小朋友,他喜欢看书和画画。他总是很安静地待在角落里面读书画画,仿佛外面的世界和他没有任何关系。他今年10岁,就读于双塔小学,学习成绩也很好。',
    metadata: {
      chapter:1,
      character:'花花',
      type:"角色介绍",
      mood:'安静聪明'
    }
  }),
  new Document({
    pageContent: '今年秋天,学校秋季运动会如期举行,明明和花花都参加了运动会。在接力赛中,他们俩配合得非常默契,最终获得了第一名。从此他们就开始成为朋友了。每当明明遇到困难的时候,花花都会第一次站出来,帮助他解决困难。每当花花遇到不开心的事情,明明都会第一个站出来,鼓励他。他们俩就这样成为了最好的朋友。',
    metadata: {
      chapter:1,
      character:'花花和明明',
      type:"友情介绍",
      mood:'鼓励'
    }
  }),
    new Document({
    pageContent: '多年后,明明成了一名足球运动员,花花成了一名画家。他们俩经常一起出去玩,一起回忆小时候的美好时光。',
    metadata: {
      chapter:1,
      character:'花花和明明',
      type:"尾声",
      mood:'温馨'
    }
  }),
]

//创建一个内存向量库,并把文档向量化存入数据库。
const vectorStore = await MemoryVectorStore.fromDocuments(docments,embeddings);

//创建一个检索器,用于从向量库中检索文档片段。
const retriever = vectorStore.asRetriever({k:3});

//创建一个问题数组,用于测试检索器。
const questions = ['花花和明明的友情是如何开始的?'];

for(const question of questions){
  //检索文档片段
  const retrivedDocs = await retriever.invoke(question);
  
  //查询文档相似度
  // const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);
  
  // retrivedDocs.forEach((doc, i)=>{
  //   const scordedResult = scoredResults.find(([sciredDoc])=>{
  //     sciredDoc.pageContent === doc.pageContent
  //   })
    
  //   const score = scordedResult?.[1] || null;
  //   const similarity = score !== null ? (1-score).toFixed() : 'N/A';
  //   console.log(`文档数:${i+1}. ${doc.pageContent} (相似度: ${similarity})`);
  // })
  
  //创建一个故事片段,用于生成故事的回答。
  const context = retrivedDocs.map((doc)=> doc.pageContent).join('\n');
  //创建一个提示,用于生成故事的回答。
  const prompt=`你是一个会讲故事的老师, 基于上面故事片段,你帮我用温情的语言,重新讲一遍这个故事。如果故事中没有提到,就说故事没有提到这个细节。
     故事片段:${context}
     问题:${question}
     老师的回答:
  `;
  
  const response = await model.invoke(prompt);
  console.log(response.content, 89999);
}

这个案例完美地贯穿了下图 image.png

  • 文档准备是:documents,里面有很多个文件片段。
  • 切片:我们没有切,直接给的文档碎片。
  • 向量化及向量存储:
 const vectorStore = await -MemoryVectorStore.fromDocuments(docments,embeddings);
  • 用户问题:
const questions = ['花花和明明的友情是如何开始的?'];
  • 检查retrival:
  const retriever = vectorStore.asRetriever({k:3});
  const retrivedDocs = await retriever.invoke(question);

组装prompt:

const context = retrivedDocs.map((doc)=> doc.pageContent).join('\n');
  //创建一个提示,用于生成故事的回答。
  const prompt=`你是一个会讲故事的老师, 基于上面故事片段,你帮我用温情的语言,重新讲一遍这个故事。如果故事中没有提到,就说故事没有提到这个细节。
     故事片段:${context}
     问题:${question}
     老师的回答:
  `;

大模型处理prompt:

 const response = await model.invoke(prompt);

2.进阶案例

显示生活中,我们的文档都是以word,pdf,youtube,url等形式存在的,所以我就必须用这些文件类型对应的loader去加载文件,然后添加到嵌入模型里面去。

image.png

说白了就是这些loader会将对应的文件切割成document碎片,然后丢给嵌入模型处理。 loader的官方地址:docs.langchain.com/oss/python/…

image.png 此时如果文件太大,就需要Splitter和Chunk切割,一般常用的切割api是:

import {RecursiveCharacterTextSplitter} from '@langchain/textsplitters';

上代码: 如果我要用DocxLoader切割本地word文件,然后将切割好的文件存入内存向量数据MemoryVectorStore,然后将你的问题包装到prompt里面给大模型使用:

import 'dotenv/config';
import 'cheerio';//运行在后端的jquery,让你在后端轻松遍历,操作,渲染html,一般使用场景爬虫,html处理,seo优化测试
import { DocxLoader } from "@langchain/community/document_loaders/fs/docx";
//需要安装pnpm add @langchain/community @langchain/core mammoth,mammoth是处理word文档的库 
import {RecursiveCharacterTextSplitter} from '@langchain/textsplitters';
import {ChatOpenAI, OpenAIEmbeddings} from '@langchain/openai';
import {MemoryVectorStore} from '@langchain/classic/vectorstores/memory';//本地内存向量数据库

const model = new ChatOpenAI({
   temperature: 0,
    model: process.env.MODEL_NAME,
    apiKey: process.env.OPENAI_API_KEY,
    configuration: {
      baseURL: process.env.OPENAI_API_BASE_URL,
    }
});

const embeddings = new OpenAIEmbeddings({
  model: process.env.EMBEDDINGS_MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_API_BASE_URL,
  }
});

const loader = new DocxLoader("src/day-006/word-test.docx");
const docs = await loader.load();

console.log(docs)

//万能分割器
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 400, //每个分块在400个字符左右,重叠100个字符。
  chunkOverlap: 100,//分块重叠100个字符。
  separators:["。","!","?",]//分块分隔符,默认为换行符。

});

const splitDocs = await textSplitter.splitDocuments(docs);
console.log("分割后的文档",splitDocs);

const vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, embeddings);

const retriever = vectorStore.asRetriever({k:2});

const question =['父亲的去世对作者的人生态度产生了什么样的影响?']

for(const q of question){
  const retrievedDoc = await retriever.invoke(q);
  const context = retrievedDoc.map((doc, i)=>`[片段${i}]\n${doc.pageContent}\n`).join("\n\n----\n\n");
  
  const prompt = `你是一个文件助手,你需要根据提供的文件内容回答问题。
  
  文件内容:
  ${context}
  
  问题:
  ${q}
  
  你的回答:
  `;
  const response = await model.invoke(prompt);
  console.log(response.content);
  console.log('结束')

}

src/day-006/word-test.docx这个文档里面写上你的文件内容即可。 不管你读的是word,pdf,还是网页,都用对应的loader就好了,其他代码都是公用的。 例如你要读取网页的数据就用下面这个

image.png

3.向量数据库

上面的例子里面我们的数据都是存在内存的向量数据库里面的,现在要介绍的向量数据库是:Milvus

1.mysql和milvus的区别

image.png

image.png

image.png

image.png

也就是说在一个agent项目里面mysql和milvus是共存的,是相互协作的关系,不是替换的关系。

2.安装milvus

  1. 安装docker,www.docker.com 安装后执行docker命令,不报错说明成功。
  2. 打开docker客户端,创建一个文件夹milvus,将文件夹配置到docker里面--相当于告诉docker,接下来向量数据要存到这里。
  3. 下载milvus,github.com/milvus-io/m… 找到./milvus-standalone-docker-compose.yml 文件。
  4. 跑命令: docker compose -f ./milvus-standalone-docker-compose.yml up -d 这个命令会自动下载milvus。
  5. 跑起来以后,docker如下:

image.png

  1. 安装milvus的数据可视化工具(GUI):attu 下载地址:github.com/zilliztech/…
  2. 用连接mysql的方式连接就好了

3.node代码使用milvus

import "dotenv/config";
import {MilvusClient, MetricType } from '@zilliz/milvus-sdk-node';
import { OpenAIEmbeddings } from "@langchain/openai";

const COLLECTION_NAME = "langchain_collection";
const VECTOR_DIM = 1024;

const embeddeings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  mode: process.env.EMBEDDING_MODE,
  configuration: {
    baseURL: process.env.EMBEDDING_BASE_URL,
  },
  dimension: VECTOR_DIM,
});

const client = new MilvusClient({
  address: 'localhost:19530',
});


async function main(){
  await client.connect();
  const query="我想看看关于户外活动的日记"
  const queryVector = await embeddeings.embedQuery(query);
  
  const searchResult = await client.search({
    collection_name: COLLECTION_NAME,
    vector: queryVector,
    limit: 5,
    output_fields: ['content'],
  });
  
  searchResult.result.forEach(item => {
    console.log(`${item.score}: ${item.content}`);
  });
}

main();

4.结合大模型使用

import "dotenv/config";
import {MilvusClient, MetricType } from '@zilliz/milvus-sdk-node';
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";

const COLLECTION_NAME = "langchain_collection";
const VECTOR_DIM = 1024;

const model = new ChatOpenAI({
  temperature: 0.7,
  model: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.CHAT_BASE_URL,
  },
});
  temperature: 0,
});

const embeddeings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  mode: process.env.EMBEDDING_MODE,
  configuration: {
    baseURL: process.env.EMBEDDING_BASE_URL,
  },
  dimension: VECTOR_DIM,
});

const client = new MilvusClient({
  address: 'localhost:19530',
});

//从Milvus中检索向量数据
async function retrieveRelevantDoc(query, k=2){
  const queryVector = await embeddeings.embedQuery(query);
  
  const searchResult = await client.search({
    collection_name: COLLECTION_NAME,
    vector: queryVector,
    limit: k,
    metric_type: MetricType.COSINE,
    output_fields: ['id','content'],
  });
  
  return searchResult.result
}

async function answerQuestion(query, k=2){
  const relevantDocs = await retrieveRelevantDoc(query, k);
  const context = relevantDocs.map(doc => doc.content).join('\n');
  const prompt = `你是一个贴心的日记助手,基于用户的日记问题用亲切的语言回答下面的问题。
  请根据一下日记内容:
  ${context}
  用户问题:${query}
  
  回答要求:
  1. 回答要亲切,多用口语化表达。
  2. 避免使用专业术语。
  
  AI助手的回答:
  `
  const response = await model.invoke(prompt)
  return response.content;
}

function main(){ 
  await client.connectPromise;
  await answerQuestion('什么能要我更快乐一些?',2)
}

main()

milvus向量数据库里面有query,insert,update,delete的方法。 存储的时候可以先用Split将文档切割成chunk然后存入milvus,然后在query的时候就不用做切割处理了。

六.总结

GAG解决的主要问题就是大模型幻觉,就是他明明不知道,一本正经地胡说八道。 解决方案就是:RAG根据prompt提示,去知识库里面搜索文档,将相似度极高的部分添加到大模型的知识背景里面来,大模型依据新的知识库给你回答问题。