news 2026/5/29 0:20:02

【手把手RAG搭建】从零手搓本地知识库(第四篇):基于 LCEL 的检索增强生成(RAG)核心引擎

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【手把手RAG搭建】从零手搓本地知识库(第四篇):基于 LCEL 的检索增强生成(RAG)核心引擎

太棒了!前三篇已经完成了“数据清洗”、“语义切片”和“向量检索”这三个地基工作。到了第四篇,我们需要把前面所有的模块串联起来,并引入大语言模型(LLM),这才是 RAG 系统的“灵魂”所在。

这一篇的核心任务是:教会程序如何利用检索到的上下文,让大模型生成精准答案,而不是胡说八道。

结合你要求的“纯CPU/本地化”背景,以及前三篇使用的bge-small-zhChromaDB,我为你撰写了第四篇博客的完整内容。


导读:前三步我们让电脑“读”懂了文档,但如果它“说”出来的话全是胡编乱造,那这个知识库依然是一堆废铁。这是《从零构建个人知识库》系列的第四篇。今天我们将彻底解决大模型的“幻觉”顽疾,利用LCEL (LangChain Expression Language)将检索到的上下文注入到 Prompt 中,打造一个只基于你提供的文档进行回答的“严谨”AI。全程适配纯 CPU 环境,代码极简且性能高效。

为什么你的 AI 总是满嘴跑火车?

当你直接问大模型“如何配置本地开发环境?”时,它会根据预训练时学到的通用知识来回答。这导致了两个问题:

  1. 幻觉(Hallucination):如果文档里没有确切答案,它会自信地编造一个看似合理但完全错误的答案。
  2. 上下文缺失:它不知道你刚刚上传的那份《内部开发手册》才是唯一的“标准答案”。

我们的解法RAG(检索增强生成)。即:用户提问 -> 检索相关文档片段 -> 将片段作为“参考书”塞进 Prompt -> 模型生成回答。这就像给大模型配了一个“随身搜索引擎”。

第一步:引入 CPU 友好的本地 LLM

既然是纯本地运行,我们就不能调用 OpenAI 的 API。我们需要一个能在 CPU 上运行的开源模型。

模型选型建议
虽然前三篇我们用了bge-small做检索,但生成回答需要更大的模型。为了保证 CPU 环境下的推理速度,推荐使用GGUF 格式的量化模型。

  • 推荐模型Qwen1.5-1.8B-Chat-GGUFPhi-3-mini-4k-instruct
  • 原因:1.8B 参数级别的模型在 4bit 量化后,仅需不到 1GB 内存,普通电脑 CPU 推理速度在可接受范围内(约 5-10秒/问),且支持 32k 上下文。

在项目根目录创建src/llm.py

