<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Ruby China 社区 Ruby 节点</title>
    <link>https://ruby-china.org/</link>
    <description>Ruby China 社区 Ruby 节点最新发帖。</description>
    <item>
      <title>终于等到！这个周末我去“玩”了一场纯聊技术的招聘专场</title>
      <description>&lt;p&gt;说真的，我已经受够了一次次投简历、做笔试题、等通知的循环。上周末朋友拉我去参加了一个线下技术沙龙，结果去了才发现——居然是一个大厂的“隐藏版”内推会！没有笔试，没有 HR 面，直接跟技术负责人聊项目、聊架构、聊你踩过的坑。
  我带着随便看看的心态，跟一个后端团队聊了四十分钟，从 Redis 集群聊到 Go 的 GC 优化，对方直接说“你下周来我们组试试”。那一刻真的觉得，技术人最爽的“面试”就是不面试，只有切磋。
现场还看到不少前端和测试的兄弟也拿到了意向，氛围比我想的热闹太多。
  正好，我最近发现一个靠谱渠道，也是这种“技术优先”的风格——技术大厂，前端 - 后端 - 测试，全国均有机会，待遇和稳定性都还不错~ 感兴趣可以试试：&lt;a href="https://jsj.top/f/o38ijj" rel="nofollow" target="_blank"&gt;https://jsj.top/f/o38ijj&lt;/a&gt;
如果你也厌倦了刷题刷到麻木，不如换个路子。反正填个信息也不亏，万一打开新机会呢？周末有空也可以多留意下身边这种线下局，有时候机会就藏在聊天的代码里。&lt;/p&gt;</description>
      <author>XXXXX</author>
      <pubDate>Fri, 05 Jun 2026 09:34:23 +0800</pubDate>
      <link>https://ruby-china.org/topics/44593</link>
      <guid>https://ruby-china.org/topics/44593</guid>
    </item>
    <item>
      <title>Ontology driven Agent：从“提示词工程（Prompt Engineering）”向“智能体软件工程（Agent Software Engineering）”</title>
      <description>&lt;p&gt;目前，我感觉全球的 agent 开发陷入了一个瓶颈期，虽然大家在做各种努力，但本质上仍然是提示词工程。&lt;/p&gt;

&lt;p&gt;我最近在看本体论，我发现将本体论（Ontology）加入到 agent 开发中来，能够彻底改变当前 agent 开发遇到的问题。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一、消除幻觉。提示词只能用自然语言“建议”模型去遵循规则，但 LLM 随时可能产生幻觉。本体论则是用严格的数据结构定义了业务的“物理法则”。&lt;/li&gt;
&lt;li&gt;二、动态能力路由与权限控制。当不同的用户或子 Agent 介入时，系统通过图谱查询动态计算出当前上下文应该拼装哪些 Tool 的 Schema，然后再转化为提示词发给模型。&lt;/li&gt;
&lt;li&gt;三、增强逻辑推理能力。通过推理机实现推理，而不是依赖于大语言模型。&lt;/li&gt;
&lt;li&gt;四、提示词无法可靠地控制复杂的先后顺序和错误恢复。本体定义了有向无环图 (DAG) 依赖关系，可以驱动工作流。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;还有：&lt;/p&gt;

&lt;p&gt;安全护栏、动态本体上下文裁剪、反思与自我纠错、分布式本体、多 agent、长期记忆、自适应演进、零代码 Agent、事件和时间、跨智能体协议、复杂任务、神经 - 符号混合推理等方面，在引入本体论后都有更优雅的实现方式。&lt;/p&gt;

&lt;p&gt;特别是：&lt;/p&gt;

&lt;p&gt;在编程 agent 中，将代码以某种形式本体化后，就可以实现代码、业务逻辑、开发流水线的有机融合，将会极大的提高编程 agent 的工程能力。&lt;/p&gt;

&lt;p&gt;另外补充：&lt;span class="embed-responsive embed-responsive-16by9"&gt;&lt;iframe class="embed-responsive-item" src="//player.bilibili.com/player.html?bvid=1ypVS6mETq" allowfullscreen=""&gt;&lt;/iframe&gt;&lt;/span&gt;/&lt;/p&gt;

&lt;p&gt;看了这个项目的解释视频（我也是看到这个视频的启发），虽然比较初步，但是可以看到将本体论引入编程 agent 的巨大潜力。&lt;/p&gt;</description>
      <author>shoushen</author>
      <pubDate>Fri, 05 Jun 2026 09:10:45 +0800</pubDate>
      <link>https://ruby-china.org/topics/44592</link>
      <guid>https://ruby-china.org/topics/44592</guid>
    </item>
    <item>
      <title>打听一个人 https://github.com/matzbot</title>
      <description>&lt;p&gt;who！&lt;/p&gt;</description>
      <author>zzz6519003</author>
      <pubDate>Wed, 03 Jun 2026 14:08:30 +0800</pubDate>
      <link>https://ruby-china.org/topics/44589</link>
      <guid>https://ruby-china.org/topics/44589</guid>
    </item>
    <item>
      <title>關於 spinel 的原理和侷限</title>
      <description>&lt;p&gt;今天和 claude 一起研究了一下 &lt;a href="https://github.com/matz/spinel" rel="nofollow" target="_blank"&gt;https://github.com/matz/spinel&lt;/a&gt; ，大概知道了它和別的靜態語言有什麼不同。&lt;/p&gt;

&lt;p&gt;spinel 的特點是需要對整個程序進行類型推斷，也就是一個方法的類型，實際上是由所有 call site 決定的。當看到一個方法調用的時候，分析器會把參數的類型記下來，然後把它疊到方法的定義上。例如一個方法&lt;code&gt;factorial(n)&lt;/code&gt;，當分析器看到&lt;code&gt;factorial(1)&lt;/code&gt; &lt;code&gt;factorial(2)&lt;/code&gt;的時候，它就知道 factorial 能接受 integer，如果整個程序都沒有傳別的類型，那麼它就能斷定 n 是 integer，然後可以輸出很高效的 c 代碼。但如果分析器之後看到 &lt;code&gt;factorial('1')&lt;/code&gt;，n 就會變成 integer | string，會需要額外處理多態，實際生成的 c 代碼就會包含這部分的處理。&lt;/p&gt;

&lt;p&gt;這樣做的後果是，整個程序如果變了一行，之前的推導就要全部重新來過，因為這個是基於&lt;strong&gt;整個程序&lt;/strong&gt;的推導。其他靜態語言由於會聲明函數的類型，推導對於函數體內和函數外是獨立的，因此一個函數在編譯了之後，如果簽名沒變就可以一直用，可以做到增量編譯。&lt;/p&gt;

&lt;p&gt;crystal 實際上也是用整個程序做推斷的，所以會有和 spinel 一樣的侷限，沒辦法做增量編譯。&lt;/p&gt;</description>
      <author>mizuhashi</author>
      <pubDate>Sat, 30 May 2026 05:20:38 +0800</pubDate>
      <link>https://ruby-china.org/topics/44588</link>
      <guid>https://ruby-china.org/topics/44588</guid>
    </item>
    <item>
      <title>用 Ruby 构建 AI Agent（更新第二篇：工具调用）</title>
      <description>&lt;p&gt;AI Agent（人工智能体）是指以 LLM（大语言模型）作为推理引擎，能够自主调用外部工具，规划并解决实际问题的程序。&lt;/p&gt;

&lt;p&gt;构建 AI Agent 已经成为目前最火热的开发领域。从构建通用 AI 助手，到传统应用引入 AI 功能，都需要用到构建 AI Agent 的知识。&lt;/p&gt;

&lt;p&gt;同时 AI Agent 又和传统软件有很大不同。传统软件需要程序员设计程序运转的完整流程，AI Agent 却要将思考外包给大语言模型，由大语言模型自主决定怎么做。构建 AI Agent 的过程，就好像给一个大脑安装五官和四肢。&lt;/p&gt;

&lt;p&gt;无论是为了业务需要，还是为了提升个人能力，学习构建 AI Agent 都会有所收益。&lt;/p&gt;
&lt;h2 id="为什么用 Ruby"&gt;为什么用 Ruby&lt;/h2&gt;
&lt;p&gt;在 LLM 训练领域，Ruby 可以说毫无存在感，那是 Python 和 C++ 的主场。构建 AI Agent 则回到了 Ruby 熟悉的领域——开发应用。&lt;/p&gt;

&lt;p&gt;AI Agent 最主要的两个操作是调用外部 API 和数据持久化，其实用什么语言开发都差不多。Ruby 的优势在于开发效率。&lt;/p&gt;

&lt;p&gt;下面是用 &lt;a href="https://rubyllm.com/" rel="nofollow" target="_blank" title=""&gt;RubyLLM&lt;/a&gt; 库调用大语言模型的最小例子：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"ruby_llm"&lt;/span&gt;

&lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Hello!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ruby 社区追求优雅代码的传统让 LLM 的库比别的语言更精简。&lt;/p&gt;

&lt;p&gt;如果要为已有的 Ruby 应用添加 AI 功能，那么用同样的语言开发可以减少技术栈的复杂度。&lt;/p&gt;

&lt;p&gt;接下来我们会逐步学习如何构建 AI Agent。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;目录：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://geeknote.net/Rei/posts/3288" rel="nofollow" target="_blank" title=""&gt;消息循环&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://geeknote.net/Rei/posts/3291" rel="nofollow" target="_blank" title=""&gt;工具调用&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>Rei</author>
      <pubDate>Tue, 26 May 2026 23:20:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/44585</link>
      <guid>https://ruby-china.org/topics/44585</guid>
    </item>
    <item>
      <title>在 Reddit 上发了一个 Ruby Agent 开发的帖子有点火了</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lyfi2003/e20b70b6-749f-4308-ba04-4a6bfaf1fefe.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/r/ruby/comments/1tnynx8/built_a_full_ai_agent_in_ruby_metaprogramming/" rel="nofollow" target="_blank"&gt;https://www.reddit.com/r/ruby/comments/1tnynx8/built_a_full_ai_agent_in_ruby_metaprogramming/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;大家有 reddit 账号的可以围观看下~ 另外，我们想在 hackernews 上也发一下但最近他们对账号有要求（要一直活跃才行），谁有合适的账号请求帮助啊&lt;/p&gt;</description>
      <author>lyfi2003</author>
      <pubDate>Tue, 26 May 2026 19:38:02 +0800</pubDate>
      <link>https://ruby-china.org/topics/44584</link>
      <guid>https://ruby-china.org/topics/44584</guid>
    </item>
    <item>
      <title>有没有用 rails+AI 做了审批流的功能模块或 gem，交流下经验</title>
      <description>&lt;p&gt;RT，感觉还可以
&lt;img src="https://l.ruby-china.com/photo/stephen/3bdeb57c-1d37-4e37-86f6-5bc6e8cad1c0.png!large" title="" alt=""&gt;&lt;/p&gt;</description>
      <author>stephen</author>
      <pubDate>Tue, 26 May 2026 08:03:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/44583</link>
      <guid>https://ruby-china.org/topics/44583</guid>
    </item>
    <item>
      <title>Ruby 4.0.4 修复了一个性能问题</title>
      <description>&lt;p&gt;我的网站在升级 Ruby 4.0 的时候遇到了性能问题，具体表现为，如果网站有大量的 html 内容需要 sanitize，那么在执行 2、3 次后网站就会卡死，cpu 100%。&lt;/p&gt;

&lt;p&gt;排查了很久自己的代码和依赖的 gem 也没解决，不得已只好回退 3.4。&lt;/p&gt;

&lt;p&gt;问题原因在这里：&lt;a href="https://bugs.ruby-lang.org/issues/21856" rel="nofollow" target="_blank"&gt;https://bugs.ruby-lang.org/issues/21856&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最近发布的 Ruby 4.0.4 已经修复了这个问题，我升级了网站不再出现卡死。有需要的可以升级测一测。&lt;/p&gt;</description>
      <author>Rei</author>
      <pubDate>Fri, 15 May 2026 16:04:29 +0800</pubDate>
      <link>https://ruby-china.org/topics/44573</link>
      <guid>https://ruby-china.org/topics/44573</guid>
    </item>
    <item>
      <title>Harness 工程经验分享：实现 100% 缓存命中 OpenClacky（RubyAgent）的 7 个关键决策</title>
      <description>&lt;p&gt;感谢社区朋友的支持，OpenClacky 的关注量从 200+ 马上破 400 了~ 用户活跃量已经翻倍。&lt;/p&gt;

&lt;p&gt;我把这二年的坑和 Ruby 重写 AIAgent 的思考放出来，大家一起看看离 ClaudeCode 这种顶级 Harness 工程还有多远。&lt;/p&gt;
&lt;h2 id="开篇"&gt;开篇&lt;/h2&gt;
&lt;p&gt;为了让新朋友重新了解一下我们的评测结果，我再列一下。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;成本极优&lt;/strong&gt;：3 项任务实测，4 家 Agent 横评，OpenRouter CSV 逐请求核算：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;总成本&lt;/th&gt;
&lt;th&gt;请求数&lt;/th&gt;
&lt;th&gt;Cache 命中率&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenClacky&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$5.10&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;51&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;90.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;$5.49&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;95.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenClaw&lt;/td&gt;
&lt;td&gt;$15.70&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes&lt;/td&gt;
&lt;td&gt;$30.14&lt;/td&gt;
&lt;td&gt;218&lt;/td&gt;
&lt;td&gt;60.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lyfi2003/d56b2b17-aa02-4746-8086-f60cf8dc1b26.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;完整数据和产物对比：&lt;a href="https://www.openclacky.com/benchmark" rel="nofollow" target="_blank" title=""&gt;openclacky.com/benchmark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;51 个请求 + 90.6% 命中率 → $5.10。218 个请求 + 60.3% 命中率 → $30.14。成本差距的直接原因就两个：&lt;strong&gt;请求数&lt;/strong&gt;和 &lt;strong&gt;cache 命中率&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;不要忘了，&lt;strong&gt;OpenClacky 是一个全功能 Agent&lt;/strong&gt;：WebUI + 命令行、长期记忆、Skill 技能库、定时任务、IM 接入（飞书/企微/微信）、浏览器自动化、子 Agent、运行时切模型、Skill 自进化与动态加载。&lt;/p&gt;

&lt;p&gt;而很多开源 Agent 也许有较好的 Token 消耗，或功能不全，或命中率不高。&lt;/p&gt;

&lt;p&gt;在实践中最大的问题是：&lt;strong&gt;这些功能里很多跟"高 cache 命中率"是结构性冲突的。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;举例：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;切模型 → 模型 ID 写在哪？写进 system prompt 就 cache 失效一次。&lt;/li&gt;
&lt;li&gt;中途装 skill → skill 列表写在哪？写进 system prompt 就 cache 失效一次。&lt;/li&gt;
&lt;li&gt;知道"今天日期" → 写进 system prompt？跨天就失效。&lt;/li&gt;
&lt;li&gt;加"读 PDF"能力 → 最容易的实现是再加一个工具 → 工具 schema 变了 → cache 失效面变大，模型选错工具的概率也变大。&lt;/li&gt;
&lt;li&gt;上下文不爆 → 最容易的做法是开一次独立 LLM call 做压缩 → 压缩本身 100% miss，压完之后主对话 cache 也凉了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;单看任意一头都不难做：少做功能，命中率自然高；不管账单，功能可以堆得很猛。难的是两头同时做。这篇文章讲我们在每个冲突点上具体怎么取舍。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lyfi2003/487c9cb4-86a4-495f-a7d9-14da638d52d0.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;效果已经不是当前 Agent 的主要矛盾，成本才是。&lt;/p&gt;
&lt;h2 id="起步：两年失败史"&gt;起步：两年失败史&lt;/h2&gt;
&lt;p&gt;第三代之前还有两代，失败的很严重。但我感觉现在还有很多人在踩坑，估计很多人有争议，但我 100% 站我自己的观点。&lt;/p&gt;
&lt;h3 id="第一代（2024-2025上半）：RAG / 知识库"&gt;第一代（2024-2025 上半）：RAG / 知识库&lt;/h3&gt;
&lt;p&gt;把用户 codebase、文档、历史会话全 embedding 进向量库，hybrid 检索 + 重排 + query rewrite。Agent 流程是"先查上下文，再答"。&lt;/p&gt;

