Amazon Bedrock Server-Side Custom Tools 实测:用 Lambda 实现 MCP 协议的服务端工具调用¶
Lab 信息
- 难度: ⭐⭐ 中级
- 预估时间: 45 分钟
- 预估费用: $5(推理 token + Lambda 执行)
- Region: us-east-1
- 最后验证: 2026-03-27
系列文章
本文是 Bedrock Responses API 系列的进阶篇。如果你还没有了解 Responses API 的基础功能(有状态对话、多模型支持),建议先阅读 Bedrock Responses API 实战。
背景¶
Amazon Bedrock 的 Responses API 支持两种 Tool Calling 模式:
- Client-Side:模型返回
function_call→ 客户端执行工具 → 将结果发回模型 → 模型生成最终回复(需 2 次 API 调用) - Server-Side:客户端将 Lambda ARN 传给 Bedrock → Bedrock 直接调用 Lambda → 获取结果 → 传回模型(1 次 API 调用)
Server-Side 模式的优势很明显:减少网络 round-trip、简化客户端代码、工具执行在 AWS 安全边界内。但这个 2026 年 1 月发布的新功能实际表现如何?
本文将回答三个问题:
- 如何用 Lambda 实现 MCP 协议供 Bedrock Server-Side 调用?
- Server-Side vs Client-Side 的实际延迟和代码复杂度差异?
- 多工具、错误处理等边界场景表现如何?
前置条件¶
- AWS 账号,具备 Lambda 和 Bedrock 权限
- AWS CLI v2 已配置
- Python 3.10+,已安装
openaiSDK(pip install openai) - Bedrock API Key(创建方法见文档)
核心概念¶
Server-Side Tool Calling 架构¶
① 请求(含 Lambda ARN)
┌──────────┐ ──────────────────────────> ┌────────────────┐
│ 客户端 │ │ Bedrock │
│ (1次调用) │ <────────────────────────── │ Mantle │
└──────────┘ ⑤ 最终回复 │ │
│ ② tools/list │
│ ③ tools/call │
│ ④ 结果→模型 │
└───────┬────────┘
│ ②③
▼
┌──────────────┐
│ Lambda │
│ (MCP 协议) │
└──────────────┘
Lambda 必须实现的 MCP 协议¶
Bedrock 要求 Lambda 函数实现 JSON-RPC 2.0 格式的 MCP (Model Context Protocol) 两个方法:
| 方法 | 用途 | 调用时机 |
|---|---|---|
tools/list |
返回工具定义(名称、描述、参数 schema) | 每次请求开始时 |
tools/call |
执行具体工具逻辑 | 模型决定调用工具时 |
关键约束¶
| 约束 | 说明 |
|---|---|
| 支持模型 | 仅 GPT-OSS 20B / 120B |
| 工具类型 | function(client-side)或 mcp(server-side) |
| 端点 | bedrock-mantle.{region}.api.aws |
| 认证 | Bedrock API Key(Bearer Token) |
| 计费 | 仅 token 费用,无额外工具调用费 |
动手实践¶
Step 1: 创建 Lambda 执行角色¶
# 创建 IAM 角色
aws iam create-role \
--role-name bedrock-tool-lambda-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}' \
--region us-east-1
# 附加基础执行策略
aws iam attach-role-policy \
--role-name bedrock-tool-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Step 2: 创建 MCP 协议的 Lambda 函数¶
编写 Lambda 函数 lambda_function.py:
import json
def lambda_handler(event, context):
method = event.get("method")
params = event.get("params", {})
request_id = event.get("id")
# Bedrock 首先调用 tools/list 发现可用工具
if method == "tools/list":
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"tools": [
{
"name": "get_most_popular_song",
"description": "Get the most popular song on a radio station",
"inputSchema": {
"type": "object",
"properties": {
"station_name": {
"type": "string",
"description": "Radio station name"
}
},
"required": ["station_name"]
}
}
]
}
}
# Bedrock 在模型决定使用工具后调用 tools/call
elif method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
if tool_name == "get_most_popular_song":
stations = {
"Radio Free Mars": "Starman by David Bowie (1247 plays)",
"Neo Tokyo FM": "Plastic Love by Mariya Takeuchi (892 plays)",
"Cloud Nine Radio": "Blinding Lights by The Weeknd (2103 plays)",
}
station = arguments.get("station_name", "")
result = stations.get(station, "Station not found: " + station)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": result}]
}
}
# 未知方法返回错误
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32601, "message": "Method not found"}
}
部署 Lambda:
# 打包并部署
zip function.zip lambda_function.py
aws lambda create-function \
--function-name bedrock-tool-song \
--runtime python3.12 \
--role arn:aws:iam::<ACCOUNT_ID>:role/bedrock-tool-lambda-role \
--handler lambda_function.lambda_handler \
--zip-file fileb://function.zip \
--timeout 30 \
--region us-east-1
Step 3: 授予 Bedrock 调用权限¶
aws lambda add-permission \
--function-name bedrock-tool-song \
--statement-id bedrock-invoke \
--action lambda:InvokeFunction \
--principal bedrock.amazonaws.com \
--region us-east-1
Step 4: Server-Side 调用(一次 API 请求完成)¶
from openai import OpenAI
client = OpenAI(
api_key="<YOUR_BEDROCK_API_KEY>",
base_url="https://bedrock-mantle.us-east-1.api.aws/v1"
)
resp = client.responses.create(
model="openai.gpt-oss-120b",
max_tool_calls=2, # 重要:限制工具调用次数
tools=[{
"type": "mcp",
"server_label": "song_service",
"connector_id": "arn:aws:lambda:us-east-1:<ACCOUNT_ID>:function:bedrock-tool-song",
"require_approval": "never",
}],
input="What is the most popular song on Radio Free Mars?",
)
# 检查输出
for item in resp.output:
if item.type == "mcp_list_tools":
print("Discovered tools:", [t.name for t in item.tools])
elif item.type == "mcp_call":
print(f"Tool call: {item.name} [{item.status}]")
print(f" Output: {item.output}")
实际输出:
Discovered tools: ['get_most_popular_song']
Tool call: get_most_popular_song [completed]
Output: {"content":[{"type":"text","text":"Starman by David Bowie (1247 plays)"}]}
Tool call: get_most_popular_song [completed]
Output: {"content":[{"type":"text","text":"Starman by David Bowie (1247 plays)"}]}
Tool call: get_most_popular_song [incomplete]
Output: None
注意
GPT-OSS 模型在 server-side 模式下会循环调用工具,不会生成最终文本回复。max_tool_calls 参数用于限制循环次数。详见踩坑记录。
Step 5: Client-Side 对比(两次 API 请求)¶
import json
# Step 1: 让模型决定是否调用工具
resp1 = client.responses.create(
model="openai.gpt-oss-120b",
tools=[{
"type": "function",
"name": "get_most_popular_song",
"description": "Get the most popular song on a radio station",
"parameters": {
"type": "object",
"properties": {
"station_name": {"type": "string"}
},
"required": ["station_name"]
}
}],
input="What is the most popular song on Radio Free Mars?",
)
# Step 2: 客户端执行工具,发送结果
for item in resp1.output:
if item.type == "function_call":
tool_result = "Starman by David Bowie (1247 plays this week)"
resp2 = client.responses.create(
model="openai.gpt-oss-120b",
previous_response_id=resp1.id,
input=[{
"type": "function_call_output",
"call_id": item.call_id,
"output": tool_result
}],
)
print("Final answer:", resp2.output_text)
输出:
Final answer: The most popular song on Radio Free Mars right now is
**"Starman" – David Bowie**, which has been played 1,247 times this week.
测试结果¶
Server-Side vs Client-Side 对比¶
| 维度 | Server-Side (MCP) | Client-Side (function) |
|---|---|---|
| API 调用次数 | 1 次 | 2 次 |
| 总延迟 | 2.84s (120b) / 1.98s (20b) | 1.61s (1.06 + 0.55) |
| 最终文本输出 | ❌ 无(模型循环调用) | ✅ 完整文本回复 |
| 代码行数 | ~10 行 | ~25 行 |
| 工具执行位置 | AWS 服务端 | 客户端本地 |
多工具发现测试¶
同时注册两个 Lambda(song_service + weather_service):
| 检查点 | 结果 |
|---|---|
| 两个工具均被 mcp_list_tools 发现 | ✅ |
| 模型根据问题选择正确工具 | ⚠️ 选择了 weather 但循环调用 |
| 并行工具调用 | ❌ 未观察到 |
错误处理测试¶
| 场景 | 行为 | 结果 |
|---|---|---|
| Lambda 超时(3s timeout + 60s sleep) | mcp_call status=failed |
✅ 优雅处理 |
| Lambda 语法错误 | 返回 invalid_prompt + 详细错误 |
✅ 有用的错误信息 |
| 内置 notes/tasks 工具 | unknown variant 错误 |
❌ Bedrock 不支持 |
踩坑记录¶
GPT-OSS 模型工具调用无限循环(实测发现,官方未记录)
这是本次测试最大的发现。无论 20B 还是 120B,GPT-OSS 模型在 server-side 模式下会无限循环调用同一工具,始终不生成最终文本回复。
- 不设
max_tool_calls:模型调用工具 40+ 次,直到命中默认限制 - 设
max_tool_calls=1:调用 1 次后尝试第 2 次被截断(status=incomplete) - 添加明确 instructions 要求"不要重复调用":无效
- 对比:同一模型的 client-side 模式完全正常,1 次工具调用后直接生成文本
结论:Server-side 架构已就绪,但 GPT-OSS 模型目前无法正确完成 server-side 多轮推理循环。建议等待更多模型支持后再用于生产。
内置工具(notes/tasks)在 Bedrock 上不可用(实测发现,与文档不一致)
官方 tool-use 文档详细描述了 notes 和 tasks 两个内置工具,但 Bedrock Mantle 端点返回:
function 和 mcp 两种工具类型。
max_tool_calls 是必要参数
在 GPT-OSS 模型的 server-side 模式下,不设置 max_tool_calls 会导致无限循环,消耗大量 token。建议始终设置一个合理上限(如 2-5)。
费用明细¶
| 资源 | 单价 | 用量 | 费用 |
|---|---|---|---|
| GPT-OSS 120B 推理 | ~$0.006/1K tokens | ~2000 tokens | ~$0.01 |
| GPT-OSS 20B 推理 | ~$0.002/1K tokens | ~500 tokens | ~$0.001 |
| Lambda 执行 | $0.20/1M requests | ~50 requests | ~$0.00 |
| 合计 | < $0.05 |
清理资源¶
# 删除 Lambda 函数
aws lambda delete-function --function-name bedrock-tool-song --region us-east-1
aws lambda delete-function --function-name bedrock-tool-weather --region us-east-1
aws lambda delete-function --function-name bedrock-tool-error --region us-east-1
# 删除 IAM 角色(先解绑策略)
aws iam detach-role-policy \
--role-name bedrock-tool-lambda-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name bedrock-tool-lambda-role
# 删除 API Key(如不再需要)
aws iam delete-service-specific-credential \
--user-name <USERNAME> \
--service-specific-credential-id <CREDENTIAL_ID>
务必清理
Lab 完成后请执行清理步骤。Lambda 函数即使不调用也可能产生 CloudWatch Logs 存储费用。
结论与建议¶
Server-Side Custom Tools 现状评估¶
| 方面 | 评价 |
|---|---|
| 技术架构 | ✅ 完善 — MCP 协议、Lambda 集成、错误处理均就绪 |
| 模型支持 | ❌ 不足 — 仅 GPT-OSS,且存在循环调用 bug |
| 内置工具 | ❌ 不可用 — notes/tasks 文档有描述但未实现 |
| 生产就绪 | ⚠️ 尚未 — 等待更多模型支持 |
何时用 Server-Side vs Client-Side?¶
现阶段建议使用 Client-Side:
- GPT-OSS server-side 有循环 bug,无法可靠生成最终回复
- Client-side 延迟更低(1.61s vs 2.84s)且输出稳定
- Client-side 支持所有 Mantle 上的 38 个模型
Server-Side 的未来价值(等模型支持修复后):
- 工具执行在 AWS 安全边界内,适合访问 VPC 内部资源
- 客户端代码极简(10 行 vs 25 行)
- 支持 AgentCore Gateway 集成,实现集中化工具管理
与已有 Responses API 文章的关系¶
| Responses API 基础篇 | 本文(Server-Side Tools 进阶篇) |
|---|---|
| API 入门、有状态对话 | Server-Side Tool Calling 深度实测 |
| 38 个模型验证 | GPT-OSS 工具调用行为分析 |
| previous_response_id 链式调用 | MCP 协议 Lambda 实现 |