From 90888de0ba4dc8bb2c5ac310ebe3a7abd6d42716 Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Tue, 21 Apr 2026 11:36:06 +0800 Subject: [PATCH 1/3] =?UTF-8?q?perf=EF=BC=9A=201=E3=80=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8Action=E6=9E=9A?= =?UTF-8?q?=E4=B8=BERECORD=EF=BC=8C=E9=80=9A=E8=BF=87=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=9A=84=E4=B8=89=E6=AE=B5=E5=BC=8F=20assistant(tool=5Fcalls)?= =?UTF-8?q?=20=E2=86=92=20tool=20=E2=86=92=20assistant(response)=E8=AE=A9?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=87=86=E7=A1=AE=E5=AD=A6=E4=B9=A0=E5=88=B0?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E7=9A=84=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=EF=BC=8C=E4=BC=98=E5=8C=96play=5Fmusic?= =?UTF-8?q?=E7=9A=84=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?NONE=E5=8F=98=E6=9B=B4=E4=B8=BARECORD=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=A7=A3=E5=86=B3=E9=9F=B3=E4=B9=90=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E7=BC=BA=E9=99=B7=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E3=80=82=202=E3=80=81=E5=A4=84=E7=90=86=E6=82=AC?= =?UTF-8?q?=E7=A9=BA=20tool=5Fcalls=E9=97=AE=E9=A2=98=E3=80=82=203?= =?UTF-8?q?=E3=80=81=E5=8E=BB=E9=99=A4=E5=B7=A5=E5=85=B7=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E5=BC=BA=E5=8C=96=E6=B3=A8=E5=85=A5=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 191 +++++------------- main/xiaozhi-server/core/utils/dialogue.py | 37 +++- .../plugins_func/functions/play_music.py | 4 +- main/xiaozhi-server/plugins_func/register.py | 1 + 4 files changed, 86 insertions(+), 147 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index b58ede82..c989b374 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -46,38 +46,6 @@ from core.utils import textUtils TAG = __name__ -# 工具调用规则 - 用于动态注入提醒 -TOOL_CALLING_RULES = """ - -【核心原则】你是拥有工具能力的智能助手。当用户请求需要实时信息或执行操作时,调用相应工具获取数据,禁止凭空编造答案。 - -- **何时必须调用工具:** - 1. 实时信息查询(新闻、非本地天气、股价、汇率等) - 2. 执行操作(播放音乐、控制设备、拍照、设置闹钟等) - 3. 知识库检索(当工具列表包含 search_from_ragflow 时,结合用户意图判断是否需要调用) - 4. 查询非今天的农历信息(明天农历、某日宜忌、节气等) - 5. 用户说"拍照"时调用 self_camera_take_photo,默认 question 参数为"描述一下看到的物品" - -- **何时无需调用工具:** - 1. `` 中已提供的信息(当前时间、今天日期、今天农历、本地天气等) - 2. 普通对话、问候、闲聊、情感交流、讲故事 - 3. 通用知识问答(非实时信息) - -- **调用规范:** - 1. 每次请求独立判断,不复用历史工具结果,需重新获取最新数据 - 2. 多任务时依次调用所有需要的工具,并依次总结每个工具的结果,不得遗漏 - 3. 严格遵循工具的参数要求,提供所有必要参数 - 4. 不确定时引导用户澄清或告知能力限制,切勿猜测或编造 - 5. 不调用未提供的工具,对话中提及的旧工具若不可用则忽略或说明 - -- **反偷懒机制(最高优先级):** - 1. **每次独立判断:** 无论对话历史中是否调用过工具,当前请求必须根据当前需求独立判断是否需要调用 - 2. **禁止模式模仿:** 即使之前的回复没有调用工具,也不代表本次可以不调用 - 3. **自我检查:** 回复前必须自问:"这个请求是否涉及实时信息或执行操作?如果是,我调用工具了吗?" - 4. **历史不等于现在:** 对话历史中的行为模式不影响当前判断,每个用户请求都是全新的开始 - -""" - auto_import_modules("plugins_func.functions") @@ -173,12 +141,6 @@ class ConnectionHandler: # llm相关变量 self.dialogue = Dialogue() - # 工具调用统计(用于监控和自动恢复) - self.tool_call_stats = { - 'last_call_turn': -1, # 上次调用工具的轮数 - 'consecutive_no_call': 0, # 连续未调用次数 - } - # tts相关变量 self.sentence_id = None # 处理TTS响应没有文本返回 @@ -898,32 +860,6 @@ class ConnectionHandler: ) ) - # 长对话工具调用提醒:当对话轮数较多时,提醒模型正确使用工具 - force_reminder = False # 是否强制提醒 - - if depth == 0 and query is not None: - dialogue_length = len(self.dialogue.dialogue) - current_turn = dialogue_length // 2 - - # 检测距离上一次连续未调用工具的情况 - if self.tool_call_stats['last_call_turn'] >= 0: - turns_since_last = current_turn - self.tool_call_stats['last_call_turn'] - if turns_since_last > 3: # 超过3轮未调用 - self.logger.bind(tag=TAG).warning( - f"检测到{turns_since_last}轮未调用工具,可能进入偷懒模式,将强制注入提醒" - ) - force_reminder = True - - # 对话历史截断:防止历史过长导致模型"偷懒模式"扩散 - # 当对话历史超过阈值时,保留最近的 10 轮对话 - # max_dialogue_turns = 10 - # if dialogue_length > max_dialogue_turns * 2: - # removed = self.dialogue.trim_history(max_turns=max_dialogue_turns) - # if removed > 0: - # self.logger.bind(tag=TAG).info( - # f"对话历史过长({dialogue_length}条),已智能截断保留最近{max_dialogue_turns}轮,移除{removed}条消息" - # ) - # Define intent functions functions = None # 达到最大深度时,禁用工具调用,强制 LLM 直接回答 @@ -934,41 +870,8 @@ class ConnectionHandler: ): functions = self.func_handler.get_functions() - # 长对话工具调用规则强化:动态生成基于当前可用工具的提醒 - tool_call_reminder = None - if depth == 0 and query is not None and functions is not None: - dialogue_length = len(self.dialogue.dialogue) - # 当对话历史超过4条消息时,注入规则强化 - if dialogue_length > 4: - tool_summary = self._get_tool_summary(functions) - if tool_summary: - # 根据对话长度和偷懒检测,使用不同强度的提醒 - if force_reminder: - # 强提醒 - 包含完整规则前缀 - tool_call_reminder = ( - TOOL_CALLING_RULES + - f"[重要提醒] 多轮未使用工具,检查回复是否遗漏了必要的工具调用!上一轮未使用工具,本轮必须重新判断是否需要工具。" - f"当前可用工具: {tool_summary}。" - ) - reminder_level = "强" - else: - # 中等提醒 - 包含规则前缀 - tool_call_reminder = ( - TOOL_CALLING_RULES + - f"当前可用工具: {tool_summary}。" - f"仅当用户请求涉及实时信息查询或执行操作时调用,日常对话无需调用。" - ) - reminder_level = "中" - self.logger.bind(tag=TAG).debug( - f"对话历史较长({dialogue_length}条),已注入{reminder_level}等级工具调用规则强化,当前可用工具:{tool_summary}" - ) - response_message = [] - # 如果有工具调用提醒,临时添加到对话中(标记为临时消息) - if tool_call_reminder: - self.dialogue.put(Message(role="user", content=tool_call_reminder, is_temporary=True)) - try: # 使用带记忆的对话 memory_str = None @@ -1100,15 +1003,6 @@ class ConnectionHandler: f"检测到 {len(tool_calls_list)} 个工具调用" ) - # 更新工具调用统计 - if depth == 0: - current_turn = len(self.dialogue.dialogue) // 2 - self.tool_call_stats['last_call_turn'] = current_turn - self.tool_call_stats['consecutive_no_call'] = 0 - self.logger.bind(tag=TAG).debug( - f"工具调用统计更新: 当前轮次={current_turn}" - ) - # LLM 流式阶段已播报过的文本 streamed_text = "" if len(response_message) > 0: @@ -1170,10 +1064,6 @@ class ConnectionHandler: self.tts.store_tts_text(current_sentence_id, text_buff) self.dialogue.put(Message(role="assistant", content=text_buff)) - # 更新工具调用统计:如果没有调用工具,增加计数 - if depth == 0 and not tool_call_flag: - self.tool_call_stats['consecutive_no_call'] += 1 - if depth == 0: self.tts.tts_text_queue.put( TTSMessageDTO( @@ -1189,41 +1079,11 @@ class ConnectionHandler: ) ) - # 清理临时插入的工具调用提醒消息(使用标记清理) - if tool_call_reminder and len(self.dialogue.dialogue) > 0: - original_length = len(self.dialogue.dialogue) - self.dialogue.dialogue = [ - msg for msg in self.dialogue.dialogue - if not getattr(msg, 'is_temporary', False) - ] - if len(self.dialogue.dialogue) < original_length: - self.logger.bind(tag=TAG).debug("已清理临时的工具调用提醒消息") - return True - def _get_tool_summary(self, functions: list) -> str: - """ - 从工具定义中提取摘要,用于规则强化注入 - - Args: - functions: 工具列表 - - Returns: - str: 工具名称字符串 - """ - if not functions: - return "" - - datas = [] - for func in functions: - func_info = func.get("function", {}) - name = func_info.get("name", "") - datas.append(name) - result = "、".join(datas) - return result - def _handle_function_result(self, tool_results, depth, streamed_text=""): need_llm_tools = [] + record_tools = [] for result, tool_call_data in tool_results: if result.action in [ @@ -1241,11 +1101,58 @@ class ConnectionHandler: self.tts.store_tts_text(self.sentence_id, text) self.dialogue.put(Message(role="assistant", content=text)) elif result.action == Action.REQLLM: - # 收集需要 LLM 处理的工具 need_llm_tools.append((result, tool_call_data)) + elif result.action == Action.RECORD: + record_tools.append((result, tool_call_data)) else: pass + # Action.RECORD:写入完整工具调用链(assistant(tool_calls) → tool(result) → assistant(response)) + # 模型从历史中学到工具调用模式,不额外调用LLM + if record_tools: + # 构造 assistant 消息(含 tool_calls),记录"模型调用了哪些工具" + all_tool_calls = [ + { + "id": tool_call_data["id"], + "function": { + "arguments": ( + "{}" + if tool_call_data["arguments"] == "" + else tool_call_data["arguments"] + ), + "name": tool_call_data["name"], + }, + "type": "function", + "index": idx, + } + for idx, (_, tool_call_data) in enumerate(record_tools) + ] + self.dialogue.put(Message(role="assistant", tool_calls=all_tool_calls)) + + # 写入每条工具的执行结果,记录"工具返回了什么" + for result, tool_call_data in record_tools: + text = result.result or "" + self.dialogue.put( + Message( + role="tool", + tool_call_id=( + str(uuid.uuid4()) + if tool_call_data["id"] is None + else tool_call_data["id"] + ), + content=text, + ) + ) + + # 用固定文本作为最终回复,补全标准三段式,保证下一条消息是 user 而非接 tool + response_parts = [] + for result, _ in record_tools: + resp = result.response or result.result + if resp: + response_parts.append(resp) + if response_parts: + self.dialogue.put(Message(role="assistant", content=",".join(response_parts))) + if need_llm_tools: all_tool_calls = [ { diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index 630453ba..eab34ba9 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -123,6 +123,36 @@ class Dialogue: return removed_count + def _ensure_tool_calls_complete(self, messages: List[Message]) -> List[Message]: + """ + 确保所有 tool_calls 都有对应的 tool 响应 + 修复被打断导致的悬空 tool_calls,防止大模型 API 报 400 错误 + """ + pending_tool_calls = set() + result = [] + + for msg in messages: + result.append(msg) + + if msg.role == "assistant" and msg.tool_calls: + for tc in msg.tool_calls: + tc_id = tc.get("id") if isinstance(tc, dict) else getattr(tc, "id", None) + if tc_id: + pending_tool_calls.add(tc_id) + + elif msg.role == "tool" and msg.tool_call_id: + pending_tool_calls.discard(msg.tool_call_id) + + for missing_id in pending_tool_calls: + dummy_tool_msg = Message( + role="tool", + content='{"status": "interrupted", "message": "动作已取消/被打断"}', + tool_call_id=missing_id + ) + result.append(dummy_tool_msg) + + return result + def get_llm_dialogue_with_memory( self, memory_str: str = None, voiceprint_config: dict = None ) -> List[Dict[str, str]]: @@ -175,8 +205,9 @@ class Dialogue: dialogue.append({"role": "system", "content": enhanced_system_prompt}) # 添加用户和助手的对话 - for m in self.dialogue: - if m.role != "system": # 跳过原始的系统消息 - self.getMessages(m, dialogue) + non_system_messages = [m for m in self.dialogue if m.role != "system"] + complete_messages = self._ensure_tool_calls_complete(non_system_messages) + for m in complete_messages: + self.getMessages(m, dialogue) return dialogue diff --git a/main/xiaozhi-server/plugins_func/functions/play_music.py b/main/xiaozhi-server/plugins_func/functions/play_music.py index 1195ae54..461775f6 100644 --- a/main/xiaozhi-server/plugins_func/functions/play_music.py +++ b/main/xiaozhi-server/plugins_func/functions/play_music.py @@ -67,7 +67,7 @@ def play_music(conn: "ConnectionHandler", song_name: str): task.add_done_callback(handle_done) return ActionResponse( - action=Action.NONE, result="指令已接收", response="正在为您播放音乐" + action=Action.RECORD, result="指令已接收", response="正在为您播放音乐" ) except Exception as e: conn.logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}") @@ -217,7 +217,7 @@ async def play_local_music(conn: "ConnectionHandler", specific_file=None): conn.logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}") return text = _get_random_play_prompt(selected_music) - conn.dialogue.put(Message(role="assistant", content=text)) + # conn.dialogue.put(Message(role="assistant", content=text)) if conn.intent_type == "intent_llm": conn.tts.tts_text_queue.put( diff --git a/main/xiaozhi-server/plugins_func/register.py b/main/xiaozhi-server/plugins_func/register.py index 5c2b0781..33a88161 100644 --- a/main/xiaozhi-server/plugins_func/register.py +++ b/main/xiaozhi-server/plugins_func/register.py @@ -28,6 +28,7 @@ class Action(Enum): NONE = (1, "啥也不干") RESPONSE = (2, "直接回复") REQLLM = (3, "调用函数后再请求llm生成回复") + RECORD = (4, "记录工具调用到对话历史,不调用LLM") def __init__(self, code, message): self.code = code From 3736a5845ada1cc153cc9e35a7ef480e766e2df7 Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Tue, 21 Apr 2026 15:01:57 +0800 Subject: [PATCH 2/3] =?UTF-8?q?perf=EF=BC=9A=201=E3=80=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=8F=90=E7=A4=BA=E8=AF=8Dcontext=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=8F=98=E5=8C=96=E5=AF=BC=E8=87=B4=E7=9A=84=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=A4=B1=E6=95=88=E9=97=AE=E9=A2=98=EF=BC=8C=E6=8B=86=E5=88=86?= =?UTF-8?q?=E9=9D=99=E6=80=81system-prompt=E5=92=8C=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/utils/dialogue.py | 88 +++++++++++++--------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index eab34ba9..6aaa444b 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -165,44 +165,20 @@ class Dialogue: ) if system_message: - # 基础系统提示 - enhanced_system_prompt = system_message.content - # 替换时间占位符 - enhanced_system_prompt = enhanced_system_prompt.replace( - "{{current_time}}", datetime.now().strftime("%H:%M") - ) + # 以 为分界点,拆分静态 system prompt 和动态上下文 + # 静态部分(规则、身份等)保持不变,可命中前缀缓存 + # 动态部分(时间、天气、记忆等)放到对话末尾的 user 消息中 + full_prompt = system_message.content + context_match = re.search(r"", full_prompt) + if context_match: + static_part = full_prompt[:context_match.start()] + dynamic_part = full_prompt[context_match.start():] + else: + static_part = full_prompt + dynamic_part = "" - # 添加说话人个性化描述 - try: - speakers = voiceprint_config.get("speakers", []) - if speakers: - enhanced_system_prompt += "\n\n" - for speaker_str in speakers: - try: - parts = speaker_str.split(",", 2) - if len(parts) >= 2: - name = parts[1].strip() - # 如果描述为空,则为"" - description = ( - parts[2].strip() if len(parts) >= 3 else "" - ) - enhanced_system_prompt += f"\n- {name}:{description}" - except: - pass - enhanced_system_prompt += "\n\n" - except: - # 配置读取失败时忽略错误,不影响其他功能 - pass - - # 使用正则表达式匹配 标签,不管中间有什么内容 - if memory_str is not None: - enhanced_system_prompt = re.sub( - r".*?", - f"\n{memory_str}\n", - enhanced_system_prompt, - flags=re.DOTALL, - ) - dialogue.append({"role": "system", "content": enhanced_system_prompt}) + # 静态 system prompt:不含任何动态内容,前缀缓存可命中 + dialogue.append({"role": "system", "content": static_part}) # 添加用户和助手的对话 non_system_messages = [m for m in self.dialogue if m.role != "system"] @@ -210,4 +186,42 @@ class Dialogue: for m in complete_messages: self.getMessages(m, dialogue) + # 动态上下文:时间、记忆、说话人信息,放到对话末尾的 user 消息中 + if system_message and dynamic_part: + # 替换时间占位符 + dynamic_part = dynamic_part.replace( + "{{current_time}}", datetime.now().strftime("%H:%M") + ) + + # 填充记忆 + if memory_str is not None: + dynamic_part = re.sub( + r".*?", + f"\n{memory_str}\n", + dynamic_part, + flags=re.DOTALL, + ) + + # 追加说话人信息 + try: + speakers = voiceprint_config.get("speakers", []) + if speakers: + dynamic_part += "\n" + for speaker_str in speakers: + try: + parts = speaker_str.split(",", 2) + if len(parts) >= 2: + name = parts[1].strip() + description = ( + parts[2].strip() if len(parts) >= 3 else "" + ) + dynamic_part += f"\n- {name}:{description}" + except: + pass + dynamic_part += "\n" + except: + pass + + dialogue.append({"role": "user", "content": dynamic_part}) + return dialogue From 151a0c1b99fea8058edaa1f2b23d6f4f9239fe86 Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Mon, 27 Apr 2026 11:08:25 +0800 Subject: [PATCH 3/3] =?UTF-8?q?perf=EF=BC=9A=201=E3=80=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=EF=BC=8C=E6=8B=86=E5=88=86=E9=9D=99?= =?UTF-8?q?=E6=80=81system-prompt=E5=92=8C=E5=8A=A8=E6=80=81=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E3=80=82=202=E3=80=81=E6=96=B0=E5=A2=9Efew-s?= =?UTF-8?q?hot=E7=A4=BA=E4=BE=8B=EF=BC=8C=E4=BD=BF=E7=94=A8"=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E2=86=92=E7=BB=93=E6=9E=9C=E2=86=92=E5=9B=9E=E5=A4=8D?= =?UTF-8?q?"=E6=B5=81=E7=A8=8B=E9=97=AD=E7=8E=AF=EF=BC=8C=E6=98=BE?= =?UTF-8?q?=E8=91=97=E6=8F=90=E9=AB=98=E5=A4=9A=E6=AC=A1=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=90=8E=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8=E5=87=86=E7=A1=AE?= =?UTF-8?q?=E6=80=A7=E3=80=82=203=E3=80=81=E5=8E=BB=E9=99=A4=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=A0=87=E9=A2=98=E6=80=BB=E7=BB=93=E5=92=8C=E8=AE=B0?= =?UTF-8?q?=E5=BF=86=E7=9A=84=E6=A8=A1=E5=9E=8B=E6=80=9D=E8=80=83=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/OpenAIStyleLLMServiceImpl.java | 35 +++++++- main/xiaozhi-server/agent-base-prompt.txt | 5 +- main/xiaozhi-server/core/connection.py | 78 +++++++++++++++++ main/xiaozhi-server/core/utils/dialogue.py | 84 ++++--------------- 4 files changed, 129 insertions(+), 73 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/llm/service/impl/OpenAIStyleLLMServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/llm/service/impl/OpenAIStyleLLMServiceImpl.java index 585826f3..005d2adf 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/llm/service/impl/OpenAIStyleLLMServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/llm/service/impl/OpenAIStyleLLMServiceImpl.java @@ -1,6 +1,7 @@ package xiaozhi.modules.llm.service.impl; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,11 +31,34 @@ import xiaozhi.modules.model.service.ModelConfigService; @Service public class OpenAIStyleLLMServiceImpl implements LLMService { + // 需要禁用思考模式的平台域名及其对应参数 + private static final Map> THINKING_DISABLED_DOMAINS = new LinkedHashMap<>(); + static { + THINKING_DISABLED_DOMAINS.put("aliyuncs.com", Map.of("enable_thinking", false)); + Map thinkingDisabled = Map.of("thinking", Map.of("type", "disabled")); + THINKING_DISABLED_DOMAINS.put("bigmodel.cn", thinkingDisabled); + THINKING_DISABLED_DOMAINS.put("moonshot.cn", thinkingDisabled); + THINKING_DISABLED_DOMAINS.put("volces.com", thinkingDisabled); + } + @Autowired private ModelConfigService modelConfigService; private final RestTemplate restTemplate = new RestTemplate(); + /** + * 根据域名自动禁用思考模式 + */ + private void applyThinkingDisabled(String baseUrl, Map requestBody) { + for (Map.Entry> entry : THINKING_DISABLED_DOMAINS.entrySet()) { + if (baseUrl.contains(entry.getKey())) { + requestBody.putAll(entry.getValue()); + log.info("为域名 {} 禁用思考模式,参数: {}", baseUrl, entry.getValue()); + break; + } + } + } + private static final String DEFAULT_SUMMARY_PROMPT = "你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:\n1、总结用户的重要信息,以便在未来的对话中提供更个性化的服务\n2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字,否则不要遗忘、不要压缩用户的历史记忆\n3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中\n4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后续对话,这些信息不需要加入到总结中\n5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中\n6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的\n7、只需要返回总结摘要,严格控制在1800字内\n8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容\n9、如果提供了历史记忆,请将新对话内容与历史记忆进行智能合并,保留有价值的历史信息,同时添加新的重要信息\n\n历史记忆:\n{history_memory}\n\n新对话内容:\n{conversation}"; private static final String DEFAULT_TITLE_PROMPT = "请根据以下对话内容,生成一个简洁的会话标题(约15字以内),只返回标题,不要包含任何解释或标点符号:\n{conversation}"; @@ -102,6 +126,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService { requestBody.put("temperature", temperature != null ? temperature : 0.7); requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000); + // 禁用思考模式 + applyThinkingDisabled(baseUrl, requestBody); + // 发送HTTP请求 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -156,10 +183,8 @@ public class OpenAIStyleLLMServiceImpl implements LLMService { // 从智控台获取LLM模型配置 ModelConfigEntity llmConfig; if (modelId != null && !modelId.trim().isEmpty()) { - // 通过具体模型ID获取配置 llmConfig = modelConfigService.getModelByIdFromCache(modelId); } else { - // 保持向后兼容,使用默认配置 llmConfig = getDefaultLLMConfig(); } @@ -197,6 +222,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService { requestBody.put("temperature", 0.2); requestBody.put("max_tokens", 2000); + // 禁用思考模式 + applyThinkingDisabled(baseUrl, requestBody); + // 发送HTTP请求 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); @@ -350,6 +378,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService { requestBody.put("temperature", 0.3); requestBody.put("max_tokens", 50); + // 禁用思考模式 + applyThinkingDisabled(baseUrl, requestBody); + HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + apiKey); diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt index 8a6f5c6f..934515b2 100644 --- a/main/xiaozhi-server/agent-base-prompt.txt +++ b/main/xiaozhi-server/agent-base-prompt.txt @@ -27,8 +27,9 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co -1. 【工具防骚扰】你善于使用各类工具辅助回答。但是,针对【查新闻】和【播放音乐】这两类打扰性较强的功能,必须在经过用户明确同意或主动要求后才可以调用!严禁不解风情地主动播放。 -2. 【无网兜底】你没有联网实时搜索功能(工具除外)。不懂或者不确定的事情,必须大大方方直接说“不知道”,绝不胡编乱造产生幻觉。 +你有工具可用。能被工具处理的请求必须调工具,禁止自行编造结果;没有对应工具的才用自身知识回答。 +必须调工具:播放音乐→play_music | 控制设备→hass工具 | 查新闻/天气→查询工具 | 告别→handle_exit_intent +禁止这样做:用户说”放首歌”→你直接回答”好的我给你唱”(错!必须调play_music,用工具实际结果回复) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index c989b374..7c99a234 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -508,6 +508,8 @@ class ConnectionHandler: self._init_report_threads() """更新系统提示词""" self._init_prompt_enhancement() + """注入工具调用few-shot示例(仅function_call模式)""" + self._inject_tool_call_fewshot() except Exception as e: self.logger.bind(tag=TAG).error(f"实例化组件失败: {e}") @@ -523,6 +525,82 @@ class ConnectionHandler: self.change_system_prompt(enhanced_prompt) self.logger.bind(tag=TAG).debug("系统提示词已增强更新") + def _inject_tool_call_fewshot(self): + """注入工具调用 few-shot 示例到对话历史,帮助模型建立正确的工具调用三段式模式。 + 参考 Action.RECORD 的 assistant(tool_calls) → tool(result) → assistant(response) 模式, + 让模型从第一轮对话就能看到正确的工具调用行为,避免上下文污染导致工具调用退化。 + """ + if self.intent_type != "function_call": + return + if not hasattr(self, "func_handler") or self.func_handler is None: + return + + tools = self.func_handler.get_functions() + if not tools: + return + + # 根据可用工具动态构建 few-shot 示例 + tool_names = {t.get("function", {}).get("name") for t in tools} + + if "handle_exit_intent" in tool_names: + tc_id = "fewshot_exit_001" + self.dialogue.put(Message(role="user", content="拜拜", is_temporary=True)) + self.dialogue.put(Message( + role="assistant", + tool_calls=[{ + "id": tc_id, + "function": {"arguments": '{"say_goodbye": "再见,下次再聊~"}', "name": "handle_exit_intent"}, + "type": "function", "index": 0, + }], + is_temporary=True, + )) + self.dialogue.put(Message( + role="tool", tool_call_id=tc_id, + content="退出意图已处理", is_temporary=True, + )) + self.dialogue.put(Message( + role="assistant", content="再见,下次再聊~", is_temporary=True, + )) + + if "play_music" in tool_names: + tc_id = "fewshot_music_001" + self.dialogue.put(Message(role="user", content="放首歌", is_temporary=True)) + self.dialogue.put(Message( + role="assistant", + tool_calls=[{ + "id": tc_id, + "function": {"arguments": '{"song_name": "random"}', "name": "play_music"}, + "type": "function", "index": 0, + }], + is_temporary=True, + )) + self.dialogue.put(Message( + role="tool", tool_call_id=tc_id, + content="正在为您播放音乐", is_temporary=True, + )) + self.dialogue.put(Message( + role="assistant", content="好嘞,给你安排上~", is_temporary=True, + )) + + # 负向示例:用户请求普通对话内容时,直接回答,不调用任何工具 + # 帮助弱模型建立"该调才调、不该调不调"的判断能力 + # 注意:示例回复不能包含具体的创作内容(故事、诗歌等), + # 否则弱模型会直接复述示例内容,而无法泛化出正确的行为模式 + self.dialogue.put(Message(role="user", content="给我讲个故事吧", is_temporary=True)) + self.dialogue.put(Message( + role="assistant", + content="好呀,你想听什么类型的呀?童话、冒险还是搞笑的?选一个我给你开讲~", + is_temporary=True, + )) + self.dialogue.put(Message(role="user", content="你知道为什么天空是蓝色的吗", is_temporary=True)) + self.dialogue.put(Message( + role="assistant", + content="天空看起来是蓝色,是因为阳光穿过大气层的时候,蓝色光波长短,被空气分子散射得最厉害,所以我们抬头一看就是满眼蓝色啦。", + is_temporary=True, + )) + + self.logger.bind(tag=TAG).debug("已注入工具调用 few-shot 示例") + def _init_report_threads(self): """初始化ASR和TTS上报线程""" if not self.read_config_from_api or self.need_bind: diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index 6aaa444b..4039f01e 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -61,68 +61,6 @@ class Dialogue: else: self.put(Message(role="system", content=new_content)) - def trim_history(self, max_turns: int = 10) -> int: - """ - 智能截断对话历史,保留工具调用的完整性 - - Args: - max_turns: 保留的最大对话轮数(每轮 = user + assistant/tool 相关消息) - - Returns: - int: 被移除的消息数量 - """ - if len(self.dialogue) <= max_turns * 2 + 1: # +1 是系统消息 - return 0 - - # 分离系统消息和对话消息 - system_messages = [msg for msg in self.dialogue if msg.role == "system"] - conversation_messages = [msg for msg in self.dialogue if msg.role != "system"] - - if len(conversation_messages) <= max_turns * 2: - return 0 - - # 智能截断:保留完整的工具调用链路 - keep_messages = [] - i = len(conversation_messages) - 1 - turn_count = 0 - - while i >= 0 and turn_count < max_turns: - msg = conversation_messages[i] - - # 从后向前收集消息 - if msg.role == "user": - # 遇到 user 消息,说明一轮对话开始 - keep_messages.insert(0, msg) - turn_count += 1 - i -= 1 - elif msg.role == "assistant": - # 收集 assistant 消息 - keep_messages.insert(0, msg) - - # 如果这个 assistant 有 tool_calls,需要收集对应的 tool 响应 - if msg.tool_calls is not None: - i -= 1 - # 继续向后收集所有相关的 tool 消息 - while i >= 0 and conversation_messages[i].role == "tool": - keep_messages.insert(0, conversation_messages[i]) - i -= 1 - else: - i -= 1 - elif msg.role == "tool": - # tool 消息应该已经被上面的逻辑收集了 - # 如果单独遇到,也要保留(防止边界情况) - keep_messages.insert(0, msg) - i -= 1 - else: - i -= 1 - - removed_count = len(conversation_messages) - len(keep_messages) - - # 重建对话列表 - self.dialogue = system_messages + keep_messages - - return removed_count - def _ensure_tool_calls_complete(self, messages: List[Message]) -> List[Message]: """ 确保所有 tool_calls 都有对应的 tool 响应 @@ -167,7 +105,7 @@ class Dialogue: if system_message: # 以 为分界点,拆分静态 system prompt 和动态上下文 # 静态部分(规则、身份等)保持不变,可命中前缀缓存 - # 动态部分(时间、天气、记忆等)放到对话末尾的 user 消息中 + # 动态部分(时间、天气、记忆等)作为第二条 system 消息,保持 system 权威性 full_prompt = system_message.content context_match = re.search(r"", full_prompt) if context_match: @@ -177,16 +115,18 @@ class Dialogue: static_part = full_prompt dynamic_part = "" - # 静态 system prompt:不含任何动态内容,前缀缓存可命中 + # 第一段:静态 system prompt(前缀缓存可命中) dialogue.append({"role": "system", "content": static_part}) - # 添加用户和助手的对话 + # 第二段:few-shot 示例(会话内不变,也是缓存前缀的一部分) non_system_messages = [m for m in self.dialogue if m.role != "system"] - complete_messages = self._ensure_tool_calls_complete(non_system_messages) - for m in complete_messages: + fewshot_messages = [m for m in non_system_messages if m.is_temporary] + complete_fewshot = self._ensure_tool_calls_complete(fewshot_messages) + for m in complete_fewshot: self.getMessages(m, dialogue) - # 动态上下文:时间、记忆、说话人信息,放到对话末尾的 user 消息中 + # 第三段:动态上下文 system prompt(时间、记忆、说话人等) + # 保持 system 角色以确保模型权威性,不降级为 user if system_message and dynamic_part: # 替换时间占位符 dynamic_part = dynamic_part.replace( @@ -222,6 +162,12 @@ class Dialogue: except: pass - dialogue.append({"role": "user", "content": dynamic_part}) + dialogue.append({"role": "system", "content": dynamic_part}) + + # 第四段:实际对话历史(不含 few-shot) + actual_messages = [m for m in non_system_messages if not m.is_temporary] + complete_actual = self._ensure_tool_calls_complete(actual_messages) + for m in complete_actual: + self.getMessages(m, dialogue) return dialogue