可靠客户端项目与自测

项目目标与边界

在完全离线环境实现一个供应商无关的可靠客户端内核:严格请求/响应合同、暂时与永久错误、Retry-After、指数退避、抖动、attempt 与 retry deadline 双上限,以及 canonical 流事件状态机。mock transport 让失败顺序可重复,不需要 SDK、网络或密钥。

这不是一个真实 HTTP 客户端。它不替代 SDK 的连接/读取/流空闲 timeout,也不声称 canonical 事件名属于任何供应商。真实 adapter 负责把官方 SDK 异常、原始事件、usage 和 request ID 映射到本地合同,并单独接受集成测试。

项目文件

文件职责
reliable_client.py请求/响应、错误分类、重试策略、attempt 记录、mock transport 与 canonical 流状态机
test_reliable_client.py22 项单元测试,覆盖输入、退避、deadline、未知失败、流顺序、partial、终止状态与资源上限

运行环境

脚本只使用 Python 标准库。在 Windows 11 的 PowerShell 7 中,从同时包含 docs/ 与 .website/ 的项目根目录执行:

Push-Location -LiteralPath 'docs\LLM API集成'  # 进入课程目录,供 unittest 使用示例的相对路径。
py -3.11 --version  # 先核对实际运行的解释器与课程验证基线一致。
py -3.11 -B -W error -m unittest discover -s .\examples -p 'test_reliable_client.py' -v  # 普通模式运行可靠客户端回归测试,并把 warning 视为失败。
py -3.11 -O -B -W error -m unittest discover -s .\examples -p 'test_reliable_client.py' -v  # 优化模式重跑,验证生产门禁不依赖裸 assert。
Pop-Location  # 返回进入课程目录前的工作位置。

-B 禁止写入 .pyc。因为项目零依赖,验证时不在 vault 内创建 .venv;真实 adapter 项目应按 环境、依赖与密钥管理 在 vault 外使用 venv + pip 并固定 SDK 版本。

分步骤阅读与实验

1. 检查 canonical 合同

LLMRequest 保存稳定的 operation_id、提示版本、逻辑模型配置与输入;同一业务操作的所有 attempts 复用同一请求对象。LLMResponse 把 completed、refused、truncated 分开,并保留服务端 request ID、供应商、实际模型和原始用量单位。

这些类只验证本地字段形状。它们不会自动脱敏 user_text;生产日志必须使用 allowlist,不能序列化整个对象。

2. 验证重试只处理已分类暂时错误

call_with_retry() 只捕获 TransientError。认证、权限、无效请求、额度耗尽与能力不支持属于 PermanentError;adapter bug 等未知异常也直接暴露,不会因为“可能恢复”而盲目重试。

退避等待同时考虑:

  • 本地指数上限与有界抖动;
  • adapter 解析出的 retry_after_seconds 最小等待;
  • max_attempts;
  • retry_deadline_seconds。

测试注入假时钟、假 sleeper 和固定随机数,所以不会真实等待。retry_deadline_seconds 约束的是本层重试调度;真实 SDK 仍必须设置 per-attempt 的连接、读取/流空闲和单次调用 timeout。

3. 观察 attempt 证据

成功结果携带每次 attempt 的 outcome、结构化错误类别、供应商 request ID 和计划等待。operation_id 保持稳定,request ID 可以每次不同。耗尽时 RetryExhaustedError 区分 attempts_exhausted 与 retry_deadline_exhausted,并保留最后一个暂时错误。

这仍不自动保证外部副作用幂等。发信、扣费、写库或执行工具应在完整生成与校验之后,由受信任业务层使用自己的唯一键提交。

4. 验证流式状态机

本地 canonical 协议只有四类:response.started、response.text.delta、response.finished 与 response.failed。只有一个合法 finished 才返回 StreamResult;拒绝和截断作为显式 terminal status 返回,不等同于完成。

