第 10 篇 · Skills Eval 技术方案
本篇是 【扩展设计】,不是当前 Langfuse OSS 已经完整实现的功能说明。它的目的,是基于当前 OSS 的抽象,设计一套“用 Langfuse 做 skills 迭代评估”的技术方案,供团队讨论和拆任务。
10.1 一页结论
我们要做的不是“给 Langfuse 加一个新的 prompt 类型”,而是在 Langfuse OSS 已有的事实层、实验层、评价层之上,补齐三块能力:
- 候选资产管理:把
skills当成可版本化、可复现、可比较的CandidateAsset。 - 受控运行环境:用
ExperimentRuntimeRunner拉起 Claude Code / Codex / 其他 agent runtime,消费某个 skill 版本和 dataset item。 - 实验产物建模:每次 dataset item 执行后产生
ExperimentArtifactSet,里面可以包含多个ExperimentArtifact和更细的ArtifactPart;evaluation 的默认主目标是 artifact set,而 trace / observation 是 artifact set 的生产过程证据。
核心公式是:
Dataset
+ SkillVersion
+ RuntimeSpec
+ EvaluatorSuite
-> DatasetRun / Experiment
-> RunnerJob per DatasetItem
-> Trace / Observation tree
-> ExperimentArtifactSet
-> ArtifactSet Score
-> Comparison / Analysis这里最关键的判断是:
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。 |
所以这个方案的原则是:
不要重写 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 总图:
- 站点可访问版本:architecture/skills-eval-system-architecture.drawio
- 源文件位置:
architecture/skills-eval-system-architecture.drawio
这张图按“控制面、运行环境、结果回收、存储落点、评分主语”组织。读图时先看左侧 Langfuse 控制面如何创建 ExperimentSpec / DatasetRun / RunnerJob,再看右侧 sandbox 如何运行 skill 和 app,最后看 Result Package 如何回到 ArtifactSet、Trace / Observation、Score 和 Run Analysis。
10.2 方案边界
本篇会同时出现 【当前实现】 和 【扩展设计】。读的时候要先分清边界。
| 类型 | 范围 | 例子 |
|---|---|---|
| 【当前实现】 | 当前 Langfuse OSS repo 已经存在的实体、表、队列、查询或 UI 抽象。 | Dataset、DatasetRuns、DatasetRunItems、scores、traces、observations、dataset_run_items_rmt。 |
| 【扩展设计】 | 为 skills eval 需要新增或泛化的实体、接口和控制面。 | CandidateAsset、SkillVersion、ExperimentRuntimeRunner、RunnerJob、ExperimentArtifactSet、ExperimentArtifact、ArtifactPart、artifact_set_id / artifact_id / artifact_part_id score target。 |
本方案不做三件事:
- 不把 Langfuse 改造成通用 CI/CD 系统。
- 不重写 ClickHouse event store。
- 不把 Claude Code / Codex 的内部循环逻辑塞进 Langfuse web 进程。
Langfuse 在这个方案里承担的是:
实验定义
+ 运行事实采集
+ artifact set / artifact / part 关系管理
+ score 落库
+ comparison / analysisRunner 承担的是:
准备受控环境
+ 注入 skill
+ 执行 dataset item
+ 采集产物
+ 上报 trace / observation
+ 回写 artifact set / artifact / part metadata10.3 为什么 artifact 是主语
在 prompt experiment 里,很多时候可以把“最终 answer”直接理解成 trace output。于是系统看起来像这样:
DatasetItem
-> prompt completion
-> trace output
-> score但 skills / agent eval 不是这样。一个 skill 执行后可能产生:
- 最终回答;
- 修改后的文件 patch;
- 新增文件;
- 测试报告;
- 命令执行结果;
- structured JSON result;
- repo workspace diff;
- agent 生成的计划、审计报告或修复说明。
这些都是 artifact。它们不等同于 trace,也不应该被建模成“一个文件”。
更准确的抽象是三层:
ExperimentArtifactSet
-> ExperimentArtifact[]
-> ArtifactPart[]| 层级 | 含义 | 例子 |
|---|---|---|
ExperimentArtifactSet | 某个 DatasetRunItem 的完整产物集合。 | 一次 Claude Code / Codex 执行后得到的 answer + patch + test report + logs。 |
ExperimentArtifact | 产物集合中的一个逻辑产物。 | final_answer、patch、test_report、workspace_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 的核心关系应该是:
这会带来一个重要设计原则:
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 编排”“产品流程”混成一团。
分层原则是:
下层决定结构:稳定、通用、少业务假设。
上层决定应用:灵活、面向业务流程、可以快速变化。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 怎么连接 trace | L2 | 所有 experiment 都需要 item-result 连接。 |
| skill bundle 如何版本化 | L3 | 这是 candidate asset 管理,不属于 event store。 |
| Claude Code 怎么启动 | L4 | runtime 可替换,不应该污染 Dataset / Score 基础模型。 |
| UI 里怎么选择 skill 和 model | L5 | 产品流程随业务变化最快。 |
| artifact set / artifact / part 文件放哪里 | L0 + L3 | blob 存内容,Postgres 存 metadata 和关系。 |
10.5 当前 OSS 抽象如何承接这个方案
10.5.1 Dataset / DatasetItem
【当前实现】 Dataset 和 DatasetItem 在 Postgres 中表达测试材料。
Dataset
id
projectId
name
metadata
inputSchema
expectedOutputSchema
DatasetItem
id
datasetId
input
expectedOutput
metadata
sourceTraceId?
sourceObservationId?
validFrom / validTo在 skills eval 中,DatasetItem.input 应该描述“要让 agent 完成的任务”。例如:
{
"repo": "example/service-a",
"task": "修复 flaky test: tests/payment/refund.test.ts",
"constraints": {
"allowedCommands": ["pnpm test", "pnpm lint"],
"maxTurns": 40
}
}DatasetItem.expectedOutput 不一定只是字符串答案,可以是评价标准:
{
"mustPassTests": ["tests/payment/refund.test.ts"],
"mustNotModify": ["packages/shared/src/auth/**"],
"expectedBehavior": "退款失败时应保留原订单状态"
}10.5.2 DatasetRun / Experiment
【当前实现】 DatasetRuns 表达一次 experiment run。当前字段比较通用:
DatasetRuns
id
projectId
name
description
metadata
datasetId它没有强绑定 prompt,所以可以承接 skills eval。关键是要把 run 的不可变配置写清楚:
{
"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。长期更清晰的做法是新增 ExperimentSpec 或 ExperimentRuntimeConfig 表,避免 metadata 成为不可查询的大 JSON。
10.5.3 DatasetRunItem
【当前实现】 DatasetRunItems 现在是 experiment item 和 trace 的连接表:
DatasetRunItems
id
datasetRunId
datasetItemId
traceId
observationId?这对 prompt experiment 够用,但 skills eval 还缺 artifact set 连接。
【扩展设计】 不建议直接把所有 artifact 字段塞进 DatasetRunItems,而是新增 ExperimentArtifactSet,再从 set 下挂多个 artifact:
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 可以表达:
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_id、observation_id、session_id、dataset_run_id 等维度,experiment comparison 也会围绕 score 做聚合。
【扩展设计】 skills eval 应该把 score 分成六个层级:
| Score 层级 | Target | 用途 |
|---|---|---|
| Artifact set score | artifact_set_id | 最核心,评价某个 dataset item run 的整体产物质量。 |
| Artifact score | artifact_id | 评价某个逻辑产物,例如 patch、final answer、test report。 |
| Artifact part score | artifact_part_id | 评价某个文件、diff 片段或报告片段,主要用于诊断。 |
| Trace score | trace_id | 评价整次运行过程,例如是否超时、是否过度调用工具。 |
| Observation score | observation_id | 评价某一步,例如 retrieval 是否命中、tool call 是否正确。 |
| Experiment score | dataset_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 是“被拿来评估的候选方案”的通用抽象。
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 的不可变版本。它应该能回答:
这次 experiment 到底用了哪一组 markdown / code 文件?
这些文件的 hash 是什么?
之后能不能复现?建议字段:
CandidateVersion
id
projectId
assetId
version
contentHash
artifactUri
manifest
status // draft | active | archived
createdBy
createdAtmanifest 示例:
{
"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
}
}不变量:
一旦 SkillVersion 被 experiment 引用,内容不能再变。
如果文件变了,必须生成新的 version 和 contentHash。10.6.3 ExperimentSpec
ExperimentSpec 是一次 experiment 的不可变运行说明。兼容 MVP 可以放在 DatasetRuns.metadata,长期建议一等表。
ExperimentSpec
datasetRunId
candidateVersionId
runtimeType
runtimeConfig
evaluatorSuiteId
createdAt示例:
{
"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 是编排层状态,不是长期分析事实。
RunnerJob
id
projectId
datasetRunId
datasetItemId
candidateVersionId
runtimeType
status // pending | running | artifact_ready | evaluated | failed
traceId?
datasetRunItemId?
error?
attempts
startedAt?
finishedAt?它应该放 Postgres 或专门的 job state 表,队列只传递轻量 payload:
{
"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。
ExperimentArtifactSet
id
projectId
datasetRunId
datasetRunItemId
datasetItemId
candidateVersionId
runnerJobId
traceId
status // complete | partial | failed
summary
metadata
createdAtsummary 用来给 comparison 页面快速展示,不放大对象内容:
{
"artifactTypes": ["final_answer", "patch", "test_report", "workspace_diff"],
"artifactCount": 4,
"changedFiles": 2,
"testsPassed": 18,
"testsFailed": 0
}不变量:
一个 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。
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
createdAtartifact 内容可以有三种形态:
| 形态 | 存储方式 | 例子 |
|---|---|---|
| 内联小对象 | Postgres JSON metadata | final answer、短 JSON result。 |
| 单 blob 大对象 | Blob/S3 + Postgres metadata | patch 文件、测试日志、压缩包。 |
| 多 part 对象 | ArtifactPart[] + blob | workspace diff、多文件 patch、分段测试报告。 |
artifact metadata 示例:
{
"type": "patch",
"baseRevision": "abc123",
"changedFiles": [
"src/payment/refund.ts",
"tests/payment/refund.test.ts"
],
"summary": {
"filesChanged": 2,
"insertions": 45,
"deletions": 12
}
}不变量:
Artifact 是 artifact set 下的逻辑产物。
Artifact 不要求是一份文件。
Artifact 一旦被 score 引用,不应该原地覆盖。
如果逻辑产物重算,生成新的 artifact 或新的 artifact set。10.6.7 ArtifactPart
ArtifactPart 表示某个 artifact 内部的文件、blob、片段或结构化子对象。它主要解决“artifact 可能不只是一个文件”的问题。
ArtifactPart
id
projectId
artifactId
artifactSetId
kind // file | blob | json_path | diff_hunk | log_chunk | metric
path?
uri?
contentHash?
mimeType?
sizeBytes?
metadata
createdAt例子:
{
"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
}
}不变量:
ArtifactPart 只表达 artifact 内部结构。
ArtifactPart 不是默认 comparison 主体。
只有需要细粒度诊断时,才对 part 打 score。10.7 数据关系图
10.7.1 业务实体关系
读这张图时注意:
CandidateVersion是输入候选资产。ExperimentArtifactSet是输出实验产物集合,也是默认评价主目标。ExperimentArtifact是集合里的逻辑产物,ArtifactPart是产物内部的文件或片段。Trace / Observation是过程证据。Score应该默认贴到 artifact set,也可以贴到 artifact、artifact part、trace、observation 或 dataset run。
10.7.2 数据流关系
10.8 存储设计
10.8.1 存储落点
10.8.2 什么放哪里
| 数据 | 建议存储 | 原因 |
|---|---|---|
| skill 名称、描述、owner | Postgres | 关系型控制面,可权限检查、可列表查询。 |
| skill version manifest | Postgres | 小 JSON,参与 experiment spec 和审计。 |
| skill bundle 内容 | Blob/S3 | markdown/code 可能较大,且需要按 hash 复现。 |
| dataset / dataset item | Postgres | 当前 OSS 已有,保留。 |
| dataset run / run item | Postgres + ClickHouse 投影 | 当前 OSS 已有,run item 关系需要分析聚合。 |
| runner job 状态 | Postgres | 需要可靠状态机、重试、错误展示。 |
| runner queue payload | Redis/BullMQ | 短期调度,不作为事实库。 |
| trace / observation | ClickHouse Event Store | 高吞吐过程事实。 |
| artifact set / artifact / part metadata | Postgres | artifact set 是关系对象,要和 run item、score、candidate version 关联;artifact/part 用于表达多产物和多文件结构。 |
| artifact / part 大内容 | Blob/S3 | patch/report/log/workspace diff 可能很大。 |
| score | ClickHouse scores | 评价事实,需要聚合、筛选、分析。 |
10.8.3 为什么 artifact metadata 不直接放 ClickHouse
artifact set / artifact / part 是关系对象,不只是分析事实。它需要:
- 被权限系统保护;
- 被 UI 列表查询;
- 和
DatasetRunItem、RunnerJob、CandidateVersion建立关系; - 表达一个 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 和代码文件。系统做:
- 校验文件结构;
- 计算文件 hash 和整体 content hash;
- 保存 bundle 到 blob;
- 创建
CandidateAsset和SkillVersion。
这一步不运行实验,只是在建立候选资产。
Step 2:创建 experiment
用户选择:
- dataset;
- skill version;
- runtime 类型;
- model;
- sandbox / timeout / max turns;
- evaluator suite。
系统创建 DatasetRun,并记录不可变 ExperimentSpec。
Step 3:生成 RunnerJob
每个 active DatasetItem 生成一个 RunnerJob。这一步是控制面状态,不是运行事实。
DatasetRun
-> RunnerJob(item_1)
-> RunnerJob(item_2)
-> RunnerJob(item_3)Step 4:Runner 执行
Runner 根据 job 准备环境:
- 下载 skill bundle;
- 拉取或准备 repo workspace;
- 注入 dataset item input;
- 配置 Langfuse tracing 环境变量;
- 启动 Claude Code / Codex;
- 限制资源、超时、工具权限;
- 捕获最终产物。
Runner 不应该把完整执行逻辑塞进 Langfuse web 进程。它应该是可水平扩展、可隔离、可替换的执行器。
Step 5:写入 trace / observation
Claude Code / Codex 运行过程通过 Langfuse integration 或 OpenTelemetry 上报,进入现有 ingestion / event store。
每条 trace 必须带上 correlation metadata:
{
"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:
ExperimentArtifactSet
-> final_answer
-> patch
-> file diff parts
-> test_report
-> junit xml part
-> stdout log part
-> workspace_diff
-> changed file parts
-> runner_logs然后创建 ExperimentArtifactSet、ExperimentArtifact、ArtifactPart metadata,更新 RunnerJob 状态,确保 artifact set 与 DatasetRunItem、Trace、CandidateVersion 关联。
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。
最终回答:
哪个 SkillVersion 在同一组 DatasetItem 上表现更好?
哪些 case 退化了?
退化的 artifact set 是什么?
artifact set 里哪个 artifact 或 part 出问题?
生产 artifact set 的 trace 显示哪里出了问题?10.10 核心接口契约
10.10.1 ExperimentRuntimeRunner
Runner 接口应该稳定,具体 runtime 可以替换。
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。
{
"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 的完整产物集合。
{
"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:
{
"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:
{
"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 更清楚。
Evaluator / Scorer = 评分定义,说明怎么评、评谁、什么时候评。
ScoreConfig = 分数格式,说明 score 是 numeric / boolean / categorical / text。
Score = 评分结果,是最终落库的评价事实。
ExperimentArtifactSet = 默认被评分对象。
Trace / Observation = 评分时可读取的过程上下文。所以扩展后的关系是:
Evaluator 定义“怎么打分”
Evaluation Rule 定义“artifact set ready 后评谁”
Evaluator Runtime 执行规则、代码或 LLM judge
Score 保存“评出来什么”Evaluator 可以有多种实现:
| 类型 | 怎么打分 | 例子 |
|---|---|---|
| deterministic evaluator | 用规则、脚本、测试结果计算 | tests_passed、lint_passed、patch_applies。 |
| LLM-as-judge evaluator | 用 LLM 读取 artifact set、expected output、trace 上下文后判断 | patch_quality、instruction_following、answer_helpfulness。 |
| hybrid evaluator | 先跑规则,再让 LLM 解释或补充分数 | 测试通过后再评代码质量。 |
| external evaluator | 调用外部服务或 CI 平台 | 安全扫描、性能 benchmark。 |
| human annotation | 人工审核 artifact set 或 artifact | reviewer acceptability。 |
Evaluator definition 的核心字段可以这样理解:
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 示例:
{
"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。它应该看完整上下文:
DatasetItem.input
DatasetItem.expectedOutput
ExperimentArtifactSet
ExperimentArtifact
ArtifactPart
Trace / Observation tree
Runner logs
Runtime metadata不同 evaluator 读取不同输入:
| Evaluator | 主要输入 | 输出 score |
|---|---|---|
| 测试通过率 | artifact set 里的 test report | tests_passed |
| patch 质量 | artifact set 里的 patch + trace context | patch_quality |
| 指令遵循 | final answer artifact + dataset expectedOutput | instruction_following |
| 成本效率 | trace / observation usage | cost_efficiency |
| 工具使用合理性 | observation tree | tool_use_quality |
| 安全检查 | patch / final answer / file diff | safety |
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 聚合得到。
artifact set scores per item
-> group by datasetRunId
-> calculate pass rate / mean / p50 / p95 / regression count
-> write dataset_run_id score例如:
{
"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:
同一个 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 | 分数。 |
scoreComment | evaluator 解释。 |
runtimeType | Claude Code / Codex 等。 |
model | 使用的模型。 |
10.12.2 UI 上应该回答的问题
对团队有价值的页面,不是只显示“平均分 0.82”,而是回答:
- 哪个 skill version 总体最好?
- 哪些 dataset item 上新版本退化?
- 退化 item 对应的 artifact set 是什么?
- artifact set 里具体哪个 artifact / part 出问题?
- 这个 artifact set 是怎么生成的,trace 里哪一步有问题?
- 是 skill 问题、runtime 问题、model 问题,还是 evaluator 问题?
- 是否可以把失败 artifact set、artifact、part 或 trace 回流成新的 dataset item?
10.12.3 Query 设计
兼容型 MVP 可以这样查:
DatasetRun
-> DatasetRunItem
-> traceId
-> scores
-> score.metadata.artifactSetId / artifactId / artifactPartId
-> ExperimentArtifactSet / ExperimentArtifact / ArtifactPart metadata长期一等 artifact target 后,可以这样查:
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 写入事件。
不改动原则:
运行过程事实仍进入 Event Store。
Artifact 大内容不塞进 Event Store。
Artifact metadata 通过 trace metadata 和 Postgres 关系连接。10.13.2 接 9.3 Experiment
9.3 讲的是 Dataset -> DatasetRun -> DatasetRunItem -> Trace -> Score。
本方案把它扩展成:
Dataset
-> DatasetRun
-> DatasetRunItem
-> RunnerJob
-> Trace
-> ExperimentArtifactSet
-> ExperimentArtifact / ArtifactPart
-> ScorePrompt 不再是唯一 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。
新增的是:
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 表反而会造成概念重复:
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 进程里执行。
推荐:
Langfuse web/worker 负责控制面和队列。
Runner service 负责受控执行。
Runner 通过 API / SDK / OTel 与 Langfuse 连接。这样可以独立扩缩容、隔离权限、控制成本。
10.14.5 Score 先兼容,后升级 target
短期最小改动:
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长期正确模型:
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_artifact 或 experiment_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/ArtifactPartmetadata; - runner 支持 Claude Code;
- artifact 内容存 blob;
- score 写现有
trace_id/dataset_run_id,metadata 带artifactSetId,必要时带artifactId/artifactPartId; - comparison 页面能按 artifact set 展示分数,并能展开 artifact / part 和 trace。
这阶段的核心验证:
同一个 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 需要团队拍板的问题
这些是进入实现前必须明确的决策:
ArtifactSet target 什么时候一等化? Phase 1 是否接受
score.metadata.artifactSetId/artifactId/artifactPartId的兼容方案?Skill bundle 的来源是什么? UI 上传、Git repo 引用、对象存储导入,还是三者都支持?
Runner 放在哪里? 独立 service、K8s Job、Temporal/Workflow、还是现有 worker 派生?
Claude Code / Codex 的权限边界是什么? 是否允许联网、写文件、跑 shell、访问私有 repo?
Artifact 保留策略是什么? 保存多久、是否可删除、删除后 score 如何展示?
Evaluator suite 如何版本化? evaluator prompt/code/model 变化后,旧 score 是否仍可比较?
Comparison 的第一批主指标是什么? 建议先定 3 到 5 个,不要第一版做过多指标。
10.18 最小可用闭环
最终 MVP 应该能完成这条闭环:
这条闭环跑通以后,团队就可以用 Langfuse OSS 的稳定底层能力做 skills 迭代:
下层:Event Store / Score / DatasetRun 保持通用稳定。
中层:CandidateVersion / ExperimentArtifactSet / ExperimentArtifact / ArtifactPart 让 experiment 可复现、可比较。
上层:Claude Code / Codex runner 和 skill 工作流贴近业务快速变化。10.19 源码对应关系
当前 OSS 可参考的源码入口:
| 主题 | 源码位置 | 说明 |
|---|---|---|
| Dataset / DatasetRun schema | packages/shared/prisma/schema.prisma | Dataset、DatasetItem、DatasetRuns、DatasetRunItems。 |
| Score schema | packages/shared/prisma/schema.prisma、packages/shared/clickhouse/migrations/*/0003_scores.up.sql | Score 基础模型。 |
| Score dataset_run_id | packages/shared/clickhouse/migrations/*/0017_add_run_id_column_scores.up.sql | run-level score 支持。 |
| Dataset run item ingestion | worker/src/services/IngestionService/index.ts | 将 run item 事件补全后写入 ClickHouse。 |
| Experiment comparison 查询 | packages/shared/src/server/repositories/experiments.ts | experiment 级 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 时应遵循的位置。 |
读源码时建议按这个顺序:
schema.prisma
-> clickhouse migrations
-> IngestionService dataset run item / score 写入
-> experiments repository
-> evaluation worker
-> UI comparison 页面这样能看清当前 OSS 已经支持什么,再判断新增 artifact/candidate/runner 应该插在哪一层。
10.20 本方案的不变量
最后把不变量列清楚,后续实现时不能破坏:
- SkillVersion immutable:被 experiment 引用后不能原地修改。
- DatasetRun 是 experiment run:不要另造一套平行 experiment 主模型。
- ArtifactSet 是评价主目标:主质量分主要打 artifact set,不要只打 trace。
- Artifact 可以下钻:artifact / artifact part 用于细分诊断,不要替代 artifact set 成为默认 comparison 主体。
- Trace 是过程证据:trace/observation 用于解释 artifact set 如何产生。
- Score 是统一评价事实:不要新建一套 eval result 表绕开 score analytics。
- Runner 是可替换执行器:Claude Code / Codex 是 runtime 实现,不是底层数据模型。
- 大对象进 blob:skill bundle、patch、log、workspace diff 不进关系表大字段。
- 队列只传轻量指针:RunnerJob id 比完整 payload 更适合跨进程契约。
- correlation metadata 必须完整:trace、artifact set、artifact、part、score 必须能回到 dataset run、item、candidate version。
- 下层通用,上层业务化:不要让 skill 特有字段污染 Event Store / Score 的基础抽象。