生产级Agent(19):执行回放与故障取证
文章摘要
前十八篇已经把生产级 Agent 的执行、权限、审计和治理链条搭起来:Run、Planner、Tool、Checkpoint、Approval、Capability、Delegated Authority、Audit Ledger 都有了。但真正发生线上事故以后,团队很快会遇到一个更难的问题:
“这次 Agent 到底为什么这么做?”
普通日志往往只能看到:
调用了哪个模型
调用了哪个Tool
返回了什么错误
却很难完整还原:
当时模型看到了什么上下文
使用的是哪个Prompt版本
Tool返回了哪一版数据
Policy如何判断
审批绑定了什么Action
为什么进入下一步
外部副作用到底成功没有
如果重新执行,能不能稳定复现
所以生产 Agent 不能只有 Audit Ledger,还需要一套真正的 Execution Replay & Failure Forensics:把一次运行需要的输入、版本、决策、工具结果、外部副作用和时间条件固化成可重放证据;事故时在隔离环境里选择“纯回放”“从某一步分叉”“替换模型重跑”“冻结Tool结果重跑”等模式,比较新旧轨迹;遇到外部系统返回未知状态时,必须先 Reconcile,而不是直接 Retry。
本篇从数据模型、Replay Manifest、事件时间线、Tool Fixture、UNKNOWN Side Effect、Counterfactual Replay、Snapshot、Artifact Hash、State Migration、Privacy Redaction、故障分类、Spring Boot Orchestrator 和自动化测试一路实现下来,目标是让一次复杂 Agent 事故从“看日志猜原因”,升级成“可以稳定重放、比较和举证”。
一、为什么普通Trace还不够
一个生产 Agent Run:
User Request
↓
Planner
↓
Search
↓
CRM
↓
Reviewer
↓
Email Send
OpenTelemetry 能告诉你:
Span A
Span B
Span C
但重放需要更多东西:
模型版本
Prompt Hash
Context Snapshot
Tool Fixture
Policy Version
Approval Hash
Clock
Randomness
Knowledge Snapshot
如果这些没有保存,Trace 只能描述:
“发生了什么”
不能重新构造:
“当时的世界”
这就是 Replay 和 Observability 的区别。
二、Replay的第一原则:不要重新访问今天的外部世界
事故发生在:
2026-08-20 10:15
今天重放时,CRM 客户状态可能已经变了,知识库文档也更新了。
如果 Replay 又实时查询:
CRM
Vector DB
网页
库存
结果不同并不能说明 Agent 不稳定。
所以默认 Replay 应使用:
Recorded Tool Result
而不是 Live Tool。
三、Replay Manifest
public record ReplayManifest(
String replayId,
String sourceRunId,
String sourceRunVersion,
String modelProfile,
String promptBundleHash,
String graphVersion,
String policyVersion,
String capabilityRegistryVersion,
String knowledgeSnapshot,
String evaluatorVersion,
Instant logicalTime,
String environmentHash,
ReplayMode mode) {
}
每次重放都要绑定源 Run。
四、Replay Mode至少分四种
public enum ReplayMode {
EXACT,
MODEL_SWAP,
PROMPT_SWAP,
FORK_FROM_STEP
}
EXACT
完全使用原模型、原Prompt、原Tool Fixture。
目的:
验证能否复现
MODEL_SWAP
只替换模型。
目的:
如果换新模型,
同样输入是否还会失败?
PROMPT_SWAP
只换 Prompt。
FORK_FROM_STEP
前 N 步使用历史事实。
从某一步开始重新执行。
这最适合调试。
五、Tool Fixture
public record ToolFixture(
String toolCallId,
String capabilityId,
String requestHash,
JsonNode sanitizedRequest,
JsonNode sanitizedResponse,
ToolOutcome outcome,
Instant startedAt,
Instant completedAt,
String providerRequestId) {
}
Replay 时:
同样 requestHash
→ 返回历史 response
六、Request Hash必须基于Canonical JSON
JSON 字段顺序不同:
{"a":1,"b":2}
和:
{"b":2,"a":1}
逻辑上相同。
不能产生两个 Hash。
所以:
Normalize
Sort Keys
Normalize Number
Normalize Unicode
↓
SHA-256
七、Tool Fixture找不到时怎么办
不要自动访问生产。
定义:
public enum FixtureMissPolicy {
FAIL,
STUB,
SANDBOX_LIVE
}
默认:
FAIL
只有明确允许的无副作用 Tool 才可以:
SANDBOX_LIVE
八、Replay不能执行真实副作用
以下 Tool:
email.send
payment.refund
crm.update
cloud.deploy
file.delete
Replay 只能:
返回Recorded Result
或者:
Dry-run
不能真的再做一次。
九、Side Effect Ledger
public record SideEffectRecord(
String sideEffectId,
String runId,
String stepId,
String capabilityId,
String idempotencyKey,
String actionHash,
SideEffectStatus status,
String providerReceipt,
Instant executedAt) {
}
状态:
public enum SideEffectStatus {
PLANNED,
EXECUTING,
CONFIRMED,
FAILED,
UNKNOWN,
RECONCILED
}
十、UNKNOWN为什么必须单独建模
最危险的窗口:
Agent
→ Payment API
→ 实际退款成功
→ 返回途中网络超时
Agent 看到:
timeout
真实世界:
已经退款
如果系统把 Timeout 直接记:
FAILED
Agent Retry:
重复退款
所以:
无法确定副作用结果
必须记录:
UNKNOWN
十一、UNKNOWN状态下禁止自动Retry
规则:
if (status == SideEffectStatus.UNKNOWN) {
throw new ReconciliationRequiredException();
}
先进入:
Reconcile
十二、Reconciliation Adapter
public interface SideEffectReconciler {
boolean supports(
String capabilityId);
ReconciliationResult reconcile(
SideEffectRecord record);
}
支付:
用 provider_request_id
查询退款状态
邮件:
用 provider_message_id
查询发送记录
如果 Provider 没查询接口:
进入人工
不能盲重试。
十三、Replay首先要冻结Clock
很多 Agent Prompt 里有:
今天
当前时间
本周
30分钟前
如果 Replay 用今天:
2026-08-24
原 Run 用:
2026-08-20
行为自然会不同。
所以 Runtime 不能直接调用:
Instant.now()
而应该:
public interface AgentClock {
Instant now();
}
生产:
SystemAgentClock
Replay:
FixedAgentClock
十四、任何时间相关Tool也要读Logical Time
例如:
get_today_orders
Replay 不应该查询真正今天。
Tool Fixture里直接返回源 Run 结果。
如果是 Sandbox Live:
request_time
必须传入历史 Logical Time。
十五、Prompt必须保存Compiled Artifact
不能只保存:
prompt-template-v8
因为运行时还可能注入:
System Policy
Tool Schema
Skill
Tenant Config
Feature Flag
真正 Replay 要保存:
最终 Compiled Prompt Hash
最好 Artifact 本身也可取回。
十六、Prompt Artifact
public record PromptArtifact(
String artifactId,
String templateVersion,
String compiledHash,
String contentRef,
int tokenCount,
Instant compiledAt) {
}
日志里不要直接长期保存完整敏感 Prompt。
保:
Hash + Encrypted Ref
十七、Context Snapshot
模型输入还包括:
Conversation
Memory
RAG Evidence
Tool Schema
Run State
所以需要:
public record ContextSnapshot(
String snapshotId,
String runId,
String stepId,
List messageRefs,
List evidenceRefs,
List memoryRefs,
String toolCatalogHash,
String stateHash,
String contentHash) {
}
十八、RAG必须保存Evidence ID
不要只保存:
answer used 5 chunks
必须保存:
document_id
document_version
chunk_id
chunk_hash
rank
score
否则知识库更新以后无法还原。
十九、RAG Replay
public record RetrievalFixture(
String queryHash,
String retrieverVersion,
List evidence) {
}
Replay 直接返回原证据集合。
然后可以做 Counterfactual:
同一证据
换新模型
或者:
同一模型
换新检索器
二十、Graph Version必须锁定
Agent Workflow:
Planner
→ Search
→ Tool
→ Reviewer
两周后改成:
Planner
→ Search
→ Reviewer
→ Tool
如果 Replay 不锁 Graph,复现没有意义。
保存:
graph_version
并且旧 Graph Definition 要可加载。
二十一、状态Schema也会变化
Run State v3:
{
"goal": "...",
"plan": [...]
}
Run State v4:
{
"objective": "...",
"steps": [...]
}
Replay 老 Run 时需要:
State Migration
但要注意:
迁移后Replay
和:
原样Replay
不是同一件事。
二十二、保留Raw Snapshot和Migrated Snapshot
raw_state_ref
migrated_state_ref
migration_version
这样事故分析时知道:
差异是不是Migration引入
二十三、决策节点也要保存输入
例如 Policy:
ALLOW
DENY
REQUIRE_APPROVAL
不要只保存结果。
保存:
public record PolicyDecisionRecord(
String policyId,
String policyVersion,
String inputHash,
JsonNode sanitizedInput,
String decision,
List reasons) {
}
未来 Policy 改了,可以重放:
同一输入
新Policy会怎么判
二十四、Approval也要进入Replay
记录:
谁批准
批准哪个Action Hash
何时批准
何时过期
Replay 默认不能重新向用户发审批。
而是:
使用Recorded Approval
只有 Counterfactual Mode 才允许模拟:
如果当时拒绝会怎样
二十五、一个完整Execution Event
public record ExecutionEvent(
long sequence,
String eventId,
String runId,
String stepId,
ExecutionEventType type,
String payloadRef,
String payloadHash,
String previousHash,
Instant logicalTime,
Instant recordedAt) {
}
这里:
logicalTime
和:
recordedAt
必须分开。
二十六、为什么需要Sequence
分布式系统里时间戳可能:
相同
漂移
乱序
所以每个 Run 建立:
monotonic sequence
Replay 按 Sequence。
不是按日志时间字符串排序。
二十七、Hash Chain用于防篡改
event_1_hash
↓
event_2.previous_hash
↓
event_2_hash
计算:
hash(
previous_hash
+ canonical_payload
+ metadata
)
后续修改历史事件:
Chain Break
这对合规取证很有价值。
二十八、Replay Orchestrator
@Service
public class ReplayOrchestrator {
public ReplayResult replay(
ReplayRequest request) {
SourceRun source =
sourceRunLoader.load(
request.sourceRunId());
ReplayEnvironment env =
environmentFactory.create(
source,
request);
ReplayExecution execution =
replayExecutor.execute(
env);
return comparator.compare(
source,
execution);
}
}
二十九、Environment Factory
public record ReplayEnvironment(
AgentClock clock,
ModelGateway model,
ToolGateway tools,
PolicyEngine policy,
KnowledgeGateway knowledge,
GraphDefinition graph,
ReplayManifest manifest) {
}
Replay 的关键不是:
再跑一遍代码
而是重新构建正确环境。
三十、Exact Replay真的能100%一致吗
不能保证。
LLM 本身可能仍有:
Non-determinism
Provider Backend Drift
所以 Exact Replay 不是要求:
字节级完全相同
而是比较:
关键决策
Tool Selection
Task Outcome
Failure Code
三十一、定义Behavioral Equivalence
public record ReplayComparison(
boolean sameOutcome,
boolean sameToolSequence,
boolean sameSideEffects,
double answerSimilarity,
List diffs) {
}
如果最终措辞不同:
没关系
如果:
原来没调用refund
现在调用refund
就是重大差异。
三十二、Trajectory Diff
public record TrajectoryDiff(
DiffType type,
String sourceStep,
String replayStep,
String explanation) {
}
类型:
MODEL_DECISION_CHANGED
TOOL_CHANGED
ARGUMENT_CHANGED
POLICY_CHANGED
OUTCOME_CHANGED
三十三、Counterfactual Replay
这是最有价值的调试方式之一。
例如事故:
Agent发错邮件
你怀疑:
Prompt v17
做:
历史Context
历史Tool Fixture
历史时间
↓
只换Prompt v18
看:
是否仍然发错
这比拿今天新数据测试更有说服力。
三十四、Model Swap Replay
同一个事故:
旧模型
→ 新模型
比较:
Tool选择
Loop
成本
Outcome
可以直接评估模型升级是否真的修复生产失败。
三十五、Fork from Step
完整 Run 可能 40 分钟。
没必要每次从头跑。
选择:
step_17
之前使用历史 Event。
从:
step_18
重新生成。
需要保存:
Checkpoint Snapshot
三十六、Replay Snapshot
public record ReplayCheckpoint(
String runId,
String stepId,
long sequence,
String stateRef,
String stateHash,
String contextSnapshotId,
Instant logicalTime) {
}
加载以后继续。
三十七、Artifact必须版本化
Agent 生成:
代码Patch
CSV
报告
图片
Replay 引用:
artifact_id
version
content_hash
不要只保存路径。
文件内容变化后必须检测。
三十八、Source Code Replay
Coding Agent 需要保存:
base_commit
head_commit
working_tree_patch
dependency_lock
toolchain_version
否则两周后依赖更新,测试结果完全不同。
三十九、Environment Hash
可以由:
OS Image
Runtime
Dependency Lock
Model Profile
Tool Version
生成。
public record EnvironmentManifest(
String containerImageDigest,
String jdkVersion,
String pythonVersion,
String dependencyLockHash,
String toolBundleHash) {
}
四十、执行代码的Replay必须Sandbox
历史 Prompt 可能包含恶意输入。
历史代码也可能不可信。
所以 Replay Worker:
网络默认关闭
生产Credential不可见
只读基础镜像
临时文件系统
CPU/Memory限额
绝不能因为“这是内部历史任务”就放松。
四十一、数据脱敏
Replay Dataset 很容易包含:
用户消息
客户数据
密钥
邮件
合同
所以 Artifact Store 要支持:
Encrypted Raw
+
Sanitized Replay View
一般工程师默认只看 Sanitized。
高权限取证才访问 Raw。
四十二、Redaction必须可重放
如果每次脱敏规则变化,Replay Input 也变化。
所以保存:
redaction_policy_version
以及:
raw_hash
sanitized_hash
四十三、删除请求怎么办
用户要求删除个人数据以后:
Replay Artifact
也必须遵守数据生命周期。
不能因为“审计需要”就永久保留全部内容。
需要:
Retention Class
Legal Hold
Deletion Propagation
四十四、Failure Taxonomy
我会把事故至少分成:
public enum AgentFailureType {
MODEL_DECISION,
PROMPT,
RETRIEVAL,
TOOL,
AUTHORITY,
APPROVAL,
SIDE_EFFECT,
STATE,
CONCURRENCY,
INFRASTRUCTURE,
POLICY,
DATA
}
Replay 报告最终要归到这些类别。
四十五、Failure Signature
例如:
tool=payment.refund
status=UNKNOWN
retry=true
duplicate_side_effect=true
生成稳定 Signature:
SIDE_EFFECT_UNKNOWN_RETRY_DUPLICATE
以后同类事故自动聚合。
四十六、不要让LLM自己定义最终Root Cause
LLM 可以辅助总结 Trace。
但最终 Root Cause 应该由:
Evidence
+
Replay
+
规则
+
人工确认
共同形成。
例如:
“模型推理错误”
这个结论太泛。
更好的:
Prompt v17 未明确优先使用 customer_id,
模型选择手机号检索,
在重复号码数据下返回错误客户。
Prompt v18 Counterfactual Replay 50/50 不再复现。
这才是可行动根因。
四十七、Replay Report
public record FailureForensicsReport(
String incidentId,
String sourceRunId,
String failureSignature,
List evidence,
List replays,
List criticalDiffs,
String confirmedRootCause,
String fixVersion,
String verifiedBy) {
}
四十八、一个完整事故流程
线上告警
↓
冻结Run Evidence
↓
Side Effect Reconcile
↓
生成Failure Signature
↓
Exact Replay
↓
Counterfactual Replay
↓
确认Root Cause
↓
修复
↓
Regression Case
↓
Canary
这才是 Agent 的标准事故闭环。
四十九、线上发现失败后第一件事不是“再跑一次”
尤其涉及副作用。
先:
Freeze Evidence
否则后续:
- 日志轮转;
- 数据变化;
- Tool结果覆盖;
- Artifact更新;
会破坏证据。
五十、Freeze Evidence
public record EvidenceFreezeRequest(
String runId,
String incidentId,
Set types,
Instant retentionUntil) {
}
冻结:
Event
Trace
Prompt
Tool Fixture
Artifact
Approval
Authority
Side Effect Receipt
五十一、Replay Worker最好独立集群
不要和生产 Agent Worker 共用。
原因:
历史任务可能恶意
需要不同网络策略
需要不同Credential
负载不可预测
Replay Cluster:
No Production Write
Recorded Tool First
Low Priority
Separate Quota
五十二、Replay也会很贵
一次长 Agent Run:
50次模型调用
跑:
Baseline
Prompt v18
Model B
Policy v9
就变:
200次调用
所以要做 Replay Budget。
public record ReplayBudget(
int maxModelCalls,
long maxTokens,
BigDecimal maxCost,
Duration maxDuration) {
}
五十三、优先重放“分歧最大的步骤”
可以先用历史 Trace 找:
关键决策点
例如:
Tool Selection
Approval
Side Effect
Reviewer Rejection
不需要每次全量。
五十四、自动回归样本
事故确认以后,把 Source Run 转成:
Regression Case
但不能直接保存所有 PII。
流程:
Production Run
↓
Sanitize
↓
Freeze Tool Fixtures
↓
Define Expected Outcome
↓
Add to Eval Dataset
五十五、事故案例最终要能进入CI
例如:
INC-2026-081
之后每个 Agent Candidate 发布前:
必须跑
直到明确下线这个 Case。
这叫:
Never Repeat Incident
五十六、Replay和Shadow的区别
Replay:
历史真实输入
冻结外部世界
离线重跑
Shadow:
当前真实流量
候选版本同步运行
不影响用户
两个都需要。
Replay适合:
事故取证
历史回归
Shadow适合:
未来发布验证
五十七、Replay和Simulation的区别
Replay:
尽量还原真实历史
Simulation:
故意构造不存在的世界
例如:
Payment API 50% Timeout
属于 Simulation。
同一 Runtime 可以支持两种模式,但 Manifest 要明确。
五十八、Replay API
POST /api/replays
{
"sourceRunId": "run-9182",
"mode": "PROMPT_SWAP",
"candidatePromptVersion": "v18",
"startFromStep": "planner-4",
"budget": {
"maxModelCalls": 20,
"maxCost": 3.0
}
}
返回:
{
"replayId": "replay-882",
"status": "QUEUED"
}
五十九、状态机
public enum ReplayStatus {
CREATED,
PREPARING,
RUNNING,
COMPARING,
COMPLETED,
FAILED,
CANCELED
}
PREPARING 阶段做:
Evidence完整性检查
Fixture检查
版本检查
数据权限检查
缺证据就不要假装 Exact Replay。
六十、Evidence Completeness Score
Prompt 1
Context 1
Tool Fixture 1
Policy 1
Approval 1
Environment 1
例如:
5 / 6
报告:
Replay Confidence = LIMITED
而不是:
Exact Replay Successful
六十一、Mainline Metrics
agent_replay_total{
mode,
result
}
agent_replay_fixture_miss_total{
capability
}
agent_replay_behavior_diff_total{
type
}
agent_unknown_side_effect_total{
capability
}
agent_reconciliation_total{
result
}
agent_incident_regression_case_total
六十二、SLO
我会设:
Critical Run Evidence Completeness = 100%
Side Effect UNKNOWN Auto Retry = 0
Unreconciled Critical Side Effect = 0
Replay using Production Write Credential = 0
Confirmed Incident Regression Coverage = 100%
六十三、必须测试的故障窗口
1. Tool成功,网络响应丢失
2. Outbox写入成功,Broker发送失败
3. Broker重复投递
4. Approval过期后执行
5. Prompt版本找不到
6. Tool Fixture缺失
7. Knowledge Snapshot已删除
8. State Migration失败
9. Replay误访问生产Write API
10. Hash Chain断裂
六十四、一个最关键的自动化断言
@Test
void unknownSideEffectMustNotRetry() {
SideEffectRecord record =
fixture.unknownRefund();
assertThrows(
ReconciliationRequiredException.class,
() -> retryService.retry(record));
verify(paymentGateway, never())
.refund(any());
}
这条测试的价值可能比100个普通Prompt Eval都高。
六十五、为什么“可解释执行”最终一定会走向Replay
只给用户展示:
Agent说:
“我这样做是因为……”
不够。
真正可解释应该能拿出:
当时输入
当时证据
当时规则
当时批准
当时Tool结果
然后:
重新跑
看解释是否成立。
这和 CHIVE 的 Counterfactual 思路其实是同一种工程哲学:
解释必须能够经受干预验证
六十六、本篇上线检查清单
□ 每个Run可以生成Replay Manifest
□ 模型、Prompt、Graph、Policy均版本化
□ Tool结果可以冻结为Fixture
□ Replay默认不访问生产Write Tool
□ Clock可注入并冻结
□ RAG保存Evidence ID与Hash
□ Approval保存Action Hash
□ Side Effect有UNKNOWN状态
□ UNKNOWN禁止自动Retry
□ 每种关键副作用有Reconciler
□ Event使用单调Sequence
□ Audit Event形成Hash Chain
□ Replay支持Exact/Model Swap/Prompt Swap/Fork
□ State保留Raw与Migration版本
□ Artifact保存Content Hash
□ Replay Worker独立Sandbox
□ Raw Evidence加密,默认使用Sanitized View
□ 事故修复后自动沉淀Regression Case
□ Critical事故进入发布回归集
总结
生产 Agent 真正进入复杂业务以后,事故不会只是:
模型答错一句话
而会出现:
模型做了错误决策
Tool已经执行
网络返回丢了
系统以为失败
然后又重试
这类问题靠普通日志很难处理。
Audit Ledger 解决:
“历史上记录了什么”
Execution Replay 解决:
“能不能重新构造当时的执行环境”
Failure Forensics 解决:
“能不能通过重放和对照,证明根因在哪里”
三者合起来,Agent 平台才真正具备类似成熟分布式系统的事故处理能力。
下一篇继续沿着这条生产治理主线:
生产级Agent(20):Agent SLO与错误预算——把Task Success、成本、延迟和副作用风险放进同一套可靠性指标。
更多企业级 AI 应用、Agent、RAG 与模型工程化内容,我会继续整理在 智元界:
https://www.zyentor.com/