流中 failure 或连接正常结束但缺少 terminal event 时,异常保留 request ID 与 partial text,并把 partial 标记为失败状态。未知 canonical 事件、字段漂移、顺序错误、非法 usage 和终止后继续到来的事件都会失败。状态机把 canonical 事件数限制为 4096,并用同一个累计 text_chars 上限把完整文本限制为 100,000 字符;它没有另设单 delta 阈值,但每次 append 前的累计检查会同时拦住单个超大 delta 和许多小 delta。越界会抛出不可自动重试的 StreamLimitError,其中保留资源名、limit、observed、request ID 与有界 partial,既防止内存无限增长,也留下可审计终止证据。真实 adapter 可忽略已明确理解的供应商遥测事件,但必须先按当前官方协议作出显式决定,并在失败/取消时关闭真实 stream。

本机验证记录

2026-07-21 在 Python 3.11.9 上完成:

  • py -3.11 -m py_compile:脚本与测试通过。
  • 普通模式:22 项测试全部通过,warnings-as-errors。
  • python -O 模式:同一 22 项测试全部通过,关键门禁不依赖会被移除的裸 assert。
  • 测试不发网络请求、不读取环境变量、不创建真实等待,也不需要 API key。

未执行任何供应商 live API、SDK 兼容性、真实 timeout、限流头或流式事件测试,也未验证 Obsidian 阅读视图。

接入真实供应商的步骤

  1. 固定官方 SDK 版本,核对当前认证、timeout、默认重试、错误、限流、streaming、usage 和 schema 文档。
  2. 新建 adapter,把原始异常按结构化 code/type 映射;不要依赖错误消息字符串。
  3. 明确由 SDK 还是本层拥有重试,关闭或预算化其他层,避免请求数相乘。
  4. 从 secret manager 或环境变量读取密钥,不输出值;用最小非敏感请求做 smoke test。
  5. 将真实 typed events 映射到 canonical 状态,故障注入流中断、未知事件、拒绝、截断和不完整工具参数。
  6. 记录 operation/trace ID、每次 request ID、版本、usage、延迟与结果状态;不默认记录原始正文。
  7. 跑任务评测、受控集成测试和小流量验证后再上线。

扩展任务

  1. 先完成 三家 Provider 合同测试,理解 canonical 内核、Reference/SDK 事件投影与真实 wire/live 合同之间的边界。
  2. 再为一个锁定版本的实际 SDK 增加 adapter,但保留离线测试完全不联网。
  3. 注入 fake transport latency,增加 per-attempt timeout 与端到端业务 deadline 测试。
  4. 解析 RFC 9110 的 Retry-After delta-seconds/HTTP-date,再映射到 canonical 秒数。
  5. 增加本地并发/令牌桶调度,比较“提前排队”与“收到 429 后重试”的故障率。
  6. 为工具调用增加业务幂等键和状态存储,模拟“服务端已执行但响应丢失”。

自测题

  1. 为什么相同 429 可能分别属于暂时限流和永久额度问题?
  2. SDK 两次、外层三次、队列四次重试时最坏会有多少 attempt?
  3. retry deadline 与一次 SDK read timeout 分别约束什么?
  4. HTTP 200 和首个 text delta 为什么都不能证明流已完成?事件数与累计 text_chars 为什么要分别设限,而同一个累计上限又如何拦截单个超大 delta?
  5. 为什么中断后不能把旧 partial 与新生成直接拼接?
  6. operation ID、trace ID 与供应商 request ID 各自解决什么问题?

掌握检查

  • 我能运行普通与 -O 两组 22 项测试,并解释每类失败。
  • 只有已分类暂时错误进入一个有 attempts、deadline、退避和抖动的重试预算。
  • 永久与未知错误不重试;额度不足不会被当成普通 429 循环。
  • 完成、拒绝、截断、流中错误、缺少终止和未知事件各有明确状态。
  • 事件数或累计文本越界(包括单个 delta 直接使累计值越界)都会可审计失败,且不会释放 provisional 结果。
  • partial 不会被当成完整输出,外部副作用在完整校验后且具备业务幂等性。
  • 真实 adapter 的 SDK 版本、默认重试、timeout、事件与 usage 均由官方资料和集成测试证明。

主要参考资料

回到目录

继续 三家 Provider 合同测试,或返回 LLM API 集成学习目录。