
清空了记录,为什么还会引用旧会话?
清空会话之后,历史列表变成了空白。按直觉,这应该是一次新的开始。
但程序开始下一次操作时,还可能拿出一个旧编号,沿着原来的对话继续。
问题藏在列表之外。我们研究的 OpenAI Agents SDK——供开发者构建 AI 助手的工具包——有一段会话管理代码,清掉了历史,却遗漏了后续操作仍会读取的引用。本文追查的是这些引用如何残留,并不是发现 AI 在“偷偷记忆”。
要理解它为什么发生,先得看看:一段对话除了文字,还留下了什么?
内容删了,“书签”还在
为了继续一段对话,程序不一定每次都把所有内容重新发送。它也可以使用响应编号,让后续请求沿已有的响应链继续。
可以把这个编号想成程序保存的一枚书签。历史列表存着对话内容,书签则告诉下一次操作要从哪里接上。清掉一份列表,并不会自动让所有保存书签的地方一起更新。
在受测代码里,后续操作是“压缩上下文”:把较长的对话整理成适合后续使用的较短内容。负责这件事的会话组件,可以提供当前内容,也可以沿旧响应编号继续。只要它仍能选中那个旧编号,空列表就不足以说明旧关系已经切断。
贡献者 sbguangha 在 OpenAI Agents SDK 提出的候选修复 #5000,补上的正是两处遗漏:当前响应编号,以及最近一次尚未存入历史的响应编号。这里的两类编号不是额外两份完整聊天记录,而是决定后续请求如何续接的引用。这个修复及其测试启发了我们的清空对照实验。
我们正在开发多 AI 助手协作系统 CodeFlowMu,也需要考虑任务继续、上下文重建时,程序实际采用了哪些旧状态。这个来源特别吸引我们的地方,是它在已有锁和状态变化记录的情况下,仍然遗漏了具体引用。它提供了一个值得验证的区别:防止操作互相冲突,与让过期引用真正失效,是否是两件需要分别检查的事?对方现成的失败场景让这个问题可以通过实验回答;实现不同,我们不能直接认定本地也有同样的缺陷。
正常清空时把它们一并清掉,似乎就够了。可是,失败的清空又该怎么办?
最难的时刻:已经删了,却没收到“删好了”
“删除完成”和“调用者收到成功确认”,中间可以有一段间隙。
我们测试了两种情况:本地数据库已经提交删除,但操作在返回之前被取消;或者删除已经提交,返回确认时却抛出了错误。
此时,调用者看到的是失败,数据库里的历史却真的空了。如果程序把失败理解为“什么都没发生”,继续保留旧编号,内容和引用就会走向两个不同的状态。
为了抓住这个间隙,测试使用真实的临时 SQLite 数据库——一种可直接在本地文件中存数据的数据库。在删除提交后、返回前暂停,再注入取消或错误。我们检查的不只是历史有没有清空,还包括后续操作能否继续使用旧引用、锁是否释放,以及还能不能写入新内容。
换回旧方法,问题就出现了
我们固定完整工具包和上游原测试,先运行候选修复,再保持其余代码不变,只换回旧的清空方法。这样更容易判断差异是否来自清空处理本身。
五项检查的结果如下:
| 检查的问题 | 旧清空方法 | 候选修复 |
|---|---|---|
| 正常清空后,旧响应引用是否失效? | 未通过 | 通过 |
| 删除提交后被取消,旧引用是否仍会失效? | 未通过 | 通过 |
| 删除提交后确认报错,旧引用是否仍会失效? | 未通过 | 通过 |
| 清空后自动整理上下文,是否改用当前输入? | 未通过 | 通过 |
| 清空是否等待正在进行的整理,最终留下空历史? | 通过 | 通过 |
旧方法四项失败、一项通过,候选修复五项通过。最后一行是原来就存在的并发等待检查:它帮助我们排除一个过于笼统的解释——不能把这次问题归结为完全没有并发保护。

图 1:列表清空与旧引用失效,要沿两条线检查。来源:本轮 runs/sdk.json 与原始测试日志;本地数据库提交真实执行,远端压缩接口由测试替身提供。
这里有一个必须分清的观察边界:我们检查了本地组件怎样准备后续请求,没有联系真实的 OpenAI 服务。实验能说明旧引用路径仍可被采用,不能据此声称远端恢复了已删除的数据。
多一道保护,不一定补得上遗漏
这个工具包原本就有锁,用来协调同时发生的操作;也有记录状态变化的内部代次。它们各有作用,但不会自动清掉每一处旧编号。
这次修复没有另造一套代次机制,而是在成功和异常清理分支里,补上两类响应引用的失效处理。
这也是实验最有用的启发:检查“清空”时,可以顺着下一次操作反查——它到底从哪里取内容、编号或待处理工作?这些地方在正常清空、被取消和确认失败时,分别会留下什么?
对开发者而言,这比只断言“历史条数等于零”更接近真正的使用过程。对使用者而言,也能理解为什么一个空白界面,还不足以回答整个系统下一步会采用什么。
延伸阅读:会话接对了,为什么任务还会接错?
另一条研究来源是贡献者 cryppadotta 提出的 Paperclip #13345。Paperclip 用来组织 AI 助手执行任务;这个改动要解决的是,会话恢复了,调用提示却没带入用户更新后的任务说明,旧评论反而再次成为目标。它启发了我们检查“下一次调用实际拿到了什么”。
我们对原任务说明选择函数做了七组双版本检查。在已经规范化的唤醒输入下,两组普通恢复由只取简短任务标识,变为带入当前完整说明;新任务、重新分配和恢复动作等对照保持相应行为。实验只覆盖选择函数及辅助函数,没有运行数据库目标选择或真实 AI 会话恢复。
它与清空引用需要不同的处理:一个要切断旧关系,一个要把新要求送进仍然有效的会话。两者提供了同一种检查方法:沿着下一次调用,看看它真正拿到了什么。
清空后的验收,不妨多走一步:下一次操作,会从哪里重新开始? 顺着这个问题检查,才能找到空列表之外还需要处理的关系。这是我们从实验中得到的研究启发,尚未据此确认 CodeFlowMu 的对应缺陷或启动开发。 对照结果还把我们推向一个更具体的追问:这两处引用失效之后,已经排队的重试或延迟操作,会不会仍携带旧编号?本轮五项检查没有覆盖这些路径。要回答它,需要在清空前安排这样的待执行工作,再观察清空后真正发起的调用采用了什么。它是下一步可验证的问题,不能先写成又发现了一个故障。
实验范围与复现。 修复在本轮读取时尚未合入;本文针对固定候选源码,不代表所有已发布版本。远端接口使用模拟对象,SQLite 删除提交在本地真实执行;未验证跨进程重启、远端数据删除或通用权限撤销。五项 SDK 对照、七组 Paperclip 输入、固定版本和失败日志见实验材料,完整材料保存在研究仓库。
你点击“清空”时,期待它清掉哪些东西:眼前的记录、下一次回答使用的上下文,还是两者都清掉?工具给出的说明是否清楚?如果你负责会话系统,也欢迎补充你的反例或验证方法:清空之前已经排队的工作,在清空之后应如何处理?