线上事故复盘:Agent 跑了一半被 kill,重启后用户直接破防 😱

记一次线上事故:Agent 执行到一半被中断,恢复后人傻了 😱

事故现场

周二的下午,线上突然告警:某用户的报表查询请求超时了。

运维的兄弟二话不说,先 kill -9 掉了卡住的进程,然后重启服务。

结果用户炸了------

"我的查询进度怎么全丢了?!我跑了 2 小时的数据分析,重启之后全没了,从头开始?!"

我赶紧去看日志,发现问题比想象中严重:

ini 复制代码
[14:32:01] User request: 查询 2024Q1 报表
[14:32:02] Node: fetch_data -> checkpoint saved
[14:32:03] Node: process_data -> checkpoint saved
[14:35:47] ⚠️ Process killed (OOM)
[14:35:52] Process restarted
[14:35:53] User request: resume from last checkpoint
  → ❌ No checkpoint found for thread_id=xxx
[14:35:53] Starting from scratch...

用户以为他在等一个查询,实际上他在等 两个小时的计算。而运维的"快速止血"直接把这些计算全清了。

小李看到这一幕,脸都绿了:

"哥,LangGraph 不是有 checkpoint 吗?为什么没恢复?"

这个问题好,我也想知道答案。


一、什么是 Checkpoint?为什么需要它?

短期记忆 vs 长期记忆

在 LangGraph 里,Agent 的记忆分两层:

类型 存储 用途 生命周期
Checkpoint(短期记忆) Redis / Postgres 保存 Node 执行过程中的中间状态 一个 Thread 的执行期间
Store(长期记忆) Redis / Postgres / S3 跨 session 的持久化知识 永久,直到手动删除

Checkpoint 是执行过程中的快照。类比人的话,就是工作记忆------当前正在做的事的各种中间状态。

为什么需要 Checkpoint?

三个核心场景:

1. 中断恢复

css 复制代码
Agent 执行中...
  → Node A done,状态写入 Checkpoint
  → Node B done,状态写入 Checkpoint
  → Node C 执行到一半,进程崩了
  → 重启后,从 Node C 继续(不用从头跑 A、B)

2. 并发控制

ini 复制代码
User A: thread_id=user_a,checkpoint=001
User B: thread_id=user_a,checkpoint=002  ← 乐观锁,版本冲突则重试

3. 多轮对话

sql 复制代码
Round 1: User 说"帮我查Q1报表" → checkpoint001
Round 2: User 说"再加个Q2" → 从 checkpoint001 恢复,继续执行 → checkpoint002
Round 3: User 说"导出Excel" → 从 checkpoint002 恢复 → 完成

二、deepagents 的 Checkpoint 体系

create_deep_agent 的 checkpointer 参数

deepagents/graph.py 里的 create_deep_agent 签名(line 231):

python 复制代码
def create_deep_agent(
    model: str | BaseChatModel | None = None,
    tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
    *,
    # ... 其他参数 ...
    checkpointer: Checkpointer | None = None,  # ← 关键参数
    store: BaseStore | None = None,
    # ...
) -> CompiledStateGraph:

这个 checkpointer 最终透传给 create_agent(line 772):

python 复制代码
return create_agent(
    model,
    system_prompt=final_system_prompt,
    tools=_tools,
    middleware=deepagent_middleware,
    # ...
    checkpointer=checkpointer,  # ← 透传到 LangChain Agent
    store=store,
    # ...
)

三种 Checkpointer

LangGraph 支持多种存储后端:

python 复制代码
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.checkpoint.redis import RedisSaver
from langgraph.checkpoint.memory import MemorySaver  # 仅用于测试

# 生产环境
checkpointer = PostgresSaver.from_conn_string("postgresql://...")
# 或者 Redis(性能更高)
checkpointer = RedisSaver.from_url("redis://...")
Checkpointer 适用场景 性能 持久性
MemorySaver 测试 最快 进程重启丢失 ❌
RedisSaver 生产(单机) Redis 持久化 ✅
PostgresSaver 生产(分布式) Postgres 持久化 ✅

