Claude Code官方文档Agent SDKAgent Loop

Claude Agent SDK 工作循环原理 - 消息生命周期 / 工具执行 / 上下文窗口

了解 SDK 代理的消息生命周期、工具执行、上下文窗口和支持架构。

· 阅读约 29 分钟

代理循环如何工作

了解消息生命周期、工具执行、上下文窗口和支持 SDK 代理的架构。

Agent SDK 让你能够在自己的应用程序中嵌入 Claude Code 的自主代理循环。SDK 是一个独立的包,让你能够以编程方式控制工具、权限、成本限制和输出。你不需要安装 Claude Code CLI 就能使用它。

启动代理时,SDK 运行与 Claude Code 相同的执行循环:Claude 评估你的提示,调用工具采取行动,接收结果,然后重复直到任务完成。本页解释循环内部发生的情况,以便你能够有效地构建、调试和优化代理。

循环概览

每个代理会话都遵循相同的周期:

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

一个快速问题(“这里有什么文件?“)可能需要一两个轮次调用 Glob 并响应结果。一个复杂任务(“重构认证模块并更新测试”)可以跨多个轮次链接数十个工具调用,读取文件、编辑代码和运行测试,Claude 根据每个结果调整其方法。

轮次和消息

轮次是循环内的一个往返:Claude 产生包含工具调用的输出,SDK 执行这些工具,结果自动反馈给 Claude。这发生在不将控制权交回给你的代码的情况下。轮次继续进行,直到 Claude 产生没有工具调用的输出,此时循环结束并交付最终结果。

考虑对于提示”修复 auth.ts 中的失败测试”的完整会话可能是什么样子。

首先,SDK 将你的提示发送给 Claude 并产生一个 SystemMessage,包含会话元数据。然后循环开始:

  1. 轮次 1: Claude 调用 Bash 运行 npm test。SDK 产生一个 AssistantMessage,包含工具调用,执行命令,然后产生一个 UserMessage,包含输出(三个失败)。
  2. 轮次 2: Claude 在 auth.tsauth.test.ts 上调用 Read。SDK 返回文件内容并产生一个 AssistantMessage
  3. 轮次 3: Claude 调用 Edit 修复 auth.ts,然后调用 Bash 重新运行 npm test。所有三个测试都通过。SDK 产生一个 AssistantMessage
  4. 最后轮次: Claude 产生仅包含文本的响应,没有工具调用:“修复了认证错误,所有三个测试现在都通过了。” 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 还产生额外的可观测性事件(hook 事件、工具进度、速率限制、任务通知),提供额外的细节,但不是驱动循环所必需的。

处理消息

你处理哪些消息取决于你正在构建什么:

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

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

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

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

Python:

from claude_agent_sdk import query, AssistantMessage, ResultMessage

async for message in query(prompt="Summarize this project"):
    if isinstance(message, AssistantMessage):
        print(f"Turn completed: {len(message.content)} content blocks")
    if isinstance(message, ResultMessage):
        if message.subtype == "success":
            print(message.result)
        else:
            print(f"Stopped: {message.subtype}")

TypeScript:

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

for await (const message of query({ prompt: "Summarize this project" })) {
  if (message.type === "assistant") {
    console.log(`Turn completed: ${message.message.content.length} content blocks`);
  }
  if (message.type === "result") {
    if (message.subtype === "success") {
      console.log(message.result);
    } else {
      console.log(`Stopped: ${message.subtype}`);
    }
  }
}

工具执行

工具赋予你的代理采取行动的能力。没有工具,Claude 只能用文本响应。有了工具,Claude 可以读取文件、运行命令、搜索代码并与外部服务交互。

内置工具

SDK 包含与 Claude Code 相同的工具:

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

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

工具权限

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

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

你也可以使用 "Bash(npm *)" 之类的规则来限制单个工具,以仅允许特定命令。

当工具被拒绝时,Claude 接收拒绝消息作为工具结果,通常尝试不同的方法或报告它无法继续。

并行工具执行

