Claude Code 怎样管理 Agent 循环、上下文与权限

假设项目里有一个登录 Bug:用户输入正确密码后仍然被重定向到登录页。你把这句话丢给 Claude Code,它要做的并不只是“生成一段修复代码”。它得先读项目结构和认证逻辑,必要时申请权限,运行测试和命令,改文件,再根据新的报错判断下一步,然后把这一切重复若干轮,直到它认为修好了。

这篇文章想做一件具体的事:把这个任务放进 Claude Code,然后一层层拆开,看一轮到底发生了什么。不是猜某个内部函数叫什么名字,而是弄清楚一个能用的编码 Agent 为什么需要模型之外的大量工程。上下文里到底装了什么,模型是怎么“调用”一个工具的,权限在哪一步拦截,长任务快撑爆窗口时又靠什么续命。

读完这篇,我希望一个刚接触 Agent 的人能用自己的话回答一个问题:Claude Code 处理一轮请求时,发给模型的到底是什么,模型又回了什么。这个问题答不上来,后面的权限、子代理、Hook 都只是名词。

先说明本文的证据边界

在拆之前先划清楚哪些是事实、哪些是推断,否则很容易把“我觉得它这么实现”写成“它就是这么实现”。

Claude Code 是持续迭代的产品。公开文档能确认它的用户可见能力,但不会逐一公开内部实现细节。社区有一些针对特定版本的源码级或逆向研究,可以帮助理解设计取向,但不能当成 Anthropic 的官方承诺。

内容层次 本文如何使用
Anthropic 官方文档 说明 Claude Code 当前支持的能力、配置方式,以及底层 Messages API 的工具调用协议
通用 Agent 机制 Agent Loop、工具调用回合、上下文重发这些是公开 API 的既定行为,不是某个私有实现,我会明确标注
社区架构研究 理解 Harness 与 Runtime 的参考,不据此断言内部细节
我的工程判断 明确标注为实践建议,不当成产品机制

举个需要小心的例子。VILA-Lab 对 Claude Code v2.1.88 的研究提出“Agent Loop 很小,外围基础设施占主要复杂度”的观点,还给出 1.6% 与 98.4% 的样本统计。这是一个值得讨论的研究结论,但它属于特定版本的特定样本,不是本文用来描述当前产品的事实基线。(VILA-Lab 研究)

我会先讲一轮 Agent Loop,它是整篇文章的地基,其余所有概念都挂在它上面。

Agent Loop:一次修 Bug 其实是很多轮请求

初学者最容易有的一个误解,是以为“让 Claude Code 修个 Bug”是一次调用:你说一句,模型想一会儿,吐出答案。实际上它是一个循环,而循环的每一圈都是一次独立的模型 API 请求。

一圈里发生什么

先记住这条主链路。无论界面在终端、IDE、桌面端还是网页端,Claude Code 都在“理解任务”和“操作环境”之间反复切换:

关键在于图里那条从 G 回到 B 的箭头。它不是装饰,它就是“Loop”本身。一圈的内容是固定的四步:

  1. 组织上下文:把当前该让模型看到的东西拼成一次请求。
  2. 调用模型:模型读完上下文,输出这一轮的决定。要么请求执行某个工具,要么认为任务完成、直接给出文字回答。
  3. 判断分支:如果模型请求了工具,进入权限检查和执行;如果没有,循环结束。
  4. 把结果写回:工具的输出(测试通过了没、diff 是什么、报了什么错)被追加进上下文,进入下一圈。

官方把 Claude Code 定义为能读取代码库、编辑文件、运行命令并接入开发工具的编码 Agent,能在多个文件和工具之间完成开发任务。(Claude Code 概览) 这句话背后的执行形态,就是上面这个循环。

循环什么时候停

这是初学者第二个容易忽略的点:循环不是无限的,也不是跑够 N 轮就停。它停在模型不再请求任何工具、而是直接输出文字回答的那一圈。用底层 Messages API 的话说,模型每次响应都带一个 stop_reason:只要是 tool_use,运行环境就执行工具、把结果喂回去、再问一次;直到某一轮变成 end_turn,才代表模型认为可以收尾了。这套“请求工具就继续、直接作答就停止”的判定,是公开 API 的既定行为,不是 Claude Code 的私有约定。

