ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kiln 评估架构重构:将 Eval Trace 与 Eval Score 彻底分离的实战指南

Kiln 评估架构重构:将 Eval Trace 与 Eval Score 彻底分离的实战指南
  • 人工智能
  • AI 应用
  • AI 评测
  • RAG
  • 微调
  • 提示工程
  • 开发工具
  • 桌面应用

【免费下载链接】Kiln

Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.

项目地址:https://gitcode.com/gh_mirrors/ki/Kiln
点击查看免费下载

导读

在 Kiln 的 V2 评估体系中,EvalRun长期同时承载「模型生成痕迹(trace)」与「评判分数(scores)」两类数据,导致一个严重的实践问题:只要给已有评估新增一个评判器(judge),整个评估就会因去重键包含eval_config.id而从零开始重新生成全部输出——而 V2 的 8 种评估类型中有 7 种(exact_match、pattern_match、contains、set_check、tool_call_check、step_count_check、code_eval)都是本地计算、评分近乎免费,只有llm_judge消耗真金白银。本文基于 功能规格书(配套阅读 架构文档 与 研究文档),完整讲解 Kiln 如何把 trace 与 score 拆分为两种独立记录,实现「加评判器只重新评分、不重新生成」「生成结果先落盘、评分失败不毁迹」「评判器之间可做配对比较」三大目标。读完你将掌握这一重构的数据模型、复用键设计、运行器改造、读路径兼容与迁移方案的全部细节。

1. 问题与目标:为什么必须拆分

1.1 缺陷的本质

EvalRun当前以EvalConfig为父节点存储,一份记录里同时装了两类语义完全不同的数据:

  • trace 侧:input、output、task_run_trace、task_run_usage、dataset_id/eval_input_id、task_run_config_id;
  • score 侧:scores、intermediate_outputs(评判思维过程)、skipped_reason/skipped_detail。

研究文档以一张证据表确认了这个缺陷的真实性(research.md §1):EvalConfig(..., parent_of={"runs": EvalRun})把 trace 和 score 绑在同一记录上;"already run" 去重键包含eval_config.id(对应libs/core/kiln_ai/adapters/eval/eval_runner.py中的collect_tasks逻辑),新 EvalConfig 从空集合起步,导致每个任务都重新执行evaluator.run_task(...)。

从数学上看,trace 是 (input, task_run_config) 的函数,score 是 (trace, eval_config) 的函数。把两者塞进一个以eval_config为键的记录里,等于让昂贵的生成环节被廉价的评分环节绑架。

1.2 三个目标

  1. 复用而非重生成:给已有评估新增评判器时,复用已有 trace,只重新评分。
  2. trace 先于评分持久化:生成一旦完成立即落盘,后续评分失败绝不销毁它。
  3. 评判器配对比较:两个评判器对同一份生成结果打分,差异可归因于评判器本身,而非生成噪声——这正是"我的评判器好不好?"这一核心工作流的统计基础。

1.3 非目标

不改变任何评估度量本身:给定(item, run config, eval config)所产生的分数保持不变。

2. 总体形态:trace 就是 TaskRun,EvalRun 只做纯分数记录

这是整个重构最关键的设计决策(决策 D12/D15):不新建任何 trace 实体。Eval trace 直接复用TaskRun;EvalRun则从"trace + score 混合体"收窄为纯粹的分数记录。

Task ├── runs/ TaskRun —— 数据集语料 AND 评估生成的 trace ├── eval_inputs/ EvalInput(不变) └── evals/ └── {eval}/ └── configs/ └── {config}/ └── runs/ EvalRun —— 现在纯粹是分数记录

TaskRun已经携带一个 trace 所需的全部要素:trace、output,以及 run config(既以output.source.run_config_id形式、又以冻结的output.source.run_config属性形式存在)。复用它还免费获得output.rating(人工评分)、feedback子记录和repaired_output。磁盘布局上,评估生成的 trace 直接落在Task.runs/目录中(见 task.py 中的 runs 访问器说明)。

