1. LangChain入门,先搞懂它到底在解决什么问题
1.1 没有LangChain的时候,我们是怎么写代码的
先聊点实在的。你在接触LangChain之前,肯定写过一段直接调大模型接口的代码,大概长这样:
import openai openai.api_key = "sk-xxx" def chat_with_gpt(prompt): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "你是一个智能助手"}, {"role": "user", "content": prompt} ] ) return response.choices[0].message.content result = chat_with_gpt("介绍一下LangChain") print(result)这段代码跑通完全没问题,网上很多教程教你写的东西也差不多到这个程度。但一到真实项目里,麻烦事就一个接一个冒出来。
你要接一个外部工具API,比如查天气、搜数据库,你得自己处理工具调用的逻辑和返回结果的解析。你要做一个多轮对话,得自己管理历史消息的拼接,messages数组越来越长,Token消耗越来越高,最后还要面对上下文窗口被塞满的窘境。你要做一个知识库问答,那就更头疼了——文档加载、文本切分、向量化、向量存储、相似度检索、把检索结果拼进Prompt里,这一整套流程全部自己写,大概要几百行代码,而且你还得在不同向量库之间做适配。
这些零零碎碎的活儿,本质上是把大模型接入业务系统时所必需的"管道工程"。管道的每一段都有成熟的第三方库可以替代,但如何把它们可靠地串联起来,需要一套完整的编排逻辑。LangChain就是在这样的背景下出现的。
1.2 LangChain和LangGraph、vLLM、Ollama到底是什么关系
在这个话题上,网上搜索量最大的一个问题就是"langchain、vllm跟pytorch框架是一个类型吗?有什么区别?"。这个问题的出现,说明你在LangChain入门阶段就得先把生态位搞清楚。
直接给结论:它们不是一个层级的东西。
PyTorch是大模型训练的底层计算框架,负责张量运算、自动求导、模型权重管理,这是整个AI大厦的地基。vLLM是模型推理加速引擎,它解决的是模型训练完之后,怎么高效地对外提供推理服务的问题,核心卖点是PagedAttention显存管理、高吞吐并发推理。Ollama是模型部署工具,它把模型打包成一条命令就能跑起来的本地服务,主打的是极简部署体验。OpenAI是大模型服务的提供商,你通过API调用它的模型。LangChain则是应用开发编排层,它不关心模型底层推理性能,也不关心模型权重怎么存储,它专注于把"大模型API+业务代码+外部工具+向量库+记忆机制"这些零部件编排成一条完整的应用链路。
如果你用盖房子来类比,PyTorch就相当于钢材水泥的生产设备,vLLM相当于混凝土搅拌车,OpenAI的API相当于装修好的毛坯房,而LangChain是帮你做整体装修、水电布局、功能分区的那套设计方案和施工流程。
经常有新手在同一个项目里,既装了vLLM,又装了Ollama,又装了LangChain社区版的模型包,结果环境冲突一堆,然后来问"是不是我框架选错了"。大概率不是你选错了,是你没搞清楚每层在做什么。
2. 核心设计拆解:五个核心组件撑起整个LangChain生态
2.1 LangChain基础架构的六个核心模块
LangChain的架构从设计之初就是模块化的,整个生态由几个核心模块组合而成,分别是Models(模型层)、Prompts(提示词层)、Chains(链层)、Agents(智能体层)、Memory(记忆层)、Indexes(索引层)。在向量化存储场景下,索引层承担文档加载、切分、向量化和检索的完整链路,也是RAG应用的基础。
这个模块化设计带来的直接好处是:每个模块都是可替换的。你今天用OpenAI的模型,明天想换成本地部署的模型,只需要改一行代码。之前用Chroma作为向量库,后来发现数据量大了想换Milvus,也只需要换一个类。这种"插拔式"设计,对实际项目非常重要,因为你无法预判半年后公司的基础设施会变成什么样。
2.2 Model层的封装:为什么别人都说LangChain过度封装
打开LangChain的源码,你会发现Model层其实做了很多事。以ChatOpenAI为例,当你在代码里初始化这个类的时候,它已经帮你处理了API Key的加载、超时重试机制、模型参数的默认配置、多轮对话消息格式的转换等逻辑。
有一些人批评LangChain过度封装,逻辑太黑盒,代码出了问题不好排查。这个批评确实有道理。我的建议是:在开发调试阶段,尽量多用message打印中间结果,看每一步的入参出参,遇到问题不要被报错信息吓到,报错再长,核心信息往往就一两句。
不同模型在LangChain中的接入方式也比较统一,ChatOpenAI负责OpenAI接口协议兼容的服务,包含使用国内各家大模型厂商提供的OpenAI兼容网关;HuggingFacePipeline负责走本地推理管线;Ollama通过ChatOllama类接入本地模型。这种统一接口设计,让你换模型的时候不用改动业务逻辑代码,只要改初始化部分就可以。
2.3 Prompt模板和输出解析器
说完了Model层,接下来要说的是Prompt管理。实际场景中,你不会像第一条代码那样手动拼字符串,而是用LangChain的PromptTemplate来做这件事。
from langchain_core.prompts import ChatPromptTemplate prompt_template = ChatPromptTemplate.from_messages([ ("system", "你是{company}的智能客服,请用{style}的风格回答用户问题"), ("user", "{user_question}") ]) formatted_prompt = prompt_template.format_messages( company="某电商平台", style="专业且耐心", user_question="我的订单已经付款了,为什么还没发货?" )为什么要用模板而不是直接拼字符串?几个很现实的原因。
第一是可维护性。你业务里可能有一百个Prompt,每个Prompt经过反复调试后长度可能达到上千字,如果全部散落在代码里用f-string拼接,后面优化Prompt的时候,改起来非常痛苦,还容易把代码结构弄乱。把Prompt统一收拢成模板文件,项目经理和运营人员也能直接参与修改,不依赖开发同学。
第二是变量管理的规范性。用户在对话里输入的内容,和业务系统传进来的结构化数据,都通过模板变量注入,后续要加"敏感词过滤""上下文注入"等逻辑时,集中在一个位置就能解决。
配合Prompt模板的另一半是OutputParser(输出解析器)。大模型返回的是自然语言文本,但业务代码往往需要结构化数据。比如让模型抽取一段会议纪里的参会人名单,你希望返回的是JSON数组,而不是一段有前后缀说明的文字。
from langchain_core.output_parsers import CommaSeparatedListOutputParser from langchain_core.prompts import PromptTemplate parser = CommaSeparatedListOutputParser() format_instructions = parser.get_format_instructions() prompt = PromptTemplate( template="列出所有参会人员的姓名。\n{format_instructions}", input_variables=["format_instructions"] )常见的解析器有StrOutputParser,直接返回文本字符串;CommaSeparatedListOutputParser,返回列表;PydanticOutputParser,把输出解析成Pydantic结构体,适合字段较多时的结构化输出;JSONOutputParser,返回JSON对象。PydanticOutputParser我建议重点掌握,因为它在接后端服务时特别好用。
注意:让大模型输出严格合规的JSON时,偶尔会出现字段缺失或JSON格式不完整的问题。使用PydanticOutputParser能让解析器在解析失败时提供清晰的错误信息,配合重试机制,比自己在业务代码里写一大堆正则去解析可靠得多。
3. LCEL表达式语言:LangChain的核心灵魂
3.1 管道符写法和背后的设计哲学
LangChain开发到后期,官方主推的写法是LCEL(LangChain Expression Language,LangChain表达式语言)。它不是一门新语言,只是Python里的一种链式调用语法,核心就一个管道符|。
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser model = ChatOpenAI(model="gpt-4o-mini", temperature=0.7) prompt = ChatPromptTemplate.from_messages([ ("system", "你是精通{field}的专家"), ("user", "帮我解答这个问题:{question}") ]) chain = prompt | model | StrOutputParser() result = chain.invoke({ "field": "Python编程", "question": "Python的GIL锁对多线程有什么影响?" }) print(result)这段代码里,prompt是一个带两个变量的模板对象,model是一个模型实例,StrOutputParser()是一个输出解析器对象,它们通过管道符|连成了一条执行链。
每次执行chain.invoke()时,输入字典会先流进prompt,把模板里的变量都替换成实际内容,生成一条完整的消息列表,然后传给模型,模型返回的响应对象再流进解析器,最终得到纯文本字符串。
这个设计非常像Unix命令行的管道思想——cat file | grep keyword | sort | uniq,每个组件只做一件事,组件之间通过约定好的输入输出格式衔接。LangChain官方特别强调LCEL的三大优点:方便并行执行、方便流式输出、方便异步调用。你可以在链上直接使用chain.stream()拿到逐字返回的流式内容,这对开发聊天机器人是刚需能力。
3.2 RunnablePassthrough和RunnableParallel的妙用
在实际项目里,链上的数据流转不会总是单一方向的,有时你需要把原始输入原封不动地在链上传递下去,有时你需要同时执行两个不同路径的任务再把结果合并。
RunnablePassthrough(直通)的作用是:不做什么变换,直接返回输入内容。有个经典的使用场景是做RAG问答链路时,把用户原始问题一字不改地传递给最后的生成模型,同时把检索到的上下文也传过去,这样模型才知道用户问的原始问题是什么。
RunnableParallel(并行)的作用是:把同一个输入同时分发给多个处理单元,最后合并各个单元的输出。比如对一篇文章同时做摘要、提取关键词、打标签,三个任务互不依赖,就可以并行执行,显著提升处理时长。
from langchain_core.runnables import RunnableParallel, RunnablePassthrough def extract_keywords(text): return ["LangChain", "LCEL", "RAG"] chain = RunnableParallel( summary=summary_chain, keywords=lambda x: extract_keywords(x["content"]), original_content=RunnablePassthrough() )实际项目里数据流一旦复杂起来,并行和直通这两个工具的使用频率非常高,属于必需掌握的技巧。
3.3 LCEL和普通Python函数的取舍之问
有朋友在讨论时提到:"我自己写一个Python函数,按顺序调用模型、拼字符串,效果难道和LCEL不一样吗?为什么非要学这个新语法?" 我的回答是:确实可以不一样,但对大多数项目而言,LCEL带来的收益远大于学习成本。
第一点是统一接口。LCEL链本质上是一个Runnable接口,它支持同步调用invoke,也支持异步调用ainvoke,还支持流式输出stream,以及批处理batch。如果你自己手写函数,这些能力要么不去实现,要么得为每个函数单独写一套,最终代码量会膨胀得很厉害。
第二点是原生支持LangSmith的可视化追踪。LangSmith是LangChain官方的调试和监控平台,它能把一条链上每一步的输入输出都记录下来。这是凭经验手写代码很难获得的能力,一旦你的链路涉及多步骤,排查问题时这类可视化追踪的价值非常大。
第三点是更高的复用性。LCEL链本身可以当作组件内嵌到更大的链中,配合RunnableParallel等操作符,组合方式非常灵活,调试时也更容易定位问题。
4. LangChain RAG实战:从文档加载到知识库问答的实现
4.1 文档加载和文本切分:RAG流程的入口
RAG(Retrieval-Augmented Generation,检索增强生成)是目前LangChain应用最广泛的方向。其核心思路是:不让大模型凭空编造答案,而是先从一个外部知识库中检索出相关内容,再把检索到的内容拼进Prompt,让模型基于参考内容来回答。
完整RAG链路的第一步是文档加载。LangChain提供了层出不穷的加载器,覆盖PDF、Word、Markdown、网页、数据库等多种数据源。
from langchain_community.document_loaders import PyPDFLoader loader = PyPDFLoader("./product_manual.pdf") documents = loader.load() print(f"加载了{len(documents)}页文档")文档加载完后,你基本都会面对一个现实问题:原始文档太长,直接塞进Prompt会被Token上限卡住,就算勉强塞进去,模型也只会把注意力放在开头和结尾,中间的内容很可能被忽略。所以你需要做文档切分。
LangChain官方推荐的切分器是RecursiveCharacterTextSplitter,它按照一个可配置的分隔符优先级列表来执行递归切分,依次对段落、句子、空格进行尝试,直到块的大小符合要求。
from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", ",", ",", " ", ""] ) chunks = text_splitter.split_documents(documents) print(f"切分成了{len(chunks)}个文本块")这里有两个参数需要仔细调:chunk_size控制每个文本块的最大字符数,chunk_overlap控制相邻文本块之间的重叠字符数。为什么要设置重叠?因为一个完整的知识点可能刚好被切分在边界上,如果不重叠,这个知识点就可能会丢失完整性,放上重叠部分能在很大程度上保留上下文连贯性。
from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS embedding_model = HuggingFaceEmbeddings( model_name="BAAI/bge-large-zh-v1.5" ) vectorstore = FAISS.from_documents(chunks, embedding_model)注意:embedding模型的选择直接影响检索质量。常见的中文场景里,BGE系列和多语言模型是比较好用的。我个人实际测试,bge-large-zh-v1.5对中文语义理解能力明显优于部分通用型模型。你先确认自己的显存或内存是否能扛住模型体量,再决定用哪个。
4.2 向量化存储和检索:从语义相似度说起
向量化就是把文本变成一串数字数组,本质上是把语言改成计算机能计算的数学坐标。这个过程的玄妙之处在于,语义接近的两段文本,它们在向量空间里的位置也是接近的,于是"检索"就变成了"找距离最近的一组向量"。
如何衡量距离?最常用的是余弦相似度,计算的是两个向量在方向上的接近程度;还有欧式距离,衡量的是向量在空间里的直线距离。选哪种度量方式,需要和你用的向量数据库保持一致,否则检索结果可能出现偏差。
LangChain把向量检索封装成一个Retriever对象:
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4} )search_kwargs里的k控制每次召回多少条最相似的文档片段。这个参数也需要按场景调优。知识库问答场景,k设4到8基本够用;如果是合同审查这种需要严谨性的场景,建议k设大一点,同时把后续的Prompt调整为"只允许基于提供的文档内容回答,不要使用用户提供的文档中没有的信息"。
4.3 完整RAG链路:从检索到生成的串联
组装完整链路的代码如下:
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough from langchain_openai import ChatOpenAI prompt = ChatPromptTemplate.from_messages([ ("system", "你是企业内部知识库助理。请只基于以下提供的参考内容回答用户问题。" "如果参考内容中没有提到答案,请直接回复:抱歉,在知识库中未找到相关信息。\n\n" "参考内容:\n{context}"), ("user", "问题:{question}") ]) model = ChatOpenAI(model="gpt-4o-mini", temperature=0.3) def format_docs(docs): return "\n\n".join([doc.page_content for doc in docs]) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | model | StrOutputParser() ) answer = rag_chain.invoke("公司年假制度是怎么规定的?") print(answer)这条链的执行流程不再只是单向直通。第一步,把用户的原始问题同时分发给两条路径:一条路径通过retriever从向量库里检索出相关文档,再用format_docs函数对文档做格式化拼装;另一条路径通过RunnablePassthrough原样保持用户问题。接下来把这两部分内容分别填入Prompt模板里的context变量与question变量,喂给模型,最后得到字符串形式的答案。
RunnablePassthrough在这里的作用就是保证用户原始问题不被前面环节干扰,原封不动地进入Prompt模板。
4.4 关于向量库选型的经验
做RAG离不开向量库。LangChain支持Chroma、FAISS、Milvus、Pinecone、Weaviate、Qdrant等众多向量数据库。
如果只是个人项目、学习入门的阶段,FAISS或者Chroma就够了。FAISS是Meta开源的向量检索库,本地单机跑的方案,部署简单,根本不需要启动额外服务。Chroma的特点是轻量、支持持久化,适合快速做原型验证。
如果到了生产环境,数据量大、并发高、需要分布式扩展,Milvus和Qdrant这些专业的向量数据库会是更好的选择。选型时没有捷径可走,你需要结合数据量、QPS、运维成本、团队技术水平做综合权衡。
4.5 和MCP的关系,以及和前后端工具的生态位置
最近关于"langchain prompt rag mcp"的搜索多了起来。MCP是Model Context Protocol,模型上下文协议,它本质上定义了大模型应用和外部工具、数据源之间的统一通信协议。如果说LangChain做的事情是"把能用的零件组装成流水线",那么MCP做的事情是"把零件的接口统一成标准尺寸",两者定位层面不同,未来会形成互补。现在LangChain已经增加了对MCP工具的支持,你可以在Agent中直接调用符合MCP协议的工具。
5. Agent实战:让LangChain模型学会自己用工具
5.1 Agent和普通Chain的核心区别
普通的Chain,比如前面写的RAG链,流程是写死的:用户提问,检索知识库,拿参考内容拼接Promot,最后生成答案。它没有自主判断的能力,如果你问的问题知识库里没有,它只会给你一个等心的兜底回复。
Agent(智能体)则不同。Agent让模型充当决策中枢:面对用户任务时,它先思考需要借助什么工具,然后调用工具获取结果,根据结果再决定下一步做什么,如此循环直到任务完成。
和Agent相关的还有几个高频对比词:"智能体和skill的区别""和LangChain的关系"。Agent是具备"感知-决策-行动"循环的智能单元,skill则是智能体可复用的某项技能模块。你可以把Agent理解成一个员工,把skill理解为这个员工掌握的某项技能,比如"熟练使用搜索引擎""能编写Python脚本"等。Agent负责调度,skill负责执行具体动作。LangChain既是构建Agent的框架,也是这类概念的早期普及者。
5.2 一个完整的LangChain工具调用代码
定义一个工具,最常见的方式是用LangChain Tools体系的装饰器:
from langchain_core.tools import tool import datetime @tool def get_current_time() -> str: """获取当前的日期和时间,格式为YYYY-MM-DD HH:MM:SS。""" return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") @tool def calculate(expression: str) -> str: """计算数学表达式,例如'2*3+5',返回计算结果。""" try: return str(eval(expression, {"__builtins__": {}}, {})) except Exception as e: return f"计算错误: {e}"这里两件事很重要。第一件,给每个工具写的docstring非常重要,因为模型就是靠你的docstring来理解"这个工具是干什么的、什么时候应该用"的。第二件,calculate工具里的eval调用,在实际生产环境一定要慎用,否则可能引入安全问题,这个例子本身确实使用了危险的简单实现,如果有需求建议改用表达式解析安全库。
接下来创建Agent:
from langchain_openai import ChatOpenAI from langchain.agents import create_tool_calling_agent, AgentExecutor tools = [get_current_time, calculate] model = ChatOpenAI(model="gpt-4o-mini", temperature=0) prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个智能助手,可以借助工具帮助用户完成任务。"), ("user", "{input}"), ]) agent = create_tool_calling_agent(model, tools, prompt) agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) response = agent_executor.invoke({"input": "今天是什么日子?3天后的日期是多少?"}) print(response["output"])注意AgentExecutor内部会执行一个循环:模型分析输入,生成工具调用指令,AgentExecutor执行工具,拿到结果后把对话历史和工具返回值重新交给模型,模型继续判断任务是否完成,完成后才输出最终回复。verbose=True参数可以让你在控制台看到这个推理过程的完整日志,新手阶段一定要打开,否则遇到Agent行为不符合预期时很难排查。
5.3 Agent实战中的两大坑
Agent代码比普通Chain要复杂,实战中踩坑的概率也高很多,这里重点说两个高频问题。
第一个问题:模型一直不调用工具,或者反复调用同一个工具。这个问题的根源大概率在Prompt设置。模型选择工具的依据是系统Prompt加上工具的docstring描述,如果你写的工具描述过于含糊,比如写成"计算一下",模型可能不知道该在什么时候触发它。建议把描述写得更明确一些,例如"当用户提出涉及数学计算的问题时,使用本工具"。另外模型温度也要尽量调低,温度过高会让模型的决策不稳定。
第二个问题:Agent陷入死循环,不停地调用工具却停不下来。我遇到过最常见的原因是工具返回的格式不符合模型预期,模型读不懂结果就只能再次调用。排查手段很简单,把verbose打开,看最后一次工具返回的内容到底是什么,是不是返回了模型无法理解的报错信息。还有一个兜底方案,是在AgentExecutor里设置max_iterations参数,比如max_iterations=5,超过次数就强制终止循环,避免消耗过多Token费用。
6. 记忆机制:多轮对话怎么让LangChain"不失忆"
6.1 无状态模型的对话困境
使用过模型API的开发者都会发现,大模型本身是不带记忆的。每轮答调用,它看到的只有当前请求中messages列表里的内容。要实现"还记得我刚才说过什么"这种体验,就需要把历史对话信息一并传给模型。
最原始的做法是维护一个messages数组,每次都把整个对话历史塞进去。但一个现实问题是:随着对话轮次增多,历史记录会越攒越多,Token消耗呈线性上涨。当消息长度接近模型上下文窗口上限时,API直接报错,服务不可用。所以记忆机制不只是"存下来"这么简单,还要考虑怎么在有限的上下文里保留最有效的信息。
6.2 几种LangChain记忆方案的对比
LanguageChain提供了多套记忆管理方案,各有各的取舍:
第一种是ConversationBufferMemory:把它理解成一个数组,回放全部历史消息原文,实现简单、信息完整,但Token消耗最大,适合短期对话场景。
第二种是ConversationBufferWindowMemory:只保留最近N轮对话,超过N轮的直接丢弃。这种方案控制Token消耗很直接,但会丢掉更早的关键信息。
第三种是ConversationSummaryMemory:把已经过去的对话内容做摘要,只把"摘要+最近几轮原文"传给模型。Token消耗控制得很好,还能保留比较关键的历史信息,但代价是摘要本身有额外的大模型调用费用,而且摘要过程有信息损失。
第四种是VectorStoreRetrieverMemory:把历史消息转换成向量后存入向量库,需要时按相关性召回。这种方式能走RAG的检索逻辑,比较适合对历史信息有精确检索需求的场景,例如客服系统里查找用户之前的反馈,但实现复杂度会高一些。
6.3 LangChain记忆实战示例
推荐一种简洁的写法,用RunnableWithMessageHistory配合session_id管理:
from langchain.memory import ChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder model = ChatOpenAI(model="gpt-4o-mini") prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个友好的聊天助手。"), MessagesPlaceholder(variable_name="history"), ("user", "{input}") ]) chain = prompt | model store = {} def get_session_history(session_id: str) -> ChatMessageHistory: if session_id not in store: store[session_id] = ChatMessageHistory() return store[session_id] chain_with_history = RunnableWithMessageHistory( chain, get_session_history, input_messages_key="input", history_messages_key="history" ) result1 = chain_with_history.invoke( {"input": "我叫小李,我喜欢爬山。"}, config={"configurable": {"session_id": "session-001"}} ) print(result1.content) result2 = chain_with_history.invoke( {"input": "我刚才介绍了什么爱好?"}, config={"configurable": {"session_id": "session-001"}} ) print(result2.content)注意一下运行原理。RunnableWithMessageHistory是自动管理历史的玩法:每次invoke的时候,get_session_history返回session_id对应的消息历史对象;链执行前,自动把历史消息填充到MessagesPlaceholder的位置;执行完成后,把最新的对话内容追加到历史中,整体流程清晰可控。
生产环境里,store里的内存字典建议替换成Redis等外部存储,否则服务重启后所有session历史都会丢失。在真实项目里,我遇到过因为记忆存储太简单导致会话状态丢失的问题,后来在分布式环境下不得不尽快从内存存储切换到按session隔离的Redis方案。
注意:记忆方案的Token消耗不能忽略,建议在每次链执行前后对消息列表打印长度,观察整个服务里的Token用量。否则一旦线上用户并发多了,费用账单会教你做人。
7. LangGraph和LangChain的区别
7.1 为什么LangChain要搞一个LangGraph出来
LangGraph是LangChain团队后来推出的一个低层级编排框架,可以简单理解成"给LangChain应用加上了状态机的控制能力"。很多人会问:LangChain本身不是已经能写Agent了吗?为什么还要再整一个LangGraph出来?
答案在于LangChain早期版本的AgentExecutor在对复杂场景的控制上偏弱。它把"模型要不要调用工具、调哪个工具、调用结果怎么处理"都封装在一个黑盒循环里,你只能通过verbose打印日志观察,想干预内部执行流程很费劲。
真实业务场景经常会有这样的需求:第一步先做敏感信息检测,第二步调用检索工具,第三步做答案生成,第四步做事实校验,如果校验不通过,回退到第二步重新检索。这种多分支、状态可流转的场景,用一个有向图来描述是最自然的。LangGraph就是干这个的,它让你把应用流程定义成一个图结构,节点里运行函数或模型,边负责控制状态流转方向。
7.2 LangGraph和LangChain的具体区别
两者的关系可以这样概括:LangChain提供的是开箱即用的高层组件,LangGraph提供的是更底层、更灵活、更可控的编排能力。LangGraph自己构建在LangChain的基础模块之上,两者不是替代关系,而是互补关系。
还是拿做饭做类比。LangChain是那种"预制菜料理包",打开包装按说明加热就能上桌,快但可发挥空间有限。LangGraph则更像是"高级厨房里的整套厨具和调料架"——每种工具都能用,但你得自己规划做菜的流程和火候。它给了你把零件自由组装成复杂应用的能力。
从选型角度看:如果你的业务场景是标准的RAG问答,比较固定的流程编排,用LangChain现成的LCEL链路就够了,写起来快,出活率高。如果你的业务流程分支多、有循环判断、需要支持人在环审核、需要精细控制每一步,那用LangGraph更合适,它来做这类事情体验好太多。
7.3 简单感受一下LangGraph的代码风格
为了让你直观理解,贴一段LangGraph的最小节点图示例:
from langgraph.graph import StateGraph, START, END from typing import TypedDict class State(TypedDict): messages: list def node_a(state: State): return {"messages": state["messages"] + ["处理A"]} def node_b(state: State): return {"messages": state["messages"] + ["处理B"]} graph_builder = StateGraph(State) graph_builder.add_node("a_node", node_a) graph_builder.add_node("b_node", node_b) graph_builder.add_edge(START, "a_node") graph_builder.add_edge("a_node", "b_node") graph_builder.add_edge("b_node", END) compiled_graph = graph_builder.compile() result = compiled_graph.invoke({"messages": ["开始"]}) print(result["messages"])StateGraph接收一个state定义,节点函数接收state并返回state的更新内容,边表示节点之间的流转关系。这套思路在有明确流程形态的应用里比AgentExecutor的隐式循环要清晰得多。如果你有精力,我建议从最简单的RAG链路开始,用LangGraph重写一遍,体验到"每一步都尽在掌控"的感觉后,你就能理解为什么LangChain团队现在把主线精力放在LangGraph上了。
8. 面试高频题和避坑指南
8.1 常见问题与排查技巧总结
按照技术社区里大家问得最多的问题,整理了一份速查表:
| 现象 | 大概率原因 | 排查方向 |
|---|---|---|
| 调用模型时报无效ApiKey | 环境变量没生效或密钥错误 | 检查os.getenv是否正确加载,打印env确认 |
| 提示词没生效,模型不听话 | Prompt里的System消息被覆盖或模型版本差异 | 打印最终拼装后的完整消息列表,人工确认模板变量替换位置是否正常 |
| 检索相关度差 | 切分粒度不合理或embedding模型不匹配 | visual review chunk切分样例,尝试换embedding模型,降低chunk_size并增加重叠 |
| Agent不调用工具 | Temperature过高或docstring描述不清晰 | 降低Temperature到0到0.2,重写工具描述,确认模型本身支持工具调用 |
| 对话重启后历史丢失 | Memory存放在了内存字典里 | 迁移到Redis等外部存储,按session做持久化 |
| 链执行非常慢 | 多个环节串行导致性能瓶颈 | 检查是否有不依赖的步骤能并行,考虑把同步调用替换成异步调用 |
8.2 面试高频题整理
面试相关问题搜索量一直不低,大概是因为LangChain相关岗位确实多了起来。结合真实面试经验和面经,整理了这几类问题。
第一类是概念理解类:"LangChain的核心模块有哪些?""说一个你印象深刻的LCEL特性。"回答这类问题要结合你实际做过的项目来讲,比只背概念好得多。
第二类是方案设计类:"如果要做一个企业知识库问答系统,你打算怎么设计?"这里要能说清楚文档加载、切分、embedding、向量存储、检索、生成这整条链路的选型理由,同时要能说明你遇到过的最棘手的问题和解决方案。
第三类是框架对比类,也就是文中反复提到的"LangGraph和LangChain的区别""LangChain和LlamaIndex的区别""LangChain过时了吗"这类问题。框架对比考察的其实是你能不能跳出框架本身看架构本质。这里可以分享我对"LangChain过时了吗"的判断:框架本身在高速迭代,底层的抽象思想不会过时。只要大模型还是以API方式对外服务,需要编排的环节就会一直存在,这些编排能力相关的项目也会一直有新的形态出现。
8.3 给LangChain入门学习者的一些建议
最后一条经验建议给准备入手LangChain的朋友。框架本身迭代快,API变动大,很多教程拿半年前的项目来教你,照着敲一遍会发现根本跑不通,这不是你的问题。学习这门技术,关键是把核心概念穿透式地搞明白——搞清楚什么是LCEL、什么时候该用Agent、记忆机制的本质是什么、为什么向量检索是这样做的,模型换成哪个,这些基础思路不会变。
动手方向,建议不要直接上手LangGraph,先从最简单的LCEL链写起,再升级到RAG链路,然后再去玩Agent,最后再碰LangGraph这类复杂编排工具,这个顺序是按照难度层层递进的。每一步都动手敲实实在在的代码,亲自跑通跑好,比看十篇文章都管用。
另外一个比较实用的做法是:从第一天起就用LangSmith或者手动print的方式记录每一步的输入输出,这会帮助你建立"可观测性"的习惯。在这个大模型应用开发的领域,黑盒式的调用方式会浪费你大量调试时间,做好可观测性的设计,往往能让排查问题的效率提高很多。
可能最开始你会觉得LangChain的包装层次太多,封装也多,不理解为什么要包这么多层。等你用过一段时间,就会慢慢感觉到它的抽象方式能省下不少重复工作,也让你把精力更集中到业务逻辑本身。