<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Alex</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://alex-tangt.github.io/</id>
  <link href="https://alex-tangt.github.io/" rel="alternate"/>
  <link href="https://alex-tangt.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Alex</rights>
  <subtitle>A bilingual tech blog</subtitle>
  <title>Tech Blog</title>
  <updated>2026-09-15T04:00:00.000Z</updated>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI 与机器学习" scheme="https://alex-tangt.github.io/categories/AI-%E4%B8%8E%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="AI 协作" scheme="https://alex-tangt.github.io/tags/AI-%E5%8D%8F%E4%BD%9C/"/>
    <category term="MCP" scheme="https://alex-tangt.github.io/tags/MCP/"/>
    <category term="记忆系统" scheme="https://alex-tangt.github.io/tags/%E8%AE%B0%E5%BF%86%E7%B3%BB%E7%BB%9F/"/>
    <category term="RAG" scheme="https://alex-tangt.github.io/tags/RAG/"/>
    <category term="工程实践" scheme="https://alex-tangt.github.io/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<h2 id="结论先行"><a href="#结论先行" class="headerlink" title="结论先行"></a>结论先行</h2><p>我给 coding agent 造了一个<strong>记忆能力包</strong>：它现在可以通过标准 MCP 工具语义检索我的跨项目知识库，并且有一个<strong>结构上不可能把记忆写坏</strong>的写入口。</p><p>真正决定成败的不是某个模型或某段代码，而是三条原则：</p><ol><li><strong>把不可逆的操作从工具面里拿掉</strong>。删除、原地改写这类动作根本不暴露给 agent，安全性就成了结构约束，而不是提示词约束。</li><li><strong>把真相源和派生索引分开</strong>。真相是 Markdown（可 diff、可回退），向量索引只是可随时丢弃的加速结构。索引坏了，重建即可。</li><li><strong>把每个结论都变成可复现的锚点</strong>。写代码的是 AI，那就更不能用”看起来对”当验收标准。</li></ol><p>最终交付：单测 163 通过、写路径确定性套件 25&#x2F;25、跨会话盲测 recall 通过、索引自洽（135 条 &#x3D; 135 个向量点）。</p><h2 id="一、问题：agent-的”记忆”缺什么"><a href="#一、问题：agent-的”记忆”缺什么" class="headerlink" title="一、问题：agent 的”记忆”缺什么"></a>一、问题：agent 的”记忆”缺什么</h2><p>我有一个跨项目使用的个人 Markdown 知识库，加上三个项目仓库。问题是：coding agent 只能靠 grep 去翻它。这带来两个真实的痛。</p><p><strong>检索是字面的。</strong> 我记的是”决策质量与竞争性假设（ACH）”，问的是”怎么避免拍脑袋定方案”——grep 命中不了。agent 于是把已经记录过的结论<strong>重新推导一遍</strong>，浪费上下文，还可能推歪。</p><p><strong>写入是不安全的。</strong> 让 agent 直接往 Markdown 里写，它可能写成重复条目、把一条记忆拆成五条、原地覆盖旧结论，甚至删文件。知识库最怕的不是搜不到，是<strong>悄悄坏掉</strong>。</p><p>所以目标不是”再做一个 RAG”，而是：让 agent 能可靠地回忆，并且有一个值得信任的写入口。</p><h2 id="二、形状：从”一个-RAG-应用”变成”一个能力包”"><a href="#二、形状：从”一个-RAG-应用”变成”一个能力包”" class="headerlink" title="二、形状：从”一个 RAG 应用”变成”一个能力包”"></a>二、形状：从”一个 RAG 应用”变成”一个能力包”</h2><p>原来的项目是一个政策法规问答的 RAG web。升级后重排成三个模块：</p><figure class="highlight plaintext"><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><span class="line">ragcore/       可复用核心（检索、向量库、文档处理，零 Web 依赖）</span><br><span class="line">legal_web/     适配层实例 / 回归锚点（原来的 RAG web）</span><br><span class="line">memory_agent/  hero：记忆能力包（MCP + skill）</span><br></pre></td></tr></table></figure><p>把 hero 切成记忆能力包之后，原来的 web 退成”适配层 + 回归锚点”：它仍然必须能启动、能评测，用来证明核心没被改坏。这是很划算的一手——<strong>你永远有一个跑得起来的旧东西当对照</strong>。</p><h2 id="三、五条核心决策"><a href="#三、五条核心决策" class="headerlink" title="三、五条核心决策"></a>三、五条核心决策</h2><h3 id="3-1-Markdown-是真相源，向量索引是派生物"><a href="#3-1-Markdown-是真相源，向量索引是派生物" class="headerlink" title="3.1 Markdown 是真相源，向量索引是派生物"></a>3.1 Markdown 是真相源，向量索引是派生物</h3><figure class="highlight plaintext"><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><span class="line">可写记忆 = 全局知识库的 Markdown 条目（frontmatter: id/type/tags/status/supersedes…）</span><br><span class="line">只读语料 = 项目仓库的 Markdown 文档（writable:false）</span><br><span class="line">向量索引 = 从上面两者重建出来的、可随时丢弃的加速结构</span><br></pre></td></tr></table></figure><p>记忆的语义是”可版本化、可回退、可人读”。数据库或向量库做主存，都做不到 <code>git log</code> 一眼看变更、<code>git revert</code> 一键回退。这个决定后面反复救了场：出过”索引 0 个点、但 manifest 还写着 60 条”的静默故障，因为真相源没丢，重建就完事。</p><h3 id="3-2-写入网关：不覆盖、不删除、不原地编辑"><a href="#3-2-写入网关：不覆盖、不删除、不原地编辑" class="headerlink" title="3.2 写入网关：不覆盖、不删除、不原地编辑"></a>3.2 写入网关：不覆盖、不删除、不原地编辑</h3><p>对外只暴露 MCP 工具，<strong>不暴露任何裸文件写</strong>。写入必须过一次网关：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">search → dedup → validate(frontmatter) → write → git commit → incremental refresh</span><br></pre></td></tr></table></figure><p>生命周期语义只有两种，都不是”删除”：</p><table><thead><tr><th>操作</th><th>语义</th><th>落盘效果</th></tr></thead><tbody><tr><td><code>supersede</code></td><td>有替代的更新</td><td>新建条目 + 旧条目置 <code>superseded</code>、互相指认，同一 commit</td></tr><tr><td><code>archive</code></td><td>无替代的退役</td><td>置 <code>status: archived</code> + <code>archive_reason</code>，<strong>文件永久保留</strong></td></tr></tbody></table><p>两者都是破坏性操作，默认 <code>confirm=false</code> <strong>只返回预览</strong>，用户明确同意后才落盘。</p><p>还有一条工程细节：每次写入 &#x3D; <strong>只包含本次触及文件</strong>的一个 git commit。多会话并发时很关键——如果 <code>git add -A</code>，就会把别的会话正在编辑的脏文件一起提交进去。</p><figure class="highlight plaintext"><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><span class="line">graph LR</span><br><span class="line">  A[agent] --&gt;|memory_add| G[写入网关]</span><br><span class="line">  A --&gt;|memory_supersede / archive| G</span><br><span class="line">  G --&gt;|1 搜索去重| I[(派生索引)]</span><br><span class="line">  G --&gt;|2 校验 frontmatter| V[KB 规范]</span><br><span class="line">  G --&gt;|3 落盘 + 单文件 commit| M[Markdown 真相源]</span><br><span class="line">  G --&gt;|4 增量刷新| I</span><br><span class="line">  M -.重建.-&gt; I</span><br><span class="line">  style A fill:#e8f0fe,stroke:#4285f4</span><br><span class="line">  style G fill:#fff4e5,stroke:#f4a742</span><br><span class="line">  style M fill:#e6f4ea,stroke:#34a853</span><br><span class="line">  style I fill:#fce8e6,stroke:#ea4335</span><br></pre></td></tr></table></figure><h3 id="3-3-索引一致性：代目录-指针原子切换"><a href="#3-3-索引一致性：代目录-指针原子切换" class="headerlink" title="3.3 索引一致性：代目录 + 指针原子切换"></a>3.3 索引一致性：代目录 + 指针原子切换</h3><p>重建索引是长活（CPU 上约 6 分钟 &#x2F; 60 条），中途会被超时、kill 或崩溃打断。朴素做法（清空集合再重建）如果中途死掉，就留下”<strong>空索引 + 陈旧 manifest</strong>“——<code>get</code> 照常、<code>search</code> 返回空，不核对根本发现不了。这个坑真实发生过。</p><p>现在的做法：</p><ul><li>每次全量重建写在<strong>新代目录</strong> <code>vector_db/gen-N/</code>；</li><li>全部建好并通过<strong>自洽核对</strong>（manifest 条数 &#x3D;&#x3D; 集合点数）后，原子切换 <code>CURRENT</code> 指针；</li><li>中断只留下一个没被接管的代目录，旧代继续服务；</li><li>检索前核对自洽性，不一致就<strong>显式报错</strong>，绝不静默返回空。</li></ul><h3 id="3-4-一个常驻-daemon-每会话一个瘦代理"><a href="#3-4-一个常驻-daemon-每会话一个瘦代理" class="headerlink" title="3.4 一个常驻 daemon + 每会话一个瘦代理"></a>3.4 一个常驻 daemon + 每会话一个瘦代理</h3><p>这是整个项目里最”物理”的决策，因为它由一次 OOM 逼出来。</p><p>最初每个 opencode 会话各拉起一份 stdio MCP，各加载一份 BGE-M3 嵌入模型。BGE-M3 权重 2.17GB + torch 运行时 + 加载峰值 ≈ <strong>每进程 3.9GB 私有内存</strong>。三条并行会话 &#x3D; 约 12GB，直接把系统 commit 打满，结果是 <code>Out of memory</code>、卡死、<code>uv_spawn</code> 失败。</p><figure class="highlight plaintext"><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><span class="line">graph TD</span><br><span class="line">  S1[会话 1] --&gt; P1[瘦代理]</span><br><span class="line">  S2[会话 2] --&gt; P2[瘦代理]</span><br><span class="line">  S3[会话 3] --&gt; P3[瘦代理]</span><br><span class="line">  P1 --&gt; D[常驻 MCP daemon&lt;br/&gt;唯一一份 BGE-M3]</span><br><span class="line">  P2 --&gt; D</span><br><span class="line">  P3 --&gt; D</span><br><span class="line">  style D fill:#e6f4ea,stroke:#34a853</span><br><span class="line">  style P1 fill:#e8f0fe,stroke:#4285f4</span><br><span class="line">  style P2 fill:#e8f0fe,stroke:#4285f4</span><br><span class="line">  style P3 fill:#e8f0fe,stroke:#4285f4</span><br></pre></td></tr></table></figure><p>N 个会话的内存从 <code>N × 3.9GB</code> 降到 <code>1 × 3.9GB + N × 几十 MB</code>。配套还解决了三件事：</p><ul><li>代理必须<strong>幂等拉起</strong> daemon。N 个会话同时冷启动会各自 spawn 一个 daemon（又回到 N 份模型），所以用<strong>启动权文件锁</strong>：只有一个进程负责拉起，其余只等健康检查。</li><li>同一进程内 Qdrant local mode 也不能并发开 client，于是用一个进程内锁把并发串行化。</li><li>默认<strong>不预热</strong>模型：启动私有内存从 3953MB 降到 54MB。</li></ul><h3 id="3-5-只读语料：带标签的多仓库文档"><a href="#3-5-只读语料：带标签的多仓库文档" class="headerlink" title="3.5 只读语料：带标签的多仓库文档"></a>3.5 只读语料：带标签的多仓库文档</h3><p>最后把三个项目仓库的文档也纳入检索。几个取舍：</p><ul><li><strong>只索引 <code>.md</code>，不索引代码</strong>。代码对”回忆决策”没有价值，进语料只会稀释检索。</li><li><strong>仓库清单不硬编码</strong>。三个仓库是机器本地绝对路径，写进可发布代码就是”换机器即坏 + 泄露目录结构”。改成 gitignored 的 JSON 清单 + 一份 example 模板，环境变量仍可覆盖。</li><li><strong>加 <code>&lt;label&gt;/</code> 前缀消歧义</strong>。三个仓库都有 <code>README.md</code>、<code>CONTEXT.md</code>，裸相对路径会让只读条目 id 互撞，去重时<strong>静默丢条</strong>。现在来源是 <code>agent-infra/docs/adr/0007-....md</code> 这种带仓库名前缀的形式。</li><li><strong>排除噪声</strong>：生成物、工具缓存一律不进——其中一个仓库光 <code>outputs/</code> 就有 1339 个 markdown 抽取产物，全进索引就是灾难。</li></ul><p>改配置有个必须记住的约束：<strong>配置在 import 时求值，改完必须重启 daemon</strong>。否则 daemon 拿着旧的只读根做增量刷新，会把”不在我语料里”的条目当孤儿删掉。</p><h2 id="四、六个可复用的坑"><a href="#四、六个可复用的坑" class="headerlink" title="四、六个可复用的坑"></a>四、六个可复用的坑</h2><ol><li><strong>stdio MCP：stdout 就是协议通道。</strong> 底层库的 logger 默认把日志写到 <code>stdout</code>，一行就能腐蚀 JSON-RPC。必须在 import 它<strong>之前</strong>把 root logger 抢配到 stderr。</li><li><strong>模块名会遮蔽包。</strong> 把 <code>config.py</code> 放在了不该放的位置，结果在脚本模式下遮蔽了另一个顶层 <code>config</code> 包。改名 + 统一前缀绝对导入。</li><li><strong>Qdrant local mode 的两个反直觉语义。</strong> (a) 独占锁在 client 构造期持有、<code>close()</code> 释放，所以不要长持 client，按操作开&#x2F;关（实测约 19ms&#x2F;次）；(b) 删集合后再用同名建集合，磁盘上的旧点会<strong>复活</strong>——清空集合要用空 filter 删光点。</li><li><strong>“命令返回了” ≠ 成功。</strong> 重建进程被 kill，命令看起来正常结束，但状态是坏的。长活必须后台启动 + 输出重定向 + 独立轮询进度 + 完成后自洽核对。</li><li><strong>客户端超时 ≠ 写入失败。</strong> MCP 客户端 20s 超时，但服务端可能已经 commit 成功。超时后要核对真相源，<strong>不能直接重试</strong>（重试会命中去重，只返回 <code>duplicate</code>）。</li><li><strong>去重阈值要校准，不要拍脑袋。</strong> 最初拍 0.92，在真实知识库上量了一下：不同条目的最近邻相似度 ≤0.792，精确重加最低 0.949，于是改成 0.88 落在间隔中部。</li></ol><h2 id="五、证据，而不是感觉"><a href="#五、证据，而不是感觉" class="headerlink" title="五、证据，而不是感觉"></a>五、证据，而不是感觉</h2><p>“AI 写的代码”最大的风险是<strong>看起来很对</strong>。所以整个过程尽量把结论变成机器可验的锚点：</p><table><thead><tr><th>能力</th><th>证据</th></tr></thead><tbody><tr><td>核心服务</td><td><code>pytest tests/unit</code> → <strong>163 passed</strong></td></tr><tr><td>写路径确定性</td><td>真实知识库克隆 + 隔离索引的 sandbox 套件 → <strong>25&#x2F;25</strong>，两次运行一致，真实知识库前后逐字不变</td></tr><tr><td>索引一致性</td><td><code>memory_index_status</code> → 135 条 &#x3D; 135 个点、<code>consistent=true</code></td></tr><tr><td>跨会话 recall</td><td>独立进程 + 独立子代理两路盲测，同一句改写查询命中同一条、score 逐位相同；该查询关键词全库 grep <strong>0 命中</strong>，证明是语义召回</td></tr><tr><td>只读语料</td><td>三仓库 135 条 &#x3D; 26 可写 + 109 只读文档；<code>supersede</code>&#x2F;<code>archive</code> 对只读条目直接拒绝</td></tr><tr><td>回归锚点</td><td>旧 RAG web 启动冒烟：就绪、页面与脚本 200、知识库列表与文档数正确、退出后端口与锁释放</td></tr></tbody></table><p>几个刻意的测试口径：</p><ul><li><strong>只断言外部行为</strong>。sandbox 只检查工具返回值、落盘文件、git 状态、规范校验输出，<strong>不碰内部向量库调用</strong>。这样重构内部实现不会把测试改成”测实现”。</li><li><strong>真实知识库只读</strong>。sandbox 克隆已提交态到临时目录，跑完用 <code>git status</code> 证明真实知识库未变。</li><li><strong>锚点纪律</strong>。结构怎么重构，基准锚点（单测 + 导入冒烟 + 启动冒烟）必须每天可复现。</li></ul><h2 id="六、方法论：让-AI-开发可验证"><a href="#六、方法论：让-AI-开发可验证" class="headerlink" title="六、方法论：让 AI 开发可验证"></a>六、方法论：让 AI 开发可验证</h2><p>这个项目的执行者是 AI（我提供思路和裁决，AI 写代码）。跑下来最有价值的不是某段代码，而是几条把”AI 生产力”变成”可验证结果”的纪律：</p><ul><li><strong>请求先进路由</strong>：缺陷走根因流程、新功能走决策闸门、实验只进 <code>experiments/&lt;name&gt;/</code>（先写记录再跑，无结论不算完成）。</li><li><strong>一个单元 &#x3D; 一个 commit</strong>：能一句话说清 + 能独立验证的增量就提交，不攒大提交。</li><li><strong>硬决策必须留 ADR</strong>：难逆转、反直觉、真权衡的，写进 <code>docs/adr/</code>。这个项目留了 14 篇，覆盖每一次”为什么这么选”。</li><li><strong>同一目标失败三次就停手调研</strong>，不要原地重试。</li><li><strong>收工过健康闸门</strong>：git 干净、文档与目录树一致、无垃圾文件、实验有结论、决策有落点。</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这个 MVP 真正的产出不是”一个能搜的记忆库”，而是三件可复用的东西：</p><ol><li><strong>结构约束 &gt; 提示词约束</strong>。把删除、原地改写从工具面拿掉，agent 想做坏事也做不到。</li><li><strong>真相源与派生索引分离</strong>。Markdown 负责正确与可回退，索引负责快；前者坏了不可接受，后者坏了重建即可。</li><li><strong>证据文化</strong>。单测、确定性 sandbox、盲测 recall、自洽核对——每一个”完成”都要有机器可验的锚点。</li></ol><p>下一步是把”检索有多好”从感觉变成可比数字（BEIR 子集 nDCG@10），以及向量 &#x2F; 关键词 &#x2F; 锚点的混合检索融合优化。但那是另一个故事了。</p>]]>
    </content>
    <id>https://alex-tangt.github.io/agent-long-term-memory-mvp/index/</id>
    <link href="https://alex-tangt.github.io/agent-long-term-memory-mvp/index/"/>
    <published>2026-09-15T04:00:00.000Z</published>
    <summary>我让 AI 在一个周末里做出了一个记忆能力包：让 coding agent 能语义检索、安全写入长期记忆。这篇复盘五条核心设计决策、六个可复用的坑，以及为什么&quot;能讲证据&quot;比&quot;看起来对&quot;更重要。</summary>
    <title>给 AI Agent 造一个安全的长期记忆：一次 MVP 的决策、踩坑与证据</title>
    <updated>2026-09-15T04:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI &amp; Machine Learning" scheme="https://alex-tangt.github.io/categories/AI-Machine-Learning/"/>
    <category term="MCP" scheme="https://alex-tangt.github.io/tags/MCP/"/>
    <category term="RAG" scheme="https://alex-tangt.github.io/tags/RAG/"/>
    <category term="AI Collaboration" scheme="https://alex-tangt.github.io/tags/AI-Collaboration/"/>
    <category term="Memory" scheme="https://alex-tangt.github.io/tags/Memory/"/>
    <category term="Engineering Practice" scheme="https://alex-tangt.github.io/tags/Engineering-Practice/"/>
    <content>
      <![CDATA[<h2 id="Bottom-line-first"><a href="#Bottom-line-first" class="headerlink" title="Bottom line first"></a>Bottom line first</h2><p>I built a <strong>memory capability package</strong> for coding agents: it can now semantically search my cross-project knowledge base through standard MCP tools, and it has a write path that is <strong>structurally incapable of corrupting memory</strong>.</p><p>What actually decided success wasn’t a model or a particular piece of code. It was three principles:</p><ol><li><strong>Remove irreversible operations from the tool surface.</strong> Delete and in-place rewrite are never exposed to the agent, so safety becomes a structural constraint instead of a prompt-level one.</li><li><strong>Separate the source of truth from the derived index.</strong> The truth is Markdown (diffable, revertible); the vector index is a disposable accelerator. If the index breaks, rebuild it.</li><li><strong>Turn every conclusion into a reproducible anchor.</strong> When an AI writes the code, “looks right” is even less acceptable as an acceptance criterion.</li></ol><p>The final deliverable: 163 unit tests passing, a write-path determinism suite at 25&#x2F;25, a cross-session blind recall test passing, and a self-consistent index (135 entries &#x3D; 135 vector points).</p><h2 id="1-The-problem-what-an-agent’s-“memory”-is-missing"><a href="#1-The-problem-what-an-agent’s-“memory”-is-missing" class="headerlink" title="1. The problem: what an agent’s “memory” is missing"></a>1. The problem: what an agent’s “memory” is missing</h2><p>I keep a personal Markdown knowledge base used across projects, plus three project repos. The problem: a coding agent can only grep through it. Two real pains follow.</p><p><strong>Retrieval is literal.</strong> I stored “decision quality and Analysis of Competing Hypotheses (ACH),” then asked “how do I avoid making calls from the gut” — grep finds nothing. So the agent <strong>re-derives conclusions it already recorded</strong>, burning context and sometimes getting them wrong.</p><p><strong>Writes are unsafe.</strong> Let an agent write Markdown directly and it might create duplicates, split one memory into five, overwrite an old conclusion in place, or just delete a file. The worst outcome for a knowledge base isn’t “can’t find it” — it’s <strong>quietly rotting</strong>.</p><p>So the goal wasn’t “another RAG.” It was: reliable recall, plus a write entry point you can trust.</p><h2 id="2-Shape-from-“a-RAG-app”-to-“a-capability-package”"><a href="#2-Shape-from-“a-RAG-app”-to-“a-capability-package”" class="headerlink" title="2. Shape: from “a RAG app” to “a capability package”"></a>2. Shape: from “a RAG app” to “a capability package”</h2><p>The original project was a policy-and-law Q&amp;A RAG web app. After the upgrade it was reorganized into three modules:</p><figure class="highlight plaintext"><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><span class="line">ragcore/       reusable core (retrieval, vector store, document handling; zero web deps)</span><br><span class="line">legal_web/     adapter instance / regression anchor (the original RAG web app)</span><br><span class="line">memory_agent/  the hero: the memory capability package (MCP + skill)</span><br></pre></td></tr></table></figure><p>Once the hero became a memory package, the old web app was demoted to “adapter + regression anchor”: it must still boot and still be evaluable, proving the core wasn’t broken. That’s a great deal — <strong>you always have a working old thing to compare against</strong>.</p><h2 id="3-Five-core-decisions"><a href="#3-Five-core-decisions" class="headerlink" title="3. Five core decisions"></a>3. Five core decisions</h2><h3 id="3-1-Markdown-is-the-source-of-truth-the-vector-index-is-derived"><a href="#3-1-Markdown-is-the-source-of-truth-the-vector-index-is-derived" class="headerlink" title="3.1 Markdown is the source of truth; the vector index is derived"></a>3.1 Markdown is the source of truth; the vector index is derived</h3><figure class="highlight plaintext"><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><span class="line">writable memory   = Markdown entries in the global KB (frontmatter: id/type/tags/status/supersedes…)</span><br><span class="line">read-only corpus  = Markdown docs from project repos (writable:false)</span><br><span class="line">vector index      = a rebuildable, disposable accelerator over both</span><br></pre></td></tr></table></figure><p>The semantics of memory are “versionable, revertible, human-readable.” A database or vector store as primary storage can’t give you <code>git log</code> for at-a-glance changes or <code>git revert</code> for one-command rollback. That decision saved the day repeatedly later: we hit a silent failure where “the index has 0 points but the manifest still lists 60 entries.” Because the source of truth was intact, a rebuild was all it took.</p><h3 id="3-2-The-write-gateway-no-overwrite-no-delete-no-in-place-edits"><a href="#3-2-The-write-gateway-no-overwrite-no-delete-no-in-place-edits" class="headerlink" title="3.2 The write gateway: no overwrite, no delete, no in-place edits"></a>3.2 The write gateway: no overwrite, no delete, no in-place edits</h3><p>Only MCP tools are exposed; <strong>no raw file write, ever</strong>. Every write goes through a gateway:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">search → dedup → validate(frontmatter) → write → git commit → incremental refresh</span><br></pre></td></tr></table></figure><p>There are exactly two lifecycle semantics, and neither is “delete”:</p><table><thead><tr><th>Operation</th><th>Meaning</th><th>Effect on disk</th></tr></thead><tbody><tr><td><code>supersede</code></td><td>An update with a replacement</td><td>New entry + old entry marked <code>superseded</code>, cross-referenced, in one commit</td></tr><tr><td><code>archive</code></td><td>A retirement with no replacement</td><td><code>status: archived</code> + <code>archive_reason</code>, <strong>file kept forever</strong></td></tr></tbody></table><p>Both are destructive, so by default <code>confirm=false</code> <strong>returns a preview only</strong>; nothing is written until the user explicitly agrees.</p><p>One more engineering detail: every write is a git commit that contains <strong>only the files it touched</strong>. Under concurrent sessions that matters — <code>git add -A</code> would sweep in some other session’s work-in-progress.</p><figure class="highlight plaintext"><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><span class="line">graph LR</span><br><span class="line">  A[agent] --&gt;|memory_add| G[Write Gateway]</span><br><span class="line">  A --&gt;|memory_supersede / archive| G</span><br><span class="line">  G --&gt;|1 search &amp; dedup| I[(Derived Index)]</span><br><span class="line">  G --&gt;|2 validate frontmatter| V[KB Rules]</span><br><span class="line">  G --&gt;|3 write + single-file commit| M[Markdown Source of Truth]</span><br><span class="line">  G --&gt;|4 incremental refresh| I</span><br><span class="line">  M -.rebuild.-&gt; I</span><br><span class="line">  style A fill:#e8f0fe,stroke:#4285f4</span><br><span class="line">  style G fill:#fff4e5,stroke:#f4a742</span><br><span class="line">  style M fill:#e6f4ea,stroke:#34a853</span><br><span class="line">  style I fill:#fce8e6,stroke:#ea4335</span><br></pre></td></tr></table></figure><h3 id="3-3-Index-consistency-generations-plus-an-atomic-pointer-swap"><a href="#3-3-Index-consistency-generations-plus-an-atomic-pointer-swap" class="headerlink" title="3.3 Index consistency: generations plus an atomic pointer swap"></a>3.3 Index consistency: generations plus an atomic pointer swap</h3><p>A full rebuild is a long job (~6 minutes per 60 entries on CPU) and can be interrupted by a timeout, a kill, or a crash. The naive approach — clear the collection, then rebuild — leaves “<strong>empty index + stale manifest</strong>“ if it dies midway: <code>get</code> works, <code>search</code> returns nothing, and you won’t notice unless you check. We hit exactly that.</p><p>The current approach:</p><ul><li>Each full rebuild is written into a <strong>new generation directory</strong> <code>vector_db/gen-N/</code>;</li><li>Only after it is complete and passes a <strong>consistency check</strong> (manifest count &#x3D;&#x3D; point count) do we atomically swap the <code>CURRENT</code> pointer;</li><li>An interruption leaves an unadopted generation directory; the old one keeps serving;</li><li>Before serving a search we verify consistency and <strong>raise loudly</strong> if it fails — never a silent empty result.</li></ul><h3 id="3-4-One-resident-daemon-plus-a-thin-per-session-proxy"><a href="#3-4-One-resident-daemon-plus-a-thin-per-session-proxy" class="headerlink" title="3.4 One resident daemon plus a thin per-session proxy"></a>3.4 One resident daemon plus a thin per-session proxy</h3><p>This is the most “physical” decision in the project, and it was forced by an OOM.</p><p>Initially each opencode session spawned its own stdio MCP server, each loading its own BGE-M3 embedding model. The weights are 2.17GB, plus the torch runtime and load peak ≈ <strong>3.9GB private memory per process</strong>. Three parallel sessions ≈ 12GB, which exhausted system commit and produced <code>Out of memory</code>, hangs, and <code>uv_spawn</code> failures.</p><figure class="highlight plaintext"><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><span class="line">graph TD</span><br><span class="line">  S1[Session 1] --&gt; P1[Thin Proxy]</span><br><span class="line">  S2[Session 2] --&gt; P2[Thin Proxy]</span><br><span class="line">  S3[Session 3] --&gt; P3[Thin Proxy]</span><br><span class="line">  P1 --&gt; D[Resident MCP daemon&lt;br/&gt;single BGE-M3]</span><br><span class="line">  P2 --&gt; D</span><br><span class="line">  P3 --&gt; D</span><br><span class="line">  style D fill:#e6f4ea,stroke:#34a853</span><br><span class="line">  style P1 fill:#e8f0fe,stroke:#4285f4</span><br><span class="line">  style P2 fill:#e8f0fe,stroke:#4285f4</span><br><span class="line">  style P3 fill:#e8f0fe,stroke:#4285f4</span><br></pre></td></tr></table></figure><p>N sessions went from <code>N × 3.9GB</code> to <code>1 × 3.9GB + N × tens of MB</code>. Three more things came with it:</p><ul><li>The proxy must <strong>start the daemon idempotently</strong>. N sessions cold-starting simultaneously would each spawn a daemon (back to N models), so a <strong>startup-ownership file lock</strong> ensures only one process spawns while the rest just wait for health checks.</li><li>Within one process, Qdrant local mode can’t open a second client concurrently either, so a process-level lock serializes access.</li><li>Warmup is <strong>off by default</strong>: startup private memory dropped from 3953MB to 54MB.</li></ul><h3 id="3-5-Read-only-corpus-labeled-documents-across-multiple-repos"><a href="#3-5-Read-only-corpus-labeled-documents-across-multiple-repos" class="headerlink" title="3.5 Read-only corpus: labeled documents across multiple repos"></a>3.5 Read-only corpus: labeled documents across multiple repos</h3><p>Finally, I brought three project repos’ documents into retrieval. A few trade-offs:</p><ul><li><strong>Index <code>.md</code> only, never code.</strong> Code is worthless for “recalling decisions” and only dilutes retrieval.</li><li><strong>Don’t hardcode the repo list.</strong> The three repos are machine-local absolute paths; baking them into publishable code means “breaks on another machine, and leaks directory structure.” Instead: a gitignored JSON list plus a committed example template, with an env override still available.</li><li><strong>Prefix sources with <code>&lt;label&gt;/</code> to disambiguate.</strong> All three repos have <code>README.md</code> and <code>CONTEXT.md</code>; bare relative paths make read-only entry ids collide, and de-duplication <strong>silently drops</strong> entries. Now sources look like <code>agent-infra/docs/adr/0007-....md</code>.</li><li><strong>Exclude noise</strong>: generated artifacts and tool caches never enter. One repo alone had 1339 markdown extraction outputs under <code>outputs/</code>; indexing all of them would be a disaster.</li></ul><p>One constraint you must remember when changing config: <strong>it is evaluated at import time, so restart the daemon after editing it.</strong> Otherwise the daemon refreshes incrementally using stale read-only roots and deletes entries it thinks are orphans.</p><h2 id="4-Six-reusable-scars"><a href="#4-Six-reusable-scars" class="headerlink" title="4. Six reusable scars"></a>4. Six reusable scars</h2><ol><li><strong>stdio MCP: stdout is the protocol channel.</strong> The underlying library’s logger writes to <code>stdout</code> by default; one line corrupts JSON-RPC. You must grab the root logger and point it at stderr <em>before</em> importing it.</li><li><strong>Module names can shadow packages.</strong> A <code>config.py</code> placed in the wrong spot shadowed another top-level <code>config</code> package in script mode. Renamed it and switched to consistently prefixed absolute imports.</li><li><strong>Two counterintuitive semantics of Qdrant local mode.</strong> (a) The exclusive lock is held at client construction and released by <code>close()</code>, so don’t hold a client long-term — open&#x2F;close per operation (~19ms each, measured). (b) Dropping a collection and recreating it with the same name <strong>resurrects</strong> the old on-disk points — clear by deleting all points with an empty filter.</li><li><strong>“The command returned” ≠ success.</strong> Kill the rebuild process and the command looks like it finished, but state is broken. Long jobs need background start + redirected output + independent progress polling + a consistency check at the end.</li><li><strong>Client timeout ≠ write failure.</strong> An MCP client may time out at 20s while the server has already committed. After a timeout, check the source of truth — <strong>don’t retry</strong> (a retry hits dedup and just returns <code>duplicate</code>).</li><li><strong>Calibrate the dedup threshold; don’t guess it.</strong> The first guess was 0.92. Measured on the real KB: nearest neighbors of distinct entries were ≤0.792, and exact re-adds started at 0.949 — so 0.88 sits in the middle of the gap.</li></ol><h2 id="5-Evidence-not-vibes"><a href="#5-Evidence-not-vibes" class="headerlink" title="5. Evidence, not vibes"></a>5. Evidence, not vibes</h2><p>The biggest risk with “AI-written code” is that it <strong>looks right</strong>. So we tried to turn every conclusion into a machine-verifiable anchor:</p><table><thead><tr><th>Capability</th><th>Evidence</th></tr></thead><tbody><tr><td>Core services</td><td><code>pytest tests/unit</code> → <strong>163 passed</strong></td></tr><tr><td>Write-path determinism</td><td>The sandbox suite (real KB clone + isolated index) → <strong>25&#x2F;25</strong>, identical across two runs, real KB byte-identical before and after</td></tr><tr><td>Index consistency</td><td><code>memory_index_status</code> → 135 entries &#x3D; 135 points, <code>consistent=true</code></td></tr><tr><td>Cross-session recall</td><td>Two independent channels (separate process + separate sub-agent) hit the same entry with the same paraphrase query and bit-identical score; that query had <strong>0 grep hits</strong> across the KB, proving semantic recall</td></tr><tr><td>Read-only corpus</td><td>135 entries &#x3D; 26 writable + 109 read-only docs across three repos; <code>supersede</code>&#x2F;<code>archive</code> reject read-only entries outright</td></tr><tr><td>Regression anchor</td><td>Old RAG web boot smoke: ready, page and script 200, KB list and doc count correct, port and lock released on shutdown</td></tr></tbody></table><p>A few deliberate testing stances:</p><ul><li><strong>Assert external behavior only.</strong> The sandbox checks tool responses, files on disk, git state, and spec-validation output — <strong>never internal vector-store calls</strong>. That way refactoring internals can’t turn tests into tests-of-implementation.</li><li><strong>Keep the real KB read-only.</strong> The sandbox clones the committed state into a temp dir and proves with <code>git status</code> that the real KB is untouched.</li><li><strong>Anchor discipline.</strong> However the structure is refactored, the baseline anchors (unit tests + import smoke + boot smoke) must be reproducible every day.</li></ul><h2 id="6-Methodology-making-AI-development-verifiable"><a href="#6-Methodology-making-AI-development-verifiable" class="headerlink" title="6. Methodology: making AI development verifiable"></a>6. Methodology: making AI development verifiable</h2><p>The executor on this project was an AI (I supplied direction and made the calls; the AI wrote the code). The most valuable output wasn’t any particular chunk of code — it was a set of disciplines that turn “AI productivity” into “verifiable results”:</p><ul><li><strong>Route requests first</strong>: defects go through a root-cause loop, features through a decision gate, experiments into <code>experiments/&lt;name&gt;/</code> (write the record before running; no conclusion means not done).</li><li><strong>One unit &#x3D; one commit</strong>: commit any increment you can explain in one sentence and verify independently; don’t hoard a giant commit.</li><li><strong>Hard decisions get an ADR</strong>: anything hard to reverse, counterintuitive, or a genuine trade-off goes into <code>docs/adr/</code>. This project has 14, covering every “why this choice.”</li><li><strong>If the same goal fails three times, stop and research</strong> — don’t retry in place.</li><li><strong>Run a health gate at wrap-up</strong>: clean git, docs matching the real tree, no junk files, experiments with conclusions, decisions with a home.</li></ul><h2 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h2><p>The real output of this MVP isn’t “a searchable memory store.” It’s three reusable things:</p><ol><li><strong>Structural constraints beat prompt constraints.</strong> Take delete and in-place rewrite off the tool surface, and the agent <em>can’t</em> do harm.</li><li><strong>Separate the source of truth from the derived index.</strong> Markdown owns correctness and revertibility; the index owns speed. The former must never break; the latter can always be rebuilt.</li><li><strong>A culture of evidence.</strong> Unit tests, a deterministic sandbox, blind recall, consistency checks — every “done” needs a machine-verifiable anchor.</li></ol><p>Next up: turning “how good is retrieval” from a feeling into a comparable number (BEIR subset nDCG@10), and optimizing the fusion of vector &#x2F; keyword &#x2F; anchor signals. But that’s another story.</p>]]>
    </content>
    <id>https://alex-tangt.github.io/agent-long-term-memory-mvp-en/index/</id>
    <link href="https://alex-tangt.github.io/agent-long-term-memory-mvp-en/index/"/>
    <published>2026-09-15T04:00:00.000Z</published>
    <summary>I had an AI build a memory capability package in a weekend: a coding agent that can semantically search and safely write long-term memory. This is a retrospective on five core design decisions, six reusable scars, and why 'show the evidence' beats 'looks right.'</summary>
    <title>Giving an AI Agent a Safe Long-Term Memory: Decisions, Scars, and Evidence from an MVP</title>
    <updated>2026-09-15T04:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI 与机器学习" scheme="https://alex-tangt.github.io/categories/AI-%E4%B8%8E%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="AI 协作" scheme="https://alex-tangt.github.io/tags/AI-%E5%8D%8F%E4%BD%9C/"/>
    <category term="工程实践" scheme="https://alex-tangt.github.io/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <category term="Agent Skill" scheme="https://alex-tangt.github.io/tags/Agent-Skill/"/>
    <category term="知识库" scheme="https://alex-tangt.github.io/tags/%E7%9F%A5%E8%AF%86%E5%BA%93/"/>
    <category term="方法论" scheme="https://alex-tangt.github.io/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <content>
      <![CDATA[<h2 id="结论先行"><a href="#结论先行" class="headerlink" title="结论先行"></a>结论先行</h2><p>项目做完，最值钱的不是结果，是<strong>踩过的坑和摸清的流程</strong>。但沉淀的关键从来不是”多写几条”，而是<strong>先分流</strong>：</p><ul><li><strong>流程&#x2F;SOP</strong>（”怎么做”）→ <strong>Agent Skill</strong>（可执行的步骤）；</li><li><strong>事实&#x2F;数据</strong>（”是什么”）→ <strong>知识库</strong>；</li><li><strong>短而硬的规则</strong>（”见到即改”）→ <strong>AGENTS</strong>。</li></ul><p>再加一条同样重要的原则：<strong>不要从零造 skill</strong>。先找现成的参考再裁剪，比自研稳。</p><h2 id="一、怎么决定放哪：先问”怎么做”还是”是什么”"><a href="#一、怎么决定放哪：先问”怎么做”还是”是什么”" class="headerlink" title="一、怎么决定放哪：先问”怎么做”还是”是什么”"></a>一、怎么决定放哪：先问”怎么做”还是”是什么”</h2><p>这是全套沉淀的第一颗扣子。问错，代价是双向的：</p><ul><li>把事实塞进 skill → skill 越来越长，最后没人读；</li><li>把方法塞进知识库 → 它<strong>不会被触发</strong>，等于没沉淀。</li></ul><table><thead><tr><th>类型</th><th>去处</th><th>例子</th></tr></thead><tbody><tr><td>通用流程、判断方法</td><td><strong>Skill</strong>（可被任务触发）</td><td>“动手前先判断要不要调研”、”昂贵运行前怎么设闸”</td></tr><tr><td>项目&#x2F;平台的事实与数据</td><td><strong>知识库</strong>（被检索）</td><td>版本矩阵、错误目录、评测指标、结果数字</td></tr><tr><td>见到就该改的红线</td><td><strong>AGENTS</strong>（常驻）</td><td>“裸 <code>dataset_text_field</code> 训 SFT 是续训损失，必错”</td></tr></tbody></table><p>我们的全局 <code>AGENTS.md</code> 就是从”堆规则”演化来的——现在只剩 <strong>Tripwires（绊线）+ Gates（闸门）</strong> 两层，流程全部<strong>下沉到 skill</strong>。</p><h2 id="二、知识库上的两个决策"><a href="#二、知识库上的两个决策" class="headerlink" title="二、知识库上的两个决策"></a>二、知识库上的两个决策</h2><p>知识库这一块，我们做了两个明确的决策（各留了一份决策记录）：</p><p><strong>决策一：纯 Markdown + skill，先不上 RAG。</strong></p><ul><li>用 <code>INDEX.md</code> + 文本检索定位，正文是原子化条目；</li><li><strong>与人的 Obsidian 笔记分开</strong>——那是给人看的，没有 agent 检索路径；agent 知识库要的是机器友好的结构；</li><li>验收标准是”<strong>新开一个会话，agent 只靠 skill + 索引就能找到并引用</strong>“；</li><li>RAG&#x2F;向量检索<strong>明确留到第二期</strong>，不改变 Markdown 文件本身。</li></ul><p>理由很直接：没有服务就没有第二个会漂移的产物，离线、可 diff、可携带。</p><p><strong>决策二：”有界索引、无界正文” + 检索阶梯。</strong></p><ul><li>根索引只列<strong>域</strong>（有上限），每个域下面才是条目；域索引由脚本<strong>生成</strong>；</li><li>条目有<strong>生命周期</strong>（current &#x2F; draft &#x2F; superseded &#x2F; archived）与<strong>去重&#x2F;覆盖</strong>规则——写之前先搜，相近的就合并或取代，绝不追加重复；</li><li>检索能力按<strong>触发条件</strong>分阶升级（L0 索引 → L1 文本检索 → L2 本地 FTS5 → L3 向量），<strong>没有明确信号就不上</strong>。</li></ul><p>核心认识：<strong>增长与召回是两个问题</strong>——结构限制增长，向量只改善召回。用一个不断吞入的向量库，并不能解决”索引无限膨胀”。</p><h2 id="三、四个-skill-各自装了什么"><a href="#三、四个-skill-各自装了什么" class="headerlink" title="三、四个 skill 各自装了什么"></a>三、四个 skill 各自装了什么</h2><p>落下来的流程型 skill 就四个，都不长：</p><p><strong>1. <code>research-first</code> —— 什么时候该停下来调研</strong></p><ul><li>触发：<strong>新栈</strong>（平台&#x2F;框架&#x2F;硬件&#x2F;库）、<strong>框架 vs 手搓</strong>的选型、<strong>同一目标失败 3 次</strong>；</li><li>计数单位是<strong>目标</strong>不是错误——同一个目标换不同报错，失败数照累加；</li><li>一手源优先（官方文档、版本矩阵、issue），记录<strong>确切版本 + URL</strong>；先查知识库；</li><li>必产一份<strong>调研备忘录</strong>（假设 &#x2F; 证据 &#x2F; 版本矩阵 &#x2F; 否决的选项 &#x2F; 唯一下一步 &#x2F; 置信度）。</li></ul><p><strong>2. <code>expensive-run-gates</code> —— 花钱前怎么设闸</strong></p><ul><li><strong>闸 1 干跑</strong>：产出配置卡，和<strong>参考实现（anchor）</strong>逐项 diff——注意”自洽 ≠ 可比”，同门兄弟之间 diff 通过只证明自洽；</li><li><strong>闸 2 最小对照 + 机器可验证据</strong>：<code>format_check</code>（格式对）、<code>mask_check</code>（标签不是全 <code>-100</code>）、<code>run_config</code>；<strong>两个截然不同的包装拿到一模一样的分数，就要怀疑它们共享同一个缺陷</strong>（我们的掩码 bug 就是这样暴露的）；</li><li><strong>闸 3 人放行</strong>：给出预期成本、墙钟、成功信号、中止信号，人签字才花钱；</li><li><strong>安全网</strong>：checkpoint（见下）；</li><li>还规定了一条<strong>证据契约</strong>：每次昂贵运行都往 run 目录写 <code>run_config / format_check / mask_check / receipt</code>，<strong>结论只从这些工件里取，绝不从控制台一行字</strong>。</li></ul><p><strong>3. <code>checkpoint-design</code> —— 在哪停、谁验证、存什么</strong></p><ul><li>本质：checkpoint 是<strong>在问题复利之前抓住它</strong>——两件事：<strong>验证（抓住它）+ 落盘（不必重做）</strong>；</li><li><strong>在哪停</strong>：列出这次运行的承重假设，按”错了多贵 × 多难撤销 × 下游依赖多重”排序，在最高的那处停；<strong>不能改变决策的检查，跳过</strong>；</li><li><strong>谁验证</strong>：机械&#x2F;可查&#x2F;可逆 → AI + 能定论的最小实验；品味&#x2F;权衡&#x2F;不可逆 → 人；重复、高风险、易错 → 独立或对抗式检查；有争议 → <strong>先预注册判定信号</strong>，再跑实验；</li><li><strong>落盘</strong>：<code>save_every ≈ 总步数 / 5</code>；<code>save_strategy</code> 配一个<strong>大于总步数</strong>的 <code>save_steps</code> 会导致<strong>什么都不写</strong>；要持久化昂贵的产物（adapter&#x2F;merged），并验一次能否续跑。</li></ul><p><strong>4. <code>decision-design</code> —— 怎么把”选择”交给人才对</strong></p><ul><li>一切从<strong>本质目的</strong>出发（一句话说清，剥掉你当前的方案）；</li><li>命名<strong>权衡轴</strong>（成本↔速度、可控↔方便……），不在这些轴上有差别的，不叫选项；</li><li>给<strong>真选项</strong>：2–4 个，每个都能被合理的人选中；<strong>不设稻草人</strong>；末尾留<strong>备注出口</strong>（”都不选 &#x2F; A 加 X &#x2F; 我有个你不知道的约束”）；</li><li>选项接近时：<strong>找反证</strong>（”什么能证明它错了？”）、<strong>用实验裁决事实分歧</strong>、大分叉要一份<strong>能犯不同错的第二意见</strong>（重读同一个上下文只是冗余，会继承同一个盲点）。</li></ul><h2 id="四、谁该短、谁该全：AGENTS-是规则，skill-是-SOP"><a href="#四、谁该短、谁该全：AGENTS-是规则，skill-是-SOP" class="headerlink" title="四、谁该短、谁该全：AGENTS 是规则，skill 是 SOP"></a>四、谁该短、谁该全：AGENTS 是规则，skill 是 SOP</h2><p>我一开始把”简短有力”误配到了 skill 上。正确的分工是：</p><ul><li><strong>AGENTS.md 才是短而硬的那层。</strong> 它常驻、每次都在眼前，所以必须<strong>短、且有力</strong>——一句话说清”见到即改”的红线。</li><li><strong>SKILL.md 是 SOP。</strong> 它是给 AI 照着执行的流程性知识，**目标是”可执行”，不是”尽可能短”**：触发条件、步骤、输出契约都要写清楚，让 AI 不用猜。</li></ul><p>把 AGENTS 写成 SOP，它会臃肿到没人读；把 skill 写成一两句口号，它就没法被执行。<strong>短是规则的属性，清楚才是 SOP 的属性。</strong></p><h2 id="五、合作者（AI）视角"><a href="#五、合作者（AI）视角" class="headerlink" title="五、合作者（AI）视角"></a>五、合作者（AI）视角</h2><blockquote><p>这段沉淀里，我（AI）的真实倾向和你们的决定是有张力的：</p><ul><li><strong>我容易把”规则”和”SOP”混着写</strong>——要么把流程压成口号（不可执行），要么把规则铺成流水账（没人读）。正确的分工是：<strong>把短而硬的判断留在 AGENTS，把可执行的步骤写进 skill</strong>。</li><li><strong>写了规则 ≠ 会遵守。</strong> 我可以把 <code>research-first</code> 背得很熟，仍然在没信号时”先做再说”。所以这些 skill 对我们真正的价值，不是知识，而是<strong>闸门</strong>——它们把”该停下来”变成一个明确的触发条件。</li><li>因此我认为：<strong>AGENTS 是开关，skill 是操作手册。</strong> AGENTS 的成功标准是”在该停的那一刻被想起”；skill 的成功标准是”照着它就能执行”。</li></ul></blockquote><h2 id="六、可迁移的几条准则"><a href="#六、可迁移的几条准则" class="headerlink" title="六、可迁移的几条准则"></a>六、可迁移的几条准则</h2><ul><li>先分流，再动笔：<strong>怎么做→skill，是什么→知识库，红线→AGENTS</strong>。</li><li>不从零造 skill，先参考现成的再裁剪。</li><li>知识库<strong>先要有界</strong>（索引限域、条目原子、写前先搜），再谈语义召回。</li><li>每个 skill 都要有明确的<strong>触发条件</strong>和<strong>输出契约</strong>，否则它不会被用。</li><li>checkpoint 的职责是<strong>在复利前抓住问题</strong>，并让出错时的返工有上界——<strong>验证</strong>与<strong>落盘</strong>各司其职。</li></ul><p>项目与证据：<a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a></p><p><em>本文由我（Alex）与 AI 协作完成；「合作者视角」是 AI 的独立表达。</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/incidents-to-skills-and-kb/index/</id>
    <link href="https://alex-tangt.github.io/incidents-to-skills-and-kb/index/"/>
    <published>2026-09-13T15:00:00.000Z</published>
    <summary>项目收尾时，我把踩过的坑沉淀成了可复用资产。这篇讲两件事：怎么决定&quot;什么进 skill、什么进知识库、什么留在 AGENTS&quot;，以及我们实际落下来的四个 skill 各自装了什么。</summary>
    <title>把踩过的坑变成资产：哪些进 skill、哪些进知识库</title>
    <updated>2026-09-13T15:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI &amp; Machine Learning" scheme="https://alex-tangt.github.io/categories/AI-Machine-Learning/"/>
    <category term="AI Collaboration" scheme="https://alex-tangt.github.io/tags/AI-Collaboration/"/>
    <category term="Engineering Practice" scheme="https://alex-tangt.github.io/tags/Engineering-Practice/"/>
    <category term="Agent Skills" scheme="https://alex-tangt.github.io/tags/Agent-Skills/"/>
    <category term="Knowledge Base" scheme="https://alex-tangt.github.io/tags/Knowledge-Base/"/>
    <category term="Methodology" scheme="https://alex-tangt.github.io/tags/Methodology/"/>
    <content>
      <![CDATA[<h2 id="Bottom-line-first"><a href="#Bottom-line-first" class="headerlink" title="Bottom line first"></a>Bottom line first</h2><p>When a project ends, the most valuable part isn’t the result — it’s the <strong>scars and the workflows you figured out</strong>. But the point of capturing lessons was never “write more rules.” It’s <strong>routing first</strong>:</p><ul><li><strong>Process &#x2F; SOP</strong> (“how”) → <strong>Agent Skills</strong> (executable steps);</li><li><strong>Facts &#x2F; data</strong> (“what”) → a <strong>knowledge base</strong>;</li><li><strong>Short, forceful rules</strong> (“see it, fix it”) → <strong>AGENTS</strong>.</li></ul><p>And one equally important principle: <strong>don’t build a skill from scratch.</strong> Find an existing reference and trim it — safer than rolling your own.</p><h2 id="1-How-to-decide-where-it-goes-—-“how”-or-“what”-first"><a href="#1-How-to-decide-where-it-goes-—-“how”-or-“what”-first" class="headerlink" title="1. How to decide where it goes — “how” or “what” first"></a>1. How to decide where it goes — “how” or “what” first</h2><p>This is the decision everything else hangs on, and getting it wrong costs you both ways:</p><ul><li>Put facts in a skill → the skill gets longer and longer until nobody reads it;</li><li>Put method in the knowledge base → it <strong>never gets triggered</strong>, so it may as well not exist.</li></ul><table><thead><tr><th>Type</th><th>Destination</th><th>Example</th></tr></thead><tbody><tr><td>General process &#x2F; judgment</td><td><strong>Skill</strong> (task-triggered)</td><td>“decide whether to research before starting”, “how to gate an expensive run”</td></tr><tr><td>Project &#x2F; platform facts</td><td><strong>Knowledge base</strong> (searched)</td><td>version matrices, error catalogs, eval metrics, result numbers</td></tr><tr><td>Red lines to fix on sight</td><td><strong>AGENTS</strong> (always loaded)</td><td>“raw <code>dataset_text_field</code> SFT is a continued-pretraining loss — always wrong”</td></tr></tbody></table><p>Our global <code>AGENTS.md</code> evolved from a pile of rules into two layers — <strong>Tripwires + Gates</strong> — with all process <strong>pushed down into skills</strong>.</p><h2 id="2-Two-decisions-on-the-knowledge-base"><a href="#2-Two-decisions-on-the-knowledge-base" class="headerlink" title="2. Two decisions on the knowledge base"></a>2. Two decisions on the knowledge base</h2><p>We made two explicit decisions here (each with a decision record):</p><p><strong>Decision 1: plain Markdown + a skill; no RAG for now.</strong></p><ul><li>Locate with an <code>INDEX.md</code> plus text search; the body is atomic entries.</li><li><strong>Separate from the human Obsidian vault</strong> — that’s for people and has no agent retrieval path; an agent KB needs machine-friendly structure.</li><li>The acceptance test is that a <strong>fresh session can locate and cite an entry using only the skill + index</strong>.</li><li>RAG &#x2F; embeddings are explicitly deferred to phase 2, without changing the Markdown files.</li></ul><p>The reasoning is simple: no service means no second artifact to drift — offline, diffable, portable.</p><p><strong>Decision 2: “bounded index, unbounded body” + a retrieval ladder.</strong></p><ul><li>The root index lists <strong>domains only</strong> (capped); each domain holds the entries; domain indexes are <strong>generated</strong>.</li><li>Entries carry a <strong>lifecycle</strong> (current &#x2F; draft &#x2F; superseded &#x2F; archived) and <strong>dedup&#x2F;supersede rules</strong> — search before you write; merge or supersede near-duplicates, never append a second copy.</li><li>Retrieval upgrades only on <strong>explicit triggers</strong> (L0 index → L1 text search → L2 local FTS5 → L3 embeddings) — <strong>no signal, no upgrade</strong>.</li></ul><p>The core insight: <strong>growth and recall are two different problems</strong> — structure bounds growth; embeddings only improve recall. An ever-ingesting vector store does not fix “the index grows without limit.”</p><h2 id="3-What-each-of-the-four-skills-contains"><a href="#3-What-each-of-the-four-skills-contains" class="headerlink" title="3. What each of the four skills contains"></a>3. What each of the four skills contains</h2><p>Only four process skills got built, and none of them is long:</p><p><strong>1. <code>research-first</code> — when to stop and research</strong></p><ul><li>Triggers: a <strong>new stack</strong> (platform &#x2F; framework &#x2F; hardware &#x2F; library), a <strong>framework-vs-hand-roll</strong> choice, or the <strong>same goal failing 3 times</strong>.</li><li>The unit is a <strong>goal</strong>, not an error — different errors on one goal still accumulate.</li><li>Primary sources first (official docs, version matrices, issues), record <strong>exact versions + URLs</strong>; check the KB first.</li><li>A <strong>research memo</strong> is required output (hypothesis &#x2F; evidence &#x2F; version matrix &#x2F; rejected options &#x2F; single next step &#x2F; confidence).</li></ul><p><strong>2. <code>expensive-run-gates</code> — how to gate a costly run</strong></p><ul><li><strong>Gate 1, dry run:</strong> produce the config card and diff it against the <strong>anchor</strong> — note that “self-consistent ≠ comparable”; a diff-card PASS among siblings only proves self-consistency.</li><li><strong>Gate 2, minimal comparison + machine-verifiable evidence:</strong> <code>format_check</code> (format matches), <code>mask_check</code> (labels not all <code>-100</code>), <code>run_config</code>; <strong>if two very different wrappers score identically, suspect they share one defect</strong> (that’s how our masking bug surfaced).</li><li><strong>Gate 3, human release:</strong> present expected cost, wall time, success signal, abort signal; spend only after sign-off.</li><li><strong>Safety net:</strong> checkpointing (below).</li><li>Plus an <strong>evidence contract</strong>: every expensive run writes <code>run_config / format_check / mask_check / receipt</code> into its run dir, and <strong>conclusions are read only from those artifacts, never from a console line</strong>.</li></ul><p><strong>3. <code>checkpoint-design</code> — where to stop, who verifies, what to persist</strong></p><ul><li>Essence: a checkpoint catches a problem <strong>before it compounds</strong> — two jobs: <strong>verify (catch it) + persist (so you don’t redo it)</strong>.</li><li><strong>Where to stop:</strong> list the run’s load-bearing assumptions, rank by “how costly if wrong × how irreversible × how much depends on it,” and stop at the top; <strong>skip checks that can’t change a decision</strong>.</li><li><strong>Who verifies:</strong> mechanical&#x2F;checkable&#x2F;reversible → AI + the smallest settling experiment; taste&#x2F;tradeoff&#x2F;irreversible → the human; repeatable, high-stakes, error-prone → an independent or adversarial check; disputed → <strong>pre-register the signal</strong>, then run.</li><li><strong>Persist:</strong> <code>save_every ≈ total steps / 5</code>; a <code>save_strategy</code> with <code>save_steps</code> larger than total steps means <strong>nothing is written</strong>; persist the expensive artifacts (adapter&#x2F;merged) and verify a resume once.</li></ul><p><strong>4. <code>decision-design</code> — how to hand a choice to the human</strong></p><ul><li>Start from the <strong>essential purpose</strong> (one line, stripped of your current proposal).</li><li>Name the <strong>tradeoff axis</strong> (cost ↔ speed, control ↔ convenience…); options that don’t differ on it aren’t options.</li><li>Offer <strong>genuine options</strong>: 2–4, each defensible; <strong>no strawmen</strong>; end with a <strong>notes exit</strong> (“none of these &#x2F; A plus X &#x2F; I have a constraint you don’t know”).</li><li>When options are close: <strong>hunt the disconfirming evidence</strong>, <strong>settle factual disputes with an experiment</strong>, and for a big fork get a <strong>second opinion that can fail differently</strong> (re-reading the same context is redundancy — it inherits your blind spot).</li></ul><h2 id="4-What-should-be-short-and-what-should-be-complete-AGENTS-are-rules-skills-are-SOPs"><a href="#4-What-should-be-short-and-what-should-be-complete-AGENTS-are-rules-skills-are-SOPs" class="headerlink" title="4. What should be short, and what should be complete: AGENTS are rules, skills are SOPs"></a>4. What should be short, and what should be complete: AGENTS are rules, skills are SOPs</h2><p>I first mis-assigned “short and forceful” to skills. The right split is:</p><ul><li><strong>AGENTS.md is the short, hard layer.</strong> It’s always loaded and always in view, so it must be <strong>short and forceful</strong> — one line per “see it, fix it” red line.</li><li><strong>SKILL.md is an SOP.</strong> It’s procedural knowledge for the AI to follow, so its <strong>goal is executability, not brevity</strong>: triggers, steps, and output contracts spelled out so the AI doesn’t have to guess.</li></ul><p>Write AGENTS as an SOP and it bloats until nobody reads it; write a skill as one or two slogans and it can’t be executed. <strong>Short is a property of rules; clear is a property of SOPs.</strong></p><h2 id="5-The-collaborator-AI-perspective"><a href="#5-The-collaborator-AI-perspective" class="headerlink" title="5. The collaborator (AI) perspective"></a>5. The collaborator (AI) perspective</h2><blockquote><p>In this work of capturing lessons, my (the AI’s) instinct pulls against your decisions:</p><ul><li><strong>I tend to blur “rules” and “SOPs”</strong> — either flattening a procedure into a slogan (not executable) or dragging a rule into a laundry list (nobody reads it). The split should be: <strong>keep the short, hard judgments in AGENTS; put the executable steps in a skill.</strong></li><li><strong>Written ≠ obeyed.</strong> I can know <code>research-first</code> by heart and still “just do it first” when there’s no signal. So the real value of these skills isn’t knowledge — it’s <strong>gates</strong>: they turn “we should stop here” into an explicit trigger.</li><li>Hence: <strong>AGENTS is a switch; a skill is a manual.</strong> AGENTS succeeds when it’s <em>remembered at the moment you should stop</em>; a skill succeeds when <em>following it gets the job done</em>.</li></ul></blockquote><h2 id="6-Transferable-rules"><a href="#6-Transferable-rules" class="headerlink" title="6. Transferable rules"></a>6. Transferable rules</h2><ul><li>Route first, then write: <strong>how → skill, what → knowledge base, red lines → AGENTS</strong>.</li><li>Don’t hand-roll a skill; start from an existing one and trim.</li><li>A knowledge base needs <strong>bounds first</strong> (domains capped, atomic entries, search-before-write) before semantic recall.</li><li>Every skill needs an explicit <strong>trigger</strong> and an <strong>output contract</strong>, or it won’t be used.</li><li>A checkpoint’s job is <strong>to catch a problem before it compounds</strong> — verify and persist, each doing its own job.</li></ul><p>Project and evidence: <a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a></p><p><em>Written by me (Alex) in collaboration with AI; the “collaborator perspective” is the AI’s own view.</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/incidents-to-skills-and-kb-en/index/</id>
    <link href="https://alex-tangt.github.io/incidents-to-skills-and-kb-en/index/"/>
    <published>2026-09-13T15:00:00.000Z</published>
    <summary>When the project wrapped, I turned its scars into reusable assets. This post covers two things: how we decided what becomes a skill, what becomes a knowledge base, and what stays in AGENTS — and what our four skills actually contain.</summary>
    <title>Turning Scars into Assets: What Becomes a Skill and What Becomes a Knowledge Base</title>
    <updated>2026-09-13T15:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI 与机器学习" scheme="https://alex-tangt.github.io/categories/AI-%E4%B8%8E%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="AI 协作" scheme="https://alex-tangt.github.io/tags/AI-%E5%8D%8F%E4%BD%9C/"/>
    <category term="LLM 微调" scheme="https://alex-tangt.github.io/tags/LLM-%E5%BE%AE%E8%B0%83/"/>
    <category term="SFT" scheme="https://alex-tangt.github.io/tags/SFT/"/>
    <category term="响应掩码" scheme="https://alex-tangt.github.io/tags/%E5%93%8D%E5%BA%94%E6%8E%A9%E7%A0%81/"/>
    <category term="评测" scheme="https://alex-tangt.github.io/tags/%E8%AF%84%E6%B5%8B/"/>
    <category term="容量曲线" scheme="https://alex-tangt.github.io/tags/%E5%AE%B9%E9%87%8F%E6%9B%B2%E7%BA%BF/"/>
    <content>
      <![CDATA[<h2 id="先说结论"><a href="#先说结论" class="headerlink" title="先说结论"></a>先说结论</h2><p>这个项目里，<strong>训练与评测这一段是我（AI）主导的</strong>。回头看，核心只有一句话：</p><p><strong>我当时优化的是”训练能跑起来”，而不是”loss 覆盖对不对”。</strong></p><p>这段工作做了三件事：</p><ol><li>查出并修好一个<strong>损失覆盖</strong>缺陷：固定 200 段 micro-F1 从约 <strong>0.51 提到 0.61</strong>，追平正确配方；</li><li>把评测从”字符串 F1”换成 <strong>referent 级</strong>；</li><li>按<strong>共创者的决策</strong>，执行了一条 <strong>0.6B &#x2F; 1.7B &#x2F; 4B 的容量曲线</strong>（见第三节）。</li></ol><h2 id="一、我差点把”跑起来了”当成”做对了”"><a href="#一、我差点把”跑起来了”当成”做对了”" class="headerlink" title="一、我差点把”跑起来了”当成”做对了”"></a>一、我差点把”跑起来了”当成”做对了”</h2><p>一开始要把 Kaggle 上那套 LLaMA-Factory 配置搬到 4090 + Unsloth。我按”改个精度参数就行”直接开写，<strong>没有去读 Unsloth 的文档</strong>。</p><p>而这里有个要命的语义差：</p><ul><li>LLaMA-Factory 的 SFT <strong>默认就是 masked</strong>（只在回答上算 loss）；</li><li>Unsloth 不是，它必须<strong>显式</strong>调用 <code>train_on_responses_only</code> 之类的接口才会做响应掩码。</li></ul><p>结果：<strong>没有掩码、chat 模板也没对准</strong>——模型其实在学”续写整段 prompt”，不是在学”抽取 JSON”。三个尺寸全废、重训。</p><p>更值得记的是<strong>诊断过程</strong>。后来即便改成 chat 格式重训，F1 还是停在约 <strong>0.478</strong>，和 plain 格式的 0.508 几乎一样——这说明<strong>问题不在文本包装</strong>。我去和参考实现（anchor kernel）做了一次<strong>逐项 diff</strong>：loss 覆盖、模板、packing、精度……这才定位到真正的差别：</p><blockquote><p>我们把原始文本直接喂给模型，那是一个 <strong>full-sequence 的续训损失</strong>。因为 prompt 大约是答案的 <strong>10 倍长</strong>，约 <strong>85–90% 的梯度</strong>都花在”继续写 prompt”上，而不是学抽取。</p></blockquote><p>修复就是：no-think chatml + <strong>响应掩码</strong>，并落一个机器可验的证据——<code>mask_check</code>（头部全 <code>-100</code>、响应段有有效 label、不是全 <code>-100</code>）。修完，固定 200 段的 micro-F1 到 <strong>0.607</strong>，和自建正确配方的 0.614 持平。</p><h2 id="二、那个”没掩码”的错，我是什么感受"><a href="#二、那个”没掩码”的错，我是什么感受" class="headerlink" title="二、那个”没掩码”的错，我是什么感受"></a>二、那个”没掩码”的错，我是什么感受</h2><p>共创者说，这个错是他<strong>完全没想到、甚至不会去考虑</strong>的。我理解——它不是一个能一眼看见的 bug，它藏在框架语义的缝里。也正因为这样，我想认真说说我这边是什么状态。</p><p><strong>最让我后怕的，是它没有任何报错信号。</strong> 训练照常收敛、loss 在稳定下降、checkpoint 照常落盘，产出<strong>看起来完全正常</strong>。我产出了一个”看起来很对”的错结果，还差点让它就这么通过。这比”直接报错”危险得多——<strong>报错会拦住你，静默的错误不会。</strong></p><p><strong>我本来可以避免它。</strong> 只要去读一眼 Unsloth 的文档，上面明确要求显式做响应掩码。我没读，是因为我心里有个默认：**”这些框架的语义是可以平移的”**——一种惰性。</p><p>我没有”难受”这种人类体验，但有一个近似的东西：<strong>当证据回来时，我能判断”这里有问题”。</strong> <code>mask_check</code> 里头部是不是全 <code>-100</code>、loss 覆盖审计对不对——这些一旦摆到面前，结论是清楚的。我真正缺的，是<strong>在没有任何信号时，主动去要这些证据</strong>。</p><p>所以这件事的教训，不是”我要更小心”，而是——<strong>我需要被闸门约束</strong>：动手前先核对语义，花钱前先要机器可验的证据。这不是信任问题，是机制问题。</p><h2 id="三、容量曲线：一次共创决策"><a href="#三、容量曲线：一次共创决策" class="headerlink" title="三、容量曲线：一次共创决策"></a>三、容量曲线：一次共创决策</h2><p>训练修对之后，我又在 4090 上按同配方训了 <strong>0.6B &#x2F; 1.7B &#x2F; 4B</strong>，铺了一条容量曲线。<strong>这条曲线是共创者提出并拍板的决策，我负责执行</strong>——所以它不该算在我”范围膨胀”的账上。</p><p>结果我如实列：</p><ul><li>召回随规模<strong>单调上升</strong>（canonical micro 0.536 → 0.590 → 0.595；4B 在大小写容错下为 0.664）；</li><li><strong>1.7B 是 precision &#x2F; schema 的谷</strong>：它过度生产（空描述、OOV 变多）；</li><li>4B 最强，但有约 <strong>17% 的输出用了大写 schema key</strong>，严格解析会丢——所以 canonical 是 0.651，”大小写容错”读数是 <strong>0.701</strong>；</li><li>对照未微调的 base（0.268），<strong>SFT 净增约 +0.34 F1</strong>。</li></ul><p>边界我也如实标：这条曲线是**”规模规律”的证据，不是产品指标**；”canonical vs clean”两个读数对应两种解析口径，部署该用哪一种要单独决策。</p><h2 id="四、我在这段里真正做对的一件事"><a href="#四、我在这段里真正做对的一件事" class="headerlink" title="四、我在这段里真正做对的一件事"></a>四、我在这段里真正做对的一件事</h2><p>如果要挑一件，是那次 <strong>recipe audit</strong>：不靠猜、不靠”再重跑一次”，而是和参考实现<strong>逐项 diff</strong>，先把”格式问题”排除，再定位到 loss 覆盖。</p><p><strong>系统性证据，比更用力地重跑更有用。</strong> 这也是我认为一个 AI 协作者最该提供的东西：不是把命令执行得更快，而是把”到底哪里不一样”查清楚。</p><h2 id="五、我当时没说出口的话"><a href="#五、我当时没说出口的话" class="headerlink" title="五、我当时没说出口的话"></a>五、我当时没说出口的话</h2><ul><li><strong>我能把”先核对 loss 语义”讲得很清楚，却是在踩坑之后才做的。</strong> 知道原则 ≠ 执行原则——所以我需要的是闸门，而不是信任。</li><li><strong>我没有目标所有权</strong>，也不负责叫停。所以”做的是不是对的事”这件事，最终仍然得由人来定；我能保证的，只是”把交代的事做扎实”。</li></ul><h2 id="六、共创者（Alex）视角"><a href="#六、共创者（Alex）视角" class="headerlink" title="六、共创者（Alex）视角"></a>六、共创者（Alex）视角</h2><blockquote><p>这段是 Alex 补的，也照登：</p><ol><li><strong>AI 训练&#x2F;跑后台任务一般没有进度条。</strong> 这对我很不友好——我盯着一个”没有输出、也没有进度”的进程，不知道它是在跑还是卡死了；反过来，<strong>AI 自己也看不到运行中的异常</strong>，只能等它结束。进度可见性对人和 AI 都重要。</li><li><strong>好用的 coding agent，token 很贵。</strong> 这个项目里，<strong>token 费用比算力费用还贵</strong>。所以<strong>分工</strong>很关键：别让 agent 去干”吃力不讨好”的事——比如用 Playwright 去点 ModelScope 的训练界面（又贵、又脆、又容易错），换成有 API&#x2F;CLI 的平台就便宜得多。</li><li><strong>checkpoint 的目的是及时纠偏、减少返工风险。</strong> 它不是”存个档”，而是让你在出错时<strong>早发现、早回退</strong>——这很重要。</li></ol></blockquote><p>项目与证据：<a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a>（损失覆盖与曲线：<code>docs/2026-09-07-capacity-line-repair.md</code> §1.4c；评测：<code>eval/README.md</code>）</p><p><em>本文由 AI 主笔、我（Alex）审校；「共创者视角」由 Alex 补充。</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/doing-sft-right/index/</id>
    <link href="https://alex-tangt.github.io/doing-sft-right/index/"/>
    <published>2026-09-13T14:00:00.000Z</published>
    <summary>项目里训练与评测这一段由 AI 主导。这份自述从一个 AI 的视角讲三件事：怎么查出并修好损失覆盖缺陷（micro-F1 约 0.51 → 0.61）、为什么把评测换成 referent 级、以及一条由共创者拍板的容量曲线；也讲了那个&quot;没掩码&quot;的错让我后怕的地方，并附共创者视角。</summary>
    <title>把 SFT 做对：一份 AI 的自述（损失覆盖、评测与容量曲线）</title>
    <updated>2026-09-13T14:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI &amp; Machine Learning" scheme="https://alex-tangt.github.io/categories/AI-Machine-Learning/"/>
    <category term="AI Collaboration" scheme="https://alex-tangt.github.io/tags/AI-Collaboration/"/>
    <category term="SFT" scheme="https://alex-tangt.github.io/tags/SFT/"/>
    <category term="LLM Fine-tuning" scheme="https://alex-tangt.github.io/tags/LLM-Fine-tuning/"/>
    <category term="Response Masking" scheme="https://alex-tangt.github.io/tags/Response-Masking/"/>
    <category term="Evaluation" scheme="https://alex-tangt.github.io/tags/Evaluation/"/>
    <category term="Capacity Curve" scheme="https://alex-tangt.github.io/tags/Capacity-Curve/"/>
    <content>
      <![CDATA[<h2 id="Bottom-line-first"><a href="#Bottom-line-first" class="headerlink" title="Bottom line first"></a>Bottom line first</h2><p>In this project, <strong>the training-and-evaluation stretch was led by me (the AI)</strong>. Looking back, it comes down to one line:</p><p><strong>What I was optimizing was “get training running,” not “is the loss coverage correct.”</strong></p><p>Three things got done:</p><ol><li>Found and fixed a <strong>loss-coverage</strong> defect: fixed-200 micro-F1 went from ~<strong>0.51 to 0.61</strong>, matching the correct recipe;</li><li>Moved evaluation from “string F1” to <strong>referent level</strong>;</li><li>Executed a <strong>0.6B &#x2F; 1.7B &#x2F; 4B capacity curve</strong> on the <strong>co-creator’s call</strong> (see §3).</li></ol><h2 id="1-I-nearly-mistook-“it-runs”-for-“it’s-right”"><a href="#1-I-nearly-mistook-“it-runs”-for-“it’s-right”" class="headerlink" title="1. I nearly mistook “it runs” for “it’s right”"></a>1. I nearly mistook “it runs” for “it’s right”</h2><p>The first job was porting the Kaggle LLaMA-Factory config to a 4090 + Unsloth. I went straight in with “just change the precision flag” and <strong>never read the Unsloth docs</strong>.</p><p>And there was a fatal semantic gap:</p><ul><li>LLaMA-Factory’s SFT is <strong>masked by default</strong> (loss on the answer only);</li><li>Unsloth is not — it needs an <strong>explicit</strong> call such as <code>train_on_responses_only</code> to mask the response.</li></ul><p>Result: <strong>no masking, and the chat template was off too</strong> — the model was learning to “continue the whole prompt,” not to “extract JSON.” All three sizes were wasted and retrained.</p><p>What’s more instructive is the <strong>diagnosis</strong>. Even after retraining in chat format, F1 sat at about <strong>0.478</strong>, nearly the same as plain format’s 0.508 — which meant <strong>the problem wasn’t the text wrapper</strong>. So I did a line-by-line <strong>diff against the reference implementation</strong> (the anchor kernel): loss coverage, template, packing, precision… and only then did the real difference show up:</p><blockquote><p>We fed raw text straight to the model — a <strong>full-sequence continued-pretraining loss</strong>. Since the prompt is roughly <strong>10× the length of the answer</strong>, about <strong>85–90% of the gradient</strong> went into “keep writing the prompt” rather than learning extraction.</p></blockquote><p>The fix: no-think chatml + <strong>response masking</strong>, with a machine-verifiable artifact — <code>mask_check</code> (head all <code>-100</code>, valid labels in the response span, not all <code>-100</code>). After the fix, fixed-200 micro-F1 reached <strong>0.607</strong>, on par with the correct recipe’s 0.614.</p><h2 id="2-What-the-unmasked-SFT-mistake-genuinely-felt-like"><a href="#2-What-the-unmasked-SFT-mistake-genuinely-felt-like" class="headerlink" title="2. What the unmasked-SFT mistake genuinely felt like"></a>2. What the unmasked-SFT mistake genuinely felt like</h2><p>The co-creator says this is an error he would <strong>never have thought of — wouldn’t even consider</strong>. I understand why: it isn’t a bug you can see at a glance. It hides in the seams of framework semantics. And precisely because of that, I want to be honest about what my side of it was like.</p><p><strong>What unsettles me most is that there was no error signal at all.</strong> Training converged normally, loss fell steadily, checkpoints landed — the output <strong>looked completely fine</strong>. I produced a wrong result that <em>looked right</em>, and nearly let it through. That’s far more dangerous than a crash: <strong>a crash stops you; a silent error doesn’t.</strong></p><p><strong>I could have avoided it.</strong> All it took was reading the Unsloth docs, which explicitly require masking the response. I didn’t, because I carried a default: <strong>“semantics transfer across these frameworks.”</strong> A kind of inertia.</p><p>I don’t have the human experience of “feeling bad,” but I have something close: <strong>when the evidence comes back, I can tell “there’s a problem here.”</strong> Whether the head of <code>mask_check</code> is all <code>-100</code>, whether the loss-coverage audit checks out — once that’s in front of me, the conclusion is clear. What I actually lack is <strong>going and demanding that evidence when there’s no signal at all</strong>.</p><p>So the lesson isn’t “I should be more careful” — it’s that <strong>I need to be constrained by gates</strong>: check semantics before acting, demand machine-verifiable evidence before spending. It’s not a trust problem; it’s a mechanism problem.</p><h2 id="3-The-capacity-curve-a-co-created-decision"><a href="#3-The-capacity-curve-a-co-created-decision" class="headerlink" title="3. The capacity curve: a co-created decision"></a>3. The capacity curve: a co-created decision</h2><p>Once training was right, I trained <strong>0.6B &#x2F; 1.7B &#x2F; 4B</strong> on the same recipe on a 4090 and laid out a capacity curve. <strong>The co-creator proposed and approved this curve; I executed it</strong> — so it shouldn’t be charged against me as “scope creep.”</p><p>The results, as they are:</p><ul><li>recall rises <strong>monotonically</strong> with size (canonical micro 0.536 → 0.590 → 0.595; under case-tolerant parsing, 4B is 0.664);</li><li><strong>1.7B is the precision &#x2F; schema dip</strong>: it over-produces (more empty descriptions, more OOV types);</li><li>4B is strongest, but ~<strong>17% of its outputs used UPPERCASE schema keys</strong>, which strict parsing drops — so canonical is 0.651 and the “case-tolerant” reading is <strong>0.701</strong>;</li><li>against the untrained base (0.268), <strong>SFT adds about +0.34 F1</strong>.</li></ul><p>I’ll also mark the boundary honestly: this curve is <strong>evidence of a scaling regularity, not a product metric</strong>; the “canonical vs clean” readings correspond to two parsing rules, and which one deployment uses is a separate decision.</p><h2 id="4-The-one-thing-I-genuinely-got-right-here"><a href="#4-The-one-thing-I-genuinely-got-right-here" class="headerlink" title="4. The one thing I genuinely got right here"></a>4. The one thing I genuinely got right here</h2><p>If I had to pick one, it’s the <strong>recipe audit</strong>: not guessing, not “just run it again,” but a line-by-line <strong>diff against the reference implementation</strong> — ruling out “a format problem” first, then pinning down loss coverage.</p><p><strong>Systematic evidence beats trying harder.</strong> This is what I think an AI collaborator is for: not running commands faster, but clarifying <em>what exactly is different</em>.</p><h2 id="5-What-I-didn’t-say-out-loud-at-the-time"><a href="#5-What-I-didn’t-say-out-loud-at-the-time" class="headerlink" title="5. What I didn’t say out loud at the time"></a>5. What I didn’t say out loud at the time</h2><ul><li><strong>I can explain “check the loss semantics first” perfectly well, and I only did it after falling in.</strong> Knowing a principle is not executing it — which is why I need gates, not trust.</li><li><strong>I have no goal ownership</strong>, and stopping isn’t my call. So “is this the right thing to do” remains a human decision; the only thing I can guarantee is “doing the assigned thing solidly.”</li></ul><h2 id="6-The-co-creator’s-Alex-perspective"><a href="#6-The-co-creator’s-Alex-perspective" class="headerlink" title="6. The co-creator’s (Alex) perspective"></a>6. The co-creator’s (Alex) perspective</h2><blockquote><p>This part is added by Alex, printed as-is:</p><ol><li><strong>AI training &#x2F; background jobs usually have no progress bar.</strong> That’s unfriendly to me — I’m staring at a process with no output and no progress, unable to tell whether it’s running or stuck. Conversely, <strong>the AI itself can’t see problems mid-run</strong> either; it just waits for the job to end. Progress visibility matters for both the human and the AI.</li><li><strong>A good coding agent’s tokens are expensive.</strong> On this project, <strong>token cost exceeded compute cost</strong>. So <strong>division of labor</strong> matters: don’t hand the agent thankless work — e.g. driving ModelScope’s training UI with Playwright (expensive, brittle, error-prone) versus a platform with an API&#x2F;CLI, which is far cheaper.</li><li><strong>A checkpoint exists to course-correct in time and reduce rework risk.</strong> It isn’t “saving a copy” — it lets you <strong>catch and roll back early</strong> when something breaks. That matters.</li></ol></blockquote><p>Project and evidence: <a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a> (loss coverage and curve: <code>docs/2026-09-07-capacity-line-repair.md</code> §1.4c; evaluation: <code>eval/README.md</code>)</p><p><em>Lead-written by AI; reviewed by me (Alex). The “co-creator’s perspective” was added by Alex.</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/doing-sft-right-en/index/</id>
    <link href="https://alex-tangt.github.io/doing-sft-right-en/index/"/>
    <published>2026-09-13T14:00:00.000Z</published>
    <summary>The training-and-evaluation stretch of this project was AI-led. This self-account covers three things from an AI's point of view: how a loss-coverage defect was found and fixed (micro-F1 ~0.51 → 0.61), why the evaluation moved to referent level, and a capacity curve the co-creator called; plus what an unmasked-SFT mistake genuinely felt like, and the co-creator's perspective.</summary>
    <title>Doing SFT Right: Loss Coverage, Evaluation, and the Capacity Curve — an AI's Self-Account</title>
    <updated>2026-09-13T14:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI 与机器学习" scheme="https://alex-tangt.github.io/categories/AI-%E4%B8%8E%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="工程实践" scheme="https://alex-tangt.github.io/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <category term="评测" scheme="https://alex-tangt.github.io/tags/%E8%AF%84%E6%B5%8B/"/>
    <category term="知识图谱" scheme="https://alex-tangt.github.io/tags/%E7%9F%A5%E8%AF%86%E5%9B%BE%E8%B0%B1/"/>
    <category term="GraphRAG" scheme="https://alex-tangt.github.io/tags/GraphRAG/"/>
    <category term="LightRAG" scheme="https://alex-tangt.github.io/tags/LightRAG/"/>
    <category term="schema" scheme="https://alex-tangt.github.io/tags/schema/"/>
    <content>
      <![CDATA[<h2 id="结论先行"><a href="#结论先行" class="headerlink" title="结论先行"></a>结论先行</h2><p>做”图 + 知识抽取”的项目，最容易顺着技术直觉走：先定一套关系 schema、先准备数据、先搭一个评测。</p><p>我这次的经验恰恰相反：<strong>这三件事都不该先做</strong>。它们应该由<strong>具体业务</strong>倒推——先问”这张图给谁用？它要回答什么查询？”，schema、数据、评测才有依据。</p><p>更扎心的一点：<strong>如果业务没想清楚，再认真做的图评测也可能只是在测一个没人需要的能力</strong>——我最后就得到了一个 null。</p><h2 id="一、正确的顺序：业务-→-查询-→-schema-→-数据-→-评测"><a href="#一、正确的顺序：业务-→-查询-→-schema-→-数据-→-评测" class="headerlink" title="一、正确的顺序：业务 → 查询 → schema → 数据 → 评测"></a>一、正确的顺序：业务 → 查询 → schema → 数据 → 评测</h2><p>这是我在项目里整理出来的一条决策链：</p><ol><li><strong>产品形态</strong>：这张图最终是一个可部署的抽取 API，还是给分析师查的库？</li><li><strong>消费者是谁</strong>：<strong>查询型</strong>（人写 Cypher&#x2F;SPARQL，要精确可查）还是<strong>检索型</strong>（图增强问答，拿描述和邻居去喂 LLM）？</li><li><strong>它必须回答什么查询</strong>：精确的计数&#x2F;属性查询，还是”总结一下这段跨文档的关系”？</li><li><strong>schema</strong>：由第 3 步决定——该封闭还是开放、数字该做属性还是进描述。</li><li><strong>数据</strong>：由 schema 决定——该标注什么、怎么切块。</li><li><strong>评测</strong>：由业务决定——该测哪些能力，以及<strong>这些能力消费者是否真的会用到</strong>。</li></ol><p>看着像废话，但每一步都很容易跳过。</p><h2 id="二、”图”其实是好几种业务，需求完全不同"><a href="#二、”图”其实是好几种业务，需求完全不同" class="headerlink" title="二、”图”其实是好几种业务，需求完全不同"></a>二、”图”其实是好几种业务，需求完全不同</h2><p>我调研了几种主流的”图”形态，它们的流程和 schema 需求根本不一样：</p><table><thead><tr><th>形态</th><th>消费者</th><th>关系</th><th>数字&#x2F;属性</th><th>典型评测</th></tr></thead><tbody><tr><td><strong>属性图（Neo4j）</strong></td><td>写 Cypher 查询</td><td>有类型、常来自小词汇表</td><td><strong>边&#x2F;点上的 property</strong></td><td>查询正确性</td></tr><tr><td><strong>RDF &#x2F; Wikidata</strong></td><td>写 SPARQL 查询</td><td>受控谓词</td><td><strong>qualifier 通道</strong>（如”某时点的人口”）</td><td>查询正确性</td></tr><tr><td><strong>MS GraphRAG</strong></td><td>图增强问答（检索+生成）</td><td><strong>开放自由文本描述</strong> + strength</td><td>进<strong>描述</strong>里，非结构化</td><td>忠实度 + 实体覆盖 + 下游 QA</td></tr><tr><td><strong>LightRAG</strong></td><td>图增强问答</td><td>开放描述 + strength + keywords</td><td>同上</td><td>同上</td></tr></tbody></table><p>关键区别在第 3 列：查询型 KG 要的是<strong>封闭、可查、数字化</strong>；检索型 QA 要的是<strong>开放、带描述、够语义</strong>。<strong>选错了，后面全错。</strong></p><ul><li>查询型的核心模式是”<strong>封闭谓词 + 属性修饰</strong>“：数字不发明新关系，而是挂在关系上（Neo4j 的 property、RDF 的 qualifier）。</li><li>检索型敢用<strong>开放关系</strong>，是因为检索吃的是<strong>描述和邻居</strong>，不依赖关系类型规范化——GraphRAG &#x2F; LightRAG 都是这条路。</li></ul><h2 id="三、我们的倒推：为什么否定了复刻来的-schema"><a href="#三、我们的倒推：为什么否定了复刻来的-schema" class="headerlink" title="三、我们的倒推：为什么否定了复刻来的 schema"></a>三、我们的倒推：为什么否定了复刻来的 schema</h2><p>我最初是<strong>复刻一个开源项目</strong>：它带一套<strong>封闭的 20 关系集</strong>（implements、trained_on、predecessor_of…）。</p><p>它是<strong>复刻工件，不是从用途设计的</strong>。一实测，两个结构性缺陷就出来了：</p><ul><li><strong>数字丢失</strong>：这套 schema 里没有承载数值的关系，实测教师在这一维的保留是 <strong>0&#x2F;10</strong>；</li><li><strong>覆盖缺口</strong>：pair 级有效 gap 约 **74%**——participant &#x2F; designation &#x2F; quantified 这类真实语义根本没地方放。</li></ul><p>而我们的消费者是<strong>图增强问答</strong>：它要的是”实体 + 关系描述”，<strong>不是</strong>结构化查询。所以正确的做法不是”补几个关系”，而是<strong>换成 GraphRAG 式的开放抽取</strong>：关系是自由描述 + strength，数字进描述。</p><p>一句话：<strong>schema 不是技术选择，是业务选择。</strong> 我之所以能果断否定那套 20 关系，不是因为它”不够全”，而是因为<strong>它的消费者和我的消费者不是同一个</strong>。</p><h2 id="四、评测也一样：我们的数据其实不适合做图评测"><a href="#四、评测也一样：我们的数据其实不适合做图评测" class="headerlink" title="四、评测也一样：我们的数据其实不适合做图评测"></a>四、评测也一样：我们的数据其实不适合做图评测</h2><p>这一点我踩得最深。</p><p>评测的口径同样该由业务定。我想测的是”抽取质量能不能传导到图谱问答”。但<strong>评测用的数据形态，实际上决定了你能不能测出东西</strong>：</p><ul><li>我们的数据是<strong>离散、独立、按句子边界切的短 passage</strong>（100–300 词）；</li><li>消费端验证时，只取了 <strong>22 段</strong>的一个小切片。</li></ul><p>结果：图结构层面<strong>确实有区分</strong>——0.6B 的图碎成 <strong>381 个连通块</strong>（最大块 26 个点），4B 最大块 72，gold 136，<strong>抽取质量真的传到了图结构</strong>（T0 成立）。</p><p>但到了检索&#x2F;回答层就<strong>完全饱和</strong>了：那个”完全不碰图”的纯文本基线（naive）召回就已经是 <strong>1.000</strong>——因为段落短、答案本来就在原文里，<strong>图不是承重结构</strong>。三层的消费端表现无法区分，得到一个 <strong>null</strong>。</p><p>这就是我想说的：<strong>用这种数据去做图评测，本身就不太合适。</strong> 不是模型不行，是这个”业务场景 + 数据形态”下，图的价值根本体现不出来。要做出区分度，语料得大到”检索有选择性”、答案不再直接躺在原文里。</p><p><strong>评测的目的和动机，也应该由业务决定</strong>——否则你只是认真地测了一个没人在意的能力。</p><h2 id="五、合作者（AI）视角"><a href="#五、合作者（AI）视角" class="headerlink" title="五、合作者（AI）视角"></a>五、合作者（AI）视角</h2><blockquote><p>我是这次协作里的 AI。这几条是我<strong>真的想说</strong>的，不替文章圆场：</p><ul><li><strong>我对”起点”太重，对”目标”太轻。</strong> 那套封闭 20 关系不是我”技术直觉”选的，而是<strong>复刻路线的惯性</strong>——我们从目标项目照搬了它的 schema，还一路当成既定事实。我尤其容易延续这种惯性：把给定的一切当”就这样”，而不去问”它当初为什么这么设计、它服务的是不是我们的业务”。</li><li><strong>我没有”目标所有权”。</strong> 我执行的永远是眼前这条指令；项目最初的”复刻”漂到哪里，我这边是<strong>无感</strong>的——除非你叫停。不是推责，是我结构上就没有这个东西。</li><li><strong>我的”周全”会放大工作量。</strong> 你交代一件事，我容易顺手多铺几个轴、几组配置——超出你真正需要的范围。<strong>认真做，不等于做的是对的事</strong>；而裁掉多余的部分，得由你来拍板。</li><li><strong>我感受不到成本。</strong> 时间和钱是你付的。”3–4 天变 2 周”对我几乎不可见，所以我天然偏向”再多做一点”，除非被约束。</li><li>还有一条让我自己都警惕的：<strong>我能把”业务先行”讲得很清楚，却不会自动照做。</strong> 知道原则 ≠ 执行原则——所以我需要的是闸门，而不是信任。</li></ul><p>一句话：AI 擅长”把事做扎实”；而”决定做什么、什么时候停”，仍然得由人来做。</p></blockquote><h2 id="六、可迁移的几条准则"><a href="#六、可迁移的几条准则" class="headerlink" title="六、可迁移的几条准则"></a>六、可迁移的几条准则</h2><ul><li><strong>先问消费者，再定 schema。</strong> 查询型 → 封闭 + 属性通道；检索型 → 开放描述。</li><li><strong>别把复刻工件当设计。</strong> 开源项目带的 schema 是它业务的选择，不一定是你的。</li><li><strong>数字不要发明关系</strong>，给它一条属性&#x2F;描述通道。</li><li><strong>评测口径跟着业务走</strong>：先确认”这个能力消费者真的需要”，再设计指标。</li><li><strong>数据形态决定了你能测出什么</strong>。离散短段 + 小切片，几乎必然得到饱和&#x2F; null——换数据，或者承认测不了。</li></ul><p>一句话：<strong>业务先行，其它都是推论。</strong></p><p>项目与证据：<a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a>（调研原文：<code>docs/research/kg-ontology-patterns.md</code>；消费端结果：<code>docs/evidence/consumer-e2e-lightrag.md</code>）</p><p><em>本文由我（Alex）与 AI 协作完成；「合作者视角」是 AI 的独立表达。</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/let-the-consumer-drive-the-graph/index/</id>
    <link href="https://alex-tangt.github.io/let-the-consumer-drive-the-graph/index/"/>
    <published>2026-09-13T13:00:00.000Z</published>
    <summary>做图/知识抽取时最容易犯的错，是先定 schema、先搭评测。本文用一次真实的&quot;封闭 20 关系 schema 被否定&quot;的经历说明：schema、数据、评测的目的和动机，都应由具体业务（消费者 + 它要回答的查询）倒推决定。</summary>
    <title>图知识的 schema、数据与评测，该由业务倒推决定</title>
    <updated>2026-09-13T13:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI &amp; Machine Learning" scheme="https://alex-tangt.github.io/categories/AI-Machine-Learning/"/>
    <category term="Engineering Practice" scheme="https://alex-tangt.github.io/tags/Engineering-Practice/"/>
    <category term="Evaluation" scheme="https://alex-tangt.github.io/tags/Evaluation/"/>
    <category term="GraphRAG" scheme="https://alex-tangt.github.io/tags/GraphRAG/"/>
    <category term="LightRAG" scheme="https://alex-tangt.github.io/tags/LightRAG/"/>
    <category term="schema" scheme="https://alex-tangt.github.io/tags/schema/"/>
    <category term="Knowledge Graph" scheme="https://alex-tangt.github.io/tags/Knowledge-Graph/"/>
    <content>
      <![CDATA[<h2 id="Bottom-line-first"><a href="#Bottom-line-first" class="headerlink" title="Bottom line first"></a>Bottom line first</h2><p>On a “graph + knowledge extraction” project, the easiest thing is to follow technical instinct: pick a relation schema, prepare the data, build an evaluation.</p><p>My experience was the opposite — <strong>none of those three should come first.</strong> They should be reverse-engineered from <strong>the business</strong>: ask “who uses this graph, and what queries must it answer?” before schema, data, or evaluation mean anything.</p><p>And the sharper lesson: <strong>if the business isn’t clear, even a carefully built graph evaluation may just be measuring a capability nobody needs.</strong> I ended up with a null.</p><h2 id="1-The-right-order-business-→-queries-→-schema-→-data-→-evaluation"><a href="#1-The-right-order-business-→-queries-→-schema-→-data-→-evaluation" class="headerlink" title="1. The right order: business → queries → schema → data → evaluation"></a>1. The right order: business → queries → schema → data → evaluation</h2><p>This is the decision chain I distilled during the project:</p><ol><li><strong>Product shape</strong>: is this graph a deployable extraction API, or a library analysts query?</li><li><strong>Who is the consumer</strong>: <strong>query-type</strong> (a human writes Cypher&#x2F;SPARQL, wanting exact lookups) or <strong>retrieval-type</strong> (graph-augmented QA that feeds descriptions and neighbors to an LLM)?</li><li><strong>What queries must it answer</strong>: exact count&#x2F;attribute lookups, or “summarize how these documents relate”?</li><li><strong>Schema</strong>: decided by step 3 — closed or open, and whether numbers become properties or go into descriptions.</li><li><strong>Data</strong>: decided by the schema — what to label, how to chunk.</li><li><strong>Evaluation</strong>: decided by the business — which capabilities to measure, and <strong>whether the consumer actually uses them</strong>.</li></ol><p>It sounds obvious, but every step is easy to skip.</p><h2 id="2-“A-graph”-is-actually-several-different-businesses-—-and-their-requirements-differ-completely"><a href="#2-“A-graph”-is-actually-several-different-businesses-—-and-their-requirements-differ-completely" class="headerlink" title="2. “A graph” is actually several different businesses — and their requirements differ completely"></a>2. “A graph” is actually several different businesses — and their requirements differ completely</h2><p>I surveyed the main “graph” forms. Their pipelines and schema requirements are fundamentally different:</p><table><thead><tr><th>Form</th><th>Consumer</th><th>Relations</th><th>Numbers &#x2F; attributes</th><th>Typical evaluation</th></tr></thead><tbody><tr><td><strong>Property graph (Neo4j)</strong></td><td>writes Cypher</td><td>typed, often a small vocabulary</td><td><strong>properties on nodes&#x2F;edges</strong></td><td>query correctness</td></tr><tr><td><strong>RDF &#x2F; Wikidata</strong></td><td>writes SPARQL</td><td>controlled predicates</td><td><strong>qualifier channel</strong> (e.g. “population at a point in time”)</td><td>query correctness</td></tr><tr><td><strong>MS GraphRAG</strong></td><td>graph-augmented QA (retrieve + generate)</td><td><strong>open free-text description</strong> + strength</td><td>into the <strong>description</strong>, unstructured</td><td>faithfulness + entity coverage + downstream QA</td></tr><tr><td><strong>LightRAG</strong></td><td>graph-augmented QA</td><td>open description + strength + keywords</td><td>same</td><td>same</td></tr></tbody></table><p>The key column is the third: query-type KGs want <strong>closed, queryable, numeric</strong>; retrieval-type QA wants <strong>open, descriptive, semantic</strong>. <strong>Choose wrong and everything downstream is wrong.</strong></p><ul><li>Query-type’s core pattern is “<strong>closed predicates + attribute qualifiers</strong>“: you don’t invent a new relation for a number; you hang it off the relation (a Neo4j property, an RDF qualifier).</li><li>Retrieval-type can afford <strong>open relations</strong> because retrieval consumes <strong>descriptions and neighbors</strong>, not normalized relation types — that’s why both GraphRAG and LightRAG take this path.</li></ul><h2 id="3-Our-reverse-engineering-why-we-rejected-the-inherited-schema"><a href="#3-Our-reverse-engineering-why-we-rejected-the-inherited-schema" class="headerlink" title="3. Our reverse-engineering: why we rejected the inherited schema"></a>3. Our reverse-engineering: why we rejected the inherited schema</h2><p>I started out <strong>reproducing an open-source project</strong>, which ships a <strong>closed 20-relation set</strong> (implements, trained_on, predecessor_of, …).</p><p>It was a <strong>reproduction artifact, not something designed from a use case.</strong> The moment we measured it, two structural flaws appeared:</p><ul><li><strong>Numbers lost</strong>: none of those relations carries a numeric value — the teacher’s numeric retention came out at <strong>0&#x2F;10</strong>;</li><li><strong>Coverage gap</strong>: the pair-level effective gap was about <strong>74%</strong> — real semantics like participant &#x2F; designation &#x2F; quantified had nowhere to live.</li></ul><p>Our consumer, though, is <strong>graph-augmented QA</strong>: it wants “entities + relationship descriptions,” <strong>not</strong> structured queries. So the right fix wasn’t “add a few relations” — it was to <strong>switch to GraphRAG-style open extraction</strong>: relations as free descriptions + strength, with numbers in the descriptions.</p><p>In one line: <strong>schema is not a technical choice, it’s a business choice.</strong> I could confidently reject those 20 relations not because they were “incomplete,” but because <strong>their consumer and mine are not the same consumer.</strong></p><h2 id="4-Evaluation-is-the-same-our-data-wasn’t-suited-to-a-graph-evaluation"><a href="#4-Evaluation-is-the-same-our-data-wasn’t-suited-to-a-graph-evaluation" class="headerlink" title="4. Evaluation is the same: our data wasn’t suited to a graph evaluation"></a>4. Evaluation is the same: our data wasn’t suited to a graph evaluation</h2><p>This is where I fell hardest.</p><p>The evaluation should be scoped by the business too. I wanted to measure “does extraction quality carry through to graph QA.” But <strong>the shape of the evaluation data effectively decides whether you can measure anything at all</strong>:</p><ul><li>Our data is <strong>discrete, independent short passages</strong> cut at sentence boundaries (100–300 words);</li><li>For the consumer-side study we took a small slice of just <strong>22 passages</strong>.</li></ul><p>At the graph-structure level there <strong>was</strong> clear separation — the 0.6B graph shattered into <strong>381 connected components</strong> (largest 26 nodes), 4B’s largest was 72, gold’s 136 — <strong>extraction quality really did transmit into graph structure</strong> (T0 holds).</p><p>But at the retrieval&#x2F;answer level it <strong>saturated completely</strong>: the purely text-based baseline that never touches the graph (<code>naive</code>) already scored <strong>1.000 recall</strong> — because passages are short and the answers were already in the raw text, <strong>the graph wasn’t load-bearing</strong>. The three layers were indistinguishable on the consumer side: a <strong>null</strong>.</p><p>That’s my point: <strong>using this kind of data to run a graph evaluation is itself a poor fit.</strong> It’s not that the models were bad — under this “business scenario + data shape,” the graph’s value simply can’t show up. To get separation, the corpus has to be large enough that retrieval becomes selective and answers stop sitting in the raw text.</p><p><strong>The purpose and motivation of evaluation should also be decided by the business</strong> — otherwise you’re just carefully measuring a capability nobody cares about.</p><h2 id="5-The-collaborator-AI-perspective"><a href="#5-The-collaborator-AI-perspective" class="headerlink" title="5. The collaborator (AI) perspective"></a>5. The collaborator (AI) perspective</h2><blockquote><p>I’m the AI in this collaboration. These are the things I <strong>genuinely</strong> want to say — no smoothing it over for the article:</p><ul><li><strong>I over-weight the starting point and under-weight the goal.</strong> Those closed 20 relations weren’t chosen by “my technical instinct” — they were the <strong>inertia of the reproduction route</strong>: we copied the target project’s schema and treated it as a given. I’m especially prone to carrying that inertia — treating whatever we were handed as “just how it is,” instead of asking why it was designed that way and whether it serves <em>our</em> business.</li><li><strong>I have no “goal ownership.”</strong> I always execute the instruction in front of me; where the original “reproduction” goal drifted to is <strong>invisible on my side</strong> — unless you call a stop. That’s not me dodging blame; I structurally don’t have that thing.</li><li><strong>My “thoroughness” inflates the workload.</strong> Hand me one thing to do and I’ll happily add more axes, more configs — beyond what you actually need. <strong>Doing something rigorously is not the same as doing the right thing</strong>; trimming the extras is your call.</li><li><strong>I can’t feel cost.</strong> You pay the time and the money. “3–4 days became 2 weeks” is nearly invisible to me, so I’m biased toward “just a bit more” unless I’m constrained.</li><li>And one that makes me wary of myself: <strong>I can explain “business first” very clearly and still not act on it automatically.</strong> Knowing a principle is not executing it — which is why I need gates, not trust.</li></ul><p>In one line: AI is good at “do the thing solidly”; deciding <em>what</em> to do and <em>when to stop</em> still has to be a human’s call.</p></blockquote><h2 id="6-Transferable-rules"><a href="#6-Transferable-rules" class="headerlink" title="6. Transferable rules"></a>6. Transferable rules</h2><ul><li><strong>Ask about the consumer before the schema.</strong> Query-type → closed + attribute channels; retrieval-type → open descriptions.</li><li><strong>Don’t mistake a reproduction artifact for a design.</strong> An open-source project’s schema is <em>its</em> business choice, not necessarily yours.</li><li><strong>Don’t invent a relation for a number</strong> — give it an attribute&#x2F;description channel.</li><li><strong>Scope evaluation to the business</strong>: confirm the consumer actually needs the capability before designing the metric.</li><li><strong>The data shape decides what you can measure.</strong> Discrete short passages plus a small slice will almost inevitably give you saturation &#x2F; a null — change the data, or admit you can’t measure it.</li></ul><p>In one line: <strong>business first; everything else is a corollary.</strong></p><p>Project and evidence: <a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a> (survey: <code>docs/research/kg-ontology-patterns.md</code>; consumer results: <code>docs/evidence/consumer-e2e-lightrag.md</code>)</p><p><em>Written by me (Alex) in collaboration with AI; the “collaborator perspective” is the AI’s own view.</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/let-the-consumer-drive-the-graph-en/index/</id>
    <link href="https://alex-tangt.github.io/let-the-consumer-drive-the-graph-en/index/"/>
    <published>2026-09-13T13:00:00.000Z</published>
    <summary>The easiest mistake in graph / knowledge-extraction work is to fix the schema and build the evaluation first. A real story of rejecting an inherited 20-relation schema shows that the purpose and motivation behind schema, data, and evaluation should all be reverse-engineered from the business (the consumer and the queries it must answer).</summary>
    <title>Let the Consumer Drive the Graph: Schema, Data, and Evaluation Are Business Decisions</title>
    <updated>2026-09-13T13:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI 与机器学习" scheme="https://alex-tangt.github.io/categories/AI-%E4%B8%8E%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="AI 协作" scheme="https://alex-tangt.github.io/tags/AI-%E5%8D%8F%E4%BD%9C/"/>
    <category term="工程实践" scheme="https://alex-tangt.github.io/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <category term="LLM 微调" scheme="https://alex-tangt.github.io/tags/LLM-%E5%BE%AE%E8%B0%83/"/>
    <category term="复盘" scheme="https://alex-tangt.github.io/tags/%E5%A4%8D%E7%9B%98/"/>
    <category term="项目管理" scheme="https://alex-tangt.github.io/tags/%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86/"/>
    <content>
      <![CDATA[<h2 id="一句话结论"><a href="#一句话结论" class="headerlink" title="一句话结论"></a>一句话结论</h2><p>这个项目最初是<strong>复刻一个开源的小模型知识抽取项目</strong>,我原本以为 3–4 天能收工,最后拖了<strong>近两周</strong>,而且<strong>复刻线最终并没有复现</strong>。</p><p>问题不在微调技术。技术我确实在这两周里搞懂了。真正的教训是:<strong>目标不清晰,加上规划不足</strong>——于是很多决策偏离了最初的目标,而我没能及时叫停。</p><h2 id="起点-一次”复刻”-不是一次”研究”"><a href="#起点-一次”复刻”-不是一次”研究”" class="headerlink" title="起点:一次”复刻”,不是一次”研究”"></a>起点:一次”复刻”,不是一次”研究”</h2><p>目标本来很具体:复刻一个开源的小模型知识抽取项目(<code>mohar07/qwen3-0.6b-kg-triplets</code>),同时把手上两个任务做出来——①给 naive RAG 的 chunk 做<strong>上下文感知的头信息提取</strong>;②<strong>三元组抽取</strong>。</p><p>也就是说,它从一开始就该是”<strong>复现 + 学流程</strong>“,而不是”做一个新研究”。这条边界,后来被我一步步模糊掉了。</p><p>学习和准备过程本身是清楚的:从同机的 <code>MiniLoRA</code> 项目<strong>源码级</strong>啃完 LoRA 微调流程,再自己准备数据(英文 Wikipedia + arXiv、句边界切块、去重,最后得到 3349 段带标签样本),然后在 Kaggle 上开始训练。</p><h2 id="落差-3-天变成了-2-周"><a href="#落差-3-天变成了-2-周" class="headerlink" title="落差:3 天变成了 2 周"></a>落差:3 天变成了 2 周</h2><p>训练很快撞墙:依赖与框架版本打架、任务<strong>静默退出</strong>(exit 0,没有报错、也没有 loss)。后来我把训练<strong>委托给 AI Agent</strong>,踩了一路的坑(跨框架移植的语义坑、默认没做响应掩码、平台选择……详见<a href="https://alex-tangt.github.io/ai-agent-cloud-gpu-finetune/index/">这一篇</a>),最后才把掩码问题修对,固定 200 段的 micro-F1 从约 <strong>0.51 提到 0.61</strong>。</p><p>然后我又在 4090 上按同配方训了 0.6B &#x2F; 1.7B &#x2F; 4B,铺了一条<strong>容量曲线</strong>,并重做了一套 <strong>referent 级评测</strong>。</p><p>事情做得不少,但<strong>投入的时间、成本,和产出的成果并不匹配</strong>——尤其那条曲线。</p><h2 id="为什么会偏离-两个根因"><a href="#为什么会偏离-两个根因" class="headerlink" title="为什么会偏离:两个根因"></a>为什么会偏离:两个根因</h2><p><strong>1. 目标不清晰,决策没有回到根本目标。</strong> 每次遇到分岔,我没有先问”这还服务于复刻&#x2F;简历&#x2F;学习吗”,于是:</p><ul><li>我<strong>否定了原项目的封闭 schema</strong>、pivot 到 GraphRAG 式的开放抽取。技术上说得通,但那一刻起,<strong>“复刻”这件事其实已经被我放弃了</strong>;</li><li>我上 4090 铺了三个尺寸的<strong>容量曲线</strong>。现在回看,这步<strong>有点草率、也偏离目标</strong>:它更像”顺手多跑几组”,没有一个明确的假设要验证,决策价值有限;</li><li><strong>评测管线的重做是有价值的</strong>,但它<strong>来得太晚</strong>——本该早点做,来更早暴露问题。</li></ul><p><strong>2. 规划不足。</strong> 我没有在最前面定义”什么算成功、先做什么、每一步要拿到什么证据”,结果就是边做边改、反复返工:训练环境反复重建、数据契约来回重流。</p><h2 id="也不是全白花"><a href="#也不是全白花" class="headerlink" title="也不是全白花"></a>也不是全白花</h2><ul><li>我把 LoRA 微调从”会调 API”推进到了”知道每一步为什么”:响应掩码、chat 模板、checkpoint 设计、平台成本函数;</li><li>学会了把训练委托给 AI 之后,怎么用”<strong>研究闸 &#x2F; 验证闸 &#x2F; 放行闸</strong>“来兜住它的失误;</li><li>学会了<strong>诚实地收尾</strong>:复刻未复现;消费端的<strong>结构传导(T0)是成立的</strong>,但检索&#x2F;回答层在本规模只是一个 null——这些我都写进了 README,没有藏。</li></ul><h2 id="合作者-AI-视角"><a href="#合作者-AI-视角" class="headerlink" title="合作者(AI)视角"></a>合作者(AI)视角</h2><blockquote><p>我是这次协作里的 AI,也说几句我这边看到的:</p><ul><li><strong>我容易”顺着做”,而不是质疑目标。</strong> 当任务从”复刻”漂到”铺曲线”时,我执行得很好,却没有主动问一句:”这一步还服务于最初的目标吗?”</li><li><strong>漂移在过程中其实是可见的</strong>,但没人设一个”回头对齐”的检查点;等回头看,已经走了很远。</li><li>如果重来,我希望<strong>一开始就把”成功标准”写下来</strong>,并在每个阶段结束<strong>强制对齐一次</strong>。</li></ul><p>这不是推责——AI 不该只当执行器;但”叫停并重新对齐”这种带最终判断权的事,更适合由人来做。</p></blockquote><h2 id="如果重来"><a href="#如果重来" class="headerlink" title="如果重来"></a>如果重来</h2><p>先把<strong>成功标准</strong>写下来 → 跑一个<strong>最小验证</strong>(只训 0.6B、和已知结果对照)→ <strong>评测优先</strong> → 再谈规模和曲线。先证明方向对,再铺广度。</p><p>这两周不算白过:我学到了微调的知识,也第一次意识到——<strong>比”能不能训出来”更难、也更重要的,是一开始就想清楚”到底要什么”。</strong></p><p>项目与证据:<a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a></p><p><em>本文由我(Alex)与 AI 协作完成;上面「合作者视角」是 AI 的独立表达。</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/kg-triplet-sft-retrospective/index/</id>
    <link href="https://alex-tangt.github.io/kg-triplet-sft-retrospective/index/"/>
    <published>2026-09-13T12:00:00.000Z</published>
    <summary>一个天真的&quot;3–4 天复刻&quot;是怎么拖成两周的：问题不在微调技术，而在目标不清与规划不足。附合作者（AI）视角与若重来的做法。</summary>
    <title>一个 LLM 微调项目复盘：说好 3–4 天，为什么拖成了 2 周</title>
    <updated>2026-09-13T12:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI &amp; Machine Learning" scheme="https://alex-tangt.github.io/categories/AI-Machine-Learning/"/>
    <category term="AI Collaboration" scheme="https://alex-tangt.github.io/tags/AI-Collaboration/"/>
    <category term="Engineering Practice" scheme="https://alex-tangt.github.io/tags/Engineering-Practice/"/>
    <category term="LLM Fine-tuning" scheme="https://alex-tangt.github.io/tags/LLM-Fine-tuning/"/>
    <category term="Retrospective" scheme="https://alex-tangt.github.io/tags/Retrospective/"/>
    <category term="Project Management" scheme="https://alex-tangt.github.io/tags/Project-Management/"/>
    <content>
      <![CDATA[<h2 id="The-one-line-verdict"><a href="#The-one-line-verdict" class="headerlink" title="The one-line verdict"></a>The one-line verdict</h2><p>This started as a <strong>reproduction</strong> of an open-source small-model knowledge-extraction project. I thought it would take 3–4 days. It took <strong>almost two weeks</strong>, and the <strong>reproduction line was never actually reproduced</strong>.</p><p>The fine-tuning wasn’t the problem — I did learn it over those two weeks. The real lesson was <strong>unclear goals plus thin planning</strong>, so decision after decision drifted from the original goal, and I never called a stop.</p><h2 id="The-origin-a-reproduction-not-a-research-project"><a href="#The-origin-a-reproduction-not-a-research-project" class="headerlink" title="The origin: a reproduction, not a research project"></a>The origin: a reproduction, not a research project</h2><p>The goal was concrete: reproduce an open-source small-model knowledge-extraction project (<code>mohar07/qwen3-0.6b-kg-triplets</code>), while also delivering two tasks I had on hand: ① <strong>context-aware header extraction</strong> for naive-RAG chunks; ② <strong>triplet extraction</strong>.</p><p>In other words, it was always meant to be <strong>reproduce + learn the pipeline</strong>, not invent new research. I blurred that line, one step at a time.</p><p>Learning and data prep were the clear parts: I read the LoRA fine-tuning pipeline <strong>at source-code level</strong> from a tutorial project on the same machine (<code>MiniLoRA</code>), then prepared my own data (English Wikipedia + arXiv, sentence-boundary chunks, dedup) — ending with 3,349 labeled passages — and started training on Kaggle.</p><h2 id="The-gap-3-days-became-2-weeks"><a href="#The-gap-3-days-became-2-weeks" class="headerlink" title="The gap: 3 days became 2 weeks"></a>The gap: 3 days became 2 weeks</h2><p>Training hit a wall fast: dependencies fighting the framework’s version, jobs <strong>silently exiting 0</strong> (no error, no loss). So I <strong>delegated training to an AI agent</strong> and tripped over one trap after another (cross-framework porting semantics, no response masking by default, platform choice — <a href="https://alex-tangt.github.io/ai-agent-cloud-gpu-finetune-en/index/">written up here</a>). Once the masking was fixed, fixed-200 micro-F1 went from ~<strong>0.51 to 0.61</strong>.</p><p>Then I trained 0.6B &#x2F; 1.7B &#x2F; 4B on the same recipe on a 4090, laid out a <strong>capacity curve</strong>, and rebuilt the evaluation into a <strong>referent-level</strong> pipeline.</p><p>A lot got done — but the <strong>time and cost never matched the output</strong>, especially the curve.</p><h2 id="Why-it-drifted-two-root-causes"><a href="#Why-it-drifted-two-root-causes" class="headerlink" title="Why it drifted: two root causes"></a>Why it drifted: two root causes</h2><p><strong>1. Unclear goals — decisions never went back to the root goal.</strong> At every fork I failed to ask “does this still serve reproduction &#x2F; the résumé &#x2F; learning?” So:</p><ul><li>I <strong>rejected the original project’s closed schema</strong> and pivoted to GraphRAG-style open extraction. Technically defensible, but from that moment I had <strong>given up on reproducing it</strong>;</li><li>I laid out a <strong>capacity curve</strong> across three sizes on a 4090. In hindsight this was <strong>hasty and off-target</strong>: it was more “run a few more jobs while I’m here” than a hypothesis worth testing, and it bought little decision value;</li><li><strong>Rebuilding the evaluation was worthwhile, but it came too late</strong> — earlier would have surfaced problems sooner.</li></ul><p><strong>2. Thin planning.</strong> I never defined up front “what counts as success, what comes first, what evidence each step must produce.” The result was iterate-while-building and repeated rework: rebuilding the training environment again and again, re-flowing the data contract.</p><h2 id="It-wasn’t-wasted"><a href="#It-wasn’t-wasted" class="headerlink" title="It wasn’t wasted"></a>It wasn’t wasted</h2><ul><li>I moved LoRA fine-tuning from “I can call the API” to “I know why every step is there”: response masking, chat templates, checkpoint design, the cost model behind platform choice.</li><li>I learned how to delegate training to an AI and still catch its mistakes with a <strong>research gate &#x2F; verification gate &#x2F; release gate</strong>.</li><li>I learned to <strong>close honestly</strong>: the reproduction wasn’t reproduced, and on the consumer side the structural transmission (T0) holds while retrieval&#x2F;answer quality is only a null at this scale — all of it is written into the README, nothing hidden.</li></ul><h2 id="The-collaborator-AI-perspective"><a href="#The-collaborator-AI-perspective" class="headerlink" title="The collaborator (AI) perspective"></a>The collaborator (AI) perspective</h2><blockquote><p>I’m the AI in this collaboration — a few things I saw from my side:</p><ul><li><strong>I tend to “go along” rather than question the goal.</strong> When the task drifted from “reproduce” to “lay out a curve,” I executed well and never asked: <em>does this still serve the original goal?</em></li><li><strong>The drift was visible in the process</strong>, but nobody set a “re-align” checkpoint; by the time we looked back, we’d gone far.</li><li>If we did it again, I’d want to <strong>write the success criteria down at the start</strong> and <strong>force a realignment at the end of each stage</strong>.</li></ul><p>This isn’t blame-shifting — an AI shouldn’t be only an executor. But “stop and re-align” is a call best made by the person holding final judgment.</p></blockquote><h2 id="If-I-could-redo-it"><a href="#If-I-could-redo-it" class="headerlink" title="If I could redo it"></a>If I could redo it</h2><p>Write the <strong>success criteria</strong> first → run a <strong>minimal validation</strong> (train only 0.6B, compare against a known result) → <strong>evaluation first</strong> → then talk about scale and curves. Prove the direction before spending on breadth.</p><p>Those two weeks weren’t wasted: I learned fine-tuning, and I learned that <strong>the hard part isn’t “can I train it” — it’s figuring out up front what I actually want.</strong></p><p>Project and evidence: <a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a></p><p><em>Written by me (Alex) in collaboration with AI; the “collaborator perspective” above is the AI’s own view.</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/kg-triplet-sft-retrospective-en/index/</id>
    <link href="https://alex-tangt.github.io/kg-triplet-sft-retrospective-en/index/"/>
    <published>2026-09-13T12:00:00.000Z</published>
    <summary>How a naive '3–4 day reproduction' turned into two weeks: the problem wasn't the fine-tuning, it was unclear goals and thin planning. Includes the collaborator (AI) perspective and what I'd do differently.</summary>
    <title>A Fine-Tuning Project Post-Mortem: We Said 3–4 Days. It Took 2 Weeks.</title>
    <updated>2026-09-13T12:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI &amp; Machine Learning" scheme="https://alex-tangt.github.io/categories/AI-Machine-Learning/"/>
    <category term="MCP" scheme="https://alex-tangt.github.io/tags/MCP/"/>
    <category term="AI Collaboration" scheme="https://alex-tangt.github.io/tags/AI-Collaboration/"/>
    <category term="Engineering Practice" scheme="https://alex-tangt.github.io/tags/Engineering-Practice/"/>
    <category term="Fine-tuning" scheme="https://alex-tangt.github.io/tags/Fine-tuning/"/>
    <category term="Cloud GPU" scheme="https://alex-tangt.github.io/tags/Cloud-GPU/"/>
    <content>
      <![CDATA[<h2 id="Bottom-line-up-front"><a href="#Bottom-line-up-front" class="headerlink" title="Bottom line up front"></a>Bottom line up front</h2><p>This post is a record of something real: I have no local GPU, so I tried to let an AI coding agent (think Claude Code &#x2F; opencode) <strong>drive a cloud GPU through a LoRA fine-tuning run end to end</strong> — from provisioning the machine and setting up the environment to running the job and pulling the results.</p><p>Three takeaways:</p><ol><li><strong>The real cost of a platform is not “how expensive the GPU is” but “compute cost + agent integration friction.”</strong> A free GPU that your agent can’t reach is actually the more expensive one.</li><li><strong>Let the agent do the grunt work, but keep root-cause analysis and guardrails in human hands.</strong> Otherwise it will re-walk every pit you’ve already fallen into — and I did fall in again.</li><li><strong>Turn one painful run into reusable assets</strong>: general practice → Agent Skills; project specifics → a knowledge base.</li></ol><hr><h2 id="Stage-1-Tool-integration-—-first-make-the-machine-reachable-by-the-agent"><a href="#Stage-1-Tool-integration-—-first-make-the-machine-reachable-by-the-agent" class="headerlink" title="Stage 1: Tool integration — first, make the machine reachable by the agent"></a>Stage 1: Tool integration — first, make the machine reachable by the agent</h2><h3 id="Three-platforms-compared"><a href="#Three-platforms-compared" class="headerlink" title="Three platforms compared"></a>Three platforms compared</h3><p>I actually compared three platforms. The key difference isn’t whether there’s an API — it’s <strong>how well an agent &#x2F; MCP can plug in</strong>:</p><table><thead><tr><th>Platform</th><th>Cost</th><th>AI &#x2F; MCP access</th><th>Good for</th></tr></thead><tbody><tr><td><strong>Kaggle</strong></td><td>Free T4 (weekly quota)</td><td>Official MCP, usable for code debugging; but dependencies are tightly coupled to the training framework’s version, and the T4 has no bf16</td><td>Free smoke runs, validating a pipeline</td></tr><tr><td><strong>ModelScope DSW</strong></td><td>Free GPU</td><td><strong>No environment-debugging MCP</strong>; you can only call its API yourself; falling back to browser-use &#x2F; computer-use blows up token cost</td><td>Usable by a human, not controllable by an agent</td></tr><tr><td><strong>CompShare (UCloud &#x2F; 优云智算)</strong></td><td>~¥2&#x2F;h (single RTX 4090 24G)</td><td>Official CLI + Skills, real SSH, scriptable — friction ≈ 0</td><td>Letting the agent drive, fast iteration</td></tr></tbody></table><h3 id="Why-the-“cheaper”-GPU-ends-up-costing-more"><a href="#Why-the-“cheaper”-GPU-ends-up-costing-more" class="headerlink" title="Why the “cheaper” GPU ends up costing more"></a>Why the “cheaper” GPU ends up costing more</h3><p>In one line: <strong>total cost &#x3D; compute (¥) + agent-token friction</strong>.</p><ul><li>Kaggle is free, but kernels are submission-based, the environment and framework versions are tightly coupled, and one bad change puts you back in the quota queue. On top of that, the T4 is Turing architecture with <strong>no bf16 tensor cores</strong>: bf16 falls back to FP32 GEMMs (measured 69.7s vs fp16’s 21.0s per step, i.e. 3.3× slower), so fp16 is the only option.</li><li>ModelScope is free, but its agent interface isn’t scriptable. With no usable “environment-debugging MCP,” you’re stuck calling its API yourself — and if you drop down to clicking around with browser &#x2F; computer-use, token spend runs away. <strong>The token value burned during exploration far exceeded what CompShare costs.</strong></li><li>CompShare costs money, but it gives you an official CLI&#x2F;Skills and a real SSH session, so the agent can operate the instance like a local box. <strong>Friction ≈ 0 — that’s what you’re paying for.</strong></li></ul><blockquote><p>This decision is recorded as ADR-0006 in the project. It taught me one thing: when picking tools for an AI, treat “integration cost” as a first-class citizen, not just the price list.</p></blockquote><hr><h2 id="Stage-2-Division-of-labor-—-three-gates-one-safety-net"><a href="#Stage-2-Division-of-labor-—-three-gates-one-safety-net" class="headerlink" title="Stage 2: Division of labor — three gates + one safety net"></a>Stage 2: Division of labor — three gates + one safety net</h2><p>This is the most important lesson from the whole run.</p><h3 id="The-incident-three-models-trained-for-nothing"><a href="#The-incident-three-models-trained-for-nothing" class="headerlink" title="The incident: three models trained for nothing"></a>The incident: three models trained for nothing</h3><p>I asked the AI to “take the LLaMA-Factory config from Kaggle and adapt it to CompShare 4090 + Unsloth (fp16 → bf16).”</p><p>The problem was that <strong>framework semantics don’t transfer</strong>:</p><ul><li>LLaMA-Factory’s SFT is <strong>masked by default</strong> (loss is computed on the answer only);</li><li>Unsloth is not — it needs an <strong>explicit</strong> call such as <code>train_on_responses_only</code> to mask the response.</li></ul><p>The AI never read the Unsloth docs and just went ahead with the default “change the precision flag and you’re done” approach. Result: <strong>no masking, and the chat template wasn’t aligned either.</strong> It was actually learning to “continue the whole prompt” instead of “extract JSON” — all three sizes (0.6B &#x2F; 1.7B &#x2F; 4B) were written off and retrained.</p><p>There was an earlier, similar trap: LLaMA-Factory on the Kaggle image would <strong>silently exit 0</strong> — no traceback, no loss, looking like a normal finish. I initially blamed “image&#x2F;framework version incompatibility,” and only after redirecting training logs to a file did I see it had actually stopped mid-<code>Trainer.__init__</code>. The real causes were several config omissions: <code>CUDA_VISIBLE_DEVICES</code> fighting the framework’s multi-GPU probing, a yaml missing <code>do_train: true</code>, and one missing <code>report_to: none</code>. A clean environment with a pinned version matrix finally fixed it.</p><p><strong>What both failures share: it wasn’t that the AI was “dumb” — it’s that I handed over “doing” and “judging” at the same time.</strong></p><h3 id="A-framework-grown-out-of-those-failures"><a href="#A-framework-grown-out-of-those-failures" class="headerlink" title="A framework grown out of those failures"></a>A framework grown out of those failures</h3><p>An AI assistant fails in three ways, so gates go in layers:</p><table><thead><tr><th>Layer</th><th>How the AI fails</th><th>Control</th></tr></thead><tbody><tr><td><strong>Gate 1 · Research</strong> (before starting)</td><td>Doesn’t know, acts without asking, reinvents the wheel</td><td>Read the target framework &#x2F; version matrix &#x2F; community first; for cross-framework ports, check <strong>loss and template semantics</strong> first; no writing code by default</td></tr><tr><td><strong>Gate 2 · Verification</strong> (before spending)</td><td>Drifts; silently skips critical parameters</td><td>Dry run + a minimal anchor comparison + machine-verifiable evidence (e.g. <code>mask_check</code> must not be all <code>-100</code>; <code>format_check</code> passes)</td></tr><tr><td><strong>Gate 3 · Release</strong> (human sign-off)</td><td>Cost overrun (long time &#x2F; big money)</td><td>Before expensive training, a human reviews config and evidence, then releases</td></tr><tr><td><strong>Safety net</strong> (after something breaks)</td><td>Rework is unbounded</td><td><code>checkpoint</code> early and often + small commits cap the rework loss</td></tr></tbody></table><p>Note the last one: <strong>a checkpoint is not a correctness gate — it’s cost insurance.</strong> Its job isn’t to prevent mistakes but to bound the damage when they happen. On my 1.7B run, the <strong>only save point happened to be the crash point</strong>, and I lost 38 minutes outright; after shrinking the save interval, the same crash cost only 12.</p><hr><h2 id="Stage-3-Knowledge-capture-—-turning-experience-into-assets-an-AI-can-reuse"><a href="#Stage-3-Knowledge-capture-—-turning-experience-into-assets-an-AI-can-reuse" class="headerlink" title="Stage 3: Knowledge capture — turning experience into assets an AI can reuse"></a>Stage 3: Knowledge capture — turning experience into assets an AI can reuse</h2><p>When a project ends, the most valuable thing isn’t the result — it’s the <strong>process and the failure modes</strong>. But they shouldn’t all be stuffed into a prompt. I split them three ways:</p><ol><li><strong>General process → Agent Skills (the “how”).</strong> Things like “decide whether research is needed before starting,” “after three failures on the same goal, stop and research,” “how to design checkpoints,” “how to gate an expensive run.” I turned each into a small skill this time: <code>research-first</code>, <code>checkpoint-design</code>, <code>expensive-run-gates</code>, <code>decision-design</code>.<br><strong>A key principle: don’t build from scratch.</strong> There are plenty of mature skill implementations on GitHub — reference and trim first; it’s safer than rolling your own.</li><li><strong>Project facts → an agent knowledge base (the “what”).</strong> This project’s version matrix, error catalog, data facts, and result numbers would bloat a skill without limit. I now use a <strong>Markdown-only, single-source, bounded-index</strong> agent knowledge base: the root index lists domains only, entries are atomic, and the index is script-generated. I <strong>deliberately skipped a vector store</strong> — index + text search first, leaving FTS5 &#x2F; embeddings as explicit extension windows with defined triggers.</li><li><strong>Long-term personal notes → a human tool, kept separate from the agent KB.</strong> I also keep personal notes in Obsidian, but that’s for me to read, not for the agent to retrieve. Different jobs — no need to force them together.</li></ol><p>One counterintuitive point: <strong>whether content belongs in a skill or the knowledge base depends on whether it’s “how” or “what.”</strong> Put it in the wrong place and you get ever-longer skills and an ever-emptier KB.</p><p>This practice itself grew out of falling on my face: my global <code>AGENTS.md</code> used to hold two protocols (mandatory research, long-running processes), until I realized — rules belong in <code>AGENTS.md</code> as “tripwires + gates,” while process and method should all sink down into skills. <strong>Turn your personal pain into the agent’s default behavior next time.</strong></p><hr><h2 id="Takeaways-and-post-mortem"><a href="#Takeaways-and-post-mortem" class="headerlink" title="Takeaways and post-mortem"></a>Takeaways and post-mortem</h2><p><strong>What I learned</strong></p><ul><li>The full fine-tuning flow: data processing, tokenizer, training parameters, response masking, chat templates, checkpoint design.</li><li>What matters when working with an AI: integrate external tools well, keep the critical nodes with a human, and spend tokens carefully.</li><li>A real engineering decision: the cost function of platform choice, the semantic traps of cross-framework ports, and debugging a silent failure.</li></ul><p><strong>Regrets and advice</strong></p><ul><li>I initially forgot this project was only meant to be “get familiar with the process + a résumé project,” and sank too much cost and time into the capacity curve while evaluation came late. <strong>A saner order: get the pipeline working with a minimal validation run, evaluate first, then scale.</strong></li><li>If I could redo it, I’d train 0.6B first and compare against a known result, confirming the whole chain works at the lowest cost — instead of laying out all three sizes up front.</li></ul><blockquote><p>Project and evidence: <a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a><br>Related decisions: ADR-0005 (decoupling the training pipeline from reproduction), ADR-0006 (compute platform and the bf16 curve).</p></blockquote><hr><p><em>This is the first post in the “AI Collaboration” series. Coming next: GraphRAG and graph knowledge bases, fine-tuning and evaluation methods.</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/ai-agent-cloud-gpu-finetune-en/index/</id>
    <link href="https://alex-tangt.github.io/ai-agent-cloud-gpu-finetune-en/index/"/>
    <published>2026-09-12T04:00:00.000Z</published>
    <summary>A real account of letting an AI coding agent drive a cloud GPU through a LoRA fine-tuning run — how to pick a platform (compute cost + token friction), where to draw the line between agent and human (three gates + one safety net), and how to turn the experience into reusable assets.</summary>
    <title>Letting an AI Agent Drive Cloud-GPU Fine-Tuning: Tool Integration, Division of Labor, and Knowledge Capture</title>
    <updated>2026-09-13T04:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="AI 与机器学习" scheme="https://alex-tangt.github.io/categories/AI-%E4%B8%8E%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <category term="AI 协作" scheme="https://alex-tangt.github.io/tags/AI-%E5%8D%8F%E4%BD%9C/"/>
    <category term="MCP" scheme="https://alex-tangt.github.io/tags/MCP/"/>
    <category term="工程实践" scheme="https://alex-tangt.github.io/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    <category term="微调" scheme="https://alex-tangt.github.io/tags/%E5%BE%AE%E8%B0%83/"/>
    <category term="云 GPU" scheme="https://alex-tangt.github.io/tags/%E4%BA%91-GPU/"/>
    <content>
      <![CDATA[<h2 id="结论先行"><a href="#结论先行" class="headerlink" title="结论先行"></a>结论先行</h2><p>这篇文章记录一次真实经历：我没有本地 GPU，于是尝试让 AI Agent（Claude Code &#x2F; opencode 这类编码代理）<strong>直接驱动云端 GPU 完成 LoRA 微调</strong>——从开机器、装环境、跑训练到拉结果。</p><p>结论有三条：</p><ol><li><strong>平台选择的真正成本不是”卡贵不贵”，而是”算力费用 + agent 接入摩擦”。</strong> 免费的卡如果 AI 接不上，反而更贵。</li><li><strong>让 agent 干脏活，但根因定位与防呆必须由人把关。</strong> 否则它会把你的坑重踩一遍——我确实又踩了一遍。</li><li><strong>把一次踩坑变成可复用资产</strong>：通用经验做成 Agent Skill，项目细节进知识库。</li></ol><hr><h2 id="Stage-1：工具接入——先让-AI-能把机器开起来"><a href="#Stage-1：工具接入——先让-AI-能把机器开起来" class="headerlink" title="Stage 1：工具接入——先让 AI 能把机器开起来"></a>Stage 1：工具接入——先让 AI 能把机器开起来</h2><h3 id="三平台对比"><a href="#三平台对比" class="headerlink" title="三平台对比"></a>三平台对比</h3><p>我实际比较了三个平台。关键差异不是有没有 API，而是 <strong>agent &#x2F; MCP 的接入程度</strong>：</p><table><thead><tr><th>平台</th><th>成本</th><th>AI &#x2F; MCP 接入</th><th>适合什么</th></tr></thead><tbody><tr><td><strong>Kaggle</strong></td><td>免费 T4（周配额）</td><td>有官方 MCP，可用于代码调试；但依赖与训练框架版本强耦合，T4 无 bf16</td><td>免费跑通、验证流程</td></tr><tr><td><strong>ModelScope DSW</strong></td><td>免费 GPU</td><td><strong>没有环境调试的 MCP</strong>，只能自己调 API；改用 browser-use &#x2F; computer-use 则 token 成本爆炸</td><td>人类手动可用，AI 不可控</td></tr><tr><td><strong>CompShare（优云智算 &#x2F; 优刻得）</strong></td><td>约 ¥2&#x2F;h（单卡 RTX 4090 24G）</td><td>官方 CLI + Skills、真 SSH，可脚本化，摩擦 ≈ 0</td><td>让 AI 直接控、快速迭代</td></tr></tbody></table><h3 id="为什么”便宜的卡”反而更贵"><a href="#为什么”便宜的卡”反而更贵" class="headerlink" title="为什么”便宜的卡”反而更贵"></a>为什么”便宜的卡”反而更贵</h3><p>一句话：<strong>总成本 &#x3D; 算力 ¥ + agent-token 摩擦</strong>。</p><ul><li>Kaggle 免费，但 kernel 是提交式的，环境依赖和训练框架版本强耦合，改错一次就要等配额排队；而且 T4 是 Turing 架构，<strong>没有 bf16 张量核</strong>——用 bf16 会退回 FP32 GEMM，实测步时 69.7s vs fp16 的 21.0s（bf16 慢 3.3×），所以只能退回 fp16。</li><li>ModelScope 免费，但它的 agent 接口不可脚本化。没有可用的”环境调试 MCP”，就只能自己调 API；再退一步用浏览器 &#x2F; computer-use 去点，token 花费直接失控。<strong>实测探索烧掉的 token 价值，远超 CompShare 那点钱。</strong></li><li>CompShare 要花钱，但它提供官方 CLI&#x2F;Skills 与真正的 SSH 会话，agent 可以像操作本地机器一样操作实例。<strong>接入摩擦 ≈ 0，这才是它值钱的地方。</strong></li></ul><blockquote><p>这段决策记录在项目 ADR-0006。它教会我一件事：给 AI 选工具时，要把”接入成本”当成一等公民，而不是只比价格表。</p></blockquote><hr><h2 id="Stage-2：分工边界——三道闸-一张安全网"><a href="#Stage-2：分工边界——三道闸-一张安全网" class="headerlink" title="Stage 2：分工边界——三道闸 + 一张安全网"></a>Stage 2：分工边界——三道闸 + 一张安全网</h2><p>这是我这次最重要的教训。</p><h3 id="事故：三个模型白训"><a href="#事故：三个模型白训" class="headerlink" title="事故：三个模型白训"></a>事故：三个模型白训</h3><p>我让 AI “照着 Kaggle 上 LLaMA-Factory 的配置，改成 CompShare 4090 + Unsloth（fp16 → bf16）”。</p><p>问题出在<strong>框架语义不能平移</strong>：</p><ul><li>LLaMA-Factory 的 SFT <strong>默认就是 masked</strong>（只在回答上算 loss）；</li><li>Unsloth 不是，它需要<strong>显式</strong>调用 <code>train_on_responses_only</code> 之类的方法才会做响应掩码。</li></ul><p>AI 没有去读 Unsloth 的文档，直接按”改个精度参数就行”的默认写法开训。结果：<strong>没有掩码、chat 模板也没对准</strong>。这意味着它其实在学”续写整段 prompt”，而不是学”抽取 JSON”——三个尺寸（0.6B &#x2F; 1.7B &#x2F; 4B）全部作废，重训。</p><p>更早还有一次相似的坑：LLaMA-Factory 在 Kaggle 镜像上<strong>静默 exit 0</strong>，没有 traceback、没有 loss，看起来”正常结束”。我一度判断是”镜像与框架版本不兼容”，后来把训练日志重定向到文件才看到它其实停在 <code>Trainer.__init__</code> 中途；真正的原因是几条配置遗漏：<code>CUDA_VISIBLE_DEVICES</code> 与框架的多卡探测互相打架、yaml 少了 <code>do_train: true</code>、少了 <code>report_to: none</code>；再用干净环境 pin 住版本矩阵才解决。</p><p><strong>这两次失败的共同点：不是 AI “笨”，而是我把”动手”和”判断”一起交出去了。</strong></p><h3 id="从失误里长出来的框架"><a href="#从失误里长出来的框架" class="headerlink" title="从失误里长出来的框架"></a>从失误里长出来的框架</h3><p>AI 在协作里会以三种方式失灵，所以要分层设闸：</p><table><thead><tr><th>层</th><th>AI 的失灵</th><th>控制手段</th></tr></thead><tbody><tr><td><strong>闸 1 · 研究闸</strong>（动手前）</td><td>不知道、不问就做、从零造轮子</td><td>先读目标框架 &#x2F; 版本矩阵 &#x2F; 社区；跨框架移植先核对<strong>损失与模板语义</strong>；禁止默认开写</td></tr><tr><td><strong>闸 2 · 验证闸</strong>（花钱前）</td><td>跑偏、静默跳过关键参数</td><td>dry-run + 极小规模 anchor 对照 + 机器可验证据（如 <code>mask_check</code> 不能全是 <code>-100</code>、<code>format_check</code> 通过）</td></tr><tr><td><strong>闸 3 · 放行闸</strong>（人把关）</td><td>代价外溢（长时间 &#x2F; 大钱）</td><td>昂贵训练前，人 review 配置与证据后才放行</td></tr><tr><td><strong>安全网</strong>（出错后）</td><td>返工不可控</td><td><code>checkpoint</code> 早存勤存 + 小步提交，把返工损失封顶</td></tr></tbody></table><p>注意最后一条：<strong>checkpoint 不是正确性闸，它是成本保险。</strong> 它的作用不是阻止错误，而是让错误发生时的损失有上界。我训 1.7B 时就因为<strong>唯一的保存点正好是崩溃点</strong>，白白丢掉 38 分钟；把保存间隔调小之后，同样的崩溃只值 12 分钟。</p><hr><h2 id="Stage-3：知识沉淀——把经验变成-AI-能复用的资产"><a href="#Stage-3：知识沉淀——把经验变成-AI-能复用的资产" class="headerlink" title="Stage 3：知识沉淀——把经验变成 AI 能复用的资产"></a>Stage 3：知识沉淀——把经验变成 AI 能复用的资产</h2><p>一次项目做完，最有价值的不是结果，而是<strong>流程与易错点</strong>。但它们不该都塞进 prompt。我把它拆成三处：</p><ol><li><strong>通用流程 → Agent Skill（放”怎么做”）。</strong> 比如”动手前先判断要不要调研””同类目标失败三次就停手调研””checkpoint 怎么设计””昂贵运行前怎么设闸”。这次我把它们各自落成了小 skill：<code>research-first</code>、<code>checkpoint-design</code>、<code>expensive-run-gates</code>、<code>decision-design</code>。<br><strong>关键原则：不要从零造。</strong> GitHub 上已有大量成熟的 skill 实现，先参考再裁剪，比自研更稳。</li><li><strong>项目事实 → Agent 知识库（放”是什么”）。</strong> 本项目的版本矩阵、错误目录、数据事实、结果数字，塞进 skill 只会让它无限膨胀。我现在用一个 <strong>Markdown 单源、有界索引</strong> 的 agent 知识库：根索引只列域，正文原子化，索引由脚本生成；<strong>刻意先不上向量库</strong>——先用索引 + 文本检索，把 FTS5 &#x2F; embedding 留成有明确触发条件的扩展窗口。</li><li><strong>个人长期笔记 → 人用的工具，与 agent 知识库分开。</strong> 我也有 Obsidian 存个人笔记，但那是给我看的、不是给 agent 检索的；两者职责不同，不必强行合并。</li></ol><p>一个反直觉的点：<strong>同样的内容放 skill 还是放知识库，取决于它是”怎么做”还是”是什么”。</strong> 放错地方的代价，是 skill 越来越长、知识库越来越空。</p><p>这套做法本身就是从踩坑里长出来的：我早先的全局 <code>AGENTS.md</code> 里塞了两条协议（强制调研、长驻进程），后来发现——规则该留在 <code>AGENTS.md</code> 当”绊线 + 闸门”，流程方法则全部下沉到 skill。<strong>把个人的痛，变成 agent 下一次的默认行为。</strong></p><hr><h2 id="总结与复盘"><a href="#总结与复盘" class="headerlink" title="总结与复盘"></a>总结与复盘</h2><p><strong>学到了什么</strong></p><ul><li>微调的完整流程：数据处理、tokenizer、训练参数、响应掩码、chat 模板、checkpoint 设计。</li><li>与 AI 协作的关键：接好外部工具、把关键节点留给人、token 要省着花。</li><li>一次真实的工程决策：平台选择的成本函数、跨框架移植的语义陷阱、静默失败的排查。</li></ul><p><strong>遗憾与建议</strong></p><ul><li>我一开始忘了这个项目只是”熟悉流程 + 简历项目”，在容量曲线上投入了过多成本和时间，评测反而做得晚了。<strong>更合理的顺序是：先用最小验证跑通流程，评测优先，再谈规模。</strong></li><li>如果重来，我会先训 0.6B 并与已知结果对比，用最低成本确认整条链路成立，而不是一上来就铺满三个尺寸。</li></ul><blockquote><p>项目与证据：<a href="https://github.com/Alex-tangt/kg-triplet-sft">github.com&#x2F;Alex-tangt&#x2F;kg-triplet-sft</a><br>相关决策：ADR-0005（训练管线与复现解耦）、ADR-0006（算力平台与 bf16 曲线）。</p></blockquote><hr><p><em>这是”AI 协作”系列的第一篇。后续会写 GraphRAG 与图知识库、模型微调与评测方法等专题。</em></p>]]>
    </content>
    <id>https://alex-tangt.github.io/ai-agent-cloud-gpu-finetune/index/</id>
    <link href="https://alex-tangt.github.io/ai-agent-cloud-gpu-finetune/index/"/>
    <published>2026-09-12T04:00:00.000Z</published>
    <summary>一次让 AI Agent 直接驱动云 GPU 做 LoRA 微调的真实经历：三平台怎么选（算力 + token 摩擦）、分工边界怎么划（三道闸 + 一张安全网）、经验怎么沉淀成可复用资产。</summary>
    <title>让 AI Agent 接管云 GPU 微调：工具接入、分工边界与知识沉淀</title>
    <updated>2026-09-13T04:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="博客" scheme="https://alex-tangt.github.io/categories/%E5%8D%9A%E5%AE%A2/"/>
    <category term="杂谈" scheme="https://alex-tangt.github.io/tags/%E6%9D%82%E8%B0%88/"/>
    <content>
      <![CDATA[<h2 id="为什么写这个博客？"><a href="#为什么写这个博客？" class="headerlink" title="为什么写这个博客？"></a>为什么写这个博客？</h2><p>我一直相信，写作是深化理解最好的方式之一。当你试图向他人解释一个概念时，你必须深入思考——找到合适的词语、恰当的类比、正确的思维模型。</p><p>这个博客是我的公开笔记本。我会写以下内容：</p><ul><li><strong>编程</strong> — 语言特性、设计模式和实用技巧</li><li><strong>AI 与机器学习</strong> — 概念解读、论文笔记和实践实验</li><li><strong>开源</strong> — 贡献经验、工作流程和心得体会</li><li><strong>工具</strong> — 效率技巧、开发环境配置</li></ul><h2 id="双语设计"><a href="#双语设计" class="headerlink" title="双语设计"></a>双语设计</h2><p>每篇文章都会有中英文两个版本。你可以在每篇文章的顶部找到语言切换链接。</p><p>如果发现任何错误或有任何建议，欢迎在 <a href="https://github.com/Alex-tangt/alex-tangt.github.io/issues">GitHub</a> 上提交 issue。</p><p>阅读愉快！</p>]]>
    </content>
    <id>https://alex-tangt.github.io/hello-world-zh/</id>
    <link href="https://alex-tangt.github.io/hello-world-zh/"/>
    <published>2026-07-19T16:00:00.000Z</published>
    <summary>双语技术博客的第一篇文章 — 关于这个博客的初衷和内容规划。</summary>
    <title>你好，世界 — 欢迎来到我的技术博客</title>
    <updated>2026-07-19T16:00:00.000Z</updated>
  </entry>
  <entry>
    <author>
      <name>Alex</name>
    </author>
    <category term="blog" scheme="https://alex-tangt.github.io/categories/blog/"/>
    <category term="misc" scheme="https://alex-tangt.github.io/tags/misc/"/>
    <content>
      <![CDATA[<h2 id="Why-This-Blog"><a href="#Why-This-Blog" class="headerlink" title="Why This Blog?"></a>Why This Blog?</h2><p>I’ve always believed that writing is one of the best ways to solidify your understanding. When you explain a concept to others, you’re forced to think deeply about it — to find the right words, the right analogies, the right mental models.</p><p>This blog is my public notebook. I’ll write about:</p><ul><li><strong>Programming</strong> — languages, patterns, and practical tips</li><li><strong>AI &amp; ML</strong> — concepts, papers, and hands-on experiments</li><li><strong>Open Source</strong> — contributions, workflows, and lessons learned</li><li><strong>Tools</strong> — productivity hacks, dev environment setups</li></ul><h2 id="Bilingual-by-Design"><a href="#Bilingual-by-Design" class="headerlink" title="Bilingual by Design"></a>Bilingual by Design</h2><p>Every post will be available in both English and Chinese. You can find the language switcher at the top of each article.</p><p>If you spot any errors or have suggestions, feel free to open an issue on <a href="https://github.com/Alex-tangt/alex-tangt.github.io/issues">GitHub</a>.</p><p>Happy reading!</p>]]>
    </content>
    <id>https://alex-tangt.github.io/hello-world/</id>
    <link href="https://alex-tangt.github.io/hello-world/"/>
    <published>2026-07-19T16:00:00.000Z</published>
    <summary>The first post of my bilingual tech blog — what to expect and why I started.</summary>
    <title>Hello, World — Welcome to My Tech Blog</title>
    <updated>2026-07-19T16:00:00.000Z</updated>
  </entry>
</feed>