3. 复用键:不靠哈希,靠三条不变式

一个 trace 由(source_type, source_id, task_run_config_id)唯一标识(决策 D9)——没有哈希,没有指纹。这不是新发明,而是数据模型早已宣称的不变式的兑现:TaskRunConfig的语义是"用相同的 RunConfig 跑相同的输入,应对模型发起完全相同的调用"。

其中source_type必不可少:id 只在各自 store 内唯一,TaskRun和EvalInput的 id 可能碰撞(这正是eval_splits.ItemKey存在的原因)。两个来源类型含义如下:

source_typesource_id指向适用场景
"eval_input"EvalInput.idV2 EvalInput 支撑的 split
"task_run"TaskRun.id(数据集条目本身)V1 TaskRun 支撑的 split

因此,一个 legacy 来源的评估 trace,就是eval_source指向另一个TaskRun 的 TaskRun:生成的 trace 回指它生成所依据的数据集条目,两者始终是不同的记录。这与 splits 分支的ItemKey = Tuple[ItemSource, ID_TYPE]完全同构(见 eval_splits 相关代码)。

4. 数据模型变更详解

4.1TaskRun新增一个字段:eval_source

class EvalItemSource(BaseModel): """The eval *dataset item* this run was generated for. Not the run config — that already lives on this TaskRun at `output.source.run_config_id`. """ source_type: Literal["eval_input", "task_run"] source_id: ID_TYPE class TaskRun(...): eval_source: EvalItemSource | None = None

在源码中,EvalItemSource定义在 task_run.py 而非eval.py——eval.py仅在TYPE_CHECKING下导入TaskRun,把类型放回eval.py会制造真实的循环导入。配套的eval_item_key()辅助函数把 source 转换为 splits 分支的ItemKey词汇(task_run.py)。

几个关键设计要点:

  • eval_source的存在本身就是"评估生成"的标记。没有单独的布尔值——评估生成的 run 必有来源条目,普通数据集 run 必无,二者不可能产生分歧。
  • 它刻意不指名任何 Eval:trace 与评估无关,这正是它能跨评估复用的原因(D1、D12)。
  • 所有其他 TaskRun 字段与验证器保持不变。
  • 实现细节:source_id用str而非ID_TYPE(Optional[str]),因为无 id 的 source 会让 trace-index 键与其他所有无 id 来源碰撞;"不存在"已由eval_source本身为None表达。

4.2Task.runs()默认排除评估生成的 run

Task.runs(readonly=False, include_intermediate_runs=False, include_eval_generated=False) # NEW

源码实现在 task.py:过滤在既有叶子过滤之后执行,if not include_eval_generated: runs = [r for r in runs if r.eval_source is None],两个过滤器可叠加(run 必须同时通过两者才被返回)。

默认排除而非 opt-out 过滤是刻意为之:忘记处理评估 trace 时,失败方式是可见的(数据缺失),而不是静默的(微调集被污染、评估输出渗入 few-shot prompt)。架构文档 §4 对全部调用点做了审计(architecture.md §4),确认每个调用点默认排除都是正确的:

调用点用途默认排除是否正确
run_api.py:284,355数据集列表与计数正确
run_api.py:632标签计数正确
dataset_split.py:146微调集拆分正确
dataset_formatter.py:378微调导出正确
prompt_builders.py:206few-shot 示例正确——最关键的一处,评估输出进 few-shot 是反馈回路
finetune_api.py:234微调样本选择正确
agent_api.py:235数据集统计正确
eval_api.py:541,549,1625golden 集过滤器、split 成员正确——golden 项是人工评分的用户 run(D11)
eval_runner.py:149,257数据集条目收集器正确——V1-backed split 迭代的是用户 run,评估 trace 绝不能作为条目出现

整个仓库只有TraceIndex._seed()传include_eval_generated=True。

4.3EvalRun:一个新增字段、五个废弃字段

新增两个字段:

scored_run_id: ID_TYPE | None = None # 这条分数计算所依据的 TaskRun eval_usage: Usage | None = Field( default=None, description="The usage of the evaluation model (judge) that produced this eval run's scores, aggregated across every LLM call the judgment made. ...", )

eval_usage填补了一个真实缺口:LLM 评判器的 token 成本此前无处记录。它也是废弃task_run_usage的自然对应物——任务的usage 移到 TaskRun,评判器的usage 在分数记录上取而代之。源码见 eval.py。

保留且有意义的字段:

字段角色
父EvalConfig哪个评判器产生了这条分数
scores分数本身
task_run_config_id反范式化的 run config,供免 join 聚合
dataset_id/eval_input_id数据集条目——这就是 ItemKey
intermediate_outputs评判器思维过程
skipped_reason/skipped_detail评分跳过记录
eval_usage新——评判器自身的 token/成本消耗
eval_config_eval不变;已随 V1 发布(D18)

废弃字段(永远保持声明与可加载,但新记录永不写入,D15):input、output、task_run_trace、task_run_usage、reference_answer。其中input从必填改为可选。源码中这些字段的 description 均带DEPRECATED:前缀,使废弃状态直达生成的 OpenAPI schema 与前端类型。

什么都不删除,现有文件一律不重写。

4.4 三种记录状态:Pointer / Skipped / Legacy inline

每份记录恰好处于一种状态,由验证器强制(D17,源码见 eval.py 的 validate_record_mode):

状态判别器scored_run_idscoresinput+ trace 字段
Pointer(新,已评分)scored_run_id已设、无 skip必填非空必须为 None
Skippedskipped_reason已设可选——trace 存在则设,生成前跳过则 None空新记录必须为 None;见下注
Legacy inlinescored_run_id为 None、无 skipNone非空input必填,trace 字段允许

关于 Skipped 行的注记:"必须为 None"是新记录写入的规则,而非验证器强制。只有 pointer 记录被禁止携带内联数据。一条无scored_run_id的跳过记录可以携带内联数据——因为拆分前的每条 EvalRun 都必填input,字面执行该行会拒绝磁盘上所有已存在的跳过记录。所以规则是:凡scored_run_id已设,内联数据一律禁止;否则允许。pointer 规则的"禁止"半边才是真正发挥作用的:pointer 模式记录绝不能携带它所评分内容的过期第二份拷贝。

validate_scores保持不变——除非跳过,非空分数仍然必填。不存在"没有分数的 EvalRun"状态,因为"已生成但未评分"表现为"一个没有 EvalRun 指向它的 TaskRun"。

验证器还有一个顺序细节:skip 分支在 pointer 分支之后检查,这样在评分时跳过的记录(带scored_run_id)仍会执行无内联数据检查。validate_output_fields增加 pointer 模式提前返回,避免旧的"V1 EvalRun 必须带 output"规则误伤新记录。

5. Runner 行为:活的 trace 查询 + 即时的 trace 持久化

5.1 每任务流程

对每个(item, run_config, eval_config)任务:

  1. Trace 查找——找一条eval_source == (source_type, source_id)且output.source.run_config_id == task_run_config_id的 TaskRun。
    • 命中:复用,不生成。
    • 未命中:生成,然后在评分前立即持久化 TaskRun。
  2. Score 查找——在该EvalConfig下找scored_run_id == <那条 TaskRun>的 EvalRun。
    • 命中:无事可做。
    • 未命中:评分,持久化 EvalRun。

源码中_resolve_trace先检查校准项(golden 项直接作为 trace 返回),否则构造(item_key, run_config_id, world)键交给TraceIndex.get_or_create,生成回调_generate_trace调用evaluator.run_task(...)后盖上eval_source再保存(eval_runner.py);分数统一经_persist_score写出(永不设置内联 trace 字段),_persist_judgment与_persist_skip都是它的薄封装(eval_runner.py)。

5.2 trace 查找必须是活的

第一步的查找在任务执行时评估,而非在collect_tasks()阶段预计算。这是对既有already_run模式的刻意背离:预计算的集合看不到同一次运行中早前已持久化的 trace,会造成并发重复生成与重试时再生成。

新组件TraceIndex(libs/core/kiln_ai/adapters/eval/trace_index.py)是运行器自有的活索引,构造时从磁盘种子一次、随后持续更新,并带 per-key 锁:

TraceKey = tuple[str, ID_TYPE, ID_TYPE] # (source_type, source_id, run_config_id) class TraceIndex: def __init__(self, task: Task): self._task = task self._paths: dict[TraceKey, Path] = {} self._key_locks: dict[TraceKey, asyncio.Lock] = {} self._registry_lock = asyncio.Lock() self._seed() async def get_or_create( self, key: TraceKey, generate: Callable[[], Awaitable[TaskRun]] ) -> tuple[TaskRun, bool]: """Returns (trace, was_generated). `generate` must persist the TaskRun.""" async with self._registry_lock: lock = self._key_locks.setdefault(key, asyncio.Lock()) async with lock: path = self._paths.get(key) if path is not None: return TaskRun.load_from_file(path), False run = await generate() if run.path is None: raise ValueError("generate() must persist the TaskRun before returning") self._paths[key] = run.path return run, True

设计要点(源码均有对应实现与注释):

  • 存路径而非对象:评估 trace 是大字段,全量驻留内存不可接受;ModelCache使按路径 + mtime 的重复load_from_file保持廉价。
  • 两级锁:_registry_lock只守卫锁的创建,纳秒级持有;per-key 锁横跨整个生成过程,仅串行化会重复劳动的作业,不同键的作业互不阻塞。源码用AsyncLockManager实现并自动回收闲置条目。
  • 种子不是额外扫描:collect_tasks本就遍历task.runs(),同一趟构建索引。
  • setdefault实现 D8 的"先到先得",处理同步产生的重复 trace。
  • generate返回前必须持久化,满足"trace 先于评分落盘"的硬性要求;_generated_path还会核对生成的 run 是否按请求的键自归档(从 run 自身的eval_source、output.source.run_config_id、world_episode反读),防止"看似正常却永不复用"的静默重生成。
  • 读侧_load_indexed对文件丢失、解析失败、键漂移三种情况降级为"移除条目并重生成",绝不因一条坏 trace 拖垮整个作业。

5.3 重试:idempotent 查找就是阶段划分

重试机制不变。AsyncJobRunner继续在RetryableError时重试整个run_job。由于 trace 查找是活的,评分失败后的重试会找到已持久化的 TaskRun,跳过生成,只重新评分。幂等查找本身就是阶段划分——不需要把重试粒度改到单阶段。

5.4 重复 trace:先到先得

同键可能有多条 TaskRun 匹配——同步意味着唯一性无法保证(两个人分别运行后同步)。系统对此已具备容忍度:首个匹配胜出(D8),没有任何路径会刻意创建第二条。种子与写路径都通过setdefault实现这一点。

5.5 校准(calibration):不生成、不标记

在eval_config_eval=True的校准模式下,不进行任何生成:被评分的 TaskRun就是人工评分过的 golden 数据集条目。scored_run_id直接指向它,不创建任何评估生成的 TaskRun——因此 golden 条目永远不会被打上评估标记、永远不会被默认排除、永远不会被删除保护拒绝。源码中_resolve_trace对校准任务直接return job.item, False(eval_runner.py)。

5.6 跳过:三种情况三种落盘

条件落盘记录
该条目无法生成(如incompatible_input_shape)EvalRun,设skipped_reason,无scored_run_id
trace 存在但该评判器无法评分(missing_reference_key、extraction_failed、missing_trace、code_eval_not_trusted、type_not_available)EvalRun,设skipped_reason,设scored_run_id
生成本身失败什么都不落盘(D14)——TaskRun.output必填,失败的生成无可表示之物。条目显示为未完成,下次运行重试,与现行"absence = not-yet-run"约定一致

6. 读路径:分数聚合不变,usage 汇总换源

