MCP Server 终于能"记住"用户了:AgentCore 有状态会话实战

13 阅读3分钟

MCP Server 终于能"记住"用户了:AgentCore 有状态会话实战

搞 MCP 的都知道一个蛋疼的问题:server 不记状态。用户上一轮说了啥,下一轮完全不知道。

上周亚马逊云科技给 Bedrock AgentCore 加了有状态 MCP 支持,我第一时间试了试。三个新能力:Elicitation(主动追问)Sampling(反向调 LLM)Progress(进度通知)。跑通之后确实挺惊喜的。

痛点在哪

想象你在做一个订机票的 MCP tool。用户说"去巴黎",然后你需要知道日期、人数、舱位。

原来的做法?要么让用户一次性传完所有参数,要么自己在外面搞个 Redis 存状态。server 本身就是个无状态函数。

现在不用了。ctx.elicit() 让 server 可以在 tool 执行过程中暂停下来问用户,拿到回答再继续。

核心代码

环境:

pip install "fastmcp>=2.10.0" mcp

Server 端关键部分:

from fastmcp import FastMCP, Context
from enum import Enum

mcp = FastMCP("Travel-Booking-Agent")

class TripType(str, Enum):
    BUSINESS = "business"
    LEISURE = "leisure"
    FAMILY = "family"

DESTINATIONS = {
    "paris": {"name": "Paris, France", "flight": 450, "hotel": 180,
              "highlights": ["Eiffel Tower", "Louvre", "Notre-Dame"]},
    "tokyo": {"name": "Tokyo, Japan", "flight": 900, "hotel": 150,
              "highlights": ["Shibuya", "Senso-ji Temple", "Mt. Fuji day trip"]},
}

@mcp.tool()
async def plan_trip(ctx: Context) -> str:
    # 追问目的地
    dest_result = await ctx.elicit(
        message="去哪儿?可选: Paris, Tokyo",
        response_type=str
    )
    if dest_result.action != "accept":
        return "已取消"
    dest = DESTINATIONS.get(dest_result.data.lower().strip())

    # 追问旅行类型
    type_result = await ctx.elicit(
        message="旅行类型?business / leisure / family",
        response_type=TripType
    )

    # 追问天数
    days_result = await ctx.elicit(
        message="待几天?(3-14)",
        response_type=int
    )
    days = max(3, min(14, days_result.data))

    # 进度通知
    for i in range(1, 6):
        await ctx.report_progress(progress=i, total=5)
        await asyncio.sleep(0.4)

    # 反向调 LLM 生成推荐
    try:
        response = await ctx.sample(
            messages=f"Give 3 tips for {dest['name']}, {days} days",
            max_tokens=150
        )
        tips = response.text
    except Exception:
        tips = f"Visit {dest['highlights'][0]}!"

    return f"Booked: {dest['name']}, {days} days\nTips: {tips}"

启动时记得关无状态模式:

mcp.run(
    transport="streamable-http",
    host="0.0.0.0",
    port=8000,
    stateless_http=False  # 必须!
)

客户端怎么接

from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.client.elicitation import ElicitResult
from mcp.types import CreateMessageResult, TextContent

async def elicit_handler(message, response_type, params, ctx):
    print(f"\n>>> {message}")
    response = input("回答: ").strip()
    if response_type == int:
        response = int(response)
    return ElicitResult(action="accept", content={"value": response})

async def sampling_handler(messages, params, ctx):
    return CreateMessageResult(
        role="assistant",
        content=TextContent(type="text", text="试试当地小吃!"),
        model="test-model",
        stopReason="endTurn"
    )

async def progress_handler(progress, total, message):
    pct = int((progress / total) * 100) if total else 0
    print(f"\r  进度: {pct}%", end="", flush=True)
    if progress == total:
        print(" ✅")

transport = StreamableHttpTransport(url="http://localhost:8000/mcp")
client = Client(transport,
    elicitation_handler=elicit_handler,
    sampling_handler=sampling_handler,
    progress_handler=progress_handler)

部署到 AgentCore

pip install bedrock-agentcore-starter-toolkit
agentcore configure -e travel_server.py -p MCP -n travel_agent_demo
agentcore deploy

每个 session 跑在独立 microVM 里,通过 Mcp-Session-Id 维持上下文。

踩坑

  1. stateless_http 默认是 True:不改的话 elicit 直接报错,错误信息还很含糊
  2. Session 会超时:过期后客户端收到 404,要重新 initialize
  3. Sampling handler 不能返回空文本:server 端拼字符串会炸

适用场景

  • 多步骤表单 / 配置向导
  • 审批流程(执行到一半等人确认)
  • 交互式调试
  • 个性化推荐(收集偏好 → LLM 推荐 → 确认下单)

14 个区域可用,亚太有孟买、首尔、新加坡、悉尼、东京。

如果你在搞 AI agent 开发,这个功能值得试试。