Type something to search...
从 LangChain 到 LangGraph:让多模型药物机器人具有个性化和可教性

从 LangChain 到 LangGraph:让多模型药物机器人具有个性化和可教性

为人机协作聊天机器人添加记忆和学习能力

药物试验测试新药物在人类中的安全性、有效性和疗效。这些关键研究对于开发和批准拯救生命的疗法至关重要。虽然药物试验为无数患有严重疾病的患者带来了希望,但许多人仍然不知道自己是否符合条件或潜在的好处。一个用户友好的药物试验信息系统可以弥补这一差距。它应该包含一个具有权威信息的数据库和一个易于使用的前端,患者可以通过它在复杂的临床研究世界中导航并发现有益的机会。

在我之前的文章中,DuckDB作为DrugDB:一个免费且简单的多模型药物和试验数据库为多模型DuckDB定制多模型聊天机器人,我构建了一个名为DrugDB的药物数据库及其对应的聊天机器人DrugBot。DrugDB建立在DuckDB之上,这是一个多模型数据库。用户可以结合SQL、图形、向量和全文查询来探索药物、疾病、作用机制(MOA)和药物试验的网络。然而,其技术性质可能对普通患者来说令人望而生畏。为弥补这一差距,我开发了一个用户友好的聊天机器人,利用LangChain和Streamlit。这个聊天机器人根据用户输入生成草稿查询。用户可以进一步完善这些查询,并让聊天机器人执行它们以找到答案。通过结合DuckDB、LangChain和Streamlit的优势,患者可以轻松访问重要的医学信息,例如药物作用机制和相关的临床试验。这种人机协作的方法确保了准确性和可靠性,减轻了潜在的LLM幻觉。

然而,DrugBot还有很大的改进空间。它缺乏会话内和用户特定的跨会话记忆,阻碍了它理解代词或用户定义术语(如彼得·阿提亚的“四骑士疾病”)的能力。此外,虽然它可以有效使用单个工具,但在处理复杂查询时却难以将它们结合起来,例如识别与前7种肝脏相关疾病相关的5个试验。此外,DrugBot无法请求对模糊查询的澄清,有时会生成不正确的LIMIT子句。

为了克服这些限制,我用LangGraph重写了DrugBot。LangGraph将LLM工作流管理为图形,信息沿着功能节点之间的边缘流动。这与状态和记忆机制相结合,使新DrugBot能够理解用户定义的术语并在对话中保持上下文。受到Vanna.ai的启发,LangGraph使DrugBot能够从过去的互动中学习,存储复杂查询,甚至为将来使用复制它们。此外,聊天机器人现在可以在遇到模糊情况时主动寻求澄清。最后,LangGraph简化了Python代码的组织,促进了可维护性。接下来,我将在本文中详细探讨这些修订。该项目的代码托管在我的GitHub仓库中。

1. DrugDB

LangGraph DrugBot 接口与我之前文章中使用的相同的 DrugDB 数据库 (1, 2, 3, 4, 和 5)。这个庞大的数据库包含超过 5000 种药物、500 种作用机制、2000 种疾病和 2000 项药物 试验。DrugDB 由 DuckDB 提供支持,并使用 DuckPGQ、fts 和 vss 扩展,支持多种查询组合,包括 SQL、图形、向量和全文搜索。这一能力使得复杂的多跳查询成为可能,例如代码 1 中的 SQL-图形混合查询,可以识别 4 项测试“UGT1A9 抑制剂”药物的试验。

/* Code 1 */

SELECT Trials.PostingID AS trial_id, Trials.StudyTitle AS StudyTitle, drug_with_moa.drug_name
FROM Trials, 
    GRAPH_TABLE(drug_graph MATCH (i:Drug)-[m:HAS_MOA]->(a:MOA WHERE a.name='UGT1A9 Inhibitors')
              COLUMNS (i.drug_cui AS drug_cui, i.name AS drug_name))  drug_with_moa
WHERE list_contains(Trials.drug_cui, drug_with_moa.drug_cui)
LIMIT 4

尽管这样的多模型数据库功能强大,但对于普通用户来说,构建准确的查询可能具有挑战性。这就是聊天机器人可以简化这一过程的地方,使用户更容易访问所需的信息。

