大模型应用开发学习-基于langchain框架做一个个人文档问答助手

很多人都听说过大模型应用开发,但不知道具体是什么或者应该如何学习,今天我就写一个简单个人文档文档助手,分享一些我学习大模型应用开发的心得体会;

一、整体实现思路

复制代码
加载文档 → 分割文本 → 生成向量 → 存储向量 → 检索相关文本 → 结合大模型回答问题

二、环境准备

本地大模型下载(ollama本地大模型如何部署,可以看我之前发过的文章):

复制代码
# 生成向量嵌入模型
ollama pull nomic-embed-text
# 本地依赖的大模型引擎
ollama pull qwen2.5:7b

需要安装的依赖:

复制代码
# LangChain 核心组件
pip install langchain
pip install langchain-community
pip install langchain-core
pip install langchain-text-splitters

# 向量数据库与 Ollama 集成
pip install langchain-chroma
pip install langchain-ollama

# 文档加载器依赖
pip install pypdf          # PDF 支持
pip install docx2txt       # Word 文档支持

# 环境变量管理
pip install python-dotenv

三、技术架构

|-------|--------------------------------------|--------------------|
| 组件 | 技术选型 | 说明 |
| 文档加载 | LangChain Loaders | 支持 PDF、TXT、DOCX 格式 |
| 文本分割 | RecursiveCharacterTextSplitter | 智能分割,保留上下文 |
| 向量嵌入 | OllamaEmbeddings (nomic-embed-text) | 本地嵌入模型 |
| 向量存储 | Chroma | 轻量级本地向量数据库 |
| 大语言模型 | ChatOllama | 本地部署的 LLM |
| 链式调用 | LCEL (LangChain Expression Language) | 最新版链式编排 |

四、RAG 原理图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      预处理阶段                              │
├─────────────────────────────────────────────────────────────┤
│  文档 → 文本分割 → 向量嵌入 → Chroma 向量库                   │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      问答阶段                                │
├─────────────────────────────────────────────────────────────┤
│  用户问题 → 向量检索 → 获取相关文档 → 构建 Prompt → LLM 生成   │
└─────────────────────────────────────────────────────────────┘

五、代码详解

5.1 load_documents(file_path: str)

功能:根据文件扩展名加载不同格式的文档

参数

  • file_path (str): 文档的绝对或相对路径

返回值

  • list[Document]: 文档对象列表,每个对象包含 page_contentmetadata

支持的文件格式

|---------|----------------|-------------|
| 扩展名 | 加载器 | 说明 |
| .pdf | PyPDFLoader | PDF 文档,按页分割 |
| .txt | TextLoader | 纯文本文件 |
| .docx | Docx2txtLoader | Word 文档 |

异常处理

  • 不支持的格式抛出 ValueError
  • 文件读取失败返回空列表 []

示例

复制代码
docs = load_documents("./data/manual.pdf")
# 输出: 加载文档成功: ./data/manual.pdf,共 50 页/段

5.2 split_documents(documents: list[Document])

功能:将长文档分割成适合向量化的文本块

参数

  • documents (list[Document]): 文档对象列表

返回值

  • list[Document]: 分割后的文本块列表

分割策略

复制代码
RecursiveCharacterTextSplitter(
    chunk_size=1000,       # 每块最大 1000 字符
    chunk_overlap=200,     # 相邻块重叠 200 字符(保留上下文连贯性)
    separators=["\n\n", "\n", "。", "!", "?", ",", "、", " "]  # 中文优先分隔符
)

设计考量

  • chunk_overlap=200:确保上下文不丢失,避免答案被截断
  • 中文分隔符优先级:段落 → 句子 → 标点 → 词

5.3 init_vector_db(splits)

功能:初始化向量数据库并存储文档向量

参数

  • splits (list[Document]): 分割后的文本块列表

返回值

  • Chroma: Chroma 向量数据库对象

关键配置

复制代码
embeddings = OllamaEmbeddings(
    model="nomic-embed-text",           # 嵌入模型名称
    base_url=os.getenv("LOCAL_MODEL_URL")  # Ollama 服务地址
)

vectorstore = Chroma.from_documents(
    documents=splits,
    embedding=embeddings,
    persist_directory="./chroma_db"     # 持久化目录
)

注意:向量库持久化后,下次启动可直接加载,无需重新向量化。


5.4 build_agent(vectorstore)

功能:构建 RAG 问答链(核心方法)

参数

  • vectorstore (Chroma): 已初始化的向量数据库

返回值

  • tuple: (rag_chain, get_sources)
    • rag_chain: LCEL 链对象,用于执行问答
    • get_sources: 辅助函数,获取检索来源

LCEL 链式调用详解

复制代码
rag_chain = (
    RunnablePassthrough.assign(
        context=lambda x: "\n\n".join(
            [doc.page_content for doc in retriever.invoke(x["question"])]
        )
    )   # 1. 检索相关文档,赋值给 context
    | prompt      # 2. 填充 Prompt 模板
    | llm         # 3. 调用大模型生成回答
    | output_parser  # 4. 解析输出为字符串
)