⚠️ 生产环境绝对不能用 MemorySaver。进程重启后数据全丢。


三、deepagents 怎么做到中断继续?

核心链路

arduino 复制代码
graph.invoke(input, config={"configurable": {"thread_id": "xxx"}})
  ↓
deepagents 内部调用 LangChain create_agent
  ↓
LangChain Agent 编译成 Pregel 图
  ↓
Pregel.run() 执行循环,每个 Node 完成后写 checkpoint
  ↓
如果配置了 interrupt_on,触发 HumanInTheLoopMiddleware

HumanInTheLoopMiddleware:中断机制的核心

langchain/agents/middleware/human_in_the_loop.py 实现了 deepagents 的中断功能:

python 复制代码
class HumanInTheLoopMiddleware(AgentMiddleware):
    def __init__(
        self,
        interrupt_on: dict[str, bool | InterruptOnConfig],
        *,
        description_prefix: str = "Tool execution requires approval",
    ) -> None:
        # interrupt_on 格式:
        # {"edit_file": True, "delete_file": {"allowed_decisions": ["approve", "reject"]}}
        self.interrupt_on = resolved_configs

    def after_model(self, state, runtime) -> dict[str, Any] | None:
        # 在 LLM 返回 tool_calls 后,检查哪些需要人工审批
        last_ai_msg = messages[-1]  # 拿到 LLM 的响应
        if not last_ai_msg.tool_calls:
            return None

        for tool_call in last_ai_msg.tool_calls:
            if tool_call["name"] in self.interrupt_on:
                # 调用 langgraph.types.interrupt 暂停执行
                decisions = interrupt(hitl_request)["decisions"]

interrupt() 函数:暂停执行的机制

langgraph/types.py line 801:

python 复制代码
def interrupt(value: Any) -> Any:
    """Interrupt the graph with a resumable exception from within a node.

    The first invocation of this function raises a `GraphInterrupt` exception,
    halting execution. The provided `value` is included with the exception
    and sent to the client.

    A client resuming the graph must use `Command` primitive to specify a
    value for the interrupt and continue execution.
    """

关键 :当 interrupt() 被调用时:

scss 复制代码
interrupt(value)
  → raise GraphInterrupt(value)
  → 执行暂停,状态保存到 Checkpointer
  → 客户端收到中断请求,审查 tool_calls
  → 客户端调用 graph.invoke(Command(resume=value))
  → 从断点恢复,继续执行

中断时的状态保存

python 复制代码
# 伪代码,实际在 Pregel.run() 里
for node in graph.nodes:
    result = node.execute(state)
    # 每个 Node 执行完毕后,写入 checkpoint
    checkpointer.put(
        thread_id=thread_id,
        checkpoint_id=new_checkpoint_id,
        state=current_state  # 包含所有节点的输出
    )
    if should_interrupt(node):
        # 保存中断点
        save_interrupt_checkpoint(thread_id, node.name, result)
        raise GraphInterrupt(resume_data)

四、中断恢复的完整流程

创建 Agent 时配置 interrupt_on

python 复制代码
from deepagents.graph import create_deep_agent
from langgraph.checkpoint.redis import RedisSaver

checkpointer = RedisSaver.from_url("redis://localhost:6379")

agent = create_deep_agent(
    model=model,
    tools=ALL_TOOLS,
    checkpointer=checkpointer,
    interrupt_on={
        "edit_file": True,           # 所有 edit_file 调用都中断
        "execute": True,              # 所有 execute 调用都中断
        "delete_file": {             # 自定义审批策略
            "allowed_decisions": ["approve", "reject", "edit"],
            "description": "确认删除文件"
        }
    }
)

第一次调用:正常执行直到中断点

python 复制代码
result = agent.invoke(
    {"messages": [{"role": "user", "content": "帮我修改 config.py,把端口改成 8080"}]},
    config={"configurable": {"thread_id": "user_123_session_456"}}
)
# 执行到 edit_file,触发中断
# 返回 GraphInterrupt,携带待审批的 tool_call 信息

恢复执行:批准/修改/拒绝

python 复制代码
from langgraph.types import Command

