Agent 对话中断恢复:用户断线后如何无缝继续会话
Agent 对话中断恢复用户断线后如何无缝继续会话一、用户切了个网络Agent 忘了刚才聊到哪了Agent 对话最大的不确定性不是模型幻觉不是工具调用失败是网络。用户在地铁上跟 Agent 聊产品需求分析信号断了 30 秒重连回来——对话历史全部丢失Agent 重新打招呼你好有什么可以帮你。用户心态崩溃重来一次体验灾难。HTTP 是无状态协议WebSocket 可以保持连接但在移动端切网络4G → WiFi → 4G的场景下WebSocket 照样会断开。所以对话恢复不能依赖连接不断——连接一定会断——而要依赖状态不丢。对话中断恢复的核心不是重连技术WebSocket 自动重连库很多而是对话状态的持久化和恢复。状态包括三个关键部分对话历史messages、工具调用中间态当前正在调用的工具及参数、用户上下文用户偏好、当前任务目标。只要这三个状态有 Checkpoint 持久化用户无论断多久重连都能接着聊。二、底层机制与原理剖析会话恢复的三个关键设计消息序号Sequence Number每条消息用户发送、Agent 回复都有一个严格递增的序号。重连时客户端上报自己收到的最后一条消息序号服务端从这个序号之后开始推送。这个设计解决了服务端不知道客户端收到多少的问题。Session Snapshot会话快照不只是消息列表——还包括 Agent 当前的状态。如果 Agent 正在执行工具调用如搜索恢复时不能让 Agent 重新搜一遍——它应该知道上次搜索的结果是 X。所以快照中要包含工具调用的中间结果。重连窗口会话不能永远保留——设置一个窗口。例如断开 5 分钟内的会话自动恢复5 分钟以上的视为过期Agent 任务可能已经失去时效性或者用户已经重开了一个新会话。三、生产级代码实现 Agent 对话中断恢复系统 核心数据结构SessionSnapshot 核心策略消息序号 增量同步 重连窗口 import asyncio import json import time import uuid import logging from typing import Dict, List, Optional, Any from dataclasses import dataclass, field from enum import Enum from datetime import datetime, timedelta import redis.asyncio as redis logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class SessionStatus(Enum): ACTIVE active INTERRUPTED interrupted # 连接断开等待恢复 EXPIRED expired # 超时未恢复已清理 COMPLETED completed class MessageRole(Enum): USER user AGENT agent SYSTEM system TOOL tool dataclass class Message: 单条消息 seq: int # 消息序号严格递增 role: MessageRole content: str timestamp: float # Unix timestamp metadata: Dict[str, Any] field(default_factorydict) # 工具调用信息等 def to_dict(self) - dict: return { seq: self.seq, role: self.role.value, content: self.content, timestamp: self.timestamp, metadata: self.metadata, } classmethod def from_dict(cls, data: dict) - Message: return cls( seqdata[seq], roleMessageRole(data[role]), contentdata[content], timestampdata[timestamp], metadatadata.get(metadata, {}), ) dataclass class SessionSnapshot: 会话快照 —— 持久化到 Redis session_id: str user_id: str status: SessionStatus SessionStatus.ACTIVE messages: List[Message] field(default_factorylist) last_seq: int 0 # Agent 中间态 pending_tool_call: Optional[Dict[str, Any]] None # 正在执行的工具调用 agent_context: Dict[str, Any] field(default_factorydict) # Agent 上下文 # 时间窗口 created_at: float 0.0 last_active_at: float 0.0 interrupted_at: Optional[float] None def __post_init__(self): now time.time() if not self.created_at: self.created_at now if not self.last_active_at: self.last_active_at now def to_dict(self) - dict: return { session_id: self.session_id, user_id: self.user_id, status: self.status.value, messages: [m.to_dict() for m in self.messages], last_seq: self.last_seq, pending_tool_call: self.pending_tool_call, agent_context: self.agent_context, created_at: self.created_at, last_active_at: self.last_active_at, interrupted_at: self.interrupted_at, } classmethod def from_dict(cls, data: dict) - SessionSnapshot: return cls( session_iddata[session_id], user_iddata[user_id], statusSessionStatus(data[status]), messages[Message.from_dict(m) for m in data.get(messages, [])], last_seqdata.get(last_seq, 0), pending_tool_calldata.get(pending_tool_call), agent_contextdata.get(agent_context, {}), created_atdata.get(created_at, 0), last_active_atdata.get(last_active_at, 0), interrupted_atdata.get(interrupted_at), ) class SessionManager: 会话管理器 设计思路 1. 所有会话状态持久化到 Redis内存不保存 2. 支持从任意断点恢复基于消息序号 3. 重连窗口5 分钟——过期会话自动清理 RECONNECT_WINDOW 300 # 重连窗口5 分钟 SESSION_TTL 3600 # Session TTL1 小时 KEY_PREFIX agent:session: def __init__(self, redis_client: redis.Redis): self.redis redis_client def _key(self, session_id: str) - str: return f{self.KEY_PREFIX}{session_id} async def create_session(self, user_id: str) - SessionSnapshot: 创建新会话 session SessionSnapshot( session_idstr(uuid.uuid4()), user_iduser_id, ) await self._save(session) logger.info(Session created: %s for user %s, session.session_id, user_id) return session async def get_session(self, session_id: str) - Optional[SessionSnapshot]: 获取会话快照 如果会话状态为 INTERRUPTED计算是否在重连窗口内 - 在窗口内状态恢复为 ACTIVE返回完整快照 - 窗口外状态标记为 EXPIRED返回 None key self._key(session_id) raw await self.redis.get(key) if not raw: return None session SessionSnapshot.from_dict(json.loads(raw)) if session.status SessionStatus.INTERRUPTED: assert session.interrupted_at is not None elapsed time.time() - session.interrupted_at if elapsed self.RECONNECT_WINDOW: # 超时——标记为过期 session.status SessionStatus.EXPIRED await self._save(session) logger.info(Session %s expired (interrupted %.0fs ago), session_id, elapsed) return None else: # 在窗口内——恢复 session.status SessionStatus.ACTIVE session.interrupted_at None session.last_active_at time.time() await self._save(session) logger.info(Session %s recovered after %.0fs, session_id, elapsed) return session async def mark_interrupted(self, session_id: str): 标记会话中断 key self._key(session_id) raw await self.redis.get(key) if not raw: return session SessionSnapshot.from_dict(json.loads(raw)) session.status SessionStatus.INTERRUPTED session.interrupted_at time.time() await self._save(session) logger.info(Session %s marked interrupted, session_id) async def add_message(self, session_id: str, role: MessageRole, content: str, metadata: Optional[Dict] None) - Optional[int]: 添加一条消息返回消息序号 key self._key(session_id) raw await self.redis.get(key) if not raw: logger.error(Session %s not found for add_message, session_id) return None session SessionSnapshot.from_dict(json.loads(raw)) session.last_seq 1 msg Message( seqsession.last_seq, rolerole, contentcontent, timestamptime.time(), metadatametadata or {}, ) session.messages.append(msg) session.last_active_at time.time() await self._save(session) return session.last_seq async def get_messages_after(self, session_id: str, after_seq: int) - List[Message]: 获取指定序号之后的消息用于重连后的增量同步 key self._key(session_id) raw await self.redis.get(key) if not raw: return [] session SessionSnapshot.from_dict(json.loads(raw)) return [m for m in session.messages if m.seq after_seq] async def update_agent_context(self, session_id: str, context: Dict[str, Any]): 更新 Agent 中间上下文 key self._key(session_id) raw await self.redis.get(key) if not raw: return session SessionSnapshot.from_dict(json.loads(raw)) session.agent_context.update(context) session.last_active_at time.time() await self._save(session) async def _save(self, session: SessionSnapshot): 持久化到 Redis 并设置 TTL key self._key(session.session_id) await self.redis.setex( key, self.SESSION_TTL, json.dumps(session.to_dict(), ensure_asciiFalse), ) # --------------------------------------------------------------------------- # 模拟 WebSocket 连接处理 # --------------------------------------------------------------------------- async def handle_websocket(websocket, session_manager: SessionManager): WebSocket 事件处理器 处理两种连接场景 1. 新会话无 session_id→ 创建会话 2. 恢复会话有 session_id→ 从断点恢复 try: # 1. 接收初始消息包含 session_id init_msg await asyncio.wait_for(websocket.receive_text(), timeout10.0) init_data json.loads(init_msg) session_id init_data.get(session_id) if session_id: # 尝试恢复会话 session await session_manager.get_session(session_id) if session: # 发送从断点之后的消息 last_received init_data.get(last_seq, 0) missed await session_manager.get_messages_after(session_id, last_received) for msg in missed: await websocket.send_text(json.dumps({ type: history, **msg.to_dict(), })) else: # 会话过期或不存在——创建新会话 session await session_manager.create_session(init_data.get(user_id, anonymous)) session_id session.session_id else: # 新会话 session await session_manager.create_session(init_data.get(user_id, anonymous)) session_id session.session_id # 2. 回执 session_id客户端保存用于下次重连 await websocket.send_text(json.dumps({ type: session_created, session_id: session_id, })) # 3. 消息循环 async for raw_message in websocket: msg_data json.loads(raw_message) content msg_data.get(content, ) role MessageRole.USER # 保存用户消息 await session_manager.add_message(session_id, role, content) # 模拟 Agent 回复生产环境调用 Agent 引擎 response fAgent 回复: {content} seq await session_manager.add_message(session_id, MessageRole.AGENT, response) await websocket.send_text(json.dumps({ type: agent_message, seq: seq, content: response, })) except asyncio.TimeoutError: logger.error(WebSocket: initial message timeout) except Exception as e: logger.error(WebSocket error: %s, e) # 连接异常断开标记会话为 INTERRUPTED if session_id: await session_manager.mark_interrupted(session_id)四、边界分析与架构权衡重连窗口的设定太短如 30 秒地铁过隧道、电梯进出这种网络短暂中断的用户无法恢复太长如 1 小时会话长期占用 Redis 内存且恢复一个 30 分钟前的会话没有意义推荐 5 分钟覆盖大部分短暂断网同时控制资源开销消息重放的性能如果对话历史有 200 条消息重连时逐条重放会导致用户端长时间刷屏优化重放时把历史消息合并为一条type: history_batch消息客户端一次性渲染对于极长对话1000 条只重放最近 50 条 摘要安全性注意事项session_id 不能是可猜测的——必须使用 UUID v4 或更高强度的随机标识用户只能恢复自己的会话——get_session时需要校验 session 的 user_id 与请求的 user_id 是否一致敏感会话如涉及支付、个人信息的对话恢复时需要二次认证五、总结Agent 对话中断恢复核心不是传输层WebSocket 重连库很多是应用层的状态持久化。消息序号 Session Snapshot 重连窗口三个机制组合序号解决增量同步快照解决 Agent 中间状态恢复窗口解决资源控制。关键是认知转变——连接一定会断要让断开无感。

