feat: 重构为 Agent 架构并实现工具系统

This commit is contained in:
zihanjian
2026-02-04 17:35:08 +08:00
parent 10559d06f1
commit f0339f79ac
12 changed files with 1113 additions and 582 deletions

103
REFACTOR_PLAN.md Normal file
View File

@@ -0,0 +1,103 @@
# Bubbles 改造计划
## 核心问题
Bubbles 是个单次意图分类器,不是 Agent。AI Router 做一次 `chat/function` 分类就结束了,无法多步推理、自主决策。这是"死板"的根源。
---
## 一、Agent 循环(最高优先级)
`processMsg` 的单次分类改成工具调用循环。AI 自己决定调什么工具、调几次、什么时候停。
**改动范围:** `robot.py` 的消息处理主流程,`commands/ai_router.py` 的路由逻辑
**目标状态:**
```
消息进入 → Agent 循环开始
→ LLM 返回工具调用 → 执行工具 → 结果喂回 LLM → 继续推理
→ LLM 返回纯文本 → 循环结束,发送回复
→ 达到最大步数 → 强制结束
```
**必须包含的防护:**
- 最大步数限制(防无限循环)
- 死循环检测:同一工具连续 3 次相同输入时中断
- 单步超时
---
## 二、工具标准化
定义统一的 Tool 接口,把现有功能改写成标准工具,让 Agent 循环能调用。
**改动范围:** 新建 `tools/` 目录,重构 `commands/ai_functions.py``commands/reminder_router.py`
**Tool 接口定义:**
```python
class Tool:
name: str # 工具唯一标识
description: str # 给 LLM 看的功能描述
parameters: dict # JSON Schema 参数定义
async def execute(self, params: dict, ctx: MessageContext) -> str:
"""执行工具,返回文本结果"""
```
**需要改写的现有功能:**
- `reminder_hub``reminder_create` / `reminder_list` / `reminder_delete`(拆开,消灭二级路由)
- `perplexity_search``web_search`
- `handle_chitchat` 不再是工具,而是 Agent 循环的默认文本输出路径
**工具描述走 LLM 原生的 function calling / tool_use 协议**,不再拼进提示词字符串。
---
## 三、模型 Fallback
当前模型挂了就挂了。必须加 fallback 链。
**改动范围:** `robot.py` 的模型调用层,各 `ai_providers/` 适配器
**目标状态:**
```yaml
# config.yaml
models:
default:
primary: deepseek
fallbacks: [chatgpt, kimi]
```
**必须实现:**
- 区分可重试错误429 限流、超时、服务端 500和不可重试错误401 密钥无效)
- 可重试错误:指数退避重试(初始 2s最大 30s
- 不可重试或重试耗尽:切下一个 fallback 模型
- 记录失败模型的冷却时间,短期内不再尝试
---
## 四、上下文压缩
当前 `max_history` 按条数硬截断,丢失早期重要信息。
**改动范围:** `robot.py` 的历史消息获取逻辑,`commands/handlers.py` 的对话构建
**目标状态:**
- 监控当前对话的 token 总量
- 接近模型上下文窗口上限时,对早期消息做摘要压缩
- 保留最近 N 轮完整对话 + 早期对话的 LLM 生成摘要
- 替代现在的简单条数截断
---
## 执行状态
```
一、工具标准化 ✅ 已完成 — tools/__init__.py, tools/reminder.py, tools/web_search.py, tools/history.py
二、Agent 循环 ✅ 已完成 — 移除 AI RouterLLM 直接通过 _execute_with_tools 自主调用工具
三、模型 Fallback ✅ 已完成 — _handle_chitchat 级联候选模型ai_providers/fallback.py 重试/冷却
四、上下文压缩 ✅ 已完成 — func_summary.get_compressed_context(),字符预算代替固定条数截断
```