Prompt 模板设计

复制代码
你是一个专业的文档问答助手,仅基于以下提供的上下文信息回答用户的问题。
如果上下文没有相关信息,请明确说明"无法从文档中找到相关答案",不要编造内容。
不知道的信息,直接回答不知道。

上下文: {context}
问题: {question}

设计要点

  • 明确约束"仅基于上下文回答",防止模型幻觉
  • 检索数量 k=2:平衡准确性和响应速度

5.5 qa_interaction(rag_chain, get_sources)

功能:交互式问答界面

参数

  • rag_chain: RAG 链对象
  • get_sources: 获取检索来源的函数

交互流程

复制代码
1. 显示欢迎信息
2. 循环等待用户输入
   ├── 输入 "exit" → 退出程序
   ├── 输入为空 → 提示重新输入
   └── 有效问题 → 调用 RAG 链 → 输出答案和来源

输出格式

复制代码
助手:[模型生成的回答]

[来源] 回答来源(前2条):
  来源1:[相关文档片段]...
  来源2:[相关文档片段]...

六、完整代码

复制代码
#整体逻辑是:加载文档 → 分割文本 → 生成向量 → 存储向量 → 检索相关文本 → 结合大模型回答问题
import os
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader, TextLoader, Docx2txtLoader
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings, ChatOllama

# 核心:最新 LCEL 组件
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 加载环境变量
load_dotenv()

# 1. 加载文档

def load_documents(file_path: str):
    """
    加载文档
    根据不同文件的后缀加载不同格式的文档
    :param file_path: 文档路径
    :return: 加载后的文档对象列表
    """
    # 获取文件后缀
    file_ext=os.path.splitext(file_path)[-1]

    try:
        if file_ext==".pdf":
            loader=PyPDFLoader(file_path)
        elif file_ext==".txt":
            loader=TextLoader(file_path)
        elif file_ext==".docx":
            loader=Docx2txtLoader(file_path)
        else:
            raise ValueError(f"暂时不支持的文件格式: {file_ext}")

        # 加载文档并返回
        documents=loader.load()
        print(f"加载文档成功: {file_path},共{len(documents)} 页/段")
        return documents
    except Exception as e:
        print(f"加载文件失败: {e}")
        return []

# 2. 分割文本
def split_documents(documents: list[Document]):
    """
    分割文本
    将长文本分割成小文本
    :param documents: 加载后的文档对象列表
    :return: 分割后的文本列表
    """
    # 初始化文本分割器
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,  # 每个块最大 1000 字符
        chunk_overlap=200,  # 相邻块重叠 200 字符(保留上下文)
        length_function=len,  # 字符长度函数(默认)
        separators=["\n\n", "\n", "。", "!", "?", ",", "、", " "]  # 分隔符优先级:中文分割符
    )

    # 分割文档
    splits = text_splitter.split_documents(documents)
    print(f"分割文档成功: 共{len(splits)} 段")
    return splits

# 3. 初始化向量库,并存储向量
def init_vector_db(splits):
    """
    初始化向量库,并存储向量
    :param splits: 分割后的文本列表
    :return: Chroma向量库对象
    """
    # 初始化 Ollama 本地嵌入模型
    embeddings=OllamaEmbeddings(model="nomic-embed-text",base_url=os.getenv("LOCAL_MODEL_URL"))

    # 初始化本地 Chroma 向量库并添加文档
    vectorstore = Chroma.from_documents(
        documents=splits,
        embedding=embeddings,
        persist_directory="./chroma_db"  # 本地持久化目录(可选,自动创建)
    )
    print("[OK] 千问嵌入模型已初始化,向量库存储到 ./chroma_db 目录")
    return vectorstore

# 4. 构建问答链
def build_agent(vectorstore):
    """
    构建问答链
    :param vectorstore: Chroma向量库对象
    :return: 问答链对象
    """
    # 加载模型
    llm = ChatOllama(
        model=os.getenv("LOCAL_MODEL_NAME"),
        base_url=os.getenv("LOCAL_MODEL_URL"),
        temperature=0.5
    )

    # 定义检索器(从向量库检索相关文档,使用 MMR 提高多样性)
    retriever = vectorstore.as_retriever(
        search_type="mmr",  # 使用 MMR 算法,减少重复结果
        search_kwargs={"k": 3, "fetch_k": 10}  # 返回3个结果,从10个候选中选择
    )

    # 构建问答链 RAG 链(推荐新方式)
    prompt = ChatPromptTemplate.from_template(
        """你是一个专业的文档问答助手。请遵循以下规则:
1. 仅基于提供的上下文信息回答问题
2. 如果上下文没有相关信息,请明确回答"根据文档内容,无法回答该问题"
3. 回答要简洁准确,不要编造内容

上下文:
{context}

问题: {question}

回答:"""
    )
    # 定义输出解析器
    output_parser = StrOutputParser()

    # 5. 构建 RAG 链(LCEL 链式调用,核心!)
    rag_chain = (
        RunnablePassthrough.assign(
            context=lambda x: "\n\n---\n\n".join(
                [doc.page_content for doc in retriever.invoke(x["question"])])
        )
        | prompt
        | llm
        | output_parser
    )

    # 获取检索来源的辅助函数(用于展示回答来源)
    def get_sources(question):
        """检索问题相关的文档来源"""
        return retriever.invoke(question)

    print("[OK] RAG 问答链构建完成,可开始提问!")
    return rag_chain, get_sources


