尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
LangChain实战指南:从LCEL语法到RAG问答应用开发与避坑
简介面向希望快速入门LangChain的Python开发者这份项目代码包提供了一套可运行的入门实践示例。通过模块化组合LLM接口、提示词模板、链式操作、记忆功能与工具调用资源直观演示了如何将外部数据与语言模型连接以应对信息过时、无法执行外部操作、记忆有限等典型痛点。压缩包共3个文件其中inscode文件承载主要示例代码html说明文档梳理了框架核心概念与运行逻辑gitignore用于工程配置整体仅5KB轻量且便于下载后直接查阅。目前已有107人学习适合需要在智能运维、问答系统或对话机器人等场景中快速验证LangChain能力的开发者。掌握这些基础模块后可结合官方学习资源与面试题库进一步搭建更完整的LLM应用具有清晰的进阶参考价值。1. LangChain不是“会调API就够了”它到底帮你省了什么一个很常见的入门场景你已经能用一段几十行的代码把大模型API调通生成一段文案甚至做了个能跑的小工具。可一旦要做正经项目比如让模型基于你手头的一堆文档回答问题还要把回答链路接进已有系统事情立刻就变了文档怎么切分、历史对话怎么塞进上下文、模型偶尔返回JSON格式乱了怎么办、换一个模型供应商要不要重写业务代码这些问题每一个单拎出来都能解决但它们凑在一起你会在胶水代码里消耗掉大量时间而LangChain就是从这个痛点切入的它把“调大模型”这件事从单次请求升级成了一条可编排、可替换、可测试的流水线。对刚接触LangChain的人来说最容易踩的认知坑是把它当成一个新框架重新学一遍。实际上你只需要把它理解为一套围绕大模型应用的标准零件模型封装Model、提示词模板Prompt、记忆Memory、检索Retriever、链与AgentChain / Agent外加一套把这些零件拼起来的组装语法。你原有的大模型API经验不会作废反而能让你更快分辨出哪些是LangChain的便利设施哪些是它自己的设计逻辑。这篇文章不做泛泛的框架介绍我直接顺着一条可运行的主线带你走一遍先搭好最小环境、跑通第一个调用再拆开核心抽象然后动手写一个完整的“本地文档问答”项目代码——这是LangChain最主流、也是搜索最多的一类落地场景。每一步都有能直接抄的代码、参数说明和失败时该看哪里。如果你是希望用LangChain做点真实事情的开发者而不是只看概念图的旁观者这篇就是按你的需求写的。2. 在本地跑通第一个LangChain程序环境搭建与最小链路先把一个最让人困惑的问题说透langchain、langchain-openai、langchain-community、langchain-core这些包到底有什么区别为什么要拆成这么多我的理解是LangChain团队为了应对不同模型供应商和社区贡献者提交的代码把核心抽象、各家模型适配、社区组件拆成了独立包避免每次升级都拖着一大堆依赖。作为入门者你不用全装只装三个就够起步。2.1 先明确版本策略为什么我建议你锁定主版本LangChain的API迭代比大多数开源库都快0.1到0.2是一次较大的接口迁移0.3又在可观测性和部分回调上做了调整。如果你直接pip install langchain拉最新版打开教程时很可能因为API名称对不上而寸步难行。我一般会指定一个大版本区间来安装保证入口一致。# 创建虚拟环境避免污染系统Python python -m venv .langchain-demo source .langchain-demo/bin/activate # 安装核心包注意指定主版本教程以0.2.x的API为准 pip install langchain0.2,0.3 pip install langchain-openai0.1,0.2 pip install langchain-community0.2,0.3 pip install python-dotenv逻辑说明第一行创建隔离环境是所有入门项目里最容易跳过的步骤但LangChain依赖较多直接装进系统环境后续很容易和机器学习库产生冲突。source命令激活环境之后的pip都装在这个环境里。版本限制使用的是与的组合意思是安装0.2.x最新版但不会自动跳到0.3。参数说明python-dotenv是用来读取.env文件的工具很多初学者把API Key直接写死在代码里提交代码时一不留神就泄露了。用环境变量管理密钥是业界默认习惯后面会看到具体用法。2.2 最小链路模型、提示词、输出解析器三者串起来安装完成后用最经典的“模型调用 模板 解析输出”来完成一次有效对话。这里不使用传统的LLMChain写法因为LangChain在0.2里已经明确推荐LCELLangChain Expression Language链式表达语言作为首选组装方式。import os from dotenv import load_dotenv from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser load_dotenv() # 初始化模型读取环境变量中的API Key model ChatOpenAI( modelgpt-4o-mini, temperature0.3, api_keyos.getenv(OPENAI_API_KEY), ) # 声明提示词模板{topic} 是待填充的占位符 prompt ChatPromptTemplate.from_template( 用三句话向产品经理介绍{topic}的核心价值避免专业术语。 ) # 输出解析器把模型返回的完整消息对象转成纯字符串 parser StrOutputParser() # 使用LCEL用管道符组装链路 chain prompt | model | parser # 调用链路传入参数 result chain.invoke({topic: 检索增强生成}) print(result)逻辑说明prompt | model | parser是LCEL的核心表达方式在这里就是数据依次流经模板渲染、模型调用、输出解析三个环节。invoke负责真正执行参数格式是字典键名必须与模板中的占位符对应。字符串输出听起来简单但它保证了你拿到的是一个str而不是带着各种元数据的AIMessage对象这对接下来的Web接口封装非常重要。参数说明temperature0.3是生成随机性的控制参数0.3偏保守适合解释类任务。如果做创意文案可以调到0.8以上。模型名选择gpt-4o-mini是性价比考量入门阶段没必要用更贵的模型。如果你的环境里配置的是其他兼容接口比如通过代理网关访问开源模型只需要替换为对应的模型类链路写法完全不变这正是LangChain封装第一层价值。2.3 跑不起来时先查这四处新环境第一次跑最常见的问题不是代码逻辑而是环境配置。如果上面的脚本报错按顺序排查第一.env文件是否在项目根目录且键名与代码中读取的名字一致第二API Key是否有效很多报错信息写着连接超时实际是Key过期第三是否在虚拟环境内执行激活环境后which python应该指向项目的.langchain-demo/bin/python最后检查模型名是否在当前网关下可用有些兼容服务并不支持全部模型名。解决了上面这四步你的LangChain第一个程序就算正式跑通了。这时候你已经有了一条最小可用的“提示词 → 模型 → 输出”组装链但这还不够因为真实项目的难点从来不在调用模型而在如何组织长流程。下一章我们把这几个核心零件的边界拆开分别是模型如何选、提示词模板和普通字符串到底差在哪里、链在什么时候必须引入。3. 理解LangChain三大核心抽象模型封装、提示词模板与链很多入门教程把这几个概念描述得很玄其实它们完全可以用工程视角理解。你早就会写requests调模型接口也早就会在代码里拼字符串提示词那为什么还需要LangChain的抽象层理由只有一个让业务代码与模型供应商解耦、让流程可以被组合与复用。这一章把三个抽象逐一拆开重点说明它们各自解决什么问题、在什么场景下可以不用。3.1 模型封装为什么不是“封装了个寂寞”一句话解释模型封装它把不同供应商的“对话消息格式”统一成一套Python调用入口。OpenAI接口的messages格式、Anthropic的system/user/assistant结构、国内一些兼容OpenAI协议的网关细节各不相同。如果你直接在自己代码里逐条构造这些消息体换供应商等于重写整个请求逻辑。但通过ChatOpenAI这样的封装类你只需要替换类名和模型名其余代码全部保留。from langchain_openai import ChatOpenAI # 兼容OpenAI协议的服务也可以用同样的类改base_url即可 model ChatOpenAI( base_urlhttp://你的网关地址/v1, api_keyyour-key, modelyour-model-name, temperature0.7, max_tokens2048, timeout60, )参数说明base_url是一个经常被忽略但实用的参数。很多团队不是直连官方API而是通过内部网关做审计和转发这时你不需要另一个SDK只改这个字段就行。timeout60用来处理长文本生成任务默认值在一些网络环境下偏短容易误报超时。需要提醒的是封装并不意味着“永远不用看文档”。不同模型在Tools/Function Call上的返回格式差异较大这类高级功能LangChain的抽象还没有完全抹平差异。所以入门阶段建议把模型封装当作“统一的调用入口”而不是“万能转换器”。3.2 提示词模板从f-string到PromptTemplate的边界在哪很多初学者会问我直接用f-string拼接提示词为什么还要ChatPromptTemplate答案在于消息结构。你平时用f-string拼接的是单个字符串但对话模型接收的是“系统消息 多轮历史 当前提问”的消息列表。ChatPromptTemplate的优势在于它可以把这几类消息组合起来并统一做变量校验。from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_messages([ (system, 你是一个严谨的技术文档助手回答必须基于给定的资料不得编造。), (human, 文档内容如下{context}), (human, 请基于以上内容回答{question}), ]) # 传入参数时缺失键会直接报错而不是等到调用模型时才暴露 formatted prompt.invoke({ context: LangChain是一个用于构建大模型应用的开发框架。, question: LangChain主要解决什么问题 })逻辑说明from_messages接收的是元组列表每个元组第一位是消息角色第二位是内容模板。invoke方法会把模板渲染成ChatPromptValue对象内部已经包含了一条系统消息和两条用户消息。这样做的好处有两点一是模板本身是声明式的方便统一管理二是运行时如果有占位符没有传参会立刻抛出KeyError避免把未渲染的{变量}直接发给模型。边界在哪里如果你的提示词只是在一个函数内部拼一次且不涉及多角色消息用f-string完全没问题不要为了“用框架”而用框架。一旦你要维护多套场景提示词、需要做变量校验或版本管理PromptTemplate的价值才开始体现。3.3 链Chain从“调用模型”到“编排流程”的关键一步链解决的问题是一个真实功能往往不是一次模型调用就能完成的。比如你做文档问答需要先根据问题召回相关片段再把片段拼进提示词最后让模型作答。这个流程如果全部写在业务函数里代码会充满临时变量和if-else。LCEL用管道符把这些步骤连成数据流每一步的输入输出类型是已知的调试时能清晰地看到断点在哪一段。from langchain_core.runnables import RunnablePassthrough def format_docs(docs): return \n\n.join(doc.page_content for doc in docs) chain ( {context: retriever | format_docs, question: RunnablePassthrough()} | prompt | model | parser )逻辑说明这里展示的是RAG链的骨架retriever先根据问题检索文档format_docs把检索结果拼成纯文本RunnablePassthrough()把用户的原始提问透传下去最终形成context和question两个变量填充进提示词模板。LCEL的管道在这里表现出了很强的表达力每一步的输入都是上一步的输出你可以随时在任意两段之间插入日志或缓存节点而不需要改动其他代码。需要澄清的是并不是所有项目都必须用链。单个模型调用、业务逻辑本身已经很简单的工具直接用模型类就行。链是给“多步骤、涉及其它组件如检索器、需要复用和测试”的流程准备的。这个判断标准比“因为大家都在用所以要学”要实用得多。4. 项目代码实战用LCEL实现一个本地RAG问答Demo前面两章解决了“零件怎么用”的问题这一章把这些零件组装成一个完整项目一个可以对本地文档提问的RAG问答Demo。我做这类项目时习惯遵循五条主线文档加载、文本切分、向量化、检索、生成。每一步都不复杂但连起来很容易在衔接处出问题。我会把这个项目命名为“某本地知识库问答Demo”然后逐步实现全程使用LCEL写法。4.1 文档加载PDF、Markdown、网页三种来源怎么统一到一个接口RAG流程的第一步是把原始文档转换成LangChain的统一文档对象一个带page_content和metadata的Document对象。不同格式的加载器类名不同但返回的对象类型一致所以业务代码可以完全屏蔽来源差异。from langchain_community.document_loaders import PyPDFLoader, TextLoader from langchain_community.document_loaders import WebBaseLoader # 加载PDF路径指向一个本地文件 pdf_loader PyPDFLoader(./data/manual.pdf) pdf_docs pdf_loader.load() # 加载Markdown/纯文本需要注意编码问题显式指定utf-8 text_loader TextLoader(./data/notes.md, encodingutf-8) text_docs text_loader.load() # 加载网页自动抓取页面正文 web_loader WebBaseLoader(https://example.com/docs/start) web_docs web_loader.load() # 合并所有文档后续切分和向量化只需要处理这一个list all_docs pdf_docs text_docs web_docs print(f加载完成共 {len(all_docs)} 个文档片段)逻辑说明load()返回的是List[Document]PDF按页拆、网页按结构拆Markdown则整个文件作为一个文档对象。合并列表这一步是把异构数据源变成同构输入的关键后续所有组件不管来源只认这一种对象。参数说明WebBaseLoader对目标网站有隐性要求有些站点开启了反爬有的页面是JavaScript动态渲染抓下来的内容会是空的。如果你的文档主要在本地网页加载器可以先不引入等以后确实需要再叠加避免一上来就踩网络抓取的坑。4.2 文本切分为什么“直接按字数切”会让回答质量大打折扣切分是RAG里最容易糊弄、但最影响效果的环节。如果切得太大超过模型的上下文窗口检索精度也下降如果切得太小一句话的语义被拦腰截断检索时召回的内容不完整。LangChain提供了多种切分器最常见的是递归字符切分器它按段落、句子、词组的优先级逐级切分尽量保持语义完整。from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , , , , ], length_functionlen, ) split_docs text_splitter.split_documents(all_docs) print(f切分完成共 {len(split_docs)} 个文本块)逻辑说明chunk_size500表示每个文本块的目标长度这里用len函数计算的是字符串长度。chunk_overlap50让相邻两个块之间重叠50个字符目的是避免一个完整意思恰好落在两个块的边界上而丢失上下文。separators列表是切分优先级顺序先尝试按双换行切再按单换行最后按标点切都不满足时才硬切。参数说明chunk_size的选择需要结合你的模型上下文窗口和Embedding模型的能力来定。常见做法是先设500到800之间进行测试如果回答总说“资料不足”可能是因为切得太碎导致上下文不全如果回答偏离主题可能是因为单块过长噪声太多。可以明确地说这两个参数在项目初期值得花半天时间调性价比远高于换模型。4.3 向量化与检索Embedding模型和向量库选型的一次性判断文本块切好后要转成向量才能支持相似度检索。这一步有两个选型决策Embedding模型决定文本语义表达的粒度向量库决定检索性能和存储方式。对入门Demo来说我最常用的组合是text-embedding-3-small配合本地内存向量库InMemoryVectorStore。简单、零部署、数据量不大时性能充足。from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import InMemoryVectorStore embeddings OpenAIEmbeddings( modeltext-embedding-3-small, dimensions1536, ) vector_store InMemoryVectorStore.from_documents( documentssplit_docs, embeddingembeddings, ) retriever vector_store.as_retriever( search_typesimilarity, search_kwargs{k: 4} )逻辑说明from_documents内部会遍历所有切分后的文本块逐个调用Embedding接口生成向量然后存入内存向量库。as_retriever把向量库包装成LangChain的检索器接口search_kwargs里的k4表示每次检索返回最相似的4个文本块这个数量决定后续提示词里能塞多少参考内容。参数说明dimensions这个参数要特别留意。OpenAI的嵌入模型支持降维输出如果你在创建向量库时用了1536维后续检索时用了不同维度相似度计算会直接报错。更隐蔽的是如果将来换用了其他Embedding模型比如开源的bge-m3输出维度是1024旧的向量库必须重建这个问题在第五章会专门展开。4.4 组装RAG链用LCEL把检索和生成串成一条线前面几步产出都是静态数据现在把它们变成动态链路用户提出问题 → 检索器召回文档片段 → 拼接上下文 → 模型生成答案。这一章第一节的RunnablePassthrough在这里派上用场。from langchain_core.runnables import RunnablePassthrough from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser def format_docs(docs): return \n\n.join( f【片段{i1}】\n{doc.page_content} for i, doc in enumerate(docs) ) rag_prompt ChatPromptTemplate.from_messages([ (system, 你是一个严格基于资料回答的助手。如果资料中没有答案直接说资料中未找到相关信息禁止自行编造。), (human, 参考资料\n{context}\n\n用户问题{question}), ]) model ChatOpenAI(modelgpt-4o-mini, temperature0.2) parser StrOutputParser() rag_chain ( {context: retriever | format_docs, question: RunnablePassthrough()} | rag_prompt | model | parser ) # 模拟用户提问 answer rag_chain.invoke(LangChain支持哪些文档格式) print(answer)逻辑说明检索器的输入是字符串问题输出是Document列表经过format_docs转成带编号的文本。注意这一段使用了函数组合retriever | format_docs意思是先执行检索再执行格式化这在LCEL里是完全合法的。{context: ..., question: ...}这个字典结构同时接收两个数据流最终拼成提示词所需的所有变量。参数说明为了调试方便chain调用时只传入invoke(问题)因为question是透传参数context由检索器自动生成。如果你希望看到每一段中间结果可以在管道任意位置插入RunnableLambda(lambda x: print(x) or x)这样的调试节点LCEL的管道结构让这种“偷看中间结果”的操作变得非常简单。4.5 给Demo加一层自测逻辑别等部署后才发现链路是断的项目代码写到一个阶段最高频的翻车点是“单测没跑过但Demo是好的”——因为交互式调用时偷偷拿到了缓存或大模型随机生成的内容掩盖了问题。我给这个Demo补一个最简单的自测预设一个已知答案的问题断言模型回答中包含特定关键词。def test_rag_chain(): question 这个Demo支持的文档格式有哪些 expected_keywords [PDF, Markdown] answer rag_chain.invoke(question) missing [kw for kw in expected_keywords if kw not in answer] if missing: raise AssertionError(f回答中缺少关键词: {missing}) print(断言通过回答覆盖了预期关键词) test_rag_chain()逻辑说明test_rag_chain做的事情非常简单但有真实价值它验证的是从问题输入到最终回答的整条链路是通的而不是单测某个组件。expected_keywords需要根据你实际放入的文档内容来设定不要写一个文档里根本没有的词那样相当于让系统故意答错。这一步做完你的“某本地知识库问答Demo”就已经是一个可以拿出去演示的完整项目了。但在投入更多数据之前我建议先把第五章的几个高频坑全部排掉因为这些问题在我自己带过的入门项目里出现的频率极高而且每一个都能让你浪费至少半天时间。5. LangChain避坑指南输出解析、上下文截断与版本依赖问题这章写的是我最想在你动手前就告诉你的内容。所有问题都来自真实的调试经历按“现象 → 原因 → 解决”的方式记录。每一条背后对应一个具体的LangChain机制理解了机制就不会只在出问题时搜报错。5.1 模型返回了“多余前缀”JSON输出解析失败现象使用JsonOutputParser或PydanticOutputParser解析模型输出时报“JSON解码失败”。打印出原始内容发现模型在JSON前面加了一句“好的以下是您需要的JSON”导致解析器无法正常解析。原因LLM在生成结构化内容时经常会带一些自然语言前缀这是模型本身的习惯而非Bug。StrOutputParser不关心内容形态所以没事但JSON解析器对格式要求严格任何前后缀都会导致解析中断。解决最可靠的办法不是靠提示词纠正而是在解析前做一次清洗。常见做法是使用PydanticOutputParser附带的格式指令同时配合一个简单的文本清理函数用正则截取第一个{到最后一个}之间的内容。import json import re def extract_json_string(text: str) - dict: # 定位JSON部分从第一个左花括号到最后一个右花括号 match re.search(r\{.*\}, text, re.DOTALL) if not match: raise ValueError(f未找到JSON内容原始输出{text}) return json.loads(match.group(0))逻辑说明re.DOTALL让点号能匹配换行保证多行JSON也能被正确捕获。这种方法本质上是在“尽力解析”而不是严格依赖模型输出格式。提示如果你用了LangChain的with_structured_output这类新接口它会自动处理格式约束这个问题出现概率会低很多。5.2 上下文塞太多长对话场景下模型开始“忘记”最早的内容现象在对话历史较长的场景中比如超过10轮模型开始重复回答、引用错误信息甚至忽略系统提示词。排查代码没有发现bug偶尔一次报告“上下文长度超限”。原因RAG项目的上下文由三部分组成系统提示词 检索到的文档片段 累积的对话历史。很多人只关注检索片段裁剪忽略了历史对话也在持续消耗token窗口。当整体超过模型的上下文长度限制时线上请求直接报错没超限时模型注意力被碎片化的历史占满指令遵循能力显著下降。解决引入消息历史裁剪策略。最直接的做法是只保留最近N轮对话并把系统提示词保持在简短状态。from langchain_core.messages import HumanMessage, AIMessage MAX_HISTORY_ROUNDS 6 def trim_history(messages): # messages格式为 [HumanMessage/AIMessage] 列表按轮次裁剪 if len(messages) MAX_HISTORY_ROUNDS * 2: return messages return messages[-(MAX_HISTORY_ROUNDS * 2):]逻辑说明MAX_HISTORY_ROUNDS6意味着保留最近6轮问答也就是12条消息每轮一条用户消息加一条AI消息。trim_history返回裁剪后的列表在实际调用时可以放在chain.invoke之前对历史变量做预处理。这个参数建议后续根据模型窗口大小和实际效果调整通常定位在5到10之间。5.3 依赖升级把项目“搞挂了”出现AttributeError现象过了一个假期回来项目启动直接报AttributeError: module langchain_core has no attribute xxx或ImportError: cannot import name xxx from langchain。原因LangChain的API在次版本更新中也会调整尤其是0.2向0.3过渡时部分旧类被移动或改名。许多入门项目完全没有锁版本别人跑通后复制到自己的环境就全挂了。解决把依赖固定到具体版本并生成锁文件而不是只写包名。# 导出当前环境的精确版本 pip freeze requirements.txt # 安装时使用精确版本号杜绝意外升级 pip install langchain0.2.17 langchain-openai0.1.22逻辑说明pip freeze生成的requirements.txt会把所有包的精确版本记录下载。注意不要用pip install -r requirements.txt在新机器上恢复一套不一致的环境而是先检查其中是否有不需要的间接依赖去重后再安装。更推荐用pip-compile这类工具生成约束文件但入门阶段用requirements.txt已经足够。5.4 Embedding维度不一致导致的向量检索报错现象代码昨天还能跑今天把Embedding模型从text-embedding-3-small换成了text-embedding-3-large再查询时抛出向量维度不匹配的错误。原因向量库中已有内容的向量维度不会因你换了模型而自动改变。每个Embedding模型输出的维度不同比如小模型默认1536维大模型是3072维。新旧向量混在同一库里相似度计算必然失败。解决更换Embedding模型时必须重建整个向量库而不是只替换Embedding类。融资级的做法是在代码里显式把向量库版本和Embedding模型名绑定重建时一并更新。# 用模型名作为向量库目录名的一部分避免版本混乱 from pathlib import Path embedding_key text-embedding-3-small-1536 store_dir Path(f./vector_store/{embedding_key}) store_dir.mkdir(parentsTrue, exist_okTrue) # 实际使用时向量库会和这个key一起持久化逻辑说明embedding_key把模型名维度作为标识这样即使你换了Embedding模型创建的也是另一个目录不会和旧数据混在一起。加上这一层隔离之后线上切换模型的风险就降到了“重新灌库”这个可控范围。这四条避坑经验覆盖了我见过的至少八成入门卡点。但解决了“跑通”的问题还不够一个项目要从Demo变成本地服务还要考虑接口封装、异常兜底这些工程化问题。这也是最后一章要处理的内容。6. 从Demo到可用把RAG问答链封装成Web服务并加兜底能不能用和好不好用是两件事。把rag_chain封装成一个带输入校验、超时控制、降级回复的服务接口本地工具就拥有了变成一个可靠后端的基础。6.1 FastAPI封装同步链路也可以安全暴露给外部虽然是Demo我还是建议一开始就按服务的标准来写后续接前端、企业微信机器人或命令行工具都能复用。封装时注意LangChain的invoke是阻塞调用如果在FastAPI的异步接口里直接使用会阻塞事件循环。入门阶段最简单的方案是用普通def定义路由让FastAPI自动放到线程池中执行。from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI(title本地知识库问答服务) class QueryRequest(BaseModel): question: str temperature: float 0.2 app.post(/ask) def ask_question(req: QueryRequest): if not req.question.strip(): raise HTTPException(status_code400, detailquestion不能为空) try: answer rag_chain.invoke(req.question) return {answer: answer} except Exception as e: # 兜底任何异常都返回可读消息而不是堆栈 return {answer: f服务暂时不可用请稍后重试。错误类型{type(e).__name__}}逻辑说明QueryRequest用Pydantic做请求体校验question为空时直接返回400。try/except包住invoke是为了防止上游API波动导致整个服务返回500。请求方拿到的是带error_type的提示而不是让内部错误直接暴露。参数说明给接口暴露temperature是实用设计调用方可以在不重启服务的情况下切换回答风格。6.2 两层兜底检索结果为空与大模型超时重试服务化之后两个高频生产问题会立刻浮出来检索器有时返回空结果文档库里没有相关内容模型接口偶发超时。这两件事的处理策略完全不同。检索为空是业务侧可预期的正常分支我直接做降级模型超时是外部依赖故障做一次重试。from tenacity import retry, stop_after_attempt, wait_random_exponential retry(stopstop_after_attempt(2), waitwait_random_exponential(multiplier1, max5)) def safe_invoke(chain, question: str) - str: return chain.invoke(question) app.post(/ask_with_fallback) def ask_with_fallback(req: QueryRequest): docs retriever.invoke(req.question) if not docs: return {answer: 抱歉资料库中未找到相关内容请换一种问法或补充文档。} try: answer safe_invoke(rag_chain, req.question) return {answer: answer} except Exception as e: return {answer: 模型服务暂时不可用请稍后再试。}逻辑说明先用检索器单独检查是否有可用资料这一步在调用大模型之前就可以完成能省下一次昂贵的模型调用。tenacity库做重试时wait_random_exponential意思是退避时间在1到5秒之间随机增长避免多个请求同时重试造成二次雪崩。两次尝试仍失败就直接走降级文案。到这里这个项目已经不只是一个能跑的脚本而是一个有输入校验、有超时重试、有业务降级的完整问答服务。我把这套封装方式用在每一个需要交付给非技术同事使用的Demo上效率提升明显——对方只需要知道“有一个接口传问题进去就有回答出来”而不需要关心LangChain链路内部发生了什么。回顾整个流程环境搭建、最小链路、核心抽象、RAG实战、避坑、封装服务一条线从零走到了可用。最终我想留下的一个习惯是每写一条LangChain链子先问自己它会在哪一环失败——是检索召回空还是模型返回格式乱再决定要不要加兜底。别等第一个外部调用者把问题抛回来才去补。希望这篇能帮你少踩几个我踩过的坑顺利把LangChain用起来。本文还有配套的精品资源点击获取
RELATED

