跳转到内容

第 55 章 LLMOps I:版本记录、运行追踪与发布回滚

昨天,助手还在回答 750。一次配置修改之后,同样的问题却得到 600,引用看起来也没有问题。维护者首先记下“模型没变”。这句话虽然属实,却几乎帮不上忙:当前服务采用原文摘录,变化发生在组织答案之前。宿主把用户的 2026 年日期替换成了 2025 年日期,随后准确找到了对原问题不适用的制度,并如实引用了它。

第 54 章让请求过程可以观察。本章还要确定,究竟是什么产生了这次结果,哪些修改可以开始接收请求。LLMOps 指围绕 LLM 应用持续开展的工程工作,包括维护版本、证据、发布和运行。这些机制同样适用于没有调用神经模型的某条路径,因为它们管理的是系统行为,不是模型品牌。

55.1 记录所有可能改变答案的依赖

Section titled “55.1 记录所有可能改变答案的依赖”

只保存最终文字,足够让人重新阅读,却不足以解释答案怎样产生。即使再保存问题和模型名,仍然不知道当时提供了哪份制度、怎样选中它、周围有哪些指令,以及哪些检查接受了结果。

例 55.1:同一住宿请求的版本记录。 输入仍用例 54.1 的 Beijing lodging,日期为 2026-09-14,主题为差旅,语言为英文。服务给接纳的请求分配一份不可变的版本清单。清单(manifest) 以结构化方式记录本次一起选用的组件和配置。这里说“不可变”,是指请求保留自己的快照;切换活动候选,不会改写这份快照。

表 55.1:清单记录什么,为什么要记

组件本地实际身份什么变化可能影响结果
代码与运行环境文件的 SHA-256 摘要;Python 3.12.10;jsonschema 4.25.1守卫条件、解析或日期处理改变
回答/模型程序原文摘录适配器;没有神经模型权重换成模型后,必须按契约重新评测
文本切分沿用的词法切分程序与词表字节;没有神经模型 Tokenizer查询词变化可能改变排序
提示词与上下文KA-2 内容及上下文契约文件;轨迹中的实际证据范围遗漏或额度变化可能删掉必要资料
数据未改动的逐行 v1/v2 资料库及其摘要原文、来源或生效区间改变
索引BM25 构建规则、索引卡片字节、A—E 候选顺序、稳定并列规则进入上下文的候选或分数改变
输出结构ka-answer-v1 schema 的字节可接受类型或必需引用改变
工具与权限区分租户的只读存储;独立的只读 MCP 实现所有权检查或允许操作改变
Skill来源核验 1.0.0,以及已审查资源的摘要辅助程序可能悄悄接受无效来源
发布配置与界面日期覆盖设置、回答缓存有效期、界面与内容资源字节错误解释日期,或误导用户

SHA-256 从文件字节计算出固定长度的摘要。清单保存完整的 64 个十六进制字符,不只保留便于显示的短前缀。与已审查摘要匹配,可以帮助发现内容变化,却不能证明内容安全,也不能说明是谁批准了它。rc-a 这样的版本名只是便于交流的标签;整份清单的摘要才标识一起选定的资源和配置。

本地默认路径明确记录:没有神经模型权重、神经模型 Tokenizer 和采样种子,而不是填入虚构值。词法运算仍使用 binary64,分数仍按已有的舍入与并列约定处理。如果加入神经模型适配器,就要补记权重或不可变的提供方快照、Tokenizer、提示模板、解码选项、支持时的随机种子,以及执行环境。一个不变的 API 模型名,未必能标识提供方内部的具体字节。

本项目的索引由很小的固定卡片重建,所以“构建规则加输入字节”可以标识它。大型持久索引还需要记录构建产物,以及它与资料库、嵌入模型之间的兼容关系。两个都叫“current”的索引文件,不一定包含相同知识。反过来,保留归档 v1 是正确的,因为回答历史日期问题仍然需要它。

750 元答案的依赖关系
  1. 模型之外资料库与词法切分 → 索引
  2. 应用保存的记录原问题日期与 v2 → 上下文
  3. 处理步骤回答程序、结构与 Skill → 答案

即使资料库、模型字段和结构不变,改日期仍会改上下文。清单标识整组选择。

回滚前后的请求接纳
  1. 应用保存的记录回滚前:错误日期 → 600
  2. 处理步骤把活动候选恢复为 A
  3. 应用保存的记录新任务:A → 750;旧任务仍为 600

