| 项目 | 内容 |
|---|---|
| 文档标题 | Telegram Bot 上下文管理重构技术设计 |
| 版本 | V1.0 |
| 作者 | Claude Agent |
| 创建日期 | 2025-12-10 |
| 状态 | 待评审 |
| 版本 | 日期 | 作者 | 变更内容 |
|---|---|---|---|
| V1.0 | 2025-12-10 | Claude Agent | 初始版本 |
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 导致历史记录指数级膨胀: - 第1条: CLAUDE.md + 消息1 - 第2条: CLAUDE.md + (CLAUDE.md + 消息1) + 消息2 - 第N条: 指数级增长
位置: src/claude_agent/telegram/claude_adapter.py 第758-773行
if len(full_prompt) > 1000000: # 1MB 才触发! full_prompt = full_prompt[:20000] # 字符截断,破坏上下文结构
经研究确认,Claude Code SDK 的 query() 函数是无状态的:
| 指标 | 目标值 |
|---|---|
| 最大支持对话轮数 | 50+ 轮(当前配置) |
| 单条消息响应时间 | < 5秒 |
| 上下文 Token 利用率 | > 80% |
| 内存占用(每会话) | < 1MB |
flowchart TD subgraph 当前流程-有问题 A[用户消息] --> B[_build_full_prompt] B --> C["拼接: CLAUDE.md + 全部历史 + 消息"] C --> D[传递给 AgentCore] D --> E["存入 conversation_history<br/>(包含完整拼接内容)"] E --> F[query SDK] F --> G[响应也存入历史] G --> H["下次请求时历史更大<br/>恶性循环"] end style C fill:#ff6b6b style E fill:#ff6b6b style H fill:#ff6b6b
flowchart TD subgraph 新流程-修复后 A[用户消息] --> B[ContextBuilder] B --> C["分离: 系统提示 / 历史 / 当前消息"] C --> D[TokenCounter] D --> E{超出限制?} E -->|是| F[智能截断-保留最近] E -->|否| G[构建最终 Prompt] F --> G G --> H[query SDK] H --> I["仅存储原始消息<br/>(不含系统提示)"] I --> J[持久化存储] end style C fill:#51cf66 style F fill:#51cf66 style I fill:#51cf66
classDiagram class ContextBuilder { <<新增>> +system_prompt: str +max_tokens: int +build_prompt(history, message): str +estimate_tokens(text): int -_truncate_history(history, budget): List } class MessageStore { <<重构>> +add_message(role, content) +get_recent(limit): List +get_token_count(): int -_messages: deque -_max_messages: int } class ClaudeAgentAdapter { <<修改>> -context_builder: ContextBuilder -message_store: Dict[str, MessageStore] +create_streaming_response(chat_id, message) -_save_raw_message(chat_id, role, content) } class AgentCore { <<修改>> +conversation_history: List +add_message(role, content) +get_context_for_prompt(): List -_ensure_history_clean() } ContextBuilder --> MessageStore : uses ClaudeAgentAdapter --> ContextBuilder : uses ClaudeAgentAdapter --> AgentCore : manages
目的: 准确估算文本的 Token 数量,用于上下文管理
实现方案: 使用简化的中英文混合估算(避免引入 tiktoken 依赖)
flowchart LR A[输入文本] --> B{语言检测} B -->|英文为主| C["字符数 / 4"] B -->|中文为主| D["字符数 / 1.5"] B -->|混合| E["加权计算"] C --> F[Token 估算值] D --> F E --> F
设计接口:
classDiagram class TokenEstimator { <<utility>> +estimate(text: str): int +estimate_messages(messages: List): int +CHARS_PER_TOKEN_EN: float = 4.0 +CHARS_PER_TOKEN_ZH: float = 1.5 +MAX_CONTEXT_TOKENS: int = 150000 }
目的: 分离系统提示、历史消息、当前消息的组装逻辑
核心原则:
sequenceDiagram participant User as 用户消息 participant CB as ContextBuilder participant TE as TokenEstimator participant Store as MessageStore participant SDK as Claude SDK User->>CB: build_prompt(message) CB->>Store: get_recent_messages() Store-->>CB: [msg1, msg2, ..., msgN] CB->>TE: estimate_tokens(system + history + message) TE-->>CB: total_tokens alt tokens > MAX_LIMIT CB->>CB: truncate_history(keep_recent) CB->>TE: re-estimate end CB->>SDK: query(final_prompt) SDK-->>CB: response CB->>Store: add_message("user", original_message) CB->>Store: add_message("assistant", response)
当前问题: conversation_history 存储了拼接后的完整 prompt
修复方案: 只存储原始消息内容
flowchart TD subgraph 消息存储结构 A[MessageStore] A --> B["messages: deque(maxlen=100)"] B --> C["{'role': 'user', 'content': '原始消息', 'timestamp': 123}"] B --> D["{'role': 'assistant', 'content': 'AI回复', 'timestamp': 124}"] end subgraph 禁止存储 E["❌ 系统提示"] F["❌ 拼接后的历史"] G["❌ full_prompt"] end
在 configs/default.toml 中新增上下文管理配置:
# 上下文管理配置 [context] # 最大上下文 Token 数 (预留 50K 给响应) max_context_tokens = 150000 # 系统提示最大 Token 数 max_system_prompt_tokens = 10000 # 每条消息最大 Token 数 (超出则截断) max_message_tokens = 5000 # 历史消息最小保留数量 (即使超出 token 也保留) min_history_messages = 5 # Token 估算方式: "simple" | "tiktoken" token_estimation = "simple"
| 文件 | 修改类型 | 修改内容 |
|---|---|---|
src/claude_agent/utils/token_estimator.py | 新增 | Token 估算工具类 |
src/claude_agent/core/context_builder.py | 新增 | 上下文构建器 |
src/claude_agent/core/agent.py | 修改 | 移除污染的历史存储逻辑 |
src/claude_agent/telegram/claude_adapter.py | 修改 | 重构 _build_full_prompt,使用新组件 |
configs/default.toml | 修改 | 新增 [context] 配置节 |
agent.py 修改移除: 第83行的污染存储
# 当前代码 (有问题) async def process_user_input(self, user_input: str) -> str: self.conversation_history.append({"role": "user", "content": user_input}) # ❌ user_input 是完整 prompt # 修改后 async def process_user_input(self, user_input: str) -> str: # 不在这里存储,由 adapter 层控制 pass
新增: 清洁的消息添加方法
def add_raw_message(self, role: str, content: str, max_length: int = 5000): """添加原始消息(自动截断过长内容)""" if len(content) > max_length: content = content[:max_length] + "...[截断]" self.conversation_history.append({ "role": role, "content": content, "timestamp": int(time.time()) }) self._trim_if_needed()
claude_adapter.py 修改重构: _build_full_prompt 方法
# 当前代码 (有问题) def _build_full_prompt(self, user_context, message, conversation_history): prompt_parts = [] prompt_parts.append(f"系统提示:\n{self._claude_md_content}") # ❌ 每次都加 prompt_parts.append(f"群聊历史对话:\n...") # ❌ 包含污染的历史 return "\n\n".join(prompt_parts) # 修改后 def _build_full_prompt(self, user_context: str, message: str, conversation_history: List[Dict]) -> str: """构建提示词(使用 ContextBuilder)""" return self._context_builder.build( system_prompt=self._claude_md_content, history=conversation_history, user_context=user_context, current_message=message )
新增: 响应后只存储原始消息
async def _handle_response(self, chat_id: str, user_message: str, response: str): """处理响应后的存储""" agent = self._agents.get(chat_id) if agent: # 只存储原始消息,不存储 full_prompt agent.add_raw_message("user", user_message) agent.add_raw_message("assistant", response) await self._save_agent_state(chat_id, agent)
sequenceDiagram participant TG as Telegram participant Bot as TelegramBot participant Adapter as ClaudeAgentAdapter participant CB as ContextBuilder participant Agent as AgentCore participant SDK as Claude SDK participant Store as PersistenceManager TG->>Bot: 用户消息 Bot->>Adapter: handle_message(chat_id, text) Adapter->>Adapter: _get_or_create_agent(chat_id) Adapter->>Agent: get_conversation_history() Agent-->>Adapter: [原始历史消息] Adapter->>CB: build(system, history, message) CB->>CB: estimate_tokens() CB->>CB: truncate_if_needed() CB-->>Adapter: final_prompt Adapter->>SDK: query(final_prompt) SDK-->>Adapter: response_stream loop 流式响应 Adapter->>TG: 更新消息 end Adapter->>Agent: add_raw_message("user", original_text) Adapter->>Agent: add_raw_message("assistant", response) Adapter->>Store: save_agent_state()
pie title Token 预算分配 (200K 总限制) "系统提示 (CLAUDE.md)" : 10 "用户上下文" : 2 "历史消息" : 60 "当前消息" : 8 "预留给响应" : 20
| 风险 | 可能性 | 影响 | 缓解措施 |
|---|---|---|---|
| Token 估算不准确 | 中 | 低 | 预留 20% 安全边际;支持切换到 tiktoken |
| 历史截断丢失重要上下文 | 中 | 中 | 保证最少 5 条历史;优先截断最老消息 |
| 持久化数据格式不兼容 | 低 | 高 | 添加版本号;编写迁移脚本 |
| 多平台行为不一致 | 低 | 中 | 统一使用 ContextBuilder |
| 测试项 | 测试内容 | 预期结果 |
|---|---|---|
| TokenEstimator.estimate | 中英文混合文本 | 误差 < 20% |
| ContextBuilder.build | 正常上下文构建 | 包含所有必要部分 |
| ContextBuilder.truncate | 超长历史截断 | 保留最近消息 |
| Agent.add_raw_message | 消息存储 | 只存储原始内容 |
| Agent.add_raw_message | 超长消息 | 自动截断 |
| 测试场景 | 步骤 | 预期结果 |
|---|---|---|
| 长对话测试 | 连续发送 100+ 条消息 | Bot 持续正常响应 |
| 大消息测试 | 发送 10KB+ 的长消息 | 消息被截断,Bot 正常响应 |
| 重启恢复测试 | 对话后重启 Bot | 历史正确恢复,无污染 |
| 并发测试 | 多个群组同时对话 | 上下文不串台 |
# 1. 长对话压力测试 python tests/blackbox/test_long_conversation.py # 2. 上下文正确性验证 python tests/blackbox/test_context_integrity.py # 3. 持久化恢复测试 python tests/blackbox/test_persistence_recovery.py
| 核心组件/配置 | 实现状态 | 文件路径 | 核心功能 | 关键代码位置 |
|---|---|---|---|---|
| 新增组件 | ||||
TokenEstimator | 待实现 | src/claude_agent/utils/token_estimator.py | Token 估算 | estimate() 方法 |
ContextBuilder | 待实现 | src/claude_agent/core/context_builder.py | 上下文构建 | build() 方法 |
| 修改组件 | ||||
AgentCore | 待修改 | src/claude_agent/core/agent.py | 移除污染存储 | add_raw_message() 方法 |
ClaudeAgentAdapter | 待修改 | src/claude_agent/telegram/claude_adapter.py | 重构 prompt 构建 | _build_full_prompt() 方法 |
| 配置文件 | ||||
default.toml | 待修改 | configs/default.toml | 新增 context 配置 | [context] 节 |
| 待删除代码 | ||||
| 历史污染存储 | 待删除 | src/claude_agent/core/agent.py | 第83行 append 逻辑 | N/A |
| 粗暴截断逻辑 | 待删除 | src/claude_agent/telegram/claude_adapter.py | 第758-773行 | N/A |
TokenEstimator 类ContextBuilder 类AgentCore.add_raw_message()ClaudeAgentAdapter._build_full_prompt()conversations.json/home/bmy001/Work/aichatbot/CLAUDE.md/home/bmy001/Work/aichatbot/docs/technical/DIRECTORY_STRUCTURE.md