先说个结论:LangChain本身不是模型,也不是某种“大模型能力外挂”,它是一个工程框架。它的价值在于把大模型应用开发里那些重复造的轮子——提示词管理、模型调用、外部工具接入、记忆状态、文档检索——做成了一套相对统一的抽象接口。
我个人做AI应用落地一年多,最直观的体会是:没有LangChain,一套带检索和工具调用的聊天机器人也能写,但代码会越写越乱,到处是待兼容的模型API、拼字符串的Prompt、手写的JSON解析;用LangChain串起来之后,虽然偶尔也会被框架的封装绕得头痛,但长期维护和迭代的效率确实高出一大截。
这篇文章不是写“LangChain零基础入门教程”那种官方文档复读,而是按我实际做过的项目来讲:组件选型为什么这么选、RAG和Agent两条主线的完整落地流程、常见坑怎么排,以及LangGraph继承者登场之后LangChain的定位怎么变。适合有一定Python基础、准备把大模型接进真实业务的后端开发者,也适合面试前突击LangChain核心考点的同学。
1. LangChain到底解决什么问题,为什么说它是工程框架
1.1 大模型应用开发的三道坎
这几年来,接入大模型API本身已经不难了,难的是“从单次对话到能解决实际问题的应用”。
第一道坎是模型API五花八门。OpenAI、Anthropic、国内的各家模型,接口规范互不相同,请求参数和返回结构各有差异。你今天用这家,明天要换那家,如果所有代码都直接调SDK,换模型等于重写一部分业务逻辑。
第二道坎是提示词的工程化。简单场景下直接往API里塞字符串没问题,可一旦是业务系统,提示词里要动态嵌套用户提问、历史会话、检索回来的文档片段、工具返回结果。如果你用f-string手工拼,要不了几次就会遇到转义、截断、格式混乱的问题。
第三道坎是应用不止“一问一答”。回答问题前要先查知识库,查完要组装上下文,需要算数时得调计算器API,需要查天气时得调天气服务。这就涉及流程编排、状态管理、工具调用循环。这些逻辑如果全部手写,稳定性很难保证。
1.2 LangChain的解题思路:抽象标准加组件化
LangChain对上面三道坎的回应,本质上是两件事。
第一件事是抽象标准。它对“模型”“提示词”“文档”“记忆”“工具”“检索器”都定义了通用接口。哪怕是不同厂商的模型,包装后接口基本一致;不同格式的文档,加载后都变成标准的Document对象。
第二件事是组件化拼装。LangChain用Chain和Agent的概念把组件串起来。Chain是“按固定顺序走”的流程,Agent是“模型自主决定调用哪个工具、走哪个分支”的流程。
刚开始接触LangChain的人容易被它的类名吓到,其实背后就是一套“输入到输出”的数据流。所有组件定义好输入输出的数据类型,然后你用管道一样的方式把它们接起来。这种设计降低了替换和扩展的成本,但也带来了一个代价——框架本身有学习成本,很多“简单问题”被抽象层包裹后反而显得绕。所以下面我会把核心组件逐个拆开讲,搞清楚每一种抽象解决的是什么具体问题。
2. 最快上手的核心组件:模型、提示词、链、记忆
2.1 模型接口的统一封装
在LangChain里,模型被封装成两类接口:生成文本的LLM类和面向对话的ChatModel类。
一个比较典型的ChatModel使用方式是这样的:
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", temperature=0.2, )对于本地模型,用法也同样简单:
from langchain_ollama import ChatOllama llm = ChatOllama( model="qwen2.5:7b", temperature=0.1, )换模型时,只要接口兼容,业务代码几乎不用动。这就是抽象标准的直接收益。我自己踩过的坑是temperature这个参数。代码生成、数据抽取类任务建议调到0.1或0.2,创造性写作再考虑提高;在Agent流程里温度太高会导致工具调用格式不稳定,模型会“自由发挥”出不存在的参数名。
2.2 提示词模板与其重要性
PromptTemplate把提示词里变化的和不变的部分拆开。例如一个信息抽取模板:
from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个严谨的信息抽取助手。只提取文本中提到的实体,不要推断。"), ("human", "文本内容如下:\n{text}\n\n请提取所有实体并返回JSON。"), ])模板里定义的变量text,在调用时再传入。它在代码里把“规则”和“动态数据”隔离开,也方便以后做多版本提示词的AB测试。实际项目中,我最小化模板数量和变量粒度,一个模板只服务于一个明确任务,变量越少越不容易乱。
2.3 链的串接与输出解析
链是LangChain的核心概念,最简单的链就是把模板、模型、输出解析器按顺序执行:
from langchain_core.output_parsers import StrOutputParser chain = prompt | llm | StrOutputParser() result = chain.invoke({"text": "张三在北京上班。"})这里的|管道符是LangChain的LCEL语法,左边组件输出格式满足右边组件输入要求即可。STROutputParser把模型返回的内容转成纯字符串,后面还会讲到更复杂的JSON输出解析器。
链式写法的好处是每个环节都可以单独替换或插入新处理。比如你想在交给模型前对文本做长度截断,插一个自定义函数就能实现。
2.4 记忆与状态管理
带对话历史的应用会用到记忆。LangChain早期版本提供ConversationBufferMemory这类组件,但那套记忆和LCEL链整合得一般,运行时状态管理经常出问题。后来的LangGraph把状态机制重新设计了一遍,把这个痛点解决得更好。
如果你的项目只是简单会话,并且用的模型本身就支持多轮上下文,我反而建议先自行把历史消息列表传给模型,而不是一上来就套记忆组件。框架的记忆组件适合有复杂状态的场景,普通场景手动维护messages数组更直观,也更省心。
3. RAG落地:Ollama加Chroma搭建本地知识库
3.1 整体流程与架构选择
RAG(检索增强生成)是目前把大模型用进企业内部知识库最务实的一条路。它不微调模型,而是把外部文档切碎、向量化、存进向量数据库,用户提问后先检索相关片段,再把这些片段连同问题一起交给模型组织答案。
我推荐这套本地方案的原因很直接:数据不出内网、模型推理成本可控、部署依赖简单。
架构选型可以这样定:模型用Ollama跑Qwen系列,做检索侧的Embedding也用Ollama里的开源嵌入模型;向量库用Chroma,它是嵌入式库,一个目录就能跑,适合中小规模知识库。
3.2 文档加载与切块细节
原始文档五花八门,PDF、Markdown、Word、HTML,先用文档加载器统一转成LangChain的Document,然后再切块。
from langchain_community.document_loaders import TextLoader, PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter loader = TextLoader("docs/产品手册.txt") docs = loader.load() splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=100, separators=["\n\n", "\n", "。", ";", ",", " ", ""], ) chunks = splitter.split_documents(docs)这里有两个参数特别关键。
第一个是chunk_size。切块太小,召回的内容缺乏上下文,答案经常是“碎片拼凑”;切块过大,向量表示被稀释,检索精确度会下降,输入模型的Token也会变多。800到1000个字符是我在通用文档上的常用起调值。
第二个是chunk_overlap。相邻块之间保留少量重叠,可以避免关键句子正好被切断导致谁都搜不到。一般取块大小的十分之一。
separators列表代表切分优先级,中文文档把中文标点放进来,否则切块会硬生生把句子掰断。按这个顺序递归地去匹配分隔符,能尽量保持语义完整性。
3.3 Embedding的加载与向量入库
Chroma这里要处理好持久化目录。一个容易踩的坑是:不清空旧的数据目录就重复执行入库脚本,每次都会追加重复向量。做试验阶段,建议每次跑脚本前先删掉向量库目录,保证检索测试在一个干净的环境里做。
from langchain_chroma import Chroma from langchain_ollama import OllamaEmbeddings embeddings = OllamaEmbeddings(model="bge-m3") vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db", )选Embedding模型时,中英文混合场景可以优先看bge-m3这类多语言模型。语言不匹配会直接影响检索效果,这个比向量库选型的影响还要大。
3.4 检索增强方法详解
基础检索是用相似度搜索找回TopK个块:
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})但实践里很快会发现一个问题:适合向量检索的块不一定适合直接作为参考答案。块太碎时召回命中率高但内容不完整。于是就有了MultiVectorRetriever这类“摘要映射”思路。
MultiVectorRetriever的核心是“每个原始文档对应多个向量表示”。比较实用的做法是:每个文档生成一段概要存入索引,原始全文存到文档存储区;检索时用概要向量去做召回,命中了概要再把完整文档拿出来送给模型。
from langchain.retrievers.multi_vector import MultiVectorRetriever from langchain.storage import InMemoryByteStore docstore = InMemoryByteStore() retriever = MultiVectorRetriever( vectorstore=vectorstore, byte_store=docstore, id_key="doc_id", ) # 每条文档生成概要后,把概要做向量索引,docstore里放summary和代码块对应的原始文本这个方案解决了“检索粒度与语义覆盖冲突”的问题。代价是离线索引逻辑变复杂,我一般在文档质量参差不齐、单块能独立回答的信息比较少时才启用它。
还有一个检索增强技巧是ParentDocumentRetriever,它把文档切成更小的子块用于检索,命中的子块再映射回上一级的父文档。这个和MultiVectorRetriever思路相反,应用在“小块负责命中,大块负责提供完整答案”的场景。
3.5 RAG流程的问答闭环
搭建完毕之后,具体的问答流程可以这样组装:
from langchain_core.runnables import RunnablePassthrough def format_docs(docs): return "\n\n---\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() )实际跑通这套流程后,还需要做一轮检索质量测评。我给自己定的及格线是:二十个真实业务问题中,前四个召回结果里至少有一个能覆盖问题核心。如果大量问题都召不回有效信息,优先检查Embedding模型和切块策略,先不要怀疑模型参数不够。
4. Agent实战:让LangChain自动把测试用例转成UI自动化脚本
4.1 目标拆解与工具设计
这是我最近比较完整的一个实战项目。需求描述很简单:输入一份测试用例,Agent自动产出可执行的UI自动化测试脚本。
第一刀先拆需求。测试用例常见的字段包括前置条件、操作步骤、期望结果。UI自动化的流程包括启动浏览器、定位元素、点击输入、断言结果。真正要做的,就是让Agent完成从“自然语言步骤”到“可执行代码”的翻译。
拆完之后,我开始想:这个Agent到底需要哪些工具?
- 读取测试用例文件的工具;
- 查询被测系统页面元素定位信息的工具;
- 执行代码片段或查询已有脚本模板的工具;
- 生成代码文件并落盘的工具。
4.2 Agent中间件的正确打开方式
LangChain把Agent可以调用的外部能力封装成Tool,也就是常说的中间件。定义工具的方式有两种:用@tool装饰器直接装饰函数,或者继承BaseTool做更复杂的自定义。
from langchain_core.tools import tool @tool def get_page_element_info(selector_hint: str) -> str: """根据元素提示返回页面元素定位信息,返回格式为JSON。 参数selector_hint可能是按钮文字、输入框placeholder等。 """ # 这里可以查元素仓库、读页面快照,或者反过来调用一次Playwright探查 return element_store.lookup(selector_hint)工具描述特别重要。模型的工具选择能力,很大程度上取决于你为每个工具写的文档字符串是否清晰、参数说明是否准确。工具名要像函数名一样明确,描述里写明“什么情况下调用”“参数格式是什么”“返回格式是什么”。
实际调试阶段最复杂的场景是“模型来回选错工具”。现象是某个简单问题,模型非要绕三个步骤才完成,或者选了个无关工具。我的处理方式之一是加一个“路由工具”的提示词模板,强调工具选择规则。另一个方式是给高频工具“让路”——把常用工具的描述写得更靠前、更详细,让模型更容易命中。
4.3 用Playwright能力生成脚本的落地路径
要使生成的脚本真正可执行,我让Agent基于两个输入工作:测试用例的自然语言步骤,以及被测系统的页面录制快照。
具体思路是先用Playwright自带代码生成能力记录一遍手工操作,把元素定位器、页面路径信息整理出来,作为“页面事实”存进工具查询仓库。模型生成脚本时,查询的是现有定位信息,而不是“猜”定位符,这样生成结果可靠性高得多。
from langgraph.prebuilt import create_react_agent agent = create_react_agent(model=llm, tools=[read_case_file, get_page_element_info, execute_playwright_snippet, save_script_file]) result = agent.invoke({"messages": [{"role": "user", "content": "请根据tests/login_case.json生成测试脚本"}]})这里我用LangGraph而不是传统LangChain AgentExecutor,原因是整个流程有循环:Agent可能先读用例、再查元素、生成代码后尝试执行、执行失败后又回到查元素重试。这种动态循环用Graph表达更自然,下面第5节专门讲LangChain和LangGraph的关系。
生成的代码要真正跑得稳,最后还要追一层“脚本自检”的机制:让Agent把生成的脚本放到沙箱运行,如果失败,把报错信息回传给它自己修改。这一步能显著提升从“看起来像代码”到“实际能跑”的转化率。
4.4 这类的通用架构沉淀
做完这个项目后,我总结出“用例转工具调用”类Agent的通用架构:业务输入解析成结构化参数、领域知识做成可查询工具、生成结果可执行可自检。这种架构不仅适用于测试脚本生成,做数据报表生成、接口契约生成、审批流配置生成,套路基本都是这一套。
5. LangChain与LangGraph怎么选
5.1 两者的定位差异
LangChain和LangGraph是两个不同时代的产物。LangChain核心是链式抽象,适合“线性流程”和简单的工具调用;LangGraph的核心是状态图,节点之间有明确的边与条件分支。
可以这样理解两者的区别:Chain像是流水线,产品按固定工序往前走;Graph像是带交通信号和绕行路线的路网,车(状态)根据路况选择不同路线走,甚至可能绕回上一个路口重新走一遍。
复杂Agent的每一步都需要知道前面发生了什么、下一步有哪些合法选择,这些信息要集中管理。LangGraph把状态对象显式化,每一步执行的都是图的节点,节点内部可以调用LangChain的组件,所以两者不是替代关系,而是互补关系。
5.2 从Chain到Graph的迁移什么时候有必要
我的判断标准很简单:流程里出现“根据执行结果决定下一步走哪个分支”时,就值得考虑LangGraph。
比如客服Agent,问用户问题、判断回答是否包含必要信息、包含则继续、不包含则重新追问。这种循环,用Chain写会很别扭,但用Graph就是一个带条件边的回环。
from langgraph.graph import StateGraph, START, END def ask_node(state): ... def judge_node(state): return "ask" or "next" builder = StateGraph(AgentState) builder.add_node("ask", ask_node) builder.add_node("next", next_node) builder.add_edge(START, "ask") builder.add_conditional_edges("ask", judge_node, {"ask": "ask", "next": "next"}) builder.add_edge("next", END)这个例子里,状态(AgentState)是LangGraph的命脉。设计状态字段时一定想清楚:哪些信息需要跨步骤共享,哪些只是局部临时变量。状态设计不合理,后续排错会非常痛苦,我在最早一次迁移时把整段对话记录都塞进了状态,结果每个节点都在反复处理超大对象,性能很差。
5.3 基于LangChain与LangGraph的面试考点
实践中被问过很多次的问题主要有这么几个:
Agent的ReAct循环是什么?Answer: 模型推理出一个“下一步该做什么”,调用工具拿回观察结果,把这个结果再喂给模型,继续推理,直到得到最终回答。
LangChain支持的SQL查询链、文档问答链、摘要链,核心区别在Prompt模板和前后处理逻辑不同,底层都是LCEL的组合。
LangGraph的StateGraph与LangChain的AgentExecutor相比,优势在于显式状态管理与循环控制,调试时可观测性更好。
LangChain的abroad能力,在不熟悉的时候不要乱讲,直接从状态图、节点、边的角度讲自己的理解就足够。
模型内部如果不支持带循环的流程编排,就必须交给Graph层来处理。这也正是LangGraph存在的价值。
5.4 关于DeepAgents这类新配置的观察
LangChain生态里出现了一些更高层面的封装,比如DeepAgents这类预设Agent配置。它把常用的智能体框架整合成可直接使用的结构,目标是把构建Agent的流程再简化一步。
我的看法是:这类封装适合快速原型验证,但生产环境中,框架预设的提示词和行为策略未必匹配你的业务模型与工具风格。很多情况下还是要回到LangGraph亲手搭建流程。“框架降低的是75%的重复工作,剩下25%的业务定制反而是决定成败的地方。”
如果把LangChain比作标准件的世界,LangGraph就是你可以自己画流程图的白板。两者结合,才是大多数真实系统的最终形态。
6. 实战排错与效率提升笔记
6.1 Chat模型的输出格式化问题
结构化输出是最常翻车的位置。模型直接返回JSON、套一个输出解析器、用函数调用机制强制结构化,是三层不同强度的方案。
对绝大多数场景,我推荐第三层,只要求模型必须以JSON格式返回,并用JSON输出解析器处理结果。
如果模型偶尔输出带解释文字的JSON,可以启用LangChain的with_structured_output,在接口层强制模型按指定Schema输出。这种方式要求你先把Schema定义清楚:
from pydantic import BaseModel, Field class Entity(BaseModel): name: str = Field(description="实体名称") entity_type: str = Field(description="实体类型") structured_llm = llm.with_structured_output(Entity) result = structured_llm.invoke("张三在北京上班")底层依赖模型的工具调用能力,如果你的模型不支持严格结构化输出,调用时会报错或退回普通输出。出现这种情况时,回到“提示词加解析器”方案,并做好解析兜底。
6.2 工具调用的失败排查
工具调用出问题时,先区分是模型没有选择工具,还是工具返回错误被模型“误解”了。
模型不调用工具的常见原因有三个:工具描述写得像绕口令、工具数量过多导致选择困难、当前模型能力本身较弱。处理方式是精简工具并强化描述,必要时升级模型。
工具返回错误被误解,常见现象是模型把异常信息当成答案回复给了用户。解决方法是让工具在异常时返回结构化的错误信息,并在系统提示词里注明错误处理策略。
6.3 Token与成本控制的经验值
长链路Agent的Token消耗经常超出预期。一次真实的测试用例生成任务,模型从读取文件到调用工具,最终产出脚本,消耗的Token可能是最终脚本文本量的五倍。控制成本不能只盯着单次问答。
我的三个做法是:缓存重复工具结果,尤其是页面元素查询这种高频高耗的调用;控制历史消息的保留范围,只保留与当前任务相关的关键状态;在生成脚本阶段优先使用便宜的快速模型,只把最终审核阶段交给更强模型。
6.4 本地模型场景的响应延迟问题
用Ollama跑本地模型时,响应延迟往往是最大的痛点。除了换更好的显卡,一些实际有效的优化包括量化模型、减小输入上下文的冗余度、以及用流式输出提升体验。真正压测时,并发请求一多,本地单卡推理的队列延迟会很明显,这时可以考虑做推理服务化与负载均衡。
7. 最后再分享一点我的体会
LangChain生态变化快,API版本变动频繁,这一直被吐槽,也确实是事实。我的应对策略是尽量使用langchain_core中的稳定接口(Prompt、ChatModel、Retriever等),社区组件部分减少依赖,把环境锁在一个固定的版本组合里,升级前先看变更日志。
写到这里,回头看LangChain这门“实战课”,真正重要的其实不是记住每个类的名字,而是理解它的抽象思想:接口统一、组件可拼装、流程可编排。框架会迭代,类名会变,但这套抽象方法论会一直有用。
也不必焦虑是不是每一个组件都要精通。我和团队的实际经验是:先趟通一条主流程,比如RAG或一个简单Agent,再在真实需求驱动下逐步扩展。等你有了一条完整跑通的主流程,再来回看LangChain的大小概念,会觉得清晰很多。
这套经验分享到这里,希望能给你接下来的LangChain项目一个相对清爽的起点。