把 LangGraph 客服智能体迁移到 Amazon Bedrock AgentCore:从托管运行到模型驱动规划

2026-09-04 30 预计阅读时间: 1 分钟
来源: aws.amazon.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

一个能在 Notebook 中回答问题的智能体,距离生产系统还差一整套运行基础设施:身份认证、工具接入、会话记忆、故障恢复、可观测性,以及可重复的部署流程。

迁移这类工作负载时,一次性重写通常会放大风险。更稳妥的路径分成两个阶段:先保留 LangGraph 的业务编排,把运行、工具和记忆迁移到 Amazon Bedrock AgentCore;待生产边界稳定后,再用 Strands Agents 引入模型驱动规划,逐步减少手写工作流节点。

第一阶段:保留工作流,替换运行基础设施

第一阶段的目标不是改变智能体如何思考,而是改变它在哪里运行、怎样调用工具,以及如何保存跨轮次状态。

可以把现有系统拆成四层:

层次 迁移前的常见实现 第一阶段的目标
编排 LangGraph 节点、边和条件路由 暂时保留
运行 Notebook、脚本或自建 API 容器 AgentCore Runtime
工具 直接调用内部 HTTP API AgentCore Gateway
状态 进程内对象、自建数据库或检查点 AgentCore Memory

这种拆分保留了已经验证过的客服流程。例如,“识别意图 → 查询订单 → 判断是否升级人工”的图结构可以继续工作,但节点不再直接持有数据库凭据,也不再依赖当前进程中的聊天记录。

Runtime:把一次实验变成可管理的执行单元

进入 Runtime 前,入口函数应当具备清晰的请求和响应契约。不要让部署入口依赖 Notebook 全局变量,也不要把 HTTP 请求对象一路传入业务节点。一个合适的边界至少包含:

  • session_id:标识一次连续会话。
  • user_id:用于授权和记忆隔离,不能由不可信客户端任意冒充。
  • message:当前用户输入。
  • request_id:用于重试时去重。
  • 结构化响应:文本之外,还应允许返回工具调用状态、升级人工标记和错误码。

Runtime 托管了执行环境,并不意味着应用天然具备幂等性。订单取消、退款或工单创建等有副作用的操作,仍应使用 request_id 或业务幂等键防止重复执行。

Gateway:把工具调用变成受控接口

在 Notebook 原型中,工具往往只是一个调用内部接口的 Python 函数。生产环境需要进一步回答:谁能调用、参数是否合法、超时是多少、哪些字段可以进入日志、失败后是否允许重试。

迁移到 Gateway 时,应先整理工具契约,而不是把任意内部 API 原样暴露给模型。以订单查询工具为例,只开放客服任务需要的字段:

{
  "name": "get_order_status",
  "description": "Return the fulfillment status of an order owned by the authenticated customer.",
  "input": {
    "order_id": "string"
  },
  "output": {
    "status": "processing | shipped | delivered | cancelled",
    "estimated_delivery": "string | null"
  }
}

工具描述应说明使用条件,但授权必须由服务端执行。模型决定“要调用哪个工具”,不能等同于模型决定“有权读取哪个订单”。Gateway 后端应根据可信身份校验订单归属。

Memory:保存有价值的状态,而不是保存所有内容

客服智能体通常同时需要两种记忆:

  • 短期状态:当前问题、最近一次工具结果、尚未确认的操作。
  • 长期信息:用户允许保存的偏好、产品信息或历史问题摘要。

不要默认把完整对话永久写入长期记忆。支付信息、身份凭证、访问令牌以及不必要的个人信息应该在写入前过滤,并设置保留期限。还要定义删除流程,否则“记住用户”很容易变成数据治理负担。

用适配器降低迁移风险

下面是一个可直接运行的本地示例。它不调用真实 AWS 服务,而是明确模拟 Runtime、Gateway 和 Memory 的边界。实际迁移时,可以保留 handle_request,将三个本地适配器替换成 AgentCore 对应 SDK 或服务调用;具体 SDK 方法和部署参数应以项目采用的版本为准。

将代码保存为 agent_adapter.py,使用 Python 3.10 或更高版本运行:

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Protocol


class Memory(Protocol):
    def load(self, user_id: str, session_id: str) -> list[dict[str, str]]: ...
    def append(self, user_id: str, session_id: str, role: str, text: str) -> None: ...


class Gateway(Protocol):
    def call(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]: ...


class LocalMemory:
    def __init__(self) -> None:
        self._sessions: dict[tuple[str, str], list[dict[str, str]]] = {}

    def load(self, user_id: str, session_id: str) -> list[dict[str, str]]:
        return list(self._sessions.get((user_id, session_id), []))

    def append(self, user_id: str, session_id: str, role: str, text: str) -> None:
        key = (user_id, session_id)
        self._sessions.setdefault(key, []).append({"role": role, "text": text})


