ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AI Agent 记忆与上下文工程实战(8):长任务的记忆治理:checkpoint、恢复与失效策略

AI Agent 记忆与上下文工程实战(8):长任务的记忆治理:checkpoint、恢复与失效策略 崩溃不是意外是排班表上的常态上一篇把还活着的任务管起来了状态表投影管窗口失效传播管返工。但那套治理有个没说出口的假设——进程一直在跑。长任务的真实死法多得多限流把会话掐断、发布重启把解释器换掉、笔记本合盖、抢占式实例被回收。任务时长一旦超过进程寿命“活着才是小概率事件崩溃才是默认事件。这时候记忆治理的问题就从窗口里放什么变成磁盘上存什么、怎么拿回来”多久存一次频率账、存什么载荷账、恢复时旧检查点和活记忆打架听谁的一致性账。数据库工程这四十年一直在答同样的题——WAL、checkpoint、崩溃恢复、ARIES 的 redo/undoAgent 的记忆库本质上是一个被多个随死进程主循环、摘要器、抽取任务共享的数据库却没有 DBA。本篇把这三笔账各做一个确定性实验。照例声明本机以确定性模拟演示固定种子、不依赖时钟生产环境要换成真实崩溃统计与真实向量库/键值库。实验一checkpoint 频率的成本洼地频率账最直觉也最容易被拍脑袋定。模拟一个 120 步的长任务四个崩溃点由固定种子给出checkpoint 有写入成本恢复有载入成本对比从不设点、五种间隔下的总耗时与白工重放步数importrandom N120CKPT_COST2# 一次 checkpoint: 写入校验GROW18# 窗口拷贝法: 每个执行步让上下文长大 18 单位SNAP_UNITS40# 状态快照法: 一份 checkpoint 恒为 40 单位defcrashes(seed):returnsorted(random.Random(seed).sample(range(8,N),4))CRASHEScrashes(495)defsimulate(k):totalredon_ck0last0p1pendinglist(CRASHES)# 每次崩溃只触发一遍whilepN:ifpendingandppending[0]:pending.pop(0)resumelast1ifkelse1redop-resumeifk:total4# 载入快照校验回灌状态presumecontinuetotal1ifkandp%k0:lastp n_ck1totalCKPT_COST p1returntotal,redo,n_ckprint(任务本体 %d 步, 崩溃点(固定种子): 第 %s 步%(N,, .join(map(str,CRASHES))))print()print(间隔 总耗时 白工步数 checkpoint次数)bestNoneforkin(0,10,15,20,30,60):t,r,nsimulate(k)print(%4d %7d %9d %13d%(k,t,r,n))ifbestisNoneortbest[1]:best(k,t)print()kbest[0]cCRASHES[-1]ck_before(c-1)//k*kprint(最优间隔 %d (总耗时 %d)。%(k,best[1]))print(再看携带量(最后崩溃第 %d 步、其前最近一次 checkpoint 在第 %d 步):%(c,ck_before))print( 状态快照: 恒 %d 单位; 窗口拷贝: %d x %d %d 单位%(SNAP_UNITS,ck_before,GROW,ck_before*GROW))print(频率决定白工多少, 载荷决定恢复多贵——两个轴都要单独治理。)运行输出任务本体 120 步, 崩溃点(固定种子): 第 28, 32, 76, 95 步 间隔 总耗时 白工步数 checkpoint次数 0 347 227 0 10 177 17 12 15 169 17 8 20 195 47 6 30 191 47 4 60 247 107 2 最优间隔 15 (总耗时 169)。 再看携带量(最后崩溃第 95 步、其前最近一次 checkpoint 在第 90 步): 状态快照: 恒 40 单位; 窗口拷贝: 90 x 18 1620 单位 频率决定白工多少, 载荷决定恢复多贵——两个轴都要单独治理。三个读数。第一从不设点的代价不是可能全丢这种玄学而是白纸黑字的 347 对 169崩溃点在前半程的任务白工 227 步超过任务本体的 1.9 倍——而且模型全程没有报错它只是在勤奋地重做已经做完的事这正是长任务最难看的失败形态不是崩是循环着崩。第二最优间隔不值钱。注意 20 和 30 那两行设点更勤的 20 反而比 30 慢因为第 28、32 两步崩溃恰好落在 20 的格子里、离上一个设点很远。频率曲线对崩溃位置高度敏感而崩溃位置不可预测——花力气调间隔是伪优化正确动作是把设点从按时钟改成按事件每次状态迁移过验收、每次产物落盘立刻设点让 checkpoint 网格贴住任务的语义节点第 494 篇的状态机正好提供了这个触发源固定间隔只当兜底。第三输出末尾那两行是载荷账同样是恢复状态快照法永远扛 40 单位回来窗口拷贝法在第 90 步要扛 1620 单位——40 倍还只算了一次窗口以每步 18 单位在长任务越往后崩恢复越像在重播一整段历史污染连 lost-in-the-middle 的中段垃圾都原样复活。载荷之问存状态不存现场顺着第三点把话说透。checkpoint 的载荷白名单应该只有四样状态表每节点一行状态/摘要/指针——就是上一篇的投影、记忆库的版本边界该时刻各写入者见到的版本号下一节见用途、验收断言与产物指针done 的依据和落盘位置、未决意图开了一半的头。黑名单恰好是新手最爱存的消息流原文可再生性最差、载荷最大、压缩摘要链摘要是当时那份窗口的函数窗口不可复原则摘要不可复现恢复后一段没有源头的断言反而没法校验、工具 schema注册表本来就在磁盘上。还有一条容易漏done 要绑环境指纹。第 70 步测试通过这个状态隐含了当时的依赖版本、系统日期、远端服务状态恢复到一个新容器里就无条件续跑等于把过期断言当真话。给每个 done 记一枚环境摘要镜像 tag关键依赖版本恢复时对不上就把该节点降回 pending——宁可重跑一步不可继承一个错误的已完成。实验二恢复即复活——三种回灌策略一致性账最阴险。设定记忆库活在进程之外SQLite/向量库崩溃只死掉执行进程恢复时要把 checkpoint 里抄录的记忆项跟活库对账。操作流 100 步固定种子生成第 40 步设点第 70 步崩溃对比三种回灌A 检查点条目无条件写回B 按版本号 LWW但库里查无此键被当作检查点发现的新事实补录C 在 B 之上库把删除记为墓碑而非抹除importrandom KEYS[f%d%iforiinrange(1,9)]CKPT40CRASH70rngrandom.Random(4952)ops[]foriinrange(1,101):rrng.random()krng.choice(KEYS)ifr0.52:ops.append((i,up,k))elifr0.75:ops.append((i,read,k))else:ops.append((i,del,k))defapply(store,i,kind,k,tombFalse):ifkindup:store[k](i,v%d%i)elifkinddel:iftomb:store[k](i,gone)else:store.pop(k,None)defreplay(limit,tombFalse):store{}fori,kind,kinops:ifilimit:breakapply(store,i,kind,k,tomb)returnstore livereplay(CRASH)# 崩溃时刻记忆库真值(物理删除语义)live_treplay(CRASH,True)# 崩溃时刻记忆库真值(墓碑语义)snapreplay(CKPT)# 检查点里的记忆项(无墓碑)cnt{}fori,kind,kinops:cnt[kind]cnt.get(kind,0)1print(100 步操作流(固定种子): 更新 %d, 读取 %d, 删除 %d%(cnt[up],cnt[read],cnt[del]))print(第 %d 步做 checkpoint; 第 %d 步进程崩溃, 用检查点回灌记忆库。%(CKPT,CRASH))print()deffirst_bad(store,tomb):reflive_tiftombelsereplay(CRASH,True)stdict(store)fori,kind,kinops:ifiCRASH:continueapply(ref,i,kind,k,True)apply(st,i,kind,k,tomb)ifkindreadandst.get(k,(0,None))[1]!ref.get(k,(0,None))[1]:returnireturn-print(回灌策略 旧值踩新值 僵尸记忆 首个错误读取(步))adict(live)owsum(1fork,einsnap.items()ifkinaanda[k][0]e[0])fork,einsnap.items():a[k]eprint(%-20s %10d %10d %14s%(A 直写覆盖,ow,len([kforkinsnapifknotinlive]),first_bad(a,False)))bdict(live)rz[kforkinsnapifknotinb]forkinrz:b[k]snap[k]print(%-20s %10d %10d %14s%(B LWW/无墓碑,0,len(rz),first_bad(b,False)))cdict(live_t)fork,einsnap.items():ifknotincore[0]c[k][0]:c[k]eprint(%-20s %10d %10d %14s%(C LWW/带墓碑,0,0,first_bad(c,True)))print()zmin(rz)dstepmin(ifori,kd,kkinopsifkddelandkkzandCKPTiCRASH)print(僵尸示例: %s 已在第 %d 步被删除, 检查点仍存其 %s,%(z,dstep,snap[z][1]))print( 恢复后 %s 以 %s 复活, 之后每次引用它都不自知地错。%(z,snap[z][1]))print(直写覆盖 %d 次, 等于让第 %d~%d 步的新记忆全部退回旧版本。%(ow,CKPT,CRASH))运行输出100 步操作流(固定种子): 更新 44, 读取 23, 删除 33 第 40 步做 checkpoint; 第 70 步进程崩溃, 用检查点回灌记忆库。 回灌策略 旧值踩新值 僵尸记忆 首个错误读取(步) A 直写覆盖 3 2 79 B LWW/无墓碑 0 2 92 C LWW/带墓碑 0 0 - 僵尸示例: f5 已在第 59 步被删除, 检查点仍存其 v10, 恢复后 f5 以 v10 复活, 之后每次引用它都不自知地错。 直写覆盖 3 次, 等于让第 40~70 步的新记忆全部退回旧版本。A 的账最疼但最轻三处旧值踩掉新值损失被第 40~70 步这个窗口卡死而且行为看得见——下次读到旧值还有察觉机会。B 才是惯犯版本号挡住了旧踩新可它对缺席的语义判断错了——一个键不在活库里可能是检查点之后新出现的事实也可能是被删掉的旧事实两种含义共享同一个形态查无此键LWW 只能赌。它赌赢了 0 次旧值覆盖赌输了 2 条僵尸f5 在第 59 步被删检查点里还躺着它的 v10回灌即复活。注意首个错误读取从 79 推迟到了 92——潜伏期更长的错误反而更危险它可能一路活到最终交付物里。C 的修法便宜到令人尴尬删除不抹除改写一条 gone版本 的墓碑缺席就重新变成只有从未存在一种含义僵尸无处寄生。这就是第 490 篇结构化记忆里valid_to字段的第二个用途它本来就是墓碑天然抗恢复。代价是墓碑会积累回收规则必须写死只回收比最新 checkpoint 还老的墓碑——把比检查点新的墓碑删了等于亲手给下一场恢复挖坟。恢复协议五件套把两篇实验的教训编成一个可以直接抄的流程。一是事件驱动设点状态迁移过验收即设点固定间隔只做兜底二是载荷白名单状态表版本边界断言与指针未决意图消息原文与摘要链不进 checkpoint三是恢复对账不是回灌检查点、活库、未落盘写入三方按版本号合并任何一侧都没有无条件写回的权利四是步级幂等每步带 step key重放不重复副作用——恢复时你永远不知道上一步是做完了没记上还是做了一半只有幂等让这两种情况合流五是恢复断言先行跑第一步之前先验 checkpoint 自洽状态表行数产物指针数、版本单调、环境指纹匹配验不过就拒绝续跑宁可人工介入。常见陷阱一是把恢复了当恢复正确了进程起来了、计划读回来了就认为状态是对的——恢复断言不是仪式实验二里 A/B 两种坏状态都能正常启动。二是按墙钟设点任务推进速度极不均匀一次深度反思 40 分钟、一次查询 3 秒等间隔设点等于在薄的地方反复设、厚的地方裸奔。三是 checkpoint 里存压缩摘要却不存断言摘要失去了产生它的窗口变成不可校验的孤本模型对孤本摘要只会照单全收。四是墓碑回收不划线或一刀切前者库越滚越大后者直接量产僵尸。五是多写入者共库还无条件 LWW主循环、摘要器、抽取器同时写墙钟时钟粒度粗于写入间隔时整段并发写会被静默吃掉——记忆库要么单写入者排队要么键级冲突检测加显式合并别指望时间戳替你做仲裁。落地清单checkpoint 与事件绑定每次状态迁移过验收即设点固定间隔仅兜底载荷只存四样状态表、版本边界、断言指针、未决意图原文与摘要链落盘挂指针恢复走三方对账检查点/活库/WAL 尾按版本号合并禁止无条件写回删除一律墓碑化回收线划在比最新 checkpoint 更老处步级幂等 恢复断言先行断言不过拒绝自动续跑到这里别把任务弄丢的治理讲完了。但还有一类退化不会崩、不会报错记忆库越攒越多回答却越来越差——写入污染、检索失配、合并漂移每一口都是慢性病。下一篇《AI Agent 记忆与上下文工程实战9记忆效果观测定位越学越笨的退化问题》给记忆系统装上探针把感觉变笨了拆成能定位的病灶。参考来源Wikipedia, Write-ahead logginghttps://en.wikipedia.org/wiki/Write-ahead_loggingWikipedia, Transaction loghttps://en.wikipedia.org/wiki/Transaction_logWikipedia, Algorithms for Recovery and Isolation Exploiting Semanticshttps://en.wikipedia.org/wiki/Algorithms_for_Recovery_and_Isolation_Exploiting_SemanticsWikipedia, Two-phase commit protocolhttps://en.wikipedia.org/wiki/Two-phase_commit_protocolLangGraph Documentation, Persistence checkpointinghttps://langchain-ai.github.io/langgraph/concepts/persistence/Chhikara et al., MemGPT: Towards LLMs as Operating Systemshttps://arxiv.org/abs/2310.08560
返回列表