本地大模型编程实战(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系统 可能对您入门有帮助。


代码

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

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

参考

🪐感谢您观看,祝好运🪐

相关推荐
LTPP2 分钟前
自动化 Rust 开发的革命性工具:lombok-macros
前端·后端·github
一个热爱生活的普通人2 分钟前
Go语言中 Mutex 的实现原理
后端·go
Victor3562 分钟前
Dubbo(31)如何优化Dubbo的启动速度?
后端
qianmoq3 分钟前
轻松掌握Java多线程 - 第二章:线程的生命周期
java·后端
Postkarte不想说话4 分钟前
FreeSWITCH与FreeSWITCH对接
后端
孔令飞5 分钟前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学5 分钟前
实时系统降低延时的利器
后端·性能优化·go
风象南5 分钟前
Spring Boot 实现文件断点续传
java·spring boot·后端
Cache技术分享5 分钟前
36. Java 控制流语句 Break 语句
前端·后端
极特架构笔记7 分钟前
百万QPS秒杀如何解决超卖少卖问题?(图解+秒懂)
后端