Skip to content

fix(eval): apply experiment config to the runtime and record requested/effective (#114) - #116

Merged
helsome merged 2 commits into
helsome:mainfrom
RXQ6:fix/114-effective-config
Sep 18, 2026
Merged

helsome merged 2 commits into
helsome:mainfrom
RXQ6:fix/114-effective-config

Conversation

@RXQ6

@RXQ6 RXQ6 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Refs #114

本 PR 只落 #114内部接线增量(配置真正下发 + requested/effective 分离)。真实双模型 A/B 验证需要在具备模型凭证的环境执行,边界留在 #114,本 PR 不关闭它。

问题回顾

评测记录写了某个 model/provider,不代表运行时真的用了它。run.ts 只把 CLI 参数放进 ExperimentConfigExperimentService 从不把它们应用到 Pi/LLM runtime,也没有任何生效值校验 —— "标签是模型 A、实际跑默认模型" 的无效横向比较就这样静默产生。

改动

1. 配置真正应用到运行时(每个 case 启动前)

ExperimentService.runCasestartRun 之前调用新的 applyRequestedConfig

  • model + provider 齐备 → 经 ExperimentLlmControl.setModel() 应用,随后 getState() 回读校验必须完全一致;应用失败或回读不一致 → 该 run 以 CONFIG_APPLY_FAILED 记为 failed(绝不静默回退到 runtime 残留的模型上执行)。
  • 仅请求部分维度 / 未请求模型 → 回读 runtime 当前状态,把"实际会执行什么"如实记下来。
  • thinkingLevelsetThinkingLevel() 应用 + 回读校验,语义同上。
  • 无控制面的维度(strategyId;fixture/local 模式下的一切)→ 显式记为 unapplied 并给出原因,绝不冒充已生效。

2. requested / effective 分离

  • packages/core:新增 EffectiveRuntimeConfig(readback 确认的 model/provider/thinking + unapplied 列表 + confirmedAt)与 UnappliedConfigItemEvaluationRun 增加可选 effectiveConfig。旧记录缺失该字段 = 历史未知,不反填推测值。
  • trace 元数据只记录 readback 确认值model / provider 仅在有回读依据时写入;无回读依据(控制面不存在、或该维度被记为 unapplied)时保持 unknown,绝不回退成 requested 标签。请求值单独命名记录为 requestedModel / requestedProvider(只进 metadata,不生成 model:/provider: tag,也不进 generation span 的 model 字段)。
  • CLI 边界归一化:--model provider/model-id 拆成 provider + 纯 modelId 后再进入 ExperimentConfig(此前只推断 provider、前缀仍留在 model 里,导致 setModel('prov', 'prov/model-id'))。只按第一个 / 切分,openrouter/anthropic/claude-sonnet-4-5 保留 anthropic/claude-sonnet-4-5 作为 modelId;显式 --provider 优先。该段抽出为 normalizeModelSelection()packages/shared/src/evaluation/model-selection.ts),fix(eval): separate live execution validity from quality and propagat… #122 可直接复用同一段,不必各自维护一份。
  • eval CLI 新增:每 run 打印 requested vs effective 摘要行(含 unapplied 原因与 CONFIG_APPLY_FAILED),并新增 --thinking 接线到 config.thinkingLevel
  • ExperimentKernel.getLlmApi 放宽为 ExperimentLlmControlLlmRuntimeApi 的结构子集,setModel/setThinkingLevel 可选),对现有实现向后兼容。

3. 隔离

每个 case 都重新应用当前实验的请求配置并回读 —— 连续两个实验无法互相串用配置(有专门测试覆盖)。

验证

按 issue 的验证标准逐条对照(runtime fake 全部为可观测调用序列 + 状态可注入的 FakeKernel):

  • 配置在 startRun 前应用 — 测试断言 setModel 调用序在 startRun 之前
  • 回读不匹配阻止运行 — mismatchSetModel 注入,run 记为 failed 且 startedRuns === 0
  • provider/model 不支持明确失败 — failSetModel 注入,错误信息含所请求的模型标识
  • 连续两个实验不串配置 — setModel 调用序列断言为 [provA/m1, provA/m2],两个 run 的 effectiveConfig 各自正确
  • CLI 输入 → 归一化后的真实参数 → runtime 控制面 → run metadata 全链路有测试:断言控制面收到 setModel:provA/m1(不是 provA/provA/m1),且 run 的 effectiveConfig 与实验 metadata.providerConfiguration 都是 { provider: 'provA', model: 'm1' }
  • 归一化自身的边界用例:多段 / 只切第一个、显式 --provider 优先、无 /provA/ 不改写
  • 凭证不入报告;无回读依据的参数不冒充 effective(fixture 模式下 requested model 记为 unapplied,effectiveConfig.model 保持 undefined)
  • trace 元数据不冒充:无控制面时 trace 里 requestedModel/requestedProvider 有值、model/provider 键缺失,且不生成带 model 的 generation span;有回读时 model/provider 才写入
  • 旧记录可读(effectiveConfig 为可选字段,测试中旧形态 run 正常持久化/读取)