相关新闻

OpenClaw与CoPaw:轻量级AI代理的Token优化指南

OpenClaw与CoPaw:轻量级AI代理的Token优化指南

1. OpenClaw与CoPaw项目概述OpenClaw和CoPaw是阿里巴巴开源的轻量级AI代理组合,专为解决AI代理运行时的内存占用和本地自动化问题而设计。这对组合采用Manager-Worker多代理协作架构,其中Manager作为"AI管家"负责协调多个专业Worker&#xff0…

2026/10/2 6:21:38 阅读更多 →
全球顶级AI认证解析与职业发展指南

全球顶级AI认证解析与职业发展指南

1. 行业认证的价值与选择逻辑在人工智能行业深耕多年,我深刻体会到专业认证对于职业发展的重要性。不同于普通培训证书,真正具有含金量的认证往往需要投入大量时间系统学习,并通过严格考核才能获得。这类认证不仅能系统化梳理知识体系&#x…

2026/10/13 4:57:30 阅读更多 →
零跑Lafa5标准版与Ultra版核心差异解析

零跑Lafa5标准版与Ultra版核心差异解析

1. Lafa5标准版与Ultra版核心差异解析 作为零跑汽车2023年推出的重磅车型,Lafa5凭借其25处储物空间的实用设计和廊桥式中央扶手等创新元素,迅速成为都市年轻家庭的热门选择。但在标准版与Ultra版之间近4万元的价差背后,究竟隐藏着哪些实质性升…