importosfromlangchain_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请检查模型路径。")llm=ChatLlamaCpp(# 初始化模型 ChatLlamaCpp 实例model_path=model_path,n_ctx=4096,# 上下文长度n_batch=512,# 批处理大小n_threads=4,# CPU 线程数 (请根据你的电脑核心数调整)verbose=False,# 关闭详细日志以减少干扰#temperature=0.1, # 严谨模式temperature=0.5,# 稍微提高温度,增加回答的自然度#max_tokens=1024, #max_tokens=512,# 减少最大生成长度,加快响应top_k=40,# 采样时考虑的词元数量top_p=0.9,# 核采样概率repeat_penalty=1.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_TEMPLATE=ChatPromptTemplate.from_messages([("system",SYSTEM_PROMPT),("human","{question}")])# 相似度过滤阈值(L2 距离,越小越相似;bge 归一化后范围 0~2,一般 1.2~1.5 为宜)MIN_SCORE_THRESHOLD=1.5# 拒绝回答的固定消息(知识库无相关内容时使用)REFUSE_MSG="抱歉,我在本地知识库中没有找到相关信息,无法回答这个问题。"# --- 3. 初始化组件 ---llm=get_cpu_llm()vectorstore=get_vector_store()# --- 4. 工具函数 ---defretrieve_with_filter(query:str,min_score:float=None)->str:""" 带相似度过滤的检索器(返回格式化字符串,供外部调用) Args: query: 检索查询文本 min_score: 最小相似度阈值,None 时使用默认值 MIN_SCORE_THRESHOLD Returns: 高于相似度阈值的文档内容拼接字符串,无相关文档时返回"【无相关上下文】" """ifmin_scoreisNone:min_score=MIN_SCORE_THRESHOLD results=vectorstore.similarity_search_with_score(query,k=3)# --- 调试输出:展示检索结果和相似度 ---print("\n📋 【知识库检索结果】")fori,(doc,score)inenumerate(results,1):source=doc.metadata.get("source","Unknown")print(f" [{i}] 距离:{score:.4f}| 来源:{source}")print(f" 内容预览:{doc.page_content[:80]}...")# 过滤低相关性的结果(ChromaDB 使用 L2 距离,越小越相似)relevant_docs=[docfordoc,scoreinresultsifscore<min_score]ifnotrelevant_docs:print(f" ➜ 所有结果距离均 >={min_score},判定为【无相关上下文】")return"【无相关上下文】"print(f" ➜ 过滤后保留{len(relevant_docs)}条相关文档")fordoc,scoreinresults:ifscore<min_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 使用 """results=vectorstore.similarity_search_with_score(query,k=3)# --- 调试输出:展示检索结果和相似度 ---print("\n📋 【知识库检索结果】")fori,(doc,score)inenumerate(results,1):source=doc.metadata.get("source","Unknown")print(f" [{i}] 距离:{score:.4f}| 来源:{source}")print(f" 内容预览:{doc.page_content[:80]}...")relevant_docs=[docfordoc,scoreinresultsifscore<MIN_SCORE_THRESHOLD]ifnotrelevant_docs:print(f" ➜ 所有结果距离均 >={MIN_SCORE_THRESHOLD},判定为【无相关上下文】")else:print(f" ➜ 过滤后保留{len(relevant_docs)}条相关文档")fordoc,scoreinresults:ifscore<MIN_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.py

importos os.environ["LANGCHAIN_TRACING_V2"]="false"# 关闭 LangSmith 遥测,避免超时fromsrc.rag_chainimportrag_chaindefmain():print("欢迎使用本地知识库助手!(LCEL 链模式)")print("输入 'quit' 或 'exit' 退出程序。\n")whileTrue:user_query=input("你: ").strip()ifuser_query.lower()in['quit','exit','退出']:print("再见!")breakifnotuser_query:print("请输入有效的问题。\n")continueprint("AI 正在思考中,请稍候...")try:response=rag_chain.invoke(user_query)print(f"AI 答:{response}\n")exceptExceptionase:print(f"程序出错:{e}\n")if__name__=="__main__":os.environ['HF_ENDPOINT']='https://hf-mirror.com'main()

预期运行结果

当你运行这段代码时,控制台会输出类似以下内容(取决于你的文档内容):

LLM 模型加载成功 Loading weights: 100%|██████████| 71/71 [00:00<00: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 系统不再只能在黑框里跑,而是变成一个真正可用的网页应用。点击关注,更新时第一时间收到通知,带你一步步把这个项目真正跑在你的电脑上!
``

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/29 0:14:19

MHMarkets迈汇:“油价回落考验供应预期”

MHMarkets迈汇&#xff1a;“油价回落考验供应预期”国际油价周三快速下行&#xff0c;市场关注美国与伊朗谈判是否可能推动霍尔木兹海峡运输恢复&#xff0c;MHMarkets迈汇表示&#xff0c;油价短线反应显示地缘风险溢价正在被重新评估。报道显示&#xff0c;布伦特原油一度跌…

作者头像 李华
网站建设 2026/5/29 0:12:29

终极指南:Detect It Easy跨平台文件类型检测工具深度解析

终极指南&#xff1a;Detect It Easy跨平台文件类型检测工具深度解析 【免费下载链接】Detect-It-Easy Program for determining types of files for Windows, Linux and MacOS. 项目地址: https://gitcode.com/gh_mirrors/de/Detect-It-Easy 在恶意软件分析、数字取证和…

作者头像 李华
网站建设 2026/5/29 0:03:42

流式输出(Streaming):为你的自研 AI 平台添加类似打字机的体验

一、引言:为什么你的 AI 产品需要流式输出 想象一个场景:用户在你的自研 AI 平台上提了一个问题,然后盯着空白页面等了整整 15 秒,最后屏幕上突然“刷”地一下弹出了 800 字的完整回答。与此同时,你的竞品接入了 ChatGPT 的流式输出——用户敲下回车的瞬间,第一个字就出…

作者头像 李华