# 场景1:批准原始调用
result = agent.invoke(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config={"configurable": {"thread_id": "user_123_session_456"}}
)

# 场景2:修改参数后执行
result = agent.invoke(
    Command(resume={
        "decisions": [{
            "type": "edit",
            "edited_action": {
                "name": "edit_file",
                "args": {"file_path": "config.py", "old_string": "port=3000", "new_string": "port=8080"}
            }
        }]
    }),
    config={"configurable": {"thread_id": "user_123_session_456"}}
)

# 场景3:拒绝
result = agent.invoke(
    Command(resume={
        "decisions": [{
            "type": "reject",
            "message": "这个文件不能修改,请换其他方案"
        }]
    }),
    config={"configurable": {"thread_id": "user_123_session_456"}}
)

多轮对话:同一个 thread_id

python 复制代码
# Round 1
result = agent.invoke(
    {"messages": [HumanMessage(content="查Q1报表")]},
    config={"configurable": {"thread_id": "user_456"}}
)
# checkpoint_001 saved

# Round 2(从 checkpoint_001 恢复,继续执行)
result = agent.invoke(
    {"messages": [HumanMessage(content="再加个Q2")]},
    config={"configurable": {"thread_id": "user_456"}}
)
# checkpoint_002 saved

五、应用层怎么做中断恢复?

实战代码:完整的中断恢复流程

python 复制代码
from deepagents.graph import create_deep_agent
from langgraph.checkpoint.redis import RedisSaver
from langgraph.types import Command
from langchain.agents.middleware.human_in_the_loop import HITLRequest

checkpointer = RedisSaver.from_url("redis://localhost:6379")

agent = create_deep_agent(
    model="claude-sonnet-4-6",
    tools=[...],
    checkpointer=checkpointer,
    interrupt_on={
        "edit_file": True,
        "execute": True,
    }
)

def run_with_hitl(user_input: str, thread_id: str):
    """带有人工审批的 Agent 执行"""
    while True:
        try:
            result = agent.invoke(
                {"messages": [HumanMessage(content=user_input)]},
                config={"configurable": {"thread_id": thread_id}}
            )
            return result

        except GraphInterrupt as e:
            # e.value 是 HITLRequest,包含待审批的 tool_calls
            hitl_request: HITLRequest = e.value
            print(f"⚠️  需要人工审批 {len(hitl_request['action_requests'])} 个操作")

            # 这里应该调用审批接口,让人类确认
            decisions = prompt_user_approval(hitl_request)

            # 用审批结果恢复执行
            user_input = None  # 恢复时不需要新输入
            result = agent.invoke(
                Command(resume={"decisions": decisions}),
                config={"configurable": {"thread_id": thread_id}}
            )
            return result

自动恢复:幂等调用

python 复制代码
def safe_invoke(agent, user_input: str, thread_id: str):
    """如果已经被中断过,直接从 checkpoint 恢复"""
    try:
        # 先检查是否有未完成的 checkpoint
        existing = checkpointer.get(thread_id, "latest")
        if existing and existing.get("pending_tool_calls"):
            # 存在未完成的流程,直接恢复
            return agent.invoke(
                Command(resume={"decisions": []}),  # 用空决策恢复
                config={"configurable": {"thread_id": thread_id}}
            )

        # 没有 checkpoint,正常执行
        return agent.invoke(
            {"messages": [HumanMessage(content=user_input)]},
            config={"configurable": {"thread_id": thread_id}}
        )
    except Exception as e:
        logger.error(f"Invoke failed: {e}")
        raise

异常处理与恢复

python 复制代码
from langgraph.errors import CheckpointNotFoundError

def run_with_recovery(thread_id: str, input_data: dict, max_retries: int = 3):
    for attempt in range(max_retries):
        try:
            result = agent.invoke(
                input_data,
                config={"configurable": {"thread_id": thread_id}}
            )
            return result

        except CheckpointNotFoundError:
            # 没有 checkpoint,从头开始
            input_data = None  # 恢复时会从 checkpoint 读取状态,不需要新 input
            continue

        except GraphInterrupt as e:
            # 人工审批中断,按正常流程处理
            raise

        except Exception as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(2 ** attempt)  # 指数退避

