WWW.YOUINFO.SITE
标签聚合 命中率

/tag/命中率

LinuxDo 最新话题 · 2026-05-24 16:52:50+08:00 · tech

这几天用 CodexManager ,发现缓存命中率特别低,30~40%,但觉得很耐用,怎么用都用不完。今天有空掏出来 CPA ,测试缓存到90%以上了,但是额度消耗又特别快。检查了 sevice_tier ,没有设置成 fast ,而且这个字段也在 new_api 也被过滤掉了。 伴随着各种疑问,我掏出了6个满额度的日抛plus号,分别给 CPA 和 CodexManager 3个进行用量测试。反代路径为: codex(V0.133.0) -> newapi(V1.0.0 rc4) -> CPA(V7.0.4) / CodexManager(V0.3.4) 以下为结果,,交给gpt5.5分析了一轮用量。反直觉的发现是: 缓存命中率越低,总tokens数量越高,额度越耐用 。 这是偶然,还是确实是这个原理 ,有佬友了解过吗 5 个帖子 - 4 位参与者 阅读完整话题

LinuxDo 最新话题 · 2026-05-16 20:49:52+08:00 · tech

以下所有内容转载自微信视频号‘张司机在路上’博主的视频,有部份修改。 从2.1.36开始,A社很鸡贼的在Claude code里的API请求里塞了一行 x-anthropic-billing-header , 是当成system prompt的第一块发过去的. 里面有个叫 cch 的5位hex字段, 每次请求都不一样。 博主实测同一个session里连续三轮, cch依次是 97bd6 → 24c2d → ead88 , 前一轮辛苦建好的缓存, 下一轮直接对不上前缀hash。 A社自己的服务端知道怎么处理. 但所有第三方Anthropic兼容代理、Bedrock、本地vLLM是不知道的,就会把这段当成system prompt的一部分算缓存key。当前缀每次都变的情况下, 命中率就会大大下降。 在Claude code的GitHub issue里有很多人提了,但是A社就是装看不见 博主从Claude Code二进制里扒出了源代码, 找到了这个header的函数, 如图: 解决办法:在环境变量 ~/.claude/settings.json 的env段添加一段: CLAUDE_CODE_ATTRIBUTION_HEADER=0 , 重启即可。 注:这个视频是博主刚发的,但因为指明的cc版本比较旧了,所以我不太确定是不是过时的消息,不清楚现在其他厂商是否做了服务器适配。 如果在用第三方API转发Claude Code的朋友, 可以试试这个方法,反正就是加一行参数的事。 5 个帖子 - 4 位参与者 阅读完整话题

V2EX - 技术 · 2026-05-16 18:11:14+08:00 · tech