所以“修一个 Bug 用了几轮”不是你能预设的数字,而是任务本身决定的。定位越曲折、来回验证越多,圈数就越多。

登录 Bug 在循环里长什么样

把抽象的四步落到具体任务上,一次典型的修复大概会这样展开。每个“第 N 轮”都是一次完整的模型请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
第 1 轮  上下文:任务描述 + 项目里的 CLAUDE.md
模型决定:请求用 grep 搜索 "redirect" 出现的位置
执行结果:命中 authMiddleware.js 和 routes/login.js

第 2 轮 上下文:上面全部 + grep 结果
模型决定:请求读取 authMiddleware.js
执行结果:文件内容返回,发现 session 校验早于 cookie 写入

第 3 轮 上下文:再加上文件内容
模型决定:请求编辑 authMiddleware.js,调整校验顺序
执行结果:编辑成功,返回 diff

第 4 轮 上下文:再加上 diff
模型决定:请求运行 npm test
执行结果:认证相关测试全部通过

第 5 轮 上下文:再加上测试输出
模型决定:不再请求工具,直接说明改了什么、怎么验证的 → 循环结束

注意每一轮的“上下文”都在变长,它是累加的。这就引出下一个问题:这个不断变长的“上下文”,里面到底装了什么?

上下文到底装了什么

“上下文”是这类文章里最被滥用的词。初学者听到“给模型足够的上下文”,脑子里往往是一团模糊的“背景信息”。但在工程上它有非常具体的形状:上下文就是每一轮真正发给模型的那一段内容,它是一个有结构的列表。

一轮请求的四个组成部分

底层 Messages API 里,一次请求的内容大致由这几块拼成,模型看到的就是它们的总和:

组成部分 装的东西 在登录 Bug 里对应什么
系统提示(system) Agent 的角色、行为准则、可用能力说明 “你是一个编码 Agent,可以读写文件、运行命令……”
工具定义(tools) 每个工具的名字、用途描述、参数结构 read_fileeditbashgrep 各自的 schema
对话历史(messages) 用户说过的话、模型每一轮的决定 “修复登录重定向 Bug” + 前几轮模型的动作
工具结果(tool_result) 上一轮工具执行后返回的输出 grep 命中列表、文件内容、diff、测试输出

这里有一个初学者几乎必然会踩的认知坑:模型 API 本身不保存对话状态。服务端不会“记得”你上一轮聊了什么。是运行环境(也就是 Claude Code 这个外层程序)在本地维护着这个列表,每一轮都把从头到现在的全部内容重新发一遍。模型的“记忆”不是它自己的能力,而是这个被反复重发的列表。

这解释了两件事。第一,为什么上一节里每轮的上下文都在变长:因为工具结果在不断往列表尾部追加。第二,为什么长任务会变难。不是模型突然变笨了,而是这个列表越拼越大,接近了模型能一次读入的上限,也就是上下文窗口。

上下文窗口不只是“大小”问题

很多人以为上下文的难点是窗口不够大,塞不下。窗口大小确实是硬约束,但更隐蔽的难点是信息筛选。

设想第 20 轮时,列表里已经堆了十几个文件的全文、几百行日志、来回好几次的 diff。就算这些都还没超出窗口,模型在做第 20 轮判断时,也要在这一大堆里找出真正相关的几行。噪声越多,判断越容易被带偏。所以“给足上下文”和“别给太多上下文”其实是同一个问题的两面,这也是后面“上下文压缩”和“子代理”两节要解决的核心。

社区研究常把模型之外的这层工程统称为 Harness 或 Runtime。我理解它就是“让模型在有限且嘈杂的上下文里持续工作”的外层机器。这个概念有帮助,但它不是某个固定的官方模块名,别把它当成一个具体的类去理解。

工具调用的真实协议:模型只是“提议”

现在拆整篇最容易讲得含糊、也最值得讲清楚的一环。原来那句“模型负责判断,运行环境负责执行”是对的,但它没告诉你这中间的接力棒是怎么传的。

