LangChain核心组件与实战:从RAG到Agent的大模型编排指南
我第一次用大模型 API 写业务功能时天真地以为只要把接口调通、提示词写得足够妙项目就稳了。结果第一个稍微像样的 Demo 就把我教育了一顿同一段提示词要在三个地方复用用户多轮对话得自己维护上下文模型偶尔规规矩矩返回 JSON偶尔夹杂一堆废话还得先清洗才能入库。最要命的是当我想把「先检索资料、再整理信息、最后生成回答」这三个步骤串成一条流水线时代码规模肉眼可见地失控了。LangChain 解决的就是这件事——它把大模型开发里那些重复而琐碎的工作抽象成标准积木让开发者像搭积木一样编排复杂流程。这篇文章我尽量按「保姆级」的标准来写先讲清楚它到底解决什么问题再拆核心组件接着用完整例子带你把 RAG 问答、Agent 工具调用跑起来最后把我实际踩过的版本坑、调试技巧以及「什么时候别用它」的选型建议一并交底。适合刚接触大模型应用开发、想让代码从「能跑」变成「好维护」的人也适合想系统理解 LangChain 框架设计思路的开发者。1. 先捅破窗户纸LangChain 解决的是「编排」问题不是「调用」问题很多新手第一次看到 LangChain 的文档第一反应是不就是把 API 封装了一下吗我自己写个函数包一层不也一样说实话如果只是「调一次接口」LangChain 确实可有可无。真正让它有价值的地方是把多个模型调用、数据处理、业务逻辑组合成一个可复用、可监控、可替换的流水线。要理解它得先知道你裸调 API 时到底痛在哪。1.1 裸调 API 的三个典型痛感先看一段最常见的裸调代码。假设你要做一个客服助手没有框架时大概是这样的import os, json import requests resp requests.post( https://some-model-api.example.com/v1/chat/completions, headers{Authorization: fBearer {os.getenv(MODEL_API_KEY)}}, json{ model: your-model-name, messages: [ {role: system, content: 你是客服助手回答要简洁专业。}, {role: user, content: f用户说{user_input}} ], }, ) text resp.json()[choices][0][message][content]这段代码本身没问题但它只是「一次调用」。真实业务里你会立刻撞上三堵墙第一堵墙是无状态。模型不记得上次聊了什么多轮对话时你得自己把历史消息拼进 messages 数组还要时刻盯着上下文长度别爆掉。只要逻辑稍微复杂一点这段「组装历史」的代码就会散落在各个函数里。第二堵墙是单次调用无法完成复杂任务。比如用户问「今天上海适合带伞吗」你需要先查天气接口拿到数据再把数据交给模型生成回答。模型自己是不会去查接口的你必须在代码里手动编排「先调天气服务再调模型」的先后顺序。步骤一多业务逻辑和 API 调用就糊成了一团。第三堵墙是模型输出的野格式。模型返回的是自然语言你要的却是 JSON 或结构化字段。于是你得写一堆解析逻辑去掉开头结尾的 Markdown 代码块标记、处理 JSON 里夹杂的废话、防着它哪天给你多打一个逗号导致解析失败。每个项目都在重复发明这些轮子而且每个轮子都长得不太一样。1.2 LangChain 的思路统一接口、自由组合、不绑死模型LangChain 的思路是把这三堵墙都拆掉。它把「模型」「提示词模板」「输出解析器」「文档加载」「向量检索」「工具调用」这些都做成了组件然后给所有组件定了同一套调用协议invoke()同步调用、stream()流式返回、batch()批量执行、对应的ainvoke/astream/abatch异步版本。一旦所有组件都遵守这套协议它们之间就可以自由组合。就像乐高积木无论你手上拿的是车门还是轮子接口尺寸统一就能拼到同一块底座上。这个设计带来的直接好处是业务代码跟具体模型解绑。你今天用的是 A 厂商的模型明天因为成本或效果要换成 B 厂商的模型只要换掉最底层的那个模型对象上面所有编排逻辑一行都不用改。这一点在项目中期、模型快速迭代的时候特别香我自己的项目里就靠这个能力省过好几次大改的功夫。1.3 学 LangChain 之前需要什么基础坦白说门槛比你想的低。你需要的是熟悉 Python 基本语法会装依赖、会看报错理解「调用一个 HTTP API 并处理返回数据」这件事哪怕你没写过也行对大模型有最基本的认知知道提示词、上下文、API Key 是什么愿意在电脑上装一个 Python 环境跑得起来 Jupyter Notebook 或者普通的脚本文件。你不需要提前精通任何框架知识。下面所有内容我都会先讲原理再给代码并明确告诉你哪些地方可以照抄、哪些地方必须按你自己的场景改。如果你连大模型 API 都还没接过建议先随便找一家服务商注册个账号拿到你的 API Key再回来继续看会顺畅得多。2. 核心三件套模型对象、提示词模板、输出解析器LangChain 的组件很多但九成以上的功能都建立在三个最基础的组件之上模型对象、提示词模板、输出解析器。把这组「三件套」吃透后面学 Chain、Agent、RAG 都会轻松很多。2.1 模型对象为什么非要再包一层模型对象是 LangChain 的入口它做的事本质上就是把你刚才那段裸调 API 的代码封装成了标准接口。但这层封装不是白做的它会顺带给你三样裸调很难得到的东西第一是流式返回。调用.stream()就能拿到一个生成器逐块吐出文字。做对话类产品时用户可以像用网页版聊天一样看到逐字输出体验差距很大。第二是回调机制。模型开始调用、结束调用、异常报错都会触发对应事件。你可以挂上日志、监控、统计 token 用量而不需要在代码里到处埋点。第三是统一的可替换性。不同厂商的模型在 LangChain 生态里都有对应的集成包创建出来的对象长得几乎一样接口也完全一致。切换模型时你改的只是创建对象的那一行。为了不绑定任何一家厂商下文代码里的llm我都用注释占位。你只需要记住一个原则用你正在用的那家服务商的 LangChain 扩展包创建出一个模型对象命名为llm后面所有例子都复用它。import os # 伪代码占位换成你所用厂商的 LangChain 集成类即可 # llm ChatModel( # model你的模型标识, # api_keyos.getenv(MODEL_API_KEY), # temperature0.3, # )提示temperature这个参数控制回答的随机性。做客服问答、信息提取这类任务建议调低到 0.20.4减少胡编乱造做创意写作再调高让回答更有发散性。2.2 PromptTemplate把提示词从字符串升级成模板很多新手项目里提示词是直接写在业务代码里的像这样prompt f你是一位资深编辑请改写这段用户提问{user_input}写一次两次没问题但当同一个提示词在多个地方被引用、需要针对不同场景微调时字符串拼接就会变成灾难。你开始在各个文件里搜索这段文字改了一处忘了另一处最后连自己都分不清线上跑的到底是哪个版本。PromptTemplate做的事情很朴素把提示词里会变的部分抽成变量固定不变的部分沉淀成模板。LangChain 最常用的是ChatPromptTemplate它分别管理系统消息和用户消息from langchain_core.prompts import ChatPromptTemplate prompt ChatPromptTemplate.from_template( 你是一位资深售后客服。请根据用户问题给出简洁、友好的回答。\n用户问题{question} ) # 传入变量得到组装好的消息列表 messages prompt.invoke({question: 七天无理由退货需要什么条件}) print(messages)变量名用花括号包住调用时用字典传入对应字段就行。模板还能用.partial()预设部分固定内容比如提前注入当前日期、用户所在城市或者其他已经算好的上下文。这相当于给团队立了一个规矩提示词是资产不是散落在代码里的字符串。2.3 OutputParser让模型的回答「按答题卡交卷」如果说 PromptTemplate 是给模型的「出题」那 OutputParser 就是教模型「按答题卡交卷」。没有它的时候你拿到模型输出还得自己写正则、找 JSON 边界有了它模型输出会直接被解析成你定义的 Python 数据结构。最简单的StrOutputParser只做一件事把模型返回的对象变成纯字符串适合整段文字直接展示的场景。真正体现威力的是结构化解析器。比如我要让模型从用户描述里提取电影信息可以定义一个 Pydantic 模型作为「答题卡」from pydantic import BaseModel, Field from langchain_core.output_parsers import PydanticOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnableLambda class MovieInfo(BaseModel): title: str Field(description电影片名) rating: float Field(description评分保留一位小数) year: int Field(description上映年份) def fake_model(messages): # 真实项目中这里换成你接好的 llm示例里直接返回符合要求的 JSON return {title: 星际救援, rating: 8.5, year: 2023} parser PydanticOutputParser(pydantic_objectMovieInfo) prompt ChatPromptTemplate.from_template( 请从用户描述中提取电影信息。\n{format_instructions}\n用户描述{text} ).partial(format_instructionsparser.get_format_instructions()) chain prompt | RunnableLambda(fake_model) | parser movie chain.invoke({text: 去年看的那部太空题材电影评分八点五挺不错的。}) print(movie.title, movie.rating, movie.year) print(type(movie)) # 已经是一个 MovieInfo 对象不再是字符串get_format_instructions()是整套机制里最妙的部分解析器会把自己需要的格式要求自动写进提示词相当于提前告诉模型「你要怎么回答答案长什么样」。而PydanticOutputParser拿到文本后会做严格的字段校验和类型转换rating是 8.5 而不是字符串 8.5year是整数而不是 2023。程序里直接就能用一点清洗代码都不用写。提示这个例子特意用了一个不依赖真实 API 的fake_model函数来模拟模型目的是让你在零成本的情况下先跑通整套链路。把fake_model换成真正的llm对象它就是一段可以上线用的代码。3. LCEL 管道符真正值钱的是「把组件串起来」这件事上面那个例子里你可能已经注意到那根神秘的竖线|了。这是 LangChain 表达式语言LCEL的核心语法。理解了它你就理解了 LangChain 一半的设计精髓。3.1 Runnable 协议为什么一根竖线能串起所有东西LCEL 里所有可执行组件都叫 Runnable它们都实现了同一套接口invoke、stream、batch。|运算符会把左边的输出作为右边的输入像流水线一样传下去。from langchain_core.runnables import RunnableLambda def add_one(x: int) - int: return x 1 def double(x: int) - int: return x * 2 chain RunnableLambda(add_one) | RunnableLambda(double) print(chain.invoke(10)) # ((10 1) * 2) 22这段代码不依赖任何模型装好langchain-core就能跑。先输入 10第一步加 1 变成 11第二步乘以 2 变成 22。就这么简单。你可能会觉得这不就是函数复合吗有什么稀罕的关键在于两点。第一每个环节的类型都是明确的前一环节输出什么、后一环节接收什么组合起来之后整条链还能继续作为 Runnable 被别人拼接。第二语言表达式的组件支持复杂的控制流并不只是一条直线。3.2 并行、分支、回退真实业务里的三种拼法真实业务不会是简单的一根直线所以我重点讲三种常用拼法它们对应的场景你迟早会遇到。并行分支同一个输入要交给多个处理函数最后汇总结果。比如用户输入一串金额我既想把它转成中文提示又想算出对应手续费from langchain_core.runnables import RunnableLambda, RunnableParallel def as_money(x: int) - str: return f金额{x} 元 def as_fee(x: int) - str: return f预估手续费{round(x * 0.01, 2)} 元 branch RunnableParallel(moneyas_money, feeas_fee) print(branch.invoke(1000)) # {money: 金额1000 元, fee: 预估手续费10.0 元}这个能力在做「一次请求同时查询多个数据源再汇总」时非常有用。比如客服场景里用户问订单你并行去查订单状态、物流信息、退款规则最后让模型综合回答。条件分支根据输入内容走不同的下游。比如判断用户是不是在打招呼走不同的话术from langchain_core.runnables import RunnableBranch, RunnableLambda def is_greeting(x: dict) - bool: return x.get(text, ).startswith(你好) def handle_greeting(x: dict) - str: return 你好呀请问有什么可以帮你 def handle_default(x: dict) - str: return 我理解你的问题是 x.get(text, ) branch RunnableBranch( (is_greeting, RunnableLambda(handle_greeting)), RunnableLambda(handle_default), ) print(branch.invoke({text: 你好我想退货})) print(branch.invoke({text: 怎么退货}))回退兜底主链路失败时自动切到备用方案。我在生产环境里常用它做「大模型挂了就回退到规则回答」保证线上不会出现空白响应def always_fail(x): raise ValueError(故意失败用来演示 fallback) chain RunnableLambda(always_fail).with_fallbacks( [RunnableLambda(lambda x: 系统繁忙请稍后再试。)] ) print(chain.invoke(任意输入)) # 主链路报错后自动返回备用结果3.3 为什么要用 LCEL 而不是老式 Chain 类如果你搜过老教程大概率见过LLMChain、ConversationChain这类写法。那是 0.0.x、0.1.x 时代的产物现在已经标记为废弃新项目不建议再用了。原因很简单老式 Chain 类把逻辑封装在类内部想改一步就得新建子类或者改源码难以组合、调试也不直观。LCEL 则把每一步都摊开摆在明面上提示词、模型、解析器、并行分支、条件路由全部是显式声明的一段表达式。你想看某一步的输入输出直接在这步前后挂个调试函数就行想换成异步调用同一个接口的ainvoke就行。对一个框架来说「能组合」比「功能多」更重要LCEL 就是靠这种极致的可组合性站住了脚。4. Agent 与工具调用让模型学会「找帮手」而不是硬编前面讲的都是「模型单打独斗」你给它提示词它回答你。但真实业务里模型需要去查订单、查天气、算价格、操作某个服务。这时候就需要 Agent 机制——让模型自己决定「我该调用哪个工具」。4.1 模型的边界它不知道你的订单系统里有什么大模型本质上是一个「只会说话、手里没工具」的实习生。你问它「我的订单到哪了」它能编出一套像模像样的物流状态但它实际上并不知道你订单系统里有没有这笔单。你必须在旁边放一排工具并教会它什么时候该用哪个工具。这里有个关键认知工具调用的决策是模型做的但工具的定义是代码做的。模型看到的不是函数的 Python 实现而是函数的名字、参数说明、用途描述。它根据描述决定调用谁真正执行的时候才轮到你的代码。4.2 tool 三步定义一个工具函数名、参数签名、docstring 缺一不可LangChain 里定义工具非常简单用tool装饰器修饰一个普通函数即可from langchain_core.tools import tool tool def get_order_status(order_id: str) - str: 查询订单当前状态。 参数说明 order_id: 订单编号例如 SO-2024-001。 返回说明 返回订单状态、承运商和预计送达时间。 # 真实项目中在这里调用订单系统接口 return 订单已发出承运商某快递公司预计两日内送达。 # 工具元信息会自动提取名字、描述、参数结构 print(get_order_status.name) print(get_order_status.description) print(get_order_status.args)这里三个细节决定工具是不是好用第一函数名就是工具名。模型调用工具靠名字精确匹配名字越贴近自然语言越好。get_order_status显然比query_data强得多。第二参数签名就是参数 schema。给每个参数加上类型注解模型会据此知道该传整数还是字符串。第三docstring 就是工具说明书。模型判断「该不该用这个工具」只看这一段描述不看你函数内部的实现。所以我强烈建议在 docstring 里写清楚这个工具负责什么、参数的含义和格式、返回内容的格式。描述越明确模型越不会乱用。4.3 Agent 循环的本质Thought → Action → Observation很多人把 Agent 当成黑魔法其实它的核心循环非常朴素老派认知科学里有个说法叫 ReAct推理Reasoning加行动Acting。循环大概长这样模型看到任务先「思考」这件事我需要查订单状态吗该用哪个工具模型输出一个「行动指令」我要调用get_order_status参数是SO-2024-001。代码解析出工具名和参数真正执行工具拿到返回值。把返回值作为「观察结果」塞回对话上下文让模型继续推理。直到模型觉得信息够了给出最终答案循环结束。我把这段逻辑写成伪代码你就能看清它的骨架# 伪代码Agent 循环的本质 def agent_loop(prompt, tools, max_iterations5): messages [(system, 你是一个会使用工具的助手。), (user, prompt)] for _ in range(max_iterations): answer llm.invoke(messages) # 模型给了最终答案 → 结束 if 最终答案 in answer.content: return answer.content # 模型想调用工具 → 解析动作 tool_name, tool_input parse_action(answer.content) observation tools[tool_name].invoke(tool_input) # 把动作和观察结果放回上下文继续下一轮 messages.append((assistant, answer.content)) messages.append((user, f工具观察结果{observation})) return 达到最大迭代次数强制结束LangChain 官方后来推荐用 LangGraph 这类状态图方案来编排 Agent你能在官方文档的 Agent 章节里看到更工程化的实现。但不管底层框架怎么变这个「思考—行动—观察」的循环本质是不变的。理解了这个循环你调试 Agent 的时候就不会两眼一抹黑——出问题无非是四个环节之一出了错模型想错了、工具调错了、观察结果没喂回去、循环没收敛。4.4 Agent 上线前必须处理的三件事第一件设max_iterations上限。模型偶尔会陷入「查了又查、反复调用同一个工具」的死循环不设上限它会一直烧 token。设一个 510 次的阈值超了强制结束比挂服务器更靠谱。第二件工具返回信息要「够判断」。比如查询订单状态返回的字符串里最好包含订单是否支持退货、当前处于哪个环节等信息否则模型拿到结果还是没法继续推理只好再调一次或直接放弃。第三件涉及对外部系统造成影响的工具一定要加人工确认。查询类工具可以自动化但「发短信」「改订单状态」「扣款」这类有副作用的调用必须让真实用户确认后再执行。这不是技术问题是基本的责任边界问题。5. 实战做一个本地文档问答机器人RAG 全流程上手Agent 是让模型「学会用工具」RAG检索增强生成则是让模型「学会查资料再回答」两者经常配合使用。这一节我们做一个完整的本地文档问答机器人把一份售后政策文档扔进去然后让模型回答里面的问题。5.1 为什么选 RAG 而不是微调如果你的模型需要回答公司内部的售后政策有两个路径微调或 RAG。微调是让模型背下规则代价是成本高、更新慢而且模型背错了一个条款你也很难找到原因。RAG 是「开卷考试」给模型一本参考资料允许它翻书答案必须从参考资料的片段里得出。这样做的好处非常明显——政策更新时你只要换文档重新索引不用重新训练模型回答还能带出处用户追问时你有据可查。5.2 加载与切分chunk_size 和 chunk_overlap 不是随便填的RAG 的第一步是加载文档。LangChain 里各类文档加载器长得很整齐本地纯文本用TextLoaderPDF 有对应的 PDF 加载器网页有网页加载器。重要的是加载器只负责把文件变成Document对象真正影响效果的是接下来的切分。为什么要切分因为大模型有上下文长度限制你不可能把整本政策文档都塞进提示词。而且语义上针对「运费谁承担」这个问题模型需要的可能只是文档里某一两段而不是全文。切分的核心参数有两个chunk_size每个片段包含多少个字符。太小上下文信息不完整太大检索容易捞进来一堆无关内容还浪费上下文空间。我常用 300800具体取决于文档风格。chunk_overlap相邻片段重叠多少个字符。这是为了处理「一句话被拦腰切断」的问题——某段关键信息刚好在边界处被劈成两半有了重叠两边都能看全。from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # 第 1 步加载本地文档 loader TextLoader(售后政策.md, encodingutf-8) documents loader.load() # 第 2 步切分文档 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) chunks splitter.split_documents(documents) print(f原文被切成 {len(chunks)} 个片段)RecursiveCharacterTextSplitter的「递归」体现在它会尝试按字符层级逐级切分先是段落段落太长再按句子句子太长再按标点。这样切出来的片段大多是语义完整的自然段落而不是生硬截断的一堆字。5.3 嵌入与向量库把「语义」变成坐标切好的片段要变成模型能检索的东西这就要用到嵌入模型。你可以把嵌入理解成「语义坐标生成器」一句「七天无理由退货的运费谁承担」和一句「退货时该谁出运费」会被映射到向量空间里的两个点而且它们之间的距离非常近。这就是语义检索的基础。流程是这样的把每个片段喂给嵌入模型得到一个几百维的向量存入向量库用户提问时同样把问题转成向量在库里找出距离最近的那几个片段作为参考内容返回。# 第 3 步向量化入库示意代码 # 你需要准备两样东西并替换为实际对象 # embeddings —— 一个嵌入模型对象 # vectorstore —— 一个向量库对象内存版、本地文件版或服务版皆可 # vectorstore VectorStore.from_documents(chunks, embeddingembeddings) # retriever vectorstore.as_retriever(search_kwargs{k: 3})向量库的选择看场景。演示原型用内存向量库就够了重启就清空正式项目建议用支持本地持久化的库避免每次启动都重新嵌入一遍耗时不短。5.4 组装 RAG 链检索器和大模型如何协作检索器准备好的下一步就是把检索结果拼进提示词交给模型生成回答。完整链路我用一段连贯代码展示其中embeddings和llm是你需要按第 2、5 节方法替换的两个对象其余代码可以直接照搬from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough template 你是售后问答助手。请只依据下面提供的资料片段回答用户问题。 如果资料里没有答案请明确回答「资料中未找到相关内容」不要编造。 资料片段 {context} 问题{question} prompt ChatPromptTemplate.from_template(template) def format_docs(docs): lines [] for i, doc in enumerate(docs, 1): source doc.metadata.get(source, 未知来源) lines.append(f[片段{i}来自{source}]\n{doc.page_content}) return \n\n.join(lines) rag_chain ( {context: retriever | format_docs, question: RunnablePassthrough()} | prompt | llm | StrOutputParser() ) answer rag_chain.invoke(七天无理由退货的运费谁承担) print(answer)这段代码里值得拆开看的是中间那个字典表达式{context: retriever | format_docs, question: RunnablePassthrough()}。它表示并行执行两条支路——一条把问题拿去检索检索结果再经过format_docs整理成「片段列表」文本另一条把原问题原样透传。两条支路的结果合并成一个字典作为提示词模板的输入。这就是前面讲的 LCEL 并行能力在真实业务里的典型应用。format_docs是很多新手会忽略的小细节但它直接影响回答质量。我把每个片段都编了号、注明了来源模型回答时会更倾向于「第 2 个片段里提到……」而不是含糊其辞。对需要出处的业务场景来说这个习惯值得养成。5.5 跑通之后先别高兴验证检索质量才是关键很多人的 RAG 第一次跑通后发现回答东拉西扯第一反应是「换个大模型」。但根据我的经验RAG 系统的上限是由检索决定的回答不好八成是检索捞错了片段。RAG 里有一句经典判断LLM 只负责把你给它的资料讲成人话它没法回答你没给它的内容。所以排查顺序应该是先单独看检索结果打印出retriever.invoke(你的问题)返回了哪些片段跟问题是否相关如果不相关调大k返回更多候选片段、调大chunk_overlap或者检查切分粒度是不是把关键信息切碎了如果相关但回答还是不好再考虑是不是提示词没写清楚「只依据资料回答」、是否需要降低temperature。我见过太多人跳过检索验证、直接怪模型白白烧了很多调参的时间。检索对了后面的事自然顺了。6. 版本碎片化与调试实操同一段代码网上有五种写法该信谁的LangChain 被吐槽最多的一点就是版本演进太快、网上教程互相打架。你今天搜到一个写法跑起来一看报错说某个类已经废弃了。这一节我把版本演进和排错思路讲透你就不会再被这个问题吓到了。6.1 从大杂烩到分包LangChain 版本演进到底改了什么早期的 LangChain 0.0.x 一个大包走天下什么模型、链、记忆、向量库都在langchain这个包里。这种设计的坏处是你只是想用个提示词模板却要装上整个沉重的框架依赖冲突也容易炸。0.1.0 开始官方做了拆分形成了今天的三层结构langchain-core核心抽象包括 Runnable 协议、提示词模板、输出解析器、工具基类。这一层最稳定是所有人的公共地基。langchain-community社区贡献的集成比如各种文档加载器、向量库适配。体积大、良莠不齐但胜在全面。各厂商自己的集成包模型厂商会发布专属的 LangChain 扩展包专供自家模型使用。到 0.2、0.3 版本老式 Chain 类开始大幅标记废弃LCEL 成为主流编写方式。到 1.0 时代旧 API 被正式清理。所以你会看到「同一段代码网上有三种写法」——不是谁错了只是年代不同。6.2 新老写法对照与 pip 安装清单我把最典型的迁移对照列出来你就知道该认哪种写法了场景老写法0.0.x0.1.x 常见新推荐写法0.2模型调用从langchain大包里 import 模型类从对应厂商的扩展包 import 模型类组合链LLMChain(prompt..., llm...)用 提示词PromptTemplate.format(...)ChatPromptTemplate.from_template(...).invoke(...)向量库全在langchain.vectorstores从langchain_community或厂商包导入对应地安装依赖的时候最省心的做法是「地基 按需外挂」pip install langchain-core langchain-community langchain-text-splitters pip install 你所用厂商的langchain扩展包 # 按实际集成包名安装langchain-core和langchain-text-splitters几乎是所有项目都要用到的稳定底座langchain-community覆盖各种加载器和向量库适配。其余的包用到哪个装哪个不要一口气装全否则 pydantic 版本冲突能让你怀疑人生。提示pydantic 是 LangChain 的底层依赖v1 和 v2 在 API 上不兼容。如果你在装其他包时被「顺手装上了旧版 pydantic」LangChain 经常直接罢工。建议在项目里固定pydantic2并检查环境里有没有重复的 pydantic 版本。6.3 调试三板斧全局开关、回调、看图遇到问题先别急着乱改代码按这三步走九成问题都能定位。第一板斧是打开全局调试。LangChain 提供一行代码开关from langchain_core.globals import set_debug set_debug(True) # 终端会打印每一步组件的输入输出打开之后模型收到了什么提示词、解析器收到了什么文本、每一步返回了什么都会原样打出来。这是定位「模型答非所问是提示词问题还是解析问题」的最快路径。生产环境记得关掉因为输出的信息量很大。第二板斧是自定义回调。Debug 模式是全量打印适合开发环境如果想线上做精细监控就自己挂回调函数监听指定的组件事件# 示意在回调里记录模型输入的 token 数、耗时、异常 # 具体回调类名以你当前版本官方文档为准 # def on_llm_start(serialized, prompts, **kwargs): # print(模型开始调用输入, prompts)第三板斧是让框架自己画图。LCEL 链路本质上是一张有向图LangChain 可以直接把它画成 ASCII 结构图打出来chain.get_graph().print_ascii()你一眼就能看到整条链的先后顺序和并行分支比盯着代码猜结构直观多了。我培养学生排查 LCEL 链路问题的时候第一步永远是让他先打这个图。6.4 常见报错速查表把环境里最常见的几类问题整理成表遇到类似报错先对号入座现象可能原因处理建议cannot import name xxx from langchain版本太新旧集中式导入路径已移除查当前版本官方文档换新导入路径出现LangChainDeprecationWarning用了旧 Chain API老代码能跑就暂用新代码改 LCELValidationError一堆字段错误工具函数签名不规范或输出不匹配 schema给参数加类型仔细写 docstring检查解析器Agent 反复调用同一工具不收敛缺少最大迭代限制或工具返回无法支撑下一步判断设max_iterations优化工具返回内容pydantic 相关报错依赖版本冲突统一 pydantic 版本重建虚拟环境7. 选型边界与我的最后建议什么时候该用什么时候手写更香写到这里我反而想给你泼点冷水。LangChain 不是万能药有些场景用它纯属给自己加戏。7.1 这几类场景建议放心用如果你的项目命中下面任意一类LangChain 的收益是实打实的需要多步编排检索→改写→生成→校验步骤超过三步手写容易乱用链反而清晰需要模型调用外部工具Agent 机制帮你处理了「模型说想调什么工具→代码去执行→结果回填」的完整循环自己实现这套东西要写不少轮子需要做 RAG文档加载、切分、向量化、检索、回填提示词每个环节都是标准化组件改参数比改代码快得多团队里模型不固定今天用这家明天换那家标准接口能省下大把迁移成本。7.2 这几个场景我会劝你停手反过来下面这些情况我会更建议你直接写普通 Python 代码只是单次调用一个提示词、一次返回没有任何编排你用 requests 十行代码就能搞定流程完全固定且几乎不变比如固定的日报生成输入输出结构稳定没必要引入框架抽象抽象反而增加理解成本对依赖体积和启动时间极敏感轻量脚本、边缘设备场景一个框架的依赖树可能比你整个项目还重团队没人熟悉版本演进如果没有人能应对「破版本升级」这件事框架带来的灵活性会被维护成本抵消。判断标准其实很简单如果你用十个函数就能写完整个流程并且未来半年大概率不用改那就别用框架。框架的价值是在复杂度上升时体现的不是为了给简历加一行「熟悉 LangChain」的。7.3 最后几条实操心得一路走到这里最后分享几条我从踩坑里攒出来的心得。第一把版本号写死。项目依赖里明确锁住langchain-core、langchain-community的版本不要用「最新版」。框架迭代快今天能跑的代码下周可能就报警告甚至是报错。锁住版本至少你的调试环境是稳定的。第二提示词不要硬编码在代码里。用小文件、配置表或者数据库存提示词正文代码里只留变量引用。改提示词就不需要动代码、再过一遍发布流程运营同事自己就能改。第三先跑最小链路再加复杂度。任何新功能都从「提示词模型解析器」这条三件套链路起步跑通了再往上挂检索、挂并行、挂 Agent。复杂链路出问题时逐段解耦调试比整体瞎猜快得多。第四RAG 场景先把检索单独验证再谈生成质量。检索不对换再好的模型都白搭。这一条我反复遇到值得再强调一次。说到底LangChain 是一套「大模型应用的编排语言」。掌握它的核心不是背下多少个类名而是理解「组件如何通过统一接口组合成复杂流程」。带着这个视角去读它的官方文档你会发现那些不断变化的 API 表面之下设计思想一直很稳定。希望你读完这篇能少走我当年绕过的弯路把精力花在真正有价值的功能上。