接上一篇 https://v2ex.com/t/1211434 很多朋友好奇我们的工程实践,我把这二年的坑和 Ruby 重写的思考放出来,大家一起看看离 ClaudeCode 这种顶级 Harness 工程还有多远。 开篇 为了让新朋友重新了解一下我们的评测结果,我再列一下。 成本极优 :3 项任务实测,4 家 Agent 横评,OpenRouter CSV 逐请求核算: Agent 总成本 请求数 Cache 命中率 OpenClacky $5.10 51 90.6% Claude Code $5.49 70 95.2% OpenClaw $15.70 81 88.7% Hermes $30.14 218 60.3% 完整数据和产物对比: openclacky.com/benchmark 51 个请求 + 90.6% 命中率 → $5.10 。218 个请求 + 60.3% 命中率 → $30.14 。成本差距的直接原因就两个: 请求数 和 cache 命中率 。 不要忘了, OpenClacky 是一个全功能 Agent :WebUI + 命令行、长期记忆、Skill 技能库、定时任务、IM 接入(飞书/企微/微信)、浏览器自动化、子 Agent 、运行时切模型、Skill 自进化与动态加载。 而很多开源 Agent 也许有较好的 Token 消耗,或功能不全,或命中率不高。 在实践中最大的问题是: 这些功能里很多跟"高 cache 命中率"是结构性冲突的。 举例: 切模型 → 模型 ID 写在哪?写进 system prompt 就 cache 失效一次。 中途装 skill → skill 列表写在哪?写进 system prompt 就 cache 失效一次。 知道"今天日期" → 写进 system prompt ?跨天就失效。 加"读 PDF"能力 → 最容易的实现是再加一个工具 → 工具 schema 变了 → cache 失效面变大,模型选错工具的概率也变大。 上下文不爆 → 最容易的做法是开一次独立 LLM call 做压缩 → 压缩本身 100% miss ,压完之后主对话 cache 也凉了。 单看任意一头都不难做:少做功能,命中率自然高;不管账单,功能可以堆得很猛。难的是两头同时做。这篇文章讲我们在每个冲突点上具体怎么取舍。 效果已经不是当前 Agent 的主要矛盾,成本才是。 起步:两年失败史 第三代之前还有两代,失败的很严重。但我感觉现在还有很多人在踩坑,估计很多人有争议,但我 100% 站我自己的观点。 第一代( 2024-2025 上半):RAG / 知识库 把用户 codebase 、文档、历史会话全 embedding 进向量库,hybrid 检索 + 重排 + query rewrite 。Agent 流程是"先查上下文,再答"。 实际跑下来的问题: 成本高,每次更新的 codebase ,需要同步更新向量,实时性无法保证。 准确率有限,例如听起来 90% 的召回率是不是还不错,但是对不起,不仅没有用,还可能有害,我预测,97%的召回率可能才刚刚够用。 多了一个会失败的部件(向量库),增加了很多延迟。 结论:千万不要搞任何 RAG 、知识库分片。如果你要上 Agent ,请直接上 Agent ,外加一个适合 AI 去阅读的网站就可以了。(参考我们自反思 Skill product-help 的实现) 第二代( 2025 中期):SWEBench / 多 Agent 工作流 Planner / Coder / Reviewer / Tester 各一个 agent ,消息总线 + 角色 prompt 编排。 实际跑下来的问题: 每个 sub-agent 各有 system prompt ,各有 cache 命名空间。Agent 间交接靠消息序列化状态, 每次交接 = 一次 cache miss 。 一个单 agent 4 分钟能完成的任务,多 agent 编排到 14 分钟,成本 6×。 SWEBench 分数能刷上去,但榜单跑分跟用户实际感受脱节得很厉害。 结论: 不要做工作流编排。 多 Agent 在结构上就是 cache 灾难。人类的分工不对 AI 有任何价值。AI 是万能的。 不要被 benchmark 绑架。 模型每 6 个月跨一个台阶,用今天的二流模型 + 工作流堆出来的分数,会被半年后顶级模型 + 朴素 harness 直接抹平。把工程预算花在 harness 上,不要花在编排上。对 Agent 工程来说,Benchmark 本身也并不重要,一个朴素的 Agent 思想打败一切:站在 AI 的角度思考你的上下文。 第三代( 2025 年底至今) Ruby 从零重写,4 个月。围绕"cache 局部性"和"工具集稳定性"来组织。后面讲的所有决策都属于这一代。 核心决策 1:双 cache 标记 + 允许失败回退 OpenClacky 同时跑在 Claude / OpenAI 兼容这两条主线上,两边的 prompt cache 行为不同,但工程上我们只关心一个共性: cache 是按"前缀"匹配的——前缀里改一个字节,从那里往后全部失效 。 所以前缀的"层次"和"标记位置",决定了你下一轮还能 hit 到哪里。我们把请求前缀分成几段考虑: session-stable 段 :system prompt 、工具 schema 。session 内绝不变。 append-only 段 :历史消息。只追加、不修改。 session-volatile 段 :当前轮新消息(用户输入、工具结果、模型回复)。 前两段交给"系统提示词层"的天然断点,后续每轮都能 hit 。 真正需要工程的是"append-only 段"——它每轮都在长尾部,标记打哪儿、打几个,决定了下一轮还认不认得它。 朴素做法为什么不够 最直觉的做法是"每轮在 messages 末尾打一个 marker"。它在以下场景都会失效: history 单调追加 :第 N 轮在 messages[-1] 打 marker ,第 N+1 轮 messages 又长了一条,原 marker 的位置内容已经不一样了——服务端找不到匹配,整段 history 上 cache miss 。 模型回退一次工具调用 :工具报错、用户 Ctrl-C 重试、或者模型自己决定换一种 tool call——这一刻"原本的最后一条"被丢弃,单 marker 直接作废。 运行时切模型 :用户在 session 中途从 Sonnet 切到 Opus ,请求路由到新 endpoint ,最理想情况下我们希望两个模型共享尽可能多的前缀。任何不必要的 marker 抖动都会让"切换"成为新的 cache miss 事件。 我们一开始就栽在 (1) 上。修复链能从 git log 里看出节奏: 8ff66cc fix: cache 6ea99fe fix: prompt cache e9a3602 feat: prompt cache works fine 7734c97 feat: try 2 point cache 前三个 commit 是逐步逼近,最后一个是结构性正解。 双标记是怎么工作的 每轮我们标 两条 连续消息,不是一条: 第 N 轮: [..., msg_A, msg_B(*), msg_C(*)] ↑ ↑ marker 1 marker 2 第 N+1 轮: [..., msg_A, msg_B(*), msg_C(*), msg_D(*)] ↑ ↑ ↑ (仍在) (仍在) 新 marker 第 N+1 轮发出请求时: 服务端尝试匹配 msg_C 的 marker → 命中到 msg_C 之前的所有内容 ( system prompt + 工具 + 整段历史除最后一条)。 我们在 msg_D 上加新 marker ,建立新的尾部断点供下一轮使用。 这是一个滚动双缓冲:任何时刻都持有两个断点——一个"刚建立的"(写)和一个"上一轮建立的"(读)。下一轮把"读"再读一次,把"写"扔掉,再在新尾部写一个。 永远不会出现两个 buffer 同时失效的瞬间。 为什么是 2 ,不是 3 或 4 主流大模型的 cache 都允许多个标记位(上限不一),但 更多并不更好 : 每多一个 marker ,那一轮就多一次 cache write ,按写入费率收。 双标记要解决的失败模式只有一个位置:**"昔日尾部 / 今日尾部"这个边界 。两个 marker 正好覆盖。第三个 marker 落在更靠前的位置,对应的 cache 段在下一轮 仍然会被前两个 marker 之一覆盖**——它写的是一段永远不会被独立读到的前缀。 标记多了之后,部分 endpoint 上服务端的候选前缀匹配代价也会涨。 简单说:2 是覆盖尾部边界的最小数量,3 多余,4 浪费。 允许失败:单步回退仍然命中 这是双标记的第二个好处,也是当时 7734c97 的真正动机。 模型偶尔需要回退一次 tool call:工具返回错误、用户 Ctrl-C 重试、或者上游 streaming 断了一半。这种情况下"昨天的最后一条"被丢弃了,但 倒数第二个 marker 通常仍然落在仍存在的消息上 ——单步回退后还能命中。 单 marker 在回退时直接作废;双标记是能扛住单步回退的最小数量。我们没继续往上加(三标记也能扛两步回退,但成本不划算)——回退超过一步的概率已经低到可以接受全 miss 一次。 模型切换:为什么要 marker 不动 OpenClacky 支持在 session 中途换模型。工程上要保证两件事: 新模型的请求前缀和老模型尽量一致。 我们不在 system prompt 里写当前模型 ID (写在 [session context] 块里,见决策 2 ),换模型不动 system prompt 。 marker 位置不变。 切完模型的下一轮,前两个 marker 落在和切换前完全相同的 message 上。新 endpoint 第一次请求会因为"换了上游账号 / 区域"产生一次 cache write ,但 前缀的几何结构是连续的 ,warm-up 只发生一轮。 这个细节不做的话,每次切模型一定要都要付完整 cache 重建的钱,用户会很不开心。 不能标的位置 marker 选择逻辑里有一条硬规则: 跳过 system_injected: true 的消息 。 [session context] 块就是典型例子——它是一次性信息,下一轮尾部已经变了,落在它身上的 marker 是一笔永远读不回来的写入。压缩指令注入也是同样的处理(决策 5 会展开)。 marker 选择从尾部往前走, system_injected 的跳过,凑够两个真实对话消息为止。 本节总结 system prompt + 工具 schema:靠 system prompt 段的天然断点 hit 。 history 滚动:靠双标记。 单步回退:靠双标记容错。 模型切换:靠"动态信息不写进 system prompt"+ marker 位置不变。 把这四件事同时做到,普通一轮的 cache 命中率才有可能稳定在 95%+。 前三件是 cache 几何,第四件是设计纪律。 决策 2:永不变的 system prompt OpenClacky 的 system prompt 在 session 启动时一次性构建,之后字节冻结。 任何"想往 system prompt 里塞动态信息"的需求,必须重定向到别的位置。 这条纪律是 cache 命中率的第一道地基——system prompt 一变,后面所有 cache 全废,没有任何"局部修补"能挽回。 但日常跑下来,至少有四类信息"天然想插入到 system prompt": 当前时间、当前工作目录、操作系统 ——模型需要这些来生成正确的命令和路径。 当前模型 ID ——模型知道自己是谁有助于自适应行为。 用户装了新 skill ——模型需要看到新的 skill 名称和描述才能调用。 用户更新了 USER.md / SOUL.md ——agent 的人格和用户偏好发生了变化。 这四类信息都是"session 中途可能变"的。如果写进 system prompt ,任何一次变更都意味着全量 cache 失效。 [session context] 块 我们的做法是把这些信息写进 message 流,而非 system prompt 。每当环境发生模型需要感知的变化时(跨天、切模型、切工作目录),agent 在 history 里追加一条 user 角色的消息: [Session context: Today is 2026-05-13, Tuesday. Current model: claude-sonnet-4-6. OS: macOS. Working directory: /Users/.../project] 这条消息被标记为 system_injected: true 。它不会被 cache marker 选中(决策 1 已经讲过),不会被算作真实用户轮数,压缩时也不会被原样搬进新历史。 注入是按日期 gate 的:同一天内只注入一条。跨天了,插一条新的。切了模型,插一条新的。大多数 session 里你只会看到一条 session context 块。 这个设计踩过的坑 第一版 inject_session_context 是在 agent 构造期就急切注入的。结果 @history.empty? 返回 false , run() 误以为是后续轮,跳过了 system prompt 的构建—— 第一次请求带着一条"today is Tuesday"但没有 system prompt 就发出去了 。agent 的行为诡异了大约一天才定位到。 修复只有一行:等 system prompt 构建完毕之后再注入。代码里有一段注释记录了这个约束: # IMPORTANT: Skip injection when the system prompt hasn't been built yet. # Otherwise, appending a user message to an empty history makes # @history.empty? false, which causes run() to skip building the # system prompt entirely. 教训是: 前缀的组装顺序比前缀的内容更要紧。 你可以花大力气设计每一段的内容,但只要组装顺序错一步,整个 cache 策略就是废的。 Skill 列表怎么处理 Skill 列表是最容易跟"永不变的 system prompt"冲突的需求。用户可以随时装新 skill ,模型需要看到 skill 名和描述才能通过 invoke_skill 去调用它。 我们的取舍: skill 列表在 session 启动时渲染进 system prompt ,之后冻结。 session 中途装的新 skill ,模型在当前 session 里看不到——它会看到一条 [session context] 通告说"skill 列表已更新,新 skill 从下一个 session 可用"。 这意味着用户装完 skill 想立刻用会发现用不了,要开新 session 。我们接受这个摩擦,因为替代方案是重渲染 system prompt 导致全量 cache 失效——这个代价打到所有用户的所有 session 的每一轮上。装 skill 是低频操作,cache 命中是每轮都在享受的收益,取舍方向很清楚。 USER.md / SOUL.md 的更新也是同样的处理:session 启动时读取,session 内不再变。 但是,在用户体验上,我们虽然降低了一些 Skill 发现的概率,但一旦用户主动提起新的 skill 时,我们系统仍能及时发现新 Skill 。没有任何缓存,每次都会重建 Skill 列表。 决策 3:invoke_skill 的妙用 invoke_skill 是 OpenClacky 的 16 个工具之一,它是整个 OpenClacky 最核心的设计,花费的时间也最多,它提供 Skill 热加载能力,子 Agent 架构支持,记忆召回能力、Skill 进化能力,但它只占 system prompt 不超过 200 个 Token 。 启动子 agent 。 子 agent 用的工具集跟主 agent 完全相同 ( 16 个)。它不是一个"精简版",它能做主 agent 能做的一切事情。 子 agent 执行完后,把结果文本返回给主 agent ,主 agent 的 history 里只看到"invoke_skill → 结果"这一对消息。 这个设计一口气解决了好几个问题: 子 agent = 状态隔离 做代码审查的 skill 可能需要读几十个文件、跑 grep 、输出长篇分析。如果这些中间步骤都在主 agent 的 history 里,history 会膨胀得很快——cache 命中率没变,但上下文总量上去了,压缩触发得更早,成本更高。 子 agent 把这些中间过程隔离在自己的 session 里。主 agent 只看到最终结论。 主 agent 的 history 没有被污染。 动态加载 Skill ,不改 system prompt 装新 skill 的流程就是把一个 SKILL.md 放到 ~/.clacky/skills/<name>/ 或 .clacky/skills/<name>/ 下。skill 列表渲染进 system prompt 的时间点是 session 启动,决策 2 已经讲过。 但 invoke_skill 这个工具本身是 始终存在 的——它不需要 system prompt 里列出所有 skill 才能调用。模型可以通过 [session context] 通告知道新 skill 的名称,然后直接 invoke_skill(skill_name: "xxx") 。Skill 的 SKILL.md 是在调用那一刻才读取的,不是预编译进 system prompt 的。 所以"动态加载 skill"这个能力,实际上是 invoke_skill 的运行时读取 + [session context] 的通告组合出来的。 不需要改 system prompt ,不需要改工具列表,不需要重启 session 。 Skill 注入与路径处理 每个 skill 的 SKILL.md 可以引用相对路径的资源文件(模板、配置等)。 invoke_skill 在启动子 agent 之前会把 skill 的目录作为上下文路径注入,子 agent 能用 file_reader 、 glob 直接读到 skill 附带的资源。 这让 skill 可以做到"自包含"——一个 skill zip 包里既有指令又有模板,装上就能用。 加密 Skill 与选择性落盘 部分 skill 包含商业敏感内容(客户的 prompt 策略、内部流程等)。OpenClacky 支持对 SKILL.md 做加密存储,运行时解密到内存、用完不落盘。同时 session 的落盘也是选择性的——对于涉及加密 skill 的 session ,可以配置为不持久化到磁盘,只在内存中存在。 这不是 cache 工程的范畴,但它是 invoke_skill 架构的延伸:因为子 agent 的状态是隔离的,选择性不落盘可以精确到某次 skill 调用,而不需要把整个 session 的落盘关掉。 决策 4:控制稳定可靠的工具集 16 个 工具 schema 紧贴 system prompt 之后,在 cache 前缀里。schema 一变,后面全失效。这意味着: 每多加一个工具,你不只是多了一份 schema 的 token 成本,你还多了一份"下次改工具时全量 cache 失效"的风险面。 另一面,工具太少也有代价:模型本来一步能做完的事,现在要分两三步(先调一个通用工具获取信息,再调另一个来操作),轮次上去了,每轮都要付 cache 和 output 的钱。 所以这不是一个"越少越好"的问题,而是一个 经验平衡点 。我们的答案是 16 个 。 这 16 个分别是什么 类别 工具 说明 文件读写 file_reader , write , edit 读、写、搜索替换 代码搜索 glob , grep 文件查找 + 内容搜索 执行 terminal shell 命令 浏览器 browser 接管 Chrome/Edge 网络 web_search , web_fetch 搜索 + 抓取网页内容 任务管理 todo_manager , list_tasks , undo_task , redo_task 规划、撤销、重做 交互 request_user_feedback 需要用户输入时 扩展 invoke_skill 调用 skill (决策 3 ) 安全 trash_manager 安全删除( rm → trash ) 设计原则 简化参数。 每个工具的参数尽量少、语义尽量明确。比如 glob 只要 pattern 和 base_path ,不需要模型去组合 --include / --exclude / --type 这些 flag 。参数越多,模型出错的概率越高,出错就要重试,重试就是成本。 够用但不冗余。 glob 和 grep 是两个工具而不是一个: glob 负责"哪些文件匹配", grep 负责"文件里哪些行匹配"。合成一个会让参数变复杂,模型调错的概率上升。但也没有继续拆成 find_files / list_dir / tree 三个—— glob 一个就能覆盖这三个场景。 为每个工具写丰富的测试用例。 工具是 agent 跟外部世界的接口,一个工具出 bug 的代价远高于普通代码出 bug——它会让模型产生错误的观察,进而做出错误的决策,进而需要更多轮次来纠正。我们一共有 1600+ 的用例去覆盖各种场景的处理。最近有 V 友还给我们提交了子项目扫描慢(对,OpenClacky 支持子项目处理)的一个相关优化 issue 。 为什么不是 10 个,也不是 25 个 10 个做不到。 undo_task / redo_task / list_tasks 这些看起来"可以不要"的工具,拿掉之后模型就只能用 terminal 跑 git 来处理代码回滚——成功率远低于专用工具,而且 git 操作的副作用很难控制。很多工具设计了一个 code_run ,我们并不推荐,实测反而导致任务变慢(需要写长代码),轮次变多(多次尝试)。 不需要 40+,只需要 16 个。 省掉的能力 替代方式 工具数节省 代码库分析专用工具 code-explorer Skill ~5 个 记忆读写专用工具 recall-memory Skill ~3 个 浏览器自动化(多动作拆分为多工具) 单一 browser 工具统一覆盖 ~8 个 Sub-agent 编排工具 invoke_skill 统一入口 ~6 个 定时任务管理工具 cron-task-creator Skill ~4 个 如果以后需要第 17 个,我们会加。4 个月了,还没加。 决策 5:压缩——不换模型、空闲时做、压到底 上下文窗口是有限的。不管 200K 还是 1M ,长任务跑下来总会填满。填满之前必须压缩,否则要么截断丢信息,要么溢出直接报错。 压缩是 cache 命中率最大的单点威胁:老的消息被替换成一段摘要,前缀从那一刻起就跟之前不一样了—— 必然 cache miss 。但压缩不可避免,所以问题不是"要不要压",而是"怎么把压缩的破坏降到最低"。 结论一:不要换模型压缩 很多 agent 的压缩流程是开一个 独立的 LLM call ,用一个便宜/快速的小模型来做摘要。 问题: 独立 call 的 system prompt 跟主 session 不一样(通常是"你是一个摘要助手"), 跟主 session 的 cache 没有任何共享前缀 ,压缩本身就是一次 100% cache miss 。 压缩完之后,主 session 的 history 被替换了(老消息变成了摘要),主 session 的 cache 也跟着失效——接下来 4–5 轮跑在 cold 费率上。 等于你为每次压缩付了 两笔钱 :一笔给压缩 call 本身的 cache miss ,一笔给主 session 压缩后的 cold-warm 阶段。 我们的做法: 压缩不开独立 call ,而是把压缩指令作为一条消息插进当前对话的末尾 ( Insert-then-Compress )。 这条指令被打上 system_injected: true ,走正常请求路径。效果: 压缩 call 命中现有 cache :同样的 system prompt 、同样的 tools 、同样的 history 前缀。只有尾部的压缩指令是 cold 的,几百 token 。 压缩完成后,重建 history : [system_prompt, summary, last_N_messages] 。这一刻 cache 确实会 miss 一次——但只 miss 一轮,从第二轮开始双标记重新接管。 对比(一次 50K-token 会话的压缩事件): 独立 call 方案 Insert-then-Compress 压缩 call 的 cache hit 0% ~95% 压缩期间 cold token ~50,000 ~500 主 session cold-warm 轮数 4–5 1 结论二:20–30 万 token 是压缩的甜区 太早压:浪费了上下文里还有价值的细节,摘要丢信息。 太晚压:上下文太长导致模型注意力分散、推理变慢、输出质量下降。 我们测过多个阈值。20–30 万 token 是效果和成本的甜区——模型还能有效利用上下文,但离溢出还有足够余量来完成压缩本身。 压缩后 无论如何会压到 1 万 token 以内 。这不是省钱,这是控制后续每一轮的 baseline 成本——history 越短,每轮 input 越少,cache miss 时的惩罚也越小。 结论三:空闲第 3 分钟启动压缩 这是跟 cache TTL 的博弈。大模型厂商的 prompt cache 普遍有 TTL—— cache 在一段时间无请求后会过期 。过期之后下一轮的 input 是全量 cold ,直接翻到 10× 成本。而且后续每轮都在叠加成本,直到 cache 重新 warm 起来。 所以我们跑了一个空闲计时器( idle_compression_timer.rb ): 用户停止输入 90 秒 后开始检查。 如果 history 已经接近压缩阈值 → 立刻触发压缩 。此时 cache 还是热的,压缩代价很低。 压缩完之后,新的短 history 在 TTL 过期前就建立了新的 cache 断点。 效果是:用户思考了几分钟回来,看到的是一个 已经压缩好、cache 已经 warm 的 session 。相比之下,如果不做空闲压缩,用户回来时面对的是一个 cache 过期的长 history——那一轮的 input 可能是 30 万 token 全量付费。 单这一个行为,在长思考间隔的场景下就能省 10× 的钱。 空闲计时器跑在后台线程里。记得加锁! 百万上下文的真相 "百万 token 上下文"听起来很性感,但做 agent 有两个现实: 过长的上下文对模型效果并不总是正面的。 模型在超长上下文里的注意力分散问题是已知的——关键信息被淹没在大量历史里,输出质量反而下降。 你真不一定用得起。 记住,模型每轮都要把上一轮所有的上下文全部带上。100 万 token 的 input ,即使全部 cache hit ( 0.1× 费率),一轮也要付 10 万 token 等价的钱。如果 cache miss 了一次,那就是 100 万 token 全价。 真实世界用户停下来思考太过于常见,Cache Missing 太容易发生,Agent 开发者必须想办法帮用户减少开销。 所以我们的策略不是"尽量用满上下文",而是" 积极压缩,保持 history 短小 "。1 万 token 的压缩后 history + 95% cache hit ,比 100 万 token 的未压缩 history + 99% cache hit 便宜得多,效果也更可控。 如何确保压缩后仍然保证足够好的效果,这是另一个话题,我们后面展开。 决策 6:自进化的工具能力 PDF 、Excel 、Word 、PPT 的阅读和解析是 Agent 经常遇到的需求。处理这类文件通常有两种路径: 内置一个 tool :比如 read_pdf 、 read_excel 。好处是开箱即用,坏处是每个格式一个工具,工具列表膨胀(违背决策 4 ),而且解析库的依赖链往往需要 C 扩展,装起来就不"零痛"了。 做成 skill 让用户装 :对用户来说不友好——遇到一个 PDF 还得先去装 skill ,体验断裂。而且 skill 描述怎么写、什么时候触发,AI 效果不可控。 我们选了第三种路径: 首次安装时把预设的文档处理脚本 copy 到用户目录,之后允许 AI 自行更新维护这些脚本。 具体做法: 首装 OpenClacky 时, onboard skill 会把一组 Python 脚本( PDF 解析、Excel 读取、OCR 等) copy 到 ~/.clacky/scripts/ 。 这些脚本不是 Ruby ,而是 Python 3 。原因很实际:Python 的文档处理生态( pdfplumber 、 openpyxl 、 python-docx 、 python-pptx )是当前最成熟的,OCR 方面 pytesseract / paddleocr 也远比 Ruby 生态完善。 当 agent 需要读一个 PDF 时,它不调一个专用 tool——它用 terminal 工具跑 python3 ~/.clacky/scripts/ read_pdf.py <file> 。 工具列表没有增加。 如果脚本跑不过去(缺依赖、格式变了),agent 可以直接 write 修改脚本、 terminal 跑 pip install 装依赖。下次再遇到同类文件就不会出问题了。 这就是"自进化"的含义: 处理文档的能力不是写死在 gem 里的,它活在用户目录的脚本里,agent 自己可以维护。 第一次可能需要装个 pdfplumber ,装完之后就是永久能力。 这个设计把"文档处理"从工具层面拉到了脚本层面,避免了工具列表膨胀,也避免了硬编码 C 扩展依赖。trade-off 是用户机器上需要有 Python 3——但 macOS 和大多数 Linux 发行版默认自带,这个前提在实际用户群里几乎都满足。 决策 7:内置浏览器工具,No Headless 浏览器自动化是 Agent 越来越重要的能力——验证前端改动、抓取文档、自动化测试流程。 市面上主流的做法有两种: Headless 浏览器 ( Puppeteer / Playwright ):agent 启一个无头浏览器实例,完全在后台跑。 外接 MCP :通过 MCP 协议连接一个外部浏览器服务,agent 发 JSON-RPC 指令。 我们两种都不用,或者说—— 我们自己内置了一个 MCP Client ,去接管用户已经在跑的 Chrome / Edge 。 为什么不用 Headless Headless 浏览器的问题是"看不见"。agent 操作的页面用户看不到、不知道 agent 在干什么、出了问题也无法判断。对于 Agent 的使用场景——用户在旁边盯着 agent 干活——"看不见"是很大的信任问题。 另外,Headless 经常遇到反爬检测:登录态拿不到、Cloudflare challenge 过不去、需要手动验证。用户自己的浏览器里已经登录好了、cookie 都在,为什么不直接用? 我们怎么做的 lib/clacky/tools/browser.rb ( 610 行)+ lib/clacky/server/browser_manager.rb 是整套实现。架构是: 用户的 Chrome / Edge 开启 Remote Debugging 端口(一次性配置, browser-setup skill 引导完成)。 OpenClacky 内置一个 MCP Client ,通过 stdio JSON-RPC 2.0 连接 chrome-devtools-mcp 这个 daemon 。 daemon 进程首次调用时启动,后续跨多次 tool call 保持存活。 browser 工具对外暴露的是高层语义动作: snapshot 、 click 、 type 、 navigate 、 screenshot 等——不是底层 CDP 指令。 对模型来说,"浏览器"就是 16 个工具里的 1 个,schema 跟其他工具一样稳定,不会因为浏览器的状态变化而改 schema 。 这符合决策 4 的原则。 为什么不把浏览器做成外部 MCP 我们可以不内置浏览器、让用户自己配一个 Browser MCP 服务。但这样做的问题是: 用户体验差 :装 agent 之外还要装 MCP 服务、配端口、配认证。 稳定性不可控 :外部 MCP 的版本、协议兼容性、超时行为都不在我们手里。 工具 schema 不可控 :外部 MCP 可能暴露几十个细粒度工具( page.click 、 page.evaluate 、 page.waitForSelector ……),直接打进主 agent 的 tool list 就违背了决策 4 。 内置一层封装的代价是我们要自己维护 MCP Client 和 daemon 的生命周期管理—— browser_manager.rb 里处理了 daemon 启动、心跳检测、超时、crash recovery 。但这个代价是一次性的工程投入,换来的是用户零配置(只要 Chrome 在跑)和工具列表的稳定。 最后,选择 Ruby 的理由 这不是一个显而易见的选择。LLM agent 生态里 Python 和 TypeScript 是主流,Ruby 几乎没有前例。但我们选 Ruby ,而且选对了。 动态语言 + 元编程 Ruby 的元编程能力是我们实现 Skill 自进化、动态加载、工具注册等能力的基础。 method_missing 、 define_method 、 class_eval 这些能力让运行时的行为修改非常自然。Python 也有类似能力,但 Ruby 在这一层的表达力明显更高。 对于一个"agent 自己可能改自己的辅助脚本"的系统来说,动态语言比静态语言更合适——你不需要重编译、不需要重启,改了就生效。 极致的分发能力 gem install openclacky 一行搞定。RubyGems 的分发链路非常成熟:版本管理、依赖解析、全局可执行文件注册( clacky 命令)都是开箱即用的。用户不需要 clone 仓库、不需要 npm install 、不需要 pip 虚拟环境。 对比 Python 的分发—— pip install + 虚拟环境 + 可能的 C 扩展编译——Ruby gem 的安装体验明显更丝滑。 零 C 库依赖 这是我们做了大量工程投入才做到的。看 openclacky.gemspec 的依赖列表: faraday, thor, tty-prompt, tty-spinner, diffy, pastel, tty-screen, tty-markdown, base64, logger, websocket, webrick, artii, rubyzip, rouge, chunky_png 全部是纯 Ruby gem , 没有一个需要编译 C 扩展 。 这意味着在 macOS / Linux 上,只要有 Ruby ( 2.6+), gem install openclacky 就能装上、立刻能跑。不需要 brew install libxml2 ,不需要 apt-get install libffi-dev ,不需要 Xcode Command Line Tools 。 为了做到这一点,我们做了一些反常规的选择: WebSocket :没有用 websocket-driver (需要 C 扩展做 UTF-8 校验),而是用了纯 Ruby 的 websocket gem 。性能差一点点,但对 agent 场景来说完全够用,换来的是安装零阻力。 LLM 接口调用 :完全零依赖,没有用任何第三方 LLM SDK ( anthropic-rb 、 ruby-openai 等都没用)。直接用 faraday 做 HTTP ,自己处理 streaming 、tool_use 协议、cache_control 注入。这样我们对请求格式有完全的控制权——决策 1 的双标记就是在 client.rb 里直接操作 cache_control 字段实现的。 TUI :没有用 curses ( C 扩展),直接用 tty-screen + ANSI escape code "画"出整个终端界面。 这一切是 AI Coding 的产物 说实话,"从零重写 WebSocket 客户端"、"从零实现 LLM streaming 协议"、"用 ANSI escape code 手画 TUI"——这些事情如果纯手写,工程量很大,这在以往完全不现实。 但 OpenClacky 本身就是一个 AI coding agent 。这些"为了极致安装体验而大胆从零重写依赖"的决策, 是用 OpenClacky 自己来完成的 。一个能写代码的 agent 让"零依赖"从不切实际变成了可执行。这是一个自举的过程——产品帮助自己变得更好。 结语 回头看这 7 个决策,它们背后其实只有一句话: 把工程预算花在 harness 上,把智能预算留给模型。 不做 RAG ,不做多 Agent 编排,不做工具堆叠——不是因为这些东西没用,而是因为模型在快速变好。半年前需要 4 个 agent 协作才能勉强通过的任务,今天一个 agent + 一个好的 harness 就能做得更快更便宜。 我们选择把精力放在那些 不会随模型进步而过时 的事情上:cache 命中率、工具稳定性、安装体验、压缩策略。这些是 harness 层面的基础设施,不管模型换到哪一代都用得上。 如果这篇对你有用,请帮我们点赞,欢迎 PR 。欢迎转发和分享。 OpenClacky 完全开源,MIT 协议: github.com/clacky-ai/openclacky gem install openclacky 一行装完即用,不需要 Docker 、不需要 clone 仓库。如果你也在做 Agent ,欢迎试试,遇到问题直接开 issue 聊。 4 家 Agent 横评的完整数据、产物对比、录像回放: openclacky.com/benchmark 本文引用的核心代码: Cache 标记 · Insert-then-Compress · Session context 注入 · 空闲压缩 · 浏览器工具