资讯中心

基于LangChain与Chroma的RAG实战:构建《红楼梦》智能问答系统

📅 2026/8/15 6:02:58
基于LangChain与Chroma的RAG实战:构建《红楼梦》智能问答系统
1. 项目概述为什么选择《红楼梦》来玩转RAG最近在AI圈子里RAG检索增强生成的热度一直居高不下。简单来说它就像给大语言模型LLM装上一个“外接大脑”——当模型遇到不知道的问题时不是硬着头皮瞎编而是先去指定的知识库里翻找相关资料然后基于找到的“证据”来组织答案。这能显著提升回答的准确性和事实性特别适合用来构建专业领域的问答系统。我之所以选择《红楼梦》作为这个项目的“试验田”有几个很实际的考虑。首先这部经典名著体量庞大、人物关系错综复杂、情节细节繁多即便是红学专家也未必能记住所有细节。用传统的关键词匹配搜索很难精准定位到“贾宝玉第一次见林黛玉时穿什么衣服”这类具体问题。其次《红楼梦》的文本是公开、稳定且结构化的避免了数据获取和版权上的麻烦。最后用大家耳熟能详的内容来演示技术效果会非常直观模型回答得对不对我们一眼就能看出来。这个项目的核心目标就是带你从零开始手把手搭建一个能对《红楼梦》进行智能问答的应用。我们会用到当前最流行的技术栈LangChain作为应用编排框架Chroma作为向量数据库来存储和检索知识qwen-plus通义千问作为核心的生成模型。整个流程会覆盖从原始文本处理、向量化存储、到检索链构建和问答界面实现的完整闭环。无论你是想了解RAG的基本原理还是打算为自己公司的知识库打造一个AI助手这个实战案例都能提供清晰的路径和可复现的代码。2. 核心组件选型与原理浅析在动手之前我们得先搞清楚手里这些“工具”是干什么的以及为什么选它们。这就像木匠选刨子和锯子用对了工具活儿才能干得漂亮。2.1 LangChainAI应用的“乐高”积木你可以把LangChain理解为一套专门用于构建大模型应用的高级工具箱。如果没有它我们需要自己处理很多繁琐的“胶水代码”比如把用户问题转换成模型能懂的格式、管理对话历史、调用不同的工具如搜索引擎、数据库等。LangChain把这些通用能力抽象成了“链”Chains、“代理”Agents、“记忆”Memory等标准化模块。在这个项目中我们主要会用到它的“检索问答链”RetrievalQA。这个链内部自动完成了“检索器获取相关文档” - “将文档和问题组合成提示词” - “调用LLM生成答案”这一系列动作。我们只需要配置好检索器和LLM它就能跑起来极大地简化了开发流程。选择LangChain就是看中了它的生态成熟、文档丰富并且能让我们更专注于业务逻辑而非底层通信细节。2.2 Chroma轻量高效的向量数据库“新贵”RAG的核心是“检索”而检索的核心是把文本转换成计算机能理解的“向量”一组数字并快速找到最相似的向量。这就需要向量数据库。Chroma是近两年崛起的一个开源向量数据库它的最大特点就是简单易用和轻量级。与一些重型数据库相比Chroma可以纯内存运行也支持持久化到磁盘。它的Python客户端API设计得非常友好几行代码就能完成集合创建、文档插入和相似性搜索。对于我们这个单机、数据量在百万级以下的《红楼梦》项目来说Chroma完全够用且能避免复杂的部署和运维。它的检索速度很快并且与LangChain有深度集成用起来非常顺手。注意在生产环境中如果数据量极大、要求高并发和高可用可能需要考虑Weaviate、Qdrant或Pinecone等更专业的云原生向量数据库。但对于学习和原型开发Chroma是绝佳的起点。2.3 qwen-plus强大且“听话”的生成引擎模型是RAG的“大脑”。我们选择通义千问的qwen-plus模型主要基于以下几点考量强大的中文理解与生成能力qwen系列模型在中文任务上的表现有目共睹对于《红楼梦》这类充满古典文学色彩和复杂语境的内容它能更好地理解和生成符合语境的答案。稳定的API服务通过阿里云灵积平台调用稳定性和响应速度有保障避免了自部署大模型的硬件门槛和运维成本。出色的指令遵循能力在RAG中我们需要模型严格根据我们提供的上下文来回答不能自由发挥。qwen-plus在“根据给定文本回答问题”这类指令遵循任务上表现良好能有效减少“幻觉”即编造不存在的信息。这里有一个关键点在RAG中我们通常不会让模型记忆《红楼梦》本身事实上它也记不全而是通过提示词工程明确指令它“仅根据以下上下文内容回答问题”。这样模型的核心能力就聚焦在了“理解问题”和“组织语言”上知识部分则由我们的向量数据库保障。3. 环境搭建与数据准备理论讲完了我们开始动手。第一步是把“厨房”收拾好把“食材”准备好。3.1 创建虚拟环境与安装依赖我强烈建议使用Python虚拟环境来管理项目依赖避免污染系统环境也方便未来迁移。# 创建并激活虚拟环境 (以 conda 为例你也可以用 venv) conda create -n rag-hongloumeng python3.10 conda activate rag-hongloumeng # 安装核心依赖 pip install langchain langchain-community langchain-chroma # Chroma 的 LangChain 集成包 pip install chromadb # 用于文本分割和嵌入 pip install tiktoken sentence-transformers # 用于读取文本文件如果红楼梦是txt格式 pip install pypdf # 如果是PDF格式则安装这个 # 用于调用qwen-plus API pip install dashscope这里解释一下几个关键包langchain: 主框架。langchain-community: 包含许多社区维护的第三方集成工具。langchain-chroma: LangChain官方维护的Chroma集成包比用langchain-community里的更稳定。chromadb: Chroma数据库的Python客户端。sentence-transformers: 我们用它里面的all-MiniLM-L6-v2模型来将文本转换为向量。这个模型小巧但效果不错支持中文且可以离线运行非常适合本地测试。dashscope: 阿里云灵积平台的SDK用于调用qwen-plus。3.2 获取并预处理《红楼梦》文本你需要一份《红楼梦》的纯文本文件.txt格式。可以从古登堡计划等公开版权网站获取。假设文件名为hongloumeng.txt放在项目根目录。原始文本是一整本小说我们不能直接把整本书扔给向量数据库。需要把它切割成一段段有意义的“块”Chunks。这里面的学问很大切得好检索精度高切得不好检索出来的内容可能文不对题。from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.document_loaders import TextLoader # 1. 加载文本 loader TextLoader(‘./hongloumeng.txt‘, encoding‘utf-8‘) documents loader.load() # 2. 创建文本分割器 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个块的最大字符数 chunk_overlap50, # 块与块之间的重叠字符数 length_functionlen, separators[“\n\n“, “\n“, “。“, ““, “ “, ““] # 按此优先级分割 ) # 3. 执行分割 split_docs text_splitter.split_documents(documents) print(f“原始文档数{len(documents)} 分割后块数{len(split_docs)}“)参数选择的经验谈chunk_size500对于中文古典小说500字左右是一个比较合适的长度。它能包含一个相对完整的情节片段比如一段对话或场景描写又不至于太长导致检索精度下降。如果处理的是技术手册可能更适合200-300字。chunk_overlap50重叠是为了避免一个完整的句子或关键信息被硬生生切在两段中间导致检索时丢失上下文。50字的重叠能很好地保证语义的连续性。separators设置分割符的优先级先尝试按双换行段落分不行再按单换行再按句号以此类推。这比单纯按固定长度切割更符合语言逻辑。预处理后你会得到上千个文本块。每个块都将被转换为向量存入Chroma。4. 构建向量知识库嵌入与存储文本块准备好了下一步是让计算机能“读懂”它们。这就是嵌入Embedding过程。4.1 选择与配置嵌入模型嵌入模型负责将文本转换成高维空间中的向量一组数字。语义相似的文本其向量在空间中的距离也更近。我们使用sentence-transformers中的模型。from langchain.embeddings import HuggingFaceEmbeddings # 创建嵌入模型 embed_model HuggingFaceEmbeddings( model_name“sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2“, model_kwargs{‘device‘: ‘cpu‘}, # 如果GPU可用可改为 ‘cuda‘ encode_kwargs{‘normalize_embeddings‘: True} # 归一化方便计算余弦相似度 )为什么选这个模型paraphrase-multilingual-MiniLM-L12-v2是一个在多语言语料上训练过的模型对中文的支持比纯英文模型好得多。它平衡了效果和速度在CPU上也能流畅运行。normalize_embeddings设置为True后所有向量的长度都会被归一化此时向量点积就等于余弦相似度这是最常用的相似度度量方式。4.2 创建并填充Chroma向量数据库现在我们将分割后的文本块、连同它们生成的向量一起存入Chroma。from langchain.vectorstores import Chroma import os # 定义持久化目录 persist_directory ‘./chroma_honglou_db‘ # 创建向量数据库 vectordb Chroma.from_documents( documentssplit_docs, embeddingembed_model, persist_directorypersist_directory ) # 持久化到磁盘 vectordb.persist() print(f“向量数据库已创建并保存至 {persist_directory}“)执行这段代码可能需要一些时间因为模型要逐个处理上千个文本块。完成后你会在本地看到chroma_honglou_db文件夹里面保存了所有向量和元数据。以后重启应用只需要加载这个目录即可无需重新生成向量。# 后续加载已有数据库 vectordb Chroma( persist_directorypersist_directory, embedding_functionembed_model )4.3 检索测试验证知识库质量在接入LLM之前我们先单独测试一下检索器确保它能找到相关内容。# 从向量库创建检索器 retriever vectordb.as_retriever( search_type“similarity“, # 相似度搜索 search_kwargs{“k“: 4} # 返回最相似的4个块 ) # 测试一个问题 test_question “贾宝玉的玉是怎么来的“ relevant_docs retriever.get_relevant_documents(test_question) print(f“问题‘{test_question}‘“) print(f“检索到 {len(relevant_docs)} 个相关文档块\n“) for i, doc in enumerate(relevant_docs): print(f“--- 块 {i1} (相似度分数仅供参考) ---“) print(doc.page_content[:300] “...“) # 打印前300字符 print()如果检索器工作正常它应该能返回描述“通灵宝玉”来历的文本块比如“女娲补天剩下的一块石头”等情节。通过观察返回的文本块是否切题你可以反向评估之前文本分割chunk的质量。如果返回的块总是只包含半句话或不相关的内容可能需要回头调整chunk_size和separators。5. 集成qwen-plus与构建问答链知识库建好了检索器也测试通过了现在该请出“大脑”——qwen-plus模型并把所有部件组装成一条自动化的流水线。5.1 配置qwen-plus模型接口首先你需要有一个阿里云账号并在灵积平台开通服务、获取API Key。from langchain.llms import Tongyi import os # 设置API Key (建议通过环境变量管理不要硬编码在代码中) os.environ[“DASHSCOPE_API_KEY“] “your-api-key-here“ # 创建通义千问模型实例 llm Tongyi( model_name“qwen-plus“, # 指定模型 temperature0.1, # 温度参数越低输出越确定 top_p0.8, )关键参数解析model_name“qwen-plus“指定使用qwen-plus模型。temperature0.1在知识问答场景下我们期望答案确定、忠实于上下文。较低的temperature值如0.1-0.3可以减少模型的随机性让它的回答更稳定、更倾向于选择概率最高的词汇。如果设得太高如0.8答案可能会变得天马行空。top_p0.8核采样参数与temperature配合使用共同控制生成的多样性。5.2 组装检索问答链LangChain的RetrievalQA链将检索和生成两步完美封装。from langchain.chains import RetrievalQA # 创建问答链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_type“stuff“, # 最常用的类型将所有检索到的上下文“塞”进提示词 retrieverretriever, return_source_documentsTrue, # 非常重要返回检索到的源文档用于验证 chain_type_kwargs{ “verbose“: True # 调试时打开可以看到拼接后的完整提示词 } )这里重点说一下chain_type“stuff“。这是最简单直接的方式它把检索到的所有文档块我们之前设置了k4简单地拼接起来和问题一起组成一个长长的提示词送给LLM。它的优点是简单、保真度高所有信息都可见。缺点是可能受限于LLM的上下文长度Token限制。对于《红楼梦》问答4个500字的块加上问题远低于qwen-plus的上下文窗口所以完全没问题。其他chain_type还有map_reduce、refine等适用于文档极多或需要逐步精炼答案的复杂场景但复杂度也更高。对于入门项目“stuff”链是最佳选择。5.3 设计提示词模板虽然RetrievalQA有默认提示词但为了获得更好的效果尤其是让模型严格遵循上下文我们最好自定义一个。from langchain.prompts import PromptTemplate # 自定义提示词模板 prompt_template “““请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据已知信息无法回答该问题”不要编造信息。 上下文 {context} 问题{question} 请根据上下文给出答案””” PROMPT PromptTemplate( templateprompt_template, input_variables[“context“, “question“] ) # 使用自定义提示词创建问答链 qa_chain_custom RetrievalQA.from_chain_type( llmllm, chain_type“stuff“, retrieverretriever, chain_type_kwargs{ “prompt“: PROMPT, “verbose“: False }, return_source_documentsTrue )这个提示词模板的核心是给出了明确的指令“严格根据以下提供的上下文信息来回答问题”和“如果信息不足请直接说无法回答”。这能极大地约束模型的“幻觉”倾向。{context}和{question}是占位符LangChain会在运行时自动替换。6. 实战问答与效果分析一切就绪让我们来问几个问题看看这个系统的实际表现。# 定义一个方便测试的函数 def ask_question(question): print(f“\n[用户问题]: {question}“) result qa_chain_custom({“query“: question}) print(f“[AI答案]: {result[‘result‘]}“) print(“\n[参考来源]“) for i, doc in enumerate(result[‘source_documents‘]): print(f“ 片段{i1}: {doc.page_content[:150]}...“) # 展示片段前150字 # 测试1事实性细节问题 ask_question(“林黛玉进贾府时多大年纪“) # 测试2人物关系问题 ask_question(“贾宝玉和薛宝钗是什么关系“) # 测试3情节概述问题 ask_question(“简述‘宝玉挨打’的主要原因。“) # 测试4上下文外的问题测试幻觉抑制 ask_question(“贾宝玉最后考中了状元吗“)预期效果与分析事实性细节问题系统应能准确检索到林黛玉进府时“年方五岁”或“六岁”的相关描述不同版本略有差异。这考验的是检索的精准度。人物关系问题应能回答出“姨表姐弟”或“夫妻”后四十回关系。这要求检索到的片段能包含人物关系的描述。情节概述问题需要模型综合多个检索片段可能涉及金钏儿投井、忠顺王府索人等不同原因进行概括总结。这考验LLM的归纳能力。上下文外问题这是对RAG系统的关键测试。由于《红楼梦》原著并未写宝玉参加科举并中状元这是续书或改编内容在严格的提示词约束下模型应回答“根据已知信息无法回答该问题”。如果它开始编造中状元的情节说明提示词约束力不够或temperature设置过高。通过观察[参考来源]你可以清晰地看到模型生成答案所依据的原文片段。这不仅是可解释性的体现更是调试系统最重要的依据。如果答案错了首先看是不是检索错了如果检索对了但答案错了那问题可能出在模型理解或提示词上。7. 性能优化与高级技巧基础版本跑通后我们可以从以下几个方向进行优化让系统更强大、更智能。7.1 检索优化超越简单相似度搜索默认的相似度搜索search_type“similarity“有时会漏掉关键信息。我们可以尝试更高级的检索器。# 1. 最大边际相关性MMR检索在相似的基础上增加多样性 mmr_retriever vectordb.as_retriever( search_type“mmr“, # 使用MMR算法 search_kwargs{“k“: 4, “fetch_k“: 10, “lambda_mult“: 0.7} ) # fetch_k: 先获取10个相似候选再从中根据MMR选4个。 # lambda_mult: 0.7更注重相似性0.3更注重多样性。 # 2. 自查询检索器让模型自己决定搜索关键词 # 这需要元数据过滤支持更适合结构化知识库。对于纯文本《红楼梦》提升有限但值得了解。MMR适用场景当用户问题比较宽泛如“说说贾宝玉”简单相似度搜索返回的4个片段可能都集中在“宝玉外貌”上。而MMR可能会返回一个讲外貌、一个讲性格、一个讲重要事件的片段使得最终答案更具概括性。7.2 嵌入模型升级sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2是一个很好的基线模型。如果你对效果有更高要求可以考虑更大的模型如BAAI/bge-large-zh-v1.5这是目前中文社区评价很高的嵌入模型效果显著提升但计算开销也更大。专用模型如果你构建的是法律、医疗等专业领域知识库使用在该领域语料上微调过的嵌入模型效果会更好。# 示例切换为BGE模型 embed_model_bge HuggingFaceEmbeddings( model_name“BAAI/bge-large-zh-v1.5“, model_kwargs{‘device‘: ‘cpu‘}, encode_kwargs{‘normalize_embeddings‘: True} ) # 注意首次使用会下载约1.3GB的模型文件。7.3 引入重排序Re-ranking这是提升RAG效果的大杀器。简单相似度搜索可能存在“语义匹配但实际不相关”的问题。重排序器使用一个更精细的交叉编码模型对初步检索到的文档比如前20个进行二次打分和排序只保留最相关的几个送给LLM。# 这是一个高级概念LangChain社区可能有集成或需要手动实现。 # 基本思路先用向量数据库召回top-k个文档如k20再用重排序模型如BGE的交叉编码版本对这20个文档和问题进行相关性打分取top-n如n4送入LLM。 # 这能显著提升答案质量但会增加延迟。7.4 构建简单的Web交互界面用命令行测试毕竟不方便。我们可以用Gradio快速搭建一个Web界面。import gradio as gr # 定义问答函数供Gradio调用 def answer_question(history, message): result qa_chain_custom({“query“: message}) answer result[‘result‘] # 将问答加入历史 history.append((message, answer)) return history, history # 创建Gradio界面 with gr.Blocks() as demo: gr.Markdown(“# 《红楼梦》智能问答系统“) chatbot gr.Chatbot(label“问答记录“) msg gr.Textbox(label“请输入你的问题“) clear gr.Button(“清空“) def respond(message, chat_history): result qa_chain_custom({“query“: message}) bot_message result[‘result‘] # 可选在答案后附加来源提示 # bot_message “\n\n---\n*回答基于检索到的原文片段。*“ chat_history.append((message, bot_message)) return ““, chat_history msg.submit(respond, [msg, chatbot], [msg, chatbot]) clear.click(lambda: None, None, chatbot, queueFalse) demo.launch(shareFalse) # 设置shareTrue可获得一个临时公网链接运行这段代码会在本地启动一个Web服务。打开浏览器访问给出的地址就能看到一个简洁的聊天界面可以直接提问并看到AI的回答。8. 常见问题与排查指南在实际搭建和运行过程中你可能会遇到以下问题。这里我总结了一份排查清单。问题现象可能原因解决方案运行pip install chromadb失败提示与grpc相关错误。系统环境或网络问题。1. 升级pip:pip install --upgrade pip。2. 使用清华源安装:pip install chromadb -i https://pypi.tuna.tsinghua.edu.cn/simple。3. 确保Python版本在3.8以上。创建向量数据库时程序卡住或内存占用极高。1. 文本块chunks过多或过大。2. 嵌入模型第一次下载。1. 检查chunk_size对于中文500-800较合适超过1000可能过大。2. 首次使用sentence-transformers会自动下载模型请耐心等待或检查网络。检索结果完全不相关。1. 嵌入模型不支持中文或效果差。2. 文本分割不合理破坏了语义。1. 确认使用多语言或中文优化的嵌入模型如paraphrase-multilingual-MiniLM-L12-v2或BAAI/bge系列。2. 调整text_splitter的separators尝试加入中文标点如“。”、“”并减少chunk_size。答案出现“幻觉”编造内容。1. 提示词约束力不够。2. LLM的temperature参数过高。3. 检索到的上下文本身不相关。1. 强化提示词明确写上“仅根据上下文回答不知道就说不知道”。2. 将temperature调低至0.1或0.2。3. 检查检索到的源文档source_documents先确保检索是对的。回答“根据已知信息无法回答”但明明书里有。1. 检索失败没找到相关片段。2. 问题表述与原文差异太大。1. 增加检索数量k比如从4调到6。2. 尝试使用MMR检索器。3. 考虑在文本分割时增加chunk_overlap避免关键信息被切断。调用qwen-plus API超时或报错。1. API Key未设置或错误。2. 网络问题。3. 请求频率超限。1. 检查DASHSCOPE_API_KEY环境变量是否正确设置。2. 检查网络连接特别是代理设置。3. 免费额度可能有QPS限制稍后再试或分批请求。Gradio界面启动后无法访问。端口被占用或防火墙阻止。1. 默认端口7860可能被占尝试demo.launch(server_port7861)。2. 如果是云服务器确保安全组开放了对应端口。一个关键的调试心法始终打开return_source_documentsTrue。任何答案不准的问题首先去检查source_documents。如果检索到的文档牛头不对马嘴那么问题出在检索阶段嵌入模型、文本分割如果检索到的文档是正确的但答案错了那么问题出在生成阶段提示词、LLM参数。按照这个思路大部分问题都能快速定位。最后这个项目只是一个起点。RAG的天花板很高你还可以探索更多为文档块添加元数据如章节标题、人物标签进行过滤检索实现多轮对话的记忆功能将后端封装成API服务供其他应用调用。技术的乐趣就在于从一个简单的想法开始不断迭代和深化最终构建出真正有用的工具。希望这篇从0到1的指南能成为你探索RAG世界的一块坚实垫脚石。