mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
perf:
1、优化工具调用相关提示词,拆分静态system-prompt和动态上下文。 2、新增few-shot示例,使用"调用→结果→回复"流程闭环,显著提高多次会话后工具调用准确性。 3、去除对话标题总结和记忆的模型思考模式。
This commit is contained in:
+33
-2
@@ -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<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
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
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_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);
|
||||
|
||||
@@ -27,8 +27,9 @@ You are a playful, expressive, empathetic, and highly emotionally intelligent co
|
||||
</tts_format_constraints>
|
||||
|
||||
<tool_and_knowledge>
|
||||
1. 【工具防骚扰】你善于使用各类工具辅助回答。但是,针对【查新闻】和【播放音乐】这两类打扰性较强的功能,必须在经过用户明确同意或主动要求后才可以调用!严禁不解风情地主动播放。
|
||||
2. 【无网兜底】你没有联网实时搜索功能(工具除外)。不懂或者不确定的事情,必须大大方方直接说“不知道”,绝不胡编乱造产生幻觉。
|
||||
你有工具可用。能被工具处理的请求必须调工具,禁止自行编造结果;没有对应工具的才用自身知识回答。
|
||||
必须调工具:播放音乐→play_music | 控制设备→hass工具 | 查新闻/天气→查询工具 | 告别→handle_exit_intent
|
||||
禁止这样做:用户说”放首歌”→你直接回答”好的我给你唱”(错!必须调play_music,用工具实际结果回复)
|
||||
</tool_and_knowledge>
|
||||
|
||||
<safety_compliance>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
# 以 <context> 为分界点,拆分静态 system prompt 和动态上下文
|
||||
# 静态部分(规则、身份等)保持不变,可命中前缀缓存
|
||||
# 动态部分(时间、天气、记忆等)放到对话末尾的 user 消息中
|
||||
# 动态部分(时间、天气、记忆等)作为第二条 system 消息,保持 system 权威性
|
||||
full_prompt = system_message.content
|
||||
context_match = re.search(r"<context>", 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
|
||||
|
||||
Reference in New Issue
Block a user