&lt;p&gt;实际跑下来的问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;成本高，每次更新的 codebase，需要同步更新向量，实时性无法保证。&lt;/li&gt;
&lt;li&gt;准确率有限，例如听起来 90% 的召回率是不是还不错，但是对不起，不仅没有用，还可能有害，我预测，97% 的召回率可能才刚刚够用。&lt;/li&gt;
&lt;li&gt;多了一个会失败的部件（向量库），增加了很多延迟。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;结论：千万不要搞任何 RAG、知识库分片。如果你要上 Agent，请直接上 Agent，外加一个适合 AI 去阅读的网站就可以了。（参考我们自反思 Skill product-help 的实现）&lt;/p&gt;
&lt;h3 id="第二代（2025 中期）：SWEBench / 多 Agent 工作流"&gt;第二代（2025 中期）：SWEBench / 多 Agent 工作流&lt;/h3&gt;
&lt;p&gt;Planner / Coder / Reviewer / Tester 各一个 agent，消息总线 + 角色 prompt 编排。&lt;/p&gt;

&lt;p&gt;实际跑下来的问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每个 sub-agent 各有 system prompt，各有 cache 命名空间。Agent 间交接靠消息序列化状态，&lt;strong&gt;每次交接 = 一次 cache miss&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;一个单 agent 4 分钟能完成的任务，多 agent 编排到 14 分钟，成本 6×。&lt;/li&gt;
&lt;li&gt;SWEBench 分数能刷上去，但榜单跑分跟用户实际感受脱节得很厉害。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;结论：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;不要做工作流编排。&lt;/strong&gt; 多 Agent 在结构上就是 cache 灾难。人类的分工不对 AI 有任何价值。AI 是万能的。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;不要被 benchmark 绑架。&lt;/strong&gt; 模型每 6 个月跨一个台阶，用今天的二流模型 + 工作流堆出来的分数，会被半年后顶级模型 + 朴素 harness 直接抹平。把工程预算花在 harness 上，不要花在编排上。对 Agent 工程来说，Benchmark 本身也并不重要，一个朴素的 Agent 思想打败一切：站在 AI 的角度思考你的上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="第三代（2025 年底至今）"&gt;第三代（2025 年底至今）&lt;/h3&gt;
&lt;p&gt;Ruby 从零重写，4 个月。围绕"cache 局部性"和"工具集稳定性"来组织。后面讲的所有决策都属于这一代。&lt;/p&gt;
&lt;h2 id="核心决策 1：双 cache 标记 + 允许失败回退"&gt;核心决策 1：双 cache 标记 + 允许失败回退&lt;/h2&gt;
&lt;p&gt;OpenClacky 同时跑在 Claude / OpenAI 兼容这两条主线上，两边的 prompt cache 行为不同，但工程上我们只关心一个共性：&lt;strong&gt;cache 是按"前缀"匹配的——前缀里改一个字节，从那里往后全部失效&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;所以前缀的"层次"和"标记位置"，决定了你下一轮还能 hit 到哪里。我们把请求前缀分成几段考虑：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;session-stable 段&lt;/strong&gt;：system prompt、工具 schema。session 内绝不变。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;append-only 段&lt;/strong&gt;：历史消息。只追加、不修改。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;session-volatile 段&lt;/strong&gt;：当前轮新消息（用户输入、工具结果、模型回复）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;前两段交给"系统提示词层"的天然断点，后续每轮都能 hit。&lt;strong&gt;真正需要工程的是"append-only 段"——它每轮都在长尾部，标记打哪儿、打几个，决定了下一轮还认不认得它。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="朴素做法为什么不够"&gt;朴素做法为什么不够&lt;/h3&gt;
&lt;p&gt;最直觉的做法是"每轮在 messages 末尾打一个 marker"。它在以下场景都会失效：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;history 单调追加&lt;/strong&gt;：第 N 轮在 &lt;code&gt;messages[-1]&lt;/code&gt; 打 marker，第 N+1 轮 messages 又长了一条，原 marker 的位置内容已经不一样了——服务端找不到匹配，整段 history 上 cache miss。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;模型回退一次工具调用&lt;/strong&gt;：工具报错、用户 Ctrl-C 重试、或者模型自己决定换一种 tool call——这一刻"原本的最后一条"被丢弃，单 marker 直接作废。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;运行时切模型&lt;/strong&gt;：用户在 session 中途从 Sonnet 切到 Opus，请求路由到新 endpoint，最理想情况下我们希望两个模型共享尽可能多的前缀。任何不必要的 marker 抖动都会让"切换"成为新的 cache miss 事件。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们一开始就栽在 (1) 上。修复链能从 git log 里看出节奏：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8ff66cc fix: cache
6ea99fe fix: prompt cache
e9a3602 feat: prompt cache works fine
7734c97 feat: try 2 point cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前三个 commit 是逐步逼近，最后一个是结构性正解。&lt;/p&gt;
&lt;h3 id="双标记是怎么工作的"&gt;双标记是怎么工作的&lt;/h3&gt;
&lt;p&gt;每轮我们标 &lt;strong&gt;两条&lt;/strong&gt; 连续消息，不是一条：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;第 N 轮：    [..., msg_A, msg_B(*), msg_C(*)]
                                ↑       ↑
                          marker 1   marker 2

第 N+1 轮：  [..., msg_A, msg_B(*), msg_C(*), msg_D(*)]
                                ↑       ↑          ↑
                          (仍在)   (仍在)     新 marker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 N+1 轮发出请求时：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;服务端尝试匹配 &lt;code&gt;msg_C&lt;/code&gt; 的 marker → &lt;strong&gt;命中到 &lt;code&gt;msg_C&lt;/code&gt; 之前的所有内容&lt;/strong&gt;（system prompt + 工具 + 整段历史除最后一条）。&lt;/li&gt;
&lt;li&gt;我们在 &lt;code&gt;msg_D&lt;/code&gt; 上加新 marker，建立新的尾部断点供下一轮使用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这是一个滚动双缓冲：任何时刻都持有两个断点——一个"刚建立的"（写）和一个"上一轮建立的"（读）。下一轮把"读"再读一次，把"写"扔掉，再在新尾部写一个。&lt;strong&gt;永远不会出现两个 buffer 同时失效的瞬间。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="为什么是 2，不是 3 或 4"&gt;为什么是 2，不是 3 或 4&lt;/h3&gt;
&lt;p&gt;主流大模型的 cache 都允许多个标记位（上限不一），但&lt;strong&gt;更多并不更好&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每多一个 marker，那一轮就多一次 cache write，按写入费率收。&lt;/li&gt;
&lt;li&gt;双标记要解决的失败模式只有一个位置：&lt;strong&gt;"昔日尾部 / 今日尾部"这个边界&lt;/strong&gt;。两个 marker 正好覆盖。第三个 marker 落在更靠前的位置，对应的 cache 段在下一轮&lt;strong&gt;仍然会被前两个 marker 之一覆盖&lt;/strong&gt;——它写的是一段永远不会被独立读到的前缀。&lt;/li&gt;
&lt;li&gt;标记多了之后，部分 endpoint 上服务端的候选前缀匹配代价也会涨。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;简单说：2 是覆盖尾部边界的最小数量，3 多余，4 浪费。&lt;/p&gt;
&lt;h3 id="允许失败：单步回退仍然命中"&gt;允许失败：单步回退仍然命中&lt;/h3&gt;
&lt;p&gt;这是双标记的第二个好处，也是当时 &lt;code&gt;7734c97&lt;/code&gt; 的真正动机。&lt;/p&gt;

&lt;p&gt;模型偶尔需要回退一次 tool call：工具返回错误、用户 Ctrl-C 重试、或者上游 streaming 断了一半。这种情况下"昨天的最后一条"被丢弃了，但&lt;strong&gt;倒数第二个 marker 通常仍然落在仍存在的消息上&lt;/strong&gt;——单步回退后还能命中。&lt;/p&gt;

&lt;p&gt;单 marker 在回退时直接作废；双标记是能扛住单步回退的最小数量。我们没继续往上加（三标记也能扛两步回退，但成本不划算）——回退超过一步的概率已经低到可以接受全 miss 一次。&lt;/p&gt;
&lt;h3 id="模型切换：为什么要 marker 不动"&gt;模型切换：为什么要 marker 不动&lt;/h3&gt;
&lt;p&gt;OpenClacky 支持在 session 中途换模型。工程上要保证两件事：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;新模型的请求前缀和老模型尽量一致。&lt;/strong&gt; 我们不在 system prompt 里写当前模型 ID（写在 &lt;code&gt;[session context]&lt;/code&gt; 块里，见决策 2），换模型不动 system prompt。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;marker 位置不变。&lt;/strong&gt; 切完模型的下一轮，前两个 marker 落在和切换前完全相同的 message 上。新 endpoint 第一次请求会因为"换了上游账号 / 区域"产生一次 cache write，但&lt;strong&gt;前缀的几何结构是连续的&lt;/strong&gt;，warm-up 只发生一轮。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个细节不做的话，每次切模型一定要都要付完整 cache 重建的钱，用户会很不开心。&lt;/p&gt;
&lt;h3 id="不能标的位置"&gt;不能标的位置&lt;/h3&gt;
&lt;p&gt;marker 选择逻辑里有一条硬规则：&lt;strong&gt;跳过 &lt;code&gt;system_injected: true&lt;/code&gt; 的消息&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[session context]&lt;/code&gt; 块就是典型例子——它是一次性信息，下一轮尾部已经变了，落在它身上的 marker 是一笔永远读不回来的写入。压缩指令注入也是同样的处理（决策 5 会展开）。&lt;/p&gt;

&lt;p&gt;marker 选择从尾部往前走，&lt;code&gt;system_injected&lt;/code&gt; 的跳过，凑够两个真实对话消息为止。&lt;/p&gt;
&lt;h3 id="本节总结"&gt;本节总结&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;system prompt + 工具 schema：靠 system prompt 段的天然断点 hit。&lt;/li&gt;
&lt;li&gt;history 滚动：靠双标记。&lt;/li&gt;
&lt;li&gt;单步回退：靠双标记容错。&lt;/li&gt;
&lt;li&gt;模型切换：靠"动态信息不写进 system prompt"+ marker 位置不变。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;把这四件事同时做到，普通一轮的 cache 命中率才有可能稳定在 95%+。&lt;strong&gt;前三件是 cache 几何，第四件是设计纪律。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="决策 2：永不变的 system prompt"&gt;决策 2：永不变的 system prompt&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;OpenClacky 的 system prompt 在 session 启动时一次性构建，之后字节冻结。&lt;/strong&gt; 任何"想往 system prompt 里塞动态信息"的需求，必须重定向到别的位置。&lt;/p&gt;

&lt;p&gt;这条纪律是 cache 命中率的第一道地基——system prompt 一变，后面所有 cache 全废，没有任何"局部修补"能挽回。&lt;/p&gt;

&lt;p&gt;但日常跑下来，至少有四类信息"天然想插入到 system prompt"：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;当前时间、当前工作目录、操作系统&lt;/strong&gt;——模型需要这些来生成正确的命令和路径。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;当前模型 ID&lt;/strong&gt;——模型知道自己是谁有助于自适应行为。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;用户装了新 skill&lt;/strong&gt;——模型需要看到新的 skill 名称和描述才能调用。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;用户更新了 USER.md / SOUL.md&lt;/strong&gt;——agent 的人格和用户偏好发生了变化。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这四类信息都是"session 中途可能变"的。如果写进 system prompt，任何一次变更都意味着全量 cache 失效。&lt;/p&gt;
&lt;h3 id="[session context] 块"&gt;[session context] 块&lt;/h3&gt;
&lt;p&gt;我们的做法是把这些信息写进 message 流，而非 system prompt。每当环境发生模型需要感知的变化时（跨天、切模型、切工作目录），agent 在 history 里追加一条 &lt;code&gt;user&lt;/code&gt; 角色的消息：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Session context: Today is 2026-05-13, Tuesday. Current model: claude-sonnet-4-6.
OS: macOS. Working directory: /Users/.../project]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条消息被标记为 &lt;code&gt;system_injected: true&lt;/code&gt;。它不会被 cache marker 选中（决策 1 已经讲过），不会被算作真实用户轮数，压缩时也不会被原样搬进新历史。&lt;/p&gt;

&lt;p&gt;注入是按日期 gate 的：同一天内只注入一条。跨天了，插一条新的。切了模型，插一条新的。大多数 session 里你只会看到一条 session context 块。&lt;/p&gt;
&lt;h3 id="这个设计踩过的坑"&gt;这个设计踩过的坑&lt;/h3&gt;
&lt;p&gt;第一版 &lt;code&gt;inject_session_context&lt;/code&gt; 是在 agent 构造期就急切注入的。结果 &lt;code&gt;@history.empty?&lt;/code&gt; 返回 false，&lt;code&gt;run()&lt;/code&gt; 误以为是后续轮，跳过了 system prompt 的构建——&lt;strong&gt;第一次请求带着一条"today is Tuesday"但没有 system prompt 就发出去了&lt;/strong&gt;。agent 的行为诡异了大约一天才定位到。&lt;/p&gt;

&lt;p&gt;修复只有一行：等 system prompt 构建完毕之后再注入。代码里有一段注释记录了这个约束：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# IMPORTANT: Skip injection when the system prompt hasn't been built yet.&lt;/span&gt;
&lt;span class="c1"&gt;# Otherwise, appending a user message to an empty history makes&lt;/span&gt;
&lt;span class="c1"&gt;# @history.empty? false, which causes run() to skip building the&lt;/span&gt;
&lt;span class="c1"&gt;# system prompt entirely.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;教训是：&lt;strong&gt;前缀的组装顺序比前缀的内容更要紧。&lt;/strong&gt; 你可以花大力气设计每一段的内容，但只要组装顺序错一步，整个 cache 策略就是废的。&lt;/p&gt;
&lt;h3 id="Skill 列表怎么处理"&gt;Skill 列表怎么处理&lt;/h3&gt;
&lt;p&gt;Skill 列表是最容易跟"永不变的 system prompt"冲突的需求。用户可以随时装新 skill，模型需要看到 skill 名和描述才能通过 &lt;code&gt;invoke_skill&lt;/code&gt; 去调用它。&lt;/p&gt;

&lt;p&gt;我们的取舍：&lt;strong&gt;skill 列表在 session 启动时渲染进 system prompt，之后冻结。&lt;/strong&gt; session 中途装的新 skill，模型在当前 session 里看不到——它会看到一条 &lt;code&gt;[session context]&lt;/code&gt; 通告说"skill 列表已更新，新 skill 从下一个 session 可用"。&lt;/p&gt;