当 Claude 在单个轮次中请求多个工具调用时,两个 SDK 都可以根据工具并发或顺序运行它们。只读工具(如 ReadGlobGrep 和标记为只读的 MCP 工具)可以并发运行。修改状态的工具(如 EditWriteBash)顺序运行以避免冲突。

自定义工具默认为顺序执行。要为自定义工具启用并行执行,请在其注释中设置 readOnlyHint

控制循环如何运行

你可以限制循环进行多少轮次、成本多少、Claude 推理的深度,以及工具是否需要在运行前获得批准。所有这些都是 ClaudeAgentOptions(Python)/ Options(TypeScript)上的字段。

轮次和预算

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

当达到任一限制时,SDK 返回一个 ResultMessage,包含相应的错误子类型(error_max_turnserror_max_budget_usd)。

努力级别

effort 选项控制 Claude 应用多少推理。较低的努力级别每个轮次使用更少的令牌并降低成本。并非所有模型都支持努力参数。

级别行为适合
"low"最小推理,快速响应文件查找、列出目录
"medium"平衡推理常规编辑、标准任务
"high"彻底分析重构、调试
"xhigh"扩展推理深度编码和代理任务;在 Opus 4.7 上推荐
"max"最大推理深度需要深度分析的多步骤问题

如果你不设置 effort,Python SDK 会将参数保留未设置,并遵从模型的默认行为。TypeScript SDK 默认为 "high"

ℹ️ effort 在每个响应内交换延迟和令牌成本以获得推理深度。扩展思考 是一个单独的功能,在输出中产生可见的思维链块。它们是独立的:你可以设置 effort: "low" 并启用扩展思考,或 effort: "max" 而不启用它。

对于执行简单、范围明确的任务(如列出文件或运行单个 grep)的代理,使用较低的努力来降低成本和延迟。在顶级 query() 选项中为整个会话设置 effort,或在 AgentDefinition 上使用 effort 字段为每个子代理设置以覆盖会话级别。

权限模式

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

模式行为
"default"不被允许规则覆盖的工具触发你的批准回调;没有回调意味着拒绝
"acceptEdits"自动批准文件编辑和常见文件系统命令(mkdirtouchmvcp 等);其他 Bash 命令遵循默认规则
"plan"只读工具运行;Claude 探索并产生计划而不编辑你的源文件
"dontAsk"从不提示。由权限规则预批准的工具运行,其他一切被拒绝
"auto"(仅 TypeScript)使用模型分类器批准或拒绝每个工具调用。
"bypassPermissions"运行所有允许的工具而不询问。在 Unix 上以 root 身份运行时无法使用。仅在隔离环境中使用,其中代理的操作无法影响你关心的系统

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

模型

如果你不设置 model,SDK 使用 Claude Code 的默认值,这取决于你的身份验证方法和订阅。显式设置它(例如,model="claude-sonnet-4-6")以固定特定模型或使用较小的模型以获得更快、更便宜的代理。

上下文窗口

上下文窗口是会话期间可用于 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 hook: 在压缩发生前运行自定义逻辑,例如存档完整成绩单。hook 接收一个 trigger 字段(manualauto)。请参阅 hooks
  • 手动压缩: 发送 /compact 作为提示字符串以按需触发压缩。

示例: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 工具可以通过按需加载工具而不是预加载所有工具来帮助。
  • 对常规任务使用较低的努力。 为仅需要读取文件或列出目录的代理设置努力"low"。这减少了令牌使用和成本。

会话和连续性

与 SDK 的每次交互都创建或继续一个会话。从 ResultMessage.session_id(在两个 SDK 中都可用)捕获会话 ID 以稍后恢复。TypeScript SDK 也将其作为初始化 SystemMessage 上的直接字段公开;在 Python 中它嵌套在 SystemMessage.data 中。

当你恢复时,来自先前轮次的完整上下文被恢复:读取的文件、执行的分析和采取的操作。你也可以分叉一个会话以分支到不同的方法而不修改原始方法。

有关恢复、继续和分叉模式的完整指南,请参阅会话管理

ℹ️ 在 Python 中,ClaudeSDKClient 跨多个调用自动处理会话 ID。

