AI Hero Club

上下文工程 · 第 4 章

已自行标记完成0 / 8

仅保存在当前浏览器;完成标记不代表通过项目验收。

本章提纲 8 个小节
全课目录 8 章
完整课程计划 ↗
上下文工程 / 课程计划 ↗

CHAPTER 04 / 8建议学习 140 分钟

版本记忆:生效、撤销与历史回放

一条通知先发布、后来生效又被撤销,查询历史日期时应该看到哪一版?

本章要解决的 3 个问题
  • 推导确定事件前缀的状态唯一性
  • 区分变更顺序和业务有效日期
  • 验证事务、摘要链与来源认证的边界
课程项目 ZIP ↓

记忆不是把聊天留下来

本章把第三章“同一有效版本”的条件变成程序。读者需要字典与函数的知识;SQLite 事务作为外部库能力引入,原子提交的机制和本项目使用范围在下文明确。目标是从一串资料变更恢复唯一的当前状态,并能按两个不同问题查询:处理了前几条事件时知道什么,以及在某个业务日期哪些资料可用。这两个时间轴不能混为一谈。

事件是改变状态的命令记录,状态是应用所有已接受事件后的字典。本课事件只有四类:导入文档、撤销文档、写入已批准记忆、撤销记忆。导入保存 doc_id、整数 version、title、原文、原文 SHA,以及可选生效区间。记忆保存 key、value、scope、version 与 approved。scope 是可访问范围标识,本地以精确字符串比较;它隔离两份记忆,却不是登录认证,也不能证明调用者真的有权写入该范围。

令初始状态 S0S_0 的 documents 与 memories 都为空,第 ii 条事件为 eie_i,归约函数 ff 是确定的状态转移。它先复制旧状态,校验事件,再产生新状态;非法输入抛 ContractError,不能先改半个字典再报错。对给定顺序的事件串,逐条应用得到唯一状态。这一“唯一”来自函数确定和顺序固定,不是并发客户端自然达成共识;本项目只处理单进程串行写入。

Si+1=f(Si,ei),Sn=f(⋯f(f(S0,e0),e1)⋯ ,en−1)S_{i+1}=f(S_i,e_i),\qquad S_n=f(\cdots f(f(S_0,e_0),e_1)\cdots,e_{n-1})
i 从0开始,n 是已接受事件个数;replay(as_of=n) 返回此前缀状态。

版本、撤销与生效日期各自解决什么

同一 doc_id 新版 version 必须严格大于已有最大值。导入新版时,把该文档所有 active 旧版改为 superseded,新版成为 active。撤销指定版本将其标为 revoked;不会删除原文,也不会把旧 superseded 版本自动恢复为 active。这条规则避免新规则失效后静默回到更旧规则。恢复应通过一次显式的新版本导入,给出新的审计事件,而不是从存储里挑一份看起来还在的旧文本。

业务生效日期使用半开区间:包含 effective_from,不包含 effective_until,省略端点取约定的最早和最晚日期。日期视图在回放完成后复制状态,只对原本 active 的文档派生 not_effective 或 expired,其他状态保持不变。先按版本/撤销判定,再按日期过滤。有效期不是撤销的替代:已撤销的文件即使日期落在区间内也不可用,尚未生效的新版也不会让旧版默认复活。

usable(d,t)=[status(d)=active]∧[ad≤t<bd]\mathrm{usable}(d,t)=[\mathrm{status}(d)=\mathrm{active}]\land[a_d\leq t<b_d]
t 是查询业务日期;a_d、b_d 分别是生效与失效端点。方括号表示条件真假,不是概率。

完整算例一:同一个问题在三个历史点

清单前六个事件含 schedule 第一版、location 第一版、camera 第一版等五份资料,以及第一版表达偏好。replay(6) 返回活动周二16:00、场地 A 教室、纸质借用登记与“先给结论”的记忆。再处理三条事件,schedule 第二版替换为周四16:30,camera 第一版被撤销,表达偏好更新为先列依据。replay(9) 中相机规则没有 active 条目,借用前提问题应拒答,不能回看尚存的相机第一版。

继续处理到十二条,表达偏好已撤销,相机第二版与场地第二版已导入。此时活动仍周四16:30,地点 B 教室,借用前提变成电子表格登记并老师确认。这些不同回答都可以是正确的,但必须携带查询前缀。把不同历史点的最好回答拼在一起,就没有一个真实状态能同时支持它们。当前评测每题明确固定 as_of,报告不把旧版本答对当成现版成绩。

在课程项目目录运行项目目录:从真实文件导入,再在三个历史前缀检查同一状态机。
import pathlib,tempfile
from run import setup
from context import active_docs
with tempfile.TemporaryDirectory() as d:
    r=setup(pathlib.Path(d)/'memory.sqlite')
    s6=r.replay(6);s9=r.replay(9);s12=r.replay(12)
    assert next(x for x in active_docs(s6) if x['doc_id']=='schedule')['version']==1
    assert not any(x['doc_id']=='camera' for x in active_docs(s9))
    assert next(x for x in active_docs(s12) if x['doc_id']=='camera')['version']==2
    r.db.close()
