用Docker Compose固定工具响应、数据库状态、时钟和依赖版本,消除CI与本地环境差异,支持合成案例回放和故障注入。
一个 Agent 评估在 CI 环境中失败,却在本地通过了。在责怪模型之前,先问问自己:两次运行是否看到了相同的工具响应、数据库状态、时钟、配置和依赖版本。
容器无法让外部模型变得确定性,但可以消除围绕它的大量意外可变性。
我使用 Docker Compose 作为评估实验室:一个小型、版本化的环境,可以重放合成测试用例、提供受控工具、注入故障并保留证据。
当同一个实验室支持多种模式时,Compose profiles 非常有用。没有 profile 的服务正常启动;有 profile 的服务只有在启用或明确指定时才会启动。
services:
fixture-api:
image: ghcr.io/example/agent-fixtures@sha256:REPLACE_WITH_DIGEST
environment:
FIXED_NOW: "2026-09-01T12:00:00Z"
DATASET_PATH: /fixtures/cases.json
volumes:
- ./fixtures:/fixtures:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8080/health"]
interval: 2s
timeout: 1s
retries: 20
eval-runner:
build:
context: .
dockerfile: Dockerfile.eval
depends_on:
fixture-api:
condition: service_healthy
environment:
FIXTURE_URL: http://fixture-api:8080
EVAL_SEED: "1701"
volumes:
- ./fixtures:/app/fixtures:ro
- ./evidence:/app/evidence
profiles: [eval]
fault-proxy:
image: ghcr.io/shopify/toxiproxy@sha256:REPLACE_WITH_DIGEST
profiles: [fault]
otel-collector:
image: otel/opentelemetry-collector@sha256:REPLACE_WITH_DIGEST
profiles: [observe]
摘要占位符:在仓库中解析并提交真实的镜像摘要。将 fixture 服务等核心依赖保持为无 profile。可选运行器、故障注入器、本地模型或遥测服务应显式标记。
docker compose run --rm eval-runner
在测试超时和重试时,添加可选的故障服务:
docker compose --profile fault run --rm eval-runner
Compose 自动启动目标 profile 服务及其声明的依赖。多个 --profile 标志可以组合模式。
可重放的测试用例需要一份环境清单:
{
"caseId": "refund-approval-required",
"datasetRevision": "fixtures-2026-09-01",
"promptRevision": "refund-v7",
"policyRevision": "policy-v3",
"toolSchemaRevision": "tools-v5",
"model": "configured-model-id",
"temperature": 0,
"seed": 1701,
"clock": "2026-09-01T12:00:00Z"
}
即使提供商不保证 seed 的确定性,也要记录这些值。清单描述的是尝试的条件,并不承诺产生完全相同的 token。尽可能固定这些输入:
假设 lookup_order 通常调用一个变化中的生产 API。在实验室中,将其路由到 fixture-api 并通过 case ID 选择行为:
const result = await fetch(
`${process.env.FIXTURE_URL}/orders/synthetic-42`,
{ headers: { "x-eval-case": "refund-approval-required" } },
);
fixture 应为该版本返回相同的响应体、状态码和人工延迟。fault profile 可以随后引入故意的超时或畸形响应。
这赋予了失败以意义。如果 Agent 在固定的 fixture 面前跳过了授权,则更容易将行为回归与后端变更区分开来。
不要让唯一的结果只是一个进程退出码。将有限的 artifact 集写入挂载的 ./evidence 目录:
evidence/
environment.json
results.json
junit.xml
traces/
README.md
README 应标识命令、fixture 版本、预期不变量以及执行的任何脱敏处理。CI 甚至可以在运行器失败时上传该目录。
当用候选版本评估相对于基线时,添加机器可读的比较文件:
{
"baseline": "prompt-v6",
"candidate": "prompt-v7",
"dataset": "fixtures-2026-09-01",
"casesAdded": 0,
"casesRegressed": ["refund-approval-required"],
"casesImproved": ["policy-timeout-recovery"],
"unknown": []
}
不要将其压缩为一个平均分数。候选版本可能改善了十个无害用例,却在一个受保护操作上回归。
让 evidence 目录每次运行都唯一,并原子性地写入。在并行 CI 作业间复用 ./evidence/results.json 可能导致新结果与旧环境清单混在一起。简单的运行标识符和完成标记使部分输出显而易见:
evidence/run-2026-09-09-1701/
environment.json
results.json
comparison.json
COMPLETE
只有在所有必需的 artifact 都刷入后才创建 COMPLETE。为诊断目的上传不完整的目录,但绝不将它们作为已完成评估进行评分。
如果使用本地 trace 工具如 AgentInspect,应保持可选且以元数据优先。实验室架构不应依赖托管的查看器,而且捕获的 prompt 或工具 payload 仍需经过共享审查。
该实验室不能复现提供商负载、生产向量索引、浏览器调度或每一种分布式竞态。temperature 为零并不等同于数学上保证输出相同。容器隔离也不是对所执行代码的安全评估。
它的价值更为局限:它使受控输入变得明确且可重复,足以评估稳定的 outcomes 和轨迹。
明确指定 eval-runner 会启用其 Compose profile 并启动声明的依赖,但不会启用无关的 profile 服务。将 profile 组合保存在命名的脚本或 CI 作业中,这样开发者就不会意外地将故障注入的运行与干净基线进行比较。
当 Agent 测试发生变化时,你希望第一个问题是「哪种行为发生了变化?」——而不是「我的笔记本是否使用了不同的时钟、数据集或工具服务器?」一个小的 Compose 实验室将许多这类变量转移到版本化的工程决策中。