## 长任务一定会被中断

网络会断，模型服务会限流，进程会重启，用户会关闭电脑，外部 API 也可能在返回结果前超时。

如果 Agent 只能在一段内存里的消息数组中工作，那么任何中断都可能让它忘记已经做了什么。更麻烦的是，外部世界不会跟着它一起回滚：邮件可能已经发出，工单可能已经创建，只是成功回执没有来得及保存。

负责让任务经得起这些情况的，是 Harness 第六个核心部分：**状态与持久化**。

它不只是“保存聊天记录”，而是记录任务执行中的事实、阶段和副作用，让系统可以回答：发生了什么、现在进行到哪里、接下来怎样安全继续。

本文是《[把 Harness 讲清楚：Agent 背后的运行系统](https://www.ittt.cc/articles/15)》八个核心部分的第六篇。

## Harness 需要保存哪些状态

至少有五类：

| 状态 | 示例 |
| --- | --- |
| 会话状态 | 用户消息、模型回复、工具调用与结果 |
| 任务状态 | 目标、当前阶段、计划、待办、完成标准 |
| 执行状态 | 当前 Turn、Step、预算、重试次数 |
| 外部副作用 | 已创建的订单、已发送消息、已发布文章 |
| 控制状态 | 审批、拒绝、取消、暂停和恢复决定 |

这些状态的持久化要求不同。流式输出片段可以批量写入，付款结果和审批决定则必须尽快可靠保存。

## 为什么只保存最终消息不够

假设日志里只有：

```text
用户：发布这篇文章。
Agent：文章已发布。
```

这无法回答：发布工具是否真的被调用？目标分类是什么？接口返回了哪个文章 ID？网络超时后是否重试过？最终结论来自真实回执还是模型猜测？

更可靠的记录应包括过程事实：

```text
turn/start
user/message
assistant/message，包含 publish_article 调用
tool/call，携带调用 ID 与参数摘要
tool/result，返回文章 ID 23 和公开地址
assistant/message，向用户报告结果
turn/end，状态 completed
```

模型可见历史可以从这些事实投影出来，审计和恢复也使用同一份记录。

## 事件日志比可变快照更容易追踪

只保存一份不断覆盖的 `current_state.json` 很简单，却会失去变化过程。事件日志则记录每次变化：

```json
{"seq": 101, "type": "step/start", "turn": 4, "step": 2}
{"seq": 102, "type": "tool/call", "call_id": "c17", "name": "run_tests"}
{"seq": 103, "type": "tool/result", "call_id": "c17", "exit_code": 0}
{"seq": 104, "type": "step/end", "turn": 4, "step": 2}
```

事件是仅追加的事实，当前状态则由事件折叠得到：

```python
def reduce(state, event):
    if event.type == "step/start":
        state.current_step = event.step
    elif event.type == "tool/result":
        state.results[event.call_id] = event.result
    elif event.type == "turn/end":
        state.turn_status = event.reason
    return state
```

为了提高读取速度，可以定期保存快照，但快照应能由事件重新构建，而不是成为另一份互相冲突的真相。

## 一次安全恢复的流程

```text
进程重新启动
    ↓
读取最后一个持久检查点
    ↓
重放检查点之后的事件
    ↓
检查是否存在未闭合的 Turn、Step 或 Tool Call
    ↓
为中断状态补充明确结果
    ├─ 尚未开始 → 可以重新调度
    ├─ 已完成并有回执 → 直接复用
    └─ 可能执行但无回执 → 标记 outcome_unknown
    ↓
恢复上下文，但默认不自动继续高风险工作
    ↓
按策略或用户决定恢复执行
```

恢复不是简单地把最后一条消息重新发给模型。系统必须先修复到一个协议合法、语义清楚的状态。

## 最难处理的是“结果未知”

调用发布接口时，服务端已经创建文章，但连接在响应到达前断开。Harness 只知道请求发出过，不知道外部结果。

此时有三种错误做法：

- 当作失败并直接重试，可能重复发布；
- 当作成功继续，可能实际没有发布；
- 丢掉这段状态，让模型自行猜测。

正确做法是显式记录 `outcome_unknown`，再根据工具语义处理：

```python
if tool.is_read_only:
    retry()
elif tool.is_idempotent and request_key_exists:
    retry_with_same_key()
elif tool.can_query_status:
    verify_external_state()
else:
    ask_user_before_retry()
```

“不知道”是一种必须被保存的状态，而不是异常文本里的临时描述。

## 幂等让重试变得安全

幂等意味着同一个逻辑请求重复执行，不会产生多个不同副作用。

创建文章时可以生成稳定请求键：

```python
request_key = hash(
    task_id,
    operation="publish_article",
    source_revision=document.revision,
)

result = publish_article(
    content=document.content,
    request_key=request_key,
)
```

如果客户端超时，再次使用同一个键，服务端应返回第一次创建的文章，而不是新建第二篇。

幂等键不能每次重试都随机生成，否则失去意义。内容发生实质变化时，又应该生成新版本键，避免错误复用旧结果。

## 检查点应该放在哪里

所有 token 都同步落盘最安全，却可能严重影响性能；只在任务结束保存最快，却经不起崩溃。

常见检查点包括：

- 接收任务契约后；
- 每次模型消息组装完成后；
- 有副作用工具执行前和结果返回后；
- 每个 Step 或 Turn 结束时；
- 用户审批、暂停和取消后；
- 上下文压缩或状态迁移后。

高风险事件需要更强的持久保证。纯 UI 流式片段可以采用较宽松的批量策略。

## 状态版本与并发控制

多个进程、标签页或子 Agent 可能同时更新同一任务。如果只做最后写入覆盖，后完成的旧操作会抹掉新状态。

可以使用 revision 做比较并设置：

```python
def update_task(task_id, expected_revision, change):
    task = database.lock(task_id)

    if task.revision != expected_revision:
        raise Conflict("STALE_REVISION")

    event = append_event(task_id, change)
    task.revision += 1
    return event, task.revision
```

调用方遇到陈旧 revision 时，应重新读取状态并决定是否仍然适用，而不是强行覆盖。

## 会话恢复不等于自动续跑

重新加载后可以恢复任务目标、历史和未完成状态，但不一定应该立刻继续执行。

例如用户昨天批准过一次生产部署，今天进程恢复时不能默认把这份临时许可继续使用。运行中的取消信号、一次性审批和临时凭据通常不应持久化为永久授权。

更安全的做法是：状态可以恢复，执行能力重新启用则由策略决定。对高风险任务，恢复后先展示当前状态并要求用户确认。

## 数据迁移与兼容性

Harness 升级后，事件结构和状态 schema 可能变化。持久化设计应从开始就考虑：

- 事件带版本号；
- 新代码能读取旧事件；
- 迁移过程可重复且有校验；
- 未知字段尽量保留；
- 无法安全迁移时明确阻止恢复；
- 迁移前保留备份和审计记录。

会话能保存几个月，就意味着状态格式是一项长期公共接口。

## 常见的失败方式

### 只保存聊天文本

工具调用、审批、预算和外部回执丢失，无法可靠恢复或审计。

### 失败后从头重跑

只读分析可能只是浪费成本，外部写入则可能重复产生副作用。

### 随机生成重试键

每次重试都换键，服务端无法识别为同一个逻辑操作。

### 把内存状态当作持久事实

进程内“工具已完成”尚未落盘时发生崩溃，恢复后就会出现认知分叉。

### 恢复后自动继承所有临时授权

暂停、重启和跨设备恢复可能跨越原审批语境。一次性授权应过期。

## 状态与持久化检查清单

- 是否保存任务、会话、执行、副作用和控制状态？
- 关键事实是否使用仅追加事件记录？
- 当前状态能否由日志确定性重建？
- 是否区分未开始、失败、成功和结果未知？
- 外部写操作是否支持稳定幂等键或状态查询？
- 检查点是否覆盖高风险副作用前后？
- 并发更新是否有 revision 或事务保护？
- 恢复时是否修复未闭合的调用和步骤？
- 临时审批与执行能力是否会在恢复后重新判断？
- 持久格式是否有版本和迁移策略？

## 最后理解状态与持久化

持久化的价值不是让聊天记录一直存在，而是让系统对已经发生的现实负责。

好的 Harness 能在任何时候回答：任务走到哪里，哪些动作已经发生，哪些结果可以确认，哪些仍然未知，以及怎样继续才不会重复伤害外部世界。

一句话总结：**可恢复不是重新执行，而是在保留事实的前提下安全地继续。**
