6.2 上下文版本与调试


6.2 上下文版本与调试

本节摘要:动态层每轮都在变——驱逐在删条目、压缩在改写历史、检索在进新料——出事故的那天,你必须能回答"模型当时到底看见了什么"。本节给三条纪律。快照:每轮组装完成后落一份上下文快照(条目结构 + 四成分账本 + 组合版本号),这是可观测的最小单位;没有快照的调试是盲调。版本号:3.1 节给系统提示立过版本,但一轮上下文由多个版本共同决定——完整组合是"系统提示 × 组装器 × 记忆策略 × 检索配置",任一变更即新组合,须过评测;版本号记进每轮日志,"当时是 v 几"的答案是一组而不是一个。调试工作流:症状 → 三问归位(1.2 节的第四种用法)→ 快照 diff → 定位成分与策略 → 修复后回放。附 ce_snapshot.py 快照与轮间 diff 示意,末尾给账本指标与告警水位。压缩、驱逐、检索从此都有据可查。

学习目标

阅读完本节,你应当能够:

  1. 说出上下文快照的最小结构,为自己的应用落一份。
  2. ce_snapshot.py 的 diff 定位一次驱逐或压缩事件。
  3. 写出自己系统的组合版本号清单,并说明每个版本管什么。
  4. 按标准工作流定位一次上下文事故(含复述三个典型案例)。

一、可观测的最小单位:快照

问一个刺的问题:上周四 15:02 那次糟糕的回复,模型当时看见了什么?答不出来的系统,一切调优都是盲调——你在调一个无法复现的现场。可观测的最小单位是每轮快照:组装完成后、发送请求前,把本轮窗口落一份存档:

snapshot 轮次 31(结构示意) ├── meta:组合版本号(见第三节)、时间戳、会话 ID ├── items:逐条目 {标记, 成分, 轮次, token 数, 内容头部 40 字} ├── ledger:四成分账本(沿用 ce_budget.py 的 audit() 口径) └── total:总占用与可用额度的百分比

两个取舍:存头部不存全文——快照用于定位("那轮到底有没有那条红线"),头部 40 字足够回答有无;全文可按需存对象存储(配合 6.1 节的 offloading,原文本来就该在外面试)。每轮都落——事故不挑时间,抽样快照等于没有不在场证明的那天恰好没拍。

二、ce_snapshot.py:快照与轮间 diff

# ce_snapshot.py —— 上下文快照与轮间 diff(写法示意,以实际工程为准) import json def est_tokens(text: str) -> int: # 与 ce_budget.py 同口径 cn = sum(1 for ch in text if "\u4e00" <= ch <= "\u9fff") return cn + (len(text) - cn) // 4 def snapshot(turn: int, entries: list[dict], versions: dict) -> dict: """每轮组装完成后调用:落一份含组合版本号与四成分账本的快照。""" items = [{"key": [e["tag"], e["turn"]], "kind": e["kind"], "tokens": est_tokens(e["text"]), "head": e["text"][:40]} for e in entries] ledger: dict[str, int] = {} for it in items: ledger[it["kind"]] = ledger.get(it["kind"], 0) + it["tokens"] return {"turn": turn, "versions": dict(versions), "items": items, "ledger": ledger, "total": sum(ledger.values())} def diff(prev: dict, curr: dict) -> list[str]: """两份快照的差异报告:新增 / 消失 / 膨胀——驱逐与压缩在 diff 里现形。""" def index(s: dict) -> dict: return {tuple(i["key"]): i for i in s["items"]} p, c, lines = index(prev), index(curr), [] for k, it in c.items(): if k not in p: lines.append(f"+ {it['kind']}/{k[0]} @{k[1]}: {it['tokens']} tok") elif it["tokens"] != p[k]["tokens"]: lines.append(f"~ {it['kind']}/{k[0]} @{k[1]}: " f"{p[k]['tokens']} -> {it['tokens']} tok(压缩或截断痕迹)") for k, it in p.items(): if k not in c: lines.append(f"- {it['kind']}/{k[0]} @{k[1]}: 被驱逐") return lines if __name__ == "__main__": # 演示:第 30、31 轮快照的 diff(示意) v = {"prompt": "v12", "assembler": "v3", "memory": "v2", "retrieval": "v5"} s30 = snapshot(30, [ {"kind": "指令", "tag": "task", "turn": 1, "text": "用户要求对订单 88880 部分退款并保留会员权益"}, {"kind": "素材", "tag": "stale_tool_output", "turn": 12, "text": "log " * 600}, {"kind": "素材", "tag": "early_chat", "turn": 3, "text": "早期试错与闲聊讨论" * 8}, ], v) s31 = snapshot(31, [ {"kind": "指令", "tag": "task", "turn": 1, "text": "用户要求对订单 88880 部分退款并保留会员权益"}, {"kind": "素材", "tag": "stale_tool_output", "turn": 12, "text": "log " * 50 + "…[中间截断]…"}, {"kind": "素材", "tag": "retrieved_doc", "turn": 31, "text": "《退款政策》v8 第 3 条:二手商品不支持无理由退货……"}, ], v) print(json.dumps({"versions": s31["versions"], "diff": diff(s30, s31)}, ensure_ascii=False, indent=1)) # 预期输出(示意): # + 素材/retrieved_doc @31(本轮新检索) # ~ 素材/stale_tool_output @12: 1500 -> 176 tok(丢大块保线索) # - 素材/early_chat @3: 被驱逐

