claude-agent-sdk:Agent 循环的工作原理

14 阅读20分钟

code.claude.com/docs/en/age…

代理循环的工作原理

Understand the message lifecycle, tool execution, context window, and architecture that power your SDK agents.

Agent SDK 允许您将 Claude Code 的自主代理循环嵌入到自己的应用程序中。该 SDK 是一个独立的软件包,为您提供对工具、权限、成本限制和输出的程序化控制。您不需要安装 Claude Code CLI 就可以使用它。当你启动一个代理时,SDK 会运行与 Claude Code 相同的核心执行循环:Claude 评估你的提示,调用工具执行操作,接收结果,并重复此过程直至任务完成。本页面将解释该循环内部的运作机制,以便你能够有效构建、调试和优化你的代理。

循环概览

每个代理会话都遵循相同的循环:Agent loop: prompt enters, Claude evaluates, branches to tool calls or final answer

  1. 接收提示。  Claude 接收你的提示,包括系统提示、工具定义和对话历史。SDK 生成一个子类型为 "init" 的 SystemMessage,其中包含会话元数据。
  2. 评估与响应。  Claude 评估当前状态并决定下一步操作。它可能直接返回文本、请求一个或多个工具调用,或两者兼具。SDK 生成一个包含文本和任何工具调用请求的 AssistantMessage
  3. 执行工具。  SDK 运行每个请求的工具并收集结果。每组工具结果会反馈给 Claude 用于下一轮决策。你可以使用钩子在工具执行前拦截、修改或阻止工具调用。
  4. 重复。  步骤 2 和 3 会重复为一个循环。每个完整的循环算作一次回合。Claude 会持续调用工具并处理结果,直到它生成一个没有工具调用的响应。
  5. 返回结果。 SDK 会产出最终的 AssistantMessage,其中包含文本响应(没有工具调用),随后是 ResultMessage,包含最终文本、token 使用情况、成本和会话 ID。

一个简单的问题(“这里有哪些文件?”)可能只需要调用一次或两次 Glob 并返回结果。一个复杂的任务(“重构认证模块并更新测试”)可能会在多次回合中链式调用数十个工具,读取文件、编辑代码、运行测试,Claude 会根据每次结果调整其方法。

回合和消息

一回合是在循环内部的一次完整往返:Claude 生成包含工具调用的输出,SDK 执行这些工具,结果自动反馈给 Claude。这个过程不会将控制权交还给您的代码。回合会持续进行,直到 Claude 生成不包含工具调用的输出,此时循环结束,最终结果被交付。考虑一下对于提示“Fix the failing tests in auth.ts”的完整会话可能是什么样子。首先,SDK 会将你的提示发送给 Claude,并产出带有会话元数据的 SystemMessage。然后循环开始:

  1. 第一轮:  Claude 调用 Bash 来运行 npm test。SDK 生成一个带有工具调用的 AssistantMessage,执行命令后,再生成一个包含输出(三个失败)的 UserMessage
  2. 第二轮:  Claude 调用 Read 读取 auth.ts 和 auth.test.ts。SDK 返回文件内容并生成一个 AssistantMessage
  3. 第三轮:  Claude 调用 Edit 修复 auth.ts,然后调用 Bash 重新运行 npm test。所有三个测试都通过了。SDK 生成一个 AssistantMessage
  4. 最后一轮:  Claude 生成一个无工具调用的纯文本响应:“修复了 auth 错误,现在所有三个测试都通过了。” SDK 生成一个包含这段文本的最终 AssistantMessage,然后是一个带有相同文本以及成本和用量的 ResultMessage

那是四个轮次:三个带有工具调用,一个最终的纯文本响应。你可以用 max_turns / maxTurns 来限制循环,这个计数只包括工具使用次数。例如,在上述循环中设置 max_turns=2,会在编辑步骤之前停止。你也可以使用 max_budget_usd / maxBudgetUsd 来根据支出阈值限制循环次数。如果没有限制,循环会一直运行直到 Claude 自行完成,这对于范围明确的任务来说是没问题的,但对于开放式提示(如“改进这个代码库”)可能会运行很长时间。为生产代理设置预算是一个好的默认做法。有关选项的参考,请见下文 循环次数和预算 

消息类型

