1 line
181 KiB
JSON
1 line
181 KiB
JSON
{"repo": "andcher3/advanced-technologies-of-china-graph-database-construction", "files": {"neo4j_query_executor.py": {"b7da1189e522da68d660f70e23d475467ef9bf3e": {"t": 1785745438.3993764, "n": 20474, "h": "a0e41937adaee171", "c": "import os\nimport re\nfrom typing import List, Tuple, Optional, Any, Dict\n\nfrom neo4j.graph import Node, Relationship, Path\nfrom neo4j import GraphDatabase, Record\n\n# --- Neo4j 连接信息 (需要根据你的实际情况修改) ---\n# 建议从配置文件或环境变量读取\nNEO4J_URI = \"bolt://1.92.129.26:2223\"\nNEO4J_USER = \"neo4j\"\nNEO4J_PASSWORD = \"123456788\"\n\n# 全局 Driver 实例\nneo4j_driver: Optional[GraphDatabase.driver] = None\n# 全局 LLM 客户端实例\nllm_client_instance: Optional[Any] = None # 使用 Any 类型,因为具体类型取决于导入的库\n\ndef get_neo4j_driver():\n \"\"\"获取或初始化 Neo4j Driver 实例\"\"\"\n global neo4j_driver\n if neo4j_driver is None:\n try:\n # 可以根据需要调整连接池大小等参数\n neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n # 验证连接\n neo4j_driver.verify_connectivity()\n print(\"Neo4j Driver 初始化成功。\")\n except Exception as e:\n print(f\"Neo4j Driver 初始化或连接失败: {e}\")\n neo4j_driver = None\n return neo4j_driver\n\n\ndef close_neo4j_driver():\n \"\"\"关闭 Neo4j Driver 连接\"\"\"\n global neo4j_driver\n if neo4j_driver:\n neo4j_driver.close()\n neo4j_driver = None\n print(\"Neo4j Driver 已关闭。\")\n\n\n# --- 结果格式化函数 ---\ndef _format_neo4j_value_for_llm(value: Any) -> str:\n \"\"\"\n 将 Neo4j 返回的不同类型的值格式化为适合大模型阅读的字符串表示。\n 尝试识别常见的节点ID属性(如 name, title, paper_id)。\n \"\"\"\n if isinstance(value, Node):\n labels = \":\".join(list(value.labels))\n properties_str_parts = []\n # 尝试获取主要的标识属性\n if 'name' in value:\n properties_str_parts.append(f\"name: '{value['name']}'\")\n if 'title' in value:\n properties_str_parts.append(f\"title: '{value['title']}'\")\n if 'paper_id' in value: # 假设你的 paper_id 属性\n properties_str_parts.append(f\"paper_id: '{value['paper_id']}'\")\n # 可以添加更多常见的标识属性\n\n # 如果没有找到标识属性,或者想包含更多属性,可以遍历所有属性\n for prop_key, prop_value in value.items():\n if prop_key not in ['name', 'title', 'paper_id']: # 避免重复添加\n # 递归格式化属性值,注意避免无限递归复杂对象\n properties_str_parts.append(f\"{prop_key}: '{prop_value}'\") # 简化处理,直接转字符串可能不够\n\n properties_str = \", \".join(properties_str_parts)\n return f\"({labels} {{{properties_str}}})\" if properties_str else f\"({labels})\"\n\n elif isinstance(value, Relationship):\n # 只显示关系类型\n return f\"-[:{value.type}]-\"\n # 如果需要包含关系属性,会更复杂\n # return f\"-[:{value.type} {{...}}]-\"\n\n elif isinstance(value, Path):\n path_parts = []\n for i, item in enumerate(value):\n if i % 2 == 0: # 节点\n path_parts.append(_format_neo4j_value_for_llm(item))\n else: # 关系\n # 路径中的关系是带有方向的,但这里的格式化简化为无方向\n path_parts.append(_format_neo4j_value_for_llm(item))\n return \"->\".join(path_parts) # 简化表示,不严格区分关系方向\n\n elif isinstance(value, list):\n # 格式化列表中的每个元素\n formatted_items = [_format_neo4j_value_for_llm(item) for item in value]\n return \"[\" + \", \".join(formatted_items) + \"]\"\n\n elif isinstance(value, dict):\n # 格式化字典中的键值对\n formatted_items = [f\"{k}: {_format_neo4j_value_for_llm(v)}\" for k, v in value.items()]\n return \"{\" + \", \".join(formatted_items) + \"}\"\n\n else:\n # 格式化基本数据类型\n return str(value)\n\n\ndef format_neo4j_results_for_llm(result_records: List[Record], returned_keys: List[str]) -> str:\n \"\"\"\n 将 Neo4j 查询返回的 Record 列表格式化为适合大模型阅读的字符串。\n \"\"\"\n if not result_records:\n return \"查询没有返回任何结果。\"\n\n formatted_output_lines = [\"查询结果如下:\"]\n\n for i, record in enumerate(result_records):\n formatted_output_lines.append(f\"--- 记录 {i + 1} ---\")\n record_str_parts = []\n # 遍历查询返回的每一列\n for key in returned_keys:\n value = record[key]\n formatted_value = _format_neo4j_value_for_llm(value)\n record_str_parts.append(f\"{key}: {formatted_value}\")\n formatted_output_lines.append(\", \".join(record_str_parts))\n\n return \"\\n\".join(formatted_output_lines)\n\n\n# --- Cypher 查询执行函数 ---\ndef execute_cypher_query(query_string: str) -> Tuple[Optional[List[Record]], Optional[str]]:\n \"\"\"\n 执行给定的 Cypher 查询字符串,返回查询结果(Record 列表)或错误信息。\n\n 参数:\n query_string: 要执行的 Cypher 查询字符串。\n\n 返回:\n 一个元组 (results, error_message)。\n 如果执行成功,results 是 Record 对象的列表,error_message 为 None。\n 如果执行失败,results 为 None,error_message 为错误字符串。\n \"\"\"\n driver = get_neo4j_driver()\n if driver is None:\n return None, \"Neo4j Driver 未成功初始化,无法连接数据库。\"\n\n try:\n with driver.session() as session:\n # 执行查询\n result = session.run(query_string, suppress=[\"Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning\"])\n # 获取所有返回的记录\n records = list(result)\n # 返回 Record 对象列表和 None 作为错误信息\n return records, None\n\n except Exception as e:\n # 捕获查询执行过程中的任何异常\n error_message = str(e)\n print(f\"Cypher 查询执行失败: {error_message}\\n查询: {query_string}\")\n return None, error_message\n\n\ndef extract_search_terms_from_cypher(cypher_string: str) -> List[str]:\n \"\"\"\n 尝试从 Cypher 查询字符串中提取用于匹配 Keyword, Topic 或 Title 属性的值。\n 这个函数使用简化的正则表达式,可能无法处理所有复杂的Cypher语法。\n\n 参数:\n cypher_string: LLM 生成的 Cypher 查询字符串。\n\n 返回:\n 提取到的潜在搜索词列表。\n \"\"\"\n # 这个正则表达式尝试匹配以下模式中的单引号或双引号括起来的字符串:\n # name: \"...\" 或 name: '...' (用于 Keyword, Topic, Author, Org 等)\n # title = \"...\" 或 title = '...'\n # title CONTAINS \"...\" 或 title CONTAINS '...'\n # title =~ \"...\" 或 title =~ '...' (以及 abstract =~)\n # abstract CONTAINS \"...\" 或 abstract CONTAINS '...'\n # abstract =~ \"...\" 或 abstract =~ '...'\n # ([^\"']+) 捕获引号内的内容,但不处理转义引号(简化的体现)\n # 使用非贪婪匹配 .*? 以防止跨越多个匹配项\n pattern = r\"(?:name|title)\\s*[:=~]?=?(?:\\s*CONTAINS)?\\s*[\\\"'](.*?)[\\\"']\"\n\n search_terms = []\n # 查找所有匹配项\n matches = re.findall(pattern, cypher_string, re.IGNORECASE | re.DOTALL) # 忽略大小写,让 '.' 匹配换行\n\n # 过滤掉可能是属性名本身或其他非搜索词的情况(简单过滤短词)\n for term in matches:\n term = term.strip()\n if term and len(term) > 1: # 过滤掉空字符串和过短的词\n search_terms.append(term)\n\n # 移除重复项\n search_terms = list(dict.fromkeys(search_terms))\n\n print(f\"从 Cypher 中提取的潜在搜索词: {search_terms}\")\n return search_terms\n\n\n# --- 与大模型交互的集成示例 (概念性代码) ---\n\n# 假设你已经有了大模型的客户端初始化代码\nfrom openai import OpenAI\nDEEPSEEK_API_KEY = \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\" # 你的 API Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-chat\" # 或者 \"deepseek-chat\" 如果你觉得它更适合处理结构化文本\n\n\n# 假设你的图谱 Schema 可以用文本描述(需要根据你的实际 Schema 编写)\n# 可以是节点标签、属性、关系类型及其方向的列表或结构化描述\nGRAPH_SCHEMA_DESCRIPTION = \"\"\"\n你的知识图谱包含以下节点和关系:\n节点标签:\n- Journal_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"journal\", \"notes\", \"pages\", \"reference_type\", \"url\"\n \"doi\", \"volume\", \"year\", \"issue\"}\n- Conference_Proceedings {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"pages\", \"reference_type\", \"url\", \"year\", \"secondary_title\", \n \"subsidiary_author\", \"tertiary_title\"} \n- Patent {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"subsidiary_author\", \"date\", \"srcdatabase\", \"subject\"}\n- Newspaper_Article {\"title\", \":ID\", \":LABEL\", \"database_provider\", \"doi\", \"notes\", \"pages\", \"reference_type\", \"url\", \"secondary_title\", \"date\"}\n- Thesis {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"reference_type\", \"url\", \"year\", \"type_of_work\"}\n- Other_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"date\"}\n- Book {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"pages\", \"reference_type\", \"date\", \"edition\"}\n- Author {\"name\", \":ID\", \":LABEL\"}\n- Keyword {\"name\", \":ID\", \":LABEL\"}\n- Organization {\"name\", \":ID\", \":LABEL\"}\n- Author_Address {\"name\", \":ID\", \":LABEL\"}\n- Topic {\"name\",, \":ID\", \":LABEL\"} \n\n关系类型:\n- (Author)-[:AUTHORED]->(Paper/Patent)\n- (Author)-[:TERTIARY_AUTHORED]->(Paper/Patent)\n- (Paper/Patent)-[:HAS_KEYWORD]->(Keyword)\n- (Paper/Patent)-[:PUBLISHED_BY]->(Organization)\n- (Paper/Patent)-[:HAS_AUTHOR_ADDRESS]->(AuthorAddress)\n- (Keyword)-[:ALIAS_OF]->(Keyword)\n- (Organization)-[:ALIAS_OF]->(Organization)\n- (AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)\n- (Paper/Patent)-[:HAS_TOPIC]->(Topic)\n\n查询时请使用节点属性进行匹配,例如 `title`, `name`, `paper_id`\n\"\"\"\n\n\ndef query_knowledge_graph_with_llm(user_question: str) -> str:\n \"\"\"\n 使用大模型回答关于知识图谱的问题。\n 这是一个简化的两阶段过程:用户问题 -> LLM生成Cypher -> 执行Cypher -> LLM根据结果生成答案。\n 实际问答系统会更复杂,包含多轮交互、意图识别、实体链接、回退机制等。\n \"\"\"\n # 阶段 1: LLM 将用户问题转换为 Cypher 查询\n print(f\"用户问题: {user_question}\")\n print(\"阶段 1: 请求 LLM 生成 Cypher 查询...\")\n\n cypher_generation_prompt = f\"\"\"\n 你的知识图谱Schema如下:\n {GRAPH_SCHEMA_DESCRIPTION}\n \n 重要说明:\n - **别名关系**: `(Keyword)-[:ALIAS_OF]->(Keyword)`, `(Organization)-[:ALIAS_OF]->(Organization)`, `(AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)`. 并非所有 Keyword/Organization/AuthorAddress 节点都有别名关系。\n - **部分节点属性可能缺失**: 例如,某些文献可能没有 year, abstract, url, doi 等属性。\n - **地址关系**: 在用户查询文献的发布机构时,应当将该文献连接的Author_Adress节点一并考虑在查询范围内。\n \n 生成Cypher查询的规则:\n 1. **处理可选关系 (如别名关系):** 当你需要查询一个节点及其别名或相关联的可选节点时,请使用 `OPTIONAL MATCH`。例如,要查找一个Keyword及其别名,使用 `MATCH (k:Keyword {{name: \"某个词\"}}) OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias)`。之后在 WHERE 子句中可以使用 `WHERE relatedNode = k OR relatedNode = alias` 来匹配原始节点或其别名。\n 2. **处理可选属性:** 在 WHERE 子句中直接比较属性值 (例如 `p.year = 2022`) 会自动排除缺失该属性的节点。如果你只想查找**拥有**某个特定属性的节点,可以使用 `WHERE node.property IS NOT NULL`。\n 3. **确保数据类型匹配:** 在比较属性值时,请根据Schema使用正确的数据类型,例如年份是整数,使用 `p.year = 2022` 而不是 `\"2022\"`。\n 4. **使用节点属性匹配:** 在 MATCH 或 MERGE 时,使用 `name`, `title`, `paper_id` 等属性来精确匹配节点。\n 5. **限制结果数量:** 通常在 RETURN 语句后加上 `LIMIT 10`,除非用户明确要求返回所有结果。\n \n 请严格按照上述Schema和规则生成Cypher查询语句,只输出查询本身,不要包含任何额外文字或解释。\n 用户的输入可能会带有错别字,请谨慎的判别是否有错别字并替换为正确的短语,若用户要查询的内容中带有其他语言,请翻译成中文查询。\n \n 示例1 (处理关键词别名和年份过滤):\n 用户问题: 2022年关于肺癌的论文有哪些?\n Cypher 查询:\n MATCH (k:Keyword {{name: \"肺癌\"}})\n OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias) // 使用 OPTIONAL MATCH 处理可选的别名关系\n MATCH (p)-[:HAS_KEYWORD]->(relatedKeyword) // 匹配与关键词相关的论文\n WHERE (relatedKeyword = k OR relatedKeyword = alias) // 相关的关键词是原始关键词或其别名\n AND (p:Paper OR p:Patent) // 确保是文献节点 (根据你的实际主要文献标签调整)\n AND p.year = 2022 // 过滤年份,直接比较数值类型\n RETURN p.title, p.year LIMIT 10\n \n 示例2 (查找所有有摘要的专利):\n 用户问题: 查找所有有摘要的专利。\n Cypher 查询:\n MATCH (p:Patent)\n WHERE p.abstract IS NOT NULL // 只匹配有摘要属性的专利\n RETURN p.title, p.abstract LIMIT 10\n \n 示例3 (查找某个作者在特定年份发表的论文):\n 用户问题: 张三在2023年发表了哪些论文?\n Cypher 查询:\n MATCH (a:Author {{name: \"张三\"}})-[:AUTHORED]->(p:Paper) // 假设AUTHORED关系只连接Paper\n WHERE p.year = 2023\n RETURN p.title, p.year LIMIT 10\n \n \n 用户问题: {user_question}\n Cypher 查询:\n \"\"\"\n\n messages_for_cypher = [\n {\"role\": \"system\", \"content\": cypher_generation_prompt},\n {\"role\": \"user\", \"content\": user_question} # 也可以把问题放在用户消息里,\n\n ]\n\n try:\n # 调用大模型 API\n response_cypher = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_cypher,\n temperature=0, # 生成 Cypher 需要确定性\n max_tokens=500 # 根据预期 Cypher 长度调整\n )\n\n generated_cypher = response_cypher.choices[0].message.content.strip()\n # 从生成的 Cypher 中提取搜索词\n potential_search_terms = extract_search_terms_from_cypher(generated_cypher)\n\n print(f\"LLM 生成的 Cypher 查询:\\n{generated_cypher}\")\n\n except Exception as e:\n print(f\"LLM 生成 Cypher 查询失败: {e}\")\n return f\"抱歉,无法将您的问题转换为图谱查询 ({e})\"\n\n # 阶段 2: 执行 Cypher 查询\n print(\"阶段 2: 执行 Cypher 查询...\")\n query_results, execution_error = execute_cypher_query(generated_cypher)\n\n # 阶段 3: LLM 根据查询结果生成最终答案\n if execution_error:\n # 如果执行失败,将错误信息反馈给用户(或者尝试让LLM修正查询)\n print(f\"查询执行错误: {execution_error}\")\n return f\"抱歉,执行图谱查询时出错: {execution_error}\"\n else:\n print(f\"查询执行成功,返回 {len(query_results)} 条记录。\")\n # 将查询结果格式化为文本\n formatted_results_text = format_neo4j_results_for_llm(query_results,\n query_results[0].keys() if query_results else [])\n\n # 如果结果为空,直接返回提示\n if \"查询没有返回任何结果\" in formatted_results_text:\n # 调用回退搜索函数\n fallback_answer = fallback_abstract_search(user_question, potential_search_terms)\n # 如果回退搜索找到了结果,返回回退结果;否则返回“未能找到信息”的提示\n return fallback_answer if fallback_answer is not None else \"抱歉,未能找到相关信息。\"\n\n print(\"阶段 3: 请求 LLM 根据结果生成自然语言答案...\")\n answer_generation_prompt = f\"\"\"\n 你是一个知识图谱问答助手。根据提供的查询结果,用自然语言回答用户的问题,回答要尽可能专业、逻辑清晰。\n 只使用提供的查询结果中的信息。如果结果无法回答问题,请说明。\n\n **特别注意:如果查询结果中包含了文献的 'abstract' (摘要) 属性,并且用户的问题与文献内容相关,请在回答时充分利用这些摘要信息。具体处理方法可以参考下面的内容。**\n - 如果问题是直接索取摘要(例如:“请给我XX论文的摘要”),你的回答应该清晰地呈现该摘要。\n - 如果问题是关于文献内容、主要观点、科研方法、主要贡献等,请基于摘要进行总结和回答。\n - 如果问题非常具体,请根据问题进行回答,问题可能是:研究问题、方法论、主要发现、创新点、局限性、结论等。\n - 如果摘要内容较长,可以进行适当的概括,但要忠于原文。\n\n 用户问题: {user_question}\n\n 查询结果:\n {formatted_results_text}\n\n 根据以上信息,用自然语言回答用户的问题:\n \"\"\"\n\n messages_for_answer = [\n {\"role\": \"system\", \"content\": answer_generation_prompt},\n {\"role\": \"user\", \"content\": f\"回答用户问题:'{user_question}'\"} # 再次强调问题\n ]\n\n try:\n # 调用大模型 API 生成答案\n response_answer = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_answer,\n temperature=0.5, # 生成答案可以稍微灵活\n max_tokens=500 # 根据预期答案长度调整\n )\n final_answer = response_answer.choices[0].message.content.strip()\n print(f\"LLM 生成的最终答案:\\n{final_answer}\")\n return final_answer\n\n except Exception as e:\n print(f\"LLM 生成最终答案失败: {e}\")\n return f\"抱歉,处理查询结果时遇到问题 ({e})\"\n\n\n# --- 回退机制:摘要文本搜索 ---\n\ndef fallback_abstract_search(user_question: str, search_terms: List[str], max_results: int = 100) -> Optional[str]:\n \"\"\"\n 当图谱查询无结果时,作为回退机制在文献摘要中进行文本搜索(使用 Cypher 正则),\n 并将匹配到的摘要交给大模型进行判断和总结。\n\n 参数:\n user_question: 用户的原始问题。\n llm_client: 大模型 API 客户端。\n search_terms: 从用户问题中提取的用于文本搜索的关键词/短语列表。\n max_results: 从数据库获取的最大匹配结果数量。\n\n 返回:\n LLM 基于匹配摘要生成的答案,或 None 表示未能找到相关信息。\n \"\"\"\n if not search_terms:\n print(\"回退搜索:未提取到搜索关键词。\")\n return None\n\n print(f\"回退搜索:图谱查询无结果,尝试在摘要中进行 Cypher 正则文本匹配。搜索词: {search_terms}\")\n\n # 1. 构建 Cypher 查询\n # Condition 1: Abstract matches ALL search terms\n if not search_terms:\n # Cannot match all terms if search_terms is empty, so this condition is false\n abstract_regex_condition = \"false\"\n else:\n # Build regex for matching ALL terms\n escaped_search_terms = [re.escape(term) for term in search_terms]\n # Pattern: (?i)(?=.*term1)(?=.*term2)...*\n abstract_regex_pattern = \"(?i)\" + \"\".join(f\"(?=.*{term})\" for term in escaped_search_terms) + \".*\"\n abstract_regex_condition = f\"p.abstract IS NOT NULL AND p.abstract =~ '{abstract_regex_pattern}'\"\n\n\n # Condition 2: Related Topic matches ANY search term\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n topic_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n topic_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n topic_condition = f\"topic IN topics WHERE topic.name IS NOT NULL AND topic.name =~ '(?i).*({topic_terms_list})'\"\n\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n address_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n address_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n address_condition = f\"address IN addresses WHERE address.name IS NOT NULL AND address.name =~ '(?i).*({address_terms_list})'\"\n\n\n # Combine the two conditions with OR\n # We need to structure the query to allow OPTIONAL MATCH for topic while filtering papers\n # Approach: Filter papers by abstract condition first (if applicable), then OPTIONAL MATCH topic, then filter by topic condition\n # Or, apply both conditions in the final WHERE after OPTIONAL MATCH\n # Let's use the latter for simplicity, assuming the abstract IS NOT NULL check is sufficient before regex\n\n fallback_query = f\"\"\"\n MATCH (p)\n WHERE (p:Patent OR p:Journal_Article OR p:Conference_Proceedings OR p:Thesis OR p:Other_Article OR p:Newspaper_Article OR p:Book)\n OPTIONAL MATCH (p)-[:HAS_TOPIC]->(t:Topic) // Optional match for Topic node\n OPTIONAL MATCH(p) - [: AUTHOR_ADDRESS]->(addr:Author_Address)\n WITH p, collect(t) AS topics, collect(addr) AS addresses\n WHERE ({abstract_regex_condition}) OR any({topic_condition}) OR any({address_condition}) // Apply the combined condition\n RETURN p // Return the node itself\n LIMIT {max_results} // Limit the number of results\n \"\"\"\n\n print(f\"回退搜索:执行 Cypher 联合查询:\\n{fallback_query}\")\n\n # 2. 执行 Cypher 联合查询\n abstract_records, error = execute_cypher_query(fallback_query)\n\n if error:\n print(f\"回退搜索:执行 Cypher 联合查询失败: {error}\")\n return None\n\n if not abstract_records:\n print(\"回退搜索:Cypher 联合查询没有匹配到任何结果。\")\n return None\n\n print(f\"回退搜索:Cypher 联合查询匹配到 {len(abstract_records)} 条结果:\")\n print(abstract_records)\n # 3. 将匹配到的结果格式化并发送给 LLM 进行判断和总结\n # 使用 format_neo4j_results_for_llm 格式化结果\n returned_keys = abstract_records[0].keys() if abstract_records else []\n formatted_abstract_results_text = format_neo4j_results_for_llm(abstract_records, returned_keys)\n\n print(\"回退搜索:将匹配结果发送给 LLM 进行判断和总结。\")\n\n abstract_summary_prompt = f\"\"\"\n 根据以下提供的文献信息(标题、摘要等)中与用户问题相关的内容,回答用户的原始问题。\n 只使用提供的信息进行回答。如果提供的信息都与问题不相关,请直接返回”查询没有返回任何结果“。\n 用自然语言回答,简洁明了。\n\n 用户原始问题: {user_question}\n\n 提供的文献信息:\n {formatted_abstract_results_text}\n\n 请根据以上信息,回答用户的问题:\n \"\"\"\n\n messages_for_summary = [\n {\"role\": \"system\", \"content\": abstract_summary_prompt},\n {\"role\": \"user\", \"content\": f\"总结与问题 '{user_question}' 相关的文献信息。\"}\n ]\n\n try:\n # 调用大模型 API\n summary_response = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL, # 使用与生成Cypher相同的模型,或根据需要选择\n messages=messages_for_summary,\n temperature=0.7, # 总结可以稍微灵活\n max_tokens=1000 # 总结长度,根据结果数量调整\n )\n summary_answer = summary_response.choices[0].message.content.strip()\n print(f\"回退搜索:LLM 总结的结果:\\n{summary_answer}\")\n\n # 简单的判断 LLM 是否认为结果相关(可能需要更健壮的判断逻辑)\n if \"查询没有返回任何结果\" in summary_answer:\n return None # LLM 认为结果不相关或无法总结\n\n return summary_answer # 返回 LLM 总结的内容\n\n except Exception as e:\n print(f\"回退搜索:LLM 总结失败: {e}\")\n return None # LLM 调用失败\n\n\n\ndef init():\n # 初始化 Neo4j Driver\n neo4j_driver = get_neo4j_driver()\n if neo4j_driver is None:\n exit(1) # 如果驱动初始化失败则退出\n\n # 导入大模型客户端 (需要安装 openai 库并配置 API Key)\n try:\n from openai import OpenAI\n\n # 请确保在这里设置你的 DeepSeek 或其他 API Key\n # 建议使用环境变量或配置文件\n DEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\")\n if not DEEPSEEK_API_KEY:\n print(\"错误:请设置 DEEPSEEK_API_KEY 环境变量或在代码中提供。\")\n exit(1)\n\n global llm_client_instance\n llm_client_instance = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n\n # 可以再次验证 LLM API 连接\n # llm_client_instance.models.list()\n print(\"大模型客户端初始化成功。\")\n\n except ImportError:\n print(\"错误:请安装 openai 库 (`pip install openai`) 以使用大模型API。\")\n exit(1)\n except Exception as e:\n print(f\"大模型客户端初始化失败: {e}\")\n exit(1)\n\n\n\ndef shutdown_resources():\n \"\"\"关闭资源\"\"\"\n close_neo4j_driver()\n\n\n# --- 主函数示例 (需要实际运行环境和 API Key) ---\nif __name__ == \"__main__\":\n llm_client_instance = init()\n\n # --- 示例问答流程 ---\n print(\"\\n--- 开始示例问答 ---\")\n\n # 确保你的数据库中有能匹配的节点和关系,以及对应的摘要等属性\n # 这里的示例问题需要LLM能够理解并转换为Cypher\n example_question = \"'你能查询哪些信息?\"\n\n final_answer = query_knowledge_graph_with_llm(example_question)\n\n print(f\"\\n问答系统最终回答:\\n{final_answer}\")\n\n print(\"\\n--- 示例问答结束 ---\")\n\n # 关闭 Neo4j Driver\n close_neo4j_driver()\n"}, "786bc655b9b56f2ea59b312d5d304ca4e088849f": {"t": 1785753220.0055933, "n": 20474, "h": "a0e41937adaee171", "c": "import os\nimport re\nfrom typing import List, Tuple, Optional, Any, Dict\n\nfrom neo4j.graph import Node, Relationship, Path\nfrom neo4j import GraphDatabase, Record\n\n# --- Neo4j 连接信息 (需要根据你的实际情况修改) ---\n# 建议从配置文件或环境变量读取\nNEO4J_URI = \"bolt://1.92.129.26:2223\"\nNEO4J_USER = \"neo4j\"\nNEO4J_PASSWORD = \"123456788\"\n\n# 全局 Driver 实例\nneo4j_driver: Optional[GraphDatabase.driver] = None\n# 全局 LLM 客户端实例\nllm_client_instance: Optional[Any] = None # 使用 Any 类型,因为具体类型取决于导入的库\n\ndef get_neo4j_driver():\n \"\"\"获取或初始化 Neo4j Driver 实例\"\"\"\n global neo4j_driver\n if neo4j_driver is None:\n try:\n # 可以根据需要调整连接池大小等参数\n neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n # 验证连接\n neo4j_driver.verify_connectivity()\n print(\"Neo4j Driver 初始化成功。\")\n except Exception as e:\n print(f\"Neo4j Driver 初始化或连接失败: {e}\")\n neo4j_driver = None\n return neo4j_driver\n\n\ndef close_neo4j_driver():\n \"\"\"关闭 Neo4j Driver 连接\"\"\"\n global neo4j_driver\n if neo4j_driver:\n neo4j_driver.close()\n neo4j_driver = None\n print(\"Neo4j Driver 已关闭。\")\n\n\n# --- 结果格式化函数 ---\ndef _format_neo4j_value_for_llm(value: Any) -> str:\n \"\"\"\n 将 Neo4j 返回的不同类型的值格式化为适合大模型阅读的字符串表示。\n 尝试识别常见的节点ID属性(如 name, title, paper_id)。\n \"\"\"\n if isinstance(value, Node):\n labels = \":\".join(list(value.labels))\n properties_str_parts = []\n # 尝试获取主要的标识属性\n if 'name' in value:\n properties_str_parts.append(f\"name: '{value['name']}'\")\n if 'title' in value:\n properties_str_parts.append(f\"title: '{value['title']}'\")\n if 'paper_id' in value: # 假设你的 paper_id 属性\n properties_str_parts.append(f\"paper_id: '{value['paper_id']}'\")\n # 可以添加更多常见的标识属性\n\n # 如果没有找到标识属性,或者想包含更多属性,可以遍历所有属性\n for prop_key, prop_value in value.items():\n if prop_key not in ['name', 'title', 'paper_id']: # 避免重复添加\n # 递归格式化属性值,注意避免无限递归复杂对象\n properties_str_parts.append(f\"{prop_key}: '{prop_value}'\") # 简化处理,直接转字符串可能不够\n\n properties_str = \", \".join(properties_str_parts)\n return f\"({labels} {{{properties_str}}})\" if properties_str else f\"({labels})\"\n\n elif isinstance(value, Relationship):\n # 只显示关系类型\n return f\"-[:{value.type}]-\"\n # 如果需要包含关系属性,会更复杂\n # return f\"-[:{value.type} {{...}}]-\"\n\n elif isinstance(value, Path):\n path_parts = []\n for i, item in enumerate(value):\n if i % 2 == 0: # 节点\n path_parts.append(_format_neo4j_value_for_llm(item))\n else: # 关系\n # 路径中的关系是带有方向的,但这里的格式化简化为无方向\n path_parts.append(_format_neo4j_value_for_llm(item))\n return \"->\".join(path_parts) # 简化表示,不严格区分关系方向\n\n elif isinstance(value, list):\n # 格式化列表中的每个元素\n formatted_items = [_format_neo4j_value_for_llm(item) for item in value]\n return \"[\" + \", \".join(formatted_items) + \"]\"\n\n elif isinstance(value, dict):\n # 格式化字典中的键值对\n formatted_items = [f\"{k}: {_format_neo4j_value_for_llm(v)}\" for k, v in value.items()]\n return \"{\" + \", \".join(formatted_items) + \"}\"\n\n else:\n # 格式化基本数据类型\n return str(value)\n\n\ndef format_neo4j_results_for_llm(result_records: List[Record], returned_keys: List[str]) -> str:\n \"\"\"\n 将 Neo4j 查询返回的 Record 列表格式化为适合大模型阅读的字符串。\n \"\"\"\n if not result_records:\n return \"查询没有返回任何结果。\"\n\n formatted_output_lines = [\"查询结果如下:\"]\n\n for i, record in enumerate(result_records):\n formatted_output_lines.append(f\"--- 记录 {i + 1} ---\")\n record_str_parts = []\n # 遍历查询返回的每一列\n for key in returned_keys:\n value = record[key]\n formatted_value = _format_neo4j_value_for_llm(value)\n record_str_parts.append(f\"{key}: {formatted_value}\")\n formatted_output_lines.append(\", \".join(record_str_parts))\n\n return \"\\n\".join(formatted_output_lines)\n\n\n# --- Cypher 查询执行函数 ---\ndef execute_cypher_query(query_string: str) -> Tuple[Optional[List[Record]], Optional[str]]:\n \"\"\"\n 执行给定的 Cypher 查询字符串,返回查询结果(Record 列表)或错误信息。\n\n 参数:\n query_string: 要执行的 Cypher 查询字符串。\n\n 返回:\n 一个元组 (results, error_message)。\n 如果执行成功,results 是 Record 对象的列表,error_message 为 None。\n 如果执行失败,results 为 None,error_message 为错误字符串。\n \"\"\"\n driver = get_neo4j_driver()\n if driver is None:\n return None, \"Neo4j Driver 未成功初始化,无法连接数据库。\"\n\n try:\n with driver.session() as session:\n # 执行查询\n result = session.run(query_string, suppress=[\"Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning\"])\n # 获取所有返回的记录\n records = list(result)\n # 返回 Record 对象列表和 None 作为错误信息\n return records, None\n\n except Exception as e:\n # 捕获查询执行过程中的任何异常\n error_message = str(e)\n print(f\"Cypher 查询执行失败: {error_message}\\n查询: {query_string}\")\n return None, error_message\n\n\ndef extract_search_terms_from_cypher(cypher_string: str) -> List[str]:\n \"\"\"\n 尝试从 Cypher 查询字符串中提取用于匹配 Keyword, Topic 或 Title 属性的值。\n 这个函数使用简化的正则表达式,可能无法处理所有复杂的Cypher语法。\n\n 参数:\n cypher_string: LLM 生成的 Cypher 查询字符串。\n\n 返回:\n 提取到的潜在搜索词列表。\n \"\"\"\n # 这个正则表达式尝试匹配以下模式中的单引号或双引号括起来的字符串:\n # name: \"...\" 或 name: '...' (用于 Keyword, Topic, Author, Org 等)\n # title = \"...\" 或 title = '...'\n # title CONTAINS \"...\" 或 title CONTAINS '...'\n # title =~ \"...\" 或 title =~ '...' (以及 abstract =~)\n # abstract CONTAINS \"...\" 或 abstract CONTAINS '...'\n # abstract =~ \"...\" 或 abstract =~ '...'\n # ([^\"']+) 捕获引号内的内容,但不处理转义引号(简化的体现)\n # 使用非贪婪匹配 .*? 以防止跨越多个匹配项\n pattern = r\"(?:name|title)\\s*[:=~]?=?(?:\\s*CONTAINS)?\\s*[\\\"'](.*?)[\\\"']\"\n\n search_terms = []\n # 查找所有匹配项\n matches = re.findall(pattern, cypher_string, re.IGNORECASE | re.DOTALL) # 忽略大小写,让 '.' 匹配换行\n\n # 过滤掉可能是属性名本身或其他非搜索词的情况(简单过滤短词)\n for term in matches:\n term = term.strip()\n if term and len(term) > 1: # 过滤掉空字符串和过短的词\n search_terms.append(term)\n\n # 移除重复项\n search_terms = list(dict.fromkeys(search_terms))\n\n print(f\"从 Cypher 中提取的潜在搜索词: {search_terms}\")\n return search_terms\n\n\n# --- 与大模型交互的集成示例 (概念性代码) ---\n\n# 假设你已经有了大模型的客户端初始化代码\nfrom openai import OpenAI\nDEEPSEEK_API_KEY = \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\" # 你的 API Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-chat\" # 或者 \"deepseek-chat\" 如果你觉得它更适合处理结构化文本\n\n\n# 假设你的图谱 Schema 可以用文本描述(需要根据你的实际 Schema 编写)\n# 可以是节点标签、属性、关系类型及其方向的列表或结构化描述\nGRAPH_SCHEMA_DESCRIPTION = \"\"\"\n你的知识图谱包含以下节点和关系:\n节点标签:\n- Journal_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"journal\", \"notes\", \"pages\", \"reference_type\", \"url\"\n \"doi\", \"volume\", \"year\", \"issue\"}\n- Conference_Proceedings {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"pages\", \"reference_type\", \"url\", \"year\", \"secondary_title\", \n \"subsidiary_author\", \"tertiary_title\"} \n- Patent {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"subsidiary_author\", \"date\", \"srcdatabase\", \"subject\"}\n- Newspaper_Article {\"title\", \":ID\", \":LABEL\", \"database_provider\", \"doi\", \"notes\", \"pages\", \"reference_type\", \"url\", \"secondary_title\", \"date\"}\n- Thesis {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"reference_type\", \"url\", \"year\", \"type_of_work\"}\n- Other_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"date\"}\n- Book {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"pages\", \"reference_type\", \"date\", \"edition\"}\n- Author {\"name\", \":ID\", \":LABEL\"}\n- Keyword {\"name\", \":ID\", \":LABEL\"}\n- Organization {\"name\", \":ID\", \":LABEL\"}\n- Author_Address {\"name\", \":ID\", \":LABEL\"}\n- Topic {\"name\",, \":ID\", \":LABEL\"} \n\n关系类型:\n- (Author)-[:AUTHORED]->(Paper/Patent)\n- (Author)-[:TERTIARY_AUTHORED]->(Paper/Patent)\n- (Paper/Patent)-[:HAS_KEYWORD]->(Keyword)\n- (Paper/Patent)-[:PUBLISHED_BY]->(Organization)\n- (Paper/Patent)-[:HAS_AUTHOR_ADDRESS]->(AuthorAddress)\n- (Keyword)-[:ALIAS_OF]->(Keyword)\n- (Organization)-[:ALIAS_OF]->(Organization)\n- (AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)\n- (Paper/Patent)-[:HAS_TOPIC]->(Topic)\n\n查询时请使用节点属性进行匹配,例如 `title`, `name`, `paper_id`\n\"\"\"\n\n\ndef query_knowledge_graph_with_llm(user_question: str) -> str:\n \"\"\"\n 使用大模型回答关于知识图谱的问题。\n 这是一个简化的两阶段过程:用户问题 -> LLM生成Cypher -> 执行Cypher -> LLM根据结果生成答案。\n 实际问答系统会更复杂,包含多轮交互、意图识别、实体链接、回退机制等。\n \"\"\"\n # 阶段 1: LLM 将用户问题转换为 Cypher 查询\n print(f\"用户问题: {user_question}\")\n print(\"阶段 1: 请求 LLM 生成 Cypher 查询...\")\n\n cypher_generation_prompt = f\"\"\"\n 你的知识图谱Schema如下:\n {GRAPH_SCHEMA_DESCRIPTION}\n \n 重要说明:\n - **别名关系**: `(Keyword)-[:ALIAS_OF]->(Keyword)`, `(Organization)-[:ALIAS_OF]->(Organization)`, `(AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)`. 并非所有 Keyword/Organization/AuthorAddress 节点都有别名关系。\n - **部分节点属性可能缺失**: 例如,某些文献可能没有 year, abstract, url, doi 等属性。\n - **地址关系**: 在用户查询文献的发布机构时,应当将该文献连接的Author_Adress节点一并考虑在查询范围内。\n \n 生成Cypher查询的规则:\n 1. **处理可选关系 (如别名关系):** 当你需要查询一个节点及其别名或相关联的可选节点时,请使用 `OPTIONAL MATCH`。例如,要查找一个Keyword及其别名,使用 `MATCH (k:Keyword {{name: \"某个词\"}}) OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias)`。之后在 WHERE 子句中可以使用 `WHERE relatedNode = k OR relatedNode = alias` 来匹配原始节点或其别名。\n 2. **处理可选属性:** 在 WHERE 子句中直接比较属性值 (例如 `p.year = 2022`) 会自动排除缺失该属性的节点。如果你只想查找**拥有**某个特定属性的节点,可以使用 `WHERE node.property IS NOT NULL`。\n 3. **确保数据类型匹配:** 在比较属性值时,请根据Schema使用正确的数据类型,例如年份是整数,使用 `p.year = 2022` 而不是 `\"2022\"`。\n 4. **使用节点属性匹配:** 在 MATCH 或 MERGE 时,使用 `name`, `title`, `paper_id` 等属性来精确匹配节点。\n 5. **限制结果数量:** 通常在 RETURN 语句后加上 `LIMIT 10`,除非用户明确要求返回所有结果。\n \n 请严格按照上述Schema和规则生成Cypher查询语句,只输出查询本身,不要包含任何额外文字或解释。\n 用户的输入可能会带有错别字,请谨慎的判别是否有错别字并替换为正确的短语,若用户要查询的内容中带有其他语言,请翻译成中文查询。\n \n 示例1 (处理关键词别名和年份过滤):\n 用户问题: 2022年关于肺癌的论文有哪些?\n Cypher 查询:\n MATCH (k:Keyword {{name: \"肺癌\"}})\n OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias) // 使用 OPTIONAL MATCH 处理可选的别名关系\n MATCH (p)-[:HAS_KEYWORD]->(relatedKeyword) // 匹配与关键词相关的论文\n WHERE (relatedKeyword = k OR relatedKeyword = alias) // 相关的关键词是原始关键词或其别名\n AND (p:Paper OR p:Patent) // 确保是文献节点 (根据你的实际主要文献标签调整)\n AND p.year = 2022 // 过滤年份,直接比较数值类型\n RETURN p.title, p.year LIMIT 10\n \n 示例2 (查找所有有摘要的专利):\n 用户问题: 查找所有有摘要的专利。\n Cypher 查询:\n MATCH (p:Patent)\n WHERE p.abstract IS NOT NULL // 只匹配有摘要属性的专利\n RETURN p.title, p.abstract LIMIT 10\n \n 示例3 (查找某个作者在特定年份发表的论文):\n 用户问题: 张三在2023年发表了哪些论文?\n Cypher 查询:\n MATCH (a:Author {{name: \"张三\"}})-[:AUTHORED]->(p:Paper) // 假设AUTHORED关系只连接Paper\n WHERE p.year = 2023\n RETURN p.title, p.year LIMIT 10\n \n \n 用户问题: {user_question}\n Cypher 查询:\n \"\"\"\n\n messages_for_cypher = [\n {\"role\": \"system\", \"content\": cypher_generation_prompt},\n {\"role\": \"user\", \"content\": user_question} # 也可以把问题放在用户消息里,\n\n ]\n\n try:\n # 调用大模型 API\n response_cypher = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_cypher,\n temperature=0, # 生成 Cypher 需要确定性\n max_tokens=500 # 根据预期 Cypher 长度调整\n )\n\n generated_cypher = response_cypher.choices[0].message.content.strip()\n # 从生成的 Cypher 中提取搜索词\n potential_search_terms = extract_search_terms_from_cypher(generated_cypher)\n\n print(f\"LLM 生成的 Cypher 查询:\\n{generated_cypher}\")\n\n except Exception as e:\n print(f\"LLM 生成 Cypher 查询失败: {e}\")\n return f\"抱歉,无法将您的问题转换为图谱查询 ({e})\"\n\n # 阶段 2: 执行 Cypher 查询\n print(\"阶段 2: 执行 Cypher 查询...\")\n query_results, execution_error = execute_cypher_query(generated_cypher)\n\n # 阶段 3: LLM 根据查询结果生成最终答案\n if execution_error:\n # 如果执行失败,将错误信息反馈给用户(或者尝试让LLM修正查询)\n print(f\"查询执行错误: {execution_error}\")\n return f\"抱歉,执行图谱查询时出错: {execution_error}\"\n else:\n print(f\"查询执行成功,返回 {len(query_results)} 条记录。\")\n # 将查询结果格式化为文本\n formatted_results_text = format_neo4j_results_for_llm(query_results,\n query_results[0].keys() if query_results else [])\n\n # 如果结果为空,直接返回提示\n if \"查询没有返回任何结果\" in formatted_results_text:\n # 调用回退搜索函数\n fallback_answer = fallback_abstract_search(user_question, potential_search_terms)\n # 如果回退搜索找到了结果,返回回退结果;否则返回“未能找到信息”的提示\n return fallback_answer if fallback_answer is not None else \"抱歉,未能找到相关信息。\"\n\n print(\"阶段 3: 请求 LLM 根据结果生成自然语言答案...\")\n answer_generation_prompt = f\"\"\"\n 你是一个知识图谱问答助手。根据提供的查询结果,用自然语言回答用户的问题,回答要尽可能专业、逻辑清晰。\n 只使用提供的查询结果中的信息。如果结果无法回答问题,请说明。\n\n **特别注意:如果查询结果中包含了文献的 'abstract' (摘要) 属性,并且用户的问题与文献内容相关,请在回答时充分利用这些摘要信息。具体处理方法可以参考下面的内容。**\n - 如果问题是直接索取摘要(例如:“请给我XX论文的摘要”),你的回答应该清晰地呈现该摘要。\n - 如果问题是关于文献内容、主要观点、科研方法、主要贡献等,请基于摘要进行总结和回答。\n - 如果问题非常具体,请根据问题进行回答,问题可能是:研究问题、方法论、主要发现、创新点、局限性、结论等。\n - 如果摘要内容较长,可以进行适当的概括,但要忠于原文。\n\n 用户问题: {user_question}\n\n 查询结果:\n {formatted_results_text}\n\n 根据以上信息,用自然语言回答用户的问题:\n \"\"\"\n\n messages_for_answer = [\n {\"role\": \"system\", \"content\": answer_generation_prompt},\n {\"role\": \"user\", \"content\": f\"回答用户问题:'{user_question}'\"} # 再次强调问题\n ]\n\n try:\n # 调用大模型 API 生成答案\n response_answer = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_answer,\n temperature=0.5, # 生成答案可以稍微灵活\n max_tokens=500 # 根据预期答案长度调整\n )\n final_answer = response_answer.choices[0].message.content.strip()\n print(f\"LLM 生成的最终答案:\\n{final_answer}\")\n return final_answer\n\n except Exception as e:\n print(f\"LLM 生成最终答案失败: {e}\")\n return f\"抱歉,处理查询结果时遇到问题 ({e})\"\n\n\n# --- 回退机制:摘要文本搜索 ---\n\ndef fallback_abstract_search(user_question: str, search_terms: List[str], max_results: int = 100) -> Optional[str]:\n \"\"\"\n 当图谱查询无结果时,作为回退机制在文献摘要中进行文本搜索(使用 Cypher 正则),\n 并将匹配到的摘要交给大模型进行判断和总结。\n\n 参数:\n user_question: 用户的原始问题。\n llm_client: 大模型 API 客户端。\n search_terms: 从用户问题中提取的用于文本搜索的关键词/短语列表。\n max_results: 从数据库获取的最大匹配结果数量。\n\n 返回:\n LLM 基于匹配摘要生成的答案,或 None 表示未能找到相关信息。\n \"\"\"\n if not search_terms:\n print(\"回退搜索:未提取到搜索关键词。\")\n return None\n\n print(f\"回退搜索:图谱查询无结果,尝试在摘要中进行 Cypher 正则文本匹配。搜索词: {search_terms}\")\n\n # 1. 构建 Cypher 查询\n # Condition 1: Abstract matches ALL search terms\n if not search_terms:\n # Cannot match all terms if search_terms is empty, so this condition is false\n abstract_regex_condition = \"false\"\n else:\n # Build regex for matching ALL terms\n escaped_search_terms = [re.escape(term) for term in search_terms]\n # Pattern: (?i)(?=.*term1)(?=.*term2)...*\n abstract_regex_pattern = \"(?i)\" + \"\".join(f\"(?=.*{term})\" for term in escaped_search_terms) + \".*\"\n abstract_regex_condition = f\"p.abstract IS NOT NULL AND p.abstract =~ '{abstract_regex_pattern}'\"\n\n\n # Condition 2: Related Topic matches ANY search term\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n topic_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n topic_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n topic_condition = f\"topic IN topics WHERE topic.name IS NOT NULL AND topic.name =~ '(?i).*({topic_terms_list})'\"\n\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n address_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n address_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n address_condition = f\"address IN addresses WHERE address.name IS NOT NULL AND address.name =~ '(?i).*({address_terms_list})'\"\n\n\n # Combine the two conditions with OR\n # We need to structure the query to allow OPTIONAL MATCH for topic while filtering papers\n # Approach: Filter papers by abstract condition first (if applicable), then OPTIONAL MATCH topic, then filter by topic condition\n # Or, apply both conditions in the final WHERE after OPTIONAL MATCH\n # Let's use the latter for simplicity, assuming the abstract IS NOT NULL check is sufficient before regex\n\n fallback_query = f\"\"\"\n MATCH (p)\n WHERE (p:Patent OR p:Journal_Article OR p:Conference_Proceedings OR p:Thesis OR p:Other_Article OR p:Newspaper_Article OR p:Book)\n OPTIONAL MATCH (p)-[:HAS_TOPIC]->(t:Topic) // Optional match for Topic node\n OPTIONAL MATCH(p) - [: AUTHOR_ADDRESS]->(addr:Author_Address)\n WITH p, collect(t) AS topics, collect(addr) AS addresses\n WHERE ({abstract_regex_condition}) OR any({topic_condition}) OR any({address_condition}) // Apply the combined condition\n RETURN p // Return the node itself\n LIMIT {max_results} // Limit the number of results\n \"\"\"\n\n print(f\"回退搜索:执行 Cypher 联合查询:\\n{fallback_query}\")\n\n # 2. 执行 Cypher 联合查询\n abstract_records, error = execute_cypher_query(fallback_query)\n\n if error:\n print(f\"回退搜索:执行 Cypher 联合查询失败: {error}\")\n return None\n\n if not abstract_records:\n print(\"回退搜索:Cypher 联合查询没有匹配到任何结果。\")\n return None\n\n print(f\"回退搜索:Cypher 联合查询匹配到 {len(abstract_records)} 条结果:\")\n print(abstract_records)\n # 3. 将匹配到的结果格式化并发送给 LLM 进行判断和总结\n # 使用 format_neo4j_results_for_llm 格式化结果\n returned_keys = abstract_records[0].keys() if abstract_records else []\n formatted_abstract_results_text = format_neo4j_results_for_llm(abstract_records, returned_keys)\n\n print(\"回退搜索:将匹配结果发送给 LLM 进行判断和总结。\")\n\n abstract_summary_prompt = f\"\"\"\n 根据以下提供的文献信息(标题、摘要等)中与用户问题相关的内容,回答用户的原始问题。\n 只使用提供的信息进行回答。如果提供的信息都与问题不相关,请直接返回”查询没有返回任何结果“。\n 用自然语言回答,简洁明了。\n\n 用户原始问题: {user_question}\n\n 提供的文献信息:\n {formatted_abstract_results_text}\n\n 请根据以上信息,回答用户的问题:\n \"\"\"\n\n messages_for_summary = [\n {\"role\": \"system\", \"content\": abstract_summary_prompt},\n {\"role\": \"user\", \"content\": f\"总结与问题 '{user_question}' 相关的文献信息。\"}\n ]\n\n try:\n # 调用大模型 API\n summary_response = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL, # 使用与生成Cypher相同的模型,或根据需要选择\n messages=messages_for_summary,\n temperature=0.7, # 总结可以稍微灵活\n max_tokens=1000 # 总结长度,根据结果数量调整\n )\n summary_answer = summary_response.choices[0].message.content.strip()\n print(f\"回退搜索:LLM 总结的结果:\\n{summary_answer}\")\n\n # 简单的判断 LLM 是否认为结果相关(可能需要更健壮的判断逻辑)\n if \"查询没有返回任何结果\" in summary_answer:\n return None # LLM 认为结果不相关或无法总结\n\n return summary_answer # 返回 LLM 总结的内容\n\n except Exception as e:\n print(f\"回退搜索:LLM 总结失败: {e}\")\n return None # LLM 调用失败\n\n\n\ndef init():\n # 初始化 Neo4j Driver\n neo4j_driver = get_neo4j_driver()\n if neo4j_driver is None:\n exit(1) # 如果驱动初始化失败则退出\n\n # 导入大模型客户端 (需要安装 openai 库并配置 API Key)\n try:\n from openai import OpenAI\n\n # 请确保在这里设置你的 DeepSeek 或其他 API Key\n # 建议使用环境变量或配置文件\n DEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\")\n if not DEEPSEEK_API_KEY:\n print(\"错误:请设置 DEEPSEEK_API_KEY 环境变量或在代码中提供。\")\n exit(1)\n\n global llm_client_instance\n llm_client_instance = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n\n # 可以再次验证 LLM API 连接\n # llm_client_instance.models.list()\n print(\"大模型客户端初始化成功。\")\n\n except ImportError:\n print(\"错误:请安装 openai 库 (`pip install openai`) 以使用大模型API。\")\n exit(1)\n except Exception as e:\n print(f\"大模型客户端初始化失败: {e}\")\n exit(1)\n\n\n\ndef shutdown_resources():\n \"\"\"关闭资源\"\"\"\n close_neo4j_driver()\n\n\n# --- 主函数示例 (需要实际运行环境和 API Key) ---\nif __name__ == \"__main__\":\n llm_client_instance = init()\n\n # --- 示例问答流程 ---\n print(\"\\n--- 开始示例问答 ---\")\n\n # 确保你的数据库中有能匹配的节点和关系,以及对应的摘要等属性\n # 这里的示例问题需要LLM能够理解并转换为Cypher\n example_question = \"'你能查询哪些信息?\"\n\n final_answer = query_knowledge_graph_with_llm(example_question)\n\n print(f\"\\n问答系统最终回答:\\n{final_answer}\")\n\n print(\"\\n--- 示例问答结束 ---\")\n\n # 关闭 Neo4j Driver\n close_neo4j_driver()\n"}, "966f26d4d524aa90feeb4c11386f72e15f336a50": {"t": 1785753221.4586446, "n": 20445, "h": "adc75c92f2798f26", "c": "import os\nimport re\nfrom typing import List, Tuple, Optional, Any, Dict\n\nfrom neo4j.graph import Node, Relationship, Path\nfrom neo4j import GraphDatabase, Record\n\n# --- Neo4j 连接信息 (需要根据你的实际情况修改) ---\n# 建议从配置文件或环境变量读取\nNEO4J_URI = \"bolt://1.92.129.26:2223\"\nNEO4J_USER = \"neo4j\"\nNEO4J_PASSWORD = \"123456788\"\n\n# 全局 Driver 实例\nneo4j_driver: Optional[GraphDatabase.driver] = None\n# 全局 LLM 客户端实例\nllm_client_instance: Optional[Any] = None # 使用 Any 类型,因为具体类型取决于导入的库\n\ndef get_neo4j_driver():\n \"\"\"获取或初始化 Neo4j Driver 实例\"\"\"\n global neo4j_driver\n if neo4j_driver is None:\n try:\n # 可以根据需要调整连接池大小等参数\n neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n # 验证连接\n neo4j_driver.verify_connectivity()\n print(\"Neo4j Driver 初始化成功。\")\n except Exception as e:\n print(f\"Neo4j Driver 初始化或连接失败: {e}\")\n neo4j_driver = None\n return neo4j_driver\n\n\ndef close_neo4j_driver():\n \"\"\"关闭 Neo4j Driver 连接\"\"\"\n global neo4j_driver\n if neo4j_driver:\n neo4j_driver.close()\n neo4j_driver = None\n print(\"Neo4j Driver 已关闭。\")\n\n\n# --- 结果格式化函数 ---\ndef _format_neo4j_value_for_llm(value: Any) -> str:\n \"\"\"\n 将 Neo4j 返回的不同类型的值格式化为适合大模型阅读的字符串表示。\n 尝试识别常见的节点ID属性(如 name, title, paper_id)。\n \"\"\"\n if isinstance(value, Node):\n labels = \":\".join(list(value.labels))\n properties_str_parts = []\n # 尝试获取主要的标识属性\n if 'name' in value:\n properties_str_parts.append(f\"name: '{value['name']}'\")\n if 'title' in value:\n properties_str_parts.append(f\"title: '{value['title']}'\")\n if 'paper_id' in value: # 假设你的 paper_id 属性\n properties_str_parts.append(f\"paper_id: '{value['paper_id']}'\")\n # 可以添加更多常见的标识属性\n\n # 如果没有找到标识属性,或者想包含更多属性,可以遍历所有属性\n for prop_key, prop_value in value.items():\n if prop_key not in ['name', 'title', 'paper_id']: # 避免重复添加\n # 递归格式化属性值,注意避免无限递归复杂对象\n properties_str_parts.append(f\"{prop_key}: '{prop_value}'\") # 简化处理,直接转字符串可能不够\n\n properties_str = \", \".join(properties_str_parts)\n return f\"({labels} {{{properties_str}}})\" if properties_str else f\"({labels})\"\n\n elif isinstance(value, Relationship):\n # 只显示关系类型\n return f\"-[:{value.type}]-\"\n # 如果需要包含关系属性,会更复杂\n # return f\"-[:{value.type} {{...}}]-\"\n\n elif isinstance(value, Path):\n path_parts = []\n for i, item in enumerate(value):\n if i % 2 == 0: # 节点\n path_parts.append(_format_neo4j_value_for_llm(item))\n else: # 关系\n # 路径中的关系是带有方向的,但这里的格式化简化为无方向\n path_parts.append(_format_neo4j_value_for_llm(item))\n return \"->\".join(path_parts) # 简化表示,不严格区分关系方向\n\n elif isinstance(value, list):\n # 格式化列表中的每个元素\n formatted_items = [_format_neo4j_value_for_llm(item) for item in value]\n return \"[\" + \", \".join(formatted_items) + \"]\"\n\n elif isinstance(value, dict):\n # 格式化字典中的键值对\n formatted_items = [f\"{k}: {_format_neo4j_value_for_llm(v)}\" for k, v in value.items()]\n return \"{\" + \", \".join(formatted_items) + \"}\"\n\n else:\n # 格式化基本数据类型\n return str(value)\n\n\ndef format_neo4j_results_for_llm(result_records: List[Record], returned_keys: List[str]) -> str:\n \"\"\"\n 将 Neo4j 查询返回的 Record 列表格式化为适合大模型阅读的字符串。\n \"\"\"\n if not result_records:\n return \"查询没有返回任何结果。\"\n\n formatted_output_lines = [\"查询结果如下:\"]\n\n for i, record in enumerate(result_records):\n formatted_output_lines.append(f\"--- 记录 {i + 1} ---\")\n record_str_parts = []\n # 遍历查询返回的每一列\n for key in returned_keys:\n value = record[key]\n formatted_value = _format_neo4j_value_for_llm(value)\n record_str_parts.append(f\"{key}: {formatted_value}\")\n formatted_output_lines.append(\", \".join(record_str_parts))\n\n return \"\\n\".join(formatted_output_lines)\n\n\n# --- Cypher 查询执行函数 ---\ndef execute_cypher_query(query_string: str) -> Tuple[Optional[List[Record]], Optional[str]]:\n \"\"\"\n 执行给定的 Cypher 查询字符串,返回查询结果(Record 列表)或错误信息。\n\n 参数:\n query_string: 要执行的 Cypher 查询字符串。\n\n 返回:\n 一个元组 (results, error_message)。\n 如果执行成功,results 是 Record 对象的列表,error_message 为 None。\n 如果执行失败,results 为 None,error_message 为错误字符串。\n \"\"\"\n driver = get_neo4j_driver()\n if driver is None:\n return None, \"Neo4j Driver 未成功初始化,无法连接数据库。\"\n\n try:\n with driver.session() as session:\n # 执行查询\n result = session.run(query_string, suppress=[\"Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning\"])\n # 获取所有返回的记录\n records = list(result)\n # 返回 Record 对象列表和 None 作为错误信息\n return records, None\n\n except Exception as e:\n # 捕获查询执行过程中的任何异常\n error_message = str(e)\n print(f\"Cypher 查询执行失败: {error_message}\\n查询: {query_string}\")\n return None, error_message\n\n\ndef extract_search_terms_from_cypher(cypher_string: str) -> List[str]:\n \"\"\"\n 尝试从 Cypher 查询字符串中提取用于匹配 Keyword, Topic 或 Title 属性的值。\n 这个函数使用简化的正则表达式,可能无法处理所有复杂的Cypher语法。\n\n 参数:\n cypher_string: LLM 生成的 Cypher 查询字符串。\n\n 返回:\n 提取到的潜在搜索词列表。\n \"\"\"\n # 这个正则表达式尝试匹配以下模式中的单引号或双引号括起来的字符串:\n # name: \"...\" 或 name: '...' (用于 Keyword, Topic, Author, Org 等)\n # title = \"...\" 或 title = '...'\n # title CONTAINS \"...\" 或 title CONTAINS '...'\n # title =~ \"...\" 或 title =~ '...' (以及 abstract =~)\n # abstract CONTAINS \"...\" 或 abstract CONTAINS '...'\n # abstract =~ \"...\" 或 abstract =~ '...'\n # ([^\"']+) 捕获引号内的内容,但不处理转义引号(简化的体现)\n # 使用非贪婪匹配 .*? 以防止跨越多个匹配项\n pattern = r\"(?:name|title)\\s*[:=~]?=?(?:\\s*CONTAINS)?\\s*[\\\"'](.*?)[\\\"']\"\n\n search_terms = []\n # 查找所有匹配项\n matches = re.findall(pattern, cypher_string, re.IGNORECASE | re.DOTALL) # 忽略大小写,让 '.' 匹配换行\n\n # 过滤掉可能是属性名本身或其他非搜索词的情况(简单过滤短词)\n for term in matches:\n term = term.strip()\n if term and len(term) > 1: # 过滤掉空字符串和过短的词\n search_terms.append(term)\n\n # 移除重复项\n search_terms = list(dict.fromkeys(search_terms))\n\n print(f\"从 Cypher 中提取的潜在搜索词: {search_terms}\")\n return search_terms\n\n\n# --- 与大模型交互的集成示例 (概念性代码) ---\n\n# 假设你已经有了大模型的客户端初始化代码\nfrom openai import OpenAI\nDEEPSEEK_API_KEY = \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\" # 你的 API Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-chat\" # 或者 \"deepseek-chat\" 如果你觉得它更适合处理结构化文本\n\n\n# 假设你的图谱 Schema 可以用文本描述(需要根据你的实际 Schema 编写)\n# 可以是节点标签、属性、关系类型及其方向的列表或结构化描述\nGRAPH_SCHEMA_DESCRIPTION = \"\"\"\n你的知识图谱包含以下节点和关系:\n节点标签:\n- Journal_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"journal\", \"notes\", \"pages\", \"reference_type\", \"url\"\n \"doi\", \"volume\", \"year\", \"issue\"}\n- Conference_Proceedings {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"pages\", \"reference_type\", \"url\", \"year\", \"secondary_title\", \n \"subsidiary_author\", \"tertiary_title\"} \n- Patent {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"subsidiary_author\", \"date\", \"srcdatabase\", \"subject\"}\n- Newspaper_Article {\"title\", \":ID\", \":LABEL\", \"database_provider\", \"doi\", \"notes\", \"pages\", \"reference_type\", \"url\", \"secondary_title\", \"date\"}\n- Thesis {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"reference_type\", \"url\", \"year\", \"type_of_work\"}\n- Other_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"date\"}\n- Book {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"pages\", \"reference_type\", \"date\", \"edition\"}\n- Author {\"name\", \":ID\", \":LABEL\"}\n- Keyword {\"name\", \":ID\", \":LABEL\"}\n- Organization {\"name\", \":ID\", \":LABEL\"}\n- Author_Address {\"name\", \":ID\", \":LABEL\"}\n- Topic {\"name\",, \":ID\", \":LABEL\"} \n\n关系类型:\n- (Author)-[:AUTHORED]->(Paper/Patent)\n- (Author)-[:TERTIARY_AUTHORED]->(Paper/Patent)\n- (Paper/Patent)-[:HAS_KEYWORD]->(Keyword)\n- (Paper/Patent)-[:PUBLISHED_BY]->(Organization)\n- (Paper/Patent)-[:HAS_AUTHOR_ADDRESS]->(AuthorAddress)\n- (Keyword)-[:ALIAS_OF]->(Keyword)\n- (Organization)-[:ALIAS_OF]->(Organization)\n- (AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)\n- (Paper/Patent)-[:HAS_TOPIC]->(Topic)\n\n查询时请使用节点属性进行匹配,例如 `title`, `name`, `paper_id`\n\"\"\"\n\n\ndef query_knowledge_graph_with_llm(user_question: str) -> str:\n \"\"\"\n 使用大模型回答关于知识图谱的问题。\n 这是一个简化的两阶段过程:用户问题 -> LLM生成Cypher -> 执行Cypher -> LLM根据结果生成答案。\n 实际问答系统会更复杂,包含多轮交互、意图识别、实体链接、回退机制等。\n \"\"\"\n # 阶段 1: LLM 将用户问题转换为 Cypher 查询\n print(f\"用户问题: {user_question}\")\n print(\"阶段 1: 请求 LLM 生成 Cypher 查询...\")\n\n cypher_generation_prompt = f\"\"\"\n 你的知识图谱Schema如下:\n {GRAPH_SCHEMA_DESCRIPTION}\n \n 重要说明:\n - **别名关系**: `(Keyword)-[:ALIAS_OF]->(Keyword)`, `(Organization)-[:ALIAS_OF]->(Organization)`, `(AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)`. 并非所有 Keyword/Organization/AuthorAddress 节点都有别名关系。\n - **部分节点属性可能缺失**: 例如,某些文献可能没有 year, abstract, url, doi 等属性。\n \n 生成Cypher查询的规则:\n 1. **处理可选关系 (如别名关系):** 当你需要查询一个节点及其别名或相关联的可选节点时,请使用 `OPTIONAL MATCH`。例如,要查找一个Keyword及其别名,使用 `MATCH (k:Keyword {{name: \"某个词\"}}) OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias)`。之后在 WHERE 子句中可以使用 `WHERE relatedNode = k OR relatedNode = alias` 来匹配原始节点或其别名。\n 2. **处理可选属性:** 在 WHERE 子句中直接比较属性值 (例如 `p.year = 2022`) 会自动排除缺失该属性的节点。如果你只想查找**拥有**某个特定属性的节点,可以使用 `WHERE node.property IS NOT NULL`。\n 3. **确保数据类型匹配:** 在比较属性值时,请根据Schema使用正确的数据类型,例如年份是整数,使用 `p.year = 2022` 而不是 `\"2022\"`。\n 4. **使用节点属性匹配:** 在 MATCH 或 MERGE 时,使用 `name`, `title`, `paper_id` 等属性来精确匹配节点。\n 5. **限制结果数量:** 通常在 RETURN 语句后加上 `LIMIT 10`,除非用户明确要求返回所有结果。\n \n 请严格按照上述Schema和规则生成Cypher查询语句,只输出查询本身,不要包含任何额外文字或解释。\n 用户的输入可能会带有错别字,请谨慎的判别是否有错别字并替换为正确的短语,若用户要查询的内容中带有其他语言,请翻译成中文查询。\n \n 示例1 (处理关键词别名和年份过滤):\n 用户问题: 2022年关于肺癌的论文有哪些?\n Cypher 查询:\n MATCH (k:Keyword {{name: \"肺癌\"}})\n OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias) // 使用 OPTIONAL MATCH 处理可选的别名关系\n MATCH (p)-[:HAS_KEYWORD]->(relatedKeyword) // 匹配与关键词相关的论文\n WHERE (relatedKeyword = k OR relatedKeyword = alias) // 相关的关键词是原始关键词或其别名\n AND (p:Paper OR p:Patent) // 确保是文献节点 (根据你的实际主要文献标签调整)\n AND p.year = 2022 // 过滤年份,直接比较数值类型\n RETURN p.title, p.year LIMIT 10\n \n 示例2 (查找所有有摘要的专利):\n 用户问题: 查找所有有摘要的专利。\n Cypher 查询:\n MATCH (p:Patent)\n WHERE p.abstract IS NOT NULL // 只匹配有摘要属性的专利\n RETURN p.title, p.abstract LIMIT 10\n \n 示例3 (查找某个作者在特定年份发表的论文):\n 用户问题: 张三在2023年发表了哪些论文?\n Cypher 查询:\n MATCH (a:Author {{name: \"张三\"}})-[:AUTHORED]->(p:Paper) // 假设AUTHORED关系只连接Paper\n WHERE p.year = 2023\n RETURN p.title, p.year LIMIT 10\n \n \n 用户问题: {user_question}\n Cypher 查询:\n \"\"\"\n\n messages_for_cypher = [\n {\"role\": \"system\", \"content\": cypher_generation_prompt},\n {\"role\": \"user\", \"content\": user_question} # 也可以把问题放在用户消息里,\n\n ]\n\n try:\n # 调用大模型 API\n response_cypher = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_cypher,\n temperature=0, # 生成 Cypher 需要确定性\n max_tokens=500 # 根据预期 Cypher 长度调整\n )\n\n generated_cypher = response_cypher.choices[0].message.content.strip()\n # 从生成的 Cypher 中提取搜索词\n potential_search_terms = extract_search_terms_from_cypher(generated_cypher)\n\n print(f\"LLM 生成的 Cypher 查询:\\n{generated_cypher}\")\n\n except Exception as e:\n print(f\"LLM 生成 Cypher 查询失败: {e}\")\n return f\"抱歉,无法将您的问题转换为图谱查询 ({e})\"\n\n # 阶段 2: 执行 Cypher 查询\n print(\"阶段 2: 执行 Cypher 查询...\")\n query_results, execution_error = execute_cypher_query(generated_cypher)\n\n # 阶段 3: LLM 根据查询结果生成最终答案\n if execution_error:\n # 如果执行失败,将错误信息反馈给用户(或者尝试让LLM修正查询)\n print(f\"查询执行错误: {execution_error}\")\n return f\"抱歉,执行图谱查询时出错: {execution_error}\"\n else:\n print(f\"查询执行成功,返回 {len(query_results)} 条记录。\")\n # 将查询结果格式化为文本\n formatted_results_text = format_neo4j_results_for_llm(query_results,\n query_results[0].keys() if query_results else [])\n\n # 如果结果为空,直接返回提示\n if \"查询没有返回任何结果\" in formatted_results_text:\n # 调用回退搜索函数\n fallback_answer = fallback_abstract_search(user_question, potential_search_terms)\n # 如果回退搜索找到了结果,返回回退结果;否则返回“未能找到信息”的提示\n return fallback_answer if fallback_answer is not None else \"抱歉,未能找到相关信息。\"\n\n print(\"阶段 3: 请求 LLM 根据结果生成自然语言答案...\")\n answer_generation_prompt = f\"\"\"\n 你是一个知识图谱问答助手。根据提供的查询结果,用自然语言回答用户的问题,回答要尽可能专业、逻辑清晰。\n 只使用提供的查询结果中的信息。如果结果无法回答问题,请说明。\n\n **特别注意:如果查询结果中包含了文献的 'abstract' (摘要) 属性,并且用户的问题与文献内容相关,请在回答时充分利用这些摘要信息。具体处理方法可以参考下面的内容。**\n - 如果问题是直接索取摘要(例如:“请给我XX论文的摘要”),你的回答应该清晰地呈现该摘要。\n - 如果问题是关于文献内容、主要观点、科研方法、主要贡献等,请基于摘要进行总结和回答。\n - 如果问题非常具体,请根据问题进行回答,问题可能是:研究问题、方法论、主要发现、创新点、局限性、结论等。\n - 如果摘要内容较长,可以进行适当的概括,但要忠于原文。\n\n 用户问题: {user_question}\n\n 查询结果:\n {formatted_results_text}\n\n 根据以上信息,用自然语言回答用户的问题:\n \"\"\"\n\n messages_for_answer = [\n {\"role\": \"system\", \"content\": answer_generation_prompt},\n {\"role\": \"user\", \"content\": f\"回答用户问题:'{user_question}'\"} # 再次强调问题\n ]\n\n try:\n # 调用大模型 API 生成答案\n response_answer = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_answer,\n temperature=0.5, # 生成答案可以稍微灵活\n max_tokens=500 # 根据预期答案长度调整\n )\n final_answer = response_answer.choices[0].message.content.strip()\n print(f\"LLM 生成的最终答案:\\n{final_answer}\")\n return final_answer\n\n except Exception as e:\n print(f\"LLM 生成最终答案失败: {e}\")\n return f\"抱歉,处理查询结果时遇到问题 ({e})\"\n\n\n# --- 回退机制:摘要文本搜索 ---\n\ndef fallback_abstract_search(user_question: str, search_terms: List[str], max_results: int = 100) -> Optional[str]:\n \"\"\"\n 当图谱查询无结果时,作为回退机制在文献摘要中进行文本搜索(使用 Cypher 正则),\n 并将匹配到的摘要交给大模型进行判断和总结。\n\n 参数:\n user_question: 用户的原始问题。\n llm_client: 大模型 API 客户端。\n search_terms: 从用户问题中提取的用于文本搜索的关键词/短语列表。\n max_results: 从数据库获取的最大匹配结果数量。\n\n 返回:\n LLM 基于匹配摘要生成的答案,或 None 表示未能找到相关信息。\n \"\"\"\n if not search_terms:\n print(\"回退搜索:未提取到搜索关键词。\")\n return None\n\n print(f\"回退搜索:图谱查询无结果,尝试在摘要中进行 Cypher 正则文本匹配。搜索词: {search_terms}\")\n\n # 1. 构建 Cypher 查询\n # Condition 1: Abstract matches ALL search terms\n if not search_terms:\n # Cannot match all terms if search_terms is empty, so this condition is false\n abstract_regex_condition = \"false\"\n else:\n # Build regex for matching ALL terms\n escaped_search_terms = [re.escape(term) for term in search_terms]\n # Pattern: (?i)(?=.*term1)(?=.*term2)...*\n abstract_regex_pattern = \"(?i)\" + \"\".join(f\"(?=.*{term})\" for term in escaped_search_terms) + \".*\"\n abstract_regex_condition = f\"p.abstract IS NOT NULL AND p.abstract =~ '{abstract_regex_pattern}'\"\n\n\n # Condition 2: Related Topic matches ANY search term\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n topic_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n topic_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n topic_condition = f\"topic IN topics WHERE topic.name IS NOT NULL AND topic.name =~ '(?i).*({topic_terms_list})'\"\n\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n address_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n address_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n address_condition = f\"address IN addresses WHERE address.name IS NOT NULL AND address.name =~ '(?i).*({address_terms_list})'\"\n\n\n # Combine the two conditions with OR\n # We need to structure the query to allow OPTIONAL MATCH for topic while filtering papers\n # Approach: Filter papers by abstract condition first (if applicable), then OPTIONAL MATCH topic, then filter by topic condition\n # Or, apply both conditions in the final WHERE after OPTIONAL MATCH\n # Let's use the latter for simplicity, assuming the abstract IS NOT NULL check is sufficient before regex\n\n fallback_query = f\"\"\"\n MATCH (p)\n WHERE (p:Patent OR p:Journal_Article OR p:Conference_Proceedings OR p:Thesis OR p:Other_Article OR p:Newspaper_Article OR p:Book)\n OPTIONAL MATCH (p)-[:HAS_TOPIC]->(t:Topic) // Optional match for Topic node\n OPTIONAL MATCH(p) - [: AUTHOR_ADDRESS]->(addr:Author_Address)\n WITH p, collect(t) AS topics, collect(addr) AS addresses\n WHERE ({abstract_regex_condition}) OR any({topic_condition}) OR any({address_condition}) // Apply the combined condition\n RETURN p // Return the node itself\n LIMIT {max_results} // Limit the number of results\n \"\"\"\n\n print(f\"回退搜索:执行 Cypher 联合查询:\\n{fallback_query}\")\n\n # 2. 执行 Cypher 联合查询\n abstract_records, error = execute_cypher_query(fallback_query)\n\n if error:\n print(f\"回退搜索:执行 Cypher 联合查询失败: {error}\")\n return None\n\n if not abstract_records:\n print(\"回退搜索:Cypher 联合查询没有匹配到任何结果。\")\n return None\n\n print(f\"回退搜索:Cypher 联合查询匹配到 {len(abstract_records)} 条结果:\")\n print(abstract_records)\n # 3. 将匹配到的结果格式化并发送给 LLM 进行判断和总结\n # 使用 format_neo4j_results_for_llm 格式化结果\n returned_keys = abstract_records[0].keys() if abstract_records else []\n formatted_abstract_results_text = format_neo4j_results_for_llm(abstract_records, returned_keys)\n\n print(\"回退搜索:将匹配结果发送给 LLM 进行判断和总结。\")\n\n abstract_summary_prompt = f\"\"\"\n 根据以下提供的文献信息(标题、摘要等)中与用户问题相关的内容,回答用户的原始问题。\n 只使用提供的信息进行回答。如果提供的信息都与问题不相关,请直接返回”查询没有返回任何结果“。\n 用自然语言回答,简洁明了。\n\n 用户原始问题: {user_question}\n\n 提供的文献信息:\n {formatted_abstract_results_text}\n\n 请根据以上信息,回答用户的问题:\n \"\"\"\n\n messages_for_summary = [\n {\"role\": \"system\", \"content\": abstract_summary_prompt},\n {\"role\": \"user\", \"content\": f\"总结与问题 '{user_question}' 相关的文献信息。\"}\n ]\n\n try:\n # 调用大模型 API\n summary_response = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL, # 使用与生成Cypher相同的模型,或根据需要选择\n messages=messages_for_summary,\n temperature=0.7, # 总结可以稍微灵活\n max_tokens=1000 # 总结长度,根据结果数量调整\n )\n summary_answer = summary_response.choices[0].message.content.strip()\n print(f\"回退搜索:LLM 总结的结果:\\n{summary_answer}\")\n\n # 简单的判断 LLM 是否认为结果相关(可能需要更健壮的判断逻辑)\n if \"查询没有返回任何结果\" in summary_answer:\n return None # LLM 认为结果不相关或无法总结\n\n return summary_answer # 返回 LLM 总结的内容\n\n except Exception as e:\n print(f\"回退搜索:LLM 总结失败: {e}\")\n return None # LLM 调用失败\n\n\n\ndef init():\n # 初始化 Neo4j Driver\n neo4j_driver = get_neo4j_driver()\n if neo4j_driver is None:\n exit(1) # 如果驱动初始化失败则退出\n\n # 导入大模型客户端 (需要安装 openai 库并配置 API Key)\n try:\n from openai import OpenAI\n\n # 请确保在这里设置你的 DeepSeek 或其他 API Key\n # 建议使用环境变量或配置文件\n DEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\")\n if not DEEPSEEK_API_KEY:\n print(\"错误:请设置 DEEPSEEK_API_KEY 环境变量或在代码中提供。\")\n exit(1)\n\n global llm_client_instance\n llm_client_instance = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n\n # 可以再次验证 LLM API 连接\n # llm_client_instance.models.list()\n print(\"大模型客户端初始化成功。\")\n\n except ImportError:\n print(\"错误:请安装 openai 库 (`pip install openai`) 以使用大模型API。\")\n exit(1)\n except Exception as e:\n print(f\"大模型客户端初始化失败: {e}\")\n exit(1)\n\n\n\ndef shutdown_resources():\n \"\"\"关闭资源\"\"\"\n close_neo4j_driver()\n\n\n# --- 主函数示例 (需要实际运行环境和 API Key) ---\nif __name__ == \"__main__\":\n llm_client_instance = init()\n\n # --- 示例问答流程 ---\n print(\"\\n--- 开始示例问答 ---\")\n\n # 确保你的数据库中有能匹配的节点和关系,以及对应的摘要等属性\n # 这里的示例问题需要LLM能够理解并转换为Cypher\n example_question = \"'生成式人工智能技术对教育领域的影响——关于ChatGPT的专访'这一篇文献的主要内容是什么?\"\n\n final_answer = query_knowledge_graph_with_llm(example_question)\n\n print(f\"\\n问答系统最终回答:\\n{final_answer}\")\n\n print(\"\\n--- 示例问答结束 ---\")\n\n # 关闭 Neo4j Driver\n close_neo4j_driver()\n"}, "30864247396a23788e355ae92704511f71ddba0c": {"t": 1785753222.9564571, "n": 19793, "h": "330a8d75bf3b8e73", "c": "import os\nimport re\nfrom typing import List, Tuple, Optional, Any, Dict\n\nfrom neo4j.graph import Node, Relationship, Path\nfrom neo4j import GraphDatabase, Record\n\n# --- Neo4j 连接信息 (需要根据你的实际情况修改) ---\n# 建议从配置文件或环境变量读取\nNEO4J_URI = \"bolt://1.92.129.26:2223\"\nNEO4J_USER = \"neo4j\"\nNEO4J_PASSWORD = \"123456788\"\n\n# 全局 Driver 实例\nneo4j_driver: Optional[GraphDatabase.driver] = None\n# 全局 LLM 客户端实例\nllm_client_instance: Optional[Any] = None # 使用 Any 类型,因为具体类型取决于导入的库\n\ndef get_neo4j_driver():\n \"\"\"获取或初始化 Neo4j Driver 实例\"\"\"\n global neo4j_driver\n if neo4j_driver is None:\n try:\n # 可以根据需要调整连接池大小等参数\n neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n # 验证连接\n neo4j_driver.verify_connectivity()\n print(\"Neo4j Driver 初始化成功。\")\n except Exception as e:\n print(f\"Neo4j Driver 初始化或连接失败: {e}\")\n neo4j_driver = None\n return neo4j_driver\n\n\ndef close_neo4j_driver():\n \"\"\"关闭 Neo4j Driver 连接\"\"\"\n global neo4j_driver\n if neo4j_driver:\n neo4j_driver.close()\n neo4j_driver = None\n print(\"Neo4j Driver 已关闭。\")\n\n\n# --- 结果格式化函数 ---\ndef _format_neo4j_value_for_llm(value: Any) -> str:\n \"\"\"\n 将 Neo4j 返回的不同类型的值格式化为适合大模型阅读的字符串表示。\n 尝试识别常见的节点ID属性(如 name, title, paper_id)。\n \"\"\"\n if isinstance(value, Node):\n labels = \":\".join(list(value.labels))\n properties_str_parts = []\n # 尝试获取主要的标识属性\n if 'name' in value:\n properties_str_parts.append(f\"name: '{value['name']}'\")\n if 'title' in value:\n properties_str_parts.append(f\"title: '{value['title']}'\")\n if 'paper_id' in value: # 假设你的 paper_id 属性\n properties_str_parts.append(f\"paper_id: '{value['paper_id']}'\")\n # 可以添加更多常见的标识属性\n\n # 如果没有找到标识属性,或者想包含更多属性,可以遍历所有属性\n for prop_key, prop_value in value.items():\n if prop_key not in ['name', 'title', 'paper_id']: # 避免重复添加\n # 递归格式化属性值,注意避免无限递归复杂对象\n properties_str_parts.append(f\"{prop_key}: '{prop_value}'\") # 简化处理,直接转字符串可能不够\n\n properties_str = \", \".join(properties_str_parts)\n return f\"({labels} {{{properties_str}}})\" if properties_str else f\"({labels})\"\n\n elif isinstance(value, Relationship):\n # 只显示关系类型\n return f\"-[:{value.type}]-\"\n # 如果需要包含关系属性,会更复杂\n # return f\"-[:{value.type} {{...}}]-\"\n\n elif isinstance(value, Path):\n path_parts = []\n for i, item in enumerate(value):\n if i % 2 == 0: # 节点\n path_parts.append(_format_neo4j_value_for_llm(item))\n else: # 关系\n # 路径中的关系是带有方向的,但这里的格式化简化为无方向\n path_parts.append(_format_neo4j_value_for_llm(item))\n return \"->\".join(path_parts) # 简化表示,不严格区分关系方向\n\n elif isinstance(value, list):\n # 格式化列表中的每个元素\n formatted_items = [_format_neo4j_value_for_llm(item) for item in value]\n return \"[\" + \", \".join(formatted_items) + \"]\"\n\n elif isinstance(value, dict):\n # 格式化字典中的键值对\n formatted_items = [f\"{k}: {_format_neo4j_value_for_llm(v)}\" for k, v in value.items()]\n return \"{\" + \", \".join(formatted_items) + \"}\"\n\n else:\n # 格式化基本数据类型\n return str(value)\n\n\ndef format_neo4j_results_for_llm(result_records: List[Record], returned_keys: List[str]) -> str:\n \"\"\"\n 将 Neo4j 查询返回的 Record 列表格式化为适合大模型阅读的字符串。\n \"\"\"\n if not result_records:\n return \"查询没有返回任何结果。\"\n\n formatted_output_lines = [\"查询结果如下:\"]\n\n for i, record in enumerate(result_records):\n formatted_output_lines.append(f\"--- 记录 {i + 1} ---\")\n record_str_parts = []\n # 遍历查询返回的每一列\n for key in returned_keys:\n value = record[key]\n formatted_value = _format_neo4j_value_for_llm(value)\n record_str_parts.append(f\"{key}: {formatted_value}\")\n formatted_output_lines.append(\", \".join(record_str_parts))\n\n return \"\\n\".join(formatted_output_lines)\n\n\n# --- Cypher 查询执行函数 ---\ndef execute_cypher_query(query_string: str) -> Tuple[Optional[List[Record]], Optional[str]]:\n \"\"\"\n 执行给定的 Cypher 查询字符串,返回查询结果(Record 列表)或错误信息。\n\n 参数:\n query_string: 要执行的 Cypher 查询字符串。\n\n 返回:\n 一个元组 (results, error_message)。\n 如果执行成功,results 是 Record 对象的列表,error_message 为 None。\n 如果执行失败,results 为 None,error_message 为错误字符串。\n \"\"\"\n driver = get_neo4j_driver()\n if driver is None:\n return None, \"Neo4j Driver 未成功初始化,无法连接数据库。\"\n\n try:\n with driver.session() as session:\n # 执行查询\n result = session.run(query_string, suppress=[\"Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning\"])\n # 获取所有返回的记录\n records = list(result)\n # 返回 Record 对象列表和 None 作为错误信息\n return records, None\n\n except Exception as e:\n # 捕获查询执行过程中的任何异常\n error_message = str(e)\n print(f\"Cypher 查询执行失败: {error_message}\\n查询: {query_string}\")\n return None, error_message\n\n\ndef extract_search_terms_from_cypher(cypher_string: str) -> List[str]:\n \"\"\"\n 尝试从 Cypher 查询字符串中提取用于匹配 Keyword, Topic 或 Title 属性的值。\n 这个函数使用简化的正则表达式,可能无法处理所有复杂的Cypher语法。\n\n 参数:\n cypher_string: LLM 生成的 Cypher 查询字符串。\n\n 返回:\n 提取到的潜在搜索词列表。\n \"\"\"\n # 这个正则表达式尝试匹配以下模式中的单引号或双引号括起来的字符串:\n # name: \"...\" 或 name: '...' (用于 Keyword, Topic, Author, Org 等)\n # title = \"...\" 或 title = '...'\n # title CONTAINS \"...\" 或 title CONTAINS '...'\n # title =~ \"...\" 或 title =~ '...' (以及 abstract =~)\n # abstract CONTAINS \"...\" 或 abstract CONTAINS '...'\n # abstract =~ \"...\" 或 abstract =~ '...'\n # ([^\"']+) 捕获引号内的内容,但不处理转义引号(简化的体现)\n # 使用非贪婪匹配 .*? 以防止跨越多个匹配项\n pattern = r\"(?:name|title)\\s*[:=~]?=?(?:\\s*CONTAINS)?\\s*[\\\"'](.*?)[\\\"']\"\n\n search_terms = []\n # 查找所有匹配项\n matches = re.findall(pattern, cypher_string, re.IGNORECASE | re.DOTALL) # 忽略大小写,让 '.' 匹配换行\n\n # 过滤掉可能是属性名本身或其他非搜索词的情况(简单过滤短词)\n for term in matches:\n term = term.strip()\n if term and len(term) > 1: # 过滤掉空字符串和过短的词\n search_terms.append(term)\n\n # 移除重复项\n search_terms = list(dict.fromkeys(search_terms))\n\n print(f\"从 Cypher 中提取的潜在搜索词: {search_terms}\")\n return search_terms\n\n\n# --- 与大模型交互的集成示例 (概念性代码) ---\n\n# 假设你已经有了大模型的客户端初始化代码\nfrom openai import OpenAI\nDEEPSEEK_API_KEY = \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\" # 你的 API Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-chat\" # 或者 \"deepseek-chat\" 如果你觉得它更适合处理结构化文本\n\n\n# 假设你的图谱 Schema 可以用文本描述(需要根据你的实际 Schema 编写)\n# 可以是节点标签、属性、关系类型及其方向的列表或结构化描述\nGRAPH_SCHEMA_DESCRIPTION = \"\"\"\n你的知识图谱包含以下节点和关系:\n节点标签:\n- Journal_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"journal\", \"notes\", \"pages\", \"reference_type\", \"url\"\n \"doi\", \"volume\", \"year\", \"issue\"}\n- Conference_Proceedings {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"pages\", \"reference_type\", \"url\", \"year\", \"secondary_title\", \n \"subsidiary_author\", \"tertiary_title\"} \n- Patent {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"subsidiary_author\", \"date\", \"srcdatabase\", \"subject\"}\n- Newspaper_Article {\"title\", \":ID\", \":LABEL\", \"database_provider\", \"doi\", \"notes\", \"pages\", \"reference_type\", \"url\", \"secondary_title\", \"date\"}\n- Thesis {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"reference_type\", \"url\", \"year\", \"type_of_work\"}\n- Other_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"date\"}\n- Book {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"pages\", \"reference_type\", \"date\", \"edition\"}\n- Author {\"name\", \":ID\", \":LABEL\"}\n- Keyword {\"name\", \":ID\", \":LABEL\"}\n- Organization {\"name\", \":ID\", \":LABEL\"}\n- Author_Address {\"name\", \":ID\", \":LABEL\"}\n- Topic {\"name\",, \":ID\", \":LABEL\"} \n\n关系类型:\n- (Author)-[:AUTHORED]->(Paper/Patent)\n- (Author)-[:TERTIARY_AUTHORED]->(Paper/Patent)\n- (Paper/Patent)-[:HAS_KEYWORD]->(Keyword)\n- (Paper/Patent)-[:PUBLISHED_BY]->(Organization)\n- (Paper/Patent)-[:HAS_AUTHOR_ADDRESS]->(AuthorAddress)\n- (Keyword)-[:ALIAS_OF]->(Keyword)\n- (Organization)-[:ALIAS_OF]->(Organization)\n- (AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)\n- (Paper/Patent)-[:HAS_TOPIC]->(Topic)\n\n查询时请使用节点属性进行匹配,例如 `title`, `name`, `paper_id`\n\"\"\"\n\n\ndef query_knowledge_graph_with_llm(user_question: str) -> str:\n \"\"\"\n 使用大模型回答关于知识图谱的问题。\n 这是一个简化的两阶段过程:用户问题 -> LLM生成Cypher -> 执行Cypher -> LLM根据结果生成答案。\n 实际问答系统会更复杂,包含多轮交互、意图识别、实体链接、回退机制等。\n \"\"\"\n # 阶段 1: LLM 将用户问题转换为 Cypher 查询\n print(f\"用户问题: {user_question}\")\n print(\"阶段 1: 请求 LLM 生成 Cypher 查询...\")\n\n cypher_generation_prompt = f\"\"\"\n 你的知识图谱Schema如下:\n {GRAPH_SCHEMA_DESCRIPTION}\n \n 重要说明:\n - **别名关系**: `(Keyword)-[:ALIAS_OF]->(Keyword)`, `(Organization)-[:ALIAS_OF]->(Organization)`, `(AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)`. 并非所有 Keyword/Organization/AuthorAddress 节点都有别名关系。\n - **部分节点属性可能缺失**: 例如,某些文献可能没有 year, abstract, url, doi 等属性。\n \n 生成Cypher查询的规则:\n 1. **处理可选关系 (如别名关系):** 当你需要查询一个节点及其别名或相关联的可选节点时,请使用 `OPTIONAL MATCH`。例如,要查找一个Keyword及其别名,使用 `MATCH (k:Keyword {{name: \"某个词\"}}) OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias)`。之后在 WHERE 子句中可以使用 `WHERE relatedNode = k OR relatedNode = alias` 来匹配原始节点或其别名。\n 2. **处理可选属性:** 在 WHERE 子句中直接比较属性值 (例如 `p.year = 2022`) 会自动排除缺失该属性的节点。如果你只想查找**拥有**某个特定属性的节点,可以使用 `WHERE node.property IS NOT NULL`。\n 3. **确保数据类型匹配:** 在比较属性值时,请根据Schema使用正确的数据类型,例如年份是整数,使用 `p.year = 2022` 而不是 `\"2022\"`。\n 4. **使用节点属性匹配:** 在 MATCH 或 MERGE 时,使用 `name`, `title`, `paper_id` 等属性来精确匹配节点。\n 5. **限制结果数量:** 通常在 RETURN 语句后加上 `LIMIT 10`,除非用户明确要求返回所有结果。\n \n 请严格按照上述Schema和规则生成Cypher查询语句,只输出查询本身,不要包含任何额外文字或解释。\n 用户的输入可能会带有错别字,请谨慎的判别是否有错别字并替换为正确的短语,若用户要查询的内容中带有其他语言,请翻译成中文查询。\n \n 示例1 (处理关键词别名和年份过滤):\n 用户问题: 2022年关于肺癌的论文有哪些?\n Cypher 查询:\n MATCH (k:Keyword {{name: \"肺癌\"}})\n OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias) // 使用 OPTIONAL MATCH 处理可选的别名关系\n MATCH (p)-[:HAS_KEYWORD]->(relatedKeyword) // 匹配与关键词相关的论文\n WHERE (relatedKeyword = k OR relatedKeyword = alias) // 相关的关键词是原始关键词或其别名\n AND (p:Paper OR p:Patent) // 确保是文献节点 (根据你的实际主要文献标签调整)\n AND p.year = 2022 // 过滤年份,直接比较数值类型\n RETURN p.title, p.year LIMIT 10\n \n 示例2 (查找所有有摘要的专利):\n 用户问题: 查找所有有摘要的专利。\n Cypher 查询:\n MATCH (p:Patent)\n WHERE p.abstract IS NOT NULL // 只匹配有摘要属性的专利\n RETURN p.title, p.abstract LIMIT 10\n \n 示例3 (查找某个作者在特定年份发表的论文):\n 用户问题: 张三在2023年发表了哪些论文?\n Cypher 查询:\n MATCH (a:Author {{name: \"张三\"}})-[:AUTHORED]->(p:Paper) // 假设AUTHORED关系只连接Paper\n WHERE p.year = 2023\n RETURN p.title, p.year LIMIT 10\n \n \n 用户问题: {user_question}\n Cypher 查询:\n \"\"\"\n\n messages_for_cypher = [\n {\"role\": \"system\", \"content\": cypher_generation_prompt},\n {\"role\": \"user\", \"content\": user_question} # 也可以把问题放在用户消息里,\n\n ]\n\n try:\n # 调用大模型 API\n response_cypher = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_cypher,\n temperature=0, # 生成 Cypher 需要确定性\n max_tokens=500 # 根据预期 Cypher 长度调整\n )\n\n generated_cypher = response_cypher.choices[0].message.content.strip()\n # 从生成的 Cypher 中提取搜索词\n potential_search_terms = extract_search_terms_from_cypher(generated_cypher)\n\n print(f\"LLM 生成的 Cypher 查询:\\n{generated_cypher}\")\n\n except Exception as e:\n print(f\"LLM 生成 Cypher 查询失败: {e}\")\n return f\"抱歉,无法将您的问题转换为图谱查询 ({e})\"\n\n # 阶段 2: 执行 Cypher 查询\n print(\"阶段 2: 执行 Cypher 查询...\")\n query_results, execution_error = execute_cypher_query(generated_cypher)\n\n # 阶段 3: LLM 根据查询结果生成最终答案\n if execution_error:\n # 如果执行失败,将错误信息反馈给用户(或者尝试让LLM修正查询)\n print(f\"查询执行错误: {execution_error}\")\n return f\"抱歉,执行图谱查询时出错: {execution_error}\"\n else:\n print(f\"查询执行成功,返回 {len(query_results)} 条记录。\")\n # 将查询结果格式化为文本\n formatted_results_text = format_neo4j_results_for_llm(query_results,\n query_results[0].keys() if query_results else [])\n\n # 如果结果为空,直接返回提示\n if \"查询没有返回任何结果\" in formatted_results_text:\n # 调用回退搜索函数\n fallback_answer = fallback_abstract_search(user_question, potential_search_terms)\n # 如果回退搜索找到了结果,返回回退结果;否则返回“未能找到信息”的提示\n return fallback_answer if fallback_answer is not None else \"抱歉,未能找到相关信息。\"\n\n print(\"阶段 3: 请求 LLM 根据结果生成自然语言答案...\")\n answer_generation_prompt = f\"\"\"\n 你是一个知识图谱问答助手。根据提供的查询结果,用自然语言回答用户的问题,回答要尽可能专业、逻辑清晰。\n 只使用提供的查询结果中的信息。如果结果无法回答问题,请说明。\n\n **特别注意:如果查询结果中包含了文献的 'abstract' (摘要) 属性,并且用户的问题与文献内容相关,请在回答时充分利用这些摘要信息。具体处理方法可以参考下面的内容。**\n - 如果问题是直接索取摘要(例如:“请给我XX论文的摘要”),你的回答应该清晰地呈现该摘要。\n - 如果问题是关于文献内容、主要观点、科研方法、主要贡献等,请基于摘要进行总结和回答。\n - 如果问题非常具体,请根据问题进行回答,问题可能是:研究问题、方法论、主要发现、创新点、局限性、结论等。\n - 如果摘要内容较长,可以进行适当的概括,但要忠于原文。\n\n 用户问题: {user_question}\n\n 查询结果:\n {formatted_results_text}\n\n 根据以上信息,用自然语言回答用户的问题:\n \"\"\"\n\n messages_for_answer = [\n {\"role\": \"system\", \"content\": answer_generation_prompt},\n {\"role\": \"user\", \"content\": f\"回答用户问题:'{user_question}'\"} # 再次强调问题\n ]\n\n try:\n # 调用大模型 API 生成答案\n response_answer = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_answer,\n temperature=0.5, # 生成答案可以稍微灵活\n max_tokens=500 # 根据预期答案长度调整\n )\n final_answer = response_answer.choices[0].message.content.strip()\n print(f\"LLM 生成的最终答案:\\n{final_answer}\")\n return final_answer\n\n except Exception as e:\n print(f\"LLM 生成最终答案失败: {e}\")\n return f\"抱歉,处理查询结果时遇到问题 ({e})\"\n\n\n# --- 回退机制:摘要文本搜索 ---\n\ndef fallback_abstract_search(user_question: str, search_terms: List[str], max_results: int = 100) -> Optional[str]:\n \"\"\"\n 当图谱查询无结果时,作为回退机制在文献摘要中进行文本搜索(使用 Cypher 正则),\n 并将匹配到的摘要交给大模型进行判断和总结。\n\n 参数:\n user_question: 用户的原始问题。\n llm_client: 大模型 API 客户端。\n search_terms: 从用户问题中提取的用于文本搜索的关键词/短语列表。\n max_results: 从数据库获取的最大匹配结果数量。\n\n 返回:\n LLM 基于匹配摘要生成的答案,或 None 表示未能找到相关信息。\n \"\"\"\n if not search_terms:\n print(\"回退搜索:未提取到搜索关键词。\")\n return None\n\n print(f\"回退搜索:图谱查询无结果,尝试在摘要中进行 Cypher 正则文本匹配。搜索词: {search_terms}\")\n\n # 1. 构建 Cypher 查询\n # Condition 1: Abstract matches ALL search terms\n if not search_terms:\n # Cannot match all terms if search_terms is empty, so this condition is false\n abstract_regex_condition = \"false\"\n else:\n # Build regex for matching ALL terms\n escaped_search_terms = [re.escape(term) for term in search_terms]\n # Pattern: (?i)(?=.*term1)(?=.*term2)...*\n abstract_regex_pattern = \"(?i)\" + \"\".join(f\"(?=.*{term})\" for term in escaped_search_terms) + \".*\"\n abstract_regex_condition = f\"p.abstract IS NOT NULL AND p.abstract =~ '{abstract_regex_pattern}'\"\n\n\n # Condition 2: Related Topic matches ANY search term\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n topic_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n topic_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n topic_condition = f\"topic IN topics WHERE topic.name IS NOT NULL AND topic.name =~ '(?i).*({topic_terms_list})'\"\n\n\n # Combine the two conditions with OR\n # We need to structure the query to allow OPTIONAL MATCH for topic while filtering papers\n # Approach: Filter papers by abstract condition first (if applicable), then OPTIONAL MATCH topic, then filter by topic condition\n # Or, apply both conditions in the final WHERE after OPTIONAL MATCH\n # Let's use the latter for simplicity, assuming the abstract IS NOT NULL check is sufficient before regex\n\n fallback_query = f\"\"\"\n MATCH (p)\n WHERE (p:Patent OR p:Journal_Article OR p:Conference_Proceedings OR p:Thesis OR p:Other_Article OR p:Newspaper_Article OR p:Book)\n OPTIONAL MATCH (p)-[:HAS_TOPIC]->(t:Topic) // Optional match for Topic node\n WITH p, collect(t) AS topics\n WHERE ({abstract_regex_condition}) OR any({topic_condition}) // Apply the combined condition\n RETURN p // Return the node itself\n LIMIT {max_results} // Limit the number of results\n \"\"\"\n\n print(f\"回退搜索:执行 Cypher 联合查询:\\n{fallback_query}\")\n\n # 2. 执行 Cypher 联合查询\n abstract_records, error = execute_cypher_query(fallback_query)\n\n if error:\n print(f\"回退搜索:执行 Cypher 联合查询失败: {error}\")\n return None\n\n if not abstract_records:\n print(\"回退搜索:Cypher 联合查询没有匹配到任何结果。\")\n return None\n\n print(f\"回退搜索:Cypher 联合查询匹配到 {len(abstract_records)} 条结果:\")\n print(abstract_records)\n # 3. 将匹配到的结果格式化并发送给 LLM 进行判断和总结\n # 使用 format_neo4j_results_for_llm 格式化结果\n returned_keys = abstract_records[0].keys() if abstract_records else []\n formatted_abstract_results_text = format_neo4j_results_for_llm(abstract_records, returned_keys)\n\n print(\"回退搜索:将匹配结果发送给 LLM 进行判断和总结。\")\n\n abstract_summary_prompt = f\"\"\"\n 根据以下提供的文献信息(标题、摘要等)中与用户问题相关的内容,回答用户的原始问题。\n 只使用提供的信息进行回答。如果提供的信息都与问题不相关,请直接返回”查询没有返回任何结果“。\n 用自然语言回答,简洁明了。\n\n 用户原始问题: {user_question}\n\n 提供的文献信息:\n {formatted_abstract_results_text}\n\n 请根据以上信息,回答用户的问题:\n \"\"\"\n\n messages_for_summary = [\n {\"role\": \"system\", \"content\": abstract_summary_prompt},\n {\"role\": \"user\", \"content\": f\"总结与问题 '{user_question}' 相关的文献信息。\"}\n ]\n\n try:\n # 调用大模型 API\n summary_response = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL, # 使用与生成Cypher相同的模型,或根据需要选择\n messages=messages_for_summary,\n temperature=0.7, # 总结可以稍微灵活\n max_tokens=1000 # 总结长度,根据结果数量调整\n )\n summary_answer = summary_response.choices[0].message.content.strip()\n print(f\"回退搜索:LLM 总结的结果:\\n{summary_answer}\")\n\n # 简单的判断 LLM 是否认为结果相关(可能需要更健壮的判断逻辑)\n if \"查询没有返回任何结果\" in summary_answer:\n return None # LLM 认为结果不相关或无法总结\n\n return summary_answer # 返回 LLM 总结的内容\n\n except Exception as e:\n print(f\"回退搜索:LLM 总结失败: {e}\")\n return None # LLM 调用失败\n\n\n\ndef init():\n # 初始化 Neo4j Driver\n neo4j_driver = get_neo4j_driver()\n if neo4j_driver is None:\n exit(1) # 如果驱动初始化失败则退出\n\n # 导入大模型客户端 (需要安装 openai 库并配置 API Key)\n try:\n from openai import OpenAI\n\n # 请确保在这里设置你的 DeepSeek 或其他 API Key\n # 建议使用环境变量或配置文件\n DEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\")\n if not DEEPSEEK_API_KEY:\n print(\"错误:请设置 DEEPSEEK_API_KEY 环境变量或在代码中提供。\")\n exit(1)\n\n global llm_client_instance\n llm_client_instance = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n\n # 可以再次验证 LLM API 连接\n # llm_client_instance.models.list()\n print(\"大模型客户端初始化成功。\")\n\n except ImportError:\n print(\"错误:请安装 openai 库 (`pip install openai`) 以使用大模型API。\")\n exit(1)\n except Exception as e:\n print(f\"大模型客户端初始化失败: {e}\")\n exit(1)\n\n\n\ndef shutdown_resources():\n \"\"\"关闭资源\"\"\"\n close_neo4j_driver()\n\n\n# --- 主函数示例 (需要实际运行环境和 API Key) ---\nif __name__ == \"__main__\":\n llm_client_instance = init()\n\n # --- 示例问答流程 ---\n print(\"\\n--- 开始示例问答 ---\")\n\n # 确保你的数据库中有能匹配的节点和关系,以及对应的摘要等属性\n # 这里的示例问题需要LLM能够理解并转换为Cypher\n example_question = \"'生成式人工智能技术对教育领域的影响——关于ChatGPT的专访'这一篇文献的主要内容是什么?\"\n\n final_answer = query_knowledge_graph_with_llm(example_question)\n\n print(f\"\\n问答系统最终回答:\\n{final_answer}\")\n\n print(\"\\n--- 示例问答结束 ---\")\n\n # 关闭 Neo4j Driver\n close_neo4j_driver()\n"}, "4c7dc9d59cc178cc5373b2acc2b7c90c386185d0": {"t": 1785753224.390499, "n": 19794, "h": "56f3b9b695a71fa7", "c": "import os\nimport re\nfrom typing import List, Tuple, Optional, Any, Dict\n\nfrom neo4j.graph import Node, Relationship, Path\nfrom neo4j import GraphDatabase, Record\n\n# --- Neo4j 连接信息 (需要根据你的实际情况修改) ---\n# 建议从配置文件或环境变量读取\nNEO4J_URI = \"bolt://10.5.132.251:7687\"\nNEO4J_USER = \"neo4j\"\nNEO4J_PASSWORD = \"123456788\"\n\n# 全局 Driver 实例\nneo4j_driver: Optional[GraphDatabase.driver] = None\n# 全局 LLM 客户端实例\nllm_client_instance: Optional[Any] = None # 使用 Any 类型,因为具体类型取决于导入的库\n\ndef get_neo4j_driver():\n \"\"\"获取或初始化 Neo4j Driver 实例\"\"\"\n global neo4j_driver\n if neo4j_driver is None:\n try:\n # 可以根据需要调整连接池大小等参数\n neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n # 验证连接\n neo4j_driver.verify_connectivity()\n print(\"Neo4j Driver 初始化成功。\")\n except Exception as e:\n print(f\"Neo4j Driver 初始化或连接失败: {e}\")\n neo4j_driver = None\n return neo4j_driver\n\n\ndef close_neo4j_driver():\n \"\"\"关闭 Neo4j Driver 连接\"\"\"\n global neo4j_driver\n if neo4j_driver:\n neo4j_driver.close()\n neo4j_driver = None\n print(\"Neo4j Driver 已关闭。\")\n\n\n# --- 结果格式化函数 ---\ndef _format_neo4j_value_for_llm(value: Any) -> str:\n \"\"\"\n 将 Neo4j 返回的不同类型的值格式化为适合大模型阅读的字符串表示。\n 尝试识别常见的节点ID属性(如 name, title, paper_id)。\n \"\"\"\n if isinstance(value, Node):\n labels = \":\".join(list(value.labels))\n properties_str_parts = []\n # 尝试获取主要的标识属性\n if 'name' in value:\n properties_str_parts.append(f\"name: '{value['name']}'\")\n if 'title' in value:\n properties_str_parts.append(f\"title: '{value['title']}'\")\n if 'paper_id' in value: # 假设你的 paper_id 属性\n properties_str_parts.append(f\"paper_id: '{value['paper_id']}'\")\n # 可以添加更多常见的标识属性\n\n # 如果没有找到标识属性,或者想包含更多属性,可以遍历所有属性\n for prop_key, prop_value in value.items():\n if prop_key not in ['name', 'title', 'paper_id']: # 避免重复添加\n # 递归格式化属性值,注意避免无限递归复杂对象\n properties_str_parts.append(f\"{prop_key}: '{prop_value}'\") # 简化处理,直接转字符串可能不够\n\n properties_str = \", \".join(properties_str_parts)\n return f\"({labels} {{{properties_str}}})\" if properties_str else f\"({labels})\"\n\n elif isinstance(value, Relationship):\n # 只显示关系类型\n return f\"-[:{value.type}]-\"\n # 如果需要包含关系属性,会更复杂\n # return f\"-[:{value.type} {{...}}]-\"\n\n elif isinstance(value, Path):\n path_parts = []\n for i, item in enumerate(value):\n if i % 2 == 0: # 节点\n path_parts.append(_format_neo4j_value_for_llm(item))\n else: # 关系\n # 路径中的关系是带有方向的,但这里的格式化简化为无方向\n path_parts.append(_format_neo4j_value_for_llm(item))\n return \"->\".join(path_parts) # 简化表示,不严格区分关系方向\n\n elif isinstance(value, list):\n # 格式化列表中的每个元素\n formatted_items = [_format_neo4j_value_for_llm(item) for item in value]\n return \"[\" + \", \".join(formatted_items) + \"]\"\n\n elif isinstance(value, dict):\n # 格式化字典中的键值对\n formatted_items = [f\"{k}: {_format_neo4j_value_for_llm(v)}\" for k, v in value.items()]\n return \"{\" + \", \".join(formatted_items) + \"}\"\n\n else:\n # 格式化基本数据类型\n return str(value)\n\n\ndef format_neo4j_results_for_llm(result_records: List[Record], returned_keys: List[str]) -> str:\n \"\"\"\n 将 Neo4j 查询返回的 Record 列表格式化为适合大模型阅读的字符串。\n \"\"\"\n if not result_records:\n return \"查询没有返回任何结果。\"\n\n formatted_output_lines = [\"查询结果如下:\"]\n\n for i, record in enumerate(result_records):\n formatted_output_lines.append(f\"--- 记录 {i + 1} ---\")\n record_str_parts = []\n # 遍历查询返回的每一列\n for key in returned_keys:\n value = record[key]\n formatted_value = _format_neo4j_value_for_llm(value)\n record_str_parts.append(f\"{key}: {formatted_value}\")\n formatted_output_lines.append(\", \".join(record_str_parts))\n\n return \"\\n\".join(formatted_output_lines)\n\n\n# --- Cypher 查询执行函数 ---\ndef execute_cypher_query(query_string: str) -> Tuple[Optional[List[Record]], Optional[str]]:\n \"\"\"\n 执行给定的 Cypher 查询字符串,返回查询结果(Record 列表)或错误信息。\n\n 参数:\n query_string: 要执行的 Cypher 查询字符串。\n\n 返回:\n 一个元组 (results, error_message)。\n 如果执行成功,results 是 Record 对象的列表,error_message 为 None。\n 如果执行失败,results 为 None,error_message 为错误字符串。\n \"\"\"\n driver = get_neo4j_driver()\n if driver is None:\n return None, \"Neo4j Driver 未成功初始化,无法连接数据库。\"\n\n try:\n with driver.session() as session:\n # 执行查询\n result = session.run(query_string, suppress=[\"Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning\"])\n # 获取所有返回的记录\n records = list(result)\n # 返回 Record 对象列表和 None 作为错误信息\n return records, None\n\n except Exception as e:\n # 捕获查询执行过程中的任何异常\n error_message = str(e)\n print(f\"Cypher 查询执行失败: {error_message}\\n查询: {query_string}\")\n return None, error_message\n\n\ndef extract_search_terms_from_cypher(cypher_string: str) -> List[str]:\n \"\"\"\n 尝试从 Cypher 查询字符串中提取用于匹配 Keyword, Topic 或 Title 属性的值。\n 这个函数使用简化的正则表达式,可能无法处理所有复杂的Cypher语法。\n\n 参数:\n cypher_string: LLM 生成的 Cypher 查询字符串。\n\n 返回:\n 提取到的潜在搜索词列表。\n \"\"\"\n # 这个正则表达式尝试匹配以下模式中的单引号或双引号括起来的字符串:\n # name: \"...\" 或 name: '...' (用于 Keyword, Topic, Author, Org 等)\n # title = \"...\" 或 title = '...'\n # title CONTAINS \"...\" 或 title CONTAINS '...'\n # title =~ \"...\" 或 title =~ '...' (以及 abstract =~)\n # abstract CONTAINS \"...\" 或 abstract CONTAINS '...'\n # abstract =~ \"...\" 或 abstract =~ '...'\n # ([^\"']+) 捕获引号内的内容,但不处理转义引号(简化的体现)\n # 使用非贪婪匹配 .*? 以防止跨越多个匹配项\n pattern = r\"(?:name|title)\\s*[:=~]?=?(?:\\s*CONTAINS)?\\s*[\\\"'](.*?)[\\\"']\"\n\n search_terms = []\n # 查找所有匹配项\n matches = re.findall(pattern, cypher_string, re.IGNORECASE | re.DOTALL) # 忽略大小写,让 '.' 匹配换行\n\n # 过滤掉可能是属性名本身或其他非搜索词的情况(简单过滤短词)\n for term in matches:\n term = term.strip()\n if term and len(term) > 1: # 过滤掉空字符串和过短的词\n search_terms.append(term)\n\n # 移除重复项\n search_terms = list(dict.fromkeys(search_terms))\n\n print(f\"从 Cypher 中提取的潜在搜索词: {search_terms}\")\n return search_terms\n\n\n# --- 与大模型交互的集成示例 (概念性代码) ---\n\n# 假设你已经有了大模型的客户端初始化代码\nfrom openai import OpenAI\nDEEPSEEK_API_KEY = \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\" # 你的 API Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-chat\" # 或者 \"deepseek-chat\" 如果你觉得它更适合处理结构化文本\n\n\n# 假设你的图谱 Schema 可以用文本描述(需要根据你的实际 Schema 编写)\n# 可以是节点标签、属性、关系类型及其方向的列表或结构化描述\nGRAPH_SCHEMA_DESCRIPTION = \"\"\"\n你的知识图谱包含以下节点和关系:\n节点标签:\n- Journal_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"journal\", \"notes\", \"pages\", \"reference_type\", \"url\"\n \"doi\", \"volume\", \"year\", \"issue\"}\n- Conference_Proceedings {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"pages\", \"reference_type\", \"url\", \"year\", \"secondary_title\", \n \"subsidiary_author\", \"tertiary_title\"} \n- Patent {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"subsidiary_author\", \"date\", \"srcdatabase\", \"subject\"}\n- Newspaper_Article {\"title\", \":ID\", \":LABEL\", \"database_provider\", \"doi\", \"notes\", \"pages\", \"reference_type\", \"url\", \"secondary_title\", \"date\"}\n- Thesis {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"reference_type\", \"url\", \"year\", \"type_of_work\"}\n- Other_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"date\"}\n- Book {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"pages\", \"reference_type\", \"date\", \"edition\"}\n- Author {\"name\", \":ID\", \":LABEL\"}\n- Keyword {\"name\", \":ID\", \":LABEL\"}\n- Organization {\"name\", \":ID\", \":LABEL\"}\n- Author_Address {\"name\", \":ID\", \":LABEL\"}\n- Topic {\"name\",, \":ID\", \":LABEL\"} \n\n关系类型:\n- (Author)-[:AUTHORED]->(Paper/Patent)\n- (Author)-[:TERTIARY_AUTHORED]->(Paper/Patent)\n- (Paper/Patent)-[:HAS_KEYWORD]->(Keyword)\n- (Paper/Patent)-[:PUBLISHED_BY]->(Organization)\n- (Paper/Patent)-[:HAS_AUTHOR_ADDRESS]->(AuthorAddress)\n- (Keyword)-[:ALIAS_OF]->(Keyword)\n- (Organization)-[:ALIAS_OF]->(Organization)\n- (AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)\n- (Paper/Patent)-[:HAS_TOPIC]->(Topic)\n\n查询时请使用节点属性进行匹配,例如 `title`, `name`, `paper_id`\n\"\"\"\n\n\ndef query_knowledge_graph_with_llm(user_question: str) -> str:\n \"\"\"\n 使用大模型回答关于知识图谱的问题。\n 这是一个简化的两阶段过程:用户问题 -> LLM生成Cypher -> 执行Cypher -> LLM根据结果生成答案。\n 实际问答系统会更复杂,包含多轮交互、意图识别、实体链接、回退机制等。\n \"\"\"\n # 阶段 1: LLM 将用户问题转换为 Cypher 查询\n print(f\"用户问题: {user_question}\")\n print(\"阶段 1: 请求 LLM 生成 Cypher 查询...\")\n\n cypher_generation_prompt = f\"\"\"\n 你的知识图谱Schema如下:\n {GRAPH_SCHEMA_DESCRIPTION}\n \n 重要说明:\n - **别名关系**: `(Keyword)-[:ALIAS_OF]->(Keyword)`, `(Organization)-[:ALIAS_OF]->(Organization)`, `(AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)`. 并非所有 Keyword/Organization/AuthorAddress 节点都有别名关系。\n - **部分节点属性可能缺失**: 例如,某些文献可能没有 year, abstract, url, doi 等属性。\n \n 生成Cypher查询的规则:\n 1. **处理可选关系 (如别名关系):** 当你需要查询一个节点及其别名或相关联的可选节点时,请使用 `OPTIONAL MATCH`。例如,要查找一个Keyword及其别名,使用 `MATCH (k:Keyword {{name: \"某个词\"}}) OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias)`。之后在 WHERE 子句中可以使用 `WHERE relatedNode = k OR relatedNode = alias` 来匹配原始节点或其别名。\n 2. **处理可选属性:** 在 WHERE 子句中直接比较属性值 (例如 `p.year = 2022`) 会自动排除缺失该属性的节点。如果你只想查找**拥有**某个特定属性的节点,可以使用 `WHERE node.property IS NOT NULL`。\n 3. **确保数据类型匹配:** 在比较属性值时,请根据Schema使用正确的数据类型,例如年份是整数,使用 `p.year = 2022` 而不是 `\"2022\"`。\n 4. **使用节点属性匹配:** 在 MATCH 或 MERGE 时,使用 `name`, `title`, `paper_id` 等属性来精确匹配节点。\n 5. **限制结果数量:** 通常在 RETURN 语句后加上 `LIMIT 10`,除非用户明确要求返回所有结果。\n \n 请严格按照上述Schema和规则生成Cypher查询语句,只输出查询本身,不要包含任何额外文字或解释。\n 用户的输入可能会带有错别字,请谨慎的判别是否有错别字并替换为正确的短语,若用户要查询的内容中带有其他语言,请翻译成中文查询。\n \n 示例1 (处理关键词别名和年份过滤):\n 用户问题: 2022年关于肺癌的论文有哪些?\n Cypher 查询:\n MATCH (k:Keyword {{name: \"肺癌\"}})\n OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias) // 使用 OPTIONAL MATCH 处理可选的别名关系\n MATCH (p)-[:HAS_KEYWORD]->(relatedKeyword) // 匹配与关键词相关的论文\n WHERE (relatedKeyword = k OR relatedKeyword = alias) // 相关的关键词是原始关键词或其别名\n AND (p:Paper OR p:Patent) // 确保是文献节点 (根据你的实际主要文献标签调整)\n AND p.year = 2022 // 过滤年份,直接比较数值类型\n RETURN p.title, p.year LIMIT 10\n \n 示例2 (查找所有有摘要的专利):\n 用户问题: 查找所有有摘要的专利。\n Cypher 查询:\n MATCH (p:Patent)\n WHERE p.abstract IS NOT NULL // 只匹配有摘要属性的专利\n RETURN p.title, p.abstract LIMIT 10\n \n 示例3 (查找某个作者在特定年份发表的论文):\n 用户问题: 张三在2023年发表了哪些论文?\n Cypher 查询:\n MATCH (a:Author {{name: \"张三\"}})-[:AUTHORED]->(p:Paper) // 假设AUTHORED关系只连接Paper\n WHERE p.year = 2023\n RETURN p.title, p.year LIMIT 10\n \n \n 用户问题: {user_question}\n Cypher 查询:\n \"\"\"\n\n messages_for_cypher = [\n {\"role\": \"system\", \"content\": cypher_generation_prompt},\n {\"role\": \"user\", \"content\": user_question} # 也可以把问题放在用户消息里,\n\n ]\n\n try:\n # 调用大模型 API\n response_cypher = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_cypher,\n temperature=0, # 生成 Cypher 需要确定性\n max_tokens=500 # 根据预期 Cypher 长度调整\n )\n\n generated_cypher = response_cypher.choices[0].message.content.strip()\n # 从生成的 Cypher 中提取搜索词\n potential_search_terms = extract_search_terms_from_cypher(generated_cypher)\n\n print(f\"LLM 生成的 Cypher 查询:\\n{generated_cypher}\")\n\n except Exception as e:\n print(f\"LLM 生成 Cypher 查询失败: {e}\")\n return f\"抱歉,无法将您的问题转换为图谱查询 ({e})\"\n\n # 阶段 2: 执行 Cypher 查询\n print(\"阶段 2: 执行 Cypher 查询...\")\n query_results, execution_error = execute_cypher_query(generated_cypher)\n\n # 阶段 3: LLM 根据查询结果生成最终答案\n if execution_error:\n # 如果执行失败,将错误信息反馈给用户(或者尝试让LLM修正查询)\n print(f\"查询执行错误: {execution_error}\")\n return f\"抱歉,执行图谱查询时出错: {execution_error}\"\n else:\n print(f\"查询执行成功,返回 {len(query_results)} 条记录。\")\n # 将查询结果格式化为文本\n formatted_results_text = format_neo4j_results_for_llm(query_results,\n query_results[0].keys() if query_results else [])\n\n # 如果结果为空,直接返回提示\n if \"查询没有返回任何结果\" in formatted_results_text:\n # 调用回退搜索函数\n fallback_answer = fallback_abstract_search(user_question, potential_search_terms)\n # 如果回退搜索找到了结果,返回回退结果;否则返回“未能找到信息”的提示\n return fallback_answer if fallback_answer is not None else \"抱歉,未能找到相关信息。\"\n\n print(\"阶段 3: 请求 LLM 根据结果生成自然语言答案...\")\n answer_generation_prompt = f\"\"\"\n 你是一个知识图谱问答助手。根据提供的查询结果,用自然语言回答用户的问题,回答要尽可能专业、逻辑清晰。\n 只使用提供的查询结果中的信息。如果结果无法回答问题,请说明。\n\n **特别注意:如果查询结果中包含了文献的 'abstract' (摘要) 属性,并且用户的问题与文献内容相关,请在回答时充分利用这些摘要信息。具体处理方法可以参考下面的内容。**\n - 如果问题是直接索取摘要(例如:“请给我XX论文的摘要”),你的回答应该清晰地呈现该摘要。\n - 如果问题是关于文献内容、主要观点、科研方法、主要贡献等,请基于摘要进行总结和回答。\n - 如果问题非常具体,请根据问题进行回答,问题可能是:研究问题、方法论、主要发现、创新点、局限性、结论等。\n - 如果摘要内容较长,可以进行适当的概括,但要忠于原文。\n\n 用户问题: {user_question}\n\n 查询结果:\n {formatted_results_text}\n\n 根据以上信息,用自然语言回答用户的问题:\n \"\"\"\n\n messages_for_answer = [\n {\"role\": \"system\", \"content\": answer_generation_prompt},\n {\"role\": \"user\", \"content\": f\"回答用户问题:'{user_question}'\"} # 再次强调问题\n ]\n\n try:\n # 调用大模型 API 生成答案\n response_answer = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_answer,\n temperature=0.5, # 生成答案可以稍微灵活\n max_tokens=500 # 根据预期答案长度调整\n )\n final_answer = response_answer.choices[0].message.content.strip()\n print(f\"LLM 生成的最终答案:\\n{final_answer}\")\n return final_answer\n\n except Exception as e:\n print(f\"LLM 生成最终答案失败: {e}\")\n return f\"抱歉,处理查询结果时遇到问题 ({e})\"\n\n\n# --- 回退机制:摘要文本搜索 ---\n\ndef fallback_abstract_search(user_question: str, search_terms: List[str], max_results: int = 100) -> Optional[str]:\n \"\"\"\n 当图谱查询无结果时,作为回退机制在文献摘要中进行文本搜索(使用 Cypher 正则),\n 并将匹配到的摘要交给大模型进行判断和总结。\n\n 参数:\n user_question: 用户的原始问题。\n llm_client: 大模型 API 客户端。\n search_terms: 从用户问题中提取的用于文本搜索的关键词/短语列表。\n max_results: 从数据库获取的最大匹配结果数量。\n\n 返回:\n LLM 基于匹配摘要生成的答案,或 None 表示未能找到相关信息。\n \"\"\"\n if not search_terms:\n print(\"回退搜索:未提取到搜索关键词。\")\n return None\n\n print(f\"回退搜索:图谱查询无结果,尝试在摘要中进行 Cypher 正则文本匹配。搜索词: {search_terms}\")\n\n # 1. 构建 Cypher 查询\n # Condition 1: Abstract matches ALL search terms\n if not search_terms:\n # Cannot match all terms if search_terms is empty, so this condition is false\n abstract_regex_condition = \"false\"\n else:\n # Build regex for matching ALL terms\n escaped_search_terms = [re.escape(term) for term in search_terms]\n # Pattern: (?i)(?=.*term1)(?=.*term2)...*\n abstract_regex_pattern = \"(?i)\" + \"\".join(f\"(?=.*{term})\" for term in escaped_search_terms) + \".*\"\n abstract_regex_condition = f\"p.abstract IS NOT NULL AND p.abstract =~ '{abstract_regex_pattern}'\"\n\n\n # Condition 2: Related Topic matches ANY search term\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n topic_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n topic_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n topic_condition = f\"topic IN topics WHERE topic.name IS NOT NULL AND topic.name =~ '(?i).*({topic_terms_list})'\"\n\n\n # Combine the two conditions with OR\n # We need to structure the query to allow OPTIONAL MATCH for topic while filtering papers\n # Approach: Filter papers by abstract condition first (if applicable), then OPTIONAL MATCH topic, then filter by topic condition\n # Or, apply both conditions in the final WHERE after OPTIONAL MATCH\n # Let's use the latter for simplicity, assuming the abstract IS NOT NULL check is sufficient before regex\n\n fallback_query = f\"\"\"\n MATCH (p)\n WHERE (p:Patent OR p:Journal_Article OR p:Conference_Proceedings OR p:Thesis OR p:Other_Article OR p:Newspaper_Article OR p:Book)\n OPTIONAL MATCH (p)-[:HAS_TOPIC]->(t:Topic) // Optional match for Topic node\n WITH p, collect(t) AS topics\n WHERE ({abstract_regex_condition}) OR any({topic_condition}) // Apply the combined condition\n RETURN p // Return the node itself\n LIMIT {max_results} // Limit the number of results\n \"\"\"\n\n print(f\"回退搜索:执行 Cypher 联合查询:\\n{fallback_query}\")\n\n # 2. 执行 Cypher 联合查询\n abstract_records, error = execute_cypher_query(fallback_query)\n\n if error:\n print(f\"回退搜索:执行 Cypher 联合查询失败: {error}\")\n return None\n\n if not abstract_records:\n print(\"回退搜索:Cypher 联合查询没有匹配到任何结果。\")\n return None\n\n print(f\"回退搜索:Cypher 联合查询匹配到 {len(abstract_records)} 条结果:\")\n print(abstract_records)\n # 3. 将匹配到的结果格式化并发送给 LLM 进行判断和总结\n # 使用 format_neo4j_results_for_llm 格式化结果\n returned_keys = abstract_records[0].keys() if abstract_records else []\n formatted_abstract_results_text = format_neo4j_results_for_llm(abstract_records, returned_keys)\n\n print(\"回退搜索:将匹配结果发送给 LLM 进行判断和总结。\")\n\n abstract_summary_prompt = f\"\"\"\n 根据以下提供的文献信息(标题、摘要等)中与用户问题相关的内容,回答用户的原始问题。\n 只使用提供的信息进行回答。如果提供的信息都与问题不相关,请直接返回”查询没有返回任何结果“。\n 用自然语言回答,简洁明了。\n\n 用户原始问题: {user_question}\n\n 提供的文献信息:\n {formatted_abstract_results_text}\n\n 请根据以上信息,回答用户的问题:\n \"\"\"\n\n messages_for_summary = [\n {\"role\": \"system\", \"content\": abstract_summary_prompt},\n {\"role\": \"user\", \"content\": f\"总结与问题 '{user_question}' 相关的文献信息。\"}\n ]\n\n try:\n # 调用大模型 API\n summary_response = llm_client_instance.chat.completions.create(\n model=DEEPSEEK_MODEL, # 使用与生成Cypher相同的模型,或根据需要选择\n messages=messages_for_summary,\n temperature=0.7, # 总结可以稍微灵活\n max_tokens=1000 # 总结长度,根据结果数量调整\n )\n summary_answer = summary_response.choices[0].message.content.strip()\n print(f\"回退搜索:LLM 总结的结果:\\n{summary_answer}\")\n\n # 简单的判断 LLM 是否认为结果相关(可能需要更健壮的判断逻辑)\n if \"查询没有返回任何结果\" in summary_answer:\n return None # LLM 认为结果不相关或无法总结\n\n return summary_answer # 返回 LLM 总结的内容\n\n except Exception as e:\n print(f\"回退搜索:LLM 总结失败: {e}\")\n return None # LLM 调用失败\n\n\n\ndef init():\n # 初始化 Neo4j Driver\n neo4j_driver = get_neo4j_driver()\n if neo4j_driver is None:\n exit(1) # 如果驱动初始化失败则退出\n\n # 导入大模型客户端 (需要安装 openai 库并配置 API Key)\n try:\n from openai import OpenAI\n\n # 请确保在这里设置你的 DeepSeek 或其他 API Key\n # 建议使用环境变量或配置文件\n DEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\")\n if not DEEPSEEK_API_KEY:\n print(\"错误:请设置 DEEPSEEK_API_KEY 环境变量或在代码中提供。\")\n exit(1)\n\n global llm_client_instance\n llm_client_instance = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n\n # 可以再次验证 LLM API 连接\n # llm_client_instance.models.list()\n print(\"大模型客户端初始化成功。\")\n\n except ImportError:\n print(\"错误:请安装 openai 库 (`pip install openai`) 以使用大模型API。\")\n exit(1)\n except Exception as e:\n print(f\"大模型客户端初始化失败: {e}\")\n exit(1)\n\n\n\ndef shutdown_resources():\n \"\"\"关闭资源\"\"\"\n close_neo4j_driver()\n\n\n# --- 主函数示例 (需要实际运行环境和 API Key) ---\nif __name__ == \"__main__\":\n llm_client_instance = init()\n\n # --- 示例问答流程 ---\n print(\"\\n--- 开始示例问答 ---\")\n\n # 确保你的数据库中有能匹配的节点和关系,以及对应的摘要等属性\n # 这里的示例问题需要LLM能够理解并转换为Cypher\n example_question = \"'生成式人工智能技术对教育领域的影响——关于ChatGPT的专访'这一篇文献的主要内容是什么?\"\n\n final_answer = query_knowledge_graph_with_llm(example_question)\n\n print(f\"\\n问答系统最终回答:\\n{final_answer}\")\n\n print(\"\\n--- 示例问答结束 ---\")\n\n # 关闭 Neo4j Driver\n close_neo4j_driver()\n"}, "bd35aa339a1fb06abea51f54f0427801d6b5991f": {"t": 1785753227.0197458, "n": 20140, "h": "9765315a619fcdc3", "c": "import json\nimport os\nimport re\nfrom typing import List, Tuple, Optional, Any, Dict\n\nfrom neo4j.graph import Node, Relationship, Path\nfrom neo4j import GraphDatabase, Record\n\n# --- Neo4j 连接信息 (需要根据你的实际情况修改) ---\n# 建议从配置文件或环境变量读取\nNEO4J_URI = \"bolt://10.5.132.251:7687\"\nNEO4J_USER = \"neo4j\"\nNEO4J_PASSWORD = \"123456788\"\n\n# 全局 Driver 实例\nneo4j_driver: Optional[GraphDatabase.driver] = None\n\n\ndef get_neo4j_driver():\n \"\"\"获取或初始化 Neo4j Driver 实例\"\"\"\n global neo4j_driver\n if neo4j_driver is None:\n try:\n # 可以根据需要调整连接池大小等参数\n neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n # 验证连接\n neo4j_driver.verify_connectivity()\n print(\"Neo4j Driver 初始化成功。\")\n except Exception as e:\n print(f\"Neo4j Driver 初始化或连接失败: {e}\")\n neo4j_driver = None\n return neo4j_driver\n\n\ndef close_neo4j_driver():\n \"\"\"关闭 Neo4j Driver 连接\"\"\"\n global neo4j_driver\n if neo4j_driver:\n neo4j_driver.close()\n neo4j_driver = None\n print(\"Neo4j Driver 已关闭。\")\n\n\n# --- 结果格式化函数 ---\ndef _format_neo4j_value_for_llm(value: Any) -> str:\n \"\"\"\n 将 Neo4j 返回的不同类型的值格式化为适合大模型阅读的字符串表示。\n 尝试识别常见的节点ID属性(如 name, title, paper_id)。\n \"\"\"\n if isinstance(value, Node):\n labels = \":\".join(list(value.labels))\n properties_str_parts = []\n # 尝试获取主要的标识属性\n if 'name' in value:\n properties_str_parts.append(f\"name: '{value['name']}'\")\n if 'title' in value:\n properties_str_parts.append(f\"title: '{value['title']}'\")\n if 'paper_id' in value: # 假设你的 paper_id 属性\n properties_str_parts.append(f\"paper_id: '{value['paper_id']}'\")\n # 可以添加更多常见的标识属性\n\n # 如果没有找到标识属性,或者想包含更多属性,可以遍历所有属性\n for prop_key, prop_value in value.items():\n if prop_key not in ['name', 'title', 'paper_id']: # 避免重复添加\n # 递归格式化属性值,注意避免无限递归复杂对象\n properties_str_parts.append(f\"{prop_key}: '{prop_value}'\") # 简化处理,直接转字符串可能不够\n\n properties_str = \", \".join(properties_str_parts)\n return f\"({labels} {{{properties_str}}})\" if properties_str else f\"({labels})\"\n\n elif isinstance(value, Relationship):\n # 只显示关系类型\n return f\"-[:{value.type}]-\"\n # 如果需要包含关系属性,会更复杂\n # return f\"-[:{value.type} {{...}}]-\"\n\n elif isinstance(value, Path):\n path_parts = []\n for i, item in enumerate(value):\n if i % 2 == 0: # 节点\n path_parts.append(_format_neo4j_value_for_llm(item))\n else: # 关系\n # 路径中的关系是带有方向的,但这里的格式化简化为无方向\n path_parts.append(_format_neo4j_value_for_llm(item))\n return \"->\".join(path_parts) # 简化表示,不严格区分关系方向\n\n elif isinstance(value, list):\n # 格式化列表中的每个元素\n formatted_items = [_format_neo4j_value_for_llm(item) for item in value]\n return \"[\" + \", \".join(formatted_items) + \"]\"\n\n elif isinstance(value, dict):\n # 格式化字典中的键值对\n formatted_items = [f\"{k}: {_format_neo4j_value_for_llm(v)}\" for k, v in value.items()]\n return \"{\" + \", \".join(formatted_items) + \"}\"\n\n else:\n # 格式化基本数据类型\n return str(value)\n\n\ndef format_neo4j_results_for_llm(result_records: List[Record], returned_keys: List[str]) -> str:\n \"\"\"\n 将 Neo4j 查询返回的 Record 列表格式化为适合大模型阅读的字符串。\n \"\"\"\n if not result_records:\n return \"查询没有返回任何结果。\"\n\n formatted_output_lines = [\"查询结果如下:\"]\n\n for i, record in enumerate(result_records):\n formatted_output_lines.append(f\"--- 记录 {i + 1} ---\")\n record_str_parts = []\n # 遍历查询返回的每一列\n for key in returned_keys:\n value = record[key]\n formatted_value = _format_neo4j_value_for_llm(value)\n record_str_parts.append(f\"{key}: {formatted_value}\")\n formatted_output_lines.append(\", \".join(record_str_parts))\n\n return \"\\n\".join(formatted_output_lines)\n\n\n# --- Cypher 查询执行函数 ---\ndef execute_cypher_query(query_string: str) -> Tuple[Optional[List[Record]], Optional[str]]:\n \"\"\"\n 执行给定的 Cypher 查询字符串,返回查询结果(Record 列表)或错误信息。\n\n 参数:\n query_string: 要执行的 Cypher 查询字符串。\n\n 返回:\n 一个元组 (results, error_message)。\n 如果执行成功,results 是 Record 对象的列表,error_message 为 None。\n 如果执行失败,results 为 None,error_message 为错误字符串。\n \"\"\"\n driver = get_neo4j_driver()\n if driver is None:\n return None, \"Neo4j Driver 未成功初始化,无法连接数据库。\"\n\n try:\n with driver.session() as session:\n # 执行查询\n result = session.run(query_string, suppress=[\"Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning\"])\n # 获取所有返回的记录\n records = list(result)\n # 返回 Record 对象列表和 None 作为错误信息\n return records, None\n\n except Exception as e:\n # 捕获查询执行过程中的任何异常\n error_message = str(e)\n print(f\"Cypher 查询执行失败: {error_message}\\n查询: {query_string}\")\n return None, error_message\n\n\ndef extract_search_terms_from_cypher(cypher_string: str) -> List[str]:\n \"\"\"\n 尝试从 Cypher 查询字符串中提取用于匹配 Keyword, Topic 或 Title 属性的值。\n 这个函数使用简化的正则表达式,可能无法处理所有复杂的Cypher语法。\n\n 参数:\n cypher_string: LLM 生成的 Cypher 查询字符串。\n\n 返回:\n 提取到的潜在搜索词列表。\n \"\"\"\n # 这个正则表达式尝试匹配以下模式中的单引号或双引号括起来的字符串:\n # name: \"...\" 或 name: '...' (用于 Keyword, Topic, Author, Org 等)\n # title = \"...\" 或 title = '...'\n # title CONTAINS \"...\" 或 title CONTAINS '...'\n # title =~ \"...\" 或 title =~ '...' (以及 abstract =~)\n # abstract CONTAINS \"...\" 或 abstract CONTAINS '...'\n # abstract =~ \"...\" 或 abstract =~ '...'\n # ([^\"']+) 捕获引号内的内容,但不处理转义引号(简化的体现)\n # 使用非贪婪匹配 .*? 以防止跨越多个匹配项\n pattern = r\"(?:name|title)\\s*[:=~]?=?(?:\\s*CONTAINS)?\\s*[\\\"'](.*?)[\\\"']\"\n\n search_terms = []\n # 查找所有匹配项\n matches = re.findall(pattern, cypher_string, re.IGNORECASE | re.DOTALL) # 忽略大小写,让 '.' 匹配换行\n\n # 过滤掉可能是属性名本身或其他非搜索词的情况(简单过滤短词)\n for term in matches:\n term = term.strip()\n if term and len(term) > 1: # 过滤掉空字符串和过短的词\n search_terms.append(term)\n\n # 移除重复项\n search_terms = list(dict.fromkeys(search_terms))\n\n print(f\"从 Cypher 中提取的潜在搜索词: {search_terms}\")\n return search_terms\n\n\n# --- 与大模型交互的集成示例 (概念性代码) ---\n\n# 假设你已经有了大模型的客户端初始化代码\nfrom openai import OpenAI\nDEEPSEEK_API_KEY = \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\" # 你的 API Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-chat\" # 或者 \"deepseek-chat\" 如果你觉得它更适合处理结构化文本\nllm_client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n\n# 假设你的图谱 Schema 可以用文本描述(需要根据你的实际 Schema 编写)\n# 可以是节点标签、属性、关系类型及其方向的列表或结构化描述\nGRAPH_SCHEMA_DESCRIPTION = \"\"\"\n你的知识图谱包含以下节点和关系:\n节点标签:\n- Journal_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"journal\", \"notes\", \"pages\", \"reference_type\", \"url\"\n \"doi\", \"volume\", \"year\", \"issue\"}\n- Conference_Proceedings {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"pages\", \"reference_type\", \"url\", \"year\", \"secondary_title\", \n \"subsidiary_author\", \"tertiary_title\"} \n- Patent {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"subsidiary_author\", \"date\", \"srcdatabase\", \"subject\"}\n- Newspaper_Article {\"title\", \":ID\", \":LABEL\", \"database_provider\", \"doi\", \"notes\", \"pages\", \"reference_type\", \"url\", \"secondary_title\", \"date\"}\n- Thesis {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"reference_type\", \"url\", \"year\", \"type_of_work\"}\n- Other_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"date\"}\n- Book {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"pages\", \"reference_type\", \"date\", \"edition\"}\n- Author {\"name\", \":ID\", \":LABEL\"}\n- Keyword {\"name\", \":ID\", \":LABEL\"}\n- Organization {\"name\", \":ID\", \":LABEL\"}\n- Author_Address {\"name\", \":ID\", \":LABEL\"}\n- Topic {\"name\",, \":ID\", \":LABEL\"} \n\n关系类型:\n- (Author)-[:AUTHORED]->(Paper/Patent)\n- (Author)-[:TERTIARY_AUTHORED]->(Paper/Patent)\n- (Paper/Patent)-[:HAS_KEYWORD]->(Keyword)\n- (Paper/Patent)-[:PUBLISHED_BY]->(Organization)\n- (Paper/Patent)-[:HAS_AUTHOR_ADDRESS]->(AuthorAddress)\n- (Keyword)-[:ALIAS_OF]->(Keyword)\n- (Organization)-[:ALIAS_OF]->(Organization)\n- (AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)\n- (Paper/Patent)-[:HAS_TOPIC]->(Topic)\n\n查询时请使用节点属性进行匹配,例如 `title`, `name`, `paper_id`\n\"\"\"\n\n\ndef query_knowledge_graph_with_llm(user_question: str, llm_client: Any) -> str:\n \"\"\"\n 使用大模型回答关于知识图谱的问题。\n 这是一个简化的两阶段过程:用户问题 -> LLM生成Cypher -> 执行Cypher -> LLM根据结果生成答案。\n 实际问答系统会更复杂,包含多轮交互、意图识别、实体链接、回退机制等。\n \"\"\"\n # 阶段 1: LLM 将用户问题转换为 Cypher 查询\n print(f\"用户问题: {user_question}\")\n print(\"阶段 1: 请求 LLM 生成 Cypher 查询...\")\n\n cypher_generation_prompt = f\"\"\"\n 你的知识图谱Schema如下:\n {GRAPH_SCHEMA_DESCRIPTION}\n \n 重要说明:\n - **别名关系**: `(Keyword)-[:ALIAS_OF]->(Keyword)`, `(Organization)-[:ALIAS_OF]->(Organization)`, `(AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)`. 并非所有 Keyword/Organization/AuthorAddress 节点都有别名关系。\n - **部分节点属性可能缺失**: 例如,某些文献可能没有 year, abstract, url, doi 等属性。\n \n 生成Cypher查询的规则:\n 1. **处理可选关系 (如别名关系):** 当你需要查询一个节点及其别名或相关联的可选节点时,请使用 `OPTIONAL MATCH`。例如,要查找一个Keyword及其别名,使用 `MATCH (k:Keyword {{name: \"某个词\"}}) OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias)`。之后在 WHERE 子句中可以使用 `WHERE relatedNode = k OR relatedNode = alias` 来匹配原始节点或其别名。\n 2. **处理可选属性:** 在 WHERE 子句中直接比较属性值 (例如 `p.year = 2022`) 会自动排除缺失该属性的节点。如果你只想查找**拥有**某个特定属性的节点,可以使用 `WHERE node.property IS NOT NULL`。\n 3. **确保数据类型匹配:** 在比较属性值时,请根据Schema使用正确的数据类型,例如年份是整数,使用 `p.year = 2022` 而不是 `\"2022\"`。\n 4. **使用节点属性匹配:** 在 MATCH 或 MERGE 时,使用 `name`, `title`, `paper_id` 等属性来精确匹配节点。\n 5. **限制结果数量:** 通常在 RETURN 语句后加上 `LIMIT 10`,除非用户明确要求返回所有结果。\n \n 请严格按照上述Schema和规则生成Cypher查询语句,只输出查询本身,不要包含任何额外文字或解释。\n 用户的输入可能会带有错别字,请谨慎的判别是否有错别字并替换为正确的短语,若用户要查询的内容中带有其他语言,请翻译成中文查询。\n \n 示例1 (处理关键词别名和年份过滤):\n 用户问题: 2022年关于肺癌的论文有哪些?\n Cypher 查询:\n MATCH (k:Keyword {{name: \"肺癌\"}})\n OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias) // 使用 OPTIONAL MATCH 处理可选的别名关系\n MATCH (p)-[:HAS_KEYWORD]->(relatedKeyword) // 匹配与关键词相关的论文\n WHERE (relatedKeyword = k OR relatedKeyword = alias) // 相关的关键词是原始关键词或其别名\n AND (p:Paper OR p:Patent) // 确保是文献节点 (根据你的实际主要文献标签调整)\n AND p.year = 2022 // 过滤年份,直接比较数值类型\n RETURN p.title, p.year LIMIT 10\n \n 示例2 (查找所有有摘要的专利):\n 用户问题: 查找所有有摘要的专利。\n Cypher 查询:\n MATCH (p:Patent)\n WHERE p.abstract IS NOT NULL // 只匹配有摘要属性的专利\n RETURN p.title, p.abstract LIMIT 10\n \n 示例3 (查找某个作者在特定年份发表的论文):\n 用户问题: 张三在2023年发表了哪些论文?\n Cypher 查询:\n MATCH (a:Author {{name: \"张三\"}})-[:AUTHORED]->(p:Paper) // 假设AUTHORED关系只连接Paper\n WHERE p.year = 2023\n RETURN p.title, p.year LIMIT 10\n \n \n 用户问题: {user_question}\n Cypher 查询:\n \"\"\"\n\n messages_for_cypher = [\n {\"role\": \"system\", \"content\": cypher_generation_prompt},\n {\"role\": \"user\", \"content\": user_question} # 也可以把问题放在用户消息里,\n\n ]\n\n try:\n # 调用大模型 API\n response_cypher = llm_client.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_cypher,\n temperature=0, # 生成 Cypher 需要确定性\n max_tokens=500 # 根据预期 Cypher 长度调整\n )\n\n generated_cypher = response_cypher.choices[0].message.content.strip()\n # 从生成的 Cypher 中提取搜索词\n potential_search_terms = extract_search_terms_from_cypher(generated_cypher)\n\n print(f\"LLM 生成的 Cypher 查询:\\n{generated_cypher}\")\n\n except Exception as e:\n print(f\"LLM 生成 Cypher 查询失败: {e}\")\n return f\"抱歉,无法将您的问题转换为图谱查询 ({e})\"\n\n # 阶段 2: 执行 Cypher 查询\n print(\"阶段 2: 执行 Cypher 查询...\")\n query_results, execution_error = execute_cypher_query(generated_cypher)\n\n # 阶段 3: LLM 根据查询结果生成最终答案\n if execution_error:\n # 如果执行失败,将错误信息反馈给用户(或者尝试让LLM修正查询)\n print(f\"查询执行错误: {execution_error}\")\n return f\"抱歉,执行图谱查询时出错: {execution_error}\"\n else:\n print(f\"查询执行成功,返回 {len(query_results)} 条记录。\")\n # 将查询结果格式化为文本\n formatted_results_text = format_neo4j_results_for_llm(query_results,\n query_results[0].keys() if query_results else [])\n\n # 如果结果为空,直接返回提示\n if \"查询没有返回任何结果\" in formatted_results_text:\n # 调用回退搜索函数\n fallback_answer = fallback_abstract_search(user_question, llm_client, potential_search_terms)\n # 如果回退搜索找到了结果,返回回退结果;否则返回“未能找到信息”的提示\n return fallback_answer if fallback_answer is not None else \"抱歉,未能找到相关信息。\"\n\n print(\"阶段 3: 请求 LLM 根据结果生成自然语言答案...\")\n answer_generation_prompt = f\"\"\"\n 你是一个知识图谱问答助手。根据提供的查询结果,用自然语言回答用户的问题,回答要尽可能专业、逻辑清晰。\n 只使用提供的查询结果中的信息。如果结果无法回答问题,请说明。\n\n **特别注意:如果查询结果中包含了文献的 'abstract' (摘要) 属性,并且用户的问题与文献内容相关,请在回答时充分利用这些摘要信息。具体处理方法可以参考下面的内容。**\n - 如果问题是直接索取摘要(例如:“请给我XX论文的摘要”),你的回答应该清晰地呈现该摘要。\n - 如果问题是关于文献内容、主要观点、科研方法、主要贡献等,请基于摘要进行总结和回答。\n - 如果问题非常具体,请根据问题进行回答,问题可能是:研究问题、方法论、主要发现、创新点、局限性、结论等。\n - 如果摘要内容较长,可以进行适当的概括,但要忠于原文。\n\n 用户问题: {user_question}\n\n 查询结果:\n {formatted_results_text}\n\n 根据以上信息,用自然语言回答用户的问题:\n \"\"\"\n\n messages_for_answer = [\n {\"role\": \"system\", \"content\": answer_generation_prompt},\n {\"role\": \"user\", \"content\": f\"回答用户问题:'{user_question}'\"} # 再次强调问题\n ]\n\n try:\n # 调用大模型 API 生成答案\n response_answer = llm_client.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_answer,\n temperature=0.5, # 生成答案可以稍微灵活\n max_tokens=500 # 根据预期答案长度调整\n )\n final_answer = response_answer.choices[0].message.content.strip()\n print(f\"LLM 生成的最终答案:\\n{final_answer}\")\n return final_answer\n\n except Exception as e:\n print(f\"LLM 生成最终答案失败: {e}\")\n return f\"抱歉,处理查询结果时遇到问题 ({e})\"\n\n\n# --- 回退机制:摘要文本搜索 ---\n\ndef fallback_abstract_search(user_question: str, llm_client: Any, search_terms: List[str], max_results: int = 100) -> Optional[str]:\n \"\"\"\n 当图谱查询无结果时,作为回退机制在文献摘要中进行文本搜索(使用 Cypher 正则),\n 并将匹配到的摘要交给大模型进行判断和总结。\n\n 参数:\n user_question: 用户的原始问题。\n llm_client: 大模型 API 客户端。\n search_terms: 从用户问题中提取的用于文本搜索的关键词/短语列表。\n max_results: 从数据库获取的最大匹配结果数量。\n\n 返回:\n LLM 基于匹配摘要生成的答案,或 None 表示未能找到相关信息。\n \"\"\"\n if not search_terms:\n print(\"回退搜索:未提取到搜索关键词。\")\n return None\n\n print(f\"回退搜索:图谱查询无结果,尝试在摘要中进行 Cypher 正则文本匹配。搜索词: {search_terms}\")\n\n # 1. 构建 Cypher 查询\n # Condition 1: Abstract matches ALL search terms\n if not search_terms:\n # Cannot match all terms if search_terms is empty, so this condition is false\n abstract_regex_condition = \"false\"\n else:\n # Build regex for matching ALL terms\n escaped_search_terms = [re.escape(term) for term in search_terms]\n # Pattern: (?i)(?=.*term1)(?=.*term2)...*\n abstract_regex_pattern = \"(?i)\" + \"\".join(f\"(?=.*{term})\" for term in escaped_search_terms) + \".*\"\n abstract_regex_condition = f\"p.abstract IS NOT NULL AND p.abstract =~ '{abstract_regex_pattern}'\"\n\n\n # Condition 2: Related Topic matches ANY search term\n if not search_terms:\n # Cannot match any topic if search_terms is empty, so this condition is false\n topic_condition = \"false\"\n else:\n # Build list literal for IN clause\n quoted_terms = [f\"{term}\" for term in search_terms]\n topic_terms_list = \"|\".join(quoted_terms)\n # Use OPTIONAL MATCH and check if topic exists AND its name is in the list\n topic_condition = f\"topic IN topics WHERE topic.name IS NOT NULL AND topic.name =~ '(?i).*({topic_terms_list})'\"\n\n\n # Combine the two conditions with OR\n # We need to structure the query to allow OPTIONAL MATCH for topic while filtering papers\n # Approach: Filter papers by abstract condition first (if applicable), then OPTIONAL MATCH topic, then filter by topic condition\n # Or, apply both conditions in the final WHERE after OPTIONAL MATCH\n # Let's use the latter for simplicity, assuming the abstract IS NOT NULL check is sufficient before regex\n\n fallback_query = f\"\"\"\n MATCH (p)\n WHERE (p:Patent OR p:Journal_Article OR p:Conference_Proceedings OR p:Thesis OR p:Other_Article OR p:Newspaper_Article OR p:Book)\n OPTIONAL MATCH (p)-[:HAS_TOPIC]->(t:Topic) // Optional match for Topic node\n WITH p, collect(t) AS topics\n WHERE ({abstract_regex_condition}) OR any({topic_condition}) // Apply the combined condition\n RETURN p // Return the node itself\n LIMIT {max_results} // Limit the number of results\n \"\"\"\n\n print(f\"回退搜索:执行 Cypher 联合查询:\\n{fallback_query}\")\n\n # 2. 执行 Cypher 联合查询\n abstract_records, error = execute_cypher_query(fallback_query)\n\n if error:\n print(f\"回退搜索:执行 Cypher 联合查询失败: {error}\")\n return None\n\n if not abstract_records:\n print(\"回退搜索:Cypher 联合查询没有匹配到任何结果。\")\n return None\n\n print(f\"回退搜索:Cypher 联合查询匹配到 {len(abstract_records)} 条结果:\")\n print(abstract_records)\n # 3. 将匹配到的结果格式化并发送给 LLM 进行判断和总结\n # 使用 format_neo4j_results_for_llm 格式化结果\n returned_keys = abstract_records[0].keys() if abstract_records else []\n formatted_abstract_results_text = format_neo4j_results_for_llm(abstract_records, returned_keys)\n\n print(\"回退搜索:将匹配结果发送给 LLM 进行判断和总结。\")\n\n abstract_summary_prompt = f\"\"\"\n 根据以下提供的文献信息(标题、摘要等)中与用户问题相关的内容,回答用户的原始问题。\n 只使用提供的信息进行回答。如果提供的信息都与问题不相关,请直接返回”查询没有返回任何结果“。\n 用自然语言回答,简洁明了。\n\n 用户原始问题: {user_question}\n\n 提供的文献信息:\n {formatted_abstract_results_text}\n\n 请根据以上信息,回答用户的问题:\n \"\"\"\n\n messages_for_summary = [\n {\"role\": \"system\", \"content\": abstract_summary_prompt},\n {\"role\": \"user\", \"content\": f\"总结与问题 '{user_question}' 相关的文献信息。\"}\n ]\n\n try:\n # 调用大模型 API\n summary_response = llm_client.chat.completions.create(\n model=DEEPSEEK_MODEL, # 使用与生成Cypher相同的模型,或根据需要选择\n messages=messages_for_summary,\n temperature=0.7, # 总结可以稍微灵活\n max_tokens=1000 # 总结长度,根据结果数量调整\n )\n summary_answer = summary_response.choices[0].message.content.strip()\n print(f\"回退搜索:LLM 总结的结果:\\n{summary_answer}\")\n\n # 简单的判断 LLM 是否认为结果相关(可能需要更健壮的判断逻辑)\n if \"查询没有返回任何结果\" in summary_answer:\n return None # LLM 认为结果不相关或无法总结\n\n return summary_answer # 返回 LLM 总结的内容\n\n except Exception as e:\n print(f\"回退搜索:LLM 总结失败: {e}\")\n return None # LLM 调用失败\n\n\n# --- 主函数示例 (需要实际运行环境和 API Key) ---\nif __name__ == \"__main__\":\n # 初始化 Neo4j Driver\n neo4j_driver = get_neo4j_driver()\n if neo4j_driver is None:\n exit(1) # 如果驱动初始化失败则退出\n\n # 导入大模型客户端 (需要安装 openai 库并配置 API Key)\n try:\n from openai import OpenAI\n\n # 请确保在这里设置你的 DeepSeek 或其他 API Key\n # 建议使用环境变量或配置文件\n DEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\")\n if not DEEPSEEK_API_KEY:\n print(\"错误:请设置 DEEPSEEK_API_KEY 环境变量或在代码中提供。\")\n exit(1)\n\n llm_client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n # 可以再次验证 LLM API 连接\n # llm_client.models.list()\n print(\"大模型客户端初始化成功。\")\n\n except ImportError:\n print(\"错误:请安装 openai 库 (`pip install openai`) 以使用大模型API。\")\n exit(1)\n except Exception as e:\n print(f\"大模型客户端初始化失败: {e}\")\n exit(1)\n\n # --- 示例问答流程 ---\n print(\"\\n--- 开始示例问答 ---\")\n\n # 确保你的数据库中有能匹配的节点和关系,以及对应的摘要等属性\n # 这里的示例问题需要LLM能够理解并转换为Cypher\n example_question = \"'生成式人工智能技术对教育领域的影响——关于ChatGPT的专访'这一篇文献的主要内容是什么?\"\n # 假设 LLM 可能会生成类似这样的 Cypher:\n # MATCH (p:Paper)-[:HAS_KEYWORD]->(k:Keyword) WHERE k.name = '人工智能' RETURN p.title LIMIT 10\n\n # 如果你有 Topic 节点\n # example_question = \"关于量子计算的主题有哪些论文?\"\n # 假设 LLM 可能会生成类似这样的 Cypher:\n # MATCH (p:Paper)-[:HAS_TOPIC]->(t:Topic {name: '量子计算'}) RETURN p.title LIMIT 10\n\n # 询问特定文献内容\n # example_question = \"介绍一下标题为'一种新的量子密钥分发协议'的论文内容。\"\n # 假设 LLM 可能生成 Cypher 来获取摘要:\n # MATCH (p:Paper {title: '一种新的量子密钥分发协议'}) RETURN p.abstract LIMIT 1\n\n final_answer = query_knowledge_graph_with_llm(example_question, llm_client)\n print(f\"\\n问答系统最终回答:\\n{final_answer}\")\n\n print(\"\\n--- 示例问答结束 ---\")\n\n # 关闭 Neo4j Driver\n close_neo4j_driver()\n"}, "17fa31ed4507c0a77f6bbd4bfd5f93bdcb85e44d": {"t": 1785753228.4421566, "n": 13633, "h": "ef7a8b8c6db003bc", "c": "import json\nimport os\nfrom typing import List, Tuple, Optional, Any, Dict\n\nfrom neo4j.graph import Node, Relationship, Path\nfrom neo4j import GraphDatabase, Record\n\n# --- Neo4j 连接信息 (需要根据你的实际情况修改) ---\n# 建议从配置文件或环境变量读取\nNEO4J_URI = \"bolt://10.9.116.110:7687\"\nNEO4J_USER = \"neo4j\"\nNEO4J_PASSWORD = \"123456788\"\n\n# 全局 Driver 实例\nneo4j_driver: Optional[GraphDatabase.driver] = None\n\n\ndef get_neo4j_driver():\n \"\"\"获取或初始化 Neo4j Driver 实例\"\"\"\n global neo4j_driver\n if neo4j_driver is None:\n try:\n # 可以根据需要调整连接池大小等参数\n neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))\n # 验证连接\n neo4j_driver.verify_connectivity()\n print(\"Neo4j Driver 初始化成功。\")\n except Exception as e:\n print(f\"Neo4j Driver 初始化或连接失败: {e}\")\n neo4j_driver = None\n return neo4j_driver\n\n\ndef close_neo4j_driver():\n \"\"\"关闭 Neo4j Driver 连接\"\"\"\n global neo4j_driver\n if neo4j_driver:\n neo4j_driver.close()\n neo4j_driver = None\n print(\"Neo4j Driver 已关闭。\")\n\n\n# --- 结果格式化函数 ---\ndef _format_neo4j_value_for_llm(value: Any) -> str:\n \"\"\"\n 将 Neo4j 返回的不同类型的值格式化为适合大模型阅读的字符串表示。\n 尝试识别常见的节点ID属性(如 name, title, paper_id)。\n \"\"\"\n if isinstance(value, Node):\n labels = \":\".join(list(value.labels))\n properties_str_parts = []\n # 尝试获取主要的标识属性\n if 'name' in value:\n properties_str_parts.append(f\"name: '{value['name']}'\")\n if 'title' in value:\n properties_str_parts.append(f\"title: '{value['title']}'\")\n if 'paper_id' in value: # 假设你的 paper_id 属性\n properties_str_parts.append(f\"paper_id: '{value['paper_id']}'\")\n # 可以添加更多常见的标识属性\n\n # 如果没有找到标识属性,或者想包含更多属性,可以遍历所有属性\n for prop_key, prop_value in value.items():\n if prop_key not in ['name', 'title', 'paper_id']: # 避免重复添加\n # 递归格式化属性值,注意避免无限递归复杂对象\n properties_str_parts.append(f\"{prop_key}: '{prop_value}'\") # 简化处理,直接转字符串可能不够\n\n properties_str = \", \".join(properties_str_parts)\n return f\"({labels} {{{properties_str}}})\" if properties_str else f\"({labels})\"\n\n elif isinstance(value, Relationship):\n # 只显示关系类型\n return f\"-[:{value.type}]-\"\n # 如果需要包含关系属性,会更复杂\n # return f\"-[:{value.type} {{...}}]-\"\n\n elif isinstance(value, Path):\n path_parts = []\n for i, item in enumerate(value):\n if i % 2 == 0: # 节点\n path_parts.append(_format_neo4j_value_for_llm(item))\n else: # 关系\n # 路径中的关系是带有方向的,但这里的格式化简化为无方向\n path_parts.append(_format_neo4j_value_for_llm(item))\n return \"->\".join(path_parts) # 简化表示,不严格区分关系方向\n\n elif isinstance(value, list):\n # 格式化列表中的每个元素\n formatted_items = [_format_neo4j_value_for_llm(item) for item in value]\n return \"[\" + \", \".join(formatted_items) + \"]\"\n\n elif isinstance(value, dict):\n # 格式化字典中的键值对\n formatted_items = [f\"{k}: {_format_neo4j_value_for_llm(v)}\" for k, v in value.items()]\n return \"{\" + \", \".join(formatted_items) + \"}\"\n\n else:\n # 格式化基本数据类型\n return str(value)\n\n\ndef format_neo4j_results_for_llm(result_records: List[Record], returned_keys: List[str]) -> str:\n \"\"\"\n 将 Neo4j 查询返回的 Record 列表格式化为适合大模型阅读的字符串。\n \"\"\"\n if not result_records:\n return \"查询没有返回任何结果。\"\n\n formatted_output_lines = [\"查询结果如下:\"]\n\n for i, record in enumerate(result_records):\n formatted_output_lines.append(f\"--- 记录 {i + 1} ---\")\n record_str_parts = []\n # 遍历查询返回的每一列\n for key in returned_keys:\n value = record[key]\n formatted_value = _format_neo4j_value_for_llm(value)\n record_str_parts.append(f\"{key}: {formatted_value}\")\n formatted_output_lines.append(\", \".join(record_str_parts))\n\n return \"\\n\".join(formatted_output_lines)\n\n\n# --- Cypher 查询执行函数 ---\ndef execute_cypher_query(query_string: str) -> Tuple[Optional[List[Record]], Optional[str]]:\n \"\"\"\n 执行给定的 Cypher 查询字符串,返回查询结果(Record 列表)或错误信息。\n\n 参数:\n query_string: 要执行的 Cypher 查询字符串。\n\n 返回:\n 一个元组 (results, error_message)。\n 如果执行成功,results 是 Record 对象的列表,error_message 为 None。\n 如果执行失败,results 为 None,error_message 为错误字符串。\n \"\"\"\n driver = get_neo4j_driver()\n if driver is None:\n return None, \"Neo4j Driver 未成功初始化,无法连接数据库。\"\n\n try:\n with driver.session() as session:\n # 执行查询\n result = session.run(query_string, suppress=[\"Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning\"])\n # 获取所有返回的记录\n records = list(result)\n # 返回 Record 对象列表和 None 作为错误信息\n return records, None\n\n except Exception as e:\n # 捕获查询执行过程中的任何异常\n error_message = str(e)\n print(f\"Cypher 查询执行失败: {error_message}\\n查询: {query_string}\")\n return None, error_message\n\n\n# --- 与大模型交互的集成示例 (概念性代码) ---\n\n# 假设你已经有了大模型的客户端初始化代码\nfrom openai import OpenAI\nDEEPSEEK_API_KEY = \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\" # 你的 API Key\nDEEPSEEK_BASE_URL = \"https://api.deepseek.com\"\nDEEPSEEK_MODEL = \"deepseek-chat\" # 或者 \"deepseek-chat\" 如果你觉得它更适合处理结构化文本\nllm_client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n\n# 假设你的图谱 Schema 可以用文本描述(需要根据你的实际 Schema 编写)\n# 可以是节点标签、属性、关系类型及其方向的列表或结构化描述\nGRAPH_SCHEMA_DESCRIPTION = \"\"\"\n你的知识图谱包含以下节点和关系:\n节点标签:\n- Journal_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"journal\", \"notes\", \"pages\", \"reference_type\", \"url\"\n \"doi\", \"volume\", \"year\", \"issue\"}\n- Conference_Proceedings {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"pages\", \"reference_type\", \"url\", \"year\", \"secondary_title\", \n \"subsidiary_author\", \"tertiary_title\"} \n- Patent {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"subsidiary_author\", \"date\", \"srcdatabase\", \"subject\"}\n- Newspaper_Article {\"title\", \":ID\", \":LABEL\", \"database_provider\", \"doi\", \"notes\", \"pages\", \"reference_type\", \"url\", \"secondary_title\", \"date\"}\n- Thesis {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"doi\", \"reference_type\", \"url\", \"year\", \"type_of_work\"}\n- Other_Article {\"title\", \":ID\", \":LABEL\", \"abstract\", \"notes\", \"reference_type\", \"date\"}\n- Book {\"title\", \":ID\", \":LABEL\", \"abstract\", \"database_provider\", \"isbn/issn\", \"pages\", \"reference_type\", \"date\", \"edition\"}\n- Author {\"name\", \":ID\", \":LABEL\"}\n- Keyword {\"name\", \":ID\", \":LABEL\"}\n- Organization {\"name\", \":ID\", \":LABEL\"}\n- Author_Address {\"name\", \":ID\", \":LABEL\"}\n- Topic {\"name\",, \":ID\", \":LABEL\"} \n\n关系类型:\n- (Author)-[:AUTHORED]->(Paper/Patent)\n- (Author)-[:TERTIARY_AUTHORED]->(Paper/Patent)\n- (Paper/Patent)-[:HAS_KEYWORD]->(Keyword)\n- (Paper/Patent)-[:PUBLISHED_BY]->(Organization)\n- (Paper/Patent)-[:HAS_AUTHOR_ADDRESS]->(AuthorAddress)\n- (Keyword)-[:ALIAS_OF]->(Keyword)\n- (Organization)-[:ALIAS_OF]->(Organization)\n- (AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)\n- (Paper/Patent)-[:HAS_TOPIC]->(Topic)\n\n查询时请使用节点属性进行匹配,例如 `title`, `name`, `paper_id`\n\"\"\"\n\n\ndef query_knowledge_graph_with_llm(user_question: str, llm_client: Any) -> str:\n \"\"\"\n 使用大模型回答关于知识图谱的问题。\n 这是一个简化的两阶段过程:用户问题 -> LLM生成Cypher -> 执行Cypher -> LLM根据结果生成答案。\n 实际问答系统会更复杂,包含多轮交互、意图识别、实体链接、回退机制等。\n \"\"\"\n # 阶段 1: LLM 将用户问题转换为 Cypher 查询\n print(f\"用户问题: {user_question}\")\n print(\"阶段 1: 请求 LLM 生成 Cypher 查询...\")\n\n cypher_generation_prompt = f\"\"\"\n 你的知识图谱Schema如下:\n {GRAPH_SCHEMA_DESCRIPTION}\n \n 重要说明:\n - **别名关系**: `(Keyword)-[:ALIAS_OF]->(Keyword)`, `(Organization)-[:ALIAS_OF]->(Organization)`, `(AuthorAddress)-[:ALIAS_OF]->(AuthorAddress)`. 并非所有 Keyword/Organization/AuthorAddress 节点都有别名关系。\n - **部分节点属性可能缺失**: 例如,某些文献可能没有 year, abstract, url, doi 等属性。\n \n 生成Cypher查询的规则:\n 1. **处理可选关系 (如别名关系):** 当你需要查询一个节点及其别名或相关联的可选节点时,请使用 `OPTIONAL MATCH`。例如,要查找一个Keyword及其别名,使用 `MATCH (k:Keyword {{name: \"某个词\"}}) OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias)`。之后在 WHERE 子句中可以使用 `WHERE relatedNode = k OR relatedNode = alias` 来匹配原始节点或其别名。\n 2. **处理可选属性:** 在 WHERE 子句中直接比较属性值 (例如 `p.year = 2022`) 会自动排除缺失该属性的节点。如果你只想查找**拥有**某个特定属性的节点,可以使用 `WHERE node.property IS NOT NULL`。\n 3. **确保数据类型匹配:** 在比较属性值时,请根据Schema使用正确的数据类型,例如年份是整数,使用 `p.year = 2022` 而不是 `\"2022\"`。\n 4. **使用节点属性匹配:** 在 MATCH 或 MERGE 时,使用 `name`, `title`, `paper_id` 等属性来精确匹配节点。\n 5. **限制结果数量:** 通常在 RETURN 语句后加上 `LIMIT 10`,除非用户明确要求返回所有结果。\n \n 请严格按照上述Schema和规则生成Cypher查询语句,只输出查询本身,不要包含任何额外文字或解释。\n 用户的输入可能会带有错别字,请谨慎的判别是否有错别字并替换为正确的短语。\n \n 示例1 (处理关键词别名和年份过滤):\n 用户问题: 2022年关于肺癌的论文有哪些?\n Cypher 查询:\n MATCH (k:Keyword {{name: \"肺癌\"}})\n OPTIONAL MATCH (k)-[:ALIAS_OF]->(alias) // 使用 OPTIONAL MATCH 处理可选的别名关系\n MATCH (p)-[:HAS_KEYWORD]->(relatedKeyword) // 匹配与关键词相关的论文\n WHERE (relatedKeyword = k OR relatedKeyword = alias) // 相关的关键词是原始关键词或其别名\n AND (p:Paper OR p:Patent) // 确保是文献节点 (根据你的实际主要文献标签调整)\n AND p.year = 2022 // 过滤年份,直接比较数值类型\n RETURN p.title, p.year LIMIT 10\n \n 示例2 (查找所有有摘要的专利):\n 用户问题: 查找所有有摘要的专利。\n Cypher 查询:\n MATCH (p:Patent)\n WHERE p.abstract IS NOT NULL // 只匹配有摘要属性的专利\n RETURN p.title, p.abstract LIMIT 10\n \n 示例3 (查找某个作者在特定年份发表的论文):\n 用户问题: 张三在2023年发表了哪些论文?\n Cypher 查询:\n MATCH (a:Author {{name: \"张三\"}})-[:AUTHORED]->(p:Paper) // 假设AUTHORED关系只连接Paper\n WHERE p.year = 2023\n RETURN p.title, p.year LIMIT 10\n \n \n 用户问题: {user_question}\n Cypher 查询:\n \"\"\"\n\n messages_for_cypher = [\n {\"role\": \"system\", \"content\": cypher_generation_prompt},\n {\"role\": \"user\", \"content\": user_question} # 也可以把问题放在用户消息里\n ]\n\n try:\n # 调用大模型 API\n response_cypher = llm_client.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_cypher,\n temperature=0, # 生成 Cypher 需要确定性\n max_tokens=500 # 根据预期 Cypher 长度调整\n )\n generated_cypher = response_cypher.choices[0].message.content.strip()\n print(f\"LLM 生成的 Cypher 查询:\\n{generated_cypher}\")\n\n except Exception as e:\n print(f\"LLM 生成 Cypher 查询失败: {e}\")\n return f\"抱歉,无法将您的问题转换为图谱查询 ({e})\"\n\n # 阶段 2: 执行 Cypher 查询\n print(\"阶段 2: 执行 Cypher 查询...\")\n query_results, execution_error = execute_cypher_query(generated_cypher)\n\n # 阶段 3: LLM 根据查询结果生成最终答案\n if execution_error:\n # 如果执行失败,将错误信息反馈给用户(或者尝试让LLM修正查询)\n print(f\"查询执行错误: {execution_error}\")\n return f\"抱歉,执行图谱查询时出错: {execution_error}\"\n else:\n print(f\"查询执行成功,返回 {len(query_results)} 条记录。\")\n # 将查询结果格式化为文本\n formatted_results_text = format_neo4j_results_for_llm(query_results,\n query_results[0].keys() if query_results else [])\n\n # 如果结果为空,直接返回提示\n if \"查询没有返回任何结果\" in formatted_results_text:\n return \"抱歉,未能找到相关信息。\"\n\n print(\"阶段 3: 请求 LLM 根据结果生成自然语言答案...\")\n\n answer_generation_prompt = f\"\"\"\n 你是一个知识图谱问答助手。根据提供的查询结果,用自然语言简洁地回答用户的问题。\n 只使用提供的查询结果中的信息。如果结果无法回答问题,请说明。\n\n 用户问题: {user_question}\n\n 查询结果:\n {formatted_results_text}\n\n 根据以上信息,用自然语言回答用户的问题:\n \"\"\"\n\n messages_for_answer = [\n {\"role\": \"system\", \"content\": answer_generation_prompt},\n {\"role\": \"user\", \"content\": f\"回答用户问题:'{user_question}'\"} # 再次强调问题\n ]\n\n try:\n # 调用大模型 API 生成答案\n response_answer = llm_client.chat.completions.create(\n model=DEEPSEEK_MODEL,\n messages=messages_for_answer,\n temperature=0.5, # 生成答案可以稍微灵活\n max_tokens=500 # 根据预期答案长度调整\n )\n final_answer = response_answer.choices[0].message.content.strip()\n print(f\"LLM 生成的最终答案:\\n{final_answer}\")\n return final_answer\n\n except Exception as e:\n print(f\"LLM 生成最终答案失败: {e}\")\n return f\"抱歉,处理查询结果时遇到问题 ({e})\"\n\n\n# --- 主函数示例 (需要实际运行环境和 API Key) ---\nif __name__ == \"__main__\":\n # 初始化 Neo4j Driver\n neo4j_driver = get_neo4j_driver()\n if neo4j_driver is None:\n exit(1) # 如果驱动初始化失败则退出\n\n # 导入大模型客户端 (需要安装 openai 库并配置 API Key)\n try:\n from openai import OpenAI\n\n # 请确保在这里设置你的 DeepSeek 或其他 API Key\n # 建议使用环境变量或配置文件\n DEEPSEEK_API_KEY = os.environ.get(\"DEEPSEEK_API_KEY\", \"sk-5d02bdfb0c9a4c67a4ea6bf27ecb0792\")\n if not DEEPSEEK_API_KEY:\n print(\"错误:请设置 DEEPSEEK_API_KEY 环境变量或在代码中提供。\")\n exit(1)\n\n llm_client = OpenAI(api_key=DEEPSEEK_API_KEY, base_url=DEEPSEEK_BASE_URL)\n # 可以再次验证 LLM API 连接\n # llm_client.models.list()\n print(\"大模型客户端初始化成功。\")\n\n except ImportError:\n print(\"错误:请安装 openai 库 (`pip install openai`) 以使用大模型API。\")\n exit(1)\n except Exception as e:\n print(f\"大模型客户端初始化失败: {e}\")\n exit(1)\n\n # --- 示例问答流程 ---\n print(\"\\n--- 开始示例问答 ---\")\n\n # 确保你的数据库中有能匹配的节点和关系,以及对应的摘要等属性\n # 这里的示例问题需要LLM能够理解并转换为Cypher\n example_question = \"2022年带有唐尿病关键词的论文有哪些?\"\n # 假设 LLM 可能会生成类似这样的 Cypher:\n # MATCH (p:Paper)-[:HAS_KEYWORD]->(k:Keyword) WHERE k.name = '人工智能' RETURN p.title LIMIT 10\n\n # 如果你有 Topic 节点\n # example_question = \"关于量子计算的主题有哪些论文?\"\n # 假设 LLM 可能会生成类似这样的 Cypher:\n # MATCH (p:Paper)-[:HAS_TOPIC]->(t:Topic {name: '量子计算'}) RETURN p.title LIMIT 10\n\n # 询问特定文献内容\n # example_question = \"介绍一下标题为'一种新的量子密钥分发协议'的论文内容。\"\n # 假设 LLM 可能生成 Cypher 来获取摘要:\n # MATCH (p:Paper {title: '一种新的量子密钥分发协议'}) RETURN p.abstract LIMIT 1\n\n final_answer = query_knowledge_graph_with_llm(example_question, llm_client)\n print(f\"\\n问答系统最终回答:\\n{final_answer}\")\n\n print(\"\\n--- 示例问答结束 ---\")\n\n # 关闭 Neo4j Driver\n close_neo4j_driver()\n"}}}} |