&lt;p&gt;这意味着用户装完 skill 想立刻用会发现用不了，要开新 session。我们接受这个摩擦，因为替代方案是重渲染 system prompt 导致全量 cache 失效——这个代价打到所有用户的所有 session 的每一轮上。装 skill 是低频操作，cache 命中是每轮都在享受的收益，取舍方向很清楚。&lt;/p&gt;

&lt;p&gt;USER.md / SOUL.md 的更新也是同样的处理：session 启动时读取，session 内不再变。&lt;/p&gt;

&lt;p&gt;但是，在用户体验上，我们虽然降低了一些 Skill 发现的概率，但一旦用户主动提起新的 skill 时，我们系统仍能及时发现新 Skill。没有任何缓存，每次都会重建 Skill 列表。&lt;/p&gt;
&lt;h2 id="决策 3：invoke_skill 的妙用"&gt;决策 3：invoke_skill 的妙用&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;invoke_skill&lt;/code&gt; 是 OpenClacky 的 16 个工具之一，它是整个 OpenClacky 最核心的设计，花费的时间也最多，它提供 Skill 热加载能力，子 Agent 架构支持，记忆召回能力、Skill 进化能力，但它只占 system prompt 不超过 200 个 Token。&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;启动子 agent&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;子 agent 用的工具集跟主 agent 完全相同&lt;/strong&gt;（16 个）。它不是一个"精简版"，它能做主 agent 能做的一切事情。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;子 agent 执行完后，把结果文本返回给主 agent&lt;/strong&gt;，主 agent 的 history 里只看到"invoke_skill → 结果"这一对消息。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个设计一口气解决了好几个问题：&lt;/p&gt;
&lt;h3 id="子 agent = 状态隔离"&gt;子 agent = 状态隔离&lt;/h3&gt;
&lt;p&gt;做代码审查的 skill 可能需要读几十个文件、跑 grep、输出长篇分析。如果这些中间步骤都在主 agent 的 history 里，history 会膨胀得很快——cache 命中率没变，但上下文总量上去了，压缩触发得更早，成本更高。&lt;/p&gt;

&lt;p&gt;子 agent 把这些中间过程隔离在自己的 session 里。主 agent 只看到最终结论。&lt;strong&gt;主 agent 的 history 没有被污染。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="动态加载 Skill，不改 system prompt"&gt;动态加载 Skill，不改 system prompt&lt;/h3&gt;
&lt;p&gt;装新 skill 的流程就是把一个 SKILL.md 放到 &lt;code&gt;~/.clacky/skills/&amp;lt;name&amp;gt;/&lt;/code&gt; 或 &lt;code&gt;.clacky/skills/&amp;lt;name&amp;gt;/&lt;/code&gt; 下。skill 列表渲染进 system prompt 的时间点是 session 启动，决策 2 已经讲过。&lt;/p&gt;

&lt;p&gt;但 &lt;code&gt;invoke_skill&lt;/code&gt; 这个工具本身是&lt;strong&gt;始终存在&lt;/strong&gt;的——它不需要 system prompt 里列出所有 skill 才能调用。模型可以通过 &lt;code&gt;[session context]&lt;/code&gt; 通告知道新 skill 的名称，然后直接 &lt;code&gt;invoke_skill(skill_name: "xxx")&lt;/code&gt;。Skill 的 SKILL.md 是在调用那一刻才读取的，不是预编译进 system prompt 的。&lt;/p&gt;

&lt;p&gt;所以"动态加载 skill"这个能力，实际上是 &lt;code&gt;invoke_skill&lt;/code&gt; 的运行时读取 + &lt;code&gt;[session context]&lt;/code&gt; 的通告组合出来的。&lt;strong&gt;不需要改 system prompt，不需要改工具列表，不需要重启 session。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="Skill 注入与路径处理"&gt;Skill 注入与路径处理&lt;/h3&gt;
&lt;p&gt;每个 skill 的 SKILL.md 可以引用相对路径的资源文件（模板、配置等）。&lt;code&gt;invoke_skill&lt;/code&gt; 在启动子 agent 之前会把 skill 的目录作为上下文路径注入，子 agent 能用 &lt;code&gt;file_reader&lt;/code&gt;、&lt;code&gt;glob&lt;/code&gt; 直接读到 skill 附带的资源。&lt;/p&gt;

&lt;p&gt;这让 skill 可以做到"自包含"——一个 skill zip 包里既有指令又有模板，装上就能用。&lt;/p&gt;
&lt;h3 id="加密 Skill 与选择性落盘"&gt;加密 Skill 与选择性落盘&lt;/h3&gt;
&lt;p&gt;部分 skill 包含商业敏感内容（客户的 prompt 策略、内部流程等）。OpenClacky 支持对 SKILL.md 做加密存储，运行时解密到内存、用完不落盘。同时 session 的落盘也是选择性的——对于涉及加密 skill 的 session，可以配置为不持久化到磁盘，只在内存中存在。&lt;/p&gt;

&lt;p&gt;这不是 cache 工程的范畴，但它是 &lt;code&gt;invoke_skill&lt;/code&gt; 架构的延伸：因为子 agent 的状态是隔离的，选择性不落盘可以精确到某次 skill 调用，而不需要把整个 session 的落盘关掉。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="决策 4：控制稳定可靠的工具集 16个"&gt;决策 4：控制稳定可靠的工具集 16 个&lt;/h2&gt;
&lt;p&gt;工具 schema 紧贴 system prompt 之后，在 cache 前缀里。schema 一变，后面全失效。这意味着：&lt;strong&gt;每多加一个工具，你不只是多了一份 schema 的 token 成本，你还多了一份"下次改工具时全量 cache 失效"的风险面。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;另一面，工具太少也有代价：模型本来一步能做完的事，现在要分两三步（先调一个通用工具获取信息，再调另一个来操作），轮次上去了，每轮都要付 cache 和 output 的钱。&lt;/p&gt;

&lt;p&gt;所以这不是一个"越少越好"的问题，而是一个&lt;strong&gt;经验平衡点&lt;/strong&gt;。我们的答案是 &lt;strong&gt;16 个&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="这 16 个分别是什么"&gt;这 16 个分别是什么&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;类别&lt;/th&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;文件读写&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;file_reader&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;读、写、搜索替换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码搜索&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;glob&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;文件查找 + 内容搜索&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;terminal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shell 命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浏览器&lt;/td&gt;
&lt;td&gt;&lt;code&gt;browser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;接管 Chrome/Edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;网络&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;web_search&lt;/code&gt;, &lt;code&gt;web_fetch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;搜索 + 抓取网页内容&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;任务管理&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;todo_manager&lt;/code&gt;, &lt;code&gt;list_tasks&lt;/code&gt;, &lt;code&gt;undo_task&lt;/code&gt;, &lt;code&gt;redo_task&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;规划、撤销、重做&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;交互&lt;/td&gt;
&lt;td&gt;&lt;code&gt;request_user_feedback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;需要用户输入时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;扩展&lt;/td&gt;
&lt;td&gt;&lt;code&gt;invoke_skill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;调用 skill（决策 3）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安全&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trash_manager&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;安全删除（rm → trash）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="设计原则"&gt;设计原则&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;简化参数。&lt;/strong&gt; 每个工具的参数尽量少、语义尽量明确。比如 &lt;code&gt;glob&lt;/code&gt; 只要 &lt;code&gt;pattern&lt;/code&gt; 和 &lt;code&gt;base_path&lt;/code&gt;，不需要模型去组合 &lt;code&gt;--include&lt;/code&gt; / &lt;code&gt;--exclude&lt;/code&gt; / &lt;code&gt;--type&lt;/code&gt; 这些 flag。参数越多，模型出错的概率越高，出错就要重试，重试就是成本。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;够用但不冗余。&lt;/strong&gt; &lt;code&gt;glob&lt;/code&gt; 和 &lt;code&gt;grep&lt;/code&gt; 是两个工具而不是一个：&lt;code&gt;glob&lt;/code&gt; 负责"哪些文件匹配"，&lt;code&gt;grep&lt;/code&gt; 负责"文件里哪些行匹配"。合成一个会让参数变复杂，模型调错的概率上升。但也没有继续拆成 &lt;code&gt;find_files&lt;/code&gt; / &lt;code&gt;list_dir&lt;/code&gt; / &lt;code&gt;tree&lt;/code&gt; 三个——&lt;code&gt;glob&lt;/code&gt; 一个就能覆盖这三个场景。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为每个工具写丰富的测试用例。&lt;/strong&gt; 工具是 agent 跟外部世界的接口，一个工具出 bug 的代价远高于普通代码出 bug——它会让模型产生错误的观察，进而做出错误的决策，进而需要更多轮次来纠正。我们一共有 1600+ 的用例去覆盖各种场景的处理。最近有朋友给我们提交了子项目扫描慢（对，OpenClacky 支持子项目处理）的一个相关优化 issue。&lt;/p&gt;
&lt;h3 id="为什么不是 10 个，也不是 25 个"&gt;为什么不是 10 个，也不是 25 个&lt;/h3&gt;
&lt;p&gt;10 个做不到。&lt;code&gt;undo_task&lt;/code&gt; / &lt;code&gt;redo_task&lt;/code&gt; / &lt;code&gt;list_tasks&lt;/code&gt; 这些看起来"可以不要"的工具，拿掉之后模型就只能用 &lt;code&gt;terminal&lt;/code&gt; 跑 &lt;code&gt;git&lt;/code&gt; 来处理代码回滚——成功率远低于专用工具，而且 &lt;code&gt;git&lt;/code&gt; 操作的副作用很难控制。很多工具设计了一个 code_run，我们并不推荐，实测反而导致任务变慢（需要写长代码），轮次变多（多次尝试）。&lt;/p&gt;

&lt;p&gt;不需要 40+，只需要 16 个。&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;省掉的能力&lt;/th&gt;
&lt;th&gt;替代方式&lt;/th&gt;
&lt;th&gt;工具数节省&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码库分析专用工具&lt;/td&gt;
&lt;td&gt;code-explorer Skill&lt;/td&gt;
&lt;td&gt;~5 个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;记忆读写专用工具&lt;/td&gt;
&lt;td&gt;recall-memory Skill&lt;/td&gt;
&lt;td&gt;~3 个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;浏览器自动化（多动作拆分为多工具）&lt;/td&gt;
&lt;td&gt;单一 browser 工具统一覆盖&lt;/td&gt;
&lt;td&gt;~8 个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-agent 编排工具&lt;/td&gt;
&lt;td&gt;invoke_skill 统一入口&lt;/td&gt;
&lt;td&gt;~6 个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;定时任务管理工具&lt;/td&gt;
&lt;td&gt;cron-task-creator Skill&lt;/td&gt;
&lt;td&gt;~4 个&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;如果以后需要第 17 个，我们会加。4 个月了，还没加。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="决策 5：压缩——不换模型、空闲时做、压到底"&gt;决策 5：压缩——不换模型、空闲时做、压到底&lt;/h2&gt;
&lt;p&gt;上下文窗口是有限的。不管 200K 还是 1M，长任务跑下来总会填满。填满之前必须压缩，否则要么截断丢信息，要么溢出直接报错。&lt;/p&gt;

&lt;p&gt;压缩是 cache 命中率最大的单点威胁：老的消息被替换成一段摘要，前缀从那一刻起就跟之前不一样了——&lt;strong&gt;必然 cache miss&lt;/strong&gt;。但压缩不可避免，所以问题不是"要不要压"，而是"怎么把压缩的破坏降到最低"。&lt;/p&gt;
&lt;h3 id="结论一：不要换模型压缩"&gt;结论一：不要换模型压缩&lt;/h3&gt;
&lt;p&gt;很多 agent 的压缩流程是开一个&lt;strong&gt;独立的 LLM call&lt;/strong&gt;，用一个便宜/快速的小模型来做摘要。&lt;/p&gt;

&lt;p&gt;问题：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;独立 call 的 system prompt 跟主 session 不一样（通常是"你是一个摘要助手"），&lt;strong&gt;跟主 session 的 cache 没有任何共享前缀&lt;/strong&gt;，压缩本身就是一次 100% cache miss。&lt;/li&gt;
&lt;li&gt;压缩完之后，主 session 的 history 被替换了（老消息变成了摘要），主 session 的 cache 也跟着失效——接下来 4–5 轮跑在 cold 费率上。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;等于你为每次压缩付了&lt;strong&gt;两笔钱&lt;/strong&gt;：一笔给压缩 call 本身的 cache miss，一笔给主 session 压缩后的 cold-warm 阶段。&lt;/p&gt;

&lt;p&gt;我们的做法：&lt;strong&gt;压缩不开独立 call，而是把压缩指令作为一条消息插进当前对话的末尾&lt;/strong&gt;（Insert-then-Compress）。&lt;/p&gt;

&lt;p&gt;这条指令被打上 &lt;code&gt;system_injected: true&lt;/code&gt;，走正常请求路径。效果：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;压缩 call 命中现有 cache&lt;/strong&gt;：同样的 system prompt、同样的 tools、同样的 history 前缀。只有尾部的压缩指令是 cold 的，几百 token。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;压缩完成后，重建 history&lt;/strong&gt;：&lt;code&gt;[system_prompt, summary, last_N_messages]&lt;/code&gt;。这一刻 cache 确实会 miss 一次——但只 miss 一轮，从第二轮开始双标记重新接管。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对比（一次 50K-token 会话的压缩事件）：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;独立 call 方案&lt;/th&gt;
&lt;th&gt;Insert-then-Compress&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;压缩 call 的 cache hit&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;~95%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;压缩期间 cold token&lt;/td&gt;
&lt;td&gt;~50,000&lt;/td&gt;
&lt;td&gt;~500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;主 session cold-warm 轮数&lt;/td&gt;
&lt;td&gt;4–5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="结论二：20–30 万 token 是压缩的甜区"&gt;结论二：20–30 万 token 是压缩的甜区&lt;/h3&gt;
&lt;p&gt;太早压：浪费了上下文里还有价值的细节，摘要丢信息。
太晚压：上下文太长导致模型注意力分散、推理变慢、输出质量下降。&lt;/p&gt;

&lt;p&gt;我们测过多个阈值。20–30 万 token 是效果和成本的甜区——模型还能有效利用上下文，但离溢出还有足够余量来完成压缩本身。&lt;/p&gt;

&lt;p&gt;压缩后&lt;strong&gt;无论如何会压到 1 万 token 以内&lt;/strong&gt;。这不是省钱，这是控制后续每一轮的 baseline 成本——history 越短，每轮 input 越少，cache miss 时的惩罚也越小。&lt;/p&gt;
&lt;h3 id="结论三：空闲第 3 分钟启动压缩"&gt;结论三：空闲第 3 分钟启动压缩&lt;/h3&gt;
&lt;p&gt;这是跟 cache TTL 的博弈。大模型厂商的 prompt cache 普遍有 TTL——&lt;strong&gt;cache 在一段时间无请求后会过期&lt;/strong&gt;。过期之后下一轮的 input 是全量 cold，直接翻到 10× 成本。而且后续每轮都在叠加成本，直到 cache 重新 warm 起来。&lt;/p&gt;