随着循环的运行,SDK 会生成一条消息流。每条消息都带有类型信息,告诉你它来自循环的哪个阶段。五种核心类型是:

  • SystemMessage:  会话生命周期事件。通过 subtype 字段来区分它们:"init" 是第一条消息(会话元数据),而 "compact_boundary" 在 压缩 之后触发。在 TypeScript 中,压缩边界是一个独立的 SDKCompactBoundaryMessage 类型,而不是 SDKSystemMessage 的子类型。
  • AssistantMessage:  在每次 Claude 响应后发出,包括最终的纯文本响应。包含该轮次的文本内容块和工具调用块。
  • UserMessage:  在每次工具执行后发出,并将工具结果内容发送回 Claude。也用于在循环中途流式传输的任何用户输入。
  • StreamEvent:  仅在启用部分消息时发出。包含原始 API 流式传输事件(文本增量、工具输入块)。参见流式响应 
  • ResultMessage:  标记代理循环的结束。包含最终文本结果、令牌使用情况、成本和会话 ID。检查 subtype 字段以确定任务是否成功或达到限制。一小部分尾随系统事件(如 prompt_suggestion)可能会在其后到达,因此应迭代流式传输至完成,而不是在结果上中断。参见处理结果 

这五种类型涵盖了 SDK 中代理循环的完整生命周期。TypeScript SDK 还会产生额外的可观察性事件(钩子事件、工具进度、速率限制、任务通知),这些事件提供了更多详细信息,但并非驱动循环所必需。请参阅 Python 消息类型参考和 TypeScript 消息类型参考以获取完整列表。

处理消息

您需要处理哪些消息取决于您正在构建的内容:

  • 仅最终结果:  处理 ResultMessage 以获取输出、成本以及任务是否成功或达到限制。
  • 进度更新:  处理 AssistantMessage 以查看 Claude 每一步的操作,包括调用了哪些工具。
  • 实时流式传输:  启用部分消息(Python 中的 include_partial_messages,TypeScript 中的 includePartialMessages)以实时获取 StreamEvent 消息。请参阅实时流式响应 

您检查消息类型的方式取决于 SDK。

  • Python:  使用 isinstance() 检查消息类型,与从 claude_agent_sdk 导入的类进行对比(例如,isinstance(message, ResultMessage))。
  • TypeScript:  检查 type 字符串字段(例如,message.type === "result")。AssistantMessage 和 UserMessage 将原始 API 消息包装在 .message 字段中,因此内容块位于 message.message.content,而不是 message.content

示例:检查消息类型并处理结果

工具执行

工具赋予你的代理执行操作的能力。没有工具,Claude 只能以文本形式回应。有了工具,Claude 可以读取文件、运行命令、搜索代码以及与外部服务交互。

内置工具

SDK 包含驱动 Claude Code 的相同工具:

分类工具他们做什么
文件操作读 编辑 读取、修改和创建文件
搜索GlobGrep通过模式查找文件,使用正则表达式搜索内容
执行Bash运行 shell 命令、脚本、git 操作
网络WebSearchWebFetch搜索网络,获取并解析页面
发现ToolSearch动态按需查找和加载工具,而不是预先加载所有工具
编排AgentSkillAskUserQuestionTodoWrite生成子代理,调用技能,询问用户,跟踪任务

除了内置工具,您还可以:

工具权限

Claude 根据任务确定要调用哪些工具,但你可以控制是否允许这些调用执行。你可以自动批准特定工具,完全阻止其他工具,或要求对所有调用进行审批。三种选项协同工作,以确定运行内容:

  • allowed_tools / allowedTools 自动批准列出的工具。一个只读代理,其允许工具列表中包含 ["Read", "Glob", "Grep"],会无提示运行这些工具。未列出的工具仍然可用,但需要权限。
  • disallowed_tools / disallowedTools 会阻止列出的工具,无论其他设置如何。有关规则在工具运行前检查的顺序,请参阅 权限 
  • permission_mode / permissionMode 控制未被允许或拒绝规则覆盖的工具的行为。有关可用模式,请参阅 权限模式 

您还可以使用规则(如 "Bash(npm *)")对单个工具进行范围限制,仅允许特定命令。有关完整的规则语法,请参阅 权限 。当工具被拒绝时,Claude 会收到一个作为工具结果的拒绝消息,并通常会尝试不同的方法或报告无法继续进行。

并行工具执行

当 Claude 在单次回合中请求多个工具调用时,SDK 可以根据工具的不同选择并发或顺序执行。只读工具(如 ReadGlobGrep 以及标记为只读的 MCP 工具)可以并发执行。修改状态的工具(如 EditWrite 和 Bash)则顺序执行以避免冲突。自定义工具默认顺序执行。若要为自定义工具启用并行执行,请在其注解中将其标记为只读:readOnly(TypeScript)或 readOnlyHint(Python)。

