前后花了两周时间,从零搭了一套跑在内网的私有化企业 RAG 知识库。起因很简单:公司手里的产品手册、技术规范、项目验收文档越来越多,几千份资料散在各个共享盘里,找人问不如翻文档,翻文档不如问 AI。但数据敏感,明文往外传是不可能的事情,甲方那边也明确提出“人过留痕、数据不出域”,所以采购方案直接被否掉,只能自己造轮子。
这套系统现在每天跑着几十万条向量检索,支撑着产品、售前、实施三个部门的知识问答。整个过程踩了不少坑,尤其是“看起来能跑”和“真正好用”之间的距离,远比你想象的大。这篇把完整架构、技术选型、核心链路和踩坑复盘都写出来,给后面准备做私有化 RAG 的团队一点参考。
1. 为什么自建而不采购:私有化RAG的立项逻辑
1.1 需求源头:数据出不去,能力要落地
一开始业务方的需求描述很简单:“我们想把所有产品文档做成一个能对话的智能客服,员工问什么它答什么。”听起来简单,拆开之后全是约束条件。
数据方面,公司内部文档包含客户合同条款、内部网络拓扑、未公开的产品规划,这些内容别说上传到公共大模型平台,就连内部群聊分享都有安全审批流程。合规层面要求所有处理和存储必须在公司内网完成,不能依赖任何外部 API 调用。这意味着整条链路——文档解析、向量化、检索、生成——全部要本地化。
场景方面,使用人群不是技术背景的人。售前同事问的是“这个方案能不能跑在国产化服务器上”,实施同事问的是“某个接口的超时参数在哪里调”,他们不会写复杂查询语句,只会用自然语言提问。这就要求检索链路对口语化表达、同义替换、指代消解都有一定容忍度。
规模方面,文档以小几千份为起点,但考虑到后续还会接入售后工单、竞品分析、FAQ,设计目标要支持百万级 chunk 量级,不能从一开始就做成玩具。
1.2 两周节奏怎么排:时间线规划与阶段性目标
两周时间听起来紧张,但只要有清晰的目标拆解,实际是够用的。我的排期是这样的:
- 第 1-2 天:需求梳理和选型,确定模型、向量库、框架方向,同时把评测集建起来。
- 第 3-6 天:文档解析与切分管道,这是最枯燥但最关键的一步。
- 第 7-9 天:向量库部署、检索链路开发,跑通“检索召回”环节。
- 第 10-12 天:生成链路、Prompt 组装、前端问答界面和权限过滤。
- 第 13-14 天:联调、性能测试、处理并发和缓存问题,同步做检索评测回归。
这里有个非常重要的教训:头两天必须先建评测集。哪怕是人工整理 50-100 组“问题-标准答案片段”,后面所有优化才有判断基准。不然检索参数调来调去,全凭感觉,永远不知道改对了还是改差了。
2. 整体架构:一条从文档到答案的完整链路
2.1 分层设计:接入层、处理层、检索层、生成层
整套系统从结构上分为四层,每层职责单一,互不依赖,方便后续单独替换组件。
接入层负责两件事:一是员工问答的前端界面,二是管理员的知识库管理后台。界面要求支持流式输出和引用来源展示,后台则负责文档上传、解析状态查看、测试问答。权限模型也在这层落地,按部门做知识库隔离,A 部门的人只能检索到 A 部门授权过的文档。
处理层是离线管道,处理所有入库文档。上传后的文件先做格式解析,变成干净文本,再按章节结构切成 chunks,每段通过嵌入模型生成向量,最后连同原文和元数据一起写入向量库。这个过程支持增量执行,文档有更新时只重跑对应文件。
检索层是在线链路的核心,用户提问后先把问题向量化,同时做一次关键词检索,两部分结果融合截断,再经过重排模型精排,最终从向量库取回最相关的 Top-K 段落。
生成层负责把相关段落和用户问题组装成预设格式的提示词,交给本地部署的大语言模型,生成带引用标注的回答。所有问答日志都会落库,方便后续做质量分析。
2.2 数据流梳理:一次提问要走多远
一次真实提问的处理路径是这样:用户在界面输入问题,请求先经过权限中间件,确认这个用户对哪些知识库有访问权;然后问题被并行发送给两个检索器——向量检索器把问题转成向量去向量库做 ANN 检索,关键词检索器(BM25)直接对文本倒排索引做精确匹配。
两组结果通过 RRF(Reciprocal Rank Fusion)算法融合成一个有序列表,截断到 Top 100;这 100 个候选段落实在太长,直接塞给生成模型既浪费 token 又稀释注意力,所以需要一个重排模型在中间把粒度从“相关”精修到“高相关”,最后只保留 Top 5。
这 5 个段落连同各自的来源元数据(文档名、页码、块序号)拼装成提示词,送进生成模型,模型逐字输出答案,前端以流式效果呈现,回答末尾附上引用来源列表。用户点击来源可以直接跳到原文对应位置。
2.3 组件清单:每个环节放什么东西
最终选型形成的组件清单:
| 环节 | 组件 | 作用 |
|---|---|---|
| 文档解析 | PyMuPDF + LibreOffice + PaddleOCR | PDF 文本提取、Office 转 PDF、扫描件 OCR |
| 文本切分 | 自研结构感知切分器 | 按标题层级切块,保留上下文 |
| 嵌入模型 | BGE-M3 | 中文语义向量 + 稠密/稀疏混合能力 |
| 向量库 | Milvus | 向量存储、ANN 检索、元数据过滤 |
| 关键词检索 | Milvus 内置 BM25 或 Elasticsearch | 精确匹配兜底 |
| 重排模型 | BGE-Reranker-v2-M3 | 精排候选段落 |
| 生成模型 | Qwen2.5-14B-Instruct | 最终问答生成 |
| 推理部署 | vLLM | 高并发 ServedLLM 推理 |
| 编排框架 | 自研 Python 管道 | 串联整条链路 |
每个组件都是踩过坑之后留下来的,具体为什么是它们,下一节展开说。
3. 技术选型:哪些组件值得自己写,哪些直接用现成的
3.1 模型服务:Embedding 与生成模型的分工
先聊最重的两块:嵌入模型和生成模型。很多人一开始纠结“应该选什么大模型”,我的观点是,先定嵌入模型,再定生成模型。因为 RAG 的上限主要由检索决定,检索不行的话,生成模型再强也只能“一本正经地胡说八道”。
嵌入模型选的 BGE-M3。原因很直接:中文效果好、支持 8192 长度、同时输出稠密向量和稀疏向量,稠密向量管语义匹配,稀疏向量管关键词精确匹配,一套模型干了两件事,省掉单独维护两套向量的麻烦。实际测下来,在内部文档上的 hit rate@5 稳定在 78% 以上,比之前试的几个通用模型高不少。
生成模型最终选了 Qwen2.5-14B-Instruct。之前也纠结过要不要上 7B 模型,因为服务器只有一张 16G 显存的卡。但实测同一批 100 个评测问题上,7B 模型有相当一部分回答缺乏归纳能力,会直接复读检索段落;14B 模型的归纳、改写、引用标注能力明显高一档。幸运的是通过 4-bit 量化跑 vLLM,14B 模型能把显存压到 12G 左右,生成速度和效果都能接受。
这里有一个很多团队容易误解的点:部署大模型不是显存够就完事了,还要考虑并发。Ollama 的默认行为是“随用随加载、空闲卸载”,单用户测试没问题,但几个用户同时提问时,模型换入换出会导致首 token 延迟飙到几十秒,体验极差。后来统一用 vLLM 做常驻服务,配合 continuous batching,并发提升明显。如果项目规模小、就几个人用,Ollama 确实是最快的启动方式;但只要是部门级及以上使用,建议直接上 vLLM。
3.2 向量库选择:Milvus / Qdrant / pgvector 的取舍
向量库是 RAG 的地基,选错了后面迁移成本很高。我在这块比较了很久,也做了一轮对比:
| 对比项 | Milvus | Qdrant | pgvector |
|---|---|---|---|
| 部署复杂度 | 中高(依赖 etcd 等组件) | 低(单进程即可) | 极低(复用现有 PG) |
| 亿元级向量能力 | 强,支持分布式 | 较强,单机可扛较高规模 | 较弱,适合百万级以内 |
| 元数据过滤 | 强,支持复杂过滤 | 强 | 需配合 SQL |
| 混合检索 | 内置 BM25,支持稠密+稀疏 | 需自行实现 | 不支持 |
| 多租户隔离 | partition key 方案成熟 | filter 方案也可以 | filter 方案也可以 |
我们选 Milvus 不是因为它最流行,而是需求里有两个硬性要求:一是后续可能增长到百万级向量,二是有多部门数据隔离的需求,Milvus 的 Partition Key 可以在检索时直接按 partition 裁剪数据,速度和隔离性都比纯 filter 扫全量好。如果你们的场景是单知识库、数据量在百万级以内,Qdrant 单机部署会更省心,运维成本确实低一截。
3.3 编排框架:自研 Pipeline 还是用社区平台
市面上现成的开源框架不少,Dify、MaxKB、RAGFlow 我都快速试了一遍。说实话,它们做 Demo 非常快,半天就能跑通一个问答页面,内置的工作流和知识库管理也够日常用。但我们最终没有依赖它们作为主链路,原因主要有三点。
第一,权限模型不够灵活。我们要求按部门、按文档目录做两级隔离,Dify 的应用级权限可以做,但要做到“同一知识库内不同用户只能看到不同分片”就比较吃力。第二,切分策略是效果的核心竞争力,现成平台大多只提供固定窗口或简单递归切分,对技术手册里的“章节”“代码块”“表格区域”感知很弱,这正好是我们在文档层面积累的价值。第三,黑盒里的链路透明性不足,出了检索质量问题,需要能逐层排查召回、重排、提示词各部分,自研管道加日志全链路打点更可控。
不过这块要客观说一句:自研不等于从零造轮子。向量库用 Milvus,模型服务用 vLLM,解析层用成熟库,我们自研的只是中间的“胶水逻辑”——切分策略、检索编排、评测回归、权限过滤。研发量可控,两周内能做完主链路也验证了这个思路是对的。
4. 文档解析与切分:RAG 效果的地基
4.1 从 PDF 到干净文本:解析链路与 OCR 兜底
经历了这次项目之后,我越来越认同一个观点:RAG 项目的难点根本不在模型,而在文档。如果解析出来的文本是脏的,后面检索、生成做得再好,也都是从垃圾堆里找素材。
我们文档库里格式最麻烦的是 PDF。有些 PDF 本身带文字层,直接提取即可,但内部很多扫描件和打印盖章件,必须走 OCR。这条链路的最终方案是:所有 PDF 先用 PyMuPDF 提取文本和坐标信息,同时把每页渲染成图片;如果文本提取结果里有效字符密度低于阈值,自动触发 PaddleOCR 从图片重新识别。
这里有个经验:OCR 之后不要直接丢给切分器。扫描件的识别结果经常带着奇怪的全角空格、错误标点、断行的表格线,我们做了一个清洗管道,统一做全半角转换、去除多余空白、修复断行、合并被切断的段落。清洗前后的命中率差距非常明显,粗测大约能拉开 8-10 个百分点。
Office 文件(Word、PPT、Excel)的处理也踩了坑。直接解析 .docx 可以拿到 XML 里的正文,但 .doc 老格式、以及大量排版复杂的 PPT 很不稳定。最后统一用 LibreOffice 的无头模式转成 PDF,再走 PDF 链路,省掉了维护多套解析器的成本。代价是转换速度和偶尔的样式失真,但换来的是稳定的输入格式,值得。
4.2 切分策略:固定窗口、结构切分与语义切分
切分是整个 RAG 里最容易被低估的一步。很多人上来就是“固定 512 个字符一刀切”,结果把技术手册里的“步骤 2 与步骤 3”硬生生拆到两个段落里,检索命中一半内容,生成结果自然残缺。
我测试了三种切分策略:
- 固定窗口切分:按字符数硬切,设置 overlap。实现最简单,但会切断句子、标题、表格,产生大量语义残缺块。
- 结构感知切分:先识别文档的标题层级(通过 PDF 字体大小、Word 样式、Markdown 标题),在每个章节内再做段落级切分,如果段落过长再按句子边界补充切分。这种策略对技术文档、规范类文本最友好。
- 语义切分:通过嵌入模型计算相邻句子的相似度,找到语义转折点作为切分边界。效果不错,但计算量大,对短文档容易过度切分。
最终主用的是结构感知切分。内部文档特点鲜明:有明确的章、节、条款层级,按标题锁定边界,块内整合多个段落,这样每个 chunk 都有相对完整的主旨,检索命中时上下文足够。说实话,语义切分虽然听起来“聪明”,但在我们这种以结构化文本为主的场景里,反而没有结构切分稳。
4.3 chunk 大小与 overlap 的实测对比
切分参数直接影响检索质量。我专门用 100 个评测问题做了参数对比,结果如下:
| 参数配置 | hit rate@5 | 平均答案完整度(人工打分) | 说明 |
|---|---|---|---|
| chunk=128, overlap=16 | 71% | 6.8/10 | 召回精确但上下文不足,答案容易缺前提 |
| chunk=256, overlap=32 | 79% | 8.1/10 | 平衡点,多数场景推荐 |
| chunk=512, overlap=64 | 76% | 7.4/10 | 块内容太多,注意力分散,且浪费 token |
| 结构切分,可变长度 | 84% | 8.8/10 | 跟随文档原本的逻辑边界,效果最好 |
chunk 太小的坏处比想象中严重。128 字时检索命中的段落往往只覆盖答案的一部分,比如文档里是“配置参数分为三类:A 表示超时时间,B 表示重试次数,C 表示 ...”,切成块之后可能只留下“A 表示超时时间”,模型看到的信息不完整,自然答不全。所以我们最终走的是结构切分 + 上限 512 + 下限 64 的约束逻辑,既能跟着章节走,又不会让块太大。
5. 检索链路优化:召回、融合与重排
5.1 纯向量检索的局限:为什么必须加 BM25
纯向量检索在大多数语义相似场景表现不错,但有一个致命问题:对“符号型”查询不敏感。内部文档里大量出现型号编号、接口路径、错误码,比如“R-1024”“cfg_timeout”“ORA-12170”,这些词在语义空间里几乎没有正规分布,向量检索要么完全召回不到,要么把“R-1024”和“R-1204”混淆成相近向量。这种精确匹配场景,传统关键词检索反而更可靠。
因此检索层做的是双路召回。向量检索负责“语义扩展”,BM25 负责“精确锁定”。实操上,BGE-M3 本身就能输出稀疏向量,Milvus 的 BM25 检索可以直接挂在同一个模型后面,不需要额外升级 Elasticsearch,检索时两路并行查询,再用 RRF 融合。
5.2 RRF 融合的细节:分数归一化与权重
两路检索结果如何合并,常见的选择有:分数加权求和、Convex Combination、RRF。我实测下来 RRF 最稳,它不看绝对分数,只看排名位置,避免向量分数和 BM25 分数量纲不统一的问题。
RRF 公式是 score(d) = Σ 1/(k + rank(d)),k 通常取 60。每个文档在多个结果列表中都有排名位置,融合后取总分排序。这个公式的好处是对异常分数不敏感,也不会因为某一路检索引擎抽风全输出 0 分导致整体失效。我们还加了两个细节:一是给两路结果加权重,因为内部文档语义匹配更重要,向量路权重略高于 BM25 路;二是对完全重复的内容块做去重,避免同一文档的多个切块霸榜。
5.3 重排模型:用 BGE-Reranker 把精度再拉一个档
召回阶段的目标是“漏得少”,Top 100 里可能有一半是弱相关;但生成阶段给模型的东西必须“精”。所以召回之后还有一道重排,把列表从 Top 100 精修到 Top 5-8。
重排模型和嵌入模型是两回事。嵌入模型把文本压成一个向量,追求“大致差不多”,重排模型则直接同时读入 query 和 passage,输出一个相关分数,精度更高但速度慢。我们用的是 BGE-Reranker-v2-M3,每次推理的耗时会随候选长度上升,所以重排前必须用 RRF 先粗筛到 50 条以内。
加完重排这一层之后,评测集上的 hit rate@5 大约又提升了 5-6 个百分点,问答结果的可信度提升明显。这里提醒一句:重排模型也是资源消耗点,建议做查询级缓存,同一问题在短时间窗口内直接命中缓存结果,能省掉大量重复计算。
6. 踩坑实录:两周里最耗时的五个问题
6.1 集合 Schema 定错,推倒重来:先想清楚元数据再建表
这是第一个大坑,直接浪费了一整天。Milvus 里创建 Collection 时,主键、向量字段、标量字段的 schema 是固定的,后面想加字段,得删掉重建,向量需要全部重新导入。
我当时只设计了 id、embedding、chunk_text、document_id 几个字段,等到做权限隔离时发现每个 chunk 需要记录所属部门、文档来源、上传时间、文件类型等多个标量字段做过滤条件,而集合已经灌了几十万条数据。想改?没门,只能把集合删掉重建,几十万条向量重新走了一遍嵌入模型推理,白白耗掉大量 GPU 时间。
现在回头总结,建库前必须先把这张表设计清楚:
| 字段 | 类型 | 说明 |
|---|---|---|
| pk | INT64 | 主键,自动生成 |
| embedding | FLOAT_VECTOR | 向量,维度对齐嵌入模型 |
| chunk_text | VARCHAR(8192) | 切分后的文本内容 |
| document_id | VARCHAR(64) | 所属文档 |
| doc_name | VARCHAR(256) | 文档名,用于引用展示 |
| page_no | INT32 | 页码 |
| department | VARCHAR(64) | 部门隔离标签,用于 partition key |
| file_type | VARCHAR(20) | PDF/Word/PPT |
| upload_time | INT64 | 时间戳,用于增量清理 |
元数据字段的设计,一定要从检索过滤和引用展示两个方向倒推,否则后面肯定返工。
6.2 多模型并发导致显存溢出
项目到集成测试阶段,我把嵌入模型、重排模型、生成模型全部部署在同一台 16G 显存的机器上,然后启动服务。第一个问题,显存直接 OOM。原因是 vLLM 和嵌入模型推理服务同时把模型常驻显存,重排模型推理时也要临时分配显存,三者叠在一起,16G 根本不够。
解决方法不是买显卡,而是把嵌入、重排、生成三个服务分开部署,嵌入和重排放到 CPU 上跑,用 ONNX Runtime 加速,生成模型独占 GPU。实测效果:嵌入模型在 CPU 上 batch=32 时,每万条文本约耗时 8-10 分钟,完全在可接受范围内;重排模型在 CPU 上对 50 条候选做精排,单次约 1-2 秒,也不影响体验。GPU 就让给生成模型,这是全链路里唯一不能牺牲速度的环节。
这个经验我特别想提醒后面的人:别指望在一张卡上全塞满。资源塌缩的正确姿势是把低频计算(嵌入、重排)压到 CPU,把高频计算(生成)放到 GPU,而不是把全部模型都往显存里挤。
6.3 中文文档的乱码与切分断裂
解析链路刚跑通时,检索结果里出现大量半截话,源头是解析后的文本乱成一团。常见的脏数据包括:PDF 提取文本时把中文标点转成英文标点、OCR 结果里插入全角空格、Word 转 PDF 后段落顺序错乱、表格里的内容被拆到不同页。
印象最深的是一个性能测试报告,表格跨了三页,表头在第一页,表体在后两页,结构切分器把这三部分当成了三个独立 chunk,检索时只命中表体没有表头,模型完全不知道这些数字对应什么指标。
解决方案分三层:解析层做文本清洗,把全角转半角、压缩连续空格;结构层跨页合并表格区域;切分层对标题和相关段落配对,确保“表头+表体”在同一个 chunk 内。这块没有现成工具,只能针对自家文档特征写规则。也因为这个,我强烈建议:先抽取 30 份代表性文档做解析质量验收,再全量入库,不要一上来就灌几千份。
6.4 上下文超长与引用缺失:把命中的文字回填给模型
另一个影响用户体验的问题是:模型一旦拿不到原文,就会“发挥”。早期版本 prompt 里只给了 chunk_text 拼接,模型回答看起来流畅,但句句经不起对照原文的推敲。而且回答没有引用来源,用户反馈“就像在跟一个不懂装懂的人聊天”。
后来做了两个改动,效果立竿见影。第一,prompt 中每个 chunk 前加上来源标记,例如【来源:产品手册 V2.1 第 42 页】,让模型在生成时把对应来源编号附着在答案的每个分段后面。第二,限制生成模型只能依据给定片段作答,如果片段中没有信息,必须明确说“未在已导入文档中找到相关内容”,而不是自行编造。
在 Prompt 组装时还要处理超长问题。Top 5 个 chunk 加标记后很容易超过 8k token,所以重排结果要再根据模型最大上下文做动态截断,优先保留重排分数高的段落,同时按来源去重,避免同一篇文档占掉太多位置。
6.5 检索质量怎么量化:用 hit rate 和 MRR 做回归测试
没有量化就没有优化。项目开始前我建了一个评测集:从各个业务线搜集 120 个真实问题,每个问题对应一个标准答案片段(文档路径 + 页码 + 段落内容)。每次调整检索参数、切分策略、重排阈值,就跑一遍评测脚本,输出两个指标:
- hit rate@K:前 K 个结果里是否包含标准答案片段,衡量“找没找到”。
- MRR(Mean Reciprocal Rank):标准答案片段出现排名位置的倒数均值,衡量“排得靠不靠前”。
这套评测集在整个两周周期里帮了大忙。比如我发现把 HNSW 索引的 efSearch 参数从 80 调到 200,hit rate@5 提升了 4 个百分点,代价是查询延迟多了 15 毫秒,这个收益立刻就能量化;再比如调查“权限过滤导致检索降级”有没有办法补偿,跑一遍评测集就知道 filter 对 hit rate 的影响到底有多大。没有评测集,这些调整都只能靠主观感觉,那才是真正的“调参玄学”。
7. 私有化落地之后的运维与演进
7.1 性能与容量:并发策略、模型量化与缓存
上线前做了并发压测,模拟 50 个用户同时提问,主要瓶颈在生成模型和重排模型。优化手段有三个:一是把生成模型用 AWQ 量化到 13G 显存,换取更大的 batch 吞吐;二是重排环节做查询缓存,对缓存命中直接跳过重排;三是支持对高热度问题做预计算,把常见问答结果提前生成并存到 Redis,用户直接秒回。
向量检索本身不是瓶颈。几十万条向量的 ANN 查询在 HNSW 索引下延迟在 10 毫秒级别,真正的时间都花在“把上下文拼成长文本、喂给模型、生成自然语言结果”这一步。所以性能优化的重心,始终要放在模型推理而不是检索上。
7.2 知识更新:增量入库与定期重建
文档知识库不是一成不变的,产品手册每季度更新、工单记录每天都在新增。更新策略采用“按 document_id 删除再重插”:文档改版时,用同一个 document_id 把旧 chunk 全部删除,重新解析、切分、向量化后写入。对新增文件则走增量扫描,定时任务每隔一段时间检查配置的共享目录,发现新文件自动进管道。
这套机制有一个需要考虑的问题:重排和生成阶段依赖的原文索引如果长时间不重建,会积累“死链”引用。所以我们每晚做一次校验,把向量库里引用的 doc_id 和文件源表做比对,对不一致的引用做标记清理,保证引用出来的内容永远能找到原文。
7.3 下一阶段:从 RAG 到 Agentic RAG 和 GraphRAG
两周的“最小可用版本”上线后,我已经在规划下一阶段。当前这套 RAG 是单次“检索-生成”,面对多跳问题很吃力,比如“某客户采购了 A 系列产品,它配的配件 B 质保期多久”,需要把“客户方案”和“配件文档”两类信息串起来。这类场景需要 Agentic RAG,让模型能主动规划调用多个子知识库,而不是一次检索就给出答案。
另一个方向是 GraphRAG 和本体增强。内部文档里大量实体关系(产品与配件、制定标准与适用场景、合同与客户名称),用向量做模糊匹配会丢失关系结构。计划在切分后的段落上提取实体和关系,构建一张轻量级知识图谱,检索时先沿图谱找相关实体,再回向量库补充细节。这块工程量不小,但方向是明确的。
8. 最后分享几点个人体会
整套系统从立项到上线只用了两周,其间踩过的坑比我预想的多得多。我的核心感受有三条:
第一,别迷恋模型。真正决定 RAG 好用程度的,是文档解析的清洁度、切分的合理性、检索评测的闭环。模型只是链条末端的表达工具,文档进得去、找得着,答案才会有质量。第二,评测集要早建,建完所有优化都有抓手,否则检索参数调来调去,全靠玄学,一旦出问题连回滚都不知道回滚到哪。第三,权限和引用标注别放到最后才想,它们直接影响系统能不能在企业内部真正用起来,返工成本远比想象的高。
最后再分享一个小技巧:在做文档解析质量验收时,别只看提取出的文字数量,要随机抽出 20 页和原 PDF 逐句比对了再看效果。许多脏数据问题,在“看起来字数不少”的假象下很容易被忽略。数据质量这块,值得花一半的项目时间去打磨。