本文将用实际代码演练一个简单的 RAG(Retrieval Augmented Generation,检索增强生成) 系统。

使用 qwen2.5deepseek-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 与知识库结合的关键技术。

准备

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

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

创建 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

下面我们直接用 langchaincreate_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.5deepseek-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.5deepseek-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 方法。

总结

我们使用 langchaincreate_rag_chain 方法创建了 RAG链 ,又使用 create_react_agent 方法创建了智能体。
在本文的 RAG 场景中,智能体的表现更优秀,reAct agent(智能体) 可能是实现大多数 RAG 系统的更好途径

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

代码

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

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

🪐感谢您观看,祝好运🪐