知识库问答系统
基于 RAG 架构构建企业知识库问答系统的完整方案。
什么是知识库问答?
知识库问答系统让用户可以用自然语言查询企业内部文档,获得准确的答案。它解决了大模型的两个核心问题:
| 问题 | 说明 | RAG 如何解决 |
|---|---|---|
| 知识截止 | 模型不知道最新信息 | 检索最新文档 |
| 幻觉 | 模型可能编造信息 | 基于真实文档回答 |
| 专业知识 | 模型不了解企业内部信息 | 检索企业文档 |
RAG(Retrieval-Augmented Generation)的核心思想:
用户问题 → 检索相关文档 → 将文档作为上下文 → LLM 生成答案
与微调的对比:
| 方面 | RAG | 微调 |
|---|---|---|
| 知识更新 | 实时(更新文档即可) | 需要重新训练 |
| 成本 | 低(无需训练) | 高(需要 GPU) |
| 可解释性 | 高(可追溯来源) | 低 |
| 适用场景 | 知识密集型任务 | 行为/风格调整 |
系统架构
一个完整的知识库问答系统包含离线处理和在线查询两个流程:
flowchart TD
subgraph 离线处理
A[文档] --> B[解析]
B --> C[分块]
C --> D[Embedding]
D --> E[(向量数据库)]
end
subgraph 在线查询
F[用户问题] --> G[Embedding]
G --> H[向量检索]
E --> H
H --> I[重排序]
I --> J[构建 Prompt]
J --> K[LLM 生成]
K --> L[返回答案]
end各环节说明:
| 环节 | 作用 | 关键技术 |
|---|---|---|
| 文档解析 | 提取文本内容 | PDF 解析、OCR |
| 分块 | 切分为适当大小 | 语义分块、重叠 |
| Embedding | 转换为向量 | text-embedding-3 |
| 向量检索 | 找到相关内容 | 相似度搜索 |
| 重排序 | 优化排序 | Cross-encoder |
| Prompt 构建 | 组织上下文 | 模板设计 |
| LLM 生成 | 生成答案 | GPT-4o、Claude |
文档处理流程
1. 文档解析
支持多种文档格式的解析:
| 格式 | 解析方式 | 推荐工具 | 注意事项 |
|---|---|---|---|
| 文本提取 | PyPDF2, pdfplumber | 扫描件需 OCR | |
| Word | 结构解析 | python-docx | 保留格式 |
| Markdown | 直接读取 | - | 最简单 |
| HTML | 标签清理 | BeautifulSoup | 去除样式 |
| Excel | 表格转文本 | pandas | 保留表格结构 |
解析示例:
from langchain_community.document_loaders import (
PyPDFLoader,
Docx2txtLoader,
UnstructuredMarkdownLoader
)
# PDF
loader = PyPDFLoader("document.pdf")
docs = loader.load()
# Word
loader = Docx2txtLoader("document.docx")
docs = loader.load()
2. 文本分块
分块是 RAG 系统最关键的环节之一。分块太大会引入噪音,太小会丢失上下文。
flowchart LR
A[长文档] --> B{分块策略}
B --> C[固定长度]
B --> D[语义分块]
B --> E[结构分块]分块策略对比
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定长度 | 简单、均匀 | 可能切断语义 | 通用 |
| 重叠分块 | 保留上下文 | 冗余存储 | 推荐默认 |
| 按段落 | 语义完整 | 长度不均 | 结构化文档 |
| 按标题 | 层次清晰 | 依赖格式 | 技术文档 |
| 语义分块 | 最佳语义 | 计算复杂 | 高质量需求 |
推荐参数
CHUNK_CONFIG = {
"chunk_size": 500, # 每块字符数(中文约 500 字)
"chunk_overlap": 50, # 重叠字符数(10% 左右)
"separator": "\n\n" # 优先分割符
}
分块大小选择:
| 场景 | 建议大小 | 原因 |
|---|---|---|
| 问答系统 | 300-500 | 精确匹配 |
| 文档摘要 | 1000-2000 | 更多上下文 |
| 代码库 | 按函数/类 | 语义完整 |
3. 元数据提取
为每个文档块添加元数据,便于过滤和溯源:
chunk_metadata = {
"source": "产品手册.pdf",
"page": 15,
"section": "安装指南",
"created_at": "2024-01-01",
"category": "技术文档",
"author": "技术部"
}
元数据的作用:
- 过滤:只搜索特定类别的文档
- 溯源:告诉用户答案来自哪里
- 权重:新文档可以有更高权重
- 权限:不同用户看到不同文档
检索优化
检索质量直接决定了最终答案的质量。“检索不到,就回答不了”。
1. 混合检索
单纯的向量检索可能遗漏关键词匹配的结果。混合检索结合两种方式:
flowchart TD
A[查询] --> B[关键词检索]
A --> C[向量检索]
B --> D[BM25 结果]
C --> E[语义结果]
D --> F[结果融合]
E --> F
F --> G[最终结果]两种检索的互补:
| 查询类型 | 关键词检索 | 向量检索 |
|---|---|---|
| “API 文档” | ✅ 精确匹配 | ⚠️ 可能不准 |
| “如何调用接口” | ⚠️ 可能遗漏 | ✅ 语义理解 |
| 混合查询 | 部分匹配 | 部分匹配 |
融合算法
def reciprocal_rank_fusion(results_list, k=60):
"""RRF 融合算法 - 简单有效的结果融合方法"""
scores = {}
for results in results_list:
for rank, doc in enumerate(results):
doc_id = doc.id
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
2. 查询改写
优化用户查询以提高检索效果:
flowchart LR
A[原始查询] --> B[查询改写]
B --> C[扩展查询]
B --> D[同义词替换]
B --> E[HyDE]HyDE(假设文档嵌入)
def hyde_search(query, retriever, llm):
# 1. 生成假设答案
hypothetical = llm.generate(f"回答这个问题:{query}")
# 2. 用假设答案检索
results = retriever.search(hypothetical)
return results
3. 重排序
对初步检索结果进行精排:
flowchart LR
A[Top 20 结果] --> B[重排序模型]
B --> C[Top 5 结果]| 重排序方法 | 效果 | 成本 |
|---|---|---|
| LLM 打分 | 好 | 高 |
| Cross-Encoder | 很好 | 中 |
| 规则过滤 | 一般 | 低 |
Prompt 构建
基础模板
RAG_PROMPT = """基于以下参考信息回答用户问题。
## 参考信息
{context}
## 规则
- 只基于参考信息回答
- 如果信息不足,诚实说明
- 引用来源时标注 [来源: xxx]
## 用户问题
{question}
## 回答
"""
带引用的模板
RAG_PROMPT_WITH_CITATION = """基于以下参考信息回答问题,并标注引用来源。
## 参考信息
{numbered_context}
## 用户问题
{question}
## 要求
- 回答后标注引用,格式:[1][2]
- 无相关信息时说明"未找到相关信息"
"""
答案生成
1. 基础问答
def answer_question(question, retriever, llm):
# 检索
docs = retriever.search(question, top_k=5)
# 构建上下文
context = "\n\n".join([d.content for d in docs])
# 生成答案
prompt = RAG_PROMPT.format(context=context, question=question)
answer = llm.generate(prompt)
return answer, docs
2. 带引用的问答
def answer_with_citation(question, retriever, llm):
docs = retriever.search(question, top_k=5)
# 编号上下文
numbered = "\n\n".join([
f"[{i+1}] {d.content}\n来源: {d.metadata['source']}"
for i, d in enumerate(docs)
])
prompt = RAG_PROMPT_WITH_CITATION.format(
numbered_context=numbered,
question=question
)
answer = llm.generate(prompt)
return answer, docs
3. 多轮对话
flowchart TD
A[用户问题] --> B{需要检索?}
B -->|是| C[检索相关文档]
B -->|否| D[直接对话]
C --> E[结合历史生成]
D --> E
E --> F[返回答案]评估指标
检索评估
| 指标 | 说明 | 计算方式 |
|---|---|---|
| Recall@K | 召回率 | 相关文档数 / 总相关数 |
| MRR | 平均倒数排名 | 1/首个相关文档排名 |
| NDCG | 归一化折损累积增益 | 考虑排序的相关性 |
生成评估
| 指标 | 说明 |
|---|---|
| 准确性 | 答案是否正确 |
| 相关性 | 是否回答了问题 |
| 完整性 | 信息是否充分 |
| 忠实度 | 是否基于检索内容 |
自动评估
def evaluate_faithfulness(answer, context, llm):
"""评估答案是否忠实于上下文"""
prompt = f"""判断答案是否完全基于给定的上下文。
上下文:{context}
答案:{answer}
评分(1-5):"""
score = llm.generate(prompt)
return int(score)
常见问题
Q: 检索不到相关内容?
| 原因 | 解决方案 |
|---|---|
| 分块太大 | 减小 chunk_size |
| 查询太短 | 查询扩展 |
| 语义差异 | 使用 HyDE |
| 数据不足 | 补充知识库 |
Q: 答案不准确?
| 原因 | 解决方案 |
|---|---|
| 检索结果差 | 优化检索 |
| 上下文不足 | 增加 top_k |
| Prompt 不好 | 优化提示词 |
| 模型幻觉 | 强调”基于上下文” |
Q: 响应太慢?
| 优化点 | 方法 |
|---|---|
| 检索 | 使用 ANN 索引 |
| 重排序 | 减少候选数量 |
| 生成 | 使用更快的模型 |
| 缓存 | 缓存热门问答 |
最佳实践
| 实践 | 说明 |
|---|---|
| 定期更新 | 知识库需要持续维护 |
| 质量监控 | 收集用户反馈 |
| 分类管理 | 按主题组织文档 |
| 权限控制 | 敏感文档访问控制 |
| 溯源展示 | 显示答案来源 |