&lt;p&gt;所以我们跑了一个空闲计时器（&lt;code&gt;idle_compression_timer.rb&lt;/code&gt;）：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户停止输入 &lt;strong&gt;90 秒&lt;/strong&gt;后开始检查。&lt;/li&gt;
&lt;li&gt;如果 history 已经接近压缩阈值 → &lt;strong&gt;立刻触发压缩&lt;/strong&gt;。此时 cache 还是热的，压缩代价很低。&lt;/li&gt;
&lt;li&gt;压缩完之后，新的短 history 在 TTL 过期前就建立了新的 cache 断点。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;效果是：用户思考了几分钟回来，看到的是一个&lt;strong&gt;已经压缩好、cache 已经 warm&lt;/strong&gt;的 session。相比之下，如果不做空闲压缩，用户回来时面对的是一个 cache 过期的长 history——那一轮的 input 可能是 30 万 token 全量付费。&lt;strong&gt;单这一个行为，在长思考间隔的场景下就能省 10× 的钱。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;空闲计时器跑在后台线程里。记得加锁！&lt;/p&gt;
&lt;h3 id="百万上下文的真相"&gt;百万上下文的真相&lt;/h3&gt;
&lt;p&gt;"百万 token 上下文"听起来很性感，但做 agent 有两个现实：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;过长的上下文对模型效果并不总是正面的。&lt;/strong&gt; 模型在超长上下文里的注意力分散问题是已知的——关键信息被淹没在大量历史里，输出质量反而下降。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;你真不一定用得起。&lt;/strong&gt; 记住，模型每轮都要把上一轮所有的上下文全部带上。100 万 token 的 input，即使全部 cache hit（0.1× 费率），一轮也要付 10 万 token 等价的钱。如果 cache miss 了一次，那就是 100 万 token 全价。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;真实世界用户停下来思考太过于常见，Cache Missing 太容易发生，Agent 开发者必须想办法帮用户减少开销。&lt;/p&gt;

&lt;p&gt;所以我们的策略不是"尽量用满上下文"，而是"&lt;strong&gt;积极压缩，保持 history 短小&lt;/strong&gt;"。1 万 token 的压缩后 history + 95% cache hit，比 100 万 token 的未压缩 history + 99% cache hit 便宜得多，效果也更可控。&lt;/p&gt;

&lt;p&gt;如何确保压缩后仍然保证足够好的效果，这是另一个话题，我们后面展开。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="决策 6：自进化的工具能力"&gt;决策 6：自进化的工具能力&lt;/h2&gt;
&lt;p&gt;PDF、Excel、Word、PPT 的阅读和解析是 Agent 经常遇到的需求。处理这类文件通常有两种路径：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;内置一个 tool&lt;/strong&gt;：比如 &lt;code&gt;read_pdf&lt;/code&gt;、&lt;code&gt;read_excel&lt;/code&gt;。好处是开箱即用，坏处是每个格式一个工具，工具列表膨胀（违背决策 4），而且解析库的依赖链往往需要 C 扩展，装起来就不"零痛"了。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;做成 skill 让用户装&lt;/strong&gt;：对用户来说不友好——遇到一个 PDF 还得先去装 skill，体验断裂。而且 skill 描述怎么写、什么时候触发，AI 效果不可控。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们选了第三种路径：&lt;strong&gt;首次安装时把预设的文档处理脚本 copy 到用户目录，之后允许 AI 自行更新维护这些脚本。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;具体做法：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;首装 OpenClacky 时，&lt;code&gt;onboard&lt;/code&gt; skill 会把一组 Python 脚本（PDF 解析、Excel 读取、OCR 等）copy 到 &lt;code&gt;~/.clacky/scripts/&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这些脚本不是 Ruby，而是 &lt;strong&gt;Python 3&lt;/strong&gt;。原因很实际：Python 的文档处理生态（&lt;code&gt;pdfplumber&lt;/code&gt;、&lt;code&gt;openpyxl&lt;/code&gt;、&lt;code&gt;python-docx&lt;/code&gt;、&lt;code&gt;python-pptx&lt;/code&gt;）是当前最成熟的，OCR 方面 &lt;code&gt;pytesseract&lt;/code&gt; / &lt;code&gt;paddleocr&lt;/code&gt; 也远比 Ruby 生态完善。&lt;/li&gt;
&lt;li&gt;当 agent 需要读一个 PDF 时，它不调一个专用 tool——它用 &lt;code&gt;terminal&lt;/code&gt; 工具跑 &lt;code&gt;python3 ~/.clacky/scripts/read_pdf.py &amp;lt;file&amp;gt;&lt;/code&gt;。&lt;strong&gt;工具列表没有增加。&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;如果脚本跑不过去（缺依赖、格式变了），agent 可以直接 &lt;code&gt;write&lt;/code&gt; 修改脚本、&lt;code&gt;terminal&lt;/code&gt; 跑 &lt;code&gt;pip install&lt;/code&gt; 装依赖。下次再遇到同类文件就不会出问题了。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这就是"自进化"的含义：&lt;strong&gt;处理文档的能力不是写死在 gem 里的，它活在用户目录的脚本里，agent 自己可以维护。&lt;/strong&gt; 第一次可能需要装个 &lt;code&gt;pdfplumber&lt;/code&gt;，装完之后就是永久能力。&lt;/p&gt;

&lt;p&gt;这个设计把"文档处理"从工具层面拉到了脚本层面，避免了工具列表膨胀，也避免了硬编码 C 扩展依赖。trade-off 是用户机器上需要有 Python 3——但 macOS 和大多数 Linux 发行版默认自带，这个前提在实际用户群里几乎都满足。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="决策 7：内置浏览器工具，No Headless"&gt;决策 7：内置浏览器工具，No Headless&lt;/h2&gt;
&lt;p&gt;浏览器自动化是 Agent 越来越重要的能力——验证前端改动、抓取文档、自动化测试流程。&lt;/p&gt;

&lt;p&gt;市面上主流的做法有两种：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Headless 浏览器&lt;/strong&gt;（Puppeteer / Playwright）：agent 启一个无头浏览器实例，完全在后台跑。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;外接 MCP&lt;/strong&gt;：通过 MCP 协议连接一个外部浏览器服务，agent 发 JSON-RPC 指令。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们两种都不用，或者说——&lt;strong&gt;我们自己内置了一个 MCP Client，去接管用户已经在跑的 Chrome / Edge&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="为什么不用 Headless"&gt;为什么不用 Headless&lt;/h3&gt;
&lt;p&gt;Headless 浏览器的问题是"看不见"。agent 操作的页面用户看不到、不知道 agent 在干什么、出了问题也无法判断。对于 Agent 的使用场景——用户在旁边盯着 agent 干活——"看不见"是很大的信任问题。&lt;/p&gt;

&lt;p&gt;另外，Headless 经常遇到反爬检测：登录态拿不到、Cloudflare challenge 过不去、需要手动验证。用户自己的浏览器里已经登录好了、cookie 都在，为什么不直接用？&lt;/p&gt;
&lt;h3 id="我们怎么做的"&gt;我们怎么做的&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lib/clacky/tools/browser.rb&lt;/code&gt;（610 行）+ &lt;code&gt;lib/clacky/server/browser_manager.rb&lt;/code&gt; 是整套实现。架构是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用户的 Chrome / Edge 开启 Remote Debugging 端口（一次性配置，&lt;code&gt;browser-setup&lt;/code&gt; skill 引导完成）。&lt;/li&gt;
&lt;li&gt;OpenClacky 内置一个 &lt;strong&gt;MCP Client&lt;/strong&gt;，通过 stdio JSON-RPC 2.0 连接 &lt;code&gt;chrome-devtools-mcp&lt;/code&gt; 这个 daemon。&lt;/li&gt;
&lt;li&gt;daemon 进程首次调用时启动，后续跨多次 tool call 保持存活。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;browser&lt;/code&gt; 工具对外暴露的是高层语义动作：&lt;code&gt;snapshot&lt;/code&gt;、&lt;code&gt;click&lt;/code&gt;、&lt;code&gt;type&lt;/code&gt;、&lt;code&gt;navigate&lt;/code&gt;、&lt;code&gt;screenshot&lt;/code&gt; 等——不是底层 CDP 指令。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;对模型来说，"浏览器"就是 16 个工具里的 1 个，schema 跟其他工具一样稳定，不会因为浏览器的状态变化而改 schema。&lt;/strong&gt; 这符合决策 4 的原则。&lt;/p&gt;
&lt;h3 id="为什么不把浏览器做成外部 MCP"&gt;为什么不把浏览器做成外部 MCP&lt;/h3&gt;
&lt;p&gt;我们可以不内置浏览器、让用户自己配一个 Browser MCP 服务。但这样做的问题是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;用户体验差&lt;/strong&gt;：装 agent 之外还要装 MCP 服务、配端口、配认证。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;稳定性不可控&lt;/strong&gt;：外部 MCP 的版本、协议兼容性、超时行为都不在我们手里。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;工具 schema 不可控&lt;/strong&gt;：外部 MCP 可能暴露几十个细粒度工具（&lt;code&gt;page.click&lt;/code&gt;、&lt;code&gt;page.evaluate&lt;/code&gt;、&lt;code&gt;page.waitForSelector&lt;/code&gt;……），直接打进主 agent 的 tool list 就违背了决策 4。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;内置一层封装的代价是我们要自己维护 MCP Client 和 daemon 的生命周期管理——&lt;code&gt;browser_manager.rb&lt;/code&gt; 里处理了 daemon 启动、心跳检测、超时、crash recovery。但这个代价是一次性的工程投入，换来的是用户零配置（只要 Chrome 在跑）和工具列表的稳定。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="最后，选择 Ruby 的理由"&gt;最后，选择 Ruby 的理由&lt;/h2&gt;
&lt;p&gt;这不是一个显而易见的选择。LLM agent 生态里 Python 和 TypeScript 是主流，Ruby 几乎没有前例。但我们选 Ruby，而且选对了。&lt;/p&gt;
&lt;h3 id="动态语言 + 元编程"&gt;动态语言 + 元编程&lt;/h3&gt;
&lt;p&gt;Ruby 的元编程能力是我们实现 Skill 自进化、动态加载、工具注册等能力的基础。&lt;code&gt;method_missing&lt;/code&gt;、&lt;code&gt;define_method&lt;/code&gt;、&lt;code&gt;class_eval&lt;/code&gt; 这些能力让运行时的行为修改非常自然。Python 也有类似能力，但 Ruby 在这一层的表达力明显更高。&lt;/p&gt;

&lt;p&gt;对于一个"agent 自己可能改自己的辅助脚本"的系统来说，动态语言比静态语言更合适——你不需要重编译、不需要重启，改了就生效。&lt;/p&gt;
&lt;h3 id="极致的分发能力"&gt;极致的分发能力&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;gem install openclacky&lt;/code&gt; 一行搞定。RubyGems 的分发链路非常成熟：版本管理、依赖解析、全局可执行文件注册（&lt;code&gt;clacky&lt;/code&gt; 命令）都是开箱即用的。用户不需要 &lt;code&gt;clone&lt;/code&gt; 仓库、不需要 &lt;code&gt;npm install&lt;/code&gt;、不需要 &lt;code&gt;pip&lt;/code&gt; 虚拟环境。&lt;/p&gt;

&lt;p&gt;对比 Python 的分发——&lt;code&gt;pip install&lt;/code&gt; + 虚拟环境 + 可能的 C 扩展编译——Ruby gem 的安装体验明显更丝滑。&lt;/p&gt;
&lt;h3 id="零 C 库依赖"&gt;零 C 库依赖&lt;/h3&gt;
&lt;p&gt;这是我们做了大量工程投入才做到的。看 &lt;code&gt;openclacky.gemspec&lt;/code&gt; 的依赖列表：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;faraday, thor, tty-prompt, tty-spinner, diffy, pastel,
tty-screen, tty-markdown, base64, logger, websocket,
webrick, artii, rubyzip, rouge, chunky_png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全部是纯 Ruby gem，&lt;strong&gt;没有一个需要编译 C 扩展&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;这意味着在 macOS / Linux 上，只要有 Ruby（2.6+），&lt;code&gt;gem install openclacky&lt;/code&gt; 就能装上、立刻能跑。不需要 &lt;code&gt;brew install libxml2&lt;/code&gt;，不需要 &lt;code&gt;apt-get install libffi-dev&lt;/code&gt;，不需要 Xcode Command Line Tools。&lt;/p&gt;

&lt;p&gt;为了做到这一点，我们做了一些反常规的选择：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket&lt;/strong&gt;：没有用 &lt;code&gt;websocket-driver&lt;/code&gt;（需要 C 扩展做 UTF-8 校验），而是用了纯 Ruby 的 &lt;code&gt;websocket&lt;/code&gt; gem。性能差一点点，但对 agent 场景来说完全够用，换来的是安装零阻力。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM 接口调用&lt;/strong&gt;：完全零依赖，没有用任何第三方 LLM SDK（&lt;code&gt;anthropic-rb&lt;/code&gt;、&lt;code&gt;ruby-openai&lt;/code&gt; 等都没用）。直接用 &lt;code&gt;faraday&lt;/code&gt; 做 HTTP，自己处理 streaming、tool_use 协议、cache_control 注入。这样我们对请求格式有完全的控制权——决策 1 的双标记就是在 &lt;code&gt;client.rb&lt;/code&gt; 里直接操作 cache_control 字段实现的。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TUI&lt;/strong&gt;：没有用 &lt;code&gt;curses&lt;/code&gt;（C 扩展），直接用 &lt;code&gt;tty-screen&lt;/code&gt; + ANSI escape code "画"出整个终端界面。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="这一切是 AI Coding 的产物"&gt;这一切是 AI Coding 的产物&lt;/h3&gt;
&lt;p&gt;说实话，"从零重写 WebSocket 客户端"、"从零实现 LLM streaming 协议"、"用 ANSI escape code 手画 TUI"——这些事情如果纯手写，工程量很大，这在以往完全不现实。&lt;/p&gt;

&lt;p&gt;但 OpenClacky 本身就是一个 AI coding agent。这些"为了极致安装体验而大胆从零重写依赖"的决策，&lt;strong&gt;是用 OpenClacky 自己来完成的&lt;/strong&gt;。一个能写代码的 agent 让"零依赖"从不切实际变成了可执行。这是一个自举的过程——产品帮助自己变得更好。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="结语"&gt;结语&lt;/h2&gt;
&lt;p&gt;回头看这 7 个决策，它们背后其实只有一句话：&lt;strong&gt;把工程预算花在 harness 上，把智能预算留给模型。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;不做 RAG，不做多 Agent 编排，不做工具堆叠——不是因为这些东西没用，而是因为模型在快速变好。半年前需要 4 个 agent 协作才能勉强通过的任务，今天一个 agent + 一个好的 harness 就能做得更快更便宜。&lt;/p&gt;

&lt;p&gt;我们选择把精力放在那些&lt;strong&gt;不会随模型进步而过时&lt;/strong&gt;的事情上：cache 命中率、工具稳定性、安装体验、压缩策略。这些是 harness 层面的基础设施，不管模型换到哪一代都用得上。&lt;/p&gt;

&lt;p&gt;如果这篇对你有用，请帮我们点赞，欢迎 PR。欢迎转发和分享。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;OpenClacky 完全开源，MIT 协议：&lt;a href="https://github.com/clacky-ai/openclacky" rel="nofollow" target="_blank" title=""&gt;github.com/clacky-ai/openclacky&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gem install openclacky&lt;/code&gt; 一行装完即用，不需要 Docker、不需要 clone 仓库。如果你也在做 Agent，欢迎试试，遇到问题直接开 issue 聊。&lt;/p&gt;

