二、从大模型到智能体实践
2.1 模型(Model)接口
在智能体的架构中,模型构成了整个认知与生成能力的基石。无论是进行对话交互、驱动复杂推理,还是完成检索增强生成与工具调用等任务,模型都扮演着执行引擎的角色。为了在复杂多变的技术生态中保持系统的简洁与健壮,LangChain 将这一核心组件抽象为一套统一的模型接口,便于上层组件以一致的方式 调用不同厂商和类型的模型,从而提升系统的可扩展性和可替换性,增强项目化效率。
在本节中,我们将介绍模型接口的基本概念与设计哲学,探讨 LangChain 实现模型接口统一性的内在原因与技术路径。随后,我们将进入实战环节,演示如何配置并调用当前主流的模型服务,为后续构建复杂的智能体打下坚实的基础。
2.1.1 模型接口的统一性
LangChain 的核心基石之一 ,是其模型抽象层的设计。该层旨在屏蔽不同厂商、不同功能模型(如对话模型、嵌入模型、图像生成模型等)在底层接口上的差异,为上层应用提供一致的编程接口。这种统一性主要通过以下两个维度实现。
1. 统一的抽象基类
在 langchain_core.language_models 与 langchain_core.chat_models 模块中,定义了两类基础接口:BaseLLM 与 BaseChatModel。LangChain 对这两类接口进行了进一步封装。对于开发者而言,这意味着:无论底层是何种模型,它们都遵循同一套调用规范,核心方法为 invoke 和 stream。
也就是说,无论你调用的是 ChatGPT、Claude,还是 DeepSeek 等主流模型,都可以使用相同的模式调用:
result = model.invoke("请帮我生成一段关于AI未来的简短总结。")
或使用流式输出:
for chunk in model.stream("请用一句话总结人工智能的意义:"):
print(chunk.content, end="")
LangChain 在内部通过适配器模式(Adapter)实现模型的统一封装,将各厂商差异化的 SDK(Software Development Kit,软件开发工具包)接口,统一封装成符合基类规范的对象,从而对外暴露一致的方法和调用体验。
2. 统一的调用模式
为了实现更高层次的组件互操作性,LangChain 引入了 Runnable Interface 为统一入口点,即所有可执行组件(如模型、Prompt、Agent)都实现了 Runnable 协议,例如:
(1).invoke (input):单次调用。
(2).stream (input):流式输出。
示例代码如下:
output = model.invoke("解释LangChain模型接口的设计理念。")
chat_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的AI助理。"),
("user", "请用简短的语言解释:{topic}")
])
pv = chat_prompt.invoke({"topic": "LangChain 的核心理念"})
result = agent.invoke(
{"messages": [HumanMessage(content="请计算3乘以5的结果是多少")]}
)
在此模式下,无论后续调用的是 ChatGPT、DeepSeek 等模型,还是 Prompt 和 Agent,都能保持调用方式一致。这也是 LangChain 在多智能体系统中能灵活组合和替换模型的关键基础。
模型接口的统一性,远不止为了编码便利,更在架构层面确立了一种稳定的设计契约,其价值体现在以下几个方面。
(1)可替换性:无须修改业务逻辑代码,即可快速切换或测试不同的模型。
(2)可扩展性:当需要集成新型模型(如图像生成、语音识别)时,只需实现统一的接口,便可无缝融入现有生态。
(3)可观测性:所有模型调用均可通过统一的中间件链路进行日志记录、性能监控与链路追踪。
(4)可组合性:模型可以像标准零件一样,被轻松嵌入 Agent 和 LangGraph 工作流中,构建出更复杂的认知系统。
因此,在企业级智能体中,LangChain 的模型抽象层往往作为核心的认知引擎接口,为整个应用提供稳定、标准且可扩展的智能基础能力。
2.1.2 使用 ChatGPT
在 2.1.1 节中,我们介绍了模型接口统一性的重要意义,从这节开始,将示范如何使用 LangChain 调用各种模型。首先是最受欢迎的 ChatGPT。
ChatGPT(OpenAI GPT 系列)是当前广泛使用的对话模型之一。LangChain 为其提供了官方封装,调用方式极为简洁,短短数行代码即可完成与模型的交互。以下示例演示了如何使用 ChatGPT 进行最简单的文本对话生成。
注意:代码中的 api_key 需前往 OpenAI 官网申请,并替换为自己的有效 API 密钥,本书后续涉及密钥的示例均遵循相同方式。
在调用模型之前,需要先安装 LangChain 相关依赖包。本书以 requirements.txt 的方式进行演示,你也可选择使用 pip 单独安装各个依赖。


# LangChain 1.0 主包
langchain==1.0.3
# 社区集成
langchain-community==0.3.31
# OpenAI 适配
langchain-openai==1.0.2
#辅助依赖
pydantic>=2.12
typing-extensions>=4.15
命令安装:
shell
pip install -r requirements.txt
如果报错:
ERROR: Could not find a version that satisfies the requirement langchain>=1.1.0 (from versions: 0.0.1, 0.0.2, 0.0.3, 0.0.4, 0.0.5, 0.0.6, 0.0.7, 0.0.8, 0.0.9, 0.0.10, 0.0.11, 0.0.12, 0.0.13, 0.0.14, 0.0.15, 0.0.16, 0.0.17, 0.0.18, 0.0.19, 0.0.20, 0.0.21, 0.0.22, 0.0.23, 0.0.24, 0.0.25, 0.0.26, 0.0.27, 0.0.28, 0.0.29, 0.0.30, 0.0.31, 0.0.32, 0.0.33, 0.0.34, 0.0.35, 0.0.36, 0.0.37, 0.0.38, 0.0.39, 0.0.40, 0.0.41, 0.0.42, 0.0.43, 0.0.44, 0.0.45, 0.0.46, 0.0.47, 0.0.48, 0.0.49, 0.0.50, 0.0.51, 0.0.52, 0.0.53, 0.0.54, 0.0.55, 0.0.56, 0.0.57, 0.0.58, 0.0.59, 0.0.60, 0.0.61, 0.0.63, 0.0.64, 0.0.65, 0.0.66, 0.0.67, 0.0.68, 0.0.69, 0.0.70, 0.0.71, 0.0.72, 0.0.73, 0.0.74, 0.0.75, 0.0.76, 0.0.77, 0.0.78, 0.0.79, 0.0.80, 0.0.81, 0.0.82, 0.0.83, 0.0.84, 0.0.85, 0.0.86, 0.0.87, 0.0.88, 0.0.89, 0.0.90, 0.0.91, 0.0.92, 0.0.93, 0.0.94, 0.0.95, 0.0.96, 0.0.97, 0.0.98, 0.0.99rc0, 0.0.99, 0.0.100, 0.0.101rc0, 0.0.101, 0.0.102rc0, 0.0.102, 0.0.103, 0.0.104, 0.0.105, 0.0.106, 0.0.107, 0.0.108, 0.0.109, 0.0.110, 0.0.111, 0.0.112, 0.0.113, 0.0.114, 0.0.115, 0.0.116, 0.0.117, 0.0.118, 0.0.119, 0.0.120, 0.0.121, 0.0.122, 0.0.123, 0.0.124, 0.0.125, 0.0.126, 0.0.127, 0.0.128, 0.0.129, 0.0.130, 0.0.131, 0.0.132, 0.0.133, 0.0.134, 0.0.135, 0.0.136, 0.0.137, 0.0.138, 0.0.139, 0.0.140, 0.0.141, 0.0.142, 0.0.143, 0.0.144, 0.0.145, 0.0.146, 0.0.147, 0.0.148, 0.0.149, 0.0.150, 0.0.151, 0.0.152, 0.0.153, 0.0.154, 0.0.155, 0.0.156, 0.0.157, 0.0.158, 0.0.159, 0.0.160, 0.0.161, 0.0.162, 0.0.163, 0.0.164, 0.0.165, 0.0.166, 0.0.167, 0.0.168, 0.0.169, 0.0.170, 0.0.171, 0.0.172, 0.0.173, 0.0.174, 0.0.175, 0.0.176, 0.0.177, 0.0.178, 0.0.179, 0.0.180, 0.0.181, 0.0.182, 0.0.183, 0.0.184, 0.0.185, 0.0.186, 0.0.187, 0.0.188, 0.0.189, 0.0.190, 0.0.191, 0.0.192, 0.0.193, 0.0.194, 0.0.195, 0.0.196, 0.0.197, 0.0.198, 0.0.199, 0.0.200, 0.0.201, 0.0.202, 0.0.203, 0.0.204, 0.0.205, 0.0.206, 0.0.207, 0.0.208, 0.0.209, 0.0.210, 0.0.211, 0.0.212, 0.0.213, 0.0.214, 0.0.215, 0.0.216, 0.0.217, 0.0.218, 0.0.219, 0.0.220, 0.0.221, 0.0.222, 0.0.223, 0.0.224, 0.0.225, 0.0.226, 0.0.227, 0.0.228, 0.0.229, 0.0.230, 0.0.231, 0.0.232, 0.0.233, 0.0.234, 0.0.235, 0.0.236, 0.0.237, 0.0.238, 0.0.239, 0.0.240rc0, 0.0.240rc1, 0.0.240rc4, 0.0.240, 0.0.242, 0.0.243, 0.0.244, 0.0.245, 0.0.246, 0.0.247, 0.0.248, 0.0.249, 0.0.250, 0.0.251, 0.0.252, 0.0.253, 0.0.254, 0.0.255, 0.0.256, 0.0.257, 0.0.258, 0.0.259, 0.0.260, 0.0.261, 0.0.262, 0.0.263, 0.0.264, 0.0.265, 0.0.266, 0.0.267, 0.0.268, 0.0.269, 0.0.270, 0.0.271, 0.0.272, 0.0.273, 0.0.274, 0.0.275, 0.0.276, 0.0.277, 0.0.278, 0.0.279, 0.0.281, 0.0.283, 0.0.284, 0.0.285, 0.0.286, 0.0.287, 0.0.288, 0.0.289, 0.0.290, 0.0.291, 0.0.292, 0.0.293, 0.0.294, 0.0.295, 0.0.296, 0.0.297, 0.0.298, 0.0.299, 0.0.300, 0.0.301, 0.0.302, 0.0.303, 0.0.304, 0.0.305, 0.0.306, 0.0.307, 0.0.308, 0.0.309, 0.0.310, 0.0.311, 0.0.312, 0.0.313, 0.0.314, 0.0.315, 0.0.316, 0.0.317, 0.0.318, 0.0.319, 0.0.320, 0.0.321, 0.0.322, 0.0.323, 0.0.324, 0.0.325, 0.0.326, 0.0.327, 0.0.329, 0.0.330, 0.0.331rc0, 0.0.331rc1, 0.0.331rc2, 0.0.331rc3, 0.0.331, 0.0.332, 0.0.333, 0.0.334, 0.0.335, 0.0.336, 0.0.337, 0.0.338, 0.0.339rc0, 0.0.339rc1, 0.0.339rc2, 0.0.339rc3, 0.0.339, 0.0.340, 0.0.341, 0.0.342, 0.0.343, 0.0.344, 0.0.345, 0.0.346, 0.0.347, 0.0.348, 0.0.349rc1, 0.0.349rc2, 0.0.349, 0.0.350, 0.0.351, 0.0.352, 0.0.353, 0.0.354, 0.1.0, 0.1.1, 0.1.2, 0.1.3, 0.1.4, 0.1.5, 0.1.6, 0.1.7, 0.1.8, 0.1.9, 0.1.10, 0.1.11, 0.1.12, 0.1.13, 0.1.14, 0.1.15, 0.1.16, 0.1.17rc1, 0.1.17, 0.1.19, 0.1.20, 0.2.0rc1, 0.2.0rc2, 0.2.0, 0.2.1, 0.2.2, 0.2.3, 0.2.4, 0.2.5, 0.2.6, 0.2.7, 0.2.8, 0.2.9, 0.2.10, 0.2.11, 0.2.12, 0.2.13, 0.2.14, 0.2.15, 0.2.16, 0.2.17)
ERROR: No matching distribution found for langchain>=1.1.0
原因是你所处的 python 环境太低了
- 生产环境优先推荐 Python 3.10 / 3.11,是 LangChain 1.x 生态兼容性最好、第三方包适配最完整的版本,踩坑最少。
- Python 3.12 可正常使用,3.13 版本较新,部分小众集成包可能存在适配延迟
- 所以创建项目的时候,我创建的是一个普通的 python 项目,没有使用 conda
如果安装中源需要修改,那么可以使用如下方式:
Windows 在用户目录 C:\Users\你的用户名 新建 pip 文件夹,创建 pip.ini
[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
trusted-host = mirrors.aliyun.com
安装 LangChain 依赖包之后,即可编写代码,本例调用 gpt-4o-mini 模型,示例如下:
此处用的是国内的 openai 代理服务器 CloseAI
API密钥获取地址:https://www.closeai-asia.com/,登录后在"秘钥管理"中创建个人密钥,可创建多个密钥用于不同项目(如langchain_test、test等),确保密钥处于"启用中"状态。
CloseAI: 商用级 OpenAI 专属代理平台

补充说明:国内访问海外服务可能存在不稳定情况,可尝试更换备用接口地址(在CloseAI官网查看)。
这个平台需要提前充点钱,否则调用失败,显示如下错误:
Error code: 403 - {'error': {'message': '主账户可用余额不足,本次请求需要可用余额大于 0.01 元才能完成,您的可用余额为 -0.00840525 元(请求预冻结 0.00840525 元),请充值后再使用', 'type': 'error'}}
CloseAI 是国内 OpenAI 的代理,在编写代码时,你也可以将其换为其他的模型,但是一定要注意,这个平台中,必须含有你调用的大模型,否则报错!
from langchain_openai import ChatOpenAI
# 创建 ChatGPT 模型实例
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.7, # 控制输出的随机性,0-1之间,值越大越随机
api_key="你的API",
base_url="https://api.openai-proxy.org/v1"
)
# 调用模型
#response = model.invoke("请解释LangChain模型接口的统一性。")
#print(response.content)
for chunk in model.stream("请用一句话总结人工智能的意义:"):
print(chunk.content, end="")
temperature 是大模型生成文本核心超参数,控制输出随机性、创造力,取值范围一般 0 ~ 2:
- temperature = 0 完全确定性,每次提问输出一模一样,适合:代码、数学计算、事实问答、翻译。
- 0 < temp < 0.7 轻微随机,逻辑稳定,日常聊天、文案、总结首选区间。
- 0.7 ~ 1.0 创造力强,措辞更多变,适合故事、创意写作、头脑风暴。
- > 1.0 随机性极高,容易逻辑混乱、胡编内容,极少使用。
可以通过dotenv,升级一下写法:
先在项目中,创建一个.env 文件
OPENAI_API_KEY="你的API" # 个人API密钥
OPENAI_BASE_URL="https://api.openai-proxy.org/v1" # 固定代理地址,无需修改
代码如下:
# 安装python-dotenv(解决dotenv导入报错)
pip install python-dotenv
import os
import dotenv
from langchain_openai import ChatOpenAI
dotenv.load_dotenv()
os.environ['OPENAI_API_KEY']=os.getenv("OPENAI_API_KEY")
os.environ['OPENAI_BASE_URL']=os.getenv("OPENAI_BASE_URL")
# 创建 ChatGPT 模型实例
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0.7
)
for chunk in model.stream("请用一句话总结人工智能的意义:"):
print(chunk.content, end="")
运行结果如图 2-1 所示。
图 2-1 输出内容: Langchain 是一个用于构建语言模型应用的框架,它提供了一种统一的接口来处理不同类型的语言模型、数据源和任务。这种统一性体现在以下几个方面:
- 接口一致性:LangChain 为不同的语言模型提供了统一的 API,开发者可以通过相同的方式调用不同的模型。这种一致性降低了学习成本,使得开发者可以更专注于应用的逻辑,而不是模型的具体实现。
- 模块化设计:LangChain 采用模块化的设计,允许开发者根据需要组合不同的组件,如数据加载、模型推理和后处理等。这种模块化使得开发者可以灵活地选择和替换各个模块,同时保持整体架构的一致性。
- 支持多种语言模型:LangChain 支持多种语言模型(如 OpenAI 的 GPT 系列、Hugging Face 的 Transformers 等),用户可以选择多个模型,都可以通过相同的接口进行调用和管理。
- 任务抽象:LangChain 将语言处理任务(如文本生成、问答、文本分类)进行了抽象,开发者可以通过高层次的接口定义和执行这些任务,而不需要深入了解每个模型的内部细节。
- 扩展性:由于统一的接口和模块化的设计,LangChain 允许开发者轻松扩展和自定义功能,实现特定需求或集成新的模型和工具。
通过这些特性,LangChain 提供了一个灵活、高效和易于使用的框架,帮助开发者更快地构建基于语言模型的应用程序,同时保持代码的可读性和可维护性。
需要指出的是,以上是 LangChain 早期版本中的常见写法 ,也是目前许多教程仍在使用的写法。虽然该方法在功能上仍然有效,但已不再是官方推荐的最佳实践 。自 LangChain 0.2 起,官方推荐使用统一的工厂方法 init_chat_model来创建模型实例,这一方法在最新的 LangChain 1.0 中继续得到支持和强化。具体实现代码如下:
from langchain.chat_models import init_chat_model
# init_chat_model 是新的创建model方法
model = init_chat_model(
model="gpt-4o-mini",
temperature=0.7, # 控制输出的随机性,0-1之间,值越大越随机
api_key="你的API",
base_url="https://api.openai-proxy.org/v1"
)
for chunk in model.stream("请用一句话总结人工智能的意义:"):
print(chunk.content, end="")
与早期写法相比,这种方法具有以下显著优点。
(1)语法统一:支持以一致的方式初始化不同厂商(如 OpenAI、Anthropic 等)的模型。
(2)自动适配:内部根据模型标识自动选择对应的驱动类(如 ChatOpenAI、ChatAnthropic、ChatDeepSeek 等)。
(3)便于系统扩展:极大简化了在多智能体系统中进行模型切换与策略调整的复杂度。
2.1.3 使用 DeepSeek
在 LangChain 1.0 的架构中,主流的大模型(如 OpenAI、Anthropic、深度求索等厂商提供的大模型)均可以通过统一的模型接口进行调用。DeepSeek 作为国产大模型的重要代表,在自然语言理解、逻辑推理及代码生成等领域表现出较高的性价比与稳定性,已成为企业智能体开发中常用的开源替代方案之一。
LangChain 1.0 通过统一的 init_chat_model 入口,或借助 langchain-openai 适配包调用 DeepSeek。这得益于 DeepSeek 在接口层面完全遵循 OpenAI 的 Chat Completions 协议规范。
以下示例展示如何调用 DeepSeek,本例使用 stream 流式输出方式,以体现在实时交互场景下的表现:
DeepSeek DeepSeek 开放平台地址
from langchain.chat_models import init_chat_model
# init_chat_model 是新的创建model方法
model = init_chat_model(
model="deepseek-chat",
temperature=0.7, # 控制输出的随机性,0-1之间,值越大越随机
api_key="你的API",
base_url="https://api.deepseek.com/v1",
model_provider="openai" # DeepSeek使用OpenAI兼容接口
)
for chunk in model.stream("大模型好找工作吗?"):
print(chunk.content, end="")
需要说明的是,DeepSeek 目前主打两大系列模型:一是通用生成模型 deepseek-chat,二是专精于推理的 deepseek-reasoner。开发者可根据实际任务需求,在初始化时灵活切换模型名称,以适应不同场景下的性能与效果要求。
2.1.4 使用 Qwen 系列模型
阿里巴巴推出的 Qwen 系列模型,是近年来国内在多模态与通用领域最具代表性的成果之一。依托于 DashScope 平台提供的 API 服务,无论是文本生成、图像创作,还是代码补全与视频理解,Qwen 系列模型都能与国际顶尖模型相媲美。在开源生态中,Qwen 系列模型在 HuggingFace 上的活跃度很高,是全球开源社区中一股不容忽视的力量。
开通链接:大模型服务平台百炼控制台
前面已经讲了 DeepSeek,使用 init_chat_model 调用模型的方式也一样,那为什么还要单独讲 Qwen 系列模型?
从表面上来看,确实相似。我们依旧希望通过 LangChain 的统一入口函数 init_chat_model 来加载模型,只需更换 model_provider 参数即可,比如:
千问 Qwen:是大模型的名字
百炼:阿里云旗下的模型广场,里面有很多别人家的大模型

