批处理、缓存与可靠性

本节目标

你将把一次 embed(text) 调用扩展成可恢复流水线:输入有稳定身份,批次遵守多重限制,响应逐项对账,失败有界重试,缓存绑定完整空间,写入前校验维度/有限值/范数/哈希,日志不泄露正文。

先在隔离环境试验

Windows 11 / PowerShell 7 的基础方式仍是 venv + pip。在 vault 外的练习目录执行,并确保 .venv/ 被忽略,不要提交虚拟环境:

python -m venv .venv  # 在当前练习目录创建隔离虚拟环境,避免污染系统 Python。
.\.venv\Scripts\Activate.ps1  # 在 PowerShell 会话中激活刚创建的虚拟环境。
python -m pip install --upgrade pip  # 用当前解释器对应的 pip 升级安装工具。
python -m pip install package-name  # 将占位符换成已确认版本的官方 SDK 或本地运行库。

最后一行的 package-name 只是占位,应替换为所选官方 SDK 或本地运行库,并固定经过验证的版本。掌握基础后可再使用 uv 管理环境;换工具不会替代锁定依赖、模型 revision 和复现实验。

本库离线项目不需要安装第三方包。

工作项而不是裸字符串

进入队列前,把每个输入变成稳定工作项:

{
  "item_id": "chunk-018",
  "source_revision": "rev-7",
  "role": "document",
  "input_sha256": "<hash of exact embedding input>",
  "space_id": "candidate-2026-07",
  "attempt": 0,
  "state": "pending"
}

JSON 不支持合法的行尾注释:item_id 关联 canonical 输入,source_revision 固定来源版本,role 选择官方 query/document 路径,input_sha256 绑定实际送入模型的文本,space_id 防止跨空间缓存命中,attempt 计数重试,state 驱动可恢复的状态机。保持代码块为纯 JSON,才能直接作为队列样例解析。

实际正文可从受控内容存储按 ID 读取,不要复制到普通队列日志。input_sha256 必须针对真正送入模型的文本,包括模型要求的标题、query/document prefix 或任务指令;只哈希展示正文会让缓存与向量来源不一致。

推荐状态:

pending → submitted → received → validated → indexed

失败分为:

  • retryable:网络中断、限流、明确的临时服务错误;
  • permanent:认证、权限、非法角色、超长输入、坏 payload;
  • unknown outcome:客户端超时但不确定服务端是否完成,需要 request ID/幂等策略和成本对账;
  • quarantined:反复异常、需人工检查的单项。

批次同时受多种上限约束

只写“每批 100 条”不够。可能同时存在:

  • 最大 item 数;
  • 每项输入长度;
  • 批次总 token/字符;
  • HTTP payload bytes;
  • 模型/设备显存;
  • 响应体和数据库事务大小;
  • 并发与每分钟/每日配额。

打包器应按官方限制和安全余量执行 first-fit 或顺序装箱,并在加入下一项前检查所有约束。一个超长 item 应单独拒绝/重切,不要让整批无限重试。

对于本地模型,还要记录实际 padding 浪费。把相近长度分桶可能提高吞吐,但排序后必须用 item_id 恢复对应关系。

不要盲目 zip 输入和响应

可靠 adapter 要定义并验证:

  1. 请求包含哪些稳定 ID;
  2. 服务是否保证响应顺序;
  3. 响应是否带 index/ID;
  4. 部分失败如何表达;
  5. usage 是按请求还是逐项返回;
  6. 重试后如何去重。

即使官方当前保证顺序,adapter 也应检查:

  • 响应数量等于期望;
  • 每个 index/ID 唯一且在范围内;
  • 没有漏项或重复项;
  • 每个向量通过空间校验;
  • 再按 item_id 写入。

不能只做 zip(items, vectors) 并假设网络、SDK 和批处理永远完整有序。

写入前的向量校验

