feat: 统一LLM错误处理并添加系统错误回复配置

在多个LLM提供者中移除try-catch块,将错误处理统一到connection.py的流处理层
添加system_error_response配置项,支持自定义系统错误时的回复内容
在意图识别和流处理中捕获异常时返回配置的错误回复,避免硬编码错误信息

Fixes #2075
This commit is contained in:
huozaimengli
2026-01-25 16:48:01 +08:00
parent 275102f5b7
commit 6ae0af278b
13 changed files with 542 additions and 551 deletions
+3
View File
@@ -202,6 +202,9 @@ prompt: |
# 默认系统提示词模板文件 # 默认系统提示词模板文件
prompt_template: agent-base-prompt.txt prompt_template: agent-base-prompt.txt
# 系统错误时的回复
system_error_response: "主人,小智现在有点忙,我们稍后再试吧。"
# 结束语prompt # 结束语prompt
end_prompt: end_prompt:
enable: true # 是否开启结束语 enable: true # 是否开启结束语
+23
View File
@@ -40,8 +40,10 @@ from config.logger import setup_logging, build_module_string, create_connection_
from config.manage_api_client import DeviceNotFoundException, DeviceBindException from config.manage_api_client import DeviceNotFoundException, DeviceBindException
from core.utils.prompt_manager import PromptManager from core.utils.prompt_manager import PromptManager
from core.utils.voiceprint_provider import VoiceprintProvider from core.utils.voiceprint_provider import VoiceprintProvider
from core.utils.util import get_system_error_response
from core.utils import textUtils from core.utils import textUtils
TAG = __name__ TAG = __name__
auto_import_modules("plugins_func.functions") auto_import_modules("plugins_func.functions")
@@ -869,6 +871,7 @@ class ConnectionHandler:
content_arguments = "" content_arguments = ""
self.client_abort = False self.client_abort = False
emotion_flag = True emotion_flag = True
try:
for response in llm_responses: for response in llm_responses:
if self.client_abort: if self.client_abort:
break break
@@ -909,6 +912,26 @@ class ConnectionHandler:
content_detail=content, content_detail=content,
) )
) )
except Exception as e:
self.logger.bind(tag=TAG).error(f"LLM stream processing error: {e}")
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.TEXT,
content_detail=get_system_error_response(self.config),
)
)
if depth == 0:
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.ACTION,
)
)
self.llm_finish_task = True
return
# 处理function call # 处理function call
if tool_call_flag: if tool_call_flag:
bHasError = False bHasError = False
@@ -2,11 +2,14 @@ from typing import List, Dict
from ..base import IntentProviderBase from ..base import IntentProviderBase
from plugins_func.functions.play_music import initialize_music_handler from plugins_func.functions.play_music import initialize_music_handler
from config.logger import setup_logging from config.logger import setup_logging
from core.utils.util import get_system_error_response
import re import re
import json import json
import hashlib import hashlib
import time import time
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -115,12 +118,16 @@ class IntentProvider(IntentProviderBase):
return prompt return prompt
def replyResult(self, text: str, original_text: str): def replyResult(self, text: str, original_text: str):
try:
llm_result = self.llm.response_no_stream( llm_result = self.llm.response_no_stream(
system_prompt=text, system_prompt=text,
user_prompt="请根据以上内容,像人类一样说话的口吻回复用户,要求简洁,请直接返回结果。用户现在说:" user_prompt="请根据以上内容,像人类一样说话的口吻回复用户,要求简洁,请直接返回结果。用户现在说:"
+ original_text, + original_text,
) )
return llm_result return llm_result
except Exception as e:
logger.bind(tag=TAG).error(f"Error in generating reply result: {e}")
return get_system_error_response(self.config)
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str: async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
if not self.llm: if not self.llm:
@@ -194,9 +201,13 @@ class IntentProvider(IntentProviderBase):
llm_start_time = time.time() llm_start_time = time.time()
logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}") logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}")
try:
intent = self.llm.response_no_stream( intent = self.llm.response_no_stream(
system_prompt=prompt_music, user_prompt=user_prompt system_prompt=prompt_music, user_prompt=user_prompt
) )
except Exception as e:
logger.bind(tag=TAG).error(f"Error in intent detection LLM call: {e}")
return '{"function_call": {"name": "continue_chat"}}'
# 记录LLM调用完成时间 # 记录LLM调用完成时间
llm_time = time.time() - llm_start_time llm_time = time.time() - llm_start_time
@@ -21,7 +21,6 @@ class LLMProvider(LLMProviderBase):
check_model_key("AliBLLLM", self.api_key) check_model_key("AliBLLLM", self.api_key)
def response(self, session_id, dialogue): def response(self, session_id, dialogue):
try:
# 处理dialogue # 处理dialogue
if self.is_No_prompt: if self.is_No_prompt:
dialogue.pop(0) dialogue.pop(0)
@@ -95,10 +94,6 @@ class LLMProvider(LLMProviderBase):
if chunk: if chunk:
yield chunk yield chunk
except Exception as e:
logger.bind(tag=TAG).error(f"【阿里百练API服务】响应异常: {e}")
yield "【LLM服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
# 阿里百练当前未支持原生的 function call。为保持兼容,这里回退到普通文本流式输出。 # 阿里百练当前未支持原生的 function call。为保持兼容,这里回退到普通文本流式输出。
# 上层会按 (content, tool_calls) 的形式消费,这里始终返回 (token, None) # 上层会按 (content, tool_calls) 的形式消费,这里始终返回 (token, None)
@@ -11,7 +11,6 @@ class LLMProviderBase(ABC):
pass pass
def response_no_stream(self, system_prompt, user_prompt, **kwargs): def response_no_stream(self, system_prompt, user_prompt, **kwargs):
try:
# 构造对话格式 # 构造对话格式
dialogue = [ dialogue = [
{"role": "system", "content": system_prompt}, {"role": "system", "content": system_prompt},
@@ -22,10 +21,6 @@ class LLMProviderBase(ABC):
result += part result += part
return result return result
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
return "【LLM服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
""" """
Default implementation for function calling (streaming) Default implementation for function calling (streaming)
@@ -20,7 +20,6 @@ class LLMProvider(LLMProviderBase):
logger.bind(tag=TAG).error(model_key_msg) logger.bind(tag=TAG).error(model_key_msg)
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try:
# 取最后一条用户消息 # 取最后一条用户消息
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user") last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
conversation_id = self.session_conversation_map.get(session_id) conversation_id = self.session_conversation_map.get(session_id)
@@ -87,10 +86,6 @@ class LLMProvider(LLMProviderBase):
): ):
yield event["answer"] yield event["answer"]
except Exception as e:
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
yield "【服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
if len(dialogue) == 2 and functions is not None and len(functions) > 0: if len(dialogue) == 2 and functions is not None and len(functions) > 0:
# 第一次调用llm, 取最后一条用户消息,附加tool提示词 # 第一次调用llm, 取最后一条用户消息,附加tool提示词
@@ -19,7 +19,6 @@ class LLMProvider(LLMProviderBase):
logger.bind(tag=TAG).error(model_key_msg) logger.bind(tag=TAG).error(model_key_msg)
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try:
# 取最后一条用户消息 # 取最后一条用户消息
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user") last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
@@ -63,10 +62,6 @@ class LLMProvider(LLMProviderBase):
except Exception as e: except Exception as e:
continue continue
except Exception as e:
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
yield "【服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
logger.bind(tag=TAG).error( logger.bind(tag=TAG).error(
f"fastgpt暂未实现完整的工具调用(function call),建议使用其他意图识别" f"fastgpt暂未实现完整的工具调用(function call),建议使用其他意图识别"
@@ -15,7 +15,6 @@ class LLMProvider(LLMProviderBase):
self.api_url = f"{self.base_url}/api/conversation/process" # 拼接完整的 API URL self.api_url = f"{self.base_url}/api/conversation/process" # 拼接完整的 API URL
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try:
# home assistant语音助手自带意图,无需使用xiaozhi ai自带的,只需要把用户说的话传递给home assistant即可 # home assistant语音助手自带意图,无需使用xiaozhi ai自带的,只需要把用户说的话传递给home assistant即可
# 提取最后一个 role 为 'user' 的 content # 提取最后一个 role 为 'user' 的 content
@@ -60,11 +59,6 @@ class LLMProvider(LLMProviderBase):
else: else:
logger.bind(tag=TAG).warning("API 返回数据中没有 speech 内容") logger.bind(tag=TAG).warning("API 返回数据中没有 speech 内容")
except RequestException as e:
logger.bind(tag=TAG).error(f"HTTP 请求错误: {e}")
except Exception as e:
logger.bind(tag=TAG).error(f"生成响应时出错: {e}")
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
logger.bind(tag=TAG).error( logger.bind(tag=TAG).error(
f"homeassistant不支持(function call),建议使用其他意图识别" f"homeassistant不支持(function call),建议使用其他意图识别"
@@ -25,7 +25,6 @@ class LLMProvider(LLMProviderBase):
self.is_qwen3 = self.model_name and self.model_name.lower().startswith("qwen3") self.is_qwen3 = self.model_name and self.model_name.lower().startswith("qwen3")
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try:
# 如果是qwen3模型,在用户最后一条消息中添加/no_think指令 # 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
if self.is_qwen3: if self.is_qwen3:
# 复制对话列表,避免修改原始对话 # 复制对话列表,避免修改原始对话
@@ -89,12 +88,7 @@ class LLMProvider(LLMProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"Error processing chunk: {e}") logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama response generation: {e}")
yield "【Ollama服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
try:
# 如果是qwen3模型,在用户最后一条消息中添加/no_think指令 # 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
if self.is_qwen3: if self.is_qwen3:
# 复制对话列表,避免修改原始对话 # 复制对话列表,避免修改原始对话
@@ -169,7 +163,3 @@ class LLMProvider(LLMProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"Error processing function chunk: {e}") logger.bind(tag=TAG).error(f"Error processing function chunk: {e}")
continue continue
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
yield f"【Ollama服务响应异常: {str(e)}", None
@@ -56,7 +56,6 @@ class LLMProvider(LLMProviderBase):
return dialogue return dialogue
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try:
dialogue = self.normalize_dialogue(dialogue) dialogue = self.normalize_dialogue(dialogue)
request_params = { request_params = {
@@ -77,6 +76,7 @@ class LLMProvider(LLMProviderBase):
if value is not None: if value is not None:
request_params[key] = value request_params[key] = value
# raise ValueError("model_name is required")
responses = self.client.chat.completions.create(**request_params) responses = self.client.chat.completions.create(**request_params)
is_active = True is_active = True
@@ -96,11 +96,7 @@ class LLMProvider(LLMProviderBase):
if is_active: if is_active:
yield content yield content
except Exception as e:
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
def response_with_functions(self, session_id, dialogue, functions=None, **kwargs): def response_with_functions(self, session_id, dialogue, functions=None, **kwargs):
try:
dialogue = self.normalize_dialogue(dialogue) dialogue = self.normalize_dialogue(dialogue)
request_params = { request_params = {
@@ -136,7 +132,3 @@ class LLMProvider(LLMProviderBase):
f"输出 {getattr(usage_info, 'completion_tokens', '未知')}" f"输出 {getattr(usage_info, 'completion_tokens', '未知')}"
f"共计 {getattr(usage_info, 'total_tokens', '未知')}" f"共计 {getattr(usage_info, 'total_tokens', '未知')}"
) )
except Exception as e:
logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
yield f"【OpenAI服务响应异常: {e}", None
@@ -31,7 +31,6 @@ class LLMProvider(LLMProviderBase):
raise raise
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try:
logger.bind(tag=TAG).debug( logger.bind(tag=TAG).debug(
f"Sending request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}" f"Sending request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}"
) )
@@ -59,12 +58,7 @@ class LLMProvider(LLMProviderBase):
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"Error processing chunk: {e}") logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Xinference response generation: {e}")
yield "【Xinference服务响应异常】"
def response_with_functions(self, session_id, dialogue, functions=None): def response_with_functions(self, session_id, dialogue, functions=None):
try:
logger.bind(tag=TAG).debug( logger.bind(tag=TAG).debug(
f"Sending function call request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}" f"Sending function call request to Xinference with model: {self.model_name}, dialogue length: {len(dialogue)}"
) )
@@ -89,10 +83,3 @@ class LLMProvider(LLMProviderBase):
yield content, tool_calls yield content, tool_calls
elif tool_calls: elif tool_calls:
yield None, tool_calls yield None, tool_calls
except Exception as e:
logger.bind(tag=TAG).error(f"Error in Xinference function call: {e}")
yield {
"type": "content",
"content": f"【Xinference服务响应异常: {str(e)}",
}
@@ -162,6 +162,7 @@ class MemoryProvider(MemoryProviderBase):
msgStr += f"当前时间:{time_str}" msgStr += f"当前时间:{time_str}"
if self.save_to_file: if self.save_to_file:
try:
result = self.llm.response_no_stream( result = self.llm.response_no_stream(
short_term_memory_prompt, short_term_memory_prompt,
msgStr, msgStr,
@@ -169,12 +170,11 @@ class MemoryProvider(MemoryProviderBase):
temperature=0.2, temperature=0.2,
) )
json_str = extract_json_data(result) json_str = extract_json_data(result)
try:
json.loads(json_str) # 检查json格式是否正确 json.loads(json_str) # 检查json格式是否正确
self.short_memory = json_str self.short_memory = json_str
self.save_memory_to_file() self.save_memory_to_file()
except Exception as e: except Exception as e:
print("Error:", e) logger.bind(tag=TAG).error(f"Error in saving memory: {e}")
else: else:
# 当save_to_file为False时,调用Java端的聊天记录总结接口 # 当save_to_file为False时,调用Java端的聊天记录总结接口
summary_id = session_id if session_id else self.role_id summary_id = session_id if session_id else self.role_id
+11
View File
@@ -573,3 +573,14 @@ def validate_mcp_endpoint(mcp_endpoint: str) -> bool:
return False return False
return True return True
def get_system_error_response(config: dict) -> str:
"""获取系统错误时的回复
Args:
config: 配置字典
Returns:
str: 系统错误时的回复
"""
return config.get("system_error_response", "主人,小智现在有点忙,我们稍后再试吧。")