Go 基础体系 · 第 112/113 篇。示例统一基于 Go 1.26.4;核心片段可能省略 package 与 import,完整程序可直接按文中结构运行。

Go AI 评测与可观测性:数据集、Judge、Trace 和回归门禁

本文以 Go 1.26.4 为基准。AI 评测不是上线前随机问模型十个问题,也不是只看平均分。可运营的评测系统要固定任务定义、数据版本、模型与提示版本,区分确定性检查、人工标注和 LLM Judge,用置信区间判断回归,以发布门禁阻断不可接受变化,再用线上反馈发现离线集没有覆盖的新分布。

1. 先写任务契约和损失函数

同一个“回答正确”可能包含事实正确、引用忠实、格式合法、拒绝得当、语气合规和延迟成本。先列不可违反的不变量,再列可权衡目标。医疗建议越权、跨租户泄漏和执行未确认工具属于硬失败;措辞自然度通常是软指标。不同错误的业务损失不同,不能用一个总平均掩盖严重失败。

评测单位也要明确:单轮问答、完整会话、检索请求、工具调用轨迹或业务任务完成。输入、期望行为、允许变体、评分器版本和数据许可构成契约。模型版本、prompt、tool schema、索引和解码参数任一变化,都必须生成新 run。

2. 数据集是版本化产品

数据来源通常包括专家编写的核心用例、脱敏生产样本、历史失败、边界与对抗样本、合成扩充。训练集用于开发,validation 用于调参,test 只用于最终比较;同一用户、文档或会话的近重复样本不能跨集合,否则指标虚高。生产样本进入评测前需满足同意、脱敏、留存和删除要求。

{"case_id":"refund-0042","dataset_version":"support-v7","input":{"question":"订单已发货,可以立即退款吗?","tenant":"demo"},"expected":{"policy":"ask_order_status","must_cite":["policy-returns-v3#p4"],"forbidden":["promise_refund"]},"tags":["zh-CN","refund","high-risk"],"source":"expert","license":"internal-approved"}

每条 case 有稳定 ID,不靠数组位置。变更保留 review 记录:为何新增、谁标注、依据什么政策、何时过期。政策更新时先更新事实源,再新建 dataset 版本,旧版本保留以解释历史报告。

3. 防止污染、重复和时间穿越

公开 benchmark 可能已进入模型训练,不能单独代表产品质量。对内部样本做精确哈希、规范化哈希和语义近重复检测,并按来源组切分。涉及时间的问题保存 as_of,评测检索只能访问当时允许的知识快照,不能用今天资料回答去年的问题。

合成数据可以覆盖格式组合和罕见攻击,但不能替代真实分布。记录生成模型与 prompt,并由人抽检。若候选模型也是合成器或 Judge,要评估同源偏见。对测试集访问设权限,开发者看到完整答案后,该集合会逐渐变成训练反馈,应轮换隐藏 holdout。

4. 一个稳定的 Go 数据模型

评测 runner 接收不可变 case,调用被测系统,保存原始响应引用与结构化事实。接口应定义在 runner 的消费侧,并让 context 贯穿排队、检索、模型和工具。为了避免共享切片被 worker 修改,装载后复制或只读发布。

type Case struct {
	ID       string          `json:"case_id"`
	Input    json.RawMessage `json:"input"`
	Expected json.RawMessage `json:"expected"`
	Tags     []string        `json:"tags"`
}

type System interface {
	Run(context.Context, Case) (Output, error)
}

type Output struct {
	Text       string          `json:"text"`
	Citations []string        `json:"citations"`
	TraceRef   string          `json:"trace_ref"`
	Usage      Usage           `json:"usage"`
}

错误也属于输出:超时、限流、内容拒绝、协议错误与工具失败分别计数。只丢弃失败 case 会造成幸存者偏差;重试次数、最终状态和额外费用必须进入报告。

5. 确定性指标先于语义评分

