跳到正文
Elaine Blog
返回

一次任务怎样继续:会话历史、工作状态与 Checkpoint

更新于:
Context Engineering

用户第一轮说:“帮我看看 A1042 能不能退,先别提交。”第二轮说:“如果可以,明天下午取件。”第三轮又改成:“后天上午吧。”

把三句话存下来很容易。难的是下一次调用应该知道:当前订单仍是 A1042,取件意向已经改变,而且“先别提交”的约束仍然有效。要实现这种连续性,应用需要明确区分历史、当前状态和执行进度。

历史告诉你发生过什么,状态告诉你现在是什么

消息历史保留表达过程,包括用户原话、模型回复和工具交互。结构化任务状态提取当前需要使用的值,例如订单号、取件时间、核实状态和待办。

可以把第三轮之后的状态表达为:

{
  "task_id": "T9",
  "revision": 3,
  "order_id": "A1042",
  "pickup_preference": "2026-09-13T09:00:00+08:00/2026-09-13T12:00:00+08:00",
  "quality_verified": false,
  "submission_authorized": false,
  "stage": "consultation",
  "source_events": ["U17", "U18", "U19"]
}

例子的当前日期是九月十一日,因此“后天”被解释为九月十三日。时间解析必须保留时区与原始消息时间;隔一天重新读取“后天”,不能再次按新的日期计算。真实系统还需要确认取件时间段是否可用,这里只是用户意向。

submission_authorized 不应由语言模型看到“如果可以”就直接改成 true。需要由应用定义的确认流程、可信事件和对应任务参数共同决定。一个模糊布尔值在正式业务中往往还不够,需要关联被确认的订单、金额、操作和有效期。

状态转换为什么比重新总结全部历史更可靠

应用可以把用户表达转为候选事件,再由状态转换函数校验。例如 change_pickup 只修改取件意向,不改变质量核实结果,也不自动提交申请。

from dataclasses import dataclass, replace

@dataclass(frozen=True)
class TaskState:
    # revision 用于发现并发修改,不是业务订单的版本号。
    revision: int
    pickup: str | None
    authorized: bool


def change_pickup(state: TaskState, new_slot: str) -> TaskState:
    # 本例规定:修改操作参数后,之前针对旧参数的确认不能继续使用。
    # replace 产生新状态,保留旧对象,便于观察前后变化。
    return replace(state, revision=state.revision + 1,
                   pickup=new_slot, authorized=False)

如果每轮都让模型从全部聊天重新推断状态,它可能把历史计划当成当前计划,把“准备提交”当成“已提交”。结构化更新将这些容易混淆的变化变成明确规则。

模型仍可帮助理解自然语言,但它输出的是待校验事件。订单编号格式正确不代表用户拥有该订单,日期语法正确不代表该时段可用。语义提取和业务校验需要分别完成。

Checkpoint 保存的是可恢复的执行进度

检查点可以记录某个工作流步骤后的状态和继续执行所需的信息。消息历史只是其中一种数据。它可以帮助框架在下一次调用或中断恢复时找到之前的状态,而不必从空白重新开始。

LangGraph 的检查点按线程组织,线程内可以保留多个状态快照;具体字段与写入时机见官方 Persistence。本文的业务状态表是教学设计,不是框架自动提供的售后状态机。

下面这些标识解决不同问题:

标识用途不能替代什么
tenant_id / user_id确定访问主体与数据范围不能仅凭客户端字符串相信身份
thread_id标识一段连续会话或执行线程不能充当访问授权
task_id标识实际售后任务一个任务不一定只有一个会话
checkpoint_id定位某次执行快照不代表当前业务事实仍有效
request_id追踪一次请求不等于业务操作幂等键

同一用户可能有两个并行售后任务,同一任务也可能从网页切换到手机继续。若把 user_id 直接当唯一 thread_id,两个任务的历史和状态就容易混在一起。

内存保存为什么不能直接用于多实例服务

进程内字典和 InMemorySaver 适合演示状态变化。进程重启,内存状态消失;请求被负载均衡到另一实例,也未必能读到原状态。

持久化存储让多个实例访问共同记录,但“用了数据库”仍不等于解决并发。假设两个请求同时读取 revision 3:请求甲修改时间并写入 revision 4,请求乙基于旧值确认提交。若乙无条件覆盖,甲的修改可能丢失。

可以采用乐观并发控制:更新时附带 expected_revision=3,存储只允许当前仍为 3 时提交。若已经变成 4,就重新读取并决定是否可以重做这次转换。不能仅仅捕获冲突后机械重试“确认提交”,因为用户看到并确认的参数可能已经变了。

配套 state_transitions.py下载 使用字典模拟这个比较过程。正式数据库需要事务或条件更新;Python 的“先检查后赋值”不具备跨进程原子性。

恢复工作流不代表可以重复外部动作

设想退款接口已经处理成功,但应用在保存“退款成功”检查点前崩溃。恢复后只看到旧状态“待退款”,如果再次调用接口,可能重复操作。

这就是检查点与外部副作用之间的空隙。需要由业务服务支持与同一逻辑操作绑定的幂等键,保存操作意图与回执,必要时查询实际执行结果。恢复点不等于业务系统的事务边界。

另一种情况是检查点保存了昨晚的订单状态,今天订单已经关闭。恢复后要重新确认影响操作合法性的事实。历史快照适合解释“上次走到哪里”,实时业务来源决定“现在还可不可以继续”。

怎样决定下一轮真正发送什么

完整历史保存在外部,当前调用优先带上本轮问题、当前状态、仍有效的约束与最近交互。要解释用户为何改时间,再读取相关历史;要执行操作,则读取最新业务事实和相应授权记录。

状态中可以保存结构化值与来源事件 ID,而不是每次复制全部消息。这样历史可以长,模型上下文仍可以保持任务所需的大小。

现有 memory_graph.py下载 演示同一线程两次调用时历史累积,回复由本地函数生成,不包含真实模型推理。第十篇会进一步沿源码解释消息怎样合并;跨会话的用户偏好则属于下一篇的长期记忆。

继续阅读

上一篇: 需要时再加载:渐进式披露、工具发现与外部工作区

下一篇: 哪些信息值得长期记住:记忆的提取、读取、纠正与遗忘

完整文件、依赖与运行边界见配套指南下载


分享这篇文章:

上一篇
需要时再加载:渐进式披露、工具发现与外部工作区
下一篇
哪些信息值得长期记住:记忆的提取、读取、纠正与遗忘