LLM输出的内容先通过正则清理多余内容

This commit is contained in:
2025-08-28 14:29:19 +08:00
parent 660b5f6561
commit abc868225a
5 changed files with 95 additions and 119 deletions
+15 -43
View File
@@ -204,41 +204,7 @@ class QueryRewriteProcessor:
enable_query_expansion=True,
use_jieba=True,
cur_soft_name=current_softname))
# 提取分类信息
classification = result["classification"]
original_query = result["rewrite"]["rewrite"]
query_list = result["query_expand"]["all"]
# 将字典转换为Classification对象
classification_obj = Classification(**classification)
# 根据enable_retrieval参数决定是否进行文档检索
retrieved_doc = None
retrieved_doc_titles=[]
if retrieved_doc:
retrieved_doc_titles=[doc["title"].split("/")[-1] for doc in retrieved_doc]
# 提取槽位填充信息
slot_filling = result.get("slot_filling", {})
slot_filling_str = ""
if slot_filling and "filled_data" in slot_filling:
# 格式化槽位填充结果
slot_filling_str = json.dumps({
"is_complete": slot_filling.get("is_complete", False),
"missing_slots": slot_filling.get("missing_slots", {}),
"filled_data": slot_filling.get("filled_data", {})
}, ensure_ascii=False, indent=2)
# 处理成功,返回结果
return {
"问题": query,
"问题分类": f"{classification['vertical_classification']} - {classification['sub_classification']}",
"问题改写": result["rewrite"]["rewrite"],
"槽位信息": slot_filling_str,
"检索的文档": "\n".join(retrieved_doc_titles),
"检索的内容": json.dumps(retrieved_doc, ensure_ascii=False, indent=2) if retrieved_doc else "",
}
return result
except Exception as e:
logging.error(f"处理问题 '{query}' 时出错: ",exc_info=True)
retry_count += 1
@@ -424,19 +390,25 @@ def main():
logging.info(f"所有处理完成,最终结果已保存至: {output_file}")
else:
logging.info(f"文档检索功能状态: 已启用")
for idx, query in enumerate(examples):
if query.strip() == "":
continue
query="怎么调整报表顺序"
nCount=0
while True:
query="请问怎么在南网版概算工程软件版里面查施工费?"
conversation_context={
"current_softname": "储能计价通C1软件"
"current_softname": "电力建设计价通软件"
}
# 在调试模式下使用完整的参数
print(json.dumps(processor.process_query(
result = processor.process_query(
query,
conversation_context=conversation_context,
enable_retrieval=True
), ensure_ascii=False, indent=2))
)
# 在调试模式下使用完整的参数
# print(json.dumps(processor.process_query(
# query,
# conversation_context=conversation_context,
# enable_retrieval=True
# ), ensure_ascii=False, indent=2))
nCount+=1
print("测试数量: ", nCount)
def setup_logging():
# 配置日志输出到控制台
+40 -39
View File
@@ -132,9 +132,48 @@ class QueryRewrite(BaseModel):
"rewrite": "问题改写"
}
字段说明:
rewrite 类型:str 描述:问题改写之后的内容
"rewrite" 类型:str 描述:问题改写之后的内容
"""
# 意图优化环节数据模型
class StepBackPrompt(BaseModel):
"""后退提示数据模型"""
original_query: str = Field(description="原始查询")
can_use_back_prompt: bool = Field(description="原始查询是否可以进行后退提示(true/false),如果原始查询没有限定词或其他限定词语,则不能进行后退提示")
step_back_query: List[str] = Field(description="后退提示生成的抽象查询(多个)")
@classmethod
def get_format_instructions(cls):
return """
格式如下,必须严格以纯JSON格式输出
{
"original_query": "原始查询",
"can_use_back_prompt": "原始查询是否可以进行后退提示(true/false),如果原始查询没有限定词或其他限定词语,则不能进行后退提示",
"step_back_query": "后退提示生成的抽象查询(多个)"
}
字段说明:
"original_query" 类型:str 描述:用户输入的原始查询
"can_use_back_prompt" 类型:bool 描述:原始查询是否可以进行后退提示(true/false),如果原始查询没有限定词或其他限定词语,则不能进行后退提示
"step_back_query" 类型:list[str] 描述:后退提示生成的抽象查询(多个)
"""
class FollowUpQuestions(BaseModel):
"""后续问题数据模型"""
original_query: str = Field(description="原始查询")
follow_up_query: str = Field(description="基于历史对话生成的独立问题")
class HypotheticalDocument(BaseModel):
"""假设文档数据模型"""
original_query: str = Field(description="原始查询")
hypothetical_answer: str = Field(description="假设性回答")
class MultiQuestions(BaseModel):
"""多问题查询数据模型"""
original_query: str = Field(description="原始查询")
sub_questions: List[str] = Field(description="从不同角度生成的子问题列表")
##########################槽位模型###########################
class SlotBase(BaseModel):
"""槽位基础模型"""
@@ -329,41 +368,3 @@ class IntentAndSlotResult(BaseModel):
ProblemDiagnosisSlots,
OtherSlots
]
# 意图优化环节数据模型
class StepBackPrompt(BaseModel):
"""后退提示数据模型"""
original_query: str = Field(description="原始查询")
can_use_back_prompt: bool = Field(description="原始查询是否可以进行后退提示(true/false),如果原始查询没有限定词或其他限定词语,则不能进行后退提示")
step_back_query: List[str] = Field(description="后退提示生成的抽象查询(多个)")
@classmethod
def get_format_instructions(cls):
return """
格式如下,必须严格以纯JSON格式输出
{
"original_query": "原始查询",
"can_use_back_prompt": "原始查询是否可以进行后退提示(true/false),如果原始查询没有限定词或其他限定词语,则不能进行后退提示",
"step_back_query": "后退提示生成的抽象查询(多个)"
}
字段说明:
original_query 类型:str 描述:用户输入的原始查询
can_use_back_prompt 类型:bool 描述:原始查询是否可以进行后退提示(true/false),如果原始查询没有限定词或其他限定词语,则不能进行后退提示
step_back_query 类型:List[str] 描述:后退提示生成的抽象查询(多个)
"""
class FollowUpQuestions(BaseModel):
"""后续问题数据模型"""
original_query: str = Field(description="原始查询")
follow_up_query: str = Field(description="基于历史对话生成的独立问题")
class HypotheticalDocument(BaseModel):
"""假设文档数据模型"""
original_query: str = Field(description="原始查询")
hypothetical_answer: str = Field(description="假设性回答")
class MultiQuestions(BaseModel):
"""多问题查询数据模型"""
original_query: str = Field(description="原始查询")
sub_questions: List[str] = Field(description="从不同角度生成的子问题列表")
+37 -34
View File
@@ -170,6 +170,24 @@ class AsyncIntentRecognizer:
except Exception as e:
raise RuntimeError(f"加载后缀关键词失败: {e}") from e
def _clean_llm_json(self, content: str) -> str:
"""
统一清洗LLM返回的JSON字符串:
1) 去首尾空白
2) 去除<think>…</think>段
3) 通过第一个"{"与最后一个"}"裁剪内容,移除首尾多余字符
4) 压缩所有空白字符
"""
content = content.strip()
content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL)
# 裁剪到最外层花括号范围
start = content.find('{')
end = content.rfind('}')
if start != -1 and end != -1 and start < end:
content = content[start:end+1]
content = re.sub(r'\s+', '', content)
return content
async def _classify_intent_async(self, query: str, conversation_context: str = "",
chat_history: List[Dict[str, str]] = None,
previous_slots: Dict[str, Any] = None) -> Classification:
@@ -194,13 +212,11 @@ class AsyncIntentRecognizer:
# 解析输出
try:
# 异步调用LLM
response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"})
# 尝试直接解析JSON响应
response.content = response.content.strip()
clean_output = re.sub(r'<think>.*?</think>', '', response.content, flags=re.DOTALL)
clean_output = re.sub(r'\s+', '', clean_output)
clean_output = self._clean_llm_json(response.content)
parsed_output = classification_parser.parse(clean_output)
# 计算并打印耗时
@@ -264,13 +280,11 @@ class AsyncIntentRecognizer:
formatted_prompt = formatted_prompt.replace("{output_format}", terms_list_parser.get_format_instructions())
# 异步调用LLM
response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"})
# 尝试使用Pydantic解析器解析TermList
response.content = response.content.strip()
clean_output = re.sub(r'<think>.*?</think>', '', response.content, flags=re.DOTALL)
clean_output = re.sub(r'\s+', '', clean_output)
clean_output = self._clean_llm_json(response.content)
parsed_output = terms_list_parser.parse(clean_output)
return parsed_output.terms
@@ -341,20 +355,16 @@ class AsyncIntentRecognizer:
1、请从当前提问内容中提取电力造价行中定额编码、定额名称、清单编码、清单名称
2、请勿随机编造,如果没有提取到内容返回空的JSON
3、返回结果为json格式,必须严格以纯JSON格式输出
```json
{{
"dinge_info_list":{{"dinge_code_list":["xxxx","xxxx"], "dinge_name_list":["xxxx","xxxx"]}},
"qingdan_info":{{"qingdan_code_list":["xxxx","xxxx"], "qingdan_name_list":["xxxx","xxxx"]}}
}}
```json
"""
try:
response = await self._llm.ainvoke(prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(prompt, extra_body={"enable_thinking": False})
response.content = response.content.strip()
clean_output = re.sub(r'<think>.*?</think>', '', response.content, flags=re.DOTALL)
clean_output = re.sub(r'\s+', '', clean_output)
# response = await self._llm.ainvoke(prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
response = await self._llm.ainvoke(prompt, response_format={"type": "json_object"})
clean_output = self._clean_llm_json(response.content)
parsed_output = JsonOutputParser().parse(clean_output)
# 计算并打印耗时
@@ -393,11 +403,9 @@ class AsyncIntentRecognizer:
# 解析输出
try:
# 异步调用LLM
response = await self._llm.ainvoke(formatted_prompt,response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, extra_body={"enable_thinking": False})
response.content = response.content.strip()
clean_output = re.sub(r'<think>.*?</think>', '', response.content, flags=re.DOTALL)
clean_output = re.sub(r'\s+', '', clean_output)
# response = await self._llm.ainvoke(formatted_prompt,response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
response = await self._llm.ainvoke(formatted_prompt,response_format={"type": "json_object"})
clean_output = self._clean_llm_json(response.content)
parsed_output = query_rewrite_parser.parse(clean_output)
end_time = time.time()
process_time=end_time-start_time
@@ -642,9 +650,7 @@ class AsyncIntentRecognizer:
# 异步调用LLM
response = await self._llm.ainvoke(formatted_prompt,response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, extra_body={"enable_thinking": False})
response.content = response.content.strip()
clean_output = re.sub(r'<think>.*?</think>', '', response.content, flags=re.DOTALL)
clean_output = re.sub(r'\s+', '', clean_output)
clean_output = self._clean_llm_json(response.content)
# 尝试解析LLM响应
parsed_output = slot_parser.parse(clean_output)
return parsed_output
@@ -677,22 +683,18 @@ class AsyncIntentRecognizer:
try:
# 异步调用LLM
response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
response = await self._llm.ainvoke(formatted_prompt, response_format={"type": "json_object"})
# 解析输出
response.content = response.content.strip()
clean_output = re.sub(r'<think>.*?</think>', '', response.content, flags=re.DOTALL)
clean_output = re.sub(r'\s+', '', clean_output)
clean_output = self._clean_llm_json(response.content)
parsed_output = step_back_parser.parse(clean_output)
step_back_end_time = time.time()
step_back_time = step_back_end_time - step_back_start_time
logging.info(f"后退提示生成耗时统计 - 总耗时: {step_back_time:.2f}")
return parsed_output
except Exception as e:
# 如果解析失败,返回原始查询作为后退提示
logging.error(f"后退提示生成失败: {e}", exc_info=True)
return StepBackPrompt(original_query=query, can_use_back_prompt=False, step_back_query=[query])
raise RuntimeError(f"解析后退提示结果时出错: {e}") from e
async def _find_matching_software_docs_async(self, query: str, soft_name: str,
chat_history: List[Dict[str, str]] = None,
@@ -739,7 +741,8 @@ class AsyncIntentRecognizer:
try:
# 异步调用LLM
start_time = time.time()
response = await self._llm.ainvoke(prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
# response = await self._llm.ainvoke(prompt, response_format={"type": "json_object"}, extra_body={"enable_thinking": False})
response = await self._llm.ainvoke(prompt, response_format={"type": "json_object"})
end_time = time.time()
# 解析JSON响应
+1 -1
View File
@@ -30,7 +30,7 @@ class APIKeyManager:
# 密钥使用计数和上次使用时间
_key_usage: Dict[str, Dict] = {}
# 当前正在使用的密钥索引
_current_index = 0
_current_index = -1
api_file_path = "api_key.txt"
+2 -2
View File
@@ -200,7 +200,7 @@ class OpenAiLLM:
except Exception as e:
retry_count += 1
if retry_count == max_retries:
raise RuntimeError(f"OpenAiLLM:invoke:error:{str(e)}.api_key:{api_key}") from e
raise RuntimeError(f"OpenAiLLM:invoke:error:{str(e)}") from e
else:
time.sleep(5*retry_count) # 重试前等待5秒*重试次数
else:
@@ -215,7 +215,7 @@ class OpenAiLLM:
)
return completion.choices[0].message
except Exception as e:
raise RuntimeError(f"OpenAiLLM:invoke:error:{str(e)}.api_key:{api_key}") from e
raise RuntimeError(f"OpenAiLLM:invoke:error:{str(e)}") from e
async def ainvoke(self, user_prompt="你是谁?", **extra_kwargs):
"""异步调用OpenAI API"""