第3章 解析不是清洗,而是建立可定位的投影
一段回答附带“来源:员工手册”只能说明候选出处。要核查金额是否确实存在,需要知道检索处理后的文本是什么,以及引用位置怎样映射回它。解析器的产出因此应包含稳定投影,而不是一串无法重建的清洗结果。
先定义规范文本
lab/text.py 的 normalize() 把 CRLF 和 CR 换为 LF,再执行 Unicode NFC 规范化。Markdown 输入保留正文标记,采用简单 ATX 标题表达式寻找标题,没有把 Markdown 渲染为纯文本。规范化后的字符串就是后续切分、摘要和引用共同使用的基准。
JSON 输入只接受一种明确结构:顶层恰好包含 title 与 sections,每个章节恰好包含 heading 与 text,相关值必须是规定类型。投影依次生成一级标题、二级标题和段落,章节之间用空行连接。不认识的格式和多余字段会报错,而不是被悄悄丢弃。
{"title":"远程办公申请","sections":[
{"heading":"审批流程","text":"申请居家办公须部门负责人同意,每周最多2天。"}
]}这份结构会成为一段 Markdown 风格的规范文本。引用坐标指向这段投影,不指向原始 JSON 的字节位置。接入 PDF、表格或网页时,也必须说明投影保留了什么结构、丢失了什么关系,以及原始页码如何另行保存。
坐标的单位必须明确
本书的 [start,end) 使用 Python Unicode 码点索引,左闭右开。它不是 UTF-8 字节偏移,也不是手机屏幕上看到的字形数量。一个组合字符在 NFC 前后可能有不同长度;换行转换也会改变原始文件位置,因此坐标只能作用于同一版规范文本。
片段必须满足 body[start:end] == quote,并把片段 UTF-8 编码后的 SHA-256 记录下来。回答函数不是相信搜索结果中的 text 字段,而是重新从当前数据库读出片段,再复核它与文档切片的关系。测试把调用方命中的文本改成伪造内容,得到的仍是数据库里的真实摘录。
摘要也有边界。一个匹配的 SHA-256 说明投影内容没有意外变化,不说明内容事实正确,更不说明用户有权阅读。引用完整性、授权和业务真实性需要分别判断。
教学解析器没有假装全能
当前 Markdown 标题识别只是正则表达式,会把围栏代码块里形似标题的行也当成标题元数据。它没有实现完整 CommonMark 语法、表格关系、附件、PDF OCR 或网页正文抽取。示例文档使用简单标题与自然段,因此这个范围足以解释实验,但不能直接推广到任意企业文档。
面对复杂输入,应先保留原始对象与解析日志,选择适配格式的解析器,再建立可回放的投影版本。解析失败时应保留旧的有效索引,而不是先删除旧内容再尝试新解析。随书更新流程先在事务外完成解析,失败不会触碰已有数据库状态。
可以用一条简单验收检查解析接入:从最终引用反查规范文本,再回到带版本的源对象,是否有任何一步只能靠人工猜测。如果有,问题应在投影层解决,不能交给回答模型补全。