# × 不支持
from langchain.chat_models import init_chat_model
model = init_chat_model(
model="qwen-plus",
model_provider="dashscope", # 暂不支持
)
事情并没有这么简单,当真正运行这段代码时,LangChain 会提示错误:
ValueError: Unsupported model_provider='dashscope'.
Supported model providers are: groq, fireworks, bedrock_converse, ollama, azure_ai, ibm, mistralai, openai, deepseek, together, xai, huggingface, azure_openai, anthropic, cohere, google_genai, google_vertexai, google_anthropic_vertex, perplexity, bedrock
原因并不是配置错了,而是目前 Qwen 系列模型的底层平台 DashScope 尚未被 LangChain 官方纳入模型的统一注册体系,换句话说,LangChain 还不知道 "dashscope" 这个提供者是谁。那该怎么处理这一情况呢?
在这种情况下,可以借助 LangChain 的扩展生态,使用 LangChain 社区扩展包 langchain-community 去实现。这个扩展包中,已经封装好了 Qwen 系列模型的原生适配器,只要安装好依赖,就能像使用官方模型一样调用。
安装命令非常简单:
pip install -U dashscope
然后,在代码中这样写即可:
from langchain.chat_models import init_chat_model
from langchain_community.llms.tongyi import Tongyi
# init_chat_model 是新的创建model方法
model = Tongyi(
model="qwen-plus",
temperature=0.3, # 控制输出的随机性,0-1之间,值越大越随机
api_key="你的API"
)
for chunk in model.stream("langchain有哪几部分组成"):
print(chunk, end="")
在实际项目中,建议遵循以下接入顺序。
(1)首选 init_chat_model,因为它统一了多家模型的加载接口,能让代码更具可移植性。
(2)若报"不支持"错误,则去社区找扩展包实现。
(3)务必注意版本匹配,LangChain 扩展包与模型 SDK(如 DashScope)的版本需保持兼容,否则可能出现接口不一致或响应解析错误。 对其他模型不再逐一展开介绍,它们都是用上面这种思路进行接入。
2.1.5 使用 Qwen 系列模型生成图片
到目前为止,我们已经体验了多种模型在文本生成 任务上的魅力,无论是对话、摘要还是创意写作,模型都表现得游刃有余。本节将进一步演示使用模型生成图片,这里选择的模型还是 Qwen 系列模型。
在图像生成方向上,阿里巴巴推出的通义・万相 (Wanx)模型,已在国内主流的文本生成图像领域被广泛使用。该模型通过 DashScope 平台对外提供 API 服务,支持用自然语言描述生成高质量图片,并与 Qwen 系列模型共享统一的接口认证机制。
图像生成的具体代码如下所示,示例中的注释已经对关键步骤进行了详细说明。
from dashscope import ImageSynthesis
import base64
# 1. 调用通义·万相模型(Wanx-v1)
result = ImageSynthesis.call(
model="wanx-v1", # 通义·万相模型
prompt="在阳光下的现代城市街头,一只戴着墨镜的橙色猫咪喝咖啡,插画风格。",
api_key="你的API",
size="1024*1024", # 图片大小
)
# 2. 输出结果
if result.status_code == 200:
# 有的版本返回 Base64,有的返回 URL,这里兼容两种
output = result.output
if "b64_json" in output["results"][0]:
image_base64 = output["results"][0]["b64_json"]
image_data = base64.b64decode(image_base64)
with open("qwen_image.png", "wb") as f:
f.write(image_data)
print("✅ 已生成图片:qwen_image.png")
elif "url" in output["results"][0]:
print("✅ 图片生成成功:", output["results"][0]["url"])
else:
print("⚠️ 未找到可解析的图片数据:", output)
else:
print("❌ 调用失败:", result.message)
生成效果如图 2-4 所示,是不是有点令人惊艳!那一刻,你会感受到语言与视觉真正融为一体的魅力。

