ARTICLE 2026—0909FOLIO 2026—0909NOTE / 2026—0909ACRETIONDISKARCHIVUM PERSONAEOBSERVATION NOTES

把教材变成可以解题的本地知识库:一次医学 RAG 实现记录

从图片型医学 PDF、OCR、页码引用,到题型识别和多轮检索,我把一个本地教材检索库推进成了可以交给 Agent 解题的证据层。

把教材变成可以解题的本地知识库

前几天我做了一个很小、但我觉得很有用的项目:把《系统解剖学》第 5 版放进一个本地知识库里。

一开始的目标只是“能搜索教材”。但当我真的输入“肩关节的组成和特点是什么?”时,我发现只返回几段相似文本还不够——我更希望它能识别题型、找到多处证据,再把答案交给 Agent 组织出来。

所以这次开发,我把它从一个教材检索层推进成了一个面向医学题目的证据层:它不擅自编造医学结论,但可以为 Codex 或其他 Agent 准备带页码、章节和原文的作答材料。

1. 第一个问题:PDF 看起来有 368 页,但其实没有文字

原始 PDF 共 368 页。用 PyMuPDF 打开之后,页面可以正常显示,却几乎提取不到文字。这是典型的图片型 PDF:人眼能看,程序却拿不到文本层。

如果直接执行普通导入,程序会得到一个“看起来完成、实际上搜不到内容”的空索引。因此我先接入了 RapidOCR,把每页渲染成图片后识别,再保存成 Markdown。

处理结果保留了几类重要信息:

  • 原书页码;
  • 章节边界;
  • 正文;
  • 图注和图例;
  • OCR 页面平均置信度;
  • 原始页面与处理后文本的对应关系。

最终索引状态是:

书籍:系统解剖学 第5版(处理后教材)
原书页数:368
章节数:18
文本块:460
图片页:0

这里的“页码”指原书页码,而不是 18 个章节文件的数量。中间还修复了一个统计问题:结构化 Markdown 原先会被错误显示成 18 页,现在会从 ## 原书第 XX 页 中计算真实页数。

2. 只有关键词搜索,回答题目时还不够

最初的搜索主要是中文单字、字符片段和关键词出现次数。它对“肱骨”这样的查询还可以,但完整题目里经常有很多通用词:

位置、组成、特点、作用、结构、描述

这些词很容易把不相关章节顶到前面。

所以这次增加了一个题目分析层,放在:

medical_rag/qa.py

它会做几件事:

  1. 判断题目类型;
  2. 去掉“请简述”“下列说法正确的是”等考试套话;
  3. 提取核心医学概念;
  4. 生成多组互补查询;
  5. 对搜索结果去重和重排。

目前支持识别:

  • 选择题;
  • 填空题;
  • 名词解释;
  • 比较题;
  • 原因题;
  • 简答题。

例如:

为什么肩关节容易发生脱位?

会被拆成类似下面的检索方向:

完整题干
肩关节 脱位
肩关节
脱位

完整题干负责精确匹配,核心概念负责提高 OCR 场景下的召回率,单概念查询只作为补充,不让它轻易压过完整题干结果。

3. 新的 answer_question 做了什么

在 Library 中新增了:

Library.answer_question(question, limit=6)

它返回的不是一句未经验证的“标准答案”,而是一份 citation-first 的证据包:

{
  "status": "evidence_found",
  "plan": {
    "question_type": "short_answer",
    "concepts": ["肩关节"],
    "queries": ["肩关节的组成和特点", "肩关节"]
  },
  "evidence": [
    {
      "book": "系统解剖学 第5版(处理后教材)",
      "page": 53,
      "section": "02-关节学",
      "chunk_id": "...",
      "text": "...",
      "context": []
    }
  ]
}

每个证据块除了自己的原文,还会带上相邻页的文本块。因为简答题经常不是一句话就结束,组成、特点、运动和临床意义可能分布在连续几页里。

这个设计的边界也很明确:

如果本地教材没有返回证据,系统就应该说“知识库未检索到依据”,而不是编一个看起来合理的答案。

这次我还拿到了一张教材原题的现场截图。题目来自解剖学练习页,图片里的“外展肩关节”“胸上肢肌”“斜方肌”等内容,正好对应知识库需要处理的 OCR、题干和选项信息。

教材题目原始页面截图

4. 接入 MCP 后,Codex 就可以负责最后一步

项目原本已经有一个 MCP stdio server,提供:

list_books
search_medical_books
get_book_section

这次新增了:

answer_medical_question

在 Codex 中,完整流程变成:

用户输入题目
    ↓
answer_medical_question
    ↓
题型识别 + 多轮检索
    ↓
返回教材证据、页码和上下文
    ↓
Codex 根据 evidence 组织最终答案
    ↓
答案附《书名》、原书页码、章节和 chunk_id

我特意没有让本地 Python 服务直接“拍脑袋生成答案”。生成答案是 Agent 擅长的部分,而检索服务更应该专注于把证据找准、保存好、引用清楚。

5. 命令行现在也可以输入题目

普通检索仍然可以使用:

python -m medical_rag.cli search "肱骨的形态特点"

如果想走题目分析流程:

python -m medical_rag.cli answer "请简述骨的构造"

也可以输出 JSON:

python -m medical_rag.cli answer "为什么肩关节容易脱位?" --json

命令行版本目前输出的是证据包,适合交给其他 Agent。接入 Codex MCP 后,Agent 会继续根据证据生成自然语言答案。

6. PiDeck 也被我顺手记录下来了

这次开发时,另一个 Agent 中正好开着 PiDeck。我没有猜测它内部的对话内容,而是直接使用了这张现场截图:PiDeck 中已经出现了基于《系统解剖学》第 5 版处理后教材生成的参考答案。

PiDeck 中基于知识库解题的实际运行截图

这张图由我在实际运行过程中截取,保留了题目、参考答案、教材依据和模型运行信息;没有补写截图之外的对话内容。另一个 Agent 的 PiDeck 现场,至此也成为这次知识库开发的一部分。

7. 这次改动涉及哪些文件

知识库项目的核心代码位于:

medical_rag/qa.py
medical_rag/library.py
medical_rag/cli.py
medical_rag/mcp_server.py

同时增加了测试:

tests/test_medical_rag.py

当前测试覆盖:

  • 中文医学概念提取;
  • 选择题识别和选项提取;
  • 名词解释识别;
  • Markdown 教材导入;
  • 原书页数统计;
  • 题目检索;
  • 证据和相邻上下文返回。

测试结果:

Ran 3 tests
OK

8. 还没有解决的问题

这仍然不是一个可以替代教材和老师的系统。当前最明显的不足有:

  • OCR 仍会把部分图表、页眉和正文混在一起;
  • 中文医学词典和同义词扩展还不够完整;
  • 字符匹配对“意思相近但字面不同”的题目支持有限;
  • 目前还没有向量检索和 rerank;
  • 选择题还没有做到逐个选项的证据核对;
  • 命令行尚未接入本地模型,所以不会直接生成最终自然语言答案。

下一步比较值得做的是建立一组带标准页码的医学题目测试集,再引入本地向量索引。这样每次调整检索算法,都可以知道它到底有没有把真正相关的页面排到前面。

结语

我越来越觉得,知识库项目的重点不是“塞进去多少文本”,而是:

能不能找到正确的那一段
能不能说明它来自哪里
能不能在没有证据时停下来

对于医学内容尤其如此。现在这个项目还只是一个本地教材检索层,但它已经可以把一份图片型 PDF 变成 Agent 可调用、可追溯、可以用于题目分析的证据来源。

先让答案有出处,再让答案变得漂亮。这个顺序可能比直接接一个更大的模型更重要。

读完了FINIS这一页,显影完成

文章到这里结束本条目至此结束把这一刻留在纸上,再去看一看生活。