从零手写一个 mini-harness——看懂 agent 会干活的底层 1、引言你每天在 Claude Code 里敲一句帮我把这个 bug 修了,它就自己读文件、改代码、跑测试、再改,直到绿。这中间到底发生了什么?把它包成会自己干活的那层东西,叫harness。这篇就用最近从零手写的练手项目My-Agent(Go DeepSeek,刻意不套任何 SDK)把这层东西拆开:它由哪几个零件组成、那个让它转起来的循环长什么样、怎么一步步从能跑长成好用、可演进到企业级。先把窗户纸一句捅破:大模型本身只是个无状态的文本函数——喂它一段文本,它吐一段文本,仅此而已;它不记事、不动手、不会自己干第二步。所谓 agent 能自己读文件、记得住你、连着干十几步,没有一行是模型的魔法,全是你在它外面用普通代码搭的脚手架(harness)。看懂这层脚手架,agent 的神秘感就消失了——这也是全文要带你看到的那个原来如此。全文用一个具体任务当主线,从头演示到尾:你对它说:我叫张三,帮我读一下项目根目录的 README,看完告诉我这项目是干嘛的,顺便记住我是谁。后面每讲一个概念,都回到张三这句话,看它在这一层是怎么被处理的。全文路线—— 先看裸模型缺什么、harness 怎么把缺的补上,再拆零件、钻循环、做到好用,最后聊怎么上手、往后能长成什么。你是重度 AI 编码工具用户,Claude Code、Cursor 闭着眼用。可有人问你一句它底层到底是 agent 还是 harness?那个循环谁在控制?模型怎么就会自己读文件了?——你大概率卡壳。不是不聪明,是这层底座平时被工具糊得严严实实,你从没机会看它一眼。更别扭的是另一条路:想搞懂就去套壳——拿现成的 agent 框架配一配。可那等于换了个壳子用别人的 harness,该不懂的还是不懂,你看到的依然是封装好的 API,不是裸大模型怎么变成会干活的 agent。所以这次反着来:不套壳,从零手写。用几十行 Go 把那个循环、工具、记忆全摊开,亲手让一个只会说话的大模型变成会自己读文件、记得住你是谁的 agent。骨架和 Claude Code 同源,工程量差着十万八千里,但够你彻底看懂它为什么这么设计。要看懂加了什么,得先看清原本缺什么——下面这张图就是裸大模型 API 和一个会干活的 agent 的差距。裸大模型 API 只会文本进文本出;harness 在外面补上循环、工具、记忆,才成了会自己干活的 agent2、harness 到底给大脑补了什么痛点:你直接调一次 DeepSeek 的/chat/completions,把张三那句话发过去,会拿到什么?一段文本——好的张三,但我看不到你的文件,请把 README 内容贴给我。就这么一次,结束了。它不记事(下次再问我是谁它一脸茫然)、不会动手(碰不到你的文件和网络)、不会循环(给完一段话就停,不会读完再想下一步)。裸 API 就是一个只会说话、被锁在玻璃房里的大脑。注意上面那三个不——不记事、不会动手、不会循环。harness 干的事,就是逐个把它们补上:配一本随时翻阅的记事本(记忆),接一双能真正读文件/跑命令的手(工具),套一个让它想一步、做一步、再想的循环。开头那句模型只会说话、其余全是外面搭的脚手架,落到实处就是这三件。Claude Code、Cursor 本质都是把这三样做厚、做稳。举个具体例子:张三那句话进了 harness,会变成这样一段交互——大脑说我要用read_file工具,路径./README.md(它只是开口点单),harness 真的去磁盘把文件读出来、塞回给大脑,大脑这才接着说这项目是一个 Go agent harness……;同时 harness 把用户叫张三记进了记事本。模型负责动脑,harness 负责动手和记事——这就是分工。实际用途:理解了这层,你就能回答开头那个卡壳的问题了,也能自己造一个——给任何一个大模型接上你自己的工具(查数据库、调内部 API),让它替你干活。harness 给只会说话的大脑接上手(工具)、循环(会想下一步)、记事本(记忆)三样东西顺带厘清 agent 和 harness:宏观上agent指这个会自主干活的整体,harness指支撑它的那层脚手架——日常说Claude Code 是个 agent,落到代码上你写的就是它的 harness。本文写的 My-Agent,既是一个 agent,也是一个 mini-harness。3、一个最小 harness 的五个零件痛点:脚手架听着虚,到底要写哪些代码?很多人卡在这——以为要一上来搭个庞大框架,结果还没开始就放弃了。其实最小可跑版就五个零件,加起来几十行。把张三的任务跑通,需要这五样,各司其职:零件干什么在 My-Agent 里会话状态一个消息列表(system / user / assistant / 工具结果),每轮往里追加harness/types.go的Message模型客户端把消息 工具定义 POST 给 DeepSeek,拿回一条回复harness/client.go,裸net/http工具注册表每个工具 定义(名字/参数 schema) 真正执行它的 Go 函数harness/tools.go循环harness 的灵魂:调模型→有工具就执行→塞回→再调,直到给最终答案harness/agent.go记忆按 session 存取历史,跨轮(甚至跨重启)记得住harness/memory.gomemstore/举个具体例子:工具注册表里的一个工具长这样——就是给模型看的说明书加真正干活的函数两半:// read_file:告诉模型有这么个工具、要一个 path 参数, // 以及 harness 真正执行它时跑的 Go 函数。 Tool{ Def: FunctionDef{ Name: read_file, Description: Read a UTF-8 text file and return its content., Parameters: /* JSON Schema: {path: string} */, }, Run: func(args json.RawMessage) (string, error) { // 解析出 path,os.ReadFile 读出来返回 }, }实际用途:看懂这五个零件的边界,你就知道加一个能力该往哪儿塞——加工具改注册表、换记忆改 memstore、调提示词改设置,核心循环一行不用动。这正是它能慢慢长大的根。最小 harness 的五个零件:会话状态、模型客户端、工具注册表、循环、记忆4、那个循环,就是 agent 的心跳这是整个 harness 的灵魂,也是最该看懂的一段。看懂了这十几行,你就看懂了 Claude Code 的本质——剩下的全是工程量(更多工具、更稳的记忆、更细的权限),不是新原理。痛点:大模型一次调用只能说一段话,可张三的任务明明需要好几步——先读文件、再理解、再回答。单次调用怎么变成读完再想下一步的多步干活?答案不在模型里,在外面那个循环。它是什么:一个朴素到家的for循环,每轮干这么几件事:loop: ① 调模型(带上所有工具定义) ② 模型没要工具 → 这就是最终答案,返回 ③ 模型要调工具 → 逐个真执行(harness 的手) ④ 把每个工具结果按 role:tool 塞回上下文 ⑤ 带着新结果再循环,让模型决定下一步 (兜底:撞到 MaxSteps 就停,防跑飞)举个具体例子(张三的任务在这个循环里走了两圈):第 1 圈:模型看到读 README 并总结 可用工具列表,回了一句我要调read_file,path./README.md(finish_reasontool_calls)。循环走到 ③,harness 真的去读了文件,把内容按role:tool塞回消息列表。第 2 圈:带着 README 全文再调一次模型,这回它不要工具了,直接给出这项目是一个从零手写的 Go agent harness……——② 命中,返回最终答案。两圈,一次工具调用,任务完成。模型全程碰不到你的文件,它只会开口点单;真正动手的永远是 harness。回到开头那句话——agent 会连着干好几步这件看似神奇的事,本质就是这个for循环多转了几圈,没有别的魔法。那个循环:调模型→要工具就执行→结果塞回→再调,直到最终答案;撞 MaxSteps 兜底这里藏着一个最容易被忽略、却最关键的分工:tool use 不是模型自己执行了工具,而是模型说要调哪个工具、给什么参数,harness替它执行、再把结果喂回去。模型是点菜的客人,harness 是后厨。想明白这条,你就不会再问模型怎么会读我文件——它不会,是 harness 读的。tool use 的分工:模型只开口点单(说要调哪个工具),harness 才是真正动手执行的那只手5、从能跑到好用,还要补几件事五个零件能跑通张三的任务了,但离好用、企业级还差得远。My-Agent 接着补了几样,每一样都对着一个真实痛点。它们共享同一条设计原则——先把这条总纲说透,后面的可插拔记忆、分层记忆、权限、agent team 就都顺了。一条贯穿始终的设计原则痛点:项目要慢慢做大,最怕的就是加个新功能得动核心代码——改一次,处处回归。比如记忆,今天用本地文件,明天想换 Postgres,要是写死在循环里,换一次伤筋动骨。原则:凡是有、或将来可能有多种实现的能力,一律抽成一个接口、给多个实现、用配置里一个带默认值的开关选用哪个——绝不写死一种再加兜底。加能力 加一个实现 一个选项,核心不动。下面这张图就是这条原则的样子,后面记忆、执行后端全是它的实例。第一设计原则:同一种能力 一个接口 多个实现 一个配置开关,加能力不动核心可插拔记忆,换后端不动核心痛点:记忆存哪,不同人需求天差地别——你只想试跑,纯内存就行;想持久又不想装数据库,落本地文件最省事;团队上生产,得用 Postgres。要是框架替你写死一种,总有人不爽。怎么做:记忆就是一个三方法的Memory接口(Append/Load/Reset),底下挂五种实现:filewiki(本地文件,默认)、postgres、redis、inmem、none。启动时一个backend开关选哪个,首次启动还会问你一句记忆存哪,而不是默默替你拍板。举个例子:张三那句记住我是谁,在 filewiki 后端下就落成本地一个.jsonl文件,一行一条消息;换成 Postgres,就变成一张表里的几行——循环和工具完全不知道、也不关心记忆存哪,因为它们只认Memory接口。分层记忆,让它记得前天和昨天痛点:光把原始对话堆着,有两个毛病——越攒越大撑爆上下文窗口;而且前天聊过的事埋在几千条消息里,它根本翻不到。你周一告诉它我在做 My-Agent,周三再聊它早忘了。怎么做:在后端之上再套一层分层记忆。对话按天归档,每天对话结束滚动出一份≤2000 字的当天摘要;下次对话时,按新近度权重把近 30 天的每日摘要 当天原始对话拼进上下文。默认保留 30 天,超期自动清理。短期(当天原始)和长期(每日摘要)共用同一个 Memory 接口,跟用哪个后端无关。举个例子:周一张三聊了一下午 My-Agent,当晚这段被总结成一句用户张三,在做 My-Agent 的 Go 项目,偏好简洁代码存进digest;周三他开口问我上次说在忙啥来着?,harness 把这条摘要(连同更早几天的)按时间近的优先拼回去,它就答得出来——像它一直记得。分层记忆:对话按天归档→每天滚动出≤2000字摘要→按新近度权重召回近30天摘要当天原始操作权限,别让它真把你的 .ssh 读出来痛点:你给了它read_file工具,它就能读任何文件。万一模型自作主张(或被一段恶意提示带跑),来一句我读一下~/.ssh/id_rsa看看——你的私钥就被读出来塞进上下文了。能力越大,越得有闸。怎么做:工具执行前过一道权限闸。每个工具标个敏感度(read/write/exec),配置里给每类一个动作:allow(放行)/ask(执行前问一次)/deny(禁止)。另外不管策略怎么配,read_file对像密钥的路径(.ssh、*.pem、config.local.json……)一律硬拒。举个例子:张三的任务里read_file ./README.md是普通读,放行;可一旦路径变成~/.ssh/id_rsa,直接被硬拒,返回一句refused: looks like a secret file喂回模型,任务继续但私钥纹丝不动。agent team,主 agent 派小弟各司其职痛点:一个复杂任务塞给单个 agent,它容易既要又要顾头不顾尾;有些子任务还希望换个专门的人设来干(代码审查 vs 写诗),甚至想把不可信的活关进隔离环境跑。怎么做:加一个spawn_subagent(role, task)工具,主 agent 能把子任务派给一个角色化的子 agent,自己拿回结果再继续。子 agent 在哪儿跑由Runner接口决定——local(本进程,默认)或docker(每个子 agent 起一个一次性容器隔离),一个mode开关切换。这又是上面那条设计原则的实例。举个例子:你让主 agent派个数据库专家解释下索引,它就spawn_subagent(数据库专家, 一句话解释数据库索引);local模式下子 agent 在本进程跑完把答案带回,docker模式下它在一个容器里跑、结果从 stdout 带回——主 agent 全程只管派活和收结果。agent team:主 agent 用 spawn_subagent 派角色化子 agent,Runner 决定它在本进程还是容器里跑6、推荐学习路线,照着梯子爬别想着一口吃成 Claude Code。按这个顺序,每一阶都能跑通、有正反馈:先把那个循环跑起来(最重要的一步)。一个模型客户端 一个for循环 一个最简单的工具(比如返回当前时间的now),让模型学会点单→你执行→塞回→再问。看懂这 50 行,你就过了最大的坎。加一个真正动手的工具:read_file。体会模型只点单、harness 才动手的分工,顺便撞上要不要让它随便读文件这个安全问题。加跨轮记忆:先用一个内存里的map实现Memory接口,让它记得住张三;再换成落本地文件,体会接口不变、实现可换。加配置与权限:把系统提示词、模型名抽到配置文件,密钥单独放;给工具加上allow/ask/deny的闸。再上分层记忆、子 agent、容器隔离:这些都是在前四阶的接口上加实现,不碰核心。推荐学习路线:循环→加工具→加记忆→加配置与权限→分层记忆/子 agent,一阶一个可跑的正反馈7、后续的扩展方向My-Agent 还远没完工,但好处是——下面每一条都是加一个实现 一个开关,核心不用动。按三个方向拆:接更多模型和工具模型 provider 可插拔—— 今天是 DeepSeek,抽一个Provider接口就能挂上别的模型,一个开关切换。MCP 接入—— 实现 MCP 客户端,自动发现并注册外部工具,瞬间接上一大票现成能力。把交互做顺滑流式输出—— 让答案一个字一个字往外蹦,而不是憋完才给。上下文压缩—— 长对话快撑爆窗口时,自动把早期内容摘要替换,聊一整天不断片。更聪明也更可控语义记忆(RAG)—— 给记忆加一个 pgvector / 向量库实现,做到按意思召回而不只是按时间。可观测—— 每一步的 token、成本、耗时都记下来,方便排查和省钱。后续扩展方向:模型 provider、流式、上下文压缩、语义记忆、MCP、可观测——每条都是加实现开关8、回到开头,你换来了什么文章开头那个让你卡壳的问题——它底层到底是 agent 还是 harness?那个循环谁在控制?模型怎么就会自己读文件了?——读到这儿,你应该能一句句答上来了。这就是亲手写一遍的价值,三件实打实的东西:看懂了底座:你不再把 Claude Code 当黑盒。那个循环、tool use 的分工、记忆怎么跨轮——全是你亲手写过的;再被问它底层是什么,你能画给对方看。能改、能扩、不怕大:因为接口 多实现 开关这条原则,换记忆、加工具、接新模型都是加一个实现 一个选项,核心稳如老狗。一个企业级雏形:配置/密钥分离、本地可视化后台、权限闸、子 agent 隔离——这些不是玩具特性,是真往生产方向铺的地基。自己写一遍换来的三样东西:看懂底座、能改能扩不怕大、一个企业级雏形说到底,大模型那一下文本进文本出始终没变;变魔术的从来不是模型,是你在外面那几十行循环、工具、记忆。自己把这层脚手架写一遍,你对AI 怎么变成会干活的 agent的理解,会从听说过变成我造过。这中间的差距,值得你花一个周末。