LangChain基础:它能解决什么问题,以及何时可以完全跳过它。
了解LangChain实际提供了什么功能,为何多提供者工具调用会推动团队使用它,需要安装哪些包,以及何时选择轻量封装层更为合适。
来源: [Day 11/100] LangChain 101: Why It Exists, and When You Should (and Should Not) Use It
五十行规模的智能体不再只是五十行
在小型手动编写的智能体中添加新工具成本很低。只需编写一个函数,将其注册到字典中,再用模式文件进行描述,而循环结构本身则无需更改。这类智能体开发工作让人感觉很轻松,也容易让人误以为整个项目的规模会一直保持如此之小。
这时真正的需求就显现出来了。OpenAI 的接口偶尔会返回 503 错误,因此需要通过延迟重试的方式来处理。有时候服务提供商会停机二十分钟,所以需要另一个供应商作为备用。如果用户要等待四次连续调用完成,而每次都要等八秒,他们就会认为应用卡住了,因此需要引入令牌流处理机制。由于某个字段返回的是字符串而非数字,下游服务会拒绝接收响应,所以现在必须对结构化输出进行验证。而一旦有十个人同时使用该服务,阻塞式调用就不再可行,所有操作都必须改为异步处理。
所有这些功能都是建立在原始的 openai 和 requests 之上的,最终该智能体代码量增长到了大约五百行。其中很少有代码真正用于实现智能体的实际功能,大多是 JSON 解析、指数退避策略,以及处理各家厂商自行设计的消息格式转换。每个团队最终都不得不编写这些基础代码,而没人愿意重复编写。
LangChain 提供的正是这类基础代码。下文将介绍该库的真正作用、它旨在解决的问题,以及同样重要的——在哪些情况下使用它反而是错误的。
该库的作用(与局限)
LangChain不会为你运行智能体,也不托管任何内容,更无法提升模型的推理能力。可以将其视为一组共享接口,以及针对那些原本需要你自己编写的LLM应用组件而预先准备好的实现方案。
如果你已经构建过一个最基础的智能体,那么其实你已经用不同的名称实现了这些组件中的大部分。你传递的消息列表对应的是ChatMessageHistory。工具的架构会变成一个Tool对象,或者是一个被@tool装饰的普通函数。AgentExecutor所做的就是通过while循环不断调用模型,直到模型不再请求使用工具。而直接调用client.chat.completions.create的操作则对应ChatModel。从概念上来说,这里并没有什么新的内容。变化在于,这些组件现在有了一个数千个其他项目已经在使用的统一结构,而且各种组件的组合方式也是预先设定好的。
它为解决哪些问题而设计
首先从工具调用开始,因为这最能直观地展现混乱状况。OpenAI会将tool_calls附加在助手回复中,并期望以role: "tool"标记的消息形式收到返回结果。Anthropic则发送tool_use内容块,同时期待在后续用户轮次中收到tool_result块。Gemini则使用function_call和function_response结构。核心思路虽同,但这三种API的格式彼此不兼容。
只要使用单一供应商,就永远不会遇到这类问题。但当你需要第二个供应商时——可能是因为第一个出现故障,或者价格变得更高——你要么编写并维护三个适配器,要么使用已有功能来处理。这正是 LangChain 创立的初衷:提供一个 ChatModel 接口,无论 Token 由哪家公司提供,.invoke() 和 .bind_tools() 的功能都能保持一致。从 ChatOpenAI 更改为 ChatAnthropic 只需要修改导入语句,无需重新编写消息处理逻辑。
第二个问题是架构设计。将提示词输入模型,再将模型输出给解析器,最后由解析器处理结果并传回另一个模型,这意味着在每个接口处都需要添加连接代码,而每当处理流程发生变化时,这些连接代码都不得不重新编写。LangChain让这些接口变得统一。实现这一目标的机制是LCEL,即LangChain表达式语言,它值得专门介绍。
第三个问题是代码复用问题。一个需要执行除调用模型之外其他操作的智能体,必然需要借助他人已经开发好的组件:适用于四十多种文档格式的加载器、十几种向量数据库的客户端、重试策略、流式处理、批量处理以及追踪钩子等功能。使用现成的集成方案几乎总是比在周五临时编写一个PDF解析器后再花费时间调试要好得多。
无需自行编写即可获得的功能
统一的模型接口。 用于本地模型的 ChatOpenAI、ChatAnthropic、ChatGoogleGenerativeAI 以及 ChatOllama 均提供了 .invoke()、.stream() 和 .bind_tools() 方法。这样一来,您的代码就无需关心调用背后的是哪个模型训练出来的。
可组合的构建模块。 利用 LCEL 的管道操作符,只需编写 prompt | model | parser,即可得到一个兼具各组件功能的对象,它能够自动实现流式处理、批量处理以及异步运行,无需额外编码。
将容错能力作为方法调用。 任何实现了 Runnable 接口的对象都具备 .with_retry() 和 .with_fallbacks() 方法,因此您无需手动编写重试循环或提供者切换逻辑。
无处不在的流处理与异步功能。 每个 Runnable 对象都具备 .stream()、.ainvoke()、.astream() 和 .batch() 方法,即便其被封装的对象本身并不原生支持这些功能。
丰富的集成选项。 包括文档加载器、向量存储、搜索工具、代码执行以及数据库工具等。许多工具由供应商自行维护,且它们都遵循相同的 Runnable 接口。
通往 LangGraph 的便捷路径。 当你的智能体需要循环处理、状态保存,或需要线性链无法实现的人工审批步骤时,控制逻辑会转移到 LangGraph 中。由于二者基于相同的底层抽象,因此在 LangChain 中学到的知识可直接应用到 LangGraph 中。
何时无需使用它
大多数入门资料都会跳过这部分内容,而恰恰是这部分内容能避免日后出现最多的问题。
一个提供商,一个调用,长久使用。如果整个流程仅仅是“将此提示发送给GPT-4o并获取返回文本”,使用自己编写的小型封装会比依赖数百个中间包更易于理解和调试。
你需要查看每一条请求。抽象层的便利之处就在于它能隐藏细节,但一旦出现问题,这种隐藏就会带来麻烦。在经过六层Runnable封装层后才找到真正的问题根源,无疑是在浪费时间。那些希望查看代码与HTTP调用之间每一行内容的团队,通常会更倾向于直接使用原始SDK。
对延迟敏感的路径。每一层都会增加一定的开销。对于那些需要500毫秒或更长时间才能完成的模型调用而言,这些开销通常可以忽略不计;但在那些每一毫秒都至关重要的关键路径上,情况就可能有所不同。
非固定版本的升级。LangChain会频繁推出会破坏现有功能的更新。依赖最新版本的项目最终会面临导入路径变更或默认设置改变的问题。应像处理其他依赖项一样锁定版本,在自己决定时再进行升级,而非由pip自动决定。
这并不意味着永远不能使用LangChain,只是意味着应当根据具体项目有意识地做出选择,而非出于习惯。
各包的功能
LangChain由多个包组成,在安装任何组件之前,了解每个包的用途是很有必要的。
langchain-core包含了基础抽象类:Runnable、BaseChatModel、BaseTool,以及提示词模板和输出解析器。它的依赖项较少,几乎所有其他组件都是基于它构建的。
langchain在核心功能之上增加了链、智能体以及检索逻辑,且不会将你绑定到特定的服务提供商。langchain-community汇集了各类第三方集成方案。由于任何人都可以贡献代码,其中有些质量很高,有些则已被废弃。在将其用于生产环境之前,请先查看该集成最后一次更新的时间。langchain-openai、langchain-anthropic和langchain-google-genai这样的合作伙伴封装包是与各提供商自身的SDK一同维护的。只需安装你实际会使用的那些封装包即可。langchain-classic是v1版本中1.0之前的智能体构建器所在之处:包括AgentExecutor、create_tool_calling_agent以及旧版的create_react_agent。现有代码仍然可以正常运行,但新开发的智能体不应基于此版本创建。langgraph 是建立在 langchain-core 之上的独立包,负责处理非线性流程的控制逻辑。自 langchain v1 起,推荐的智能体构建函数 langchain.agents.create_agent 即直接在该包上实现。要开始使用,只需三个包即可:
uv add langchain langchain-openai python-dotenv
将原始客户端替换为ChatOpenAI
此步骤不会带来任何显著变化。其目的是用LangChain的聊天模型封装器替换普通的OpenAI客户端,并确认能得到相同的回复。
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o", temperature=0)
response = llm.invoke("What is the weather in Paris?")
print(response.content)
llm.invoke(...) 返回的是一个 AIMessage 对象而非原始字典,但通过 .content 即可获取文本,这与 .choices[0].message.content 的用法相同。实际上,ChatOpenAI 仍然会调用你手动输入时的那个 OpenAI 接口,模型本身并未发生任何变化。不同之处在于 llm 现在是一个 Runnable 对象,因此 .stream()、.batch() 和 .ainvoke() 这些方法已经存在,而你无需再自行编写它们。
for chunk in llm.stream("Tell me one fact about Paris."):
print(chunk.content, end="", flush=True)
运行它后,令牌会逐块传来。你无需编写流式处理程序,那个简单的循环几乎就是该库存在的全部意义。
关于模型名称的简要说明:示例中使用了 "gpt-4o" 作为占位符。服务提供商提供的模型更新速度远超任何教程的更新速度,因此应将每个 model="..." 字符串视为占位符,在运行代码时替换为你的服务提供商提供的实际模型名称,操作方式保持不变。
在自己的智能体上尝试
- 使用
uv add将langchain和langchain-openai添加到项目中。 - 在智能体的模型处理步骤中,用
ChatOpenAI替换原有的OpenAI()调用,其余循环部分保持不变。确认智能体的行为与之前完全一致。
llm.stream(...),并在每个数据块生成时立即打印出来。这样,你就算没有实现相关功能,也能实现流式处理。经过这次修改后,你的智能体并不会变得更聪明。你所获得的是一个基础框架,通过它,重试、备用方案、流式处理以及提供者切换都只需一次方法调用即可实现,而无需再编写上百行复杂的代码。