首页
/ LibreChat activity-label 评测基架解析:用固定语料与逐因素变体度量提示词文本质量

LibreChat activity-label 评测基架解析:用固定语料与逐因素变体度量提示词文本质量

2026-09-08 12:21:16作者:滑思眉Philip

本技术指南以 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.tscreateActivityLabelHook 中:这是一个 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 中逐条落实:

  1. 单一因素变更:每个变体只相对生产指令改动一个假设,这样“某个结果只归因于一个假设”。文件头注释明确列出四个初始假设——verbs(Good 示例中的动词分布会诱发语域塌缩,因为 9 条生产标题里有 6 条以 “Confirmed” 开头)、ordered(4–9 词的长度约束放在段落中间会被挤掉,把格式约束挪到最后可提升遵从率)、continuity(把本轮此前的标题回喂给模型可消除跨批次冗余)、baseline(照搬当前分支实际下发的指令)。
  2. 基线从“构建产物”读取baseline 不是手工复制粘贴的指令文本,而是通过 createRequire已构建的 packages/api/dist 中读取 ACTIVITY_INSTRUCTIONvariants.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——这使你可以“在一个尚未构建的工作树里给另一个分支的指令打分”。
  3. 漂移自检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 条记录按原始生产序列合并成一个 EvalCasesandbox-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 重构前的旧顺序,保留用于回归扫描)、verbsorderedcontinuityexamplescomposed(把已验证有效的改动组合起来)、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:优先取环境变量,其次解析根目录 .envANTHROPIC_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-5concurrency 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、全部 flagswordCountfirstWord、延迟与 token 用量,见 types.mts)。

为什么逐用例表格比聚合数字更重要

README 用整整一节强调了一个反直觉的经验:机械检查负责抓格式违规和词法重复,但生产上真实发生的失败——标题在信息层面冗余、却词法多样——得分低于重叠度阈值。换句话说,真正该修的 bug 恰好是聚合指标“看不到”的那类。

因此这套基架把评分分工设计成两层(checks.mts 注释原话):

  • 机械层checkLabel 对每条生成标签输出一串 flags,全部类型定义在 types.mtslen(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.mtsLEGACY_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.tscreateActivityLabelHook 中:已提交标题按内容索引存入 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,值得提炼的通用配方有四条:

  1. 保留真实负载作为“黄金语料”captured.json 是“从 Langfuse 逐字节抓取、随 trace 过期而不可再生”的一手数据——任何测评都应尽早建立这样的真实样本库,并把它重放成与生产相同的序列形态,而不是仅靠合成用例。
  2. 为生产从未命中的分支路径补合成用例。corpus.mts 的合成用例清单本身就是一份 LLM 文本生成功能的失败模式检查表:全失败 / 部分失败、并行批次、快速连续重复、条目数量溢出、截断输出、空成功、错误形状成功、超长命名空间工具名……每个模式都对应一类可预见的输出退化。
  3. 多步骤链式测量跨批次状态。把第 N 步的生成物作为第 N+1 步的上下文(run.mtschain),让“连续性类”假设在没有生产字段支持的情况下也能先被测量——这是 prompt 工程里“先测量、后立项”的极佳示范。
  4. 人工表格是召回工具,聚合指标是回归守卫。机械指标抓不到“词法多样但信息冗余”这类缺陷,必须在交付判断里保留结构化的人工复读环节(latest.md 逐用例表格),同时用聚合指标拦住未来的回归。

把评测基架接回生产链路

基架的存在意义最终要落在生产提示词上。目前仓库中这条链路是完整闭环的:

  • 生产指令本体定义在 runtime.tsACTIVITY_INSTRUCTION,经 index.ts 统一导出,与评测基架读取的构建产物是同一份代码;
  • 评测基架模拟的 prompt 组装(buildPrompt,含连续性段落与各截断预算 ENTRIES_CHAR_BUDGET / LABEL_OUTPUT_CHAR_LIMIT)就来自该 runtime;而宿主侧真正提交给 SDK 的负载上下文由 wiring.tscaptureActivityBlockContext 采集(只取 reasoning 摘录与助手最近文本、绝不混入人类消息,且在遇到上一条 activity label 时停止收集 reasoning,避免把上一批的推理串进本批负载);
  • 该模块的单元测试(runtime.spec.tswiring.spec.ts)与宿主级测试(__tests__/host.spec.ts)共同守护生产侧的这些行为,而 scripts/activity-labels 则守护提示词的文本质量——两套测试一里一外,恰好形成“结构正确 + 文本质优”的双保险。

从开发实践看,这给同类项目提供了一条清晰路径:任何需要 LLM 生成“展示型短文本”(标题、摘要、命名)的功能,都值得配一个这样的评测基架——用固定语料把不可控的模型输出变成可重复、可比较、可回归的测量对象,再让每一次失败的测量反哺生产提示词本身的句子级设计。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
521
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
392