Skill 执行完毕后,它的状态和中间结果如何持久化?什么情况下需要持久化,什么情况下不需要?
面试官:“Skill 执行完之后,它的状态和中间结果你们是怎么持久化的?什么情况下需要持久化,什么情况下不需要?”
候选人:
“持久化的本质是为系统的韧性买单。不是所有东西都值得落盘,我有一条核心决策原则:如果这个状态或结果的丢失会导致用户可感知的错误、业务损失或无法复现问题,就必须持久化;如果它只是无状态的瞬间快照,丢了也没人知道,那就没必要。
我分四个层面来展开:持久化的内容结构、存储选择、判断是否持久化的决策树,以及实际项目中的权衡。
一、持久化的内容结构
当决定持久化时,我会把 Skill 相关的信息分三类存入不同的结构:
-
任务级上下文:整个任务的全局状态,包括用户原始意图、任务ID、当前执行到的 DAG 节点、已完成 Skill 列表、全局变量(如总费用、用户偏好)。这部分存在一个
TaskContext的序列化快照里,通常放入 Redis(带过期)或数据库中。 -
Skill 执行快照:单个 Skill 的执行状态,包括 Skill 名称、版本、输入参数、输出结果、执行耗时、状态(成功/失败/等待中)。这相当于每个 Skill 的“执行回执”,通常以追加日志的形式记录,或作为任务上下文的一部分。
-
业务关键数据:比如生成的订单 ID、发送的邮件 ID、支付的交易流水。这些数据具有外部性,即使 Skill 执行完毕,后续也需要审计和补偿。它们会单独存入业务数据库,而不是仅放在上下文中。
二、存储选型与生命周期
| 存储类型 | 适用数据 | 生命周期 |
|---|---|---|
| Redis(带 TTL) | 任务上下文、等待用户确认的暂停状态 | 任务完成或超时后自动清理(如 24 小时) |
| 关系数据库(MySQL/PostgreSQL) | 业务关键数据、审计日志、补偿所需的事务记录 | 长期保留,按合规要求归档 |
| Elasticsearch / 日志系统 | Skill 执行摘要、耗时、异常堆栈 | 按日志保留策略(如 30 天) |
| 不持久化(内存) | 纯查询 Skill 的瞬时结果、中间缓存 | 进程存活期间 |
三、是否需要持久化的决策树
我把判断逻辑画成一个简单的决策路径:
Skill 执行完毕
│
├── 是否涉及外部副作用?
│ ├── 是(扣款、发邮件、创建订单) → ✅ 必须持久化
│ └── 否 → 继续
│
├── 是否需要断点续执行?
│ ├── 是(任务可能暂停等用户输入) → ✅ 持久化上下文和状态
│ └── 否 → 继续
│
├── 是否需要事后审计或排错?
│ ├── 是(金融、医疗、关键业务) → ✅ 持久化执行日志
│ └── 否 → 继续
│
└── 是否可能被其他 Skill 或后续请求复用?
├── 是(如用户画像分析结果) → ✅ 持久化到缓存/数据库
└── 否 → ❌ 不持久化,仅存活在内存
四、典型场景举例
-
需要持久化:用户预订机票的 Skill。产生了订单号、支付流水,且任务可能在“等待支付确认”阶段暂停数小时。此时必须把上下文和订单信息持久化到数据库,以便用户关闭 App 后重新打开仍能继续支付。
-
不需要持久化:用户查询天气的 Skill。输入是城市名,输出是当前温度,无副作用,不依赖外部状态。即使进程崩溃,用户重问一次即可,没有持久化必要。
-
部分持久化:一个“生成周报” Skill,它内部调用了三个子 Skill 分别统计销售、流量和工单数据。每个子 Skill 的结果作为中间数据缓存在 Redis 中 10 分钟,防止重复计算;但最终生成的周报文件写入对象存储后,中间缓存立即失效。
五、持久化带来的代价与优化
持久化不是免费的,它会增加延迟和存储成本。我的优化策略是:
-
异步写入:Skill 执行结果的业务数据在主流程中同步写入,执行日志和监控数据通过异步队列批量刷盘。
-
压缩与裁剪:上下文快照使用 JSON 压缩,并只保留最近几次状态变更,老版本主动清理。
-
分层 TTL:不同重要程度的数据设置不同的过期时间,避免存储膨胀。
一个线上教训:之前有个“提交审批” Skill,我们只把审批结果发给了消息队列,没有持久化到数据库。结果审批服务宕机重启后,队列中的消息丢失,用户提交的审批单全部“消失”,客服电话被打爆。后来我们强制要求:任何涉及用户可感知状态变更的操作,必须先把结果落库,再发消息。 这才彻底解决了丢失问题。
持久化的决策,本质上是在权衡“记忆”与“遗忘”的成本。记住所有事情,大脑会不堪重负;忘记重要的事,又会带来灾难。好的系统就像一个阅历丰富的老者,懂得把重要的经历刻在石头上(数据库),把临时的想法记在沙子上(缓存),而把那些无关紧要的琐事,放心地交给遗忘。