项目记录
知识入库与增量指纹
通过数据契约、两级切分、内容指纹与质量门禁,构建可增量更新的企业知识入库链路。
本文目录 · 8 节
检索效果差的锅,八成不在检索,而在入库。
企业文档的真实状态是:有的 PDF 是扫描件,有的 Word 里塞了表格,有的目录分类全靠文件名, 还有人今天改了一版制度、明天又撤回。如果入库阶段没有约定,后面所有环节都是在流沙上盖楼。
先给结论
入库要解决四件事,缺一件都会在后期反噬:
1. 契约 —— 每份资料属于哪个 source,由 scenario.toml 说了算
2. 切分 —— Parent-Child 两级切分,检索用小的、生成用大的
3. 变更 —— SHA256 内容指纹决定「谁需要重建」,而不是全量重跑
4. 门禁 —— 入库就有质量闸,脏数据不许进库
一、数据契约:source 不是场景
scenario.toml 定义了这个场景的全部契约,其中最关键的是 valid_sources:
scenario_id = "enterprise_knowledge"
valid_sources = ["hr", "it", "finance"]
faq_collection = "enterprise_faq_hybrid_v1"
doc_collection = "enterprise_doc_hybrid_v1"
[source_patterns]
hr = "入职|离职|请假|考勤|绩效|社保|公积金|转正|试用期|岗位变更"
it = "VPN|电脑|账号|邮箱|权限|网络|打印机|软件|数据安全|API Token"
finance = "报销|发票|预算|付款|借款|费用|单据|付款凭证"
这里有一个容易被误读的点:hr / it / finance 是场景内部的知识分类,不是三个业务场景。
对外只有一个助手,只在检索和过滤时用到这些分类。把它们当成三个场景来演进,
规则、阈值和评测集就永远调不准——因为每次调参都在让另一个分类变差。
FAQ 走的是纯字段契约,没有解析环节:
source,question,answer
hr,年假和调休应该怎么申请?,员工应先提交请假申请,经直属负责人审批后生效。
集合也分成两个:enterprise_faq_hybrid_v1 和 enterprise_doc_hybrid_v1。
FAQ 用来做高频问题的直出快路径,文档集合负责知识查询和追问。
分开存的好处是两条链路可以各自调参,不会互相污染召回结果。
二、解析:格式不同,处理方式也不同
入站格式支持 Markdown、TXT、PDF、Word、PPT、CSV 和 Excel,但路径不完全一样:
| 内容形态 | 处理方式 | 代码位置 |
|---|---|---|
| 普通文本 / Markdown | 直读 + 归一化(空白、编码、标点统一) | qa_core/indexing/document_loaders.py |
| 表格内容 | 转成结构化的行描述再入库 | qa_core/indexing/table_documents.py |
| 扫描件 / 图片型 PDF | 离线 OCR,产出候选后再人工提升 | qa_core/indexing/ocr_documents.py、ocr_review.py |
| 含图片风险的内容 | 标注风险等级,阻断项不许入库 | qa_core/indexing/image_risk.py |
OCR 刻意设计成离线跑 + 人工提升:scripts/ocr/run_offline_ocr.py 产出候选文本,
scripts/ocr/promote_ocr_candidates.py 才把它推进知识库。
理由是 OCR 一定会有错字,错字进库之后,检索到的就是错的证据——比召回不到更危险。
三、切分:为什么是 1000 和 350
Parent chunk_size=1000, overlap=100
Child chunk_size=350, overlap=50
两级切分的分工很明确:
入库时:Parent 切成 Child,Child 进向量库(粒度小,匹配准)
生成时:Child 命中 -> 回填它所属的 Parent -> 把 Parent 交给 LLM(上下文完整)
只用一种粒度必然二选一:chunk 大了匹配不准,chunk 小了答案被截断。
Parent-Child 让「匹配用的小」和「生成用的大」同时成立。
切分器用中文标点优先的递归分隔符(qa_core/indexing/chunking.py),
避免把一句话从中间劈开;切分规则版本记在 chunk_schema_version 里(当前 parent_child_validity_v2),
规则一变,历史 chunk 就能被识别出来重建。
四、增量:靠指纹,不靠时间戳
每份文档入库前算一次 SHA256 内容指纹,写进 manifest。 下一轮入库时先比对指纹:没变就跳过,变了的才重新解析、切分、写向量。
不要用文件修改时间判断变更——同步工具会把 mtime 全刷新一遍,你会白跑一次全量重建。 内容指纹只认内容本身,这是可复现的前提。
五、门禁:脏数据不许进库
入库质量门禁(qa_core/quality/ingestion.py)是一组「必须为 0」的计数:
失败文件 / 不支持格式 / 空文件 / 低质量切分 / 重复 chunk
FAQ 空问题 / FAQ 空答案 / FAQ 重复问题 / FAQ 非法 source
FAQ 与文档内容冲突 / 图片风险阻断文件
默认阈值全是 0,一票否决。听起来很严,但它拦住的正是那种「跑完没报错、 但线上开始答错」的隐性故障。
动手验证
# 全量重建(会重置 collection)
docker compose --env-file .env.single run --rm api python scripts/rebuild_scenarios.py --scenarios enterprise_knowledge --reset-collections
# 再跑一次:观察指纹命中,未变更文档被跳过
docker compose --env-file .env.single run --rm api python scripts/rebuild_scenarios.py --scenarios enterprise_knowledge --reset-collections
# 日常更新走增量版本:新建版本 -> 从 active 增量 -> 过门禁 -> 激活
docker compose --env-file .env.single run --rm api python scripts/rebuild_kb_version.py --scenario enterprise_knowledge --new-version --incremental-from active --quality-gate --activate --description "enterprise knowledge update"
入库报告落在 reports/ingestion/enterprise_knowledge/,里面能看到每个文件的解析结果、
chunk 数量和指纹命中情况。第二次跑的时候,跳过数应该接近全量——如果不是,说明指纹没生效。
取舍与边界
- 切分参数和文档语言强相关:1000 / 350 是中文企业制度类文档的经验值; 换成代码仓库或英文长文档,这两个值都要重新标定。
- 离线 OCR 换来的是准确率,付出的是流程成本:需要有人看一眼候选文本。
- manifest 是有状态的:删掉它,增量能力就退化成全量重建。
下一篇进入查询侧:用户问一句话,系统凭什么决定它该走 FAQ 直出、文档检索,还是追问。