| 项目 | 内容 |
|---|---|
| 文档标题 | Telegram Bot 上下文管理重构技术设计 |
| 版本 | V2.2 |
| 作者 | Claude Agent |
| 创建日期 | 2025-12-10 |
| 最后更新 | 2025-12-10 |
| 状态 | 待评审 |
| 版本 | 日期 | 作者 | 变更内容 |
|---|---|---|---|
| V1.0 | 2025-12-10 | Claude Agent | 初始版本 - 基于 claude-code-sdk 的手动上下文管理方案 |
| V2.0 | 2025-12-10 | Claude Agent | 重大更新 - 迁移到 claude-agent-sdk,使用内置 automatic compaction |
| V2.1 | 2025-12-10 | Claude Agent | 简化持久化 - Session 恢复即历史恢复,移除本地对话历史存储 |
| V2.2 | 2025-12-10 | Claude Agent | 修正 Session 存储位置 - Session 数据存储在本地 CLI 目录,非服务端 |
Telegram Bot 在长时间对话后出现“抽风”现象:
通过代码审查发现,问题根源是上下文管理机制的设计缺陷:
位置: src/claude_agent/telegram/claude_adapter.py _build_full_prompt() 方法
每次请求都将 CLAUDE.md + 全部历史消息 + 新消息 拼接成一个巨大字符串 没有任何 Token 限制检查
位置: src/claude_agent/core/agent.py process_user_input() 方法
完整的 full_prompt (包含系统提示和所有历史) 被原样存入 conversation_history 导致历史记录指数级膨胀
位置: src/claude_agent/telegram/claude_adapter.py 第758-773行
if len(full_prompt) > 1000000: # 1MB 才触发! full_prompt = full_prompt[:20000] # 字符截断,破坏上下文结构
当前使用: claude-code-sdk (已废弃)
query() 函数应该迁移到: claude-agent-sdk (新版)
ClaudeSDKClient 支持多轮对话| 指标 | 目标值 |
|---|---|
| 最大支持对话轮数 | 无限制 (SDK 自动压缩) |
| 单条消息响应时间 | < 5秒 |
| 上下文管理 | SDK 内置,自动处理 |
| 内存占用(每会话) | < 1MB |
| 特性 | claude-code-sdk (旧) | claude-agent-sdk (新) |
|---|---|---|
| 维护状态 | ❌ 已废弃 | ✅ 活跃维护 |
| 最新版本 | 0.0.25 (2025-09-29) | 0.1.13 (2025-12-05) |
| Context 管理 | ❌ 无,需手动实现 | ✅ 内置 Automatic Compaction |
| 多轮对话 | ❌ 每次 query() 独立 | ✅ ClaudeSDKClient 支持 |
| Session 管理 | ❌ 无 | ✅ 内置 |
| 工具生态 | 基础 | 丰富(文件、代码执行、MCP) |
| Hooks | 有限 | 完整(PreToolUse, PostToolUse 等) |
| 方案 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| V1.0 手动管理 | 自己实现 Token 计数、截断 | 完全控制 | 复杂、易出错、重复造轮子 |
| V2.0 SDK 内置 | 使用 claude-agent-sdk 的 automatic compaction | 简单、可靠、官方维护 | 需要迁移代码 |
选择: V2.0 - 迁移到 claude-agent-sdk
flowchart TD subgraph 当前流程 A[用户消息] --> B[_build_full_prompt] B --> C["拼接: CLAUDE.md + 全部历史 + 消息"] C --> D["query() - 无状态调用"] D --> E["存入 conversation_history<br/>(污染的数据)"] E --> F["下次请求: 历史更大"] F --> G["最终: 超出限制, Bot 抽风"] end style C fill:#ff6b6b style E fill:#ff6b6b style G fill:#ff6b6b
flowchart TD subgraph 新流程 A[用户消息] --> B[ClaudeSDKClient] B --> C{上下文接近限制?} C -->|是| D["SDK 自动触发 Compaction<br/>(内置功能)"] D --> E[生成 Summary] E --> F[继续对话] C -->|否| F F --> G["SDK 内部管理历史"] G --> H[响应用户] H --> I["持久化 Session 状态"] end style D fill:#51cf66 style E fill:#51cf66 style G fill:#51cf66
classDiagram class ClaudeAgentAdapter { <<重构>> -_clients: Dict[str, ClaudeSDKClient] -_options: ClaudeAgentOptions +get_or_create_client(chat_id): ClaudeSDKClient +send_message(chat_id, message): AsyncGenerator +close_client(chat_id) -_build_options(): ClaudeAgentOptions } class ClaudeSDKClient { <<SDK 内置>> +query(prompt): void +receive_response(): AsyncGenerator +close(): void -automatic_compaction: 内置 -session_management: 内置 } class ClaudeAgentOptions { <<SDK 内置>> +system_prompt: str +max_turns: int +allowed_tools: List[str] +permission_mode: str +cwd: Path +mcp_servers: Dict +hooks: Dict } class SessionPersistence { <<新增>> +save_session(chat_id, client_state) +load_session(chat_id): Optional[state] +clear_session(chat_id) } ClaudeAgentAdapter --> ClaudeSDKClient : manages ClaudeAgentAdapter --> ClaudeAgentOptions : configures ClaudeAgentAdapter --> SessionPersistence : uses
requirements.txt 修改:
- claude-code-sdk>=0.0.20 + claude-agent-sdk>=0.1.13
agent.py 修改:
# 旧代码 from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions, query # 新代码 from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, query from claude_agent_sdk.types import AssistantMessage, SystemMessage, ResultMessage
class ClaudeAgentAdapter: """重构后的 Claude Agent 适配器 - 基于 claude-agent-sdk""" def __init__(self, config: Dict, persistence: PersistenceManager): self.config = config self.persistence = persistence self._clients: Dict[str, ClaudeSDKClient] = {} self._claude_md_content = self._load_claude_md() # 构建基础配置 self._base_options = ClaudeAgentOptions( system_prompt=self._claude_md_content, max_turns=100, # SDK 会自动 compact permission_mode='bypassPermissions', allowed_tools=['Read', 'Write', 'Bash'], )
async def get_or_create_client(self, chat_id: str) -> ClaudeSDKClient: """获取或创建指定聊天的 SDK 客户端""" chat_key = str(chat_id) if chat_key not in self._clients: # 尝试恢复 session saved_state = self.persistence.load_session_state(chat_key) options = ClaudeAgentOptions( system_prompt=self._build_system_prompt(chat_id), max_turns=100, permission_mode='bypassPermissions', # 如果有保存的 session,可以通过 resume 恢复 resume=saved_state.get('session_id') if saved_state else None, ) client = ClaudeSDKClient(options=options) await client.__aenter__() # 初始化客户端 self._clients[chat_key] = client logger.info(f"为聊天 {chat_id} 创建新的 SDK 客户端") return self._clients[chat_key]
async def create_streaming_response( self, chat_id: Union[int, str], message: str, user_info: Optional[Dict] = None ) -> AsyncGenerator[str, None]: """ 创建流式响应 - 使用 claude-agent-sdk SDK 自动处理: - 上下文管理 - Automatic Compaction (上下文压缩) - Session 管理 """ chat_key = str(chat_id) try: client = await self.get_or_create_client(chat_key) # 构建用户消息(包含用户信息) formatted_message = self._format_user_message(message, user_info) # 发送消息 await client.query(formatted_message) # 接收流式响应 full_response = "" async for msg in client.receive_response(): if isinstance(msg, AssistantMessage): for block in msg.content: if hasattr(block, 'text'): chunk = block.text full_response += chunk yield chunk # 保存 session 状态用于持久化 await self._save_session_state(chat_key, client) logger.info(f"聊天 {chat_id} 响应完成,长度: {len(full_response)}") except Exception as e: logger.error(f"流式响应生成错误: {e}") yield f"抱歉,处理请求时出现错误: {str(e)}"
⚠️ 重要修正:Session 数据存储在本地 CLI 目录,而非服务端。
claude-agent-sdk 的 session 机制:
~/.claude/sessions/ 目录resume 参数从本地 session 文件恢复对话| 部署场景 | Session 文件位置 | 风险 |
|---|---|---|
| 本地开发 | ~/.claude/sessions/ | 低 - 文件持久存在 |
| Docker 容器 | 容器内 /root/.claude/sessions/ | 高 - 容器重启丢失 |
| Kubernetes | Pod 内临时存储 | 高 - Pod 重建丢失 |
| 服务器部署 | 服务器 ~/.claude/sessions/ | 中 - 需确保目录持久化 |
由于 session 文件可能丢失,建议采用双重持久化策略:
class SessionPersistence: """Session 持久化 - 双重保险策略""" def __init__(self, storage_dir: Path, claude_sessions_dir: Path = None): self.storage_dir = storage_dir self.sessions_file = storage_dir / "sessions.json" # CLI session 目录(默认 ~/.claude/sessions/) self.claude_sessions_dir = claude_sessions_dir or Path.home() / ".claude" / "sessions" def save_session_id(self, chat_id: str, session_id: str) -> bool: """保存 session_id 映射""" try: sessions = self._load_sessions() sessions[chat_id] = { "session_id": session_id, "last_updated": int(time.time()) } return self._save_sessions(sessions) except Exception as e: logger.error(f"保存 session_id 失败: {e}") return False def load_session_id(self, chat_id: str) -> Optional[str]: """加载 session_id(带有效性检查)""" sessions = self._load_sessions() data = sessions.get(chat_id) if not data: return None session_id = data.get("session_id") # 检查 CLI session 文件是否存在 if session_id and not self._session_file_exists(session_id): logger.warning(f"Session 文件不存在: {session_id},将创建新 session") return None return session_id def _session_file_exists(self, session_id: str) -> bool: """检查 CLI session 文件是否存在""" # Session 文件通常在 ~/.claude/sessions/{session_id}/ session_path = self.claude_sessions_dir / session_id return session_path.exists() def clear_session(self, chat_id: str) -> bool: """清除 session""" try: sessions = self._load_sessions() if chat_id in sessions: del sessions[chat_id] return self._save_sessions(sessions) return True except Exception as e: logger.error(f"清除 session 失败: {e}") return False
async def get_or_create_client(self, chat_id: str) -> ClaudeSDKClient: """获取或创建 SDK 客户端(带 session 有效性检查)""" chat_key = str(chat_id) if chat_key not in self._clients: # 尝试恢复 session(会检查文件是否存在) session_id = self.persistence.load_session_id(chat_key) options = ClaudeAgentOptions( system_prompt=self._claude_md_content, # 如果 session 文件存在才尝试恢复 resume=session_id, ) async with ClaudeSDKClient(options=options) as client: self._clients[chat_key] = client if session_id: logger.info(f"聊天 {chat_id} 恢复 session: {session_id}") else: logger.info(f"聊天 {chat_id} 创建新 session") return self._clients[chat_key]
# Docker Compose 示例 - 挂载 session 目录 services: telegram-bot: image: your-bot-image volumes: - ./data/claude-sessions:/root/.claude/sessions # 持久化 session - ./data/storage:/app/data/storage # 持久化 session_id 映射
| 文件 | 位置 | 内容 | 说明 |
|---|---|---|---|
sessions.json | data/storage/*/ | chat_id → session_id 映射 | 我们管理 |
| Session 文件 | ~/.claude/sessions/ | 对话历史、上下文 | CLI 管理 |
conversations.json | data/storage/*/ | 废弃 | 不再需要 |
agents.json | data/storage/*/ | 废弃 | SDK 管理 |
Claude Agent SDK 的 automatic compaction 是内置功能,无需手动配置:
sequenceDiagram participant User as 用户 participant Adapter as ClaudeAgentAdapter participant SDK as ClaudeSDKClient participant Claude as Claude API User->>Adapter: 发送消息 Adapter->>SDK: client.query(message) SDK->>SDK: 检查上下文大小 alt 上下文接近限制 SDK->>Claude: 请求生成 Summary Claude-->>SDK: 返回压缩后的 Summary SDK->>SDK: 替换旧历史为 Summary Note over SDK: Automatic Compaction 完成 end SDK->>Claude: 发送请求(含压缩后上下文) Claude-->>SDK: 流式响应 SDK-->>Adapter: 转发响应 Adapter-->>User: 显示响应
关键点:
sequenceDiagram participant TG as Telegram participant Bot as TelegramBot participant Adapter as ClaudeAgentAdapter participant SDK as ClaudeSDKClient participant Persist as SessionPersistence TG->>Bot: 用户消息 Bot->>Adapter: handle_message(chat_id, text) Adapter->>Persist: load_session_state(chat_id) Persist-->>Adapter: session_state (或 None) Adapter->>SDK: get_or_create_client(chat_id, session_state) Adapter->>SDK: client.query(message) SDK->>SDK: [内部] Automatic Compaction (如需要) loop 流式响应 SDK-->>Adapter: message chunk Adapter-->>TG: 更新消息 end Adapter->>Persist: save_session_state(chat_id, client_state) Persist-->>Adapter: success
在 configs/default.toml 中更新:
# Agent SDK 配置 (替换原有 agent 配置) [agent] # 思考模式: interactive 或 yolo default_mode = "interactive" # Claude 模型配置 (SDK 默认使用最新模型) model = "claude-sonnet-4-5-20250929" # API 超时时间 api_timeout = 60 # 最大对话轮数 (SDK 会自动 compact,可以设置较大值) max_turns = 100 # 权限模式: default, acceptEdits, plan, bypassPermissions permission_mode = "bypassPermissions" # Session 持久化配置 [agent.session] # 是否启用 session 持久化 enabled = true # Session 过期时间 (秒),0 表示永不过期 expire_seconds = 86400
| 文件 | 修改类型 | 修改内容 |
|---|---|---|
requirements.txt | 修改 | claude-code-sdk → claude-agent-sdk |
src/claude_agent/core/agent.py | 重构 | 使用 ClaudeSDKClient 替代 query() |
src/claude_agent/telegram/claude_adapter.py | 重构 | 移除手动上下文管理,使用 SDK 内置功能 |
src/claude_agent/storage/persistence.py | 简化 | 只保留 session_id 存储,删除对话历史存储 |
configs/default.toml | 修改 | 更新配置项 |
以下代码将被删除(不再需要手动管理):
| 文件 | 删除内容 | 原因 |
|---|---|---|
agent.py | clean_conversation_history() | SDK 自动管理 |
agent.py | trim_conversation_history() | SDK 自动管理 |
agent.py | conversation_history 列表 | 服务端存储 |
claude_adapter.py | _build_full_prompt() 中的历史拼接 | SDK 自动管理 |
claude_adapter.py | 1MB 截断逻辑 (758-773行) | SDK 自动 compact |
claude_adapter.py | _merge_conversation_history() | SDK 内部管理 |
persistence.py | save_conversation_history() | 服务端存储 |
persistence.py | load_conversation_history() | 服务端存储 |
| 文件 | 状态 | 说明 |
|---|---|---|
data/storage/*/conversations.json | 废弃 | 对话历史由服务端存储 |
data/storage/*/agents.json | 废弃 | Agent 状态由 SDK 管理 |
data/storage/*/sessions.json | 新增 | 只存 session_id 映射 |
| 风险 | 可能性 | 影响 | 缓解措施 |
|---|---|---|---|
| SDK 升级导致 API 变化 | 中 | 中 | 锁定版本号,逐步升级 |
| Session 文件丢失 | 中 | 高 | 挂载持久化卷,检查文件存在性 |
| Docker/K8s 重启丢失 | 高 | 高 | 必须挂载 ~/.claude/sessions/ 目录 |
| Session 恢复失败 | 中 | 低 | 失败时创建新 session,优雅降级 |
| Compaction 质量不稳定 | 低 | 中 | SDK 官方维护,持续优化 |
| Python 版本要求提高 | 低 | 中 | 项目已使用 Python 3.10+ |
| 测试项 | 测试内容 | 预期结果 |
|---|---|---|
| SDK 导入 | 导入 claude-agent-sdk | 成功导入 |
| 客户端创建 | 创建 ClaudeSDKClient | 成功创建 |
| 基本对话 | 发送消息并接收响应 | 正常响应 |
| Session 持久化 | 保存和加载 session | 数据一致 |
| 测试场景 | 步骤 | 预期结果 |
|---|---|---|
| 长对话测试 | 连续发送 100+ 条消息 | Bot 持续正常响应 (SDK 自动 compact) |
| 重启恢复测试 | 对话后重启 Bot | Session 正确恢复 |
| 并发测试 | 多个群组同时对话 | 上下文不串台 |
| Compaction 测试 | 触发自动压缩 | 对话继续,信息保留 |
| 核心组件/配置 | 实现状态 | 文件路径 | 核心功能 | 关键代码位置 |
|---|---|---|---|---|
| 依赖更新 | ||||
requirements.txt | 待修改 | requirements.txt | SDK 依赖 | claude-agent-sdk>=0.1.13 |
| 核心组件 | ||||
ClaudeAgentAdapter | 待重构 | src/claude_agent/telegram/claude_adapter.py | 使用 ClaudeSDKClient | get_or_create_client() |
AgentCore | 待重构 | src/claude_agent/core/agent.py | 简化,移除手动管理 | 移除 conversation_history |
SessionPersistence | 待简化 | src/claude_agent/storage/persistence.py | 只存 session_id | save_session_id() |
| 配置文件 | ||||
default.toml | 待修改 | configs/default.toml | 更新 SDK 配置 | [agent] 节 |
| 待删除代码 | ||||
| 手动历史管理 | 待删除 | agent.py | conversation_history 列表 | N/A |
| 手动截断 | 待删除 | claude_adapter.py | 1MB 截断逻辑 | N/A |
| 历史合并 | 待删除 | claude_adapter.py | _merge_conversation_history() | N/A |
| 对话存储 | 待删除 | persistence.py | save/load_conversation_history() | N/A |
| 待废弃文件 | ||||
conversations.json | 废弃 | data/storage/*/ | 对话历史 → 服务端存储 | N/A |
agents.json | 废弃 | data/storage/*/ | Agent 状态 → SDK 管理 | N/A |
requirements.txtclaude-agent-sdkAgentCore - 使用 ClaudeSDKClientClaudeAgentAdapter - 移除手动上下文管理SessionPersistenceclaude-agent-sdkClaudeSDKClient 正确初始化和管理| 对比项 | V1.0 手动管理 | V2.2 SDK 内置 |
|---|---|---|
| SDK | claude-code-sdk (废弃) | claude-agent-sdk (推荐) |
| 上下文管理 | 手动实现 TokenEstimator, ContextBuilder | SDK 内置 Automatic Compaction |
| 压缩触发 | 需自己实现四重条件检测 | SDK 自动检测和触发 |
| Session | 无 | 内置 Session 管理 |
| 对话历史存储 | 本地 conversations.json | 本地 CLI 目录 (~/.claude/sessions/) |
| Session 目录可配置 | N/A | 不可配置 (CLI 硬编码) |
| 本地持久化 | 存储完整对话历史 | 只存 session_id 映射 |
| 代码量 | 新增 ~500 行 | 删除 ~300 行,简化架构 |
| 维护成本 | 高(需要跟进 Claude 变化) | 低(官方维护) |
| 可靠性 | 依赖自己实现质量 | 官方测试保证 |
| 部署考虑 | 无特殊要求 | 需挂载 session 目录 (Docker/K8s) |
结论: V2.2 方案更简单、更可靠、维护成本更低。但需注意 Session 存储在本地 CLI 目录,Docker/K8s 部署时必须挂载持久化卷。