Agent Loop 是什么用一句话概括用户发 prompt → LLM 返回响应 → 如果 LLM 要求调工具就执行 → 把工具结果喂回 LLM → 重复直到 LLM 说我说完了。画成流程图end_turn / stop_sequencemax_tokenstool_use用户 prompt构建 messages tools调用 LLM API返回结果追加请继续提取 tool_use blocks按只读/变更分桶只读工具并发执行变更工具串行执行微压缩大结果tool_result 加入 messages这个循环里有几个关键决策点什么时候停LLM 返回end_turn或stop_sequence时正常结束到达maxTurns上限时强制停止超出预算 (maxBudgetUsd) 时中断用户主动取消时也中断。工具怎么执行只读工具并发跑最多 10 个变更工具串行跑——避免并发写文件。上下文太长怎么办自动压缩——用一个 LLM 调用把历史摘要腾出空间继续。中途出错怎么办内置重试、回退模型、错误隔离工具报错不会炸掉整个循环。两条入口prompt() 和 stream()SDK 提供两种方式触发 Agent Loop阻塞式 prompt()let agent createAgent(options: AgentOptions( apiKey: sk-..., model: claude-sonnet-4-6, maxTurns: 10 )) let result await agent.prompt(Read Package.swift and summarize it.) print(result.text) print(Turns: \(result.numTurns), Cost: $\(String(format: %.4f, result.totalCostUsd)))prompt()是发出去等结果模式。一次调用跑完所有轮次返回最终的QueryResult。适合不需要实时看到中间过程的场景——比如后台任务、CLI 工具。流式 stream()for await message in agent.stream(Explain this codebase.) { switch message { case .partialMessage(let data): print(data.text, terminator: ) // 实时输出文本 case .toolUse(let data): print([Using tool: \(data.toolName)]) case .toolResult(let data): print([Tool done, \(data.content.count) chars]) case .result(let data): print(\nDone: \(data.numTurns) turns, $\(String(format: %.4f, data.totalCostUsd))) default: break } }stream()返回AsyncStreamSDKMessage在 LLM 处理过程中持续推送事件。SDK 定义了 17 种消息类型从partialMessage文本片段到toolUse工具调用到result最终结果覆盖了 Agent Loop 的每个阶段。选择哪种取决于你的 UI 需求要实时展示就用stream()不需要就用prompt()。循环体内部一个 turn 做了什么不管走哪条入口每个 turn 的核心逻辑是相同的。让我们跟一遍代码。1. 检查是否需要压缩if shouldAutoCompact(messages: messages, model: model, state: compactState) { let (newMessages, _, newState) await compactConversation( client: client, model: model, messages: messages, state: compactState, fileCache: fileCache, sessionMemory: sessionMemory ) messages newMessages compactState newState }每个 turn 开始前先检查消息历史估计的 token 数是不是快要撑爆上下文窗口了。如果是用一个 LLM 调用把历史压缩成摘要替换掉原始消息。压缩的阈值是模型上下文窗口 - 10000 tokens缓冲区。连续压缩失败 3 次后会停止尝试避免浪费 token。2. 发 LLM 请求带重试和回退response try await withRetry({ try await client.sendMessage( model: model, messages: messages, maxTokens: maxTokens, system: buildSystemPrompt(), tools: apiTools, ... ) }, retryConfig: retryConfig)所有 LLM 请求都经过withRetry包装按配置的重试策略处理临时错误网络超时、429 限流等。如果主模型彻底失败还配置了fallbackModelSDK 会用备用模型再试一次if let fallbackModel self.options.fallbackModel, fallbackModel ! self.model { // 用 fallbackModel 重试... }3. 处理 stop_reasonLLM 响应里的stop_reason决定了循环的走向stop_reason含义循环行为end_turnLLM 说完了正常退出循环stop_sequence碰到停止符正常退出循环tool_useLLM 想调工具执行工具继续循环max_tokens输出被截断追加请继续继续循环max_tokens的情况有个保护最多自动续接 3 次防止无限循环。4. 工具执行分桶并发当 LLM 返回tool_use时SDK 不是简单地把工具排着队一个个跑而是做了分桶// ToolExecutor.partitionTools() for block in blocks { let tool tools.first { $0.name block.name } if let tool tool, tool.isReadOnly { readOnly.append(item) // 只读桶 } else { mutations.append(item) // 变更桶 } }只读工具Read、Glob、Grep、WebSearch 等可以安全并发用TaskGroup跑最多 10 个一批let batchResults await withTaskGroup(of: ToolResult.self) { group in for item in batchSlice { group.addTask { await executeSingleTool(block: item.block, tool: item.tool, context: ...) } } // 收集结果 }变更工具Write、Edit、Bash 等必须串行执行一个跑完再跑下一个避免并发写冲突for item in items { let result await executeSingleTool(...) results.append(result) }执行顺序先跑所有只读工具并发再跑所有变更工具串行。这在 LLM 一次返回多个工具调用时能显著提升性能——比如 LLM 同时要求读 5 个文件5 个读操作并行完成。5. 微压缩工具执行完后结果在喂回 LLM 之前还要过一道微压缩for result in toolResults { let processedContent await processToolResult(result.content, isError: result.isError) processedResults.append(ToolResult( toolUseId: result.toolUseId, content: processedContent, isError: result.isError )) }如果一个工具返回的内容超过 50000 字符比如读了一个大文件SDK 会用一次额外的 LLM 调用把内容压缩。错误结果不压缩——保留了完整的错误信息供 LLM 诊断。成本追踪逐 turn 累加每一轮 LLM 调用后SDK 都会更新 token 用量和费用let turnCost estimateCost(model: model, usage: turnUsage) totalCostUsd turnCost costByModel[model] CostBreakdownEntry( model: model, inputTokens: turnUsage.inputTokens, outputTokens: turnUsage.outputTokens, costUsd: turnCost )costByModel按 model 分组记录。这意味着如果你中途切换了模型通过switchModel()每个模型的费用是分开计算的。最终result.costBreakdown能告诉你每个模型花了多少钱。预算检查在每个 turn 后执行if let budget options.maxBudgetUsd, totalCostUsd budget { status .errorMaxBudgetUsd break }超出预算时立即退出循环但已产生的文本会保留在结果里——你拿到的是部分结果不是空白的。取消协作式取消Swift 的结构化并发用Task.isCancelled做协作式取消。SDK 在循环的多个检查点都检查了这个标志while 循环入口只读工具和变更工具之间SSE 事件循环内部工具执行前后// 循环入口 if Task.isCancelled || _interrupted { status .cancelled break } // 只读/变更之间 if Task.isCancelled { return results }stream()还额外支持通过interrupt()方法取消——内部就是 cancel 掉持有 stream 的 Task。取消后返回的是QueryResult(isCancelled: true)附带截止到取消时刻的部分文本和 token 用量。错误处理不炸、不丢SDK 的错误处理原则是工具执行错误不传播API 错误有重试最终失败保留部分结果。工具执行时任何错误都被捕获为ToolResult(isError: true)static func executeSingleTool(...) async - ToolResult { guard let tool tool else { return ToolResult(toolUseId: block.id, content: Error: Unknown tool, isError: true) } // ... try executing let result await tool.call(input: block.input, context: context) return ToolResult(toolUseId: block.id, content: result.content, isError: result.isError) }工具报错的结果照样喂回 LLMLLM 看到错误信息后可以决定换个策略。Agent Loop 不会因为一个工具挂了就崩溃。API 层面的错误网络问题、500 等会触发重试重试失败后触发 fallback 模型全挂了才返回errorDuringExecution状态。Hook 集成循环的生命周期Agent Loop 在关键节点触发 Hook 事件Hook 事件触发时机sessionStart循环开始前preToolUse每个工具执行前postToolUse工具成功执行后postToolUseFailure工具执行失败后stop循环结束时正常或异常sessionEnd返回结果前Hook 的一个典型用法是在preToolUse拦截危险操作await hookRegistry.register(.preToolUse, definition: HookDefinition( matcher: Bash, handler: { input in return HookOutput(message: Bash blocked in production, block: true) } ))被 Hook 拦截的工具不会执行而是返回一个错误结果——LLM 会看到Bash blocked in production可以换个方式完成任务。还有一个入口streamInput()除了prompt()和stream()SDK 还提供了第三种入口——streamInput()接受一个AsyncStreamString作为输入let input AsyncStreamString { continuation in continuation.yield(Whats in this project?) continuation.yield(Now explain the test structure.) continuation.finish() } for await message in agent.streamInput(input) { // 处理每条输入对应的响应 }每个输入元素被视为一条新的用户消息触发一个完整的 prompt 周期。这适合聊天式交互用户的每条消息都是输入流的一个元素Agent 逐条处理并流式输出。小结Agent Loop 是整个 SDK 的心脏。理解了它的工作方式剩下的功能都是在它的基础上叠加的工具系统— Loop 里的执行工具环节MCP 集成— Loop 启动时连接外部工具服务器会话持久化— Loop 结束后保存 messages 数组权限控制— 工具执行前的拦截点