跳转到内容

Agent 内存与状态持久化

Agent 内存与状态持久化

学习目标

读完本章后,你将能够:

  • 理解 Agent 内置内存字段的设计动机
  • 设计跨 Agent 的键值共享存储系统
  • 实现内存淘汰策略防止存储膨胀
  • 处理 JSON 序列化与原生 JSON 列的兼容迁移

核心概念

为什么 Agent 需要内存

Agent 的核心循环(check/receive)是无状态的函数调用——每次调用都是独立的。但很多场景需要跨次调用保留状态:

场景需要记忆的内容
网站变更检测上一次抓取的内容,用于对比
峰值检测历史时间序列数据,用于统计分析
去重过滤已处理过的事件指纹
累计汇总增量计数器或累加器
条件编排动态配置的目标 Agent 状态

内存层次结构


本地内存设计

JSON 序列化字段

Agent 的 memory 字段是一个 JSON 序列化列,存储任意键值对:

# Agent 基类中声明
json_serialize :options, :memory
# 子类 Agent 中使用
def receive(events)
memory['data'] ||= {}
memory['data']['some_key'] = value
# memory 自动序列化并保存到数据库
end

关键设计

  1. 惰性初始化memory['data'] ||= {} 在首次访问时自动创建
  2. 原子保存memory 的修改在 Agent save 时自动序列化
  3. 类型安全:反序列化后返回 HashWithIndifferentAccess,支持字符串和符号键

双模式兼容

迁移策略

  • 初始版本使用 TEXT 列存储 JSON 字符串
  • 可选迁移到原生 JSON/JSONB 列(通过 NATIVE_JSON_COLUMNS=true 环境变量)
  • JsonSerializedField 模块自动检测列类型并选择合适的序列化器
  • 迁移期间兼容两种格式

共享内存设计

KeyValueStoreAgent

当多个 Agent 需要共享状态时,使用专门的键值存储 Agent:

工作流程

  1. 写入:Source Agent 将 KeyValueStoreAgent 设为 Controller target
  2. 存储:KeyValueStoreAgent 的 memory 作为键值存储
  3. 读取:Consumer Agent 在 Liquid 模板中通过变量名访问存储数据

内存管理策略

配置项默认值作用
max_keys100最大保留键数
淘汰策略FIFO超出 max_keys 时删除最旧的键值对
值序列化字符串化 + as_object 过滤器支持存储非字符串值
空值处理值为空时删除键自动清理无效数据

内存使用模式

模式一:时间序列存储

# PeakDetectorAgent 的模式
memory['data'] ||= {}
memory['data'][group] ||= []
memory['data'][group] << [value, timestamp]
# 滑动窗口清理
def cleanup(group)
newest_time = memory['data'][group].last.last
memory['data'][group].reject! { |_value, time|
time <= newest_time - window_duration
}
end

模式二:去重缓存

# DeDuplicationAgent 的模式
memory['properties'] ||= []
def is_unique?(property)
!memory['properties'].include?(property)
end
def update_memory(property, lookback)
# 超出容量时移除最旧项
if lookback != 0 && memory['properties'].length == lookback
memory['properties'].shift
end
memory['properties'].push(property)
end

模式三:累计状态

# 计数器模式
memory['count'] ||= 0
memory['count'] += 1
# 最近状态模式
memory['last_value'] = current_value
memory['last_updated_at'] = Time.now.to_i

陷阱与对策

陷阱一:内存膨胀

Agent 不断向 memory 写入数据但不清理,最终导致数据库行过大。

对策

  • 设置明确的上限(max_keyslookback
  • 定期清理过期数据(滑动窗口淘汰)
  • 使用 CRC32 哈希缩短长键(Zlib.crc32(property).to_s

陷阱二:并发写入

多个 Worker 同时修改同一 Agent 的 memory 可能导致数据竞争。

对策

  • 每个 Agent 由单一 Worker 处理(通过 agent_id 路由)
  • 数据库层面的行级锁(ActiveRecord save 自动加锁)

陷阱三:序列化开销

大型 memory 字段在每次 save 时都完整序列化,影响性能。

对策

  • 只保留必要数据,定期清理
  • 迁移到原生 JSON 列减少序列化开销
  • 使用 memory 字段而非 events 存储聚合结果(减少行数)

设计取舍

Agent 内存 vs 独立缓存服务

维度Agent 内置内存独立缓存(Redis)
部署复杂度零(复用数据库)需额外服务
一致性与 Agent 状态同事务需处理一致性问题
性能中等(DB I/O)高(内存访问)
查询能力有限(JSON 字段查询)丰富(数据结构操作)
适用规模中小型(数千 Agent)大型(数万+ Agent)

选择内置内存的原因:零额外依赖,中小规模完全够用。当 Agent 数量达到万级别时,才需要考虑引入独立缓存。

FIFO vs LRU 淘汰

维度FIFO(先进先出)LRU(最近最少使用)
实现复杂度极低(数组 shift/push)中等(需要访问追踪)
命中率
内存开销无额外开销需要访问时间戳

选择 FIFO 的原因:实现简单且对于大多数 Agent 场景足够好——最新的数据最有价值,旧数据自然被淘汰。


参考来源