1. 背景介绍

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

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

本次项目以"某物流行业"为例,基于物流信息构建RAG系统,测试问答效果

注意:除物流场景外,使用者可以自由切换其他行业类型知识,实现本地知识库问答的效果

2. 项目基本原理

RAG原理

项目流程

项目原理

主要功能:

  • 基于本地知识库的问答:系统可以根据用户的提问,在本地的知识库中进行搜索,并返回相关的答案
  • 多模型支持: 项目支持使用不同的语言模型(Embeding模型:做检索, LLMs: 大模型出结果),可以根据需求选择合适的模型进行使用(进行微调)
  • 离线私有化: 可以将该问答系统部署在本地环境中,确保数据的安全性和隐私性

3. 项目流程

4. 环境配置

5. 代码实现

代码目录如下:

物流信息.pdf信息如下:

5.1 本地知识库构建

目的: 将本地pdf文档信息进行抽取,然后进行分块,最后Embedding存储向量数据库中

代码local_db.py如下:

        代码实现了一个基于 LangChain 框架的 RAG(检索增强生成)基础流程,主要功能是将本地的 PDF 文档解析、切分、向量化,并最终存入本地的 FAISS 向量数据库中

from langchain_community.document_loaders import PyMuPDFLoader # 用于加载和读取本地的 PDF 文件
from langchain.text_splitter import RecursiveCharacterTextSplitter # 用于将长文本按规则递归切分成较小的文本块(chunks)
from langchain_community.embeddings import OllamaEmbeddings # 用于调用本地 Ollama 服务中的嵌入模型,将文本转换为向量
from langchain_community.vectorstores import FAISS  # Meta 开源的本地向量数据库,用于存储向量并进行相似度检索


def get_vector():
    # 第一步:加载文档
    loader = PyMuPDFLoader("物流信息.pdf")
    # 将文本转成 Document 对象
    # 调用 load() 方法将 PDF 内容转化为 LangChain 的 Document 对象列表(通常一页对应一个 Document)
    data = loader.load()
    print(f'data-->{data}')
    print(f'len(data):{len(data)}')

    # # 第二步:切分文本
    # 初始化文本分割器 RecursiveCharacterTextSplitter,设置了两个关键参数:
    #   chunk_size=50:每个文本块的最大长度为 50 个字符。
    #   chunk_overlap=20:相邻文本块之间有 20 个字符的重叠,以保证上下文语义的连贯性
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
    # 切割加载的 document
    # 调用 split_documents(data) 对加载的文档进行切分,并打印切分后的文本块数量和具体内容
    split_docs = text_splitter.split_documents(data)
    print("split_docs size:", len(split_docs))
    print(split_docs)
    #
    # # 第三步:初始化 hugginFace 的 embeddings 对象
    # 实例化 OllamaEmbeddings,指定使用 "mxbai-embed-large" 这个嵌入模型。该模型负责将文本转化为计算机可理解的高维向量表示
    embeddings = OllamaEmbeddings(model="mxbai-embed-large")
    #
    #
    # # 第四步:将 document通过embeddings对象计算得到向量信息并永久存入FAISS向量数据库,并构建 FAISS 向量索引,用于后续匹配查询
    db = FAISS.from_documents(split_docs, embeddings)
    db.save_local("./faiss/wuliu")

if __name__ == '__main__':
    result = get_vector()

执行一下, 如果报错: 

