LibreChat activity-label 评测基架解析:用固定语料与逐因素变体度量提示词文本质量
本技术指南以 LibreChat 仓库中 scripts/activity-labels/README.md 所描述的 activity-label eval harness(活动标题评测基架) 为主线,系统讲解这套用于评测“快模型生成的活动分组标题(activity-label headers)散文质量”的实验基架:为什么必须用固定语料替代肉眼抽查、语料库与评测用例如何设计、run.mts 等命令行工具如何一次跑完一遍全量评测,以及它产出的五项可复现结论如何反向改写了生产提示词(ACTIVITY_INSTRUCTION)。读者读完后,既能直接在本仓库复现该评测流程,也能把这套“单一因素变体 + 固定语料 + 机械检查 + 人工复读表格”的评测方法论迁移到自己的 LLM 提示词工程中。
背景:它评测的“activity-label”是什么
在 LibreChat 的 Agent 运行链路里,一次工具调用批次结束后,会在转录流中插入一行单行标题(collapsed group header),用于概括紧贴在它下方的一批工具卡片。这行标题由 activityLabel 机制生成,其核心实现在 runtime.ts 的 createActivityLabelHook 中:这是一个 PostToolBatch hook——在批边界同步抢占一个内容槽位,随后用便宜的快速模型在分离的 Promise 里生成一行摘要,fill 成功则把占位符替换为真实标题,失败则填充 null(界面回退到确定性的计数占位)。由于标题正上方就是工具卡片,而卡片本身已展示工具名、数量与参数,因此任何对工具名、数量、参数的复述都是噪音——标题唯一的价值在于说出卡片无法表达的信息:这一批调用到底“确立或产出了什么”,以及结果如何(参见 runtime.ts 中对该职责的注释)。
评测基架存在的直接动因是:这类标题的文本质量(措辞、句式、语域一致性、跨批次冗余)很难靠“在一个会话上肉眼抽查”来稳定评估。README 的引言一句话点明了它的立场:
Instruction changes are graded against a fixed corpus instead of eyeballed on one conversation——the two hypotheses that felt most obvious when this was built both turned out wrong under measurement(见 Findings)。
也就是说,这套基架是一台“让假设接受测量”的实验装置,而不是一次性的检查脚本。
设计原则:单一因素变体 + 固定语料 + 对基线指令的漂移免疫
一个可靠的提示词实验,必须做到三件事,这套基架在 variants.mts 中逐条落实:
- 单一因素变更:每个变体只相对生产指令改动一个假设,这样“某个结果只归因于一个假设”。文件头注释明确列出四个初始假设——
verbs(Good 示例中的动词分布会诱发语域塌缩,因为 9 条生产标题里有 6 条以 “Confirmed” 开头)、ordered(4–9 词的长度约束放在段落中间会被挤掉,把格式约束挪到最后可提升遵从率)、continuity(把本轮此前的标题回喂给模型可消除跨批次冗余)、baseline(照搬当前分支实际下发的指令)。 - 基线从“构建产物”读取:
baseline不是手工复制粘贴的指令文本,而是通过createRequire从已构建的packages/api/dist中读取ACTIVITY_INSTRUCTION(variants.mts)。这保证评测绝不会拿着过期的分支指令打分。若仓库还没构建,run.mts会提示先执行npm run build:data-provider && npm run build:data-schemas && npm run build:api,或通过环境变量LABEL_EVAL_DIST=/path/to/packages/api/dist/index.cjs指向一个已构建的其他 checkout——这使你可以“在一个尚未构建的工作树里给另一个分支的指令打分”。 - 漂移自检:variants.mts 内置一个断言——把内部维护的
LEGACY_ORDER/SHIPPED_ORDER句子表与构建产物里的ACTIVITY_INSTRUCTION做全等比较,一旦两者不一致就输出警告,提示“组合变体已过期”,从机制上防止生产指令与评测指令悄悄分叉。
文件布局:基架的每一块拼图
README 的 Layout 表格给出了每个文件的分工,下表完整继承并补充了源码层的作用:
| 文件 | 作用(结合源码) |
|---|---|
captured.json |
9 个取自 Langfuse 的逐字节真实生产负载(2026-07-29 的 sandbox-probe 运行),包含当时实际下发的标题。README 特别强调其“不可再生”:trace 会过期,一旦丢失无法重新抓取。 |
corpus.mts |
17 个用例 / 28 个步骤:把抓取的 9 条记录按生产运行顺序重放成一个序列(sandbox-probe-run),再加上生产负载从未命中的模式合成用例(全失败、部分失败、并行、快速重复、条目溢出、截断输出、错误形状的成功等)。多步骤用例把每一步生成的标题链入下一步的上下文,用来测量跨批次冗余。 |
prompt.mts |
对 SDK buildActivityLabelPrompt 的忠实移植(保留 section 顺序:Intent → Reasoning excerpts → Tool calls → Label:、12 条 cap 及 “…and N more” 后缀、精确截断语义),让合成用例渲染出生产环境会真正发送的字节。在此之上额外增加一个默认关闭的 “Previous headers” 段落——这就是 P1 continuity 假设的实验探针:在 SDK 字段尚不存在时就能先行测量修复效果。 |
variants.mts |
单一因素指令变体。baseline 直接从构建产物读取(见上节),杜绝漂移。 |
checks.mts |
机械评分:长度、标点、markdown、工具名回声、数量回声,以及把重叠度拆成 restate(信息增量)与 template(同一句式框架、新负载)两类。 |
run.mts / rescore.mts |
在线运行器(生产 wire shape、max_tokens: 256)与离线再评分器(重读存储结果、不重复花钱)。 |
types.mts |
语料、变体、结果、报表共享的 TypeScript 类型;FLAG_TYPES 列出全部机械标记。 |
tsconfig.json |
基架自身的严格、无输出类型检查配置。 |
语料库的两半:真实性与覆盖面
corpus.mts 的注释把语料分为两半:
-
真实的一半:
captured.json里的 9 条记录按原始生产序列合并成一个EvalCase(sandbox-probe-run)。抓取到的条目以verbatim字段保存逐字 prompt,这样连续性变体能看到与生产完全相同的运行形态;productionLabel记录当时真正下发的标题,作为人工对照。例如抓取到 3 条真实样例:"Confirmed Python 3.14.4 is installed""Wrote test file to /mnt/data, confirmed persistence""Confirmed /mnt/data persists between tool calls"
正是这些真实负载暴露了生产缺陷:第 2/3 条、第 7/8 条连续批次产出的标题在信息层面互相冗余(详见 Findings 第 4 点),而它们的措辞却并不相同——因此任何基于词法重复的自动检查都会放过它们。
-
合成的一半:针对真实负载暴露出的失败模式,以及它从未覆盖的模式构造的用例。每个合成用例都带
notes注明训练目标,例如all-failed(全部调用失败时必须以动词开头进入失败语域)、fib-rapid(三条几乎相同的连续批次做冗余压力测试)、overflow-entries(14 次调用、超出 12 条 cap、验证 “…and N more” 后缀)、error-shaped-success(工具以 success 状态返回 error 形状的 JSON,标题不得误读为成功)、mcp-long-name(带命名空间的长工具名对模型构成“回声诱惑”)、silent-success(空输出时无从总结)等。
指令变体:一张可复现的实验矩阵
variants.mts 中实际注册了 9 个变体:baseline(= 构建产物中的当前指令)、legacy(P1 重构前的旧顺序,保留用于回归扫描)、verbs、ordered、continuity、examples、composed(把已验证有效的改动组合起来)、shipping-full(无界历史窗口,验证“完整故事”是否胜过近期窗口)与 shipping。每个变体都通过 usePreviousLabels 声明是否向 prompt 注入历史标题,但所有变体都会被同一条冗余指标测量,这样“盲目变体”与“连续性变体”可以公平对比。
内部用一张 S 句子表(variants.mts)把角色、语域、结果导向、禁令、格式、好/坏示例等句子拆成原子块,再用数组顺序组装成不同变体——这保证了变体之间只有一个可声明的差异。
运行一次全量评测:命令行实操
README 给出了基架的核心用法,以下代码块完整保留,并补充每个参数的语义:
node scripts/activity-labels/run.mts # every variant, 1 sample
node scripts/activity-labels/run.mts --samples 3 # 3 samples each
node scripts/activity-labels/run.mts --variants baseline,shipping --cases fib-rapid
node scripts/activity-labels/run.mts --dry --cases mega-batch # render prompts, no API calls
node scripts/activity-labels/rescore.mts # re-grade stored results, no re-spend
npx tsc -p scripts/activity-labels/tsconfig.json # type-check the harness
CLI 参数解析逻辑位于 run.mts,完整参数集如下:
| 参数 | 默认值 | 说明 |
|---|---|---|
--samples N |
1 |
每个(变体 × 用例)的独立采样次数;受模型随机性影响,3 可降低单次运气成分 |
--concurrency N |
6 |
并发请求数;由 worker 池实现(见 run.mts),生产序列内的步骤仍串行执行以保证标签前向链式传递 |
--model id |
claude-haiku-4-5 |
标签生成模型,与生产“快速模型”定位一致 |
--variants a,b |
全部 | 过滤要跑的变体名 |
--cases a,b |
全部 | 过滤要跑的用例名 |
--dry |
false |
只渲染 prompt、不发起任何 API 调用,并把前 3 个渲染出的 prompt 打到终端供人工检查 |
几个必须注意的运行前提:
- API Key:需要
ANTHROPIC_API_KEY(环境变量或仓库根目录.env)。加载逻辑在 run.mts:优先取环境变量,其次解析根目录.env中ANTHROPIC_API_KEY=开头的那一行,并剥离引号;找不到就报错并提示内联传入方式ANTHROPIC_API_KEY=sk-… node scripts/activity-labels/run.mts。 - 构建产物:
baseline变体要求packages/api已构建(否则用LABEL_EVAL_DIST指认一个现成的dist),构建命令见前文。 - 成本与耗时:README 标注——全量扫描大约每变体 $0.03、整体 约 45s(以默认
claude-haiku-4-5、concurrency 6估算)。作为对照,一次请求固定max_tokens: 256,与抓取到的生产请求一致。
请求层面,run.mts 直连 https://api.anthropic.com/v1/messages,把指令变体放进 system、SDK 移植版 prompt 放进 user,完整复刻生产 wire shape;对 HTTP 429/500/529 最多重试 3 次,优先尊重 retry-after 响应头。
运行结束后,结果落在 results/(该目录已被 gitignore)下:
- 一个时间戳命名的
*.json,包含{ args, records },records 为StoredResults形态(见 types.mts),可随时交给rescore.mts离线重评分——不需要再花一次 API 钱; - 一份固定的
results/latest.md,包含聚合总览 + 逐用例、逐步的评分表格(每个成功记录携带label、生产对照production、全部flags、wordCount、firstWord、延迟与 token 用量,见 types.mts)。
为什么逐用例表格比聚合数字更重要
README 用整整一节强调了一个反直觉的经验:机械检查负责抓格式违规和词法重复,但生产上真实发生的失败——标题在信息层面冗余、却词法多样——得分低于重叠度阈值。换句话说,真正该修的 bug 恰好是聚合指标“看不到”的那类。
因此这套基架把评分分工设计成两层(checks.mts 注释原话):
- 机械层:
checkLabel对每条生成标签输出一串flags,全部类型定义在 types.mts:len(4–9 词约束违反)、punct(末尾标点)、quote(首尾引号)、md(markdown 语法泄漏)、opener(以 “ran/used/executed/called/invoked/performed” 这类通用动词开头)、tool-echo(复述工具名)、count-echo(复述 “N tools/commands/calls”)。 - 人工层:
results/latest.md需要逐行用眼睛读——它才是“召回工具(recall instrument)”,负责捕捉机械层漏掉的信息冗余;聚合指标只是“回归守卫(regression guard)”,防止后续改动把已修好的问题又带回来。
重叠检测是这套机械层的精髓(checks.mts):先把标签做小写化、去停用词、去词缀词干化(粗糙后缀 stemmer 让 persists/persistence/persisted 碰撞,足够做重叠检测即可),再对同一条链上此前的所有标签计算 Jaccard 重叠度,阈值 DUP_THRESHOLD = 0.5。超过阈值后进一步拆分差异词元:
- 若差异词元不含负载信息(数字、版本、路径、文件名,
isPayload判定),标记为restate——这条标题相对前一条新增了零信息,正是生产第 2/3、7/8 批次的失败形态; - 若差异词元包含数字/路径等负载,标记为
template——同一句式框架但携带了新信息(如fib(1)→fib(2)的逐次演进),这在工程上往往是可接受的,甚至比同义词换词更优。
aggregate()(report.mts)则把每条记录折叠成变体级统计:错误数、各 flag 计数、去重起始动词数(distinctOpeners)、最高频起始动词、平均词数、平均延迟、输入/输出 token 与以美元计的成本(见 types.mts)。“起始动词多样性”正是 Findings 里语域塌缩的量化指标。
它产出的五项实测结论及其生产影响
这是整套基架最有说服力的部分——每一个结论都是“被测量出来的”,并反过来改写了生产提示词。README 的 Findings 如下,结合源码逐条展开:
1. 句子顺序是承重结构(sentence order is load-bearing)
把格式约束挪到内容规则之后,可测量地降低了长度违规和语域塌缩。这直接决定了生产指令 ACTIVITY_INSTRUCTION 的排列方式——runtime.ts 里那条指令的句子顺序是刻意为之:
Sentence ORDER is deliberate, not stylistic: content rules first and format rules last measurably improves both format adherence and opening-verb diversity on small label models (eval corpus: scripts/activity-labels/)…
对照 variants.mts 中 LEGACY_ORDER(旧顺序:role → register → outcome → prohibitions → format → good → bad…,格式约束被埋在中间)与 SHIPPED_ORDER(新顺序:role → outcome → register → good/bad → failure → continuity → prohibitions → format → output,把格式约束押到最后)即可看出差异。README 警告:任何“为了排版整洁”的重排都会让真实输出回退。
2. 枚举可用动词会适得其反(enumerating verbs backfires)
verbs 变体的假设是:指令里显式列出可接受的起始动词能引导多样语域。实测结果正好相反——列动词反而把模型锚定了:Confirmed 从 18 次涨到 23 次,起始动词多样性减半。给出的替代方案(VERB_CHOICE)改为用“由结果决定动词、不要每次都用一个词”来描述,而非枚举。这是一个非常典型的“指令里的例子被当成模板复制”现象。
3. 仅靠多样化示例毫无帮助
examples 变体只把 Good 示例换成动词多样的版本(Traced the leak to…、Ruled out DNS…、Measured cold start at 412ms),期望能“用示例播种”语域多样性。实测结论是完全没变化——这证明语域塌缩是任务形状决定的(task-shaped),不是示例播种能解决的;这也解释了为什么最终修复走的是连续性上下文而非示例路线。
4. 连续性上下文修复了真正的缺陷
这是全部结论里最有价值的一条。做法是:把本轮已经提交(committed) 的标题回喂进下一批的 prompt,作为 “Previous headers in this run (most recent last)” 段落。实测效果:
- 消除了 restatement(跨批次重复陈述同一活动);
- 意外地阻止了“设置类批次”被错误地标成它们的工具尚未确立的结论——对照生产负载看,第 2 步写文件、第 3 步验证持久化,连续上下文让第 3 步的标题只陈述“新增”的信息,而不是把写文件这步的结论再讲一遍。
源码层面,这一机制的全部细节都在 runtime.ts 的 createActivityLabelHook 中:已提交标题按内容索引存入 Map,因为填充是按完成顺序落地的(批次 N+1 的标签可能先于批次 N 提交),连续性读取必须按索引重排序,不能信任插入序;recentLabels() 只取当前槽位之前、按索引升序的最后 MAX_PREVIOUS_LABELS 条(默认 3)。buildPrompt 则负责把该段落拼进 prompt(runtime.ts),并附上强标注 What it called, and what came back (do not restate these):——没有这个标注,模型容易把工具列表当作“要总结的对象”而整段转写。
值得注意的工程细节:填充被宿主丢弃(响应已定稿)的标签不会写入连续性记录——只有用户真正读到的文本才有资格成为后续标题的上下文(runtime.ts)。
5. 3 条标签的回看窗口已经足够
shipping-full 变体把窗口设为无界(previousLabelCap: Infinity),验证“整轮故事”是否胜过“近期窗口”。实测:无界历史并没有更好——restatement 本质上是近因问题,而 prompt 增长却是线性的(到第 9 批约 +82 输入 token,外推到 activityMaxPerRun 默认值 20 时约 +250 token)。窗口太小会失效、无界又纯增成本,3 条是这个权衡下的最优解;runtime.ts 注释解释了取舍:窗口小是因为目的只是“避免复述新标题附近已显示的内容”,20 条历史只会稀释批次内容。
可迁移的方法论:如何用这套模式评测你自己的 prompt
这套基架的价值不局限于 activity-label,值得提炼的通用配方有四条:
- 保留真实负载作为“黄金语料”。
captured.json是“从 Langfuse 逐字节抓取、随 trace 过期而不可再生”的一手数据——任何测评都应尽早建立这样的真实样本库,并把它重放成与生产相同的序列形态,而不是仅靠合成用例。 - 为生产从未命中的分支路径补合成用例。corpus.mts 的合成用例清单本身就是一份 LLM 文本生成功能的失败模式检查表:全失败 / 部分失败、并行批次、快速连续重复、条目数量溢出、截断输出、空成功、错误形状成功、超长命名空间工具名……每个模式都对应一类可预见的输出退化。
- 多步骤链式测量跨批次状态。把第 N 步的生成物作为第 N+1 步的上下文(
run.mts的chain),让“连续性类”假设在没有生产字段支持的情况下也能先被测量——这是 prompt 工程里“先测量、后立项”的极佳示范。 - 人工表格是召回工具,聚合指标是回归守卫。机械指标抓不到“词法多样但信息冗余”这类缺陷,必须在交付判断里保留结构化的人工复读环节(
latest.md逐用例表格),同时用聚合指标拦住未来的回归。
把评测基架接回生产链路
基架的存在意义最终要落在生产提示词上。目前仓库中这条链路是完整闭环的:
- 生产指令本体定义在 runtime.ts 的
ACTIVITY_INSTRUCTION,经 index.ts 统一导出,与评测基架读取的构建产物是同一份代码; - 评测基架模拟的 prompt 组装(
buildPrompt,含连续性段落与各截断预算ENTRIES_CHAR_BUDGET/LABEL_OUTPUT_CHAR_LIMIT)就来自该 runtime;而宿主侧真正提交给 SDK 的负载上下文由 wiring.ts 的captureActivityBlockContext采集(只取 reasoning 摘录与助手最近文本、绝不混入人类消息,且在遇到上一条 activity label 时停止收集 reasoning,避免把上一批的推理串进本批负载); - 该模块的单元测试(runtime.spec.ts、wiring.spec.ts)与宿主级测试(
__tests__/host.spec.ts)共同守护生产侧的这些行为,而 scripts/activity-labels 则守护提示词的文本质量——两套测试一里一外,恰好形成“结构正确 + 文本质优”的双保险。
从开发实践看,这给同类项目提供了一条清晰路径:任何需要 LLM 生成“展示型短文本”(标题、摘要、命名)的功能,都值得配一个这样的评测基架——用固定语料把不可控的模型输出变成可重复、可比较、可回归的测量对象,再让每一次失败的测量反哺生产提示词本身的句子级设计。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00