Week 3 -- Day 4:生产级部署

Day 4:生产级部署

引言:从开发到生产的最后一公里

前三天的学习让我们掌握了 LangGraph 的核心能力,从单个 Agent 的 ReAct 循环到复杂的多智能体协作系统,这些知识已经足以构建功能完备的 LLM 应用。然而一个在本地 python main.py 下运行正常的 Agent 距离真正的生产级服务还有一段关键的距离。这段距离不体现在功能性上,而体现在系统面对真实世界不可预测性时的韧性。网络抖动可能导致 API 调用超时,上游服务的瞬时故障可能让工具执行失败,并发请求可能触发速率限制,不同客户端框架之间的工具复用需要一套标准化的协议,而团队协作和快速迭代则需要一个让非技术人员也能参与 Agent 构建的平台。Day 4 将聚焦于这三个弥合开发与生产差距的关键主题,错误处理与重试机制为 Agent 注入容错能力,MCP 协议让工具跨框架跨平台复用成为可能,而 LangSmith Fleet 则提供了一套从创建到部署到监控的全生命周期管理平台。

错误处理与重试:让 Agent 学会自我修复

在开发环境中,你可能会习惯性地在 try-except 块里包裹 Agent 调用,简单地打印错误信息然后重启整个流程。但在生产环境中,一个任务的中间步骤可能有数十步,仅仅因为一次临时性的网络超时就丢弃所有上下文重新开始,这在经济和用户体验上都是不能接受的。LangChain 和 LangGraph 提供了多层级的错误处理机制,从最底层的 HTTP 重试到顶层的 Agent 自愈逻辑,它们协同工作让系统具备优雅降级的能力。

最基础的防护层是 RunnableConfig 中的 timeoutmax_retries 配置。timeout 控制单次模型调用或工具执行的最长等待时间,单位为秒,超过这个时间会抛出 TimeoutErrormax_retries 指定在遇到可重试错误时的最大重试次数。这个配置通过 config 参数传入 invoke()ainvoke()

from langchain_core.runnables import RunnableConfig

# 配置超时和重试
config = RunnableConfig(
    max_retries=3,
    timeout=30  # 30 秒超时
)

# 传入 Agent 调用
result = agent.invoke(
    {"messages": [{"role": "user", "content": "帮我分析这份报告"}]},
    config=config
)

max_retries 并非对所有异常都自动生效,它主要针对的是底层 HTTP 客户端识别为可重试的错误类型(网络超时、429 限流、5xx 服务端错误),对于业务逻辑错误或工具返回的语义错误,重试不会自动触发。因此仅仅依赖 RunnableConfig 还不够,你通常需要在更上层构建自定义的错误处理策略。

在 LangGraph 图的层面,你可以为每个节点单独配置重试策略。通过 RetryPolicy 指定最大重试次数、初始等待时间和退避倍增系数,让 LangGraph 在节点执行失败时自动按照指数退避节奏重试:

from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langgraph.types import RetryPolicy

def call_model(state: MessagesState):
    """调用 LLM 的节点,可能因网络波动失败。"""
    response = model.invoke(state["messages"])
    return {"messages": [response]}

# 为节点绑定重试策略:最多重试 3 次,初始等待 1 秒,指数退避系数 2x
graph = StateGraph(MessagesState)
graph.add_node(
    "call_model",
    call_model,
    retry=RetryPolicy(
        max_attempts=3,
        initial_interval=1.0,
        backoff_factor=2
    )
)

initial_interval=1.0backoff_factor=2 的组合意味着如果节点失败,LangGraph 会等待 1 秒后重试,如果再次失败则等待 2 秒,最后等待 4 秒,形成 $1 \to 2 \to 4$ 秒的指数退避节奏。这是处理速率限制和临时故障的最佳实践。你还可以通过 retry_on 参数指定只在特定异常类型时触发重试:

from langgraph.types import RetryPolicy

graph.add_node(
    "call_model",
    call_model,
    retry=RetryPolicy(
        max_attempts=3,
        initial_interval=1.0,
        backoff_factor=2,
        retry_on=(TimeoutError, ConnectionError)  # 仅在这两种异常时重试
    )
)

这样做可以避免对不可恢复的错误(如 API 密钥无效、权限不足)做无谓的重试。retry_on 接收一个异常类型或异常元组,只有匹配的异常才会触发重试逻辑,其他异常照常向上传播。

