claude-mem × LoCoMo 评测:Phase 02 全量数据集批量入库的可恢复批处理设计
本文围绕 claude-mem 仓库中 LoCoMo 长程对话记忆评测的第 02 阶段(.maestro/playbooks/Wizard-2026-02-22/2026-02-22-LoCoMo-Eval/LOCOMO-EVAL-02.md)展开,讲清楚如何把 10 条 LoCoMo 长对话经 claude-mem worker API 批量压缩入库:批量脚本的可恢复(resume)设计、逐会话的 ingest 调用链、基于 /api/observations 的完整性校验,以及一次"进度文件显示全部完成但数据实际丢失"的真实事故复盘。读完后你能掌握一套可复制的长耗时批处理范式——断点续跑、进度落盘、优雅失败、入库校验,并能理解 claude-mem 的 worker HTTP 接口契约。
1. 背景:LoCoMo 评测在 claude-mem 中的位置
claude-mem 的定位是"为各类 Agent 提供跨会话持久化上下文":捕获会话中的一切、用 AI 压缩、再把相关上下文注入未来的会话。.maestro/playbooks/Wizard-2026-02-22/2026-02-22-LoCoMo-Eval/ 目录下的 6 个 Phase 文档构成一个完整的评测剧本:用 LoCoMo(ACL 2024 提出的长程对话问答基准,10 条多人多会话长对话 + 配套 QA)来检验"对话 → 观察(observations)压缩入库 → 检索 → 作答"这条记忆管线。
整个评测的关键前提(见 Phase 01):
- 入库方式:把每条 LoCoMo 会话伪装成一次工具执行(
tool_name: "Read",tool_response为原始对话文本),走 claude-mem 正常的 observation 压缩链路,由 Sonnet 4.6 自然抽取观察,模拟真实使用而非为评测定制; - 隔离方式:每段对话(conversation)映射为独立项目名
locomo-eval-{sample_id},QA 阶段检索只限定在本项目内,实现检索隔离; - Phase 02 的任务:在 Phase 01 已验证"单条对话能走通端到端"的基础上,把全部 10 条对话都入库,形成完整的记忆存储——这是整个评测中 API 调用最密集、耗时最长的阶段。
需要注意适用前提:该阶段脚本位于 evals/locomo/ 目录(当前仓库快照中不包含该目录,属评测工作产物),运行需要本地 worker 已在 http://localhost:37777 启动(可用 bun plugin/scripts/worker-service.cjs start 启动),并已配置有效的 Anthropic API 凭据(worker 凭据从 ~/.claude-mem/.env 读取)。
2. 批量入库脚本 ingest-all.ts 的四个设计点
Phase 02 的第一个任务是构建带恢复支持的批量入库脚本 evals/locomo/scripts/ingest-all.ts,复用 Phase 01 已建好的 worker 客户端与适配器模块(evals/locomo/src/ingestion/worker-client.ts、evals/locomo/src/ingestion/adapter.ts)。文档明确了四个设计约束,每一个都对应一个真实的批处理痛点:
(1)先查后写,实现断点续跑。 在摄入每段对话之前,先用其项目名(locomo-eval-{sample_id})在 claude-mem 中检索是否已有 observations——有则跳过。这意味着脚本天然幂等:中断后重新执行同一命令即可从断点继续,不需要单独的 checkpoint 状态机。这个"以存储本身为状态"的做法,后面在校验环节也复用了同一机制。
(2)串行而非并发。 对话严格逐条处理(one at a time),理由文档写得很直接:避免压垮 worker 的处理队列。对每个会话内部仍是 Phase 01 的固定四步流程:
Init session → queue observation → wait for processing → complete session
对应到 claude-mem 的 worker HTTP 契约:会话路由定义在 SessionRoutes(init / observations 入队 / complete / status 轮询),worker 在入队后由生成器异步压缩 observation,waitForProcessing 每 2 秒轮询一次状态直到队列为空或超时。串行的选择与这个"入队—异步压缩—轮询"的模型一致:压缩走的是真实 Anthropic API 调用,并发只会引入配额与队列压力,换不来整体吞吐提升。
(3)滚动进度输出。 每处理一步打印形如 Conversation {N}/10 [{sample_id}] — Session {M}/{total} — Elapsed: {time} 的进度行,让长时间运行(本阶段总计约 40 分钟)时人可以随时判断卡点在哪一段哪一会话。
(4)单会话失败不中断全局。 如果某个会话在重试后仍然失败,记录详细错误并继续下一个会话——"don't abort the entire run"。配合(1)的跳过逻辑,失败会话可以在下次运行时被单独重跑,而不是把整批 10 条对话的成果作废。
此外,每完成一条对话就把其状态追加写入 evals/locomo/results/ingestion-progress.json(不存在则创建),形成一份人可读的运行日志。
3. 实际运行结果:10/10 完成,但 conv-42 暴露了恢复逻辑的盲区
文档记录的真实运行结果为:10/10 对话处理完成(2 条跳过、8 条新入库、0 失败),总耗时 40m41s。其中值得玩味的是一条脚注:
conv-42 has 10/29 sessions due to partial ingestion from a prior run — resume logic skipped it since it already had observations.
也就是说 conv-42 在之前某次运行中只完成了 29 个会话里的 10 个,而"是否已入库"的判定粒度是对话级(只要该项目下存在任何 observation 就整条跳过),导致剩下的 19 个会话被永久漏掉。这是一个很典型的恢复逻辑粒度问题:断点判定的检查单位(conversation)粗于最小入库单位(session)。对读者而言的启示是——设计 resume 时,跳过粒度必须与重试粒度匹配,否则"部分完成"的状态会变成静默的数据缺口。
4. 入库校验:/api/observations 与通配符的坑
Phase 02 的第三个任务是构建入库完整性校验脚本 evals/locomo/scripts/verify-all-ingestion.ts:对 10 个对话分别按项目名检索 observations、计数,并与数据集中的会话数比对,输出完整性报告表:
sample_id | sessions | observations | status
-----------------+----------+--------------+---------
{sample_id_1} | 12 | 12 | complete
{sample_id_2} | 8 | 8 | complete
...
任何 observation 数量显著低于会话数的对话都要被标记出来;在推进 Phase 03(QA 作答管线)之前,全部 10 条对话应呈现 complete 或 near-complete。
这一步踩到并记录了一个具体的实现坑:/api/search?query=* 的通配符假设是错的——在 Chroma 语义检索里 * 被当作字面文本处理,而不是通配符。校验脚本因此改用 /api/observations 端点做纯过滤查询(自动分页)。这个端点确实存在且契约明确,在 worker 服务架构文档中有完整定义:
GET /api/observations?project=my-project&limit=20&offset=0
project(可选):按项目名过滤——这正是本评测"一对话一项目"隔离设计的用武之地;limit(默认 20)/offset(默认 0):分页参数,hasMore指示是否还有下一页。
响应体包含 id、title、type、narrative、created_at 等字段。该路由在源码 DataRoutes 中注册(app.get('/api/observations', ...)),同文件还注册了 GET /api/observations/by-file 与 POST /api/observations/batch(按 ID 批量取回)。
为了支撑这种"只要过滤、不要语义检索"的用法,本次还给 WorkerClient 新增了 listObservationsByProject 方法——专门走 SQLite 过滤路径,绕开语义检索层。这是一个合理的客户端分层:检索语义与列举过滤是两种不同的数据访问需求,不该挤在同一个 search() 方法里。
4.1 校验揭穿的事故:进度文件说谎了
校验脚本跑出的结果是文档中最有信息量的一段:272 条 observations 中只有 2 条真正持久化进了 claude-mem(conv-26 与 conv-42 各 1 条),数据库中只存在这 2 个项目,其余 8 条对话的项目完全没有数据。而前一次运行产生的 ingestion-progress.json 却声称 10 条全部完成。
文档给出的排查结论是:入库流程"跑完了"(进度文件已追加),但 observations 没有留下——可能的原因包括 worker 重启、数据库变更或压缩失败。这个案例的价值在于它展示了批处理校验为什么必须独立于进度日志存在:
- 进度文件只证明"执行过",不证明"持久化了"。进度追加发生在流程推进时,若后续环节(压缩、落库)因 worker 重启等原因失败,进度与实际存储就会脱节;
- 以存储为事实源的交叉验证(拿项目名去查库里的实际记录数)是唯一能发现这类脱节的手段;
- 处置措施明确:Phase 03 开始之前需要重新入库。
5. 入库适配器的确定性测试
Phase 02 的第四个任务是为入库适配器补测试(evals/locomo/tests/adapter.test.ts)。适配器负责把 LoCoMo 会话结构转成 worker API 参数,其两个核心约定来自 Phase 01 的设计:
generateContentSessionId(sampleId, sessionId)→ 确定性 ID(形如locomo-{sampleId}-s{sessionId});generateProjectName(sampleId)→locomo-eval-{sampleId}。
本次扩展了 5 个新用例:
| 用例 | 验证目标 |
|---|---|
| 确定性 ID 一致性 | 相同输入必产生相同输出(resume 跳过逻辑成立的前提) |
| 唯一 ID 碰撞检查 | 不同 sampleId/sessionId 组合永不碰撞 |
| 单轮会话 | 边界输入不崩 |
| 空 turns 数组 | 边界输入不崩 |
| 60 轮长对话 | 长文本格式化路径(formatSessionAsToolExecution) |
其中 formatSessionAsToolExecution 把会话渲染成 tool_name: "Read"、tool_input 为 {"file_path": "conversation-transcript/session-N.txt"} 的伪工具调用,tool_response 则是保留说话人前缀的原始对话([Session {N} — {date}] 头 + 逐行 {speaker}: {text}),user_prompt 为 "Conversation between {A} and {B} on {date}"。测试会验证 tool_response 包含全部发言行、user prompt 提及两位说话人。最终 14 个适配器测试全部通过,6 个评测测试文件的 57 个测试全部通过(运行方式:bun test evals/locomo/tests/adapter.test.ts)。
确定性 ID 的测试并非过度设计:正是因为 Phase 02 的 resume 判定依赖"按项目名查已有 observations",而 observations 又关联 content session id,ID 生成若不具幂等性,重跑就会制造重复数据。
6. 可复制的要点与后续
把 Phase 02 抽象出来,是一个长耗时、依赖外部 API 的批处理任务的完整骨架,四个要素缺一会出事:
- 幂等入口:先查存储再执行,粒度与最小重试单位对齐(本例的教训:对话级跳过粒度粗于会话级入库粒度,漏掉了 conv-42 的 19 个会话);
- 失败隔离:单条失败记录后继续,不整批中止;
- 进度落盘:
ingestion-progress.json追加式记录,便于人眼追踪 40 分钟级长跑; - 独立校验:用与入库不同的路径(过滤查询而非语义检索)回读存储,验证"执行过 = 持久化了"这一隐含假设——本例正是靠它发现 272 条只剩 2 条。
在 claude-mem 侧,本阶段依赖的能力全部来自其既有 worker API:会话与 observation 入队路由(SessionRoutes、SearchRoutes)、分页 observations 查询(DataRoutes),以及文档化的端点契约(worker-service 架构文档)。Phase 02 完成后,评测进入 Phase 03:基于检索结果构建上下文窗口、用 Opus 生成抽取式答案,并与 Mem0/Zep 等记忆系统的公开指标对比。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00