控制循环运行方式

您可以限制循环的回合次数、执行成本、Claude 推理的深度,以及工具是否在运行前需要审批。所有这些选项都是 ClaudeAgentOptions(Python)/ Options(TypeScript)的字段。

回合数和预算

选项它控制什么默认
最大回合数(max_turns / maxTurns最大工具使用往返次数无限制
最大预算(max_budget_usd / maxBudgetUsd停止前的最大成本无限制

当任一限制被达到时,SDK 会返回一个 ResultMessage,其中包含相应的错误子类型 (error_max_turns 或 error_max_budget_usd)。有关如何检查这些子类型的说明,请参阅 处理结果 ;有关语法说明,请参阅 ClaudeAgentOptions / Options

努力程度

effort 选项控制 Claude 应用的推理程度。较低的 effort 级别每次回合使用的 token 较少,并降低成本。并非所有模型都支持 effort 参数。请参阅 Effort 了解哪些模型支持它。

级别行为适合
"低"最小推理,快速响应文件查找,列出目录
"中"平衡推理常规编辑,标准任务
"高"彻底分析重构,调试
"xhigh"扩展推理深度编程和代理任务;建议在 Opus 4.7 上使用
"max"最大推理深度需要深度分析的复杂问题

如果你没有设置 effort,Python SDK 会保留该参数未设置,并委托模型的默认行为。TypeScript SDK 默认为 "high"

effort 在响应内的推理深度之间权衡延迟和 token 成本。 扩展思考是一个独立的功能,它在输出中生成可见的推理块。它们是独立的:你可以启用扩展思考并设置 effort: "low",或者不启用它并设置 effort: "max"

为执行简单、范围明确的任务(如列出文件或运行单个 grep)的代理使用较低的努力程度,以降低成本和延迟。effort 是在顶层 query() 选项中设置的,而不是针对每个子代理。

权限模式

权限模式选项(Python 中的 permission_mode,TypeScript 中的 permissionMode)控制代理在使用工具前是否请求批准:

模式行为
"default"未被允许规则覆盖的工具会触发您的批准回调;没有回调则表示拒绝
"接受编辑"自动批准文件编辑和常见的文件系统命令(mkdirtouchmvcp 等);其他 Bash 命令遵循默认规则
"计划"无工具执行;Claude 生成计划供审查
"不要询问"从不提示。由权限规则预先批准的工具运行,其他所有内容都将被拒绝
"自动"(仅限 TypeScript)使用模型分类器来批准或拒绝每次工具调用。有关可用性和行为,请参阅自动模式
"绕过权限"直接运行所有允许的工具,无需询问。在 Unix 系统上以 root 身份运行时不可使用。仅在代理的操作不会影响你关心的系统的隔离环境中使用

对于交互式应用,使用 "default" 配合工具审批回调来显示审批提示。对于开发机上的自主代理,"acceptEdits" 会自动批准文件编辑和常见的文件系统命令(mkdirtouchmvcp 等),同时仍然通过允许规则控制其他 Bash 命令。将 "bypassPermissions" 保留用于 CI、容器或其他隔离环境。有关完整详情,请参阅权限 

模型

如果你没有设置 model,SDK 将使用 Claude Code 的默认值,该值取决于你的认证方法和订阅情况。明确设置它(例如,model="claude-sonnet-4-6"),以锁定特定模型或使用较小的模型以获得更快、更便宜的代理。有关可用 ID,请参阅 models

上下文窗口

上下文窗口是在会话期间 Claude 可用的全部信息量。在会话内的不同回合之间不会重置。所有内容都会累积:系统提示、工具定义、对话历史、工具输入和工具输出。跨回合保持不变的内容(系统提示、工具定义、CLAUDE.md)会自动提示缓存 ,这减少了重复前缀的成本和延迟。

消耗上下文

这是每个组件如何在 SDK 中影响上下文:

加载时影响
系统提示每个请求小的固定成本,始终存在
CLAUDE.md 文件会话开始,通过 settingSources每个请求都包含完整内容(但会话缓存,所以只有第一个请求支付完整费用)
工具定义每个请求每个工具都会添加其模式;使用 MCP 工具搜索按需加载工具,而不是一次性全部加载
对话历史随回合累积随每次回合增长:提示、响应、工具输入、工具输出
技能描述通过设置源启动会话简短摘要;完整内容仅在调用时加载

大型工具输出会消耗大量上下文。读取大文件或运行具有详细输出的命令可能在一轮中使用数千个标记。上下文会随着轮次累积,因此包含许多工具调用的长时间会话比短时间会话积累的上下文显著更多。

自动压缩

当上下文窗口接近其限制时,SDK 会自动压缩对话:它将旧的历史记录进行总结以释放空间,同时保留您最近的交流记录和关键决策。当发生这种情况时,SDK 会在流中发出一条带有 type: "system" 和 subtype: "compact_boundary" 的消息(在 Python 中这是一个 SystemMessage;在 TypeScript 中它是一个单独的 SDKCompactBoundaryMessage 类型)。压缩操作会用总结替换旧消息,因此早期对话中的具体指令可能无法保留。持久性规则应放在 CLAUDE.md(通过 settingSources 加载)中,而不是初始提示中,因为 CLAUDE.md 的内容会在每个请求时重新注入。您可以以多种方式自定义压缩行为:

  • CLAUDE.md 中的摘要说明:  压缩器会像读取其他任何上下文一样读取你的 CLAUDE.md,因此你可以包含一个部分,告诉它在摘要时需要保留什么内容。部分标题可以自由定义(不是魔法字符串);压缩器会根据意图进行匹配。
  • PreCompact 钩子:  在压缩操作发生前运行自定义逻辑,例如归档完整转录文本。该钩子接收一个 trigger 字段(manual 或 auto)。参见钩子 
  • 手动压缩:  将 /compact 作为提示字符串发送,按需触发压缩。(通过这种方式发送的斜杠命令是 SDK 输入,而非仅限 CLI 的快捷方式。参见 SDK 中的斜杠命令 。)

示例:CLAUDE.md 中的摘要指令

在你的项目的 CLAUDE.md 文件中添加一个部分,告诉压缩器需要保留的内容。标题名称没有特殊要求;使用任何清晰的标签即可。

CLAUDE.md

# Summary instructions

When summarizing this conversation, always preserve:
- The current task objective and acceptance criteria
- File paths that have been read or modified
- Test results and error messages
- Decisions made and the reasoning behind them

保持上下文高效

长时间运行的代理的几种策略:

  • 为子任务使用子代理。  每个子代理都以全新的对话开始(没有先前的消息历史,但它会加载自己的系统提示和项目级上下文,如 CLAUDE.md)。它看不到父代理的回合,并且只有它的最终响应作为工具结果返回给父代理。主代理的上下文通过该摘要增长,而不是通过完整的子任务转录。有关详细信息,请参阅子代理继承的内容 
  • 谨慎选择工具。  每个工具定义都占用上下文空间。使用 AgentDefinition 上的 tools 字段将子代理限制在它们需要的最小集,并使用 MCP 工具搜索按需加载工具,而不是预先加载所有工具。
  • 关注 MCP 服务器成本。  每个 MCP 服务器都会将其所有工具模式添加到每个请求中。少数拥有许多工具的服务器可能会在代理执行任何工作之前消耗大量上下文。通过按需加载工具而不是预先加载所有工具,ToolSearch 工具可以提供帮助。有关配置,请参阅 MCP 工具搜索 
  • 为常规任务使用较低的努力程度。  对于只需要读取文件或列出目录的代理,将 effort 设置为 "low"。这可以减少 token 使用量和成本。

对于每项功能的上下文成本详细说明,请参阅理解上下文成本 

会话和延续性

每次与 SDK 交互都会创建或继续一个会话。从 ResultMessage.session_id(在两个 SDK 中都可用)捕获会话 ID 以便稍后继续。TypeScript SDK 还将它作为 init SystemMessage 上的直接字段公开;在 Python 中它嵌套在 SystemMessage.data 中。当你继续时,之前回合的完整上下文将被恢复:读取的文件、执行的分析和采取的行动。你也可以分支会话以采用不同的方法,而无需修改原始会话。有关恢复、继续和分支模式的完整指南,请参阅会话管理 

在 Python 中,ClaudeSDKClient 会自动处理跨多次调用的会话 ID。详情请参阅 Python SDK 参考 

处理结果

当循环结束时,ResultMessage 会告诉你发生了什么并给出输出。subtype 字段(在两个 SDK 中都可用)是检查终止状态的主要方式。

结果子类型发生了什么result 字段可用?
成功Claude 正常完成任务
error_max_turns超出 maxTurns 限制未完成
error_max_budget_usd超出 maxBudgetUsd 限制未完成
error_during_execution由于 API 失败或请求被取消等错误,循环被中断
error_max_structured_output_retries在配置的重试限制后,结构化输出验证失败

result 字段(最终文本输出)仅在 success 变体中出现,因此读取前请始终检查子类型。所有结果子类型都包含 total_cost_usdusagenum_turns 和 session_id,以便您可以在出现错误后跟踪成本并继续进行。在 Python 中,total_cost_usd 和 usage 被声明为可选类型,在某些错误路径上可能为 None,因此在格式化前需要加以防护。有关如何解释 usage 字段的详细信息,请参阅 Tracking costs and usage。结果还包括一个 stop_reason 字段(TypeScript 中为 string | null,Python 中为 str | None),用于指示模型在最后一个回合停止生成的原因。常见值有 end_turn(模型正常结束)、max_tokens(达到输出 token 限制)和 refusal(模型拒绝请求)。在错误结果子类型中,stop_reason 包含循环结束前最后一个助手响应的值。要检测拒绝情况,请检查 stop_reason === "refusal"(TypeScript)或 stop_reason == "refusal"(Python)。有关完整类型,请参阅 SDKResultMessage(TypeScript)或 ResultMessage(Python)。

钩子

钩子是在循环特定点触发的回调函数:工具运行前、工具返回后、代理完成时等等。一些常用的钩子包括:

钩子触发时机常见用途
PreToolUse在工具执行前验证输入,阻止危险指令
PostToolUse在工具返回后审计输出,触发副作用
用户提示提交当发送提示时向提示注入额外上下文
停止当代理完成时验证结果,保存会话状态
子代理开始 子代理停止当子代理生成或完成时跟踪和聚合并行任务结果
预压缩在上下文压缩之前在摘要之前存档完整转录文本

钩子运行在你的应用程序进程中,而不是在代理的上下文窗口中,所以它们不会消耗上下文。钩子还可以中断循环:一个拒绝工具调用的 PreToolUse 钩子会阻止其执行,Claude 会收到拒绝消息。两个 SDK 都支持上述所有事件。TypeScript SDK 包含 Python 尚未支持的附加事件。有关完整的事件列表、每个 SDK 的可用性以及完整的回调 API,请参阅 使用钩子控制执行 

将所有内容结合起来

这个示例将本页面的关键概念整合到一个单一的代理中,用于修复失败的测试。它配置了代理的允许工具(自动批准,以便代理自主运行)、项目设置以及回合数和推理工作量的安全限制。在循环运行过程中,它捕获会话 ID 以备后续恢复,处理最终结果,并打印总成本。

Python

TypeScript

import { query } from "@anthropic-ai/claude-agent-sdk";

let sessionId: string | undefined;

for await (const message of query({
  prompt: "Find and fix the bug causing test failures in the auth module",
  options: {
    allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"], // Listing tools here auto-approves them (no prompting)
    settingSources: ["project"], // Load CLAUDE.md, skills, hooks from current directory
    maxTurns: 30, // Prevent runaway sessions
    effort: "high" // Thorough reasoning for complex debugging
  }
})) {
  // Save the session ID to resume later if needed
  if (message.type === "system" && message.subtype === "init") {
    sessionId = message.session_id;
  }

  // Handle the final result
  if (message.type === "result") {
    if (message.subtype === "success") {
      console.log(`Done: ${message.result}`);
    } else if (message.subtype === "error_max_turns") {
      // Agent ran out of turns. Resume with a higher limit.
      console.log(`Hit turn limit. Resume session ${sessionId} to continue.`);
    } else if (message.subtype === "error_max_budget_usd") {
      console.log("Hit budget limit.");
    } else {
      console.log(`Stopped: ${message.subtype}`);
    }
    console.log(`Cost: $${message.total_cost_usd.toFixed(4)}`);
  }
}

下一步

现在你已经了解了这个循环,根据你正在构建的内容,以下是你可以继续的方向:

  • 还没有运行过代理?  从 快速入门 开始,安装 SDK 并查看一个完整的端到端运行示例。
  • 准备好集成到你的项目中了吗?  加载 CLAUDE.md、技能和文件系统钩子 ,以便代理能自动遵循你的项目规范。
  • 正在构建交互式界面?  启用流式传输 ,以便在循环运行时显示实时文本和工具调用。
  • 需要更紧密地控制代理能做什么?  使用权限锁定工具访问,并使用钩子在执行前审计、阻止或转换工具调用。
  • 正在运行长时间或昂贵的任务?  将独立工作卸载到子代理 ,以保持主上下文简洁。

有关代理循环的更广泛概念图(非特定于 SDK),请参阅 《Claude Code 如何工作》