Skip to content
Figo Blogs
Go back

工业级 Agent Session:Event Log、State Reducer、Resume 与 Replay

Contents

Table of contents

Open Table of contents

1. 为什么 messages[] 不足以支撑工业级 Agent

最小 Agent 通常直接维护:

messages = [
    {"role": "system", "content": "..."},
    {"role": "user", "content": "..."},
    {"role": "assistant", "tool_calls": [...]},
    {"role": "tool", "content": "..."},
]

对于 Demo,这已经足够。

但一旦 Agent 进入长期运行,就会遇到:

用户中途 Steering
Tool 执行中断
Permission Pending
Permission Denied
Session Resume
Context Compaction
Tool Retry
并发 Tool Call
Subagent
Checkpoint
模型输出部分成功
进程崩溃恢复
审计与 Replay

这些信息并不天然适合被压进一串模型消息。

例如:

用户拒绝某个 Tool

模型最终可能只需要看到:

Tool execution was denied.

但 Runtime 还需要知道:

谁拒绝的?
什么时候拒绝的?
哪条 Policy 命中?
原 Tool Call 参数是什么?
是否允许后续重新申请?

因此:

模型需要看到的 Context,只是 Session 事实的一种投影,而不是 Session 本身。


2. 三层模型:Event、State、Context

这是整篇最重要的心智模型。

2.1 Event Log:发生过什么

事件是不可变事实,例如:

UserMessage
AssistantMessage
ToolCallRequested
PermissionRequested
PermissionGranted
PermissionDenied
ToolExecutionStarted
ToolExecutionCompleted
ToolExecutionFailed
SteeringReceived
CompactionCreated
CheckpointCreated
SessionSuspended
SessionResumed
CancellationRequested

2.2 Derived State:现在是什么状态

例如:

当前用户目标
当前 step
哪些 Tool Call 正在执行
哪些 Tool Call 已完成
是否等待用户审批
是否存在 Steering
最近一次 Compaction
当前 Token Budget
是否处于 Suspended

这些状态可以从 Event Log 推导,而不一定需要作为独立“事实”反复写入。


2.3 Context:模型下一步应该看到什么

Context Builder 决定:

System Instructions
Project Instructions
Relevant Conversation
Current Goal
Recent Tool Results
Compacted History
Pending Constraints
Latest Steering
Tool Schemas

最终转换成模型 API 所需要的:

messages
tools
metadata
Important

Session State 和 Model Context 不能混为一谈。

Runtime 可以知道很多信息,但模型不一定需要全部看到。


3. Event Sourcing 思维为什么适合 Agent

Agent 天然是一个事件密集型系统:

用户输入
↓
模型响应
↓
Tool Call
↓
Permission
↓
执行
↓
Tool Result
↓
下一轮推理

所以采用 Append-only Event Log 有明显优势。

它允许把:

状态推进
持久化
审计
调试
上下文构建
回放

建立在同一组事实之上。


4. Event 设计:不要把所有东西都叫 Message

一个常见坏味道是:

Everything = Message

结果:

Permission
Tool Runtime
Checkpoint
Compaction
Cancellation

都被强行伪装成对话消息。

更合理的 Event Taxonomy 可以拆成几类。

4.1 Conversation Events

UserMessage
AssistantMessage
SystemObservation
SteeringReceived

这些事件可能直接影响模型上下文。


4.2 Tool Lifecycle Events

ToolCallRequested
ToolCallValidated
ToolExecutionStarted
ToolExecutionCompleted
ToolExecutionFailed
ToolExecutionCancelled
ToolExecutionTimedOut

这些事件描述真实执行生命周期。


4.3 Permission Events

PermissionRequested
PermissionGranted
PermissionDenied
PermissionExpired

4.4 Context Events

CompactionStarted
CompactionCompleted
ContextPruned
SummaryCreated

4.5 Session Lifecycle Events

SessionCreated
SessionSuspended
SessionResumed
CheckpointCreated
SessionCompleted
SessionFailed

4.6 Runtime / Governance Events

RetryScheduled
LoopDetected
CancellationRequested
PolicyViolation
SandboxViolation
Tip

Event 类型越明确,后续 Observability、Eval、Replay 越容易做。

