线上事故复盘: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 必须固定,否则中断恢复不到正确状态。

相关推荐
甲维斯1 分钟前
马斯克Grok4.5太会了!狙击GPT5.6,贴脸Opus4.8!
人工智能·ai编程
雪碧聊技术16 分钟前
Badge 应用场景与落地实践指南
大数据·人工智能
ai产品老杨39 分钟前
NVIDIA GPU部署AI视频分析项目实战记录
人工智能·音视频
LL334455672 小时前
创业自动化平台怎么选
大数据·人工智能
OceanBase数据库官方博客3 小时前
OceanBase AI 时代,数据库的变与不变(技术解析与实践)
数据库·人工智能·oceanbase
冬奇Lab3 小时前
MCP 系列(01):MCP 是什么——为什么 Function Calling 不够
人工智能·llm·mcp
冬奇Lab3 小时前
每日一个开源项目(第154篇):Warp - 从‘好看的终端‘到 Agentic 开发环境
人工智能·rust·llm
ZZZMMM.zip3 小时前
基于鸿蒙HarmonyOS NEXT开发AI股票分析应用:智能投资新体验与鸿蒙Flutter框架跨端实践
人工智能·flutter·华为·harmonyos·鸿蒙
龙腾亚太3 小时前
当大语言模型遇上USV集群:大模型驱动的自适应路径规划方法
人工智能·语言模型·自然语言处理