LangChain项目实战

LangChain项目实战

RAG项目实战

课程目标

  • 掌握RAG 工作流程

  • 掌握能够基于 langchain 开发出基础的 RAG 问答系统


RAG 介绍

概述

2023年以来,随着ChatGPT的火爆,使得LLM成为研究和应用的热点,但是市面上大部分LLM都存在一个共同的问题:模型都是基于过去的经验数据进行训练完成,无法获取最新的知识,以及各企业私有的知识。因此很多企业为了处理私有的知识,主要借助以下两种手段来实现

  • 利用企业私有知识,基于开源大模型进行微调

  • 基于LangChain集成向量数据库以及LLM搭建本地知识库的问答(RAG)

RAG(Retrieval-Augmented Generation)检索增强生成,在不改变模型权重的情况下,提升大模型生成能力。用户query 会从知识库中检索出相关文档,大模型依据文档生成用户的 query 回答。这样可以实现低成本提升大模型的回复能力。RAG 核心点在于知识库的构建和检索策略。

RAG 的工作流程图解

RAG 标准流程由索引(Indexing)、检索(Retriever)和生成(Generation)三个核心阶段组成。

  • 索引阶段

    • 加载文件
    • 内容提取
    • 文本分割 ,形成chunk
    • 文本向量化
    • 存向量数据库
  • 检索阶段

    • query向量化
    • 在文本向量中匹配出与问句向量相似的top_k个
  • 生成阶段

    • 匹配出的文本作为上下文和问题一起添加到prompt中
    • 提交给LLM生成答案:

环境准备

安装依赖

  • 首先,确保你的机器安装了conda,创建虚拟环境,pycharm 选择 conda 环境

  • 也可以使用python创建虚拟环境

# 终端查看python的版本
conda create -n rag python=3.12
# 推荐在pycharm中选择uv环境或者Virtualenv创建虚拟环境
  • 紧接着安装项目的依赖
# 安装全部依赖
pip install -r requirements.txt

Qwen在线大模型调用准备

物流行业信息咨询智能问答系统(RAG)

项目需求

本次项目以"某物流行业"为例,基于物流信息构建RAG系统,测试问答效果。 注意:除物流场景外,使用者可以自由切换其他行业类型知识,实现本地知识库问答的效果。

项目思路

  • 离线部分

    • 本地知识文件加载,读取
    • 文本切分
    • 向量化
    • 存向量库
  • 在线部分

    • query 向量化
    • 在文本向量中匹配出与问句向量相似的top_k个
    • 匹配出的文本作为上下文和问题一起添加到prompt中
    • 提交给LLM生成答案

项目代码

项目结构

├── chroma # 向量库
├── data # 数据集
│   └── 物流信息.pdf
├── db.py # 根据语料创建向量库
├── main.py # 运行主函数
├── model.py # 模型文件 包括大模型的和embedding模型
└── web_qa.py # 对话页面

db.py 创建离线知识库

  • PyMuPDFLoader加载pdf文件

  • 对文本进行分割

  • 向量化,得到向量库的对象

  • 创建检索器并返回

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Author  : Vincent
@Time    : 2026/1/4 18:20
@File    : db.py
@Function :
"""
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyMuPDFLoader
# from langchain_chroma import Chroma
from langchain_community.vectorstores import Chroma


def create_db(file_path, embeddings, persist_directory='./chroma'):
    # 第一步:加载文档
    loader = PyMuPDFLoader(file_path)
    docs = loader.load()
    # 第二部:切分文本
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
    split_texts = text_splitter.split_documents(docs)
    print(split_texts)
    # 第三步:将 document通过embeddings对象计算得到向量信息并永久存入Chroma向量数据库,用于后续匹配查询
    vectordb = Chroma.from_documents(
        documents=split_texts,
        embedding=embeddings,
        persist_directory=persist_directory
    )
    retriever = vectordb.as_retriever(search_kwargs={'k': 2})
    return retriever


if __name__ == '__main__':
    from model import embeddings

    retriever = create_db('./data/物流信息.pdf', embeddings)
    print(retriever.invoke("仓库"))

model.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Author  : Vincent
@Time    : 2026/1/4 18:20
@File    : model.py
@Function :
"""
import os

from langchain_community.chat_models.tongyi import ChatTongyi
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_openai import ChatOpenAI

# 初始化 llm 对象
llm = ChatTongyi(
    api_key=os.getenv("API_KEY"),
    model="qwen-flash"
)

# llm = ChatOpenAI(
#     model="qwen-max",
#     api_key=os.getenv("API_KEY"),
#     base_url=os.getenv("BASE_URL"),
# )

# 初始化embeddings 对象
embeddings = DashScopeEmbeddings(
    model="text-embedding-v1",  # text-embedding-v3
    dashscope_api_key=os.getenv("API_KEY")
)

