📑 本页目录(点开跳转)
智能体工程教程 · 按需阅读
先证明文档入对了库,再追问模型为什么答错。
内容、版本和权限是三件事;任何一件没验收,回答都可能没有可信依据。
先观察一个失败也能解释的结果
同一版本重复导入不应制造重复块;发布失败时旧版继续服务;撤权后旧引用也必须被拒绝。
11b · 文档入库:更新、删除和撤权都算数据
⏱ 按阶段实践 | 先跑文本,再按岗位补扫描件与复杂表格
🎯 一句话
先证明进入索引的内容正确、当前且有权访问,再讨论模型是否聪明。 这一页把“上传文档”展开成可回放的入库任务。
🔗 前置是检索与 RAG;环境见共同项目。命令在仓库根目录运行。实现见 core.py,负例见 test_core.py。
先看到内容:解析与文档身份
先跑 inspect,再用字段与输入类型检查表核对结果。
🔬 先观察解析后的内容
python examples/knowledge-assistant/lab.py inspect examples/knowledge-assistant/corpus/limits.md --chunk-chars 450
python examples/knowledge-assistant/lab.py init
python examples/knowledge-assistant/lab.py jobs
第一条要看 body 中表头、数值和单位是否在一起;后两条分别发布初始版本、查看任务记录。
重复 init 应报告已有相同版本,而不是制造重复块。先核对文件名与 manifest,不要把失败当成“模型没理解”。
一个文档至少携带哪些信息
| 字段 | 来源 | 为什么需要 |
|---|---|---|
| tenant、readers | 已认证连接器与权限目录 | 用户不能在提问里自报租户获得权限 |
| doc_id | 源系统稳定 ID | 改标题后仍知道是同一个文档 |
| revision | 源系统递增版本,或服务端分配 | 阻止迟到的旧事件覆盖新版本 |
| 原始内容哈希 | 文件字节 | 发现同版本内容冲突、定位输入 |
| parser / chunk 配置 | 构建版本 | 解释为什么同一文件产生不同块 |
| 页码、章节、块序号 | 解析器 | 引用能返回到原文位置 |
| 状态、尝试次数、错误码 | 入库任务 | 失败后能回放,区别“未读到”和“已发布” |
本项目 manifest 是受信任管理员的导入接口,不能直接暴露为匿名上传 JSON。路径被限制在语料目录内;将来接对象存储还要限制桶、前缀、文件大小与下载超时。
一、解析完成不等于解析正确
| 输入 | 先检查的可观察结果 | 失败去向 |
|---|---|---|
| Markdown / UTF-8 文本 | 标题归属、中文解码、段落顺序 | 空内容或坏编码拒绝发布 |
| 普通 PDF 文本层 | 每页字符、阅读顺序、页码对应 | 本项目可选 pypdf;稀疏页进入人工/OCR 复核 |
| 扫描 PDF / 照片 | OCR 文本、置信度、旋转、漏页 | OCR 是扩展任务,当前代码没有实现 |
| 双栏、跨页表格 | 左右栏顺序、重复表头、单位、脚注 | 逐页视觉核对,不能仅看提取字符数 |
| 图表、公式、合同印章 | 原图定位、识别内容与适用范围 | 保留图像来源;无法识别时显式说明 |
有文本的 PDF 也可能读错顺序。 pypdf 能读到字符不代表复杂版面已验收;本项目的“短页拒绝”只是保守筛查,不能检测全部 OCR 错误。
岗位要求文档智能时,单独准备文字页、扫描页、旋转页、双栏页、跨页表各类夹具,并保存页图与抽取结果对照。
切块保留语义,不只追求长度
表头、单位、条件不要离开它们约束的答案。
二、切块先保护语义,再测长度
本项目按标题、段落和句子切分;Markdown 表格连同前一段说明作为整体保留。chunk_chars 按字符计算,不是模型 token,表格可能超过目标长度。上线模型前,用它自己的 tokenizer 统计实际输入长度和截断率。
| 调整 | 想解决什么 | 可能付出的代价 | 验收 |
|---|---|---|---|
| 减小块 | 大块混入多个主题 | 条件、单位与答案分离 | 检查原问题仍能在同块或关联块得到依据 |
| 增大块 | 答案缺上下文 | 相似度被稀释、上下文挤占 | Recall 与有效证据占比一起看 |
| overlap | 边界断句 | 重复召回、更多向量与费用 | 去重后有效文档数,不只看块命中 |
| 父子块 | 小块定位、大段回答 | 引用与权限映射复杂 | 父段同样验 ACL,不能借扩展段泄漏 |
| 上下文补全 | 孤立片段缺产品/章节名 | 生成上下文可能编造条件 | 保存原块与补全文分栏,抽查事实一致性 |
当前参考实现没有 overlap、父子索引和 LLM 上下文补全;它保留简单基线,便于逐项加上后做消融。调参记录方法见11c。
🛑 现在可以停:已经能解释一个块来自哪一页、为什么这样切。下一段处理数据上线后才出现的问题。
发布与迁移:不能露出半个版本
先看事件到达后的正确状态,再运行负例测试。
三、发布是一笔事务,失败不能露出半个版本
本项目的顺序是:登记任务 → 解析和校验 → 在事务内检查版本 → 替换该文档全部块 → 发布元数据。事务失败时旧版本仍可检索;任务单独留下失败记录。读者不会看到“新文档标题搭配旧段落”。
| 到达的事件 | 正确结果 | 参考实现中的证据 |
|---|---|---|
| 相同版本、相同内容重投 | 成功且不重复 | test_duplicate_ingestion_is_idempotent |
| 相同版本、不同内容 | 明确冲突 | test_same_revision_different_content_is_rejected |
| v3 后收到 v2 | 保留 v3 | test_out_of_order_revision_cannot_win |
| 解析后崩溃 / 提交前失败 | 旧版继续服务,可回放任务 | test_publish_failure_preserves_previous_version |
| 删除后迟到的旧任务 | 保留删除墓碑,不复活 | test_delete_tombstone_blocks_delayed_job |
| 文档撤权但旧引用还在 | 再次验证时拒绝 | test_acl_change_invalidates_old_citation |
运行这些负例:
python -m unittest discover -s examples/knowledge-assistant -p test_core.py -v
本地 SQLite 事务演示发布边界;它没有分布式队列、自动租约抢占与死信平台。
接生产队列时,在同一逻辑上补任务租约、心跳、最大尝试次数、退避、人工重放入口和唯一事件键。队列“至少一次”投递要求消费者幂等,不能靠“通常不会重复”。
改解析器和 embedding 时怎么迁移
解析器版本和切块参数进入指纹;同一文档版本不能悄悄改结果。本地实验换配置用新的数据库。正式系统建立新索引版本,回填历史数据并接住增量,比较抽样与总数、权限和时间水位,再切读取别名。
回滚依赖旧索引仍能表达当前撤权和删除状态。只保留一份旧向量快照,却不继续应用权限变更,会把“回滚”变成信息泄漏。新旧 embedding 空间不能混搜;记录模型、维度、归一化、距离函数、查询前缀与权重哈希。
权限不是检索末尾的一次过滤
下载、父段扩展、缓存和恢复都要重新核对权限。
四、权限要穿过每一条取数路径
SQL 先按可信租户筛选,本项目再检查 readers;BM25、向量、融合都只看可见块。引用复核再查当前权限与版本。向量仅在进程内缓存可见快照,模型权重可以落盘。
扩展时逐个检查:搜索、资源 URI、父段扩展、下载原文、历史消息、向量缓存、答案缓存和备份恢复。答案缓存键至少绑定租户、权限版本、语料版本与模型配置;撤权要能使缓存失效。答案缓存命中不能跳过当前授权。
检查自己的解释,再进入检索调优
能说出一个失败的最终状态,就有了下一轮排查起点。
✅ 检查点
- 为什么文件名和内容哈希不能代替版本号?
- 换切块参数后,用相同版本直接覆盖有什么问题?
- 为什么过滤检索结果后再做父段扩展仍可能泄漏?
展开答案
- 文件名会改,哈希只能判断是否相同,不能判断事件先后。需要稳定 ID 与可比较的版本。
- 无法区分同版本冲突与新索引规则,也难以回放。用新索引版本或明确的服务端版本迁移。
- 父段可能跨权限边界;每条扩展取数都需要按当前身份重新授权。
先验解析,再切块;同一版本原子发布;旧事件不能覆盖更新与删除;权限跟着每一次取数走。
🔗 接下来去哪
➡️ 下一站:11c · 检索评测与调优实验。