用了不下十个ChatPDF类的在线工具之后,我决定自己动手写一个免费的AI文档阅读器。原因其实很朴素:要么免费版只让传几十页,要么传第二份文件就开始弹付费墙,更不用说把合同、论文整个丢到别人服务器上之后,那种“数据到底被拿去干嘛了”的忐忑感。所以这个项目从一开始就定下两条死规矩:免费开源,而且可以完全在本地跑。把PDF、Word、图片丢进去,它能自动解析内容、做总结、围绕文档内容连续问答,还会尽量告诉你答案出自哪一页。
这个项目适合谁?两类人最合适:一种是日常要读大量论文、审合同、整理技术文档的普通用户,不想被在线工具的会员体系绑架;另一种是想入门RAG(检索增强生成)开发的技术朋友,可以直接把这份代码当成一个结构清晰的参考实现,改造成自己的知识库问答系统。接下来我从设计思路、核心模块、部署实操到坑位排查,把这个项目的里里外外都讲透。
1. 为什么我要亲手造这个轮子
1.1 在线ChatPDF工具的真实痛点
市面上那些“AI读PDF”的产品,其实解决的问题都一样:把非结构化的文档内容,变成可以查询的结构化知识。但我在实际使用中遇到了几个绕不开的问题。
第一是隐私和信任。把公司内部的技术方案、未公开的论文原稿传到第三方服务器,哪怕对方写着一百遍“数据加密传输”,你自己心里那关也过不去。尤其是法务同事审合同的时候,每上传一份文件我都能看到她在皱眉,那已经不是技术问题,是信任问题。
第二是用量限制。免费额度看起来很多,真正用起来完全不是那么回事。一份三四百页的PDF,分块嵌入之后消耗的token量非常可观,免费档很快就见底。更夸张的是有些产品限制文档总页数,扫描版书籍根本传不进去。
第三是功能天然受限。大多数在线工具只支持PDF,Word文档、图片截图、扫描件这些日常高频格式反而不支持。我经常收到同事发来的需求:“这个Word文档你帮我总结一下”,传到PDF工具里格式乱成一团。
第四是模型不可控。在线工具内置什么模型你就得用什么模型,没法切换更强的模型,也没法降级到更经济的方案。API的并发、限流、响应速度,完全是个黑盒。
1.2 这个项目定下的边界:本地优先、格式兼容、模型可换
既然决定自研,功能边界必须想清楚,不然一做起来就没完没了。我最终给项目定下了四个核心目标:
- 免费开源,所有代码都放在公开仓库里,任何人可以下载、修改、部署。
- 本地优先,默认情况下所有解析、嵌入、向量检索都在本地完成,不上传用户的文档内容。
- 格式兼容,支持PDF、Word、图片、纯文本这几类最常见的工作文档。
- 模型可换,既能接入商业大模型API,也能对接本地运行的Ollama模型,用户按自己的硬件条件和隐私要求选择。
为什么强调“本地优先”而不是“纯本地”?因为在实际使用中,本地模型的意图理解、长文本生成能力确实还有差距。我的思路是架构上做好抽象:用户如果愿意调用云端API获得更好效果,完全没问题;如果处理敏感文档,一键切到本地模型,所有数据不出机器。这种“可进退”的设计,比单纯要求用户必须在本地跑一个几十GB的大模型要务实得多。
很多人会问,既然有LangChain这类框架,为什么不直接用?我仔细评估过,LangChain的文档问答链路可以跑通,但它抽象层太多,出了问题排查链路过长,而且对中文PDF和扫描件的支持需要自己补大量的胶水代码。这个项目最终选择自己实现完整流程,原因很简单:我需要一个每一环都看得懂、改得动的代码库,而不是一个黑盒框架的配置文件。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体架构拆解:一条从文档到答案的流水线
2.1 核心流程:五个环节的接力赛
整个系统的核心流程,可以类比成一条文档处理流水线。文档进来之后,依次经过五个环节:
上传与解析。用户上传文件后,后端根据文件类型调用不同的解析器,把PDF、Word、图片转成纯文本。这一步的关键是保留页码信息,方便后面回答问题时能追溯到出处。
文本分块。解析出来的长文本不能直接塞给模型,因为模型有上下文长度限制,而且长文本里检索效率也低。需要把文本切成固定大小、有重叠的片段,每个片段带上页码标签。
向量嵌入。每个文本片段通过嵌入模型转换成向量,这相当于给每个片段生成了一个“语义指纹”。相近内容的片段,向量距离也相近。
向量检索。用户提问时,先把问题同样转成向量,然后在向量库中搜索与问题最相似的几个片段,作为参考上下文。
生成回答。把检索到的片段和用户问题一起交给大模型,由模型根据给定片段生成答案。
这五个环节串起来,就是最经典的RAG方案。为什么不用“把整篇文档直接扔给模型总结”的粗暴方案?因为长文档会直接击穿模型的上下文窗口,而且让模型在一篇几万字的文档里找答案,准确率会随着长度急剧下降。RAG的思路是先缩小范围,再精准回答,工程上更可控。
2.2 技术选型背后的取舍
直接列一下各模块的技术选型,然后解释为什么这么选。
| 模块 | 选型 | 理由 |
|---|---|---|
| 后端框架 | FastAPI | 轻量、支持异步、自带交互式API文档,调试方便 |
| 前端界面 | 原生HTML + JavaScript | 减少学习成本,不做重前端,方便二次开发 |
| PDF解析 | pdfplumber | 文本提取质量稳定,能精确获取页码位置 |
| OCR识别 | PaddleOCR | 中文识别效果好,能处理扫描版PDF和图片 |
| Word解析 | python-docx | 成熟稳定,能提取段落和表格 |
| 向量数据库 | FAISS | 轻量、内存索引速度快,适合单机部署 |
| 嵌入模型 | sentence-transformers | 生态成熟,中英文模型都有现成的 |
| 大模型接入 | OpenAI兼容API + Ollama | 统一接口,可以灵活切换云端模型或本地模型 |
后端选择FastAPI而不是Flask或Django,主要看中它天然支持异步处理。文档解析和向量化都是耗时操作,如果用一个同步框架,一个用户上传大文档的时候,其他请求全部卡死,这在真实使用中是灾难性的体验。FastAPI把耗时任务丢到后台线程池执行,API接口可以立刻返回一个处理任务ID,前端轮询进度,用户体验会好很多。
嵌入模型我推荐了一个重要选项:BAAI/bge-small-zh。这是一个针对中文优化的轻量级嵌入模型,参数量只有约1亿,但中文语义匹配的效果在同等体量模型里属于第一梯队。如果用户处理的是英文文档,也可以换成all-MiniLM-L6-v2。这里的关键是嵌入模型决定了检索质量的上限,大模型再聪明,检索到的片段不对,答案也不可能对。
大模型接入层我特意选择了OpenAI兼容接口格式,而不是绑定某一家厂商的SDK。原因很简单,现在几乎所有大模型服务商都提供OpenAI兼容接口,包括本地部署的Ollama。用统一的接口格式,用户只需要改base_url和model名字,就能在云端API、本地模型之间任意切换,这个设计在实际使用中非常省心。
向量库没用ChromaDB、Milvus这类重量级的系统,而是选了FAISS。原因很直接:这个项目定位是单机工具,不是大规模在线服务。FAISS直接嵌入Python进程,不需要单独维护一个服务,索引可以保存到本地文件,重启后直接加载,几十万条的文本片段都能轻松应对。对于个人文档库这个量级,上分布式向量库纯属过度设计。
3. 三个核心模块的落地细节
3.1 文档解析:PDF、Word、图片到底怎么读进去
文档解析是整个项目最容易翻车的地方,也是最体现工程经验的部分。我在第一版实现里吃了不少亏,最后沉淀出一套处理逻辑。
对于PDF,核心要区分两种类型:文本型PDF和扫描型PDF。文本型PDF里面有文字层,直接可以用pdfplumber提取。判断方法也很简单,先尝试用pdfplumber提取全文,如果提取出的有效字符数占页面内容比例过低,比如每页不到几十个字,基本就是扫描件,这时候就需要走OCR流程。
python复制import pdfplumber
def extract_text_from_pdf(pdf_path):
texts = []
with pdfplumber.open(pdf_path) as pdf:
for page_num, page in enumerate(pdf.pages, start=1):
text = page.extract_text() or ""
texts.append({
"page": page_num,
"text": text.strip()
})
return texts
对于扫描型PDF和图片,我统一交给PaddleOCR处理。这里有一个细节:OCR之前一定要先做图像预处理。实际测试下来,直接把扫描页喂给OCR,识别率大概在85%左右,但先做灰度化、对比度增强、必要时放大两倍,识别率能明显提升到92%以上。尤其是那些用手机拍的书页,原始图片光线不均,文字边缘发虚,预处理的效果立竿见影。
Word文档的处理同样有讲究。python-docx提取段落文本很容易,但很多人会忽略Word里的表格,因为表格里的关键信息量通常非常大。我的处理方式是:段落按顺序提取,每个表格在提取时转换成“以竖线分隔的文本行”,插在表格出现的位置。这样既能保留表格语义,又不会打乱文档原有的阅读顺序。
3.2 文本分块与向量检索:细节决定问答质量
解析出来的纯文本不能直接用,必须做分块。这里有两个关键参数:块大小和块重叠。块大小决定每个片段包含多少信息量,块重叠决定相邻片段之间的语义衔接。
我实测下来,针对中文文档,chunk_size设为500到800个字符,overlap设为100到200个字符,效果比较均衡。块太小,语义不完整,检索到的片段信息量不足;块太大,片段内噪音太多,而且超出嵌入模型的最大输入长度。overlap的作用是避免一个完整句子的后半部分被截到下一个片段里,检索时能减少前后语境的断裂感。
每个文本片段入库前,我还会在片段内容前拼接一个元信息前缀,格式是这样的:
text复制[页码] 第3页
(正文内容)
这样做的目的有两个:一是检索时如果关键词恰好出现在元信息里,可以提升该片段与问题的匹配度;二是把页码跟随正文一起存储,最终生成回答时,模型能看到页码信息,回答中就能引用来源页码。
向量检索时,我默认取top_k=5,也就是把与问题最相似的5个片段作为上下文。这个数字不是拍脑袋定的,做过对比实验:top_k设1到3,上下文太单薄,模型容易答非所问;设到8以上,无关片段变多,反而分散模型注意力,影响回答精度。5是一个在信息量与准确性之间相对平衡的值。
3.3 问答生成:提示词如何约束模型不要乱说
生成接入了大模型之后,最大的挑战不是让它说得更多,而是让它不要说得太离谱。RAG系统最怕的幻觉问题,绝大部分可以通过提示词约束来缓解。我实际使用的提示词模板是这样的:
text复制你是一个文档阅读助手。请基于用户提供的文档片段回答问题。
规则:
1. 优先引用片段中的原文,并标注出处页码,格式为[第X页]。
2. 如果片段中没有足够信息,直接回答“文档中没有找到相关内容”,不要编造。
3. 回答用中文,简洁准确。
4. 如果需要归纳,只能归纳片段中出现的信息。
文档片段:
{context}
用户问题:
{question}
这版提示词在实践中效果不错。关键点有两个:一个是明确要求“不要编造”,一个是强制输出页码。加上页码这个要求后,回答的可信度提升非常明显,用户看到答案时能直接翻到对应位置验证,对工具的信任感会强很多。
上下文组装顺序也有讲究。从向量库检索出来的5个片段,按照它们与问题的相似度从高到低排列,拼进context占位符。模型会优先关注前面的内容,所以把最相关的内容放在前面,能有效提升回答精度。
4. 部署实操指南:从零到第一次问答
4.1 环境准备与依赖安装
部署过程我尽量控制在十分钟以内。前置条件只有两个:Python 3.9以上版本,以及一个可以联网安装依赖的环境。建议用虚拟环境,避免把依赖装到全局Python里。
bash复制git clone https://github.com/yourname/ai-doc-reader.git
cd ai-doc-reader
python -m venv venv
source venv/bin/activate # Windows用 venv\Scripts\activate
pip install -r requirements.txt
requirements.txt主要包括这些核心库:
text复制fastapi
uvicorn
python-multipart
pdfplumber
python-docx
paddleocr
paddlepaddle
sentence-transformers
faiss-cpu
openai
python-dotenv
这里有个常见坑:PaddlePaddle和FAISS的安装在某些环境下可能会因为Python版本或系统架构问题失败。我的经验是优先用pip安装官方预编译包,不要从源码编译。如果Python是3.12以上,个别老版本库可能还没有适配,建议先用Python 3.10或3.11。
4.2 配置:API接入与本地模型二选一
项目使用.env文件管理配置,复制一份示例文件后按需修改即可。
bash复制cp .env.example .env
完整配置项长这样:
dotenv复制# 大模型提供商:openai 或 ollama
LLM_PROVIDER=openai
# 使用OpenAI兼容API时配置
OPENAI_API_KEY=sk-your-key
OPENAI_BASE_URL=https://api.example.com/v1
OPENAI_MODEL=gpt-4o-mini
# 使用Ollama本地模型时配置
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen2.5:7b
# 嵌入模型
EMBEDDING_MODEL=BAAI/bge-small-zh
TOPK=5
CHUNK_SIZE=750
CHUNK_OVERLAP=150
两种模式怎么选?我的建议很直接:如果你电脑有独立显卡或者内存16G以上,优先试试Ollama模式,完全离线运行,隐私性最好,qwen2.5:7b这种量级的模型在文档问答场景下效果已经足够。如果你的机器跑不动大模型,或者追求更高质量的答案,那就用OpenAI兼容API指向你喜欢的云端模型。
切换模式只需要改LLM_PROVIDER这一个值,其他配置自动适配。这个弹性设计让项目适应面变得很广。
4.3 启动项目与新手验证路径
配置完环境后,执行启动命令:
bash复制python main.py
看到类似“Uvicorn running on http://localhost:8000”的日志后,用浏览器打开http://localhost:8000,就能看到上传界面。
新手第一轮测试,建议按这个路径走:先传一份十来页、带目录和章节标题的PDF,等进度条走完,依次试三个问题:“这篇文章主要讲了什么?”、“第三章的核心观点是什么?”、“在第X页提到了什么关键数据?”。
这三个问题分别验证三种能力:全文总结能力、定向章节查找能力、按页码精准定位能力。如果三个问题都能给出合理回答,说明整个链路是通的。我建议不要一上来就传上千页的大部头,链路还没熟悉前,先用小文档跑通全流程,排查问题效率最高。
5. 我踩过的坑和排查思路
5.1 现象级速查表:从现象直接定位根因
开发和使用过程中我遇到了不少问题,整理成一张速查表,按现象、可能原因、解决办法三列列出,遇到问题时直接对照。
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 上传PDF后提示解析失败 | PDF加密或损坏 | 去掉密码保护或用工具重新导出PDF |
| 扫描版PDF识别不出文字 | OCR依赖未安装 | 确认已安装paddleocr和paddlepaddle |
| OCR结果乱码严重 | 图片质量差、光线不均 | 先做灰度化、增强对比度、放大预处理 |
| 回答明显答非所问 | 分块过大或检索数量过少 | 调小CHUNK_SIZE,调大TOPK |
| 嵌入模型首次加载很慢 | 模型文件体积大,首次需要下载 | 提前手动下载模型文件放到缓存目录 |
| 大文档处理时内存暴涨 | 一次性把整个文档加载进内存 | 改为按页流式处理,释放已完成页面的变量 |
| 用Ollama模式回答延迟高 | 本地模型推理速度不够 | 换更小的量化模型,或改用API模式 |
| 同一问题多次回答不一致 | 大模型温度参数过高 | 把temperature降到0.2以下 |
这里重点说两个排查思路。第一个是“先判断问题出在解析层还是生成层”:如果检索出来的片段本身就不对,改提示词也没用;如果片段对但回答错,那才是模型的问题。我调试时习惯先打开后端的日志接口,直接查看每次提问时检索到的5个片段内容,一眼就能定位问题在哪一层。
第二个是分块参数的调整逻辑。如果你发现模型回答的内容跟文档相关但不够准确,优先检查是不是块太大导致片段里混了太多无关信息。如果你发现某个问题明明文档里有答案,但检索不到相关内容,优先检查是不是块太小导致关键信息被切碎,或者overlap设置不够导致语义断裂。这两个参数是问答质量最核心的调节旋钮。
5.2 几条从坑里爬出来的经验
第一,先验证解析层,再调AI参数。很多刚开始做RAG的朋友,回答效果不好就急着调模型、调提示词,结果折腾半天发现是PDF解析出来全是乱码,或者Word里表格内容根本没提取进去。基础数据是脏的,上面花再多功夫都是白搭。所以我把解析模块单独做了个命令行调试工具,可以随时随地打印任意文档的解析结果,这个调试路径帮我省了无数时间。
第二,不要迷信大模型,要把系统控制权掌握在自己手里。最初版本的返回逻辑完全依赖模型自由发挥,偶尔会出现荒谬答案。后来我强制要求模型按规则输出,如果检索到的片段与问题的相关度低于某个阈值,我直接在代码层拦截,告诉用户“文档中没有找到相关内容”,而不是让模型硬答。这一条改进,让系统的可靠性有了质的飞跃。
第三,向量索引要落盘。第一版实现里,每次启动都会重新解析文档、重新计算嵌入向量,处理一份几百页的PDF要等好几分钟。后来我把解析结果和FAISS索引都持久化到本地,按文档ID做缓存,二次打开同一份文档只需要加载索引,秒级完成。这一步优化对日常使用体验的提升非常明显,尤其适合经常盯着同一批文档反复工作的场景。
第四,嵌入模型的选型要跟文档语言匹配。项目默认支持中英文,但如果你主要处理中文文档,强烈建议用bge或m3e这类中文优化模型,不要直接拿英文嵌入模型处理中文文本。我用一个通俗的方式解释:嵌入模型相当于给每个文本片段贴“语义标签”,英文模型学过的标签体系里没有多少中文概念的位置,贴出来的标签自然不准,后续检索质量会大打折扣。
最后分享一个小技巧
项目上线一版之后,我收到最多的使用反馈,不是“识别率怎么提升”这类技术问题,而是“我想让AI读我手机上拍的一堆纸质笔记”。这个场景之前的版本完全没有覆盖。后来我在图片解析入口加了一个简单的批处理模式:一次上传多张图片,自动按拍摄时间排序,拼接成一份虚拟文档后进行问答。实现起来其实不复杂,但对真实用户的帮助非常大。
如果让我重写一遍这个项目,我大概率会把精力优先投入到两件事上:一是把文档解析层再做厚一点,支持更多格式比如EPUB和Markdown目录解析;二是给前端加一个“查看检索证据”的交互面板,让用户每次看到答案时都能直接确认依据来源。如果你准备在自己的场景中使用这个项目,我建议也从这个角度入手去做定制,而不是把时间花在调模型上——数据进得来、证据看得见,这两点才是AI文档阅读器真正让人愿意长期用下去的核心。
