1. 从一次“工具调用卡死”说起Claude Code 的 Agent 主循环到底在做什么如果你正在读 Claude Code 的源码大概率会遇到这样一个场景模型明明返回了tool_use终端却迟迟没有输出或者工具执行完了下一轮模型却像失忆一样重复调用同一个工具。这不是模型的问题而是 Agent 主循环的编排逻辑在某个环节没有收敛。Claude Code 的核心架构可以拆成三条主线Agent 主循环、工具调用协议、上下文管理。这三条线不是并列关系而是主循环驱动工具协议、工具结果回流影响上下文、上下文压缩又反过来决定主循环能否继续推进。理解这个闭环比记住某个具体文件的结构更重要。我试过把它的主循环抽象成一个状态机发现最关键的约束只有一条每个tool_use必须对应唯一的tool_result即使被中断也要补齐。这条约束看起来简单但它决定了整个系统的错误恢复、流式执行、中断收敛的设计方式。这篇文章面向想深入理解编程 Agent 内部机制的开发者。我会从主循环的伪代码开始逐步拆解工具调用协议、上下文压缩管线然后给出可复制的本地调试配置和逐层验证 Agent 行为的操作步骤。你不需要读完整个源码仓库但需要能跑起一个最小的 Agent 循环来对照验证。适合谁写过 LLM 应用、想搞清楚“为什么我的 Agent 会卡住/重复调用/上下文爆炸”的开发者正在设计自己的编程 Agent、想参考工程实现的人以及需要调试 Claude Code 行为、想知道每一轮到底发生了什么的人。2. 前置准备用 TaoToken 接入 Claude Code 并拿到可调试的 API 入口在拆解源码之前你需要一个能实际跑起来的 Claude Code 环境。这一步不是为了“注册账号”而是为了让你在后续调试主循环时能真实看到每一轮的请求和响应。TaoToken 提供了兼容 Anthropic API 的接入方式你可以把它理解成一个统一的 API 网关Claude Code 发出的messages.create请求通过配置的 Base URL 转发到对应模型。这样你在本地调试时可以随时切换模型、查看请求日志而不需要改 Claude Code 的源码。2.1 获取 API Key 和 Base URL访问 TaoToken 控制台创建 API Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_code_archutm_campaignrewrite创建后你会拿到一个以sk-开头的 Key。Base URL 使用https://taotoken.net/api注意API 地址不加 UTM 参数直接使用上面的地址即可。2.2 配置 Claude Code 的环境变量Claude Code 读取ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY两个环境变量。你可以在 shell 配置文件里写入export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEYsk-你的Key如果你不想污染全局环境也可以在项目目录下创建一个.env文件然后用dotenv加载。但更推荐的方式是使用 Claude Code 的 settings 文件这样不同项目可以用不同的配置。2.3 验证接入是否成功配置完成后运行一个最简单的请求claude -p 用一句话解释什么是 Agent 主循环如果返回了正常文本说明接入成功。如果报 401检查 Key 是否正确如果报连接超时检查 Base URL 是否可达。这一步的意义在于后续我们调试主循环时需要能看到真实的 API 请求。你可以在 TaoToken 控制台的日志页面看到每次请求的 model、token 数、耗时这对理解上下文压缩和工具调用的成本非常有帮助。2.4 准备一个可调试的本地项目创建一个测试目录初始化 gitmkdir agent-debug cd agent-debug git init echo # Agent Debug README.md git add . git commit -m init然后在目录下启动 Claude Codeclaude进入交互模式后你可以输入/status查看当前会话状态输入/cost查看 token 消耗。这些命令对应源码里的 local command 分支后面我们会拆解它们的执行路径。3. 可复制配置Agent 主循环的关键参数与调试开关Claude Code 的行为受多个配置项影响。如果你想逐层验证 Agent 行为需要先理解这些配置的作用。3.1 settings.json 的核心字段在项目目录下创建.claude/settings.json{ model: claude-sonnet-4-5-20250929, permissions: { allow: [ Read, Glob, Grep ], deny: [ Bash(rm:*), Bash(git push:*) ], ask: [ Edit, Write ] }, env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的Key, MAX_THINKING_TOKENS: 10000 }, includeCoAuthoredBy: false }这个配置做了三件事指定模型、设置工具权限、注入环境变量。其中permissions字段直接对应源码里的ToolPermissionContext决定了哪些工具对模型可见、哪些需要审批、哪些直接拒绝。3.2 调试开关打开详细日志Claude Code 支持通过环境变量打开调试日志export CLAUDE_CODE_DEBUG_LOGS_DIR$HOME/.claude/debug export DEBUGclaude:*启动后每次会话会在~/.claude/debug/下生成一个{sessionId}.txt文件里面记录了模块加载、API 请求、工具执行的时间线。这是理解主循环最直接的入口。如果你想看更细的 trace可以打开 Perfettoexport CLAUDE_CODE_PERFETTO_TRACE1运行后会在~/.claude/traces/下生成trace-{sessionId}.json用 Perfetto UI 打开可以看到 API 调用、工具执行、等待用户输入的时间轴。3.3 工具权限的 JSON 配置片段工具权限是 Agent 安全边界的核心。下面是一个更完整的配置示例展示了 allow/deny/ask 的优先级{ permissions: { allow: [ Bash(git status:*), Bash(git diff:*), Bash(npm test:*), Read, Glob, Grep ], deny: [ Bash(rm -rf:*), Bash(curl:*), Bash(wget:*), Read(.env), Read(**/secrets/**) ], ask: [ Edit, Write, Bash(git commit:*), Bash(git push:*) ] } }这里的规则匹配逻辑是deny 优先于 allow。也就是说即使你在 allow 里写了Bash(git:*)如果 deny 里有Bash(git push:*)那么git push仍然会被拒绝。这个优先级在源码的checkPermissions()里实现。3.4 上下文压缩的阈值配置上下文压缩是 Agent 长会话的关键。Claude Code 的压缩管线分五层你可以通过环境变量调整触发阈值export CLAUDE_CODE_AUTO_COMPACT_WINDOW0.93 export MAX_TOOL_RESULT_SIZE_CHARS50000 export MAX_STATUS_CHARS2000CLAUDE_CODE_AUTO_COMPACT_WINDOW控制 autocompact 的触发比例默认是 0.93也就是当估算 token 达到窗口的 93% 时触发整体压缩。MAX_TOOL_RESULT_SIZE_CHARS控制单条工具结果的最大字符数超过这个值会被替换成占位符并外挂到文件。3.5 验证配置是否生效启动 Claude Code 后输入/status你会看到当前的模型、权限模式、MCP 连接状态、token 使用情况。如果配置正确model字段应该显示你设置的模型permissions应该显示你配置的规则数量。再输入/config这会打开配置面板你可以看到所有生效的设置。注意/config是一个 local-jsx 命令它不会进入模型主循环而是在本地渲染一个 Ink 组件。4. 逐层验证从主循环伪代码到工具调用的完整链路这一节是全文的核心。我会把 Agent 主循环拆成可验证的步骤每一步都给出伪代码和验证方法。4.1 主循环的骨架Claude Code 的主循环在query.ts里核心结构是一个while(true)循环。简化后的伪代码async function* queryLoop(params) { let state { messages: params.initialMessages, turnCount: 0, transition: next_turn }; while (true) { // 1. 上下文压缩管线 state.messages await applyCompactionPipeline(state.messages); // 2. 调用模型 const stream await callModel({ messages: state.messages, systemPrompt: params.systemPrompt, tools: params.tools, model: state.currentModel }); // 3. 流式处理响应 const toolUseBlocks []; for await (const event of stream) { if (event.type text) { yield { type: text, content: event.text }; } if (event.type tool_use) { toolUseBlocks.push(event); // 流式启动工具执行 executor.addTool(event); } } // 4. 如果没有工具调用检查停止条件 if (toolUseBlocks.length 0) { const stopResult await handleStopHooks(state); if (stopResult.shouldContinue) { state.messages.push(stopResult.blockingError); continue; } return { reason: completed }; } // 5. 执行工具并收集结果 const toolResults await executor.getRemainingResults(); state.messages.push(...toolResults); // 6. 刷新工具列表MCP 可能已连接 params.refreshTools(); // 7. 检查轮次上限 state.turnCount; if (state.turnCount params.maxTurns) { return { reason: max_turns }; } } }这个骨架里有几个关键点第一压缩管线在每轮开始时执行。它不是一次性操作而是每轮都检查是否需要压缩。压缩分五层toolResultBudget单条结果裁剪、snip删除标记的消息、microcompact清理旧工具结果、contextCollapse折叠中段、autocompact整体摘要。前四层零 LLM 成本只有 autocompact 会调用模型生成摘要。第二工具执行是流式的。模型还在输出时只要一个tool_useblock 完整出现就立即启动执行。这通过StreamingToolExecutor实现。它的调度规则是保守的当前无执行中工具时可以启动任何工具当前执行中的工具全都并发安全、且新工具也并发安全时才允许并行。否则新工具排队等待。第三停止条件不是简单的end_turn。模型返回end_turn后还要经过 Stop hooks、teammate hooks、token budget 检查。Stop hooks 可以返回blockingError要求模型修复后重试或preventContinuation终止整个对话。4.2 验证主循环观察一次完整的工具调用让我们用一个具体例子验证。在 Claude Code 里输入读取 README.md 并告诉我文件内容观察终端输出。你应该看到模型先输出一段文本可能是“我来读取文件”然后出现工具调用提示Read(README.md)工具执行后显示文件内容模型基于文件内容生成最终回答这个过程对应主循环的完整链路模型输出tool_use→ 主循环识别并执行 → 工具结果作为tool_result回灌 → 模型生成最终回答。如果你想看更细的日志打开~/.claude/debug/{sessionId}.txt搜索tool_use和tool_result。你会看到类似这样的记录[query] turn 1, tool_use: Read(README.md) [tool] executing Read, input: {file_path: README.md} [tool] result: Read completed, 1234 chars [query] turn 2, no tool_use, end_turn4.3 工具调用协议Tool 接口的关键方法Claude Code 的工具不是简单的函数而是一个实现了Tool接口的对象。关键方法包括type Tool { name: string; inputSchema: ZodSchema; call(args, context, canUseTool, parentMessage): PromiseToolResult; validateInput(input, context): PromiseValidationResult; checkPermissions(input, context): PromisePermissionResult; isConcurrencySafe(input): boolean; mapToolResultToToolResultBlockParam(content, toolUseID): ToolResultBlockParam; renderToolUseMessage(): ReactNode; renderToolResultMessage(): ReactNode; };其中call()是执行入口checkPermissions()决定是否允许执行isConcurrencySafe()决定能否并行。mapToolResultToToolResultBlockParam()把工具结果转换成 API 需要的tool_resultblock。验证方法在 Claude Code 里输入一个需要权限的操作比如删除 README.md你会看到权限提示。这个提示来自checkPermissions()的返回结果。如果你在 settings 里把Bash(rm:*)加入 deny这个操作会被直接拒绝不会弹出提示。4.4 上下文压缩观察 autocompact 的触发要验证上下文压缩你需要一个长会话。可以连续让 Claude Code 读取多个大文件读取 src 目录下所有 TypeScript 文件并总结每个文件的作用这会触发大量工具调用上下文快速增长。当 token 达到阈值时你会看到终端提示Context low · /compact to compact或者自动触发压缩。压缩后之前的详细对话会被摘要替换。你可以在 debug 日志里搜索autocompact看到压缩前后的 token 数。压缩管线的五层顺序是toolResultBudget单条工具结果超过 50000 字符时替换为占位符snip删除被标记的消息需要模型主动调用 SnipToolmicrocompact清理旧的工具结果替换为[Old tool result content cleared]contextCollapse把中段消息折叠成摘要autocompact整体摘要替换所有历史前四层不调用 LLM只有第五层会调用模型生成摘要。这就是为什么 Claude Code 能在长会话里保持较低成本。4.5 中断处理CtrlC 后的收敛在工具执行过程中按 CtrlC观察会发生什么。你会看到当前工具被取消未完成的工具生成 synthetictool_result主循环返回aborted_tools这个行为对应源码里的中断收敛逻辑getRemainingResults()会等待取消过程收敛并为未完成工具合成错误型tool_result保证 API 协议不会留下悬空的tool_use。验证方法在 Claude Code 执行一个耗时命令时按 CtrlC然后检查 debug 日志。你会看到类似[query] aborted_streaming, generating synthetic tool_result for toolu_xxx [query] return { reason: aborted_tools }5. 常见报错排查401、local proxy failed、reading choices、OAuth在调试 Agent 行为时你会遇到几类典型错误。这一节按真实报错信息给出排查路径。5.1 401 Unauthorized报错信息API Error: 401 Unauthorized原因API Key 无效或未正确配置。排查步骤检查ANTHROPIC_API_KEY是否设置echo $ANTHROPIC_API_KEY检查 Key 是否以sk-开头是否有空格或换行。检查ANTHROPIC_BASE_URL是否为https://taotoken.net/api。如果使用 settings.json检查env字段是否正确嵌套。修复重新从 TaoToken 控制台复制 Key确保没有多余字符。如果使用 settings.json注意 JSON 格式不能有注释。5.2 local proxy failed报错信息Error: local proxy failed to connect原因Claude Code 尝试连接本地代理但代理未启动或端口不对。排查步骤检查是否有HTTP_PROXY或HTTPS_PROXY环境变量env | grep -i proxy如果有检查代理是否运行。如果不需要代理取消这些环境变量unset HTTP_PROXY HTTPS_PROXY修复Claude Code 会读取系统代理设置。如果你在本地调试建议直接连接不要走代理。5.3 reading choices of undefined报错信息TypeError: Cannot read properties of undefined (reading choices)原因API 返回格式不符合预期。这通常发生在 Base URL 配置错误或者使用了不兼容的 API 格式。排查步骤检查ANTHROPIC_BASE_URL是否指向兼容 Anthropic Messages API 的端点。检查模型名称是否正确。Claude Code 使用claude-sonnet-4-5-20250929这样的模型 ID。用 curl 直接测试 APIcurl -X POST https://taotoken.net/api/v1/messages \ -H x-api-key: sk-你的Key \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d {model:claude-sonnet-4-5-20250929,max_tokens:100,messages:[{role:user,content:hi}]}如果 curl 返回正常说明 API 没问题问题在 Claude Code 配置。5.4 OAuth token expired报错信息OAuth token has expired. Please run /login again.原因Claude Code 的 OAuth 认证过期。这通常发生在使用官方登录方式时。排查步骤如果你使用 API Key 接入不需要 OAuth。检查是否误用了/login命令。如果确实需要 OAuth运行/login重新认证。检查~/.claude.json里的oauthAccount字段。修复使用 API Key 接入时确保ANTHROPIC_API_KEY已设置Claude Code 会优先使用 API Key 而不是 OAuth。5.5 工具调用卡住不返回现象模型返回了tool_use但终端一直没有输出。排查步骤检查 debug 日志搜索tool_use和tool_result。如果只有tool_use没有tool_result说明工具执行卡住。检查工具是否需要权限审批但审批提示没有显示。修复这通常是权限配置问题。检查permissions.ask里的工具是否在等待用户确认。如果是 headless 模式shouldAvoidPermissionPrompts会导致保守拒绝而不是弹出提示。5.6 上下文爆炸导致请求失败报错信息API Error: 413 Request Entity Too Large原因上下文超过模型窗口限制且压缩管线没有及时触发。排查步骤检查CLAUDE_CODE_AUTO_COMPACT_WINDOW是否设置过高。检查是否有超大工具结果没有被裁剪。查看/cost输出的 token 数。修复降低CLAUDE_CODE_AUTO_COMPACT_WINDOW到 0.85或者手动运行/compact。如果问题持续检查MAX_TOOL_RESULT_SIZE_CHARS是否设置过大。6. 从源码到实践把 Agent 主循环用起来理解 Claude Code 的架构最终要落到“怎么用”上。这一节给出几个实际场景的操作路径。6.1 调试自己的 Agent 循环如果你想在自己的项目里复现 Claude Code 的主循环可以从最小实现开始async function* agentLoop(prompt: string, tools: Tool[]) { const messages [{ role: user, content: prompt }]; while (true) { const response await callModel({ messages, tools }); if (response.stop_reason end_turn) { yield response.content; return; } if (response.stop_reason tool_use) { const toolUse response.content.find(b b.type tool_use); const tool tools.find(t t.name toolUse.name); const result await tool.call(toolUse.input); messages.push({ role: assistant, content: response.content }); messages.push({ role: user, content: [{ type: tool_result, tool_use_id: toolUse.id, content: result }] }); } } }这个最小实现包含了主循环的核心调用模型、识别工具调用、执行工具、回灌结果、继续循环。Claude Code 的复杂度在于它在这个骨架上加了压缩、权限、流式执行、中断恢复、Stop hooks 等工程细节。6.2 用 TaoToken 验证模型行为在调试 Agent 时你可能需要对比不同模型的表现。TaoToken 支持多个模型你可以在 settings.json 里切换{ model: claude-opus-4-20250514 }或者在运行时用/model命令切换。切换后观察同一个任务在不同模型下的工具调用序列。这能帮你理解模型能力对 Agent 行为的影响。如果你想看模型对话的原始请求可以使用 TaoToken 的模型对话功能https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_code_archutm_campaignrewrite6.3 长期编码任务的配置建议如果你用 Claude Code 做长期编码任务建议配置 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_code_archutm_campaignrewriteCoding Plan 针对长会话做了优化包括更高的 token 配额和更稳定的连接。配合 Claude Code 的压缩管线可以支持数小时的连续编码。6.4 接入文档和 API 参考如果你需要更详细的接入说明参考https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_code_archutm_campaignrewriteAPI 参考https://taotoken.net/api6.5 一个实际的调试案例假设你遇到一个问题Claude Code 在读取大文件后下一轮模型重复调用同一个工具。排查路径打开 debug 日志找到重复调用的轮次。检查tool_result是否被正确回灌。搜索tool_result和tool_use_id。如果tool_result存在但模型仍然重复调用检查上下文压缩是否把工具结果清理了。搜索microcompact或autocompact看是否在工具结果回灌后触发了压缩。如果是压缩导致的调整CLAUDE_CODE_AUTO_COMPACT_WINDOW或MAX_TOOL_RESULT_SIZE_CHARS。这个案例说明Agent 的行为问题往往不是单一原因而是主循环、工具协议、上下文管理三条线交织的结果。理解架构的价值在于你能快速定位问题在哪一层。6.6 关键文件速查如果你要深入源码这几个文件是必读的src/query.ts主循环所有对话推进的入口src/Tool.ts工具协议定义src/services/tools/toolExecution.ts工具执行链src/services/api/claude.tsAPI 请求组装src/state/AppStateStore.ts会话状态定义src/utils/claudemd.ts上下文发现和加载src/services/compact/压缩管线实现从query.ts开始读遇到不认识的类型再跳到对应文件。这样比从头到尾读一遍效率高得多。 SEO 优化官网定制响应式建站教育培训建站