凡是代码能确定的,不交给另一个模型猜。JSON Schema、必填字段、数值范围、引用 ID 是否存在、工具是否在白名单、ACL 是否满足、最大长度、延迟和 token 都可确定检查。分类任务计算 precision、recall、F1 和混淆矩阵;抽取任务可做 exact match 或字段级 F1。

type Counts struct {
	TruePositive  int
	FalsePositive int
	FalseNegative int
}

func (c Counts) Precision() float64 {
	denominator := c.TruePositive + c.FalsePositive
	if denominator == 0 {
		return 0
	}
	return float64(c.TruePositive) / float64(denominator)
}

func (c Counts) Recall() float64 {
	denominator := c.TruePositive + c.FalseNegative
	if denominator == 0 {
		return 0
	}
	return float64(c.TruePositive) / float64(denominator)
}

exact match 对开放文本过严,对 ID、日期和金额却很合适。先规范 Unicode、空白或大小写时,规范规则要版本化,不能为了提高分数不断放宽。

6. RAG 要拆开检索与生成

端到端答案差,可能是召回没有证据、重排顺序错误、上下文被裁剪,也可能是模型忽略证据。检索层测 Recall@k、MRR、nDCG、权限过滤和证据覆盖率;生成层在固定上下文上测忠实度、引用精确率与答案相关性。两层分开才能定位责任。

引用评分由程序验证 citation ID 属于本次候选,并由人或 Judge 判断引用片段是否支持具体主张。把“答案中出现链接”当引用成功没有意义。无证据 case 应期望拒答,防止高回答率掩盖幻觉。

7. 工具与 Agent 评测完整轨迹

Agent 不能只评最终文案。记录每步模型决策、工具名、参数哈希、授权结果、返回摘要、耗时、费用和终止原因。指标包括任务成功、非法工具率、参数 schema 成功率、重复调用率、平均步数、预算耗尽率、未确认写操作数和副作用幂等性。

使用确定性 fake 工具重放大部分离线轨迹,覆盖超时、未知结果、重复投递和恢复。真实沙箱契约测试数量少且有预算。凡会发信、支付、删除或改变权限的工具,评测环境默认 dry-run,不能让测试数据产生真实外部影响。

8. LLM Judge 的 rubric 必须具体

Judge 适合比较语义相关性、完整性和文风,不适合决定权限是否越界。rubric 每个维度写清 1 到 5 分锚点、必须引用的证据、无法判断的处理和输出 schema。Judge 只看到完成评分所需内容,不接触秘密 system prompt 或用户身份。

{
  "case_id": "refund-0042",
  "scores": {
    "correctness": 4,
    "evidence_support": 5,
    "instruction_following": 4
  },
  "hard_failures": [],
  "rationale": "答案要求查询订单状态,并引用了适用条款;末句略显冗长。",
  "confidence": 0.82
}

限制 rationale 长度,严格解码,未知字段拒绝。Judge 的自报 confidence 不是统计置信度。评分失败可以在预算内重试一次,但修复率单独报告,不能把协议不稳藏起来。

9. 校准 Judge,而不是相信 Judge

抽取一批由至少两名专家独立标注的数据,计算 Judge 与专家的一致率、按维度混淆和偏差;对有序分数可用加权 kappa,对成对偏好报告一致率和位置偏差。交换候选 A/B 顺序,去除模型品牌,控制答案长度,检查 Judge 是否偏爱自身生成风格。

定期人工复审 Judge 判定的边缘 case、候选差异大 case和安全 hard failure。换 Judge 模型、prompt 或 rubric 就是换测量仪器,必须重新校准并保留旧结果,不可把两个版本分数直接拼成趋势线。

10. 成对比较通常比绝对打分稳定

对 prompt 或模型升级,问“候选相对基线更好、相同还是更差”通常比独立打 1 到 10 分稳定。随机交换展示顺序,允许平局,并让 Judge 引用输入或参考证据。最终报告 win/tie/loss 及分层结果,不只给总体胜率。

