mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5
2388 字
12 分钟
RAG 知识库搭建经验总结

一句话摘要#

这篇文章复盘了一套 RAG 知识库从分片、向量化、召回、重排到生成的落地过程,并整理了前后端技术选型、业务链路和实际踩坑。

目录#

背景与问题#

标准 RAG(检索增强生成)的核心流程可以概括为:分片 -> 索引 -> 召回 -> 重排 -> 生成

在这个流程里,系统会先把文章或资料切成 Chunk,再通过 Embedding 模型将每个片段转换为向量,并存入向量数据库。用户提问时,问题本身也会被转换为向量,系统再从向量数据库中检索出语义最相近的 nn 个片段。

接下来,系统会用 Cross-encoder 模型对召回结果进行重排,筛选出相关度最高的 mm 个片段。最后,将用户问题和这些高相关片段一并输入给大模型,生成最终回答。

本章小结#

RAG 的本质是把“可靠资料检索”接入大模型生成链路,本质就是上下文工程,给大模型提供参考资料。

关键思路#

这次项目的后端用 Node.js / Hono,前端用 Vue3,向量数据库基于 Docker + PGVector 搭建,AI 协调控制技术栈选择 LangChain + Vercel AI SDK

它们在系统中的职责划分如下:

  • PGVector:作为向量数据库,利用 Docker 快速容器化部署 PostgreSQL,并加载 PGVector 插件,提供本地化向量存储和相似度匹配服务。
  • LangChain.js:负责后端的数据加载、文本切片、文档对象构建,以及向向量数据库发起检索。
  • Vercel AI SDK:负责大模型调用、流式数据通讯和前端 UI 状态接管,减轻前端处理流式响应的负担。

为什么需要 Vercel AI SDK#

如果不使用 Vercel AI SDK,在 Vue3 中手工开发 AI 对话界面会遇到不少重复问题。

首先是 SSE 或 ReadableStream 的解析。后端返回的通常不是完整 JSON,而是连续的数据流,前端需要手动处理 Chunk 解码、字符串拼接和异常状态。

其次是打字机效果。每次收到数据碎片,都要手动更新消息数组;如果渲染节奏控制不好,很容易引发界面卡顿或频繁重绘。

最后是状态管理。isLoading、停止生成、重新生成、上下文消息数组等状态都需要手动维护,代码很容易变得松散。

Vercel AI SDK 在 Vue 项目中可以通过 @ai-sdk/vue 接管这些状态。UI 直接循环渲染 messages,输入框绑定 input,提交绑定 handleSubmit,加载状态绑定 isLoading,整体实现会轻很多。

它也支持生成式结构化输出(Generative UI)。当需要 AI 返回“目标、热量、餐次”等结构化 JSON 数据时,SDK 可以在接收碎片流期间逐步处理内容,甚至驱动前端生成动态交互组件。

本章小结#

后端用 LangChain 管检索链路,底层数据的向量存储交由 PGVector,前端和模型流式交互交给 Vercel AI SDK。这样分工后,系统边界清晰,前后端都不用重复处理多余的组件底座工作。

实践过程#

在具体落地时,LangChain 服务端的各个核心组件(Loaders、Splitters、Embeddings、Vector Stores)并没有通过 RunnableSequence 强行声明式串联,而是分散在导入脚本和 RAG 检索层中进行了灵活组装。

步骤1:业务链路与数据融合#

在基础 RAG 问答能力之上,这个项目重点融合了用户本地数据。整体处理链路分为四步:

  1. 读取本地用户数据:通过 SQL 从浏览器 IndexedDB 中读取身高、体重、近期体重变化、热量摄入等属性数据。
  2. 知识库向量检索:将用户问题向量化,在 PGVector 中检索相关的专业知识内容。
  3. 融合提示词:把“用户本地私有数据”、“向量库返回的专业资料”和“用户原始问题”汇总并拼装成完整的 Prompt。
  4. 大模型生成:后端将组装好的 Prompt 发给模型,然后利用 Vercel AI SDK 将数据平滑流式地返回给前端展示。

这条链路的重点突破:本地系统数据解决“个性化背景限制”,知识库检索解决“干货专业依据缺乏”,这两只手抓稳后双管齐下交由大模型统筹整合。

步骤2:数据加载与切块(Loaders & Splitters)#

首先,需要从源文件(例如 PDF)中提取文本,并使用 Document Loaders 转换为 LangChain 的 Document。在此步骤我们同时注入了页码、书名等 Metadata,为的是日后给解答提供清晰的引用原出处:

import { Document } from '@langchain/core/documents'
export const createKnowledgeDocumentsFromPages = ({ filePath, pages }: CreateKnowledgeDocumentsOptions) => {
const bookTitle = inferBookTitleFromFileName(filePath)
return pages.flatMap(({ pageNumber, text }) => {
const pageContent = cleanKnowledgeText(text)
return [
new Document({
metadata: { bookTitle, pageNumber, source: filePath },
pageContent,
}),
]
})
}

