先看结论与判断条件
- 向量索引不是独立缓存,而是嵌入模型、预处理、语料、切分规则、距离度量和格式共同计算出的派生产物。
- 模型版本号不足以标识真实输入,manifest 还应保存模型摘要、关联文件、向量维度、归一化方式和运行时约束。
- 语料快照需要绑定文档集合、内容摘要、权限范围和删除状态,避免旧索引继续返回已撤回或无权访问的片段。
- 更新流程应先验证签名元数据与整组摘要,在隔离目录完成装载自检,再以单个激活指针切换,失败时保留旧组。
- 供应链证明可以把索引产物与构建者、构建类型、参数和依赖材料关联,但声明格式本身不能保证事实真实。
- 验收应同时检查完整性、语义一致性、授权过滤、回滚拒绝和崩溃恢复;没有项目样本时不能宣称准确率或攻击阻断已验证。
先把索引看成模型与语料共同生成的产物
端侧 RAG 常把向量索引当成可以独立替换的缓存文件,应用只要能打开数据库并返回若干近邻就算更新成功。这个判断忽略了索引向量的来源。每个向量都由特定嵌入模型、分词或预处理配置、文档内容、切分边界、归一化规则和数值精度共同决定。任一输入变化,旧向量与新查询向量就可能不再位于同一个可比较空间。
最危险的故障往往不会崩溃。模型升级后维度仍然相同,索引库也能正常读取,距离查询甚至会稳定返回结果,但排序含义已经改变。用户看到的是“像真的一样”的错误引用,而监控只看到查询成功和耗时正常。工程验收必须把语义兼容性放到文件可读性之前,确认查询编码器与文档编码器来自同一绑定组。
绑定组应具有独立身份,例如 bundle_id 与 generation。组内至少包含嵌入模型摘要、预处理与关联文件摘要、语料快照摘要、切分配置摘要、索引格式、向量维度、距离度量、索引文件摘要和授权范围。应用只能激活完整且自洽的一组资产,不能从多个下载批次中挑选“看起来最新”的单个文件拼装。
| 资产 | 负责的语义 | 常见错配 | 可观察结果 |
|---|---|---|---|
| 嵌入模型 | 把查询与文档映射到向量空间 | 查询模型升级而文档向量未重建 | 查询成功但近邻排序失真 |
| 预处理配置 | 分词、归一化与输入张量构造 | 大小写、截断或特殊标记不同 | 部分语料召回异常且难以复现 |
| 语料快照 | 定义允许检索的文档集合 | 旧文档未删除或新文档未进入 | 返回过期、缺失或越权内容 |
| 切分规则 | 决定文档片段边界和元数据 | chunk 标识与正文位置变化 | 引用定位错误或片段上下文破裂 |
| 索引格式 | 保存向量、图结构或量化数据 | 运行时读取不兼容的格式版本 | 装载失败、静默降级或查询偏差 |
| 授权范围 | 约束哪些主体可访问哪些片段 | 索引更新未同步权限快照 | 文件完整却泄露不应展示的结果 |
用 manifest 固定整组资产的身份
manifest 不能只列文件名和版本号。文件名可被替换,版本号也可能因为构建重跑、渠道差异或人工复制而重复。每个可执行或数据资产都应记录加密摘要,并把决定语义的非文件参数纳入规范化摘要。对于索引,向量维度、距离度量、量化方式、格式版本、文档数量和生成器版本都属于身份的一部分,而不是可忽略的备注。
嵌入模型条目需要区分逻辑模型名、发布版本和实际文件摘要。LiteRT 模型元数据可以携带输入输出描述、归一化参数、标签和关联文件,这说明模型行为并不总由单个权重文件完整定义。若 tokenizer、词表、标签或预处理配置以关联文件存在,它们必须分别保存摘要,再由 bundle binding digest 把它们与主模型合并。
manifest 自身也需要可信来源。最小做法是由发布系统签名,将签名者、生成时间、过期时间和目标渠道写入受保护元数据。客户端先验证签名与期限,再核对每个本地文件摘要,最后重算整组 binding digest。仅在 JSON 内写一个自报摘要没有安全意义,因为替换者可以同时修改字段与摘要。公开检查代码可以验证内部一致性,签名真实性仍要由项目密钥体系完成。
| 字段 | 必须绑定的对象 | 检查方法 | 失败动作 |
|---|---|---|---|
| bundle_id 与 generation | 完整索引组和发布代次 | 与激活记录和策略单调比较 | 拒绝未知组或未批准回滚 |
| model_sha256 | 实际嵌入模型文件 | 流式读取文件并重算摘要 | 停止装载并删除未激活临时组 |
| preprocess_sha256 | 分词与预处理配置 | 核对关联文件和规范化配置 | 要求重新下载完整绑定组 |
| corpus_sha256 | 文档集合与内容快照 | 重建有序文档清单摘要 | 拒绝使用旧权限或旧内容快照 |
| index_format | 读取器、维度、度量和量化方式 | 与当前运行时能力矩阵比对 | 保持旧组并报告不兼容 |
| index_sha256 | 最终索引文件 | 下载完成后重算文件摘要 | 拒绝激活且不覆盖旧索引 |
| binding_sha256 | 所有语义输入的规范化组合 | 按固定字段顺序重算 | 任一差异都视为整组不一致 |
把模型运行时约束写进兼容合同
同一个模型文件在不同移动运行时上不一定具有相同可用性。ONNX Runtime Mobile 的集成要考虑目标平台、执行提供程序、模型使用的算子和定制包配置;Core ML 负责 Apple 平台上的模型转换、集成、装载和预测。它们说明运行时能力是交付合同的一部分,但这些框架不会替项目验证索引签名、语料权限或防回滚策略。
兼容合同应把 runtime_family、minimum_runtime_version、execution_provider、vector_dimension、distance_metric、normalization 和 numeric_type 写入 manifest。装载前先比对静态能力,再用公开安全的固定查询做自检,检查输出维度、有限数值、归一化约束和预期近邻集合。自检样本只能证明基本语义未明显错位,不能替代真实业务语料上的召回评估。
模型转换或量化也应产生新身份。即使逻辑模型名和训练版本不变,转换器、算子融合、量化参数或关联文件改变,都可能影响嵌入结果。工程上应将转换后的移动模型当成新的供应链产物,保存产物摘要和生成材料;索引只能绑定这个实际交付文件,不能绑定远端训练模型的名称后假设转换结果等价。
- 记录实际交付模型文件而非仅记录训练模型名称
- 固定运行时家族、最低版本和执行提供程序
- 核对向量维度、距离度量、归一化和数值类型
- 转换或量化变化时生成新的模型产物身份
- 关联 tokenizer、词表与预处理文件各自有摘要
- 固定查询自检只作为兼容支持证据,不冒充召回评测
- 运行时支持结论不扩大为签名、授权或防回滚结论
让语料快照同时表达内容与权限
语料快照不应只是一个导出日期。生成器需要对每篇文档使用稳定标识,记录内容版本、可见范围、删除状态和切分配置,再按确定顺序生成快照摘要。若只对合并后的文本文件求摘要,团队很难解释是哪篇文档变化,也无法确认权限撤销是否进入索引。结构化清单使增量构建可以审计新增、修改、删除和权限变化。
切分结果也属于快照。chunk_id 应由文档稳定标识、内容版本、切分规则版本和片段位置共同派生,不能只用数组序号。否则文档开头插入一段文字后,所有序号都改变,旧向量、引用坐标和权限元数据可能互相错位。索引条目至少保存 chunk_id、document_id、content_digest、access_scope 与向量位置,查询返回后还要执行最终权限过滤。
权限过滤不能只依赖构建时快照。对于组织成员变更、文档紧急撤回等需要快速生效的场景,端侧应用应有短期授权状态、撤销清单或强制刷新机制。索引内的 access_scope 用于减少候选集合和提供离线边界,服务端或受信策略仍应决定当前主体是否可见。离线窗口越长,旧权限残留风险越大,必须明确记录。
| 变化类型 | manifest 表达 | 索引动作 | 验收重点 |
|---|---|---|---|
| 新增文档 | 新增 document_id 与内容摘要 | 生成新 chunk 与向量 | 文档只出现一次且权限正确 |
| 正文修改 | 内容版本和摘要变化 | 旧 chunk 失效并重建 | 旧引用不再返回 |
| 文档删除 | 删除记录或新快照中缺失 | 移除全部相关向量 | 缓存和引用元数据同步清理 |
| 权限收紧 | access_scope 版本变化 | 更新过滤元数据或重建索引 | 旧主体无法从候选结果读取 |
| 切分规则变化 | chunking_config_sha256 变化 | 全量或可证明等价的重建 | chunk_id 与引用位置闭合 |
| 预处理变化 | preprocess_sha256 变化 | 重新编码受影响语料 | 查询与文档编码链一致 |
| 顺序变化 | 有序清单规则保持稳定 | 不应产生无意义内容漂移 | 快照摘要可重现 |
把下载、验证和激活设计成一笔事务
索引更新不应直接覆盖正在使用的目录。客户端先把 manifest、模型关联文件、语料元数据和索引下载到隔离目录,逐项验证签名、版本、过期时间与摘要,再执行运行时装载和固定查询自检。只有所有步骤通过,才用一个原子激活指针切换到新 bundle_id。进程崩溃或设备掉电后,恢复逻辑只承认旧激活组或完整的新组,不扫描目录拼出混合状态。
The Update Framework 的规范把根、时间戳、快照和目标元数据分成不同角色,并通过签名、版本、过期和一致性关系处理回滚、冻结与混搭风险。端侧 RAG 可以借用这些原则发布整组资产:目标元数据列出文件摘要,快照固定目标集合,时间戳提供新鲜度。TUF 不定义向量语义,binding digest 与兼容自检仍由 RAG 交付协议承担。
回滚应当是显式发布动作,而不是客户端看到新组失败后随意选择任意旧目录。允许回滚时,签名策略需要指明可接受的 generation、模型版本、语料快照与到期时间,并生成新的审计事件。紧急恢复旧版本也要确认旧语料权限仍合法。否则一次技术回滚可能让已删除文档重新进入检索,形成完整性通过但授权失效的事故。
| 阶段 | 输入 | 成功条件 | 失败后的安全状态 |
|---|---|---|---|
| 发现更新 | 签名元数据与当前 generation | 版本更新且元数据未过期 | 继续使用已验证旧组 |
| 隔离下载 | 完整目标清单 | 每个文件长度与摘要一致 | 清理临时文件而不触碰旧组 |
| 绑定核对 | 模型、预处理、语料、格式和索引字段 | binding digest 重算一致 | 拒绝任何跨组拼接 |
| 运行时装载 | 目标运行时与模型产物 | 能力矩阵和基本装载通过 | 卸载新组并保留旧实例 |
| 语义自检 | 固定查询和已知近邻样本 | 维度、数值和预期关系符合门禁 | 标记新组不合格 |
| 原子激活 | 完整新组和持久化指针 | 单次提交后读者只见一个 generation | 重启仍指向旧组或完整新组 |
| 旧组回收 | 新组稳定回执与保留策略 | 无活动读者且回滚窗口已结束 | 延迟回收而非影响当前服务 |
用只读检查器验证 manifest 内部绑定
下面的 Python 示例读取一份公开安全的索引 manifest,检查模型、预处理、语料、切分配置和索引文件摘要的格式,再按固定字段重算 binding_sha256。它还核对向量维度、距离度量、格式版本和 generation。任何输入派生字段不符合约束都会以非零状态退出,适合放在构建门禁或移动端下载后的诊断流程中。
代码只验证 manifest 内部一致性,没有读取实际模型、语料或索引文件,也没有验证发布签名。生产实现必须流式计算目标文件摘要,并通过平台安全库验证受信签名者和元数据期限。把 manifest 与文件同时替换仍可能通过这段检查器,因此它不能被描述为完整防篡改方案,更不能作为模型运行时不可提取的证据。
规范化规则必须在发布端和验证端共享。示例使用固定字段名、UTF-8 JSON、排序键和紧凑分隔符生成绑定输入;任何协议升级都应改变 schema_version,并让旧验证器明确拒绝未知版本。不要在失败后删除当前激活组,检查器只报告问题,由更新状态机决定清理尚未激活的临时目录。
#!/usr/bin/env python3
import hashlib
import json
import re
import sys
from pathlib import Path
SHA256_PATTERN = re.compile(r"^[a-f0-9]{64}$")
ALLOWED_METRICS = {"cosine", "dot_product", "euclidean"}
ALLOWED_FORMATS = {"flat-v1", "hnsw-v1", "ivf-v1"}
BINDING_FIELDS = (
"model_sha256", "preprocess_sha256", "corpus_sha256",
"chunking_sha256", "index_sha256", "index_format",
"vector_dimension", "distance_metric", "generation"
)
def require_value(manifest, name):
value = manifest.get(name)
if value is None or value == "":
print("missing field: " + name, file=sys.stderr)
raise SystemExit(3)
return value
def validate_digest(manifest, name):
value = require_value(manifest, name)
if not isinstance(value, str) or not SHA256_PATTERN.fullmatch(value):
print("invalid digest: " + name, file=sys.stderr)
raise SystemExit(4)
return value
def validate(manifest):
if manifest.get("schema_version") != 1:
print("unsupported schema_version", file=sys.stderr)
raise SystemExit(5)
for name in ("model_sha256", "preprocess_sha256", "corpus_sha256",
"chunking_sha256", "index_sha256"):
validate_digest(manifest, name)
index_format = require_value(manifest, "index_format")
if index_format not in ALLOWED_FORMATS:
print("unsupported index_format", file=sys.stderr)
raise SystemExit(6)
metric = require_value(manifest, "distance_metric")
if metric not in ALLOWED_METRICS:
print("unsupported distance_metric", file=sys.stderr)
raise SystemExit(7)
dimension = require_value(manifest, "vector_dimension")
if not isinstance(dimension, int) or dimension <= 0 or dimension > 65536:
print("invalid vector_dimension", file=sys.stderr)
raise SystemExit(8)
generation = require_value(manifest, "generation")
if not isinstance(generation, int) or generation < 1:
print("invalid generation", file=sys.stderr)
raise SystemExit(9)
binding = {name: manifest[name] for name in BINDING_FIELDS}
encoded = json.dumps(binding, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
calculated = hashlib.sha256(encoded.encode("utf-8")).hexdigest()
expected = validate_digest(manifest, "binding_sha256")
if calculated != expected:
print("binding digest mismatch", file=sys.stderr)
raise SystemExit(10)
def main():
if len(sys.argv) != 2:
print("Usage: rag_manifest_check.py MANIFEST_JSON", file=sys.stderr)
raise SystemExit(2)
manifest_path = Path(sys.argv[1])
if not manifest_path.is_file():
print("manifest file not found", file=sys.stderr)
raise SystemExit(2)
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print("cannot read manifest: " + str(exc), file=sys.stderr)
raise SystemExit(2)
if not isinstance(manifest, dict):
print("manifest must be an object", file=sys.stderr)
raise SystemExit(2)
validate(manifest)
print("manifest binding accepted")
if __name__ == "__main__":
main()用 provenance 追溯索引是怎样生成的
摘要能证明当前文件与记录值一致,却不能解释记录值从何而来。SLSA Provenance 把产物主体、构建者、构建类型、外部参数和依赖材料组织为可验证声明。索引构建可以把最终 index artifact 作为 subject,把嵌入模型、语料快照、切分配置和生成器镜像作为 materials,使审计人员能够判断这份索引是否来自批准的流水线与输入集合。
in-toto Attestation Statement v1 使用 subject 摘要把有类型的 predicate 绑定到具体产物。对 RAG 索引而言,predicate 可以描述生成器版本、规范化规则、输入材料引用、测试摘要和策略版本。声明格式只提供结构;签发者是否可信、构建环境是否受控、参数是否真实仍需组织自己的身份、签名和门禁体系证明,不能看到 attestation 文件就自动判定供应链安全。
provenance 还应避免泄露私有语料。材料条目可以使用受控快照标识和摘要,不应把文档正文、客户名称、内部路径或访问令牌写进公开元数据。需要深入复核时,通过权限受控的证据包把摘要映射到构建记录。公开页面只能说明字段与验证方法,没有真实项目回执时应明确标记未接入,而不是虚构某次构建已经通过。
- 索引产物摘要作为 provenance subject
- 嵌入模型、语料快照和切分配置作为材料引用
- 构建者身份与构建类型进入签名声明
- 外部参数采用规范化且可复核的公开安全字段
- 私有语料正文、客户信息和内部路径不进入公开声明
- 声明签名者与发布策略分别校验
- provenance 通过不扩大为运行时安全通过
用完整性、语义与授权三类证据验收
索引验收应先固定候选 bundle_id、应用版本、签名身份、模型摘要和语料快照。完整性用例验证签名元数据、文件摘要、binding digest、generation 和原子激活;语义用例使用公开安全的固定查询检查维度、有限数值、预期近邻和引用闭合;授权用例确认权限收紧、文档删除、离线窗口和撤销事件都能改变可见结果。三类证据不能互相替代。
负向用例可以替换单个摘要、交换模型与索引、使用旧语料快照、改变距离度量、提供未知格式或模拟激活时崩溃,观察系统是否保持旧组并拒绝混合状态。这些用例验证防御门禁,不需要公开真实模型、文档、包名或生产命令。测试报告应写明在哪一层拒绝、旧组是否继续可用以及临时文件如何处理。
当前没有具体候选包、模型文件、语料快照、索引产物与设备运行回执,因此本文只给出设计、代码和验收方法,不能宣称某个端侧 RAG 已经提升召回率、阻断篡改或消除泄露。落地时应把同一候选身份下的构建证明、文件摘要、负向用例和设备自检装入受控证据包;模型、语料、格式或授权策略变化后重新验收。
| 证据类别 | 最低检查 | 合格结果 | 不能推出 |
|---|---|---|---|
| 来源身份 | 签名者、版本、期限和目标渠道 | 元数据来自批准发布链 | 索引语义一定正确 |
| 文件完整性 | 模型、配置、语料清单和索引摘要 | 本地文件与目标清单一致 | 文件运行时不可提取 |
| 组内绑定 | binding digest 与 generation | 不存在跨版本混搭 | 真实业务召回率达标 |
| 运行时兼容 | 模型装载、算子、维度和数值 | 目标环境完成基本自检 | 所有设备和系统版本兼容 |
| 语义自检 | 固定查询、近邻和引用位置 | 查询编码与文档编码关系未明显错位 | 未知业务问题都能正确回答 |
| 授权边界 | 删除、权限收紧、离线和撤销 | 无权主体不获得受限片段 | 设备内历史明文已经抹除 |
| 事务恢复 | 中断下载、校验失败与激活崩溃 | 只保留旧组或完整新组 | 后续更新永远不会失败 |
| 供应链证明 | subject、materials、builder 和签名 | 产物可追溯到声明的构建过程 | 声明内容天然真实或运行时安全 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 移动模型元数据可以描述输入输出、归一化参数、标签和关联文件,因此模型行为不一定只由主权重文件决定。 | LiteRT model metadata | 元数据结构不提供索引签名、业务授权或防回滚结论,关联文件仍需单独验证摘要。 |
| ONNX Runtime Mobile 支持在 Android 和 iOS 运行模型,并需要考虑算子、包体与执行提供程序等约束。 | ONNX Runtime Mobile | 运行时支持不等于模型与索引语义兼容,也不提供签名、授权或防篡改保证。 |
| Core ML 提供 Apple 平台上的模型转换、集成、装载和预测能力。 | Apple Core ML | 框架能力不能证明模型运行时不可提取,也不替项目核对语料、索引和授权范围。 |
| 更新客户端可以通过签名、版本、过期时间和一致性关系识别回滚、冻结与目标混搭风险。 | The Update Framework specification | TUF 处理更新信任链,不定义向量空间兼容、语料权限或移动 RAG 的业务语义。 |
| 构建 provenance 可以把产物主体与构建者、构建类型、外部参数和依赖材料关联。 | SLSA Provenance v1.1 | provenance 只能证明所声明的构建记录,不能单独证明运行时安全、检索准确或授权正确。 |
| in-toto Statement 使用产物摘要作为 subject,并将其绑定到有明确类型的 predicate。 | in-toto Attestation Statement v1 | 声明结构不保证内容真实,仍需验证签名者、签名、策略和构建环境。 |
| 工程判断:嵌入模型、预处理、语料快照、切分规则、索引格式和授权范围必须共享一个可重算的绑定摘要。 | 工程判断:基于派生产物身份、向量空间兼容和最小授权原则 | 字段集合和格式需要按项目运行时与数据模型调整,不能把示例 manifest 当成通用标准。 |
| 当前没有具体候选包、模型、语料、索引和设备回执,不能断言端侧 RAG 的召回、篡改阻断或权限撤销已经验证。 | 项目证据尚未接入 | 文章仅提供公开可复核的设计、检查代码和验收矩阵,不包含客户案例、性能数字或产品实测结论。 |
工程常见问题
索引文件摘要正确,为什么还可能返回错误结果?
摘要只能证明索引文件没有偏离记录值,不能证明生成它的嵌入模型、预处理、语料和切分规则与当前查询链一致。旧索引与新模型可能使用相同维度并正常查询,却处在不同向量空间。必须验证整组 binding digest 和语义自检。
只绑定嵌入模型版本号是否足够?
不够。相同版本名可能对应重新转换、不同量化或不同关联文件。应记录实际移动模型文件摘要,并绑定 tokenizer、词表、预处理、向量维度、距离度量和运行时合同。版本号用于人类识别,摘要用于锁定真实产物。
文档删除后,为什么必须生成新的语料快照?
索引是文档集合的派生产物。仅从原始库删除文档,不会自动移除设备上的向量、片段正文和引用元数据。新快照需要表达删除状态并让相关 chunk 失效;紧急撤回还应通过短期授权或撤销机制限制旧离线索引。
TUF 能直接解决端侧 RAG 索引完整性吗?
TUF 可以提供签名元数据、版本、过期和一致性关系,帮助防止更新回滚、冻结与混搭,但它不了解向量维度、嵌入空间、切分规则和语料权限。RAG 协议仍需定义 binding digest、兼容合同和语义自检。
供应链证明存在是否代表索引一定安全?
不代表。provenance 与 attestation 让产物、构建者、参数和材料可追溯,但声明可能由不受信签名者签发,构建环境也可能不满足项目策略。验证时还要核对签名身份、门禁规则、文件摘要、运行时兼容和授权结果。
怎样验收索引更新不会造成半新半旧状态?
在隔离目录下载整组资产,验证签名、期限、每个文件摘要和 binding digest,完成装载与固定查询自检后,再原子切换唯一激活指针。测试应覆盖中断下载、摘要失败、格式不兼容和激活时崩溃,重启后只能看到旧组或完整新组。