模型不会真的读文件

一个反直觉但必须建立的认知:模型没有能力读你的文件,也没有能力运行命令。 它能做的只有一件事,就是输出文本。所谓“调用工具”,本质是模型输出了一段结构化的请求,说“我想调用名为 read_file 的工具,参数是 path: authMiddleware.js”。这段请求本身不执行任何操作,它只是一个意图的声明。

真正去打开文件、把内容读出来的,是运行环境。它解析模型这段结构化请求,在本地执行对应操作,再把结果包装成一条工具结果塞回上下文,然后开始下一轮。整个协议是一来一回:

把这条链路记牢,很多东西就通了:

  • 为什么权限系统能生效:因为工具是运行环境执行的,不是模型执行的。模型只能“请求”,运行环境完全可以在中间拦下来说“这个不允许”。模型没有绕过这道关卡的通道。
  • 为什么验证不能交给模型自觉:模型输出“测试应该通过了”只是它的一段文本判断;只有运行环境真的跑了 npm test 并把输出喂回去,才有事实。这两者的差别,就是“写了代码”和“任务完成”的差别。

为什么要有专门的工具,而不是只给一个 bash

你可能会想:既然模型能请求运行命令,那给它一个万能的 bash 工具不就够了?它想读文件就 cat,想搜索就 grep,为什么还要 read_fileedit 这些专门的工具?

区别在于运行环境能对这次调用做什么。bash 给模型的能力最广,但也把一切都压成了一个不透明的命令字符串,运行环境只看到 "...",很难对它做细粒度的判断。而一个专门的 edit 工具带着明确的参数(改哪个文件、把哪段换成哪段),运行环境就能做很多事:拦截并要求确认、在写入前检查文件是否被别人改过、把这次编辑渲染成一个清晰的 diff 给你看、甚至判断它能不能和别的操作并行。(工具与命令)

换句话说,把一个动作从 bash 提升为专门的工具,等于给运行环境装上了针对这个动作的抓手。需要管控、需要渲染、需要审计的动作,就值得做成专门工具;只是图个方便的宽泛操作,才留给 bash。这也是理解权限系统的前提。

一次修 Bug 时,各层分别做什么

有了上面三节的地基,可以把整条链路按职责摊平来看。这张表不再是“有哪些层”的罗列,而是每一层在协议里站在哪个位置:

环节 登录 Bug 任务中的动作 它在协议里的位置
上下文 把项目规则、认证代码、已有测试和报错拼进请求 决定模型这一轮能看到什么
模型 读完上下文,输出下一步:请求工具,或直接作答 只做判断,输出的是意图,不是操作
权限 在工具真正执行前,判断是否放行 卡在“模型请求”与“运行环境执行”之间
工具执行 搜索调用点、读文件、改实现、跑测试 把模型的意图变成真实操作并产生结果
观察与验证 把测试输出、diff、报错写回上下文 让下一轮的判断建立在事实、而不是猜测上

最容易被误解的还是模型这一行。模型会说“读这个文件”“运行这条测试”“编辑这个函数”,但它说的每一句都只是请求,真正动手的是运行环境。理解了这一点,权限系统就不再是一个突兀的设置项,而是这条协议里的必然一环。

权限系统:为什么它是架构而不是开关

Claude Code 的权限不是一个“允许 / 不允许”的总开关。当前文档支持 allow、ask、deny 三类规则,并支持 defaultacceptEditsplanautodontAskbypassPermissions 等模式。规则的匹配顺序是先看 deny,再看 ask 和 allow,命中的结果决定这次工具调用怎么处理。(权限设置)

为什么 deny 优先

这个匹配顺序不是随便定的,它反映了一个安全默认:禁止应当压过允许。如果你写了一条“允许所有 Bash”,又写了一条“禁止读取 .env”,你几乎肯定希望后者赢,哪怕前者的范围更宽。deny 先匹配,保证了你明确不想让它碰的东西,不会被一条宽泛的 allow 意外放行。写权限配置时顺着这个心智模型走,就不容易配出漏洞。

以修 Bug 为例,一份合理的权限设计可以是:

1
2
3
4
5
6
7
{
"permissions": {
"allow": ["Bash(npm test *)", "Bash(npm run lint)"],
"ask": ["Bash(git push *)"],
"deny": ["Read(./.env)", "Read(./secrets/**)"]
}
}

这份配置在说:跑测试和 lint 直接放行,它们无害且频繁;推送远端要先问我,因为有外部副作用;读密钥文件一律拒绝,哪怕模型有正当理由也不给。这比“全部允许”更接近真实团队需要的边界。

权限和沙箱是两层

更严格的场景还可以启用 Bash 沙箱,分别限制文件系统写入和网络访问。要区分清楚:权限规则管的是“这次工具调用放不放行”,沙箱管的是“就算放行了,它能触及的文件和网络范围有多大”。两者是不同层面的控制,应按项目风险一起设计,而不是二选一。(沙箱配置)

回到上一节的协议图你会发现,权限之所以能拦得住,正是因为工具由运行环境执行。模型请求和实际执行之间天然有一道缝,权限就站在这道缝里。

上下文压缩与子代理:长任务如何续命

前面讲过,上下文是累加的,越拼越长,迟早逼近窗口上限,或者噪声大到影响判断。Claude Code 有两种机制应对这件事,方向正好相反:一种是压缩已有内容,一种是隔离新产生的内容。

上下文压缩:把旧内容折叠成摘要

当会话逼近窗口上限时,可以让系统自动把较早的历史总结成一段更短的摘要,用摘要替换掉原来那一大段逐字记录,从而腾出空间继续跑。注意它的动作是“总结并替换”,不是“直接删掉”,所以关键信息尽量保留,冗长的过程被压掉。这是控制长任务信息量的主要手段:让模型带着一份浓缩过的过去,继续处理新的一轮。

这解释了开头流程图里那条 上下文压缩 -.整理.-> 组织上下文 的虚线。它不是常驻的一步,而是在会话变长、需要腾地方时才触发的整理动作。

子代理:让高噪声的活儿在别处发生

压缩解决的是“已经堆进来的东西太多”,子代理解决的是“别让某些东西堆进来”。

如果主 Agent 要一边研究认证代码、一边跑完整测试套件、一边翻大量日志,主会话很快会被这些细节淹没,而这些细节里,绝大部分之后再也用不上。Claude Code 的子代理可以在独立的上下文里完成一项自包含任务,跑完之后只把摘要返回主会话。

官方把它的典型用途说得很具体:把“文件内容很多、但主会话不会再次引用”的工作移出去。例如让子代理运行整个测试套件,只返回失败的测试和错误信息。(子代理文档) 关键在于“独立上下文”这四个字:子代理翻遍的那几百行日志,从头到尾都待在它自己的上下文里,主会话只收到一句“这三个测试挂了,原因是这些”。

对登录 Bug,我会这样分工:

1
2
3
4
主 Agent:确定修复范围,维护最终计划和改动
Explore 子代理:找出认证中间件、路由和重定向调用链,只回报关键位置
测试子代理:运行认证相关测试,只回报失败项与关键日志
主 Agent:根据两份摘要修改代码,再运行目标验证

子代理不是越多越好。任务需要频繁来回修改、多个阶段共享大量背景,或者只是一个很小的改动时,主会话通常更快,因为子代理的“隔离”本身有成本,来回传摘要也要占主会话的上下文。官方也提醒,过多子代理的详细回传仍会消耗主会话的额度。(使用边界)

把这两节连起来看,压缩和子代理其实在回答同一个问题:怎么让模型在有限的上下文里,把一件需要很多步的事持续做下去。压缩往回收拾旧账,子代理把新麻烦挡在门外。

项目指令、记忆和上下文不是一回事

到这里可以澄清一组经常被混为一谈的概念。它们都会“进入上下文”,但进入的时机和用途完全不同。

Claude Code 支持用 CLAUDE.md 保存项目约定,比如编码规范、架构决策、常用命令和 review 清单。官方文档说明,它会在会话开始时读取这些指令;同时也支持跨会话保存自动记忆。(指令与记忆)

