本文将使用 langgraph
实现一个简单的 RAG(Retrieval Augmented Generation,检索增强生成)
系统。
使用
qwen2.5
、deepseek-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 使用包含问题和检索到的数据的提示词生成答案。
准备
在正式开始撸代码之前,需要准备一下编程环境。
-
计算机
本文涉及的所有代码可以在没有显存的环境中执行。 我使用的机器配置为:- 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大模型 。
创建矢量数据库对象
我们直接使用之前使用 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系统 可能对您入门有帮助。
代码
本文涉及的所有代码以及相关资源都已经共享,参见:
为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。
参考
🪐感谢您观看,祝好运🪐