Skip to content
On this page

知识库与 RAG · 让 Agent 记住你的私有资料 ​

基础篇里我们讲过 RAG 的道理:在模型开口前,先把相关段落捞出来递给它。 这一节把这件事在 Node 里真正做通,并接进我们那个会对话的 Agent。

本节基于环境与依赖的工程继续。这一节的代码会明显变多,因为它要把"切块→向量化→存库→检索→生成"五步都跑起来。别怕,每一步都不难,只是步骤长。

RAG 装进 Node 的整条流水线 ​

读文档 → 切成段 → 每段变向量 → 存进向量库 → 提问时搜 → 带答案给模型

我们用最小但可跑的方案走通它:文档用本地一个 .txt(后面可换 PDF/网页),向量库用一个纯 Node 的 memoryvectorstore(内存版,零安装依赖,适合学习;生产可换 Chroma/Qdrant,详见文末)。

1. 装依赖 ​

bash
npm install langchain @langchain/openai @langchain/text-splitters
# 内存向量库其实已经包含在 langchain 里,所以通常不用单独装

如果需要生产级向量库(可选),装:

bash
npm install chromadb          # Chroma,本地跑
# 或 npm install @qdrant/js-client-rest   # Qdrant

2. 准备一份"公司文档"当语料 ​

在项目根目录建一个目录放文档,随便写点只有"你的公司"才知道的内容——用来验证模型"凭空答不出、读了资料才答得出":

text
# 员工手册 · 桔子科技
## 加班费
加班按打卡时长计费。工作日超过 20:00 的部分按 1.5 倍计算;周末全天按 2 倍计算;法定节假日按 3 倍计算。加班需提前在 OA 系统申请。

## 报销额度
员工出差补贴:一线城市每日 300 元,二线城市 200 元。业务招待需提前报备,单次满 500 元需附发票。

关键验证点:问模型"周末加班几倍?" 一个没读过资料的模型会乱猜或道歉;读了资料后能准确答"2 倍"。

3. 预处理:把文档切块 + 向量化 + 入库 ​

写一个一次性脚本 src/build-vectorstore.js:

js
// src/build-vectorstore.js —— 跑一次,把文档变成向量存进内存库
import 'dotenv/config'
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { RecursiveCharacterTextSplitter } from '@langchain/text-splitters'
import { OpenAIEmbeddings } from '@langchain/openai'
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
import { writeFile } from 'node:fs/promises'

// ① 读取文档
const loader = new TextLoader('./data/员工手册.txt')
const docs = await loader.load()

// ② 切成小段(按 500 字一段、重叠 80 字)
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500,
  chunkOverlap: 80,
})
const chunks = await splitter.splitDocuments(docs)
console.log(`切成了 ${chunks.length} 段`)

// ③ 每一段转成向量, ④ 存进内存向量库
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,
  configuration: { baseURL: process.env.OPENAI_API_BASE },
})
const store = await MemoryVectorStore.fromDocuments(chunks, embeddings)

// ⑤ (生产可序列化保存)这里直接把对象导出,供主程序复用
await writeFile('./data/vectorstore.json', JSON.stringify({
  chunks: chunks.map(c => ({ text: c.pageContent, source: c.metadata.source })),
}), 'utf8')
console.log('向量库建立完成!')

跑一次:

bash
node src/build-vectorstore.js
# 输出: 切成了 N 段 / 向量库建立完成!

发生了什么: 文档被切成小段,每段用"向量模型"转成数学向量,进了向量库。这就是基础篇说的"提前把书编好索引"。向量模型用的是 OpenAIEmbeddings——它是与对话模型不同的一个"专用模型",专门负责把文字变向量。

向量模型也是要花钱调用的,它一般很便宜。国内模型也都有对应的 embeddings 接口(如 DashScope 的 text-embedding-v3)。

4. 检索:提问时把相关段落捞出来 ​

js
// src/search.js —— 独立验证"检索"这一步
import 'dotenv/config'
import { OpenAIEmbeddings } from '@langchain/openai'
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { RecursiveCharacterTextSplitter } from '@langchain/text-splitters'

// 重新临时构建(实际项目会复用提前建好的库,这里演示直接逻辑)
const docs = await new TextLoader('./data/员工手册.txt').load()
const chunks = await new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 80 }).splitDocuments(docs)
const embeddings = new OpenAIEmbeddings({ apiKey: process.env.OPENAI_API_KEY, configuration: { baseURL: process.env.OPENAI_API_BASE } })
const store = await MemoryVectorStore.fromDocuments(chunks, embeddings)

// 提问 → 转向量 → 捞最相关的 3 段
const question = '周末加班费怎么算?'
const results = await store.similaritySearch(question, 3) // ← 语义检索:按"意思"找

console.log('命中的片段:')
results.forEach((r, i) => {
  console.log(`\n── 第 ${i + 1} 段 ──`)
  console.log(r.pageContent)
})

