【LangChain 1.x】08、Agent 核心入门|create_agent、中间件、结构化与流式输出

0 阅读17分钟

摘要: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,推理 + 行动)。

以"查询最流行的无线耳机并查询库存"举例:

1. 推理:要找"最流行",得先搜索 → 行动:调 search_products
2. 观察:搜到 WH-1000XM5 排第一 → 推理:还得查它的库存
3. 行动:调 check_inventory("WH-1000XM5") → 观察:库存 104. 推理:信息够了 → 给出最终答案

第 6 篇我们手动写过这个循环(解析 tool_calls → 执行 → 回传 ToolMessage → 再问模型);create_agent 就是把这个循环自动化的 Harness——你不用再自己写编排逻辑。

  • 第 6 篇讲的"LLM + 工具调用"是中间形态——你得手动写循环;
  • 本篇的 Agent 把循环交给 create_agent,你只管配置;

理解 Agent,可以把它和前两种形态对比着看:

维度LLMLLM + 工具调用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 可选)。

先看最简的——只给模型和一个工具:

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)
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 的"角色"和"使命"。比如让它只回答天气问题:

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 会按提示拒答:

回答: 我只能查天气。

备注:system_prompt 还可以是 SystemMessage 对象,或者使用 @dynamic_prompt 中间件根据上下文动态生成(后续中间件章节介绍)

2.3 异步调用 ainvoke

Agent 也支持异步,方法和参数与 invoke 一样,调用时需要添加 await关键字:

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_agentnode-styleAgent 开始(每次 invoke 一次)(state, runtime) -> dict|None
before_modelnode-style每次调模型之前(state, runtime) -> dict|None
after_modelnode-style每次模型响应之后(state, runtime) -> dict|None
after_agentnode-styleAgent 结束(每次 invoke 一次)(state, runtime) -> dict|None
wrap_model_callwrap-style包裹每次模型调用(request, handler) -> ModelResponse
wrap_tool_callwrap-style包裹每次工具调用(request, handler) -> ToolMessage|Command

两类区别:

  • 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)

最简单的中间件——每次调模型前后打印一行。用装饰器定义:

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 会调两次模型(第一次决定调工具、第二次基于工具结果回答),所以钩子各触发两次:

  [before_model] 准备调模型,当前消息数: 1
  [after_model] 模型已响应
  [before_model] 准备调模型,当前消息数: 3
  [after_model] 模型已响应

3.3 实例二:wrap_model_call 动态选模型(wrap-style)

wrap_model_call 能在每次调模型前改写请求。经典用法——动态模型选择(这也是前一篇欠下、本篇补上的内容):消息少时用便宜模型,多了切强模型。

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 继续往下执行。实测同一次天气查询,两次模型调用分别选了不同模型:

  [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 读取,来生成提示词。

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 的关键字参数传入:

agent.invoke(
    {"messages": [{"role": "user", "content": "如何重启服务器?"}]},
    context=UserContext(user_role="admin"),
)

这样一来,就可以实现同一个问题用户角色 guest 和 admin 得到不同提示词、不同风格的回答:

--- 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_agentresponse_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 产出结构化结果:

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"])
structured_response: city='北京' condition='晴朗' temperature='25°C'
类型: WeatherReport

structured_responseWeatherReport 实例,可以直接通过 .city.temperature 获取字段值

4.3 tool_message_content:省 token

默认情况下,结构化输出产生的那条 ToolMessage 会把完整数据写进对话历史,浪费 token。可以使用 tool_message_content 将它替换成一句短确认,structured_response 仍返回完整数据,只影响对话历史

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}")
structured_response(完整数据,照常): city='北京' condition='晴朗' temperature='25°C'
历史里最后一条 ToolMessage 内容: 天气报告已生成

在消息历史中是短字符串,结构化数据正常返回,在长会话中可以节省不少 token。

4.4 handle_errors:校验失败自动重试

handle_errors=True 时,如果模型填的结构化数据通不过 Pydantic 校验,错误信息会反馈给模型、让它重试,直到合法。

要演示这个过程,得先让模型"填错"。这里用 field_validatorrating 加一个模型从 schema 看不出的隐藏约束(必须等于 3)——模型按"评分 1-5"自然会填 5 之类,必然违反这个约束,从而稳定触发重试:

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']}")
[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(默认)每步的增量更新(哪个节点变了什么)监控执行步骤
messagestoken 级分片 + 元数据类 ChatGPT 打字机效果
values每步的完整状态快照要完整状态、做持久化
custom工具/节点内用 get_stream_writer 推送的自定义数据业务进度、自定义日志
tasks任务信息(id、错误)监控任务生命周期
debug比 tasks 多步骤、时间戳调试
checkpoints检查点状态状态持久化、断点续跑

下面以实际开发中常用的几种模式进行演示:updates、messages、values、custom、模式组合

5.1 updates(默认):看 ReAct 步骤

updates 模式,每一步给出一个 {节点名: 状态更新},节点名通常是 model / tools

稍微解析一下,ReAct 的"决策→执行→回答"就一目了然:

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}")
  [model] 决定调用工具: ['get_weather']
  [tools] 执行了工具: get_weather
  [model] 给出最终回答

5.2 messages:token 级流式

messages 模式会进行逐 token 的返回,适合做打字机效果。

每次输出的结构是 (chunk, metadata),其中的chunk.content 是这次流出的文本片段:

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 会单独输出:

  [Lang]
  [Chain]
  [ ]
  [是一个]
  [用于]
  ...

在实际做打字机效果,把每个 chunk.contentprint(..., end="", flush=True) 拼起来即可。

5.3 values:完整状态快照

values 模式,每次输出会给出当前全量 state:

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)}")
  本步完整 state1 条): human
  本步完整 state2 条): human → ai
  本步完整 state3 条): human → ai → tool
  本步完整 state4 条): human → ai → tool → ai

5.4 custom:工具内推送进度

custom 模式,接收工具/节点内通过调用 get_stream_writer() 主动推送的数据,适合用于输出业务处理进度(比如:"已处理 10/100 条"之类)。

在工具中的实现方式:

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" 接收,工具内部的执行进度可以被调用方实时接受到,提升用户体验:

  自定义数据: {'进度': '33%'}
  自定义数据: {'进度': '66%'}
  自定义数据: {'进度': '99%'}

5.5 模式组合

stream_mode 参数可以传 list,使用多种模式同时输出,每个输出结构是 (模式名, 数据)

下面,使用“updates 的步骤结构 + messages 的逐字流”进行演示:

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}")
  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

from deepagents import create_deep_agent   # 来自 deepagents 包

create_deep_agent 面向长时编码 / 复杂研究这类"重型"任务,一行调用就带上了完成长任务所需的完整能力栈:

  • 文件系统:可读写工作目录,处理多文件、多步骤任务
  • 对话摘要:自动压缩历史,防止长任务上下文爆炸
  • 子 Agent 委派:把子任务拆给专门的子 Agent,主 Agent 负责统筹
  • 任务清单(TodoList):自己规划、跟踪待办,长任务不跑偏
  • Prompt 缓存:命中重复前缀,降低长会话成本

这些能力背后是 FilesystemMiddlewareSummarizationMiddlewareSubAgentMiddlewareTodoListMiddleware 等一系列内置中间件,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_modelbefore/after_agent(方法签名 state, runtime
    • wrap-style:wrap_model_callwrap_tool_call(方法签名 request, handler
    • @dynamic_prompt 是动态提示词的便捷封装。
  • 结构化输出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 等。