对每项执行:

  • 长度等于 contract.dimension;
  • 每个值是数值而非 boolean;
  • 没有 NaN 或 Inf;
  • L2 norm 有限且大于 0;
  • 若 contract 声明 normalized,则 norm 在约定容差内接近 1;
  • role、model/revision、dimension、metric、dtype 与目标索引一致;
  • 返回项对应当前 source/input hash;
  • ACL、tenant 与 source metadata 不缺失。

“API 返回 200”只说明请求成功,不说明向量属于正确输入和空间。Norm 分布突然整体改变可能提示角色、预处理、模型 alias 或归一化变化,应触发告警与样本回归。

缓存键必须描述整个空间

一种可审计形式:

也可直接哈希规范化 JSON:

from hashlib import sha256  # 导入 SHA-256,用于生成固定长度且不可逆的缓存键。
import json  # 导入标准 JSON 编码器,保证复合契约能稳定序列化。
from typing import Any  # 导入 Any,表示 contract 的字段值可包含多种 JSON 类型。
 
 
def cache_key(contract: dict[str, Any], input_sha256: str) -> str:  # 把完整空间契约和实际输入哈希压缩成一个缓存身份。
    payload = {  # 先显式构造需要参与身份计算的最小对象。
        "contract": contract,  # contract 内应含 provider、model、revision、role、维度和归一化等字段。
        "input_sha256": input_sha256,  # 只接受已针对真实 embedding 输入计算出的哈希。
    }  # 字典结束;任何缺失的契约字段都可能导致错误复用,调用方应先校验。
    encoded = json.dumps(  # 在本教学 Python 进程中稳定序列化;它不是跨语言的 JSON Canonicalization Scheme。
        payload,  # 编码上面构造的完整缓存身份对象。
        ensure_ascii=False,  # 保留非 ASCII 字符,避免不必要的转义差异。
        allow_nan=False,  # 拒绝 NaN/Infinity,防止生成不属于标准 JSON 的缓存键。
        sort_keys=True,  # 固定字典键顺序,避免同一 Python 对象的插入顺序改变缓存结果。
        separators=(",", ":"),  # 去除空白,确保格式变化不会改变同一对象的身份。
    ).encode("utf-8")  # 将 JSON 文本转成明确的 UTF-8 字节,供哈希函数处理。
    return sha256(encoded).hexdigest()  # 返回十六进制摘要;它是键而不是原文或向量。

这里的 sort_keys=True 只解决 Python dict 的键顺序;它没有规定跨语言的数字、Unicode 字符串或所有 JSON 值如何得到同一字节串。因此此示例适合受限字段、同一受控运行时的缓存键,不应被宣称为通用的“规范 JSON”或签名格式。若键要跨服务/语言共享,先冻结字段 schema 与序列化 revision,并使用经实现验证的跨语言 canonicalization(例如 RFC 8785 JSON Canonicalization Scheme)或直接把已版本化的字节协议作为输入;同一契约必须配跨实现测试向量。

模型别名、revision、query/document role、维度或归一化不同,都不能误命中旧向量。若 SDK 版本会改变默认预处理,也应纳入可追踪构建信息。

缓存向量是原文的派生产物,不应默认公开。它继承 tenant、ACL、保留与删除策略;备份和调试导出也要受控。

有界重试与总尝试预算

沿用 [[Knowledge/AI Agent Engineer/docs-CN/API/00-目录|API]]与 [[Knowledge/AI Agent Engineer/docs-CN/LLM API集成/00-目录|LLM API集成]]的模式:

  • 显式 connect timeout、read timeout 与总 deadline;
  • 对 429 和明确临时错误使用有界指数退避 + jitter;
  • 尊重官方 Retry-After 或等价 header;
  • 认证、权限和输入 4xx 修复后再试,不原样重放;
  • SDK 内重试与外层队列重试合并计算总次数;
  • 已验证成功项按 item/cache key no-op,不重复重算;
  • 保存 provider request ID、attempt、错误类别、items/tokens、duration 和费用归属。

若一次批次中只有一项永久失败,可二分/隔离该项;不要重试整批并让成功项反复计费。是否支持服务端幂等 key 取决于当前 API,必须查提供商文档,不能假设。

本地模型的复现契约

