资讯动态

基于LangChain与ChromaDB的RAG应用实战:以《红楼梦》问答为例

发布时间:2026/8/14 3:17:23 来源:尧图企业网站定制
1. 项目缘起为什么是《红楼梦》与RAG最近在折腾大模型应用发现一个挺有意思的现象很多朋友一上来就想搞个“万能知识库”恨不得把公司所有文档、个人所有笔记都喂进去。结果往往是要么向量化过程卡死要么召回结果驴唇不对马嘴最后项目不了了之。我自己的经验是从一个小而具体的领域切入把全链路跑通、吃透远比一开始就铺个大摊子要实在得多。所以这次我选了《红楼梦》作为实验对象。原因很简单第一文本体量适中全文约73万字既不会像单篇文档那样过于简单也不会像海量文档库那样复杂到让人迷失第二内容结构清晰有明确的人物、情节、诗词非常适合测试问答系统的准确性第三文化内涵丰富很多问题需要结合上下文理解比如“林黛玉为什么葬花”、“‘冷月葬花魂’这句诗好在哪里”这能很好地检验RAG系统是否真的“理解”了文本而不是简单的关键词匹配。这个项目的核心目标就是手把手带你从零开始搭建一个能针对《红楼梦》进行高质量问答的RAG应用。我们会用到LangChain这个当今最流行的AI应用框架来组织流程用轻量高效的ChromaDB作为向量数据库存储知识最后调用通义千问的qwen-plus模型来生成最终答案。整个过程我会把每一步的原理、踩过的坑、以及那些官方文档里不会写的调试技巧都掰开揉碎了讲清楚。无论你是刚接触LangChain的新手还是想找一个具体项目来深化理解的开发者相信都能有所收获。2. 核心组件选型为什么是LangChain Chroma qwen-plus在动手之前我们得先搞清楚手里的“兵器”。市面上框架、模型、数据库那么多为什么偏偏是这套组合这背后是经过一番权衡和实际测试的。2.1 LangChain不只是“胶水”更是“脚手架”很多人把LangChain理解为连接大模型和外部工具的“胶水”这个说法对但不全面。在我实际使用中它更像一个高度模块化、提供了最佳实践范式的“脚手架”。对于RAG应用它至少解决了三个关键问题文档加载与处理的标准化一本《红楼梦》的txt文件你怎么读LangChain提供了TextLoader、UnstructuredFileLoader等一大堆文档加载器能处理PDF、Word、HTML等多种格式。更重要的是它内置了文本分割RecursiveCharacterTextSplitter的逻辑这是RAG的基石。你自己写分割逻辑很容易切碎一个完整的句子或诗词而LangChain提供的分割器已经考虑到了中英文的标点、换行等特性开箱即用省心不少。流程编排的抽象化RAG的核心流程“检索 - 增强 - 生成”是一个固定范式。LangChain将其抽象为RetrievalQA链。你只需要配置好检索器Retriever和大模型LLM它就能自动帮你完成“将用户问题转化为向量 - 去向量库检索 - 将检索到的上下文和问题组合成Prompt - 发给LLM生成答案”这一整套流程。这避免了我们在业务逻辑里写大量胶水代码让开发更聚焦于核心优化点。生态与扩展性LangChain有极其丰富的集成生态。今天我们用Chroma明天想换Milvus或Pinecone可能只需要改一行代码。想给检索结果加个重排序Re-ranking模块也有现成的接口可以接入。这种设计保证了项目的可迭代性。注意LangChain的版本迭代很快API有时会有变动。建议在项目开始时就用pip freeze requirements.txt锁定核心库的版本避免后续跑不通。本项目基于langchain0.1.0和langchain-community0.0.10进行。2.2 Chroma轻量且开发者友好的向量数据库向量数据库是RAG的“记忆体”。选择ChromaDB主要是看中它的两个特点极致简单内置嵌入Chroma最大的优势是“开箱即用”。它内置了多种开源的句子嵌入模型比如all-MiniLM-L6-v2你甚至不需要单独去申请Embedding API的密钥就能快速把文本转化为向量。这对于原型验证和中小型项目来说极大地降低了门槛。它可以直接在内存中运行也支持持久化到磁盘部署方式非常灵活。与LangChain深度集成Chroma是LangChain官方推荐和深度支持的向量数据库之一集成度非常高。从创建集合Collection、添加文档到构建检索器都有非常简洁的API。当然Chroma不适合海量数据比如上亿条的生产环境它的集群能力和性能优化相比专业的商业向量数据库有差距。但对于我们这个百万字级别的《红楼梦》项目以及绝大多数个人或中小团队的知识库场景它都是绰绰有余且最佳的选择。2.3 qwen-plus选择闭源大模型的现实考量模型层我们选择了通义千问的qwen-plus。这里可能有人会问为什么不用开源的Llama 3或者Qwen2.5原因在于项目阶段的务实选择。稳定性与易用性在项目搭建和调试阶段我们最需要的是模型输出的稳定性和API调用的便捷性。闭源模型如qwen-plus、GPT-4提供了稳定可靠的云端服务我们无需关心模型部署、显卡资源、推理优化等问题可以把全部精力放在应用逻辑本身。强大的指令遵循与长上下文能力qwen-plus拥有128K的上下文长度这对于RAG应用非常重要。当我们的检索器返回多段相关文本时需要模型有能力在长长的Prompt中准确找到并利用这些信息。它的指令遵循能力也经过优化能更好地理解我们设计的Prompt模板输出我们想要的格式。成本可控对于《红楼梦》这个规模的问答调用次数有限使用qwen-plus的API成本极低甚至可能在新用户赠额范围内。这比自己去部署一个同等能力的开源模型要经济、省事得多。一个重要的心法在项目初期用钱换时间和稳定性是划算的。先用成熟的闭源API快速跑通流程、验证想法等到流程稳定、需求明确后如果成本成为问题再考虑用开源模型进行替换和优化这才是更高效的路径。LangChain的模型接口是抽象的未来切换模型供应商核心代码几乎不用动。3. 环境搭建与数据准备从文本到向量的第一步理论说再多不如动手做。我们首先需要一个干净的环境并把《红楼梦》的原始文本处理成向量数据库能“消化”的格式。3.1 创建虚拟环境与安装依赖强烈建议使用虚拟环境来管理依赖避免包冲突。# 创建并激活虚拟环境 (以conda为例venv同理) conda create -n rag-honglou python3.10 conda activate rag-honglou # 安装核心依赖 pip install langchain0.1.0 langchain-community0.0.10 # ChromaDB及其依赖 pip install chromadb # 用于调用通义千问API pip install dashscope # 用于文档加载处理txt pip install unstructured # 可选但推荐用于更准确的文本分割 pip install tiktoken这里解释一下tiktoken它是OpenAI开源的BPE分词器非常高效。LangChain的RecursiveCharacterTextSplitter可以用它来精确地按token数量分割文本这对于控制上下文长度、优化成本至关重要。即使我们不用OpenAI的模型这个分词器本身也是一个优秀的工具。3.2 获取并清洗《红楼梦》文本你可以在古登堡计划等网站找到《红楼梦》的纯文本版本。下载后通常需要做一些简单的清洗去除无关信息删除文件开头和结尾的版权声明、项目介绍等非正文内容。处理章节标题确保章节标题格式统一例如“第一回 甄士隐梦幻识通灵 贾雨村风尘怀闺秀”。这有助于后续分割时章节信息能作为一个整体被保留。统一换行符将文本中的换行符统一为\n。假设我们清洗后的文件保存为hongloumeng.txt。一个干净的文本是后续高质量向量化的前提。3.3 文档加载与智能分割RAG成败的关键这是整个流程中技术含量最高、最容易被忽视也最容易出问题的环节。很多RAG效果差问题八成出在这里。from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载文档 loader TextLoader(‘./hongloumeng.txt‘, encoding‘utf-8‘) documents loader.load() print(f“原始文档加载完毕共 {len(documents)} 个文档对象。”) # 通常为1 # 2. 配置文本分割器 text_splitter RecursiveCharacterTextSplitter( chunk_size500, # 每个文本块的最大字符数或token数 chunk_overlap50, # 相邻块之间的重叠字符数 length_functionlen, # 计算长度的方法这里用字符数。如果用tiktoken可以换成 tiktoken_len separators[“\n\n“, “\n“, “。“, ““, ““, “ “, ““] # 分割优先级列表 ) # 3. 执行分割 split_docs text_splitter.split_documents(documents) print(f“分割后得到 {len(split_docs)} 个文本块。”)参数选择的艺术与实战经验chunk_size500为什么是500对于中文古典文学《红楼梦》单句信息密度高。如果设置太大如1000一个块里可能包含多个不相关的情节导致检索精度下降设置太小如200可能会把一句完整的诗词或对白切断丢失关键信息。500是一个经过测试的折中点能较好地容纳一个相对完整的情节片段或人物描写。chunk_overlap50重叠是必须的这是为了避免一个关键信息比如一个人名出现在两个块的边界被硬生生切开。50个字符的重叠能确保上下文的连贯性。例如一段描写结束在“黛玉听了不觉”下一段开头是“红了脸”有了重叠就能保证“黛玉听了不觉红了脸”这个完整语义能被检索到。separators列表这个列表定义了分割的“刀”下在哪里。LangChain的分割器会按列表顺序尝试分割。这里我们把双换行\n\n通常表示段落分隔放在最前面其次是单换行、句号等。这意味着它会优先保证段落的完整性其次才是句子。这个顺序对中文文本效果很好。一个我踩过的坑最初我用默认的英文分隔符如\n\n,\n,“, “. “,“,“,“结果发现很多中文句号被忽略导致块过大。**务必根据你的文本语言特性调整separators**。对于中文加入“。”、“”、“”是必要的。执行完这一步你会得到上千个文本块split_docs。每个块都是一个Document对象包含page_content文本内容和metadata元数据如来源属性。接下来我们就要把这些块变成向量。4. 构建向量知识库让ChromaDB记住《红楼梦》有了文本块下一步就是将它们“嵌入”Embedding到高维向量空间并存入ChromaDB。4.1 初始化嵌入模型与向量数据库Chroma的一大便利是内置了嵌入模型。我们使用它默认的all-MiniLM-L6-v2模型这是一个在多语言语料上训练过的轻量级模型对中文有不错的效果。from langchain.vectorstores import Chroma from langchain.embeddings import HuggingFaceEmbeddings import os # 定义持久化路径 PERSIST_DIRECTORY ‘./chroma_honglou_db‘ # 1. 初始化嵌入模型 # 使用HuggingFace上的开源模型 embeddings HuggingFaceEmbeddings( model_name“sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2“, model_kwargs{‘device‘: ‘cpu‘}, # 使用CPU如需GPU可改为 ‘cuda‘ encode_kwargs{‘normalize_embeddings‘: True} # 归一化向量有利于相似度计算 ) # 2. 创建并持久化向量数据库 vectordb Chroma.from_documents( documentssplit_docs, # 我们分割好的文本块列表 embeddingembeddings, # 使用的嵌入模型 persist_directoryPERSIST_DIRECTORY # 指定持久化目录 ) vectordb.persist() # 显式持久化到磁盘 print(f“向量数据库已创建并保存至 {PERSIST_DIRECTORY}”)关键点解析HuggingFaceEmbeddings这里我换成了paraphrase-multilingual-MiniLM-L12-v2它比Chroma默认的模型对中文的支持更好一些。model_kwargs指定模型运行的设备encode_kwargs中的normalize_embeddings设置为True非常重要它会对生成的向量进行归一化处理使得后续的余弦相似度计算更加准确和高效。Chroma.from_documents这个方法一次性完成了三件事将每个文档块通过embeddings模型转换为向量在Chroma中创建一个集合Collection将所有向量和对应的原始文本、元数据存储进去。持久化调用persist()后所有数据会保存到本地目录。下次启动应用时可以直接加载这个目录无需重新生成向量节省大量时间。这个过程可能会花费几分钟取决于你的文本块数量和模型速度。完成后你的目录下会生成一个chroma_honglou_db文件夹里面就是《红楼梦》的向量化知识库了。4.2 验证向量库进行第一次检索测试在接入大模型之前我们先验证一下向量库的检索能力是否正常。# 重新加载持久化的向量数据库 vectordb Chroma( persist_directoryPERSIST_DIRECTORY, embedding_functionembeddings ) # 定义一个简单的检索测试函数 def test_retrieval(query, k3): print(f“\n问题‘{query}‘”) docs vectordb.similarity_search(query, kk) print(f“检索到 {len(docs)} 个相关片段”) for i, doc in enumerate(docs): print(f“\n--- 片段 {i1} (相似度仅供参考) ---”) print(doc.page_content[:200] “...“) # 打印前200字符 print(f“来源: {doc.metadata}”) # 测试几个问题 test_retrieval(“林黛玉第一次进贾府是怎样的场景“) test_retrieval(“‘好了歌‘的内容是什么“) test_retrieval(“贾宝玉的玉上刻了什么字“)运行这段代码你会看到控制台输出与问题最相关的几个文本片段。这是检验你之前文本分割和嵌入模型是否有效的黄金时刻。如何判断检索质量相关性返回的片段是否直接回答了问题例如问“黛玉进府”返回的片段是否确实描述了第三回“接外孙贾母惜孤女”的场景完整性关键信息是否在一个完整的片段内比如“好了歌”的全文是否被完整地检索出来而不是被切成了两半排序性最相关的片段是否排在第一位如果测试结果不理想大概率要回溯调整文本分割器的参数chunk_size,chunk_overlap,separators。这是RAG调优中最重要的一环。5. 接入大模型与构建问答链让qwen-plus“开口说话”知识库准备好了现在需要请出“大脑”——大语言模型qwen-plus并用LangChain的链Chain把检索和生成两个环节无缝衔接起来。5.1 配置通义千问qwen-plus模型首先你需要前往阿里云百炼平台或灵积平台创建一个API-KEY。from langchain.llms import Tongyi import os # 设置你的通义千问API-KEY os.environ[“DASHSCOPE_API_KEY“] “your-api-key-here“ # 替换成你的真实Key # 初始化qwen-plus模型 llm Tongyi( model_name“qwen-plus“, # 指定模型 temperature0.1, # 温度参数控制随机性。越低输出越确定。 top_p0.8, # 核采样参数与temperature配合使用。 streamingFalse, # 是否流式输出调试时可设为False )参数解读temperature在问答任务中我们通常希望答案确定、准确。因此设置为一个较低的值0.1-0.3。如果设得太高如0.9模型可能会开始“编造”一些《红楼梦》里不存在的情节。top_p与temperature一起作用控制采样范围。0.8是一个常用值能在保证一定多样性的同时避免跑偏。5.2 设计Prompt模板告诉模型如何“答题”直接给模型“问题”和“上下文”它可能不知道该如何组织答案。我们需要一个清晰的指令Prompt Template来引导它。from langchain.prompts import PromptTemplate # 定义一个针对我们问答任务的Prompt模板 prompt_template “““请根据以下提供的《红楼梦》相关上下文片段回答用户的问题。 如果你在提供的上下文中找不到明确答案请直接说“根据已知信息无法回答该问题”不要编造答案。 上下文 {context} 问题{question} 请给出准确、简洁的答案””” PROMPT PromptTemplate( templateprompt_template, input_variables[“context“, “question“] )这个模板有几个设计要点明确指令开头就告诉模型任务是什么。设定边界强调“根据上下文”并明确要求对于未知信息说“无法回答”。这是控制模型幻觉Hallucination的关键一步。没有这个约束模型很可能会用它的内部知识可能不准确或与本书不符来编造答案。结构化输入用{context}和{question}作为占位符LangChain会在运行时自动填充。要求简洁要求“准确、简洁”避免模型生成冗长的无关内容。5.3 组装RetrievalQA链完成最后拼图现在把向量数据库检索器、Prompt模板和大模型组装成一条自动化流水线。from langchain.chains import RetrievalQA # 从向量数据库创建检索器 retriever vectordb.as_retriever( search_type“similarity“, # 使用相似度搜索 search_kwargs{“k“: 4} # 每次检索返回4个最相关的片段 ) # 创建RetrievalQA链 qa_chain RetrievalQA.from_chain_type( llmllm, chain_type“stuff“, # 最常用的类型将所有检索到的上下文“塞”进Prompt retrieverretriever, chain_type_kwargs{“prompt“: PROMPT}, # 使用我们自定义的Prompt return_source_documentsTrue # 非常重要返回检索到的源文档便于调试 )关键参数解释chain_type“stuff“这是最简单直接的方式将所有检索到的文档片段合并成一个长字符串放入Prompt的{context}中。它的优点是简单缺点是有上下文长度限制取决于模型。对于qwen-plus的128K上下文和我们的4个片段完全够用。其他还有map_reduce、refine等复杂类型适用于文档极多或需要摘要的场景但复杂度高本例不需要。return_source_documentsTrue务必设置为True。这能让链在返回答案的同时也返回它参考了哪些原文片段。这是后期调试和优化的生命线。当你发现答案不对时可以立刻查看模型到底“看”到了什么材料从而判断是检索出了问题还是模型理解出了问题。6. 实战问答与深度调试从“能用”到“好用”激动人心的时刻到了让我们问几个问题看看这个亲手搭建的系统表现如何。# 定义一个漂亮的问答函数 def ask_question(question): print(f“\n 用户问题{question}”) print(“-“ * 50) result qa_chain({“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字 # 开始提问 ask_question(“贾宝玉和林黛玉是什么关系“) ask_question(“‘金陵十二钗‘正册里都有谁“) ask_question(“薛宝钗吃的‘冷香丸‘是怎么制作的“) ask_question(“贾府最后为什么被抄家了“)运行后你应该能看到模型生成的答案以及它背后参考的原文。现在才是真正工作的开始——调试与优化。你可能会遇到以下几种典型情况6.1 情况一答案不准确或未命中问题问“冷香丸配方”答案却只说了“薛宝钗从胎里带来一股热毒”没提具体的药材和制作工艺。排查立刻看source_documents。如果检索到的片段里根本没有配方细节那就是检索环节的问题。解决方案调整检索数量将search_kwargs{“k“: 4}中的k调大比如到6或8增加命中概率。优化检索方式search_type可以尝试从“similarity“相似度改为“mmr“最大边际相关性。后者会在考虑相关性的同时尽量让返回的片段多样性更高避免内容冗余。retriever vectordb.as_retriever( search_type“mmr“, search_kwargs{“k“: 6, “fetch_k“: 20, “lambda_mult“: 0.5} )回溯检查分割如果调整检索后依然找不到很可能配方描述被你的chunk_size切碎了。你需要回到第3步检查相关章节的原文调整分割策略。对于“冷香丸”这种配方描述可能需要更小的chunk_size或不同的separators来保证其完整性。6.2 情况二答案包含幻觉或无关信息问题回答“贾府被抄家的原因”时模型开始自由发挥扯上一些历史背景或自己推断的原因。排查查看source_documents如果提供的片段里没有明确原因但模型却给出了答案这就是模型幻觉。解决方案强化Prompt指令在Prompt模板中把“不要编造答案”的警告写得更严厉、更具体。例如“你必须严格依据上下文作答。上下文未提及的信息一律视为未知绝对不允许自行推断或添加任何外部知识。”降低模型“创造力”将temperature参数进一步调低比如到0.01让模型输出更加保守和确定。后处理过滤在代码层面对答案进行校验如果答案中出现“可能”、“我觉得”、“历史上”等词汇或者与源文档相关性极低可以触发一个重答或提示“信息不足”。6.3 情况三答案冗长或格式不佳问题答案把检索到的几个片段内容几乎原样复述了一遍没有提炼。解决方案优化Prompt在Prompt中明确要求“用简洁的语言概括”、“直接回答核心问题”、“分点列出”。尝试不同的chain_type虽然stuff简单但refine链类型可以让模型迭代式地精炼答案。不过这会增加调用次数和复杂度对于简单问答不一定必要。模型层面控制除了temperature还可以调整max_tokens来限制答案的最大长度。一个重要的调试习惯始终打开return_source_documentsTrue。任何一次错误的回答都不要直接去修改模型参数或Prompt而是先看源文档。90%的问题出在检索向量化环节而不是生成环节。7. 进阶优化与扩展思路当基础问答跑通后我们可以考虑让这个系统变得更强大、更智能。这里分享几个有明确提升效果的进阶方向。7.1 为文档块添加元数据Metadata我们之前分割的文档块丢失了重要的章节信息。添加上下文元数据能极大提升检索质量和答案的可解释性。# 假设我们在分割时能够知道每个块属于第几回可以通过解析原文标题实现 # 这里演示如何后补元数据实际应在分割时处理 for i, doc in enumerate(split_docs): # 这里需要根据你的文本解析逻辑来填充此处为示例 doc.metadata {“source“: “hongloumeng.txt“, “chapter“: “第五回“} # 然后使用带元数据的docs创建向量库 vectordb Chroma.from_documents( documentssplit_docs_with_metadata, # 带元数据的文档 embeddingembeddings, persist_directoryPERSIST_DIRECTORY )在检索时元数据可以作为过滤器Filter使用。例如用户可以问“在‘宝玉挨打’那一回里贾政说了什么”。我们可以先检索“宝玉挨打”相关的回目元数据再在该范围内进行相似度搜索结果会精准得多。7.2 实现多路召回与重排序Rerank这是工业级RAG系统的常见优化手段。核心思想是先用简单的检索方法如关键词BM25快速召回一批候选文档再用更精细的模型交叉编码器对它们进行重排序最后把最相关的几篇交给大模型。# 伪代码思路 from rank_bm25 import BM25Okapi from sentence_transformers import CrossEncoder # 1. 关键词召回 (BM25) bm25_index BM25Okapi([doc.page_content for doc in split_docs]) keyword_candidates bm25_index.get_top_n(question, split_docs, n20) # 2. 向量召回 vector_candidates vectordb.similarity_search(question, k20) # 3. 合并去重 all_candidates merge_and_deduplicate(keyword_candidates, vector_candidates) # 4. 使用交叉编码器重排序 model CrossEncoder(‘cross-encoder/ms-marco-MiniLM-L-6-v2‘) pairs [[question, doc.page_content] for doc in all_candidates] scores model.predict(pairs) # 根据scores对all_candidates排序 # 5. 取Top-K作为最终上下文 final_context top_k_reranked_candidates这种方法结合了关键词匹配的“字面”相关性和向量匹配的“语义”相关性能显著提升召回文档的质量。LangChain社区也有相关的ContextualCompressionRetriever等组件可以探索。7.3 构建Web应用界面一个命令行工具毕竟不方便。我们可以用Gradio或Streamlit快速构建一个Web界面。# 使用Gradio的示例 import gradio as gr def answer_question(history, message): # history是对话历史这里我们实现单轮问答 result qa_chain({“query“: message}) answer result[‘result‘] # 可以附上参考来源 sources “\n”.join([f“- {doc.page_content[:100]}...“ for doc in result[‘source_documents‘]]) full_response f“{answer}\n\n**参考来源**\n{sources}” return full_response # 创建界面 demo gr.ChatInterface( fnanswer_question, title“《红楼梦》智能问答助手“, description“基于RAG技术构建知识来源于《红楼梦》全文。请提问吧“, ) if __name__ “__main__“: demo.launch(shareTrue) # shareTrue会生成一个临时公网链接运行这段代码一个拥有聊天界面的Web应用就启动了。你可以把它分享给朋友让他们也来体验一下。从一行代码没有到拥有一个能回答《红楼梦》问题的智能应用这个过程本身就是一个完整的“从0到1”的项目实践。它涉及了数据处理、算法应用、系统集成和交互设计。更重要的是你亲手摸清了RAG每一个环节的“脾气”知道了问题可能出在哪里以及如何去优化。这套方法论和工具链完全可以平移到你自己的专业文档、公司知识库、甚至是个人笔记的管理上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价