不要只存一个:

{"type": "error", "message": "..."}

然后让所有后处理系统猜它到底是什么错误。


5. 推荐 Event 数据结构

一个 Event 至少应该具备:

@dataclass
class SessionEvent:
    event_id: str
    session_id: str
    seq: int

    type: str
    timestamp: float

    payload: dict

    parent_event_id: str | None = None
    correlation_id: str | None = None

核心字段含义:

字段作用
event_id全局唯一标识
session_id所属 Session
seqSession 内严格排序
type事件类型
timestamp时间
payload事件数据
parent_event_id因果关系
correlation_id把一次 Tool Call / Retry / Approval 链路串起来

对于 Agent Runtime 来说,seq 往往比时间戳更关键,因为:

时间相近不代表逻辑顺序,Session 状态恢复需要确定的事件顺序。


6. State Reducer:从事件推导状态

可以把 State Reducer 理解成:

state = reduce(events)

它的职责不是“调用模型”,而是:

根据发生过的事实
↓
得到现在的 Runtime 状态

例如:

@dataclass
class AgentState:
    status: str
    step: int

    pending_tool_calls: dict
    running_tool_calls: dict
    completed_tool_calls: dict

    pending_approval: dict | None

    latest_user_goal: str | None
    latest_steering: str | None

    last_compaction_event_id: str | None

Reducer:

def reduce_event(state, event):
    if event.type == "ToolExecutionStarted":
        state.running_tool_calls[event.payload["call_id"]] = event.payload

    elif event.type == "ToolExecutionCompleted":
        call_id = event.payload["call_id"]
        state.running_tool_calls.pop(call_id, None)
        state.completed_tool_calls[call_id] = event.payload

    elif event.type == "PermissionRequested":
        state.pending_approval = event.payload

    elif event.type == "PermissionGranted":
        state.pending_approval = None

    elif event.type == "SteeringReceived":
        state.latest_steering = event.payload["content"]

    return state

7. 为什么 Derived State 很重要

如果把所有状态都直接修改:

session.pending_tool = ...
session.status = ...
session.messages.append(...)
session.permission = ...

系统崩溃后你必须相信:

内存中的这些字段一定全部同步成功

而 Event Log 思路是:

事实先落盘
↓
状态由事实推导

因此恢复时:


8. Crash Consistency:事件什么时候写入

这是生产级 Session 很关键的细节。

例如:

Agent 决定执行 Tool
↓
Tool 实际执行成功
↓
Harness 崩溃
↓
ToolResult 尚未写入 Session

恢复后系统看到:

ToolCallRequested

但不知道:

Tool 到底有没有执行成功?

这是典型的 Unknown Outcome。

因此 Tool Lifecycle 最好显式记录:

ToolCallRequested
↓
ToolExecutionStarted
↓
ToolExecutionCompleted

而不是只有:

ToolCall
↓
ToolResult

9. Tool 执行恢复与幂等性

恢复时最危险的是:

不知道 Tool 是否已经产生副作用

比如:

发送邮件
创建 GitHub Issue
删除文件
创建云资源
提交订单

不能简单:

if no ToolResult:
    retry()

更合理的流程:

Important

Resume 不等于 Retry。

Resume 首先要做的是恢复事实与状态,然后根据 Tool 的副作用性质决定下一步。


10. Checkpoint:是否还需要

如果已经有完整 Event Log,理论上可以从第一个 Event 一路 Replay。

但 Session 很长以后:

10 万 Event

每次都从头 Reduce 会很昂贵。

因此可以加入 Checkpoint:

Event 1
Event 2
...
Event 5000
↓
Checkpoint
↓
Event 5001
Event 5002
...

恢复时:

Load Latest Checkpoint
↓
Replay Events After Checkpoint
↓
Current State

11. Checkpoint 和 Compaction 不是一回事

这两个概念很容易混淆。

概念面向谁目的
CheckpointRuntime快速恢复状态
CompactionModel Context减少模型上下文体积

Checkpoint 保存的可能是:

pending tool calls
permission state
session status
latest event seq
runtime metadata

Compaction 保存的是:

任务摘要
重要历史
关键文件修改
未解决问题

因此:

Checkpoint 是 Runtime State Optimization;Compaction 是 Context Optimization。


