LangChain Agent 核心机制深度剖析:从流式模式到生产级最佳实践
本文是一份生产环境视角下的深度笔记,基于个人
.py学习笔记整理。我们将聚焦langchain.agents.create_agent的核心机制,特别是stream_mode的多种流式模式,并深入探讨模型控制、中间件、结构化输出、消息系统架构以及 Anthropic 提示词缓存等高级特性。
阅读对象:已了解 LangChain 基础概念的开发者,希望进阶掌握 Agent 流式传输和生产级配置。
0. 写在前面:笔记的原始风格说明
本笔记采用 代码 + 详细注释 + 执行流程拆解 的形式,力求让读者不仅知道“怎么写”,更理解“为什么这样写”以及“内部发生了什么”。
⚠️ 重要:笔记中的代码示例多为“演示片段”,部分中间件和动态提示示例未完整实现(仅展示模式),生产使用时需根据实际需求补充逻辑。
1. 核心流式模式详解
LangGraph Agent 提供了多种流式模式,让你能根据不同场景灵活选择。我们先从 stream_mode="updates" 开始介绍。
1.1 stream_mode="updates":按节点流式传输
这个模式是干什么的?
该模式在每个 图节点(如 Agent 的推理步骤、一个工具调用节点) 完成后发出一个事件。
为什么有用?
它非常适合向用户展示 Agent 的“思考过程”,例如“正在查询天气…”、“正在计算…”,让用户感知到系统在逐步推进,而不是长时间无响应。
注意:下面的代码首先创建一个 Agent 实例(使用 create_agent),但 stream_mode 的具体用法将在代码之后演示。这里先熟悉 Agent 的创建方式。
# 核心流式模式详解
# 1. stream_mode="updates":按节点流式传输
# 该模式在每个 图节点(如一个 Agent 的推理步骤、一个工具调用)完成后发出一个事件。
# 它适合向用户展示 Agent 的“思考过程”,例如“正在查询天气…”、“正在计算…”。
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气(示例工具)"""
return f"{city}weather" # 简化返回,实际应调用 API
# 创建一个简单的 Agent
agent = create_agent(
model="deepseek-chat", # 模型名称,也可以是 "openai:gpt-4o"
tools=[get_weather], # 工具列表
system_prompt="你是一个天气助手", # 系统提示
)
# 1.1 create_agent() 讲解
1.1.1 create_agent() 返回的对象
"""
返回的对象:
create_agent() 返回一个可调用的智能体对象,它实际上是一个 Runnable 接口的实现。
这个对象有以下主要方法:
agent.invoke(input_dict) - 同步调用,返回完整结果
agent.stream(input_dict, stream_mode=...) - 流式调用,返回生成器
agent.batch(input_list) - 批量调用,处理多个输入
"""
输入字典格式:invoke 和 stream 的 input_dict 必须遵循特定结构。
"""
{
"messages":
[
{"role": "user", "content": "问题内容"}
# 也可以包含多轮对话历史
]
}
"""
1.2 深入 create_agent 的核心参数
1.2.1 model:不仅仅是“选哪个模型”
为什么这个参数重要?
模型是 Agent 的“大脑”,但生产环境中你需要的不只是模型名称,而是对模型行为的精确控制(如随机性、超时、重试等)。直接传字符串会导致使用默认参数,可能引发不可预期的行为或成本失控。
# ❌ 不推荐生产环境:无法控制模型行为
agent2 = create_agent(
model="openai:gpt-4o", # 内部使用默认 temperature=0.7, timeout=60
tools=[]
)
# ✅ 生产环境推荐:精确控制
model = ChatOpenAI(
model="gpt-4o",
temperature=0.1, # 降低随机性,提高确定性
max_tokens=2048, # 控制输出长度,管理成本
timeout=30, # 防止单次调用阻塞整个 Agent
max_retries=2, # 网络抖动时的自动重试
model_kwargs={"seed": 42} # 某些提供商支持确定性输出
)
agent3 = create_agent(
model=model, # 传入的是实例,内部直接使用,不再重新初始化
tools=[]
)
# 执行时:agent.invoke() 内部调用 model.invoke()
# 如果模型调用失败,会触发 max_retries=2 的重试逻辑
# 重试仍失败后,异常向上抛出,可被 middleware 捕获处理
这段代码如何体现知识点?
- 通过对比直接传字符串和传实例,说明精确控制的重要性。
- 通过注释解释重试和异常抛出机制。
1.2.2 system_prompt:从静态文本到动态上下文
为什么重要?
系统提示是控制 Agent 行为的核心手段。生产环境中,系统提示通常需要根据用户身份、对话阶段动态变化。此外,对于支持缓存的模型(如 Anthropic),合理设置 cache_control 可以大幅降低成本。
from langchain.agents import create_agent
from langchain.messages import SystemMessage
# 场景:构建一个支持提示词缓存的 Agent(Anthropic 特有)
# 将经常重复使用的长文本(如公司政策)标记为缓存,降低 90% 成本
cached_system_prompt = SystemMessage(
content=[
{
"type": "text",
"text": "你是某电商平台的客服助手。以下是完整的退换货政策:\n" +
"1. 7天无理由退货...\n2. 质量问题15天内换货...\n" +
"(共3000字政策全文)"
},
{
"type": "text",
"text": "请基于上述政策回答用户问题。",
"cache_control": {"type": "ephemeral"} # 标记这段内容可缓存
}
]
)
agent4 = create_agent(
model="anthropic:claude-sonnet-4-5",
tools=[],
system_prompt=cached_system_prompt # 直接传 SystemMessage
)
# 执行效果:
# 第1次调用:完整发送系统提示,Claude 返回时告知哪些内容被缓存
# 第2次调用(相同 thread_id):只发送缓存引用,成本降低约 90%
代码与知识点的对应:
- 展示了如何将
SystemMessage作为system_prompt传入。 - 通过
cache_control标记长文本,实现缓存。 - 注释说明了两次调用的不同行为。
1.2.3 middleware:Agent 的“中间件层”
内部机制:
中间件在 Agent 执行的各个阶段(模型调用前、工具调用前、模型调用后)插入钩子。多个中间件按列表顺序执行,可以修改 request 或 state。LangGraph 内部将中间件包装为图节点,在模型节点和工具节点之间插入。
下面介绍三种生产级中间件模式。
模式1:动态模型选择(成本优化)
原理:根据对话复杂度(如消息数量、工具调用次数)动态切换模型:简单问题用小模型(快、便宜),复杂问题用大模型(强、贵)。
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain_openai import ChatOpenAI
# 配置:简单问题用小模型,复杂问题用大模型
basic_model = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
advanced_model = ChatOpenAI(model="gpt-4o", temperature=0.1)
@wrap_model_call
def dynamic_model_selector(request: ModelRequest, handler) -> ModelResponse:
"""
根据对话复杂度动态选择模型
执行流程:
1. 拦截模型调用请求
2. 分析当前 state 中的消息数量和工具调用次数
3. 决定使用哪个模型
4. 将修改后的请求传给 handler
"""
# 这里应该添加真实的复杂度判断逻辑
# 示例:if len(request.state["messages"]) > 10: model = advanced_model
return handler(request.override(model=model)) # model 需根据条件赋值
agent5 = create_agent(
model=basic_model, # 默认模型
tools=[],
middleware=[dynamic_model_selector]
)
# 执行效果:
# 第1轮:"帮我搜索 Python 教程" → 使用 gpt-4o-mini
# 第5轮(已进行3次工具调用后):"基于搜索结果,帮我分析哪个教程最适合初学者" → 自动切换到 gpt-4o
模式2:工具错误处理(系统健壮性)
为什么重要:默认情况下,工具异常会导致 Agent 执行中断。通过中间件,可以让 Agent 继续执行,并向 LLM 提供错误信息,让它自己决定下一步(重试、换工具、告知用户)。
from langchain.agents.middleware import wrap_tool_call
@wrap_tool_call
def graceful_tool_error_handler(request, handler):
"""
拦截工具执行,将异常转换为友好的 ToolMessage
为什么重要:默认情况下,工具异常会导致 Agent 执行中断
通过中间件,可以让 Agent 继续执行,并向 LLM 提供错误信息,让它自己决定下一步
"""
# 实际使用时需要 try-except 并返回 ToolMessage
pass
agent6 = create_agent(
model=model,
tools=[], # 这些工具可能因网络/权限失败
middleware=[graceful_tool_error_handler]
)
# 执行效果(数据流转):
# 1. Agent 调用 database_query_tool(sql="SELECT * FROM users")
# 2. 数据库连接超时 → 抛出 ConnectionError
# 3. 中间件捕获异常 → 返回 ToolMessage("工具 'database_query_tool' 执行失败:连接超时...")
# 4. LLM 收到这个 ToolMessage 后,可以:
# - 重试(调整参数)
# - 告知用户暂时无法查询
# - 使用其他工具替代
# 5. Agent 不会崩溃,继续执行
模式3:动态系统提示(上下文感知)
from langchain.agents.middleware import dynamic_prompt
@dynamic_prompt
def context_aware_system_prompt(request: ModelRequest) -> str:
"""
根据用户上下文动态生成系统提示
执行时机:每次模型调用前
数据来源:invoke() 时传入的 context 参数
"""
# 示例:根据用户角色返回不同提示
# user_role = request.context.get("user_role", "guest")
# if user_role == "admin":
# return "你是管理员助手,可以执行敏感操作。"
# return "你是普通用户助手。"
pass
1.2.4 response_format:结构化输出
为什么重要:当 Agent 作为上游系统的一部分时,你需要的是可解析的数据,而不是自然语言。例如,一个数据分析 Agent 需要返回 JSON 给前端图表组件。
内部机制(ToolStrategy):create_agent() 内部创建一个名为 structured_output_response 的工具,强制模型在最终回答时调用它,工具调用参数即为你定义的结构。
ProviderStrategy:直接使用模型的 response_format 参数,模型返回的 AIMessage 的 additional_kwargs 中包含解析后的结构化数据。
from pydantic import BaseModel
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy, ProviderStrategy
# 定义输出结构
class OrderInfo(BaseModel):
order_id: str
status: str
estimated_delivery: str | None
items: list[str]
# 方式1:ToolStrategy(适用于所有模型)
agent_tool = create_agent(
model="gpt-4o-mini", # 任何支持工具调用的模型都可以
#tools=[query_order_tool],
response_format=ToolStrategy(OrderInfo)
)
result = agent_tool.invoke({
"messages": [{"role": "user", "content": "帮我查订单 ORD-12345 的状态"}]
})
# result["structured_response"] 是 OrderInfo 实例
print(result["structured_response"].order_id) # "ORD-12345"
print(result["structured_response"].status) # "shipped"
# 方式2:ProviderStrategy(仅支持原生结构化输出的提供商)
agent_provider = create_agent(
model="openai:gpt-4o", # 必须支持原生 structured output
#tools=[query_order_tool],
response_format=ProviderStrategy(OrderInfo)
)
1.2.5 context_schema:依赖注入的类型安全(略)
为什么重要:生产环境中,你的工具和中间件通常需要访问数据库连接、用户 ID、配置等外部依赖。context_schema 配合 middleware 提供了类型安全的依赖注入机制。(本节略,感兴趣可查阅官方文档)
1.3 回到流式模式:agent.stream() 的使用
现在我们已经创建好 Agent(变量名为 agent),下面演示如何用 stream_mode="updates" 获取节点级输出。
user_input = {"messages": [{"role": "user", "content": "北京的天气如何"}]}
# 2.11 agent.stream() 的返回对象结构与数据流转 stream_mode="updates"
# stream() 返回的是一个生成器,每次迭代产出一个节点执行结果的快照字典,
# 让你能实时追踪 Agent 的每一步执行。
# 模式1:updates(默认)- 只返回节点输出的增量更新
for chunk in agent.stream(user_input, stream_mode="updates"):
print(chunk)
# 模式2:values - 返回每次更新后的完整状态
for chunk in agent.stream(user_input, stream_mode="values"):
print(chunk)
# 更详细的处理示例
for chunk in agent.stream(user_input, stream_mode="updates"):
for node_name, node_output in chunk.items():
print(f"{node_name} -> {node_output}")
last_msg = node_output["messages"][-1]
# Python 的 -1 索引是取列表最后一个元素的惯用写法,
# 无论列表多长,[-1] 永远返回最新消息。
if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
print(f"工具调用请求: {last_msg.tool_calls}")
# hasattr(obj, name) 检查对象 obj 是否具有属性 name。
elif last_msg.content:
print(f"内容: {last_msg.content}")
chunk 的详细结构说明(注释原文):
"""
agent.stream()返回一个生成器(generator),每次迭代产生一个chunk字典
# chunk示例格式/所具有的元素(stream_mode="updates"时):
{
"agent": # ← node_name 字符串
{
"messages": [
AIMessage(content="思考中..."),# AI消息对象
AIMessage(tool_calls=[...])
# 或带content的消息或 ToolMessage, HumanMessage, SystemMessage等
]
AIMessage:AI的响应消息
可能包含.content(文本内容)
可能包含.tool_calls(工具调用列表)
ToolMessage:工具执行结果消息
包含.content(工具返回结果)
HumanMessage:用户输入消息
包含.content(用户问题)
SystemMessage:系统消息
包含.content(系统提示)
},
"tools": # ← node_name 字符串
{
"messages": [
ToolMessage(content="工具执行结果...")
]
}
}
"""
一个完整的 Agent 执行流程示例(三个 chunk 依次输出):
"""
假设一个完整的 Agent 执行流程,三个 chunk 依次是:
Chunk 1(agent 节点发出工具调用)
{
"agent": { # ← node_name
"messages": [ ←node_output 层:节点输出数据
AIMessage(
content="", # 无文本
tool_calls=[{"name": "get_weather", "args": {"city": "北京"}, "id": "call_001"}]
)
]
}
}
↑ 这对应注释中的 AIMessage(tool_calls=[...])
Chunk 2(tools 节点返回结果)
{
"tools": { # ← node_name
"messages": [ ←node_output 层:节点输出数据
ToolMessage(
content="北京:晴天,24°C",
name="get_weather",
tool_call_id="call_001"
)
]
}
}
↑ 这对应注释中的 ToolMessage(content="工具执行结果...")
Chunk 3(agent 节点输出最终答案)
{
"agent": { # ← node_name
"messages": [ ←node_output 层:节点输出数据
AIMessage(content="北京今天天气晴朗,温度24°C")
]
}
}
↑ 这对应你注释中的 AIMessage(content="思考中...") 或最终答案
"""
1.4 stream_mode="messages":令牌级流式(打字机效果)
该模式在 LLM 生成每一个令牌(token)时都发出事件,是实现“打字机效果”的基础,能极大提升交互流畅感。
# 2.12 agent.stream() 的返回对象结构与数据流转 stream_mode="messages"
# 使用 messages 模式流式执行
user_input = {"messages": [{"role": "user", "content": "北京今天天气如何?"}]}
for token, metadata in agent.stream(user_input, stream_mode="messages"):
# token 是消息块,metadata 包含该块所属的节点等信息
node_name = metadata.get("langgraph_node", "unknown")
# 提取内容块(可能是文本或工具调用片段)
for content_block in token.content_blocks:
block_type = content_block.get("type")
if block_type == "text":
# 普通文本令牌,直接输出
print(content_block.get("text", ""), end="", flush=True)
elif block_type == "tool_call_chunk":
# 工具调用的流式片段,通常不直接展示给用户
tool_name = content_block.get("name")
args_chunk = content_block.get("args", "")
if tool_name:
print(f"\n[调用工具: {tool_name}]", end="")
if args_chunk:
print(args_chunk, end="")
返回值拆解(原始笔记中的详细注释):
"""
# 每次迭代返回的是一个 Python 元组,包含两个元素
(token, metadata) = (token_object, metadata_dict)
token 的类型通常是 AIMessageChunk 或 ToolMessageChunk,它们有一个 content_blocks 属性,包含具体的块数据。
元组元素 类型 关键属性 说明
token AIMessageChunk .content_blocks 文本片段或工具调用片段列表
.tool_call_chunks 工具调用的流式片段
.content 累积的文本内容(在最终块中)
metadata dict ["langgraph_node"] 产生该 token 的节点名称("model"/"tools")
["langgraph_step"] 执行步骤序号
["thread_id"] 会话线程 ID(如果配置了)
["checkpoint_id"] 检查点 ID
"""
完整数据流转示例(分阶段):
"""
阶段1:工具调用开始(第一个 token)
# 第1个 token - 工具调用开始标记
(
AIMessageChunk(
content="", #.content_blocks
tool_call_chunks=[ #.tool_call_chunks
{
"name": "get_weather",
"args": "",
"id": "call_abc123",
"index": 0,
"type": "tool_call_chunk"
}
]
),
# metadata 部分
{
"langgraph_node": "model",
"langgraph_step": 1,
"langgraph_triggers": ["branch:router"],
"langgraph_path": ("__pregel_push", 0),
"langgraph_checkpoint_ns": "model:1",
"checkpoint_id": "1ef7d8a2-3b4c-5d6e-7f8a-9b0c1d2e3f4a",
"thread_id": "session_456"
}
)
阶段2:工具参数流式输出(逐步构建)
# 第2个 token - 参数片段: '{'
# 第3个 token - 参数片段: '"city"'
# 第4个 token - 参数片段: ':"北京"'
# 第5个 token - 参数片段: '}'
阶段3:工具执行节点
(
ToolMessageChunk(
content=
{
"type":"text"
"text":"北京:晴天,24°C"
},
tool_call_id="call_abc123",
name="get_weather"
),
{
"langgraph_node": "tools",
"langgraph_step": 2,
...
}
)
阶段4:最终回答文本流式输出
(
AIMessageChunk(content="北京", tool_call_chunks=[]),
{...}
)
(
AIMessageChunk(content="今天", tool_call_chunks=[]),
{...}
)
...
"""
1.5 stream_mode="custom":自定义进度更新
这是最灵活的模式。你可以在工具函数内部,通过 get_stream_writer() 获取一个写入器,主动向客户端发送任意状态信息。
# 3. stream_mode="custom":自定义进度更新【拓展,不做要求】
# 这是最灵活的模式。你可以在工具函数内部,
# 通过 get_stream_writer() 获取一个写入器,主动向客户端发送任意状态信息。
# custom 模式允许你在工具函数内部,通过 get_stream_writer()
# 主动向外部发送自定义事件。这些事件会以独立流的形式输出,与 updates 或 messages 模式完全隔离。
from langgraph.config import get_stream_writer
@tool
def get_weather_with_progress(city: str) -> str:
"""带进度提示的天气查询工具"""
writer = get_stream_writer()
# 发送自定义进度消息
writer(f"⏳ 正在解析城市: {city}...")
# 模拟 API 调用前的预处理
import time
time.sleep(0.5)
writer(f"🌐 正在连接天气服务...")
time.sleep(0.5)
writer(f"✅ 成功获取{city}天气数据")
return f"{city}当前天气晴朗,温度24°C"
# 创建使用该工具的 Agent
agent_with_custom = create_agent(
model="openai:gpt-4o",
tools=[get_weather_with_progress],
system_prompt="你是一个天气助手。"
)
# 使用 custom 模式流式执行
user_input = {"messages": [{"role": "user", "content": "上海今天天气如何?"}]}
for custom_event in agent_with_custom.stream(user_input, stream_mode="custom"):
# custom_event 就是在工具中 writer() 传入的内容
print(f"[自定义进度] {custom_event}")
1.6 四种流式模式对比
"""
模式 每次迭代返回 触发时机 典型用途
stream_mode="updates" dict(节点输出) 每个节点完成后 展示 Agent 步骤
stream_mode="messages" (token, metadata) 元组 每个 token 生成时 实现打字机效果
stream_mode="custom" 任意值(你传入的) 调用 writer() 时 自定义进度、状态
stream_mode="values" dict(完整状态) 每次状态变更后 调试、保存快照
"""
2. 补充知识:消息系统的三层架构
为什么需要理解这个?
LangChain 的消息系统抽象了不同 LLM 提供商的差异,统一为 BaseMessage 及其子类。理解三层架构有助于你正确构建多模态输入、设置缓存控制,以及调试消息序列化问题。
"""
┌─────────────────────────────────────────────────────────────────┐
│ 第1层:消息类型(Role 层) │
│ 定义了消息的发送者身份,决定消息在对话中的语义角色 │
├─────────────────────────────────────────────────────────────────┤
│ • SystemMessage → role="system" → 设定 AI 行为边界 │
│ • HumanMessage → role="user" → 用户输入 │
│ • AIMessage → role="assistant" → AI 回复 │
│ • ToolMessage → role="tool" → 工具执行结果 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 第2层:消息结构(Message 层) │
│ 定义消息的组成部分,LangChain 统一不同提供商的差异 │
├─────────────────────────────────────────────────────────────────┤
│ class BaseMessage: │
│ content: str | List[dict] # 第3层决定格式 │
│ additional_kwargs: dict # 提供商特有字段 │
│ response_metadata: dict # 响应元数据 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 第3层:内容格式(Content Block 层) │
│ 定义 content 字段的内部结构,支持多模态和扩展元数据 │
├─────────────────────────────────────────────────────────────────┤
│ content = [ │
│ {"type": "text", "text": "..."}, # 文本块 │
│ {"type": "image", "url": "..."}, # 图片块 │
│ {"type": "tool_use", ...}, # 工具调用块 │
│ ] │
└─────────────────────────────────────────────────────────────────┘
"""
完整的演示代码
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
# ========== 步骤1:在 LangChain 中创建消息 ==========
# 系统消息:角色是 "system",内容是多模态列表
system_msg = SystemMessage(
content=[
{"type": "text", "text": "你是一个图像分析助手"},
{"type": "image", "url": "https://example.com/photo.png"}
]
)
# 用户消息:角色是 "user",内容是简单字符串
human_msg = HumanMessage(content="这张图片里有什么?")
# AI 消息:角色是 "assistant",内容 + 工具调用
ai_msg = AIMessage(
content="让我分析一下这张图片",
tool_calls=[{"name": "analyze_image", "args": {"image_url": "..."}, "id": "call_123"}]
)
# ========== 步骤2:查看 LangChain 内部存储结构 ==========
print(f"SystemMessage 类型: {type(system_msg)}") # <class 'SystemMessage'>
print(f"SystemMessage.role: {system_msg.type}") # 'system'
print(f"SystemMessage.content: {system_msg.content}") # [{'type': 'text', ...}, ...]
print(f"HumanMessage.type: {human_msg.type}") # 'human'
print(f"AIMessage.type: {ai_msg.type}") # 'ai'
# ========== 步骤3:发送给 OpenAI 时的序列化 ==========
model = ChatOpenAI(model="gpt-4o")
# LangChain 内部调用 model._convert_messages() 将消息转换为 OpenAI 格式
# 转换结果大致如下:
openai_format_messages = [
{
"role": "system",
"content": [
{"type": "text", "text": "你是一个图像分析助手"},
{"type": "image_url", "image_url": {"url": "https://example.com/photo.png"}}
]
},
{
"role": "user",
"content": "这张图片里有什么?"
},
{
"role": "assistant",
"content": "让我分析一下这张图片",
"tool_calls": [{"id": "call_123", "type": "function", "function": {...}}]
}
]
# ========== 步骤4:发送给 Anthropic 时的序列化(格式不同) ==========
anthropic_format_messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "你是一个图像分析助手"}, # Anthropic 没有 system role
{"type": "image", "source": {"type": "url", "url": "..."}}
]
},
{
"role": "user",
"content": "这张图片里有什么?"
}
]
场景化使用指南
# 场景1:纯文本对话(99% 的日常使用)
system_msg = SystemMessage(content="你是一个助手")
human_msg = HumanMessage(content="你好")
# 场景2:需要多模态输入(图片、音频、文档)
# 必须使用内容块列表
system_msg2 = SystemMessage(
content=[
{"type": "text", "text": "你是文档 OCR 专家"},
{"type": "image", "url": "document_page1.png"}
]
)
# 场景3:需要缓存控制(高频调用,长系统提示)
# 必须在内容块中设置 cache_control
system_msg3 = SystemMessage(
content=[
{
"type": "text", "text": "3000字系统指令...",
"cache_control": {"type": "ephemeral"}
},
{"type": "text", "text": "当前用户消息"}
]
)
3. 补充知识:cache_control 缓存机制深度解析(提示词缓存)
为什么需要提示词缓存?
在你的生产 Agent 中,每次调用都传输相同的 3000 字系统提示,会导致:
- 高延迟:每次都要处理 3000 个 token
- 高成本:输入 token 按量计费
- 资源浪费:LLM 重复处理相同内容
一句话总结:缓存让 LLM 记住重复出现的提示前缀,后续调用只传输变化的部分,大幅降低延迟和成本。
缓存机制流程图
"""
┌─────────────────────────────────────────────────────────────────┐
│ 第一次调用(缓存写入) │
├─────────────────────────────────────────────────────────────────┤
│ 请求内容: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 系统提示(3000字)+ cache_control 标记 │ │
│ │ 用户消息:"分析这份合同" │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Claude API: │ │
│ │ 1. 处理完整提示(耗时) │ │
│ │ 2. 将"系统提示"写入缓存(断点位置 = 标记处) │ │
│ │ 3. 返回响应 + 缓存元数据 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 响应中的缓存指标: │
│ {"cache_creation_input_tokens": 3000, ← 写入缓存的 token 数 │
│ "cache_read_input_tokens": 0, ← 命中缓存的 token 数 │
│ "input_tokens": 3020} │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 第二次调用(缓存命中) │
├─────────────────────────────────────────────────────────────────┤
│ 请求内容(相同 thread_id): │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 系统提示(3000字)+ cache_control 标记 ← 命中缓存! │ │
│ │ 用户消息:"换个角度分析..."(只有这 20 字是新内容) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Claude API: │ │
│ │ 1. 检测到缓存命中 │ │
│ │ 2. 直接从缓存读取系统提示(不重新计算) │ │
│ │ 3. 只处理新的用户消息 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 响应中的缓存指标: │
│ {"cache_creation_input_tokens": 0, ← 本次未写入新缓存 │
│ "cache_read_input_tokens": 3000, ← 命中缓存的 token 数 │
│ "input_tokens": 20} ← 实际处理的 token 数 │
└─────────────────────────────────────────────────────────────────┘
"""
cache_control 是什么?
a_dict = {"cache_control": {"type": "ephemeral"}}
# ephemeral: adj 短暂的;瞬息的
# 1. cache_control 到底是什么?
# 一句话解释:cache_control 是一个提示标记,告诉 Anthropic 的 API:
# “从这里开始往后的内容,请帮我缓存起来,下次调用时别再重新计算了。”
# 它不是缓存“这条消息”,而是缓存从消息开头到这个标记处为止的所有内容
# 使用方式:多个缓存断点(推荐)
# 生产最佳实践:在静态内容块后都设置缓存
content = [
# 块1:系统角色(短文本,不缓存也行)
{"type": "text", "text": "你是数据分析助手"},
# 块2:工具定义(静态,建议缓存)
{"type": "text", "text": "工具1: query_database...\n工具2: analyze_data...",
"cache_control": {"type": "ephemeral"}},
# 块3:系统规则(静态,建议缓存)
{"type": "text", "text": "规则: 1. 优先使用缓存...\n2. 遵循数据安全规范...",
"cache_control": {"type": "ephemeral"}},
# 块4:用户消息(动态,不设缓存)
{"type": "text", "text": "用户输入"}
]
成本对比
"""
重要性:
场景 输入Token 费用(按 $3/M 输入计)
无缓存:每次传 3000 字系统提示 + 20 字用户消息 3020 $0.00906
有缓存:仅传 20 字用户消息 20 $0.00006
每次调用节省 - $0.009(99% 成本降低)
对于每天 10 万次调用的生产系统:
无缓存:$906/天
有缓存:$6/天
年节省:约 $328,500
"""
4. 补充知识:消息简写格式 ("role", "content") 的完整解析
这个格式是 LangChain 提供的便捷消息创建方式。让我详细解释它的机制、作用和与标准格式的关系。
"""
一句话解释:消息简写格式是为了减少代码冗余,让你用元组列表快速构建对话历史,
而无需手动实例化 SystemMessage、HumanMessage、AIMessage 类。
在生产场景中,你可能需要:
- 从数据库加载对话历史(存储为 JSON)
- 从 API 接收用户消息(格式可能是 {role, content})
- 快速测试不同的对话流程
手动创建消息对象代码冗长:
"""
from langchain_core.messages import *
# 冗长的方式
messages = [
SystemMessage(content="你是一个边塞诗人。"),
HumanMessage(content="写一首唐诗。"),
AIMessage(content="锄禾日当午..."),
HumanMessage(content="按照你上一个回复的格式,在写一首唐诗。")
]
# 使用简写格式,一行搞定:
# 简洁的方式
messages1 = [
("system", "你是一个边塞诗人。"),
("human", "写一首唐诗。"),
("ai", "锄禾日当午..."),
("human", "按照你上一个回复的格式,在写一首唐诗。")
]
# 数据流转过程:
# 你的输入: 简写元组
input_messages = [
("system", "你是一个边塞诗人。"),
("human", "写一首唐诗。"),
("ai", "锄禾日当午..."),
("human", "按照你上一个回复的格式,在写一首唐诗。")
]
# ↓ LangChain 内部转换 ↓ 消息对象
converted_messages = [
SystemMessage(content="你是一个边塞诗人。"),
HumanMessage(content="写一首唐诗。"),
AIMessage(content="锄禾日当午..."),
HumanMessage(content="按照你上一个回复的格式,在写一首唐诗。")
]
# ↓ 发送给 LLM API(OpenAI 格式)↓ 字典格式
openai_messages = [
{"role": "system", "content": "你是一个边塞诗人。"},
{"role": "user", "content": "写一首唐诗。"},
{"role": "assistant", "content": "锄禾日当午..."},
{"role": "user", "content": "按照你上一个回复的格式,在写一首唐诗。"}
]
写在最后
本文完整保留了原始 .py 笔记中 95% 以上的注释和讲解,并按照 “先解释概念 → 再展示代码 → 代码内注释 → 代码后分析流转” 的教学顺序重新组织。希望这份笔记能帮助你在实际项目中构建更高效、更稳健、更低成本的 Agent 应用。
关键 Takeaways:
- 选择合适的流式模式:
updates展示步骤,messages实现打字机,custom做自定义进度 - 生产环境务必精确控制模型参数:
temperature、max_retries、timeout等 - 利用
middleware实现横切关注点:动态模型选择、统一错误处理、上下文感知提示 - 结构化输出让 Agent 可组合:使用
ToolStrategy或ProviderStrategy - 消息系统三层架构:理解 Role → Message → Content Block 的层次
- 提示词缓存(Anthropic)可降低 99% 成本:对于长系统提示的高频场景至关重要
- 简写格式提升代码可读性:
("role", "content")元组列表
本文为个人学习笔记与实践总结,如有错误或疏漏,欢迎指正交流。
如果觉得本文对你有帮助,欢迎点赞、收藏、转发!
个人笔记完整的代码如下:读者可以直接复制粘贴到编译器里面跟着示例敲一遍(可能示例不是特别好哈),敲一遍复习完效果更好哦!
#核心流式模式详解
# 1. stream_mode="updates":按节点流式传输
# 该模式在每个 图节点(如一个 Agent 的推理步骤、一个工具调用)完成后发出一个事件。
# 它适合向用户展示 Agent 的“思考过程”,例如“正在查询天气…”、“正在计算…”。
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
@tool
def get_weather(city:str)->str:
return f"{city}weather"
agent = create_agent(
model="deepseek-chat",
tools=[get_weather],
system_prompt="你是一个人天气助手",
)
# 使用 updates 模式流式执行
# for chunk in agent.stream(user_input, stream_mode="updates"):
# print(f"收到节点输出: {chunk}")
#1.1 create_agent()讲解
"""
返回的对象:
create_agent()返回一个可调用的智能体对象,它实际上是一个Runnable接口的实现。这个对象有以下主要方法:
agent.invoke(input_dict) - 同步调用
agent.stream(input_dict, stream_mode=...) - 流式调用
agent.batch(input_list) - 批量调用
"""
#input_dict:输入字典,必须是特定格式
"""
{
"messages":
[
{"role": "user", "content": "问题内容"}
]
}
"""
#1.11model:不仅仅是“选哪个模型”
#为什么重要:模型是 Agent 的“大脑”,但生产环境中你需要的不只是模型名称,而是对模型行为的精确控制。
# ❌ 不推荐生产环境:无法控制模型行为
agent2 = create_agent(
model="openai:gpt-4o", # 内部使用默认 temperature=0.7, timeout=60
tools=[]
)
# ✅ 生产环境推荐:精确控制
model = ChatOpenAI(
model="gpt-4o",
temperature=0.1, # 降低随机性,提高确定性
max_tokens=2048, # 控制输出长度,管理成本
timeout=30, # 防止单次调用阻塞整个 Agent
max_retries=2, # 网络抖动时的自动重试
model_kwargs={"seed": 42} # 某些提供商支持确定性输出
)
agent3 = create_agent(
model=model, # 传入的是实例,内部直接使用,不再重新初始化
tools=[]
)
# 执行时:agent.invoke() 内部调用 model.invoke()
# 如果模型调用失败,会触发 max_retries=2 的重试逻辑
# 重试仍失败后,异常向上抛出,可被 middleware 捕获处理
#1.12 system_prompt:从静态文本到动态上下文
#为什么重要:系统提示是控制 Agent 行为的核心手段。生产环境中,系统提示通常需要根据用户身份、对话阶段动态变化。
from langchain.agents import create_agent
from langchain.messages import SystemMessage
# 场景:构建一个支持提示词缓存的 Agent(Anthropic 特有)
# 将经常重复使用的长文本(如公司政策)标记为缓存,降低 90% 成本
cached_system_prompt = SystemMessage(
content=[
{
"type": "text",
"text": "你是某电商平台的客服助手。以下是完整的退换货政策:\n" +
"1. 7天无理由退货...\n2. 质量问题15天内换货...\n" +
"(共3000字政策全文)"
},
{
"type": "text",
"text": "请基于上述政策回答用户问题。",
"cache_control": {"type": "ephemeral"} # 标记这段内容可缓存
}
]
)
agent4 = create_agent(
model="anthropic:claude-sonnet-4-5",
tools=[],
system_prompt=cached_system_prompt # 直接传 SystemMessage
)
# 执行效果:
# 第1次调用:完整发送系统提示,Claude 返回时告知哪些内容被缓存
# 第2次调用(相同 thread_id):只发送缓存引用,成本降低约 90%
#1.13 middleware:Agent 的“中间件层”
"""
内部机制:
中间件在 Agent 执行的各个阶段(模型调用前、工具调用前、模型调用后)插入钩子
多个中间件按列表顺序执行,可以修改 request 或 state
LangGraph 内部将中间件包装为图节点,在模型节点和工具节点之间插入
"""
# 三种生产级中间件模式:中间件的简单介绍!
# 模式1:动态模型选择(成本优化)
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain_openai import ChatOpenAI
# 配置:简单问题用小模型,复杂问题用大模型
basic_model = ChatOpenAI(model="gpt-4o-mini", temperature=0.1)
advanced_model = ChatOpenAI(model="gpt-4o", temperature=0.1)
@wrap_model_call
def dynamic_model_selector(request: ModelRequest, handler) -> ModelResponse:
"""
根据对话复杂度动态选择模型
执行流程:
1. 拦截模型调用请求
2. 分析当前 state 中的消息数量和工具调用次数
3. 决定使用哪个模型
4. 将修改后的请求传给 handler
"""
return handler(request.override(model=model))
agent5 = create_agent(
model=basic_model, # 默认模型
tools=[],
middleware=[dynamic_model_selector]
)
# 执行效果:
# 第1轮:"帮我搜索 Python 教程" → 使用 gpt-4o-mini
# 第5轮(已进行3次工具调用后):"基于搜索结果,帮我分析哪个教程最适合初学者" → 自动切换到 gpt-4o
#模式2:工具错误处理(系统健壮性)
from langchain.agents.middleware import wrap_tool_call
@wrap_tool_call
def graceful_tool_error_handler(request, handler):
"""
拦截工具执行,将异常转换为友好的 ToolMessage
为什么重要:默认情况下,工具异常会导致 Agent 执行中断
通过中间件,可以让 Agent 继续执行,并向 LLM 提供错误信息,让它自己决定下一步
"""
pass
agent6 = create_agent(
model=model,
tools=[], # 这些工具可能因网络/权限失败
middleware=[graceful_tool_error_handler]
)
# 执行效果(数据流转):
# 1. Agent 调用 database_query_tool(sql="SELECT * FROM users")
# 2. 数据库连接超时 → 抛出 ConnectionError
# 3. 中间件捕获异常 → 返回 ToolMessage("工具 'database_query_tool' 执行失败:连接超时...")
# 4. LLM 收到这个 ToolMessage 后,可以:
# - 重试(调整参数)
# - 告知用户暂时无法查询
# - 使用其他工具替代
# 5. Agent 不会崩溃,继续执行
#模式3:动态系统提示(上下文感知)
from langchain.agents.middleware import dynamic_prompt
@dynamic_prompt
def context_aware_system_prompt(request: ModelRequest) -> str:
"""
根据用户上下文动态生成系统提示
执行时机:每次模型调用前
数据来源:invoke() 时传入的 context 参数
"""
pass
#1.14 response_format:结构化输出
# 为什么重要:当 Agent 作为上游系统的一部分时,你需要的是可解析的数据,
# 而不是自然语言。例如,一个数据分析 Agent 需要返回 JSON 给前端图表组件。
# ToolStrategy:create_agent() 内部创建一个名为 structured_output_response 的工具
# 强制模型在最终回答时调用它,工具调用参数即为你定义的结构
#
# ProviderStrategy:直接使用模型的 response_format 参数,
# 模型返回的 AIMessage 的 additional_kwargs 中包含解析后的结构化数据
from pydantic import BaseModel
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy, ProviderStrategy
# 定义输出结构
class OrderInfo(BaseModel):
order_id: str
status: str
estimated_delivery: str | None
items: list[str]
# 方式1:ToolStrategy(适用于所有模型)
agent_tool = create_agent(
model="gpt-4o-mini", # 任何支持工具调用的模型都可以
#tools=[query_order_tool],
response_format=ToolStrategy(OrderInfo)
)
result = agent_tool.invoke({
"messages": [{"role": "user", "content": "帮我查订单 ORD-12345 的状态"}]
})
# result["structured_response"] 是 OrderInfo 实例
print(result["structured_response"].order_id) # "ORD-12345"
print(result["structured_response"].status) # "shipped"
# 方式2:ProviderStrategy(仅支持原生结构化输出的提供商)
agent_provider = create_agent(
model="openai:gpt-4o", # 必须支持原生 structured output
#tools=[query_order_tool],
response_format=ProviderStrategy(OrderInfo)
)
#1.15:context_schema:依赖注入的类型安全(略)
# 为什么重要:生产环境中,你的工具和中间件通常需要访问数据库连接、
# 用户 ID、配置等外部依赖。context_schema 配合 middleware 提供了类型安全的依赖注入机制
user_input={"messages":[{"role":"user","content":"北京的天气如何"}]}
# 2.11 agent.stream() 的返回对象结构与数据流转stream_mode="updates"
#stream() 返回的是一个生成器,每次迭代产出一个节点执行结果的快照字典,让你能实时追踪 Agent 的每一步执行。
# 模式1:updates(默认)- 只返回节点输出的增量更新
for chunk in agent.stream(user_input, stream_mode="updates"):
print(chunk)
# 模式2:values - 返回每次更新后的完整状态
for chunk in agent.stream(user_input, stream_mode="values"):
print(chunk)
for chunk in agent.stream(user_input,stream_mode="updates"):
for node_name,node_output in chunk.items():
print(f"{node_name} -> {node_output}")
last_msg=node_output["messages"][-1]
#Python 的 -1 索引是取列表最后一个元素的惯用写法,无论列表多长,[-1] 永远返回最新消息.
if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
print(f"工具调用请求: {last_msg.tool_calls}")
# hasattr(obj, name)检查对象obj是否具有属性name。
elif last_msg.content:
print(f"内容: {last_msg.content}")
"""
agent.stream()返回一个生成器(generator),每次迭代产生一个chunk字典
# chunk示例格式/所具有的元素(stream_mode="updates"时):
{
"agent": # ← node_name 字符串
{
"messages": [
AIMessage(content="思考中..."),# AI消息对象
AIMessage(tool_calls=[...])
# 或带content的消息或 ToolMessage, HumanMessage, SystemMessage等
]
AIMessage:AI的响应消息
可能包含.content(文本内容)
可能包含.tool_calls(工具调用列表)
ToolMessage:工具执行结果消息
包含.content(工具返回结果)
HumanMessage:用户输入消息
包含.content(用户问题)
SystemMessage:系统消息
包含.content(系统提示)
},
"tools": # ← node_name 字符串
{
"messages": [
ToolMessage(content="工具执行结果...")
]
}
}
假设一个完整的 Agent 执行流程,三个 chunk 依次是:
Chunk 1(agent 节点发出工具调用)
{
"agent": { # ← node_name
"messages": [ ←node_output 层:节点输出数据
AIMessage(
content="", # 无文本
tool_calls=[{"name": "get_weather", "args": {"city": "北京"}, "id": "call_001"}]
)
]
}
}
↑ 这对应注释中的 AIMessage(tool_calls=[...])
Chunk 2(tools 节点返回结果)
{
"tools": { # ← node_name
"messages": [ ←node_output 层:节点输出数据
ToolMessage(
content="北京:晴天,24°C",
name="get_weather",
tool_call_id="call_001"
)
]
}
}
↑ 这对应注释中的 ToolMessage(content="工具执行结果...")
Chunk 3(agent 节点输出最终答案)
{
"agent": { # ← node_name
"messages": [ ←node_output 层:节点输出数据
AIMessage(content="北京今天天气晴朗,温度24°C")
]
}
}
↑ 这对应你注释中的 AIMessage(content="思考中...") 或最终答案
"""
# 2.12 agent.stream() 的返回对象结构与数据流转stream_mode="messages"
#当使用 stream_mode="messages" 时,每次迭代返回一个 元组 (token, metadata)。
# 2. stream_mode="messages":按令牌流式传输
# 该模式在 LLM 生成 每一个令牌(token) 时都发出事件。
# 这是实现“打字机效果”的基础,能极大提升交互流畅感。
# 使用 messages 模式流式执行
user_input = {"messages": [{"role": "user", "content": "北京今天天气如何?"}]}
for token, metadata in agent.stream(user_input, stream_mode="messages"):
# token 是消息块,metadata 包含该块所属的节点等信息
node_name = metadata.get("langgraph_node", "unknown")
# 提取内容块(可能是文本或工具调用片段)
for content_block in token.content_blocks:
block_type = content_block.get("type")
if block_type == "text":
# 普通文本令牌,直接输出
print(content_block.get("text", ""), end="", flush=True)
elif block_type == "tool_call_chunk":
# 工具调用的流式片段,通常不直接展示给用户
tool_name = content_block.get("name")
args_chunk = content_block.get("args", "")
if tool_name:
print(f"\n[调用工具: {tool_name}]", end="")
if args_chunk:
print(args_chunk, end="")
#拆解agent.stream(user_input, stream_mode="messages"):的返回值
"""
# 每次迭代返回的是一个 Python 元组,包含两个元素
(token, metadata) = (token_object, metadata_dict)
token 的类型通常是 AIMessageChunk 或 ToolMessageChunk,它们有一个 content_blocks 属性,包含具体的块数据。
元组元素 类型 关键属性 说明
token AIMessageChunk .content_blocks 文本片段或工具调用片段列表
.tool_call_chunks 工具调用的流式片段
.content 累积的文本内容(在最终块中)
metadata dict ["langgraph_node"] 产生该 token 的节点名称("model"/"tools")
["langgraph_step"] 执行步骤序号
["thread_id"] 会话线程 ID(如果配置了)
["checkpoint_id"] 检查点 ID
阶段1:工具调用开始(第一个 token)
# 第1个 token - 工具调用开始标记
(
# token 部分
AIMessageChunk(
content="", #.content_blocks
tool_call_chunks=[ #.tool_call_chunks
{
"name": "get_weather",
"args": "",
"id": "call_abc123",
"index": 0,
"type": "tool_call_chunk"
}
]
),
# metadata 部分
{
"langgraph_node": "model",
"langgraph_step": 1,
"langgraph_triggers": ["branch:router"],
"langgraph_path": ("__pregel_push", 0),
"langgraph_checkpoint_ns": "model:1",
"checkpoint_id": "1ef7d8a2-3b4c-5d6e-7f8a-9b0c1d2e3f4a",
"thread_id": "session_456"
}
)
阶段2:工具参数流式输出(逐步构建)
# 第2个 token - 参数片段: '{'
(
AIMessageChunk(
content="",
tool_call_chunks=[
{
"name": None,
"args": "{",
"id": None,
"index": 0,
"type": "tool_call_chunk"
}
]
),
{
"langgraph_node": "model",
"langgraph_step": 1,
# ... 其他 metadata 同阶段1
}
)
# 第3个 token - 参数片段: '"city"'
(
AIMessageChunk(
content="",
tool_call_chunks=[
{
"name": None,
"args": '"city"',
"id": None,
"index": 0,
"type": "tool_call_chunk"
}
]
),
metadata # 同前
)
# 第4个 token - 参数片段: ':"北京"'
(
AIMessageChunk(
content="",
tool_call_chunks=[
{
"name": None,
"args": ':"北京"',
"id": None,
"index": 0,
"type": "tool_call_chunk"
}
]
),
metadata
)
# 第5个 token - 参数结束: '}'
(
AIMessageChunk(
content="",
tool_call_chunks=[
{
"name": None,
"args": "}",
"id": None,
"index": 0,
"type": "tool_call_chunk"
}
]
),
metadata
)
阶段3:工具执行节点
# 工具节点的输出 token
(
ToolMessageChunk(
content=
{
"type":"text"
"text":"北京:晴天,24°C"
},
tool_call_id="call_abc123",
name="get_weather"
),
{
"langgraph_node": "tools",
"langgraph_step": 2,
"langgraph_triggers": ["branch:model"],
"langgraph_path": ("__pregel_push", 1),
"langgraph_checkpoint_ns": "tools:1",
"checkpoint_id": "1ef7d8a2-3b4c-5d6e-7f8a-9b0c1d2e3f4b",
"thread_id": "session_456"
}
)
阶段4:最终回答文本流式输出
# 第N个 token - 文本片段
(
AIMessageChunk(
content="北京",
tool_call_chunks=[] # 无工具调用
),
{
"langgraph_node": "model",
"langgraph_step": 3,
"langgraph_triggers": ["branch:tools"],
"langgraph_path": ("__pregel_push", 2),
"langgraph_checkpoint_ns": "model:2",
"checkpoint_id": "1ef7d8a2-3b4c-5d6e-7f8a-9b0c1d2e3f4c",
"thread_id": "session_456"
}
)
# 下一个 token
(
AIMessageChunk(
content="今天",
tool_call_chunks=[]
),
metadata # 同前
)
# 再下一个
(
AIMessageChunk(
content="天气",
tool_call_chunks=[]
),
metadata
)
# 继续直到完成
(
AIMessageChunk(
content="晴朗,温度24°C",
tool_call_chunks=[]
),
metadata
)
"""
# 3. stream_mode="custom":自定义进度更新【拓展,不做要求】
# 这是最灵活的模式。你可以在工具函数内部,
# 通过 get_stream_writer() 获取一个写入器,主动向客户端发送任意状态信息。
# custom 模式允许你在工具函数内部,通过 get_stream_writer()
# 主动向外部发送自定义事件。这些事件会以独立流的形式输出,与 updates 或 messages 模式完全隔离。
from langgraph.config import get_stream_writer
@tool
def get_weather_with_progress(city: str) -> str:
"""带进度提示的天气查询工具"""
writer = get_stream_writer()
# 发送自定义进度消息
writer(f"⏳ 正在解析城市: {city}...")
# 模拟 API 调用前的预处理
import time
time.sleep(0.5)
writer(f"🌐 正在连接天气服务...")
time.sleep(0.5)
writer(f"✅ 成功获取{city}天气数据")
return f"{city}当前天气晴朗,温度24°C"
# 创建使用该工具的 Agent
agent_with_custom = create_agent(
model="openai:gpt-4o",
tools=[get_weather_with_progress],
system_prompt="你是一个天气助手。"
)
# 使用 custom 模式流式执行
user_input = {"messages": [{"role": "user", "content": "上海今天天气如何?"}]}
for custom_event in agent_with_custom.stream(user_input, stream_mode="custom"):
# custom_event 就是在工具中 writer() 传入的内容
print(f"[自定义进度] {custom_event}")
"""
模式 每次迭代返回 触发时机 典型用途
stream_mode="updates" dict(节点输出) 每个节点完成后 展示 Agent 步骤
stream_mode="messages" (token, metadata) 元组 每个 token 生成时 实现打字机效果
stream_mode="custom" 任意值(你传入的) 调用 writer() 时 自定义进度、状态
stream_mode="values" dict(完整状态) 每次状态变更后 调试、保存快照
"""
#==========================================================================
#补充:1.12中关于格式 消息系统的三层架构
"""
┌─────────────────────────────────────────────────────────────────┐
│ 第1层:消息类型(Role 层) │
│ 定义了消息的发送者身份,决定消息在对话中的语义角色 │
├─────────────────────────────────────────────────────────────────┤
│ • SystemMessage → role="system" → 设定 AI 行为边界 │
│ • HumanMessage → role="user" → 用户输入 │
│ • AIMessage → role="assistant" → AI 回复 │
│ • ToolMessage → role="tool" → 工具执行结果 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 第2层:消息结构(Message 层) │
│ 定义消息的组成部分,LangChain 统一不同提供商的差异 │
├─────────────────────────────────────────────────────────────────┤
│ class BaseMessage: │
│ content: str | List[dict] # 第3层决定格式 │
│ additional_kwargs: dict # 提供商特有字段 │
│ response_metadata: dict # 响应元数据 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 第3层:内容格式(Content Block 层) │
│ 定义 content 字段的内部结构,支持多模态和扩展元数据 │
├─────────────────────────────────────────────────────────────────┤
│ content = [ │
│ {"type": "text", "text": "..."}, # 文本块 │
│ {"type": "image", "url": "..."}, # 图片块 │
│ {"type": "tool_use", ...}, # 工具调用块 │
│ ] │
└─────────────────────────────────────────────────────────────────┘
"""
#完整的演示
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
# ========== 步骤1:在 LangChain 中创建消息 ==========
# 系统消息:角色是 "system",内容是多模态列表
system_msg = SystemMessage(
content=[
{"type": "text", "text": "你是一个图像分析助手"},
{"type": "image", "url": "https://example.com/photo.png"}
]
)
# 用户消息:角色是 "user",内容是简单字符串
human_msg = HumanMessage(content="这张图片里有什么?")
# AI 消息:角色是 "assistant",内容 + 工具调用
ai_msg = AIMessage(
content="让我分析一下这张图片",
tool_calls=[{"name": "analyze_image", "args": {"image_url": "..."}, "id": "call_123"}]
)
# ========== 步骤2:查看 LangChain 内部存储结构 ==========
print(f"SystemMessage 类型: {type(system_msg)}") # <class 'SystemMessage'>
print(f"SystemMessage.role: {system_msg.type}") # 'system'
print(f"SystemMessage.content: {system_msg.content}") # [{'type': 'text', ...}, ...]
print(f"HumanMessage.type: {human_msg.type}") # 'human'
print(f"AIMessage.type: {ai_msg.type}") # 'ai'
# ========== 步骤3:发送给 OpenAI 时的序列化 ==========
model = ChatOpenAI(model="gpt-4o")
# LangChain 内部调用 model._convert_messages() 将消息转换为 OpenAI 格式
# 转换结果大致如下:
openai_format_messages = [
{
"role": "system",
"content": [
{"type": "text", "text": "你是一个图像分析助手"},
{"type": "image_url", "image_url": {"url": "https://example.com/photo.png"}}
]
},
{
"role": "user",
"content": "这张图片里有什么?"
},
{
"role": "assistant",
"content": "让我分析一下这张图片",
"tool_calls": [{"id": "call_123", "type": "function", "function": {...}}]
}
]
# ========== 步骤4:发送给 Anthropic 时的序列化(格式不同) ==========
anthropic_format_messages = [
{
"role": "user",
"content": [
{"type": "text", "text": "你是一个图像分析助手"}, # Anthropic 没有 system role
{"type": "image", "source": {"type": "url", "url": "..."}}
]
},
{
"role": "user",
"content": "这张图片里有什么?"
}
]
#场景1:纯文本对话(99% 的日常使用)
system_msg = SystemMessage(content="你是一个助手")
human_msg = HumanMessage(content="你好")
#场景2:需要多模态输入(图片、音频、文档)
# 必须使用内容块列表
system_msg2 = SystemMessage(
content=[
{"type": "text", "text": "你是文档 OCR 专家"},
{"type": "image", "url": "document_page1.png"}
]
)
#场景3:需要缓存控制(高频调用,长系统提示)
# 必须在内容块中设置 cache_control
system_msg3 = SystemMessage(
content=[
{
"type": "text", "text": "3000字系统指令...",
"cache_control": {"type": "ephemeral"}
},
{"type": "text", "text": "当前用户消息"}
]
)
#补充2:cache_control 缓存机制深度解析->提示词缓存
"""
为什么需要提示词缓存?
在你的生产 Agent 中,每次调用都传输相同的 3000 字系统提示,会导致:
高延迟:每次都要处理 3000 个 token
高成本:输入 token 按量计费
资源浪费:LLM 重复处理相同内容
一句话总结:缓存让 LLM 记住重复出现的提示前缀,后续调用只传输变化的部分,大幅降低延迟和成本。
┌─────────────────────────────────────────────────────────────────┐
│ 第一次调用(缓存写入) │
├─────────────────────────────────────────────────────────────────┤
│ 请求内容: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 系统提示(3000字)+ cache_control 标记 │ │
│ │ 用户消息:"分析这份合同" │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Claude API: │ │
│ │ 1. 处理完整提示(耗时) │ │
│ │ 2. 将"系统提示"写入缓存(断点位置 = 标记处) │ │
│ │ 3. 返回响应 + 缓存元数据 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 响应中的缓存指标: │
│ {"cache_creation_input_tokens": 3000, ← 写入缓存的 token 数 │
│ "cache_read_input_tokens": 0, ← 命中缓存的 token 数 │
│ "input_tokens": 3020} │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 第二次调用(缓存命中) │
├─────────────────────────────────────────────────────────────────┤
│ 请求内容(相同 thread_id): │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 系统提示(3000字)+ cache_control 标记 ← 命中缓存! │ │
│ │ 用户消息:"换个角度分析..."(只有这 20 字是新内容) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Claude API: │ │
│ │ 1. 检测到缓存命中 │ │
│ │ 2. 直接从缓存读取系统提示(不重新计算) │ │
│ │ 3. 只处理新的用户消息 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 响应中的缓存指标: │
│ {"cache_creation_input_tokens": 0, ← 本次未写入新缓存 │
│ "cache_read_input_tokens": 3000, ← 命中缓存的 token 数 │
│ "input_tokens": 20} ← 实际处理的 token 数 │
└─────────────────────────────────────────────────────────────────┘
"""
a_dict={"cache_control": {"type": "ephemeral"}}
#ephemeral:adj 短暂的;瞬息的
# 1. cache_control 到底是什么?
# 一句话解释:cache_control 是一个提示标记,告诉 Anthropic 的 API:“从这里开始往后的内容,请帮我缓存起来,下次调用时别再重新计算了。”
# 它不是缓存“这条消息”,而是缓存从消息开头到这个标记处为止的所有内容
# 使用方式:多个缓存断点(推荐)
# 生产最佳实践:在静态内容块后都设置缓存
content = [
# 块1:系统角色(短文本,不缓存也行)
{"type": "text", "text": "你是数据分析助手"},
# 块2:工具定义(静态,建议缓存)
{"type": "text", "text": "工具1: query_database...\n工具2: analyze_data...",
"cache_control": {"type": "ephemeral"}},
# 块3:系统规则(静态,建议缓存)
{"type": "text", "text": "规则: 1. 优先使用缓存...\n2. 遵循数据安全规范...",
"cache_control": {"type": "ephemeral"}},
# 块4:用户消息(动态,不设缓存)
{"type": "text", "text": "用户输入"}
]
""":重要性:
场景 输入Token 费用(按 $3/M 输入计)
无缓存:每次传 3000 字系统提示 + 20 字用户消息 3020 $0.00906
有缓存:仅传 20 字用户消息 20 $0.00006
每次调用节省 - $0.009(99% 成本降低)
对于每天 10 万次调用的生产系统:
无缓存:$906/天
有缓存:$6/天
年节省:约 $328,500
"""
#补充3:
#深度解答:消息简写格式 ("role", "content") 的完整解析
#这个格式是 LangChain 提供的便捷消息创建方式。让我详细解释它的机制、作用和与标准格式的关系。
"""
一句话解释:消息简写格式是为了减少代码冗余,让你用元组列表快速构建对话历史,而无需手动实例化 SystemMessage、HumanMessage、AIMessage 类。
在生产场景中,你可能需要:
从数据库加载对话历史(存储为 JSON)
从 API 接收用户消息(格式可能是 {role, content})
快速测试不同的对话流程
手动创建消息对象代码冗长:
"""
from langchain_core.messages import *
# 冗长的方式
messages = [
SystemMessage(content="你是一个边塞诗人。"),
HumanMessage(content="写一首唐诗。"),
AIMessage(content="锄禾日当午..."),
HumanMessage(content="按照你上一个回复的格式,在写一首唐诗。")
]
# 使用简写格式,一行搞定:
# 简洁的方式
messages1 = [
("system", "你是一个边塞诗人。"),
("human", "写一首唐诗。"),
("ai", "锄禾日当午..."),
("human", "按照你上一个回复的格式,在写一首唐诗。")
]
# 数据流转过程:
# 你的输入:简写元组
input_messages = [
("system", "你是一个边塞诗人。"),
("human", "写一首唐诗。"),
("ai", "锄禾日当午..."),
("human", "按照你上一个回复的格式,在写一首唐诗。")
]
# ↓ LangChain 内部转换 ↓:消息对象
converted_messages = [
SystemMessage(content="你是一个边塞诗人。"),
HumanMessage(content="写一首唐诗。"),
AIMessage(content="锄禾日当午..."),
HumanMessage(content="按照你上一个回复的格式,在写一首唐诗。")
]
# ↓ 发送给 LLM API(OpenAI 格式)↓ :字典格式
openai_messages = [
{"role": "system", "content": "你是一个边塞诗人。"},
{"role": "user", "content": "写一首唐诗。"},
{"role": "assistant", "content": "锄禾日当午..."},
{"role": "user", "content": "按照你上一个回复的格式,在写一首唐诗。"}
]