本文将使用 langgraph 实现一个简单的 RAG(Retrieval Augmented Generation,检索增强生成) 系统。

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

关于 RAG

RAG(Retrieval Augmented Generation,检索增强生成) 是一种 结合检索(Retrieval)与 生成(Generation) 的AI技术,用于提升 LLM(大语言模型) 的回答质量。
典型的 RAG 应用程序有两个主要组件:

  • 索引:从源中提取数据并对其进行索引。这通常是离线进行的。
  • 检索和生成:它在运行时接受用户查询并从索引中检索相关数据,然后将其传递给模型。

索引

  • 加载:首先,我们需要加载数据。这可以通过文档加载器完成。langchain 提供了 html、csv、pdf 等诸多加载器。
  • 拆分:文本拆分器将大型文档拆分成较小的块。这对于索引数据和将其传递到模型都很有用,因为大块数据更难搜索,并且不适合模型的有限上下文窗口。
  • 存储:我们需要一个地方来存储和索引我们的拆分,以便以后可以搜索它们。这通常使用 VectorStore 和 Embeddings 模型来完成。

索引过程

检索和生成

  • 检索:给定用户输入,使用检索器从存储中检索相关分割后的文档。
  • 生成:ChatModel/LLM 使用包含问题和检索到的数据的提示词生成答案。

检索和生成过程

准备

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

  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大模型

创建矢量数据库对象

我们直接使用之前使用 chroma 创建好的本地嵌入数据库,它的数据源是一个 csv 文件,每一行包含了一种动物的信息,例如:

名称,学名,特点,作用
狗,Canis lupus familiaris,忠诚、聪明、社交性强,看家护院、导盲、搜救、警务、情感陪伴
猫,Felis catus,独立、高冷、善于捕鼠,消灭害鼠、陪伴、缓解压力

详细的创建过程可参见:本地大模型编程实战(14)初探智能体Agent(1)

embed_model_name = "shaw/dmeta-embedding-zh"
vector_store = Chroma(persist_directory=get_persist_directory(embed_model_name),embedding_function=OllamaEmbeddings(model=embed_model_name))

设置提示词

这一步主要设置系统提示词,它指导大语言模型如何工作。

prompt = ChatPromptTemplate.from_messages([
    ("human", """你是问答任务的助手。
     请使用以下检索到的**上下文**来回答问题。
     如果你不知道答案,就说你不知道。最多使用三句话,并保持答案简洁。
     
     问题: {question} 

     上下文: {context} 

     回答:"""),
])

使用 langgraph 构建 RAG 系统

我们将使用 LangGraph 将检索和生成步骤整合到一个应用程序中。要实现这个应用程序,需要完成三件事:

  • 应用程序的状态;
  • 应用程序的节点(即应用程序步骤);
  • 应用程序的“控制流”(即节点的执行顺序)。

状态

应用程序的状态控制着哪些数据输入到应用程序、在步骤之间传输以及应用程序输出。它通常是 TypedDict,但也可以是 Pydantic BaseModel。
对于一个简单的 RAG 应用程序,我们只需跟踪输入的问题、检索到的上下文和生成的答案:

class State(TypedDict):
    """状态:在 langgraph 中传递"""

    question: str
    context: List[Document]
    answer: str

节点

定义由两个步骤的简单序列:检索和生成:

def retrieve(state: State):
    """节点:检索"""
    
    retrieved_docs = vector_store.similarity_search(state["question"],k=2)
    return {"context": retrieved_docs}

检索步骤只是使用输入问题运行相似性搜索。

def generate(state: State):
    """节点:生成 """

    llm = ChatOllama(model=llm_model_name,temperature=0, verbose=True)
    docs_content = "\n\n".join(doc.page_content for doc in state["context"])
    messages = prompt.invoke({"question": state["question"], "context": docs_content})
    response = llm.invoke(messages)
    return {"answer": response.content}

生成步骤将检索到的上下文和原始问题格式化为聊天模型的提示词。

控制流

现在,我们可以用控制流把节点连接起来,RAG 系统就可以工作了:

def build_graph(llm_model_name):
    """构建langgraph"""

    # 定义步骤
    graph_builder = StateGraph(State).add_sequence([retrieve, generate])
    graph_builder.add_edge(START, "retrieve")
    graph = graph_builder.compile()

    return graph

我们可以用 pillow 包绘制一下这个应用程序的流程图,直观的感受一下这个 LangGraph 应用:

from utils import show_graph
show_graph(graph)

控制流

见证效果

现在可以定义一个函数接受用户的询问了:

def ask(llm_model_name,question):
    """提问"""

    print(f'--------{llm_model_name}----------')
    graph = build_graph(llm_model_name)
    response = graph.invoke({"question": question})
    print(f'the answer is: \n{response["answer"]}')

下面是问题:

question = "大象的学名是什么?它有什么显著特点?对人类有什么帮助?"

我们来看看各大模型给出的回答:

  • qwen2.5
大象的学名为Elephas spp.。它具有极高的智商和善于记忆的特点。大象在文化中象征着神圣,曾用于古代战争,并可用于运输和旅游业。
  • deepseek-r1
<think>
好的,我现在需要回答关于大象的学名、显著特点以及对人类有什么帮助的问题。首先,根据提供的上下文,大象的学名是Elephas spp.。

...

我需要将这些信息组织成一个简洁的回答,最多使用三句话,并且避免使用复杂的结构。同时,要确保回答准确无误,不遗漏任何关键点。因此,我会先列出学名,然后描述特点,最后说明作用。
</think>

大象的学名是Elephas spp.,它们具有极高的智商和良好的记忆能力。大象不仅在文化上被视为神圣动物,还曾作为古代战争中的战象,对运输、旅游也有重要作用。
  • llama3.1
大象的学名是Elephas spp.。它具有极高的智商和记忆力,与人类有复杂的关系。大象对人类有帮助,例如作为文化象征、古代战象、运输工具和旅游景点。

三个大模型都做出了妥善的回答!

在各个节点内的代码上增加断点,就可以跟踪节点的执行情况。

总结

我们使用 langgraph 构建了 RAG链 ,与使用传统的 langchain 构建的链相比,它的代码多了一些,但是也带来了以下好处:

  • 可视化程度高:容易理解和跟踪细节;
  • 兼容性强:上述三种大模型均可顺畅运行,而使用 langchain 构建的应用程序则有时候 deepseek-r1 跑不起来;
  • 易定制:可以比较容易的修改各个节点的执行细节,也可以随意增删节点。

如果您想自己实现一个包含前端和后端的 RAG 系统,从零搭建langchain+本地大模型+本地矢量数据库的RAG系统 可能对您入门有帮助。


代码

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

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

参考

🪐感谢您观看,祝好运🪐