langchain-ai/langchain#39087 · InMemoryRecordManager 批次内逐文档取时间戳——同批次时间基准漂移导致清理遗漏 · 2026-07-28
InMemoryRecordManager.update() 在
for 循环内逐文档调用 get_time(),
同一批次的文档拿到各不相同的时间戳。
索引器在开始时快照 index_start_dt 作为清理分界线;
当批次尾部文档的 updated_at 越过该分界线时,
cleanup="full" 静默漏删陈旧文档(issue 报告 num_deleted 在 800~900 间漂移,预期 1000)。
影响范围
langchain-core indexing API 全部清理模式
框架现状
⚠️ open · bug/core 标签 · 多人认领修复
ARK 方案
✅ OutputValidator 不变式 + OTel 留痕
index(docs, ..., cleanup="full") →
索引器快照 index_start_dt →
update() 循环内每个 key 单独取 get_time() →
大批次尾部文档 updated_at > index_start_dt →
清理阶段按 before=index_start_dt 查询陈旧文档时漏掉这些"时间穿越"记录 →
num_deleted 静默少于预期,向量库残留脏文档
langchain_core/indexing/base.py · InMemoryRecordManager.update():
时间基准应是批次级属性,却被实现成文档级属性。
循环内每次 get_time() 都读一次系统时钟,
同批次记录时间戳单调递增而非批次一致;与索引器"开始时刻快照分界线"的隐含假设冲突。
本质是批次操作缺乏原子时间基准 → 清理谓词的不变式(同批次记录同侧于分界线)被破坏。
OutputValidator 的守备范围。
📄 根因代码路径(issue 定位 + ARK 源码核实)
# InMemoryRecordManager.update() — langchain_core/indexing/base.py
for key, group_id in zip(keys, group_ids):
if time_at_least and self.get_time() < time_at_least: # ❌ 循环内取时钟 #1
raise AssertionError(...)
self.records[key] = {
"group_id": group_id,
"updated_at": self.get_time(), # ❌ 循环内取时钟 #2
}
# → 同批次 N 个文档拿到 N 个递增时间戳,批次时间基准不存在
📄 ARK 实机测量(2026-07-27,langchain-core 1.4.8 / py3.12 / macOS)
批次内时间戳漂移(100 keys/批): ~0.1ms(首尾差,实测确认非批次一致)✅ issue 复现脚本 5 次运行结果: num_deleted = 1000 / 1000 / 1000 / 1000 / 1000 issue 声称结果: num_deleted = 800~900(波动) 判定:反模式属实(代码级确定),触发依赖时钟精度/负载(行为级概率性)⚠️
失败可见性
静默(数字悄悄错)
触发确定性
非确定(平台相关)
后果
RAG 脏文档残留
✅ 方案A(推荐):OutputValidator 守住"清理完整性不变式"
from ark import OutputValidator
# 不变式:cleanup="full" 时,删除数必须等于「旧集合 − 新集合」
schema = {
"type": "object",
"fields": {
"num_added": {"type": "integer", "min": 0},
"num_deleted": {"type": "integer", "min": expected_deleted,
"max": expected_deleted},
},
"required": ["num_added", "num_deleted"],
}
validator = OutputValidator(schema)
result = index(docs, record_manager, vector_store, cleanup="full")
validator.validate(result) # 漏删 → 立即 ValidationError,而非静默残留💡 非确定性缺陷的克星不是"多测几次",而是把预期固化为运行时校验——生产哪次触发,哪次现形。
✅ 方案B:框架侧修复(与社区 PR 方向一致)
# 批次时间基准:循环前取一次,全批次共用
current_time = self.get_time() # ✅ 批次级快照
if time_at_least and current_time < time_at_least:
raise AssertionError(...) # ✅ 守卫移出循环(批次级检查)
for key, group_id in zip(keys, group_ids):
self.records[key] = {"group_id": group_id, "updated_at": current_time}💡 issue 下已有多位贡献者认领(PR #39090 方向相同)。ARK 立场:即便框架修复合入,不变式校验仍应保留——SQL/Postgres RecordManager 等其他实现同样可能存在时间基准问题。
✅ 方案C:OTel 留痕让"概率性漏删"可审计
import ark ark.auto_init() # 每次索引清理的 num_deleted 校验结果 # → ark.validation.pass / ark.validation.fail 事件 # → OTLP/JSON → Langfuse / Jaeger / Tempo # 「上周三凌晨那次漏删」从此可回溯、有证据
💡 非确定性缺陷的第二克星是留痕:漂移何时发生、幅度多大,观测栈里有名有姓。
55/100 · 功能正确,但一致性保障依赖时钟精度这一不可控因素
💡 健康得分 55/100。一致性仅 40——清理完整性取决于"批次执行速度是否跑赢时钟漂移",
这在测试机上几乎总是成立、在高负载生产上随机失守。接入 ARK
OutputValidator(清理完整性不变式)后,
漏删在边界 1 秒可见,一致性分可回到 90+。
本报告由 ARK 生成 · 智能体健康感知系统
langchain-ai/langchain#39087
· 锚定自真实 Issue(open · bug/core · ARK 实机部分复现,验证过程如实披露)