同样是 Claude,同样是 GPT,在一个工具里,它能连着改十几个文件、跑测试、发现错了自己回头修;换一个工具,连把一个函数改个名都做不利索。模型是同一个,差别在模型外面那一圈——harness 和 agent runtime。这两个词经常被混着用,其实是两层。下面用 Windows 的消息机制,把这两层一次讲清楚。
一、两个词,两层
先各给一句话。
Agent runtime,是「跑」的那一层:进程在哪儿起、工具在哪儿执行、文件系统是哪一个、网络能不能出去、沙箱怎么隔离。它回答的是:这个 agent 能做什么、在哪儿做、怎么活着。
Harness,是「决定下一步做什么」的那一层:上下文怎么拼、提示词写什么、工具怎么暴露给模型、模型吐出来的工具调用怎么解析、循环什么时候停、历史太长了怎么压缩、执行前要不要先问一句用户。它回答的是:拿到这一轮输入,接下来到底发生什么。
一句话记住:runtime 提供能力,harness 提供语义。
这两句现在听着还有点飘,我们把它放到一个你熟悉的东西上去看——Windows 的消息机制。
二、三行心跳
Windows 程序的核心是一个循环。主函数里就三行:取一条消息,翻译一下,派发出去。
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
就这三行,是整个 Windows 图形程序的心跳。三十年来,几乎每一个桌面程序,都跑在这三行上面。
而这三行背后,站着三层东西。第一层,操作系统。鼠标动了、键盘按了、定时器到点、窗口该重绘了——这些事件是系统内核产生的,塞进你这个线程的消息队列。队列由系统维护,你碰不到它的内部,只能取。
三、队列 / 循环 / 处理函数
第二层,你的消息循环。取出来之后干什么,由你的代码说了算。派发之前你可以先拦一手:是给对话框的,走 IsDialogMessage;是快捷键,走 TranslateAccelerator。策略在你手里。
第三层,窗口过程(WndProc)。一个巨大的分支判断:WM_PAINT 怎么办,WM_LBUTTONDOWN 怎么办,不认识的消息,扔给 DefWindowProc 兜底。
记住这三层:系统的队列、你的循环、你的处理函数。
四、一一对应
现在把它盖到 agent 上。
操作系统那一层,就是 agent runtime。用户敲进来的一句话、工具返回的结果、子 agent 完成的通知、定时任务到点——全都是事件,都要塞进队列里排队。谁负责产生、投递、隔离?runtime。你不写它,你站在它上面。
你的消息循环加上窗口过程,合起来就是 harness。取到一条事件,翻译成模型看得懂的形式,拼进上下文,调模型;回复是一句话就交给用户,是工具调用就去执行,执行完把结果变成新消息扔回队列,循环继续。
什么时候停?模型说「我做完了」——那就是 agent 世界里的 WM_QUIT。
那模型本身是什么?模型是窗口过程里那个分支判断的决策部分。而它有一个关键性质,恰好也正是窗口过程的性质:无状态。
五、对应一:无状态,和状态外置
窗口过程每次被调用,都不记得上一次发生了什么。那窗口的状态存在哪儿?存在窗口对象里——GWLP_USERDATA 上挂一个指针,或者干脆把整个对象绑上去。
大模型一模一样。每一次调用它都是全新的,不记得三分钟前你们聊过什么。状态在上下文里,harness 每一轮都得把历史、工具结果、文件内容重新捞出来,拼成完整输入喂进去。
所以「上下文工程」这个听起来很新的词,本质上就是当年那句:我该往窗口的用户数据里塞什么。区别只有一个:窗口状态可以无限大,上下文有硬上限。
六、对应二:消息合并,和上下文压缩
这里有个细节,很多人写了十年程序都没留意:WM_PAINT 不是你每次 InvalidateRect 都发一条。系统把所有失效区域累积成一整块,等队列空下来才合成一条给你。WM_TIMER 也一样,来不及处理就合并,绝不堆积。
为什么?因为队列不能无限长。当产生速度超过处理速度,系统必须有损地丢弃和归并,否则程序被自己淹死。
上下文压缩是同一件事的翻版。历史越滚越长,在撑爆窗口之前,harness 必须把前面几十轮有损地压成一段摘要,丢掉细节,保住意图。这不是新发明,是任何「输入速率大于处理容量」的系统都逃不掉的一步——Windows 在八十年代就在干了。
七、对应三:子 agent,和多线程消息队列
每个界面线程,有自己独立的消息队列。跨线程通信就两个函数:SendMessage 是同步的,我发出去就卡在这儿,等你处理完返回;PostMessage 是异步的,我扔进你的队列就走,你什么时候处理我不管。
这不就是今天的子 agent 模型。主 agent 派一个子任务出去:下一步必须拿到结果才能继续,那就同步等;它能在后台慢慢跑、跑完发个通知,我先干别的,那就异步投递。连坑都一样:同步等容易互相死锁,异步回来的通知,你得自己处理时序。
再加一个小的:Windows 的消息钩子(SetWindowsHookEx)和窗口子类化,让你在消息真正到达窗口过程之前插一脚,看一眼、改一改、甚至直接吞掉。现代 harness 的 hook 一模一样:工具执行前拦一道,检查、改写、拒绝。动机都是同一个——不改核心代码,从外面注入策略。
八、为什么非要分两层
既然这两层咬得这么紧,为什么非要分开?答案跟当年那套分层是同一个:可替换性。
方向一,同一个 runtime 上,能跑完全不同的 harness。同一个 Windows,跑记事本和跑 Photoshop,收到的是同一条 WM_LBUTTONDOWN,但一个是把光标挪过去,一个是开始画一笔。语义完全由窗口过程决定。今天不同的编程 agent 跑在同一台机器、同一套沙箱上,表现天差地别,差的就是 harness。
方向二,同一个 harness 能换 runtime。你的窗口过程一行不改,拿到别的兼容层上照跑。同一套 harness 逻辑,可以跑在本地终端、云端沙箱、编辑器插件、CI 流水线上。变的是执行环境,不变的是「下一步该干什么」的判断。
这条分界线的价值就在这儿:一边可以独立换身体,一边可以独立换脑子。
九、一句实话
这条线不是标准,是行业里约定俗成,各家画的位置并不一样。上下文压缩算 harness 还是算 runtime,权限系统靠哪一边——不同产品的答案不同。
还有个经常被混进来的词,agent framework(LangChain 这一类)。它既不是 harness 也不是 runtime,而是帮你搭 harness 的库——就像当年 MFC,帮你把消息循环和窗口过程,包成一个个 On 开头的回调函数。
对照速查
| Windows 消息机制 | Agent | 共同的那个问题 |
|---|---|---|
| 内核 + USER32 产生事件、维护线程消息队列 | agent runtime | 谁产生事件、谁隔离、谁调度 |
GetMessage 循环 + WndProc |
harness | 取事件、翻译、派发、决定下一步 |
WndProc 无状态,状态挂在 GWLP_USERDATA |
模型无状态,状态在上下文 | 状态必须外置 |
WM_PAINT 失效区累积合并、WM_TIMER 不堆积 |
上下文压缩 | 容量有上限时的有损归并 |
每线程一队列;SendMessage 同步 / PostMessage 异步 |
子 agent:阻塞等结果 / 后台跑完通知 | 并发、时序与死锁 |
SetWindowsHookEx、窗口子类化 |
hook:执行前拦截、改写、拒绝 | 不改核心,从外部注入策略 |
WM_QUIT |
模型判定完成,循环退出 | 终止条件由谁给 |
三十年前,我们在 WinMain 里写下那三行消息循环,把一堆硬件中断变成了一个能用的图形界面。
今天写 agent,本质上是在写一个新时代的 WinMain。只不过队列里流的不再是 WM_PAINT,而是用户意图和工具结果;窗口过程里那个分支判断,也不再由你写死,而是交给一个概率模型来填。
搞清楚哪一部分是 runtime、哪一部分是 harness,你就知道——出了问题的时候,该去修哪一层。