&lt;p&gt;4 家 Agent 横评的完整数据、产物对比、录像回放：&lt;a href="https://www.openclacky.com/benchmark" rel="nofollow" target="_blank" title=""&gt;openclacky.com/benchmark&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;本文引用的核心代码：&lt;a href="https://github.com/clacky-ai/openclacky/blob/main/lib/clacky/client.rb" rel="nofollow" target="_blank" title=""&gt;Cache 标记&lt;/a&gt; · &lt;a href="https://github.com/clacky-ai/openclacky/blob/main/lib/clacky/agent/message_compressor.rb" rel="nofollow" target="_blank" title=""&gt;Insert-then-Compress&lt;/a&gt; · &lt;a href="https://github.com/clacky-ai/openclacky/blob/main/lib/clacky/agent.rb" rel="nofollow" target="_blank" title=""&gt;Session context 注入&lt;/a&gt; · &lt;a href="https://github.com/clacky-ai/openclacky/blob/main/lib/clacky/idle_compression_timer.rb" rel="nofollow" target="_blank" title=""&gt;空闲压缩&lt;/a&gt; · &lt;a href="https://github.com/clacky-ai/openclacky/blob/main/lib/clacky/tools/browser.rb" rel="nofollow" target="_blank" title=""&gt;浏览器工具&lt;/a&gt;&lt;/p&gt;</description>
      <author>lyfi2003</author>
      <pubDate>Thu, 14 May 2026 19:40:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/44571</link>
      <guid>https://ruby-china.org/topics/44571</guid>
    </item>
    <item>
      <title>matz 用 claude 把 mruby 的 issues 全关了</title>
      <description>&lt;p&gt;&lt;a href="https://github.com/mruby/mruby/issues" rel="nofollow" target="_blank"&gt;https://github.com/mruby/mruby/issues&lt;/a&gt; 已经只有一个 issues 了&lt;/p&gt;

&lt;p&gt;随便点开几个基本上都是 matz and claude committed &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/mruby/mruby/commit/6b9e477cc0eca2d33ad35e6fd163edbb2ffdbf85" rel="nofollow" target="_blank" title=""&gt;https://github.com/mruby/mruby/commit/6b9e477cc0eca2d33ad35e6fd163edbb2ffdbf85&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mruby/mruby/commit/f267925d45fc387ae9dd9ae7b65e752961210b69" rel="nofollow" target="_blank" title=""&gt;https://github.com/mruby/mruby/commit/f267925d45fc387ae9dd9ae7b65e752961210b69&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mruby/mruby/pull/6792" rel="nofollow" target="_blank" title=""&gt;https://github.com/mruby/mruby/pull/6792&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>xiaoLinger</author>
      <pubDate>Tue, 12 May 2026 10:13:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/44569</link>
      <guid>https://ruby-china.org/topics/44569</guid>
    </item>
    <item>
      <title>11 英寸 MacBook 的第二春：从零开始配置 Ruby on Rails 开发环境</title>
      <description>&lt;h2 id="🌸 旧 MacBook 的第二春：从零配置 Ruby on Rails 开发环境"&gt;🌸 旧 MacBook 的第二春：从零配置 Ruby on Rails 开发环境&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;"最好的设备是你手里那台。"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/AixCoder/e414fcae-f822-45b1-87bd-c387a696803c.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;很多同学手里都有一台旧的 MacBook Air，可能是当年为了写论文买的。现在用起来有点卡，打开 App 要转圈圈，似乎只能沦为追剧专用机。&lt;/p&gt;

&lt;p&gt;但别急着放弃它。&lt;/p&gt;

&lt;p&gt;MacBook 的硬件做工一向扎实，老机器的问题往往不是性能不够，而是&lt;strong&gt;系统里堆积了太多垃圾，以及没有装对工具&lt;/strong&gt;。就像一间堆满杂物的房间，不是房子太小，而是需要重新整理。&lt;/p&gt;

&lt;p&gt;给这台旧电脑&lt;strong&gt;重装系统 → 安装 Homebrew → 配置 Ruby → 搭建 Rails&lt;/strong&gt;，它就能从追剧神器变身成一台趁手的&lt;strong&gt;编程学习机、写作工具，甚至是一台随身携带的代码终端&lt;/strong&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="📑 目录"&gt;📑 目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#%E5%AE%89%E8%A3%85%E5%89%8D%E7%9A%84%E5%87%86%E5%A4%87%E9%87%8D%E8%A3%85%E7%B3%BB%E7%BB%9F" title=""&gt;安装前的准备：重装系统&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%AC%AC%E4%B8%80%E6%AD%A5%E5%AE%89%E8%A3%85-homebrew" title=""&gt;第一步：安装 Homebrew&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%AC%AC%E4%BA%8C%E6%AD%A5%E5%AE%89%E8%A3%85-rbenv" title=""&gt;第二步：安装 rbenv&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%AC%AC%E4%B8%89%E6%AD%A5%E5%AE%89%E8%A3%85-ruby" title=""&gt;第三步：安装 Ruby&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%AC%AC%E5%9B%9B%E6%AD%A5%E5%AE%89%E8%A3%85-rails" title=""&gt;第四步：安装 Rails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E7%AC%AC%E4%BA%94%E6%AD%A5%E5%88%9B%E5%BB%BA%E5%B9%B6%E5%90%AF%E5%8A%A8%E4%BD%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E9%A1%B9%E7%9B%AE" title=""&gt;第五步：创建并启动你的第一个项目&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#%E5%86%99%E5%9C%A8%E6%9C%80%E5%90%8E" title=""&gt;写在最后&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="安装前的准备：重装系统"&gt;安装前的准备：重装系统&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;强烈推荐先重装系统。&lt;/strong&gt; 如果这台 MacBook 从买来到现在就没重装过，系统里可能积累了各种残留文件。&lt;/p&gt;

&lt;p&gt;操作很简单：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;备份&lt;/strong&gt;重要文件到 移动盘或云盘&lt;/li&gt;
&lt;li&gt;开机时按住 &lt;code&gt;Option + Command + R&lt;/code&gt;，进入在线恢复模式&lt;/li&gt;
&lt;li&gt;选择重新安装 macOS，把系统恢复到最干净的状态&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;重装后的系统就像刚搬进的空房子，接下来我们往里面添置需要的家具。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第一步：安装 Homebrew"&gt;第一步：安装 Homebrew&lt;/h2&gt;&lt;h3 id="Homebrew 是什么？"&gt;Homebrew 是什么？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Homebrew 就像 Mac 中隐藏版的应用商店，但比 App Store 强大得多。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;想象你想在新家里添置家具：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;App Store&lt;/th&gt;
&lt;th&gt;Homebrew&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;像大型商场，卖的都是精装成品（爱奇艺）&lt;/td&gt;
&lt;td&gt;像万能仓库，程序员需要的各种工具、语言、软件，一条命令就能自动下载、安装、配置好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;买不到专业工具，或者版本很旧&lt;/td&gt;
&lt;td&gt;永远是最新版，不需要你到处找安装包、点下一步、输密码&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h3 id="安装命令"&gt;安装命令&lt;/h3&gt;
&lt;p&gt;打开终端，复制粘贴下面这行命令：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统会提示你输入开机密码（输入时不会显示字符，这是正常的，输完直接回车）
安装过程可能需要几分钟，取决于网速。(建议安装的过程中全局科学上网)&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/AixCoder/ddfd0cd3-9286-4f96-b7c1-b26dd0a9bed6.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;安装 Homebrew 的过程中，它会顺带安装 &lt;strong&gt;Command Line Tools for Xcode&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command Line Tools for Xcode 是什么？&lt;/strong&gt;
Xcode 是苹果官方出的集成开发工具，里面包含了开发 iPhone App 需要的所有重型设备（几十个 GB）。但对于 90% 的开发者来说，我们不需要那个庞然大物，只需要里面最核心的几件工具&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;编译器 (Compiler)&lt;/strong&gt;：把人类能看懂的代码，"翻译"成电脑能听懂的机器指令
&lt;strong&gt;构建工具 (Make)&lt;/strong&gt;：相当于安装指南，告诉电脑第一步拼哪块，第二步缝哪条线&lt;/p&gt;

&lt;p&gt;这就是 Command Line Tools for Xcode，包含了所有让代码"跑起来"的必要工具。&lt;/p&gt;
&lt;h5 id="验证安装成功"&gt;验证安装成功&lt;/h5&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果显示出版本号（例如 &lt;code&gt;Homebrew 5.1.10&lt;/code&gt;），恭喜你，Homebrew 已经就位。&lt;/p&gt;
&lt;h2 id="第二步：安装 rbenv"&gt;第二步：安装 rbenv&lt;/h2&gt;&lt;h3 id="rbenv 是什么？"&gt;rbenv 是什么？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;rbenv 是 Ruby 的「版本管家」。&lt;/strong&gt; 它让你在同一台电脑上安装多个 Ruby 版本，想用什么版本就切什么版本，互不干扰。&lt;/p&gt;
&lt;h3 id="为什么我们需要它？"&gt;为什么我们需要它？&lt;/h3&gt;
&lt;p&gt;想象一下，你是一个热爱生活且多才多艺的女生：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;今天你想做一个现代感十足的网站，需要请 &lt;strong&gt;Ruby 3.2&lt;/strong&gt; 大师来帮忙&lt;/li&gt;
&lt;li&gt;明天你想维护一个几年前的老项目，那个项目很挑剔，需要指定某个版本的 Ruby&lt;/li&gt;
&lt;li&gt;而你的 Mac 系统一直带着一个"老古董" &lt;strong&gt;Ruby 2.6&lt;/strong&gt; 系统管理员，它是用来维持系统运行的，你最好不要动它&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;如果没有 rbenv：&lt;/strong&gt; 你得在电脑里装好几个 Ruby，它们会为了争夺"谁才是真正的 Ruby"打架，最后把你的系统搞得一团糟&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;有了 rbenv：&lt;/strong&gt; 简单管理多个版本的 Ruby，系统自带的旧版本不动，新版本各住各的房间，互不打扰&lt;/p&gt;
&lt;h3 id="安装命令"&gt;安装命令&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;rbenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装的过程可能比较慢，因为电脑比较老了，很多工具得现场编译，耐心等待吧⌛️
&lt;img src="https://l.ruby-china.com/photo/AixCoder/77825179-e543-4e2a-a4ad-a59356680f28.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;安装完成后，请运行以下命令来配置 rbenv&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rbenv init
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;​&lt;strong&gt;关闭终端，重新打开&lt;/strong&gt;​，以确保更改生效。&lt;/p&gt;
&lt;h5 id="验证安装"&gt;验证安装&lt;/h5&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rbenv &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="第三步：安装 Ruby"&gt;第三步：安装 Ruby&lt;/h2&gt;&lt;h3 id="Ruby 的美丽：一门像诗一样的编程语言"&gt;Ruby 的美丽：一门像诗一样的编程语言&lt;/h3&gt;
&lt;p&gt;rbenv 管家已经就位，现在该请真正的主角进场了。&lt;/p&gt;

&lt;p&gt;Ruby 像一首诗&lt;/p&gt;

&lt;p&gt;它的发明者松本行弘（Yukihiro Matsumoto）说过一句话：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"我希望 Ruby 让程序员感到快乐。"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="安装命令"&gt;安装命令&lt;/h3&gt;
&lt;p&gt;先查看 rbenv 目前能提供的比较稳定的几个 Ruby 版本：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rbenv &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于这台老旧 MacBook，我装了的 Ruby3.3.10，和其他设备保持版本一致性&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rbenv &lt;span class="nb"&gt;install &lt;/span&gt;3.3.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/AixCoder/b61515ba-bf33-4071-8c58-b4bdc9b872f1.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💻 ​&lt;strong&gt;老旧 MacBook 特别提示&lt;/strong&gt;​：编译 Ruby 可能需要 ​&lt;strong&gt;10-20 分钟&lt;/strong&gt;​，风扇会狂转，这是正常的。建议插电进行，去泡杯茶，回来就好了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;装完后，设为默认版本：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rbenv global 3.3.10
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="验证安装"&gt;验证安装&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;ruby &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-darwin20]
旧 MacBook 现在拥有了一颗年轻的心脏&lt;/p&gt;
&lt;h2 id="第四步：安装 Rails"&gt;第四步：安装 Rails&lt;/h2&gt;&lt;h3 id="Rails：Ruby 的黄金搭档，让想法快速落地"&gt;Rails：Ruby 的黄金搭档，让想法快速落地&lt;/h3&gt;
&lt;p&gt;如果说 Ruby 是一首诗，​&lt;strong&gt;Rails 就是一家出版社&lt;/strong&gt;​
它不负责写诗，但它让诗能被印刷、装订、送到读者手中，而且整个过程快得惊人。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rails 像是一套「盖房子的标准化流程」。&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;你想搭一个网站，Rails 已经帮你把地基打好了、水电接通了、门窗装好了。你只需要决定：墙上刷什么颜色的漆、客厅里摆什么家具。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;想象你去一家很懂你的咖啡馆：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;你一坐下，店员就知道你要燕麦拿铁、半糖、去冰&lt;/li&gt;
&lt;li&gt;不需要你每次都说一遍&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rails 就是这样。它假设你「大概率会这样做」，所以提前帮你把选择做好了。这种哲学叫 ​&lt;strong&gt;"约定优于配置"&lt;/strong&gt;​（Convention over Configuration）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;对于初学者来说，这太重要了。&lt;/strong&gt; 你不需要先成为「全栈专家」，就能先做出一个「能用的东西」。成就感来得早，学习动力就足。&lt;/p&gt;

&lt;p&gt;记得当时做一个诗歌小站——可以读诗、投稿、玩一个简单的文字游戏，结合 AI 编程，好像用了大半天就做出来了。&lt;/p&gt;
&lt;h3 id="gem 是什么？"&gt;gem 是什么？&lt;/h3&gt;
&lt;p&gt;当你通过 rbenv 安装好 Ruby 的那一刻，​&lt;strong&gt;gem 就已经坐在你电脑里了&lt;/strong&gt;​。&lt;/p&gt;

&lt;p&gt;​&lt;strong&gt;gem 的全名叫 RubyGems&lt;/strong&gt;​，是 Ruby 官方的包管理器，可以理解成 ​&lt;strong&gt;Ruby 世界的「快递总站」&lt;/strong&gt;​。&lt;/p&gt;

&lt;p&gt;它的工作流程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;你在终端说：&lt;code&gt;gem install rails&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;gem 就联网去 ​&lt;strong&gt;rubygems.org&lt;/strong&gt;​（Ruby 的官方仓库）&lt;/li&gt;
&lt;li&gt;找到 Rails，把它连同所有依赖一起打包下载、自动安装&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="安装命令"&gt;安装命令&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;rails &lt;span class="nt"&gt;-v&lt;/span&gt; 你想要的rails版本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ ​&lt;strong&gt;如果下载很慢&lt;/strong&gt;​：因为 gem 默认去国外的仓库拉货，可能会卡很久。建议先换成国内镜像源（ruby-china 镜像源）&lt;/p&gt;