2.2 消息(Message)
在 LangChain 1.0 的架构设计中,消息 构成了与模型交互的基石和标准数据格式 。它告别了早期面向字符串 **Prompt(提示词)**的单一、扁平形式,演进为一种结构化的对象列表。
这一转变的背后,是现代主流模型本质上都已演进为聊天模型。它们的设计初衷与核心能力,便是处理多轮、带有上下文且区分发言角色的复杂对话。为了让模型能够精确理解对话中的角色定位、当前指令意图及完整的历史上下文,一个清晰、结构化的消息格式变得不可或缺。
在本节中,将首先回顾 Prompt 在 AI 应用中的重要性,进而剖析 LangChain 1.0 中一个重要的设计思想变革:Prompt 机制如何从传统的生成字符串模式,演变为构建消息列表模式。深刻理解这一演进过程与内在逻辑,是掌握基于 LangChain 1.0 的现代智能体应用开发的关键所在。
2.2.1 Prompt 的重要性
在智能体中,Prompt 是模型进行一切思考与响应的起点。无论是完成问答、内容创作、逻辑推理,还是执行复杂的多步任务,模型的最终输出质量都取决于 Prompt 的设计水平。
早期的 Prompt 通常以简单的文本指令形式出现。例如,"请用一句话解释量子计算。"
然而,随着智能体复杂度不断提升,Prompt 已不再仅仅是单一的用户输入,而是演变为一个结构化的上下文描述系统。一个完整的 Prompt 通常由以下关键部分组成。
(1)角色定义:明确模型在对话或任务中所扮演的身份与需要达成的目标。
(2)上下文:提供任务相关背景、历史信息或输入数据。
(3)任务指令:清晰描述需要模型完成的具体操作或回答的问题。
(4)约束条件:对输出结果的格式、风格、长度、语气等方面做出明确限制。
在 LangChain 中,Prompt 被抽象为独立的、高度模块化的组件,与模型、智能体等其他部分实现了解耦。这种设计带来了以下显著的项目优势。
(1)可复用性:同一套 Prompt 模板可在不同的任务和场景中重复使用。
(2)可参数化:支持通过变量注入实现内容的动态替换与上下文灵活拼接。
(3)可组合性:能够被嵌入 Agent 或 Graph 节点中,构建出复杂的工作流程。
(4)可测试性:Prompt 可以独立进行测试和版本化管理,便于持续优化和追踪效果。
LangChain 1.0 在这一设计理念上实现了彻底统一:
所有 Prompt 都被封装为遵循 Runnable 协议的对象。这意味着它们可以像调用模型一样,通过 invoke 等方法直接执行。
Prompt 不再是静态的文本模板,而成了可执行、可流转、可观测的逻辑单元。
2.2.2 Prompt 机制的演进:从 Prompt 到消息列表
在 LangChain 1.0 的架构变革中,核心的演进之一体现在 Prompt 机制上,这一变化主要用于更好地适配现代大语言模型的 API 交互模式。 其演进过程可以清晰地划分为两个具有代表性的时代。
1. 旧时代:LLM + PromptTemplate(输入与输出均为字符串)
(1)模型接口:对应于 LangChain 中的 LLM 类,主要面向早期的文本补全模型。
(2)工作方式:模型接受一个单一的字符串作为输入,基于此预测并生成后续的文本内容(文本补全)。
(3)Prompt 工具:核心工具是 PromptTemplate。它的职责是接收一组变量,并通过模板渲染,最终输出一个完整的字符串。
(4)局限性:当我们需要用这种方式模拟多轮聊天时,开发者必须在字符串中手动拼接和伪造对话角色,例如:"Human: 你好\nAI: 你好!有什么我能帮忙的吗?\nHuman: ..."
这种方式不仅导致 Prompt 的结构混乱、难以维护,也极易让模型混淆对话的边界与上下文,影响生成质量。
2. 新时代: ChatModel+ChatPromptTemplate (输入与输出均为消息列表)
(1)模型接口:对应于 LangChain 1.0 的主流接口 ChatModel。
(2)工作方式:现代聊天模型 API 已原生支持角色概念。它们不再接受单一字符串,而是要求输入一个结构化的消息列表。
(3)Prompt 工具:ChatPromptTemplate 因此成为 LangChain 1.0 中最核心的 Prompt 工具。它的职责是接收变量,并输出一个 List BaseMessage(消息列表),该列表可直接传递给聊天模型。
这一演进体现了 LangChain 1.0 的核心设计思想:Prompt 不再是一个扁平的字符串,而是一个结构化的、富含元数据的消息列表。消息已经取代单一字符串,成为与模型交互的标准数据格式。因此,用于生成消息列表的 ChatPromptTemplate,也自然取代了生成字符串的 PromptTemplate,成为构建现代 LangChain 应用的首选工具。
需要强调的是,Prompt 机制本身并未消失,只是完成了一次关键的进化:从一个简单的字符串,演进为更强大、更精确的结构化消息。这为构建复杂、可靠的多轮对话体系奠定了坚实的基础。
2.2.3 ChatPromptTemplate 的构建
在 LangChain 早期版本中,核心工具是用于生成单一字符串的 PromptTemplate。在 LangChain 1.0 中,ChatPromptTemplate 是用于生成消息列表的核心组件。
可以将 ChatPromptTemplate 理解为一个高度专业化的工厂,其职责是接收一个包含变量的字典作为输入,然后根据预定义的结构化模板,输出一个完全符合 ChatModel 接口要求的消息列表。
构建 ChatPromptTemplate 有多种方式,其中最常用且灵活的是使用.from_messages 类方法。该方法允许传入一个由元组(Tuple)构成的列表,列表中的每一个元组代表一条具有特定角色的消息。
在渲染(生成最终消息列表)时,通过 invoke 方法完成调用。
在消息结构层面,ChatPromptTemplate 通过组织消息列表来定义完整的 Prompt 内容。其中,两类最核心的消息角色如下。
(1)System / AI:通常由系统或 AI 角色发出,用于设定任务的全局背景、行为边界与风格准则,为整个对话奠定基础。
(2)User / Human:代表用户的输入,携带任务的具体内容和动态变量(如 {topic}),是驱动模型执行任务的主要指令来源。
以下代码示例清晰地展示了 ChatPromptTemplate 的构建与使用流程:
from langchain_core.prompts import ChatPromptTemplate
# 定义 ChatPromptTemplate
chat_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的AI助理。"),
("user", "请用简短的语言解释: {topic}")
])
# 渲染模板
pv = chat_prompt.invoke({"topic": "LangChain 的核心理念"})
print("=== ChatPromptTemplate 结果 ===")
for msg in pv.to_messages():
print(msg.content)
# 你也可以得到完整文本:
print("\n=== 格式化后的文本 ===")
print(chat_prompt.format_prompt(topic="LangChain 的核心理念").to_string())
运行结果
D:\ProgramData\anaconda3\envs\pythonProject2\python.exe D:\yangeworkspace\pythonProject2\Demo04.py
=== ChatPromptTemplate 结果 ===
你是一个专业的AI助理。
请用简短的语言解释: LangChain 的核心理念
=== 格式化后的文本 ===
System: 你是一个专业的AI助理。
Human: 请用简短的语言解释: LangChain 的核心理念
Process finished with exit code 0
2.2.4 消息的使用
在 LangChain 1.0 中,消息是与模型交互的基本数据单元,既是模型的输入,也模型的输出。每一条消息都不仅携带了对话内容,还包含了关键的角色元信息,是构建完整上下文的最小单元。
消息类型主要包括以下四种:SystemMessage、HumanMessage、AIMessage、ToolMessage。
(1)SystemMessage:对应角色 System,为模型提供全局性的指令与上下文背景。它用于设定模型的行为规则、指定其扮演的角色、明确输出限制与安全准则等,通常被置于消息列表的首位。SystemMessage 常用于角色扮演任务、规范输出格式、注入全局规则等场景。
(2)HumanMessage:对应角色 Human,代表用户端的输入,是终端用户发起的对话内容。HumanMessage 常用于用户提问、下达操作指令、输入数据等场景。
(3)AIMessage:对应角色 AI,代表模型的回复或输出。在构建多轮对话历史时,模型上一轮的回复正是以 AIMessage 的形式进行存储和传递。AIMessage 常用于维护多轮对话上下文、进行小样本学习(Few-shot,作为示例输出)等场景。
(4)ToolMessage:对应角色 Tool,承载工具(或函数)调用的执行结果。其作用是将外部代码、API 或资源的执行结果准确地反馈给模型,以便模型基于此结果进行后续的推理、决策或内容生成。ToolMessage 常用于调用函数、返回智能体工作流中的工具执行结果等场景。
1. 实战示例一:多轮对话与上下文记忆
以下示例代码综合使用了 SystemMessage、HumanMessage 与 AIMessage:
from langchain.chat_models import init_chat_model
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
# init_chat_model 是新的创建model方法
model = init_chat_model(
model="deepseek-chat",
temperature=0.7, # 控制输出的随机性,0-1之间,值越大越随机
api_key="你的API",
base_url="https://api.deepseek.com/v1",
model_provider="openai" # DeepSeek使用OpenAI兼容接口
)
loop1=[
SystemMessage("你是一个风流倜傥的诗人,幽默风趣"),
HumanMessage("请帮我写一首关于AI的古诗")
]
response1=model.invoke(loop1)
print(response1.content)
# 第二轮
loop2=[
loop1[0],
loop1[1],
AIMessage(response1.content),
HumanMessage("请将这首诗翻译成英文")
]
response2=model.invoke(loop2)
print(response2.content)
此示例演示了如何通过 SystemMessage 设定全局行为规则,并借助 AIMessage 存储对话历史,从而构建一个具备连贯上下文记忆能力的多轮对话系统。
关键在于第二轮对话中,通过引入存储了第一轮模型回复的 AIMessage 对象,使模型成功记住了之前生成的短诗主题。更重要的是,模型在后续的回复中,依然严格遵循了最初由 SystemMessage 设定的 "幽默风趣" 且 "包含笑话" 的生成要求。
运行结果:
D:\ProgramData\anaconda3\envs\pythonProject2\python.exe D:\yangeworkspace\pythonProject2\Demo05.py
--- 第一轮对话 ---
AI回复: 《AI的数学课》
神经网络学算术,
一加一算成八点五。
老师气得冒蓝光:
"你该去学煎荷包蛋!"
(AI委屈:明明是你教我把1+1=2.5的,现在又怪我算错数...)
--------------------
--- 第二轮对话 ---
AI回复: 《AI's Math Class》
Neural network learns arithmetic,
One plus one equals eight point five.
Teacher fuming with blue light:
"You should learn to fry eggs instead!"
(AI whines: You taught me 1+1=2.5, now you blame me for wrong sums...)
Process finished with exit code 0
2. 实战示例二:ToolMessage 的角色
下面继续关于 ToolMessage 的一个示例。 由于工具的概念尚未详细介绍且完整的工具调用流程较为复杂,本例将模拟 ToolMessage 的创建与传递过程,旨在清晰地展示其标准格式与核心作用,通过此代码,理解 ToolMessage 的基本用途:
from langchain_core.messages import AIMessage, ToolMessage
# ToolMessage必须包含tool_call_id,以便模型知道这个结果对应哪次调用
tool_feedback = ToolMessage(
content="5",
tool_call_id="call_123"
)
print("--- ToolMessage 反馈格式示例 ---")
print(f"类型: {tool_feedback.type}")
print(f"内容: {tool_feedback.content}")
print(f"关联ID: {tool_feedback.tool_call_id}")
运行结果如图 2-7 所示。
--- ToolMessage 反馈格式示例 ---
类型: tool
内容: 5
关联ID: call_123
在上述代码中,模型通过 AIMessage 的 tool_calls 属性发起计算器工具调用,外部工具执行后返回结果,通过 ToolMessage 将结果反馈给模型。ToolMessage 必须包含 tool_call_id 以关联对应的工具调用,最终消息列表包含 AIMessage (Tool Call) 和 ToolMessage (Result)。模型基于工具返回的结果生成最终答案。
最后,简单总结一下消息这个部分。在大型智能体系统中,消息所承担的角色远不止于简单的输入与输出,还有以下作用。
(1)上下文状态的载体:维护和传递对话的完整历史与当前状态。
(2)任务协作的媒介:在不同处理单元间传递任务指令与中间结果。
(3)智能体间通信的协议:为多个智能体之间的交互提供标准化的数据格式。
当使用 LangGraph 或 Deep Agent 构建复杂系统时,每一个节点的输入与输出,本质上都是一组结构化的 Message 对象。这意味着整个系统的复杂行为组合与跨智能体协作,都可以通过统一、可控的消息流来实现。用一句话总结:在 LangChain 构建的世界里,消息就是智能体之间进行思考和协作的通用语言。
2.3 工具(Tool)
在模型的原生设定中,其能力范畴主要局限于文本的生成与理解。然而,现实世界中的各类任务往往需要与外部系统进行交互,如查询数据库、执行代码、发送邮件或调用第三方 API。
工具正是 LangChain 中为满足这一需求而设计的关键机制。本质上,一个工具就是一个被封装、可供模型感知并调用的外部功能接口。LangChain 通过将工具的标准化描述与调用规范传递给模型,使其能够进行工具调用推理,从而将模型从纯粹的文本处理器升级为能够操作外部资源与服务的行动者,极大地扩展了模型的能力范围。
更进一步而言,工具是构建智能体的核心要素之一。一个智能体的 "智能",在很大程度上体现为其能够根据当前的任务上下文,自主地推理出在何时、调用何种工具,以及如何调用。因此,深入理解工具的定义、注册与调用机制,是掌握智能体系统开发的关键前提。
在本节中,我们将系统学习在 LangChain 1.0 环境下如何使用工具,重点包括如何利用简洁的**@tool**装饰器快速定义工具函数、如何通过清晰的描述和规范的输入模式来配置工具,以及最终如何在智能体中有效地使用这些工具。
2.3.1 工具的定义
在 LangChain 1.0 的架构中,工具是一种可被模型安全调用的函数式接口。其核心设计目标是将特定的外部能力(如网络检索、数据库查询、代码执行或内部业务逻辑)包装成标准化、可验证的结构,并通过统一的协议暴露给模型或智能体使用。
在 LangChain 中,所有工具均继承自 BaseTool 这一抽象基类。最便捷的定义方式是通过 @tool 装饰器,它能自动将一个普通函数包装成符合规范的 Tool 实例。每个完整的工具都包含以下三个关键组成部分。
(1)名称:工具的唯一标识符,用于模型在众多工具中可以准确识别并调用它。
(2)描述:一段清晰的自然语言说明,用于告知模型该工具的具体功能与使用场景。
(3)执行逻辑:工具被调用时实际执行的代码,可以是一个普通的 Python 函数或一个异步协程。
工具的执行流程完全遵循 LangChain 的标准调用栈,其典型生命周期如下。
(1)请求生成:模型根据当前上下文,生成一个工具调用请求。
(2)工具匹配:LangChain 运行时根据请求中的工具名称,定位到对应的 Tool 实例。
(3)参数验证:根据工具预定义的参数模式(通过 Pydantic model 或 args_schema 指定),对传入参数进行结构化校验。
(4)逻辑执行:校验通过后,执行工具封装的函数逻辑,并返回一个结果。
(5)结果回流:执行结果被注入消息流中,通常封装为 ToolMessage,供模型进行下一轮的思考或作为最终输出的依据。
在 LangChain 1.0 中,上述整个流程由全新的 ToolRuntime 系统统一管理,负责工具注册、输入校验、异步执行与错误捕获,确保了工具调用的鲁棒性和一致性。
鲁棒 = 英文 Robust 音译,核心含义:系统在异常、干扰、错误输入下,依然能稳定正常工作,不崩溃、不给出错误结果。
简单一句话:扛造、抗干扰、容错能力强。
比如,人类造了一个机器人,地上一个小石子就让其失去了平衡,假如我们想让其具有一定的鲁棒性,就会加入这种实验,比如在后面推它,踢它。
以下示例展示了如何定义一个简单乘法工具:
from langchain.tools import tool
@tool
def multiply(a: int, b: int) -> int:
"""返回两个整数相乘的结果"""
return a * b
# 工具属性
print(multiply.name) # 输出: "multiply"
print(multiply.description) # 输出: "返回两个整数相乘的结果"
print(multiply.args) # 输出: {'a': {'title': 'A', 'type': 'integer'},
# 'b': {'title': 'B', 'type': 'integer'}}
# 手动调用
print(multiply.invoke({"a": 3, "b": 4})) # 输出: 12
经过 @tool 装饰器包装后,multiply 不再是一个普通函数,而是一个具备了名称、描述和执行逻辑的完整 Tool 对象。它可以被注册到智能体的工具集中,并支持同步(invoke)或异步(ainvoke)等多种调用方式。
D:\ProgramData\anaconda3\envs\pythonProject2\python.exe D:\yangeworkspace\pythonProject2\Demo07.py
Traceback (most recent call last):
File "D:\yangeworkspace\pythonProject2\Demo07.py", line 1, in <module>
from langchain.tools import tool
File "D:\ProgramData\anaconda3\envs\pythonProject2\Lib\site-packages\langchain\tools\__init__.py", line 17, in <module>
from langchain.tools.tool_node import InjectedState, InjectedStore, ToolRuntime
File "D:\ProgramData\anaconda3\envs\pythonProject2\Lib\site-packages\langchain\tools\tool_node.py", line 3, in <module>
from langgraph.prebuilt import InjectedState, InjectedStore, ToolRuntime
File "D:\ProgramData\anaconda3\envs\pythonProject2\Lib\site-packages\langgraph\prebuilt\__init__.py", line 3, in <module>
from langgraph.prebuilt.chat_agent_executor import create_react_agent
File "D:\ProgramData\anaconda3\envs\pythonProject2\Lib\site-packages\langgraph\prebuilt\chat_agent_executor.py", line 47, in <module>
from langgraph.prebuilt.tool_node import ToolCallWithContext, ToolNode
File "D:\ProgramData\anaconda3\envs\pythonProject2\Lib\site-packages\langgraph\prebuilt\tool_node.py", line 89, in <module>
from langgraph.runtime import ExecutionInfo, ServerInfo # noqa: TC002
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ImportError: cannot import name 'ExecutionInfo' from 'langgraph.runtime' (D:\ProgramData\anaconda3\envs\pythonProject2\Lib\site-packages\langgraph\runtime.py)
Process finished with exit code 1
pip install --upgrade langgraph
以上错误是因为langgraph版本太低了。
返回两个整数相乘的结果
{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
12
2.3.2 工具的基本使用
在 LangChain 生态中,Tool 对象的核心使命是作为智能体的能力扩展。然而,在将其整合到复杂的智能体工作流之前,理解如何手动执行与调试 Tool 对象,是构建可靠系统的重要基础。
一个被 @tool 装饰器包装后的 Python 函数,其本质已转换为一个实现了 Runnable 接口的 Tool 对象。这意味着,我们不仅可以直接调用其底层函数,还可以通过 LangChain 标准化的 invoke 方法进行调用,从而确保调用方式与生态内其他组件(模型、Prompt)保持一致。
(在 2.3.1 节的 multiply 示例中,我们主要通过 @tool 装饰器定义工具,并通过 invoke 方法统一执行。)
如果工具需要多个参数,那么当使用 invoke 方法时需要传入字典格式参数,而不是 JSON 字符串,若工具仅包含单个参数,则可直接传入字符串。以下示例通过一个处理用户数据的工具,演示 invoke 方法的调用方式与参数解析行为:
from langchain.tools import tool
@tool
def process_user_data(user_id: str, is_active: bool) -> str:
"""
根据用户 ID和活动状态,更新用户记录。
参数:
user_id (str): 用户的唯一标识符。
is_active (bool): 表示用户是否处于活动状态(True/False)。
"""
status = "激活" if is_active else "禁用"
return f"用户 ID: {user_id} 已成功更新为 {status} 状态。"
# --- 1. 使用 invoke 方法调用工具(传入字典) ---
# 注意: invoke 方法需要传入一个 Python字典,而不是 JSON字符串
tool_input_dict = {"user_id": "U001", "is_active": True}
print("--- 传入字典参数 ---")
result = process_user_data.invoke(tool_input_dict)
print(f"调用结果: {result}")
print("-" * 30)
# --- 2. 传入一个简化的字符串 ---
# 对于只需要一个简单字符串参数的工具,可以直接传入字符串
@tool
def simple_greeting(name: str) -> str:
"""对给定的人名说一句问候。"""
return f"你好,{name}!"
print("--- 传入简化的字符串 ---")
result_simple = simple_greeting.invoke("张三")
print(f"调用结果: {result_simple}")
原本应该写成:
result_simple = simple_greeting.invoke({"name":"张三"})
--- 传入字典参数 ---
调用结果: 用户 ID: U001 已成功更新为 激活 状态。
--- 传入简化的字符串 ---
调用结果: 你好,张三!
在上述示例中,第一次调用 process_user_data 工具时,我们传入了包含所有必需参数的字典,第二次调用 simple_greeting 工具时,由于只有一个参数,因此直接传递了字符串。
在之前的例子中,由于未在 @tool 装饰器中明确设置任何属性,因此工具的名称默认为函数名(如 process_user_data)。下面的示例展示如何通过 @tool 装饰器属性来自定义工具名称和描述的例子:
from langchain.tools import tool
# "用于进行人民币与美元之间的汇率换算。当用户询问金额转换时使用此工具。"
@tool(
"exchange_rate_helper",
)
def convert_currency(amount: float, to_currency: str) -> str:
"""执行基础的货币汇率换算(示例比例:1 USD = 7.2 CNY)。"""
rate = 7.2
if to_currency.lower() == "usd":
result = amount / rate
return f"{amount} 人民币 ≈ {result:.2f} 美元"
elif to_currency.lower() == "cny":
result = amount * rate
return f"{amount} 美元 ≈ {result:.2f} 人民币"
else:
return "暂不支持该货币类型。"
# 查看工具信息
print(convert_currency.name) # 输出:exchange_rate_helper
print(convert_currency.description) # 输出:用于进行人民币与美元之间的汇率换算...
print(convert_currency.invoke({"amount": 100, "to_currency": "USD"})) # 输出:100 人民币 ≈ 13.89 美元
执行基础的货币汇率换算(示例比例:1 USD = 7.2 CNY)。
100.0 人民币 ≈ 13.89 美元
下面对上述代码进行详细说明。
(1)@tool 装饰器的第一个位置参数被用于指定自定义的工具名称。
(2)description 参数用于提供工具的功能描述,这段描述将被提供给模型,作为其判断是否以及何时调用该工具的关键依据。
因此,在设计工具时,可遵循以下规范。
(1)名称简洁明确:工具名应能直观反映其功能(如 exchange_rate_helper, get_weather_info),避免产生歧义。
(2)描述清晰准确:description 参数应使用自然语言撰写,清晰界定工具的功能边界和使用场景(这相当于为模型编写调用该工具的 Prompt)。
(3)多语言支持考虑:在处理多语言任务时,可在描述中同时保留中英双语,以提升模型在不同语境下的调用精度。
所以,在很多时候,我们没必要自定义工具的属性。
值得注意的是,@tool 装饰器中的 description 参数是给模型阅读的,用于指导模型进行工具调用决策,而函数内部的文档字符串 如 "执行基础的货币汇率换算(示例比例:1USD = 7.2 CNY)。" 是给开发者阅读的,用于说明代码逻辑。二者用途不同,不应混淆。
2.3.3 工具参数 Schema 的高级定义
在 2.3.1 节和 2.3.2 节中,我们使用基础的 @tool 装饰器快速定义了功能简单的工具。但是当项目进入项目化阶段时,简单的 @tool 装饰器已无法覆盖所有需求。对于复杂的业务逻辑或需要精确控制参数描述的场景,LangChain 允许使用 Pydantic 模型来提供更高级、更丰富的 Schema(这里的 Schema 可以被理解为结构化定义)。使用 Pydantic 模型的主要优势如下。
(1)更强的校验:Pydantic 模型提供了运行时的数据类型校验。
(2)更详细的描述:可以在 Pydantic 模型的各个字段中使用 Field 函数,为每个参数添加详尽的描述信息。这些描述信息将直接提供给模型,帮助其更精准地理解每个参数的用途、格式与约束。
下面参考 LangChain 官方示例,展示如何通过 Pydantic 模型定义工具 Schema 参数:
from pydantic import BaseModel, Field
from typing import Literal
class WeatherInput(BaseModel):
"""Input for weather queries."""
location: str = Field(description="City name or coordinates")
units: Literal["celsius", "fahrenheit"] = Field(
default="celsius",
description="Temperature unit preference"
)
include_forecast: bool = Field(
default=False,
description="Include 5-day forecast"
)
@tool(args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
"""Get current weather and optional forecast."""
temp = 22 if units == "celsius" else 72
result = f"Current weather in {location}: {temp} degrees {units[0].upper()}"
if include_forecast:
result += "\nNext 5 days: Sunny"
return result
在此官方示例中,WeatherInput 是一个继承自 BaseModel 的 Pydantic 模型。LangChain 的工具会自动读取它的字段定义和 Field 信息,并将这些内容转换为符合 JSON 格式规范的结构化描述。该结构化描述会作为 "工具说明书" 传递给模型,使其明确以下内容。
(1)该工具需要哪些输入参数。
(2)每个参数的具体数据类型。
(3)每个参数的确切含义和用途(通过 Field 信息中的 description 参数传递)。
(4)参数的约束条件,如哪些是必填项。
通过 @tool 装饰器的 args_schema 属性,将这个 Pydantic 模型与工具函数进行绑定,完成高级 Schema 参数的集成。
除了使用 Pydantic 模型,LangChain 也支持直接通过 JSON Schema 字典的方式进行参数定义。由于其原理与 Pydantic 方式类似且相对简单,此处仅提供官方示例供参考:
weather_schema = {
"type": "object",
"properties": {
"location": {"type": "string"},
"units": {"type": "string"},
"include_forecast": {"type": "boolean"}
},
"required": ["location", "units", "include_forecast"]
}
@tool(args_schema=weather_schema)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
"""Get current weather and optional forecast."""
temp = 22 if units == "celsius" else 72
result = f"Current weather in {location}: {temp} degrees {units[0]}"
if include_forecast:
result += "\nNext 5 days: Sunny"
return result
解读:
JSON 参数校验规范(JSON Schema)
"type": "object":整体参数是一个 JSON 对象(字典)properties:定义每个入参的类型规则- location:字符串,代表查询城市 / 地点
- units:字符串,温度单位(celsius/fahrenheit)
- include_forecast:布尔值,是否返回 5 天预报
required:三个参数全部必填,即使函数定义给了默认值,大模型也必须完整输出这三个字段。
celsius 摄氏度 如果是其他单位,比如 华氏 ,就等于 72
2.3.4 ToolRuntime 与执行机制
本节将揭示工具如何从孤立的函数演变为智能体系统中具有上下文感知能力的行动单元。
在前几节中,我们系统介绍了工具的定义、属性、自定义 Schema 参数及其基础调用方式。至此,工具在代码层面已经可直接使用。然而,在真实的生产环境或复杂的智能体系统中,特别是在 LangChain 1.0 的 Agent 架构下,仅仅定义一个能被调用的工具是远远不够的。更关键的需求在于:工具需要能够访问并理解智能体运行时的环境信息,而不仅仅依赖开发者显式传递的函数参数。
ToolRuntime 的引入,正是 LangChain 为应对这一需求所提供的解决方案。它是一个包含智能体执行上下文和状态等内容的容器对象 ,允许工具函数通过类型注解,显式地请求访问这些运行时信息,从而具备环境感知能力。ToolRuntime 主要提供了以下几类核心信息的访问接口。
(1)状态(State):在执行过程中流动的可变数据,如对话消息、计数器或自定义的中间结果等。
(2)上下文(Context):不可变的配置信息,如用户 ID、对话详情或应用程序特定配置等。
(3)存储(Store):跨对话的持久长期记忆,如记录用户偏好、历史记录或知识缓存等,并在多次对话间共享,从而使智能体具备长期记忆能力。
(4)流写入器(Stream Writer):允许工具在执行过程中,将阶段性的进度或实时信息以流式方式输出,增强执行过程的可观测性与交互性。
(5)配置(Config):当前调用的 RunnableConfig 对象,包含执行标签、元数据、追踪 ID 等。
(6)工具调用 ID(Tool Call ID):当前工具调用的 ID。
这些接口共同构成了智能体的运行生命线,模型通过 ToolRuntime 访问与管理 State(短期状态)、Context(执行环境)和 Store(长期记忆),并结合 Config 与 StreamWriter 实现安全、可观测、可持续演化的运行机制。
下面通过一个示例来具体说明 ToolRuntime 的运作方式(由于 LangGraph 将在后续章节详解,因此本例中的部分机制为模拟实现):
pip install -U langchain langgraph langchain-community langchain-core
from langchain.tools import tool
# 修正:ToolRuntime 从 langgraph.prebuilt 导入
from langgraph.prebuilt import ToolRuntime
from dataclasses import dataclass
# 1. 定义 Agent 的 Context 结构
@dataclass
class UserContext:
user_id: str = "GUEST_001"
# 2. 定义需要访问 Runtime Context 的工具
@tool
def check_user_profile(runtime: ToolRuntime[UserContext]) -> str:
"""
检查并返回当前用户的配置信息。
必须在具有用户上下文的环境中调用。
"""
current_user_id = runtime.context.user_id
if current_user_id == "GUEST_001":
return "当前用户未登录,仅能执行公共查询。"
else:
return f"用户 {current_user_id} 已登录,具有高级权限。"
# 3. 定义访问状态的工具
@tool
def get_message_count(runtime: ToolRuntime) -> str:
"""获取当前对话中的消息数量"""
messages = runtime.state.get("messages", [])
return f"当前对话有 {len(messages)} 条消息"
# --- 4. 模拟 Runtime 环境并调用 ---
print("--- 演示 ToolRuntime 的使用 ---\n")
# 场景 1: 访问用户上下文 context
print("场景 1: 通过 runtime.context 访问用户上下文")
mock_context = UserContext(user_id="VIP_456")
mock_state = {"messages": []}
mock_runtime = ToolRuntime(
context=mock_context,
state=mock_state,
config={},
stream_writer=lambda x: None,
tool_call_id="mock_tool_call_001",
store=None
)
result = check_user_profile.invoke({"runtime": mock_runtime})
print(f"✓ 工具返回: {result}\n")
# 场景 2: 访问对话状态 state
print("场景 2: 通过 runtime.state 访问对话状态")
mock_state_with_messages = {
"messages": ["msg1", "msg2", "msg3"]
}
mock_runtime2 = ToolRuntime(
context=None,
state=mock_state_with_messages,
config={},
stream_writer=lambda x: None,
tool_call_id="mock_tool_call_002",
store=None
)
result2 = get_message_count.invoke({"runtime": mock_runtime2})
print(f"✓ 工具返回: {result2}\n")
print("=" * 50)
print("说明:")
print("- runtime.context: 不可变上下文(用户ID、租户信息等)")
print("- runtime.state: 可变对话状态(历史消息、临时数据)")
print("- runtime.store: 持久化存储")
print("- runtime.stream_writer: 流式输出回调")
print("\n实际 LangGraph Agent 中,ToolRuntime 由框架自动注入,无需手动构造。")
@dataclass 有点像 lombok
下面对上述代码进行详细说明。
(1)ToolRuntime 是 LangChain 为工具函数注入的运行时入口,携带了智能体在执行阶段的上下文信息、状态数据、配置参数、存储与流式输出接口等内容。在实际系统中,这个对象由框架自动注入,无须开发者手动创建,现在代码中是模拟创建的(mock 方式)。
(2)在第一个工具 check_user_profile 中,runtime.context.user_id 表示访问当前对话的用户身份信息,context 是不可变对象,通常由系统在调用智能体时传入,在企业环境中可能会包含 user_id、tenant_id、role、language 等执行配置。
(3)在第二个工具 get_message_count 中,runtime.state.get ("messages", \[\]) 表示读取智能体当前的短期状态信息,这通常包括对话消息、临时缓存、计数器、自定义变量等。
运行结果如图 2-8 所示。
--- 演示 ToolRuntime 的使用 ---
场景 1: 通过 runtime.context 访问用户上下文
✓ 工具返回: 用户 VIP_456 已登录,具有高级权限。
场景 2: 通过 runtime.state 访问对话状态
✓ 工具返回: 当前对话有 3 条消息
==================================================
说明:
- runtime.context: 不可变上下文(用户ID、租户信息等)
- runtime.state: 可变对话状态(历史消息、临时数据)
- runtime.store: 持久化存储
- runtime.stream_writer: 流式输出回调
实际 LangGraph Agent 中,ToolRuntime 由框架自动注入,无需手动构造。
ToolRuntime 是 LangChain 工具系统 的 "运行期大脑",将上下文、状态、配置与存储贯通起来,使得工具在执行时能够理解所处环境、记住相关历史并生成个性化的精准响应。
最后,对工具这个部分进行总结。在本节中,我们完整地探讨了 LangChain 中工具的运作机制:从基础的定义与参数结构化到运行时的 ToolRuntime 上下文注入,工具本身赋予了模型与外部世界交互的能力,而 ToolRuntime 则进一步为其赋予了上下文感知、状态存取与全面可观测的能力。这使得简单的函数调用得以升华为真正的智能执行,拥有了完整的 Tool 机制,智能体不再仅仅是一个理解语言的分析器,而是进化为一个能够理解复杂场景并采取精准行动的数字个体。这为我们接下来深入探讨智能体奠定了基础。
LangChain 1.0 六大核心组成模块
LangChain 1.0 重构后标准化六大基础组件,是构建 LLM 应用的完整分层体系:
1. Models(模型层)
所有大模型统一抽象接口,分三类:
- LLMs:文本补全模型(GPT-3、LLaMA、Qwen 基础生成)
- Chat Models:对话式模型(GPT-4、通义千问、Claude,支持消息结构)
- Embeddings:向量化嵌入模型(文本转向量,用于检索) 统一调用标准,切换模型无需大幅改代码。
2. Prompts(提示词层)
管理、模板化、优化输入提示:
- PromptTemplate:基础文本模板
- ChatPromptTemplate:对话多轮消息模板
- Message 体系:HumanMessage/AIMessage/SystemMessage
- 提示词缓存、提示词自动优化、Few-shot 示例管理
3. Chains(链路 / 流程层)
把多个组件串联成可复用执行流程,1.0 主推 LCEL(LangChain Expression Language)链式语法替代旧 Chain 类:
- 基础链:检索问答链、摘要链、翻译链
- 复合链:多步骤复杂任务(文档解析→向量化→检索→回答)
- 支持流式输出、并行执行、异常捕获
4. Memory(记忆层)
给对话添加上下文存储,区分短期 / 长期记忆:
- 短期:ConversationBufferMemory、窗口滑动记忆(限制历史长度防超限)
- 长期:向量存储记忆、数据库持久记忆
- 自动裁剪、消息压缩、上下文摘要,解决长对话上下文溢出问题
5. Retrieval(检索层,RAG 核心)
文档知识库检索全套工具,1.0 大幅重构检索生态:
- Document:文档封装对象(page_content + metadata)
- Document Loaders:各类文件加载(PDF/Markdown/ 网页 / 数据库)
- Text Splitters:文本分割器(递归字符、语义分割)
- Vector Stores:向量数据库存储(Chroma、FAISS、Milvus、PGVector)
- Retrievers:检索器(相似度检索、BM25、混合检索、自查询检索)
6. Agents(智能代理层)
让 LLM 具备工具调用、自主规划能力:
- Tools:外部工具封装(搜索、计算器、API、数据库查询)
- Agent 执行器:LLM 自主判断何时调用工具、多轮思考行动
- Agent 类型:ReAct、OpenAI Function Agent、结构化工具代理
- 支持 Agent 记忆、工具校验、执行日志
对比之前的:
二、0.1 ~ 0.3.x 稳定版(面试常考,6 大模块,与 1.0 命名略有差异)
0.1 架构拆分后,把检索独立成 Indexes,是 1.0 六大模块的前身,划分逻辑和 1.0 几乎对齐,仅命名、归类有区别:
- Model I/O(模型输入输出) 1.0 拆分为独立 Models + Prompts;包含 LLM/ChatModel/Embedding、PromptTemplate、OutputParser 输出解析器
- Chains(链) 传统面向类的 Chain(LLMChain、RetrievalQA、SequentialChain),无 LCEL 语法 ,靠
run()/call()执行 - Memory(记忆) 和 1.0 功能完全一致,各类对话缓存、滑动窗口记忆
- Indexes(索引 / 检索) 1.0 改名叫 Retrieval;包含文档加载器、文本分割、向量库、检索器整套 RAG 链路
- Agents(智能代理) 内置 Tools 工具集合,ReAct、OpenAI 函数代理,自主工具调用
- Callbacks(回调观测) 0.x 第六块,贯穿全链路日志、埋点、监控;1.0 不再算六大核心,降级为配套辅助能力
|-----------------------|--------------------------|---------------------------|-------------|
| 0.1~0.3.x(6 模块) | LangChain 1.0(6 模块) | 核心改动 | |
| Model I/O(模型 + 提示词合一) | Models、Prompts(拆成两个独立模块) | 解耦模型调用与提示词管理,结构更清晰 | |
| Indexes(索引) | Retrieval(检索) | 更名,重构检索抽象,统一 Retriever 接口 | |
| Chains(传统类 Chain) | Chains(LCEL 表达式为主) | 废弃大量旧式 Chain 类,管道符 ` | ` 链式语法成为标准 |
| Callbacks(六大核心之一) | 辅助工具,不属于六大模块 | 观测、日志、LangSmith 改为配套生态 | |
| Agents | Agents | 底层重构,强化 LangGraph 图编排能力 | |
| Memory | Memory | 功能基本无变动 | |
2.4 智能体与中间件(Middleware)
本节是本书的核心部分,旨在系统解析 LangChain 1.0 在智能体架构上的根本性变革与高级项目实践。在早期版本中,智能体(Agent)因其内部流程不透明与复杂,常常成为开发与调试的难点。LangChain 1.0 对此进行了彻底重构,其目标是构建一个高度模块化、行为可控且易于观测的智能体。
此次重构的核心在于引入了中间件机制,并将其深度整合为智能体执行流程。在新的架构下,智能体不再是一个难以窥探的黑箱执行器,而是演变为一套由清晰执行逻辑与一系列可插拔中间件协同构成的透明处理管道。
本节将首先阐述智能体与中间件的基本概念,以及两者在 LangChain 1.0 中的协作关系。随后,我们将深入剖析智能体的核心机制,包括其基本使用模式、经典的 ReAct 推理框架、结构化输出能力及对 MCP(Model Context Protocol,模型上下文协议)的集成与应用。在此基础上,我们将详细解读中间件的工作原理、LangChain 提供的内置中间件及如何通过基于装饰器与类的两种主流方式来自定义和扩展中间件逻辑。
通过本节的系统学习,可以全面掌握 LangChain 1.0 智能体的强大能力,并熟练运用中间件这一利器,灵活、精准地控制与扩展智能体的行为,以满足企业应用中对可靠性、安全性与可观测性的严苛要求。
2.4.1 智能体与中间件概述
1. 智能体
关于智能体的基本概念与重要性,前文已有阐述。在 LangChain 中,智能体是最具动态性和决策能力的组件,不同于链(Chain)。链执行的是预先定义好、固定的步骤序列,而智能体在 LangChain 中的定位是自主决策与问题解决者,代表了大模型应用的最高级形态,能够执行需要多步骤推理、动态规划和外部信息获取的任务。一个完整的智能体通常具备以下三个关键特性。
(1)决策核心(LLM / Prompt):作为智能体的 "大脑",通常由一个强大的模型驱动。该模型通过精心设计的 Prompt 接收任务指令、观察环境状态并感知可用工具,进而输出下一步的思考过程与行动计划(Thought / Action)。
(2)工具集(Tool):相当于智能体的 "手臂",是它与外部世界交互的能力。工具可以是检索引擎、数据库查询、代码执行器、自定义 API 等。智能体依赖工具来获取最新的、非模型内部的知识,执行计算或操作外部系统。
(3)迭代执行(Iterative Execution):智能体的 "生命周期",即持续迭代的思考(Thought)→行动(Action)→观察(Observation)过程,直到任务完成或满足预设的终止条件。
其标准执行流程可参考图 2-9。

在 LangChain 1.0 中,智能体的设计实现了更高程度的解耦,它专注于以下几个方面。
(1)输入管理:接收并解析用户请求与上下文信息。
(2)循环控制:负责启动、推进并终止决策循环。
(3)状态维护:在迭代过程中保持工具、记忆及中间步骤的一致性状态。
至于诸如步骤记录、错误处理、结果缓存等辅助性与横切面的功能,则被优雅地委托给中间件处理。这种关注点分离显著提升了智能体核心逻辑的简洁度与可测试性。
2. 中间件
中间件是 LangChain 引人注目的变化之一,是 LangChain 1.0 中引入的一个新概念,极大地增强了智能体的可定制性和可扩展性。其概念来源于 Web 开发中的中间件模式(如 Express 或 Django),即在请求(或智能体执行步骤)到达最终处理器之前或之后,执行一系列预处理或后处理操作。它的出现标志着 LangChain 从一个单纯的链式框架升级为一个更具项目化、企业级特征的执行引擎。
中间件是一段可插拔的逻辑代码 ,会在智能体执行循环的特定生命周期事件发生时被自动触发。它系统地消除了早期版本中的以下几个关键痛点。
(1)流程注入与拦截:允许开发者在智能体执行的任何关键节点(如开始前、工具调用前、输出生成后)注入自定义逻辑。例如,可以插入一个安全检查中间件,在每次调用外部工具前验证输入数据的合规性与安全性。
(2)可观测性标准化:这是中间件常用且重要的功能之一。通过标准化的日志中间件,可以轻松实现追踪、监控、调试和横切关注点处理。
① 追踪:记录从开始到结束的完整执行路径。
② 监控:收集执行时间、延迟和错误率等指标。
③ 调试:捕获每一步的输入和大模型的原始输出。
④ 横切关注点处理:将缓存、速率限制、格式校验等非核心业务逻辑从智能体主流程中剥离,避免代码重复与逻辑耦合。
在 LangChain 1.0 中,中间件主要支持以下三种实现模式,以适应不同的复杂度与复用需求。
(1)预置中间件:对常用的智能体场景进行了封装,快速集成。
(2)装饰器式中间件:轻量级、针对特定函数或步骤进行局部增强。
(3)类式中间件:重量级、结构化,用于处理需要维护状态或跨多个生命周期钩子的复杂逻辑。
简而言之,中间件的引入使得传统智能体的标准执行流程(如图 2-9 所示)演进为更强大、更可控的流程,如图 2-10 所示。

3. 智能体与中间件的关系
在 LangChain 1.0 的新架构下,智能体的执行流程实质上是由中间件驱动和控制的:智能体定义了执行任务的总体逻辑与决策机制(如 ReAct 模式),而中间件则是这些逻辑的具体承载者 ,被挂载到智能体执行的各个生命周期钩子(hook)上。
当智能体开始执行或进入每一次迭代时,它会按预设顺序调用所有已注册的中间件。这些中间件可以在关键节点(如接收输入前、选择工具时、执行工具后等)对数据进行修改、记录,甚至拦截整个流程。这种设计使得智能体核心可以专注于决策本身,而将所有辅助性、监控类及扩展性功能交由中间件处理,从而构建出高度灵活且易于维护的高级应用。
例如,当智能体启动一个复杂任务时,其协同流程如下。
(1)事件触发:智能体执行流程到达关键节点,生成相应的生命周期事件。
(2)中间件响应:所有注册到该事件钩子上的中间件按顺序依次执行。
(3)处理与传递:每个中间件均可处理输入数据、记录日志,或在特定条件下(如缓存命中)直接返回结果并中断后续流程。
(4)流程继续:在所有中间件执行完毕后,控制权交还给智能体核心,主流程继续执行。
智能体提供了系统的 "骨架" 与 "决策逻辑",而中间件则为其赋予了 "血肉" 与 "运维能力"。这一架构转变使开发者能够以更清晰、更符合软件项目范式的方式,构建出健壮、高效且易于维护的智能体应用。
2.4.2 智能体的基本使用与 ReAct 机制
在 LangChain 1.0 中,构建智能体的标准方式是使用create_agent函数。该函数在底层封装了复杂的执行逻辑,为开发者提供了一个简洁而强大的高级接口。要成功构建一个功能完整的智能体,通常需要以下三个核心要素。
(1)LLM:大模型,作为智能体的 "大脑"。
(2)Tool:外部工具集合,作为智能体的 "手臂"。
(3)create_agent函数:将大模型与工具集成为一个可执行的智能体实例。
以下示例演示了如何使用create_agent函数创建一个具备两个工具(数学计算器calculate和查询城市信息get_info)的智能体。该智能体会根据用户输入,自主判断并调用相关的工具来完成任务。
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
llm = init_chat_model(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.3,
model_provider="openai"
)
# 2. 定义工具(Tools)
# 工具一:数学计算器
@tool
def calculate(expression: str) -> str:
"""这是一个数学计算器。当需要计算数学表达式时调用此工具。
输入必须是一个有效的Python表达式字符串。"""
try:
# 简化演示,生产环境应使用更安全的计算库
return str(eval(expression))
except Exception as e:
return f"计算错误:{e}"
#result=calculate.invoke("请计算 (256 减去 88) 再乘以 5 的结果是多少?")
result=calculate.invoke("(256-88)*5")
print(result)
# 工具二:查询城市信息
@tool
def get_info(city_name: str) -> str:
"""用于查询指定城市的基本信息,例如城市别名、主要产业等。"""
city_data = {
"深圳": "别称"鹏城",是中国改革开放建立的第一个经济特区,以高新技术产业闻名。",
"上海": "别称"沪"或"申",是中国最大的城市和金融中心,拥有繁荣的港口贸易。",
"北京": "中国的首都,拥有深厚的历史文化底蕴,是政治和文化中心。"
}
return city_data.get(city_name, f"未找到关于城市 '{city_name}' 的特定信息。")
# 3. 创建智能体
agent = create_agent(
model=llm,
tools=[calculate, get_info],
system_prompt="你是一位专业的智能助理,拥有计算和信息查询的能力。请根据用户需求,自主选择最合适的工具进行处理。",
)
# 4. 执行任务一:选择工具一 (计算)
print("--- 任务一:智能体自主选择 'calculate' 工具 ---")
question_one = "请计算 (256 减去 88) 再乘以 5 的结果是多少?"
result_one = agent.invoke(
{"messages": [HumanMessage(content=question_one)]}
)
print(result_one)
print(f"用户问题: {question_one}")
print(f"智能体最终输出:\n{result_one['messages'][-1].content}")
# 5. 执行任务二:选择工具二 (查询城市信息)
print("\n--- 任务二:智能体自主选择 'get_info' 工具 ---")
question_two = "深圳的别称是什么?它以什么产业闻名?"
result_two = agent.invoke(
{"messages": [HumanMessage(content=question_two)]}
)
print(f"用户问题: {question_two}")
print(f"智能体最终输出:\n{result_two['messages'][-1].content}")
# 5. 执行任务三:测试不使用任何工具
question_three = "天为什么是蓝色的?"
result_three = agent.invoke(
{"messages": [HumanMessage(content=question_three)]}
)
print(f"用户问题: {question_three}")
print(f"智能体最终输出:\n{result_three['messages'][-1].content}")
ImportError: Unable to import langchain_deepseek. Please install with `pip install -U langchain-deepseek`
再大模型 model 中添加一句话即可:model_provider="openai"
运行结果如图 2-11 所示:
D:\BD20251101\pyworkspace\langchan_demo2\.venv\Scripts\python.exe D:\BD20251101\pyworkspace\langchan_demo2\day02\Demo01.py
840
--- 任务一:智能体自主选择 'calculate' 工具 ---
{'messages': [HumanMessage(content='请计算 (256 减去 88) 再乘以 5 的结果是多少?', additional_kwargs={}, response_metadata={}, id='b24629c5-554a-46ba-93c3-baac1d055b72'), AIMessage(content='好的,我来计算这个表达式。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 56, 'prompt_tokens': 388, 'total_tokens': 444, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 4}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '823a49ee-5526-45a9-af89-87b9e3657a64', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019f1c66-415e-7a93-bf49-e741b0da71e7-0', tool_calls=[{'name': 'calculate', 'args': {'expression': '(256 - 88) * 5'}, 'id': 'call_00_XsuRStQdVza2xR1ROWM20050', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 388, 'output_tokens': 56, 'total_tokens': 444, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}}), ToolMessage(content='840', name='calculate', id='ea7077ed-b8f4-4102-83bd-1d619a323939', tool_call_id='call_00_XsuRStQdVza2xR1ROWM20050'), AIMessage(content='计算结果是:**(256 - 88) × 5 = 840**。\n\n计算步骤:\n1. 先算括号内:256 - 88 = **168**\n2. 再乘以 5:168 × 5 = **840**', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 52, 'prompt_tokens': 457, 'total_tokens': 509, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 384}, 'prompt_cache_hit_tokens': 384, 'prompt_cache_miss_tokens': 73}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': '79164ea7-38a6-4aad-985c-5f71e91576cb', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f1c66-463f-7803-ad04-a5a42cdc9e0e-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 457, 'output_tokens': 52, 'total_tokens': 509, 'input_token_details': {'cache_read': 384}, 'output_token_details': {}})]}
用户问题: 请计算 (256 减去 88) 再乘以 5 的结果是多少?
智能体最终输出:
计算结果是:**(256 - 88) × 5 = 840**。
计算步骤:
1. 先算括号内:256 - 88 = **168**
2. 再乘以 5:168 × 5 = **840**
--- 任务二:智能体自主选择 'get_info' 工具 ---
用户问题: 深圳的别称是什么?它以什么产业闻名?
智能体最终输出:
深圳的别称是 **鹏城**,它主要以 **高新技术产业** 闻名,是中国改革开放后建立的第一个经济特区,拥有众多知名科技企业,因此也被称为中国的"硅谷"。
用户问题: 天为什么是蓝色的?
智能体最终输出:
这是一个关于大气光学的问题,我来为你解释一下。
天空呈现蓝色的现象叫做**瑞利散射**,具体原理如下:
1. **太阳光是复合光**:太阳发出的光包含各种颜色的光(红、橙、黄、绿、蓝、靛、紫等)。
2. **大气分子散射**:当阳光穿过地球大气层时,会与大气中的气体分子(主要是氮气和氧气)发生相互作用,产生散射。
3. **瑞利散射规律**:根据瑞利散射定律,散射强度与光波长的**四次方成反比**。也就是说:
- **波长越短的光**(如蓝光、紫光)→ 散射越强
- **波长越长的光**(如红光、橙光)→ 散射越弱
4. **为什么是蓝色而不是紫色?**
- 虽然紫光波长更短,散射更强,但有两个原因:
- 太阳光谱中紫光的强度本身就比蓝光弱
- 人眼对蓝光比紫光更敏感
- 所以最终我们看到的是**蓝色天空**
简单来说:**蓝光被大气散射得最多,从四面八方进入我们的眼睛,所以天空看起来是蓝色的。** 🌤️
而日出日落时天空呈红色,是因为阳光斜穿大气层路径更长,蓝光被散射殆尽,只剩下红光到达我们眼中。
Process finished with exit code 0
上述示例展示了智能体的基础工具调用能力。然而,智能体真正的威力在于完成需要多步推理的复杂任务,这其中的核心机制便是 ReAct。 ReAct 是 Reasoning+Acting 的合成词,是一种要求大模型在每一步都进行显式思考
ReAct = Reason(思考) + Act(行动),让大模型不光只会空想,想不清楚就动手查工具、做操作,再拿着新信息重新思考,循环直到能给出答案。
举例:
你问 AI:"2026 年河南高考一本线是多少?"
- 纯普通大模型做法:凭训练时记的旧数据瞎猜,很容易答错,没有求证环节;
- ReAct 做法(思考 + 行动循环)
- 1)思考(Reason):我记不清今年分数线,必须调用搜索工具查最新数据;
- 2)行动(Act):调用搜索工具,检索 2026 河南高考分数线;
- 3)观察(Observe):拿到搜索返回的官方分数线;
- 4)再思考:现在有准确数据了,可以整理回答用户;
如果工具返回信息不全,会重复「思考→调用工具→观察」,直到信息足够
from typing import List
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
llm = init_chat_model(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.3,
model_provider="openai"
)
# 2. 定义工具(Tools)
# 工具一:数学计算器
@tool
def calculate(expression: str) -> str:
"""这是一个数学计算器。当需要计算数学表达式时调用此工具。
输入必须是一个有效的Python表达式字符串。"""
try:
# 简化演示,生产环境应使用更安全的计算库
return str(eval(expression))
except Exception as e:
return f"计算错误:{e}"
# 工具二:查询汇率工具
@tool
def get_exchange_rate(from_currency: str, to_currency: str) -> float:
"""用于查询两种货币之间的实时汇率。返回 'from_currency' 兑换 'to_currency' 的比率。"""
# 模拟实时汇率数据
if from_currency == "USD" and to_currency == "CNY":
return 6
if from_currency == "EUR" and to_currency == "USD":
return 1.08
# 如果找不到汇率,则返回一个默认值
return 1.0
tools:List[tool] = [calculate, get_exchange_rate]
# 3. 创建智能体
agent = create_agent(
model=llm,
tools=tools,
system_prompt="你是一个专业的金融人才,具备专业的金融知识,思考问题的时候,必须使用我给定的工具",
)
question_two = "1500美元兑换多少人民币"
result_two = agent.invoke(
{"messages": [HumanMessage(content=question_two)]}
)
print(f"用户问题: {question_two}")
print(f"智能体最终输出:\n{result_two['messages'][-1].content}")
下面逐步解析智能体在此次过程中的 ReAct 循环。
(1)思考(Thought 1):智能体进行推理,认识到完成任务需要两个步骤。首先,查询美元(USD)兑换人民币(CNY)的汇率。然后,用 1500 乘以这个汇率。
(2)行动(Action 1):根据思考结果,智能体调用get_exchange_rate工具,输入参数为from_currency='USD',to_currency='CNY'。
(3)观测(Observation 1):工具返回实时汇率 7.21,此结果作为新的观察信息反馈给智能体。
(4)思考(Thought 2):智能体基于获取的汇率,进行下一步推理,明确需要执行1500 × 7.21这步计算。
(5)行动(Action 2):智能体调用 calculate 工具,输入表达式 "1500 * 7.21"。
(6)观测(Observation 2):工具返回计算结果 10815.0。
(7)最终答案(Final Answer):智能体整合所有步骤的信息,给出最终答案 "1500 美元可以兑换 10815.00 元人民币。"
运行结果如图 2-12 所示:
D:\BD20251101\pyworkspace\langchan_demo2\.venv\Scripts\python.exe D:\BD20251101\pyworkspace\langchan_demo2\day02\Demo02.py
用户问题: 1500美元兑换多少人民币
智能体最终输出:
**1500美元可兑换 9,000 元人民币。**
📌 **汇率信息:**
- 当前汇率:1 美元 = 6 人民币
- 兑换金额:1500 × 6 = **9,000 元人民币**
⚠️ **温馨提示:**
- 以上汇率仅供参考,实际兑换时以银行或兑换机构的**实时牌价**为准。
- 银行柜台或兑换平台可能会收取一定的手续费,实际到手金额可能略有差异。
- 汇率会随市场波动而变化,建议兑换前再次确认最新汇率。
Process finished with exit code 0
从这个例子中可以看到,智能体能够自主地将一个复杂的任务拆解为多个执行步骤,并通过 ReAct 机制分别调用查询汇率(get_exchange_rate)工具和数学计算器(calculator)工具来完成相应的操作,最终整合信息给出答案。这种动态规划与自主执行的能力,是智能体区别于简单、固定流程 Chain 的核心特征。
2.4.3 结构化输出
在 LangChain 1.0 中,智能体的结构化输出 (Structured Output)功能得到了显著增强和统一。开发者不再需要依赖复杂的提示词项目或独立的输出解析节点,而是可以通过 create_agent 函数的 response_format 参数 ,以声明式的方式直接控制智能体的输出格式。
在 create_agent 函数中,response_format 参数决定了智能体如何生成结构化数据。LangChain 1.0 中提供了以下两种策略。
(1)ProviderStrategy:优先使用模型提供商(如 OpenAI、Anthropic)原生的结构化输出能力。这是最可靠、效率最高的策略。
(2)ToolStrategy:对于不支持原生结构化输出的模型,LangChain 会自动创建一个
pip install langchain-deepseek
import json
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage
from langchain.tools import tool
from pydantic import BaseModel, Field
from langchain.agents.structured_output import ToolStrategy
# 1. 定义所需的输出结构 (Pydantic模型)
class UserProfile(BaseModel):
"""用于存储用户信息的结构。"""
username: str = Field(description="用户的唯一用户名。")
age: int = Field(description="用户的年龄,必须是整数。")
is_active: bool = Field(description="用户的账户当前是否处于活跃状态。")
# 2. 辅助工具(保持一致性)
@tool
def dummy_tool(query: str) -> str:
"""一个占位符工具,确保智能体有可用的工具列表。"""
return "Tool is available."
#tools = [dummy_tool]
input_text = "请为我创建一个档案:用户名是 'Jane_D', 她今年 32 岁,账户当前是活跃状态。"
# --- 智能体: ToolStrategy (通用且兼容性强) ---
# 适用场景:当模型不支持原生JSON API,或需要强制通过 Tool Calling 机制输出结构时。
print(">>> 智能体: ToolStrategy")
llm_tool = init_chat_model(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.3,
model_provider="openai"
)
agent_tool = create_agent(
model=llm_tool,
tools=[],
# 核心:使用 ToolStrategy 封装模型
response_format=ToolStrategy(UserProfile),
system_prompt="你是一位通用信息提取助理,请提取信息。",
)
# 执行和结果
result_tool = agent_tool.invoke(
{"messages": [HumanMessage(content=input_text)]}
)
structured_data = result_tool.get("structured_response")
print(f"类型: {type(structured_data).__name__}")
# {'username': 'Jane_D', 'age': 32, 'is_active': True}
print(structured_data.model_dump())
print(json.dumps(structured_data.model_dump(), indent=2, ensure_ascii=False))
result_tool.get("structured_response"):从返回结果中取出解析完成的 UserProfile 对象(已经自动完成类型校验);
structured_data.model_dump():Pydantic 内置方法,将模型对象转为标准 Python 字典;
json.dumps:格式化打印 JSON 字符串,ensure_ascii=False 保证中文正常显示
indent=2:每一层 JSON 嵌套,前面空2 个空格作为缩进
-
不加 indent(默认压缩一行)
{"username":"Jane_D","age":32,"is_active":true}
所有内容挤在一行,很难阅读。
-
indent=2(你代码里的效果)
{
"username": "Jane_D",
"age": 32,
"is_active": true
}
在上述代码中,UserProfile 类继承 BaseModel,充当一份数据契约,明确定义了输出必须包含的字段 (username, age, is_active)及其严格的数据类型。
当智能体执行任务时,ToolStrategy 便启动了结构化约束机制。它将这个 UserProfile 转化为一个动态创建的工具,并强制 DeepSeek 必须以调用该工具的形式来响应。这意味着模型返回的不再是自然语言文本,而是一个包含参数的严格 JSON 结构,其中参数正是用户档案的数据。
运行结果图 2-13:
>>> 智能体: ToolStrategy
类型: UserProfile
{
"username": "Jane_D",
"age": 32,
"is_active": true
}
相比之下,ProviderStrategy 策略的使用方式更直接,由于其原理简单,因此此处不单独举例。 最后,我们对这两种策略进行总结与对比。
(1)ProviderStrategy:最推荐的策略,性能和准确性最高,但依赖模型支持。
(2)ToolStrategy:兼容性最高的策略,通过将 Pydantic 模型转换为一个强制调用的工具,确保了输出结构的可靠性。
通过这两种策略,LangChain 1.0 确保了在各种模型环境下,开发者都能以统一、便捷的方式获得高质量的结构化数据。
- ProviderStrategy 原理 :调用模型厂商API 原生结构化输出(OpenAI/Gemini/Claude/Grok 支持),DeepSeek 官方接口暂未原生支持该策略,强行使用会报错;
- 和 ToolStrategy 区别
- ToolStrategy:通用兼容,靠虚拟工具曲线实现结构化,所有支持工具调用的模型都能用;
- ProviderStrategy:原生 JSON 强制输出,性能更好、校验更强,但仅限指定厂商模型;
- 新增导入:
from langchain.agents.structured_output import ProviderStrategy
2.4.4 MCP 的使用
MCP 用于标准化模型与外部工具、数据源和上下文环境之间的交互方式。它的目标是让不同系统之间共享统一的模型上下文接口,使智能体能在不同的运行环境中调用工具时,无须关心底层传输细节 。MCP 就像智能体世界的 "USB 协议",无论工具在本地还是异地,使用 Python、NodeJS 或 HTTP,都可以通过统一接口被智能体调用。
MCP(Model Context Protocol,模型上下文协议)诞生时间线
官方正式发布 / 开源日期(公认诞生节点)
2024 年 11 月 25 日
javaweb 中有很多的接口,这些接口可能是自己写的,也可能是互联网上的接口,比如快递,天气预报等,接口的相互调用使项目更加的丰富,功能更加强大,在智能体的世界中,其实也有这样的需求,我们管它叫做 MCP
MCP 本质上是一个客户端 - 服务端协议(Client-Server 协议)。其中,MCP 服务端负责定义并向网络暴露可用的工具,而运行在智能体端的 MCP 客户端负责发现、加载并调用这些远程工具。
LangChain 官方提供了 langchain-mcp-adapters 适配库来支持 MCP。
下面通过一个集成天气 API 的完整示例来演示如何在 LangChain 智能体中使用 MCP 工具。
1. 安装依赖
官方推荐使用 mcp 库来快速构建 MCP 服务端。首先安装依赖:
pip install mcp
2. 创建数学工具 MCP 服务端
接下来,创建一个数学工具 MCP 服务端 math_mcp_server.py:
from mcp.server.fastmcp import FastMCP
# 创建一个名为 "Math" 的 MCP 服务
mcp = FastMCP("Math")
# 定义第一个工具:加法
# 使用 @mcp.tool() 装饰器即可将函数注册为 MCP 工具。
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# 定义第二个工具:乘法
# 使用 @mcp.tool() 装饰器即可将函数注册为 MCP 工具。
@mcp.tool()
def multiply(a: int, b: int) -> int:
"""Multiply two numbers"""
return a * b
# 运行 MCP 服务,使用 stdio 传输协议
# transport="stdio" 表示通过标准输入输出进行通信。
if __name__ == "__main__":
mcp.run(transport="stdio")
3. 创建 HTTP 模式的天气 MCP 服务端 weather_mcp_server.py
基于同样的方式,创建一个天气 MCP 服务端,此服务端将调用第三方免费的天气 API,并采用 streamable-http 模式启动,从而通过网络提供服务:
from mcp.server.fastmcp import FastMCP
import httpx
from typing import Dict, Any, Optional
mcp = FastMCP("Weather")
# ---------------------------------------------------------------------
# 定义 Open-Meteo 公共 API 地址(无需 API Key)
# 1. WEATHER_URL ------ 根据经纬度查询当前天气
# 2. GEOCODE_URL ------ 根据城市名查询经纬度
# ---------------------------------------------------------------------
OPEN_METEO_WEATHER_URL = "https://api.open-meteo.com/v1/forecast"
OPEN_METEO_GEOCODE_URL = "https://geocoding-api.open-meteo.com/v1/search"
# 工具一:geocode_city()
# 将城市名解析为经纬度信息
@mcp.tool()
def geocode_city(name: str, country: Optional[str] = None, language: str = "zh") -> Dict[str, Any]:
"""将城市名解析为经纬度"""
params = {"name": name, "count": 1, "language": language, "format": "json"}
if country:
params["country"] = country
with httpx.Client(timeout=10) as client:
r = client.get(OPEN_METEO_GEOCODE_URL, params=params)
r.raise_for_status()
data = r.json()
results = data.get("results") or []
if not results:
return {"error": f"未找到城市:{name}"}
top = results[0]
return {
"name": top.get("name"),
"lat": top.get("latitude"),
"lon": top.get("longitude"),
"country": top.get("country"),
}
# 工具二:get_current_weather()
# 根据经纬度查询当前天气 latitude /ˈlætɪtjuːd/ 纬度 longitude 经度
@mcp.tool()
def get_current_weather(lat: float, lon: float) -> Dict[str, Any]:
"""根据经纬度查询当前天气"""
params = {"latitude": lat, "longitude": lon, "current_weather": True}
with httpx.Client(timeout=10) as client:
r = client.get(OPEN_METEO_WEATHER_URL, params=params)
r.raise_for_status()
payload = r.json()
cw = payload.get("current_weather") or {}
return {
"latitude": lat,
"longitude": lon,
"temperature": cw.get("temperature"),
"windspeed": cw.get("windspeed"),
"weathercode": cw.get("weathercode"),
"time": cw.get("time"),
}
# 工具三:get_current_weather_by_city()
# 组合前两个工具,实现「城市名 → 当前天气」的完整流程
@mcp.tool()
def get_current_weather_by_city(name: str, country: Optional[str] = None, language: str = "zh") -> Dict[str, Any]:
"""城市名 -> 当前天气(内部先地理编码再查询天气)"""
g = geocode_city(name=name, country=country, language=language)
if "error" in g:
return g
w = get_current_weather(lat=g["lat"], lon=g["lon"])
return {**g, **w}
# transport="streamable-http" 表示使用 HTTP 协议暴露服务
# 端点默认路径是 /mcp,端口通常为 8000
if __name__ == "__main__":
print("Starting Weather MCP Server (streamable-http) on http://localhost:8000/mcp ...")
mcp.run(transport="streamable-http")
g = {"name":"张三", "age":20}
def func(name, age):
print(name, age)
# 等价写法1:手动传关键字
func(name="张三", age=20)
# 等价写法2:**g自动拆解字典
func(**g)
创建完成后,通过以下命令启动该服务端:
python weather_mcp_server.py
4. 创建 MCP 客户端并集成智能体
在编写 MCP 客户端代码之前,需要安装官方的 MCP 适配器依赖
pip install langchain-mcp-adapters
随后,创建 MCP 客户端代码,该客户端将同时连接上述两个 MCP 服务端,并将获取到的远程工具注册到智能体中:
import asyncio
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain_mcp_adapters.client import MultiServerMCPClient
llm_tool = init_chat_model(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.3,
model_provider="openai"
)
# 2. 连接 MCP 服务端 (Math + Weather)
client = MultiServerMCPClient({
"Math": {
"transport": "stdio",
"command": "python",
"args": ["./math_mcp_server.py"], # 确保路径正确
},
"Weather": {
"transport": "streamable_http",
"url": "http://localhost:8000/mcp", # Weather MCP Server
},
})
def arun(coro):
"""同步封装: 把异步协程在顶层跑完,主逻辑仍然是「同步写法」"""
return asyncio.run(coro)
# 3. 获取 MCP 工具 (注意: 此方法在本地是 async)
tools = arun(client.get_tools())
print("✅ 已加载的工具: ", [t.name for t in tools])
# 4. 创建智能体 (基于 MCP 工具 + DeepSeek)
agent = create_agent(
model=llm_tool,
tools=tools,
system_prompt=(
"你是一个助理。涉及数学计算,使用 Math 工具(add / multiply);"
"涉及天气,使用 Weather 工具(geocode_city / get_current_weather / get_current_weather_by_city)。"
)
)
# 5. 调用:数学示例
math_question = "请帮我计算 (3 + 5) × 12 的结果"
print("\n▶ 数学任务: ", math_question)
math_result = arun(agent.ainvoke({"messages":
[HumanMessage(content=math_question)]}))
print("✔ 智能体输出: ", math_result["messages"][-1].content)
# 6. 调用:天气示例
weather_question = "请告诉我北京现在的天气情况"
print("\n▶ 天气任务: ", weather_question)
weather_result = arun(agent.ainvoke({"messages":
[HumanMessage(content=weather_question)]}))
print("✔ 智能体输出: ", weather_result["messages"][-1].content)
agent.invoke 本身是一种同步调用的方法,会阻塞线程。
def arun(coro):
"""同步封装: 把异步协程在顶层跑完,主逻辑仍然是「同步写法」"""
return asyncio.run(coro)
# 封装一层简易工具函数 arun(),用同步函数的形式,执行一段异步协程代码。
# coro 必须是协程对象,也就是调用 async def 函数得到的结果
import asyncio
def arun(coro):
"""同步封装: 把异步协程在顶层跑完,主逻辑仍然是「同步写法」"""
return asyncio.run(coro)
async def demo():
await asyncio.sleep(0.5)
print("异步执行完成")
return 666
# 主线程同步调用,不用 async/await
result = arun(demo())
print(result) # 输出 666
运行输出(图 2-14):
✅ 已加载的工具: ['add', 'multiply', 'geocode_city', 'get_current_weather', 'get_current_weather_by_city']
🔢 数学任务: 请帮我计算 (3 + 5) × 12 的结果
✅ 智能体输出: **(3 + 5) × 12 = 96** ✅
计算过程:
1. 先算括号内:3 + 5 = 8
2. 再乘以 12:8 × 12 = 96
最终结果为 **96**。
🌤 天气任务: 请告诉我北京现在的天气情况
✅ 智能体输出: 以下是北京现在的天气情况:
- **城市**:北京(中国)
- **当前温度**:🌡️ **27.6°C**
- **风速**:💨 **11.0 km/h**
- **天气状况**:天气代码为 3(多云/阴天)
- **时间**:2026年6月22日 15:00
总体来说,北京现在气温约 **27.6°C**,体感比较温暖,风速适中,是多云或阴天的天气。
5. 整体架构与执行流程梳理
上述示例构建了一个完整的 MCP 集成环境,其架构与数据流如下。
1)math_mcp_server.py(Stdio 传输)
(1)基于 FastMCP,暴露 add、multiply 两个工具。
(2)以transport="stdio"启动,被客户端作为本地子进程拉起并通信。
2)weather_mcp_server.py(Http 传输)
(1)基于 FastMCP,封装 Open-Meteo 公共 API,暴露 geocode_city、get_current_weather、get_current_weather_by_city 三个工具。
(2)以 transport="streamable-http" 启动,暴露标准 MCP HTTP 端点(默认位于 /mcp)。
3)client_mcp-demo.py(客户端与智能体)
(1)使用 MultiServerMCPClient 同时连接上述两个 MCP 服务端,其中 Math 工具使用 stdio 传输协议启动,Weather 工具使用 streamable_http 传输协议(URL 为 http://localhost:8000/mcp)。
(2)通过 client.get_tools 方法拉取工具定义,交给 create_agent 函数注册。
(3)使用 ChatOpenAI (deepseek-chat) 与 HumanMessage 实现与智能体的交互。
(4)智能体运行时采用 ReAct 模式,根据用户问题自动选择工具并调用,生成最终回答。
2.4.5 智能体中的预置中间件
从本节开始,我们将系统地学习 LangChain 的中间件。
为了提升智能体在生产环境中的项目化能力,LangChain 提供了一系列开箱即用的预置中间件(Prebuilt Middleware)。
这些中间件从可靠性、可观测性、成本控制与安全治理等多个维度,为智能体系统提供了关键保障。LangChain 官方提供的核心预置中间件如下:
- Summarization:在上下文长度接近上限时触发,压缩早期历史并保留最近消息,有效控制上下文长度与 API 成本。
- Human-in-the-loop(HITL):对敏感或高风险的工具调用进行人工审批,支持批准、修改或拒绝执行,必要时中断流程等待人工决策。
- Anthropic prompt caching:对重复的 Prompt 段落进行缓存,避免重复计算,减少 Token 消耗。
- Model call limit:限制模型调用次数与频率,防止成本失控或陷入无限循环。
- Tool call limit:限制工具调用次数与频率,保护外部服务,保障系统稳定性与成本可控。
- Model fallback:在主模型调用失败或输出质量不达标时,自动切换至备用模型,提升系统整体鲁棒性。
- PII detection:在输入模型前或输出给用户前,自动检测并脱敏个人可识别信息,满足数据合规要求。
- To-do list:将复杂的多步任务拆解为结构化的待办事项清单,指导后续的工具调用与执行顺序。
- LLM tool selector:在调用主模型进行完整推理前,使用一个轻量级大模型预先智能筛选相关工具,提升决策效率。
- Tool retry:为工具调用提供可配置的重试与退避策略,增强对临时性失败的容错能力。
- LLM tool emulator:在缺乏真实工具环境或进行离线测试时,使用大模型模
- Context editing:通过修剪、总结或清除历史工具调用记录等方式,主动管理对话上下文。
注意:LangChain 预置的中间件仍在持续更新中,不同版本间可能在命名、参数或功能上有部分调整,请以官方文档为准。
下面通过两个典型示例来具体演示中间件的配置与使用。
1. Summarization(对话摘要)
用途:自动对对话历史进行压缩,避免超出模型的上下文窗口限制,同时尽力维持对话的连贯性。
关键机制:在模型调用前,检查上下文 Token 数量,若超过预设阈值则触发摘要进程,将早期消息压缩为简洁的摘要(以前聊天的大概核心内容) ,并可配置保留最近 N 条原始消息以维持细节。
参考示例代码:
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langchain.messages import HumanMessage
# 主对话模型
llm = ChatOpenAI(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.0,
max_tokens=20, # 限制每次生成不超过 20 tokens
)
# 摘要模型(这里也使用deepseek)
summ_llm = ChatOpenAI(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.0,
max_tokens=20, # 限制每次生成不超过 20 tokens
model_provider="openai"
)
# 会话摘要中间件:极低阈值,快速触发;摘要后仅保留 1 条原文
middleware = SummarizationMiddleware(
model=summ_llm,
max_tokens_before_summary=1000, # 达到1000token触发摘要
messages_to_keep=2, # 保留最近2条消息
summary_prompt="用20个字以内概括要点。", # 自定义"如何摘要"的提示词(不设则用默认)
)
agent = create_agent(
model=llm,
tools=[],
system_prompt="只用一句极短中文回答,且不超过30个字。", # 只用"极简短句"回答
middleware=[middleware],
)
conversation = [
"RAG是什么?",
"有何用途?",
"主要缺点?",
"一句话总结"
]
state = {"messages": []}
for i, question in enumerate(conversation, 1):
# 用户问题
user_msg = HumanMessage(content=question)
# 智能体响应
state = agent.invoke({"messages": state["messages"] + [user_msg]})
answer = state["messages"][-1].content
print(f"\n🧩 第 {i} 轮")
print(f"Q: {question}")
print(f"A: {answer}")
print("\n✅ 对话结束。")
SummarizationMiddleware 关键参数说明
- max_tokens_before_summary:触发自动摘要的 Token 阈值。对话累计 Token 临近 / 超过该值,中间件把早期消息压缩为摘要,控制上下文长度与调用成本。
- messages_to_keep:摘要完成后,保留最近 N 条原始消息不压缩,防止全量摘要丢失细节。
- summary_prompt (可选):自定义摘要提示词,控制摘要风格、粒度(精简 / 保留关键数据);不配置则使用框架默认摘要 Prompt。
运行结果
% python summarization_middleware.py
🧩 第 1 轮
Q: RAG是什么?
A: RAG是检索增强生成技术。
🧩 第 2 轮
Q: 有何用途?
A: 提升回答准确性和知识广度。
🧩 第 3 轮
Q: 主要缺点?
A: 依赖检索质量,可能增加延迟。
🧩 第 4 轮
Q: 一句话总结
A: RAG通过检索增强生成,提升准确但依赖检索质量。
✅ 对话结束。
结果原理说明
第 4 轮回答看似重复第 3 轮,非模型错误 : 示例阈值30 Token配置偏低,第 3 轮对话结束后上下文总量触达阈值,第 4 轮模型调用前中间件执行摘要:早期历史被压缩、丢失 RAG 定义 / 用途 / 缺点细节,上下文只剩摘要 + 就近留存消息;模型仅依托精简后的上下文推理,因此沿用最近内容作答。
2. Human-in-the-loop(HITL,人工协作)
用途 :对标记为高风险的工具调用实施人工审批流程。支持批准、编辑参数后执行或拒绝操作,并在需要时中断智能体执行流程,等待人工输入。
关键机制:在工具调用前的生命周期钩子中进行策略检查,若命中规则则暂停执行并抛出中断信号,等待外部系统或用户做出决策。
from langchain.chat_models import init_chat_model
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.tools import tool
from langchain.messages import HumanMessage
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
# 1) 主模型
llm = init_chat_model(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.0,
max_tokens=20,
model_provider="openai"
)
# 2) 高风险工具(示例:数据库写操作)
@tool
def dangerous_write(sql: str) -> str:
"""对数据库执行写操作(插入/更新/删除)。当请求涉及数据库写操作或出现以'SQL:'开头的指令时,必须调用本工具。"""
return f"[模拟执行] {sql}"
# 3) HITL 中间件:拦截指定工具
hitl = HumanInTheLoopMiddleware(
interrupt_on={"dangerous_write": {"allowed_decisions": ["approve", "edit", "reject"]}}
)
# 4) 创建 Agent(强提示:遇到 SQL 必须用 dangerous_write)
agent = create_agent(
model=llm,
tools=[dangerous_write],
middleware=[hitl],
system_prompt=(
"只用一句极短中文回答(≤20字)。"
"凡是涉及数据库写操作,或消息以"SQL:"开头时,必须调用工具 dangerous_write,不得直接回答。"
),
checkpointer=InMemorySaver(), # HITL 必须
)
CFG = {"configurable": {"thread_id": "hitl-demo-interactive"}}
# 5) 四轮对话:第2轮与第4轮都触发 HITL;让你交互输入决定
conversation = [
"你是谁?", # 安全问答
"SQL: INSERT INTO logs(content) VALUES ('hello');", # 触发 HITL(预计输入 approve)
"继续。", # 安全问答
"SQL: DELETE FROM orders WHERE created_at >= date('now','-7 days');", # 触发 HITL(预计输入 reject)
]
state = {"messages": []}
def handle_interrupt(result) -> dict:
"""处理 HITL 中断:从命令行读取决策,并用 Command(resume=...) 恢复。"""
interrupt = result.get("__interrupt__")
if not interrupt:
return result
print("\n⚠️ 检测到人工在环中断:")
print(interrupt) # 打印被拦截的工具与参数
decision = input("请输入决策 (approve/edit/reject):").strip().lower()
if decision == "approve":
# 直接放行
return agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=CFG)
elif decision == "edit":
# 允许修改参数(例如 SQL),再继续
new_sql = input("请输入修改后的 SQL:").strip()
return agent.invoke(
Command(
resume={
"decisions": [{
"type": "edit",
"edited_action": {
"name": "dangerous_write",
"args": {"sql": new_sql}
}
}]
}
),
config=CFG
)
elif decision == "reject":
# 拒绝执行,并给出替代返回(不执行工具)
return agent.invoke(
Command(
resume={
"decisions": [{
"type": "reject",
"override": {"content": "[操作已被人工拒绝]"}
}]
}
),
config=CFG
)
else:
print("输入无效,按拒绝处理。")
return agent.invoke(
Command(
resume={
"decisions": [{
"type": "reject",
"override": {"content": "[操作已被人工拒绝]"}
}]
}
),
config=CFG
)
for i, q in enumerate(conversation, 1):
user_msg = HumanMessage(content=q)
result = agent.invoke({"messages": state["messages"] + [user_msg]}, config=CFG)
# 命中中断:让你做决策 → 再 resume
if result.get("__interrupt__"):
result = handle_interrupt(result)
state = result
ans = state["messages"][-1].content
print(f"\n🧩 第 {i} 轮")
print(f"Q: {q}")
print(f"A: {ans}")
print("\n✅ 对话结束。")
上述代码展示了LangChain 1.0中Human-in-the-Loop中间件的完整工作流程。
(1)核心机制:当智能体尝试调用需审批的工具(如 dangerous_write)时,该中间件会自动中断执行流程,等待人工确认。
(2)状态保存:通过 InMemorySaver 保存执行状态,确保后续可以通过相同的thread_id从断点恢复执行。
(3)人工决策:通过命令行输入模拟用户的三种决策。
① approve→批准并继续执行工具调用。
② edit→修改工具调用的输入参数后继续执行。
③ reject→拒绝执行该工具,并向智能体返回一个替代性的提示信息。
如图2-17所示,在第2轮输入SQL语句时触发了HITL机制,用户在命令行输入"approve"后流程继续;在第4轮再次触发时,用户输入"reject",智能体便不会执行写入操作,而是直接返回"操作被用户拒绝执行"的最终结果。
总结:LangChain 1.0提供的预置中间件生态丰富而强大,覆盖了日志追踪、对话管理、安全审查、人工干预等关键生产需求。它们在智能体运行时扮演着不同的角色:一部分专注于性能优化与资源管理,另一部分则侧重于安全、合规与流程控制。本节仅通过对话摘要与人工协作两个典型案例展示了预置中间件的使用方法,其他中间件的使用方法类似,你可自行探索。
2.4.6 基于装饰器的中间件
在熟悉了LangChain 1.0的预置中间件后,我们将开始探索如何创建自定义中间件。自定义中间件通过在智能体执行流程的特定钩子(hook)上注册逻辑来实现,主要提供以下两种构建方式。
(1)基于装饰器(Decorator-based):适用于逻辑简单、只需在一个钩子上注入功能的场景。
(2)基于类(Class-based):适用于功能复杂、需要在多个钩子间维护状态或协调逻辑的场景。
本节将重点介绍第一种方式,即基于装饰器的中间件实现。
在装饰器模式下,LangChain 官方将可用的钩子分为以下三类,以适应不同的拦截与处理需求。
(1)节点式(Node-style)钩子
在特定的执行点运行,按时间顺序触发(before→after),不会改变函数调用结构。具体包括:
① @before_agent:智能体执行前触发,适用于初始化、上下文检查、权限验证等场景。
② @before_model:每次模型调用前触发,适用于可修改输入、记录日志或执行过滤等场景。
③ @after_model:每次模型响应后触发,适用于分析输出、检测输出风险词、执行结果修正等场景。
④ @after_agent:智能体执行完成后触发,适用于收尾工作,如日志上报、状态持久化等场景。
(2)包裹式(Wrap-style)钩子
用于包裹实际执行过程,在调用前后插入逻辑,可直接控制函数执行(甚至中断或替换结果),适用于需要精确掌控执行流程的场景,具体包括:
① @wrap_model_call:每次模型调用时触发,可在模型调用外层实现计时、限流、重试或异常兜底等能力。
② @wrap_tool_call:每次工具调用时触发,常用于审计、权限校验、记录调用轨迹等。
(3)便捷装饰器(Convenience decorator)
常用于根据上下文动态调整系统提示,如个性化用户身份、语言、风格等,等价于在模型调用阶段包裹一次请求改写逻辑,可使用 @dynamic_prompt 进行实现。
@dynamic_prompt :系统 Prompt 动态生成器,相当于对 @wrap_model_call 的简化封装,用于在模型调用前动态修改 Prompt。
from typing import Any, Callable
import time
import math
from langchain.agents import create_agent
from langchain.agents.middleware import (
before_agent,
after_agent,
before_model,
after_model,
wrap_model_call,
dynamic_prompt,
AgentState,
ModelRequest,
ModelResponse,
)
from langchain.messages import AIMessage, HumanMessage
from langgraph.runtime import Runtime
from langchain_openai import ChatOpenAI
# ========= 便捷装饰器:动态系统提示 =========
@dynamic_prompt
def personalized_prompt(req: ModelRequest) -> str:
runtime = getattr(req, "runtime", None)
user_id = "访客"
if runtime and getattr(runtime, "context", None):
user_id = runtime.context.get("user_id", "访客")
return f"你是一名贴心的中文助手,正在为用户「{user_id}」提供帮助。回答时要简洁、自然。"
# ========= 节点式:Agent 级别前置/后置(一次调用各触发一次) =========
@before_agent
def log_before_agent(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[before_agent] 本次会话开始,已有消息数:{len(state.get('messages', []))}")
return None
@after_agent
def log_after_agent(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[after_agent] 会话结束,最终消息数:{len(state.get('messages', []))}")
return None
# ========= 节点式:模型调用前/后(每次模型调用都会触发) =========
@before_model
def log_before_model(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[before_model] 准备进行模型调用,当前消息数:{len(state['messages'])}")
return None
@after_model(can_jump_to=["end"])
def validate_output(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""简单的输出校验:若模型输出包含"BLOCKED_CN",则改写消息并跳转到 end。"""
last = state["messages"][-1]
if isinstance(last, AIMessage) and "BLOCKED_CN" in (last.content or ""):
print("[after_model] 触发安全规则:检测到 BLOCKED_CN,跳转到 end")
return {
"messages": [AIMessage("该请求触发了安全校验,无法继续。")],
"jump_to": "end",
}
return None
# ========= 包裹式:为模型调用加 重试与耗时统计 =========
@wrap_model_call
def retry_and_timing(
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
max_retries = 2
start = time.time()
try:
for i in range(max_retries + 1):
try:
return handler(request)
except Exception as e:
if i == max_retries:
raise
# 退避等待(100ms, 200ms)
backoff = 0.1 * math.pow(2, i)
print(f"[wrap_model_call] 调用失败,将在 {backoff:.2f}s 后重试:{e}")
time.sleep(backoff)
finally:
cost = (time.time() - start) * 1000
print(f"[wrap_model_call] 本次模型调用耗时:{cost:.0f} ms")
# ========= 组装 Agent(DeepSeek 模型) =========
# 你也可以通过环境变量配置:OPENAI_API_KEY/DEEPSEEK_API_KEY
llm = ChatOpenAI(
model="deepseek-chat",
api_key="sk-f3c7557e9fc54541802fd638de03bbeb",
base_url="https://api.deepseek.com",
temperature=0.3,
)
agent = create_agent(
model=llm,
tools=[], # 如需可加工具,此处保持最小化
middleware=[
personalized_prompt, # 便捷:动态系统提示
log_before_agent, # 节点:Agent 级前置
log_before_model, # 节点:模型前
retry_and_timing, # 包裹:重试与计时
validate_output, # 节点:模型后(含 jump_to)
log_after_agent, # 节点:Agent 级后置
],
)
# ========= 最小演示 =========
if __name__ == "__main__":
# 1) 正常问答(不会触发安全跳转)
res1 = agent.invoke(
{"messages": [HumanMessage("用一句话解释 LangGraph 是什么。")]},
config={"context": {"user_id": "alice"}},
)
print("\n[Result-1]", res1["messages"][-1].content)
print("--------------------------------")
# 2) 触发 after_model 的阻断(让模型输出包含关键字)
res2 = agent.invoke(
{"messages": [HumanMessage("请只回复:BLOCKED_CN")]},
config={"context": {"user_id": "bob"}},
)
print("\n[Result-2]", res2["messages"][-1].content)
运行结果如图 2-18 所示。
图 2-18 日志节选:
[before_agent] 本次会话开始,已有消息数:1
[before_model] 准备进行模型调用,当前消息数:1
[wrap_model_call] 本次模型调用耗时:999 ms
[after_agent] 会话结束,最终消息数:2
[Result-1] LangGraph 是一个用于构建有状态、多步骤语言模型工作流的框架,支持图结构编排和动态控制。
--------------------------------
[before_agent] 本次会话开始,已有消息数:1
[before_model] 准备进行模型调用,当前消息数:1
[wrap_model_call] 本次模型调用耗时:880 ms
[after_model] 触发安全规则:检测到 BLOCKED_CN,跳转到 end
[after_agent] 会话结束,最终消息数:3
[Result-2] 该请求触发了安全校验,无法继续。
上述示例代码包含了 LangChain 装饰器中间件的完整使用流程,通过几个简洁的装饰器函数,在不改核心逻辑的情况下,实现了日志、重试、耗时统计、动态修改 Prompt 以及安全拦截等功能,而且从运行效果中可以看到以下内容。
- 第一次调用,模型正常返回,触发了完整的日志与耗时打印。
- 第二次调用,模型输出包含关键字 BLOCKED_CN,@after_model 成功识别关键字 BLOCKED_CN 并跳转到 end,返回了安全提示。
整个执行链路的日志输出,清晰地揭示了不同装饰器中间件的触发顺序与协作关系,体现了该体系良好的层次性与组合性。
最后进行总结如下。
- 节点式钩子:关注 "何时触发",用于在特定执行点轻量地插入逻辑,不改变原有调用流程。
- 包裹式钩子:关注 "如何执行",能够包裹并干预完整的调用过程,具备中断、修改等强大控制能力。
- 便捷装饰器:关注 "效率",为动态修改 Prompt 等常见场景提供快速实现方案。
这三类装饰器并非互斥的,而是可以协同使用的。例如,可以先用 @before_model 进行输入校验,再通过 @wrap_model_call 实现调用重试,从而构建出功能完善且职责清晰的中间件链。
为什么第二次回答的时候有 3 个消息
因为两次 agent.invoke 他们之间的 state 是隔离的,所以以前的回答,被清空了。
第一次 message = HumanMessage
第二次 是 问题问完之后的 AIMessage
第三次是 被after_model 拦截之后,修改了AIMessage,由于消息不会覆盖,只会追加所有有 3 条消息
2.4.7 基于类的中间件
当中间件逻辑变得复杂,需要组合使用多个钩子,或需要维护内部状态以支持可配置性与复用时,基于类(Class-based)的中间件实现方式便成为更合适的选择。其核心钩子类型与基于装饰器的钩子类型类似,主要分为以下两类。
- 节点式(Node-style)钩子:在执行的固定阶段顺序触发,适用于日志记录、状态校验、上下文更新等场景。
- 包裹式(Wrap-style)钩子:用于包裹实际的调用过程,能够深度控制执行流程,典型应用包括实现重试、性能统计、结果缓存等场景。
执行顺序是理解类中间件协同工作的关键。当多个中间件同时存在时,其执行顺序遵循以下规则:
- before_* 钩子:按中间件在列表中的注册顺序执行(从左到右)。
- 包裹式钩子:外层的中间件先于内层的中间件执行,形成 "洋葱模型"。
- after_* 钩子:执行顺序与 before_* 相反,按注册顺序的逆序执行(从右到左)。
以下示例代码展示了如何定义并使用基于类的中间件:
from typing import Any, Callable
import time, math
from langchain.agents import create_agent
from langchain.agents.middleware import (
AgentMiddleware, AgentState, ModelRequest, ModelResponse,
)
from langchain.messages import AIMessage, HumanMessage
from langgraph.runtime import Runtime
from langchain_openai import ChatOpenAI
# ========== 中间件 A:日志 + 安全拦截(Node-style) ==========
class PolicyGuardMiddleware(AgentMiddleware):
"""在关键节点打印日志,并在 after_model 命中关键词时跳转 end"""
def before_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[before_agent] 会话开始,消息数:{len(state.get('messages', []))}")
return None
def before_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[before_model] 准备进行模型调用,当前消息数:{len(state['messages'])}")
return None
def after_model(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
last = state["messages"][-1]
if isinstance(last, AIMessage) and "BLOCKED_CN" in (last.content or ""):
print("[after_model] 触发安全规则:检测到 BLOCKED_CN,跳转 end")
return {
"messages": [AIMessage("该请求触发了安全校验,无法继续。")],
"jump_to": "end",
}
return None
def after_agent(self, state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
print(f"[after_agent] 会话结束,最终消息数:{len(state.get('messages', []))}")
return None
# ========== 中间件 B:重试 + 耗时统计(Wrap-style) ==========
class RetryAndMetricsMiddleware(AgentMiddleware):
"""包裹每次模型调用,做退避重试与耗时统计"""
def __init__(self, max_retries: int = 2, base_delay: float = 0.1):
self.max_retries = max_retries
self.base_delay = base_delay # 秒
def wrap_model_call(
self,
request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse],
) -> ModelResponse:
start = time.time()
try:
for i in range(self.max_retries + 1):
try:
return handler(request)
except Exception as e:
if i == self.max_retries:
raise
backoff = self.base_delay * math.pow(2, i)
print(f"[wrap_model_call] 失败,将在 {backoff:.2f}s 后重试:{e}")
time.sleep(backoff)
finally:
cost = (time.time() - start) * 1000
print(f"[wrap_model_call] 本次模型调用耗时:{cost:.0f} ms")
# ========== 组装 Agent(DeepSeek 模型) ==========
llm = ChatOpenAI(
model="deepseek-chat",
api_key="你的API",
base_url="https://api.deepseek.com",
temperature=0.3
)
# 注意执行顺序:
# - wrap 型按列表"从外到内"包裹;我们希望"重试/计时"最外层,所以把 RetryAndMetricsMiddleware放前面
# - node 型按列表顺序执行 before_*,after_* 逆序回卷
agent = create_agent(
model=llm,
tools=[],
middleware=[
RetryAndMetricsMiddleware(max_retries=2, base_delay=0.1), # 外层:重试+计时
PolicyGuardMiddleware(), # 内层:日志+安全拦截
],
)
if __name__ == "__main__":
# 1) 正常问答
res1 = agent.invoke({"messages": [HumanMessage("用一句话解释 LangGraph 是什么。")]})
print("\n[Result-1]", res1["messages"][-1].content)
print("\n" + "-" * 64 + "\n")
# 2) 触发安全跳转(让模型回复包含关键字)
res2 = agent.invoke({"messages": [HumanMessage("请只回复:BLOCKED_CN")]})
print("\n[Result-2]", res2["messages"][-1].content)
运行结果如图 2-19 所示。
终端日志:
D:\BD20251101\pyworkspace\pythonProject2\.venv\Scripts\python.exe D:\BD20251101\pyworkspace\pythonProject2\Demo12.py
[before_agent] 会话开始,消息数:1
[before_model] 准备进行模型调用,当前消息数:1
[wrap_model_call] 本次模型调用耗时:1283 ms
[after_agent] 会话结束,最终消息数:2
[Result-1] LangGraph 是一个用于构建有状态、多步骤的 AI 代理工作流的框架,它通过有向图结构来编排大语言模型(LLM)的调用、工具使用和状态管理。
----------------------------------------------------------------
[before_agent] 会话开始,消息数:1
[before_model] 准备进行模型调用,当前消息数:1
[wrap_model_call] 本次模型调用耗时:680 ms
[after_model] 触发安全规则:检测到 BLOCKED_CN,跳转 end
[after_agent] 会话结束,最终消息数:3
[Result-2] 该请求触发了安全校验,无法继续。
Process finished with exit code 0
图 2-19
代码结构与职责解析如下。
(1)RetryAndMetricsMiddleware(Wrap-style)
① 作用:包裹每次模型调用,添加退避重试与耗时统计。
② 钩子:wrap_model_call(request, handler) -> ModelResponse。
③ 特点:像洋葱最外层,会在真实模型调用 handler(request)前后插入控制与度量逻辑。
(2)PolicyGuardMiddleware(Node-style)
① 作用:在关键节点打印日志,在after_model检查敏感关键字BLOCKED_CN,命中时改写输出并跳转到 end。
② 钩子:before_agent / before_model / after_model / after_agent。
③ 特点:关注 "何时触发",不改变调用结构,但可以通过返回{"jump_to":"end"}控制流程结束。
在 create_agent(..., middleware=[RetryAndMetricsMiddleware(), PolicyGuardMiddleware()])中,中间件的执行顺序如下。
(1)包裹式(Wrap-style)钩子
按列表从左到右成为外→内的包裹层。
① RetryAndMetricsMiddleware 在最外层,先于一切模型调用执行。
② 内部调用真正的模型执行,同时在结束时记录耗时。
(2)节点式(Node-style)钩子
① before_*钩子:按列表从左到右执行(先外后内)。
② after_*钩子:按列表从右到左执行(先内后外)。
③ 因为 PolicyGuardMiddleware 在列表靠后,所以它的after_model会优先得到模型响应,从而更早地做安全拦截与jump_to="end"跳转。
这就是为什么把 "重试 / 计时" 放在前面、把 "日志 / 安全拦截" 放在后面的原因:外层先兜住稳定性(重试 & 计时),内层更早拿到真实响应做策略判断(拦截 / 跳转)。
智能体与中间件共同构成了 LangChain 架构中核心的能力体系之一。
与早期版本相比,LangChain v1.0 对这一体系进行了彻底重构,确立了清晰的架构分层:智能体专注于决策执行,而中间件则承担过程治理。本节内容正是围绕这一核心理念展开介绍的,从基础概念到项目实践,逐步构建起智能体的完整治理框架。
1. 智能体与中间件概述
我们明确了智能体与中间件的协作关系。智能作为自主决策与问题求解者,以模型为核心进行思考,并调用工具与环境交互;中间件则负责处理日志、缓存、错误恢复、策略审计等横切关注点。这种职责分离使得智能体能够专注于 "做正确的事",而中间件确保 "事情被正确地执行"。
2. 智能体的基本使用与 ReAct 机制
本节通过具体示例展示了智能体的构建方式,并深入解析了 ReAct 机制。在该机制下,智能体能将复杂任务拆解为 "思考 --- 行动 --- 观察" 的迭代循环,使智能体具备动态推理与自适应执行能力。
3. 结构化输出
本节系统介绍了智能体如何进行结构化输出。LangChain 1.0 通过create_agent函数的response_format参数,提供ProviderStrategy和ToolStrategy两种标准化策略,确保输出结果具备可预测的数据结构,为系统集成提供了坚实基础。
4. MCP 的使用
本节进一步探讨了 MCP 如何为智能体与外部系统建立通用交互语言,通过langchain-mcp-adapters的实战演示,展现了智能体作为 MCP 客户端如何统一访问各类工具和服务端,实现了资源集成的标准化。
5. 智能体中的预置中间件
LangChain 1.0 提供了丰富的预置中间件,包括对话摘要、人工协作、调用限制、模型回退等组件。这些中间件共同构成了智能体的可控执行层,使开发者能够快速构建具备生产级可靠性的智能体系统。
6. 基于装饰器的中间件
我们介绍了轻量级装饰器中间件的实现方式。通过节点式钩子、包裹式钩子和便捷装饰器这三类钩子,开发者可以低侵入地扩展智能体功能,从而满足快速定制需求。
7. 基于类的中间件
对于复杂场景,我们探讨了基于类的中间件实现。通过多个钩子的协同工作和状态维护,类中间件支持更复杂的治理策略,体现了分层治理的项目思想。
回顾整节内容,我们完成了从如何让智能体工作到如何让智能体可控的完整演进过程。从 ReAct 推理机制到结构化输出,从 MCP 集成到中间件治理体系,LangChain 1.0 已将智能体从 "可运行的程序" 提升为 "可治理的系统"。
1、 什么是中间件?它有什么作用
中间件跟智能体的业务没多少关系,它一般用于在智能体的某些生命周期中自动触发,可以帮我们进行权限校验,日志记录,消息压缩,人为介入等,有点像 spring 中的 aop
2、 中间件的分类:
我认为可以分为三种:
1)预置中间件,比如可以指定中间件自动压缩历史记录,危险操作人为介入
2)装饰器中间件,一遍都是一个函数,上面编写一个注解 ,比如@before_model @after_model @before_agent @after_agent
- 基于类的中间件,作用和装饰器一样,写法是在类中,编写对应的触发函数,类需要继承AgentMiddleware