项目记录

知识入库与增量指纹

通过数据契约、两级切分、内容指纹与质量门禁,构建可增量更新的企业知识入库链路。

发布于
本文目录 · 8 节

检索效果差的锅,八成不在检索,而在入库。

企业文档的真实状态是:有的 PDF 是扫描件,有的 Word 里塞了表格,有的目录分类全靠文件名, 还有人今天改了一版制度、明天又撤回。如果入库阶段没有约定,后面所有环节都是在流沙上盖楼。

先给结论

入库要解决四件事,缺一件都会在后期反噬:

text
1. 契约   —— 每份资料属于哪个 source,由 scenario.toml 说了算
2. 切分   —— Parent-Child 两级切分,检索用小的、生成用大的
3. 变更   —— SHA256 内容指纹决定「谁需要重建」,而不是全量重跑
4. 门禁   —— 入库就有质量闸,脏数据不许进库

一、数据契约:source 不是场景

scenario.toml 定义了这个场景的全部契约,其中最关键的是 valid_sources:

toml
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 走的是纯字段契约,没有解析环节:

csv
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

text
Parent  chunk_size=1000, overlap=100
Child   chunk_size=350,  overlap=50

两级切分的分工很明确:

text
入库时: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」的计数:

text
失败文件 / 不支持格式 / 空文件 / 低质量切分 / 重复 chunk
FAQ 空问题 / FAQ 空答案 / FAQ 重复问题 / FAQ 非法 source
FAQ 与文档内容冲突 / 图片风险阻断文件

默认阈值全是 0,一票否决。听起来很严,但它拦住的正是那种「跑完没报错、 但线上开始答错」的隐性故障。

动手验证

powershell
# 全量重建(会重置 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 直出、文档检索,还是追问。