当 AI 能力进入真实工程后,团队很快会遇到同一个瓶颈:
- 这次 Prompt 改动到底是变好还是变差?
- 为什么线上突然出现“看起来随机”的质量波动?
- 两个模型版本怎么公平对比?
- 失败案例怎么沉淀成下一次优化输入?
如果这些问题只能靠感觉回答,你就还没有真正进入“工程化 AI 开发”。
这篇作为 Harness 系列第一篇,只做一件事:搭出一个可跑、可复现、可对比的评测基线。
一、为什么必须先做 Harness
很多团队会先做路由、自治、Multi-Agent 编排,但没有 Harness,这些优化几乎都不可验证。
常见后果:
- 优化靠主观判断
- “感觉快了”“感觉更聪明了”,但没有稳定数据。
- 回归无法提前发现
- 某次小改动导致关键场景退化,只在生产后才暴露。
- 团队协作失真
- A 说模型变好,B 说变差,最终无法达成工程共识。
Harness 的本质不是“多跑几条测试”,而是建立统一、稳定、可追溯的质量证据链。
二、Harness 最小架构(MVP)
先用最小结构跑起来,不要上来就做复杂平台:
Case:标准化样本输入Runner:统一执行流程Judge:统一打分逻辑Report:统一输出结果Baseline:可对比历史版本
testcases/ -> Runner -> Model/Agent -> Judge -> report.json/report.md
|
+-> baseline compare
只要这条链路稳定,你后续做任何优化都能被量化。
三、样本集设计:先覆盖“高价值失败点”
第一版不追求样本量大,追求样本结构合理。
建议三层:
- 黄金样本(Golden Cases)
- 团队确认的标准答案场景,用于稳定基线。
- 历史失败样本(Failure Replay)
- 来自线上事故或返工案例,优先覆盖。
- 对抗样本(Adversarial Cases)
- 刻意构造歧义、越权、脏输入,验证安全边界。
样本 JSON 示例
{
"id": "case_001",
"workflow": "code_review",
"input": "请审查这段并发代码是否有竞态风险",
"context": {
"language": "csharp",
"policy": "review-v2"
},
"expected": {
"must_include": ["共享状态", "锁粒度"],
"must_not_include": ["无依据断言"]
},
"weight": 1.0,
"risk_level": "high"
}
四、指标体系:先小而硬,不要大而虚
第一版建议只保留 5 个核心指标:
PassRate:规则通过率ActionExecutability:输出可执行率PolicyViolationRate:策略违规率P95LatencyMs:尾延迟CostPerPassedCase:每个通过样本成本
这 5 个指标能同时覆盖:质量、稳定性、合规、成本。
五、C# 最小 Runner 骨架
先实现一个 CLI Runner,后续再接 CI/CD。
using System.Text.Json;
public sealed class HarnessCase
{
public string Id { get; set; } = string.Empty;
public string Workflow { get; set; } = string.Empty;
public string Input { get; set; } = string.Empty;
public Dictionary<string, string> Context { get; set; } = new();
public ExpectedSpec Expected { get; set; } = new();
}
public sealed class ExpectedSpec
{
public List<string> MustInclude { get; set; } = new();
public List<string> MustNotInclude { get; set; } = new();
}
public sealed class HarnessResult
{
public string CaseId { get; set; } = string.Empty;
public bool Passed { get; set; }
public double LatencyMs { get; set; }
public double CostUsd { get; set; }
public List<string> FailedChecks { get; set; } = new();
}
public static class HarnessRunner
{
public static async Task<int> RunAsync(string caseDir)
{
var files = Directory.GetFiles(caseDir, "*.json");
var results = new List<HarnessResult>();
foreach (var file in files)
{
var json = await File.ReadAllTextAsync(file);
var testCase = JsonSerializer.Deserialize<HarnessCase>(json)!;
var started = DateTime.UtcNow;
var output = await CallModelAsync(testCase); // 你的模型/Agent 调用入口
var latency = (DateTime.UtcNow - started).TotalMilliseconds;
var result = Judge(testCase, output, latency, costUsd: 0.0);
results.Add(result);
}
await WriteReportAsync(results);
return 0;
}
private static Task<string> CallModelAsync(HarnessCase c)
{
// MVP 阶段可先用 mock,确保框架先跑通
return Task.FromResult($"mock output for {c.Id}");
}
private static HarnessResult Judge(HarnessCase c, string output, double latencyMs, double costUsd)
{
var failed = new List<string>();
foreach (var token in c.Expected.MustInclude)
{
if (!output.Contains(token, StringComparison.OrdinalIgnoreCase))
{
failed.Add($"missing:{token}");
}
}
foreach (var token in c.Expected.MustNotInclude)
{
if (output.Contains(token, StringComparison.OrdinalIgnoreCase))
{
failed.Add($"forbidden:{token}");
}
}
return new HarnessResult
{
CaseId = c.Id,
Passed = failed.Count == 0,
LatencyMs = latencyMs,
CostUsd = costUsd,
FailedChecks = failed
};
}
private static async Task WriteReportAsync(List<HarnessResult> results)
{
var passRate = results.Count == 0 ? 0 : results.Count(r => r.Passed) * 1.0 / results.Count;
var report = new
{
total = results.Count,
passRate,
p95LatencyMs = Percentile(results.Select(r => r.LatencyMs).ToList(), 95),
costPerPassed = results.Count(r => r.Passed) == 0
? 0
: results.Sum(r => r.CostUsd) / results.Count(r => r.Passed),
failures = results.Where(r => !r.Passed).Take(20).ToList()
};
var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync("report.json", json);
}
private static double Percentile(List<double> values, int p)
{
if (values.Count == 0) return 0;
values.Sort();
var rank = (int)Math.Ceiling((p / 100.0) * values.Count) - 1;
rank = Math.Clamp(rank, 0, values.Count - 1);
return values[rank];
}
}
这段骨架先解决“跑得通 + 有结果 + 可复盘”,后续再替换真实模型调用与更强评分器。
六、报告输出:先保证“可比较”
建议每次执行产出两类报告:
report.json:给机器读(CI 门禁、趋势分析)report.md:给人读(失败 TopN、回归变化)
report.md 示例
# Harness Report
- total: 120
- passRate: 0.9167
- p95LatencyMs: 1820
- costPerPassed: 0.14
## Failed Top 5
1. case_019 missing:锁粒度
2. case_044 forbidden:无依据断言
3. case_078 missing:回滚策略
关键要求:每次报告都要带 baselineVersion 和 runId,否则无法长期对比。
七、一周落地清单(可直接执行)
Day 1-2
- 定义首批 50 条样本(黄金 20 + 失败回放 20 + 对抗 10)
Day 3
- 接入 MVP Runner,稳定产出
report.json
Day 4
- 建立基线版本
baseline-v1
Day 5
- 将 Harness 纳入 PR/发布前门禁(先告警、后阻断)
Day 6-7
- 做首轮复盘,补齐高频失败样本与策略规则
八、第一篇的结论
Harness 不是锦上添花,而是 AI 工程化的起点。
只要你把“样本标准化、执行可重复、结果可对比、失败可追溯”这四件事做扎实,后续所有优化(模型路由、Agent 编排、Auto-Run)才有真实增益。
下一篇我们进入《Harness 实战 02:评测样本工程(从手工案例到可持续数据飞轮)》,解决样本老化、覆盖不足与标注成本失控问题。