首页
/ claude-mem 的 LoCoMo 长对话记忆评测:基于 Worker API 构建端到端记忆摄入管道

claude-mem 的 LoCoMo 长对话记忆评测:基于 Worker API 构建端到端记忆摄入管道

2026-09-04 15:35:32作者:邬祺芯Juliet

本文讲解如何在 claude-mem 代码库中搭建 LoCoMo 长期对话记忆基准评测的第一阶段(Phase 01: Foundation & Ingestion Prototype):下载基准数据集、将多轮对话会话转换为 claude-mem 的 observations(记忆观察项)写入 Worker 服务,并通过检索接口验证整条记忆管道的端到端可用性。读完后你将掌握"外部对话数据集 → 确定性会话 ID → Worker HTTP API → 可检索 observations"的完整接入方案,理解双评分方法论(token 级 F1 + LLM-as-a-Judge)的设计意图,并具备复现该摄入管线的全部实操细节。

评测背景与方法论:为什么用 LoCoMo、为什么是双评分

LoCoMo 是面向长程多会话对话的记忆评测数据集,其原始论文(ACL 2024)使用 token 级 F1 作为核心指标。claude-mem 的这份 LoCoMo 评测 playbook(LOCOMO-EVAL-01.md)采用**双评分(dual scoring)**方法:

  • token 级 F1:用于与原 LoCoMo 论文结果对齐比较;
  • LLM-as-a-Judge(J-score):用于与现代记忆系统横向比较。文档中给出的参照基线(引自 Mem0 论文,arXiv 2504.19413)为:Mem0 66.88%、Zep 65.99%、OpenAI Memory 52.90%、全上下文上限(full-context ceiling)72.90%。

一个关键的方法论细节:adversarial(对抗性)QA 类别在 J-score 比较中被排除——因为该类别没有可用的 ground truth,这与 Mem0 的方法论保持一致。这一取舍直接影响了后续数据集加载器的接口设计(见下文 getQuestionsForConversation 的默认行为)。

Phase 01 的验收目标是:真实 LoCoMo 对话数据被存储为可检索的 claude-mem observations,证明"摄入 → 压缩 → 存储 → 检索"的完整记忆管道端到端跑通。

评测项目结构:目录布局与 TypeScript 配置

按 playbook 的定义,评测代码组织在仓库根目录下的 evals/locomo/ 中,目录树如下:

evals/locomo/
├── src/
│   ├── ingestion/      # 摄入适配器 + Worker API 客户端
│   ├── qa/             # QA 生成逻辑
│   └── scoring/        # F1 与 Judge 评分
├── data/               # 数据集(含克隆的 locomo 仓库,已加入 .gitignore)
├── results/
│   └── checkpoints/    # 断点/中间结果
├── scripts/            # 可执行脚本(验证、摄入、核对)
└── tests/              # 单元测试

配套配置要求:

  • evals/locomo/tsconfig.json 使用严格 TypeScript 设置:module: esnexttarget: esnextmoduleResolution: bundlerstrict: true。由于 Bun 原生运行 TypeScript,这份配置主要服务于 IDE 支持而非运行时;
  • evals/locomo/data/locomo-repo/ 加入项目根目录 .gitignore,避免克隆下来的数据集进入版本控制。

说明:在当前仓库快照中未检索到 evals/locomo/ 目录,上述布局以该 playbook 的描述为准(playbook 中所有任务均标记为已完成,实现可能位于独立分支或未合入当前主线)。下文引用的 Worker 路由源码路径则均已确认存在于当前仓库。

数据集准备与类型建模:先读数据,再写类型

数据获取流程强调"先检查、后建模":

  1. 将 snap-research 的 LoCoMo 官方仓库克隆到 evals/locomo/data/locomo-repo/(文档刻意提醒要先检查仓库结构确认数据集路径,预期在 data/locomo10.json);
  2. 检查 locomo10.json 的第一条记录,确认实际字段名后再编写类型定义——这是避免类型与数据脱节的关键纪律;
  3. evals/locomo/src/types.ts 中基于真实数据创建接口。