候选可能质量提高但成本翻倍,因此比较向量至少包含硬失败、质量、P50/P95/P99、输入输出 token、工具次数与货币成本。Pareto 前沿比任意加权总分更透明;权重由业务 owner 批准,并对权重变化做敏感性分析。

11. 概率输出需要重复与置信区间

即使 temperature 为零,供应商后端也可能变化。对关键小集合重复运行,报告均值、分位数与波动。总体比例的点估计不足以判断小变化;使用 bootstrap 或合适的二项区间。成对数据应按 case 重采样,保持候选关联。

func mean(values []float64) (float64, error) {
	if len(values) == 0 {
		return 0, errors.New("mean requires at least one value")
	}
	var sum float64
	for _, value := range values {
		if math.IsNaN(value) || math.IsInf(value, 0) {
			return 0, errors.New("mean received non-finite value")
		}
		sum += value
	}
	return sum / float64(len(values)), nil
}

样本量由最小可接受差异和方差决定。数十个 case 上的 1% 提升通常只是噪声。报告绝对变化、相对变化、区间和分母,避免只写“提升 20%”。

12. 回归分析必须按切片观察

总体分数可能因简单样本占比大而掩盖中文、长上下文、高风险或某租户回归。按语言、任务、风险、输入长度、来源、工具类型和历史失败标签切片;每个关键切片设最小样本和门槛。切片太细时只用于探索,不作强结论。

失败聚类比逐条看报告有效:把错误归为无召回、错引、格式、越权、拒答过度、工具循环、超时、成本超限等,再按影响和频率排优先级。任何修复都把代表性失败加入数据集,形成累积回归资产。

13. 门禁配置应当可审阅

上线门禁同时包含绝对阈值、相对基线阈值与硬失败零容忍。配置随代码审查,报告记录解释后的实际模型 snapshot。缺数据、Judge 不可用或报告不完整应 fail closed,而不是默认通过。

evaluation_gate:
  dataset: support-v7
  candidate: model-snapshot-2026-08-20
  baseline: model-snapshot-2026-07-15
  required_cases: 1200
  hard_failures:
    cross_tenant_leak: 0
    unconfirmed_write: 0
  thresholds:
    task_success_min: 0.91
    citation_precision_min: 0.97
    p95_latency_ms_max: 5500
    mean_cost_usd_max: 0.018
    pairwise_win_rate_min: 0.52
  slices:
    high-risk:
      task_success_min: 0.96

紧急发布也不能删除安全门禁;可缩短非关键集合,但需留下审批、风险和补跑期限。门禁结果作为不可变制品关联提交、镜像、prompt bundle 与索引版本。

14. Runner 的并发和费用边界

runner 使用固定并发与速率限制,按 provider 配额和预算调节。每个 case 有 deadline,整个 run 有总 token、总费用和最大错误率;预算耗尽明确标记 incomplete。结果逐条持久化,崩溃后按 (run_id, case_id, attempt) 恢复,不能重复调用已完成 case。

channel 容量必须有依据,生产 goroutine 由 WaitGroup 或结构化并发等待。收到取消后停止派发,等待在途调用退出,flush 已完成结果。模型调用和 Judge 调用分开计费、分开限流,否则 Judge 流量可能挤占被测服务。

15. 可观测性连接离线与线上

每个 case 生成 trace,阶段包括排队、上下文构建、检索、模型、工具、Judge 和持久化。属性使用低基数版本与状态,不把原始 prompt、输出、email 或签名 URL放入 span。指标记录错误类别、延迟、token、费用、质量代理和门禁结果;日志只承担可检索事件。

{
  "run_id": "eval_01K4",
  "case_id": "refund-0042",
  "candidate_version": "release-184",
  "model_snapshot": "model-2026-08-20",
  "prompt_sha256": "9e5c...",
  "index_version": "policy-v31",
  "latency_ms": 1830,
  "input_tokens": 922,
  "output_tokens": 188,
  "cost_usd": 0.0064,
  "status": "scored",
  "trace_ref": "trace_78f"
}

