从"存得进"到"用得上"
上一篇把长期记忆的写入路径铺好了:向量库管联想,结构化表管断言,写入过闸门。但存储的全部意义在于使用——同一条记忆,在第几轮被查、查完塞进消息数组的哪个位置、以什么形态呈现,对回答质量的影响大到可以翻转结论。这一篇把"检索时机"和"注入方式"这两个正交维度做成可量化的对照。先说结论的形状:检索时机是一张成本-质量的帕累托面,没有免费午餐;注入方式是一张位置-形态质量矩阵,最大差距接近两倍。多数团队的记忆系统不是死在存不上,而是死在"每轮无脑全注入"或者"想起来才注入、位置随机"这两个极端上。
检索时机:三种触发策略的账
触发时机有三档谱系。每轮必查:每次组装上下文前先跑一遍检索,注入 top-k。召回率 100%,代价是大量回合根本不需要记忆,注入的全是稀释注意力的噪声,还按轮计费。模型自判:把"要不要查记忆"交给模型自己决定——通常实现为让模型显式调用检索工具或输出路由标记。成本随需而省,质量上限被模型的判断力封死:判断错误双向都犯,该查查不到、不该查瞎查。检索器阈值:不劳驾模型,用查询与记忆的相关度分数配一个硬阈值,超过才注入。三条路各有失效面,而工程上必须先在损益表上对齐一件事:漏检和噪声注入的代价不对称。该用记忆时没用,模型只能靠通用先验硬答,错得理直气壮;不需要时多塞了一条记忆,只是轻微稀释注意力。所以触发策略的第一优化目标是压低漏检,第二才是省预算。下面用 60 回合的模拟会话对三策略对账,相关性分数按"该查回合偏高、不该查回合偶有高分"的重叠分布生成,固定种子。
importrandom rng=random.Random(491)STEPS=60# 一次会话的回合数MEMORY_NEED=0.35# 真正需要长期记忆的回合占比BUDGET_PER_STEP=1000.0# 每轮上下文可用预算(计量单位)MEMORY_BLOCK=620.0# 一次注入的记忆块体积P_SENSE=0.80# 模型"这轮该查记忆"判断的正确率THRESHOLD=0.55# 混合策略的相关度触发阈值TRIALS=200streams=[]for_inrange(TRIALS):stream=[]foriinrange(STEPS):needs=rng.random()<MEMORY_NEEDifneeds:rel=rng.uniform(0.35,0.95)# 该查的回合: 相关度普遍偏高但有漏网else:rel=rng.uniform(0.10,0.70)# 不该查的回合: 偶尔冒高分(近似记忆)stream.append((needs,rel))streams.append(stream)defdecide(strategy,needs,rel,jrng):ifstrategy=="每轮必查":returnTrueifstrategy=="模型自判":returnjrng.random()<(P_SENSEifneedselse1-P_SENSE)returnrel>=THRESHOLDprint("策略 注入次数 漏检数 噪声注入数 答对率 记忆预算占比")forstin("不查记忆","每轮必查","模型自判","阈值混合"):inj=mis=noise=0score=0.0forsi,streaminenumerate(streams):jrng=random.Random(49100+si)# 每局独立的判断噪声forneeds,relinstream:go=Falseifst=="不查记忆"elsedecide(st,needs,rel,jrng)ifgo:inj+=1ifnotneeds:noise+=1elifneeds:mis+=1ifneedsandgo:score+=1.00# 该查且查到elifneedsandnotgo:score+=0.45# 漏检: 靠通用先验硬答elifgoandnotneeds:score+=0.92# 噪声注入: 稀释注意力else:score+=1.00# 不需要也没注入n=TRIALS*STEPS budget=inj*MEMORY_BLOCK/(n*BUDGET_PER_STEP)print("%-8s %7.1f %5.1f %7.1f %.3f %5.1f%%"%(st,inj/TRIALS,mis/TRIALS,noise/TRIALS,score/n,budget*100))print("\n注: 漏检一次扣 0.55, 噪声一次只扣 0.08——检索时机策略优先防'该查没查'。")print("模型自判受 P_SENSE=%.2f 封顶, 判断力本身就是上下文工程要喂出来的。"%P_SENSE)print("\n阈值混合的相关度阈值扫描:")forthin(0.40,0.55,0.70,0.85):inj=mis=noise=0score=0.0forstreaminstreams:forneeds,relinstream:go=rel>=th inj+=go noise+=(goandnotneeds)mis+=(needsandnotgo)ifneedsandgo:score+=1.00elifneedsandnotgo:score+=0.45elifgoandnotneeds:score+=0.92else:score+=1.00print(" 阈值 %.2f: 注入 %4.1f, 漏检 %4.1f, 噪声 %4.1f, 平均答对率 %.3f, 预算占比 %.1f%%"%(th,inj/TRIALS,mis/TRIALS,noise/TRIALS,score/(TRIALS*STEPS),inj*MEMORY_BLOCK/(TRIALS*STEPS*BUDGET_PER_STEP)*100))运行输出:
策略 注入次数 漏检数 噪声注入数 答对率 记忆预算占比 不查记忆 0.0 21.0 0.0 0.808 0.0% 每轮必查 60.0 0.0 39.0 0.948 62.0% 模型自判 24.7 4.2 7.9 0.951 25.5% 阈值混合 23.8 6.9 9.7 0.924 24.6% 注: 漏检一次扣 0.55, 噪声一次只扣 0.08——检索时机策略优先防'该查没查'。 模型自判受 P_SENSE=0.80 封顶, 判断力本身就是上下文工程要喂出来的。 阈值混合的相关度阈值扫描: 阈值 0.40: 注入 38.6, 漏检 1.7, 噪声 19.3, 平均答对率 0.959, 预算占比 39.9% 阈值 0.55: 注入 23.8, 漏检 6.9, 噪声 9.7, 平均答对率 0.924, 预算占比 24.6% 阈值 0.70: 注入 8.7, 漏检 12.3, 噪声 0.0, 平均答对率 0.887, 预算占比 9.0% 阈值 0.85: 注入 3.5, 漏检 17.5, 噪声 0.0, 平均答对率 0.839, 预算占比 3.6%读表。每轮必查答对率 0.948,但吃掉了 62% 的上下文预算——花八成钱养两成回合,典型的"保险过头"。模型自判以 25.5% 的预算做到 0.951,看似最优,但注意它的天花板就是 P_SENSE:判断力再差 5 个点,漏检立刻翻倍,而把判断力喂上去靠的是提示词与评测,不是代码。阈值扫描给出了最有工程价值的信息:阈值 0.40 用 39.9% 预算买到 0.959,好于模型自判——一个调校的阈值常常胜过精巧的路由,先调阈值再谈智能。两个策略配合才是终局答案:阈值做默认闸门,模型自判做例外通道(用户提到"我之前说过"时强制检索,无视分数)。另外别忘了模拟的代价结构是假设出来的:真实系统里漏检与噪声的损益比要用线上badcase回归测出来,阈值才有调准的依据。
注入哪里、以什么形态:第二个正交维度
决定"查不查"之后,"查到的东西放哪"同样致命。位置维度上,第一篇的 U 形曲线已经给出预告:系统区开头、历史中段、紧邻问题上方、问题后重申,四个位置的注意力利用率天差地别。形态维度上,一条记忆裸文本转储进上下文,模型可能把它当成"用户说过的话"与历史纠缠;带上出处与置信度的标注块(形如[记忆#1237 | 置信度0.86 | 出自第44轮])让模型能显式权衡可信度,错误注入还有拒收余地。下面把位置×形态做成 12 格矩阵。
importrandom rng=random.Random(4911)TRIALS=300POSITIONS={"系统区开头":0.05,# 组装后上下文的相对位置槽"历史中段":0.50,"紧邻问题上方":0.88,"问题后重申":0.97,}defposition_utilization(slot):"""位置利用率: U 形, 两端高中段低(与系列第 1 篇实验二同一族曲线)。"""d=min(slot,1-slot)# 到最近端点的距离return0.98-0.36*min(1.0,d/0.45)+0.03*slot FORMAT={# 形态乘子: 带出处标注的记忆比裸文好采信; 裸文可能被当成"用户说的""裸文本转储":0.90,"标注引用":1.00,"标注引用+置信度":1.05,}rows=[]forpos,slotinPOSITIONS.items():forfmt,multinFORMAT.items():scores=[]for_inrange(TRIALS):q=position_utilization(slot)*mult q*=rng.gauss(1.0,0.03)# 采样噪声scores.append(min(1.0,max(0.05,q)))rows.append((pos,fmt,sum(scores)/TRIALS))base=next(mforp,f,minrowsifp=="历史中段"andf=="裸文本转储")print("注入方案(位置 x 形态) 平均质量分 相对'中段裸文本'提升")forpos,fmt,meaninrows:print("%-10s | %-12s %.3f %+6.1f%%"%(pos,fmt,mean,(mean/base-1)*100))best=max(rows,key=lambdar:r[2])worst=min(rows,key=lambdar:r[2])print("\n最优: %s+%s(%.3f) 最差: %s+%s(%.3f) 倍率 %.2fx"%(best[0],best[1],best[2],worst[0],worst[1],best[2]/worst[2]))print("\n成本侧账(每个记忆块按 620 单位, 三种位置的边际成本):")print(" 塞进系统区: 每轮重发 x 会话长度, 长会话下累计成本最高且永久占据头部")print(" 注入中段: 一次性成本, 但利用率 %.2f 起步"%position_utilization(0.5))print(" 尾部重申: 一次性成本, 利用率 %.2f, 且可随时摘除"%position_utilization(0.97))运行输出:
注入方案(位置 x 形态) 平均质量分 相对'中段裸文本'提升 系统区开头 | 裸文本转储 0.847 +48.0% 系统区开头 | 标注引用 0.939 +64.0% 系统区开头 | 标注引用+置信度 0.981 +71.3% 历史中段 | 裸文本转储 0.572 +0.0% 历史中段 | 标注引用 0.635 +11.0% 历史中段 | 标注引用+置信度 0.665 +16.2% 紧邻问题上方 | 裸文本转储 0.817 +42.8% 紧邻问题上方 | 标注引用 0.911 +59.1% 紧邻问题上方 | 标注引用+置信度 0.955 +66.8% 问题后重申 | 裸文本转储 0.886 +54.8% 问题后重申 | 标注引用 0.980 +71.2% 问题后重申 | 标注引用+置信度 0.997 +74.2% 最优: 问题后重申+标注引用+置信度(0.997) 最差: 历史中段+裸文本转储(0.572) 倍率 1.74x 成本侧账(每个记忆块按 620 单位, 三种位置的边际成本): 塞进系统区: 每轮重发 x 会话长度, 长会话下累计成本最高且永久占据头部 注入中段: 一次性成本, 但利用率 0.64 起步 尾部重申: 一次性成本, 利用率 0.99, 且可随时摘除矩阵对角线读法:同一个"裸文本"形态,从最差格(中段 0.572)换到最好格(尾部重申 0.886),只动位置就白捡 55% 的提升;同一个"尾部"位置,从裸文本升级到带置信度标注,再抬一截。两个维度相乘,最优与最差差 1.74 倍——很多所谓"模型记性差",拆开看只是把记忆放在了中段还裸着塞。成本侧最后一行提醒别把答案绝对化:"问题后重申"质量最高但它是每轮临时块,会话一长仍要靠系统区固化,正确姿势是:稳定偏好的摘要常驻头部,本轮召回的证据尾部重申,两处内容互相引用编号而不是重复全文。
工程化改进:触发与注入的默认配置
第一档,闸门默认走检索器分数:查询改写后打相关度,阈值先设 0.4 起步(宁可多查),观察噪声 badcase 再收紧。第二档,语义触发器兜底:用户话语里出现"我之前说过/按老规矩/上次那个"这类线索词,无条件强制检索,无视阈值——漏检代价高的场景全部走这条豁免通道。第三档,形态规范落进组装器:所有注入记忆统一为带编号、出处、置信度的标注块,并在系统提示里解释标注的语义(“置信度低于 0.6 的记忆仅作参考,与用户当前陈述冲突时以当前陈述为准”)。第四档,位置策略双轨:常驻层(人设、长期偏好摘要)固化在系统区;会话层(本轮召回)永远注入在紧邻问题的尾部,组装器保证任何时刻"尾部 1/4 窗口"留给最新证据与问题本身。第五档,全链路打点:触发决策、阈值、注入位置、模型是否引用了记忆(按编号回查)逐轮落日志,为下一环节的退化观测留证据。
常见陷阱
一是检索做成"每轮无条件 top-k":预算被噪声记忆常年霸占,还会诱发模型把过期记忆当事实复述——注入越多,错得越频繁。二是把"要不要查"完全交给模型自觉:没在提示词里给判断标准、没有评测回放,自判策略退化为随机查。三是注入块没有出处标注:记忆与用户原话在语义上无法区分,用户没说过的事被安到用户头上,投诉类事故十有八九源于此。四是把高置信度当唯一排序键:检索器分数标定很差,0.9 分未必可靠,需要用自己业务集重标定后再配阈值。五是尾部位置忘回收:上一轮重申的记忆块没摘除,新证据又进来,尾部堆成第二个"中段",U 形曲线重新惩罚你。
落地清单
- 触发默认走相关度阈值(0.4 起步),线索词强制检索做例外通道,两者都打日志
- 注入形态统一:编号+出处+置信度的标注块,系统提示中定义标注语义与拒收规则
- 组装器保证问题与最新证据占据尾部 1/4;常驻摘要放头部,两处用编号互指不重复全文
- 检索器分数用自有业务集重标定;阈值调整必须带漏检/噪声双指标回归
- 逐轮记录"模型是否引用了注入记忆",引用率长期偏低说明形态或位置出了问题
单人会话的检索调度讲完了,但窗口这个稀缺资源还会被"别人"占用:多 Agent 协作时,每个角色的工作集、共享黑板、消息总线三类空间怎么分?下一篇《AI Agent 记忆与上下文工程实战(5):多 Agent 协作的上下文隔离:角色、黑板与消息总线》把隔离做成预算问题。
参考来源
- Liu et al., Lost in the Middle: How Language Models Use Long Contexts:https://arxiv.org/abs/2307.03172
- Park et al., Generative Agents: Interactive Simulacra of Human Behavior:https://arxiv.org/abs/2304.03442
- Yao et al., ReAct: Synergizing Reasoning and Acting in Language Models:https://arxiv.org/abs/2210.03629
- Asai et al., Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection:https://arxiv.org/abs/2310.11511
- Anthropic Engineering, Effective context engineering for AI agents:https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents