Skip to content

第 10 篇 · Skills Eval 技术方案

本篇是 【扩展设计】,不是当前 Langfuse OSS 已经完整实现的功能说明。它的目的,是基于当前 OSS 的抽象,设计一套“用 Langfuse 做 skills 迭代评估”的技术方案,供团队讨论和拆任务。


10.1 一页结论

我们要做的不是“给 Langfuse 加一个新的 prompt 类型”,而是在 Langfuse OSS 已有的事实层、实验层、评价层之上,补齐三块能力:

  1. 候选资产管理:把 skills 当成可版本化、可复现、可比较的 CandidateAsset
  2. 受控运行环境:用 ExperimentRuntimeRunner 拉起 Claude Code / Codex / 其他 agent runtime,消费某个 skill 版本和 dataset item。
  3. 实验产物建模:每次 dataset item 执行后产生 ExperimentArtifactSet,里面可以包含多个 ExperimentArtifact 和更细的 ArtifactPart;evaluation 的默认主目标是 artifact set,而 trace / observation 是 artifact set 的生产过程证据。

核心公式是:

text
Dataset
  + SkillVersion
  + RuntimeSpec
  + EvaluatorSuite
  -> DatasetRun / Experiment
  -> RunnerJob per DatasetItem
  -> Trace / Observation tree
  -> ExperimentArtifactSet
  -> ArtifactSet Score
  -> Comparison / Analysis

这里最关键的判断是:

text
Experiment 产生 artifact set。
Evaluator 默认评价 artifact set,也可以评价其中的 artifact / artifact part。
Trace / Observation 记录 artifact set 如何被生产出来。
Score 是评价事实,最终用于 experiment comparison。

当前 Langfuse OSS 已经具备这条链路的大部分底座:

能力当前 OSS 是否已有说明
Dataset / DatasetItem测试材料和单个 case。
DatasetRun / Experiment一次 dataset 运行快照。
DatasetRunItem连接 dataset item 与 trace / observation。
Trace / Observation记录一次运行的过程事实。
Score统一评价事实,支持 trace / observation / session / dataset run 等目标。
Experiment comparison基于 dataset run item、trace、score 做比较分析。
Prompt candidate当前内置候选资产主要是 Prompt。
Skill candidate没有需要新增 skill asset/version 控制面。
Agent runtime runner没有当前内置 prompt experiment 主要是单次 LLM completion。
ExperimentArtifactSet / Artifact / Part 一等实体没有当前可以从 trace output 间接看产物,但没有一等 artifact set / artifact / part。
ArtifactSet / Artifact / Part score target没有当前可先兼容写入 trace / dataset run score,长期应支持 artifact set / artifact / artifact part target。

所以这个方案的原则是:

text
不要重写 Langfuse 的事实层。
不要重写 Langfuse 的 score 层。
不要把 skill 硬塞进 prompt。
新增的部分应该是 candidate asset、runtime runner、experiment artifact set、artifact set / artifact score target。

10.1.1 总览 draw.io 图

这张技术方案有一个配套的 draw.io XML 总图:

这张图按“控制面、运行环境、结果回收、存储落点、评分主语”组织。读图时先看左侧 Langfuse 控制面如何创建 ExperimentSpec / DatasetRun / RunnerJob,再看右侧 sandbox 如何运行 skill 和 app,最后看 Result Package 如何回到 ArtifactSetTrace / ObservationScoreRun Analysis


10.2 方案边界

本篇会同时出现 【当前实现】【扩展设计】。读的时候要先分清边界。

类型范围例子
【当前实现】当前 Langfuse OSS repo 已经存在的实体、表、队列、查询或 UI 抽象。DatasetDatasetRunsDatasetRunItemsscorestracesobservationsdataset_run_items_rmt
【扩展设计】为 skills eval 需要新增或泛化的实体、接口和控制面。CandidateAssetSkillVersionExperimentRuntimeRunnerRunnerJobExperimentArtifactSetExperimentArtifactArtifactPartartifact_set_id / artifact_id / artifact_part_id score target。

本方案不做三件事:

  1. 不把 Langfuse 改造成通用 CI/CD 系统。
  2. 不重写 ClickHouse event store。
  3. 不把 Claude Code / Codex 的内部循环逻辑塞进 Langfuse web 进程。

Langfuse 在这个方案里承担的是:

text
实验定义
  + 运行事实采集
  + artifact set / artifact / part 关系管理
  + score 落库
  + comparison / analysis

Runner 承担的是:

text
准备受控环境
  + 注入 skill
  + 执行 dataset item
  + 采集产物
  + 上报 trace / observation
  + 回写 artifact set / artifact / part metadata

10.3 为什么 artifact 是主语

在 prompt experiment 里,很多时候可以把“最终 answer”直接理解成 trace output。于是系统看起来像这样:

text
DatasetItem
  -> prompt completion
  -> trace output
  -> score

但 skills / agent eval 不是这样。一个 skill 执行后可能产生:

  • 最终回答;
  • 修改后的文件 patch;
  • 新增文件;
  • 测试报告;
  • 命令执行结果;
  • structured JSON result;
  • repo workspace diff;
  • agent 生成的计划、审计报告或修复说明。

这些都是 artifact。它们不等同于 trace,也不应该被建模成“一个文件”。

更准确的抽象是三层:

text
ExperimentArtifactSet
  -> ExperimentArtifact[]
      -> ArtifactPart[]
层级含义例子
ExperimentArtifactSet某个 DatasetRunItem 的完整产物集合。一次 Claude Code / Codex 执行后得到的 answer + patch + test report + logs。
ExperimentArtifact产物集合中的一个逻辑产物。final_answerpatchtest_reportworkspace_diff
ArtifactPart一个逻辑产物下面的文件、片段或 blob part。patch 中的某个文件 diff、测试报告中的 junit xml、workspace snapshot 中的某个文件。

这样可以覆盖两类情况:

情况如何表达
一个简单回答一个 ExperimentArtifactSet,包含一个 final_answer artifact。
一个复杂 agent run一个 ExperimentArtifactSet,包含 answer、patch、test report、workspace diff、runner logs 多个 artifact;每个 artifact 还能继续拆 part。
对象负责回答在 eval 中的角色
Trace这次运行发生了什么?过程证据、调试入口、成本/延迟/工具调用分析。
Observation运行中的某一步做了什么?细粒度过程事实,例如模型调用、工具调用、文件读取、测试执行。
ExperimentArtifactSet这次运行整体产出了什么?默认评价主目标和 comparison 主体。
ExperimentArtifact这次运行产出的某个逻辑结果是什么?细分评价目标,例如 patch 或 test report。
ArtifactPart某个 artifact 内部的文件或片段是什么?更细粒度诊断目标。
Score这个产物好不好?评价事实,用于 comparison 和 analysis。

所以 skills eval 的核心关系应该是:

这会带来一个重要设计原则:

text
Score 的默认主 target 应该是 artifact set。
Evaluator 可以进一步对 artifact / artifact part 打分。
Trace / Observation 可以参与 evaluator 输入,但不应该代替产物成为默认被评价对象。

为什么这件事重要?

如果只给 trace 打分会发生什么问题
artifact set 没有一等身份很难比较“这个 item run 的整体产物是否更好”。
artifact 没有一等身份很难比较“哪个 patch / answer / report 被评了”。
score 和产物版本关系弱以后 artifact 被重写、覆盖、截断时,score 难以复现。
多 artifact 场景表达困难一次运行可能同时产出 answer、patch、test report。
多文件产物表达困难patch、workspace diff、测试报告常常不是单文件。
过程分和结果分混在一起tool 调用成功率、最终答案质量、patch 可合并性会被混成同一层。
comparison 不够清晰团队真正关心的是 candidate 产生的结果质量,不只是运行日志质量。

10.4 分层架构:越下层越稳定,越上层越业务化

这套方案必须分层,否则很容易把“事实存储”“实验定义”“skill 管理”“runner 编排”“产品流程”混成一团。

分层原则是:

text
下层决定结构:稳定、通用、少业务假设。
上层决定应用:灵活、面向业务流程、可以快速变化。

10.4.1 总体分层图

10.4.2 每层职责

稳定程度职责当前 OSS 状态
L0 Storage & Infra最稳定存储、队列、大对象、分析库。已有。
L1 Fact & Evaluation稳定trace / observation / score 等事实模型。已有。
L2 Experiment Core稳定dataset、run、run item 的实验骨架。已有。
L3 Artifact & Candidate中等稳定被测资产版本、实验产物集合、逻辑产物和产物片段。需要扩展。
L4 Runtime Orchestration可替换runner、sandbox、任务、重试、资源控制。需要扩展。
L5 Product Workflow最灵活UI 和业务流程。需要按团队场景设计。

越往下,越不应该出现 “Claude Code 特有字段” 或 “skills 目录约定” 这样的业务细节。越往上,越可以围绕团队当前的 skill 管理和 agent runtime 做定制。

10.4.3 分层后的设计判断

设计问题应该落在哪一层原因
Score 怎么聚合、怎么筛选L1这是通用评价事实,不应该绑定 skill。
DatasetRunItem 怎么连接 traceL2所有 experiment 都需要 item-result 连接。
skill bundle 如何版本化L3这是 candidate asset 管理,不属于 event store。
Claude Code 怎么启动L4runtime 可替换,不应该污染 Dataset / Score 基础模型。
UI 里怎么选择 skill 和 modelL5产品流程随业务变化最快。
artifact set / artifact / part 文件放哪里L0 + L3blob 存内容,Postgres 存 metadata 和关系。

10.5 当前 OSS 抽象如何承接这个方案

10.5.1 Dataset / DatasetItem

【当前实现】 DatasetDatasetItem 在 Postgres 中表达测试材料。

text
Dataset
  id
  projectId
  name
  metadata
  inputSchema
  expectedOutputSchema

DatasetItem
  id
  datasetId
  input
  expectedOutput
  metadata
  sourceTraceId?
  sourceObservationId?
  validFrom / validTo

在 skills eval 中,DatasetItem.input 应该描述“要让 agent 完成的任务”。例如:

json
{
  "repo": "example/service-a",
  "task": "修复 flaky test: tests/payment/refund.test.ts",
  "constraints": {
    "allowedCommands": ["pnpm test", "pnpm lint"],
    "maxTurns": 40
  }
}

DatasetItem.expectedOutput 不一定只是字符串答案,可以是评价标准:

json
{
  "mustPassTests": ["tests/payment/refund.test.ts"],
  "mustNotModify": ["packages/shared/src/auth/**"],
  "expectedBehavior": "退款失败时应保留原订单状态"
}

10.5.2 DatasetRun / Experiment

【当前实现】 DatasetRuns 表达一次 experiment run。当前字段比较通用:

text
DatasetRuns
  id
  projectId
  name
  description
  metadata
  datasetId

它没有强绑定 prompt,所以可以承接 skills eval。关键是要把 run 的不可变配置写清楚:

json
{
  "candidate": {
    "type": "skill",
    "assetId": "skill_bugfix",
    "versionId": "skillver_20260702_001",
    "contentHash": "sha256:..."
  },
  "runtime": {
    "type": "claude_code",
    "runnerVersion": "runner_0.3.0",
    "model": "claude-sonnet-4",
    "sandboxImage": "ghcr.io/company/skill-runner:2026-07-02"
  },
  "evaluation": {
    "suiteId": "suite_bugfix_v1",
    "scoreNames": ["tests_passed", "patch_quality", "instruction_following"]
  }
}

兼容型 MVP 可以先把这些内容放进 DatasetRuns.metadata。长期更清晰的做法是新增 ExperimentSpecExperimentRuntimeConfig 表,避免 metadata 成为不可查询的大 JSON。

10.5.3 DatasetRunItem

【当前实现】 DatasetRunItems 现在是 experiment item 和 trace 的连接表:

text
DatasetRunItems
  id
  datasetRunId
  datasetItemId
  traceId
  observationId?

这对 prompt experiment 够用,但 skills eval 还缺 artifact set 连接。

【扩展设计】 不建议直接把所有 artifact 字段塞进 DatasetRunItems,而是新增 ExperimentArtifactSet,再从 set 下挂多个 artifact:

text
DatasetRunItem
  -> Trace / Observation
  -> ExperimentArtifactSet
      -> ExperimentArtifact[]
          -> ArtifactPart[]

这样可以表达:

  • 一个 dataset item 的整体产物集合;
  • 一个 artifact set 下有多个 artifact;
  • 一个 artifact 下有多个文件、blob 或片段;
  • artifact 内容很大,需要 blob 存储;
  • artifact 有类型、hash、schema、版本;
  • score 可以明确指向 artifact set、某个 artifact,或者某个 artifact part。

10.5.4 Trace / Observation

【当前实现】 Trace / Observation 是运行事实。对 Claude Code / Codex 这种 agent runtime,observation tree 可以表达:

text
Trace: dataset item 的一次 agent run
  Observation: 读取任务说明
  Observation: 调用模型生成计划
  Observation: 读取文件
  Observation: 执行命令
  Observation: 修改文件
  Observation: 运行测试
  Observation: 总结结果

这非常适合做调试和分析,但它不是默认最终评价对象。默认最终评价对象应该是 artifact set;需要诊断时再下钻到 artifact / artifact part。

10.5.5 Score

【当前实现】 Score 是评价事实。ClickHouse scores 已经支持 trace_idobservation_idsession_iddataset_run_id 等维度,experiment comparison 也会围绕 score 做聚合。

【扩展设计】 skills eval 应该把 score 分成六个层级:

Score 层级Target用途
Artifact set scoreartifact_set_id最核心,评价某个 dataset item run 的整体产物质量。
Artifact scoreartifact_id评价某个逻辑产物,例如 patch、final answer、test report。
Artifact part scoreartifact_part_id评价某个文件、diff 片段或报告片段,主要用于诊断。
Trace scoretrace_id评价整次运行过程,例如是否超时、是否过度调用工具。
Observation scoreobservation_id评价某一步,例如 retrieval 是否命中、tool call 是否正确。
Experiment scoredataset_run_id对整个 run 做汇总,例如 pass rate、平均质量、回归风险。

MVP 可以先用现有 trace_id / dataset_run_id 字段,并在 score.metadata.artifactSetId / artifactId / artifactPartId 中记录产物目标。长期应把 artifact_set_id / artifact_id / artifact_part_id 做成一等 score target。


10.6 新增领域对象

10.6.1 CandidateAsset

CandidateAsset 是“被拿来评估的候选方案”的通用抽象。

text
CandidateAsset
  id
  projectId
  type            // skill | prompt | workflow | agent
  name
  description
  createdBy
  createdAt
  updatedAt

对当前需求,第一类 candidate 是 skill。但不要把表命名成只服务 skills 的形态,因为以后可能还有 workflow、agent config、tool set。

10.6.2 CandidateVersion / SkillVersion

SkillVersion 是某个 skill asset 的不可变版本。它应该能回答:

text
这次 experiment 到底用了哪一组 markdown / code 文件?
这些文件的 hash 是什么?
之后能不能复现?

建议字段:

text
CandidateVersion
  id
  projectId
  assetId
  version
  contentHash
  artifactUri
  manifest
  status          // draft | active | archived
  createdBy
  createdAt

manifest 示例:

json
{
  "type": "skill",
  "name": "bugfix-skill",
  "version": "2026.07.02-001",
  "entrypoints": ["SKILL.md"],
  "files": [
    {
      "path": "SKILL.md",
      "sha256": "..."
    },
    {
      "path": "scripts/diagnose.ts",
      "sha256": "..."
    }
  ],
  "runtimeHints": {
    "requiresRepo": true,
    "allowedTools": ["read", "edit", "shell"],
    "defaultMaxTurns": 40
  }
}

不变量:

text
一旦 SkillVersion 被 experiment 引用,内容不能再变。
如果文件变了,必须生成新的 version 和 contentHash。

10.6.3 ExperimentSpec

ExperimentSpec 是一次 experiment 的不可变运行说明。兼容 MVP 可以放在 DatasetRuns.metadata,长期建议一等表。

text
ExperimentSpec
  datasetRunId
  candidateVersionId
  runtimeType
  runtimeConfig
  evaluatorSuiteId
  createdAt

示例:

json
{
  "datasetId": "dataset_bugfix_cases",
  "candidateVersionId": "skillver_20260702_001",
  "runtime": {
    "type": "claude_code",
    "model": "claude-sonnet-4",
    "maxTurns": 40,
    "timeoutSeconds": 1800,
    "sandboxImage": "ghcr.io/company/skill-runner:2026-07-02"
  },
  "artifactPolicy": {
    "capture": ["final_answer", "patch", "test_report", "workspace_diff", "runner_logs"],
    "maxArtifactBytes": 10485760
  },
  "evaluation": {
    "suiteId": "suite_code_fix_v1",
    "runAfterArtifactReady": true
  }
}

10.6.4 RunnerJob

RunnerJob 是编排层状态,不是长期分析事实。

text
RunnerJob
  id
  projectId
  datasetRunId
  datasetItemId
  candidateVersionId
  runtimeType
  status          // pending | running | artifact_ready | evaluated | failed
  traceId?
  datasetRunItemId?
  error?
  attempts
  startedAt?
  finishedAt?

它应该放 Postgres 或专门的 job state 表,队列只传递轻量 payload:

json
{
  "runnerJobId": "rjob_123",
  "projectId": "proj_123",
  "datasetRunId": "run_123",
  "datasetItemId": "item_123"
}

队列 payload 不要直接塞完整 skill bundle、dataset input 或 artifact 内容。大对象应该通过 Postgres / blob URI 获取。

10.6.5 ExperimentArtifactSet

ExperimentArtifactSet 是本方案新增的核心对象。它表示某个 DatasetRunItem 的完整产物集合,默认作为 evaluator / scorer 的主 target。

text
ExperimentArtifactSet
  id
  projectId
  datasetRunId
  datasetRunItemId
  datasetItemId
  candidateVersionId
  runnerJobId
  traceId
  status           // complete | partial | failed
  summary
  metadata
  createdAt

summary 用来给 comparison 页面快速展示,不放大对象内容:

json
{
  "artifactTypes": ["final_answer", "patch", "test_report", "workspace_diff"],
  "artifactCount": 4,
  "changedFiles": 2,
  "testsPassed": 18,
  "testsFailed": 0
}

不变量:

text
一个 DatasetRunItem 默认对应一个主 ExperimentArtifactSet。
Artifact set 是 item run 的完整产物快照。
Artifact set 一旦被 score 引用,不应该原地覆盖。
如果同一个 item 重跑,生成新的 RunnerJob 和新的 artifact set。

10.6.6 ExperimentArtifact

ExperimentArtifact 表示 artifact set 中的一个逻辑产物。它可以是一个小 JSON、一段文本、一个 patch、一个测试报告、一个 workspace diff,也可以是一个多文件产物的 manifest。

text
ExperimentArtifact
  id
  projectId
  artifactSetId
  datasetRunId
  datasetRunItemId
  datasetItemId
  candidateVersionId
  runnerJobId
  traceId
  type             // final_answer | patch | test_report | file_tree | workspace_diff | custom
  name
  uri
  contentHash
  mimeType
  sizeBytes
  schemaVersion
  metadata
  createdAt

artifact 内容可以有三种形态:

形态存储方式例子
内联小对象Postgres JSON metadatafinal answer、短 JSON result。
单 blob 大对象Blob/S3 + Postgres metadatapatch 文件、测试日志、压缩包。
多 part 对象ArtifactPart[] + blobworkspace diff、多文件 patch、分段测试报告。

artifact metadata 示例:

json
{
  "type": "patch",
  "baseRevision": "abc123",
  "changedFiles": [
    "src/payment/refund.ts",
    "tests/payment/refund.test.ts"
  ],
  "summary": {
    "filesChanged": 2,
    "insertions": 45,
    "deletions": 12
  }
}

不变量:

text
Artifact 是 artifact set 下的逻辑产物。
Artifact 不要求是一份文件。
Artifact 一旦被 score 引用,不应该原地覆盖。
如果逻辑产物重算,生成新的 artifact 或新的 artifact set。

10.6.7 ArtifactPart

ArtifactPart 表示某个 artifact 内部的文件、blob、片段或结构化子对象。它主要解决“artifact 可能不只是一个文件”的问题。

text
ArtifactPart
  id
  projectId
  artifactId
  artifactSetId
  kind            // file | blob | json_path | diff_hunk | log_chunk | metric
  path?
  uri?
  contentHash?
  mimeType?
  sizeBytes?
  metadata
  createdAt

例子:

json
{
  "kind": "file",
  "path": "tests/payment/refund.test.ts",
  "uri": "s3://bucket/artifacts/artifact_123/parts/refund.test.ts.diff",
  "contentHash": "sha256:...",
  "metadata": {
    "changeType": "modified",
    "insertions": 18,
    "deletions": 4
  }
}

不变量:

text
ArtifactPart 只表达 artifact 内部结构。
ArtifactPart 不是默认 comparison 主体。
只有需要细粒度诊断时,才对 part 打 score。

10.7 数据关系图

10.7.1 业务实体关系

读这张图时注意:

  1. CandidateVersion 是输入候选资产。
  2. ExperimentArtifactSet 是输出实验产物集合,也是默认评价主目标。
  3. ExperimentArtifact 是集合里的逻辑产物,ArtifactPart 是产物内部的文件或片段。
  4. Trace / Observation 是过程证据。
  5. Score 应该默认贴到 artifact set,也可以贴到 artifact、artifact part、trace、observation 或 dataset run。

10.7.2 数据流关系


10.8 存储设计

10.8.1 存储落点

10.8.2 什么放哪里

数据建议存储原因
skill 名称、描述、ownerPostgres关系型控制面,可权限检查、可列表查询。
skill version manifestPostgres小 JSON,参与 experiment spec 和审计。
skill bundle 内容Blob/S3markdown/code 可能较大,且需要按 hash 复现。
dataset / dataset itemPostgres当前 OSS 已有,保留。
dataset run / run itemPostgres + ClickHouse 投影当前 OSS 已有,run item 关系需要分析聚合。
runner job 状态Postgres需要可靠状态机、重试、错误展示。
runner queue payloadRedis/BullMQ短期调度,不作为事实库。
trace / observationClickHouse Event Store高吞吐过程事实。
artifact set / artifact / part metadataPostgresartifact set 是关系对象,要和 run item、score、candidate version 关联;artifact/part 用于表达多产物和多文件结构。
artifact / part 大内容Blob/S3patch/report/log/workspace diff 可能很大。
scoreClickHouse scores评价事实,需要聚合、筛选、分析。

10.8.3 为什么 artifact metadata 不直接放 ClickHouse

artifact set / artifact / part 是关系对象,不只是分析事实。它需要:

  • 被权限系统保护;
  • 被 UI 列表查询;
  • DatasetRunItemRunnerJobCandidateVersion 建立关系;
  • 表达一个 item run 的完整产物集合;
  • 表达多 artifact 和多文件 artifact 的内部结构;
  • 被 evaluator 读取;
  • 被 score 引用;
  • 支持删除、归档、保留策略。

这些都更适合 Postgres。ClickHouse 适合保存高吞吐事实和聚合投影,不适合做 artifact set / artifact / part 的主记录。


10.9 运行链路

10.9.1 Sequence Diagram

10.9.2 分步骤解释

Step 1:上传 skill

用户上传一组 markdown 和代码文件。系统做:

  1. 校验文件结构;
  2. 计算文件 hash 和整体 content hash;
  3. 保存 bundle 到 blob;
  4. 创建 CandidateAssetSkillVersion

这一步不运行实验,只是在建立候选资产。

Step 2:创建 experiment

用户选择:

  • dataset;
  • skill version;
  • runtime 类型;
  • model;
  • sandbox / timeout / max turns;
  • evaluator suite。

系统创建 DatasetRun,并记录不可变 ExperimentSpec

Step 3:生成 RunnerJob

每个 active DatasetItem 生成一个 RunnerJob。这一步是控制面状态,不是运行事实。

text
DatasetRun
  -> RunnerJob(item_1)
  -> RunnerJob(item_2)
  -> RunnerJob(item_3)

Step 4:Runner 执行

Runner 根据 job 准备环境:

  1. 下载 skill bundle;
  2. 拉取或准备 repo workspace;
  3. 注入 dataset item input;
  4. 配置 Langfuse tracing 环境变量;
  5. 启动 Claude Code / Codex;
  6. 限制资源、超时、工具权限;
  7. 捕获最终产物。

Runner 不应该把完整执行逻辑塞进 Langfuse web 进程。它应该是可水平扩展、可隔离、可替换的执行器。

Step 5:写入 trace / observation

Claude Code / Codex 运行过程通过 Langfuse integration 或 OpenTelemetry 上报,进入现有 ingestion / event store。

每条 trace 必须带上 correlation metadata:

json
{
  "datasetRunId": "run_123",
  "datasetItemId": "item_123",
  "runnerJobId": "rjob_123",
  "candidateAssetType": "skill",
  "candidateVersionId": "skillver_123",
  "runtimeType": "claude_code"
}

这些 metadata 是后续查询、排错、回填关系的关键。

Step 6:生成 ExperimentArtifactSet

Runner 结束后保存 artifact set。一个 artifact set 可以包含多个逻辑 artifact,每个 artifact 又可以包含多个 part:

text
ExperimentArtifactSet
  -> final_answer
  -> patch
      -> file diff parts
  -> test_report
      -> junit xml part
      -> stdout log part
  -> workspace_diff
      -> changed file parts
  -> runner_logs

然后创建 ExperimentArtifactSetExperimentArtifactArtifactPart metadata,更新 RunnerJob 状态,确保 artifact set 与 DatasetRunItemTraceCandidateVersion 关联。

Step 7:触发 evaluation

Evaluation worker 读取:

  • artifact set;
  • artifact;
  • artifact part;
  • dataset item expected output;
  • trace / observation 上下文;
  • evaluator config;
  • score config。

然后产生 Score

Step 8:Comparison / Analysis

UI 按这些维度分析:

  • datasetRunId
  • candidateVersionId
  • datasetItemId
  • artifactSetId
  • artifactId
  • scoreName
  • runtimeType
  • model

最终回答:

text
哪个 SkillVersion 在同一组 DatasetItem 上表现更好?
哪些 case 退化了?
退化的 artifact set 是什么?
artifact set 里哪个 artifact 或 part 出问题?
生产 artifact set 的 trace 显示哪里出了问题?

10.10 核心接口契约

10.10.1 ExperimentRuntimeRunner

Runner 接口应该稳定,具体 runtime 可以替换。

ts
export type ExperimentRuntimeRunnerInput = {
  projectId: string;
  datasetRunId: string;
  datasetItemId: string;
  runnerJobId: string;
  candidate: {
    type: "skill";
    assetId: string;
    versionId: string;
    artifactUri: string;
    contentHash: string;
    manifest: Record<string, unknown>;
  };
  datasetItem: {
    input: unknown;
    expectedOutput?: unknown;
    metadata?: Record<string, unknown>;
  };
  runtime: {
    type: "claude_code" | "codex";
    model?: string;
    timeoutSeconds: number;
    maxTurns?: number;
    sandboxImage?: string;
    env?: Record<string, string>;
  };
  tracing: {
    traceId: string;
    langfusePublicKey: string;
    langfuseHost: string;
  };
};

export type ExperimentRuntimeRunnerOutput = {
  status: "success" | "failed" | "timeout";
  traceId: string;
  artifactSet: {
    summary?: Record<string, unknown>;
    artifacts: Array<{
      type:
        | "final_answer"
        | "patch"
        | "test_report"
        | "workspace_diff"
        | "runner_logs"
        | "custom";
      name: string;
      uri?: string;
      contentHash?: string;
      mimeType?: string;
      sizeBytes?: number;
      metadata?: Record<string, unknown>;
      parts?: Array<{
        kind: "file" | "blob" | "json_path" | "diff_hunk" | "log_chunk" | "metric";
        path?: string;
        uri?: string;
        contentHash?: string;
        mimeType?: string;
        sizeBytes?: number;
        metadata?: Record<string, unknown>;
      }>;
    }>;
  };
  error?: {
    message: string;
    code?: string;
    retryable: boolean;
  };
};

10.10.2 Skill Manifest

Skill manifest 用来让 runner 知道如何装载 skill。

json
{
  "schemaVersion": "skills.langfuse.dev/v1",
  "name": "bugfix-skill",
  "description": "修复代码问题并验证测试",
  "entrypoint": "SKILL.md",
  "files": [
    {
      "path": "SKILL.md",
      "sha256": "..."
    }
  ],
  "runtimeHints": {
    "requiresWorkspace": true,
    "defaultRuntime": "claude_code",
    "allowedTools": ["read", "edit", "shell"],
    "maxTurns": 40
  }
}

这个 manifest 是 candidate asset 层的契约,不应该包含某次 experiment 的 dataset item。

10.10.3 Artifact Set Manifest

Artifact set manifest 用来描述某个 item run 的完整产物集合。

json
{
  "schemaVersion": "experiment-artifact-set.langfuse.dev/v1",
  "artifactSetId": "aset_123",
  "datasetRunId": "run_123",
  "datasetRunItemId": "dri_123",
  "datasetItemId": "item_123",
  "candidateVersionId": "skillver_123",
  "runnerJobId": "rjob_123",
  "traceId": "trace_123",
  "summary": {
    "artifactCount": 4,
    "changedFiles": 2,
    "testsPassed": 18,
    "testsFailed": 0
  },
  "artifacts": [
    {
      "artifactId": "artifact_patch_123",
      "type": "patch",
      "content": {
        "uri": "s3://bucket/artifacts/artifact_patch_123.patch",
        "sha256": "...",
        "mimeType": "text/x-diff",
        "sizeBytes": 20480
      },
      "parts": [
        {
          "partId": "part_file_1",
          "kind": "file",
          "path": "src/payment/refund.ts",
          "uri": "s3://bucket/artifacts/artifact_patch_123/parts/refund.ts.diff"
        }
      ]
    }
  ]
}

10.10.4 Score 写入契约

兼容型 MVP:

json
{
  "name": "patch_quality",
  "value": 0.82,
  "dataType": "NUMERIC",
  "traceId": "trace_123",
  "datasetRunId": "run_123",
  "metadata": {
    "targetType": "experiment_artifact_set",
    "artifactSetId": "aset_123",
    "artifactId": "artifact_123",
    "artifactPartId": null,
    "datasetRunItemId": "dri_123",
    "candidateVersionId": "skillver_123"
  }
}

长期一等 target:

json
{
  "name": "patch_quality",
  "value": 0.82,
  "dataType": "NUMERIC",
  "targetType": "experiment_artifact_set",
  "targetId": "aset_123",
  "context": {
    "artifactId": "artifact_patch_123",
    "traceId": "trace_123",
    "datasetRunId": "run_123",
    "datasetRunItemId": "dri_123",
    "candidateVersionId": "skillver_123"
  }
}

长期模型更干净,但会影响 score ingestion、ClickHouse schema、query builder、score analytics、experiment comparison 和 UI。


10.11 Evaluation:定义 evaluator / scorer,对 artifact set 打分

Artifact 模型不替代 evaluator / scorer。相反,artifact set 让 evaluator 的 target 更清楚。

text
Evaluator / Scorer = 评分定义,说明怎么评、评谁、什么时候评。
ScoreConfig = 分数格式,说明 score 是 numeric / boolean / categorical / text。
Score = 评分结果,是最终落库的评价事实。
ExperimentArtifactSet = 默认被评分对象。
Trace / Observation = 评分时可读取的过程上下文。

所以扩展后的关系是:

text
Evaluator 定义“怎么打分”
Evaluation Rule 定义“artifact set ready 后评谁”
Evaluator Runtime 执行规则、代码或 LLM judge
Score 保存“评出来什么”

Evaluator 可以有多种实现:

类型怎么打分例子
deterministic evaluator用规则、脚本、测试结果计算tests_passedlint_passedpatch_applies
LLM-as-judge evaluator用 LLM 读取 artifact set、expected output、trace 上下文后判断patch_qualityinstruction_followinganswer_helpfulness
hybrid evaluator先跑规则,再让 LLM 解释或补充分数测试通过后再评代码质量。
external evaluator调用外部服务或 CI 平台安全扫描、性能 benchmark。
human annotation人工审核 artifact set 或 artifactreviewer acceptability。

Evaluator definition 的核心字段可以这样理解:

text
EvaluatorDefinition
  name
  targetType              // artifact_set | artifact | artifact_part | trace | observation | dataset_run
  inputMapping            // 从 dataset / artifact set / trace 取哪些字段
  implementationType      // code | llm_judge | external | human
  implementationConfig    // code ref / prompt / model / endpoint
  scoreConfig             // numeric / boolean / categorical / text
  triggerPolicy           // artifact_set_ready 后触发,或手动触发

LLM judge 示例:

json
{
  "name": "patch_quality",
  "targetType": "artifact_set",
  "implementationType": "llm_judge",
  "inputMapping": {
    "task": "$.datasetItem.input.task",
    "expected": "$.datasetItem.expectedOutput",
    "patch": "$.artifactSet.artifacts[?type=patch]",
    "testReport": "$.artifactSet.artifacts[?type=test_report]",
    "traceSummary": "$.trace.summary"
  },
  "scoreConfig": {
    "dataType": "NUMERIC",
    "min": 0,
    "max": 1
  },
  "triggerPolicy": {
    "on": "artifact_set_ready"
  }
}

10.11.1 Evaluator 的输入

skills eval 的 evaluator 不应该只看 trace output。它应该看完整上下文:

text
DatasetItem.input
DatasetItem.expectedOutput
ExperimentArtifactSet
ExperimentArtifact
ArtifactPart
Trace / Observation tree
Runner logs
Runtime metadata

不同 evaluator 读取不同输入:

Evaluator主要输入输出 score
测试通过率artifact set 里的 test reporttests_passed
patch 质量artifact set 里的 patch + trace contextpatch_quality
指令遵循final answer artifact + dataset expectedOutputinstruction_following
成本效率trace / observation usagecost_efficiency
工具使用合理性observation treetool_use_quality
安全检查patch / final answer / file diffsafety

10.11.2 Score 层级不要混

这些 score 有关系,但不是同一个东西:

Score 类型是否适合作为主 comparison 指标说明
Artifact set score直接评价 candidate 在某个 dataset item 上的完整输出产物。
Artifact score视指标而定评价某个逻辑产物,例如 patch_quality。
Artifact part score主要用于诊断,例如哪个文件 diff 有问题。
Trace score辅助解释成本、效率、失败原因。
Observation score辅助定位某一步为什么坏。
Experiment score汇总用于整体 run 的概览和 release gate。

10.11.3 Experiment score 怎么来

Experiment score 不应该凭空产生。它通常由 artifact set scores 聚合得到。

text
artifact set scores per item
  -> group by datasetRunId
  -> calculate pass rate / mean / p50 / p95 / regression count
  -> write dataset_run_id score

例如:

json
{
  "name": "pass_rate",
  "value": 0.86,
  "dataType": "NUMERIC",
  "datasetRunId": "run_123",
  "metadata": {
    "derivedFrom": "artifact_set_score",
    "sourceTargetType": "artifact_set",
    "sourceScoreName": "tests_passed",
    "itemCount": 50
  }
}

这样 comparison 可以同时看:

  • 每个 item 的 artifact set score;
  • artifact / artifact part 的诊断分;
  • 整个 run 的 aggregate score;
  • candidate version 之间的差异;
  • 哪些 dataset item 发生 regression。

10.12 Comparison 和 Analysis

10.12.1 Comparison 的数据基准

比较不能只按 DatasetRun 比,必须落到 item 和 artifact set:

text
同一个 DatasetItem
  在 SkillVersion A 下产生 ArtifactSet A1
  在 SkillVersion B 下产生 ArtifactSet B1
  对 A1 / B1 分别打 artifact set score
  必要时展开看其中的 artifact / part score
  再比较 score delta

推荐 comparison row:

字段含义
datasetItemId哪个 case。
candidateVersionId哪个 skill 版本。
datasetRunId哪次 experiment。
artifactSetId哪个 item run 的完整产物集合。
artifactId哪个逻辑产物,通常用于展开诊断。
artifactPartId哪个文件或片段,通常用于更细诊断。
traceId产物生产过程。
scoreName评价指标。
scoreValue分数。
scoreCommentevaluator 解释。
runtimeTypeClaude Code / Codex 等。
model使用的模型。

10.12.2 UI 上应该回答的问题

对团队有价值的页面,不是只显示“平均分 0.82”,而是回答:

  1. 哪个 skill version 总体最好?
  2. 哪些 dataset item 上新版本退化?
  3. 退化 item 对应的 artifact set 是什么?
  4. artifact set 里具体哪个 artifact / part 出问题?
  5. 这个 artifact set 是怎么生成的,trace 里哪一步有问题?
  6. 是 skill 问题、runtime 问题、model 问题,还是 evaluator 问题?
  7. 是否可以把失败 artifact set、artifact、part 或 trace 回流成新的 dataset item?

10.12.3 Query 设计

兼容型 MVP 可以这样查:

text
DatasetRun
  -> DatasetRunItem
  -> traceId
  -> scores
  -> score.metadata.artifactSetId / artifactId / artifactPartId
  -> ExperimentArtifactSet / ExperimentArtifact / ArtifactPart metadata

长期一等 artifact target 后,可以这样查:

text
ExperimentArtifactSet
  -> score target artifact_set_id
  -> ExperimentArtifact / ArtifactPart
  -> datasetRunItem
  -> datasetRun
  -> candidateVersion

第二种查询更自然,也更适合 artifact set comparison。


10.13 和当前三个子系统的关系

10.13.1 接 9.2 Ingestion / Event Store

9.2 讲的是事实数据如何进入 ClickHouse。

本方案复用它来保存:

  • Claude Code / Codex 的 trace;
  • model calls;
  • tool calls;
  • shell commands;
  • test execution observation;
  • runner 自身关键事件;
  • score 写入事件。

不改动原则:

text
运行过程事实仍进入 Event Store。
Artifact 大内容不塞进 Event Store。
Artifact metadata 通过 trace metadata 和 Postgres 关系连接。

10.13.2 接 9.3 Experiment

9.3 讲的是 Dataset -> DatasetRun -> DatasetRunItem -> Trace -> Score

本方案把它扩展成:

text
Dataset
  -> DatasetRun
  -> DatasetRunItem
  -> RunnerJob
  -> Trace
  -> ExperimentArtifactSet
  -> ExperimentArtifact / ArtifactPart
  -> Score

Prompt 不再是唯一 candidate。SkillVersion 是新的 candidate type。

10.13.3 接 9.4 Evaluation

9.4 讲的是 evaluator、score config、evaluation rule、score analytics。

本方案复用:

  • ScoreConfig 定义 score schema;
  • evaluator 执行链路;
  • score ingestion;
  • experiment comparison;
  • score analytics。

新增的是:

text
Evaluator target 需要支持 artifact set / artifact / artifact part。
Evaluator input 需要能读取 artifact set、artifact content 和 part content。
Score 需要能表达 artifact set target,或至少 metadata artifactSetId / artifactId / artifactPartId。

10.14 关键设计决策

10.14.1 不把 Skill 当 Prompt

Prompt 是文本模板和模型参数。Skill 是 markdown + code + 调用约定 + 文件结构 + runtime hints。

如果把 skill 塞进 prompt,会产生几个问题:

  • 版本语义不对;
  • 文件 bundle 很难表达;
  • code artifact 难以 hash;
  • runner 装载约定混乱;
  • prompt comparison 和 skill comparison 混在一个概念里。

所以应新增 CandidateAsset(type=skill),而不是复用 Prompt 表。

10.14.2 DatasetRun 继续作为 Experiment

当前 OSS 已经把 dataset run 作为 experiment run 的核心对象。它和 prompt 并不强绑定,所以应继续复用。

新增一张 experiments 表反而会造成概念重复:

text
DatasetRun 是一次 dataset 运行。
Experiment 是产品语义。
两者可以是一回事。

如果需要更多配置,可以补 ExperimentSpec,但不要另起一套 parallel experiment model。

10.14.3 Artifact Set 一等建模

artifact set 是 experiment eval 的主对象。它不能只作为 trace output 的一个字段存在,也不能简化成“一个文件”。

原因:

  • artifact set 是某个 item run 的完整输出边界;
  • artifact set 下可能有多个 artifact;
  • artifact 下可能有多个文件、blob 或片段;
  • artifact / part 需要 hash、URI、类型和 schema;
  • artifact set 是 score 的默认主目标;
  • artifact set 是团队评审和回放的对象。

10.14.4 Runner 独立于 Langfuse web / worker

Claude Code / Codex runner 可能执行代码、访问 repo、运行 shell、消耗大量 CPU/网络/token。它不应该在 web 进程里执行。

推荐:

text
Langfuse web/worker 负责控制面和队列。
Runner service 负责受控执行。
Runner 通过 API / SDK / OTel 与 Langfuse 连接。

这样可以独立扩缩容、隔离权限、控制成本。

10.14.5 Score 先兼容,后升级 target

短期最小改动:

text
score.traceId = artifact set 对应 trace
score.datasetRunId = experiment id
score.metadata.artifactSetId = artifact set id
score.metadata.artifactId = artifact id, optional
score.metadata.artifactPartId = artifact part id, optional

长期正确模型:

text
score.targetType = experiment_artifact_set
score.targetId = artifact_set_id
score.context.traceId = trace id
score.context.datasetRunId = run id
score.context.datasetRunItemId = run item id

如果 evaluator 明确只评价某个逻辑产物或某个文件片段,长期模型可以把 targetType 切到 experiment_artifactexperiment_artifact_part。但默认主质量分仍应落在 experiment_artifact_set,这样 comparison 的主行才稳定。

这应该作为两个阶段,而不是第一版就大改 score schema。


10.15 MVP 分期

Phase 1:兼容型 MVP

目标:不大改 Langfuse score schema,先跑通 skills eval 闭环。

范围:

  • 新增 CandidateAsset / CandidateVersion
  • 支持上传 skill bundle;
  • 使用 DatasetRuns.metadata 保存 experiment spec;
  • 新增 RunnerJob
  • 新增 ExperimentArtifactSet / ExperimentArtifact / ArtifactPart metadata;
  • runner 支持 Claude Code;
  • artifact 内容存 blob;
  • score 写现有 trace_id / dataset_run_id,metadata 带 artifactSetId,必要时带 artifactId / artifactPartId
  • comparison 页面能按 artifact set 展示分数,并能展开 artifact / part 和 trace。

这阶段的核心验证:

text
同一个 Dataset
  用 SkillVersion A 跑一次
  用 SkillVersion B 跑一次
  每个 item 产生 artifact set
  evaluator 对 artifact set 打分
  UI 能比较 A/B 哪个更好

Phase 2:ArtifactSet / Artifact score target 一等化

目标:让 artifact set / artifact / artifact part 成为 score target。

范围:

  • 扩展 score ingestion schema;
  • ClickHouse scores 增加 artifact_set_id / artifact_id / artifact_part_id,或统一 target_type / target_id
  • query builder 支持 artifact set / artifact / part score;
  • experiment comparison 以 artifact set 为主 join;
  • score analytics 能筛选 artifact set / artifact / artifact part target;
  • evaluator rule 支持 target artifact set / artifact / part。

这阶段会动到核心 score 查询,风险更高,应该在 Phase 1 验证产品价值后再做。

Phase 3:多 runtime 和更强分析

目标:把 runtime 和 candidate 都泛化。

范围:

  • 支持 Codex runner;
  • 支持自定义 agent runtime;
  • 支持多 artifact 类型 diff viewer;
  • 支持 artifact part 级 diff viewer;
  • 支持 skill version diff;
  • 支持自动 regression gate;
  • 支持失败 case 回流 dataset;
  • 支持成本/质量 Pareto 分析。

10.16 风险与缓解

风险表现缓解
sandbox 安全skill 或 agent 执行任意代码。runner 隔离、最小权限、网络策略、只注入短期 token。
复现困难之后无法知道当时跑的是什么 skill / runtime。SkillVersion immutable、contentHash、runtime image、model、env、dataset version 全部记录。
trace 关联丢失artifact set 有了,但找不到生产过程。runner 必须注入 traceId 和 correlation metadata。
artifact / part 过大patch/log/workspace diff 太大影响 DB。大内容进 blob,Postgres 只存 metadata 和 URI。
score target 混乱同一分数有时打 trace,有时打 artifact set 或 artifact。统一约定:主质量分打 artifact set,细分诊断分打 artifact / part,过程分打 trace / observation,汇总分打 dataset run。
evaluator 不稳定LLM judge 自身漂移。evaluator version、prompt hash、model 记录到 score metadata;关键指标优先用 deterministic evaluator。
成本失控runner 并发、模型调用、测试执行成本高。queue concurrency、budget、timeout、max turns、per project quota。
部分失败某些 item 失败导致整个 run 不可用。item 级状态、artifact set 保留 failed 状态和 error,artifact 可为空,comparison 展示 failed item。
数据泄露repo、prompt、artifact、trace 包含敏感信息。project 权限、artifact retention、secret scrubber、最小化日志。

10.17 需要团队拍板的问题

这些是进入实现前必须明确的决策:

  1. ArtifactSet target 什么时候一等化? Phase 1 是否接受 score.metadata.artifactSetId / artifactId / artifactPartId 的兼容方案?

  2. Skill bundle 的来源是什么? UI 上传、Git repo 引用、对象存储导入,还是三者都支持?

  3. Runner 放在哪里? 独立 service、K8s Job、Temporal/Workflow、还是现有 worker 派生?

  4. Claude Code / Codex 的权限边界是什么? 是否允许联网、写文件、跑 shell、访问私有 repo?

  5. Artifact 保留策略是什么? 保存多久、是否可删除、删除后 score 如何展示?

  6. Evaluator suite 如何版本化? evaluator prompt/code/model 变化后,旧 score 是否仍可比较?

  7. Comparison 的第一批主指标是什么? 建议先定 3 到 5 个,不要第一版做过多指标。


10.18 最小可用闭环

最终 MVP 应该能完成这条闭环:

这条闭环跑通以后,团队就可以用 Langfuse OSS 的稳定底层能力做 skills 迭代:

text
下层:Event Store / Score / DatasetRun 保持通用稳定。
中层:CandidateVersion / ExperimentArtifactSet / ExperimentArtifact / ArtifactPart 让 experiment 可复现、可比较。
上层:Claude Code / Codex runner 和 skill 工作流贴近业务快速变化。

10.19 源码对应关系

当前 OSS 可参考的源码入口:

主题源码位置说明
Dataset / DatasetRun schemapackages/shared/prisma/schema.prismaDatasetDatasetItemDatasetRunsDatasetRunItems
Score schemapackages/shared/prisma/schema.prismapackages/shared/clickhouse/migrations/*/0003_scores.up.sqlScore 基础模型。
Score dataset_run_idpackages/shared/clickhouse/migrations/*/0017_add_run_id_column_scores.up.sqlrun-level score 支持。
Dataset run item ingestionworker/src/services/IngestionService/index.ts将 run item 事件补全后写入 ClickHouse。
Experiment comparison 查询packages/shared/src/server/repositories/experiments.tsexperiment 级 score filter 和比较逻辑。
Dataset run item 查询packages/shared/src/server/repositories/dataset-run-items.ts通过 dataset_run_items_rmt 聚合 trace scores。
Evaluation 子系统worker/src/features/evaluation/**evaluator 执行、score event、job execution。
Queue 契约packages/shared/src/server/queues.ts新增 runner queue 时应遵循的位置。

读源码时建议按这个顺序:

text
schema.prisma
  -> clickhouse migrations
  -> IngestionService dataset run item / score 写入
  -> experiments repository
  -> evaluation worker
  -> UI comparison 页面

这样能看清当前 OSS 已经支持什么,再判断新增 artifact/candidate/runner 应该插在哪一层。


10.20 本方案的不变量

最后把不变量列清楚,后续实现时不能破坏:

  1. SkillVersion immutable:被 experiment 引用后不能原地修改。
  2. DatasetRun 是 experiment run:不要另造一套平行 experiment 主模型。
  3. ArtifactSet 是评价主目标:主质量分主要打 artifact set,不要只打 trace。
  4. Artifact 可以下钻:artifact / artifact part 用于细分诊断,不要替代 artifact set 成为默认 comparison 主体。
  5. Trace 是过程证据:trace/observation 用于解释 artifact set 如何产生。
  6. Score 是统一评价事实:不要新建一套 eval result 表绕开 score analytics。
  7. Runner 是可替换执行器:Claude Code / Codex 是 runtime 实现,不是底层数据模型。
  8. 大对象进 blob:skill bundle、patch、log、workspace diff 不进关系表大字段。
  9. 队列只传轻量指针:RunnerJob id 比完整 payload 更适合跨进程契约。
  10. correlation metadata 必须完整:trace、artifact set、artifact、part、score 必须能回到 dataset run、item、candidate version。
  11. 下层通用,上层业务化:不要让 skill 特有字段污染 Event Store / Score 的基础抽象。