01|什么是 Agent Harness?为什么 LLM 裸用跑不动

这是《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 不是变得不重要,而是承接的责任更重了

这篇讲了什么?

  1. LLM 是个无状态的文本补全函数,有三个天生残疾:没记忆、动不了手、不会自我推进。所以它能聊天,但独立干不了活。

  2. Harness 就是补上这三个残疾的工程系统------用「存历史」补记忆、用「工具」补手脚、用「循环」补自我推进。它的内核是 Agent Loop。模型是发动机,harness 是底盘。

  3. catbuddy 的 harness 由六大器官 组成(心脏/手脚/眼睛/记忆/韧性/进阶),灵魂是解耦------每个器官只认接口、不认实现。这张全景地图请收藏,是后面所有文章的导航页。

下一篇 :概念聊够了,该动手了。下一篇我们抛开 catbuddy 的全部复杂度,用大约 50 行代码从零手写一个能调工具的最小 harness,让你亲眼看到那颗「心脏」是怎么自己跳起来的------然后你就知道,catbuddy 那上千行循环到底在这 50 行之上加了些什么。

相关推荐
浪遏1 小时前
05|手脚②:MCP 接外部生态,Skill 教 Agent 套路
ai编程
浪遏1 小时前
03|心脏:两个循环 + 三层命令路由
ai编程
浪遏1 小时前
09|不改核心代码,只插 Hook:Agent 生命周期扩展
ai编程
浪遏1 小时前
06|眼睛:LLM 的「视野」怎么拼出来,又怎么不爆
ai编程
浪遏1 小时前
04|手脚①:工具的注册、调度与文件安全边界
ai编程
浪遏1 小时前
07|记忆:从 JSONL 持久化到 Dream 后台学习
ai编程
浪遏7 小时前
08|韧性:一套接口接三家 API —— LLMProvider
ai编程
chaors8 小时前
DeepResearchSystem 0x06:LLM as Judge
llm·agent·ai编程
浪遏9 小时前
02|50 行跑通一个 Agent Loop:harness 的最小内核
ai编程