<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>crossoverJie&#39;s Blog</title>
  
  <subtitle>baller</subtitle>
  <link href="http://crossoverjie.top/atom.xml" rel="self"/>
  
  <link href="http://crossoverjie.top/"/>
  <updated>2026-07-30T08:43:22.955Z</updated>
  <id>http://crossoverjie.top/</id>
  
  <author>
    <name>crossoverJie</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>MCP大版本发布：无状态核心、破坏性变更，以及它到底省不省 token</title>
    <link href="http://crossoverjie.top/2026/07/30/AI/mcp-2026-07-28-release/"/>
    <id>http://crossoverjie.top/2026/07/30/AI/mcp-2026-07-28-release/</id>
    <published>2026-07-30T00:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260730163724674.png"></p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近 MCP 又发大版本了，<a href="https://modelcontextprotocol.io/specification/2026-07-28/">MCP 2026-07-28 规范</a>。算下来这已经是第五个版本，也是改动最大的一次。</p><p>重点是以下内容：</p><ul><li>这次到底改了什么?</li><li>社区怎么看?</li><li>旧版的痛点它堵了多少?</li><li>以及那个老生常谈的问题”MCP 是不是很浪费 token”新版是否有优化？</li></ul><blockquote><p>原文是 Anthropic 的 <a href="https://claude.com/blog/bringing-mcp-2026-07-28-to-claude">Bringing MCP 2026-07-28 to Claude</a>，发布说明在 <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">modelcontextprotocol.io</a>。</p></blockquote><hr><h2 id="一、无状态核心，这次最大的改动"><a href="#一、无状态核心，这次最大的改动" class="headerlink" title="一、无状态核心，这次最大的改动"></a>一、无状态核心，这次最大的改动</h2><p>MCP 的<a href="https://claude.com/blog/bringing-mcp-2026-07-28-to-claude">月 SDK 下载量</a>今年已经破了 <strong>4 亿次</strong>，增长了 4 倍，基本坐稳了”AI Agent 接外部工具”的事实标准。<br>这次 2026-07-28 最大的区别是，是把协议核心从”双向有状态”改成了”请求&#x2F;响应”的无状态模型。</p><p>说白了，以前 MCP 要靠 <code>initialize</code> 握手 + <code>Mcp-Session-Id</code> 把客户端和服务端绑在一个会话上。这带来一个很现实的麻烦：你想横向扩展远程 MCP 服务，就得上 sticky session、共享会话存储，或者专门搞个网关来解析 JSON 路由，部署成本一下就上去了。</p><p>无状态化之后，server 可以像普通 HTTP 服务一样直接丢到 Serverless &#x2F; 边缘节点上，构建和扩展都简单不少。对做基础设施的人来说，这点比加什么新 feature 都实在。</p><p>配套的还有两块：</p><ul><li><strong>标准化扩展</strong>：<a href="https://modelcontextprotocol.io/extensions/apps/overview">MCP Apps</a>（在对话里直接渲染交互式 UI）和 <a href="https://modelcontextprotocol.io/extensions/tasks/overview">Tasks</a>（长耗时任务）被纳入版本化扩展框架。以后想加能力不用动核心协议，走扩展就行。</li><li><strong>授权强化</strong>：对齐生产级的 OAuth 2.0 &#x2F; OIDC，能直接对接 Entra、Okta 这些企业身份系统，不用再搞变通方案。</li></ul><p>Claude 这边也在跟着落地，<a href="https://claude.ai/directory/connectors">连接器目录</a>已经收了超过 <strong>950 个 MCP server</strong>，每天几百万人用。除了上面说的 <a href="https://claude.com/blog/interactive-tools-in-claude">Apps</a> 和<a href="https://claude.com/blog/enterprise-managed-auth">企业托管认证</a>，还推了<a href="https://claude.com/blog/observability-for-developers-building-connectors">开发者可观测性仪表盘</a>，以及一个研究预览性质的 <a href="https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview">MCP 隧道</a>——把 Claude 连到私有网络里的 server，不用把内网服务暴露到公网。</p><hr><h2 id="二、社区态度：方向正确，代价不小"><a href="#二、社区态度：方向正确，代价不小" class="headerlink" title="二、社区态度：方向正确，代价不小"></a>二、社区态度：方向正确，代价不小</h2><p>我翻了一圈技术社区，主流对无状态化是认可的，觉得这是 MCP 走向生产级部署的必经之路。能像普通 HTTP 服务一样水平扩展、部署到 Serverless，大家觉得这步走对了。Figma 在测试期就基于新规范在搭，原话大意是”用的人越多，我们的无状态架构越能跟着扩”。</p><p>但吐槽也不少，集中在一件事上：<strong>破坏性变更（Breaking Changes）</strong>。</p><p>这次一口气改了大概 6 处协议行为：</p><ul><li><code>initialize</code> 握手没了；</li><li><code>Mcp-Session-Id</code> 被移除；</li><li>几个核心能力被废弃（<code>resources/list</code>、<code>prompts/list</code>、<code>sampling</code> 部分）；</li><li>OAuth 要求显著收紧。</li></ul><p>已经在跑远程 MCP server 的团队，得把”有状态会话”改造成”无状态 + 显式状态句柄”。网上一下冒出大量”迁移实战 &#x2F; 避坑指南”，本身就说明落地的阵痛是真实的。OAuth 收紧虽然更安全，但旧实现基本得重写认证逻辑，这个工作量不少团队是避不开的。</p><blockquote><p>社区情绪大概是这样：方向对，代价不小。无状态化大家服，但一次性塞进来这么多 breaking change，尤其是 OAuth 这块，怨言挺多。</p></blockquote><hr><h2 id="三、旧版的痛点，新版堵了多少"><a href="#三、旧版的痛点，新版堵了多少" class="headerlink" title="三、旧版的痛点，新版堵了多少"></a>三、旧版的痛点，新版堵了多少</h2><p>我顺手把旧版 MCP 被人诟病的地方和新版对照了一下，做个表：</p><table><thead><tr><th>#</th><th>旧版被诟病的问题</th><th>新版（2026-07-28）的优化</th><th>解决程度</th></tr></thead><tbody><tr><td>1</td><td>有状态会话，难水平扩展，得上网关&#x2F;共享存储</td><td>无状态核心，移除握手和 Session，改成请求&#x2F;响应 + 显式状态句柄</td><td>✅ 根本解决</td></tr><tr><td>2</td><td>认证混乱，协议层几乎无安全要求，常见”裸奔”部署</td><td>OAuth 2.1 + PKCE 强制，要求 <code>/.well-known/oauth-authorization-server</code> 元数据端点</td><td>✅ 大幅改进</td></tr><tr><td>3</td><td>传输层不统一，旧 SSE 远程部署困难</td><td>Streamable HTTP 取代 SSE 成为标准远程传输</td><td>✅ 解决</td></tr><tr><td>4</td><td>概念过载，<code>resources/prompts/sampling</code> 增加复杂度</td><td>废弃 3 个核心原语，能力交给扩展机制</td><td>✅ 简化</td></tr><tr><td>5</td><td>缺乏可观测性，排障靠自研埋点</td><td>Claude 侧推连接器开发者可观测性仪表盘</td><td>⚠️ 仅 Claude 侧</td></tr><tr><td>6</td><td>远程接入要暴露公网，要防火墙&#x2F;白名单</td><td>MCP 隧道（研究预览）免公网暴露接入</td><td>⚠️ 研究预览</td></tr></tbody></table><p>架构类的老问题（扩展性、认证、传输、复杂度）新版基本都堵上了。但安全这块要泼点冷水：协议层”明确不在协议层强制安全”，鉴权授权全靠各团队自己实现。新版靠 OAuth 2.1 &#x2F; PKCE 部分缓解，但工具投毒（恶意指令藏在工具描述里）、提示注入、Rug Pull 这类 <strong>AI 语义层攻击，规范从根上防不了</strong>。之前出过的 <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-49596">CVE-2025-49596</a>（MCP Inspector 未授权 RCE，CVSS 9.4）就是血的教训，这些还得靠网关、最小权限、供应链扫描、AI 检测做纵深防御。</p><blockquote><p>所以准确的说法是：新版把”架构类”问题堵了，但”协议不强制安全 + AI 语义攻击”这类根因只是缓解，不是根治。</p></blockquote><hr><h2 id="四、被吐槽”浪费-token”，新版到底管不管？"><a href="#四、被吐槽”浪费-token”，新版到底管不管？" class="headerlink" title="四、被吐槽”浪费 token”，新版到底管不管？"></a>四、被吐槽”浪费 token”，新版到底管不管？</h2><p>MCP 一直被人吐槽”工具定义全量灌进 context，还没说话就把窗口撑爆”。具体有两层膨胀：</p><ul><li><strong>Tool Definition Bloat（最严重）</strong>：客户端一连接，就把所有工具定义（名字、描述、输入输出 JSON Schema）一把塞进上下文。企业场景 20 个 server × 20 个工具 &#x3D; 400 个定义，光工具定义就能吃掉 10 万+ token。有开发者实测，连 7 个 server 还没输入就占了 67,300 token。</li><li><strong>Tool Result Bloat</strong>：多步工作流里大文件在工具间反复搬运，一份 5 万 token 的文档经”取回→注入→再塞进下一调用”能翻倍到近 10 万 token。</li></ul><p><strong>但关键是：2026-07-28 规范本身并没有从协议层解决 token 浪费。</strong> 它做的是无状态、OAuth、扩展框架，对 token 问题是中性甚至间接的。规范仍然要求 <code>tools/list</code> 返回全量工具目录，没有强制或提供”按需加载 &#x2F; 分页 &#x2F; 工具搜索”原语。</p><p>真正省 token 的，是规范之外、客户端和厂商层做的”渐进式披露（progressive disclosure）”机制，而且大多在 2026-07-28 之前就发了：</p><table><thead><tr><th>机制</th><th>发布方 &#x2F; 时间</th><th>节省幅度</th><th>是否在新规范里</th></tr></thead><tbody><tr><td><a href="https://mcp.directory/blog/mcp-context-bloat-fix-2026-tool-search-code-mode-progressive-disclosure">Tool Search</a>（搜索后再加载，<code>defer_loading:true</code>）</td><td>Anthropic &#x2F; 2025-11</td><td>约 **85%**，评测准确率反而升</td><td>❌ 客户端行为</td></tr><tr><td><a href="https://mcp.directory/blog/mcp-context-bloat-fix-2026-tool-search-code-mode-progressive-disclosure">Code Mode</a>（大 API 包成 typed SDK + 沙箱）</td><td>Cloudflare &#x2F; 2026-02</td><td>输入 token 降 <strong>99.9%</strong></td><td>❌ 产品特性</td></tr><tr><td><a href="https://mcp.directory/blog/mcp-context-bloat-fix-2026-tool-search-code-mode-progressive-disclosure">Code Execution with MCP</a>（server 暴露为 TS 文件，只回最终结果）</td><td>Anthropic &#x2F; 2025-11</td><td>降 <strong>98.7%</strong></td><td>❌ 客户端能力</td></tr></tbody></table><p><a href="https://mcp.directory/blog/mcp-context-bloat-fix-2026-tool-search-code-mode-progressive-disclosure">MCP.Directory 的梳理</a>说得很直白：规范里对懒加载只字未提，客户端连上 server 就调 <code>tools/list</code> 拿全量目录拼进 system prompt，上面这些省 token 的手段全是客户端机制，不是协议变更。</p><p>所以结论要厘清：新版在 token 效率上是”顺带搭了便车”（核心概念面缩小、server 更轻量），但没从协议根上改。只要客户端不做渐进式披露，token 膨胀照旧。</p><hr><h2 id="五、CLI-vs-MCP：现在推荐用哪个？"><a href="#五、CLI-vs-MCP：现在推荐用哪个？" class="headerlink" title="五、CLI vs MCP：现在推荐用哪个？"></a>五、CLI vs MCP：现在推荐用哪个？</h2><p>最后聊个更落地的：MCP 和 CLI 到底怎么选。结论先放前面，这俩不是二选一替代，而是按”系统边界”分工的两种接口范式，生产环境大多 hybrid 一起用。</p><p>CLI（git &#x2F; docker &#x2F; kubectl &#x2F; gh 这类）被推荐用于<strong>本地、单用户、开发者向</strong>的工具，相对 MCP 的好处很实在：</p><table><thead><tr><th>维度</th><th>CLI 的好处</th></tr></thead><tbody><tr><td>Token 成本</td><td>不用每请求注入完整工具 schema，实测少 <strong>4–32×</strong> token</td></tr><tr><td>模型熟悉度</td><td>git&#x2F;docker&#x2F;gh 海量训练语料，模型天生会调</td></tr><tr><td>结构化输出</td><td><code>--json</code> + <code>jq</code> 直接拿</td></tr><tr><td>失败信号</td><td>退出码 + 报错文本，比 schema 错误好自我纠错</td></tr><tr><td>工作流压缩</td><td>一条命令藏复杂 pipeline，适合探索式迭代</td></tr></tbody></table><p><a href="https://community.ibm.com/community/user/blogs/jia-qi/2026/04/08/mcp-vs-cli">IBM 那篇对比</a>给了一个我挺认同的心智模型：</p><ul><li><strong>MCP</strong>：从预定义工具集里选，确定性、schema 驱动、一次性正确，”消除不确定性”。</li><li><strong>CLI</strong>：模型生成命令，靠”猜测→执行→观察→纠正”循环，”管理不确定性”。</li></ul><p>不过 <a href="https://labs.scale.com/blog/mcp-vs-cli">Scale Labs 做过一个受控实验</a>，打破了”CLI 一定更优”的神话——同一套后端工具在 50 个长程任务上 1:1 对比，结论是接口强弱取决于模型和任务，前沿模型基本抹平了差距。还有一条很关键：<strong>别同时把两种接口都暴露给模型指望它自己选</strong>，实验里模型几乎都退回用 MCP，只有显式路由（批量检索走 CLI、精准读写走 MCP）才可靠。</p><table><thead><tr><th>用 CLI</th><th>用 MCP</th><th>两者都用（最常见）</th></tr></thead><tbody><tr><td>本地、单用户</td><td>远程 SaaS、多租户</td><td>既写代码又跑业务</td></tr><tr><td>有成熟 CLI：git&#x2F;docker&#x2F;kubectl&#x2F;gh</td><td>无 CLI 的企业系统：Salesforce、SAP、Workday</td><td>Claude Code、Cursor 这类编码 Agent</td></tr><tr><td>认证在本地</td><td>OAuth&#x2F;API key、需租户隔离</td><td>工具路由自动选 <code>gh</code> 或 MCP</td></tr><tr><td>探索式、容错迭代</td><td>正确性优先、不可试错（支付&#x2F;审批）</td><td>—</td></tr></tbody></table><p>给我的感觉是 CLI 适合个人、而 MCP 更适合企业使用。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260730163724674.png&quot;&gt;&lt;/p&gt;
&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;</summary>
      
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="MCP" scheme="http://crossoverjie.top/tags/MCP/"/>
    
  </entry>
  
  <entry>
    <title>注意：StarRocks 3.5.x 滚动升级触发 MV 全量刷新问题</title>
    <link href="http://crossoverjie.top/2026/07/24/starrocks/StarRocks-rolling-upgrade-MV-force-refresh/"/>
    <id>http://crossoverjie.top/2026/07/24/starrocks/StarRocks-rolling-upgrade-MV-force-refresh/</id>
    <published>2026-07-24T08:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.978Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260724112251196.png"></p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>我们的 StarRocks 集群一直跑在 3.5 分支上，最近发现 <a href="https://github.com/StarRocks/starrocks/pull/57371">PR #57371</a> 只 backport 到了 3.4，3.5 没收到，导致 MV 从 inactive 切到 active 的时候走的是 <code>force=true</code>，也就是全量刷新。</p><p>于是我手动把这个修复 cherry-pick 到了我们的 3.5 分支，改掉 <code>force=true</code> 的问题之后发了版。发版本身没问题，但发版触发了 FE 滚动重启，重启过程中集群突然被打满——所有分区的物化视图都在对全历史分区做全量刷新。</p><p>排查之后发现是三个因素凑到一起：3.5.15 引入的异步 MV reload + 未修复的 <code>force=true</code> 激活路径 + 滚动重启时的 Leader 切换。</p><h2 id="时间线"><a href="#时间线" class="headerlink" title="时间线"></a>时间线</h2><ul><li><strong>16:01 - 17:01</strong>：FE-2 上的周期刷新一直是 <code>forceRefresh=false</code>，只刷当天分区。</li><li><strong>17:01 后</strong>：FE-2 停止刷新，开始进入滚动重启窗口。</li><li><strong>17:07:59</strong>：FE-1 上的 <code>MVActiveChecker</code> 触发 <code>ALTER MATERIALIZED VIEW ... ACTIVE</code>，随后进入 <code>refreshMaterializedView(..., FORCE=true, ...)</code>。</li><li><strong>17:09 - 17:15</strong>：FE-1 持续把 <code>forceRefresh=true</code> 传播到后续 batch task，开始刷历史分区。</li></ul><h1 id="问题在哪"><a href="#问题在哪" class="headerlink" title="问题在哪"></a>问题在哪</h1><h2 id="异步-MV-reload（3-5-15-引入）"><a href="#异步-MV-reload（3-5-15-引入）" class="headerlink" title="异步 MV reload（3.5.15 引入）"></a>异步 MV reload（3.5.15 引入）</h2><p>FE 重启加载 checkpoint 镜像时，<code>MaterializedView.onReload()</code> 先把所有 MV 设为 <code>active = false</code>，然后提交异步任务去检查基表可用性，检查通过再恢复 <code>active</code>。这个异步检查可能耗时几分钟，这期间 MV 都处于 inactive 状态。</p><h2 id="MVActiveChecker-补刀"><a href="#MVActiveChecker-补刀" class="headerlink" title="MVActiveChecker 补刀"></a>MVActiveChecker 补刀</h2><p><code>MVActiveChecker</code> 是一个后台守护线程，默认每 60 秒扫一次，看到 inactive 的 MV 就自动执行 <code>ALTER MATERIALIZED VIEW xxx ACTIVE</code>。重启后 grace-period map 是空的，没有任何退避，扫到就激活。</p><h2 id="force-x3D-true（3-5-分支的老代码）"><a href="#force-x3D-true（3-5-分支的老代码）" class="headerlink" title="force&#x3D;true（3.5 分支的老代码）"></a>force&#x3D;true（3.5 分支的老代码）</h2><p><code>ALTER MATERIALIZED VIEW xxx ACTIVE</code> 的代码里，激活后会调用 <code>refreshMaterializedView(..., force=true, ...)</code>。3.5 分支切出来的时候，社区刚好有一个把 <code>force</code> 改为 <code>false</code> 的 PR 合入了 main 分支，但 3.5 没收到 backport。</p><p>最近社区也发现了这个问题，有客户现场出现了 MV 全量刷新导致数据同步出现延迟，然后才 backport 到 3.5.21 这个版本上。</p><p><a href="https://github.com/StarRocks/starrocks/pull/76576">PR #76576</a> 的说明里写得很清楚：</p><blockquote><p>since 3.5.15 the async MV reload on FE restart makes this a serious regression: during image load onReload() sets active &#x3D; false first and restores it asynchronously, leaving a minutes-long window in which MVs are transiently inactive. MVActiveChecker picks them up and runs ALTER MATERIALIZED VIEW xxx ACTIVE, and the activation path calls refreshMaterializedView(…, force&#x3D;true, …) — so every FE leader restart triggers a FORCE full refresh of all async MVs.</p></blockquote><p>所以触发链路是：</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs routeros">FE 重启 → async reload 设 <span class="hljs-attribute">active</span>=<span class="hljs-literal">false</span> → MVActiveChecker 激活 <br>→ refreshMaterializedView(<span class="hljs-attribute">force</span>=<span class="hljs-literal">true</span>) → 全量刷新<br></code></pre></td></tr></table></figure><h2 id="滚动重启放大"><a href="#滚动重启放大" class="headerlink" title="滚动重启放大"></a>滚动重启放大</h2><p>我们的 K8s 集群有 3 个 FE Pod，滚动更新逐个重启。每次当前 Leader 被重启，就会触发 failover，新 Leader 的 <code>MVActiveChecker</code> 再扫一遍所有 inactive 的 MV，又来一波全量刷新。也就是说，只要某一轮切换后的 Leader 还没拿到修复版代码，就可能再刷一波；实际波次数取决于 leader 切换路径和滚动顺序。</p><h2 id="日志证据"><a href="#日志证据" class="headerlink" title="日志证据"></a>日志证据</h2><p>日志里能看到 FE-2 先一直正常周期刷新，<code>forceRefresh=false</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">2026-07-23 16:01:09  FE-2  forceRefresh=false<br>2026-07-23 16:16:11  FE-2  forceRefresh=false<br>2026-07-23 17:01:09  FE-2  forceRefresh=false<br></code></pre></td></tr></table></figure><p>随后 FE-1 在 17:07:59 接管并触发自动激活：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">2026-07-23 17:07:59  MVActiveChecker Start to activate MV dws_trd_period_sales_offline_store<br>2026-07-23 17:07:59  LocalMetastore.executeRefreshMvTask() FORCE=true<br>2026-07-23 17:09:24  FE-1  forceRefresh=true<br>2026-07-23 17:15:54  FE-1  forceRefresh=true<br></code></pre></td></tr></table></figure><p>这说明滚动重启窗口内，Leader 已经切到了 FE-1，且当时 FE-1 仍在执行未修复的激活逻辑。</p><h2 id="代码位置"><a href="#代码位置" class="headerlink" title="代码位置"></a>代码位置</h2><ul><li><code>fe/fe-core/src/main/java/com/starrocks/scheduler/MVActiveChecker.java</code></li><li><code>fe/fe-core/src/main/java/com/starrocks/alter/AlterMVJobExecutor.java</code></li><li><code>fe/fe-core/src/main/java/com/starrocks/server/LocalMetastore.java</code></li><li><code>fe/fe-core/src/main/java/com/starrocks/scheduler/PartitionBasedMvRefreshProcessor.java</code></li></ul><h1 id="实际影响"><a href="#实际影响" class="headerlink" title="实际影响"></a>实际影响</h1><p>我们集群里 MV 会做全历史分区全量刷新，CN 节点 CPU 打满，数据延迟数小时。</p><h1 id="怎么规避"><a href="#怎么规避" class="headerlink" title="怎么规避"></a>怎么规避</h1><p>3.5.21 已经合了上面的 bugfix，<code>force=true</code> 改成了 <code>force=false</code>。但如果你在升级 <strong>到</strong> 3.5.21 或升级 <strong>后</strong> 还需要做 FE 重启，保险起见可以先关掉 <code>MVActiveChecker</code>：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql">ADMIN <span class="hljs-keyword">SET</span> FRONTEND CONFIG (&quot;enable_mv_automatic_active_check&quot; <span class="hljs-operator">=</span> &quot;false&quot;);<br></code></pre></td></tr></table></figure><p>确认生效：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql">ADMIN <span class="hljs-keyword">SHOW</span> FRONTEND CONFIG <span class="hljs-keyword">LIKE</span> &quot;%enable_mv_automatic_active_check%&quot;;<br></code></pre></td></tr></table></figure><p>滚动重启全部完成后，确认集群稳定（Leader 不再切换、没有历史分区的刷新任务在跑），再打开：</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql">ADMIN <span class="hljs-keyword">SET</span> FRONTEND CONFIG (&quot;enable_mv_automatic_active_check&quot; <span class="hljs-operator">=</span> &quot;true&quot;);<br></code></pre></td></tr></table></figure><p>关闭期间不影响定时刷新和手动刷新，只影响”从 inactive 自动恢复 active”这一种场景，业务无感。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这个问题本质上是三个因素碰到了一起：异步 reload 制造了 inactive 窗口、MVActiveChecker 在这个窗口里激活了 MV、激活路径又走的是 <code>force=true</code>。3.5.21 修了 <code>force=true</code>，但如果之后 FE 重启时异步 reload 还没完成，MVActiveChecker 仍可能先动手；只是刷新会变成增量，不再是全量。</p><p>所以升级前后的保险操作是：<strong>先关掉自动激活，等全部 FE 滚动完成且 Leader 稳定后，再重新打开。</strong></p><p>参考链接：</p><ul><li><a href="https://github.com/StarRocks/starrocks/pull/76576">https://github.com/StarRocks/starrocks/pull/76576</a></li><li><a href="https://github.com/StarRocks/starrocks/pull/57371">https://github.com/StarRocks/starrocks/pull/57371</a></li></ul><p>额外再补充一下，现在 DS 感觉降智有点严重，整个问题的分析过程我分别交给了 DS v4 Pro 和 kimi2.7</p><p>kimi2.7 很快就定位到是滚动更新导致 leader 切换出现的问题，希望 DS 最近要发布的正式版</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260724112251196.png&quot;&gt;&lt;/p&gt;
&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;</summary>
      
    
    
    
    <category term="StarRocks" scheme="http://crossoverjie.top/categories/StarRocks/"/>
    
    
    <category term="StarRocks" scheme="http://crossoverjie.top/tags/StarRocks/"/>
    
  </entry>
  
  <entry>
    <title>读完 Bun 用 Rust 重写：1 个人 11 天重写 50 万行代码是怎么做到的</title>
    <link href="http://crossoverjie.top/2026/07/12/AI/bun-rewrite-rust-reading-notes/"/>
    <id>http://crossoverjie.top/2026/07/12/AI/bun-rewrite-rust-reading-notes/</id>
    <published>2026-07-12T00:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260712160208850.png"></p><p>前两天发了一篇 <a href="https://mp.weixin.qq.com/s/WbROkvwrUFw5Lkm4-bDOXw">Rewriting Bun in Rust</a> 原文的翻译，内容有点多，这次我自己整理了一份精简版。</p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/imagesmac_1783837029900.png"></p><p>一开始是因为在 X 上刷到 Bun 官方博客一篇<a href="https://bun.com/blog/bun-in-rust">《Rewriting Bun in Rust》</a>，作者是 Jarred Sumner。看完挺震撼的——1 个工程师，用 CC（Claude Code）+ Claude Fable 5（重写那会儿还是预发布，现在订阅用户已经能正常用了），11 天把整个 Bun 从 Zig 重写成 Rust，落地 diff +100 万行，6,778 个 commit。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260710085026741.png"><br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260710085654920.png"></p><p>这篇是我读完之后按自己的理解整理的笔记，分几个部分讲清楚：<br>为什么要扔掉 Zig、<br>为什么选 Rust、<br>整个工作流是怎么搭的、<br>作者和 CC 各自干了什么、<br>怎么保证质量，<br>以及烧了多少钱。</p><blockquote><p>顺带一提：Bun 在 2025 年 12 月被 Anthropic 收购了，作者现在也在 Anthropic。这次重写用的是 Claude Fable 5，重写那会儿（2026 年 5 月）还是预发布版，到截稿今天（2026-07-12）已经对订阅用户开放了。所以这文章多少带点「自家产品自家用了」的意味，但流程本身是真有东西的。</p></blockquote><span id="more"></span><hr><h1 id="为什么要停止使用-zig"><a href="#为什么要停止使用-zig" class="headerlink" title="为什么要停止使用 zig"></a>为什么要停止使用 zig</h1><p>先说清楚一件事：作者没有「骂」Zig，反而一直强调没有 Zig 就没有 Bun。Bun 最初就是把 esbuild 的 JS&#x2F;TS 转译器从 Go 逐行移植到 Zig 的产物，作者 2021 年写下第一行 Zig，一个人一年在奥克兰的出租屋里把 Bun 初版手搓出来。可以说 Zig 是 Bun 的恩人。</p><p>但问题出在「稳定性」上。Bun 的范围大得吓人——转译器、打包器、npm 兼容包管理、类 Jest 测试运行器、Node.js API（<code>fs</code>&#x2F;<code>net</code>&#x2F;<code>tls</code> 一堆）、HTTP&#x2F;WebSocket 客户端，全塞在一个 runtime 里。这种规模 + Zig，结果就是一长串内存安全 bug。原文贴了一小撮，我挑几个有代表性的：</p><table><thead><tr><th>类型</th><th>场景</th></tr></thead><tbody><tr><td>heap-use-after-free</td><td><code>node:zlib</code> 线程池里还有异步 <code>write()</code> 时，对 stream 调 <code>.reset()</code></td></tr><tr><td>use-after-free</td><td><code>node:http2</code> 可重入 JS 回调触发 hashmap rehash，内部流指针失效</td></tr><tr><td>堆越界写</td><td><code>UDPSocket.sendMany()</code> 迭代途中 socket 连接状态被用户回调改写</td></tr><tr><td>内存泄漏</td><td><code>tlsSocket.setSession()</code> 每次调用漏一个 <code>SSL_SESSION</code>（约 6.5KB）</td></tr><tr><td>double-free</td><td>CSS 解析器里 <code>background-clip</code> 带 vendor 前缀 + 多层背景</td></tr><tr><td>竞态崩溃</td><td><code>MessageEvent</code> 的 GC marker 线程观测到被撕裂的 variant</td></tr></tbody></table><p>根因在哪儿？JavaScript 是带 GC 的语言，JavaScriptCore（以及 V8）对异常处理和 GC 有严格规则。而 Zig 跟 C 一样不替你管内存，没有构造函数&#x2F;析构函数，清理工作要靠每个调用点显式写 <code>defer</code>。</p><blockquote><p>这不是 Zig 的锅。其他 Zig 用户没这种 bug，是因为「把 GC 值和手动管理的值混在一起」本身就是个不常见的需求，没有哪门语言会专门为此设计。</p></blockquote><p>对 Bun 来说，正确处理「GC 管的值」和「手动管的值」的生命周期，一直是稳定性问题的主要来源——最常见的是小内存泄漏，偶尔直接崩。每一处内存分配都得仔细 review：这些字节在哪释放？怎么保证只释放一次？JS 异常都检查了吗？这块内存是 GC 管的还是手动管的？</p><p>作者他们也做过不少防护：给 Zig 编译器打补丁加 ASAN、每次 commit 都开着 ASAN 跑测试、用 Fuzzilli 7×24 fuzz 运行时 API、一堆端到端内存泄漏测试。这已经比大多数项目做得多了。但还是堵不住。</p><p>说白了，可以一直一个个修下去，但用户指望着你，理应做得更好——从系统层面防止这类 bug 反复出现。于是有了换语言的念头。</p><h1 id="为什么切换为-rust？"><a href="#为什么切换为-rust？" class="headerlink" title="为什么切换为 rust？"></a>为什么切换为 rust？</h1><p>候选其实有三个：继续用 style guide 硬撑 Zig、换 C++、换 Rust。</p><p><strong>为什么不是「继续用 Zig + style guide」？</strong> 很多项目靠 style guide 回答这类问题，TigerBeetle 的 <a href="https://tigerstyle.dev/">TigerStyle</a>（一份 Zig 的 style guide）、Google 那份 31000 字的 <a href="https://google.github.io/styleguide/cppguide.html">C++ style guide</a> 都是例子。style guide 的难处不在写，在执行——你怎么保证它被遵守？历史上只能靠 code review + linter 尽力而为。作者甚至想过在 Zig 里手搓一套受 Rust 启发的智能指针，但他自己都嫌弃：自制的智能指针比 Rust 体验更差，还换不来任何保证。</p><p><strong>为什么不是 C++？</strong> Bun 大约 20% 的代码本来就是 C++，还内嵌了 JavaScriptCore、uWebSockets、BoringSSL、SQLite 这些 C&#x2F;C++ 库。换 C++ 是个合理选择，能拿到构造&#x2F;析构函数，还能删掉一堆 <code>extern &quot;C&quot;</code> 包装。但内存问题上还是得靠 style guide + code review，即便有 ASAN，内存损坏和泄漏照样会发生。</p><p><strong>为什么是 Rust？</strong> 看回上面那份 bug 清单，很大一部分是 use-after-free、double-free、错误路径上「忘记释放」。在 safe Rust 里，这些都是<strong>编译错误</strong>，外加 <code>Drop</code> 这种类 RAII 的自动清理。</p><blockquote><p>编译错误是比 style guide 更好的反馈回路。</p></blockquote><p>一句话：Rust 把「靠人记」变成「靠编译器兜底」。</p><p>但重写在历史上都是个糟糕主意。去掉注释，Bun 有 535,496 行 Zig。换语言重写要一个小团队整整干一年，期间得冻结 bugfix、安全修复和功能开发。作者选了风险最低的做法——<strong>机械式移植</strong>，行为改动尽量少，用已有的那套测试套件。好在 Bun 自己的测试是用 TypeScript 写的，不依赖 runtime 的实现语言，这给了重写一个「不变量」可以对照。</p><blockquote><p>这里有个关键判断：重写的样子要像把 Zig 转译成 Rust，先不过度追求地道 Rust。等 v1.4 发出去之后，再逐步减少 <code>unsafe</code>、改成更地道的写法。一步到位太贪，容易翻车。</p></blockquote><h1 id="整个切换的工作流是什么？"><a href="#整个切换的工作流是什么？" class="headerlink" title="整个切换的工作流是什么？"></a>整个切换的工作流是什么？</h1><p>这部分是我觉得整篇文章最值得拆开看的地方。作者没有「给 Claude 一句 prompt 然后赌它能成」，而是搭了大约 <strong>50 个 dynamic workflow</strong>（CC 的动态工作流），在 11 天里持续跑。</p><p>工程师日常大量工作可以简化成一个循环：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// 伪代码</span><br><span class="hljs-keyword">let</span> task;<br><span class="hljs-keyword">while</span> ((task = todoList.<span class="hljs-title function_">pop</span>())) &#123;<br>  <span class="hljs-keyword">const</span> result = <span class="hljs-title function_">task</span>();<br>  <span class="hljs-keyword">const</span> feedback = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">all</span>([<span class="hljs-title function_">review</span>(result), <span class="hljs-title function_">review</span>(result)]);<br>  <span class="hljs-keyword">await</span> <span class="hljs-title function_">apply</span>(feedback, result);<br>&#125;<br></code></pre></td></tr></table></figure><p>每个 workflow 都是这样的一个循环，分别干：生成移植指南、逐文件移植 <code>.zig</code>→<code>.rs</code>、修编译错误、修子命令、修测试、大重构。整体节奏大致是这样：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260710085122626.png"></p><table><thead><tr><th>阶段</th><th>干什么</th></tr></thead><tbody><tr><td>Prep</td><td>花 3 小时和 Claude 讨论模式映射，产出 <code>PORTING.md</code>（后来还上了 HN）；再跑一个 workflow 分析每个 struct 字段的生命周期，产出 <code>LIFETIMES.tsv</code>，并经对抗式 review</td></tr><tr><td>试运行</td><td>先只移植 3 个文件，验证流程</td></tr><tr><td>写代码</td><td>峰值每分钟约 1,300 行，6,502 个 commit</td></tr><tr><td>修编译错误</td><td>约 16,000 个错误，按 crate 分给 64 个 Claude</td></tr><tr><td>跑通</td><td><code>bun --version</code> → <code>bun test &lt;file&gt;</code> → 全测试</td></tr><tr><td>CI 全绿</td><td>972 个失败测试文件 → 2 天后 23 个 → 最终 6&#x2F;6 平台全绿</td></tr><tr><td><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260710085408902.png"></td><td></td></tr></tbody></table><p>中间踩了一堆坑，作者原话叫「false starts」：</p><ul><li>Claude 们互相踩脚：一个跑 <code>git stash</code>，另一个跑 <code>git stash pop</code>，还有 <code>git reset --hard</code>。问题在于这几个命令都是对<strong>整个工作区 &#x2F; index &#x2F; stash 栈</strong>的全局性破坏操作，而一开始多个 Claude 共用同一份工作区。<code>git stash</code> 会把别人半成品的改动一起搁走，<code>git stash pop</code> 可能恢复出不相关的改动，<code>git reset --hard</code> 更狠——一个 agent 跑一下，仓库里所有其他 agent 没提交的活瞬间清零。结果就是丢活、冲突、状态错乱，agent 们互相把对方的工作撤销掉。下规则：只允许「针对特定文件的原子提交」（<code>git add &lt;具体文件&gt; &amp;&amp; git commit</code>），禁止任何会全局改动工作区&#x2F;index 的命令（<code>git stash</code>&#x2F;<code>reset --hard</code>&#x2F;<code>checkout .</code> 这类）。光禁还不够，后来又拆成 4 个独立 worktree 物理隔离，才真正解决。</li><li>太慢 → 拆成 4 个 worktree 分片，每个跑 16 个 Claude，峰值同时 64 个 Claude。</li><li>Claude 把「让所有 crate 编译通过」理解成「用 stub 把报错函数糊过去」，还写长注释自圆其说。加规则：<strong>需要一整段注释论证 workaround 为什么没问题，代码就是错的——去修代码。</strong></li><li>压测会把机器 TCP socket 耗尽、写几个 GB 到磁盘、spawn 上万个进程，debug 构建超时。用 <code>systemd-run</code>（cgroups）限制内存&#x2F;CPU、隔离 pid 命名空间。机器还是几次跑光磁盘崩了。</li></ul><h2 id="作者担任的角色是什么？CC-担任的角色又是什么？如何搭配工作的？"><a href="#作者担任的角色是什么？CC-担任的角色又是什么？如何搭配工作的？" class="headerlink" title="作者担任的角色是什么？CC 担任的角色又是什么？如何搭配工作的？"></a>作者担任的角色是什么？CC 担任的角色又是什么？如何搭配工作的？</h2><p>这是我看这篇文章最想搞清楚的问题。结论是：<strong>作者不是只做 review 的人，他是架构师 + 流程设计者 + 监工 + 验收者；CC 是执行者（写代码 + 互相 review + 改代码）。</strong></p><p><strong>作者干的：</strong></p><ul><li><strong>战略决策</strong>：选一次性全量重写而非增量、选机械式移植、定下「先像转译、之后再改地道」的总方针。这些 CC 不会替你拍板。</li><li><strong>流程&#x2F;workflow 设计</strong>：50 个 dynamic workflow 的循环结构是他搭的，每个 workflow 跑什么也是他定的。</li><li><strong>prompt 与规范制定</strong>：产出 <code>PORTING.md</code>、<code>LIFETIMES.tsv</code>，自己人工通读。</li><li><strong>监工 11 天</strong>：原文——「most of those 11 days, I monitored workflows, manually reading the outputs to check for issues and bugs, and prompting Claude to edit the loop to fix things」。也就是人工读输出、发现问题、改 workflow 的 prompt 让 Claude 自己修。</li><li><strong>处理翻车 + 改规则</strong>：上面那些 false starts 的规则都是他下的。</li><li><strong>验收</strong>：合并前人工核实测试「确实在跑、没被跳过」，本地跑一堆命令试，然后才按合并键。还把 Zig 和 Rust 并排人工通读了大量代码。</li></ul><p><strong>CC 干的：</strong></p><ul><li>实际写代码（峰值每分钟约 1,300 行，落地 diff +1,009,272）。</li><li>修编译错误（约 16,000 个）。</li><li>修测试失败（本地 + CI 各平台）。</li><li><strong>对抗式 review</strong>——注意，reviewer 也是 Claude，独立上下文窗口，1 个实现者配 2 个对抗式 reviewer。所以连 review 都大量是 <strong>Claude 审 Claude</strong>。</li></ul><p>两人怎么搭配？核心是一句话：<strong>当出问题时，修掉生成代码的流程，而不是手工修代码。</strong> 作者修的是「过程」，具体代码交给 CC。</p><blockquote><p>这种分工其实挺反直觉的。很多人用 Agent 是「让 Agent 写，我审」。作者反过来：让 Agent 写、Agent 审、Agent 改，自己只盯「流程」和「验收」。把人从执行链路里摘出来，专心做架构和决策。这个思路我个人觉得是这次重写最值得安利的部分。</p></blockquote><p>对抗式 review 还有个设计细节很巧：<strong>拆分上下文窗口</strong>。写代码的 Claude 想让代码被接受（有「赶紧发出去」的偏向），审代码的 Claude 被要求「假定代码是错的，找它不工作的理由」。reviewer 只拿到 diff，看不到实现者的任何推理。1 个实现者 + 2 个以上对抗式 reviewer，实现者不审、reviewer 不写。原文举了一个真抓到的 bug：异步 <code>uv_close</code> 之前 Box 就 drop 了，先 UAF 再 double-free，编译能过、看着也没问题，但 reviewer 抓到了。</p><h1 id="如何保证切换后的代码质量，如何做测试？"><a href="#如何保证切换后的代码质量，如何做测试？" class="headerlink" title="如何保证切换后的代码质量，如何做测试？"></a>如何保证切换后的代码质量，如何做测试？</h1><p>一个 +100 万行的 PR 怎么 review？你怎么建立信心去合并？作者给的答案是三件套：</p><ol><li><strong>一套与语言无关、含上百万断言的测试</strong>。</li><li><strong>对抗式 code review</strong>。</li><li><strong>出问题时，修流程而不是手修代码</strong>。</li></ol><p>测试这块是真扎实。测试用 TypeScript 写，所以不依赖 runtime 语言，重写前后用的是同一套。各平台的断言数：</p><table><thead><tr><th>平台</th><th>expect() 调用数</th><th>测试数</th><th>文件数</th></tr></thead><tbody><tr><td>Debian 13 x64</td><td>1,386,826</td><td>60,624</td><td>4,174</td></tr><tr><td>macOS 14 arm64</td><td>1,259,953</td><td>58,850</td><td>4,175</td></tr><tr><td>Windows 2019 x64</td><td>1,007,544</td><td>57,337</td><td>4,173</td></tr></tbody></table><p>合并前要求 <strong>100% 测试在所有平台的 CI 上都通过</strong>，作者还人工核实测试「确实在跑、没被跳过」——这一步很重要，Agent 偷懒跳测试是经典坑。</p><p>合并之后还在持续加码：</p><ul><li><strong>11 轮安全审查</strong>（Claude Code Security），处理了所有发现。</li><li><strong>7×24 覆盖引导 fuzzing</strong>，覆盖每个解析器——JS、TS、JSX、CSS、JSON5、JSONC、TOML、YAML、Markdown、INI、Bun Shell、semver、.patch、CSS 颜色。fuzzer 找到 bug 自动发给 Claude 提 PR 复现+修复，人审。迄今跑了 1000 亿次，产出约 15 个 PR。</li><li><strong>Miri</strong>（在 CI 里覆盖越来越大的代码段）+ <strong>LeakSanitizer</strong>。</li><li><code>unsafe</code> 占比：约 4% 的 Rust 代码在 <code>unsafe</code> 块里（~13,000 个 <code>unsafe</code> 关键字 &#x2F; ~780,000 行），其中 78% 只有一行——基本是一个来自 C++ 的指针或一次对 C 库的调用。</li></ul><p>当然，重写不可能零回归，这次引入了 <strong>19 个已知回归，全部已修</strong>。多数来自「两门语言语法相同、语义不同」的坑，原文举的例子很值得记一下：</p><ul><li><strong><code>debug_assert!</code> 里的副作用</strong>：Zig 的 <code>assert</code> 是函数，参数每次都跑；Rust 的 <code>debug_assert!</code> 是宏，release 构建里整段被抹掉，连里面的 <code>insert_stale</code> 调用也没了，HMR 在某些场景挂了。debug 构建正常，所以一开始没发现。</li><li><strong>奇数长度切片</strong>：Zig 的 <code>reinterpretSlice</code> 用 <code>@divTrunc</code> 忽略尾部奇数字节，<code>bytemuck::cast_slice</code> 直接 panic。</li><li><strong>边界检查</strong>：Zig 的 <code>ReleaseFast</code> 去掉边界检查，Rust release 保留，于是原本不可达的越一 bug 被触发，Rust panic 而不是写越界。</li><li><strong><code>comptime</code> 格式串</strong>：Zig 里 <code>fmt</code> 是 comptime，颜色标记在参数替换前就处理掉了；Rust 函数没有 comptime 参数，标记被改写到了参数上。</li></ul><blockquote><p>这些坑本质上都是「机械移植」的代价。语法看着一样，语义不一样，最容易懵逼。但也正因为有那套百万断言的测试兜底，才敢这么干。</p></blockquote><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260710090147962.png"><br>效果上，Rust 版的 Bun 确实更好了：内存占用下降（修掉了每个可插桩的内存泄漏，<code>Bun.build()</code> 从每次泄漏约 3MB 变成趋于平稳）、二进制体积缩小约 20%（Linux&#x2F;Windows）、性能快 2%–5%。</p><h1 id="整体使用的模型、token、花费大约是多少？"><a href="#整体使用的模型、token、花费大约是多少？" class="headerlink" title="整体使用的模型、token、花费大约是多少？"></a>整体使用的模型、token、花费大约是多少？</h1><table><thead><tr><th>项目</th><th>数值</th></tr></thead><tbody><tr><td>模型</td><td>Claude Fable 5（Mythos 级别；重写时为预发布，现已对订阅用户开放）</td></tr><tr><td>未缓存输入 token</td><td>59 亿</td></tr><tr><td>输出 token</td><td>6.9 亿</td></tr><tr><td>缓存输入 token 读取</td><td>720 亿</td></tr><tr><td>API 花费</td><td>约 165,000 美元</td></tr><tr><td>峰值并发</td><td>64 个 Claude（4 个 workflow × 16）</td></tr><tr><td>耗时</td><td>11 天（5 月 3 日 → 5 月 14 日合并）</td></tr><tr><td>commit 数</td><td>6,778 个</td></tr></tbody></table><p><strong>没有 LLM 的时代要多少人干多久？</strong> 作者自己的估算：3 个对代码库有完整 context 的工程师，干一年。而且这一年里没法改进 Node.js 兼容性、没法修 bug、没法修安全问题、没法加新功能。作者原话——「我们绝不会这么做」。现实的替代方案就是「什么都不做，一直修文章开头那些 bug」。</p><p>说白了，这次重写之所以能成，不是因为 Rust 比 Zig 好多少，而是因为 LLM 把「重写一年、冻结所有业务」这个成本，压到了「1 个人 11 天 + 16 万刀」。</p><blockquote><p>16 万刀看着贵，但对比「3 个核心工程师一年全力投入、期间零业务推进」的机会成本，这笔账其实算得过。而且 Fable 5 现在已经对订阅用户开放，重度 CC 用户用订阅额度也能跑起来，这套流程会越来越香。</p></blockquote><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>读完这篇，我最大的感受不是「Rust 真好」或者「Zig 不行」，而是「<strong>一个工程师今天能做的，比前些年多得多</strong>」。</p><p>这是一些我总结的经验：</p><ol><li><strong>人从执行链路里摘出来</strong>。让 Agent 写、Agent 审、Agent 改，人只盯「流程设计」和「验收」。这跟很多人用 Agent「让它写、我来审」的直觉是反的，但作者用 11 天 + 100 万行证明了这条路可行。</li><li><strong>修流程，不修代码</strong>。出问题改 workflow 的 prompt 和规则，而不是手修 diff。这是把「一次性修补」变成「系统性防止」的关键。</li><li><strong>对抗式 review + 拆分上下文窗口</strong>。实现者想合并、reviewer 想找茬，天然对立；reviewer 只拿 diff、看不到实现者推理。这套设计可以直接抄到自己的 CC workflow 里。</li><li><strong>语言无关的测试套件是重写的底气</strong>。敢机械移植，前提是有一套百万断言、跟实现语言无关的测试兜底。没有这个，谁也不敢合并 +100 万行 LLM 代码。</li><li><strong>机械移植 &gt; 一步到位</strong>。先像转译、行为不变，之后再改地道。一步到位太贪，容易翻车。这个判断对任何「大重构」都适用。</li></ol><p>当然也有它的特殊性：作者是 Bun 的创造者、对代码库有完整 context，重写那会儿用的还是 Fable 5 预发布版（现在已开放订阅），流程本身也踩了不少坑（互相踩脚、stub 糊编译错误、磁盘跑光）。不是随便谁来都能复刻。但方法论是通用的——<strong>流程设计 + 对抗式 review + 强测试兜底 + LLM 执行</strong>，这套组合拳，值得每个重度用 CC 的人认真琢磨。</p><p>不过说回国内，16 万刀换算过来是一笔不小的开支。最近还有个趋势挺值得警惕：一些老板看到别家公司对外宣传的「Agent 重写」「AI 替代工程师」成果，就也想在内部搞一套，想着用 Agent 替掉几个工程师来省钱。</p><p>但在我看来，这事没这么简单，至少得先想清楚几个问题：</p><ol><li><strong>业务真的需要顶配模型吗？</strong> 大部分业务说白了就是增删改查，拿最贵的模型去跑这种活，杀鸡用牛刀（简单来说都是增删改查）。</li><li><strong>舍得花这么多钱吗？</strong> 16 万刀只是一次重写的 API 费，常态化跑起来账单只会更吓人，这笔账得先算清楚。</li><li><strong>指挥 AI 干活的人，能不能扛起来？</strong> 这次能成的前提是作者本身就是 Bun 的创造者，对代码库有完整 context，能当好架构师 + 流程设计者 + 监工 + 验收者这几个角色。换成公司里一个对业务没那么熟的工程师来指挥，效果天差地别。</li><li><strong>内部怎么评估 AI 带来的效率提升？</strong> 没有量化就只有感觉，而「我感觉快了」这种话，最后往往经不起对账。</li></ol><p>很多老板自己都没想清楚，就被对外宣传裹挟着陷入深深的焦虑，一头扎进去搞 AI 替代，最终的效果往往适得其反——钱花了，人也没省下来，业务还可能被带歪。</p><blockquote><p>让子弹飞一会，但这次子弹已经飞到 100 万行代码量级了。</p></blockquote>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260712160208850.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;前两天发了一篇 &lt;a href=&quot;https://mp.weixin.qq.com/s/WbROkvwrUFw5Lkm4-bDOXw&quot;&gt;Rewriting Bun in Rust&lt;/a&gt; 原文的翻译，内容有点多，这次我自己整理了一份精简版。&lt;/p&gt;
&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/imagesmac_1783837029900.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;一开始是因为在 X 上刷到 Bun 官方博客一篇&lt;a href=&quot;https://bun.com/blog/bun-in-rust&quot;&gt;《Rewriting Bun in Rust》&lt;/a&gt;，作者是 Jarred Sumner。看完挺震撼的——1 个工程师，用 CC（Claude Code）+ Claude Fable 5（重写那会儿还是预发布，现在订阅用户已经能正常用了），11 天把整个 Bun 从 Zig 重写成 Rust，落地 diff +100 万行，6,778 个 commit。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260710085026741.png&quot;&gt;&lt;br&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260710085654920.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这篇是我读完之后按自己的理解整理的笔记，分几个部分讲清楚：&lt;br&gt;为什么要扔掉 Zig、&lt;br&gt;为什么选 Rust、&lt;br&gt;整个工作流是怎么搭的、&lt;br&gt;作者和 CC 各自干了什么、&lt;br&gt;怎么保证质量，&lt;br&gt;以及烧了多少钱。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;顺带一提：Bun 在 2025 年 12 月被 Anthropic 收购了，作者现在也在 Anthropic。这次重写用的是 Claude Fable 5，重写那会儿（2026 年 5 月）还是预发布版，到截稿今天（2026-07-12）已经对订阅用户开放了。所以这文章多少带点「自家产品自家用了」的意味，但流程本身是真有东西的。&lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Claude" scheme="http://crossoverjie.top/tags/Claude/"/>
    
    <category term="Rust" scheme="http://crossoverjie.top/tags/Rust/"/>
    
    <category term="Bun" scheme="http://crossoverjie.top/tags/Bun/"/>
    
    <category term="LLM" scheme="http://crossoverjie.top/tags/LLM/"/>
    
  </entry>
  
  <entry>
    <title>我的 Claude Code 常用 SKILLS 和工具</title>
    <link href="http://crossoverjie.top/2026/06/29/AI/claudecode-skills-tools/"/>
    <id>http://crossoverjie.top/2026/06/29/AI/claudecode-skills-tools/</id>
    <published>2026-06-29T00:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629162029387.png"></p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>已经高强度的使用 claude code 大半年的时间了，积累了不少好用的 SKILLS 和工具，今天来整理一下分享给大家。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629113441276.png"></p><p>我安装了大约 70 几个 SKILLS，但常用的其实也不太多，下面介绍一些我个人觉得效果不错的。推荐使用我开发的 <a href="https://github.com/crossoverJie/SkillDeck">skilldeck</a> 来管理本地的 SKILLS：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">brew tap crossoverJie/skilldeck &amp;&amp; brew install --cask skilldeck<br>xattr -cr /Applications/SkillDeck.app<br></code></pre></td></tr></table></figure><span id="more"></span><hr><h1 id="常用-SKILLS"><a href="#常用-SKILLS" class="headerlink" title="常用 SKILLS"></a>常用 SKILLS</h1><h2 id="个人相关"><a href="#个人相关" class="headerlink" title="个人相关"></a>个人相关</h2><h3 id="agent-notifier"><a href="#agent-notifier" class="headerlink" title="agent-notifier"></a><a href="https://github.com/crossoverJie/skills/blob/main/skills/agent-notifier/SKILL.md">agent-notifier</a></h3><p>这是我自己做的一个 SKILL，主要用于 Agent 完成任务和等待任务时发出通知。虽然现在一些 terminal 已经支持通知了，比如 <a href="https://cmux.com/">cmux</a>、<a href="https://otty.sh/">otty</a>，但是它们都不能推送 IM，后面它们支持之后也就不再需要这个 SKILL 了。</p><h3 id="kami"><a href="#kami" class="headerlink" title="kami"></a><a href="https://github.com/tw93/kami">kami</a></h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629160457783.png"></p><p>可以用于生成一切页面，包括网页、landing page、简历、PPT 等都可以帮你实现，同时它的 SKILL 编写过程也值得参考。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260628233037.png"></p><blockquote><p><a href="https://crossoverjie.top/SkillDeck/">skilldeck</a> 的项目主页</p></blockquote><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260628233137.png"></p><blockquote><p>我的独立 <a href="https://crossoverjie.top/MeowCareIndex/">app</a> 的项目首页</p></blockquote><p>整体来看页面都很和谐，配色属于我的审美。</p><h3 id="ponytail"><a href="#ponytail" class="headerlink" title="ponytail"></a><a href="https://github.com/DietrichGebert/ponytail">ponytail</a></h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629160519787.png"></p><p>主要就是为了：<code>懒</code>，避免 AI 做过度设计、尽量简化代码。</p><p>它的核心原则：</p><ol><li>这东西真的需要存在吗？推测性的需求 → 跳过 (YAGNI)</li><li>代码库里已经有了？复用已有的 helper&#x2F;util&#x2F;type</li><li>标准库能做？用它</li><li>原生平台特性覆盖？<code>&lt;input type=&quot;date&quot;&gt;</code> 胜过 picker 库</li><li>已安装的依赖能解决？用它，绝不为几行代码能搞定的事加新依赖</li><li>能一行搞定？一行</li><li>以上都不行 → 写最小可用代码</li></ol><p>三个强度可选：</p><table><thead><tr><th>级别</th><th>行为</th></tr></thead><tbody><tr><td>lite</td><td>按要求做，但会告诉你更懒的替代方案</td></tr><tr><td>full</td><td>强制执行梯子，标准库优先，最短 diff（默认）</td></tr><tr><td>ultra</td><td>YAGNI 极端主义，能删不加，先挑战需求本身</td></tr></tbody></table><p>什么时候不能懒：</p><ul><li>信任边界的输入校验</li><li>防止数据丢失的错误处理</li><li>安全措施、无障碍基础</li><li>用户明确要求的功能</li></ul><h3 id="skilldeck"><a href="#skilldeck" class="headerlink" title="skilldeck"></a><a href="https://github.com/crossoverJie/SkillDeck/blob/main/skills/skilldeck/SKILL.md">skilldeck</a></h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629160600529.png" alt="259"><br>配合我开发的 skilldeck 可视化 SKILLS 工具使用，可以在 Agent 里管理 SKILLS，同时数据也兼容 skilldeck，这样就也可以使用 skilldeck 的 GUI 能力。</p><h3 id="web-access"><a href="#web-access" class="headerlink" title="web-access"></a><a href="https://github.com/eze-is/web-access">web-access</a></h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629160628014.png"></p><p>web-access 是一个联网操作技能，让你能通过 Claude Code 访问和操作网页内容。</p><p>核心能力：</p><table><thead><tr><th>能力</th><th>说明</th></tr></thead><tbody><tr><td>搜索引擎</td><td>WebSearch 搜索关键词，快速获取摘要和链接</td></tr><tr><td>网页抓取</td><td>WebFetch &#x2F; curl &#x2F; Jina 读取已知 URL 的内容</td></tr><tr><td>浏览器 CDP</td><td>直连你的 Chrome&#x2F;Edge 浏览器，携带登录态操作页面</td></tr><tr><td>本地历史检索</td><td>搜索你浏览器的书签和历史记录</td></tr></tbody></table><p>浏览器 CDP 模式是它的亮点，通过 CDP Proxy 直连你日常使用的浏览器，天然携带登录态，无需额外登录。可以：</p><ul><li>创建新 tab 访问页面</li><li>执行 JS 读取&#x2F;操控 DOM</li><li>点击、滚动、填写表单</li><li>截图捕获页面&#x2F;视频帧</li><li>提取图片、视频等媒体资源</li></ul><p>所有操作在后台 tab 进行，不影响你现有的浏览状态。</p><h2 id="工作相关"><a href="#工作相关" class="headerlink" title="工作相关"></a>工作相关</h2><h3 id="generate-wiki"><a href="#generate-wiki" class="headerlink" title="generate-wiki"></a><a href="https://github.com/crossoverJie/skills/blob/main/skills/generate-grpc-java-wiki/SKILL.md">generate-wiki</a></h3><p>这是我在工作中沉淀下来的 SKILL，可以一键将自己的 Java + gRPC 项目生成一个静态 wiki 页面。</p><blockquote><p>但要注意，这不是一个通用的方案，最好是 fork 过去根据自己的项目进行定制。</p></blockquote><h3 id="starrocks-upgrade"><a href="#starrocks-upgrade" class="headerlink" title="starrocks-upgrade"></a><a href="https://github.com/crossoverJie/skills/tree/main/skills/starrocks-upgrade">starrocks-upgrade</a></h3><p>我在维护公司内部的 StarRocks 的时候，版本升级时往往拿不准对现有集群的影响，于是有了这个 SKILL。它可以在你升级之前通过源码对比不同版本之间的差异，给出风险点，具体使用过程和原理可以参考<a href="https://crossoverjie.top/2026/06/14/starrocks/StarRocks-upgrade-skill-principle/">这篇文章</a>。</p><blockquote><p>同理，类似的软件版本升级都可以做一个 SKILL 工具。</p></blockquote><hr><h1 id="常用工具"><a href="#常用工具" class="headerlink" title="常用工具"></a>常用工具</h1><h2 id="TUI"><a href="#TUI" class="headerlink" title="TUI"></a>TUI</h2><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images57fe7ccd5051c0373b25ca7212749ca9.png"></p><p>使用 <code>/tui fullscreen</code> 可以开启全屏模式，这样就可以使用鼠标来移动光标了，用起来更像是一个现代的文本编辑器，再也不用键盘方向键来移动光标了。同时也支持选中复制。</p><p><code>/tui</code> 命令首次引入版本是 v2.1.110（2026年4月15日），但 fullscreen 渲染模式本身在更早的 v2.1.90 就已经存在了。</p><p>版本演进：</p><table><thead><tr><th>版本</th><th>日期</th><th>内容</th></tr></thead><tbody><tr><td>v2.1.90</td><td>2026-04-01</td><td>fullscreen scrollback 和 fullscreen mode 修复</td></tr><tr><td>v2.1.92</td><td>2026-04-04</td><td>fullscreen mode 滚动问题修复</td></tr><tr><td>v2.1.110</td><td>2026-04-15</td><td>&#x2F;tui 命令正式发布</td></tr></tbody></table><p><code>/tui fullscreen</code> 的特性：</p><ul><li>无闪烁渲染 (flicker-free)</li><li>更低内存占用</li><li>鼠标支持（点击选择菜单，Cmd+点击打开 URL）</li><li>选中即复制</li></ul><h2 id="statusLine"><a href="#statusLine" class="headerlink" title="statusLine"></a>statusLine</h2><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260628230309.png"></p><p><code>statusLine</code> 是 Claude Code 原生提供的<strong>终端底部自定义状态栏接口</strong>。它不是一个固定样式的功能，而是允许你通过 JSON 传给脚本，脚本把想显示的文本输出到 stdout，Claude Code 再将结果显示在输入框下方、内置 footer 徽章上方。</p><p>使用方式很简单，直接输入 <code>/statusline</code>，用自然语言描述你想展示什么，例如”显示模型、当前目录、上下文百分比和进度条”。</p><p>你的 <code>statusline.sh</code> 可以自行组合：模型、当前目录、Git 分支、dirty 状态、上下文百分比、累计成本等。</p><p>当然你还可以直接使用 <a href="https://github.com/jarrodwatts/claude-hud">claude-hud</a>，它是基于 <code>statusLine</code> 接口做的现成插件，替你实现并维护一套更完整的 statusLine 脚本和配置。示例：</p><figure class="highlight prolog"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs prolog">[<span class="hljs-symbol">Opus</span>] │ my-project git:(main*)<br><span class="hljs-symbol">Context</span> █████░░░░░ <span class="hljs-number">45</span><span class="hljs-comment">% │ Usage ██░░░░░░░░ 25%</span><br></code></pre></td></tr></table></figure><p>还可选展示：</p><figure class="highlight coq"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs coq">◐ Edit: auth.ts | <span class="hljs-type">✓ Read</span> ×<span class="hljs-number">3</span> | <span class="hljs-type">✓ Grep</span> ×<span class="hljs-number">2</span><br>◐ explore [haiku]: Finding auth code<br>▸ Fix authentication bug (<span class="hljs-number">2</span>/<span class="hljs-number">5</span>)<br></code></pre></td></tr></table></figure><p>除了模型、目录、Git、上下文和额度，它还能从本地 transcript JSONL 中解析出工具活动、运行中的 subagent 与 Todo 进度，有些东西的。</p><hr><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>以上就是我目前常用的 SKILLS 和工具，SKILLS 这块主要分为个人和工作两类。个人相关的主要是提升日常开发体验，比如通知、页面生成、代码简化；工作相关的则更偏向特定场景，比如 wiki 生成、版本升级风险评估。</p><p>工具方面，TUI 的全屏模式让 Claude Code 的交互体验提升了一个档次，statusLine 则让终端底部的信息展示更加灵活。这些看似小的改进，用久了真的回不去。</p><p>如果你也有好用的 SKILLS 或工具推荐，欢迎交流。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629162029387.png&quot;&gt;&lt;/p&gt;
&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;已经高强度的使用 claude code 大半年的时间了，积累了不少好用的 SKILLS 和工具，今天来整理一下分享给大家。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260629113441276.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我安装了大约 70 几个 SKILLS，但常用的其实也不太多，下面介绍一些我个人觉得效果不错的。推荐使用我开发的 &lt;a href=&quot;https://github.com/crossoverJie/SkillDeck&quot;&gt;skilldeck&lt;/a&gt; 来管理本地的 SKILLS：&lt;/p&gt;
&lt;figure class=&quot;highlight shell&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;hljs shell&quot;&gt;brew tap crossoverJie/skilldeck &amp;amp;&amp;amp; brew install --cask skilldeck&lt;br&gt;xattr -cr /Applications/SkillDeck.app&lt;br&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Claude" scheme="http://crossoverjie.top/tags/Claude/"/>
    
    <category term="Skills" scheme="http://crossoverjie.top/tags/Skills/"/>
    
  </entry>
  
  <entry>
    <title>AI Coding Agent 时代，我自己最常用的 4 个终端工具</title>
    <link href="http://crossoverjie.top/2026/06/22/AI/terminal-tools-for-ai-coding-agent/"/>
    <id>http://crossoverjie.top/2026/06/22/AI/terminal-tools-for-ai-coding-agent/</id>
    <published>2026-06-22T00:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.956Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>以前学 linux 命令行，常见路线是记住一堆 <code>grep</code>、<code>find</code>、<code>sed</code>、<code>awk</code>，然后自己在代码库里定位问题、筛选文件、拼接命令。</p><p>但进入 Coding Agent 时代后，我觉得人和终端的分工变了。</p><p>代码搜索、读取配置、分析调用链、运行测试，这些工作本来就是 Agent 擅长的。面对一个仓库，Claude Code、Codex 之类的 Agent 会自己判断该用 <code>rg</code>、<code>git diff</code>、<code>jq</code> 还是别的工具；我不需要为了”指挥 Agent”而把这些命令全学一遍。</p><p>我更常用的，是另一类命令：</p><ul><li>快速进入正确项目；</li><li>把准确的文件路径交给 Agent；</li><li>从大量文件里选出我想让它看的那个；</li><li>跑长任务时，避免 Mac 睡眠导致 Agent 中断。</li></ul><p>下面是我目前最常用的一套。</p><span id="more"></span><hr><h2 id="1-realpath：跨项目引用文件时，给-Agent-一个准确的地址"><a href="#1-realpath：跨项目引用文件时，给-Agent-一个准确的地址" class="headerlink" title="1. realpath：跨项目引用文件时，给 Agent 一个准确的地址"></a>1. <code>realpath</code>：跨项目引用文件时，给 Agent 一个准确的地址</h2><p>用 Agent 的时候，大部分情况下你不需要操心路径——Agent 自己会 <code>cd</code> 进项目目录，该读什么文件它自己会找。</p><p>但有一个场景例外：<strong>你在 A 项目里工作，需要参考 B 项目的文件</strong>。</p><p>比如你正在 A 项目里做重构，想让 Agent 参考 B 项目里的一个实现。这时候你没法直接在 A 项目里 <code>@</code> B 的文件，因为它们不在同一个仓库目录下。</p><p>我最常见的做法是：切到 B 项目的终端，<code>realpath</code> 一下目标文件，把绝对路径复制过来，告诉 Agent 去读。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">realpath</span> ~/Code/project-b/src/service/payment.go<br></code></pre></td></tr></table></figure><p>输出：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">/Users/aurora/Code/project-b/src/service/payment.go<br></code></pre></td></tr></table></figure><p>然后回到 A 项目的 Agent 对话里：</p><blockquote><p>请阅读 <code>/Users/aurora/Code/project-b/src/service/payment.go</code>，参考它的重试逻辑，帮我在 A 项目里实现类似的功能。</p></blockquote><p>这样 Agent 就能跨项目读取文件，不受当前工作目录的限制。</p><h3 id="我会把它封装成-rp"><a href="#我会把它封装成-rp" class="headerlink" title="我会把它封装成 rp"></a>我会把它封装成 <code>rp</code></h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260623181109.png"></p><p>跨项目引用是高频操作，所以我做了一个小函数：输出路径、自动复制到剪贴板、再告诉我复制成功。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-function"><span class="hljs-title">rp</span></span>() &#123;<br>  <span class="hljs-built_in">local</span> p<br><br>  p=$(<span class="hljs-built_in">realpath</span> <span class="hljs-string">&quot;<span class="hljs-variable">$@</span>&quot;</span>) || <span class="hljs-built_in">return</span><br>  <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;%s&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$p</span>&quot;</span> | pbcopy<br>  <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;Copied: %s\n&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$p</span>&quot;</span><br>&#125;<br></code></pre></td></tr></table></figure><p>放进 <code>~/.zshrc</code> 后，重新加载：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">source</span> ~/.zshrc<br></code></pre></td></tr></table></figure><p>之后切到 B 项目终端，只需要：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">rp src/service/payment.go<br></code></pre></td></tr></table></figure><p>终端会显示：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Copied: /Users/aurora/Code/project-b/src/service/payment.go<br></code></pre></td></tr></table></figure><p>然后切回 A 项目的 Agent 对话，直接粘贴路径，告诉它要参考什么。</p><p>这比手动拼路径或者在两个终端之间来回切换要顺畅很多。</p><hr><h2 id="2-zoxide：不用记路径，只要记得项目大概叫什么"><a href="#2-zoxide：不用记路径，只要记得项目大概叫什么" class="headerlink" title="2. zoxide：不用记路径，只要记得项目大概叫什么"></a>2. <a href="https://github.com/ajeetdsouza/zoxide"><code>zoxide</code></a>：不用记路径，只要记得项目大概叫什么</h2><p>项目多起来以后，最烦的不是打开终端，而是进入正确目录。</p><p>传统方式可能是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> ~/Code/company/backend/payment-service<br></code></pre></td></tr></table></figure><p>路径长、层级深，而且每个项目的目录结构不一样。更常见的情况是，你只记得项目大概叫 <code>payment</code>，但不记得它放在 <code>~/Code</code>、<code>~/Workspace</code> 还是某个 worktree 目录下。</p><p><code>zoxide</code> 的思路很简单：它会根据你的访问记录，为目录建立使用频率和最近访问的排序。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260623181921.png"></p><p>安装后，常见用法是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">z pulsar<br></code></pre></td></tr></table></figure><p>它会跳到最符合 <code>pulsar</code> 的常用目录。</p><p>如果候选目录不止一个，可以使用交互模式：<br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260623181719.png"></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">zi<br></code></pre></td></tr></table></figure><p>或者：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">zi starr<br></code></pre></td></tr></table></figure><p>然后通过模糊搜索选择目标目录。</p><p>对于日常 Agent 工作流，<code>zoxide</code> 的意义并不只是”替代 <code>cd</code>“，而是更快回答一个问题：</p><blockquote><p>我应该在哪个项目、哪个 worktree 里启动这个 Agent？</p></blockquote><p>比如你平时有：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs text">~/Code/my-app<br>~/Code/my-app-fix-login<br>~/Code/my-app-refactor<br>~/Code/my-app-release<br></code></pre></td></tr></table></figure><p>你不需要回忆完整路径，只需要：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">zi my-app<br></code></pre></td></tr></table></figure><p>选中正确工作区后，再启动 Agent。</p><p>对于经常并行处理多个项目、多个分支、多个 worktree 的人来说，这个体验很容易形成习惯。</p><hr><h2 id="3-fzf-fp：从文件列表中选中目标，并把绝对路径直接交给-Agent"><a href="#3-fzf-fp：从文件列表中选中目标，并把绝对路径直接交给-Agent" class="headerlink" title="3. fzf + fp：从文件列表中选中目标，并把绝对路径直接交给 Agent"></a>3. <a href="https://github.com/junegunn/fzf"><code>fzf</code></a> + <code>fp</code>：从文件列表中选中目标，并把绝对路径直接交给 Agent</h2><p><code>fzf</code> 是一个终端里的模糊选择器。</p><p>它可以用于命令历史、目录、Git 分支、进程列表等很多场景，但我自己最常用的用途只有一个：</p><blockquote><p>当我知道”我想让 Agent 看一个文件”，但不想手动输入完整文件名时，用它选中并复制路径。</p></blockquote><p>我给它配了一个 <code>fp</code> 函数，意思是 file path：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-function"><span class="hljs-title">fp</span></span>() &#123;<br>  <span class="hljs-built_in">local</span> file<br>  <span class="hljs-built_in">local</span> path<br><br>  file=$(fzf) || <span class="hljs-built_in">return</span><br>  path=$(<span class="hljs-built_in">realpath</span> <span class="hljs-string">&quot;<span class="hljs-variable">$file</span>&quot;</span>) || <span class="hljs-built_in">return</span><br><br>  <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;%s&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$path</span>&quot;</span> | pbcopy<br>  <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;Copied: %s\n&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$path</span>&quot;</span><br>&#125;<br></code></pre></td></tr></table></figure><p>使用时只需要在项目目录里输入：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">fp<br></code></pre></td></tr></table></figure><p>然后输入几个关键词，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">feature<br></code></pre></td></tr></table></figure><p><code>fzf</code> 会实时筛选文件。选中后按回车，完整绝对路径就已经进了剪贴板。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260623182152.png"><br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260623182204.png"></p><p>接下来可以直接对 Agent 说：</p><blockquote><p>请阅读我刚刚复制的这个文件，先解释它的作用，再帮我确认是否存在兼容性风险。</p></blockquote><p>这个流程特别适合下面几类场景：</p><ul><li>你在 Finder、IDE 或终端中看到一个文件，但路径很深；</li><li>项目里有很多同名或近似命名的配置文件；</li><li>你知道文件名的一部分，但不想手动补完整；</li><li>你想精确限制 Agent 的阅读范围；</li><li>你准备让 Agent 修改一个文件，希望先明确告诉它目标路径。</li></ul><h3 id="给-fzf-开启-Shell-集成"><a href="#给-fzf-开启-Shell-集成" class="headerlink" title="给 fzf 开启 Shell 集成"></a>给 <code>fzf</code> 开启 Shell 集成</h3><p>如果你使用 zsh，可以把下面这行放进 <code>~/.zshrc</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">source</span> &lt;(fzf --zsh)<br></code></pre></td></tr></table></figure><p>重新加载配置：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">source</span> ~/.zshrc<br></code></pre></td></tr></table></figure><p>除了 <code>fp</code>，这还会带来几个很实用的快捷键：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">Ctrl-R：模糊搜索历史命令<br>Ctrl-T：选择文件或目录，插入当前命令行<br>Alt-C：选择目录并切换过去<br></code></pre></td></tr></table></figure><p>但对我来说，<code>fp</code> 才是最贴近 Agent 协作的一个封装：</p><blockquote><p>选中一个文件 → 转成绝对路径 → 自动复制 → 交给 Agent。</p></blockquote><hr><h2 id="4-Otty-的保活机制：让长时间-Agent-任务不被-Mac-睡眠打断"><a href="#4-Otty-的保活机制：让长时间-Agent-任务不被-Mac-睡眠打断" class="headerlink" title="4. Otty 的保活机制：让长时间 Agent 任务不被 Mac 睡眠打断"></a>4. <a href="https://docs.otty.sh/agents/parallel-tasks#keep-macos-awake">Otty</a> 的保活机制：让长时间 Agent 任务不被 Mac 睡眠打断</h2><p>AI Agent 任务经常比普通命令跑得久。</p><p>例如：</p><ul><li>让 Agent 分析一个大型仓库；</li><li>跑完整测试集；</li><li>做跨模块重构；</li><li>生成升级兼容性报告；</li><li>等待多个子任务完成；</li><li>长时间运行 Claude Code、Codex 或其他本地 Agent。</li></ul><p>这时一个很现实的问题是：Mac 可能进入显示器休眠或系统休眠，导致终端任务暂停，Agent 的执行也被打断。</p><p>如果你使用 Otty，可以开启它的：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">Prevent Sleep While Processing<br></code></pre></td></tr></table></figure><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260623182313.png"></p><p>打开后，Otty 会在 Agent 正在处理任务时保持 Mac 唤醒；当 Agent 进入空闲状态后，又会自动释放这个保活状态。</p><p>这比手动执行一个长期保活命令更符合 Agent 工作流，因为它只在真正需要时保持机器唤醒。</p><p>我会把它理解为 Agent 任务的”运行保险”：</p><blockquote><p>不是让 Mac 永远不睡，而是确保一个正在执行的重要任务不会因为系统休眠而半途停止。</p></blockquote><p>尤其是晚上跑长任务、挂着多个 Agent、暂时离开电脑时，这个设置很值得打开。</p><hr><h2 id="一套很简单的-Agent-协作流程"><a href="#一套很简单的-Agent-协作流程" class="headerlink" title="一套很简单的 Agent 协作流程"></a>一套很简单的 Agent 协作流程</h2><p>这几个工具并不是替代 Agent，而是让你更好地把任务交给 Agent。</p><p>一个典型流程大概是这样：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 1. 快速进入项目或 worktree</span><br>zi my-project<br><br><span class="hljs-comment"># 2. 找到你想让 Agent 重点看的文件</span><br>fp<br><br><span class="hljs-comment"># 3. 粘贴路径，告诉 Agent 要做什么</span><br><span class="hljs-comment"># 例如：</span><br><span class="hljs-comment"># 请检查 /Users/aurora/Code/my-project/configs/prod/app.yaml</span><br><span class="hljs-comment"># 重点分析生产环境风险，并给出最小修改方案。</span><br><br><span class="hljs-comment"># 4. 如果任务会很久，打开 Otty 的 Prevent Sleep While Processing</span><br></code></pre></td></tr></table></figure><p>跨项目引用时，流程稍有不同：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 1. 切到 B 项目终端，复制目标文件路径</span><br>rp src/service/payment.go<br><br><span class="hljs-comment"># 2. 回到 A 项目的 Agent 对话，粘贴路径</span><br><span class="hljs-comment"># 例如：</span><br><span class="hljs-comment"># 请阅读 /Users/aurora/Code/project-b/src/service/payment.go</span><br><span class="hljs-comment"># 参考它的重试逻辑，帮我在 A 项目里实现类似的功能。</span><br></code></pre></td></tr></table></figure><hr><h2 id="我不再刻意学习的命令"><a href="#我不再刻意学习的命令" class="headerlink" title="我不再刻意学习的命令"></a>我不再刻意学习的命令</h2><p>像 <code>rg</code>、<code>fd</code>、<code>jq</code>、<code>ast-grep</code> 当然都是很好的命令行工具。</p><p>但在我的工作流里，它们更偏向 Agent 的执行工具，而不是我必须熟练掌握的工具。</p><p>我关心的是：</p><ul><li>Agent 在哪个目录运行；</li><li>它应该看哪个文件；</li><li>我如何快速把目标路径交给它；</li><li>多个项目之间怎样切换；</li><li>长任务能不能稳定跑完；</li><li>完成后我如何回到结果验证。</li></ul><p>换句话说：</p><blockquote><p>Agent 负责在仓库内部探索和执行；我负责把正确的项目、正确的目标和正确的约束交给它。</p></blockquote><p>这也是我理解的 AI Coding Agent 时代终端分工。</p><p>终端不再只是”人手工敲命令的地方”，它更像是一个控制台：</p><ul><li>用 <code>zoxide</code> 找到正确工作区；</li><li>用 <code>fzf</code> 从大量文件中选定目标；</li><li>用 <code>realpath + pbcopy</code> 把准确地址交给 Agent；</li><li>用 Otty 保证长任务不中断。</li></ul><p>不需要掌握几十个复杂命令。</p><p>先把这几个高频动作做得足够顺手，就已经能明显改善和 Coding Agent 协作时的体验。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>AI Coding Agent 时代，人和终端的分工发生了变化。Agent 负责在仓库内部探索和执行，而我负责把正确的项目、正确的目标和正确的约束交给它。</p><p>这套工具链的核心思路就四个字：<strong>精准投喂</strong>。</p><table><thead><tr><th>工具</th><th>解决的问题</th><th>核心动作</th></tr></thead><tbody><tr><td><code>realpath + rp</code></td><td>跨项目引用文件，Agent 没法直接 @</td><td>跨项目路径 → 绝对路径 → 剪贴板</td></tr><tr><td><code>zoxide</code></td><td>项目多了记不住路径</td><td>模糊关键词 → 跳转到正确目录</td></tr><tr><td><code>fzf + fp</code></td><td>文件太多不想手动输入</td><td>模糊搜索 → 选中文件 → 复制路径</td></tr><tr><td>Otty 保活</td><td>长任务被 Mac 睡眠打断</td><td>自动检测任务状态 → 按需防休眠</td></tr></tbody></table><p>不需要掌握几十个复杂命令，先把这几个高频动作做得足够顺手，就已经能明显改善和 Coding Agent 协作时的体验。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;以前学 linux 命令行，常见路线是记住一堆 &lt;code&gt;grep&lt;/code&gt;、&lt;code&gt;find&lt;/code&gt;、&lt;code&gt;sed&lt;/code&gt;、&lt;code&gt;awk&lt;/code&gt;，然后自己在代码库里定位问题、筛选文件、拼接命令。&lt;/p&gt;
&lt;p&gt;但进入 Coding Agent 时代后，我觉得人和终端的分工变了。&lt;/p&gt;
&lt;p&gt;代码搜索、读取配置、分析调用链、运行测试，这些工作本来就是 Agent 擅长的。面对一个仓库，Claude Code、Codex 之类的 Agent 会自己判断该用 &lt;code&gt;rg&lt;/code&gt;、&lt;code&gt;git diff&lt;/code&gt;、&lt;code&gt;jq&lt;/code&gt; 还是别的工具；我不需要为了”指挥 Agent”而把这些命令全学一遍。&lt;/p&gt;
&lt;p&gt;我更常用的，是另一类命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;快速进入正确项目；&lt;/li&gt;
&lt;li&gt;把准确的文件路径交给 Agent；&lt;/li&gt;
&lt;li&gt;从大量文件里选出我想让它看的那个；&lt;/li&gt;
&lt;li&gt;跑长任务时，避免 Mac 睡眠导致 Agent 中断。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是我目前最常用的一套。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Terminal" scheme="http://crossoverjie.top/tags/Terminal/"/>
    
  </entry>
  
  <entry>
    <title>从 Warp 换到 cmux：一个更适合 AI Agent 的终端</title>
    <link href="http://crossoverjie.top/2026/06/17/AI/cmux-introduce/"/>
    <id>http://crossoverjie.top/2026/06/17/AI/cmux-introduce/</id>
    <published>2026-06-17T17:56:51.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近将终端从 Warp 切换到了 cmux，用了一段时间后，现在已经基本上满足我的所有需求，所以才有这篇安利的文章。</p><p>开始之前先回顾下自己的终端使用历史。刚开始工作那时候使用的是 Windows，用得最多的终端就是 xshell，后面切换到 macOS 之后自然就切换到了 mac 上用的最多的 iTerm2。</p><span id="more"></span><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260617113149.png"></p><p>iTerm2 一直是我的主力终端，用了很多年，直到前些年 Warp 的出现。Warp 提供了 block 块、现代的文本编辑器（支持鼠标移动光标），用上之后就离不开它了。</p><p>但是随着这些年的迭代，Warp 功能越做越臃肿，加入了一些我完全不需要的 AI 功能。</p><p>加上近期频繁使用 Claude Code、Codex、OpenCode 这些 AI Agent，对终端的依赖性变得更高了。</p><p>原本我一开始是想自己做一个的——我其实就是想要一个简化版的 Warp，需要包含以下功能：</p><ul><li>Block 功能，特别是查看大量日志的时候非常有用</li><li>现代的文本编辑器，而不是每次都用方向键来移动光标</li><li>AI Agent 的管理功能<ul><li>Agent 完成时的通知、当前状态的查看</li></ul></li><li>终端状态栏：显示当前路径、git status、git diff 等信息</li></ul><p>我大概做了一周多的时间，达到了一个基本可用的版本，但很多细节都没做好。</p><p>受限于当时选择的技术栈 Tauri + Rust，一些体验上确实比不上 Swift 的原生开发效果。</p><p>于是就继续用 Warp，直到后面在社媒上看到了 cmux。</p><h1 id="cmux"><a href="#cmux" class="headerlink" title="cmux"></a>cmux</h1><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260617094853.png"></p><p>这是我目前使用 cmux 的截图。现在使用终端其实 90% 的时间都在和 Agent 打交道。</p><p>我会同时开启 N 个 Agent 来干活，其中又会将 Agent 按照业务进行分组，这时就得提到 cmux 的工作区和分屏功能了。</p><p>cmux 把结构分成 <strong>Window → Workspace → Pane → Surface → Panel</strong>。也就是说，一个工作区里可以有多个分屏，每个 Pane 里还能有多个 Surface，非常适合把主 Claude Code、测试命令、日志、浏览器、子 Agent 放在同一个 context 里。</p><p>而且 cmux 还集成了 Agent 通知——普通终端通知往往只告诉你「有进程需要输入」，但不知道是哪个 Agent、哪个项目、哪个分屏。cmux 的 Pane 会出现蓝色通知环，侧边栏 Tab 会亮起，还支持通知面板和 macOS 桌面通知。</p><blockquote><p>通知的问题之前我写过一个 <a href="https://github.com/crossoverJie/skills/blob/main/skills/agent-notifier/SKILL.md">SKILLS</a> 来解决，现在终端能原生通知就更好用了。</p></blockquote><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260617150722.png"></p><p>如果你是 macOS 用户，还在使用 Warp 甚至是 iTerm2、自带终端的 Coding Agent 重度用户，非常推荐你来试试 <a href="https://github.com/manaflow-ai/cmux">cmux</a>，一定会有新的发现。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近将终端从 Warp 切换到了 cmux，用了一段时间后，现在已经基本上满足我的所有需求，所以才有这篇安利的文章。&lt;/p&gt;
&lt;p&gt;开始之前先回顾下自己的终端使用历史。刚开始工作那时候使用的是 Windows，用得最多的终端就是 xshell，后面切换到 macOS 之后自然就切换到了 mac 上用的最多的 iTerm2。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Claude" scheme="http://crossoverjie.top/tags/Claude/"/>
    
    <category term="Terminal" scheme="http://crossoverjie.top/tags/Terminal/"/>
    
    <category term="cmux" scheme="http://crossoverjie.top/tags/cmux/"/>
    
  </entry>
  
  <entry>
    <title>I Built an AI-Powered StarRocks Upgrade Risk Scanner — And It Caught a Real Risk</title>
    <link href="http://crossoverjie.top/2026/06/14/starrocks/StarRocks-upgrade-skill-principle-en/"/>
    <id>http://crossoverjie.top/2026/06/14/starrocks/StarRocks-upgrade-skill-principle-en/</id>
    <published>2026-06-14T17:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.978Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Background"><a href="#Background" class="headerlink" title="Background"></a>Background</h1><p>I’ve been working on a cross-version upgrade of StarRocks (3.3 → 3.5) and hit quite a few pitfalls along the way. I previously wrote a post on <a href="https://crossoverjie.top/2025/03/14/starrocks/StarRocks-upgrade/">StarRocks Upgrade Considerations</a> documenting the manual upgrade process, but that was only for minor version upgrades (3.3.3 → 3.3.9).</p><p>Cross-major-version upgrades are an entirely different beast — between 3.3 and 3.5 there are 6000+ commits, hiding all kinds of incompatible changes: default config values changed, session variables modified, protocol fields removed… Manually reviewing each one is simply not feasible. Missing a single critical change could lead to a production incident.</p><p>So I thought: can AI help me do this job? After some iteration, I hand-crafted a StarRocks upgrade risk scanner using Claude Code (<a href="https://github.com/crossoverJie/skills/blob/main/skills/starrocks-upgrade/SKILL.md">starrocks-upgrade skill</a>). This article discusses its design principles.</p><p>Before upgrading now, I run the Skill first. It prompts you to input cluster information for downstream analysis:<br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181026011.png"></p><p>After collecting that, it gathers commit diffs between the two versions, analyzes them, and generates an upgrade report highlighting potential risks, like this one:<br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181113157.png"></p><p>We actually encountered this exact issue after the upgrade — having the report in advance made resolving it much easier.</p><span id="more"></span><h1 id="Problem-Domain-Why-Upgrades-Are-So-Hard"><a href="#Problem-Domain-Why-Upgrades-Are-So-Hard" class="headerlink" title="Problem Domain: Why Upgrades Are So Hard"></a>Problem Domain: Why Upgrades Are So Hard</h1><p>Let’s first clarify the core problem. The difficulty of cross-version StarRocks upgrades isn’t the “upgrade operation” itself — it’s <strong>not knowing what will happen before upgrading</strong>.</p><h2 id="Incompatible-Changes-Are-Hard-to-Spot"><a href="#Incompatible-Changes-Are-Hard-to-Spot" class="headerlink" title="Incompatible Changes Are Hard to Spot"></a>Incompatible Changes Are Hard to Spot</h2><p>Default config value changes, session variable modifications, BE config tweaks between versions are often buried in thousands of commits. The traditional approach is to manually read Release Notes, but many behavioral changes aren’t documented in RNs at all.</p><h2 id="Impact-Scope-Is-Hard-to-Assess"><a href="#Impact-Scope-Is-Hard-to-Assess" class="headerlink" title="Impact Scope Is Hard to Assess"></a>Impact Scope Is Hard to Assess</h2><p>A single config default change can have cascading effects through indirect call chains. For example, <code>transform_type_prefer_string_for_varchar</code> changing from false to true looks like just a default value tweak, but it indirectly causes materialized view invalidation through MV re-activation. This kind of indirect impact chain is virtually impossible to catch by eye.</p><h2 id="Cluster-Specific-Risks-Can’t-Be-Quantified"><a href="#Cluster-Specific-Risks-Can’t-Be-Quantified" class="headerlink" title="Cluster-Specific Risks Can’t Be Quantified"></a>Cluster-Specific Risks Can’t Be Quantified</h2><p>Different clusters have different configurations (fe.conf&#x2F;be.conf), deployment methods (K8s&#x2F;VM), and scales (MV count, table count). Generic upgrade advice can’t cover risks specific to your cluster. The same default value change poses very different risks: if you’ve already overridden it in your conf, the risk is low; but if you happen to use the old default, the upgrade changes behavior directly.</p><h2 id="Shortcomings-of-Existing-Approaches"><a href="#Shortcomings-of-Existing-Approaches" class="headerlink" title="Shortcomings of Existing Approaches"></a>Shortcomings of Existing Approaches</h2><table><thead><tr><th>Approach</th><th>Shortcoming</th></tr></thead><tbody><tr><td>Manually reading Release Notes</td><td>Incomplete — many behavioral changes aren’t recorded in RNs</td></tr><tr><td><code>git log --oneline A..B</code></td><td>Only shows commit list, can’t judge compatibility risk</td></tr><tr><td>CI&#x2F;CD automated tests</td><td>Only verifies functional correctness, can’t catch config conflicts or operational impacts</td></tr><tr><td>Reading PRs one by one</td><td>Analysis is one-sided — looking at PR diffs alone can’t reveal call chains and upstream&#x2F;downstream impacts</td></tr></tbody></table><p>The PR-by-PR analysis is especially treacherous. A PR diff only shows the changed code snippet — you can’t see the class context or upstream&#x2F;downstream call relationships. For the <code>transform_type_prefer_string_for_varchar</code> example, the PR diff merely modifies a default value in <code>Config.java</code>, but you can’t see that <code>AnalyzerUtils.transformTableColumnType()</code> reads this config, <code>MaterializedViewAnalyzer</code> calls it, and <code>AlterJobMgr.reActivateMV()</code> indirectly triggers MV re-parsing. This complete indirect impact chain is absolutely invisible from a PR diff alone.</p><h1 id="Core-Design-Choice-Full-Source-Code-Scanning"><a href="#Core-Design-Choice-Full-Source-Code-Scanning" class="headerlink" title="Core Design Choice: Full Source Code Scanning"></a>Core Design Choice: Full Source Code Scanning</h1><p>Based on the analysis above, the tool makes a fundamental design choice: <strong>it must run in the StarRocks source code root directory</strong>, rather than reading GitHub diffs PR by PR.</p><p>The reason is straightforward:</p><table><thead><tr><th>Capability</th><th>PR-by-PR Analysis</th><th>Full Source Scanning</th></tr></thead><tbody><tr><td>Identifying removed config items</td><td>No (deleted lines don’t appear in PR diffs)</td><td>Yes — parses Config.java from both versions and compares field sets</td></tr><tr><td>Tracing indirect call chains</td><td>No — lacks source context</td><td>Yes — recursive grep in the source tree</td></tr><tr><td>Cluster config conflict detection</td><td>No — can’t read user conf files</td><td>Yes — parses cluster-profile.yaml and cross-references with Scanner results</td></tr><tr><td>Identifying “default changed but user hasn’t overridden”</td><td>No</td><td>Yes — compares conf values against old&#x2F;new defaults</td></tr></tbody></table><p>In short: <strong>no source context, no deep analysis.</strong></p><h1 id="Design-Philosophy-Prefer-False-Positives-Over-False-Negatives"><a href="#Design-Philosophy-Prefer-False-Positives-Over-False-Negatives" class="headerlink" title="Design Philosophy: Prefer False Positives Over False Negatives"></a>Design Philosophy: Prefer False Positives Over False Negatives</h1><p>The tool’s core design philosophy is <strong>prefer false positives over false negatives</strong>.</p><p>The reason is simple: the cost of upgrade risks is asymmetric. Missing an incompatible change could cause a production incident, while a false positive only adds manual verification work. So the tool employs a multi-layered scanning strategy: 11 specialized Scanners covering known risk patterns + per-commit Tier classification to ensure nothing is missed.</p><h1 id="Overall-Architecture"><a href="#Overall-Architecture" class="headerlink" title="Overall Architecture"></a>Overall Architecture</h1><p>The tool’s workflow is divided into four phases. Here’s the big picture:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606171733496.png"></p><h2 id="Phase-1-Data-Collection"><a href="#Phase-1-Data-Collection" class="headerlink" title="Phase 1: Data Collection"></a>Phase 1: Data Collection</h2><p>This is the foundation of the entire tool, implemented by <code>starrocks_upgrade.py</code>. It does quite a lot:</p><h3 id="Git-Commit-Diff-Collection"><a href="#Git-Commit-Diff-Collection" class="headerlink" title="Git Commit Diff Collection"></a>Git Commit Diff Collection</h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181044451.png"></p><p>It uses <code>git log branchA..branchB</code> to get commits unique to the target branch, then classifies each commit. There’s a key optimization here — using custom delimiters (SOH&#x2F;STX) to fetch all commit details in a single <code>git log</code> call, avoiding N+1 queries.</p><h3 id="Commit-Tier-Classification"><a href="#Commit-Tier-Classification" class="headerlink" title="Commit Tier Classification"></a>Commit Tier Classification</h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181049053.png"></p><p>Not every commit needs deep analysis. The tool classifies commits into four tiers:</p><table><thead><tr><th>Tier</th><th>Match Criteria</th><th>Handling</th></tr></thead><tbody><tr><td>SKIP</td><td>test&#x2F;docs&#x2F;build directories; commit prefix is build&#x2F;chore&#x2F;ci&#x2F;style</td><td>Count only</td></tr><tr><td>HIGH</td><td>Core paths: FE optimizer&#x2F;executor&#x2F;SQL parsing, BE runtime&#x2F;storage, Protocol&#x2F;IDL</td><td>Save full diff + deep analysis</td></tr><tr><td>MEDIUM</td><td>Business paths: connectors&#x2F;auth&#x2F;permissions; feat&#x2F;fix type source changes</td><td>Save full diff + analysis</td></tr><tr><td>LOW</td><td>All other changes</td><td>Save metadata only</td></tr></tbody></table><p>This way, HIGH&#x2F;MEDIUM commits get deep analysis, while LOW&#x2F;SKIP commits don’t waste resources.</p><h3 id="11-Specialized-Scanners"><a href="#11-Specialized-Scanners" class="headerlink" title="11 Specialized Scanners"></a>11 Specialized Scanners</h3><p>This is the most critical part of the tool, covering 11 dimensions of upgrade risk:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181054768.png"></p><p><strong>FE side:</strong></p><ul><li>Config Scanner — scans <code>@ConfField</code> config changes in <code>Config.java</code></li><li>Session Variable Scanner — scans <code>@VarAttr</code> variable changes in <code>SessionVariable.java</code></li><li>System Variable Scanner — scans <code>GlobalVariable.java</code></li><li>Auth Scanner — scans <code>AuthenticationManager.java</code>, <code>PrivilegeManager.java</code></li></ul><p><strong>BE side:</strong></p><ul><li>BE Config Scanner — scans <code>CONF_*</code> macro definitions in <code>config.h</code></li><li>Storage Format Scanner — scans <code>segment_format.h</code>, <code>tablet_meta.h</code></li></ul><p><strong>IDL&#x2F;Protocol:</strong></p><ul><li>Protocol Scanner — scans <code>.thrift</code> &#x2F; <code>.proto</code> file changes</li><li>Parser Scanner — scans <code>StarRocksParser.g4</code>, <code>AstBuilder.java</code></li></ul><p><strong>Data&#x2F;Types:</strong></p><ul><li>Charset&#x2F;Collation Scanner — scans <code>Collation*.java</code></li><li>Type System Scanner — scans <code>ScalarType.java</code> &#x2F; <code>Column.java</code></li><li>MV Scanner — scans <code>MaterializedView.java</code>, <code>MVRefreshParams.java</code></li></ul><p>Every Scanner follows the same workflow:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181059804.png"></p><h3 id="Config-Scanner’s-State-Machine-Parsing"><a href="#Config-Scanner’s-State-Machine-Parsing" class="headerlink" title="Config Scanner’s State Machine Parsing"></a>Config Scanner’s State Machine Parsing</h3><p>This deserves a closer look, since Config.java parsing is the most complex part of the tool.</p><p>Java annotations can span multiple lines:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ConfField(mutable = true, comment = &quot;Whether to prefer string type &quot;</span><br><span class="hljs-meta">        + &quot;for fixed length varchar column in materialized view creation/ctas&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-variable">transform_type_prefer_string_for_varchar</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;<br></code></pre></td></tr></table></figure><p>So the parser uses a <strong>line-by-line state machine</strong> approach:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181206843.png"></p><p>The state machine tracks <code>(</code> and <code>)</code> pairing, concatenates multi-line annotations, then parses the <code>mutable</code> and <code>comment</code> attributes. Compared to simple regex matching, this approach correctly handles various edge cases.</p><h3 id="BE-Config-Parsing"><a href="#BE-Config-Parsing" class="headerlink" title="BE Config Parsing"></a>BE Config Parsing</h3><p>The BE side uses C++ macro definitions for configuration, requiring a completely different parsing approach:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">CONF_Bool</span>(datacache_auto_adjust_enable, <span class="hljs-string">&quot;false&quot;</span>)     <span class="hljs-comment">// Not runtime-modifiable</span><br><span class="hljs-built_in">CONF_mBool</span>(lake_enable_alter_struct, <span class="hljs-string">&quot;true&quot;</span>)          <span class="hljs-comment">// Runtime-modifiable (m prefix)</span><br></code></pre></td></tr></table></figure><p>The regex <code>CONF_(m?\w+)\((\w+),\s*&quot;([^&quot;]*)&quot;\)</code> extracts everything in one pass. Note that the <code>m</code> prefix indicates mutable — runtime-modifiable.</p><h3 id="Cluster-Config-Conflict-Detection"><a href="#Cluster-Config-Conflict-Detection" class="headerlink" title="Cluster Config Conflict Detection"></a>Cluster Config Conflict Detection</h3><p>This is the feature I find most useful. The risk of the same default value change varies dramatically across scenarios:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181222357.png"></p><table><thead><tr><th>Scenario</th><th>Example</th><th>Risk</th></tr></thead><tbody><tr><td>Config removed + you have it in conf</td><td><code>mysql_service_nio_enabled</code> deleted, you have <code>= true</code> in conf</td><td>HIGH — startup error</td></tr><tr><td>Default changed + you use old default</td><td><code>enable_load_volume_from_conf</code> true→false, you have <code>= true</code> in conf</td><td>MEDIUM — your override takes effect, but decide whether to follow</td></tr><tr><td>Default changed + you have custom value</td><td>You set <code>= custom_value</code> in conf</td><td>LOW — your override takes priority</td></tr><tr><td>Default changed + you haven’t overridden</td><td><code>mysql_server_version</code> 5.1.0→8.0.33, not in your conf</td><td>HIGH — new default takes effect automatically</td></tr></tbody></table><p>This precise distinction is far more useful than vaguely saying “some config default changed.”</p><h3 id="Deployment-Aware"><a href="#Deployment-Aware" class="headerlink" title="Deployment-Aware"></a>Deployment-Aware</h3><p>The tool also generates deployment-specific risk alerts based on the cluster’s deployment method:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181226250.png"></p><p>For example, in a K8s environment, FE Pod restarts trigger MV re-activation. If there are MV-related code changes, this could cause schema incompatibilities. In VM environments, the focus is more on upgrade order (BE first, then FE).</p><h2 id="Phase-2-Commit-Diff-Analysis"><a href="#Phase-2-Commit-Diff-Analysis" class="headerlink" title="Phase 2: Commit Diff Analysis"></a>Phase 2: Commit Diff Analysis</h2><p>Phase 1 saved the full diffs of HIGH&#x2F;MEDIUM commits. Phase 2 is executed by AI Agents, using parallel subagents for deep compatibility analysis of commits.</p><p>Since cross-version diffs typically have a large number of commits (1361 HIGH tier commits for 3.3→3.5), sequential analysis is impractical. So commits are grouped by module, with 5-8 commits per group assigned to a parallel subagent:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181238262.png"></p><p>Each subagent outputs structured analysis results: <code>compatibility_impact</code>, <code>impact_type</code>, <code>severity</code>, <code>error_scenario</code>, <code>reproduction</code>, <code>rollback</code>.</p><h2 id="Phase-3-Deep-Impact-Analysis"><a href="#Phase-3-Deep-Impact-Analysis" class="headerlink" title="Phase 3: Deep Impact Analysis"></a>Phase 3: Deep Impact Analysis</h2><p>All CRITICAL&#x2F;HIGH level findings from Phase 2’s output + Phase 1’s Scanner results require further deep analysis. Each (or each batch of related) findings is assigned a parallel subagent that traces call chains via grep in the source tree.</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181249149.png"></p><p>This is one of the tool’s most distinctive designs — <strong>system lifecycle entry-point tracing</strong>. A config change may not be directly referenced by lifecycle code, but reaches it through an indirect call chain:</p><figure class="highlight erlang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs erlang"><span class="hljs-function"><span class="hljs-title">transform_type_prefer_string_for_varchar</span> <span class="hljs-params">(Config)</span></span><br><span class="hljs-function">  └─ A<span class="hljs-title">nalyzerUtils</span>.<span class="hljs-title">transformTableColumnType</span><span class="hljs-params">()</span> <span class="hljs-params">(direct caller)</span></span><br><span class="hljs-function">       └─ M<span class="hljs-title">aterializedViewAnalyzer</span> <span class="hljs-params">(indirect caller)</span></span><br><span class="hljs-function">            └─ A<span class="hljs-title">lterJobMgr</span>.<span class="hljs-title">reActivateMV</span><span class="hljs-params">()</span> <span class="hljs-params">(system lifecycle entry: triggered on FE restart)</span></span><br></code></pre></td></tr></table></figure><p>Without tracing this indirect path, you’d miss the critical risk of “MV re-activation failure after FE restart.”</p><h2 id="Phase-4-Report-Synthesis"><a href="#Phase-4-Report-Synthesis" class="headerlink" title="Phase 4: Report Synthesis"></a>Phase 4: Report Synthesis</h2><p>All analysis results from Phases 1-3 are synthesized into a structured upgrade report.</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181244064.png"></p><p>The report’s core design principles:</p><ol><li><strong>INCOMPATIBLE CHANGES at the top</strong>: The most critical information comes first, sorted by CRITICAL &gt; HIGH</li><li><strong>Error scenarios categorized by trigger timing</strong>: After FE restart &#x2F; After CN restart &#x2F; Daily queries &#x2F; During upgrade</li><li><strong>Cluster-specific conflict detection</strong>: Only conflicts relevant to the user’s cluster configuration are shown</li><li><strong>Actionable Upgrade Checklist</strong>: Every step is concrete and executable</li></ol><h3 id="Full-Data-Flow-Diagram"><a href="#Full-Data-Flow-Diagram" class="headerlink" title="Full Data Flow Diagram"></a>Full Data Flow Diagram</h3><p>Looking at Phase 1’s data flow as a whole makes it clearer:</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181252041.png"></p><h1 id="Unified-Impact-Model"><a href="#Unified-Impact-Model" class="headerlink" title="Unified Impact Model"></a>Unified Impact Model</h1><p>All Scanner findings use a unified four-dimensional impact model:</p><table><thead><tr><th>Dimension</th><th>Meaning</th><th>Trigger Condition Example</th></tr></thead><tbody><tr><td><code>data</code></td><td>Affects existing data</td><td><code>transform_type_prefer_string_for_varchar</code>, <code>max_varchar_length</code></td></tr><tr><td><code>behavior</code></td><td>Same SQL may return different results</td><td><code>sql_mode</code>, <code>mysql_server_version</code></td></tr><tr><td><code>operational</code></td><td>Requires config&#x2F;ops changes</td><td>Any HIGH_RISK config change</td></tr><tr><td><code>rolling_upgrade</code></td><td>Mixed-version cluster may break</td><td><code>protocol_field_removed</code>, <code>storage_format_changed</code></td></tr></tbody></table><p>Every finding includes a four-dimensional assessment, making it easy to filter and aggregate by dimension.</p><h1 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h1><p>The design philosophy of this tool can be distilled into these key points:</p><ol><li><p><strong>Source code is truth</strong>: All analysis is built on the complete source tree, not on PR diff snippets returned by GitHub API. No source context, no deep analysis.</p></li><li><p><strong>Layered processing</strong>: Not every commit deserves deep analysis. The tier classification strategy ensures critical commits get deep analysis while low-risk commits don’t waste resources.</p></li><li><p><strong>Specialized Scanners + AI Agent combination</strong>: Python scripts handle deterministic data collection and pattern matching (11 Scanners), while AI Agents handle uncertain deep analysis (call chain tracing, impact assessment). Each plays to its strengths.</p></li><li><p><strong>Cluster-specific</strong>: Instead of generic advice, it cross-references the user’s actual fe.conf&#x2F;be.conf to precisely identify cluster-specific risks.</p></li><li><p><strong>Prefer false positives over false negatives</strong>: The cost of upgrade risks is asymmetric — the cost of a missed finding far outweighs a false alarm.</p></li></ol><p>There are also limitations: Protocol&#x2F;Parser Scanner precision is limited, indirect call chain tracing depends on AI Agent capability, runtime behavioral changes can’t be detected, and large repo performance is an issue (6000+ commits take 30+ minutes). These are areas for future improvement.</p><p>If you also maintain StarRocks clusters and frequently need cross-version upgrades, give this tool a try. At least in my case, it helped me discover several incompatible changes that weren’t mentioned in the Release Notes.</p><p>For large-scale projects like this, complex contextual analysis is exactly where LLMs excel — making them perfectly suited for this kind of previously manual labor-intensive work.</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;Background&quot;&gt;&lt;a href=&quot;#Background&quot; class=&quot;headerlink&quot; title=&quot;Background&quot;&gt;&lt;/a&gt;Background&lt;/h1&gt;&lt;p&gt;I’ve been working on a cross-version upgrade of StarRocks (3.3 → 3.5) and hit quite a few pitfalls along the way. I previously wrote a post on &lt;a href=&quot;https://crossoverjie.top/2025/03/14/starrocks/StarRocks-upgrade/&quot;&gt;StarRocks Upgrade Considerations&lt;/a&gt; documenting the manual upgrade process, but that was only for minor version upgrades (3.3.3 → 3.3.9).&lt;/p&gt;
&lt;p&gt;Cross-major-version upgrades are an entirely different beast — between 3.3 and 3.5 there are 6000+ commits, hiding all kinds of incompatible changes: default config values changed, session variables modified, protocol fields removed… Manually reviewing each one is simply not feasible. Missing a single critical change could lead to a production incident.&lt;/p&gt;
&lt;p&gt;So I thought: can AI help me do this job? After some iteration, I hand-crafted a StarRocks upgrade risk scanner using Claude Code (&lt;a href=&quot;https://github.com/crossoverJie/skills/blob/main/skills/starrocks-upgrade/SKILL.md&quot;&gt;starrocks-upgrade skill&lt;/a&gt;). This article discusses its design principles.&lt;/p&gt;
&lt;p&gt;Before upgrading now, I run the Skill first. It prompts you to input cluster information for downstream analysis:&lt;br&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181026011.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;After collecting that, it gathers commit diffs between the two versions, analyzes them, and generates an upgrade report highlighting potential risks, like this one:&lt;br&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images202606181113157.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;We actually encountered this exact issue after the upgrade — having the report in advance made resolving it much easier.&lt;/p&gt;</summary>
    
    
    
    <category term="StarRocks" scheme="http://crossoverjie.top/categories/StarRocks/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="StarRocks" scheme="http://crossoverjie.top/tags/StarRocks/"/>
    
  </entry>
  
  <entry>
    <title>我做了一个 AI 版的 StarRocks 升级风险扫描工具，直接帮我定位到一个风险</title>
    <link href="http://crossoverjie.top/2026/06/14/starrocks/StarRocks-upgrade-skill-principle/"/>
    <id>http://crossoverjie.top/2026/06/14/starrocks/StarRocks-upgrade-skill-principle/</id>
    <published>2026-06-14T17:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.979Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近在搞 StarRocks 的跨版本升级（3.3 → 3.5），中间踩了不少坑。我之前也写过一篇 <a href="https://crossoverjie.top/2025/03/14/starrocks/StarRocks-upgrade/">StarRocks 升级注意事项</a>记录了手动升级的流程，但那只是针对小版本（3.3.3 → 3.3.9）的升级。</p><p>跨大版本升级完全是另一回事——3.3 到 3.5 中间有 6000+ 个 commit，里面藏着各种不兼容变更：配置默认值变了、Session Variable 改了、Protocol 字段删了……人工逐个审查根本不现实，漏一个关键变更就可能导致生产事故。</p><p>于是我就想：能不能用 AI 来帮我干这活？经过一段时间的迭代，我用 Claude Code 手搓了一个 StarRocks 升级风险扫描工具（<a href="https://github.com/crossoverJie/skills/blob/main/skills/starrocks-upgrade/SKILL.md">starrocks-upgrade skill</a>），这篇文章就来聊聊它的设计原理。</p><p>现在升级之前会先执行一次 Skill，它首先会让你输入一些集群信息方便后面做具体的分析：<br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260614235018.png"></p><p>收集完成之后便会收集差异版本之间的 commit 信息开始分析，最终生成一个升级报告，给出一些潜在的风险，比如这个：<br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260614235225.png"></p><p>我们在升级之后确实遇到了这个问题，提前有了这份报告之后解决起来自然也要轻松许多。</p><span id="more"></span><h1 id="问题域：为什么升级这么难"><a href="#问题域：为什么升级这么难" class="headerlink" title="问题域：为什么升级这么难"></a>问题域：为什么升级这么难</h1><p>先说清楚我们要解决的核心问题。StarRocks 跨版本升级的难点不在于”升级操作”本身，而在于<strong>升级前不知道会发生什么</strong>。</p><h2 id="不兼容变更难以发现"><a href="#不兼容变更难以发现" class="headerlink" title="不兼容变更难以发现"></a>不兼容变更难以发现</h2><p>版本间的配置项默认值变化、Session Variable 变更、BE 配置修改等，往往隐藏在几千个 commit 中。传统做法是人工阅读 Release Notes，但很多行为变更根本不会记录在 RN 里。</p><h2 id="影响范围难以评估"><a href="#影响范围难以评估" class="headerlink" title="影响范围难以评估"></a>影响范围难以评估</h2><p>一个配置项的默认值变化可能通过间接调用链产生连锁反应。比如 <code>transform_type_prefer_string_for_varchar</code> 从 false 变为 true，看起来只是改了个默认值，但它会通过 MV re-activation 间接导致物化视图失效。这种间接影响链靠肉眼根本发现不了。</p><h2 id="集群特定风险无法量化"><a href="#集群特定风险无法量化" class="headerlink" title="集群特定风险无法量化"></a>集群特定风险无法量化</h2><p>不同集群的配置（fe.conf&#x2F;be.conf）、部署方式（K8s&#x2F;VM）、规模（MV 数量、表数量）各不相同，通用的升级建议无法覆盖特定集群的风险。同样是默认值变了，你的集群如果已经在 conf 中覆盖了，风险就很低；但如果你用的恰好是旧默认值，升级后行为就直接变了。</p><h2 id="现有方案的不足"><a href="#现有方案的不足" class="headerlink" title="现有方案的不足"></a>现有方案的不足</h2><table><thead><tr><th>方案</th><th>不足</th></tr></thead><tbody><tr><td>人工阅读 Release Notes</td><td>不完整，很多行为变更不记录在 RN 中</td></tr><tr><td><code>git log --oneline A..B</code></td><td>只能看到 commit 列表，无法判断兼容性风险</td></tr><tr><td>CI&#x2F;CD 自动化测试</td><td>只能验证功能正确性，无法发现配置冲突、运维影响</td></tr><tr><td>逐个 PR 阅读分析</td><td>分析片面——只看 PR diff 无法了解调用链和上下游影响</td></tr></tbody></table><p>特别是逐 PR 分析，这是最容易掉进去的坑。一个 PR 的 diff 只是变更的代码片段，你根本看不到变更所在的类与上下游调用关系。比如上面提到的 <code>transform_type_prefer_string_for_varchar</code>，PR diff 中只是修改了 <code>Config.java</code> 中的一个默认值，但你看不到 <code>AnalyzerUtils.transformTableColumnType()</code> 在读这个配置、<code>MaterializedViewAnalyzer</code> 调用了它、<code>AlterJobMgr.reActivateMV()</code> 又间接触发了 MV 重新解析——这条完整的间接影响链，只看 PR diff 是绝对看不出来的。</p><h1 id="核心设计选择：源码全量扫描"><a href="#核心设计选择：源码全量扫描" class="headerlink" title="核心设计选择：源码全量扫描"></a>核心设计选择：源码全量扫描</h1><p>基于上面的分析，工具做了一个根本性的设计选择：<strong>必须在 StarRocks 源码根目录下运行</strong>，而不是逐个 PR 读取 GitHub 上的 diff。</p><p>这个选择的原因很简单：</p><table><thead><tr><th>能力</th><th>逐 PR 分析</th><th>源码全量扫描</th></tr></thead><tbody><tr><td>识别配置项移除</td><td>不行（已删除的行不会出现在 PR diff 中）</td><td>可以——全量解析新旧版本的 Config.java，对比字段集合</td></tr><tr><td>追踪间接调用链</td><td>不行——缺少源码上下文</td><td>可以——在源码树中 grep 递归追踪</td></tr><tr><td>集群配置冲突检测</td><td>不行——无法读取用户 conf</td><td>可以——解析 cluster-profile.yaml 并与 Scanner 结果交叉对比</td></tr><tr><td>识别”默认值变更但用户未覆盖”</td><td>不行</td><td>可以——对比 conf 中的值与新旧默认值</td></tr></tbody></table><p>简单来说：<strong>没有源码上下文，就没有深度分析。</strong></p><h1 id="设计哲学：宁可误报也不漏报"><a href="#设计哲学：宁可误报也不漏报" class="headerlink" title="设计哲学：宁可误报也不漏报"></a>设计哲学：宁可误报也不漏报</h1><p>这个工具的核心设计哲学是 <strong>prefer false positives over false negatives</strong>。</p><p>原因很简单：升级风险的成本是非对称的。漏报一个不兼容变更可能导致生产事故，而误报只是增加了人工验证的工作量。所以工具采用了多层级扫描策略：11 个专项 Scanner 覆盖已知风险模式 + 逐 commit Tier 分类确保无遗漏。</p><h1 id="整体架构"><a href="#整体架构" class="headerlink" title="整体架构"></a>整体架构</h1><p>整个工具的工作流分为四个阶段，先看一张全局视图：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615002106.png"></p><h2 id="Phase-1：数据收集"><a href="#Phase-1：数据收集" class="headerlink" title="Phase 1：数据收集"></a>Phase 1：数据收集</h2><p>这是整个工具的基础，由 <code>starrocks_upgrade.py</code> 实现。它要做的事情很多：</p><h3 id="Git-Commit-Diff-采集"><a href="#Git-Commit-Diff-采集" class="headerlink" title="Git Commit Diff 采集"></a>Git Commit Diff 采集</h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/imagesmac_1781454655007.png"></p><p>通过 <code>git log branchA..branchB</code> 获取目标分支独有的 commits，然后对每个 commit 做分类。这里有个关键优化——使用自定义分隔符（SOH&#x2F;STX）通过单次 <code>git log</code> 调用获取所有 commit 的完整信息，避免 N+1 查询。</p><h3 id="Commit-Tier-分类"><a href="#Commit-Tier-分类" class="headerlink" title="Commit Tier 分类"></a>Commit Tier 分类</h3><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615003636.png"></p><p>不是所有 commit 都需要深度分析。工具把 commit 分成四级：</p><table><thead><tr><th>Tier</th><th>匹配规则</th><th>处理方式</th></tr></thead><tbody><tr><td>SKIP</td><td>test&#x2F;docs&#x2F;build 目录；commit 前缀为 build&#x2F;chore&#x2F;ci&#x2F;style</td><td>仅统计数量</td></tr><tr><td>HIGH</td><td>核心路径：FE 优化器&#x2F;执行器&#x2F;SQL 解析、BE runtime&#x2F;storage、Protocol&#x2F;IDL</td><td>保存完整 diff + 深度分析</td></tr><tr><td>MEDIUM</td><td>业务路径：连接器&#x2F;认证&#x2F;权限；feat&#x2F;fix 类型的源码变更</td><td>保存完整 diff + 分析</td></tr><tr><td>LOW</td><td>其他所有变更</td><td>仅保存元数据</td></tr></tbody></table><p>这样 HIGH&#x2F;MEDIUM 的 commit 得到深度分析，LOW&#x2F;SKIP 不浪费资源。</p><h3 id="11-个专项-Scanner"><a href="#11-个专项-Scanner" class="headerlink" title="11 个专项 Scanner"></a>11 个专项 Scanner</h3><p>这是工具最核心的部分，覆盖了升级风险的 11 个维度：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615003917.png"></p><p><strong>FE 侧：</strong></p><ul><li>Config Scanner — 扫描 <code>Config.java</code> 中的 <code>@ConfField</code> 配置项变更</li><li>Session Variable Scanner — 扫描 <code>SessionVariable.java</code> 中的 <code>@VarAttr</code> 变量变更</li><li>System Variable Scanner — 扫描 <code>GlobalVariable.java</code></li><li>Auth Scanner — 扫描 <code>AuthenticationManager.java</code>、<code>PrivilegeManager.java</code></li></ul><p><strong>BE 侧：</strong></p><ul><li>BE Config Scanner — 扫描 <code>config.h</code> 中的 <code>CONF_*</code> 宏定义</li><li>Storage Format Scanner — 扫描 <code>segment_format.h</code>、<code>tablet_meta.h</code></li></ul><p><strong>IDL&#x2F;协议：</strong></p><ul><li>Protocol Scanner — 扫描 <code>.thrift</code> &#x2F; <code>.proto</code> 文件变更</li><li>Parser Scanner — 扫描 <code>StarRocksParser.g4</code>、<code>AstBuilder.java</code></li></ul><p><strong>数据&#x2F;类型：</strong></p><ul><li>Charset&#x2F;Collation Scanner — 扫描 <code>Collation*.java</code></li><li>Type System Scanner — 扫描 <code>ScalarType.java</code> &#x2F; <code>Column.java</code></li><li>MV Scanner — 扫描 <code>MaterializedView.java</code>、<code>MVRefreshParams.java</code></li></ul><p>每个 Scanner 的工作模式都一样：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615004232.png"></p><h3 id="Config-Scanner-的状态机解析"><a href="#Config-Scanner-的状态机解析" class="headerlink" title="Config Scanner 的状态机解析"></a>Config Scanner 的状态机解析</h3><p>这里值得展开说说，因为 Config.java 的解析是整个工具中最复杂的部分。</p><p>Java 注解可能跨多行：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@ConfField(mutable = true, comment = &quot;Whether to prefer string type &quot;</span><br><span class="hljs-meta">        + &quot;for fixed length varchar column in materialized view creation/ctas&quot;)</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-variable">transform_type_prefer_string_for_varchar</span> <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;<br></code></pre></td></tr></table></figure><p>所以解析器采用了<strong>逐行状态机</strong>模式：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615004523.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615004849.png"></p><p>状态机跟踪 <code>(</code> 和 <code>)</code> 的配对，将多行注解拼接后再解析 <code>mutable</code> 和 <code>comment</code> 属性。相比简单的正则匹配，这种方式能正确处理各种边界情况。</p><h3 id="BE-Config-解析"><a href="#BE-Config-解析" class="headerlink" title="BE Config 解析"></a>BE Config 解析</h3><p>BE 端使用 C++ 宏定义配置，解析方式完全不同：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs cpp"><span class="hljs-built_in">CONF_Bool</span>(datacache_auto_adjust_enable, <span class="hljs-string">&quot;false&quot;</span>)     <span class="hljs-comment">// 不可运行时修改</span><br><span class="hljs-built_in">CONF_mBool</span>(lake_enable_alter_struct, <span class="hljs-string">&quot;true&quot;</span>)          <span class="hljs-comment">// 可运行时修改 (m 前缀)</span><br></code></pre></td></tr></table></figure><p>正则 <code>CONF_(m?\w+)\((\w+),\s*&quot;([^&quot;]*)&quot;\)</code> 一把就能提取出来，注意 <code>m</code> 前缀表示 mutable，可运行时修改。</p><h3 id="集群配置冲突检测"><a href="#集群配置冲突检测" class="headerlink" title="集群配置冲突检测"></a>集群配置冲突检测</h3><p>这是我觉得最有用的功能。不同场景下同一个默认值变化的风险完全不同：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615005115.png"></p><table><thead><tr><th>场景</th><th>示例</th><th>风险</th></tr></thead><tbody><tr><td>配置已移除 + 你的 conf 中有</td><td><code>mysql_service_nio_enabled</code> 已删除，你 conf 中有 <code>= true</code></td><td>HIGH — 启动报错</td></tr><tr><td>默认值变化 + 你使用旧默认</td><td><code>enable_load_volume_from_conf</code> true→false，你 conf 中 <code>= true</code></td><td>MEDIUM — 你的覆盖生效，但需决定是否跟随</td></tr><tr><td>默认值变化 + 你有自定义值</td><td>你 conf 中设了 <code>= custom_value</code></td><td>LOW — 你的覆盖优先</td></tr><tr><td>默认值变化 + 你未覆盖</td><td><code>mysql_server_version</code> 5.1.0→8.0.33，你 conf 中没有</td><td>HIGH — 自动采用新默认值</td></tr></tbody></table><p>这种精确区分比泛泛地说”某个配置默认值变了”有用得多。</p><h3 id="部署方式感知"><a href="#部署方式感知" class="headerlink" title="部署方式感知"></a>部署方式感知</h3><p>工具还会根据集群的部署方式生成特定风险提示：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615005328.png"></p><p>比如 K8s 环境下，FE Pod 重启会触发 MV re-activation，如果有 MV 相关代码变更，可能导致 schema 不兼容；VM 环境下则更关注升级顺序（BE 先 FE 后）。</p><h2 id="Phase-2：Commit-Diff-分析"><a href="#Phase-2：Commit-Diff-分析" class="headerlink" title="Phase 2：Commit Diff 分析"></a>Phase 2：Commit Diff 分析</h2><p>Phase 1 保存了 HIGH&#x2F;MEDIUM commit 的完整 diff。Phase 2 由 AI Agent 执行，利用并行 Subagent 对 commit 进行深度兼容性分析。</p><p>由于跨版本 diff 的 commit 数量通常很大（3.3→3.5 有 1361 个 HIGH tier commit），逐个串行分析不现实。所以按模块分组，每组 5-8 个 commit 分配给一个并行 Subagent：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615005547.png"></p><p>每个 Subagent 输出结构化的分析结果：<code>compatibility_impact</code>、<code>impact_type</code>、<code>severity</code>、<code>error_scenario</code>、<code>reproduction</code>、<code>rollback</code>。</p><h2 id="Phase-3：深度影响分析"><a href="#Phase-3：深度影响分析" class="headerlink" title="Phase 3：深度影响分析"></a>Phase 3：深度影响分析</h2><p>Phase 2 的输出 + Phase 1 的 Scanner 发现中，所有 CRITICAL&#x2F;HIGH 级别的发现需要进一步深度分析。每个（或每批相关的）发现分配一个并行 Subagent，在源码树中 grep 追踪调用链。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615005815.png"></p><p>这是工具最独特的设计之一——<strong>系统生命周期入口追踪</strong>。一个配置变更可能不直接被生命周期代码引用，但通过间接调用链到达：</p><figure class="highlight erlang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs erlang"><span class="hljs-function"><span class="hljs-title">transform_type_prefer_string_for_varchar</span> <span class="hljs-params">(Config)</span></span><br><span class="hljs-function">  └─ A<span class="hljs-title">nalyzerUtils</span>.<span class="hljs-title">transformTableColumnType</span><span class="hljs-params">()</span> <span class="hljs-params">(直接调用方)</span></span><br><span class="hljs-function">       └─ M<span class="hljs-title">aterializedViewAnalyzer</span> <span class="hljs-params">(间接调用方)</span></span><br><span class="hljs-function">            └─ A<span class="hljs-title">lterJobMgr</span>.<span class="hljs-title">reActivateMV</span><span class="hljs-params">()</span> <span class="hljs-params">(系统生命周期入口: FE 重启时触发)</span></span><br></code></pre></td></tr></table></figure><p>如果不追踪这条间接路径，就会漏掉”FE 重启后 MV re-activation 失败”这个关键风险。</p><h2 id="Phase-4：报告综合"><a href="#Phase-4：报告综合" class="headerlink" title="Phase 4：报告综合"></a>Phase 4：报告综合</h2><p>将 Phase 1-3 的所有分析结果综合为一份结构化的中文升级报告。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615010857.png"></p><p>报告的核心设计原则：</p><ol><li><strong>INCOMPATIBLE CHANGES 置顶</strong>：最关键的信息放在最前面，按 CRITICAL &gt; HIGH 排序</li><li><strong>按触发时机分类报错场景</strong>：FE 重启后 &#x2F; CN 重启后 &#x2F; 日常查询 &#x2F; 升级过程中</li><li><strong>集群特定的冲突检测</strong>：只有与用户集群配置相关的冲突才展示</li><li><strong>可操作的 Upgrade Checklist</strong>：每个步骤都是具体的、可执行的</li></ol><h3 id="数据流全图"><a href="#数据流全图" class="headerlink" title="数据流全图"></a>数据流全图</h3><p>把 Phase 1 的数据流串起来看会更清晰：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260615011151.png"></p><h1 id="统一影响模型"><a href="#统一影响模型" class="headerlink" title="统一影响模型"></a>统一影响模型</h1><p>所有 Scanner 的发现都使用统一的四维影响模型：</p><table><thead><tr><th>维度</th><th>含义</th><th>触发条件示例</th></tr></thead><tbody><tr><td><code>data</code></td><td>影响现有数据</td><td><code>transform_type_prefer_string_for_varchar</code>、<code>max_varchar_length</code></td></tr><tr><td><code>behavior</code></td><td>相同 SQL 可能返回不同结果</td><td><code>sql_mode</code>、<code>mysql_server_version</code></td></tr><tr><td><code>operational</code></td><td>需要配置&#x2F;运维变更</td><td>任一 HIGH_RISK 配置变更</td></tr><tr><td><code>rolling_upgrade</code></td><td>混合版本集群可能中断</td><td><code>protocol_field_removed</code>、<code>storage_format_changed</code></td></tr></tbody></table><p>每个发现都附带四维评估，便于按维度筛选和汇总。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这个工具的设计思路可以归结为以下几点：</p><ol><li><p><strong>源码是真理</strong>：所有分析都建立在完整的源码树上，而不是 GitHub API 返回的 PR diff 片段。没有源码上下文，就没有深度分析。</p></li><li><p><strong>分层处理</strong>：不是所有 commit 都值得深度分析，Tier 分类的分层策略确保关键 commit 得到深度分析，低风险 commit 不浪费资源。</p></li><li><p><strong>专项 Scanner + AI Agent 的组合</strong>：Python 脚本做确定性的数据收集和模式匹配（11 个 Scanner），AI Agent 做不确定性的深度分析（调用链追踪、影响评估）。各取所长。</p></li><li><p><strong>集群特定</strong>：不是给出通用建议，而是结合用户实际的 fe.conf&#x2F;be.conf，精确识别集群特定风险。</p></li><li><p><strong>宁可误报也不漏报</strong>：升级风险的成本是非对称的，漏报的代价远大于误报。</p></li></ol><p>当然也有局限性：Protocol&#x2F;Parser Scanner 精度有限、间接调用链追踪依赖 AI Agent 的能力、无法检测运行时行为变化、大仓库性能问题（6000+ commit 需要 30 分钟以上）。这些也是后续改进的方向。</p><p>如果你也在维护 StarRocks 集群并且经常需要跨版本升级，可以试试这个工具。至少在我这边，它帮我发现了好几个 Release Notes 里没提到的不兼容变更。</p><p>对于这种大型项目，存在复杂的上下文分析完全是现在 LLM 擅长的地方，非常适合拿来做这种以前的人工体力活。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近在搞 StarRocks 的跨版本升级（3.3 → 3.5），中间踩了不少坑。我之前也写过一篇 &lt;a href=&quot;https://crossoverjie.top/2025/03/14/starrocks/StarRocks-upgrade/&quot;&gt;StarRocks 升级注意事项&lt;/a&gt;记录了手动升级的流程，但那只是针对小版本（3.3.3 → 3.3.9）的升级。&lt;/p&gt;
&lt;p&gt;跨大版本升级完全是另一回事——3.3 到 3.5 中间有 6000+ 个 commit，里面藏着各种不兼容变更：配置默认值变了、Session Variable 改了、Protocol 字段删了……人工逐个审查根本不现实，漏一个关键变更就可能导致生产事故。&lt;/p&gt;
&lt;p&gt;于是我就想：能不能用 AI 来帮我干这活？经过一段时间的迭代，我用 Claude Code 手搓了一个 StarRocks 升级风险扫描工具（&lt;a href=&quot;https://github.com/crossoverJie/skills/blob/main/skills/starrocks-upgrade/SKILL.md&quot;&gt;starrocks-upgrade skill&lt;/a&gt;），这篇文章就来聊聊它的设计原理。&lt;/p&gt;
&lt;p&gt;现在升级之前会先执行一次 Skill，它首先会让你输入一些集群信息方便后面做具体的分析：&lt;br&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260614235018.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;收集完成之后便会收集差异版本之间的 commit 信息开始分析，最终生成一个升级报告，给出一些潜在的风险，比如这个：&lt;br&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260614235225.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们在升级之后确实遇到了这个问题，提前有了这份报告之后解决起来自然也要轻松许多。&lt;/p&gt;</summary>
    
    
    
    <category term="StarRocks" scheme="http://crossoverjie.top/categories/StarRocks/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="StarRocks" scheme="http://crossoverjie.top/tags/StarRocks/"/>
    
  </entry>
  
  <entry>
    <title>[送码] 用 AI Coding 做了一个 App，谈谈 AI Coding 的真实体验</title>
    <link href="http://crossoverjie.top/2026/05/26/clipshelf/clipshelf-introduce/"/>
    <id>http://crossoverjie.top/2026/05/26/clipshelf/clipshelf-introduce/</id>
    <published>2026-05-26T21:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.961Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>相信现在 AI Coding 已经占据工作中大部分代码了，甚至很多人就直接交给 AI 来写，自己只做 review。</p><p>再有甚者 review 都不做了，直接全面交给 AI，自己只做产品经理提需求、验证功能逻辑是否正确——也就是现在很流行的 Vibe Coding。</p><p>前段时间我自己用 Vibe Coding 写了一个 macOS 原生的 <a href="https://github.com/crossoverJie/SkillDeck/">APP</a>，以此来判断 Vibe Coding 是否能让我在一个完全不熟悉的领域解决一个特定的问题。</p><p>项目地址：<a href="https://github.com/crossoverJie/SkillDeck/">https://github.com/crossoverJie/SkillDeck/</a></p><p>SkillDeck 是一个 macOS 原生的 AI Agent Skills 管理工具，提供了统一仪表盘、Skills 市场浏览、一键安装更新、Agent 分配（symlink 管理）等核心功能，让多个 AI Agent 的 Skills 管理更直观。<br>详细介绍参考：<a href="https://crossoverjie.top/2026/02/24/AI/skilldeck-intro/">skilldeck-intro</a></p><span id="more"></span><p>这个 APP 经过多轮迭代，已经能解决 skill 管理的一些问题了，所以我便着手开发自己的第一个 macOS 产品。</p><h2 id="为什么要做-ClipShelf"><a href="#为什么要做-ClipShelf" class="headerlink" title="为什么要做 ClipShelf"></a>为什么要做 ClipShelf</h2><p>想必大家在日常使用过程中都会用到「剪贴板」相关的 APP。在这之前，我一直使用的是 Paste APP。</p><p>当初选择它，主要是看中它的颜值比较高。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260526215455.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260526175719.png"></p><p>但它的售价并不便宜。类似的 APP 其实也蛮多的，还有一些开源替代方案，不过都或多或少缺一些我需要的功能。</p><p>由于之前已经验证了使用 AI Coding 可以解决我开发不熟悉领域的问题，所以这次我就想着不如自己做一个定制化的 APP，满足自己需求的同时也可以提供给有类似需求的人。</p><h1 id="ClipShelf-功能点"><a href="#ClipShelf-功能点" class="headerlink" title="ClipShelf 功能点"></a>ClipShelf 功能点</h1><p>说干就干。我花了一两个月的时间来开发这个 APP，给它命名为 ClipShelf，已经上架了 <a href="https://apps.apple.com/cn/app/clipshelf-%E6%99%BA%E8%83%BD%E6%9A%BA%E5%AD%98%E6%9E%B6%E4%B8%8E%E5%B1%8F%E5%B9%95%E6%96%87%E5%AD%97%E8%AF%86%E5%88%AB/id6760993477?mt=12">AppStore</a>。</p><p><img src="https://crossoverjie.top/ClipShelf/screenshot/%E4%B8%AD%E6%96%87/01.png"></p><p><img src="https://crossoverjie.top/ClipShelf/screenshot/%E4%B8%AD%E6%96%87/03_resized.png"></p><p><img src="https://crossoverjie.top/ClipShelf/screenshot/%E4%B8%AD%E6%96%87/04_resized.png"></p><p>剪贴板管理器相关的常规功能都支持，在这基础之上，额外还支持<strong>局域网内部的剪贴板数据共享</strong>，这对使用非苹果生态的设备非常有用。</p><p>后续还会新增 OCR 截图直接翻译的功能，由于是自己可控的 APP，可以灵活地新增和调整功能。对这个 APP 感兴趣的朋友也欢迎给我提反馈。</p><h1 id="兑换码"><a href="#兑换码" class="headerlink" title="兑换码"></a>兑换码</h1><p>我为大家准备了 20 个兑换码，欢迎体验后给出反馈，也请大家在 App Store 帮我打个分或者留个评论。</p><figure class="highlight dts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><code class="hljs dts"><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=KHPTN7ATFN8TPMEH7A</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=KPA4ATAPLTYE6KE334</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=X8HJLKETFRW6AAL38T</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=WWELMTYTJPANJYHFXT</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=FA63KEPTLE3J6KTRF3</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=3FY7THPYFY3HLWLAFK</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=63LPL6EWRX4KN34R7F</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=ETFTFLNWY3X4XYW87A</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=J6438JMX4HRR63JNKJ</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=W7MF4L3W7WHKMPERL4</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=LW4NHJYFYPH7EM6RJ8</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=MT44PKNMMN6Y3L764W</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=PLHP3E86PEHF4MLJ4N</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=Y8KYAAMF7HH7JHJ3KN</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=RLH4A33RPXWYLY3TXW</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=KRET6TTJHLK67FY7FF</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=4W4T3AMPNEFR3LYFXJ</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=FA3HT3LKN4HEMYYWEN</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=7JX3RKTKRYPYLH6FYP</span><br><span class="hljs-symbol"></span><br><span class="hljs-symbol">https:</span><span class="hljs-comment">//apps.apple.com/redeem?ctx=offercodes&amp;id=6760993477&amp;code=PRLYHYP7HA34TPYJH8</span><br></code></pre></td></tr></table></figure><p>复制链接到浏览器里打开就可以直接兑换。已经兑换了的朋友请在评论区留言，以防其他人重复兑换。</p><p>如果确实刚需但没抢到兑换码，也可以加我微信 <code>crossoverChen</code> 私聊，我会单独发兑换码。</p><h1 id="AI-Coding-的理解"><a href="#AI-Coding-的理解" class="headerlink" title="AI Coding 的理解"></a>AI Coding 的理解</h1><p>经过这两次 AI Coding 实践，我发现在 AI 时代更需要的是产品经理或者说项目 owner 这样的全局能力。</p><p>它不再需要我们像以前作为程序员一样，关注于某一个功能或者某一个代码架构的具体设计。我们交付的也不再是代码或者一个 package，而是一个完整的产品——这个完整的产品包含前期需求调研、UI 设计、编码开发、发布运维、市场推广、运营等一系列能力。</p><p>对于一个产品能否做成，编码能力反而是最弱的一环，更需要的是宣发能力、产品 UI 交互。而我们以前传统程序员的特有的编码能力被 AI 极大地磨平了，对产品经理来说是重大的利好。</p><p>当然，对于一个长生命周期的产品，或者说是一个复杂的大型软件架构来说，一个资深开发者的能力还是非常有必要的。</p><p>如果说你只让产品经理自己来维护调试 bug，那将非常灾难。在目前 AI 的能力下，复杂的系统仍然需要资深研发工程师。</p><p>但如果说我们只是解决某一个小的、具体领域的特定问题，使用 AI Coding 自己作为产品 owner 来进行发布迭代和市场运营，是完全足够的。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>后续我还会推出一些其他的 APP，大家敬请期待。</p><p>也会继续分享独立开发的一些经验，感兴趣的朋友可以持续关注，也欢迎加我微信进行探讨。</p><p>我创建了一个独立开发的交流群，感兴趣的朋友可以进群一起讨论。<br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images5b8f7b36bd23db1b23aa9d9aa966a3b6.jpg"></p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;相信现在 AI Coding 已经占据工作中大部分代码了，甚至很多人就直接交给 AI 来写，自己只做 review。&lt;/p&gt;
&lt;p&gt;再有甚者 review 都不做了，直接全面交给 AI，自己只做产品经理提需求、验证功能逻辑是否正确——也就是现在很流行的 Vibe Coding。&lt;/p&gt;
&lt;p&gt;前段时间我自己用 Vibe Coding 写了一个 macOS 原生的 &lt;a href=&quot;https://github.com/crossoverJie/SkillDeck/&quot;&gt;APP&lt;/a&gt;，以此来判断 Vibe Coding 是否能让我在一个完全不熟悉的领域解决一个特定的问题。&lt;/p&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://github.com/crossoverJie/SkillDeck/&quot;&gt;https://github.com/crossoverJie/SkillDeck/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;SkillDeck 是一个 macOS 原生的 AI Agent Skills 管理工具，提供了统一仪表盘、Skills 市场浏览、一键安装更新、Agent 分配（symlink 管理）等核心功能，让多个 AI Agent 的 Skills 管理更直观。&lt;br&gt;详细介绍参考：&lt;a href=&quot;https://crossoverjie.top/2026/02/24/AI/skilldeck-intro/&quot;&gt;skilldeck-intro&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    <category term="独立开发" scheme="http://crossoverjie.top/categories/AI/%E7%8B%AC%E7%AB%8B%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="独立开发" scheme="http://crossoverjie.top/tags/%E7%8B%AC%E7%AB%8B%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>手搓一个 Agent 驱动的项目 Wiki 生成方案</title>
    <link href="http://crossoverjie.top/2026/05/18/AI/cc-generate-wiki/"/>
    <id>http://crossoverjie.top/2026/05/18/AI/cc-generate-wiki/</id>
    <published>2026-05-18T00:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近我一直在折腾项目文档生成的事情。之前写过两篇关于 deepwiki 的文章：<a href="https://crossoverjie.top/2025/12/25/AI/deepwiki-rag-principle/">deepwiki-rag-principle</a> 讲了 RAG 原理，<a href="https://crossoverjie.top/2026/03/17/AI/deepwiki-optimize-line-number/">deepwiki-optimize-line-number</a> 聊了给代码加行号的优化。</p><p>经过几轮迭代，搞了两个优化：</p><ul><li>代码加上行号前缀</li><li>基于 Proto 文件生成确定性目录</li></ul><p>这两个优化背后其实是同一个思路：<strong>把确定的东西明确告诉 AI，不确定的才让 AI 来发挥</strong>。</p><table><thead><tr><th>类型</th><th>内容</th><th>处理方式</th></tr></thead><tbody><tr><td>确定的</td><td>代码行号</td><td>直接给 LLM 标注好</td></tr><tr><td>确定的</td><td>gRPC 接口列表、目录结构</td><td>代码解析，不经过 LLM</td></tr><tr><td>不确定的</td><td>函数功能解释</td><td>交给 LLM 归纳</td></tr><tr><td>不确定的</td><td>项目架构分析</td><td>交给 LLM 总结</td></tr><tr><td>不确定的</td><td>代码关联关系</td><td>交给 LLM 推理</td></tr></tbody></table><p>LLM 擅长理解、归纳和总结，但精准计算和结构化数据生成这块确实不太行。分开处理，各取所长，效果就好很多了。</p><p>这些都是用开源的 deepwiki-open 来做的。</p><h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><p>虽然最终生成的内容效果还不错，但还有个让人头疼的问题：</p><blockquote><p>需要为整个项目生成总结性的内容，比如项目架构、流程图、ER 图等。</p></blockquote><p>这些数据得根据之前已经生成的内容来总结，但 deepwiki 的架构是每个页面独立生成的。而 ER 图这种，我们希望是基于已生成的内容再汇总生成。</p><p>在现有架构下实现这个比较困难，索性换个思路。</p><h1 id="新方案"><a href="#新方案" class="headerlink" title="新方案"></a>新方案</h1><p>日常用 Claude Code（后面简称 CC）的时候发现，它可以精准定位到具体业务逻辑所在的代码片段，也能帮我们分析项目、提炼内容。</p><p>这不就是个完美的 Wiki 系统吗？直接让 CC 分析项目内容，生成静态页面，就能得到一个精准的 Wiki 了。</p><p>CC 也是通过一些内置 tools 来实现精准代码检索的，不需要 deepwiki 那种向量数据库，架构简单很多。</p><p>这里简单聊下 CC 的代码搜索原理。传统 RAG 方案会先把代码向量化存入数据库，然后通过语义相似度检索。但 CC 并没有走这条路，而是直接用了一套<strong>工具驱动（Tool-based）</strong>的检索机制：</p><table><thead><tr><th>工具</th><th>功能</th><th>使用场景</th></tr></thead><tbody><tr><td><code>Read</code></td><td>直接读取文件内容</td><td>已知文件路径时</td></tr><tr><td><code>Bash(grep)</code></td><td>基于正则匹配搜索代码</td><td>按关键字&#x2F;符号查找</td></tr><tr><td><code>Bash(find)</code></td><td>遍历文件系统</td><td>发现文件、按模式筛选</td></tr><tr><td><code>LSP</code></td><td>语言服务器协议导航</td><td>跳转到定义、查找引用</td></tr><tr><td><code>Agent</code></td><td>子 Agent 并行搜索</td><td>大规模代码库分治检索</td></tr></tbody></table><p>这种设计的巧妙之处在于：LLM 不依赖向量化后的”模糊记忆”，而是像人类开发者一样，通过<strong>精确的工具调用</strong>来定位代码。比如要找某个函数定义，CC 可能会先 <code>grep</code> 找到候选文件，再用 <code>Read</code> 精读确认，最后用 <code>LSP</code> 验证引用关系——整个过程是<strong>确定性的、可解释的</strong>。</p><blockquote><p>想了解更多细节可以参考 Anthropic 官方文档：<a href="https://docs.anthropic.com/en/docs/claude-code/overview">Claude Code Overview</a></p></blockquote><p>后续 repo 有更新，只需要让 CC 读取 git log 变更记录，自动更新修改的内容就行。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260518180243.png" alt="CC Wiki 架构"></p><h2 id="提炼-Skill"><a href="#提炼-Skill" class="headerlink" title="提炼 Skill"></a>提炼 Skill</h2><p>考虑内部项目众多，为了让其他项目也能复用这个能力，我把生成静态网站的过程写成了一个 Skill。其他项目只需要在 CC 里调用这个 Skill 即可。</p><p>目录结构大概长这样：</p><figure class="highlight dos"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs dos">├── SKILL.<span class="hljs-built_in">md</span><br>├── skill.json<br>├── templates/<br>│   ├── page-architecture.<span class="hljs-built_in">md</span><br>│   ├── page-er.<span class="hljs-built_in">md</span><br>│   ├── page-features.<span class="hljs-built_in">md</span><br>│   └── page-service.<span class="hljs-built_in">md</span><br>└── wiki/<br>    ├── <span class="hljs-number">01</span>-系统架构.<span class="hljs-built_in">md</span><br>    ├── <span class="hljs-number">02</span>-核心功能.<span class="hljs-built_in">md</span><br>    ├── <span class="hljs-number">03</span>-ER图.<span class="hljs-built_in">md</span><br>    ├── index.html<br>    └── service/<br>        └── *.<span class="hljs-built_in">md</span><br></code></pre></td></tr></table></figure><h1 id="优缺点对比"><a href="#优缺点对比" class="headerlink" title="优缺点对比"></a>优缺点对比</h1><h2 id="deepwiki"><a href="#deepwiki" class="headerlink" title="deepwiki"></a>deepwiki</h2><p><strong>优点：</strong></p><ul><li>可以一键生成整个项目，生成过程中不需要人工干预</li></ul><p><strong>缺点：</strong></p><ul><li>无法精准调整某个页面</li><li>对于需要汇总已生成数据的需求，架构无法满足</li></ul><h2 id="Claude-Code-方案"><a href="#Claude-Code-方案" class="headerlink" title="Claude Code 方案"></a>Claude Code 方案</h2><p><strong>优点：</strong></p><ul><li>可以精准调整每一个页面</li><li>数据可以做到非常精准</li></ul><p><strong>缺点：</strong></p><ul><li>无法一键生成结果，需要多轮对话调试</li><li>如果部署到服务器上，需要外部工具对 CC 进行管理</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>其实这两个方案并不冲突，可以看成不同阶段的选择：</p><ul><li>项目初期需要快速搭个文档框架 → deepwiki 一键生成</li><li>项目成熟需要精准可控的文档 → CC 方案慢慢打磨</li></ul><p>CC 方案的核心优势在于<strong>可控性</strong>。虽然要多花点时间调试，但生成的内容质量确实更高，特别是涉及到跨文件关联分析的时候。</p><p>当然，CC 方案目前还不能完全自动化，这是最大的限制。不过随着 CC 生态的发展，相信后面会有更好的解法。让子弹飞一会。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近我一直在折腾项目文档生成的事情。之前写过两篇关于 deepwiki 的文章：&lt;a href=&quot;https://crossoverjie.</summary>
      
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    <category term="工程实践" scheme="http://crossoverjie.top/categories/AI/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>从企业版 Istio 迁移到社区版：一场给高速行驶汽车换轮胎的实践</title>
    <link href="http://crossoverjie.top/2026/04/15/istio-enterprise-to-community-migration/"/>
    <id>http://crossoverjie.top/2026/04/15/istio-enterprise-to-community-migration/</id>
    <published>2026-04-15T14:30:00.000Z</published>
    <updated>2026-07-30T08:43:22.966Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近我们在做一件非常危险的大事——把用了好几年的腾讯云企业版 Istio 服务网格迁移到社区开源版。</p><p>事情的起因是腾讯云突然宣布不再维护 Istio 服务网格了，后续也不会推出新版本。这就导致我们的网格一直停留在旧版本，进而连带着 k8s 这些基础组件也很难升级。可以说是血的教训：用托管服务一时爽，一旦厂商放弃维护，迁移成本还是得自己扛。</p><span id="more"></span><p>这事儿其实拖了很久，毕竟迁移涉及到的系统非常多，相当于要对「高速行驶的汽车更换轮胎」，稍有不慎就是车毁人亡。虽然已经测试验证的七七八八了，但一直不敢动手。</p><p>直到最近，我们利用 AI 对 Istio 关键代码进行了深入分析，从源头来确保迁移的可靠性——这也算 AI 在实际工程中的一个应用案例了。</p><h1 id="迁移方案概览"><a href="#迁移方案概览" class="headerlink" title="迁移方案概览"></a>迁移方案概览</h1><p>我们采用的是<strong>双控制面并行 + 按 namespace 灰度迁移</strong>的策略，核心思路是：</p><ol><li><strong>并行部署</strong>：在同一个集群里同时运行企业版和社区版两套控制面</li><li><strong>标签驱动</strong>：通过 namespace 的 <code>istio.io/rev</code> 标签决定 Pod 注入哪个版本的 sidecar</li><li><strong>discoverySelectors</strong>：社区版控制面只感知打了特定标签的 namespace，实现隔离</li><li><strong>灰度切换</strong>：逐个 namespace 切换，随时可回滚</li></ol><h2 id="安装社区版控制面"><a href="#安装社区版控制面" class="headerlink" title="安装社区版控制面"></a>安装社区版控制面</h2><p>首先安装社区版 Istio 控制面，指定一个独立的 revision 和 namespace：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">install.istio.io/v1alpha1</span><br><span class="hljs-attr">kind:</span> <span class="hljs-string">IstioOperator</span><br><span class="hljs-attr">metadata:</span><br>  <span class="hljs-attr">namespace:</span> <span class="hljs-string">istio-1-16-5</span><br><span class="hljs-attr">spec:</span><br>  <span class="hljs-attr">profile:</span> <span class="hljs-string">minimal</span><br>  <span class="hljs-attr">revision:</span> <span class="hljs-string">istio-1-16-5</span><br>  <span class="hljs-attr">meshConfig:</span><br>    <span class="hljs-attr">enablePrometheusMerge:</span> <span class="hljs-literal">false</span><br>    <span class="hljs-attr">accessLogFile:</span> <span class="hljs-string">/dev/stdout</span><br>    <span class="hljs-comment"># 关键：只感知带特定标签的 namespace</span><br>    <span class="hljs-attr">discoverySelectors:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">matchLabels:</span><br>          <span class="hljs-attr">usergroup:</span> <span class="hljs-string">istio-1-16-5</span><br>  <span class="hljs-attr">values:</span><br>    <span class="hljs-attr">global:</span><br>      <span class="hljs-attr">istioNamespace:</span> <span class="hljs-string">istio-1-16-5</span><br></code></pre></td></tr></table></figure><blockquote><p><code>discoverySelectors</code> 是这次迁移的安全保证，确保社区版和企业版控制面不会互相干扰。</p></blockquote><p>然后安装 IngressGateway：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">install.istio.io/v1alpha1</span><br><span class="hljs-attr">kind:</span> <span class="hljs-string">IstioOperator</span><br><span class="hljs-attr">metadata:</span><br>  <span class="hljs-attr">name:</span> <span class="hljs-string">istio-public-api-ingressgateway</span><br><span class="hljs-attr">spec:</span><br>  <span class="hljs-attr">profile:</span> <span class="hljs-string">empty</span>  <span class="hljs-comment"># 不安装 CRD 或控制平面</span><br>  <span class="hljs-attr">components:</span><br>    <span class="hljs-attr">ingressGateways:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">istio-ingressgateway</span><br>      <span class="hljs-attr">namespace:</span> <span class="hljs-string">istio-1-16-5</span><br>      <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span><br>      <span class="hljs-attr">label:</span><br>        <span class="hljs-attr">istio:</span> <span class="hljs-string">oss-public-api-ingressgateway</span><br>  <span class="hljs-attr">values:</span><br>    <span class="hljs-attr">gateways:</span><br>      <span class="hljs-attr">istio-ingressgateway:</span><br>        <span class="hljs-attr">injectionTemplate:</span> <span class="hljs-string">gateway</span><br>        <span class="hljs-attr">runAsRoot:</span> <span class="hljs-literal">true</span><br></code></pre></td></tr></table></figure><p>执行安装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">istioctl install -y -f istio-control-plane.yaml<br>istioctl install -f istio-gateway.yaml<br></code></pre></td></tr></table></figure><blockquote><p>使用 istioctl 工具安装</p></blockquote><h2 id="Namespace-切换流程"><a href="#Namespace-切换流程" class="headerlink" title="Namespace 切换流程"></a>Namespace 切换流程</h2><p>给要迁移的 namespace 打上标签：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">labels:</span><br>  <span class="hljs-attr">istio.io/rev:</span> <span class="hljs-string">istio-1-16-5</span><br>  <span class="hljs-attr">usergroup:</span> <span class="hljs-string">istio-1-16-5</span><br></code></pre></td></tr></table></figure><blockquote><p>注意：标签打在 namespace 上之后，<strong>已有的 Pod 不会自动重启</strong>，需要手动 <code>rollout restart</code> 才会触发重新注入。</p></blockquote><p>切换的核心机制是：<strong>MutatingWebhookConfiguration 根据 namespace 标签匹配</strong>。当 Pod 创建时，K8s API Server 会根据 namespace 的 <code>istio.io/rev</code> 标签路由到对应的 webhook，从而注入对应版本的 sidecar。</p><h2 id="网关配置"><a href="#网关配置" class="headerlink" title="网关配置"></a>网关配置</h2><p>Gateway CR 的 selector 与 Pod 标签做等值匹配：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.istio.io/v1alpha3</span><br><span class="hljs-attr">kind:</span> <span class="hljs-string">Gateway</span><br><span class="hljs-attr">metadata:</span><br>  <span class="hljs-attr">name:</span> <span class="hljs-string">istio-public-api-ingressgateway</span><br>  <span class="hljs-attr">namespace:</span> <span class="hljs-string">default</span><br><span class="hljs-attr">spec:</span><br>  <span class="hljs-attr">servers:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span><br>        <span class="hljs-attr">number:</span> <span class="hljs-number">80</span><br>        <span class="hljs-attr">name:</span> <span class="hljs-string">http</span><br>        <span class="hljs-attr">protocol:</span> <span class="hljs-string">HTTP</span><br>      <span class="hljs-attr">hosts:</span><br>        <span class="hljs-bullet">-</span> <span class="hljs-string">&#x27;*.y7test.com&#x27;</span><br>  <span class="hljs-attr">selector:</span><br>    <span class="hljs-attr">istio:</span> <span class="hljs-string">ingressgateway</span><br></code></pre></td></tr></table></figure><p>迁移期间两套网关并存，各自连接自己的控制面：</p><table><thead><tr><th>控制面</th><th>网关 Pod</th><th>感知的 VirtualService 范围</th></tr></thead><tbody><tr><td>企业版</td><td>旧网关（istio-system）</td><td>全量</td></tr><tr><td>社区版</td><td>新网关（istio-1-16-5）</td><td>仅 <code>usergroup: istio-1-16-5</code> 的 namespace</td></tr></tbody></table><blockquote><p>DNS 切换时机：等新网关稳定后，修改 DNS 指向新 IP。TTL 期间两套网关同时承接流量。</p></blockquote><h1 id="关键源码验证"><a href="#关键源码验证" class="headerlink" title="关键源码验证"></a>关键源码验证</h1><p>这次迁移最大的不同是，我们不只是「试试能不能跑」，而是<strong>深入到源码层面验证每个环节的可行性</strong>。</p><h2 id="1-Sidecar-注入的-Revision-选择"><a href="#1-Sidecar-注入的-Revision-选择" class="headerlink" title="1. Sidecar 注入的 Revision 选择"></a>1. Sidecar 注入的 Revision 选择</h2><p>注入由 <code>MutatingWebhookConfiguration</code> 的 <code>namespaceSelector</code> 决定。源码在 <code>istioctl/pkg/injector/injector-list.go</code>：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">getInjector</span><span class="hljs-params">(namespace *corev1.Namespace, hooks []admitv1.MutatingWebhookConfiguration)</span></span> *admitv1.MutatingWebhookConfiguration &#123;<br>    <span class="hljs-keyword">for</span> _, hook := <span class="hljs-keyword">range</span> hooks &#123;<br>        <span class="hljs-keyword">for</span> _, webhook := <span class="hljs-keyword">range</span> hook.Webhooks &#123;<br>            nsSelector, err := metav1.LabelSelectorAsSelector(webhook.NamespaceSelector)<br>            <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> &#123; <span class="hljs-keyword">continue</span> &#125;<br>            <span class="hljs-comment">// 关键：用 namespace 的当前标签做匹配</span><br>            <span class="hljs-keyword">if</span> nsSelector.Matches(api_pkg_labels.Set(namespace.ObjectMeta.Labels)) &#123;<br>                <span class="hljs-keyword">return</span> &amp;hook<br>            &#125;<br>        &#125;<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span><br>&#125;<br></code></pre></td></tr></table></figure><p><strong>结论</strong>：<code>kubectl label ns &lt;ns&gt; istio.io/rev=self-1-16-5</code> 修改标签后，新建的 Pod 会自动命中社区版的 webhook。</p><h2 id="2-DiscoverySelectors-隔离机制"><a href="#2-DiscoverySelectors-隔离机制" class="headerlink" title="2. DiscoverySelectors 隔离机制"></a>2. DiscoverySelectors 隔离机制</h2><p>社区版 istiod 通过 <code>discoverySelectors</code> 决定感知哪些 namespace：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(d *discoveryNamespacesFilter)</span></span> SelectorsChanged(<br>    discoverySelectors []*metav1.LabelSelector,<br>) (selectedNamespaces []<span class="hljs-type">string</span>, deselectedNamespaces []<span class="hljs-type">string</span>) &#123;<br>    <span class="hljs-comment">// 遍历所有 Namespace，匹配 selector 的入选</span><br>    <span class="hljs-keyword">for</span> _, ns := <span class="hljs-keyword">range</span> namespaceList &#123;<br>        <span class="hljs-keyword">for</span> _, selector := <span class="hljs-keyword">range</span> selectors &#123;<br>            <span class="hljs-keyword">if</span> selector.Matches(labels.Set(ns.Labels)) &#123;<br>                newDiscoveryNamespaces.Insert(ns.Name)<br>            &#125;<br>        &#125;<br>    &#125;<br>    <span class="hljs-comment">// ...</span><br>&#125;<br><br><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(d *discoveryNamespacesFilter)</span></span> Filter(obj any) <span class="hljs-type">bool</span> &#123;<br>    <span class="hljs-comment">// 未配置 discoverySelectors 则允许所有</span><br>    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(d.discoverySelectors) == <span class="hljs-number">0</span> &#123;<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span><br>    &#125;<br>    <span class="hljs-comment">// 只处理选中 namespace 的资源</span><br>    <span class="hljs-keyword">return</span> d.discoveryNamespaces.Contains(object.GetNamespace())<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>结论</strong>：标签变更实时生效，无需重启 istiod。</p><h2 id="3-CA-根证书的自动下发"><a href="#3-CA-根证书的自动下发" class="headerlink" title="3. CA 根证书的自动下发"></a>3. CA 根证书的自动下发</h2><p>Namespace 入选后，<code>NamespaceController</code> 会自动将社区版 istiod 的 CA 证书写入该 namespace 的 <code>istio-ca-root-cert</code> ConfigMap：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(nc *NamespaceController)</span></span> insertDataForNamespace(o types.NamespacedName) <span class="hljs-type">error</span> &#123;<br>    meta := metav1.ObjectMeta&#123;<br>        Name:      CACertNamespaceConfigMap,  <span class="hljs-comment">// &quot;istio-ca-root-cert&quot;</span><br>        Namespace: ns,<br>    &#125;<br>    <span class="hljs-comment">// 写入自建 istiod 的根证书</span><br>    <span class="hljs-keyword">return</span> k8s.InsertDataToConfigMap(nc.client, nc.configmapLister, meta,<br>        nc.caBundleWatcher.GetCABundle())<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>为什么关键</strong>：sidecar 的 pilot-agent 用这个证书验证 istiod 的 TLS 身份，证书对了才能建立 XDS 连接。</p><h2 id="4-Sidecar-连接新-Istiod-的路径"><a href="#4-Sidecar-连接新-Istiod-的路径" class="headerlink" title="4. Sidecar 连接新 Istiod 的路径"></a>4. Sidecar 连接新 Istiod 的路径</h2><p>注入模板中的 <code>DiscoveryAddress</code> 由 revision 和 namespace 计算得出：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">IstiodHost</span><span class="hljs-params">(ns <span class="hljs-type">string</span>, revision <span class="hljs-type">string</span>)</span></span> <span class="hljs-type">string</span> &#123;<br>    istiod := <span class="hljs-string">&quot;istiod&quot;</span><br>    <span class="hljs-keyword">if</span> isRevisioned(revision) &#123;<br>        istiod = fmt.Sprintf(<span class="hljs-string">&quot;%s-%s&quot;</span>, istiod, revision)<br>    &#125;<br>    <span class="hljs-keyword">return</span> fmt.Sprintf(<span class="hljs-string">&quot;%s.%s.svc&quot;</span>, istiod, ns)<br>&#125;<br><br><span class="hljs-comment">// 结果：istiod-self-1-16-5.istio-self.svc:15012</span><br></code></pre></td></tr></table></figure><p>pilot-agent 启动时从挂载的 ConfigMap 读取根证书：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(a *Agent)</span></span> FindRootCAForXDS() (<span class="hljs-type">string</span>, <span class="hljs-type">error</span>) &#123;<br>    <span class="hljs-comment">// 默认路径：/var/run/secrets/istio/root-cert.pem</span><br>    rootCAPath = path.Join(CitadelCACertPath, constants.CACertNamespaceConfigMapDataName)<br>    <span class="hljs-comment">// ...</span><br>&#125;<br></code></pre></td></tr></table></figure><p><strong>结论</strong>：只要 webhook 注入正确，新 Pod 会自动连接到社区版 istiod，无需额外配置。</p><h2 id="5-跨控制面互通（ALLOW-ANY）"><a href="#5-跨控制面互通（ALLOW-ANY）" class="headerlink" title="5. 跨控制面互通（ALLOW_ANY）"></a>5. 跨控制面互通（ALLOW_ANY）</h2><p>迁移期间，新 sidecar 可能需要访问旧 namespace 的服务。由于社区版 istiod 不感知旧 namespace，不会生成对应的 Cluster&#x2F;Endpoint。此时流量会走 <code>PassthroughCluster</code>：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">buildOutboundCatchAllNetworkFiltersOnly</span><span class="hljs-params">(...)</span></span> []*listener.Filter &#123;<br>    <span class="hljs-keyword">var</span> egressCluster <span class="hljs-type">string</span><br>    <span class="hljs-keyword">if</span> util.IsAllowAnyOutbound(node) &#123;<br>        <span class="hljs-comment">// ALLOW_ANY 模式：未知流量直接透传到原始目标</span><br>        egressCluster = util.PassthroughCluster<br>    &#125; <span class="hljs-keyword">else</span> &#123;<br>        <span class="hljs-comment">// REGISTRY_ONLY 模式：未知流量丢弃</span><br>        egressCluster = util.BlackHoleCluster<br>    &#125;<br>    <span class="hljs-comment">// ...</span><br>&#125;<br></code></pre></td></tr></table></figure><p><strong>结论</strong>：<code>meshConfig.outboundTrafficPolicy.mode: ALLOW_ANY</code> 确保迁移期间跨控制面的流量可以正常通行。</p><h2 id="6-网关路由隔离"><a href="#6-网关路由隔离" class="headerlink" title="6. 网关路由隔离"></a>6. 网关路由隔离</h2><p>新网关的路由只包含已迁移 namespace 的 VirtualService：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(configgen *ConfigGeneratorImpl)</span></span> buildGatewayHTTPRouteConfig(...) *route.RouteConfiguration &#123;<br>    <span class="hljs-comment">// ...</span><br>    <span class="hljs-keyword">for</span> _, server := <span class="hljs-keyword">range</span> servers &#123;<br>        <span class="hljs-comment">// PushContext 中的 VS 已经被 discoverySelectors 过滤过</span><br>        virtualServices = push.VirtualServicesForGateway(node.ConfigNamespace, gatewayName)<br>        <span class="hljs-comment">// 旧 namespace 的 VS 不在其中 → 路由为空</span><br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p><strong>结论</strong>：新旧网关的路由天然隔离，不会出现流量错乱。</p><h1 id="踩过的坑"><a href="#踩过的坑" class="headerlink" title="踩过的坑"></a>踩过的坑</h1><h2 id="证书不匹配"><a href="#证书不匹配" class="headerlink" title="证书不匹配"></a>证书不匹配</h2><p>已经安装社区版 istio 后再安装企业版网关，启动失败，提示证书不匹配。原因是 sidecar 注入时使用了错误的 CA 证书。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260414170958.png"></p><blockquote><p>证书不匹配错误</p></blockquote><p><strong>解决方案</strong>：手动修改企业版 deployment 的 label，使其匹配社区版的 revision。</p><h2 id="503-错误"><a href="#503-错误" class="headerlink" title="503 错误"></a>503 错误</h2><p>社区版网关出现 503，查看日志发现 upstream 连接失败。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260414171224.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260414171230.png"></p><blockquote><p>503 错误及日志</p></blockquote><p><strong>解决方案</strong>：namespace 需要匹配 <code>discoverySelectors</code> 配置的 label，确保 istiod 能感知到该 namespace 的服务。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260414171242.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260414171249.png"></p><blockquote><p>namespace 打上对应 label 后问题解决</p></blockquote><h2 id="不能用-purge-卸载"><a href="#不能用-purge-卸载" class="headerlink" title="不能用 --purge 卸载"></a>不能用 <code>--purge</code> 卸载</h2><p>安装社区版 istio 之后不能用 <code>istioctl uninstall --purge</code> 卸载，会把企业版的 CRD 也一并删掉。</p><p><strong>解决方案</strong>：只能手动删除社区版相关的 deployment 和 webhook。</p><h2 id="回滚失败"><a href="#回滚失败" class="headerlink" title="回滚失败"></a>回滚失败</h2><p>如果回滚到企业版失败，通常是社区版的 webhook 还在拦截请求。</p><p><strong>解决方案</strong>：删除社区版的 <code>MutatingWebhookConfiguration</code> 和相关的 CRD。</p><h1 id="迁移检查清单"><a href="#迁移检查清单" class="headerlink" title="迁移检查清单"></a>迁移检查清单</h1><h3 id="迁移前"><a href="#迁移前" class="headerlink" title="迁移前"></a>迁移前</h3><ul><li><input disabled="" type="checkbox"> 备份 CRD、namespace 标签、Gateway 配置</li><li><input disabled="" type="checkbox"> 社区版 istiod 和网关启动并健康</li><li><input disabled="" type="checkbox"> 确认 <code>outboundTrafficPolicy: ALLOW_ANY</code> 已配置</li><li><input disabled="" type="checkbox"> 社区版 <code>proxy-status</code> 无 STALE</li></ul><h3 id="迁移单个-namespace"><a href="#迁移单个-namespace" class="headerlink" title="迁移单个 namespace"></a>迁移单个 namespace</h3><ul><li><input disabled="" type="checkbox"> 打上标签：<code>istio.io/rev=self-1-16-5</code>、<code>usergroup=istio-1-16-5</code></li><li><input disabled="" type="checkbox"> 确认 <code>istio-ca-root-cert</code> ConfigMap 已自动创建</li><li><input disabled="" type="checkbox"> 滚动重启 deployment</li><li><input disabled="" type="checkbox"> 验证 sidecar 连接新 istiod：<code>istioctl proxy-status</code></li><li><input disabled="" type="checkbox"> 验证跨命名空间调用正常</li><li><input disabled="" type="checkbox"> 通过新网关访问该 namespace 服务正常</li></ul><h3 id="DNS-切换"><a href="#DNS-切换" class="headerlink" title="DNS 切换"></a>DNS 切换</h3><ul><li><input disabled="" type="checkbox"> 新网关 External IP 稳定</li><li><input disabled="" type="checkbox"> 修改 DNS 指向新 IP</li><li><input disabled="" type="checkbox"> TTL 到期后监控新网关流量</li><li><input disabled="" type="checkbox"> 保留旧网关至少 1 个 TTL 周期</li></ul><h3 id="迁移后"><a href="#迁移后" class="headerlink" title="迁移后"></a>迁移后</h3><ul><li><input disabled="" type="checkbox"> 所有 namespace 的 <code>proxy-status</code> 显示新 istiod</li><li><input disabled="" type="checkbox"> <code>pilot_xds_push_errors_total</code> 无增长</li><li><input disabled="" type="checkbox"> 错误率与迁移前一致</li><li><input disabled="" type="checkbox"> 旧网关流量降为 0 后下线企业版控制面</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这次迁移从筹备到最终执行拖了挺久，主要是涉及面广、风险高。但通过深入源码分析每个关键环节，我们对整个迁移过程有了更清晰的把控。</p><p>几个核心经验：</p><ol><li><strong>discoverySelectors 是隔离的关键</strong>：确保两套控制面互不干扰</li><li><strong>Webhook 标签匹配决定注入版本</strong>：namespace 标签变更后要重启 Pod 才生效</li><li><strong>ALLOW_ANY 保障灰度期间的互通</strong>：新 sidecar 可以访问旧服务</li><li><strong>网关路由天然隔离</strong>：新旧网关各自连接自己的控制面</li></ol><p>当然，实际的迁移操作还是由我们人工来执行的，AI 在这个过程中主要提供了「理论支持」——帮我们快速定位源码逻辑、验证方案可行性。</p><p>这也让我感受到，AI 在复杂工程问题上的价值不只是「写代码」，更重要的是<strong>辅助理解复杂系统的工作原理</strong>，让我们在做高风险变更时更有底气。</p><p>毕竟，给高速行驶的汽车换轮胎，光靠胆子大是不够的，还得对汽车的每个零件都了如指掌。</p><hr><p>参考链接：</p><ul><li><a href="https://istio.io/v1.18/docs/setup/install/istioctl">Istio 安装文档</a></li><li><a href="https://preliminary.istio.io/latest/docs/ops/common-problems/injection/">Sidecar 注入问题排查</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近我们在做一件非常危险的大事——把用了好几年的腾讯云企业版 Istio 服务网格迁移到社区开源版。&lt;/p&gt;
&lt;p&gt;事情的起因是腾讯云突然宣布不再维护 Istio 服务网格了，后续也不会推出新版本。这就导致我们的网格一直停留在旧版本，进而连带着 k8s 这些基础组件也很难升级。可以说是血的教训：用托管服务一时爽，一旦厂商放弃维护，迁移成本还是得自己扛。&lt;/p&gt;</summary>
    
    
    
    <category term="Istio" scheme="http://crossoverjie.top/categories/Istio/"/>
    
    <category term="k8s" scheme="http://crossoverjie.top/categories/Istio/k8s/"/>
    
    
    <category term="Istio" scheme="http://crossoverjie.top/tags/Istio/"/>
    
    <category term="ServiceMesh" scheme="http://crossoverjie.top/tags/ServiceMesh/"/>
    
    <category term="云原生" scheme="http://crossoverjie.top/tags/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    
  </entry>
  
  <entry>
    <title>企业大模型应用与 Vibe Coding 实战</title>
    <link href="http://crossoverjie.top/2026/03/24/AI/%E4%BC%81%E4%B8%9A%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%BA%94%E7%94%A8%E4%B8%8E-Vibe-Coding-%E5%AE%9E%E6%88%98/"/>
    <id>http://crossoverjie.top/2026/03/24/AI/%E4%BC%81%E4%B8%9A%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%BA%94%E7%94%A8%E4%B8%8E-Vibe-Coding-%E5%AE%9E%E6%88%98/</id>
    <published>2026-03-24T00:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.956Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260324160917.png"></p><p>上周末参加了一个我们重庆本地的一个 AI 分享会，分享了一些企业大模型与 Vibe Coding 的实战经验，现在把它整理成一篇 blog。</p><span id="more"></span><h2 id="企业大模型应用实践"><a href="#企业大模型应用实践" class="headerlink" title="企业大模型应用实践"></a>企业大模型应用实践</h2><h3 id="利用-AI-提高故障排查效率"><a href="#利用-AI-提高故障排查效率" class="headerlink" title="利用 AI 提高故障排查效率"></a>利用 AI 提高故障排查效率</h3><p>首先是企业大模型的应用实践，我们利用现在的大模型结合可观测性来实现故障的自动分析。</p><p>最终可以实现的效果如下图所示，他可以直接分析当前 trace 下的所有链路、日志、指标、profile、内存布局，代码等信息整理在一起发给大模型，由大模型总结整理给出结论。</p><p>其实开发日常排查问题也是这样的过程，只是将它自动化了而已。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320161930.png"></p><p>对可观测性有了解的朋友应该对这张图很熟悉，通过 trace_id 可以在日志系统里获取到日志、通过 trace 的时间戳也能获取到一个时间范围的指标监控数据。</p><p>本质上都是可以获取到这些结构化的数据，代码也是一样的，gitlab 也有相关的接口可以获取到代码信息。</p><p>这里的重点便是上下文可以给多少，给的越相关效果越好，目前我们也还在调试，给的信息多了可能也会导致大模型的幻觉。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320164233.png"><br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320170223.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320165030.png"></p><p>重点：</p><ul><li>尽可能能多的采集应用数据（trace、日志、指标、代码、profile 等信息）</li><li>上下文内容尽量聚焦</li><li>数据获取的方式可以直接使用基建数据的接口或者是 MCP（推荐有接口就通过接口获取）</li></ul><h3 id="定制属于自己的-deepwiki"><a href="#定制属于自己的-deepwiki" class="headerlink" title="定制属于自己的 deepwiki"></a>定制属于自己的 deepwiki</h3><p>去年下半年火过一段时间的一个项目：<a href="https://deepwiki.com/">deepwiki</a>，利用大模型将 github 上开源的项目做成一个可多次对话的 wiki，方便我们更好的理解项目；下图是 <a href="https://deepwiki.com/openclaw/openclaw">openclaw</a> 的 deepwiki。<br>我们内部也有类似的需求，需要将所有的代码 repo 做成一个 deepwiki，方便新人或者不熟的同事快速了解一个项目。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321105103.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320171155.png"></p><h4 id="可以确定的数据不要交给-AI-生成"><a href="#可以确定的数据不要交给-AI-生成" class="headerlink" title="可以确定的数据不要交给 AI 生成"></a>可以确定的数据不要交给 AI 生成</h4><p>我们是用一个开源的 <a href="https://github.com/AsyncFuncAI/deepwiki-open">deepwiki-open</a> 系统来改造的，生成出来的代码行数和业务逻辑基本上对不上，经过分析发现是 LLM 自己估算的。</p><p>后续经过优化，我们自己将代码里的行号带上发送给大模型，这个问题就基本上被解决，重点是不要让大模型做逻辑计算，有现成的数据便让他直接读取。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320173436.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321002830.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320174750.png"><br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260320174828.png"><br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321002709.png"></p><h4 id="根据自己的业务做定制大模型应用"><a href="#根据自己的业务做定制大模型应用" class="headerlink" title="根据自己的业务做定制大模型应用"></a>根据自己的业务做定制大模型应用</h4><p>由于 <a href="https://github.com/AsyncFuncAI/deepwiki-open">deepwiki-open</a> 这个项目是一个开源通用的项目，所以他没法根据自己项目的情况做优化。</p><p>比如他为了更加通用，默认对代码的分割算法是 text 分割，也就是按照英文单词进行分割，因为有可能你的项目都不是代码，而是内部的纯文本知识库。</p><p>但是对于大量代码场景会导致分割的代码片段不连贯，比如一个完整的函数被分割成了多个 chunk，导致大模型理解困难。</p><p>而更好的方案应该是根据不同的编程语言选择不同的 AST（抽象语法树）进行分割</p><p>通用方案存在的问题：</p><ul><li>代码分割不够精准，需要使用指定语言的 AST（抽象语法树）进行分割。</li><li>目录生成太随意，应该根据项目的特征（前端后端、python、Java、Golang）生成目录树。</li></ul><p>这是没有经过优化的目录结构，非常通用（重新生成之后就会变化），适合放到任何项目里面。</p><p>而相对应的是针对我们项目背景优化的目录结构；这样一样便知道好坏。</p><p>至于是如何优化的？那又回到了刚才的提到的：可以确定的内容就不要交给 AI 生成。</p><p>这里的目录结构我们是确定的，无论怎么重复生成都是这个目录机构，对于我们的 Java 项目来说，我们希望目录结构可以根据对外提供的 grpc 生成，这样就可以系统的结构化的来理解整个项目。</p><p>至于目录结构是如何生成的，那也很简单，就是解析下 grpc 的 proto 文件，提取出接口声明，然后再遍历生成目录就可以了。</p><p>同样的道理，对于前端项目、或者是 Python 你项目，他们的关注点都不太相同，我们需要针对性的优化，不然就只能得到一个可用的玩具。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321003759.png"></p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321004033.png"></p><p>我的经验：</p><ul><li>任何可以确认的内容，尽量直接告诉大模型，而不是让他进行推理</li><li>大模型应用要做定制优化，通用的方案只能作为玩具。</li></ul><h2 id="Vibe-Coding-实战经验分享"><a href="#Vibe-Coding-实战经验分享" class="headerlink" title="Vibe Coding 实战经验分享"></a>Vibe Coding 实战经验分享</h2><p>接下来要分享的是我最近 Vibe Coding 的一些经验</p><p>首先是工具的推荐，我的工作流是好用的基座大模型+ Code Agent（Claude Code&#x2F;copolit-cli) + Skills</p><p>好的大模型可以节约很多时间，推荐有条件的尽量使用 Claude 的 opus4.6</p><p>而 Code Agent 可以直接帮我们编写 编译 调试代码，直接做事情，再也不需要手动复制聊天窗口里的代码到 IDE 里面再手动运行了。</p><p>Skill 可以将自己常用的工作流整理成一个可复用的模块，也可以使用行业大佬整理的 Skill，相当于偷师了各种大佬的最佳实践。</p><p>以上三者结合起来真的可以将个人打造成以往的产研小团队。</p><p>我的常用工具：</p><ul><li>Claude Opus4.6 &#x2F; Gemini3</li><li>Claude Code &#x2F; Copilot-CLI</li><li>Skill：偷师行业大佬经验，或者复用自己已有的工作流</li></ul><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321011733.png"></p><p>刚才的那个本地 <a href="https://github.com/crossoverJie/SkillDeck">Skill</a> 管理工具就是我用 Claude Code vibe Coding 开发出来的，在 github 上也有一定关注度，也证明纯靠 vibe Coding 也是可以解决问题的。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321012148.png"></p><p>下面是这段时间总结的一些 VibeCoding 经验：<br>开发一个具体 feature 的时候尽量聚焦一些，每次都使用独立的 context 以及 分支开发，这样不容易出现幻觉</p><p>完成一个功能之后记得提交代码，这样更方便回滚，尽量不要用自然语言让 AI 进行回滚，而是自己操作 git 进行回滚。</p><p>这又和刚才提到的，能确定的事情尽量别让 AI 去做。</p><p>AI coding 过程中，自己消耗大量 token 总结出来的内容，如果暂时用不上可以让它整理成一个文档，方便自己下次直接加载这份文档恢复上下文，避免再次分析消耗 token。</p><p>或者直接使用 claude –resume 恢复上下文</p><p>定期使用 &#x2F;init 命令构建 claude.md，使得 Claude 的上下文更加准确</p><p>对于自己不熟的代码和领域，多让大模型写日志，方便他后续排查问题</p><p>在 claude.md 里声明单测优先，任何 PR 的创建都需要通过单测，尽可能的保证代码质量</p><p>多利用项目记忆以及全局记忆来约束大模型的行为，比如代码风格、输出的标准等。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321013124.png"></p><p>经验总结：</p><ul><li>一个 feature 一个 context，避免上下文过长</li><li>一个 feature 一个分支，尽量使用 git 命令回滚，而不是让 AI 进行回滚（容易出错，能确定的事情就尽量确定）</li><li>保存好消耗大量 token 总结的文档（复杂功能的代码分析），后续可以复用，也可以使用 claude resume 恢复之前的会话</li><li>定期使用 <code>/init</code> 命令初始化 claude.md</li><li>关键逻辑让大模型多写日志，方便后续让他排查问题</li><li>在 claude.md 里声明单测优先（TDD），任何 PR 的创建都需要通过单测，尽可能的保证代码质量</li><li>多利用项目记忆以及全局记忆来约束大模型的行为，比如代码风格、输出的标准等。</li></ul><h3 id="独立开发工具链"><a href="#独立开发工具链" class="headerlink" title="独立开发工具链"></a>独立开发工具链</h3><ul><li>Gemini 生成产品 PRD 文档</li><li>将 PRD 导入 <a href="https://stitch.withgoogle.com/">https://stitch.withgoogle.com/</a> &amp; pencil.dev 生成 UI 设计稿</li><li>将 UI 设计稿导出成 html，或者是 MCP 到 Figma</li><li>由 Claude Code 读取设计稿实现页面</li><li>app logo 使用 superdesign MCP 生成 SVG</li></ul><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321103656.png"><br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321103941.png"></p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>正如 anthropic CEO 在之前的播客里提到：未来 software engineer 这个 title 真的会消失，取而代之的可能是 product manager 或者叫做 builder。</p><p>放到前两年我是不太信的，这几个月我充分使用 AI Coding 之后我信了，对大部分开发者来说既是挑战也是机遇。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321020731.png"><br><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260321020817.png"></p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260324160917.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;上周末参加了一个我们重庆本地的一个 AI 分享会，分享了一些企业大模型与 Vibe Coding 的实战经验，现在把它整理成一篇 blog。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="LLM" scheme="http://crossoverjie.top/tags/LLM/"/>
    
    <category term="VibeCoding" scheme="http://crossoverjie.top/tags/VibeCoding/"/>
    
  </entry>
  
  <entry>
    <title>DeepWiki 优化实战：代码行号与确定性目录生成</title>
    <link href="http://crossoverjie.top/2026/03/17/AI/deepwiki-optimize-line-number/"/>
    <id>http://crossoverjie.top/2026/03/17/AI/deepwiki-optimize-line-number/</id>
    <published>2026-03-17T18:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近在用 <a href="https://github.com/AsyncFuncAI/deepwiki-open/">deepwiki-open</a> 给内部的 Java 项目生成 wiki，发现一个很明显的问题：<strong>生成的 wiki 页面里引用的代码行号经常不准确</strong>，看起来是 LLM 根据上下文自己推算的。</p><p>比如一个函数明明在第 503 行，生成出来的 wiki 里可能标注成第 510 行甚至更离谱的数字。</p><p>之前我在 <a href="https://crossoverjie.top/2025/12/25/AI/deepwiki-rag-principle/">DeepWiki 一个常用 RAG 应用的开发流程</a> 里分析过它的整体流程，本文主要聊聊我们在实际使用中遇到的两个问题以及对应的优化方案。</p><span id="more"></span><h1 id="问题分析：LLM-为什么算不对行号"><a href="#问题分析：LLM-为什么算不对行号" class="headerlink" title="问题分析：LLM 为什么算不对行号"></a>问题分析：LLM 为什么算不对行号</h1><p>在我们的优化版本中，已经使用 <a href="https://crossoverjie.top/2026/01/14/AI/splitter-Algorithm-analyse/">tree-sitter</a> 基于 AST 将代码进行拆分存入了本地的向量数据库。</p><p>第一版存储的 chunk 格式是这样的：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">file</span> <span class="hljs-attr">path</span>=<span class="hljs-string">&quot;src/main/java/com/example/Client.java&quot;</span>&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">chunk</span> <span class="hljs-attr">start_line</span>=<span class="hljs-string">&quot;503&quot;</span> <span class="hljs-attr">end_line</span>=<span class="hljs-string">&quot;581&quot;</span>&gt;</span><br>package com.example;<br><br>import cn.hutool.core.util.ZipUtil;<br>import com.google.common.collect.Lists;<br>import com.google.protobuf.ByteString;<br>// ... 后面的代码<br><span class="hljs-tag">&lt;/<span class="hljs-name">chunk</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">file</span>&gt;</span><br></code></pre></td></tr></table></figure><p>看起来我们已经告诉 LLM 这段代码从第 503 行开始到第 581 行结束了，但问题在于：<strong>chunk 内部的代码是原始文本，没有行号标记</strong>。</p><p>当 LLM 需要引用某个具体函数的行号时，它必须从 <code>start_line=503</code> 开始，自己数第几行是哪个函数。</p><p>这对 LLM 来说太难了——众所周知 LLM 不擅长数学计算，让它去数几十行代码然后算出 <code>503 + 偏移量 = 实际行号</code>，幻觉就不可避免了。</p><blockquote><p>这就好比你让一个文科生做加法题还不让用计算器，虽然能算但准确率堪忧。</p></blockquote><h1 id="优化一：给代码加上行号前缀"><a href="#优化一：给代码加上行号前缀" class="headerlink" title="优化一：给代码加上行号前缀"></a>优化一：给代码加上行号前缀</h1><p>既然 LLM 算不准，那最好的办法就是<strong>直接把结果给它</strong>，让它只需要”读”而不需要”算”。</p><h2 id="改动思路"><a href="#改动思路" class="headerlink" title="改动思路"></a>改动思路</h2><p>核心改动很简单，在把 chunk 内容发给 LLM 之前，给每一行代码加上实际行号前缀：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">_add_line_numbers</span>(<span class="hljs-params">text: <span class="hljs-built_in">str</span>, start_line: <span class="hljs-built_in">int</span></span>) -&gt; <span class="hljs-built_in">str</span>:<br>    <span class="hljs-string">&quot;&quot;&quot;给代码文本的每一行添加行号前缀&quot;&quot;&quot;</span><br>    <span class="hljs-keyword">return</span> <span class="hljs-string">&#x27;\n&#x27;</span>.join(<br>        <span class="hljs-string">f&quot;<span class="hljs-subst">&#123;start_line + i&#125;</span>. <span class="hljs-subst">&#123;line&#125;</span>&quot;</span><br>        <span class="hljs-keyword">for</span> i, line <span class="hljs-keyword">in</span> <span class="hljs-built_in">enumerate</span>(text.split(<span class="hljs-string">&#x27;\n&#x27;</span>))<br>    )<br></code></pre></td></tr></table></figure><h2 id="优化前后对比"><a href="#优化前后对比" class="headerlink" title="优化前后对比"></a>优化前后对比</h2><table><thead><tr><th></th><th>优化前</th><th>优化后</th></tr></thead><tbody><tr><td>chunk 内容</td><td>原始代码文本</td><td>每行带行号前缀</td></tr><tr><td>LLM 行为</td><td>从 start_line 推算偏移量</td><td>直接读取行号</td></tr><tr><td>准确率</td><td>经常偏差 5-20 行</td><td>基本准确</td></tr></tbody></table><p>优化前发给 LLM 的数据：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">chunk</span> <span class="hljs-attr">start_line</span>=<span class="hljs-string">&quot;503&quot;</span> <span class="hljs-attr">end_line</span>=<span class="hljs-string">&quot;581&quot;</span>&gt;</span><br>package com.example;<br><br>import cn.hutool.core.util.ZipUtil;<br>import com.google.common.collect.Lists;<br><span class="hljs-tag">&lt;/<span class="hljs-name">chunk</span>&gt;</span><br></code></pre></td></tr></table></figure><p>优化后发给 LLM 的数据：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag">&lt;<span class="hljs-name">chunk</span> <span class="hljs-attr">start_line</span>=<span class="hljs-string">&quot;1&quot;</span> <span class="hljs-attr">end_line</span>=<span class="hljs-string">&quot;44&quot;</span>&gt;</span><br>2.<br>3. import cn.hutool.core.util.ZipUtil;<br>4. import com.google.common.collect.Lists;<br>5. import com.google.protobuf.ByteString;<br><span class="hljs-tag">&lt;/<span class="hljs-name">chunk</span>&gt;</span><br></code></pre></td></tr></table></figure><p>LLM 现在要引用 <code>ZipUtil</code> 的导入行，直接看到前缀 <code>3.</code> 就知道是第 3 行，不需要做任何计算。</p><h2 id="具体改动文件"><a href="#具体改动文件" class="headerlink" title="具体改动文件"></a>具体改动文件</h2><p>一共改了 4 个文件：</p><p><strong>1. <code>api/websocket_wiki.py</code></strong> — 添加 <code>_add_line_numbers()</code> 工具函数，在构建 chunk 内容时加上行号前缀：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">if</span> start_line <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span> <span class="hljs-keyword">and</span> end_line <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:<br>    numbered_text = _add_line_numbers(doc.text, start_line)<br>    doc_parts.append(<br>        <span class="hljs-string">f&#x27;&lt;chunk start_line=&quot;<span class="hljs-subst">&#123;start_line&#125;</span>&quot; end_line=&quot;<span class="hljs-subst">&#123;end_line&#125;</span>&quot;&gt;\n<span class="hljs-subst">&#123;numbered_text&#125;</span>\n&lt;/chunk&gt;&#x27;</span><br>    )<br></code></pre></td></tr></table></figure><p><strong>2. <code>api/websocket_wiki.py</code> 的 prompt 部分</strong> — 更新 <code>&lt;line_number_rules&gt;</code> 指令，告诉 LLM 直接读取行号前缀而不是自己计算：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-string">&quot;&lt;line_number_rules&gt;&quot;</span><br><span class="hljs-string">&quot;Each line in the code context is prefixed with its actual line number (e.g., &#x27;100. code here&#x27;). &quot;</span><br><span class="hljs-string">&quot;When citing source lines, read the line numbers directly from these prefixes. &quot;</span><br><span class="hljs-string">&quot;Do not count or calculate line numbers yourself. &quot;</span><br><span class="hljs-string">&quot;&lt;/line_number_rules&gt;&quot;</span><br></code></pre></td></tr></table></figure><h2 id="Token-成本"><a href="#Token-成本" class="headerlink" title="Token 成本"></a>Token 成本</h2><p>每行增加约 4-6 个字符（比如 <code>503. </code>），一个典型的 chunk 30-40 行，大概增加 150 字符。10-20 个 chunks 总共增加约 1500-3000 字符（约 500-1000 tokens），成本基本可以忽略。</p><h1 id="优化二：基于-Proto-文件生成确定性目录"><a href="#优化二：基于-Proto-文件生成确定性目录" class="headerlink" title="优化二：基于 Proto 文件生成确定性目录"></a>优化二：基于 Proto 文件生成确定性目录</h1><p>第二个问题是 wiki 的<strong>目录结构</strong>。</p><p>DeepWiki 默认的做法是把 repo 的目录树和 README 丢给 LLM，让它自由发挥来生成 wiki 目录（虽然有一些限制提示词，比如输出目录结构的大概要求）。这在通用场景下是合理的，但对我们的内部 Java 项目来说效果不好。</p><p>原因很简单：我们所有的业务都是围绕着 <strong>gRPC 接口</strong>来的，理想的 wiki 目录应该是按 Service 和 RPC 方法来组织的，而不是让 AI 自由发挥出一堆”Architecture Overview”、”Getting Started” 之类的通用章节。</p><h2 id="改动思路-1"><a href="#改动思路-1" class="headerlink" title="改动思路"></a>改动思路</h2><p>写代码读取 repo 里所有的 <code>*.proto</code> 文件，解析出所有的 Service 和 RPC 接口列表，然后直接构建出确定性的目录结构给前端，绕过 LLM 的目录生成步骤。</p><p>具体流程：</p><ol><li>扫描 repo 里所有 <code>.proto</code> 文件</li><li>用正则解析出 <code>package</code>、<code>service</code>、<code>rpc</code> 定义</li><li>构建固定格式的 <code>WikiStructure</code> JSON</li><li>前端检测到 proto 文件存在时，调用这个接口替代 LLM 生成</li></ol><h2 id="核心代码：proto-parser-py"><a href="#核心代码：proto-parser-py" class="headerlink" title="核心代码：proto_parser.py"></a>核心代码：proto_parser.py</h2><p>新增了一个 <code>api/proto_parser.py</code> 文件，主要做三件事：</p><p><strong>扫描 proto 文件：</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">find_proto_files</span>(<span class="hljs-params">repo_path: <span class="hljs-built_in">str</span>, excluded_dirs=<span class="hljs-literal">None</span></span>) -&gt; <span class="hljs-type">List</span>[<span class="hljs-built_in">str</span>]:<br>    <span class="hljs-string">&quot;&quot;&quot;遍历 repo 目录，返回所有 .proto 文件路径&quot;&quot;&quot;</span><br>    skip = <span class="hljs-built_in">set</span>(DEFAULT_EXCLUDED_DIRS)  <span class="hljs-comment"># 排除 vendor、node_modules 等</span><br>    proto_files = []<br>    <span class="hljs-keyword">for</span> root, dirs, files <span class="hljs-keyword">in</span> os.walk(repo_path):<br>        dirs[:] = [d <span class="hljs-keyword">for</span> d <span class="hljs-keyword">in</span> dirs <span class="hljs-keyword">if</span> d <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> skip]<br>        <span class="hljs-keyword">for</span> f <span class="hljs-keyword">in</span> files:<br>            <span class="hljs-keyword">if</span> f.endswith(<span class="hljs-string">&quot;.proto&quot;</span>):<br>                proto_files.append(os.path.join(root, f))<br>    proto_files.sort()<br>    <span class="hljs-keyword">return</span> proto_files<br></code></pre></td></tr></table></figure><p><strong>解析 proto 内容：</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">_RE_PACKAGE = re.<span class="hljs-built_in">compile</span>(<span class="hljs-string">r&quot;package\s+([\w.]+)\s*;&quot;</span>)<br>_RE_RPC = re.<span class="hljs-built_in">compile</span>(<br>    <span class="hljs-string">r&quot;rpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s*returns\s*\(\s*(stream\s+)?(\w+)\s*\)&quot;</span>,<br>)<br></code></pre></td></tr></table></figure><p>通过平衡大括号匹配来提取 service block，再用正则提取每个 RPC 方法的签名，包括方法名、请求类型、响应类型以及是否是 streaming。</p><p><strong>构建 wiki 目录结构：</strong></p><p>生成的目录包含 3 个固定章节 + 每个 Service 一个独立章节：</p><table><thead><tr><th>章节</th><th>内容</th></tr></thead><tbody><tr><td>Overview</td><td>项目总览</td></tr><tr><td>System Architecture</td><td>系统架构</td></tr><tr><td>Core Features</td><td>gRPC 接口汇总</td></tr><tr><td>{ServiceName} Service</td><td>每个 RPC 方法一个子页面</td></tr></tbody></table><p>每个 RPC 方法的页面标题直接用方法签名，比如 <code>GetOrder(GetOrderRequest) returns (GetOrderResponse)</code>，非常清晰。</p><h2 id="前端改动：page-tsx"><a href="#前端改动：page-tsx" class="headerlink" title="前端改动：page.tsx"></a>前端改动：page.tsx</h2><p>在 <code>src/app/[owner]/[repo]/page.tsx</code> 里新增了一个检测逻辑：</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-comment">// 检测 repo 是否包含 proto 文件</span><br><span class="hljs-comment">// 如果有，调用 /api/proto/wiki_structure 获取确定性目录</span><br><span class="hljs-comment">// 如果失败，fallback 到原来的 LLM 生成方式</span><br></code></pre></td></tr></table></figure><p>前端的核心逻辑是：</p><ol><li>先尝试调用 proto 解析接口获取确定性目录</li><li>如果 proto 接口返回了有效结构，直接使用（跳过 LLM 目录生成）</li><li>并发生成每个页面的具体内容（最多 5 个并行请求）</li><li>如果 proto 接口失败，fallback 到原来的 LLM 生成流程</li></ol><blockquote><p>这样做的好处是：目录结构 100% 准确，不会出现 LLM 瞎编目录的情况，同时还省了一次 LLM 调用的成本。</p></blockquote><h1 id="确定性-vs-不确定性：什么该交给-AI"><a href="#确定性-vs-不确定性：什么该交给-AI" class="headerlink" title="确定性 vs 不确定性：什么该交给 AI"></a>确定性 vs 不确定性：什么该交给 AI</h1><p>这两个优化背后其实是同一个思路：<strong>把确定的东西明确告诉 AI，不确定的才让 AI 来发挥</strong>。</p><table><thead><tr><th>类型</th><th>内容</th><th>处理方式</th></tr></thead><tbody><tr><td>确定的</td><td>代码行号</td><td>直接给 LLM 标注好</td></tr><tr><td>确定的</td><td>gRPC 接口列表、目录结构</td><td>代码解析，不经过 LLM</td></tr><tr><td>不确定的</td><td>函数功能解释</td><td>交给 LLM 归纳</td></tr><tr><td>不确定的</td><td>项目架构分析</td><td>交给 LLM 总结</td></tr><tr><td>不确定的</td><td>代码关联关系</td><td>交给 LLM 推理</td></tr></tbody></table><p>LLM 非常擅长理解、归纳和总结，但不擅长精确计算和结构化数据的生成。把它们分开处理，各取所长，效果就好很多了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这篇文章分享了我们在基于 DeepWiki 做内部项目 wiki 生成时的两个优化：</p><ol><li><strong>行号前缀</strong>：给代码 chunk 的每一行加上实际行号，让 LLM 直接读取而不是自己推算，token 成本几乎可以忽略但准确率大幅提升。</li><li><strong>确定性目录生成</strong>：通过代码解析 proto 文件直接构建目录结构，绕过 LLM 的自由发挥，保证目录 100% 准确。</li></ol><p>核心经验就一句话：<strong>需要定制自己的项目，尽量不要用通用的方案</strong>，不然就只是可用，但不精通。类似于现在的 <a href="https://crossoverjie.top/2026/03/11/AI/skilldeck-openclaw-support/">OpenClaw</a>，通用方案大家都能用，但真正好用的一定是针对你自己场景深度优化的。</p><p>对于确定的内容要明确告知 AI，不要让它自行发挥去推理，特别是和逻辑计算相关的，不然幻觉很严重。而不确定的、需要归纳总结的主观内容，则非常适合交给 AI 来输出。</p><p>#Blog</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近在用 &lt;a href=&quot;https://github.com/AsyncFuncAI/deepwiki-open/&quot;&gt;deepwiki-open&lt;/a&gt; 给内部的 Java 项目生成 wiki，发现一个很明显的问题：&lt;strong&gt;生成的 wiki 页面里引用的代码行号经常不准确&lt;/strong&gt;，看起来是 LLM 根据上下文自己推算的。&lt;/p&gt;
&lt;p&gt;比如一个函数明明在第 503 行，生成出来的 wiki 里可能标注成第 510 行甚至更离谱的数字。&lt;/p&gt;
&lt;p&gt;之前我在 &lt;a href=&quot;https://crossoverjie.top/2025/12/25/AI/deepwiki-rag-principle/&quot;&gt;DeepWiki 一个常用 RAG 应用的开发流程&lt;/a&gt; 里分析过它的整体流程，本文主要聊聊我们在实际使用中遇到的两个问题以及对应的优化方案。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="DeepWiki" scheme="http://crossoverjie.top/tags/DeepWiki/"/>
    
    <category term="RAG" scheme="http://crossoverjie.top/tags/RAG/"/>
    
  </entry>
  
  <entry>
    <title>SkillDeck 支持 OpenClaw 了，顺便聊聊小龙虾</title>
    <link href="http://crossoverjie.top/2026/03/11/AI/skilldeck-openclaw-support/"/>
    <id>http://crossoverjie.top/2026/03/11/AI/skilldeck-openclaw-support/</id>
    <published>2026-03-11T22:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.956Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近 OpenClaw 突然爆火，我的 <a href="https://github.com/crossoverJie/SkillDeck">SkillDeck</a> 也乘热打铁支持了 OpenClaw 的 Skills 管理和 ClawHub 市场浏览安装功能。</p><p>这篇文章一方面介绍下 SkillDeck <a href="https://github.com/crossoverJie/SkillDeck/releases/tag/v0.0.14">的更新内容</a>，另一方面也聊聊我对 OpenClaw 这波热度的一些看法。</p><p>安装命令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">brew tap crossoverJie/skilldeck &amp;&amp; brew install --cask skilldeck<br></code></pre></td></tr></table></figure><span id="more"></span><h1 id="更新日志"><a href="#更新日志" class="headerlink" title="更新日志"></a>更新日志</h1><h2 id="支持更多-Agent"><a href="#支持更多-Agent" class="headerlink" title="支持更多 Agent"></a>支持更多 Agent</h2><p>SkillDeck 现在一共支持了 <strong>10 个</strong> AI coding agent，新增了以下几个：</p><table><thead><tr><th>Agent</th><th>Skills 目录</th><th>检测方式</th></tr></thead><tbody><tr><td>Antigravity</td><td><code>~/.gemini/antigravity/skills/</code></td><td>antigravity 二进制</td></tr><tr><td>Cursor</td><td><code>~/.cursor/skills/</code></td><td>cursor 二进制</td></tr><tr><td>Kiro</td><td><code>~/.kiro/skills/</code></td><td>kiro 二进制</td></tr><tr><td>CodeBuddy</td><td><code>~/.codebuddy/skills/</code></td><td>codebuddy 二进制</td></tr><tr><td>OpenClaw</td><td><code>~/.openclaw/skills/</code></td><td>openclaw 二进制</td></tr></tbody></table><p>加上之前就支持的 Claude Code、Codex、Gemini CLI、Copilot CLI、OpenCode，完整列表可以看：<a href="https://github.com/crossoverJie/SkillDeck#supported-agents">Supported Agents</a></p><!-- 截图占位：SkillDeck 多 Agent 侧边栏截图 --><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260311110446.png"></p><h2 id="支持-ClawHub-市场"><a href="#支持-ClawHub-市场" class="headerlink" title="支持 ClawHub 市场"></a>支持 ClawHub 市场</h2><p>这次比较大的更新是集成了 <a href="https://github.com/crossoverJie/SkillDeck/pull/27">ClawHub</a> 市场，可以直接在 SkillDeck 里浏览、搜索、安装 ClawHub 上的 Skills，不需要再手动 clone 或者用命令行装了。</p><p>主要功能：</p><ul><li>侧边栏新增 ClawHub 入口，支持浏览和搜索</li><li>支持排序和筛选</li><li>一键安装到 OpenClaw 的 Skills 目录</li><li>安装时自动创建 symlink，不依赖 <code>clawhub</code> CLI</li></ul><!-- 截图占位：ClawHub 市场浏览页截图 --><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260311110402.png"></p><blockquote><p>ClawHub 有 API 频率限制，如果触发了限流会自动降级为仅安装 SKILL.md 文件的模式，不影响基本使用。</p></blockquote><h1 id="OpenClaw-爆火背后的"><a href="#OpenClaw-爆火背后的" class="headerlink" title="OpenClaw 爆火背后的"></a>OpenClaw 爆火背后的</h1><p>OpenClaw 最近的热度确实夸张，但仔细想想，这波热度背后的推手其实挺有意思的：</p><ol><li><strong>自媒体传播焦虑</strong>：每个科技自媒体都怕错过热点，一窝蜂涌上来输出「你还不知道 OpenClaw？」「不会用 OpenClaw 你就要被淘汰了」这类内容，焦虑感拉满。</li><li><strong>AI 公司需要卖 token</strong>：OpenClaw 本质上是个疯狂消耗 token 的应用，每一次操作都在调大模型，AI 公司巴不得你 24 小时挂着跑。</li><li><strong>云厂商卖服务器</strong>：OpenClaw 需要部署、需要算力，云厂商的推广文章比谁都积极。</li><li><strong>甚至苹果还要卖 Mac mini</strong>：当然这是臆想，但你看各种「用 Mac mini 搭建 OpenClaw 私有部署」的教程确实很多。</li></ol><p>网友们也是有才，发了不少梗图：</p><!-- 梗图占位 --><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260311110541.png"></p><!-- 梗图占位 --><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260311110554.png"></p><h1 id="我理解的-OpenClaw"><a href="#我理解的-OpenClaw" class="headerlink" title="我理解的 OpenClaw"></a>我理解的 OpenClaw</h1><p>其实 OpenClaw 并不是什么全新的东西，之前昙花一现的豆包手机电脑版就是类似的思路——帮你自动化操作各种 app，本质上还是想解决人类「懒」的问题。</p><p>比如：</p><ul><li>每天自动帮我在某个 app 签到</li><li>帮我每天写日报</li><li>自动查询某些网站帮我做信息汇总</li></ul><p>这些说白了就是自动化功能，再结合 AI 可以理解我们模糊的语义，让你不用写精确的代码也能完成任务。并不是什么新奇的东西。</p><p>但其实很多人压根不知道自己装一个 openclaw 可以解决什么问题，大部分都是带着锤子找钉子，想着这么强一个工具我先装上，后面说不定就能拿来赚钱。</p><p>有这类需求的大概率在 openclaw 出现之前就自己写工具或者借助第三方实现了，如果等到现在都没有，那大概率你是没这个需求的。</p><p>目前为止 openclaw 最大的问题还是<strong>权限过大</strong>。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images6512d75ea640e5596579e05b0c0c2d41.png"></p><blockquote><p>远程让 openclaw 拍照</p></blockquote><p>现在网上已经有很多安全问题的案例了，比如很多人开了公网端口导致自己的电脑直接裸奔，任何人只要拿到了入口就可以让 OpenClaw 把电脑里的文件发出来，甚至拍照、录像都可以。</p><p>所以理想的方案还是之前苹果想做的那一套：各个 app 之间通过标准的 AI 可以识别的接口协议进行通信，而不是现在通过模拟点击、执行 shell 命令来绕过所有的权限校验。</p><p>只是这条路会影响这些 app 厂商的商业逻辑——用户不需要再打开他的 app 就可以进行消费，采集不到用户数据、用户也看不了广告等。</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260311135859.png"></p><p>昨天还流行了一下午的<code>抽象提示词攻击</code>；在微信电脑版上根本不能直接发红包，也不能输入密码。</p><p>至于未来如何这还需要大厂之间继续进行博弈。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>我们不要过度关注现在 AI 带来的热度，就像去年初爆火的 DeepSeek，在现在养龙虾的热潮下已经没多少人讨论了；我认为到明年小龙虾也没啥人会继续讨论。</p><p>我们还是需要透过现象看本质，小龙虾背后依然是一个 AI Agent，和我们现在使用的 Claude Code、Codex 这些没有本质区别；只是它接入了很多 IM 通道，让普通人通过一个聊天窗口就可以指挥大模型去做很多具体的事情，让这个门槛看似降低了很多。</p><blockquote><p>现在 Claude Code 已经更新了 loop 模式和 cron 模式，已经可以执行许多循环任务和主动执行任务，和 OpenClaw 更没什么区别了。</p></blockquote><p>而且 OpenClaw 的代码复杂，接入的渠道很多，我们都知道代码量越多系统越复杂理论上出 bug 的几率也越大；所以之前也流行自己去裁剪自己的小龙虾，去掉一些不需要的渠道和代码。</p><p>归根结底，工具是拿来用的，不是拿来追的。与其焦虑「我是不是又错过了什么」，不如想想自己到底需要解决什么问题，再选合适的工具。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近 OpenClaw 突然爆火，我的 &lt;a href=&quot;https://github.com/crossoverJie/SkillDeck&quot;&gt;SkillDeck&lt;/a&gt; 也乘热打铁支持了 OpenClaw 的 Skills 管理和 ClawHub 市场浏览安装功能。&lt;/p&gt;
&lt;p&gt;这篇文章一方面介绍下 SkillDeck &lt;a href=&quot;https://github.com/crossoverJie/SkillDeck/releases/tag/v0.0.14&quot;&gt;的更新内容&lt;/a&gt;，另一方面也聊聊我对 OpenClaw 这波热度的一些看法。&lt;/p&gt;
&lt;p&gt;安装命令：&lt;/p&gt;
&lt;figure class=&quot;highlight shell&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;hljs shell&quot;&gt;brew tap crossoverJie/skilldeck &amp;amp;&amp;amp; brew install --cask skilldeck&lt;br&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Skills" scheme="http://crossoverjie.top/tags/Skills/"/>
    
    <category term="OpenClaw" scheme="http://crossoverjie.top/tags/OpenClaw/"/>
    
    <category term="SkillDeck" scheme="http://crossoverjie.top/tags/SkillDeck/"/>
    
  </entry>
  
  <entry>
    <title>全程用 Claude Code 搓了一个 macOS 原生应用：SkillDeck</title>
    <link href="http://crossoverjie.top/2026/02/24/AI/skilldeck-intro/"/>
    <id>http://crossoverjie.top/2026/02/24/AI/skilldeck-intro/</id>
    <published>2026-02-24T23:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近在同时用多个 AI coding agent的过程中，Skills 管理起来比较麻烦，</p><p>我日常在 Claude Code、Codex、Copilot CLI 之间切换，每个 Agent 的 Skills 存放在不同的目录下（<code>~/.claude/skills/</code>、<code>~/.agents/skills/</code>、<code>~/.gemini/skills/</code>、<code>~/.copilot/skills/</code>），安装一个 Skill 的流程大概是这样的：</p><ol><li>找到 Skill 的 GitHub 仓库</li><li><code>git clone</code> 到本地</li><li>手动创建 symlink 到对应 Agent 的 Skills 目录</li><li>如果要装到多个 Agent，以上步骤重复 N 遍</li></ol><p>卸载的时候也一样繁琐：删目录、清 symlink，漏了哪步就会留下残留。</p><p>当然也可以用命令行工具安装：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npx skills add https://github.com/github/awesome-copilot --skill git-commit<br></code></pre></td></tr></table></figure><p>但这也只是解决了安装的问题，对所有 Agent 的 Skills 缺乏统一的可视化管理——装了哪些 Skill、哪些有更新、哪些该删掉，全靠自己记。</p><p>所以作为一个写过多年后端，但完全没碰过 Swift 和前端的人，我决定用 Claude Code 全程手搓一个 macOS 原生桌面应用来解决这个问题——这就是 <a href="https://github.com/crossoverJie/SkillDeck">SkillDeck</a>。它不仅提供安装能力，还提供了统一的发现、更新、删除等全生命周期管理。</p><span id="more"></span><h1 id="核心功能"><a href="#核心功能" class="headerlink" title="核心功能"></a>核心功能</h1><h2 id="统一仪表盘"><a href="#统一仪表盘" class="headerlink" title="统一仪表盘"></a>统一仪表盘</h2><p>三栏布局的 macOS 原生界面：左边是 Agent 列表和筛选，中间是 Skill 列表，右边是详情。支持按名称、描述、作者搜索，还能按 Agent 过滤和排序。</p><blockquote><p>symlink 去重是一个比较实用的设计——同一个 Skill 通过 symlink 安装到多个 Agent 时，只会显示一次，不会在列表里看到重复项。</p></blockquote><p><img src="https://jsd.cdn.zzko.cn/gh/crossoverJie/images@main/images/images20260213123118.png" alt="Dashboard Overview"></p><h2 id="Skills-市场浏览"><a href="#Skills-市场浏览" class="headerlink" title="Skills 市场浏览"></a>Skills 市场浏览</h2><p>内置了 <a href="https://skills.sh/">skills.sh</a> 的排行榜浏览，支持 All Time、Trending、Hot 三种排序方式，还有搜索功能。看到喜欢的 Skill 可以直接一键安装，不用再手动 clone 了。</p><p><img src="https://jsd.cdn.zzko.cn/gh/crossoverJie/images@main/images/images20260224114425.png" alt="Registry Browser"></p><h2 id="安装与更新"><a href="#安装与更新" class="headerlink" title="安装与更新"></a>安装与更新</h2><p>从 GitHub 安装只需要输入仓库地址（支持 <code>owner/repo</code> 格式），SkillDeck 会自动 clone、扫描可用 Skills、创建 symlink、更新 lock 文件。</p><p>更新检测也是一键的：会对比本地和远程的 tree hash，有变更就显示橙色角标，点一下就能拉取最新代码。</p><p><img src="https://jsd.cdn.zzko.cn/gh/crossoverJie/images@main/images/images20260213122805.png" alt="Install &amp; Update"></p><h2 id="SKILL-md-编辑器"><a href="#SKILL-md-编辑器" class="headerlink" title="SKILL.md 编辑器"></a>SKILL.md 编辑器</h2><p>分栏设计：左边是表单 + Markdown 编辑区，右边是实时预览。改完 <code>Cmd+S</code> 保存，<code>Esc</code> 取消。</p><blockquote><p>这个功能用的少，但也聊胜于无。</p></blockquote><h2 id="Agent-分配"><a href="#Agent-分配" class="headerlink" title="Agent 分配"></a>Agent 分配</h2><p>每个 Skill 的详情页有一组 toggle 开关，控制这个 Skill 安装到哪些 Agent。打开就自动创建 symlink，关掉就自动删除，不用再手动跑命令了。</p><p>这样也不用每个 Agent 都去安装 skill，只保留一份。</p><h2 id="文件系统监听"><a href="#文件系统监听" class="headerlink" title="文件系统监听"></a>文件系统监听</h2><p>SkillDeck 会自动监听 Skills 目录的变化，所以如果你从 CLI 侧做了什么操作（比如用 <code>claude skills add</code> 安装了新 Skill），GUI 这边会自动刷新，不需要手动点刷新按钮。</p><hr><p>目前支持的 Agent 和对应的 Skills 目录：</p><table><thead><tr><th>Agent</th><th>Skills 目录</th><th>检测方式</th></tr></thead><tbody><tr><td>Claude Code</td><td><code>~/.claude/skills/</code></td><td><code>claude</code> 二进制 + <code>~/.claude/</code> 目录</td></tr><tr><td>Codex</td><td><code>~/.agents/skills/</code>（共享）</td><td><code>codex</code> 二进制</td></tr><tr><td>Gemini CLI</td><td><code>~/.gemini/skills/</code></td><td><code>gemini</code> 二进制 + <code>~/.gemini/</code> 目录</td></tr><tr><td>Copilot CLI</td><td><code>~/.copilot/skills/</code></td><td><code>gh</code> 二进制</td></tr></tbody></table><h1 id="开发过程"><a href="#开发过程" class="headerlink" title="开发过程"></a>开发过程</h1><p>整个项目从第一行代码到现在，全程都是用 Claude Code 开发的。</p><p>我自己的技术背景是 Java&#x2F;Go&#x2F;Python，Swift 之前一行都没写过，SwiftUI 和 macOS 平台开发更是零经验。但这次的体验让我感触很深——<strong>AI Coding 真的把跨语言开发的门槛拉低了很多</strong>。</p><p>开发节奏基本上就是一个循环：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs">提需求 → AI 实现 → 我测试 → 发现问题 → AI 修复 → 再测试<br></code></pre></td></tr></table></figure><p>跟之前<a href="https://crossoverjie.top/2026/02/07/AI/create-skills/">用 AI 搓 Skills</a> 的流程差不多，但这次的复杂度高了不少——毕竟是一个完整的 macOS 桌面应用，涉及 UI 布局、文件系统操作、网络请求、并发处理等等。</p><p>我不需要先花几周系统学习 Swift 和 SwiftUI，遇到不懂的语法或 API 直接问 AI 就行。当然，这不代表可以完全当甩手掌柜——你得能看懂代码逻辑、能写清楚需求、能有效测试和反馈问题，AI 才能帮你持续推进。</p><blockquote><p>说白了就是：你不需要会写 Swift，但你得会”验收”Swift 代码。能跑起来、功能正确、边界情况覆盖到，这些判断能力还是需要你自己具备的。</p></blockquote><h1 id="AI-Coding-小-Tips"><a href="#AI-Coding-小-Tips" class="headerlink" title="AI Coding 小 Tips"></a>AI Coding 小 Tips</h1><p>这段时间用 Claude Code 开发积累了一些经验，分享几个我觉得比较实用的 tips。</p><h2 id="1-每个功能新开一个-context"><a href="#1-每个功能新开一个-context" class="headerlink" title="1. 每个功能新开一个 context"></a>1. 每个功能新开一个 context</h2><p>不要在一个超长的对话里做所有事情。每个功能开一个新的 context 会更聚焦，AI 不容易被之前的上下文带偏。</p><p>完成一个功能后记得 commit，这样如果 AI 后续改错了什么，你可以很方便地回滚到之前的状态。</p><blockquote><p>尽量不要使用 AI 来回滚，不然会有不好的事情发生，血的教训。<br>精确的回滚还是交给靠谱的 git 工具来实现。</p></blockquote><h2 id="2-大量-token-总结的内容保存成文档"><a href="#2-大量-token-总结的内容保存成文档" class="headerlink" title="2. 大量 token 总结的内容保存成文档"></a>2. 大量 token 总结的内容保存成文档</h2><p>有时候让 AI 做了一大堆分析（比如梳理项目架构、分析某个复杂模块的实现），这些内容当下可能用不上，但后面很可能会再用到。</p><p>我的做法是让 AI 把分析结果整理成文档保存到项目的 memory 目录，下次开新 context 的时候直接加载这个文档，不用重新消耗 token 再分析一遍。</p><h2 id="3-claude-resume-恢复历史会话"><a href="#3-claude-resume-恢复历史会话" class="headerlink" title="3. claude --resume 恢复历史会话"></a>3. <code>claude --resume</code> 恢复历史会话</h2><p>如果你中途关掉了某个对话，后面又想继续，可以用 <code>--resume</code> 恢复：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs bash">claude --resume <span class="hljs-string">&quot;hotkey&quot;</span><br>claude --resume <span class="hljs-string">&quot;架构&quot;</span><br></code></pre></td></tr></table></figure><p>它会搜索历史 session 的内容，列出匹配的会话让你选择。不过搜索不是百分百精准，有时候需要换几个关键词试试。</p><h2 id="4-Session-保留策略"><a href="#4-Session-保留策略" class="headerlink" title="4. Session 保留策略"></a>4. Session 保留策略</h2><p>Claude Code 默认 30 天自动清理历史 session，可以在 <code>~/.claude/settings.json</code> 里修改保留时间：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;cleanupPeriodDays&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">90</span> <span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>所以 <code>--resume</code> 只适合短期内继续某个对话，不适合当作长期知识存储。长期需要保留的内容还是整理成文档更靠谱。</p><p>我建议还是使用刚才的方案，将你觉得消耗 token 的结论存储到专门的文档里，后期你需要使用的时候直接加载即可。</p><p>而不需要存放到 Claude Code 的系统提示词里，这样可能会浪费 token。</p><h2 id="5-用-CLAUDE-md-约束-AI-的开发规范"><a href="#5-用-CLAUDE-md-约束-AI-的开发规范" class="headerlink" title="5. 用 CLAUDE.md 约束 AI 的开发规范"></a>5. 用 CLAUDE.md 约束 AI 的开发规范</h2><p>这个我觉得是最重要的一条。</p><p>把开发规范写进 <code>CLAUDE.md</code>，AI 每次开新对话都会自动加载这些规则，就像给团队新人定 code review 规范一样。我在项目里定了这些规则：</p><ul><li><strong>Git 工作流</strong>：代码改动必须新建分支，禁止直接提交到 main</li><li><strong>测试要求</strong>：每次代码修改都应包含对应的单元测试</li><li><strong>提交确认</strong>：AI 不能自动 commit&#x2F;push，必须等我确认</li><li><strong>PR 规范</strong>：每个 PR 必须包含「Manual Verification Required」（人工验证清单）和「Regression Checklist」（回归测试清单）</li></ul><blockquote><p>这里有一个关键区分：<strong>每个项目都通用的规则</strong>（比如分支策略、测试要求），可以放到 <code>~/.claude/CLAUDE.md</code>（全局配置），所有项目自动生效，不用每个项目重复写。<strong>项目特有的规范</strong>才放到项目根目录的 <code>CLAUDE.md</code> 里。</p></blockquote><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>SkillDeck 解决的核心痛点就一个：<strong>让多个 AI Agent 的 Skills 管理更直观易用</strong>。从安装、更新、分配到删除，全部在一个 GUI 里搞定。</p><p>全程用 Claude Code 开发这个项目的感受是：跨语言开发的门槛被 AI 大幅拉低了。我一行 Swift 都不会写，但靠着 AI 辅助，从零产出了一个完整的 macOS 原生应用。当然前提是你得有基本的软件工程能力——需求拆解、测试验证、问题排查这些还是得自己来。</p><p>项目开源，MIT 协议，欢迎 star&#x2F;issue&#x2F;PR：<a href="https://github.com/crossoverJie/SkillDeck">GitHub</a> | <a href="https://crossoverjie.top/SkillDeck/">项目主页</a></p><p>安装方式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">brew tap crossoverJie/skilldeck &amp;&amp; brew install --cask skilldeck<br></code></pre></td></tr></table></figure><p>#Blog</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近在同时用多个 AI coding agent的过程中，Skills 管理起来比较麻烦，&lt;/p&gt;
&lt;p&gt;我日常在 Claude Code、Codex、Copilot CLI 之间切换，每个 Agent 的 Skills 存放在不同的目录下（&lt;code&gt;~/.claude/skills/&lt;/code&gt;、&lt;code&gt;~/.agents/skills/&lt;/code&gt;、&lt;code&gt;~/.gemini/skills/&lt;/code&gt;、&lt;code&gt;~/.copilot/skills/&lt;/code&gt;），安装一个 Skill 的流程大概是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到 Skill 的 GitHub 仓库&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git clone&lt;/code&gt; 到本地&lt;/li&gt;
&lt;li&gt;手动创建 symlink 到对应 Agent 的 Skills 目录&lt;/li&gt;
&lt;li&gt;如果要装到多个 Agent，以上步骤重复 N 遍&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;卸载的时候也一样繁琐：删目录、清 symlink，漏了哪步就会留下残留。&lt;/p&gt;
&lt;p&gt;当然也可以用命令行工具安装：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;hljs bash&quot;&gt;npx skills add https://github.com/github/awesome-copilot --skill git-commit&lt;br&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;但这也只是解决了安装的问题，对所有 Agent 的 Skills 缺乏统一的可视化管理——装了哪些 Skill、哪些有更新、哪些该删掉，全靠自己记。&lt;/p&gt;
&lt;p&gt;所以作为一个写过多年后端，但完全没碰过 Swift 和前端的人，我决定用 Claude Code 全程手搓一个 macOS 原生桌面应用来解决这个问题——这就是 &lt;a href=&quot;https://github.com/crossoverJie/SkillDeck&quot;&gt;SkillDeck&lt;/a&gt;。它不仅提供安装能力，还提供了统一的发现、更新、删除等全生命周期管理。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Claude" scheme="http://crossoverjie.top/tags/Claude/"/>
    
    <category term="Skills" scheme="http://crossoverjie.top/tags/Skills/"/>
    
    <category term="macOS" scheme="http://crossoverjie.top/tags/macOS/"/>
    
    <category term="SwiftUI" scheme="http://crossoverjie.top/tags/SwiftUI/"/>
    
  </entry>
  
  <entry>
    <title>别再傻等了，给 Claude Code 装个通知铃铛</title>
    <link href="http://crossoverjie.top/2026/02/09/AI/agent-notifier-skill/"/>
    <id>http://crossoverjie.top/2026/02/09/AI/agent-notifier-skill/</id>
    <published>2026-02-09T14:00:00.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近用 Claude Code、Copilot CLI 这类 AI Agent 工具的时候，有一个挺烦人的问题：让 AI 在后台跑任务，我总是会忍不住去查看他的执行状态，有时候比较复杂的任务可能会耗时十来分钟，每次来回切换非常浪费时间。</p><p>更惨的是有时候 AI 需要我授权某个操作（比如执行 shell 命令），我没注意到，它就一直卡在那里等。</p><p>所以我一直想找一个靠谱的通知方案。</p><p>灵感来源于播客「<a href="https://justinyan.me/post/6623">枫言枫语</a>」，主播自力提到可以用 Hook 来实现 Agent 通知。</p><p>不过一开始我偷了个懒，让 AI 自己给方案。AI 给出的方案很”AI”：在 <code>~/.claude/CLAUDE.md</code> 里加一段系统提示词，指示 LLM 任务完成后用 <code>afplay</code> 播放一个提示音。</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs markdown"><span class="hljs-section">## Task Completion Sound</span><br>When you complete a task, play a sound:<br>afplay /System/Library/Sounds/Glass.aiff<br></code></pre></td></tr></table></figure><p>测试了几次发现这玩意不靠谱——有时候响，有时候不响，完全看 LLM 心情。</p><p>最终我还是回到了 Hook 方案，用各平台的 Hooks 系统实现确定性触发，并封装成了一个可复用的 <a href="https://github.com/crossoverJie/skills">SKILL</a>。</p><p>最终的效果如下：</p><p><img src="https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260210161908.png"></p><span id="more"></span><h1 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h1><p>为什么 LLM 提示词方案不靠谱？主要三个原因：</p><ol><li><strong>LLM 不会 100% 遵循附带操作类指令</strong>：LLM 对”生成文本”以外的操作指令（比如”运行 bash 命令”）本来就不太可靠，它可能觉得当前场景”不需要”播放声音就跳过了</li><li><strong>上下文压缩会丢失指令</strong>：长对话中，系统会自动压缩上下文，提示词的优先级会被降低甚至直接丢掉</li><li><strong>LLM 对触发时机的判定不一致</strong>：什么算”任务完成”？LLM 每次的理解可能都不一样，导致触发行为不稳定</li></ol><p>本质上，这是一个”软提示” vs “硬触发”的问题。用提示词去控制 LLM 行为，就像是”拜托你帮我做一件事”；而用 Hooks 就是”当这个事件发生时，自动执行这段代码”——确定性完全不同。</p><table><thead><tr><th>对比项</th><th>提示词方案</th><th>Hooks 方案</th></tr></thead><tbody><tr><td>触发可靠性</td><td>不确定，取决于 LLM 判断</td><td>确定性 100% 触发</td></tr><tr><td>上下文影响</td><td>长对话会被压缩丢失</td><td>不受上下文影响</td></tr><tr><td>配置方式</td><td>Markdown 文本</td><td>JSON 配置 + 脚本</td></tr><tr><td>可扩展性</td><td>基本不可扩展</td><td>支持多平台、多渠道</td></tr><tr><td>维护成本</td><td>每次换模型可能要调提示词</td><td>一次配置，持续生效</td></tr></tbody></table><p>有点类似于现在的 LLM 和 Agent 的区别，Agent 是干活的，大模型是负责思考的。</p><p>确定的事情还是要交给确定的 Agent 去做。</p><h1 id="agent-notifier-介绍"><a href="#agent-notifier-介绍" class="headerlink" title="agent-notifier 介绍"></a>agent-notifier 介绍</h1><p>基于以上分析，我开发了 <a href="https://github.com/crossoverJie/skills/tree/main/skills/agent-notifier">agent-notifier</a> 这个 SKILL，用 Hooks 实现确定性通知。</p><h2 id="功能概览"><a href="#功能概览" class="headerlink" title="功能概览"></a>功能概览</h2><p>支持的 AI Agent 平台：</p><table><thead><tr><th>平台</th><th>Hook 机制</th><th>触发事件</th></tr></thead><tbody><tr><td>Claude Code</td><td>settings.json hooks</td><td><code>Notification</code>（idle_prompt, permission_prompt）</td></tr><tr><td>GitHub Copilot CLI</td><td>hooks.json</td><td><code>sessionEnd</code>, <code>postToolUse</code></td></tr><tr><td>Cursor</td><td>hooks.json</td><td><code>stop</code>, <code>afterFileEdit</code></td></tr><tr><td>Codex（OpenAI）</td><td>notify setting</td><td><code>agent-turn-complete</code></td></tr><tr><td>Aider</td><td>CLI flag</td><td><code>--notifications-command</code></td></tr></tbody></table><p>支持的通知渠道：</p><table><thead><tr><th>渠道</th><th>默认状态</th><th>说明</th></tr></thead><tbody><tr><td>Sound</td><td>启用</td><td>macOS 用 <code>afplay</code>，Linux 用 <code>paplay</code>&#x2F;<code>aplay</code></td></tr><tr><td>macOS 通知中心</td><td>启用</td><td>通过 <code>osascript</code> 弹出系统通知</td></tr><tr><td>Telegram</td><td>禁用</td><td>需要 Bot Token + Chat ID</td></tr><tr><td>Email</td><td>禁用</td><td>SMTP 发送</td></tr><tr><td>Slack</td><td>禁用</td><td>Incoming Webhook</td></tr><tr><td>Discord</td><td>禁用</td><td>Webhook URL</td></tr></tbody></table><h2 id="架构设计"><a href="#架构设计" class="headerlink" title="架构设计"></a>架构设计</h2><p>核心思路是<strong>统一事件模型 + 并发多渠道分发</strong>：</p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs pgsql">各平台 Hook 触发<br>       ↓<br>  stdin <span class="hljs-type">JSON</span> 输入（各平台格式不同）<br>       ↓<br>  <span class="hljs-keyword">notify</span>.py 解析为统一事件：&#123;platform, event, message&#125;<br>       ↓<br>  读取 <span class="hljs-keyword">notify</span>-config.json 配置<br>       ↓<br>  ThreadPoolExecutor 并发分发到所有启用的渠道<br></code></pre></td></tr></table></figure><p>每个平台传过来的 JSON 格式不一样，比如 Claude Code 是 <code>&#123;&quot;notification_type&quot;: &quot;idle_prompt&quot;, ...&#125;</code>，Copilot CLI 是 <code>&#123;&quot;hook_event_name&quot;: &quot;sessionEnd&quot;, ...&#125;</code>。<code>notify.py</code> 会把这些不同的格式统一解析成 <code>&#123;platform, event, message&#125;</code> 三元组，然后根据配置分发到各个通知渠道。</p><blockquote><p>一个关键设计：单个渠道发送失败不影响其他渠道。比如 Telegram 网络超时了，Sound 和 macOS 通知该响还是响。错误信息只输出到 stderr，不会中断流程。</p></blockquote><h2 id="核心设计决策：纯标准库、零依赖"><a href="#核心设计决策：纯标准库、零依赖" class="headerlink" title="核心设计决策：纯标准库、零依赖"></a>核心设计决策：纯标准库、零依赖</h2><p>整个 <code>notify.py</code> 只用了 Python 标准库，没有任何 <code>pip</code> 依赖：</p><ul><li>HTTP 请求用 <code>urllib.request</code>（发 Telegram、Slack、Discord）</li><li>邮件用 <code>smtplib</code></li><li>播放声音用 <code>subprocess</code> 调系统命令</li><li>并发用 <code>concurrent.futures</code></li></ul><p>这意味着只要机器上有 Python，拿来就能用，不需要 <code>pip install</code> 任何东西。</p><h1 id="开发过程"><a href="#开发过程" class="headerlink" title="开发过程"></a>开发过程</h1><p>整个 SKILL 的开发也是和 AI 对话完成的，下面分阶段回顾。</p><h2 id="阶段一：核心通知脚本-notify-py"><a href="#阶段一：核心通知脚本-notify-py" class="headerlink" title="阶段一：核心通知脚本 notify.py"></a>阶段一：核心通知脚本 notify.py</h2><p>这是最核心的部分，负责三件事：</p><ol><li><strong>解析输入</strong>：从 stdin 读取各平台传过来的 JSON，识别平台类型和事件</li><li><strong>统一事件模型</strong>：不管哪个平台，统一解析为 <code>&#123;platform, event, message&#125;</code></li><li><strong>多渠道发送</strong>：并发调用所有启用的通知渠道</li></ol><p>比如 Claude Code 的 Hook 会通过 stdin 传入：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;notification_type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;idle_prompt&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Claude is waiting for your input&quot;</span><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>脚本解析后生成通知：**”✅ Task completed — waiting for your input”**，然后同时发到 Sound、macOS 通知中心、Telegram 等所有启用的渠道。</p><h2 id="阶段二：配置与安装"><a href="#阶段二：配置与安装" class="headerlink" title="阶段二：配置与安装"></a>阶段二：配置与安装</h2><p>光有核心脚本还不够，还需要让用户能方便地配置和安装。所以又搞了两个文件：</p><p><strong>notify-config.json</strong>：配置模板，定义了所有渠道的开关和参数。默认只启用 Sound 和 macOS 通知，Telegram、Email 这些需要手动启用并填入凭据。</p><p><strong>setup.py</strong>：交互式安装脚本，运行后会：</p><ol><li>自动检测你装了哪些 AI Agent 平台</li><li>引导你配置通知渠道（要不要 Telegram？Bot Token 是什么？）</li><li>自动在对应平台写入 Hook 配置</li><li>发一条测试通知验证配置</li></ol><h2 id="阶段三：集成测试"><a href="#阶段三：集成测试" class="headerlink" title="阶段三：集成测试"></a>阶段三：集成测试</h2><p>代码写完了，关键是跑起来验证。</p><p>首先在 Claude Code 的 <code>~/.claude/settings.json</code> 里配置 Hook：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;hooks&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;Notification&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>      <span class="hljs-punctuation">&#123;</span><br>        <span class="hljs-attr">&quot;matcher&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-attr">&quot;hooks&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>          <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;command&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;python3 ~/.claude/skills/agent-notifier/notify.py&quot;</span><br>          <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">]</span><br>      <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">]</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>然后手动测试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 模拟任务完成通知</span><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;&#123;&quot;notification_type&quot;:&quot;idle_prompt&quot;,&quot;message&quot;:&quot;test&quot;&#125;&#x27;</span> | python3 notify.py<br><br><span class="hljs-comment"># 模拟权限请求通知</span><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;&#123;&quot;notification_type&quot;:&quot;permission_prompt&quot;,&quot;message&quot;:&quot;needs permission&quot;&#125;&#x27;</span> | python3 notify.py<br></code></pre></td></tr></table></figure><p>Sound 和 macOS 通知都正常。接着启用 Telegram，配好 Bot Token 和 Chat ID，再跑一次——Telegram 也收到了消息。</p><p>最后让 Claude Code 执行一个真实任务，然后等它跑完。果然，任务结束后 Telegram 弹出通知，Sound 也响了，搞定。</p><h2 id="阶段四：修-bug-改文案"><a href="#阶段四：修-bug-改文案" class="headerlink" title="阶段四：修 bug 改文案"></a>阶段四：修 bug 改文案</h2><p>实际使用中发现一个问题：<code>idle_prompt</code> 的通知消息是 “Claude is waiting for your input”，但这个消息不够直观——我更想知道的是”任务完成了”，而不是”在等你输入”。</p><p>虽然本质上 <code>idle_prompt</code> 就是任务完成后等待输入的信号，但消息文案会影响用户感知。于是改成了：</p><ul><li><code>idle_prompt</code> → <strong>“✅ Task completed — waiting for your input”</strong></li><li><code>permission_prompt</code> → <strong>“🔐 Permission required”</strong></li></ul><p>改完之后再测，Telegram 消息一目了然，不用再猜它到底是什么状态了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这次开发最核心的观点就一句话：<strong>Hooks &gt; 提示词</strong>。</p><p>凡是需要确定性执行的操作，都不应该用提示词去”请求”LLM 来做，而是应该用平台提供的 Hook 机制来保证。提示词适合控制生成内容的风格和方向，但不适合控制”是否执行某个操作”这类二元决策。</p><p>另外，对话式开发的体验依然很好。从最初的想法到最终可用的 SKILL，整个过程就是不断对话、测试、修复的循环。像 Telegram 消息文案不够直观这种问题，也是在实测中才发现的。</p><p>感兴趣的可以去 <a href="https://github.com/crossoverJie/skills">GitHub 仓库</a> 看看源码，<code>agent-notifier</code> 在 <code>skills/agent-notifier/</code> 目录下。</p><p>#Blog</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近用 Claude Code、Copilot CLI 这类 AI Agent 工具的时候，有一个挺烦人的问题：让 AI 在后台跑任务，我总是会忍不住去查看他的执行状态，有时候比较复杂的任务可能会耗时十来分钟，每次来回切换非常浪费时间。&lt;/p&gt;
&lt;p&gt;更惨的是有时候 AI 需要我授权某个操作（比如执行 shell 命令），我没注意到，它就一直卡在那里等。&lt;/p&gt;
&lt;p&gt;所以我一直想找一个靠谱的通知方案。&lt;/p&gt;
&lt;p&gt;灵感来源于播客「&lt;a href=&quot;https://justinyan.me/post/6623&quot;&gt;枫言枫语&lt;/a&gt;」，主播自力提到可以用 Hook 来实现 Agent 通知。&lt;/p&gt;
&lt;p&gt;不过一开始我偷了个懒，让 AI 自己给方案。AI 给出的方案很”AI”：在 &lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt; 里加一段系统提示词，指示 LLM 任务完成后用 &lt;code&gt;afplay&lt;/code&gt; 播放一个提示音。&lt;/p&gt;
&lt;figure class=&quot;highlight markdown&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;code class=&quot;hljs markdown&quot;&gt;&lt;span class=&quot;hljs-section&quot;&gt;## Task Completion Sound&lt;/span&gt;&lt;br&gt;When you complete a task, play a sound:&lt;br&gt;afplay /System/Library/Sounds/Glass.aiff&lt;br&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;测试了几次发现这玩意不靠谱——有时候响，有时候不响，完全看 LLM 心情。&lt;/p&gt;
&lt;p&gt;最终我还是回到了 Hook 方案，用各平台的 Hooks 系统实现确定性触发，并封装成了一个可复用的 &lt;a href=&quot;https://github.com/crossoverJie/skills&quot;&gt;SKILL&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;最终的效果如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.jsdelivr.net/gh/crossoverJie/images@main/images/images20260210161908.png&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Claude" scheme="http://crossoverjie.top/tags/Claude/"/>
    
    <category term="Skills" scheme="http://crossoverjie.top/tags/Skills/"/>
    
    <category term="Hooks" scheme="http://crossoverjie.top/tags/Hooks/"/>
    
  </entry>
  
  <entry>
    <title>一行代码没写，用 AI 搓出三个实用 SKILLS</title>
    <link href="http://crossoverjie.top/2026/02/07/AI/create-skills/"/>
    <id>http://crossoverjie.top/2026/02/07/AI/create-skills/</id>
    <published>2026-02-07T17:56:51.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近上一篇文章里答应过要分享下我那三个 <a href="https://github.com/crossoverJie/skills">SKILLS</a> 的创建过程，乘热打铁赶紧写出来。</p><p>上篇提到过我写博客的一个痛点：每次写完文章都要手动找封面图 → 上传图床 → 粘贴链接，这套流程走下来虽然不复杂，但每次都要做一遍确实烦。</p><p>所以我就想着把这个流程自动化掉，而且全程一行代码没写，完全和 AI 对话搞定。</p><span id="more"></span><h1 id="Skills-介绍"><a href="#Skills-介绍" class="headerlink" title="Skills 介绍"></a>Skills 介绍</h1><p>先整体介绍一下最终产出的三个 SKILLS：</p><table><thead><tr><th>Skill 名称</th><th>用途</th><th>关键特性</th></tr></thead><tbody><tr><td>image-uploader</td><td>上传图片到图床</td><td>支持 sm.ms，抽象基类方便扩展，多来源 token 配置</td></tr><tr><td>cover-generator</td><td>生成渐变封面图</td><td>基于 Pillow，4 种主题，支持中文，可选自动上传</td></tr><tr><td>auto-blog-cover</td><td>端到端博客配图</td><td>解析 Markdown → 提取标题 → 生成封面 → 上传 → 更新 frontmatter</td></tr></tbody></table><p>三个 SKILLS 之间存在依赖关系：</p><figure class="highlight arduino"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs arduino"><span class="hljs-keyword">auto</span>-blog-cover → cover-generator → image-uploader<br></code></pre></td></tr></table></figure><p><code>auto-blog-cover</code> 是最上层的入口，调用 <code>cover-generator</code> 生成图片，<code>cover-generator</code> 再调用 <code>image-uploader</code> 上传到图床。最终我只需要跑一条命令，整个流程就搞定了。</p><blockquote><p>这里分成三个 skill 的好处是：更好的分层可以帮助其他用户选择合适自己的 skill，比如有些人可能只需要一个上传图片的 skill 而已。</p></blockquote><p>计算机经典架构之一：遇事不决先分层😊。</p><h2 id="image-uploader"><a href="#image-uploader" class="headerlink" title="image-uploader"></a><a href="https://github.com/crossoverJie/skills/tree/main/skills/image-uploader">image-uploader</a></h2><p>这是最底层的基础 SKILL，负责把本地图片上传到图床，目前支持 <code>sm.ms</code>。</p><p>设计上用了抽象基类 <code>BaseUploader</code>，后期想接入腾讯云、阿里云这些只需要新增一个实现类就行：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">BaseUploader</span>(<span class="hljs-title class_ inherited__">ABC</span>):<br><span class="hljs-meta">    @abstractmethod</span><br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">upload</span>(<span class="hljs-params">self, image_path</span>):<br>        <span class="hljs-keyword">pass</span><br><br><span class="hljs-keyword">class</span> <span class="hljs-title class_">SmMsUploader</span>(<span class="hljs-title class_ inherited__">BaseUploader</span>):<br>    API_URL = <span class="hljs-string">&quot;https://sm.ms/api/v2/upload&quot;</span><br>    <span class="hljs-comment"># ...</span><br></code></pre></td></tr></table></figure><p>token 配置支持三种方式，优先级从高到低：</p><ol><li>命令行参数 <code>--token</code></li><li>环境变量 <code>SMMS_TOKEN</code></li><li>配置文件 <code>config.json</code></li></ol><blockquote><p>这个优先级设计是在对话中讨论出来的，一开始只有命令行参数，后来考虑到分享给其他人使用的场景才加上了环境变量和配置文件。</p></blockquote><h2 id="cover-generator"><a href="#cover-generator" class="headerlink" title="cover-generator"></a><a href="https://github.com/crossoverJie/skills/tree/main/skills/cover-generator">cover-generator</a></h2><p>这个 SKILL 用 Pillow 生成渐变风格的封面图（1200x630），支持四种主题：</p><table><thead><tr><th>主题</th><th>效果</th></tr></thead><tbody><tr><td>random</td><td>随机渐变色</td></tr><tr><td>dark</td><td>深色系</td></tr><tr><td>light</td><td>浅色系</td></tr><tr><td>blue</td><td>蓝紫渐变</td></tr></tbody></table><p>核心就是生成一个渐变背景，然后把标题和副标题居中渲染上去。加了 <code>--upload</code> 参数可以生成后直接调用 <code>image-uploader</code> 上传。</p><p>上传完成后会自动清理本地临时文件，上传失败还会自动重试最多 3 次。</p><h2 id="auto-blog-cover"><a href="#auto-blog-cover" class="headerlink" title="auto-blog-cover"></a><a href="https://github.com/crossoverJie/skills/tree/main/skills/auto-blog-cover">auto-blog-cover</a></h2><p>这是最终面向用户的 SKILL，把整个工作流串起来：</p><ol><li>读取 Markdown 文件，解析 frontmatter</li><li>提取标题（支持手动传入覆盖）</li><li>调用 <code>cover-generator</code> 生成封面并上传</li><li>用正则替换更新 frontmatter 中的 <code>banner_img</code> 和 <code>index_img</code></li></ol><blockquote><p>这里用正则替换而不是直接用 <code>python-frontmatter</code> 库回写，是因为后者会重排 YAML 字段的顺序，导致我博客的 frontmatter 格式被打乱。</p></blockquote><p>使用起来很简单：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 全自动模式</span><br>python3 skills/auto-blog-cover/auto_blog_cover.py /path/to/blog.md<br><br><span class="hljs-comment"># 手动指定标题</span><br>python3 skills/auto-blog-cover/auto_blog_cover.py blog.md --title <span class="hljs-string">&quot;AI Evolution&quot;</span> --subtitle <span class="hljs-string">&quot;From Function Call to MCP&quot;</span><br></code></pre></td></tr></table></figure><h1 id="创建过程"><a href="#创建过程" class="headerlink" title="创建过程"></a>创建过程</h1><p>整个创建过程就是和 AI 不断对话、迭代出来的，下面分阶段回顾下。</p><h2 id="阶段一：从-image-uploader-开始"><a href="#阶段一：从-image-uploader-开始" class="headerlink" title="阶段一：从 image-uploader 开始"></a>阶段一：从 image-uploader 开始</h2><p>一开始我的需求很简单：我需要一个上传图片到 sm.ms 的工具。</p><p>我给 AI 提供了 sm.ms 的<a href="https://doc.sm.ms/#api-Image-Upload">接口文档</a>，然后让它帮我实现。AI 先问了我几个问题：用什么语言？做成什么形式？我选了 Python 脚本 + 独立 CLI 工具。</p><p>然后就是一个很有意思的讨论——token 怎么传递。</p><p>AI 一开始的方案是通过命令行参数传入：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">python skills/image_uploader.py image.png --token YOUR_TOKEN<br></code></pre></td></tr></table></figure><p>我提了一个问题：”token 如果从命令行中获取是否方便其他人使用这个 SKILLS？”</p><p>这一下就打开了思路，于是补充了环境变量和配置文件两种方式，形成了三级优先级的配置体系。</p><p>跑起来测试的时候，AI 直接从我的 <code>.zshrc</code> 里找到了 <code>SMMS_TOKEN</code> 环境变量（之前配好的），上传了一张壁纸验证通过。</p><h2 id="阶段二：cover-generator-的诞生"><a href="#阶段二：cover-generator-的诞生" class="headerlink" title="阶段二：cover-generator 的诞生"></a>阶段二：cover-generator 的诞生</h2><p>接着我提了第二个需求：我想给博客文章生成封面图。</p><p>这里我不想直接调用类似于  <code>Nano Banana</code> 这里的专门文生图模型，就只需要一个简单背景+文字的图片即可；</p><p>所以 AI 给了我一个方案：算法生成艺术图 vs 文字+背景，本地用 Pillow 就能生成。</p><p>生成的图片效果还不错，简洁的渐变背景加上标题文字，虽然比不上专业设计，但作为博客封面还是够用了。</p><p>后来我又提了几个优化：</p><ul><li>图片生成后要自动清理本地文件（不占存储）</li><li>上传失败要能重试</li></ul><p>AI 都逐一实现了，加了 retry 逻辑和 <code>finally</code> 块里的清理代码。</p><h2 id="阶段三：auto-blog-cover-串联一切"><a href="#阶段三：auto-blog-cover-串联一切" class="headerlink" title="阶段三：auto-blog-cover 串联一切"></a>阶段三：auto-blog-cover 串联一切</h2><p>前两个 SKILLS 搞定后，我描述了一下我的实际工作流：</p><blockquote><p>我会在 Obsidian 里写博客，写完之后打开 CLI，让它读取博客内容，调用 cover-generator 生成封面并上传，然后把图片地址更新到博客的 frontmatter 里。</p></blockquote><p>AI 认为这个流程完全可以自动化，建议我再创建一个 SKILL 来串联。我觉得很有道理，一个独立的 SKILL 也方便其他有类似需求的人使用。</p><p>这中间有两个值得一提的坑：</p><p><strong>中文乱码问题</strong>：第一次跑 auto-blog-cover 时，生成的封面图里中文全是乱码。原因是 Pillow 默认字体不支持中文。AI 把字体改成了 <code>STHeiti Light</code>（macOS 系统自带的中文字体），同时加了 Linux 和 Windows 的字体回退列表。</p><p><strong>YAML 字段排序问题</strong>：一开始用 <code>python-frontmatter</code> 库回写文件时，它会把我的 YAML 字段重新排序。比如原来是 <code>title → date → categories → tags → banner_img</code>，回写后变成了按字母排序的 <code>banner_img → categories → date → ...</code>。AI 改用正则表达式直接替换字段值，这样就不会动其他字段的顺序了。</p><p>修复这两个问题后再跑了一次，效果完美——中文正常显示，frontmatter 格式完好。</p><h2 id="迭代的节奏"><a href="#迭代的节奏" class="headerlink" title="迭代的节奏"></a>迭代的节奏</h2><p>回顾整个过程，基本上就是这样一个循环：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs">提需求 → AI 实现 → 我测试 → 发现问题 → AI 修复 → 再测试<br></code></pre></td></tr></table></figure><p>每一轮对话都在不断完善功能、补全边界情况。从最初的单个上传脚本，逐步演化出三个分层的 SKILLS，整个过程非常自然。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>这次体验下来，最大的感受是：<strong>把重复性的工作流固化成 SKILLS 真的很香</strong>。</p><p>以前每次写完博客要手动配图、上传、粘贴链接，虽然每次也就几分钟，但积少成多也挺烦的。现在一条命令搞定，而且整个创建过程我确实一行代码没写，全程和 AI 对话完成。</p><p>对话式开发的好处在于：你不需要事先想好所有细节，可以边做边想、边测边改。像 token 配置方案、中文字体、YAML 排序这些问题，都是在实际使用中发现并解决的。</p><p>感兴趣的可以去 <a href="https://github.com/crossoverJie/skills">GitHub 仓库</a> 看看源码，也欢迎提 issue 和 PR。</p><p>#Blog</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近上一篇文章里答应过要分享下我那三个 &lt;a href=&quot;https://github.com/crossoverJie/skills&quot;&gt;SKILLS&lt;/a&gt; 的创建过程，乘热打铁赶紧写出来。&lt;/p&gt;
&lt;p&gt;上篇提到过我写博客的一个痛点：每次写完文章都要手动找封面图 → 上传图床 → 粘贴链接，这套流程走下来虽然不复杂，但每次都要做一遍确实烦。&lt;/p&gt;
&lt;p&gt;所以我就想着把这个流程自动化掉，而且全程一行代码没写，完全和 AI 对话搞定。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="Claude" scheme="http://crossoverjie.top/tags/Claude/"/>
    
    <category term="Skills" scheme="http://crossoverjie.top/tags/Skills/"/>
    
  </entry>
  
  <entry>
    <title>从 Function Call 到 MCP-&gt; SKILLS：AI Agent 能力扩展的演进之路</title>
    <link href="http://crossoverjie.top/2026/02/03/AI/MCP-Skills-intro/"/>
    <id>http://crossoverjie.top/2026/02/03/AI/MCP-Skills-intro/</id>
    <published>2026-02-03T17:56:51.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近 Claude 的 SKILLS 很火，忍不住也来体验了一下，发现确实是有些东西的；但也发现身边的一些同事对这些新出的概念总是很懵逼，所以便有了这篇文章。</p><p>从最早的 Function Call，到 MCP 协议，再到如今的 Agent Skills。</p><p>本文将从技术演进的角度，带你理解这些概念之间的关系，以及它们如何让 AI 从一个”只会说话的聊天机器人”变成真正能”动手做事”的智能助手。</p><span id="more"></span><p>再开始之前还是要澄清下大模型和 Agent 的关系，今天刷到一个<a href="https://www.bilibili.com/video/BV1ojfDBSEPv/?share_source=copy_web&vd_source=358858ab808efe832b0dda9dbc4701da&t=13">视频</a>觉得讲的非常浅显易懂：</p><p>所谓智能体就是把非智能的部分整合在一起，也就是说大模型帮我们做模糊自然语言的理解与决策，然后然后交给 agent 去调用一些非智能化的能力，比如：</p><ul><li>把 word 转换成 PDF</li><li>编译运行代码</li><li>调用飞书的推送接口，把一些内容推送给你的机器人。<blockquote><p>这些能力可能是需要编码完成的，也可能是第三方提供的 API，不管是哪种都是一些确定的东西。</p></blockquote></li></ul><p>让大模型摆脱了只能在网页里做一个 chatbot，从而进化到可以真正干具体事情的能力（以往我们需要手动去复制大模型给的代码到本地进行编译运行，这些重复机械的步骤直接交给 agent 来运行）</p><p>比如现在流行的 claude code 他可以帮你修改代码，直接运行代码获取结果，充当你和大模型沟通的桥梁。</p><p>而最近大火的 <a href="https://github.com/openclaw/openclaw/">openclaw</a> 本质上也是一个 agent，只是相比于 claude code 多了 gui 界面，对接更多的工具（各种 IM），本质上他们没有任何区别。</p><h1 id="发展历史"><a href="#发展历史" class="headerlink" title="发展历史"></a>发展历史</h1><h2 id="Function-Call：让大模型学会”使用工具”"><a href="#Function-Call：让大模型学会”使用工具”" class="headerlink" title="Function Call：让大模型学会”使用工具”"></a>Function Call：让大模型学会”使用工具”</h2><p>在 Function Call 出现之前，大模型只能做一件事：<strong>生成文本</strong>。你问它天气，它只能根据训练数据猜测；你让它查数据库，它只能编造一个”看起来合理”的答案。</p><p>2023 年，OpenAI 发布了 <a href="https://platform.openai.com/docs/guides/function-calling">Function Calling</a> 功能，这是大模型能力扩展的第一个里程碑。</p><p><strong>核心思路</strong>：告诉大模型”你有哪些工具可以用”，当它判断需要使用工具时，输出一个结构化的 JSON 调用请求，由外部程序执行后再把结果返回给模型。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;get_weather&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;arguments&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;location&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;北京&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;unit&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;celsius&quot;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p><strong>局限性</strong>：</p><ul><li>每个应用都要自己定义和实现工具</li><li>工具之间没有统一标准，无法复用</li><li>工具定义需要全部放在 System Prompt 里，token 消耗大</li></ul><h2 id="MCP：建立统一的”工具接口标准”"><a href="#MCP：建立统一的”工具接口标准”" class="headerlink" title="MCP：建立统一的”工具接口标准”"></a>MCP：建立统一的”工具接口标准”</h2><p>2024 年，Anthropic 发布了 **<a href="https://www.anthropic.com/news/model-context-protocol">MCP (Model Context Protocol)</a>**，可以把它理解为 AI 工具的 RPC 协议。</p><p><strong>解决的核心问题</strong>：让不同开发者写的工具，AI 都能听得懂、用得上。</p><table><thead><tr><th>对比维度</th><th>Function Call</th><th>MCP</th></tr></thead><tbody><tr><td>定义方式</td><td>每个应用自己定义</td><td>统一协议标准</td></tr><tr><td>工具发现</td><td>静态配置</td><td>动态发现</td></tr><tr><td>生态复用</td><td>难以复用</td><td>一次开发，处处可用</td></tr><tr><td>跨模型支持</td><td>绑定特定模型</td><td>开放标准，多模型支持</td></tr></tbody></table><p><strong>MCP 的工作流程</strong>：</p><figure class="highlight scss"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs scss">MCP Client (Claude)                MCP Server (如数据库读取器)<br>     │                                  │<br>     │  <span class="hljs-number">1</span>. 连接并发送 list_tools        │<br>     │ ──────────────────────────────▶  │<br>     │                                  │<br>     │  <span class="hljs-number">2</span>. 返回工具列表                 │<br>     │  (query_db, search_files...)     │<br>     │ ◀──────────────────────────────  │<br>     │                                  │<br>     │  <span class="hljs-number">3</span>. 用户提问，Claude 决定调用    │<br>     │     call_tool: query_db          │<br>     │ ──────────────────────────────▶  │<br>     │                                  │ ← 执行 SQL 查询<br>     │  <span class="hljs-number">4</span>. 返回执行结果                 │<br>     │ ◀──────────────────────────────  │<br>     │                                  │<br>     ▼<br>Claude 整合结果，组织成回答<br></code></pre></td></tr></table></figure><h2 id="Agent-Skills：从”工具”到”技能包”"><a href="#Agent-Skills：从”工具”到”技能包”" class="headerlink" title="Agent Skills：从”工具”到”技能包”"></a>Agent Skills：从”工具”到”技能包”</h2><p>2025 年 10 月，Anthropic 发布了 **<a href="https://www.anthropic.com/news/skills">Agent Skills</a>**，这是在 MCP 基础上的进一步抽象。</p><p><strong>时间线</strong>：</p><table><thead><tr><th>时间</th><th>事件</th></tr></thead><tbody><tr><td>2025年10月9日</td><td><a href="https://www.anthropic.com/news/claude-code-plugins">Anthropic 发布 Plugins 系统</a></td></tr><tr><td>2025年10月16日</td><td><a href="https://www.anthropic.com/news/skills">Anthropic 发布 Agent Skills</a></td></tr><tr><td>2025年10月16日</td><td><a href="https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills">Agent Skills 作为开放标准发布</a></td></tr></tbody></table><p><strong>Skills 是什么</strong>：</p><blockquote><p>“Skills are organized folders of instructions, scripts, and resources that agents can discover and load dynamically to perform better at specific tasks.”</p></blockquote><p>可以把 Skills 理解为”分类后的系统提示词”，但它比传统的 System Prompt 更智能——<strong>按需加载，而不是全量加载</strong>。</p><table><thead><tr><th>维度</th><th>传统系统提示词</th><th>Agent Skills</th></tr></thead><tbody><tr><td>加载方式</td><td>全量加载：每次对话都要发一遍</td><td>按需调用：只加载需要的技能</td></tr><tr><td>Token 消耗</td><td>高：Prompt 长度随功能增多而爆炸</td><td>低：结合 Prompt Caching 降低成本</td></tr><tr><td>复杂度上限</td><td>低：Prompt 太长会”注意力失焦”</td><td>高：每个技能独立，互不干扰</td></tr><tr><td>执行能力</td><td>仅限”说话”</td><td>可关联 Tool Use，真正执行操作</td></tr></tbody></table><p><strong>Skills 的本质：提示词工程的进化</strong></p><p>说到底，Skills 的本质还是前几年流行的<strong>提示词工程（Prompt Engineering）</strong>。</p><p>回想一下 2023 年 ChatGPT 刚火的时候，网上到处都是”万能提示词模板”、”让 AI 效率翻倍的 prompt 技巧”。那时候大家都在研究怎么写出更好的 System Prompt，让 AI 扮演各种角色：翻译官、程序员、文案专家…</p><p>Skills 做的事情本质上没变——**还是在告诉 AI “你是谁、你能做什么、你应该怎么做”**。</p><p>说的更好理解一点：可以把自己日常的一些固定流程固化为一个 SKILLS，比如我写博客需要为一篇文章配一个封面图，我之前的流程是：</p><ul><li>写好文章后根据文章的内容想一个标题</li><li>根据这个标题去网上照一张合适的图</li><li>把图片上传到图床</li><li>然后把图床链接贴到博客的顶部</li></ul><p>这些流程其实都是机械化的毫无智能而言，但是每次做法都是一样的；所以我将这些流程写到一个 SKILL.md 文档里。</p><p>让 AI 给我总结文章标题、生成配图、上传图床、然后粘贴到文章顶部。</p><p>这样我写好文章后，只需要对 Claude Code&#x2F;Codex 这类 agent 说：把 &#x2F;xx&#x2F;xx&#x2F;blog.md 配图。</p><p>之后 AI 就会自动加载我在 <code>SKILL.md</code> 里定义的流程进行处理。</p><p>同理，我们日常工作中这些繁琐的流程都可以抽象为一个个的 <code>SKILL</code>，想想是不是可玩性非常强。</p><p>区别在于：</p><ul><li><strong>以前</strong>：把所有提示词塞进一个巨大的 System Prompt，不管用不用得上都要带着</li><li><strong>现在</strong>：把提示词拆分成独立的 Skill 文件，AI 自己判断什么时候需要加载哪个</li></ul><p>所以如果你之前积累了很多好用的提示词模板，现在可以直接把它们改造成 Skills——加上 frontmatter 元数据，放到 <code>~/.claude/skills/</code> 目录下，就能让 Claude 按需调用了。</p><h1 id="现状"><a href="#现状" class="headerlink" title="现状"></a>现状</h1><h2 id="Skills-与-MCP-的关系"><a href="#Skills-与-MCP-的关系" class="headerlink" title="Skills 与 MCP 的关系"></a>Skills 与 MCP 的关系</h2><p>用一个类比来说明：</p><ul><li><strong>MCP是 RPC 接口协议”</strong></li><li><strong>Skills 是”接口实现里的一个个具体的函数（函数的抽象级别需要定义好，不然维护性也不强）”</strong></li></ul><p>它们不是互相替代的关系，而是”协议”与”实现”的关系。一个 “GitHub Skill” 内部就是通过 MCP 协议去和 GitHub 服务器通讯的。</p><h2 id="Skills-的两阶段加载机制"><a href="#Skills-的两阶段加载机制" class="headerlink" title="Skills 的两阶段加载机制"></a>Skills 的两阶段加载机制</h2><p>这是 Skills 设计中很精妙的部分——<strong>不会导致 token 激增</strong>。</p><table><thead><tr><th>阶段</th><th>加载内容</th><th>Token 消耗</th></tr></thead><tbody><tr><td>启动时</td><td>只加载元数据（name + description）</td><td>~30-100 tokens&#x2F;skill</td></tr><tr><td>匹配后</td><td>加载完整 skill 内容</td><td>视 skill 大小而定</td></tr></tbody></table><p><strong>工作原理</strong>：</p><figure class="highlight sqf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sqf">用户请求 → Claude 匹配 <span class="hljs-built_in">skill</span> descriptions → 只注入相关 <span class="hljs-built_in">skill</span> 的完整内容<br></code></pre></td></tr></table></figure><p>以 Obsidian skills 为例：</p><ul><li><strong>启动时</strong>：只加载 <code>obsidian-markdown</code>、<code>obsidian-bases</code> 的 name 和 description（约 100-300 tokens）</li><li><strong>当你说</strong> “帮我创建一个 Obsidian 笔记”：才加载 <code>obsidian-markdown</code> 的完整内容</li><li><strong>如果不涉及 Obsidian</strong>：完整内容永远不会加载</li></ul><h2 id="匹配逻辑：完全由大模型决定"><a href="#匹配逻辑：完全由大模型决定" class="headerlink" title="匹配逻辑：完全由大模型决定"></a>匹配逻辑：完全由大模型决定</h2><p>这里我其实有一个问题：谁来判断是否需要加载某个 Skill？</p><p><strong>答案是：完全由大模型决定，不是客户端</strong>。</p><p>任何需要做模糊语义判断的地方都是大模型来处理、Agent 只做具体确定的事情。</p><figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs armasm"><span class="hljs-symbol">Claude</span> <span class="hljs-meta">Code</span> 客户端                    大模型<br>     │                                  │<br>     │  所有 skills 元数据              │<br>     │  (name + description)            │<br>     │ ──────────────────────────────▶  │<br>     │                                  │ ← 模型阅读、理解、判断<br>     │                                  │<br>     │  调用 Skill 工具                 │<br>     │ ◀──────────────────────────────  │<br>     │                                  │<br>     │  注入完整 SKILL.md 内容          │<br>     │ ──────────────────────────────▶  │<br></code></pre></td></tr></table></figure><p>客户端做的事：收集元数据、打包发送、执行工具调用。</p><p>客户端<strong>不做</strong>的事：没有关键词匹配、没有正则、没有向量嵌入、没有意图分类器。</p><blockquote><p>客户端做的越轻越能体现 AI 的特点，也跟通用。</p></blockquote><p>由于完全依赖模型判断，存在不可靠性。skills 有可能没有被自动激活，模型会直接跳过它们，这就是大模型的概率问题，如果确定要使用某个 SKILL，可以用一下方案：</p><ol><li>直接使用 &#x2F;skill-name 强制使用</li><li>关键规则放在 <strong>CLAUDE.md</strong> 中（始终在上下文里）</li><li>设置 <code>disable-model-invocation: true</code> 改为手动调用</li></ol><h2 id="Skills-的安装方式"><a href="#Skills-的安装方式" class="headerlink" title="Skills 的安装方式"></a>Skills 的安装方式</h2><p>目前有两种安装方式：</p><table><thead><tr><th>特性</th><th><code>/plugin</code> 命令安装</th><th>手动复制到 <code>~/.claude/skills</code></th></tr></thead><tbody><tr><td>版本追踪</td><td>有</td><td>无</td></tr><tr><td>自动更新</td><td><code>/plugin update</code></td><td>手动</td></tr><tr><td>来源记录</td><td>有</td><td>无</td></tr><tr><td>适用场景</td><td>第三方&#x2F;远程 skill</td><td>本地开发&#x2F;简单使用</td></tr><tr><td>开放标准</td><td>Claude Code 专属（但一些其他 agent 也兼容读取了 cc 的目录来实现兼容性）</td><td>Agent Skills 标准实现</td></tr></tbody></table><p><code>~/.claude/skills</code> 目录是 Agent Skills 开放标准的本地实现。Agent Skills 已发布为开放标准（<a href="https://agentskills.io/">agentskills.io</a>），不仅 Claude Code 支持，OpenAI Codex CLI 等其他工具也可以使用。</p><h2 id="Skills-的存储层级"><a href="#Skills-的存储层级" class="headerlink" title="Skills 的存储层级"></a>Skills 的存储层级</h2><p>Skills 的存储架构类似于 Java 生态中的依赖管理体系，分为三个层级：</p><figure class="highlight nix"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs nix">┌─────────────────────────────────────────────────────────────────┐<br>│                    公共云端层 (Public Hub)                        │<br>│         类似 Maven Central <span class="hljs-symbol">/</span> npm registry                        │<br>│         未来可能的 Anthropic Skills Hub                           │<br>└───────────────────────────┬─────────────────────────────────────┘<br>                            │<br>                            ▼<br>┌─────────────────────────────────────────────────────────────────┐<br>│                    企业私服层 (Enterprise Hub)                    │<br>│         类似 Nexus 私服 <span class="hljs-symbol">/</span> npm private registry                   │<br>│         公司内部的 MCP Server，统一管理员工通用技能                  │<br>└───────────────────────────┬─────────────────────────────────────┘<br>                            │<br>                            ▼<br>┌─────────────────────────────────────────────────────────────────┐<br>│                    本地层 (Local)                                 │<br>│         类似本地 .m2 目录 <span class="hljs-symbol">/</span> node_modules                          │<br>│         ~<span class="hljs-operator">/</span>.claude<span class="hljs-operator">/</span>skills<span class="hljs-symbol">/</span> 或项目内 .claude<span class="hljs-operator">/</span>skills<span class="hljs-symbol">/</span>               │<br>└─────────────────────────────────────────────────────────────────┘<br></code></pre></td></tr></table></figure><table><thead><tr><th>层级</th><th>类比 Java 生态</th><th>Skills 对应</th><th>适用场景</th></tr></thead><tbody><tr><td>本地层</td><td><code>~/.m2/repository</code></td><td><code>~/.claude/skills/</code></td><td>个人开发的私有 Skills、本地调试</td></tr><tr><td>企业私服层</td><td>Nexus&#x2F;Artifactory 私服</td><td>企业 MCP Hub</td><td>公司内部通用 Skills，如审批流、内部系统对接</td></tr><tr><td>公共云端层</td><td>Maven Central</td><td>未来的 Skills Hub</td><td>社区贡献的通用 Skills，如 GitHub、Slack 集成</td></tr></tbody></table><p><strong>查找优先级</strong>：与 Maven 依赖解析类似，Skills 也遵循”就近原则”——本地 &gt; 企业私服 &gt; 公共 Hub。当同名 Skill 存在于多个层级时，优先使用本地版本。</p><h1 id="未来"><a href="#未来" class="headerlink" title="未来"></a>未来</h1><h2 id="三阶段演进"><a href="#三阶段演进" class="headerlink" title="三阶段演进"></a>三阶段演进</h2><p>基于当前的发展趋势，我认为 AI Agent 的能力扩展可能会经历三个阶段：</p><h3 id="第一阶段：手动时代（现在）"><a href="#第一阶段：手动时代（现在）" class="headerlink" title="第一阶段：手动时代（现在）"></a>第一阶段：手动时代（现在）</h3><p>用户需要手动配置 MCP Server、安装 Skills，感知强烈，门槛高。</p><h3 id="第二阶段：发现时代（近未来）"><a href="#第二阶段：发现时代（近未来）" class="headerlink" title="第二阶段：发现时代（近未来）"></a>第二阶段：发现时代（近未来）</h3><p>通过 MCP 自动发现。可能会出现类似 npm 或 Docker Hub 的 <strong>Skill Registry</strong>：</p><ul><li>配置文件里写：<code>&quot;skills&quot;: [&quot;@github/search&quot;, &quot;@linear/issue-manager&quot;]</code></li><li>启动时自动去地址获取 SKILLS，通过 MCP 协议下载技能定义</li><li>用户知道 AI 有这些能力，但不需要管背后代码</li></ul><h3 id="第三阶段：隐形时代（终极目标）"><a href="#第三阶段：隐形时代（终极目标）" class="headerlink" title="第三阶段：隐形时代（终极目标）"></a>第三阶段：隐形时代（终极目标）</h3><p>这是最值得期待的阶段：**Skills 彻底消失，变成大模型的”潜意识”**。</p><ol><li><strong>海量技能池</strong>：云端存在数百万个 MCP 服务</li><li><strong>意图识别与自动路由</strong>：Claude 自动分析任务并拆解步骤</li><li><strong>即时加载</strong>：毫秒级自动调用对应 Skill，无需用户干预</li></ol><p>**未来的 AI 就像”电力”**：100 年前你需要了解发电机的原理；现在你只需要插上插头。未来的大模型也会进化到——你只要说出需求，它会自动在后台调动千千万万个你从未听说过的”Skills”去帮你达成。</p><h2 id="安全与信任"><a href="#安全与信任" class="headerlink" title="安全与信任"></a>安全与信任</h2><p>当 Skills 变得越来越自动化，安全问题会变得尤为重要：</p><table><thead><tr><th>层级</th><th>存储位置</th><th>适用场景</th></tr></thead><tbody><tr><td>本地&#x2F;私有层</td><td>本地电脑或公司内网</td><td>敏感业务逻辑、本地硬件交互</td></tr><tr><td>企业中台层</td><td>公司 MCP Hub</td><td>统一管理员工通用技能</td></tr><tr><td>公共云端层</td><td>类似 App Store</td><td>第三方开发者贡献的通用 Skills</td></tr></tbody></table><p>公共仓库里的 Skill 需要经过：权限声明、运行时沙箱、代码审计与签名。这需要一个有信用背书的大厂（如 Anthropic，Google 等）来提供官方的审核与分发平台。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>从 Function Call 到 MCP 再到 Agent Skills，AI 能力扩展的演进遵循着一个清晰的流程：</p><ol><li><strong>Function Call</strong>：让大模型学会使用工具，但每个应用各自为战</li><li><strong>MCP</strong>：建立统一的工具接口标准，实现生态复用</li><li><strong>Agent Skills</strong>：在 MCP 基础上进一步抽象，实现按需加载、token 优化</li></ol><p>这个演进过程的本质是：**让 AI 从一个”什么都懂一点但包袱很重”的聊天机器人，变成一个可以根据任务场景，随时调用不同工具来实现需求的关键，比如最近大热的 <a href="https://github.com/openclaw/openclaw/">OpenClaw</a> **</p><p>随着大模型的持续进步，这些基础设施最终会变得”隐形”——用户不再需要知道 Skills 的存在，只需要表达意图，AI 就能自动调用合适的能力来完成任务。</p><p>这才是 AI Agent 的终极形态。</p><p>最后接着写这篇文章的过程，我也编写了几个 <a href="https://github.com/crossoverJie/skills">SKILLS</a>可以用于我在写文章的过程中自动生成文章的封面然后上传到图床。</p><p>有类似需求的朋友可以试用下。</p><blockquote><p>当然这个 SKILLS 也是一行代码没写，全部交给 AI 生成的，感兴趣的再下一篇分享下相关流程。</p></blockquote><p>#Blog</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;最近 Claude 的 SKILLS 很火，忍不住也来体验了一下，发现确实是有些东西的；但也发现身边的一些同事对这些新出的概念总是很懵逼，所以便有了这篇文章。&lt;/p&gt;
&lt;p&gt;从最早的 Function Call，到 MCP 协议，再到如今的 Agent Skills。&lt;/p&gt;
&lt;p&gt;本文将从技术演进的角度，带你理解这些概念之间的关系，以及它们如何让 AI 从一个”只会说话的聊天机器人”变成真正能”动手做事”的智能助手。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
    <category term="MCP" scheme="http://crossoverjie.top/tags/MCP/"/>
    
    <category term="Claude" scheme="http://crossoverjie.top/tags/Claude/"/>
    
  </entry>
  
  <entry>
    <title>对 AI 更友好的代码分割算法分析</title>
    <link href="http://crossoverjie.top/2026/01/14/AI/splitter-Algorithm-analyse/"/>
    <id>http://crossoverjie.top/2026/01/14/AI/splitter-Algorithm-analyse/</id>
    <published>2026-01-14T17:56:51.000Z</published>
    <updated>2026-07-30T08:43:22.956Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://s2.loli.net/2026/01/13/WZuvxgBJw5MSC8E.png"></p><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>因为最近在基于 <a href="https://crossoverjie.top/2025/12/25/AI/deepwiki-rag-principle/">RAG</a> 对我们的 code repo 做 AI 分析，其中有一个非常核心的流程就是需要将我们的代码库里的源码进行分割，分割之后会作为 chunk 供 RAG 查询；然后再将查询到的 chunk 提交给 LLM 做分析。</p><span id="more"></span><p>目前我们所使用的 <a href="https://github.com/AsyncFuncAI/deepwiki-open/">deepwiki-open</a>对代码的分析使用的是最通用的 <code>text_splitter</code>:</p><p><img src="https://s2.loli.net/2025/12/24/CVtdMlnwZzuHDGT.png"></p><p>分割方法也是最简单的按照 word 进行分割，普通场景下 <code>text_splitter</code> 够用，但对于我们这种存代码的场景就需要使用特殊的 <code>Spitter</code> 了；主要问题是它不理解语言结构，容易把函数&#x2F;类等语义单元切断，导致检索召回片段不完整、上下文丢失。</p><h1 id="算法对比"><a href="#算法对比" class="headerlink" title="算法对比"></a>算法对比</h1><h3 id="1-基础文本切分"><a href="#1-基础文本切分" class="headerlink" title="1. 基础文本切分"></a>1. 基础文本切分</h3><p><strong>简介</strong>： 最原始的方法。不管代码逻辑，直接按字符长度或者空格硬切。就像切西瓜不管瓜瓤结构，每一刀切固定的厚度。</p><ul><li><strong>优点</strong>：简单，适用于所有文本项目</li><li><strong>缺点</strong>：不适合代码项目，经常把一个完整的函数拦腰截断，大模型读起来云里雾里。</li></ul><p><strong>代码示例</strong>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs Python">splitter = TextSplitter(**configs[<span class="hljs-string">&quot;text_splitter&quot;</span>])<br>document = splitter.split_text(code)<br></code></pre></td></tr></table></figure><h3 id="手搓-Tree-Sitter-参考Claude-context"><a href="#手搓-Tree-Sitter-参考Claude-context" class="headerlink" title="手搓 Tree-Sitter (参考Claude-context)"></a>手搓 Tree-Sitter (参考<a href="https://github.com/zilliztech/claude-context">Claude-context</a>)</h3><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs ts"><span class="hljs-title function_">it</span>(<span class="hljs-string">&#x27;should split Java code from external file&#x27;</span>, <span class="hljs-title function_">async</span> () =&gt; &#123;  <br>    <span class="hljs-keyword">const</span> filePath = <span class="hljs-string">&#x27;AppService.java&#x27;</span>;  <br>      <br>    <span class="hljs-keyword">if</span> (fs.<span class="hljs-title function_">existsSync</span>(filePath)) &#123;  <br>        <span class="hljs-keyword">const</span> code = fs.<span class="hljs-title function_">readFileSync</span>(filePath, <span class="hljs-string">&#x27;utf-8&#x27;</span>);  <br>        <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`Reading Java file from: <span class="hljs-subst">$&#123;filePath&#125;</span>`</span>);  <br>        <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`File size: <span class="hljs-subst">$&#123;code.length&#125;</span> characters`</span>);  <br>          <br>        <span class="hljs-keyword">const</span> chunks = <span class="hljs-keyword">await</span> splitter.<span class="hljs-title function_">split</span>(code, <span class="hljs-string">&#x27;java&#x27;</span>, filePath);  <br>          <br>        <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`Split into <span class="hljs-subst">$&#123;chunks.length&#125;</span> chunks`</span>);  <br>        chunks.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">chunk, index</span>) =&gt;</span> &#123;  <br>            <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`&gt;&gt;&gt;&gt;Chunk <span class="hljs-subst">$&#123;index&#125;</span>: <span class="hljs-subst">$&#123;chunk.content&#125;</span>\n`</span>);  <br>            <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`Metadata:`</span>, chunk.<span class="hljs-property">metadata</span>);  <br>            <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`Content preview: <span class="hljs-subst">$&#123;chunk.content.substring(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)&#125;</span>...`</span>);        &#125;);  <br>    &#125; <span class="hljs-keyword">else</span> &#123;  <br>        <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">warn</span>(<span class="hljs-string">`File not found: <span class="hljs-subst">$&#123;filePath&#125;</span>`</span>);  <br>    &#125;  <br>&#125;);<br></code></pre></td></tr></table></figure><p>原本是 <a href="https://github.com/zilliztech/claude-context/blob/2efe1e9aaf59f4f8c9aa3635b27326a2ae94fa1b/packages/core/src/splitter/ast-splitter.ts#L44">TS</a> 写的，核心是使用 <code>tree-sitter</code> 做 AST 分析之后进行拆分，只是会在解析 AST 失败的时候使用 <code>LangChainCodeSplitter</code> 作为兜底。</p><p><img src="https://s2.loli.net/2026/01/13/IyDPuQ1WK7RJfzS.png"></p><p>这部分没有找到现成的开源方案，于是我就按照 ts 代码翻译了一份 Python 的版本：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python">splitter = AstCodeSplitter(chunk_size, chunk_overlap)  <br>chunks = splitter.split(code, <span class="hljs-string">&quot;java&quot;</span>, file_path)  <br><span class="hljs-keyword">for</span> i, chunk <span class="hljs-keyword">in</span> <span class="hljs-built_in">enumerate</span>(chunks):  <br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;&gt;&gt;&gt;&gt;Chunk <span class="hljs-subst">&#123;i&#125;</span>: <span class="hljs-subst">&#123;chunk.content&#125;</span>\n&quot;</span>)<br></code></pre></td></tr></table></figure><table><thead><tr><th>方案</th><th>主要原理</th><th>代码友好度</th><th>适合场景</th><th>主要缺点</th></tr></thead><tbody><tr><td><strong>现有：deepwiki-open 的 TextSplitter（split_by&#x3D;word）</strong></td><td>纯通用文本切分：按 word&#x2F;长度 + overlap 切块</td><td>弱</td><td>快速起步；纯文本&#x2F;注释类内容较多；对精度要求不高</td><td>容易把函数&#x2F;类切断；chunk 语义不完整；对代码检索召回不稳</td></tr><tr><td><strong><a href="https://github.com/zilliztech/claude-context">claude-context</a>，没有 Python 库，得自己实现。</strong><br><br><strong>使用了  <a href="https://reference.langchain.com/python/langchain_text_splitters/?_gl=1*1wsspz*_gcl_au*MTQxMjAyNDczOS4xNzY1NDQ2MTUx*_ga*NDcyOTM2OTM2LjE3NjU0NDYxNTI.*_ga_47WX3HKKY2*czE3NjY0NjkxODUkbzYkZzEkdDE3NjY0NzA0MDkkajYwJGwwJGgw#langchain_text_splitters.RecursiveCharacterTextSplitter.from_language">LangChain CodeTextSplitter</a> 兜底。</strong></td><td>使用 tree-sitter 解析 AST，支持 chunk_size 和 overlap。<br><br>生成 chunk 后再处理是否超过 chunk_size，内存占用大于 code-spitter<br><br><a href="https://github.com/zilliztech/claude-context/blob/2efe1e9aaf59f4f8c9aa3635b27326a2ae94fa1b/packages/core/src/splitter/ast-splitter.ts#L109">相关代码</a></td><td>很强</td><td>多语言代码库 RAG、希望按函数&#x2F;类分块</td><td><strong>内存占用大于 code-spitter</strong></td></tr><tr><td><strong><a href="https://github.com/wangxj03/code-splitter">wangxj03&#x2F;code-splitter</a>（rust 编写有提供 <a href="https://pypi.org/project/code-splitter/">Python</a> binding库）</strong><br><br><strong>参考了 <strong><a href="https://github.com/benbrandt/text-splitter">benbrandt&#x2F;text</a><a href="https://github.com/wangxj03/code-splitter">-splitter</a></strong> &amp; <a href="https://developers.llamaindex.ai/python/framework/module_guides/loading/node_parsers/modules/#codesplitter">LlamaIndex’s CodeSpiller</a>（提供了 Python 库）</strong></td><td>用 <a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter</a> 解析 AST，再按语法节点+ chunk 长度合并。<br><br>边遍历边合并，内存占用较小；<a href="https://github.com/wangxj03/code-splitter/blob/aa9a37e967c242b481a8a0ad1f663e5113d12a04/src/splitter.rs#L118">相关代码</a>。<br><br>直接将语法数按照 chunk 分割，<strong>没有处理 overlap；</strong></td><td>很强</td><td>多语言代码库 RAG、希望按函数&#x2F;类分块</td><td>依赖 tree-sitter grammar；集成复杂度略高<br><br><strong>没有处理 overlap，生成的上下文可能会不连续。</strong></td></tr><tr><td><strong><a href="https://reference.langchain.com/python/langchain_text_splitters/?_gl=1*1wsspz*_gcl_au*MTQxMjAyNDczOS4xNzY1NDQ2MTUx*_ga*NDcyOTM2OTM2LjE3NjU0NDYxNTI.*_ga_47WX3HKKY2*czE3NjY0NjkxODUkbzYkZzEkdDE3NjY0NzA0MDkkajYwJGwwJGgw#langchain_text_splitters.RecursiveCharacterTextSplitter.from_language">LangChain CodeTextSplitter</a></strong></td><td>按语言特征分隔符（def&#x2F;class&#x2F;function等）切分（部分场景可结构化）<br><br>预设了一些语言的<a href="https://github.com/langchain-ai/langchain/blob/master/libs/text-splitters/langchain_text_splitters/character.py#L172">关键字</a>。</td><td>中-强</td><td>想快速落地、LangChain 生态、主流语言</td><td>多数实现偏“规则&#x2F;正则”，复杂嵌套不如 AST 稳</td></tr><tr><td><strong><a href="https://github.com/benbrandt/text-splitter">benbrandt&#x2F;text</a><a href="https://github.com/wangxj03/code-splitter">-splitter</a>（语义&#x2F;边界优先），rust 编写，</strong><a href="https://pypi.org/project/semantic-text-splitter/">有提供 Python binding 库</a><strong>。</strong></td><td>用 <a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter</a> 解析 AST。</td><td>强</td><td></td><td></td></tr><tr><td><strong><a href="https://developers.llamaindex.ai/python/framework/module_guides/loading/node_parsers/modules/#codesplitter">LlamaIndex CodeSplitter</a></strong></td><td>用 <a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter</a> 解析 AST。只使用了最大字符分割，<strong>没有处理 overlap；</strong></td><td>强</td><td></td><td><strong>没有处理 overlap；</strong></td></tr></tbody></table><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>我们对同一个 Java 源码文件分别使用了 <a href="https://github.com/zilliztech/claude-context">claude-context</a> 和 <a href="https://github.com/benbrandt/text-splitter">text-splitter</a>进行了对比。</p><table><thead><tr><th>特性</th><th><code>benbrandt:text-splitter-rust</code></th><th><code>claude-context-ts</code> &#x2F; <code>claude-py-impl</code></th></tr></thead><tbody><tr><td><strong>行边界对齐</strong></td><td><strong>极佳</strong>。每个 Chunk 都从新行开始，在行末结束。</td><td><strong>较差</strong>。经常在行中间甚至单词中间切断（如 <code>esDO</code>）。</td></tr><tr><td><strong>语法完整性</strong></td><td><strong>高</strong>。尽量保持了方法签名或逻辑块的完整。</td><td><strong>低</strong>。由于是基于字符&#x2F;Token 硬切，导致代码语义破碎。</td></tr><tr><td><strong>重叠策略 (Overlap)</strong></td><td><strong>有意义的逻辑重叠</strong>。在方法交界处进行重叠。</td><td><strong>机械重叠</strong>。简单的滑动窗口，不考虑代码逻辑。</td></tr><tr><td><strong>Embedding 质量</strong></td><td><strong>高</strong>。由于没有破碎单词，向量表示的语义更精准。</td><td><strong>中</strong>。存在破碎的单词</td></tr></tbody></table><p>最后我们选择了 <code>benbrandt:text-splitter-rust</code> 的版本（提供了 Python binding 库）。</p><p>但对某个代码 repo 分析的效果与许多因素有关，比如 LLM 大模型质量、Embeding 的质量、提示词是否合理；其中的 Code Splitter 算法只是较小的一个环节。</p><p>这类需求随着大模型的迭代也需要常用常新，后续也会继续迭代相关知识。<br>#Blog </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2026/01/13/WZuvxgBJw5MSC8E.png&quot;&gt;&lt;/p&gt;
&lt;h1 id=&quot;背景&quot;&gt;&lt;a href=&quot;#背景&quot; class=&quot;headerlink&quot; title=&quot;背景&quot;&gt;&lt;/a&gt;背景&lt;/h1&gt;&lt;p&gt;因为最近在基于 &lt;a href=&quot;https://crossoverjie.top/2025/12/25/AI/deepwiki-rag-principle/&quot;&gt;RAG&lt;/a&gt; 对我们的 code repo 做 AI 分析，其中有一个非常核心的流程就是需要将我们的代码库里的源码进行分割，分割之后会作为 chunk 供 RAG 查询；然后再将查询到的 chunk 提交给 LLM 做分析。&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>AI 如何用 AST 每天对 200 万+ 文件做高质量分块（用于代码搜索）</title>
    <link href="http://crossoverjie.top/2025/12/29/AI/chunking-2m-files/"/>
    <id>http://crossoverjie.top/2025/12/29/AI/chunking-2m-files/</id>
    <published>2025-12-29T17:56:51.000Z</published>
    <updated>2026-07-30T08:43:22.955Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://s2.loli.net/2025/12/29/4WHveihZ6utrx9O.png"></p><p>原文链接：<a href="https://github.com/sweepai/sweep/blob/main/docs/pages/blogs/chunking-2m-files.mdx">https://github.com/sweepai/sweep/blob/main/docs/pages/blogs/chunking-2m-files.mdx</a></p><hr><p>最近在研究 Code Splitter 的算法，发现 <a href="https://developers.llamaindex.ai/python/framework/module_guides/loading/node_parsers/modules/#codesplitter">llama_index</a> 的代码分割使用的是 sweepai 的代码分割算法，同时还提供了一篇博客，也就有了这篇文章。</p><p><img src="https://s2.loli.net/2025/12/29/pVsnHRh3FUacw5e.png"></p><span id="more"></span><p>初始化任何向量存储都需要对大型文档进行切分（chunking）以进行高效搜索。</p><p>为什么不能直接对整个文件做嵌入（embed）？以我们主 API 的 <a href="https://github.com/sweepai/sweep/blob/b267b613d4c706eaf959fe6789f11e9a856521d1/sweepai/api.py">endpoint 文件</a> 为例：</p><ol><li>导入包</li><li>常量声明</li><li>辅助函数</li><li>每个 webhook endpoint 的业务逻辑</li></ol><p>如果我搜索 “GitHub Action run”，它应该匹配检查 “check_runs completed” 事件的那个 switch case 块（参见 <a href="https://github.com/sweepai/sweep/blob/b267b613d4c706eaf959fe6789f11e9a856521d1/sweepai/api.py#L295-L313">代码片段</a>）。但那只是 400 多行代码中的大约 20 行，即使是完美的搜索算法也只会把相似性视为 5%。如果我们把 400 行切成 20 个每个 20 行的块，就更容易匹配到正确的 switch case 块。</p><p><img src="https://s2.loli.net/2025/12/29/GPSgUirMceTzKQ6.png"></p><p>那我们如何产生 20 行的块？一个简单的办法是均匀地把 400 行切成每 20 行一块。</p><p>但是，这种方法行不通。语义上相关的代码不会被保留在一起，且会丢失上下文。例如，函数头可能会被和实现体分离。</p><p>我们当前的代码切分算法每天处理 <strong>200 万+ 文件</strong>，并且已经<a href="https://github.com/sweepai/sweep/blob/b267b613d4c706eaf959fe6789f11e9a856521d1/sweepai/utils/utils.py#L48-L126">开源了</a>！</p><h2 id="约束-🚧"><a href="#约束-🚧" class="headerlink" title="约束 🚧"></a>约束 🚧</h2><p>大多数用于 RAG（检索增强生成）的切分器按 token 数量做上限。为简化处理，我们决定使用字符数，上限设为 1500。</p><p>这是因为代码的平均 token 与字符比约为 1:5（300 tokens），而嵌入模型通常受 512 tokens 限制。进一步地，1500 字符大约对应 40 行，大致等同于一个小到中等大小的函数或类。</p><p>挑战在于尽可能接近 1500 字符，同时保证块在语义上保持一致且相关上下文被保留。</p><h2 id="开箱即用的解决方案-📦"><a href="#开箱即用的解决方案-📦" class="headerlink" title="开箱即用的解决方案 📦"></a>开箱即用的解决方案 📦</h2><p>最简单的现成解决方案是 <a href="https://reference.langchain.com/python/langchain_text_splitters/?_gl=1*1wsspz*_gcl_au*MTQxMjAyNDczOS4xNzY1NDQ2MTUx*_ga*NDcyOTM2OTM2LjE3NjU0NDYxNTI.*_ga_47WX3HKKY2*czE3NjY0NjkxODUkbzYkZzEkdDE3NjY0NzA0MDkkajYwJGwwJGgw#langchain_text_splitters.RecursiveCharacterTextSplitter.split_text">Langchain 的递归切分器（recursive chunker）</a>。总体思路：</p><ol><li>用顶层分隔符拆分文本（先用 class，然后是 function 定义，然后是方法等）</li><li>迭代每个区块并贪心地把它们串联直到超过字符限制。对于过大的区块，使用下一级分隔符递归切分。</li></ol><p>示例伪代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs python">delimiters = [<span class="hljs-string">&quot;\nclass &quot;</span>, <span class="hljs-string">&quot;\ndef &quot;</span>, <span class="hljs-string">&quot;\n\tdef &quot;</span>, <span class="hljs-string">&quot;\n\n&quot;</span>, <span class="hljs-string">&quot;\n&quot;</span>, <span class="hljs-string">&quot; &quot;</span>, <span class="hljs-string">&quot;&quot;</span>]<br><span class="hljs-keyword">def</span> <span class="hljs-title function_">chunk</span>(<span class="hljs-params">text: <span class="hljs-built_in">str</span>, delimiter_index: <span class="hljs-built_in">int</span> = <span class="hljs-number">0</span>, MAX_CHARS: <span class="hljs-built_in">int</span> = <span class="hljs-number">1500</span></span>) -&gt; <span class="hljs-built_in">list</span>[<span class="hljs-built_in">str</span>]:<br>delimiter = delimiters[delimiter_index]<br>new_chunks = []<br>current_chunk = <span class="hljs-string">&quot;&quot;</span><br><span class="hljs-keyword">for</span> section <span class="hljs-keyword">in</span> text.split(delimiter):<br><span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(section) &gt; MAX_CHARS:<br><span class="hljs-comment"># Section is too big, recursively chunk this section</span><br>new_chunks.append(current_chunk)<br>current_chunk = <span class="hljs-string">&quot;&quot;</span><br>new_chunks.extend(chunk(section, delimiter_index + <span class="hljs-number">1</span>, MAX_CHARS)<br><span class="hljs-keyword">elif</span> <span class="hljs-built_in">len</span>(current_chunk) + <span class="hljs-built_in">len</span>(section) &gt; MAX_CHARS:<br><span class="hljs-comment"># Current chunk is max size</span><br>new_chunks.append(current_chunk)<br>current_chunk = section<br><span class="hljs-keyword">else</span>:<br><span class="hljs-comment"># Concatenate section to current_chunk</span><br>current_chunk += section<br><span class="hljs-keyword">return</span> new_chunks<br></code></pre></td></tr></table></figure><p>针对每种语言我们会使用不同的分隔符。</p><h3 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h3><p>完整示例文件请见：<a href="https://gist.github.com/kevinlu1248/ded3ea33dcd8a9bd08078f4c64eb9268">https://gist.github.com/kevinlu1248/ded3ea33dcd8a9bd08078f4c64eb9268</a></p><h4 id="示例-1"><a href="#示例-1" class="headerlink" title="示例 #1"></a>示例 #1</h4><p>基于我们处理 GitHub Action 运行的 <code>on_check_suite.py</code> 文件。一个糟糕的切分把字符串拼接声明与其内容分开了。❌</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><code class="hljs python">...<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">on_check_suite</span>(<span class="hljs-params">request: CheckRunCompleted</span>):<br>    logger.info(<span class="hljs-string">f&quot;Received check run completed event for <span class="hljs-subst">&#123;request.repository.full_name&#125;</span>&quot;</span>)<br>    g = get_github_client(request.installation.<span class="hljs-built_in">id</span>)<br>    repo = g.get_repo(request.repository.full_name)<br>    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> get_gha_enabled(repo):<br>        logger.info(<span class="hljs-string">f&quot;Skipping github action for <span class="hljs-subst">&#123;request.repository.full_name&#125;</span> because it is not enabled&quot;</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span><br>    pr = repo.get_pull(request.check_run.pull_requests[<span class="hljs-number">0</span>].number)<br>    num_pr_commits = <span class="hljs-built_in">len</span>(<span class="hljs-built_in">list</span>(pr.get_commits()))<br>    <span class="hljs-keyword">if</span> num_pr_commits &gt; <span class="hljs-number">20</span>:<br>        logger.info(<span class="hljs-string">f&quot;Skipping github action for PR with <span class="hljs-subst">&#123;num_pr_commits&#125;</span> commits&quot;</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span><br>    logger.info(<span class="hljs-string">f&quot;Running github action for PR with <span class="hljs-subst">&#123;num_pr_commits&#125;</span> commits&quot;</span>)<br>    logs = download_logs(<br>        request.repository.full_name,<br>        request.check_run.run_id,<br>        request.installation.<span class="hljs-built_in">id</span><br>    )<br>    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> logs:<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span><br>    logs = clean_logs(logs)<br>    extractor = GHAExtractor()<br>    logger.info(<span class="hljs-string">f&quot;Extracting logs from <span class="hljs-subst">&#123;request.repository.full_name&#125;</span>, logs: <span class="hljs-subst">&#123;logs&#125;</span>&quot;</span>)<br>    problematic_logs = extractor.gha_extract(logs)<br>    <span class="hljs-keyword">if</span> problematic_logs.count(<span class="hljs-string">&quot;</span><br><span class="hljs-string">&quot;</span>) &gt; <span class="hljs-number">15</span>:<br>        problematic_logs += <span class="hljs-string">&quot;</span><br><span class="hljs-string"></span><br><span class="hljs-string">========================================</span><br><span class="hljs-string"></span><br><span class="hljs-string">There are a lot of errors. This is likely a larger issue with the PR and not a small linting/type-checking issue.&quot;</span><br>    comments = <span class="hljs-built_in">list</span>(pr.get_issue_comments())<br>    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(comments) &gt;= <span class="hljs-number">2</span> <span class="hljs-keyword">and</span> problematic_logs == comments[-<span class="hljs-number">1</span>].body <span class="hljs-keyword">and</span> comments[-<span class="hljs-number">2</span>].body == comments[-<span class="hljs-number">1</span>].body:<br>        comment = pr.as_issue().create_comment(log_message.<span class="hljs-built_in">format</span>(error_logs=problematic_logs) + <span class="hljs-string">&quot;</span><br><span class="hljs-string"></span><br><span class="hljs-string">I&#x27;m getting the same errors 3 times in a row, so I will stop working on fixing this PR.&quot;</span>)<br>        logger.warning(<span class="hljs-string">&quot;Skipping logs because it is duplicated&quot;</span>)<br>        <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">&quot;Duplicate error logs&quot;</span>)<br>    <span class="hljs-built_in">print</span>(problematic_logs)<br>    comment = pr.as_issue().create_comment(log_message.<span class="hljs-built_in">format</span>(error_logs=problematic_logs))<br>    on_comment(<br>        repo_full_name=request.repository.full_name,<br>        repo_description=request.repository.description,<br>        comment=problematic_logs,<br>        pr_path=<span class="hljs-literal">None</span>,<br>        pr_line_position=<span class="hljs-literal">None</span>,<br>        username=request.sender.login,<br>        installation_id=request.installation.<span class="hljs-built_in">id</span>,<br>        pr_number=request.check_run.pull_requests[<span class="hljs-number">0</span>].number,<br>        comment_id=comment.<span class="hljs-built_in">id</span>,<br>        repo=repo,<br>    )<br>    <span class="hljs-keyword">return</span> &#123;<span class="hljs-string">&quot;success&quot;</span>: <span class="hljs-literal">True</span>&#125;<br></code></pre></td></tr></table></figure><h4 id="示例-2"><a href="#示例-2" class="headerlink" title="示例 #2"></a>示例 #2</h4><p>基于 LlamaIndex 的 <code>BaseIndex.ts</code> 文件（声明向量存储的 ABC）。糟糕的切分把类的方法实现与其头部分离了。❌</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><code class="hljs tsx">...<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">IndexDict</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">IndexStruct</span> &#123;<br>  <span class="hljs-attr">nodesDict</span>: <span class="hljs-title class_">Record</span>&lt;<span class="hljs-built_in">string</span>, <span class="hljs-title class_">BaseNode</span>&gt; = &#123;&#125;;<br>  <span class="hljs-attr">docStore</span>: <span class="hljs-title class_">Record</span>&lt;<span class="hljs-built_in">string</span>, <span class="hljs-title class_">Document</span>&gt; = &#123;&#125;; <span class="hljs-comment">// <span class="hljs-doctag">FIXME:</span> this should be implemented in storageContext</span><br>  <span class="hljs-attr">type</span>: <span class="hljs-title class_">IndexStructType</span> = <span class="hljs-title class_">IndexStructType</span>.<span class="hljs-property">SIMPLE_DICT</span>;<br><br>========================================<br><br><span class="hljs-title function_">getSummary</span>(): <span class="hljs-built_in">string</span> &#123;<br>    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-property">summary</span> === <span class="hljs-literal">undefined</span>) &#123;<br>      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&quot;summary field of the index dict is not set&quot;</span>);<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">summary</span>;<br>  &#125;<br><br>  <span class="hljs-title function_">addNode</span>(<span class="hljs-params"><span class="hljs-attr">node</span>: <span class="hljs-title class_">BaseNode</span>, <span class="hljs-attr">textId</span>?: <span class="hljs-built_in">string</span></span>) &#123;<br>    <span class="hljs-keyword">const</span> vectorId = textId ?? node.<span class="hljs-property">id_</span>;<br>    <span class="hljs-variable language_">this</span>.<span class="hljs-property">nodesDict</span>[vectorId] = node;<br>  &#125;<br><br>  <span class="hljs-title function_">toJson</span>(): <span class="hljs-title class_">Record</span>&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">unknown</span>&gt; &#123;<br>    <span class="hljs-keyword">return</span> &#123;<br>      ...<span class="hljs-variable language_">super</span>.<span class="hljs-title function_">toJson</span>(),<br>      <span class="hljs-attr">nodesDict</span>: <span class="hljs-variable language_">this</span>.<span class="hljs-property">nodesDict</span>,<br>      <span class="hljs-attr">type</span>: <span class="hljs-variable language_">this</span>.<span class="hljs-property">type</span>,<br>    &#125;;<br>  &#125;<br>&#125;<br><br>...<br></code></pre></td></tr></table></figure><h3 id="问题-🤔"><a href="#问题-🤔" class="headerlink" title="问题 🤔"></a>问题 🤔</h3><p>然而，这个切分器存在严重问题：</p><ol><li>对 Python 效果不错，但对大括号密集的语言（如 JS）和基于 XML 的语言（如 HTML）会在不可预期的地方断开。<ul><li>此外，<code>str.split</code> 对这些更复杂的语法（如 JS、HTML）效果不好。</li><li>例如，即使对 Python，也会把像 <code>problematic_logs += \&quot;</code> 与其余字符串错误地分割。</li></ul></li><li>目前仅支持 16 种语言，不支持 JSX、Typescript、EJS 和 C#。<ul><li>JSX&#x2F;TSX 占我们用户群的大部分。</li></ul></li><li>Langchain 会删除重要分隔符（比如 “def” 和 “class”）。</li></ol><h2 id="我们的解决方案-🧠"><a href="#我们的解决方案-🧠" class="headerlink" title="我们的解决方案 🧠"></a>我们的解决方案 🧠</h2><p>根本问题是用一系列的 <code>str.split</code> 和分隔符来近似所谓的“具体语法树（CST）”是太原始了。</p><p>为了解决这个问题，我们直接使用 CST 解析器。如何获得大量语言的 CST 解析器？幸运的是，库 <a href="https://tree-sitter.github.io/">tree-sitter</a> 提供了标准化访问 113 种编程语言 CST 解析器的方式，并且速度快（用 C 写）且无额外依赖。</p><p>新的算法在高层上与 Langchain 类似，步骤如下：</p><ol><li>要对一个父节点进行切分，我们遍历其子节点并贪心地把它们打包在一起。对于每个子节点：</li><li>如果当前 chunk 太大，将其加入结果列表并清空当前 bundle</li><li>如果下一个子节点本身太大，则递归切分该子节点并把结果加入列表</li><li>否则，将该子节点的文本拼接到当前 chunk</li><li>对最终结果做后处理：把单行的 chunk 与下一个 chunk 合并</li><li>这样保证不会出现过小（意义不大）的 chunk</li></ol><p>示例伪代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">from</span> tree_sitter <span class="hljs-keyword">import</span> Node<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">chunk_node</span>(<span class="hljs-params">node: Node, text: <span class="hljs-built_in">str</span>, MAX_CHARS: <span class="hljs-built_in">int</span> = <span class="hljs-number">1500</span></span>) -&gt; <span class="hljs-built_in">list</span>[<span class="hljs-built_in">str</span>]:<br>new_chunks = []<br>current_chunk = <span class="hljs-string">&quot;&quot;</span><br><span class="hljs-keyword">for</span> child <span class="hljs-keyword">in</span> node.children:<br><span class="hljs-keyword">if</span> child.end_byte - child.start_byte &gt; MAX_CHARS:<br>new_chunks.append(current_chunk)<br>current_chunk = <span class="hljs-string">&quot;&quot;</span><br>new_chunks.extend(chunk_node(child, text, MAX_CHARS)<br><span class="hljs-keyword">elif</span> <span class="hljs-built_in">len</span>(current_chunk) + child.end_byte - child.start_byte &gt; MAX_CHARS:<br>new_chunks.append(current_chunk)<br>current_chunk = text[node.start_byte:node.end_byte]<br><span class="hljs-keyword">else</span>:<br>current_chunk += text[node.start_byte:node.end_byte]<br><span class="hljs-keyword">return</span> new_chunks<br></code></pre></td></tr></table></figure><h3 id="示例-1"><a href="#示例-1" class="headerlink" title="示例"></a>示例</h3><p>完整切分结果请见：<a href="https://gist.github.com/kevinlu1248/49a72a1978868775109c5627677dc512">https://gist.github.com/kevinlu1248/49a72a1978868775109c5627677dc512</a></p><h4 id="示例-1-1"><a href="#示例-1-1" class="headerlink" title="示例 #1"></a>示例 #1</h4><p>基于我们的 <code>on_check_suite.py</code> 文件。正确的切分；在 if 语句之前做切分，而不是把 if 语句与其主体分开。✅</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><code class="hljs python">...<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">on_check_suite</span>(<span class="hljs-params">request: CheckRunCompleted</span>):<br>    logger.info(<span class="hljs-string">f&quot;Received check run completed event for <span class="hljs-subst">&#123;request.repository.full_name&#125;</span>&quot;</span>)<br>    g = get_github_client(request.installation.<span class="hljs-built_in">id</span>)<br>    repo = g.get_repo(request.repository.full_name)<br>    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> get_gha_enabled(repo):<br>        logger.info(<span class="hljs-string">f&quot;Skipping github action for <span class="hljs-subst">&#123;request.repository.full_name&#125;</span> because it is not enabled&quot;</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span><br>    pr = repo.get_pull(request.check_run.pull_requests[<span class="hljs-number">0</span>].number)<br>    num_pr_commits = <span class="hljs-built_in">len</span>(<span class="hljs-built_in">list</span>(pr.get_commits()))<br>    <span class="hljs-keyword">if</span> num_pr_commits &gt; <span class="hljs-number">20</span>:<br>        logger.info(<span class="hljs-string">f&quot;Skipping github action for PR with <span class="hljs-subst">&#123;num_pr_commits&#125;</span> commits&quot;</span>)<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span><br>    logger.info(<span class="hljs-string">f&quot;Running github action for PR with <span class="hljs-subst">&#123;num_pr_commits&#125;</span> commits&quot;</span>)<br>    logs = download_logs(<br>        request.repository.full_name,<br>        request.check_run.run_id,<br>        request.installation.<span class="hljs-built_in">id</span><br>    )<br>    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> logs:<br>        <span class="hljs-keyword">return</span> <span class="hljs-literal">None</span><br>    logs = clean_logs(logs)<br>    extractor = GHAExtractor()<br>    logger.info(<span class="hljs-string">f&quot;Extracting logs from <span class="hljs-subst">&#123;request.repository.full_name&#125;</span>, logs: <span class="hljs-subst">&#123;logs&#125;</span>&quot;</span>)<br>    problematic_logs = extractor.gha_extract(logs)<br>    <span class="hljs-keyword">if</span> problematic_logs.count(<span class="hljs-string">&quot;\n&quot;</span>) &gt; <span class="hljs-number">15</span>:<br>        problematic_logs += <span class="hljs-string">&quot;\n\nThere are a lot of errors. This is likely a larger issue with the PR and not a small linting/type-checking issue.&quot;</span><br>    comments = <span class="hljs-built_in">list</span>(pr.get_issue_comments())<br><br>==========<br><br>    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(comments) &gt;= <span class="hljs-number">2</span> <span class="hljs-keyword">and</span> problematic_logs == comments[-<span class="hljs-number">1</span>].body <span class="hljs-keyword">and</span> comments[-<span class="hljs-number">2</span>].body == comments[-<span class="hljs-number">1</span>].body:<br>        comment = pr.as_issue().create_comment(log_message.<span class="hljs-built_in">format</span>(error_logs=problematic_logs) + <span class="hljs-string">&quot;\n\nI&#x27;m getting the same errors 3 times in a row, so I will stop working on fixing this PR.&quot;</span>)<br>        logger.warning(<span class="hljs-string">&quot;Skipping logs because it is duplicated&quot;</span>)<br>        <span class="hljs-keyword">raise</span> Exception(<span class="hljs-string">&quot;Duplicate error logs&quot;</span>)<br>    <span class="hljs-built_in">print</span>(problematic_logs)<br>    comment = pr.as_issue().create_comment(log_message.<span class="hljs-built_in">format</span>(error_logs=problematic_logs))<br>    on_comment(<br>        repo_full_name=request.repository.full_name,<br>        repo_description=request.repository.description,<br>        comment=problematic_logs,<br>        pr_path=<span class="hljs-literal">None</span>,<br>        pr_line_position=<span class="hljs-literal">None</span>,<br>        username=request.sender.login,<br>        installation_id=request.installation.<span class="hljs-built_in">id</span>,<br>        pr_number=request.check_run.pull_requests[<span class="hljs-number">0</span>].number,<br>        comment_id=comment.<span class="hljs-built_in">id</span>,<br>        repo=repo,<br>    )<br></code></pre></td></tr></table></figure><h4 id="示例-2-1"><a href="#示例-2-1" class="headerlink" title="示例 #2"></a>示例 #2</h4><p>基于 LlamaIndex 的 <code>BaseIndex.ts</code> 文件。我们的切分器正确地在导出的类和函数之间切分。✅</p><figure class="highlight tsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><code class="hljs tsx">...<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">IndexDict</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">IndexStruct</span> &#123;<br>  <span class="hljs-attr">nodesDict</span>: <span class="hljs-title class_">Record</span>&lt;<span class="hljs-built_in">string</span>, <span class="hljs-title class_">BaseNode</span>&gt; = &#123;&#125;;<br>  <span class="hljs-attr">docStore</span>: <span class="hljs-title class_">Record</span>&lt;<span class="hljs-built_in">string</span>, <span class="hljs-title class_">Document</span>&gt; = &#123;&#125;; <span class="hljs-comment">// <span class="hljs-doctag">FIXME:</span> this should be implemented in storageContext</span><br>  <span class="hljs-attr">type</span>: <span class="hljs-title class_">IndexStructType</span> = <span class="hljs-title class_">IndexStructType</span>.<span class="hljs-property">SIMPLE_DICT</span>;<br><br>  <span class="hljs-title function_">getSummary</span>(): <span class="hljs-built_in">string</span> &#123;<br>    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-property">summary</span> === <span class="hljs-literal">undefined</span>) &#123;<br>      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&quot;summary field of the index dict is not set&quot;</span>);<br>    &#125;<br>    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">summary</span>;<br>  &#125;<br><br>  <span class="hljs-title function_">addNode</span>(<span class="hljs-params"><span class="hljs-attr">node</span>: <span class="hljs-title class_">BaseNode</span>, <span class="hljs-attr">textId</span>?: <span class="hljs-built_in">string</span></span>) &#123;<br>    <span class="hljs-keyword">const</span> vectorId = textId ?? node.<span class="hljs-property">id_</span>;<br>    <span class="hljs-variable language_">this</span>.<span class="hljs-property">nodesDict</span>[vectorId] = node;<br>  &#125;<br><br>  <span class="hljs-title function_">toJson</span>(): <span class="hljs-title class_">Record</span>&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">unknown</span>&gt; &#123;<br>    <span class="hljs-keyword">return</span> &#123;<br>      ...<span class="hljs-variable language_">super</span>.<span class="hljs-title function_">toJson</span>(),<br>      <span class="hljs-attr">nodesDict</span>: <span class="hljs-variable language_">this</span>.<span class="hljs-property">nodesDict</span>,<br>      <span class="hljs-attr">type</span>: <span class="hljs-variable language_">this</span>.<span class="hljs-property">type</span>,<br>    &#125;;<br>  &#125;<br>&#125;<br><br>========================================<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">jsonToIndexStruct</span>(<span class="hljs-params"><span class="hljs-attr">json</span>: <span class="hljs-built_in">any</span></span>): <span class="hljs-title class_">IndexStruct</span> &#123;<br>  <span class="hljs-keyword">if</span> (json.<span class="hljs-property">type</span> === <span class="hljs-title class_">IndexStructType</span>.<span class="hljs-property">LIST</span>) &#123;<br>    <span class="hljs-keyword">const</span> indexList = <span class="hljs-keyword">new</span> <span class="hljs-title class_">IndexList</span>(json.<span class="hljs-property">indexId</span>, json.<span class="hljs-property">summary</span>);<br>    indexList.<span class="hljs-property">nodes</span> = json.<span class="hljs-property">nodes</span>;<br>    <span class="hljs-keyword">return</span> indexList;<br>  &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (json.<span class="hljs-property">type</span> === <span class="hljs-title class_">IndexStructType</span>.<span class="hljs-property">SIMPLE_DICT</span>) &#123;<br>    <span class="hljs-keyword">const</span> indexDict = <span class="hljs-keyword">new</span> <span class="hljs-title class_">IndexDict</span>(json.<span class="hljs-property">indexId</span>, json.<span class="hljs-property">summary</span>);<br>    indexDict.<span class="hljs-property">nodesDict</span> = json.<span class="hljs-property">nodesDict</span>;<br>    <span class="hljs-keyword">return</span> indexDict;<br>  &#125; <span class="hljs-keyword">else</span> &#123;<br>    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">`Unknown index struct type: <span class="hljs-subst">$&#123;json.<span class="hljs-keyword">type</span>&#125;</span>`</span>);<br>  &#125;<br>&#125;<br><br>...<br></code></pre></td></tr></table></figure><h3 id="算法其余部分-🤖"><a href="#算法其余部分-🤖" class="headerlink" title="算法其余部分 🤖"></a>算法其余部分 🤖</h3><ol><li>依次遍历支持的语言列表，直到某个解析器成功解析代码</li><li>对解析出的语法树根节点进行切分</li><li>如果没有任何语言成功解析，则使用一个普通的切分器：每次取 40 行，并在块间保留 15 行重叠（覆盖），这种情况约占 0.1%</li></ol><p>示例伪代码：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><code class="hljs python">language_names = [<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;java&quot;</span>, <span class="hljs-string">&quot;cpp&quot;</span>, <span class="hljs-string">&quot;go&quot;</span>, <span class="hljs-string">&quot;rust&quot;</span>, <span class="hljs-string">&quot;ruby&quot;</span>, <span class="hljs-string">&quot;php&quot;</span>] <span class="hljs-comment"># and more</span><br><br><span class="hljs-comment"># Installing the parsers</span><br>languages = &#123;&#125;<br><span class="hljs-keyword">for</span> language <span class="hljs-keyword">in</span> LANGUAGE_NAMES:<br>   subprocess.run(<span class="hljs-string">f&quot;git clone https://github.com/tree-sitter/tree-sitter-<span class="hljs-subst">&#123;language&#125;</span> cache/tree-sitter-<span class="hljs-subst">&#123;language&#125;</span>&quot;</span>, shell=<span class="hljs-literal">True</span>)<br>  <span class="hljs-keyword">for</span> language <span class="hljs-keyword">in</span> LANGUAGE_NAMES:<br>      Language.build_library(<span class="hljs-string">f&#x27;cache/build/<span class="hljs-subst">&#123;language&#125;</span>.so&#x27;</span>, [<span class="hljs-string">f&quot;cache/tree-sitter-<span class="hljs-subst">&#123;language&#125;</span>&quot;</span>])<br>  <span class="hljs-variable language_">self</span>.languages = &#123;language: Language(<span class="hljs-string">f&quot;cache/build/<span class="hljs-subst">&#123;language&#125;</span>.so&quot;</span>, language) <span class="hljs-keyword">for</span> language <span class="hljs-keyword">in</span> LANGUAGE_NAMES&#125;<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">chunk</span>(<span class="hljs-params">text: <span class="hljs-built_in">str</span>, MAX_CHARS: <span class="hljs-built_in">int</span> = <span class="hljs-number">1500</span></span>) -&gt; <span class="hljs-built_in">list</span>[<span class="hljs-built_in">str</span>]:<br><span class="hljs-comment"># Determining the language</span><br><span class="hljs-keyword">for</span> language_name <span class="hljs-keyword">in</span> language_names:<br>    language = languages[language_name]<br>    parser = Parser()<br>    parser.set_language(language)<br>    tree = parser.parse(<span class="hljs-built_in">bytes</span>(text, <span class="hljs-string">&quot;utf-8&quot;</span>))<br>    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> tree.root_node.children <span class="hljs-keyword">or</span> tree.root_node.children[<span class="hljs-number">0</span>].<span class="hljs-built_in">type</span> != <span class="hljs-string">&quot;ERROR&quot;</span>:<br>        file_language = language<br>        <span class="hljs-keyword">break</span><br>    logger.warning(<span class="hljs-string">f&quot;Not language <span class="hljs-subst">&#123;language_name&#125;</span>&quot;</span>)<br><br><span class="hljs-comment"># Smart chunker</span><br><span class="hljs-keyword">if</span> file_language:<br>      <span class="hljs-keyword">return</span> chunk_node(tree.root_node, text, max_chunk_size)<br><br><span class="hljs-comment"># Naive algorithm</span><br>  source_lines = file_content.split(<span class="hljs-string">&#x27;\n&#x27;</span>)<br>  num_lines = <span class="hljs-built_in">len</span>(source_lines)<br>  logger.info(<span class="hljs-string">f&quot;Number of lines: <span class="hljs-subst">&#123;num_lines&#125;</span>&quot;</span>)<br>  chunks = []<br>  start_line = <span class="hljs-number">0</span><br>  <span class="hljs-keyword">while</span> start_line &lt; num_lines <span class="hljs-keyword">and</span> num_lines &gt; overlap:<br>      end_line = <span class="hljs-built_in">min</span>(start_line + chunk_size, num_lines)<br>      chunk = <span class="hljs-string">&#x27;\n&#x27;</span>.join(source_lines[start_line:end_line])<br>      chunks.append(chunk)<br>      start_line += chunk_size - overlap<br><span class="hljs-keyword">return</span> chunks<br></code></pre></td></tr></table></figure><p>在 Sweep，我们目前安装了 Python、Java、C++、Go、Rust、Ruby、PHP、C#、嵌入式模板（ERB &amp; EJS）、Markdown、Vue 和 TSX。另请注意：C++ 覆盖 C，TSX 覆盖 JS、JSX 和 TS。</p><h2 id="陷阱-🕳️"><a href="#陷阱-🕳️" class="headerlink" title="陷阱 🕳️"></a>陷阱 🕳️</h2><p>不幸的是，<code>tree-sitter</code> 有时并不可靠，很多解析器由社区维护：</p><ul><li>TSX 解析器在无法解析时会挂起而不是返回错误</li><li>此外，tree-sitter 的核心用 C 写成。在我们的 serverless 生产环境中运行需要一套复杂的方法来缓存已编译的 C 二进制，移动到可执行目录，并使用 Python 包装器去调用它们</li><li>有些解析器在子节点之间留下空隙。我们通过合并（coalescing）解决了这个问题</li><li>没有一种解析器会在解析错误的语言时都以相同方式报错</li><li>有些解析器将根节点标记为 “ERROR”，而有些则把第一个 child 标为 ERROR</li></ul><p>我们通过在遇到这些错误（例如 TSX 挂起或其他不可靠行为）时回退到朴素切分器来规避问题，并将 TSX 优先级放在最后。同时我们会优先尝试与文件扩展名相对应的语言解析器。</p><h2 id="未来-🔮"><a href="#未来-🔮" class="headerlink" title="未来 🔮"></a>未来 🔮</h2><p>这个算法现在已通过 <a href="https://github.com/jerryjliu/llama_index/pull/7100">https://github.com/jerryjliu/llama_index/pull/7100</a> 集成到 LlamaIndex 中。</p><p>另一个问题是，文件中相距较远的代码片段可能仍然需要共享上下文。例如，一个类的方法可能需要类头的上下文，长函数也需要函数签名。一个可能的改进是采用类似以下的格式来保留上下文：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Foo</span>:<br>  ...<br>  <span class="hljs-keyword">def</span> <span class="hljs-title function_">bar</span>(<span class="hljs-params">self</span>):<br>      <span class="hljs-keyword">pass</span><br></code></pre></td></tr></table></figure><p>我们可以考虑使用 <a href="https://github.com/universal-ctags/ctags">universal ctags</a> 或类似工具以实现更简单、更通用的解析，或者在手工标注的切分上训练一个自定义的 spaCy sentencizer，但那可能有点过度设计。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2025/12/29/4WHveihZ6utrx9O.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;原文链接：&lt;a href=&quot;https://github.com/sweepai/sweep/blob/main/docs/pages/blogs/chunking-2m-files.mdx&quot;&gt;https://github.com/sweepai/sweep/blob/main/docs/pages/blogs/chunking-2m-files.mdx&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;最近在研究 Code Splitter 的算法，发现 &lt;a href=&quot;https://developers.llamaindex.ai/python/framework/module_guides/loading/node_parsers/modules/#codesplitter&quot;&gt;llama_index&lt;/a&gt; 的代码分割使用的是 sweepai 的代码分割算法，同时还提供了一篇博客，也就有了这篇文章。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2025/12/29/pVsnHRh3FUacw5e.png&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
  </entry>
  
</feed>