相关推荐

React架构实践:中后台项目分层设计与性能优化

React架构实践:中后台项目分层设计与性能优化

最近“real”这个词在圈子里被说得很频繁,意思是真实、不玩虚的。手头这个内部项目代号就叫“rea”,全称可以展开成 React Ecosystem Architecture,虽然名字取的是简称顺口,但真正把这套体系从头到尾折腾一遍之后,我发…

📅 2026/10/18 16:13:47
两阶段鲁棒优化在微网电源容量配置中的工程落地

两阶段鲁棒优化在微网电源容量配置中的工程落地

简介:本资源面向电力系统规划、微电网优化及鲁棒优化算法研究领域的研究生与工程师,聚焦含高比例可再生能源的微网电源容量协同配置问题,解决风电、光伏、储能与燃气轮机在不确定性环境下的联合规划难题。资源包共19个文件,涵盖6个…

📅 2026/10/18 16:13:47
开源AI编程实战教程:从提示词模板到2万Star的增长复盘

开源AI编程实战教程:从提示词模板到2万Star的增长复盘

上周打开仓库后台,Star 数稳稳停在 2 万出头,评论区还有人留言说“在 AI 助手的回答里看到这个教程来补课的”。看到这条留言的时候,我才意识到,这份开源 AI 编程教程不再只是我自己的一块自留地,它已经成了不少开发者…