diff 的三种符号对应动态层的三种事件:+ 是新进窗口的内容(检索、召回、新工具结果),- 是被驱逐的条目,~ 是同一条目的 token 突变——除了截断,最常见的就是压缩改写。驱逐顺位错了、压缩误伤禁区、检索重复注入,在 diff 里都是一行就能看见的事实。

三、版本号纪律:从单版本到组合版本

3.1 节给系统提示立过版本纪律(唯一真相源、变更记录、改动过评测)。现在把它推广:一轮上下文不是由系统提示一个版本决定的,而是由一组版本共同决定

版本 管什么 变更示例
prompt 系统提示(四块模板) 边界块新增禁止项
assembler 组装器(分配表、驱逐顺序、位置编排) 驱逐顺位调整、K 从 4 改 5
memory 记忆策略(写入门槛、召回额度、TTL) 写入门槛收紧
retrieval 检索配置(索引版本、k、阈值) 知识库重建索引、k 收紧

三条纪律:组合记录——每轮快照的 meta 里记全四个版本(ce_snapshot.pyversions 字段),任一变更即新组合;变更过评测——3.1 节对系统提示的要求推广到全部四项:改驱逐顺序与改系统提示一样是高风险变更;回放能对上——事故复盘时,"当时是 v 几"的答案是一组(prompt v12 × assembler v3 × memory v2 × retrieval v5),而不是一个。

💡 组合版本号是 A/B 与回归的地基:两组对话效果差异,先 diff 版本组合,再 diff 快照内容——版本相同而内容不同,问题在数据或策略执行;内容也相同,问题在模型侧或产品侧。

四、调试工作流与三个典型案例

症状(模型行为反常) ↓ ① 三问归位:把当轮窗口内容逐段过三问(1.2 节第四种用法) ↓ ② 快照 diff:拉事故轮与前一轮的快照对比,看 + / - / ~ 各是什么 ↓ ③ 定位:成分错位?驱逐误伤?检索没进?版本漂移? ↓ ④ 修复:改对应层的策略 / 版本,登记变更 ↓ ⑤ 回放:用事故轮的快照重放请求,验证修复生效

三个典型案例,覆盖三类根因:

案例 症状 diff 证据 根因与修复
红线失忆 第 30 轮模型做出政策外承诺 - 记忆/redline @25: 被驱逐 红线只在历史里出现一次,被当普通早期对话驱逐。修复:红线进 pinned / 末尾复述(2.2 节),assembler 升 v4
回复变短 JSON 被截断、总结"偷懒" 账本:输出预留被素材挤到 8% 检索 k 被调大后素材超配。修复:k 收紧、素材配额回调(2.1 节),retrieval 升 v6
引用幻觉 回答引用了不存在的 [5] 快照:该轮只注入 4 段 模型在无依据时仍硬贴编号。修复:契约加"无依据即声明"+ 机器校验编号集合(5.2 节),prompt 升 v13

注意三个案例的修复动作分别落在三个不同的版本上——这正是组合版本号存在的意义:修哪里、升哪个版本、重放验证,形成闭环。

五、账本指标与告警水位

快照的账本(ledger)攒起来就是动态层的体检曲线:

指标 怎么来 告警含义(阈值示意)
用量占比 每轮 total / 可用额度 持续 > 80%:结构性超支,该触发 6.1 的压缩
驱逐计数 diff 里 - 行数 每轮都在驱逐:分配表失真,重算(2.1 节第五节)
压缩频次 diff 里 ~ 的简报类突变 频繁压缩:任务粒度太重或预留不足
素材命中率 引用编号被答案实际使用的比例 低命中:检索质量或片段选择有问题(5.2 节)

趋势比单点重要:一轮 85% 是突发,十轮连涨到 85% 是失控——后者才是"历史只进不出"或"检索重复注入"这类慢性病的体征。

本节要点回顾

  1. 快照是最小可观测单位:每轮组装后落一份(条目 + 账本 + 版本号),存头部不存全文;没有快照的调试是盲调。
  2. diff 三符号+ 新进、- 驱逐、~ 压缩或截断——驱逐误伤与压缩越界在这里现形。
  3. 组合版本号:prompt × assembler × memory × retrieval,任一变更即新组合、过评测;"当时是 v 几"的答案是一组。
  4. 调试工作流:症状 → 三问归位 → diff → 定位 → 修复升版 → 快照回放;三个典型案例分别修在 assembler、retrieval、prompt 上。
  5. 指标四件:用量、驱逐计数、压缩频次、素材命中率——看趋势,不看单点。
  6. 至此运维侧齐了:压缩(6.1)让长会话活下来,快照与版本(6.2)让每次改写有据可查。

全书的手艺到此讲完:预算与成分(第 12 章)、常驻层(第 3 章)、动态层的供给与运维(第 46 章)。最后一章回到现实世界——这些手艺在 LangChain / LangGraph 等框架里对应哪些组件,以及合起来做一个完整的客服 Agent 要走哪些步骤。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U