当重试机制无法解决问题时,系统需要兜底策略。LangChain 提供了 with_fallbacks() 方法,允许你为任何 Runnable 绑定后备方案。最常见的场景是当一个模型不可用时自动切换到备用模型:

from langchain.chat_models import init_chat_model

# 主模型
primary_model = init_chat_model("Qwen/Qwen3.6-27B", model_provider="openai")

# 备用模型
fallback_model = init_chat_model("deepseek-ai/DeepSeek-V4-Pro", model_provider="openai")

# 将备用模型绑定到主模型:主模型失败时自动降级
robust_model = primary_model.with_fallbacks([fallback_model])

# 使用容错模型创建 Agent
agent = create_agent(robust_model, tools=[...])

with_fallbacks() 的语义是按顺序尝试,当前一个 Runnable 抛出异常时自动尝试下一个。你也可以传入多个后备方案形成降级链,甚至将在线工具与离线缓存串联,API 查询失败时自动回退到本地数据:

from langchain_core.tools import tool

@tool
def online_search(query: str) -> str:
    """在线搜索,可能因网络问题失败。"""
    # ... 调用搜索引擎 API
    pass

@tool
def cached_search(query: str) -> str:
    """从本地缓存中查询,作为后备方案。"""
    # ... 查询本地缓存
    pass

# 在线搜索失败时自动降级到缓存
robust_search = online_search.with_fallbacks([cached_search])

在生产环境中还有一个重要模式是自定义错误回调。你可以在 Agent 的中间件层拦截异常,根据异常类型执行不同的恢复策略:

import time
from langchain.agents.middleware import wrap_tool_call

@wrap_tool_call
def error_handling_middleware(request, handler):
    """自定义错误处理中间件:拦截不同异常类型并执行对应恢复策略。"""
    try:
        return handler(request)
    except RateLimitError:
        # 遇到限流,等待 60 秒后重试
        time.sleep(60)
        return handler(request)
    except TimeoutError:
        # 超时返回友好提示,不中断整个流程
        return "服务响应超时,请稍后重试。当前使用缓存数据作为参考。"
    except Exception as e:
        # 其他未知异常,记录日志后返回降级响应
        print(f"工具 {request.tool_call['name']} 执行失败: {e}")
        return f"工具执行遇到错误,已记录日志。请尝试其他方式获取信息。"

model = init_chat_model("Qwen/Qwen3.6-27B", model_provider="openai")
agent = create_agent(
    model=model,
    tools=[...],
    middleware=[error_handling_middleware]
)

这段代码的核心在于异常类型的判断分支。RateLimitError 是可恢复的,等待冷却时间后直接重试。TimeoutError 说明上游服务暂时不可达,返回一个带有降级说明的字符串让模型继续处理。最外层的 Exception 兜底所有未知错误,记录日志后返回降级响应,确保 Agent 不会因为某个工具的偶发故障而整体崩溃。

在 MCP 工具层面,自 langchain-mcp-adapters v0.3.0 起引入了更智能的错误处理。传统上 MCP 工具执行失败会直接抛出 ToolException 导致整个运行中断,而新版本默认行为是将工具执行错误包装为带有 status="error"ToolMessage 返回给模型,让模型看到错误信息后自行决定如何纠正:

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient(
    {
        "math": {
            "transport": "http",
            "url": "http://localhost:3000/mcp"
        }
    }
)
model = init_chat_model("Qwen/Qwen3.6-27B", model_provider="openai")
# handle_tool_errors=True(默认):错误以 ToolMessage 返回,Agent 可自愈
tools = await client.get_tools()
agent = create_agent(model=model, tools=tools)
# 工具执行出错时,模型会看到 status="error" 的消息并自行调整策略

# 如果希望错误立即抛出(严格事务场景),显式关闭:
tools_strict = await client.get_tools(handle_tool_errors=False)

这种设计让 Agent 具备了"自愈"能力,模型看到工具返回的错误消息后,可以尝试换一种参数调用、选择另一个工具、或者向用户说明情况并等待人工介入,而不是让整个流程崩溃。值得注意的是传输层故障和会话级别的错误始终会抛出异常,不受 handle_tool_errors 设置的影响。

