优化对话转工单功能,添加重试机制以提高稳定性,限制处理会话数量为前2000个,更新示例查询和文件路径,增强代码可读性和维护性。同时新增数据库客户端功能,支持批量处理会话数据并导出至Excel。

This commit is contained in:
2025-06-17 19:46:04 +08:00
parent a5c1548240
commit 22d48c951f
10 changed files with 718 additions and 96 deletions
+3 -1
View File
@@ -150,12 +150,14 @@ class SoftwareFunctionSlots(SlotBase):
software_name: str = Field(default="", description="软件名称")
function_name: str = Field(default="", description="具体功能名称")
operation: str = Field(default="", description="用户操作意图(如何使用功能、功能入口、功能使用场景)")
project_type: Optional[str] = Field(default="单工程", description="工程类型(单工程、多工程、批次工程)")
project_type: Optional[str] = Field(default="单工程", description="工程类型(单工程、多工程、批次工程), 未明确提及则默认下是(单工程)")
software_version: Optional[str] = Field(default="", description="软件版本")
operation_steps: Optional[str] = Field(default="", description="操作步骤描述")
def check_required_slots(self) -> Tuple[bool, Dict[str, str]]:
"""检查必填槽位是否都存在"""
if self.project_type is None or len(self.project_type) == 0:
self.project_type="单工程"
missing_slots = {}
if not self.software_name:
missing_slots["software_name"] = f"{SoftwareFunctionSlots.model_fields['software_name'].description},可选值:{', '.join([name.value for name in SoftwareName if name not in [SoftwareName.UNKNOWN, SoftwareName.ALIASES]])}"
+73 -12
View File
@@ -14,6 +14,8 @@ import json
from typing import List, Tuple, Dict, Any, Optional
import re
import jieba
import time
from .PromptTemplates import (classification_prompt, query_rewrite_prompt,
extract_nouns_prompt, classification_info,
slot_filling_prompt)
@@ -95,7 +97,9 @@ class IntentRecognizer:
except Exception as e:
raise RuntimeError(f"加载后缀关键词失败: {e}") from e
def _classify_intent(self, query: str) -> Classification:
def _classify_intent(self, query: str, conversation_context: str = "",
chat_history: List[Dict[str, str]] = None,
previous_slots: Dict[str, Any] = None) -> Classification:
"""
对用户输入进行意图分类
@@ -109,7 +113,9 @@ class IntentRecognizer:
classification_parser = PydanticOutputParser(pydantic_object=Classification)
formatted_prompt = classification_prompt.format(user_input=query,
classification_info=classification_info,
output_format=classification_parser.get_format_instructions())
output_format=classification_parser.get_format_instructions(),
conversation_context=conversation_context,
chat_history=json.dumps(chat_history, ensure_ascii=False))
# 调用LLM
response = self._llm.invoke(formatted_prompt, False)
@@ -208,7 +214,7 @@ class IntentRecognizer:
term_texts = ["名称:" + term.name + "|" + "同义词:" + ";".join(term.synonymous) for term in matched_terms]
# 使用重排序模型
xinference_reranker = SiliconFlowReRankerModel()
xinference_reranker = XinferenceReRankerModel()
rerank_results = xinference_reranker.rerank(query_key, term_texts, top_k=top_k)
# 将matched_terms转换为列表以便按索引访问
@@ -220,7 +226,7 @@ class IntentRecognizer:
return reranked_terms
except Exception as e:
raise RuntimeError(f"SiliconFlowReRankerModel重排失败:{e}") from e
raise RuntimeError(f"_rerank_matched_terms重排失败:{e}") from e
def _match_keywords(self, query: str, use_jieba: bool = False) -> Tuple[TermList, List[str]]:
"""
@@ -233,18 +239,23 @@ class IntentRecognizer:
Returns:
匹配到的关键词列表
"""
start_time = time.time()
query_keys=[]
# 步骤1: 使用LLM提取查询中的关键词
try:
llm_start_time = time.time()
extracted_terms = self._extract_keywords_with_llm(query, use_jieba)
for term in extracted_terms:
query_keys.append(term.name)
llm_end_time = time.time()
llm_time = llm_end_time - llm_start_time
except Exception as e:
raise RuntimeError(f"LLM关键词提取失败: {e}") from e
matched_terms = [] # 存储匹配到的Term对象
# 步骤2: 使用向量检索找到相似的专业名词
try:
vector_start_time = time.time()
# 对matched_terms中的每个关键字进行向量检索
for current_key in query_keys:
vector_results = self._noun_retriever.query(current_key, top_k=5, use_intersection=False)
@@ -262,12 +273,20 @@ class IntentRecognizer:
if len(current_key_terms) > 0:
reranked_terms = self._rerank_matched_terms(current_key, current_key_terms)
matched_terms.extend(reranked_terms)
vector_end_time = time.time()
vector_time = vector_end_time - vector_start_time
except Exception as e:
raise RuntimeError(f"向量检索关键词时出错: {e}") from e
# 提取所有Term对象的名称并排序
# 将set类型的matched_terms转换为TermList类型
term_list = TermList(terms=list(matched_terms))
end_time = time.time()
total_time = end_time - start_time
# 输出整合的时间日志
logging.info(f"关键词匹配耗时统计 - 总耗时: {total_time:.2f}秒, 问题关键词提取: {llm_time:.2f}秒, 向量检索+重排序: {vector_time:.2f}")
return term_list, query_keys
def _rewrite_query(self, query: str, keywords: TermList, query_keys:List[str], chat_history: List[Dict[str, str]] = None, context: str = "") -> QueryRewrite:
@@ -282,6 +301,8 @@ class IntentRecognizer:
Returns:
改写结果
"""
rewrite_start_time = time.time()
# 准备问题改写提示
# terms_dict = [term.model_dump(exclude={"description"}) for term in keywords.terms]
terms_dict = [term.model_dump() for term in keywords.terms]
@@ -295,7 +316,7 @@ class IntentRecognizer:
keywords=keywords_str,
chat_history=chat_history,
context=context)
# 调用LLM
response = self._llm.invoke(formatted_prompt, False)
@@ -303,6 +324,9 @@ class IntentRecognizer:
try:
# 尝试直接解析JSON响应
parsed_output = query_rewrite_parser.parse(response.content)
rewrite_end_time = time.time()
rewrite_time = rewrite_end_time - rewrite_start_time
logging.info(f"问题改写耗时统计 - 总耗时: {rewrite_time:.2f}")
return parsed_output
except Exception as e:
raise RuntimeError(f"解析问题改写结果时出错: {e}") from e
@@ -360,7 +384,10 @@ class IntentRecognizer:
# suffix_terms.append(suffix_term)
# return Classification(vertical_classification="安装下载", sub_classification="查询"), TermList(terms=suffix_terms), QueryRewrite(rewrite=query), matched_suffixes
if chat_history is None:
chat_history = []
if previous_slots is None:
previous_slots = {}
# 步骤1: 匹配关键词
keywords_terms, query_keys = self._match_keywords(query, use_jieba)
@@ -397,7 +424,9 @@ class IntentRecognizer:
# }
def _fill_slots(self, query: str, classification: Classification) -> Dict[str, Any]:
def _fill_slots(self, query: str, classification: Classification, conversation_context: str = "",
chat_history: List[Dict[str, str]] = None,
previous_slots: Dict[str, Any] = None,) -> Dict[str, Any]:
"""
根据分类结果对问题进行槽位填充
@@ -415,7 +444,7 @@ class IntentRecognizer:
raise RuntimeError("未找到匹配的槽位模型")
# 使用LLM进行槽位填充
filled_slots = self._fill_slots_with_llm(query, classification, slot_model)
filled_slots = self._fill_slots_with_llm(query, classification, slot_model, conversation_context, chat_history, previous_slots)
# 检查必填槽位是否都已填充
is_complete, missing_slots = filled_slots.check_required_slots()
@@ -467,7 +496,12 @@ class IntentRecognizer:
return None
def _fill_slots_with_llm(self, query: str, classification: Classification, slot_model_class: type) -> Any:
def _fill_slots_with_llm(self, query: str,
classification: Classification,
slot_model_class: type,
conversation_context: str = "",
chat_history: List[Dict[str, str]] = None,
previous_slots: Dict[str, Any] = None) -> Any:
"""
使用LLM进行槽位填充
@@ -486,7 +520,10 @@ class IntentRecognizer:
query=query,
vertical_classification=classification.vertical_classification,
sub_classification=classification.sub_classification,
output_format=slot_parser.get_format_instructions()
output_format=slot_parser.get_format_instructions(),
conversation_context=conversation_context,
chat_history=json.dumps(chat_history,ensure_ascii=False),
previous_slots=json.dumps(previous_slots,ensure_ascii=False),
)
# 调用LLM
@@ -537,9 +574,14 @@ class IntentRecognizer:
output_format=parser.get_format_instructions(),
classification_info=classification_info
)
# 调用LLM
llm_start_time = time.time()
response = self._llm.invoke(formatted_prompt + output_example, False)
llm_end_time = time.time()
llm_time = llm_end_time - llm_start_time
try:
# 解析LLM响应为JSON
result_json = parser.parse(response.content)
@@ -552,8 +594,19 @@ class IntentRecognizer:
if expected_slot_model is None:
# 添加容错处理,应对LLM返回错误分类信息,一级分类跟二级分类错乱
# 重新分类
classification = self._classify_intent(user_input)
fill_slots = self._fill_slots(user_input, classification)
classify_start_time = time.time()
classification = self._classify_intent(user_input, conversation_context, chat_history, previous_slots)
classify_end_time = time.time()
classify_time = classify_end_time - classify_start_time
# logging.info(f"重新分类耗时: {classify_time:.2f}秒")
fill_start_time = time.time()
fill_slots = self._fill_slots(user_input, classification, conversation_context, chat_history, previous_slots)
fill_end_time = time.time()
fill_time = fill_end_time - fill_start_time
all_time=fill_end_time-llm_start_time
logging.info(f"总耗时:{all_time:.2f}秒,首次槽位+分类:{llm_time:.2f}秒, 重新分类耗时: {classify_time:.2f}秒, 重新槽位填充耗时: {fill_time:.2f}")
result = {
"classification": classification.model_dump(),
"slot_filling": fill_slots
@@ -562,13 +615,21 @@ class IntentRecognizer:
return result
elif expected_slot_model.__name__ != type(slot_filling).__name__:
# 添加容错处理,应对LLM槽位与分类不匹配。重新填充槽位
fill_start_time = time.time()
slot_filling = self._fill_slots(user_input, classification)
fill_end_time = time.time()
fill_time = fill_end_time - fill_start_time
all_time=fill_end_time-llm_start_time
logging.info(f"总耗时:{all_time:.2f}秒,首次槽位+分类:{llm_time:.2f}秒, 重新槽位填充耗时: {fill_time:.2f}")
result = {
"classification": classification.model_dump(),
"slot_filling": slot_filling
}
logging.warning(f"重新填充槽点")
return result
logging.info(f"意图识别+槽位LLM调用耗时: {llm_time:.2f}")
# 构建最终结果
result = {
@@ -126,7 +126,7 @@ query_rewrite_prompt_pro_old="""
query_rewrite_prompt_pro="""
# 电力造价问答优化工程师(精简版)
**角色**:基于历史对话和专业术语库重构问题,提升知识库检索准确率。
**角色**:基于历史对话和术语库重构问题,提升知识库检索准确率。
## 核心原则
1. 语义保真 → 保持问题核心意图
@@ -135,8 +135,14 @@ query_rewrite_prompt_pro="""
## 处理流程
### 一、输入解析
- 原始问题(需保留核心语义){query}
- 关键词集合:{keywords}
- 原始问题(需保留核心语义):
<query>
{query}
</query>
- 术语库集合:
<keywords>
{keywords}
</keywords>
- 历史对话记录:
<history>
{chat_history}
@@ -159,14 +165,14 @@ graph TD
### 三、重构优先级
1. **背景补充**
- 历史对话中确定的背景信息需要保留(例:"这软件""【配网工程D3"
- 历史对话中确定的背景信息需要保留(例:"这软件""【配网工程计价通D3软件"
2. **术语处理**
- 同义词转标准词 → 批量设置定额
- 同义词转标准词 → 将提问中的同义词(synonymous)替换为标准词(name)
- 存在即标记 → 【计算式】
3. **结构优化**
- 保持原问题的5W2H特征
- 保持原问题的5W2H特征,确保问题意图不发生改变。
- 明确指代关系("该功能""【批量导入】功能"
## 输出规范
@@ -184,7 +190,7 @@ graph TD
- [] 背景信息是否合理补充?
- [] 术语标记是否完整【】?
- [] 语句是否自然流畅?
- [] 避免过度补充无关信息
- [] 避免补充无关信息
"""
@@ -349,7 +355,7 @@ def generate_slot_mapping_doc() -> str:
doc.append(f"- {sub_class} -> {slot_model}")
doc.append("\n## 【注意事项】")
doc.append("1. 分类与槽位模型必须严格对应")
doc.append("1. 分类与槽位模型必须严格对应。严格遵守,不得违背")
doc.append("2. 每个分类只能使用其对应的槽位模型")
doc.append("3. 不允许混用不同分类的槽位模型")
@@ -58,6 +58,12 @@ classification_prompt="""
用户正在使用电力造价软件或想询问电力造价领域相关知识,你需要根据用户的输入内容,将其归类为以下垂直领域之一:
{classification_info}
## 【会话背景信息】
{conversation_context}
## 【历史对话记录】
{chat_history}
【用户输入】:
{user_input}
@@ -154,6 +160,15 @@ slot_filling_prompt = """
【用户问题】
{query}
## 【会话背景信息】
{conversation_context}
## 【历史对话记录】
{chat_history}
## 【历史槽位信息】
{previous_slots}
【问题分类】
垂直领域分类: {vertical_classification}
子分类: {sub_classification}