
拿大模型 API 写一个问答机器人这件事我闭着眼睛都能完成一个 HTTP 请求、一段 system prompt、一条用户消息回答就返回了。但一旦要让 AI 真正“干活”——查数据库、改文件、跑测试、调外部服务——普通 API 调用立刻顶不住。原因很简单API 只给你一次“思考回复”的瞬间能力而干活需要的是“思考 → 行动 → 观察结果 → 再思考”的循环。Claude Agent SDK 正是冲着这个缺口来的它把模型、工具、循环和会话状态打包成一个可编程运行时让我可以用代码直接控制一个 AI 助手去执行多步任务。这篇文章我会把 Agent 循环的原理讲透再带你从零搭环境、定义自定义工具最后用一个“自动整理代码仓库”的实战案例收尾并聊聊生产化改造时的步数、权限、成本和那些文档里不会写的坑。适合已经写过 Claude API 调用、现在想往 agent 方向走的人也适合手里有一堆重复杂活、想找一个可靠框架来承接的开发者。1. 从问答接口到干活循环Agent SDK 到底补上了什么1.1 普通 API 调用缺的不是“智能”是“循环”很多人在第一次接触 Agent 这个概念时都会有个困惑我不就是用 prompt 让模型去调用工具吗Messages API 本身就支持 tool use我自己写个 if-else 也能把工具结果塞回去为什么要多一个 SDK我早期也这么干过。自己维护一个 while 循环判断响应里有没有 tool_use 块有就执行本地函数再把结果拼成新的 user 消息发回去。跑通一个 demo 不难难在它只解决了“单轮工具调用”。真实任务往往是这样模型要先列目录看到一堆文件后决定读某个文件读完又发现需要去查 git 历史查完又回来改写文件最后还要验证结果。这个链条可能有七八轮每一轮都要处理截断、错误、并行调用、上下文膨胀。更麻烦的是模型每次重试都会把整段历史重新送进去token 消耗成倍增长。普通 API 调用的本质缺陷是它没有“状态”。每次请求都是一次独立判断而 Agent 需要的是一个持续运转、能记住自己刚才做过什么、并根据结果调整下一步的执行器。所谓 Agent 化不是让模型变得更聪明而是给模型配上一套可以持续行动的循环。Claude Agent SDK 核心就是把这个循环做成标准件并且把循环里的每一步都开放给你观测和干预。1.2 Agent 循环的五个阶段我习惯把 Agent 的一次完整任务理解成五段式循环理解了它你基本就理解了整个 SDK。第一段是“注入”。SDK 把系统提示词、工具定义、历史消息和当前用户指令整理成一次大模型的请求。第二段是“决策”。模型返回两类东西之一要么是普通文本表示它认为任务已经完成要么是一个或多个 tool_use 块表示它希望调用某个工具并附带结构化参数。第三段是“执行”。SDK 拿到 tool_use 后找出对应的本地函数或外部服务把参数传进去运行。第四段是“反馈”。执行结果被包装成 tool_result 消息连同工具的标识 ID 一起送回模型模型能看到“你刚才那步做完了结果是这个”。第五段是“回到第二段”。模型根据结果继续决策直到它觉得自己干完了输出最终文本为止。这里有个容易误解的点工具调用本身不复杂复杂的是“多轮”。一个循环里可能有三次、八次甚至二十次工具调用模型需要根据上一次的结果修正下一次的参数。比如它第一次用 Bash 列目录发现文件不在预期位置第二次就会换一个路径去查。这种自主修正就是 Agent 和普通“函数调用 demo”的本质区别。SDK 把这些细节全部管理起来了包括并行 tool_use 的执行、工具返回过大时的截断、会话历史的自动维护。1.3 它在整个工具链里的位置如果你用过 Claude Code 的命令行交互版可以把 Agent SDK 理解成它的“可编程内核”。命令行交互版适合人在终端里实时对话而 Agent SDK 适合把同一套 Agent 能力嵌进你自己的服务、定时任务或自动化流水线里。两者共用底层的一整套能力内置工具Bash、读文件、写文件、编辑、MCP 服务连接、权限控制、会话恢复。再往下对比Messages API 是“一锤子买卖”适合你明确知道自己要发什么、收什么。Agent SDK 是“按项目承包”它承载的是不确定性较高的多步任务你告诉它目标它在循环里自己摸索路径。中间还有一层就是 MCP它解决的是“工具从哪里来”的问题。MCP 服务可以把外部系统数据库、浏览器、内部系统包装成标准工具Agent SDK 直接消费这些工具。所以你可以这么理解MCP 是给 Agent 装新器官Agent SDK 是给 Agent 装身体模型是大脑而你是那个负责设定边界和验收结果的人。2. 环境准备与第一个能自主调用工具的 Agent2.1 安装和鉴权里两个容易翻车的细节先说我踩过的第一个坑Node 版本。Agent SDK 是 TypeScript 优先的依赖一些比较新的运行时特性我最早在 Node 16 的环境里装启动时直接报语法错误。后来统一换到 Node 18 以上才稳定。如果你用的是 npm安装很简单一条命令npm install anthropic-ai/claude-agent-sdk鉴权方面SDK 默认从环境变量读取 API 密钥。建议在项目根目录放一个.env文件用 dotenv 加载ANTHROPIC_API_KEYsk-ant-...。这里有两个细节一是密钥千万别写进代码仓库我见过不止一次有人把本地测试用的密钥提交上去然后账单爆炸二是如果你在团队协作最好通过密钥管理服务注入环境变量而不是每个人各自复制一份。另外SDK 跑起来会真实消耗 token本地调试时建议先给账号设置好预算上限别问我为什么强调这一点。2.2 最小可运行示例让 Agent 自己决定用什么工具我先把话说明白下面代码基于我常用的 SDK 版本写法这类库迭代很快小版本之间字段可能微调。你跑之前先看一眼node_modules里自带的类型定义实在对不上就按编辑器提示改这比硬记文档里的例子更稳。先做一个不接任何自定义工具的版本让 Agent 用内置的 Bash 工具去完成一个需要“观察后行动”的任务import dotenv/config; import { Agent } from anthropic-ai/claude-agent-sdk; const agent new Agent({ prompt: 你是一位可靠的开发助手。回答前先观察工作区需要验证时使用工具不要凭空猜测。, }); async function main() { for await (const event of agent.start({ prompt: 列出当前目录下所有文件按体积从大到小排序后输出前 10 个。, })) { if (event.type stream_event) { const text event.event.content_block_delta.delta?.text; if (text) process.stdout.write(text); } } } main().catch((e) { console.error(e); process.exit(1); });这个例子看着简单但已经包含了 Agent 的核心体验我并没有告诉它“用 Bash 运行 ls”只说了一个目标。它会自己决定调用 Bash自己决定怎么组装命令然后把结果读一遍再输出。你观察事件流就能看到模型先输出一段“让我看看目录下有什么”然后产生一个 tool_useSDK 执行工具模型拿到结果后再写最终回答。这就是编程控制 AI 助手的起点你控制的是目标和边界而不是每一步的具体动作。2.3 事件流里有哪些值得关注的信号第一次跑通 Agent 后建议别急着加功能先花十分钟把事件流打出来看看。SDK 默认暴露的是一系列流式事件我在调试时最常关注这几类事件类型出现时机调试时的价值stream_event模型边生成边推送看模型“在想什么”实时输出文本tool_event某个工具开始/结束执行确认模型的行动路径、工具参数message_event一整条消息完成拿到完整消息和 token 用量session_event会话建立/更新拿到 session_id用于后续恢复result_event多子代理任务的局部结果观察子代理返回内容我一般会在本地写一个小函数把 tool_event 单独高亮打印因为它能非常直观地暴露模型是不是在同一个工具上反复打转。如果看到它连续三次用几乎一样的参数调用同一个工具说明提示词或者工具描述写得有问题得尽早干预。这个观测习惯在你做生产化改造时会变成救命稻草。另外这类事件类型的字段名在各版本间有变动。一个很实用的办法是先跑一个空任务把完整事件对象JSON.stringify打印出来对照实际结构再写你的解析逻辑。官方文档永远是对的但“你本地装到的那个版本”才是你真正要面对的。3. 自定义工具把外部能力交到 Agent 手里3.1 定义第一个自定义工具内置工具再全也不可能覆盖你系统的专有能力。比如你希望 Agent 能查你们内部的订单状态或者给你自己的后端接口发请求这时就需要自定义工具。SDK 提供了类似装饰器的定义方式我常用的写法是这样import { Agent, defineCustomTool } from anthropic-ai/claude-agent-sdk; const queryOrderStatus defineCustomTool({ name: query_order_status, description: 查询内部订单系统的订单状态。传入订单号返回当前状态和最近一次更新时间。, inputSchema: { type: object, properties: { orderId: { type: string, description: 订单号格式如 ORD-2025-0001 }, }, required: [orderId], }, handler: async ({ orderId }) { const result await fetchOrderStatus(orderId); // 你自己的业务逻辑 return { type: success, data: { orderId, status: result.status, updatedAt: result.updatedAt, }, }; }, }); const agent new Agent({ prompt: 你是一个售后助手查询订单时先确认订单号格式正确。, tools: { query_order_status: queryOrderStatus }, });整个工作流程很简单模型在决策阶段看到工具的名字、描述和 inputSchema判断“我需要查订单”于是生成一个带参数的 tool_useSDK 拿到参数后调用你的 handlerhandler 的返回值会被序列化回传给模型。你在 handler 里写什么业务逻辑都行调用数据库、请求内网接口、操作文件系统对模型来说它只是“一个工具”。3.2 输入 Schema 是写给模型看的“说明书”这是我认为自定义工具里最值得花功夫的地方。很多人把 inputSchema 当成后端接口的校验参数来写只写类型不写说明。但你要记住这个 Schema 的第一个读者不是你的后端代码而是大模型。模型根据字段描述来决定传什么值描述写得越模糊它给出的参数就越离谱。举个例子一个订单查询工具如果你只写orderId: string模型很可能传一个“用户的订单号”进去而你的系统实际要的是带前缀的标准格式。我建议在每个字段描述里带上格式示例和取值规则甚至可以写“如果用户只提供了部分信息不要猜测询问完整条件”。必要的时候用 enum 把可选值框死properties: { timeRange: { type: string, enum: [today, this_week, this_month], description: 查询范围必须是这三个值之一, }, }这个习惯的价值在长期运行里体现得很明显模型在大部分情况下都会严格遵循枚举和格式因为描述给了它足够强的约束信号。工具越“笨”、越确定Agent 整体就越可靠。3.3 工具失败之后的兜底设计自定义工具在执行阶段一定会出错接口超时、数据不存在、权限不足。这时候最忌讳的做法是让 handler 直接抛异常因为异常会中断整个循环模型拿不到失败原因你的任务也就戛然而止。更合理的做法是把失败本身包装成返回值的一部分让模型知道发生了什么、有没有别的路径可走。我常用的模式是返回一个带status字段的结构成功和失败都走同一个出口handler: async ({ orderId }) { try { const result await fetchOrderStatus(orderId); return { type: success, data: result }; } catch (e) { return { type: error, data: { message: 订单 ${orderId} 查询失败${e.message}可能原因单号格式错误或系统维护中 }, }; } }模型读到这个错误信息后通常有两种反应要么纠正参数重新调用要么直接告诉用户“当前无法查询可能是系统维护”而不会陷入沉默或幻觉。你还可以在 description 里明确写“查询失败时尝试一次后停止不要重复调用”这能有效降低模型在同一个坏工具上反复重试的概率。另一个需要注意的点是工具返回体量。如果你的工具返回一个几万字符的日志模型读起来又慢又贵甚至直接触发上下文截断。好的做法是在 handler 里做好摘要和切片只回传关键字段细节留在日志文件里让模型需要时再主动去查。4. 实战用 Agent 自动整理一个混乱的代码仓库4.1 先把任务拆成工具集理论讲再多不如跑一个真实任务。我拿一个常见的场景举例接手了一个历史项目仓库里散落着临时脚本、调试日志、编译产物文档目录里有好几个命名混乱的旧版本说明。人工整理很烦而且每个人整理完的标准还不一样。用 Agent 来做好处是它可以边看边决策而且每次整理的决策过程都能留下记录。我没有直接把 Bash、Write、Edit 全部开放给它而是定义了一组更贴近任务语义的自定义工具让模型在有限的、安全的操作集合里工作工具名作用安全约束list_directory列出目录下的文件与大小只读不进入 node_modulesread_text_file读取指定文本文件前若干行只读限制单次读取大小create_directory新建目录限制新建路径必须在项目内move_file移动文件到目标目录禁止移动隐藏配置delete_file删除指定文件必须二次确认文件名匹配write_report把整理报告写入指定文档只允许写入 docs 目录每个工具的描述里我都写了它适合什么场景、禁止什么场景。这一层约束非常关键模型不会主动越界但你要把边界画出来。它在这个工具集里能做的所有事情都是可审计、可回滚的这就让整个整理流程可以放心挂到自动化任务里。4.2 运行实录Agent 是怎么一步步干完的下面是这个任务的一次简化运行记录省略了模型的长篇文本只看工具调用链路就能感受到“循环”的节奏工具调用list_directory({ path: . }) → 发现根目录有 temp_data.csv、app.log、backup/、docs/、src/ 工具调用read_text_file({ path: temp_data.csv, lines: 5 }) → 确认是某次调试临时导出的数据 工具调用create_directory({ path: archive/temp }) 工具调用move_file({ source: temp_data.csv, target: archive/temp/temp_data.csv }) 工具调用list_directory({ path: docs }) → 发现 2024 旧版说明、2025 新版说明、README_backup.md 工具调用read_text_file({ path: docs/README_backup.md, lines: 10 }) → 内容与主 README 重复判断为遗留备份 工具调用delete_file({ path: docs/README_backup.md }) → 删除前模型自己停下来输出“确认这个文件已不被任何入口引用” 工具调用write_report({ path: docs/reorganize_report.md, content: ... }) → 写入完整整理报告注意第三步到第四步之间模型没有问我要不要执行因为它在系统提示词里已经被告知“这是一个自动化整理任务遇到明显分类清晰的项可以直接执行”。但它删除文件前会先读内容验证这个验证动作不是我在代码里写死的而是模型根据“安全约束”描述自己形成的判断。这是 Agent 和普通脚本最大的区别脚本只会按你写的规则执行Agent 会在规则之内动态调整自己的执行策略。这个案例跑下来大概花了 6 分钟、四十多次工具调用比人工快但更重要的是过程留痕。整理完的仓库长什么样、每个文件为什么去了那里全部写在报告里。对团队协作来说这种可解释性比“快”更有价值。4.3 会话恢复与 Checkpoint干到一半断了怎么办长任务最烦的问题就是中断网络抖动、进程被杀、API 报错。以前我自己写的简易循环一断就全完因为所有历史都在内存里。Agent SDK 的会话机制解决的就是这个问题。每次启动任务时留意 session_event 里返回的 session_id把它存到本地或者数据库。下次需要继续时在 start 参数里带上这个 session_idconst sessionId await readLastSessionId(); for await (const event of agent.start({ prompt: 继续刚才的仓库整理任务先报告已经完成的部分。, session_id: sessionId, })) { // 处理事件 }模型会带着之前的上下文继续干活它能记得自己已经把临时文件归档了、正在处理文档目录而不是从头开始一遍。如果任务特别长还可以开启 Checkpoint 能力SDK 会在关键节点保存会话快照进程崩溃后可以从最近的快照恢复而不是从零重跑。我在一个每周自动清理构建产物的任务里用了这套机制之前偶尔会因为中途超时导致重复清理现在中断后能续上重复操作明显减少了。5. 生产化改造步数限制、权限闸门与成本控制5.1 maxTurns 与 permissionMode给 Agent 装上刹车跑通 demo 之后第一个要考虑的问题是如果 Agent 失控了怎么办我的一个同事把一句“清理磁盘”交给 Agent它扫描完目录后开始递归寻找大文件一步步往系统目录深处走要不是设置了最大步数它能跑出一个惊人的 token 账单。这就是为什么我在所有生产级任务里都会显式设置maxTurns。任务越危险、越开放上限就越低。整理文件的活我一般给 40 步代码审查类任务给 60 步纯问答不到 10 步就够。超步数后任务会抛出一个可捕获的错误你可以在外面决定是重试、给更多步数还是直接放弃。权限模式也要从一开始就考虑清楚。SDK 里有几档默认模式遇到未授权工具会停下来等人确认适合交互场景计划模式只允许模型规划、不允许执行适合先让它出方案、人看完再动手还有直接放行的模式适合完全可信的 CI 环境。我自己的偏好是交互式任务保持默认自动化任务里把所有工具白名单列全高危操作一律靠 hooks 拦截而不是简单粗暴地全局放行。5.2 Hooks在工具执行前后插入你自己的规则Hook 是生产化改造里最实用的机制它让你在 Agent 每次调用工具之前和之后介入控制。我最常做的两件事拦截高危命令、强加人工审批。下面的代码示意了一个 PreToolUse hook 的拦截逻辑const agent new Agent({ // ... hooks: [ { matcher: PreToolUse, hook: async (input) { const command input.tool_input.command ?? ; if (input.tool_name Bash /rm\s-rf/.test(command)) { return { hookSpecificOutput: { hookDecision: block, reason: 高危删除命令已被策略拦截请改用安全归档方式, }, }; } return { hookSpecificOutput: { hookDecision: continue } }; }, }, ], });这里的关键点是被 block 后模型会收到一条拦截原因它不会直接放弃而是换一种不违规的方式继续尝试。比如你禁止了rm -rf它会改用移动到临时目录再定期清理最终目标还是达成了。Hook 不是要把 Agent 变成提线木偶而是把它的行动路径约束在你信任的范围内。生产环境里我还建议在 PostToolUse 阶段做审计把每一步的工具名、参数、返回状态拼成结构化日志存起来。出了问题能回溯整个决策链用户质疑时有据可查模型优化时也有真实数据可以看。这套日志我做下来发现远比想象中有价值。5.3 并发、限流与成本观测Agent SDK 每个 Agent 实例管理自己的会话多个任务可以并行跑。但并行不是免费的每个任务都在消耗 API 配额你本地跑 10 个整理任务等于瞬间发出去 10 路的请求。遇到 429 限流或者超时不要裸奔式重试我用的是指数退避第一次等 2 秒第二次 4 秒第三次 8 秒配合最大重试次数。暴力重试只会让限流来得更快。成本观测是很多人忽略的大问题。一个多步 Agent 任务看起来只处理一个小需求实际 token 消耗可能是普通问答的几十倍。我在接入生产之前特意写了个汇总逻辑把每个事件里的 usage 数据累加起来let inputTokens 0; let outputTokens 0; for await (const event of agent.start({ prompt })) { if (event.type message_event) { const usage event.event.usage; inputTokens usage.input_tokens ?? 0; outputTokens usage.output_tokens ?? 0; } } console.log(input: ${inputTokens}, output: ${outputTokens});有了这个数据你才能判断某个任务是不是“成本黑洞”。我见过一个看似简单的文档整理任务因为工具描述不清晰模型反复读大文件、反复确认最后 token 消耗是正常值的 8 倍。成本问题本质上是设计问题工具描述越清楚、返回值越精简、约束越明确模型的无效尝试就越少账单自然可控。6. 踩坑记录与“该不该用 Agent”的判断标准6.1 我在实际项目里踩过的三个坑第一个坑是提示词写得像“客服”。我给一个内部工具写的系统提示是“你是一个友好助手乐于帮助用户解决问题”结果模型在任务里特别啰嗦每做一步都要解释一遍。后来改成“你是任务执行器完成目标后只输出结果清单不要额外说明动机”整个任务立刻利落很多。记住Agent 不是陪聊提示词里应该写清楚任务的完成标准、输出格式和终止条件。第二个坑是工具描述里的“承诺”太满。我给查询服务写的描述是“返回订单所有信息”结果模型以为一次调用就能拿到物流、金额、发票全部内容实际上我的接口只能返回基础状态。模型只能按照描述去理解工具能力所以描述必须写清楚边界“仅返回 CSV 文件列表不返回内容如需要读取内容请使用另一个工具”。边界写清楚模型就不会产生不切实际的预期。第三个坑是我之前提过的重试问题。有一次 Agent 在调用一个偶发超时的工具时连续重试了五次每次都把同样的参数原样传进去。后来我在工具描述里加了“如果连续两次失败直接向用户报告不要自动重试”同时在 PostToolUse hook 里统计了同一工具连续失败次数超过阈值直接中断。两层防护之后这个问题才真正解决。6.2 适合 Agent 的任务和不该交给它的任务用了大半年之后我对“什么活适合 Agent”的判断标准越来越简单目标明确、过程开放、验证容易。整理文件、代码审查、跑测试并修问题、抓日志定位故障这类任务都合适因为结果是否达成很容易验证路径让模型自己探索反而效率更高。反过来目标模糊、结果不可验证、涉及重大责任的场景我坚决不碰。比如涉及财务对账的任务哪怕模型最后给出了和人工一样的数字我也不愿意为中间任何一步的不确定性负责还有需要大量主观判断的创意工作模型容易“过度发挥”反而需要人来反复返工。另一个雷区是“持续实时决策”比如在线交易系统的自动监控Agent 的循环节奏是几百毫秒到几秒一次不适合毫秒级响应场景。下面是适合与不适合的任务对照我接到需求时都会先过一遍这张表适合交给 Agent不适合交给 Agent仓库、文档、日志清理财务对账与合同审核重复性代码修改与批量重构不可逆的高风险写入操作故障排查与数据采集需要实时毫秒级响应的控制逻辑测试执行与结果汇总高度依赖人类直觉与主观审美的产出定期的数据处理流水线法律法规相关的决策性输出如果我不确定就让 Agent 先跑一个“只读版本”只观察、只规划、不执行把方案先交上来。这个习惯帮我挡住了不少本来会造成事故的任务。我第一次在团队里推广 Agent 方案时定的规矩也是头两周全部走只读模式跑顺了再逐步放开写权限。现在回想起来这个渐进路径比一上来就上生产任务稳妥得多。你第一次跑自己的 Agent 任务时也建议遵循同样的节奏先让它观察再让它建议最后才让它动手。