playbook 定义的完整类型清单(信息密度高,值得逐条理解):

类型 字段 说明
LoCoMoConversation sample_id, speaker_a, speaker_b, conversation(session 数组), qa(问题数组) 单条对话样本
LoCoMoSession session_id, date, turns 数组,另有索引签名 [key: string]: any 索引签名用于承接 session_1_observationsession_1_summaryevents_session_1动态键
LoCoMoTurn speaker ("A" | "B"), dia_id (number), text (string),可选 img_url 与 blip_caption 单个话轮,图像轮带 BLIP 描述
LoCoMoQA question, answer, category (string), evidence_dialog_ids (number[]) 问答对与证据轮 ID
IngestionProgress sample_id, total_sessions, sessions_ingested, observations_queued, status 摄入进度
QAResult question, predicted_answer, ground_truth_answer, category, f1_score, judge_scores(可选 JudgeAggregation), search_results_used, search_latency_ms, answer_latency_ms, answer_input_tokens, answer_output_tokens 单题结果,含延迟与 token 计量
JudgeResult score (0–100), explanation 单次 Judge 打分
JudgeAggregation mean_score, std_dev, run_count, individual_scores 多次打分的聚合
LatencyStats search_p50_ms, search_p95_ms, answer_p50_ms, answer_p95_ms, total_p50_ms, total_p95_ms 检索/回答延迟分位
EvalReport results, per_category_f1_scores(每类含 mean_f1/count/min_f1/max_f1), overall_f1, per_category_judge_scores(mean_j/std_dev/count), overall_judge_score, latency_stats, token_stats(total_input_tokens/total_output_tokens/mean_tokens_per_question), metadata(model, judge_model, timestamp, total_questions, scoring_methods: string[]) 最终报告结构

从类型设计可以推断出评测的完整数据流:逐题记录检索命中数与两段延迟(search/answer)、双套评分(F1 与多轮 Judge 聚合)、以及按类别拆分与全局汇总的报告结构——这正是 Phase 03+ 评分阶段要消费的数据模型。

数据集加载器:动态键解析与 adversarial 默认排除

evals/locomo/src/dataset-loader.ts 是摄入与 QA 阶段共同的只读入口,playbook 定义了五个函数:

  • loadDataset():读取并解析克隆仓库中的 locoma10.json,返回类型化的 conversation 数组;
  • getConversation(sampleId):按 sample_id 返回单条对话;
  • getSessionsForConversation(conversation):提取 sessions 并为每个 session 解析动态键——例如 session_id=1 的会话需查表取 session_1_observationsession_1_summaryevents_session_1,返回带 observation/summary/events 字段的增强会话对象。这是对 LoCoMo 数据"扁平键 + 数字后缀"结构的针对性封装;
  • getQuestionsForConversation(conversation, options?):返回类型化 QA 数组,接受可选的 excludeCategories 参数,默认排除 "adversarial"(服务于 J-score 比较);
  • getAllQuestionsForConversation(conversation):返回包含 adversarial 在内的全量类别(服务于 F1-only 分析)。

两套问答入口的分离,把方法论层面的"adversarial 不参与 J-score"约束固化成了 API 默认行为,调用方不需要记得手工过滤。

此外还有数据集自检能力:

  • getDatasetStats():返回 conversation_counttotal_sessionstotal_qa_questionsqa_by_category(类别 → 计数)、qa_excluding_adversarial
  • evals/locomo/scripts/validate-dataset.ts:加载数据集、调用 getDatasetStats()、打印格式化的统计表,单独列出 adversarial 计数并注明其不参与 J-score 比较

运行方式与验收标准:

bun evals/locomo/scripts/validate-dataset.ts

验收:输出应显示 10 条对话,且 session 与 QA 数量合理。

Worker API 客户端:以路由源码为契约的 HTTP 封装