六、事故复盘:为什么那次没恢复?

回到开头的事故。分析日志发现:

ini 复制代码
[14:32:03] Node: process_data -> checkpoint saved  ← ✅ 写入成功
[14:35:47] ⚠️ Process killed (OOM)                 ← ✅ OOM 杀进程
[14:35:52] Process restarted                        ← ❌ 重启后用了新的 thread_id
[14:35:53] resume from checkpoint                    ← ❌ thread_id 不匹配,查不到

根因 :运维重启服务后,代码里每次请求都生成了新的 thread_id,导致 Checkpoint 查不到。

python 复制代码
# ❌ 错误:每次都用新的 thread_id(我司事故的根因)
thread_id = str(uuid4())  # 每次请求都是新的 UUID
result = graph.invoke(input, config={"configurable": {"thread_id": thread_id}})

# ✅ 正确:同一个用户/会话用同一个 thread_id
thread_id = f"user_{user_id}_session_{session_id}"  # 固定的
result = graph.invoke(input, config={"configurable": {"thread_id": thread_id}})

修复方案:Session 管理

python 复制代码
class SessionManager:
    def __init__(self, user_id: str, session_id: str):
        self.thread_id = f"user_{user_id}_session_{session_id}"

    def get_or_create(self, user_id: str, session_id: str) -> str:
        """获取固定的 thread_id"""
        return f"user_{user_id}_session_{session_id}"

    def invoke(self, agent, user_input: str):
        return agent.invoke(
            {"messages": [HumanMessage(content=user_input)]},
            config={"configurable": {"thread_id": self.thread_id}}
        )

    def resume(self, agent, decisions: list):
        return agent.invoke(
            Command(resume={"decisions": decisions}),
            config={"configurable": {"thread_id": self.thread_id}}
        )

七、Checkpoint 的数据结构

Checkpoint 完整结构

langgraph/checkpoint/base/__init__.py 定义了 Checkpoint 的结构(line 92-123):

python 复制代码
class Checkpoint(TypedDict):
    """State snapshot at a given point in time."""

    v: int                        # 版本号,当前是 `2`
    id: str                      # 唯一 ID,递增,用于排序
    ts: str                      # ISO 8601 时间戳
    channel_values: dict[str, Any]   # 各 channel 的当前值
    channel_versions: dict[str, str | int | float]  # 各 channel 的版本号
    versions_seen: dict[str, ChannelVersions]  # 每个节点见过的 channel 版本
    updated_channels: list[str] | None  # 本次更新的 channel 列表

channel_values 实际存储示例

对于 deepagents 的 Agent,典型的 channel_values 长这样:

python 复制代码
{
    "messages": [  # ← 使用 DeltaChannel,不是直接存完整列表
        AIMessage(content="用户的问题是...", id="msg_001"),
        ToolMessage(content="工具返回了...", tool_call_id="call_001", id="msg_002"),
        AIMessage(content="分析结果...", id="msg_003"),
    ],
    "fetch_data": {"status": "done", "rows": 1024},
    "process_data": {"status": "done", "result": [...]},
    "llm_output": {"reasoning": "...", "final": "..."},
}

注意messages 字段不是普通的 list,而是 DeltaChannel 类型,用特殊的 reducer 来增量更新。

CheckpointMetadata

python 复制代码
class CheckpointMetadata(TypedDict, total=False):
    source: Literal["input", "loop", "update", "fork"]
    """checkpoint 来源:input=输入、loop=循环、update=手动更新、fork=分支"""

    step: int
    """步骤号。-1 是第一个 input,0 是第一个 loop"""

    parents: dict[str, str]
    """父 checkpoint ID(用于回溯)"""

    run_id: str
    """创建这个 checkpoint 的 run ID"""

    counters_since_delta_snapshot: dict[str, tuple[int, int]]
    """每个 channel 的更新次数和 superstep 数(用于 DeltaChannel 快照策略)"""

CheckpointTuple:完整的检查点元组