📅 2026/10/18 16:08:47
MORE NEWS

更多资讯

📰

QGC二次开发:从自定义面板到MAVLink消息

简介:这是一份面向初学者的QGC(QGroundControl)二次开发入门项目代码,适合具备少量C与QT基础、希望掌握无人机地面站定制与功能扩展的开发者。压缩包内共3个文件,包含inscode项目配置、html说明页面及gitignore文件&am…

📰

BrowserSkill:Chrome 浏览器原生 CLI 编程工具

1. 项目概述:BrowserSkill 是什么,它解决的不是“浏览器自动化”,而是开发者工作流的物理层重构BrowserSkill(常缩写为 bsk)不是又一个 Selenium 封装库,也不是 Puppeteer 的 CLI 包装器。我第一次在内部工…

📰

智能风控AI决策引擎架构实战:从0到1搭建实时拦截系统

智能风控AI决策引擎的架构实战:拆解一个实时拦截系统的设计与落地做风控架构这行久了,你会发现一个很残酷的现实:市面上讲风控模型算法的文章车载斗量,但真正把“决策引擎”从0到1搭起来、还能稳定扛住高并发和业务迭代的实战经验…

📰

飞控计算机硬实时操作系统核心机制与工程选型全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Agent Skills实战:五个高复用技能封装与自定义教程

最近在项目里被 Agent 折腾到差点怀疑人生。模型明明很聪明,可一旦让它处理多步任务,它就开始抓瞎:会写代码但不会先做计划,会搜资料但看不懂哪段有用,会回答知识库问题但每次都没出处。后来我把一套 Agent Skills 逐步…

📰

AIGC痕迹检测全解析:从检测原理到降重实操策略

1. 毕业季的“隐形大考”:从论文写作到AIGC痕迹检测最近这段时间,我身边不少朋友和学生都在为一件事头疼——毕业论文。比起开题、实验、数据分析这些常规关卡,今年多了一道让他们格外焦虑的坎:AIGC痕迹检测。坦白说,这…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