验收材料(环境与命令)

环境:Bun 1.4.2 · Windows 11 (10.0.26200, x64)。本机 node_modules 因符号链接权限问题未完整安装,bun run typechecktsc)无法本地执行,类型检查以 CI 为准。

# 1) 本次改动所在测试文件
bun test packages/shared/src/evaluation/experiment-service.test.ts
→ Ran 24 tests across 1 file: 24 pass / 0 fail / 0 skip(新增 2 个用例:CLI 归一化链路、trace 只记确认值)

# 2) 相邻评测测试(不扩大范围)
bun test --isolate packages/shared/src/evaluation
→ Ran 163 tests across 9 files: 162 pass / 1 fail / 0 skip
  唯一失败:langfuse backend > does not throw when Langfuse is down —— 本地既有失败(与本改动无关)

关于那一条失败:把本 PR 触及的 langfuse/metadata.ts 改动临时还原后,同一条用例仍以完全相同的方式失败(backend.lastErrorDetails 为 undefined,即本机沙箱下 mock 的 500 响应没有走到错误分支),因此判定为本地环境问题而非本 PR 引入;该用例在 CI 为绿色。

bun scripts/eval/run.ts --max-cases 1 --mode fixture 端到端通过;再加一个归一化可见证据:

bun scripts/eval/run.ts --max-cases 1 --mode fixture --model deepseek/deepseek-chat
→ --- Config (requested vs effective) ---
  requested: model=deepseek-chat provider=deepseek thinking=— strategy=—
  <runId>: effective=— · model not applied (runtime exposes no LLM control surface (local/fixture mode)); provider not applied (…)

即 CLI 传入的 deepseek/deepseek-chat 已被拆成 provider=deepseek + modelId=deepseek-chat;修复前这里会显示 model=deepseek/deepseek-chat,并把这串前缀原样交给 setModel()

  • 同一 case 的两次真实模型运行(A/B):本环境无模型 API 凭证 —— 这是环境条件而非代码缺陷;[P1][Eval/Harness] 将实验配置真正应用到运行时,并记录 requested / effective 配置差异 #114 明确要求的真实双模型验证可由具备授权环境的维护者协助执行。需要的是两个 --model <provider/model-a> / --model <provider/model-b> 的真实 live run,从 run 记录里读 requested/effectiveConfig 与 run id(CLI 会直接打印这一对照)。因此本 PR 标记为 Refs #114,不 Closes

#122 的关系

#122 也包含 --model provider/id 归一化。本 PR 把它抽成 normalizeModelSelection()packages/shared/src/evaluation/model-selection.ts,语义与原实现一致:只切第一个 /,显式 provider 优先),#122 可直接改为调用该函数,避免两套独立实现。effective-config 契约(EffectiveRuntimeConfig / requested-vs-effective 命名)以本 PR 为主,不要求 #122 再设计一套模型选择。

Scope 边界

不重写 AgentKernel、不新建 SDK/Manifest 系统;复用现有 LlmRuntimeApi 控制面与 #21 方向的快照要素(effective config + dataset/version + gitSha 均已落在实验/run 记录里)。与 #113 分工不变:#113 检查本轮是否有效执行,本 PR 检查有效执行时到底用了哪套配置。

