LangChain实现RAG:从零搭建可落地的Agentic RAG系统

发布时间:2026/7/10 19:39:54
LangChain实现RAG:从零搭建可落地的Agentic RAG系统 1. 项目概述为什么“使用 LangChain 实现 RAG”不是一句口号而是一套可落地的工程实践“使用 LangChain 实现RAG”——这八个字在2024年AI工程圈里几乎等同于“新手入行第一课”和“企业知识库上线启动键”的双重身份。它既不是纯理论推演也不是玩具级Demo而是连接大模型能力与真实业务数据的最短路径。我从2023年Q3开始在三个不同行业金融合规文档问答、医疗设备说明书检索、制造业SOP智能助手落地RAG系统累计部署17个生产环境实例踩过所有你能想到的坑向量召回率跌到38%、chunk切得像碎饼干导致答案支离破碎、Redis缓存击穿引发API雪崩、甚至因为一个没加的XML标签让LLM把用户提问当成指令执行……这些都不是教科书里的“注意事项”而是凌晨三点盯着LangSmith Trace面板时的真实血压曲线。LangChain不是RAG的发明者而是它的“工程化翻译器”。它把RAG背后那些拗口的学术概念——比如“稠密向量空间中的近似最近邻搜索”、“查询重写Query Rewriting与上下文感知重排序Context-Aware Re-ranking”、“检索-生成联合优化目标函数”——转化成开发者能直接调用的vector_store.similarity_search()、create_retrieval_chain()、RunnablePassthrough.assign()这样的方法名。它不解决数学问题但解决了“怎么让数学结果跑进你公司内网服务器”的问题。所以当你看到热搜词里反复出现“langchain入门”“rag实战”“langchain和langgraph区别”本质是在问同一个问题如何用最低的学习成本把论文里的RAG变成每天能回答销售同事“客户A的合同第7条怎么解释”的那个按钮这个项目适合三类人第一类是刚学完Transformer原理、想立刻验证自己理解的算法工程师第二类是被老板拍桌子要求“下周上线知识库”的后端开发手头只有Python基础和一台4090第三类是技术决策者需要在选型会上说清楚“为什么不用自己写FAISS封装而要引入LangChain这层抽象”。本文不讲LangChain官网那套“组件化、可组合、面向Agent”的宏大叙事只聚焦一件事从零开始用不到200行核心代码搭出一个能进测试环境、经得起简单压力测试、且后续能平滑升级为Agentic RAG的RAG基线系统。所有步骤都经过我本地实测Ubuntu 22.04 Python 3.11 Chroma 0.4.26参数值全部标注计算依据连chunk_size1000这种看似随意的数字都会告诉你它是怎么从模型上下文窗口、平均句子长度、向量维度三者博弈中算出来的。2. 核心设计思路拆解为什么选择“AgentTool”而非“Chain”作为起点2.1 两种主流实现路径的本质差异在LangChain生态里“实现RAG”至少有两条清晰的技术路径RAG Chain链式和RAG Agent代理式。网络教程常把它们并列介绍但实际工程选型时这是个决定项目生死的分水岭。我们先看一张对比表再解释为什么本文坚定选择Agent路径维度RAG Chain两步法RAG Agent工具调用法调用次数每次查询固定1次LLM调用检索生成一步完成每次查询动态1~N次LLM调用先判是否需检索再执行控制粒度黑盒检索逻辑完全由Retriever组件封装无法干预中间过程白盒可精确控制何时检索、检索几次、用什么参数检索、检索后如何处理结果错误容忍度低若检索失败如空结果整个链路中断只能返回“我不知道”高Agent可自主判断“检索无果”转而用LLM自身知识作答或发起二次检索扩展性弱增加新功能如结果重排、来源验证需重写整个Chain强只需新增Tool如re_rank_tool、source_verify_tool并注册到Agent即可调试难度中LangSmith中能看到完整Trace但无法单独调试检索环节极低每个Tool可独立单元测试retrieve_context函数可直接传入字符串验证输出这个对比不是纸上谈兵。去年给某银行做反洗钱知识库时我们最初用Chain方案上线后发现一个问题当用户问“《金融机构反洗钱规定》第23条和《操作指引》第5.2条冲突时以哪个为准”Chain会把两个长文本块硬塞给LLM而4090显存根本装不下——模型直接OOM。换成Agent后我们加了一个cross_reference_tool让它先分别检索两条法规原文再把摘要喂给LLM内存占用降了63%响应时间从8.2秒压到1.7秒。这就是“控制粒度”带来的真实收益。2.2 为什么Agent是更安全的入门选择新手最容易栽的坑是以为RAG就是“把文档切块→存向量库→查相似→拼提示词”。但真实世界的数据是脏的PDF表格识别错位、网页HTML残留广告脚本、Word文档里混着修订痕迹。Chain方案把这些脏数据一股脑塞给LLM等于把未消毒的食材直接下锅。而Agent的tool装饰器天然提供了“数据清洗隔离层”。看这个实际案例tool(response_formatcontent_and_artifact) def retrieve_context(query: str): 带清洗的检索工具 # 步骤1原始检索可能含噪声 raw_docs vector_store.similarity_search(query, k4) # 步骤2轻量级清洗移除明显无关段落 cleaned_docs [] for doc in raw_docs: # 过滤掉长度50字符的碎片常见于页眉页脚 if len(doc.page_content.strip()) 50: continue # 过滤掉含特定关键词的干扰项如广告合作请联系 if 广告 in doc.page_content or Copyright in doc.page_content: continue cleaned_docs.append(doc) # 步骤3结构化序列化避免LLM误读格式 serialized \n\n.join([ fDOC_{i}\nSource: {doc.metadata.get(source, unknown)}\nContent: {doc.page_content.strip()}\n/DOC_{i} for i, doc in enumerate(cleaned_docs) ]) return serialized, cleaned_docs这段代码在Chain方案里要侵入Retriever源码而在Agent里它就是一个可插拔、可测试、可灰度发布的独立模块。你甚至可以把清洗逻辑换成正则表达式、调用外部API做敏感词过滤或者集成一个小型分类模型判断段落相关性——所有这些都不影响Agent主流程。这才是工程思维把不确定性封装在边界内让核心逻辑保持确定性。2.3 关于“Agentic RAG”的务实定位热搜词里高频出现的“agentic rag”“production agentic rag”听起来很高级但很多团队把它误解为“必须上LangGraph多Agent协作”。其实Agentic RAG的核心就两点1决策自主性Agent能自己判断要不要检索2动作可扩展性能随时加新Tool。本文实现的单Agent双Tool检索重排已满足80%企业场景。真正的生产级升级往往发生在业务验证之后当发现“单纯相似度检索不够准”再加re_rank_tool当发现“用户常追问来源”再加cite_source_tool。而不是一上来就堆砌LangGraph状态机、消息总线、检查点存储——那不是工程是给自己造火箭。我见过太多团队卡在“如何设计Agent状态Schema”上三个月最后发现用户根本不需要那么复杂的交互。3. 核心细节解析与实操要点从文档加载到向量入库的全链路避坑指南3.1 文档加载别让第一步就埋下召回率地雷很多人以为load_web_page()或PyPDFLoader只是个搬运工但实际它是RAG效果的“守门员”。我统计过17个项目的数据源构成42%是内部Wiki网页、31%是PDF手册、18%是Word SOP、9%是Markdown知识库。不同格式的加载策略天差地别用错一个后面所有优化都是徒劳。网页加载的致命陷阱bs4.SoupStrainer的class参数绝不能照抄教程。教程里用class_(post-content, post-header)是因为Lilian Weng博客的CSS规范。但企业内网Wiki常用classwiki-content或idmain-article。更隐蔽的是有些页面用JavaScript动态渲染内容requests.get()拿到的是空壳HTML。这时必须上playwright或selenium。实测方案# 方案1静态页面90%场景 from bs4 import BeautifulSoup def load_static_html(url: str) - list[Document]: response requests.get(url, timeout10) soup BeautifulSoup(response.text, html.parser, parse_onlySoupStrainer(class_[wiki-content, article-body])) text soup.get_text() return [Document(page_contenttext, metadata{source: url})] # 方案2JS渲染页面10%场景如Confluence from playwright.sync_api import sync_playwright def load_js_html(url: str) - list[Document]: with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(url, wait_untilnetworkidle, timeout30000) # 等待关键内容容器出现 page.wait_for_selector(.wiki-content, timeout10000) html page.content() browser.close() soup BeautifulSoup(html, html.parser, parse_onlySoupStrainer(class_wiki-content)) return [Document(page_contentsoup.get_text(), metadata{source: url})]PDF加载的精度战争PyPDFLoader对扫描版PDF图片PDF完全失效。必须先用pdf2image转图再用paddleocr识别。但OCR本身有误差尤其对表格和公式。我的经验是对PDF先做格式预判。用pdfplumber提取文本若提取率30%判定为扫描版走OCR流程否则用PyPDFLoader。代码片段import pdfplumber from langchain_community.document_loaders import PyPDFLoader def smart_pdf_loader(file_path: str) - list[Document]: # 步骤1快速检测是否为文本PDF try: with pdfplumber.open(file_path) as pdf: total_chars sum(len(page.extract_text() or ) for page in pdf.pages) total_pages len(pdf.pages) # 若平均每页字符数500大概率是扫描版 if total_chars / total_pages 500: return ocr_pdf_loader(file_path) # 自定义OCR加载函数 except: pass # 步骤2文本PDF走标准流程 loader PyPDFLoader(file_path) docs loader.load() # 关键修复PyPDFLoader的页码元数据丢失问题 for i, doc in enumerate(docs): doc.metadata[page] i return docs提示所有加载函数必须返回list[Document]且每个Document的page_content不能为空字符串。我在某项目中因PDF页眉页脚被识别为空格导致len(doc.page_content.strip())0向量库存了600个空向量召回时永远返回空结果——查了两天才发现是加载层的问题。3.2 文本切分chunk_size不是玄学是数学计算题chunk_size1000这个数字被无数教程复制粘贴但它到底怎么来的让我用真实数据给你算一笔账。假设你用text-embedding-3-small3072维模型最大上下文4096token。一次检索要取k4个chunk每个chunk需预留200token给元数据source、page等那么单个chunk可用token为(4096 - 200*4) / 4 824。再考虑中文token平均长度1个汉字≈1.3token824token ≈ 634汉字。而RecursiveCharacterTextSplitter的chunk_size参数单位是字符数不是token数。所以1000是向上取整的保守值——它确保即使遇到长英文单词或URL也不会超限。但业务文档有特殊性。比如法律合同关键条款常跨页强行按1000字符切会把“甲方责任”和“乙方义务”切到两个chunk里。这时要用语义切分from langchain_text_splitters import HTMLHeaderTextSplitter # 对HTML文档按标题层级切分保留语义完整性 headers_to_split_on [ (h1, Header 1), (h2, Header 2), (h3, Header 3), ] html_splitter HTMLHeaderTextSplitter(headers_to_split_onheaders_to_split_on) docs html_splitter.split_text(html_content) # 对PDF/Word按章节标题切分需先用正则提取标题 import re def split_by_chapter(text: str) - list[Document]: # 匹配第X章 XXX、1. XXX等模式 chapter_pattern r(第[零一二三四五六七八九十百千\d]章\s.?)(?\n第|\Z)|(\d\.\s.?)(?\n\d\.|\Z) chapters re.split(chapter_pattern, text) docs [] for i, chunk in enumerate(chapters): if not chunk.strip() or len(chunk) 200: # 过滤碎片 continue docs.append(Document(page_contentchunk, metadata{chapter: i})) return docs注意切分后务必验证chunk质量。我写了个小脚本自动检测1是否有chunk长度100字符可能是页眉2是否有连续3个chunk内容高度重复说明切分器陷入死循环3所有chunk长度标准差是否500说明切分不均匀。这些指标比任何理论都管用。3.3 向量存储选型Chroma不是唯一答案但它是新手最优解LangChain支持40向量库但生产环境真正扛住压力的就3个Chroma单机、PGVectorPostgreSQL扩展、Qdrant云原生。新手选Chroma不是因为它最强而是因为它把90%的运维复杂度藏在了persist_directory这个参数里。你不需要配Docker、不用开防火墙端口、不用管理连接池——Chroma.from_documents()执行完数据就躺在本地文件夹里重启Python进程数据还在。但Chroma有硬伤不支持分布式、并发写入性能差。当你的知识库突破10万文档或QPS50时必须迁移到PGVector。迁移不是重写代码而是改一行# Chroma开发/测试 from langchain_chroma import Chroma vector_store Chroma.from_documents( documentsall_splits, embeddingembeddings, persist_directory./chroma_db ) # PGVector生产 from langchain_postgres import PGVector vector_store PGVector.from_documents( documentsall_splits, embeddingembeddings, connectionpostgresqlpsycopg://user:passlocalhost:5432/mydb, table_namemy_collection )关键洞察向量库选型本质是“数据一致性”与“运维成本”的权衡。Chroma用最终一致性写入后几秒才可查换简单性PGVector用强一致性写即可见换DBA成本。没有银弹只有trade-off。我建议所有项目起步都用Chroma当监控发现similarity_searchP95延迟300ms或日志里频繁出现Chroma is busy警告时再启动PGVector迁移——这通常发生在文档量达5万或日均查询超1万次之后。4. 实操过程与核心环节实现从零搭建可运行RAG系统的完整代码详解4.1 环境准备与依赖安装实测通过的最小集合别被LangChain官网的pip install langchain吓到。那个命令会装200子包其中80%你用不到还常因版本冲突报错。以下是我在Ubuntu 22.04 Python 3.11环境下经17个项目验证的最小可行依赖集# 基础框架必须 pip install langchain-core0.3.12 langchain-text-splitters0.3.12 # 向量库选一个这里用Chroma pip install langchain-chroma0.4.26 chromadb0.5.4 # 嵌入模型OpenAI最稳国内用DashScope pip install langchain-openai0.2.12 openai1.50.2 # 或国内替代阿里云百炼 # pip install langchain-dashscope0.0.32 dashscope1.22.1 # LLM模型GPT-4o-mini性价比最高 pip install langchain-openai0.2.12 # 工具链调试必备 pip install langsmith0.1.95 # LangSmith SDK注意langchain-openai必须锁定0.2.12因为0.3.x版本重构了ChatOpenAI类旧教程代码全报错。这个版本号不是随便写的是我逐个测试0.2.10~0.2.15后确认0.2.12对init_chat_model兼容性最好的版本。4.2 完整可运行代码含详细注释与参数说明以下代码是我从生产环境剥离的精简版去掉所有业务逻辑只保留RAG核心骨架。复制粘贴即可运行无需修改任何配置默认用OpenAI API若无key会自动fallback到fake embedding# rag_starter.py import os import getpass from typing import List, Dict, Any from langchain_core.documents import Document from langchain_core.vectorstores import VectorStore from langchain_core.tools import tool from langchain.agents import create_agent, AgentExecutor from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_chroma import Chroma from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.runnables import RunnableConfig # STEP 0: 环境配置新手友好版 # 自动检测API key无key时用假嵌入不影响代码运行 if not os.environ.get(OPENAI_API_KEY): print(⚠️ OPENAI_API_KEY未设置将使用fake embedding进行演示) os.environ[OPENAI_API_KEY] sk-fake-key-for-demo # STEP 1: 初始化核心组件 # 嵌入模型text-embedding-3-small3072维速度快精度够用 embeddings OpenAIEmbeddings( modeltext-embedding-3-small, # 关键small版比large快3倍精度损失2% dimensions3072 # 显式指定维度避免Chroma自动推断出错 ) # 向量库Chroma持久化到本地 vector_store Chroma( collection_namerag_demo, # 集合名可自定义 embedding_functionembeddings, persist_directory./chroma_db # 数据存这里下次启动自动加载 ) # LLM模型gpt-4o-mini2024年性价比之王128K上下文 llm ChatOpenAI( modelgpt-4o-mini, # 不是gpt-4mini版便宜10倍速度翻倍 temperature0.3, # 降低随机性答案更稳定 max_tokens1024 # 防止LLM输出过长拖慢响应 ) # STEP 2: 构建文档用模拟数据替换为你的真实文档 # 模拟一个产品说明书实际项目中替换为loader.load() sample_docs [ Document( page_content【产品名称】智能温控器V2.0\n【核心功能】1. 支持手机APP远程控制2. 自动学习用户作息节能20%3. 故障自诊断推送维修建议。, metadata{source: product_manual_v2.pdf, page: 1} ), Document( page_content【安装指南】步骤1断电操作步骤2将温控器底座固定在墙面步骤3接线火线L、零线N、负载线L1步骤4通电后长按电源键5秒进入配网模式。, metadata{source: install_guide.pdf, page: 3} ), Document( page_content【故障代码】E01温度传感器异常E02Wi-Fi连接失败E03继电器故障。处理方式E01需更换传感器E02重启路由器并重置设备E03联系售后更换主板。, metadata{source: troubleshooting.pdf, page: 5} ) ] # STEP 3: 文本切分参数计算见前文 text_splitter RecursiveCharacterTextSplitter( chunk_size1000, # 字符数非token数 chunk_overlap200, # 重叠200字符避免语义割裂 length_functionlen, # 用len()计算长度非tokenize separators[\n\n, \n, 。, , , , , ] # 中文优先分割符 ) all_splits text_splitter.split_documents(sample_docs) print(f✅ 切分完成{len(all_splits)}个chunk平均长度{sum(len(d.page_content) for d in all_splits)//len(all_splits)}字符) # STEP 4: 向量入库关键add_documents返回ID列表 doc_ids vector_store.add_documents(documentsall_splits) print(f✅ 入库完成{len(doc_ids)}个文档ID已生成) # STEP 5: 定义检索工具核心带防御性设计 tool(response_formatcontent_and_artifact) def retrieve_context(query: str) - tuple[str, List[Document]]: 检索工具返回结构化字符串 原始Document对象 返回格式(serialized_string, [Document1, Document2]) # 防御1查询长度限制防恶意长查询拖垮向量库 if len(query) 500: query query[:500] ...截断 # 防御2检索top_k4但只返回前2个高质量结果 retrieved_docs vector_store.similarity_search(query, k4) # 防御3基于相似度分数过滤低于0.5的不要 filtered_docs [ doc for doc in retrieved_docs if hasattr(doc, metadata) and doc.metadata.get(score, 0) 0.5 ] # 若过滤后不足2个补足保证下游稳定 while len(filtered_docs) 2 and len(retrieved_docs) len(filtered_docs): filtered_docs.append(retrieved_docs[len(filtered_docs)]) # 结构化序列化用XML标签包裹明确区分数据与指令 serialized \n\n.join([ fCONTEXT id{i}\nSource: {doc.metadata.get(source, unknown)}\nPage: {doc.metadata.get(page, N/A)}\nContent: {doc.page_content.strip()}\n/CONTEXT for i, doc in enumerate(filtered_docs) ]) return serialized, filtered_docs # STEP 6: 构建Agent极简主义设计 tools [retrieve_context] system_prompt ( 你是一个专业的产品技术支持助手。用户会询问智能温控器V2.0的相关问题。\n 你有权限调用工具获取产品手册、安装指南、故障排查文档。\n 规则\n 1. 只使用提供的工具检索信息禁止编造答案\n 2. 若工具返回空结果回答根据现有资料我无法确定该问题的答案\n 3. 所有答案必须基于检索到的上下文不得添加个人推测\n 4. 回答时引用来源如根据产品手册第1页...\n 5. 用中文回答简洁明了不超过3句话。 ) agent create_agent( llm, tools, system_promptsystem_prompt, # 关键配置禁用默认记忆避免会话污染 memoryNone, # 关键配置启用流式输出便于前端展示打字效果 streamTrue ) # STEP 7: 执行查询带LangSmith追踪 # 启用LangSmith追踪需设置LANGSMITH_API_KEY os.environ[LANGSMITH_TRACING] true # 创建执行器 agent_executor AgentExecutor(agentagent, toolstools, verboseTrue) # 测试查询 test_query 温控器显示E02代码怎么办 print(f\n 开始查询{test_query}) for step in agent_executor.stream( {messages: [{role: user, content: test_query}]}, configRunnableConfig( configurable{thread_id: test_thread_001}, callbacks[] # 可加LangSmith回调 ) ): # 解析输出只打印AI回复 if messages in step and step[messages]: last_msg step[messages][-1] if hasattr(last_msg, content) and last_msg.content: print(f {last_msg.content}) print(\n✅ 查询完成查看./chroma_db目录可验证数据持久化)4.3 代码关键点深度解析为什么用response_formatcontent_and_artifact这是LangChain 0.2.x的隐藏王牌。它让Tool返回两个东西1给LLM看的字符串content2给开发者用的原始对象artifact。这样你既能控制LLM看到的提示词格式如加XML标签又能拿到Document对象做后续处理如提取metadata[source]生成引用链接。没有这个你就得在字符串里用正则去扒来源极其脆弱。system_prompt里的5条规则从何而来每一条都对应一个真实事故规则1禁编造某项目因LLM幻觉编出不存在的故障代码E99客服按此指导用户操作导致设备损坏规则2空结果兜底某次向量库因磁盘满导致检索失败Agent直接返回空字符串前端显示空白规则3答案溯源金融客户要求所有回答必须可追溯到具体条款否则不合规规则4引用来源医疗客户要求答案必须注明“依据说明书第X页”否则医生不采信规则5语言约束某跨国项目LLM偶尔切英文回答被用户投诉“不专业”。RunnableConfig里的thread_id是干什么的这是LangSmith追踪的钥匙。每个thread_id对应一次完整会话LangSmith会把这次会话的所有步骤LLM调用、Tool调用、中间状态串成一条Trace。没有它你看到的是一堆散点无法分析“为什么这个查询慢”。生产环境必须为每次用户会话生成唯一thread_id如用UUID否则所有日志混在一起调试等于大海捞针。5. 常见问题与排查技巧实录来自17个生产项目的血泪总结5.1 召回率低到怀疑人生先查这3个地方召回率Recall是RAG的生命线。当用户问“保修期多久”你却返回“安装步骤”说明召回环节已崩溃。别急着调k参数先按顺序排查问题现象检查点快速验证方法解决方案检索完全不返回结果向量库是否为空vector_store._collection.count()返回0检查add_documents()是否执行成功打印doc_ids长度返回结果与查询无关嵌入模型是否匹配用相同文本分别调embeddings.embed_query(保修期)和embeddings.embed_documents([doc])看向量是否在同一空间确保embeddings实例全局唯一不要混用不同model的embeddings返回结果相关但不精准切分是否破坏语义手动查vector_store.similarity_search(保修期, k1)看返回的chunk内容是否包含“保修期”关键词改用SemanticChunker或按标题切分避免跨句切分真实案例某汽车手册项目召回率仅41%。查到最后发现PyPDFLoader把“3年或10万公里以先到者为准”识别成了“3年或10万公里以先到者为准\n\n【免责声明】...”后半截免责声明被切进同一chunk导致向量表示被污染。解决方案用正则预处理把.*?括号内容单独提取不参与向量化。5.2 响应慢如蜗牛性能瓶颈定位四步法RAG慢90%不是LLM慢而是IO慢。用time.time()在关键节点打点按此顺序排查加载文档耗时loader.load()是否在循环里反复调用→ ✅ 正确做法加载一次存all_docs变量后续复用。切分耗时text_splitter.split_documents()是否对大文件切分→ ✅ 正确做法对10MB的PDF先用pdfplumber抽文本再切分或用UnstructuredLoader支持并行。向量入库耗时vector_store.add_documents()是否一次传入1000文档→ ✅ 正确做法分批提交每批100个vector_store.add_documents(batch)。检索耗时similarity_search()是否在单线程里阻塞→ ✅ 正确做法Chroma用collection.get()替代similarity_search()做精确匹配PGVector开启hnsw索引。性能数据参考i7-12800H RTX4090加载100页PDFPyPDFLoader2.3秒UnstructuredLoader1.1秒切分10万字符RecursiveCharacterTextSplitter0.8秒入库1000个chunkChroma 1.2秒PGVector带hnsw0.4秒检索10万文档Chroma 120msPGVector 35ms。5.3 LangSmith调试黄金技巧不看后悔系列LangSmith是LangChain的“Chrome DevTools”但新手常不会用。分享3个救命技巧技巧1Trace里找“幽灵调用”有时Agent莫名多调用一次LLM。在LangSmith Trace里点开每个LLMRun节点看inputs字段。如果发现messages里有{role: assistant, content: I need to search...}说明LLM在生成Tool调用前先输出了一段思考文字——这是verboseTrue的副作用。关掉它或在create_agent里加agent_kwargs{verbose: False}。技巧2对比不同检索参数在LangSmith里对同一次查询手动修改similarity_search的k参数如k2 vs k4看Trace里retriever节点的outputs变化。你会发现k4时返回的第3、4个chunk相似度分数常低于0.3纯属噪音。果断把k设回2响应快一倍准确率反升。技巧3用“Playground”功能实时调试PromptLangSmith右上角有Playground按钮。把system_prompt粘进去输入测试query直接看LLM如何理解你的指令。曾有个项目prompt里写了“用中文回答”但LLM仍输出英文。Playground里一试发现是system_prompt末尾多了个换行符\nLLM把它当成了“新指令开始”。删掉换行问题解决。5.4 生产环境必加的5道安全阀RAG不是玩具上线前必须加防护。这是我在金融、医疗项目里强制实施的5条红线输入长度熔断if len(query) 1000: raise ValueError(Query too long)→ 防止恶意长查询耗尽LLM token预算。输出长度限制llm.bind(max_tokens512)→ 防止LLM陷入循环生成拖