摘要 :Agent 系列开篇。从 LangChain 官网的 Agent = Model + Harness (驾驭工程)讲起,用
create_agent创建并调用 Agent,重点打两块地基------中间件范式 (六个钩子,后续记忆/HITL/护栏的基础)、response_format结构化输出与流式输出七种模式。配合 DeepSeek + OpenAI 实测,讲清"工具调用循环自动化""wrap_model_call 动态选模型""@dynamic_prompt 按上下文改提示词"等要点。
前言
前面的 Models 模块,我们重点介绍了与"模型"有关的知识点:初始化、调用方式、结构化输出、工具调用、生产环境管理;
传送门:【LangChain 1.x】07、生产环境模型管理|能力检测、限流、监控与容错
本篇起,进入 LangChain 1.x 的另一个重头:Agent
第 2 篇快速上手时,我们用 create_agent 搭过查天气的小 Agent,它自己调工具、拿结果、再回答;第 6 篇又把工具调用的循环手动拆开看了一遍。本篇开始,正式介绍 Agent:用 create_agent 组装一个能自主推理、循环调工具的智能体,并把它内部的几个关键机制------中间件、结构化输出、流式------讲清楚;
本篇是 Agent 系列(第 8--13 篇)的第一篇,重点打两块地基:create_agent 本身,以及中间件------后面的记忆、人机协同、安全护栏,全都建立在中间件之上;
版本提醒 :本篇起,代码运行环境升级到 langchain 1.3.14(之前使用的langchain版本是 1.2.0)
建议整体升级:
pip install --upgrade langchain langgraph langchain-core langchain-openai
一、什么是 Agent
来自 LangChain 官网的定义:Agent = Model + Harness。
在 Harness Engineering 驾驭工程中,Harness 指围绕大模型构建的约束控制与执行编排系统,意为"驾驭装置"------类比马具(缰绳、笼头),用来引导强模型的能力按预期方向稳定、可控地释放。
在 LangChain 中,它就是把"推理 → 调工具 → 看结果 → 再推理"这个循环编排起来、驱动模型自主完成任务的执行层:模型负责决策(调哪个工具、传什么参数、何时收尾),Harness 负责循环调度、工具执行与状态流转。这个循环有个经典名字------ReAct(Reasoning + Acting,推理 + 行动)。
以"查询最流行的无线耳机并查询库存"举例:
arduino
1. 推理:要找"最流行",得先搜索 → 行动:调 search_products
2. 观察:搜到 WH-1000XM5 排第一 → 推理:还得查它的库存
3. 行动:调 check_inventory("WH-1000XM5") → 观察:库存 10 件
4. 推理:信息够了 → 给出最终答案
第 6 篇我们手动写过这个循环(解析 tool_calls → 执行 → 回传 ToolMessage → 再问模型);create_agent 就是把这个循环自动化的 Harness------你不用再自己写编排逻辑。
- 第 6 篇讲的"LLM + 工具调用"是中间形态------你得手动写循环;
- 本篇的 Agent 把循环交给
create_agent,你只管配置;
理解 Agent,可以把它和前两种形态对比着看:
| 维度 | LLM | LLM + 工具调用 | Agent |
|---|---|---|---|
| 本质 | 文本生成器 | 增强型 LLM(能调函数) | 自主推理 + 执行的智能系统 |
| 工作模式 | 单次问答 | 单轮"请求-调用-响应" | 多轮"推理-行动-观察"循环(ReAct) |
| 状态/记忆 | 自己管 | 自己管 | 内置(后续篇章讲) |
| 错误恢复 | 无 | 无 | 可重试、降级(靠中间件) |
二、create_agent:创建与调用
2.1 最简 Agent
跑本篇代码若报
ImportError: cannot import name 'ExecutionInfo' from 'langgraph.runtime',是 langgraph 太旧、和 langchain 错配,按前言的命令整体升级即可。
create_agent 的三件套是 model + tools + system_prompt(system_prompt 可选)。
先看最简的------只给模型和一个工具:
python
from langchain.agents import create_agent
from langchain_core.tools import tool
from my_llm import deepseek_llm
@tool
def get_weather(city: str) -> str:
"""获取指定城市的天气信息。"""
return f"{city}:晴朗,25°C"
agent = create_agent(model=deepseek_llm, tools=[get_weather])
resp = agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样?"}]})
print(resp["messages"][-1].content)
less
agent 类型: CompiledStateGraph
resp 类型: dict, keys: ['messages']
最终回答: 北京现在的天气情况:晴朗,25°C......
几个要点:
create_agent返回的不是普通 Runnable,而是一个CompiledStateGraph------底层基于 LangGraph 构建的执行图。- 使用
invoke调用,输入是固定的{"messages": [...]}格式(每条消息包含 role 和 content)。 - 返回的
resp是个 dict,最终回答在resp["messages"][-1].content。工具定义(@tool)和第 6 篇完全一样,这里就不重复了。
2.2 system_prompt:塑造 Agent 行为
传 system_prompt 可以定义 Agent 的"角色"和"使命"。比如让它只回答天气问题:
python
agent = create_agent(
model=deepseek_llm,
tools=[get_weather],
system_prompt="你是一个天气查询助手,只回答天气相关的问题;其他问题直接说:我只能查天气。",
)
resp = agent.invoke({"messages": [{"role": "user", "content": "100 + 50 等于多少?"}]})
print(resp["messages"][-1].content)
问个数学题,Agent 会按提示拒答:
makefile
回答: 我只能查天气。
备注:system_prompt 还可以是
SystemMessage对象,或者使用@dynamic_prompt中间件根据上下文动态生成(后续中间件章节介绍)
2.3 异步调用 ainvoke
Agent 也支持异步,方法和参数与 invoke 一样,调用时需要添加 await关键字:
python
import asyncio
resp = await agent.ainvoke({"messages": [{"role": "user", "content": "上海天气?"}]})
异步在 Agent 集成多个外部工具(网络、数据库)时优势明显------等待 IO 时能把 CPU 让给别的任务。单次调用看不出差别,这里点到为止。
三、中间件:Agent 的扩展地基
create_agent 自带的循环只解决"调工具 + 回答"。要让 Agent 真正好用------动态切模型、按角色改提示词、记记忆、人审批、PII 脱敏------全靠中间件(Middleware)。
中间件是 LangChain 1.x 的核心扩展,本篇只做一些基础介绍,后续篇章主题几乎都有中间件的应用。
3.1 六个钩子
中间件挂在 Agent 执行链路的六个时机上,分两类:
| 钩子 | 类型 | 触发时机 | 签名 |
|---|---|---|---|
before_agent |
node-style | Agent 开始(每次 invoke 一次) | `(state, runtime) -> dict |
before_model |
node-style | 每次调模型之前 | `(state, runtime) -> dict |
after_model |
node-style | 每次模型响应之后 | `(state, runtime) -> dict |
after_agent |
node-style | Agent 结束(每次 invoke 一次) | `(state, runtime) -> dict |
wrap_model_call |
wrap-style | 包裹每次模型调用 | (request, handler) -> ModelResponse |
wrap_tool_call |
wrap-style | 包裹每次工具调用 | `(request, handler) -> ToolMessage |
两类区别:
- node-style (前四个):签名是
(state, runtime),返回 dict 可以直接改状态(返回None表示不改)。适合做日志、状态预处理。 - wrap-style (后两个):签名是
(request, handler),要执行下一步就调handler(request),可以在它前后做事、甚至改request后再交给 handler。适合"拦截 + 改写",比如动态换模型、工具错误处理。
另外, @dynamic_prompt 装饰器专门用来动态生成 system prompt(底层是 wrap_model_call 的封装)。
中间件通过 create_agent(middleware=[...]) 注册,可以装饰器函数和类实例混用。
下面通过三个实例简单感受一下:
3.2 实例一:before_model / after_model 做日志(node-style)
最简单的中间件------每次调模型前后打印一行。用装饰器定义:
python
from langchain.agents.middleware import before_model, after_model, AgentState
from langgraph.runtime import Runtime
@before_model
def log_before(state: AgentState, runtime: Runtime):
print(f" [before_model] 准备调模型,当前消息数: {len(state['messages'])}")
return None
@after_model
def log_after(state: AgentState, runtime: Runtime):
print(" [after_model] 模型已响应")
return None
agent = create_agent(model=deepseek_llm, tools=[get_weather],
middleware=[log_before, log_after])
agent.invoke({"messages": [{"role": "user", "content": "北京天气怎么样?"}]})
工具型 Agent 一次 invoke 会调两次模型(第一次决定调工具、第二次基于工具结果回答),所以钩子各触发两次:
csharp
[before_model] 准备调模型,当前消息数: 1
[after_model] 模型已响应
[before_model] 准备调模型,当前消息数: 3
[after_model] 模型已响应
3.3 实例二:wrap_model_call 动态选模型(wrap-style)
wrap_model_call 能在每次调模型前改写请求。经典用法------动态模型选择(这也是前一篇欠下、本篇补上的内容):消息少时用便宜模型,多了切强模型。
python
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
@wrap_model_call
def dynamic_model(request: ModelRequest, handler) -> ModelResponse:
n = len(request.state["messages"])
chosen = openai_llm if n >= 3 else deepseek_llm
name = "openai" if n >= 3 else "deepseek"
print(f" [wrap_model_call] 消息数={n} → 选用 {name}")
return handler(request.override(model=chosen)) # 关键:改 model 后交给 handler
handler(request.override(model=...)) 是核心------override 改请求,handler 继续往下执行。实测同一次天气查询,两次模型调用分别选了不同模型:
csharp
[wrap_model_call] 消息数=1 → 选用 deepseek
[wrap_model_call] 消息数=3 → 选用 openai
wrap_model_call还能 override 别的(如tools=动态筛选工具集),那是第 9 篇"工具进阶"的内容。
3.4 实例三:@dynamic_prompt + context_schema
实际开发中,很多场景需要"按用户身份/上下文"动态调整提示词。
可以通过 context_schema 声明运行时的上下文结构,再配合使用 @dynamic_prompt 读取,来生成提示词。
python
from dataclasses import dataclass
from langchain.agents.middleware import dynamic_prompt, ModelRequest
@dataclass
class UserContext:
user_role: str # "admin" 或 "guest"
@dynamic_prompt
def prompt_by_role(request: ModelRequest) -> str:
role = request.runtime.context.user_role
if role == "admin":
return "你是面向管理员的运维助手,可以给出具体的技术命令和操作步骤。"
return "你是面向普通用户的助手,不要给技术命令,建议联系技术支持。"
agent = create_agent(model=deepseek_llm, tools=[get_weather],
middleware=[prompt_by_role],
context_schema=UserContext) # 声明上下文结构
调用时,context 作为 invoke 的关键字参数传入:
python
agent.invoke(
{"messages": [{"role": "user", "content": "如何重启服务器?"}]},
context=UserContext(user_role="admin"),
)
这样一来,就可以实现同一个问题用户角色 guest 和 admin 得到不同提示词、不同风格的回答:
ini
--- role=guest ---
[dynamic_prompt] role=guest → 你是面向普通用户的助手......
回答: 重启服务器是个需要谨慎的技术操作......建议联系技术支持。
--- role=admin ---
[dynamic_prompt] role=admin → 你是面向管理员的运维助手......
回答: 我来介绍重启服务器的方法......## Linux:`sudo reboot`
总结一下:
- node-style 钩子:before 和 after_model 适合观察/预处理状态
- wrap-style 钩子:wrap_model_call 和 wrap_tool_call 适合拦截改写请求
- @dynamic_prompt 注解:可以实现动态提示词
记住这套机制,后面记忆、HITL、护栏都是它的具体应用。
四、结构化输出:response_format
很多业务场景,需要 Agent 返回结构化数据 ,而不是一段自然语言,比如:抽取客户信息、生成分析报告。可以使用create_agent 的 response_format 参数来实现,结果存在 resp["structured_response"]
4.1 四种策略
response_format 支持四种策略:
| 策略 | 原理 | 适用 |
|---|---|---|
ProviderStrategy(Schema) |
用模型厂商的原生结构化输出 | OpenAI / Anthropic 等支持原生结构化的模型 |
ToolStrategy(Schema) |
添加"虚拟工具",模型调用工具产出结构 | 任何支持工具调用的模型 |
Schema(直接传类型) |
按模型能力自动选上面两种 | 图省事时用 |
None(默认) |
不使用结构化,自然语言返回 | ------ |
实际开发中 ToolStrategy 最通用,能够兼容所有支持工具调用的模型。
Schema 的四种类型(Pydantic / Dataclass / TypedDict / JsonSchema)在第 5 篇讲过,本篇不重复,下面示例中使用 Pydantic
4.2 ToolStrategy + Pydantic
定义一个 Pydantic schema,传给 ToolStrategy。Agent 调完工具后,会按 schema 产出结构化结果:
python
from pydantic import BaseModel, Field
from langchain.agents.structured_output import ToolStrategy
class WeatherReport(BaseModel):
city: str = Field(description="城市名")
condition: str = Field(description="天气状况")
temperature: str = Field(description="温度")
agent = create_agent(
model=deepseek_llm,
tools=[get_weather],
response_format=ToolStrategy(WeatherReport)
)
resp = agent.invoke({"messages": [{"role": "user", "content": "查一下北京天气"}]})
print(resp["structured_response"])
ini
structured_response: city='北京' condition='晴朗' temperature='25°C'
类型: WeatherReport
structured_response 是 WeatherReport 实例,可以直接通过 .city、.temperature 获取字段值
4.3 tool_message_content:省 token
默认情况下,结构化输出产生的那条 ToolMessage 会把完整数据写进对话历史,浪费 token。可以使用 tool_message_content 将它替换成一句短确认,structured_response 仍返回完整数据,只影响对话历史:
python
agent = create_agent(
model=deepseek_llm,
tools=[get_weather],
response_format=ToolStrategy(
WeatherReport,
tool_message_content="天气报告已生成"
)
)
resp = agent.invoke({"messages": [{"role": "user", "content": "查一下北京天气"}]})
print(f"structured_response(完整数据,照常): {resp['structured_response']}")
tool_msgs = [m for m in resp["messages"] if m.type == "tool"]
print(f"历史里最后一条 ToolMessage 内容: {tool_msgs[-1].content}")
ini
structured_response(完整数据,照常): city='北京' condition='晴朗' temperature='25°C'
历史里最后一条 ToolMessage 内容: 天气报告已生成
在消息历史中是短字符串,结构化数据正常返回,在长会话中可以节省不少 token。
4.4 handle_errors:校验失败自动重试
handle_errors=True 时,如果模型填的结构化数据通不过 Pydantic 校验,错误信息会反馈给模型、让它重试,直到合法。
要演示这个过程,得先让模型"填错"。这里用 field_validator 给 rating 加一个模型从 schema 看不出的隐藏约束(必须等于 3)------模型按"评分 1-5"自然会填 5 之类,必然违反这个约束,从而稳定触发重试:
python
from pydantic import BaseModel, Field, field_validator
class ProductEvaluation(BaseModel):
product_name: str = Field(default="", description="产品名称")
rating: int = Field(default=1, description="评分1-5", ge=1, le=5)
sentiment: Literal["正面", "负面", "中性"] = Field(default="中性")
@field_validator("rating")
def rating_must_be_three(cls, v):
if v != 3:
raise ValueError("rating 必须等于 3") # 隐藏约束,schema 描述里看不出
return v
agent = create_agent(
model=deepseek_llm,
tools=[],
response_format=ToolStrategy(
ProductEvaluation,
handle_errors=True
),
)
resp = agent.invoke({"messages": [{"role": "user", "content": "这个产品很不错,我给好评"}]})
# 打印消息链看重试过程(结构化输出时 AI 的数据在 tool_calls 里)
for m in resp["messages"]:
if m.type == "ai":
tcs = getattr(m, "tool_calls", None) or []
print(f"[ai] tool_calls={[(tc['name'], tc['args']) for tc in tcs]}")
else:
print(f"[{m.type}] {str(m.content)[:80]}")
print(f"最终: {resp['structured_response']}")
ini
[human] 这个产品很不错,我给好评
[ai] tool_calls=[('ProductEvaluation', {'product_name': '产品', 'rating': 5, 'sentiment': '正面'})]
[tool] Error: Failed to parse structured output for tool 'ProductEvaluation'...
[ai] tool_calls=[('ProductEvaluation', {'product_name': '产品', 'rating': 3, 'sentiment': '正面'})]
[tool] Returning structured response: product_name='产品' rating=3 sentiment='正面'
最终: product_name='产品' rating=3 sentiment='正面'
消息链中体现了模型重试的过程:模型第一次填了 rating=5 → Pydantic 校验失败(违反"必须等于 3")→ handle_errors 把错误反馈给模型 → 模型第二次改填 rating=3 → 成功。如果没有 handle_errors,第一次校验失败就会直接抛异常、程序中断。
说明:结构化输出时,模型通常会自觉遵守 schema 中的约束(如
ge=1 le=5、枚举),所以很难靠 system_prompt 诱导它填错;这里使用
field_validator添加隐藏约束,是为了稳定复现"校验失败→重试"的过程。而在实际开发中,重试的动作会在复杂 schema(嵌套、严格格式、Union 多类型)场景下自然发生。
handle_errors还支持传字符串、异常类型、自定义函数来定制错误提示。
五、流式输出:stream 的七种模式
"多轮模型+工具调用"场景下,Agent 执行可能需要较长时间,流式输出可以边执行边输出、有效提升交互体验。
agent.stream() 通过 stream_mode 参数提供七种模式:
| 模式 | 输出内容 | 适用场景 |
|---|---|---|
updates(默认) |
每步的增量更新(哪个节点变了什么) | 监控执行步骤 |
messages |
token 级分片 + 元数据 | 类 ChatGPT 打字机效果 |
values |
每步的完整状态快照 | 要完整状态、做持久化 |
custom |
工具/节点内用 get_stream_writer 推送的自定义数据 |
业务进度、自定义日志 |
tasks |
任务信息(id、错误) | 监控任务生命周期 |
debug |
比 tasks 多步骤、时间戳 | 调试 |
checkpoints |
检查点状态 | 状态持久化、断点续跑 |
下面以实际开发中常用的几种模式进行演示:updates、messages、values、custom、模式组合
5.1 updates(默认):看 ReAct 步骤
updates 模式,每一步给出一个 {节点名: 状态更新},节点名通常是 model / tools。
稍微解析一下,ReAct 的"决策→执行→回答"就一目了然:
python
agent = create_agent(model=deepseek_llm, tools=[get_weather])
for chunk in agent.stream({"messages": [{"role": "user", "content": "北京天气怎么样?"}]}):
for node, data in chunk.items():
last = data["messages"][-1]
if node == "model":
tcs = getattr(last, "tool_calls", None) or []
print(f" [model] 决定调用工具: {[tc['name'] for tc in tcs]}" if tcs
else " [model] 给出最终回答")
elif node == "tools":
print(f" [tools] 执行了工具: {last.name}")
less
[model] 决定调用工具: ['get_weather']
[tools] 执行了工具: get_weather
[model] 给出最终回答
5.2 messages:token 级流式
messages 模式会进行逐 token 的返回,适合做打字机效果。
每次输出的结构是 (chunk, metadata),其中的chunk.content 是这次流出的文本片段:
python
for item in agent.stream({"messages": [{"role": "user", "content": "用一句话介绍 LangChain"}]},
stream_mode="messages"):
chunk = item[0] if isinstance(item, tuple) else item
if hasattr(chunk, "content") and chunk.content:
print(f" [{chunk.content}]")
每个 chunk 会单独输出:
css
[Lang]
[Chain]
[ ]
[是一个]
[用于]
...
在实际做打字机效果,把每个 chunk.content 用 print(..., end="", flush=True) 拼起来即可。
5.3 values:完整状态快照
values 模式,每次输出会给出当前全量 state:
python
for chunk in agent.stream({"messages": [{"role": "user", "content": "北京天气怎么样?"}]},
stream_mode="values"):
msgs = chunk["messages"]
print(f" 本步完整 state({len(msgs)} 条): {' → '.join(m.type for m in msgs)}")
perl
本步完整 state(1 条): human
本步完整 state(2 条): human → ai
本步完整 state(3 条): human → ai → tool
本步完整 state(4 条): human → ai → tool → ai
5.4 custom:工具内推送进度
custom 模式,接收工具/节点内通过调用 get_stream_writer() 主动推送的数据,适合用于输出业务处理进度(比如:"已处理 10/100 条"之类)。
在工具中的实现方式:
python
from langgraph.config import get_stream_writer
@tool
def generate_report() -> str:
"""生成一份报告。"""
writer = get_stream_writer()
for i in range(1, 4):
time.sleep(0.3)
writer({"进度": f"{i * 33}%"})
return "报告完成:总收入 150 万"
用 stream_mode="custom" 接收,工具内部的执行进度可以被调用方实时接受到,提升用户体验:
css
自定义数据: {'进度': '33%'}
自定义数据: {'进度': '66%'}
自定义数据: {'进度': '99%'}
5.5 模式组合
stream_mode 参数可以传 list,使用多种模式同时输出,每个输出结构是 (模式名, 数据)。
下面,使用"updates 的步骤结构 + messages 的逐字流"进行演示:
python
node_steps, msg_count = [], 0
for mode, chunk in agent.stream({"messages": [{"role": "user", "content": "北京天气怎么样?"}]},
stream_mode=["updates", "messages"]):
if mode == "updates":
node_steps.extend(chunk.keys())
elif mode == "messages":
msg_count += 1
print(f" updates 捕获的节点顺序: {node_steps}")
print(f" messages 流出的 chunk 数: {msg_count}")
less
updates 捕获的节点顺序: ['model', 'tools', 'model']
messages 流出的 chunk 数: 79
stream_mode 模式选型上:
- 实时对话选 messages
- 观察步骤选 updates
- 要完整状态选 values
- 业务进度选 custom
- 根据场景需要,按需组合使用
六、预置 Agent 一瞥:create_deep_agent
前面介绍了 create_agent + 中间件,至此我们已经可以自己组装 Agent。而开篇提到的 Harness Engineering(驾驭工程) ,眼下正是大模型应用最火的工程方向:围绕强模型构建约束控制与执行编排系统,让模型能力稳定、可控地释放。LangChain 1.x 提供了一个把整套 Harness 能力打包好的开箱实现:create_deep_agent。
python
from deepagents import create_deep_agent # 来自 deepagents 包
create_deep_agent 面向长时编码 / 复杂研究这类"重型"任务,一行调用就带上了完成长任务所需的完整能力栈:
- 文件系统:可读写工作目录,处理多文件、多步骤任务
- 对话摘要:自动压缩历史,防止长任务上下文爆炸
- 子 Agent 委派:把子任务拆给专门的子 Agent,主 Agent 负责统筹
- 任务清单(TodoList):自己规划、跟踪待办,长任务不跑偏
- Prompt 缓存:命中重复前缀,降低长会话成本
这些能力背后是 FilesystemMiddleware、SummarizationMiddleware、SubAgentMiddleware、TodoListMiddleware 等一系列内置中间件,create_deep_agent 就是 Harness 工程的一个完整参考实现。
本篇只做个引子。
create_deep_agent涉及的驾驭工程实践、多种预置中间件、构建与应用,后续会单开一个专栏展开讲
七、总结
本篇是 Agent 系列的开篇,主要介绍了以下几个部分:
-
Agent 的创建:create_agent:
model + tools + system_prompt组装 Agent,返回CompiledStateGraph;invoke传{"messages": [...]},结果在resp["messages"];system_prompt塑造行为;ainvoke异步。
-
Agent 的灵魂------中间件:六个钩子分两类,支撑了动态切换模型、动态提示词、记忆、HITL、安全护栏等能力
- node-style:
before/after_model、before/after_agent(方法签名state, runtime) - wrap-style:
wrap_model_call、wrap_tool_call(方法签名request, handler) @dynamic_prompt是动态提示词的便捷封装。
- node-style:
-
结构化输出 :
response_format=ToolStrategy(Schema)让 Agent 返回结构化数据(在resp["structured_response"]),ToolStrategy最通用;tool_message_content省 token、handle_errors自动重试。 -
流式输出 :
stream()七种模式,常用 updates(看步骤)、messages(打字机)、values(完整状态)、custom(工具进度),可组合。
下一篇进入工具进阶 ,看 Agent 语境下工具新能力:ToolRuntime 访问运行时上下文、Command 改 Agent 状态、return_direct 短路循环、动态工具选择、Headless tools 等。