12. Context Projection:Event 不应该全部进入模型

Session 可能有:

PermissionRequested
PermissionGranted
ToolExecutionStarted
Trace Metadata
RetryScheduled
SandboxProfile
CheckpointCreated

模型并不需要看到这些全部事实。

Context Builder 应该把 Runtime Event 转换为模型有用的信息。

例如:

PermissionRequested
PermissionDenied

可以投影成:

Tool execution was denied by user policy.
Choose a safer alternative.

这就是:


13. Session 与 Context 的边界

推荐强制建立这条规则:

Session can be richer than Context.

例如 Session 可以保存:

完整 Tool 输出
完整 Diff
完整 Trace
Permission Metadata
Sandbox Metadata
Token Metrics
Timing Metrics

但 Context 只选择:

下一步推理真正需要的信息

这样才能做到:

可审计
+
可恢复
+
上下文受控

而不是为了节省 Token 直接丢掉历史事实。


14. Compaction 应该产生 Event

如果系统进行了 Context Compaction,不应该悄悄把:

旧 messages

直接替换掉。

更合理的是记录:

CompactionCompleted
{
    source_event_range,
    summary,
    strategy,
    token_before,
    token_after
}

这样:

Runtime 历史仍然完整
↓
Context 可以选择 Summary

因此:

Compaction 不应该破坏 Session 的事实历史。


15. Steering 也应该是一等 Event

用户中途说:

不要修改 auth.py,只改调用方。

这不是普通聊天消息那么简单。

它会影响:

当前 Goal
当前 Plan
正在执行的 Tool
后续 Context

因此推荐:

SteeringReceived

作为显式事件。

处理流程:


16. Resume:恢复的是 Session,不只是 Messages

最简单的 Resume:

messages = load_messages()
continue_loop()

只适用于没有复杂 Runtime 状态的 Agent。

工业级 Resume 至少需要恢复:

Last Event Seq
Current Session Status
Pending Approval
Running / Unknown Tool Calls
Latest Goal
Latest Steering
Last Compaction
Checkpoint
Cancellation State

推荐流程:


17. Replay:不是简单重新调用模型

Replay 可以有两种不同含义。

17.1 State Replay

只重放 Event:

Events
↓
Reducer
↓
Derived State

目的是:

恢复状态
调试
审计

不重新调用模型。


17.2 Execution Replay

真正重新运行:

模型
Tool
Sandbox

目的是:

复现实验
回归测试
Eval

但由于模型输出可能非确定,Execution Replay 不一定生成完全相同结果。

因此:

State Replay 可以高度确定;Agent Execution Replay 通常只能做到“环境与输入可复现”,不能保证模型路径完全一致。


18. Deterministic Replay 的边界

如果希望最大程度复现一次 Agent 行为,需要固定:

Model Version
Prompt / Context
Tool Schemas
Repository Commit
Environment Image
Dependency Versions
Sandbox Policy
Random Seed(若支持)
Tool Outputs / External Responses

但仍然要承认:

Remote Model Service
Network
External API
动态网页
时间相关数据

都可能导致不同结果。

所以生产级 Trace 应区分:

Replayable Input
Recorded Observation
External Nondeterminism

19. Fork:从某个历史点分叉

Fork 对 Agent 非常有价值。

例如:

同一个 Bug
↓
路径 A:修改 API
路径 B:修改调用方

如果有 Event Log,可以:

Fork 可以用于:

Plan Comparison
Model Comparison
Prompt Experiment
Human Alternative
Eval

20. Fork 的存储模型

不需要复制整个 Session。

可以表示成:

Branch Session
├── parent_session_id
├── fork_event_seq
└── branch_events[]

读取时:

Parent Events until fork point
+
Branch Events

得到当前分支状态。

这和 Git 的思维非常接近。


21. Event Causality:并发 Tool 后顺序怎么办

如果 Agent 同时执行:

read_file(A)
read_file(B)
grep(C)

它们可能不同时间完成。

Session 中至少存在两种顺序:

逻辑请求顺序
实际完成顺序

因此 Event 可以记录:

call_id
parent_event_id
correlation_id
batch_id

避免只依赖:

timestamp

推断因果。


