从本地模型调用、工具集成、到云端部署的完整 Agent 开发流程,详解 Bedrock(模型)、Strands(工具循环)和 AgentCore(生产部署)的协作。
从在 AWS 上构建到部署 AI 智能体的基础介绍:从单次模型调用到托管云端点,帮助你理解 Bedrock、Strands 和 AgentCore 各自的作用,以及它们如何协同工作。
如果你更喜欢视频形式,可以在我们的 YouTube 频道查看这部分内容。
你将构建什么:用三种方式回答同一个问题:“What should I make for dinner?”。先进行一次原始模型调用,再构建一个拥有单个工具的本地智能体,最后把同一个智能体部署到 AWS。每一步都会展示下一层 AWS 服务带来了什么。
三个层次:Amazon Bedrock 是模型,也就是大脑。Strands 是为模型提供工具和循环的运行框架。Amazon Bedrock AgentCore 则负责让它在生产环境中运行。
成本:几乎为零。第 1 章和第 2 章只使用本地 Node 和一次 Bedrock 调用。第 3 章会创建真实资源,并在最后将其销毁。符合 Free Tier 条件的账户可以覆盖这些成本,AWS 新用户还可以获得最高 200 美元的抵扣额度。
时间:快速完成大约需要 30 分钟;如果你想花时间深入理解每个部分,则需要 1 小时以上。
我询问一个 AI 模型晚餐应该做什么。它给了我一些建议,然后问我手头有哪些食材。
这还用说!要想知道该推荐什么,这是个关键问题,但它对此一无所知。我的厨房里有鸡蛋、菠菜、大蒜、米饭和一些切达奶酪,而模型完全看不到这些。它拥有一个敏锐的大脑,却无法接触我的现实环境,并且只有一次猜测机会。
这个缺口正是本文要讲述的全部故事。模型本身既聪明又盲目。要让它真正有用,你需要在外面套上一层运行框架,为它提供工具和循环。然后,当它能够在你的机器上正常运行后,你又会撞上下一堵墙:如何让其他人可以全天候使用它,而无需有人时刻照看服务器。
我会用三种方式回答同一个问题:“What should I make for dinner?”,每次更换其背后的实现。首先是没有任何附加能力的原始模型调用。然后是一个本地食品储藏室厨师智能体,它拥有一个名为 get_pantry 的工具,可以实际检查厨房。最后,让同一个智能体在 AWS 上运行。工具只会出现在第 2 章和第 3 章中,因为只有到了那时,才有智能体能够使用它。
演进过程如下:
原始 Amazon Bedrock。只有模型。它可以回答问题,但看不到食品储藏室。
本地 Strands 智能体。增加一个工具和一个循环。现在,它会检查食品储藏室,并基于实际情况给出答案。
在 Amazon Bedrock AgentCore Runtime 上运行的同一个智能体。它被部署到 AWS,现在通过一个托管端点在云端运行,任何获得你授权的人都可以调用它。
完成后,你将亲自运行这三种实现,并且清楚 AWS 的每个组件分别负责什么。
第 1 章:原始 Bedrock——一个没有眼睛的大脑
第 2 章:本地 Strands 智能体——让答案有据可依的循环
第 3 章:部署到 AgentCore Runtime
究竟谁可以调用这个智能体?
后续可以逐步加入的额外能力:记忆、网关和可观测性
用一张图看懂整个技术栈
亲自复现
在编写任何代码之前,请先记住一个概念。
智能体等于模型加运行框架。模型是大脑,负责读取你的请求并进行推理。运行框架则是包围在大脑周围的代码,为它提供工具、指令,以及使用这些工具的循环。

