1. 项目概述一个面向AI代理开发的TypeScript SDK如果你正在尝试构建一个能够自主执行复杂任务的AI代理或者想将大语言模型LLM的能力更深度地集成到你的业务流程中那么你很可能已经感受到了其中的复杂性。从定义代理的思考逻辑、管理工具调用、维护对话历史到处理异步操作和错误每一步都需要大量的底层代码。strands-agents/sdk-typescript这个项目就是为了解决这些痛点而生的。它是一个开源的TypeScript SDK旨在为开发者提供一个结构化、可扩展且类型安全的基础设施用以高效地构建、编排和运行AI代理。简单来说它不是一个“开箱即用”的聊天机器人框架而更像是一个“乐高积木”式的开发工具包。它不强制你使用特定的LLM提供商如OpenAI、Anthropic也不限定你的代理必须长什么样。相反它提供了一套核心的抽象概念如Agent、Tool、Step和一套健壮的执行引擎Runtime让你可以专注于设计代理的“大脑”和“技能”而将繁琐的流程控制、状态管理和工具调度交给SDK来处理。对于中高级的TypeScript/JavaScript开发者而言尤其是在企业级应用或复杂自动化场景中这个SDK能显著降低开发门槛提升系统的可维护性和可靠性。2. 核心架构与设计哲学2.1 核心抽象Agent, Tool, Step 与 Runtime要理解这个SDK首先要吃透它的四个核心抽象这构成了其设计哲学的基石。Agent代理这是系统的核心“演员”。一个Agent不仅仅是一个LLM的调用封装它被定义为一个接收输入、内部状态和上下文然后产生一个或多个Step的函数。关键在于Agent是无副作用的纯函数或异步函数。它只负责“思考”和“计划”即根据当前情况决定下一步要做什么调用工具、生成回复、分解子任务等而不直接执行任何外部操作如读写数据库、调用API。这种设计将决策逻辑与执行逻辑解耦使得代理的逻辑更清晰、更易于测试。Tool工具这是代理的“手”和“脚”。Tool是一个可以执行并产生副作用的操作例如查询数据库、调用外部API、发送邮件、操作文件系统等。每个Tool都有明确的输入和输出Schema通常用Zod等库定义SDK会利用这些Schema来生成供LLM理解的工具描述。当Agent决定调用某个Tool时它会生成一个相应的Step。Step步骤这是连接Agent“思考”和Tool“执行”的桥梁。Step是一个不可变的数据结构代表一个即将发生、正在发生或已经完成的原子操作。它可以是ToolCallStep调用工具、ResponseStep生成最终响应或ParallelStep并行执行多个子步骤等。Runtime会执行Step并将执行结果StepResult反馈给Agent作为下一轮思考的输入。Runtime运行时这是系统的“中央调度器”和“执行引擎”。它负责接收一个初始输入和Agent定义然后循环执行以下流程1) 调用Agent函数获取其计划的下一个或一组Step2) 执行这些Step例如真正调用Tool对应的函数3) 收集Step的执行结果和可能产生的新信息RuntimeLog4) 将结果和历史步骤作为新的上下文再次调用Agent直到Agent产生一个ResponseStep标志着任务完成或达到某些终止条件如最大步数。Runtime管理着整个对话或任务的生命周期、状态持久化和日志流。这种架构的优势在于极高的模块化和可观测性。你可以单独测试Agent的逻辑、Mock Tool的执行、或者通过Runtime的日志清晰地追踪代理的整个决策链路这对于调试复杂代理行为至关重要。2.2 类型安全与开发者体验作为TypeScript项目类型安全是strands-agents/sdk-typescript的一大卖点。它深度利用TypeScript的泛型和条件类型使得整个代理流程的开发体验如同拼装类型安全的积木。例如当你定义一个Tool时你需要提供其输入参数的Zod Schema和实现函数。SDK会自动推断出这个Tool的输入类型和输出类型。当你将这个Tool注册到某个Agent的可用工具列表时Agent函数内部就能获得完美的类型提示——你知道可以调用哪些工具并且调用时需要的参数类型和返回的结果类型都是明确的。这极大地减少了运行时错误并提升了开发效率。同样Runtime执行后返回的最终结果、中间步骤的历史记录其类型也都是根据你所定义的Agent和Tools动态推断出来的。这意味着你可以自信地访问result.finalResponse或遍历history.stepsTypeScript编译器会全程为你保驾护航。注意要充分享受这种类型安全的好处建议在项目中配合使用像zod这样的运行时验证库来定义Schema。SDK通常与zod有很好的集成它能将Zod Schema无缝转换为JSON Schema供LLM使用同时提供坚实的类型基础。3. 从零开始构建你的第一个代理理论说得再多不如动手实践。让我们构建一个简单的“天气查询助手”代理来体验SDK的工作流程。3.1 环境准备与项目初始化首先创建一个新的Node.js项目并安装核心依赖。mkdir weather-agent cd weather-agent npm init -y npm install typescript ts-node types/node --save-dev npm install strands-agents由于SDK本身不绑定具体的LLM我们还需要选择一个LLM客户端。这里以OpenAI为例你需要准备一个OPENAI_API_KEYnpm install openai接着配置tsconfig.json以支持现代ES模块和严格的类型检查。{ compilerOptions: { target: ES2022, module: NodeNext, moduleResolution: NodeNext, esModuleInterop: true, strict: true, skipLibCheck: true, outDir: ./dist, rootDir: ./src }, include: [src/**/*], exclude: [node_modules] }3.2 定义工具获取天气的“手”代理需要工具来获取真实数据。我们定义一个getWeather工具。首先安装zod用于定义输入模式。npm install zod然后创建src/tools/weather.tsimport { z } from zod; import { tool } from strands-agents; // 模拟一个天气API const mockWeatherAPI async (city: string): Promisestring { // 这里本应调用真实的天气API如OpenWeatherMap await new Promise(resolve setTimeout(resolve, 100)); // 模拟网络延迟 const weatherMap: Recordstring, string { 北京: 晴15°C, 上海: 多云18°C, 广州: 阵雨22°C, 深圳: 阴24°C, }; return weatherMap[city] || 未找到城市“${city}”的天气信息。; }; // 使用SDK的tool辅助函数定义工具 export const getWeatherTool tool({ // 工具的唯一标识符LLM将通过此名称来调用 name: get_weather, // 工具的描述LLM用它来理解工具的用途 description: 获取指定城市的当前天气信息。, // 输入参数的Schema使用zod定义确保类型安全和LLM理解 inputSchema: z.object({ city: z.string().describe(城市名称例如北京、上海), }), // 工具的执行函数 execute: async ({ city }) { console.log([Tool Called] 正在查询 ${city} 的天气...); try { const weatherInfo await mockWeatherAPI(city); return { content: weatherInfo, isError: false, }; } catch (error) { return { content: 查询天气失败: ${error.message}, isError: true, }; } }, });这个工具定义清晰地展示了几个要点1)name和description是给LLM看的2)inputSchema用zod定义既约束了TypeScript类型也会被自动转换为JSON Schema传给LLM3)execute函数是实际执行逻辑的地方返回一个标准格式的结果。3.3 构建代理定义“大脑”的思考逻辑接下来在src/agents/weatherAssistant.ts中创建代理。代理的核心是一个函数它接收上下文历史、当前输入等并决定下一步做什么。import { Agent, Step } from strands-agents; import { getWeatherTool } from ../tools/weather.js; // 定义代理的上下文类型 interface WeatherAssistantContext { // 可以在这里定义代理需要维护的长期状态例如用户偏好 userPreferredCity?: string; } // 创建代理函数 export const weatherAssistantAgent: AgentWeatherAssistantContext async ({ input, // 本轮用户输入 context, // 代理的上下文状态 history, // 历史步骤记录 tools, // 可用的工具集这里我们只注入一个工具 }) { // 首先分析用户输入。在实际项目中这里可能会用LLM来分析意图。 // 为了简化我们做一个简单的关键词匹配。 const userMessage input.toString().toLowerCase(); const cityMatch userMessage.match(/(北京|上海|广州|深圳)/); let cityToQuery: string; if (cityMatch) { cityToQuery cityMatch[1]; } else if (context.userPreferredCity) { // 如果用户没指定城市但上下文中有偏好城市则使用它 cityToQuery context.userPreferredCity; // 我们可以决定在调用工具前先给用户一个提示 return new Step.Response(您未指定城市将使用您偏好的城市“${cityToQuery}”进行查询。); // 注意实际更复杂的代理可能会在同一个函数调用中返回多个Step或者使用更复杂的逻辑。 // 这里为了演示我们直接返回响应实际上下一步应该调用工具。 // 更合理的流程见下面的“改进版”。 } else { // 既无输入城市也无偏好城市则询问用户 return new Step.Response(请问您想查询哪个城市的天气); } // 如果确定了要查询的城市则生成一个调用工具的Step return new Step.ToolCall({ tool: get_weather, // 必须与工具定义中的name一致 input: { city: cityToQuery }, }); }; // 改进版一个更贴近真实场景利用LLM进行决策的Agent函数框架 // 假设我们有一个LLM客户端实例 llmClient /* export const smartWeatherAssistantAgent: AgentWeatherAssistantContext async ({ input, context, history, tools }) { // 1. 将历史、上下文、可用工具列表构造成LLM的提示词Prompt const messages constructMessages(history, input, tools); // 2. 调用LLM请求其决定下一步行动可能是回复也可能是调用某个工具及参数 const llmResponse await llmClient.chat.completions.create({ model: gpt-4, messages, tools: convertToolsToOpenAITools(tools), // 将SDK工具格式转换为OpenAI工具调用格式 }); // 3. 解析LLM的响应将其转换为SDK的Step对象 const choice llmResponse.choices[0]; if (choice.message.tool_calls) { // LLM决定调用工具 const toolCall choice.message.tool_calls[0]; return new Step.ToolCall({ tool: toolCall.function.name, input: JSON.parse(toolCall.function.arguments), }); } else { // LLM决定直接回复 return new Step.Response(choice.message.content || ); } }; */在基础版中我们用了简单的规则逻辑。注释中的“改进版”展示了如何集成真实的LLM如GPT-4来让代理自主决策。strands-agentsSDK的设计巧妙之处在于它不关心你的Agent函数内部是如何做出决策的——无论是规则引擎、LLM调用还是其他AI模型——它只关心你的函数最终返回一个合法的Step对象。3.4 配置运行时并执行任务最后我们将所有部分组装起来在src/main.ts中创建Runtime并运行代理。import { Runtime } from strands-agents; import { weatherAssistantAgent } from ./agents/weatherAssistant.js; import { getWeatherTool } from ./tools/weather.js; async function main() { // 1. 创建Runtime实例并为其配置可用的工具 const runtime new Runtime({ agent: weatherAssistantAgent, tools: [getWeatherTool], // 将工具注册到Runtime这样它才能被正确执行 }); // 2. 定义初始上下文可选 const initialContext { // userPreferredCity: 北京, // 可以在这里设置初始偏好 }; console.log( 天气查询助手启动 ); console.log(尝试输入“上海天气怎么样”或直接说“查询天气”); // 3. 模拟一个交互循环在实际应用中这可能是HTTP服务器或消息队列的消费者 const userInputs [上海天气怎么样, 那北京呢, 谢谢]; let currentContext initialContext; let accumulatedHistory: any []; // 用于累积历史在实际运行时由Runtime管理 for (const userInput of userInputs) { console.log(\n[用户] ${userInput}); // 4. 使用Runtime执行单轮交互 // 注意在实际的持续对话中我们需要将上一轮的历史传递给下一轮。 // 这里简化了流程每轮都是独立的。更完整的例子会维护一个会话状态。 const result await runtime.run({ input: userInput, context: currentContext, // history: accumulatedHistory, // 在真实多轮对话中需要传递历史 }); // 5. 处理结果 if (result.isSuccess()) { const finalStep result.finalStep; if (finalStep instanceof Step.Response) { console.log([助手] ${finalStep.content}); } else if (finalStep instanceof Step.ToolCall) { // 工具调用结果会在result.logs或finalStep.result中 const toolResult finalStep.result; if (toolResult !toolResult.isError) { console.log([助手] 查询结果${toolResult.content}); } else { console.log([助手] 工具执行出错${toolResult?.content}); } } // 更新上下文和历史在真实场景中Runtime可能会返回更新后的上下文 // currentContext result.newContext; // accumulatedHistory result.history; } else { console.error([运行时错误], result.error); } } console.log(\n 会话结束 ); } main().catch(console.error);运行这个程序(npx ts-node src/main.ts)你将看到代理根据输入决定是直接回复、询问还是调用天气工具。这个简单的例子揭示了SDK的核心工作流定义工具 - 构建代理决策函数 - 通过Runtime运行。4. 高级特性与实战模式掌握了基础之后我们可以探索SDK更强大的能力以应对复杂场景。4.1 并行步骤与复杂工作流现实任务很少是线性的。一个代理可能需要同时查询多个数据源或者在执行主任务时并行处理一些辅助任务。Step.Parallel允许你做到这一点。假设我们的天气助手升级了用户问“对比一下北京和上海的天气”我们希望并行查询两个城市。// 在Agent函数内部 if (userMessage.includes(对比)) { const citiesToCompare [北京, 上海]; // 实际中应从输入中提取 // 创建并行步骤 return new Step.Parallel({ steps: citiesToCompare.map(city new Step.ToolCall({ tool: get_weather, input: { city }, }) ), // 可以指定一个“聚合”函数在所有并行步骤完成后将结果合并处理 // reducer: (results) new Step.Response(对比结果...), }); }Runtime会并发执行这些ToolCall步骤等待所有步骤完成后再继续下一步。你可以通过reducer函数定义如何聚合这些结果并生成下一个Step比如一个总结对比的ResponseStep。4.2 上下文管理与状态持久化在长时间运行的对话或自动化任务中代理需要记住一些信息。这就是Context的用武之地。Context是一个在Runtime执行过程中可以被Agent读取和修改的任意类型的状态对象。最佳实践不可变更新Agent函数接收context参数并可以在返回Step的同时返回一个新的上下文对象。Runtime会在下一轮迭代中将更新后的上下文传递回来。这鼓励了不可变更新模式使得状态变化更可预测、更易于调试。export const userProfileAgent: Agent{ userName?: string; queryCount: number } async ({ input, context }) { const newContext { ...context, queryCount: context.queryCount 1 }; if (input.includes(我叫)) { const name extractName(input); newContext.userName name; return { step: new Step.Response(好的${name}我记住你了。), context: newContext, // 返回更新后的上下文 }; } if (context.userName) { return new Step.Response(${context.userName}这是您今天的第${newContext.queryCount}次查询。); } // ... };状态持久化对于需要跨会话或服务器重启保持状态的场景你需要自行实现持久化逻辑。通常的做法是在Runtime执行前从数据库加载上下文在执行后将Runtime返回的新上下文保存回数据库。SDK的Runtime.run方法接受初始上下文并返回执行后的最终上下文这与你自己的存储层可以很好地集成。4.3 自定义工具与复杂输入输出工具不仅限于简单的API调用。你可以创建具有复杂逻辑和丰富输出的工具。输出结构化数据工具可以返回任何JSON可序列化的数据而不仅仅是字符串。这对于后续步骤的处理非常有用。export const analyzeSalesDataTool tool({ name: analyze_sales, description: 分析指定时间段的销售数据并返回关键指标。, inputSchema: z.object({ startDate: z.string(), endDate: z.string() }), execute: async ({ startDate, endDate }) { // 模拟数据分析 const metrics { totalRevenue: 150000, averageOrderValue: 250, topProduct: { name: Product A, sales: 50000 }, trend: upward, }; return { content: metrics, // 返回一个对象 isError: false, }; }, });当Agent调用这个工具后工具返回的metrics对象会成为后续步骤上下文的一部分Agent或后续的工具可以基于这些结构化数据做进一步决策例如如果trend是downward则触发一个警报工具。处理流式或异步长时间任务有些工具执行时间很长如训练模型、处理视频。一个好的模式是让工具立即返回一个任务ID然后通过另一个“检查状态”的工具来轮询结果。SDK的异步Step执行模型能很好地支持这种模式。5. 生产环境部署与最佳实践将基于strands-agents的代理投入生产环境需要考虑以下几个方面。5.1 错误处理与鲁棒性工具级错误处理每个工具的execute函数都应该有完善的try-catch并返回格式化的错误信息{ content: string, isError: true }。这允许Agent在收到错误后有机会尝试其他方案或向用户报告友好错误。Runtime级错误处理Runtime的执行也可能出错如网络超时、LLM服务不可用。确保用try-catch包裹runtime.run的调用并实现重试、降级或警报逻辑。设置超时与最大步数在创建Runtime时务必设置maxSteps和timeout选项防止代理陷入无限循环或长时间无响应。const runtime new Runtime({ agent: myAgent, tools: myTools, maxSteps: 50, // 防止无限循环 timeout: 30000, // 30秒超时 });5.2 可观测性与日志记录SDK的Runtime在执行过程中会生成详细的RuntimeLog。在生产环境中你应该将这些日志收集到像ELK Stack、Datadog或Sentry这样的可观测性平台。const runtime new Runtime({ agent: myAgent, tools: myTools, // 可以提供一个自定义的logger logger: { info: (msg, meta) console.log([INFO] ${msg}, meta), error: (msg, meta) console.error([ERROR] ${msg}, meta), debug: (msg, meta) console.debug([DEBUG] ${msg}, meta), } }); // 运行后可以从result中获取详细的logs const result await runtime.run(...); console.log(本次执行日志:, result.logs);日志中会包含每一步的开始结束时间、工具调用的输入输出、Agent的决策等信息是调试复杂代理行为和进行性能分析的无价之宝。5.3 性能优化与扩展工具缓存对于频繁调用且结果变化不快的工具如某些数据查询可以在工具内部或外层添加缓存层如Redis避免重复计算或API调用。Agent逻辑优化Agent函数应尽可能保持轻量级避免在其中进行昂贵的计算或同步IO操作。它的主要职责是“快速决策”。水平扩展Runtime本身是无状态的状态存在于传递的Context和History中。这意味着你可以轻松地水平扩展你的代理服务。在多实例部署中只需确保Context的持久化存储如数据库是所有实例共享的即可。5.4 安全考量工具权限控制不是所有代理都应该能调用所有工具。你可以在Runtime层面或Agent函数内部实现权限检查。例如根据代理的身份或上下文中的用户角色动态过滤可用的tools列表。const runtimeForUser new Runtime({ agent: myAgent, tools: getPermittedToolsForUser(currentUser), // 根据用户返回工具子集 });输入验证与清理尽管有Zod Schema验证工具的输入但来自用户或外部系统的原始输入在传递给Agent之前仍需进行必要的清理和验证防止注入攻击。LLM提示词安全确保传递给LLM的提示词Prompt不包含敏感信息并对LLM的输出进行后处理过滤不当内容。6. 常见问题与排查技巧实录在实际使用中你可能会遇到一些典型问题。以下是一些记录和解决方案。6.1 工具调用失败名称不匹配或参数错误问题现象Agent返回了一个ToolCallStep但Runtime执行时报错“Tool not found”或工具执行时参数验证失败。排查步骤检查工具名称确保Step.ToolCall中的tool属性字符串与工具定义时的name属性完全一致大小写敏感。检查输入Schema使用zod的parse或safeParse方法在工具外部测试你的输入对象确保其符合Schema。LLM生成的参数有时格式会略有偏差如数字被写成字符串。查看Runtime日志开启Debug日志查看Runtime接收到Step的详细信息以及它尝试调用工具时传递的具体参数。实操心得为每个工具编写简单的单元测试是非常值得的。测试不仅验证工具函数本身也验证其输入Schema是否能正确解析你期望的输入格式。6.2 代理陷入循环或无法终止问题现象代理不停地调用工具或思考始终不产生最终的ResponseStep。原因与解决LLM指令不清晰如果你的Agent依赖LLM可能是你的Prompt没有明确指示在什么条件下任务算完成。在Prompt中强调“当你获得了所有必要信息后请用最终答案来回复用户”。缺少终止条件SDK的maxSteps是最后一道防线。你应该在Agent逻辑中设计更智能的终止条件。例如在上下文Context中设置一个consecutiveEmptySteps计数器如果Agent连续多次返回无实质内容的Step或重复的Step则强制返回一个结束响应的Step。工具副作用未更新上下文有时Agent在等待某个工具改变外部世界状态然后根据新状态做决策。如果工具执行成功但未能让Agent感知到状态变化例如变化未反映在下一轮传入的Context中代理可能会重复调用同一个工具。确保状态变更能通过上下文或工具返回结果有效传递。6.3 类型推断不工作或报错问题场景在Agent函数内部tools对象没有正确的类型提示或者调用工具时参数类型为any。解决方案确保使用tool()辅助函数定义工具时务必使用从strands-agents导入的tool函数它能提供丰富的类型信息。正确注入工具创建Runtime时tools数组必须包含你定义的工具实例。TypeScript会根据这个数组推断出所有可用工具的类型联合。使用satisfies关键字如适用如果你动态生成工具数组可以使用TypeScript的satisfies关键字来帮助类型推断const myTools [getWeatherTool, analyzeSalesTool] as const satisfies Tool[]; const runtime new Runtime({ agent: myAgent, tools: myTools });检查TypeScript配置确保tsconfig.json中的strict模式已开启并且没有忽略node_modules中types包的类型。6.4 处理流式响应与实时交互需求对于需要长时间运行或希望实时向用户推送进度的代理如“正在为您生成报告已完成30%...”标准的一次性runtime.run模式不太适合。模式建议分步执行与中间响应不要等待Runtime跑完所有步骤。你可以使用更低层的API手动控制执行循环。在每执行完一个Step特别是生成了有意义中间结果的Step后就暂停一下将当前结果如“已找到10条相关数据”通过WebSocket或Server-Sent Events推送给前端。使用Step.Response作为进度更新Agent可以在任务中间返回ResponseStep来发送进度信息然后紧接着返回下一个ToolCallStep继续工作。前端可以将这些中间响应实时显示出来。异步工具与回调对于非常耗时的工具可以将其设计为异步触发并立即返回一个“任务已开始”的Response。然后通过另一个轮询工具或外部回调机制在后台任务完成后通知代理继续。构建基于AI代理的应用是一场充满挑战但也极具回报的旅程。strands-agents/sdk-typescript提供的这套类型安全、抽象合理的框架就像为你提供了坚固的脚手架和精良的工具让你能更专注于让代理变得“智能”的核心逻辑而不是陷于胶水代码的泥潭。从简单的自动化脚本到复杂的多智能体协作系统这个SDK都能提供可靠的支撑。我个人的体会是初期花时间理解其Agent、Tool、Step、Runtime的核心抽象模型是至关重要的一旦掌握开发效率会大幅提升。最后多利用其强大的类型系统和运行时日志它们是你调试和优化代理行为的最佳伙伴。