优化对话转工单功能,添加重试机制以提高稳定性,限制处理会话数量为前2000个,更新示例查询和文件路径,增强代码可读性和维护性。同时新增数据库客户端功能,支持批量处理会话数据并导出至Excel。
This commit is contained in:
@@ -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]])}"
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user