From abc868225a62719fef16cd01a3c4f184fd592de9 Mon Sep 17 00:00:00 2001 From: ouyangyouzhang Date: Thu, 28 Aug 2025 14:29:19 +0800 Subject: [PATCH] =?UTF-8?q?LLM=E8=BE=93=E5=87=BA=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=85=88=E9=80=9A=E8=BF=87=E6=AD=A3=E5=88=99=E6=B8=85?= =?UTF-8?q?=E7=90=86=E5=A4=9A=E4=BD=99=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rag2_0/demo/intent_recognition_example.py | 58 ++++---------- rag2_0/intent_recognition/DataModels.py | 79 ++++++++++--------- .../intent_recognition/IntentRecognition.py | 71 +++++++++-------- rag2_0/tool/APIKeyManager.py | 2 +- rag2_0/tool/ModelTool.py | 4 +- 5 files changed, 95 insertions(+), 119 deletions(-) diff --git a/rag2_0/demo/intent_recognition_example.py b/rag2_0/demo/intent_recognition_example.py index 0f55823..2981b7e 100755 --- a/rag2_0/demo/intent_recognition_example.py +++ b/rag2_0/demo/intent_recognition_example.py @@ -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(): # 配置日志输出到控制台 diff --git a/rag2_0/intent_recognition/DataModels.py b/rag2_0/intent_recognition/DataModels.py index 60d6cec..1ed0085 100755 --- a/rag2_0/intent_recognition/DataModels.py +++ b/rag2_0/intent_recognition/DataModels.py @@ -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="从不同角度生成的子问题列表") \ No newline at end of file diff --git a/rag2_0/intent_recognition/IntentRecognition.py b/rag2_0/intent_recognition/IntentRecognition.py index 06d6977..14cbe9e 100755 --- a/rag2_0/intent_recognition/IntentRecognition.py +++ b/rag2_0/intent_recognition/IntentRecognition.py @@ -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) 去除段 + 3) 通过第一个"{"与最后一个"}"裁剪内容,移除首尾多余字符 + 4) 压缩所有空白字符 + """ + content = content.strip() + content = re.sub(r'.*?', '', 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'.*?', '', 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'.*?', '', 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'.*?', '', 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'.*?', '', 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'.*?', '', 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'.*?', '', 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响应 diff --git a/rag2_0/tool/APIKeyManager.py b/rag2_0/tool/APIKeyManager.py index 2926f0d..0767e77 100755 --- a/rag2_0/tool/APIKeyManager.py +++ b/rag2_0/tool/APIKeyManager.py @@ -30,7 +30,7 @@ class APIKeyManager: # 密钥使用计数和上次使用时间 _key_usage: Dict[str, Dict] = {} # 当前正在使用的密钥索引 - _current_index = 0 + _current_index = -1 api_file_path = "api_key.txt" diff --git a/rag2_0/tool/ModelTool.py b/rag2_0/tool/ModelTool.py index d223f42..be9daad 100755 --- a/rag2_0/tool/ModelTool.py +++ b/rag2_0/tool/ModelTool.py @@ -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"""