6.1 分数聚合一行不动

compute_score_summary、eval-config 对比汇总、per-run-config 汇总全部读取eval_config.runs()并按task_run_config_id与条目键分桶。分数计算触及的每个字段都留在EvalRun上,所以那部分代码完全不动。完整性判定仍为(eval_config, run_config, item)三元组的 EvalRun 存在性:未评分的 trace 视为不完整——这是正确的。

唯一例外:per-run-config 汇总从eval_run.task_run_usage汇总 token、成本和延迟(对应 eval_api.py 的汇总函数)。该字段废弃后,汇总改从 join 到的 TaskRun 读取。

6.2 读TaskRun.usage,不读cumulative_usage——两个已验证的理由

  1. usage已覆盖 run 内每一次 LLM 调用。它是运行累加器而非单轮值:源码中usage += call_usage(工具调用循环内,litellm_adapter.py)与usage += turn_result.usage(跨轮次)都有实锤。一个 5 轮 agentic run 报告全部 5 轮的用量。TaskRun.cumulative_usage的 docstring 也一致——"对全新(非 seed)run,token/成本字段与usage相等"。
  2. cumulative_usage会静默丢弃延迟。它类型为MessageUsage,刻意不含total_llm_latency_ms——"跨整条 trace 聚合会混合不同时间点的延迟"。Usage是增加该字段的子类,而汇总今天就在报告它。

两字段仅在 run 从seeded prior trace续跑时不同:cumulative_usage汇总整条消息列表,usage只计本次 run 自己的调用。新生成的评估 trace 没有 seeded prior trace,二者相等。若未来出现 seeded 评估 trace,正确做法不是换字段,而是 token/成本读cumulative_usage、延迟读usage——它们回答的是不同问题。

Legacy 记录仍携带task_run_usage,故scored_run_id未设时汇总回退到它。

6.3 run-results 视图改为 join 视图

get_eval_run_results原返回List[EvalRun](内联input/output),pointer 模式下这些字段为 None。端点改为返回join 视图模型EvalRunWithTrace(eval_api.py)——EvalRun 字段加上从被引用 TaskRun 拉取的input、output与 trace,前端契约得以保留。内容不反范式化到 EvalRun 上,因为存储拷贝会过期:

  • scored_run_id为 None → 读 EvalRun 自身内联字段(from_own_fields)。
  • scored_run_id已设 → 加载 TaskRun,使用input、output.output(绝不使用repaired_output——修复可能发生在评分之后)、trace、usage(from_scored_run)。
  • TaskRun 缺失 → 四个字段全为 None,分数照常渲染。

显示规则:展示分数计算所依据的内容时显示TaskRun.output,绝不显示repaired_output。端点用Task.from_ids_and_parent_path批量加载被引用 TaskRun,一次目录扫描而非逐 id 读取。api_schema.d.ts随之重新生成。

6.4 悬挂引用优雅降级

若scored_run_id指向缺失的 TaskRun,分数照常显示、聚合照常计数,仅 trace 下钻不可用。不要级联删除分数,不要在悬挂 id 上抛错——这正是删除保护之外,同步/导出场景下分数仍能存活的保证。

7. 删除保护:唯一的用户可见新表面

删除带eval_source的 TaskRun 被拒绝,返回信息:"This is needed for an eval."(D16)。无需反向引用扫描——TaskRun 上的标记本身就足够,无论当前是否有 EvalRun 指向它,保护都成立。这是整个项目唯一新增的用户界面(产品面 scope 决策 D20:复用可见性、强制全新生成、配置编辑过期处理全部明确划出范围)。

源码实现在 libs/server/kiln_server/run_api.py:delete_run与批量delete_runs都增加if run.eval_source is not None守卫,抛出 HTTP 409(请求格式正确、资源状态禁止,故用 409 而非 400);批量端点对被拒的评估 run 是上报而非静默跳过——既有的 per-run 错误收集把原因带回给用户,让其明白选择为何被部分删除。此外run_api.py还拒绝客户端通过 PATCH 设置eval_source({"eval_source": null}同样被拒),确保该标记只能由评估运行器写入。

