本地大模型编程实战(20)用langgraph和智能体实现RAG(Retrieval Augmented Generation,检索增强生成)(4)

上一篇文章我们演练了一个 langgraph 实现的 RAG(Retrieval Augmented Generation,检索增强生成) 系统。本文将要在此基础上,增加自动记录聊天历史的功能,另外,我们还将使用一个 Agent(智能体) 来实现几乎同样的功能,我们来一起体会一下用 langgraphAgent(智能体) 实现 RAG系统 的区别。

  • 本次构建的 LangGraph 链结构如下图:

如上图,query_or_respond 是一个条件节点,它通过能否根据用户的问题生成 工具调用(tool_calls) ,来判断是否需要检索矢量知识库:如果 工具调用 为空,则直接由大语言模型处理;否则通过 工具调用 调用 tools 进行检索。

  • 实现类似功能的智能体结构如下图:

我们可以直观的发现:Agent(智能体) 实现更加简单

使用 qwen2.5deepseek 以及 llama3.1 做实验,用 shaw/dmeta-embedding-zh 做中文嵌入和检索。

准备

在正式开始撸代码之前,需要准备一下编程环境。

  1. 计算机

    本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:

    • CPU: Intel i5-8400 2.80GHz
    • 内存: 16GB
  2. Visual Studio Code 和 venv 这是很受欢迎的开发工具,相关文章的代码可以在 Visual Studio Code 中开发和调试。 我们用 pythonvenv 创建虚拟环境, 详见:
    在Visual Studio Code中配置venv

  3. Ollama 在 Ollama 平台上部署本地大模型非常方便,基于此平台,我们可以让 langchain 使用 llama3.1qwen2.5deepseek 等各种本地大模型。详见:
    在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系统 可能对您入门有帮助。


代码

本文涉及的所有代码以及相关资源都已经共享,参见:

为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。

参考

🪐感谢您观看,祝好运🪐

相关推荐
k layc1 分钟前
【论文解读】《Training Large Language Models to Reason in a Continuous Latent Space》
人工智能·python·机器学习·语言模型·自然语言处理·大模型推理
代码猪猪傻瓜coding10 分钟前
【模块】 ASFF 模块
人工智能·深度学习
muxue17810 分钟前
go:运行第一个go语言程序
开发语言·后端·golang
米饭好好吃.11 分钟前
【Go】Go wire 依赖注入
开发语言·后端·golang
闲猫12 分钟前
go 接口interface func (m Market) getName() string {
开发语言·后端·golang
Good Note12 分钟前
Golang的静态强类型、编译型、并发型
java·数据库·redis·后端·mysql·面试·golang
可爱de艺艺12 分钟前
Go入门之struct
开发语言·后端·golang
信徒_15 分钟前
Go 语言中的协程
开发语言·后端·golang
阿正的梦工坊16 分钟前
Sliding Window Attention(滑动窗口注意力)解析: Pytorch实现并结合全局注意力(Global Attention )
人工智能·pytorch·python
m0_7482365828 分钟前
跟据spring boot版本,查看对应的tomcat,并查看可支持的tomcat的版本范围
spring boot·后端·tomcat