1. 为什么大模型应用开发绕不开LangChain
1.1 从“裸调API”到“框架化开发”的必然转变
2023年初,我接了一个需求:把公司内部的客服知识库接上大模型,做一个能回答产品问题的问答机器人。当时我的第一反应是——直接调API不就行了?写个函数,把用户问题拼进prompt,发给模型,拿回结果,完事。
结果真动手才发现,事情远没有这么简单。用户问“你们的产品支持批量导出吗”,我需要先去知识库里检索相关文档片段,再把片段和问题一起塞给模型;模型回答完之后,我还得判断它有没有胡编乱造;如果它说“我需要查一下订单状态”,我还得让它去调一个内部API;多轮对话的时候,历史消息怎么截断、怎么保留上下文,全是坑。
这些活儿如果全部手写,代码会迅速膨胀成一个几千行的意大利面条。更麻烦的是,每换一个模型供应商,接口格式、参数命名、返回结构都不一样,迁移成本极高。LangChain解决的正是这个问题:它把大模型应用开发中反复出现的模式抽象成标准组件,让你用搭积木的方式组织逻辑,而不是每次都从零造轮子。
打个比方,裸调API就像直接用砖头水泥盖房子,什么都要自己砌;LangChain则提供了一套预制件——门窗、梁柱、管道接口都是标准化的,你只需要决定怎么组装。当然,预制件也有它的代价,后面我会详细讲什么时候该用、什么时候不该用。
1.2 LangChain到底包含哪些核心模块
很多人第一次打开LangChain文档会被吓到,模块太多了。但真正日常用到的核心其实就几块,我用一张表把它们和实际用途对应起来:
| 模块 | 作用 | 典型使用场景 |
|---|---|---|
| Models | 统一不同大模型的调用接口 | 切换OpenAI、通义、本地Ollama |
| Prompts | 模板化管理提示词 | 动态填充变量、少样本示例 |
| Chains | 把多个步骤串成流水线 | 检索→回答→格式化 |
| Memory | 管理多轮对话历史 | 聊天机器人记住上下文 |
| Retrieval | 文档加载、切分、向量检索 | 本地知识库问答 |
| Agents | 让模型自主决定调用哪个工具 | 需要计算、查库、调API的任务 |
| Callbacks | 监控和日志 | 调试、计费统计、追踪链路 |
刚入门的时候,我建议你先把Models、Prompts、Chains这三块吃透,它们构成了80%应用的基础骨架。Memory和Retrieval是进阶,Agents是另一个台阶。不要一上来就想把所有模块都用上,那只会让你陷入配置地狱。
1.3 适合谁学,学到什么程度够用
这篇文章面向的是有一定Python基础、想快速上手大模型应用开发的人。你不需要懂深度学习,不需要会训练模型,甚至不需要理解Transformer的内部结构。你需要的是:会写Python函数、知道什么是API、能看懂JSON。
学到什么程度算够用?我的判断标准是:能独立用LangChain搭出一个带知识库检索的问答应用,并且知道每个环节出问题时该去哪里排查。这个目标听起来不高,但覆盖了实际工作中大部分需求。至于更复杂的Agent编排、多智能体协作,那是后面的事,先把基础打牢。
2. 环境搭建与第一个可运行示例
2.1 Python环境与依赖安装的实操细节
LangChain的安装本身不复杂,但环境隔离这一步千万别省。我见过太多人因为全局环境里包版本冲突,折腾半天以为是LangChain的问题,其实是环境脏了。
我习惯用conda建一个独立环境,Python版本选3.10或3.11,这两个版本兼容性最好:
conda create -n langchain-demo python=3.11 conda activate langchain-demo pip install langchain langchain-community langchain-openai这里解释一下为什么装三个包。langchain是核心库,提供基础抽象;langchain-community包含大量第三方集成(各种向量库、文档加载器);langchain-openai是OpenAI的官方集成包。从0.1版本开始,LangChain把各家模型的集成拆成了独立包,这样做的好处是核心库更轻量,你用什么装什么。
如果你打算用本地模型,比如通过Ollama跑一个开源模型,那还需要装langchain-ollama。如果你要用Chroma做向量存储,装langchain-chroma。记住一个原则:核心库只装langchain,其他按需装独立包。
注意:网上很多老教程还在用
from langchain.llms import OpenAI这种写法,这在0.1版本之后已经废弃了。如果你照着老教程写报ImportError,不是你环境的问题,是API变了。新写法是from langchain_openai import ChatOpenAI。
2.2 用ChatOpenAI跑通第一条链路
环境好了,先跑一个最小可运行示例,确认整条链路是通的。我建议第一段代码不要搞太复杂,就是“给模型一句话,让它回一句话”:
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.7, api_key="你的key" ) response = llm.invoke("用一句话解释什么是向量数据库") print(response.content)这段代码虽然简单,但包含了几个关键信息。model参数指定用哪个模型,temperature控制输出的随机性(0最确定,1最发散),invoke是LangChain统一的调用入口。注意返回的是一个AIMessage对象,真正的内容在.content属性里,直接print整个对象会看到一堆元数据。
如果你用的是国内模型或者本地模型,把ChatOpenAI换成对应的类就行,invoke的用法完全一样。这就是LangChain的价值——换模型不改调用逻辑。
2.3 提示词模板:让输入变得可复用
直接传字符串只能应付一次性任务,实际应用里提示词往往是带变量的模板。LangChain的ChatPromptTemplate就是干这个的:
from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个{role},回答要简洁专业。"), ("human", "{question}") ]) chain = prompt | llm result = chain.invoke({ "role": "数据库专家", "question": "索引失效的常见原因有哪些" }) print(result.content)这里出现了LangChain最有特色的语法——管道符|。它叫LCEL(LangChain Expression Language),意思是把prompt的输出喂给llm。你可以把它理解成Unix管道,左边的东西流到右边。这种写法的好处是天然支持流式输出、批量调用、异步调用,而且链路清晰,一眼能看出数据怎么流动。
我实测下来,LCEL的写法比老版本的LLMChain更直观,也更灵活。老写法把prompt和llm绑死在一个Chain对象里,想换个组合方式就得重写;LCEL是组合式的,prompt和llm各自独立,想怎么拼就怎么拼。
3. 核心组件深度拆解与避坑指南
3.1 模型调用的参数选择与成本控制
模型调用看着简单,但参数选不对,要么效果差,要么账单爆炸。我把几个关键参数的实际影响列出来:
| 参数 | 作用 | 我的经验值 |
|---|---|---|
| temperature | 输出随机性 | 问答0.2-0.3,创意写作0.7-0.9 |
| max_tokens | 限制输出长度 | 按需设置,防止模型啰嗦烧钱 |
| timeout | 请求超时 | 30-60秒,本地模型可放宽 |
| max_retries | 失败重试次数 | 2-3次,配合指数退避 |
temperature这个参数我要多说一句。很多人做知识问答时设成0.7甚至1.0,结果模型开始自由发挥,答案里混进不存在的信息。知识问答场景temperature设0.1到0.3就够了,你要的是稳定复现,不是创造力。反过来,如果你让模型写营销文案,temperature太低会显得干巴巴,0.8左右比较合适。
成本控制方面,除了选便宜的模型,还有一个容易被忽略的点:prompt的长度直接决定输入token数。我见过有人把整篇文档塞进prompt,一次调用花掉几毛钱。正确做法是先检索,只把最相关的几个片段塞进去。这个后面讲RAG的时候会展开。
3.2 输出解析器:把模型的“话”变成程序能用的数据
模型返回的是自然语言,但程序需要的是结构化数据。比如你想让模型从一段文本里提取姓名、电话、公司,返回一个JSON,这时候就需要输出解析器。
LangChain提供了几种解析器,最常用的是PydanticOutputParser和JsonOutputParser。我用Pydantic举个例子:
from langchain_core.output_parsers import PydanticOutputParser from pydantic import BaseModel, Field class PersonInfo(BaseModel): name: str = Field(description="姓名") phone: str = Field(description="电话") company: str = Field(description="公司") parser = PydanticOutputParser(pydantic_object=PersonInfo) prompt = ChatPromptTemplate.from_messages([ ("system", "从文本中提取信息。\n{format_instructions}"), ("human", "{text}") ]).partial(format_instructions=parser.get_format_instructions()) chain = prompt | llm | parser result = chain.invoke({"text": "张三,13800138000,就职于某某科技"}) print(result.name, result.phone, result.company)这里的关键是get_format_instructions(),它会把Pydantic模型的字段说明自动转成一段提示词,告诉模型该输出什么格式。这一步非常重要,没有它模型不知道你要JSON还是YAML,字段名叫什么。
踩过的坑:模型有时候会在JSON外面包一层json的代码块标记,导致解析失败。解决办法是在prompt里明确说“只输出JSON,不要任何其他文字”,或者用JsonOutputParser配合容错处理。我一般会在解析器外面套一个try-except,解析失败就重试一次,重试还失败就返回原始文本人工处理。
3.3 链的组合方式:顺序、分支与并行
单个链只能做一件事,实际应用往往是多个链组合。LCEL支持几种组合方式,我按使用频率排个序:
顺序组合最常见,就是|一路串下去。比如“检索文档 → 生成回答 → 翻译成英文”,三个步骤依次执行。
并行组合用RunnableParallel,适合同时做多件事。比如用户提问后,一边检索知识库,一边查用户历史订单,两边结果都拿到后再一起给模型。这样比串行快一倍。
分支组合用RunnableBranch,根据条件走不同路径。比如判断用户问题是“咨询”还是“投诉”,走不同的处理流程。
from langchain_core.runnables import RunnableParallel, RunnableBranch # 并行示例 parallel = RunnableParallel( knowledge=retriever_chain, history=history_chain ) # 分支示例 branch = RunnableBranch( (lambda x: "投诉" in x["question"], complaint_chain), (lambda x: "咨询" in x["question"], consult_chain), default_chain )我的建议是:能用顺序就别用分支,能用分支就别用Agent。每增加一层复杂度,调试难度就翻一倍。很多新手一上来就想搞全自动Agent,结果出了问题根本不知道是哪一步错了。
4. 用RAG搭建本地知识库问答
4.1 RAG的核心流程与为什么需要它
RAG(检索增强生成)是目前大模型落地最实用的模式。它的逻辑很简单:模型本身不知道你的私有数据,那就在回答问题之前,先从你的知识库里找到相关内容,一起塞给模型,让它基于这些内容回答。
为什么不能直接把所有文档塞进prompt?两个原因:一是token成本,二是模型对超长上下文的注意力会衰减,塞太多反而找不到重点。RAG的本质是“先检索,后生成”,用检索来缩小模型需要关注的范围。
完整流程分五步:加载文档 → 切分文本 → 向量化 → 存入向量库 → 检索并生成。每一步都有讲究,我逐个说。
4.2 文档切分:最容易被忽视的关键环节
文档切分看着简单,实际上直接决定检索质量。切太大,检索出来的片段包含太多无关信息,干扰模型;切太小,语义不完整,检索不到。
LangChain提供了多种切分器,最常用的是RecursiveCharacterTextSplitter。它的逻辑是优先按段落切,段落太长再按句子切,句子还长再按字符切,尽量保持语义完整。
from langchain_text_splitters import RecursiveCharacterTextSplitter splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = splitter.split_text(long_text)chunk_size=500表示每个片段大约500个字符,chunk_overlap=50表示相邻片段重叠50个字符。重叠是为了防止一句话正好被切断,导致两边都检索不到。中文场景下,separators里一定要加上中文标点,否则切分器不认识中文句子边界。
我的经验值:技术文档chunk_size设500-800,聊天记录设300-500,法律合同设800-1000。没有万能参数,要拿实际数据试。一个实用的技巧是:切完之后随机抽几个片段读一读,如果读起来语义完整、能独立理解,说明切分合理。
4.3 向量化与检索:选对嵌入模型和检索策略
切分完的文本要转成向量才能做语义检索。嵌入模型的选择直接影响检索效果。OpenAI的text-embedding-3-small性价比很高,中文场景也可以用智谱、通义等国内模型的嵌入接口。
from langchain_openai import OpenAIEmbeddings from langchain_chroma import Chroma embeddings = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_texts( texts=chunks, embedding=embeddings, persist_directory="./chroma_db" ) retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4} )k=4表示检索最相似的4个片段。这个数字不是越大越好,k太大反而引入噪声。我一般从4开始试,看效果调整到3或5。
检索策略除了默认的相似度检索,还有MMR(最大边际相关性)。MMR的好处是检索结果更多样,不会四个片段都在说同一件事。如果你的知识库内容重复度高,建议用MMR:
retriever = vectorstore.as_retriever( search_type="mmr", search_kwargs={"k": 4, "fetch_k": 20} )fetch_k=20表示先取20个候选,再从里面挑4个最有多样性的。
4.4 完整RAG链的组装与调试
把上面几步串起来,就是一个完整的RAG链:
from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser template = """基于以下资料回答问题,如果资料中没有相关信息,就说不知道。 资料: {context} 问题:{question} """ prompt = ChatPromptTemplate.from_template(template) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) answer = rag_chain.invoke("你们的退货政策是什么")这段代码里,RunnablePassthrough的作用是把用户的问题原样传递下去,retriever | format_docs则是检索并把片段拼成字符串。整个链的数据流向是:问题同时进入检索器和passthrough,检索结果格式化后和问题一起填入prompt,再交给模型生成答案。
调试RAG链的时候,我习惯先把retriever单独拿出来测,看看检索出来的片段是不是真的相关。如果检索结果就不对,后面怎么调prompt都没用。检索质量是RAG的天花板,生成质量只是在这个天花板下面发挥。
5. Agent与工具调用:让模型自己决定做什么
5.1 Agent和Chain的本质区别
Chain是你预先定义好步骤,模型按部就班执行。Agent是你给模型一堆工具,让它自己决定用哪个、用几次。Chain是流水线,Agent是决策者。
举个例子:用户问“北京今天天气怎么样,适合穿什么衣服”。Chain的做法是你得先写死“先查天气,再根据天气推荐衣服”。Agent的做法是你给它一个查天气的工具,它自己判断需要先调天气工具,拿到结果后再生成建议。
LangChain的Agent核心是ReAct模式:Reason(推理)→ Act(行动)→ Observe(观察),循环直到任务完成。模型每一步都会输出它的思考过程和要调用的工具,程序执行工具后把结果返回给模型,模型继续下一步。
5.2 定义工具与创建Agent
工具就是一个普通的Python函数,加上装饰器说明它的用途:
from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """查询指定城市的天气""" # 实际调用天气API return f"{city}今天晴,气温25度" @tool def calculate(expression: str) -> str: """计算数学表达式""" return str(eval(expression))工具的docstring非常重要,模型就是靠这段描述来判断什么时候该用这个工具。描述要写清楚“这个工具做什么、什么时候用、参数是什么”。我见过有人工具描述写得含糊,结果模型该调的时候不调,不该调的时候乱调。
创建Agent:
from langchain.agents import create_tool_calling_agent, AgentExecutor agent = create_tool_calling_agent(llm, [get_weather, calculate], prompt) executor = AgentExecutor(agent=agent, tools=[get_weather, calculate], verbose=True) result = executor.invoke({"input": "北京天气怎么样?顺便算一下25乘以4"})verbose=True会打印出模型的每一步思考和工具调用,调试的时候必开。
5.3 Agent的常见问题与限制
Agent很强大,但坑也多。我列几个实际遇到的:
死循环:模型反复调用同一个工具,停不下来。解决办法是设置max_iterations限制最大循环次数,一般设5-10次。
工具选择错误:模型选了不合适的工具。这通常是工具描述不够清晰,或者工具太多导致模型混淆。工具数量控制在5个以内,超过就考虑分组或改用Chain。
参数格式错误:模型传的参数类型不对。解决办法是在工具函数里做类型校验和容错,别指望模型每次都传对。
成本失控:Agent每一步都是一次模型调用,一个复杂任务可能调十几次。生产环境用Agent一定要设预算上限和超时。
我的建议是:能用Chain解决的绝不用Agent。Agent适合那些步骤不确定、需要动态决策的场景,比如“帮我查一下最近的订单,如果有未发货的就催一下”。如果步骤是固定的,Chain更稳定、更便宜、更好调试。
6. 常见问题排查与实战经验
6.1 版本兼容性:LangChain最大的坑
LangChain迭代速度极快,网上大量教程是0.0.x版本的,直接照抄大概率报错。我整理了几个高频的API变化:
| 老写法 | 新写法 | 说明 |
|---|---|---|
| from langchain.llms import OpenAI | from langchain_openai import ChatOpenAI | 模型集成拆包 |
| from langchain.vectorstores import Chroma | from langchain_chroma import Chroma | 向量库拆包 |
| LLMChain(llm=llm, prompt=prompt) | prompt | llm | LCEL替代 |
| initialize_agent(...) | create_tool_calling_agent(...) | Agent重构 |
遇到ImportError先查版本,再看官方文档的迁移指南。不要在网上随便找个博客就照着写,LangChain官方文档更新很及时,以官方为准。
6.2 检索效果差的排查思路
RAG效果不好,按这个顺序排查:
- 先看检索结果:把retriever单独跑一下,看返回的片段是否相关。不相关就是切分或嵌入模型的问题。
- 再看切分粒度:片段太长包含噪声,太短语义不全。调整chunk_size试试。
- 然后看嵌入模型:中文场景用英文嵌入模型效果会打折,换中文优化的模型。
- 最后看prompt:前面都没问题,才考虑调整prompt的措辞。
很多人一上来就改prompt,这是本末倒置。检索不对,prompt写出花来也没用。
6.3 流式输出与异步调用
用户体验上,流式输出几乎是必须的。LangChain的LCEL天然支持流式:
for chunk in rag_chain.stream("你的问题"): print(chunk, end="", flush=True)异步调用用ainvoke和astream,适合Web服务场景,能显著提升并发能力。注意异步链里的每个组件都要支持异步,如果中间某个自定义函数是同步的,整个链会退化成同步。
6.4 我踩过的几个典型坑
坑一:把API Key硬编码在代码里。正确做法是用环境变量或.env文件,配合python-dotenv加载。代码提交到仓库前一定要检查有没有泄露key。
坑二:忽略token限制。每个模型都有最大上下文长度,prompt加历史加检索结果超了就会报错。解决办法是加一个token计数和截断逻辑,或者用支持更长上下文的模型。
坑三:Memory无限增长。多轮对话如果不过滤历史,几轮之后token就爆了。LangChain提供了ConversationBufferWindowMemory(只保留最近N轮)和ConversationSummaryMemory(把历史压缩成摘要),按场景选。
坑四:向量库持久化没做。每次重启都重新嵌入所有文档,又慢又费钱。Chroma、FAISS都支持持久化,建库时指定persist_directory就行。
7. LangChain与LangGraph:什么时候该换工具
7.1 两者的定位差异
LangGraph是LangChain团队推出的另一个库,专门解决复杂流程编排问题。LangChain适合线性的、步骤确定的流程;LangGraph适合有循环、有分支、有状态管理的复杂流程。
举个例子:一个客服机器人,如果只是“检索→回答”,LangChain够了。但如果它需要“判断问题类型→如果是技术问题走技术流程→技术流程里可能需要多轮追问→追问后重新判断”,这种带循环和状态的就该用LangGraph。
LangGraph的核心概念是“图”:节点是处理步骤,边是流转条件,状态在节点之间传递。它比LangChain的Chain更灵活,但也更复杂。
7.2 选型建议
我的判断标准很简单:
- 流程是直线,没有回头路 → LangChain
- 需要根据中间结果决定下一步 → 先试试LangChain的RunnableBranch
- 分支复杂、有循环、需要人工介入 → LangGraph
- 多智能体协作 → LangGraph
不要因为LangGraph新就无脑上。我见过用LangGraph写一个简单的问答流程,代码量比LangChain多三倍,维护成本极高。工具是拿来解决问题的,不是拿来炫技的。
7.3 从LangChain迁移到LangGraph的时机
如果你发现代码里出现了这些信号,就该考虑迁移了:
- 大量的if-else嵌套判断下一步做什么
- 需要让流程“回到上一步重新来”
- 需要人工审核后再继续(human-in-the-loop)
- 多个Agent之间需要共享状态
迁移不是重写,LangGraph可以复用LangChain的模型、工具、检索器,只是把编排层换掉。先把手头的LangChain应用跑通,遇到瓶颈再迁移,这是最务实的路径。
8. 一些实际项目中的体会
我做过的几个LangChain项目里,最深的体会是:框架能帮你省掉重复劳动,但省不掉对业务的理解。检索策略怎么定、prompt怎么写、工具怎么设计,这些都得结合具体场景反复试。LangChain提供的是脚手架,房子盖成什么样,还是取决于你。
另一个体会是:不要追求一步到位。先跑通最小闭环,再逐步加检索、加记忆、加工具。每加一个组件就测一次,确保它是有效的。我见过有人一口气把所有模块都堆上去,结果出了问题根本定位不到是哪一层。
最后分享一个实用习惯:给每个链加callback日志。LangChain的Callback机制可以记录每一步的输入输出和耗时,调试和优化的时候非常有用。生产环境还能用来做计费和监控。这个习惯帮我省了无数次排查时间。
至于LangChain会不会过时,我的看法是:抽象层会变,但“检索增强”“工具调用”“流程编排”这些模式不会变。学会这些模式,就算以后换了别的框架,迁移成本也很低。工具是死的,思路是活的。