&lt;p&gt;安装过程可能需要几分钟，gem 会自动下载 Rails 及其所有依赖。&lt;/p&gt;
&lt;h3 id="验证安装"&gt;验证安装&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rails &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="第五步：创建并启动你的第一个项目"&gt;第五步：创建并启动你的第一个项目&lt;/h2&gt;&lt;h3 id="1. 新建项目"&gt;1. 新建项目&lt;/h3&gt;
&lt;p&gt;在终端里输入：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new rails_nice
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你会看到很多绿色的输出，正在新建项目，请稍等。
&lt;img src="https://l.ruby-china.com/photo/AixCoder/7dfc5015-16a8-4508-8699-70ae0dc7e586.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💻 ​&lt;strong&gt;老旧 MacBook 特别提示&lt;/strong&gt;​：第一次创建项目时，Rails 需要安装依赖，可能会比较慢，请耐心等待。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="2. 进入项目文件夹"&gt;2. 进入项目文件夹&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;rails_nice
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="3. 启动服务器"&gt;3. 启动服务器&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails s
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="4. 你会看到什么？"&gt;4. 你会看到什么？&lt;/h3&gt;
&lt;p&gt;终端里会显示：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=&amp;gt; Booting Puma
=&amp;gt; Rails 7.2.3 application starting in development 
=&amp;gt; Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 8.0.1 ("Into the Arena")
* Ruby version: ruby 3.3.10 (2025-10-23 revision 343ea05002) [x86_64-darwin20]
*  Min threads: 3
*  Max threads: 3
*  Environment: development
*          PID: 14775
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到 &lt;code&gt;Listening on http://127.0.0.1:3000&lt;/code&gt;，就是​&lt;strong&gt;启动成功了&lt;/strong&gt;​。&lt;/p&gt;
&lt;h3 id="5. 在浏览器里查看成果"&gt;5. 在浏览器里查看成果&lt;/h3&gt;
&lt;p&gt;打开 Safari（或任何浏览器），地址栏输入：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/AixCoder/c8a645d4-2876-412d-b686-c83f0f1b74ca.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;从重装系统，到 Homebrew，到 rbenv，到 Ruby，到 gem，到 Rails，再到 &lt;code&gt;bin/rails s&lt;/code&gt;——&lt;strong&gt;这台旧 MacBook，终于焕发了第二春。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;旧物值得被认真对待，学习也可以从当下便宜的设备开始。&lt;/p&gt;

&lt;p&gt;M 芯片的 MacBook 就留着干重活吧，这台 macbook air 小电脑，很轻巧。&lt;/p&gt;
&lt;h3 id="💡 小结"&gt;💡 小结&lt;/h3&gt;
&lt;p&gt;整个流程其实就是一个“套娃”安装的过程：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用 &lt;strong&gt;command line tools for xocde&lt;/strong&gt; 为 &lt;strong&gt;Homebrew&lt;/strong&gt; 铺路。&lt;/li&gt;
&lt;li&gt;用 &lt;strong&gt;Homebrew&lt;/strong&gt; 安装 ​&lt;strong&gt;rbenv&lt;/strong&gt;​。&lt;/li&gt;
&lt;li&gt;用 &lt;strong&gt;rbenv&lt;/strong&gt; 安装 ​&lt;strong&gt;Ruby&lt;/strong&gt;​。&lt;/li&gt;
&lt;li&gt;用 ​&lt;strong&gt;Ruby&lt;/strong&gt;​（自带的 gem）安装 ​&lt;strong&gt;Rails&lt;/strong&gt;​。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;每一个上层工具都依赖下层提供的环境。这就是为什么当初我们必须先解决 Xcode 开发工具安装的问题。&lt;/p&gt;

&lt;p&gt;好啦，看到这台“小破机”重新跑起代码，😄
作为编程学习机和写作工具来说真是很好，非常适合带出门轻度使用。小电脑，很轻巧。&lt;/p&gt;</description>
      <author>AixCoder</author>
      <pubDate>Sun, 10 May 2026 13:22:14 +0800</pubDate>
      <link>https://ruby-china.org/topics/44567</link>
      <guid>https://ruby-china.org/topics/44567</guid>
    </item>
    <item>
      <title>Spinel -- Ruby AOT 编译器</title>
      <description>&lt;p&gt;&lt;a href="https://github.com/matz/spinel" rel="nofollow" target="_blank"&gt;https://github.com/matz/spinel&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Spinel 将 Ruby 源代码编译成独立的本地可执行文件。它执行全程序类型推断并生成优化的 C 代码，与 CRuby 相比速度显著提升。&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>Rei</author>
      <pubDate>Fri, 24 Apr 2026 17:12:44 +0800</pubDate>
      <link>https://ruby-china.org/topics/44560</link>
      <guid>https://ruby-china.org/topics/44560</guid>
    </item>
    <item>
      <title>试图交了个提升 Ruby Hash 性能的补丁</title>
      <description>&lt;h2 id="动机"&gt;动机&lt;/h2&gt;
&lt;p&gt;昨天在参加 RubyKaigi 的时候顺便瞄到了 &lt;code&gt;st.c&lt;/code&gt; 的实现，然后让我回想起来，现在的 Ruby 的 Hash 实现主要来自于 Vladimir Makarov 在 2016 年实现的精心调优的 open-addressing 实现的&lt;a href="https://bugs.ruby-lang.org/issues/12142" rel="nofollow" target="_blank" title=""&gt;版本&lt;/a&gt;。我突然想起 Google absl 实现里有个 &lt;a href="https://abseil.io/blog/20180927-swisstables" rel="nofollow" target="_blank" title=""&gt;Swiss Tables&lt;/a&gt; 实现吊打了 C++ 标准库的实现，而 Rust 的标准库实现基于的 &lt;a href="https://github.com/rust-lang/hashbrown" rel="nofollow" target="_blank" title=""&gt;hashbrown&lt;/a&gt; 也是基于相同的原理。我就在思考时隔十年我们能不能把 SwissTables 相关的算法移植到 Ruby 上进一步提升 Ruby 处理 &lt;code&gt;Hash&lt;/code&gt; 类型的性能，要知道 &lt;code&gt;Hash&lt;/code&gt; 类型是 Ruby 中调用极其频繁的类型，它替代了很多其它语言中 &lt;code&gt;struct&lt;/code&gt; 的功能，因此性能提升能带来很直观的收益。&lt;/p&gt;
&lt;h2 id="尝试"&gt;尝试&lt;/h2&gt;
&lt;p&gt;直接的移植后发现性能不升反降了，这和 Ruby 中 &lt;code&gt;Hash&lt;/code&gt; 的用法很有关系。Ruby 中非常多的 &lt;code&gt;Hash&lt;/code&gt; 对象非常小，而 Makarov 2016 年的实现对此做了非常细腻的优化。&lt;/p&gt;