Amazon Bedrock 为你提供大脑。它是一种托管式、无服务器的服务,让你无需租用 GPU,也无需运行服务器,就能调用 Anthropic、Meta 等提供商的顶级模型。你只需选择模型、发送提示词,然后获得响应。
Strands 是运行框架。它是 AWS 推出的开源 SDK,可以替你运行工具调用循环,并且几乎适用于任何模型。
AgentCore 是最终智能体在生产环境中的运行场所。它为智能体提供托管式、无服务器的运行环境,还提供智能体实际运行所需的附加能力,例如记忆、连接 API 的网关、可观测性等。
这三个层次可以无缝组合在一起。选择你的大脑,构建你的智能体,然后让它真正运行起来。本文剩余的内容,就是用代码实现这句话。
一个 AWS 账户。个人账户就可以。符合 Free Tier 条件的账户足以完成本文的全部操作,AWS 新用户还可以获得最高 200 美元的抵扣额度。
Bedrock 模型访问权限。本演示使用 Claude,但你可以使用任何想要的模型。请在 Amazon Bedrock 控制台中确认,该模型已在你首选的区域启用。
Node.js 22 或更高版本。我使用的是 v24。可以通过 node --version 检查版本。
已经完成配置的 AWS CLI v2。运行 aws configure(或使用 SSO),设置默认区域,然后通过 aws sts get-caller-identity 进行确认。如果该命令返回了你的账户信息,就说明配置正常。安装指南见此处。
一个区域。本文的所有操作都使用 us-east-1。us.* 模型推理配置文件可以解析到该区域,并且 AgentCore Runtime 在该区域可用。如果你选择其他区域,请在开始之前再次确认你的模型和 AgentCore Runtime 是否都在该区域中可用。
对于第 3 章的部署,你还需要另外两个工具。第 1 章和第 2 章不需要它们,因此可以稍后再安装:
AWS CDK,通过 npm install -g aws-cdk 进行全局安装。
对你的账户和区域执行一次 CDK 引导。进行到相关步骤时,我们会详细说明。简而言之:它会在你的账户中创建一个名为 CDKToolkit 的小型 CloudFormation 堆栈。它位于云端,而不是你的项目文件夹中,并且只需执行一次。
AgentCore CLI,通过 npm install -g @aws/agentcore 安装。
为什么这很重要:第 1 章和第 2 章只依赖 Node 和你的 AWS 凭证。如果你只是想看看模型如何回答问题,以及智能体如何让答案基于实际信息,那么可以在完成第 2 章后停止,无需安装 CDK 或 AgentCore CLI。
这里我们只是直接调用模型。我们会得到一个不错的回答,但它最终还是会问你的厨房里有什么。下一章要填补的,正是这块缺失的上下文。
关于模型,以及为什么你的输出会与我的不同。这里的每个代码示例都使用 us.anthropic.claude-sonnet-5,也就是 Claude Sonnet 5 的美国跨区域推理配置文件。三章始终使用同一个模型,可以保证比较公平。不过要注意,模型的输出并不具有确定性。连续两次询问“What should I make for dinner?”,你可能会得到用不同措辞描述的同一道菜,也可能得到完全不同的菜。这是正常现象。你的食谱可能与我的不同,甚至你自己运行两次得到的结果也可能不同。
mkdir 01-bedrock-raw && cd 01-bedrock-raw
npm init -y
npm pkg set type=module
npm install @aws-sdk/client-bedrock-runtime
npm install -D tsx typescript @types/node
npm pkg set type=module 很重要。代码使用了 ES 模块的 import 语法和顶层 await,这个设置会告诉 Node 将该文件视为模块。我们使用 tsx 直接运行 TypeScript,因此不需要单独的编译步骤。
import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'
const client = new BedrockRuntimeClient({ region: 'us-east-1' })
const response = await client.send(new ConverseCommand({
modelId: 'us.anthropic.claude-sonnet-5',
messages: [
{ role: 'user', content: [{ text: 'What should I make for dinner?' }] },
],
}))
console.log(response.output?.message?.content?.[0]?.text)
下面逐一讲解每个部分。
客户端指向某个区域中的 Bedrock:
const client = new BedrockRuntimeClient({ region: 'us-east-1' })
ConverseCommand 使用的是 Converse API,这是一种与 Bedrock 上任意聊天模型通信的统一方式。你需要指定模型,并向它传入消息列表:
const response = await client.send(new ConverseCommand({
modelId: 'us.anthropic.claude-sonnet-5',
messages: [
{ role: 'user', content: [{ text: 'What should I make for dinner?' }] },
],
}))
然后从响应中取出文本。这个访问路径看起来有些繁琐,是因为一条消息可能包含多个内容块,所以这里取的是第一个内容块:
console.log(response.output?.message?.content?.[0]?.text)
npx tsx bedrock.ts
你会得到一个有帮助但比较泛泛的回答。下面是一次经过删减的运行结果(你的输出会有所不同,而且完整列表会更长):
# Dinner Ideas
I'd love to help! To give you good suggestions, it helps to know a bit more:
- What ingredients do you have on hand (or are willing to shop for)?
- How much time do you want to spend cooking?
- Any dietary preferences/restrictions?
...
Let me know what you've got in the fridge/pantry, and I can suggest something more specific!
再读一遍最后一行。模型正在询问我的厨房里有什么。它根本无从得知,因此只能根据训练数据给出一些建议,最后让我们有点悬而未决。模型本身没有任何问题。它很聪明,只是看不见现实环境,而且只有一次回答机会。
现在,为同一个大脑增加一个工具和一个循环。它会先检查食品储藏室,然后再回答,回复也会因此彻底改变。
mkdir 02-strands-agent && cd 02-strands-agent
npm init -y
npm pkg set type=module
npm install @strands-agents/sdk
npm install -D tsx typescript @types/node
import { Agent, tool, BedrockModel } from '@strands-agents/sdk'
const getPantry = tool({
name: 'get_pantry',
description: 'Return the ingredients the user has at home right now.',
callback: () => ['eggs', 'spinach', 'garlic', 'rice', 'cheddar cheese'],
})
const agent = new Agent({
model: new BedrockModel({ modelId: 'us.anthropic.claude-sonnet-5', region: 'us-east-1' }),
tools: [getPantry],
systemPrompt:
'Suggest a recipe to make, check the pantry',
})
await agent.invoke('What should I make for dinner?')
仍然很简洁。最后一行与第 1 章的请求相同。上面的所有内容都是框架。让我们分解三个重要的部分。
工具。这是智能体与我的世界的连接:
const getPantry = tool({
name: 'get_pantry',
description: 'Return the ingredients the user has at home right now.',
callback: () => ['eggs', 'spinach', 'garlic', 'rice', 'cheddar cheese'],
})
描述不是注释。模型读取它来决定何时调用工具,所以要为模型编写。此工具不接受参数,因此没有其他内容需要声明。回调是模型调用工具时运行的代码。我的返回一个硬编码数组,这对演示来说是完美的。在真实应用中,这是你可能访问数据库或 API 的地方。
智能体。模型、工具、说明,连接在一起:
const agent = new Agent({
model: new BedrockModel({ modelId: 'us.anthropic.claude-sonnet-5', region: 'us-east-1' }),
tools: [getPantry],
systemPrompt:
'Suggest a recipe to make, check the pantry',
})
与第 1 章相同的模型,有意为之,这样你可以看到核心模型没有改变。我在这里明确传递它,尽管 Strands TS SDK 在你忽略它时默认使用 Bedrock Claude Sonnet 模型。systemPrompt 告诉智能体该做什么,并将其指向工具。tools 数组是它允许调用的列表。
调用。一行:
await agent.invoke('What should I make for dinner?')
没有编排代码。这是值得暂停的地方。
当你调用 invoke 时,Strands 运行一个你无需编写的循环:
那个循环就是智能体循环。使用 Strands 等 SDK 的整个意义在于你可以免费获得循环、工具调用和消息管道。对于简单的智能体,你需要带来的就是工具和提示。

