1. 背景与痛点
在构建私有知识库(RAG)的过程中,我们经历了从“跑通”到“好用”的艰难跨越。早期的 Naive RAG(朴素 RAG)方案在面对真实业务文档时暴露出了明显的缺陷:
- 表格数据丢失:传统的 PDF 转文本会破坏表格结构,导致大模型无法统计数据(如“获奖次数”统计错误)。
- 上下文破碎:为了检索精准度切分过细,导致大模型看不到完整的段落逻辑。
- 实体张冠李戴:检索无法区分“某部门奖励”和“某实验室奖励”,导致召回大量噪音。
- 逻辑推理疲软:小参数模型(7B/8B)在处理多文档冲突时容易产生幻觉。
为了解决上述问题,我们基于 Windows + AMD ROCm 硬件环境,利用 Ollama + Qwen3-14B,构建了一套结构感知型(Structure-Aware)高级 RAG 系统。
2. 系统架构总览
本系统采用 Modular Advanced RAG 架构,核心理念是“精准召回,全局审计”。系统不依赖外部 API,全本地运行,保障数据隐私。
技术栈
- 计算底座:Windows + AMD ROCm (PyTorch)
- 大模型 (LLM):Qwen3-14B (Ollama) —— 逻辑推理核心
- Embedding:Qwen3-Embedding —— 中文语义向量
- 编排层:Python (Custom Implementation) + LangChain Core
- 解析层:PyMuPDF4LLM (Markdown)
- 存储层:FAISS (子文档向量) + Pickle (父文档存储)
3. 核心模块实现细节
3.1 数据解析层:Markdown 重构 PDF
痛点:pypdf 等工具将 PDF 解析为纯文本流,表格被压扁,大模型无法理解行与列的对应关系。
解法:使用 PyMuPDF4LLM 将 PDF 转换为 Markdown 格式。
class MarkdownPDFLoader:
def load(self):
# 将 PDF 转换为 Markdown,完美保留表格(| Col1 | Col2 |)
text = pymupdf4llm.to_markdown(self.file_path)
return [Document(page_content=text, metadata={"source": self.file_path})]
效果:表格变成了 Markdown 语法,大模型可以轻松通过表头进行数据统计。
3.2 索引层:手写父子索引 (Parent-Child Indexing)
痛点:检索需要切片小(精准),生成需要切片大(完整)。
解法:实现父子文档策略。
- 父文档:2000 字符的大块(包含完整表格),存入磁盘(Pickle)。
- 子文档:400 字符的小块,存入向量库(FAISS)。
- 联动:检索时匹配子文档,返回时映射回父文档。
注:为了解决 LangChain 依赖冲突问题,我们手动实现了 ManualParentRetriever 类。
3.3 检索层:多路召回与硬过滤 (The Gatekeeper)
这是本系统最核心的“防噪”机制。
A. 智能多路查询 (Multi-Query)
LLM 不再直接搜索用户的问题,而是先生成 3 个维度的查询词:
- 广度查询:仅包含实体(如“某实验室”),确保召回率。
- 精度查询:实体+意图(如“某实验室 2025 奖励”)。
- 同义改写:覆盖不同说法。
B. 实体硬过滤 (Entity Hard Filtering)
为了解决“检索到无关部门”的问题,我们引入了 Gatekeeper 机制。
- 提取:LLM 从问题中提取核心实体(如
['某实验室'])。 - 过滤:在广谱召回(Top 50)后,强制剔除不包含核心实体的文档。
def _filter_docs(self, docs, keywords):
# 强制白名单机制
must_have = keywords.split(',')
filtered = [d for d in docs if any(k in d.page_content for k in must_have)]
return filtered if filtered else docs[:5] # 回退策略
3.4 排序层:深度重排 (Reranking)
经过硬过滤后的文档,使用 FlashRank 进行语义打分。
- 模型:
ms-marco-MiniLM-L-12-v2 - 策略:从 50 个候选集中选出最相关的 Top 8-10。
- 上下文重组:使用
LongContextReorder将高分文档放在上下文的首尾,防止 LLM 的“中间迷失”现象。
3.5 生成层:审计师模式 (Auditor Persona)
痛点:大模型容易数错数(重复计算),或回答啰嗦。
解法:设计 CoT(思维链)Prompt。
Prompt 策略:
- 角色:严谨的数据审计师。
- 步骤:先列出证据,再去重,最后统计。
- 格式:结论先行 (BLUF)。
要求:
1. 全局审计所有表格和段落。
2. 结论先行:第一句话直接给出统计结果。
3. 详细列出证据来源,并注意去重(如同一个人在不同片段出现,只算一次)。
4. 性能优化与工程实践
4.1 速度优化
为了在本地显卡上获得流畅体验,我们将流程进行了合并:
- Old:提取实体 -> 改写查询 -> 检索 -> 生成 (3次 LLM 调用)
- New:智能分析 (提取+改写) -> 检索 -> 生成 (2次 LLM 调用)
- 收益:响应速度提升 40%。
4.2 全链路日志
构建了自动日志系统,每次对话生成 logs/问题_时间.txt。
- 记录 Query Optimizer 的中间输出(检查改写是否准确)。
- 记录硬过滤前后的文档数量(检查是否误杀)。
- 记录最终喂给 LLM 的 Context(检查是否包含正确答案)。
4.3 硬件适配
针对 AMD 显卡在 Windows 上的兼容性问题,在代码入口处注入 PyTorch 分布式补丁:
if not hasattr(torch.distributed, 'is_initialized'):
torch.distributed.is_initialized = lambda: False
5. 效果对比
| 测试场景 | Naive RAG (基线) | Pro Max RAG (本系统) |
|---|---|---|
| 表格统计 | 错误 (表格乱码) | 正确 (Markdown 解析生效) |
| 模糊查询 | 漏召回 | 召回成功 (Multi-Query 生效) |
| 实体区分 | 混淆不同部门数据 | 精准隔离 (硬过滤生效) |
| 多人名去重 | 重复计数 (5人) | 正确去重 (4人, CoT生效) |
6. 总结
本系统证明了在不依赖 OpenAI 等外部 API 的情况下,仅靠本地算力(Qwen3-14B)和精细化的工程设计(Parent-Child, Hard Filtering, Markdown Parsing),完全可以构建出企业级的 RAG 应用。
核心经验:RAG 的上限不在于模型有多大,而在于数据清洗的质量和检索链路的控制力。