1. Strands Agents TypeScript SDK:一个模型驱动的AI智能体开发框架深度解析
最近在探索如何用TypeScript构建更可靠、更易维护的AI智能体时,我深度体验了Strands Agents的TypeScript SDK。作为一个长期在Node.js和前端领域耕耘的开发者,我对市面上各种AI工具链的“玩具感”一直不太满意——要么封装过度,失去了灵活性;要么过于底层,写起来像是在重复造轮子。Strands Agents的出现,让我感觉找到了一个不错的平衡点。它提出的“模型驱动”理念,本质上是通过强类型和模式化的方式,把智能体开发从“提示词工程”的玄学,拉回到了我们熟悉的软件工程轨道上。今天,我就结合自己近一个月的实际项目踩坑经验,来聊聊这个框架的核心设计、最佳实践,以及那些官方文档里没写的细节。
简单来说,Strands Agents TypeScript SDK是一个用于在TypeScript/JavaScript环境中构建、运行和编排AI智能体的框架。它最大的特点是模型驱动和类型安全。所谓模型驱动,是指它用Zod模式(Schema)来定义工具(Tools)的输入输出、结构化响应(Structured Output)的格式,甚至多智能体之间的协作流程。这带来的直接好处是,你的智能体行为可以被清晰地定义和验证,而不是依赖大语言模型(LLM)的自由发挥。类型安全则贯穿始终,从工具回调函数的参数,到最终响应的解析结果,你都能获得完整的TypeScript类型推断和编译时检查,这在大中型项目中简直是救命稻草。
这套框架适合谁呢?如果你正在用Node.js后端或浏览器环境开发需要集成AI能力的应用,比如智能客服、自动化工作流、数据分析助手,或者想实验多智能体协作(Multi-Agent)的复杂场景,那么Strands Agents值得你花时间研究。它尤其适合那些对代码质量、可测试性和系统可观测性有要求的团队。接下来,我会从设计思路拆解开始,带你一步步深入这个框架的肌理。
1.1 核心设计思路:为什么是“模型驱动”?
初次接触“模型驱动”这个词可能会觉得有点抽象。在Strands Agents的语境里,它指的不仅仅是使用大语言模型(LLM),更是用数据模式(Schema)来驱动整个智能体的交互逻辑。这与传统的、主要依靠精心设计的提示词(Prompt)来引导LLM的方式有本质区别。
传统的智能体开发,我们往往写一个很长的系统提示词,告诉LLM“你可以使用A、B、C这些工具,它们的用法是……”,然后在代码里解析LLM返回的文本,判断它想调用哪个工具,再手动拼接参数、调用函数、把结果塞回上下文。这个过程充满了不确定性:LLM可能不按你期望的格式返回,参数解析可能出错,工具执行结果的结构也可能五花八门。
Strands Agents的做法是,把这一切都“契约化”。你用Zod定义一个工具,包括它的名字、描述、输入参数的模式。框架在生成给LLM的提示词时,会将这些模式转换成模型能理解的结构化描述(比如OpenAI的function calling格式或Anthropic的tool use格式)。当LLM决定调用工具时,它返回的是一个结构化的调用请求,框架能直接、安全地将其解析成强类型的JavaScript对象,然后调用你定义的回调函数。同样,回调函数的返回值,也会被框架包装成结构化的结果,送回给LLM作为下一步思考的上下文。
这种设计带来的几个关键优势:
- 可靠性提升:输入输出验证从“运行时字符串解析”变成了“模式验证”。Zod会在调用前验证参数是否符合模式,不符合的直接抛出清晰的错误,避免了因格式错误导致的工具调用失败或副作用。
- 开发体验飞跃:全程类型安全。你在写工具回调函数时,
input参数的类型就是Zod模式推断出来的类型,IDE能提供完美的自动补全和类型检查。这极大地减少了低级错误。 - 意图理解更精准:LLM对于结构化的工具描述理解得更好、更一致。相比于用自然语言描述工具用法,结构化的模式减少了歧义,提高了工具调用的准确率。
- 组合与复用性增强:工具成为了一个个拥有明确定义接口的“组件”,可以像乐高一样在不同的智能体之间轻松组合和复用。
实操心得:从“提示词驱动”转向“模型驱动”需要一点思维转变。一开始你可能会不习惯为每个工具都写Zod模式,觉得有点繁琐。但坚持下来会发现,这前期的一点投入,在后续的调试、扩展和维护阶段会带来巨大的回报。尤其是在团队协作中,一个定义清晰的工具模式就是最好的文档。
2. 环境准备与核心概念落地
理论说再多,不如动手跑一遍。我们从一个最基础的智能体开始,逐步添加功能,来感受Strands Agents的工作方式。首先,确保你的环境是Node.js 20或更高版本,这是SDK的硬性要求,因为它使用了一些较新的ES模块特性。
2.1 基础安装与智能体初始化
创建一个新的项目目录,初始化并安装SDK:
mkdir my-strands-agent && cd my-strands-agent npm init -y npm install @strands-agents/sdk zod这里我们同时安装了zod,因为定义工具和结构化输出离不开它。接下来,创建一个index.ts文件,写入最基本的智能体调用代码:
import { Agent } from '@strands-agents/sdk'; // 创建一个最简单的智能体,使用默认配置 const agent = new Agent(); async function main() { try { const result = await agent.invoke('What is the capital of France?'); console.log('Agent Response:', result.content); } catch (error) { console.error('Error:', error); } } main();运行前,你需要配置模型访问。SDK默认使用Amazon Bedrock作为模型提供商,并且默认尝试调用Claude 3.5 Sonnet模型。因此,你需要确保:
- 拥有一个AWS账户,并在目标区域(如
us-east-1)启用了Bedrock服务,且对Claude模型有访问权限。 - 本地配置了有效的AWS凭证。通常可以通过
aws configure命令设置,或设置AWS_ACCESS_KEY_ID和AWS_SECRET_ACCESS_KEY环境变量。
如果你更习惯使用OpenAI,切换起来也非常简单:
import { Agent } from '@strands-agents/sdk'; import { OpenAIModel } from '@strands-agents/sdk/models/openai'; // 设置你的OpenAI API Key process.env.OPENAI_API_KEY = 'your-api-key-here'; const model = new OpenAIModel({ api: 'chat', // 使用Chat Completions API model: 'gpt-4o', // 指定模型,默认为'gpt-4' }); const agent = new Agent({ model });注意事项:关于模型提供商的选择,这里有个细节。BedrockModel在初始化时,
modelId参数需要严格按照Bedrock控制台提供的模型ID来填写,例如anthropic.claude-3-5-sonnet-20241022-v2:0。不同区域、不同版本的模型ID可能不同,填错了会直接导致调用失败。而OpenAIModel则相对简单,使用通用的模型名称即可。
2.2 理解智能体的核心循环与配置
Agent类是这个框架的核心。当你调用agent.invoke(prompt)时,背后触发了一个标准的“思考-行动”循环(ReAct模式的一种实现):
- 思考:将用户输入、系统提示词、对话历史(如果启用)和可用工具的描述组合成一个提示,发送给LLM。
- 决策:LLM返回一个包含“最终答案”或“工具调用请求”的响应。
- 行动:如果是工具调用,框架会解析参数、执行对应的工具函数,并将结果作为新的上下文附加到对话中。
- 循环:带着工具执行的结果,回到第1步,让LLM继续思考,直到它决定给出最终答案。
你可以通过配置项来精细控制这个循环:
import { Agent } from '@strands-agents/sdk'; const agent = new Agent({ // 系统提示词,定义智能体的角色和行为准则 systemPrompt: '你是一个专业、简洁的助手。回答要准确,如果不知道就说不知道。', // 最大迭代次数,防止智能体陷入无限的工具调用循环 maxSteps: 10, // 是否启用对话历史管理 enableConversationHistory: true, // 对话历史的Token数量上限,用于控制上下文窗口 maxConversationTokens: 4000, });maxSteps是一个非常重要的安全阀。在复杂任务中,智能体可能会在多个工具之间来回调用,或者陷入“我再查一下”的循环。设置一个合理的上限(比如10-20次),可以防止单个请求消耗过多的Token和API费用,并在智能体“卡住”时及时抛出错误。
3. 构建类型安全的工具(Tools)
工具是智能体与外部世界交互的桥梁。Strands Agents让创建类型安全的工具变得异常优雅。
3.1 创建你的第一个工具
假设我们要构建一个查询城市天气的智能体。首先,我们定义一个getWeather工具:
import { Agent, tool } from '@strands-agents/sdk'; import { z } from 'zod'; // 使用 tool() 高阶函数创建工具 const getWeatherTool = tool({ name: 'get_weather', // 工具名称,LLM通过这个名称来调用 description: '获取指定城市的当前天气信息。', // 清晰的描述帮助LLM理解工具用途 // 使用Zod定义输入参数的模式 inputSchema: z.object({ location: z.string().describe('城市名称,例如:北京、San Francisco, CA'), unit: z.enum(['celsius', 'fahrenheit']).optional().default('celsius').describe('温度单位'), }), // 回调函数,input的类型会自动推断为 { location: string; unit?: 'celsius' | 'fahrenheit' } callback: async (input) => { console.log(`[Tool Called] get_weather with input:`, input); // 这里应该是真实的API调用,例如调用OpenWeatherMap // 为了示例,我们模拟一个返回 const mockTemperatures: Record<string, number> = { '北京': 22, 'San Francisco, CA': 18, '纽约': 25, }; const temp = mockTemperatures[input.location] || 20; const finalTemp = input.unit === 'fahrenheit' ? (temp * 9/5) + 32 : temp; const unitSymbol = input.unit === 'fahrenheit' ? '°F' : '°C'; return `当前${input.location}的天气为晴,气温${finalTemp}${unitSymbol},湿度65%,风速10km/h。`; }, }); // 将工具装配给智能体 const agent = new Agent({ systemPrompt: '你是一个天气助手,可以帮助用户查询全球城市的天气。', tools: [getWeatherTool], }); // 测试调用 async function test() { const result = await agent.invoke('上海今天天气怎么样?'); console.log(result.content); // 输出可能为:当前上海的天气为晴,气温20°C,湿度65%,风速10km/h。 // 注意:智能体可能会先调用get_weather工具,然后将工具返回的结果整合成最终回答。 } test();当你运行这段代码时,观察控制台日志,你会看到[Tool Called]的打印信息。这证实了智能体确实解析了用户问题中的“上海”,生成了符合inputSchema的调用参数({location: ‘上海’, unit: ‘celsius’}),并执行了你的回调函数。
避坑技巧:工具的描述(
description)至关重要。它不仅是给后续维护者看的,更是LLM决定是否以及如何调用该工具的主要依据。描述要简洁、准确、无歧义,最好能包含一两个调用示例。模糊的描述会导致工具被误用或弃用。
3.2 处理异步操作与错误
现实中的工具大多是异步的,比如网络请求、数据库查询。你的工具回调函数完全可以是一个async函数。框架会妥善处理异步操作。更重要的是错误处理:如果工具执行中抛出错误,框架会捕获这个错误,并将其作为“工具执行失败”的信息反馈给LLM,LLM可以据此决定重试或调整策略。
const riskyTool = tool({ name: 'fetch_data', description: '从一个可能不稳定的API获取数据。', inputSchema: z.object({ url: z.string().url() }), callback: async (input) => { const response = await fetch(input.url); if (!response.ok) { // 抛出错误,智能体会收到“工具调用失败”的信号 throw new Error(`HTTP ${response.status}: ${await response.text()}`); } return response.json(); }, });3.3 使用内置的“已供应工具”(Vended Tools)
为了提升开发效率,SDK预置了一些常用工具,可以通过单独的包引入。例如,文件编辑工具和HTTP请求工具:
npm install @strands-agents/toolsimport { FileEditorTool, HttpRequestTool } from '@strands-agents/tools'; const fileTool = new FileEditorTool({ // 可以限制文件操作的根目录,增强安全性 basePath: './workspace', }); const httpTool = new HttpRequestTool(); const agent = new Agent({ tools: [fileTool, httpTool], systemPrompt: '你可以帮我读写文件,或者从网上获取信息。', }); // 现在智能体可以理解“请创建一个名为note.txt的文件,内容为Hello World”或“获取https://api.example.com/data的信息”这样的指令。安全警告:像
FileEditorTool和HttpRequestTool这类拥有系统级或网络访问权限的工具,必须谨慎使用。在生产环境中,至少应该通过basePath限制文件访问范围,或者为HTTP工具设置允许列表(allowlist)来限制可访问的域名,避免智能体被恶意提示词诱导执行危险操作。
4. 实现结构化输出(Structured Output)
让LLM返回自由的文本固然灵活,但在很多自动化场景下,我们需要它返回结构化的数据,以便后续的程序处理。例如,从一段文本中提取联系人信息、生成JSON配置、或者格式化一个任务列表。Strands Agents的结构化输出功能,通过结合Zod模式和自动重试机制,优雅地解决了这个问题。
4.1 定义输出模式与基础使用
假设我们需要从一个自由格式的文本中提取会议信息:
import { Agent } from '@strands-agents/sdk'; import { z } from 'zod'; // 1. 用Zod定义我们期望的输出结构 const MeetingSchema = z.object({ title: z.string().describe('会议主题'), participants: z.array(z.string()).describe('参会人名单'), scheduledTime: z.string().datetime().describe('会议时间,ISO 8601格式'), durationMinutes: z.number().int().positive().describe('会议时长(分钟)'), location: z.string().optional().describe('会议地点,可选'), }); // 2. 在创建智能体时指定结构化输出模式 const agent = new Agent({ systemPrompt: '你是一个信息提取助手,请从文本中提取结构化的会议信息。', structuredOutputSchema: MeetingSchema, // 关键配置 }); async function extractMeeting() { const text = `明天下午两点(2024-06-15T14:00:00Z)我们团队要开一个季度复盘会,预计开90分钟。参加的人有张三、李四和王五。地点在301会议室。`; const result = await agent.invoke(`请从以下文本中提取会议信息:\n${text}`); // 3. 访问结构化结果,类型是 MeetingSchema 推断出的类型 const meeting = result.structuredOutput; console.log('提取的会议信息:'); console.log(`主题: ${meeting.title}`); // 季度复盘会 console.log(`时间: ${meeting.scheduledTime}`); // 2024-06-15T14:00:00.000Z console.log(`时长: ${meeting.durationMinutes}分钟`); // 90 console.log(`参会人: ${meeting.participants.join(', ')}`); // 张三, 李四, 王五 console.log(`地点: ${meeting.location}`); // 301会议室 // result.content 仍然包含模型的原始文本回复 console.log('\n模型原始回复:', result.content); } extractMeeting();这个过程的核心是验证与重试。当LLM第一次返回文本时,框架会尝试将其解析为JSON,然后用MeetingSchema进行验证。如果验证失败(比如缺少必填字段、时间格式不对),框架不会直接抛出错误给用户,而是会自动构造一个新的提示词,其中包含验证错误的具体信息(例如:“scheduledTime字段必须是有效的ISO 8601日期时间字符串”),然后重新调用LLM。这个循环会持续进行,直到返回有效的结构化数据,或者达到重试上限(默认3次)。
4.2 错误处理与重试控制
你可以捕获结构化输出失败的错误,并获取详细的验证信息:
import { StructuredOutputError } from '@strands-agents/sdk'; try { const result = await agent.invoke('一些模糊的文本...'); // 处理成功结果 } catch (error) { if (error instanceof StructuredOutputError) { console.error('结构化输出失败!'); console.error('错误信息:', error.message); console.error('验证详情:', error.cause?.errors); // Zod的详细错误数组 console.error('LLM最后一次返回的原始内容:', error.rawResponse); console.error('已重试次数:', error.retryCount); } else { // 处理其他类型的错误(如网络错误、模型错误等) console.error('其他错误:', error); } }你还可以在创建智能体时配置重试行为:
const agent = new Agent({ structuredOutputSchema: MeetingSchema, structuredOutput: { maxRetries: 5, // 最大重试次数 retryDelayMs: 1000, // 每次重试的延迟(毫秒) }, });实操心得:结构化输出功能极大地提升了AI集成到生产流水线中的可靠性。但要注意,复杂的模式(如嵌套对象、联合类型)可能会增加LLM的理解难度和重试概率。我的经验是:模式设计要尽量简单、扁平。如果确实需要复杂结构,可以考虑拆分成多个步骤,先让LLM提取原始文本片段,再用专门的解析逻辑进行处理。
5. 集成模型上下文协议(MCP)
模型上下文协议(Model Context Protocol, MCP)是一个新兴的开放协议,旨在标准化AI应用与外部工具、数据源之间的连接方式。你可以把它想象成AI世界的“驱动程序”标准。Strands Agents内置了MCP客户端支持,让你能轻松地将任何MCP服务器提供的工具集成到你的智能体中。
5.1 连接本地MCP服务器
假设你本地运行了一个提供数据库查询工具的MCP服务器。集成步骤如下:
import { Agent, McpClient } from "@strands-agents/sdk"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; // 1. 创建MCP客户端,通过标准输入输出(stdio)连接到本地服务器进程 const dbClient = new McpClient({ transport: new StdioClientTransport({ command: "node", // 启动服务器的命令 args: ["./path/to/your-mcp-server.js"], // 服务器脚本参数 }), }); // 2. 在连接客户端之前,通常需要初始化(例如,交换密钥) await dbClient.connect(); // 3. 将MCP客户端作为一个工具源传递给智能体 const agent = new Agent({ systemPrompt: "你是一个数据分析助手,可以使用数据库工具。", tools: [dbClient], // 直接传入客户端,SDK会自动发现其提供的所有工具 }); // 4. 现在智能体就可以使用数据库工具了 const result = await agent.invoke("查询用户表中最近10条记录"); console.log(result.content); // 5. 任务完成后,断开连接 await dbClient.disconnect();5.2 使用现成的MCP服务器
社区已经有很多优秀的MCP服务器。例如,aws-documentation-mcp-server可以提供AWS服务的官方文档搜索工具。你可以通过uvx(一个Python工具运行器)快速启动它:
# 首先确保安装了 uv pip install uvimport { Agent, McpClient } from "@strands-agents/sdk"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const awsDocsClient = new McpClient({ transport: new StdioClientTransport({ command: "uvx", args: ["awslabs.aws-documentation-mcp-server@latest"], }), }); const agent = new Agent({ tools: [awsDocsClient], systemPrompt: "你是一个AWS专家,可以查阅最新的AWS官方文档来回答问题。", }); // 智能体现在可以回答“S3的PutObject API最近有什么更新?”这类问题 // 它会自动调用MCP服务器提供的文档搜索工具注意事项:MCP服务器通常运行在独立的进程中。你需要管理好这些进程的生命周期,确保在智能体不再需要时正确关闭它们,避免资源泄漏。另外,MCP工具的动态性很强,智能体在运行时才能知道具体有哪些工具可用,这为构建高度可扩展的插件化系统提供了可能,但也增加了对提示词设计的挑战——你需要让智能体学会“探索”和“选择”可用的工具。
6. 多智能体编排实战:Graph与Swarm模式
当单个智能体无法胜任复杂任务时,我们就需要多个智能体协同工作。Strands Agents提供了两种内置的编排模式:Graph(图)和Swarm(蜂群)。这是框架最强大的功能之一。
6.1 Graph模式:确定性的工作流
Graph模式允许你定义一个确定性的执行流程图。智能体作为图中的节点(Node),节点之间的边(Edge)定义了执行顺序。一个节点只有在它的所有前置依赖节点都执行完毕后才会开始运行。这种模式适合步骤清晰、流程固定的任务,比如“研究 -> 分析 -> 撰写报告”。
import { Agent, BedrockModel, Graph } from '@strands-agents/sdk'; // 使用同一个模型实例,节省成本并保持上下文一致性 const sharedModel = new BedrockModel({ maxTokens: 2048 }); // 定义三个各司其职的智能体 const researcher = new Agent({ model: sharedModel, id: 'researcher', // 节点必须有唯一ID systemPrompt: '你是一个研究专员。根据用户问题,从提供的上下文或知识中提炼关键事实和要点。输出要简洁、客观。', }); const analyst = new Agent({ model: sharedModel, id: 'analyst', systemPrompt: '你是一个分析师。接收研究员提供的事实,进行深入分析,识别趋势、矛盾或深层含义。输出你的分析结论。', }); const writer = new Agent({ model: sharedModel, id: 'writer', systemPrompt: '你是一个文案写手。接收研究员的事实和分析师的结论,将其整合成一篇结构清晰、语言流畅的简短报告。输出最终报告。', }); // 构建执行图:researcher -> analyst -> writer const workflow = new Graph({ nodes: [researcher, analyst, writer], edges: [ ['researcher', 'analyst'], // researcher 完成后,analyst 开始 ['analyst', 'writer'], // analyst 完成后,writer 开始 ], }); async function runGraphWorkflow() { const topic = '人工智能在气候变化应对中的作用'; console.log(`开始处理主题: ${topic}`); // invoke 方法会将初始提示传给第一个节点(researcher) // 每个节点的输出会自动成为下一个节点的输入的一部分 const finalResult = await workflow.invoke(topic); // finalResult 包含了整个图的最终输出(即writer节点的输出) console.log('\n=== 最终报告 ==='); console.log(finalResult.content); // 你还可以访问每个节点的独立输出 // console.log(workflow.getNodeOutput('researcher')); // console.log(workflow.getNodeOutput('analyst')); } runGraphWorkflow();在这个例子中,执行流程是线性的、确定的。researcher先工作,它的输出被传递给analyst,analyst的输出再传递给writer。Graph模式也支持并行执行,只需将没有依赖关系的节点同时作为起点即可。
6.2 Swarm模式:动态的、模型驱动的路由
Swarm模式则把执行路径的决定权交给了智能体本身。每个智能体在完成任务后,可以自主决定是“移交”(handoff)给另一个智能体,还是直接给出最终响应。这模拟了人类团队协作的场景:一个同事处理完他的部分后,根据情况决定把任务转给更合适的另一位同事。
import { Agent, BedrockModel, Swarm } from '@strands-agents/sdk'; const sharedModel = new BedrockModel({ maxTokens: 1024 }); // 定义一群智能体,每个都有明确的职责描述 const receptionist = new Agent({ model: sharedModel, id: 'receptionist', description: '接待员,负责接收用户请求并进行初步分类和分流。', systemPrompt: `你是前台接待员。请分析用户请求: - 如果是简单的信息查询(如天气、时间、定义),请直接回答。 - 如果是需要复杂分析或创作的任务(如写文章、分析数据),请说“我将为您转接给专家”,然后移交(handoff)给 specialist。 - 如果是技术性非常强的编程问题,请移交(handoff)给 technician。 你的回答必须非常简短。`, }); const specialist = new Agent({ model: sharedModel, id: 'specialist', description: '通用专家,处理复杂的分析和创作任务。', systemPrompt: `你是通用问题专家。请深入处理接收到的任务,提供详细、专业的回答。这是最终步骤,完成後不要移交。`, }); const technician = new Agent({ model: sharedModel, id: 'technician', description: '技术专家,解决编程和深度技术问题。', systemPrompt: `你是技术专家。请解决编程或技术架构问题,提供代码示例或详细方案。这是最终步骤,完成後不要移交。`, }); // 创建蜂群,指定起始节点和最大步数(防止无限循环) const swarm = new Swarm({ nodes: [receptionist, specialist, technician], start: 'receptionist', // 所有请求都先发给接待员 maxSteps: 6, // 最多允许6次“思考-行动”步骤(包括移交) }); async function runSwarm() { const queries = [ '今天天气怎么样?', '写一篇关于海洋塑料污染的短评。', '如何在TypeScript中实现一个安全的深拷贝函数?', ]; for (const query of queries) { console.log(`\n>>> 用户提问: ${query}`); const result = await swarm.invoke(query); console.log(`<<< 最终回答 (来自 ${result.lastNodeId}):\n${result.content}\n`); } } runSwarm();运行这段代码,你会看到:
- 对于“今天天气怎么样?”,
receptionist可能直接回答。 - 对于“写短评”,
receptionist会将其移交给specialist,由specialist生成最终回答。 - 对于“TypeScript深拷贝”,
receptionist会将其移交给technician。
Swarm模式的强大之处在于其动态性。你无需预先定义完整的流程,智能体们会根据对任务的理解实时决定协作路径。当然,这也对每个智能体的提示词设计提出了更高要求,必须清晰地定义其职责和移交规则。
编排模式选择指南:
- 选择Graph模式当:任务流程固定、步骤清晰、需要严格保证执行顺序和阶段产出物。例如,数据处理流水线、审核流程、多阶段内容生成。
- 选择Swarm模式当:任务类型多变、难以预先规划所有路径、需要智能体自主协作。例如,智能客服路由、开放式问题解决、创意头脑风暴。
- 混合使用:在复杂系统中,可以外层用Graph定义几个大的阶段,每个阶段内部再用Swarm进行动态任务分配。
7. 高级特性与生产实践
掌握了核心功能后,我们来看看那些能让你的智能体更健壮、更易观测的高级特性和实践。
7.1 流式响应与实时交互
对于需要长时间运行或希望提供实时反馈的应用,流式响应(Streaming)至关重要。Strands Agents的agent.stream()方法返回一个异步迭代器,让你可以实时接收处理过程中的各种事件。
const agent = new Agent({ systemPrompt: '你是一个讲故事的人。', }); async function streamStory() { console.log('开始生成故事...\n'); for await (const event of agent.stream('讲一个关于星辰与猫的短故事。')) { switch (event.type) { case 'llm_start': console.log('[LLM] 开始思考...'); break; case 'llm_token': // 逐词输出模型生成的内容,实现打字机效果 process.stdout.write(event.token); break; case 'tool_call_start': console.log(`\n[工具调用] 开始调用工具: ${event.toolName}`); break; case 'tool_call_end': console.log(`[工具调用] 工具“${event.toolName}”调用完成。`); break; case 'agent_step': console.log(`\n[步骤] 第${event.step}步完成。`); break; case 'agent_end': console.log('\n\n[完成] 故事生成完毕。'); break; case 'error': console.error('\n[错误]', event.error); break; } } } streamStory();流式响应不仅提升了用户体验,也是调试复杂智能体行为的利器。你可以清晰地看到智能体在每一步做了什么决定,调用了什么工具。
7.2 生命周期钩子与监控
SDK提供了丰富的生命周期钩子(Hooks),让你能在智能体运行的关键节点注入自定义逻辑,用于日志记录、性能监控、修改中间结果等。
import { Agent } from '@strands-agents/sdk'; const agent = new Agent({ systemPrompt: '...', }); // 注册钩子 agent.hooks.on('llmStart', (context) => { console.log(`[监控] 开始调用LLM,提示词长度: ${context.messages.length}`); }); agent.hooks.on('llmEnd', (context, response) => { console.log(`[监控] LLM调用结束,消耗Token数: ${response.usage?.totalTokens}`); console.log(`[监控] 模型回复首句: ${response.content?.substring(0, 50)}...`); }); agent.hooks.on('toolCallStart', (context, toolCall) => { console.log(`[监控] 即将调用工具: ${toolCall.name},参数:`, JSON.stringify(toolCall.arguments)); }); agent.hooks.on('toolCallEnd', (context, toolCall, result) => { console.log(`[监控] 工具 ${toolCall.name} 执行完毕,结果长度: ${result?.length || 0}`); }); agent.hooks.on('agentEnd', (context, finalResult) => { console.log(`[监控] 智能体运行结束,总步数: ${context.step},最终结果长度: ${finalResult.content.length}`); });通过这些钩子,你可以轻松集成像OpenTelemetry这样的分布式追踪系统,为每个智能体调用生成详细的追踪链路,便于在生产环境中定位性能瓶颈和异常。
7.3 对话历史管理与上下文窗口控制
智能体的记忆力来自对话历史。SDK提供了灵活的对话历史管理策略。
import { Agent, TokenBufferConversationHistory } from '@strands-agents/sdk'; const agent = new Agent({ systemPrompt: '...', conversationHistory: new TokenBufferConversationHistory({ maxTokens: 3000, // 历史记录的最大Token数 strategy: 'lru', // 当超出限制时,采用LRU(最近最少使用)策略移除最早的消息 }), }); // 进行多轮对话 await agent.invoke('你好,我叫小明。'); await agent.invoke('你还记得我的名字吗?'); // 智能体会记得“小明”TokenBufferConversationHistory会自动计算每条消息的Token消耗(使用近似算法),并确保总历史不超过maxTokens限制。这对于控制API成本和使用长上下文模型至关重要。你也可以实现自己的ConversationHistory类,来实现更复杂的逻辑,比如将历史存储到数据库。
8. 常见问题、排查技巧与性能优化
在实际使用中,你肯定会遇到各种问题。下面是我总结的一些常见坑点和解决思路。
8.1 智能体不调用工具
问题:你明明定义了工具,但智能体总是直接回答,而不去调用工具。排查步骤:
- 检查工具描述:这是最常见的原因。工具的描述是否清晰、无歧义?是否准确说明了工具的用途和输入?用更具体、更具操作性的语言重写描述。
- 检查系统提示词:你的系统提示词是否明确指示智能体“可以使用工具”?尝试在提示词中加入:“你可以使用我提供的工具来获取信息或执行操作。如果你需要的信息不在你的知识范围内,请优先考虑使用工具。”
- 降低温度(Temperature):将模型配置中的
temperature参数调低(如从0.7调到0.2)。较低的温度使模型输出更确定、更遵循指令,可能更倾向于调用工具。 - 查看完整日志:启用调试日志或使用流式API查看
llmEnd事件中的完整模型响应。有时模型在思考过程中提到了工具,但最终选择了不调用。这能帮你理解模型的“决策过程”。
8.2 结构化输出验证持续失败
问题:配置了结构化输出,但LLM始终无法返回有效的格式,达到重试上限后抛出StructuredOutputError。解决思路:
- 简化模式:你的Zod模式是否太复杂?包含太多嵌套、联合类型或高级校验?尝试先用一个极其简单的模式(如
z.object({ answer: z.string() }))测试,确认基础功能正常。 - 增强提示:在系统提示词中明确强调输出格式。例如:“你必须严格按照指定的JSON格式回应。只输出JSON,不要有任何额外的解释或标记。”
- 提供示例:在用户提示词中提供一个输出示例。例如:“请提取信息,并以如下JSON格式输出:{\”name\”: \”示例人名\”, \”age\”: 30}”
- 更换模型:不同的LLM在遵循结构化指令方面能力有差异。Claude系列和GPT-4通常比一些小型或旧模型表现更好。
8.3 多智能体协作陷入循环或停滞
问题:在Swarm模式中,智能体之间来回移交,无法产生最终答案;或者在Graph模式中,某个节点卡住。调试方法:
- 设置
maxSteps:这是最重要的安全网。为Agent和Swarm都设置一个合理的maxSteps(如10-15),防止无限循环。 - 分析节点输出:使用
Graph的getNodeOutput方法或监听Swarm的流式事件,查看每个节点的输入和输出。问题可能出在某个节点的输出质量不高,导致下一个节点无法处理。 - 优化节点提示词:在Swarm中,确保每个节点的
systemPrompt和description清晰定义了其边界和移交条件。例如,明确写明“这是最终步骤,完成后不要移交(handoff)”。 - 引入“裁判”或“终结者”:在Swarm中设置一个专用的
coordinator或finalizer智能体,其职责就是分析当前状态并决定是否应该结束流程。
8.4 性能优化与成本控制
- 复用模型实例:在创建多个
Agent或编排多个智能体时,尽可能复用同一个BedrockModel或OpenAIModel实例。这有助于内部连接的复用和潜在的性能优化。 - 精细控制Token:
- 设置合理的
maxTokens参数,防止单次请求生成过长的内容。 - 使用
TokenBufferConversationHistory并设置合适的maxTokens,自动修剪旧历史。 - 在工具描述和系统提示词中力求简洁,减少不必要的Token消耗。
- 设置合理的
- 异步与并行:对于Graph中可并行执行的节点,确保它们之间没有不必要的依赖边,以充分利用计算资源。
- 实现缓存层:对于频繁查询且结果不变的工具(如某些数据查询),可以在工具回调函数中实现简单的内存缓存或使用外部缓存(如Redis),避免重复调用昂贵的外部API或模型。
经过上面八个部分的拆解,你应该对Strands Agents TypeScript SDK有了一个从入门到进阶的全面认识。从我个人的使用体验来看,它最大的价值在于将AI智能体开发从“脚本小子”的玩具级别,提升到了“软件工程”的严肃级别。类型安全、模式验证、清晰的架构,这些特性使得构建可靠、可维护的AI应用成为可能。当然,框架目前还处于公开预览阶段,API可能会有变动,但核心的设计理念已经非常稳固。如果你正在寻找一个既强大又务实的TypeScript智能体框架,Strands Agents绝对值得你投入时间深入探索。