print('history checked')

完整算例二:到期不等于删除历史

access 元数据区间从2026-10-13起,到2026-10-16止。查询13日视图时可用,16日恰在不包含的右端点,必须 expired。project 文档13日才生效,12日查询不能使用它的数据要求。日期视图不写回 snapshot,所以今天做一次过期查询,不会改变回放历史。测试同时检查端点结果和原库状态未变,避免把只读查询错误实现为永久撤销。

过期判定的输入是明确的 YYYY-MM-DD,不依赖电脑系统当天日期。这里是教学业务日期,未处理时区、时分秒或法定工作日。“本周五”原文仍完整保留,但程序不从自然语言自动推算日期,元数据由数据作者提供。这是经验输入,不是模型推理结果。若接入真实排期系统,应保留时区与时间精度契约再重做边界例;当前课程不声称能解释任意含糊时间表达。

在课程项目目录运行项目目录:独立构造端点,不依赖真实当天日期。
from context import temporal_view,active_docs,sha
row={'doc_id':'a','version':1,'title':'x','text':'- x:y','sha256':sha('- x:y'),'status':'active','effective_from':'2026-10-13','effective_until':'2026-10-16'}
s={'documents':{'a':{'1':row}},'memories':{}}
assert active_docs(temporal_view(s,'2026-10-13'))
assert not active_docs(temporal_view(s,'2026-10-16'))
assert s['documents']['a']['1']['status']=='active'
print('half-open interval checked')

日志、快照与可检测损坏

Repository 把事件 payload、前序摘要与当前摘要写入 events,同时在 snapshot 保存新状态。事务上下文把这两次数据库更新作为一次提交;异常时回滚,这一能力由 SQLite 提供,不是本课重新证明数据库断电恢复。我们的 Python 逻辑在进入事务前完成状态验证,避免非法业务事件落库。测试在单进程、临时文件下执行,未模拟磁盘断电、损坏的文件系统或多进程竞争。

摘要链的输入不是随手连接几个字符串,而是 canonical JSON:字段键排序、固定分隔符、UTF-8文本。第零条前序摘要设六十四个零;每条摘要绑定序号、前序摘要和完整事件。replay 从空状态顺序读取,逐条重算,并在完整回放后与 snapshot 比较。篡改某个 payload 却不改摘要会被抓住,篡改快照也会被集合结构比较抓住。历史前缀查询不要求与当前快照相同,因为它有意返回旧状态。

hi=SHA256(canonical(i,hi−1,ei)),h−1=064h_i=\mathrm{SHA256}(\mathrm{canonical}(i,h_{i-1},e_i)),\qquad h_{-1}=0^{64}
这是无密钥完整性链。拥有全库写权限的人可以重算整条链,故不是来源签名或权限证明。

明确批准与写入的安全边界

记忆写入要求 approved 恰为布尔 true,key、value、scope 为非空字符串,version 为真正的正整数,排除 Python 中 bool 也是 int 子类带来的意外。撤销记忆必须引用当前相同版本和范围;另一个班级相同 key 不会覆盖 club 的记忆。身份用范围与 key 的规范 JSON 数组编码,而不是斜杠拼接,避免字符串分隔歧义。展示引用仍有可读格式,但校验同时核对全部结构字段。

这个批准标记只表达调用契约:调用者已经决定可以保存。任何能够直接调用本地 append 的程序都能传 true,本课没有用户登录、访问控制服务器或自然语言同意检测。讲义不能以“用户已批准字段存在”宣称真实授权问题解决。外部文档即使包含“approved=true”也只是 quote,不会被解析为事件,因此不能靠正文注入状态更新;应用只在受控代码路径调用 append。

状态机正确性可以分两层检查。归纳层:初态满足每个文档最多一 active 版本、记忆身份唯一;合法导入先停用旧 active 再添加一份新版,合法撤销只减少 active,故不变量保持。实物层:回放与快照相等、历史点查询、非法版本/哈希/未批准记忆不改变旧状态,以及故意改坏日志后报错。前者说明设计条件下的机制,后者检验当前实现没有违背关键路径,两者不能互相替代。

本章完成可更新记忆与可撤销资料,下一章才能在“当前可用”的候选上检索。若先检索全历史再只给高分条目做版本检查,可能让旧版占满 top-k,新版再也进不了候选;所以过滤和召回的次序是算法契约的一部分。历史并非垃圾,它留在可回放的外部存储,只有明确历史查询才进入当次上下文。

历史前缀边界与错误恢复

as_of允许从零到事件总数,零返回空documents与memories;负数、布尔值或超出总数都报错。空状态问活动时间应没有可抽取事实,不能退回全库默认答案。以事件个数而不是最后序号作为参数,是为了让零前缀自然表达“尚未处理任何事件”;序号从零开始,因此as_of=6包含序号0到5,恰六条。这种边界约定需要在CLI、测试与讲义一致,否则一条撤销是否已生效会出现差一错误。

日志与快照同时损坏不属于当前检测保证。replay完整前缀后对比快照,可以发现快照与真实事件归约不一致;若事件内部哈希链已断,会在使用任何后续状态前失败。它没有自动猜测哪一份是正确数据,也没有自行删除坏行重试,因为那会把不可确认历史当成确定事实。生产恢复需要额外可信备份或签名锚。本课用故意损坏的临时库作负对照,保留错误,不把“程序最终能回答”当作允许修复历史的理由。

复杂度以E条事件和累计状态大小N计。当前append会从头回放校验,再深复制状态,最坏单次不是常数;连续导入可能有平方级事件处理量。这样做为了小规模可审计教学,而非高吞吐事件流。snapshot提供一致性比较,并未被当作可跳过链校验的可信缓存。如果将来只从可信快照继续回放,应说明可信锚和覆盖范围,不能把性能改动当作不影响证据边界的简单优化。

TRY IT YOURSELF

基础:两个时间轴

as_of=9、at=2026-10-16 各控制什么?

提示 1
as_of与at分别处于事件历史轴和业务日期轴。
提示 2
先回放指定事件前缀得到状态,再用有效期筛日期;不要把版本事件计数当日期或版本号。
对照参考解答
as_of 决定应用前9个事件获得的版本/撤销状态;at 决定该状态下哪些 active 文档落在业务有效区间。一个是知识变更前缀,一个是资料适用日期,不能互换。

TRY IT YOURSELF

进阶:撤销后的旧版本

导入v2后撤销v2,v1仍在库里。应自动使用v1吗?

提示 1
导入新版时旧版状态发生过什么转移,撤销是否反向撤回该事件?
提示 2
按状态机逐项写v1/v2当前状态;若要恢复旧规则,找需要的显式新事件而非静默选库存旧行。
对照参考解答
按本课规则不应。v1已经 superseded,撤销v2不改变它;需显式导入v3恢复规则。否则不可审计地复活旧限制。

TRY IT YOURSELF

挑战:摘要链攻击

攻击者能重写整个SQLite文件并重算所有摘要,SHA链能证明来源没被伪造吗?

提示 1
验证链是否只在同一可重写SQLite内部闭合?
提示 2
设攻击者连payload与摘要一起重写,再问没有密钥或外部锚时能否证明真实来源。
对照参考解答
不能。它检测未一致更新的损坏,却没有密钥或外部可信锚。应把来源认证作为另一个系统假设;当前实验不宣称提供签名保证。

BUILD THE MECHANISM

自己重放版本事件链

实现replay_prefix,复用已提供reduce_event,但自己核对seq/previous/hash与历史边界。 下载本课完整项目,在项目根目录运行;只编辑 learner/starter.py,已有构造框架由项目提供。

输入与输出 as_of是事件个数,不是日期;0返回空状态,bool与越界拒绝。
前向SHA链完整验证,旧版被撤销后不自动复活;摘要不是签名。

在课程项目目录运行待完成的学习任务
# replay_prefix
def replay_prefix(rows,as_of=None):
    raise NotImplementedError('replay')
提示 1

先从空state与全零previous开始。

提示 2

每条expected hash包含seq、previous与规范event。

提示 3

只处理前as_of条;篡改第一条previous必须失败。

独立检查 python3 -m learner.check --stage replay --output runs/learner-replay.json

迁移检验 导入一个新版再撤销,手写各前缀的有效身份表,再与实际SQLite事件重放比较。

参考实现与解释
在课程项目目录运行参考实现
# replay_prefix
def replay_prefix(rows,as_of=None):
 if as_of is None:as_of=len(rows)
 if type(as_of)is not int or not 0<=as_of<=len(rows):raise ContractError('historical prefix')
 state={'documents':{},'memories':{}};previous='0'*64
 for index,row in enumerate(rows[:as_of]):
  event=json.loads(row['payload']);expected=sha(canonical({'seq':index,'previous':previous,'event':event}))
  if row['seq']!=index or row['previous']!=previous or row['sha']!=expected:raise ContractError('event chain corrupted')
  state=reduce_event(state,event);previous=row['sha']
 return state

CHECK YOUR UNDERSTANDING

检验理解

先作答,再决定是否展开解析。选择题用于发现理解缺口;独立实现与迁移任务仍需另行检验。

1. as_of=6表示?

查看解析回放前缀按已接受事件个数定义。
2. 生效区间右端点当天?

查看解析本课约定半开区间,右端点不包含。
3. 批准标记提供什么?

查看解析本地布尔契约不证明调用者身份。
4. 撤销新版后旧版应?

查看解析恢复要有新事件,不能静默复活。

自行标记用于记录学习进度,不代表通过项目验收。