回滚影响新任务。旧任务要另外取消或等待完成;恢复配置不会反转订单副作用。

图 55.1——恢复兼容的依赖,同时保留每次请求的版本归属。 本书原创图示 · CC BY-SA 4.0。

图 55.1 说明了为什么版本构成依赖图:索引依赖文档与切分规则,上下文依赖原问题与选中的证据,最终答案则同时依赖答案组织和核验。回滚必须恢复一组兼容的组件,不能只修改最先被注意到的那个版本号。

运行轨迹(trace) 把属于同一请求的操作联系起来。跨度记录(span) 描述一项有起止范围的操作,记录开始、结束,以及它与另一项操作的关系。本例的根跨度覆盖整个已接纳任务,子跨度覆盖检索/组织答案和来源 Skill。事件则记录接纳、缓存查询、尝试调用和进入终止状态等变化。这样的关系参考了 OpenTelemetry,本地仍使用小型 JSON 格式,没有声称实现 OTLP 导出器或符合完整 SDK 规范。

例 55.2:经过的时间,与其中包含的工作。 给定一组教学时间:根跨度从 0 到 220 ms,检索/组织答案的子跨度从 70 到 100 ms,Skill 子跨度从 100 到 200 ms。整个请求历时 220 ms,检索用 30 ms,Skill 用 100 ms。把根与子跨度相加得到 350 ms,却重复计算了本来已经包含在根跨度中的时间。没有落在两个子跨度内的 90 ms,来自它们开始前的 70 ms 和结束后的 20 ms。要把这些时间归到具体原因,还需补充观测点。

若两个子跨度重叠,光是把子跨度的耗时相加,也可能超过父操作等待的时间。嵌套表示关系,不表示应该把每个数字都加起来。实际记录使用测得的单调时钟时间,并保留连接子跨度与请求的 ID。轨迹能帮助定位较慢的阶段,却不能据此认定延迟来自模型内部的解题推理。

日常诊断文件保存 ID、版本身份、事件和跨度,不保存凭据、问题原文或回答正文。公开实验包另外保留完整输入输出,是因为所有账户与制度都是虚构的。两类记录有意分开。把生产提示词复制进公开的“复现实验”目录,会带来新的隐私问题;给很短的私密问题算一个哈希,也不能阻止别人枚举猜测。

采样还会改变轨迹集合能说明什么。如果只保留每十次请求中的一次,找不到失败轨迹,不代表没有失败请求。服务计数与保留上限因此需要单独核算。第 56 章会明确统计窗口,不把一个轨迹文件夹直接当成全部请求。

55.3 常被混称为“回放”的四种操作

Section titled “55.3 常被混称为“回放”的四种操作”

同事说:“把那次坏请求回放一下。”动手前,先确定他想得到什么。本例中,四种操作产生的影响不同。

表 55.2:重复了什么,什么可能改变

操作读取或执行什么结果与影响边界
阅读已存轨迹原有事件、输入和输出字节查看当时的记录,不生成新答案,也不操作订单
重新计算只读请求在声明的清单下重新执行检索与摘录得到一次新观察,可以与旧结果比较
再次调用模型重建输入,调用模型及运行程序固定设置也不普遍保证相同输出
恢复业务操作先查持久意图、审批与回执,再决定下一步可能涉及受控写入,不能由播放轨迹推导出来

本地实验把失败轨迹读取两次,得到相同记录,没有新增模型调用,也没有新增订单副作用。回滚后,实验又实际发送了一次新的 HTTP 请求,得到 750。这是恢复配置后的新请求,不能证明历史上的 600 从未返回过。

随机种子是随机过程的一项输入,不是普遍有效的复现保证。模型或运行程序变化、执行顺序、浮点运算,以及提供方不再提供旧版本,都可能阻碍精确再生成。即使分布保持不变,一次新的随机调用也未必重复某一次抽签结果。应当说明承诺的是哪一层:已存字节相同、条件可比,还是独立重复得到某个结果。本例确定性的摘录回放,比通用模型的复现主张窄得多。

如果订单已经取消,只是响应丢了,正确的下一步是第 49 章依据回执恢复。毕业项目重新执行这项检查后,再次观察到:重开数据库后副作用仍为一次,读取事件不增加副作用。“再执行每个 span”或者“把回答模型回滚”,都不能安全地替代它。

55.4 让失败候选在接收普通请求之前失败

