RAG 检索增强生成
RAG(Retrieval-Augmented Generation)通过在大模型推理前检索相关知识片段,将外部知识注入上下文,从而解决模型知识截止、幻觉和领域知识不足的问题。核心流程为:问题改写 → 向量检索 → 上下文注入 → 增强生成。
核心原理
为什么需要 RAG
| 问题 | 纯 LLM 表现 | RAG 增强后 |
|---|---|---|
| 知识截止 | 无法回答训练数据之后的问题 | 检索最新文档 |
| 幻觉 | 编造不存在的事实 | 基于真实文档生成 |
| 领域知识 | 对私有数据一无所知 | 检索企业知识库 |
| 可追溯性 | 无法标注信息来源 | 可引用原始文档 |
RAG 完整流程
图表渲染中…
向量检索基础
Embedding 原理
Embedding 将文本映射为高维向量空间中的点,语义相近的文本在向量空间中距离更近:
typescript
// 调用 Embedding API
async function getEmbedding(text: string): Promise<number[]> {
const response = await fetch(`${EMBEDDING_ENDPOINT}/embeddings`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: 'text-embedding-3-small',
input: text,
}),
});
const data = await response.json();
return data.data[0].embedding; // 1536维向量
}相似度计算
| 算法 | 公式 | 适用场景 |
|---|---|---|
| 余弦相似度 | (\cos(\theta) = \frac{A \cdot B}{|A| |B|}) | 文本语义匹配(最常用) |
| 欧氏距离 | (\sqrt{\sum(a_i - b_i)^2}) | 绝对距离度量 |
| 点积 | (A \cdot B = \sum a_i b_i) | 已归一化向量 |
主流向量数据库
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| Pinecone | 全托管、高性能 | 生产环境、快速上线 |
| Milvus | 开源、分布式 | 大规模私有化部署 |
| Chroma | 轻量、嵌入式 | 原型开发、本地测试 |
| Weaviate | 混合搜索(向量+关键词) | 需要精确匹配+语义 |
| pgvector | PostgreSQL 扩展 | 已有 PG 基础设施 |
知识库构建
文档处理流水线
图表渲染中…
分块策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 固定大小 | 按字符数/token 数切割 | 通用文档 |
| 段落分割 | 按自然段落边界切割 | 结构化文章 |
| 语义分割 | 按主题变化点切割 | 长篇叙述 |
| 递归分割 | 先按大结构再递归细分 | 代码/技术文档 |
| 重叠窗口 | 相邻块保留 10-20% 重叠 | 避免上下文断裂 |
typescript
// 递归分块示例
function splitDocument(
text: string,
options: {
chunkSize: number; // 目标块大小
chunkOverlap: number; // 重叠大小
separators: string[]; // 分隔符优先级
}
): string[] {
const { chunkSize, chunkOverlap, separators } = options;
const [sep, ...restSeps] = separators;
if (!sep || text.length <= chunkSize) return [text];
const parts = text.split(sep);
const chunks: string[] = [];
let current = '';
for (const part of parts) {
if ((current + sep + part).length > chunkSize && current) {
chunks.push(current.trim());
// 保留重叠
const overlapText = current.slice(-chunkOverlap);
current = overlapText + sep + part;
} else {
current = current ? current + sep + part : part;
}
}
if (current.trim()) chunks.push(current.trim());
// 对过长的块递归使用下一级分隔符
return chunks.flatMap((chunk) =>
chunk.length > chunkSize
? splitDocument(chunk, { ...options, separators: restSeps })
: [chunk]
);
}
// 使用
const chunks = splitDocument(document, {
chunkSize: 500,
chunkOverlap: 50,
separators: ['\n## ', '\n### ', '\n\n', '\n', '。', ' '],
});问题改写策略
为什么需要问题改写
用户的原始问题可能:
- 表述模糊("那个东西怎么用")
- 包含指代("它的价格是多少")
- 过于口语化("帮我搞一下那个登录的 bug")
改写方法
typescript
const rewritePrompt = `
你是一个查询优化器。根据对话历史和用户当前问题,生成适合知识库检索的查询。
规则:
1. 将指代词替换为具体实体
2. 补充上下文中的关键信息
3. 生成 2-3 个不同角度的检索 query
4. 保持简洁,每个 query 不超过 20 字
对话历史:
${history}
用户问题:${question}
输出 JSON:
{
"rewritten_queries": ["query1", "query2", "query3"],
"intent": "用户真实意图"
}
`;多路检索融合
图表渲染中…
上下文注入与生成
Prompt 模板
typescript
function buildRAGPrompt(question: string, chunks: RetrievedChunk[]): string {
const context = chunks
.map((c, i) => `[${i + 1}] 来源: ${c.source}\n${c.content}`)
.join('\n\n');
return `
基于以下参考资料回答用户问题。
规则:
- 仅使用参考资料中的信息回答
- 如果资料不足以回答,明确说明
- 在回答中标注引用来源编号 [n]
- 保持回答简洁准确
参考资料:
${context}
用户问题:${question}
`;
}检索质量评估
| 指标 | 说明 | 优化方向 |
|---|---|---|
| 召回率 | 相关文档是否被检索到 | 增加 Top-K、多路检索 |
| 精确率 | 检索结果中相关的比例 | 优化 Embedding 模型、Rerank |
| 上下文利用率 | 模型是否有效使用了检索内容 | 优化注入格式、减少噪声 |
| 回答忠实度 | 回答是否忠于检索内容 | 强化"仅基于资料"指令 |
常见问题与陷阱
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 检索不到相关内容 | Query 与文档表述差异大 | 问题改写 + 同义词扩展 |
| 回答仍含幻觉 | 模型未严格遵循上下文 | 强化约束指令 + 降低 temperature |
| 分块后语义断裂 | 切割点不当 | 使用语义分割 + 重叠窗口 |
| 检索延迟高 | 向量库规模大 | 使用 ANN 索引(HNSW/IVF) |
| 多语言混合效果差 | Embedding 模型不支持 | 使用多语言模型(如 BGE-M3) |