# -------------------------- 6. 交互问答(调用 RAG 链) --------------------------
def qa_interaction(rag_chain, get_sources):
    print("\n" + "=" * 50)
    print("       RAG 文档问答助手")
    print("=" * 50)
    print("输入问题进行提问,输入 'exit' 或 'quit' 退出\n")

    while True:
        try:
            question = input("你:").strip()  # 去除首尾空格
        except (EOFError, KeyboardInterrupt):
            print("\n[!] 再见!")
            break

        # 退出判断
        if question.lower() in ("exit", "quit", "q"):
            print("[!] 再见!")
            break

        # 空输入判断
        if not question:
            print("[!] 请输入有效问题!")
            continue

        try:
            # 调用 RAG 链
            answer = rag_chain.invoke({"question": question})
            # 获取回答来源
            sources = get_sources(question)

            # 输出结果
            print(f"\n助手:{answer}")

            # 去重显示来源
            if sources:
                print("\n" + "-" * 40)
                print("回答来源:")
                seen = set()
                for i, doc in enumerate(sources):
                    # 简单去重:取前100字符判断
                    content_key = doc.page_content[:100]
                    if content_key not in seen:
                        seen.add(content_key)
                        source_info = doc.metadata.get("source", "未知来源")
                        page_info = doc.metadata.get("page", "")
                        page_str = f" (第{page_info + 1}页)" if page_info != "" else ""
                        print(f"  [{len(seen)}] {source_info}{page_str}")
                        print(f"      {doc.page_content[:150]}...")
                        if len(seen) >= 2:  # 最多显示2条
                            break
                print("-" * 40)
        except Exception as e:
            print(f"[错误] 回答生成失败:{e}")


# -------------------------- 主函数 --------------------------
if __name__ == "__main__":
    import sys

    print("=" * 50)
    print("       RAG 文档问答系统")
    print("=" * 50)

    # 检查是否已有向量库
    db_path = "./chroma_db"
    use_existing = False

    if os.path.exists(db_path):
        choice = input("\n检测到已有向量库,是否使用?(y/n): ").strip().lower()
        if choice == "y":
            use_existing = True

    if use_existing:
        # 加载已有向量库
        embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url=os.getenv("LOCAL_MODEL_URL"))
        vector_db = Chroma(
            persist_directory=db_path,
            embedding_function=embeddings
        )
        print("[OK] 已加载现有向量库")
    else:
        # 新建向量库
        DOCUMENT_PATH = input("\n请输入文档路径:").strip()
        if not DOCUMENT_PATH:
            print("[错误] 文档路径不能为空")
            sys.exit(1)

        docs = load_documents(DOCUMENT_PATH)
        if not docs:
            sys.exit(1)

        splits = split_documents(docs)
        vector_db = init_vector_db(splits)

    # 构建问答链并开始交互
    rag_chain, get_sources = build_agent(vector_db)
    qa_interaction(rag_chain, get_sources)

七、脚本测试效果

我输入了一个本地的关于交换机的pdf文档,加载到了向量数据库,并保存到了本地;

问题:如何查看交换机的版本

回答效果如下:

总结

通过这一个小小的案例,可以清楚的了解到大模型应用开发的整个流程,数据如何保存,数据如何读取,提示词模板如何设计等,算是一个入门的一个教程,没有写前端页面,有兴趣的也可以写一个前端页面;后续关于大模型应用开发的探索学习我将会持续分享一些心得体会;

相关推荐
m0_743470372 小时前
Python字典与集合:高效数据管理的艺术
jvm·数据库·python
2501_945423542 小时前
使用Scrapy框架构建分布式爬虫
jvm·数据库·python
承渊政道2 小时前
【优选算法】(实战体验滑动窗口的奇妙之旅)
c语言·c++·笔记·学习·算法·leetcode·visual studio
2401_851272992 小时前
使用Python进行量化交易入门
jvm·数据库·python
huohuopro2 小时前
UML的概念和主图学习
学习·uml
93_Ryan2 小时前
企业级混合智能体核心引擎架构设计
人工智能·python
故城、2 小时前
Python进阶
开发语言·python
C羊驼2 小时前
C语言学习笔记(十):操作符
c语言·开发语言·经验分享·笔记·学习
鹭天2 小时前
RAG学习笔记
笔记·学习