太棒了前三篇已经完成了“数据清洗”、“语义切片”和“向量检索”这三个地基工作。到了第四篇我们需要把前面所有的模块串联起来并引入大语言模型LLM这才是 RAG 系统的“灵魂”所在。这一篇的核心任务是教会程序如何利用检索到的上下文让大模型生成精准答案而不是胡说八道。结合你要求的“纯CPU/本地化”背景以及前三篇使用的bge-small-zh和ChromaDB我为你撰写了第四篇博客的完整内容。导读前三步我们让电脑“读”懂了文档但如果它“说”出来的话全是胡编乱造那这个知识库依然是一堆废铁。这是《从零构建个人知识库》系列的第四篇。今天我们将彻底解决大模型的“幻觉”顽疾利用LCEL (LangChain Expression Language)将检索到的上下文注入到 Prompt 中打造一个只基于你提供的文档进行回答的“严谨”AI。全程适配纯 CPU 环境代码极简且性能高效。为什么你的 AI 总是满嘴跑火车当你直接问大模型“如何配置本地开发环境”时它会根据预训练时学到的通用知识来回答。这导致了两个问题幻觉Hallucination如果文档里没有确切答案它会自信地编造一个看似合理但完全错误的答案。上下文缺失它不知道你刚刚上传的那份《内部开发手册》才是唯一的“标准答案”。我们的解法RAG检索增强生成。即用户提问 - 检索相关文档片段 - 将片段作为“参考书”塞进 Prompt - 模型生成回答。这就像给大模型配了一个“随身搜索引擎”。第一步引入 CPU 友好的本地 LLM既然是纯本地运行我们就不能调用 OpenAI 的 API。我们需要一个能在 CPU 上运行的开源模型。模型选型建议虽然前三篇我们用了bge-small做检索但生成回答需要更大的模型。为了保证 CPU 环境下的推理速度推荐使用GGUF 格式的量化模型。推荐模型Qwen1.5-1.8B-Chat-GGUF或Phi-3-mini-4k-instruct。原因1.8B 参数级别的模型在 4bit 量化后仅需不到 1GB 内存普通电脑 CPU 推理速度在可接受范围内约 5-10秒/问且支持 32k 上下文。在项目根目录创建src/llm.pyimportosfromlangchain_community.chat_modelsimportChatLlamaCppdefget_cpu_llm(model_path:str./models/Qwen1.5-1.8B-Chat-GGUF/qwen1_5-1_8b-chat-q4_k_m.gguf): 初始化 CPU 专用的本地 LLM 注意请确保模型文件已下载到指定路径。 推荐模型Qwen1.5-1.8B-Chat-GGUF (4-bit 量化) ifnotos.path.exists(model_path):raiseFileNotFoundError(f 模型文件未找到:{model_path}\n请检查模型路径。)llmChatLlamaCpp(# 初始化模型 ChatLlamaCpp 实例model_pathmodel_path,n_ctx4096,# 上下文长度n_batch512,# 批处理大小n_threads4,# CPU 线程数 (请根据你的电脑核心数调整)verboseFalse,# 关闭详细日志以减少干扰#temperature0.1, # 严谨模式temperature0.5,# 稍微提高温度增加回答的自然度#max_tokens1024, #max_tokens512,# 减少最大生成长度加快响应top_k40,# 采样时考虑的词元数量top_p0.9,# 核采样概率repeat_penalty1.1,# 重复惩罚)print( LLM 模型加载成功)returnllm下载提示请前往 HuggingFace 或 ModelScope 下载Qwen1.5-1.8B-Chat的 GGUF 版本文件名通常包含q4_k_m并解压放入项目根目录的models文件夹中。第二步告别旧版 Chain拥抱 LCEL 流水线很多旧教程还在用RetrievalQA这种封装好的黑盒类。强烈建议大家使用LCEL (LangChain Expression Language)。它是 LangChain 新版的官方标准代码更清晰支持流式输出打字机效果且调试极其方便。我们在src/rag_chain.py中构建这个流水线fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.runnablesimportRunnablePassthrough,RunnableLambdafromlangchain_core.output_parsersimportStrOutputParser# --- 1. 导入组件 ---from.vectorstoreimportget_vector_storefrom.llmimportget_cpu_llm# --- 2. 定义 Prompt 模板 ---SYSTEM_PROMPT你是一个严格的知识库问答助手。你只能根据下方提供的【参考上下文】来回答问题。 【绝对禁止】 - 禁止使用你自身的知识、记忆或常识来回答问题 - 禁止推测、联想或补充任何【参考上下文】中没有明确提到的信息 - 如果【参考上下文】为空、标记为无相关上下文、或与问题无关你必须且只能回答 抱歉我在本地知识库中没有找到相关信息无法回答这个问题。 【参考上下文】 {context}PROMPT_TEMPLATEChatPromptTemplate.from_messages([(system,SYSTEM_PROMPT),(human,{question})])# 相似度过滤阈值L2 距离越小越相似bge 归一化后范围 0~2一般 1.2~1.5 为宜MIN_SCORE_THRESHOLD1.5# 拒绝回答的固定消息知识库无相关内容时使用REFUSE_MSG抱歉我在本地知识库中没有找到相关信息无法回答这个问题。# --- 3. 初始化组件 ---llmget_cpu_llm()vectorstoreget_vector_store()# --- 4. 工具函数 ---defretrieve_with_filter(query:str,min_score:floatNone)-str: 带相似度过滤的检索器返回格式化字符串供外部调用 Args: query: 检索查询文本 min_score: 最小相似度阈值None 时使用默认值 MIN_SCORE_THRESHOLD Returns: 高于相似度阈值的文档内容拼接字符串无相关文档时返回【无相关上下文】 ifmin_scoreisNone:min_scoreMIN_SCORE_THRESHOLD resultsvectorstore.similarity_search_with_score(query,k3)# --- 调试输出展示检索结果和相似度 ---print(\n 【知识库检索结果】)fori,(doc,score)inenumerate(results,1):sourcedoc.metadata.get(source,Unknown)print(f [{i}] 距离:{score:.4f}| 来源:{source})print(f 内容预览:{doc.page_content[:80]}...)# 过滤低相关性的结果ChromaDB 使用 L2 距离越小越相似relevant_docs[docfordoc,scoreinresultsifscoremin_score]ifnotrelevant_docs:print(f ➜ 所有结果距离均 {min_score}判定为【无相关上下文】)return【无相关上下文】print(f ➜ 过滤后保留{len(relevant_docs)}条相关文档)fordoc,scoreinresults:ifscoremin_score:print(f ✅ 距离{score:.4f}:{doc.page_content[:60]}...)# 合并多个文档内容context_str\n\n.join([doc.page_contentfordocinrelevant_docs])returncontext_strdef_retrieve_docs(query:str)-list: 内部检索函数带相似度过滤返回 Document 列表供 chain 使用 resultsvectorstore.similarity_search_with_score(query,k3)# --- 调试输出展示检索结果和相似度 ---print(\n 【知识库检索结果】)fori,(doc,score)inenumerate(results,1):sourcedoc.metadata.get(source,Unknown)print(f [{i}] 距离:{score:.4f}| 来源:{source})print(f 内容预览:{doc.page_content[:80]}...)relevant_docs[docfordoc,scoreinresultsifscoreMIN_SCORE_THRESHOLD]ifnotrelevant_docs:print(f ➜ 所有结果距离均 {MIN_SCORE_THRESHOLD}判定为【无相关上下文】)else:print(f ➜ 过滤后保留{len(relevant_docs)}条相关文档)fordoc,scoreinresults:ifscoreMIN_SCORE_THRESHOLD:print(f ✅ 距离{score:.4f}:{doc.page_content[:60]}...)returnrelevant_docs# --- 5. 构建 LCEL 流水线 ---defformat_docs(docs):将检索到的文档格式化为字符串若无相关文档则返回提示ifnotdocs:return【无相关上下文】return\n\n.join([doc.page_contentfordocindocs])rag_chain({context:RunnableLambda(_retrieve_docs)|RunnableLambda(format_docs),question:RunnablePassthrough()}|PROMPT_TEMPLATE|llm|StrOutputParser())print( RAG 核心引擎已组装完毕)代码解析LCEL 的魔法RunnablePassthrough()它像一个透明的管道把用户的问题原封不动地传给 LLM同时允许我们在这个过程中并行处理 Context。StrOutputParser()它负责把模型输出的乱七八糟的对象包含 token id 等清洗成人类可读的字符串。第三步编写测试脚本见证“检索增强”的威力最后我们在main.py中调用这个流水线看看它是否学会了“不懂就问懂就答”。更新main.pyimportos os.environ[LANGCHAIN_TRACING_V2]false# 关闭 LangSmith 遥测避免超时fromsrc.rag_chainimportrag_chaindefmain():print(欢迎使用本地知识库助手LCEL 链模式)print(输入 quit 或 exit 退出程序。\n)whileTrue:user_queryinput(你: ).strip()ifuser_query.lower()in[quit,exit,退出]:print(再见)breakifnotuser_query:print(请输入有效的问题。\n)continueprint(AI 正在思考中请稍候...)try:responserag_chain.invoke(user_query)print(fAI 答:{response}\n)exceptExceptionase:print(f程序出错:{e}\n)if__name____main__:os.environ[HF_ENDPOINT]https://hf-mirror.commain()预期运行结果当你运行这段代码时控制台会输出类似以下内容取决于你的文档内容LLM 模型加载成功 Loading weights: 100%|██████████| 71/71 [00:0000:00, 6163.37it/s] 正在连接/创建本地向量库: ./chroma_db RAG 核心引擎已组装完毕 输入 quit 或 exit 退出程序。 你: 药品副作用是什么 AI 正在思考中请稍候... 【知识库检索结果】 [1] 距离: 0.8887 | 来源: ./data/test_doc.md 内容预览: 。 5、对噻嗪、磺胺过敏者慎用。 6、对单胺氧化酶抑制剂和嗜铬细胞瘤引起的高血压无效。 药理作用 能松弛血管平滑肌降低周围血管阻力使血压急剧下降。一次快速 ... [2] 距离: 0.9271 | 来源: ./data/test_doc.md 内容预览: 药品名称二氮嗪注射液华润双鹤 Diazoxide Injection 请仔细阅读说明书并在医生的指导下使用 成份 二氮嗪。 规格 10 ml:0.15g ... ➜ 过滤后保留 2 条相关文档 距离 0.8887: 。 5、对噻嗪、磺胺过敏者慎用。 6、对单胺氧化酶抑制剂和嗜铬细胞瘤引起的高血压无效。 药理作用 能松弛血管平滑肌降低... 距离 0.9271: 药品名称二氮嗪注射液华润双鹤 Diazoxide Injection 请仔细阅读说明书并在医生的指导下使用 成份 ... AI 答: 药品副作用是指在正常剂量下某些药物或其代谢产物可能产生与用药目的不相符的药理效应从而给患者带来不同程度的身体不适、生活质量下降甚至生命危险等严重后果。总结与进阶恭喜你你已经成功构建了一个具备防幻觉能力的 RAG 系统。在这个架构下模型的知识边界被严格限制在了你提供的文档之内这正是企业级知识库的基石。性能优化建议针对 CPU 用户流式输出目前的代码是等待模型生成完整答案后才打印。在下一篇 Web 界面开发中我会教你如何利用 LCEL 的stream()方法实现“打字机”效果让用户体验更好。模型切换如果你觉得 1.8B 模型回答太慢可以尝试更小的Phi-3-mini如果觉得智商不够可以尝试Qwen-7B的 GGUF 4bit 版本需要 8GB 内存。系列预告这是《从零构建个人知识库》系列的第四篇下一篇我将详细讲解如何使用 Streamlit 快速构建可视化 Web 界面让你的 RAG 系统不再只能在黑框里跑而是变成一个真正可用的网页应用。点击关注更新时第一时间收到通知带你一步步把这个项目真正跑在你的电脑上