npx tsx agent.ts
Strands TypeScript SDK 附带一个默认启用的控制台打印机,所以你可以看到智能体思考、调用工具和回答,完全没有你写的日志代码。一个代表性的运行(措辞会有所不同):

同样的核心模型。同样的问题。完全不同的答案。它看到了鸡蛋、菠菜、大蒜、米饭和切达奶酪,并围绕它们构建了真实的食谱,而不是问我有什么。我没有写循环、解析器或编排器。我给了模型一个工具,让 Strands 处理来回对话。
这是一个有效的智能体。在我的笔记本电脑上。这正是下一个问题开始的地方。
我的智能体在我的机器上运行得很好。然后我考虑让其他人使用它。现在我在考虑托管、扩展以及当多个用户同时出现时保持其健康。我不想仅仅为了暴露一个函数而编写和操作 Web 服务器。
这正是 Amazon Bedrock AgentCore Runtime 处理的事情。这是在生产中运行你的智能体的托管无服务器方式。你带来已经编写的智能体,CLI 将其打包并部署,你得到一个端点。相同的智能体逻辑,没有服务器给你运行。
与第 2 章相同的食品柜厨师智能体,现在放在 AgentCore CLI 管理的端点后面。
npm install -g @aws/agentcore aws-cdk
agentcore --version # I had 0.21.1
cdk --version # I had 2.1128.1
AgentCore 通过 AWS CDK 进行部署,CDK 需要对每个账户和区域进行一次性设置,称为引导:
cdk bootstrap aws://<ACCOUNT_ID>/us-east-1
替换成你的 12 位账户 ID。这会创建一个名为 CDKToolkit 的 CloudFormation 栈和一个配套的 S3 存储桶。你每个账户和区域只需执行一次,所以如果你之前在这里引导过可以跳过。要检查:
aws cloudformation describe-stacks --region us-east-1 --stack-name CDKToolkit
如果返回状态为 CREATE_COMPLETE 的栈,说明你已经引导过。更多关于引导的信息见此处。
agentcore create 命令为新智能体项目搭建框架。它可以通过交互式向导引导你,但我会直接传递选项,这样步骤可复用且你能准确看到我们的选择:
agentcore create \
--project-name PantryChef --name PantryChef --type create \
--build CodeZip --language TypeScript --framework Strands \
--model-provider Bedrock --memory none --skip-git
这些标志表示:一个名为 PantryChef 的 TypeScript 项目,构建为 CodeZip(代码作为 zip 文件部署),基于 Strands 框架,使用 Bedrock 作为模型提供者,暂时不启用内存功能。它在后台运行 npm install,所以稍等一会儿。完成后,你会有一个 PantryChef/ 目录,结构如下:
PantryChef/
agentcore/
agentcore.json # 运行时配置: CodeZip, NODE_22, PUBLIC, HTTP
cdk/ # CLI 为你部署的 CDK 应用
app/PantryChef/
main.ts # 入口点:将你的智能体包装在运行时处理程序中
model/load.ts # 模型配置在这里,不在 main.ts
mcp_client/client.ts # 示例 MCP 客户端,我们的智能体不需要
package.json
tsconfig.json
这里是视频在摄像头外做的事。agentcore create 不会给你一个空白项目。它生成一个有效的示例智能体,而示例不是食品柜厨师。两个文件附带的内容需要你替换。
首先是模型。打开 app/PantryChef/model/load.ts,你会看到:
import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
export function loadModel(): BedrockModel {
return new BedrockModel({ modelId: 'global.anthropic.claude-sonnet-4-5-20250929-v1:0' });
}
这是一个真实的、固定的模型 ID,但不是这个演示使用的。框架默认为 Claude Sonnet 4.5。我们一直在用 Sonnet 5,所以这个文件需要改。
其次是智能体本身。打开 app/PantryChef/main.ts,你会找到一个相加两个数字并连接示例 MCP 客户端的示例:
import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
import { Agent, McpClient, tool, type ToolList } from '@strands-agents/sdk';
import { z } from 'zod';
import { loadModel } from './model/load.js';
import { getStreamableHttpMcpClient } from './mcp_client/client.js';
// 定义 MCP 客户端集合(过滤掉初始化失败的任何内容)
const mcpClients: McpClient[] = [getStreamableHttpMcpClient()].filter(
(client): client is McpClient => Boolean(client)
);
// 定义模型使用的工具集合
const tools: ToolList = [];
// 定义一个简单的函数工具 —— Zod schema 为我们免费提供类型推导和运行时验证
const addNumbers = tool({
name: 'add_numbers',
description: 'Return the sum of two numbers',
inputSchema: z.object({
a: z.number(),
b: z.number(),
}),
callback: async ({ a, b }) => a + b,
});
tools.push(addNumbers);
// 将 MCP 客户端添加到工具
tools.push(...mcpClients);
const SYSTEM_PROMPT = `
You are a helpful assistant. Use tools when appropriate.
`;
// ... 文件的其余部分(运行时处理程序)如下所示
MCP 不熟悉?现在不用太担心。这是将外部工具连接到智能体的标准方式。框架包含一个示例客户端来展示这是可能的,但食品柜厨师不需要它。我们即将用自己的工具和提示替换整个示例。
所以"智能体逻辑保持相同"对工具、提示和模型是真的,但围绕它有真实的运行时管道,是 CLI 为你编写的。将此示例转换为食品柜厨师正好需要两次编辑。
用这个替换整个文件:
import { BedrockModel } from '@strands-agents/sdk/models/bedrock';
export function loadModel(): BedrockModel {
return new BedrockModel({ modelId: 'us.anthropic.claude-sonnet-5', region: 'us-east-1' });
}
与脚手架相比有两处改动。模型 ID 现在是 us.anthropic.claude-sonnet-5,并且我添加了 region: 'us-east-1',以便模型能够在我们一直使用的区域中正确解析。
用下面的内容替换整个文件。这是第 2 章中的 get_pantry 工具和提示词,现在将它们放进了 CLI 生成的运行时处理程序中:
import { BedrockAgentCoreApp } from 'bedrock-agentcore/runtime';
import { Agent, tool, type ToolList } from '@strands-agents/sdk';
import { loadModel } from './model/load.js';
// The one tool this agent has: what is in the kitchen right now.
const getPantry = tool({
name: 'get_pantry',
description: 'Return the ingredients the user has at home right now.',
callback: () => ['eggs', 'spinach', 'garlic', 'rice', 'cheddar cheese'],
});
const tools: ToolList = [getPantry];
const SYSTEM_PROMPT = `
Suggest a recipe to make, check the pantry
`;
let cachedAgent: Agent | null = null;
async function getOrCreateAgent(): Promise<Agent> {
if (!cachedAgent) {
const model = await loadModel();
cachedAgent = new Agent({
model,
systemPrompt: SYSTEM_PROMPT,
tools,
});
}
return cachedAgent;
}
const app = new BedrockAgentCoreApp({
invocationHandler: {
async *process(payload: any, context: any) {
const agent = await getOrCreateAgent();
for await (const event of agent.stream(payload.prompt ?? '')) {
if (
event.type === 'modelStreamUpdateEvent' &&
event.event?.type === 'modelContentBlockDeltaEvent' &&
event.event.delta?.type === 'textDelta'
) {
yield { data: event.event.delta.text };
}
}
},
},
});
app.run({ port: parseInt(process.env.PORT ?? '8080') });
上半部分是第 2 章中的 AI 智能体,没有任何改动。注意,这里没有使用 zod。脚手架为示例中的 add_numbers 工具导入了它,因为该工具接收的参数需要 schema,但 get_pantry 不接收任何参数。Strands 的 tool() 辅助函数将 inputSchema 视为可选项,并默认使用空 schema,因此移除 zod 不会改变模型所看到的工具。下半部分是 CLI 为你生成的内容,值得理解一下,因为正是这部分把它从脚本变成了可部署的服务。
getOrCreateAgent 只创建一次 AI 智能体并将其缓存,因此无需在每次请求时重新创建:
let cachedAgent: Agent | null = null;
async function getOrCreateAgent(): Promise<Agent> {
if (!cachedAgent) {
const model = await loadModel();
cachedAgent = new Agent({ model, systemPrompt: SYSTEM_PROMPT, tools });
}
return cachedAgent;
}
BedrockAgentCoreApp 是运行时处理程序。否则,你就需要自己把这部分编写成 Web 服务器。process 生成器接收传入的请求负载,以流式方式处理 AI 智能体的输出,并在文本生成时只产出文本内容:
const app = new BedrockAgentCoreApp({
invocationHandler: {
async *process(payload: any, context: any) {
const agent = await getOrCreateAgent();
for await (const event of agent.stream(payload.prompt ?? '')) {
if (
event.type === 'modelStreamUpdateEvent' &&
event.event?.type === 'modelContentBlockDeltaEvent' &&
event.event.delta?.type === 'textDelta'
) {
yield { data: event.event.delta.text };
}
}
},
},
});
app.run({ port: parseInt(process.env.PORT ?? '8080') });
注意这个处理程序流式传输的内容:只有文本增量。因此,调用方能够看到菜谱,但看不到 get_pantry 工具调用那一行。该工具仍然会在服务器上运行,只是你不会把这部分内容流式传输给客户端。先记住这一点,接近文末讨论可观测性时还会提到它。
你不需要运行 npm run build。本地开发环境会直接运行 TypeScript,而部署流程会在 CDK 步骤中完成编译和打包。这个工作流不需要手动构建。
CLI 提供了一个行为与部署版本一致的本地服务器。打开两个终端,并确保它们都位于 PantryChef 目录中。
在终端 1 中启动服务器(等待几秒钟,让它完成启动):
cd PantryChef
agentcore dev --logs
在终端 2 中发送提示词:
agentcore dev "What should I make for dinner?" --stream
你会在终端 2 中看到炒饭菜谱以流式方式返回。在终端 1 中,--logs 输出会显示 get_pantry 工具在服务器端触发,这正是流式客户端输出中没有展示的工具调用。
如果你想在实际部署前查看部署将执行哪些操作,可以先进行预览:
agentcore deploy --dry-run
agentcore deploy -y -v
这个过程大约需要一分钟。在底层,CLI 会压缩你的代码,并使用 CDK 创建少量资源:一个名为 AgentCore-PantryChef-default 的 CloudFormation 堆栈、一个包含相应策略的 IAM 执行角色,以及 AWS::BedrockAgentCore::Runtime 本身。完成后,它会打印输出,内容大致如下(账户 ID 使用占位符表示):
Runtime ARN: arn:aws:bedrock-agentcore:us-east-1:111122223333:runtime/PantryChef_PantryChef-xxxxxxxxxx
Runtime ID: PantryChef_PantryChef-xxxxxxxxxx
Role ARN: arn:aws:iam::111122223333:role/AgentCore-PantryChef-defa-ApplicationAgentPantryChe-xxxxxxxxxxxx
Stack: AgentCore-PantryChef-default
从云端调用它
agentcore invoke "What should I make for dinner?" --stream
几秒钟后,同一个厨房助手会给出回答,只不过这次它运行在托管运行时中,而不是你的笔记本电脑上:
![用于测试从云端托管端点运行 AI 智能体的终端命令 agentcore invoke](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tay1