
Anthropic 公开过一组对比:同一颗 Claude Opus 4.5,放进 Claude Code 里跑 benchmark 是 78 分;换一个开源 agent 框架,42 分。
同样的模型,同样的题,差 36 分。
模型一模一样。差掉的这 36 分,全在"循环外面那层工程"——行话叫 harness。
这篇文章只想说服你一件事:agent 没有魔法,它的本质是一个 50 行的循环;真正值钱、真正拉开差距的,是循环外面那层。 而搞懂那层最快的办法,不是去啃源码,是亲手搓一个。今天就把那 50 行的骨架写出来——写完你就站在了能看懂那 36 分的位置上。
先把那个循环写出来
先分清楚 agent 和聊天机器人的区别:聊天机器人是你问一句它答一句;agent 是你给一个目标,它自己决定先干什么、再干什么,直到走到终点。
你跟它说"看看当前目录有什么,挑一个文件总结一下",它自己决定先 ls、再 cat、再读、最后总结。决定下一步干什么的是模型,不是你。
实现这件事,要先纠正一个几乎所有人都搞错的事实:
模型从头到尾,没有执行任何代码的能力。它能做的只有一件事——说话。
那它怎么"跑命令"?靠分工。你给模型的不是一个函数,是一份函数的说明书:有个叫 run_bash 的东西,接受一个字符串参数,能在机器上跑命令。模型在回复里用结构化的方式说"我想调 run_bash,参数是 ls"——然后你的代码真的去执行,再把结果贴回对话。
写成一个公式:
agent = while (没完成) { 模型说 → 你的代码做 → 结果喂回 }
落成 TypeScript,核心就这一段(完整版在仓库 day1 标签):
const messages = [{ role: "user", content: task }];
for (let round = 1; round <= 10; round++) {
const res = await client.chat.completions.create({
model: "deepseek-chat", messages, tools,
});
const msg = res.choices[0].message;
messages.push(msg);
// 不调工具了 = 模型认为干完了,这就是终态
if (!msg.tool_calls?.length) { console.log(msg.content); return; }
// 替它执行,把结果以 role:"tool" 塞回历史,进下一轮
for (const call of msg.tool_calls) {
const args = JSON.parse(call.function.arguments);
messages.push({
role: "tool", tool_call_id: call.id,
content: runBash(args.command).slice(0, 4000),
});
}
}
逐块看就三件事:messages 数组是对话历史,也是模型的全部记忆——它本身不记得任何事,每次被调用都重读一遍;回复里有tool_calls 就执行、喂回、进下一轮;没有就说明它认为任务完成了,打印总结,结束。
记住这句,后面 9 天所有设计都建在它上面:模型负责想,你的代码负责干。
这 50 行里藏了三个决定
一、本质是循环,值钱的是循环外面那层。
回到开头的 78 和 42。接下来 9 天我们要做的所有事——重试、权限、上下文压缩、子 agent、回归测试——没有一件改动这个循环本身,全是往它外面加东西。骨架始终是五根:loop(循环)+ tool(工具)+ context(上下文)+ sub-agent(分身)+ eval(考试)。任何 agent 产品,都是这五件套的排列组合。搞懂这五样,你看所有 AI 编码工具都是透明的。
二、命令失败不抛异常,把错误喂回给模型。
注意上面 runBash 的处理:命令失败时返回的是一段错误文字,不是 throw。传统软件的逻辑是"出错就崩,人来修";AI 软件的逻辑是"出错就恢复"。模型看到 Command failed: ...,会自己换个办法重试——这是它训练出来的本能。
不信你试:让它"用 pythonn 打印 hello"(故意拼错命令),它下一轮自己就改成了 python。你要做的不是防止它出错,是保证出错的信息能回到对话里。

三、循环必须有上限。
那个 round <= 10 不是随手写的。没有这个上限会怎样?Claude Code 有过真实事故:某段自动重试逻辑缺了一个保险丝,单个会话连续失败重试了 3272 次——每一次都是真金白银的 API 调用。
所以记一条铁律:任何会"自动继续"的行为,都必须有"连续 N 次就停"的保险丝。 今天先用最笨的 for 循环兜住,第 4 天我们把它升级成正经的熔断器。

你一定会踩的坑
API key 进了 git。 这是为什么第一步就要写 .gitignore,而且必须在第一次 commit 之前。公开仓库里的 key,泄露后的存活时间是以分钟计的——扫库机器人几分钟就抓走盗刷。万一真泄露:去 DeepSeek 后台删掉重建,光删 commit 没用,历史里还在。
工具结果不截断。 让它 cat 一个 10MB 的日志,整个文件原样进对话历史,下一次调用直接超长报错;或者更糟,不报错,token 全烧出去了。所以代码里有那个 .slice(0, 4000)。粗暴,但"先有上限,再谈优雅"。
最 meta 的一点
这个项目本身,是"人定方向、AI 写大部分代码"搓出来的——我负责架构和每个设计决策,AI 负责敲字,教程里每个"坑"都是这过程里真踩到的。
这就有意思了:用 AI 搓一个类 Claude Code。 搞懂了 harness 骨架的人去指挥 AI,和不懂的人,产出完全是两个世界。这正是这门课想带你到的地方——不只看懂 agent,还能指挥 AI 给你搓一个。
顺便交个底:整个教程从开发到测试,API 总花费不到 5 块钱(用的 DeepSeek,接口和 OpenAI 完全兼容,代码换个 baseURL 就能跑别家)。学这件事的试错成本,低到一顿早饭钱。
现在就能做的下一步
5 分钟能跑起来:
Node 20.6+,一个 DeepSeek key(充 10 块够你跑完整个教程还有富余); 空目录里 npm i openai,装上 tsx; 把上面那个循环抄进去,npx tsx --env-file=.env src/raw-loop.ts 跑。
完整 50 行在仓库 github.com/ftfw0621/mini-agent,切到 tag day1 就是这一章的全部代码。这个仓库的 commit 历史就是课程目录——git checkout day1 看第一章,day2 看第二章,一行 commit 就是一天。

写在最后
这是「从零手搓 Claude Code」系列第 1 篇。一天一章,每章一个设计决定。
下一篇(第 2 篇)给它装上真正的手:read_file / write_file / edit_file / search 四个工具,让它直接改你的代码。到时候你会撞见一个反直觉的事实——工具好不好用,一半在代码,另一半在你给模型写的那份"说明书"(Claude Code 光一个 Bash 工具的说明书,就写了几千字)。
后面还会一篇篇拆下去:一个会话怎么连烧 3272 次、AI 怎么"装失忆"、为什么提示词里不能写日期、deny 永远赢的权限边界……全部代码 + 35 章教程免费开源,跟着 tag 一天一章。
持续更新,关注「老李是谁」别错过下一章;觉得有用,给仓库点个 star ⭐——这是我一天一章更下去的最大动力。
最后说句实在的:这门课的全部意义就是不调包(不是 LangChain 三行跑个 demo),自己把骨架搓出来。如果你身边有人还在喊"AI 要取代程序员",把这篇转给他——能指挥 AI 把 Claude Code 搓出来的人,和只会喊口号的人,看到的根本不是同一个世界。
夜雨聆风