乐于分享
好东西不私藏

ClaudeCode源码精读 | 02 所谓 Agent,不过是给模型套了一个循环

ClaudeCode源码精读 | 02 所谓 Agent,不过是给模型套了一个循环

这是「Claude Code 源码精读」系列的第 2 篇。上一篇我们建立了对整个源码的全局认知,这一篇我们进入第一个核心模块:Agent主循环。

读完你会明白,所谓 Agent,并不神秘。

你会搞清楚三件事:

  • • Agent 到底是什么——一个模型驱动的工具使用循环
  • • 它和传统软件最本质的区别在哪里
  • • Claude Code 的主循环里,藏了哪些工程细节

从聊天框到 Agent,核心变化是什么?

大模型最早出现在公众视野里,是以聊天框的形式——你输入一段话,它回复一段话,对话结束。ChatGPT、DeepSeek,都是这个模式。

这个模式能解答问题、写文章、帮你想方案。但它有一个根本的局限:模型只能说,不能做。 它可以告诉你怎么改这段代码,但它自己改不了;它可以告诉你服务器可能挂了,但重启不了。说和做之间,隔着一道墙。

这道墙意味着,所有模型给出的答案,最终都要人来执行。模型是顾问,你是执行者。这个分工在简单任务上还行,但一旦任务变复杂——需要读十几个文件、反复修改、跑测试、看报错、再修改——人变成了流水线上的传话员,模型说一步,你做一步,效率极低,真正的价值也就这么被卡住了。

于是人们开始想:能不能把这道墙打掉?能不能让模型自己去读文件、自己执行命令、自己调用外部 API?

这个想法催生了”工具调用”——给模型配上一套工具,每个工具对应一个真实的操作:读文件、写文件、执行终端命令、查数据库……模型不再只是”建议你去做”,它可以通过工具伸手到外部世界

但工具调用带来了一个新问题:谁来管这个过程?

模型调用了一个工具,结果出来了,接下来怎么办?是直接结束,还是把结果告诉模型,让它决定下一步?如果下一步又需要用工具呢?如果任务很复杂,需要连续调用十几个工具呢?

这就是 Agent 设计(也可以说是 harness engineering)要解决的核心问题:管理模型和工具之间的协作过程。

模型负责思考——看到当前状态,决定该用哪个工具,或者任务已经完成了;工具负责执行——拿到模型的指令,去真实世界里操作,把结果带回来。这两件事交替发生,一轮一轮的执行下去。

这个一轮一轮协作的骨架,就是一个Agent主循环。也就是这篇文章要讲的核心。理解了这个主循环,你也就理解了Agent最核心的概念,之后其他的概念都是在这个基础上去延展的。


一个例子

光说”Agent 循环”太抽象,直接看一个具体的案例。

假设你对 Claude Code 说:“我的程序跑起来一直报一个 TypeError,帮我修一下”

你发了这条消息,等了十几秒,Claude code 回复:”找到问题了,是第 42 行没做空值判断,已经修好了。”

这十几秒背后,就是一个完成的 Agent 循环。

第 1 轮

Claude 收到指令,但它不知道错误在哪里。它的第一个动作:调用”执行命令”工具,把程序跑一遍,拿到完整的报错信息。

工具执行,报错日志返回了。循环继续。

第 2 轮

Claude 看到了报错:TypeError: cannot read property 'id' of undefined,定位到了出错的文件和行号。但光看报错不够,它需要看上下文。于是它调用”读文件”工具,读取出错的那段代码。

工具执行,代码内容返回了。循环继续。

第 3 轮

Claude 读完代码,找到了问题:某个地方没有判断对象是否为空就直接取值了。它调用”修改文件”工具,加上空值判断,把 bug 修掉。

工具执行,修改成功。循环继续。

第 4 轮

Claude 修完了,但它不确定改对了。它再次调用”执行命令”工具,重新跑一遍程序验证。

这次程序正常运行,没有报错。Claude 回复你:”找到问题了,是第 42 行没做空值判断,已经修好了,刚才跑了一遍确认没有报错。”

这一轮,它没有调用任何工具。循环结束。


一个 Agent 循环,就是一次从用户发起对话到Agent完成任务的完整过程。

整个过程,Claude code 调用了 Claude 模型API 四次,执行工具三次每一轮做什么,都是根据上一轮工具返回的结果来决定的——不是预先规划好的,而是走一步看一步。

不管任务是调试报错、重构代码、整理报告、完成定时任务,还是发送一封邮件,底层都是这同一个循环:

任务越复杂,循环跑的轮次越多。但骨架始终是这一个。

Agent 设计,就是围绕这个循环展开的。

query.ts主循环

打开 Claude Code 的源码,query.ts 文件的第 307 行,就是这个循环的起点:

// query.ts(精简后的骨架)while (true) {  // 第一步:把完整消息历史发给模型,流式接收响应  for await (const message of callModel({ messages, tools, systemPrompt })) {    // 收集模型输出的文本块和工具调用块  }  // 第二步:模型这轮没有调用工具 → 任务完成,退出  if (!needsFollowUp) {    return { reason: 'completed' }  }  // 第三步:模型调用了工具 → 执行工具,把结果拼回历史,继续下一轮  await runTools(toolUseBlocks, ...)  state = {    messages: [...旧历史, ...模型回复, ...工具结果],    transition: { reason: 'next_turn' }  }  // continue → 回到 while 顶部,开始下一轮}

其中的四个元素,就是整个 Agent 能力的底层支柱:

  • • while (true):循环本身。让模型不止运行一次,可以反复执行,直到任务完成。
  • • callModel:每一轮调用模型 API,把完整的消息历史、工具列表、系统提示一起发出去,流式拿回模型的响应。
  • • needsFollowUp:这一轮模型有没有调工具?有就继续,没有就退出。它是整个循环的开关。
  • • runTools:执行模型请求的工具,把真实世界里的操作结果带回来,追加进消息历史,供下一轮使用。

其余的代码,都是在这个骨架上的延展。


其实到这个部分,这篇文章的核心就已经讲完了。但我还是想补充聊几个附加的问题。

1、谁来决定这个循环?

传统软件是确定性的——代码写死了每一步该做什么,输入相同,输出相同,流程可预测。

但在 Agent 循环里,模型读到工具结果,根据自己的判断决定下一步——调哪个工具、用什么参数、还是直接回答。没有人预先规定它走哪条路。

也就是说,每一轮该做什么,是模型决定的。他作为大脑去做决策。所以Agent的设计和模型的能力一定是相辅相成的。

这就是为什么这件事叫”驾驭工程(Harness Engineering)”——你不是在写一个执行固定步骤的程序,你是在驾驭一个有自主判断能力的模型。它的行为是不确定的,你的工作是给它设定好环境,让它在这个环境里自主完成任务。

2、上下文消息是怎么流转的?

Agent 循环的核心动作,是一条不断滚大的消息历史。每一轮,系统把完整的历史打包发给模型。模型回复之后,回复内容和工具结果又追加进历史,供下一轮使用。

用数据流的视角来看我们最前面的案例:

第 1 轮发给模型:  [你的指令]第 2 轮发给模型:  [你的指令]  [Claude 第1轮的回复:调用运行代码工具]  [工具结果:程序报错,TypeError 在第42行]第 3 轮发给模型:  [你的指令]  [Claude 第1轮的回复:调用了运行代码工具]  [工具结果:程序报错,TypeError 在第42行]  [Claude 第2轮的回复:调用了读取文件工具]  [工具结果:第42行代码内容是……]  ← 第 N 轮就是前面所有轮次的完整累积

Claude 每一轮看到的都是从头到尾的完整历史,不是只看这一轮。它相当于每次都从头重读一遍对话记录,再决定下一步,来保证任务的持续一致。

值得注意的是,追加进历史的不只是工具输出本身,还有几类”附件”一起打包:文件变更通知、记忆预取、技能发现、用户插入的新消息。这些附件保证 Claude 每一轮都能看到完整上下文,这也是上下文越跑越大的原因之一,这里不展开了,后面上下文压缩再单独讲)。

3、边界条件的工程设计

正常情况下,循环的结束很简单——Claude 这一轮没有调用工具,任务完成,回复返回给用户。

但作为一个工程产品,这远远不够。现实里总有各种意外:上下文撑爆了、模型输出被截断、用户中途按了 Ctrl+C……每一种都要有明确的处理方式,不能让循环就这么悄悄挂掉。

源码里,query.ts 的 queryLoop 函数有 12 处 return(退出)和 7 处 continue(续跑),覆盖了各种异常情况:

  • • 上下文超限 → 先尝试压缩历史(reactive_compact_retry),压缩失败才退出(prompt_too_long
  • • 模型输出被截断 → 先把 token 上限从 8k 升到 64k 重试(max_output_tokens_escalate),还不够就注入”请继续”再跑一轮(max_output_tokens_recovery
  • • 钩子发现问题 → 把问题注入消息,让模型自己修正(stop_hook_blocking
  • • 用户按 Ctrl+C → 立即退出(aborted_streaming / aborted_tools

这也是 CC 源码里最能体现工程成熟度的地方,也是它这么丝滑的体验所必不可少的 “dirty work” —— 不只有主路径,它是针对每一条可能出问题的分支,都做了很细致、围绕用户体验的管控处理。


如果你想自己搭一个 Agent

理解了主循环的结构,最小可用的 Agent 其实只需要三件事:

1. 一个模型调用把消息历史 + 工具列表发给 API,拿回模型的回复。

2. 一套工具执行机制解析模型回复里的工具调用,执行对应的操作,拿到结果。

3. 把结果喂回历史,再调一次把工具结果追加进消息历史,回到第一步。

循环,直到模型不再调用工具。

就这三步,你就有了一个 Agent 的骨架。它可以接任何模型(GPT、Gemini、DeepSeek),可以挂任何工具(搜索、数据库、发邮件)。Claude Code 源码里那几十万行代码,大多数是在把这三步做得更健壮——上下文太长怎么办、模型报错怎么办、用户中断怎么办、多个工具并发怎么调度。

但骨架就是这三步。

到这里,你就理解了,不管是所谓的Agent 设计、Harness 工程,这些高大上的词汇,本质上也不过就是给模型套了一个循环而已。

下一期,我们来看循环里反复出现的”工具”到底是什么,它又是怎么帮助模型实现从说到做的跨越的。


「Claude Code 源码精读」系列持续更新,关注我,每期拆解一个核心模块。