OpenClacky 的 Harness 工程

两年三代架构迭代,第三代的实践与说明。


过去两年,我们经历了三代 Agent 架构。第一代尝试 RAG,第二代在 SWEBench 和云端多 Agent 上下了很多功夫——踩了很多坑,也学到了很多。

最深的教训是:用户想要的只是把任务又快又好地完成。最好的架构,不是盲目追求多 Agent 和复杂编排,而是在单 Agent 上把效果和成本控制做到极致。

于是有了第三代:用 Ruby 语言从零重构,历时三个月,这就是现在的 OpenClacky。

本文记录第三代架构的核心工程决策,以及每个决策背后的权衡。


一、始终追求 100% Cache 命中

Prompt Caching 是降低 LLM 成本最有效的手段之一。但大多数 Agent 实现都在这里漏掉了大量收益——天价账单和推理变慢,很多时候根源都在 cache 架构设计不合理。

Session 不重启,System Prompt 不变

OpenClacky 在 session 开始时完成 system prompt 的构建,之后永远不再修改,只向后追加新消息。

但"不修改"不等于"不能动态更新"。我们设计了一套 [session context] 机制:把需要动态变化的内容(比如 Skill 列表重载、模型切换)以独立块的形式插入到上下文中,而不是重新构建 system prompt。

效果是:session 全程 system prompt 的 cache 断点始终有效。绝大多数 Agent 遇到这类更新时会选择重启 session,Claude Code 和 OpenClaw 均是如此——重启意味着所有缓存全部失效、重新计费。我们的设计让这个代价降为零。

双重 Cache 标记

最直觉的做法是对最后一条消息打 cache_control。但这会造成结构性 miss:

第 N 轮:标记 messages[-1],建立缓存
第 N+1 轮:messages[-1] 变成了 messages[-2],标记丢失 → cache miss

我们同时标记最后 2 条消息:

第 N 轮:标记 messages[-2] 和 messages[-1]
第 N+1 轮:messages[-2] 标记仍然存在 → 命中缓存

这同时解决了回滚的问题——当发生 LLM 重试或出错后换方向重构时,双重标记确保仍能最大程度保住已有缓存,而不是全部重新付费。

Insert-then-Compress

压缩是另一个 cache 杀手。常见做法是开一个新的 LLM 调用来压缩历史——这会让所有已建立的 cache 全部失效。

我们把压缩指令插入当前对话流,在下一轮正常请求时顺带完成压缩。Cache 天然复用,压缩成本接近零。


二、最小工具集:一切皆 Skill

工具越多,模型选择时的噪声越大,schema 越长,cache 越难保持稳定。这是大多数 Agent 工具膨胀的根本代价。

Hermes Agent 是目前比较流行的 Agent 框架,内置 52 个工具。虽然支持按需加载,但一旦配置完成,大部分工具都会出现在上下文里。52 个工具的 schema 本身就是一笔固定成本,还会因为工具说明互相干扰而引发更多幻觉。工具越多,模型越容易在"用哪个"上出错。

架构第一决策:invoke_skill

OpenClacky 的核心工具集只有 16 个。这不是靠精简掉什么功能实现的,而是靠一个架构决策:把 invoke_skill 作为第一等公民。

invoke_skill 本质上是一个元工具——它让 Agent 可以将任何复杂能力委托给独立的 Skill,而不是把所有工具塞进核心列表。这个设计一次性解决了多个问题:

Sub-agent 调用:Skill 就是 sub-agent。调用一个复杂的代码审查 Skill,和调用一个 shell 命令,在协议上是完全等价的。Agent 不需要理解 sub-agent 的内部实现,只需要知道"有这个能力"。

复合工具能力:一些操作需要多步工具配合才能完成——记忆召回(读文件 + 语义判断 + 摘要)、代码库探索(glob + grep + 多文件阅读 + 结构分析)、历史压缩(读取 + 总结 + 写回)——这些放进核心工具列表会产生大量 schema 噪声,做成 Skill 就是一次干净的调用。