class LocalGateway:
    def call(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
        if tool_name != "get_order_status":
            raise ValueError(f"Tool is not allowed: {tool_name}")
        if arguments.get("order_id") != "A-100":
            return {"found": False}
        return {
            "found": True,
            "status": "shipped",
            "estimated_delivery": "2026-04-12",
        }


@dataclass(frozen=True)
class Request:
    user_id: str
    session_id: str
    request_id: str
    message: str


def langgraph_workflow(
    request: Request,
    history: list[dict[str, str]],
    gateway: Gateway,
) -> str:
    # 迁移第一阶段可在这里调用现有的 LangGraph graph.invoke(...)。
    if "A-100" in request.message:
        order = gateway.call("get_order_status", {"order_id": "A-100"})
        if order.get("found"):
            return (
                f"订单 A-100 当前为 {order['status']},"
                f"预计送达日期为 {order['estimated_delivery']}。"
            )
    return f"我还需要订单号。当前会话已有 {len(history)} 条历史消息。"


def handle_request(payload: dict[str, str], memory: Memory, gateway: Gateway) -> dict[str, str]:
    required = {"user_id", "session_id", "request_id", "message"}
    missing = required - payload.keys()
    if missing:
        raise ValueError(f"Missing fields: {sorted(missing)}")

    request = Request(**{key: payload[key] for key in required})
    history = memory.load(request.user_id, request.session_id)
    memory.append(request.user_id, request.session_id, "user", request.message)

    answer = langgraph_workflow(request, history, gateway)
    memory.append(request.user_id, request.session_id, "assistant", answer)
    return {"request_id": request.request_id, "answer": answer}


if __name__ == "__main__":
    memory = LocalMemory()
    gateway = LocalGateway()
    response = handle_request(
        {
            "user_id": "customer-42",
            "session_id": "support-7",
            "request_id": "req-001",
            "message": "请查询订单 A-100",
        },
        memory,
        gateway,
    )
    print(response)

运行命令:

python3 agent_adapter.py

真实接入时,建议保持接口不变并分别替换实现:

memory: Memory = AgentCoreMemoryAdapter(...)
gateway: Gateway = AgentCoreGatewayAdapter(...)
result = handle_request(runtime_event, memory, gateway)

这里的 AgentCoreMemoryAdapterAgentCoreGatewayAdapter 是项目侧适配器名称,并非假定存在的官方类。它们的作用是隔离 SDK 版本、认证方式和序列化格式,让 LangGraph 工作流不必同时承担基础设施迁移。

第二阶段:用 Strands Agents 接管规划

第一阶段稳定后,系统仍然由 LangGraph 显式规定节点和路由。第二阶段可以把适合动态决策的部分迁移到 Strands Agents,让模型根据用户目标、工具描述和当前上下文选择下一步动作。

适合模型驱动规划的任务通常具有这些特征:

  • 工具组合会随问题变化,难以列出固定路径。
  • 大多数工具是只读或低风险操作。
  • 调用失败后可以安全重试或请求用户补充信息。
  • 已有离线评测集,可以衡量答案和工具选择是否退化。

并非所有 LangGraph 节点都应删除。退款、取消订单、修改地址等高风险动作仍适合确定性流程:先校验资格,再展示影响,获得明确确认后执行。可行的混合模式是让 Strands 负责规划和信息收集,由受约束的服务端工作流执行最终写操作。

迁移时可以按意图逐步切换,而不是一次替换整个图:

def route_agent(intent: str, request: dict) -> dict:
    deterministic = {"refund", "cancel_order", "change_address"}
    if intent in deterministic:
        return existing_langgraph.invoke(request)
    return strands_agent.invoke(request)

这段代码是迁移模式示例,invoke 的实际签名需要根据所用 LangGraph、Strands Agents 和 AgentCore SDK 版本调整。灰度阶段应记录路由结果、工具序列、最终答案和人工接管情况,以便对两套实现进行对照评估。

上线前需要验证什么

迁移的完成标准不应只是“AgentCore 上能够返回文本”。至少需要检查以下项目:

  • 身份边界:客户端提供的 user_id 是否会被可信身份覆盖或验证。
  • 工具权限:每个工具是否按最小权限访问后端,是否校验资源归属。
  • 幂等性:Runtime 重试后,有副作用的操作是否只执行一次。
  • 记忆治理:写入前是否过滤敏感数据,是否有 TTL、删除和租户隔离策略。
  • 超时预算:模型、Gateway 和后端 API 是否共享明确的端到端延迟预算。
  • 可观测性:是否能够用 request_id 串联运行、工具调用和错误,但不记录敏感正文。
  • 质量评测:是否覆盖正常查询、工具失败、提示注入、越权请求和人工升级。
  • 回退路径:Strands 规划异常时,是否能回到确定性流程或转交人工。

这条两阶段路线的核心价值在于分离风险:先让 AgentCore 承担 Runtime、Gateway 和 Memory 对应的生产职责,同时保留熟悉的 LangGraph 行为;再用 Strands Agents 改造规划层。团队因此可以分别衡量平台迁移和智能体行为变化,而不是在一次发布中同时调试两类问题。


相关推荐