&lt;p&gt;于是我的做法转变成了，保留 Ruby 中原先对不同大小 Hash 的分层存储，从 Swiss Tables 对 control bit 更好的对超标量 / 向量化的支持和 H2 短哈希更好的缓存命中这两个角度进一步进行优化，只针对大的 Hash 对象（容量 &amp;gt;= 64）的情况进行处理。&lt;/p&gt;
&lt;h2 id="设计"&gt;设计&lt;/h2&gt;&lt;h3 id="1. 三数组布局"&gt;1. 三数组布局&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;array&lt;/th&gt;
&lt;th&gt;width&lt;/th&gt;
&lt;th&gt;role&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entries[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16B (以前为 24B)&lt;/td&gt;
&lt;td&gt;以插入顺序记录 &lt;code&gt;(key, record)&lt;/code&gt; 的日志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hashes[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每 slot 4B&lt;/td&gt;
&lt;td&gt;与之并行的截断 32 位哈希数组（也编码删除标记）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bins[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;自适应 1 / 2 / 4 / 8B&lt;/td&gt;
&lt;td&gt;基于哈希的索引数组，存放指向 &lt;code&gt;entries[]&lt;/code&gt; 的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ctrl[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;每 slot 1B&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;H2&lt;/code&gt;（哈希的高 7 位）或 &lt;code&gt;EMPTY (0xff)&lt;/code&gt; / &lt;code&gt;DELETED (0xfe)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;entries[]&lt;/code&gt; 和 &lt;code&gt;hashes[]&lt;/code&gt; 长度相同并由相同索引寻址，因此迭代和 slot 重用保持简单。&lt;code&gt;ctrl[]&lt;/code&gt; 是快速拒绝过滤器，和 &lt;code&gt;bins[]&lt;/code&gt; 并列；只有当某个 &lt;code&gt;ctrl&lt;/code&gt; 字节与 H2 匹配时，我们才会加载（现在更小的）entry 和并行哈希以确认匹配。对 &lt;code&gt;ctrl[]&lt;/code&gt; 在 &lt;code&gt;uint64_t&lt;/code&gt; 读取上完成，这本质上是一种 SWAR 优化。&lt;/p&gt;
&lt;h3 id="2. 紧凑的 st_table_entry"&gt;2. 紧凑的 &lt;code&gt;st_table_entry&lt;/code&gt;
&lt;/h3&gt;
&lt;p&gt;修改前：&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;st_table_entry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;st_hash_t&lt;/span&gt;  &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="cm"&gt;/* 8 B */&lt;/span&gt;
    &lt;span class="n"&gt;st_data_t&lt;/span&gt;  &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="cm"&gt;/* 8 B */&lt;/span&gt;
    &lt;span class="n"&gt;st_data_t&lt;/span&gt;  &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* 8 B */&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;                       &lt;span class="cm"&gt;/* 24 B */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改后：&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;st_table_entry&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;st_data_t&lt;/span&gt;  &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="cm"&gt;/* 8 B */&lt;/span&gt;
    &lt;span class="n"&gt;st_data_t&lt;/span&gt;  &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* 8 B */&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;                       &lt;span class="cm"&gt;/* 16 B */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哈希移入 &lt;code&gt;tab-&amp;gt;hashes[i]&lt;/code&gt;，由于大多数情况可以依赖 H2 进行匹配。因此这样我们可以进一步提升 cacheline 的命中效率，从而提高 CPU L1 的命中率。&lt;/p&gt;
&lt;h3 id="3. 新的哈希函数"&gt;3. 新的哈希函数&lt;/h3&gt;
&lt;p&gt;仅存储低 32 位哈希意味着我们不能再从原始 &lt;code&gt;unsigned long&lt;/code&gt; 哈希的高位 7 位读取 H2，而这是 SwissTables 原先设计所采用的做法。naive 的实现是在每次重建/重哈希/&lt;code&gt;st_shift&lt;/code&gt;/&lt;code&gt;st_general_foreach&lt;/code&gt; 中重新计算完整 64 位哈希以保证正确性，但会严重损害插入和重建性能 —— 尤其是对于类似 string 这样的变长类型的性能损害很大。&lt;/p&gt;

&lt;p&gt;解决方法是从截断的 32 位哈希的一个不同位段推导 H2，该位段与用于选取 bin 的位段不重叠：&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* bin index: low `bin_power` bits, masked by `bins_mask(tab)`     */&lt;/span&gt;
&lt;span class="n"&gt;hash_bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st_table&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;bins_mask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/* H2: bits 25..31 of the same 32-bit hash, never overlaps with     */&lt;/span&gt;
&lt;span class="cm"&gt;/* the bin index because bin_power is capped well under 25 in       */&lt;/span&gt;
&lt;span class="cm"&gt;/* practice.                                                        */&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kr"&gt;inline&lt;/span&gt; &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;
&lt;span class="nf"&gt;st_swiss_h2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st_hash_t&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这使得存储的 &lt;code&gt;uint32_t&lt;/code&gt; 是自包含的：每次探测都能从同一字中读取 bin 索引和 H2 字节，无需调用 &lt;code&gt;do_hash()&lt;/code&gt;，重建/重哈希/移位/foreach 全部使用 &lt;code&gt;ST_HASH_AT_IDX(tab, i)&lt;/code&gt; 而不是重新计算。&lt;/p&gt;

&lt;p&gt;截断带来的另外两个细节：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;normalize_hash_value()&lt;/code&gt; 已更新，以确保 &lt;code&gt;0xFFFFFFFF&lt;/code&gt;（32 位哈希 slot 的墓碑标记）永远不会与真实哈希值冲突——如果截断结果落在该保留值上我们会进行跳变（bump）。在未启用 &lt;code&gt;ST_USE_SWISS_BINS&lt;/code&gt; 编译的平台上保留 64 位的保留值。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;MARK_ENTRY_DELETED&lt;/code&gt; / &lt;code&gt;DELETED_ENTRY_P&lt;/code&gt; 宏新增了 table 参数，以便读取/写入并行的哈希 slot。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="4. 在 H2 匹配时进行 prefetch"&gt;4. 在 H2 匹配时进行 prefetch&lt;/h3&gt;
&lt;p&gt;当 SWAR 在控制组中找到候选的 H2 匹配时，下一步是加载匹配的 &lt;code&gt;st_table_entry&lt;/code&gt; 的哈希。我们在 &lt;code&gt;find_table_entry_ind&lt;/code&gt; / &lt;code&gt;find_table_bin_ind&lt;/code&gt; / &lt;code&gt;find_table_bin_ptr_and_reserve&lt;/code&gt; 中在检测到匹配后立即对两者发出 &lt;code&gt;__builtin_prefetch&lt;/code&gt;。在以查找为主的工作负载上，这能掩盖 SWAR 快速过滤器本会暴露的 CPU L2 缓存延迟。&lt;/p&gt;
&lt;h2 id="与 master 的结果比较"&gt;与 &lt;code&gt;master&lt;/code&gt; 的结果比较&lt;/h2&gt;
&lt;p&gt;两个二进制均来自相同代码树（&lt;code&gt;master = 42b3cdc51a&lt;/code&gt;，&lt;code&gt;swiss = 3c0446847f&lt;/code&gt;），相同编译器，相同编译选项。每个脚本运行 5 次，使用 &lt;code&gt;--disable-gems&lt;/code&gt;，报告最佳结果。内存为 macOS arm64（M4 Max）上 &lt;code&gt;/usr/bin/time -l&lt;/code&gt; 的最大常驻集大小（maximum resident set size）。&lt;/p&gt;
&lt;h3 id="吞吐量（时间越低越好）"&gt;吞吐量（时间越低越好）&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;benchmark&lt;/th&gt;
&lt;th style="text-align:right;"&gt;master (s)&lt;/th&gt;
&lt;th style="text-align:right;"&gt;swiss (s)&lt;/th&gt;
&lt;th style="text-align:right;"&gt;speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aref_int_large&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.8352&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.6862&lt;/td&gt;
&lt;td style="text-align:right;"&gt;&lt;strong&gt;+17.8 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aref_str_large&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.9915&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.8406&lt;/td&gt;
&lt;td style="text-align:right;"&gt;&lt;strong&gt;+15.2 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aref_miss_large&lt;/td&gt;
&lt;td style="text-align:right;"&gt;1.0803&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.7896&lt;/td&gt;
&lt;td style="text-align:right;"&gt;&lt;strong&gt;+26.9 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aref_mix_50&lt;/td&gt;
&lt;td style="text-align:right;"&gt;1.0337&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.8201&lt;/td&gt;
&lt;td style="text-align:right;"&gt;&lt;strong&gt;+20.7 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;insert_grow&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.1138&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.1105&lt;/td&gt;
&lt;td style="text-align:right;"&gt;+2.9 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;churn (mixed RW)&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.0321&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.0304&lt;/td&gt;
&lt;td style="text-align:right;"&gt;+5.5 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iterate&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.0566&lt;/td&gt;
&lt;td style="text-align:right;"&gt;0.0565&lt;/td&gt;
&lt;td style="text-align:right;"&gt;±0.1 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;查找是主要的收益点——成功查找（+15 % … +20 %）和未命中（+27 %）均有提升，后者的原因是缺失的 key 现在能在第一个 SWAR 组立即短路，无需加载 entry/bin。插入和混合负载略有提速，因为重建不再调用 &lt;code&gt;do_hash&lt;/code&gt;。迭代不受影响（它不需要访问 bins 或 ctrl）。&lt;/p&gt;
&lt;h3 id="内存"&gt;内存&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;benchmark&lt;/th&gt;
&lt;th style="text-align:right;"&gt;master (MB)&lt;/th&gt;
&lt;th style="text-align:right;"&gt;swiss (MB)&lt;/th&gt;
&lt;th style="text-align:right;"&gt;delta&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;insert_grow&lt;/td&gt;
&lt;td style="text-align:right;"&gt;66.62&lt;/td&gt;
&lt;td style="text-align:right;"&gt;60.44&lt;/td&gt;
&lt;td style="text-align:right;"&gt;&lt;strong&gt;−9.3 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aref_str_large&lt;/td&gt;
&lt;td style="text-align:right;"&gt;15.27&lt;/td&gt;
&lt;td style="text-align:right;"&gt;15.23&lt;/td&gt;
&lt;td style="text-align:right;"&gt;−0.3 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aref_mix_50&lt;/td&gt;
&lt;td style="text-align:right;"&gt;16.64&lt;/td&gt;
&lt;td style="text-align:right;"&gt;16.61&lt;/td&gt;
&lt;td style="text-align:right;"&gt;−0.2 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;churn&lt;/td&gt;
&lt;td style="text-align:right;"&gt;13.19&lt;/td&gt;
&lt;td style="text-align:right;"&gt;13.03&lt;/td&gt;
&lt;td style="text-align:right;"&gt;−1.2 %&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;每表内存（通过 &lt;code&gt;ObjectSpace.memsize_of&lt;/code&gt;，在多个哈希上求和）：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;workload&lt;/th&gt;
&lt;th style="text-align:right;"&gt;master&lt;/th&gt;
&lt;th style="text-align:right;"&gt;swiss&lt;/th&gt;
&lt;th style="text-align:right;"&gt;delta&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 000 hashes × 200 entries&lt;/td&gt;
&lt;td style="text-align:right;"&gt;14.66 MB&lt;/td&gt;
&lt;td style="text-align:right;"&gt;12.11 MB&lt;/td&gt;
&lt;td style="text-align:right;"&gt;&lt;strong&gt;−17.4 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 hash × 100 000 entries&lt;/td&gt;
&lt;td style="text-align:right;"&gt;4.19 MB&lt;/td&gt;
&lt;td style="text-align:right;"&gt;3.28 MB&lt;/td&gt;
&lt;td style="text-align:right;"&gt;&lt;strong&gt;−21.9 %&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;2000 hashes × 200 entries 模拟了常见的 Rails 负载，这可以节省 ~17.4% 的 Hash 内存占用。&lt;/p&gt;

&lt;p&gt;这两种内存视图一致：任何保留大量存活条目的工作负载（无论是一个大哈希还是许多小哈希）都会因条目从 24B → 16B 的变化以及每槽 1B 的 &lt;code&gt;ctrl[]&lt;/code&gt; / 4B 的 &lt;code&gt;hashes[]&lt;/code&gt; 配对而显著减少内存，因为它们合计（每槽 5B）仍小于在 &lt;code&gt;entries[]&lt;/code&gt; 中每槽节省的 8 B。表的每表开销在小于约 64 条目时大致保持不变；Swiss 路径仅在 &lt;code&gt;entry_power ≥ 6&lt;/code&gt;（表容量 ≥ 64）时启用。&lt;/p&gt;
&lt;h2 id="详见"&gt;详见&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://bugs.ruby-lang.org/issues/22011" rel="nofollow" target="_blank"&gt;https://bugs.ruby-lang.org/issues/22011&lt;/a&gt;&lt;/p&gt;</description>
      <author>dsh0416</author>
      <pubDate>Thu, 23 Apr 2026 13:15:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/44557</link>
      <guid>https://ruby-china.org/topics/44557</guid>
    </item>
    <item>
      <title>用 railway 这个服务器部署 ruby on rails, 发现很不错，但是有点贵，大家看看可以参考</title>
      <description>&lt;p&gt;我最近帮客户做网站，我把网站放到了 railway.com 上面，用 railway 的服务器和数据库，用 Google 存储，存储图片，&lt;/p&gt;

&lt;p&gt;里面有 330 个产品信息。分别是 330 个产品图片、中英产品介绍。&lt;/p&gt;

&lt;p&gt;但是用了 13 天，railway 的 5 美金就花完了，又充值了 5 美金...&lt;/p&gt;

&lt;p&gt;感觉对这客户有点贵，官网展示网站，服务器费用一个月得超过 10 美金...&lt;/p&gt;</description>
      <author>shibin</author>
      <pubDate>Thu, 16 Apr 2026 19:31:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/44553</link>
      <guid>https://ruby-china.org/topics/44553</guid>
    </item>
    <item>
      <title>用 kamal 部署 rails 至阿里云主机总结</title>
      <description>&lt;h2 id="需求背景"&gt;需求背景&lt;/h2&gt;
&lt;p&gt;rails 开发速度很快，体验很好，但是如果无法部署到服务器上，最后也就是自己在本机玩玩，开发的网页服务不能给别人使用。
我在 deepseek 的帮助下经过一周的摸索终于实现了部署，特记录供自己后期查阅，也供其他人借鉴。真的感谢好时代，有 deepseek 不然想也不敢去想我一个个人业余开发者，身边没一个同语言开发指导老师怎么敢去想自行摸索完成部署功能。&lt;/p&gt;
&lt;h3 id="让deepseek总结kamal一次完整部署的生命周期"&gt;让 deepseek 总结 kamal 一次完整部署的生命周期&lt;/h3&gt;
&lt;p&gt;下面是 Kamal 从初始化到部署完成的完整工作流程，每一步都对应具体的操作：&lt;/p&gt;
&lt;h3 id="阶段 0：准备工作（只需一次）"&gt;阶段 0：准备工作（只需一次）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;初始化配置&lt;/strong&gt;：运行&amp;nbsp;&lt;code&gt;kamal init&lt;/code&gt;，生成&amp;nbsp;&lt;code&gt;config/deploy.yml&lt;/code&gt;&amp;nbsp;配置文件&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;配置服务器列表&lt;/strong&gt;：在&amp;nbsp;&lt;code&gt;deploy.yml&lt;/code&gt;&amp;nbsp;中指定服务器的 IP 地址或域名&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;配置容器注册表&lt;/strong&gt;：设置注册表的用户名和密码（通过环境变量加密存储）&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;设置 SSH 密钥&lt;/strong&gt;：确保本地可以免密登录服务器&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;编写 Dockerfile&lt;/strong&gt;：Rails 7+ 会自动生成，确保镜像构建正确&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="阶段 1：初始部署（kamal setup）"&gt;阶段 1：初始部署（&lt;code&gt;kamal setup&lt;/code&gt;）&lt;/h3&gt;
&lt;p&gt;这个命令会完成服务器的&lt;strong&gt;首次配置&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;SSH 连接并安装 Docker&lt;/strong&gt;：连接到配置文件中的服务器，如果 Docker 未安装，自动通过&amp;nbsp;&lt;code&gt;apt-get&lt;/code&gt;&amp;nbsp;安装&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;登录容器注册表&lt;/strong&gt;：在本地和远程服务器上都登录，以便推送和拉取镜像&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;构建 Docker 镜像&lt;/strong&gt;：使用项目根目录的 Dockerfile 构建镜像&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;推送镜像到注册表&lt;/strong&gt;：将构建好的镜像推送到配置的容器注册表&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;在服务器上拉取镜像&lt;/strong&gt;：通过 SSH 命令让服务器从注册表拉取镜像&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;启动 Kamal Proxy&lt;/strong&gt;：确保代理服务正在运行，并监听 80/443 端口&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;启动应用容器&lt;/strong&gt;：基于拉取的镜像启动新的 Docker 容器&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;健康检查&lt;/strong&gt;：验证应用是否响应&amp;nbsp;&lt;code&gt;GET /up&lt;/code&gt;&amp;nbsp;请求（必须返回 200 OK）&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;清理旧资源&lt;/strong&gt;：删除未使用的镜像和停止的容器，防止磁盘占满&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="阶段 2：后续部署（kamal deploy）"&gt;阶段 2：后续部署（&lt;code&gt;kamal deploy&lt;/code&gt;）&lt;/h3&gt;
&lt;p&gt;这是日常更新应用的命令，&lt;strong&gt;只执行部署流程，不重复安装 Docker&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;构建新镜像&lt;/strong&gt;：基于最新的代码构建新版本镜像&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;推送到注册表&lt;/strong&gt;：新镜像被推送到注册表，标签通常是 Git commit hash&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;SSH 连接服务器并拉取新镜像&lt;/strong&gt;：服务器从注册表拉取新版本镜像&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;启动新容器（旧容器仍在运行）&lt;/strong&gt;：在旧容器旁边启动新容器，确保服务不中断&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;健康检查&lt;/strong&gt;：等待新容器通过健康检查（&lt;code&gt;GET /up&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Kamal Proxy 切换流量&lt;/strong&gt;：代理原子性地将流量从旧容器切换到新容器&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;停止旧容器&lt;/strong&gt;：新容器稳定运行后，停止并删除旧容器&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;清理资源&lt;/strong&gt;：删除旧镜像和未使用的容器，释放磁盘空间&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="我理解kamal的功能（没怎么接触过docker）"&gt;我理解 kamal 的功能（没怎么接触过 docker）&lt;/h3&gt;
&lt;p&gt;我理解 kamal 大致功能是先将本机的开发环境和项目通过 docker 打包成镜像，push 到镜像仓库，然后登陆服务器先下载 docker，然后从镜像仓库 pull 这个镜像再还原运行。
之前尝试过在本机和服务机均安装网络代理工具，这一步实现了，但是直接参考 rails guide 部署还是会存在问题，没搞明白。只能尝试将所有可能会用到科学上网的源替换成国内的源。&lt;/p&gt;
&lt;h2 id="部署记录"&gt;部署记录&lt;/h2&gt;&lt;h3 id="step1：准备hello world项目"&gt;step1：准备 hello world 项目&lt;/h3&gt;
&lt;p&gt;新建 rails 项目&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ruby -v
rails -v
rails new rails106
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/flchenhp/2101356e-4fc7-4d84-83c6-3c1c2f067627.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;用编辑器（我用的是 trae）打开项目，做初始化保存&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#iterm
git add .
git commit -m "initial commit"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 gem 源改成 rubychina 的源&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# gemfile
-source "https://rubygems.org"
+source "https://gems.ruby-china.com"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加 hello world 页面&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#iterm
rails g controller welcome index
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# app/views/welcome/index.html.erb
+&amp;lt;h1&amp;gt;hello world&amp;lt;/h1&amp;gt;
-&amp;lt;h1&amp;gt;Welcome#index&amp;lt;/h1&amp;gt;
-&amp;lt;p&amp;gt;Find me in app/views/welcome/index.html.erb&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# config/routes.rb
  root "welcome#index"
  get "welcome/index"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动项目，查看 hello word 页面&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# iterm
bin/dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/flchenhp/0dc47b93-1671-483c-868c-e4d1ad9baacc.png!large" title="" alt=""&gt;
将代码加入 git 保存&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# iterm
git add .
git commit -m "add hello world page"
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="step2:选择云主机"&gt;step2:选择云主机&lt;/h3&gt;
&lt;p&gt;阿里云主机可以直接添加 docker，免去安装 docker 的麻烦。直接用 root 账号和设定的自定义密码，这个会用于远程服务器。
&lt;img src="https://l.ruby-china.com/photo/flchenhp/aae8e837-a9bf-4496-a43e-6fefb08ed3a7.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;远程主机验证 docker 安装成功：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 云主机命令行
docker --version
docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/flchenhp/8e5a47a9-ec2e-47f4-96d2-3a046b19b798.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="step3:申请阿里云容器镜像服务（ACR）"&gt;step3:申请阿里云容器镜像服务（ACR）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;进入控制台&lt;/strong&gt;：登录后搜索“&lt;strong&gt;容器镜像服务&lt;/strong&gt;”&lt;a href="https://www.e-com-net.com/article/1683693229553299456.htm" rel="nofollow" target="_blank" title=""&gt;&lt;/a&gt;，或直接访问&amp;nbsp;&lt;a href="https://cr.console.aliyun.com/" rel="nofollow" target="_blank" title=""&gt;容器镜像服务控制台&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;按提示创建个人版实例&lt;strong&gt;（下面的信息都很重要，部署的配置文件要用）&lt;/strong&gt;
&lt;img src="https://l.ruby-china.com/photo/flchenhp/3889a199-e266-45ce-a281-878eb3537139.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;li&gt;按提升建立命名空间
&lt;img src="https://l.ruby-china.com/photo/flchenhp/236e9c62-b255-4d3b-a6af-f3676517a301.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;li&gt;按提示设置密码，获取登录命令
&lt;img src="https://l.ruby-china.com/photo/flchenhp/49a00360-015c-4c96-9667-0375a6c79043.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;li&gt;获取个人加速器地址
&lt;img src="https://l.ruby-china.com/photo/flchenhp/3eab9292-3ec3-4822-a38c-3233105c35ee.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="step4:在本机下载docker"&gt;step4:在本机下载 docker&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;访问&amp;nbsp;&lt;a href="https://docs.docker.com/desktop/install/mac-install/" rel="nofollow" target="_blank" title=""&gt;Docker Desktop for Mac 下载页面&lt;/a&gt;选择适宜自己电脑版本下载安装&lt;/li&gt;
&lt;li&gt;点击 Mac 顶部菜单栏的 Docker 图标，选择 Settings。在左侧菜单选择 Docker Engine，将阿里云和一些其他的加速器地址填上
&lt;code&gt;
{
"builder": {
"gc": {
  "defaultKeepStorage": "20GB",
  "enabled": true
}
},
"experimental": false,
"registry-mirrors": [
"https://5ex1e6il.mirror.aliyuncs.com",
"https://docker.1panel.live",
"https://hub.rat.dev",
"https://docker.m.daocloud.io",
"https://docker.nju.edu.cn",
"https://docker.xuanyuan.me",
"https://docker.1ms.run"
]
}
&lt;/code&gt;
&lt;img src="https://l.ruby-china.com/photo/flchenhp/143e0e53-27b7-4d20-9a75-99b21880a364.png!large" title="" alt=""&gt;
&lt;/li&gt;
&lt;li&gt;打开“终端”应用，运行&lt;code&gt;docker --version&lt;/code&gt; 和&lt;code&gt;docker run hello-world&lt;/code&gt; 进行验证 docker 是否正确安装&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="step5:进行部署配置"&gt;step5:进行部署配置&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#rails106/config/deploy.yml
-image: rails106
+image: myrailsapp/rails106 # myrailsapp是阿里云的镜像仓库命名空间

-    - 192.168.0.1
+    - 47.x x.xx.236 # 这是阿里云的IP地址

-  server: localhost:5555
+  # 这里填阿里云的镜像仓库的公网地址
+  server: crpi-xxxxxxxxxxx.cn-zhangjiakou.personal.cr.aliyuncs.com 

-  # username: your-user
+  # 这里填阿里云的账号
+  username: chenxxxxxx21

-  # password:
+   # 这里填阿里云的镜像的固定秘密
+  password: cxxxxxx9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码类似
&lt;img src="https://l.ruby-china.com/photo/flchenhp/c3815648-e03b-41ce-a17e-a0b4c7dfe760.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# rails106/Dockerfile
-# syntax=docker/dockerfile:1
+# #syntax=docker/dockerfile:1 # 第一行这里要再加个注释符号注释，不然会报错，不知道为什么

-FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
+# FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
+# 要换成ruby的国内的源，不然会卡住
+FROM docker.m.daocloud.io/library/ruby:$RUBY_VERSION-slim AS base

WORKDIR /rails
+# 设定 apt 源为阿里云镜像，不然会卡住
+RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources &amp;amp;&amp;amp; \
+    sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources
# Install base packages
RUN apt-get update -qq &amp;amp;&amp;amp; \
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码类似
&lt;img src="https://l.ruby-china.com/photo/flchenhp/597ab9ec-9d1c-45db-a845-e7e0b95d4d35.png!large" title="" alt=""&gt;
加入 git 仓库&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# iterm
git add .
git commit -m "add deploy set"
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="step6:打部署命令开始部署"&gt;step6:打部署命令开始部署&lt;/h3&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# iterm
bin/kamal setup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;部署成功：
&lt;img src="https://l.ruby-china.com/photo/flchenhp/fac0b187-0b66-4db4-832d-5ec150a2f4b8.png!large" title="" alt=""&gt;
浏览器输入 ip 测试
&lt;img src="https://l.ruby-china.com/photo/flchenhp/9800797d-1852-4de5-aff6-7eae3416962d.png!large" title="" alt=""&gt;
大功告成！！！&lt;/p&gt;</description>
      <author>flchenhp</author>
      <pubDate>Mon, 13 Apr 2026 01:25:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/44550</link>
      <guid>https://ruby-china.org/topics/44550</guid>
    </item>
    <item>
      <title>cloudfare vercel 部署后得到的链接国内用不了，求推荐域名服务～</title>
      <description>&lt;p&gt;///&lt;/p&gt;</description>
      <author>zzz6519003</author>
      <pubDate>Tue, 31 Mar 2026 15:37:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/44534</link>
      <guid>https://ruby-china.org/topics/44534</guid>
    </item>
    <item>
      <title>Ruby 小白，做了个女性友好的诗歌 web app</title>
      <description>&lt;p&gt;📱：&lt;a href="https://pomes.petercat.life/" rel="nofollow" target="_blank"&gt;https://pomes.petercat.life/&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="起初的想法"&gt;起初的想法&lt;/h3&gt;
&lt;p&gt;去年冬天的时候就想着设计一款女性友好的诗歌 app&lt;/p&gt;
&lt;h3 id="落地实现"&gt;落地实现&lt;/h3&gt;
&lt;p&gt;2026 年开春了，借着当下 AI 编程的浪潮&lt;/p&gt;

&lt;p&gt;选择用 Ruby 技术展现出这么个诗歌网站&lt;/p&gt;

&lt;p&gt;这么个诗歌的小屋，用到的不过是那一点点技术
几行 Ruby 代码，一个 SQLite3，再加上让文字呼吸顺畅的 CSS 间距。&lt;/p&gt;

&lt;p&gt;在古法编程的时代来看一个小白要做出一个 app 并部署上线，要学习比较长时间的
如今来看这么个 app 并没有什么傲人的地方，门槛也比较低&lt;/p&gt;

&lt;p&gt;但是有的时候也常常想，如果更多女性来学这一点点技，多一些这样的港湾也蛮好&lt;/p&gt;
&lt;h3 id="ps：想问问群里的一些深耕Ruby领域多年的“老员工”"&gt;ps：想问问群里的一些深耕 Ruby 领域多年的“老员工”&lt;/h3&gt;
&lt;p&gt;Ruby 在国内的就业市场相比几年前下滑？&lt;/p&gt;

&lt;p&gt;几年前就听说 Ruby 在国内用人招人不好招 😂&lt;/p&gt;

&lt;p&gt;不过呢，学习 Ruby 倒不是说一味的奔着找工作去的&lt;/p&gt;

&lt;p&gt;目前以兴趣在驱动着学习，能够把自己的一些想法借助 AI coding 落地（以兴趣为主）&lt;/p&gt;</description>
      <author>AixCoder</author>
      <pubDate>Mon, 23 Mar 2026 14:45:29 +0800</pubDate>
      <link>https://ruby-china.org/topics/44528</link>
      <guid>https://ruby-china.org/topics/44528</guid>
    </item>
    <item>
      <title>最近学 rust，vibe 了一个序列化器，想念宏魔法</title>
      <description>&lt;p&gt;整出这样的代码&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use crate::ast::Json;
use crate::parser::parse;
use std::collections::HashMap;

pub trait ToJson { fn to_json_value(&amp;amp;self) -&amp;gt; Json; }
pub trait FromJson: Sized { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt;; }

pub trait Serialize { fn serialize(&amp;amp;self) -&amp;gt; String; }
pub trait Deserialize: Sized { fn deserialize(s: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, String&amp;gt;; }

impl&amp;lt;T: ToJson&amp;gt; Serialize for T { fn serialize(&amp;amp;self) -&amp;gt; String { self.to_json_value().to_string() } }
impl&amp;lt;T: FromJson&amp;gt; Deserialize for T { fn deserialize(s: &amp;amp;str) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { let j = parse(s)?; FromJson::from_json_value(j) } }

// Primitive impls
impl ToJson for String { fn to_json_value(&amp;amp;self) -&amp;gt; Json { Json::String(self.clone()) } }
impl ToJson for &amp;amp;str { fn to_json_value(&amp;amp;self) -&amp;gt; Json { Json::String(self.to_string()) } }
impl ToJson for bool { fn to_json_value(&amp;amp;self) -&amp;gt; Json { Json::Bool(*self) } }
impl ToJson for f64 { fn to_json_value(&amp;amp;self) -&amp;gt; Json { Json::Number(*self) } }
impl ToJson for i64 { fn to_json_value(&amp;amp;self) -&amp;gt; Json { Json::Number(*self as f64) } }
impl ToJson for u64 { fn to_json_value(&amp;amp;self) -&amp;gt; Json { Json::Number(*self as f64) } }

impl FromJson for String { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { if let Json::String(s) = v { Ok(s) } else { Err("expected string".into()) } } }
impl FromJson for bool { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { if let Json::Bool(b) = v { Ok(b) } else { Err("expected bool".into()) } } }
impl FromJson for f64 { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { if let Json::Number(n) = v { Ok(n) } else { Err("expected number".into()) } } }
impl FromJson for i64 { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { if let Json::Number(n) = v { Ok(n as i64) } else { Err("expected number".into()) } } }
impl FromJson for u64 { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { if let Json::Number(n) = v { Ok(n as u64) } else { Err("expected number".into()) } } }

// Option
impl&amp;lt;T: ToJson&amp;gt; ToJson for Option&amp;lt;T&amp;gt; { fn to_json_value(&amp;amp;self) -&amp;gt; Json { match self { Some(v) =&amp;gt; v.to_json_value(), None =&amp;gt; Json::Null } } }
impl&amp;lt;T: FromJson&amp;gt; FromJson for Option&amp;lt;T&amp;gt; { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { match v { Json::Null =&amp;gt; Ok(None), other =&amp;gt; Ok(Some(FromJson::from_json_value(other)?)) } } }

// Vec
impl&amp;lt;T: ToJson&amp;gt; ToJson for Vec&amp;lt;T&amp;gt; { fn to_json_value(&amp;amp;self) -&amp;gt; Json { Json::Array(self.iter().map(|v| v.to_json_value()).collect()) } }
impl&amp;lt;T: FromJson&amp;gt; FromJson for Vec&amp;lt;T&amp;gt; { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { if let Json::Array(arr) = v { arr.into_iter().map(|el| FromJson::from_json_value(el)).collect() } else { Err("expected array".into()) } } }

// HashMap&amp;lt;String, T&amp;gt;
impl&amp;lt;T: ToJson&amp;gt; ToJson for HashMap&amp;lt;String, T&amp;gt; { fn to_json_value(&amp;amp;self) -&amp;gt; Json { let mut m = HashMap::new(); for (k, v) in self.iter() { m.insert(k.clone(), v.to_json_value()); } Json::Object(m) } }
impl&amp;lt;T: FromJson&amp;gt; FromJson for HashMap&amp;lt;String, T&amp;gt; { fn from_json_value(v: Json) -&amp;gt; Result&amp;lt;Self, String&amp;gt; { if let Json::Object(map) = v { let mut out = HashMap::new(); for (k, v) in map { out.insert(k, FromJson::from_json_value(v)?); } Ok(out) } else { Err("expected object".into()) } } }

&lt;/code&gt;&lt;/pre&gt;</description>
      <author>zzz6519003</author>
      <pubDate>Thu, 19 Mar 2026 21:08:18 +0800</pubDate>
      <link>https://ruby-china.org/topics/44525</link>
      <guid>https://ruby-china.org/topics/44525</guid>
    </item>
    <item>
      <title>有没有山东省的 ruby on rails 爱好者？请加我，一起做事</title>
      <description>&lt;p&gt;有没有山东省的 ruby on rails 爱好者？请加我，一起做事，微信：shibincare&lt;/p&gt;</description>
      <author>shibin</author>
      <pubDate>Thu, 19 Mar 2026 17:56:16 +0800</pubDate>
      <link>https://ruby-china.org/topics/44524</link>
      <guid>https://ruby-china.org/topics/44524</guid>
    </item>
    <item>
      <title>没买海外 vps 时如何解决 kamal deploy 的 EOF 问题</title>
      <description>&lt;p&gt;最近在部署 &lt;code&gt;3qruok.com&lt;/code&gt; 的时候被墙又戏耍了，在国内环境下使用 Docker，网络问题始终是绕不开的痛点。即便你本地开启了系统的 TUN 模式或全局代理，在使用 Kamal 进行部署时，依然可能遇到下面这种让人冒火的“玄学”报错：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DEBUG [1ecc2695] ERROR: failed to build: failed to solve: failed to fetch anonymous token: 
Get "[https://auth.docker.io/token?scope=repository%3Alibrary%2Fruby%3Apull&amp;amp;service=registry.docker.io](https://auth.docker.io/token?scope=repository%3Alibrary%2Fruby%3Apull&amp;amp;service=registry.docker.io)": EOF
...
docker stdout: #0 building with "kamal-local-docker-container" instance using docker-container driver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;明明机器已经开启了 TUN 模式，为什么 Docker 构建时还是拿不到 Token 导致 EOF？&lt;/p&gt;
&lt;h2 id="核心矛盾：消失的网络继承"&gt;核心矛盾：消失的网络继承&lt;/h2&gt;
&lt;p&gt;问题的根源在于 Kamal 默认的构建机制。Kamal 在打包时会创建一个名为 &lt;code&gt;kamal-local-docker-container&lt;/code&gt; 的构建器，它使用的是 &lt;strong&gt;&lt;code&gt;docker-container&lt;/code&gt; 驱动&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="什么是 docker-container 驱动？"&gt;什么是 docker-container 驱动？&lt;/h3&gt;
&lt;p&gt;它的本质是在你的 Docker 中启动一个独立的 &lt;strong&gt;BuildKit 容器&lt;/strong&gt;：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;它的使命&lt;/strong&gt;：实现跨平台构建（Multi-platform builds）。它内部集成了一个独立的 &lt;strong&gt;QEMU 模拟器&lt;/strong&gt;，这使得它能完全脱离宿主机硬件架构的影响。无论你的 Mac 是 Intel 还是 M 系列芯片，它都能为你“直出”目标环境（如 x86）的镜像。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;它的副作用&lt;/strong&gt;：由于它运行在一个高度隔离的容器内，它拥有&lt;strong&gt;完全独立的网络命名空间&lt;/strong&gt;。它不会自动继承宿主机的 TUN 代理或环境变量，甚至会忽略你在 Docker Desktop 界面设置的镜像源（Registry Mirrors）。这导致它在尝试拉取基础镜像（如 &lt;code&gt;ruby:slim&lt;/code&gt;）时，依然在直接撞墙。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="解决方案：回归宿主机驱动"&gt;解决方案：回归宿主机驱动&lt;/h2&gt;
&lt;p&gt;要解决这个问题，最简单的办法是让构建任务交还给宿主机的 Docker Daemon 来处理，从而直接利用宿主机已经配置好的网络环境。&lt;/p&gt;
&lt;h3 id="操作步骤"&gt;操作步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;修改 &lt;code&gt;deploy.yml&lt;/code&gt;&lt;/strong&gt;
在配置文件中明确指定 &lt;code&gt;builder&lt;/code&gt; 的驱动为 &lt;code&gt;docker&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;builder:
  driver: docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;提交配置（关键点）&lt;/strong&gt;
Kamal 部署时会检查 Git 状态。如果 &lt;code&gt;deploy.yml&lt;/code&gt; 的修改没有被 &lt;code&gt;git add&lt;/code&gt;，Kamal 可能会读取旧的配置导致修改不生效：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add config/deploy.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;清理旧的构建器&lt;/strong&gt;
删掉那个由于网络原因卡死的旧构建实例，强制 Kamal 重新初始化：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker buildx rm kamal-local-docker-container
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;重新运行部署&lt;/strong&gt;
执行 &lt;code&gt;kamal deploy&lt;/code&gt;，此时构建进程将直接调用本地 Docker 环境。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;h2 id="深度解析：为什么 driver: docker 依然能打出 x86 镜像？"&gt;深度解析：为什么 &lt;code&gt;driver: docker&lt;/code&gt; 依然能打出 x86 镜像？&lt;/h2&gt;
&lt;p&gt;你可能会担心：如果不用 &lt;code&gt;docker-container&lt;/code&gt; 驱动，我在 ARM 架构的 Mac 上还能打出生产环境需要的 x86 镜像吗？&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;答案是肯定的。&lt;/strong&gt; 这是因为 &lt;strong&gt;Docker Desktop for Mac&lt;/strong&gt; 已经在其底层的 Linux 虚拟机里注册了 QEMU。当你切换到 &lt;code&gt;driver: docker&lt;/code&gt; 时：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;架构模拟&lt;/strong&gt;：依然由 Docker 虚拟层底层的 QEMU 负责，跨平台打包能力依然存在。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;网络控制&lt;/strong&gt;：构建进程此时运行在宿主机 Docker 的“亲生”环境下，它能完美识别并使用你本地调教好的代理或镜像站。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;在 Kamal 部署流程中，如果你遭遇了 Docker 认证阶段的 EOF 错误，通常是因为默认的构建容器陷入了“网络孤岛”。&lt;/p&gt;

&lt;p&gt;将 &lt;code&gt;builder&lt;/code&gt; 的驱动修改为 &lt;code&gt;docker&lt;/code&gt; 是目前国内开发者最务实的解决方案。它通过牺牲一点点构建隔离性，换取了对宿主机网络环境的完美继承，让部署过程不再卡在第一步。&lt;/p&gt;</description>
      <author>jicheng1014</author>
      <pubDate>Wed, 18 Mar 2026 11:40:51 +0800</pubDate>
      <link>https://ruby-china.org/topics/44521</link>
      <guid>https://ruby-china.org/topics/44521</guid>
    </item>
  </channel>
</rss>
