乐于分享
好东西不私藏

Claude Code 源码架构解密之如何实现一个最小的 Agent

Claude Code 源码架构解密之如何实现一个最小的 Agent

前言

最近花了一些时间读 Claude Code 的源码,了解了里面的很多工程设计:Agent Loop、工具调用、权限控制、上下文管理、Todo、Subagent、Skill、MCP 等等。

准备写一系列的文章围绕 Claude Code 的工程实现做一次系统拆解,帮助搞清楚一个工业级 Agent 到底应该怎么从零搭起来。

Agent 到底是什么

要理解 Agent,先要理解为什么模型需要 Tool

首先要知道:大模型只能回答其训练数据时间截止前的信息。

单独调用模型时,模型只能基于 已有知识 和 上下文 做判断。

这里的 已有知识,主要指模型在训练阶段学到的通用知识,比如语言、代码、常识、公开资料里的规律和概念。

而 上下文,指的是你这次对话里发给模型的内容,比如你的问题、前面的聊天记录、System Prompt、你粘贴进去的文件片段等。

所以 LLM 无法做到浏览网页、查询数据库、执行代码、编辑文件等等操作。

这就是为什么我们需要给大模型提供工具,让它能拥有额外的能力。

Tool 打通了模型外部能力接口

Tool 可以让模型调用外部系统、获取外部信息的能力,比如:

  • • weather:查询实时天气。
  • • web_search:查询外部网页。
  • • edit_file:修改文件。

模型本身仍然不直接执行这些动作。它只是生成一个结构化的工具调用请求:要调用哪个工具、传入什么参数。

然后程序才能根据工具名找到对应函数,根据参数执行真实动作,再把结果返回给模型。

比如模型生成的 tool_use 可能长这样:

{  "type": "tool_use",  "id": "toolu_001",  "name": "weather",  "input": {    "city": "北京",    "date": "今天下午"  }}

工具执行完以后,程序会把结果包装成 tool_result 再返回给模型:

{  "type": "tool_result",  "tool_use_id": "toolu_001",  "content": "北京今天下午有小雨,降水概率 70%。"}

大模型只是生成一个结构化的工具调用请求,真正执行动作的是程序里的工具系统。

有 Tool 调用后,流程变成:

Agent Loop

模型本身存在幻觉,并不能保证通过一次工具的调用就能完成任务,大多时候大模型都需要通过多次的 工具调用 才能实现任务,所以还需要解决另一个问题:

模型拿到工具结果后,能不能继续推进任务?

比如买咖啡这件事:

帮我买一杯冰美式,如果没有冰美式,就换成拿铁

模型第一次可以调用工具去问:

还有冰美式吗?

如果工具返回:

冰美式卖完了。

这时任务还没有结束,模型必须继续判断:

那我应该再问有没有拿铁。

也就是大模型需要根据工具的调用结果进行判断,即进入一个循环:

思考下一步-> 调用工具-> 观察工具结果-> 反思当前结果是否完成任务-> 如果没完成,就继续思考下一步

这个循环,就是 Agent Loop。

可以看到这个 Agent Loop 的核心逻辑就是通过思考-行动-观察的循环机制实现目标,即

Reasoning -> Acting -> Observation 的循环思考 -> 行动 -> 观察

这就是 ReAct 思维模式落到 Agent 代码里的工程实现。

Tool 让模型有了“手”,Agent Loop 让模型有了“持续行动和思考的能力”。

所以 Agent 的最小闭环 = 工具 + Agent Loop。

Agent loop 的上下文过程

大模型本身是没有记忆的,它之所以看起来“记得刚才发生了什么”,是因为 Agent 在每一轮都把新的消息追加进 上下文 中,再把这份越来越长的上下文重新发给模型。

所以,可以用上下文的累积观测 用户、Agent 和 大模型之间信息是怎么流转的。

还是买咖啡这个例子:

帮我点一杯冰美式。如果没有,就换成拿铁。

3.1 用“上下文栈”看累积过程

初始时,上下文信息 messages[] 只有用户任务:

messages = [  user("帮我点一杯冰美式。如果没有,就换成拿铁。")]

模型第一次思考后,追加一条 assistant 消息:

messages = [  user("帮我点一杯冰美式。如果没有,就换成拿铁。"),  assistant(tool_use: "问店员还有冰美式吗?")]

工具执行完,结果也会追加进去:

messages = [  user("帮我点一杯冰美式。如果没有,就换成拿铁。"),  assistant(tool_use: "问店员还有冰美式吗?"),  user(tool_result: "冰美式已经售罄")]

这时模型下一轮看到之前得到的信息:

+ 用户原始目标+ 模型刚才调用过什么工具+ 工具返回了什么结果

所以它才知道:用户允许替换成拿铁,于是继续追加下一轮,这就是 Agent Loop 里不断的循环,最终所有的上下文如下:

messages = [  user("帮我点一杯冰美式。如果没有,就换成拿铁。"),  assistant(tool_use: "问店员还有冰美式吗?"),  user(tool_result: "冰美式已经售罄"),  assistant(tool_use: "问店员还有拿铁吗?"),  user(tool_result: "拿铁还有,可以下单"),  assistant("冰美式已经售罄,我已经为你改点拿铁。")]

所以从代码实现角度看 Agent Loop 其实就是:

围绕上下文不断追加 assistant 输出和 tool_result,然后把更新后的上下文重新发给模型。

Agent Loop 代码实现

这里可以看下 Agent Loop 简略版的实现:(Claude Code 里叫 queryLoop)

//  claude code 里叫 queryLoopasync function queryLoop(messages) {  while (true) {    // 1. 每一轮都把当前完整上下文发给模型。    const response = await client.messages.create({      model: MODEL,      system: SYSTEM,      messages,      tools: TOOLS,      max_tokens: 8000,    })    // 2. 模型这一轮的输出也要进入上下文。    messages.push({      role: 'assistant',      content: response.content,    })    // 3. 如果模型没有请求工具,说明它认为任务结束了。    if (response.stop_reason !== 'tool_use') {      return    }    const results = []    for (const block of response.content) {      if (block.type === 'tool_use') {        // 4. 模型请求工具时,由程序真正执行工具。        const tool = TOOLS.find((tool) => tool.name === name)        if (!tool) {          return `Error: Unknown tool: ${name}`        }        const output = await tool.call(input)        // 5. 工具结果必须带上 tool_use_id,模型才能知道它对应哪次工具调用。        results.push({          type: 'tool_result',          tool_use_id: block.id,          content: output,        })      }    }    // 6. 把工具结果作为 user message 追加回上下文,然后进入下一轮。    messages.push({      role: 'user',      content: results,    })  }}

总结

看完上面的内容应该能理解一个最简单的 Agent 是要怎么实现了,这也是 Claude Code 这类 Coding Agent 的第一性原理。不过在 Claude Code 源码里,这个主循环叫 queryLoop()

当然, 要想让一个生产级 Agent 长期稳定地跑起来,还需要解决很多工程问题:比如上下文如何压缩和恢复、工具权限如何拦截、工具如何自我纠错、Skill 的实现原理、如何通过 MCP 接入外部工具生态等。

这些内容我会在后续文章里继续拆解。如果你对“从零拆解工业级 Agent”这个系列感兴趣,可以点个关注~