处理结果

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

结果子类型发生了什么result 字段可用?
successClaude 正常完成了任务
error_max_turns在完成前达到 maxTurns 限制
error_max_budget_usd在完成前达到 maxBudgetUsd 限制
error_during_execution错误中断了循环(例如,API 失败或取消的请求)
error_max_structured_output_retries结构化输出验证在配置的重试限制后失败

result 字段(最终文本输出)仅在 success 变体上存在,因此在读取它之前始终检查子类型。所有结果子类型都包含 total_cost_usdusagenum_turnssession_id,因此你可以跟踪成本并在错误后恢复。

结果还包括一个 stop_reason 字段(TypeScript 中的 string | null,Python 中的 str | None),指示模型为什么在其最后轮次停止生成。常见值是 end_turn(模型正常完成)、max_tokens(达到输出令牌限制)和 refusal(模型拒绝了请求)。在错误结果子类型上,stop_reason 携带循环结束前最后一个助手响应的值。要检测拒绝,检查 stop_reason === "refusal"(TypeScript)或 stop_reason == "refusal"(Python)。

Hooks

Hooks 是在循环中特定点触发的回调:在工具运行前、返回后、代理完成时等。一些常用的 hooks 是:

Hook何时触发常见用途
PreToolUse在工具执行前验证输入、阻止危险命令
PostToolUse在工具返回后审计输出、触发副作用
UserPromptSubmit当发送提示时将额外上下文注入提示
Stop当代理完成时验证结果、保存会话状态
SubagentStart / SubagentStop当子代理生成或完成时跟踪和聚合并行任务结果
PreCompact在上下文压缩前在总结前存档完整成绩单

Hooks 在你的应用程序进程中运行,而不是在代理的上下文窗口内,因此它们不消耗上下文。Hooks 也可以短路循环:拒绝工具调用的 PreToolUse hook 防止它执行,Claude 接收拒绝消息。

两个 SDK 都支持上述所有事件。TypeScript SDK 包括 Python 尚不支持的额外事件。

将其全部放在一起

此示例将本页的关键概念组合到修复失败测试的单个代理中。它使用允许的工具(自动批准,以便代理自主运行)、项目设置和轮次和推理努力的安全限制来配置代理。当循环运行时,它捕获会话 ID 以进行潜在恢复、处理最终结果并打印总成本。

Python:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def run_agent():
    session_id = None

    async for message in query(
        prompt="Find and fix the bug causing test failures in the auth module",
        options=ClaudeAgentOptions(
            allowed_tools=[
                "Read",
                "Edit",
                "Bash",
                "Glob",
                "Grep",
            ],  # Listing tools here auto-approves them (no prompting)
            setting_sources=[
                "project"
            ],  # Load CLAUDE.md, skills, hooks from current directory
            max_turns=30,  # Prevent runaway sessions
            effort="high",  # Thorough reasoning for complex debugging
        ),
    ):
        # Handle the final result
        if isinstance(message, ResultMessage):
            session_id = message.session_id  # Save for potential resumption

            if message.subtype == "success":
                print(f"Done: {message.result}")
            elif message.subtype == "error_max_turns":
                # Agent ran out of turns. Resume with a higher limit.
                print(f"Hit turn limit. Resume session {session_id} to continue.")
            elif message.subtype == "error_max_budget_usd":
                print("Hit budget limit.")
            else:
                print(f"Stopped: {message.subtype}")
            if message.total_cost_usd is not None:
                print(f"Cost: ${message.total_cost_usd:.4f}")


asyncio.run(run_agent())

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、技能和文件系统 hooks,以便代理自动遵循你的项目约定。
  • 构建交互式 UI? 启用流式传输以在循环运行时显示实时文本和工具调用。
  • 需要对代理能做什么进行更严格的控制? 使用权限锁定工具访问,并使用 hooks 在工具执行前审计、阻止或转换工具调用。
  • 运行长期或昂贵的任务? 将隔离的工作卸载到子代理以保持你的主上下文精简。

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


本文翻译自 Anthropic Claude Code 官方文档,最近一次同步:2025-05-01。