使用Llama 3.1、NVIDIA NIM和LangChain构建基于知识图谱的代理

2024年08月05日 由 alex 发表 104 0

大多数人都关注非结构化文本(如公司文件或文档)的 RAG,而我却非常看好结构化信息的检索系统,尤其是知识图谱。关于 GraphRAG,特别是微软的实现,已经引起了很多人的兴趣。不过,在他们的实现中,输入数据是文档形式的非结构化文本,然后使用大型语言模型(LLM)将其转换为知识图谱。


在这篇文章中,我们将展示如何在知识图谱上实现检索器,该知识图谱包含来自美国食品药物管理局不良事件报告系统(FDA Adverse Event Reporting System,FAERS)的结构化信息。如果你曾经接触过知识图谱和检索,你首先想到的可能是使用 LLM 生成数据库查询,以便从知识图谱中检索相关信息来回答给定的问题。然而,使用 LLM 生成数据库查询的技术仍在不断发展,可能还不能提供最一致或最强大的解决方案。那么,目前有哪些可行的替代方案呢?


我认为,目前最好的解决方案是所谓的动态查询生成。这种方法不是完全依赖 LLM 来生成完整的查询,而是采用一个逻辑层,根据预定义的输入参数确定性地生成数据库查询。使用函数调用功能的优势在于,可以向 LLM 定义如何为函数准备结构化输入。这种方法可以确保查询生成过程的可控性和一致性,同时允许用户灵活输入。


6


图片说明了理解用户问题以检索特定信息的流程。流程包括三个主要步骤:

  1. 用户询问有关 35 岁以下人群服用 Lyrica 药物的常见副作用的问题。
  2. LLM 决定调用哪个函数以及所需的参数。在本例中,它选择了一个名为 "side_effects "的函数,参数包括药物 "Lyrica "和最大年龄 35 岁。
  3. 确定的函数和参数用于确定性地动态生成数据库查询(Cypher)语句,以检索相关信息。


功能调用支持对于高级 LLM 用例至关重要,例如允许 LLM 根据用户意图使用多个检索器或构建多代理流。我曾写过一些文章,使用的是支持原生函数调用的商用 LLM。不过,在这篇博文中,我们将使用 Llama-3.1,这是最近刚刚发布的一款支持原生函数调用的优秀开源 LLM。


设置知识图谱

我们将使用原生图数据库 Neo4j 来存储不良事件信息。你可以通过以下链接建立一个预填充 FAERS 的免费云沙箱项目。


实例化的数据库实例有一个具有以下模式的图。


7


该模式以 "病例 "节点为中心,将药物安全报告的各个方面联系起来,包括所涉及的药物、所经历的反应、结果和处方疗法。每种药物都有其特征,包括主要药物、次要药物、伴随药物或相互作用药物。病例还与制造商、患者年龄组和报告来源等信息相关联。这种模式允许以结构化的方式跟踪和分析药物、其反应和结果之间的关系。


首先,我们将通过实例化 Neo4jGraph 对象来创建与数据库的连接。


os.environ["NEO4J_URI"] = "bolt://18.206.157.187:7687"
os.environ["NEO4J_USERNAME"] = "neo4j"
os.environ["NEO4J_PASSWORD"] = "elevation-reservist-thousands"
graph = Neo4jGraph(refresh_schema=False)


设置 LLM 环境

托管 Llama-3.1 等开源 LLM 有很多选择。在本博文中,我们将使用英伟达 API 目录,它提供英伟达 NIM 推理微服务并支持 Llama 3.1 模型的函数调用。创建账户后,你将获得 1000 个代币,这对完成本篇博文来说绰绰有余。你需要创建一个 API 密钥并将其复制到笔记本中。


os.environ["NVIDIA_API_KEY"] = "nvapi-"
llm = ChatNVIDIA(model="meta/llama-3.1-70b-instruct")


我们将使用 llama-3.1-70b,因为 8b 版本在函数定义中的可选参数方面存在一些问题。


NVIDIA NIM 微服务的好处是,如果你有安全或其他方面的顾虑,你可以很容易地将它们托管到本地,所以它真的很容易交换,你只需要在 LLM 配置中添加一个 url 参数。


