<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>AI Coding on Code is cheap, let&#39;s talk</title>
    <link>https://blog.ferstar.org/series/ai-coding/</link>
    <description>Code is cheap, let&#39;s talk</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>zh-CN</language>
    <copyright>Copyright 2026 ferstar</copyright>
    <lastBuildDate>Mon, 22 Jun 2026 12:00:00 +0800</lastBuildDate>
    <ttl>60</ttl><atom:link href="https://blog.ferstar.org/series/ai-coding/index.xml" rel="self" type="application/rss+xml" /><image>
      <url>https://blog.ferstar.org/site-logo.png</url>
      <title>Code is cheap, let&#39;s talk</title>
      <link>https://blog.ferstar.org/</link>
    </image>
    
    <item>
      <title>把 Codex 工作流养成活系统：从会话扫描到 Skills</title>
      <link>https://blog.ferstar.org/posts/codex-workflow-skills-feedback-loop/</link>
      <pubDate>Mon, 22 Jun 2026 12:00:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/codex-workflow-skills-feedback-loop/</guid>
      <description>Codex 长期使用后会积累大量重复排查和交付动作；扫描本机会话，找出高频摩擦点，再沉淀成 skills、脚本和跨机器同步；让工作流自己持续变轻。</description><content:encoded><![CDATA[<p>前几天干了一件很土的事：把本机 <code>~/.codex</code> 里的会话记录扫了一遍。</p>
<p>不是怀旧，也不是做数据面板。就想看看自己到底在哪些地方反复浪费时间。</p>
<p>猜也猜得到。真正耗人的不是某次代码改得难，是一堆小动作每天都得来一遍：</p>
<ul>
<li><code>git status</code>、<code>git diff</code>、<code>glab api</code>、<code>glab mr</code></li>
<li>查 CI 第一个挂掉的 job</li>
<li>远端主机查 SSH、PATH、Tailscale、权限</li>
<li>判断这次改动该跑哪组测试</li>
<li>release / deploy 前确认 SHA、workflow、artifact</li>
<li>中途续会话重新捋 issue、branch、MR</li>
</ul>
<p>这些事都太小了，小到你懒得专门去管。但就是因为小，才一直被放过。结果就是每天泡在这些手工胶水里。</p>
<p>这件事最开始的提示词其实很短：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">根据我最近 codex 的项目和线程，帮我提出一些可以简化项目流程和提升效率的方法，使用子代理分头分析。</span></span></code></pre></div></div>
<p>第一轮结果还是太偏最近几个项目，于是又补了一句：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">不止这几个项目，扫描 ~/.codex 下所有可能的会话，分派多个 sub agent 分头分析，最后汇总。</span></span></code></pre></div></div>
<p>重点不是“让模型想点优化建议”，而是把分析对象从印象里的项目，换成真实会话里的重复动作。</p>

<h3 class="relative group">别急着写工具
    <div id="别急着写工具" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%88%ab%e6%80%a5%e7%9d%80%e5%86%99%e5%b7%a5%e5%85%b7" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>我以前也这样——看见重复就觉得该写脚本。后来发现这一步常常太早。</p>
<p>很多重复不是命令本身重复，是判断过程重复。比如 CI 挂了，真正该固化的不是某条 <code>gh run view</code>，而是：</p>
<ol>
<li>先确认 run 和 head SHA</li>
<li>找第一个挂掉的 job</li>
<li>把有效错误截出来</li>
<li>再判断是 workflow 的问题、依赖的问题、测试还是代码</li>
</ol>
<p>一上来就写个大工具，容易把错误假设焊进去。轻一点的做法是先写成 skill：什么时候用、最小几步、别干什么、输出什么。</p>
<p>skill 不是百科全书，就是张便签——让 agent 少走一条死路。</p>
<pre class="not-prose mermaid">
flowchart LR
  A[Session history] --> B[Repeated friction]
  B --> C[Small skill]
  C --> D[Run on real tasks]
  D --> E[Script only when repeated]
  E --> C
</pre>

<p>最后只留了这几个：</p>
<ul>
<li><code>agent-preflight</code>：开工前读真实 repo 状态，不靠印象</li>
<li><code>gitlab-mr-context</code>：用 <code>glab api</code> 拉 issue / MR / pipeline / notes，稳得多</li>
<li><code>ci-first-failure</code>：先找第一个真实失败点，再动代码</li>
<li><code>path-verify</code>：按改动的文件选最小验证命令</li>
<li><code>release-deploy-preflight</code>：部署前确认 full SHA、workflow、artifact、健康检查</li>
<li><code>remote-health</code>：远端主机先查 SSH、PATH、服务、锁和 Tailscale</li>
</ul>
<p>名字都不酷，好处是不用想就知道该什么时候用。</p>

<h3 class="relative group">skill 先行，脚本后补
    <div id="skill-先行脚本后补" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#skill-%e5%85%88%e8%a1%8c%e8%84%9a%e6%9c%ac%e5%90%8e%e8%a1%a5" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>还有一个教训：别一上来就给每个 skill 配脚本目录。</p>
<p>很多流程写个 <code>SKILL.md</code> 就够用了。<code>path-verify</code> 不是替你跑测试，是提醒你按变更路径选最小检查。让它先跟 agent 在真实任务里跑几轮，自动化等确认了再说。</p>
<p>脚本只干一类事：已经确定重复、确定机械、确定低风险的。</p>
<p>这轮我只加了一个——把 repo 里的 skills 链接到用户目录：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">scripts/link-user-skills.sh</span></span></code></pre></div></div>
<p>Windows 补了个 PowerShell 版：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="p">.\</span><span class="n">scripts</span><span class="p">\</span><span class="nb">link-user</span><span class="n">-skills</span><span class="p">.</span><span class="n">ps1</span></span></span></code></pre></div></div>
<p>中间踩了个坑：软链接方向。</p>
<p>正确方向是 repo 放真实文件，用户目录放链接：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">~/.agents/skills/glab -> /path/to/repo/skills/glab</span></span></code></pre></div></div>
<p>这样 repo 能提交真实内容，本机 Codex 也能直接用。搞反了就糟了——repo 里只剩一个指向 <code>~/.agents</code> 的链接，推上去别人拿不到内容，Git 还以为原文件被删了。</p>

<h3 class="relative group">让它跨机器能跑
    <div id="让它跨机器能跑" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%ae%a9%e5%ae%83%e8%b7%a8%e6%9c%ba%e5%99%a8%e8%83%bd%e8%b7%91" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>我常年在 macOS、Windows、远端之间切。skill 要是只在一台机器上能用，价值直接打折。</p>
<p>所以本机搞完之后，把 repo 同步到 <code>my-win</code>，Windows 上也跑同一套维护。PowerShell 版用的是 directory junction 而不是 symlink——Windows 上建 symlink 经常跟权限干架，junction 对目录链接已经够用了。</p>
<p>挺琐碎的一步。但不做的话，工作流沉淀很快退化成一台机器的偏方。</p>

<h3 class="relative group">我现在这样想
    <div id="我现在这样想" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%88%91%e7%8e%b0%e5%9c%a8%e8%bf%99%e6%a0%b7%e6%83%b3" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>做了这轮以后，几个想法慢慢变硬了。</p>
<p>先从会话里找重复，别从想象里设计系统。如果 <code>git status</code>、<code>glab api</code>、<code>ssh</code>、<code>pnpm test</code> 真的是高频，就从它们开刀。别为了"流程治理"编一套没人用的东西出来。</p>
<p>skill 要短。一个只堵一个口子。它唯一的作用是让 agent 少问一次、少查一次、少猜一次。别往里面塞百科全书。</p>
<p>脚本只做机械活——链接 skills、采 CI 日志、远端健康检查。产品判断、风险边界、要不要部署，该留人的确认就留，至少留个显式 preflight。</p>
<p>错误得回炉。软链接方向我一上来就搞反了。修完以后经验不能只停在对话窗口里，得落到脚本和 README 里，不然下次还犯。</p>

<h3 class="relative group">最后留下的
    <div id="最后留下的" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9c%80%e5%90%8e%e7%95%99%e4%b8%8b%e7%9a%84" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>不多：</p>
<ul>
<li>几个短 skill</li>
<li>一个 Bash 链接脚本</li>
<li>一个 PowerShell 链接脚本</li>
<li>一次 Windows 同步确认</li>
<li>一条规则：repo 放真实 skill，用户目录放链接</li>
</ul>
<p>够了。</p>
<p>越来越觉得，AI coding 的工作流不是造一个大平台。是把最烦人的五分钟，一遍一遍地拿掉。每次少一点，系统就轻一点。这些小规则攒够了，agent 才像是在一个配好的工程环境里干活，而不是每次从野地里重新开路。</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>用 SQLite Trigger 给 Codex 日志库止血</title>
      <link>https://blog.ferstar.org/posts/codex-sqlite-log-trigger/</link>
      <pubDate>Sat, 20 Jun 2026 16:55:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/codex-sqlite-log-trigger/</guid>
      <description>Codex 本地 SQLite 日志库增长太快；用一个 BEFORE INSERT trigger 暂时拦住新日志写入，并保留恢复命令，快速降低磁盘 IO 和 WAL 增长。</description><content:encoded><![CDATA[<p>Codex 最近把本地日志写进 <code>~/.codex/logs_2.sqlite</code>，我的库已经涨到 1GB 以上。真正占空间的不是 WAL 文件，而是日志表本身：<code>TRACE</code>、<code>DEBUG</code>、<code>INFO</code> 一直进 SQLite，时间一长就没必要地消耗磁盘和 IO。</p>
<p>官方配置里能调的东西有限：<code>RUST_LOG</code> 可以降日志级别，<code>log_dir</code> 只管明文 TUI log，<code>history.max_bytes</code> 只影响 <code>history.jsonl</code>。我没找到公开的 <code>logs_2.sqlite</code> retention、max size 或 journal mode 配置。</p>
<p>所以先用 SQLite 自己的机制止血。</p>

<h3 class="relative group">一条 trigger 拦住新增日志
    <div id="一条-trigger-拦住新增日志" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%80%e6%9d%a1-trigger-%e6%8b%a6%e4%bd%8f%e6%96%b0%e5%a2%9e%e6%97%a5%e5%bf%97" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sqlite3 ~/.codex/logs_2.sqlite <span class="s2">"CREATE TRIGGER IF NOT EXISTS block_log_inserts BEFORE INSERT ON logs BEGIN SELECT RAISE(IGNORE); END;"</span></span></span></code></pre></div></div>
<p>这条 trigger 的意思很直接：每次有人往 <code>logs</code> 表插入数据时，SQLite 直接忽略这次插入。</p>
<p>验证也很简单：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sqlite3 ~/.codex/logs_2.sqlite <span class="s2">"
</span></span></span><span class="line"><span class="cl"><span class="s2">SELECT count(*) FROM logs;
</span></span></span><span class="line"><span class="cl"><span class="s2">INSERT INTO logs(ts, ts_nanos, level, target, feedback_log_body, estimated_bytes)
</span></span></span><span class="line"><span class="cl"><span class="s2">VALUES(strftime('%s','now'), 0, 'INFO', 'trigger_test', 'should_not_exist', 1);
</span></span></span><span class="line"><span class="cl"><span class="s2">SELECT count(*) FROM logs;
</span></span></span><span class="line"><span class="cl"><span class="s2">SELECT count(*) FROM logs WHERE target='trigger_test';
</span></span></span><span class="line"><span class="cl"><span class="s2">"</span></span></span></code></pre></div></div>
<p>如果前后行数一样，并且 <code>trigger_test</code> 是 <code>0</code>，说明生效。</p>

<h3 class="relative group">Windows PowerShell 版本
    <div id="windows-powershell-版本" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#windows-powershell-%e7%89%88%e6%9c%ac" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Windows 上路径通常是：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$db</span> <span class="p">=</span> <span class="nb">Join-Path</span> <span class="nv">$env:USERPROFILE</span> <span class="s2">".codex\logs_2.sqlite"</span>
</span></span><span class="line"><span class="cl"><span class="n">sqlite3</span> <span class="nv">$db</span> <span class="s2">"CREATE TRIGGER IF NOT EXISTS block_log_inserts BEFORE INSERT ON logs BEGIN SELECT RAISE(IGNORE); END;"</span></span></span></code></pre></div></div>
<p>我在远端 Windows 机器上验证过，测试 insert 前后行数不变：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">trigger: block_log_inserts
</span></span><span class="line"><span class="cl">before: 76387
</span></span><span class="line"><span class="cl">after: 76387
</span></span><span class="line"><span class="cl">trigger_test_rows: 0</span></span></code></pre></div></div>

<h3 class="relative group">恢复日志写入
    <div id="恢复日志写入" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%81%a2%e5%a4%8d%e6%97%a5%e5%bf%97%e5%86%99%e5%85%a5" aria-label="锚点">#</a>
    </span>
    
</h3>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sqlite3 ~/.codex/logs_2.sqlite <span class="s2">"DROP TRIGGER IF EXISTS block_log_inserts;"</span></span></span></code></pre></div></div>
<p>PowerShell：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$db</span> <span class="p">=</span> <span class="nb">Join-Path</span> <span class="nv">$env:USERPROFILE</span> <span class="s2">".codex\logs_2.sqlite"</span>
</span></span><span class="line"><span class="cl"><span class="n">sqlite3</span> <span class="nv">$db</span> <span class="s2">"DROP TRIGGER IF EXISTS block_log_inserts;"</span></span></span></code></pre></div></div>

<h3 class="relative group">顺手压缩旧日志
    <div id="顺手压缩旧日志" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%a1%ba%e6%89%8b%e5%8e%8b%e7%bc%a9%e6%97%a7%e6%97%a5%e5%bf%97" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>trigger 只拦新增，不会自动缩小已经膨胀的库。退出 Codex 后再做一次 checkpoint 和 <code>VACUUM</code>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sqlite3 ~/.codex/logs_2.sqlite <span class="s2">"
</span></span></span><span class="line"><span class="cl"><span class="s2">PRAGMA wal_checkpoint(TRUNCATE);
</span></span></span><span class="line"><span class="cl"><span class="s2">DELETE FROM logs WHERE level IN ('TRACE','DEBUG');
</span></span></span><span class="line"><span class="cl"><span class="s2">DELETE FROM logs WHERE level = 'INFO' AND ts < strftime('%s','now','-3 days');
</span></span></span><span class="line"><span class="cl"><span class="s2">VACUUM;
</span></span></span><span class="line"><span class="cl"><span class="s2">"</span></span></span></code></pre></div></div>
<p>如果 Codex 还开着，SQLite 可能会报 <code>database is locked</code>。这不是坏事，关掉 Codex 再跑。</p>

<h3 class="relative group">这个办法的边界
    <div id="这个办法的边界" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%bf%99%e4%b8%aa%e5%8a%9e%e6%b3%95%e7%9a%84%e8%be%b9%e7%95%8c" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>它不是“修复 Codex 日志系统”，只是本机止血。</p>
<p>好处是不用改 Codex，不用等版本发布，也不用写守护脚本。坏处是之后 <code>logs_2.sqlite</code> 里不会再有新日志，本地排障能力会下降。需要排障时删掉 trigger，复现问题，再重新加回来。</p>
<p>长期看，正确做法还是 Codex 自己提供日志库 retention 或 max-size 配置。但在那之前，一个 SQLite trigger 已经够用了。</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>ACE 靠不住了：ace-wrapper 之后我换了个方式做语义检索</title>
      <link>https://blog.ferstar.org/posts/ace-wrapper-to-fast-context/</link>
      <pubDate>Mon, 01 Jun 2026 10:35:09 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/ace-wrapper-to-fast-context/</guid>
      <description>ace-wrapper 写完后，ACE 越来越难薅，中转也纷纷扑街；不如直接逆向 Windsurf 的 SWE-grep 协议，再加本地 Semble 缓存做降级，最后做成一个混合检索工具 fast-context。</description><content:encoded><![CDATA[<p><a href="/posts/ace-wrapper-semantic-search-ai-coding-harness/" >上一篇</a> 我写过 <code>ace-wrapper</code>：把 ACE（Augment Context Engine）的 filesystem context search 包成一个 shell 命令，让 agent 在关键词不明确时先走语义检索，再决定读哪些文件。</p>
<p>结果 ACE 开始不稳定了。</p>
<p>API key 换着花样失效，免费额度越来越难薅，几个中转服务也一个个扑街。</p>
<p>这也怪不了谁，毕竟本来就是 preview 功能。问题在于，编码助手的工作流已经长在语义检索上了：一天几十次 <code>ace</code> 调用，少了它，agent 又回到盲猜关键词的老路。</p>
<p>所以我换了个办法：</p>
<p><a href="https://github.com/ferstar/fast-context"  target="_blank" rel="noreferrer">ferstar/fast-context</a></p>
<p>这次我没走第三方 API，而是直接逆向 Windsurf 的 SWE-grep 协议——也就是 Codex CLI 和 Windsurf IDE 自己在用的那个语义搜索后端——同时在本地加了一层 Semble 缓存做降级。</p>

<h3 class="relative group">结构跟 ace-wrapper 最大的区别
    <div id="结构跟-ace-wrapper-最大的区别" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%bb%93%e6%9e%84%e8%b7%9f-ace-wrapper-%e6%9c%80%e5%a4%a7%e7%9a%84%e5%8c%ba%e5%88%ab" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>ace-wrapper 是纯远程调用：本地只传参数，一切靠 ACE 服务。</p>
<p>fast-context 则是本地和远端一起上。</p>
<pre class="not-prose mermaid">
flowchart TB
  subgraph Input
    Q[User query]
  end

  subgraph Local
    S[Semble local prefetch<br/>cached index + chunk search]
    A[Lexical anchors<br/>filename / path / literal hits]
    R["Repo map<br/>(auto-shrink when too large)"]
  end

  subgraph Remote
    WS[Windsurf SWE-grep<br/>agentic verify + expand]
  end

  subgraph Output
    O["Candidate files<br/>line ranges<br/>follow-up terms<br/>(or local chunks when remote fails)"]
  end

  Q --> S
  Q --> A
  Q --> R
  S --> WS
  A --> WS
  R --> WS
  WS -- success --> O
  WS -- auth / rate-limit / timeout --> O
  S -- fallback path --> O
</pre>

<p>流程变成：</p>
<ol>
<li><strong>先在本地跑 Semble</strong>——缓存的索引+ chunk 搜索，毫秒级返回命中</li>
<li><strong>收集本地 lexical anchors</strong>——精确的文件名、路径片段、内容中的字面量匹配</li>
<li><strong>生成 repo map</strong>——代码树结构，太大了就自动压缩</li>
<li><strong>把这三样打包发给 Windsurf</strong>——Semble 的 chunk 候选当提示，lexical anchors 当锚点，repo map 给路径上下文</li>
<li><strong>Windsurf 用 rg/readfile/tree/ls/glob 验证和扩展</strong>——agent 层的工具调用循环</li>
<li><strong>远端走不通时，直接返回本地 Semble 结果</strong>——不空手，不卡住</li>
</ol>
<p>这个“不空手”其实很关键。ace-wrapper 依赖 ACE 时，服务一挂，那一轮搜索就没了。现在远端断了，本地缓存至少还能给出 chunk 级别的候选，质量差一点，但工作流不会直接卡死。</p>

<h3 class="relative group">逆向 SWE-grep 的过程
    <div id="逆向-swe-grep-的过程" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e9%80%86%e5%90%91-swe-grep-%e7%9a%84%e8%bf%87%e7%a8%8b" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Windsurf 的 SWE-grep 走的是 Connect-RPC + Protobuf，和典型的 REST API 完全不是一回事。</p>
<p>最麻烦的是 Connect 协议的帧编码。每个 RPC 帧前有个 5 字节头（1 字节 flag + 4 字节大端长度），请求和响应都这么包。协议本身还要求先发一条 Connect-Connect 帧，然后才是实际数据。</p>
<p>Protobuf 这边更烦。Windsurf 用的是自定义 proto schema，公开定义找不到。核心数据结构的 field numbers 只能从抓包或已知的 Wireshark 解密配置里猜——比如调用链 <code>{1: name, 2: args, 3: id}</code>、变量定义 <code>{1: name, 2: type, 3: value}</code>。猜错就整个请求失败，而且没有什么友好的报错。</p>
<p>整个编码器大概这样（<a href="https://github.com/ferstar/fast-context/blob/main/src/core.py#L64"  target="_blank" rel="noreferrer">ProtobufEncoder</a>）：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ProtobufEncoder</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="s2">"""手动 protobuf 编码器，完全匹配 Windsurf 的请求格式。"""</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">buf</span> <span class="o">=</span> <span class="nb">bytearray</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_varint</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">value</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bytes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">parts</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">        <span class="k">while</span> <span class="n">value</span> <span class="o">></span> <span class="mh">0x7F</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">value</span> <span class="o">&</span> <span class="mh">0x7F</span><span class="p">)</span> <span class="o">|</span> <span class="mh">0x80</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">value</span> <span class="o">>>=</span> <span class="mi">7</span>
</span></span><span class="line"><span class="cl">        <span class="n">parts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">value</span> <span class="o">&</span> <span class="mh">0x7F</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">parts</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_tag</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">wire</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bytes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_varint</span><span class="p">((</span><span class="n">field</span> <span class="o"><<</span> <span class="mi">3</span><span class="p">)</span> <span class="o">|</span> <span class="n">wire</span><span class="p">)</span></span></span></code></pre></div></div>
<p>反过来，接 Windsurf 返回的流式响应也得自己解码——拆帧、读数据、找流结束标志——最后才能拿到语义结果。比调 REST API 麻烦得多，但好处也明显：不需要任何中间服务，直接打 Windsurf 后端。</p>

<h3 class="relative group">本地 Semble 缓存为什么管用
    <div id="本地-semble-缓存为什么管用" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%9c%ac%e5%9c%b0-semble-%e7%bc%93%e5%ad%98%e4%b8%ba%e4%bb%80%e4%b9%88%e7%ae%a1%e7%94%a8" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>当初加 Semble 之前，我其实犹豫过：本地建一份索引，会不会是多此一举？</p>
<p>后来 benchmark 一跑，这事就没悬念了。</p>
<p>我拿 40 条标注查询在两个仓库上跑了对比（fastapi 和 axios），结果是：</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">Backend</th>
          <th style="text-align: right">NDCG@10</th>
          <th style="text-align: right">Recall@10</th>
          <th style="text-align: right">Top-1</th>
          <th style="text-align: right">Batch p50</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">local（仅 Semble）</td>
          <td style="text-align: right">0.854</td>
          <td style="text-align: right">0.946</td>
          <td style="text-align: right">0.775</td>
          <td style="text-align: right">30 ms</td>
      </tr>
      <tr>
          <td style="text-align: left">remote（仅 Windsurf）</td>
          <td style="text-align: right">0.453</td>
          <td style="text-align: right">0.467</td>
          <td style="text-align: right">0.450</td>
          <td style="text-align: right">24.4 s</td>
      </tr>
      <tr>
          <td style="text-align: left">hybrid（Semble + Windsurf）</td>
          <td style="text-align: right">0.890</td>
          <td style="text-align: right">0.979</td>
          <td style="text-align: right">0.825</td>
          <td style="text-align: right">28.3 s</td>
      </tr>
  </tbody>
</table>
<p>本地 Semble 自己的召回率已经 94.6%，p50 只有 30 毫秒。Windsurf 单独跑反而有点拉——成功率只有 52.5%，剩下的不是被限流，就是报 <code>resource_exhausted</code>。</p>
<p>hybrid 模式则是把 Windsurf 放到 Semble 结果后面做验证和扩展，NDCG@10 涨到 0.890，召回率升到 97.9%。</p>
<p>这个结果让我确定了两件事：</p>
<ul>
<li><strong>本地缓存不是备胎，是第一道防线</strong>。它在 30 毫秒内能搞定绝大部分常见搜索，远端挂了就是降级路径，而不是直接废掉。</li>
<li><strong>Windsurf 的价值在验证，不在首轮搜索</strong>。直接让它从头搜，容易超时或被限流；给它 Semble 的 chunk 候选和精确的关键词锚点后，它只需要在已知问题上做确认，成功率明显高很多。</li>
</ul>

<h3 class="relative group">凭据处理也比之前复杂了
    <div id="凭据处理也比之前复杂了" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%87%ad%e6%8d%ae%e5%a4%84%e7%90%86%e4%b9%9f%e6%af%94%e4%b9%8b%e5%89%8d%e5%a4%8d%e6%9d%82%e4%ba%86" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>ace-wrapper 只需要一个 API key。fast-context 拿的是 Windsurf 的 session token，存在本地的 <code>state.vscdb</code>（SQLite 数据库）。</p>
<p>提取逻辑在 <a href="https://github.com/ferstar/fast-context/blob/main/src/extract_key.py"  target="_blank" rel="noreferrer">extract_key.py</a>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">从 state.vscdb 的 ItemTable 里查 key 为 'windsurf.api_key' 的行
</span></span><span class="line"><span class="cl">→ 如果有，直接返回
</span></span><span class="line"><span class="cl">→ 如果没查到，再查 key 包含 'devin-session-token' 的行
</span></span><span class="line"><span class="cl">→ 两种格式都能用
</span></span><span class="line"><span class="cl">→ 也可以通过 WINDSURF_API_KEY 环境变量覆盖</span></span></code></pre></div></div>
<p>为什么两种格式都要支持？因为 Windsurf 自己就在变。前期是标准 API key，后来改成了 <code>devin-session-token$...</code> 这种 session 风格的凭据。不跟着变，用户升级 IDE 后工具就废了。</p>

<h3 class="relative group">现在的工作流
    <div id="现在的工作流" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%8e%b0%e5%9c%a8%e7%9a%84%e5%b7%a5%e4%bd%9c%e6%b5%81" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>ace-wrapper 阶段，我的 AGENTS.md 长这样：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">用 ace 做语义检索找候选文件 → 读文件 → 用 rg 确认精确证据</span></span></code></pre></div></div>
<p>现在改成了：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">用 fast-context search（默认 hybrid）找候选文件 + 行号范围
</span></span><span class="line"><span class="cl">如果 hybrid 超时或无结果，试试 fast-context local-search
</span></span><span class="line"><span class="cl">如果有 chunk 候选想看相关代码，用 fast-context find-related
</span></span><span class="line"><span class="cl">读完文件后用 rg/ast-grep 确认精确证据</span></span></code></pre></div></div>
<p>路径是多了几条，但每条都知道失败后该往哪退。</p>
<p>远程也搭了一套模型 fallback 链：</p>
<ol>
<li>默认用 <code>MODEL_SWE_1_6_FAST</code></li>
<li>遇到 <code>resource_exhausted</code> 或限流，自动降级到 <code>MODEL_SWE_1_5</code></li>
<li>还能通过 <code>WS_FALLBACK_MODELS</code> 自定义 fallback 顺序</li>
</ol>

<h3 class="relative group">Benchmark 数据
    <div id="benchmark-数据" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#benchmark-%e6%95%b0%e6%8d%ae" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>用 fair runner（completion-based cooldown, 40 queries）重新跑 benchmark 后，几个指标更能说明问题：</p>
<ul>
<li><strong>hybrid 模式非空输出率 100%</strong>——40 条查询全部返回了有效结果</li>
<li><strong>remote 模式非空输出率只有 50%</strong>——剩下一半要么超时要么被限流</li>
<li><strong>local 模式零失败</strong>——100% 非空，p50 延迟 30 ms</li>
</ul>
<p>这意味着，如果纯靠远程语义搜索，高峰期可能一半查询直接没响应。hybrid 模式有本地 Semble 打底后，最差也会给出本地 chunk，而不是空结果。</p>

<h3 class="relative group">这次暂时不想再推翻重来的几个点
    <div id="这次暂时不想再推翻重来的几个点" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e8%bf%99%e6%ac%a1%e6%9a%82%e6%97%b6%e4%b8%8d%e6%83%b3%e5%86%8d%e6%8e%a8%e7%bf%bb%e9%87%8d%e6%9d%a5%e7%9a%84%e5%87%a0%e4%b8%aa%e7%82%b9" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>这次重写有几个架构选择，目前看来是对的：</p>
<ol>
<li><strong>始终保持降级路径</strong>。任何远程依赖都得有本地回退。ACE 那次已经吃过亏了。</li>
<li><strong>纯 Python 更好维护</strong>。ace-wrapper 也是 Python，但这次代码量从几百行涨到了两千多行——有 protobuf 编码器、Connect 帧协议、Semble 适配层、benchmark runner——结构清楚比语言选型重要得多。Python 只是我最顺手。</li>
<li><strong>Benchmark 要跟代码一起放</strong>。<a href="https://github.com/ferstar/fast-context/tree/main/benchmarks/"  target="_blank" rel="noreferrer">benchmarks/</a> 里的 40 条标注查询和 runner，跑一次就能看到各个 backend 的真实差异。没有数据支撑的优化决策，基本靠猜。</li>
<li><strong>凭据提取要自动适配</strong>。<code>devin-session-token</code> 这种变化是预料之外的，但代码结构上留了扩展点——查不到 key 就换个 pattern 再查一次，不用改主流程。</li>
</ol>

<h3 class="relative group">收尾
    <div id="收尾" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%94%b6%e5%b0%be" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>ace-wrapper 到现在我还在用——ACE 偶尔又能通了。但我已经不想把工作流绑死在它上面。</p>
<p>fast-context 的核心思路其实很简单：语义搜索先靠本地缓存托底，远端负责验证和补充。纯远程方案一旦上游抽风，就容易断绳。</p>
<p>如果你也踩过这个坑，代码在这：<a href="https://github.com/ferstar/fast-context"  target="_blank" rel="noreferrer">ferstar/fast-context</a></p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>把语义检索放进 AI Coding Harness：ace-wrapper 开源小记</title>
      <link>https://blog.ferstar.org/posts/ace-wrapper-semantic-search-ai-coding-harness/</link>
      <pubDate>Sat, 09 May 2026 14:38:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/ace-wrapper-semantic-search-ai-coding-harness/</guid>
      <description>AI coding 长任务常卡在不知道该读哪里；用 ace-wrapper 把语义检索放进 Read -&gt; Search -&gt; Change -&gt; Verify；让 agent 先找候选文件、再验证证据，减少盲改和上下文浪费。</description><content:encoded><![CDATA[<p><a href="/posts/ai-coding-harness-engineering-workflow/" >上一篇</a>写 Harness Engineering 时，我把 AI coding 的默认顺序压成了几步：</p>
<ol>
<li>Read</li>
<li>Search</li>
<li>Change</li>
<li>Verify</li>
<li>Record</li>
</ol>
<p>这里面最容易被低估的是 <code>Search</code>。</p>
<p>很多 agent 失败，第一步就错了：读错地方。用户说的是一个行为、一个 bug、一个跨层流程，代码里却不一定有同名函数。直接 <code>rg login</code>、<code>rg upload</code>、<code>rg session</code> 很快，但它只适合已知关键词。关键词都不知道时，快只会更快地跑偏。</p>
<p>所以我把最近常用的一小层工具开源了：</p>
<p><a href="https://github.com/ferstar/ace-wrapper"  target="_blank" rel="noreferrer">ferstar/ace-wrapper</a></p>
<p>它做的事很窄：把 Augment Context Engine 的 filesystem context search 包成一个 <code>ace</code> 命令，让 coding agent 可以在 shell 里先做语义检索。</p>

<h3 class="relative group">为什么需要这一层
    <div id="为什么需要这一层" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e4%b8%ba%e4%bb%80%e4%b9%88%e9%9c%80%e8%a6%81%e8%bf%99%e4%b8%80%e5%b1%82" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>我关心的点很具体：把搜索动作放进 harness。</p>
<p>以前我经常看到这种路径：</p>
<pre class="not-prose mermaid">
flowchart LR
  A[User describes behavior] --> B[Agent guesses keywords]
  B --> C[Reads nearby files]
  C --> D[Edits plausible code]
  D --> E[Verification fails]
  E --> B
</pre>

<p>这个循环的问题是，失败后 agent 往往会继续围着同一批错误文件打转。它有修改能力，缺的是更好的候选文件入口。说白了，一开始摸错门，后面再努力也容易越走越偏。</p>
<p><code>ace-wrapper</code> 想补的是这里：</p>
<pre class="not-prose mermaid">
flowchart LR
  A[User describes behavior] --> B[ace semantic retrieval]
  B --> C[Candidate files]
  C --> D[Read returned files]
  D --> E[rg / tests confirm evidence]
  E --> F[Small patch]
  F --> G[Verify]
</pre>

<p>这里的关键是顺序：<code>ace</code> 只负责找候选文件。真正的证据仍然来自读文件、精确搜索和测试。它的定位很小，就是帮 agent 少走一点冤枉路。</p>

<h3 class="relative group">用法很短
    <div id="用法很短" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e7%94%a8%e6%b3%95%e5%be%88%e7%9f%ad" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>安装：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv tool install ace-wrapper</span></span></code></pre></div></div>
<p>本地开发版：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">uv tool install /path/to/ace-wrapper</span></span></code></pre></div></div>
<p>查一个不知道关键词的工作流：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">timeout 60s ace <span class="s2">"user uploads an unsupported file and should see skipped-file feedback"</span> -w /repo
</span></span><span class="line"><span class="cl">rg -n <span class="s2">"unsupported|skipped|upload|file"</span> /repo</span></span></code></pre></div></div>
<p>第一条命令回答“可能在哪些文件”。第二条命令确认“代码里到底有没有这些标识、事件、文案或测试”。</p>
<p>我一般会把这段规则放进项目的 <code>AGENTS.md</code>：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Use `timeout 60s ace "<query>" -w <repo-root>` for semantic codebase discovery.
</span></span><span class="line"><span class="cl">Treat `ace` results as candidate files.
</span></span><span class="line"><span class="cl">After it returns results, read the relevant files and use exact search before using them as evidence.</span></span></code></pre></div></div>
<p>这几行比“多读上下文”更有用，因为它给了 agent 一个具体动作，也给了防止误判的边界。</p>

<h3 class="relative group">它和 rg 怎么配合
    <div id="它和-rg-怎么配合" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%83%e5%92%8c-rg-%e6%80%8e%e4%b9%88%e9%85%8d%e5%90%88" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><code>ace</code> 和 <code>rg</code> 更适合前后配合使用。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">场景</th>
          <th style="text-align: left">先用什么</th>
          <th style="text-align: left">为什么</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">不知道实现在哪里，只知道用户行为</td>
          <td style="text-align: left"><code>ace</code></td>
          <td style="text-align: left">行为描述能跨文件、跨命名找到候选入口</td>
      </tr>
      <tr>
          <td style="text-align: left">知道函数名、事件名、错误文案</td>
          <td style="text-align: left"><code>rg</code></td>
          <td style="text-align: left">精确、完整、可枚举</td>
      </tr>
      <tr>
          <td style="text-align: left">要做结构性重构</td>
          <td style="text-align: left"><code>ast-grep</code></td>
          <td style="text-align: left">需要 AST 级别匹配，不能靠文本近似</td>
      </tr>
      <tr>
          <td style="text-align: left">要确认一个功能是否存在</td>
          <td style="text-align: left"><code>ace</code> + 读文件 + <code>rg</code></td>
          <td style="text-align: left">语义命中不能证明功能存在</td>
      </tr>
  </tbody>
</table>
<p>我特意在 README 里写了边界：ACE 只生成候选文件，证据还要从代码和测试里确认。这个边界很重要。</p>
<p>语义检索会返回“相近”的东西。你问一个不存在的功能，它也可能找出看起来相关的文件。如果 agent 把“有结果”理解成“功能存在”，后面就会开始编故事。只有读到实现、测试、路由、配置或调用点，结论才算站得住。</p>

<h3 class="relative group">它在 Harness Engineering 里的位置
    <div id="它在-harness-engineering-里的位置" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%ae%83%e5%9c%a8-harness-engineering-%e9%87%8c%e7%9a%84%e4%bd%8d%e7%bd%ae" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><code>ace-wrapper</code> 很小，也不该变成平台。它更像 harness 里的一个小齿轮：把“开放式找代码”这件事变成可重复、可约束的命令。</p>
<p>我现在更喜欢这样的项目规则：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Read -> Search -> Change -> Verify</span></span></code></pre></div></div>
<p>其中 <code>Search</code> 要按问题类型选工具：</p>
<ul>
<li>开放式、行为式、跨层链路：先 <code>ace</code></li>
<li>精确标识、报错、路由、配置：用 <code>rg</code></li>
<li>结构性替换：用 <code>ast-grep</code></li>
<li>外部策略和行业做法：用 web research</li>
<li>旧决策、历史踩坑：用 memory</li>
</ul>
<p>这套分工能减少 agent 的随机性。它先用语义检索缩小读文件范围，再用确定性工具确认事实，最后才动代码。顺序看起来啰嗦一点，但比一上来改错文件省事太多。</p>

<h3 class="relative group">对 agent 来说，最重要的是提示方式
    <div id="对-agent-来说最重要的是提示方式" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%af%b9-agent-%e6%9d%a5%e8%af%b4%e6%9c%80%e9%87%8d%e8%a6%81%e7%9a%84%e6%98%af%e6%8f%90%e7%a4%ba%e6%96%b9%e5%bc%8f" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>好的 <code>ace</code> query 要把行为讲完整，不能只堆关键词：</p>
<div class="highlight-wrapper"><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">timeout 60s ace <span class="s2">"frontend sends requestId to backend and starts a processing job"</span> -w /repo
</span></span><span class="line"><span class="cl">timeout 60s ace <span class="s2">"用户拖入不支持的文件后应该显示跳过文件提示"</span> -w /repo
</span></span><span class="line"><span class="cl">timeout 60s ace <span class="s2">"how provider config is persisted and restored after app restart"</span> -w /repo</span></span></code></pre></div></div>
<p>我会尽量包含四类信息：</p>
<ul>
<li>用户动作：点击、拖拽、上传、停止生成</li>
<li>运行边界：frontend to backend、CLI handler to core service</li>
<li>预期效果：persist config、abort loop、show skipped-file feedback</li>
<li>已知字段：<code>sessionId</code>、<code>requestId</code>、<code>files</code>、<code>workspace</code></li>
</ul>
<p>这比只搜 <code>upload</code> 或 <code>provider</code> 稳得多。它让检索系统按行为和数据流找入口，也提醒 agent：这一步还只是语义检索，不能直接当证据。</p>

<h3 class="relative group">开源它的原因
    <div id="开源它的原因" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%bc%80%e6%ba%90%e5%ae%83%e7%9a%84%e5%8e%9f%e5%9b%a0" aria-label="锚点">#</a>
    </span>
    
</h3>
<p><code>ace-wrapper</code> 的代码量很小，核心就是 <code>FileSystemContext.create(str(workspace))</code> 加 <code>context.search(args.query)</code>。我更想保存的是这几行 Python 周围的工作流约束：</p>
<ol>
<li>不知道关键词时，先语义检索</li>
<li>一次 query 只问一个工作流</li>
<li>把结果当候选文件</li>
<li>读文件后再用 <code>rg</code> 确认精确证据</li>
<li>没证据就不要下结论</li>
</ol>
<p>这些规则放进工具 README、skill 和 agent prompt 后，才会稳定生效。否则每个会话都会重新靠人提醒一遍，提醒多了人也烦。</p>
<p>上一篇说 Harness Engineering 是给 AI 外面套工程轨道。<code>ace-wrapper</code> 就是其中一小段轨道：它不让 agent 更会写代码，只是让它更容易先读对地方。</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>从 Vibe Coding 到 Harness Engineering：AI Coding 的工作流进化</title>
      <link>https://blog.ferstar.org/posts/ai-coding-harness-engineering-workflow/</link>
      <pubDate>Sat, 09 May 2026 14:19:00 +0800</pubDate>
      
      <guid isPermaLink="true">https://blog.ferstar.org/posts/ai-coding-harness-engineering-workflow/</guid>
      <description>AI coding 能生成代码但长期交付容易失控；用 Harness Engineering 管住任务、上下文、验证和恢复；让 AI 输出进入可执行、可验证、可复盘的工程流程。</description><content:encoded><![CDATA[<p>这篇是一次组内分享的文字版，slides 在这里：</p>
<p><a href="/slides/harness-engineering-ai-coding/" >从 Vibe Coding 到 Harness Engineering</a></p>
<div style="position:relative;width:100%;aspect-ratio:16/9;margin:1.5rem 0 2rem;border:1px solid rgba(127,127,127,.25);overflow:hidden;">
  <iframe src="/slides/harness-engineering-ai-coding/" title="从 Vibe Coding 到 Harness Engineering" style="position:absolute;inset:0;width:100%;height:100%;border:0;" loading="lazy" allowfullscreen></iframe>
</div>
<p>前阵子我一直在看一件事：AI 到底能不能承担大部分编码工作。</p>
<p>现在答案基本不悬了。项目上下文、质量门禁、验证流程都跟得上时，AI 生成的代码可以稳定进入工程流程。人的时间会从“手写代码”慢慢挪到“把关”：拆需求、判断架构、整理上下文、验边界、处理失败。</p>
<p>最近这轮实践又往前走了一点。问题从 prompt 怎么写得更漂亮，变成了整个工作流能不能扛住长任务。这个坑我也踩了不少，尤其是早上打开电脑发现 agent 跑了一夜，但很难判断哪些改动该留的时候。</p>

<h3 class="relative group">变化在哪里
    <div id="变化在哪里" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%8f%98%e5%8c%96%e5%9c%a8%e5%93%aa%e9%87%8c" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>早期的 Vibe Coding 解决的是入口问题：把需求说清楚，把项目规则写进 <code>AGENTS.md</code> / <code>CLAUDE.md</code>，再用测试、lint、review 接住模型输出。</p>
<p>这套方法仍然好用，只是更偏单次任务。任务一长，毛病就开始冒头：</p>
<ul>
<li>上下文越塞越多，模型反而抓不到重点</li>
<li>失败后继续重试，容易把问题越修越偏</li>
<li>外部资料没查清，策略靠感觉拍脑袋</li>
<li>跑了很多轮，人醒来不知道哪些变化该保留</li>
<li>用户拒绝、权限阻断、空输出这类状态没有明确停止语义</li>
</ul>
<p>所以我现在更愿意把这层东西叫做 Harness Engineering：给 AI 外面套一段工程轨道，让任务可执行、结果可验证、失败可恢复。名字听起来有点大，其实就是少相信一点“它会自己搞定”，多给几根护栏。</p>
<pre class="not-prose mermaid">
flowchart LR
  A[Task scope] --> B[Context route]
  B --> C[Agent loop]
  C --> D[Verification gate]
  D --> E[Recovery / memory]
  D -->|failed| F[Patch harness]
  F --> C
</pre>


<h3 class="relative group">我会先管这四件事
    <div id="我会先管这四件事" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e6%88%91%e4%bc%9a%e5%85%88%e7%ae%a1%e8%bf%99%e5%9b%9b%e4%bb%b6%e4%ba%8b" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>第一件事是任务边界。</p>
<p>中型任务开始前，至少写清楚 <code>done when</code>、<code>out of scope</code>、改动面和验证命令。不需要长文档，很多时候 5 行就够。重点是让执行侧知道什么时候该停，少一点“顺手再优化一下”。</p>
<p>第二件事是上下文路由。</p>
<p><code>AGENTS.md</code> 不适合写成百科全书。它更适合当索引：项目规则是什么，入口在哪里，验证命令是什么，哪些东西不能碰，下一层文档去哪读。真正的长上下文按需打开，不要整包塞回会话里。塞太满以后，模型会像我开太多浏览器标签页一样，看起来很努力，实际已经找不到重点。</p>
<p>第三件事是验证闭环。</p>
<p>我现在默认按这个顺序推进：</p>
<ol>
<li>Read：读 README、AGENTS、旧文、关键实现</li>
<li>Search：用 <code>ace</code>、<code>rg</code>、<code>ast-grep</code>、<code>nmem</code>、Exa 找证据</li>
<li>Change：小范围 patch，少做顺手重构</li>
<li>Verify：先跑窄测试，再按风险扩大</li>
<li>Record：把反复踩坑写回规则、测试或 memory</li>
</ol>
<p>这个顺序很朴素，但能压住很多失控场景。先读和先查，可以少一点模型脑补；先窄测，可以避免一口气改太大，最后谁也不知道哪一步坏了。</p>
<p>第四件事是失败处理。</p>
<p>失败后先分类型：停、重试、补 harness，还是沉淀记忆。</p>
<table>
  <thead>
      <tr>
          <th style="text-align: left">类型</th>
          <th style="text-align: left">什么时候用</th>
          <th style="text-align: left">处理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left">停</td>
          <td style="text-align: left">用户拒绝、权限阻断、有副作用、重复空转</td>
          <td style="text-align: left">断开 loop，交还控制权</td>
      </tr>
      <tr>
          <td style="text-align: left">重试</td>
          <td style="text-align: left">网络抖动、参数可修、读取失败且无副作用</td>
          <td style="text-align: left">小步重试，保留日志</td>
      </tr>
      <tr>
          <td style="text-align: left">补</td>
          <td style="text-align: left">同类错误第二次出现</td>
          <td style="text-align: left">补测试、规则、脚本或日志</td>
      </tr>
      <tr>
          <td style="text-align: left">记</td>
          <td style="text-align: left">以后还会遇到</td>
          <td style="text-align: left">留触发条件、验证命令和证据入口</td>
      </tr>
  </tbody>
</table>
<p>我以前会把很多失败都当成“再试一次”。现在会谨慎一点：能重试的问题才重试，该停的问题就得停。让 agent 带着错的前提硬冲，通常只会生成更多需要人收拾的 diff。</p>

<h3 class="relative group">外部资料怎么进来
    <div id="外部资料怎么进来" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%a4%96%e9%83%a8%e8%b5%84%e6%96%99%e6%80%8e%e4%b9%88%e8%bf%9b%e6%9d%a5" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>这轮工作流里，Exa 或类似 web search 工具的位置也更清楚了。</p>
<p>我一般不查宏观趋势，更常查具体工程问题：</p>
<ul>
<li>超时应该设多少</li>
<li>失败要不要重试</li>
<li>默认策略怎么拆</li>
<li>主流工具给了哪些边界</li>
<li>真实 issue 里暴露了哪些失败样本</li>
</ul>
<p>查完也不照搬。外部资料只给参照系，最后还要回到当前 repo 的约束里取舍。真正有用的结论，要落到 spec、项目规则、测试或脚本里。不然下次遇到同类问题，还是会再查一遍，等于把时间花两次。</p>

<h3 class="relative group">Autoresearch 和 Ralph Loop
    <div id="autoresearch-和-ralph-loop" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#autoresearch-%e5%92%8c-ralph-loop" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>Autoresearch 更适合有明确指标的长循环。先给 agent 一个目标、一个 guard、一个验证命令，每轮只允许一个可回滚变化。这样它跑偏时，损失还能控制住。</p>
<p>Ralph Loop 我现在理解成“持久单负责人执行”。同一个 owner 负责推进，先有 PRD 和 test spec，再让 agent 跑长任务。它更关心长任务里的上下文、判断和验证线索，不急着把更多 agent 同时拉进来。人少一点，有时反而更容易知道责任在哪里。</p>
<p>这两种做法的共同点是：先定义轨道，再让 agent 跑。轨道里必须有指标、边界、验证，以及哪些改动保留、哪些改动丢弃的规则。</p>

<h3 class="relative group">先抄三步就够
    <div id="先抄三步就够" class="anchor"></div>
    
    <span
        class="absolute top-0 w-6 transition-opacity opacity-0 -start-6 not-prose group-hover:opacity-100 select-none">
        <a class="text-primary-300 dark:text-neutral-700 !no-underline" href="#%e5%85%88%e6%8a%84%e4%b8%89%e6%ad%a5%e5%b0%b1%e5%a4%9f" aria-label="锚点">#</a>
    </span>
    
</h3>
<p>如果要把这套方法挪到团队里，我建议先别急着平台化。明天就能抄三步：</p>
<ol>
<li>每个中型任务写清 <code>done when</code> 和 <code>out of scope</code></li>
<li>让 agent 先列文件、证据和改动面，确认后再允许修改</li>
<li>失败一次后先补测试、规则或脚本，再继续让 agent 跑</li>
</ol>
<p>这三步做完，AI coding 的体验会从“能产出”往“能交付”挪一点。后面再谈 autoresearch、Ralph Loop、team worker、memory，心里也会更有底。</p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