能力无限扩展,核心列表不变:用户安装新 Skill,工具数量不增加,schema 不变,cache 不受影响。这是 16 个工具能覆盖无限能力边界的核心原因。

省下了什么

省掉的能力 替代方式 工具数节省
代码库分析专用工具 code-explorer Skill ~5 个
记忆读写专用工具 recall-memory Skill ~3 个
浏览器自动化(多动作拆分为多工具) 单一 browser 工具统一覆盖 ~8 个
Sub-agent 编排工具 invoke_skill 统一入口 ~6 个
定时任务管理工具 cron-task-creator Skill ~4 个

核心工具列表稳定,cache 断点不移动。 工具数量不是竞争力,任务完成率才是。

竞品对比

各家 Agent 在核心功能上并无本质差异,真正的区别在于工具数量。

OpenClacky Claude Code OpenClaw Hermes Agent
内置工具数 16 40+ 23(含插件/MCP/channel 可达 30–50) 52
Sub-agent 调用
Channel 集成
Web Search / Fetch
Browser 自动化
能力热扩展(无需重启) ✅ Skill 热加载
工具膨胀应对 架构上不产生膨胀 ToolSearchTool(超 ~30 自动启用) 扩展形式按需加载 按需加载

Claude Code 的 ToolSearchTool 是一个有创意的工程方案:工具数超过 ~30 后,模型改为按需搜索工具,避免 context 爆炸。这是在工具已经膨胀的前提下做的补救。OpenClacky 的选择是从源头不让工具膨胀,所以不需要这一层。


三、FuzzySearchReplace

代码编辑是 Coding Agent 最高频的操作。LLM 生成的 old_string 有一个固有问题:转义符、缩进、行尾空白——这些细节在模型生成时很容易出错,导致精确匹配失败,进入重试循环。

问题

每次 edit 失败,Agent 需要重新理解错误、生成新的 old_string、再次尝试。在长任务里,这类失败会积累成显著的 token 浪费和时间消耗。

我们的决策

StringMatcher 里实现了 5 层降级匹配策略:

层级 策略 针对的问题
1 精确匹配 正常情况
2 Trim 前后空白 模型生成时多了换行或空格
3 Unescape 转义符 \n \t \uXXXX 未被展开
4 Trim + Unescape 组合 两种问题同时存在
5 逐行智能匹配(Tab/Space 容差) 文件缩进风格不一致

大多数情况在第 2-3 层就能命中。这不是模糊匹配——每一层都针对 LLM 输出的具体出错模式设计,匹配结果是确定性的。


四、自进化的 Parsers & Scripts

PDF、Word、Excel、图片处理是 Agent 的核心基础能力。但本地环境的多样性让这件事很棘手——不同系统、不同版本、不同依赖,总会遇到意料之外的格式问题。

面对这个困境,通常只有两条路:放弃这部分能力,或者提高安装门槛,要求用户在安装时预置好所有依赖。两条路都不好走。

我们的决策

我们选了第三条路:把工具进化的权力交给 Agent 本身

Gem 内置默认解析脚本,首次运行时复制到用户目录 ~/.clacky/parsers/。此后 Agent 永远使用这个用户空间的版本。

当解析失败时,Agent 直接定位到对应脚本,自行修复 bug,下次执行自动生效。Shell 脚本(~/.clacky/scripts/)采用相同机制。

结果是:安装时零门槛,使用中越来越好用。第一次遇到某种特殊格式会失败,Agent 自修复之后,永久修好。不需要等待版本更新,也不需要用户手动干预。


五、有门槛的 Skill 自进化

Skill 自动创建是个双刃剑。没有门槛的自动创建会积累大量低质量 Skill,它们互相矛盾,检索时产生噪声,反而干扰 Agent 的判断——我们把这个问题叫做 Skill 熵增。

我们的决策

两个自进化钩子,触发条件都很克制:

SkillAutoCreator(从任务中提炼新 Skill):
- 完成一个 ≥ 12 轮迭代的复杂任务
- 且任务过程中没有调用现有 Skill(说明确实是新工作流)
- 由 LLM 自己判断这个工作流是否值得沉淀为 Skill

