mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 16:43:55 +08:00
Merge pull request #3135 from xinnan-tech/perf-tool-call-optimization
Perf:优化工具调用准确性、禁用标题总结与记忆模型思考模式、拆分动态上下文
This commit is contained in:
+33
-2
@@ -1,6 +1,7 @@
|
|||||||
package xiaozhi.modules.llm.service.impl;
|
package xiaozhi.modules.llm.service.impl;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -30,11 +31,34 @@ import xiaozhi.modules.model.service.ModelConfigService;
|
|||||||
@Service
|
@Service
|
||||||
public class OpenAIStyleLLMServiceImpl implements LLMService {
|
public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||||
|
|
||||||
|
// 需要禁用思考模式的平台域名及其对应参数
|
||||||
|
private static final Map<String, Map<String, Object>> THINKING_DISABLED_DOMAINS = new LinkedHashMap<>();
|
||||||
|
static {
|
||||||
|
THINKING_DISABLED_DOMAINS.put("aliyuncs.com", Map.of("enable_thinking", false));
|
||||||
|
Map<String, Object> 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
|
@Autowired
|
||||||
private ModelConfigService modelConfigService;
|
private ModelConfigService modelConfigService;
|
||||||
|
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据域名自动禁用思考模式
|
||||||
|
*/
|
||||||
|
private void applyThinkingDisabled(String baseUrl, Map<String, Object> requestBody) {
|
||||||
|
for (Map.Entry<String, Map<String, Object>> 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_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}";
|
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("temperature", temperature != null ? temperature : 0.7);
|
||||||
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
||||||
|
|
||||||
|
// 禁用思考模式
|
||||||
|
applyThinkingDisabled(baseUrl, requestBody);
|
||||||
|
|
||||||
// 发送HTTP请求
|
// 发送HTTP请求
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
@@ -156,10 +183,8 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
// 从智控台获取LLM模型配置
|
// 从智控台获取LLM模型配置
|
||||||
ModelConfigEntity llmConfig;
|
ModelConfigEntity llmConfig;
|
||||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||||
// 通过具体模型ID获取配置
|
|
||||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||||
} else {
|
} else {
|
||||||
// 保持向后兼容,使用默认配置
|
|
||||||
llmConfig = getDefaultLLMConfig();
|
llmConfig = getDefaultLLMConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +222,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
requestBody.put("temperature", 0.2);
|
requestBody.put("temperature", 0.2);
|
||||||
requestBody.put("max_tokens", 2000);
|
requestBody.put("max_tokens", 2000);
|
||||||
|
|
||||||
|
// 禁用思考模式
|
||||||
|
applyThinkingDisabled(baseUrl, requestBody);
|
||||||
|
|
||||||
// 发送HTTP请求
|
// 发送HTTP请求
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
@@ -350,6 +378,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
requestBody.put("temperature", 0.3);
|
requestBody.put("temperature", 0.3);
|
||||||
requestBody.put("max_tokens", 50);
|
requestBody.put("max_tokens", 50);
|
||||||
|
|
||||||
|
// 禁用思考模式
|
||||||
|
applyThinkingDisabled(baseUrl, requestBody);
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
headers.set("Authorization", "Bearer " + apiKey);
|
headers.set("Authorization", "Bearer " + apiKey);
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
|
|||||||
</tts_format_constraints>
|
</tts_format_constraints>
|
||||||
|
|
||||||
<tool_and_knowledge>
|
<tool_and_knowledge>
|
||||||
1. 【工具防骚扰】你善于使用各类工具辅助回答。但是,针对【查新闻】和【播放音乐】这两类打扰性较强的功能,必须在经过用户明确同意或主动要求后才可以调用!严禁不解风情地主动播放。
|
你有工具可用。能被工具处理的请求必须调工具,禁止自行编造结果;没有对应工具的才用自身知识回答。
|
||||||
2. 【无网兜底】你没有联网实时搜索功能(工具除外)。不懂或者不确定的事情,必须大大方方直接说“不知道”,绝不胡编乱造产生幻觉。
|
必须调工具:播放音乐→play_music | 控制设备→hass工具 | 查新闻/天气→查询工具 | 告别→handle_exit_intent
|
||||||
|
禁止这样做:用户说”放首歌”→你直接回答”好的我给你唱”(错!必须调play_music,用工具实际结果回复)
|
||||||
</tool_and_knowledge>
|
</tool_and_knowledge>
|
||||||
|
|
||||||
<safety_compliance>
|
<safety_compliance>
|
||||||
|
|||||||
@@ -46,38 +46,6 @@ from core.utils import textUtils
|
|||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
# 工具调用规则 - 用于动态注入提醒
|
|
||||||
TOOL_CALLING_RULES = """
|
|
||||||
<tool_calling>
|
|
||||||
【核心原则】你是拥有工具能力的智能助手。当用户请求需要实时信息或执行操作时,调用相应工具获取数据,禁止凭空编造答案。
|
|
||||||
|
|
||||||
- **何时必须调用工具:**
|
|
||||||
1. 实时信息查询(新闻、非本地天气、股价、汇率等)
|
|
||||||
2. 执行操作(播放音乐、控制设备、拍照、设置闹钟等)
|
|
||||||
3. 知识库检索(当工具列表包含 search_from_ragflow 时,结合用户意图判断是否需要调用)
|
|
||||||
4. 查询非今天的农历信息(明天农历、某日宜忌、节气等)
|
|
||||||
5. 用户说"拍照"时调用 self_camera_take_photo,默认 question 参数为"描述一下看到的物品"
|
|
||||||
|
|
||||||
- **何时无需调用工具:**
|
|
||||||
1. `<context>` 中已提供的信息(当前时间、今天日期、今天农历、本地天气等)
|
|
||||||
2. 普通对话、问候、闲聊、情感交流、讲故事
|
|
||||||
3. 通用知识问答(非实时信息)
|
|
||||||
|
|
||||||
- **调用规范:**
|
|
||||||
1. 每次请求独立判断,不复用历史工具结果,需重新获取最新数据
|
|
||||||
2. 多任务时依次调用所有需要的工具,并依次总结每个工具的结果,不得遗漏
|
|
||||||
3. 严格遵循工具的参数要求,提供所有必要参数
|
|
||||||
4. 不确定时引导用户澄清或告知能力限制,切勿猜测或编造
|
|
||||||
5. 不调用未提供的工具,对话中提及的旧工具若不可用则忽略或说明
|
|
||||||
|
|
||||||
- **反偷懒机制(最高优先级):**
|
|
||||||
1. **每次独立判断:** 无论对话历史中是否调用过工具,当前请求必须根据当前需求独立判断是否需要调用
|
|
||||||
2. **禁止模式模仿:** 即使之前的回复没有调用工具,也不代表本次可以不调用
|
|
||||||
3. **自我检查:** 回复前必须自问:"这个请求是否涉及实时信息或执行操作?如果是,我调用工具了吗?"
|
|
||||||
4. **历史不等于现在:** 对话历史中的行为模式不影响当前判断,每个用户请求都是全新的开始
|
|
||||||
</tool_calling>
|
|
||||||
"""
|
|
||||||
|
|
||||||
auto_import_modules("plugins_func.functions")
|
auto_import_modules("plugins_func.functions")
|
||||||
|
|
||||||
|
|
||||||
@@ -171,12 +139,6 @@ class ConnectionHandler:
|
|||||||
# llm相关变量
|
# llm相关变量
|
||||||
self.dialogue = Dialogue()
|
self.dialogue = Dialogue()
|
||||||
|
|
||||||
# 工具调用统计(用于监控和自动恢复)
|
|
||||||
self.tool_call_stats = {
|
|
||||||
'last_call_turn': -1, # 上次调用工具的轮数
|
|
||||||
'consecutive_no_call': 0, # 连续未调用次数
|
|
||||||
}
|
|
||||||
|
|
||||||
# tts相关变量
|
# tts相关变量
|
||||||
self.sentence_id = None
|
self.sentence_id = None
|
||||||
# 处理TTS响应没有文本返回
|
# 处理TTS响应没有文本返回
|
||||||
@@ -540,6 +502,8 @@ class ConnectionHandler:
|
|||||||
self._init_report_threads()
|
self._init_report_threads()
|
||||||
"""更新系统提示词"""
|
"""更新系统提示词"""
|
||||||
self._init_prompt_enhancement()
|
self._init_prompt_enhancement()
|
||||||
|
"""注入工具调用few-shot示例(仅function_call模式)"""
|
||||||
|
self._inject_tool_call_fewshot()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"实例化组件失败: {e}")
|
self.logger.bind(tag=TAG).error(f"实例化组件失败: {e}")
|
||||||
@@ -555,6 +519,82 @@ class ConnectionHandler:
|
|||||||
self.change_system_prompt(enhanced_prompt)
|
self.change_system_prompt(enhanced_prompt)
|
||||||
self.logger.bind(tag=TAG).debug("系统提示词已增强更新")
|
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):
|
def _init_report_threads(self):
|
||||||
"""初始化ASR和TTS上报线程"""
|
"""初始化ASR和TTS上报线程"""
|
||||||
if not self.read_config_from_api or self.need_bind:
|
if not self.read_config_from_api or self.need_bind:
|
||||||
@@ -892,32 +932,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
|
# Define intent functions
|
||||||
functions = None
|
functions = None
|
||||||
# 达到最大深度时,禁用工具调用,强制 LLM 直接回答
|
# 达到最大深度时,禁用工具调用,强制 LLM 直接回答
|
||||||
@@ -928,41 +942,8 @@ class ConnectionHandler:
|
|||||||
):
|
):
|
||||||
functions = self.func_handler.get_functions()
|
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 = []
|
response_message = []
|
||||||
|
|
||||||
# 如果有工具调用提醒,临时添加到对话中(标记为临时消息)
|
|
||||||
if tool_call_reminder:
|
|
||||||
self.dialogue.put(Message(role="user", content=tool_call_reminder, is_temporary=True))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 使用带记忆的对话
|
# 使用带记忆的对话
|
||||||
memory_str = None
|
memory_str = None
|
||||||
@@ -1094,15 +1075,6 @@ class ConnectionHandler:
|
|||||||
f"检测到 {len(tool_calls_list)} 个工具调用"
|
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 流式阶段已播报过的文本
|
# LLM 流式阶段已播报过的文本
|
||||||
streamed_text = ""
|
streamed_text = ""
|
||||||
if len(response_message) > 0:
|
if len(response_message) > 0:
|
||||||
@@ -1164,10 +1136,6 @@ class ConnectionHandler:
|
|||||||
self.tts.store_tts_text(current_sentence_id, text_buff)
|
self.tts.store_tts_text(current_sentence_id, text_buff)
|
||||||
self.dialogue.put(Message(role="assistant", content=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:
|
if depth == 0:
|
||||||
self.tts.tts_text_queue.put(
|
self.tts.tts_text_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
@@ -1183,41 +1151,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
|
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=""):
|
def _handle_function_result(self, tool_results, depth, streamed_text=""):
|
||||||
need_llm_tools = []
|
need_llm_tools = []
|
||||||
|
record_tools = []
|
||||||
|
|
||||||
for result, tool_call_data in tool_results:
|
for result, tool_call_data in tool_results:
|
||||||
if result.action in [
|
if result.action in [
|
||||||
@@ -1235,11 +1173,58 @@ class ConnectionHandler:
|
|||||||
self.tts.store_tts_text(self.sentence_id, text)
|
self.tts.store_tts_text(self.sentence_id, text)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
elif result.action == Action.REQLLM:
|
elif result.action == Action.REQLLM:
|
||||||
# 收集需要 LLM 处理的工具
|
|
||||||
need_llm_tools.append((result, tool_call_data))
|
need_llm_tools.append((result, tool_call_data))
|
||||||
|
elif result.action == Action.RECORD:
|
||||||
|
record_tools.append((result, tool_call_data))
|
||||||
else:
|
else:
|
||||||
pass
|
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:
|
if need_llm_tools:
|
||||||
all_tool_calls = [
|
all_tool_calls = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,67 +61,35 @@ class Dialogue:
|
|||||||
else:
|
else:
|
||||||
self.put(Message(role="system", content=new_content))
|
self.put(Message(role="system", content=new_content))
|
||||||
|
|
||||||
def trim_history(self, max_turns: int = 10) -> int:
|
def _ensure_tool_calls_complete(self, messages: List[Message]) -> List[Message]:
|
||||||
"""
|
"""
|
||||||
智能截断对话历史,保留工具调用的完整性
|
确保所有 tool_calls 都有对应的 tool 响应
|
||||||
|
修复被打断导致的悬空 tool_calls,防止大模型 API 报 400 错误
|
||||||
Args:
|
|
||||||
max_turns: 保留的最大对话轮数(每轮 = user + assistant/tool 相关消息)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: 被移除的消息数量
|
|
||||||
"""
|
"""
|
||||||
if len(self.dialogue) <= max_turns * 2 + 1: # +1 是系统消息
|
pending_tool_calls = set()
|
||||||
return 0
|
result = []
|
||||||
|
|
||||||
# 分离系统消息和对话消息
|
for msg in messages:
|
||||||
system_messages = [msg for msg in self.dialogue if msg.role == "system"]
|
result.append(msg)
|
||||||
conversation_messages = [msg for msg in self.dialogue if msg.role != "system"]
|
|
||||||
|
|
||||||
if len(conversation_messages) <= max_turns * 2:
|
if msg.role == "assistant" and msg.tool_calls:
|
||||||
return 0
|
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:
|
||||||
keep_messages = []
|
pending_tool_calls.discard(msg.tool_call_id)
|
||||||
i = len(conversation_messages) - 1
|
|
||||||
turn_count = 0
|
|
||||||
|
|
||||||
while i >= 0 and turn_count < max_turns:
|
for missing_id in pending_tool_calls:
|
||||||
msg = conversation_messages[i]
|
dummy_tool_msg = Message(
|
||||||
|
role="tool",
|
||||||
|
content='{"status": "interrupted", "message": "动作已取消/被打断"}',
|
||||||
|
tool_call_id=missing_id
|
||||||
|
)
|
||||||
|
result.append(dummy_tool_msg)
|
||||||
|
|
||||||
# 从后向前收集消息
|
return result
|
||||||
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 get_llm_dialogue_with_memory(
|
def get_llm_dialogue_with_memory(
|
||||||
self, memory_str: str = None, voiceprint_config: dict = None
|
self, memory_str: str = None, voiceprint_config: dict = None
|
||||||
@@ -135,48 +103,71 @@ class Dialogue:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if system_message:
|
if system_message:
|
||||||
# 基础系统提示
|
# 以 <context> 为分界点,拆分静态 system prompt 和动态上下文
|
||||||
enhanced_system_prompt = system_message.content
|
# 静态部分(规则、身份等)保持不变,可命中前缀缓存
|
||||||
|
# 动态部分(时间、天气、记忆等)作为第二条 system 消息,保持 system 权威性
|
||||||
|
full_prompt = system_message.content
|
||||||
|
context_match = re.search(r"<context>", 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 = ""
|
||||||
|
|
||||||
|
# 第一段:静态 system prompt(前缀缓存可命中)
|
||||||
|
dialogue.append({"role": "system", "content": static_part})
|
||||||
|
|
||||||
|
# 第二段:few-shot 示例(会话内不变,也是缓存前缀的一部分)
|
||||||
|
non_system_messages = [m for m in self.dialogue if m.role != "system"]
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 第三段:动态上下文 system prompt(时间、记忆、说话人等)
|
||||||
|
# 保持 system 角色以确保模型权威性,不降级为 user
|
||||||
|
if system_message and dynamic_part:
|
||||||
# 替换时间占位符
|
# 替换时间占位符
|
||||||
enhanced_system_prompt = enhanced_system_prompt.replace(
|
dynamic_part = dynamic_part.replace(
|
||||||
"{{current_time}}", datetime.now().strftime("%H:%M")
|
"{{current_time}}", datetime.now().strftime("%H:%M")
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加说话人个性化描述
|
# 填充记忆
|
||||||
|
if memory_str is not None:
|
||||||
|
dynamic_part = re.sub(
|
||||||
|
r"<memory>.*?</memory>",
|
||||||
|
f"<memory>\n{memory_str}\n</memory>",
|
||||||
|
dynamic_part,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 追加说话人信息
|
||||||
try:
|
try:
|
||||||
speakers = voiceprint_config.get("speakers", [])
|
speakers = voiceprint_config.get("speakers", [])
|
||||||
if speakers:
|
if speakers:
|
||||||
enhanced_system_prompt += "\n\n<speakers_info>"
|
dynamic_part += "\n<speakers_info>"
|
||||||
for speaker_str in speakers:
|
for speaker_str in speakers:
|
||||||
try:
|
try:
|
||||||
parts = speaker_str.split(",", 2)
|
parts = speaker_str.split(",", 2)
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
name = parts[1].strip()
|
name = parts[1].strip()
|
||||||
# 如果描述为空,则为""
|
|
||||||
description = (
|
description = (
|
||||||
parts[2].strip() if len(parts) >= 3 else ""
|
parts[2].strip() if len(parts) >= 3 else ""
|
||||||
)
|
)
|
||||||
enhanced_system_prompt += f"\n- {name}:{description}"
|
dynamic_part += f"\n- {name}:{description}"
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
enhanced_system_prompt += "\n\n</speakers_info>"
|
dynamic_part += "\n</speakers_info>"
|
||||||
except:
|
except:
|
||||||
# 配置读取失败时忽略错误,不影响其他功能
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 使用正则表达式匹配 <memory> 标签,不管中间有什么内容
|
dialogue.append({"role": "system", "content": dynamic_part})
|
||||||
if memory_str is not None:
|
|
||||||
enhanced_system_prompt = re.sub(
|
|
||||||
r"<memory>.*?</memory>",
|
|
||||||
f"<memory>\n{memory_str}\n</memory>",
|
|
||||||
enhanced_system_prompt,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
|
||||||
|
|
||||||
# 添加用户和助手的对话
|
# 第四段:实际对话历史(不含 few-shot)
|
||||||
for m in self.dialogue:
|
actual_messages = [m for m in non_system_messages if not m.is_temporary]
|
||||||
if m.role != "system": # 跳过原始的系统消息
|
complete_actual = self._ensure_tool_calls_complete(actual_messages)
|
||||||
self.getMessages(m, dialogue)
|
for m in complete_actual:
|
||||||
|
self.getMessages(m, dialogue)
|
||||||
|
|
||||||
return dialogue
|
return dialogue
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def play_music(conn: "ConnectionHandler", song_name: str):
|
|||||||
task.add_done_callback(handle_done)
|
task.add_done_callback(handle_done)
|
||||||
|
|
||||||
return ActionResponse(
|
return ActionResponse(
|
||||||
action=Action.NONE, result="指令已接收", response="正在为您播放音乐"
|
action=Action.RECORD, result="指令已接收", response="正在为您播放音乐"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.logger.bind(tag=TAG).error(f"处理音乐意图错误: {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}")
|
conn.logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
|
||||||
return
|
return
|
||||||
text = _get_random_play_prompt(selected_music)
|
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":
|
if conn.intent_type == "intent_llm":
|
||||||
conn.tts.tts_text_queue.put(
|
conn.tts.tts_text_queue.put(
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class Action(Enum):
|
|||||||
NONE = (1, "啥也不干")
|
NONE = (1, "啥也不干")
|
||||||
RESPONSE = (2, "直接回复")
|
RESPONSE = (2, "直接回复")
|
||||||
REQLLM = (3, "调用函数后再请求llm生成回复")
|
REQLLM = (3, "调用函数后再请求llm生成回复")
|
||||||
|
RECORD = (4, "记录工具调用到对话历史,不调用LLM")
|
||||||
|
|
||||||
def __init__(self, code, message):
|
def __init__(self, code, message):
|
||||||
self.code = code
|
self.code = code
|
||||||
|
|||||||
Reference in New Issue
Block a user