Skip to content

大模型训练完后,知识就不再更新了,它没法知道最新的一些信息,以及一些非互联网上公开的信息。所以对于它不知道的东西,会胡乱回答,也就是幻觉问题。解决这个问题的方式就是 RAG。RAG 是检索、增强、生成,会基于用户的 query 去检索知识库,拿到相关文档后放到 Prompt 里增强它,之后给大大模型来生成回答。检索肯定是要语义检索,但是关键词检索做不到这点,我们需要用向量来做,通过嵌入模型把知识向量化,这样就可以通过向量的余弦相似度(也就是夹角大小)来计算出两个知识的相关性,从而根据用户的 query 查询出相关的文档。我们基于 LangChain 写了 RAG 的代码:fromDocuments api 基于 embeddings 模型把文档向量化存入数据库。asRetriever 指定查询相似度最大的几个文档。similaritySearchWithScore 相似度评分retriever.invoke 来查询文档。只要你理解了 RAG 的流程,这些 api 自然也就会用了。

RAG:把文档向量化,实现语义搜索

大模型的知识来自训练数据,问它不知道的东西,它不会说不知道,而是胡编 — 这就是幻觉

解决思路很直觉:用户提问时,先从知识库里查出相关文档,塞到 prompt 里当背景知识,再让大模型回答。

这就是 RAG — Retrieval(检索)Augmented(增强)Generation(生成)。

核心问题:怎么查?

关键词搜索太弱,搜"东东和光光怎么成为朋友的",不可能靠关键词匹配到。需要的是语义搜索

做法是把文档转成向量(Embedding),用余弦相似度算相关性。

向量化(Embedding)

用嵌入模型把文本转成高维向量,语义相近的文本向量距离近、夹角小。

bash
pnpm install @langchain/classic
js
import { OpenAIEmbeddings } from '@langchain/openai';
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { Document } from '@langchain/core/documents';

// 嵌入模型(比大模型便宜很多)
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  model: process.env.EMBEDDINGS_MODEL_NAME,  // text-embedding-v3
  configuration: { baseURL: process.env.OPENAI_BASE_URL },
});

存入向量数据库

js
const documents = [
  new Document({
    pageContent: '光光是一个活泼开朗的小男孩,最喜欢踢足球...',
    metadata: { chapter: 1, character: '光光', type: '角色介绍' },
  }),
  new Document({
    pageContent: '东东是光光最好的朋友,安静而聪明,喜欢读书和画画...',
    metadata: { chapter: 2, character: '东东', type: '角色介绍' },
  }),
  // ...更多文档
];

// 文档向量化 → 存入内存向量数据库
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings);

每个文档转成向量时,元信息(来源、章节等)会一起保存。

检索 + 生成

js
// 创建检索器,返回最相似的 3 个文档
const retriever = vectorStore.asRetriever({ k: 3 });

const question = '东东和光光是怎么成为朋友的?';

// 语义检索
const docs = await retriever.invoke(question);

// 也可以拿到相似度分数
const scoredResults = await vectorStore.similaritySearchWithScore(question, 3);

// 把检索到的文档拼成上下文,增强 prompt
const context = docs.map((doc, i) => `[片段${i + 1}]\n${doc.pageContent}`).join('\n\n');

const prompt = `基于以下故事片段回答问题,如果没提到就说不知道。

故事片段:
${context}

问题: ${question}`;

const response = await model.invoke(prompt);

完整流程

文档 → 嵌入模型 → 向量化 → 存入向量数据库

用户提问 → 嵌入模型 → 向量化 → 语义检索 → 拿到相关文档

                                    塞进 prompt 当背景知识

                                        大模型生成回答

要点

  • 不是关键词匹配,是语义匹配 — 靠向量的余弦相似度(夹角大小)算相关性
  • 嵌入模型很便宜 — 专门做向量化的模型,比大模型便宜得多
  • metadata 要写好 — 向量的元信息记录了来源文档,方便追溯
  • k 值控制召回量asRetriever({ k: 3 }) 返回最相似的 3 个文档