python 复制代码
class CheckpointTuple(NamedTuple):
    config: RunnableConfig          # 包含 thread_id, checkpoint_id 等
    checkpoint: Checkpoint           # 实际的 checkpoint 数据
    metadata: CheckpointMetadata     # 元数据
    parent_config: RunnableConfig | None = None  # 父 checkpoint 的配置
    pending_writes: list[PendingWrite] | None = None  # 已提交但未 flush 的写

PendingWrite:中间态写入

python 复制代码
PendingWrite = tuple[str, str, Any]  # (task_id, channel_name, value)

当一个 Node 执行完毕但 checkpoint 还没写入时,写入会先存在 pending_writes 里。


八、_messages_delta_reducer:消息怎么增量存储

问题:为什么需要特殊的 reducer?

对于 messages 字段,如果每次都存完整列表:

python 复制代码
# ❌ 朴素存储:每次追加都是 O(N)
messages: [msg1, msg2, ..., msg1000]  # N=1000 时,序列化/反序列化很慢

随着对话进行,消息列表越来越长,每次序列化和反序列化的成本是 O(N²)------每次写入都要复制整个列表。

解决方案:DeltaChannel + _messages_delta_reducer

deepagents 的 _DeepAgentStatedeepagents/graph.py line 63-66):

python 复制代码
class _DeepAgentState(AgentState):
    """AgentState with DeltaChannel on messages to reduce checkpoint growth from O(N²) to O(N)."""

    messages: Required[
        Annotated[
            list[AnyMessage],
            DeltaChannel(_messages_delta_reducer, snapshot_frequency=50)  # ← 关键
        ]
    ]

这行代码的意思是:

  • messages 是一个 list[AnyMessage]
  • DeltaChannel 包装,而不是普通 channel
  • snapshot_frequency=50 表示每 50 次更新才写一次完整快照

_messages_delta_reducer 做了什么?

deepagents/_messages_reducer.py 的核心逻辑(line 31-87):

python 复制代码
def _messages_delta_reducer(
    state: list[AnyMessage],      # 当前已存在的消息列表
    writes: list[list[AnyMessage]]  # 新写入的消息批次
) -> list[AnyMessage]:
    """增量 reducer:只追加新消息,不复制旧消息"""
    # 1. 扁平化所有 writes
    flat = []
    for w in writes:
        flat.extend(w) if isinstance(w, list) else flat.append(w)

    # 2. 按 ID 去重(ID 已经在 LangGraph 层面分配好了)
    # 3. 墓碑机制:RemoveMessage(id) 把对应 ID 的消息标记为删除
    # 4. REMOVE_ALL_MESSAGES:清空所有消息
    result = []
    index = {}  # id -> position
    for m in state:
        if m.id is not None:
            index[m.id] = len(result)
        result.append(m)

    for msg in flat:
        mid = msg.id
        if mid is None:
            result.append(msg)
        elif isinstance(msg, RemoveMessage):
            if mid in index:
                result[index[mid]] = None  # 墓碑
                del index[mid]
        elif mid in index:
            result[index[mid]] = msg  # 更新已有消息
        else:
            index[mid] = len(result)
            result.append(msg)

    return [m for m in result if m is not None]  # 过滤墓碑

增量存储的实际效果

对话轮数 朴素存储 DeltaChannel 存储
Round 1 msg1 msg1
Round 10 msg1,...,msg10 msg1 + delta
Round 100 msg1,...,msg100 msg1 + 2个snapshot
Round 1000 msg1,...,msg1000 msg1 + 10个snapshot

snapshot_frequency=50 意味着:每 50 条消息写一次完整快照,中间只存增量 delta。这样反序列化时最多重放 50 条消息,成本从 O(N²) 降到 O(N)

墓碑机制:怎么删除消息?

python 复制代码
# 墓碑消息,标记某个 ID 的消息被删除
RemoveMessage(id="msg_003")

# 在 reducer 里:
elif isinstance(msg, RemoveMessage):
    if mid in index:
        result[index[mid]] = None  # 标记为 None
        del index[mid]

# 最终过滤掉 None
return [m for m in result if m is not None]

九、DeltaChannel 的快照策略

什么时候写完整快照?

DeltaChannel 有两个触发快照的条件(langgraph/channels/delta.py line 50-55):

  1. 更新次数达到 snapshot_frequency(默认 1000)
  2. superstep 数达到 DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT(系统级上限,默认 5000)
python 复制代码
# DeltaChannel.checkpoint() 永远返回 MISSING
# 快照写入由 create_checkpoint 决定
def checkpoint(self) -> Any:
    return MISSING  # 不存完整值,只存 sentinel

真正写入快照的地方在 create_checkpoint,它会检查:

python 复制代码
# 如果这个 channel 的更新次数 >= snapshot_frequency
# 或者系统 superstep 数 >= DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT
# 就写入 _DeltaSnapshot(value)

_DeltaSnapshot 结构

python 复制代码
# langgraph/checkpoint/serde/types.py
class _DeltaSnapshot:
    value: Any  # 当时 channel 的完整值

恢复时:

python 复制代码
# DeltaChannel.from_checkpoint()
if isinstance(checkpoint, _DeltaSnapshot):
    new.value = checkpoint.value  # 直接恢复快照
else:
    # 否则需要重放增量writes
    new.replay_writes(writes)

十、面试题

Q1:LangGraph 的 Checkpoint 和 Store 有什么区别?

维度 Checkpoint Store
用途 执行过程中的中间状态 跨 session 的持久化知识
生命周期 一个 Thread 执行期间 永久
触发时机 每个 Node 执行完毕 手动写入
类比 工作记忆(RAM) 长期记忆(硬盘)

Q2:Checkpoint 的存储结构是什么样的?thread_id 在哪?

python 复制代码
# Checkpoint 是 KV 存储,key 是 (thread_id, checkpoint_id)
# thread_id 在 config 里,不是在 checkpoint 内部
{
    "key": ("user_123", "checkpoint_003"),
    "value": {
        "v": 2,
        "id": "checkpoint_003",
        "ts": "2024-03-01T14:32:03.000Z",
        "channel_values": {
            "messages": [...],
            "fetch_data": {"status": "done"}
        }
    }
}

thread_id 是查询条件,不是存储内容。所以换个 thread_id 就查不到,这是开头事故的根因。

Q3:为什么 messages 用 DeltaChannel 而不是普通列表?

时间复杂度:朴素存储是 O(N²),DeltaChannel 增量是 O(N)。

实际例子

python 复制代码
# 1000 轮对话后
朴素存储: 1000 条消息 × 序列化 = O(1000²) = 百万级操作
DeltaChannel: 1个snapshot + 20个delta × 重放50条 = O(1000) 线性

Q4:DeltaChannel 的快照策略是什么?什么时候不写快照?

两个触发条件:

  1. 该 channel 的更新次数达到 snapshot_frequency(默认 1000)
  2. 系统 superstep 数达到 DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT(默认 5000)

中间状态只存增量 writes,不存完整值。

Q5:什么是墓碑机制?为什么需要它?

墓碑RemoveMessage(id="xxx"),标记某个 ID 的消息被删除。

为什么需要:消息在 reducer 层去重和更新时,需要一个标记来表示"这条消息曾经存在,但现在被删了"。直接删除会导致 ID 错位,墓碑保留位置但过滤掉最终输出。

Q6:deepagents 的中断是怎么实现的?

核心依赖 langgraph.types.interrupt()

python 复制代码
# 当 HumanInTheLoopMiddleware 检测到需要审批的 tool_call
decisions = interrupt(hitl_request)["decisions"]
# → raise GraphInterrupt(hitl_request)

客户端收到 GraphInterrupt 后,必须用 Command(resume={...}) 恢复执行。

Q7:Checkpoint 怎么保证并发安全?

乐观锁。写入时会检查版本号:

python 复制代码
def put(thread_id, checkpoint_id, state, version):
    existing = db.get(thread_id, checkpoint_id)
    if existing.version != version:
        raise ConcurrentUpdateError()  # 版本冲突
    db.put(thread_id, checkpoint_id, state)