至少记录:

  • 模型仓库与不可变 revision;
  • tokenizer revision;
  • 库版本、Python、OS;
  • pooling、prompt、normalize、dimension;
  • device、dtype、batch size;
  • 最大输入与截断策略;
  • 是否 eval()、是否使用近似/量化 backend;
  • 一小组 canary 输入的输出 shape、norm 和检索排序。

不要声称跨 CPU/GPU、驱动或精度能 bitwise 一致。发布门禁应关注容差、排序与业务指标,而不只是逐浮点完全相等。

可观测性与隐私

记录:

类别建议字段
身份space_id、job/batch/item ID、source revision
性能queue wait、request duration、items/tokens、throughput
可靠性attempt、HTTP/provider code、retry class、部分失败
质量dimension、norm 分布、zero/nonfinite/rejected count
成本cache hit、billable units、估算/实际费用
追踪provider request ID、deployment/model revision

默认不要记录 API key、完整正文、完整向量或用户查询。排障样本用授权 ID 回查,必要内容做最小化、脱敏和时限控制。

失败恢复流程

以 1,000 个 chunks 为例:

  1. 从 canonical source 生成 work items;
  2. 缓存命中且完整契约相同的项标记 validated;
  3. 其余按 item/token/payload 装批;
  4. 每批提交并保存 request ID;
  5. 校验响应,成功项逐项持久化;
  6. 临时失败项回队列并增加 attempt;
  7. 永久超长项回到 Chunking,其他永久错误隔离;
  8. 对 expected item IDs - validated IDs 做差集;
  9. 全量数量、ACL、dimension、norm、hash 对账后才发布。

恢复任务从状态表继续,不从头重跑整个语料。

常见错误与排查

  • 吞吐突然下降:检查长度分布、padding、配额、重试放大和缓存命中。
  • 向量与 ID 错位:检查响应顺序/索引、批次拆分和排序恢复。
  • 缓存命中却质量漂移:缓存键缺 model revision、role 或 input hash。
  • 某一坏项卡死队列:没有永久错误分类与隔离。
  • 费用翻倍:SDK 与外层多层重试,或未知结果整批重放。
  • 数据库正文与向量不一致:静默截断或只哈希原始正文。
  • 日志泄露文本:错误对象直接序列化了请求 payload。

练习

  1. 为 1,000 个不同长度 chunks 设计同时限制 item 数、总 token 和 payload bytes 的装批器。
  2. 第 7 批第 3 项永久超长时,画出其他项如何继续、该项如何回到 Chunking。
  3. 列出一个完整缓存键;删除 role 后构造一次错误命中。
  4. 为“服务端完成但客户端超时”写出请求 ID、状态与成本对账方案。
  5. 设计 norm 分布 canary:正常范围、告警条件、回查信息和禁止记录的内容。
  6. 比较本地 CPU float32 与 GPU float16 时,定义比 bitwise equality 更合理的验收指标。

掌握检查

  • 我在 vault 外用 venv + pip 建隔离环境,并不提交 .venv。
  • 批次同时遵守 item、长度、payload、内存和配额上限。
  • 每个输入/响应用稳定 ID 对账,不盲目 zip。
  • 写入前校验空间、维度、有限值、norm、hash 与 ACL。
  • 缓存键包含完整空间契约和实际输入哈希。
  • 重试有总预算,永久错误会隔离,未知结果会对账。
  • 日志保留可诊断 metadata,不记录密钥、全文或完整向量。
  • 失败恢复从已验证状态继续,而不是全量重跑。

小结与下一步

可靠批处理保证“每个向量确实属于这个输入和空间”。下一步要保证检索配置与空间一致,并在模型升级时不混写、不停机、不丢删除:[[Knowledge/AI Agent Engineer/docs-CN/Embedding/04-相似度索引与版本迁移|相似度、索引与版本迁移]]。

参考资料

来源获取日期:2026-07-22。具体批次、配额、错误码与重试头属于动态 API 事实,应在接入当天查对应官方参考。返回 [[Knowledge/AI Agent Engineer/docs-CN/Embedding/00-目录|Embedding 目录]]。