8. 旧数据共存:V1 原样存活

  • V1 EvalRun(g_eval/llm_as_judge配置下)以 legacy-inline 模式完全按今天的方式加载与渲染,永不迁移、永不重写。
  • EvalConfig 构造上不可变(D21):eval_api只暴露 create/get/list,没有 PATCH 或 DELETE。因此分数相对其配置永远不会过期。配合复用键,每条 EvalRun 由三件不可变之物完整描述:条目、run config、eval config。
  • 无任何 V1 文件格式变更,无任何 schema 字段被移除。

研究文档(D21)特别强调这是承重不变式而非附注:整个复用设计建立在其上——"分数不可能相对其配置过期,因为配置不可能改变"。

9. 迁移:V2 内部数据的migrate-eval-runs命令

V2 尚未发布,但内部项目已携带 V2 评估数据。迁移脚本是必备交付物,作为最终阶段运行;懒加载折叠不可用,因为记录要跨目录搬迁。

对每个 V2EvalRun(父EvalConfig.config_type == "v2"):

  1. 从内联 trace 字段(input、output、task_run_trace、task_run_usage)为每条 EvalRun 创建一个 TaskRun,eval_source由dataset_id/eval_input_id对设定,run config 取自task_run_config_id。
  2. 将 EvalRun 重写为 pointer 模式:设scored_run_id,清空内联字段。刻意不去重——多个 eval config 的 EvalRun 可能描述同一份生成,按(item key, task_run_config_id)归组会坍缩成共享 TaskRun,但这是单一内部项目的数据,重复量小而无害,且今后的活 trace 查找自然会按键产出一条 trace。
  3. 校准 EvalRun(eval_config_eval=True)已通过dataset_id引用真实 TaskRun——直接设scored_run_id = dataset_id,不创建任何东西。
  4. 无 trace 的跳过 EvalRun——清空内联字段,scored_run_id保持未设。
  5. V1 EvalRun 整体跳过。

脚本幂等:已是 pointer 模式的 EvalRun 原样跳过。实现位于 libs/core/kiln_ai/cli/commands/migrate_eval_runs.py,带--dry-run旗标(只打印计划、写零字节)与-y/--yes跳过确认;顺序保证先存 TaskRun、后重写 EvalRun,使中断的运行留下无害的孤儿 TaskRun(默认不可见)而非悬挂指针;_exit_if_incomplete对部分迁移的项目以非零退出码结束,干跑亦如此,便于脚本化预检。

10. 词汇统一:scoring vs calibration

新代码、注释、文档与 UI 统一采用(存储字段eval_config_eval保持原名):

概念术语
评估一个 task run config:先生成再评判。"模型 A 比模型 B 好吗?"scoring
评估一个 eval config:评判人工评分条目并做相关性分析。"我的评判器靠谱吗?"calibration

"Trace" 指 TaskRun;"Score" 指 EvalRun。这个命名并非表面文章:eval_config_eval的旧名把两种不同语义塞进一个布尔值(且 False 分支的含义完全不在名字里),而 scoring/calibration 的区分正是整个拆分后模型语义的自然投影——calibration 下被评分的 TaskRun 就是 golden 数据集条目本身,没有任何"生成"发生。

11. 范围之外:刻意不做的事

事项原因
"用此评判器给现有 run 打分"动作现有 Run 按钮在 trace 可复用后已自动实现
复用可见性 / 进度计数快就够了;一次快速运行不算 bug
强制全新生成范围蔓延,且今天本就没有 force-fresh
配置编辑过期处理不存在此问题;EvalConfig 不可变(D21)
跨评估复用作为功能任务级 trace 存储自然带出;无 pointer-EvalInput 类型(D2)
Metrics / 观测型评估作为读侧概念推迟,由 split ref 限定范围(D3)
EvalInput-backed golden 集人工评分住在TaskRun.output.rating;golden 保持 TaskRun-only(D11)
移除eval_config_eval已证明冗余但随 V1 发布——无关的兼容性搅动(D18)
multi_turn_drive_config移到EvalInput归 eb-v2 分支所有;是多轮跨评估复用的前置条件,不是本项目工作(D5)
既有的生成漂移漏洞接受:未冻结动态 prompt、按 id 引用工具、task.instruction(D10)