SkillReflector(优化已有 Skill):
- 用户显式调用了一个 Skill(不是被动推断触发的)
- 执行超过 5 轮迭代(说明 Skill 确实承担了复杂任务)
- LLM 反思指令是否清晰,是否有遗漏的边缘情况

宁可少创建,不创建垃圾。~/.clacky/skills/ 里的每一个 Skill,都应该是经过足够多真实使用验证的。


六、白名单记忆触发

长期记忆对 Agent 的连续性很重要,但记忆写入本身有成本:一次额外的 LLM 调用,以及写入内容的质量问题。低质量的记忆在未来的会话里会成为上下文噪声。

我们的决策

记忆写入需要同时满足两个条件:

  • 对话轮数 ≥ 10 轮(短对话通常没有值得长期保留的信息)
  • 出现明确的高价值信号:用户的显式决策、新建立的持久上下文、反复出现的错误模式

不满足条件时,会话结束后什么都不写。满足条件时,LLM 自己判断哪些内容值得保留、写到哪个文件、如何与已有记忆合并。

~/.clacky/memories/ 里的每一条记忆,都是经过筛选的。召回时的信号噪声比更高,判断更准确。


七、无 Gateway 设计

许多 Agent 工具会常驻一个后台服务进程——即使你已经停止使用,它依然在监听端口,依然可以被访问,依然可能在你不知情的情况下响应请求。

我们认为这不对。用户说停,就应该真的停。

OpenClacky 不常驻任何额外的后台进程。关掉 Agent,端口随之释放,没有任何监听入口留在系统里。这是用户应该拥有的完整控制权。

无缝升级

不常驻进程通常意味着一个代价:升级时需要停服。用户在 Web UI 点击升级,如果服务中断、页面刷新失败,体验会很糟糕。

我们用的方案和 nginx 等顶级服务器程序相同:Master-Worker 分离 + Socket 继承

Master 进程永久持有 TCP 端口,自身不处理任何请求。Worker 进程继承 Master 的 socket,负责真正的 HTTP 服务。升级时,Master 用新版本启动一个新 Worker,等它就绪后再向旧 Worker 发送退出信号,整个切换过程端口始终保持占用,连接无中断。

对用户来说,在 Web UI 点击升级,新版本静默生效,页面不会断开。


小结

这七个决策有一条共同的底层逻辑:不用复杂性换能力,而是用精确性换效率。

大多数 Agent 框架走的是加法路线——更多工具、更多记忆、更多编排层——然后在膨胀之后用补丁弥补:工具太多加个搜索层,记忆太杂加个过滤器,上下文太长加个压缩器。每一层补丁都带来额外的 token 消耗和潜在的失控点。

我们的选择是在每一层主动设边界:

  • Cache 层:不接受默认的缓存失效。双标记窗口锁住 system prompt,Insert-then-Compress 让压缩本身也能命中缓存。命中率接近 100%,每次调用的成本可预期。
  • 工具层:16 个工具,不是因为懒,而是因为工具数量直接影响 schema 噪声和模型判断。扩展能力通过 invoke_skill 外包出去,不污染核心工具集。
  • 容错层:FuzzySearchReplace 5 层降级,是承认字符串匹配在真实代码里会失败,提前把失败路径设计进去。
  • 知识层:记忆写入有白名单门槛,Skill 自进化有轮次门槛,Parser 自修复而非等升级。每一条进入长期知识库的信息,都经过了足够的使用验证。信号密度高,噪声少。
  • 可靠性层:Time Machine 让文件和对话状态同步回滚,用户可以大胆尝试。无 Gateway 让停止就是真的停止,没有隐藏服务。Master-Worker 分离让升级无缝,端口全程不释放。

这些决策没有一个是炫技。它们是两年踩坑之后,对「AI Agent 应该怎么设计」这个问题的诚实回答。


完全开源,MIT 协议:github.com/clacky-ai/openclacky

了解如何安装 → · 模型与 API 配置 →