raise ImportError(
ImportError: `PyMuPDF` package not found, please install it with `pip install pymupdf`

则安装pip install pymupdf即可

如果报 没有发现 mxbai-embed-large模型错误, 则使用ollama安装即可: ollama pull mxbai-embed-large

显示结果如下:

data-->[Document(metadata={'source': '物流信息.pdf', 'file_path': '物流信息.pdf', 'page': 0, 'total_pages': 1, 'format': 'PDF 1.3', 'title': '物流信息', 'author': '', 'subject': '', 'keywords': '', 'creator': 'typora', 'producer': 'macOS 版本10.16(版号21E230) Quartz PDFContext', 'creationDate': "D:20240604062031Z00'00'", 'modDate': "D:20240604062031Z00'00'", 'trapped': ''}, page_content='物流公司:速达物流 公司总部:北京市 业务范围:国际快递、仓储管理\n货物追踪:\n货物编号:ABC123456\n发货⽇期:2023-01-15\n当前位置:上海分拨中⼼\n预计到达⽇期:2023-01-20\n运输⽅式:\n运输公司:快运通\n运输⽅式:陆运\n出发地:⼴州\n⽬的地:重庆\n预计运输时间:3天\n仓储信息:\n仓库名称:东⽅仓储中⼼\n仓库位置:深圳市\n存储货物类型:电⼦产品\n存储条件:常温仓储\n当前库存量:1000件\n')]
len(data):1
split_docs size: 6
[Document(metadata={'source': '物流信息.pdf', 'file_path': '物流信息.pdf', 'page': 0, 'total_pages': 1, 'format': 'PDF 1.3', 'title': '物流信息', 'author': '', 'subject': '', 'keywords': '', 'creator': 'typora', 'producer': 'macOS 版本10.16(版号21E230) Quartz PDFContext', 'creationDate': "D:20240604062031Z00'00'", 'modDate': "D:20240604062031Z00'00'", 'trapped': ''}, page_content='物流公司:速达物流 公司总部:北京市 业务范围:国际快递、仓储管理\n货物追踪:'), Document(metadata={'source': '物流信息.pdf', 'file_path': '物流信息.pdf', 'page': 0, 'total_pages': 1, 'format': 'PDF 1.3', 'title': '物流信息', 'author': '', 'subject': '', 'keywords': '', 'creator': 'typora', 'producer': 'macOS 版本10.16(版号21E230) Quartz PDFContext', 'creationDate': "D:20240604062031Z00'00'", 'modDate': "D:20240604062031Z00'00'", 'trapped': ''}, page_content='货物追踪:\n货物编号:ABC123456\n发货⽇期:2023-01-15\n当前位置:上海分拨中⼼'), Document(metadata={'source': '物流信息.pdf', 'file_path': '物流信息.pdf', 'page': 0, 'total_pages': 1, 'format': 'PDF 1.3', 'title': '物流信息', 'author': '', 'subject': '', 'keywords': '', 'creator': 'typora', 'producer': 'macOS 版本10.16(版号21E230) Quartz PDFContext', 'creationDate': "D:20240604062031Z00'00'", 'modDate': "D:20240604062031Z00'00'", 'trapped': ''}, page_content='当前位置:上海分拨中⼼\n预计到达⽇期:2023-01-20\n运输⽅式:\n运输公司:快运通'), Document(metadata={'source': '物流信息.pdf', 'file_path': '物流信息.pdf', 'page': 0, 'total_pages': 1, 'format': 'PDF 1.3', 'title': '物流信息', 'author': '', 'subject': '', 'keywords': '', 'creator': 'typora', 'producer': 'macOS 版本10.16(版号21E230) Quartz PDFContext', 'creationDate': "D:20240604062031Z00'00'", 'modDate': "D:20240604062031Z00'00'", 'trapped': ''}, page_content='运输⽅式:\n运输公司:快运通\n运输⽅式:陆运\n出发地:⼴州\n⽬的地:重庆\n预计运输时间:3天'), Document(metadata={'source': '物流信息.pdf', 'file_path': '物流信息.pdf', 'page': 0, 'total_pages': 1, 'format': 'PDF 1.3', 'title': '物流信息', 'author': '', 'subject': '', 'keywords': '', 'creator': 'typora', 'producer': 'macOS 版本10.16(版号21E230) Quartz PDFContext', 'creationDate': "D:20240604062031Z00'00'", 'modDate': "D:20240604062031Z00'00'", 'trapped': ''}, page_content='⽬的地:重庆\n预计运输时间:3天\n仓储信息:\n仓库名称:东⽅仓储中⼼\n仓库位置:深圳市'), Document(metadata={'source': '物流信息.pdf', 'file_path': '物流信息.pdf', 'page': 0, 'total_pages': 1, 'format': 'PDF 1.3', 'title': '物流信息', 'author': '', 'subject': '', 'keywords': '', 'creator': 'typora', 'producer': 'macOS 版本10.16(版号21E230) Quartz PDFContext', 'creationDate': "D:20240604062031Z00'00'", 'modDate': "D:20240604062031Z00'00'", 'trapped': ''}, page_content='仓库位置:深圳市\n存储货物类型:电⼦产品\n存储条件:常温仓储\n当前库存量:1000件')]

优化建议:
代码中设置的 chunk_size=50 相对较小,这可能会导致切分出的文本块丢失完整的上下文语义,进而影响后续检索的准确度。对于中文文档,通常建议将 chunk_size 设置在 500 到 800 之间,并将 chunk_overlap 设置在 50 到 100 之间,这样能在检索精度和上下文完整性之间取得更好的平衡

5.2 构建本地问答RAG系统

目的: 基于本地实现RAG系统的检索问答
具体local_qa. py代码:

        代码实现了一个基于 LangChain 框架的 RAG(检索增强生成)问答流程的后半部分:从本地加载向量库、检索相关文档、构建 Prompt 并调用大模型生成答案

# coding:utf-8
# 导入必备的工具包
import time
from local_db import * # 从自定义模块 local_db 导入了 FAISS
from langchain import PromptTemplate
from langchain_community.llms import Ollama

# 加载FAISS向量库
# 初始化了 OllamaEmbeddings 嵌入模型(注意:这里指定了 mxbai-embed-large作为嵌入模型,
# 通常嵌入模型应该是专门的 embedding 模型如 mxbai-embed-large
embeddings = OllamaEmbeddings(model="mxbai-embed-large", temperature=0)
# 使用 FAISS.load_local 加载了之前保存的向量库,
# 并开启了 allow_dangerous_deserialization=True(这是新版 LangChain 加载本地 FAISS 必须的参数
db = FAISS.load_local("faiss/wuliu", embeddings, allow_dangerous_deserialization=True)
# db = FAISS.load_local("faiss/wuliu", embeddings)

start_time = time.time()


def get_related_content(related_docs):
    """
    文档检索与内容提取
    :param related_docs: 接收检索到的文档列表 related_doc
    :return:
    """
    # print(f'related_docs--》{related_docs}')
    related_content = []
    for doc in related_docs: # 遍历文档,提取 page_content 并用 \n 替换掉 \n\n(为了压缩 Prompt 长度)
        # print(f'doc.page_content--》{doc.page_content}')
        related_content.append(doc.page_content.replace("\n\n", "\n"))
    # print(f'related_content列表状态--》{related_content}')
    # 将所有相关文档的内容拼接成一个完整的字符串返回
    return "\n".join(related_content)


def define_prompt():
    # 硬编码了一个测试问题:'我的快递出发地是哪?预计几天的时间到达?'
    question = '我的快递出发地是哪?预计几天的时间到达?'
    # 调用 db.similarity_search(question, k=2) 从向量库中检索出与问题最相似的 2 个文本块
    docs = db.similarity_search(question, k=2)
    #
    related_content = get_related_content(docs)
    # print('*' * 80)
    # print(f'related_content字符串状态-->{related_content}')
    # print('*'*80)
    # 定义了 Prompt 模板,要求模型“基于已知信息回答,不允许编造”
    PROMPT_TEMPLATE = """
        基于以下已知信息,简洁和专业的来回答用户的问题。不允许在答案中添加编造成分。
        已知内容:
        {context}
        问题:
        {question}"""

    prompt = PromptTemplate(
        input_variables=["context", "question"],
        template=PROMPT_TEMPLATE, )

    my_pmt = prompt.format(context=related_content, question=question)

    return my_pmt


def qa():
    model = Ollama(model="qwen2.5:7b")
    # print(f'model-->{model}')
    my_pmt = define_prompt()
    print(f'my_pmt--》{my_pmt}')
    result = model.invoke(my_pmt)
    return result


if __name__ == '__main__':
    result = qa()
    print(result)
    end_time = time.time()
    print(end_time - start_time)

优化建议:

  1. 取消注释并获取流式/完整响应: 将 qa() 中的 result = model.invoke(my_pmt) 取消注释,并确保函数有 return result
  2. Embedding 模型与 LLM 模型区分: 代码中 OllamaEmbeddings(model="qwen2.5:7b") 可能是个笔误。qwen2.5:7b 是对话模型,不是嵌入模型。建议改回你之前构建向量库时使用的 mxbai-embed-large,否则加载向量库时可能会因为向量维度不匹配而报错。
  3. 使用 LangChain 的 RAG Chain 简化代码: 手动拼接 Prompt 和检索文档比较繁琐,可以使用 LangChain 的 create_retrieval_chain 自动完成“检索 -> 填充上下文 -> 生成答案”的全流程

5.3 构建web界面实现 RAG检索

代码实现了一个基于 Streamlit 和 LangChain 的交互式 RAG 问答界面,并使用了 ConversationalRetrievalChain 来支持多轮对话(记忆历史)

web_qa.py代码如下:

# 从自定义模块 local_qa 中导入所有必要的组件(如大模型实例、向量数据库 db 等)
from local_qa import *
# 从 LangChain 导入 ConversationalRetrievalChain,该链专门用于处理带有历史上下文的检索问答
# ConversationalRetrievalChain作用:自动保存历史对话信息
from langchain.chains import ConversationalRetrievalChain
# 导入 Streamlit 库,用于快速构建 Web 交互界面
import streamlit as st

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

# 初始化全局变量, 用于存储对话历史
# 注意:在 Streamlit 中,每次页面交互都会重新运行整个脚本,普通全局变量无法跨次交互保留状态
chat_history = []


# 定义检索链函数
def new_retrival():
    """
    创建基于 ConversationalRetrievalChain 的问答链
    该链会自动处理:检索相关文档 -> 将文档和历史对话作为上下文 -> 传给大模型生成答案
    """
    chain = ConversationalRetrievalChain.from_llm(
        llm=Ollama(model="qwen2.5:7b"),  # 使用本地大模型
        retriever=db.as_retriever()  # 基于本地数据库的检索器
    )
    return chain


# 主逻辑
def main():
    """
    Streamlit 主页面的交互逻辑, 负责界面渲染和状态管理
    """
    # print(f'st.session_state-->{st.session_state}')
    # 初始化会话状态
    # 初始化 Streamlit 的会话状态(Session State)
    # 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"]}')
        # 根据消息角色(user 或 assistant)创建对应的聊天气泡
        with st.chat_message(message["role"]):
            st.markdown(message["content"])  # 将消息内容以 Markdown 格式渲染显示

    # 接受用户输入
    # prompt = st.chat_input("请输入你的问题:")
    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 = ""

            # 调用检索链获取答案: 每次提问时重新创建检索链
            chain = new_retrival()
            # 调用检索链,传入当前问题和对话历史,获取包含答案的字典结果
            result = chain.invoke({"question": prompt, "chat_history": chat_history})
            print(f'result--->{result}')
            # 将当前的问答对追加到全局的 chat_history 列表中,以维持多轮对话记忆
            chat_history.append((prompt, result["answer"]))  # 更新聊天历史
            # 提取大模型生成的最终答案
            assistant_response = result["answer"]
            # 将答案渲染到占位符中
            message_placeholder.markdown(assistant_response)

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


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

在终端运行 streamlit run .\web_qa.py 即可,如果报错:

streamlit : 无法将“streamlit”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ streamlit run .\web_qa.py
+ ~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (streamlit:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

说明没有安装streamlit插件, 使用命令 pip install streamlit 安装即可

其他大模型使用:

from PyPDF2 import PdfReader # 读取pdf文件
from langchain.text_splitter import CharacterTextSplitter # 文本分割器
from langchain.vectorstores import FAISS # 向量库
from langchain.llms import QianfanLLMEndpoint # 千帆大模型平台库
import streamlit as st # 搭建web界面
from langchain.chains import ConversationalRetrievalChain # 对话检索链
from config import * # config文件配置API_key等参数
import os
from langchain.embeddings import QianfanEmbeddingsEndpoint
# 使用国外OPENAI的模型,需要导入API-KEY(需要科学上网)
# os.environ[ "OPENAI_API_KEY" ] = OPENAI_AK
# 使用国内百度千帆平台的模型,需要导入API-KEY和SERECT-KEY
os.environ['QIANFAN_AK'] = QIANFAN_AK
os.environ['QIANFAN_SK'] = QIANFAN_SK
llm = QianfanLLMEndpoint()
# 设置web页面:比如标题、描述功能
st.title("《农学院--物流行业信息咨询》")
st.write("请上传一个关于物流信息的pdf文档.")
# 设置上传pdf文件的功能
uploaded_file = st.file_uploader("选择一个pdf文档", type="pdf")
print('uploaded_file', uploaded_file)
if uploaded_file:
# 读取pdf文件
    doc_reader = PdfReader(uploaded_file)
    print(f'doc_reader--》{doc_reader}')
# 从pdf中提取文档
    raw_text = ""
    for i, page in enumerate(doc_reader.pages):
        print(f'page-->{page}')
        text = page.extract_text()
        print(f'text-->{text}')
        if text:
            raw_text += text
    #将文本切分成小的模块
    print(raw_text)
    print('*'*80)
    text_splitter = CharacterTextSplitter(separator=' ', chunk_size=10, chunk_overlap=1)

    texts = text_splitter.split_text(raw_text)
    print(f'texts-->{texts}')
    print(f'len(texts)-->{len(texts[0])}')
    print('*'*80)
    # embeddings模型
    # EMBEDDING_MODEL = "/Users/ligang/PycharmProjects/llm/langchain_apply/Knowledge_QA/moka-ai/m3e-base"
    # embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
    embeddings = QianfanEmbeddingsEndpoint()
    # print(embeddings.embed_query("你好吗"))
    # 创建文档搜索
    docsearch = FAISS.from_texts(texts, embeddings)
    # 创建对话链
    qa = ConversationalRetrievalChain.from_llm(
                        llm=llm,
                        retriever=docsearch.as_retriever(),
                        return_source_documents=True, )
                        # 初始化聊天记录列表
    chat_history = []
                        # 获取用户的查询
    query = st.text_input("请给出你的问题")
    #添加一个生成按钮
    generate_button = st.button("生成答案")
    if generate_button and query:
        with st.spinner("答案生成中..."):
        # 将问题以及历史对话记录传入对话链获得模型输出结果
            result = qa({"question": query, "chat_history": chat_history})
            answer = result["answer"]
            source_documents = result['source_documents']
            #将答案和source_documents合并为单个响应(输出)
            response = { "answer": answer, "source_documents": source_documents
            }
            st.write("response:", response)

5.4 结果展示

终端运行  streamlit run .\web_qa.py:

5.5 整体代码如下

(1).local_db.py
from langchain_community.document_loaders import PyMuPDFLoader # 用于加载和读取本地的 PDF 文件
from langchain.text_splitter import RecursiveCharacterTextSplitter # 用于将长文本按规则递归切分成较小的文本块(chunks)
from langchain_community.embeddings import OllamaEmbeddings # 用于调用本地 Ollama 服务中的嵌入模型,将文本转换为向量
from langchain_community.vectorstores import FAISS  # Meta 开源的本地向量数据库,用于存储向量并进行相似度检索


def get_vector():
    # 第一步:加载文档
    loader = PyMuPDFLoader("物流信息.pdf")
    # 将文本转成 Document 对象
    # 调用 load() 方法将 PDF 内容转化为 LangChain 的 Document 对象列表(通常一页对应一个 Document)
    data = loader.load()
    print(f'data-->{data}')
    print(f'len(data):{len(data)}')

    # # 第二步:切分文本
    # 初始化文本分割器 RecursiveCharacterTextSplitter,设置了两个关键参数:
    #   chunk_size=50:每个文本块的最大长度为 50 个字符。
    #   chunk_overlap=20:相邻文本块之间有 20 个字符的重叠,以保证上下文语义的连贯性
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
    # 切割加载的 document
    # 调用 split_documents(data) 对加载的文档进行切分,并打印切分后的文本块数量和具体内容
    split_docs = text_splitter.split_documents(data)
    print("split_docs size:", len(split_docs))
    print(split_docs)
    #
    # # 第三步:初始化 hugginFace 的 embeddings 对象
    # 实例化 OllamaEmbeddings,指定使用 "qwen2.5:7b" 这个嵌入模型。该模型负责将文本转化为计算机可理解的高维向量表示
    embeddings = OllamaEmbeddings(model="mxbai-embed-large")
    #
    #
    # # 第四步:将 document通过embeddings对象计算得到向量信息并永久存入FAISS向量数据库,并构建 FAISS 向量索引,用于后续匹配查询
    db = FAISS.from_documents(split_docs, embeddings)
    db.save_local("./faiss/wuliu")

if __name__ == '__main__':
    result = get_vector()
(2).local_qa.py
# coding:utf-8
# 导入必备的工具包
import time
from local_db import * # 从自定义模块 local_db 导入了 FAISS
from langchain_core.prompts import PromptTemplate
from langchain_community.llms import Ollama

# 加载FAISS向量库
# 初始化了 OllamaEmbeddings 嵌入模型(注意:这里指定了 mxbai-embed-large 作为嵌入模型,
# 通常嵌入模型应该是专门的 embedding 模型如 mxbai-embed-large
embeddings = OllamaEmbeddings(model="mxbai-embed-large", temperature=0)
# 使用 FAISS.load_local 加载了之前保存的向量库,
# 并开启了 allow_dangerous_deserialization=True(这是新版 LangChain 加载本地 FAISS 必须的参数
db = FAISS.load_local("faiss/wuliu", embeddings, allow_dangerous_deserialization=True)
# db = FAISS.load_local("faiss/wuliu", embeddings)

start_time = time.time()


def get_related_content(related_docs):
    """
    文档检索与内容提取
    :param related_docs: 接收检索到的文档列表 related_doc
    :return:
    """
    # print(f'related_docs--》{related_docs}')
    related_content = []
    for doc in related_docs: # 遍历文档,提取 page_content 并用 \n 替换掉 \n\n(为了压缩 Prompt 长度)
        # print(f'doc.page_content--》{doc.page_content}')
        related_content.append(doc.page_content.replace("\n\n", "\n"))
    # print(f'related_content列表状态--》{related_content}')
    # 将所有相关文档的内容拼接成一个完整的字符串返回
    return "\n".join(related_content)


def define_prompt():
    # 硬编码了一个测试问题:'我的快递出发地是哪?预计几天的时间到达?'
    question = '我的快递出发地是哪?预计几天的时间到达?'
    # 调用 db.similarity_search(question, k=2) 从向量库中检索出与问题最相似的 2 个文本块
    docs = db.similarity_search(question, k=2)
    #
    related_content = get_related_content(docs)
    # print('*' * 80)
    # print(f'related_content字符串状态-->{related_content}')
    # print('*'*80)
    # 定义了 Prompt 模板,要求模型“基于已知信息回答,不允许编造”
    PROMPT_TEMPLATE = """
        基于以下已知信息,简洁和专业的来回答用户的问题。不允许在答案中添加编造成分。
        已知内容:
        {context}
        问题:
        {question}"""

    prompt = PromptTemplate(
        input_variables=["context", "question"],
        template=PROMPT_TEMPLATE, )

    my_pmt = prompt.format(context=related_content, question=question)

    return my_pmt


def qa():
    model = Ollama(model="qwen2.5:7b")
    # print(f'model-->{model}')
    my_pmt = define_prompt()
    print(f'my_pmt--》{my_pmt}')
    result = model.invoke(my_pmt)
    return result


if __name__ == '__main__':
    result = qa()
    print(result)
    end_time = time.time()
    print(end_time - start_time)
(3).web_qa.py
# 从自定义模块 local_qa 中导入所有必要的组件(如大模型实例、向量数据库 db 等)
from local_qa import *
# 从 LangChain 导入 ConversationalRetrievalChain,该链专门用于处理带有历史上下文的检索问答
# ConversationalRetrievalChain作用:自动保存历史对话信息
from langchain.chains import ConversationalRetrievalChain
# 导入 Streamlit 库,用于快速构建 Web 交互界面
import streamlit as st

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

# 初始化全局变量, 用于存储对话历史
# 注意:在 Streamlit 中,每次页面交互都会重新运行整个脚本,普通全局变量无法跨次交互保留状态
chat_history = []


# 定义检索链函数
def new_retrival():
    """
    创建基于 ConversationalRetrievalChain 的问答链
    该链会自动处理:检索相关文档 -> 将文档和历史对话作为上下文 -> 传给大模型生成答案
    """
    chain = ConversationalRetrievalChain.from_llm(
        llm=Ollama(model="qwen2.5:7b"),  # 使用本地大模型
        retriever=db.as_retriever()  # 基于本地数据库的检索器
    )
    return chain


# 主逻辑
def main():
    """
    Streamlit 主页面的交互逻辑, 负责界面渲染和状态管理
    """
    # print(f'st.session_state-->{st.session_state}')
    # 初始化会话状态
    # 初始化 Streamlit 的会话状态(Session State)
    # 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"]}')
        # 根据消息角色(user 或 assistant)创建对应的聊天气泡
        with st.chat_message(message["role"]):
            st.markdown(message["content"])  # 将消息内容以 Markdown 格式渲染显示

    # 接受用户输入
    # prompt = st.chat_input("请输入你的问题:")
    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 = ""

            # 调用检索链获取答案: 每次提问时重新创建检索链
            chain = new_retrival()
            # 调用检索链,传入当前问题和对话历史,获取包含答案的字典结果
            result = chain.invoke({"question": prompt, "chat_history": chat_history})
            print(f'result--->{result}')
            # 将当前的问答对追加到全局的 chat_history 列表中,以维持多轮对话记忆
            chat_history.append((prompt, result["answer"]))  # 更新聊天历史
            # 提取大模型生成的最终答案
            assistant_response = result["answer"]
            # 将答案渲染到占位符中
            message_placeholder.markdown(assistant_response)

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


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

github地址: 物流行业RAG,制作不易,请Star

【上一篇】【AI大模型应用开发】【基础】7.LangChain基础知识入门

【下一篇】【AI大模型应用开发】【项目实战】9.基于GPT2搭建医疗问诊机器人

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