这是《Agent全栈开发》的第 1 篇。整个系列以 catbuddy (个人项目:一个本地优先的 Agent,约 3.6 万行 TypeScript,基于mororepo构建桌面端,web端,服务端Gateway)
由浅入深拆解一个「能干活的 AI」背后那层工程系统。
这一篇先把最基础的问题讲清楚:模型已经那么强了,为什么直接调 API 却做不出一个像样的 Agent?中间到底缺了什么?最后给你一张全景地图,后面拆任何细节你都能对号入座。

一个让我困惑了很久的反差
我第一次用 Claude Code 改代码的时候,是有点震撼的。
我说「把这个组件的状态管理从 useState 迁移到 useReducer」,它真的去读了文件、理解了现有结构、改了代码、还顺手把引用它的地方一起更新了。整个过程它自己读、自己写、自己验证,连续干了七八轮。
然后我回到自己的项目里,用同一家的模型、同一个 API Key,想复刻这个体验。结果呢?我调一次 API,它回我一大段「你可以这样做......」的文字。我把它的回答贴回去,它又回一段。它知道 该怎么做,但它什么都做不了------它读不了我的文件,跑不了我的命令,记不住三句话之前我们聊到哪了。
同样的模型,差别怎么这么大?
答案是:Claude Code 不只是「调了一下模型」。模型外面包了一层东西,正是这层东西,把一个「会聊天的模型」变成了「会干活的智能体」。这层东西,业界叫它 Harness。
1. 你调的 LLM API ,本质是个无状态的文本补全函数
要理解 harness 补了什么,得先看清楚「裸模型」到底是什么。
抛开所有包装,一次 LLM API 调用的本质,就是一个纯函数 :输出文本 = LLM(输入文本)。你给它一段文本(系统提示、历史对话、你的问题),它根据训练学到的概率分布,吐出最可能的下一段文本。就这么简单。这个函数有三个「天生的残疾」,恰恰是它没法独立干活的根因:

残疾一:它没有记忆。 这个函数是无状态的,你这次调用和上次之间,它没有任何「记得」。所谓「多轮对话」全是假象------其实是你每次都把之前所有的对话重新塞进输入里,它才「看起来」记得。一旦你不塞,它就是个失忆的人。
残疾二:它动不了手。 它的输出永远只是文本。它可以在文本里说「我要读取 src/index.ts」,但它没有手去真的读那个文件。它能描述动作,但不能执行动作。
残疾三:它不会自我推进。 你问一句,它答一句,然后这次调用就结束了。它不会主动说「我先读个文件,看完再决定下一步」------因为读文件这个动作它做不到(残疾二),而且一次调用结束后它就「死」了(残疾一)。
一个真正的编程任务长什么样?「帮我修复这个 bug」------这需要:先看报错、再读相关文件、定位问题、改代码、跑测试、看结果、可能再改一轮。这是一个需要反复行动、观察、再决策的过程。而裸 LLM 只能做其中「决策」那一小步,剩下的行动、观察、推进,它全做不了。
2. 从「补全一段话」到「完成一件事」
那怎么办?把那三个残疾一个个补上:
-
它没记忆 → 我们在外面存住历史,每次调用时喂给它;
-
它动不了手 → 我们给它一套工具,它在文本里说「我要调用 read_file」,我们就真的去读,再把结果喂回去;
-
它不会自我推进 → 我们用一个循环包住它:调一次模型,看它想干嘛,干完把结果给它,再调一次,直到它说「我搞定了」。
把这三件事做出来,你就得到了一个最朴素的 Agent,骨架长这样:

看到那个从 Exec 绕回 Call 的环 了吗?这就是整个 Agent 的灵魂------业界叫它 Agent Loop(智能体循环) 。它把「一次性的文本补全」变成了「持续的行动---观察---再决策」。模型负责思考,循环负责让思考能落地、能继续。
这个把「裸模型」包起来、让它能持续完成任务的整套工程系统,就是 Harness。
3. 一句话定义:harness 是发动机外面的 底盘
如果一定要下个定义:
Harness 是包裹在 大模型 外面、把「一次文本补全」转化为「持续完成任务的 智能体 行为」的那一整套工程系统。
我最喜欢的类比是汽车:
| 汽车 | AI Agent |
|---|---|
| 发动机 | 大模型(提供动力/智能) |
| 底盘、变速箱、传动 | Agent Loop(把动力传导成前进) |
| 方向盘、油门、刹车 | 工具系统、中断控制(让它能操作、能停) |
| 仪表盘、后视镜 | 上下文工程(决定司机能看到什么) |
| 行车记录仪 | 记忆系统(记住走过的路) |
| 安全气囊、ABS | 可靠性工程(出事时不至于散架) |
发动机再强,没有底盘、方向盘和刹车,它也只是一台在原地轰鸣的机器,去不了任何地方。模型决定了 智能体 的上限,harness 决定了它能不能真的开上路。
4. 一段代码看清差别
光说概念有点虚,上代码。先看「裸调用」------这是大多数人第一次接触 LLM API 时写的东西:
php
// 裸调用:一问一答,到此为止
const res = await llm.chat({
messages: [{ role: "user", content: "修复 src/app.ts 里的类型错误" }],
});
console.log(res.content); // → "你可以打开 app.ts,找到第 X 行......"(然后呢?没有然后了)
它只会告诉你怎么做。再看套了 harness 之后(极度简化,但骨架就是这样):
php
// Harness:循环 + 工具,直到任务完成
const history = [{ role: "user", content: "修复 src/app.ts 里的类型错误" }];
while (true) {
const res = await llm.chat({ messages: history, tools: TOOLS }); // 带着工具说明
history.push(res.message);
if (!res.toolCalls) break; // 模型说「完成了」→ 退出循环
for (const call of res.toolCalls) {
const result = await runTool(call); // ⭐ 真的去读文件 / 改代码 / 跑命令
history.push({ role: "tool", content: result });// 把结果喂回去,继续下一轮
}
}
差别就在那个 while 循环和 runTool 上。多了这十来行,模型从「嘴上谈兵」变成了「真的去改」。当然,真实的 harness 远不止这十行------catbuddy 的核心循环加上各种治理逻辑有上千行。但内核就是这个样子。下一篇我们就亲手把这个内核写出来跑通。
5. 案例项目:一个「代码不出本地」的 harness
为什么这个系列用 catbuddy 当案例,而不是直接讲 Claude Code?因为它有一个很硬的约束,逼出了一套有意思的设计取舍。
catbuddy 诞生的原点是一个真实场景:有人因为代码含业务逻辑,被公司安全部门禁用了所有云端 AI 工具------想用 AI 辅助编程,但源码 不能离开本地。市面上的工具分三类,没有一种同时满足「最强推理 + 代码不出机器」:
| 类型 | 代表 | 问题 |
|---|---|---|
| 纯云端 | 网页版助手 | 代码要上传,合规过不了 |
| IDE 插件 | Copilot/Cursor | 推理服务器仍在云端 |
| 纯本地模型 | Ollama 跑小模型 | 模型太弱,干不了复杂活 |
catbuddy 的选择是第四条路:让最强的云端模型做推理,但 Agent 进程跑在本地。你用自己的 API Key 直连 Anthropic/OpenAI,但读文件、改代码、跑命令这些动作全在你机器上发生,源码从不经过任何第三方服务器。这个「本地优先」的约束,会在后面很多设计里反复冒头------为什么 Gateway 只是个「不碰文件的邮局」、为什么存储是本地 JSONL 文件、为什么工作区要做沙箱隔离------记住它,很多取舍就顺理成章了。
6. 全景地图:harness 的六大器官
讲了这么多,该给你一张能对照的全局图了。学复杂系统最难受的不是某个点看不懂,而是不知道这个点在整体里站哪。所以下面这张图你可以收藏起来,当作读后面文章时的「导航页」。
catbuddy 的 harness 由六大器官组成,外面再套一层产品化外壳。我们让一条真实消息「活」起来,看它依次惊动了哪些器官:

抓住一个主轴就行:用户消息进来 → 交给心脏(AgentLoop)→ 心脏在每一轮里,调度眼睛拼上下文、调度手脚执行工具、隔着韧性层去问 大模型 → 结果再出去。 六大器官各管一摊,对应后面的文章:
| 器官 | 职责 | 真实模块 | 在第几篇 |
|---|---|---|---|
| ❤️ 心脏 | 驱动「问→做→再问」的核心循环 | loop.ts / runner.ts |
02 · 03 |
| 🖐️ 手脚 | 让模型能真的读写文件、跑命令、接外部工具 | tools/ |
04 · 05 |
| 👁️ 眼睛 | 决定模型每轮「能看到什么、看多少」 | context/ |
06 |
| 💾 记忆 | 跨会话记得住,把对话提炼成长期记忆 | memory.ts / dream.ts |
07 |
| 🛡️ 韧性 | 在真实世界的混乱中活下去 | providers/ / hook.ts |
08 · 09 |
| 🚀 进阶 | 子代理并发、自省、让 AI 画图 | subagent.ts / self.ts |
10 · 11 |
这套分层背后有一条很硬的设计原则------每个器官只对它的上下游负责,互相不知道对方的内部细节 。比如 AgentRunner 拿到的永远是同一个 LLMProvider 接口,它根本不知道自己调的是 Claude 还是 DeepSeek、是不是已经故障转移到备选模型了。所以加一家新 provider,Runner 一行都不用改。这就是为什么这套 harness 能一边扛生产、一边持续加功能------改动被锁在单个器官内部,不会牵一发而动全身。这条主线会贯穿整个系列。
7. 三个常见误解,顺手澄清
误解一:「harness 不就是写个好 prompt 吗?」 不是。prompt 是喂给模型的输入,是 harness 的一部分 (属于上下文工程)。但 harness 还包括循环、工具执行、错误恢复这些模型之外的代码逻辑。再好的 prompt 也没法让模型自己去读文件。
误解二:「用 LangChain / 某框架不就有现成的吗?」 那些框架帮你封装了一部分,但「你的 Agent 在什么场景下该怎么管上下文、怎么降级、给哪些工具」这些决策,框架替不了你。框架是材料,harness 是你用材料盖的房子。这个系列讲的就是怎么盖。
误解三:「模型够强,harness 就不重要了。」 恰恰相反。模型越强,你越想把更复杂、更长、更高风险的任务交给它------而任务越复杂,对循环稳定性、上下文管理、错误恢复的要求就越高。模型变强,harness 不是变得不重要,而是承接的责任更重了。
这篇讲了什么?
裸 LLM 是个无状态的文本补全函数,有三个天生残疾:没记忆、动不了手、不会自我推进。所以它能聊天,但独立干不了活。
Harness 就是补上这三个残疾的工程系统------用「存历史」补记忆、用「工具」补手脚、用「循环」补自我推进。它的内核是 Agent Loop。模型是发动机,harness 是底盘。
catbuddy 的 harness 由六大器官 组成(心脏/手脚/眼睛/记忆/韧性/进阶),灵魂是解耦------每个器官只认接口、不认实现。这张全景地图请收藏,是后面所有文章的导航页。
下一篇 :概念聊够了,该动手了。下一篇我们抛开 catbuddy 的全部复杂度,用大约 50 行代码从零手写一个能调工具的最小 harness,让你亲眼看到那颗「心脏」是怎么自己跳起来的------然后你就知道,catbuddy 那上千行循环到底在这 50 行之上加了些什么。