diff --git a/main/manager-api/src/main/resources/db/changelog/202504112044.sql b/main/manager-api/src/main/resources/db/changelog/202504112044.sql
index 9fd2257d..df00d3b8 100644
--- a/main/manager-api/src/main/resources/db/changelog/202504112044.sql
+++ b/main/manager-api/src/main/resources/db/changelog/202504112044.sql
@@ -32,7 +32,7 @@ INSERT INTO `ai_model_config` VALUES ('TTS_EdgeTTS', 'TTS', 'EdgeTTS', 'Edge语
INSERT INTO `ai_model_config` VALUES ('TTS_DoubaoTTS', 'TTS', 'DoubaoTTS', '豆包语音合成', 0, 1, '{\"type\": \"doubao\", \"api_url\": \"https://openspeech.bytedance.com/api/v1/tts\", \"voice\": \"BV001_streaming\", \"output_dir\": \"tmp/\", \"authorization\": \"Bearer;\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcano_tts\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CosyVoiceSiliconflow', 'TTS', 'CosyVoiceSiliconflow', '硅基流动语音合成', 0, 1, '{\"type\": \"siliconflow\", \"model\": \"FunAudioLLM/CosyVoice2-0.5B\", \"voice\": \"FunAudioLLM/CosyVoice2-0.5B:alex\", \"output_dir\": \"tmp/\", \"access_token\": \"\", \"response_format\": \"wav\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CozeCnTTS', 'TTS', 'CozeCnTTS', 'Coze中文语音合成', 0, 1, '{\"type\": \"cozecn\", \"voice\": \"7426720361733046281\", \"output_dir\": \"tmp/\", \"access_token\": \"\", \"response_format\": \"wav\"}', NULL, NULL, 4, NULL, NULL, NULL, NULL);
-INSERT INTO `ai_model_config` VALUES ('TTS_FishSpeech', 'TTS', 'FishSpeech', 'FishSpeech语音合成', 0, 1, '{\"type\": \"fishspeech\", \"output_dir\": \"tmp/\", \"response_format\": \"wav\", \"reference_id\": null, \"reference_audio\": [\"/tmp/test.wav\"], \"reference_text\": [\"你弄来这些吟词宴曲来看,还是这些混话来欺负我。\"], \"normalize\": true, \"max_new_tokens\": 1024, \"chunk_length\": 200, \"top_p\": 0.7, \"repetition_penalty\": 1.2, \"temperature\": 0.7, \"streaming\": false, \"use_memory_cache\": \"on\", \"seed\": null, \"channels\": 1, \"rate\": 44100, \"api_key\": \"\", \"api_url\": \"http://127.0.0.1:8080/v1/tts\"}', NULL, NULL, 5, NULL, NULL, NULL, NULL);
+INSERT INTO `ai_model_config` VALUES ('TTS_FishSpeech', 'TTS', 'FishSpeech', 'FishSpeech语音合成', 0, 1, '{\"type\": \"fishspeech\", \"output_dir\": \"tmp/\", \"response_format\": \"wav\", \"reference_id\": null, \"reference_audio\": [\"/tmp/test.wav\"], \"reference_text\": [\"你弄来这些吟词宴曲来看,还是这些混话来欺负我。\"], \"normalize\": true, \"max_new_tokens\": 1024, \"chunk_length\": 200, \"top_p\": 0.7, \"repetition_penalty\": 1.2, \"temperature\": 0.7, \"streaming\": false, \"use_memory_cache\": \"on\", \"seed\": 1, \"channels\": 1, \"rate\": 44100, \"api_key\": \"\", \"api_url\": \"http://127.0.0.1:8080/v1/tts\"}', NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GPT_SOVITS_V2', 'TTS', 'GPT_SOVITS_V2', 'GPT-SoVITS V2', 0, 1, '{\"type\": \"gpt_sovits_v2\", \"url\": \"http://127.0.0.1:9880/tts\", \"output_dir\": \"tmp/\", \"text_lang\": \"auto\", \"ref_audio_path\": \"caixukun.wav\", \"prompt_text\": \"\", \"prompt_lang\": \"zh\", \"top_k\": 5, \"top_p\": 1, \"temperature\": 1, \"text_split_method\": \"cut0\", \"batch_size\": 1, \"batch_threshold\": 0.75, \"split_bucket\": true, \"return_fragment\": false, \"speed_factor\": 1.0, \"streaming_mode\": false, \"seed\": -1, \"parallel_infer\": true, \"repetition_penalty\": 1.35, \"aux_ref_audio_paths\": []}', NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GPT_SOVITS_V3', 'TTS', 'GPT_SOVITS_V3', 'GPT-SoVITS V3', 0, 1, '{\"type\": \"gpt_sovits_v3\", \"url\": \"http://127.0.0.1:9880\", \"output_dir\": \"tmp/\", \"text_language\": \"auto\", \"refer_wav_path\": \"caixukun.wav\", \"prompt_language\": \"zh\", \"prompt_text\": \"\", \"top_k\": 15, \"top_p\": 1.0, \"temperature\": 1.0, \"cut_punc\": \"\", \"speed\": 1.0, \"inp_refs\": [], \"sample_steps\": 32, \"if_sr\": false}', NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_MinimaxTTS', 'TTS', 'MinimaxTTS', 'MiniMax语音合成', 0, 1, '{\"type\": \"minimax\", \"output_dir\": \"tmp/\", \"group_id\": \"\", \"api_key\": \"你的api_key\", \"model\": \"speech-01-turbo\", \"voice_id\": \"female-shaonv\"}', NULL, NULL, 8, NULL, NULL, NULL, NULL);
diff --git a/main/manager-web/src/views/ProviderManagement.vue b/main/manager-web/src/views/ProviderManagement.vue
index a476c5e2..7634b511 100644
--- a/main/manager-web/src/views/ProviderManagement.vue
+++ b/main/manager-web/src/views/ProviderManagement.vue
@@ -26,7 +26,7 @@
-
+
diff --git a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
index 195d058a..2a3e023f 100644
--- a/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
+++ b/main/xiaozhi-server/core/providers/intent/intent_llm/intent_llm.py
@@ -218,6 +218,15 @@ class IntentProvider(IntentProviderBase):
f"llm 识别到意图: {function_name}, 参数: {function_args}"
)
+ # 如果是继续聊天,清理工具调用相关的历史消息
+ if function_name == "continue_chat":
+ # 保留非工具相关的消息
+ clean_history = [
+ msg for msg in conn.dialogue.dialogue
+ if msg.role not in ["tool", "function"]
+ ]
+ conn.dialogue.dialogue = clean_history
+
# 添加到缓存
self.intent_cache[cache_key] = {
"intent": intent,
diff --git a/main/xiaozhi-server/core/providers/llm/ollama/ollama.py b/main/xiaozhi-server/core/providers/llm/ollama/ollama.py
index fb34ced5..aad6354f 100644
--- a/main/xiaozhi-server/core/providers/llm/ollama/ollama.py
+++ b/main/xiaozhi-server/core/providers/llm/ollama/ollama.py
@@ -21,27 +21,67 @@ class LLMProvider(LLMProviderBase):
api_key="ollama" # Ollama doesn't need an API key but OpenAI client requires one
)
+ # 检查是否是qwen3模型
+ self.is_qwen3 = self.model_name and self.model_name.lower().startswith("qwen3")
+
def response(self, session_id, dialogue):
try:
+ # 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
+ if self.is_qwen3:
+ # 复制对话列表,避免修改原始对话
+ dialogue_copy = dialogue.copy()
+
+ # 找到最后一条用户消息
+ for i in range(len(dialogue_copy) - 1, -1, -1):
+ if dialogue_copy[i]["role"] == "user":
+ # 在用户消息前添加/no_think指令
+ dialogue_copy[i]["content"] = "/no_think " + dialogue_copy[i]["content"]
+ logger.bind(tag=TAG).debug(f"为qwen3模型添加/no_think指令")
+ break
+
+ # 使用修改后的对话
+ dialogue = dialogue_copy
+
responses = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
stream=True
)
- is_active=True
+ is_active = True
+ # 用于处理跨chunk的标签
+ buffer = ""
+
for chunk in responses:
try:
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
content = delta.content if hasattr(delta, 'content') else ''
+
if content:
- if '' in content:
+ # 将内容添加到缓冲区
+ buffer += content
+
+ # 处理缓冲区中的标签
+ while '' in buffer and '' in buffer:
+ # 找到完整的标签并移除
+ pre = buffer.split('', 1)[0]
+ post = buffer.split('', 1)[1]
+ buffer = pre + post
+
+ # 处理只有开始标签的情况
+ if '' in buffer:
is_active = False
- content = content.split('')[0]
- if '' in content:
+ buffer = buffer.split('', 1)[0]
+
+ # 处理只有结束标签的情况
+ if '' in buffer:
is_active = True
- content = content.split('')[-1]
- if is_active:
- yield content
+ buffer = buffer.split('', 1)[1]
+
+ # 如果当前处于活动状态且缓冲区有内容,则输出
+ if is_active and buffer:
+ yield buffer
+ buffer = "" # 清空缓冲区
+
except Exception as e:
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
@@ -51,6 +91,22 @@ class LLMProvider(LLMProviderBase):
def response_with_functions(self, session_id, dialogue, functions=None):
try:
+ # 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
+ if self.is_qwen3:
+ # 复制对话列表,避免修改原始对话
+ dialogue_copy = dialogue.copy()
+
+ # 找到最后一条用户消息
+ for i in range(len(dialogue_copy) - 1, -1, -1):
+ if dialogue_copy[i]["role"] == "user":
+ # 在用户消息前添加/no_think指令
+ dialogue_copy[i]["content"] = "/no_think " + dialogue_copy[i]["content"]
+ logger.bind(tag=TAG).debug(f"为qwen3模型添加/no_think指令")
+ break
+
+ # 使用修改后的对话
+ dialogue = dialogue_copy
+
stream = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
@@ -58,8 +114,49 @@ class LLMProvider(LLMProviderBase):
tools=functions,
)
+ is_active = True
+ buffer = ""
+
for chunk in stream:
- yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
+ try:
+ delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
+ content = delta.content if hasattr(delta, 'content') else None
+ tool_calls = delta.tool_calls if hasattr(delta, 'tool_calls') else None
+
+ # 如果是工具调用,直接传递
+ if tool_calls:
+ yield None, tool_calls
+ continue
+
+ # 处理文本内容
+ if content:
+ # 将内容添加到缓冲区
+ buffer += content
+
+ # 处理缓冲区中的标签
+ while '' in buffer and '' in buffer:
+ # 找到完整的标签并移除
+ pre = buffer.split('', 1)[0]
+ post = buffer.split('', 1)[1]
+ buffer = pre + post
+
+ # 处理只有开始标签的情况
+ if '' in buffer:
+ is_active = False
+ buffer = buffer.split('', 1)[0]
+
+ # 处理只有结束标签的情况
+ if '' in buffer:
+ is_active = True
+ buffer = buffer.split('', 1)[1]
+
+ # 如果当前处于活动状态且缓冲区有内容,则输出
+ if is_active and buffer:
+ yield buffer, None
+ buffer = "" # 清空缓冲区
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"Error processing function chunk: {e}")
+ continue
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")