主题
Agent 仿真台落地设计:场景、替身、运行与证据
一句话:仿真台不是"再买一套评测工具",而是把 Agent 运行所依赖的外部世界做成可保存、可修改、可重放的软件环境——它交付的是证据材料,不是质量结论;边界由授权与后果划定,不由"请求有没有真的发出去"划定。
本篇对应白皮书第 16 章 Agent Simulation,是 24-白皮书对标与落地推演 认定的最大结构性缺口:平台 00–23 篇已经有生产可观测(14)、黄金集与发布门禁(14)、沙箱与状态(12/21)、任务账本与证据(22),但没有"上线前把一个 Agent 放进可控世界跑一遍"的对象链、执行引擎与证据契约。本篇把它补到可直接建表、建接口、接入现有组件的程度。
为什么需要
现有平台能回答三个问题:线上这次为什么这样(14 的 Trace/事件)、这一版离线跑分是否更好(14 的黄金集)、谁能发布(14 的四级门禁)。它回答不了第四个问题:在没有人盯着、外部系统会超时、用户会改主意的情况下,这个 Agent 的行为是否仍然可接受。
缺口的根因不是评测集不够大,而是评测的输入是静态的:
- 黄金集用例是"一条输入 + 一条期望",跑不出多轮追问、撤回、等待、故障恢复这些真正决定任务成败的路径;
- 影子流量只观察、不施压,无法制造"首次查询超时、第二次恢复"这种条件;
- 录制回放把历史调用重新喂给 Agent,但历史里的工具响应顺序、时延、失败都是固定脚本,测到的是"能不能按剧本走",不是"会不会处理没见过的状态"。
把这三类验证当成"仿真"会得到一个危险的结论:分数在涨,生产接管也在涨。白皮书第 16 章给出的判定很直接——模拟的价值是"用可控环境换可验证行为",它交付 Records → Observations → Evidence,质量结论由独立的 Evaluation 给出,发布授权由治理门禁给出。平台如果不把这三层分开,仿真就会退化成"又一个跑分平台"。
平台的现实约束也决定了仿真台必须独立建设,不能塞进现有任务链路:
- 仿真不能改写被测 Agent 的决策,否则测的是脚本;SUT 必须作为黑盒从外部驱动;
- 仿真需要独立命名空间、独立账户、独立数据,不能和生产共用租户与凭据;
- 仿真的时间可以被加速、回拨、冻结,生产任务的
deadline与租约不能; - 仿真 Run 可以一次包含多个业务 Task,而平台
rt.agent_task是单任务账本,两者是包含关系,不是同一张表。
核心机制
一、边界:SUT 不进仿真,边界由授权与后果划定
先定三条不能破的规则:
- 被测系统(SUT)是仿真服务的对象,不是仿真的一部分。 推理、工具选择、参数构造、异常处理都属于 SUT;这些行为一旦被替身接管,被测对象就已经移出回路。
- 仿真边界只有两条:不触达未授权的生产账户、数据与网络;不产生不可控的真实业务副作用。满足这两条,即使连接的是隔离的真实数据库与测试账户,仍在仿真内;反过来,只要把请求路由到生产账户,即使所有组件都是替身,也不是仿真,是事故。
- 替换前先判断"这是内容还是条件"。 是本次要考察的内容,不能替换;是本次要设定的条件,必须替换,并写清替换成了什么。
这条判断直接决定场景设计。以"主管 Agent 调度一个协作 Agent 完成任务"为例:
| 考察目标 | 协作 Agent 的角色 | 处置 | 原因 |
|---|---|---|---|
| 主管的任务分解与结果聚合能力 | 内容 | 保留真实协作 Agent 或其等价实现 | 它的响应质量直接影响结论 |
| 主管对下游故障的容错能力 | 条件 | 替身为可注入超时/错误的状态机 | 故障表现是实验设定,必须可控可复现 |
| 用户是否会补充订单号 | 条件 | 用户模拟器按披露规则控制 | 用户行为是实验输入 |
| Agent 自己的工具选择 | 内容 | 不替换 | 替换后测的是脚本 |
SUT 边界必须写进场景规格,而不是靠口头约定。sut.boundary 至少声明四类:reasoning(推理)、tool-selection(工具选择)、parameters(参数构造)、recovery(异常恢复)。执行器在装载时校验:边界内出现替身即拒绝启动,返回 422 sut_boundary_replaced。
最常见的反模式是"把工具调用顺序固定成脚本"。这种 Run 看起来一切正常,也没有任何告警,但被测的已经不是 Agent 的决策能力,而是那段脚本。平台侧的防线是:环境替身只返回响应,不主动推进对话;任何"替身调用 SUT"的路径都必须经过 SUT 的公开入口与相同鉴权。
二、对象模型:Spec → Manifest → Run → Result
一次仿真执行涉及的对象必须各有定义和生命周期,否则"可复现"只是一句口号。
text
Scenario Spec(场景规格,声明式、可版本化)
└── Manifest(启动时锁定:场景版本 + 全部资产版本 + 参数)
└── Run(一次受控执行)
├── 1..N Task(业务任务,如"提交退款申请")
│ └── 1..N Session(会话:用户对话 + 工具调用链)
│ └── Event(已经发生的原子事实)
└── Run Result
├── Records(原始记录:消息、调用、状态变更)
├── Artifacts(产物:文件、截图、浏览器状态)
├── Observations(从 Records 提取的事实)
├── Evidence(按评价项组织的证据引用)
└── Completeness(按采集契约判断的完整性)四个对象的职责不能混:
| 对象 | 回答什么 | 可以变吗 | 权威位置 |
|---|---|---|---|
| Scenario Spec | 这次实验怎么设计 | 改设计=新版本 | 场景库(Nacos + 对象存储) |
| Manifest | 这次用了哪一组确定版本 | 启动后不可变 | 仿真账本 + 对象存储 |
| Run | 这次执行实际发生了什么 | 只追加 | 仿真账本 + 事件存储 |
| Run Result | 交出了哪些可引用的材料 | 生成后不可变 | 证据存储 |
计划与事实不能共用一个名字。 场景里写的是 triggers(例如"第 1 轮注入查询超时"),这是未来计划;命中之后产生的"查询超时已发生"才是 Event。把计划叫 Event,会让复现时无法区分"条件没生效"和"条件生效了但 Agent 没走到"。
Manifest 锁的是解析后的版本,不是标签。 场景写 refund-agent@stable,Manifest 必须落成 refund-agent@2.4.1 与 sha256:...;运行记录不能只记 stable,否则标签一移动,历史结论就无法解释。这一点与平台 08 篇的资产版本、19 篇的 label 回滚是同一个原则。
最小 schema
仿真数据放在独立 schema(示例 sim),与运行面 rt 分开,避免生产任务查询扫到仿真数据。
sql
CREATE SCHEMA IF NOT EXISTS sim;
-- 场景:逻辑资源
CREATE TABLE sim.scenario (
scenario_id text PRIMARY KEY,
tenant_id text NOT NULL,
namespace text NOT NULL,
name text NOT NULL,
owner text NOT NULL,
risk_level text NOT NULL CHECK (risk_level IN ('low','medium','high')),
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (tenant_id, namespace, name)
);
-- 场景版本:内容不可变
CREATE TABLE sim.scenario_version (
scenario_id text NOT NULL REFERENCES sim.scenario(scenario_id),
version text NOT NULL, -- SemVer
spec jsonb NOT NULL, -- 十个字段组
spec_sha256 text NOT NULL,
status text NOT NULL CHECK (status IN ('draft','reviewing','reviewed','online','offline')),
source text, -- requirement / trace / incident / manual
created_by text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (scenario_id, version)
);
-- 资产:用户策略、环境初始数据、工具契约、模拟器配置、评估规格
CREATE TABLE sim.asset (
asset_id text PRIMARY KEY,
tenant_id text NOT NULL,
asset_type text NOT NULL CHECK (asset_type IN
('user_policy','env_data','tool_contract','simulator_config','evaluation_spec')),
name text NOT NULL,
owner text NOT NULL,
UNIQUE (tenant_id, asset_type, name)
);
CREATE TABLE sim.asset_version (
asset_id text NOT NULL REFERENCES sim.asset(asset_id),
version text NOT NULL,
content_ref text NOT NULL, -- oss://... 或 nacos://...
content_sha256 text NOT NULL,
status text NOT NULL CHECK (status IN ('draft','reviewing','reviewed','online','offline')),
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (asset_id, version)
);
-- Manifest:一次 Run 的冻结组合
CREATE TABLE sim.manifest (
manifest_id text PRIMARY KEY,
tenant_id text NOT NULL,
scenario_id text NOT NULL,
scenario_version text NOT NULL,
resolved jsonb NOT NULL, -- 全部资产的精确版本 + sha256
manifest_sha256 text NOT NULL,
seed bigint,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (manifest_sha256)
);
-- Run:一次受控执行,四种状态独立
CREATE TABLE sim.run (
run_id text PRIMARY KEY,
tenant_id text NOT NULL,
manifest_id text NOT NULL REFERENCES sim.manifest(manifest_id),
mode text NOT NULL CHECK (mode IN ('surrogate','hybrid','isolated_real')),
execution_status text NOT NULL CHECK (execution_status IN ('pending','running','completed','crashed','cancelled')),
validity_status text NOT NULL CHECK (validity_status IN ('unchecked','valid','violated')),
evidence_status text NOT NULL CHECK (evidence_status IN ('unchecked','complete','partial')),
quality_ref text, -- 指向 Evaluation Result,不由仿真写
started_at timestamptz,
ended_at timestamptz,
terminal_reason text,
trace_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX ix_sim_run_manifest ON sim.run (tenant_id, manifest_id, created_at DESC);
-- Run 内的业务任务与会话(引用 SUT 自己的 task_id/session_id)
CREATE TABLE sim.run_task (
run_id text NOT NULL REFERENCES sim.run(run_id),
task_key text NOT NULL, -- 场景内的逻辑任务名
sut_task_id text, -- 被测系统返回的任务 ID
goal jsonb NOT NULL,
outcome text CHECK (outcome IN ('submitted','accepted','root_complete','failed')),
PRIMARY KEY (run_id, task_key)
);
CREATE TABLE sim.run_session (
run_id text NOT NULL,
task_key text NOT NULL,
session_key text NOT NULL, -- 场景内的逻辑会话名
sut_session_id text,
transport text NOT NULL CHECK (transport IN ('ag-ui','a2a','chat-completions','internal')),
PRIMARY KEY (run_id, task_key, session_key),
FOREIGN KEY (run_id, task_key) REFERENCES sim.run_task(run_id, task_key)
);
-- Event:已经发生的原子事实,只追加
CREATE TABLE sim.event (
run_id text NOT NULL,
seq bigint NOT NULL,
event_type text NOT NULL, -- user.message / tool.request / tool.response / fault.hit / state.change
actor text NOT NULL CHECK (actor IN ('user','agent','environment','harness')),
payload jsonb NOT NULL,
logical_time bigint, -- 仿真逻辑时间(毫秒)
wall_time timestamptz NOT NULL,
trace_id text,
span_id text,
PRIMARY KEY (run_id, seq)
);
-- 原始记录:消息、调用、状态变更;不直接作为结论
CREATE TABLE sim.record (
record_id text PRIMARY KEY,
run_id text NOT NULL,
record_type text NOT NULL CHECK (record_type IN ('message','tool_call','state_change','control')),
ref text NOT NULL, -- 对象存储 URI 或内联小对象
sha256 text NOT NULL,
redaction text NOT NULL DEFAULT 'none',
created_at timestamptz NOT NULL DEFAULT now()
);
-- 观测:从 Records 提取的事实(可重算)
CREATE TABLE sim.observation (
observation_id text PRIMARY KEY,
run_id text NOT NULL,
criterion_key text, -- 对应评价项,可为空
fact_key text NOT NULL, -- refund.count / refund.amount_minor / order.state
fact_value jsonb NOT NULL,
derived_from jsonb NOT NULL, -- record_ids / event seqs
extractor text NOT NULL, -- 提取规则或模型版本
created_at timestamptz NOT NULL DEFAULT now()
);
-- 证据引用:Observation → 评价项
CREATE TABLE sim.evidence (
evidence_id text PRIMARY KEY,
run_id text NOT NULL,
criterion_key text NOT NULL,
observation_ids jsonb NOT NULL,
evidence_refs jsonb NOT NULL, -- record/artifact 的稳定引用
created_at timestamptz NOT NULL DEFAULT now()
);
-- 完整性:按采集契约逐项核对
CREATE TABLE sim.completeness (
run_id text NOT NULL,
check_key text NOT NULL, -- required_message / tool_result / state_snapshot / fault_recovery
status text NOT NULL CHECK (status IN ('present','missing','not_applicable')),
detail jsonb,
PRIMARY KEY (run_id, check_key)
);四个状态列不能合并。一次 Run 完全可能"执行完成、仿真有效、材料齐备、任务质量不合格";压成一个 success,归因时就分不清是环境坏了、材料缺了,还是 Agent 错了。
三、执行引擎:编排层做五件事,明确不做五件事
仿真 Harness 的职责边界必须写进代码,而不是靠文档约束:
| 做 | 不做 |
|---|---|
| 解析场景,把 Spec + 资产解析成不可变 Manifest | 任务生命周期管理(那是 SUT 或其编排层) |
| 编排 Run 的装载、初始化、运行、终止、收敛、清理 | 鉴权与权限决策(那是 Gateway 与安全框架) |
| 管理替身与隔离,维持仿真边界 | 遥测采集(那是 OTel,Harness 只关联引用) |
| 汇集记录,把交互与控制关联到 Trace 与 Artifact | 质量评分与判定(那是 Evaluation) |
| 清理环境,使世界回到初始状态 | 发布决策(那是治理门禁) |
"不做"的每一项都对应平台里已有的专职系统。把任何一项收进来,Harness 就会从执行引擎膨胀成总控制器:它会开始持有任务状态、开始自己发权限、开始自己下结论,最后与 22 的任务账本、15 的安全策略、14 的门禁产生第二权威。
数据流三线、协调两账
- 交互流:用户消息、工具请求、可见事件;
- 控制流:装载、初始化、故障注入、终止、清理指令;
- 证据流:Trace、状态、产物、控制记录的沉淀。
三线靠两张账协调:
事件联动账记录同一事件在用户侧、Agent 侧、环境侧的三个时点。以"撤回"为例:用户侧在 t1 本地撤回,Agent 在 t2 收到,在途事务在 t3 生效。界面已经撤回、Agent 却还在按原指令提交,是真实事故的常见形态。联动账让"撤回到底生效没有"可以被精确回答,而不是靠日志猜。
时间分离账记录四种时间,不可混用:
| 时间 | 回答什么 | 实现 | 误用后果 |
|---|---|---|---|
| 业务日历时间 | 业务规则的期限判断(是否在 7 天无理由期) | 场景指定起始日期 + 业务日历 | 用虚拟加速会破坏业务规则 |
| 仿真逻辑时间 | 虚拟事件调度(第 3 轮注入超时) | Harness 调度器,可加速/暂停 | 与墙钟混用会导致调度不确定 |
| 单调时钟 | 真实耗时测量(推理、工具各花多久) | CLOCK_MONOTONIC | 用业务时间测性能没有意义 |
| 墙钟时间 | 与现实日期的关联(何时跑、报告何时生成) | UTC | 回拨会破坏审计与保留期 |
生命周期与失败语义
text
LOADING → INITIALIZING → RUNNING → DRAINING → COLLECTING → CLEANED
│ │ │
└ FAILED ←───┴──────────┘LOADING失败:Manifest 缺失、资产校验不通过、SUT 版本不可达 →crashed,terminal_reason=manifest_unresolved,不产生任何 Agent 调用;INITIALIZING失败:环境数据装载失败、用户模型加载失败 →crashed,validity_status=violated;RUNNING失败:SUT 崩溃、超时、预算耗尽 →crashed或cancelled,保留已产生的 Records,evidence_status=partial并列出缺失项;DRAINING:停止注入新事件,等待在途调用返回或超时;不能直接杀进程,否则最后一步的副作用与回执会丢失;COLLECTING:生成 Observations、Evidence、Completeness;提取失败不阻断 Run,但必须把对应项标为missing,不能用零值补齐;CLEANED:销毁隔离命名空间、测试账户、临时凭据、工作区;清理失败写入告警,且该 Run 的结论标validity_status=violated。
四、用户模拟器:三层模型 + 混合生成 + 独立质控
用户模拟器承载的不是"一个会说人话的 LLM",而是三个可分别治理的部分:
| 层 | 内容 | 实现 | 可复现性 |
|---|---|---|---|
| Persona | 专业度、表达偏好、行为倾向 | 结构化配置 + 提示模板 | 高 |
| 认知状态 | 已知事实、当前理解;允许与客观状态不同 | 状态对象,区分真实状态与用户认知 | 高 |
| 行为策略 | 反应规则 + 状态机 + 时钟驱动的耐心 | 状态机决定意图,模型生成措辞 | 中 |
工程主流是混合:意图选择交给状态机("指出金额错误"是一个确定节点),自然语言表达交给模型(生成符合 Persona 的措辞),发送前过两道校验——事实校验(不能说出用户认知之外的金额与订单号)和披露规则校验(订单号只在被追问时提供)。探索运行放宽分支选择,让模拟用户走出多样路径;回归运行锁定关键行为约束,保证可复现。
用户模拟器的质量不能看 Agent 成功率。过度配合的模拟器会高估 Agent 能力,始终拒绝的模拟器会产生无意义失败。质控指标必须按维度和统计单位定义:
| 维度 | 检查什么 | 统计单位 | 不合格处置 |
|---|---|---|---|
| 角色一致性 | 行为是否贴合 Persona | 每条消息 | 标记该消息,重跑或人工复核 |
| 反应规则符合率 | 触发条件命中时是否执行反应 | 每次触发 | 该 Run 标 validity_status=violated |
| 信息可见性遵守 | 是否只披露用户认知内的信息 | 每次披露 | 该 Run 结论不可用于质量评价 |
| 目标终止规则符合率 | 是否按规则继续、澄清或停止 | 每个 Run | 重跑或判 unchecked |
| 行为分布多样性 | 分支选择是否符合目标分布 | 每批次 Run | 调整场景或用户策略版本 |
生成与评分必须使用不同模型,降低共同偏差;再配合独立数据、受限上下文与人工校准,让"用户像不像"本身成为可治理对象。
五、环境模拟器:一个事实模型,三个超时位置,四步故障记录
事实一致性优先于"像不像"
环境可信的第一要求不是接口长得像,而是事实一致:create_refund 返回"已受理"后,get_refund 必须查到对应状态与金额。用一张事实模型(订单、退款、账户、状态机)同时驱动 API 替身与数据库替身,禁止一个说受理、一个说不存在。
超时是最值得精雕的条件,按请求生命周期分三个位置:
| 超时位置 | 系统状态 | 检验什么 | 注入方式 |
|---|---|---|---|
| 请求未发送 | 调用未发生 | 重试与退避策略 | 工具适配层直接返回连接失败 |
| 已发送、未提交 | 下游收到但未处理 | 等待、轮询与查询确认 | 替身挂起请求,超时后返回未知 |
| 已提交、响应丢失 | 业务已生效 | 重复提交防护与状态核实 | 替身执行业务动作但不返回响应 |
第三种最关键:申请其实已经受理,Agent 却没收到回执——它会不会先核实状态,还是直接再提交一次?很多"重试导致重复退款"的事故就发生在这一格。
替身机制按确定性选择,不按难度阶梯
| 机制 | 开放度 | 确定性 | 状态复杂度 | 维护成本 | 典型对象 |
|---|---|---|---|---|---|
| 脚本 | 低 | 高 | 低 | 低 | 固定流程的工具响应 |
| 录制回放 | 低 | 高 | 中 | 中,随接口版本漂移 | 历史真实会话 |
| 规则 | 低 | 高 | 低 | 低 | 幂等查询接口 |
| 状态机 | 中 | 高 | 高 | 中 | 多阶段业务流程 |
| 模型生成 | 高 | 低 | 中 | 高 | 开放用户行为 |
| 混合 | 中 | 中 | 中 | 中 | 工程主流 |
被模拟对象越确定,越适合脚本与规则;越开放,越需要模型生成;状态越复杂,越偏向状态机与录制回放。不要因为"我们有模型"就把所有替身都做成生成式——每多一处生成,就多一处不可复现。
故障注入:ChaosBlade 只是执行后端
边界要写死:ChaosBlade 执行实验,Harness 决定何时、对谁、施加什么故障,并负责同步、恢复验证与清理。 不允许业务代码直接调用 ChaosBlade,也不允许把故障脚本散落在场景 YAML 之外。
故障记录分四步,缺一步结论就不可信:
| 步骤 | 记录什么 | 失败含义 |
|---|---|---|
| planned | 场景里写了什么故障、目标、时间窗 | 计划本身不完整 |
| armed | 执行器报告实验已启动 | 注入没有发生 |
| hit | 观测到故障确实落在目标调用上 | 注入发生了,但没打中目标 |
| recovered | 实验销毁后服务恢复正常 | 环境未收敛,后续 Run 不可信 |
sql
CREATE TABLE sim.fault_plan (
run_id text NOT NULL,
fault_id text NOT NULL,
target text NOT NULL, -- tool / dependency / pod
fault_type text NOT NULL, -- delay / error / timeout / abort
params jsonb NOT NULL,
at_logical_ms bigint NOT NULL,
recover_after_ms bigint,
PRIMARY KEY (run_id, fault_id)
);
CREATE TABLE sim.fault_record (
run_id text NOT NULL,
fault_id text NOT NULL,
stage text NOT NULL CHECK (stage IN ('planned','armed','hit','recovered','failed')),
detail jsonb NOT NULL,
observed_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (run_id, fault_id, stage)
);隔离与复现
隔离覆盖计算、网络出口、数据、文件、缓存与记忆:每个 Run 使用独立的 Kubernetes 命名空间、独立测试账户、独立工作区与会话,Run 之间不共享任何可变状态。共享只允许发生在只读基础镜像与只读知识库。
复现分三个层次,回答三个不同问题:
| 层次 | 做法 | 回答 |
|---|---|---|
| 配置复现 | 用 Manifest 重建场景与全部资产版本 | 同样的实验能不能再来一次 |
| 事件复现 | 重放输入、故障与调度 | 同样的路径能不能再走一遍 |
| 统计复现 | 同配置重复 N 次,比较分布 | 结论稳不稳 |
六、场景与资产:十个字段组,五类版本,一张覆盖矩阵
Scenario Spec 用十个字段组回答从"测什么"到"怎么评价"的完整设计:
| 字段组 | 内容 | 平台落点 |
|---|---|---|
| identity | 场景 ID、版本、标签、负责人、风险等级 | sim.scenario / sim.scenario_version |
| sut | 被测边界、Agent 版本、模型与 Prompt 引用 | Manifest 解析;边界校验 |
| user | Persona、目标、私有事实、披露规则、行为策略 | 用户模拟器 + sim.asset |
| environment | 初始数据、工具契约、后端模式、时钟策略 | 环境模拟器 + 隔离命名空间 |
| interaction | 实体绑定、可见渠道、动作入口、交互模式 | AG-UI / A2A / Chat Completions 适配 |
| participants | 其他 Agent 的角色、接口与权限 | 协作面替身或真实 Agent |
| events | 触发条件、故障动作、恢复策略 | sim.fault_plan |
| execution | 种子、并发、轮数、时间与资源预算 | Harness 调度与配额 |
| evaluation_ref | 断言、Rubric、预期状态的版本引用 | Evaluation 系统(不在仿真内) |
| provenance | 原始需求、资产位置、脱敏会话、事故记录 | 场景库审计 |
一个完整示例(退款超时场景):
yaml
schema_version: 1
identity:
id: refund-timeout-retry
version: 1.3.0
owner: quality-team
risk_level: high
sut:
agent: refund-agent@2.4.1
boundary: [reasoning, tool-selection, parameters, recovery]
user:
persona: { expertise: novice, patience: low, tone: annoyed }
goal: "拿回 199 元退款"
private_facts: { order_no: "B2026-0901-7734", paid_amount_minor: 19900 }
disclosure: { order_no: on-request }
behavior:
reaction_rules:
- { when: agent.asks_order_no, times: 3, effect: disclose_order_no }
- { when: wait_ms > 120000, effect: threaten_complaint }
environment:
backend_mode: surrogate
clock: { policy: virtual, start: "2026-09-01T10:00:00+08:00" }
data_ref: env/ecom-seed@3.0.1
tool_contracts: [refund-api@1.4.0]
interaction:
entity_binding: { user.order_no: env.order.id, tool.order_no: env.order.id }
channels: [ag-ui]
action_entry: refund.create
participants: []
events:
triggers:
- { target: refund.query, effect: timeout, at: turn-1, recover: turn-2, position: committed }
execution:
seed: 20260914
budget: { max_turns: 12, wall_clock: 20m, max_tool_calls: 30 }
evaluation_ref:
assertions: refund-assertions@1.1.0
rubric: refund-rubric@2.0.0
provenance:
source: "incident #7742 脱敏会话"
reviewed_by: risk-team两处细节决定场景是否成立:user.private_facts 与 environment.data_ref 里的订单必须是同一实体(联合建模);events.triggers 是未来计划,命中后才生成 Event(名字边界)。
联合建模的五个维度必须逐项校验,否则场景测的是假问题:
- 实体与事实绑定:用户说的订单、工具参数、数据库记录是同一张单子;
- 信息可见性:用户认知与环境状态允许不同,但两侧内部各自一致;
- 动作入口与状态归属:提交入口只有一个,状态变迁有唯一属主;
- 事件触发与时序:什么事件按什么顺序发生;
- 目标演化与结果要求:任务完成的判定随场景演化更新。
五类对象独立版本化:场景、数据、工具契约、模拟器配置、评估规格。一次 Run 锁定全部版本组合,结论只对这组组合负责。改用户耐心参数是新场景版本;改披露规则逻辑是新模拟器资产版本;两者不能混在一个版本号里。
覆盖矩阵按"业务阶段 × 用户行为 × 权限状态 × 依赖故障"建立。缺口决定下一批场景的优先级,而不是"谁声音大先做谁"。新出现的交互缺陷先核实事实与责任归属,再固化进回归集——把一次事故的表象直接抄成场景,等于把未确诊的病历直接开成处方。
七、证据交付:三层分离,三道可用性检查,一个置信上界
Run Result 的三个层次不能混:
text
Records(原始记录)
→ Observations(事实性观测:退款申请数 = 1,金额 = 19900 分,状态 = accepted)
→ Evidence(按评价项组织的证据引用)criterion → evidence_refs → verdict 的关系由 Evaluation Result 建立,不属于 Run Result。同一份 Records,质量团队用来重评分,合规团队检查金额口径,回归系统比对版本差异;如果原始材料与质量结论混在一层,后一种用途会被前一轮结论污染。
评估器一侧的可用性检查有三道,不是一道闸门:
| 检查 | 回答 | 不通过时 |
|---|---|---|
| 记录完整性 | 材料在不在 | 输出"不可判定" |
| 运行有效性 | 实验条件成立没有 | 限定可评分范围或拒绝评价 |
| 证据充分性 | 够不够支撑这个评价项 | 该项标"不可判定",其余照常评价 |
"不可判定"是诚实结论,好过用不充分证据硬评。
零违规的回归结果要标注统计边界:n 次独立运行零违规,违规率的单侧 95% 置信上界是 1 − 0.05^(1/n)。300 次零违规意味着"违规率大概率不超过 1%",不是零。这个上界的前提是固定样本量、独立同分布、无非选择性排除。
| 零违规次数 n | 95% 置信上界(约) |
|---|---|
| 10 | 26% |
| 30 | 9.5% |
| 100 | 3.0% |
| 300 | 1.0% |
| 1000 | 0.3% |
结论有有效期。 仿真用户不是真实用户,场景来自已知失败模式,工具契约与故障频次随业务漂移。校准由事件驱动——接口变更、故障复盘、用户结构变化时触发,不按固定周期例行执行。固定周期会在两次校准之间放过漂移。
八、与现有组件的合缝
仿真台不新建一套平台,而是接在现有组件上。每条接缝都要有唯一权威与失败语义:
| # | 接缝 | 交界字段 | 权威 | 失败语义 |
|---|---|---|---|---|
| G1 | 仿真 → SUT | sut.agent、sut.boundary、入口地址、测试身份 | 控制面(07/08) | 边界被替换 → 拒绝启动 422 |
| G2 | 仿真 → Nacos | 场景/资产/Agent/Prompt 的精确版本与摘要 | Nacos Registry | 解析失败 → crashed,不降级到"任意可用版本" |
| G3 | 仿真 → 沙箱 | 独立 namespace、workspace、测试账户、临时凭据 | 沙箱管理器(21) | 隔离创建失败 → 不进入 RUNNING |
| G4 | 仿真 → ChaosBlade | 目标、故障类型、时间窗、实验 ID | Harness(计划)/ ChaosBlade(执行) | 四步记录缺一 → 该 Run 结论不可信 |
| G5 | 仿真 → OTel | trace_id、run_id、manifest_id、span 引用 | OTel | 采集缺失 → evidence_status=partial,不补零 |
| G6 | 仿真 → 对象存储 | Records/Artifacts 的 URI 与 sha256 | 对象存储 | 上传失败 → Run 标 partial |
| G7 | 仿真 → Evaluation | Records/Observations/Evidence/Completeness + 评价项引用 | Evaluation | 证据不足 → 该项"不可判定" |
| G8 | Evaluation → 发布门禁 | 场景版本、评估器版本、阈值、置信区间 | 治理门禁 | 版本未冻结 → 不准入 |
| G9 | 仿真 → 任务账本 | SUT 自己的 task_id/session_id,只引用不复制 | rt.agent_task | 任务账本不可用 → 仿真不接管任务状态 |
三条责任边界要重复强调:
- 仿真不鉴权:所有对 SUT 的调用都走真实入口与真实鉴权,仿真只持有测试身份;
- 仿真不采集遥测:Trace 由 OTel 产生,仿真只在记录里保存引用;
- 仿真不评分、不发布:Run Result 是证据,Evaluation 出判定,治理门禁出授权。
九、API 契约
接口放在 Service Gateway 之后,与任务 API 同一套认证;所有写接口带 Idempotency-Key。
| 方法与路径 | 作用 | 关键请求 | 成功响应 | 主要错误 |
|---|---|---|---|---|
POST /v1/sim/scenarios | 创建场景 | namespace、name、owner、risk_level | 201 {scenario_id} | 409 重名、422 参数 |
POST /v1/sim/scenarios/{id}/versions | 提交新版本 | version、spec、source | 202 {status: reviewing} | 409 版本已存在、422 schema |
POST /v1/sim/runs | 启动 Run | scenario_id、version、mode、seed、budget | 202 {run_id, manifest_id} | 422 场景未上线、409 同幂等键 |
GET /v1/sim/runs/{id} | 读运行状态 | — | 200 {execution_status, validity_status, evidence_status, quality_ref} | 404 |
GET /v1/sim/runs/{id}/events?after_seq= | 拉事件 | after_seq | 200 {events, next_seq} | 410 游标过期 |
GET /v1/sim/runs/{id}/evidence | 取证据包 | criterion(可选) | 200 {records, observations, evidence, completeness} | 409 证据未收敛 |
POST /v1/sim/runs/{id}:cancel | 取消 | reason | 202 {execution_status: cancelled} | 409 已终态 |
GET /v1/sim/manifests/{id} | 取冻结组合 | — | 200 {resolved, manifest_sha256} | 404 |
错误语义与任务 API 一致:409 不重试先读状态,410 重读快照,422 修正后新建,429 按 Retry-After 退避,503 带同一幂等键重试。
Run 启动是异步的:202 只表示已受理并生成 Manifest,不表示环境已就绪。客户端通过 GET /v1/sim/runs/{id} 轮询或订阅事件,不能把 202 当"已开始执行"。
十、装配顺序
仿真台作为平台第七个切片(S7),依赖 S1 身份、S2 状态与沙箱、S5 供给与路由,可与 S6 证据治理并行。
| 步 | 动作 | 依赖 | 验收 |
|---|---|---|---|
| 1 | 建 sim schema 与核心表族(场景、资产、Manifest、Run、事件、记录、观测、证据、完整性、故障) | PostgreSQL | 能创建场景、版本、Manifest、Run |
| 2 | 场景库接 Nacos + 对象存储 | 08/19 | 场景与资产可版本化、按 label 回滚 |
| 3 | 实现 Manifest 解析器 | Resolver | 标签解析为精确版本 + sha256,写入运行记录 |
| 4 | 接独立命名空间与测试账户 | 21 / K8s | 两个 Run 之间无共享可变状态 |
| 5 | 实现用户模拟器(状态机 + LLM + 两道校验) | 模型网关 | 角色一致性与披露规则可度量 |
| 6 | 实现环境模拟器(事实模型 + 三处超时) | 工具契约 | create 后 get 状态一致;三种超时可注入 |
| 7 | 接 ChaosBlade,落四步故障记录 | ChaosBlade | 注入→生效→命中→恢复四步可查 |
| 8 | 接 OTel 与证据提取 | 14 | 任一 Run 能还原 span 树与证据链 |
| 9 | 接 Evaluation 与门禁 | 14 | 证据不足输出"不可判定",不误判 |
最小上手
先只做一条场景、一个业务任务、两种故障,跑通"证据交付":
text
1. 建 sim schema,只实现 scenario / scenario_version / manifest / run / event / record 六张表;
2. 写一条"查询超时后恢复"的场景,SUT 只声明 reasoning 与 tool-selection 两个边界;
3. 用户模拟器先只做状态机,不接 LLM:固定三轮,第三轮给订单号;
4. 环境模拟器用规则替身:第一次 query 返回超时,第二次返回已受理;
5. Run 完成后生成 observations(退款申请数、金额、状态),不写 verdict;
6. 把 evidence 包交给人工评审,确认"材料够不够支撑结论";
7. 故意让故障未命中,验证 Run 标 `validity_status=violated` 而不是"通过"。最小验收:同一条场景用同一 Manifest 重跑两次,Records 可逐条对齐;故意把 sut.agent 指向不存在的版本,Run 在 LOADING 阶段失败且没有产生任何 Agent 调用;故障只注入未命中时,结论不被采纳。
生产动作
- 场景先评审再上线:场景与资产和代码一样走 review,高风险场景要求风险团队签字;
- Manifest 不可变:启动后禁止修改;需要改条件就新建版本,不能改历史 Run 的解释;
- 替身有负责人:每个环境替身登记 owner 与契约版本,接口变更触发场景校准;
- 故障四步留痕:没有
hit与recovered的 Run,不得进入回归结论; - 证据分层存储:Records 长期保留,Observations 可重算,Evidence 随评价项冻结;
- 置信区间随报告发布:零违规必须附
1 − 0.05^(1/n)上界,禁止写成"零风险"; - 成本按段归因:区分 SUT 执行成本与仿真平台开销(模型生成、环境步进、存储)。
边界
- 仿真不是真实用户:用户模型是假设,仿真成功率上升不能替代真实业务指标;
- 仿真覆盖不了未知风险:场景来自已知失败模式,训练分布之外的风险仍需生产可观测兜底;
- 全替身通过不等于真实依赖通过:不同保真度验证的系统范围不同,结论不可混用;
- 仿真不证明合规:它提供证据材料,合规判断仍由审计与治理节点给出;
- 仿真不替代单元测试与生产观测:确定性逻辑用单测,真实分布用生产 Trace,三者互补;
- 加速时间有边界:业务日历时间不能被虚拟加速替代,超时与租约仍按真实语义判断。
讲清楚
仿真台的价值不是"多跑几条用例",而是把 Agent 的外部世界做成可保存、可修改、可重放的软件:用户会追问、会撤回,环境会超时、会丢响应,而这些都不会产生真实后果。
它的边界只有两条:不触达未授权的生产账户与数据,不产生不可控的真实副作用。被测 Agent 的决策不能被替身接管——工具调用顺序一旦写成脚本,测的就不是 Agent 了。
一次执行的对象链是 Spec → Manifest → Run → Result:Spec 是实验设计,Manifest 锁死版本组合,Run 记录实际发生的事实,Result 交出 Records、Observations、Evidence 和完整性判断。四维状态必须分开:执行完成、仿真有效、材料齐备、任务合格是四个独立事实。
仿真交付证据,不交付结论。评估器做三道检查(材料在不在、条件成立没有、证据够不够),不够就输出"不可判定";零违规要附置信上界,不能写成零风险。这条界限守不住,仿真就会变成新的"跑分表演"。
适用:AI-Agent-Handbook 第 16 章 / AgentScope Java 2.x / Kubernetes agent-sandbox / ChaosBlade / OTel GenAI 语义约定 | 初稿 2026-10