这里就体会到了向量检索的魔力:你问的是"周末加班费怎么算",文档里没有原样这句话,但向量能"按意思"找出讲"加班费/2倍"的那一段。

5. 生成:RAG 全流程,让模型照着资料答 ​

把"检索 + 对话"装进我们那个 Agent,这才是 RAG 的完整形态:

js
// src/index.js —— 升级:带 RAG 的对话 Agent
import 'dotenv/config'
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai'
import { TextLoader } from 'langchain/document_loaders/fs/text'
import { RecursiveCharacterTextSplitter } from '@langchain/text-splitters'
import { MemoryVectorStore } from 'langchain/vectorstores/memory'
import readline from 'node:readline/promises'

// —— 模型 & 向量库(启动时一次性建好)——
const model = new ChatOpenAI({
  model: process.env.MODEL_NAME ?? 'gpt-4o-mini',
  apiKey: process.env.OPENAI_API_KEY,
  configuration: { baseURL: process.env.OPENAI_API_BASE },
})

const docs = await new TextLoader('./data/员工手册.txt').load()
const chunks = await new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 80 }).splitDocuments(docs)
const embeddings = new OpenAIEmbeddings({ apiKey: process.env.OPENAI_API_KEY, configuration: { baseURL: process.env.OPENAI_API_BASE } })
const store = await MemoryVectorStore.fromDocuments(chunks, embeddings)

// —— RAG 的核心函数:先搜资料,再带资料回答 ——
async function askWithRag(question) {
  const hits = await store.similaritySearch(question, 3)      // ① 捞相关段落
  const context = hits.map(h => h.pageContent).join('\n\n')   // ② 拼成上下文

  // ③ 把"资料 + 问题"一起交给模型,并强制它"照资料答,查不到就说不知道"
  const reply = await model.invoke([
    {
      role: 'system',
      content:
        '你是一个客服助手。回答必须严格依据下面提供的【参考资料】,' +
        '素材里没有的就说不知道,绝不编造。回答末尾附上最相关的来源。\n\n' +
        '【参考资料】\n' + context,
    },
    { role: 'user', content: question },
  ])
  return reply.content
}

// —— 命令行 demo ——
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
console.log('📚 RAG 客服机器人(输入 bye 退出)')
while (true) {
  const q = await rl.question('你: ')
  if (q === 'bye') break
  const answer = await askWithRag(q)
  console.log('AI: ' + answer + '\n')
}
bash
npm run dev
# 你: 公司周末加班几倍工资?
#   周末加班按 2 倍计算(依据:员工手册)
# 你: 一线城市出差补贴多少?
#   每天 300 元
# 你: 公司的股票代码?  ← 资料里没有
#   参考资料里没有提到,我无法回答。

发现区别了没? 同样一个模型,这一刻它"知道"了你是哪个公司的人、你的内部规定。而且就算它不知道,它也会诚实说不知道,而不是瞎编——这正是 RAG 对抗幻觉的核心价值。

6. 把 RAG 玩得更专业:来源交代 + 检索重排(可选) ​

  • 强制来源:在 system prompt 里写"引用时标注出处段落",做hits[].metadata.source 拼接;
  • 混合检索:向量检索 + 关键词搜索结合(人名/编号/拼写类更准);
  • 重排(Rerank):先粗捞 20 段,再用重排模型精排取 3 段(更准但多一次调用,后面微调篇会展开)。

7. 生产环境换正式向量库 ​

刚才用的是 MemoryVectorStore(内存),重启就丢,适合学习。生产换成本地持久化或云库,只改一处——把 MemoryVectorStore.fromDocuments 换成对应平台的:

js
// Chroma(本地持久化)
import { Chroma } from '@langchain/community/vectorstores/chroma'
const store = await Chroma.fromDocuments(chunks, embeddings, { collectionName: 'company_docs' })

// Qdrant(云/本地)
// const store = await QdrantVectorStore.fromDocuments(chunks, embeddings, { url, collectionName })

核心逻辑(切块/向量化/检索/生成)完全不变,只是"存哪"换了。这就是 LangChain 统一抽象的甜头。

🧪 本篇自检 ​

  • [ ] build-vectorstore.js 能切块并建立向量库
  • [ ] search.js 能用"语义"检索出相关段落(而不是纯字面)
  • [ ] 完整 Agent 能答出私有文档里的问题,且"资料没有的会拒绝"

这一篇的灵魂,一句话:

RAG = 先把文档切成段、每段变成向量入库;提问时把问题也变向量,按语义捞出相关段,再让模型照着资料回答。 至此你的 Agent 已多了一块"私有知识",能答内部问题了。

但知识是"静态的"。要让 Agent 真正上手干活——查天气、算数据、查数据库——需要给它装上那双手: 工具调用。

要保持清醒 永远不抱有意外的幻想 凭空的期待最要命