…d/effective (helsome#114)

Requested model/provider/thinking was stored in ExperimentConfig and echoed
back in metadata without ever reaching the runtime, so a run labeled
model A could silently execute the runtime's previous/default model.

- ExperimentService now applies the requested model/provider (and optional
  thinking level) via the runtime LLM control surface BEFORE each case run,
  then confirms by readback; application failure or readback mismatch fails
  the run with CONFIG_APPLY_FAILED instead of executing mislabeled.
- New EffectiveRuntimeConfig on EvaluationRun records the readback-confirmed
  model/provider/thinking per run; dimensions with no control surface
  (strategyId, everything in fixture mode) are recorded as unapplied with a
  reason. Historical runs simply omit the field (never backfilled).
- ExperimentKernel.getLlmApi widened to ExperimentLlmControl (structural
  subset of LlmRuntimeApi, optional setModel/setThinkingLevel).
- eval CLI: --thinking flag wired to config.thinkingLevel; new
  requested-vs-effective summary printed per run.
- Fake-kernel tests cover: apply-before-startRun ordering, readback mismatch
  blocking, unsupported model failing explicitly, no config leakage between
  consecutive experiments, fixture-mode unapplied marking, readback-only
  effective recording, thinking apply + mismatch.
@RXQ6

RXQ6 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

PR #116 已提,CI 状态正常:Typecheck / Focused tests / Full unit tests (advisory) / Secret scan 4 项全过,剩下的 "Full unit tests" 是 PR 上按 pr.yml 设计跳过的(只在 push main 时跑),不是失败。
改动是把实验配置真正下发到 runtime + 回读校验,并给每个 run 记录 requested/effective 差异;issue 里要求的真实双模型 A/B 因本机无 API 凭证未跑,PR 正文已标注。有空帮忙看一下。

@helsome helsome left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

实现方向正确,CI success 也已核对。当前有两个很具体的接线问题,请局部修正,不用扩成新框架:

  1. CLI 仍只从 --model provider/model-id 推断 provider,没有把 config.model 去掉 provider 前缀;新增 applyRequestedConfig 会实际调用 setModel('provider', 'provider/model-id')。请在 CLI 边界拆出 provider + 纯 modelId(保留 modelId 本身后续的 /),并覆盖 CLI 输入→真实参数归一化→runtime fake→run metadata 的测试。目前新增用例直接传 config={provider:'provA',model:'m1'},没有覆盖这条公开命令。
  2. trace 元数据仍使用 effectiveConfig?.model ?? config.model(provider 同理);当控制面不存在/维度 unapplied 时,又会把 requested 写回实际模型标签。请把 requested 单独命名记录,没有回读依据的 actual/effective 保持 unknown,不要退回 requested 冒充。

验收材料请补 Bun 版本、OS、相邻80条测试的准确命令及 pass/fail/skip,已有聚焦范围不用扩大。缺 Key 是环境条件,不是代码缺陷;#114 明确要求的真实双模型验证可以由具备授权环境的维护者协助。若先落内部接线增量,改成 Refs #114,并把尚未验证的真实模型边界留在父任务;不要在未完成 A/B 时 Closes #114

#122 也含 CLI 模型前缀归一化,请双方协调复用这一小段处理;effective-config 契约以本 PR 为主,不要求两套独立模型选择设计。

…th requested values (helsome#114)

Addresses the two wiring defects from review.

1. `--model provider/model-id` only inferred the provider; the prefixed id
   still travelled as the model, so applyRequestedConfig called
   setModel('provider', 'provider/model-id') and the run metadata carried a
   model id no provider serves. The split now happens once, at the CLI
   boundary: normalizeModelSelection() returns provider + bare model id
   (only the first segment is the provider, so `openrouter/anthropic/x`
   keeps `anthropic/x` as the id). The helper lives in
   packages/shared/src/evaluation/model-selection.ts so helsome#122 can reuse the
   same snippet instead of growing a second normalization.

2. Trace metadata no longer falls back to the requested model/provider. The
   readback-confirmed values are passed only when they exist; a dimension
   with no runtime readback stays unknown instead of being filled in with
   the requested label. The request is recorded under its own keys
   (requestedModel / requestedProvider) in the Langfuse metadata record —
   deliberately not as `model:` / `provider:` tags, and not as the snapshot's
   model, so no generation span can claim a model that never ran.

Tests: normalizeModelSelection cases (shorthand, multi-slash id, explicit
--provider, trailing separator) plus the end-to-end chain CLI input →
normalized params → fake runtime control call → run metadata, and a trace
assertion that an unapplied request stays `requestedModel` while the
confirmed readback lands under `model`.
@RXQ6

RXQ6 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

两个接线问题已按您的方向做了局部修正(没有扩框架),新提交 d1c9830 已在 PR 上。

1) CLI 边界拆出 provider + 纯 modelId

原实现只用 --model a/b 推断 provider,前缀仍留在 config.model 里,applyRequestedConfig 实际会调 setModel('a', 'a/b')。现在归一化发生在 CLI 边界、进入 ExperimentConfig 之前,并抽成独立函数 normalizeModelSelection()packages/shared/src/evaluation/model-selection.ts):

  • 只按第一个 / 切分,openrouter/anthropic/claude-sonnet-4-5 → provider openrouter + modelId anthropic/claude-sonnet-4-5(保留后续 /);
  • 显式 --provider 优先;
  • /、以及 provA/ 这种「切完 modelId 为空」的写法不改写,交给 runtime 明确报错而不是静默丢维度。

