跳到主要内容

任务快照持久化 ​

概述 ​

AgentRunner 在执行任务时,会持续保存任务的最新进度。默认情况下,这些进度保存在当前应用的内存中, 适合本地开发和简单测试。

但在正式应用中,任务可能需要等待几分钟甚至几天。例如:

  • 退款申请正在等待主管审批;
  • 用户还没有填写补充表单;
  • 外部服务暂时不可用,任务等待下次重试;
  • 业务调度器稍后显式推进任务;
  • 应用正在发布新版本,需要重启或切换到其他实例。

如果任务进度只在内存中,应用一旦重启,这些等待中的任务就无法找回。多个服务实例也无法看到彼此保存的 状态。

AgentTurnStore 用来解决这个问题。它是 Agent 任务的存储接口,负责按 turnId 保存和读取任务进度。 Agents-Flex 提供内存、JDBC 和 Redis 三种选择:

text
AgentRunner
    ↓ 自动保存和读取
AgentTurnStore
    ├─ 内存:本地开发和测试
    ├─ JDBC:保存到业务数据库
    └─ Redis:保存到共享 Redis

业务代码通常不需要直接操作 Store。将 Store 配置到 Runner 后,任务创建、等待、恢复、重试和结束时的 保存操作都由 Runner 完成。

如何选择 ​

Store适用场景需要注意
InMemoryAgentTurnStore本地开发、单元测试、可以接受重启后丢失的演示数据只存在当前进程中
JDBC Store已有 MySQL、PostgreSQL 等数据库,希望统一备份和管理需要创建表并配置数据库连接池
Redis Store已有共享 Redis,任务读取和调度频繁需要配置 Redis 持久化、容量和高可用
自定义 Store必须接入公司内部存储平台需要完整实现并发保护和后台领取能力

大多数业务不需要自己实现 Store。已经有稳定业务数据库时,可以优先使用 JDBC;基础设施主要依赖 Redis 时,可以使用 Redis。生产环境应根据实际负载独立完成压测。

快速开始 ​

JDBC 和 Redis 选择一种即可,不需要同时配置。

1. 添加依赖 ​

使用 JDBC:

xml
<dependency>
    <groupId>com.agentsflex</groupId>
    <artifactId>agents-flex-agent-store-jdbc</artifactId>
    <version>${agents-flex.version}</version>
</dependency>

使用 Redis:

xml
<dependency>
    <groupId>com.agentsflex</groupId>
    <artifactId>agents-flex-agent-store-redis</artifactId>
    <version>${agents-flex.version}</version>
</dependency>

Store 模块的版本应与 agents-flex-agent 保持一致。JDBC 方式还需要应用自己提供数据库驱动和 DataSource;Redis 模块使用 Jedis 连接 Redis。

2. 创建 Store ​

根据实际存储选择下面一种配置。

使用 JDBC ​

下面的 dataSource 表示应用已经配置好的数据库连接池:

java
JdbcAgentStoreConfig storeConfig =
    JdbcAgentStoreConfig.builder(dataSource)
        .tablePrefix("af_agent_")
        .binaryColumnType("BLOB")
        .build();

storeConfig.schema().initialize();

AgentTurnStore turnStore =
    storeConfig.turnStore();
配置作用
builder(dataSource)使用应用已有的数据库连接池
tablePrefix(...)设置表名前缀,默认值为 af_agent_
binaryColumnType(...)设置当前数据库保存二进制内容使用的列类型
schema().initialize()创建 Store 所需的表和索引
turnStore()获得可以配置到 Runner 的 AgentTurnStore

BLOB 适用于 MySQL 等数据库;PostgreSQL 通常使用 BYTEA。应根据实际数据库选择列类型。

schema().initialize() 可以重复调用,适合本地开发和测试。生产环境通常应将相同的建表语句纳入 Flyway、 Liquibase 等数据库迁移流程,避免多个应用实例在启动时同时修改表结构。

DataSource 由业务应用创建和管理,关闭应用时仍由业务应用负责关闭。

使用 Redis ​

可以直接使用 Redis URI:

java
RedisAgentStoreConfig storeConfig =
    RedisAgentStoreConfig
        .builder("redis://127.0.0.1:6379")
        .keyPrefix("agents-flex:agent:")
        .build();

AgentTurnStore turnStore =
    storeConfig.turnStore();
配置作用
builder(redisUri)根据 URI 创建 Redis 客户端
keyPrefix(...)设置所有 Agent Store 数据的键前缀
turnStore()获得可以配置到 Runner 的 AgentTurnStore

使用 URI 创建客户端时,应用关闭阶段需要关闭 storeConfig:

java
storeConfig.close();

如果应用已经统一创建了 JedisPooled,也可以直接传入:

java
RedisAgentStoreConfig storeConfig =
    RedisAgentStoreConfig.builder(jedisPooled)
        .keyPrefix("production:agents-flex:")
        .build();

这种方式下,Redis 客户端由业务应用管理,关闭 storeConfig 不会关闭传入的 JedisPooled。

