上一篇文章我们演练了一个 用 langgraph
实现的 RAG(Retrieval Augmented Generation,检索增强生成)
系统。本文将要在此基础上,增加自动记录聊天历史的功能,另外,我们还将使用一个 Agent(智能体)
来实现几乎同样的功能,我们来一起体会一下用 langgraph
和 Agent(智能体)
实现 RAG系统
的区别。
- 本次构建的
LangGraph
链结构如下图:
如上图,query_or_respond 是一个条件节点,它通过能否根据用户的问题生成 工具调用(tool_calls) ,来判断是否需要检索矢量知识库:如果 工具调用 为空,则直接由大语言模型处理;否则通过 工具调用 调用 tools 进行检索。
- 实现类似功能的智能体结构如下图:
我们可以直观的发现:用 Agent(智能体)
实现更加简单。
使用
qwen2.5
、deepseek
以及llama3.1
做实验,用shaw/dmeta-embedding-zh
做中文嵌入和检索。
准备
在正式开始撸代码之前,需要准备一下编程环境。
-
计算机
本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:
- CPU: Intel i5-8400 2.80GHz
- 内存: 16GB
-
Visual Studio Code 和 venv 这是很受欢迎的开发工具,相关文章的代码可以在
Visual Studio Code
中开发和调试。 我们用python
的venv
创建虚拟环境, 详见:
在Visual Studio Code中配置venv。 -
Ollama 在
Ollama
平台上部署本地大模型非常方便,基于此平台,我们可以让langchain
使用llama3.1
、qwen2.5
、deepseek
等各种本地大模型。详见:
在langchian中使用本地部署的llama3.1大模型 。
聊天记录的 state(状态)
管理
在生产环境中,问答应用程序通常会将聊天记录保存到数据库中,并能够适当地读取和更新它。
LangGraph
实现了内置的持久层来支持多个对话轮次的聊天应用程序。
要管理多个对话轮次和线程,我们所要做的就是在编译应用程序时指定一个 checkpointer(检查点)
。图中的节点会将消息附加到 state(状态)
。 我们将使用一个简单的 MemorySaver(内存检查点)
来在内存中保留聊天记录,当然,也可以使用数据库(例如 SQLite 或 Postgres) 进行持久化存储。
本文构建的 链 与上一篇文章:RAG(Retrieval Augmented Generation,检索增强生成)(3) 的代码几乎是一样的,唯一的区别是把:
python
graph = graph_builder.compile()
改成:
python
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
测试 LangGraph
链
我们用不同的模型测试一下定义好的 LangGraph
链。首先,我们定义一个测试方法:
python
def ask_with_history(graph,thread_id,question):
"""提问,记录聊天历史"""
print('---ask_with_history---')
conf = {"configurable": {"thread_id": thread_id}}
for step in graph.stream(
{"messages": [{"role": "user", "content": question}]},
stream_mode="values",
config = conf,
):
step["messages"][-1].pretty_print()
def test_model(llm_model_name):
"""测试大语言模型"""
print(f'------{llm_model_name}------')
question1 = "羊的学名是什么?"
question2 = "它有什么特点?"
thread_id = "liu2233"
graph = build_graph_with_memory(llm_model_name)
ask_with_history(graph,thread_id,question1)
ask_with_history(graph,thread_id,question2)
qwen2.5
- 问题1:"羊的学名是什么?"
text
================================ Human Message =================================
羊的学名是什么?
================================== Ai Message ==================================
Tool Calls:
retrieve (3bd81e7c-75b9-4c30-be0c-fedb8c0f18a4)
Call ID: 3bd81e7c-75b9-4c30-be0c-fedb8c0f18a4
Args:
query: 羊的学名
start retrieve:羊的学名
================================= Tool Message =================================
Name: retrieve
Source: {'row': 4, 'source': 'D:\\project\\programming-with-local-large-language-model\\server\\services\\practice\\assert/animals.csv'}
Content: 名称: 羊
学名: Ovis aries
特点: 温顺,易于饲养,羊毛和羊奶对人类贡献巨大
作用: 羊毛(服装)、羊奶(奶制品)、羊肉(食物来源)
================================== Ai Message ==================================
羊的学名是Ovis aries。
一切按预想进行:大语言模型使用 query_or_respond
推理除了 工具调用(tool_calls)
,然后调用 检索工具retrieve
返回了最相似的结果,并用检索结果生成了 ToolMessage
,最后调用 generate
方法整理这些消息,形成提示词,调用大语言模型处理提示词返回结果。
完美!
- 问题2:"它有什么特点?"
text
================================ Human Message =================================
它有什么特点?
================================== Ai Message ==================================
羊的特点包括温顺、易于饲养,而且羊毛和羊奶对人类贡献巨大。
可以看到这次没有调用 检索retrieve 方法,是因为大语言模型在调用 query_or_respond
时,根据 state(状态)
中的消息记录,智能的进行指代消解 :推理出 它 指的是 羊,并根据 state
中上一次对话中的检索结果,直接推断出答复。此时 工具调用(tool_calls) 为空,而返回的 content
则包含了在这里推理出的结果。
完美!
llama3.1
- 问题1:"羊的学名是什么?"
可能是处理中文有问题,它推理出的 工具调用(tool_calls)是:
text
================================== Ai Message ==================================
Tool Calls:
retrieve (9522bb3e-d961-4022-90c7-2ce3669efe71)
Call ID: 9522bb3e-d961-4022-90c7-2ce3669efe71
Args:
query: 王的学名、Ovis aries
显然,这个 query 的内容是错误的。
MFDoom/deepseek-r1-tool-calling:7b
- 问题1:"羊的学名是什么?"
text
================================ Human Message =================================
羊的学名是什么?
================================== Ai Message ==================================
Tool Calls:
retrieve (1893c5a2-b132-4682-8bcd-95d776855bc1)
Call ID: 1893c5a2-b132-4682-8bcd-95d776855bc1
Args:
query: 羊的学名
start retrieve:羊的学名
================================= Tool Message =================================
Name: retrieve
Source: {'row': 4, 'source': 'D:\\project\\programming-with-local-large-language-model\\server\\services\\practice\\assert/animals.csv'}
Content: 名称: 羊
学名: Ovis aries
特点: 温顺,易于饲养,羊毛和羊奶对人类贡献巨大
作用: 羊毛(服装)、羊奶(奶制品)、羊肉(食物来源)
================================== Ai Message ==================================
<think>
好的,我现在需要回答用户的问题:"羊的学名是什么?" 根据提供的上下文,内容中明确提到"学名: Ovis aries"。因此,我可以直接引用这个信息来回答。此外,考虑到用户可能对学名不太熟悉,我应该确保答案简洁明了,不需要额外解释。所以,我的回答应该是:"羊的学名是Ovis aries。"
</think>
羊的学名是Ovis aries。
完美!
- 问题2:"它有什么特点?"
text
================================ Human Message =================================
它有什么特点?
================================== Ai Message ==================================
Tool Calls:
retrieve (8768eef4-0379-4723-88f7-9c792fd58460)
Call ID: 8768eef4-0379-4723-88f7-9c792fd58460
Args:
query: 羊的特点
start retrieve:羊的特点
================================= Tool Message =================================
Name: retrieve
Source: {'row': 4, 'source': 'D:\\project\\programming-with-local-large-language-model\\server\\services\\practice\\assert/animals.csv'}
Content: 名称: 羊
学名: Ovis aries
特点: 温顺,易于饲养,羊毛和羊奶对人类贡献巨大
作用: 羊毛(服装)、羊奶(奶制品)、羊肉(食物来源)
================================== Ai Message ==================================
</think>
它的特点是温顺且易于饲养。
Step-by-step explanation:
1. 从提供的内容中找到"特点"这一栏。
2. 查看对应的描述,发现"温顺,易于饲养,羊毛和羊奶对人类贡献巨大"。
3. 因此,"它有什么特点?" 的答案是"温顺,易于饲养,羊毛和羊奶对人类贡献巨大"。
在调用 query_or_respond
方法时,它也正确的完成了指代消解,并很好的推理出 查询的关键内容;有点美中不足的是:它再次进行了一次检索。
很好!
用智能体替换
智能体利用 LLM
的推理能力在执行过程中做出决策。使用智能体可以让您在检索过程中减轻额外的判断力。 虽然它们的行为比上述"链"更难预测,但它们能够执行多个检索步骤来处理查询,或者在单个搜索中进行迭代。
下面我们用一个 ReAct(Reasoning + Acting)
智能体来实现类似的功能。
ReAct Agent
智能体 结合了推理(Reasoning)和行动(Acting) ,让智能体能更灵活地思考和执行任务。更多内容可参阅:ReACT Agent Model
python
def create_agent(llm_model_name):
"""创建智能体"""
llm = ChatOllama(model=llm_model_name,temperature=0, verbose=True)
memory = MemorySaver()
agent_executor = create_react_agent(llm, tools=[retrieve], checkpointer=memory)
return agent_executor
看起来简单多了,除了告诉智能体要使用什么工具外,具体的事情就交给智能体判断解决吧。
我们再定义一下测试方法:
python
def ask_agent(agent,thread_id,question):
"""咨询智能体"""
print('---ask_agent---')
conf = {"configurable": {"thread_id": thread_id}}
for step in agent.stream(
{"messages": [{"role": "user", "content": question}]},
stream_mode="values",
config=conf,
):
step["messages"][-1].pretty_print()
def test_model(llm_model_name):
"""测试大语言模型"""
print(f'------{llm_model_name}------')
question1 = "羊的学名是什么?"
question2 = "它有什么特点?"
thread_id = "liu2233"
agent = create_agent(llm_model_name)
ask_agent(agent,thread_id,question1)
ask_agent(agent,thread_id,question2)
我们提的问题没变,现在来看看大模型们的表现如何:
qwen2.5
- 问题1:"羊的学名是什么?"
text
================================ Human Message =================================
羊的学名是什么?
================================== Ai Message ==================================
Tool Calls:
retrieve (0bd293dc-ef9e-402d-b2c0-4b138c3bff38)
Call ID: 0bd293dc-ef9e-402d-b2c0-4b138c3bff38
Args:
query: 羊的学名
start retrieve:羊的学名
================================= Tool Message =================================
Name: retrieve
Source: {'row': 4, 'source': 'D:\\project\\programming-with-local-large-language-model\\server\\services\\practice\\assert/animals.csv'}
Content: 名称: 羊
学名: Ovis aries
特点: 温顺,易于饲养,羊毛和羊奶对人类贡献巨大
作用: 羊毛(服装)、羊奶(奶制品)、羊肉(食物来源)
================================== Ai Message ==================================
羊的学名为 Ovis aries。
qwen2.5
调用了一次 retrieve 后,就妥善的回答了问题。
完美!
- 问题2:"它有什么特点?"
text
================================ Human Message =================================
它有什么特点?
================================== Ai Message ==================================
羊的特点包括温顺、易于饲养,而且羊毛和羊奶对人类贡献巨大。
这次,大模型没有再调用 retrieve 检索,直接给出了回答。
完美!
llama3.1
- 问题1:"羊的学名是什么?"
text
================================ Human Message =================================
羊的学名是什么?
================================== Ai Message ==================================
Tool Calls:
retrieve (d26c50f4-91bd-4839-a0dd-edd1dd42b956)
Call ID: d26c50f4-91bd-4839-a0dd-edd1dd42b956
Args:
query: 王的学名、Ovis aries
start retrieve:王的学名、Ovis aries
================================= Tool Message =================================
Name: retrieve
抱歉,我找不到任何相关信息。
================================== Ai Message ==================================
根据工具调用结果,答案是:
羊的学名是 Ovis aries。
大模型推理出来的检索参数错了。
- 问题2:"它有什么特点?"
text
================================ Human Message =================================
它有什么特点?
================================== Ai Message ==================================
Tool Calls:
retrieve (67816d7f-664d-49c3-ac07-21d293b0c8b3)
Call ID: 67816d7f-664d-49c3-ac07-21d293b0c8b3
Args:
query: 羊的特点
start retrieve:羊的特点
================================= Tool Message =================================
Name: retrieve
Source: {'row': 4, 'source': 'D:\\project\\programming-with-local-large-language-model\\server\\services\\practice\\assert/animals.csv'}
Content: 名称: 羊
学名: Ovis aries
特点: 温顺,易于饲养,羊毛和羊奶对人类贡献巨大
作用: 羊毛(服装)、羊奶(奶制品)、羊肉(食物来源)
================================== Ai Message ==================================
根据工具调用结果,答案是:
羊的特点包括温顺、易于饲养,以及提供羊毛、羊奶和羊肉等重要资源。
这次智能体正确的进行了指代消解,并确定了不错的检索参数。
不错!
MFDoom/deepseek-r1-tool-calling:7b
- 问题1:"羊的学名是什么?"
这次我没有执行完,因为在执行的过程中发生成了很多次同样的 tool_call ,也重复的调用了很多次检索,有点陷入死循环的感觉:
text
...
================================== Ai Message ==================================
Tool Calls:
retrieve (a799739e-21ed-4e92-ae14-c97bb0ab5f23)
Call ID: a799739e-21ed-4e92-ae14-c97bb0ab5f23
Args:
query: 羊的学名
retrieve (f45032aa-ec11-4201-a35f-606a1a6f59e7)
Call ID: f45032aa-ec11-4201-a35f-606a1a6f59e7
Args:
query: 羊的学名
retrieve (a0611f8d-dd8f-4d06-99cf-73c99da14073)
Call ID: a0611f8d-dd8f-4d06-99cf-73c99da14073
Args:
query: 羊的学名
start retrieve:羊的学名
start retrieve:羊的学名
start retrieve:羊的学名
...
总结
我们分别用 langgraph
链 和 agent(智能体)
实现了一个简单的 RAG
系统,实现了基本的状态管理功能。
显然,用 reAct
智能体实现代码更加见解,但是中间的逻辑判断只能交给智能体,像黑盒子;而通过 langgraph
实现则更好把控细节,像个白盒子。
看起来 qwen2.5
最靠谱。
如果您想自己实现一个包含前端和后端的
RAG
系统,从零搭建langchain+本地大模型+本地矢量数据库的RAG系统 可能对您入门有帮助。
代码
本文涉及的所有代码以及相关资源都已经共享,参见:
为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。
参考
🪐感谢您观看,祝好运🪐