Agent 状态持久化方案:Redis、数据库和文件存储的选型对比
·
Agent 状态持久化方案:Redis、数据库和文件存储的选型对比
一、为什么你的 Agent 会话总是"失忆"
Agent 执行多步任务时,最大的坑不是模型能力不足,而是状态丢失。
比如一个数据分析 Agent 需要先查库、再做聚合、最后生成报告。如果每一步都要重新推理上下文,响应延迟会从 3 秒飙到 15 秒,Token 消耗直接翻倍。
更糟的是,用户中途关掉页面再打开,Agent 可能完全不记得之前聊了什么。
这些问题都指向同一个根因:缺乏可靠的状态持久化机制。
Agent 的状态通常包含:对话历史、任务进度、中间推理结果、工具调用栈。这些数据的读写模式各不相同,所以不存在"一把梭"的存储方案。
二、三种方案的底层原理与数据流
先看一张典型的多步 Agent 执行流程中,状态读写发生的时机:
三种存储方案在读写模式上的核心差异:
| 特性 | Redis | PostgreSQL | 文件存储 |
|---|---|---|---|
| 读延迟 | <1ms | 1-5ms | 5-50ms |
| 写延迟 | <1ms | 1-10ms | 10-100ms |
| 数据结构 | KV/Hash/List | 关系表 | 文件/对象 |
| 持久化 | 可配置 | 强持久化 | 强持久化 |
| 查询能力 | Key 精确匹配 | SQL 查询 | 路径匹配 |
三、生产级分层存储实现
以下是 Go 语言实现的分层状态管理器:
package agent
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
// AgentState Agent 执行状态
type AgentState struct {
SessionID string `json:"session_id" gorm:"primaryKey"`
TaskID string `json:"task_id" gorm:"index"`
StepIndex int `json:"step_index"`
Context json.RawMessage `json:"context" gorm:"type:jsonb"` // 当前推理上下文
ToolStack []ToolCallRecord `json:"tool_stack" gorm:"type:jsonb"` // 工具调用栈
Status string `json:"status"` // running/completed/failed
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ToolCallRecord 工具调用记录
type ToolCallRecord struct {
ToolName string `json:"tool_name"`
Arguments json.RawMessage `json:"arguments"`
Result json.RawMessage `json:"result,omitempty"`
CalledAt time.Time `json:"called_at"`
}
// StateManager 分层状态管理器
type StateManager struct {
redis *redis.Client // 热状态缓存层
db *gorm.DB // 持久化层
}
// GetState 先查 Redis 缓存,miss 则回源数据库
func (sm *StateManager) GetState(ctx context.Context, sessionID string) (*AgentState, error) {
// 第一层:尝试从 Redis 读取热状态
key := fmt.Sprintf("agent:state:%s", sessionID)
data, err := sm.redis.Get(ctx, key).Bytes()
if err == nil {
var state AgentState
if err := json.Unmarshal(data, &state); err != nil {
return nil, fmt.Errorf("反序列化 Redis 缓存失败: %w", err)
}
return &state, nil
}
// 第二层:Redis miss,回源数据库
var state AgentState
if err := sm.db.WithContext(ctx).Where("session_id = ?", sessionID).
First(&state).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("会话 %s 不存在", sessionID)
}
return nil, fmt.Errorf("查询数据库失败: %w", err)
}
// 回填 Redis 缓存,过期时间根据活跃度设定
cached, _ := json.Marshal(state)
sm.redis.Set(ctx, key, cached, 30*time.Minute)
return &state, nil
}
// SaveState 写入数据库并更新 Redis 缓存
func (sm *StateManager) SaveState(ctx context.Context, state *AgentState) error {
state.UpdatedAt = time.Now()
// 先写数据库(强持久化)
if err := sm.db.WithContext(ctx).Save(state).Error; err != nil {
return fmt.Errorf("保存状态到数据库失败: %w", err)
}
// 再更新 Redis 缓存(允许失败,缓存是优化而非必需)
key := fmt.Sprintf("agent:state:%s", state.SessionID)
cached, _ := json.Marshal(state)
sm.redis.Set(ctx, key, cached, 30*time.Minute)
return nil
}
// ArchiveState 归档已完成会话——迁移到冷存储
func (sm *StateManager) ArchiveState(ctx context.Context, sessionID string) error {
// 归档时删除 Redis 缓存,释放内存
key := fmt.Sprintf("agent:state:%s", sessionID)
sm.redis.Del(ctx, key)
// 更新数据库状态标记为已归档
return sm.db.WithContext(ctx).
Model(&AgentState{}).
Where("session_id = ?", sessionID).
Update("status", "archived").Error
}
使用时只需组合这三种存储能力:
- Redis 负责正在执行中的热状态(30 分钟 TTL)
- PostgreSQL 负责全量状态记录和查询
- 文件存储(S3/MinIO) 负责工具调用产出的中间文件
四、边界分析与 Trade-offs
一致性边界:上述代码先写数据库再写 Redis。如果 Redis 写入失败,读请求会回源数据库——不会丢数据,但会多一次 DB 查询。这是一种最终一致性设计,而非强一致。
状态膨胀:Agent 的多轮对话会产生大量 JSONB 数据。尤其是 ToolStack 字段,每调用一次工具就追加一条。生产建议在工具调用超过 50 次时触发摘要压缩。
并发写入:同一个 Session 可能被多个 goroutine 同时更新(如并行工具调用)。需要加分布式锁,后续文章会单独展开。
归档策略:不要无限保留所有会话的活跃状态。设置归档阈值:
- 会话完成后 1 小时归档(移出 Redis)
- 30 天后冷存储到 S3
- 90 天后按 GDPR 要求删除
五、总结
Agent 状态持久化不存在银弹方案,核心策略是分层存储:
- Redis 做热缓存,保证低延迟读写
- 关系数据库 做全量记录,支持查询和管理
- 对象存储 放大文件和归档数据
选型时从三个维度评估:读写频率、查询复杂度、保留周期。不要一开始就上全家桶——先跑一个 Redis+PG 的组合,等状态数据超过 10GB 再引入冷存储。
更多推荐



所有评论(0)