以上我们从 RunnableConfig 一路走到 MCP 工具级错误处理,形成了一个从底层 HTTP 重试到顶层 Agent 自愈的完整容错链条。但错误处理解决的是"出了问题之后怎么办",而另一个同样重要的生产级挑战是,如何让一套工具定义被多个平台复用。为每个平台(LangChain、Cursor、Claude Desktop)各写一套工具适配代码,不仅开发成本翻倍,维护时更是噩梦。MCP 协议正是为解决这个问题而生的——它让工具的定义与消费端彻底解耦。

MCP:一次开发,多平台复用的工具协议

Model Context Protocol 是由 Anthropic 提出并开源的一个标准化协议,它定义了一套统一的接口,让 LLM 应用与外部工具、数据源之间的通信变得标准化。在 MCP 出现之前,如果你想让一个工具同时服务于 LangChain Agent、Cursor 编辑器中的 AI 助手和 Claude Desktop 桌面应用,你通常需要为每个平台分别编写适配代码。MCP 的核心理念是让工具的定义和实现与消费端解耦,你只需编写一个 MCP 服务器,所有支持 MCP 协议的客户端都可以直接使用它的能力。

要创建一个 MCP 服务器,最便捷的方式是使用 FastMCP 库。你通过 @mcp.tool() 装饰器将 Python 函数注册为 MCP 工具,函数的签名和 docstring 会自动成为工具的输入 schema 和描述信息:

# math_server.py
from fastmcp import FastMCP

mcp = FastMCP("Math")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

@mcp.tool()
def multiply(a: int, b: int) -> int:
    """Multiply two numbers"""
    return a * b

if __name__ == "__main__":
    mcp.run(transport="stdio")

这个简单的数学服务器通过 stdio 协议对外提供服务。在 LangChain 侧,你使用 MultiServerMCPClient 连接它并加载工具:

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient(
    {
        "math": {
            "transport": "stdio",
            "command": "python",
            "args": ["/path/to/math_server.py"],
        }
    }
)

model = init_chat_model("Qwen/Qwen3.6-27B", model_provider="openai")
tools = await client.get_tools()
agent = create_agent(model=model, tools=tools)

response = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "what's (3 + 5) x 12?"}]}
)

MultiServerMCPClient 的核心价值在于它支持同时连接多个 MCP 服务器,每个服务器可以有不同的传输协议和连接参数。以下是一个连接本地数学服务器(stdio)和远程天气服务器(HTTP)的完整示例:

# weather_server.py —— 部署在远程的 HTTP MCP 服务
from fastmcp import FastMCP

mcp = FastMCP("Weather")

@mcp.tool()
async def get_weather(location: str) -> str:
    """Get weather for location."""
    return f"It's always sunny in {location}!"

if __name__ == "__main__":
    mcp.run(transport="http")  # 默认监听 8000 端口

天气服务器使用 transport="http" 而非 "stdio",这说明它是作为一个独立的网络服务运行,可以被任何能发送 HTTP 请求的客户端访问。在生产环境中你会把它部署到一台独立的服务器上,用 Nginx 做反向代理,通过 headers 字段配置 API 密钥鉴权。现在我们来看客户端如何同时消费这两个异构的 MCP 服务:

# 客户端:同时连接两个 MCP 服务器
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain.agents import create_agent

client = MultiServerMCPClient(
    {
        "math": {
            "transport": "stdio",
            "command": "python",
            "args": ["/path/to/math_server.py"],
        },
        "weather": {
            "transport": "http",
            "url": "http://localhost:8000/mcp",
            "headers": {  # HTTP 传输支持自定义认证头
                "Authorization": "Bearer YOUR_TOKEN",
            }
        }
    }
)

model = init_chat_model("Qwen/Qwen3.6-27B", model_provider="openai")
tools = await client.get_tools()
agent = create_agent(model=model, tools=tools)

math_response = await agent.ainvoke(
    {"messages": "what's (3 + 5) x 12?"}
)
weather_response = await agent.ainvoke(
    {"messages": "what is the weather in nyc?"}
)

这段客户端代码揭示了 MultiServerMCPClient 的核心设计哲学 协议透明math 服务器通过 stdio 协议在本地子进程中运行,weather 服务器通过 http 协议在远程端口上运行,但 client.get_tools() 把它们统一加载为 BaseTool 列表,Agent 完全不知道也不关心每个工具来自哪里。headers 字段仅在 http 传输下生效,它会在每个 HTTP 请求中携带认证头,让远程服务器验证客户端的身份。这种统一接口让工具消费变得极其简单——无论工具是本地的还是远程的,无论它是 Python 写的还是 TypeScript 写的,只要遵循 MCP 协议,LangChain Agent 就能直接调用。