main.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Author  : Vincent
@Time    : 2026/1/4 18:20
@File    : main.py
@Function :
"""
from langchain_core.prompts import PromptTemplate
from db import create_db
from model import llm, embeddings

# 加载向量库
retriever = create_db('./data/物流信息.pdf', embeddings, persist_directory='./chroma')


def get_related_content(related_docs):
    related_content = []
    for doc in related_docs:
        related_content.append(doc.page_content.replace("\n\n", "\n"))
    return "\n".join(related_content)


def define_prompt(query):
    # 1. 检索出query的相关文档
    docs = retriever.invoke(query)
    print("检索出上下文:", f"[{len(docs)}]", docs)
    # 2. 类型转换,把多篇上下文转为字符串
    related_content = get_related_content(docs)

    PROMPT_TEMPLATE = """
        基于以下已知信息,简洁和专业的来回答用户的问题。不允许在答案中添加编造内容。
        已知内容:
        {context}
        问题:
        {question}
        """
    # 3. 提示词模板组装
    prompt = PromptTemplate(
        input_variables=["context", "question"],
        template=PROMPT_TEMPLATE, )
    my_pmt = prompt.format(context=related_content,
                           question=query)
    print(my_pmt)
    return my_pmt


def qa(question):
    my_pmt = define_prompt(question)  # 返回提示词
    result = llm.invoke(my_pmt)
    return result.content


if __name__ == '__main__':
    question = '我们用的什么快递公司?'
    result = qa(question)
    print(result)

运行结果:

web_qa.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
@Author  : Vincent
@Time    : 2026/1/4 18:21
@File    : web_qa.py
@Function :
"""
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from model import llm, embeddings
from db import create_db
import streamlit as st

# 设置标题
st.set_page_config(page_title="物流行业信息咨询系统")
st.title("物流行业信息咨询RAG系统")

# 初始化全局变量
retriever = None


def get_retriever():
    """
    获取或创建检索器(单例模式)
    """
    global retriever
    if retriever is None:
        retriever = create_db('data/物流信息.pdf', embeddings, persist_directory='./chroma')
    return retriever


def format_docs(docs):
    """
    格式化检索到的文档
    """
    return "\n\n".join(doc.page_content for doc in docs)


def create_chain(retriever):
    """
    创建基于 RAG 的问答链(使用 LangChain Expression Language)
    """
    # 定义提示词模板
    template = """基于以下已知信息回答用户的问题。如果你不知道答案,就说你不知道,不要编造信息。

已知信息:
{context}

对话历史:
{chat_history}

问题:{question}

回答:"""

    prompt = ChatPromptTemplate.from_template(template)

    # 构建 RAG 链
    rag_chain = (
            RunnablePassthrough.assign(
                context=lambda x: format_docs(retriever.invoke(x["question"])),
            )
            | prompt
            | llm
            | StrOutputParser()
    )

    return rag_chain


# 主逻辑
def main():
    """
    Streamlit 主页面的交互逻辑。
    """
    # print(f'st.session_state-->{st.session_state}')
    # 初始化会话状态
    if "messages" not in st.session_state:
        st.session_state.messages = []  # 用于保存聊天记录
    # print(f'st.session_state-->{st.session_state}')
    # 展示历史聊天记录
    for message in st.session_state.messages:
        # print(f'message["role"]-->{message["role"]}')
        with st.chat_message(message["role"]):
            st.markdown(message["content"])  # 显示消息内容

    # 接受用户输入
    if prompt := st.chat_input("请输入你的问题:"):
        # 保存用户消息到会话状态
        print(f'prompt--》{prompt}')
        st.session_state.messages.append({"role": "user", "content": prompt})

        # 显示用户输入
        with st.chat_message("user"):
            st.markdown(prompt)

        # 调用模型获取回答
        with st.chat_message("assistant"):
            # 占位符用于显示逐字生成的回答
            message_placeholder = st.empty()
            full_response = ""

            # 获取检索器并创建链
            retriever = get_retriever()
            chain = create_chain(retriever)

            # 准备对话历史
            chat_history_str = ""
            if len(st.session_state.messages) > 1:
                # 构建对话历史字符串
                history_messages = st.session_state.messages[:-1]  # 排除当前最新消息
                for i in range(0, min(len(history_messages), 6), 2):  # 最近 3 轮对话
                    if i + 1 < len(history_messages):
                        user_msg = history_messages[i]
                        ai_msg = history_messages[i + 1]
                        if user_msg["role"] == "user" and ai_msg["role"] == "assistant":
                            chat_history_str += f"用户:{user_msg['content']}\nAI: {ai_msg['content']}\n"

            # 调用链获取答案
            result = chain.invoke({
                "question": prompt,
                "chat_history": chat_history_str
            })
            print(f'result--->{result}')

            assistant_response = result
            message_placeholder.markdown(assistant_response)

            # 保存回答到会话状态
            st.session_state.messages.append({"role": "assistant", "content": assistant_response})


# 运行主逻辑
if __name__ == "__main__":
    main()

streamlit run web_qa.py

小结

本文主要讲述了RAG的流程,并且实现了基础的RAG系统。

需要思考优化的方向:

  1. 如果用户的query很随意,或者query的描述不清楚,应该如何处理?

  2. 如果检索出的内容过长,或者检索的内容相关性不高。相当于引入了大量噪音,可能会降低大模型的回答能力。如何优化检索出的内容相关性?

  3. RAG主要是解决大模型幻觉问题,知识缺乏问题,有什么办法能够从根本上缓解?