<?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-06-23T10:32:57.544Z</updated>
  <id>http://crossoverjie.top/</id>
  
  <author>
    <name>crossoverJie</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.568Z</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-06-23T10:32:57.568Z</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-06-23T10:32:57.550Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.556Z</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-06-23T10:32:57.544Z</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="VibeCoding" scheme="http://crossoverjie.top/tags/VibeCoding/"/>
    
    <category term="LLM" scheme="http://crossoverjie.top/tags/LLM/"/>
    
  </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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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-06-23T10:32:57.544Z</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>
  
  <entry>
    <title>DeepWiki 一个常用 RAG 应用的开发流程</title>
    <link href="http://crossoverjie.top/2025/12/25/AI/deepwiki-rag-principle/"/>
    <id>http://crossoverjie.top/2025/12/25/AI/deepwiki-rag-principle/</id>
    <published>2025-12-25T17:56:51.000Z</published>
    <updated>2026-06-23T10:32:57.544Z</updated>
    
    <content type="html"><![CDATA[<p>上一篇文章：<a href="https://crossoverjie.top/2025/12/23/AI/LLM-app-concept/">大模型应用开发必需了解的基本概念</a> 分享了关于 LLM 大模型应用开发的一些基础知识，本文乘热打铁，借助一个真实的大模型应用来分析下其中的流程</p><h1 id="deepwiki-介绍"><a href="#deepwiki-介绍" class="headerlink" title="deepwiki 介绍"></a>deepwiki 介绍</h1><p>这里我们还是以 <a href="https://github.com/AsyncFuncAI/deepwiki-open/">deepwiki-open</a>为例进行分析。</p><p><img src="https://s2.loli.net/2025/12/24/uaLBwngDf5cyQMo.png"></p><p>通过这个截图可以知道它的主要功能：一键把任意 GitHub&#x2F;GitLab&#x2F;Bitbucket 仓库生成“可浏览的交互式 Wiki”</p><ul><li>支持 RAG 的问答，根据 repo 的现有内容进行问答。</li><li>支持多种模型（Google Gemini、OpenAI、OpenRouter、Azure OpenAI、本地 Ollama等）</li><li>支持 DeepResearch：多轮研究流程，自动迭代直至给出结构化结论（适合复杂问题）</li></ul><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>要使用也很简单，我们用一个兼容 openai 的 key 就可以使用了。</p><p>在 <code>.env</code> 里配置下相关环境变量：</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">OPENAI_API_KEY=&quot;xxcdxxe&quot;  <br>OPENAI_BASE_URL=&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;<br></code></pre></td></tr></table></figure><p>同时我们在 <code>generator.json</code> 里为 openai 新增一个你所使用的模型：</p><p><img src="https://s2.loli.net/2025/12/24/GnNhFULzrKE3g2X.png"><br>这样就可以在页面上选择这个模型了。</p><p><img src="https://s2.loli.net/2025/12/24/lwp5zyV3JedY4KZ.png"></p><p>同时我们还需要再 <code>embedder.json</code> 配置一个 embedding 模型，这个你的 LLM 提供商也会提供：</p><p><img src="https://s2.loli.net/2025/12/24/zYLm1ZV8QFrshbR.png"><br><img src="https://s2.loli.net/2025/12/24/6wFGvg4Uip9ZnNq.png"></p><blockquote><p>注意这里的 batch_size 需要修改为模型支持的大小</p></blockquote><p>然后我们便可以填入一个 repo 地址，系统会自动生成 wiki。</p><h1 id="流程"><a href="#流程" class="headerlink" title="流程"></a>流程</h1><p>官方提供了一个流程图如下：<br><img src="https://s2.loli.net/2025/12/24/7dnTIKvMRPyCEzu.png"></p><p>这个流程图略显粗糙，我整理一版更细的的流程如下：</p><ol><li>获取 repo 信息（前端）<br>    1.1. clone repo，同时在本地生成 RAG。<br>    1.2. 根据目录结构树和 readme 拼接内容传递给 AI 生成 wiki 的<strong>目录结构</strong>：<a href="%5Bhttps://github.com/AsyncFuncAI/deepwiki-open/blob/bb3c67d235e0f504c38ddf661dc3022fdc7ebcef/src/app/%5Bowner%5D/%5Brepo%5D/page.tsx#L712-L832%5D(https://github.com/AsyncFuncAI/deepwiki-open/blob/bb3c67d235e0f504c38ddf661dc3022fdc7ebcef/src/app/%5Bowner%5D/%5Brepo%5D/page.tsx#L712-L832)"> prompt </a></li></ol><p>        1.2.1. 通过【目录结构树和 readme】 先到 RAG 里查询具体的文档，然后再拼接与 system_prompt 拼接成一个完整 prompt 生成目录结构</p><p>        1.2.2. 如果 repo 过大导致目录树和 readme 的内容超过 token 限制，<strong>则不会去 RAG 里查询具体的内容来拼接生成目录结构</strong>，只会根据目录树和 readme 来生成，这样的目录结构信息可能会不全。</p><ol start="2"><li>根据<strong>目录结构</strong>拼接 prompt 生成每个目录的具体内容：<a href="%5Bhttps://github.com/AsyncFuncAI/deepwiki-open/blob/bb3c67d235e0f504c38ddf661dc3022fdc7ebcef/src/app/%5Bowner%5D/%5Brepo%5D/page.tsx#L419-L526%5D(https://github.com/AsyncFuncAI/deepwiki-open/blob/bb3c67d235e0f504c38ddf661dc3022fdc7ebcef/src/app/%5Bowner%5D/%5Brepo%5D/page.tsx#L419-L526)">prompt</a>（前端）<br>    2.1 根据前端提交的<strong>目录 prompt 执行 RAG 检索</strong>，找出需要查询的 document，根据 document 里的源码构建最终的发往 LLM 的 prompt（后端）<br>        3. 将文件分组，拼接成 <a href="%5Bhttps://github.com/AsyncFuncAI/deepwiki-open/blob/cdf06314e416074fe9750de36e0829f79497711e/api/websocket_wiki.py#L199-L223%5D(https://github.com/AsyncFuncAI/deepwiki-open/blob/cdf06314e416074fe9750de36e0829f79497711e/api/websocket_wiki.py#L199-L223)">context_text</a><br>    2.2. 与 system_prompt 拼接成一个完整 <a href="%5Bhttps://github.com/AsyncFuncAI/deepwiki-open/blob/cdf06314e416074fe9750de36e0829f79497711e/api/websocket_wiki.py#L407-L421%5D(https://github.com/AsyncFuncAI/deepwiki-open/blob/cdf06314e416074fe9750de36e0829f79497711e/api/websocket_wiki.py#L407-L421)">prompt</a>（后端）</li></ol><p> 4. 循环 2， 继续处理前端提交上来的目录结构 prompt。</p><p><img src="https://s2.loli.net/2025/12/24/qWrCBmNkx7pJUv4.png"></p><h2 id="生成本地本地向量数据库"><a href="#生成本地本地向量数据库" class="headerlink" title="生成本地本地向量数据库"></a>生成本地本地向量数据库</h2><p>第一步是 clone 我们指定的 repo，同时会读取该 repo 里的所有内容在本地生成一个向量数据库。</p><p>相关的关键代码：<br><img src="https://s2.loli.net/2025/12/24/7SchRv3zriZu4dI.png"><br><img src="https://s2.loli.net/2025/12/24/LxzyOKsM7RinagY.png"></p><h2 id="Spitter"><a href="#Spitter" class="headerlink" title="Spitter"></a>Spitter</h2><p>在生成向量之前我们还需要构建一个分词器，它用于将我们的文本切分为一个个 chunk，以便：</p><ul><li>避免超出模型&#x2F;嵌入接口的长度上限</li><li>提高检索命中率（更细粒度地召回与问题相关的片段）</li><li>减少无关上下文的干扰，提升回答质量</li></ul><p>在 deepwiki 里的配置如下：<br><img src="https://s2.loli.net/2025/12/24/CVtdMlnwZzuHDGT.png"></p><ul><li>split_by: “word”（按“词”维度切分）</li><li>chunk_size: 350（单块目标长度，约等于几百个词）</li><li>chunk_overlap: 100（相邻块的重叠长度，保证跨块语义连续）</li></ul><p>可能新手对 overlap 的作用不太清楚，它的好处是：</p><ul><li>代码或文档的关键信息可能跨越边界；设置 overlap 能让相邻块共享一部分上下文，减少“切断语义”的风险。</li></ul><p>对他的配置也需要按需使用：</p><ul><li>过大 overlap 会导致重复计算、存储和费用增加；过小可能丢失跨段语义。</li></ul><p>普通场景下 <code>text_splitter</code> 够用，但对于我们这种存代码的场景就需要使用特殊的 <code>Spitter</code> 了；主要问题是它不理解语言结构，容易把函数&#x2F;类等语义单元切断，导致检索召回片段不完整、上下文丢失。</p><table><thead><tr><th>Splitter 类型</th><th>核心思路</th><th>主要优点</th></tr></thead><tbody><tr><td>AST&#x2F;语法树型（<a href="https://pypi.org/project/code-splitter/">Tree-sitter</a>、<a href="https://developers.llamaindex.ai/python/framework/module_guides/loading/node_parsers/modules/#codesplitter">LlamaIndex CodeSplitter</a>）</td><td>按语言语法解析，按文件→模块→类&#x2F;函数→代码块分层切分</td><td>边界与语义单元对齐（函数&#x2F;类&#x2F;方法）；检索更精准；可附带符号名&#x2F;签名&#x2F;路径等元数据；减少“切断语义”导致的幻觉</td></tr><tr><td>语言&#x2F;模式感知启发式（<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 Recursive</a> + 语言分隔符）</td><td>维护各语言的分隔符（class&#x2F;def&#x2F;function&#x2F;export 等），先递归按分隔符切分，再做 token 约束</td><td>实现简单、跨语言容易落地；比纯词&#x2F;字符切分更稳；成本低、工程集成快</td></tr><tr><td>这两者的对比结果还在做测试，但都会比存文本分割好很多；具体对比结果可以参考后续的文章。</td><td></td><td></td></tr></tbody></table><h3 id="embedding"><a href="#embedding" class="headerlink" title="embedding"></a>embedding</h3><p>这里有一个关键的 embedding 操作，他是将我们的文字、语音、视频等非结构化数据转换为一个向量；类似于下面的代码：</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">import</span> os<br><span class="hljs-keyword">from</span> openai <span class="hljs-keyword">import</span> OpenAI<br><br>client = OpenAI(<br>    api_key=os.getenv(<span class="hljs-string">&quot;DASHSCOPE_API_KEY&quot;</span>),  <span class="hljs-comment"># 如果您没有配置环境变量，请在此处用您的API Key进行替换</span><br>    base_url=<span class="hljs-string">&quot;https://dashscope.aliyuncs.com/compatible-mode/v1&quot;</span>  <span class="hljs-comment"># 百炼服务的base_url</span><br>)<br><br>completion = client.embeddings.create(<br>    model=<span class="hljs-string">&quot;text-embedding-v4&quot;</span>,<br>    <span class="hljs-built_in">input</span>=<span class="hljs-string">&#x27;衣服的质量杠杠的，很漂亮，不枉我等了这么久啊，喜欢，以后还来这里买&#x27;</span>,<br>    dimensions=<span class="hljs-number">1024</span>, <span class="hljs-comment"># 指定向量维度（仅 text-embedding-v3及 text-embedding-v4支持该参数）</span><br>    encoding_format=<span class="hljs-string">&quot;float&quot;</span><br>)<br><br><span class="hljs-built_in">print</span>(completion.model_dump_json())<br></code></pre></td></tr></table></figure><p>而模型返回的数据如下：</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><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 json"><span class="hljs-punctuation">&#123;</span> <br>  <span class="hljs-attr">&quot;data&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;embedding&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-number">0.0023064255</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-number">-0.009327292</span><span class="hljs-punctuation">,</span><br>        .... <br>        <span class="hljs-number">-0.0028842222</span><br>      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;index&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;object&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;embedding&quot;</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;model&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;text-embedding-v4&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;object&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;list&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;usage&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">&#123;</span><span class="hljs-attr">&quot;prompt_tokens&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">23</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;total_tokens&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">23</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;f62c2ae7-0906-9758-ab34-47c5764f07e2&quot;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>在 deepwiki 这个项目里，我们没有主动调用这个接口，而是由 <a href="https://github.com/SylphAI-Inc/AdalFlow">AdalFlow</a> 这个库在内部完成的。</p><blockquote><p>也就是这行代码 <code>db.transform(key=&quot;split_and_embed&quot;)</code></p></blockquote><p>在 deepwiki 中我们只是简单将向量数据存放在了本地，实际生产使用时还需要将其存放到一个单独的向量数据库。</p><h2 id="生成目录"><a href="#生成目录" class="headerlink" title="生成目录"></a>生成目录</h2><p>然后就是与 AI 交互了，第一步是生成目录，类似于这样：</p><p><img src="https://s2.loli.net/2025/12/24/jFkGsBCP4Aa5VUq.png"></p><p><img src="https://s2.loli.net/2025/12/24/tSDVvaLTU1Eymbl.png"></p><p>系统生成的提示词如下，其实就是把 repo 的目录结构树+readme 文件的内容与 system_prompt 拼接成一个完整的提示词告诉 LLM，让它返回一个项目的目录结构。</p><p>主要是以下的一些要求：</p><ul><li>必须只返回 XML，根节点为 <code>&lt;wiki_structure&gt;</code>，以 <code>&lt;/wiki_structure&gt;</code> 结束</li><li>XML 必须语法有效、严格按指定结构</li><li>页面数量要求：<ul><li>综合模式（comprehensive）：8–12 个页面</li><li>精简模式（concise）：4–6 个页面</li></ul></li><li>内容语言由参数指定（英文、中文、日文等）</li><li>每个页面应聚焦代码库的一个具体方面（架构、特性、部署、前端&#x2F;后端模块等）</li><li><code>relevant_files</code> 必须是仓库中的真实文件路径，用于后续生成具体页面内容</li></ul><h2 id="每个目录的具体详情页"><a href="#每个目录的具体详情页" class="headerlink" title="每个目录的具体详情页"></a>每个目录的具体详情页</h2><p>目录生成完成之后就需要生成该页面的具体内容了，比如这个页面：<br><img src="https://s2.loli.net/2025/12/24/gsEO5th9B4CD21q.png"></p><p>他的提示词如下：<br><img src="https://s2.loli.net/2025/12/24/WY8iIKlzaEAney6.png"><br>其中的主要约束如下：</p><p>相关源文件清单（强制）</p><ul><li>在最顶部的 <details> 中逐条列出所有用于生成内容的源文件，且至少 5 个。</li><li>每个文件以 Markdown 链接形式出现（已给出可点击的仓库文件 URL）。</li><li>若输入文件少于 5 个，模型需“补足更多相关文件”。</li><li>Mermaid Diagrams（大量使用，强制规则）</li></ul><p>总结就是：结构合理、层次分明，便于理解与导航点击。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>以上就是对 deepwiki 项目的分析，作为一个典型的 RAG 应用，掌握它的流程便可以举一反三来实现其他类似的 RAG 应用。</p><p>当然其中有许多需要调优的地方，比如模型的选择、Spitter 参数的配置、RAG 召回 top_k 的配置等等。</p><p>还要平衡好效果与成本。<br>#Blog  </p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;上一篇文章：&lt;a href=&quot;https://crossoverjie.top/2025/12/23/AI/LLM-app-concept/&quot;&gt;大模型应用开发必需了解的基本概念&lt;/a&gt; 分享了关于 LLM 大模型应用开发的一些基础知识，本文乘热打铁，借助一个真实的大模型应用</summary>
      
    
    
    
    <category term="AI" scheme="http://crossoverjie.top/categories/AI/"/>
    
    
    <category term="AI" scheme="http://crossoverjie.top/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>大模型应用开发必需了解的基本概念</title>
    <link href="http://crossoverjie.top/2025/12/23/AI/LLM-app-concept/"/>
    <id>http://crossoverjie.top/2025/12/23/AI/LLM-app-concept/</id>
    <published>2025-12-23T17:56:51.000Z</published>
    <updated>2026-06-23T10:32:57.544Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>AI&#x2F;LLM 大模型最近几年毋庸置疑的是热度第一，虽然我日常一直在用 AI 提效，但真正使用大模型做一个应用的机会还是少。</p><p>最近正好有这么个机会，需要将公司内部的代码 repo 转换为一个 wiki，同时还可以基于项目内容进行对话了解更具体的内容。</p><p>实际效果大概和上半年很火的 <a href="https://deepwiki.com/redis/redis">deepwiki</a> 类似。</p><p><img src="https://s2.loli.net/2025/12/22/wq5hNKEHzGSDUQY.png"></p><p>而我们是想基于开源的 <a href="github.com/AsyncFuncAI/deepwiki-open">deepwiki-open</a>进行开发，提供的功能都是类似的。</p><p>在这个过程中我也从一个大模型应用开发的小白逐步理解了其中的一些关键概念，以及了解了一个大模型应用的运行原理。</p><span id="more"></span><h1 id="LLM"><a href="#LLM" class="headerlink" title="LLM"></a>LLM</h1><p>LLM（Large Language Model，大语言模型）大家应该都比较熟悉了：</p><ul><li>本质：一个通过海量文本训练出来的概率模型</li><li>能力：理解&#x2F;生成文本、代码，做推理、对话等</li><li>特点：<ul><li><strong>参数固定</strong>：训练完之后“记忆”是固化在参数里的</li><li><strong>知识有时间点</strong>：只知道训练截止前的数据（有知识截止时间）</li></ul></li></ul><p>可以把 <strong>LLM</strong> 当成一个“通用大脑”，但不一定知道最新的、你的私有数据。</p><p>目前的 AI 也就是大模型本质上还是概率预测，当你给它一段话（Prompt）时，它在后台做的事情是：<strong>“根据我读过的几万亿字，接在这段话后面，概率最高的下一个字（Token）是什么？”</strong></p><p>所以大模型每次回答的内容可能不同，也不能 100% 的告诉你准确答案。</p><h2 id="Token"><a href="#Token" class="headerlink" title="Token"></a>Token</h2><p>大模型并不直接认识<code>java</code>、<code>Rust</code> 或者“编程”这些词。在模型内部，所有的文字都会先被转换成一系列数字。</p><ul><li><strong>字&#x2F;词 ≠ Token</strong>：一个 Token 既不是一个字符，也不是一个单纯的单词。</li><li><strong>灵活切分</strong>：<ul><li>常见的词（如 <code>the</code>, <code>apple</code>）通常对应 <strong>1 个 Token</strong>。</li><li>罕见的词或长的复合词（如 <code>microservices</code>）可能会被拆分成几个 Token（如 <code>micro</code> + <code>services</code>）。</li><li>中文通常比较特殊：一个常用的汉字可能是 1 个 Token，但不常用的汉字可能会占用 2-3 个 Token。</li></ul></li></ul><p>在做大模型应用开发的时候尤其需要注意 token 的用量，毕竟这是计费的标准。</p><p>还有一个是上下文窗口的限制，每个模型都会有最大 token 的限制（如 8k, 32k, 128k）。</p><p>如果你的 Prompt 加上模型的回复超过了这个限制，模型就会丢掉前面的记忆或者直接报错。</p><p>在日常开发估算中，可以大概估算一下这个比例：</p><ul><li><strong>英文文本</strong>：1000 Tokens ≈ 750 个单词。</li><li><strong>中文文本</strong>：1000 Tokens ≈ 500 到 600 个汉字（随着模型词表的演进，现在的模型处理中文的效率在不断提升。）。</li><li><strong>代码</strong>：代码中的空格、缩进和特殊符号都会消耗 Token。Python 等由于缩进较多，消耗通常比纯文本快。</li></ul><p>也有相关的库可以帮我们计算 token：</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></pre></td><td class="code"><pre><code class="hljs python">  <br><span class="hljs-comment"># Choose encoding based on embedder type  </span><br><span class="hljs-keyword">if</span> embedder_type == <span class="hljs-string">&#x27;ollama&#x27;</span>:  <br>    <span class="hljs-comment"># Ollama typically uses cl100k_base encoding  </span><br>    encoding = tiktoken.get_encoding(<span class="hljs-string">&quot;cl100k_base&quot;</span>)  <br><span class="hljs-keyword">elif</span> embedder_type == <span class="hljs-string">&#x27;google&#x27;</span>:  <br>    <span class="hljs-comment"># Google uses similar tokenization to GPT models for rough estimation  </span><br>    encoding = tiktoken.get_encoding(<span class="hljs-string">&quot;cl100k_base&quot;</span>)  <br><span class="hljs-keyword">else</span>:  <span class="hljs-comment"># OpenAI or default  </span><br>    <span class="hljs-comment"># Use OpenAI embedding model encoding    </span><br>    encoding = tiktoken.encoding_for_model(<span class="hljs-string">&quot;text-embedding-3-small&quot;</span>)  <br>  <br><span class="hljs-keyword">return</span> <span class="hljs-built_in">len</span>(encoding.encode(text))<br></code></pre></td></tr></table></figure><p>也可以通过 <a href="https://platform.openai.com/tokenizer">openai</a> 的一个实例网站来可视化查看 token 的计算规则：<br><img src="https://s2.loli.net/2025/12/22/P7eXa4JAtCsTQYE.png"></p><h1 id="RAG"><a href="#RAG" class="headerlink" title="RAG"></a>RAG</h1><p>RAG 的全程是Retrieval-Augmented Generation（检索增强生成），他不是类似于 LLM 的模型，而是一种架构模式。</p><p>举个例子：<br>比如你问 ChatGPT 关于你们公司的某一个规章制度，大概率 ChatGPT 的训练语料是你没有你们公司的内部数据的。</p><p>所以他回复你的多半是瞎编的内容，或者直接告诉你不知道。</p><p>此时就需要 RAG 了，他可以在真正询问 LLM 之前先到内部的资料库里通过用户的问题将相关上下文查询出来，然后再拼接成一个完整的 prompt 发送给 LLM，让 LLM 根据你通过的数据进行回答。</p><p>这样能解决一下三个问题：</p><ol><li><strong>幻觉问题</strong>：你问它一个它不知道的事情，它会一本正经地胡说八道。</li><li><strong>知识过时</strong>：大模型的知识停留在它训练结束的那一天。</li><li><strong>私有数据安全</strong>：你不能为了让 AI 懂你的业务代码，就把几百万行私有代码全发给模型提供商训练一个新模型，那太贵且不安全。</li></ol><p>使用 RAG 时还需要额外考虑到数据清洗的步骤，比如我们这里的 repo wiki 的场景，我们需要把一些第三方库、编译后产生的 target 目录等不需要的内容排除掉。</p><p>避免在查询时带上这些内容，干扰最终的结果。</p><h1 id="向量数据库"><a href="#向量数据库" class="headerlink" title="向量数据库"></a>向量数据库</h1><p>上文里提到 RAG 模式，需要一个非常关键的组件，那就是向量数据库。</p><p>我们先要在 RAG 里检索出相关的上下文就是在向量数据库里做查询，具体流程如下：</p><ol><li><strong>把文档切块</strong>（段落级别）</li><li>用一个 <strong>Embedding 模型</strong> 把每个块转成向量</li><li>把这些向量存进 <strong>向量数据库</strong></li><li>用户提问时，也把问题转成向量</li><li>用向量相似度检索出最相关的文档块</li><li>把这些文档块 + 问题喂给 LLM，让它生成答案</li></ol><p>简单来说就是将一些非结构化的数据（图片、视频、文字）通过<strong>Embedding 模型</strong> 转换成一串数字数组，即<strong>向量</strong>（例如：<code>[0.12, -0.59, 0.88, ...]</code>）。</p><p>查询的时候也会将查询内容转换为向量，然后返回在向量空间里相近的数据。</p><h1 id="Q-amp-A"><a href="#Q-amp-A" class="headerlink" title="Q&amp;A"></a>Q&amp;A</h1><p>此时也许你会有以下一些问题：</p><p>LLM + RAG + 向量数据库，是不是类似于用 LLM 训练私有化数据？这两者的效果是否类似？ 如果不同，区别在哪里？</p><p>LLM + RAG + 向量数据库：</p><ul><li><p>本质是：</p><blockquote><p>不改模型参数，用<strong>检索到的外部资料</strong>来“喂”模型，让它<strong>查完再答</strong>。  </p></blockquote></li><li><p>你的数据在<strong>外部（向量数据库里）</strong>，只是当作参考材料塞进 prompt。</p></li></ul><p>在私有数据上训练（微调 &#x2F; 预训练）：</p><ul><li><p>本质是：</p><blockquote><p>用你的数据<strong>更新模型参数</strong>，让模型“记住”这些模式和知识。  </p></blockquote></li><li><p>你的数据被“烤进”模型权重里，调用时不需要再查这份数据。</p></li></ul><table><thead><tr><th>维度</th><th>RAG（向量库）</th><th>微调 &#x2F; 私有训练</th></tr></thead><tbody><tr><td>知识存放</td><td>外部向量库</td><td>模型参数里</td></tr><tr><td>更新成本</td><td>改文档即可，重建 &#x2F; 增量向量索引</td><td>需要重新训练部署</td></tr><tr><td>生效时间</td><td>几分钟级</td><td>训练+上线，小时～天级</td></tr><tr><td>支持频繁变更</td><td>很适合</td><td>很不适合</td></tr><tr><td>透明度&#x2F;可解释性</td><td><strong>高</strong>（可以追溯到原文出处）</td><td><strong>低</strong>（模型直接给出，无法确切知道来源）</td></tr></tbody></table><p>总的来说使用 RAG 外挂私有化向量数据的成本更低，也更灵活。<br>对于一些更垂直的场景，可以考虑使用私有数据训练模型。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>总体下来的感受是 LLM 应用大部分的<strong>代码</strong>都是 prompt 提示词，普通 app 的主要内容是代码，而不同大模型应用的主要区别是提示词；反而代码大部分都是趋同的。</p><p>区别就是用了什么框架，但是共同的就是调用大模型 API，将传统的 request&#x2F;reponse 的请求模式换为流式响应（大模型的响应很慢）。</p><p>在开发应用时，需要了解 <strong>System Prompt</strong>（系统预设角色）、<strong>User Prompt</strong>（用户提问）和 <strong>Few-shot</strong>（给模型几个例子引导它）。好的 Prompt 是让 RAG 结果准确的关键。</p><p>后续还需要更加完善 <code>deepwiki-open</code>：</p><ul><li>优化 splitter，使用更适合代码分割的 splitter，比如 <a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter</a></li><li>将存储在本地的向量替换为一个独立的向量数据库</li><li>持续优化提示词，更加符合我们的项目背景</li></ul><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&amp;#x2F;LLM 大模型最近几年毋庸置疑的是热度第一，虽然我日常一直在用 AI 提效，但真正使用大模型做一个应用的机会还是少。&lt;/p&gt;
&lt;p&gt;最近正好有这么个机会，需要将公司内部的代码 repo 转换为一个 wiki，同时还可以基于项目内容进行对话了解更具体的内容。&lt;/p&gt;
&lt;p&gt;实际效果大概和上半年很火的 &lt;a href=&quot;https://deepwiki.com/redis/redis&quot;&gt;deepwiki&lt;/a&gt; 类似。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2025/12/22/wq5hNKEHzGSDUQY.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;而我们是想基于开源的 &lt;a href=&quot;github.com/AsyncFuncAI/deepwiki-open&quot;&gt;deepwiki-open&lt;/a&gt;进行开发，提供的功能都是类似的。&lt;/p&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/"/>
    
  </entry>
  
  <entry>
    <title>持续剖析超级增强：将 Trace/ Span 和 Profile 整合打通</title>
    <link href="http://crossoverjie.top/2025/11/25/OpenTelemetry/combining-tracing-and-profiling-for-enhanced-observability-introducing-span-profiles/"/>
    <id>http://crossoverjie.top/2025/11/25/OpenTelemetry/combining-tracing-and-profiling-for-enhanced-observability-introducing-span-profiles/</id>
    <published>2025-11-25T17:56:51.000Z</published>
    <updated>2026-06-23T10:32:57.545Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>最近在做持续剖析 Profile 与链路系统打通的工作，就查到了 grafana 在 24 年初写的这篇文章；觉得比较有参考意义，在这里分享给大家。</p></blockquote><p>原文链接：<a href="https://grafana.com/blog/2024/02/06/combining-tracing-and-profiling-for-enhanced-observability-introducing-span-profiles/">https://grafana.com/blog/2024/02/06/combining-tracing-and-profiling-for-enhanced-observability-introducing-span-profiles/</a></p><p>在当今复杂的数据环境中，连续剖析（continuous profiling）已成为获取应用资源使用情况细粒度洞察的关键。Grafana Labs 现通过在 <a href="/blog/2024/01/23/grafana-10.3-release-canvas-panel-updates-multi-stack-data-sources-and-more/">Grafana 10.3</a> 中引入 Span Profiles 功能，将这工作持续推进。</p><span id="more"></span><p>Span Profiles 代表着剖析方法学上的一次重大转变，它让我们能够对追踪（tracing）和剖析（profiling）数据进行更深入的联合分析。传统的连续剖析是在固定时间区间内提供全局系统视角；相比之下，Span Profiles 可以对应用内部特定执行作用域（execution scope）进行更聚焦的分析，例如单个请求或某个特定的 trace span 分析它的 Profile。</p><p>这一转变带来了更精细的性能视角：通过将剖析数据与 trace 直接关联，帮助我们更全面地理解应用行为。由此，工程团队可以更高效地识别并解决性能瓶颈。</p><p><img src="https://s2.loli.net/2025/11/25/nPdSi97fwrvZY8M.png" alt="image.png"></p><p>在我们于 Grafana Labs 内部采用这一集成的 “trace-to-profile” 方法的第一个月中，CPU 利用率提升了 4 倍，对对象存储的 API 调用减少了 3 倍，同时还降低了成本（详见下文）——因此，我们非常高兴能向社区推出这一特性！</p><hr><h2 id="与-Grafana-Trace-视图集成：无缝体验"><a href="#与-Grafana-Trace-视图集成：无缝体验" class="headerlink" title="与 Grafana Trace 视图集成：无缝体验"></a>与 Grafana Trace 视图集成：无缝体验</h2><p>借助 Span Profiles，你可以在执行作用域<strong>内部</strong>挖掘具体的性能细节。比如，以前你只知道某个 span 花了 400ms，现在则能进一步了解：在这 400ms 里<strong>具体是哪部分代码</strong>在运行；从而更快的知道性能瓶颈</p><p><img src="https://s2.loli.net/2025/11/25/cC9VkwNpOSXqd8v.png" alt="image.png"></p><blockquote><p>使用 Span Profiles 的 flamegraph 截图。</p></blockquote><p>这种有针对性的方式，让你可以比以往更细粒度地剖析性能指标。通过聚焦于单个请求或单个 trace span，Span Profiles 为你提供了一扇直接洞察应用性能关键部分的窗口。</p><p>Span Profiles 与 Grafana <a href="/docs/grafana/latest/explore/trace-integration/?pg=blog&plcmt=body-txt/#trace-view">trace 视图</a> 的集成，为用户带来无缝体验：你可以轻松地从高层级的 trace 概览，切换到对某个具体 trace span 进行深入分析。</p><p>引入 Span Profiles 不仅是一次技术上的飞跃，同时也有非常可观的业务和投资回报（ROI）价值。</p><p>通过帮助团队更快地识别并解决性能问题，Span Profiles 减少了排障所需的时间和资源投入。这种效率的提升带来显著的成本节约，让 Span Profiles 成为既能优化应用性能，又能降低运维成本的有力工具。</p><hr><h2 id="Grafana-Labs-内部的真实案例"><a href="#Grafana-Labs-内部的真实案例" class="headerlink" title="Grafana Labs 内部的真实案例"></a>Grafana Labs 内部的真实案例</h2><p>为了更直观地展示 Span Profiles 的业务价值，下面是我们在 Grafana Labs 内部使用该特性的一个实际案例。</p><p>几个月前，<a href="/oss/pyroscope/https://grafana.com/oss/pyroscope/">Grafana Pyroscope</a> 团队（Pyroscope 是支撑 <a href="https://grafana.com/products/cloud/profiles-for-continuous-profiling/">Grafana Cloud Profiles</a> 的开源连续剖析数据库）在数据库架构中新增了 <a href="https://grafana.com/docs/pyroscope/latest/reference-pyroscope-architecture/components/compactor/?pg=blog&plcmt=body-txt">compactor 组件</a>，带来了显著的性能和成本收益。</p><p>compactor 会通过合并多个 block 来提升查询性能，并减少长期存储的使用。它在为每个租户将多个 block 压缩成单个优化 block 的过程中扮演关键角色，这不仅降低了存储成本，也加快了查询速度。</p><p><img src="https://s2.loli.net/2025/11/25/KB2n4rPfl657XdY.png"></p><blockquote><p>compactor 组件结构图。</p></blockquote><p>然而，压缩过程本身非常复杂——包括竖向压缩、横向压缩以及拆分与合并（split-and-merge）策略等多个阶段——这些都带来了一些挑战，尤其是与性能瓶颈相关的挑战。例如，在密集的压缩操作期间，CPU 和内存使用可能会出现明显峰值，存储 IO 需求也会显著增加，从而可能影响整体系统稳定性。此外，在拥有大量租户的大规模集群中，管理和优化这些大规模压缩任务所需的资源也非常复杂。而这正是 Span Profiles 功能展现其独特优势的地方。</p><p>通过对每一次压缩运行进行详细剖析，Span Profiles 能够在 trace 视图中直接提供按函数维度划分的 CPU 使用情况。这种与 trace 视图相结合的细节信息至关重要：它不仅能指出压缩过程的哪个阶段出现了瓶颈，还能告诉你每一次压缩影响到了哪些用户。</p><p><img src="https://s2.loli.net/2025/11/25/xdWDUFrKRZO3h7q.png"></p><blockquote><p>展示不同压缩操作对用户影响的 flamegraph。</p></blockquote><p>例如，我们发现由于符号信息的影响，一级（level 1）压缩是一个主要瓶颈；同时，我们也识别出每次运行中存在过度的 block 同步问题。有了这些数据，我们随之对压缩算法做出了有针对性的调整。<strong>改动带来的效果立竿见影：压缩时间减少了 4 倍，对对象存储的 API 调用量减少了 3 倍。</strong></p><p><img src="https://s2.loli.net/2025/11/25/EoS4wz6rinK5aAN.png"></p><blockquote><p>对象存储 API 调用量下降的仪表板截图。</p></blockquote><p>如果只看 GET 请求的减少，节省就已经非常可观。以 Google Cloud Storage Class B&#x2F;GET 的费用来计算，这些调整每月大约节省了 8,000 美元（计算方式为：0.0004 美元&#x2F;次 GET 请求 * 每分钟节省 400 次请求 * 60 分钟 * 24 小时 * 31 天）。</p><p>Span Profiles 功能为应用剖析翻开了新篇章。通过在特定执行作用域上提供详细洞察，它彻底改变了性能问题的识别与解决方式。</p><hr><h2 id="如何开始使用-Span-Profiles"><a href="#如何开始使用-Span-Profiles" class="headerlink" title="如何开始使用 Span Profiles"></a>如何开始使用 Span Profiles</h2><p>Span Profiles 目前已经在 <a href="/docs/grafana-cloud/?pg=blog&plcmt=body-txt">Grafana Cloud</a> 和 <a href="/docs/grafana/latest/whatsnew/whats-new-in-v10-3/?pg=blog&plcmt=body-txt">Grafana 10.3</a> 中提供。想要进一步了解这一特性，你可以参考我们的<a href="/docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/configure-tempo-data-source/#trace-to-profiles">技术文档</a>，以及以下入门资源：</p><ul><li><a href="/docs/pyroscope/latest/configure-client/?pg=blog&plcmt=body-txt">配置 Pyroscope 以发送剖析数据</a>  </li><li>配置客户端包以将 trace 与 profile 关联：  <ul><li><a href="https://github.com/grafana/otel-profiling-go">Go</a>  </li><li><a href="https://github.com/grafana/otel-profiling-ruby">Ruby</a>  </li><li><a href="https://github.com/grafana/otel-profiling-java">Java</a></li></ul></li><li><a href="/docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/configure-tempo-data-source/?pg=blog&plcmt=body-txt">配置 Tempo 以发现已关联的 traces 和 profiles</a></li></ul><p>更多关于 Span Profile 的具体使用案例会在继续更新。</p><p>#Blog </p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;最近在做持续剖析 Profile 与链路系统打通的工作，就查到了 grafana 在 24 年初写的这篇文章；觉得比较有参考意义，在这里分享给大家。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;原文链接：&lt;a href=&quot;https://grafana.com/blog/2024/02/06/combining-tracing-and-profiling-for-enhanced-observability-introducing-span-profiles/&quot;&gt;https://grafana.com/blog/2024/02/06/combining-tracing-and-profiling-for-enhanced-observability-introducing-span-profiles/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在当今复杂的数据环境中，连续剖析（continuous profiling）已成为获取应用资源使用情况细粒度洞察的关键。Grafana Labs 现通过在 &lt;a href=&quot;/blog/2024/01/23/grafana-10.3-release-canvas-panel-updates-multi-stack-data-sources-and-more/&quot;&gt;Grafana 10.3&lt;/a&gt; 中引入 Span Profiles 功能，将这工作持续推进。&lt;/p&gt;</summary>
    
    
    
    <category term="OB" scheme="http://crossoverjie.top/categories/OB/"/>
    
    <category term="OpenTelemetry" scheme="http://crossoverjie.top/categories/OB/OpenTelemetry/"/>
    
    
    <category term="OpenTelemetry" scheme="http://crossoverjie.top/tags/OpenTelemetry/"/>
    
  </entry>
  
  <entry>
    <title>StarRocks 如何监控 SQL</title>
    <link href="http://crossoverjie.top/2025/11/12/starrocks/StarRocks-SQL-monitor/"/>
    <id>http://crossoverjie.top/2025/11/12/starrocks/StarRocks-SQL-monitor/</id>
    <published>2025-11-12T17:56:51.000Z</published>
    <updated>2026-06-23T10:32:57.567Z</updated>
    
    <content type="html"><![CDATA[<p>StarRocks 监控中有一个很关键的指标，就是针对慢 SQL 的监控。</p><p>在 StarRocks 中审计日志记录了所有用户的查询和连接信息，理论上我们只需要对这些日志进行分析就可以得到相关的慢 SQL，高 CPU、高内存的 SQL 信息。</p><p>类似于这样的监控界面：<br><img src="https://s2.loli.net/2025/11/12/CJEwAz732myrd9H.png"></p><p><img src="https://s2.loli.net/2025/11/12/5l9ND3Bcve2GWb7.png"></p><p><img src="https://s2.loli.net/2025/11/12/UTyBDHtgX1Ariwm.png"><br><img src="https://s2.loli.net/2025/11/12/SVpLiCIZvf4Bj2u.png"></p><span id="more"></span><p>由于这些数据都是存放在日志文件里的我们想把他拿到 <code>grafna</code> 里展示的话得额外处理下。</p><h1 id="结构化日志"><a href="#结构化日志" class="headerlink" title="结构化日志"></a>结构化日志</h1><p><a href="https://docs.starrocks.io/zh/docs/administration/management/logs/#feauditlog">默认情况</a>下审计日志是以文本格式输出的，当然我们也可以使用一个<a href="https://github.com/StarRocks/fe-plugins-auditloader">审计插件</a>，将审计日志写入到一张单独的表里供后续分析，也可以实现类似的效果。</p><p>具体使用可以参考<a href="!%5B%5D(https://s2.loli.net/2025/11/12/WSwBQc964Jqbt5M.png)">官方文档</a>：<br><img src="https://s2.loli.net/2025/11/12/WSwBQc964Jqbt5M.png"></p><p>这里我们选择一个更简单的方法，我们可以将日志输出为 JSON 格式，然后再将其结构化，为每个字段创建索引，存入到单独的日志服务里。</p><p><img src="https://s2.loli.net/2025/11/12/faJ8VBU5S6Kb2vs.png"></p><p>由于我们使用了云厂商的日志服务能力，只需要为这个日志文件（<strong>fe&#x2F;log&#x2F;fe.audit.log</strong>）配置一个采集服务，然后为其中的字段创建索引即可。</p><p><img src="https://s2.loli.net/2025/11/12/nAfM7zPs3aFVo4L.png"></p><p>这样我们可以就可以通过云厂商提供的 grafna 插件将这里的日志作为一个数据源集成到 grafna 中。</p><p>之后就可以在 <code>grafna</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">IsQuery:&quot;true&quot;<span class="hljs-operator">|</span><span class="hljs-keyword">SELECT</span> QueryId,Stmt,<span class="hljs-type">Time</span>,CpuCostNs,MemCostBytes,ScanBytes,ScanRows,ReturnRows <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> CpuCostNs <span class="hljs-keyword">DESC</span> LIMIT <span class="hljs-number">10</span><br></code></pre></td></tr></table></figure><p>比如这样的查询语句含义是：限制为查询的 SQL（还有其他的 alter delete 等 SQL）、按照 CPU 耗时排序。</p><figure class="highlight sql"><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></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">CREATE TABLE</span> starrocks_audit_db__.starrocks_audit_tbl__ (<br>  `queryId` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">64</span>) COMMENT &quot;查询的唯一ID&quot;,<br>  `<span class="hljs-type">timestamp</span>` DATETIME <span class="hljs-keyword">NOT NULL</span> COMMENT &quot;查询开始时间&quot;,<br>  `queryType` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">12</span>) COMMENT &quot;查询类型（query, slow_query, connection）&quot;,<br>  `clientIp` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">32</span>) COMMENT &quot;客户端IP&quot;,<br>  `<span class="hljs-keyword">user</span>` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">64</span>) COMMENT &quot;查询用户名&quot;,<br>  `authorizedUser` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">64</span>) COMMENT &quot;用户唯一标识，既user_identity&quot;,<br>  `resourceGroup` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">64</span>) COMMENT &quot;资源组名&quot;,<br>  `catalog` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">32</span>) COMMENT &quot;数据目录名&quot;,<br>  `db` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">96</span>) COMMENT &quot;查询所在数据库&quot;,<br>  `state` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">8</span>) COMMENT &quot;查询状态（EOF，ERR，OK）&quot;,<br>  `errorCode` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">512</span>) COMMENT &quot;错误码&quot;,<br>  `queryTime` <span class="hljs-type">BIGINT</span> COMMENT &quot;查询执行时间（毫秒）&quot;,<br>  `scanBytes` <span class="hljs-type">BIGINT</span> COMMENT &quot;查询扫描的字节数&quot;,<br>  `scanRows` <span class="hljs-type">BIGINT</span> COMMENT &quot;查询扫描的记录行数&quot;,<br>  `returnRows` <span class="hljs-type">BIGINT</span> COMMENT &quot;查询返回的结果行数&quot;,<br>  `cpuCostNs` <span class="hljs-type">BIGINT</span> COMMENT &quot;查询CPU耗时（纳秒）&quot;,<br>  `memCostBytes` <span class="hljs-type">BIGINT</span> COMMENT &quot;查询消耗内存（字节）&quot;,<br>  `stmtId` <span class="hljs-type">INT</span> COMMENT &quot;SQL语句增量ID&quot;,<br>  `isQuery` TINYINT COMMENT &quot;SQL是否为查询（1或0）&quot;,<br>  `feIp` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">128</span>) COMMENT &quot;执行该语句的FE IP&quot;,<br>  `stmt` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">1048576</span>) COMMENT &quot;SQL原始语句&quot;,<br>  `digest` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">32</span>) COMMENT &quot;慢SQL指纹&quot;,<br>  `planCpuCosts` <span class="hljs-keyword">DOUBLE</span> COMMENT &quot;查询规划阶段CPU占用（纳秒）&quot;,<br>  `planMemCosts` <span class="hljs-keyword">DOUBLE</span> COMMENT &quot;查询规划阶段内存占用（字节）&quot;,<br>  `pendingTimeMs` <span class="hljs-type">BIGINT</span> COMMENT &quot;查询在队列中等待的时间（毫秒）&quot;,<br>  `candidateMVs` <span class="hljs-type">varchar</span>(<span class="hljs-number">65533</span>) <span class="hljs-keyword">NULL</span> COMMENT &quot;候选MV列表&quot;,<br>  `hitMvs` <span class="hljs-type">varchar</span>(<span class="hljs-number">65533</span>) <span class="hljs-keyword">NULL</span> COMMENT &quot;命中MV列表&quot;,<br>  `warehouse` <span class="hljs-type">VARCHAR</span>(<span class="hljs-number">128</span>) <span class="hljs-keyword">NULL</span> COMMENT &quot;仓库名称&quot; <br>)<br></code></pre></td></tr></table></figure><p>审计数据里的信息非常丰富，可以组合出各种查询条件。</p><p>比如：</p><ul><li>限制查询时间大于多少，可以只查询慢 SQL</li><li>根据内存占用排序</li><li>执行失败的 SQL</li></ul><p>大家可以按需选择。</p><h2 id="开源替换"><a href="#开源替换" class="headerlink" title="开源替换"></a>开源替换</h2><p>如果没有使用云厂商，一些开源组件也能满足以上需求：</p><ul><li>结构化存储日志</li><li>根据字段创建索引</li><li>类 SQL 查询</li><li>支持 grafna 数据源，方便做可视化</li></ul><table><thead><tr><th>服务名称</th><th>索引能力</th><th>查询语言</th><th>Grafana 支持</th><th>优点</th><th>缺点</th><th>适用场景</th></tr></thead><tbody><tr><td><a href="https://www.elastic.co/elasticsearch/"><strong>Elasticsearch</strong></a></td><td>全文索引、倒排索引</td><td>Elasticsearch DSL（类 SQL）</td><td>官方插件，集成完善</td><td>功能强大、生态成熟、搜索能力强</td><td>资源占用大、运维成本高、JVM 调优复杂</td><td>大规模日志搜索、全文检索</td></tr><tr><td><a href="https://grafana.com/oss/loki/"><strong>Loki</strong></a></td><td>仅索引标签（Label）</td><td>LogQL</td><td>原生支持，集成最佳</td><td>轻量级、成本低、与 Grafana 生态完美</td><td>全文搜索能力弱、不适合复杂查询</td><td>中小规模、成本敏感、Grafana 用户</td></tr><tr><td><a href="https://clickhouse.com/"><strong>ClickHouse</strong></a></td><td>主键索引、二级索引、跳数索引</td><td>标准 SQL</td><td>官方插件支持</td><td>查询速度极快、擅长 OLAP、支持复杂聚合</td><td>不适合高频更新、需要结构化设计</td><td>结构化日志分析、大数据量聚合查询</td></tr><tr><td><a href="https://opensearch.org/"><strong>OpenSearch</strong></a></td><td>全文索引、倒排索引</td><td>OpenSearch DSL（兼容 ES）</td><td>兼容 ES 插件</td><td>完全开源、无商业限制、功能与 ES 相近</td><td>社区相对较小、资源占用仍较大</td><td>ES 的开源替代方案</td></tr></tbody></table><h1 id="内部日志支持-JSON"><a href="#内部日志支持-JSON" class="headerlink" title="内部日志支持 JSON"></a>内部日志支持 JSON</h1><p>StarRocks 还有定期执行的内部 SQL，目前这些 SQL 也是会记录日志，但只是记录的纯文本，无法很好的对其进行监控。</p><p>我们就出现过内部 SQL 大量占用了 CN 的 CPU 资源，将它的执行时间控制到 <code>00:00:00~08:00:00</code> 之后就会好很多了，只是不会影响到白天的业务使用。</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><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 java"><span class="hljs-comment">/**  </span><br><span class="hljs-comment"> * The start time of day when auto-updates are enabled */</span><span class="hljs-meta">@ConfField(mutable = true)</span>  <br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">String</span> <span class="hljs-variable">statistic_auto_analyze_start_time</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;00:00:00&quot;</span>;  <br>  <br><span class="hljs-comment">/**  </span><br><span class="hljs-comment"> * The end time of day when auto-updates are enabled */</span><span class="hljs-meta">@ConfField(mutable = true)</span>  <br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">String</span> <span class="hljs-variable">statistic_auto_analyze_end_time</span> <span class="hljs-operator">=</span> <span class="hljs-string">&quot;23:59:59&quot;</span>;<br></code></pre></td></tr></table></figure><p>主要是这两个配置控制的时间范围，修改之后 CN CPU 使用有明显的下降：<br><img src="https://s2.loli.net/2025/11/13/U6xir9sYCnVLhjg.png"></p><p>为了方便后续对这部分内部 SQL 进行监控，提交了一个 <a href="https://github.com/StarRocks/starrocks/pull/64660">PR</a> 用于支持内部日志输出 JSON，这样采集日志之后就可以参考上面的审计日志对内部 SQL 进行监控了。</p><p>#Blog </p>]]></content>
    
    
    <summary type="html">&lt;p&gt;StarRocks 监控中有一个很关键的指标，就是针对慢 SQL 的监控。&lt;/p&gt;
&lt;p&gt;在 StarRocks 中审计日志记录了所有用户的查询和连接信息，理论上我们只需要对这些日志进行分析就可以得到相关的慢 SQL，高 CPU、高内存的 SQL 信息。&lt;/p&gt;
&lt;p&gt;类似于这样的监控界面：&lt;br&gt;&lt;img src=&quot;https://s2.loli.net/2025/11/12/CJEwAz732myrd9H.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2025/11/12/5l9ND3Bcve2GWb7.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2025/11/12/UTyBDHtgX1Ariwm.png&quot;&gt;&lt;br&gt;&lt;img src=&quot;https://s2.loli.net/2025/11/12/SVpLiCIZvf4Bj2u.png&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="OB" scheme="http://crossoverjie.top/categories/OB/"/>
    
    <category term="StarRocks" scheme="http://crossoverjie.top/categories/OB/StarRocks/"/>
    
    
    <category term="StarRocks" scheme="http://crossoverjie.top/tags/StarRocks/"/>
    
  </entry>
  
</feed>
