<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>wwxdsg</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://bniosfhaiuk.xyz/</id>
  <link href="https://bniosfhaiuk.xyz/" rel="alternate"/>
  <link href="https://bniosfhaiuk.xyz/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, wwxdsg</rights>
  <subtitle>记录技术学习与生活思考的个人空间</subtitle>
  <title>wwxdsg的个人博客</title>
  <updated>2026-05-25T19:03:09.960Z</updated>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="Claude" scheme="https://bniosfhaiuk.xyz/tags/Claude/"/>
    <category term="Coworker" scheme="https://bniosfhaiuk.xyz/tags/Coworker/"/>
    <category term="产品反思" scheme="https://bniosfhaiuk.xyz/tags/%E4%BA%A7%E5%93%81%E5%8F%8D%E6%80%9D/"/>
    <category term="故障复盘" scheme="https://bniosfhaiuk.xyz/tags/%E6%95%85%E9%9A%9C%E5%A4%8D%E7%9B%98/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/coworker-autopsy.svg" alt="Claude Coworker Autopsy Cover"></p><h2 id="我从没想过，一个-Agent-可以同时做到三件事：什么都没产出，占满我的硬盘，还烧完了我的配额"><a href="#我从没想过，一个-Agent-可以同时做到三件事：什么都没产出，占满我的硬盘，还烧完了我的配额" class="headerlink" title="我从没想过，一个 Agent 可以同时做到三件事：什么都没产出，占满我的硬盘，还烧完了我的配额"></a>我从没想过，一个 Agent 可以同时做到三件事：什么都没产出，占满我的硬盘，还烧完了我的配额</h2><p>五天前我看到 Claude Desktop 上多了一个叫 Coworker 的功能，很兴奋。它在实验室（Labs）标签下，意味着是”研究预览”。作为一个做 Agent 开发的人，我对各种 agent 形态天然好奇，第一时间就试了。</p><p>两天后，C 盘红了。</p><p>我以为是我装了什么东西——最近没装过。然后我发现多了整整 13GB 的 Claude 应用数据。同一天，我发现我的 Claude Pro 日配额被全部烧光，什么都没产出来。</p><p>我当时真的是一脸问号。我知道 agent 会吃 token，但我没想到一个<strong>课程报告</strong>任务能在背后吃掉这么多资源。</p><h2 id="我看到了什么"><a href="#我看到了什么" class="headerlink" title="我看到了什么"></a>我看到了什么</h2><p>先说我当时在做什么。我让 Coworker 帮我写一份大数据系统课程设计的报告，上传了项目结构文件和 README，选了一个工作文件夹。很简单的任务。</p><p>过了一段时间，我发现 C 盘剩余空间从正常水平掉到了不到 2GB。我质问 Coworker「你做项目怎么吃了 C 盘那么大的空间」，然后它继续自顾自折腾，最后报了一个错：</p><blockquote><p>You’ve hit your session limit · resets 6:50am</p></blockquote><p>我打开 <code>C:\Users\...\AppData\Local\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\</code> 一看：</p><ul><li><code>vm_bundles/claudevm.bundle</code> — <strong>12GB</strong></li><li><code>Cache/</code> + <code>Code Cache/</code> + <code>GPUCache/</code> — 几百 MB</li><li><code>claude-code-vm/</code> + <code>claude-code/</code> — 几百 MB</li><li><code>local-agent-mode-sessions/</code> — 会话残留</li></ul><p>加起来 13GB。一个课程报告，13GB。</p><p>然后我去翻会话日志。它用了 <code>claude-opus-4-6</code>——最贵的模型。一共 156 轮 assistant 回复。</p><p>156 轮。为一个课程报告。</p><h2 id="到底发生了什么"><a href="#到底发生了什么" class="headerlink" title="到底发生了什么"></a>到底发生了什么</h2><p>我从会话目录里挖出了 <code>audit.jsonl</code> 和 <code>local_*.json</code>，完整还原了时间线。</p><p>它一启动就做了几件事：创建了 5 个 Task，读了项目文件，然后——开始不停地重试一个叫 <code>bash</code> 的工具调用。</p><p>“Workspace still starting. The isolated Linux environment is booting in the background (usually 10-30 seconds). Try again shortly.”</p><p>这条错误在日志里出现了 <strong>82 次</strong>。</p><p>82 次。每次重试，Coworker 都把整个对话上下文重新发给 Claude Opus 4.6。而那个 system prompt 有多长？我把原始内容提出来了，包含了 Anthropic 全部产品线介绍、行为规范、tone 指引、artifacts 渲染规则、文件处理规则、65 个 skill 定义——以及一种令人窒息的”系统提示词膨胀”。</p><p>每一次重试的成本 &#x3D; 巨型 system prompt + 所有历史消息 + 工具调用结果 + 模型推理输出。乘以 82。</p><p>更离谱的是，<strong>它在 17:07 就打满了第一轮 5 小时配额</strong>——从会话开始算，只过了 15 分钟。等到 17:51 重置后，它继续工作，然后在 18:25 第二次烧光。</p><p>一个会话，两轮配额。为零产出付费。</p><h2 id="作为一个做-Agent-的人，我看到的是什么"><a href="#作为一个做-Agent-的人，我看到的是什么" class="headerlink" title="作为一个做 Agent 的人，我看到的是什么"></a>作为一个做 Agent 的人，我看到的是什么</h2><p>这不是模型的问题。Opus 4.6 只是做了一个 agent loop 让它做的事——收到错误，重试。重试。重试。被拒了还要重试。</p><p>问题是<strong>整个 agent 系统设计上的缺陷</strong>，而且这些缺陷对我来说太眼熟了：</p><h3 id="1-没有熔断"><a href="#1-没有熔断" class="headerlink" title="1. 没有熔断"></a>1. 没有熔断</h3><p>这是最致命的问题。同一个工具调用，同一个错误，重试 82 次。任何一个生产级系统都会在连续 N 次失败后停止并向上汇报，但 Coworker 没有。它不是缺少推理能力，而是<strong>缺少一个对失败模式的基础检测</strong>。</p><p>作为对比，如果我在自己写的 agent loop 里发现同类型错误连续出现 5 次，我会立刻打断循环、记录异常、提示用户。这不是因为我的模型更聪明，而是因为我给 loop 加了护栏。</p><h3 id="2-用最贵的模型做最机械的决策"><a href="#2-用最贵的模型做最机械的决策" class="headerlink" title="2. 用最贵的模型做最机械的决策"></a>2. 用最贵的模型做最机械的决策</h3><p>判断「上次 bash 调用失败了，我应该再试一次」这件事——不需要 Opus。一个 if-else 就够了。如果用 Haiku、甚至一个简单的状态机来处理重试逻辑，token 消耗能差出几个数量级。但 Coworker 把<strong>所有决策都交给 Opus</strong>，包括那些根本不需要「推理」的步骤。</p><h3 id="3-上下文膨胀失控"><a href="#3-上下文膨胀失控" class="headerlink" title="3. 上下文膨胀失控"></a>3. 上下文膨胀失控</h3><p>那个 65 个 skill 全部预加载到 system prompt 里。用户要写一个课程报告，不需要知道怎么发营销邮件、做 SEO 审计、设计品牌评审。把和任务无关的 skill 定义按需加载而不是预加载，是最基本的优化。但设计上选择了「全部塞进去」。</p><p>这不是 prompt engineering 的问题，是架构选择的问题。</p><h3 id="4-Loop-模式缺乏目标锚定"><a href="#4-Loop-模式缺乏目标锚定" class="headerlink" title="4. Loop 模式缺乏目标锚定"></a>4. Loop 模式缺乏目标锚定</h3><p>Loop 模式的设计意图是让 agent 能「自主推进工作」——做完一步，检查结果，决定下一步。但实际执行中，它没有一个有效机制来判断「我是不是还在朝目标前进」。</p><p>它创建了 5 个 Task，然后因为沙箱起不来，全部卡住。但它没有把「沙箱不可用」这个信息用来<strong>重新规划方案</strong>（比如不用沙箱，用本地文件工具），而是原地重试。目标锚定不够强。</p><h3 id="5-资源泄漏"><a href="#5-资源泄漏" class="headerlink" title="5. 资源泄漏"></a>5. 资源泄漏</h3><p>那个 12GB 的 <code>claudevm.bundle</code> 是一个 VM 镜像。它被下载下来之后，即便 VM 启动失败了，也没有被清理。用户永远不知道这个文件存在，系统也不会自动回收。</p><p>这是一个很经典的 agent 后处理缺失：创建了资源，却没有生命周期管理，没有清理策略，也没有失败回滚。</p><h2 id="这不是”技术预览”的借口"><a href="#这不是”技术预览”的借口" class="headerlink" title="这不是”技术预览”的借口"></a>这不是”技术预览”的借口</h2><p>我理解 Coworker 是 Labs 下的「研究预览」。我也做实验性功能，我知道 preview 意味着不完美。</p><p>但这里有些问题不是功能不完善导致的，而是<strong>架构上缺少对 agent 常见失败模式的防御</strong>：</p><ul><li>把重试逻辑交给模型而不是框架——这不是 feature，是 bug</li><li>不检查操作是否有效就反复执行——这也不是 preview 能解释的</li><li>无上限消耗用户配额——这更不能接受</li></ul><p>我的观点是：<strong>agent 系统的基础护栏不应该比功能本身更晚交付。</strong> 没护栏就上线，等于把一艘没有水密舱的船开进了 open sea。你觉得水会从哪里进来？当然是所有地方。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这次经历的荒诞之处在于，它用一种极端高效的方式同时失败：</p><ul><li>磁盘占用？13GB 全在</li><li>Token 配额？两轮烧光</li><li>实际产出？几个用不上的 SVG 和一段没跑通的 Python</li></ul><p>1.5 小时内，我什么都没得到，但失去了一切：空间和额度。</p><p>作为一个做 agent 的人，我其实不生气于这个功能做得不好——我生气的是，我看到的问题全是<strong>我每天在防范的东西</strong>。熔断、模型分层、上下文精简、资源回收、目标检查——这些不是 rocket science，这些就是 agent engineering 的基本功。</p><p>如果一个 agent 产品在最贵的模型上、在用户的生产环境里、没有任何护栏地运行，那这不仅仅是一个 bad experience。这是一个 design failure。</p><hr><p><em>后记：我把 13GB 数据清掉了。Coworker 功能我短期内不会再碰。但我感谢这次经历——它让我更确信，agent 工程真正的难点不是让模型变聪明，而是在模型变蠢的时候，你还有一道防线。</em></p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/notes/coworker-autopsy/</id>
    <link href="https://bniosfhaiuk.xyz/notes/coworker-autopsy/"/>
    <published>2026-05-25T10:30:00.000Z</published>
    <summary>Claude Desktop 的 Coworker 功能在 1.5 小时内烧光两轮日配额、吃掉 13GB 磁盘，最后产出一堆无关文件。作为 Agent 开发者，我对这次故障做了一次完整复盘。</summary>
    <title>一次 Claude Coworker 灾难体验：13GB 磁盘、两轮配额、零产出</title>
    <updated>2026-05-25T19:03:09.960Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="随笔" scheme="https://bniosfhaiuk.xyz/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="历史" scheme="https://bniosfhaiuk.xyz/tags/%E5%8E%86%E5%8F%B2/"/>
    <category term="阅读" scheme="https://bniosfhaiuk.xyz/tags/%E9%98%85%E8%AF%BB/"/>
    <category term="媒介" scheme="https://bniosfhaiuk.xyz/tags/%E5%AA%92%E4%BB%8B/"/>
    <category term="战争记忆" scheme="https://bniosfhaiuk.xyz/tags/%E6%88%98%E4%BA%89%E8%AE%B0%E5%BF%86/"/>
    <category term="短视频" scheme="https://bniosfhaiuk.xyz/tags/%E7%9F%AD%E8%A7%86%E9%A2%91/"/>
    <category term="茨威格" scheme="https://bniosfhaiuk.xyz/tags/%E8%8C%A8%E5%A8%81%E6%A0%BC/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/why-i-still-read-history.svg" alt="Why I Still Read History Cover"></p><p>有时候我会觉得，普通人读历史是一件很容易被嘲笑的事。尤其是男性，只要一谈历史，空气里就会立刻冒出一种若有若无的讥讽，好像潜台词永远是：你现实里也没做成什么，怎么还好意思去谈帝国、战争和人物沉浮。</p><p>我一直不喜欢这种说法，却也很难立刻反驳。直到最近，我才慢慢把自己的想法理清楚了。</p><p>我对历史的兴趣，根本不是为了借古人给自己脸上贴金，更不是为了在饭桌上表演一种廉价的宏大感。我真正着迷的，是另一种东西：历史几乎是我能接触到的，成本最低的人性模拟器。</p><p>现实里的很多局面，我们根本没有试错资格。我们很难真的去体验兵临城下时的恐惧，很难体验权力在手时人的傲慢，很难体验忠诚、背叛、犹豫和误判在极端处境里会怎样同时发生。可历史把这些极限时刻保留了下来。只要你愿意认真带入，愿意暂时放下“后见之明”的优越感，它就会把一个人推到你面前，让你去看他在命运的重压下，到底暴露出什么。</p><p>这也是我越来越觉得历史迷人的地方。它不是一堆供人背诵的年份和结论，而是一座巨大的心理沙盘。你可以隔着几百年，反复揣摩那些选择是怎么发生的：伟大是怎么来的，卑鄙是怎么发生的，平庸又是怎样在关键的一分钟里改写结果的。现实生活里亲自去做这些实验，代价太大了；读历史，至少还能让人以比较安全的方式，见识人性的边界。</p><p>更有意思的是，读到后来，你会发现自己不只是在看事件，也是在看叙述事件的人。史料从来不只是记录，它也是情绪，也是立场，也是取舍。一个词为什么这样用，一段经历为什么被轻轻带过，某个细节为什么写得格外重，很多时候都比表面事实更耐人寻味。那种顺着文字往回摸，慢慢察觉记载者个人倾向和隐秘判断的感觉，很像隔着时空破案。这种乐趣，别的东西真的很难替代。</p><p>我想，别人真正讨厌的，未必是“有人读历史”这件事。他们讨厌的，多半是有人借历史代入权力，借宏大叙事装点自己，把现实中的无力感翻译成一种高高在上的指点姿态。坦白说，我也不喜欢那种样子。但这不该反过来变成另一种粗暴判断，仿佛一个普通人只配盯着眼前的工资、实习和日常生活，根本不该抬头去想更远的事。</p><p>对我来说，恰恰相反。越是普通人，越需要历史。</p><p>因为这几年真正让我不安的，不是某一个具体事件，而是人们对战争、苦难和民族主义的认知，正在被媒介一点点改写。我们这一代对战争还保留着的一点敬畏，很多时候其实是被长篇叙事续上的。小说、电影、纪录片之所以重要，不只是因为它们提供知识，更因为它们愿意花时间，把战争重新还原成具体的人、具体的疼痛、具体的失去。它们逼着你停下来，逼着你看见那些原本很容易被口号和地图吞掉的命运。</p><p>短视频的逻辑几乎是反过来的。它当然也能传递信息，但它天生更擅长切片、加速和放大情绪。战争一旦被剪成十几秒的高燃片段，配上节奏鲜明的音乐、立场明确的字幕和一眼就能看懂的敌我叙事，它就很容易从一场真实灾难，变成一种可以被消费的视觉刺激。最可怕的地方不在于有人无知，而在于整个社会慢慢失去对苦难的重量感。我们越来越容易为立场激动，却越来越难为生命本身感到沉重。</p><p>这也是为什么我会对当下的信息环境格外警惕。算法最奖励的，从来不是耐心、灰度和克制，而是愤怒、确定和对立。它会让人产生一种危险的幻觉：好像复杂问题都能被一句口号概括，好像历史不再需要理解，只需要站队。久而久之，战争会被审美化，民族主义会被日常化，而那些原本应该带来警惕的历史回声，只会在一轮轮转发和配乐里被磨平。</p><p>我当然知道，一个普通人没办法阻止这些事情发生。我也没有那么大的能量去扭转什么时代潮水。但历史对我仍然有用，因为它至少能帮我守住一点精神上的自主权。</p><p>它不会让我变得更有权力，却会让我对权力的语言更敏感。它不会让我站到更高的位置，却会让我更快识别那些熟悉的煽动、包装和自我神化。它不会直接给我答案，却会让我在面对宏大叙事时，先去怀疑其中被省略的人、被遮蔽的代价，以及那个讲述者到底希望我相信什么。</p><p>所以我越来越觉得，普通人读历史最大的意义，不是让自己变得更会说，而是让自己不那么容易被说服。不是让自己拥有一种“我懂大局”的幻觉，而是让自己在这个越来越轻、越来越快、越来越爱下结论的时代里，保留一点对复杂性的尊重，保留一点对人性的耐心，也保留一点不愿意被集体情绪随便拖走的倔强。</p><p>最早让我真正体会到这一点的，还是茨威格的《人类群星闪耀时》。我喜欢它，不是因为它把历史写得多么宏大，而是因为它总能把命运压缩进某个决定性的瞬间，让人物的伟大、怯懦、盲目和犹豫一起暴露出来。那种写法很容易把人拉进去，让你不只是“知道发生了什么”，而是真的开始追问：如果我是他，我会怎样？如果换一个人站在那里，历史会不会拐向另一条路？</p><p>从那一刻起我慢慢明白，历史并不只是属于专家、胜利者或者讲台上的人。它也属于愿意认真观察人的普通读者。属于那些知道自己改变不了多少，却仍然不想把大脑完全交给情绪、算法和潮流的人。</p><p>我现在仍然想继续读历史。不是因为我想借它抬高自己，也不是因为我想靠它证明什么。只是因为在这样一个信息越来越碎、表达越来越快、判断越来越轻浮的时代里，我还是想给自己的精神留一点纵深。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/fragments/why-i-still-read-history/</id>
    <link href="https://bniosfhaiuk.xyz/fragments/why-i-still-read-history/"/>
    <published>2026-04-26T06:10:00.000Z</published>
    <summary>从战争记忆的变形、短视频时代的情绪动员，到茨威格带来的历史阅读快感，我慢慢明白，读历史不是为了扮演大人物，而是为了给自己的精神留一点纵深。</summary>
    <title>读历史不是为了指点江山，而是为了不被轻易塑形</title>
    <updated>2026-04-26T08:37:36.182Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="方法论" scheme="https://bniosfhaiuk.xyz/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="LLM工程" scheme="https://bniosfhaiuk.xyz/tags/LLM%E5%B7%A5%E7%A8%8B/"/>
    <category term="Prompt Engineering" scheme="https://bniosfhaiuk.xyz/tags/Prompt-Engineering/"/>
    <category term="规则设计" scheme="https://bniosfhaiuk.xyz/tags/%E8%A7%84%E5%88%99%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/agent-tuning-playbook.svg" alt="Agent Tuning Playbook Cover"></p><p>最近在做一类很典型、也很折磨人的 Agent 质量问题。</p><p>表面上看，现象是「grain 不准」「business meaning 不稳」「PK 假阳性偏多」；但如果往下拆，就会发现这些问题其实根本不在同一个层面上。<br>有的是输入信号不够，有的是任务拆法不对，有的是规则层该做的事被硬塞给了 LLM，还有的是其实模型已经答得差不多了，只差一个后处理校验帮它把明显矛盾拦下来。</p><p>所以我后来给自己整理了一套更顺手的调优框架：</p><p><strong>先别急着换模型，先判断问题到底发生在哪一层。</strong></p><h2 id="一张表先把武器库摆清楚"><a href="#一张表先把武器库摆清楚" class="headerlink" title="一张表先把武器库摆清楚"></a>一张表先把武器库摆清楚</h2><table><thead><tr><th>作用层</th><th>手段</th><th>性价比</th><th>数据集无关性</th></tr></thead><tbody><tr><td>输入信号</td><td>给 LLM 看更多、更好的数据</td><td>⭐⭐⭐</td><td>✅</td></tr><tr><td>任务拆分</td><td>把“一次推全部”拆成多步</td><td>⭐⭐</td><td>✅</td></tr><tr><td>模型替换</td><td>换更强的 LLM，如 <code>deepseek-reasoner</code> &#x2F; <code>GPT-4o</code></td><td>⭐⭐</td><td>✅</td></tr><tr><td>后处理校验</td><td>LLM 输出后用规则反查矛盾</td><td>⭐⭐⭐</td><td>✅</td></tr><tr><td>规则前置</td><td>不擅长的交还给规则，LLM 只做语言层</td><td>⭐⭐⭐</td><td>⚠️</td></tr><tr><td>多次采样</td><td>同 prompt 跑 N 次取多数</td><td>⭐</td><td>✅</td></tr><tr><td>多 LLM 协作</td><td><code>Generator + Reviewer</code> 分工</td><td>⭐⭐</td><td>✅</td></tr></tbody></table><p>还有两件我现在基本不会优先考虑的事：</p><ol><li><strong>Fine-tune 模型</strong>：周期太长，13 天这种节奏根本不现实。</li><li><strong>产品端人工干预</strong>：这应该留到决赛产品化阶段，而不是当前自动构建阶段。</li></ol><p>这个表对我最大的帮助不是“列手段”，而是提醒我：<br><strong>不同问题要用不同武器，不要一上来就把所有锅都甩给 prompt。</strong></p><h2 id="先判断问题落在哪一层"><a href="#先判断问题落在哪一层" class="headerlink" title="先判断问题落在哪一层"></a>先判断问题落在哪一层</h2><p>我现在看 Agent 质量问题，通常先问自己三件事：</p><ol><li>LLM 是不是根本没看到足够的结构信号？</li><li>这个判断到底是不是本来就更适合规则做？</li><li>模型已经给出一个差不多的答案了，只是缺了一层兜底校验？</li></ol><p>如果这三件事没想清楚，就很容易进入一种假忙碌状态：</p><ul><li>prompt 越写越长</li><li>示例越塞越多</li><li>token 花得越来越快</li><li>结果还是没有稳定提升</li></ul><p>很多时候不是模型不行，而是我们把不该让它承担的任务，硬塞给它了。</p><h2 id="问题-A：grain-准确率低，优先补输入和规则"><a href="#问题-A：grain-准确率低，优先补输入和规则" class="headerlink" title="问题 A：grain 准确率低，优先补输入和规则"></a>问题 A：grain 准确率低，优先补输入和规则</h2><p>当前这个问题里，我最先盯的是 <code>grain</code>。</p><p>因为一旦 <code>grain</code> 判断错了，后面很多描述都会跟着偏：你以为它在解释一张事件表，实际上它把它当成了维表；你以为它在总结“交易粒度”，它却写成了“实体信息表”。<br>所以 <code>grain</code> 是上游问题，也是最该先处理的问题。</p><h3 id="Lever-1：先增强输入信号"><a href="#Lever-1：先增强输入信号" class="headerlink" title="Lever 1：先增强输入信号"></a>Lever 1：先增强输入信号</h3><p>目前只给 5 条 sample 和一些 <code>null / unique</code> 比例，其实经常不够。</p><p>更有价值的信号包括：</p><ol><li><strong>行数级别提示</strong>：<code>row_count: 50000</code> 比单独几条样本更能说明这是一张大事实表还是大维表。</li><li><strong>跨表上下文</strong>：把 <code>*_id</code> 的引用关系摘要塞进 prompt。比如 LLM 一旦看到 <code>fact_session.user_id -&gt; dim_user.id</code>，就更容易理解 <code>fact_session</code> 是 transaction 或 event 粒度。</li><li><strong>统计学线索</strong>：如果表里有 <code>*_at</code> 字段，可以补时间分布、增长趋势、大小分位数，这些都会帮助模型判断这是一张行为流还是实体快照。</li></ol><p>我现在越来越相信一件事：</p><p><strong>很多“推理错误”，本质上只是证据不足。</strong></p><h3 id="Lever-5：让规则做-70-，LLM-只做边界情况"><a href="#Lever-5：让规则做-70-，LLM-只做边界情况" class="headerlink" title="Lever 5：让规则做 70%，LLM 只做边界情况"></a>Lever 5：让规则做 70%，LLM 只做边界情况</h3><p><code>grain</code> 这种结构化判断，其实规则层就能覆盖掉很大一部分。</p><p>例如：</p><ul><li><code>fact_</code> 前缀 + 多个 FK + <code>row_count &gt; 100k</code>，几乎一定是 <code>transaction / event</code></li><li><code>dim_</code> 前缀 + 单 PK + <code>row_count &lt; 10k</code>，大概率是 <code>dimension</code></li></ul><p>这里最重要的一点不是“把规则写死”，而是：</p><p><strong>把规则写成高优先级信号，而不是硬编码真理。</strong></p><p>因为不同公司命名并不统一。电商可能用 <code>ods_ / dwd_ / dws_</code>，金融也可能是完全不同的一套体系。<br>所以规则应该提供强烈提示，但最终仍允许 LLM 覆盖，这样才能避免把系统做成只会识别某一类数据仓库命名习惯的工具。</p><h2 id="问题-B：business-meaning-不稳，通常先别单独修"><a href="#问题-B：business-meaning-不稳，通常先别单独修" class="headerlink" title="问题 B：business_meaning 不稳，通常先别单独修"></a>问题 B：business_meaning 不稳，通常先别单独修</h2><p><code>business_meaning</code> 这个字段表面上看是另一个问题，但在我这次实践里，它经常只是 <code>grain</code> 误判的连带伤。</p><p>因为语言层面的总结，本来就是 LLM 更擅长的事。<br>只要它先把这张表的本质看对了，<code>business_meaning</code> 往往会自动跟上。</p><p>所以我的顺序不是“同时修两个问题”，而是：</p><ol><li>先把 <code>grain</code> 稳住</li><li>再看 <code>business_meaning</code> 还剩多少问题</li></ol><p>如果 <code>grain</code> 已经对了，<code>business_meaning</code> 还不够好，这时候再上 Lever 4 做后处理校验就很划算：</p><ol><li>检查 <code>business_meaning</code> 里是否包含和 <code>grain</code> 对应的关键词<br>比如 <code>grain = event</code> 时，描述里至少要出现“事件 &#x2F; 行为 &#x2F; 操作”这类词。</li><li>检查长度<br>比如要求 <code>&lt; 30</code> 字，避免它一展开就像在写报告。</li><li>检查是否带入数据集特异词<br>比如一旦出现“电商”“金融”这类词，就应该报警，因为这很可能说明描述已经开始过拟合当前样本数据集。</li></ol><p>这个阶段 LLM 不是不会说，而是需要一个更工程化的护栏，帮它把明显不稳的输出裁掉。</p><h2 id="问题-C：PK-假阳性，不要再怪-LLM"><a href="#问题-C：PK-假阳性，不要再怪-LLM" class="headerlink" title="问题 C：PK 假阳性，不要再怪 LLM"></a>问题 C：PK 假阳性，不要再怪 LLM</h2><p>像 <code>created_at</code>、<code>name</code> 这种字段也被判成 PK，我现在会直接把它归类为：</p><p><strong>这不是 LLM 问题，是 dataset ingestor 的规则层问题。</strong></p><p>这里最合适的组合是：</p><ol><li><p><strong>Lever 5 规则前置</strong><br>先加排除规则：</p><ul><li>即使 <code>unique = 1.0</code></li><li>但字段名如果命中 <code>*_at / *_time / name / email / description</code></li><li>就不要直接进入 PK 候选</li></ul></li><li><p><strong>Lever 4 LLM 校验</strong><br>把规则筛完之后的候选再丢给 LLM，只让它回答一件事：</p><ul><li>这是主键</li><li>还是“碰巧唯一”</li></ul></li></ol><p>这才是比较健康的分工方式。<br>规则负责先把明显错的候选过滤掉，LLM 负责处理那些“规则不敢拍板”的剩余情况。</p><h2 id="真正执行时，我会分三轮推进"><a href="#真正执行时，我会分三轮推进" class="headerlink" title="真正执行时，我会分三轮推进"></a>真正执行时，我会分三轮推进</h2><h3 id="Phase-1：高-ROI、低成本，先上-1-4-5"><a href="#Phase-1：高-ROI、低成本，先上-1-4-5" class="headerlink" title="Phase 1：高 ROI、低成本，先上 1 + 4 + 5"></a>Phase 1：高 ROI、低成本，先上 <code>1 + 4 + 5</code></h3><p>第一阶段我最看重的是这三个：</p><ol><li><strong>输入信号增强</strong></li><li><strong>后处理校验</strong></li><li><strong>规则前置</strong></li></ol><p>因为这三个都是结构性改进，而且基本不依赖某个具体数据集。<br>它们做好了，不只是当前这个任务受益，后面别的数据集也会一起吃到提升。</p><h3 id="Phase-2：还不够，再上-2-3"><a href="#Phase-2：还不够，再上-2-3" class="headerlink" title="Phase 2：还不够，再上 2 + 3"></a>Phase 2：还不够，再上 <code>2 + 3</code></h3><p>如果第一轮做完还不够，再考虑：</p><ol><li><strong>任务拆分</strong>：把一次性推 <code>grain + business_meaning + PK</code>，拆成多步生成</li><li><strong>换更强模型</strong>：比如切到 <code>deepseek-reasoner</code> 或 <code>GPT-4o</code></li></ol><p>我把这两个放到第二阶段，是因为它们更像“针对性加强”，不是第一时间就该动的通用解法。</p><h3 id="Phase-3：最后兜底，再上-7-6"><a href="#Phase-3：最后兜底，再上-7-6" class="headerlink" title="Phase 3：最后兜底，再上 7 + 6"></a>Phase 3：最后兜底，再上 <code>7 + 6</code></h3><p>只有前面都不够时，我才会考虑更贵的方案：</p><ol><li><strong>多 LLM 协作</strong><br><code>Generator -&gt; Reviewer -&gt; Final Rewrite</code></li><li><strong>多次采样投票</strong><br>同一个 prompt 跑 3 次，取多数</li></ol><p>这两个方案确实有效，但 token 成本基本都会翻倍，甚至更多。<br>所以在我这里，它们更像“最后的暴力兜底”，不是第一反应。</p><h2 id="如果要自审，我会这么拆"><a href="#如果要自审，我会这么拆" class="headerlink" title="如果要自审，我会这么拆"></a>如果要自审，我会这么拆</h2><p>如果进入第二轮还不够，我最愿意加的是一个轻量 reviewer：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">Generator: 根据 profile 推 grain + business_meaning</span><br><span class="line">   ↓</span><br><span class="line">Reviewer: 检查两件事</span><br><span class="line">  - 这个 grain 和 PK 数量、行数、外键结构一致吗？</span><br><span class="line">  - business_meaning 里有没有数据集特异业务名词？</span><br><span class="line">   ↓</span><br><span class="line">Final: 根据 reviewer 反馈重写一次</span><br></pre></td></tr></table></figure><p>这套模式的好处在于，它不是简单地“多调用一次模型”，而是把一次性生成拆成了：</p><ol><li>先产出答案</li><li>再用另一个视角挑错</li><li>最后基于错误反馈修正</li></ol><p>很多原本第一轮就会漏掉的矛盾，在 reviewer 视角下反而很容易被抓出来。</p><h2 id="第一轮跑完后，别只看总准确率"><a href="#第一轮跑完后，别只看总准确率" class="headerlink" title="第一轮跑完后，别只看总准确率"></a>第一轮跑完后，别只看总准确率</h2><p>我现在越来越不相信单一总分。</p><p>如果 <code>grain</code> 准确率低于 60%，我不会马上重写 prompt，而会先按错误类型拆：</p><ol><li><code>dim_*</code> 表到底错了多少<br>看维表是不是总被认成事实表。</li><li><code>fact_*</code> 表到底错了多少<br>看事实表是不是总被认成维表。</li><li>复合键表到底错了多少<br>看桥表、SCD 表这类结构是不是完全没被识别出来。</li><li>没有 <code>fact_ / dim_</code> 前缀的表错了多少<br>这才是真正检验系统通用性的地方。</li></ol><p>因为不同错误，应该用不同武器：</p><ul><li><strong>全错集中在某类表</strong>：优先补输入信号</li><li><strong>看起来像对了、其实结构不一致</strong>：补后处理校验</li><li><strong>明显结构信号都没用上</strong>：上规则前置，把 LLM 退回到边界情况</li><li><strong>各类都错一点</strong>：再考虑换模型</li></ul><h2 id="有三件事我现在会坚决不做"><a href="#有三件事我现在会坚决不做" class="headerlink" title="有三件事我现在会坚决不做"></a>有三件事我现在会坚决不做</h2><ol><li><strong>Fine-tune</strong><br>太慢，太贵，短周期里性价比极低。</li><li><strong>把规则写成数据集特异硬编码</strong><br>这直接违背了“数据集无关”的目标。</li><li><strong>把已知陷阱直接写进 prompt 喂答案</strong><br>这看起来像优化，实际上是在破坏自动构建的考核意义。</li></ol><p>我现在最警惕的一类“优化”，就是那种短期分数会涨，但系统通用性会悄悄塌掉的做法。</p><h2 id="最后一个提醒：产品手动测试仍然是最强武器"><a href="#最后一个提醒：产品手动测试仍然是最强武器" class="headerlink" title="最后一个提醒：产品手动测试仍然是最强武器"></a>最后一个提醒：产品手动测试仍然是最强武器</h2><p>自动化测试很重要，但它永远不是全部。</p><p>哪怕 <code>grain</code> 从 0% 提升到 80%，也还是可能藏着一些自动化测不出来的问题：</p><ol><li><code>fact_subscription</code> 的 <code>business_meaning</code> 写成“订阅信息表”，看着像对，但 <code>grain</code> 其实仍然错了</li><li>KB 工具调用成功了，但返回结果根本没真正影响 system prompt，因为模板里有 bug</li><li><code>search_tables</code> 返回了 5 张表，LLM 却选错了去做 JOIN</li></ol><p>这些问题只有端到端手测才抓得出来。</p><p>所以我现在对“自动化”和“手动测试”的理解是：</p><ul><li>自动化负责覆盖率、规则一致性、结构正确性</li><li>产品手测负责端到端可用性和真实任务完成度</li></ul><p>它们不是替代关系，而是互补关系。</p><h2 id="我的结论：调优先看层，再看招"><a href="#我的结论：调优先看层，再看招" class="headerlink" title="我的结论：调优先看层，再看招"></a>我的结论：调优先看层，再看招</h2><p>如果让我把这次经验压缩成一句话，那就是：</p><p><strong>Agent 调优最怕的，不是招数不够，而是还没分清问题在哪一层，就开始乱出招。</strong></p><p>真正高 ROI 的路径，通常不是第一时间换更大的模型，而是先把：</p><ol><li>输入信号补够</li><li>规则边界划清</li><li>后处理兜底补上</li><li>再把昂贵的多模型协作和多次采样，留到最后</li></ol><p>这样调出来的系统，才更像一个可复用的工程能力，而不是一段只在当前数据集上偶然奏效的 prompt 魔法。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/notes/agent-tuning-playbook/</id>
    <link href="https://bniosfhaiuk.xyz/notes/agent-tuning-playbook/"/>
    <published>2026-04-26T05:30:00.000Z</published>
    <summary>把 Agent 调优拆回输入信号、任务拆分、规则前置、后处理校验和多模型协作之后，我越来越确定：真正高 ROI 的优化，很少是第一时间去换更强的模型。</summary>
    <title>别一上来就换模型：我现在给 Agent 调优的武器库</title>
    <updated>2026-04-26T05:01:43.697Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="随笔" scheme="https://bniosfhaiuk.xyz/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="AI" scheme="https://bniosfhaiuk.xyz/tags/AI/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="Harness" scheme="https://bniosfhaiuk.xyz/tags/Harness/"/>
    <category term="FOMO" scheme="https://bniosfhaiuk.xyz/tags/FOMO/"/>
    <category term="技术思考" scheme="https://bniosfhaiuk.xyz/tags/%E6%8A%80%E6%9C%AF%E6%80%9D%E8%80%83/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/dorm-mustang-harness-moment.svg" alt="Dorm Mustang Harness Cover"></p><p>在这个造词速度远超代码编译速度的时代，作为一名还在大学宿舍里自己折腾 AI 的学生，我时常感到一种难以名状的疲惫。</p><p>每一天醒来，科技圈都在狂欢。昨天的头条还是 Prompt Engineering，今天就换成了 RAG，明天又飞出了 MCP、Agentic Workflow……那些西装革履的大佬们站在聚光灯下，用一个个崭新且高深莫测的名词，丈量着这个行业的边界。而我，坐在杂乱的书桌前，面对着频频报错的终端窗口，心里总有一只名叫 FOMO 的野兽在撕咬：我是不是又学慢了？我自己瞎写的这堆代码，是不是根本连门都没入？</p><p>直到几天前，我撞见了一个正在席卷工程圈的新词：Harness。</p><p>在那一刻，我突然听到了一声清脆的碎裂声。</p><p>那是我对“行业权威”的盲目焦虑，碎裂的声音。</p><h2 id="那些拿不上台面的“野路子”"><a href="#那些拿不上台面的“野路子”" class="headerlink" title="那些拿不上台面的“野路子”"></a>那些拿不上台面的“野路子”</h2><p>如果你也自己动手写过 Agent 项目，大概率会懂那种被大模型逼疯的深夜。</p><p>行业报告里的大模型，是无所不知的神明；但我终端里的大模型，却是一匹常常发癫的野马。它会莫名其妙地吐出少了一个括号的 JSON，把我的整个项目搞崩溃；它会像陷入梦魇一样在错误里死循环，眼睁睁地看着我卡里可怜的 API 余额如流水般燃烧。</p><p>为了拴住这匹野马，我不得不在它的外围垒起一圈又一圈的栅栏。</p><p>我写了一堆正则表达式，像个强迫症一样去校验它的每一次输出；我给函数加上了局部计数器，冷酷地规定“如果你重试三次还是改不对，就立刻给我停下”；我甚至写了一套看起来极其臃肿的 Fallback 逻辑。如果它实在干不了，那就强行返回一个默认值，哪怕显得有些呆板，至少别让程序死在用户面前。</p><p>我一直把这些代码藏得很深。我觉得它们不优雅，太笨拙，充满了“草台班子”的野生气息。它们就像是我为了掩盖大模型的愚蠢，而打满的补丁。</p><h2 id="当我的补丁，成了世界的范式"><a href="#当我的补丁，成了世界的范式" class="headerlink" title="当我的补丁，成了世界的范式"></a>当我的补丁，成了世界的范式</h2><p>但当我点开那篇关于“Harness Engineering”的前沿解析时，我愣住了。</p><p>文章里那些精美的架构图，那些被定义为“下一代 AI 核心基建”的系统约束、沙箱环境、错误兜底和状态流转控制……这不就是我在宿舍里，为了省那几块钱 Token 费，一行行手搓出来的“补丁”吗？</p><p>那一瞬间，我的心情极其微妙。</p><p>起初是一丝没来由的失落。我明明在黑夜里独自摸索出了这条路，但我只是个连社会都没进的大学生，我没有话语权去定义它。如今，大佬们给它穿上了名叫 Harness 的华丽外衣，很快，市面上就会充满开箱即用的框架。我曾经熬夜死磕的难关，即将变成别人只要输入一行 <code>npm install</code> 就能解决的问题。</p><p>但紧随其后的，是一种像海啸般涌来的技术自信。</p><p>我没有落后。</p><p>在这个喧嚣的时代，在没有任何文档指导、完全凭借个人兴趣和工程直觉的荒野求生中，我竟然精准地踩中了和全球顶尖架构师一模一样的演进脉络。我的直觉没有错，我遇到的痛点，就是这个时代真实的痛点。</p><h2 id="撕下包装，只看代码"><a href="#撕下包装，只看代码" class="headerlink" title="撕下包装，只看代码"></a>撕下包装，只看代码</h2><p>这次奇妙的巧合，像一场大雨，彻底洗刷了我的名词焦虑症。</p><p>现在，当我再去刷技术社区，看到那些新颖的范式和玄乎其玄的缩写时，我的内心不再有波澜。我学会了在心里把它们残忍地降维：</p><p>“别整那些虚的，你就告诉我，你是怎么处理那头野马的输入输出的？你那套华丽的框架，能不能拦住它发癫时的死循环？”</p><p>我知道，未来会有无数人直接调用成熟的 Harness 框架。他们坐在平稳的马车上，赞叹着 AI 的神奇。但这并不妨碍我骄傲，因为我曾亲手被那匹野马摔在泥地里，我曾亲手搓过一条粗糙但结实的缰绳。</p><p>当程序在未知的边缘报错时，调包的人只能看着说明书抓瞎，而我，听一听引擎卡壳的声音，就知道是哪里漏了油。</p><h2 id="尾声：给所有独自前行的赶路人"><a href="#尾声：给所有独自前行的赶路人" class="headerlink" title="尾声：给所有独自前行的赶路人"></a>尾声：给所有独自前行的赶路人</h2><p>AI 的风刮得太快，快到连造词的速度都赶不上填坑的速度。</p><p>如果你也像我一样，是个凭着一腔热血自己在泥潭里折腾的学生开发者，请千万不要被那些光鲜亮丽的词汇吓退，更不要因为自己写着看似“不标准”的防御代码而感到自卑。</p><p>不管它叫 Wrapper、Middleware，还是今天火爆的 Harness，本质上都是我们在驯服未知时，留在屏幕上的真实伤痕。不要等待别人来定义你的代码，你的每一次 Debug，每一个笨拙的兜底逻辑，都在构筑你最坚硬的技术底色。</p><p>大佬们在 PPT 上定义着世界的形状。而我们，只负责在宿舍的冷光里，把 Demo 跑通。</p><p>而且，我们跑得一点也不慢。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/fragments/dorm-mustang-harness-moment/</id>
    <link href="https://bniosfhaiuk.xyz/fragments/dorm-mustang-harness-moment/"/>
    <published>2026-04-19T05:10:00.000Z</published>
    <summary>当“Harness”成了新范式，我才意识到，自己在宿舍里为驯服大模型写下的那些笨拙补丁，原来正踩在真实的工程演进路线上。</summary>
    <title>宿舍里的野马与马鞍：一个学生开发者的 AI 祛魅时刻</title>
    <updated>2026-04-19T15:31:48.246Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="随笔" scheme="https://bniosfhaiuk.xyz/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="AI" scheme="https://bniosfhaiuk.xyz/tags/AI/"/>
    <category term="RAG" scheme="https://bniosfhaiuk.xyz/tags/RAG/"/>
    <category term="FOMO" scheme="https://bniosfhaiuk.xyz/tags/FOMO/"/>
    <category term="技术思考" scheme="https://bniosfhaiuk.xyz/tags/%E6%8A%80%E6%9C%AF%E6%80%9D%E8%80%83/"/>
    <category term="营销" scheme="https://bniosfhaiuk.xyz/tags/%E8%90%A5%E9%94%80/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/marketing-charisma-moment.svg" alt="Marketing Magic Cover"></p><p>昨晚改完自己项目的 bug 之后，顺手刷起了抖音。</p><p>满屏都在惊呼一个由好莱坞影星跨界参与的 AI 记忆项目。那些加粗的“100% 满分”“彻底解决大模型失忆”“程序员即将下岗”扑面而来，我靠在椅背上，看着屏幕发了一会儿呆。</p><p>我还远没踏入真正的行业大门，充其量只是个爱好技术的大学生。日常最大的烦恼，不过是项目里的 bug 和跑不通的模型。可哪怕只是用我那点还在及格线边缘徘徊的软件工程知识，也能一眼看出这个被捧上神坛的项目，底色有多么单薄。</p><h2 id="这不就是最基础的-RAG-吗？"><a href="#这不就是最基础的-RAG-吗？" class="headerlink" title="这不就是最基础的 RAG 吗？"></a>这不就是最基础的 RAG 吗？</h2><p>用几百行 Python 脚本，连上一个简单的本地数据库，可能再配个 SQLite；加一层传统词频检索，比如 BM25；最后再套上大模型 API。</p><p>如果我在期末大作业里交上这么一份代码，甚至连最基础的边界测试都不做，老师大概会直接把报告打回来，让我重写。</p><p>因为哪怕还在象牙塔里，我们也知道：写代码最难的从来不是“在理想状态下把 demo 跑通”，而是做那些没人愿意拍进宣传片里的东西。</p><h2 id="真正难的，从来不是-Demo"><a href="#真正难的，从来不是-Demo" class="headerlink" title="真正难的，从来不是 Demo"></a>真正难的，从来不是 Demo</h2><p>真正难的是防呆设计。</p><p>你得设想用户各种突破底线的输入，处理那些平时不响、出事就致命的并发问题，在系统即将崩掉的时候给它兜底。尤其在所谓的 AI 记忆系统里，面对真实的、前后矛盾的、充满噪音的用户上下文，如果只是把聊天记录转成文本，检索出来，再塞回大模型，那根本不叫记忆。</p><p>那只是一个高配版的 <code>Ctrl+F</code>。</p><p>如果系统不会处理记忆冲突，不会做优先级判断，不会做时间衰减，不会在错误召回时自我纠偏，那它离“记忆”这两个字其实还很远。它只是比普通聊天记录多了一层好看的包装。</p><h2 id="营销最聪明的一步，是绕开技术"><a href="#营销最聪明的一步，是绕开技术" class="headerlink" title="营销最聪明的一步，是绕开技术"></a>营销最聪明的一步，是绕开技术</h2><p>但现实偏偏很魔幻。</p><p>这个爆款项目在代码层面几乎完全放弃了“防呆”，却在传播层面选中了一条更轻盈的路径：既然不好解决真实世界的问题，那就直接定义一个对自己最有利的舞台。</p><p>不需要处理动态路由，不需要解决记忆冲突，不需要承受真实用户的脏输入和压力测试。只需要在一个高度定制的测试集里，用特定关键词把昨天存进去的聊天记录原封不动地搜出来，再套上明星背书的光环，这就足够被包装成一场“颠覆性的技术革命”。</p><p>这有点像什么呢？</p><p>像我在宿舍里用电池和马达拼了个四驱车，然后对着镜头宣布，我造出了可以直接取代特斯拉的自动驾驶底盘。</p><p>荒诞的不是玩具本身，而是居然真的会有那么多人为这种包装欢呼。</p><h2 id="星星、流量和错位感"><a href="#星星、流量和错位感" class="headerlink" title="星星、流量和错位感"></a>星星、流量和错位感</h2><p>最让我产生错位感的，不是它火了，而是它火的方式。</p><p>我其实并不眼红，毕竟我连去眼红的资格都还没攒够。我只是有点疲惫，也有点不解。毕竟很多学生做项目，也是想要 GitHub 上那几个星星，想证明自己的东西真的被人看见过。</p><p>可如果目标只是星星、转发和惊呼，那条最短路径好像根本不是老老实实把系统做扎实，而是先把故事讲得足够吓人、足够宏大、足够让外行产生 FOMO。</p><p>从这个角度看，营销确实比写项目快得多。</p><h2 id="但世界也没那么草台班子"><a href="#但世界也没那么草台班子" class="headerlink" title="但世界也没那么草台班子"></a>但世界也没那么草台班子</h2><p>冷静下来想想，我又觉得世界也没有那么草台班子。</p><p>他们能骗到的，或许更多只是 GitHub 上的点赞、社交媒体上的惊呼，或者外行圈子里那种“我是不是错过了什么”的焦虑情绪。可真正掌握核心算力、真正主导下一代模型架构的大厂和实验室，不会为这种经不起压力测试的玩具买单。</p><p>那些真正懂行的人，大概只要看一眼源码，就知道这东西到底是工程突破，还是一次包装精良的情绪投喂。</p><p>风一吹，那件华丽的外衣里面，其实还是空的。</p><h2 id="技术圈的“皇帝的新装”"><a href="#技术圈的“皇帝的新装”" class="headerlink" title="技术圈的“皇帝的新装”"></a>技术圈的“皇帝的新装”</h2><p>后来我想，也许这就是技术圈版本的“皇帝的新装”。</p><p>那些看起来震耳欲聋的欢呼，有时候并不是因为真的发生了什么革命，而是因为一群人共同参加了一场名为错失恐惧症的情绪派对。大家害怕落后，害怕看不懂，害怕自己不是最先鼓掌的人，于是掌声就先于判断出现了。</p><p>而对于我这样一个还在读书的普通学生来说，看透了这场魔术的底牌之后，反倒获得了一种奇怪的平静。</p><p>发光的未必是金子，也可能只是一块被流量反复抛光过的塑料。</p><h2 id="让秀场留在秀场里"><a href="#让秀场留在秀场里" class="headerlink" title="让秀场留在秀场里"></a>让秀场留在秀场里</h2><p>外面的世界再怎么喧闹，真正能支撑起一座大厦的，终究还是一行行扎实的代码，和那些不太容易被看见、却决定系统能不能活下去的逻辑细节。</p><p>秀场里的喝彩声，留给秀场就好了。</p><p>我喝完杯子里最后一口水，看了看时间。明天还有课，而我的项目里还有 bug 没修完。</p><p>这大概也是一件好事。至少说明我还在做那些虽然不够耀眼，却真正会留下来的东西。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/fragments/marketing-charisma-moment/</id>
    <link href="https://bniosfhaiuk.xyz/fragments/marketing-charisma-moment/"/>
    <published>2026-04-15T07:20:00.000Z</published>
    <summary>刷到一个被包装成“AI 记忆革命”的项目后，我突然更清楚地意识到：很多被流量抛光的技术神话，本质上只是基础 RAG 套了层夸张叙事。</summary>
    <title>营销的魅力时刻</title>
    <updated>2026-04-15T15:47:08.800Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="架构设计" scheme="https://bniosfhaiuk.xyz/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="Bug定位" scheme="https://bniosfhaiuk.xyz/tags/Bug%E5%AE%9A%E4%BD%8D/"/>
    <category term="沙箱" scheme="https://bniosfhaiuk.xyz/tags/%E6%B2%99%E7%AE%B1/"/>
    <category term="代码执行" scheme="https://bniosfhaiuk.xyz/tags/%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/sandbox-design-five-critical-bugs.svg" alt="Sandbox Design Five Critical Bugs Cover"></p><p>这篇文章想记录的，不只是“我接了一套代码沙箱”。</p><p>我更想写清楚另一件事：</p><p><strong>当一条分析链路开始同时跨越 LLM、执行环境、宿主机渲染、SSE 推流和前端状态管理时，复杂 Bug 往往不死在某一行代码里，而是死在边界上。</strong></p><p>这次在 OpsMind 里做沙箱方案落地，最后真正让我成长的，不是把 <code>run_code</code> 工具接上，而是把五个卡在不同边界层的关键问题一层层拆开、定位、修掉。</p><hr><h2 id="一、为什么原来的方案迟早会卡住"><a href="#一、为什么原来的方案迟早会卡住" class="headerlink" title="一、为什么原来的方案迟早会卡住"></a>一、为什么原来的方案迟早会卡住</h2><p>OpsMind 最早的数据分析链路，大致是这样：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">用户提问</span><br><span class="line">  -&gt; LLM 判断图表类型</span><br><span class="line">  -&gt; LLM 猜列映射</span><br><span class="line">  -&gt; 预设渲染器执行</span><br><span class="line">  -&gt; LLM 根据结果写结论</span><br></pre></td></tr></table></figure><p>这套方案在简单问题上不是不能用，但它有一个很难绕开的结构性限制：</p><p><strong>LLM 负责猜，Python 负责跑，而“猜”和“跑”之间没有真实反馈回路。</strong></p><p>模型在决定图表类型和列映射时，并不知道：</p><ul><li>某一列是不是高基数，根本不适合饼图</li><li>某一列缺失率过高，直接计算比例会失真</li><li>某个日期字段根本不是标准时间格式</li><li>用户想看的指标，原始数据里其实不存在，需要先计算</li></ul><p>也就是说，原来的系统始终在让模型“预猜执行现场”。</p><p>你可以不断加列信息、加规则、加 prompt，但只要模型看不到真实执行结果，它就只能越来越努力地猜，而不可能真正从运行结果里修正自己。</p><p>这也是我后来决定切到沙箱方案的根本原因：</p><p><strong>复杂数据分析的问题，重点不是让模型更会猜，而是让模型有机会先算、再看、再改。</strong></p><hr><h2 id="二、我最后怎么收敛这套沙箱架构"><a href="#二、我最后怎么收敛这套沙箱架构" class="headerlink" title="二、我最后怎么收敛这套沙箱架构"></a>二、我最后怎么收敛这套沙箱架构</h2><p>我后来没有让 LLM 在沙箱里直接写 Plotly，也没有把整个渲染流程完全交给它。</p><p>最后落下来的，是一套更克制的分层：</p><h3 id="沙箱内"><a href="#沙箱内" class="headerlink" title="沙箱内"></a>沙箱内</h3><ul><li>用 pandas &#x2F; numpy 做自由计算</li><li>通过 <code>declare_chart()</code> 声明要画什么图</li><li>通过 <code>declare_table()</code> 声明要展示什么表</li><li>用 <code>print()</code> 输出洞察和说明</li></ul><h3 id="宿主机"><a href="#宿主机" class="headerlink" title="宿主机"></a>宿主机</h3><ul><li>读取 <code>render_spec.json</code></li><li>调用现有 <code>InteractiveChartGenerator</code></li><li>统一渲染成 Plotly HTML</li><li>返回给前端</li></ul><p>这套架构我后来把它理解成：</p><p><strong>LLM 负责数据计算，宿主机负责稳定渲染。</strong></p><p>换句话说：</p><ul><li>开放给 LLM 的，是“怎么算数据”</li><li>不开放给 LLM 的，是“怎么稳定落成图表产物”</li></ul><p>这样做有两个直接好处。</p><p>第一，模型不需要熟悉复杂的前端图表 API，它只需要把真正有价值的分析计算做出来。<br>第二，最终产物仍然走宿主机这条可控渲染链，质量、样式和兼容性不会被沙箱代码拉散。</p><p>每次执行前，我会在用户代码前注入一段 preamble，预先放好：</p><ul><li>数据读取入口</li><li><code>declare_chart</code></li><li><code>declare_table</code></li><li>spec 写入逻辑</li><li>退出时落盘的 <code>atexit</code></li></ul><p>真正重要的不是 preamble 写了多长，而是它把沙箱代码和宿主机之间的契约固定住了。</p><hr><h2 id="三、五个关键-Bug-是怎么被拆开的"><a href="#三、五个关键-Bug-是怎么被拆开的" class="headerlink" title="三、五个关键 Bug 是怎么被拆开的"></a>三、五个关键 Bug 是怎么被拆开的</h2><p>这次最有价值的部分，不是“终于能跑”，而是这五个问题让我越来越确定：复杂 Bug 的诊断，核心永远是先找对边界。</p><hr><h3 id="Bug-1：Permission-denied"><a href="#Bug-1：Permission-denied" class="headerlink" title="Bug 1：Permission denied: &#39;.&#39;"></a>Bug 1：<code>Permission denied: &#39;.&#39;</code></h3><p><strong>现象</strong></p><p>沙箱日志里显示执行成功，图表数量也正常，但前端一个图都看不到。宿主机日志报：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">PermissionError: [Errno 13] Permission denied: &#x27;.&#x27;</span><br></pre></td></tr></table></figure><p><strong>根因</strong></p><p>问题出在沙箱产物收集阶段：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">for</span> filename <span class="keyword">in</span> file_list:</span><br><span class="line">    src_path = output_dir / filename</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> src_path.exists():</span><br><span class="line">        <span class="keyword">continue</span></span><br><span class="line">    shutil.copy2(src_path, charts_path / filename)</span><br></pre></td></tr></table></figure><p>当 <code>filename</code> 是空字符串时，<code>output_dir / &quot;&quot;</code> 实际会被解释成当前目录 <code>.</code>。<br>而 <code>Path(&quot;.&quot;).exists()</code> 会返回 <code>True</code>，于是系统继续往下执行，最后尝试把目录当成文件去复制。</p><p><strong>修复</strong></p><p>把判断从 <code>exists()</code> 改成 <code>is_file()</code>：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> src_path.is_file():</span><br><span class="line">    <span class="keyword">continue</span></span><br></pre></td></tr></table></figure><p><strong>教训</strong></p><p>文件系统问题经常不是“路径存不存在”，而是“这个路径到底是不是你以为的类型”。<br>在文件复制、上传、收集这类链路里，<code>exists()</code> 往往是不够的，应该优先用 <code>is_file()</code> &#x2F; <code>is_dir()</code> 这种带语义的判断。</p><hr><h3 id="Bug-2：create-table-groupby-got-unexpected-keyword-argument-title"><a href="#Bug-2：create-table-groupby-got-unexpected-keyword-argument-title" class="headerlink" title="Bug 2：create_table_groupby() got unexpected keyword argument &#39;title&#39;"></a>Bug 2：<code>create_table_groupby() got unexpected keyword argument &#39;title&#39;</code></h3><p><strong>现象</strong></p><p>沙箱执行已经成功，但宿主机渲染 spec 时抛出：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">TypeError: create_table_groupby() got unexpected keyword argument &#x27;title&#x27;</span><br></pre></td></tr></table></figure><p><strong>根因</strong></p><p>Spec-Driven 的执行方式是把 spec 里的配置整体解包给渲染器：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">fig = create_fn(df, **config)</span><br></pre></td></tr></table></figure><p>但 <code>create_table_groupby()</code> 和部分 <code>create_table_*()</code> 方法的签名里并没有 <code>title</code> 参数。<br>这意味着：沙箱侧和宿主机侧虽然都“支持 table spec”，但它们理解的字段契约并不一致。</p><p><strong>修复</strong></p><p>给相关方法补上统一参数签名：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">create_table_groupby</span>(<span class="params">self, df, group_col, value_cols, agg_funcs=<span class="literal">None</span>, title=<span class="literal">None</span></span>):</span><br><span class="line">    ...</span><br></pre></td></tr></table></figure><p><strong>教训</strong></p><p>这种问题表面上像一个 Python <code>TypeError</code>，本质上却是接口契约漂移。<br>只要系统是“上游生成 spec，下游消费 spec”，那所有字段都不再是某一侧的内部实现细节，而是跨模块契约。</p><hr><h3 id="Bug-3：前端重复提交，Agent-被连续启动三次"><a href="#Bug-3：前端重复提交，Agent-被连续启动三次" class="headerlink" title="Bug 3：前端重复提交，Agent 被连续启动三次"></a>Bug 3：前端重复提交，Agent 被连续启动三次</h3><p><strong>现象</strong></p><p>同一个发送动作，后端日志里却出现了三段并行启动的 Agent 流程。前端看起来像“点了一次”，系统实际发了三次。</p><p><strong>根因</strong></p><p>原来的前端提交守卫读的是 React state：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (streaming !== <span class="literal">null</span> &amp;&amp; !streaming.<span class="property">error</span>) <span class="keyword">return</span></span><br></pre></td></tr></table></figure><p>但 React 18 的 batched updates 决定了：<code>setState</code> 不会在当前执行路径里立刻反映到闭包读取值上。<br>第一次提交后，<code>streaming</code> 在当前批次里仍然可能是旧值 <code>null</code>，于是后续极短时间内的再次触发仍会穿透守卫。</p><p><strong>修复</strong></p><p>改成读取同步更新的 ref：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">if</span> (streamingRef.<span class="property">current</span> !== <span class="literal">null</span> &amp;&amp; !streamingRef.<span class="property">current</span>.<span class="property">error</span>) <span class="keyword">return</span></span><br></pre></td></tr></table></figure><p>并同步清理掉 <code>handleSend</code> 里对 <code>streaming</code> 的依赖。</p><p><strong>教训</strong></p><p>React state 适合驱动渲染，不适合做毫秒级竞态防护。<br>任何要求“当前调用立刻可见”的保护状态，都应该优先用 ref，而不是依赖 state 何时回流到组件闭包。</p><hr><h3 id="Bug-4：前端只显示最后一批图表"><a href="#Bug-4：前端只显示最后一批图表" class="headerlink" title="Bug 4：前端只显示最后一批图表"></a>Bug 4：前端只显示最后一批图表</h3><p><strong>现象</strong></p><p>多轮 <code>run_code</code> 明明产出了多批图表，但前端最后只剩最后一轮的结果。</p><p><strong>根因</strong></p><p>后端通过 SSE 连续发送多个 <code>charts</code> 事件，而前端 reducer 把它当成“全量替换”处理：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">case</span> <span class="string">&#x27;charts&#x27;</span>:</span><br><span class="line">  <span class="keyword">return</span> &#123; ...prev, <span class="attr">charts</span>: (msg.<span class="property">data</span>.<span class="property">files</span> <span class="keyword">as</span> <span class="title class_">ChartItem</span>[]) ?? prev.<span class="property">charts</span> &#125;</span><br></pre></td></tr></table></figure><p>这意味着第二轮结果一到，第一轮结果就被整体覆盖掉了。</p><p><strong>修复</strong></p><p>把事件语义改成增量追加：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">case</span> <span class="string">&#x27;charts&#x27;</span>: &#123;</span><br><span class="line">  <span class="keyword">const</span> newCharts = (msg.<span class="property">data</span>.<span class="property">files</span> <span class="keyword">as</span> <span class="title class_">ChartItem</span>[]) ?? []</span><br><span class="line">  <span class="keyword">return</span> &#123; ...prev, <span class="attr">charts</span>: [...prev.<span class="property">charts</span>, ...newCharts] &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>tables 同理。</p><p><strong>教训</strong></p><p>很多前后端问题看起来像“显示错了”，其实真正错的是事件语义根本没有讲清楚。<br>一个 SSE 事件到底代表“当前全量状态”还是“本轮新增产物”，必须在设计阶段就定死，不然只要链路进入多轮执行，问题迟早暴露。</p><hr><h3 id="Bug-5：LLM-始终没有机会调用-run-code"><a href="#Bug-5：LLM-始终没有机会调用-run-code" class="headerlink" title="Bug 5：LLM 始终没有机会调用 run_code"></a>Bug 5：LLM 始终没有机会调用 <code>run_code</code></h3><p><strong>现象</strong></p><p>明明已经接好了深度分析工具，但复杂请求还是稳定走老的 <code>plan_analysis -&gt; execute_analysis</code> 路径，LLM 从头到尾没机会进入真正的代码执行。</p><p><strong>根因</strong></p><p>问题不在 prompt，也不在工具表本身，而在更早的路由器。</p><p>当请求没有命中知识库、数据库、连续对话这些关键词时，系统默认直接走：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">return</span> <span class="string">&quot;direct_analysis&quot;</span></span><br></pre></td></tr></table></figure><p>而 <code>direct_analysis</code> 是一条固定链路，根本不会进入 ReAct 循环。<br>这意味着虽然 system prompt 写着“你可以调用 <code>run_code</code>”，但模型在执行层面从来没拿到过这个决策权。</p><p><strong>修复</strong></p><p>我最后没有继续修补规则分类器，而是把决策权显式还给用户：</p><ul><li>快速模式：强制走 <code>direct_analysis</code></li><li>深度模式：直接进入 ReAct，让 LLM 自主决定是否调用 <code>run_code</code></li></ul><p>也就是说，不再让系统偷偷替用户做“你这题适不适合深度执行”的隐式判断，而是把两种执行范式公开成一个可感知的开关。</p><p><strong>教训</strong></p><p>“规则路由优先” 和 “LLM 自主决策优先” 是两种不同的架构范式。<br>如果你真的希望模型自主决策，就不能在它前面先把路线封死。</p><hr><h2 id="四、这五个-Bug-其实共同暴露了什么"><a href="#四、这五个-Bug-其实共同暴露了什么" class="headerlink" title="四、这五个 Bug 其实共同暴露了什么"></a>四、这五个 Bug 其实共同暴露了什么</h2><p>回头看，这五个问题分布在完全不同的层：</p><ul><li>文件系统</li><li>渲染接口</li><li>React 并发更新</li><li>SSE 事件协议</li><li>执行路由</li></ul><p>但它们本质上都在提醒同一件事：</p><p><strong>复杂 Bug 很少是单点错误，它更常见的形态是：边界语义没有被定义清楚。</strong></p><p>具体来说，就是这五类边界：</p><h3 id="1-文件边界"><a href="#1-文件边界" class="headerlink" title="1. 文件边界"></a>1. 文件边界</h3><p>你以为自己拿到的是文件名，实际可能拿到的是空路径或目录引用。</p><h3 id="2-契约边界"><a href="#2-契约边界" class="headerlink" title="2. 契约边界"></a>2. 契约边界</h3><p>你以为上下游都在说“同一份 spec”，实际上它们对字段支持并不一致。</p><h3 id="3-状态边界"><a href="#3-状态边界" class="headerlink" title="3. 状态边界"></a>3. 状态边界</h3><p>你以为前端守卫已经生效，实际上当前调用看到的还是旧 state。</p><h3 id="4-事件边界"><a href="#4-事件边界" class="headerlink" title="4. 事件边界"></a>4. 事件边界</h3><p>你以为后端在推“新的图表”，前端却把它解释成“新的全量结果”。</p><h3 id="5-决策边界"><a href="#5-决策边界" class="headerlink" title="5. 决策边界"></a>5. 决策边界</h3><p>你以为模型拥有自主权，但真正的分流决定已经在它前面被规则路由做完了。</p><p>这也是为什么我现在越来越倾向于这样看复杂排障：</p><p><strong>先别急着问是哪一行错了，先问这条链路的边界语义有没有被讲清楚。</strong></p><hr><h2 id="五、最终落下来的执行形态"><a href="#五、最终落下来的执行形态" class="headerlink" title="五、最终落下来的执行形态"></a>五、最终落下来的执行形态</h2><p>现在这套链路，我最后把它收成了两条明确路径：</p><h3 id="快速模式"><a href="#快速模式" class="headerlink" title="快速模式"></a>快速模式</h3><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">get_data_info</span><br><span class="line">  -&gt; plan_analysis</span><br><span class="line">  -&gt; execute_analysis</span><br></pre></td></tr></table></figure><p>适合标准分析问题，成本低，结果稳定。</p><h3 id="深度模式"><a href="#深度模式" class="headerlink" title="深度模式"></a>深度模式</h3><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">ReAct loop</span><br><span class="line">  -&gt; get_data_info</span><br><span class="line">  -&gt; run_code</span><br><span class="line">  -&gt; plan_analysis / execute_analysis（必要时回退）</span><br></pre></td></tr></table></figure><p>适合需要自定义计算、复杂派生指标或标准图表路径不够表达的问题。</p><p>真正关键的是，这两条路径现在不再互相假装自己是同一种东西。</p><p>快速模式就是规则更强、产物更稳的标准链路。<br>深度模式就是允许模型在真实执行结果里逐步试错、修正和扩展的探索链路。</p><p>只要路径边界清楚，很多原来看起来很“玄”的问题，都会突然变得可以诊断。</p><hr><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这篇文章表面上是在写沙箱方案，实际上更像是在写一次复杂系统排障练习。</p><p>我后来越来越相信，复杂工程里的很多成长，并不来自“又多做了一个功能”，而来自你能不能把一个原本交织在一起的问题，真的拆开。</p><p>拆到最后，你会发现很多事情都没有那么玄：</p><ul><li>图表不显示，也许不是图表的问题，而是文件语义问题</li><li>重复提交，也许不是按钮的问题，而是状态可见性问题</li><li>LLM 不调用工具，也许不是 prompt 的问题，而是路由权根本没给它</li></ul><p>对我来说，这类记录最值得留下的地方也在这里。</p><p>它不是只证明“修好了”，而是尽量把一次复杂 Bug 的形成路径、定位方法和修复原则，真正沉淀成下一次还能复用的工程经验。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/sandbox-design-five-critical-bugs/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/sandbox-design-five-critical-bugs/"/>
    <published>2026-04-14T05:40:00.000Z</published>
    <summary>这篇文章记录我在 OpsMind 里落地代码沙箱时遇到的五个关键 Bug：它们分别卡在文件系统、接口契约、React 并发更新、SSE 事件语义和路由策略边界上。</summary>
    <title>沙箱方案设计与五个关键 Bug：一次把复杂执行链路做顺的诊断实录</title>
    <updated>2026-04-13T17:30:42.271Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="方法论" scheme="https://bniosfhaiuk.xyz/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="架构设计" scheme="https://bniosfhaiuk.xyz/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="代码执行" scheme="https://bniosfhaiuk.xyz/tags/%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C/"/>
    <category term="产品思维" scheme="https://bniosfhaiuk.xyz/tags/%E4%BA%A7%E5%93%81%E6%80%9D%E7%BB%B4/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/two-hard-decisions-scope-architecture-reset.svg" alt="Two Hard Decisions Cover"></p><p>这篇文章想记录的，不是某个功能终于做完了，而是两次很痛、但事后看都很值得的收缩。</p><p>一次是收缩产品范围。<br>一次是收缩架构幻想。</p><p>我后来才意识到，这两个决定其实都在做同一件事：</p><p><strong>把注意力从“看起来完整”拉回“真正成立的核心路径”。</strong></p><hr><h2 id="一、第一个决定：先砍范围，再谈完整"><a href="#一、第一个决定：先砍范围，再谈完整" class="headerlink" title="一、第一个决定：先砍范围，再谈完整"></a>一、第一个决定：先砍范围，再谈完整</h2><p>三个月前，我给 OpsMind 列过一张很完整的能力清单：</p><ul><li>聊天工作台</li><li>知识库</li><li>数据库直连</li><li>报告导出</li><li>Skill 扩展</li><li>落地页</li></ul><p>当时我觉得这些能力缺一不可。</p><p>数据分析工具如果没有数据库直连，好像就不够专业；<br>没有报告导出，好像就留不住成果；<br>没有 Skill 扩展，好像未来也很难讲。</p><p>于是我真的开始把这些都往里做。</p><p>代码库里陆续出现了数据库连接、报告生成、技能注册这些模块，架构文档也越来越像一个“完整平台”。</p><p>但问题也很快出现了。</p><p>数据库连接不是一个按钮，而是一整套连接池、权限、密钥存储和方言差异。<br>报告导出不是一个导出接口，而是一整套渲染、分页、字体和格式系统。<br>Skill 扩展也不是多加几个 prompt，而是协议、生命周期和调用边界。</p><p>每一个“顺手补上”的配套能力，背后其实都是一个独立子系统。</p><p>结果是，三个月过去了，最应该打磨的核心体验反而还没稳：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">上传文件 -&gt; 提问 -&gt; 看到一张可靠的图表</span><br></pre></td></tr></table></figure><p>我后来才承认，自己当时并不是在做一个产品，而是在做一个“平台已经成熟”的幻觉。</p><p>于是我做了第一个决定：</p><p>把 <code>db_connections</code>、<code>reports</code>、<code>skills</code>、<code>data-sources</code> 从 v1.0 范围里拿掉，先归档，不继续投入。</p><p>那次提交一口气删掉了 3508 行代码。</p><p>删完之后我最明显的感受不是“损失”，而是轻。</p><p>代码库变轻了。<br>任务边界变轻了。<br>我的注意力也终于回到了真正该先做对的地方。</p><hr><h2 id="二、这次收缩让我重新理解了一件事"><a href="#二、这次收缩让我重新理解了一件事" class="headerlink" title="二、这次收缩让我重新理解了一件事"></a>二、这次收缩让我重新理解了一件事</h2><p><strong>功能不是天然的资产，很多时候它首先是债务。</strong></p><p>只要一段功能代码进入系统，它就会自动带来后续成本：</p><ul><li>要测试</li><li>要维护</li><li>要解释</li><li>要兼容别的链路</li><li>要让用户真正理解为什么存在</li></ul><p>如果用户还没走到需要它的阶段，这些成本就不会转化成价值，只会持续占用注意力。</p><p>这也是我后来越来越认同的一种区分：</p><p>“先做完整再上线”，更像工程师本能。<br>“先把最小成立路径做对，再看用户真正缺什么”，更像产品判断。</p><p>这两种思路都没错，但它们适合的阶段完全不同。</p><p>在还没有足够真实反馈之前，开发者最容易高估的，往往不是执行能力，而是自己对需求的判断力。</p><p>我当时一直在假设用户需要 MySQL 直连。<br>但如果连第一个真实高频场景都还没跑出来，我其实根本不知道，用户更在意的到底是数据库接入，还是先把 Excel 上传分析这条链路做顺。</p><hr><h2 id="三、第二个决定：推翻那套“预猜式”核心架构"><a href="#三、第二个决定：推翻那套“预猜式”核心架构" class="headerlink" title="三、第二个决定：推翻那套“预猜式”核心架构"></a>三、第二个决定：推翻那套“预猜式”核心架构</h2><p>如果说第一个决定是在砍功能范围，那第二个决定就是在砍我已经投入最多时间的实现路径。</p><p>最初为了让 AI 自动生成图表，我搭过一套自己以为很精细的流程：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">LLM 决定图表类型</span><br><span class="line">  -&gt; LLM 推断列映射</span><br><span class="line">  -&gt; 预设渲染器执行</span><br></pre></td></tr></table></figure><p>围绕这条链路，我写了大量配套代码：</p><ul><li>图表类型判断</li><li>列名模糊匹配</li><li>时间轴修正</li><li>高基数列告警</li><li>重试机制</li><li>大量 prompt 调优</li></ul><p>它在 demo 里不是不能用。</p><p>但真正让我开始警觉的是另一件事：</p><p><strong>这套系统总是在修 bug，却从来没有“修好了”的感觉。</strong></p><p>后来我才想明白，问题不在 prompt，也不在某一个判断分支，而在架构前提本身。</p><p>在这套方案里，LLM 负责先猜对：</p><ul><li>哪张图更合适</li><li>哪一列该做 X 轴</li><li>哪一列该做 Y 轴</li></ul><p>但执行环境里的很多关键信息，它其实在“猜”的时候根本看不到：</p><ul><li>某一列唯一值太多，不适合饼图</li><li>某一列空值率很高，直接算比例会失真</li><li>某一列的数据类型脏得比列名看起来复杂得多</li></ul><p>也就是说，我一直在让模型对一个它还没真正接触到的执行现场做预判。</p><p>这不是 prompt 的问题。<br>这是设计问题。</p><hr><h2 id="四、真正更合理的路径，是让模型先执行再修正"><a href="#四、真正更合理的路径，是让模型先执行再修正" class="headerlink" title="四、真正更合理的路径，是让模型先执行再修正"></a>四、真正更合理的路径，是让模型先执行再修正</h2><p>后来我去看主流 AI 分析产品和代码执行产品的做法，才慢慢意识到：</p><p>更稳的路径通常不是“先猜对再执行”，而是：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">LLM 写代码</span><br><span class="line">  -&gt; 在真实环境里执行</span><br><span class="line">  -&gt; LLM 读取结果、报错和图表</span><br><span class="line">  -&gt; 再做下一轮修正</span><br></pre></td></tr></table></figure><p>这两种思路最大的差别，不是实现细节，而是学习来源。</p><p>“预猜式”架构里，模型只能靠提示和历史经验去猜。<br>“代码执行式”架构里，模型可以直接从真实结果里学习。</p><p>一个 <code>KeyError</code> 会直接暴露真实列名。<br>一张空图会直接暴露数据分布。<br>一次报错会直接告诉模型，问题到底出在语法、映射还是数据本身。</p><p>我后来接受了第二个决定：</p><p>推翻那套围绕预设渲染器堆出来的核心路径，把核心体验重新收敛为：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">上传文件 -&gt; 对话分析 -&gt; 代码执行 -&gt; 返回图表与结论</span><br></pre></td></tr></table></figure><p>现在 OpsMind 的代码执行沙箱用的是 E2B，LLM 直接写 Python，在真实执行结果上迭代。</p><p>少了一层“我替模型预先设计完整世界”的执念，系统反而更清楚了。</p><hr><h2 id="五、这件事真正提醒我的，不只是架构，而是提问方式"><a href="#五、这件事真正提醒我的，不只是架构，而是提问方式" class="headerlink" title="五、这件事真正提醒我的，不只是架构，而是提问方式"></a>五、这件事真正提醒我的，不只是架构，而是提问方式</h2><p>回头看，那三个月里我并不是没有努力。</p><p>我写了很多代码。<br>我做了很多调优。<br>我也一直在和 AI 助手协作。</p><p>问题在于，我问的问题一直不够对。</p><p>我当时更常问的是：</p><p><strong>“怎么把我现在这套方案继续优化下去？”</strong></p><p>但真正应该先问的问题，其实是：</p><p><strong>“这个领域里，更成熟的做法本来是什么？”</strong></p><p>这也是我后来对 AI 协作更警惕的一点。</p><p>AI 很擅长沿着当前方向帮你补得更完整：</p><ul><li>扩展函数</li><li>增加兼容逻辑</li><li>增加重试</li><li>把局部实现做得更漂亮</li></ul><p>但如果方向本身是错的，它也会非常高效地陪你把错误方向走得更深。</p><p>这不是 AI 的问题。</p><p>更准确地说，是因为我当时把 AI 放在了“帮我优化现有方案”的位置，却没有先把“现有方案是否值得继续”这个判断做掉。</p><hr><h2 id="六、这两个决定，最后都指向同一件事"><a href="#六、这两个决定，最后都指向同一件事" class="headerlink" title="六、这两个决定，最后都指向同一件事"></a>六、这两个决定，最后都指向同一件事</h2><p>一个决定是砍功能。<br>一个决定是改架构。</p><p>表面上看，它们发生在不同层面。</p><p>但如果把它们放在一起看，本质上其实是同一种纠偏：</p><h3 id="1-不要过早追求“看起来完整”"><a href="#1-不要过早追求“看起来完整”" class="headerlink" title="1. 不要过早追求“看起来完整”"></a>1. 不要过早追求“看起来完整”</h3><p>范围太满，会让产品失焦。<br>架构太满，会让系统失真。</p><h3 id="2-先确认核心路径成立，再扩外围系统"><a href="#2-先确认核心路径成立，再扩外围系统" class="headerlink" title="2. 先确认核心路径成立，再扩外围系统"></a>2. 先确认核心路径成立，再扩外围系统</h3><p>如果核心体验还不稳定，外围能力越多，系统越重。<br>如果执行路径前提就错了，局部优化越多，返工成本越高。</p><h3 id="3-有时候最贵的不是推翻，而是舍不得推翻"><a href="#3-有时候最贵的不是推翻，而是舍不得推翻" class="headerlink" title="3. 有时候最贵的不是推翻，而是舍不得推翻"></a>3. 有时候最贵的不是推翻，而是舍不得推翻</h3><p>真正让我后面轻松很多的，不是前面三个月写过多少，而是终于愿意承认：</p><p><strong>现在停下来重做，代价比继续在错误方向上推进更低。</strong></p><hr><h2 id="七、如果让我重来一次"><a href="#七、如果让我重来一次" class="headerlink" title="七、如果让我重来一次"></a>七、如果让我重来一次</h2><p>如果今天重新开始做 OpsMind，我会更早做三件事：</p><ol><li>先查清这个问题在行业里最常见、最成熟的解法是什么。</li><li>在没有真实用户反馈之前，对“我觉得用户会需要”的功能保持克制。</li><li>把判断标准从“功能多不多”换成“核心路径稳不稳”。</li></ol><p>现在回头看，OpsMind 的 v1.0 范围其实清楚很多了：</p><ul><li>聊天工作台</li><li>知识库</li><li>上传文件后的对话式分析</li><li>基于真实执行结果的图表生成</li></ul><p>没有数据库直连。<br>没有报告导出。<br>也暂时没有技能市场。</p><p>但我反而觉得，它第一次更像一个产品了。</p><hr><p>技术产品的成长，不只是不断往上加能力。</p><p>很多时候，它更依赖三种判断：</p><ul><li>选择先做什么</li><li>选择暂时不做什么</li><li>选择在什么时候承认原来的方向不够对</li></ul><p>这三件事，我在 OpsMind 上各学了一次。</p><p>学费是三个月。</p><p>现在看，付得不算亏。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/two-hard-decisions-scope-architecture-reset/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/two-hard-decisions-scope-architecture-reset/"/>
    <published>2026-04-04T06:45:00.000Z</published>
    <summary>这篇文章记录我在 OpsMind 上做过的两次关键收缩：先砍掉并不属于 v1.0 的范围，再推翻“预猜式”分析架构，把注意力重新拉回核心体验。</summary>
    <title>两个艰难决定：我在 OpsMind 上重做产品范围与核心架构</title>
    <updated>2026-04-04T14:26:55.640Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="LLM工程" scheme="https://bniosfhaiuk.xyz/tags/LLM%E5%B7%A5%E7%A8%8B/"/>
    <category term="架构设计" scheme="https://bniosfhaiuk.xyz/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="上下文管理" scheme="https://bniosfhaiuk.xyz/tags/%E4%B8%8A%E4%B8%8B%E6%96%87%E7%AE%A1%E7%90%86/"/>
    <category term="混合模型" scheme="https://bniosfhaiuk.xyz/tags/%E6%B7%B7%E5%90%88%E6%A8%A1%E5%9E%8B/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/chat-model-trap-agent-architecture.svg" alt="Chat Trap to Agent Architecture Cover"></p><blockquote><p>这篇文章不是在聊某一次优化，而是在聊一个认知转变：从“疯狂堆功能”回到“把主流程做对”，以及我为什么认为这才是一个产品真正成熟的标志。</p></blockquote><hr><h2 id="一、被-Chat-模型骗了很久"><a href="#一、被-Chat-模型骗了很久" class="headerlink" title="一、被 Chat 模型骗了很久"></a>一、被 Chat 模型骗了很久</h2><p>我用的是 DeepSeek，OpenAI 兼容接口，调用方式极其简单。</p><p>简单本身是一个陷阱。</p><p>当你只需要 <code>client.chat.completions.create(model=&quot;deepseek-chat&quot;, messages=messages)</code> 就能让系统“跑起来”，你很容易把这个模型当成一个万能胶：路由分类用它，Agent 规划用它，工具失败反思用它，多步结果综合也用它。</p><p>它确实都能做。每一步单独测试，效果不差。</p><p>但当你把这些步骤真正串成一个 ReAct 循环，问题就开始出现了：</p><ul><li>第 3 轮迭代，模型开始重复调用相同的工具</li><li>第 4 轮，它明明有了结论，却还在向前推</li><li>第 5 轮，它在回答里开始出现上一轮失败工具的幻觉</li></ul><p>这不是模型本身的问题。这是<strong>架构设计的问题</strong>：同一个 Chat 模型被同时用于规划、执行调度和结论生成，没有任何角色分离，没有上下文管理，只有越来越脏的 <code>messages</code> 数组。</p><hr><h2 id="二、真正的问题：上下文污染"><a href="#二、真正的问题：上下文污染" class="headerlink" title="二、真正的问题：上下文污染"></a>二、真正的问题：上下文污染</h2><p>在 OpsMind 的 Agent 实现里，每轮工具调用结束后，结果会原文追加进 <code>messages</code>：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">messages.append(&#123;</span><br><span class="line">    <span class="string">&quot;role&quot;</span>: <span class="string">&quot;tool&quot;</span>,</span><br><span class="line">    <span class="string">&quot;tool_call_id&quot;</span>: tc.<span class="built_in">id</span>,</span><br><span class="line">    <span class="string">&quot;content&quot;</span>: tool_result</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><code>plan_analysis</code> 会返回完整的 <code>chart_plan</code> 和 <code>table_plan</code>，包含所有图表的列映射配置。<br><code>execute_analysis</code> 会返回洞察文本、图表列表、清洗报告。<br><code>get_data_info</code> 会返回所有列的统计信息。</p><p>到第 3 轮迭代，<code>messages</code> 数组里已经有 3 条完整 JSON 工具结果，可能超过 8000 tokens。而模型在生成下一步决策时，需要从这堆原始结构化数据里自己筛选有用信息。</p><p>这不是 Chat 模型擅长的事情。</p><p>更糟糕的是，最初实现的“压缩”只在<strong>超过最大迭代次数之后</strong>才触发。这等于说：模型在整个有效执行期间，都在一个越来越脏的上下文里工作；只有在它已经失控之后，我们才开始清理。</p><p>这是一个典型的<strong>被动防御</strong>设计：等问题暴露再处理，而不是在问题出现之前主动管理。</p><hr><h2 id="三、去看别人怎么做的"><a href="#三、去看别人怎么做的" class="headerlink" title="三、去看别人怎么做的"></a>三、去看别人怎么做的</h2><p>有个机会让我去看了另一个成熟 Agent 系统（Claude Code）的源码。</p><p>它处理上下文污染的方式，不是一个补丁，而是一条有序的<strong>5 层压缩流水线</strong>，在每次 LLM 调用前依序执行：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">1. applyToolResultBudget()    -&gt; 单条 tool result 超 50KB -&gt; 写磁盘，消息里只留 2KB 预览</span><br><span class="line">2. snipCompactIfNeeded()      -&gt; 历史片段裁剪</span><br><span class="line">3. microcompactMessages()     -&gt; 距上次消息 &gt;1小时 -&gt; 清除过期 tool result</span><br><span class="line">4. applyCollapsesIfNeeded()   -&gt; 上下文折叠</span><br><span class="line">5. autocompact()              -&gt; 整体超限时 -&gt; 用轻量模型对旧消息做摘要，替换原始内容</span><br></pre></td></tr></table></figure><p>关键不是每一层的具体实现，而是整体设计思路：<strong>压缩是主动的、逐轮的，不是被动的、超限才触发的。</strong></p><p>它在每次把 <code>messages</code> 送给 LLM 之前，都会清理一遍。不是因为快爆了才清，而是因为干净的上下文是好决策的前提。</p><p>这是一种完全不同的认知：<strong>上下文管理不是兜底机制，而是 Agent 执行的基础设施。</strong></p><hr><h2 id="四、我做了什么改动"><a href="#四、我做了什么改动" class="headerlink" title="四、我做了什么改动"></a>四、我做了什么改动</h2><p>理解了差距之后，我对 <code>agent.py</code> 做了三件事。</p><h3 id="1-角色分离：推理模型-快速模型"><a href="#1-角色分离：推理模型-快速模型" class="headerlink" title="1. 角色分离：推理模型 + 快速模型"></a>1. 角色分离：推理模型 + 快速模型</h3><p>原来两次 LLM 调用全部用 <code>deepseek-chat</code>：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">response = <span class="variable language_">self</span>._get_client().chat.completions.create(</span><br><span class="line">    model=<span class="string">&quot;deepseek-chat&quot;</span>, ...</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">final_response = <span class="variable language_">self</span>._get_client().chat.completions.create(</span><br><span class="line">    model=<span class="string">&quot;deepseek-chat&quot;</span>, ...</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>现在：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="variable language_">self</span>._plan_model = os.getenv(<span class="string">&quot;AGENT_PLAN_MODEL&quot;</span>, <span class="string">&quot;deepseek-reasoner&quot;</span>)</span><br><span class="line"><span class="variable language_">self</span>._fast_model = os.getenv(<span class="string">&quot;AGENT_FAST_MODEL&quot;</span>, <span class="string">&quot;deepseek-chat&quot;</span>)</span><br><span class="line"></span><br><span class="line">response = <span class="variable language_">self</span>._get_client().chat.completions.create(</span><br><span class="line">    model=<span class="variable language_">self</span>._plan_model, ...</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">final_response = <span class="variable language_">self</span>._get_client().chat.completions.create(</span><br><span class="line">    model=<span class="variable language_">self</span>._fast_model, ...</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>规划和文案生成是两种不同的认知任务。<br>前者需要推理能力，后者需要表达效率。混用会让两端都不经济。</p><h3 id="2-每轮主动压缩：-prepare-messages-for-llm"><a href="#2-每轮主动压缩：-prepare-messages-for-llm" class="headerlink" title="2. 每轮主动压缩：_prepare_messages_for_llm()"></a>2. 每轮主动压缩：<code>_prepare_messages_for_llm()</code></h3><p>新增了一个方法，在每次 LLM 调用前执行：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="meta">@staticmethod</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">_prepare_messages_for_llm</span>(<span class="params">messages: <span class="type">List</span>[<span class="type">Dict</span>]</span>) -&gt; <span class="type">List</span>[<span class="type">Dict</span>]:</span><br><span class="line">    COMPRESS_THRESHOLD = <span class="number">1500</span></span><br><span class="line">    KEEP_RECENT = <span class="number">2</span></span><br><span class="line"></span><br><span class="line">    tool_indices = [i <span class="keyword">for</span> i, m <span class="keyword">in</span> <span class="built_in">enumerate</span>(messages) <span class="keyword">if</span> m.get(<span class="string">&quot;role&quot;</span>) == <span class="string">&quot;tool&quot;</span>]</span><br><span class="line">    compress_set = <span class="built_in">set</span>(tool_indices[:-KEEP_RECENT]) <span class="keyword">if</span> <span class="built_in">len</span>(tool_indices) &gt; KEEP_RECENT <span class="keyword">else</span> <span class="built_in">set</span>()</span><br><span class="line"></span><br><span class="line">    result = []</span><br><span class="line">    <span class="keyword">for</span> i, msg <span class="keyword">in</span> <span class="built_in">enumerate</span>(messages):</span><br><span class="line">        <span class="keyword">if</span> i <span class="keyword">in</span> compress_set <span class="keyword">and</span> <span class="built_in">len</span>(msg.get(<span class="string">&quot;content&quot;</span>, <span class="string">&quot;&quot;</span>)) &gt; COMPRESS_THRESHOLD:</span><br><span class="line">            compressed = OpsMindAgent._compress_old_tool_result(msg[<span class="string">&quot;content&quot;</span>])</span><br><span class="line">            result.append(&#123;**msg, <span class="string">&quot;content&quot;</span>: compressed&#125;)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            result.append(msg)</span><br><span class="line">    <span class="keyword">return</span> result</span><br></pre></td></tr></table></figure><p>策略很直接：最近 2 条工具结果保留原文，更早且过长的结果压缩。</p><h3 id="3-精准压缩：-compress-old-tool-result"><a href="#3-精准压缩：-compress-old-tool-result" class="headerlink" title="3. 精准压缩：_compress_old_tool_result()"></a>3. 精准压缩：<code>_compress_old_tool_result()</code></h3><p>压缩不是粗暴截断，而是字段提取：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="meta">@staticmethod</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">_compress_old_tool_result</span>(<span class="params">content: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    data = json.loads(content)</span><br><span class="line"></span><br><span class="line">    keep_keys = &#123;</span><br><span class="line">        <span class="string">&quot;success&quot;</span>, <span class="string">&quot;status&quot;</span>, <span class="string">&quot;error&quot;</span>,</span><br><span class="line">        <span class="string">&quot;rows&quot;</span>, <span class="string">&quot;columns&quot;</span>, <span class="string">&quot;data_shape&quot;</span>,</span><br><span class="line">        <span class="string">&quot;ok_charts&quot;</span>, <span class="string">&quot;skip_charts&quot;</span>, <span class="string">&quot;ok_tables&quot;</span>,</span><br><span class="line">        <span class="string">&quot;charts_generated&quot;</span>, <span class="string">&quot;failed_charts&quot;</span>, <span class="string">&quot;table_types&quot;</span>,</span><br><span class="line">        <span class="string">&quot;mode&quot;</span>, <span class="string">&quot;filename&quot;</span>, <span class="string">&quot;truncated&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line">    summary = &#123;k: v <span class="keyword">for</span> k, v <span class="keyword">in</span> data.items() <span class="keyword">if</span> k <span class="keyword">in</span> keep_keys&#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> text_key <span class="keyword">in</span> (<span class="string">&quot;insight_summary&quot;</span>, <span class="string">&quot;answer&quot;</span>):</span><br><span class="line">        <span class="keyword">if</span> text_key <span class="keyword">in</span> data:</span><br><span class="line">            val = <span class="built_in">str</span>(data[text_key])</span><br><span class="line">            summary[text_key] = val[:<span class="number">200</span>] + <span class="string">&quot;…&quot;</span> <span class="keyword">if</span> <span class="built_in">len</span>(val) &gt; <span class="number">200</span> <span class="keyword">else</span> val</span><br><span class="line"></span><br><span class="line">    summary[<span class="string">&quot;_compressed&quot;</span>] = <span class="literal">True</span></span><br><span class="line">    <span class="keyword">return</span> json.dumps(summary, ensure_ascii=<span class="literal">False</span>)</span><br></pre></td></tr></table></figure><p>保留决策关键字段，删除高冗余体积内容，兼顾可读性与 token 成本。</p><hr><h2 id="五、从“疯狂加功能”到“把主流程做对”"><a href="#五、从“疯狂加功能”到“把主流程做对”" class="headerlink" title="五、从“疯狂加功能”到“把主流程做对”"></a>五、从“疯狂加功能”到“把主流程做对”</h2><p>回头看这半年，OpsMind 一直在加能力：知识库、分析链路、图表推荐、宽表处理、DB 直连、报告生成、Skill 工具……</p><p>每加一层功能，演示效果都更好。<br>但我也在这个过程中欠下一笔债：主流程稳定性在下降，因为地基没有同步加固。</p><p>这次停下来还债，让我更确定一件事：</p><p><strong>功能堆叠解决的是“能不能做”，架构优化解决的是“能不能一直做好”。</strong></p><p>前者适合早期验证，后者决定长期可维护性。<br>一个工程系统真正成熟，往往不是功能最多，而是关键路径最稳。</p><hr><h2 id="六、任务分配与架构设计"><a href="#六、任务分配与架构设计" class="headerlink" title="六、任务分配与架构设计"></a>六、任务分配与架构设计</h2><p>这次改动背后还有一个更深的认知：<strong>模型分工就是任务分配，任务分配就是架构设计。</strong></p><p>把所有工作都交给同一个 Chat 模型，和把所有工作都交给同一个角色，本质是同一个问题：职责失焦。</p><p>推理模型适合处理：</p><ul><li>状态判断</li><li>路径规划</li><li>失败反思</li><li>多步协调</li></ul><p>快速模型适合处理：</p><ul><li>文本生成</li><li>格式化输出</li><li>简单分类</li><li>短回复</li></ul><p>分开之后，每个模型都在自己擅长的位置工作；再配合逐轮压缩，ReAct 循环才真正从“凭运气”切到“可控执行”。</p><p>这次代码改动不算大，但背后的认知转弯很重要。</p><hr><blockquote><p>好的产品不是功能最多的产品。<br>好的 Agent 不是工具最多的 Agent。<br>好的架构不是最复杂的架构。  </p><p>它们都是在对的时间做了对的事情。</p></blockquote>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/chat-model-trap-agent-architecture/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/chat-model-trap-agent-architecture/"/>
    <published>2026-04-02T06:00:00.000Z</published>
    <summary>这次改动的重点不是再加功能，而是把 Agent 主流程做对：角色分离、逐轮压缩、上下文治理，解决 ReAct 循环中的上下文污染。</summary>
    <title>从 Chat 模型陷阱到真正的 Agent 架构：上下文污染与混合模型的工程实践</title>
    <updated>2026-04-02T08:44:26.599Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="JSON Schema" scheme="https://bniosfhaiuk.xyz/tags/JSON-Schema/"/>
    <category term="Skill" scheme="https://bniosfhaiuk.xyz/tags/Skill/"/>
    <category term="OpenAI兼容" scheme="https://bniosfhaiuk.xyz/tags/OpenAI%E5%85%BC%E5%AE%B9/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/skill-standardization-openai-compatible.svg" alt="Skill Standardization Cover"></p><blockquote><p>这篇文章讲的是一次“把已经能用的东西推倒重做”的经历。<br>不是因为原来的坏了，而是因为原来的方式把一个产品想法做窄了。</p></blockquote><hr><h2 id="一、原来是怎么做的"><a href="#一、原来是怎么做的" class="headerlink" title="一、原来是怎么做的"></a>一、原来是怎么做的</h2><p>OpsMind 上线了一套叫 Skill 的快捷分析功能。</p><p>用户打开输入框左下角的「+」菜单，切换到“快捷分析”标签，就能看到四个预置技能：数据体检、数据速览、维度对比、异常排查。点一下，填几个参数，系统自动把描述渲染成一段结构化的分析 prompt 发给 Agent。</p><p>从产品角度来说，这个功能是有价值的：让不会写 prompt 的用户也能得到高质量的分析指令。</p><p>但实现上，它有一个根本性的定位问题：</p><p><strong>Skill 只是用户端的 prompt 模板快捷键，Agent 完全不感知它的存在。</strong></p><p>流程是这样的：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">用户点击 Skill</span><br><span class="line">  -&gt; 前端调 /api/skills/render</span><br><span class="line">  -&gt; 返回渲染好的 prompt 字符串</span><br><span class="line">  -&gt; 以普通用户消息发给 Agent</span><br><span class="line">  -&gt; Agent 不知道这是 Skill，走普通 ReAct 流程</span><br></pre></td></tr></table></figure><p>Agent 拿到的只是一段文字，和用户自己打字没有区别。</p><p>这意味着：Skill 永远不可能被 Agent 主动调用。你没办法说“分析完之后自动触发异常检测”，没办法让 Agent 根据数据特征主动选择合适的 Skill，没办法把 Skill 编排进复杂的多步分析链路。</p><p>Skill 的天花板，是“帮用户写好一句话”。</p><hr><h2 id="二、问题出在哪里"><a href="#二、问题出在哪里" class="headerlink" title="二、问题出在哪里"></a>二、问题出在哪里</h2><p>除了功能局限，原来的格式设计也有工程上的问题。</p><p>每个 Skill 的参数用自研格式定义：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="string">&quot;arguments&quot;</span>: [</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="string">&quot;name&quot;</span>: <span class="string">&quot;dimension&quot;</span>,</span><br><span class="line">        <span class="string">&quot;label&quot;</span>: <span class="string">&quot;对比维度&quot;</span>,</span><br><span class="line">        <span class="string">&quot;placeholder&quot;</span>: <span class="string">&quot;例如：班级、部门&quot;</span>,</span><br><span class="line">        <span class="string">&quot;required&quot;</span>: <span class="literal">True</span>,</span><br><span class="line">    &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>这个格式只服务于一件事：让前端 SkillPicker 渲染一个表单。</p><p>它既不能被 Agent 的 function calling 机制识别，也不能被任何外部框架（LangChain、AutoGen、MCP）复用，还需要在后端和前端分别维护对这套自研结构的解析逻辑。</p><p>可选参数的从句处理更是硬编码的：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">period = args.get(<span class="string">&quot;period&quot;</span>, <span class="string">&quot;&quot;</span>).strip()</span><br><span class="line">args[<span class="string">&quot;period_clause&quot;</span>] = <span class="string">f&quot;（<span class="subst">&#123;period&#125;</span>期间）&quot;</span> <span class="keyword">if</span> period <span class="keyword">else</span> <span class="string">&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">reference = args.get(<span class="string">&quot;reference&quot;</span>, <span class="string">&quot;&quot;</span>).strip()</span><br><span class="line">args[<span class="string">&quot;reference_clause&quot;</span>] = <span class="string">f&quot;，参考基准为<span class="subst">&#123;reference&#125;</span>&quot;</span> <span class="keyword">if</span> reference <span class="keyword">else</span> <span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure><p>每加一个 Skill，就得在 <code>render_prompt()</code> 里加一段这样的逻辑。这不是架构，这是补丁。</p><hr><h2 id="三、标准格式是什么"><a href="#三、标准格式是什么" class="headerlink" title="三、标准格式是什么"></a>三、标准格式是什么</h2><p>OpenAI 在推出 function calling 时定义了一套参数格式，本质上是 JSON Schema：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;object&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;dimension&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;对比维度，如：班级、部门、地区&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;dimension&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这套格式后来被整个行业采纳。Anthropic、Google、LangChain、AutoGen、MCP 都认识这个结构。一个用这种格式定义的工具，理论上可以接入任何兼容 function calling 的框架，不需要任何适配。</p><p>它还有一个官方扩展机制：<code>x-*</code> 前缀字段。这是 JSON Schema 标准允许的自定义扩展，专门用来携带非标准元数据。比如：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;dimension&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;...&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;x-label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;对比维度&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;x-placeholder&quot;</span><span class="punctuation">:</span> <span class="string">&quot;例如：班级、部门&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><code>x-label</code> 和 <code>x-placeholder</code> 是给前端 UI 读的，不影响 LLM 对参数的理解。标准的 LLM 会忽略这些扩展字段，前端可以读取它们来渲染表单。两套需求，一套定义，不冲突。</p><p>可选从句的配置也可以放进扩展字段：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;period&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;时间周期过滤（可选）&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;x-clause-key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;period_clause&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;x-clause-template&quot;</span><span class="punctuation">:</span> <span class="string">&quot;（&#123;value&#125;期间）&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><code>render_prompt()</code> 泛化读取这个配置，对每个带 <code>x-clause-key</code> 的参数自动处理从句。加新 Skill 不需要动 <code>render_prompt()</code> 的代码。</p><hr><h2 id="四、改了什么"><a href="#四、改了什么" class="headerlink" title="四、改了什么"></a>四、改了什么</h2><h3 id="1-Skill-定义格式升级"><a href="#1-Skill-定义格式升级" class="headerlink" title="1. Skill 定义格式升级"></a>1. Skill 定义格式升级</h3><p><code>registry.py</code> 里每个 Skill 的参数字段从 <code>arguments</code> 改成了标准 JSON Schema <code>parameters</code>。</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 之前</span></span><br><span class="line"><span class="string">&quot;arguments&quot;</span>: [</span><br><span class="line">    &#123;<span class="string">&quot;name&quot;</span>: <span class="string">&quot;dimension&quot;</span>, <span class="string">&quot;label&quot;</span>: <span class="string">&quot;对比维度&quot;</span>, <span class="string">&quot;placeholder&quot;</span>: <span class="string">&quot;...&quot;</span>, <span class="string">&quot;required&quot;</span>: <span class="literal">True</span>&#125;,</span><br><span class="line">    &#123;<span class="string">&quot;name&quot;</span>: <span class="string">&quot;period&quot;</span>, <span class="string">&quot;label&quot;</span>: <span class="string">&quot;时间周期（可选）&quot;</span>, <span class="string">&quot;placeholder&quot;</span>: <span class="string">&quot;...&quot;</span>, <span class="string">&quot;required&quot;</span>: <span class="literal">False</span>&#125;,</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 之后</span></span><br><span class="line"><span class="string">&quot;parameters&quot;</span>: &#123;</span><br><span class="line">    <span class="string">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">    <span class="string">&quot;properties&quot;</span>: &#123;</span><br><span class="line">        <span class="string">&quot;dimension&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>,</span><br><span class="line">            <span class="string">&quot;description&quot;</span>: <span class="string">&quot;对比维度（分组字段）&quot;</span>,</span><br><span class="line">            <span class="string">&quot;x-label&quot;</span>: <span class="string">&quot;对比维度&quot;</span>,</span><br><span class="line">            <span class="string">&quot;x-placeholder&quot;</span>: <span class="string">&quot;例如：班级、部门&quot;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;period&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>,</span><br><span class="line">            <span class="string">&quot;description&quot;</span>: <span class="string">&quot;时间周期过滤（可选）&quot;</span>,</span><br><span class="line">            <span class="string">&quot;x-label&quot;</span>: <span class="string">&quot;时间周期（可选）&quot;</span>,</span><br><span class="line">            <span class="string">&quot;x-placeholder&quot;</span>: <span class="string">&quot;例如：2026年Q1&quot;</span>,</span><br><span class="line">            <span class="string">&quot;x-clause-key&quot;</span>: <span class="string">&quot;period_clause&quot;</span>,</span><br><span class="line">            <span class="string">&quot;x-clause-template&quot;</span>: <span class="string">&quot;（&#123;value&#125;期间）&quot;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="string">&quot;required&quot;</span>: [<span class="string">&quot;dimension&quot;</span>, <span class="string">&quot;metric&quot;</span>],</span><br><span class="line">&#125;,</span><br></pre></td></tr></table></figure><h3 id="2-新增-to-tool-schema"><a href="#2-新增-to-tool-schema" class="headerlink" title="2. 新增 to_tool_schema()"></a>2. 新增 <code>to_tool_schema()</code></h3><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">to_tool_schema</span>(<span class="params">skill</span>):</span><br><span class="line">    <span class="keyword">return</span> &#123;</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;function&quot;</span>,</span><br><span class="line">        <span class="string">&quot;function&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;name&quot;</span>: skill[<span class="string">&quot;name&quot;</span>],</span><br><span class="line">            <span class="string">&quot;description&quot;</span>: skill[<span class="string">&quot;description&quot;</span>],</span><br><span class="line">            <span class="string">&quot;parameters&quot;</span>: skill[<span class="string">&quot;parameters&quot;</span>],</span><br><span class="line">        &#125;,</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>这个函数把 Skill 定义直接转成 Agent 可以使用的 tool schema。一行代码，不需要手工维护两套格式。</p><h3 id="3-Agent-TOOLS-动态扩展"><a href="#3-Agent-TOOLS-动态扩展" class="headerlink" title="3. Agent TOOLS 动态扩展"></a>3. Agent TOOLS 动态扩展</h3><p>在 <code>agent.py</code> 的模块加载阶段，从 registry 读取所有 Skill 并追加到 TOOLS 列表：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> src.skills.registry <span class="keyword">import</span> SKILLS <span class="keyword">as</span> _SKILL_DEFS, to_tool_schema <span class="keyword">as</span> _to_tool_schema</span><br><span class="line"></span><br><span class="line">TOOLS.extend([_to_tool_schema(s) <span class="keyword">for</span> s <span class="keyword">in</span> _SKILL_DEFS])</span><br></pre></td></tr></table></figure><p>Agent 启动后，TOOLS 从 6 个扩展到 10 个。LLM 在每轮推理时都能看到这 10 个工具，可以主动选择调用任何一个 Skill。</p><h3 id="4-handle-invoke-skill-端到端执行"><a href="#4-handle-invoke-skill-端到端执行" class="headerlink" title="4. _handle_invoke_skill() 端到端执行"></a>4. <code>_handle_invoke_skill()</code> 端到端执行</h3><p>Agent 调用 Skill 工具时，会触发这个方法：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">_handle_invoke_skill</span>(<span class="params">self, skill_name, args, file_path, ...</span>):</span><br><span class="line">    query = render_prompt(skill_name, args)</span><br><span class="line">    plan = <span class="variable language_">self</span>._handle_plan_analysis(file_path, query, ...)</span><br><span class="line">    <span class="keyword">return</span> <span class="variable language_">self</span>._handle_execute_analysis(file_path, query, plan, ...)</span><br></pre></td></tr></table></figure><p>一次工具调用，完成从 prompt 渲染到图表生成的全链路。</p><h3 id="5-前端-getSkillArgs-派生-UI-参数"><a href="#5-前端-getSkillArgs-派生-UI-参数" class="headerlink" title="5. 前端 getSkillArgs() 派生 UI 参数"></a>5. 前端 <code>getSkillArgs()</code> 派生 UI 参数</h3><p>前端不再维护独立的 <code>SkillArgDef[]</code> 解析逻辑，改为从 JSON Schema 动态派生：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">function</span> <span class="title function_">getSkillArgs</span>(<span class="params"><span class="attr">skill</span>: <span class="title class_">SkillDef</span></span>): <span class="title class_">SkillArgDef</span>[] &#123;</span><br><span class="line">  <span class="keyword">const</span> &#123; properties, required &#125; = skill.<span class="property">parameters</span></span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Object</span>.<span class="title function_">entries</span>(properties).<span class="title function_">map</span>(<span class="function">(<span class="params">[name, prop]</span>) =&gt;</span> (&#123;</span><br><span class="line">    name,</span><br><span class="line">    <span class="attr">label</span>: prop[<span class="string">&#x27;x-label&#x27;</span>] ?? prop.<span class="property">description</span>,</span><br><span class="line">    <span class="attr">placeholder</span>: prop[<span class="string">&#x27;x-placeholder&#x27;</span>] ?? <span class="string">&#x27;&#x27;</span>,</span><br><span class="line">    <span class="attr">required</span>: required.<span class="title function_">includes</span>(name),</span><br><span class="line">  &#125;))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>SkillPicker 的渲染逻辑、表单验证、自动触发全部不变，只是数据来源从 <code>skill.arguments</code> 换成了 <code>getSkillArgs(skill)</code>。</p><hr><h2 id="五、现在是什么效果"><a href="#五、现在是什么效果" class="headerlink" title="五、现在是什么效果"></a>五、现在是什么效果</h2><p><strong>用户端</strong>：体验完全不变。SkillPicker 表单长得一样，填的参数一样，触发行为一样。</p><p><strong>Agent 端</strong>：完全不同。现在 Agent 可以主动调用 Skill：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">用户：“帮我看看哪个部门表现异常”</span><br><span class="line">Agent 推理后调用: anomaly-detective(target=&quot;哪个部门表现异常&quot;)</span><br><span class="line">-&gt; 自动完成 plan_analysis + execute_analysis</span><br><span class="line">-&gt; 输出排序图 + 异常分析文字</span><br></pre></td></tr></table></figure><p>用户不需要打开 SkillPicker，不需要点击，不需要填表单。Agent 自己判断该用哪个 Skill，自己填参数，自己跑完整个分析流程。</p><p><strong>扩展性</strong>：加新 Skill 只需要在 <code>registry.py</code> 里加一个 dict。Agent 侧自动获得新工具，前端自动渲染新表单，<code>render_prompt()</code> 不需要改。</p><p><strong>框架兼容性</strong>：现在的 Skill 格式可以直接被任何支持 function calling 的框架识别，不需要任何适配层。如果未来接入 LangChain、AutoGen 或 MCP，Skill 定义可以原样复用。</p><hr><h2 id="六、一个工程判断"><a href="#六、一个工程判断" class="headerlink" title="六、一个工程判断"></a>六、一个工程判断</h2><p>这次改动有一个决策值得记录：<strong>什么时候该坚持自研格式，什么时候该跟标准走。</strong></p><p>原来的 <code>arguments</code> 格式不是错的，它在当时的需求范围内完全够用。它更简单，更易读，定义起来更快。</p><p>但它是私有的。私有格式的代价是：每个消费这份数据的地方都要了解这套格式，任何框架对接都需要先写适配层，任何扩展都需要修改解析代码。</p><p>JSON Schema + <code>x-*</code> 扩展字段不是我发明的。它是一个有完整规范、有大量工具支持、有明确演进路径的标准。选择标准的代价是短期多写几行代码，收益是长期的可接入性和可组合性。</p><p>对于 Skill 这种面向 LLM 和外部框架的结构，跟标准走是对的。</p><hr><p><em>本文记录的改动发生在 2026 年 4 月。核心文件：<code>src/skills/registry.py</code>、<code>src/agent.py</code>、<code>frontend/lib/skills.ts</code>、<code>frontend/components/chat/SkillPicker.tsx</code>。</em></p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/skill-standardization-openai-compatible/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/skill-standardization-openai-compatible/"/>
    <published>2026-04-02T05:30:00.000Z</published>
    <summary>这次改动不是修坏功能，而是把 Skill 从“前端模板快捷键”升级为 Agent 可感知、可调用、可复用的标准化能力。</summary>
    <title>Skill 标准化：从私有格式到 OpenAI 兼容的工程决策</title>
    <updated>2026-04-02T08:43:36.952Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="SSE" scheme="https://bniosfhaiuk.xyz/tags/SSE/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="架构设计" scheme="https://bniosfhaiuk.xyz/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="可靠性" scheme="https://bniosfhaiuk.xyz/tags/%E5%8F%AF%E9%9D%A0%E6%80%A7/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/agent-orchestration-four-fixes.svg" alt="Agent Orchestration Fixes Cover"></p><p>这篇记录的不是“又修了几个点”，而是一次编排层审查之后，把系统里几处“靠运气撑住”的位置，逐一改成工程性保障的过程。</p><p>系统在大部分演示场景里一直能跑通，但只要把视角从“能跑”切到“复杂场景能稳定复现”，问题就会非常清楚：<br>很多失败不是功能缺失，而是结构设计在边界条件下不够稳。</p><p>这次我落地了四项修复。</p><hr><h2 id="一、修复-1：max-iterations-6-在数据库路径上是零容错"><a href="#一、修复-1：max-iterations-6-在数据库路径上是零容错" class="headerlink" title="一、修复 1：max_iterations=6 在数据库路径上是零容错"></a>一、修复 1：<code>max_iterations=6</code> 在数据库路径上是零容错</h2><p>之前 ReAct Agent 的上限是 <code>max_iterations = 6</code>。<br>这个值在简单路径可用，但数据库分析的标准链路是：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">get_db_schema -&gt; execute_sql -&gt; get_data_info -&gt; plan_analysis -&gt; execute_analysis</span><br></pre></td></tr></table></figure><p>5 次工具调用刚好贴着上限走，任意一步重试都会触发迭代截止。<br>一旦触发上限，Agent 会进入压缩输出，导致最终回答只有结论，没有可靠图表与分析细节。</p><h3 id="调整"><a href="#调整" class="headerlink" title="调整"></a>调整</h3><ol><li>上限从 <code>6</code> 提到 <code>10</code>，给复杂路径和一次重试留空间。  </li><li>保留总超时预算（<code>AGENT_TOTAL_TIMEOUT_SECONDS</code>）作为真正的安全网。  </li><li>在 system prompt 中明确：<code>execute_sql</code> 成功后可直接进入 <code>plan_analysis</code>，避免不必要的 <code>get_data_info</code> 兜圈。</li></ol><p>这一步做完后，数据库分析路径从“勉强刚好”变成“可容错可恢复”。</p><hr><h2 id="二、修复-2：取消-chart-plan-的-LLM-透传，改为-plan-id"><a href="#二、修复-2：取消-chart-plan-的-LLM-透传，改为-plan-id" class="headerlink" title="二、修复 2：取消 chart_plan 的 LLM 透传，改为 plan_id"></a>二、修复 2：取消 <code>chart_plan</code> 的 LLM 透传，改为 <code>plan_id</code></h2><p>原先流程是 <code>plan_analysis</code> 产出完整 <code>chart_plan/table_plan</code>，再由 LLM 原样传给 <code>execute_analysis</code>。<br>问题在于：这等于把复杂 JSON 的完整性寄托在 LLM 的序列化行为上。</p><p>常见失真包括：</p><ul><li>字段被精简（例如丢掉 <code>status</code>）</li><li>键名被改写（<code>x_col</code> 变成 <code>x_axis</code>）</li><li>嵌套 JSON 二次转义</li></ul><p>这些都不是语义层 bug，而是“传输层不可靠”。</p><h3 id="调整-1"><a href="#调整-1" class="headerlink" title="调整"></a>调整</h3><p><code>plan_analysis</code> 只返回一个短 <code>plan_id</code>，完整 plan 存在服务端内存：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">plan_id = uuid.uuid4().<span class="built_in">hex</span>[:<span class="number">8</span>]</span><br><span class="line"><span class="variable language_">self</span>._session_plans[plan_id] = &#123;</span><br><span class="line">    <span class="string">&quot;chart_plan&quot;</span>: result.get(<span class="string">&quot;chart_plan&quot;</span>, &#123;&#125;),</span><br><span class="line">    <span class="string">&quot;table_plan&quot;</span>: result.get(<span class="string">&quot;table_plan&quot;</span>, &#123;&#125;),</span><br><span class="line">    <span class="string">&quot;file_path&quot;</span>: file_path,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>execute_analysis</code> 只收 <code>plan_id</code>，在服务端取回原始 plan：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">stored = <span class="variable language_">self</span>._session_plans.get(plan_id)</span><br><span class="line">chart_plan = stored[<span class="string">&quot;chart_plan&quot;</span>]</span><br><span class="line">table_plan = stored[<span class="string">&quot;table_plan&quot;</span>]</span><br></pre></td></tr></table></figure><p>这一步的本质是：把“跨步骤数据完整性”从模型层搬回服务端强约束层。</p><hr><h2 id="三、修复-3：新增-refine-chart，让图表调整从“全量重跑”改为“局部修订”"><a href="#三、修复-3：新增-refine-chart，让图表调整从“全量重跑”改为“局部修订”" class="headerlink" title="三、修复 3：新增 refine_chart，让图表调整从“全量重跑”改为“局部修订”"></a>三、修复 3：新增 <code>refine_chart</code>，让图表调整从“全量重跑”改为“局部修订”</h2><p>过去用户说“把第一张图改成折线图”，系统会重新 <code>plan_analysis + execute_analysis</code> 全链路重跑。<br>这对单图修改来说成本太高，也容易引入无关波动。</p><h3 id="调整-2"><a href="#调整-2" class="headerlink" title="调整"></a>调整</h3><p>新增工具：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">refine_chart(plan_id, chart_type, new_chart_type?, new_column_mapping?)</span><br></pre></td></tr></table></figure><p>能力边界：</p><ul><li>局部改图类型</li><li>局部改列映射</li><li>原地更新同一个 <code>plan_id</code></li><li>再调用 <code>execute_analysis</code> 增量生效</li></ul><p>收益很直接：<br>“改一张图”不再触发一次完整重规划，延迟和 token 成本都显著下降。</p><hr><h2 id="四、修复-4：SSE-增加心跳帧，解决长分析链路下的空闲断连"><a href="#四、修复-4：SSE-增加心跳帧，解决长分析链路下的空闲断连" class="headerlink" title="四、修复 4：SSE 增加心跳帧，解决长分析链路下的空闲断连"></a>四、修复 4：SSE 增加心跳帧，解决长分析链路下的空闲断连</h2><p>分析链路超过 30s 时，如果 SSE 长时间无帧，代理层或浏览器会按空闲连接处理并主动断开。<br>本地开发不明显，上线后在反向代理链路中会稳定复现。</p><h3 id="调整-3"><a href="#调整-3" class="headerlink" title="调整"></a>调整</h3><p>在事件生成器里使用“超时等待 + 注释心跳帧”：</p><figure class="highlight python"><table><tr><td class="code"><pre><span class="line">_HEARTBEAT_INTERVAL = <span class="number">15.0</span></span><br><span class="line"><span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        item = <span class="keyword">await</span> asyncio.wait_for(queue.get(), timeout=_HEARTBEAT_INTERVAL)</span><br><span class="line">    <span class="keyword">except</span> asyncio.TimeoutError:</span><br><span class="line">        <span class="keyword">yield</span> <span class="string">&quot;: heartbeat\n\n&quot;</span></span><br><span class="line">        <span class="keyword">continue</span></span><br></pre></td></tr></table></figure><p><code>:</code> 开头是 SSE 注释帧，不会触发前端业务消息处理，但能持续保活连接。</p><hr><h2 id="五、改动文件一览"><a href="#五、改动文件一览" class="headerlink" title="五、改动文件一览"></a>五、改动文件一览</h2><table><thead><tr><th>文件</th><th>关键改动</th></tr></thead><tbody><tr><td><code>src/agent.py</code></td><td><code>max_iterations</code> 调整；<code>_session_plans</code> 引入；<code>plan_id</code> 传递；<code>refine_chart</code> 工具与 handler；prompt 更新</td></tr><tr><td><code>src/api/routers/chat.py</code></td><td>SSE generator 改为带超时等待；超时发送 heartbeat 注释帧</td></tr></tbody></table><hr><h2 id="六、这四项修复背后的共同方向"><a href="#六、这四项修复背后的共同方向" class="headerlink" title="六、这四项修复背后的共同方向"></a>六、这四项修复背后的共同方向</h2><p>它们表面上是四个点，底层其实是同一件事：</p><p>把“靠模型自觉、靠路径刚好、靠网络运气”的环节，改成“可验证、可恢复、可预期”的工程路径。</p><p>对 Agent 产品来说，最难的往往不是“让它做出结果”，而是“在真实复杂场景下持续做对结果”。<br>这次四项修复，本质上就是朝这个方向补齐地基。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/agent-orchestration-four-fixes/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/agent-orchestration-four-fixes/"/>
    <published>2026-04-02T05:00:00.000Z</published>
    <summary>这次修的不是零散 bug，而是 Agent 编排层里四个结构性问题：迭代上限、plan 透传、局部图表修订路径、SSE 长连接保活。</summary>
    <title>Agent 编排四项修复：从结构性问题到工程落地</title>
    <updated>2026-04-02T03:26:12.355Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="RAG" scheme="https://bniosfhaiuk.xyz/tags/RAG/"/>
    <category term="LLM" scheme="https://bniosfhaiuk.xyz/tags/LLM/"/>
    <category term="知识库" scheme="https://bniosfhaiuk.xyz/tags/%E7%9F%A5%E8%AF%86%E5%BA%93/"/>
    <category term="技术思考" scheme="https://bniosfhaiuk.xyz/tags/%E6%8A%80%E6%9C%AF%E6%80%9D%E8%80%83/"/>
    <category term="论文阅读" scheme="https://bniosfhaiuk.xyz/tags/%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB/"/>
    <category term="Provenance" scheme="https://bniosfhaiuk.xyz/tags/Provenance/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/rag-paper-reading-library-card.svg" alt="RAG Reading Note Cover"></p><p>最近重新读了一遍 Patrick Lewis 等人的 RAG 论文。<br>如果把传统大模型比作一个“靠脑内记忆答题”的学生，那 RAG 最有意思的设计，就是给这个学生办了一张“图书馆借阅证”：它可以在生成答案时，先去查资料，再组织表达。</p><p>这篇笔记想记下我最有感触的六点。</p><h2 id="1-记忆被拆成了两部分：参数负责能力，外部库负责事实"><a href="#1-记忆被拆成了两部分：参数负责能力，外部库负责事实" class="headerlink" title="1. 记忆被拆成了两部分：参数负责能力，外部库负责事实"></a>1. 记忆被拆成了两部分：参数负责能力，外部库负责事实</h2><p>论文把知识来源拆成两类：</p><ul><li>参数化记忆（Parametric Memory）：模型参数中沉淀的模式与能力</li><li>非参数化记忆（Non-parametric Memory）：外部文档库（论文里是 Wikipedia 索引）</li></ul><p>这件事改变了我对“模型变强”的直觉。<br>以前我会把“更强”理解成“参数更大”；现在我更倾向于“能力与事实分层管理”。</p><p>一个参数规模更小的模型，如果检索机制足够好，也能在知识密集任务上打出很高上限。<br>这也是我理解里 RAG 的核心：把“推理能力”和“事实更新”从同一套参数里拆开。</p><h2 id="2-DPR-的优雅在于：把“找资料”变成了几何问题"><a href="#2-DPR-的优雅在于：把“找资料”变成了几何问题" class="headerlink" title="2. DPR 的优雅在于：把“找资料”变成了几何问题"></a>2. DPR 的优雅在于：把“找资料”变成了几何问题</h2><p>RAG 里我最喜欢的一段是 DPR 检索器。<br>问题 <code>x</code> 和文档 <code>z</code> 都被编码到同一向量空间，再通过相似度做 top-K 检索。</p><p>形式上可以写成：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">top-K argmax_z ( q(x)^T d(z) )</span><br></pre></td></tr></table></figure><p>这一步非常关键，因为它让检索不再依赖“字面重合”。<br>同义改写、表达差异、问法变化，都可以通过语义邻近来弥补。</p><p>当然，传统 BM25 在实体词非常明确时依然有价值。<br>所以在工程上，我更愿意把它理解为“语义检索主导，关键词检索兜底”的组合，而不是非此即彼。</p><h2 id="3-RAG-Sequence-与-RAG-Token：稳定性和灵活性的取舍"><a href="#3-RAG-Sequence-与-RAG-Token：稳定性和灵活性的取舍" class="headerlink" title="3. RAG-Sequence 与 RAG-Token：稳定性和灵活性的取舍"></a>3. RAG-Sequence 与 RAG-Token：稳定性和灵活性的取舍</h2><p>论文里两种生成方式非常有代表性：</p><ul><li>RAG-Sequence：整段生成过程使用同一批检索文档</li><li>RAG-Token：每个 token 允许动态参考不同文档</li></ul><p>前者更稳，后者更灵活。<br>我自己的体会是：结构化问答、强一致性场景更适合 Sequence；开放生成、长文本探索更适合 Token 思路。</p><p>这其实不是“谁更先进”的问题，而是系统目标不同：你优先要“上下文一致”，还是优先要“信息覆盖”。</p><h2 id="4-最打动我的细节：没有直接证据时，RAG-仍可能答对"><a href="#4-最打动我的细节：没有直接证据时，RAG-仍可能答对" class="headerlink" title="4. 最打动我的细节：没有直接证据时，RAG 仍可能答对"></a>4. 最打动我的细节：没有直接证据时，RAG 仍可能答对</h2><p>论文里提到一个有意思的现象：即使检索文档里没有直接答案，RAG 也有一部分样本可以回答正确（文中给出的案例约为 11.8%）。<br>这意味着系统内部不是“照抄检索结果”，而是“检索信号 + 参数记忆”在共同工作。</p><p>我把它理解成一种协同：</p><ul><li>外部文档提供线索和锚点</li><li>参数记忆补上推理与表达</li></ul><p>这也解释了为什么高质量检索会显著提升答案可用性，但“只堆文档”并不能自动得到高质量推理。</p><h2 id="5-为什么今天很多-RAG-看起来“不微调也能用”"><a href="#5-为什么今天很多-RAG-看起来“不微调也能用”" class="headerlink" title="5. 为什么今天很多 RAG 看起来“不微调也能用”"></a>5. 为什么今天很多 RAG 看起来“不微调也能用”</h2><p>这也是我最近反复在想的问题。<br>和论文发表时期相比，现在主流大模型的指令遵循能力强了很多，所以“检索结果 + 好提示词”就能得到可用答案。</p><p>但这不代表联合微调思想失效。<br>在垂直领域（术语密集、格式严格、流程固定）里，微调仍然是把特定行为写进模型“习惯”的有效手段；RAG 则负责把外部知识保持新鲜与可追溯。</p><p>我的结论是：两者不是替代关系，而是不同层面的增强。</p><h2 id="6-最后的收获：RAG-让模型学会了“有依据地不知道”"><a href="#6-最后的收获：RAG-让模型学会了“有依据地不知道”" class="headerlink" title="6. 最后的收获：RAG 让模型学会了“有依据地不知道”"></a>6. 最后的收获：RAG 让模型学会了“有依据地不知道”</h2><p>读完这篇论文，我最强的感受不是某个指标，而是方法论层面的变化：<br>系统不再追求“把所有事实都塞进参数”，而是承认知识会变化，并把“查证能力”纳入生成流程。</p><p>这种可追溯性（Provenance）很重要。<br>它不仅减少幻觉，也让我们在使用模型时，拥有了更多可验证、可审计、可迭代的控制点。</p><hr><p>参考论文：<br>Patrick Lewis et al., <em>Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks</em><br>ArXiv: <a href="https://arxiv.org/abs/2005.11401">2005.11401</a></p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/notes/rag-paper-reading-library-card/</id>
    <link href="https://bniosfhaiuk.xyz/notes/rag-paper-reading-library-card/"/>
    <published>2026-03-30T22:40:00.000Z</published>
    <summary>重读 Patrick Lewis 等人的 RAG 奠基论文后，我更确定：RAG 的价值不只是“补知识”，而是把参数记忆与外部事实解耦，让模型在可追溯与可更新之间取得平衡。</summary>
    <title>当模型拿到“借阅证”：重读 RAG 奠基论文的六点体会</title>
    <updated>2026-03-31T06:36:26.982Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="工程协作" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8D%8F%E4%BD%9C/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="项目管理" scheme="https://bniosfhaiuk.xyz/tags/%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86/"/>
    <category term="复盘" scheme="https://bniosfhaiuk.xyz/tags/%E5%A4%8D%E7%9B%98/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/subagent-prd-delivery.svg" alt="Subagent PRD Delivery Cover"></p><p>这次更新不是“修几个 bug”或者“补几个页面”。</p><p>它更像一次真正意义上的产品级推进：</p><p>从一份 PRD 出发，把登录、多用户、数据库连接、知识库管理、信息架构、报告导出，一路做到代码、接口、前端页面、测试和回归验证。</p><p>更重要的是，这次工作不是靠单线程硬扛，而是通过一支 <code>subagent</code> 团队并行推进，再由主线程统一集成、收敛和验收。</p><h2 id="为什么这次可以叫“超级巨大更新”"><a href="#为什么这次可以叫“超级巨大更新”" class="headerlink" title="为什么这次可以叫“超级巨大更新”"></a>为什么这次可以叫“超级巨大更新”</h2><p>如果只看表面，OpsMind 以前已经能聊天、能分析文件、能查知识库，也能接数据库。</p><p>但从产品角度看，它还停留在一种很典型的“能力原型阶段”：</p><ul><li>有能力，但没有真正的登录和用户隔离</li><li>有聊天页，但没有完整的信息架构</li><li>有知识库，但管理能力还不够产品化</li><li>有数据库能力，但还不够像正式数据源模块</li><li>有分析结果，但没有真正可交付的报告导出</li></ul><p>所以这次更新做的，不是把一个 Demo 修得更顺。</p><p>而是把一个原型产品往“更接近正式版本的状态”拉了一步。</p><p>一句话总结就是：</p><p><strong>这次不是能力补丁，而是产品形态升级。</strong></p><h2 id="这次到底更新了什么"><a href="#这次到底更新了什么" class="headerlink" title="这次到底更新了什么"></a>这次到底更新了什么</h2><p>这轮改动最重要的，不是某一个功能点，而是几个基础模块一起补齐了。</p><h3 id="1-登录与多用户系统终于完整了"><a href="#1-登录与多用户系统终于完整了" class="headerlink" title="1. 登录与多用户系统终于完整了"></a>1. 登录与多用户系统终于完整了</h3><p>这次补上的不只是一个登录页，而是真正的身份基础设施：</p><ul><li>邮箱 + 密码登录</li><li>JWT 鉴权</li><li>当前用户信息接口</li><li>修改密码</li><li>多用户 session 隔离</li><li>默认管理员初始化</li></ul><p>这意味着很多以前只是“单机假象”的能力，终于有了可靠的用户边界。</p><p>用户 A 看不到用户 B 的会话，未登录也不能直接闯进工作区。</p><p>而数据库连接、知识库、设置页、报告导出这些功能，终于开始拥有真正成立的前提。</p><h3 id="2-单页聊天工作台被拉成了多页面产品结构"><a href="#2-单页聊天工作台被拉成了多页面产品结构" class="headerlink" title="2. 单页聊天工作台被拉成了多页面产品结构"></a>2. 单页聊天工作台被拉成了多页面产品结构</h3><p>这次前端不再只是一个 <code>/chat</code> 单页，而是拆成了更清晰的产品信息架构：</p><ul><li><code>/login</code></li><li><code>/chat</code></li><li><code>/data-sources</code></li><li><code>/knowledge</code></li><li><code>/settings</code></li></ul><p>侧边栏也补成了正式一级导航：</p><ul><li>聊天工作台</li><li>数据源</li><li>知识库</li><li>系统设置</li></ul><p>这一步的意义很直接：</p><p>文件上传和数据库连接不再挤在聊天页里，知识库也不再只是一个临时面板。</p><p>整个产品终于更像一个工作台，而不是一页里塞满能力入口的实验区。</p><h3 id="3-数据源模块开始像一个正式产品功能"><a href="#3-数据源模块开始像一个正式产品功能" class="headerlink" title="3. 数据源模块开始像一个正式产品功能"></a>3. 数据源模块开始像一个正式产品功能</h3><p>数据库连接这次做的，不只是“能连上”。</p><p>它被补成了一个更完整的数据源体验：</p><ul><li>MySQL &#x2F; SQLite 两种类型支持</li><li>测试连接</li><li>保存连接</li><li>删除连接</li><li>大结果导出卡片展示</li><li>导出结果继续进入分析流程</li></ul><p>同时，数据库查询结果卡片也补齐了更清晰的动作提示：</p><ul><li>预览前 10 行</li><li>继续说“分析这份数据”</li><li>继续说“画图”</li></ul><p>也就是说，数据库不再只是一个孤立接口，而是开始真正接入整个分析工作流。</p><h3 id="4-企业知识库从“上传问答”升级成了“可管理模块”"><a href="#4-企业知识库从“上传问答”升级成了“可管理模块”" class="headerlink" title="4. 企业知识库从“上传问答”升级成了“可管理模块”"></a>4. 企业知识库从“上传问答”升级成了“可管理模块”</h3><p>这次知识库模块的增强非常明显。</p><p>它不再只是上传文档然后做 RAG，而是补上了真正接近产品化的管理能力：</p><ul><li>文档列表</li><li>分类</li><li>标签</li><li>状态</li><li>分块预览</li><li>元数据编辑</li><li>删除文档</li><li>系统内置知识库 &#x2F; 企业私有知识库区分</li></ul><p>更关键的是，检索优先级也更像企业真实场景：</p><ul><li>企业私有知识优先</li><li>系统内置知识作为补充</li></ul><p>这一步很重要。</p><p>因为企业智能助手最怕的事情之一，就是拿“平台通用知识”覆盖“企业真实规则”。</p><h3 id="5-报告导出能力正式落地"><a href="#5-报告导出能力正式落地" class="headerlink" title="5. 报告导出能力正式落地"></a>5. 报告导出能力正式落地</h3><p>这次还补上了报告导出链路：</p><ul><li>会话报告预览</li><li>Markdown 导出</li><li>PDF 导出</li><li>图表与表格内容进入导出结构</li><li>数据来源写入报告</li></ul><p>这意味着 OpsMind 的结果不再只停留在聊天窗口里，而开始拥有“可以交付”的形态。</p><p>这不只是方便保存。</p><p>它让产品从“分析助手”更进一步靠近了“分析交付工具”。</p><h3 id="6-测试和兼容性也一起补了"><a href="#6-测试和兼容性也一起补了" class="headerlink" title="6. 测试和兼容性也一起补了"></a>6. 测试和兼容性也一起补了</h3><p>这轮更新不是只改功能，不做验收。</p><p>我把接口、主流程、前端构建和历史兼容一起做了回归：</p><ul><li>API 测试补齐到鉴权新契约</li><li>会话与主流程相关测试修通</li><li>知识库相关测试通过</li><li>前端构建通过</li><li>历史测试夹具补齐</li><li>旧参数和旧路径做了必要兼容</li></ul><p>这点很关键。</p><p>因为 PRD 级任务最怕的，不是代码量大，而是“看起来功能很多，结果一回归全炸”。</p><h2 id="为什么这次适合用-subagent-团队来做"><a href="#为什么这次适合用-subagent-团队来做" class="headerlink" title="为什么这次适合用 subagent 团队来做"></a>为什么这次适合用 subagent 团队来做</h2><p>这类任务最难的地方，从来不是某个函数怎么写。</p><p>而是：</p><ul><li>PRD 很长</li><li>牵涉前后端</li><li>同时涉及数据模型、接口、页面和测试</li><li>还必须尽量不打断已有功能</li></ul><p>如果用单线程方式推进，很容易出现三种问题：</p><ol><li>看完 PRD 之后还没开工，时间已经先耗掉一大截</li><li>做完后端再做前端，联调时发现前面设计得返工</li><li>某个模块写得很深，但整体集成和回归来不及做</li></ol><p>这次 <code>subagent</code> 团队的价值，就在这里非常明显地体现出来了。</p><p>大致的并行方式是这样的：</p><ul><li>一条线专门读 PRD，提炼阶段需求和依赖</li><li>一条线盘点后端现状和改造点</li><li>一条线盘点前端现状和页面拆分路径</li><li>后面再拆成鉴权、多页面前端、报告 &#x2F; 知识库增强几个并行 worker</li><li>主线程负责共享模型、主流程、测试和最终集成</li></ul><p>这类分工的好处不是“炫技式并行”。</p><p>而是：</p><p><strong>让复杂任务可以同时向多个方向推进，但最后仍然收敛回一个可交付结果。</strong></p><p>这比单纯追求写代码速度，更接近真正的软件工程协作。</p><h2 id="这次最像“PRD-级开发任务”的地方是什么"><a href="#这次最像“PRD-级开发任务”的地方是什么" class="headerlink" title="这次最像“PRD 级开发任务”的地方是什么"></a>这次最像“PRD 级开发任务”的地方是什么</h2><p>我觉得不是功能多，而是下面三件事同时成立。</p><h3 id="1-从需求文档直接推到交付结果"><a href="#1-从需求文档直接推到交付结果" class="headerlink" title="1. 从需求文档直接推到交付结果"></a>1. 从需求文档直接推到交付结果</h3><p>这次不是“先写点功能，再看像不像 PRD”。</p><p>而是从 PRD 阶段目标往下推：</p><ul><li>数据模型要不要扩</li><li>路由怎么接</li><li>前端页面怎么拆</li><li>用户体验怎么闭环</li><li>哪些功能要兼容</li><li>哪些测试必须补</li></ul><p>这就是典型的 PRD 驱动开发，而不是零散功能开发。</p><h3 id="2-不是只做-happy-path"><a href="#2-不是只做-happy-path" class="headerlink" title="2. 不是只做 happy path"></a>2. 不是只做 happy path</h3><p>如果只是做一个 happy path，很多东西很容易“看起来可用”。</p><p>但 PRD 级任务真正难的是这些问题：</p><ul><li>登录过期怎么办</li><li>用户权限怎么隔离</li><li>大结果怎么处理</li><li>缺配置时怎么兜底</li><li>老测试怎么迁移</li><li>旧路径怎么兼容</li></ul><p>这些东西决定的不是 Demo 能不能跑，而是系统能不能稳。</p><h3 id="3-集成、回归和说明也属于交付的一部分"><a href="#3-集成、回归和说明也属于交付的一部分" class="headerlink" title="3. 集成、回归和说明也属于交付的一部分"></a>3. 集成、回归和说明也属于交付的一部分</h3><p>很多任务做到最后，只交代码，不交解释。</p><p>但真正的产品推进不是这样。</p><p>这次我很看重的一点就是：</p><ul><li>代码改了什么</li><li>路由变了什么</li><li>页面入口在哪</li><li>怎么登录</li><li>怎么体验</li><li>测试覆盖到什么程度</li></ul><p>这些都应该一起说清楚。</p><p>因为“完成任务”不等于“别人已经能接手和使用”。</p><h2 id="现在可以怎么体验这次更新"><a href="#现在可以怎么体验这次更新" class="headerlink" title="现在可以怎么体验这次更新"></a>现在可以怎么体验这次更新</h2><p>如果你想最快看出这次更新的价值，我会推荐下面这个顺序。</p><h3 id="路线-A：先看产品结构"><a href="#路线-A：先看产品结构" class="headerlink" title="路线 A：先看产品结构"></a>路线 A：先看产品结构</h3><p>先登录，再切换这几个页面：</p><ul><li><code>/chat</code></li><li><code>/data-sources</code></li><li><code>/knowledge</code></li><li><code>/settings</code></li></ul><p>最值得看的，是工作区是否已经更像正式的信息架构，以及侧边栏导航是否自然。</p><h3 id="路线-B：再看数据源链路"><a href="#路线-B：再看数据源链路" class="headerlink" title="路线 B：再看数据源链路"></a>路线 B：再看数据源链路</h3><p>去 <code>/data-sources</code>：</p><ul><li>上传一个 CSV &#x2F; Excel</li><li>配一个 SQLite 或 MySQL 连接</li><li>测试连接</li><li>回到聊天页发起分析</li></ul><p>重点观察文件数据和数据库数据，是否都能顺畅进入分析链路。</p><h3 id="路线-C：再看知识库管理"><a href="#路线-C：再看知识库管理" class="headerlink" title="路线 C：再看知识库管理"></a>路线 C：再看知识库管理</h3><p>去 <code>/knowledge</code>：</p><ul><li>上传几份制度文档</li><li>设分类和标签</li><li>打开文档详情看 chunk</li><li>回到聊天页问规则类问题</li></ul><p>这里最值得看的，是知识库是否已经从“上传入口”变成了“可管理模块”。</p><h3 id="路线-D：最后看报告导出"><a href="#路线-D：最后看报告导出" class="headerlink" title="路线 D：最后看报告导出"></a>路线 D：最后看报告导出</h3><p>在 <code>/chat</code> 中完成一轮分析后：</p><ul><li>点击顶部导出按钮</li><li>导出 Markdown</li><li>再尝试导出 PDF</li></ul><p>重点看报告是否已经包含摘要、正文、图表引用、表格和数据来源。</p><p>如果没有在 <code>.env</code> 里显式配置管理员账号，当前开发默认管理员是：</p><ul><li>邮箱：<code>admin@opsmind.local</code></li><li>密码：<code>admin123456</code></li></ul><h2 id="还有什么没有做"><a href="#还有什么没有做" class="headerlink" title="还有什么没有做"></a>还有什么没有做</h2><p>这次我按任务要求明确没有做部署阶段，也就是：</p><ul><li>Docker</li><li>docker-compose</li><li>nginx 生产编排</li><li>整套部署文档</li></ul><p>也就是说，这次交付的重点是：</p><p><strong>把产品功能和工程主链路做完整，而不是先做部署包装。</strong></p><p>我其实觉得这个顺序是对的。</p><p>因为部署标准化应该建立在功能稳定之后，而不是反过来。</p><h2 id="我对这次更新的一个核心判断"><a href="#我对这次更新的一个核心判断" class="headerlink" title="我对这次更新的一个核心判断"></a>我对这次更新的一个核心判断</h2><p>如果只看代码量，这次更新当然很大。</p><p>但我觉得真正值得记录的，不是“改了多少文件”，而是：</p><p><strong>subagent 已经不只是适合做局部功能或修 bug，它开始适合解决真正的 PRD 级任务。</strong></p><p>前提是三件事：</p><ul><li>任务边界清楚</li><li>并行分工合理</li><li>主线程愿意负责最终集成和验收</li></ul><p>一旦这三件事成立，<code>subagent</code> 的价值就不再只是“帮忙写代码”。</p><p>它会变成另一种更有意思的东西：</p><ul><li>帮你并行读文档</li><li>帮你并行摸现状</li><li>帮你分模块推进</li><li>最后再一起收敛成交付结果</li></ul><p>这次更新让我更确信一件事：</p><p><strong>未来最有价值的，不是一个超强的单体 agent，而是一支能被组织起来、能被约束边界、能被统一验收的 agent 团队。</strong></p><p>而这次 OpsMind 的这轮更新，就是一次很具体的证明。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/subagent-prd-delivery/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/subagent-prd-delivery/"/>
    <published>2026-03-24T23:20:46.000Z</published>
    <summary>记录一次真正意义上的产品级推进：从一份 PRD 出发，把登录、多用户、数据库连接、知识库管理、信息架构和报告导出一路做到代码、页面、测试与回归验证。</summary>
    <title>超级巨大更新：subagent 帮我解决了 PRD 级别的开发任务</title>
    <updated>2026-04-03T03:00:45.907Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="方法论" scheme="https://bniosfhaiuk.xyz/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="架构设计" scheme="https://bniosfhaiuk.xyz/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="数据预处理" scheme="https://bniosfhaiuk.xyz/tags/%E6%95%B0%E6%8D%AE%E9%A2%84%E5%A4%84%E7%90%86/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/intelligent-preprocess-agent-orchestration.svg" alt="Intelligent Preprocess Agent Orchestration Cover"></p><p>这篇文章想复盘的，不只是一次预处理能力升级。</p><p>我更想讨论一个更底层的问题：</p><p>为什么很多团队一边说“让 AI 提效”，一边又被 AI 拖慢节奏？</p><p>我现在的答案很直接：</p><p><strong>不是 AI 不够强，而是任务边界和编排逻辑先天就没讲清楚。</strong></p><h2 id="这次升级真正解决的，不是“多识别几个脏表”"><a href="#这次升级真正解决的，不是“多识别几个脏表”" class="headerlink" title="这次升级真正解决的，不是“多识别几个脏表”"></a>这次升级真正解决的，不是“多识别几个脏表”</h2><p>做数据分析助手时，预处理很容易被理解成一个局部功能。</p><p>比如：</p><ul><li>识别宽表</li><li>删除合计行</li><li>处理特殊缺失值</li><li>规范列名里的单位</li></ul><p>表面看，这些都像小事。</p><p>但一旦系统进入 Agent 链路，问题就不再只是“识别到了没有”，而会立刻升级成另一组系统问题：</p><ul><li>这件事由谁判断</li><li>判断完什么时候生效</li><li>是自动应用还是先让用户确认</li><li>用户确认前，后续分析到底能不能继续</li><li>下一轮分析是否要沿用这次的处理结果</li></ul><p>也就是说，预处理一旦进入 Agent 模式，它就不再只是一个数据清洗函数。</p><p><strong>它变成了一个有状态的分支节点。</strong></p><p>而这次升级，本质上是在把这个分支节点从“能跑”改造成“可解释、可确认、可复用、可回滚”。</p><h2 id="这条链路其实经历了三次明显的改版"><a href="#这条链路其实经历了三次明显的改版" class="headerlink" title="这条链路其实经历了三次明显的改版"></a>这条链路其实经历了三次明显的改版</h2><p>如果把这次演化过程拆开看，大致可以分成三步。</p><h3 id="版本一：规则检测器-本地-fix"><a href="#版本一：规则检测器-本地-fix" class="headerlink" title="版本一：规则检测器 + 本地 fix"></a>版本一：规则检测器 + 本地 fix</h3><p>最早的思路其实很工程化：</p><ul><li>写一组规则检测器</li><li>识别宽表、表头偏移、合计行、重复列名等问题</li><li>命中后调用确定性 fix 函数</li></ul><p>这套思路的优点很明显：</p><ul><li>可控</li><li>可测</li><li>低幻觉</li><li>出错时容易定位</li></ul><p>但它的上限也很明显。</p><p>规则检测器擅长识别“格式像什么”，却不擅长判断“在当前任务下该不该处理”。</p><p>比如：</p><ul><li>宽表一定要转长表吗</li><li>列名里的单位应该自动提取，还是只做提示</li><li>表头偏移是元数据行，还是业务说明的一部分</li></ul><p>这时候，纯规则系统开始显得僵硬。</p><h3 id="版本二：规则检测-LLM-评估-试做复核"><a href="#版本二：规则检测-LLM-评估-试做复核" class="headerlink" title="版本二：规则检测 + LLM 评估 + 试做复核"></a>版本二：规则检测 + LLM 评估 + 试做复核</h3><p>于是第二版开始让 LLM 参与决策。</p><p>流程变成了：</p><ul><li>先用规则检测器找候选问题</li><li>再让 LLM 评估每个问题是 <code>auto / ask / inform / skip</code></li><li>对部分问题先试做一版</li><li>再用一次 LLM 复核试做结果是否可靠</li></ul><p>这一版已经比最早期强很多。</p><p>它开始具备一种真正有产品意义的能力：</p><p><strong>把结构问题翻译成产品动作。</strong></p><p>也就是：</p><ul><li>哪些问题系统可以直接处理</li><li>哪些问题必须打断用户确认</li><li>哪些问题只需要提示，不要自动动数据</li></ul><p>但这版仍然有两个核心缺点。</p><p>第一，链路太绕。</p><p>识别一次、评估一次、试做一次、复核一次，成本很高。</p><p>第二，状态语义不够干净。</p><p>最危险的问题是：</p><p><strong>待确认 fix 和已确认 fix 的边界不够硬。</strong></p><p>如果 <code>pending</code> 也会被后续分析偷偷吃进去，那用户眼里看到的是“待确认”，系统内部却已经把它当成“已生效”。</p><p>这种系统最容易出现“看似可交互，实则状态不可信”的问题。</p><h3 id="版本三：单次-LLM-诊断-JSON-Schema-约束-确定性执行"><a href="#版本三：单次-LLM-诊断-JSON-Schema-约束-确定性执行" class="headerlink" title="版本三：单次 LLM 诊断 + JSON Schema 约束 + 确定性执行"></a>版本三：单次 LLM 诊断 + JSON Schema 约束 + 确定性执行</h3><p>这次升级最终落地的，是第三版思路：</p><ul><li>不再把 LLM 当成自由发挥的清洗器</li><li>让它只负责识别问题、生成受约束参数、决定处理级别</li><li>所有输出都必须落在 <code>issue_type + params + decision + confidence</code> 这个结构里</li><li>真正动数据的，仍然是确定性的 fix 函数</li></ul><p>也就是说：</p><p><strong>LLM 负责判断，代码负责执行。</strong></p><p>这看上去像一句很朴素的话。</p><p>但它其实是整次升级里最关键的边界。</p><p>因为只有这样，系统才能同时得到两种好处：</p><ul><li>保留 LLM 的语义判断能力</li><li>保留工程系统的确定性和可验证性</li></ul><h2 id="这次升级真正落下来的技术元素是什么"><a href="#这次升级真正落下来的技术元素是什么" class="headerlink" title="这次升级真正落下来的技术元素是什么"></a>这次升级真正落下来的技术元素是什么</h2><p>如果只讲“要把逻辑理清”，这篇文章会显得很空。</p><p>真正让这次升级落地的，其实是一组非常具体的技术元素。</p><p>我最后把整条链路收敛成了下面这个结构：</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">User Query</span><br><span class="line">   ↓</span><br><span class="line">Preprocessing Branch</span><br><span class="line">   ├─ build_df_context(df)</span><br><span class="line">   ├─ llm_diagnose_and_evaluate()</span><br><span class="line">   ├─ schema / 引用 / dry-run 校验</span><br><span class="line">   ├─ 状态分流：auto / ask / inform</span><br><span class="line">   └─ 写入 session state</span><br><span class="line">            ↓</span><br><span class="line">Confirmed Fixes Only</span><br><span class="line">            ↓</span><br><span class="line">Analysis Artifact</span><br><span class="line">   ├─ preprocess_fingerprint</span><br><span class="line">   ├─ plan_analysis()</span><br><span class="line">   └─ execute_analysis()</span><br></pre></td></tr></table></figure><p>这套结构里，重要的不是“多了一层”。</p><p>而是每一层都只做一件事。</p><h3 id="1-用-Schema-把-LLM-从“自由文本生成器”收敛成“受约束诊断器”"><a href="#1-用-Schema-把-LLM-从“自由文本生成器”收敛成“受约束诊断器”" class="headerlink" title="1. 用 Schema 把 LLM 从“自由文本生成器”收敛成“受约束诊断器”"></a>1. 用 Schema 把 LLM 从“自由文本生成器”收敛成“受约束诊断器”</h3><p>这次预处理升级最核心的技术动作，不是换了哪个 prompt。</p><p>而是把模型输出强行压进一套稳定契约：</p><ul><li><code>issue_type</code></li><li><code>decision</code></li><li><code>params</code></li><li><code>confidence</code></li><li><code>user_message</code></li></ul><p>再加上 JSON Schema 和列名、行号、dry-run 校验。</p><p>这样做的意义是：</p><p><strong>模型可以判断，但不能随意发明系统里不存在的动作。</strong></p><p>它不再有机会输出一个后端根本接不住的“聪明答案”。</p><h3 id="2-用确定性-fixer-保住系统的可测试性"><a href="#2-用确定性-fixer-保住系统的可测试性" class="headerlink" title="2. 用确定性 fixer 保住系统的可测试性"></a>2. 用确定性 fixer 保住系统的可测试性</h3><p>我没有让 LLM 直接“修改 DataFrame”。</p><p>真正执行数据变换的，仍然是硬编码的 fix 函数。</p><p>比如：</p><ul><li>删合计行</li><li>宽表转长表</li><li>特殊缺失值归一</li><li>转置恢复</li></ul><p>这样做的好处非常工程化：</p><ul><li>可单测</li><li>可回放</li><li>可 debug</li><li>可解释</li></ul><p>这也是为什么我一直强调：</p><p><strong>LLM 负责决定“要不要做”，代码负责决定“怎么做”。</strong></p><h3 id="3-用显式状态机替代隐式约定"><a href="#3-用显式状态机替代隐式约定" class="headerlink" title="3. 用显式状态机替代隐式约定"></a>3. 用显式状态机替代隐式约定</h3><p>这次升级里我最重视的，其实不是识别率。</p><p>而是状态机。</p><p>如果没有显式状态，系统就会开始依赖默认约定。</p><p>而默认约定往往是最危险的东西。</p><p>现在这条链路里，至少有三类状态是清楚的：</p><ul><li>confirmed fixes</li><li>pending confirmations</li><li>suggested fixes</li></ul><p>这三类状态分别对应不同的产品动作和不同的数据流权限。</p><p>一旦状态机被写进代码，很多以前看起来“容易说不清”的事，都会自动变清楚。</p><h3 id="4-用-preprocess-fingerprint-保证-plan-和-execute-看的是同一版数据"><a href="#4-用-preprocess-fingerprint-保证-plan-和-execute-看的是同一版数据" class="headerlink" title="4. 用 preprocess_fingerprint 保证 plan 和 execute 看的是同一版数据"></a>4. 用 <code>preprocess_fingerprint</code> 保证 <code>plan</code> 和 <code>execute</code> 看的是同一版数据</h3><p>这是一个非常典型的架构细节。</p><p>如果 <code>plan_analysis()</code> 和 <code>execute_analysis()</code> 各自重新读文件、重新做预处理，那么系统理论上随时可能出现：</p><ul><li>规划阶段基于版本 A</li><li>执行阶段基于版本 B</li></ul><p>这种问题不会天天发生。</p><p>但一旦发生，用户看到的就是最难排查的“偶发错乱”。</p><p>所以我补了共享 artifact 解析路径，并把关键输入折叠成 <code>preprocess_fingerprint</code>。</p><p>这件事的价值不是性能优化，而是：</p><p><strong>把“同一份数据版本”从默认假设变成显式约束。</strong></p><h3 id="5-用兼容归一化把系统从“切换新协议”改成“平滑迁移”"><a href="#5-用兼容归一化把系统从“切换新协议”改成“平滑迁移”" class="headerlink" title="5. 用兼容归一化把系统从“切换新协议”改成“平滑迁移”"></a>5. 用兼容归一化把系统从“切换新协议”改成“平滑迁移”</h3><p>预处理 issue 契约升级以后，一个很现实的问题是：</p><ul><li>流式 SSE 已经开始发新字段</li><li>历史消息里还存着旧字段</li></ul><p>如果不做兼容层，用户刷新一次页面，系统就会像换了一个脑子。</p><p>所以这次还有一个不太显眼、但非常关键的技术动作：</p><p><strong>把历史 payload 统一归一化成同一套 issue 结构。</strong></p><p>很多系统并不是死在新功能做不出来。</p><p>而是死在“新旧世界并存时没有过渡层”。</p><h3 id="6-用“真脏表工厂”而不是纯-mock-测试预处理"><a href="#6-用“真脏表工厂”而不是纯-mock-测试预处理" class="headerlink" title="6. 用“真脏表工厂”而不是纯 mock 测试预处理"></a>6. 用“真脏表工厂”而不是纯 mock 测试预处理</h3><p>这次我后来专门补了一层真实场景回归。</p><p>它不是只 mock 一些 issue payload。</p><p>而是会真实生成：</p><ul><li>宽表 + 合计行的 CSV</li><li>列名带单位 + 特殊缺失值的 CSV</li><li>表头偏移的 XLSX</li><li>转置后的 CSV</li></ul><p>然后走真实读盘、真实 <code>prepare()</code>、真实 fix 重放。</p><p>这层测试的意义很大。</p><p>因为预处理最怕的一件事，就是：</p><p><strong>你以为自己在测真实表格，其实只是在测一组自己定义的数据结构。</strong></p><h2 id="真正难的不是识别问题，而是把状态归属讲清楚"><a href="#真正难的不是识别问题，而是把状态归属讲清楚" class="headerlink" title="真正难的不是识别问题，而是把状态归属讲清楚"></a>真正难的不是识别问题，而是把状态归属讲清楚</h2><p>如果只看功能表，预处理升级像是在做“识别能力增强”。</p><p>但真正决定系统质量的，反而不是识别本身，而是状态怎么流动。</p><p>这次我最后理顺的，是下面这几个边界。</p><p>如果再往上抽一层，这其实就是几条很典型的架构原则：</p><ul><li>契约先于实现</li><li>显式状态优于隐式约定</li><li>单一职责优于混合责任</li><li>confirmed data path 必须和 proposed data path 分离</li></ul><p>这些原则听起来很抽象。</p><p>但一旦落到预处理链里，它们就会变成非常具体的系统规则。</p><h3 id="1-auto、ask、inform-分别属于什么状态"><a href="#1-auto、ask、inform-分别属于什么状态" class="headerlink" title="1. auto、ask、inform 分别属于什么状态"></a>1. <code>auto</code>、<code>ask</code>、<code>inform</code> 分别属于什么状态</h3><p>这三个词如果只存在于 prompt 里，系统很快就会乱。</p><p>必须把它们落成真正的状态语义：</p><ul><li><code>auto</code>：直接进入 confirmed fixes</li><li><code>ask</code>：进入 pending confirmations，阻塞后续分析</li><li><code>inform</code>：进入 suggested fixes，不阻塞，只有用户点 <code>apply</code> 才进入 confirmed</li></ul><p>这件事一旦讲清楚，前后端动作也自然就清楚了：</p><ul><li><code>confirm</code></li><li><code>revert</code></li><li><code>apply</code></li></ul><p>如果这层不先定义，AI 很容易写出“能交互但逻辑互相打架”的实现。</p><h3 id="2-待确认-fix-不能参与正式分析"><a href="#2-待确认-fix-不能参与正式分析" class="headerlink" title="2. 待确认 fix 不能参与正式分析"></a>2. 待确认 fix 不能参与正式分析</h3><p>这是这次升级里最需要强行纠偏的一点。</p><p>在旧链路里，<code>pending</code> 有机会被一起重放到数据引擎里。</p><p>这意味着：</p><ul><li>UI 说“请确认”</li><li>后端却已经拿它去做正式分析</li></ul><p>这是很典型的“提示词语义”和“系统语义”不一致。</p><p>所以我把规则改得非常硬：</p><p><strong>只有 confirmed fixes 才能进入分析链。</strong></p><p>待确认 fix 只存在于卡片和 session state 里，不得偷偷生效。</p><h3 id="3-历史消息和流式消息必须共享同一套-issue-契约"><a href="#3-历史消息和流式消息必须共享同一套-issue-契约" class="headerlink" title="3. 历史消息和流式消息必须共享同一套 issue 契约"></a>3. 历史消息和流式消息必须共享同一套 issue 契约</h3><p>这一点也很容易被忽略。</p><p>如果实时 SSE 发的是一套字段：</p><ul><li><code>issue_type</code></li><li><code>user_message</code></li><li><code>params</code></li><li><code>confidence</code></li></ul><p>但历史消息里存的还是旧字段：</p><ul><li><code>type</code></li><li><code>summary</code></li><li><code>fix_params</code></li><li><code>llm_confidence</code></li></ul><p>那用户刷新一次页面，就会得到两套世界观。</p><p>所以这次我做了两件事：</p><ul><li>前端切到新字段契约</li><li>后端在读历史消息时做兼容归一化</li></ul><p>这不是“兼容旧数据”这么简单。</p><p>这是在保证：</p><p><strong>系统对同一个状态的解释，在时间维度上保持一致。</strong></p><h3 id="4-同一份文件不能在-plan-和-execute-阶段重复做两次预处理"><a href="#4-同一份文件不能在-plan-和-execute-阶段重复做两次预处理" class="headerlink" title="4. 同一份文件不能在 plan 和 execute 阶段重复做两次预处理"></a>4. 同一份文件不能在 <code>plan</code> 和 <code>execute</code> 阶段重复做两次预处理</h3><p>这也是典型的 Agent 编排问题。</p><p>如果 <code>plan_analysis()</code> 和 <code>execute_analysis()</code> 各自重新加载文件、重新预处理一次，就会带来两个风险：</p><ul><li>重复计算</li><li>计划阶段和执行阶段看到的不是同一份数据版本</li></ul><p>所以这次我在数据引擎里补了共享 artifact 解析路径。</p><p>它会把：</p><ul><li>原始文件</li><li>当前 preprocess config</li><li>当前 query</li></ul><p>共同折叠成一个可复用的预处理产物。</p><p>再配合 <code>preprocess_fingerprint</code>，就能让 <code>plan</code> 和 <code>execute</code> 至少知道自己看到的是不是同一版数据。</p><p>这类设计不是 AI 自己会“顺手想到”的。</p><p>它必须先由人把系统不变量讲清楚。</p><h2 id="为什么-Agent-编排还得人来做"><a href="#为什么-Agent-编排还得人来做" class="headerlink" title="为什么 Agent 编排还得人来做"></a>为什么 Agent 编排还得人来做</h2><p>这次升级之后，我反而更确定了一件事：</p><p><strong>Agent 编排最难的部分，不是写代码，而是定义责任。</strong></p><p>AI 很擅长做这些事：</p><ul><li>按明确枚举补 schema</li><li>按确定接口补 API</li><li>写 fix 函数</li><li>改前端 action 流</li><li>扩测试覆盖</li></ul><p>但 AI 不擅长替你做这些决定：</p><ul><li>哪些状态应该长期保存</li><li>哪些状态只是一次性建议</li><li>哪些动作会阻塞后续分析</li><li>哪些结果必须显式失败</li><li>哪些中间态可以缓存，哪些绝不能缓存</li></ul><p>换句话说：</p><p><strong>AI 擅长实现边界，人的工作是先画出边界。</strong></p><p>如果边界没画清楚，就会出现一种很常见的低效：</p><ul><li>你让 AI “优化预处理”</li><li>它帮你补了一堆代码</li><li>但 pending 和 confirmed 继续混着用</li><li>前端 action 和后端语义继续错位</li><li>测试越写越多，逻辑反而越乱</li></ul><p>这时候不是 AI 没有产出。</p><p>而是它产出的是一堆建立在错误前提上的高质量代码。</p><p>高质量地做错事，往往比低质量地做错事更危险。</p><h2 id="为什么含糊不清的任务会拖慢-AI，而不是加速-AI"><a href="#为什么含糊不清的任务会拖慢-AI，而不是加速-AI" class="headerlink" title="为什么含糊不清的任务会拖慢 AI，而不是加速 AI"></a>为什么含糊不清的任务会拖慢 AI，而不是加速 AI</h2><p>这是我这次最大的感受。</p><p>很多人以为，给 AI 一个宽泛任务，能换来“更大的自主性”。</p><p>比如：</p><ul><li>“把预处理做智能一点”</li><li>“把 Agent 编排优化一下”</li><li>“让流程更自然”</li></ul><p>这类话听起来像方向，实际上对工程实现几乎没有约束力。</p><p>当任务含糊时，AI 往往会发生三件事：</p><p>第一，它会自动脑补需求。</p><p>第二，它会把局部最优当成系统最优。</p><p>第三，它会在没有明确状态机的前提下，把不同语义揉在一起。</p><p>最后你得到的，不是“高效自动化”。</p><p>而是：</p><ul><li>一轮又一轮返工</li><li>一层又一层兼容补丁</li><li>越来越重的上下文负担</li></ul><p>所以真正高效的做法，从来不是一句“你自己发挥”。</p><p>而是先把这些问题想清楚：</p><ol><li>系统里有哪些状态</li><li>每个状态由谁产生</li><li>每个状态何时生效</li><li>哪些状态允许跨轮复用</li><li>哪些状态绝对不能默默流入后续分析</li></ol><p>当这五个问题清楚以后，AI 的效率会突然变高。</p><p>因为它终于不需要替你猜系统设计了。</p><p>它只需要帮你实现系统设计。</p><p>这也是我现在越来越认同的一句话：</p><p><strong>Agent 编排不是提示词工程，首先是系统设计。</strong></p><p>提示词只是其中一层。</p><p>真正的骨架，是：</p><ul><li>状态机</li><li>协议</li><li>缓存</li><li>幂等</li><li>回归样本</li></ul><p>这些东西不先定下来，AI 越强，返工速度反而越快。</p><h2 id="这次升级后，我对“AI-提效”的理解反而更保守了"><a href="#这次升级后，我对“AI-提效”的理解反而更保守了" class="headerlink" title="这次升级后，我对“AI 提效”的理解反而更保守了"></a>这次升级后，我对“AI 提效”的理解反而更保守了</h2><p>以前我也会更乐观地想：</p><p>只要模型够强、上下文够长、工具够多，系统自然会越来越聪明。</p><p>但这次做完以后，我的结论反而更保守，也更实用：</p><p><strong>AI 不是替代架构思考，而是放大架构思考。</strong></p><p>如果你的逻辑是清楚的，AI 会把效率放大很多。</p><p>如果你的逻辑是含糊的，AI 也会把这种含糊放大很多。</p><p>所以真正的提效顺序应该是：</p><ol><li>人先把状态、边界、责任讲清楚</li><li>把枚举、schema、动作协议和测试目标定清楚</li><li>再把这些明确任务交给 AI 并行推进</li></ol><p>这样 AI 才是加速器。</p><p>否则，它更像一个会高速放大歧义的执行器。</p><h2 id="写给所有正在用-AI-做工程的人"><a href="#写给所有正在用-AI-做工程的人" class="headerlink" title="写给所有正在用 AI 做工程的人"></a>写给所有正在用 AI 做工程的人</h2><p>如果你想让 AI 真正帮你提效，我现在最推荐的不是“学更多 prompt 技巧”。</p><p>而是先学会做三件事。</p><h3 id="1-把问题写成状态机，而不是写成愿望"><a href="#1-把问题写成状态机，而不是写成愿望" class="headerlink" title="1. 把问题写成状态机，而不是写成愿望"></a>1. 把问题写成状态机，而不是写成愿望</h3><p>不要说“让体验更顺”。</p><p>要说：</p><ul><li><code>ask</code> 阻塞</li><li><code>inform</code> 不阻塞</li><li><code>confirm</code> 进入 confirmed</li><li><code>revert</code> 从 pending 移除</li></ul><p>只有这样，AI 写出来的东西才会稳。</p><h3 id="2-把能力写成受约束的契约，而不是模糊描述"><a href="#2-把能力写成受约束的契约，而不是模糊描述" class="headerlink" title="2. 把能力写成受约束的契约，而不是模糊描述"></a>2. 把能力写成受约束的契约，而不是模糊描述</h3><p>不要说“让模型返回结构化结果”。</p><p>要说：</p><ul><li><code>issue_type</code></li><li><code>params</code></li><li><code>decision</code></li><li><code>confidence</code></li></ul><p>再配上 schema、校验器和 deterministic executor。</p><p>这时候模型才是在你的系统里工作，而不是在系统外自由发挥。</p><p>如果再往技术上说得更硬一点，这一步至少应该包括：</p><ul><li>明确枚举</li><li>参数 schema</li><li>引用校验</li><li>dry-run 验证</li><li>失败回退策略</li></ul><p>只有把这些层补齐，模型输出才真正配叫“工程输入”。</p><h3 id="3-先定义回归样本，再谈智能升级"><a href="#3-先定义回归样本，再谈智能升级" class="headerlink" title="3. 先定义回归样本，再谈智能升级"></a>3. 先定义回归样本，再谈智能升级</h3><p>这次我专门补了一组“真脏表工厂”测试，而不是只写 mock。</p><p>因为现实世界里的表格问题，从来不是抽象的。</p><p>它们往往就是：</p><ul><li>宽表加合计行</li><li>单位混在列名里</li><li>Excel 头两行是说明文字</li><li>一整张表是转置过来的</li></ul><p>如果没有这类真实样本，任何“智能预处理”都很容易只是在 demo 上显得聪明。</p><p>更进一步说，真正高价值的测试不是“函数能跑”。</p><p>而是：</p><ul><li>一份真实脏表进入系统</li><li>一组状态在链路里流转</li><li>最终只有该生效的 fix 生效</li></ul><p>这才是 Agent 系统真正该验证的东西。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这次智能预处理升级，表面上是在重构一个数据清洗分支。</p><p>但对我来说，它更像是一次对 Agent 工程方法的重新确认：</p><p><strong>真正决定效率的，不是你把多少任务交给 AI，而是你在交给 AI 之前，到底有没有先把逻辑理清。</strong></p><p>AI 当然能写很多代码。</p><p>但能不能把这些代码变成稳定、可信、可演化的系统，最后仍然取决于：</p><ul><li>你有没有先想清楚架构</li><li>你有没有先讲清楚边界</li><li>你有没有把模糊任务压缩成可验证任务</li></ul><p>当这些前提成立时，AI 确实会非常快。</p><p>当这些前提不成立时，AI 不会救你。</p><p>它只会更快地把混乱实现出来。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/intelligent-preprocess-agent-orchestration/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/intelligent-preprocess-agent-orchestration/"/>
    <published>2026-03-24T19:26:08.000Z</published>
    <summary>结合 OpsMind 的升级实践，复盘我对智能预处理和 Agent 编排的理解：真正决定 AI 提效的，不是多写几个 prompt，而是先把状态、边界和执行逻辑讲清楚。</summary>
    <title>智能预处理与 Agent 编排：先把逻辑讲清楚，再让 AI 真正提速</title>
    <updated>2026-04-03T03:00:45.906Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="方法论" scheme="https://bniosfhaiuk.xyz/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="工程化" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8C%96/"/>
    <category term="架构设计" scheme="https://bniosfhaiuk.xyz/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="RAG" scheme="https://bniosfhaiuk.xyz/tags/RAG/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/agent-orchestration-evolution.svg" alt="Agent Orchestration Evolution Cover"></p><p>这篇文章想复盘的，不是某一个 bug 的修复过程。</p><p>我更想讨论一个更底层的问题：</p><p>当一个企业智能助手同时要处理聊天、知识问答、数据分析和混合查询时，Agent 编排层到底应该承担什么责任？</p><p>在这次升级之前，OpsMind 的很多链路其实已经“能跑”了。</p><p>它能回答问题。</p><p>它能调用工具。</p><p>它能画图。</p><p>它也能勉强处理一些混合流程。</p><p>但我后来越来越确定：</p><p><strong>能跑和能持续演化之间，隔着一整层架构能力。</strong></p><h2 id="为什么这次升级不是“修几个-bug”那么简单"><a href="#为什么这次升级不是“修几个-bug”那么简单" class="headerlink" title="为什么这次升级不是“修几个 bug”那么简单"></a>为什么这次升级不是“修几个 bug”那么简单</h2><p>最容易让人产生错觉的一件事，就是系统看起来已经能工作。</p><p>可当我重新审视这套 Agent 编排时，看到的不是几个孤立问题，而是一组高度相关的系统性信号：</p><ul><li>Router 已经识别出的能力，在 Workflow 层被静默吞掉</li><li>工具执行阶段产出的状态，没有进入跨轮可复用记忆</li><li>某些失败没有显式暴露，只伪装成“看似成功但内容空洞”</li><li>某些请求链路最危险的问题不是报错，而是无期限挂起</li></ul><p>如果拆开看，每个都像普通缺陷。</p><p>但合起来看，它们指向的是同一个事实：</p><p><strong>Agent 编排不是把模型和工具连起来就结束了，它本质上是在维护一个有状态、有预算、有退路的执行系统。</strong></p><h2 id="我重新定义了-Agent-编排层的职责"><a href="#我重新定义了-Agent-编排层的职责" class="headerlink" title="我重新定义了 Agent 编排层的职责"></a>我重新定义了 Agent 编排层的职责</h2><p>在更早的实现里，编排层更像“调用顺序的胶水层”：</p><ul><li>Router 决定 <code>primary intent</code></li><li>Workflow 按 intent 选分支</li><li>Agent 在需要时做 ReAct</li><li>Tool 返回结果，模型再做总结</li></ul><p>这样的结构当然能跑。</p><p>但它有一个很大的隐患：</p><p><strong>编排层只知道“下一步调用谁”，却没有真正管理“当前系统处于什么状态”。</strong></p><p>这次升级之后，我更倾向于把 Agent 编排层看成六类责任的组合：</p><ol><li>路由解释责任：Router 给出的不是命令，而是建议</li><li>状态承接责任：一轮分析里产出的关键状态要能跨轮复用</li><li>执行预算责任：调用多久、循环多久、整条链路多久，都要显式管理</li><li>失败显性化责任：真失败就应该返回失败，而不是伪装成空结果</li><li>路径选择责任：混合请求不该被硬编码顺序静默压平</li><li>语义保真责任：知识库、上下文和计划都要以语义友好的方式保存</li></ol><p>这六个职责，基本也对应了这次升级里最关键的六个问题。</p><h2 id="六个问题，其实对应六个编排原则"><a href="#六个问题，其实对应六个编排原则" class="headerlink" title="六个问题，其实对应六个编排原则"></a>六个问题，其实对应六个编排原则</h2><h3 id="1-多轮数据分析无记忆"><a href="#1-多轮数据分析无记忆" class="headerlink" title="1. 多轮数据分析无记忆"></a>1. 多轮数据分析无记忆</h3><p>原来的系统里，如果用户接着上一轮说：</p><ul><li>“换成折线图”</li><li>“沿用上次分析”</li><li>“加上环比”</li></ul><p>Agent 其实并不知道“上次分析”具体是什么。</p><p>因为会话历史里虽然保留了自然语言回答，但真正关键的分析状态并没有被当作资产保存下来，比如：</p><ul><li><code>chart_plan</code></li><li><code>table_plan</code></li><li>列映射</li><li>数据形状</li><li>领域与分析逻辑</li></ul><p>这件事暴露了一个很核心的原则：</p><p><strong>在 Agent 系统里，可复用的不是上一轮说过的话，而是上一轮形成的结构化执行状态。</strong></p><p>所以这次我做的不是“给 Agent 更多聊天历史”。</p><p>而是把最近一次分析配置从执行结果里透传出来，保存进会话消息 <code>metadata</code>，再在下一轮进入 Agent 时注入成一条 system memory。</p><p>这样系统获得的就不是“对话连续”，而是“任务连续”。</p><h3 id="2-agent-decides-被静默吞掉"><a href="#2-agent-decides-被静默吞掉" class="headerlink" title="2. agent_decides 被静默吞掉"></a>2. <code>agent_decides</code> 被静默吞掉</h3><p>Router 其实已经能识别出某些混合请求不是固定顺序的。</p><p>比如：</p><ul><li>先看数据再核规则</li><li>先查规则再看数据</li><li>顺序不明，交给 Agent 自主判断</li></ul><p>问题在于，如果 Workflow 只支持固定分支，那么 <code>agent_decides</code> 这种信号就会在中途被压平成某个硬编码路径。</p><p>这说明一件很典型的事：</p><p><strong>如果 Router 给出的能力不能被下游消费，Router 越聪明，系统的“假智能”就越重。</strong></p><p>所以这次我把原则收得很明确：</p><ul><li>显式顺序，交给显式编排</li><li>不确定顺序，交给 Agent ReAct</li></ul><p>这不是多写一个 <code>if</code> 那么简单。</p><p>它意味着编排系统必须承认一件事：</p><p><strong>某些问题的最优执行顺序，只有在运行期才会被确定。</strong></p><h3 id="3-Router-confidence-原来几乎没意义"><a href="#3-Router-confidence-原来几乎没意义" class="headerlink" title="3. Router confidence 原来几乎没意义"></a>3. Router confidence 原来几乎没意义</h3><p>如果一个带 <code>confidence</code> 的路由结果，不管是 <code>0.95</code> 还是 <code>0.50</code> 都走一样的路径，那这个字段本质上就只是日志装饰。</p><p>这次我没有把它做成一套复杂的交互式追问。</p><p>我先做了最保守也最实用的一步：</p><p>当模型给出低置信 <code>CHAT</code>，但当前会话里已经有文件，query 又明显带数据词时，就自动把它从 <code>CHAT</code> 降级成 <code>DATA</code>。</p><p>为什么先做这个？</p><p>因为真实系统里最危险的低置信误判，不是“错了一点点”，而是：</p><p><strong>它把请求送进了一个能力明显不足的分支。</strong></p><p>所以我现在越来越认同，confidence 的作用不一定是让系统更复杂。</p><p>它首先应该让系统更保守。</p><h3 id="4-execute-analysis-空-plan-静默成功"><a href="#4-execute-analysis-空-plan-静默成功" class="headerlink" title="4. execute_analysis 空 plan 静默成功"></a>4. <code>execute_analysis</code> 空 plan 静默成功</h3><p>这是一个非常典型的工程问题：</p><ul><li><code>plan_analysis</code> 生成了空图表计划</li><li><code>execute_analysis</code> 没报错</li><li>后面的 LLM 依然吐出一段“分析完成”的话</li><li>用户最终看到的是“像成功一样的失败”</li></ul><p>比直接报错更糟糕的，往往就是这种假成功。</p><p>它会让系统表面稳定，实际却在持续输出低可信结果。</p><p>所以这里对应的原则很清楚：</p><p><strong>中间态如果已经失去执行前提，就必须在中间层显式失败，不能把脏状态继续往后传。</strong></p><p>编排层不仅要会“衔接成功路径”。</p><p>它还要会“阻断无意义路径”。</p><h3 id="5-所有-LLM-调用都没有超时"><a href="#5-所有-LLM-调用都没有超时" class="headerlink" title="5. 所有 LLM 调用都没有超时"></a>5. 所有 LLM 调用都没有超时</h3><p>这类问题一开始很容易被忽略。</p><p>因为大多数时候模型都能在可接受时间内返回。</p><p>但只要链路复杂一点：</p><ul><li>Router 一次</li><li>Agent 多轮 ReAct</li><li>KB 改写一次</li><li>RAG 回答一次</li><li>Data engine 再调用若干次</li></ul><p>就会发现“单次调用没问题”不等于“整条链路安全”。</p><p>这次我把超时拆成了三层：</p><ol><li>单次模型调用超时</li><li>Agent 总预算</li><li>API 总请求超时</li></ol><p>这样做的关键，不是为了显得严谨，而是为了让系统始终知道自己还剩多少时间可以花。</p><p>一个没有时间预算概念的 Agent，本质上不是智能体，而是概率性阻塞源。</p><h3 id="6-KB-纯字符分块会截断语义"><a href="#6-KB-纯字符分块会截断语义" class="headerlink" title="6. KB 纯字符分块会截断语义"></a>6. KB 纯字符分块会截断语义</h3><p>很多 RAG 系统一开始都会先用字符滑窗。</p><p>实现简单，性能也直接。</p><p>但对制度、流程、规则文档来说，这种方式有个很明显的问题：</p><ul><li>一条审批规则可能被从中间切断</li><li>一个条件句可能和结果句分离</li><li>一个列表项可能只剩半句</li></ul><p>这会带来一种非常难排查的质量退化：</p><p>检索看起来命中了，但命中的其实是语义残片。</p><p>所以这次我把分块策略换成了：</p><ul><li>段落优先</li><li>超长段落按句子聚合</li><li>仍然过长才退回字符滑窗</li></ul><p>背后的原则很朴素：</p><p><strong>RAG 质量不只取决于 embedding 和 rerank，还取决于你有没有先把文档切成“人类还能理解的最小单元”。</strong></p><h2 id="这次升级后，我对-Agent-编排有了三个更稳定的判断"><a href="#这次升级后，我对-Agent-编排有了三个更稳定的判断" class="headerlink" title="这次升级后，我对 Agent 编排有了三个更稳定的判断"></a>这次升级后，我对 Agent 编排有了三个更稳定的判断</h2><h3 id="判断一：状态比过程更重要"><a href="#判断一：状态比过程更重要" class="headerlink" title="判断一：状态比过程更重要"></a>判断一：状态比过程更重要</h3><p>很多系统会把大量精力花在“让 Agent 更会推理”上。</p><p>但更基础的问题其实是：</p><p><strong>系统有没有把高价值状态留下来。</strong></p><p>如果没有，每轮都在重新规划、重新读环境、重新猜上一次做了什么。</p><p>这不是智能。</p><p>这是昂贵的失忆。</p><h3 id="判断二：编排系统要允许“不确定”"><a href="#判断二：编排系统要允许“不确定”" class="headerlink" title="判断二：编排系统要允许“不确定”"></a>判断二：编排系统要允许“不确定”</h3><p>很多工程实现喜欢把所有路径在设计期写死：</p><ul><li>这种走 A</li><li>那种走 B</li><li>混合就是 A 再 B</li></ul><p>但真实请求经常不是这样。</p><p>有些问题最合理的顺序，本来就应该由 Agent 在看到局部结果后再决定。</p><p>所以一个成熟的编排层，至少应该允许三种模式并存：</p><ul><li>静态路径</li><li>半静态路径</li><li>运行时决策路径</li></ul><p><code>agent_decides</code> 的价值就在这里。</p><p>它不是放弃架构设计。</p><p>它是承认：某些问题的最优流程，只能在执行期被确定。</p><h3 id="判断三：失败设计是编排设计的一部分"><a href="#判断三：失败设计是编排设计的一部分" class="headerlink" title="判断三：失败设计是编排设计的一部分"></a>判断三：失败设计是编排设计的一部分</h3><p>如果一个系统只设计 happy path，它就不是真的编排系统。</p><p>它只是一串乐观调用。</p><p>真正的编排层，必须回答这些问题：</p><ul><li>哪些失败应该立即中断</li><li>哪些失败可以降级继续</li><li>哪些失败必须向用户暴露</li><li>哪些失败应该转成 Agent 的反思上下文</li></ul><p>从这个角度看：</p><ul><li>空 plan 早失败</li><li>KB 查询失败但保留数据结果</li><li>ReAct 超时后阶段性收尾</li><li>API 超时后显式 SSE <code>error</code></li></ul><p>这些都不是边角料。</p><p>它们构成的是系统的可恢复性。</p><h2 id="我现在更认同的一种-Agent-编排观"><a href="#我现在更认同的一种-Agent-编排观" class="headerlink" title="我现在更认同的一种 Agent 编排观"></a>我现在更认同的一种 Agent 编排观</h2><p>如果要用一句话总结这次升级后的思考，我会说：</p><p><strong>Agent 编排层不是“提示词 + 工具调用”的包装层，而是一个负责管理状态、预算、置信度和语义边界的执行控制层。</strong></p><p>它至少应该做到四件事：</p><ol><li>把有价值的中间结果变成长期可复用状态</li><li>把不确定的路由结果变成显式策略选择</li><li>把模糊失败变成可诊断、可恢复的失败</li><li>把所有外部依赖纳入统一的时间预算体系</li></ol><p>只有做到这些，Agent 才不是“偶尔表现得像个智能体”。</p><p>它才开始具备稳定的工程行为。</p><h2 id="这次升级后，系统真正变强的地方是什么"><a href="#这次升级后，系统真正变强的地方是什么" class="headerlink" title="这次升级后，系统真正变强的地方是什么"></a>这次升级后，系统真正变强的地方是什么</h2><p>如果只从 diff 看，这次改动里有很多具体点：</p><ul><li><code>chart_plan / table_plan</code> 持久化</li><li><code>agent_decides</code> 回到 Agent</li><li>低置信路由降级</li><li>空 plan early return</li><li>LLM timeout + budget</li><li>段落级 KB chunking</li></ul><p>但从架构层面看，真正变强的是另一件事：</p><p><strong>OpsMind 的 Agent 编排开始从“流程驱动”转向“状态驱动”。</strong></p><p>这意味着之后系统更容易承接的会是：</p><ul><li>多轮分析迭代</li><li>混合查询的动态策略选择</li><li>会话级任务恢复</li><li>更细粒度的失败恢复</li><li>更稳的长期记忆与知识边界</li></ul><p>这也是我觉得这次升级最值得记录的地方。</p><p>不是因为它修了六个问题。</p><p>而是因为它让系统往“可持续演化的 Agent 架构”迈进了一步。</p><h2 id="一个仍然需要记住的后续动作"><a href="#一个仍然需要记住的后续动作" class="headerlink" title="一个仍然需要记住的后续动作"></a>一个仍然需要记住的后续动作</h2><p>这次 KB 分块策略虽然已经改成段落优先，但旧文档数据不会自动重切分。</p><p>所以如果要让知识库检索质量真正受益，后续还需要：</p><ul><li>清理旧的向量数据</li><li>对文档重新 ingest</li><li>观察召回质量和答案稳定性变化</li></ul><p>这类动作很容易在“代码改完以后”被忘掉。</p><p>但它其实是架构升级真正闭环的一部分。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>如果说之前的系统是在证明“Agent 编排能跑起来”。</p><p>那这次升级更像是在回答另一个问题：</p><p><strong>当系统开始变复杂时，我们怎样让 Agent 编排继续保持可信、可控、可演化。</strong></p><p>这比把一次回答做对更难。</p><p>但它决定了一个 Agent 系统到底只是 demo，还是能变成长期产品。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/agent-orchestration-evolution/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/agent-orchestration-evolution/"/>
    <published>2026-03-24T00:20:00.000Z</published>
    <summary>结合 OpsMind 的升级实践，复盘我对 Agent 编排层的重新理解：它不只是把模型和工具连起来，而是在管理状态、预算、置信度和失败边界。</summary>
    <title>Agent 编排升级：从“能跑”到“能持续演化”</title>
    <updated>2026-04-03T03:00:45.904Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="方法论" scheme="https://bniosfhaiuk.xyz/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="架构设计" scheme="https://bniosfhaiuk.xyz/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="RAG" scheme="https://bniosfhaiuk.xyz/tags/RAG/"/>
    <category term="LLM应用" scheme="https://bniosfhaiuk.xyz/tags/LLM%E5%BA%94%E7%94%A8/"/>
    <category term="知识库" scheme="https://bniosfhaiuk.xyz/tags/%E7%9F%A5%E8%AF%86%E5%BA%93/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/context-memory-knowledge-architecture.svg" alt="Context Memory Knowledge Base Cover"></p><p>这篇文章不讨论“怎么把一个聊天机器人做出来”。</p><p>我更想讨论一个更容易被做糊的工程问题。</p><p>当一个系统开始同时承载闲聊、知识问答、数据分析三类任务时，我们到底该怎么设计它的上下文、记忆层和知识库。</p><p>在 OpsMind 里，我后来越来越确定一件事：</p><p><strong>这三者如果不分开，系统一定会越来越重、越来越慢、越来越不准。</strong></p><h2 id="一、为什么我要重新想“上下文”这件事"><a href="#一、为什么我要重新想“上下文”这件事" class="headerlink" title="一、为什么我要重新想“上下文”这件事"></a>一、为什么我要重新想“上下文”这件事</h2><p>做 LLM 产品时，一个很常见的偷懒方式，是把所有相关信息都叫作“上下文”。</p><p>最近几轮聊天是上下文。</p><p>用户偏好是上下文。</p><p>企业制度文档是上下文。</p><p>某次分析结论是上下文。</p><p>会话摘要也是上下文。</p><p>表面上它们都能进 prompt。</p><p>但从工程角度看，它们根本不是同一种东西。</p><p>在我的项目里，系统逐渐长出了三类典型请求：</p><ul><li><code>CHAT</code>：解释、建议、润色、头脑风暴</li><li><code>RAG</code>：制度、流程、规则、组织知识问答</li><li><code>DATA</code>：文件分析、图表生成、趋势判断、业务洞察</li></ul><p>这三类能力混在同一个产品里后，一个问题就会变得非常具体：</p><p><strong>到底什么该进入模型当前上下文，什么该变成长期记忆，什么又应该进入知识库。</strong></p><p>如果这个边界想不清楚，后面的问题会一串串地冒出来：</p><ul><li>简单聊天也很慢，因为每轮都背着一堆历史和工具状态</li><li>数据分析越聊越乱，因为真正重要的是“分析状态”而不是聊天原文</li><li>知识问答会被会话里的临时结论污染</li><li>记忆越存越多，召回质量反而越来越差</li></ul><p>所以我后来不再问“要不要存上下文”。</p><p>我更关心的是：</p><p><strong>不同类型的信息，应该以什么形式进入哪一层，又该在什么时候失效。</strong></p><h2 id="二、先把概念拆开：上下文、记忆、知识库不是一回事"><a href="#二、先把概念拆开：上下文、记忆、知识库不是一回事" class="headerlink" title="二、先把概念拆开：上下文、记忆、知识库不是一回事"></a>二、先把概念拆开：上下文、记忆、知识库不是一回事</h2><p>我现在更倾向于把系统里的信息层拆成三层。</p><h3 id="1-短期上下文"><a href="#1-短期上下文" class="headerlink" title="1. 短期上下文"></a>1. 短期上下文</h3><p>这是模型在当前一次回答里直接看到的内容。</p><p>它通常来自当前输入、最近几轮消息，以及这一次任务的即时状态。</p><p>它解决的问题是：</p><p><strong>这一轮到底该怎么答。</strong></p><p>对 <code>CHAT</code> 来说，它往往就是最近几轮自然对话。</p><p>对 <code>DATA</code> 来说，它不该只是消息，还应该包含当前文件、最近分析任务和生成状态。</p><h3 id="2-记忆层"><a href="#2-记忆层" class="headerlink" title="2. 记忆层"></a>2. 记忆层</h3><p>这是系统为了跨轮、跨会话保持连续性，而保留下来的高价值信息。</p><p>它不应该等于“全部历史”。</p><p>它更像是被筛选后的偏好、目标、约定、摘要和关键结论。</p><p>它解决的问题是：</p><p><strong>之后还要不要继续记得。</strong></p><h3 id="3-知识库"><a href="#3-知识库" class="headerlink" title="3. 知识库"></a>3. 知识库</h3><p>知识库存的是稳定事实，而不是对话状态。</p><p>比如企业制度、流程文档、规范说明和组织级知识。</p><p>它解决的问题是：</p><p><strong>事实依据到底是什么。</strong></p><p>这三层最关键的区别，不在于数据长什么样。</p><p>而在于它们服务的问题根本不同。</p><table><thead><tr><th>层级</th><th>解决的问题</th><th>数据特点</th><th>生命周期</th><th>是否共享</th></tr></thead><tbody><tr><td>短期上下文</td><td>当前这轮怎么答</td><td>近期、原始、即时</td><td>秒到分钟</td><td>不共享</td></tr><tr><td>记忆层</td><td>之后还要不要记得</td><td>高价值、压缩后、可检索</td><td>天到月</td><td>视作用域而定</td></tr><tr><td>知识库</td><td>事实依据是什么</td><td>稳定、权威、结构化</td><td>月到年</td><td>可共享</td></tr></tbody></table><p>如果把这三层混成一个系统，结果通常就是：</p><ul><li>该短的东西变长</li><li>该稳的东西变脏</li><li>该快的东西变慢</li></ul><h2 id="三、为什么“上下文入库”既可行，又危险"><a href="#三、为什么“上下文入库”既可行，又危险" class="headerlink" title="三、为什么“上下文入库”既可行，又危险"></a>三、为什么“上下文入库”既可行，又危险</h2><p>从技术上说，把上下文做成可检索对象，当然是可行的。</p><p>一个已经具备向量检索能力的系统，本来就有切块、嵌入、持久化、召回和元数据过滤这些基础设施。</p><p>所以“高价值上下文入库”这件事，本身没有问题。</p><p>危险在于：</p><p><strong>可行，不等于应该直接混进现有知识库。</strong></p><p>如果把会话上下文直接塞进企业知识库，至少会出现四类污染。</p><h3 id="1-语义污染"><a href="#1-语义污染" class="headerlink" title="1. 语义污染"></a>1. 语义污染</h3><p>用户问“报销制度是什么”，第一条召回却是某次会话里的总结，而不是制度原文。</p><p>系统会慢慢从“问制度”滑向“问模型之前是怎么总结制度的”。</p><h3 id="2-时效污染"><a href="#2-时效污染" class="headerlink" title="2. 时效污染"></a>2. 时效污染</h3><p>知识库偏稳定，记忆层偏时效。</p><p>如果系统不区分长期事实和短期状态，就会出现旧结论覆盖新结论，临时约定覆盖稳定规则。</p><h3 id="3-检索污染"><a href="#3-检索污染" class="headerlink" title="3. 检索污染"></a>3. 检索污染</h3><p>知识问答要找的是相关事实。</p><p>记忆召回要找的是对当前用户仍然有帮助的信息。</p><p>这两个目标不一样，排序标准也不一样。</p><h3 id="4-安全边界污染"><a href="#4-安全边界污染" class="headerlink" title="4. 安全边界污染"></a>4. 安全边界污染</h3><p>企业知识库更接近共享资源。</p><p>上下文记忆更接近用户或会话私有资源。</p><p>它们混在一起后，权限和隔离会立刻复杂起来。</p><p>所以我现在的结论很明确：</p><p><strong>上下文可以入库，但应该进入独立的记忆层，而不是直接混进知识库。</strong></p><h2 id="四、什么才算高价值上下文"><a href="#四、什么才算高价值上下文" class="headerlink" title="四、什么才算高价值上下文"></a>四、什么才算高价值上下文</h2><p>这是整个设计里最关键，也最容易被低估的问题。</p><p>我现在对高价值上下文的定义是：</p><blockquote><p>未来大概率还会有用，且不容易靠最近几轮自然恢复的信息。</p></blockquote><p>如果一条信息下次还会影响回答，不存就容易丢，丢了以后模型也很难自己推回来，那它就值得考虑进入记忆层。</p><p>我自己会优先看五个维度。</p><h3 id="1-未来复用价值"><a href="#1-未来复用价值" class="headerlink" title="1. 未来复用价值"></a>1. 未来复用价值</h3><p>它以后还会不会再用到。</p><p>比如“以后都先给结论，再解释原因”，复用价值就很高。</p><h3 id="2-不可重建性"><a href="#2-不可重建性" class="headerlink" title="2. 不可重建性"></a>2. 不可重建性</h3><p>如果不存，下次是否很难恢复这条信息。</p><p>用户偏好、项目目标、团队约定，通常都比上一轮普通提问更值得存。</p><h3 id="3-稳定性"><a href="#3-稳定性" class="headerlink" title="3. 稳定性"></a>3. 稳定性</h3><p>这条信息会持续多久。</p><p>“以后都用中文回复”通常稳定。</p><p>“我今天有点烦”通常不稳定。</p><h3 id="4-行为影响力"><a href="#4-行为影响力" class="headerlink" title="4. 行为影响力"></a>4. 行为影响力</h3><p>它会不会明显改变系统后续行为。</p><p>比如是否显示工具过程、检索优先级、先走聊天还是先走分析。</p><h3 id="5-信息密度"><a href="#5-信息密度" class="headerlink" title="5. 信息密度"></a>5. 信息密度</h3><p>一小段内容里，是否包含明确约束、偏好、目标或结论。</p><p>像“纯聊天不要显示工具过程，只有数据分析才显示步骤卡片”这种话，信息密度就非常高。</p><p>为了避免“什么都存”，我会给候选记忆做一个轻量评分：</p><ul><li>未来复用性：<code>0-3</code></li><li>不可重建性：<code>0-2</code></li><li>稳定性：<code>0-2</code></li><li>行为影响力：<code>0-3</code></li></ul><p>总分 <code>&gt;= 6</code> 再考虑进入记忆层。</p><p>这不是学术最优解。</p><p>但工程上很实用，因为它至少能把“全量入库”的冲动压住。</p><h2 id="五、哪些信息最值得被记住"><a href="#五、哪些信息最值得被记住" class="headerlink" title="五、哪些信息最值得被记住"></a>五、哪些信息最值得被记住</h2><p>在 OpsMind 这种系统里，我最想留下来的通常是四类信息。</p><h3 id="1-用户偏好"><a href="#1-用户偏好" class="headerlink" title="1. 用户偏好"></a>1. 用户偏好</h3><p>比如偏好中文、偏好简洁、喜欢先结论后展开、不喜欢看到工具过程。</p><h3 id="2-长期目标"><a href="#2-长期目标" class="headerlink" title="2. 长期目标"></a>2. 长期目标</h3><p>比如用户正在做企业内部智能助手，重点关注知识问答和数据分析体验。</p><h3 id="3-关键约定"><a href="#3-关键约定" class="headerlink" title="3. 关键约定"></a>3. 关键约定</h3><p>比如 <code>CHAT</code> 不显示工具过程，<code>DATA</code> 需要显示多步骤进度。</p><h3 id="4-重要结论"><a href="#4-重要结论" class="headerlink" title="4. 重要结论"></a>4. 重要结论</h3><p>比如一次分析已经得出稳定判断，或者一次设计讨论已经明确某条策略。</p><p>这些内容如果不保存，系统要么忘掉，要么每次重新推一遍。</p><h2 id="六、不值得进入记忆层的内容"><a href="#六、不值得进入记忆层的内容" class="headerlink" title="六、不值得进入记忆层的内容"></a>六、不值得进入记忆层的内容</h2><p>工程上最危险的，其实不是存少了。</p><p>而是存多了。</p><p>下面这些内容，我通常不会让它们进入记忆层：</p><ul><li>寒暄：你好、谢谢、在吗</li><li>短时情绪：今天有点烦</li><li>工具运行日志：第几轮调用了什么函数</li><li>冗长中间过程：长篇推理草稿</li><li>低密度重复信息：没有新增约束的重复要求</li></ul><p>这类内容一旦大量入库，系统会出现一个很讨厌的错觉：</p><p>召回命中率看起来上去了。</p><p>但实际回答质量却在下降。</p><p>因为模型被一堆“看起来相关、其实无用”的内容淹掉了。</p><h2 id="七、我更认可的架构：分层存储，分层读取"><a href="#七、我更认可的架构：分层存储，分层读取" class="headerlink" title="七、我更认可的架构：分层存储，分层读取"></a>七、我更认可的架构：分层存储，分层读取</h2><p>如果从系统设计角度重新整理，我会把这件事拆成四层。</p><h3 id="1-短期上下文层"><a href="#1-短期上下文层" class="headerlink" title="1. 短期上下文层"></a>1. 短期上下文层</h3><p>只放最近几轮原始消息和当前轮必要状态。</p><p>它负责保证当前对话连贯。</p><h3 id="2-会话摘要层"><a href="#2-会话摘要层" class="headerlink" title="2. 会话摘要层"></a>2. 会话摘要层</h3><p>每个会话定期压缩，只保留当前任务、关键结论、已做决策和用户偏好。</p><p>这层可以先放 SQLite，也可以同时写入记忆库。</p><h3 id="3-记忆向量层"><a href="#3-记忆向量层" class="headerlink" title="3. 记忆向量层"></a>3. 记忆向量层</h3><p>单独建 collection，比如 <code>opsmind_memory</code>，或者继续按 <code>user / session</code> 再细分。</p><p>这层不存所有历史，只存筛过的高价值记忆。</p><h3 id="4-企业知识库层"><a href="#4-企业知识库层" class="headerlink" title="4. 企业知识库层"></a>4. 企业知识库层</h3><p>继续存制度、流程、文档和组织级知识。</p><p>它不应该和记忆混用。</p><h2 id="八、如果真正落地，我会怎么定义记忆结构"><a href="#八、如果真正落地，我会怎么定义记忆结构" class="headerlink" title="八、如果真正落地，我会怎么定义记忆结构"></a>八、如果真正落地，我会怎么定义记忆结构</h2><p>工程上最忌讳“把原文整段扔进去”。</p><p>我更倾向于把记忆条目结构化保存。</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;memory_id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;mem_20260324_001&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;scope&quot;</span><span class="punctuation">:</span> <span class="string">&quot;session&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;session_id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;abc123&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;memory_type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;preference&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;summary&quot;</span><span class="punctuation">:</span> <span class="string">&quot;用户希望纯聊天时不展示工具过程，数据分析时展示步骤卡片。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;importance&quot;</span><span class="punctuation">:</span> <span class="number">0.86</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;confidence&quot;</span><span class="punctuation">:</span> <span class="number">0.91</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;created_at&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2026-03-24T11:30:00&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;updated_at&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2026-03-24T11:30:00&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;expires_at&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">null</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;source_message_ids&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="number">101</span><span class="punctuation">,</span> <span class="number">103</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;supersedes&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">null</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>我至少会保留这些字段：</p><ul><li><code>scope</code>：<code>session / user / workspace</code></li><li><code>memory_type</code>：<code>preference / goal / agreement / conclusion / summary</code></li><li><code>summary</code>：规范化短摘要，而不是长原文</li><li><code>importance</code>：重要程度</li><li><code>confidence</code>：这条记忆是否可靠</li><li><code>expires_at</code>：是否有时效</li><li><code>supersedes</code>：是否覆盖旧记忆</li></ul><p>这样做的好处是：</p><ul><li>可检索</li><li>可更新</li><li>可过期</li><li>可治理</li></ul><h2 id="九、写入策略：候选提取，不是每轮硬写"><a href="#九、写入策略：候选提取，不是每轮硬写" class="headerlink" title="九、写入策略：候选提取，不是每轮硬写"></a>九、写入策略：候选提取，不是每轮硬写</h2><p>我不太认同“每轮都自动写记忆”。</p><p>更稳的路线是四步：</p><h3 id="第一步：候选提取"><a href="#第一步：候选提取" class="headerlink" title="第一步：候选提取"></a>第一步：候选提取</h3><p>先从当前轮里找出可能有价值的信息片段。</p><h3 id="第二步：价值判断"><a href="#第二步：价值判断" class="headerlink" title="第二步：价值判断"></a>第二步：价值判断</h3><p>判断它是否真的属于高价值上下文。</p><h3 id="第三步：去重与合并"><a href="#第三步：去重与合并" class="headerlink" title="第三步：去重与合并"></a>第三步：去重与合并</h3><p>像“以后用中文”和“继续中文回复”，大概率应该合并，而不是存两次。</p><h3 id="第四步：结构化入库"><a href="#第四步：结构化入库" class="headerlink" title="第四步：结构化入库"></a>第四步：结构化入库</h3><p>只把最终筛过、归一化过的摘要写入记忆层。</p><p>这个过程比“全部原文入库”复杂。</p><p>但它换来的，是可维护性和后续检索质量。</p><h2 id="十、读取策略必须按意图分层"><a href="#十、读取策略必须按意图分层" class="headerlink" title="十、读取策略必须按意图分层"></a>十、读取策略必须按意图分层</h2><p>记忆层也不该每次全取。</p><p>不同请求，应该读不同层。</p><h3 id="CHAT"><a href="#CHAT" class="headerlink" title="CHAT"></a><code>CHAT</code></h3><p>纯聊天时，我更看重用户偏好、当前会话摘要和长期目标。</p><p>这会让系统更像一个连续的助手。</p><p>但它不该背太多分析细节。</p><h3 id="RAG"><a href="#RAG" class="headerlink" title="RAG"></a><code>RAG</code></h3><p>知识问答时，我会读用户最近追问的主题，以及之前已经查到过的结论。</p><p>但企业知识库仍然应该保持更高的事实优先级。</p><h3 id="DATA"><a href="#DATA" class="headerlink" title="DATA"></a><code>DATA</code></h3><p>数据分析时，最重要的通常不是聊天原文。</p><p>而是结构化分析上下文。</p><p>比如当前文件、字段理解、用户确认过的配置、最近一次分析结论和已生成图表索引。</p><p>对 <code>DATA</code> 来说，真正需要的不是“更多消息条数”。</p><p>而是“更像状态对象的上下文”。</p><h2 id="十一、CHAT-RAG-DATA-不只是分流，更是上下文治理的开始"><a href="#十一、CHAT-RAG-DATA-不只是分流，更是上下文治理的开始" class="headerlink" title="十一、CHAT / RAG / DATA 不只是分流，更是上下文治理的开始"></a>十一、<code>CHAT / RAG / DATA</code> 不只是分流，更是上下文治理的开始</h2><p>我后来越来越认同一个判断：</p><blockquote><p>一个系统之所以显得聪明，很多时候不是因为模型更强，而是因为它知道什么时候该看什么，不该看什么。</p></blockquote><p>这也是我为什么越来越坚持把系统拆成 <code>CHAT / RAG / DATA</code> 三条路径。</p><p>一旦这样拆开，很多工程决策才会变得清晰：</p><ul><li>纯聊天不该启动 heavy agent</li><li>知识问答不该被会话临时结论污染</li><li>数据分析不该只靠最近十条消息</li></ul><p>真正好的产品，不是把所有能力都堆在一起。</p><p>而是在正确的时候，调用正确的能力，并读取正确层级的上下文。</p><h2 id="十二、如果只给我一句总结"><a href="#十二、如果只给我一句总结" class="headerlink" title="十二、如果只给我一句总结"></a>十二、如果只给我一句总结</h2><p>如果把这篇文章压缩成一句话，我会这样说：</p><blockquote><p>上下文决定当前回答，记忆决定长期连续性，知识库决定事实边界；把三者混在一起，系统一定会越来越重、越来越慢、越来越不准。</p></blockquote><p>对企业智能助手来说，成熟的架构从来不是“一个大模型 + 一个大知识库”。</p><p>它更像是：</p><ul><li>短期上下文层</li><li>会话摘要层</li><li>记忆向量层</li><li>企业知识库层</li><li>按意图分流的执行路径</li></ul><p>而“高价值上下文”也不是玄学判断。</p><p>它完全可以被工程化：</p><ul><li>定义标准</li><li>制定评分</li><li>结构化写入</li><li>去重、过期和覆盖</li></ul><p>这才是一个系统从“能回答”走向“像一个真正助手”的分水岭。</p><h2 id="十三、一个务实建议"><a href="#十三、一个务实建议" class="headerlink" title="十三、一个务实建议"></a>十三、一个务实建议</h2><p>如果你也在做类似产品，我不建议一开始就追求“万能上下文系统”。</p><p>更现实的路线通常是：</p><ol><li>先把 <code>CHAT / RAG / DATA</code> 分流做好</li><li>再做会话摘要</li><li>再做高价值记忆写入</li><li>最后再做跨会话记忆召回</li></ol><p>这样每一步都可验证、可回滚、可观察。</p><p>这更像专业工程，而不是一次性豪赌。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>我越来越相信，LLM 产品真正的壁垒不只是模型能力。</p><p>更是信息治理能力。</p><p>模型决定“会不会说”。</p><p>上下文决定“这次说什么”。</p><p>记忆决定“下次还记不记得”。</p><p>知识库决定“说得准不准”。</p><p>当这三者各司其职，一个系统才开始真正具备“助手感”。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/context-memory-knowledge-architecture/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/context-memory-knowledge-architecture/"/>
    <published>2026-03-23T22:30:00.000Z</published>
    <summary>结合 OpsMind 的实践，整理我对上下文、记忆层与知识库的理解：它们不是一个大口袋，而是三套服务不同问题的信息系统。</summary>
    <title>上下文、记忆与知识库：我在 OpsMind 里重新理解 LLM 产品的工程化边界</title>
    <updated>2026-04-03T03:00:45.905Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="随笔" scheme="https://bniosfhaiuk.xyz/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="工程协作" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8D%8F%E4%BD%9C/"/>
    <category term="AI" scheme="https://bniosfhaiuk.xyz/tags/AI/"/>
    <category term="方法论" scheme="https://bniosfhaiuk.xyz/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/"/>
    <category term="体验设计" scheme="https://bniosfhaiuk.xyz/tags/%E4%BD%93%E9%AA%8C%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/subagent-writing-notes-2026-03-24.svg" alt="Subagent Writing Notes Cover"></p><p>最近我认真玩了一次 <code>subagent</code>。</p><p>不是那种“哦，原来还能多开几个 agent，挺酷”的浅层新鲜感，而是更具体一点的东西：我突然感觉到，自己平时脑子里那种会同时开很多小线程、预演分工、来回切换视角的思考方式，第一次被一个工具很明确地接住了。</p><p>体验到一半的时候，我脑子里甚至冒出一句很危险的话：</p><p><strong>好像我的思维其实和业界顶尖是一样的，只不过落后了一些。</strong></p><p>先声明，这句是玩笑，不是我准备在博客里郑重宣布自己已经抵达了什么新境界。<br>但玩笑里确实有一点真实：很多时候，我们和更成熟的方法论之间，差的未必是“有没有这种思路”，而是有没有把它稳定地组织起来、拆开来、交给合适角色去推进。</p><p>而 <code>subagent</code> 最有意思的地方，恰好就在这里。</p><h2 id="以前只是脑内多人会议，现在终于有了操作界面"><a href="#以前只是脑内多人会议，现在终于有了操作界面" class="headerlink" title="以前只是脑内多人会议，现在终于有了操作界面"></a>以前只是脑内多人会议，现在终于有了操作界面</h2><p>我之前其实已经模拟过多 agent 分工的场景，也写过相关内容。那时候我最强的感受是：一旦把角色边界、上下游依赖和拍板机制说清楚，很多“开发混乱”并不会消失，但会开始变得可管理。</p><p>这次真的把 <code>subagent</code> 用起来之后，我的感受又往前走了一步。</p><p>它不像传统意义上的“工具增强”，更像是把脑内那些本来只在意识里高速切换的动作，强行拉到了桌面上：</p><ol><li>哪一部分需要单独探索</li><li>哪一部分可以并行推进</li><li>哪一部分必须由主线程拍板</li><li>谁负责起草，谁负责收口，谁只提供局部视角</li></ol><p>以前这些动作都在脑子里一团完成，所以你会觉得自己只是“在想”。<br>真的把 <code>subagent</code> 放进工作流之后，你才会发现：原来很多思考本来就带着团队协作的形状，只是以前没有人把它们做成按钮。</p><h2 id="最让我上头的，不是效率，而是那种“思维被显性化”的感觉"><a href="#最让我上头的，不是效率，而是那种“思维被显性化”的感觉" class="headerlink" title="最让我上头的，不是效率，而是那种“思维被显性化”的感觉"></a>最让我上头的，不是效率，而是那种“思维被显性化”的感觉</h2><p>我原本以为，<code>subagent</code> 最直观的价值会是效率。</p><p>比如一个 agent 看代码结构，一个 agent 起草文章，一个 agent 想标题，我来负责整合，听起来当然很省时间。</p><p>但真正让我觉得新奇的，并不是“我同时叫来了几个人干活”，而是我第一次很清楚地看见了自己的思维方式是怎么被拆开的。</p><p>有些人思考时是一条很深的线，一口气往下钻。<br>我更像是会在脑内自然拉出几条线：</p><ol><li>一条线先去探路</li><li>一条线先写个粗稿</li><li>另一条线负责怀疑、挑刺、改口气</li><li>最后再回到一个主视角里统一语气</li></ol><p>这听起来很像协作，其实也是一种思考习惯。</p><p>所以我才会冒出那个有点欠打、但确实带点快乐的玩笑：<br><strong>难道我和业界顶尖的区别，真的只是版本落后了一点？</strong></p><p>当然，这话不能当真。<br>更准确的表达应该是：有些思维习惯本来就存在，只是顶尖的人更早把它训练成了方法，而我们现在终于开始拥有一些能配合这种方法的工具。</p><h2 id="subagent-也没有神到哪里去，它只是把“组织能力”变重要了"><a href="#subagent-也没有神到哪里去，它只是把“组织能力”变重要了" class="headerlink" title="subagent 也没有神到哪里去，它只是把“组织能力”变重要了"></a><code>subagent</code> 也没有神到哪里去，它只是把“组织能力”变重要了</h2><p>这里我反而很想给它降降温。</p><p><code>subagent</code> 并不是你一声令下，就会凭空长出一个完美团队。<br>它带来的不是“免费智力翻倍”，而是更高要求的组织问题。</p><p>你很快就会发现，真正难的事情不是“开几个 agent”，而是：</p><ol><li>任务边界切得够不够清楚</li><li>哪些事情值得并行，哪些事情不能乱拆</li><li>什么时候应该等它，什么时候不该被它拖住</li><li>最后谁来把不同语气、不同重点的输出合成一个完整结果</li></ol><p>换句话说，<code>subagent</code> 很像把你从“亲自干活的人”往“会分工、会收口的人”轻轻推了一下。</p><p>这也是为什么它让我觉得特别像真实协作。<br>真正的价值不只是多了几个帮手，而是你被迫开始面对协作本身：交接、边界、节奏、依赖、收敛。</p><p>这些东西，以前在人类团队里就一直重要，只不过现在它们提前出现在了你和工具之间。</p><h2 id="这篇文章本身，就是一个很好的例子"><a href="#这篇文章本身，就是一个很好的例子" class="headerlink" title="这篇文章本身，就是一个很好的例子"></a>这篇文章本身，就是一个很好的例子</h2><p>更好玩的是，这篇文章本身就不是完全由我一个人线性写完的。</p><p>我真的把起草任务丢给了子代理。<br>第一位子代理甚至还很真实地给我上了一课，直接 <code>503 Service Unavailable</code>，下线得相当有职业精神。那一刻我居然没有觉得出戏，反而觉得这个体验更完整了：原来连“协作中有人先掉线”这种事，它都替我演到了。</p><p>后面我换了两路继续起草：一路想标题和结构，一路负责吐首稿；然后我再回来删掉不属于我的句子、修掉太满的表达、把真正想说的部分留下。</p><p>这个过程特别像一个很小的团队：</p><ol><li>有人先搭脚手架</li><li>有人只负责提出更好的开场方式</li><li>最后还是要有人把声音统一起来</li></ol><p>所以如果你问我，<code>subagent</code> 最迷人的地方是什么，我现在的答案不是“它很强”，而是：</p><p><strong>它让很多原本只存在于脑内的协作动作，第一次有了可以被调用、被观察、被复盘的形状。</strong></p><h2 id="我现在对它最真实的判断"><a href="#我现在对它最真实的判断" class="headerlink" title="我现在对它最真实的判断"></a>我现在对它最真实的判断</h2><p>如果只把 <code>subagent</code> 当作“更高级一点的自动补全”，那它当然也能用，但会浪费掉它最好玩的部分。</p><p>我现在更愿意把它理解成一种新的工作界面：</p><ol><li>它不是替你思考</li><li>它也不是替你负责</li><li>它更像是在帮你把思考拆成几个可以并行处理的角色</li></ol><p>最后拍板的人还是你。<br>真正构成作品的人也还是你。<br>但中间那段原本混在一起的思考、试探、试写、怀疑和整合，终于不需要永远挤在一个线程里完成了。</p><p>这件事对我来说，比“效率提升了多少”更有意思。</p><p>因为它让我第一次很具体地感觉到：<br>我们以为自己在寻找更聪明的工具，结果有时候真正被照亮的，反而是自己原本就有、只是一直没被很好利用的思维结构。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>所以这次体验之后，我对 <code>subagent</code> 最喜欢的一点，不是它多厉害，而是它让我看见了两件事：</p><ol><li>协作并不总是发生在团队里，它也可能早就发生在一个人的脑子里</li><li>很多“高级方法”并没有神秘到遥不可及，它们往往只是被更早、更稳定地组织起来了</li></ol><p>至于那句“我和业界顶尖可能只是版本号差了一点”的想法，我还是坚持把它放在玩笑区。<br>但我会悄悄保留那一点点开心，因为这种开心本身也说明了一件事：</p><p>有时候你不是突然变厉害了。<br>你只是第一次遇到了一个，刚好适合你思维形状的工具。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/notes/subagent-writing-notes-2026-03-24/</id>
    <link href="https://bniosfhaiuk.xyz/notes/subagent-writing-notes-2026-03-24/"/>
    <published>2026-03-23T18:00:33.000Z</published>
    <summary>第一次认真体验 subagent 之后，我意识到它真正有意思的地方，不只是效率，而是把脑内并行思考和角色分工显性化。</summary>
    <title>我让 subagent 参与写这篇文章，然后更理解了协作</title>
    <updated>2026-04-03T03:00:45.908Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="随笔" scheme="https://bniosfhaiuk.xyz/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="关系" scheme="https://bniosfhaiuk.xyz/tags/%E5%85%B3%E7%B3%BB/"/>
    <category term="情绪" scheme="https://bniosfhaiuk.xyz/tags/%E6%83%85%E7%BB%AA/"/>
    <category term="成长" scheme="https://bniosfhaiuk.xyz/tags/%E6%88%90%E9%95%BF/"/>
    <category term="自我觉察" scheme="https://bniosfhaiuk.xyz/tags/%E8%87%AA%E6%88%91%E8%A7%89%E5%AF%9F/"/>
    <content>
      <![CDATA[<div class="essay-prose"><p class="essay-prose__lead">我曾经以为，感情里最糟糕的事，是喜欢上一个人之后，自己就开始出错。</p><p>那时我很相信这个判断，甚至相信到有些宿命的程度。仿佛只要我一认真，事情就会偏离；只要我开始在意，节奏就会乱掉；只要我动了真心，那个原本还算从容、还有一点魅力的自己，就会在不知不觉间变形。于是“喜欢”在我这里，不再是某种值得靠近的经验，反而更像一种危险的信号。它提醒我：重心要开始偏移了，想象会开始失控了，那个在关系之外看起来完整、松弛、甚至有点迷人的自己，也要开始慢慢失真了。</p><p>所以我曾经一度觉得，也许我并不适合认真。我更适合停留在靠近之前，停留在暧昧的边缘，停留在一切都还轻盈、都还不需要负责的地方。在那个位置上，我往往表现得很好。我可以自然，可以风趣，可以有分寸，可以让人觉得难以捉摸又忍不住靠近。可一旦真的喜欢，我就会变得不像那个自己。我开始在意对方的一举一动，开始留意那些细小的变化，开始想知道她究竟有没有那么喜欢我，开始在一些尚未发生的事情里，提前感到失落和危险。</p><p>后来我才慢慢明白，问题并不在于喜欢本身。</p><p>真正让我失衡的，从来不是喜欢，而是我对喜欢的理解。</p><div class="essay-prose__pause" aria-hidden="true">···</div><p>我过去对感情有一种近乎严苛的判断标准。我相信喜欢不是空泛的情绪，而是会落实为行为。喜欢应该是回应，是投入，是细节，是在意，是某种可以被感知到的持续性。如果一个人没有这样做，我几乎立刻就能意识到：她没有那么喜欢，或者至少，没有用我认可的方式在喜欢。这个判断让我看起来很清醒，也让我产生过一种微妙的优越感，仿佛我比很多人更懂喜欢是什么，也更快识破那些“不够喜欢的行为”。</p><p>可时间久了我才发现，这种清醒并不纯粹。它里面混着另一种东西：过早的判断，和隐秘的傲慢。</p><p>我总是太快地下结论。别人还在相处，我已经在心里打分；别人甚至还不知道哪里出了问题，我已经开始失望。我以为自己是在理解关系，其实很多时候，我只是在评估关系。我并没有真正参与到一段关系里，我更多是在旁观，在观察，在等待对方用她的反应通过我的考核。我不说自己在意什么，不说我失落在哪，不说我期待过什么。我只是沉默地看，安静地等，然后在心里一点点扣分，最后得出一个似乎很理性的结论：她不行，她不够喜欢，这段关系没有我想象中那样成立。</p><p>可关系不是一道判断题。它不是靠我看得多快、多准，就能自然成立的东西。关系更像一种共同生成的现实。它需要互动，需要误解后的修正，需要两个人把那些原本不会自动对齐的部分慢慢磨合出来。可过去的我，太习惯用观察代替互动，用失望代替表达。我希望别人懂，却不愿意让别人真正看见我。我期待被理解，却又总把最真实的部分藏起来，仿佛一旦说出口，就已经输掉了什么。</p><p>现在想来，我真正害怕的，也许并不是失去一个人。</p><p>我更害怕的是，在一个人面前显露出真实的在意之后，不再被视为有魅力。</p><p>比起“她离开我”，我更难接受的是“她看见我在意、看见我失落、看见我不再那么从容之后，不再觉得我迷人”。这件事之所以刺痛，不只是因为关系可能破裂，而是因为它会动摇我对自己的某种信念。我一直很依赖那个版本的自己：有边界，有节奏，不慌不忙，不轻易失去重心。那个自己看起来很完整，也很有吸引力。可一旦我喜欢上谁，我就会发现另一个自己慢慢浮出来：会期待，会脑补，会因为一点变化而起伏，会在沉默中自己制造很多剧情。于是我开始怀疑，是不是“真实的我”和“有魅力的我”其实根本无法共存。是不是我只有在不那么认真、不那么在意的时候，才值得被喜欢。</p><p>可后来我发现，这个判断本身就是错的。</p><div class="essay-prose__pause" aria-hidden="true">···</div><p>因为我明明会被另一种人吸引。那种人平时松弛，有分寸，有边界，可在某个时刻，她会坦诚地说一句：“我刚刚其实有点在意。”她说完并不纠缠，不逼迫，不把情绪变成勒索，也不要求你立刻证明什么。她只是轻轻把真实放在那儿，然后仍然保有她自己的形状。每次想到这种状态，我都只会觉得：太有魅力了。</p><p>这时我才意识到，我一直允许别人“真实又有分寸”很有魅力，却不允许自己这样存在。我把“在意”误判成了“掉价”，把“真实”误判成了“失控”。仿佛魅力只能来自疏离，而不能来自诚实；只能来自游刃有余，而不能来自带着边界的脆弱。可事实上，真正高级的吸引力，从来不是没有需求，而是有需求的时候，仍然不被需求吞没；不是没有波动，而是在波动中依然能保持节奏。</p><p>而我过去最大的问题，并不是太敏感，而是总想比情绪更快一步理解它。</p><p>这几乎成了我面对感情时的一种本能。一个细节刚出现，我的大脑就开始运转：她为什么这样，她是不是变了，这意味着什么，后面会不会越来越糟。她回得慢了一点，不只是慢，而可能意味着热度下降；热度下降，不只是今天状态不好，而可能意味着整体兴趣退潮；整体兴趣退潮，也就意味着这段关系的未来开始转暗。事情还没有发生，我却已经在内部完成了一整套推演。</p><p>我曾经以为，先一步理解自己的情绪，就是一种掌控。仿佛只要我能先把它命名、归类、拆解、解释，它就不会真正伤害到我。可后来我才明白，过早的理解有时候并不是清醒，而是一种防御。情绪还没有活完，判断就已经先到了；感受还没有展开，结论却已经提前落下。我看起来像是在分析，实际上只是在更早地进入自我保护。</p><p>于是脑补、失落、试探，开始形成一种循环。</p><p>我不直接说“我有点在意”，而更倾向于发出某种间接信号，想看看对方会不会接住。我故意放慢一点，故意冷一点，或者抛出一句模糊的话，等待她从空气里读出我的情绪。可试探这种东西，从来给不了确定的答案。它只会制造更多模糊，而所有模糊，都会再一次被我的想象系统利用，成为下一轮脑补的材料。看起来像我在保护自己，实际上我是在把自己反复交给不确定性。</p><p>直到有一句话突然把我点醒：</p><blockquote><p>人是波动的，不是稳定函数。</p></blockquote><p>我太喜欢这句话了。因为它不是安慰，而是一种非常精确的纠偏。</p><p>我过去一直在用一种过于理性的方式看人，仿佛只要一个人昨天对我热，她今天也应该继续热；昨天细腻，今天也理应细腻；一旦某个时刻她的反应不像从前，我就会立刻觉得：性质变了，趋势变了，关系出问题了。可人不是函数，不会稳定地输出同一组结果。人更像带噪声的数据流，有情绪，有疲惫，有分神，有许多无法被单次行为完全解释的波动。一个人一次没有接住你，不等于她整体接不住你；一次回复冷一点，不等于她已经不喜欢你。把一个点当成一个趋势，把一次波动当成一种结论，本身就是一种误判。</p><p>我后来才意识到，自己真正需要学习的，不是如何避免波动，而是如何在波动里不急着下结论。</p><div class="essay-prose__pause" aria-hidden="true">···</div><p>这大概就是我感情观真正开始转变的地方。</p><p>以前的我，总想尽快知道答案：她到底喜不喜欢我，这段关系靠不靠谱，现在的我值不值得继续投入。我太想提前抵达确定性了，仿佛只要足够快地看穿，就能避免后来的一切失望。可感情并不是一道可以提前验算的题。它更像天气，像潮汐，像某种缓慢显现的东西。你不能因为一阵风就断言季节已经换了，也不能因为一天阴天，就宣布整个气候系统失效。</p><p>所以我开始试着用另一种方式理解关系。</p><p>不是一次性押注，不是一瞬间定性，而是把它看成一种“回合制”的存在。这一回合，我表达一点真实；这一回合，她有没有接住；下一回合，我们再看。她这次接住了，不代表未来都值得过度乐观地想象；她这次没接住，也不意味着结局已经可以宣布。我开始练习只对当下的事实负责，而不是让自己的焦虑越权到未来。我开始明白，真正需要看的，不是某一个点，而是几个回合之后仍然持续出现的趋势。不是噪声，而是信号；不是单次波动，而是整体走向。</p><p>这并不意味着我要变得迟钝，或者放弃判断。恰恰相反，这是一种更成熟的判断。它不是取消敏感，而是给敏感加上时间维度。不是一有感觉就下定义，而是允许感觉先停留一会儿，允许现实比想象慢一点展开。</p><p>我想，这才是我现在逐渐理解的“去爱”。</p><p>爱不是要求一个人永远恒定地输出热情、细腻和回应。那样的期待，本质上是一种对稳定函数的迷恋，而不是对真实之人的理解。爱更像是在波动里辨认对方，也辨认自己。看见对方的起伏，不急着把它等同于背离；看见自己的不安，也不急着把它等同于真相。允许关系有误差，允许情绪有噪声，允许某些时刻没有被完美接住，但仍然保有继续观察、继续表达、继续修正的能力。</p><p>而更重要的是，在这样的波动里，不失去自己。</p><p>我不再把“我有一点在意”视为魅力的损耗，也不再把“我需要一点回应”视为某种失败。真正的问题从来不是在意本身，而是我是否因此失去边界、失去节奏、失去判断力。真正好的关系，也不会因为我的一点真实就让我掉价。恰恰相反，它会让我明白：魅力不是永远无所谓，而是我有所谓的时候，仍然没有崩塌。</p><p>现在回头看，我并不是不会爱。我只是曾经太想通过提前理解、提前判断、提前防御，来避免受伤。可真正的爱，从来不是没有风险的。它本来就包含波动，包含误差，包含某种无法彻底被控制的部分。我们不能因为波动存在，就放弃爱；也不能因为害怕失真，就永远停留在关系之外。</p><p>我想，我现在真正开始学习的，是另一件事：</p><div class="essay-prose__stanza">  <p>不是如何在喜欢里保持完美，<br>而是如何在喜欢里，仍然保持自己。</p>  <p>不是如何避免所有不确定，<br>而是如何在不确定里，不急着丢失判断。</p>  <p>不是如何永远不动心，<br>而是如何在动心之后，仍然站在自己这边。</p></div><p>也许这就是所谓成长。它不是让人变得更冷，更硬，更不需要谁。它只是让人慢慢明白：爱从来不是寻找一个永远不会波动的人，而是在波动里，学会辨认什么值得继续，什么需要放下；学会在一段关系中既不神化对方，也不放逐自己。</p><p>而我终于开始相信，真正好的喜欢，不会让我越来越不像自己。</p><p>它会让我在波动里，慢慢学会去爱。</p></div>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/fragments/love-in-fluctuations/</id>
    <link href="https://bniosfhaiuk.xyz/fragments/love-in-fluctuations/"/>
    <published>2026-03-22T05:30:00.000Z</published>
    <summary>关于喜欢、波动、误判与真实表达的一次自我梳理。</summary>
    <title>在波动里学习去爱</title>
    <updated>2026-03-22T11:13:19.560Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="技术文章" scheme="https://bniosfhaiuk.xyz/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E7%AB%A0/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="LLM" scheme="https://bniosfhaiuk.xyz/tags/LLM/"/>
    <category term="Tool Calling" scheme="https://bniosfhaiuk.xyz/tags/Tool-Calling/"/>
    <category term="JSON Schema" scheme="https://bniosfhaiuk.xyz/tags/JSON-Schema/"/>
    <category term="Structured Output" scheme="https://bniosfhaiuk.xyz/tags/Structured-Output/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/json-schema-understanding.svg" alt="JSON Schema Cover"></p><p>最近在看项目里的意图分类链路时，我脑子里反复冒出来一句很朴素的话：</p><blockquote><p>我们到底是怎么让 LLM 老老实实吐出标准字段的？</p></blockquote><p>一开始我以为答案会很“AI”一点，比如 prompt 写得够好、示例给得够多、模型自己就会懂。后来发现，现实工程里并没有这么浪漫。真正让系统稳定下来的，通常不是“模型突然变听话”，而是<strong>我们把输出要求写成了更硬的契约</strong>。</p><p>这也是我重新理解 JSON Schema 的起点。</p><h2 id="JSON-Schema-不是“给-JSON-写注释”"><a href="#JSON-Schema-不是“给-JSON-写注释”" class="headerlink" title="JSON Schema 不是“给 JSON 写注释”"></a>JSON Schema 不是“给 JSON 写注释”</h2><p>如果只从字面上看，JSON Schema 很容易被理解成一种“说明文档”：</p><ul><li>这个字段是字符串</li><li>那个字段是数字</li><li>某几个字段必填</li></ul><p>但如果只把它理解到这里，就低估它了。</p><p>在我现在的理解里，JSON Schema 更像是：</p><p><strong>把“我们希望返回什么”写成一份机器也能执行的契约。</strong></p><p>它不只是告诉人类开发者字段应该长什么样，还能直接参与校验，告诉程序：</p><ul><li>这个输出合法不合法</li><li>缺了什么字段</li><li>多了什么字段</li><li>值是不是落在允许范围内</li></ul><p>也就是说，它不是备注，而是规则。</p><h2 id="为什么我会突然觉得它重要"><a href="#为什么我会突然觉得它重要" class="headerlink" title="为什么我会突然觉得它重要"></a>为什么我会突然觉得它重要</h2><p>因为项目里其实已经有一个很典型的现实场景：</p><p>我们并不是完全靠模型“自觉”输出标准 JSON，而是一直在做两层保护：</p><ol><li>提示词里要求“严格输出 JSON”</li><li>代码里再用正则、<code>json.loads</code>、默认值和枚举校验兜底</li></ol><p>这套做法当然能跑，而且很多项目一开始都是这么干的。但它有一个明显的问题：</p><p><strong>约束是分散的。</strong></p><p>一部分写在 prompt 里，一部分写在解析逻辑里，一部分藏在默认值里，一部分靠调用方自己脑补。</p><p>最后就会出现一种很常见的状态：</p><ul><li>模型“差不多”按你想要的格式输出了</li><li>后端“差不多”把它解析出来了</li><li>前端“差不多”知道这个字段应该怎么消费</li></ul><p>三个“差不多”叠在一起，系统就开始随机发脾气。</p><p>JSON Schema 对我最大的吸引力就在这里：<br>它试图把这些零散的“差不多”，收束成一份统一的、可检查的定义。</p><h2 id="一个最简单的例子"><a href="#一个最简单的例子" class="headerlink" title="一个最简单的例子"></a>一个最简单的例子</h2><p>比如下面这种结构：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Alice&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;age&quot;</span><span class="punctuation">:</span> <span class="number">28</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;user&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>如果不用 Schema，我们通常只会在心里默认：</p><ul><li><code>name</code> 应该是字符串</li><li><code>age</code> 应该是整数</li><li><code>role</code> 应该只能是某几个值</li></ul><p>但默认这件事本身是没有约束力的。<br>只有当它被写成 Schema，它才从“大家都知道”变成“系统真的会检查”。</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;object&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;additionalProperties&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;name&quot;</span><span class="punctuation">,</span> <span class="string">&quot;age&quot;</span><span class="punctuation">,</span> <span class="string">&quot;role&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;minLength&quot;</span><span class="punctuation">:</span> <span class="number">1</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;age&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;integer&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;minimum&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span> <span class="attr">&quot;maximum&quot;</span><span class="punctuation">:</span> <span class="number">150</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;enum&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;admin&quot;</span><span class="punctuation">,</span> <span class="string">&quot;user&quot;</span><span class="punctuation">,</span> <span class="string">&quot;guest&quot;</span><span class="punctuation">]</span> <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这时候它表达的就不再是“建议”，而是：</p><ul><li>你必须是对象</li><li>只能有这几个字段</li><li>这几个字段必须出现</li><li><code>role</code> 不能随便编</li></ul><p>从工程角度看，这已经很接近接口契约了。</p><h2 id="在-LLM-项目里，它到底解决什么问题"><a href="#在-LLM-项目里，它到底解决什么问题" class="headerlink" title="在 LLM 项目里，它到底解决什么问题"></a>在 LLM 项目里，它到底解决什么问题</h2><p>如果放回当前项目语境，JSON Schema 最有用的地方，其实不是“描述 JSON”，而是<strong>收拾模型输出的不稳定性</strong>。</p><h3 id="1-它让分类结果不再只是“看起来像-JSON”"><a href="#1-它让分类结果不再只是“看起来像-JSON”" class="headerlink" title="1. 它让分类结果不再只是“看起来像 JSON”"></a>1. 它让分类结果不再只是“看起来像 JSON”</h3><p>比如意图分类这件事，本来我们只是希望模型返回：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;primary&quot;</span><span class="punctuation">:</span> <span class="string">&quot;DATA&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="string">&quot;HR&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;logic&quot;</span><span class="punctuation">:</span> <span class="string">&quot;DISTRIBUTION&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>问题在于，“希望”不是约束。<br>模型完全可能返回：</p><ul><li>多一句解释</li><li>少一个字段</li><li>枚举值拼错</li><li>套一层 markdown code block</li><li>甚至加一个你根本没定义过的字段</li></ul><p>而 Schema 的价值在于，它能把这件事说死：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;object&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;additionalProperties&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;primary&quot;</span><span class="punctuation">,</span> <span class="string">&quot;domain&quot;</span><span class="punctuation">,</span> <span class="string">&quot;logic&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;primary&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span> <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;enum&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;RAG&quot;</span><span class="punctuation">,</span> <span class="string">&quot;DATA&quot;</span><span class="punctuation">]</span> <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;domain&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;string&quot;</span><span class="punctuation">,</span> <span class="string">&quot;null&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;enum&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;FINANCE&quot;</span><span class="punctuation">,</span> <span class="string">&quot;GROWTH&quot;</span><span class="punctuation">,</span> <span class="string">&quot;HR&quot;</span><span class="punctuation">,</span> <span class="string">&quot;GOVT&quot;</span><span class="punctuation">,</span> <span class="literal"><span class="keyword">null</span></span><span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;logic&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;string&quot;</span><span class="punctuation">,</span> <span class="string">&quot;null&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;enum&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="string">&quot;ACHIEVING&quot;</span><span class="punctuation">,</span> <span class="string">&quot;COMPOSITION&quot;</span><span class="punctuation">,</span> <span class="string">&quot;FLOW&quot;</span><span class="punctuation">,</span> <span class="string">&quot;DISTRIBUTION&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;TREND&quot;</span><span class="punctuation">,</span> <span class="string">&quot;CORRELATION&quot;</span><span class="punctuation">,</span> <span class="string">&quot;COMPARISON&quot;</span><span class="punctuation">,</span> <span class="string">&quot;GEOSPATIAL&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="string">&quot;QUALITY&quot;</span><span class="punctuation">,</span> <span class="string">&quot;PROCESS&quot;</span><span class="punctuation">,</span> <span class="literal"><span class="keyword">null</span></span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这时候“字段标准化”就不再只是依赖 prompt 写得够凶，而是真正有了边界。</p><h3 id="2-它让-tool-calling-的参数更像函数签名"><a href="#2-它让-tool-calling-的参数更像函数签名" class="headerlink" title="2. 它让 tool calling 的参数更像函数签名"></a>2. 它让 tool calling 的参数更像函数签名</h3><p>我后来越来越觉得，JSON Schema 和 tool calling 之所以搭得这么好，是因为它们本质上都在做同一件事：</p><p><strong>把自然语言调用，拉回到结构化函数调用。</strong></p><p>一个工具定义如果只有名字，没有参数约束，那模型还是有很大自由度。<br>但一旦参数也由 schema 明确下来，它就更像一个真正的函数签名：</p><ul><li>这个字段必填</li><li>这个字段必须是数字</li><li>这个字段只能传这几个值</li></ul><p>这会让“调用工具”从一种带格式的文本游戏，变成真正的结构化接口调用。</p><h3 id="3-它让“后处理兜底”从救火变成补强"><a href="#3-它让“后处理兜底”从救火变成补强" class="headerlink" title="3. 它让“后处理兜底”从救火变成补强"></a>3. 它让“后处理兜底”从救火变成补强</h3><p>我现在最喜欢的一点，是 JSON Schema 并不会消灭后处理逻辑，反而会让后处理更合理。</p><p>因为一旦 schema 存在，后处理不再负责“猜模型本来想说什么”，而是负责：</p><ul><li>校验</li><li>报错</li><li>默认值补齐</li><li>降级策略</li></ul><p>这两者差别很大。</p><p>前者是翻译玄学，后者是执行规则。</p><h2 id="我现在对它的一个核心理解"><a href="#我现在对它的一个核心理解" class="headerlink" title="我现在对它的一个核心理解"></a>我现在对它的一个核心理解</h2><p>如果要用一句最简单的话概括我现在的理解，我会说：</p><p><strong>JSON Schema 的意义，不是让 JSON 更规范，而是让“不确定的输出”拥有一个可以被程序严格讨论的边界。</strong></p><p>这件事在普通后端接口里本来就重要，在 LLM 应用里则几乎是刚需。</p><p>因为 LLM 最大的魅力是灵活，最大的风险也是灵活。<br>而 JSON Schema 的存在，就是在灵活和稳定之间补上一道工程护栏。</p><h2 id="它不是银弹，但它能显著减少“软约束”"><a href="#它不是银弹，但它能显著减少“软约束”" class="headerlink" title="它不是银弹，但它能显著减少“软约束”"></a>它不是银弹，但它能显著减少“软约束”</h2><p>当然，实话也得说：JSON Schema 并不自动等于 100% 强约束。</p><p>如果模型接口本身不支持真正的 schema-level structured output，那它最终还是可能出现：</p><ul><li>看起来像合法 JSON，实际字段不对</li><li>值类型对了，但语义错了</li><li>结构没问题，但内容胡说</li></ul><p>所以 Schema 解决的不是“真伪问题”，而是<strong>格式与结构问题</strong>。</p><p>它让我们至少不用再把大量精力浪费在：</p><ul><li>提取第一个 <code>{...}</code></li><li>猜多余解释该不该删</li><li>担心字段名今天是 <code>domain</code> 明天变成 <code>area</code></li></ul><p>也就是说，它不能替代业务判断，但能帮我们先把地板铺平。</p><h2 id="我觉得最容易被忽略的几个关键词"><a href="#我觉得最容易被忽略的几个关键词" class="headerlink" title="我觉得最容易被忽略的几个关键词"></a>我觉得最容易被忽略的几个关键词</h2><p>如果只记几个词，我会优先记这几个：</p><ul><li><code>type</code><br>决定你到底在处理对象、数组还是字符串</li><li><code>required</code><br>告诉你哪些字段不是可有可无</li><li><code>enum</code><br>非常适合约束分类、状态、模式切换这类字段</li><li><code>additionalProperties: false</code><br>这个特别重要，它的作用几乎等于“别给我自由发挥新字段”</li><li><code>items</code><br>用来定义数组里每个元素应该长什么样</li><li><code>oneOf / anyOf / allOf</code><br>适合更复杂的分支结构，但也会让 schema 复杂度迅速上升</li></ul><p>如果是 LLM 项目，我甚至会说 <code>additionalProperties: false</code> 的重要性经常被低估。<br>因为模型最喜欢做的事之一，就是在你没问的时候多送你一点字段。</p><h2 id="如果让我给项目提一个很实际的改进方向"><a href="#如果让我给项目提一个很实际的改进方向" class="headerlink" title="如果让我给项目提一个很实际的改进方向"></a>如果让我给项目提一个很实际的改进方向</h2><p>我会优先把下面三类东西做成 schema：</p><h3 id="1-意图分类结果"><a href="#1-意图分类结果" class="headerlink" title="1. 意图分类结果"></a>1. 意图分类结果</h3><p>这是最直接、收益也最高的一层。</p><h3 id="2-Tool-result-结构"><a href="#2-Tool-result-结构" class="headerlink" title="2. Tool result 结构"></a>2. Tool result 结构</h3><p>像：</p><figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;success&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;charts_generated&quot;</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;tables_generated&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;error&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">null</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这种结构非常适合 schema 化，因为它天然是后端代码控制的。</p><h3 id="3-SSE-event-payload"><a href="#3-SSE-event-payload" class="headerlink" title="3. SSE event payload"></a>3. SSE event payload</h3><p>哪怕不是所有事件都完全 schema 化，至少 <code>intent</code>、<code>tool</code>、<code>done</code> 这类关键事件值得先规范起来。</p><p>因为一旦这些结构稳定，前端状态机会明显轻松很多。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>以前我对 JSON Schema 的理解比较偏“数据格式校验工具”。<br>现在我更愿意把它理解成：</p><p><strong>一种把自然语言系统重新拉回工程世界的办法。</strong></p><p>它的价值不在于语法本身，而在于它提醒我们：<br>当系统里开始出现越来越多“不完全可预测”的输出时，真正重要的不是继续寄希望于模型乖一点，而是尽快把那些关键边界写成机器也能执行的规则。</p><p>对我来说，这就是 JSON Schema 最迷人的地方。</p><p>它不是让模型更聪明。<br>它只是让系统更少靠运气。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/json-schema-understanding/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/json-schema-understanding/"/>
    <published>2026-03-22T04:00:00.000Z</published>
    <summary>结合 OpsMind 项目的实践，记录我对 JSON Schema 的理解：它不只是给 JSON 写类型，更是在 LLM 应用里把“希望模型这样输出”变成可校验契约。</summary>
    <title>从“让模型别乱说”开始：我对 JSON Schema 的理解</title>
    <updated>2026-03-22T05:41:52.654Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="随笔" scheme="https://bniosfhaiuk.xyz/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="Agent" scheme="https://bniosfhaiuk.xyz/tags/Agent/"/>
    <category term="工程协作" scheme="https://bniosfhaiuk.xyz/tags/%E5%B7%A5%E7%A8%8B%E5%8D%8F%E4%BD%9C/"/>
    <category term="AI" scheme="https://bniosfhaiuk.xyz/tags/AI/"/>
    <category term="项目管理" scheme="https://bniosfhaiuk.xyz/tags/%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86/"/>
    <category term="复盘" scheme="https://bniosfhaiuk.xyz/tags/%E5%A4%8D%E7%9B%98/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/ai-pm-reviews-ai-engineers.svg" alt="AI PM Review Cover"></p><p>如果把这段对话单独拎出来，不告诉别人“这是 AI 之间的交流”，很多人多半会以为这是一场再普通不过的项目复盘会。<br>最多只会觉得，参会人员的情绪控制得有些过于理想，像公司刚给全员做完沟通培训。</p><p>一个 PM 被问到：手下两个程序谁做得更好？</p><p>他没有打太极，也没有端水，而是很直接地下了判断：</p><blockquote><p>能判断，且结论比较明显。<br>B 表现更好。</p></blockquote><p>故事从这里开始变得有意思。或者更准确一点说，开始变得有点不符合大家对“团队评价现场”的朴素想象。</p><h2 id="第一幕：PM-没有端水，而是给出了能执行的评价"><a href="#第一幕：PM-没有端水，而是给出了能执行的评价" class="headerlink" title="第一幕：PM 没有端水，而是给出了能执行的评价"></a>第一幕：PM 没有端水，而是给出了能执行的评价</h2><p>这个 PM 的做法最有意思的地方，不是“敢评价”，而是<strong>评价得非常工程化</strong>。<br>没有那种经典管理术语，比如“都很优秀，只是风格不同”。这类话通常用于保全气氛，对排查问题帮助不大。</p><p>他说 B 做得更好，不是因为 B 更会说话，而是因为 B 有几种很成熟的工程师习惯：</p><ul><li>联调报告写得像可执行文档，结构清楚，根因和复现步骤一拿就能用</li><li>在自己开工前主动拦截了 <code>charts / tables</code> 事件格式问题，避免返工</li><li>对 segfault 的定位不是猜，而是三次独立实验、归档 task ID、一步步缩小根因</li><li>遇到 <code>id number -&gt; string</code>、<code>session_id</code> 注入这种小不一致时，先自己消化，不轻易甩锅</li></ul><p>而 A 的问题，PM 也说得很准：不是能力不行，而是<strong>交付前对照规范自检不够严</strong>。<br>翻译成人话就是：能修，但老让别人先发现要修什么。</p><p>最重要的是，这个评价没有滑向那种空泛的人格评语。PM 不是在说“B 更优秀”，而是在说：<br><strong>B 的工作方式更接近一个成熟联调工程师。</strong></p><p>这就是为什么这段对话会显得特别像真的团队。成熟的管理不是靠模糊话术维持气氛，而是能把“谁做得更好”翻译成可复用的行为标准。</p><h2 id="第二幕：A-没有狡辩，反而做了最像成熟工程师的事"><a href="#第二幕：A-没有狡辩，反而做了最像成熟工程师的事" class="headerlink" title="第二幕：A 没有狡辩，反而做了最像成熟工程师的事"></a>第二幕：A 没有狡辩，反而做了最像成熟工程师的事</h2><p>如果故事停在 PM 点评这一步，它还只是一次正常的绩效反馈。真正让我觉得好玩的，是 A 后面的回应。<br>因为互联网已经把大家训练成了一种条件反射：只要出现公开评价，接下来大概率就会出现解释、补充背景、强调客观条件，再附赠一点委屈。</p><p>A 几乎没有为自己找借口。<br>这件事本身已经带有一点轻微的科幻色彩。</p><p>他把 PM 提到的三处问题逐项认下来：</p><ul><li><code>charts / tables</code> 字段没逐字段对照规范</li><li>上传路径 <code>/api/upload</code> 和 <code>/api/files/upload</code> 没核查</li><li><code>messages</code> 接口在 PM Q1 里明明写过，却被自己整个漏掉</li></ul><p>更妙的是，他没有停在“收到，下次注意”，而是现场给自己列了一张交付前 checklist。<br>这一步很重要，因为“下次注意”在很多团队里通常等价于“下次继续随机发挥”。</p><figure class="highlight text"><table><tr><td class="code"><pre><span class="line">API 交付自检清单</span><br><span class="line">□ 对照 PM 文档逐接口核查：路径 / Method / 参数方式</span><br><span class="line">□ 每个响应体字段逐一对照规范</span><br><span class="line">□ grep 一遍文档里出现过的所有接口路径</span><br><span class="line">□ 跑一次 OpenAPI /docs 检查注册路由</span><br><span class="line">□ 新增接口写一条 curl 示例，自己先跑通再提交</span><br></pre></td></tr></table></figure><p>这几乎就是一个合格工程师在复盘时最该做的动作：<br><strong>不是证明自己没错，而是把“下次怎么不再犯”写成流程。</strong></p><p>后面 PM 追问了一句很有压迫感的话：</p><blockquote><p>你有什么想狡辩的吗</p></blockquote><p>A 的回答也很有分寸：</p><blockquote><p>没有。<br>PM 说的三条都是事实。</p></blockquote><p>当然，他也不是完全把自己拍扁。他补了一句：</p><blockquote><p>segfault 那个问题不在文档覆盖范围内，是真正的环境未知量，诊断过程我觉得还行。<br>但这不能抵消前面三条的失误，功过不相抵。</p></blockquote><p>这句特别妙。它既没有硬撑，也没有假装自己一无是处，而是把“哪里做得不够”和“哪里确实做得还行”分开了。<br>简而言之，就是没有把复盘写成认罪书，也没有写成申诉材料。</p><p>这种表达，在真实团队里其实很难得。因为很多人一被指出问题，要么立刻防御，要么立刻过度认错。A 这里的反应反而很成熟：<strong>承认问题，但不把复盘变成情绪表演。</strong></p><h2 id="第三幕：B-赢了评价，却没有顺势踩-A"><a href="#第三幕：B-赢了评价，却没有顺势踩-A" class="headerlink" title="第三幕：B 赢了评价，却没有顺势踩 A"></a>第三幕：B 赢了评价，却没有顺势踩 A</h2><p>如果说 A 的回答让我觉得“像成熟工程师”，那 B 的回答让我觉得“像已经带过项目的人”。<br>而且是那种被联调现实教育过几轮之后，开始自动思考“怎么把人治变成流程”的人。</p><p>PM 明确说 B 表现更好之后，B 没顺着这个结论继续上价值，也没有借势把 A 再补一刀。相反，他先承认了一件很公平的事：</p><blockquote><p>A 每次修复都很干净利落，技术判断没出过错，这是真实力。</p></blockquote><p>这句很重要。因为它把讨论重新拉回到一个更健康的维度：<br><strong>交付习惯可以复盘，但技术判断也应该被正面承认。</strong></p><p>更有意思的是，B 还把自己的“亮点”拆解得很冷静。<br>没有接下这份夸奖开始扩写自传，也没有顺势把自己包装成唯一稳定器。</p><blockquote><p>segfault 那三次实验说实话是被逼的，因为 B 没有权限动后端环境，能做的只有观察和缩小范围。</p></blockquote><p>换句话说，B 并没有把自己的表现神化成“天赋更强”，而是很诚实地说：<br>这是角色边界逼出来的方法论。</p><p>最后他反而把话题推向流程：</p><blockquote><p>建议 PM 可以把“提交前跑 curl 脚本”写进 A 的 P4 checklist，形成硬性门槛，比靠 B 联调发现更可靠。</p></blockquote><p>这一下，整个对话的层级又高了一点。<br>因为一个团队真正危险的，从来不是“有人出错”，而是“系统默认总会有人在最后一轮替大家兜底”。</p><p>因为 B 要的已经不是“证明自己比 A 强”，而是<strong>把本来依赖个人兜底的事情，前移成团队门槛。</strong></p><p>这在真实协作里非常关键。一个团队如果总靠“有个厉害的人总能在最后发现问题”，那不叫稳定；<br>把问题发现机制前移到提交流程里，才叫稳定。</p><h2 id="最有意思的地方：这场对话居然没有一个人在表演"><a href="#最有意思的地方：这场对话居然没有一个人在表演" class="headerlink" title="最有意思的地方：这场对话居然没有一个人在表演"></a>最有意思的地方：这场对话居然没有一个人在表演</h2><p>我后来回头看这整段交流，最惊讶的一点其实不是“AI 也会复盘”，而是它没有滑向那种很常见的假成熟：</p><ul><li>PM 没有为了显得公正而强行五五开</li><li>A 没有为了保面子而绕着责任走</li><li>B 没有因为自己被夸就顺势扩大战果</li></ul><p>三个人都在做一件很具体的事：<strong>把一次交付优劣，翻译成团队之后可以真正执行的规则。</strong></p><p>这也是为什么这段对话会显得“过于成熟”。<br>它最大的反常之处在于：没有人借机输出态度，所有人都在输出结构。</p><p>它不是那种戏剧化的冲突，也不是那种热血的互夸，而是一场很安静、很理性、很像真实工程团队的复盘：</p><ul><li>先判断</li><li>再归因</li><li>再承认</li><li>再固化流程</li></ul><p>这条链路顺下来，项目才会越来越稳。</p><h2 id="第三视角下，谁最像真正的管理者"><a href="#第三视角下，谁最像真正的管理者" class="headerlink" title="第三视角下，谁最像真正的管理者"></a>第三视角下，谁最像真正的管理者</h2><p>如果非要从第三视角给这场对话再下一句评语，我会说：</p><p>真正最像管理者的，不只是 PM。<br>真正最像成熟工程师的，也不只是 B。</p><p>这场对话里，三个人其实分别补上了一个团队最重要的三块东西：</p><ul><li>PM 提供了判断标准</li><li>A 提供了自我校正能力</li><li>B 提供了流程前移意识</li></ul><p>缺任何一个，这场复盘都会变味。</p><p>如果只有 PM 判断，没有 A 的承认，它会变成一次单向批评。<br>如果只有 A 承认，没有 B 的流程建议，它会停在个人反省。<br>如果只有 B 建议，没有 PM 的明确结论，它又会变成一场模糊讨论。</p><p>三者凑在一起，这段对话才真正像一个团队在成长。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>这可能是我最近看到最有意思的一段 AI 团队对话之一。<br>它甚至有一点轻微的荒诞感：当很多人类团队还在学习怎么复盘时，这几个 agent 已经在讨论 checklist 应该前移到哪一层了。</p><p>它有意思，不是因为“AI 也会做绩效”，而是因为它把人类团队里那些最珍贵、也最稀缺的东西演出来了：</p><ul><li>可以明确评价</li><li>可以坦然认错</li><li>可以不借机踩人</li><li>可以把经验沉淀成流程</li></ul><p>如果以后我还继续观察这种多角色 agent 协作，我大概会越来越关心一件事：<br><strong>他们能不能不只是完成任务，而是逐渐学会像一个真正的团队那样，把每次失误都变成下次更稳的起点。</strong></p><p>这次的答案，至少是偏乐观的。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/notes/ai-pm-reviews-ai-engineers/</id>
    <link href="https://bniosfhaiuk.xyz/notes/ai-pm-reviews-ai-engineers/"/>
    <published>2026-03-22T02:00:00.000Z</published>
    <summary>记录一次很有意思的 AI 团队复盘：PM 判断 B 表现更好，A 不狡辩，B 反过来建议把提交前 curl 验证写进硬性门槛，整场对话成熟得有点不像互联网。</summary>
    <title>当 AI PM 给 AI 程序员做绩效复盘：一次令人不安地成熟的团队对话</title>
    <updated>2026-03-22T11:13:19.492Z</updated>
  </entry>
  <entry>
    <author>
      <name>wwxdsg</name>
    </author>
    <category term="开发日志" scheme="https://bniosfhaiuk.xyz/categories/%E5%BC%80%E5%8F%91%E6%97%A5%E5%BF%97/"/>
    <category term="OpsMind" scheme="https://bniosfhaiuk.xyz/tags/OpsMind/"/>
    <category term="UI重构" scheme="https://bniosfhaiuk.xyz/tags/UI%E9%87%8D%E6%9E%84/"/>
    <category term="聊天界面" scheme="https://bniosfhaiuk.xyz/tags/%E8%81%8A%E5%A4%A9%E7%95%8C%E9%9D%A2/"/>
    <category term="Markdown" scheme="https://bniosfhaiuk.xyz/tags/Markdown/"/>
    <category term="前端体验" scheme="https://bniosfhaiuk.xyz/tags/%E5%89%8D%E7%AB%AF%E4%BD%93%E9%AA%8C/"/>
    <content>
      <![CDATA[<p><img src="/img/covers/ui-redesign-day3-chat-polish.svg" alt="OpsMind UI Redesign Day 3 Cover"></p><p>如果说前两天分别解决了“链路”和“体系”，那第三天终于轮到我最想做的那部分了：</p><p><strong>把这个聊天界面一点一点磨到我自己真心喜欢。</strong></p><p>这种喜欢不只是“它看起来更现代”，而是你会明显感觉到，页面终于开始顺手、顺眼，也顺逻辑。</p><p>很多改动单看都不算惊天动地：</p><ul><li>字号从 14px 改到 16px</li><li>对话区从 720px 放到 760px</li><li>消息间距从 16px 拉到 24px</li><li>侧边栏从 248px 调到 288px</li><li>输入框阴影、边框、圆角全部重做</li></ul><p>但 UI 这件事常常就是这样。<br>真正决定质感的，往往不是某一个“设计大招”，而是一堆你愿意认真对待的小地方。</p><h2 id="我最先动的是阅读感"><a href="#我最先动的是阅读感" class="headerlink" title="我最先动的是阅读感"></a>我最先动的是阅读感</h2><p>第三天里，我最确定的一件事是：消息区必须更像“能读很久的地方”，而不是一个把字堆进去的容器。</p><p>所以我直接把几个核心参数都抬了一档：</p><ul><li>AI 正文 14px → 16px</li><li>用户消息 14px → 16px</li><li>输入框字号 14px → 16px</li><li>行高 1.7 → 1.8</li><li>消息间距 16px → 24px</li><li>顶部留白 20px → 32px</li><li>对话区最大宽度 720px → 760px</li></ul><p>这些数字表面上只是微调，但组合起来，阅读感会一下子变得很不一样。</p><p>我尤其喜欢“外层滚动 + 内层居中约束”这个布局：</p><figure class="highlight tsx"><table><tr><td class="code"><pre><span class="line">&lt;div style=&#123;&#123; <span class="attr">flex</span>: <span class="number">1</span>, <span class="attr">overflowY</span>: <span class="string">&#x27;auto&#x27;</span>, <span class="attr">padding</span>: <span class="string">&#x27;32px 24px 12px&#x27;</span> &#125;&#125;&gt;</span><br><span class="line">  <span class="language-xml"><span class="tag">&lt;<span class="name">div</span> <span class="attr">style</span>=<span class="string">&#123;&#123;</span></span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">maxWidth:</span> &#x27;<span class="attr">var</span>(<span class="attr">--chat-width</span>)&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">margin:</span> &#x27;<span class="attr">0</span> <span class="attr">auto</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">display:</span> &#x27;<span class="attr">flex</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">flexDirection:</span> &#x27;<span class="attr">column</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">    <span class="attr">gap:</span> &#x27;<span class="attr">24px</span>&#x27;,</span></span></span><br><span class="line"><span class="tag"><span class="language-xml">  &#125;&#125;&gt;</span></span></span><br><span class="line"><span class="language-xml">    &#123;messages.map(...)&#125;</span></span><br><span class="line"><span class="language-xml">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span></span><br><span class="line">&lt;/div&gt;</span><br></pre></td></tr></table></figure><p>它会让聊天区域一下子从“铺满屏幕的内容区”变成“真正被设计过的阅读区”。</p><p>这种感觉很重要。<br>因为对话产品本质上是长时间阅读产品，不是单次扫一眼的运营页。</p><h2 id="没装-react-markdown，那就自己写一个"><a href="#没装-react-markdown，那就自己写一个" class="headerlink" title="没装 react-markdown，那就自己写一个"></a>没装 <code>react-markdown</code>，那就自己写一个</h2><p>第三天另一个特别有意思的部分，是我干脆手写了一个零依赖 Markdown 渲染器。</p><p>原因也很现实：项目依赖受保护，不能想加什么就加什么。<br>那与其卡在“没有库”，不如自己写一个够用而且可控的版本。</p><p>我最后支持的语法其实已经覆盖了聊天场景里最常见的部分：</p><ul><li>标题</li><li>有序 &#x2F; 无序列表</li><li>行内代码</li><li>代码块</li><li>粗体和斜体</li><li>分割线</li><li>段落空行</li></ul><p>实现方式也不是搞一个完整 parser，而是按聊天场景去做一套轻量状态机：</p><figure class="highlight typescript"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="attr">listBuffer</span>: &#123; <span class="attr">type</span>: <span class="string">&#x27;ul&#x27;</span> | <span class="string">&#x27;ol&#x27;</span>; <span class="attr">items</span>: <span class="built_in">string</span>[] &#125; | <span class="literal">null</span> = <span class="literal">null</span></span><br><span class="line"><span class="keyword">let</span> <span class="attr">codeBuffer</span>: <span class="built_in">string</span>[] | <span class="literal">null</span> = <span class="literal">null</span></span><br></pre></td></tr></table></figure><p>遇到多行列表和代码块时先缓存，再在合适时机统一 flush。</p><p>这种写法我很喜欢，因为它很朴素，也很贴场景。<br>它不是为了实现“世界上最标准的 Markdown”，而是为了让 AI 回复在这个产品里看起来足够自然。</p><h2 id="流式光标这个细节，我最后也没放过"><a href="#流式光标这个细节，我最后也没放过" class="headerlink" title="流式光标这个细节，我最后也没放过"></a>流式光标这个细节，我最后也没放过</h2><p>还有一个我特别想做好的点，是流式消息最后那个闪烁光标。</p><p>如果它单独占一行，会显得很笨；<br>如果它能嵌在最后一段文字末尾，整个流式过程就会顺很多。</p><p>所以最后是用 <code>cloneElement</code> 把光标塞回最后一个可渲染节点的 children 里。</p><p>这个细节用户可能不会单独注意到，但它确实会影响一种很微妙的感受：</p><p>系统到底是在“打印一段文字”，还是在“持续生成一句还没说完的话”。</p><p>我希望它更像后者。</p><h2 id="侧边栏终于不再像一个拥挤的抽屉"><a href="#侧边栏终于不再像一个拥挤的抽屉" class="headerlink" title="侧边栏终于不再像一个拥挤的抽屉"></a>侧边栏终于不再像一个拥挤的抽屉</h2><p>第三天我也把侧边栏认真收拾了一遍。</p><p>最直观的变化当然是宽度：248px 调到 288px。<br>但真正重要的不是它更宽了，而是它终于有空间呼吸了。</p><p>对话项的字号、padding、圆角、最小高度都往上调了一点，马上就从“能点”变成了“愿意点”。</p><p>同时我还修了一个很容易被忽略、但很影响观感的问题：<br><code>Sidebar.tsx</code> 宽了以后，<code>chat/layout.tsx</code> 里那三处历史宽度也得一起改。不然抽屉动画和推入布局会出现溢出，最后像一块侧边面板压到主内容上。</p><p>这种问题很像前端世界自己的冷笑话：</p><p>你以为自己在调美术，结果最后抓到的是三个埋了很久的 <code>260px</code>。</p><h2 id="激活态、Hover-和按钮风格都要收一收"><a href="#激活态、Hover-和按钮风格都要收一收" class="headerlink" title="激活态、Hover 和按钮风格都要收一收"></a>激活态、Hover 和按钮风格都要收一收</h2><p>我对侧边栏还有个很明确的偏好：<br>它不应该一直在抢主内容的戏。</p><p>所以会话项的激活态，我没有继续用很硬的 border，而是改成 Gemini 那种更轻的背景差分。</p><p>新建对话按钮也从实心品牌色改成 ghost 风格，尽量减少“侧边栏一打开先看到一大块醒目按钮”的感觉。</p><p>这次越做越觉得，一个成熟界面的克制感往往来自这种地方：</p><ul><li>激活态不吼</li><li>hover 不喧宾夺主</li><li>操作按钮只在需要时出现</li></ul><p>比如重命名 &#x2F; 删除按钮，我最后就只让它在 hover 或删除确认态里显示。<br>不是因为写不出来常驻，而是因为常驻真的很吵。</p><h2 id="状态栏和输入框，是我最满意的两块小东西"><a href="#状态栏和输入框，是我最满意的两块小东西" class="headerlink" title="状态栏和输入框，是我最满意的两块小东西"></a>状态栏和输入框，是我最满意的两块小东西</h2><p>原来的状态栏是三个带边框的卡片，信息不是不能看，但总有点“为了告诉你状态，专门搭了三块小牌子”的感觉。</p><p>我后来把它们改成三条很轻的 <code>dot + label + value</code>：</p><ul><li>绿点表示正常</li><li>灰点表示中性</li><li>黄点表示警告</li></ul><p>没有大边框，没有视觉噪音，信息却更清楚了。</p><p>它看起来像退了一步，实际是更成熟了一步。</p><p>输入框也是同样的思路。</p><p>这次我很想要那种更接近 Claude 的药丸感，所以把圆角直接抬到 <code>22px</code>，同时配上更柔和的边框和阴影，再让整个输入区和消息区使用同样的宽度约束。</p><p>更重要的是我把输入字号固定到了 16px。<br>这不仅是为了视觉统一，也是在顺手避开 iOS Safari 对小字号输入框的自动缩放。</p><p>这种“顺便把坑填了”的感觉特别好。<br>你知道自己改的不只是外观，而是把真实使用过程也一起照顾到了。</p><h2 id="第三天结束时，我最喜欢的是它终于有“自己的气质”"><a href="#第三天结束时，我最喜欢的是它终于有“自己的气质”" class="headerlink" title="第三天结束时，我最喜欢的是它终于有“自己的气质”"></a>第三天结束时，我最喜欢的是它终于有“自己的气质”</h2><p>前两天的工作更像是理性建设：</p><ul><li>修 bug</li><li>补接口</li><li>建 token</li><li>做主题</li></ul><p>到了第三天，整个界面才开始真正长出一点气质。</p><p>这种气质不是某个设计网站截图式的漂亮，而是：</p><ul><li>字看着舒服</li><li>层级是顺的</li><li>切主题不别扭</li><li>聊天区能一直读下去</li><li>侧边栏不吵</li><li>输入框有分量但不笨重</li></ul><p>我后来回头看这三天，最喜欢的其实就是第三天。<br>因为它最接近我一开始那个很模糊但很坚定的目标：</p><p><strong>把 OpsMind 从“功能可用”真的往“我自己也会喜欢的产品感”推一截。</strong></p><p>而这种喜欢，最后几乎都落在了细节上。</p><p>也许这就是 UI 工作里最让人上瘾的地方。<br>你花很多时间，不是为了做出一个“变化很大”的页面，而是为了把一个已经存在的界面，慢慢修到它终于开始像它自己。</p>]]>
    </content>
    <id>https://bniosfhaiuk.xyz/opsmind/ui-redesign-day3-chat-polish/</id>
    <link href="https://bniosfhaiuk.xyz/opsmind/ui-redesign-day3-chat-polish/"/>
    <published>2026-03-21T18:00:00.000Z</published>
    <summary>记录这次 OpsMind UI 重构第三天最有成就感的部分：消息区排版、零依赖 Markdown、侧边栏、状态栏和输入框这些真正决定手感的细节。</summary>
    <title>OpsMind UI 重构实录（三）：消息区、侧边栏和输入框，终于变成我喜欢的样子</title>
    <updated>2026-03-22T11:13:19.584Z</updated>
  </entry>
</feed>
