为什么 RAG 一定离不开向量检索:从文档向量化到语义搜索的工程实现

很多刚接触大模型应用开发的同学,都会先遇到一个非常现实的问题:模型看起来很聪明,但一旦问题超出训练语料,或者涉及企业内部文档、最新业务规则、私有知识库,它就开始一本正经地胡说八道。

这不是模型"故意骗人",而是大语言模型的工作方式决定了它并不会主动承认"我不知道"。如果没有额外上下文,它只能基于参数里已有的统计规律去补全答案。工程上,这个问题通常被归为两个字:幻觉。

如果你要做一个真正可用的 AI 助手,第一步往往不是继续堆 Prompt,而是想办法把"模型不知道但业务需要它知道的知识"补进去。RAG 的价值就出现在这里。

RAG 不是一个神秘的新概念,它本质上是在回答前多做了一步检索:先从外部知识库里找资料,再把资料塞进 Prompt,让模型基于这些资料生成结果。这个思路并不复杂,难点在于另一个问题:

当用户的提问表达方式和文档原文不完全一致时,你怎么找到真正相关的那几段内容?

这篇文章就专门讲清楚这件事。我们从问题背景出发,先解释为什么关键词搜索不够用,再讲向量、Embedding、向量数据库和 RAG 之间的关系,最后用一个可运行的 LangChain demo,把"文档向量化 + 语义检索 + 回答生成"整条链路跑通。

RAG 解决的到底是什么问题

先把几个边界讲清楚。

大模型擅长的是"基于已有知识和上下文生成语言",但它并不天然具备以下能力:

  • 访问企业内部私有文档
  • 感知训练完成之后发生的新信息
  • 稳定遵循你业务系统里的事实约束

这意味着,只要问题依赖外部知识源,单纯调用 LLM 就不够。

RAG 的核心思想可以概括为三步:

  1. Retrieval:先从知识库里检索和问题最相关的内容。
  2. Augmented:把检索结果拼进 Prompt,作为额外上下文。
  3. Generation:让大模型基于这些上下文生成回答。

它的价值不是"让模型更聪明",而是"让模型回答时有依据"。对企业问答、文档助手、客服知识库、代码知识库、合同检索这类场景来说,这一点非常关键。

关键词搜索为什么不够

很多人第一次实现 RAG,会直觉地想到全文检索:把文档存起来,用户提问时按关键词查不就行了?

如果你的问题和文档用词高度一致,这么做确实能返回结果。但一旦用户换一种说法,关键词搜索就开始暴露局限。

比如文档里写的是:

员工离职后,账号权限会在 24 小时内回收。

用户提问时却可能写成:

人员离岗之后系统访问什么时候会被关闭?

这两个句子表达的是同一件事,但表面词汇几乎没重合。关键词检索擅长匹配"字面是否出现",却很难判断"语义是否接近"。而 RAG 真正需要的,恰恰是后者。

所以,RAG 的检索层如果只停留在关键词匹配,通常只能算是"能用",很难算"好用" 。真正想把检索质量做上去,基本都会走向语义检索,而语义检索背后的核心数据结构,就是向量。

什么是向量,为什么它能表示语义

"把文本转成向量"这句话初看很抽象,但可以先用一个低维例子建立直觉。

假设我们用两个维度描述一个对象:

  • 维度 1:可食用性
  • 维度 2:硬度

那几个概念大致可以被表示为:

  • 水果:[0.9, 0.3]
  • 苹果:[0.9, 0.5]
  • 香蕉:[0.9, 0.1]
  • 石头:[0.1, 0.9]

虽然这只是人为构造的二维示意,但它很好地说明了向量表示的本质:一个对象不再靠单个词描述,而是靠一组数值来表达它在多个语义维度上的位置。

在这个空间里,苹果、香蕉、水果会更靠近,因为它们在"语义属性"上更相似;石头则会离得很远。

工程上,我们不会自己手工定义这些维度,而是交给专门的模型去学习。这个模型就是 Embedding Model,也叫嵌入模型。

Embedding 模型和 LLM 有什么区别

这两个概念很容易被混在一起,但职责其实完全不同。

  • LLM 负责理解上下文并生成回答
  • Embedding Model 负责把文本映射成向量

Embedding 模型不负责长篇生成,它的任务是把"语义相近的内容"映射到"向量空间中距离更近的位置"。这样一来,你就可以把文档转成向量存起来,也可以把用户问题转成向量,再去做相似度匹配。

这也是为什么一个完整的 RAG 系统里,通常至少会出现两类模型:

  • 生成模型:负责最后的自然语言回答
  • 嵌入模型:负责检索阶段的向量化

很多初学者会误以为"有了大模型就不需要 Embedding 了"。这在工程上通常不成立。没有 Embedding,你很难把海量文档组织成一个可搜索的语义空间。

语义搜索是怎么做出来的

当文档和用户问题都被转成向量之后,检索问题就变成了一个数学问题:找出和查询向量最接近的文档向量。

常见做法是使用余弦相似度、内积或欧氏距离来衡量两个向量的接近程度。初学阶段你只需要记住一个结论:

向量越接近,通常意味着语义越相近。

于是,一条完整的检索链路就出现了:

  1. 先把文档切分成若干片段。
  2. 用 Embedding 模型把每个片段转成向量。
  3. 把向量和对应的原文、元数据一起存进向量数据库。
  4. 用户提问时,再把问题转成向量。
  5. 在向量库里查找最相近的若干片段。
  6. 把这些片段作为上下文交给 LLM 生成回答。

这才是现代 RAG 的典型结构。

这里有一个经常被忽略但很重要的点:向量数据库存的不只是向量,还会存向量对应的原文和元信息。 否则你只能算出"第 183 个向量最相近",却不知道它对应哪段文档。

一个更符合真实业务的 demo

我们假设知识库里有三类文档:

  • 报销制度
  • VPN 访问规范
  • 年假与请假规则

用户的问题则可能是:

新员工在家办公时,怎么申请远程访问公司的内部系统?

这个问题未必直接出现过"VPN"这个关键词,但语义上显然和远程访问规范强相关。正好适合用来演示向量检索的价值。

环境准备

先初始化一个简单项目:

bash 复制代码
mkdir rag-test
cd rag-test
npm init -y
pnpm add @langchain/core @langchain/openai @langchain/classic dotenv

这个 demo 里我们用到的依赖分别负责不同角色:

  • @langchain/openai:封装聊天模型和嵌入模型
  • @langchain/core:提供 Document 等基础抽象
  • @langchain/classic:提供内存版向量存储,适合教学和本地实验
  • dotenv:从 .env 读取配置

这里特意强调一下:MemoryVectorStore 只是一个方便演示的内存向量库,适合理解流程,不适合直接拿去做生产级知识库。真实场景里,你通常会换成 Pinecone、Milvus、Qdrant、Weaviate、pgvector 之类的持久化方案。

配置模型

在项目根目录创建 .env

ini 复制代码
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL_NAME=qwen-plus
EMBEDDINGS_MODEL_NAME=text-embedding-v3

这组配置说明了一个很重要的工程事实:RAG 并不绑定某一家模型厂商。 只要对方提供兼容接口,你完全可以替换成别的聊天模型和嵌入模型。

核心代码:把知识库变成可检索的向量空间

创建 src/hello-rag.mjs

ini 复制代码
import "dotenv/config";
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { Document } from "@langchain/core/documents";
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
​
const model = new ChatOpenAI({
  model: process.env.MODEL_NAME,
  temperature: 0,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});
​
const embeddings = new OpenAIEmbeddings({
  model: process.env.EMBEDDINGS_MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL,
  },
});
​
const documents = [
  new Document({
    pageContent:
      "公司员工如需在家办公访问内部系统,需要先申请 VPN 权限。审批通过后,IT 会分配专属账号,并要求开启双因素认证。",
    metadata: {
      category: "远程办公",
      source: "IT 访问规范",
    },
  }),
  new Document({
    pageContent:
      "报销单需要在费用发生后的 15 个自然日内提交。超过时限的报销申请,需要部门负责人补充说明原因。",
    metadata: {
      category: "财务制度",
      source: "员工报销手册",
    },
  }),
  new Document({
    pageContent:
      "入职满一年后,员工每年可享受 10 天年假。请假需要至少提前 3 个工作日提交,并经过直属主管审批。",
    metadata: {
      category: "人事制度",
      source: "员工假期管理办法",
    },
  }),
  new Document({
    pageContent:
      "首次使用 VPN 时,员工需要在公司安全门户下载客户端,并绑定动态验证码设备。未开启双因素认证的账号无法访问内网。",
    metadata: {
      category: "远程办公",
      source: "VPN 使用说明",
    },
  }),
];
​
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings);
const retriever = vectorStore.asRetriever({ k: 2 });
​
const question = "新员工居家办公时,怎么申请访问公司内部系统?";
​
const retrievedDocs = await retriever.invoke(question);
const scoredDocs = await vectorStore.similaritySearchWithScore(question, 2);
​
console.log("问题:", question);
console.log("\n检索结果:");
​
retrievedDocs.forEach((doc, index) => {
  const matched = scoredDocs.find(
    ([candidate]) => candidate.pageContent === doc.pageContent,
  );
​
  const score = matched?.[1];
  console.log(`\n[片段 ${index + 1}]`);
  console.log("来源:", doc.metadata.source);
  console.log("分类:", doc.metadata.category);
  console.log("分数:", score);
  console.log("内容:", doc.pageContent);
});
​
const context = retrievedDocs
  .map((doc, index) => `[资料 ${index + 1}]\n${doc.pageContent}`)
  .join("\n\n");
​
const prompt = `
你是一名企业内部知识库助手。请严格基于给定资料回答问题:
1. 优先使用资料中的事实作答
2. 如果资料没有明确提到,就直接说明"当前资料不足以回答这个细节"
3. 回答尽量简洁、准确、可执行
​
资料:
${context}
​
问题:
${question}
`;
​
const response = await model.invoke(prompt);
​
console.log("\nAI 回答:");
console.log(response.content);

这段代码虽然不长,但已经把一个最小可用的 RAG 流程串起来了。下面按职责拆开讲。

第一步:准备两类模型

代码里同时初始化了 ChatOpenAIOpenAIEmbeddings。这不是重复,而是两个阶段的明确分工。

  • ChatOpenAI 用于最终回答生成
  • OpenAIEmbeddings 用于把文档和问题转成向量

如果你把这两个角色混在一起理解,后面写 RAG 时就容易搞不清"到底是谁在检索、谁在生成"。

第二步:构造文档和元数据

Document 里最核心的字段有两个:

  • pageContent:真正参与向量化和后续拼接上下文的文本内容
  • metadata:附加信息,比如来源、分类、时间、章节、作者

为什么元数据重要?因为 RAG 在工程上不仅要"能答",还要"可追溯"。当模型引用一段知识时,你最好能告诉用户这段内容来自哪份文档、哪个知识分类,甚至哪个时间版本。

换句话说,元数据不只是锦上添花,而是后续做来源展示、权限控制、结果过滤的重要基础。

第三步:把文档向量化并存入向量库

这一行是整个 demo 的核心:

ini 复制代码
const vectorStore = await MemoryVectorStore.fromDocuments(documents, embeddings);

它做了两件事:

  1. 调用嵌入模型,把每个文档转成向量。
  2. 把向量、原文和元数据一起放进向量存储。

很多教程把这一步说成"把文档存进数据库",但更准确的说法应该是:把文档映射成可参与相似度检索的向量表示,并建立从向量到原始文档的关联。

这一步完成之后,知识库才真正从"文本集合"变成"可做语义搜索的数据结构"。

第四步:把向量库包装成 Retriever

ini 复制代码
const retriever = vectorStore.asRetriever({ k: 2 });

Retriever 可以理解成检索接口。这里的 k: 2 表示每次查询返回最相关的两个文档片段。

为什么不是越多越好?因为召回太少,模型可能拿不到足够信息;召回太多,又会把无关文本一起塞进 Prompt,导致噪声变大、成本上升、回答质量下降。

RAG 的一个常见优化方向,恰恰就是在"召回数量、相关性、上下文长度、模型成本"之间做平衡。

第五步:先检索,再生成

这一步是 RAG 和"直接问模型"最本质的区别。

ini 复制代码
const retrievedDocs = await retriever.invoke(question);

这里不是让模型直接回答,而是先根据问题去找相关资料。拿到资料后,我们再显式构造 Prompt,把检索结果喂给模型。

javascript 复制代码
const context = retrievedDocs
  .map((doc, index) => `[资料 ${index + 1}]\n${doc.pageContent}`)
  .join("\n\n");

这一段看似只是字符串拼接,实际上体现了一个非常重要的工程原则:RAG 的增强不是抽象概念,而是把检索到的文本以结构化方式注入 Prompt。

如果没有这一步,模型依然只能依赖自身参数作答;有了这一步,它才真正拥有"外部知识上下文"。

第六步:让模型在"有依据"的前提下回答

Prompt 里我故意加了三条限制:

  • 优先使用资料中的事实
  • 如果资料不足,就明确说不知道
  • 回答要简洁、准确、可执行

这不是装饰,而是在控制生成阶段的行为边界。很多人做 RAG 只关注检索命中率,却忽略了生成约束,结果就是检索明明找对了,模型还是会自由发挥。

一个好用的 RAG 系统,本质上是"检索质量"和"回答约束"共同作用的结果。

运行后你会看到什么

如果向量检索工作正常,返回结果应该会优先命中与"远程办公""VPN""内部系统访问"相关的两段文档,而不是报销制度或年假规则。

最终回答通常会类似这样:

复制代码
员工在家办公访问内部系统时,需要先申请 VPN 权限。审批通过后,IT 会分配专属账号。首次使用时,还需要下载 VPN 客户端并绑定动态验证码设备,同时开启双因素认证,否则无法访问内网。

这时候你就能直观看到 RAG 的价值了。

如果不做检索,模型大概率会给出一段"听起来合理但未必符合公司真实制度"的泛化回答;而做了检索之后,它回答的依据来自你提供的知识库。

这个 demo 还不是真正的生产级 RAG

到这里流程已经跑通了,但如果你要做真实系统,还需要继续往下补。

最常见的几个增强方向包括:

  • 文档切分:长文档不能整篇直接做 Embedding,通常要按段落或 token 长度切块
  • 向量库替换:内存库适合教学,不适合多用户和持久化场景
  • 检索优化:加入元数据过滤、混合检索、重排序模型
  • Prompt 优化:控制引用格式、回答风格、拒答策略
  • 评估体系:用命中率、答案准确率、上下文利用率等指标验证效果

很多团队以为"把文档扔进向量库"就等于做完 RAG,这通常只是起点。真正影响效果的,往往是切块策略、召回质量和生成约束。

初学者最容易混淆的几个点

1. RAG 不等于向量数据库

向量数据库只是 RAG 检索层的一种常见基础设施。RAG 是一条完整链路,至少包含检索、上下文增强和生成三个环节。

2. Embedding 不负责回答问题

Embedding 模型负责把内容映射到向量空间,方便做语义检索;真正负责输出自然语言答案的,仍然是 LLM。

3. 检索结果相关,不代表答案一定正确

如果 Prompt 约束不严,或者模型理解上下文时发生偏差,最终答案依然可能不稳定。所以 RAG 不是"消灭幻觉",而是"显著降低无依据回答的概率"。

4. 关键词搜索不会完全消失

很多真实系统会把关键词检索和向量检索结合起来做混合检索。原因很简单:精确术语、产品型号、错误码、法规编号,往往还是关键词更稳。

从工程视角重新理解这件事

如果只看概念,RAG 很像是在 Prompt 前面多拼了一段文本;但从工程实现的角度看,它其实是在给大模型外挂一套"可更新、可追溯、可控的外部知识系统"。

这件事的意义非常大,因为它让大模型应用第一次真正具备了和业务知识库对接的能力。

你可以把它理解成一种分工:

  • LLM 负责语言理解与生成
  • Embedding 负责语义表示
  • 向量库负责近邻检索
  • Retriever 负责召回相关片段
  • Prompt 负责把外部知识注入生成链路

把这几个角色分开理解之后,RAG 的整体结构就会清晰很多。

总结

RAG 要解决的问题,从来不是"怎么让模型回答更多",而是"怎么让模型基于真实资料回答"。

而一旦你想让检索从"字面匹配"升级到"语义匹配",向量化几乎就是绕不过去的一步。Embedding 模型负责把文本放进向量空间,向量数据库负责在这个空间里做相似度搜索,Retriever 负责把最相关的片段找回来,最后再由 LLM 基于这些片段生成答案。

所以,文档向量化不是 RAG 的附属技巧,而是高质量语义检索的核心基础设施。

如果你接下来要做企业内部文档助手、客服知识库问答、研发文档检索、合规制度问答,RAG 往往就是第一条值得认真打磨的技术路线。理解了"为什么要向量化"和"检索结果是怎么参与生成的",后面的 chunking、rerank、hybrid search、agentic retrieval 才有继续深入的基础。

相关推荐
乘风gg3 小时前
为什么AI 时代来临,大部分人吃不到红利
前端·ai编程·claude
恋猫de小郭3 小时前
Android 限制侧载新进展,谷歌联合国内厂商推验证计划
android·前端·flutter
IT_陈寒3 小时前
Redis内存爆了,原来我漏掉了这个致命配置
前端·人工智能·后端
恋猫de小郭3 小时前
解读 Android 17 全新内存限制,有没有“豁免”后门?
android·前端·flutter
Hyyy5 小时前
理解LLM的基本工作原理:预训练、微调、推理的区别
前端
Gatlin5 小时前
前端逆向与反逆向:一场猫鼠游戏的底层逻辑与实战
前端
Pedantic5 小时前
本地通知(Local Notifications)学习笔记
前端
米小虾5 小时前
手把手教你搭建第一个生产级AI Agent:从选型到实战的完整指南
人工智能·agent
米小虾6 小时前
2026年AI Agent全面爆发:从开源生态到企业级应用的进化之路
人工智能·agent
森蓝情丶6 小时前
我给 AI 搭了个法庭:一个前端仔的 LangGraph 实战全记录
前端·后端