2. 实施

2.1 概述

在其前身的基础上,LangGraph DrugBot 继续为与 DrugDB 交互提供用户友好的界面。读取用户查询、生成草稿查询、请求确认和提供人类可读答案的核心功能保持不变。然而,新版本引入了显著的增强功能,包括能够回忆过去的对话、理解用户定义的概念、提出澄清问题以及通过学习示例生成复杂查询。

LangGraph 通过其基于图的方式组织 LLM 工作流程,与 LangChain 区分开来。LangGraph 图中的三个基本元素是:节点、边和状态。节点代表处理应用程序状态的功能,而边定义了这些节点之间的数据流。状态作为应用程序的记忆,在节点之间持续存在并更新。一个 LangGraph 从一个“start”节点开始,并以一个“end”节点结束。图 2 是 DrugBot 图的视觉表示。

对话从用户输入开始。当用户提供定义语句(例如,“我们将心血管疾病、癌症、阿尔茨海默病和糖尿病定义为‘四骑士疾病’。”)时,主开关节点 select_intent 将状态数据路由到 update_concept 节点。该节点随后在用户特定的记忆中添加或更新定义,如图 2 中间部分所示。

相反,当输入是一个问题时,select_intent 会激活 select_query_tool 节点,启动主要的问答过程(如图 2 右侧和图 3 所示)。如果问题模糊,LangGraph DrugBot 将请求澄清。对于其他输入类型,例如问候或表达感谢,机器人将在过渡到 end 节点之前礼貌回应。

2.2 节点

LangGraph 节点通常接收当前状态、配置和内存存储作为输入。这些节点然后执行特定操作,例如调用 LLM、利用工具或转换数据。结果修改后的状态随后通过边缘传递给下一个节点。代码 2 中 select_intent 节点的 Python 代码作为此过程的示例。

## Code 2. The select_intent node, the route_message function 
## and their conditional edge

config = {"configurable": {"thread_id": "1", "user_id": "sixing"}}

MODEL_SYSTEM_MESSAGE = """You are a helpful chatbot. 

..."""

class Choose_Direction(TypedDict):
    """ Decision on which route to go next """
    action_type: Literal['update_concept', 'select_query_tool']

def select_intent(state, config, store):
    """Load user defined concepts from the store and use them to personalize the chatbot's response."""

    # Get the user ID from the config
    user_id = config["configurable"]["user_id"]

    # Retrieve profile memory from the store
    namespace = ("profile", user_id)
    memories = store.search(namespace)
    if memories:
        user_definition = memories[0].value
    else:
        user_definition = None

    system_msg = MODEL_SYSTEM_MESSAGE.format(user_definition=user_definition)

    messages = trim_messages(
            state["messages"],
            max_tokens=32000,
            strategy="last",
            token_counter=ChatOpenAI(model="gpt-4o"),
            allow_partial=False,
        )
    
    response = llm.bind_tools([Choose_Direction], parallel_tool_calls=False).invoke([SystemMessage(content=system_msg)] + messages)

    return {"messages": [response]}


def route_message(state, config, store) -> Literal[END, "update_concept", "select_query_tool"]:
    """Reflect on the memories and chat history to decide whether to update the memory collection."""
    message = state['messages'][-1]
    if len(message.tool_calls) ==0:
        return END
    else:
        tool_call = message.tool_calls[0]

        if tool_call['args']["action_type"] == "update_concept":
            return "update_concept"
        elif tool_call['args']["action_type"] == "select_query_tool":
            return "select_query_tool"
        else:
            raise ValueError


builder.add_edge(START, "select_intent")
## conditional edge determines whether 
## it goes to update_memory or select_query_tool
builder.add_conditional_edges("select_intent", route_message)

select_intent 节点从配置中读取 user_id,并从内存存储中获取相应的用户特定定义。这些定义随后被整合到系统提示中。为了优化令牌使用,该节点修剪状态消息。经过精炼的提示和状态消息随后发送给 LLM,LLM 决定下一步行动:要么更新内存中的概念,要么选择查询工具以生成草稿查询。

2.3 工具