2026/10/6 10:49:21 阅读更多 →

最新新闻

UCINET数据导入实战:格式规范、Excel整理与高频排错

UCINET数据导入实战:格式规范、Excel整理与高频排错

1. 先搞清楚UCINET到底在等什么数据很多人在UCINET上栽的第一个跟头,不是不会跑算法,而是数据根本喂不进去。明明Excel里填得工工整整,一点Import却报错,或者导进去之后变成一团乱麻,节点对不上、值全错位。这些问题的…

2026/10/13 4:57:19 阅读更多 →
上班族碎片时间做小红书:工位运营全攻略,高效副业不焦虑

上班族碎片时间做小红书:工位运营全攻略,高效副业不焦虑

开头作为一个常年白天在公司坐班、晚上回家还想搞点副业的运营人,我发现周围越来越多同事开始把小红书当成一个“工作之外的第二战场”。但很多人卡在同一个地方:不是不会写文案,也不是不懂拍照,而是总觉得“没时间”——上班忙成…

2026/10/13 4:57:19 阅读更多 →
InterviewGuide 力扣刷题笔记:80. 删除排序数组中的重复项 II——原地去重的双指针通用解法

InterviewGuide 力扣刷题笔记:80. 删除排序数组中的重复项 II——原地去重的双指针通用解法