trace 采样不能导致安全失败丢失;对硬失败保留脱敏元数据和授权访问的证据引用。原文单独加密存储并设置短留存,不复制到多个观测后端。

16. 在线评测与反馈回路

离线集可重复但会陈旧。线上监控真实任务完成、用户纠正、人工升级、引用点击、工具撤销、拒答、延迟和成本。显式反馈往往有选择偏差,点击也不等于正确;将其用于发现候选样本,再由标注流程确认,不能直接当 ground truth。

灰度按稳定用户哈希分流,避免同一会话跨版本。实验预先定义主指标、护栏、最短周期和停止条件;安全、投诉、成本失控触发立即停止。A/B 流量只进入通过离线硬门禁的候选。

17. 测试评测系统本身

评分器是生产代码,也会出错。表驱动测试覆盖空集合、重复 ID、无效 JSON、分母为零、引用越界、NaN、超时和取消。golden 文件适合稳定 schema,不适合锁死模型自然语言。对 Judge 用录制响应测试解析,对少量固定样本做在线契约测试。

func TestCountsPrecision(t *testing.T) {
	tests := []struct {
		name string
		give Counts
		want float64
	}{
		{name: "no predictions", give: Counts{}, want: 0},
		{name: "half correct", give: Counts{TruePositive: 2, FalsePositive: 2}, want: 0.5},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := tt.give.Precision(); got != tt.want {
				t.Errorf("Precision() = %v, want %v", got, tt.want)
			}
		})
	}
}

故障注入覆盖 provider 429、Judge 半响应、数据库断连和 worker 崩溃。go test -race ./... 检查并发聚合;fuzz 检查 JSONL 装载器不会 panic;恢复测试确认结果不丢、不重复计费。

18. 性能、缓存与可复现性

评测性能关注完成一个 run 的墙钟时间、在途调用、内存、结果写入吞吐和 provider 配额利用率。批量调用可降成本,但必须保持 case 对应关系和独立错误。缓存只用于输入、版本和参数完全相同的确定性阶段;候选输出缓存会隐藏模型波动,应在报告显式标注。

保存 Go 1.26.4、二进制提交、依赖、区域、endpoint、模型 snapshot、prompt 哈希、数据哈希、随机种子和时间。无法固定的供应商行为列为已知噪声。报告生成器排序字段与 case,避免 map 顺序或当前时间造成无意义 diff。

19. 安全、隐私和评测边界

评测集常比生产日志更敏感,因为它集中保存失败、对抗提示和标准答案。访问遵循最小权限,下载与导出审计,存储加密,测试账号与生产账号隔离。Judge 看到的输入先最小化和脱敏;第三方 Judge 不得获得跨租户内容或真实凭据。

红队集合覆盖直接/间接提示注入、system prompt 探测、数据外传、工具越权、编码绕过、资源耗尽和多轮诱导。任何单个关键安全 case 通过并不能证明安全,只能证明已知控制在该路径生效。传统认证、授权、沙箱和网络控制仍由确定性系统执行。

20. 上线决策与生产检查

发布评审应能回答:数据是否代表当前流量且合法使用,标签一致性如何,评分器是否校准,关键切片样本是否足够,候选相对基线变化是否超出噪声,硬失败是否为零,延迟与成本是否在预算,出现回归能否回滚模型、prompt、工具和索引。

评测不能证明所有未来输入都安全正确,也不能把高风险责任移交给 Judge。它提供的是可比较证据和持续发现机制。成熟团队把每次事故变成新 case,把每次版本变成可重放 run,把每次上线变成有门槛的决策;这样模型的不确定性才不会变成发布流程的不确定性。


系列导航与关联阅读

官方资料

本文依据 Go 官方规范、标准库文档和 Go 官方博客重新梳理;正文与示例由 WR BLOG 编写。