第四章 LangChain应用级系统设计与RAG实践 前言 从这一章开始,我们就要从LangChain的基础用法正式“升级打怪”,进入应用级系统设计的领域啦! 如果说之前的学习是在“认识零件”,那这一章就是教大家如何把这些零件组装成能解决实际问题的“智能机器”。 核心重点是两个:链式工作流(把复杂任务拆成流水线)和RAG(给大模型装一个“实时知识库”)。 全程搭配实操代码,跟着敲就能上手,还会穿插很多“踩坑指南”,让大家少走弯路~ 4.1 链式工作流设计:复杂任务的拆解与流转 先问大家一个问题:如果让你用大模型完成“从一篇新闻稿中提取关键信息,再生成摘要,最后写成社交媒体文案”的任务,你会怎么做?直接丢给大模型一句话让它搞定?
从这一章开始,我们就要从LangChain的基础用法正式“升级打怪”,进入应用级系统设计的领域啦!
如果说之前的学习是在“认识零件”,那这一章就是教大家如何把这些零件组装成能解决实际问题的“智能机器”。
核心重点是两个:链式工作流(把复杂任务拆成流水线)和RAG(给大模型装一个“实时知识库”)。
全程搭配实操代码,跟着敲就能上手,还会穿插很多“踩坑指南”,让大家少走弯路~
先问大家一个问题:如果让你用大模型完成“从一篇新闻稿中提取关键信息,再生成摘要,最后写成社交媒体文案”的任务,你会怎么做?直接丢给大模型一句话让它搞定?大概率会得到一个不伦不类的结果——要么信息提取不全,要么文案风格不对。
这就像让一个厨师同时完成“买菜、洗菜、做菜、摆盘”,步骤越多越容易出错。
而链式工作流,就是把这些复杂任务拆成一个个小步骤,让它们按顺序(或按条件)依次执行,最终拿到满意结果。
咱们先搞懂“为什么要这么折腾”。直接调用大模型不好吗?还真不好,主要有三个痛点:
而结构化链式工作流就解决了这些问题:把大任务拆成小步骤(每个步骤一个“组件”),每个组件专注做一件事,步骤之间可以传递数据,出问题了能精准定位,还能根据需求灵活替换某个环节的模型或工具。就像工厂的流水线,每个工位只负责一道工序,最终组装出合格产品。
LangChain 0.1.x 版本后,官方重构了核心架构,旧版 SequentialChain/RouterChain/ParallelChain 已标记为遗留组件,推荐使用基于 Runnables 的全新组件实现链式工作流。
| 组件 | 核心逻辑 | 适用场景 |
|---|---|---|
RunnableSequence / | |
多个组件按固定顺序执行,前一个组件的输出作为后一个组件的输入;支持 | 运算符或 from_chain() 创建,提供 .invoke() / .stream() / .batch() 方法,可搭配 RunnablePassthrough 实现多输入传递 |
流水线式任务,如"提取信息→生成摘要→翻译"等多步骤顺序处理 |
| RunnableBranch | 根据输入内容或条件进行判断,将请求动态分发到不同处理分支 | 多场景路由任务,如智能客服(订单问题 / 售后问题)、多主题问答(历史 / 科技 / 医疗) |
| RunnableParallel / RunnableMap | 对同一输入并行执行多个子组件,最终返回结构化结果(dict) | 多维度分析任务,如从“情感、逻辑、文采”等多个角度评价同一文本 |
小提示:在实际应用中,RunnableSequence(线性流转) 与 RunnableBranch(动态路由) 是最常用的两种模式。
新版 Runnables 体系的优势在于:组件更轻量、支持流式与异步执行、可自由组合嵌套,更适合构建复杂的应用级 AI 系统。
| 运算符线性链就像“多米诺骨牌”,一个倒下带动下一个,核心是“顺序执行、数据流转”。
咱们从简单的开始练手。
核心逻辑超简单:把“提示词模板(PromptTemplate)+ 大模型(LLM)”看作一个个独立的 Runnable 组件,用 | 运算符按顺序串联,第一个组件的输出自动作为第二个组件的输入,以此类推。就像接力赛,每个运动员跑完自己的棒,把接力棒交给下一个。
举个例子:“提取新闻关键信息→生成新闻摘要”。第一步(组件1):PromptTemplate(提取信息)+ LLM,输入新闻原文,输出关键信息;第二步(组件2):PromptTemplate(生成摘要)+ LLM,输入关键信息,输出新闻摘要。用 | 串联两个组件,顺序执行就能拿到最终结果。
先做一个简单任务:给一段产品介绍,先提取核心卖点,再根据卖点写一段营销话术。咱们一步步来,先准备环境,再写代码。
第一步:环境准备
【前置准备】所有案例需先完成环境配置:
# 安装完整依赖(新手必执行) pip install langchain langchain-core langchain-openai python-dotenv
.env 文件示例
API_KEY=你的deepseek-api-key
第二步:编写代码
# ========================= # 1. 基础依赖 # ========================= from langchain_openai import ChatOpenAI from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnableLambda from dotenv import load_dotenv import os # ========================= # 2. 环境变量 & 模型 # ========================= load_dotenv() llm = ChatOpenAI( api_key=os.getenv("API_KEY"), base_url="https://api.deepseek.com", model="deepseek-chat", temperature=0.3 ) # ========================= # 3. 组件1:提取核心卖点 # ========================= sell_point_prompt = PromptTemplate( input_variables=["product_intro"], template="请从以下产品介绍中提取3个核心卖点,用简洁的语言列出:{product_intro}" ) sell_point_chain = sell_point_prompt | llm # ========================= # 4. 中间结果结构化(LangChain 风格) # ========================= extract_sell_points = RunnableLambda( lambda msg: {"sell_points": msg.content} ) # ========================= # 5. 组件2:生成营销话术 # ========================= marketing_prompt = PromptTemplate( input_variables=["sell_points"], template="请根据以下产品核心卖点,写一段吸引消费者的营销话术(适合朋友圈发布):{sell_points}" ) marketing_chain = marketing_prompt | llm # ========================= # 6. 线性串联(Sequential Runnable) # ========================= overall_chain = ( sell_point_chain | extract_sell_points | marketing_chain ) # ========================= # 7. 执行 # ========================= product_intro = """这款无线耳机采用蓝牙5.3芯片,连接稳定无延迟,支持高清通话;续航长达30小时,充电10分钟可使用2小时;机身采用亲肤硅胶材质,佩戴舒适,防水防汗,适合运动使用。""" result = overall_chain.invoke({"product_intro": product_intro}) print("\n最终营销话术:") print(result.content)
第三步:代码解释与运行结果
PromptTemplate | llm 封装成可执行组件,简洁高效;| 运算符串联组件,直观体现“流水线”式的执行逻辑;invoke() 方法执行组件,模型输出为 AIMessage 对象,需通过.content 属性获取文本内容;运行结果示例:
最终营销话术: 【运动新宠|告别卡顿与电量焦虑】✨ 戴上它,世界瞬间清晰! ✅ 蓝牙5.3芯片加持,通话如面对面般稳定流畅,再也不用担心运动时通话断断续续~ ✅ 狂飙30小时超长续航,充电10分钟畅听2小时,电量焦虑?不存在的! ✅ 亲肤硅胶轻盈贴耳,狂汗暴雨也不怕,运动暴走一整天,舒适感依然在线! ♂️ 无论是晨跑、健身还是通勤,让它成为你随时在线的“舒适能量耳伴”! 点击体验,把高清畅听与自由运动装进口袋~
踩坑指南:
若提示 API 密钥错误,先检查 .env 文件密钥是否正确,再用
print(api_key)验证环境变量是否加载成功;若提示“输入变量缺失”,检查组件间数据格式是否匹配(比如用 lambda 函数转换 AIMessage 为字典);
替换其他厂商模型(如通义千问、文心一言)时,只需修改
base_url和model参数,核心逻辑不变。
基础实践的 | 串联适合单输入单输出场景,若任务更复杂(比如“输入产品介绍和目标人群,先提取卖点,再根据卖点和人群写营销话术”),就需要用到 RunnableSequence 结合 RunnablePassthrough 实现多输入多输出。
编写代码
from langchain_openai import ChatOpenAI from langchain_core.prompts import PromptTemplate from langchain_core.runnables import RunnableMap, RunnablePassthrough from dotenv import load_dotenv import os # 1. 初始化模型 load_dotenv() llm = ChatOpenAI( api_key=os.getenv("API_KEY"), base_url="https://api.deepseek.com", model="deepseek-chat", temperature=0.3 ) # 2. Prompt 定义 sell_point_prompt = PromptTemplate( input_variables=["product_intro"], template="从以下产品介绍中提取3个核心卖点,简洁列出:{product_intro}" ) marketing_prompt = PromptTemplate( input_variables=["sell_points", "target_audience"], template="针对{target_audience},结合以下核心卖点,写一段朋友圈营销话术:{sell_points}" ) # 3. 多输入多输出线性链(教学标准版) overall_chain = ( # Step 1:生成卖点 + 透传原始输入 RunnableMap({ "sell_points": sell_point_prompt | llm | (lambda x: x.content), "target_audience": RunnablePassthrough(), }) # Step 2:营销话术生成 | marketing_prompt | llm ) # 4. 执行 input_data = { "product_intro": "这款无线耳机采用蓝牙5.3芯片,连接稳定无延迟...", "target_audience": "大学生群体(喜欢运动、预算有限、注重性价比)" } result = overall_chain.invoke(input_data) print("营销话术:") print(result.content)
代码解释与运行结果
RunnableLambda 是 LangChain 中用于将任意自定义函数 / 逻辑包装成 Runnable 接口的组件,让普通函数可以无缝接入 LangChain 的链式调用体系中。简单来说,它就是「普通函数」和「LangChain 可运行组件」之间的「适配器」。RunnableMap 是用于并行执行多个 Runnable 组件的容器,它接收一个字典(key 为自定义名称,value 为 Runnable 组件),执行时会并行调用所有子组件,并将每个组件的输出以「key: 输出结果」的形式汇总返回。RunnablePassthrough 是一个「透传组件」,它的核心作用是原样传递输入数据(或基于输入生成新数据后透传),常用来在链式调用中保留原始输入、补充数据或调整数据结构。运行结果示例:
营销话术: 【运动党必备!性价比逆天的蓝牙耳机来啦!】 跑步总被耳机线缠到崩溃?健身时耳机滑落社死现场?电话打到一半突然断连?——你的运动耳机该升级了! ✨ 蓝牙5.3芯片加持,连接稳如泰山!操场狂奔、球场激战,音乐不断连,通话清晰无延迟,团战开黑再也不怕坑队友! ⚡ 续航狂魔登场!30小时超长续航,图书馆泡一整天都不用充电。支持快充,早上洗漱时间充10分钟,就能撑完两节体育课+晚间夜跑! ♂️ 亲肤硅胶耳翼+IPX5防水,狂汗如雨也不滑落!轻若无物的佩戴感,跳绳撸铁毫无负担,下雨天晨跑照样戴~ 学生价直接打穿底线!少喝两杯奶茶,就能拥有专业运动耳机,宿舍开黑/图书馆自习/操场暴汗全适配! 点击立抢专属学生优惠,跑赢新学期的第一圈!
是不是发现,针对特定人群的话术更精准了?
这就是多输入线性链的价值——能结合多个维度的信息完成任务。
线性链适合“流程固定”的任务,但如果遇到“一个入口、多个场景”的情况就不行了。比如智能客服,用户可能问“查订单”“退货款”“问保修”,不同问题需要不同的处理流程。这时候就需要路由链——它像一个“智能分流员”,先判断用户需求,再把任务分配给对应的处理链。
另外,不管是线性链还是路由链,运行时都可能出问题(比如API调用失败、输入格式错误),所以异常处理也很重要,能让你的系统“不轻易崩溃”。
路由链的核心逻辑是“判断+分发”,就像学校的教务处:学生来办事,先问清楚“你要办什么事?”(判断),再指引到对应的科室(分发)——办成绩证明去A科,办学籍异动去B科。
在新版LangChain中,路由链基于Runnable范式实现,核心组成仍为三部分:
路由选择器是路由链的“大脑”,新版LangChain推荐用RunnableBranch实现条件路由,或用大模型驱动的路由判断。其核心工作流程:
新版中最常用的是“大模型+RunnableBranch”组合:利用大模型的自然语言理解能力解析用户需求,输出标准化标识后,由RunnableBranch完成分发,无需编写复杂的条件判断逻辑。
我们做一个简单的智能客服路由链:预设三个场景(查订单、退货款、保修政策),用户输入问题后,自动分发到对应的链处理,无法匹配则用默认链回复。采用新版LangChain写法(基于Runnable范式,使用ChatOpenAI模型)。
第一步:环境准备
和之前一样,确保安装了依赖包并配置了API密钥。
第二步:编写代码
from dotenv import load_dotenv from langchain_core.prompts import ChatPromptTemplate, PromptTemplate from langchain_core.runnables import RunnableBranch, RunnableLambda, RunnableSequence from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI import os # 1. 加载环境变量与初始化模型(新版推荐用ChatOpenAI,支持聊天模型) load_dotenv() llm = ChatOpenAI( api_key=os.getenv("API_KEY"), base_url="https://api.deepseek.com", model="deepseek-chat", temperature=0.3 ) # 2. 定义各场景的提示词模板与目标链(新版用RunnableSequence组合Prompt+LLM+Parser) # 场景1:查订单链 order_prompt = ChatPromptTemplate.from_messages([ ("system", "你是智能客服,负责解答用户的订单查询问题。"), ("human", "用户问题:{query}\n请引导用户提供订单号,并告知查询流程:1. 提供订单号;2. 系统验证;3. 反馈订单状态。") ]) order_chain = order_prompt | llm | StrOutputParser() # 新版Runnable流水线写法 # 场景2:退货款链 refund_prompt = ChatPromptTemplate.from_messages([ ("system", "你是智能客服,负责解答用户的退货款问题。"), ("human", "用户问题:{query}\n请说明退款流程:1. 申请退款(订单页面点击退款);2. 等待审核(1-3个工作日);3. 退款到账(原路返回,3-5个工作日)。如果用户问退款进度,引导提供退款申请单号。") ]) refund_chain = refund_prompt | llm | StrOutputParser() # 场景3:保修政策链 warranty_prompt = ChatPromptTemplate.from_messages([ ("system", "你是智能客服,负责解答产品保修政策问题。"), ("human", "用户问题:{query}\n请说明保修政策:本产品保修期限为1年,保修范围包括质量问题(非人为损坏),保修流程:1. 联系客服;2. 提供购买凭证;3. 寄回检测维修。") ]) warranty_chain = warranty_prompt | llm | StrOutputParser() # 3. 定义路由判断逻辑(大模型解析需求,输出场景标识) # 路由提示词:让大模型输出标准化的场景名称,用于后续分支匹配 router_prompt = ChatPromptTemplate.from_messages([ ("system", """ 你是路由选择器,需根据用户问题判断所属场景,仅输出以下标准化标识之一: - order:订单查询相关(含订单状态、订单号) - refund:退货款相关(含退款进度、退款申请) - warranty:保修相关(含维修、售后保障) - default:以上均不匹配 无需输出任何其他内容,仅返回标识字符串。 """), ("human", "用户问题:{query}") ]) # 路由解析链:输入query,输出场景标识 router_chain = router_prompt | llm | StrOutputParser() # 4. 定义默认链(兜底处理) default_prompt = ChatPromptTemplate.from_messages([ ("system", "你是智能客服。当遇到无法解答的问题时,请礼貌地告知用户你暂时无法处理该问题,并引导用户重新描述具体问题,或提供联系人工客服的方式(工作时间:9:00-18:00)。语气要友善、专业。"), ("human", "用户问题:{query}\n请生成合适的回复。") ]) default_chain = default_prompt | llm | StrOutputParser() # 5. 构建完整路由链(核心:RunnableBranch实现条件分发) # 逻辑:先通过router_chain获取场景标识,再由RunnableBranch分发到对应目标链 full_router_chain = RunnableLambda(lambda x: x) | ( # 分支1:匹配order场景 RunnableBranch( (lambda x: x["scene"] == "order", order_chain), (lambda x: x["scene"] == "refund", refund_chain), (lambda x: x["scene"] == "warranty", warranty_chain), default_chain # 默认分支 ) ).with_config(run_name="full_router_chain") # 6. 封装调用函数(整合场景解析与路由分发) def process_query(query: str): # 第一步:获取场景标识 scene = router_chain.invoke({"query": query}) # 第二步:将query和scene传入完整路由链,执行分发处理 return full_router_chain.invoke({"query": query, "scene": scene}) # 7. 测试不同场景输入 test_queries = [ "我的订单什么时候发货?", "怎么申请退款呀?", "这个产品保修多久?", "你们家有什么新品?" # 无法匹配,触发默认链 ] for query in test_queries: print(f"\n用户问题:{query}") print("客服回复:", process_query(query))
代码解释与运行结果
新版写法核心优势:基于Runnable范式,通过|(流水线)组合组件,逻辑更清晰,可扩展性更强。关键部分说明:
ChatPromptTemplate | llm | StrOutputParser()流水线替代旧版LLMChain,简洁且符合新版规范;RunnableBranch是新版推荐的条件路由组件,通过“(条件判断函数,对应链)”的元组定义分支,无需手动编写分发逻辑;运行结果示例:
用户问题:我的订单什么时候发货? 客服回复: 您好!为了帮您查询订单的发货时间,请您提供一下订单号。查询流程如下: 1. **提供订单号**:请告知您的订单号(通常可在订单确认邮件或账户订单页面找到)。 2. **系统验证**:我会根据订单号核实订单信息。 3. **反馈订单状态**:验证后,我会为您提供具体的发货进度或预计发货时间。 请提供订单号,我会尽快为您处理! 用户问题:怎么申请退款呀? 客服回复: 您好,申请退款的操作很简单,具体流程如下: 1. **提交申请**:请您进入【我的订单】页面,找到需要退款的订单,点击【申请退款】按钮,根据提示填写退款原因并提交。 2. **等待审核**:提交后,我们会在 **1-3个工作日** 内完成审核,审核结果会通过短信或站内消息通知您。 3. **退款到账**:审核通过后,退款会按原支付路径自动退回,一般 **3-5个工作日** 内到账(具体到账时间以银行或支付平台为准)。 如果您已经提交申请,想查询退款进度,可以告诉我您的 **退款申请单号**,我会帮您跟进处理。 用户问题:这个产品保修多久? 客服回复: 本产品提供**1年保修服务**,具体政策如下: **保修范围**: - 产品在正常使用情况下出现的**质量问题**(非人为损坏)。 **保修流程**: 1. **联系客服**:通过官方渠道联系客服人员。 2. **提供凭证**:提供有效的购买凭证(如订单截图、发票等)。 3. **寄回检测**:按客服指引将产品寄回指定地址进行检测与维修。 如有其他疑问,可随时告知,我会为您进一步解答! 用户问题:你们家有什么新品? 客服回复: 您好!我理解您想了解新品信息,但作为AI助手,我无法获取实时产品目录或库存信息。不过,我可以为您提供几种快速获取信息的途径: 1. **官方渠道** **官网/小程序**:访问品牌官方网站或微信小程序,通常会有“新品推荐”或“最新上市”专栏。 **客服热线**:直接致电官方客服(如您提到的工作时间9:00-18:00),他们能提供最准确的实时信息。 2. **线上平台** **电商旗舰店**:在淘宝、京东等平台的品牌旗舰店中,搜索“新品”或按“上新时间”筛选商品。 3. **其他建议** ✨ **订阅通知**:若您已注册会员,可关注品牌邮件或短信通知,新品常会优先推送。 **线下门店**:如有实体店,可咨询店员获取最新到店产品信息。 如果您需要我帮您查找特定品类的产品信息(如电子产品、美妆等),或整理新品选择的注意事项,我很乐意提供通用建议!请随时告诉我您的需求哦~
小技巧:小技巧:若需提升路由判断精度,可在router_prompt中增加场景示例(如“例:用户问‘订单号在哪找’→输出order”),或调整提示词,进一步提升自然语言理解能力。
不管是线性链还是路由链,运行时都可能遇到各种“意外”:API调用超时、输入格式错误、大模型返回空结果……新版LangChain基于Runnable范式提供了更优雅的错误处理方案,核心组件包括RunnableRetry(重试)、RunnableWithFallback(降级),结合Python原生异常捕获,构建完整的“安全防护网”。
先搞清楚链式工作流中最容易遇到的错误:
| 错误类型 | 触发场景 | 示例 |
|---|---|---|
| API相关错误 | API密钥错误、网络中断、调用频率超限、模型服务宕机 | openai.AuthenticationError(密钥错误)、requests.exceptions.ConnectionError(网络中断) |
| 输入格式错误 | 输入缺少必要变量、格式不符合Prompt要求(如需字典却传入字符串) | MissingInputError(缺少输入变量)、ValueError(输入格式错误) |
| 输出解析错误 | 大模型返回结果不符合解析器要求(如需字符串却返回空值) | OutputParserException(解析失败) |
| 业务逻辑错误 | 输入内容无法被业务逻辑处理(如查订单时输入非数字订单号) | 订单号格式错误、无此订单记录 |
针对上述错误,结合新版LangChain组件,提供三种核心解决方案:
1. 重试机制(解决临时API错误)
对于网络波动、API临时不可用等临时错误,使用RunnableRetry组件自动重试,支持自定义重试次数、间隔时间和重试条件。
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.exceptions import OutputParserException from langchain_core.runnables import Runnable import os from langchain_openai import ChatOpenAI from dotenv import load_dotenv load_dotenv() llm = ChatOpenAI( api_key=os.getenv("API_KEY"), base_url="https://api.deepseek.com", model="deepseek-chat", temperature=0.3 ) # 1️⃣ Prompt summary_prompt = ChatPromptTemplate.from_messages([ ("system", "请简洁总结以下文本核心内容,不超过50字。"), ("human", "{text}") ]) # 2️⃣ 基础链 base_chain: Runnable = summary_prompt | llm | StrOutputParser() # 3️⃣ 重试链(官方推荐:直接 with_retry) retry_chain = base_chain.with_retry( stop_after_attempt=3, # 最多重试 3 次 wait_exponential_jitter=True, # 指数退避 + 抖动(推荐) retry_if_exception_type=( ConnectionError, TimeoutError, ), ) # 4️⃣ 调用 try: result = retry_chain.invoke({ "text": "LangChain是一个用于构建大模型应用的框架,提供了丰富的Runnable组件,支持重试、降级等工程化能力。" }) print("总结结果:", result) except OutputParserException as e: # ❗ 解析错误通常是逻辑问题,不建议重试 print("输出解析失败:", e) except Exception as e: print("最终失败(已达到最大重试次数):", e)
代码解释:
with_retry是新版Runnable的内置方法,通过RunnableRetry配置重试规则,相比旧版RetryWithErrorOutputChain更灵活,可精准控制重试范围(如仅对临时API错误重试,避免无效重试)。
2. 异常捕获(解决可预知的错误)
对于输入格式错误、解析错误等可预知问题,结合Python原生try-except和LangChain异常类型,捕获错误后返回友好提示。
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.exceptions import OutputParserException from langchain_core.runnables import Runnable import os from langchain_openai import ChatOpenAI from dotenv import load_dotenv load_dotenv() llm = ChatOpenAI( api_key=os.getenv("API_KEY"), base_url="https://api.deepseek.com", model="deepseek-chat", temperature=0.3 ) # 1️⃣ 定义多变量 Prompt 链(营销话术生成示例) marketing_prompt = ChatPromptTemplate.from_messages([ ("system", "根据产品卖点和目标人群,撰写一句营销话术。"), ("human", "产品卖点:{sell_points},目标人群:{target_audience}") ]) # 2️⃣ 构建链 marketing_chain: Runnable = marketing_prompt | llm | StrOutputParser() # 3️⃣ 调用并捕获异常(官方推荐风格) inputs = { "sell_points": "无线耳机续航30小时", # "target_audience" 故意缺失,用于演示 KeyError } try: result = marketing_chain.invoke(inputs) print("营销话术:", result) except KeyError as e: # 抛出 KeyError 异常,直接提取缺失的变量名 missing_var = str(e).strip("'\"") print(f"错误提示:缺少必要输入变量 [{missing_var}],请检查输入数据是否完整。") except OutputParserException as e: # 官方推荐:逻辑解析错误不重试 print(f"解析失败:{e},请确认 Prompt 与输出格式匹配。") except Exception as e: # ❗ 兜底捕获未知异常 print(f"未知错误:{type(e).__name__}: {e},请联系开发者排查。")
运行结果:错误提示:缺少必要输入变量 [target_audience],请检查输入数据是否完整。
踩坑指南:这里实际抛出的是
KeyError(而非ValueError),错误信息格式为”Input to ChatPromptTemplate is missing variables {'target_audience'}”。捕获时应优先处理KeyError,再根据实际错误信息提取缺失的变量名。
3. 分支降级(错误时切换备用方案)
核心链失败时,使用RunnableWithFallbacks自动切换到备用链(如小模型、预设模板),保证系统可用性。相比旧版自定义函数,新版写法更简洁、可复用。
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnableWithFallbacks from langchain_openai import ChatOpenAI from langchain_core.exceptions import OutputParserException import os from dotenv import load_dotenv load_dotenv() # 使用 OpenAI 的 API 密钥(从环境变量读取) api_key = os.getenv("OPENAI_API_KEY") # 在 .env 中设置 OPENAI_API_KEY=sk-xxxxx # 注意:不需要指定 base_url,默认就是 https://api.openai.com # 1️⃣ 核心链(性能高但可能不稳定) core_llm = ChatOpenAI( api_key=api_key, model="gpt-4", temperature=0.7 ) core_prompt = ChatPromptTemplate.from_messages([ ("system", "用专业语言详细解答用户问题。"), ("human", "{query}") ]) core_chain = core_prompt | core_llm | StrOutputParser() # 2️⃣ 降级链(稳定但精度略低) fallback_llm = ChatOpenAI( api_key=api_key, model="gpt-3.5-turbo", temperature=0.5 ) fallback_prompt = ChatPromptTemplate.from_messages([ ("system", "用简洁语言解答用户问题,保证信息准确。"), ("human", "{query}") ]) fallback_chain = fallback_prompt | fallback_llm | StrOutputParser() # 3️⃣ 构建带降级的链 chain_with_fallback: RunnableWithFallbacks = core_chain.with_fallbacks( fallbacks=[fallback_chain], exceptions_to_handle=(ConnectionError, TimeoutError),# ✅ 官方推荐:只捕获临时错误或网络错误 ) # 4️⃣ 调用链并捕获异常 try: result = chain_with_fallback.invoke({"query": "什么是RAG技术?"}) print("解答:", result) except OutputParserException as e: print(f"解析失败:{e}") except Exception as e: print(f"最终失败:{e}")
代码解释:如果核心链(gpt-4)因为API错误、超时而失败,就会自动切换到降级链(gpt-3.5-turbo-instruct),保证用户能得到解答,只是可能在回答质量上略有差异。这在工程实践中非常重要,能极大提升系统的可用性。
踩坑指南:异常捕获要精准,不要用“except Exception”捕获所有错误,尽量先捕获具体的错误类型(比如KeyError、AuthenticationError),再用通用异常捕获未知错误,这样方便后续调试。
讲完了链式工作流,我们来学习本章的另一个核心——RAG。
如果说链式工作流是“让大模型按步骤做事”,那RAG就是“让大模型有东西可依”。
不知道大家有没有遇到过这种情况:问大模型“我们公司2025年的产品规划是什么?”,大模型要么说“不知道”,要么胡编乱造一个答案。
这就是大模型的原生痛点——知识滞后、事实性差。
大模型的两个致命缺点,让它在企业级应用中“寸步难行”:
为什么会这样?因为大模型的知识是“死”的,存在于训练参数中,无法实时更新,也无法验证事实的准确性。而RAG,就是给大模型装上一个“可更新的知识库”和“事实校验器”。
RAG的全称是Retrieval-Augmented Generation(检索增强生成),核心逻辑超简单:在大模型生成答案之前,先从外部知识库中检索相关的真实、最新信息,把这些信息作为“参考资料”传给大模型,让大模型基于参考资料生成答案。
就像考试时,你遇到一个不会的问题,监考老师给你一本参考书,让你基于参考书的内容答题——这样既能保证答案的准确性,又能回答你原本不会的问题。RAG的流程可以总结为三步:
小提示:RAG的本质不是替代大模型,而是“赋能”大模型——让大模型能利用外部的、实时的、准确的知识来生成答案,解决知识滞后和事实性错误的问题。
相比直接使用大模型,RAG有三个核心价值,也是企业为什么愿意用RAG的原因:
RAG不是万能的,要根据场景选择是否使用。下面列出适用和不适用的场景,帮大家避坑:
| 场景类型 | 典型案例 | 说明 |
|---|---|---|
| 企业内部知识库问答 | 员工手册、产品文档、技术手册 | 基于企业内部资料,准确回答员工常见问题 |
| 行业报告/政策文件问答 | 法规条文、财务报告、市场分析 | 需要准确事实和数据,不能凭空编造 |
| 最新信息问答 | 新闻资讯、赛事结果、股市行情 | 实时性要求高,依赖外部最新数据 |
| 私有数据问答 | 客户资料、合同文档、会议纪要 | 敏感数据,无法公开但需要检索使用 |
| 场景类型 | 典型案例 | 说明 |
|---|---|---|
| 创造性任务 | 写诗歌、编故事、设计广告语 | 不需要真实参考资料,自由发挥即可 |
| 逻辑推理任务 | 数学题、编程题、逻辑分析 | 核心是逻辑计算,不是外部知识检索 |
| 简单闲聊 | 天气询问、问候语、日常对话 | 不需要复杂的知识库检索 |
举个例子:做一个“公司产品手册问答机器人”,适合用RAG;做一个“儿童故事生成器”,不适合用RAG。
这部分是实操重点,我们将一步步构建一个完整的RAG系统。
一个标准的RAG系统,从数据准备到最终问答,需要经过4个核心步骤:文档加载→文本分割→向量存储→检索与生成。
咱们逐个环节拆解,每个环节都配实操代码。
重要前置说明:LangChain 新版已将所有第三方文档加载器迁移至 langchain-community 库(核心库仅保留基础接口),因此先确保安装必备依赖: pip install langchain-core langchain-community
第一步是“文档加载”——把我们的知识库(比如PDF、Word、Markdown、TXT文件)加载到LangChain中,转换成统一的Document对象(LangChain定义的文档格式,包含文本内容和元数据)。
不同格式的文档,加载方式不同。LangChain提供了大量的文档加载器,覆盖主流格式。下面列出常见格式的加载器和依赖包:
| 文档格式 | 官方推荐加载器 | 需要安装的依赖包 |
|---|---|---|
| TXT | TextLoad | 无需额外安装(langchain-community 内置) |
| PyPDFLoader(基础款)/ PDFPlumberLoader(复杂款) | 基础款:pip install pypdf;复杂款:pip install pdfplumber |
|
| Word(.docx) | Docx2txtLoader | pip install docx2txt |
| Markdown(.md) | MarkdownLoader(轻量款)/ UnstructuredFileLoader(通用款) | 轻量款:pip install python-markdown;通用款:pip install unstructured |
光说不练假把式!下面咱们逐个实现不同格式文档的加载,代码里都加了详细注释,跟着敲就行。先统一说明:所有文档都放在一个叫「knowledge_base」的文件夹里,建议大家先创建这个文件夹,再把测试文档放进去(比如放一个test.txt、test.pdf、test.docx、test.md)。
所有的测试文档已经在 src\code\knowledge_base 文件夹里,大家直接用即可。
1. TXT文档加载(最简单的入门案例)
TextLoader 是官方推荐的TXT专属加载器,轻量无冗余依赖,支持自定义编码,适配中文文档。
# 导入TXT加载器(新版路径:langchain_community.document_loaders) from langchain_community.document_loaders import TextLoader import os # 定义文档路径(请替换成你自己的路径) txt_path = os.path.join("knowledge_base", "test.txt") # 初始化加载器并加载文档 loader = TextLoader(txt_path, encoding="utf-8") # 指定编码,避免中文乱码 txt_docs = loader.load() # load()返回Document对象列表(即使只有一个文档) # 查看加载结果 print("TXT文档加载结果:") print(f"文档数量:{len(txt_docs)}") print(f"文档内容:{txt_docs[0].page_content[:200]}...") # 打印前200个字符 print(f"文档元数据:{txt_docs[0].metadata}") # 元数据包含文档路径等信息
踩坑指南:如果加载中文TXT出现乱码,大概率是编码问题!把encoding参数改成"gbk"试试(有些旧TXT是gbk编码)。
2. PDF文档加载(企业场景最常用)
官方推荐基础场景用 PyPDFLoader(轻量高效),复杂场景(需保留表格、精准页码、格式)用 PDFPlumberLoader。两者均会按页拆分文档,生成独立Document对象,方便后续处理。
# 方案1:基础款(官方推荐,仅提取文本,轻量) from langchain_community.document_loaders import PyPDFLoader import os # 定义PDF路径 pdf_path = os.path.join("knowledge_base", "test.pdf") # 初始化加载器并加载(按页拆分) loader = PyPDFLoader(pdf_path) pdf_docs = loader.load_and_split() # load_and_split()直接按页拆分,更易用 # 查看结果 print("\nPDF文档加载结果(基础款):") print(f"PDF总页数:{len(pdf_docs)}") print(f"第1页内容:{pdf_docs[0].page_content[:200]}...") print(f"第1页元数据:{pdf_docs[0].metadata}") # 元数据包含页码、文档路径 # 方案2:复杂款(需保留表格/格式时用) from langchain_community.document_loaders import PDFPlumberLoader loader = PDFPlumberLoader(pdf_path) pdf_docs_adv = loader.load() print("\nPDF文档加载结果(复杂款):") print(f"第1页表格/格式保留情况:{pdf_docs_adv[0].page_content[:200]}...")
3. Word文档加载(适配.docx格式)
在当前常见的 Python 教学环境中(例如 langchain_community==0.4.1),可直接使用 Docx2txtLoader 加载 .docx 文档。运行前需先安装 docx2txt 依赖。
# 导入Word加载器(兼容 langchain_community 0.4.1) from langchain_community.document_loaders import Docx2txtLoader import os # 定义Word路径 docx_path = os.path.join("knowledge_base", "test.docx") # 加载文档(需提前安装 docx2txt:pip install docx2txt) loader = Docx2txtLoader(docx_path) docx_docs = loader.load() # 查看结果 print("\nWord文档加载结果:") print(f"文档数量:{len(docx_docs)}") print(f"文档内容:{docx_docs[0].page_content[:200]}...") print(f"元数据:{docx_docs[0].metadata}")
踩坑指南:1. 仅支持 .docx 格式,旧版 .doc 需先转成 .docx;2. 若Word含大量图片,图片内容无法直接提取,需额外集成OCR工具。
4. Markdown文档加载(技术文档常用)
官方推荐轻量款 MarkdownLoader(保留标题层级、列表等结构),避免使用重量级的 UnstructuredMarkdownLoader(依赖复杂)。若需适配多格式通用场景,可备选 UnstructuredFileLoader。
# 方案1:轻量款(官方推荐,保留MD结构,优先选) from langchain_community.document_loaders import UnstructuredMarkdownLoader import os # 定义MD路径 md_path = os.path.join("knowledge_base", "test.md") # 加载文档(需提前安装python-markdown) loader = UnstructuredMarkdownLoader(md_path) md_docs = loader.load() # 查看结果 print("\nMarkdown文档加载结果(轻量款):") print(f"文档数量:{len(md_docs)}") print(f"文档内容(保留结构):{md_docs[0].page_content[:200]}...") print(f"元数据:{md_docs[0].metadata}") # 方案2:通用款(适配多格式,含MD/TXT等) from langchain_community.document_loaders import UnstructuredFileLoader loader = UnstructuredFileLoader(md_path, mode="elements") # mode="elements"保留结构 md_docs_univ = loader.load() print("\nMarkdown文档加载结果(通用款):") print(f"内容预览:{md_docs_univ[0].page_content[:200]}...")
5. 批量加载多格式文档(实用技巧)
如果知识库文件夹里有多种格式的文档,一个个加载太麻烦,咱们基于官方推荐加载器写个批量加载函数,自动识别格式并加载:
from langchain_community.document_loaders import ( TextLoader, PyPDFLoader, Docx2txtLoader, UnstructuredMarkdownLoader ) import os def batch_load_documents(folder_path): """ 批量加载文件夹内的所有官方支持格式文档(基于新版加载器) :param folder_path: 知识库文件夹路径 :return: 所有文档的Document对象列表 """ all_docs = [] # 遍历文件夹内所有文件 for filename in os.listdir(folder_path): file_path = os.path.join(folder_path, filename) # 跳过文件夹,只处理文件 if os.path.isdir(file_path): continue # 根据文件后缀选择对应的官方推荐加载器 try: if filename.endswith(".txt"): loader = TextLoader(file_path, encoding="utf-8") elif filename.endswith(".pdf"): loader = PyPDFLoader(file_path) # 基础款,复杂场景可替换为PDFPlumberLoader elif filename.endswith(".docx"): loader = Docx2txtLoader(file_path) elif filename.endswith(".md"): loader = UnstructuredMarkdownLoader(file_path) else: print(f"不支持的文件格式:{filename}") continue # 加载并添加到文档列表 docs = loader.load() all_docs.extend(docs) print(f"成功加载:{filename},生成{len(docs)}个Document对象") except Exception as e: print(f"加载失败:{filename},错误信息:{str(e)}") return all_docs # 测试批量加载 if __name__ == "__main__": knowledge_base_path = "knowledge_base" # 确保知识库文件夹存在 if not os.path.exists(knowledge_base_path): os.makedirs(knowledge_base_path) print(f"已自动创建知识库文件夹:{knowledge_base_path},请放入测试文档") else: all_docs = batch_load_documents(knowledge_base_path) print(f"\n批量加载完成,总Document对象数:{len(all_docs)}") # 打印每个文档的基本信息 for i, doc in enumerate(all_docs): print(f"\n文档{i+1}:") print(f"内容预览:{doc.page_content[:100]}...") print(f"元数据:{doc.metadata}")
这个函数超实用!后续构建自己的RAG系统时,直接调用它就能加载整个知识库的文档,省了很多重复代码。
加载完文档后,下一步要做的是「文本分割」——把大文档切成一个个小的文本片段(叫Chunks)。可能有同学会问:“直接用整个文档检索不行吗?” 还真不行!这里有两个核心原因:
举个例子:你问“LangChain的链式工作流有哪些类型?”,如果把包含链式工作流、RAG、向量存储的大文档整个拿去检索,可能会把RAG的内容也带出来;但如果切成“链式工作流类型”“RAG核心原理”等小片段,就能精准定位到需要的内容。
分割文本不是“随便切”,要遵循两个核心原则,不然会影响后续检索效果:
LangChain v0.1+ 版本对文本分割器进行了标准化优化,核心推荐 RecursiveCharacterTextSplitter 作为默认分割器(适配绝大多数场景),同时保留针对性分割方案。以下是官方推荐的实操方案(兼容最新API):
1.默认推荐:RecursiveCharacterTextSplitter(智能语义分割)
LangChain 官方推荐此分割器作为首选——它通过“分层分隔符优先级”实现语义感知分割,先按大分隔符(如空行)拆分段落,再按小分隔符(如句号、逗号)微调,直到片段长度符合要求,最大程度保证语义完整性。适配TXT、Markdown、PDF等绝大多数文本类型。
from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.document_loaders import TextLoader # LangChain推荐的基础文本加载器 from pathlib import Path # 官方推荐用pathlib处理路径(比os.path更现代) # 1. 加载文档(推荐用Path处理路径,避免跨系统兼容问题) txt_path = Path("knowledge_base") / "test.txt" loader = TextLoader(txt_path, encoding="utf-8") txt_docs = loader.load() # 返回Document对象列表(含内容+元数据) # 2. 初始化分割器(LangChain推荐参数) text_splitter = RecursiveCharacterTextSplitter( chunk_size=300, # 中文片段推荐长度:200-500字 chunk_overlap=50, # 重叠长度:建议为chunk_size的10%-20%,避免跨片段语义丢失 length_function=len, # 中文用len计数,英文可改用tiktoken.count_tokens separators=["\n\n", "\n", "。", "!", "?", ",", ";", "、"] # 中文推荐分隔符优先级 ) # 3. 执行分割(split_documents为官方推荐方法,接收Document列表) split_docs = text_splitter.split_documents(txt_docs) # 4. 验证结果 print(f"原始文档数:{len(txt_docs)}") print(f"分割后片段数:{len(split_docs)}") print("\n前3个片段示例:") for i, doc in enumerate(split_docs[:3]): print(f"\n片段{i+1}(字符数:{len(doc.page_content)}):") print(doc.page_content.strip()) print(f"片段元数据:{doc.metadata}") # 保留原始文档路径等元数据(检索时有用)
关键说明:
tiktoken.count_tokens计算token数(需安装tiktoken包),更贴合模型上下文限制。2. 基础备选:CharacterTextSplitter(按字符数分割)
仅适用于结构极简单的文本(如纯文本日志),LangChain不推荐作为默认方案。核心优势是轻量、速度快,需手动保证语义完整性。
from langchain_text_splitters import CharacterTextSplitter from langchain_community.document_loaders import TextLoader from pathlib import Path # 加载文档 txt_path = Path("knowledge_base") / "test.txt" loader = TextLoader(txt_path, encoding="utf-8") txt_docs = loader.load() # 初始化(官方推荐设置自然分隔符) text_splitter = CharacterTextSplitter( separator="\n\n", # 优先按空行分割,减少语义破坏 chunk_size=300, chunk_overlap=50, length_function=len, keep_separator=False # 官方默认False,不保留分隔符(避免片段冗余) ) split_docs = text_splitter.split_documents(txt_docs) # 验证结果 print(f"分割后片段数:{len(split_docs)}")
3. 针对性分割:MarkdownTextSplitter(保留MD标题层级)
针对Markdown文档(技术文档、笔记),LangChain推荐用MarkdownTextSplitter——它会识别MD标题层级(#、##、###),确保“标题+对应内容”不被拆分,后续检索时能通过标题快速定位主题。搭配UnstructuredMarkdownLoader加载文档(保留MD结构元数据)。
from langchain_text_splitters import MarkdownTextSplitter from langchain_community.document_loaders import UnstructuredMarkdownLoader # 官方推荐MD加载器 from pathlib import Path # 加载MD文档(保留标题层级元数据) md_path = Path("knowledge_base") / "test.md" loader = UnstructuredMarkdownLoader(str(md_path), mode="elements") # Path转字符串兼容所有版本 md_docs = loader.load() # 3. 初始化MD分割器(保留你原有核心参数,1.x版本完全兼容) text_splitter = MarkdownTextSplitter( chunk_size=500, # MD有标题引导,片段长度可稍大 chunk_overlap=50, # 重叠部分避免标题/内容割裂 length_function=len # 中文用len计数字符 ) # 4. 执行分割(方法名split_documents在1.x版本无变化) split_docs = text_splitter.split_documents(md_docs) # 5. 验证结果(片段会保留MD标题层级,符合预期) print(f"分割后片段数:{len(split_docs)}") print("\n前2个MD片段示例:") for i, doc in enumerate(split_docs[:2]): print(f"\n片段{i+1}:") print(doc.page_content.strip())
分割完文本片段后,下一步要做的是「向量嵌入」和「向量存储」。
这一步是RAG能实现“精准检索”的核心——把文字变成计算机能理解的“数字”,再存到专门的向量数据库里,后续用户提问时,也把问题变成数字,通过比对数字的相似度,找到最相关的文本片段。
用通俗的话讲:每个文本片段就像一个人,向量嵌入就是给每个人做一个“指纹”(一串数字),向量数据库就是存指纹的“数据库”。用户提问时,先给问题做个指纹,再去数据库里找指纹最像的人(文本片段),就是最相关的内容。
把文字变成向量的工具就是「嵌入模型」。好的嵌入模型能精准捕捉文本的语义——意思越接近的文本,向量越像(数字差值越小)。常见的嵌入模型有:
咱们实操用qwen3-embedding-0.6b,该模型基于通义千问大模型技术研发,轻量高效,支持中文语义精准捕捉,适合中小型RAG项目。
前置准备
首先安装最新版本的核心依赖包,确保LangChain相关组件完整性(1.x版本拆分了多个子包,需完整安装):
pip install -U langchain langchain-core langchain-community langchain-openai faiss-cpu transformers torch python-dotenv
说明:langchain-core是LCEL的核心依赖,langchain-community提供向量库、本地嵌入模型等社区组件,缺一不可。
向量需要存在专门的向量数据库里,方便后续快速检索。LangChain支持很多向量数据库,比如FAISS、Chroma、Pinecone等。咱们选FAISS——它是Meta开源的轻量级向量数据库,支持高效的相似性检索,不需要单独部署服务,直接在代码里初始化就能用,同时兼容多种嵌入模型,特别适合初学者和小型项目。
如果想进一步了解向量数据库相关知识,可学习datawhale社区《easy-vectordb》相关内容。链接:https://github.com/datawhalechina/easy-vectordb
流程:加载文档→分割文本→初始化嵌入模型→初始化向量数据库→将分割后的文本嵌入并存储。
qwen3-embedding-0.6b是一个相对比较轻量的嵌入模型,因此本节的教程推荐大家使用原生导入。地址:https://www.modelscope.cn/models/Qwen/Qwen3-Embedding-0.6B
#模型下载 from modelscope import snapshot_download model_dir = snapshot_download('Qwen/Qwen3-Embedding-0.6B',cache_dir='./models')
在使用 HuggingFaceEmbeddings 初始化词向量模型时,底层会自动调用 sentence-transformers 库。因此需要安装依赖包:
pip install sentence-transformers # Requires transformers>=4.51.0 # Requires sentence-transformers>=2.7.0
Qwen3-Embedding-0.6B模型魔塔社区访问地址:https://www.modelscope.cn/models/Qwen/Qwen3-Embedding-0.6B
实践代码
import os from dotenv import load_dotenv from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_community.vectorstores import FAISS from langchain_core.documents import Document from langchain_huggingface import HuggingFaceEmbeddings txt_path = os.path.join("knowledge_base", "test.txt") if not os.path.exists(txt_path): raise FileNotFoundError(f"文档文件不存在:{txt_path}") # 加载文本文档 loader = TextLoader(txt_path, encoding="utf-8") txt_docs: list[Document] = loader.load() # 文本分割(使用最新的 RecursiveCharacterTextSplitter 配置) text_splitter = RecursiveCharacterTextSplitter( chunk_size=300, # 每个文本块的大小 chunk_overlap=50, # 块之间的重叠长度(提升上下文连续性) length_function=len, # 长度计算函数(中文用len即可) is_separator_regex=False # 显式指定非正则分隔符(默认值,增加可读性) ) split_docs: list[Document] = text_splitter.split_documents(txt_docs) print(f"分割后的文本片段数:{len(split_docs)}") # 3. 初始化本地CPU运行的嵌入模型(替换QwenEmbeddings) embedding_model_name = "./models/Qwen/Qwen3-Embedding-0___6B" embeddings = HuggingFaceEmbeddings( model_name=embedding_model_name, model_kwargs={ "device": "cpu" # 强制使用CPU运行,无需GPU }, encode_kwargs={ "normalize_embeddings": True # 归一化向量,提升检索效果 } ) # 4. 构建并持久化FAISS向量库 try: # 生成向量并初始化FAISS(本地CPU计算,首次运行会下载模型,需联网) vector_db = FAISS.from_documents( documents=split_docs, embedding=embeddings, ) # 持久化向量库到本地 vector_db.save_local( folder_path="./faiss_db", index_name="local_cpu_faiss_index" # 索引名改为本地CPU版 ) print("向量存储完成!向量数据已保存到 ./faiss_db 文件夹") except Exception as e: raise RuntimeError(f"构建/保存向量库失败:{str(e)}") # 5. 相似性检索测试 query = "LangChain的链式工作流有哪些类型?" try: # 一次性获取带评分的检索结果 retrieved_docs_with_scores = vector_db.similarity_search_with_score(query, k=3) print(f"\n与问题「{query}」最相关的3个文本片段:") for i, (doc, score) in enumerate(retrieved_docs_with_scores): print(f"\n片段{i+1}:") print(f"内容:{doc.page_content}") print(f"相关性评分(越小越相似):{round(score, 4)}") print(f"来源:{doc.metadata.get('source', '未知')}") except Exception as e: raise RuntimeError(f"检索向量库失败:{str(e)}")
向量存储好后,下一步就是「检索」——用户提问时,从向量数据库里找到最相关的文本片段。LangChain的「检索器」(Retriever)就是干这个活的,它封装了检索逻辑,还支持多种检索策略,能提升检索精度。
直接从FAISS向量数据库创建检索器,最基础的检索方式,支持相似性检索。
import os from langchain_community.vectorstores import FAISS from langchain_huggingface import HuggingFaceEmbeddings from langchain_core.retrievers import BaseRetriever from langchain_core.documents import Document # 本地Qwen嵌入模型路径 embedding_model_name = "./models/Qwen/Qwen3-Embedding-0___6B" # 初始化本地CPU运行的嵌入模型 embeddings = HuggingFaceEmbeddings( model_name=embedding_model_name, model_kwargs={ "device": "cpu" # 强制使用CPU运行 }, encode_kwargs={ "normalize_embeddings": True # 归一化向量,提升检索效果 } ) # 加载已有的FAISS向量数据库 try: vector_db = FAISS.load_local( folder_path="./faiss_db", # 之前的持久化路径 embeddings=embeddings, allow_dangerous_deserialization=True, index_name="local_cpu_faiss_index" ) print("FAISS向量库加载成功!") except FileNotFoundError: raise FileNotFoundError("未找到 ./faiss_db 文件夹,请确认向量库已正确保存") except Exception as e: raise RuntimeError(f"加载FAISS向量库失败:{str(e)}") # 创建检索器(v0.1+ 规范) retriever: BaseRetriever = vector_db.as_retriever( search_kwargs={"k": 3}, # 每次检索返回3个最相关的片段 # 可选:按分数阈值检索(按需启用) # search_type="similarity_score_threshold", # search_kwargs={"k": 3, "score_threshold": 0.5} ) # 测试检索 query = "LangChain的SequentialChain有什么用?" try: # 核心修改:替换 get_relevant_documents → invoke(Runnable接口标准) retrieved_docs: list[Document] = retriever.invoke(query) print(f"\n检索到的相关片段({len(retrieved_docs)}个):") for i, doc in enumerate(retrieved_docs): print(f"\n片段{i+1}:") print(f"内容:{doc.page_content}") print(f"来源文件:{doc.metadata.get('source', '未知')}") # 如需获取检索评分(补充完整可运行的评分获取逻辑) print("\n===== 带评分的检索结果 =====") docs_with_scores = vector_db.similarity_search_with_score(query, k=3) for i, (doc, score) in enumerate(docs_with_scores): print(f"\n片段{i+1}(相关性评分:{round(score, 4)}):") print(f"内容:{doc.page_content}") except Exception as e: raise RuntimeError(f"检索向量库失败:{str(e)}")
检索器的优势:可以直接和LangChain的链结合(比如后续的检索-生成链),不用单独写检索逻辑,简化代码。
基础的相似性检索(Similarity Search)可能会有一个问题:检索到的片段内容太相似,导致信息冗余。
而MMR(Maximum Marginal Relevance,最大边际相关性)检索能解决这个问题——在保证相关性的同时,尽量选择多样化的片段,避免冗余。
比如你问“LangChain的链有哪些?”,相似性检索可能返回3个都讲线性链的片段;而MMR检索会返回1个线性链、1个路由链、1个并行链的片段,信息更全面。
import os from langchain_community.vectorstores import FAISS # v0.1+ 正确导入路径 from langchain_huggingface import HuggingFaceEmbeddings # 本地模型用这个 from langchain_core.retrievers import BaseRetriever from langchain_core.documents import Document # 1. 配置本地Qwen嵌入模型(替换原API版QwenEmbeddings) embedding_model_name = "./models/Qwen/Qwen3-Embedding-0___6B" # 初始化本地CPU运行的嵌入模型 embeddings = HuggingFaceEmbeddings( model_name=embedding_model_name, model_kwargs={ "device": "cpu", # 强制CPU运行,无需GPU # 如需加载量化模型,可添加以下配置(按需) # "trust_remote_code": True, # "load_in_8bit": False }, encode_kwargs={ "normalize_embeddings": True # 归一化向量,提升检索效果 } ) # 2. 加载FAISS向量数据库 try: vector_db = FAISS.load_local( folder_path="./faiss_db", embeddings=embeddings, allow_dangerous_deserialization=True, index_name="local_cpu_faiss_index" # 需和保存时的索引名一致 ) print("FAISS向量库加载成功!") except FileNotFoundError: raise FileNotFoundError("未找到 ./faiss_db 文件夹,请确认向量库已正确保存") except Exception as e: raise RuntimeError(f"加载FAISS向量库失败:{str(e)}") # 3. 创建不同类型的检索器(保留相似性+MMR对比) # 3.1 相似性检索(默认) retriever_similar: BaseRetriever = vector_db.as_retriever( search_type="similarity", # 检索类型:纯相似性 search_kwargs={"k": 3} # 返回3个最相似的片段 ) # 3.2 MMR检索(最大化边际相关性,兼顾相关性和多样性) retriever_mmr: BaseRetriever = vector_db.as_retriever( search_type="mmr", # 检索类型:MMR search_kwargs={ "k": 3, # 最终返回3个片段 "fetch_k": 10, # 先检索10个最相关的,再从中选多样化的 "lambda_mult": 0.5 # 权重:0=只看多样性,1=只看相关性,0.5平衡 } ) # 4. 测试对比两种检索方式(适配v0.1+的invoke方法) query = "LangChain的链有哪些类型?" # 4.1 相似性检索测试 print("=== 相似性检索结果 ===") try: similar_docs: list[Document] = retriever_similar.invoke(query) # 替换get_relevant_documents for i, doc in enumerate(similar_docs): print(f"\n片段{i+1}:{doc.page_content[:100]}...") print(f"来源文件:{doc.metadata.get('source', '未知')}") except Exception as e: raise RuntimeError(f"相似性检索失败:{str(e)}") # 4.2 MMR检索测试 print("\n=== MMR检索结果 ===") try: mmr_docs: list[Document] = retriever_mmr.invoke(query) # 替换get_relevant_documents for i, doc in enumerate(mmr_docs): print(f"\n片段{i+1}:{doc.page_content[:100]}...") print(f"来源文件:{doc.metadata.get('source', '未知')}") except Exception as e: raise RuntimeError(f"MMR检索失败:{str(e)}") # 可选:补充MMR检索的评分对比(便于理解差异) print("\n===== 相似性检索(带评分) =====") similar_docs_with_score = vector_db.similarity_search_with_score(query, k=3) for i, (doc, score) in enumerate(similar_docs_with_score): print(f"片段{i+1}(评分:{round(score,4)}):{doc.page_content[:80]}...")
小技巧:如果你的知识库内容比较单一,用相似性检索就行;如果知识库内容丰富,需要全面的信息,用MMR检索更好。lambda_mult参数可以调,比如0.7更偏向相关性,0.3更偏向多样性。
咱们通过一个完整案例,验证不同检索器参数对检索效果的影响。
第一步:环境准备与向量数据库加载
沿用之前创建的FAISS向量数据库(以qwen-embedding-0.6b为例),先完成环境初始化和数据库加载:
from langchain_community.vectorstores import FAISS from langchain_huggingface import HuggingFaceEmbeddings embedding_model_name = "./models/Qwen/Qwen3-Embedding-0___6B" # 初始化本地CPU运行的嵌入模型 embeddings = HuggingFaceEmbeddings( model_name=embedding_model_name, model_kwargs={ "device": "cpu", # 强制CPU运行,无需GPU # 如需加载量化模型,可添加以下配置(按需) # "trust_remote_code": True, # "load_in_8bit": False }, encode_kwargs={ "normalize_embeddings": True # 归一化向量,提升检索效果 } ) # 加载已有的FAISS向量数据库 vector_db = FAISS.load_local( folder_path="./faiss_db", # 之前存储向量的路径 embeddings=embeddings, allow_dangerous_deserialization=True ) print("向量数据库加载成功!")
第二步:配置不同参数的检索器
我们配置4种不同参数的检索器,分别验证“返回数量(k值)”“检索类型(相似性/MMR)”“多样性权重(lambda_mult)”对结果的影响:
retriever_similar_k2 = vector_db.as_retriever( search_type="similarity", search_kwargs={"k": 2} ) # 4.2 相似性检索(k=5) retriever_similar_k5 = vector_db.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) # 4.3 MMR检索(偏向相关性,lambda_mult=0.8) retriever_mmr_high_rel = vector_db.as_retriever( search_type="mmr", search_kwargs={"k": 3, "fetch_k": 10, "lambda_mult": 0.8} ) # 4.4 MMR检索(偏向多样性,lambda_mult=0.3) retriever_mmr_high_div = vector_db.as_retriever( search_type="mmr", search_kwargs={"k": 3, "fetch_k": 10, "lambda_mult": 0.3} ) # 5. 定义测试查询并执行检索(适配v0.1+ invoke方法) test_query = "RAG系统的核心价值是什么?"
第三步:执行检索并对比结果
分别调用4个检索器,打印检索结果,对比不同参数的效果差异:
def test_retriever(retriever: BaseRetriever, retriever_name: str): """测试检索器并打印结果""" try: # 核心适配:使用invoke()替代旧的get_relevant_documents() results: list[Document] = retriever.invoke(test_query) print(f"=== {retriever_name} 检索结果(共{len(results)}条) ===") for i, doc in enumerate(results): print(f"\n[{i+1}] 内容:{doc.page_content[:120]}...") print(f" 来源:{doc.metadata.get('source', '未知')}") print("\n" + "-"*80 + "\n") except Exception as e: raise RuntimeError(f"{retriever_name} 检索失败:{str(e)}") # 执行所有检索并打印结果 test_retriever(retriever_similar_k2, "相似性检索(k=2)") test_retriever(retriever_similar_k5, "相似性检索(k=5)") test_retriever(retriever_mmr_high_rel, "MMR检索(偏向相关性 λ=0.8)") test_retriever(retriever_mmr_high_div, "MMR检索(偏向多样性 λ=0.3)") # 可选:补充相似性评分展示(FAISS特有) print("=== 相似性检索(k=3)带评分结果 ===") docs_with_scores = vector_db.similarity_search_with_score(test_query, k=3) for i, (doc, score) in enumerate(docs_with_scores): print(f"\n[{i+1}] 评分:{round(score, 4)} | 内容:{doc.page_content[:80]}...")
第四步:检索结果分析与参数选择建议
运行上述代码后,会得到类似以下的结果(忽视内容部分),也可以根据自己的知识库自己的问题来比较
我们结合结果分析参数的影响:
向量数据库加载成功! === 相似性检索(k=2) 检索结果 === 片段1(字符数:286): RAG 的核心价值:提升应用准确性、拓展知识边界、降低微调成本 相比直接使用大模型,RAG有三个核心价值,也是企业为什么愿意用RAG的原因:1. 提升准确性:答案基于真实的参考资料,大大减少事实性错误。比如问公司内部政策,RAG会从公司文档中检索准确信息,而不是让大模型瞎编;2. 拓展知识边界:大模型可以获取训练数据之外的知识... 相似度评分:0.1862(越小越相关) 片段2(字符数:215): 1. 提升准确性:答案基于真实的参考资料,大大减少事实性错误。比如问公司内部政策,RAG会从公司文档中检索准确信息,而不是让大模型瞎编;2. 拓展知识边界:大模型可以获取训练数据之外的知识,包括最新信息(比如2025年的行业数据)和私有信息(比如公司内部文档、客户资料);3. 降低成本:更新知识不需要重新训练大模型(微调成本很高,动辄几十万)... 相似度评分:0.1987(越小越相关) === 相似性检索(k=5) 检索结果 === (前2个片段与上述一致,新增3个相关性较低的片段) 片段3(字符数:198): RAG的全称是Retrieval-Augmented Generation(检索增强生成),核心逻辑超简单:在大模型生成答案之前,先从外部知识库中检索相关的真实、最新信息,把这些信息作为“参考资料”传给大模型,让大模型基于参考资料生成答案。 相似度评分:0.2345 ... === MMR检索(偏向相关性,lambda=0.8) 检索结果 === 片段1(字符数:286): RAG 的核心价值:提升应用准确性、拓展知识边界、降低微调成本 相比直接使用大模型,RAG有三个核心价值,也是企业为什么愿意用RAG的原因:1. 提升准确性:答案基于真实的参考资料,大大减少事实性错误。比如问公司内部政策,RAG会从公司文档中检索准确信息,而不是让大模型瞎编;2. 拓展知识边界:大模型可以获取训练数据之外的知识... 片段2(字符数:198): RAG的全称是Retrieval-Augmented Generation(检索增强生成),核心逻辑超简单:在大模型生成答案之前,先从外部知识库中检索相关的真实、最新信息,把这些信息作为“参考资料”传给大模型,让大模型基于参考资料生成答案。 片段3(字符数:182): 大模型的两个致命缺点,让它在企业级应用中“寸步难行”:1. 知识滞后:大模型的知识截止于训练数据的时间,比如2023年训练的模型,不知道2024年之后发生的事。就像一个人很久没上网,不知道最新的热点新闻;2. 事实性错误(幻觉):大模型会“一本正经地胡说八道”... === MMR检索(偏向多样性,lambda=0.3) 检索结果 === 片段1(字符数:286): RAG 的核心价值:提升应用准确性、拓展知识边界、降低微调成本 相比直接使用大模型,RAG有三个核心价值,也是企业为什么愿意用RAG的原因:1. 提升准确性:答案基于真实的参考资料,大大减少事实性错误。比如问公司内部政策,RAG会从公司文档中检索准确信息,而不是让大模型瞎编;2. 拓展知识边界:大模型可以获取训练数据之外的知识... 片段2(字符数:175): RAG适用场景与不适用场景辨析 RAG不是万能的,要根据场景选择是否使用。下面列出适用和不适用的场景,帮大家避坑:适用场景:1. 企业内部知识库问答(比如员工手册、产品文档、技术手册);2. 行业报告/政策文件问答... 片段3(字符数:168): 向量存储与嵌入(Vector Storage & Embedding):给文本做“数字指纹” 分割完文本片段后,下一步要做的是「向量嵌入」和「向量存储」。这一步是RAG能实现“精准检索”的核心——把文字变成计算机能理解的“数字”,再存到专门的向量数据库里...
基于结果,总结参数选择建议:
踩坑指南:参数没有“最优值”,需结合知识库规模和业务场景调试!比如小知识库(100个片段以内),k=2即可;大知识库(1000+片段),k可适当增大到4-5,同时用MMR检索保证信息多样性。
前面我们已经完成了“文档加载→文本分割→向量存储→检索”的所有环节,最后一步是“生成”——将检索到的相关片段和用户问题结合,传给大模型生成最终答案。基于LangChain最新推荐的LCEL(LangChain Expression Language)风格,我们采用FAISS向量数据库和本地Qwen3嵌入模型,搭建端到端的RAG问答系统。
核心逻辑:用户提问→检索器从FAISS向量数据库获取相关片段→将“用户问题+相关片段”按指定规则拼接→传给大模型→大模型基于相关片段生成答案。
LangChain 1.x版本通过LCEL实现检索与生成的模块化协同,核心优势在于组件化串联与数据流式传递:无需依赖create_retrieval_chain等封装函数,直接通过管道符(|)将检索器、文档格式化函数、提示词模板、大模型、输出解析器等组件串联成链。数据在链中按顺序流转,上一个组件的输出自动作为下一个组件的输入,既简化了代码逻辑,又提升了自定义灵活性(如可随时插入日志、数据过滤等自定义组件)。
基于前面构建的FAISS向量数据库和检索器,搭建端到端的RAG问答系统,全程使用本地CPU运行嵌入模型,无需GPU依赖。以下是LangChain 1.x版本官方推荐写法:
第一步:环境准备与组件初始化
from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_openai import ChatOpenAI # 1.x推荐用ChatOpenAI,适配对话模型,功能更全 from langchain_core.prompts import ChatPromptTemplate # 替代PromptTemplate,适配LCEL from langchain_core.runnables import RunnablePassthrough # LCEL核心组件,传递数据 from langchain_core.output_parsers import StrOutputParser # 统一输出格式解析 from dotenv import load_dotenv import os # 加载并验证环境变量(1.x推荐显式验证,避免配置缺失) load_dotenv() api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("未找到OPENAI_API_KEY环境变量,请检查.env文件配置") # 1. 初始化本地CPU运行的Qwen3嵌入模型(参数兼容1.x版本,保持原推荐配置) embedding_model_path = "./models/Qwen/Qwen3-Embedding-0___6B" # 验证模型路径有效性 if not os.path.exists(embedding_model_path): raise FileNotFoundError(f"Qwen3嵌入模型路径不存在:{embedding_model_path}") embeddings = HuggingFaceEmbeddings( model_name=embedding_model_path, model_kwargs={ "device": "cpu", # 强制CPU运行,无需GPU "trust_remote_code": True, # Qwen3模型必选配置,信任远程代码 # 如需加载量化模型,可启用以下配置(降低内存占用) # "load_in_8bit": True, }, encode_kwargs={ "normalize_embeddings": True # 归一化向量,提升检索相似度计算精度 } ) # 2. 加载FAISS向量数据库(1.x版本用法完全兼容,保留原逻辑) # 注意:首次构建时需用FAISS.from_documents(docs, embeddings)创建并save_local vector_db = FAISS.load_local( folder_path="./faiss_db", embeddings=embeddings, allow_dangerous_deserialization=True, # 本地开发可用,生产环境需谨慎(存在安全风险) index_name="local_cpu_faiss_index" # 确保加载正确的索引文件 ) # 3. 初始化检索器(MMR策略,平衡相关性和多样性,参数无变化) retriever = vector_db.as_retriever( search_type="mmr", search_kwargs={"k": 3, "fetch_k": 10, "lambda_mult": 0.7} ) # 4. 初始化大模型(1.x推荐用ChatOpenAI,支持更丰富的对话配置) llm = ChatOpenAI( api_key=api_key, temperature=0.3, # 低温度保证答案精准,减少幻觉 timeout=30, # 新增超时配置,避免请求挂起(1.x官方推荐) max_retries=2 # 网络波动时自动重试,提升稳定性 )
第二步:用LCEL构建检索-生成链(最新推荐写法)
通过LCEL管道符(|)串联组件,逻辑更清晰、自定义性更强。核心步骤:格式化检索文档→拼接提示词→大模型生成→解析输出。
# 1. 自定义文档格式化函数(将检索到的多个文档拼接为统一文本,供提示词使用) def format_docs(docs): """格式化检索到的文档片段,用空行分隔""" return "\n\n".join([doc.page_content for doc in docs]) # 2. 自定义提示词模板(1.x推荐用ChatPromptTemplate,通过from_messages创建) # 保持原业务规则:基于参考资料、分点说明、带案例 system_prompt = """你是一个专业的RAG系统问答助手,必须基于以下提供的参考资料(context)回答用户问题。 规则: 1. 答案必须严格基于参考资料,不能编造未提及的信息; 2. 语言简洁明了,分点说明(如果有多个要点); 3. 每个要点搭配1个简单案例,帮助理解。 参考资料:{context}""" custom_prompt = ChatPromptTemplate.from_messages([ ("system", system_prompt), # 系统指令 ("human", "{question}") # 用户问题(1.x推荐用"question"键,语义更清晰) ]) # 3. 用LCEL构建完整检索-生成链(管道符串联组件,数据流式传递) rag_qa_chain = ( # 第一步:并行处理输入(传递用户问题+检索文档) {"context": retriever | format_docs, "question": RunnablePassthrough()} # 第二步:将格式化数据传入提示词模板 | custom_prompt # 第三步:传入大模型生成答案 | llm # 第四步:解析输出(统一为字符串格式) | StrOutputParser() ) # 补充:如需返回检索的源文档(用于验证答案来源),可调整链结构: rag_qa_chain_with_sources = ( { "context": retriever | format_docs, "question": RunnablePassthrough(), "source_documents": retriever # 保留原始检索文档 } | custom_prompt | llm | StrOutputParser() )
第三步:测试RAG系统并验证结果
1.x版本统一用invoke方法执行链,参数为字典格式,获取结果后可直接打印答案和源文档
# 测试问题列表(覆盖不同类型的查询) test_questions = [ "RAG系统的核心价值是什么?", "RAG和直接使用大模型相比,优势在哪里?", "企业内部知识库问答为什么适合用RAG?" ] # 执行测试并打印结果 for i, question in enumerate(test_questions): print(f"\n===== 测试问题{i+1}:{question} =====") # 执行RAG链(1.x统一用invoke方法) result = rag_qa_chain_with_sources.invoke(question) # 带源文档的链 # 打印生成的答案 print("\n生成答案:") print(result) # 打印参考资料(验证答案来源) print("\n参考资料:") # 注意:源文档从链的输入参数中获取(因链结构中保留了source_documents) sources = retriever.invoke(question) # 重新调用检索器获取源文档(或在链中传递) for j, doc in enumerate(sources): print(f"\n参考片段{j+1}:") print(doc.page_content) if doc.metadata: # 打印文档元数据(如文件名、页码等) print(f"元数据:{doc.metadata}")
第四步:运行结果与系统验证
运行代码后,将得到清晰的答案、参考资料及对应关系。核心验证点:
小技巧:RetrievalQA的chain_type除了“stuff”,还有“map_reduce”“refine”等。如果检索片段较多、较长,建议用“map_reduce”(先分别处理每个片段,再汇总答案),避免提示词超过大模型上下文窗口限制。
搭建完基础RAG系统后,实际应用中可能会遇到检索不准、生成答案质量低等问题。下面总结常见问题及对应的优化方向,帮助大家提升系统性能。
| 常见问题 | 可能原因 | 排查与解决方法 |
|---|---|---|
| 检索结果不相关 | 1. 文本分割不合理(片段过大/过小,语义不完整);2. 嵌入模型不适合中文场景;3. 检索参数设置不当(k值过大、MMR多样性过高) | 1. 调整分割参数(chunk_size=200-500字,用RecursiveCharacterTextSplitter);2. 替换为中文优化的嵌入模型(如通义千问embedding-v2);3. 减小k值(k=2-3),降低MMR多样性权重(lambda_mult=0.7-0.8) |
| 生成答案遗漏关键信息 | 1. 检索片段未覆盖关键信息;2. 提示词未明确要求“全面回答”;3. 大模型temperature过低,生成过于简洁 | 1. 增大k值(k=4-5),改用相似性检索保证核心信息覆盖;2. 优化提示词,添加“全面覆盖参考资料中的所有关键要点”;3. 适当提高temperature(0.4-0.5) |
| 生成答案包含编造信息 | 1. 提示词未严格限制“基于参考资料”;2. 检索片段相关性低,大模型无足够信息生成答案;3. 大模型temperature过高 | 1. 优化提示词,明确“禁止编造信息,无相关信息则回复‘无法回答’”;2. 提升检索精度(调整分割、嵌入模型、检索参数);3. 降低temperature(0.2-0.3) |
| 系统响应速度慢 | 1. 检索片段过多(k值过大);2. 向量数据库未优化(未持久化、片段数量过多);3. 大模型推理速度慢 | 1. 减小k值(k=2-3);2. 优化向量数据库(定期清理无效片段、使用更高效的向量数据库如FAISS);3. 替换为轻量型大模型(如GPT-3.5-turbo-instruct替代GPT-4) |
如果基础RAG系统满足不了业务需求,可以从以下3个方向进阶优化,提升系统性能:
这些进阶优化方向在企业级RAG应用中非常重要,后续章节会结合具体业务场景深入讲解。
以下部分内容,更加的专业化,工程化,适合在职或有工作经验的同学学习。
搭建完RAG系统并不意味着结束,实际应用中需要通过科学的评估发现问题,再针对性调优,才能让系统稳定输出高质量结果。
本节将从核心评估指标、评估方法、调优方向和工程化实践四个维度,完整覆盖RAG系统“评估-调优”的闭环流程。
评估RAG系统的效果,核心围绕“能否精准找到有用信息”“答案是否正确可用”“用户等待时间是否可接受”三个核心诉求,对应三个关键指标:相关性、准确性、响应速度。每个指标都有明确的定义和可量化的衡量标准。
| 指标名称 | 核心定义 | 衡量标准与计算方式 | 优化目标 |
|---|---|---|---|
| 相关性(Retrieval Relevance) | 检索器返回的文本片段与用户问题的语义关联程度 | 1. 精确率(Precision):检索到的相关片段数 / 检索到的总片段数; 2. 召回率(Recall):检索到的相关片段数 / 知识库中所有相关片段数; 3. F1分数:2×(精确率×召回率)/(精确率+召回率)(综合精确率和召回率) | F1分数≥0.8,确保检索结果“既准又全” |
| 准确性(Generation Accuracy) | 大模型生成的答案与参考资料(检索片段)的一致性、事实正确性 | 1. 事实一致性(Factuality):人工或自动化工具判断答案是否符合检索片段中的事实; 2. 信息完整性(Completeness):答案是否覆盖用户问题所需的所有核心信息; 3. 错误率:包含编造信息、错误关联的答案占比 | 事实一致性≥95%,错误率≤5%,核心信息覆盖率≥90% |
| 响应速度(Response Speed) | 用户发起提问到获取最终答案的总耗时,含检索耗时和生成耗时 | 1. 检索耗时:从接收问题到获取相关片段的时间; 2. 生成耗时:从传入检索片段到生成最终答案的时间; 3. 总耗时=检索耗时+生成耗时 | 总耗时≤2秒(普通问答场景),≤5秒(复杂文档问答场景) |
小提示:三个指标存在一定权衡关系。比如增大检索片段数量(k值)可能提升召回率,但会增加响应速度;提高大模型temperature可能提升答案丰富度,但可能降低准确性。实际优化需结合业务场景优先级调整。
根据评估成本和效率,RAG系统的评估方法分为两类:人工评估(精准但低效,适合小规模验证)和自动化评估(高效但需校准,适合大规模迭代)。
实际项目中通常结合使用,先通过自动化工具快速筛选问题,再用人工评估精准验证核心场景。
人工评估适合对核心业务场景(如高价值客户咨询、关键政策问答)的效果验证,需要制定清晰的评估标准和流程,避免主观偏差。
1. 评估流程
2. 打分标准示例(以准确性为例)
| 分数等级 | 标准描述 |
|---|---|
| 5分 | 答案完全符合检索片段事实,核心信息完整,逻辑清晰,无任何编造内容 |
| 3分 | 答案基本符合事实,但遗漏1-2个非关键信息,或表述存在轻微歧义 |
| 1分 | 答案存在事实错误,或编造检索片段中未提及的信息,无法满足用户需求 |
RAGAS是专门针对RAG系统的评估框架,支持评估相关性、准确性、完整性等核心指标,且无需标注正确答案(仅需问题、检索片段、系统答案),使用更便捷
# 安装依赖:pip install ragas datasets faiss-cpu # 补充:安装FAISS依赖 from ragas import evaluate from ragas.metrics.collections import ( ContextPrecision, # 检索精确率 ContextRecall, # 检索召回率 Faithfulness, # 事实一致性 AnswerRelevancy # 答案相关性 ) from datasets import Dataset from langchain_community.vectorstores import FAISS from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser from dotenv import load_dotenv import os load_dotenv() api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("未找到OPENAI_API_KEY环境变量") # 1. 初始化RAG系统组件 embedding_model_path = "./models/Qwen/Qwen3-Embedding-0___6B" if not os.path.exists(embedding_model_path): raise FileNotFoundError(f"Qwen3嵌入模型路径不存在:{embedding_model_path}") embeddings = HuggingFaceEmbeddings( model_name=embedding_model_path, model_kwargs={ "device": "cpu", "trust_remote_code": True, }, encode_kwargs={ "normalize_embeddings": True } ) # 加载FAISS向量库 faiss_db_path = "./faiss_db" if not os.path.exists(faiss_db_path): raise FileNotFoundError(f"FAISS向量数据库路径不存在:{faiss_db_path}") vector_db = FAISS.load_local( folder_path=faiss_db_path, embeddings=embeddings, allow_dangerous_deserialization=True ) retriever = vector_db.as_retriever(search_kwargs={"k": 3}) # 定义提示词 system_prompt = "基于以下上下文回答问题:{context}" prompt = ChatPromptTemplate.from_messages([ ("system", system_prompt), ("human", "{question}") ]) # 文档格式化函数 def format_docs(docs): return "\n\n".join([doc.page_content for doc in docs]) # 修复LCEL链语法(管道符连接) rag_qa_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | ChatOpenAI(api_key=api_key, temperature=0.3) | StrOutputParser() ) # 2. 构建测试数据集 test_questions = [ "RAG系统的核心价值是什么?", "SequentialChain的作用是什么?", "RAG系统的构建流程有哪些步骤?" ] # 3. 采集RAG输出结果 test_data = [] for question in test_questions: answer = rag_qa_chain.invoke(question) retrieved_docs = retriever.invoke(question) contexts = [doc.page_content for doc in retrieved_docs] test_data.append({ "question": question, "answer": answer, "contexts": contexts }) # 4. 转换为RAGAS标准格式 dataset = Dataset.from_list(test_data) # 5. 评估指标 metrics = [ContextPrecision(), ContextRecall(), Faithfulness(), AnswerRelevancy()] # 6. 执行评估 results = evaluate( dataset=dataset, metrics=metrics, llm=ChatOpenAI(api_key=api_key, temperature=0) ) # 7. 输出结果 print("RAG系统自动化评估结果:") print(results)
踩坑指南:自动化评估工具的结果需人工校准。比如RAGAS的事实一致性评分可能存在偏差,对于核心场景的评估结果,建议抽取10%-20%的样本进行人工复核,确保评估准确性。
根据评估结果定位问题后,需针对性调优。RAG系统的调优核心围绕“检索环节”(检索是生成的基础,检索不准则生成必错),主要包括三个方向:文本分割策略、向量模型选择、检索参数配置。
文本分割是RAG的“基础工程”,分割不合理会直接导致检索相关性低。优化需结合文档类型和问题场景调整,核心是保证片段的“语义完整性”和“大小适中”。
| 常见问题 | 优化方案 | 实操建议 |
|---|---|---|
| 片段过大(超过500字),检索时无关信息过多 | 减小chunk_size,优化分割符优先级 | 中文文档:chunk_size=200-300字,分割符顺序:\n\n→\n→。→,;英文文档:chunk_size=200-300token,分割符顺序:\n\n→\n→.→,→ |
| 片段过小(小于100字),丢失上下文信息 | 增大chunk_size,增加chunk_overlap | chunk_size调整为300-400字,chunk_overlap=50-80字,避免跨片段的知识点被切断 |
| 结构化文档(如MD、PDF)分割后破坏层级关系 | 使用针对性分割器,保留层级信息 | MD文档用MarkdownTextSplitter,PDF文档用PyPDFLoader按页分割后再二次细分(按段落) |
向量模型的质量直接决定文本嵌入的语义表征能力,进而影响检索相关性。优化需结合语言类型(中文/英文)、业务场景(通用/专业领域)选择合适的模型,必要时进行模型微调。
| 模型类型 | 代表模型 | 优势 | 适用场景 |
|---|---|---|---|
| 通用英文模型 | EmbeddingGemma(谷歌,开源)、Yuan-EB 2.0-en(浪潮信息,开源) | 语义表征能力强,多语言支持(覆盖100+种语言),轻量化易部署,部分支持自定义向量维度,兼顾速度与精度 | 英文通用文档问答(如国际新闻、英文技术文档、跨语言信息检索) |
| 通用中文模型 | Qwen3-Embedding(通义千问,开源)、Yuan-EB 2.0-zh(浪潮信息,开源) | 针对中文语义深度优化,检索与排序任务双SOTA,轻量化参数(0.3B-0.6B),支持本地部署,部分具备多模态检索能力 | 中文通用场景(如企业内部中文手册、中文客服问答、中文语义搜索) |
| 专业领域模型 | 医疗领域:Claude Opus 4.5(Anthropic);法律领域:法衡-R2(东南大学) | 医疗模型支持专业病历处理与医疗数据库接入,合规性强;法律模型具备复杂司法推理与裁判文书深度理解能力,术语表征精准 | 医疗领域(病历解读、临床试验分析、理赔申诉);法律领域(法条检索、案件争议焦点总结、裁判文书分析);金融等其他专业领域可基于通用SOTA模型微调 |
实操技巧:如果通用模型效果不佳,可尝试“模型对比测试”——用多个模型对同一批测试数据进行嵌入和检索,通过F1分数对比选择最优模型。
检索参数的调整直接影响检索结果的“相关性”和“多样性”,需结合评估指标(精确率、召回率、F1分数)动态调整,核心参数包括k值、检索类型、MMR相关参数。
| 参数类型 | 核心参数 | 调整逻辑 | 实操建议 |
|---|---|---|---|
| 基础参数 | k(返回片段数量) | 精确率低→减小k值;召回率低→增大k值 | 通用场景k=3-4;复杂问题k=4-5;简单问题k=2-3 |
| 检索类型 | similarity(相似性)/mmr(最大边际相关性) | 需精准核心信息→similarity;需全面多样化信息→mmr | 简单问答用similarity;复杂对比、多维度分析用mmr |
| MMR参数 | lambda_mult(相关性-多样性权重) | 需优先相关性→增大lambda_mult;需优先多样性→减小lambda_mult | 通用场景lambda_mult=0.7-0.8;多样化需求lambda_mult=0.4-0.5 |
在企业级RAG项目中,调优不是“一次性操作”,而是“评估-调优-验证”的循环过程。结合实际项目经验,总结出以下工程化调优流程,确保调优效果稳定、可复现。
工程化技巧:建立“调优配置中心”,将分割参数、向量模型、检索参数等配置项集中管理,支持动态切换配置并对比效果,避免硬编码导致的调优效率低下。
本章围绕LangChain的核心应用——链式工作流和RAG系统,从原理、实操、评估调优三个维度展开,覆盖了从“简单链”到“企业级RAG系统”的全链路知识。以下是核心知识点和技术落地要点的总结,帮助大家梳理脉络、巩固重点。
1. 链式工作流:让大模型按步骤解决复杂问题
2. RAG 系统:解决大模型知识滞后与事实性错误
通过分层练习巩固本章核心知识,从基础的链式工作流实现,到进阶的RAG系统构建与调优,再到拓展的工程化能力提升,逐步提升大模型应用开发实战能力。
练习任务
实现“新闻文本分类→提取核心事件→生成摘要”的多步骤任务:
练习任务
练习任务
提升RAG系统的工程化能力,通过错误处理和分支降级机制,确保系统在异常情况下仍能正常响应。