22. 推荐 Tool Event 链

例如:

AssistantMessage
↓
ToolCallRequested
↓
ToolCallValidated
↓
PermissionGranted
↓
ToolExecutionStarted
↓
ToolExecutionCompleted
↓
ToolResultProjected

如果失败:

ToolExecutionStarted
↓
ToolExecutionFailed
↓
ToolResultProjected

这样 Observability 可以清楚区分:

模型调用错了
参数错了
权限拒绝
Runtime 失败
Tool 自身失败

23. Session Status 应该显式存在

推荐最少:

CREATED
RUNNING
WAITING_APPROVAL
SUSPENDED
COMPLETED
FAILED
CANCELLED

状态迁移示意:

Important

WAITING_APPROVAL 和 SUSPENDED 不应简单等价。

前者是明确等待外部决策;后者可能是用户暂停、资源不足或计划中的中断。


24. Session 与 Agent Loop 的职责边界

一个常见坏设计:

AgentLoop
├── messages
├── persistence
├── permissions
├── retries
├── tool history
├── compaction
├── trace
└── resume

所有东西都塞进 AgentLoop。

更合理:

AgentLoop
↓
Session API

例如:

session.append_event(...)
session.current_state()
session.latest_checkpoint()
session.events_after(...)

Loop 负责推进,Session 负责事实与状态。


25. 推荐 Session API

学习版可以设计成:

class Session:
    async def append_event(self, event_type, payload): ...
    async def get_events(self, after_seq=None): ...
    async def rebuild_state(self): ...
    async def create_checkpoint(self): ...
    async def fork(self, at_seq): ...
    async def resume(self): ...

ContextManager:

class ContextManager:
    async def build(self, session): ...
    async def compact(self, session): ...

这样:

Session ≠ ContextManager

职责清晰。


26. Session Store:从内存到持久化

第一版可以:

In-memory List

第二版:

JSONL

第三版:

SQLite / Postgres

建议学习顺序:

List
↓
Append-only JSONL
↓
SQLite
↓
真正的 Event Store / Database

JSONL 很适合学习,因为每一行就是一个 Event:

{"seq":1,"type":"UserMessage","payload":{"content":"fix bug"}}
{"seq":2,"type":"AssistantMessage","payload":{...}}
{"seq":3,"type":"ToolCallRequested","payload":{...}}

27. 为什么 Append-only 很重要

如果直接 UPDATE 历史:

UPDATE messages SET content = ...

你会失去:

原始事实
修改历史
审计依据
Replay 能力

Append-only 的基本原则:

新的事实产生新的 Event,而不是悄悄修改旧事实。

例如 Permission:

错误方式:

permission.status = "approved"

更好的方式:

PermissionRequested
↓
PermissionGranted

28. 但 Event Log 也不是万能的

Event Sourcing 会引入复杂度:

Event Schema Version
Migration
Large Log
Snapshot / Checkpoint
Duplicate Event
Ordering
Consistency
Storage Cost
PII / Secret Redaction

因此不要机械照搬。

对于简单 Agent:

messages + lightweight runtime state

完全合理。

只有当系统开始需要:

Resume
Replay
Audit
Fork
Long-running
Concurrent Tool
Human Approval

时,Event Model 的收益才明显增加。


29. Event Schema Versioning

长期运行的 Harness 会升级代码。

旧 Session 里的:

ToolExecutionCompleted v1

可能和新代码的:

ToolExecutionCompleted v3

结构不同。

Event 应携带版本:

{
  "type": "ToolExecutionCompleted",
  "schema_version": 2,
  "payload": {}
}

Reducer 可以:

Old Event
↓
Upcaster / Migration
↓
Current Event Shape
↓
Reducer

30. Secret / Sensitive Data Redaction

Event Log 很容易保存:

API Key
Environment Variables
Authorization Header
User File Content
Private Repository Data

而 Event Log 又通常长期持久化。

因此 Session Store 应考虑:

Redaction
Secret Masking
Retention Policy
Encryption
Access Control

尤其不要直接把:

完整 shell environment

无条件写入 Trace。


31. Observability:Event Log 天然是 Trace 来源

Session Event 可以转换成 Trace:

Turn 4
├── Inference
│   ├── input_tokens
│   └── latency
├── ToolCall
│   ├── name = bash
│   └── args_hash
├── Permission
│   └── auto_allowed
├── Execution
│   ├── exit_code = 1
│   └── duration = 3.2s
└── Observation
    └── test failed

然后可计算:

Tool Error Rate
Retry Count
Approval Count
Compaction Count
Dead-loop Rate
Session Duration
Recovery Rate

32. Session Model 与 Eval 的关系

Eval 不应该只记录最终:

success = true

如果 Session Event 完整,就能分析:

为什么成功?
为什么失败?
失败发生在哪一层?
成功花了多少步?
有没有死循环?
发生了多少次 Permission Denied?
Compact 后是否出现性能下降?

这使得 Harness 优化可以从:

感觉

进入:

可测量的系统工程

33. 推荐的完整 Session 架构

这张图可以作为本章最终心智模型。


34. 推荐实践:自己实现一个 SessionStore

实验 1:JSONL Event Log

实现:

await session.append_event(
    "UserMessage",
    {"content": "fix bug"}
)

写入:

session.jsonl

实验 2:Reducer

实现:

state = rebuild_state(events)

要求:

  • Pending Tool
  • Completed Tool
  • Permission
  • Latest Goal
  • Session Status

都能正确恢复。


实验 3:Crash Recovery

模拟:

ToolExecutionStarted
↓
Process Crash

重启 Agent。

检查它能否检测:

Incomplete Tool Operation

而不是直接重试。


实验 4:Checkpoint

每 100 个 Event:

create_checkpoint()

比较:

Full Replay
vs
Checkpoint + Incremental Replay

恢复时间。


实验 5:Fork

从:

seq = 50

Fork 一个 Branch。

让两个 Agent 分别尝试不同修复方案。


实验 6:Compaction Event

执行 Context Compaction:

Event 1~100
↓
Summary

验证:

  • 原事件仍存在
  • Context 使用 Summary
  • Replay 仍能访问原历史

35. 本章必答的 18 个问题

  • 为什么 messages[] 不等于 Session?
  • Event Log 为什么可以成为 Source of Truth?
  • Derived State 是什么?
  • Context 为什么只是 Session 的投影?
  • 为什么 Tool Execution 需要 Started / Completed 两个事件?
  • 什么是 Unknown Outcome?
  • Resume 为什么不能简单 Retry?
  • Tool 幂等性与 Session 恢复有什么关系?
  • Checkpoint 与 Compaction 有什么区别?
  • Compaction 为什么应该产生 Event?
  • Steering 为什么适合成为一等 Event?
  • State Replay 与 Execution Replay 有什么区别?
  • 为什么模型执行无法保证完全 Deterministic Replay?
  • Fork 如何避免复制整个 Session?
  • 并发 Tool 的事件顺序怎么表达?
  • Session Status 为什么应该显式存在?
  • Event Schema 为什么需要版本管理?
  • 为什么 Event Log 必须考虑 Secret Redaction?

36. 与下一篇 Context Management 的衔接

有了 Session / Event Model 后,下一步自然就是:

Session 中有这么多事实,下一次模型到底应该看到哪些?

也就是:

Session Events
↓
Derived State
↓
Selection
↓
Pruning
↓
Truncation
↓
Compaction
↓
Ordering
↓
Prefix Stability
↓
Model Context

因此下一篇:

Context Management:上下文选择、裁剪、压缩与缓存治理

会重点解决:

模型看到什么
为什么看到这些
哪些历史被删除
哪些被压缩
Tool Result 保留多久
Context Budget 如何计算
Prefix Cache 如何稳定
Summary Drift 如何避免

37. 一句话总纲

Quote

工业级 Agent Session 的关键,不是“保存聊天记录”,而是把整个 Agent 执行过程表示成可持久化的事实序列,再由这些事实推导 Runtime State,并为每一次模型调用构造受控 Context;只有这样,Resume、Replay、Fork、Compaction、Audit 与 Observability 才能建立在同一套可靠基础之上。


Share this post:

Previous Post
Context Management:上下文选择、裁剪、压缩与缓存治理
Next Post
企业级大数据平台架构科普(华为云 HCS / FusionInsight / MRS 视角)