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 个维度的查询词:

  1. 广度查询:仅包含实体(如“某实验室”),确保召回率。
  2. 精度查询:实体+意图(如“某实验室 2025 奖励”)。
  3. 同义改写:覆盖不同说法。

B. 实体硬过滤 (Entity Hard Filtering)

为了解决“检索到无关部门”的问题,我们引入了 Gatekeeper 机制。

  1. 提取:LLM 从问题中提取核心实体(如 ['某实验室'])。
  2. 过滤:在广谱召回(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 策略

  1. 角色:严谨的数据审计师。
  2. 步骤:先列出证据,再去重,最后统计。
  3. 格式结论先行 (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 的上限不在于模型有多大,而在于数据清洗的质量检索链路的控制力