# connect to an local NIM running at localhost:8000, 
# specifying a specific model
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="disease mentioned in the question. Return None if no mentioned."
    ),
    min_age: Optional[int] = Field(
        description="Minimum age of the patient. Return None if no mentioned."
    ),
    max_age: Optional[int] = Field(
        description="Maximum age of the patient. Return None if no mentioned."
    ),
    manufacturer: Optional[str] = Field(
        description="manufacturer of the drug. Return None if no mentioned."
    ),
):
    """Useful for when you need to find common side effects."""
    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 "The mentioned drug was not found"
        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 "The mentioned manufacturer was not found"
        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"Using parameters: {params}")
    data = graph.query(side_effects_base_query, params=params)
    return data


get_side_effectsfunction 用于使用指定的搜索条件从知识图谱中检索药物的常见副作用。该函数接受药物名称、患者年龄范围和药物制造商等可选参数,以自定义搜索。每个参数都有一个说明,连同函数说明一起传递给 LLM,使 LLM 了解如何使用这些参数。然后,该函数会根据提供的输入构建一个动态 Cypher 查询,针对知识图谱执行该查询,并返回所得到的副作用数据。


让我们来测试一下这个函数。


get_side_effects("lyrica")
# Using parameters: {'drugs': ['LYRICA', 'LYRICA CR']}
# [{'drug': 'LYRICA', 'side_effect': 'Pain', 'count': 32},
#  {'drug': 'LYRICA', 'side_effect': 'Fall', 'count': 21},
# {'drug': 'LYRICA', 'side_effect': 'Intentional product use issue', 'count': 20},
# {'drug': 'LYRICA', 'side_effect': 'Insomnia', 'count': 19},
# ...


我们的工具首先将问题中提到的 "LYRICA "药物映射到知识图谱中的"['LYRICA', 'LYRICA CR']"值,然后执行相应的 Cypher 语句来查找最常见的副作用。


基于图的 LLM 代理

剩下要做的就是配置一个 LLM 代理,它可以使用定义的工具来回答有关药物副作用的问题。


8


图片描述的是一名用户与 Llama-3.1 代理交互,询问有关药物副作用的信息。该代理访问了一个副作用工具,该工具从知识图谱中检索信息,为用户提供相关数据。


我们将首先定义提示模板。


prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "You are a helpful assistant that finds information about common side effects. "
            "If tools require follow up questions, "
            "make sure to ask the user for clarification. Make sure to include any "
            "available options that need to be clarified in the follow up questions "
            "Do only the things the user specifically requested. ",
        ),
        MessagesPlaceholder(variable_name="chat_history"),
        ("user", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ]
)


提示模板包括系统消息、可选聊天记录和用户输入。agent_scratchpad 是为 LLM 预留的,因为它有时需要执行多个步骤来回答问题,比如执行工具并从工具中获取信息。


通过使用绑定工具(bind_tools)方法,LangChain 库可以直接将工具添加到 LLM 中。


tools = [get_side_effects]
llm_with_tools = llm.bind_tools(tools=tools)
agent = (
    {
        "input": lambda x: x["input"],"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 = AgentExecutor(agent=agent, tools=tools, verbose=True).with_types(
    input_type=AgentInput, output_type=Output
)


代理会通过一系列转换和处理程序来处理输入,这些转换和处理程序会格式化聊天记录、使用绑定工具应用 LLM 并解析输出。最后,该代理会设置一个执行器来管理执行流程、指定输入和输出类型,并包含执行期间详细日志的冗余度设置。


现在让我们测试一下代理。


agent_executor.invoke(
    {
        "input": "What are the most common side effects when using lyrica for people below 35 years old?"
    }
)


结果:


9


LLM 确定需要使用带有适当参数的 get_side_effects 函数。然后,该函数动态生成 Cypher 语句,获取相关信息,并返回给 LLM 以生成最终答案。


总结

函数调用功能是对 Llama 3.1 等开源模型的有力补充,使其能够与外部数据源和工具进行更有序、更可控的交互。除了查询非结构化文档外,基于图的代理还为与知识图谱和结构化数据交互提供了令人兴奋的可能性。利用NVIDIN NIM 微服务等平台托管这些模型的便捷性使它们越来越容易访问。

文章来源:https://medium.com/@bratanic-tomaz/build-a-knowledge-graph-based-agent-with-llama-3-1-nvidia-nim-and-langchain-feb65788e637
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
写评论取消
回复取消