Day51|从0学习Claude Code(一):一个while循环加一个Bash,复刻Claude Code的心脏

苦猿的大模型日记 · Day51 · 从0学习Claude Code(一)Agent Loop-帮普通人把AI学进简历系列

前言:日记开新模块,把 Claude Code 拆开复刻一遍

咱们这个大模型日记写到第 51 篇,RAG 拆完了,推理加速拆完了,Agent 的通用玩法也过了一遍。从这篇起,日记开一个新模块,干票大的:从0学习 Claude Code

先说清楚这个模块不是讲什么的------不是教你装软件、不是背快捷键、也不是"10 个 prompt 让你效率翻倍"。

它只干一件事:把 Claude Code 拆开,一个机制一个机制,自己动手用 Python 复刻一遍。权限是怎么拦命令的、子代理是怎么隔离上下文的、上下文快满时是怎么压缩的、记忆是怎么跨会话活下来的------每篇拆一个,代码全部自己敲、自己跑、自己踩坑。Day 编号照常往下排,模块内再用(一)(二)(三)接着数。

第一篇,拆它的心脏:Agent Loop

一个 while True 循环,加一个 bash 工具,142 行 Python。跑起来之后,模型能在你的终端里自己敲命令、自己执行、自己看结果、自己决定什么时候停------Claude Code 所有花活的地基,就是这 142 行。

读完这篇你会拿到三样东西:

  1. Claude Code 的真实工作原理:模型负责决策,代码负责执行,这套分工为什么成立
  2. 一份能直接跑的最小内核代码,GLM、DeepSeek、Kimi 的 Key 都能用,不需要 Anthropic 账号
  3. 一张后续十几个机制的路线图,知道这个模块往后每一步要补什么

门槛:会 Python 基础语法、有一个任意大模型的 API Key。够了,直接开始。


PART 01:先破神话------Claude Code 的心脏,是一个你早就会写的循环

在动手之前,先把一个流传很广的误解拆掉。

很多人觉得 Claude Code 强,是因为它代码量大、工程复杂------背后肯定是一套精密的调度系统,替模型规划好每一步。方向反了。

Claude Code 的架构拆到最底下,就两层:

Claude Code = 模型 + Harness。

模型是大脑,这个你没得换、也不用你操心。Harness 是载具------你自己写的那部分代码,它只干五件事:

  • 给模型工具(能执行命令、能读写文件)
  • 给模型知识(系统提示、项目文档)
  • 把工具执行结果喂回给模型
  • 提供行动接口(终端、编辑器)
  • 守住权限边界(哪些命令不许跑)

注意这五件事里没有一件是"替模型做决定"。

Claude Code 从头到尾不决定"下一步干什么"。模型说跑 ls,它就跑 ls;模型说任务完成了,它就停。决策权 100% 在模型,执行权 100% 在代码------这就是它能保持简单的全部秘密。

Agency 不在你的编排代码里

理解了上面这点,你就理解了为什么市面上大量"Agent 平台"是伪 Agent。

不少团队做 Agent 的路径是这样的:打开一个画布,拖几个节点,串几段提示词,A 节点的输出接 B 节点的输入,再写一堆 if-else 兜底。做完发现模型经常不按流程走,于是加更多节点、更多判断------越修越复杂,越复杂越笨。

问题出在认知上:感知、推理、行动的能力(agency),是预训练阶段就写进模型权重里的,不是你的编排代码写出来的。

你画的那个流程图,本质上是把一个会自己走路的模型,绑在一辆固定的推车上,按你画好的路线推。流程图画得越精巧,模型的自主性死得越彻底。

所以从零复刻 Claude Code 的正确姿势是反过来的:不训练模型,只造 Harness。把"下一步干什么"的决策完整地让给模型,你只负责把它的每个决定执行掉、把结果递回去。

Claude Code 被几百万人每天真实使用、被各种复杂项目锤炼,是这套思路目前最好的工程范本。把它拆明白,你写任何领域的 Agent------不限于写代码------都知道骨架该怎么搭。

这也是简历视角下最值钱的部分。面试里"你用过 Agent 框架吗"已经烂大街,"我从零复刻过 Claude Code 的整套机制,权限、子代理、上下文压缩都是自己写的",这句话的含金量完全不一样。


PART 02:痛点还原------模型会"说"不会"做",你一直在当人肉循环

现在看一个你几乎一定经历过的场景。

你在对话框里问模型:"帮我看看这个目录下有哪些 Python 文件,然后跑一下 main.py。"