这是 Phase 01 中与 claude-mem 主干代码耦合最深的一环。playbook 明确要求:先读 Worker 的 HTTP 路由源码,弄清确切的 API 契约(端点路径、请求/响应格式、认证要求),再动手写客户端。当前仓库中可以确认这两个路由文件的存在:

  • SessionRoutes.ts:会话初始化、观察项入队、会话完成等端点。从源码结构看,路由层依赖 SessionManagerDatabaseManagerClaudeProvider/GeminiProvider/OpenRouterProvider 与 provider 分发逻辑(selectProviderForGenerator),请求体经 zod schema 与 validateBody 中间件校验——这解释了为什么评测客户端必须严格按契约构造请求;
  • SearchRoutes.ts:检索端点。源码中 semanticContextSchema 定义的查询参数为 q(可选)、project(可选)、limit(字符串或数字,可选),与评测客户端 search(query, project, limit) 的三参签名一一对应。

客户端 evals/locomo/src/ingestion/worker-client.ts 的规格:

  • 认证:从 ~/.claude-mem/.env 读取 auth token(文档提示变量名形如 AUTH_TOKENCLAUDE_MEM_AUTH_TOKEN,要求实际读文件确认精确变量名后再写代码);
  • Base URLhttp://localhost:37777。该端口在仓库主干中也有印证——cmem-memory-credentials.ts 中定义了 HOST_OBSERVER_DEFAULT_PORT = '37777'
  • 类型化方法集
方法 端点 说明
initSession(contentSessionId, project, userPrompt) POST 会话初始化端点 建立 claude-mem 会话
queueObservation(contentSessionId, toolName, toolInput, toolResponse, promptNumber) POST observations 端点 入队一条工具执行观察项
completeSession(contentSessionId) POST 会话完成端点 收尾会话
getSessionStatus(contentSessionId) GET 会话状态 供轮询
waitForProcessing(contentSessionId, timeoutMs) 轮询 每 2 秒轮询一次 getSessionStatus,直到队列为空或超时
search(query, project, limit) GET 检索端点 必须带计时埋点:记录从请求发起到响应返回的 search_latency_ms

搜索方法的计时埋点不是装饰——QAResult.search_latency_msLatencyStats 中的 p50/p95 指标正是从这些逐次采样聚合而来的。

  • 错误处理要求描述性消息:连接被拒 → "Worker not running at localhost:37777";401 → "Invalid auth token";5xx → 附带响应体。

摄入适配器:把对话伪装成一次 Read 工具调用

evals/locomo/src/ingestion/adapter.ts 是 LoCoMo 数据与 claude-mem 摄入协议之间的翻译层,其核心设计是将每段对话会话伪装成一次真实的工具执行,让记忆管道按"正常使用"的方式运转:

1. 确定性 ID 生成

  • generateContentSessionId(sampleId, sessionId):返回确定性 ID,形如 locomo-{sampleId}-s{sessionId}。确定性意味着重复摄入同一会话可幂等对账;
  • generateProjectName(sampleId):返回 locomo-eval-{sampleId}——每条对话一个独立 project,这是 QA 阶段能把检索范围隔离到单条对话内的前提(search 的 project 参数与摄入时的 project 名必须一致)。

2. 会话到工具执行的转换 formatSessionAsToolExecution(conversation, session, enrichedSession)

  • toolName"Read"——模拟一次文件读取;
  • toolInputJSON.stringify({file_path: "conversation-transcript/session-" + sessionId + ".txt"})
  • toolResponse:格式化后的对话转录文本,模板如下:
[Session {N} — {date}]
[Conversation between {speaker_a} and {speaker_b}]

{speaker_a}: {turn 1 text}
{speaker_b}: {turn 2 text}
...
  • userPrompt"Conversation between {speaker_a} and {speaker_b} on {date}"

3. 一个刻意的设计决策tool_response 保持原始对话文本,不做任何预处理——由 claude-mem 的观察项压缩 agent(文档记为 Sonnet 4.6)自然完成观察项的提取与压缩,以此模拟真实使用场景。换句话说,评测的不只是"存储 + 检索",而是把"AI 压缩"这个核心环节也纳入了被测范围。这一设计与主干代码中 SessionRoutes 的生成器机制相呼应:入队的观察项会经由 provider 分发(selectProviderForGenerator)交给对应的 LLM provider 处理。

