为了账号安全,请及时绑定邮箱和手机立即绑定

为基于大语言模型的应用添加持久记忆功能

大家好,欢迎再次阅读另一篇文章!我最近一直在开发生产就绪的代理式LLM应用程序(即生产就绪的代理式LLM应用程序,简称PRALBA),途中遇到了不少挑战。其中一个最大的挑战是内存管理和维护我LLM应用程序的状态。

最近一段时间以来,随着LLM编排库和框架(例如 LangChain、LlamaIndex 和 CrewAI)的更新,这个问题已经在一定程度上得到了解决。我觉得和大家分享我的经验和解决方法会很酷。

想象这样一个场景:你正在和一个智能聊天软件聊天,而这个智能聊天软件不记得你们上次的对话。这会非常麻烦,因为你得提醒它之前聊过的内容。这并不太方便,对吧?你的时间可以用来做更有意义的事情。

在这篇文章中,我们将深入探讨如何使用LangGraph库构建基于LLM的持久性代理应用。这不仅让我们能够创建生产级别的代理,还能在应用中为多个用户维护记忆。让我们开始吧,加油!

由代码制作,包含王子元素的图片

环境搭建

首先,让我们设置将用于此项目的开发环境设置。我将使用Python的Poetry工具来管理和安装所需的依赖和包。

你也需要拥有一个OpenAI API密钥和一个Tavily API密钥。你可以在这里(这里)[https://platform.openai.com/api-keys]和这里(_这里_)[https://app.tavily.com/sign-in]分别获取它们

为什么要有这些API密钥
  1. OpenAI: 这将用于LLM模型的开发和应用。
  2. Tavily: 将作为代理的搜索工具,让他们搜索网络内容。

好的,从现在开始就假设你已经有这些API密钥了。我们很快就会用到它们。

首先,我们需要设置所需的一些文件夹和文件。我将使用Linux机器,如果你更喜欢,也可以用Windows或MacBook。不过,我将使用的大多数Linux命令也能在MacBook上运行。如果你用的是Windows,可以在遇到问题或错误时事先查一下资料。这样的情况发生的概率很小。

  1. 开始新建你的项目文件夹吧

在终端中输入以下命令:

创建一个新目录:mkdir langgraph_agents

2. 发起一个诗歌项目

poetry init (初始化 poetry 项目)

只需按回车键接受所有默认设置。

由代码生成的图片,向王子致敬

3. 创建持久代理

先创建一个名为persistence_agents的目录,然后列出当前目录的内容
mkdir persistence_agents  
ls

由代码制作的图片,特别为王子定制的

4:打开你最喜爱的代码编辑器

对于这一部分,我将使用VS Code,因为它有Jupyter Notebook的扩展,我可以使用。你也可以用普通的Python文件,但我不建议这样做。

图片由代码和王子共同创作

这幅图用代码制作,王子特别参与

5. 创建一个.env文件

创建一个名为 .env 的文件(注意前面有一个点),该文件中将包含我们从 OpenAI 和 Tavily 获取的所有 API 密钥,如下所示。

图片由Code With Prince制作

6. 安装依赖项:

我们将安装几个依赖,使用如下所示的 poetry 命令来安装:

可以使用 poetry 命令来添加以下依赖包:

poetry add python-dotenv langgraph langchain tavily-python langchain-openai langchain-community

7. 创建笔记本.

一旦我们准备好所有这些,我们就可以开始创建我们的第一个笔记本了。在 persistence_agents 文件夹中添加一个名为 Lesson_01.ipynb 的笔记本文件。完成后,在窗口的右上角,你应该会看到一个名为 选择内核 的按钮,点击它,选择 Python Environments,然后选择 .venv 文件夹。

图片由代码生成,和王子有关系

这个文件夹可能在你的设备上不存在,如果不在,请运行这个命令。

设置 poetry 配置,使虚拟环境在项目内创建: poetry config virtualenvs.in-project true

由代码与王子制作的图片

您的项目结构现在应该和上面显示的一样,只需在VScode中安装ipynbkernel即可。

导入和环境变量

我们已经安装了一些库,现在让我们在代码中使用它们。首先,我们需要导入这些库,并设置好配置。

import dotenv  
# 加载 dotenv 扩展并设置环境变量
%load_ext dotenv  
%dotenv
    从 langgraph.graph 导入 StateGraph 和 END  
    从 typing 导入 TypedDict, Annotated  
    导入 operator  
    从 langchain_core.messages 导入 AnyMessage, SystemMessage, HumanMessage, ToolMessage  
    从 langchain_openai 导入 ChatOpenAI  
    从 langchain_community.tools.tavily_search 导入 TavilySearchResults
工具制作

在我的 Medium 页面之前的文章中,我们讨论了智能代理使用工具的情况。工具帮助我们的代理程序与外部世界交互。你可以创建自己的自定义工具,或者使用现有的工具。我们这里将使用 Tavily 的搜索工具在网上查找信息,获取作为上下文的信息。

    tool = TavilySearchResults(max_results=2)

在这种情况下,我们希望传递一个查询字符串,并只获取2个搜索结果,这样可以避免溢出上下文窗口。从搜索工具中返回太多结果会导致文本过多,从而避免溢出上下文窗口,我们应尽量避免这种情况。

代理状态

代理状态是一个对象,用来存储代理的所有活动历史,比如之前的对话。代理状态不仅仅存储之前的对话,可以自定义包含不同的键。我们这里只用一个键来存储messages

    class AgentState(TypedDict):  
        # 消息列表,支持累加操作
        messages: Annotated[list[AnyMessage], operator.add]
注释:TypedDict 是 Python 类型提示中的一个字典类型,用于定义具有特定类型键和值的字典。

此代理状态是一个 TypedDict,消息键是一个由 LangChain 提供的 AnyMessage 类型的 sequence。然后我们使用 operator.add 方法来处理它。此方法可以确保每当代理活动产生新消息时,我们不会丢弃已存储的旧消息,而是将新消息添加到代理状态的现有(旧)消息列表中。

创建一个模型

我们将使用gpt-4模型。我发现它和代理一起工作非常方便。

    model = ChatOpenAI(model="gpt-4")

如果你没有正确设置 OPENAI_API_KEY 环境变量,上面的代码行将无法运行。请确保你已经正确设置了这个环境变量。

我们可以试试这个模型,提一个你在ChatGPT上通常会输入的问题。

    from langchain_core.messages import HumanMessage   

    response = model.invoke([HumanMessage(content="hello there")])  
    print(str(response))

这段代码用于调用模型并传入一个包含“hello there”的HumanMessage对象,最后打印出模型的响应。

代码生成的图片,带有王子元素

你可以使用漂亮打印来美化输出

    print(f"内容:\n{response.content}\n\nToken使用:{response.response_metadata['token_usage']['total_tokens']}"))

由代码与王子共同创作的图片

把工具和模型关联起来

我们现在可以让模型知道有哪些工具可供使用,这个过程叫做绑定工具到模型,这个名字也不是很正式:),下面就是我们如何做到这一点的方法:

    tools = [工具]  
    带有工具的模型 = model.bind_tools(tools)
创建一个智能代理

我们现在有了创建一个代理、模型和工具的所有材料和组件。现在,我们来实现一个代理:

from langgraph.prebuilt import create_react_agent  

# 创建一个名为agent_executor的代理执行器,使用提供的model和tools
agent_executor = create_react_agent(model, tools)

我们来试试这个代理吧。

    response = agent_executor.invoke({"messages": [HumanMessage(content="内罗毕现在的天气怎么样?")]})  

    response["messages"]

图片由代码和王子创作

我现在在肯尼亚的内罗毕。我可以向你保证,答案是正确的。你可以看到它被用于Tavily搜索工具中,以获取最新的天气信息,正如我们特别提到的,“当前”的信息。gpt-4并没有提供当前的信息。太好了,我们现在看到了工具的实际应用。

创建智能代理的记忆

让代理记住事情或之前的对话是因为记忆。我们来创建一个在内存中的记忆,好吧,这听起来有点奇怪,对吧,“内存中的记忆”?是的,这意味着一个存放在电脑RAM里的临时记忆,它不会永久保存在磁盘上的任何特定位置。

from langgraph.checkpoint.sqlite import SqliteSaver  

# 使用连接字符串":memory:"创建内存对象
memory = SqliteSaver.from_conn_string(":memory:")

我们能够创建“内存中的内存”的原因是我们指定了一个 ":memory:" 参数。你也可以传入一个 SQLite 文件数据库路径作为参数,同样可以正常使用。不过我觉得你在生产应用中可能不会想这样做,因为 SQLite 数据库在处理大量数据时表现不佳,而在生产环境中,你可能会遇到大量数据。

为代理添加内存

你记得之前我们用过 modeltools 创建了一个代理吗?是的,我们现在也可以给代理添加记忆功能,让它能记住对话。这里我要特别提一下,LangChain 在实现这一点的方式上做得非常出色。现在我们不仅仅传入一些唯一的 thread_id 来跟踪我们想要使用的记忆,这让它非常适合在多用户的生产系统中使用。我们可以根据用户的 session_id 来追踪每个用户在使用应用程序时的记忆。

    agent_executor = create_react_agent(model, tools, checkpointer=memory)  

    config = {"configurable": {"thread_id": "test_thread"}}

为了测试一下我们的代理能不能记住这些信息的具体内容,我将向它提供这些信息,然后后来再问它相关的问题。

嗯,我有一个朋友叫约翰,约翰有一个妹妹叫海伦。海伦喜欢和约翰的朋友汤玛斯一起踢球。

    for chunk in agent_executor.stream(  
        {"messages": [HumanMessage(content="我有一个叫约翰的朋友,约翰有一个叫海伦的姐姐。海伦喜欢和约翰的朋友托马斯一起踢足球。")]}, config  
    ):  
        print(chunk)
print(chunk['agent']['消息s'][0].内容)

图片由代码与王子合作完成

好的,看样子我们的代理也明白了。现在我们来问它一个问题,看看它还记得不。

    for chunk in agent_executor.stream(  
        {"messages": [HumanMessage(content="谁是Thomas?")]}, config  
    ):  
        输出(chunk)
print(chunk["agent"]["messages"][0].文本)

图片由代码和王子一起创作

这是机器人给出的回答:

根据提供的信息,汤姆是约翰的朋友,他和约翰的妹妹海伦一起踢足球。

你现在可以看到,机器人能记住之前的对话了。真酷!

开始新的对话

我们也可以用不同的线程ID(也就是 thread_id)与机器人开始新的对话。在这种情况下,机器人将不知道任何事情,因为我们使用的是不同的线程ID(也就是 thread_id)。

    # 传递新的thread_id开始新对话
    config = {"configurable": {"thread_id": "test_thread_2"}}  

    for chunk in agent_executor.stream(  
        {"messages": [HumanMessage(content="Thomas是谁?")]}, config  
    ):  
        print(chunk)
print(chunk["agent"]["消息s"][0]["内容"])

由代码制作的图片

这是机器人给出的回复

为了提供更准确的信息,你能说明一下你指的是哪个“Thomas”吗?有很多叫Thomas的名人,例如托马斯·爱迪生、托马斯·杰斐逊、托马斯·阿奎纳等。

你看,它不知道“Thomas”是谁,因为它对“Thomas”的记忆不一样。

将SQLite数据库当作内存来使用

我们一直用的是“内存中的记忆”,现在让我们把代理的记忆存储在一个更持久的地方,比如SQLite数据库。

    从 langgraph.checkpoint.sqlite 模块导入 SqliteSaver  

    memory = SqliteSaver.from_conn_string("sqlite.sqlite")

现在我们用刚刚创建的新记忆来创建一个新的代理,并且提出一些新的问题。

    agent_executor = create_react_agent(model, tools, checkpointer=memory)  # 创建一个react代理执行器,使用给定的模型、工具以及检查点器设置为内存。

    config = {"configurable": {"thread_id": "test_thread_sqlite"}}  # 可配置的设置,其中thread_id设置为test_thread_sqlite。
    for chunk in agent_executor.stream(  
        {"messages": [HumanMessage(content="我有一个叫约翰的朋友。约翰有一个叫海伦的妹妹。海伦喜欢和约翰的朋友汤姆一起踢足球。")]}, config  
    ):  
        print(chunk)
    print(chunk["agent"]["messages"][0].content)

这真有意思!你有没有关于约翰、海伦或汤姆的具体问题,或者想聊的话题?

    for chunk in agent_executor.stream(  
        {"messages": [HumanMessage(content="对于 John 来说,Thomas 是谁?")]}, config  
    ):  
        print(chunk)
print(chunk["agent"]["消息s"][0]["内容"])

根据你给的信息,汤姆是约翰的一个朋友。

一旦你运行了这些代码,你会看到在你的Notebook文件所在的同一目录下出现了一个SQLite文件。

图片由代码创作,献给王子殿下

你可以打开这个 SQLite 数据库文件,看看里面的内容。

图片由代码制作(或直接翻译为:图片由Code with Prince制作)

我使用了一个叫做“DB Browser For SQLite”的应用程序,如下所示应用程序图片底部一行,靠近Gimp和蓝牙图标的位置的。

图片由王子编码制作

结论

恭喜你走到这一步,真是不容易!在这篇文章中,我们介绍了构建能记住先前对话的持久性代理。简单来说,你已经学会如何构建有记忆的代理了。可以把记忆存到内存或像SQLite这样的数据库中。

希望这篇文章现在能帮助你清晰地说明如何在LangChain中构建带记忆的代理,并在必要时永久保存记忆。

你还可以通过以下其他平台找到我:

  1. 优兔: 欢迎订阅我的YouTube频道
  2. 推特: 欢迎关注我的Twitter
  3. 领英: 欢迎访问我的LinkedIn个人主页
  4. 迪斯科: 欢迎加入我的Discord服务器

编程愉快!下次见啦,世界依然在转。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消