模型秒回一条漂亮的命令:

复制代码
ls *.py && python main.py

然后,停了。

它不会自己跑这条命令,也看不见命令的输出。你只能:复制命令 → 切到终端 → 回车 → 看到报错 → 复制报错 → 切回对话框 → 粘贴 → 模型给你改好的命令 → 你再复制回终端......

每一个来回,你都在充当模型和真实世界之间的"中间层"。

这个过程有个准确的叫法:你在当人肉循环。模型负责"想",你负责"做"和"把做的结果念给它听"。

而且这不只是累的问题。手工搬运是有损耗的:

  • 来回切窗口五六次之后,你会开始偷懒------输出太长就不贴全了,只截一段;或者看到命令小错,自己顺手改了再跑
  • 模型拿到的信息一旦不完整,它的判断链就断了
  • 最要命的是速度:一个本来 30 秒能自己完成的任务,人肉循环要 5 分钟

Agent 这个词去掉所有包装,最小定义就一句话:把"你"从这个循环里抠掉。

具体要自动化的是三步:

  1. 模型说"我要跑这条命令"------而且必须是结构化信号,不是让你肉眼去解析它输出的文本
  2. 代码替你执行这条命令
  3. 执行结果原样塞回对话,让模型接着推理

这三步首尾相接、转起来的那一刻,就是 Agent Loop。下一部分,我们把它写出来。


PART 03:动手------142 行,四块代码拼出心脏

整个内核 142 行,拆成四块:工具定义、执行器、主循环、命令行入口。一块一块写。

准备工作(5 分钟)

新建一个目录,两个依赖就够:

复制代码
pip install anthropic python-dotenv

然后放一个 .env 文件,三个变量:

复制代码
ANTHROPIC_API_KEY=你的Key
MODEL_ID=glm-4.7
# 可选:如果你想用 GLM / DeepSeek / Kimi 的 Anthropic 兼容接口
ANTHROPIC_BASE_URL=https://open.bigmodel.cn/api/anthropic

这里有个对普通人很友好的细节:anthropic 这个 SDK 只是一种消息格式 ,跟用谁家模型无关。现在主流国产模型都提供 Anthropic 兼容接口,把 ANTHROPIC_BASE_URL 指过去、MODEL_ID 换成对应型号,就能直接跑。不需要非有 Anthropic 账号。

代码里读配置就三行:

复制代码
from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv(override=True)
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]

第一块:工具定义------只给一个 bash

先定系统提示,一句话:

复制代码
SYSTEM = f"You are a coding agent at {os.getcwd()}. Use bash to solve tasks. Act, don't explain."

翻译过来:你是一个在我这个目录下干活的编码代理,用 bash 解决问题,行动,别解释

别小看"Act, don't explain"这五个词,它是整个产品性格的起点------少废话,直接干。Claude Code 那种"闷头干活"的气质,源头就在系统提示里。

然后是工具定义,全文唯一的工具:

复制代码
TOOLS = [{
    "name": "bash",
    "description": "Run a shell command.",
    "input_schema": {
        "type": "object",
        "properties": {"command": {"type": "string"}},
        "required": ["command"],
    },
}]

一个工具就是三样东西:名字 + 描述 + 参数 schema 。模型只看得见 description,它靠这一句"Run a shell command"判断什么时候用、怎么用。

就这么点配置,模型就知道自己手里有一把锤子了。

第二块:run_bash 执行器------带三条最小安全带

模型说要跑命令,总得有人真去跑。执行器长这样:

复制代码
def run_bash(command: str) -> str:
    dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
    if any(d in command for d in dangerous):
        return "Error: Dangerous command blocked"
    try:
        r = subprocess.run(command, shell=True, cwd=os.getcwd(),
                           capture_output=True, text=True, errors="replace",
                           timeout=120)
        out = (r.stdout + r.stderr).strip()
        return out[:50000] if out else "(no output)"
    except subprocess.TimeoutExpired:
        return "Error: Timeout (120s)"
    except (FileNotFoundError, OSError) as e:
        return f"Error: {e}"

逻辑很简单:收到命令 → 查黑名单 → 用 subprocess 真实执行 → 把 stdout 和 stderr 拼起来返回。

但里面有三条安全带,每条都值得停下来想一想:

安全带一:黑名单。 rm -rf /sudoshutdownreboot,四条会直接掀桌子的命令,见到就拦。坦白讲这个名单极其粗糙------rm -rf ~/你的毕业设计 它就拦不住。真正像样的权限系统(审批、白名单、分级放行)是后面的篇章要补的机制,现在先裸奔,所以请一定在临时测试目录里玩

安全带二:120 秒超时。 模型偶尔会生成挂住的命令(比如起了个交互式程序、或者网络请求卡死)。没有超时,你的整个 Agent 就跟着一起死。

安全带三:输出截断到 50000 字符。 这条最有味道。记住一个事实:工具的输出会整段进入上下文 。模型随手跑一条 find /,输出可能几十万字符,一条命令就能把上下文撑爆,后面的对话全部崩掉。截断是最粗暴但最有效的保险------至于"怎么聪明地压缩",正是后面"上下文压缩"那篇要解决的问题。

这个内核里的每一条防御性代码,背后都对应着后续的一个完整机制。现在是创可贴,以后是器官。

第三块:主循环 agent_loop------全文最重要的一段

来了,心脏的心脏。先看完整代码,再逐机制拆:

复制代码
def agent_loop(messages: list):
    while True:
        # 1. 把整本账本发给模型
        response = client.messages.create(
            model=MODEL, system=SYSTEM, messages=messages,
            tools=TOOLS, max_tokens=8000,
        )

        # 2. 模型的回复记进账本
        messages.append({"role": "assistant", "content": response.content})

        # 3. 这轮有没有调工具?没有 → 任务完成,退出
        tool_calls = [
            block for block in response.content if block.type == "tool_use"
        ]
        if not tool_calls:
            return

        # 4. 有 → 逐个执行,收集结果
        results = []
        for block in tool_calls:
            print(f"$ {block.input['command']}")
            output = run_bash(block.input["command"])
            print(output[:200])
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": output,
            })

        # 5. 结果塞回账本,回到第 1 步
        messages.append({"role": "user", "content": results})

三十多行,一个 coding agent 的内核就没了。但这段代码里有三个机制,看懂它们,才算看懂 Agent。

机制一:messages 是一本"只追加"的账本

注意第 2 步和第 5 步:模型的回复 append 进去,工具的结果 append 进去,从头到尾只有 append,没有修改,没有删除

messages 这本账本,就是模型的全部记忆。它下一轮做的所有判断------"我刚才干了什么"、"报错是什么"、"接下来该干嘛"------依据全部来自这本账本的历史记录。

这也解释了 Agent 为什么有"上下文越长越聪明、也越贵"的特性:账本越厚,模型知道得越多,但每轮要读的字也越多。记忆、上下文压缩、子代理隔离,往后所有机制玩的都是这本账本。

机制二:tool_use_id 是回执单号

第 4 步里,每个结果都带着 tool_use_id: block.id。为什么?

因为模型一轮可以同时申请跑好几条命令。它发出的每个 tool_use 块都带一个唯一 id,就像银行转账的申请单号;你执行完,结果必须带着同一个单号塞回去,API 靠这个 id 配对"哪张回执对应哪笔申请"。

id 对不上,API 直接报错。这是所有人第一次写 Agent 必踩的坑:辛辛苦苦执行完工具,结果塞回去的时候 id 忘了带或者带错了,前功尽弃。记住:tool_result 必须和 tool_use 一一配对。

机制三:循环什么时候停,代码说了不算,模型说了算

看第 3 步那个 if not tool_calls: return

整个循环没有计数器、没有定时器、没有"最多跑 10 步"的设置。唯一的退出条件:模型这一轮没有申请任何工具。

模型不再要工具 = 模型认为任务做完了 = 循环结束。

这个设计初看朴素,细想是整个架构里最大胆的一笔。传统软件的流程控制权在程序员手里,而这里,"任务什么时候算完成"的定义权交给了模型。Agent 的自由度和边界感,全都浓缩在这一个 if 里。

第四块:命令行入口(10 行,带过)

最后补个壳,让它能在终端里交互:

复制代码
if __name__ == "__main__":
    print("s01: Agent Loop")
    print("Enter a question, press Enter to send. Type q to quit.\n")

    history = []
    while True:
        try:
            query = input("agent >> ")
        except (EOFError, KeyboardInterrupt):
            break
        if query.strip().lower() in ("q", "exit", ""):
            break
        history.append({"role": "user", "content": query})
        agent_loop(history)
        # 打印模型的最终文字回复
        for block in history[-1]["content"]:
            if getattr(block, "type", None) == "text":
                print(block.text)
        print()

注意 history 这个列表在主循环外面------你的每次提问都追加进同一本账本,所以它记得你上一句说了什么,多轮对话是免费自带的。

四块拼完,142 行。保存成 code.py


PART 04:跑起来------亲眼看着它自己干活、自己修错

光看代码没感觉,跑起来才知道这 142 行有多上头。

复制代码
python code.py

出现 agent >> 提示符,开测。

实录一:最小任务,看循环转三圈

输入:

复制代码
帮我在当前目录创建一个 hello.py,内容是打印 Hello, World!,并运行验证

终端上你会看到模型自己开始干活(黄色是它申请的命令):

复制代码
$ echo 'print("Hello, World!")' > hello.py
$ python hello.py
Hello, World!
Done. hello.py created and verified.

拆一下账本里发生了什么:

  • 第 1 圈 :模型申请 echo ... > hello.py 写文件 → 结果"(no output)"回流
  • 第 2 圈 :模型申请 python hello.py 验证 → 结果"Hello, World!"回流
  • 第 3 圈:模型没有再申请任何工具 → 循环退出,打印最终回复

注意一个细节:你只说了"创建",它自己决定多跑一步验证。没有人教它,流程图里也没有这个节点------这就是 agency 从模型权重里自己长出来的样子。

实录二:名场面------跑挂了,它自己修

来个有难度的。输入:

复制代码
写一个 divide.py,计算 10 除以 0 并打印结果,然后运行它

模型会老老实实写文件、运行,然后:

复制代码
$ python divide.py
Traceback (most recent call last):
  File "divide.py", line 1, in <module>
    print(10 / 0)
ZeroDivisionError: division by zero

红色的 Traceback 刷出来。接下来发生的事,是这个内核最接近"生命"的一刻------它没有停下来问你怎么办,而是:

复制代码
$ cat divide.py
$ echo 'try:
    print(10 / 0)
except ZeroDivisionError:
    print("不能除以零")' > divide.py
$ python divide.py
不能除以零
Fixed: added exception handling.

看明白发生了什么吗?

Traceback 不是给你看的,是给模型看的。

stderr 被原样塞进了 tool_result,下一圈循环,模型读到自己的报错,自己定位、自己改、自己再跑。错误信息就是它的眼睛------这就是"观察-行动闭环":观察(读报错)→ 行动(改代码)→ 再观察(看新结果),直到满意为止。

你平时用 Claude Code 时它"改 bug 改到通为止"的体感,底层就是这个循环在转。

实录三:反例------它不是什么都要动手

最后问个不用动手的:

复制代码
1+1 等于几?

模型直接回复"2",从头到尾一个工具都没调,循环转一圈就退出。

呼应机制三:调不调工具、什么时候停,全在模型的一念之间。该动手时动手,不该动手时闭嘴直接答------这个分寸感,同样来自模型,不来自你的代码。

一句严肃的安全提醒

再次强调:这段代码会真实执行模型生成的 shell 命令。黑名单只挡得住四条最狠的。请在一个专门的临时目录里玩,别对着有生产代码、有重要文件的目录跑。像样的权限门禁,是后面要单独拆的机制。


PART 05:为什么只给一个 bash?------"One loop & Bash is all you need"

代码跑通了,留一个最值得琢磨的问题收尾。

你的直觉可能是反对的

看到工具定义那段,你八成想过:就给一个 bash?为什么不给它 20 个精细工具------read_filewrite_filesearch_coderun_pythonlist_dir......每个工具参数结构化、职责单一,听起来工程上漂亮多了。

我之前自己写过一个小 Agent,就是这么干的:挂了 7 个精细工具,查订单、改地址、查物流,每个都单独定义。运行得也挺好。

所以第一次看清 Claude Code 的内核只给一个 bash 时,我是愣了一下的。想明白之后,服了。

bash 不是一把刀,是一台机床

想给它 20 个工具,先算一笔账:

  • 读文件,bash 里是 cat
  • 写文件,是 echo ... > file
  • 搜代码,是 grep -rn
  • 找文件,是 find
  • 跑脚本,是 python xxx.py
  • 组合操作,是管道 |、重定向 >、链式 &&

一个 bash,吞掉了你打算手写的整个工具箱。

而且 shell 的能力空间是组合爆炸的------你永远枚举不完用户会遇到的操作,但任意操作几乎都能用几条 shell 命令现凑出来。专用工具是 20 把固定的刀,bash 是一台能现加工任何刀具的机床。

换来两样更值钱的东西:

一,模型的选择负担最小。 工具列表越短,模型"该用哪个"的决策越干净,选错工具的概率越低。给它 20 个长得差不多的工具,它会在 read_filesearch_code 之间犹豫,然后选错。

二,能力上限是整个 shell 生态。 精细工具的能力上限是你写工具那天的想象力,bash 的上限是几十年积累的全部命令行工具。

当然这不是说专用工具没价值------参数结构化、好审计、行为可控,这些是 bash 给不了的。Claude Code 的选择是:底座用 bash 换最大灵活性,再靠一层层机制把"可控"补回来。 你后面会看到权限系统怎么补安全、专门的读写工具怎么补精度。

先有自由,再上枷锁,顺序不能反。

它现在还很糙------这份"糙"就是后面的目录

最后诚实地列一下这个 142 行内核的缺陷,每个缺陷都对应往后的一篇:

  • 黑名单挡不住 rm -rf ~/重要目录 → 需要权限门禁:工具执行前先过审批
  • 一条 find / 的输出能撑爆上下文,聊久了账本无限变厚 → 需要上下文压缩
  • 长任务会走神,干着干着忘了最初的目标 → 需要 Todo 计划机制
  • 所有中间过程全记在一本账上,查个资料把主上下文弄脏 → 需要子代理隔离
  • 一次只能干一件事,慢命令一跑全卡住 → 需要后台任务
  • 会话关了就全忘 → 需要跨会话记忆

这个模块的路线就此清晰了:循环从第一天写完就再也不变,变的是外面一圈圈长出来的壳------权限、工具分发、Hooks、计划、子代理、上下文压缩、记忆、任务系统、后台任务、定时调度、团队协作、插件、集成、编排、目标闭环。每一篇拆一个器官,拆完你就拥有一个自己完全理解的 Claude Code。

给你的学习建议

别只看。把这 142 行亲手敲一遍(不要复制粘贴),跑通上面三个实录。敲的过程中你大概率会在 tool_use_id 配对上报错一次------那一次报错,比看十篇文章都管用


结尾:把方向盘交出去

回顾一下这篇干了什么:从一个"模型会说不会做"的日常痛点出发,用 142 行 Python 写出了 Claude Code 的心脏------一个 while 循环、一个 bash 工具、一本只追加的账本。

从"人肉循环"到"自动循环",差的从来不是代码量,是你敢不敢把方向盘交出去:决策让给模型,执行留给自己,结果如实回流。

Agent 的强大从来不写在你的编排代码里,它写在模型的权重里。你能做的最好的事,是给它一双手,然后让开。

互动时间 :作业来了------把这篇的 142 行跑通,截一张"模型自己在终端里敲命令"的图发到评论区。另外留个问题聊聊:如果是你来迭代这个内核,会先补权限门禁 ,还是先补上下文压缩?为什么?


下一篇预告:「从0学习 Claude Code」第二篇------工具分发机制 。只有一个 bash 时,模型用 echo 写文件其实经常翻车(引号嵌套、多行内容全是坑)。给它 read / write / edit / glob 几个真正的工具之后:模型会不会一轮同时调好几个?同时调的工具会不会互相踩?工具的 description 到底要怎么写,模型才会"懂事"?下一篇见。

--- END ---

苦猿 · 帮普通人把 AI 学进简历

相关推荐
jump_jump37 分钟前
我让本地 Qwen3.8 27B 搭了一个内网穿透服务
人工智能·llm·ai编程
大锅盖139 分钟前
HarmonyOS 6.1.1 AI字幕新特性深度解析:从AICaptionComponent四大新增字段到K歌社交舞台黑霓虹紫深色架构全链路实战
人工智能·架构·harmonyos
烟锁池塘柳040 分钟前
课程学习(Curriculum Learning, CL):一种模仿人类从易到难学习过程的机器学习训练策略
人工智能·机器学习
麻雀飞吧1 小时前
近期量化工具怎么选,先看你卡在哪一环
人工智能·python
java1234_小锋1 小时前
YOLO26 计算机视觉 - YOLO26 简介 & Hello World项目搭建
人工智能·yolo·计算机视觉·机器视觉·yolo26
阿图灵1 小时前
OpenCV 图像特征与匹配:SIFT 特征检测与 BFMatcher 暴力匹配
图像处理·人工智能·python·opencv·计算机视觉·sift
Mr数据杨1 小时前
商店商品销量预测助力库存优化
人工智能·数据分析·kaggle竞赛
东方佑1 小时前
202 个参数干掉 59 万个参数:ConvLinear 的设计、融合与实战
人工智能