Section titled “55.4 让失败候选在接收普通请求之前失败”

例 55.3:有独立答案依据的发布门。 候选 A 保留用户日期,回答缓存有效期为 30 秒。候选 B 同样保留用户日期,只把有效期改成 10 秒。人为设置的错误候选则把每个问题日期都覆盖为 2025-09-14。资料库和可执行代码完全相同。这是配置实验,不是在比较新训练的模型。

运行候选之前,作者已经依据制度生效区间,为每种语言规定了五个案例。中英文使用相同标准和对应问题,预期值单独放在 expected-v1.json,不由实现自行产生。

表 55.3:三个候选在同一组十个案例上的实际评测

案例,两种语言各执行一次要求的结果AB错误的日期覆盖
现行日期:2026-09-14750,引用 v2通过通过失败:600,引用 v1
v2 生效首日:2026-01-01750,引用 v2通过通过失败:600,引用 v1
v1 适用末日:2025-12-31600,引用 v1通过通过此例通过
缺少资料的主题:火山暂不作答,不附引用通过通过通过
2026 年的审批规则有来源支持的审批制度摘录通过通过失败:没有适用来源
两种语言合计十个案例结果10/1010/104/10

现行日期的失败尤其有解释力。错误答案准确引用了 v1,也通过了针对内部替换日期的检查。JSON 可以有效,引用可以准确,用户任务却仍然失败。只检查解析或引文匹配的发布门,会漏掉这个缺陷。独立预期必须保留用户原来要求的日期。

本例的评测发布门(evaluation gate) 要求这些范围明确的回归案例全部通过。切换入口还检查候选字节,并把报告绑定到清单摘要:A 的通过报告不能批准 B。更完整的发布检查表另外要求软件/安全检查仍然有效、上一候选可用、已经演练回滚,并且有明确的操作负责人。十个案例的报告不能代替这些条件,也不能代替面向真实总体的回答准确率评测。

用三个布尔值表示决定:II 表示所选清单匹配,QQ 表示必需的任务回归通过,OO 表示运行检查表已经完成。于是,本地发布决定为:

L=I∧Q∧O.L=I\land Q\land O.

∧\land 表示所有条件都要成立,不是求平均。如果错误候选满足 I=1I=1、Q=0Q=0、O=1O=1,那么 L=0L=0。再增加一千个容易通过的案例,也不能抵消违反硬性要求的事实。这是由人负责落实的发布契约;小型 switch 方法实现清单与报告检查,其余检查表仍要由操作负责人确认并记录。

55.5 先限制影响范围,再练习怎样恢复

Section titled “55.5 先限制影响范围,再练习怎样恢复”

通过发布门,可以排除已经知道的回归问题,却不能证明每一个未来请求都会表现良好。灰度验证(canary) 让候选在有限时间内接收一小部分工作,同时保留对照。应当在相关条件下比较两者,并事先规定什么情况必须停止。否则,“几位用户看起来很满意”很容易变成未经审查的全面切换。

例 55.4:本地分组与回滚演练。 程序把十个请求编号为 0 至 9,编号能被十整除的交给候选 B,其余交给 A。只有 0 符合,因此 B 接收这组给定请求的 1/10=10%1/10=10\%,A 接收 9/10=90%9/10=90\%。十个请求都实际经过本地 HTTP 服务,并返回 750。确定性分配展示的是分组规则,不是证明 B 更安全或更快的随机实验。候选只接收了一次请求,无法据此估计有意义的可靠性。

随后,程序把活动配置切换为 B。为了练习恢复,又在隔离的本地演练中明确绕过错误候选未通过的发布门。覆盖操作标记为 forced_local_fault_drill,不会通过用户接口提供。一次现行日期请求实际返回 600。操作负责人恢复 A 后,下一个请求带着 v2 返回 750。切换日志保留每一步的前后清单及时间。

另一个在回滚前已经接纳的错误候选请求,被有意保持在运行中。它仍保留原清单,回滚后也仍返回按历史日期得到的结果。回滚改变的是新请求的接纳规则,不能悄悄重写已经接纳的工作。如果事故中不允许旧任务继续执行,就要另外取消,或停止接纳并等待已有任务结束,同时说明受影响用户遇到了什么。这项策略也需要测试。

本次演练回滚的是配置,可执行代码不变,也没有数据迁移。它没有证明可以撤销破坏性的数据库结构迁移、恢复已删除资料,或反转外部订单取消。这些变化需要兼容规划、保留数据,并在适用时进行业务补偿。把它们都笼统叫作“回滚”,会掩盖恢复成立的条件。