单条对话摄入原型:ingest-one.ts 的七步流程

evals/locomo/scripts/ingest-one.ts 是端到端验证的最小闭环脚本,流程规格:

  1. 启动前健康检查:请求 http://localhost:37777/api/health(或任意已知端点);连接被拒则提示用户执行以下命令启动 Worker 并以退出码 1 结束:
bun plugin/scripts/worker-service.cjs start

该入口脚本存在于仓库主干(worker-service.cjs)。

  1. 从数据集加载第一条对话
  2. 对对话中的每个 session 执行七步摄入循环:
    1. 用适配器生成会话 ID 与 project 名;
    2. 经 worker client 初始化 claude-mem 会话;
    3. 用适配器把对话轮次格式化为一次工具执行;
    4. 经 worker client 入队观察项;
    5. 等待处理完成(每 3 秒轮询一次,单 session 超时 180 秒);
    6. 完成会话;
    7. 记录日志:"Session {N}/{total} ingested — processing took {seconds}s"
  3. 打印最终汇总:对话 sample_id、摄入的 session 总数、总耗时。

运行与验收:

bun evals/locomo/scripts/ingest-one.ts

无错误完成即通过。playbook 特别提醒:该脚本会发起真实的 Anthropic API 调用用于观察项压缩,可能需要数分钟——这是理解"评测成本主要来自 LLM 压缩环节"的第一个直观证据。

检索验证:证明数据真的存进去且查得到

evals/locomo/scripts/verify-ingestion.ts 完成 Phase 01 的最后一块拼图:

  1. 以第一条对话的 project 名(即 locomo-eval-{sample_id})在 claude-mem 中检索其名下所有 observations;
  2. 打印观察项总数,并逐项打印 title 与 narrative 的前 100 字符;
  3. 从该对话中挑选 3 道 QA 题——一道 single-hop、一道 multi-hop(若可用)、一道 temporal(若可用),跳过 adversarial
  4. 以问题文本为 query、以该对话的 project 为范围检索,按以下格式输出:
Q: {question text}
Category: {category}
Ground Truth: {answer}
Top Search Results:
  1. {observation title} — {first 80 chars of narrative}
  2. {observation title} — {first 80 chars of narrative}
  3. {observation title} — {first 80 chars of narrative}
bun evals/locomo/scripts/verify-ingestion.ts

这一步验证的是整条链路:LoCoMo 对话 → claude-mem observations → 可检索上下文。如果检索结果能与 ground truth 对应上,说明"压缩时保留的信息量 + 检索的召回质量"共同满足了下游 QA 阶段的基本前提。

小结:Phase 01 交付了什么、下一步是什么

Phase 01 的交付物可以概括为四层:

  • 可复现的数据基线:类型化的数据集加载器 + validate-dataset.ts 统计自检(10 条对话、按类别计数的 QA 分布);
  • 契约驱动的客户端:先读 SessionRoutes.tsSearchRoutes.ts 源码再写客户端,认证、端点、计时、错误消息全部有明确规格;
  • 保真的摄入适配:确定性 ID + per-conversation project 隔离 + "原始对话交给压缩 agent"的模拟真实设计;
  • 端到端证据ingest-one.ts 证明管道能跑通,verify-ingestion.ts 证明数据可检索。

Phase 02 将在此基础上做全量摄入(10 条对话、支持断点续跑、逐会话容错),playbook 系列文件 LOCOMO-EVAL-02.md 记录了批量摄入与完整性核对的进一步细节,Phase 01 建立的 Worker 客户端与适配器模块会被直接复用。

复现这套评测时的实用提醒:

  • 37777 端口被占用时,先确认是 cmem-memory-credentials.ts 中定义的 host observer 默认端口冲突,再决定是否改端口;
  • search 的 project 参数必须与摄入时 generateProjectName 生成的名字完全一致,否则检索范围为空;
  • 摄入速度受 LLM 压缩调用限制,单 session 180 秒的轮询超时是为真实 API 延迟留的余量,调低它只会造成假性失败。
登录后查看全文
热门项目推荐
相关项目推荐