统一 Trace 协议
RunTrace / StepTrace / ToolTrace / PolicyTrace / HITLTrace / CostTrace / ErrorTrace 共享字段框架,run_id 串起所有事件
不可篡改写入
仅追加 + 哈希链 + 异步批写,写 trace 不阻塞主流程,归档冷存放在 OSS
三模式 Replay
Pure Trace 只读复盘 / Mock Replay 重跑新逻辑 / Live Sandbox Replay 副作用走专用环境
详细设计说明
让每一次 Agent 运行都可追踪、可回放、可对比
这份文档把 Trace、Audit、Replay 与合规查询统一成一个可观测协议。重点不是多写日志,而是把"发生了什么、为什么这么决策、再跑一次会有什么差异"三个问题变成可工程化的接口和数据。
设计主张
可观测性不是 SRE 的附属品,是企业 Agent Platform 的合规基础设施。Trace 必须不可篡改,Replay 必须不产生副作用,查询必须按租户隔离 —— 这三件事缺一项,Agent 就不能进生产。
评审关注点
- Trace 字段是否能跨类型关联到同一 Run
- 不可篡改证据链是否真能挡住事后改写
- 三种 Replay 的副作用边界是否清楚
- 合规查询是否覆盖审计常见路径
1. 定位与概念
说明 Trace / Replay / 审计在三层架构里的位置,回答 Run 各步骤的事实如何被收集、谁来读、为什么需要 Replay 才能闭环。
1.1 总体架构图
下图把 Trace 的写入路径、存储分层与消费面叠在同一张图:左侧是 Run 八步管道(每一步都写 Trace),中间是 Trace Recorder 收集 + Redis Stream 热缓冲 + MySQL 冷表的两段式持久化,右侧是三个消费场景 —— 合规查询接口、Replay 引擎、审计 dashboard。三种消费方都靠同一份 trace_id 链路上溯。
1.2 时序图:Trace 写入与 Replay 重放
下面两张时序图分别展示正向写入与反向重放的交互闭环。点击 lifeline 头部色块跳到对应章节。
Run 八步管道每一步都向 Trace Recorder emit 一条 trace,Recorder 异步链入 Stream 并批量落 MySQL,最后 SSE 推给 dashboard。
开发者基于 run_id 起一次 Mock Replay:拉历史 trace 重建 Run 上下文,新代码逻辑跑一遍,所有外部副作用走 mock。
1.3 为什么需要 Replay:缺口与问题
仅靠日志和 trace 还原现场只能解决"看见",不能解决"理解"和"对比"。下表列出生产事故中常见的三类追问,及只有 Replay 才能闭环的能力差距。
| 追问场景 | 仅 Trace 能给的答案 | Replay 才能闭环的能力 |
|---|---|---|
| "上周这个 Run 走错 Skill,是模型问题还是 Plan 步骤问题?" | 看到 LLM 输入输出 hash + Plan 决策记录 | 用新版 Plan 提示词重跑这次 input,对比走出的步骤序列 |
| "如果上线新 Policy 规则,过去一周哪些 Run 会被拒?" | 看不到,Policy 逻辑是代码不是数据 | 对历史 Run 批量 Mock Replay,统计被拒计数 |
| "客户问当时 Agent 为什么发了那条飞书,能复盘吗?" | 看到工具调用记录但无法重现交互全貌 | Pure Trace Replay 在 UI 上按时序回放每一步 |
| "灰度新 Harness 内核,是否影响某个 tenant 的成功率?" | 看到旧路径 trace | Live Sandbox Replay:用历史 Request 在新代码起 Run 对比 |
1.4 三层职责
整个可观测体系按消费方/数据流分三层。颜色:T1 蓝=审计看板(人在看)、T2 紫=跨 Run 聚合(程序在分析)、T3 金=单 Run 写入(Run 内核 emit)。
| 层 | 身份 | 读写关系 | 核心职责 | 不做的事 |
|---|---|---|---|---|
| T1 · 审计看板 | 合规 / 客服 / 客户 | 只读 | 按 run_id 时间线展示;hash 链校验徽章;导出审计包 | 不发起 Replay;不改写 trace |
| T2 · 跨 Run 聚合 | SRE / 平台 / Replay 引擎 | 读多写少 | 多维聚合(Skill / Tenant / 时间窗);Replay 三模式调度;Diff 报告生成 | 不直接写 RunTrace;写 replay namespace 不污染生产 |
| T3 · Run 内核 emit | Harness 八步管道 | 只写 | 每步 emit RunTrace / StepTrace / ToolTrace 等;hash 链由 Recorder 接管 | 不查 Trace;不感知 Replay 模式(mock 由 LLM/Tool 注入点切换) |
2. TraceSchema · 七类 Trace 字段定义
定义 Run 内核需要 emit 的全部 trace 类型与字段,保证每条 trace 都能从 run_id 反查上下文。
2.1 共享字段框架
所有七类 trace 共享一组通用字段,类型差异落在 metadata 子字段。共享字段是查询和聚合的统一入口,类型字段是各类 trace 的语义边界。
| 字段 | 类型 | 说明 |
|---|---|---|
trace_id | str | 当前 trace 唯一 ID(snowflake / ulid),全局唯一 |
parent_trace_id | str? | 上游 trace ID,形成树状结构(RunTrace → StepTrace → ToolTrace) |
trace_type | enum | 七类之一:Run / Step / Tool / Policy / HITL / Cost / Error |
run_id | str | 关联 Run,必填 |
session_id / user_id / agent_id | str | 会话 / 用户 / Agent 标识 |
tenant_id | int | 租户隔离的核心字段,DAO 层强制注入 |
start_time / end_time / duration_ms | iso8601 / int | 时序 |
input_hash / output_hash | str? | 输入输出指纹,原文不入 trace(PII 安全) |
seq_no | int | 同 Run 内单调递增,用于重排序与 hash 链 |
prev_hash / self_hash | str | 不可篡改链字段,详见第三章 |
metadata | dict | 类型特定字段,下文按类展开 |
2.2 RunTrace · 一次 Run 生命周期
语义:一次 Run 从创建到终态的总览记录,是其余六类 trace 的根。
写时机:Run start(status=RUNNING)、每次 state transition 增量、Run end(最终聚合)。
| metadata 字段 | 说明 |
|---|---|
job_id | 关联旧 Job 表,迁移期兼容投影 |
trigger_kind | chat / inbound_event / scheduled / api |
state_history | RunState 流转列表(PREPARING → AWAITING_HITL → RUNNING → SUCCEEDED 等) |
terminal_state | SUCCEEDED / CLOSED_REJECTED / CLOSED_FAILED |
total_tokens / total_cost | 聚合自子 CostTrace |
error | 终态失败时填错误摘要(不存敏感原文) |
2.3 StepTrace · 八步管道每步
语义:Harness 八步管道每一步的执行记录,parent_trace_id 指向 RunTrace。
写时机:每个 Step 进入和退出。
| metadata 字段 | 说明 |
|---|---|
step_id | s1..s8 |
step_name | run_create / context_assemble / skill_load / plan / policy / tool / hitl / trace |
state_before / state_after | RunState 转换 |
error | 步骤异常时填,关联 ErrorTrace |
2.4 ToolTrace · 工具调用
语义:每一次 Tool / Skill 调用的具体记录,parent_trace_id 指向对应 StepTrace(s6)。
写时机:Tool call 开始 / 结束 / 每次重试。
| metadata 字段 | 说明 |
|---|---|
tool_call_id | 模型分配的调用 ID |
tool_name / skill_id | 工具与 Skill 标识 |
status | success / failed / timeout / cancelled |
retry_count | 重试次数 |
error_code / error_class | 失败时填 |
cost_tokens | LLM Tool 时填,详细成本另写 CostTrace |
2.5 PolicyTrace · 自动规则裁决
语义:Policy 引擎每一次裁决记录,对应 Step ⑤。
写时机:每条规则评估完毕。
| metadata 字段 | 说明 |
|---|---|
rule_set | 四层之一:结构性 / 权限 / 成本 / 安全护栏 |
rule_id | 具体规则 ID |
decision | PASS / REJECT / WARN |
reason | 决策原因(结构化字段,不存自由文本) |
context_snapshot_hash | 当时上下文指纹,用于 Replay 核对 |
2.6 HITLTrace · 人工审批
语义:HITL 审批流程的全部状态变化,对应 Step ⑦。
写时机:审批请求 / 审批结果 / 超时升级。
| metadata 字段 | 说明 |
|---|---|
risk_level | HIGH / MEDIUM / LOW |
approval_level | L0_AUTO / L1_SELF_CONFIRM / L2_REVIEWER / L3_DUAL_REVIEW |
approver_id | 审批人 user_id |
decision | APPROVE / REJECT / SKIP / TIMEOUT |
requested_at / decided_at / wait_duration_ms | 时序与等待时长 |
timeout_action | 超时默认行为 |
2.7 CostTrace · 单次成本
语义:单次 LLM / Tool 调用的成本流水,独立成表方便聚合查询。
写时机:每次 LLM call / Tool call 完成时。
| metadata 字段 | 说明 |
|---|---|
model | 模型 ID |
input_tokens / output_tokens / cached_tokens / reasoning_tokens | token 统计 |
unit_price_input / unit_price_output | 单价(变化时归档当时单价) |
total_cost / cost_currency | 单次成本 |
2.8 ErrorTrace · 异常上下文
语义:任意 Step 中发生异常时的额外上下文记录,关联具体 StepTrace。
写时机:异常发生时(先于 StepTrace 关闭)。
| metadata 字段 | 说明 |
|---|---|
error_class | 异常类名 |
error_code | 业务错误码 |
error_message | 摘要(敏感原文不入) |
stack_trace_hash | 栈指纹(原栈写归档) |
recovered_action | 重试 / 降级 / 熔断 / 终止 |
retry_count / fatal | 重试次数 / 是否不可恢复 |
3. 不可篡改写入
说明 Trace 写入如何同时满足"不阻塞主流程"与"事后无法改写"两条硬约束,让审计在法律和合规层面站得住。
3.1 三个不可妥协的约束
| 约束 | 含义 | 实现要点 |
|---|---|---|
| 仅追加 | Trace 表只允许 INSERT,不允许 UPDATE / DELETE | DB 层关闭对应权限;只有专用归档迁移账号能 INSERT 到 cold 表 |
| 哈希链锁定 | 同 Run 内每条 trace 的 self_hash 包含 prev_hash,链尾在终态固化 | Recorder 串行计算 hash;终态写入时把 chain_root 落 RunTrace |
| 异步不阻塞 | Run 内核 emit 后立即返回;写盘失败不能让 Run 失败 | 内存收集 + Stream 缓冲 + 批量 ColdWriter;失败重试 + 旁路告警 |
3.2 Hash chain 形成方式
每条 trace 的 self_hash 由两部分输入:当前 trace 的归一化字段(除 hash 字段外)、上一条 trace 的 self_hash(同 Run 内按 seq_no 取上一条;Run 首条以 run_id 起种)。Run 终态时 RunTrace 记录 chain_root_hash = 链尾 hash,作为整个 Run 的不可篡改锚。
| 位置 | 字段 | 来源 |
|---|---|---|
| 每条 trace | prev_hash | 同 Run 内 seq_no - 1 的 self_hash;首条用 sha256(run_id) 起种 |
| 每条 trace | self_hash | sha256(归一化 payload + prev_hash) |
| RunTrace 终态 | chain_root_hash | 该 Run 最后一条 trace 的 self_hash |
| OSS 归档 | 归档清单 | chain_root_hash + run_id 摘要列表,定期签名 |
3.3 异步批写链路
从 Run 内核到 MySQL 中间分三段缓冲,每段都有降级策略:
- Run → Recorder:内存通道,emit 走 fire-and-forget;通道满时丢入磁盘旁路队列并告警
- Recorder → Redis Stream:单 Run 内串行(保证 hash 链顺序),跨 Run 并行;Stream 不可达时切到本地落盘 + 后台补偿
- Stream → MySQL ColdWriter:按 trace_type 分表批量 INSERT;INSERT 失败仅重试不丢事件,连续失败超阈值升级 SRE 告警
3.4 存储分层与 TTL
| 层 | 介质 | TTL | 用途 | 不可变保证 |
|---|---|---|---|---|
| Hot | Redis Stream | 24h | 实时 SSE 推流 / 调试 | 读 only API |
| Warm | MySQL trace 表 | 30 天 | 业务运营查询 / 一般审计 | 仅追加 + 行级 hash 校验 |
| Cold | OSS 归档 | 6 个月+ | 合规审计 / 长期存证 | 对象不可删除策略 + chain_root 签名清单 |
4. Replay 三模式
说明 Pure / Mock / Live Sandbox 三种 Replay 的能力边界、副作用控制和适用场景,让评审者一眼判断"在生产能不能跑"。
4.1 三模式总览
| 模式 | 是否重跑代码 | 外部副作用 | 典型用途 | 能在生产跑吗 |
|---|---|---|---|---|
| Pure Trace Replay | 不重跑,仅按时序重建 UI | 零副作用 | 客户复盘 / 客服解释 / 审计追溯 | 可以,仅读 |
| Mock Replay | 重跑 HarnessPipeline,LLM/Tool 走 mock | 不调外部、不扣费、不写生产表 | 新代码回归 / Policy 改动批量验证 | 可以,写 replay namespace |
| Live Sandbox Replay | 重跑全套,外部走 sandbox endpoint | 调专用沙箱(飞书测试号 / 测试多维表) | 新旧 Harness A/B / 端到端基线 | 不可以,仅在 Replay 专用环境 |
4.2 Pure Trace Replay · 只读回放
用途:复盘 / 审计 / 给客户讲清"当时发生了什么"。
行为:从 Redis Stream 或 MySQL 拉 RunTrace + 全部子 trace,按 seq_no 在 UI 还原八步管道每一步的输入指纹、输出指纹、决策结果;不重新执行任何代码、不调任何外部系统。
输出:可视化时间线 + 关键决策节点 + hash 链校验徽章。
4.3 Mock Replay · 重跑但 mock 外部依赖
用途:用历史数据 debug 新代码。回答"如果我现在改了 Step 5 Policy 规则,这个历史 Run 还会通过吗"这一类问题。
| 外部依赖 | Mock 数据来源 |
|---|---|
| LLM 响应 | 对应 ToolTrace(type=llm_call)的 output_hash 反查归档 |
| Tool 响应 | ToolTrace.output_hash 反查存储 |
| HITL 决策 | HITLTrace.decision 直接复用 |
| 外部 API(飞书 / 多维表 / 邮件) | 不调,返回历史 hash 对应载荷 |
禁止行为:真调任何外部 API;真消耗 LLM token;写生产 trace 表(必须写 replay_* 前缀的 namespace)。
4.4 Live Sandbox Replay · 重新执行新数据
用途:A/B 对比新旧 Harness 内核、新旧模型、新旧 Memory 策略;端到端回归基线。
行为:用历史 RunRequest 在新代码起一个全新 Run,外部副作用全部走专用 sandbox endpoint:
- 飞书 webhook → sandbox 测试号
- 多维表 → 专用测试 base
- 邮件 → 专用测试邮箱
- LLM 真调,但用专用计费账号 + 流量配额护栏
仅在专用 Replay 环境运行,生产环境禁止。
4.5 模式切换在 Run 内核上的实现
Replay 引擎调用 HarnessPipeline 时传入 replay_mode,Run 内核本身不感知三模式语义,由 LLM Router / Tool Invoker / HITL Coordinator 在注入点根据 mode 切换实现:Pure 不会进入到此(直接 UI 渲染);Mock 注入 MockHandlers;Live Sandbox 注入指向 sandbox 的 client。StateMachine 不变,合法性约束在 Replay 模式下仍生效。
4.6 安全约束
| 类型 | Pure | Mock | Live Sandbox |
|---|---|---|---|
| 外部 API 调用 | 不调 | 不调 | 调 sandbox endpoint |
| LLM 调用 | 不调 | 不调(mock) | 真调 + 配额护栏 |
| 数据库写入 | 不写 | 写 replay_* namespace | 写 replay_* namespace |
| 凭证消费 | 不消费 | 不消费 | 消费 replay 专用凭证 |
| 权限要求 | replay:read | replay:execute | replay:execute + sandbox 环境标识 |
| 跨 tenant 访问 | 需 replay:cross-tenant,仅平台运维 | 同上 | 同上 |
任何 Replay 都写一条 AuditLog 关键事件 replay_executed,含 mode / actor / 原 run_id / 新 run_id 四元组。
5. 合规查询接口
定义对外暴露的查询接口,让审计、客服、SRE 都用同一组 API,并保证租户行级过滤是接口层硬约束。
5.1 查询场景与接口映射
| 查询场景 | 接口 | 主要参数 | 返回 |
|---|---|---|---|
| 单 Run 全链路视图 | GET /api/v1/traces/run/<run_id> | run_id | RunTrace + 全部子 trace 按 seq_no 排序,hash 链校验状态 |
| 按 trace_type + 时间窗 | GET /api/v1/traces | type, from, to, tenant_id | 分页 trace 列表 |
| 按用户 / agent 过滤 | GET /api/v1/traces | user_id 或 agent_id, scope, time | 同上 |
| Skill 维度聚合 | GET /api/v1/traces/agg | dim=skill_id, metric=p95_latency | 聚合点序列 |
| User / Workspace 成本 | GET /api/v1/cost | user_id 或 workspace_id, time | 基于 CostTrace 的成本聚合 |
| 审计追溯包导出 | GET /api/v1/audit/export | run_id 或 时间窗 + tenant | 含 chain_root 签名的离线包 |
5.2 租户隔离与权限
| 层 | 规则 |
|---|---|
| API Gateway | 从 Token 解出 tenant_id 注入 request scope |
| Service 层 | 所有查询参数显式带 tenant_id;不允许 tenant_id IS NULL |
| DAO 层 | SQL 强制 WHERE tenant_id = :tid,违反编译期禁用 |
| 跨租户读 | 需 admin:cross-tenant 权限,且写一条 AuditLog |
5.3 查询性能与缓存
- 单 Run 全链路:MySQL 索引 (run_id, seq_no),单次查询 O(N) 子 trace 数
- 聚合查询:物化视图按 (tenant_id, day, dim) 预聚合,T+1 刷新
- 实时面板:直接走 Redis Stream(24h 窗口),不打 MySQL
- 导出:异步任务 + OSS 临时签名 URL,避免长连接
6. SSE 事件流
说明运行中事件如何通过 SSE 推到前端,以及历史回看怎么和实时流共用一条协议,让 dashboard / 调试 UI 都用同一个连接处理。
6.1 事件类型
| 事件 | 触发时机 | 载荷要点 |
|---|---|---|
run.started | Run 创建写入 RunTrace | run_id, agent_id, trigger_kind |
step.entered / step.exited | 每个 Step 进出 | step_id, state_before/after, duration_ms |
tool.invoked / tool.result | Tool 调用前后 | tool_call_id, status, output_hash, cost_tokens |
policy.decided | Policy 评估结束 | rule_id, decision, reason |
hitl.requested / hitl.decided | HITL 请求与回复 | risk_level, approver_id, decision |
thinking | LLM 流式 token(可选订阅) | delta token,仅热路径,不入冷存 |
error | 异常发生 | error_code, error_class, fatal |
run.finished | Run 终态 | terminal_state, total_cost, chain_root_hash |
6.2 实时流与历史回看共用协议
- 实时:客户端 GET /api/v1/sse/run/<run_id>,从 Redis Stream 当前位置 XREAD,事件即推
- 历史回看:客户端带
?from_seq=<n>,先从 MySQL 按 seq_no 顺序回放,再无缝切到 Redis Stream(如果 Run 还在跑) - 断线重连:客户端记录最后收到的 seq_no,重连用
Last-Event-IDheader,服务端从该 seq_no+1 续推
6.3 多租户与权限
| 校验 | 说明 |
|---|---|
| tenant_id 校验 | SSE 建连时必须确认 caller 与 run_id 同租户,否则 403 |
| 事件过滤 | thinking / 内部决策事件按权限分级,普通用户默认不订阅 |
| 速率护栏 | 每连接 QPS 限制;同 Run 同用户连接数上限 |
| 断线收敛 | 30 分钟无心跳自动断开,避免连接堆积 |
6.4 与查询接口的边界
SSE 给"运行中要看进度"和"事后按 seq 顺序回放事件流"这两个场景;§5 查询接口给"按多维度筛选 trace"和"做聚合统计"这两个场景。两者都基于同一份 trace 数据,只是消费形态不同:SSE 是事件推送,查询是数据集合。Replay 引擎的 Pure 模式在 UI 上其实就是历史 SSE 回看 + UI 渲染层。