12. 依赖与约束

依赖:

  • claude/eval-splits-evals-v2分支——本项目建立在其上。Eval.splits、resolve_split()、ItemKey、eval_run_item_key()全部复用而非重实现;成员关系一律经resolve_split()解析。
  • eb-v2 / multi-turn 分支——multi_turn_drive_config在发布前移到EvalInput(已与 eb-v2 达成一致)。落地后复用键无需额外项即可覆盖它(drive config 成为条目的一部分);落地前,多轮 trace 不得跨评估复用。

约束:

  • 向后兼容绝对优先:磁盘上每条 V1 记录原样加载、原样渲染;任何 schema 字段不删除。
  • 目录规模:runs/现在同时容纳数据集语料与全部评估 trace,已单独基准测试并接受。
  • 零新增依赖。
  • 每个全新任务两次保存而非一次,均在 git-syncSaveContext内完成。

13. 测试矩阵:设计如何被验证

重构的正确性由覆盖四个层面的测试守护(详见 architecture.md §9):

  • 数据模型(test_eval_model.py):validate_record_mode的六种合法/非法组合;磁盘上每条 V1 fixture 仍能原样加载(D15 回归护栏);Task.runs()默认排除/显式包含/与include_intermediate_runs叠加。
  • TraceIndex(test_trace_index.py):seed 命中复用;seed 忽略无eval_source的 run;重复种子先到先得;25 个并发get_or_create对同一键恰好调用一次generate且拿到同一 run(护住钱袋子的那条测试);不同键并发生成互不串行;未保存的generate返回值直接抛错。
  • Runner(test_eval_runner.py):头号测试——跑一次评估、加第二个 eval config、再跑一次,断言第二次run_task调用次数为零且出现第二组 EvalRun;校准不创建 TaskRun 且scored_run_id = dataset_id;评分失败后 trace 仍在、重试零重生成;trace 的eval_source与条目 ItemKey 匹配;V1 legacy 路径不变(characterization)。
  • API(test_eval_api.py):EvalRunWithTrace对 pointer 与 legacy 记录解析出同形结果;TaskRun 同时有 output 与 repaired_output 时返回前者;悬挂scored_run_id→ 全空字段 + HTTP 200;usage 汇总 pointer 记录读TaskRun.usage、legacy 读task_run_usage。
  • 删除保护(test_run_api.py):删评估生成 run → 409;混合批量删除 → 非评估 run 删除、评估 run 上报原因。
  • 迁移:含 V1、V2 pointer、V2 inline、校准、跳过记录的 fixture 项目 → 只有 V2 inline 记录发生变化;第二遍跑是 no-op;干跑零写入。

14. 实施顺序的关键约束

这是一次横跨约 8 个文件的内聚变更,而非一组各自独立的复杂组件。唯一硬性顺序约束:Task.runs()默认排除(§4.2)必须先于run_task开始持久化(§5.1)落地。反序落地会让两者之间的每一个数据集界面都被评估 trace 污染——这正是整个设计要消灭的静默失败模式。


延伸阅读:功能规格书 · 架构文档 · 研究文档(含 D1–D21 完整决策表)

  • 人工智能
  • AI 应用
  • AI 评测
  • RAG
  • 微调
  • 提示工程
  • 开发工具
  • 桌面应用

【免费下载链接】Kiln

Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.

项目地址:https://gitcode.com/gh_mirrors/ki/Kiln
点击查看免费下载

相关推荐

上一篇:Source Han Serif CN:免费开源中文字体终极指南
下一篇:N_m3u8DL-RE:跨平台流媒体下载终极指南 - 免费开源工具快速上手教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进