RAG从入门到精通LangChainChroma构建知识库问答系统根治模型幻觉一、幻觉之困与破局之道大模型好用但有个老毛病爱编。你问它“公司去年的营收是多少”它能一本正经地报出个数字听起来还挺像那么回事但你去翻财报就会发现全是假的。让它总结一篇内部会议纪要它也能凭空加出几条根本没讨论过的决议。这种“看起来合理实际上胡扯”的现象就是模型幻觉。原因也不复杂模型的知识是训练时学的不仅有个截止日期更不可能知道你们公司的私有文档和实时数据。于是 RAG检索增强生成出来了思路很直接给模型挂个外置知识库。每次提问先去库里搜出最相关的几段资料把这些资料连同问题一起丢给模型还要在提示词里强调——“只准看这些资料回答不知道就说不知道”。这样一来答案就不再依赖模型的记忆而是落到真实的文档上幻觉从根儿上就被掐断了。我写这篇文章就是想带着你用 LangChain 和 Chroma 从头搭一套能跑起来的知识库问答系统把文档加载、分割、向量化、检索、生成这五个环节都过一遍整个过程都有代码拿来就能用。二、知识地基文档加载与语义切分先装依赖Python 3.9 以上一行搞定pipinstalllangchain langchain-openai langchain-community langchain-chroma chromadb openai tiktoken pypdf unstructured sentence-transformers这里每个包都有用langchain是流程编排的核心langchain-openai和langchain-community负责模型和加载器langchain-chroma专门对接向量库chromadb是底层数据库pypdf和unstructured拿来读 PDF、TXT 等各种格式sentence-transformers则是为了后面本地运行嵌入模型准备的。当然还得准备一个 OpenAI API Key设成环境变量importos os.environ[OPENAI_API_KEY]你的api-key加载与切分文档我们假设手头有一个 PDF 产品手册和一个 TXT 技术白皮书。先用加载器把它们读进来fromlangchain_community.document_loadersimportPyPDFLoader,TextLoader pdf_loaderPyPDFLoader(documents/产品手册.pdf)txt_loaderTextLoader(documents/技术白皮书.txt,encodingutf-8)all_docspdf_loader.load()txt_loader.load()print(f共加载{len(all_docs)}个页面)文档读进来后不能整篇整篇地往向量库里扔那样检索精度会很差。我们需要把长文档切成小段每一段控制在几百字同时段与段之间留一点重叠这样不会把一个完整的意思给拦腰切断。RecursiveCharacterTextSplitter会按段落、句子、标点的优先级来切尽量保持语义完整fromlangchain.text_splitterimportRecursiveCharacterTextSplitter text_splitterRecursiveCharacterTextSplitter(chunk_size500,chunk_overlap50,separators[\n\n,\n,。,,,,, ,])splitstext_splitter.split_documents(all_docs)print(f分割后得到{len(splits)}个文本块)chunk_size我一般针对中文设 300 到 800这里取 500 比较折中chunk_overlap设为 10%~20% 的块大小能有效避免关键信息被切散。三、向量之门嵌入、存储与智能检索文本切好之后下一步就是向量化。说白了就是把每个片段转化成一串数字语义越接近的片段它们在向量空间里的距离就越近。这里你可以选择用 OpenAI 的在线嵌入服务效果不错就是得联网也可以选 HuggingFace 的本地模型完全免费而且数据不出本机。在线方案fromlangchain_openaiimportOpenAIEmbeddings embeddingsOpenAIEmbeddings(modeltext-embedding-3-small)本地方案我比较喜欢这个fromlangchain_community.embeddingsimportHuggingFaceEmbeddings embeddingsHuggingFaceEmbeddings(model_nameshibing624/text2vec-base-chinese)有了嵌入模型我们就可以把所有文本块转成向量存进 Chroma 向量数据库并且持久化到本地目录下次直接加载不用重复处理。fromlangchain_chromaimportChroma persist_directorychroma_dbvectordbChroma.from_documents(documentssplits,embeddingembeddings,persist_directorypersist_directory)print(f向量库已保存至{persist_directory}共{vectordb._collection.count()}条)以后启动的时候这样加载就好vectordbChroma(persist_directorypersist_directory,embedding_functionembeddings)配置检索器向量库里有了数据就要考虑怎么搜。Chroma 默认算余弦相似度但我们可以在检索时加一点策略比如用 MMR最大边际相关性算法让搜出来的几个片段既相关彼此之间又有一些差异性避免返回一堆意思重复的内容。retrievervectordb.as_retriever(search_typemmr,search_kwargs{k:4,fetch_k:10,lambda_mult:0.7})这里k4表示最终拿给模型参考的片段数fetch_k10是先从库里拉出 10 条再由 MMR 挑选出 4 条。lambda_mult越接近 1越看重相关性越接近 0越看重多样性。你可以根据实际效果调整。四、思维约束构建防幻觉的问答引擎现在有了检索器差一个生成答案的大模型以及一个能死死管住模型的提示词。提示词在这里是“根治幻觉”的关键我会在提示里明确告诉模型只准用参考资料里的信息不知道就直接说不知道千万别自己发挥。fromlangchain_openaiimportChatOpenAIfromlangchain.promptsimportPromptTemplatefromlangchain.chainsimportRetrievalQA llmChatOpenAI(modelgpt-3.5-turbo,temperature0.1,max_tokens1024)template你是一个严谨的知识库助手请严格根据以下规则回答问题 1. 只能使用下面“参考资料”中的信息。 2. 若资料中没有相关信息请直接说“根据现有资料无法回答”不得编造或猜测。 3. 回答时尽量引用原文细节保持条理清晰。 参考资料 {context} 用户问题{question} 最终回答QA_PROMPTPromptTemplate(templatetemplate,input_variables[context,question])qa_chainRetrievalQA.from_chain_type(llmllm,chain_typestuff,retrieverretriever,chain_type_kwargs{prompt:QA_PROMPT},return_source_documentsTrue)我用的是stuff方式也就是直接把所有搜出来的片段拼进提示词里。只要片段总长没超过模型的上下文窗口这种方式最简单直接。return_source_documentsTrue还会把原始片段返回出来方便你检查答案的来源。来试几个问题defask(question):resultqa_chain.invoke({query:question})answerresult[result]sourcesresult[source_documents]print(f 回答{answer}\n)print( 参考来源)fori,docinenumerate(sources,1):print(f [{i}]{doc.page_content[:120]}...)returnanswer ask(公司的售后服务政策是什么)ask(产品Pro版支持多少并发用户)ask(今天天气怎么样)# 知识库里肯定没有看它会不会乱编对于前两个问题模型会老老实实从参考资料里找答案第三个问题它则会回复“根据现有资料无法回答”。这说明我们预设的规则生效了没有发生幻觉而且每个答案都能追溯到具体的文档片段。五、演进之路从可用到好用的系统淬炼一套基础 RAG 系统跑通后实际落地的时候我们还会在很多地方下功夫。文档处理这边用UnstructuredFileLoader可以一口气支持 PPT、Excel甚至图片 OCR切分的时候也不一定死守固定字数试试MarkdownHeaderTextSplitter按标题层级来切让每个文本块都自带章节信息检索的时候会准很多。检索质量这块纯粹的向量检索有时会漏掉精确的关键词匹配我习惯引入 BM25 这类稀疏检索算法把稠密向量和稀疏关键词的搜索结果加权融合效果往往更好。另外用户问句通常比较口语化可以在检索前先用一个 LLM 把问题改写得更贴近文档的行文风格再拿改写后的句子去搜命中率明显提升。还有一个小技巧就是给文档块打上来源、日期之类的元数据检索的时候加个过滤条件直接缩窄范围。生成与评估也不能忽视。高频相似问题可以缓存检索结果比如用 Redis既降低延迟又省钱面向用户输出时打开流式输出体验会丝滑很多。上线之后一定要做评估用 RAGAS 这类框架自动检测答案的忠实度、相关性把每次返回的文档和用户反馈记下来慢慢形成优化闭环。如果对数据安全有要求我会用 Ollama 部署本地模型比如 Llama 3 或 Qwen整个流程完全在本地跑稳当又放心。好了整个搭建过程就是这样。我们一路从文档加载、切分、向量化、检索到提示词约束和答案生成走完了一个能实际运作的 RAG 系统全流程。下一步你可以把它封装成 FastAPI 服务接到飞书或者钉钉上甚至配合 Agent 做一些自动化的复杂任务。RAG 不只是个技术组合它是一套“让模型说真话”的思路这个思路在你手里会越用越顺。完整代码我已经整理成可运行的脚本需要的话可以在评论区告诉我也欢迎关注我我会持续分享大模型落地过程中的实战心得。