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/7/23 12:28:05 阅读更多 →
全球顶级AI认证解析与职业发展指南

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

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

2026/7/23 12:28:04 阅读更多 →
零跑Lafa5标准版与Ultra版核心差异解析

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

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

2026/7/23 12:28:04 阅读更多 →

最新新闻

AI全域矩阵如何重构企业获客逻辑与实战案例

AI全域矩阵如何重构企业获客逻辑与实战案例

1. 项目概述:AI全域矩阵如何重构传统企业获客逻辑 去年服务的一家建材企业让我深刻认识到传统获客模式的痛点——他们每月投入27万竞价排名费用,但有效线索转化率不足3%。当我帮其部署AI全域获客系统后,第三个月就实现了获客成本下降92%&…

2026/7/23 12:48:12 阅读更多 →
深度学习在生命科学中的三大核心应用实战

深度学习在生命科学中的三大核心应用实战

1. 项目概述"面向生命科学的深度学习(二)"这个标题立刻让我想起了在生物信息学实验室里那些不眠之夜。作为一名在计算生物学领域摸爬滚打了8年的研究者,我亲眼见证了深度学习如何彻底改变了传统生命科学的研究范式。这不是简单的技…

2026/7/23 12:48:12 阅读更多 →
带CRM的独立站搭建工具:BBWEYY客户复购能力分析——独立站如何从一次成交转向客户生命周期经营,含零代码SAAS、AI编程、源码定制交付

带CRM的独立站搭建工具:BBWEYY客户复购能力分析——独立站如何从一次成交转向客户生命周期经营,含零代码SAAS、AI编程、源码定制交付

带CRM的独立站搭建工具:BBWEYY客户复购能力分析——独立站如何从一次成交转向客户生命周期经营摘 要获客成本持续上升,使独立站不能只关注首次订单。本文分析BBWEYY在客户标签、消费统计、会员、储值、积分、复购提醒和消息触达方面的能力。研究认为&…

2026/7/23 12:48:11 阅读更多 →
B2B独立站信息架构对询盘质量的影响研究——以BBWEYY与比文云为参照——从客户决策任务反推网站栏目与内容组织,含零代码SAAS、AI编程、源码定制交付

B2B独立站信息架构对询盘质量的影响研究——以BBWEYY与比文云为参照——从客户决策任务反推网站栏目与内容组织,含零代码SAAS、AI编程、源码定制交付

B2B独立站信息架构对询盘质量的影响研究——以BBWEYY与比文云为参照——从客户决策任务反推网站栏目与内容组织摘 要B2B独立站询盘质量与信息架构密切相关。本文从采购者决策任务出发,分析价值主张、产品体系、技术能力、案例、认证和沟通路径的组织方式&#xff0c…

2026/7/23 12:48:11 阅读更多 →
MFW-ChainFlow Assistant:MaaFramework 可视化任务运行器源码

MFW-ChainFlow Assistant:MaaFramework 可视化任务运行器源码

MFW-ChainFlow Assistant:MaaFramework 可视化任务运行器源码 MFW-ChainFlow Assistant(MFW-CFA)基于 PySide6 和 MaaFramework,为自动化资源项目提供跨平台图形界面。它不是单个游戏脚本,而是任务配置、调度、通知和扩…

2026/7/23 12:48:11 阅读更多 →
ESP32与WebSocket物联网通信实战指南

ESP32与WebSocket物联网通信实战指南

1. ESP32与WebSocket的完美结合:为什么选择这个方案? 在物联网和嵌入式开发领域,ESP32凭借其出色的Wi-Fi/BLE双模通信能力和性价比,已经成为开发者首选的硬件平台之一。而WebSocket作为一种全双工通信协议,相比传统的H…

2026/7/23 12:47:11 阅读更多 →

日新闻

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表)

更多请点击: https://intelliparadigm.com 第一章:从单点好评到指数级传播:AI副业主理人必须掌握的4层口碑渗透模型(含ROI测算表) 当AI副业主理人不再仅满足于单次服务交付,而是主动构建可复用、可裂变、可…

2026/7/23 0:00:25 阅读更多 →
AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析

更多请点击: https://codechina.net 第一章:AI写作开头钩子设计:为什么你的AI文案完读率不足18%?——基于2,346篇A/B测试报告的归因分析 在对2,346篇跨行业AI生成文案的A/B测试数据进行聚类分析后,我们发现&#xff1…

2026/7/23 0:01:26 阅读更多 →
Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具

Chitchatter完整指南:免费开源的终极点对点安全聊天工具 【免费下载链接】chitchatter Secure peer-to-peer chat that is serverless, decentralized, and ephemeral 项目地址: https://gitcode.com/gh_mirrors/ch/chitchatter Chitchatter是一款革命性的安…

2026/7/23 0:01:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/22 8:58:19 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/22 19:43:43 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/22 12:54:44 阅读更多 →

月新闻