55.6 把候选与支持它的证据一起保留下来

Section titled “55.6 把候选与支持它的证据一起保留下来”

三个候选清单、发布门逐项结果、失败与恢复的 HTTP 轨迹、分组及切换日志,都保存在 data/part-ix/。运行入口是 code/part-ix/run_records.py。它复用前文机制,写入新的 Part IX 记录,不改写此前模型或基线结果。Notebook 阅读这些关系,并执行小型只读检查;运行手册给出完整的本地演练命令。

维护者应当在审查源文件字节之后保存候选。若每次都根据磁盘现状重新生成清单,再宣布它已经获准,就抹掉了“记录现状”和“完成审查”的区别。本项目的常规测试核对已存证据哈希;有意重建记录,是单独的作者命令。将来代码变化后,原候选就失效了,需要新的证据。

KA-10 现在已经有版本关系、请求轨迹、明确的回放边界、针对任务的发布门,以及实际执行的本地回滚。接下来的责任在运行阶段:工作重复、依赖失败时,服务愿意消耗多少容量、让用户等待多久,才会保护自身并说明情况?

一次发布是一组彼此兼容的代码、数据、处理规则和用户可见行为。清单标识这组选择,轨迹记录按它执行的工作,独立发布门检查用户要求。读取旧记录、重新计算、再次调用模型和恢复副作用,是四种不同操作。本地灰度与回滚让分流和恢复过程可检查,也保留了在途请求的版本归属,以及仅回滚配置的适用边界。

  1. 团队保留了模型权重与种子,却更换 Tokenizer 和上下文模板。还可以作出哪些比较结论?再次运行之前,还要记录什么,才能解释新结果?
  2. 根跨度历时 500 ms。两个子跨度各历时 300 ms,彼此重叠 200 ms,而且都完全位于根跨度内。至少被一个子跨度覆盖的时间是多少?两个都没覆盖的时间是多少?为什么请求耗时不是 1,100 ms?
  3. 候选通过了 999 个简单问题,却对现行住宿问题引用 v1。契约把现行日期下的来源正确性列为硬要求。99.9% 的总分能否让它通过?应怎样设置发布门?
  4. 一个请求在回滚前刚被接纳;另一笔订单在响应丢失前已经完成取消。模型/配置回滚能撤销其中哪个事实?分别应当怎样处理?
  5. 本地灰度给候选分配了十个给定请求中的一个,它通过了。写出两个有证据支持的结论,以及两个没有证据支持的结论。
  1. 旧输出仍是真实的历史记录,但新调用已不再使用相同的输入处理。应补记 Tokenizer 的字节/版本、最终输入与模板、上下文选择和执行设置,把新运行当成条件改变后的比较。相同种子不能恢复旧条件。
  2. 子跨度覆盖时间的并集为 300+300−200=400300+300-200=400 ms,另外 500−400=100500-400=100 ms 不在两者之内。把根与两个子跨度相加,会重复计算嵌套及重叠时间;请求仍历时 500 ms。
  3. 不能。来源/日期硬要求失败,对应发布门就是假,不受另外 999 个结果影响。应保留失败,修复或拒绝候选,再运行相同的相关案例。总体分数可以另报,不能用它掩盖硬性失败。
  4. 两个事实都不会自动撤销。已接纳请求保留自己的快照,除非另外取消或等待它结束;已提交的订单操作要依据回执恢复,必要时再取得补偿操作的独立授权。盲目重试或重新执行跨度可能增加副作用。
  5. 可以确认分流实现把一次请求交给候选,也可以确认这次请求满足所声明的标准。不能据此证明生产可靠性,也不能证明候选相较对照造成了质量或延迟改善。影响范围、分配方法和工作负载必须交代清楚。
  • OpenTelemetry Tracing API,SpanContext、Span、Span Creation、IsRecording。支持跨度关系,以及抽样轨迹与完整计数的区别;本地格式有意保持更小的范围。
  • Google SRE Workbook:Canarying Releases,定义与发布工程原则。支持限制候选影响范围和管理兼容版本;其中生产环境的观察不是本书服务的实测。
  • NIST AI 600-1,Generative AI Profile,MS-2.3、MS-2.5,印刷页码第 30—31 页。支持记录评测条件,避免从范围很窄的演示外推能力。