不同环境和不同业务应使用不同的 keyPrefix,避免测试、预发布和生产数据写入同一组 Redis Key。

3. 配置 AgentRunner ​

无论选择 JDBC 还是 Redis,最终都需要将得到的 turnStore 配置到 Runner:

java
AgentRunner runner = AgentRunner.builder()
    .turnStore(turnStore)
    .agentLoader(agentLoader)
    .build();
配置作用
turnStore(...)保存和读取任务进度
agentLoader(...)恢复任务时加载原来的 Agent 版本

agentLoader 表示已经创建好的 AgentLoader。Store 只保存任务数据,不保存模型客户端和 Java 工具函数, 因此需要 Loader 重新获得完整 Agent。配置方式见 Agent 加载与版本。

配置完成后,仍然按照正常方式执行和恢复任务:

java
AgentTurn turn = runner.run(agent, "生成月度报告");
String turnId = turn.getId();

AgentTurn restored = runner.restore(turnId);

Runner 会自动使用当前 Store,不需要业务代码手动调用 turnStore.save(...)。

多实例部署 ​

当多个应用实例共同处理任务时,它们必须连接同一个 JDBC 数据库或 Redis 集群:

text
应用实例 A ─┐
应用实例 B ─┼─→ 同一个 AgentTurnStore
业务调度器 ─┘

同时还应保证:

  • 所有实例能通过 AgentLoader 加载相同的 Agent ID 和历史版本;
  • 每个实例都具备对应版本需要的模型、工具和 Middleware;
  • 数据库连接池或 Redis 客户端可以承受所有实例的总请求量;
  • 写入类工具使用幂等键,避免故障接管时重复产生业务影响。

Store 可以避免多个执行线程静默覆盖同一份任务进度,但不能替代退款、发货、发送邮件等业务操作自身的 幂等设计。具体做法见工具运行上下文。

Store 保存哪些数据 ​

AgentTurnStore 主要保存任务 Snapshot,包括任务状态、消息、等待信息、重试时间、统计数据和业务 metadata。完整说明见任务快照。

以下内容不由 AgentTurnStore 保存:

内容应由谁负责
Agent 配置、模型和工具AgentLoader 与应用配置系统
多轮聊天记录ChatMemory
实时进度和执行事件AgentEventListener 或业务消息系统
订单、工单等业务数据业务数据库
合规审计记录业务审计库或可靠事件系统

Store 负责的是“这项 Agent 任务执行到哪里”,不是应用所有数据的统一存储。

与 ChatMemory 的区别 ​

AgentTurnStore 和 ChatMemory 都可能保存消息,但用途不同:

组件主要用途
AgentTurnStore保存一项任务的执行进度,使它可以恢复
ChatMemory保存多轮对话,使下一项任务理解之前聊过什么

例如,退款任务正在等待审批时,Store 保存待审批工具和当前任务状态;用户之后说“继续刚才的退款”时, ChatMemory 帮助模型理解前面的对话。

需要连续对话时,应单独配置共享的 ChatMemory。它不能替代 Store,Store 也不应被当作聊天记录查询接口。 会话配置见上下文管理。

数据安全与运维 ​

任务 Snapshot 可能包含用户消息、工具参数、表单数据、错误摘要和业务 metadata。生产环境应至少考虑:

  1. 数据库或 Redis 的访问权限与网络隔离;
  2. 传输加密、存储加密和备份保护;
  3. 密码、API Key 等敏感信息不得写入 metadata;
  4. JDBC 连接超时、Redis 超时和连接池容量;
  5. Store 保存失败、版本冲突和可执行任务积压的监控;
  6. 已完成、失败和取消任务的保留期限与清理策略。

Redis Store 是否能在服务重启后保留数据,还取决于 Redis 自身的持久化、高可用和备份配置。不要仅因为 使用 Redis Store 就默认数据一定不会丢失。

清理历史任务时,应先确认任务已经结束,并使用明确的时间范围和小批次删除,避免长事务、全表锁或 Redis 阻塞。业务审计和聊天记录可能有不同的保留期限,应分别制定策略。

进阶:自定义 AgentTurnStore ​

只有框架内置的 JDBC 和 Redis 实现无法满足要求时,才建议实现自己的 AgentTurnStore。

接口中的主要方法如下:

方法需要实现的行为
load(turnId)读取指定任务的最新 Snapshot
save(snapshot, expectedVersion)只有版本符合预期时才保存新进度
findActiveTurn(conversationId)查找某段会话中尚未结束的任务
currentTimeMillis()提供任务调度使用的统一当前时间
requestCancellation(turnId)记录取消请求,并且不能被旧状态清除

防止旧进度覆盖新进度 ​

save(snapshot, expectedVersion) 中的 expectedVersion 表示调用方读取任务时看到的版本。只有 Store 中的 当前版本仍然相同,才能保存;否则说明其他执行者已经先更新任务,应报告版本冲突。

JDBC 实现通常可以使用带版本条件的更新,并检查实际影响行数:

sql
UPDATE af_agent_turns
SET version = ?, payload = ?
WHERE turn_id = ? AND version = ?

这段 SQL 只用于展示版本条件。完整实现还需要原子更新任务状态、下次执行时间和取消标记 等字段,并在更新行数为 0 时报告版本冲突。

不能采用“先查询版本,再无条件更新”的方式,因为查询和更新之间可能已经有其他实例写入。

防止并发覆盖 ​

Store 通过版本号 CAS 防止旧 Snapshot 覆盖新进度。业务层需要确保同一 Turn 不被多个执行线程同时推进; 发生版本冲突时重新加载最新 Snapshot,再决定重试、放弃或修复。 这只是任务状态的并发保护,外部业务操作仍需幂等。

多实例部署时,currentTimeMillis() 应使用数据库或 Redis 服务端时间,避免不同应用服务器的时钟误差导致 任务被提前接管或延迟执行。

序列化 ​

JDBC 和 Redis Store 默认使用 FastjsonAgentStoreSerializer 将 Snapshot 编码为二进制数据。自定义 Serializer 时需要保证:

  • 应用重启后可以读取之前保存的数据;
  • 新版本可以读取需要保留的旧数据;
  • 只允许反序列化明确可信的业务类型;
  • 无法序列化的对象不会被写入 metadata。

如果 metadata 只使用字符串、数字、布尔值、列表和 Map,通常不需要修改默认 Serializer。

测试要求 ​

自定义 Store 至少应测试以下情况:

  • 两个执行者同时保存同一版本时,只有一个成功;
  • 取消请求不会被旧 Snapshot 覆盖;
  • 重试时间和取消标记在 CAS 保存后保持一致;

真实 MySQL 与 Redis 验证 ​

JDBC 模块的普通合约测试默认使用 H2;真实 MySQL 测试单独使用 MysqlAgentStoresIntegrationTest。Redis 合约测试连接真实 Redis,但默认在 Redis 不可用时跳过。 在 CI 或发布前,应打开 required 开关,让连接失败直接使构建失败:

bash
export MYSQL_TEST_PASSWORD='your-password'

mvn -pl agents-flex-agent-store/agents-flex-agent-store-jdbc \
  -Dmysql.test.url='jdbc:mysql://127.0.0.1:3306/agents_flex_jdbc_it?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC' \
  -Dmysql.test.user=root \
  -Dmysql.test.required=true \
  -Dtest=MysqlAgentStoresIntegrationTest test

mvn -pl agents-flex-agent-store/agents-flex-agent-store-redis \
  -Dredis.test.uri='redis://127.0.0.1:6379' \
  -Dredis.test.required=true \
  -Dtest=RedisAgentStoresIntegrationTest test

MySQL 数据库需要预先创建,测试只会创建带随机前缀的表并在结束时删除;Redis 测试使用随机 key 前缀, 不会清理测试实例中的其他 key。不要在生产数据库或生产 Redis 上运行这些测试。

  • 应用重启后,Snapshot 仍能完整读取。

事务边界 ​

Store 只能保证 Agent 任务进度自身的原子更新,不能自动与订单数据库、审批系统或消息队列组成同一个事务。

例如,审批服务已经记录“同意”,但调用恢复接口前发生故障,Agent 任务仍然保持等待状态。需要可靠衔接 外部回调时,业务系统应保存回调事件,使用唯一事件 ID 防止重复处理,并在失败后重试或扫描补偿。

同样地,工具已经完成退款,但 Snapshot 还没来得及保存时,也可能再次执行。写入类工具仍应使用业务幂等 键,不能依赖 Store 事务解决所有外部一致性问题。

版本升级 ​

旧版 JDBC 表可能还保留 lease_owner、lease_id 和 lease_until 列。新版 Agent Store 不再读取或写入这些 列;初始化 Schema 时会尝试把遗留的 lease_until 改为可空,以兼容旧表。新建表不会再创建这些列。升级后 应确认应用账号拥有修改表结构的权限,并在正式环境提前执行一次迁移验证。

升级 Agents-Flex 前,应在测试环境验证现有任务数据可以被新版本读取。不要在没有确认兼容性的情况下, 让不同框架版本的 Runner 同时读写同一批 Store 数据。

从早期 AgentRunStore 升级到当前 AgentTurnStore 时,框架不会自动迁移旧数据。已有系统应先完成旧任务, 或者在停机窗口迁移仍需保留的数据。Schema 初始化只会创建当前表,不会自动删除旧表或旧 Redis Key。

使用建议 ​

  1. 本地开发使用内存 Store,生产环境使用所有实例可访问的持久化 Store。
  2. JDBC 与 Redis 选择一种即可,并按现有基础设施进行备份、高可用和容量规划。
  3. Runner、业务调度器和回调服务必须连接同一套 Store。
  4. 配置能够加载历史 Agent 版本的 AgentLoader。
  5. 写入类工具同时实现幂等,不把 Store 当作业务事务。
  6. 为敏感数据、历史任务和版本升级制定明确策略。

相关文档 ​