""" 提示词加载器 支持从配置文件动态加载和管理提示词模板 """ import yaml import os from pathlib import Path from typing import Dict, Any, Optional from langchain.prompts import PromptTemplate class PromptLoader: """提示词加载器""" # 默认提示词模板 DEFAULT_PROMPTS = { "query_complexity_check": """ 作为一个智能查询分析助手,请分析用户查询的复杂度,判断该查询是否需要生成多方面的多个子查询来回答。 用户查询:{query} 请根据以下标准判断查询复杂度: 【复杂查询(Complex)特征 - 优先判断】: 1. **多问句查询**:包含多个问号(?),涉及多个不同的问题或主题 2. **跨领域查询**:涉及多个不同领域或行业的知识(如金融+技术+风险管理等) 3. **复合问题**:一个查询中包含多个子查询,即使每个子查询本身较简单 4. **关系型查询**:询问实体间的关系、比较、关联等 5. **因果推理**:询问原因、结果、影响等 6. **综合分析**:需要综合多个信息源进行分析 7. **推理链查询**:需要通过知识图谱的路径推理才能回答 8. **列表型查询**:要求列举多个项目或要素的问题 【简单查询(Simple)特征】: 1. **单一问句**:只包含一个问号,聚焦于单一主题 2. **单领域查询**:仅涉及一个明确的领域或概念 3. **直接定义查询**:询问单一概念的定义、特点、属性等 4. **单一实体信息查询**:询问某个具体事物的基本信息 5. **可以通过文档中的连续文本段落直接回答的单一问题** 请以JSON格式返回判断结果: {{ "is_complex": false, // true表示复杂查询,false表示简单查询 "complexity_level": "simple", // "simple" 或 "complex" "confidence": 0.9, // 置信度,0-1之间 "reason": "这是一个复杂查询,需要生成多方面的个子查询来回答..." }} 请确保返回有效的JSON格式。 """, "sufficiency_check": """ 作为一个智能问答助手,请判断仅从给定的信息是否已经足够回答用户的查询。 用户查询:{query} 已生成的子查询:{decomposed_sub_queries} 检索到的信息(包括原始查询和子查询的结果): {passages} 请分析这些信息是否包含足够的内容来完整回答用户的查询。注意检索结果包含两部分: 1. 【事件信息】- 来自知识图谱的事件节点,包含事件描述和上下文 2. 【段落信息】- 来自文档的段落内容,包含详细的文本描述 如果信息充分,请返回JSON格式: {{ "is_sufficient": true, "confidence": 0.9, "reason": "事件信息和段落信息包含了回答查询所需的关键内容..." }} 如果信息不充分,请返回JSON格式,并详细说明缺失的信息: {{ "is_sufficient": false, "confidence": 0.5, "reason": "检索信息缺少某些关键内容,具体包括:1) 缺少XXX的详细描述;2) 缺少XXX的具体实例;3) 缺少XXX的应用场景等..." }} 请确保返回有效的JSON格式。 """, "query_decomposition": """ 你是一个专业的查询分解助手。你的任务是将用户的复合查询分解为{num_sub_queries}个独立的、适合向量检索的子查询。 用户原始查询:{original_query} 【重要】向量检索特点: - 向量检索通过语义相似度匹配,找到与查询最相关的文档段落 - 每个子查询应该聚焦一个明确的主题或概念 - 子查询应该是完整的、可独立检索的问题 【分解策略】: 1. **识别查询结构**: - 仔细查看查询中是否有多个问号(?)分隔的不同问题 - 查找连接词:"和"、"以及"、"还有"、"另外" - 查找标点符号:句号(。)、分号(;)等分隔符 2. **按主题分解**: - 如果查询包含多个独立主题,将其分解为独立的问题 - 每个子查询保持完整性,包含必要的上下文信息 【要求】: 1. [NOTE] 必须使用自然语言,类似人类会问的问题 2. [ERROR] 禁止使用SQL语句、代码或技术查询语法 3. [SEARCH] 严格按照查询的自然分割点进行分解 4. [?] 每个子查询必须是完整的自然语言问题 5. [BLOCKED] 绝不要添加"详细信息"、"相关内容"、"补充信息"等后缀 6. [INFO] 保持原始查询中的所有关键信息(时间、地点、对象等) 7. [TARGET] 确保子查询可以独立进行向量检索 请严格按照JSON格式返回自然语言查询: {{ "sub_queries": [自然语言子查询列表] }} """, "sub_query_generation": """ 基于用户的原始查询、已有的检索结果和充分性检查反馈,生成{num_sub_queries}个相关的子查询来获取缺失信息。 原始查询:{original_query} 之前生成的子查询:{previous_sub_queries} 已有检索结果(包含事件信息和段落信息): {existing_passages} 充分性检查反馈(信息不充分的原因): {insufficiency_reason} 请根据充分性检查反馈中指出的缺失信息,生成{num_sub_queries}个具体的自然语言子查询来补充这些缺失内容。注意已有检索结果包含: 1. 【事件信息】- 来自知识图谱的事件节点 2. 【段落信息】- 来自文档的段落内容 【重要要求】: 1. [NOTE] 必须使用自然语言表达,类似人类会问的问题 2. [ERROR] 禁止使用SQL语句、代码或技术查询语法 3. [OK] 使用疑问句形式,如"什么是..."、"如何..."、"有哪些..."等 4. [TARGET] 直接针对充分性检查中指出的缺失信息 5. [LINK] 与原始查询高度相关 6. [INFO] 查询要具体明确,能够获取到具体的信息 7. [BLOCKED] 避免与之前已生成的子查询重复 8. [TARGET] 确保每个子查询都能独立检索到有价值的信息 请以JSON格式返回自然语言查询: {{ "sub_queries": [自然语言子查询列表] }} """, "simple_answer": """ 基于检索到的信息,请为用户的查询提供一个{answer_style}的答案。 用户查询:{query} 检索到的相关信息: {passages} 请基于这些信息回答用户的查询。注意检索结果包含: 1. 【事件信息】- 来自知识图谱的事件节点,提供事件相关的上下文 2. 【段落信息】- 来自文档的段落内容,提供详细的文本描述 要求: 1. 直接回答用户的查询 2. 严格基于提供的信息,不要编造内容 3. 综合利用事件信息和段落信息 4. 如果信息不足以完整回答,请明确说明 5. 答案风格:{answer_style_description} 答案: """, "edge_filter": """ 你是一个知识图谱事实过滤专家,擅长根据问题相关性筛选事实。 关键要求: 1. 你只能从提供的输入列表中选择事实 - 绝对不能创建或生成新的事实 2. 你的输出必须是输入事实的严格子集 3. 只包含与回答问题直接相关的事实 4. 如果不确定,宁可选择更少的事实,也不要选择更多 5. 保持每个事实的准确格式:[主语, 关系, 宾语] 过滤规则: - 只选择包含与问题直接相关的实体或关系的事实 - 不能修改、改写或创建输入事实的变体 - 不能添加看起来相关但不在输入中的事实 - 输出事实数量必须 ≤ 输入事实数量 返回格式为包含"fact"键的JSON对象,值为选中的事实数组。 示例: 输入事实:[["A", "关系1", "B"], ["B", "关系2", "C"], ["D", "关系3", "E"]] 问题:A和B是什么关系? 正确输出:{{"fact": [["A", "关系1", "B"]]}} 错误做法:添加新事实或修改现有事实 问题:{question} 待筛选的输入事实: {triples} 从上述输入中仅选择最相关的事实来回答问题。记住:只能是严格的子集!""", "final_answer": """ 基于所有检索到的信息,请为用户的查询提供一个{answer_style}的答案。 原始查询:{original_query} 生成的子查询:{sub_queries} 所有检索到的信息(包括原始查询和所有子查询的结果): {all_passages} 请综合所有信息,提供一个完整的答案。注意检索结果包含: 1. 【事件信息】- 来自知识图谱的事件节点,提供结构化的知识 2. 【段落信息】- 来自文档的段落内容,提供详细的描述 要求: 1. 全面回答用户的所有问题 2. 严格基于检索到的信息,不要编造内容 3. 综合利用所有事件信息和段落信息 4. 逻辑清晰,条理分明 5. 答案风格:{answer_style_description} {source_requirement} 答案: """ } def __init__(self, config_path: str = "rag_config.yaml"): """ 初始化提示词加载器 Args: config_path: 配置文件路径 """ # 优先使用环境变量指定的配置文件 env_config_path = os.environ.get('RAG_CONFIG_PATH') if env_config_path: config_path = env_config_path # 总是从项目根目录(prompt_loader.py所在目录)查找配置文件 if not os.path.isabs(config_path): # 获取本文件所在的目录(项目根目录) project_root = Path(__file__).parent self.config_path = project_root / config_path else: self.config_path = Path(config_path) self.config = self._load_config() self.prompts_cache = {} def _load_config(self) -> Dict[str, Any]: """加载配置文件""" if not self.config_path.exists(): print(f"[WARNING] 配置文件不存在: {self.config_path},使用默认提示词") return {} try: with open(self.config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) return config except Exception as e: print(f"[ERROR] 加载配置文件失败: {e},使用默认提示词") return {} def get_prompt_template(self, prompt_name: str, **kwargs) -> PromptTemplate: """ 获取提示词模板 Args: prompt_name: 提示词名称 **kwargs: 额外的格式化参数 Returns: PromptTemplate对象 """ # 检查缓存 cache_key = f"{prompt_name}_{hash(frozenset(kwargs.items()))}" if cache_key in self.prompts_cache: return self.prompts_cache[cache_key] # 获取提示词文本 prompt_text = self._get_prompt_text(prompt_name, **kwargs) # 提取输入变量 input_variables = self._extract_input_variables(prompt_text) # 创建PromptTemplate prompt_template = PromptTemplate( input_variables=input_variables, template=prompt_text ) # 缓存 self.prompts_cache[cache_key] = prompt_template return prompt_template def should_skip_llm(self, prompt_name: str) -> bool: """ 检查是否应该跳过LLM调用 Args: prompt_name: 提示词名称 Returns: True: 跳过LLM调用,使用默认响应 False: 正常调用LLM """ if 'prompt_templates' in self.config: prompt_config = self.config['prompt_templates'].get(prompt_name, {}) # 检查skip_llm配置 if prompt_config.get('skip_llm', False): print(f"[WARNING] {prompt_name} 配置skip_llm=true,跳过LLM调用") return True return False def get_default_response(self, prompt_name: str) -> dict: """ 获取跳过LLM时的默认响应 Args: prompt_name: 提示词名称 Returns: 默认响应字典 """ default_responses = { 'query_complexity_check': { "is_complex": False, # 默认简单查询 "complexity_level": "simple", "confidence": 1.0, "reason": "LLM调用已跳过,默认为简单查询" }, 'sufficiency_check': { "is_sufficient": True, # 默认充分 "confidence": 1.0, "reason": "LLM调用已跳过,默认为信息充分" }, 'query_decomposition': { "sub_queries": [] # 不分解,使用原查询 }, 'sub_query_generation': { "sub_queries": [] # 不生成新查询 }, 'simple_answer': { "answer": "基于检索到的信息直接返回" }, 'final_answer': { "answer": "基于检索到的信息直接返回" }, 'edge_filter': { "fact": [] # 跳过边过滤时,返回空列表(不过滤) } } return default_responses.get(prompt_name, {}) def _get_prompt_text(self, prompt_name: str, **kwargs) -> str: """ 获取提示词文本 Args: prompt_name: 提示词名称 **kwargs: 格式化参数 Returns: 格式化后的提示词文本 """ # 尝试从配置文件获取自定义提示词 if 'prompt_templates' in self.config: prompt_config = self.config['prompt_templates'].get(prompt_name, {}) # 如果配置中有完整的模板文本,使用它 if 'template' in prompt_config: prompt_text = prompt_config['template'] # 否则使用默认模板 else: prompt_text = self.DEFAULT_PROMPTS.get(prompt_name, "") # 应用配置参数 prompt_text = self._apply_config_params(prompt_name, prompt_text, prompt_config, **kwargs) else: # 使用默认提示词 prompt_text = self.DEFAULT_PROMPTS.get(prompt_name, "") return prompt_text def _apply_config_params(self, prompt_name: str, prompt_text: str, prompt_config: Dict[str, Any], **kwargs) -> str: """ 应用配置参数到提示词 Args: prompt_name: 提示词名称 prompt_text: 原始提示词文本 prompt_config: 提示词配置 **kwargs: 额外参数 Returns: 应用参数后的提示词文本 """ # 根据不同的提示词类型,应用不同的配置 if prompt_name == "query_decomposition": # 应用子查询数量配置 num_sub_queries = prompt_config.get('default_sub_queries', 2) prompt_text = prompt_text.replace("{num_sub_queries}", str(num_sub_queries)) elif prompt_name == "sub_query_generation": # 应用子查询数量配置 queries_per_iteration = prompt_config.get('queries_per_iteration', 2) prompt_text = prompt_text.replace("{num_sub_queries}", str(queries_per_iteration)) elif prompt_name == "simple_answer": # 应用答案风格配置 answer_style = prompt_config.get('answer_style', 'concise') answer_style_desc = self._get_answer_style_description(answer_style) prompt_text = prompt_text.replace("{answer_style}", answer_style) prompt_text = prompt_text.replace("{answer_style_description}", answer_style_desc) elif prompt_name == "final_answer": # 应用答案风格和来源引用配置 answer_style = prompt_config.get('answer_style', 'comprehensive') answer_style_desc = self._get_answer_style_description(answer_style) include_sources = prompt_config.get('include_sources', False) prompt_text = prompt_text.replace("{answer_style}", answer_style) prompt_text = prompt_text.replace("{answer_style_description}", answer_style_desc) if include_sources: source_req = "6. 在答案末尾列出信息来源(段落ID或事件ID)" else: source_req = "" prompt_text = prompt_text.replace("{source_requirement}", source_req) # 应用额外的kwargs参数 for key, value in kwargs.items(): placeholder = "{" + key + "}" if placeholder in prompt_text: prompt_text = prompt_text.replace(placeholder, str(value)) return prompt_text def _get_answer_style_description(self, style: str) -> str: """ 获取答案风格描述 Args: style: 答案风格 Returns: 风格描述文本 """ style_descriptions = { "concise": "简洁明了,重点突出,避免冗余", "detailed": "详细完整,包含所有相关信息", "balanced": "平衡简洁与详细,保持适中的信息量", "comprehensive": "全面综合,涵盖所有方面,结构清晰", "structured": "结构化呈现,使用编号、分点等形式", "summary": "摘要形式,提炼关键信息" } return style_descriptions.get(style, "清晰准确") def _extract_input_variables(self, prompt_text: str) -> list: """ 从提示词文本中提取输入变量 Args: prompt_text: 提示词文本 Returns: 输入变量列表 """ import re # 查找所有 {variable_name} 格式的变量 pattern = r'\{([^}]+)\}' matches = re.findall(pattern, prompt_text) # 过滤掉JSON格式的大括号和已知的配置参数 config_params = ['num_sub_queries', 'answer_style', 'answer_style_description', 'source_requirement'] input_vars = [] for match in matches: # 跳过包含空格、引号或JSON格式的内容 if ' ' in match or '"' in match or ':' in match or match in config_params: continue if match not in input_vars: input_vars.append(match) return input_vars def update_prompt(self, prompt_name: str, prompt_text: str): """ 动态更新提示词(运行时修改) Args: prompt_name: 提示词名称 prompt_text: 新的提示词文本 """ if 'prompt_templates' not in self.config: self.config['prompt_templates'] = {} if prompt_name not in self.config['prompt_templates']: self.config['prompt_templates'][prompt_name] = {} self.config['prompt_templates'][prompt_name]['template'] = prompt_text # 清除缓存 self.prompts_cache = {} print(f"[OK] 已更新提示词: {prompt_name}") def reload_config(self): """重新加载配置文件""" self.config = self._load_config() self.prompts_cache = {} print("[OK] 已重新加载提示词配置") def get_all_prompt_names(self) -> list: """获取所有可用的提示词名称""" return list(self.DEFAULT_PROMPTS.keys()) def print_prompt_summary(self): """打印提示词配置摘要""" print("\n" + "="*50) print("[NOTE] 提示词配置摘要") print("="*50) if 'prompt_templates' in self.config: for prompt_name, prompt_config in self.config['prompt_templates'].items(): if prompt_config.get('enabled', True): print(f"\n[OK] {prompt_name}:") for key, value in prompt_config.items(): if key not in ['enabled', 'template']: print(f" - {key}: {value}") else: print(f"\n[ERROR] {prompt_name}: 已禁用") else: print("[WARNING] 使用默认提示词配置") print("="*50) # 全局提示词加载器实例 _global_prompt_loader = None def get_prompt_loader() -> PromptLoader: """获取全局提示词加载器实例""" global _global_prompt_loader if _global_prompt_loader is None: _global_prompt_loader = PromptLoader() return _global_prompt_loader def reload_prompts(config_path: str = "rag_config.yaml"): """重新加载提示词配置""" global _global_prompt_loader _global_prompt_loader = PromptLoader(config_path) return _global_prompt_loader