From 6b8d2502b7fd212a4f5e1786fa70928cc9d62f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E8=83=BD=E6=B7=B7?= Date: Thu, 15 May 2025 06:32:47 +0800 Subject: [PATCH 1/4] Update ollama.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为本地ollama部署qwen3大语言模型添加适配!在识别到启用为qwen3大模型的时候自动添加no_think停用推理模式,并过滤标签转入给TTS(因为qwen3虽然用no_think停用推理后还是会输出转入到TTS) --- .../core/providers/llm/ollama/ollama.py | 113 ++++++++++++++++-- 1 file changed, 105 insertions(+), 8 deletions(-) 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}") From bdd8597f696494c0de82c606b652682ae5805c1b Mon Sep 17 00:00:00 2001 From: CGD <3030332422@qq.com> Date: Fri, 16 May 2025 14:33:37 +0800 Subject: [PATCH 2/4] =?UTF-8?q?update:=E5=9C=A8=E8=AF=86=E5=88=AB=E5=88=B0?= =?UTF-8?q?continue=5Fchat=E6=84=8F=E5=9B=BE=E6=97=B6=EF=BC=8C=E6=B8=85?= =?UTF-8?q?=E7=90=86=E5=AF=B9=E8=AF=9D=E5=8E=86=E5=8F=B2=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E7=9B=B8=E5=85=B3=E6=B6=88?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/providers/intent/intent_llm/intent_llm.py | 9 +++++++++ 1 file changed, 9 insertions(+) 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, From 76c3f401eac993c03437c66d45d635313043d300 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 16 May 2025 14:55:53 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/views/ProviderManagement.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/manager-web/src/views/ProviderManagement.vue b/main/manager-web/src/views/ProviderManagement.vue index f047f482..38f4ca36 100644 --- a/main/manager-web/src/views/ProviderManagement.vue +++ b/main/manager-web/src/views/ProviderManagement.vue @@ -24,7 +24,7 @@
- + Date: Fri, 16 May 2025 17:10:55 +0800 Subject: [PATCH 4/4] Update 202504112044.sql MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix:修复智控台fish-speech无法按声音播放bug --- .../src/main/resources/db/changelog/202504112044.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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);