大家好,欢迎再次阅读另一篇文章!我最近一直在开发生产就绪的代理式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密钥- OpenAI: 这将用于LLM模型的开发和应用。
- Tavily: 将作为代理的搜索工具,让他们搜索网络内容。
好的,从现在开始就假设你已经有这些API密钥了。我们很快就会用到它们。
首先,我们需要设置所需的一些文件夹和文件。我将使用Linux机器,如果你更喜欢,也可以用Windows或MacBook。不过,我将使用的大多数Linux命令也能在MacBook上运行。如果你用的是Windows,可以在遇到问题或错误时事先查一下资料。这样的情况发生的概率很小。
- 开始新建你的项目文件夹吧
在终端中输入以下命令:
创建一个新目录: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 数据库在处理大量数据时表现不佳,而在生产环境中,你可能会遇到大量数据。
你记得之前我们用过 model
和 tools
创建了一个代理吗?是的,我们现在也可以给代理添加记忆功能,让它能记住对话。这里我要特别提一下,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中构建带记忆的代理,并在必要时永久保存记忆。
你还可以通过以下其他平台找到我:
编程愉快!下次见啦,世界依然在转。
共同学习,写下你的评论
评论加载中...
作者其他优质文章