claude-mem LoCoMo 评测实践:双评分引擎(Token 级 F1 + LLM-as-a-Judge)的设计与实现
本文以 claude-mem 仓库中 LoCoMo 评测项目的 Phase 04 阶段文档为主体,完整拆解其"双评分引擎"的设计:如何用标准化的 Token 级 F1 指标与原 LoCoMo 论文基线对齐,如何用 LLM-as-a-Judge(J-score)与 Mem0 等现代记忆系统可比,以及评分聚合、基线对比报表和配套测试策略的具体实现。读完后你将掌握可复用的评测打分方法论——从文本归一化管道、多集合词元交集的 F1 计算,到多次判分取均值±标准差的统计设计。
1. 背景:为什么需要双指标评分
claude-mem 的 LoCoMo 评测(工作目录为 .maestro/playbooks/Wizard-2026-02-22/2026-02-22-LoCoMo-Eval/)是一套对持久记忆系统的系统性基准测试,分六个阶段推进:数据摄取(Phase 01)、数据集加载(Phase 02)、QA 回答管道(Phase 03)、双评分引擎(Phase 04)、完整评测运行器(Phase 05)、结果分析与发布(Phase 06)。Phase 04 是本篇的主体,它解决一个核心问题:两套历史悠久的评分体系不可互相替代,必须同时实现。
- Token 级 F1:沿用原 LoCoMo 论文(ACL 2024)的评分方式,用于与历史基线(LoCoMo 原论文及 Mem0 论文中的 F1 表)对比;
- LLM-as-a-Judge(J-score):沿用 Mem0 论文(arXiv 2504.19413,ECAI 已接收)的方法,用于与现代记忆系统对比——Mem0(66.88%)、Mem0g(68.44%)、Zep(65.99%)、全上下文上限(full-context,72.90%)。
J-score 判分器刻意选用 Claude Sonnet 4.6(模型 ID claude-sonnet-4-6),与回答器使用的 Opus 4.6(claude-opus-4-6)不同模型,以避免"自己判自己"的偏差(self-judging bias);每题进行 10 次独立判分以获得统计显著性,与 Mem0 的统计口径保持一致。完整方法学记录见同目录的 LOCOMO-EVAL-06.md 中规划的 methodology.md,其明确写道:"F1 存在已知的长度偏差(LoCoMo-Plus, arXiv 2602.10715v1),J-score 是现代记忆系统对比的标准做法。双指标同时提供向后兼容性与现代可比性。"
适用前提说明:本文的评分实现细节(模块路径、函数签名、参数)均以 Phase 04 阶段文档中定义的规格为准;评测代码位于独立的
evals/locomo/工作目录,未包含在当前仓库主目录的源码树中,因此下文的实现引用均指向阶段文档中的规划路径。
2. Token 级 F1 评分模块(evals/locomo/src/scoring/f1.ts)
F1 模块的核心思想是:把"预测答案"与"标准答案"当作两个词元多重集(multiset),计算交集相对各自规模的比例,再合成 F1。模块定义并导出以下四个函数供测试使用。
2.1 归一化管道 normalizeAnswer
normalizeAnswer(text: string) 实现抽取式 QA 基准测试的标准归一化流程,顺序固定:
- 转小写;
- 移除冠词:
"a"、"an"、"the"; - 移除所有标点(仅保留字母数字与空格);
- 将连续空白折叠为单个空格;
- 修剪首尾空白。
2.2 Porter 词干提取 porterStem
porterStem(word: string) 要求实现一个最小化 Porter 词干提取器——至少覆盖 Step 1a/1b/1c:复数、-ed、-ing、-ness。文档同时给出了一个务实的替代方案:检查 LoCoMo 官方仓库(规划中的 data/locomo-repo/)是否自带 Python 评分脚本,若有则严格匹配其归一化管道,以保证与论文数字对比的公平性。这一点体现了基准测试的可比性原则——自己的归一化细节差异足以造成分数不可比。
2.3 分词与 F1 计算 tokenize / computeTokenF1
tokenize(normalizedText: string):按空白切分,对每个词元应用 Porter 词干提取。
computeTokenF1(predicted: string, groundTruth: string) 是核心打分函数,完整计算步骤:
- 对两个字符串执行归一化;
- 对两个字符串执行分词(含词干提取);
- 计算词元重叠:
common = 预测词元与标准答案词元的交集——必须使用多集合交集(multiset intersection),即按每个词元出现次数计数,例如"the the the"对"the"只能取到 1 个the; - Precision =
|common| / |predicted_tokens|(预测为空时取 0); - Recall =
|common| / |ground_truth_tokens|(标准答案为空白取 0); - F1 =
2 * P * R / (P + R),当P + R = 0时取 0; - 特殊情形:归一化后双方都是空串时,F1 = 1.0(都"没说话",视为一致)。
多集合交集这一细节直接决定了重复词的处理行为,是 F1 测试集中专门用 "the the the" vs "the" 验证的点。
3. LLM-as-a-Judge 评分模块(evals/locomo/src/scoring/judge.ts)
J-score 模块基于 Anthropic SDK(Phase 03 已安装 @anthropic-ai/sdk),由三个部分组成。
3.1 判分提示词
判分系统提示词(Judge system prompt):指示 Claude Sonnet 4.6 扮演公正的评估者,在 0–100 分制上给预测答案打分,评估维度与 Mem0 的判分标准一致,共四个维度:
- 事实准确性(Factual accuracy):预测答案基于标准答案是否事实正确;
- 完整性(Completeness):是否覆盖标准答案中的关键信息;
- 相关性(Relevance):回答是否与所问问题相关;
- 语境恰当性(Contextual appropriateness):回答是否扎根于对话上下文。
判分用户提示词 buildJudgePrompt(question, groundTruth, predictedAnswer, category):
- 同时包含问题、标准答案、预测答案;
- 附带类别标签(category label),让判分器按类别施加不同标准——例如时序(temporal)类问题需要精确的日期/顺序匹配;
- 要求结构化输出:JSON 对象
{"score": <0-100>, "explanation": "<1-2 句理由>"}。
3.2 单次判分 judgeAnswer
judgeAnswer(question, groundTruth, predictedAnswer, category) 的关键 API 参数与容错策略:
| 参数/行为 | 取值 | 设计意图 |
|---|---|---|
| model | claude-sonnet-4-6 |
与回答器(Opus 4.6)不同模型,避免自判偏差 |
| max_tokens | 256 | 只需输出短 JSON |
| temperature | 0.5 | 保留判分器跨次运行的方差以估计波动,同时维持合理一致性——Mem0 报告 10 次运行的标准差为 ±0.15 至 ±0.75 |
| 返回值 | JudgeResult(score 0–100,explanation 字符串) |
— |
容错处理按三级降级:解析 JSON 响应提取 score 与 explanation → 若 JSON 解析失败,尝试从纯文本中抽取数字分 → 仍失败则返回 score = -1 并附带错误说明。-1 是"判分失败"的哨兵值,供聚合阶段过滤。
3.3 多次判分聚合 judgeAnswerMultipleRuns
judgeAnswerMultipleRuns(question, groundTruth, predictedAnswer, category, numRuns) 默认执行 10 次(numRuns 可配):
- 并行执行:用
Promise.all按每批 3 个的节奏并发,平衡速度与速率限制; - 过滤掉所有 score = -1 的失败运行;
- 对成功运行计算:均值(mean score)、标准差(std dev)、个体分数数组;
- 返回
JudgeAggregation:{ mean_score, std_dev, run_count, individual_scores }; - 若成功运行少于 5 次,记录警告——这通常意味着系统性的判分失败(如 API 持续错误),而非随机波动。
这个"10 次采样 + 均值±标准差"的设计,正是 J-score 能跨系统横向比较的统计基础:Mem0、Zep 等基线数字本身就是同样口径的均值。
4. 结果聚合与双指标报表(evals/locomo/src/scoring/reporter.ts)
报表模块从 evals/locomo/src/types.ts 导入类型,职责是把逐题结果聚合为可按类别对比的指标。
4.1 聚合函数
| 函数 | 语义 |
|---|---|
scoreResultsF1(results) |
对每条 {predicted_answer, ground_truth, category} 应用 computeTokenF1,产出填充了 f1_score 的 QAResult 数组 |
aggregateF1ByCategory(results) |
按类别分组,返回每个类别的 {mean_f1, count, min_f1, max_f1} 映射 |
computeOverallF1(results) |
宏平均(macro average):所有题目 F1 之和 ÷ 总题数 |
aggregateJudgeByCategory(results) |
按类别分组,对每题的 mean_score 再求均值,返回 {mean_j, pooled_std_dev, count} |
computeOverallJudge(results) |
每题均值 J 分的宏平均,附合并标准差(pooled standard deviation) |
4.2 F1 基线(F1_BASELINES)
来自原 LoCoMo 论文与 Mem0 论文,用于历史对比:
F1_BASELINES = {
"Human": { overall: 87.9 },
"Mem0": { overall: null, single_hop: 38.72, multi_hop: 28.64, temporal: 48.93, open_domain: 47.65 },
"Mem0g": { overall: null, single_hop: 38.09, multi_hop: 24.32, temporal: 51.55, open_domain: 49.27 },
"Zep": { overall: null, single_hop: 35.74, multi_hop: 19.37, temporal: 42.00, open_domain: 49.56 },
"LangMem": { overall: null, single_hop: 35.51, multi_hop: 26.04, temporal: 30.75, open_domain: 40.91 },
"OpenAI Memory": { overall: null, single_hop: 34.30, multi_hop: 20.09, temporal: 14.04, open_domain: 39.31 },
"A-Mem": { overall: null, single_hop: 20.76, multi_hop: 9.22, temporal: 35.40, open_domain: 33.34 },
"GPT-3.5-turbo-16K": { overall: 37.8 },
"RAG-observations (original paper)": { overall: 41.4 },
"GPT-4-turbo": { overall: 32.1 }
}
4.3 J-score 基线(J_BASELINES)
来自 Mem0 论文(arXiv 2504.19413),不含对抗类(adversarial)(该类在 Mem0 口径下无标准答案):
J_BASELINES = {
"Full-context": { overall: 72.90 },
"Mem0g": { overall: 68.44, single_hop: 65.71, multi_hop: 47.19, temporal: 58.13, open_domain: 75.71 },
"Mem0": { overall: 66.88, single_hop: 67.13, multi_hop: 51.15, temporal: 55.51, open_domain: 72.93 },
"Zep": { overall: 65.99, single_hop: 61.70, multi_hop: 41.35, temporal: 49.31, open_domain: 76.60 },
"RAG (best, k=2 256-tok)": { overall: 60.97 },
"LangMem": { overall: 58.10, single_hop: 62.23, multi_hop: 47.92, temporal: 23.43, open_domain: 71.12 },
"OpenAI Memory": { overall: 52.90, single_hop: 63.79, multi_hop: 42.92, temporal: 21.71, open_domain: 62.29 },
"A-Mem": { overall: 48.38, single_hop: 39.79, multi_hop: 18.85, temporal: 49.91, open_domain: 54.05 }
}
文档同时给出一条重要边界说明:Letta 报告的 74.0% "accuracy" 使用了不同的评分方法(非 LLM-as-a-Judge),不可直接比较,只作为脚注出现。这是基线对齐时的典型陷阱——同名指标未必同口径。
4.4 对比表格生成
formatF1ComparisonTable(evalResults, f1Baselines):生成 claude-mem F1 对全部 F1 基线的 markdown 表,按类别 + 总体;formatJudgeComparisonTable(evalResults, jBaselines):J 分对比表,按类别 + 总体,附 ±标准差;formatLatencyComparisonTable(latencyStats):延迟/Token 对比表,Mem0 公布的参照数字为 search p50 = 0.148s、search p95 = 0.200s、total p50 = 0.708s、total p95 = 1.440s、1,764 tokens/query;formatFullReport(evalResults):汇总完整 markdown 报告——三张对比表 + 按类别拆解 + top-5/bottom-5 题目错误分析 + 延迟与 Token 汇总。
5. 评分模块的测试策略
Phase 04 要求为两套评分各写一份测试文件,并给出了可验证的具体断言值,这是保证评测工具本身可信的关键一环。
5.1 F1 测试(evals/locomo/tests/f1-scoring.test.ts)
覆盖用例与设计意图:
- 精确匹配:
"the cat sat"vs"the cat sat"→ F1 = 1.0; - 部分匹配:
"the big cat sat"vs"the cat sat"→ 验证词元重叠被正确反映; - 完全不匹配:
"dog"vs"cat"→ F1 = 0.0; - 归一化:
"The Cat!"vs"the cat"→ F1 = 1.0(大小写 + 标点被消解); - 冠词移除:
"a big dog"vs"big dog"→ F1 = 1.0; - 词干提取:
"running quickly"vs"runs quick"→ 词干化后 F1 应非零; - 边界:预测为空、标准答案非空 → 0.0;双方均空 → 1.0;
- 多集合行为:
"the the the"vs"the"→ 验证按出现次数计数的 precision/recall; - 聚合正确性:mock 3 条 single-hop(F1 = 0.8/0.6/1.0)+ 2 条 multi-hop(0.5/0.7)数据,验证类别均值;
computeOverallF1与期望的加权平均一致。
5.2 Judge 测试(evals/locomo/tests/judge-scoring.test.ts)
buildJudgePrompt输出必须包含问题、标准答案、预测答案、类别四项;- mock Anthropic SDK 验证
judgeAnswer调用参数:model 为claude-sonnet-4-6、temperature 0.5、max_tokens 256——参数级断言防止配置漂移; - JSON 解析失败路径:mock 一个格式错误的响应,验证返回 score = -1;
judgeAnswerMultipleRuns聚合:mock 10 次返回[70, 72, 68, 71, 73, 69, 70, 72, 71, 70]→ 验证 mean ≈ 70.6、std ≈ 1.5;- 失败运行过滤:mock 10 次中有 2 次返回 -1 → 聚合只使用 8 次成功运行并触发警告。
阶段文档记录的验收结果为:F1 评分测试 25/25 通过,Judge 评分测试 14/14 通过,全量测试套件 119/119 通过(0 失败),运行命令为 bun test evals/locomo/tests/f1-scoring.test.ts 与 bun test evals/locomo/tests/judge-scoring.test.ts。
6. 在原型结果上的双指标打分(score-prototype.ts)
Phase 04 的落地环节是在 Phase 03 的 QA 原型输出上跑双评分,脚本为 evals/locomo/scripts/score-prototype.ts:
- 从
evals/locomo/results/qa-prototype-results.json加载原型结果(据 LOCOMO-EVAL-03.md 记载:conv-26 对话的 20 题——10 条 temporal、8 条 single-hop、2 条 multi-hop;平均搜索延迟 1165ms、平均回答延迟 2032ms、约 455 tokens/题); - F1 打分:对每题应用
scoreResultsF1,打印按类别 F1 拆解与总体 F1——纯本地计算,无 API 调用; - J 打分:每题调用
judgeAnswerMultipleRuns(10 次)。对约 20 题的原型即约 200 次判分 API 调用,批间加 200ms 延迟以规避速率限制; - 输出三张对比表:F1 对基线、J 分对基线、原型运行的延迟汇总;
- 输出中注明口径:"Prototype only(~20 题,来自 1 个对话)。完整评测在 Phase 05 执行。"
运行命令:bun evals/locomo/scripts/score-prototype.ts。
这一"原型先行、全量后置"的节奏是整个评测工程的重要模式:Phase 04 只在 20 题上验证双评分链路端到端可用,把昂贵的大规模判分推迟到 Phase 05 的带检查点运行器(LOCOMO-EVAL-05.md)——那里 J 打分被拆成独立 pass,与 QA pass 分开 checkpoint,使得"先落盘 QA 结果、再单独重跑判分"成为可能。
7. 小结:这套双评分引擎的方法学价值
Phase 04 的双评分引擎有三个可迁移的设计决策值得注意:
- 指标双轨、口径对齐:F1 服务历史可比性,J-score 服务现代可比性;每个指标都锁定一个"参考实现的口径"(LoCoMo 原论文 / Mem0 论文),并在基线表中显式标注不可比项(Letta);
- 判分器与被评系统解耦:不同模型判分(Sonnet 判 Opus 的产出)+ 多次采样(10 次)+ 失败哨兵值(-1)过滤 + 低置信度警告(成功运行 < 5 次),共同构成对 LLM 判分噪声的防御;
- 评测工具本身被测试:归一化、词干化、多集合计数、参数传递、解析降级、聚合数学全部有可断言的单元用例(验收 39 个评分用例、全量 119 个用例通过),避免"评测器 bug 污染基准结论"。
对于要在仓库内复现这套流程的读者,建议的查阅路径是:以 .maestro/playbooks/Wizard-2026-02-22/2026-02-22-LoCoMo-Eval/LOCOMO-EVAL-04.md 为评分实现规格书,用 LOCOMO-EVAL-03 对照回答管道、用 LOCOMO-EVAL-05/06 对照运行器与最终报告结构。需要说明的限制:完整评测运行依赖 ANTHROPIC_API_KEY(QA 用 Opus 4.6、判分用 Sonnet 4.6),且评测工作目录 evals/locomo/ 及数据集 data/locomo-repo/ 不属于本仓库主源码树,本文所述实现细节均以阶段文档的规格与验收记录为依据。
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 StartedRust0623
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