feat(aibox): add DeepSeek reasoning mode support with tool calling on deepseek-v4-flash
- Add api.reasoning_enabled config (default false) to enable thinking mode - Uses deepseek-v4-flash native thinking capability (thinking param), supports tool calls while reasoning - Real-time reasoning content streaming via reasoning_start/reasoning/reasoning_end WS messages - Frontend shows reasoning in gray italic area, auto-folds long reasoning (>2000 chars) - Dynamic reasoning detection: shows reasoning even if model returns it without explicit config - Increase default API timeout to 120s for longer reasoning responses - Remove duplicate _add_assistant_message_with_reasoning method - Update docs: config example, message types table, v2.5 changelog
This commit is contained in:
1 parent
81ae84851d
commit
b79044d631
5 files changed
+131
-39
No files matched your search
+4
-2
@@ -60,7 +60,9 @@ BotOps/
|
||||
- 发送:`{"type":"message","content":...}` / `{"type":"command","command":"/..."}` / `{"type":"interrupt",...}`
|
||||
- 接收:
|
||||
- `chunk`(**增量片段**):服务端 `chunk_buffer` 发送后即清零,前端直接 `append_ai` 拼接,为内容权威来源
|
||||
- `reasoning_start`(**思考开始**):创建灰色斜体思考区域,实时展示 AI 思考过程
|
||||
- `reasoning`(**思考流**):思考内容增量片段,追加到思考区域(超长自动折叠显示最后 2000 字符)
|
||||
- `reasoning_end`(**思考结束**):思考结束,切换到正式回答
|
||||
- `complete`(**收尾信号**):chunk 流式已完整渲染,complete 仅用于收尾;仅在完全丢帧(未收到任何 chunk)时用 full_text 补建消息,**不再做全文替换对齐**(避免服务端递归调用工具后 complete 只含最后一轮内容导致重复渲染)
|
||||
- `line`(过程行):🔧 工具调用提示、工具输出、💬 助手前缀,路由到任务侧栏;助手前缀/思考提示在聊天区已由 AI 消息框承载,不重复展示
|
||||
- `reasoning`(思考流):仅服务端 debug_level≥3 发送,记入任务侧栏
|
||||
- `line`(过程行):🔧 工具调用提示、工具输出,路由到任务侧栏;助手前缀/思考提示在聊天区已由对应区域承载,不重复展示
|
||||
- `dialogue_ended` / `dialogue_reset` / `task_added` / `task_process` / `task_status` / `task_error` / `interrupt_ack` / `error` / `info` / `command_result` / `connection_ack` / `notification` / `pong`
|
||||
+20
-7
@@ -58,6 +58,13 @@ class BotOpsApp(App):
|
||||
color: yellow;
|
||||
padding: 0 1;
|
||||
}
|
||||
.msg-reasoning {
|
||||
color: $text-muted;
|
||||
background: $surface;
|
||||
padding: 0 1;
|
||||
margin: 0 1;
|
||||
text-style: italic;
|
||||
}
|
||||
#status {
|
||||
height: 1;
|
||||
}
|
||||
@@ -163,21 +170,27 @@ class BotOpsApp(App):
|
||||
if t == "chunk":
|
||||
# 服务端 chunk 为增量片段(发送后即清零),直接拼接
|
||||
await self.chat.append_ai(content)
|
||||
elif t == "reasoning_start":
|
||||
# 开始深度思考,创建思考区域
|
||||
await self.chat.start_reasoning(content or "🤔 深度思考中...")
|
||||
elif t == "reasoning":
|
||||
# 思考内容增量
|
||||
stripped = content
|
||||
if stripped:
|
||||
await self.chat.append_reasoning(stripped)
|
||||
elif t == "reasoning_end":
|
||||
# 思考结束
|
||||
await self.chat.end_reasoning()
|
||||
elif t == "line":
|
||||
# 过程行(服务端 output_callback 原样透传):工具调用提示 / 工具输出 / 前缀
|
||||
stripped = content.strip()
|
||||
if not stripped:
|
||||
pass
|
||||
elif stripped.startswith(("💬 助手", "💬 Assistant", "🤔 思考中", "🤔 Thinking")):
|
||||
pass # 助手前缀 / 思考提示:聊天区已有 AI 消息框,无需展示
|
||||
elif stripped.startswith(("💬 助手", "💬 Assistant", "🤔 思考中", "🤔 Thinking", "🤔 深度思考中")):
|
||||
pass # 助手前缀 / 思考提示:聊天区已有对应区域,无需展示
|
||||
else:
|
||||
# 🔧 调用工具 / 工具输出 / 🤔 分析工具结果 等 → 任务侧栏
|
||||
self.tasks.write(stripped)
|
||||
elif t == "reasoning":
|
||||
# 思考流(服务端 debug_level>=3 才发送),记入任务侧栏
|
||||
stripped = content.strip()
|
||||
if stripped:
|
||||
self.tasks.write(f"💭 {stripped[-200:]}")
|
||||
elif t == "complete":
|
||||
# complete 携带完整全文,用于对齐已显示内容
|
||||
await self.chat.finish_ai(content)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""聊天视图:用户/AI/系统消息 + AI 回复流式 Markdown 渲染。
|
||||
|
||||
服务端协议(aibox/aibox.py output_callback 原样透传 msg_type):
|
||||
- chunk 增量片段(服务端 chunk_buffer 发送后即清零),直接 append
|
||||
- complete 收尾信号:chunk 流式是内容权威来源,complete 仅用于收尾;
|
||||
仅当流式完全丢帧(未建 widget 且无内容)时,用 full_text 补建
|
||||
- chunk 增量片段(正式回答内容),直接 append 到 AI 消息
|
||||
- reasoning_start 思考开始信号,创建思考区域
|
||||
- reasoning 思考内容增量片段,追加到思考区域
|
||||
- reasoning_end 思考结束信号
|
||||
- complete 收尾信号:chunk 流式是内容权威来源,complete 仅用于收尾;
|
||||
仅当流式完全丢帧(未建 widget 且无内容)时,用 full_text 补建
|
||||
"""
|
||||
from textual.containers import VerticalScroll
|
||||
from textual.widgets import Markdown, Static
|
||||
@@ -20,7 +23,9 @@ class ChatView(VerticalScroll):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._ai_widget: Markdown | None = None
|
||||
self._reasoning_widget: Static | None = None
|
||||
self._ai_text = ""
|
||||
self._reasoning_text = ""
|
||||
self._ai_finalized = True # 初始无活跃 AI 消息
|
||||
|
||||
@property
|
||||
@@ -49,6 +54,39 @@ class ChatView(VerticalScroll):
|
||||
await self.mount(self._ai_widget)
|
||||
self._anchor_bottom()
|
||||
|
||||
async def start_reasoning(self, hint: str = "🤔 思考中..."):
|
||||
"""开始展示思考过程区域。"""
|
||||
if self._reasoning_widget is not None:
|
||||
return
|
||||
self._reasoning_text = ""
|
||||
self._reasoning_widget = Static(hint + "\n", classes="msg-reasoning", markup=False)
|
||||
await self.mount(self._reasoning_widget)
|
||||
self._anchor_bottom()
|
||||
|
||||
async def append_reasoning(self, delta: str):
|
||||
"""追加思考内容增量。"""
|
||||
if not delta:
|
||||
return
|
||||
if self._reasoning_widget is None:
|
||||
await self.start_reasoning()
|
||||
self._reasoning_text += delta
|
||||
# 限制思考内容显示长度(最多显示最后 2000 字符,避免思考过长占满屏幕)
|
||||
display = self._reasoning_text
|
||||
if len(display) > 2000:
|
||||
display = "...(前略)\n" + display[-2000:]
|
||||
self._reasoning_widget.update(f"💭 思考中...\n{display}")
|
||||
self.call_after_refresh(self.scroll_end, animate=False)
|
||||
|
||||
async def end_reasoning(self):
|
||||
"""结束思考区域,更新提示文字。"""
|
||||
if self._reasoning_widget is not None and self._reasoning_text:
|
||||
# 思考结束后保持展示,但更新标签
|
||||
display = self._reasoning_text
|
||||
if len(display) > 2000:
|
||||
display = "...(前略)\n" + display[-2000:]
|
||||
self._reasoning_widget.update(f"💭 思考过程({len(self._reasoning_text)}字符)\n{display}\n{'─' * 30}")
|
||||
# 思考区域保留显示,不删除
|
||||
|
||||
async def append_ai(self, delta: str):
|
||||
"""追加增量片段(服务端 chunk 为增量语义,直接拼接)。"""
|
||||
if not delta:
|
||||
@@ -97,12 +135,16 @@ class ChatView(VerticalScroll):
|
||||
self._anchor_bottom()
|
||||
|
||||
self._ai_text = ""
|
||||
self._reasoning_widget = None
|
||||
self._reasoning_text = ""
|
||||
self._ai_finalized = True
|
||||
self.call_after_refresh(self.scroll_end, animate=False)
|
||||
|
||||
async def reset(self):
|
||||
"""清空聊天区(/clear)。"""
|
||||
self._ai_widget = None
|
||||
self._reasoning_widget = None
|
||||
self._ai_text = ""
|
||||
self._reasoning_text = ""
|
||||
self._ai_finalized = True
|
||||
await self.remove_children()
|
||||
+19
-4
@@ -215,9 +215,11 @@ const ws = new WebSocket('ws://localhost:8765');
|
||||
| 类型 | 说明 |
|
||||
|-----|------|
|
||||
| `chunk` | AI 响应的**增量片段**(流式输出,chunk_buffer 发送后即清零,为内容权威来源) |
|
||||
| `reasoning_start` | AI 开始思考信号(思考模式,创建思考区域展示) |
|
||||
| `reasoning` | AI 思考过程增量片段(流式输出,灰色/斜体样式) |
|
||||
| `reasoning_end` | AI 思考结束信号(切换到正式回答) |
|
||||
| `complete` | AI 响应**收尾信号**(chunk 流式是内容权威来源,complete 仅用于收尾;完全丢帧时可用 full_text 补建) |
|
||||
| `line` | 过程行(🔧 工具调用提示、工具输出、💬 助手前缀、思考提示等,供任务侧栏/终端显示) |
|
||||
| `reasoning` | AI 思考过程(思考模式,仅 debug_level≥3 发送) |
|
||||
| `line` | 过程行(🔧 工具调用提示、工具输出、💬 助手前缀等,供任务侧栏/终端显示) |
|
||||
| `error` | 错误信息(API 请求失败、解析失败等;服务端自动回滚本轮消息防止对话历史被污染) |
|
||||
| `command_result` | 命令执行结果 |
|
||||
| `task_status` | 任务状态更新(executing/complete/failed) |
|
||||
@@ -427,8 +429,9 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
|
||||
"api": {
|
||||
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
|
||||
"deepseek_model": "deepseek-v4-flash",
|
||||
"reasoning_enabled": false,
|
||||
"api_key_env": "DEEPSEEK_API_KEY",
|
||||
"timeout": 30,
|
||||
"timeout": 120,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"context": {
|
||||
@@ -492,7 +495,9 @@ AI 可以通过 `update_soul` 工具动态更新 `99_SOUL.md`,通过 `update_i
|
||||
}
|
||||
```
|
||||
|
||||
> 默认模型为 `deepseek-v4-flash`(通过 `api.deepseek_model` 修改)。API Key 从环境变量 `DEEPSEEK_API_KEY` 读取(亦支持 `~/.cert/deepseek.key` 文件)。
|
||||
> 默认模型为 `deepseek-v4-flash`(通过 `api.deepseek_model` 修改)。设置 `api.reasoning_enabled: true` 可启用思考模式,此时会在请求中添加 `thinking: {type: "enabled"}` 参数,模型会先进行思考再回答,实时展示思考过程(灰色斜体区域),回答质量更高且**支持工具调用**(文件操作、命令执行、记忆搜索等工具均可在思考模式下正常使用)。
|
||||
>
|
||||
> API Key 从环境变量 `DEEPSEEK_API_KEY` 读取(亦支持 `~/.cert/deepseek.key` 文件)。
|
||||
|
||||
## 🛠️ 命令行参数
|
||||
|
||||
@@ -701,6 +706,16 @@ elif command == "/mycommand":
|
||||
|
||||
## 📄 更新日志
|
||||
|
||||
### v2.5
|
||||
- ✅ **新增 DeepSeek 思考模式支持**(Reasoning Mode):
|
||||
- 新增 `api.reasoning_enabled` 配置项(默认 `false`),启用后在请求中添加 `thinking: {type: "enabled"}` 参数
|
||||
- 使用 `deepseek-v4-flash` 模型原生思考能力,**思考模式下完整支持工具调用**(文件操作、命令执行、记忆搜索等均可正常使用)
|
||||
- 实时展示 AI 思考过程:新增 `reasoning_start`/`reasoning`/`reasoning_end` WebSocket 消息类型
|
||||
- 思考内容在前端以灰色斜体区域独立展示,超长思考自动折叠显示最后 2000 字符
|
||||
- 即使未启用思考模式,若模型返回 `reasoning_content` 也会自动实时展示
|
||||
- API 超时默认从 30s 提高到 120s,适配思考模式更长的响应时间
|
||||
- ✅ 删除重复定义的 `_add_assistant_message_with_reasoning` 方法
|
||||
|
||||
### v2.4
|
||||
- ✅ **新增专用工作目录规范**:
|
||||
- AI 主动创建的临时文件、脚本、数据文件默认写入 `~/.aibox/workspace/`,避免污染系统其他目录
|
||||
|
||||
+43
-23
@@ -1168,8 +1168,9 @@ class ConfigManager:
|
||||
"api": {
|
||||
"deepseek_api_url": "https://api.deepseek.com/chat/completions",
|
||||
"deepseek_model": "deepseek-v4-flash",
|
||||
"reasoning_enabled": false,
|
||||
"api_key_env": "DEEPSEEK_API_KEY",
|
||||
"timeout": 30,
|
||||
"timeout": 120,
|
||||
"temperature": 0.7
|
||||
},
|
||||
"context": {
|
||||
@@ -1356,7 +1357,11 @@ class Constants:
|
||||
|
||||
@property
|
||||
def DEEPSEEK_MODEL(cls):
|
||||
return cls._get_config().get("api.deepseek_model")
|
||||
return cls._get_config().get("api.deepseek_model", "deepseek-v4-flash")
|
||||
|
||||
@property
|
||||
def REASONING_ENABLED(cls):
|
||||
return cls._get_config().get("api.reasoning_enabled", False)
|
||||
|
||||
@property
|
||||
def DEFAULT_TIMEOUT(cls):
|
||||
@@ -7169,11 +7174,13 @@ class DeepSeekChat:
|
||||
"stream": True
|
||||
}
|
||||
|
||||
# 可选:添加 thinking 模式配置(如果有)
|
||||
payload["thinking"] = {
|
||||
"type": "enabled"
|
||||
}
|
||||
# 思考模式:启用 thinking 参数
|
||||
if constants.REASONING_ENABLED:
|
||||
payload["thinking"] = {
|
||||
"type": "enabled"
|
||||
}
|
||||
|
||||
# 工具调用(v4-flash 支持思考+工具调用)
|
||||
tools = self.tool_registry.get_tools_schemas()
|
||||
if tools:
|
||||
payload["tools"] = tools
|
||||
@@ -7182,10 +7189,16 @@ class DeepSeekChat:
|
||||
current_time = datetime.now().strftime("%H:%M:%S")
|
||||
if output_callback:
|
||||
output_callback("line", f"\n{i18n.get('thinking', current_time)}\n")
|
||||
output_callback("line", i18n.get('assistant_prefix'))
|
||||
if constants.REASONING_ENABLED:
|
||||
output_callback("reasoning_start", "🤔 思考中...")
|
||||
else:
|
||||
# 普通模式先显示助手前缀
|
||||
output_callback("line", i18n.get('assistant_prefix'))
|
||||
|
||||
full_response = ""
|
||||
full_reasoning = "" # 存储思考内容
|
||||
reasoning_started = False
|
||||
reasoning_ended = False
|
||||
tool_calls_buffer = []
|
||||
current_tool_calls = {}
|
||||
|
||||
@@ -7236,10 +7249,25 @@ class DeepSeekChat:
|
||||
if 'reasoning_content' in delta and delta['reasoning_content']:
|
||||
reasoning = delta['reasoning_content']
|
||||
full_reasoning += reasoning
|
||||
# 可选:将思考内容也发送给前端(可以单独处理)
|
||||
if output_callback and self.debug_level >= 3:
|
||||
|
||||
# 发送思考开始信号(仅第一次,且未提前发送过)
|
||||
if not reasoning_started and output_callback:
|
||||
if not constants.REASONING_ENABLED:
|
||||
# 非显式思考模式但模型返回了思考内容,动态发送开始信号
|
||||
output_callback("reasoning_start", "🤔 思考中...")
|
||||
reasoning_started = True
|
||||
|
||||
# 实时发送思考内容给前端(无需debug_level限制)
|
||||
if output_callback:
|
||||
output_callback("reasoning", reasoning)
|
||||
|
||||
# 思考结束:当开始收到content且之前有reasoning时,发送结束信号
|
||||
if 'content' in delta and delta['content'] and reasoning_started and not reasoning_ended:
|
||||
reasoning_ended = True
|
||||
if output_callback:
|
||||
output_callback("reasoning_end", "")
|
||||
output_callback("line", i18n.get('assistant_prefix'))
|
||||
|
||||
# 处理正常内容
|
||||
if 'content' in delta and delta['content']:
|
||||
content = delta['content']
|
||||
@@ -7290,7 +7318,12 @@ class DeepSeekChat:
|
||||
# 处理工具调用
|
||||
if current_tool_calls:
|
||||
tool_calls_buffer = list(current_tool_calls.values())
|
||||
if output_callback:
|
||||
|
||||
# 如果之前在思考,先结束思考状态
|
||||
if reasoning_started and not reasoning_ended and output_callback:
|
||||
output_callback("reasoning_end", "")
|
||||
output_callback("line", i18n.get('calling_tools') + "\n")
|
||||
elif output_callback:
|
||||
output_callback("line", i18n.get('calling_tools') + "\n")
|
||||
|
||||
# 【关键修复1】先添加助手消息,包含 tool_calls
|
||||
@@ -7388,19 +7421,6 @@ class DeepSeekChat:
|
||||
except Exception as e:
|
||||
self.logger.error(f"消息回滚失败: {e}")
|
||||
|
||||
|
||||
def _add_assistant_message_with_reasoning(self, content: str, reasoning_content: str = None, tool_calls: List = None):
|
||||
"""添加助手消息,包含 reasoning_content"""
|
||||
msg = {"role": "assistant"}
|
||||
if content:
|
||||
msg["content"] = content
|
||||
if reasoning_content:
|
||||
msg["reasoning_content"] = reasoning_content
|
||||
if tool_calls:
|
||||
msg["tool_calls"] = tool_calls
|
||||
msg["content"] = None
|
||||
self.messages.append(msg)
|
||||
|
||||
def reset_conversation(self, reload_prompts: bool = True):
|
||||
"""
|
||||
重置对话状态 - 清除所有对话记忆,可选择是否重新加载系统提示词
|
||||
|
||||
Reference in new issue
Block a user