#122 里的同一段可以和这里合并成一份:它的语义与本实现一致(当时是 inline 版本),改成调用这个函数即可,不用维护两套。effective-config 契约仍以本 PR 为主。

2) trace 不再把 requested 当成 actual

  • model / provider 只在有 readback 依据时写入;无控制面或该维度 unapplied 时保持 unknown,删掉了 effectiveConfig?.model ?? config.model 这个回退;
  • requested 值单独命名记录为 requestedModel / requestedProvider:只进 trace metadata,生成 model:/provider: tag,也进 generation span 的 model 字段,所以不会出现「标签是 A、实际没跑 A」的观感。

测试

新增 2 个用例,覆盖您点出的这条链路:

  • CLI 输入 → 归一化 → runtime fake → run metadata:断言控制面收到 setModel:provA/m1(不是 provA/provA/m1),且 run 的 effectiveConfig 与实验 metadata.providerConfiguration 都是 { provider: 'provA', model: 'm1' };同时覆盖多段 /、显式 --provider 优先、无 /provA/ 不改写;
  • trace 断言:无控制面时 trace 里 requestedModel/requestedProvider 有值、model/provider 键缺失且无 generation span;有回读时 model/provider 才写入。

验收材料

环境:Bun 1.4.2 · Windows 11 (10.0.26200, x64)。说明一点:本机 node_modules 因 Windows 符号链接权限问题没有完整安装,bun run typecheck(需要 tsc)本地跑不了,类型检查只能以 CI 为准。

bun test packages/shared/src/evaluation/experiment-service.test.ts
→ Ran 24 tests across 1 file: 24 pass / 0 fail / 0 skip

bun test --isolate packages/shared/src/evaluation
→ Ran 163 tests across 9 files: 162 pass / 1 fail / 0 skip

那 1 条失败是 langfuse backend > does not throw when Langfuse is downbackend.lastErrorDetails 为 undefined),是本地既有失败:我把本 PR 对 langfuse/metadata.ts 的改动临时还原后,同一条用例以完全相同的方式失败,所以判为本地环境问题(mock 的 500 响应没有走到错误分支),CI 上是绿的。

CLI 端到端也补了一条可看的证据:

bun scripts/eval/run.ts --max-cases 1 --mode fixture --model deepseek/deepseek-chat
→ requested: model=deepseek-chat provider=deepseek thinking=— strategy=—

修复前这里会打印 model=deepseek/deepseek-chat

关于 #114 的收口

已把 PR 描述里的 Closes #114 改成 Refs #114:本 PR 只落内部接线增量,真实双模型 A/B 的边界留在 #114。本环境没有模型 API 凭证——这是环境条件、不是代码缺陷,需要维护者用具备授权的环境协助跑一次两个 --model <provider/model-x> 的真实 live run;CLI 会直接打印 requestedeffective 的对照,便于核对。

@RXQ6

RXQ6 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

补一条:d1c9830 上的 CI 已跑完,本机跑不了的那项由 CI 覆盖了。

Typecheck                         pass  (58s)   ← 本机缺 node_modules,typecheck 靠这项
Focused tests                     pass  (19s)
Full unit tests (advisory)        pass  (20s)
Secret scan                       pass  (4s)
Full unit tests                   skipping

其中 Full unit tests (advisory) 覆盖了我在验收材料里标出的那条本地失败用例(langfuse backend > does not throw when Langfuse is down),CI 上是通过的 —— 与「本地环境问题」的判断一致。

@helsome
helsome dismissed their stale review September 18, 2026 11:11

原 blocker 已在 head d1c9830 处理:CLI 模型选择已统一归一化,trace requested/effective 也已分离;旧 REQUEST_CHANGES 失效。

@helsome helsome left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前 head 的两个接线 blocker 已处理:normalizeModelSelection() 在 CLI 边界只切第一个 /,显式 provider 优先,并由 CLI→runtime fake→run metadata 测试覆盖;trace 的 model/provider 只写 readback-confirmed 值,请求值单独记录为 requestedModel/requestedProvider,不再冒充 actual。PR 已收窄为 Refs #114,把真实双模型 A/B 留在父 Issue,缺 API key 不作为代码 blocker。验证材料已补 Bun 1.4.2 / Windows、24/24 focused、相邻评测 162 pass / 1 本地既有失败,并且当前 PR CI 的 Typecheck / Focused / advisory full unit / Secret scan 均通过。代码可以批准;当前 mergeable=false,只需基于最新 main 解决冲突并重跑相关基础检查,不要求扩大验收范围。

@helsome
helsome merged commit ff3ed3d into helsome:main Sep 18, 2026
5 checks passed

helsome commented Sep 18, 2026

Copy link
Copy Markdown
Owner

合并后 main 的精确 post-merge 检查发现一个小型集成回归,我已停止继续合并其他 PR。结果 SHA:ff3ed3d031568a892f115c6e1cb70899d4e593c2;workflow 35340976577。Full unit tests 与 Secret scan 通过,但 Typecheck job 105586559446 失败。

准确错误:packages/shared/src/evaluation/experiment-service.ts(534,7) TS2741runCase 当前返回类型要求 { ..., aborted: boolean; runtimeUnusable: boolean }(来自已合入的 #117 超时隔离语义),而本 PR 新增的 CONFIG_APPLY_FAILED 早退分支仍返回 { run: failedRun, aborted: false },缺少 runtimeUnusable

这是 #116 与当前 main 的 #117 组合后出现的直接类型回归,不是 API key/环境问题。最小 follow-up 即让该配置应用失败分支遵守现有 return contract(预期 runtimeUnusable: false),并重跑 bun run typecheck + 对应 evaluation focused tests。无需扩大为评测架构改造或真实模型 E2E;父 #114 的真实 A/B 仍按原 scope 保持开放。

suzhiguo7 added a commit to suzhiguo7/folio that referenced this pull request Sep 18, 2026
…elsome#113)

Review follow-up: execution validity is derived from execution facts and
explicit error codes, not from the terminal status alone.

Changes:
- aggregate.ts: isInfrastructureRun stays true only for not-started runs
  (spawn/config/credential rejection) or an explicit runtime/process
  failure; PI_REQUEST_TIMEOUT / PI_RUNTIME_ERROR and other in-run errors
  remain quality outcomes. New isQualityRun keeps started tool_loop /
  timeout / budget-exhausted runs in the quality denominator; summary and
  execution counts share the same classification.
- scripts/eval/run.ts: printSummary reuses isQualityRun/isInfrastructureRun
  (single source of truth) and the CLI provider/model split delegates to
  normalizeModelSelection, shared with helsome#116 — no second config contract.
- model-selection.ts: provider/model shorthand split extracted as a shared
  helper (only the first `/` segment is the provider).
- core/evaluation.ts: document evaluated / infraFailed semantics.

Tests (reviewer-requested contrasts):
- not-started config failure (missing key) is excluded from quality
  aggregates;
- a run that started and then hit tool_loop / wall-clock timeout stays a
  valid negative quality result and counts as a failure.

Verification:
- bun test --isolate packages/shared/src/evaluation/aggregate.test.ts
  packages/shared/src/evaluation/experiment-service.test.ts scripts/eval
  -> 63 pass / 0 fail
- bun run typecheck -> all packages exit 0

UI impact: no layout or interaction changes.
suzhiguo7 added a commit to suzhiguo7/folio that referenced this pull request Sep 18, 2026
Reconcile the review follow-up with the CLI/effective-config work that
landed on main (helsome#114/helsome#116, helsome#115/helsome#117).

Conflict resolutions:
- core/evaluation.ts: keep both `execution` (helsome#113 validity) and
  `effectiveConfig` (helsome#114 readback).
- experiment-service.ts: keep the helsome#113 evaluation gating, the helsome#116
  effective-config readback, and the helsome#117 bounded teardown; the helsome#114
  config-apply failure path now stamps `execution: 'not-started'` and
  returns `runtimeUnusable`, so a config failure stays infra-invalid
  instead of being measured as a zero score.
- experiment-service.test.ts: keep both the relay-header test and the
  helsome#115 teardown suite; update the helsome#114 CONFIG_APPLY_FAILED expectation to
  the helsome#113 semantics (validity `invalid`, passRate null).
- scripts/eval/run.ts: keep the live preflight (helsome#113) and the
  effective-config summary (helsome#114); a single normalizeModelSelection
  import, with thinkingLevel from the CLI.

Verification:
- bun test --isolate -> 1539 pass / 8 skip / 0 fail
- bun run typecheck -> all packages exit 0

UI impact: no layout or interaction changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants