RAG 实战:从原理、文本切割到 Milvus 落地

从 RAG 原理讲起,覆盖文档加载、三类文本分割器对比、Milvus 与 MySQL 双库架构,并用 Node.js 串起加载、切分、入库与检索的最小工程链路。

落地大模型应用时,常见两个问题:知识滞后幻觉

预训练模型的知识是静态的:训练截止之后的新信息、企业内部文档、业务专属资料,它都感知不到。遇到未知内容,模型只能按概率补全,写出通顺却不一定正确的答案。

RAG(Retrieval-Augmented Generation,检索增强生成) 是目前缓解幻觉、做私有知识问答的主流方案,也是企业知识库与 Agent 记忆的常见底座。

本文顺着完整链路往下拆:原理 → 文档加载与切分 → Milvus 工程落地 → 最小可运行串联。

什么是 RAG

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

核心想法很直接:先检索专属知识库,再让模型基于真实资料作答,而不是凭空生成。

  • 传统问答:用户提问 → 模型靠训练数据生成
  • RAG 问答:用户提问 → 检索私有库 → 资料拼进提示词 → 模型基于资料作答

完整工作链路

整套流程分两段:离线构建知识库,以及在线问答推理。底层依赖向量语义检索,而不是传统关键词匹配。

flowchart LR
  subgraph offline["离线阶段:知识库构建"]
    A["原始文档 PDF / Word / 网页 / 笔记"] --> B["Loader 解析为标准 Document"]
    B --> C["Splitter 切成 Chunk"]
    C --> D["Embedding 文本转向量"]
    D --> E["Milvus 存储向量与业务 ID"]
  end
  subgraph online["在线阶段:用户问答"]
    F["用户提问 Query"] --> G["Embedding 生成查询向量"]
    G --> H["Milvus 相似度召回 Top-N Chunk"]
    H --> I["过滤与重排"]
    I --> J["Prompt:问题 + 参考资料"]
    J --> K["LLM 基于资料作答"]
    K --> L["返回答案"]
  end

要点可以收成三步:

  1. 知识库预处理:加载文档、切分、向量化、入库
  2. 问答检索:问题同样向量化,按相似度召回相关 Chunk
  3. 增强生成:问题 + 检索资料拼进 Prompt,再交给模型

为什么比关键词检索更合适

关键词检索只能盯字面。搜「大模型如何避免出错」,未必能命中「大模型幻觉的解决方案」。

RAG 靠 Embedding 做语义检索:

  • 文本与问题都变成高维向量
  • 用余弦相似度衡量语义接近程度
  • 字面不同、意思相近的内容也能对上

文档加载与文本预处理

RAG 效果很大一部分取决于预处理。文档太长、语义混杂时,直接整篇向量化,检索会糊成一团。常用两块:LoaderSplitter

Loader:多源文档统一接入

Loader 负责把不同来源、不同格式的数据解析成统一的 Document(文本 + 元数据),方便后续切分与向量化。LangChain 生态里有大量开源 Loader,集中在 @langchain/community

Loader 类型 支持数据源 核心能力 适用场景
PDFLoader 本地 PDF 解析正文、保留段落、提取页码等元数据 手册、制度文档、电子书
DocxLoader Word(.docx) 解析正文与段落结构 报告、方案、办公文档
WebBaseLoader 公开网页 抓取正文,尽量去掉广告与导航 资讯、博客类知识库
MarkdownLoader Markdown 保留标题、代码块、列表结构 技术文档、项目手册
TextLoader 纯文本 .txt 快速读全文 日志、简易笔记
CSVLoader CSV 按行把表格转成文本 Chunk 参数表、台账问答
NotionLoader Notion 经 API 同步在线文档 团队协作知识库
GitHubLoader GitHub 仓库 解析代码与 README 代码 / 开源项目问答

设计目标一致:屏蔽数据源差异,输出标准 Document,后面的切分、向量化、检索不用再为格式单独适配。

安装依赖:

npm install @langchain/community @langchain/core

TextLoader

import { TextLoader } from "@langchain/community/document_loaders/fs/text";

const loader = new TextLoader("./test.txt");
const docs = await loader.load();
console.log("TXT文档内容:", docs);

MarkdownLoader

import { MarkdownLoader } from "@langchain/community/document_loaders/fs/markdown";

const loader = new MarkdownLoader("./doc.md");
const docs = await loader.load();
console.log("MD文档内容:", docs);

PDFLoader

import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";

const loader = new PDFLoader("./book.pdf");
const docs = await loader.load();
console.log("PDF文档页数:", docs.length);

WebBaseLoader

import { WebBaseLoader } from "@langchain/community/document_loaders/web/web";

const loader = new WebBaseLoader("https://xxx-blog.com/article");
const docs = await loader.load();
console.log("网页正文内容:", docs);

Splitter:文本切割

长文本信息混杂,必须切成合适大小的 Chunk。Chunk 太大,噪声多、命中不准;太小,上下文碎、模型难理解。

多数通用业务场景,优先考虑 RecursiveCharacterTextSplitter

CharacterTextSplitter

最基础的字符分割:按固定分隔符切割,达到长度就截断。快、简单,但几乎不做语义判断,容易切断句子。适合日志、短句这类轻量文本。

TokenTextSplitter

按 Token 数切割,更贴近模型上下文窗口与计费单位。利于控制单次输入成本,但同样可能拆碎语义,且依赖具体 Tokenizer。

RecursiveCharacterTextSplitter(通用首选)

用多级分隔符递归切割:优先段落,再句号、逗号、空格……大块超长再往下细分;再配 overlap,让相邻 Chunk 保留一点重叠,减少上下文断裂。

flowchart TD
  A["原始长文本"] --> B["优先按段落分隔符切割"]
  B --> C{"单 Chunk 尺寸是否合规"}
  C -->|合规| D["保留语义块"]
  C -->|超标| E["用下级分隔符继续切"]
  E --> C
  D --> F["叠加 Overlap"]
  F --> G["输出标准化 Chunk"]
分割器 切割依据 语义保留 性能开销 推荐度
CharacterTextSplitter 固定字符分隔符 极低
TokenTextSplitter Token 数量 一般 中等
RecursiveCharacterTextSplitter 多级递归分隔 较好 略高

三类分割器示例

import {
  CharacterTextSplitter,
  TokenTextSplitter,
  RecursiveCharacterTextSplitter
} from "@langchain/textsplitters";

const longText = `RAG技术全称检索增强生成,用于解决大模型幻觉与知识滞后问题。
通过文档检索增强提示词,让大模型基于私有知识库生成精准答案。
是目前企业知识库、AI Agent开发的核心技术栈。`;

基础字符分割:

const charSplitter = new CharacterTextSplitter({
  chunkSize: 50,
  chunkOverlap: 10
});
const charChunks = await charSplitter.splitText(longText);
console.log("基础字符分割结果:", charChunks);

按 Token 分割:

const tokenSplitter = new TokenTextSplitter({
  chunkSize: 30,
  chunkOverlap: 5
});
const tokenChunks = await tokenSplitter.splitText(longText);
console.log("Token精准分割结果:", tokenChunks);

递归语义分割(推荐):

const recursiveSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 100,
  chunkOverlap: 20,
  separators: ["\n\n", "\n", "。", ",", " "]
});
const recursiveChunks = await recursiveSplitter.splitText(longText);
console.log("递归语义分割结果:", recursiveChunks);

两个常被忽略、却很关键的点:

  1. Overlap:相邻块留一段重叠,缓解切分带来的语义断裂
  2. 场景适配:可用 fromLanguage 适配代码文档;也可自定义 lengthFunction,在字符计数与 Token 计数之间切换

Milvus:向量存储与检索

关系型数据库擅长精确条件与事务,但不擅长高维向量的语义检索。Milvus 这类向量库,用来存海量向量并做相似度匹配,是 RAG 常见基建之一。

数据层级

常见结构是:Database → Collection → Entity。建集合时要定义 Schema;向量字段通常需要建索引,数据量大时否则检索会明显变慢。

Milvus + MySQL 双库

生产里向量库很少单打独斗。更常见的是 MySQL(或其它业务库)+ Milvus 分工:

flowchart LR
  SUB["业务原始数据"] -- 结构化字段 --> M1["MySQL"]
  SUB -- 非结构化文本 --> M2["Milvus"]
  M1 --> A["条件查询 / 关联 / 事务"]
  M1 --> B["业务元数据"]
  M2 --> C["向量存储"]
  M2 --> D["语义相似度检索"]
  A --> RES["完整业务问答结果"]
  B --> RES
  C --> RES
  D --> RES
  • MySQL:结构化字段、精准查询、事务、元数据
  • Milvus:文本向量、语义召回

例如知识库:正文向量进 Milvus 做检索;文章 ID、时间、作者等元数据进 MySQL,两边用业务 ID 关联。

最小落地流水线

把组件串起来,通用路径可以是:

  1. Loader 接入文档
  2. 递归分割成 Chunk,配置 overlap
  3. Embedding 后写入 Milvus,并关联业务 ID
  4. 提问实时向量化,做相似度检索
  5. 用业务 ID 回查结构化数据(可选)
  6. 资料 + 问题组装 Prompt,交给 LLM

下面是 Node.js 最小串联示例(加载 → 切分 → 入库 → 检索):

import { TextLoader } from "@langchain/community/document_loaders/fs/text";
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { Milvus } from "@langchain/community/vector_stores/milvus";
import { OpenAIEmbeddings } from "@langchain/openai";

const embeddings = new OpenAIEmbeddings();
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 100,
  chunkOverlap: 20
});

const loader = new TextLoader("./knowledge.txt");
const docs = await loader.load();
const splitDocs = await splitter.splitDocuments(docs);

const vectorStore = await Milvus.fromDocuments(
  splitDocs,
  embeddings,
  {
    collectionName: "rag_knowledge",
    milvusConfig: {
      address: "http://localhost:19530"
    }
  }
);

const query = "RAG技术的核心作用是什么?";
const results = await vectorStore.similaritySearch(query, 3);
console.log("检索到的参考文档:", results);

小结

  1. RAG:用检索补知识、压幻觉,让模型能用上私有与更新更快的资料
  2. 预处理:Loader 统一接入;通用场景优先 RecursiveCharacterTextSplitter,并认真配置 overlap
  3. 存储:Milvus 做语义检索,业务库管结构化元数据,双库互补
  4. 链路:加载 → 切分 → 嵌入 → 入库 → 检索 → 增强生成

企业知识库、私有文档问答、Agent 记忆,大多都绕不开这套「检索 + 向量库 + 生成」骨架;LangChain / Milvus 只是其中一种常见落地组合。