这是用ChatGPT创建的
虽然大多数人更关注于从非结构化文本(如公司文件或文档)中进行提取增强生成(RAG),但我对从结构化信息(特别是知识图谱)中提取系统非常看好。特别是微软的实现,关于GraphRAG引起了很大的关注。然而,在他们的实现中,输入数据是以文档形式的非结构化文本,这些文本通过大型语言模型(LLM)转换成知识图谱。
在这篇博客文章中,我们将展示如何在一个包含来自FDA不良事件报告系统(FAERS)中的结构化信息的知识图谱上实施检索器。FAERS提供了关于药物不良事件的信息。如果你曾经接触过知识图谱和检索,你的第一想法可能是使用大型语言模型(LLM)来生成数据库查询以从知识图谱中检索相关信息来回答给定的问题。然而,使用大型语言模型生成数据库查询的方法仍在发展,可能尚未提供最一致或最强大的解决方案。那么,当前有哪些可行的替代方法呢?
在我看来,目前最好的解决方案是动态生成查询。这种方法不是完全依赖大型语言模型生成完整查询,而是通过一个逻辑层,根据预设的输入参数确定性生成数据库查询。这个解决方案可以通过支持函数调用的大型语言模型实现。使用函数调用功能的优势在于,可以定义大型语言模型如何为函数准备结构化输入。这种方法不仅确保了查询生成过程的可控性和一致性,还允许用户输入的灵活性。
动态查询生成图 — 图片由作者制作
这张图展示了理解用户提问并从中提取特定信息的过程。整个过程分三个主要步骤。
函数调用功能对于高级LLM用例至关重要,例如允许LLM根据用户意图使用多个检索器或构建多代理流程。我写过一些相关文章,使用商业LLM。然而,我们将使用新发布的开源LLM Llama-3.1,它具有原生的函数调用支持。
代码可在GitHub上找到。
搭建知识图谱我们将使用Neo4j(一个原生图数据库)来存储不良事件信息。你可以通过这个链接免费设置一个云Sandbox项目,该项目已经预填充了FAERS数据。
实例化数据库实例包含如下模式的图。
作者提供的不良事件图表(图由作者提供)
该架构以病例节点为中心,将药物安全报告的各个部分联系在一起,包括所涉及的药物、产生的反应、结果以及开具的疗法。每种药物被描述为是主要的、次要的、伴随使用的或相互作用的。病例还涉及制造商信息、患者年龄组以及报告来源。此架构允许以结构化的方式追踪和分析药物、其反应和结果之间的相互关系。
让我们从建立一个数据库连接开始,通过实例化一个Neo4j图对象来实现。
os.environ["NEO4J_URI"] = "bolt://18.206.157.187:7687" # NEO4J_URI环境变量存储Neo4j数据库的连接字符串 os.environ["NEO4J_USERNAME"] = "neo4j" # NEO4J_USERNAME环境变量存储Neo4j的用户名 os.environ["NEO4J_PASSWORD"] = "elevation-reservist-thousands" # NEO4J_PASSWORD环境变量存储Neo4j的密码 graph = Neo4jGraph(refresh_schema=False) # graph变量被初始化为一个Neo4j图,且不刷新模式搭建LLM环境指南
有很多选择可以托管像Llama-3.1这样的开源大型语言模型。我们将使用NVIDIA API目录,它提供了NVIDIA NIM推理微服务并且支持Llama 3.1模型的功能调用功能。注册后,你将获得1,000个代币,足够你跟着教程一起使用。你需要创建一个API密钥,并将其复制到笔记本里。
os.environ["NVIDIA_API_KEY"] = "nvapi-" # 设置环境变量NVIDIA_API_KEY,用于NVIDIA的相关API调用 llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct")
我们将使用llama-3.1–70b,因为8b版本的可选参数在函数定义里有一些小毛病。
NVIDIA NIM 微服务的其中一个好处是,如果有安全或其他顾虑,你可以非常轻松地本地运行它们, 并且可以轻松替换,只需在 LLM 模型配置中添加一个 URL 参数即可:
# 连接到运行在本地的NIM,地址为localhost:8000,并指定使用特定的模型. llm = ChatNVIDIA( base_url="http://localhost:8000/v1", model="meta/llama-3.1-70b-instruct" )工具定义
我们将配置一个单一工具,该工具拥有四个可选参数。我们将根据这些参数构建相应的Cypher语句如下配置工具参数,从知识图谱中检索相关信息如下。我们的工具将能够根据输入的药物、年龄和药物制造商来识别最常见的副作用,如下配置。
@tool def get_side_effects( drug: Optional[str] = Field( description="问题中提到的药物。如果没有提到,则返回None。" ), min_age: Optional[int] = Field( description="患者的最小年龄。如果没有提到,则返回None。" ), max_age: Optional[int] = Field( description="患者的最大年龄。如果没有提到,则返回None。" ), manufacturer: Optional[str] = Field( description="药物的制造商。如果没有提到,则返回None。" ), ): """当需要查找常见副作用时很有用。""" params = {} filters = [] side_effects_base_query = """ MATCH (c:Case)-[:HAS_REACTION]->(r:Reaction), (c)-[:IS_PRIMARY_SUSPECT]->(d:Drug) """ if drug and isinstance(drug, str): candidate_drugs = [el["candidate"] for el in get_candidates(drug, "drug")] if not candidate_drugs: return "未找到提到的药物" filters.append("d.name IN $drugs") params["drugs"] = candidate_drugs if min_age and isinstance(min_age, int): filters.append("c.age > $min_age ") params["min_age"] = min_age if max_age and isinstance(max_age, int): filters.append("c.age < $max_age ") params["max_age"] = max_age if manufacturer and isinstance(manufacturer, str): candidate_manufacturers = [ el["candidate"] for el in get_candidates(manufacturer, "manufacturer") ] if not candidate_manufacturers: return "未找到提到的制造商" filters.append( "EXISTS {(c)<-[:REGISTERED]-(:Manufacturer {manufacturerName: $manufacturer})}" ) params["manufacturer"] = candidate_manufacturers[0] if filters: side_effects_base_query += " WHERE " side_effects_base_query += " AND ".join(filters) side_effects_base_query += """ RETURN d.name AS drug, r.description AS side_effect, count(*) AS count ORDER BY count DESC LIMIT 10 """ print(f"使用参数: {params}") data = graph.query(side_effects_base_query, params=params) return data
get_side_effects
根据指定的搜索条件从知识图谱中检索药物的常见副作用信息。它接受药物名称、患者年龄范围和药物制造商等可选参数,以便定制搜索。每个参数的描述都会传递给LLM,连同函数描述,使LLM能够理解如何使用这些参数。然后,该函数根据提供的参数构建动态的Cypher查询,执行此查询来从知识图谱中获取数据,并返回相应的副作用信息。
我们来试试这个功能吧
get_side_effects("lyrica") # 使用参数:{'drugs': ['LYRICA', 'LYRICA CR']} # [{'drug': 'LYRICA', 'side_effect': '疼痛', 'count': 32}, # {'drug': 'LYRICA', 'side_effect': '跌倒', 'count': 21}, # {'drug': 'LYRICA', 'side_effect': '有意滥用产品', 'count': 20}, # {'drug': 'LYRICA', 'side_effect': '失眠', 'count': 19}, # ...
我们的工具首先将问题中提到的Lyrica药物映射为知识图中的“[‘LYRICA’,‘LYRICA CR’]”值,然后执行相应的Cypher查询来查找侧效应。
图基LLM代理最后要做的就是配置一个LLM代理程序,让它能用定义的工具回答关于这种药的副作用的问题。
代理数据流图 — 图由作者提供
该图展示了一个用户与Llama 3.1 agent互动,询问药物副作用。代理访问一个用于查询副作用的工具,从知识图谱中获取信息,为用户提供相关的信息。
我们先来定义一下提示模板:
prompt = ChatPromptTemplate.from_messages( [ ( "system", "你是一个乐于助人的助手,帮助用户了解常见副作用的信息。" "如果有后续问题,记得向用户提问以澄清这些选项。" "只根据用户的明确要求行事。", ), MessagesPlaceholder(variable_name="chat_history"), ("user", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad"), ] )
提示模板包括系统消息、可选的聊天记录和用户输入。agent_scratchpad 专供 LLM 使用,因为它有时需要通过执行操作并从工具中检索信息来回答问题。
LangChain库通过使用bind_tools
方法,让添加工具到LLM变得简单。
tools = [get_side_effects] llm_with_tools = llm.bind_tools(tools=tools) # llm_with_tools 包含了绑定的工具,以便后续使用 agent = ( { "input": lambda x: x["input"], "chat_history": lambda x: _format_chat_history(x["chat_history"]) if x.get("chat_history") else [], "agent_scratchpad": lambda x: format_to_openai_function_messages( x["intermediate_steps"] ), } | prompt | llm_with_tools | OpenAIFunctionsAgentOutputParser() ) # agent_executor 是用于执行 agent 的执行器,指定了输入输出类型,并开启了详细日志 agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True).with_types( input_type=AgentInput, output_type=Output )
代理通过转换和处理程序来处理输入,这些程序会格式化聊天历史记录,使用绑定工具的应用LLM,并解析输出。最终,代理会被设置一个执行器来管理执行流程,指定输入和输出类型,并包含详细日志设置以在执行期间记录详细信息。
我们来试一下这个代理吧
agent_executor.invoke( { "input": "使用利克林时,35岁以下的人常见的副作用有哪些?" } )
结果如下:
代理执行图 — 作者的插图
LLM 发现它需要使用带有适当参数的 get_side_effects
函数。该函数随后动态生成 Cypher 语句,获取相关信息,并将这些信息返回给 LLM 以生成最终答案。
函数调用能力大大增强了开源模型如Llama 3.1的功能,使其能更有效地与外部数据源和工具互动。除了能查询非结构化文档,图代理提供了与知识图和结构化数据互动的多种可能性。通过使用类似NVIDIA NIM微服务的平台,可以很方便地托管这些模型,使得它们越来越容易使用。
一如既往,代码可在 GitHub 上获得。