LangChain + LangGraph Agent 完全指南:流式传输、结构化输出、提示词缓存

85 阅读22分钟

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)    - 批量调用,处理多个输入
"""

输入字典格式invokestreaminput_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 执行的各个阶段(模型调用前、工具调用前、模型调用后)插入钩子。多个中间件按列表顺序执行,可以修改 requeststate。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 参数,模型返回的 AIMessageadditional_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

  1. 选择合适的流式模式updates 展示步骤,messages 实现打字机,custom 做自定义进度
  2. 生产环境务必精确控制模型参数temperaturemax_retriestimeout
  3. 利用 middleware 实现横切关注点:动态模型选择、统一错误处理、上下文感知提示
  4. 结构化输出让 Agent 可组合:使用 ToolStrategyProviderStrategy
  5. 消息系统三层架构:理解 Role → Message → Content Block 的层次
  6. 提示词缓存(Anthropic)可降低 99% 成本:对于长系统提示的高频场景至关重要
  7. 简写格式提升代码可读性("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": "按照你上一个回复的格式,在写一首唐诗。"}
]