对于并发请求较多或多轮对话的场景,你可以显式管理 MCP 会话的生命周期。默认情况下 MultiServerMCPClient无状态的,每次工具调用都会创建新的 ClientSession、执行工具、然后清理。如果你需要在多轮交互中保持会话状态(比如服务器端维护了连接级别的上下文),可以使用 client.session() 进入有状态模式:

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools

client = MultiServerMCPClient({
    "math": {
        "transport": "stdio",
        "command": "python",
        "args": ["/path/to/math_server.py"],
    }
})

# 显式管理会话:在 async with 块内会话保持存活
async with client.session("math") as session:
    model = init_chat_model("Qwen/Qwen3.6-27B", model_provider="openai")
    tools = await load_mcp_tools(session)
    agent = create_agent(model=model, tools=tools)
    # 该上下文内的所有工具调用共享同一个 session
    response = await agent.ainvoke(
        {"messages": "Calculate (3 + 5) x 12 and then multiply by 2"}
    )

langchain-mcp-adapters 在 v0.3.0 中引入了一套强大的拦截器系统,它让 LangGraph 的运行时信息(状态、存储、上下文)能够穿透到 MCP 工具调用中。由于 MCP 服务器作为独立进程运行,它天然无法访问 LangGraph 的内部状态,而拦截器通过"洋葱模型"桥接了这两个世界。以下是一个在工具调用前注入用户凭证、在工具调用后更新图状态的完整示例:

from dataclasses import dataclass

@dataclass
class UserContext:
    user_id: str
    api_key: str

async def inject_user_context(request, handler):
    """在 MCP 工具调用前,将用户凭证注入到参数中。"""
    runtime = request.runtime  # 获取 LangGraph 运行时上下文
    user_id = runtime.context.user_id
    api_key = runtime.context.api_key

    # 使用 override() 创建新的请求副本,附加用户信息
    modified_request = request.override(
        args={**request.args, "user_id": user_id}
    )
    return await handler(modified_request)

client = MultiServerMCPClient(
    {
        "weather": {
            "transport": "http",
            "url": "http://localhost:8000/mcp",
        }
    },
    tool_interceptors=[inject_user_context],  # 注册拦截器
)

model = init_chat_model("Qwen/Qwen3.6-27B", model_provider="openai")
tools = await client.get_tools()
agent = create_agent(model=model, tools=tools, context_schema=UserContext)

# 调用时传入用户上下文
result = await agent.ainvoke(
    {"messages": [{"role": "user", "content": "Search my orders"}]},
    context={"user_id": "user_123", "api_key": "sk-..."}
)

request.override() 的设计遵循不可变模式,它不会修改原始请求对象,而是创建一个新的副本,这在拦截器链式调用中保证了每个拦截器看到的都是干净的原始数据。多个拦截器按列表顺序层层包裹,形成 outer → inner → tool → inner → outer 的洋葱调用顺序。拦截器还可以返回 Command 对象来更新图状态或控制执行流向,比如工具执行完成后自动跳转到另一个 Agent 节点:

from langgraph.types import Command

async def handle_task_completion(request, handler):
    """工具执行完成后,更新任务状态并跳转到汇总节点。"""
    result = await handler(request)

    if request.name == "submit_order":
        return Command(
            update={"messages": [result], "task_status": "completed"},
            goto="summary_agent",  # 执行流跳转到汇总 Agent
        )

    return result

这个拦截器展示了一种强大的模式,工具执行结果驱动图的路由。当 submit_order 工具执行完成后,拦截器不满足于仅仅返回结果,而是通过 Command(update=..., goto="summary_agent") 同时更新了 task_status 状态字段并强制跳转到汇总节点。在 Day 3 讨论多智能体协作时,我们说过 Handoffs 模式通过状态变量驱动 Agent 切换,这里的 Command(goto=...) 正是实现这种切换的低层原语,工具调用返回的那一刻,控制流就可以被精确地导向下一个应该执行的节点,无需额外的条件边来判断。如果希望直接终止图执行,只需将 goto 设为 END

除了工具之外,MCP 协议还支持资源(Resources)和提示(Prompts)两种能力,这让 MCP 服务器不仅仅是一个"函数商店",更是一个完整的上下文提供者。资源允许服务器暴露可读取的数据,比如配置文件、模板文件、数据库视图等,LangChain 通过 Blob 对象统一处理文本和二进制内容。提示则允许服务器将经过反复验证的 prompt 模板注册为命名资源,客户端按需加载并填入动态参数。以下代码演示了这两种能力的用法:

# 加载 MCP 服务器上的资源
client = MultiServerMCPClient({...})
blobs = await client.get_resources("server_name")
for blob in blobs:
    print(f"URI: {blob.metadata['uri']}, MIME type: {blob.mimetype}")
    print(blob.as_string())  # 文本内容直接输出

# 按 URI 加载特定资源
blobs = await client.get_resources("server_name", uris=["file:///path/to/file.txt"])

# 加载 MCP 服务器上的提示模板
messages = await client.get_prompt("server_name", "code_review",
    arguments={"language": "python", "focus": "security"}
)
# 返回的消息对象可以直接用于 Agent 调用

get_resources() 有两种调用方式:不带 uris 参数时加载服务器上所有已注册的资源,传入 uris 列表时只拉取指定 URI 的内容。每个资源以 Blob 形式返回,通过 metadata 可以读取 URI 和 MIME 类型,通过 as_string() 将文本内容解码为字符串。get_prompt() 则按名称加载提示模板,arguments 字典中的键值对会被填入模板的占位符位置,返回的是可以直接传给 Agent 的消息列表。想象一个典型场景:你的团队维护了一套标准的代码审查提示模板,通过 MCP 服务器发布后,所有 Agent——无论在 LangChain 里、Cursor 里还是 Claude Desktop 里——都可以统一加载使用,模板的修改只需在服务器端进行一次。

这三种能力(工具、资源、提示)合在一起,让 MCP 成为了一个名副其实的"上下文协议"——服务器可以同时向 Agent 提供可调用的函数、可读取的数据和可参考的提示模板,这正是 MCP 名称中"Context"的深意。

LangSmith Fleet:无代码 Agent 的全生命周期平台

说明:本节内容仅作了解。 LangSmith Fleet 是 LangChain 官方的无代码 Agent 平台,作为知识拓展帮助你理解生产级 Agent 平台应该具备哪些能力。真正动手实践时,请关注喵叔HUB,你将获得更贴近实际场景的学习和部署体验。

如果说 MCP 解决了工具的标准化问题,LangSmith Fleet 则代表了 Agent 平台化的另一个方向——从构想到部署的整个过程如何降低门槛、加速迭代。作为知识了解,Fleet(前身 Agent Builder)是一个运行在 LangSmith 上的无代码 Agent 构建和管理平台,它的核心哲学是让构建 Agent 变得像写一封邮件一样简单,在对话中直接用自然语言描述需求,AI 辅助生成指令和配置,然后即时测试、迭代,一键部署到 Slack、邮件等渠道中。

Fleet 提供了一套完整的 Agent 生命周期管理功能,以下五个核心能力值得了解。第一是工具集成,Fleet 内置了 Gmail、Slack、Google Calendar、GitHub 等数十种常用服务的即用型工具,同时支持通过 MCP 协议接入远程工具服务器,在 Web 界面中可视化配置。第二是频道调度,Agent 可以被动等待用户提问,也可以通过定时调度自动执行周期性任务,还可以响应 Slack、邮件等外部事件。第三是记忆系统,分为线程级短期记忆和基于文件持久化的长期记忆两层,Agent 可以跨会话积累知识。第四是人类审批,高风险操作在调用前必须经过人工确认、修改或拒绝。第五是子Agent 与技能系统,复杂任务可拆分给多个专业化子Agent 协作完成。

对于开发者而言,理解 Fleet 这类平台的意义在于把握行业趋势——Agent 开发正在从纯代码走向"低代码 + 代码"的混合模式。

练习任务

  • 为 Agent 添加完整的错误处理与重试机制
  • 尝试 MCP 协议构建可复用工具
  • 了解 LangSmith Fleet 的核心功能

考核点 ✅

  1. 错误处理代码:提交含 max_retries 和自定义错误回调的 Agent 代码
  2. MCP 工具:提交使用 MCP 协议封装的工具示例代码,说明跨框架复用原理
  3. Fleet 报告:口头总结 LangSmith Fleet 的 5 个核心功能
  4. 超时配置:提交含 timeout 配置的 Runnable Config 代码,演示超时后的行为
posted @ 2026-07-04 23:58  喵叔哟  阅读(1)  评论(0)    收藏  举报