读Crush源码-AGI时代的LLM操作系统 读 Crush 源码一个好用的 AI 编程助手背后到底套着多少层 Harness我们这一年代写代码的人每天都在和AI 编程助手打交道。但用得多了一个疑问会自然冒出来同样是接 GPT/Claude 的 API为什么有的工具能把一个复杂重构任务从头跑完有的跑两步就卡死、跑偏、或者原地转圈模型明明是同一个模型。带着这个疑问我翻了 Charmbracelet 开源的Crush一个终端里的 AI 编程助手类似 Claude Code 的开源对位产品的源码。读完最大的感受是一个能用的 agentic 产品其价值有九成不在模型而在那套套在模型外面的 harness——执行外壳。本文聊聊我从 Crush 里读到的、关于 harness 的那些工程取舍以及它带给我自己的一些思考。一、先对齐一下什么是 Harness如果你写过一点 LLM 应用多半见过最朴素的 agent 循环whileTrue:respllm.chat(messages,toolstools)ifnotresp.tool_calls:breakforcallinresp.tool_calls:resultrun_tool(call)messages.append(result)这就是一个 harness 的雏形——它决定了模型说的话怎么变成动作、动作的结果怎么回到模型。但这个十几行的循环离生产可用差了十万八千里。一旦模型决定连续改二十个文件、一旦某个工具卡住三十秒、一旦模型陷入读同一个文件、报同一个错的死循环这个循环就会把整个会话带崩。Crush 的internal/agent/agent.go里这个核心循环围绕agent.Stream连同它外围的调度、权限、hook、防循环、上下文压缩、取消机制撑起了几千行代码。这几千行就是 harness 的本体。下面我挑几个让我印象最深的设计讲讲。二、Harness 的第一性原理把不可信的执行者关进笼子读完 Crush 我最大的体会是harness 的设计哲学可以用一句话概括永远假设模型会犯错并保证每个错误都有兜底。这听起来像废话但落实在代码里是处处可见的偏执。举几个例子1. 权限是 Harness 的一等公民Crush 里有一个独立的internal/permission包。模型每次想跑一个工具bash、edit、write……都要先过 permission 这道闸。它的设计很讲究按 (session, tool, action, path) 四元组记忆授权。用户在某个会话里允许了编辑这个文件后续对同一文件的编辑就不再弹窗。这个粒度卡得很准——既不烦人又不会一次授权全局放行。Hook 可以预授权。WithHookApproval把一个 toolCallID 通过 context 传下去permission 看到这个标记就直接放行。这意味着用户写的 PreToolUse hook 可以成为一道自动审批策略——比如凡是只读操作一律放行。这其实是个很优雅的扩展点把什么操作安全这件事交给用户用 shell 脚本自己定义。--yolo模式。一键跳过所有权限。Crush 在 README 里反复用 “Be very, very careful” 警告。这是一个很诚实的取舍harness 可以默认严格但必须给信任环境的用户一条别烦我的快车道。我自己之前做 agent 时权限是事后才加上去的加得七零八落。看完 Crush 我意识到权限应该是 harness 的骨架之一从第一天就内建而不是出事了再打补丁。2. Hook用户能在 Harness 里插桩的中间件hooked_tool.go是个典型的装饰器decorator模式实现。每个内置工具被一层hookedTool包起来在真正执行前先跑用户配置的 shell 脚本PreToolUse hook。这个 hook 有三种决策能力deny阻止这次调用把错误塞回给模型让它换个思路。halt直接终结整个 turn不只是阻止这次工具。用于事态严重立刻停手。改写输入UpdatedInputhook 可以修改工具的参数。比如模型想rm -rf /tmphook 可以把它改成rm -rf /tmp/specific_dir。这个设计让我眼前一亮。它本质上是把 harness 的控制权部分让渡给用户——你不需要改 Crush 的源码写个 shell 脚本就能干预模型每一次动作。这比那种只能在配置文件里开关几个布尔值的产品高明得多。一个细节sub-agent 不会再触发 hookisSubAgent时直接返回原工具。这避免了主 agent 调用子 agent 工具时已经过了一次 hook子 agent 内部每个工具再过一遍的双重拦截。这种边界处理是好 harness 和糙 harness 的分水岭。3. 循环检测给原地打转的模型踩刹车这是我觉得整个项目最巧妙的设计之一在loop_detection.go里全部代码不到 100 行const(loopDetectionWindowSize10// 看最近 10 步loopDetectionMaxRepeats5// 同一签名出现超过 5 次)它的做法是给每一个工具调用 工具结果的组合算一个 SHA256 签名工具名、输入参数、输出结果三者拼接哈希。在最近的 10 步窗口里如果同一个签名出现超过 5 次就判定为陷入循环强行中断。为什么这个设计好它不依赖模型自报家门。很多 agent 让模型自己输出我在第几步但模型不可靠。Crush 用的是客观的输入输出指纹模型骗不了。签名包含输出。这点容易被忽略但很关键。如果只哈希工具名输入那模型每次读同一个文件、文件内容因为模型自己改了而变化签名就不一样检测就失效。把输出也纳入哈希意味着只有完全相同的动作产生完全相同的结果才会被算作重复——这才是真正的死循环。窗口 阈值的组合。10 步窗口避免了对长任务的误判5 次阈值给了模型一定的重试容错。这俩参数显然是调过的。我自己见过太多 agent 因为模型陷入读文件→报错→换个姿势读同一个文件→又报错的怪圈而把 context 烧光的。一个 100 行的循环检测能省下真金白银的 token 和用户的耐心。三、Harness 的第二性原理让模型持续可用地工作光防错还不够harness 还得保证模型能在长任务里持续工作。这涉及几个更基础设施层面的问题。1. 上下文压缩长对话的续命术任何 agentic 产品都绕不开 context window 的物理上限。模型改一个项目读几十个文件、跑几十次测试上下文很容易撑爆。Crush 在SessionAgent里有自动摘要机制——当 token 数逼近阈值调用一个小模型把历史对话压缩成摘要腾出空间继续干。这其实是 harness 里最容易被低估的难点。压缩做得粗暴会丢失关键的我刚才改过哪个文件、用户说过哪个偏好做得太细又频繁触发、拖慢响应。这是一个没有银弹、只能靠真实任务反复调参的工程问题。2. 会话级的串行化并发不是越多越好SessionAgent里有一把dispatchMu锁和一个请求队列。同一个 session 内LLM 请求和工具执行是严格串行的——如果上一个任务还在跑新来的 prompt 会被排队而不是并发插入。这乍看反直觉并发不是更快吗但细想非常合理。Agent 的状态本质上是对话历史 待执行计划这是个强状态机。两个 prompt 并发改同一个文件、并发修改同一个会话上下文结果几乎一定是灾难。宁可排队也不要在 agent 状态机上玩并发——这是 harness 区别于普通 Web 服务的重要特征。Web 服务无状态可以并发agent 有状态必须串行。3. 取消机制让用户能随时喊停agent.Stream跑起来之后模型可能要连续执行十几个工具调用每个都可能耗时。如果用户中途想停“诶方向不对”harness 必须能立刻中断而且要中断得干净——不能留下半截写坏的文件、不能让 LLM 请求继续烧 token。Crush 用context.Context贯穿整个调用链来实现取消并且在排队逻辑里做了精细处理一个被取消的 queued prompt 不会被执行但它的 RunID 仍会收到一个已取消的 RunComplete 通知避免调用方一直 hang 住。这种取消也要通知到等待者的细节是分布式系统里才常见的讲究放在 agent harness 里说明作者很懂。四、一些超出预期的设计读完核心模块还有几个设计让我觉得这帮人是真的在拿这个工具干活而不是做 demo1. 工具自描述.go配.md每个内置工具都是一对文件bash.go实现bash.md给 LLM 看的说明文档。LLM 看到的工具描述不是写死在代码里的字符串而是一份独立的 markdown。这意味着改工具的 prompt 不用碰逻辑代码改逻辑代码也不用怕影响 prompt。这种高内聚、自包含的组织方式对维护一个几十个工具的项目来说是刚需。2. 配置即服务而不是全局变量Config is a Service——配置通过config.Service访问不是 package-level 的全局变量。这使得配置可以被注入、mock、热加载。读到这里我有点汗颜我自己写过不少全局 config 结构体 init 函数的代码Crush 给我上了一课配置是依赖依赖就该走 DI而不是全局态。3. 内建 Bash 解释器做配置Crush 的crushrc配置文件不是 JSON/YAML而是真正的 Bash 脚本配合一组内置命令provider add、mcp add、permissions allow。这意味着配置里可以写if、可以source、可以$(op read ...)从 1Password 取密钥。这个取舍很大胆。好处是配置的表达力极强跨平台一致Windows 上也能跑因为自带 bash 解释器代价是配置变成了可信代码——README 里反复警告别 source 来历不明的文件。这是一个典型的权力越大责任越大的工程选择我个人很欣赏这种不把用户当傻子的设计。4. 双重 Fallback配置里指定的模型如果不可用比如 Provider 列表里没了Crush 会自动回退到安全的默认模型比如 Sonnet并写回配置。这种启动时自愈的设计让升级、换模型时的容错性好很多。一个 harness 不应该因为用户配错了一个字段就拒绝启动。五、回到我自己读完之后想改的几件事读源码的意义最终还是落回我自己的东西能怎么改进。我整理了几条对自己有触动的把权限当成骨架不是补丁。下次写 agent第一天就把 (session, tool, action, target) 四元组的权限模型建起来而不是等到出事。给 agent 一个循环检测器。这个 100 行的指纹方案可以直接抄。烧 context 的死循环是 agentic 系统最隐蔽的成本黑洞。Hook 化的中间件思维。与其把安全策略写死在代码里不如暴露成 hook 让用户自己定义。这既是扩展性也是一种谦逊——承认 harness 作者不可能穷尽所有用户场景。串行化会话状态。别在 agent 状态机上图并发那是 Web 服务的思维套到 agent 上必崩。工具的 prompt 和实现分离。这是个小习惯但对长期维护的帮助巨大。六、结语Harness 是 agentic 时代的操作系统合上 Crush 的源码我有一个越来越强烈的感受未来的 agentic 产品竞争模型层会越来越同质化大家都能调同样的 API真正的护城河是 harness。模型决定能做到什么harness 决定能不能稳定地做到。前者是上限后者是下限。一个产品如果只有好的模型接入、没有好的 harness用户用两次就会因为它又卡死了它又把文件改坏了而流失反过来一个 harness 扎实的产品哪怕接的是中等水平的模型也能给出靠谱的体验。从这个角度看Claude Code、Cursor、Crush 这些工具本质上都是在做同一件事给 LLM 写一个操作系统。进程调度agent 循环、权限管理permission、系统调用tools、信号处理cancel/halt、文件系统context 管理、shell 脚本hooks/skills……这些操作系统的经典概念在 agentic harness 里几乎一一对应。Crush 的价值在于它把这套LLM 操作系统开源了出来让你能拆开看看里面每一个齿轮怎么转。如果你在做 agentic 方向的产品不管用不用 Crush认真读一遍它的internal/agent和internal/permission比看十篇 agent 综述都有用。因为那些模型不会告诉你、论文也不会写的工程细节——循环检测怎么算签名、取消怎么不丢通知、权限怎么记忆——才是 agentic 产品真正难的地方。而这大概也是开源最大的意义不是让你免费用别人的成果而是让你看见别人踩过的坑、想明白的取舍。利益无关本文基于 Crush 公开源码的阅读笔记仅为技术探讨。Crush 是 Charmbracelet 出品的开源项目MIT 风格许可证地址 github.com/charmbracelet/crush。