本文将用实际代码演练一个简单的 RAG(Retrieval Augmented Generation,检索增强生成)
系统。
使用
qwen2.5
、deepseek-r1
以及llama3.1
做实验,用shaw/dmeta-embedding-zh
做中文嵌入和检索。
关于 RAG
RAG(Retrieval Augmented Generation,检索增强生成) 是一种 结合检索(Retrieval
)与 生成(Generation)
的AI技术,用于提升 LLM(大语言模型)
的回答质量。其核心原理如下:
- 检索(Retrieval):基于用户查询,从外部知识库(如数据库、向量存储)检索相关信息。
- 增强(Augmentation):将检索到的信息与用户查询一起作为上下文输入给
LLM(大语言模型)
。 - 生成(Generation):
LLM
基于增强后的上下文生成更加准确、丰富的回答。
它的主要用途有:
- 知识问答(QA):为企业文档、医疗、法律等领域提供基于外部知识的智能回答。
- 代码辅助:从代码库中检索相关示例,优化代码生成与解释。
- 个性化推荐:结合用户历史数据,提高对话系统的个性化与专业性。
- 减少幻觉:降低
LLM
生成错误信息的概率,提高可靠性。 RAG
适用于需要结合外部知识提高准确性的任务,是LLM与知识库结合的关键技术。
RAG
适用于需要结合外部知识提高准确性的任务,是 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大模型 。
创建 retriever(检索器)
我们直接使用之前使用 chroma
创建好的本地嵌入数据库,它的数据源是一个 csv 文件,每一行包含了一种动物的信息,例如:
名称,学名,特点,作用
狗,Canis lupus familiaris,忠诚、聪明、社交性强,看家护院、导盲、搜救、警务、情感陪伴
猫,Felis catus,独立、高冷、善于捕鼠,消灭害鼠、陪伴、缓解压力
详细的创建过程可参见:本地大模型编程实战(14)初探智能体Agent(1)
def create_retriever(embed_model_name):
"""创建检索器"""
persist_directory = get_persist_directory(embed_model_name)
db = LocalVectorDBChroma(embed_model_name,persist_directory)
# 基于Chroma 的 vector store 生成 检索器
vector_store = db.get_vector_store()
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 1},
)
return retriever
embed_model_name = "shaw/dmeta-embedding-zh"
retriever = create_retriever(embed_model_name)
def search(query):
"""查询矢量数据库"""
persist_directory = get_persist_directory(embed_model_name)
db = LocalVectorDBChroma(embed_model_name,persist_directory)
vector_store = db.get_vector_store()
results = vector_store.similarity_search_with_score(query,k=1)
return results
设置提示词
这一步主要设置系统提示词,它指导大语言模型如何工作。
system_prompt = """
您是问答任务的助手。
请使用以下**上下文**回答问题。如果您不知道答案,就说您不知道。
最多使用三句话并保持答案简洁。
下面请回答问题。
问题: {question}
上下文: {context}
"""
prompt = ChatPromptTemplate.from_messages([("system", system_prompt)])
RAG
链
下面我们直接用 langchain
的 create_rag_chain
方法来创建 RAG
链,并用不同的大预言模型做一下测试。
def create_rag_chain(llm_model_name):
"""创建RAG链"""
llm = ChatOllama(model=llm_model_name,temperature=0,verbose=True)
rag_chain = (
{
"context": itemgetter("question") | retriever,
"question": itemgetter("question")
}
| prompt
| llm
| StrOutputParser()
)
print(f'json_schema:{rag_chain.input_schema.model_json_schema()}')
return rag_chain
def test_rag_chain(llm_model_name,question):
"""测试 rag 链"""
print(f"------------{llm_model_name}-----------")
rag_chain = create_rag_chain(llm_model_name)
res = rag_chain.invoke({"question":question})
print(res)
def test_rag_chain_stream(llm_model_name,question):
"""测试 rag 链,流式输出"""
print(f"------------{llm_model_name}-----------")
rag_chain = create_rag_chain(llm_model_name)
for chunk in rag_chain.stream({"question":question}):
print(chunk,end="|")
在我们上面定义的上述 RAG
链中:
"context": itemgetter("question") | retriever
的作用是:先用itemgetter
方法获取到用户的question
,然后借助管道标识符|
,调用retriever
查询出一个文档,再将查询结果填充到context
"question": itemgetter("question")
这一步是用itemgetter
方法获取到用户的question
,然后填充到question
经过上述两步后,我们之前定义的 system_prompt
中的 {question}
和 {context}
变量就有实际内容了,这些内容将提供给大语言模型,大语言模型就能根据它给出回答了。
我们下面给大语言模型提出一个问题,看看 qwen2.5
、 deepseek-r1
以及 llama3.1
的实际表现:
query = "猪的学名是什么?它对人类有什么用处?"
test_rag_chain |
test_rag_chain_stream |
|
---|---|---|
qwen2.5 |
猪的学名是Sus scrofa domesticus。猪主要作为肉类来源,并且在生物医学研究中也有应用,例如猪心脏瓣膜移植。 | 猪-的-学-名-是-Sus- sc-ro-fa- domestic-us-。-猪-主要-作为-肉类-来源-,并-且-在-生物-医学-研究-中-也有-应用-,-例如-猪-心脏-瓣-膜-移植-。 |
deepseek-r1 |
猪的学名是Sus scrofa domesticus。它主要作为肉类食用,同时在生物医学中用于心脏瓣膜移植研究。 | 猪-的-学-名-是-Sus- sc-ro-fa- domestic-us-。-它-主要-作为-肉类-食用-,-同时-在-生物-医学-中-用于-心脏-瓣-膜-移植-研究-。 |
llama3.1 |
无内容 | 无内容 |
不难发现,qwen2.5
和 deepseek-r1
都很好的做了回答,但是 llama3.1
没有回复任何内容。
用 Agent(智能体)
实现 RAG
我们上面用 RAG链
已经可以处理用户问题了,不过目前并不清楚是否真的通过 retriever(检索器)
查询了矢量知识库,为此,我们再创建一个智能体来达到目的。
def create_agent(llm_model_name):
"""生成智能体"""
rag_chain = create_rag_chain(llm_model_name)
rag_tool = rag_chain.as_tool(
name="animal_expert",
description="获取有关动物的信息。",
)
llm = ChatOllama(model=llm_model_name,temperature=0,verbose=True)
agent = create_react_agent(llm, [rag_tool])
return agent
def test_agent(llm_model_name,question):
agent = create_agent(llm_model_name)
for chunk in agent.stream(
{"messages": [("human",question)]}
):
print(chunk)
print("----")
上述代码通过 langchain
提供的 create_react_agent
方法创建了一个 reAct(Reasoning + Acting)
智能体。
我们再提一个问题:
query = "蜜蜂的特点是什么?它对人类社会有什么作用?"
使用 qwen2.5
做实验:
{'agent': {'messages': [AIMessage(content='', ..., response_metadata={'model': 'qwen2.5', 'created_at': ...}, id=..., tool_calls=[{'name': 'animal_expert', 'args': {'question': '蜜蜂的特点是什么?它对人类社会有什么作用?'}, 'id': ..., 'type': 'tool_call'}], ...)]}}
{'tools': {'messages': [ToolMessage(content='蜜蜂的特点是勤劳且具有复杂的社会结构。它们对人类社会的作用主要体现在授粉、蜂蜜生产和提供蜂胶及蜂毒疗法等健康益处上。', name='animal_expert', id=..., tool_call_id=...)]}}
{'agent': {'messages': [AIMessage(content='蜜蜂是一种非常勤劳的昆虫,拥有复杂的社会结构。每只蜜蜂在蜂巢中都有其特定的角色和职责。\n\n蜜蜂对人类社会有着重要的作用:\n1. **授粉**:蜜蜂是自然界中最主要的传粉者之
一,对于维持生态平衡、保护生物多样性以及促进农作物产量具有重要作用。\n2. **蜂蜜生产**:通过采集花蜜酿造蜂蜜,不仅为人们提供了天然甜味剂和营养丰富的食品,还促进了相关产业的发展。\n3. **蜂胶及蜂毒疗法**:蜜蜂分 泌的蜂胶和蜂毒在传统医学中被用于治疗各种疾病,具有一定的药用价值。\n\n总之,蜜蜂对人类社会有着不可替代的作用。', additional_kwargs={}, response_metadata={'model': 'qwen2.5', ...}]}}
通过上述回答,我们能看到:智能体第1步生成了 tool_call
,第2步查询了知识库,它返回的信息在僵硬的 csv 内容基础上做了润色,第3步则根据上一步的知识,返回了更多人性化的有价值的信息。 可见,在这个场景中:智能体的能力明显高于RAG链 。
llama3.1
生成的 tool_call 的参数是乱码,这可能是它返回内容为空的原因;而deepseek-r1
不支持 agent.stream 方法。
总结
我们使用 langchain
的 create_rag_chain
方法创建了 RAG链
,又使用 create_react_agent
方法创建了智能体。
在本文的 RAG
场景中,智能体的表现更优秀,reAct agent(智能体) 可能是实现大多数 RAG
系统的更好途径。
如果您想自己实现一个包含前端和后端的
RAG
系统,从零搭建langchain+本地大模型+本地矢量数据库的RAG系统 可能对您入门有帮助。
代码
本文涉及的所有代码以及相关资源都已经共享,参见:
为便于找到代码,程序文件名称最前面的编号与本系列文章的文档编号相同。
🪐感谢您观看,祝好运🪐