冲突时框架自动重试,业务层感知不到。

Q8:进程崩溃后,Checkpoint 还能恢复吗?

取决于 Checkpointer 类型

  • MemorySaver:进程重启后丢失,不能恢复
  • RedisSaver能恢复(Redis 持久化)
  • PostgresSaver能恢复(Postgres 持久化)

十一、线上问题案例

案例一:Redis 内存打满,Checkpoint 全部丢失

现象:Redis 实例内存被打满,所有 checkpoint 数据被 Redis 主动 eviction。用户 session 全部失效。

根因:Redis 没有设置 maxmemory + 淘汰策略,所有数据存在同一 DB。

解决

  1. Redis 分开部署:checkpoint 用专用实例,设置 maxmemory + allkeys-lru
  2. 降级策略:Redis 不可用时,用 Postgres 做 fallback
  3. 监控:内存使用 > 70% 预警

案例二:thread_id 不固定导致恢复失败

现象:用户反馈"我的任务跑到一半,刷新页面后要从头跑"。

根因:前端每次请求生成新的 session_id,导致 thread_id 变化。

解决:thread_id 存储在前端 localStorage,整个会话用同一个 thread_id。

案例三:DeltaChannel 导致的历史消息消失

现象:用户反映"很早之前的消息在恢复后找不到了"。

根因:DeltaChannel 的快照策略问题------如果某个 channel 很久没更新,中间的增量 writes 可能因为 prune 操作丢失,导致恢复时无法重建完整历史。

解决

  1. 不要对使用 DeltaChannel 的 thread 做 aggressive prune
  2. 或者把 snapshot_frequency 调低(更频繁写快照)

十二、问题速查表

分类 问题 解法
中断恢复 OOM 后状态丢失 Redis/PG checkpointer,固定 thread_id
中断审批 需要人工确认工具调用 interrupt_on + HumanInTheLoopMiddleware
并发安全 多请求同一 thread_id 乐观锁,框架自动重试
存储膨胀 checkpoint 太大 DeltaChannel + 大对象存 Store
Redis 故障 全站不可用 Redis + Postgres 双写 fallback
thread_id 每次请求都是新的 用 user_id + session_id 固定
历史丢失 prune 导致 DeltaChannel 重建失败 不要 aggressive prune

十三、总结

deepagents 的中断机制依赖两层:1) Checkpointer 负责状态持久化 2) HumanInTheLoopMiddleware 触发 interrupt()。两者缺一不可。

Checkpoint 存储结构:channel_values 存各节点输出,DeltaChannel 负责 messages 的增量更新,墓碑机制处理消息删除。

核心铁律:thread_id 必须固定,否则中断恢复不到正确状态。

相关推荐
Black蜡笔小新12 小时前
制造业AI质检工作站/自动化AI算法训练服务器DLTM企业AI算力工作站筑牢制造业品质防线
人工智能·算法·自动化
hughnz12 小时前
AI 掌舵:量化上游石油和天然气的下一轮价值革命
人工智能
imbackneverdie12 小时前
论文/课题/组会PPT技术路线图绘制完整教程
人工智能·信息可视化·aigc·科研·论文写作·科研绘图·ai工具
一点一木12 小时前
Claude Opus 4.8 实测:AI 终于学会「承认自己不知道」了?
前端·人工智能·claude
Elastic 中国社区官方博客12 小时前
从平均值到任意百分位:Elasticsearch 在 ES|QL 中提供原生 exponential histogram 支持
大数据·人工智能·elasticsearch·搜索引擎·信息可视化·全文检索·数据可视化
爱和冰阔落12 小时前
【Codex配置实战】从 config.toml 到 AGENTS.md:把 AI 编程助手调成顺手的开发环境
人工智能·codex
星辰AI12 小时前
数据增强方法:提升模型泛化能力的利器
人工智能·ai·语言模型
码云骑士12 小时前
Gemini实战:用AI写CI/CD脚本,提升研发效能
人工智能·ci/cd
2601_9594801512 小时前
Moneta Markets亿汇:“软件业绩凸显云端需求”
人工智能