针对整块长文档,项目接着使用了分块器 RecursiveCharacterTextSplitter 进行更小粒度的切割(Splitters):

new RecursiveCharacterTextSplitter({
chunkOverlap: 300,
chunkSize: 1500,
})

这里限制每个文本块约保留 1500 字符,冗余重叠 300 字符。保留重叠字符是常见但也特别必须的做法,能有效减少连续语义段落正好被“一刀两断”从而在检索中匹配度过低的问题。

步骤3:向量化与入库(Embeddings & Vector Stores)#

切分完毕后,再次利用 OpenAIEmbeddings 以及 PGVectorStore 发起 Embedding 从而在向量库(Vector Stores)进行落库存储。

import { PGVectorStore } from '@langchain/community/vectorstores/pgvector'
import { OpenAIEmbeddings } from '@langchain/openai'
export const createKnowledgeVectorStore = async () => {
const { collectionName, connectionString, tableName } = getRagRuntimeConfig()
return PGVectorStore.initialize(new OpenAIEmbeddings({...}), {
collectionName,
collectionTableName: `${tableName}_collections`,
postgresConnectionOptions: { connectionString },
tableName,
})
}

实际通过脚本批量写入时,必须使用分块批处理循环写入以控制节奏:

for (let index = 0; index < chunks.length; index += batchSize) {
await vectorStore.addDocuments(chunks.slice(index, index + batchSize))
}

避坑踩雷点:在起初导入时遇到了大量的超速失败返回,我最终把 batchSize 强行从官方默认经验值的 50 压到了 10 才安全导入完成避免接口速率限制断连。

步骤4:检索召回流(Retriever Pipeline)#

这一步,当用户提问时,我们会将上面初始化的 PGVectorStore 实例转为正式的检索器(Retriever),并调用 .invoke 向模型发起近似语句匹配。这也是虽然没用显式 Runnable 管道串联,但这完全执行了相同手撕控制流的地方。

export const createKnowledgeRetriever = async () => {
const { k } = getRagRuntimeConfig()
const vectorStore = await createKnowledgeVectorStore()
return vectorStore.asRetriever({ k })
}
export const getKnowledgeContext = async (question: string, {...}) => {
const retriever = await createRetriever()
const documents = await retriever.invoke(question)
return formatKnowledgeContext(documents)
}

代码中的 k 代表你要让他在库里回捞排名前几最相似的资料条(设为 5)。不能盲目求多:如果召回偏少固然会错失某些要点,但无限度扩大后更会引发严重的文不对题噪音,而且直接把一堆没用的废话喂给 LLM 会消耗大量不必要的 Token 计费。 这一个收尾逻辑,实现了真正的 RAG Pipeline 运作链路过程:

存入流:PDF原始数据 -> Loaders转为Document -> Splitters分块切分 -> Embeddings化 -> PGVector存储
检索流:提问触发请求 -> retriever召回最相似片段 -> 附并私有数据合并出终极Prompt -> 交给聊天大模型 -> 前端流式收尾展示

本章小结#

回顾这个实践脉络会发现:数据层囊括了原文档解析与 Embedding 赋能,检索层揽收下了文本找回与大锅烩操作,再借由大模型与 Vercel 的生成能力构成生成展示层,由此跑通了一个有逻辑的完整 RAG 工作流架构。

结果与复盘#

PDF 解析问题#

一开始下的书籍是 PDF 版本,用 pdf-parse 转文本后发现很多书是空页。问ai才知道需要 OCR 处理的版本。然后又去网上找了 OCR 版本的 PDF 才成功导入。

批量导入过大#

向量化导入数据库时,把每次导入的 Batch Size 从默认的 50 调小到 10,才没有触发 Embedding 接口速率限制。

结构化输出#

最初允许 AI 一次生成任意时长的计划,但后来发现当计划超过 14 天时,输出规模会变得过大,后段很容易遭遇内容截断或 JSON 格式错误。

解决方式有三点:

  1. 从产品侧限制单次最多生成 1-14 天的计划。
  2. 使用 JSON output 属性,增加预置 JSON 结构的 Prompt 要求。
  3. 增加强验证机制。如果系统校验到错乱 JSON,就拒绝输出结果,并把错误输出和标准示例再次交给 AI 重整。

TDD#

这次实践也让我更深刻地认识到 TDD(测试驱动开发)的重要性。现在已经通过提示词限制,例如 Codex 的 agent.md,强制要求 AI 每次动及源码时都遵循 TDD 模式开发。

也就是:先写预期失败的前置测试 -> 编写业务逻辑 -> 本地测试通过后才视为完成

这种约束方式配合全量回归测试,可以给后续迭代划定明确边界,也能最大程度降低 AI 修改新逻辑时悄悄破坏老功能的风险。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

RAG 知识库搭建经验总结
https://blog.moxiaoshuai.fun/2026-05-rag知识库搭建-rag知识库搭建/
作者
莫莫
发布于
2026-05-21
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
示例歌曲
示例艺人
封面
示例歌曲
示例艺人
0:00 / 0:00