工具在 LangGraph 和 LangChain 中的操作方式相同。它们是可以被 LLM 调用以执行特定任务的函数。新的聊天机器人依赖于 SQL、图形、向量、全文和新的模仿工具来生成草稿查询(图 3)。代码 3 是模仿工具的 Python 代码(图 3 中的第 5 个工具)。

#Code 3. The mimicking tool.

@tool
def mimicking(question: str, top_k: int):
    """ When you think the question is unlikely to be answer by a single simple query tool, 
    or the question may likely require a complex combination of sql, vector, graph, and full-text search tools,
    or it may require to join several tables, use this tool to generate those complex queries by closely mimicing the examples."""
    examples = []

    for line in open("interaction.jsonl", "r").readlines():
        example = json.loads(line)
        examples.append(example)

    database_description = my_db_specifics.sql_database_prompt

    example_selector = SemanticSimilarityExampleSelector.from_examples(
        examples,
        OpenAIEmbeddings(),
        LanceDB,
        k=5,
        input_keys=["input"],
    )

    example_prompt = PromptTemplate.from_template("User input: {input}\ngraph query: {query}")

    complex_generation_prompt = FewShotPromptTemplate(
        example_selector=example_selector,
        example_prompt=example_prompt,
        prefix="""You are a duckdb expert. Given an input question, take the examples as templates, and only substitute the template variables with those extracted from the question. Closely mimicing the examples and don't modify the examplar structure easily, since they are curated by human. Add a 'LIMIT {top_k}' clause to the end of the query. \n\nHere is the relevant table info: {table_info}\n\nBelow are a number of examples of questions and their corresponding queries. Use them to as inspiration generate your query.
        - Almost always start with SELECT, unless it is a graph query.
        - The subquery in FROM clause should have an alias, without the keyword AS, Here is an example: SELECT * FROM Trials, GRAPH_TABLE( ... )  drug_for_disease WHERE Trials.drug_cui = drug_for_disease.drug_cui
        - If the search term contains a single quote, it should be escaped with another single quote. For example, 'Alzheimer's Disease' should be 'Alzheimer''s Disease'.
        - Only return query not anything else like ```sql ... ```
        - Every variable in the graph pattern has to be bound by a variable. For example, (i:Drug)-[:MAY_TREAT]->(c:Disorder WHERE c.name = 'Alzheimer''s Disease') is not correct because :MAY_TREAT is not bound to a variable. Instead, it should be (i:Drug)-[m:MAY_TREAT]->(c:Disorder WHERE c.name = 'Alzheimer''s Disease').
        - If it is a graph query, use "COLUMNS" as the return statement in the graph query.
        - Based on the question, include a 'LIMIT' clause before the end of the query. Never write 'LIMIT 0;' nor 'LIMIT;' If you are unsure about the number of results, remove the LIMIT clause entirely.
        - Make sure all parentheses are balanced.
        - Ends with a semicolon
        - Output the final query only.
        """,
        suffix="User input: {input}\ngraph query: ",
        input_variables=["input", "table_info", "top_k"],
    )

    generate_query = (
        complex_generation_prompt
        | llm | StrOutputParser()
    )

    query = generate_query.invoke({"input": question, "table_info": database_description, "top_k": top_k})
    return query

顾名思义,模仿工具通过模仿用户提供的问题-查询对生成新的查询。它首先通过识别语义相似的示例,然后用从当前问题中提取的新值替换它们的变量槽。该工具特别适用于生成涉及多个搜索工具的复杂查询。

2.4 边缘

一个工作流可以有分支,其中一个节点伴随着条件边缘,做出决策以确定后续节点。代码 2 演示了 select_intent 节点如何利用这一机制在 update_concept 节点和 select_query_tool 节点之间进行选择。

2.5 状态与内存

状态就像接力赛中的接力棒。它由节点丰富,并沿着边缘传递。它是一个包含消息数组和附加数据的字典(代码 4)。这些状态数据可以在内存中累积,使聊天机器人能够回忆起过去的对话内容。

## Code 4. The incorporation of state and memory in the graph

from langgraph.graph import StateGraph, MessagesState

class State(MessagesState):
    selected_tools: list[str]

builder = StateGraph(State)

db_path = 'checkpoints.db'
conn = sqlite3.connect(db_path, check_same_thread=False)
within_thread_memory = SqliteSaver(conn)

across_thread_memory = InMemoryStore()
app = builder.compile(checkpointer=within_thread_memory, interrupt_before=["human_feedback"], store=across_thread_memory)

然而,由于 Streamlit 的频繁应用重启会清空内存,LangGraph 聊天机器人必须将其状态存储在磁盘上。此外,LangGraph DrugBot 利用长期记忆在多个聊天会话中保留用户特定的信息,使用户能够定义自定义概念,例如“四骑士疾病。”

2.6 接口

更新后的接口通过引入查询工具下拉菜单和“保存交互”按钮(图1)增强了之前的版本。下拉菜单使用户能够为每次交互选择首选的查询方法。“保存交互”按钮允许将当前问题及其对应的 DrugDB 查询保存到 JSONL 文件中,该文件可供模拟工具用于未来的查询生成。确认面板现在集成了一个由 streamlit-monaco 提供支持的查询编辑器,提供语法高亮和行号。

3. 测试

之前的聊天机器人缺乏记忆功能,并且在生成复杂的混合查询时遇到困难。此外,SQL 和图形查询工具有时会设置不正确的限制值,导致结果不准确。新的聊天机器人专门设计用来克服这些局限性。因此,我的测试工作主要集中在评估其在这些领域的表现。

3.1 用户定义的长期记忆概念

如图1所示,用户最初定义了概念“三家小型制药公司”(“‘三家小型制药公司’是指Astellas、Novartis和ViiV。”)。LangGraph聊天机器人将此列表存储在其长期记忆中。当用户随后在第二轮中提到该概念时(“列出‘三家小型制药公司’赞助的试验数量。”),聊天机器人成功解析了该提及并生成了正确的查询。

3.2 短期记忆

后续测试展示了聊天机器人的能力,它能够通过利用短期记忆来提出后续问题并解决代词问题(图4)。

当用户询问,“这种药物可以治疗什么疾病?”时,聊天机器人最初难以理解对“这种药物”的指代。为了澄清问题,它问道,“您能否具体说明您所指的药物名称?”一旦用户提供了药物名称氟氯噻嗪,聊天机器人便能够继续并完成查询。

3.3 复杂混合查询

此外,新的聊天机器人可以使用创新的模拟工具构建复杂的混合查询。

如图5所示,用户提出了一个三跳查询:“显示5个测试药物针对前7种肝脏相关疾病的试验。”聊天机器人分析了该查询,从预定义集合中识别出最相关的示例,并基于这些示例合成了一个正确的查询。此交互可以保存以供将来参考,促进持续学习循环。

3.4 LIMIT 子句

最后,新的 SQL 和图形查询工具采用了两阶段的过程来创建草稿查询。首先,它们分析用户的问题以生成核心查询结构,包括 SELECT、FROM 和 WHERE 子句。然后,它们检查用户是否指定了任何结果限制——如果是,它们会在查询的末尾添加适当的 LIMIT 子句以限制输出大小。这种模块化的方法确保生成的查询在语法上是正确的、准确的,并且适当地被限制。

图 6 中的两个对话展示了聊天机器人准确实现用户指定的限制的能力。此外,当用户未提供明确限制时,聊天机器人设置了默认限制为 30,以防止界面冻结。

结论

如今,构建 AI 工作流的无代码平台越来越多,例如 n8nzapierVoiceflow。用户可以通过拖放界面和可视化编程快速构建 LLM 应用,而无需编写任何代码。但是,当涉及到深度数据探索和自定义数据库交互时,它们往往存在不足。当用户需要执行专业查询或根据其独特数据架构创建上下文感知的响应时,通过编码开发定制聊天机器人变得至关重要。一个旨在理解特定数据库结构细微差别的定制聊天机器人,可以利用先进的索引方法和复杂的检索算法。这使得对话更加自然,结果更加准确。新的 DrugBot 通过 LangGraph 框架增强,体现了这种方法。通过简化代码维护和提高用户响应能力,DrugBot 现在可以进行基于上下文的对话,并回答多跳查询,从而提升整体用户体验。