我会按“什么时候该被看到”来分:

放什么 适合的位置 原因
团队长期约定 CLAUDE.md 需要在每次相关会话开始时就进入上下文
可重复的专项流程 Skill 只有任务匹配时才加载,平时不占上下文
必须执行的检查 Hook、测试或 CI 需要由程序保证执行,而不是靠提示词“提醒”模型
一次任务的文件和命令输出 当前对话上下文 任务结束后通常不必长期保留

分清楚这几栏的好处是:你不会把一个“必须每次都执行”的检查写进 CLAUDE.md,然后指望模型每次都记得照做。那是提示词,不是硬约束,这个区别下一节接着说。

Hook、Skill 和 MCP 的职责不同

这三个概念都能扩展 Claude Code,但它们解决的问题不在一个维度,不该互相替代:

机制 适合做什么 登录 Bug 示例
Hook 在生命周期事件上自动执行确定性动作 编辑后自动跑格式化,提交前自动跑认证测试
Skill 提供可复用、可按需加载的工作流 “排查登录问题”的检查顺序和常见陷阱
MCP 连接外部数据或工具 查询工单、读取设计文档、查看线上日志

三者最本质的区别在“由谁保证执行”。Hook 由程序在确定的时机触发,可以在会话、每一轮、每次工具调用等生命周期节点运行命令、发 HTTP 请求或注入提示词。因为是程序触发的,它适合承担“必须发生”的动作;但也正因为它会自动执行,要像审查普通脚本一样审查它的输入、权限和网络行为。(Hook 参考) Skill 则是“可能需要时才读”的工作流,交给模型自己判断何时加载。MCP 管的是从哪里取数据、往哪里发指令。

我的判断是:能用测试、Hook 或 CI 确定执行的规则,不要只写进 CLAUDE.md;需要靠模型理解任务才决定的流程,才写成 Skill。前者是硬约束,后者是提醒,把它们分开,Agent 的行为才可预期。

一个可以自己验证的小实验

本文不声称已经验证 Claude Code 的内部实现。上面讲的协议层面(工具调用、上下文重发、循环终止)是公开 API 的行为,产品内部怎么组织这些,官方没有全部公开。如果你想判断这些设计对自己的项目有没有价值,可以做一个小实验:

1
2
3
4
5
6
任务:修复一个有稳定复现步骤的 Bug

对照 A:只给自然语言任务,不提供项目指令或专项 Skill
对照 B:增加 CLAUDE.md、一个排查 Skill 和一个测试 Hook

记录:是否先定位复现条件、无关改动的数量、测试是否真的执行、返工次数

这个实验只能反映你自己项目、模型和权限配置下的表现,不能证明 Claude Code 的内部结构。但它能回答一个更实际的问题:你加的这些规则和流程,是否真的让一次开发任务变得更可控。做的时候留意上一节的区分,看 B 组里那个测试 Hook 是不是真的每次都跑了,还是被你误写成了 CLAUDE.md 里一句“记得跑测试”。

我的结论

如果只带走一句话,我希望是这句:Claude Code 的一次任务,是一个不断把上下文重发给模型、再把工具结果写回上下文的循环;模型只做判断,运行环境才动手。 前面所有的层,权限、压缩、子代理、Hook,都是围着这个循环长出来的。

它的价值不在于“模型会不会写代码”这一件事,而在于它把模型、代码库、工具与规则放进了一个可持续的循环里。对开发者来说,最值得学的是四件事:给任务足够但不过量的上下文;把工具权限当成架构的一部分来设计;用子代理隔离高噪声的工作;用测试和 Hook 去验证结果,而不是相信模型的自我报告。

社区研究把模型之外的这层工程称为 Harness。我认同这个视角,但更愿意把它当成一份问题清单,而不是一个神秘的技术名词:上下文会不会失控,工具会不会被越权调用,长任务会不会卡住,结果有没有验证证据。把这几个问题逐个设计清楚,Agent 才会从演示走进日常的开发流程。


参考资料