feat(assistant): 识别本地 LLM 服务端常见错误并给出修复指引

用户反馈:切本地 vLLM(Qwen/Qwen3-30B-A3B)后在助手里调用工具报错:
  "auto" tool choice requires --enable-auto-tool-choice and
  --tool-call-parser to be set

这是 vLLM 0.6+ 的默认安全策略 —— 必须在启动参数显式开启工具调用
才允许客户端在 body 里带 tools 字段。ClawPanel 发的请求符合 OpenAI
规范,不是我们的 bug,但用户面对这个原始报错字面上看不出是 vLLM
配置问题也不知道怎么修。

## 解决

新增 src/lib/model-error-diagnosis.js,提供 enhanceModelCallError:
保留原始错误文本 + 附加中文修复指引。目前覆盖 5 类常见本地部署错误:

1. **vLLM tool choice 限制**(本次用户实际踩的)
   - 给出 --enable-auto-tool-choice + --tool-call-parser 启动命令
   - Qwen / Mistral / Llama 各系列推荐的 parser 值
   - 建议临时切到"聊天"模式规避

2. **llama.cpp / LM Studio 旧版本不支持 tools**
3. **Ollama 模型不支持 tools**
4. **模型 ID 不存在 / 404**
5. **上下文超长 / token limit**

在 assistant.js 的 5 个错误抛出点统一接入:
- callChatCompletions(OpenAI 聊天模式)
- callResponsesAPI(新 /v1/responses 接口)
- callAnthropicMessages(Claude)
- callGeminiGenerate(Gemini)
- callAIWithTools(工具模式,就是用户踩坑的那条路径)

## 验证

- npm run build 通过
- assistant chunk 从 153.98KB → 156.24KB(gzip +0.86KB),合理
- 所有增强都走 try { parseJSON } 之后,不会影响原有错误处理路径

## 相关

- #Compat-5 系列的一部分(运行时错误诊断)
- 用户场景:vLLM + Qwen3 MoE,切换到本地模型后调工具
- 用户侧实际修复命令:
  vllm serve <model> --enable-auto-tool-choice --tool-call-parser hermes
This commit is contained in:
晴天
2026-04-20 13:02:05 +08:00
parent e39233f2c1
commit 7c63438c0e
2 changed files with 126 additions and 5 deletions

View File

@@ -12,6 +12,7 @@ import { icon, statusIcon } from '../lib/icons.js'
import { QTCOOL, PROVIDER_PRESETS, API_TYPES as SHARED_API_TYPES, fetchQtcoolModels } from '../lib/model-presets.js'
import { t } from '../lib/i18n.js'
import { getActiveEngineId } from '../lib/engine-manager.js'
import { enhanceModelCallError } from '../lib/model-error-diagnosis.js'
// ── 常量 ──
const STORAGE_KEY = 'clawpanel-assistant'
@@ -1910,7 +1911,8 @@ async function callChatCompletions(base, messages, onChunk) {
} catch {
if (errText) errMsg += `: ${errText.slice(0, 200)}`
}
throw new Error(errMsg)
// #Compat-5: 识别 vLLM/Ollama 等本地服务端的常见拒绝消息,附加修复指引
throw new Error(enhanceModelCallError(errMsg))
}
// 检测响应是否为 SSE 流式
@@ -2004,7 +2006,8 @@ async function callResponsesAPI(base, messages, onChunk) {
} catch {
if (errText) errMsg += `: ${errText.slice(0, 200)}`
}
throw new Error(errMsg)
// #Compat-5: 识别本地服务端拒绝消息,附加修复指引
throw new Error(enhanceModelCallError(errMsg))
}
await readSSEStream(resp, (json) => {
@@ -2063,7 +2066,8 @@ async function callAnthropicMessages(base, messages, onChunk) {
} catch {
if (errText) errMsg += `: ${errText.slice(0, 200)}`
}
throw new Error(errMsg)
// #Compat-5: 识别本地服务端拒绝消息,附加修复指引
throw new Error(enhanceModelCallError(errMsg))
}
_lastDebugInfo.streaming = true
@@ -2131,7 +2135,8 @@ async function callGeminiGenerate(base, messages, onChunk) {
const errText = await resp.text().catch(() => '')
let errMsg = `API 错误 ${resp.status}`
try { errMsg = JSON.parse(errText).error?.message || errMsg } catch {}
throw new Error(errMsg)
// #Compat-5: 识别本地服务端拒绝消息,附加修复指引
throw new Error(enhanceModelCallError(errMsg))
}
_lastDebugInfo.streaming = true
@@ -2580,7 +2585,9 @@ async function callAIWithTools(messages, onStatus, onToolProgress, onChunk) {
const errText = await resp.text().catch(() => '')
let errMsg = `API 错误 ${resp.status}`
try { errMsg = JSON.parse(errText).error?.message || errMsg } catch {}
throw new Error(errMsg)
// #Compat-5: callAIWithTools 场景下 tools 带进 body 最容易踩 vLLM tool choice 限制,
// 识别并给出启动参数指引,避免用户一脸懵
throw new Error(enhanceModelCallError(errMsg))
}
// 流式累积状态