虽然聊天机器人在提供自然语言响应和解释方面表现出色,但其基于文本的格式固有地限制了其有效传达复杂数据模式和关系的能力。仪表板和数据可视化则可以通过图表、图形和交互式显示即时传达趋势、比较和多维见解,这些用语言描述起来将显得繁琐 (1, 2)。当这些互补的方法结合在一起时,真正的力量就会显现:聊天机器人可以引导用户进行数据解读并回答具体问题,而可视化则提供我们大脑天生能够理解的模式和关系的直接、直观的把握。这种结合的方法创造了一个全面的数据探索体验,既服务于需要详细见解的分析专家,也满足了偏好引导解读的普通用户,确保所有专业水平的用户都能获取数据的全部价值。

Related Posts

使用 ChatGPT 搜索网络功能的 10 种创意方法

使用 ChatGPT 搜索网络功能的 10 种创意方法

例如,提示和输出 你知道可以使用 ChatGPT 的“搜索网络”功能来完成许多任务,而不仅仅是基本的网络搜索吗? 对于那些不知道的人,ChatGPT 新的“搜索网络”功能提供实时信息。 截至撰写此帖时,该功能仅对使用 ChatGPT 4o 和 4o-mini 的付费会员开放。 ![](https://images.weserv.nl/?url=https://cdn-im

阅读更多
在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和科技这样一个动态的行业中,保持领先意味着不断提升你的技能。无论你是希望深入了解人工智能模型性能、掌握数据分析,还是希望通过人工智能转变传统领域如法律,这些课程都是你成功的捷径。以下是一个精心策划的高价值课程列表,可以助力你的职业发展,并让你始终处于创新的前沿。 1. 生成性人工智能简介课程: [生成性人工智能简介](https://genai.works

阅读更多
10 个强大的 Perplexity AI 提示,让您的营销任务自动化

10 个强大的 Perplexity AI 提示,让您的营销任务自动化

在当今快速变化的数字世界中,营销人员总是在寻找更智能的方法来简化他们的工作。想象一下,有一个个人助理可以为您创建受众档案,建议营销策略,甚至为您撰写广告文案。这听起来像是一个梦想? 多亏了像 Perplexity 这样的 AI 工具,这个梦想现在成为现实。通过正确的提示,您可以将 AI 转变为您的 个人营销助理。在本文中,我将分享 10 个强大的提示,帮助您自动

阅读更多
10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

10+ 面向 UI/UX 设计师的顶级 ChatGPT 提示

人工智能技术,如机器学习、自然语言处理和数据分析,正在重新定义传统设计方法。从自动化重复任务到实现个性化用户体验,人工智能使设计师能够更加专注于战略思维和创造力。随着这一趋势的不断增长,UI/UX 设计师越来越多地采用 AI 驱动的工具来促进他们的工作。利用人工智能不仅能提供基于数据的洞察,还为满足多样化用户需求的创新设计解决方案开辟了机会。 1. 用户角色开发 目的

阅读更多
在几分钟内完成数月工作的 100 种人工智能工具

在几分钟内完成数月工作的 100 种人工智能工具

人工智能(AI)的快速发展改变了企业的运作方式,使人们能够在短短几分钟内完成曾经需要几周或几个月的任务。从内容创作到网站设计,AI工具帮助专业人士节省时间,提高生产力,专注于创造力。以下是按功能分类的100个AI工具的全面列表,以及它们在现实世界中的使用实例。 1. 研究工具 研究可能耗时,但人工智能工具使查找、分析和组织数据变得更加容易。**ChatGPT, Cop

阅读更多
你从未知道的 17 个令人惊叹的 GitHub 仓库

你从未知道的 17 个令人惊叹的 GitHub 仓库

Github 隐藏的宝石!! 立即收藏的代码库 学习编程相对简单,但掌握编写更好代码的艺术要困难得多。GitHub 是开发者的宝藏,那里“金子”是其他人分享的精心编写的代码。通过探索 GitHub,您可以发现如何编写更清晰的代码,理解高质量代码的样子,并学习成为更熟练开发者的基本步骤。 1. notwaldorf/emoji-translate *谁需

阅读更多