这是系列文章的第三篇,介绍 Canonical 冻结后的结构感知 Chunk、Qwen3 Embedding、服务器传输、PostgreSQL、HNSW、BM25、RRF 和 Reranker。
公开说明:所有私人网络地址、账号、Token、密钥、内部仓库和来源哈希均已删除或替换。
系列目录
- 解析架构与质量基线
- 块级融合与人工质量验收
- Chunk、Embedding 与混合检索
- 服务部署、增量发布与安全运维
1. M6 阶段的执行顺序
Canonical 通过人工验收后,才解除下游禁令。索引阶段严格按以下顺序执行:
M6-01 Chunk QC
M6-02 Embedding QC
M6-03 Canonical 传输验证
M6-04 PostgreSQL 部署
M6-05 数据库导入与正式索引
M6-06 REST/MCP 检索验证
任何阶段失败,后续阶段全部阻断,同时保留已完成输出用于诊断。M6 不重新运行 MinerU,也不修改已验收的 Canonical。
2. 为什么 Chunk 也需要 Schema
简单按字符数或固定 token 窗口切分,会破坏:
- 标准条款结构;
- 表格行列关系;
- 代码块;
- 图片 caption;
- 跨页内容;
- heading path;
- 原始页码和 bbox 溯源。
因此 Chunk 不是一段字符串,而是带完整来源信息的正式数据对象。
3. 结构感知 Chunk 策略
本次采用:
普通正文目标:约 600 tokens
普通正文上限:900 tokens
标准条款:尽量保持完整
表格:按逻辑表处理
代码块:保持完整
标题路径:保留
权威文字:优先 search_text
拒绝状态:UNRESOLVED
中文 token 估算不能简单使用英文单词计数。为了在不依赖在线 tokenizer 的情况下保持确定性,可以为 CJK、拉丁单词、数字和标点建立固定估算规则,并将规则版本写入 Manifest。
4. Chunk Schema
核心字段示例:
{
"chunk_id": "content-derived-stable-id",
"schema_version": "knowledge-chunk-v1",
"document_id": "stable-document-id",
"chunk_type": "text",
"search_text": "用于检索的权威正文",
"heading_path": ["第 5 章", "5.2 条款"],
"original_pages": [117, 118],
"block_ids": ["block-001", "block-002"],
"logical_table_ids": [],
"image_ids": [],
"asset_paths": [],
"bbox_refs": [],
"source_sha256": "<source-pdf-sha256>",
"chunk_sha256": "<chunk-sha256>",
"requires_review": false
}
chunk_id 应由稳定输入派生,而不是使用随机 UUID。这样相同 Canonical 输入可以得到相同 Chunk ID,增量发布时能准确判断复用和重建。
5. 不同内容类型分别处理
最终 15,801 个 Chunk 包含:
| 类型 | 数量 |
|---|---|
| 普通文本 | 11,863 |
| 表格 | 1,803 |
| 代码 | 1,566 |
| 图片 | 564 |
| 图表 | 5 |
5.1 普通文本
优先沿标题、段落和条款边界合并。只有超过上限时才进一步切分。
5.2 表格
以 logical_table 为主要检索单位,同时保留 fragment、页码和表格 HTML。超大表格可以分块,但必须保留表头和逻辑表 ID。
5.3 代码
避免从 IF/ELSE/ENDIF、花括号或 ASN.1 结构中间截断。跨页代码块需要结合原始页码保留完整上下文。
5.4 图片和图表
Chunk 保存图题、辅助描述、原始页码和真实资产引用。Mermaid 不能成为原图的替代品。
6. Chunk 门禁
至少验证:
search_text非空;- 所有来源 block 已对齐;
UNRESOLVED为 0;- 每个 Chunk 有文档和来源哈希;
- 原始页码在文档页数范围内;
- 表格 Chunk 可定位 logical table;
- 图片 Chunk 可定位真实资产;
chunk_sha256可复算;- 所有可搜索页面均被覆盖;
- 没有意外遗漏逻辑表;
- review flag 没有丢失。
本次 2,999 个源页面中,2,943 页属于预期可搜索页面,56 页是无可搜索正文的页面;最终没有遗漏任何预期可搜索页。
7. Embedding 模型冻结
Embedding 使用:
模型系列:Qwen3-Embedding-4B GGUF
量化:Q8_0
维度:2560
Pooling:last
归一化:L2
相似度:cosine
运行时:llama-cpp-python + Vulkan GPU
下载前先固定:
- 模型仓库;
- 精确 revision;
- 精确文件名;
- 文件 SHA-256;
- 文件大小;
- 推理运行时版本;
- pooling;
- 上下文长度;
- batch 参数;
- 并发数。
不要只记录“Qwen3-Embedding-4B”。同一仓库的不同 revision、量化文件或 pooling 设置,可能得到不可比较的向量。
8. 先小批量验证,再生成全量向量
Embedding Pilot 的顺序:
- 校验模型文件哈希;
- 选取少量不同类型 Chunk;
- 生成 2,560 维向量;
- 检查维度;
- 检查无 NaN/Infinity;
- 检查 L2 范数接近 1;
- 执行余弦相似度检索;
- 模拟中断并验证增量恢复;
- Pilot 通过后启动全量。
L2 检查示例:
import numpy as np
vector = np.asarray(embedding, dtype=np.float32)
assert vector.shape == (2560,)
assert np.isfinite(vector).all()
assert abs(float(np.linalg.norm(vector)) - 1.0) < 1e-5
9. Embedding 分片与增量恢复
向量不写入一个超大文件,而是分片保存:
04-embeddings/release/
├── embedding-manifest.json
├── embedding-records.jsonl
└── shards/
├── shard-00000/
│ ├── vectors.npy
│ └── records.jsonl
├── shard-00001/
└── ...
每个分片记录:
- 输入 Chunk 指纹;
- Chunk 数量;
- 首尾 Chunk ID;
- 向量维度和 dtype;
- vectors 文件 SHA-256;
- records 文件 SHA-256;
- L2 最大偏差;
- 运行耗时。
如果进程中断,已完成且输入指纹未变化的分片直接复用。修改一个文档时,也只重建受影响的 Chunk 和 Embedding。
全量 float32 向量的最大 L2 范数偏差约为 6e-8,15,801 个向量全部为有限值。
10. 服务器只接收必要资产
服务器传输包分为:
sources.tar
canonical-core.tar
release.tar
10.1 sources.tar
包含:
- 原始 PDF;
- source manifest;
SHA256SUMS。
10.2 canonical-core.tar
每个文档包含:
blocks.jsonlpages.jsonlogical-tables.jsonpage-table-fragments.jsonimages.json- 必要的结构化裁剪图
- server document manifest
10.3 release.tar
包含:
chunks.jsonl- Chunk manifest
- Embedding shards
- Embedding manifest
- Release manifest
SHA256SUMS
本地页面 PNG、解析中间 JSON、人工校正包、布局可视化和重复图片树不上传服务器。
11. 传输校验
上传流程不是简单 scp:
- 本地构建归档;
- 本地校验每个成员;
- 上传到
.incoming/<release-id>; - 远端复算归档 SHA-256;
- 解包后复算成员 SHA-256;
- 复核所有原始 PDF 哈希;
- 检查文档、页面、图片、Chunk、Embedding 和分片数量;
- 通过后再准备
current指针切换。
只有在数据库导入和服务检索也通过后,staging 才允许清理。
12. PostgreSQL 运行架构
服务器采用:
Ubuntu Server 24.04
PostgreSQL 17
pgvector
halfvec(2560)
HNSW cosine
BM25 扩展
应用侧中文分词
PostgreSQL 只加入内部 Docker 网络,不向宿主机发布数据库端口。应用容器通过内部服务名访问数据库。
数据库表至少包括:
- releases;
- documents;
- pages;
- blocks;
- logical tables;
- page table fragments;
- images;
- chunks;
- chunk embeddings。
数据库密码通过服务器密钥文件注入,文件权限为 0600,不写入 Compose 和 Git。
13. 数据库导入流程
正式导入采用 staging:
- 创建或清空当前 Release 的 staging;
- 严格读取 Release Manifest;
- 验证输入哈希、维度和数量;
- 导入 documents/pages;
- 导入 tables/fragments/images;
- 导入 chunks;
- 导入 halfvec embeddings;
- 验证外键和双向关系;
- 创建正式 BM25/HNSW 索引;
- 执行检索 smoke test;
- 事务性提升 Release 状态。
导入后的核心计数:
documents = 41
pages = 2999
chunks = 15801
embeddings = 15801
float32 向量转换为 halfvec(2560) 后,最大 L2 偏差约为 3.6e-5,仍满足检索门禁。
14. 正式索引
14.1 HNSW 向量索引
向量列使用 halfvec(2560),距离度量使用 cosine。HNSW 提供低延迟近似最近邻检索。
14.2 BM25 索引
技术标准中的以下内容特别依赖关键词召回:
- 标准号;
- 条款号;
- 英文缩写;
- 逻辑节点;
- ASN.1 标识符;
- 图号和表号。
中文使用应用侧确定性分词,并将分词版本写入 Release/服务状态。这样同一输入在不同部署中得到一致检索词。
15. 为什么采用混合检索
只使用向量检索:语义召回较好,但可能漏掉精确标准号和代码标识。
只使用 BM25:精确词召回较好,但用户换一种表达后容易漏检。
最终流程:
16. RRF 排名融合
Reciprocal Rank Fusion 不要求两路分数处于同一量纲:
RRF_score(d) = sum(1 / (k + rank_i(d)))
本次使用 k=60。向量和 BM25 各自返回候选后,按 rank 融合,再选出一批候选交给 Reranker。
RRF 需要做确定性测试:相同输入、相同候选顺序和相同配置必须产生相同结果。
17. Reranker 精排
精排使用 Qwen3-Reranker-0.6B,固定模型 revision 和文件哈希,服务器端并发为 1。
Reranker 的作用不是替代召回,而是在 RRF 候选中重新判断问题与 Chunk 的相关性。服务应记录:
- 向量检索得分;
- BM25 得分;
- RRF 得分;
- Reranker 得分;
- 总耗时和分阶段耗时。
当 Reranker 不可用时,系统可以明确降级到 RRF 结果,但必须在响应或健康状态中暴露降级,而不能静默伪装为已精排。
18. 检索结果的来源信息
每条结果不仅返回正文,还应返回:
chunk_id;document_id;- 原始页码;
- heading path;
- block IDs;
- logical table/image 关联;
- source SHA-256;
- review flag;
- 各阶段排名或分数。
这样上层 Agent 可以引用来源页,人工也可以从回答回到原始 PDF。
19. REST 与 MCP
REST 搜索示例:
curl --request POST 'https://kb.example.com/v1/search' \
--header "Authorization: Bearer <client-token>" \
--header 'Content-Type: application/json' \
--data '{
"query": "需要检索的问题",
"limit": 5
}'
MCP 暴露三个最小工具:
search_knowledge
list_knowledge_documents
get_knowledge_chunk
通用 MCP 配置:
{
"mcpServers": {
"private-knowledge": {
"url": "https://kb.example.com/mcp",
"headers": {
"Authorization": "Bearer ${KNOWLEDGE_API_TOKEN}"
}
}
}
}
KNOWLEDGE_API_TOKEN 由客户端环境变量或系统密钥存储提供,不能写进公开配置。
20. 本阶段验收
- Chunk 覆盖全部预期可搜索页面
- 表格、代码和图片结构未被破坏
- Chunk 来源和哈希可复算
- Embedding 模型 revision 与哈希固定
- 向量维度、有限值和 L2 门禁通过
- 增量恢复通过
- 传输归档和成员哈希一致
- PostgreSQL 重启后数据仍存在
- HNSW 和 BM25 索引通过 smoke test
- RRF 结果确定性通过
- Reranker 在线验证通过
- REST 和 MCP 返回正确页码与来源
下一篇将完成整个系列:如何为不同客户端签发独立 Token,如何限制 Agent 权限,以及怎样实现 M7 增量发布、原子切换、自动回滚和可恢复备份。
可增量发布的私有知识库(三):Chunk、Embedding 与混合检索
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法