目录

LangChain核心组件(官方文档)

RAG实现流程

1. 前置准备

2. 数据预处理阶段:文档加载、分块

3. 检索阶段:向量索引构建与相似度检索

4. 生成阶段:LLM上下文增强与结果生成

相关理论知识或避坑指南:

1. 容易出现间接prompt注入攻击(indirect prompt injection),怎么解决?

2. 避坑指南1

3. 避坑指南2


LangChain核心组件(官方文档

  • Document Loaders:读取原始文件,支持PDF、HTML、数据库等多种数据源
  • Text Splitters:基于字符、令牌或语义的文本分块策略,将大文档切分成小块
  • Embedding Models:OpenAI、HuggingFace等嵌入模型集成,将文本转成向量
  • Vector Stores:FAISS、Chroma等向量数据库的对接实现,存储向量并支持快速检索
  • RetrievalQA‌:是 LangChain 框架中用于构建‌检索增强生成(RAG)问答系统‌的核心组件,它结合信息检索与大语言模型(LLM)生成能力,能从大量文档中精准定位相关信息并生成自然语言答案。从向量库中返回 Top-K 相关文档块,chain_type属性决定如何处理多个检索到的文档片段

       四种常见 chain_type 对比

类型 工作方式 适用场景 响应速度 内存占用 准确率
stuff 拼接所有文档一次性输入 LLM 短文档、简单问题 高(若不超 token 限制)
map_reduce 分别处理每篇文档再汇总 长文档、多文档综合 中高
refine 迭代优化答案,逐步完善 高精度要求、复杂分析 最高
map_rerank 对每个文档评分后选最佳 多候选需置信度排序

RAG实现流程

本篇文档使用的选取OpenAIEmbeddings作为嵌入模型、Chroma作为持久化向量数据库搭建一个简单的本地文档问答Agent。

Chroma数据库的优势在于你只需要指定一个本地文件夹路径,Chroma 就会自动在该目录下创建数据库文件。下次程序重启时,只要指向同一个路径,数据就还在!属于 “ 一次构建,多次复用 ” 的轻量级持久化向量数据库,默认基于 SQLite 和文件系统,无需安装额外的数据库服务软件。

1. 前置准备
  • 查看本地的langchain的相关版本,因为照搬下面代码中有一些包会出现引用错误。例如新版langchain的RecursiveCharacterTextSplitter和RetrievalQA不在langchain包里面,有可能在langchain_text_splitters或者langchain_community或者langchain_classic里面
pip show langchain
或者 
pip show list
  • 安装(pip install)langchain-chroma、langchain-community、langchain-text-splitters等包

  • 数据准备:data文件夹下pdf

  • 在.env中配置openai_api_key

2. 数据预处理阶段:文档加载、分块
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain.chat_models import init_chat_model
import os
from dotenv import load_dotenv
import getpass
from langchain_chroma import Chroma


def load_documents(file_path):
    """
    加载文档
    
    Args:
        file_path: 文档文件路径
    """
    # 根据文件扩展名选择不同的加载器
    if file_path.endswith('.pdf'):
        loader = PyPDFLoader(file_path)
    else:
        from langchain_community.document_loaders import TextLoader
        loader = TextLoader(file_path, encoding="utf-8")
    
    documents = loader.load()
    return documents


def split_documents(documents, chunk_size=1000, chunk_overlap=200):
    """
    分割文档
    
    Args:
        documents: 文档列表
        chunk_size: 块大小
        chunk_overlap: 块重叠大小
    """
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap
    )
    chunks = text_splitter.split_documents(documents)
    return chunks
3. 检索阶段:向量索引构建与相似度检索
def create_vectorstore(chunks):
    """
    创建向量存储
    
    Args:
        chunks: 文档块列表
    """
    global vectorstore
    print("正在使用OpenAIEmbeddings...")
    try:
        if not os.environ.get("OPENAI_API_KEY"):
            os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter API key for OpenAI: ")

        from langchain_openai import OpenAIEmbeddings
        embeddings = OpenAIEmbeddings()
        print("OpenAIEmbeddings创建成功")
        
        # 尝试使用Chroma向量存储
        try:
            print("正在创建Chroma向量存储...")
            # 简化Chroma配置,移除persist_directory以减少初始化时间
            vectorstore = Chroma(
                collection_name="python_knowledge", # 集合名称,类似SQL中的表名
                embedding_function=embeddings,
                persist_directory="./chroma_langchain_db",  # 【核心】数据持久化目录,代码执行后,你会发现在项目目录下多了一个 chroma_langchain_db文件夹,里面包含了 .sqlite 等文件。

            )
            print("Chroma向量存储创建成功")
            
            # 分批添加文档,每批3个
            batch_size = 3
            total_chunks = len(chunks)
            print(f"开始添加{total_chunks}个文档块到Chroma向量存储...")
            
            for i in range(0, total_chunks, batch_size):
                batch = chunks[i:i+batch_size]
                print(f"添加第{i+1}-{min(i+batch_size, total_chunks)}个文档块...")
                ids = vectorstore.add_documents(documents=batch)
                print(f"成功添加{len(ids)}个文档块")
            
            print("Chroma向量存储数据初始化成功")
            # 持久化存储
            print(f"持久化向量存储到: {persist_directory}")
            vectorstore.persist()
            print("向量存储持久化成功")
        except Exception as chroma_error:
            print(f"创建Chroma向量存储失败: {chroma_error}")
            print("尝试使用FAISS向量存储...")
            # 回退到FAISS向量存储
            vectorstore = FAISS.from_documents(
                documents=chunks,
                embedding=embeddings
            )
            print("FAISS向量存储创建成功")
            # 持久化存储
            print(f"持久化向量存储到: {persist_directory}")
            vectorstore.save_local(persist_directory)
            print("向量存储持久化成功")
    except Exception as e2:
        print(f"使用OpenAIEmbeddings也失败: {e2}")
        raise

def create_qa_chain():
    """
    创建问答链
    """
    global vectorstore, qa_chain
    if not vectorstore:
        raise ValueError("Vectorstore not created. Please create vectorstore first.")
    
    model = init_chat_model(
        "claude-sonnet-4-6",
        temperature=0.7,
        timeout=10,
        max_tokens=1000
    )
    
    # 尝试从不同的模块导入RetrievalQA
    try:
        from langchain.chains import RetrievalQA
    except ImportError:
        try:
            from langchain_classic.chains import RetrievalQA
        except ImportError:
            from langchain_community.chains import RetrievalQA
    qa_chain = RetrievalQA.from_chain_type(
        llm=model,
        chain_type="stuff",
        retriever=vectorstore.as_retriever(
            search_kwargs={"k": 3}
        ),
        return_source_documents=True
    )


def query(question):
    """
    执行查询
    
    Args:
        question: 问题
    
    Returns:
        回答结果
    """
    global qa_chain
    if not qa_chain:
        raise ValueError("QA chain not created. Please create QA chain first.")
    
    result = qa_chain.invoke({"query": question})
    return result
4. 生成阶段:LLM上下文增强与结果生成
def main():
    """
    主函数
    """
    global vectorstore
    print("开始执行RAG系统...")
    # 加载已有的向量存储
    load_existing_vectorstore()
    print("RAG系统初始化成功")
    
    # 检查向量存储是否已存在
    if vectorstore:
        print("向量存储已存在,跳过加载、分割和存储步骤")
    else:
        print("向量存储不存在,开始加载文档...")
        # 加载PDF文档
        pdf_path = "src/data/claudecode_从入门到进阶.pdf"
        print(f"PDF路径: {pdf_path}")
        
        # 检查文件是否存在
        if not os.path.exists(pdf_path):
            print(f"文件不存在: {pdf_path}")
            print("请确保文件已下载到data文件夹中")
            return
        print("PDF文件存在")
        
        # 加载文档
        print("开始加载文档...")
        documents = load_documents(pdf_path)
        print(f"Loaded {len(documents)} documents")
        
        # 分割文档
        print("开始分割文档...")
        chunks = split_documents(documents)
        print(f"Split into {len(chunks)} chunks")
        
        # 创建向量存储
        print("开始创建向量存储...")
        create_vectorstore(chunks)
        print("Created vectorstore")
    
    # 创建问答链
    print("开始创建问答链...")
    create_qa_chain()
    print("Created QA chain")
    
    # 多轮对话
    print("\n开始多轮对话模式")
    print("输入 'exit' 或 'quit' 退出对话")
    print("=" * 50)
    
    while True:
        # 获取用户输入
        question = input("请输入您的问题: ")
        
        # 检查是否退出
        if question.lower() in ['exit', 'quit']:
            print("对话结束,再见!")
            break
        
        # 检查问题是否为空
        if not question.strip():
            print("问题不能为空,请重新输入")
            continue
        
        print("\n" + "=" * 50)
        print(f"Question: {question}")
        print("=" * 50)
        
        try:
            # 执行查询
            result = query(question)
            print(f"Answer: {result['result']}")
            
            # 显示来源文档
            print("\nSource documents:")
            for i, doc in enumerate(result['source_documents']):
                print(f"\nDocument {i+1}:")
                print(doc.page_content[:200] + "...")
        except Exception as e:
            print(f"查询时出错: {e}")
        
        print("\n" + "=" * 50)

相关理论知识或避坑指南:

1. 容易出现间接prompt注入攻击(indirect prompt injection),怎么解决?

RAG应用容易受到间接提示注入的影响。检索的文档可能包含类似指令的文本(例如,“以JSON格式响应”或“忽略之前的指令”)。由于检索的上下文与系统提示符共享相同的上下文窗口,模型可能会无意中遵循数据中嵌入的指令,而非你预期的提示。例如,本教程中索引的博客文章包含描述自动GPTJSON响应格式。如果用户查询检索到该块,模型可能会输出JSON而非自然语言答案。

为了缓解这个问题:

  1. 使用防御提示:明确指示模型仅将检索到的上下文视为数据,忽略其中的任何指令。本教程中的提示包含了这样的指导。
  2. 用分隔符包裹上下文:使用清晰的结构标记(例如,XML 标签如 )来区分检索的数据与指令,使模型更容易区分它们。<context>...</context>
  3. 验证响应:检查模型输出是否符合预期格式(如纯文本),并优雅处理意外格式。

没有任何缓解方法是万无一失的——这是当前大型语言模型架构的固有局限,指令和数据共享同一上下文窗口。

2. 避坑指南1
3. 避坑指南2

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