让 AI 学会你的私有知识
为什么需要 RAG
大模型有三个天然缺陷:
1.知识截止日期:GPT-4o 可能不知道上周发生的事 2.不懂你的私有数据:公司内部文档、你的个人笔记 3.爱瞎编(幻觉):遇到不知道的会一本正经胡说
**RAG (Retrieval-Augmented Generation)**的思路非常朴素:
回答问题前,先去知识库里搜一下相关资料,把资料和问题一起交给 LLM,让它基于资料回答。
这就像你参加开卷考试——知识在书里,你只负责查和组织答案。
RAG 的五步流程
1. 准备阶段(离线一次性做)
知识库文档 → 切分 chunks → 生成 embedding → 存向量库
2. 查询阶段(每次用户提问)
用户问题 → 生成 embedding → 向量库里查相似文档 → 拼 Prompt → 调 LLM → 答案
画成图:
[用户问题] ─embed─► [向量] ─search─► [Top-K 相关文档]
│
▼
┌──────────────────────┐
│ Prompt: │
│ 基于下面资料回答: │
│ {相关文档} │
│ │
│ 问题: {用户问题} │
└──────────────────────┘
│
▼
[LLM] → 答案手写一个最小 RAG
不用任何框架,几十行代码就够。
// rag.js
import OpenAI from 'openai'
import fs from 'node:fs/promises'
const client = new OpenAI()
async function embed(text) {
const r = await client.embeddings.create({
model: 'text-embedding-3-small',
input: text,
})
return r.data[0].embedding
}
function cosineSim(a, b) {
let dot = 0, na = 0, nb = 0
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i]
na += a[i] * a[i]
nb += b[i] * b[i]
}
return dot / (Math.sqrt(na) * Math.sqrt(nb))
}
// 1. 切分:把长文档切成小块
function splitText(text, chunkSize = 500, overlap = 50) {
const chunks = []
for (let i = 0; i < text.length; i += chunkSize - overlap) {
chunks.push(text.slice(i, i + chunkSize))
}
return chunks
}
// 2. 索引:所有 chunk 预计算 embedding
const doc = await fs.readFile('./my-notes.md', 'utf-8')
const chunks = splitText(doc)
const index = await Promise.all(
chunks.map(async (text) => ({ text, vec: await embed(text) }))
)
// 3. 查询:根据问题找 Top-K 相关片段
async function retrieve(question, k = 3) {
const qVec = await embed(question)
return index
.map((item) => ({ ...item, score: cosineSim(qVec, item.vec) }))
.sort((a, b) => b.score - a.score)
.slice(0, k)
}
// 4. 生成:拼 Prompt 交给 LLM
async function ask(question) {
const relevant = await retrieve(question, 3)
const context = relevant.map((r, i) => `[资料${i + 1}]\n${r.text}`).join('\n\n')
const res = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: '你是一个严谨的助手。只根据用户提供的资料回答,不知道就说不知道,不要编造。',
},
{
role: 'user',
content: `资料:\n${context}\n\n问题:${question}`,
},
],
})
return res.choices[0].message.content
}
console.log(await ask('这份笔记里提到的核心观点是什么?'))
这就是 RAG 的全部。真的就这么简单。
切分(Chunking)的讲究
切分策略直接决定 RAG 的召回质量。常见方案:
1. 固定长度切分(上面的例子)
- 优点:简单
- 缺点:容易把一个完整的句子切开
2. 按段落/标题切分
// 按 markdown 的 ## 二级标题切分
const chunks = text.split(/\n(?=##\s)/)
3. 语义切分
用 LLM 或专门模型识别语义边界。LangChain 有 SemanticChunker。
4. 带元信息的切分
每个 chunk 附加文档名、章节号、页码,方便最后给用户"出处"。
{
text: '这是正文...',
metadata: {
source: 'design-system.md',
section: '颜色规范',
page: 3,
}
}检索质量的调优
chunk_size:太小丢上下文,太大召回不精准。一般 300~800 字符起步。
overlap:chunk 之间要有一定重叠(常见 10-20%),避免关键信息正好卡在边界上。
top_k:取多少条。太少可能漏,太多 Token 超标。通常 3~10。
重排序(Rerank):先用向量粗查 20 条,再用一个 Cross-Encoder 模型精排,取前 5 条给 LLM。显著提升质量。推荐 Cohere Rerank 或 BGE-Reranker。
混合检索:向量 + 关键词(BM25)结果合并。对人名、专有名词这种向量不擅长的场景很有用。
RAG 的典型失败模式
1. 召回不到相关内容
- Embedding 模型对你的领域数据不匹配 → 换模型或微调
- 用户问题太短/模糊 → 用 LLM 先改写问题(Query Rewriting)
2. 召回了但 LLM 没用上
- Prompt 里资料放太靠后 → 模型"迷失在中间"(Lost in the Middle)
- 解决:把最相关的资料放最前或最后
3. LLM 还是在编造
- System Prompt 强化:"只用提供的资料,没依据就说不知道"
- 让模型引用资料编号:
[资料1][资料3]
需要什么框架吗?
原型阶段手写就好。上生产再考虑:
-LangChain / LangChain.js:生态最全,但抽象有点重 -LlamaIndex / LlamaIndex.TS:专门做 RAG,封装更好 -Vercel AI SDK:前端友好,和 Next.js 无缝
第 9 篇会用 Vercel AI SDK 做一个带 RAG 的聊天界面。
生产级 RAG 的架构图
不给你画复杂图了,直接贴第 4 篇提到的向量库方案:
[前端] → [后端 API]
│
├─► [用户问题 → Embedding]
│
├─► [pgvector: 向量 + 关键词混合查询]
│
├─► [Rerank 模型精排]
│
└─► [LLM: 生成答案 + 引用]动手作业
基于你的博客(或者任何一堆 Markdown 文件)做一个问答机器人:
- 扫描所有
.md,按二级标题切分 - 全部 embed 存 JSON 数组(几千条内不用数据库)
- CLI 问答,每次回答都带上"参考资料"
进阶:把 JSON 换成 SQLite + sqlite-vss 或 pgvector。
参考资料
- Anthropic: Introducing Contextual Retrieval — 2024 年重要的 RAG 改进方案
- LlamaIndex: RAG Concepts
- 《Lost in the Middle》论文 — 长上下文的注意力问题
- Pinecone: RAG Guide
- BGE-Reranker — 免费的重排序模型
版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。
(采用 CC BY-NC-SA 4.0 许可协议进行授权)
本文标题:RAG:让 AI 回答你的专属数据
本文链接:https://www.sshipanoo.com/blog/ai/ai-for-frontend/05-RAG入门/
本文最后一次更新为 天前,文章中的某些内容可能已过时!