文档教程知识库 【免费下载链接】InterviewGuide 🔥🔥「InterviewGuide」是阿秀从校园->职场多年计算机自学过程的记录以及学弟学妹们计算机校招&秋招经验总结文章的汇总,包括但不限于C/C 、Golang、JavaScript、Vue、操作系统、数据结…

2026/10/13 4:57:19 阅读更多 →
AnyPS5:PS5存储扩展、外设兼容与网络优化全攻略

AnyPS5:PS5存储扩展、外设兼容与网络优化全攻略

说实话,一开始想做“AnyPS5”这个项目的动机挺简单的:我自己手头有几台不同批次、不同使用时长、甚至连摆放环境都完全不一样PS5主机。有朋友来家里玩,都会问同样的问题——为什么我这台开游戏就是快一点?为什么他那台插个手柄总是…

2026/10/13 4:57:19 阅读更多 →
Hermes Agent入门:轻量级协议语义翻译器实战指南

Hermes Agent入门:轻量级协议语义翻译器实战指南

1. 为什么“Hermes Agent”突然被大量开发者同时提及?最近两周,我在几个技术交流群和内部项目复盘会上,连续听到不同背景的开发者提到同一个词:Hermes Agent。不是Hermes引擎、不是React Native的Hermes JS引擎,而是带…

2026/10/13 4:57:19 阅读更多 →
Android ANR问题全链路分析:从判定机制到测试解决方案

Android ANR问题全链路分析:从判定机制到测试解决方案

我最早接触ANR,是线上用户反馈一个列表页“点了没反应”,过一会儿系统弹窗“进程无响应”。当时拿着一个logcat日志,翻来覆去只能看到一行“ANR in”,完全不知道怎么下手。后来带测试团队、又参与线上性能治理,踩了无数…

2026/10/13 4:56:18 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →