<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>马晓博</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://blog.maxiaobo.com.cn/</id>
  <link href="https://blog.maxiaobo.com.cn/" rel="alternate"/>
  <link href="https://blog.maxiaobo.com.cn/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 马晓博</rights>
  <subtitle>芋仔的个人技术博客，记录前端开发实践、技术原理思考和日常踩坑总结。</subtitle>
  <title>芋仔的个人博客</title>
  <updated>2026-07-08T10:35:50.888Z</updated>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>我们想试一件事：<strong>能不能让 AI 直接通过 Swagger 调用后端接口，按需做数据分析</strong>。</p><p>理论上是通的——Swagger 里有完整的接口定义，AI 看得懂；拉数据、交叉计算、给出结论这种事 AI 也不缺能力；两边接起来，产品&#x2F;运营想看某个数据、临时排查一次异常、想跨几个接口交叉算一下，直接跟 AI 对话就行，不用提需求等开发拉数。能跑通的话，「临时性数据问题」这种长尾就有机会从开发的工作量里剥离出去。</p><p>这是个<strong>探索性实验</strong>，不一定真会落到团队日常用——但要试，就得有个能跑的基础。AI 直接拿 Swagger 调用接口有个前置问题：我们后端两份 Swagger，加起来接口数接近两千，全量 schema 展开有几十万 tokens，全塞进 context 不现实。</p><p>所以先做了一个本地 MCP 服务，把接口检索 + 调用能力封装出来，让 AI 按需取数据。下面记一下这个 MCP 在开发过程中的关键设计取舍。</p><hr><h2 id="工具集：按需查询，不做全量注入"><a href="#工具集：按需查询，不做全量注入" class="headerlink" title="工具集：按需查询，不做全量注入"></a>工具集：按需查询，不做全量注入</h2><p>最初的诱惑是”反正都是 swagger，干脆把所有接口列表塞进 system prompt 里，AI 就能直接看到全貌”。我量化了一下成本：</p><ul><li>全部接口的<strong>目录信息</strong>（method&#x2F;path&#x2F;summary&#x2F;tag 等几个字段）大概是十万 tokens 级别</li><li>全部接口的<strong>完整 schema</strong>（$ref 展开）大概是几十万 tokens 级别</li><li>单个接口平均几百 tokens，长尾的能到几千</li></ul><p>十万 tokens 是个尴尬的数字——主流模型的预算就二十万左右，光接口列表就吃掉一半，剩下的要装调用结果、中间计算、对话历史、思考过程，很快爆掉。</p><p>最后做成了 4 个独立工具，<strong>把每一步显式暴露给模型</strong>，让 AI 自己决定走几步、走哪一步：</p><pre><code class="hljs plaintext">list_services()                  列出有哪些服务  ↓search_api(&#123; query &#125;)            按关键词检索接口  ↓get_api(&#123; service, name &#125;)       拿完整 schema  ↓call_api(&#123; service, name, ... &#125;) 实际调用</code></pre><p>不藏在一个万能函数里，是为了让 AI 在每个节点都有判断权：search 出来五六个候选，AI 自己看 summary 和 tag 挑一个；schema 长什么样，AI 自己看；要不要发起调用，AI 自己定。哪一步出错，AI 也能定位是哪一步的问题再重试。</p><p>这样设计下来 AI 每次只为真正要用的那一两个接口付 token 成本，目录从不进 context。</p><p><strong>要强调一下：这不是省钱，是可行性问题</strong>——10 万级别的目录、几十万级别的全量 schema 直接进 context，模型干活的空间就被挤没了。怎么装下接口返回的数据？怎么思考多接口的调用顺序？怎么读用户聊到一半的追问？所以”全量注入”不是更贵的方案，是根本走不通的方案，按需查询是唯一能跑的形态。</p><hr><h2 id="鉴权：从手填密码到借用户的浏览器"><a href="#鉴权：从手填密码到借用户的浏览器" class="headerlink" title="鉴权：从手填密码到借用户的浏览器"></a>鉴权：从手填密码到借用户的浏览器</h2><p>鉴权方案改了三次。</p><p><strong>v0</strong> 是最朴素的：让用户把登录后的 cookie 字符串粘到配置文件。两小时就放弃了——cookie 一过期就要重粘，没有人会忍。</p><p><strong>v1</strong> 想做”MCP 自己登录”：把账号、密码、2FA secret 写进配置，启动时调登录接口拿 cookie。两个问题：</p><ul><li>2FA secret（TOTP 种子）落盘是严重的安全降级</li><li>配置项一多，使用门槛就高，团队推广不动</li></ul><p><strong>v2</strong> 是现在的方案：用 Playwright + 真 Chrome 持久化 profile。</p><pre><code class="hljs ts"><span class="hljs-keyword">const</span> ctx = <span class="hljs-keyword">await</span> chromium.<span class="hljs-title function_">launchPersistentContext</span>(<span class="hljs-variable constant_">USER_DATA_DIR</span>, &#123;  <span class="hljs-attr">channel</span>: <span class="hljs-string">&#x27;chrome&#x27;</span>, <span class="hljs-comment">// 用系统装的 Chrome，不下载 Chromium</span>  <span class="hljs-attr">headless</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// 第一次需要让用户看到弹窗登录</span>&#125;)</code></pre><p>第一次调接口时弹一个 Chrome 窗口，指向登录页，用户该输密码输密码，该过 2FA 过 2FA。登录完成后 cookie 落到 <code>mcp/.session/</code>，之后所有调用都从这里读 cookie，对 AI 完全无感。</p><p>这个方案的核心是：<strong>把鉴权的复杂性还给用户的浏览器</strong>。我们自己不存任何密钥，不感知 2FA 算法，不写 SSO 客户端。用户的浏览器已经会处理这些事，凭什么我们要重做一遍？</p><hr><h2 id="中文搜索：2-gram-滑窗-同义词软打分"><a href="#中文搜索：2-gram-滑窗-同义词软打分" class="headerlink" title="中文搜索：2-gram 滑窗 + 同义词软打分"></a>中文搜索：2-gram 滑窗 + 同义词软打分</h2><p>搜索是另一个反复迭代的地方。</p><p>第一版用空格&#x2F;标点分词，按 token AND 匹配。然后用户搜某个中文双词组合零结果，搜其中单个词又有一堆。根因很简单：中文没有空格，整串中文被当成一个 token，而 swagger summary 里用的是另一种近义表达，词面对不上。</p><p>加了 <strong>滑窗分词</strong>：四字查询拆成连续的二字片段（比如 <code>AB</code>&#x2F;<code>BC</code>&#x2F;<code>CD</code>），任一命中都加分。打分从硬 AND 改为软加权，召回上来了。</p><p>第二个问题更隐蔽：用户用概念性中文表达查询时命中率低。比如查询动词是中文（”修改”、”查询”），swagger 里用的是英文（<code>update</code>、<code>query</code>），词面没有交集。</p><p>解法是加一份<strong>同义词表</strong>（<code>synonyms.ts</code>），手工维护 CN↔EN 高频映射：</p><pre><code class="hljs ts">&#123;  <span class="hljs-string">&#x27;修改&#x27;</span>: [<span class="hljs-string">&#x27;update&#x27;</span>, <span class="hljs-string">&#x27;edit&#x27;</span>, <span class="hljs-string">&#x27;modify&#x27;</span>],  <span class="hljs-string">&#x27;查询&#x27;</span>: [<span class="hljs-string">&#x27;query&#x27;</span>, <span class="hljs-string">&#x27;list&#x27;</span>, <span class="hljs-string">&#x27;get&#x27;</span>],  <span class="hljs-comment">// ... 业务名词同样维护一份对照</span>&#125;</code></pre><p>匹配时把查询词通过同义词表扩展，扩展项以 0.6 权重（低于原词）加入 token 池，中文查询就能对上英文。</p><p>中间认真考虑过上 embedding 做向量检索，最后没做。原因是：</p><ul><li>词法 RAG 在我们这个场景已经够用（验证集召回率挺高）</li><li>embedding 引入运行时依赖、向量库、索引重建——维护成本陡增</li><li>同义词表小但覆盖了大多数高频概念</li></ul><hr><h2 id="还没解决的问题：分发"><a href="#还没解决的问题：分发" class="headerlink" title="还没解决的问题：分发"></a>还没解决的问题：分发</h2><p>技术做完了，怎么让别人用上是另一个问题。</p><p>我们的 swagger JSON 是内部接口文档，不想随便外泄。发 npm 包是最丝滑的方式，但 npm 包公开就是公开，包内 JSON 是明文。发私有 npm registry 又要让每个用户配 token，也不是省心的事。</p><p>目前的方案是**保留私有仓库 + 写一个 <code>install.sh</code>**：</p><ol><li>用户 clone 仓库</li><li>跑 <code>./install.sh</code>，脚本检查 bun&#x2F;Chrome，跑 <code>bun install</code>，自动 merge 一份配置到 <code>~/Library/Application Support/Claude/claude_desktop_config.json</code></li><li>重启 Claude Desktop</li></ol><p>私有性靠 GitHub 仓库权限，便捷性靠脚本化弥补。这是个<strong>安全和便捷的妥协</strong>，不优雅但够用。</p><p>如果团队规模再大、需要支持的 IDE 再多，下一步会考虑用 <code>bun build --compile</code> 编译成单文件二进制，swagger JSON embed 进去。这只是”提高泄露门槛”而非真的加密保护，但对内部工具来说够了。</p><p>远程 HTTP MCP 这个选项被早早排除了——我们的鉴权模型是 per-user 弹用户自己的 Chrome 登自己的账号，搬到远程要服务端重新配合，支持 token 的模式，显然是不太好推动的，且在这个试验阶段，折腾的人越少迭代速度越快。</p><hr><h2 id="几条贯穿性的原则"><a href="#几条贯穿性的原则" class="headerlink" title="几条贯穿性的原则"></a>几条贯穿性的原则</h2><p>回头看，决策之间有几条隐式的共同倾向：</p><ol><li><strong>可行性优先</strong>：上下文是硬约束，不是花多花少的问题。按需查询是为了模型还有空间干活——全量注入不是更贵的方案，是走不通的方案。</li><li><strong>成本敏感（在可行性之上）</strong>：可行性满足后，token、启动时间、维护负担都倾向最低方案（词法搜索 &#x2F; 缓存优先 &#x2F; 不上 embedding）。</li><li><strong>不沾敏感数据</strong>：能让用户的浏览器处理鉴权，就不自己存密钥；能用私有仓库分发，就不发公开 npm。少存东西就少出事。</li><li><strong>预计算 &gt; 运行时</strong>：能在加载期算完的（searchFields、同义词扩展），运行时只做匹配。</li></ol><p>最后想说的是：<strong>做探索性工具最容易犯的错是过度工程化</strong>。同义词表手工维护一份不优雅，但跑得起来；脚本化分发不优雅，但用户接受。这些”不优雅”加起来，是一个能稳定跑下来不出故障的实验底座。</p><p>至于”AI 直接调后端做数据分析”这个原始命题成不成立，要看后面真的跑场景才知道。但即便最后不走这条路，这一路上对鉴权、搜索、token 经济的取舍是攒下来了的——探索本身就有这个价值。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/b510eaf0/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/b510eaf0/"/>
    <published>2026-05-22T14:04:00.000Z</published>
    <summary>
      <![CDATA[<p>我们想试一件事：<strong>能不能让 AI 直接通过 Swagger 调用后端接口，按需做数据分析</strong>。</p>
<p>理论上是通的——Swagger 里有完整的接口定义，AI 看得懂；拉数据、交叉计算、给出结论这种事 AI 也不缺能力；两边接起来，产品&]]>
    </summary>
    <title>做了一个 Swagger MCP：一些设计取舍</title>
    <updated>2026-07-08T10:35:50.888Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>你有没有过这种感觉，早上 7 点，被楼上邻居的一声”咚”吵醒。离闹钟还有半小时，意识刚刚浮上来，大脑已经开始自动”开会”了——那个需求评审还没对齐，小王的代码今天能提测吗，后端接口的字段还没确认，得跟产品再聊一下排期……</p><p>你躺在床上，眼睛都没睁开，脑子已经转了三圈。</p><p>等你终于起来，发现这半小时一点用都没有。该沟通的还是得沟通，该等的还是得等。你只是提前消耗了自己。</p><p>晚上 7 点，你去游泳。这是你一天里为数不多的”属于自己”的时间。但游了两个来回，大脑又开始了——明天的站会要说什么，那个阻塞点谁来跟，新人的成长计划该怎么定……</p><p>水从指间划过，你的身体在泳池里，你的大脑在办公室。</p><p><img src="https://cdn.jsdmirror.com/gh/yuzai/images@main/image/20260417102557-dc4d72.png" loading="lazy"></p><p>如果你也是这样，恭喜你，你不是一个人。这个东西在心理学里有个名字，叫<strong>工作反刍</strong>。</p><h2 id="什么是工作反刍"><a href="#什么是工作反刍" class="headerlink" title="什么是工作反刍"></a>什么是工作反刍</h2><p>反刍，本来是牛把吃下去的东西反复咀嚼。用在心理学里，就是大脑对同一些想法反复嚼、停不下来、而且没有产出。</p><p>心理学家把反刍分成两种。一种叫问题解决式反刍（problem-solving pondering），你真的在思考解决方案，想完了有结论，这种有用。另一种叫情感性反刍（affective rumination），就是我的状态——反复想，但一点用没有，只是在空转，还越转越焦虑。</p><p>德国心理学家 Sabine Sonnentag 提出过一个概念叫<strong>心理脱离</strong>（Psychological Detachment from Work）。她的研究发现，下班后能不能从工作中”心理脱离”，直接影响一个人的疲劳恢复和长期心理健康。</p><p>用她的术语来说，我的问题就是：<strong>心理脱离失败</strong>——人离开了工作，但大脑没有。</p><h2 id="一线开发时没这么严重，开始带人之后失控了"><a href="#一线开发时没这么严重，开始带人之后失控了" class="headerlink" title="一线开发时没这么严重，开始带人之后失控了"></a>一线开发时没这么严重，开始带人之后失控了</h2><p>说实话，我做纯前端开发的时候，这个毛病没这么明显。写代码是一件很”闭合”的事——需求明确、边界清晰，写完了就是写完了，脑子可以关掉。</p><p>当了小组长开始带人之后，一切都变了。</p><p>你的工作不再是”写完这个组件”，而是”确保这十件事都在往前推”。每件事都有不同的责任人、不同的进度、不同的阻塞点。你的脑子里永远挂着一张看不见的甘特图，随时在刷新。</p><p>更要命的是，很多事情的进度不在你手里。你能做的只是发个消息、拉个会、催一下，然后——等。</p><p>但你的大脑不会等。它会反复检查那张甘特图，即使你知道现在检查没有任何意义。心理学上叫<strong>蔡格尼克效应</strong>——未完成的任务比已完成的任务更容易占据你的注意力。带人的痛苦在于，你手上的任务永远不会”完成”，总有下一个。</p><p>所以大脑永远有东西可以嚼。</p><h2 id="大脑的”默认频道”"><a href="#大脑的”默认频道”" class="headerlink" title="大脑的”默认频道”"></a>大脑的”默认频道”</h2><p>我后来意识到，这不是某个特定场景的问题。不是”游泳时容易想工作”或者”早上躺着容易想工作”的问题。而是——只要大脑没有被强制占用，它的默认频道就是工作。</p><p><img src="https://cdn.jsdmirror.com/gh/yuzai/images@main/image/20260417102607-387673.png" loading="lazy"></p><p>跟老婆在一起的时候能忘掉，因为那是一个需要我真正”在场”的互动——有人在跟我说话、有反应、有情感交流，大脑必须实时处理，没有空隙给工作念头插进来。</p><p>打游戏以前也能占住大脑，但后来不行了，大脑能自动驾驶地玩，于是又空出来想工作。</p><p>游泳更别提了——动作重复、环境单调、不能听东西、眼睛也没什么可看的，简直是给工作念头开了 VIP 通道。</p><p>我的大脑要么被强制占满，要么就自动跑去想工作。中间没有一个”安静地什么都不想”的状态。</p><p>等于它从来没有真正休息过。</p><h2 id="我和-Claude-的一次对话"><a href="#我和-Claude-的一次对话" class="headerlink" title="我和 Claude 的一次对话"></a>我和 Claude 的一次对话</h2><p>我把上面这些困扰扔给了 Claude。没有抱着什么期待，只是想找个地方倒一倒。</p><p>但聊着聊着，它帮我把问题拆得很清楚。</p><p>首先，它指出了一个我没意识到的事情：<strong>我不是在”主动想”，是大脑在”自动运转”。</strong> 我越想”别想了”，反而越在想——就像有人说”别想白熊”，你脑子里马上就是白熊。所以对抗它是没用的。</p><p>然后它问了我一个很关键的问题：<strong>你提前想了这么多次，有任何一次真的避免了问题吗？</strong></p><p>答案是没有。那些提前想的东西，到了第二天该沟通还是得沟通，该等还是得等。空转从来没有产生过任何价值。</p><p>其实我的大脑早就知道这一点。那些在床上、在泳池里做的”规划”，从来不会被记住，也不会被落实。第二天到了工位上，还是会重新梳理一遍。所谓的”提前想”，只是大脑在自欺欺人。</p><p>我怀疑这跟我 J 人的属性有关——我天然喜欢做规划，喜欢把事情安排得井井有条。但问题是，不是所有的”想”都叫规划。</p><p>我之前写 <a href="https://github.com/yuzai/sustand">sustand</a> 的时候，连续好几天脑子里都在转这个库的设计，怎么组织 API、怎么处理状态订阅、怎么让用法更简洁。那些思考是有产出的，最后真的落地成了代码。那是问题解决式的思考，有明确的对象，有可以推进的空间。</p><p>但带人之后的大部分反刍不是这种。它是”小王的代码明天能提测吗”——你想一百遍也不会改变小王的进度。它是”后端接口还没确认”——你半夜想破头，对方也不会凌晨回你消息。这种纯粹等人、等结果的思考，没有任何可以推进的空间，只是在空转。</p><p><strong>有创造性的深度思考 vs 无意义的等待式反刍</strong>——区分这两者，是我迈出的第一步。</p><p><img src="https://cdn.jsdmirror.com/gh/yuzai/images@main/image/20260417102625-4ab93e.png" loading="lazy"></p><h2 id="三个阶段的练习"><a href="#三个阶段的练习" class="headerlink" title="三个阶段的练习"></a>三个阶段的练习</h2><p>Claude 给我规划了一条路线，拆成三个阶段：</p><p><strong>第一阶段：看见它。</strong> 不需要做任何改变，唯一的任务就是”发现自己在想”。早上醒来发现在想了，心里打个勾。游泳的时候发现了，也打个勾。不用拉回来，不用停，就只是看见。目标是从”想了半小时才发现”变成”几分钟内就能察觉”。</p><p><strong>第二阶段：拉回来。</strong> 能快速察觉之后，开始练习把注意力拉回到一个具体的感官上——水的温度、呼吸的节奏、脚踩地板的感觉。刚开始拉回来十秒可能又飘走了，正常。关键是”拉回来”这个动作越来越轻松自然。</p><p><strong>第三阶段：空转变短。</strong> 前两个阶段练够了以后，工作念头还是会冒出来，但停留的时间越来越短，从半小时缩到几分钟甚至几十秒就自己散了。</p><p>最终状态不是”再也不想工作”。那不现实。而是——念头来了你有选择权。你可以选择想一下，也可以选择不想，而不是被它拽着走。</p><p>整个过程有一个非常重要的细节：拉回来的时候，对自己的态度要轻。不是”我怎么又想了”，而是”哦，又想了”。因为一旦你烦自己”又在想”，那个烦本身又是一层新的消耗。</p><p><img src="https://cdn.jsdmirror.com/gh/yuzai/images@main/image/20260417102613-86cfa6.png" loading="lazy"></p><h2 id="用代码治愈自己"><a href="#用代码治愈自己" class="headerlink" title="用代码治愈自己"></a>用代码治愈自己</h2><p>聊到这里，身为程序员的本能上线了——我需要一个系统来追踪这件事，不然我怎么知道自己有没有进步？</p><p>于是我让 Claude 帮我做了一个正念练习追踪器。很简单的一个页面，每天花几秒钟记录：在什么场景想了工作、多快察觉到的、有没有拉回来、当天的感受。数据持久化存储，可以回看历史记录，也能看到自己当前在哪个阶段。</p><p><img src="https://cdn.jsdmirror.com/gh/yuzai/images@main/image/20260417103454-37be50.png" loading="lazy"></p><p>这特别符合程序员的思维——如果你不能度量它，你就不能改善它。当你能在追踪器里看到”察觉速度”从”想了很久才发现”慢慢变成”几分钟内发现”，再变成”几十秒就发现”，那种进步是可感知的。</p><p>比起空洞地告诉自己”要放下”，一个可量化的系统让改变变得具体。</p><h2 id="写给同样在空转的你"><a href="#写给同样在空转的你" class="headerlink" title="写给同样在空转的你"></a>写给同样在空转的你</h2><p>如果你也是一个从写代码到开始带人的程序员，如果你也发现自己的大脑在非工作时间停不下来，我想告诉你几件事：</p><p><strong>这个东西有名字。</strong> 它叫工作反刍，不是你”想太多”或者”心态不好”，是大脑的一种运行模式。</p><p><strong>它不是你的错，但它是你的课题。</strong> 一旦你开始带人，就天然容易触发反刍，因为你处理的都是开放式的、依赖他人的、永远不会真正”完成”的任务。但这不意味着你只能忍受它。</p><p><strong>解决它不需要什么大动作。</strong> 不需要冥想打坐一小时，不需要辞职换工作。从”发现自己在想”这一步开始，每天几秒钟就够了。</p><p><strong>大脑不会真正”空”下来，但它可以不再只有一个默认频道。</strong> 念头来了像路边经过的车，看到了，不上车，它自己会开走。</p><p>我还在练习的路上。但至少现在游泳的时候，偶尔能看见指尖划过水面时冒出的泡泡了。</p><p>那几秒钟里，大脑很安静。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/928ad934/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/928ad934/"/>
    <published>2026-04-17T10:27:30.000Z</published>
    <summary>
      <![CDATA[<p>你有没有过这种感觉，早上 7 点，被楼上邻居的一声”咚”吵醒。离闹钟还有半小时，意识刚刚浮上来，大脑已经开始自动”开会”了——那个需求评审还没对齐，小王的代码今天能提测吗，后端接口的字段还没确认，得跟产品再聊一下排期……</p>
<p>你躺在床上，眼睛都没睁开，脑子已经转了]]>
    </summary>
    <title>我的大脑不下班：一个前端工程师的工作反刍自救指南</title>
    <updated>2026-07-08T10:35:50.889Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>国际化链路里，文案的提取、同步、回填通常都有工具链兜着，真正卡节奏的是<strong>人工翻译</strong>：一批文案翻成 30 种语言，交给翻译平台排期，快则几小时，慢则几天。业务方等不及，于是想到交给 AI——后台调 GPT，一次性出全部语种。</p><p>听起来是个非常简单就能搞定的事。实际上前后改了三四版，每版上线都被生产环境顶出一类新问题。</p><p>这篇文章就是介绍我的踩坑之路。</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><ul><li><strong>内容</strong>：每次翻译 30–50 条业务文案，短的是 “Sign in” 这种按钮文本，长的是 200 字的规则说明</li><li><strong>语言</strong>：30+ 种，大语种（英中日韩）和一些相对冷门的语种（缅甸、马拉地、印地、孟加拉等）</li><li><strong>链路</strong>：用户点”翻译”按钮 → 前端调接口 → 后端调 OpenAI → 返回结果</li></ul><h2 id="v1：一把梭"><a href="#v1：一把梭" class="headerlink" title="v1：一把梭"></a>v1：一把梭</h2><p>第一版最朴素——所有文案 + 所有语种塞进一个 prompt，丢给 GPT：</p><pre><code class="hljs js"><span class="hljs-keyword">const</span> systemPrompt = <span class="hljs-string">`你是一个翻译助手。用户会给你一组 JSON 键值对，</span><span class="hljs-string">key 的格式是 &#123;语言代码&#125;_&#123;序号&#125;，value 是要翻译的英文原文。</span><span class="hljs-string">请将每个 value 翻译成 key 对应的语言，保持 JSON 格式原样返回。`</span><span class="hljs-comment">// 把所有文案 × 所有语种拼成一个大 JSON 丢过去</span><span class="hljs-keyword">const</span> payload = &#123;&#125;<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> lang <span class="hljs-keyword">of</span> targetLangs) &#123;  entries.<span class="hljs-title function_">forEach</span>(<span class="hljs-function">(<span class="hljs-params">text, i</span>) =&gt;</span> &#123;    payload[<span class="hljs-string">`<span class="hljs-subst">$&#123;lang&#125;</span>_<span class="hljs-subst">$&#123;i&#125;</span>`</span>] = text  &#125;)&#125;<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> openai.<span class="hljs-property">chat</span>.<span class="hljs-property">completions</span>.<span class="hljs-title function_">create</span>(&#123;  <span class="hljs-attr">model</span>: <span class="hljs-string">&#x27;gpt-5.4-mini&#x27;</span>,  <span class="hljs-attr">messages</span>: [    &#123; <span class="hljs-attr">role</span>: <span class="hljs-string">&#x27;system&#x27;</span>, <span class="hljs-attr">content</span>: systemPrompt &#125;,    &#123; <span class="hljs-attr">role</span>: <span class="hljs-string">&#x27;user&#x27;</span>, <span class="hljs-attr">content</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(payload, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>) &#125;,  ],&#125;)<span class="hljs-keyword">const</span> result = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(response.<span class="hljs-property">choices</span>[<span class="hljs-number">0</span>].<span class="hljs-property">message</span>.<span class="hljs-property">content</span>)</code></pre><p>本地自测没问题——10 条文案、1 个语种，秒级返回。上线后直接翻车：生产环境一次 50 条文案 × 30+ 语言，网关 30s 超时把请求掐了。</p><h2 id="v2：异步化"><a href="#v2：异步化" class="headerlink" title="v2：异步化"></a>v2：异步化</h2><p>超时好办，改异步：</p><ol><li>前端点翻译 → 后端创建任务，立刻返回 taskId</li><li>后端异步跑翻译</li><li>前端轮询 taskId 拿结果</li></ol><p>超时解决了，但新问题开始冒。上线后陆续收到四类反馈：</p><ul><li><strong>翻译成功了，但结果全是英文</strong></li><li><strong>偶尔报 JSON 解析错误</strong></li></ul><p>看着毫无关联，其实归两类原因。</p><p><strong>全返英文</strong>：26 个语种塞进同一个 prompt，上下文太多，AI 不见得表现更好——实践中它会直接把英文原样吐回来，而不是”尽力而为”。</p><p><strong>JSON 解析错误</strong>当时查了一圈没完全定位，只确认和”某些语种”相关。后来在 v3 加了详细日志才真正搞清楚——根因是相对冷门的语言 + 输入里的换行制表符，下面会讲。</p><p>到这一步已经能看出核心矛盾：<strong>一个请求塞太多东西，AI 扛不住</strong>。</p><h2 id="v3：拆开来打"><a href="#v3：拆开来打" class="headerlink" title="v3：拆开来打"></a>v3：拆开来打</h2><p>v2 的问题归结起来就是一锅炖太多。v3 的思路是彻底拆散：</p><ol><li>语种太多 AI 会懵 → <strong>按语言拆开，每个语种独立请求</strong></li><li>单次文案太多容易出错 → <strong>每 10 条一个批次</strong></li><li>AI 偶尔抽风 → <strong>加重试</strong></li><li>JSON 偶尔损坏 → <strong>加修复</strong></li></ol><p>整体结构变成两层并发：</p><pre><code class="hljs plaintext">translate(texts, targetLangs)│├─ 语言并行（Promise.all）──────────────────────│  │               │               ││  ▼               ▼               ▼│  ja              ko              vi│  ├─ batch 0 ─┐   ├─ batch 0 ─┐   ├─ batch 0 ─┐│  ├─ batch 1 ─┤   ├─ batch 1 ─┤   ├─ batch 1 ─┤│  └─ batch 2 ─┘   └─ batch 2 ─┘   └─ batch 2 ─┘│       │                │                ││       ▼                ▼                ▼│  批次并行         批次并行         批次并行│└─ 合并结果 → &#123; ja: [...], ko: [...], vi: [...] &#125;</code></pre><p>30 种语言 × 5 个批次 &#x3D; 150 个请求几乎同时发出，总耗时回到 15–30 秒。</p><p>单个批次内部的策略：</p><pre><code class="hljs plaintext">第 1-2 次尝试 → mini 模型（快、便宜）     ↓ 失败第 3 次尝试 → 好一点的模型（稳定、贵）+ JSON 修复     ↓ 失败抛异常，整个批次标记失败</code></pre><p>每次尝试都做三件事：调模型、解析 JSON、<strong>校验数组长度</strong>。最后一条不能省——AI 偶尔会漏翻（输入 30 条返回 28 条），不校验长度就会导致译文和原文错位，比翻译不准更严重。</p><p>代码大致长这样：</p><pre><code class="hljs ts"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> attempt = <span class="hljs-number">0</span>; attempt &lt; <span class="hljs-number">3</span>; attempt++) &#123;  <span class="hljs-keyword">const</span> isLastAttempt = attempt === <span class="hljs-number">2</span>  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetchWithRetry</span>(url, &#123;    <span class="hljs-attr">data</span>: &#123;      <span class="hljs-attr">model</span>: isLastAttempt ? <span class="hljs-string">&#x27;gpt-5.4&#x27;</span> : <span class="hljs-string">&#x27;gpt-5.4-mini&#x27;</span>,      <span class="hljs-attr">messages</span>: [systemMsg, userMsg],    &#125;,  &#125;)  <span class="hljs-keyword">let</span> arr  <span class="hljs-keyword">try</span> &#123;    arr = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(response.<span class="hljs-property">data</span>.<span class="hljs-property">choices</span>[<span class="hljs-number">0</span>].<span class="hljs-property">message</span>.<span class="hljs-property">content</span>)  &#125; <span class="hljs-keyword">catch</span> &#123;    <span class="hljs-keyword">if</span> (isLastAttempt) arr = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(<span class="hljs-title function_">cleanAndFixJson</span>(text))    <span class="hljs-keyword">else</span> <span class="hljs-keyword">continue</span>  &#125;  <span class="hljs-keyword">if</span> (!<span class="hljs-title class_">Array</span>.<span class="hljs-title function_">isArray</span>(arr) || arr.<span class="hljs-property">length</span> !== expected) &#123;    <span class="hljs-keyword">if</span> (isLastAttempt) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&#x27;length mismatch after 3 attempts&#x27;</span>)    <span class="hljs-keyword">continue</span>  &#125;  <span class="hljs-keyword">return</span> arr&#125;</code></pre><p>v3 上线后头几天很稳，几乎没收到反馈。然后问题从一个意想不到的方向冒出来了。</p><h3 id="相对冷门的语言集体翻车"><a href="#相对冷门的语言集体翻车" class="headerlink" title="相对冷门的语言集体翻车"></a>相对冷门的语言集体翻车</h3><p>上线大概一周，开始零星收到反馈说翻译结果有问题。一开始以为是随机的 AI 抽风，后来翻日志发现了规律：**出错的请求，语种几乎全集中在 <code>hi / bn / th / mr</code>**——印地、孟加拉、泰语、马拉地。英日韩法德西基本不出问题，这几个小语种却稳定翻车。</p><p>症状也很一致：</p><ol><li>丢文案（输入 30，返回 28）</li><li>缺逗号、引号不闭合，JSON 非法</li><li>非法转义符 <code>\n</code> <code>\t</code> <code>\\x</code> <code>\\a</code></li><li>极个别情况下输出被截断</li></ol><p>再进一步排查，发现了另一个规律：<strong>只有输入文案里带换行或制表符时，这几个语种才会出问题</strong>。纯文本句子翻得好好的。</p><p>根因想通之后不复杂：JSON 字符串里 <code>\n</code> <code>\t</code> 必须以转义形式存在。模型在生成输出时，要决定这一位写”真实换行”还是”字面量 <code>\n</code>“——英中日韩这种训练语料里大量见过 JSON 的语种，几乎不会搞混；但印地、孟加拉、泰语这种训练占比低的语言，模型会在两种写法之间摇摆。一旦裸换行落在字符串中间，整个 JSON 就废了。</p><p>本质是<strong>特殊字符 + 小语种训练覆盖不足</strong>的叠加效应。v2 里那些”空字符串”和”JSON 解析错误”也终于有了解释——当时多语种混在一起，这些小语种的问题被淹没在大量正常输出里，不容易看出规律。</p><h3 id="逐条翻译兜底"><a href="#逐条翻译兜底" class="headerlink" title="逐条翻译兜底"></a>逐条翻译兜底</h3><p>到这里，”三次重试 + JSON 修复”已经不够——问题不是偶发的，而是结构性的。降级链得再往下延一级：<strong>扛不住的批次，不走 JSON，一条一条翻</strong>：</p><pre><code class="hljs ts"><span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">translateSingleEntry</span>(<span class="hljs-params">text, lang</span>) &#123;  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetchWithRetry</span>(url, &#123;    <span class="hljs-attr">data</span>: &#123;      <span class="hljs-attr">model</span>: <span class="hljs-string">&#x27;gpt-5.4-mini&#x27;</span>,      <span class="hljs-attr">messages</span>: [        &#123; <span class="hljs-attr">role</span>: <span class="hljs-string">&#x27;system&#x27;</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">`你是翻译助手，只返回翻译结果，不要其他内容。`</span> &#125;,        &#123; <span class="hljs-attr">role</span>: <span class="hljs-string">&#x27;user&#x27;</span>, <span class="hljs-attr">content</span>: <span class="hljs-string">`请将以下英文翻译成 <span class="hljs-subst">$&#123;lang&#125;</span>：\n<span class="hljs-subst">$&#123;text&#125;</span>`</span> &#125;,      ],    &#125;,  &#125;)  <span class="hljs-keyword">return</span> response.<span class="hljs-property">data</span>.<span class="hljs-property">choices</span>[<span class="hljs-number">0</span>].<span class="hljs-property">message</span>.<span class="hljs-property">content</span>.<span class="hljs-title function_">trim</span>()&#125;</code></pre><p>这个 prompt 里一个字都没提 JSON，只让 AI 做一件事：翻译。</p><p>这里有个值得单独拎出来的认知：<strong>JSON 结构本身就是一种噪声</strong>。输入一旦复杂（长句、特殊字符、相对冷门的语言），让 AI 同时兼顾”翻对”和”输出合法 JSON”，成功率会明显下降；把这两件事拆开，翻译成功率几乎 100%，代价只是多调几次接口。</p><p>实测数据：31 条文案 × 27 种语言 &#x3D; 837 条翻译，拆成 108 个批次。一次任务里平均：</p><ul><li>4 次第一次 JSON 解析失败（重试救回）</li><li>2 次数组长度不匹配（重试救回）</li><li>1 次触发逐条翻译（兜底救回）</li><li>0 次英文原文兜底</li></ul><p>英文兜底几乎没触发过，但必须留着——它保证的是”不管多极端，调用方拿到的数组长度一定对”。</p><h2 id="Prompt-踩过的坑"><a href="#Prompt-踩过的坑" class="headerlink" title="Prompt 踩过的坑"></a>Prompt 踩过的坑</h2><p>v3 能稳下来，prompt 本身也改了不少。下面几条都是先反着试过、不行才改回来的。</p><h3 id="自定义分隔符比-JSON-更差"><a href="#自定义分隔符比-JSON-更差" class="headerlink" title="自定义分隔符比 JSON 更差"></a>自定义分隔符比 JSON 更差</h3><p>发现转义符问题后，第一反应是不用 JSON 了，输入输出都用 <code>|||</code> 分隔，把转义问题整个绕过去。</p><p>改完反而更差——要么全返英文，要么一半英文一半目标语种。回头想也说得通：<strong>JSON 的严格语法在某种程度上”逼” AI 认真对待每一个元素</strong>，结构约束一旦没了，AI 很容易半路开小差。</p><h3 id="带-index-的-key-会让-AI-困惑"><a href="#带-index-的-key-会让-AI-困惑" class="headerlink" title="带 index 的 key 会让 AI 困惑"></a>带 index 的 key 会让 AI 困惑</h3><p>最初用过 <code>&#123;lang&#125;_&#123;index&#125;: value</code> 格式：</p><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;ja_0&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ログイン&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;ja_1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;登録&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;ko_0&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;로그인&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;ko_1&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;가입&quot;</span> <span class="hljs-punctuation">&#125;</span></code></pre><p>直觉上挺合理，但 AI 在这种 schema 下经常乱：漏 key、index 跳号、语言代码写错。后来换成最朴素的数组 <code>[&quot;翻译1&quot;, &quot;翻译2&quot;, &quot;翻译3&quot;]</code>，稳定性立刻上来了。AI 只需要维持一个维度（顺序），不用同时盯语言和序号。<strong>结构越简单，AI 越不容易出错</strong>。</p><h3 id="语言代码要写全名"><a href="#语言代码要写全名" class="headerlink" title="语言代码要写全名"></a>语言代码要写全名</h3><p><code>my</code> <code>ms</code> <code>id</code> <code>fil</code> 这些短代码 AI 经常不认识：<code>my</code> 理解成”我的”，<code>ms</code> 理解成 Microsoft，<code>id</code> 理解成 identifier。</p><p>解法是维护映射表，prompt 里用全名：</p><pre><code class="hljs ts"><span class="hljs-keyword">const</span> <span class="hljs-variable constant_">LANG_NAME_MAP</span> = &#123;  <span class="hljs-attr">my</span>: <span class="hljs-string">&#x27;Burmese&#x27;</span>,  <span class="hljs-attr">ms</span>: <span class="hljs-string">&#x27;Malay&#x27;</span>,  <span class="hljs-attr">id</span>: <span class="hljs-string">&#x27;Indonesian&#x27;</span>,  <span class="hljs-attr">fil</span>: <span class="hljs-string">&#x27;Filipino&#x27;</span>,  <span class="hljs-comment">// ...</span>&#125;</code></pre><p>写”请翻译成 Burmese”而不是”请翻译成 my”。对容易混淆的语种（马来&#x2F;印尼、简体&#x2F;繁体），再加一对示例：</p><pre><code class="hljs plaintext">示例（英语翻译成 Burmese）：- [&quot;Sign in&quot;] =&gt; [&quot;ဝင်ရန်&quot;]- [&quot;Sign in&quot;, &quot;Sign up&quot;] =&gt; [&quot;ဝင်ရန်&quot;, &quot;ဆိုင်းအပ်&quot;]</code></pre><h2 id="三版对比"><a href="#三版对比" class="headerlink" title="三版对比"></a>三版对比</h2><table><thead><tr><th>版本</th><th>核心方案</th><th>暴露的问题</th></tr></thead><tbody><tr><td>v1</td><td>所有文案 + 所有语种打包一次请求</td><td>网关 30s 超时</td></tr><tr><td>v2</td><td>异步任务 + 前端轮询</td><td>多语种混请 → AI 偷懒；输出不稳 → JSON 损坏 &#x2F; 空串</td></tr><tr><td>v3</td><td>按语种并行 + 分批 + 重试 + JSON 修复</td><td>相对冷门的语言 + 特殊字符 → 结构性 JSON 失败</td></tr><tr><td>v3+</td><td>逐条翻译兜底 + 英文原文终极兜底</td><td>目前稳定运行</td></tr></tbody></table><p>每版不是”方案有问题”才被换掉，而是<strong>上一层的坑填了，下一层的坑才露出来</strong>：v1 卡在 HTTP 层；v2 绕过 HTTP 后撞到 AI 输出层；v3 处理完输出层，又冒出语种维度的系统性差异。</p><h2 id="几条可以迁移的经验"><a href="#几条可以迁移的经验" class="headerlink" title="几条可以迁移的经验"></a>几条可以迁移的经验</h2><ol><li><p><strong>上下文越多，AI 不见得表现更好</strong>。同时让它做 N 件事，可能一件都做不好。拆任务往往比优化 prompt 更有效。</p></li><li><p><strong>结构化输出是双刃剑</strong>。JSON 的严格性正常情况下帮你校验，输入一复杂就容易反过来变成出错源。简单任务上结构化省心，难的任务上反而是包袱——该拆就拆。</p></li><li><p><strong>兜底链要延伸到”不会失败”的那一层</strong>。这个项目里这一层是英文原文——它就是输入本身，保证调用方拿到的数组长度一定对。</p></li></ol><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>完整代码不到 300 行 TypeScript，开源在 <a href="https://github.com/yuzai/ai-batch-translate">ai-batch-translate</a>，<code>pnpm install &amp;&amp; pnpm tsx example.ts</code> 可以直接跑。目前线上跑了一个多月，累计翻了大概 5 万条文案，没再收到翻译相关的反馈。</p><p>做这类”用 AI 干某件具体事情”的项目，比较实际的做法就是先最朴素地接进去，上线后根据生产环境的反馈一版一版迭代。设计阶段能想到的问题，往往不如线上暴露出来的具体。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/ec3669a8/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/ec3669a8/"/>
    <published>2026-04-13T20:00:00.000Z</published>
    <summary>
      <![CDATA[<p>国际化链路里，文案的提取、同步、回填通常都有工具链兜着，真正卡节奏的是<strong>人工翻译</strong>：一批文案翻成 30 种语言，交给翻译平台排期，快则几小时，慢则几天。业务方等不及，于是想到交给 AI——后台调 GPT，一次性出全部语种。</p>
<p>听起来]]>
    </summary>
    <title>把翻译交给 AI 之后，我重写了三版代码</title>
    <updated>2026-07-08T10:35:50.886Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>前端没有多线程，按理说不该有并发问题。但只要你写过稍微复杂一点的项目，就一定踩过这些坑：用户连点按钮提交了两次订单、搜索框的旧结果覆盖了新结果、五个请求同时 401 触发了五次 Token 刷新……</p><p>这些问题看着各不相同，但背后其实是同一件事——<strong>多个异步流程在抢同一个资源</strong>。而解决它们的核心思路，往往只需要一个 Promise。</p><p>本文从最常见的 Token 刷新场景出发，一步步拆解前端并发问题的本质和通用解法。</p><h2 id="前端鉴权那些事"><a href="#前端鉴权那些事" class="headerlink" title="前端鉴权那些事"></a>前端鉴权那些事</h2><p>前端处理登录态，方案其实挺多的，不同项目的选择差异很大。</p><p>最传统的是 <strong>Cookie + Session</strong>：登录后服务端种一个 Cookie，之后浏览器每次请求自动带上，前端几乎不用操心。很多项目至今还在用，简单可靠。</p><p>前后端分离流行之后，<strong>JWT Token</strong> 成了主流：后端返回一个 Token，前端存在 localStorage 里，请求时塞进 Header。至于 Token 过期怎么办，不同团队的处理方式五花八门——</p><p>最简单的是 <strong>401 直接跳登录页</strong>，干脆利落，很多内部系统就是这么干的，够用了。</p><p>稍微讲究一点的会做<strong>滑动续期</strong>：后端在每次请求时检查 Token 是否快过期，快过期就在响应头里塞一个新 Token，前端替换掉旧的，类似 Session 的自动续期。还有一种是前端自己算过期时间，<strong>快到期时主动刷新</strong>，不等 401 再处理。</p><p>再往上就是<strong>双 Token 机制</strong>：一个短期的 access token 用于日常请求（比如 15 分钟过期），一个长期的 refresh token 用于续期（比如 7 天过期）。access token 过期时，前端用 refresh token 静默换一个新的，用户无感知。</p><p>说实话，双 Token 是不是”最佳实践”，社区一直有争论——有人觉得在自家系统里是过度设计，滑动续期就够了；也有人觉得职责分离确实更安全。这个争论不是本文的重点，但双 Token 的前端实现确实是<strong>最能体现并发问题的场景</strong>——因为它涉及”Token 过期后静默刷新并重发请求”，而这个过程很容易在并发时出 bug。</p><p>所以我们就用它作为切入点。双 Token 的前端实现几乎形成了一个固定范式——<strong>请求前统一注入 Token，响应后统一拦截刷新</strong>。</p><p>请求发出前，从存储中取出 access token，塞进请求头：</p><pre><code class="hljs js">axios.<span class="hljs-property">interceptors</span>.<span class="hljs-property">request</span>.<span class="hljs-title function_">use</span>(<span class="hljs-function"><span class="hljs-params">config</span> =&gt;</span> &#123;  <span class="hljs-keyword">const</span> token = <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">getItem</span>(<span class="hljs-string">&#x27;access_token&#x27;</span>);  <span class="hljs-keyword">if</span> (token) &#123;    config.<span class="hljs-property">headers</span>.<span class="hljs-property">Authorization</span> = <span class="hljs-string">`Bearer <span class="hljs-subst">$&#123;token&#125;</span>`</span>;  &#125;  <span class="hljs-keyword">return</span> config;&#125;);</code></pre><p>收到 401 响应时，不直接报错，而是悄悄用 refresh token 换一个新的 access token，然后把刚才失败的请求重新发一遍，用户甚至感知不到：</p><pre><code class="hljs js">axios.<span class="hljs-property">interceptors</span>.<span class="hljs-property">response</span>.<span class="hljs-title function_">use</span>(  <span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> response,  <span class="hljs-keyword">async</span> error =&gt; &#123;    <span class="hljs-keyword">const</span> originalRequest = error.<span class="hljs-property">config</span>;    <span class="hljs-keyword">if</span> (error.<span class="hljs-property">response</span>?.<span class="hljs-property">status</span> === <span class="hljs-number">401</span> &amp;&amp; !originalRequest.<span class="hljs-property">_retry</span>) &#123;      originalRequest.<span class="hljs-property">_retry</span> = <span class="hljs-literal">true</span>;      <span class="hljs-keyword">const</span> newToken = <span class="hljs-keyword">await</span> <span class="hljs-title function_">refreshToken</span>();      originalRequest.<span class="hljs-property">headers</span>.<span class="hljs-property">Authorization</span> = <span class="hljs-string">`Bearer <span class="hljs-subst">$&#123;newToken&#125;</span>`</span>;      <span class="hljs-keyword">return</span> <span class="hljs-title function_">axios</span>(originalRequest);    &#125;    <span class="hljs-keyword">return</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">reject</span>(error);  &#125;);</code></pre><p>如果 refresh token 也过期了呢？那就退化回最简单的方案——清除登录态，跳回登录页。双 Token 机制不是消灭了”跳登录”，只是把它推迟到了最后一刻：</p><pre><code class="hljs js"><span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">refreshToken</span>(<span class="hljs-params"></span>) &#123;  <span class="hljs-keyword">try</span> &#123;    <span class="hljs-keyword">const</span> &#123; data &#125; = <span class="hljs-keyword">await</span> axios.<span class="hljs-title function_">post</span>(<span class="hljs-string">&#x27;/auth/refresh&#x27;</span>, &#123;      <span class="hljs-attr">refresh_token</span>: <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">getItem</span>(<span class="hljs-string">&#x27;refresh_token&#x27;</span>)    &#125;);    <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">&#x27;access_token&#x27;</span>, data.<span class="hljs-property">access_token</span>);    <span class="hljs-keyword">return</span> data.<span class="hljs-property">access_token</span>;  &#125; <span class="hljs-keyword">catch</span> &#123;    <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">clear</span>();    <span class="hljs-variable language_">window</span>.<span class="hljs-property">location</span>.<span class="hljs-property">href</span> = <span class="hljs-string">&#x27;/login&#x27;</span>;    <span class="hljs-keyword">return</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">reject</span>();  &#125;&#125;</code></pre><p>打个比方：请求拦截器负责”带上门禁卡”，响应拦截器负责”门禁卡过期时自动换卡再刷一次”，换卡也失败就”回前台重新办卡”。</p><p>到这里一切看起来很完美。但有一个问题被我们忽略了——如果页面上同时有 5 个请求，它们几乎在同一瞬间都收到了 401，会发生什么？</p><p>答案是：5 个请求各自触发一次 <code>refreshToken()</code>，连发 5 次刷新请求。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260410095224070.png" loading="lazy"></p><p>这显然不对。</p><h2 id="并发难题：5-个-401-只该刷新一次"><a href="#并发难题：5-个-401-只该刷新一次" class="headerlink" title="并发难题：5 个 401 只该刷新一次"></a>并发难题：5 个 401 只该刷新一次</h2><p>这是前端 Token 鉴权最经典的并发问题。传统方案是维护一个 <code>isRefreshing</code> 标志位加一个等待队列：第一个请求负责刷新，后续请求排队等结果。这种方案能用，但代码比较啰嗦。</p><p>其实有一个更简洁的思路：<strong>不用队列，直接缓存那个 refresh 的 Promise。</strong> 多个请求发现 Token 过期时，如果已经有一个 refresh 在进行中，就直接 <code>await</code> 同一个 Promise——大家等的是同一件事，拿到的是同一个结果：</p><pre><code class="hljs js"><span class="hljs-keyword">let</span> refreshPromise = <span class="hljs-literal">null</span>;<span class="hljs-keyword">function</span> <span class="hljs-title function_">getNewToken</span>(<span class="hljs-params"></span>) &#123;  <span class="hljs-keyword">if</span> (refreshPromise) <span class="hljs-keyword">return</span> refreshPromise;  refreshPromise = axios    .<span class="hljs-title function_">post</span>(<span class="hljs-string">&#x27;/auth/refresh&#x27;</span>, &#123;      <span class="hljs-attr">refresh_token</span>: <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">getItem</span>(<span class="hljs-string">&#x27;refresh_token&#x27;</span>),    &#125;)    .<span class="hljs-title function_">then</span>(<span class="hljs-function">(<span class="hljs-params">&#123; data &#125;</span>) =&gt;</span> &#123;      <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">&#x27;access_token&#x27;</span>, data.<span class="hljs-property">access_token</span>);      <span class="hljs-keyword">return</span> data.<span class="hljs-property">access_token</span>;    &#125;)    .<span class="hljs-title function_">catch</span>(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> &#123;      <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">clear</span>();      <span class="hljs-variable language_">window</span>.<span class="hljs-property">location</span>.<span class="hljs-property">href</span> = <span class="hljs-string">&#x27;/login&#x27;</span>;      <span class="hljs-keyword">return</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">reject</span>(err);    &#125;)    .<span class="hljs-title function_">finally</span>(<span class="hljs-function">() =&gt;</span> &#123;      refreshPromise = <span class="hljs-literal">null</span>;    &#125;);  <span class="hljs-keyword">return</span> refreshPromise;&#125;</code></pre><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260410095329096.png" loading="lazy"></p><p>整个逻辑就靠一个变量 <code>refreshPromise</code>：有值说明刷新正在进行，所有人直接 await 它；没值就发起刷新并把 Promise 存起来。<code>finally</code> 里清空，这样下一轮过期时又能重新触发。</p><p>这个模式就叫 <strong>Promise Cache</strong> 吧。</p><h2 id="等一下，标志位不就够了吗？"><a href="#等一下，标志位不就够了吗？" class="headerlink" title="等一下，标志位不就够了吗？"></a>等一下，标志位不就够了吗？</h2><p>看到这里你可能会想：搞什么 Promise Cache，我用一个布尔标志位挡住重复调用不就行了？</p><pre><code class="hljs js"><span class="hljs-keyword">let</span> isRefreshing = <span class="hljs-literal">false</span>;<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">refreshToken</span>(<span class="hljs-params"></span>) &#123;  <span class="hljs-keyword">if</span> (isRefreshing) <span class="hljs-keyword">return</span>;  isRefreshing = <span class="hljs-literal">true</span>;  <span class="hljs-keyword">try</span> &#123;    <span class="hljs-keyword">const</span> &#123; data &#125; = <span class="hljs-keyword">await</span> axios.<span class="hljs-title function_">post</span>(<span class="hljs-string">&#x27;/auth/refresh&#x27;</span>);    <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">&#x27;access_token&#x27;</span>, data.<span class="hljs-property">access_token</span>);  &#125; <span class="hljs-keyword">finally</span> &#123;    isRefreshing = <span class="hljs-literal">false</span>;  &#125;&#125;</code></pre><p>对于某些场景确实够了——比如埋点上报、按钮防连点，你只需要”别重复执行”，不关心结果。但 Token 刷新不行。看看会发生什么：</p><pre><code class="hljs plaintext">请求 A 收到 401 → 发起 refresh，isRefreshing = true请求 B 收到 401 → 发现 isRefreshing → return → 拿到 undefined → 没有新 token → 重发失败请求 A 的 refresh 成功了 → 但 B 已经错过了</code></pre><p>标志位把 B “挡回去”了，但 B 还需要结果啊。Promise Cache 不一样，B 不是被拒绝，而是”挂在同一个 Promise 上等”：</p><pre><code class="hljs plaintext">请求 A 收到 401 → 发起 refresh，缓存 Promise请求 B 收到 401 → await 同一个 Promise → 等着refresh 成功 → A 和 B 同时拿到新 token → 各自重发</code></pre><p>所以判断标准很简单：调用者只需要”别重复执行”→ 标志位就够。调用者还需要”等到结果再继续”→ 必须用 Promise Cache。打个比方，前者是”门卫拦人”，后者是”拼车到终点”。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260410095417498.png" loading="lazy"></p><h2 id="举一反三：前端并发问题的两大类"><a href="#举一反三：前端并发问题的两大类" class="headerlink" title="举一反三：前端并发问题的两大类"></a>举一反三：前端并发问题的两大类</h2><p>Token 刷新只是冰山一角。一旦你理解了 Promise Cache 的本质，就会发现前端到处都有类似的并发场景。它们大致分两类：</p><h3 id="第一类：多次触发，只该执行一次"><a href="#第一类：多次触发，只该执行一次" class="headerlink" title="第一类：多次触发，只该执行一次"></a>第一类：多次触发，只该执行一次</h3><p>这正是 Promise Cache 的主场。除了 Token 刷新，还有——</p><p><strong>多个组件同时请求同一个接口。</strong> 比如页面上三个组件都需要用户信息，几乎同时调 <code>GET /user</code>，没必要发三次：</p><pre><code class="hljs js"><span class="hljs-keyword">const</span> pending = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Map</span>();<span class="hljs-keyword">function</span> <span class="hljs-title function_">dedupRequest</span>(<span class="hljs-params">key, requestFn</span>) &#123;  <span class="hljs-keyword">if</span> (pending.<span class="hljs-title function_">has</span>(key)) <span class="hljs-keyword">return</span> pending.<span class="hljs-title function_">get</span>(key);  <span class="hljs-keyword">const</span> p = <span class="hljs-title function_">requestFn</span>().<span class="hljs-title function_">finally</span>(<span class="hljs-function">() =&gt;</span> pending.<span class="hljs-title function_">delete</span>(key));  pending.<span class="hljs-title function_">set</span>(key, p);  <span class="hljs-keyword">return</span> p;&#125;<span class="hljs-title function_">dedupRequest</span>(<span class="hljs-string">&#x27;user-info&#x27;</span>, <span class="hljs-function">() =&gt;</span> axios.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;/user&#x27;</span>));</code></pre><p><strong>按钮防重复提交。</strong> 用户手快连点了三次”下单”：</p><pre><code class="hljs js"><span class="hljs-keyword">let</span> submitPromise = <span class="hljs-literal">null</span>;<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">handleSubmit</span>(<span class="hljs-params">data</span>) &#123;  <span class="hljs-keyword">if</span> (submitPromise) <span class="hljs-keyword">return</span> submitPromise;  submitPromise = axios.<span class="hljs-title function_">post</span>(<span class="hljs-string">&#x27;/order&#x27;</span>, data).<span class="hljs-title function_">finally</span>(<span class="hljs-function">() =&gt;</span> &#123;    submitPromise = <span class="hljs-literal">null</span>;  &#125;);  <span class="hljs-keyword">return</span> submitPromise;&#125;</code></pre><p>模式完全一样：有在飞的 Promise 就复用，没有就新建一个。</p><h3 id="第二类：多次触发，只保留最后一次"><a href="#第二类：多次触发，只保留最后一次" class="headerlink" title="第二类：多次触发，只保留最后一次"></a>第二类：多次触发，只保留最后一次</h3><p>搜索联想是最典型的例子。用户快速输入 a → ab → abc，三个请求飞出去，但 <code>a</code> 的请求可能最后才返回，把 <code>abc</code> 的正确结果覆盖掉。</p><p>这里要做的不是合并，而是<strong>丢弃过期的结果</strong>。最简单的方案是用一个自增 ID：</p><pre><code class="hljs js"><span class="hljs-keyword">let</span> currentRequestId = <span class="hljs-number">0</span>;<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">search</span>(<span class="hljs-params">keyword</span>) &#123;  <span class="hljs-keyword">const</span> id = ++currentRequestId;  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;/search&#x27;</span>, &#123; <span class="hljs-attr">params</span>: &#123; <span class="hljs-attr">q</span>: keyword &#125; &#125;);  <span class="hljs-keyword">if</span> (id !== currentRequestId) <span class="hljs-keyword">return</span>; <span class="hljs-comment">// 已经过时了，丢掉</span>  <span class="hljs-title function_">setResults</span>(res.<span class="hljs-property">data</span>);&#125;</code></pre><p>更彻底的做法是用 <code>AbortController</code> 直接取消上一次请求，连响应都不用判断：</p><pre><code class="hljs js"><span class="hljs-keyword">let</span> controller = <span class="hljs-literal">null</span>;<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">search</span>(<span class="hljs-params">keyword</span>) &#123;  controller?.<span class="hljs-title function_">abort</span>();  controller = <span class="hljs-keyword">new</span> <span class="hljs-title class_">AbortController</span>();  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> axios.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;/search&#x27;</span>, &#123;    <span class="hljs-attr">params</span>: &#123; <span class="hljs-attr">q</span>: keyword &#125;,    <span class="hljs-attr">signal</span>: controller.<span class="hljs-property">signal</span>,  &#125;);  <span class="hljs-title function_">setResults</span>(res.<span class="hljs-property">data</span>);&#125;</code></pre><p>你可能会问：用时间戳代替自增 ID 行不行？能用，但有坑。浏览器里 <code>Date.now()</code> 精度通常只有 1ms，有些浏览器出于安全考虑（防 Spectre 攻击）甚至故意降到 5ms。用户快速输入时，两次调用完全可能拿到同一个时间戳，竞态又回来了。自增 ID 就没这个问题，每次 <code>++</code> 天然唯一、严格递增，不依赖任何平台特性。至于溢出？<code>Number.MAX_SAFE_INTEGER</code> 约 9 千万亿，每秒自增 1000 次也要 2.85 亿年才会用完，页面一刷新还归零。</p><h2 id="异步单例：当-Promise-Cache-遇上设计模式"><a href="#异步单例：当-Promise-Cache-遇上设计模式" class="headerlink" title="异步单例：当 Promise Cache 遇上设计模式"></a>异步单例：当 Promise Cache 遇上设计模式</h2><p>聊完了接口层的并发，再看一个更”架构”的场景——SDK 初始化。</p><p>单例模式大家都熟悉：</p><pre><code class="hljs js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SDK</span> &#123;  <span class="hljs-keyword">static</span> instance = <span class="hljs-literal">null</span>;  <span class="hljs-keyword">static</span> <span class="hljs-title function_">getInstance</span>(<span class="hljs-params"></span>) &#123;    <span class="hljs-keyword">if</span> (!<span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span>) <span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span> = <span class="hljs-keyword">new</span> <span class="hljs-title function_">SDK</span>();    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span>;  &#125;&#125;</code></pre><p>同步实例化时没问题。但前端 SDK 的初始化往往是异步的——加载远程脚本、拉取配置、建立 WebSocket 连接。这时候单例就有一个微妙的 bug：</p><pre><code class="hljs plaintext">模块 A 调用 getInstance() → instance 为 null → new SDK() → 开始异步 init()...模块 B 调用 getInstance() → instance 已经存在！→ 直接返回 → 拿到一个还没初始化完的实例 → 💥</code></pre><p>问题出在哪？单例只保证了”只 new 一次”，但没保证”等初始化完再给你”。这恰好是 Promise Cache 能解决的：</p><pre><code class="hljs js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SDK</span> &#123;  <span class="hljs-keyword">static</span> initPromise = <span class="hljs-literal">null</span>;  <span class="hljs-keyword">static</span> <span class="hljs-title function_">getInstance</span>(<span class="hljs-params"></span>) &#123;    <span class="hljs-keyword">if</span> (!<span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>) &#123;      <span class="hljs-keyword">const</span> sdk = <span class="hljs-keyword">new</span> <span class="hljs-title function_">SDK</span>();      <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span> = sdk.<span class="hljs-title function_">init</span>().<span class="hljs-title function_">then</span>(<span class="hljs-function">() =&gt;</span> sdk);    &#125;    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>;  &#125;&#125;<span class="hljs-keyword">const</span> sdk1 = <span class="hljs-keyword">await</span> <span class="hljs-variable constant_">SDK</span>.<span class="hljs-title function_">getInstance</span>(); <span class="hljs-comment">// 触发初始化</span><span class="hljs-keyword">const</span> sdk2 = <span class="hljs-keyword">await</span> <span class="hljs-variable constant_">SDK</span>.<span class="hljs-title function_">getInstance</span>(); <span class="hljs-comment">// 挂在同一个 Promise 上等</span><span class="hljs-comment">// sdk1 === sdk2，且都是初始化完成的</span></code></pre><p><strong>单例保证”只创建一个实例”，Promise Cache 保证”只执行一次异步过程，且所有人都能等到结果”。</strong> 可以说，Promise Cache 就是异步世界的单例模式。</p><h3 id="但这样有个代价：async-传染"><a href="#但这样有个代价：async-传染" class="headerlink" title="但这样有个代价：async 传染"></a>但这样有个代价：async 传染</h3><p>上面的方案解决了并发问题，却带来了一个新的烦恼——初始化只需要等一次，但之后每次调用 <code>getInstance()</code> 都要写 <code>await</code>，即使 Promise 早就 resolved 了。虽然性能上没问题（只是一个 microtask），但 async 像病毒一样”传染”，逼着所有调用方都变成异步函数。</p><p>一种改进是两层缓存——初始化阶段缓存 Promise，完成后缓存实例：</p><pre><code class="hljs js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SDK</span> &#123;  <span class="hljs-keyword">static</span> instance = <span class="hljs-literal">null</span>;  <span class="hljs-keyword">static</span> initPromise = <span class="hljs-literal">null</span>;  <span class="hljs-keyword">static</span> <span class="hljs-title function_">getInstance</span>(<span class="hljs-params"></span>) &#123;    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span>) <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span>;           <span class="hljs-comment">// 已完成，同步返回</span>    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>) <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>;     <span class="hljs-comment">// 进行中，返回 Promise</span>    <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span> = <span class="hljs-keyword">new</span> <span class="hljs-title function_">SDK</span>().<span class="hljs-title function_">init</span>().<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">sdk</span> =&gt;</span> &#123;      <span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span> = sdk;      <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span> = <span class="hljs-literal">null</span>;      <span class="hljs-keyword">return</span> sdk;    &#125;);    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>;  &#125;&#125;</code></pre><p>但这带来了新的心智负担：<code>getInstance()</code> 有时返回实例，有时返回 Promise，调用方需要知道当前是哪个阶段。</p><p>更干净的做法是<strong>把初始化和获取拆成两个方法</strong>，各司其职：</p><pre><code class="hljs js"><span class="hljs-keyword">class</span> <span class="hljs-title class_">SDK</span> &#123;  <span class="hljs-keyword">static</span> instance = <span class="hljs-literal">null</span>;  <span class="hljs-keyword">static</span> initPromise = <span class="hljs-literal">null</span>;  <span class="hljs-keyword">static</span> <span class="hljs-title function_">init</span>(<span class="hljs-params"></span>) &#123;    <span class="hljs-keyword">if</span> (<span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>) <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>;    <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span> = <span class="hljs-keyword">new</span> <span class="hljs-title function_">SDK</span>().<span class="hljs-title function_">setup</span>().<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">sdk</span> =&gt;</span> &#123;      <span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span> = sdk;      <span class="hljs-keyword">return</span> sdk;    &#125;);    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">initPromise</span>;  &#125;  <span class="hljs-keyword">static</span> <span class="hljs-title function_">getInstance</span>(<span class="hljs-params"></span>) &#123;    <span class="hljs-keyword">if</span> (!<span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&#x27;SDK 未初始化，请先调用 SDK.init()&#x27;</span>);    <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">instance</span>; <span class="hljs-comment">// 永远同步</span>  &#125;&#125;</code></pre><p>使用起来职责清晰：</p><pre><code class="hljs js"><span class="hljs-comment">// 应用入口，只调一次</span><span class="hljs-keyword">await</span> <span class="hljs-variable constant_">SDK</span>.<span class="hljs-title function_">init</span>();<span class="hljs-comment">// 之后所有地方，同步获取</span><span class="hljs-keyword">const</span> sdk = <span class="hljs-variable constant_">SDK</span>.<span class="hljs-title function_">getInstance</span>();sdk.<span class="hljs-title function_">doSomething</span>();</code></pre><p>这也是大部分主流 SDK 的实际做法——在应用启动时 <code>await</code> 一次初始化，之后全同步访问。</p><p>当然，这意味着<strong>调用方需要自己保证时序</strong>——<code>getInstance()</code> 必须在 <code>init()</code> 完成之后才能调。实践中一般把 <code>init()</code> 卡在应用挂载之前来解决这个问题：</p><pre><code class="hljs js"><span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">bootstrap</span>(<span class="hljs-params"></span>) &#123;  <span class="hljs-keyword">await</span> <span class="hljs-variable constant_">SDK</span>.<span class="hljs-title function_">init</span>();  app.<span class="hljs-title function_">mount</span>(<span class="hljs-string">&#x27;#root&#x27;</span>); <span class="hljs-comment">// SDK 就绪后才启动应用</span>&#125;<span class="hljs-title function_">bootstrap</span>();</code></pre><p>这也是为什么 Vue 的 <code>app.use()</code>、各种插件的 <code>install()</code> 都设计在 <code>mount()</code> 之前——用<strong>启动流程的顺序</strong>来隐式保证时序。</p><p>归根结底是一个取舍：Promise Cache 让框架替你管时序，调用方无脑 await 就行，但 async 会传染；init&#x2F;getInstance 分离给了你同步访问的清爽，但得自己控制好初始化入口。SDK 是全局基础设施、入口明确的，分离方案更干净；初始化时机不确定、调用方散落各处的，Promise Cache 更安全。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>前端的”并发问题”大多不是真正的多线程竞争，而是多个异步流程在抢同一个资源。折腾到最后，核心解法就两个：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260410095444254.png" loading="lazy"></p><p>而 Promise Cache 的本质，就是<strong>异步世界的单例模式</strong>。一个变量，一个 <code>if</code> 判断，一个 <code>finally</code> 清理——三行逻辑，解决一大类问题。</p><p>下次再遇到”多个地方同时调、但只该执行一次”的需求，别急着加锁、加队列、加标志位。先想想：能不能缓存那个 Promise？</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/7164ec5/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/7164ec5/"/>
    <published>2026-04-09T16:32:26.000Z</published>
    <summary>
      <![CDATA[<p>前端没有多线程，按理说不该有并发问题。但只要你写过稍微复杂一点的项目，就一定踩过这些坑：用户连点按钮提交了两次订单、搜索框的旧结果覆盖了新结果、五个请求同时 401 触发了五次 Token 刷新……</p>
<p>这些问题看着各不相同，但背后其实是同一件事——<strong]]>
    </summary>
    <title>前端并发治理：从 Token 刷新聊起，一个 Promise 就够了</title>
    <updated>2026-07-08T10:35:50.888Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>最近，出于业务需要，参考 v0 的实现，蹬了一个类似 v0 的平台出来。</p><p>先看效果：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/poc1.gif" loading="lazy"></p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/poc2.gif" loading="lazy"></p><p>整体采用 Next.js 做前后端服务，E2B 提供沙箱，Claude Agent SDK 完成代码生成，沙箱提供预览和代码推送部署能力。</p><p>ps: 本文不会包含任何的代码(本身也都是 AI 生成的)，只会介绍相关方案的选型、核心的架构和实现原理。同时关于部署的环节，各个公司都有自己的部署流水线，并不具备参考价值，会弱化这个环节的介绍。</p><h2 id="方案对比和设计"><a href="#方案对比和设计" class="headerlink" title="方案对比和设计"></a>方案对比和设计</h2><p>AI 生成前端代码，一般有这么几种方式：一份 html，一份代码块，以及直接生成项目。</p><h3 id="生成-html"><a href="#生成-html" class="headerlink" title="生成 html"></a>生成 html</h3><p>生成一份 html，然后增删改查，最终存储 html 即可，不论是预览还是部署，都最为简单。</p><p>有很多产品都是这么做的，比如 Claude 的 Artifacts，Google 的 Stitch。</p><p>这是最简单，也最轻便的方案。</p><p>这里面的关键技术点有几个：</p><ol><li><p>如何让 AI 生成高质量的 HTML？当然这也无非就是需要一些非常优秀的提示词来约束 AI 的行为。</p></li><li><p>如何增量修改？通过在浏览器侧实现一个支持局部替换的 Edit Tool 即可，这也是很多 cli 工具在本地修改代码的常见策略。</p></li><li><p>后期的可维护性是这个方案最大的隐患。生成的 HTML 往往是一个几百行甚至上千行的单文件，没有组件拆分，没有模块化，样式和逻辑全部混在一起。如果需要人工介入修改，多年程序员看到这样的代码，大概会有一种被拉回刀耕火种时代的感觉——能改，但很痛苦。这也意味着，一旦走上这条路，后续的迭代就只能继续依赖 AI，项目实际上已经不再适合人来维护。</p></li></ol><p>ps: 这里可能会有人好奇，为什么不是修改某一行某几列的代码，这是因为 AI 对于行号识别不准确，反而直接执行字符搜索并替换更为准确。感兴趣的可以查看 pi-mono 项目中 edit 工具的实现，这也是绝大部分 cli 工具的实现方案。</p><p>至于 html 的预览和部署，可谓是极为简单且花费最少了。</p><h3 id="生成代码块"><a href="#生成代码块" class="headerlink" title="生成代码块"></a>生成代码块</h3><p>另一种方式是：生成代码块，存储在数据库中，预览采用 WebContainer、Sandpack，或者通过 Babel 转 CommonJS 在浏览器端模拟打包等方式来预览前端项目。</p><p>这基本是纯前端的方案，不过 WebContainer 要授权，Sandpack 倒是开源，但是加载速度上可能存在一些问题。至于 Babel 转 CommonJS 自行实现编译系统，也是 ok 的，只是要支持 jsx, vue, 要花一点时间，开发的工作量不小。</p><p>当然，除了这些建设，如何稳定 AI 的输出，也是这个方案中的一大问题，理想情况下，希望 AI 的产物是 文件名 + 内容 组合成的 json 数组。</p><p>一般可以通过几个方案来解决：</p><ol><li>换更好的模型</li><li>运用 XML 这样的提示词技巧，来让 AI 输出的更符合预期</li></ol><p>但是这个方案有几个比较大的问题：</p><ol><li>编译工程复杂度比较高</li><li>增量替换的方案，输出格式可能不如工具调用那般精准，在耗时和质量上会更低效一点。</li><li>对于外部依赖的包，需要提前做编译、告知 AI 用法等，相对不那么自由</li></ol><h3 id="直接生成项目"><a href="#直接生成项目" class="headerlink" title="直接生成项目"></a>直接生成项目</h3><p>直接生成项目，最终预览和部署都和普通的项目一样。这也是 v0 的方案。</p><p>这个方案本质上是给用户准备一个沙箱，这个沙箱中，直接启动一个 claude code 或者 codex 这样的工具，可以是 cli 也可以是 sdk。</p><p>同时指定一个工作目录，最终的项目生成和运行，都发生在这个工作目录下。用户输入直接指向 claude code，从而完成项目的生成。</p><p>这个方案的灵活度最高，同时由于背后是最顶尖的 AI 生成工具，所以在质量上和效率上，其实都不太需要担心。</p><p>但是最大的问题就在于需要给每一个用户都提供一个沙箱，对于运维部署的能力要求比较高。</p><p>同时沙箱的内存分配和 cpu 分配，资源上也不能少。</p><p>不过好在已经有很多服务商提供这样的服务，比如 E2B、Cloudflare 等服务商。付费调 API 的话，准备一个沙箱也很容易。</p><h3 id="对比表格"><a href="#对比表格" class="headerlink" title="对比表格"></a>对比表格</h3><table><thead><tr><th>维度</th><th>生成 HTML</th><th>生成代码块</th><th>直接生成项目</th></tr></thead><tbody><tr><td>实现复杂度</td><td>低</td><td>中</td><td>高</td></tr><tr><td>预览方案</td><td>直接渲染 iframe</td><td>WebContainer &#x2F; Sandpack &#x2F; Babel 转 CommonJS</td><td>沙箱内启动 dev server</td></tr><tr><td>部署复杂度</td><td>极低，存 HTML 即可</td><td>低，纯前端方案</td><td>高，需要为每个用户分配沙箱</td></tr><tr><td>增量修改精准度</td><td>高（字符串 Edit Tool）</td><td>中（输出格式不如工具调用稳定）</td><td>高（Agent SDK 原生工具调用）</td></tr><tr><td>AI 输出稳定性</td><td>高（单文件，约束简单）</td><td>中（需要结构化 JSON 输出，依赖提示词技巧）</td><td>高（由 Agent 工具链保证）</td></tr><tr><td>外部依赖支持</td><td>弱（只能用 CDN 引入）</td><td>弱（需要提前编译、告知 AI 用法）</td><td>强（npm install 自由安装）</td></tr><tr><td>代码可维护性</td><td>低（不适合人工维护）</td><td>中</td><td>高（标准项目结构）</td></tr><tr><td>资源消耗</td><td>极低</td><td>低</td><td>高（沙箱需要分配内存和 CPU）</td></tr><tr><td>灵活度</td><td>低</td><td>中</td><td>高</td></tr><tr><td>代表产品</td><td>Claude Artifacts、Google Stitch</td><td>Bolt.new（基于 StackBlitz WebContainer）</td><td>v0、本文实现</td></tr></tbody></table><h2 id="架构设计"><a href="#架构设计" class="headerlink" title="架构设计"></a>架构设计</h2><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260402111329246.png" loading="lazy"></p><p>整体的架构图如上，分为三块：</p><ol><li><p><strong>Next.js 前端</strong>：聊天输入框、消息流展示、代码文件树、实时预览 iframe，以及打断&#x2F;重试等交互控制。</p></li><li><p><strong>Next.js 后端</strong>：接收前端消息，维护会话与沙箱的映射关系，将消息转发给对应沙箱内的 Agent，并将 Agent 的流式输出透传回前端。</p></li><li><p><strong>E2B 沙箱</strong>：基于自定义模板启动，模板内预装了 Node.js 环境和项目脚手架。沙箱内运行 Claude Agent SDK，负责代码的生成与修改；同时启动 dev server 并通过 E2B 的端口暴露能力对外提供预览。</p></li></ol><h3 id="消息流转"><a href="#消息流转" class="headerlink" title="消息流转"></a>消息流转</h3><p>用户操作路径如下：</p><ol><li>用户打开平台，发起第一条消息，后端按需创建 E2B 沙箱（冷启动约需几秒）</li><li>沙箱就绪后，后端将消息投递给沙箱内的 Claude Agent SDK</li><li>Agent SDK 开始工作：调用文件读写工具生成或修改代码</li><li>Agent 的输出以流式事件的形式，经后端透传回前端实时展示</li><li>代码变更同步到文件树，预览 iframe 直接加载沙箱暴露的端口</li></ol><h3 id="会话与沙箱管理"><a href="#会话与沙箱管理" class="headerlink" title="会话与沙箱管理"></a>会话与沙箱管理</h3><p>多用户场景下，每个会话对应一个独立的沙箱实例，隔离性天然满足。</p><p>上下文的维护完全交给 Agent SDK，后端只需持久化”会话 ID → 沙箱 ID”的映射即可。考虑到沙箱有闲置超时机制，需要在映射层做好沙箱的重建和恢复逻辑，一般沙箱的服务方基本都会内置这些能力。</p><h3 id="部署发布"><a href="#部署发布" class="headerlink" title="部署发布"></a>部署发布</h3><p>代码的部署和发布，一个比较通用的方案是在沙箱内完成 Git 提交，推送到远程仓库后触发 CI&#x2F;CD 流水线，从而完成项目的上线。由于这部分强依赖各公司自身的发布体系，本文不展开。</p><p>整体来讲技术卡点并不多。最核心的 AI 代码生成能力，借助 Agent SDK 即可完成，质量和直接使用 Claude Code 打平。沙箱管理和前端页面反而是 AI 最擅长的部分，蹬起来毫无压力。</p><h2 id="心得体会"><a href="#心得体会" class="headerlink" title="心得体会"></a>心得体会</h2><p>整体蹬一个 v0，让 AI 写代码花费的时间其实并不多，大概一天左右就能蹬出来。</p><p>但是有一说一，这个方案，其实来来回回跟 AI 拉扯了几天，大到从生成 HTML，到生成片段代码，再到最后的沙箱方案，而小到增量更新的解决方案，Babel 转义的优劣，都属于考量的范畴。</p><p>包括是用 Agent SDK，还是直接用 Claude CLI，也是经过多方权衡后的结果。</p><p>一切方案落定，Plan Mode 开启，Opus 一开，反而是最轻松的时刻。</p><p>基本上第一次的产物，就能达到最小 demo 的效果。</p><p>至于交互上的细节，比如打断输入，补充说明，向用户提问明确需求，这些细节上的打磨，也是花点心思就能解决的地方。</p><p>整体来讲，在没有 AI 介入之前，其实是不太能这么快完成这样一个系统的。单单是沙箱方案的选型，可能都要花费个几天，比如沙箱的暂停和恢复，费用的对比等等，也是 AI 辅助决策的结果，有了决策，实现又是几天，确确实实在效率上提升非常大。</p><p>在这个过程中，我本身也是直接退订了 Cursor，因为完全不需要自己再上手手动修代码了，单说执行这块，AI 绝对是夯爆了。</p><p>很难说不焦虑，但又感觉不必太过焦虑。这次最大的体感不是”AI 写代码很快”，而是整个过程中，花时间最多的地方依然是人在做的事——判断方案的取舍，理解各种工具的边界，决定什么值得做、什么可以砍掉。执行层 AI 确实夯爆了，但执行之前的那些决策，AI 只是参谋，拍板的还得是人。</p><p>所以与其焦虑被替代，不如想清楚自己在一件事里到底在做什么。毕竟 AI 还是得有人蹬，至于蹬到哪里去，这个问题 AI 替你答不了。</p><h2 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h2><p>本文的 POC（Proof of Concept，概念验证）代码已开源，即用最小的实现跑通”用户输入 → Agent 生成代码 → 沙箱预览”这条核心流程，感兴趣的可以查看：<a href="https://github.com/yuzai/code-gen">https://github.com/yuzai/code-gen</a></p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/b2f61f16/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/b2f61f16/"/>
    <published>2026-04-02T15:14:00.000Z</published>
    <summary>
      <![CDATA[<p>最近，出于业务需要，参考 v0 的实现，蹬了一个类似 v0 的平台出来。</p>
<p>先看效果：</p>
<p><img src="https://gitee.com/martin_bo/images/raw/master/image/poc1.gif" loading=]]>
    </summary>
    <title>一天时间，用 Claude Code 蹬了一个 v0 出来（附源码）</title>
    <updated>2026-07-08T10:35:50.891Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>我的老婆也是一名程序员，之前由于公司的限制，一直没有使用 AI 编程工具。</p><p>直到公司放开限制，她高兴的跟我分享她写的第一条 IDE Rule: 在回答我的问题的时候，请先叫我小可爱（化名）。</p><p>听到这条规则后，我震惊了，第一条规则，竟然是提供情绪价值的？</p><p>我们这些男程序员呢？第一条规则大概率是”回答用中文”或者”代码要加注释”。</p><p>这差距，属实被教育了。</p><p>于是立马问了一下 AI，现在是否有提供情绪价值的 Skill。</p><p>得到的结论也是很意外，没有，基本上官方收录的都是怎么做 PPT，怎么剪辑视频。</p><p>唯独没有提供情绪价值的。</p><p>但是现在最流行的说法：AI 上岸，先斩程序员。</p><p>讲道理我们才是当下最为焦虑的一批人。</p><p>可我们这些男程序员呢，一门心思的写 skill，搞这个搞那个的。</p><p>唯独没有整一个给自己提供情绪价值的 skill，这才是当下最需要的啊。</p><p>每天学 MCP, Skill, Agent, 一如之前一门心思的学 CSS, HTML, Java。</p><p>有什么用？现在一出生就在 AI 元年的 AI 原生程序员，可早就不管这些了。</p><p>不如写个提供情绪价值的 skill，来抽空缓解一下自己的焦虑？</p><p>所以你看，这个世界真的不能没有女程序员——至少她们能提醒我们，工具再强大，也得先学会对自己好一点。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260401113548077.png" loading="lazy"></p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/badffe3d/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/badffe3d/"/>
    <published>2026-04-01T17:27:30.000Z</published>
    <summary>
      <![CDATA[<p>我的老婆也是一名程序员，之前由于公司的限制，一直没有使用 AI 编程工具。</p>
<p>直到公司放开限制，她高兴的跟我分享她写的第一条 IDE Rule: 在回答我的问题的时候，请先叫我小可爱（化名）。</p>
<p>听到这条规则后，我震惊了，第一条规则，竟然是提供情绪价]]>
    </summary>
    <title>这个世界不能没有女程序员</title>
    <updated>2026-07-08T10:35:50.892Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>test1111</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/cd48996/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/cd48996/"/>
    <published>2026-03-27T09:16:30.000Z</published>
    <summary>
      <![CDATA[<p>test1111</p>]]>
    </summary>
    <title>前端其实很好转全栈的</title>
    <updated>2026-07-08T10:35:50.888Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>最近同事用 AI 写了一段看起来非常合理的代码，上线后直接把线上的多语言翻译全干没了。</p><p>逻辑很简单：每次发版时，先从远端拉取最新的翻译 JSON，和本地的合并，然后一起提交上去。这样就能保证新增的翻译和已有的翻译共存。伪代码大概是这样：</p><pre><code class="hljs js"><span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">updateTranslation</span>(<span class="hljs-params">local</span>) &#123;  <span class="hljs-keyword">const</span> remote = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetchRemoteJSON</span>();  <span class="hljs-keyword">const</span> merged = &#123; ...remote, ...local &#125;;  <span class="hljs-keyword">await</span> <span class="hljs-title function_">upload</span>(merged);&#125;</code></pre><p>看起来没毛病对吧？AI 写的也很利索，一把过。</p><p>但问题出在 <code>fetchRemoteJSON</code> 失败的时候。AI 很”贴心”地加了兜底逻辑：请求失败就返回空对象，保证流程不中断。</p><pre><code class="hljs js"><span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">fetchRemoteJSON</span>(<span class="hljs-params"></span>) &#123;  <span class="hljs-keyword">try</span> &#123;    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-variable constant_">REMOTE_URL</span>);    <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> res.<span class="hljs-title function_">json</span>();  &#125; <span class="hljs-keyword">catch</span> (e) &#123;    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">warn</span>(<span class="hljs-string">&#x27;拉取远端翻译失败，使用空对象兜底&#x27;</span>);    <span class="hljs-keyword">return</span> &#123;&#125;;  &#125;&#125;</code></pre><p>于是某次发版时远端接口刚好抖了一下，<code>remote</code> 拿到了 <code>&#123;&#125;</code>，合并后只剩本次新增的几条翻译，然后被完整地提交上去——之前积累的上千条翻译，没了。</p><p>正确的做法是拉取失败时直接中断流程，而不是吞掉错误继续跑。拉不到最新数据，就不该继续合并和提交。让流程报错、让人介入，远比”优雅降级”后悄悄覆盖数据要安全得多。</p><p>AI 在写代码时有一个明显的倾向：<strong>让代码跑通</strong>。它会本能地给每个可能出错的地方加 try-catch，给每个可能为空的值加默认值。这在很多场景下是合理的，但在某些场景下，这种”兜底”本身就是 bug。像这个案例，空对象不是一个安全的兜底值——它意味着”没有任何历史翻译”，和”拉取失败”是两码事。AI 没有区分这两种语义，只是机械地保证了函数不会抛错。</p><p>review AI 代码时，多问一句：<strong>这个 catch 里的兜底值，在业务上意味着什么？它真的安全吗？</strong> 不是所有的错误都该被吞掉，有时候报错才是正确的行为。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/68f2fe4e/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/68f2fe4e/"/>
    <published>2026-03-26T09:49:30.000Z</published>
    <summary>
      <![CDATA[<p>最近同事用 AI 写了一段看起来非常合理的代码，上线后直接把线上的多语言翻译全干没了。</p>
<p>逻辑很简单：每次发版时，先从远端拉取最新的翻译 JSON，和本地的合并，然后一起提交上去。这样就能保证新增的翻译和已有的翻译共存。伪代码大概是这样：</p>
<pre><c]]>
    </summary>
    <title>AI写的代码翻车了：一个兼容逻辑引发的线上事故</title>
    <updated>2026-07-08T10:35:50.886Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>用了一个月的 Claude Code 之后，最终还是取消了 Cursor 的订阅。</p><p>我是很早就开始用 Cursor 来辅助编程的，最爱的就是 Tab 补全的能力。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260323194732137.png" loading="lazy"></p><p>一开始其实是想要保留 Cursor 的，毕竟，Tab 补全实在太好用了。</p><p>但是对于 AI 直接修改我的代码，一直都还是不太放心的，毕竟 bug 是 AI 写的，但是锅 AI 是背不了的。</p><p>其实起初这些 AI 命令行工具的出现，并没有带给我太多的惊艳，毕竟只是个终端工具，怎么可能比得上编辑器的强大呢？</p><p>但是后来机缘巧合之下，我开始重度使用 Claude Code，到现在大概刚好一个月，这期间，其实对于这种终端工具的改观，可谓是非常之大。</p><p>首先，它是终端工具，但是这类工具也有非常多的其他形态：</p><ol><li>vscode&#x2F;cursor 扩展</li><li>Claude Code 电脑客户端</li><li>Claude Code Web 端，pc &amp; mobile</li><li>Claude Mobile App</li></ol><p>其中，仅仅是编辑器的扩展，就基本上能够替代 Cursor 除了 Tab 补全的所有能力了。</p><p>而 Tab 补全，正是我一直没有退订 Cursor 的原因。</p><p>但是，如果一行代码都不需要写了呢？那还需要 Tab 补全吗？</p><p>答案显而易见，不写代码，Tab 补全自然就没有什么吸引力了。</p><p>但是说实话，真能放心把 coding 全部交给 AI 吗？</p><p>我的转折发生在一次技术需求上：给现有的平台做一个问答机器人。</p><p>这涉及到：</p><ol><li>历史对话的管理：CRUD + 前端展示</li><li>token 消耗的统计：CRUD + 前端展示</li><li>平台知识库的构建和更新：RAG or 提示词注入？</li></ol><p>可以说是一个小全栈项目了。</p><p>在 Plan Mode 和 AI Battle 多轮之后，鉴于知识库内容不多，最终确定了方案：</p><p>不走 RAG 知识库，而是借鉴 Claude Code 中 Skills（按需加载知识）的方案，渐进式的加载我们的知识文档。</p><p>也就是首先在系统提示词中注入文档描述和名称，并告知 AI 如果发现问的是某一块的知识，就用工具去读取对应的文档，再注入上下文。</p><p>从而完成所谓 Skills 的渐进式披露，也避免所谓的系统提示词爆炸的问题。</p><p>在做好计划之后的那一刻，我犹豫了，因为涉及到数据库，前端样式，知识库文档的提取脚本和工具调用的实现。</p><p>按照以往的经验，接下来我会一个小步骤一个小步骤的让 AI 来帮我实现。</p><p>但是那天，我想了想，干脆切个分支，交给 AI 全部实现吧，反正如果改的太脏了，就抛弃分支重写呗。</p><p>索性按下了回车，喝个茶的功夫，代码就生成好了，不能说完全没有 bug，但是基本能用，剩下的都是一些小细节优化。</p><p>这是我着实没想到的。</p><p>分分钟就完成了，换我自己搞，拖拖拉拉得干好几天，更别提还得一步一步 debug。</p><p>也就是这一刻开始，我退订了 Cursor，因为确实没必要写代码了。</p><p>沟通方案 -&gt; AI 执行 -&gt; 我验收提出修改意见 -&gt; AI 执行 -&gt; 我验收提出修改意见 -&gt; …..</p><p>依次循环。</p><p>如果方案不确定，那么 AI 输出确实是不稳定的。</p><p>但是在方案明确，只剩执行这个 Action 的时候，AI 绝对是扛把子，效率是人工的百倍都不止。</p><p>后来，我又和 AI Battle，讨论了 v0（Vercel 出品的 AI 前端生成工具）的原理，并得到了实现一个 v0 的方案。</p><p>这一次，我又选择了让 AI 来实现，只能说，分分钟，一个自己版本的 v0 就有了。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260325095340989.png" loading="lazy"></p><p>这里面，coding 这个环节，反而成了最轻松的环节。</p><p>Battle 方案陆陆续续花了我三天的时间，但是 demo 的产出，就是一杯咖啡的时间。</p><p>可以看到，这个工作量，手写的话有多少就不说了。单单只是看相关技术的文档，都要琢磨个几天，更别提实现了。</p><p>有一说一，AI 的出现确实极大的提高了程序员的生产力，以前几天才能实现的功能，AI 几下就搞定了。</p><p>但是生产力提升的背后，其实也是一种角色转变。</p><p>以前写代码，是程序员的核心工作。现在写代码，反而成了最不重要的一环。</p><p>真正花时间的，是想清楚要做什么、怎么做、做出来对不对。</p><p>也许这才是这一波 AI 浪潮真正在重塑的东西——不是用 AI 替代程序员，而是把程序员从”实现者”变成”决策者”。</p><p>作为一个写了好几年代码的人，说实话心情挺复杂的。曾经引以为傲的手速和调试技巧，突然就不那么重要了。但换个角度想，能把精力放在更有价值的思考上，也未尝不是一件好事。</p><p>这个趋势，不以任何人的意志为转移，顺势而为，可能才是正确的打开方式。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/c19e4d13/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/c19e4d13/"/>
    <published>2026-03-23T18:04:00.000Z</published>
    <summary>
      <![CDATA[<p>用了一个月的 Claude Code 之后，最终还是取消了 Cursor 的订阅。</p>
<p>我是很早就开始用 Cursor 来辅助编程的，最爱的就是 Tab 补全的能力。</p>
<p><img src="https://gitee.com/martin_bo/ima]]>
    </summary>
    <title>代码不用自己写了，我退订了 Cursor</title>
    <updated>2026-07-08T10:35:50.888Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>最近帮同事 Review 了两个需求，都打回重做了。</p><p>用的还是最顶尖的模型，但问题不在 AI，在人。</p><p>第一个需求是调整一个配置项的字段——原来的字段名叫 <code>buttonType</code>，对应 UI 上展示的是「按钮类型」。</p><p>对于开发来说，这很好理解，无外乎 <code>primary</code>、<code>secondary</code>、<code>dashed</code>、<code>text</code>、<code>default</code> 这些值。</p><p>但产品对「按钮类型」的理解完全不同——他们想的是跳转链接、打开弹窗、toast 提示这类行为。</p><p>所以需求是：把 <code>buttonType</code> 对应的 UI 改成「按钮 UI 类型」，同时新增一个字段让产品配置按钮点击后的行为。</p><p>需求本身不复杂，麻烦的地方在于 <code>buttonType</code> 会落库，字段一旦改名，历史数据就需要兼容处理。</p><p>AI 给出的方案是：把 <code>buttonType</code> 改为 <code>buttonUIType</code>，新增 <code>buttonClickType</code> 存储点击行为，同时在前端做兼容——读取数据时，如果存在 <code>buttonType</code>，就映射到 <code>buttonUIType</code> 上。</p><p>贴心、严谨、能跑通。</p><p>但其实有一个更简单的答案——<strong>只改 UI 上的文案，字段名一个字母都不用动。</strong></p><p>产品看到的标签从「按钮类型」变成「按钮 UI 类型」，需求满足了，历史数据无需兼容，兼容代码一行不用写。</p><p>AI 的那段兼容逻辑确实能跑，但当线上所有老数据都退出历史舞台之后，它就成了一段永远不会被执行的僵尸代码。除了徒增维护成本，还会引导 AI 在后续类似场景里照葫芦画瓢，最终积成屎山。</p><p>第二个需求麻烦一些，涉及全局埋点字段的处理：每次按钮点击，都要上报一份全局数据。</p><p>核心问题就一个：这份全局数据怎么带。</p><p>AI 在同事的指挥下给出了方案：在根组件获取全局数据，然后一层一层往下传，点击时作为参数上报。</p><p>任务是完成了，但方案是”把全局数据一层一层手动透传”——典型的用最笨的办法解决问题。</p><p>熟悉 React 的都知道，跨组件共享数据走 Context 就好了。更进一步，这类公共参数直接封装进埋点方法里，调用方根本不需要关心它的存在。</p><p>AI 没有主动建议 Context，是因为同事没有给它足够的背景——AI 只看到了「如何传参数」这个局部问题，不知道这是一个跨多层组件的全局数据场景。</p><p>返工的时候，同事也没有自己动手，而是把改进方向告诉了 AI，重新生成，顺利通过了 Code Review。</p><p>两个案例，AI 都完美执行了交代的任务。但受限于使用者自身的技术判断，最终交出的代码是不及格的。</p><p>后来我们总结了一个使用习惯：</p><p>拿到需求，先自己想清楚思路，再让 AI 执行；如果思路不清晰，先让 AI 出方案（比如 Plan 模式），对齐之后再动手。</p><p>这样既快，又不会堆屎山。</p><p>AI 是放大器——放大的是使用者的判断力，而不是替代它。锅最终还是自己的。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/85a39835/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/85a39835/"/>
    <published>2026-03-21T12:36:26.000Z</published>
    <summary>
      <![CDATA[<p>最近帮同事 Review 了两个需求，都打回重做了。</p>
<p>用的还是最顶尖的模型，但问题不在 AI，在人。</p>
<p>第一个需求是调整一个配置项的字段——原来的字段名叫 <code>buttonType</code>，对应 UI 上展示的是「按钮类型」。</p>]]>
    </summary>
    <title>同事用 AI 做的两个需求，Code Review 都被打回重做了</title>
    <updated>2026-07-08T10:35:50.889Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>最近一直在做 AI 相关的功能，写着写着突然有点懵——我这个东西，到底算不算 Agent？MCP 和 Tool Use 是一回事吗？Workflow 和 Agent 的区别又在哪？</p><p>网上的文章和视频也是越看越乱，概念繁多，颇有种大模型黑话的感觉。</p><p>直到看到了 Claude 官方的课程：Building with the Claude API，我才算把这些概念捋清楚了。</p><p>网址在这里：</p><pre><code class="hljs plaintext">https://anthropic.skilljar.com/claude-with-the-anthropic-api</code></pre><p>发现这个课程其实是来自于知乎的一个问题：<strong>如何成为一名 AI Agent 工程师？</strong>。</p><p>有从 LangChain, LangGraph 入手的，也有说从 Workflow 入手的。</p><p>有一个高赞答案吸引了我：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260302140105089.png" loading="lazy"></p><p>相比于其他的回答，确实可谓是四两拨千斤了，真正学习这个课程后，确实收获颇多。</p><h2 id="关键亮点"><a href="#关键亮点" class="headerlink" title="关键亮点"></a>关键亮点</h2><p>对于我这种看事情喜欢看原理的人来说，这门课程简直是大福利。</p><p>就比如一开始的多轮对话，其实在看之前，就会好奇，多轮对话，上下文到底是谁在维护？</p><p>实际上确实不是大模型服务测在提供，每次对话都需要携带完整的历史上下文，token 消耗会随对话轮数线性增长——这也是为什么长对话会越来越贵，context window 的大小如此重要。</p><p>这个课程一个流程图就说清楚了，因为大模型服务提供者，不做存储：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260302195552468.png" loading="lazy"></p><p>当然，这个点应该绝大部分同学都知道了。</p><p>接下来解答我困惑的就是 Tool Use 的原理。本质上就是请求大模型的时候多传递了一个 tools 列表，模型自行选择，同时将需要使用的 tool name 和入参传递给客户端，客户端内部再调用工具将结果返回，之后大模型再返回回答。一般而言这时候我们界面上就能看到模型的回答了。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260302200116988.png" loading="lazy"></p><p>另一个就是 MCP 了，它本质就是定义了其他人如何将 tool 的定义和实现传递给我们的客户端。</p><p>一般这个 tools 是由 app 或者说 client 自己实现，但是借由 MCP 这个协议，client 可以直接获取到其他人写好的 tool 并且让对方来完成 tool 的执行。</p><p>所以两者其实是互补的： MCP 重点在于客户端和工具提供方如何传递和执行 tool，而 Tool Use 讲的是客户端和大模型服务如何交互实现工具的调用和结果的返回。</p><p>而最后就是 Agent 和 Workflow 了。相比大家应该都看过类似的文章 or 视频：Agent Workflow 已死什么的。</p><p>刚巧我最近就在做一些和大模型对话的功能，有多轮对话，拿到结果后自己写代码做结果校验。就很迷惑，我做的工作算 Agent 吗？啥是 workflow，这两者有区别吗？</p><p>答案也很简单，有区别：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20260302201334068.png" loading="lazy"></p><p>最后发现，其实是 Workflow。而这个课程，最终给的结论，何时使用 Agent，何时使用 Workflow，也讲的很清楚。</p><p>简单来说：<strong>流程固定、步骤可预期</strong>，用 Workflow，比如自动生成热点文章；<strong>任务路径不固定、需要模型自主规划和决策</strong>，用 Agent，比如 Claude Code，或者豆包、元宝这类对话产品。</p><h2 id="一些课程中遇到的问题"><a href="#一些课程中遇到的问题" class="headerlink" title="一些课程中遇到的问题"></a>一些课程中遇到的问题</h2><ol><li>账号问题</li></ol><p>可能很多人一直没有使用 Claude，就是因为要输入其他国家手机号以及充值等问题。</p><p>不过这里课程站是独立出来的，并不需要手机号验证或者充值验证这样的操作。所以打破网络问题之后，直接用邮箱注册之后即可学习这个课程。</p><ol start="2"><li>API Key 的问题</li></ol><p>教程中需要使用 API Key，这一点是必须付费购买，也需要手机号验证，比较麻烦。</p><p>一方面，可以直接去第三方 API 代理平台购买，本身课程完整走下来花费不了太多的 token。</p><p>另一方面，可以使用一些平台免费赠送的额度。</p><p>这里我直接用的是 Vercel AI Gateway 赠送的 5 美金额度，访问 <code>https://vercel.com/ai</code> 注册即可领取，不需要绑卡。</p><p>领取后在控制台生成一个 API Token，然后把代码中的 base_url 和 model 替换成以下内容：</p><pre><code class="hljs py">client = Anthropic(    base_url=<span class="hljs-string">&quot;https://ai-gateway.vercel.sh&quot;</span>,)model = <span class="hljs-string">&quot;anthropic/claude-haiku-4.5&quot;</span></code></pre><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>如果你也处于”看了很多文章但依然一头雾水”的阶段，强烈推荐从这门课入手。相比各种二手解读文章，官方课程在概念准确性上确实要高出不少。</p><p>需要注意的是，课程内容稍微有些滞后，一些新概念（比如 Skill, Computer Use、Extended Thinking）还没有覆盖，可以学完后再去看 Claude 官方文档补充。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/670009c7/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/670009c7/"/>
    <published>2026-03-03T12:36:26.000Z</published>
    <summary>
      <![CDATA[<p>最近一直在做 AI 相关的功能，写着写着突然有点懵——我这个东西，到底算不算 Agent？MCP 和 Tool Use 是一回事吗？Workflow 和 Agent 的区别又在哪？</p>
<p>网上的文章和视频也是越看越乱，概念繁多，颇有种大模型黑话的感觉。</p>
<p]]>
    </summary>
    <title>搞不清 Agent、MCP、Workflow？这门 Claude 官方免费课程救了我</title>
    <updated>2026-07-08T10:35:50.890Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>我们在日常开发页面的过程中，对于固定背景图的容器的宽高的处理，往往是按照设计稿写死，很少会出现需要固定宽高比例，而宽度自适应的场景。</p><p>但是这个情况，在低代码平台的基础组件中会比较常见，比如布局组件：</p><p>用户上传一张背景图，需要用图片的宽度撑满容器，高度采用图片的宽高比进行自适应，然后内容区域由用户继续拖拽其他组件完成设计。</p><p>此时，由于不确定是否在其他容器内，所以宽度只能设置为 100%，但是高度就比较难设置。</p><span id="more"></span><p>有几个方案，方案一是通过 <code>getComputedStyle</code> 获取当前元素的实际宽度，然后 JS 根据图片的宽高比直接计算出高度来设置给元素。</p><p>方案二则是父元素采用 <code>relative</code> 定位，子元素通过 <code>img</code> 元素来作为 <code>children</code>， 再增加一个 绝对定位撑满整个父元素的 <code>div</code> 包裹其他子元素，借助 <code>img</code> 的默认的高度自适应特性完成宽高比的保留，伪代码如下：</p><pre><code class="hljs js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">Layout</span> = (<span class="hljs-params"></span>) =&gt; (    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">&quot;w-full relative&quot;</span>&gt;</span></span><span class="language-xml">        <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">&quot;xxxx&quot;</span> <span class="hljs-attr">width</span>=<span class="hljs-string">&quot;xxx&quot;</span> <span class="hljs-attr">height</span>=<span class="hljs-string">&quot;xxx&quot;</span> /&gt;</span></span><span class="language-xml">        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">&quot;absolute top-0 bottom-0 left-0 right-0&quot;</span>&gt;</span></span><span class="language-xml">            &#123;/* 其他拖拽进来的子元素 */&#125;</span><span class="language-xml">        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span><span class="language-xml">    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>)</code></pre><p>方案三和方案二类似，只不过原本依赖 <code>img</code> 标签默认的高度自适应，调整为利用 <code>padding top</code> 设置为百分比时参照的是宽度来完成宽高比例的设置，伪代码如下：</p><pre><code class="hljs js"><span class="hljs-comment">//aspectRatio = width / height，假设是 16 / 9，那么 paddingTop = (height / width) * 100%，所以 paddingTop 设置为 1 / (16 / 9) * 100%，也就是 1 / aspectRatio * 100%</span><span class="hljs-keyword">const</span> <span class="hljs-title function_">Layout</span> = (<span class="hljs-params">aspectRatio</span>) =&gt; (    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">&quot;w-full relative&quot;</span> <span class="hljs-attr">style</span>=<span class="hljs-string">&#123;&#123;</span> <span class="hljs-attr">paddingTop:</span> `$&#123;<span class="hljs-attr">1</span> / <span class="hljs-attr">aspectRatio</span> * <span class="hljs-attr">100</span>&#125;%` &#125;&#125;&gt;</span></span><span class="language-xml">        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">&quot;absolute top-0 bottom-0 left-0 right-0&quot;</span>&gt;</span></span><span class="language-xml">            &#123;/* 其他拖拽进来的子元素 */&#125;</span><span class="language-xml">        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span><span class="language-xml">    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>)</code></pre><p>我一开始采用的便是方案三，毕竟简单。</p><p>但是很快就出现了问题，子元素本来配置出来，比如两行文案是可以正常展示的，但是当文案被多语言翻译后，可能就会超出这个背景图高度，这一点在移动端更为明显。</p><p>这个问题，方案二和方案三都不能满足，方案一倒是可以把计算出来的高度设置为最小高度来解决这个问题，但是还是略显麻烦。</p><p>此时就是本文要介绍的核心 <code>CSS</code> 属性了：<strong>aspect-ratio</strong>。</p><p><code>aspect-ratio</code> 在 2021 年提出，如今已是基础能力，现代浏览器广泛支持。</p><p>不过也正是由于是 2021年提出的，我自己对于 CSS 的学习，其实是停留在了工作前，后来新增的一些特性并没有进行新的学习，反而还是比较古老的思维。这次也是在和一个同事的探讨下才进行了新的尝试。</p><p>这个属性，其实本质上就是来解决这个问题的。</p><pre><code class="hljs css"><span class="hljs-selector-class">.card</span> &#123;    <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;    <span class="hljs-attribute">aspect-ratio</span>: <span class="hljs-number">16</span> / <span class="hljs-number">9</span>;&#125;</code></pre><p>在父容器设置后，当内容元素高度不足，则会保持同样的宽高比例，当超出时，使用子元素的高度来撑开。</p><p>可以说是非常适合我当下的场景了。</p><p>最终的伪代码如下：</p><pre><code class="hljs js"><span class="hljs-keyword">const</span> <span class="hljs-title function_">Layout</span> = (<span class="hljs-params">aspectRatio</span>) =&gt; (    <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">&quot;w-full&quot;</span> <span class="hljs-attr">style</span>=<span class="hljs-string">&#123;&#123;</span></span></span><span class="hljs-tag"><span class="language-xml">        <span class="hljs-attr">aspectRatio</span>,</span></span><span class="hljs-tag"><span class="language-xml">        <span class="hljs-attr">backgroundImage:</span> `<span class="hljs-attr">url</span>(&#x27;<span class="hljs-attr">your-image.jpg</span>&#x27;)`,</span></span><span class="hljs-tag"><span class="language-xml">        <span class="hljs-attr">backgroundSize:</span> &#x27;<span class="hljs-attr">cover</span>&#x27;,</span></span><span class="hljs-tag"><span class="language-xml">        <span class="hljs-attr">backgroundPosition:</span> &#x27;<span class="hljs-attr">center</span>&#x27;,</span></span><span class="hljs-tag"><span class="language-xml">        <span class="hljs-attr">backgroundRepeat:</span> &#x27;<span class="hljs-attr">no-repeat</span>&#x27;,</span></span><span class="hljs-tag"><span class="language-xml">    &#125;&#125;&gt;</span></span><span class="language-xml">        &#123;/* 其他拖拽进来的子元素 */&#125;</span><span class="language-xml">    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>)</code></pre><p>通过设置 <strong>aspect-ratio</strong> 来完成固定宽高比的实现，背景图通过 <code>cover</code> 来保证铺满，在超出场景下，借助 <strong>aspect-ratio</strong> 的特性来完成父容器的撑开，图片由于是 <code>cover</code>，并不会显得突兀。</p><p>关于新旧两种方案，两者实现对比如下：</p><table><thead><tr><th>特性</th><th>旧的 “Padding Hack” 方式</th><th>新的 <code>aspect-ratio</code></th></tr></thead><tbody><tr><td>原理</td><td>利用 <code>padding-bottom: 56.25%</code>（9&#x2F;16）来撑开高度</td><td>直接声明 <code>aspect-ratio: 16 / 9</code></td></tr><tr><td>代码量</td><td>冗长，需要额外的容器或伪元素</td><td>一行代码</td></tr><tr><td>内容溢出</td><td>内容容易被切断，必须绝对定位内部元素</td><td><strong>智能处理</strong>：如果内容太多，盒子会自动长高</td></tr><tr><td>直观性</td><td>需要拿计算器算百分比（<code>100 * 9 / 16</code>）</td><td>直接写比例，所见即所得</td></tr></tbody></table><p>可以说是非常方便的一个属性了。</p><p>也给我自己提了个醒，现在早已不是只有 CSS3 的天下了，新特性往往好用也方便，得持续保持学习才行。</p><p>如果你也在做低代码画布组件，推荐先从这个属性开始替换旧方案。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/aad8e7a1/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/aad8e7a1/"/>
    <published>2026-02-14T10:36:26.000Z</published>
    <summary>
      <![CDATA[<p>我们在日常开发页面的过程中，对于固定背景图的容器的宽高的处理，往往是按照设计稿写死，很少会出现需要固定宽高比例，而宽度自适应的场景。</p>
<p>但是这个情况，在低代码平台的基础组件中会比较常见，比如布局组件：</p>
<p>用户上传一张背景图，需要用图片的宽度撑满容器，高度采用图片的宽高比进行自适应，然后内容区域由用户继续拖拽其他组件完成设计。</p>
<p>此时，由于不确定是否在其他容器内，所以宽度只能设置为 100%，但是高度就比较难设置。</p>]]>
    </summary>
    <title>分享一个最近帮我大忙的 CSS 属性：aspect-ratio</title>
    <updated>2026-07-08T10:35:50.888Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p><a href="https://mp.weixin.qq.com/s/CZE3NebDV2Y1CkNh7UN5wg">上一篇文章：深入聊聊 Qwen3：我们外行除了高呼沸腾了，还应该知道什么?</a>，深入分析了 Qwen3 的跑分评测是否有水分，如下图：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081412402.png" loading="lazy"></p><p>结论是 Qwen3 在跑分上，确实是做到了各个指标都基本达到了开源第一，直逼闭源模型的程度。</p><span id="more"></span><p>说是当前 (2025.5.12) 第一开源模型也不为过。</p><p>除此之外，Qwen3 是目前唯一开源的<strong>混合推理模型</strong>，也是全球目前唯三的支持混合推理的模型。</p><p>这篇文章，就着重深入聊聊 Qwen3 的混合推理。</p><h2 id="从-reasoning-model-到-hybrid-reasoning-model"><a href="#从-reasoning-model-到-hybrid-reasoning-model" class="headerlink" title="从 reasoning model 到 hybrid reasoning model"></a>从 reasoning model 到 hybrid reasoning model</h2><p>这里 reasoning，其实是一个比较有趣又容易带来狭义的单词。</p><p>它本身是推理的意思。</p><p>但是大模型每一次的回答，其实也可以算是一次推理，比如很多文章中描述的，这个模型更小，<strong>推理</strong>速度更快。</p><p>也正是因为这个原因，在中文里，对于 reasoning，更多会翻译为<strong>思考</strong>来避免歧义。</p><p>比如 deepseek-r1 的<strong>深度思考</strong>，Qwen3 的发布 <a href="https://qwenlm.github.io/blog/qwen3/">blog</a> 中，也用的是<strong>思考模式</strong>(thinking model)。</p><p>不过为了全球统一，后文还是称 deepseek-r1 这样只支持深度思考的模型称为<strong>推理模型</strong>。</p><p>而 Qwen3, claude 3.7 这样的模型，称为<strong>混合推理模型</strong>。</p><p>而不支持深度思考的模型，称为<strong>普通模型</strong>(比如 deepseek-v3)。</p><p>对于推理模型，相信国内的用户大多是从 deepseek-r1 的深度思考接触到的。</p><p>当时这个深度思考，可谓是火遍大江南北。</p><p>不过追根溯源，第一个推理模型是 openai 于 2024 年 9.12 发布的 [o1-preview][<a href="https://openai.com/index/introducing-openai-o1-preview/]">https://openai.com/index/introducing-openai-o1-preview/]</a> 版。</p><p>深度思考的引入，解决了很多复杂的，需要反复推演的问题，比如 AIME (一堆比较复杂的数学题)，现在排名靠前的全是推理模型。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505122002776.png" loading="lazy"></p><p>ps: 这里其实有几个比较有趣的数据，排名 11 和 排名 16 的 deepseek-v3-0324 和 deepseek-v3，可以看出来经过调教的普通模型，在数学能力上也能大幅提升。而排名 13 的 claude-3.7-sonnet(thinking) 的数据，也说明了，未经调教的推理模型，也不见得在数学能力上能够大幅提升，但是比不思考效果确实要好。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505122015166.png" loading="lazy"></p><p>当然，在实际的各类大模型的聊天 app 中，基本都是支持开启思考和非思考的。</p><p>只不过相比于 Qwen3 这样的混合模型，背后是两个模型的切换。</p><p>比如 deepseek-r1 和 deepseek-v3，开启深度思考，就是 r1，关闭深度思考，就是 v3。</p><p>目前唯三的混合推理模型时间线如下：</p><p>2025.2.25 日，<a href="https://www.anthropic.com/news/claude-3-7-sonnet">claude 3.7 sonnet</a> 发布，是<strong>第一个支持混合推理的模型</strong>。</p><p>2025.4.17 日，<a href="https://developers.googleblog.com/en/start-building-with-gemini-25-flash/">gemini 2.5 flash</a> 发布，是第二个支持混合推理的模型。</p><p>2025.4.29 日，<a href="https://qwenlm.github.io/blog/qwen3/">Qwen3 系列发布</a>，是第三个支持混合推理的模型，<strong>第一个支持混合推理的开源模型</strong>。</p><h2 id="训练过程"><a href="#训练过程" class="headerlink" title="训练过程"></a>训练过程</h2><p>原本闭源模型的情况下，很难看出来(但是可以猜)，混合推理模型是如何训练出来的。</p><p>但是 Qwen3 开源后，直接把训练过程放出来了：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505122011660.png" loading="lazy"></p><p>主要是后训练阶段做的工作，先是进行推理的学习，也就是图中的 stage1 和 stage2，长思维链，就是推理模式的训练核心。</p><p>然后再在此基础之上进行非推理模式的学习和融合，也就是 stage3。</p><p>最终进行通用的强化学习，Qwen3 对于指令遵循，函数调用，风格遵循的能力也就是这个阶段进行的提升。</p><p>当然，这么说可能不太好理解，这里说一说更通俗的理解方式，所谓后训练，其实本质上也是灌一堆写好的对话给大模型。</p><p>比如，stage1 和 stage2 的训练数据可能长这个样子：</p><pre><code class="hljs python">messages = [    &#123;        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;system&quot;</span>,        <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;你是一名乐于助人的助手。&quot;</span>    &#125;,    &#123;        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;user&quot;</span>,        <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;1+1 等于几&quot;</span>    &#125;,    &#123;        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;assistant&quot;</span>,        <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;&lt;think&gt;用户问我 1+1 等于几，这是一个xxxx&lt;/think&gt;正常情况下等于2&quot;</span>    &#125;]</code></pre><p>相比于普通模型的后训练，多了 <strong>think</strong> 这样的标签(实际可能有所差别，毕竟训练数据并不会放出来，此处只是示例)。</p><p>而需要融合非推理模式时，也就是 stage3 时，可以灌入这样的对话：</p><pre><code class="hljs python">messages = [    &#123;        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;system&quot;</span>,        <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;你是一名乐于助人的助手。&quot;</span>    &#125;,    &#123;        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;user&quot;</span>,        <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;1+1 等于几&quot;</span>    &#125;,    &#123;        <span class="hljs-string">&quot;role&quot;</span>: <span class="hljs-string">&quot;assistant&quot;</span>,        <span class="hljs-string">&quot;content&quot;</span>: <span class="hljs-string">&quot;&lt;think&gt;&lt;/think&gt;正常情况下等于2&quot;</span>    &#125;]</code></pre><p>也就是直接给一个空的 <strong>think</strong> 标签。</p><p>这样，模型就也能学习到不需要思考时的回答。</p><p>如此一来，当外界不需要推理模式时，只需要在 token 化的时候，主动补充上一个空的 <strong>think</strong> 标签。</p><p>由于训练样本中，对于空的 think 已经进行了调教，所以此时大模型就只会补充后面的内容而不做思考。</p><p>从而达到混合推理的效果。</p><p>宽泛一点来讲，其实也能算是提示词工程，只是由于在模型训练阶段已经进行了调教，所以输出更稳定，更规范。</p><p>当然，这些都是个人的推测，并非官方的说明，仅供参考。也可以查阅<a href="https://zhuanlan.zhihu.com/p/1900555481715570305">这篇文章</a>。</p><p>而且，说起来简单，实际上训练过程，不论是数据的准备，后期的推理，都是不小的工程。</p><p>不然也不会到现在，也只有三个支持混合推理的模型了。</p><h2 id="混合推理模型一定是优势吗？"><a href="#混合推理模型一定是优势吗？" class="headerlink" title="混合推理模型一定是优势吗？"></a>混合推理模型一定是优势吗？</h2><p>说实话，从普通模型到推理模型，是称得上创新性的突破的。</p><p>但是从推理模型，到混合推理模型，我个人认为：很难称之为创新性的突破 (但是不蒸馒头争口气，Qwen3 第一个开源支持混合推理的模型，这口气妥妥争到了)。</p><p>首先，可以看看大概的一个训练过程：</p><p>预训练数据集 + 思考数据集 &#x3D; 推理模型</p><p>预训练数据集 + 非思考数据集 &#x3D; 普通模型</p><p>预训练数据集 + 思考数据集 + 空思考数据集 &#x3D; 混合推理模型</p><p>其实本质上只是补充了一个思考数据集为空的训练。</p><p>这在一定程度上，会增加模型训练的时长。</p><p>不过可能有读者和我一开始会有一样的困惑：</p><p><strong>会不会给模型带来负面效果？</strong></p><p>在研究之前，我是觉得可能会，毕竟对于模型来讲，多学习了一种思维模式。必然可能会带来理解成本。</p><p>但是实际上，从上面的训练过程能够看出来，只是多了一个空的 think 标签。</p><p>这点理解成本或者说模式的学习，对于大模型来讲可谓是洒洒水了。</p><p>毕竟不论是风格控制，指令遵循，工具调用，哪一个不比学习一个空标签来的复杂。</p><h2 id="混合推理模型的优势"><a href="#混合推理模型的优势" class="headerlink" title="混合推理模型的优势"></a>混合推理模型的优势</h2><p>首先是，争气了。别人有的，国产也有了，别人闭源，国产直接开源，还量大管饱。</p><p>其次，混合推理模型，可能后续，可以把是否思考，交给大模型来做判断。</p><p>除此之外，比如控制预算呀，这些谈不上优势，毕竟如果真想控制预算，完全可以自行切换推理模型和普通模型。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>一句话：Qwen3，担得起开源第一，混合推理，唯一开源的模型，不得不点赞。</p><p>最后是一些碎碎念了：</p><p>其实笔者本身也并非从业人员，只是单纯对大模型的实现细节比较好奇。</p><p>好在有大模型，我在遇到问题的时候，基础的通识，大模型可以迅速帮我补充。</p><p>而复杂一点的最新的一些技术(比如混合推理模型)，可以通过溯源的方式找到最初的文章，自行研究。</p><p>配合之下，也能让笔者这样一个外行，一窥门道。</p><p>不过越研究大模型，越觉得它可能还是存在一些瓶颈。</p><p>不论是函数调用，指令遵循，推理模式，本质其实都是在训练集中加入指定格式的数据。</p><p>不过话说回来，这倒是也接近我们人类的学习方式：</p><p>想想我们学习一门编程语言的时候，其实也是，不断的输入指定格式。</p><p>然后才慢慢理解了这个编程语言。</p><p>最后，Qwen3 还针对性的对函数调用、mcp，agent 的能力做了加强，可以点个关注，等后续更新。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505132257677.png" loading="lazy"></p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/ee8b94f9/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/ee8b94f9/"/>
    <published>2025-06-10T15:14:00.000Z</published>
    <summary>
      <![CDATA[<p><a href="https://mp.weixin.qq.com/s/CZE3NebDV2Y1CkNh7UN5wg">上一篇文章：深入聊聊 Qwen3：我们外行除了高呼沸腾了，还应该知道什么?
</a>，深入分析了 Qwen3 的跑分评测是否有水分，如下图：</p>
<p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081412402.png" loading="lazy"></p>
<p>结论是 Qwen3 在跑分上，确实是做到了各个指标都基本达到了开源第一，直逼闭源模型的程度。</p>]]>
    </summary>
    <title>深入聊聊Qwen3的混合推理：全球唯三，开源唯一</title>
    <updated>2026-07-08T10:35:50.891Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>距离 Qwen3 发布 (2025.4.29) 已经过去一周多。</p><p>很多追热点的文章也早也发布完成。</p><p>所谓内行看门道，外行看热闹。</p><p>我们透过新闻看到的，多数也就只是热闹：今天这个模型发布，杀疯了；明天那个模型发布，秒天秒地秒空气。</p><p>这一次，作为一名外行但又不那么外行的程序员，我决定静下心来：</p><p>从最初的源头：Qwen3 的评测跑分数据入手。</p><p>结合评测官方最新的排行榜，对 Qwen3 真实的性能做一次完整的复盘。</p><p>试试窥一窥内行的门道，看看 Qwen3 是否真如传闻的那般沸腾?</p><span id="more"></span><h2 id="溯源"><a href="#溯源" class="headerlink" title="溯源"></a>溯源</h2><p>在如今这个时代，小某书也好，某音也罢，声音太多了。</p><p>一早上起来，就是 deepseek-r2 准备发布了，而事实是截止本文写作之时，deepseek-r2 都没有发布。</p><p>所以在接下来处理 Qwen3 发布的消息时，对于这些声音，我会直接忽略。</p><p>直接直捣黄龙：<a href="https://qwenlm.github.io/blog/qwen3/">https://qwenlm.github.io/blog/qwen3/</a></p><p>也就是 Qwen3 的官网的博客页面。</p><p>从这个博客页面上，也能看到，当下比较火的一些大模型平台：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/20250507112755.png" loading="lazy"></p><p>比如 hugging face, modelscope, kaggle 等。</p><p>好，废话不多说，找到了源头，接下来才算进入正文。</p><h2 id="跑分评测有没有水分"><a href="#跑分评测有没有水分" class="headerlink" title="跑分评测有没有水分"></a>跑分评测有没有水分</h2><p>以下是官方文档的第一段话和官方的评测图：</p><blockquote><p>今天，我们宣布推出 Qwen3，这是 Qwen 系列大型语言模型的最新成员。我们的旗舰模型 Qwen3-235B-A22B 在代码、数学、通用能力等基准测试中，与 DeepSeek-R1、o1、o3-mini、Grok-3 和 Gemini-2.5-Pro 等顶级模型相比，表现出极具竞争力的结果。此外，小型 MoE 模型 Qwen3-30B-A3B 的激活参数数量是 QwQ-32B 的 10%，表现更胜一筹，甚至像 Qwen3-4B 这样的小模型也能匹敌 Qwen2.5-72B-Instruct 的性能。</p></blockquote><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505071521964.png" loading="lazy"></p><p>我相信很多人看到这张官方的图，会觉得哇塞，几乎秒杀了其他模型啊。</p><p>仔细看也确实如此，各项指标，甚至 32B 的模型，都可以和 deepseek-R1，openai-o1 一较高下。</p><p>而 Qwen3 官方也是把最优秀的数字放在了开头：ArenaHard (构建复杂的提示词让模型回答后，由 AI 评判打分)。</p><p>95+ 的成绩基本上处于绝对一流的水准。</p><h3 id="ArenaHard"><a href="#ArenaHard" class="headerlink" title="ArenaHard"></a>ArenaHard</h3><p>但是问题是，真的是这样吗？</p><p>对，也不对。</p><p>我们看到的是 Qwen3 放出了一个非常亮眼的数字，95.6。</p><p>对于 ArenaHard v0.1 (官方排名在 2024.11.14 停止更新)，应该确实是这样的。</p><p>从 ArenaHard v0.1 的官方数据来看(如下图)，顶尖模型的得分基本就在 86 ~ 92 分，略有饱和的趋势，95.6 绝对第一名。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505071538348.png" loading="lazy"></p><p>但是 Qwen3 官方没有提及的，就是 ArenaHard v2.0 的数据。</p><p>v0.1 排名在 2024 年停止更新后，就开始了 v2.0 的预览版。</p><p>Qwen3 并没有放出来 v2.0 的跑分，不过这也确实和 v2.0 正式发布时间太短有关 (2025.4.23 正式推出)。</p><p>在 v2.0 的评测问题上，划分的更细：</p><p>一方面增加了多语言的评测，另一方面类型上也增加了创意写作类型。</p><p>而在 v2.0 的结果上，Qwen3 就显得离第一梯队还有距离了(来自 ArenaHard 官方数据)：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505071551564.png" loading="lazy"></p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505071552240.png" loading="lazy"></p><p>当然，这个排名也可能有所偏差，毕竟对于这个评测系统来言，由于是 AI 评判的，而官方裁判就是 gemini-2.5 和 gpt-4.1，不好说是否存在偏见。</p><p>从数据上看，Qwen3 在复杂提示词和风格控制的处理上，基本和 deepseek-r1 打平，但是 32B 是决计没办法与之匹敌的。</p><p>再往前还有 claude-3.7 和 openai 的 o1, o4 以及 gemini 模型。</p><p>而创意写作的跑分上，也是基本处于同样的地位。</p><p>还不错，但是秒杀一众模型，32B 模型匹敌 deepseek-r1，还是略微有点言过其实了。</p><h3 id="AIME"><a href="#AIME" class="headerlink" title="AIME"></a>AIME</h3><p>接下来是 AIME’24 和 AIME’25 了(24，25 就是 2024 年和 2025 年)。</p><p>英文全称是： American Invitational Mathematics Examination</p><p>直接翻译过来就是美国邀请数学考试，一共 30 道题，答案都是 0~999 内的整数。</p><p>而邀请，指只有在 AMC12 考试中表现前 5% 的高中生可以参加。</p><p>对此，以我看短视频的经验，对美国高中生的数学水平表示怀疑，这题目，应该也不难吧。</p><p>可以浅浅看几道：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505071709158.png" loading="lazy"></p><p>感觉应该是不太难，但是要我来做的话，看懂题目都略微有点费劲。</p><p>但是这一个测试集的结果可能并不靠谱，因为题目和答案都是公开的。</p><p>这些信息可能会包含在训练集中，可能会直接命中，从分数来看，AIME 2024 的得分，明显高于 AIME 2025，不能排除训练集命中的影响。</p><p>这是 AIME 在 vals.ai 上的排名，Qwen3 确实表现优异：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505080911512.png" loading="lazy"></p><p>而且，最亮眼的是在成本上，花费仅 1 美金，是一些模型的十分之一都不止。</p><p>这个排名中，也有之前爆火的 deepseek-03-24：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505080912695.png" loading="lazy"></p><p>得分并不高，这应该和没有 深度思考 功能有关，毕竟这些数学题，不分析一下，直接回答，真的很难做出来。</p><h3 id="liveCodeBench"><a href="#liveCodeBench" class="headerlink" title="liveCodeBench"></a>liveCodeBench</h3><p>Qwen3 官方评测图标注了 v5, 2024.10~2025.2。</p><p>v5 对于这个测试集来讲，属于比较老的一个版本了，在 2024.10 ～ 2025.2 这个时间里，包含了 166 个算法题，liveCodeBench 官方的排行只更新到 2025.2。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505080937912.png" loading="lazy"></p><p>Qwen3 的评分 70 确实可以跻身第一梯队。</p><p>在最新版本的排行中纳入了 Qwen3，如下图：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505080941591.png" loading="lazy"></p><p>确实还是很能打的，基本处于第一梯队。</p><h3 id="codeForces"><a href="#codeForces" class="headerlink" title="codeForces"></a>codeForces</h3><p>这个评测也是算法题，包含一些动态规划，图论这些面试常见的内容。</p><p>但是这个评测原本是给人类提供的比赛，没有官方的对于大模型的评分。</p><p>人类参赛选手前 1000 名都在 2200+，第一名更是 3800+。</p><p>Qwen3 是 2000 分，已经是非常优异的成绩了，而 32B 模型的 1900 分也非常出色。</p><p>在做算法题这一项上，应该是超越绝大部分程序员了。</p><h3 id="Aider"><a href="#Aider" class="headerlink" title="Aider"></a>Aider</h3><p>Aider 是一个类似于 cline, cursor 的代码生成和修改的工具，只是相比于 Cursor 等范畴更小，并没有提供 IDE 而是只提供了命令行的能力。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081024650.png" loading="lazy"></p><p>不过可能也正是因为只提供了命令行操作的方式，反而被用来做大模型修改代码的评测。</p><p>以下是官方的排名(还未更新 Qwen3)：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081026845.png" loading="lazy"></p><p>Qwen3 的评分是 61.8，基本处于 5-8 名的位置。花费估计也不高，毕竟 Qwen3 特地标注，没有开启思考模式，从而降低开销。</p><p>这里面比较有趣的就是 deepseek 了，r1 和 deepseek-0324 花费可谓是断崖式的下降，这样的评分 + 如此低的花费，也不难怪之前红极一时了。</p><p>可以看出来，Qwen3 在代码生成和编辑的能力上，排行还是不错的。</p><h3 id="LiveBench"><a href="#LiveBench" class="headerlink" title="LiveBench"></a>LiveBench</h3><p>LiveBench 这个评测是专为大模型设计的，特点如下：</p><ol><li>LiveBench 通过定期发布新问题来限制潜在的污染。</li><li>每个问题都有可验证的、客观的基本事实答案，无需 LLM 法官。</li><li>LiveBench 目前包含一组 18 种不同的任务，涵盖 6 个类别，随着时间的推移，我们将发布新的、更难的任务。</li></ol><p>看起来比 ArenaHard 请 AI 来做裁判更为靠谱一些。</p><p>Qwen3 的得分是 77.1，官方当时还没有收录，2024-11-25 的排行如下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081036020.png" loading="lazy"></p><p>可以排到第二，如果统计范围扩大到 2025-04-25，这个分值排名会稍微靠后一些：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081037861.png" loading="lazy"></p><p>略高于 deepseek-r1，与 Claude 3.7 非常接近。</p><h3 id="BFCL"><a href="#BFCL" class="headerlink" title="BFCL"></a>BFCL</h3><p>全称： Berkeley Function Calling Leaderboard</p><p>翻译过来就是 伯克利工具调用排行榜，从名字上也能看出来，核心在于评估模型的函数调用能力。</p><p>而 V3 版本，就是这个评测集的最新版本，包含了多轮交互的工具调用。</p><p>Qwen3 的评分为 70.8，可谓妥妥的第一梯队了，这一项上，远超 deepseek-r1。</p><p>官方数据如下（2025-04-25）:</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081052096.png" loading="lazy"></p><p>Qwen3 235B 基本排行在第六的位置，而 32B 的模型，也能跻身前 6。</p><p>deepseek-r1 56.9 的成绩，排行 47。</p><p>不得不说，Qwen3，这个数据还是很能打的。</p><h3 id="MultiIF"><a href="#MultiIF" class="headerlink" title="MultiIF"></a>MultiIF</h3><p>这个评测，是 facebook 开源的一个用于评估多语言能力的，该基准旨在评估 LLM 模型在多语言环境中多轮指令跟随能力的性能。</p><p>但是并没有官方的排行，从数据集里面看，主要就是考察模型的多语言能力。</p><p>Qwen3 本身支持 100+ 语言，在多语言这条路上，我觉得应该是非常可以的。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>单纯看 Qwen3 官方发布的评测结果来看，确实是非常能打的一个模型。</p><p>而深入去看每一个评测的官方排名时，更能看出 Qwen3 如今的地位：235B 模型基本排名前 10，32B 模型基本处于前 20。</p><p>对于 Qwen3 官方的评测图，存在水分但是差不太多，点评如下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202505081412402.png" loading="lazy"></p><p>ArenaHard v2.0 (AI 做裁判的评测):</p><p>在复杂提示词和语调控制上，得分 58.4，暂居第 9 (32B 模型排名 16)，略逊色于 claude3.7，gemini-2.5 以及 o3, o1 等。略高于 deepseek-r1 (58.0)。</p><p>在创意写作上，得分 73.5，暂居第 5 (32B 模型排名)，略逊色于 o3, gemini-2.5 和 deepseek-r1 (77)。</p><p>AIME (做数学题)：暂居第五(84)，但是开销最小，强于 deepseek-r1 (74)。略逊色于 o3, gemini 和 grok。</p><p>LiveCodeBench (做算法题)：最新版排名第 7。</p><p>Aider (改代码，生成代码)：排名第 7，花费也预计不会太高。</p><p>LiveBench (六大维度)：最新排名第 8，deepseek-r1 第 9，而 32B 模型，可以排名第 10，非常能打。</p><p>BFCL v3 （函数调用）：排名第 6，32B 模型排名第 7。</p><p>从评测结果来看，Qwen3 确实是一个值得沸腾的产品，作为一款国产的开源大模型，能够在诸多评测中脱颖而出，可谓是非常难得了。</p><p>并别提其独有的混合思考能力和 119 种多语言的支持。</p><p>在大模型这条路上，可以看出，Qwen 是下了大功夫的。</p><p>至于官方发布文章中提到的混合思考能力，独有的 mcp 支持，原本是打算放在一篇文章中的。</p><p>但是不曾想一个评测的篇幅就已经很长了，这些内容将会放在下一篇文章。</p><p>可以点个关注再看追更哦。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/db972afb/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/db972afb/"/>
    <published>2025-05-08T15:14:00.000Z</published>
    <summary>
      <![CDATA[<p>距离 Qwen3 发布 (2025.4.29) 已经过去一周多。</p>
<p>很多追热点的文章也早也发布完成。</p>
<p>所谓内行看门道，外行看热闹。</p>
<p>我们透过新闻看到的，多数也就只是热闹：今天这个模型发布，杀疯了；明天那个模型发布，秒天秒地秒空气。</p>
<p>这一次，作为一名外行但又不那么外行的程序员，我决定静下心来：</p>
<p>从最初的源头：Qwen3 的评测跑分数据入手。</p>
<p>结合评测官方最新的排行榜，对 Qwen3 真实的性能做一次完整的复盘。</p>
<p>试试窥一窥内行的门道，看看 Qwen3 是否真如传闻的那般沸腾?</p>]]>
    </summary>
    <title>深入聊聊Qwen3：我们外行人除了高呼沸腾了，还应该知道什么?</title>
    <updated>2026-07-08T10:35:50.891Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <category term="javascript" scheme="https://blog.maxiaobo.com.cn/tags/javascript/"/>
    <content>
      <![CDATA[<p>多大算大龄？</p><p>小明 18 岁高中毕业就开始工作，30 岁，就已经工作了 12 年。</p><p>小马 26 岁研究生毕业，30 岁，才不过工作 4 年的小喽啰。</p><p>小王 30 岁博士生毕业，30 岁，甚至都还没开始工作。</p><p>同样是九几年出生，小欢晚上了两年学，小石一不小心多读了几年书，就来到了所谓的 “大龄”危机。</p><span id="more"></span><p>好了，言归正传，去年我因为很多原因离开了职场，而原本在走的所谓正道：努力工作，升职加薪，也不复存在。</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>和普通的裁员不同，秉持着一直兢兢业业尽职尽责的工作态度，反而让我对整个职场失望透顶：大家都在”认真”表演着 owner 的角色，而只有你真的把自己当 owner。</p><p>说实话，我的离职能够如此顺利，也是在无数陌生人的帮助下，算是收获了一场还算圆满的结局。</p><p>而我的乐观，自信，在这场圆满下，得到了保留。</p><p>只是这些，远不足以对抗那些早就埋下的祸端。</p><p>不过这是后话了，离职伊始，多多少少，是不太能像现在这么豁达的。</p><p>所以，当时，排解这份郁闷的方式，就是读书，写文章，旅游。</p><h3 id="人间烟火气，最抚凡人心"><a href="#人间烟火气，最抚凡人心" class="headerlink" title="人间烟火气，最抚凡人心"></a>人间烟火气，最抚凡人心</h3><p>“人间烟火气，最抚凡人心”。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291455408.jpg" loading="lazy"></p><p>在行至绍兴一家小茶馆的时候，无意间看到的这句话，可以说是当时，最符合我心境的一句话。</p><p>事是 1 月 19 日落幕的。</p><p>篁岭是 1 月 24 日玩的。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291456892.jpg" loading="lazy"></p><p>文章是 1 月 27 日写的。<a href="https://mp.weixin.qq.com/s/eMYB7XaV2T4jrfE__3AjHA">《2023 被裁两次，一位 30 岁程序员的年终总结<br>》</a>。</p><p>绍兴是 1 月 29 日玩的。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291456076.jpg" loading="lazy"></p><p>开放麦是 1 月 31 日讲的。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291500897.jpg" loading="lazy"></p><p>仙本那是三月份去玩的，本来只是打算在欧洲签证未下来之前打发点时间。</p><p>却不曾想，直到现在也总是时常梦回那一片大海，只有自己，而一缕阳光穿越海水，正好打在向上游动的海龟。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291502241.jpg" loading="lazy"></p><p>水上，一片嘈杂，水下，一切的喧嚣都消失。那一刻，呼吸仿佛都静止，只有大海龟缓缓扇动的翅膀，在提醒我，时间还在流逝。</p><h3 id="大概是老了才”能”去的欧洲"><a href="#大概是老了才”能”去的欧洲" class="headerlink" title="大概是老了才”能”去的欧洲"></a>大概是老了才”能”去的欧洲</h3><p>“你是来留学的吧？”</p><p>“不，我只是不想等老了，等有时间了，才去看这世界”。</p><p>去程的飞机上，比较意外的是，基本都是上了年纪的叔叔阿姨。</p><p>而这个问题，也是下飞机后，众多阿姨中的随口一问。</p><p>似乎，去欧洲，更多的就是留学。</p><p>也是，即便是直飞的飞机，都要 10 多个小时，来回一趟两三天都在路上了，上班族即便狠得下心清空年假，也会被节假日高额的消费吓退。</p><p>但是谁不想去看看啊？</p><p>历史课本上提到的，水上城市威尼斯，世界名画蒙娜丽莎。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291658580.jpg" loading="lazy"></p><p>总是让我魂牵梦萦。</p><p>在去程飞机上，看着一飞机的叔叔阿姨，也是对自己的决定充满了庆幸：</p><p>还好，我不是老了，才去的欧洲。</p><p>在西班牙吃西班牙海鲜饭。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291643407.jpg" loading="lazy"></p><p>在巴塞罗那看高笛四件套。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291522084.jpg" loading="lazy"></p><p>在巴黎歌剧院听歌剧，在凡尔赛宫看镜厅，在卢浮宫里看胜利女神，看蒙娜丽莎。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291522724.jpg" loading="lazy"></p><p>在瑞士，看…emmmm，自然风光吧，虽然总是一个人走在空无一人的街道上。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291524318.jpg" loading="lazy"></p><p>在意大利吃意面，看古罗马斗兽场。</p><p>在威尼斯，体验这水城的乐趣。</p><p>也去这世界上最小的国家 — 梵蒂冈看圣彼得堡大教堂。</p><p>这世界，我算是看了一角了。</p><h3 id="意料之外的记忆，意料之内的拒绝"><a href="#意料之外的记忆，意料之内的拒绝" class="headerlink" title="意料之外的记忆，意料之内的拒绝"></a>意料之外的记忆，意料之内的拒绝</h3><p>从欧洲回来，就是 6 月份快 7 月份了，距离我离职，也接近半年。</p><p>其实出发前我就猜到：</p><p>这一去，这么长的 gap，怕是回来就不太好找工作了。</p><p>大公司基本走到最后一轮，最后以不稳定告终。</p><p>而中小公司，基本上聊的还行，但是聊到薪资基本都是不了了之。</p><p>国内，海外，基本都有投递，但多数都是拒绝。</p><p>算是意料之内吧。</p><p>而意料之外的是，互联网上，基本没有人记得之前离职的事情了。</p><p>想想自己想法有时候还是挺天真的：</p><p>那天在 xx 局外，下着雨，坐在车里，想着，闹出这么大动静，以后怕是没有机会再在这个行业混了。</p><p>而事实是：充其量最多也就一点小浪花，哪怕我说了，也没几个人会记得。</p><p>只不过，确实也有公司，面试的时候只问技术，只问项目。</p><p>不关心你的 gap，不问你在某里为什么就待几个月，也不会在走完流程后告知稳定性不够。</p><p>我想，这便是我的柳暗花明了吧。</p><h2 id="歪路探索"><a href="#歪路探索" class="headerlink" title="歪路探索"></a>歪路探索</h2><h3 id="前端校招训练营"><a href="#前端校招训练营" class="headerlink" title="前端校招训练营"></a>前端校招训练营</h3><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><h3 id="鱼皮小号、敖丙儿子-—-拆解系列的收获"><a href="#鱼皮小号、敖丙儿子-—-拆解系列的收获" class="headerlink" title="鱼皮小号、敖丙儿子 —- 拆解系列的收获"></a>鱼皮小号、敖丙儿子 —- 拆解系列的收获</h3><p>这是在前端校招训练营模仿的过程中发现的一条路径。</p><p>我做不好，那别人怎么做好的？</p><p>索性就一个一个拆解一把。</p><p>第一个自然是鱼皮了。</p><p>流量妥妥的有了，骂声居多：</p><p>“他算什么大佬，他是水货，韭菜皮，韭菜星球创始人”</p><p>而我，也收获了第一个外号：”鱼皮小号”</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291659234.jpg" loading="lazy"></p><p>之后，敖丙，java3y，双越老师，</p><p>也顺利收获了第二个外号：”敖丙儿子”。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202504291700430.jpg" loading="lazy"></p><p>这个系列怎么说呢，在我们程序员这个群体并没什么竞争对手。</p><p>但是也并没有带来什么实际的收益。</p><p>反而在分析的过程中，产品，流量，知识付费的一些方法论。</p><p>马马虎虎算是学到点皮毛吧，后续有机会再分享。</p><h3 id="做点产品试试看吧-—-今天你拉了么"><a href="#做点产品试试看吧-—-今天你拉了么" class="headerlink" title="做点产品试试看吧 —- 今天你拉了么"></a>做点产品试试看吧 —- 今天你拉了么</h3><p>独立开发者的三件套：记账，日记，TODO。</p><p>后知后觉，狠狠戳中。</p><p>今天你拉了么，就是一款记录拉粑粑的 TODO。</p><p>但也正是这款产品，也算是跟 AI 搭上边。</p><p>用户记录粑粑时间，我借助 AI 来进行预测。</p><p>核心就是提示器 + 格式化输出。现在来看就是洒洒水，当时还是觉得挺有趣的。</p><p>当然，除此之外，也尝试了几款 AI 搭边的产品，可以看 <a href="https://mp.weixin.qq.com/s/q0BxgiTDjilKPUJFQkvSLg">《当抠门程序员，遇到了免费 AI 大模型，我好像用 AI 赚到钱了？<br>》</a></p><p>整体下来，其实并没有折腾出来什么好的路径。</p><p>思路开拓不少，但是一顿操作猛如虎，一看战绩 0&#x2F;5。</p><p>在副业发展上，还得练，就当给大家分享下失败的经验吧！</p><h2 id="重回职场"><a href="#重回职场" class="headerlink" title="重回职场"></a>重回职场</h2><p>其实再次回归职场，我的很多想法都发生了改变：</p><h3 id="不再在意周末"><a href="#不再在意周末" class="headerlink" title="不再在意周末"></a>不再在意周末</h3><p>学习自 <a href="https://mp.weixin.qq.com/s/X9YXXCk3iUpGtEyCKGHxtw">周末长文肝王：小林 coding</a>。</p><p>终究是给自己打工的，和公司更多的是合作关系。</p><p>那么周末，周内，又有什么区别呢？</p><p>当然，也不是说周末不休息，只是不需要”报复性”的休息。</p><p>不需要在你想做点什么事情，仅仅因为是周末而觉得不应该做的时候不做。</p><h3 id="不再着迷于技术"><a href="#不再着迷于技术" class="headerlink" title="不再着迷于技术"></a>不再着迷于技术</h3><p>这一点，是来自于我的一些体验。</p><p>一个产品，开发只是一个环节。</p><p>idea，获客，流量 都是必不可少。</p><p>以前看到万事俱备，只差一个程序员，我会觉得瞎扯。</p><p>现在我会觉得，技术可能反而才是最廉价的。</p><p>高端的 app，往往只需要简单的技术。</p><p>尤其是现在 AI 加成，一个 1w 月薪的程序员，基本能完成以前 3w 左右的程序员的工作。</p><p>别一开始就什么高并发，长列表，什么这性能不好，那框架太 low。</p><p>先干，产品能不能活都是一个问题呢，你还在担心这个列表太长，渲染会卡顿？</p><h3 id="疯狂清负债"><a href="#疯狂清负债" class="headerlink" title="疯狂清负债"></a>疯狂清负债</h3><p>再就业之后，我好像病了。</p><p>因为第一份工作在 51 信用卡，耳濡目染之下，我是一位有非常多信用卡的用户。</p><p>为了薅羊毛，我办了 n 张卡。</p><p>什么广发生活日，招行星期三，山姆返利。</p><p>买车了，我办一张车主卡。</p><p>去欧洲，我办一张欧元卡。</p><p>去日本，我办一张 JCB 卡。</p><p>机场贵宾厅，体检？羊毛搞起来。</p><p>对于信用卡这个糖衣炮弹，我往往是拿走糖衣，推出炮弹。</p><p>但是实际上，常在河边走，哪有不湿鞋。</p><p>终究还是掉进了”超前消费”这个陷阱里。</p><p>除此之外，房贷这个事情，哈哈，想必很多程序员都会经历。</p><p>买房时，这点月供，公积金就覆盖了，正愁公积金没办法取呢。</p><p>失业后，每个月啥也不干，几大千一两万就出去了。</p><p>从这个角度看，呼吸其实是收费的。</p><p>一个人生活成本才几千块钱，而房贷几大千就没了？</p><p>直到每次看账单的时候我才意识到：</p><p>自己扣扣搜搜，一个 ps5，switch2，手机都不舍得换的几千块钱，都比不过一个月的房贷。</p><p>还真是应了那句话：</p><p>几千块钱的小件扣扣搜搜，几万的房贷说背就背。</p><p>停掉几乎所有的信用卡，工资到手就转房贷，消费贷。</p><p>这就是我生的病：时不时的打开银行 app，还个几千几万的，就开心。</p><p>就叫”还银病”吧，我只是一个无情的工资路由器。</p><h3 id="持续非技术向的”学习”"><a href="#持续非技术向的”学习”" class="headerlink" title="持续非技术向的”学习”"></a>持续非技术向的”学习”</h3><p>非技术向的学习。</p><p>有时候我们程序员会陷入一种自我提升的假象中：</p><p>新框架出来了，我去学习一下。</p><p>殊不知，学习新框架，新技术这项任务，是最容易带来自我提升错觉的事情。</p><p>我学了，我更厉害了。</p><p>但是，裁员可不是因为你技术不厉害，仅仅只是公司决策者的一句话。</p><p>模仿，思考，学习，看这个社会，别听别人说什么，而是多想想，别人没说什么。</p><p>就像我在这篇文章里，我是说了很多，但其实我没说的也很多。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>杀疯了，深夜放毒，偷袭，追热点。</p><p>试过了，这些可能并不是我擅长的输出路径。</p><p>深度思考后的产出，可能是一条更适合我的路径。</p><p>索性就这么做吧。</p><p>也非常感谢能读到这里的你。</p><p>最后，灯已熄，酒已尽，我的故事就到这里了。</p><p>下次，酒我备，希望可以听听你的故事。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/bb4c7a7e/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/bb4c7a7e/"/>
    <published>2025-04-29T17:27:30.000Z</published>
    <summary>
      <![CDATA[<p>多大算大龄？</p>
<p>小明 18 岁高中毕业就开始工作，30 岁，就已经工作了 12 年。</p>
<p>小马 26 岁研究生毕业，30 岁，才不过工作 4 年的小喽啰。</p>
<p>小王 30 岁博士生毕业，30 岁，甚至都还没开始工作。</p>
<p>同样是九几年出生，小欢晚上了两年学，小石一不小心多读了几年书，就来到了所谓的 “大龄”危机。</p>]]>
    </summary>
    <title>还是聊聊吧：&quot;大龄&quot;程序员失业的一年</title>
    <updated>2026-07-08T10:35:50.892Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>以 Cursor 为代表的 AI IDE，带来了数字世界建设成本的大幅降低，这在数次技术变革中，都可以说是一个非常好的兆头。</p><p>而这其中，程序员作为原本数字世界的建设者: <code>y = 2x + 1</code>，简单粗暴的公式，是我目前对 Cursor 增幅的答案。</p><p>其中，x 代码程序员当前的月薪，y 是 Cursor 加成后的月薪。</p><p>乘 2，表示原本能力越高的程序员，增幅越大。</p><p>加 1，表示对所有人的加成，0 基础，现在也可以充当 1w 月薪的程序员。</p><span id="more"></span><h2 id="0-基础秒变-1w-月薪"><a href="#0-基础秒变-1w-月薪" class="headerlink" title="0 基础秒变 1w 月薪"></a>0 基础秒变 1w 月薪</h2><p>前一段时间，小红书上爆火的，我是 x 厂产品经理&#x2F;设计师，借助 Cursor，用时 1 周，上线了人生 APP。</p><p>虽然事后挺多都有批露，说这只是个流量密码，背后大概率其实都是摸爬滚打多年的所谓独立开发者。</p><p>但是更多的人是旁观者，就像小马过河一样，水的深浅，只有自己尝试过才知道。</p><p>我在前一段时间，花了 3 个小时，上线了<a href="https://mp.weixin.qq.com/s/fChPX6jYY3-t2QEHxmTb3g">房来财小程序</a>。</p><p>之后，又花了半个小时，简单复刻了下 tinypng，这个非常常见的图片压缩工具,效果可以看 <a href="https://blog.maxiaobo.com.cn/my-tinypng/">mytinypng</a>。</p><p>可以打包票的说，Cursor <strong>非常公平</strong>的拉高了所有人的下限，像这款工具里的图片拖拽压缩，压缩后的图片左滑右滑对比，让程序员来实现的话，没个半天还搞不定，更别提大部分程序员还要应付需求评审，技术方案评审等等开不完的会。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503131053178.png" loading="lazy"></p><p>但是你说他完全能让普通人，披荆斩棘，1 天上线人生 APP? 粗枝大叶出个雏形还行，你说”人生”，至少目前还是差了点火候。</p><p>往往都是，生成 10s，修改半个钟。普通人的修改，可就没这么顺利了。</p><p>我和 Cursor 的爱恨情仇，可以看看 <a href="https://mp.weixin.qq.com/s/fChPX6jYY3-t2QEHxmTb3g">Cursor 初体验，2 小时上线房来财小程序</a>。</p><h2 id="所有人的门槛低了，就表示没有降低"><a href="#所有人的门槛低了，就表示没有降低" class="headerlink" title="所有人的门槛低了，就表示没有降低"></a>所有人的门槛低了，就表示没有降低</h2><p>确实，对于非程序员来说，Cursor 给了他们开发的能力。</p><p>但是对应的，对于程序员来说，Cursor 其实也给了他们原本不具备的能力。</p><p>比如常见的分工，前端，后端，侧开，算法，包括运维。</p><p>前端写后端，门槛降低，后端写前端，门槛降低。</p><p>至于测开，应该也是爽歪歪了。</p><p>而运维的工作，很多前端后端，都可以借助 Cursor 具备一个基础的能力。比如在开发 tinypng 的过程中，Cursor 就给我指出了流水线的搭建和 github 部署的详细说明。</p><p>至于算法工程师，也好把自己的想法快速搭建前端页面，而前端，也可以趁机去拱一把算法。</p><p>不论如何，如果所有人跨出去的门槛都降低了，那其实就是所有人都没有降低。</p><p>那总该有什么降低了吧，我认为的答案是<strong>生产力成本降低</strong>。</p><p>纵观每一次技术革命，开端都是生产力成本的极大降低，从宏观来说，以 Cursor 为代表的一系列 AI IDE 工具，就是份能够降低数字世界生产力成本的工具。</p><h2 id="需求会不断增长，底层逻辑是不会变的"><a href="#需求会不断增长，底层逻辑是不会变的" class="headerlink" title="需求会不断增长，底层逻辑是不会变的"></a>需求会不断增长，底层逻辑是不会变的</h2><p>那么生产力成本的降低，会带来大量失业吗？</p><p>纵观历史，答案是不会。道理也很简单，人的需求，永远都是无穷无尽的。</p><p>有自行车，我还要火车，汽车，还不够，还要飞机，还要传送阵，瞬移。</p><p>从科幻电影也能看出来，头号玩家构建的世界，我们到现在都还没有实现。</p><p>而这个数字世界，单纯靠人工程序员一行一行的敲，是不太可能的。</p><p>借助 AI，才会让这一切变的有可能。</p><p>3D，4D，真的沉浸吗？这和我理解的沉浸还差太多。</p><p>我不想看，也不想听玄幻小说，就不能打造一个玄幻世界给我沉浸式体验吗？</p><p>这一切的基础，都需要玄幻小说中的”言出法随”，说一句话，实现我想要的数字世界，如果纯靠人工，手指头打爆了都实现不了。</p><p>上述也只是冰山一角，<strong>越来越高的生产力，将会反过来带来越来越多个性化的需求，必然带来大量的就业岗位</strong>。</p><p>当然还有一个更底层的道理，<strong>水能载舟，亦能覆舟</strong>。都失业了，那还要公司生产什么呢？</p><h2 id="停止焦虑"><a href="#停止焦虑" class="headerlink" title="停止焦虑"></a>停止焦虑</h2><p>很多媒体现在会说，xx 工具，取代程序员。</p><p>程序员焦虑到失业掉头发。</p><p>前端已死，客户端凉透了。</p><p>真信了，平白焦虑，付出心力，然后报课，那你就是那个大冤种。</p><p>反之，拥抱 AI，拥抱 Cursor，也许从十年后再看现在，依旧遍地都是黄金。</p><p>最后，别忘了关注我的公众号：程序员芋仔。持续干货输出。</p><p>AI 程序员时代，我也组建了一个前端抱团取暖群，欢迎加我微信来撩：mxb151，加群备注简单的自我介绍和加群哦。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/fd8b36f1/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/fd8b36f1/"/>
    <published>2025-04-13T20:04:00.000Z</published>
    <summary>
      <![CDATA[<p>以 Cursor 为代表的 AI IDE，带来了数字世界建设成本的大幅降低，这在数次技术变革中，都可以说是一个非常好的兆头。</p>
<p>而这其中，程序员作为原本数字世界的建设者: <code>y = 2x + 1</code>，简单粗暴的公式，是我目前对 Cursor 增幅的答案。</p>
<p>其中，x 代码程序员当前的月薪，y 是 Cursor 加成后的月薪。</p>
<p>乘 2，表示原本能力越高的程序员，增幅越大。</p>
<p>加 1，表示对所有人的加成，0 基础，现在也可以充当 1w 月薪的程序员。</p>]]>
    </summary>
    <title>Cursor与程序员，从来都不是你死我活</title>
    <updated>2026-07-08T10:35:50.886Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>今天一觉醒来，又被国运级产品刷屏了。</p><p>这次是 deepseekv3 0324 版本，模型是 3 月 24 晚上开源的，刷屏是 3.25 凌晨，不知道多少自媒体熬夜开更。</p><p>在现在的这个信息爆炸，自媒体的话，夸张的嫌疑会高一些。</p><span id="more"></span><p>很多真正关心的信息，反而还是要自己去源头看，道听途说反而一知半解。</p><p>发布的源头在这里： <a href="https://huggingface.co/deepseek-ai/DeepSeek-V3-0324">https://huggingface.co/deepseek-ai/DeepSeek-V3-0324</a></p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251629099.png" loading="lazy"></p><p>一个是各种测试指标的提升，但是都是一些基础指标。</p><p>其他更令人注意的是：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251628487.png" alt="提升" loading="lazy"></p><p>前端开发的能力。</p><p>不过这一点并没有明确的指标，是否超越 claude 很难评，也不是本文的重点。</p><p>本文要介绍的是：如何在 Cursor 中体验下这个最新的模型。</p><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><ol><li>Cursor Pro 账号，必须要开 Pro 才可以使用自定义的模型</li><li>openRouter 账号，这一步目前该模型是免费的，无需充值</li></ol><h2 id="开始"><a href="#开始" class="headerlink" title="开始"></a>开始</h2><p>找到 openRouter Model 下的 deepseek0324 模型，其实这里就可以和它聊天了。打到 API tab 下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251636310.png" loading="lazy"></p><p>创建 API key，记得保存 key。</p><p>回到 Cursor -&gt; Preferences -&gt; Cursor Settings -&gt; Model。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251639519.png" loading="lazy"></p><p>点击添加模型，模型名称直接复制 openRouter 中的名称。</p><p>然后还是在 Cursor Settings -&gt; Model 下，</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251640632.png" loading="lazy"></p><p>拷贝 openRouter 中的 api key，同时修改 下方的 BaseUrl 为：<code>https://openrouter.ai/api/v1</code>即可。</p><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>打开这个功能后，很多模型会消失，比如 claude 等模型，同时当选择这些自定义模型时，也不能使用 agent 的能力。同时 tab 补全的能力也用不了。</p><p>所以在 Cursor 中，更多的可以使用 edit 或者 ask 能力来和体验这个大模型。切换模型到 deepseekv30324 即可体验。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251644090.png" loading="lazy"></p><p>可以看到，它给自己的定位也是代码专家</p><h2 id="写个静态页面吧"><a href="#写个静态页面吧" class="headerlink" title="写个静态页面吧"></a>写个静态页面吧</h2><p>来都来了，写个简单的静态页面。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251646715.png" loading="lazy"></p><p>没有啥提示词技巧，就简单的自然语言描述。</p><p>结果如下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503251648466.png" loading="lazy"></p><p>效果还是可以的，设计谈不上，但是也过得去。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>不管自媒体的吹嘘是否过于夸张也好，deepseek 毕竟是开源的模型，相信它的进化速度会越来越快。</p><p>同时，我也希望 AI 能够多搞一搞后端，测试，运维，算法的工作，怎么净逮着前端猛干呢？</p><p>最后，别忘了关注我的公众号：程序员芋仔。持续干货输出。</p><p>AI 程序员时代，我也组建了一个前端抱团取暖群，欢迎加我微信来撩：mxb151，加群备注简单的自我介绍和加群哦。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/818c75c5/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/818c75c5/"/>
    <published>2025-03-25T17:04:00.000Z</published>
    <summary>
      <![CDATA[<p>今天一觉醒来，又被国运级产品刷屏了。</p>
<p>这次是 deepseekv3 0324 版本，模型是 3 月 24 晚上开源的，刷屏是 3.25 凌晨，不知道多少自媒体熬夜开更。</p>
<p>在现在的这个信息爆炸，自媒体的话，夸张的嫌疑会高一些。</p>]]>
    </summary>
    <title>在Cursor中用上最新的deepseek0324了！</title>
    <updated>2026-07-08T10:35:50.889Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>之前写了一篇关于<a href="https://mp.weixin.qq.com/s/Pxs7H06I7mUA3XKBtdNATA">即便付费，我也还是选择 Cursor 的文章</a>。</p><p>有人喜欢，有人觉得太浅，觉得我选择的理由太过粗暴。</p><p>刚好也用了一段时间的 Cursor，就再来对两款工具进行一个更深度的对比。</p><span id="more"></span><h2 id="变量控制"><a href="#变量控制" class="headerlink" title="变量控制"></a>变量控制</h2><ol><li>版本全部用本文发布时的最新版。Cursor 0.47.8, Trae 1.2.1。</li><li>针对同一个项目，我用的是之前写的一个 AI 对话的机器人</li><li>对比的能力维度 1: chat 模式对代码的理解</li><li>对比的能力维度 2: 代码补全的能力, 这一步拆分为路由猜测，utils 意图猜测，变量修改</li><li>agent 能力对比：篇幅限制，放在下一弹</li></ol><p>ps: 这个项目大概的功能如下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211111660.png" alt="demo" loading="lazy"></p><p>对项目有兴趣的可以看看之前的文章, <a href="https://mp.weixin.qq.com/s/q0BxgiTDjilKPUJFQkvSLg">当抠门程序员，遇到了免费 AI 大模型，我好像用 AI 赚到钱了？</a></p><h2 id="项目讲解能力对比"><a href="#项目讲解能力对比" class="headerlink" title="项目讲解能力对比"></a>项目讲解能力对比</h2><p>首先是 Trae, 通过在 引用中增加当前工作区来获取上下文。得到的篇幅比较长，截取部分如下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211116560.png" alt="Trae 分析" loading="lazy"></p><p>Cursor 中，通过 commond + enter 来完成上下文的注入，得到的回答截取部分如下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211118399.png" alt="Cursor 分析" loading="lazy"></p><p>粗糙来看，两者的分析都很 AI，确实都非常好的完成了分析理解的任务。</p><p>这一点上，可以说是平分秋色吧 (毕竟，他们都夸我了，结构清晰、功能完整哈哈，没说错)。</p><p>但是两者都存在一个体验上的优化，就是当我没有按照他们的要求提供上下文时，对于如何补充上下文提示的不够明显：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211124180.png" alt="提示" loading="lazy"></p><p>都给 90 分吧。</p><h2 id="代码补全"><a href="#代码补全" class="headerlink" title="代码补全"></a>代码补全</h2><p>程序员最关心的就是代码补全了，这次提供了三个案例，来进行能力的对比：分别是路由猜测，错误处理，和变量替换</p><h3 id="路由猜测"><a href="#路由猜测" class="headerlink" title="路由猜测"></a>路由猜测</h3><p>这个项目是一个单页路由，让他们猜测下一个路由写什么：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211128998.png" loading="lazy"></p><p>在这一点上，两者很一致，项目本身有首页，登录页，确实下一个可能的路由就是注册页。</p><p>在这一次 PK 上，两者也是平手(虽然我的意图不是注册页，登录即注册)。</p><h3 id="fetch-错误处理猜测"><a href="#fetch-错误处理猜测" class="headerlink" title="fetch 错误处理猜测"></a>fetch 错误处理猜测</h3><p>如下是我原本的代码，非常常见的 Fetch 的代码，当我把鼠标放在图示位置时，我心里想的是帮我多处理几个 code，而不仅仅是 401</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211130709.png" loading="lazy"></p><p>如下是两者的输出对比：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211129627.png" loading="lazy"></p><p>右侧是 Trae，可以看到，Trae 帮我处理了 403 之后，剩余情况直接 throw，懂 js 的都知道这里面的门道有多深：我下面的代码不会执行喽。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211149387.png" loading="lazy"></p><p>而反观 Cursor，403， 404， 500，502，503 都帮我列出来了，不过再往后，Cursor 也会陷入死循环，code 不断加 1，而提示信息始终都是重复。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211137975.png" loading="lazy"></p><p>ok，从这一把的 PK 来看，Trae 完败，Cursor 虽然有缺陷，但至少我的代码功能逻辑上不会错误。</p><h3 id="修改变量名"><a href="#修改变量名" class="headerlink" title="修改变量名"></a>修改变量名</h3><p>代码中，我们修改变量名之后的意图，往往就是修改本文件中的所有相关变量名。对于 react，如果我们修改了一个 state，我们往往还会希望能够修改对应的 setState 名称。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503211141526.png" loading="lazy"></p><p>可以看到，Cursor 完全达到了预期，不仅修改了变量名，还修改了 setState 对应的变量名，并提示我下一步该修改哪里，这一点，给 100 分我都嫌少。</p><p>反观 Trae，纹丝不动。在这一把上，我甚至觉得 copilot 可能都比它强。</p><p>起初我以为是 Trae 对 react 理解不强，换成普通变量，也依旧如此。</p><p>这一波，Cursor 完胜。</p><p>而 Trae，说这个功能点完全是没有的也不过分。</p><h2 id="PK-结论"><a href="#PK-结论" class="headerlink" title="PK 结论"></a>PK 结论</h2><p>chat 模式对代码的理解，大差不差，平分秋色。</p><p>代码补全上，变量名替换，Cursor 100 分都少了，Trae 这个功能几乎等于没有。</p><p>意图猜测上，简单的 Cursor 和 Trae 差不多，复杂点的 Cursor 完胜。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>tab 补全这个能力，是我付费 Cursor 的核心原因，它在工作中，实实在在的提高了工作效率。</p><p>Trae 在这一点上，并没有下功夫。猜测可能和 Trae 的定位有关，可能第一用户不是程序员，而是更多使用 builder(agent) 能力的人。</p><p>从产品维度上来说，Trae 的愿景可能更宏大: 程序员，你别写代码了，产品，设计师，你们快来用我的 builder 吧，大家别看代码，就做你的产品！</p><p>而 Cursor 更为专业，来吧，程序员，我真的很懂你的代码！</p><p>萝卜白菜，各有所爱吧。</p><p>最后，别忘了关注我的公众号：程序员芋仔。持续干货输出。</p><p>AI 程序员时代，我也组建了一个前端抱团取暖群，欢迎加我微信来撩：mxb151，加群备注简单的自我介绍和加群哦。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/ceb5ea57/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/ceb5ea57/"/>
    <published>2025-03-21T16:14:00.000Z</published>
    <summary>
      <![CDATA[<p>之前写了一篇关于<a href="https://mp.weixin.qq.com/s/Pxs7H06I7mUA3XKBtdNATA">即便付费，我也还是选择 Cursor 的文章</a>。</p>
<p>有人喜欢，有人觉得太浅，觉得我选择的理由太过粗暴。</p>
<p>刚好也用了一段时间的 Cursor，就再来对两款工具进行一个更深度的对比。</p>]]>
    </summary>
    <title>Cursor VS Trae, 深度对比第二弹</title>
    <updated>2026-07-08T10:35:50.886Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>单纯从我简单的使用上，让我想到了魔镜：</p><span id="more"></span><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503101740967.jpg" alt="魔镜魔镜" loading="lazy"></p><p>魔镜魔镜，快告诉我，baidu.com 的 seo 做的怎么样？</p><p>魔镜魔镜，快告诉我，近 100年来，美国最聪明的人是谁？</p><p>魔镜魔镜，快告诉我，近100年来，美国最漂亮的女人是谁？</p><p>魔镜魔镜….啪，1美元没了！</p><p>开个玩笑，我们言归正传，下面简单介绍下我这次的体验吧：</p><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><p>Manus 爆火之后，github 迅速涌出 openManus，据说只花了 3个小时。</p><p>出于好奇，就跑了下看看。</p><p>首先，官方的安装教程比较友好，借助 uv(官方的推荐方法二)，一个没什么 python 基础的人(比如我)，也能很快配置好环境并完成安装。</p><p>使用上需要配置自己的 api_key 和对应的模型请求 url(这一步可能需要一点玩 ai 的基础，或者找个中间商配置一下也行)。</p><p>也是按照官方文档来配置即可，下面是我的配置：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503101657209.png" alt="配置" loading="lazy"></p><p>核心就是请求的 url 和 api_key。</p><p>这两步搞定，就可以直接，启动：<code>python main.py</code></p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503101740642.gif" alt="启动" loading="lazy"></p><p>我本来以为会打开一个网页，然后输入你的指令，openmanus 自动帮你跑啊这些的，结果是在命令行。</p><p>好吧，3个小时的话，其实也没毛病。</p><h2 id="小试牛刀"><a href="#小试牛刀" class="headerlink" title="小试牛刀"></a>小试牛刀</h2><p>接下来就是文章开头的一幕了，刚好也没啥问的，就问一些简单的问题吧。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503101643949.png" alt="谁是近百年来最美的美国女人" loading="lazy"></p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503101653014.png" alt="后续的轮次" loading="lazy"></p><p>看起来命令行一大堆内容，实际上基本可以分为两步：</p><ol><li><p>run: 执行的开始，看起来是 x&#x2F;30，似乎是设置了最多30轮的样子</p></li><li><p>toolcall: 这一阶段分为思考，选择工具，执行工具，完成工具执行</p></li></ol><p>这两步交替执行，完成最终的任务后，返回结果。</p><p>在我上述的例子中，执行了多轮：</p><p>第一轮，选择了 google_search， 找到了 n 多篇文章。</p><p>第二轮，选择了 file_saver 这个工具，把这些链接保存在了我本地的一个文件中</p><p>第三轮，选择 browser_use 这个工具，打开了这些链接中的第一个，这一步是在我本机上打开的</p><p>第四轮，选择了 python_execute 来审查网页，最终观察到了 近百年来 USA 最漂亮的女人 是奥黛丽赫本</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503101742170.jpeg" alt="赫本来了" loading="lazy"></p><p>第五轮，选择了 file_saver，把结论保存在了本地的一个文件中</p><p>第六轮，选择了 terminate 这个工具，也就是终止，并告知最终执行结果：success。</p><p>结果就是去最终本地保存的文件查看。</p><p>说实话，从这里来看，其实还不如让普通的对话机器人联网搜索下来的快。</p><p>而且这段执行下来，大概花费在 0.2美金左右，对应我选择的模型，大概是 0.2M tokens 的样子，可比单纯聊天花费多太多了。</p><h2 id="浅浅看看原理"><a href="#浅浅看看原理" class="headerlink" title="浅浅看看原理"></a>浅浅看看原理</h2><p>从实际的体验来看，个人感觉并不是特别大的技术突破。</p><p>核心还是借助大模型的能力，来从工具集中选择工具并执行。</p><p>简单画个图，大概是样子：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503101725071.png" alt="流程" loading="lazy"></p><p>说是缝合，似乎也不为过。</p><p>不过这个系统更像是一个可以成长的机器人，一方面，可以通过不断的向其中增加工具集，来完善 agent 的能力。</p><p>而另一方面，从大模型入手，将人类的思考方式灌入，比如拆分任务(这一步已经在进行中了，在 run_flow.py 中)后，再进入工具选择阶段。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>整体来讲，openManus 的推出速度可谓是非常快了，拆分任务，选择工具，执行工具，得到结果，这种模式确实很强，具备比较大的成长空间。</p><p>虽然目前比较简陋，但是随着工具集的扩充，流程的拆分，相信未来大有可为。</p><p>同时，在 token 的消耗量上，也是一个需要优化的地方，目前的消耗量，还是承担不起的，性价比并不高。</p><p>最后，回答下一开始的问题：</p><ol><li>baidu 的 seo 做的怎么样，agent 回答：The website has a good SEO score.</li><li>近百年来最聪明的美国人：Albert Einstein</li></ol><p>这两问题，花费 0.8美金，其实还蛮气人的！</p><p>当然，也别忘记关注我的公众号：程序员芋仔。</p><p>AI程序员时代(饭碗随时会丢)，我也组建了一个前端抱团取暖群，欢迎加我微信来撩：mxb151，加群备注简单的自我介绍和加群哦。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/a1547e0f/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/a1547e0f/"/>
    <published>2025-03-10T18:04:00.000Z</published>
    <summary>
      <![CDATA[<p>单纯从我简单的使用上，让我想到了魔镜：</p>]]>
    </summary>
    <title>用上OpenManus了，这玩意有点像...</title>
    <updated>2026-07-08T10:35:50.891Z</updated>
  </entry>
  <entry>
    <author>
      <name>马晓博</name>
    </author>
    <category term="原创" scheme="https://blog.maxiaobo.com.cn/categories/%E5%8E%9F%E5%88%9B/"/>
    <content>
      <![CDATA[<p>如果说 IDE 是利剑，那么 AI 可能更多扮演的是剑灵的角色 – 芋仔</p><span id="more"></span><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503071102464.webp" alt="剑来" loading="lazy"></p><p>上一篇文章讲述了我即便付费也要选择 Cursor 的原因，这一篇就要实战啦。</p><p>毕竟 20刀，只是拿来写写公司代码的话，着实有点不划算。</p><p>隔壁产品经理，设计师，都开始 2 天上线 “人生app” 了。程序员也不能落下(毕竟付了费了)。</p><p>刚好之前一直跟老婆计算自家房子到底是赚了还是亏了，这个话题往往以机会成本不好计算，而无法得到一个精确的数字。</p><p>索性做个小程序，来计算房子的收益或亏损吧：起名房来财。</p><p>ps: 欢迎大家来体验我的小程序，房来财(原名房亏亏，房价在跌，但是祝大家发财)，还在备案中，只能通过小程序码来访问啦。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503071058291.jpg" alt="房来财" loading="lazy"></p><h2 id="聊点题外话"><a href="#聊点题外话" class="headerlink" title="聊点题外话"></a>聊点题外话</h2><p>ps: 本来不打算介绍机会成本的，但是发现身边不知道这个词的大有人在，就还是先简单说一下机会成本。</p><p>假设老王，用 200w 买了一套房子，过了5年，240w 把房子卖掉了，老王逢人就吹，我这房子赚了40w。</p><p>但是这40w真的是卖房的盈利吗？如果200w没有买房，按照 2% 的年化，放银行，简单算，5年利息最少也有20w了，这还是在没有计算复利的情况下。</p><p>所谓复利，就是每年，把利息和本金都取出来，重新放银行，比如第一年年底，利息是 4w，那么第二年就是 204w 产生利息，一共是 40800，如此往复，5年利息总额只会大于 20w。而现实中，复利往往是可以以按月计，甚至按天计。</p><p>那么这里面，利息以及利息带来的复利，就是这 200w 的机会成本。</p><p>当然，老王也有可能拿 200w 去炒股，可能亏个底朝天或者年化 10%，这都是机会成本。</p><p>ps: 如果老王拿 200w 去买个跑车，5年后 50w卖掉，那么老忘是不是可以对外说，这房子帮我赚了 190w 呢！</p><h2 id="10s第一版，AI-绝对能替代我的工作"><a href="#10s第一版，AI-绝对能替代我的工作" class="headerlink" title="10s第一版，AI 绝对能替代我的工作"></a>10s第一版，AI 绝对能替代我的工作</h2><p>作为程序员，本身就有很多的小程序开发经验，所以相比于产品经理&#x2F;设计师，少去了一个确定技术选型的阶段。</p><p>直接 uniapp 上，当然，也可以 command+enter 让 cursor 再来介绍下项目结构和优劣。</p><p>接下来就开始正式合作了，就像新员工一样，一开始的时光总是美好的。</p><p>很快(10s左右)，就按照我的想法，生成了小程序的首页和计算结果页。</p><p>可以进行简单的计算，收益 &#x3D; 卖出价格 - 买入价格 - 剩余未还贷款。</p><p>同时，样式上，Cursor 自动选择了 uni-ui，也还能看的过去，如果我来写的话，应该也就刚刚好建好文件，写几个 form。</p><p>不得不说，在初始化这一点上，AI 完全能替代我的工作。</p><h2 id="这也不对，那也解决不了，AI-你还太嫩"><a href="#这也不对，那也解决不了，AI-你还太嫩" class="headerlink" title="这也不对，那也解决不了，AI 你还太嫩"></a>这也不对，那也解决不了，AI 你还太嫩</h2><p>有了第一版之后，必然是要引入更复杂的场景，比如全款时，增加机会成本，也就是 200w 放银行的收益。</p><p>而贷款时，首付有机会成本，每个月的月供，也有机会成本。</p><p>AI 在用月供去计算机会成本时，认为只有本金，是需要计算机会成本的。</p><p>但是实际上，月供中不论是本金部分还是收益部分，都是支出，都是有机会成本的。</p><p>不过好在很快 AI 能够调整过来。</p><p>但是，修好了这个，在计算最终收益的时候又出现了计算错误，本来剩余未还贷款，应该是总贷款额 - 月供中的本金部分，这下好了，直接总贷款额 - 月供。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503050937963.png" alt="月供" loading="lazy"></p><p>应该是受到机会成本计算方式的干涉。</p><p>经过一番沟通后，也还算能调整过来。</p><p>但是又出现了新的问题，单位不对了，月供，每个月的单位都是元，而卖房价格，买入价格，都是百万元，单位换算上出现了非常多的问题。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503050935009.png" alt="单位不一致问题" loading="lazy"></p><p>到这里其实我已经不耐烦了，这点时间，计算的逻辑我应该都写好了。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503050935745.png" alt="bug 这么多" loading="lazy"></p><p>让人吐血的其实还在后面，当我们终于调整好了计算逻辑，我人眼看过去，计算应该没有问题的时候，直接去视图上查看计算结果，看着每一项的结果都是0，我几近崩溃。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503050939597.png" alt="我放弃" loading="lazy"></p><h2 id="PUA-不动-AI，只能-PUA-自己：就算是我的姿势有问题吧"><a href="#PUA-不动-AI，只能-PUA-自己：就算是我的姿势有问题吧" class="headerlink" title="PUA 不动 AI，只能 PUA 自己：就算是我的姿势有问题吧"></a>PUA 不动 AI，只能 PUA 自己：就算是我的姿势有问题吧</h2><p>到这一步，其实我是比较失望的。</p><p>如果事事都要我说清楚的话，尤其是逻辑部分的代码，如果我自己把逻辑梳理的清清楚楚，那么写代码，debug，也就是分分钟的事情。</p><p>还是硬着头皮自己去看看 AI 的代码哪里出了问题(毕竟付费了)。</p><p>经过 debug 后，对于计算结果为 0 的问题，应该还是上下文理解的问题。</p><p>我当时用的是 chat 模式(当时还没有 agent 模式)，虽然我在一开始 command+enter 把上下文已经给 AI 去理解。</p><p>但是后来单独修改计算逻辑的时候，虽然还是在一个 chat 中，但是做了比较多的调整都是仅针对计算逻辑的，导致<strong>计算模块的输入输出变量名和页面中的变量名对不上</strong>。</p><p>这是一番调整后，AI 失效的根本原因。</p><p>这里我只能 PUA 自己：</p><p><strong>AI 的前后一致性问题还有待加强！需要自己多关注上下文的注入。</strong></p><p>经此发现，改变不了 AI，那就改变我自己把。如此一来，逻辑部分和 UI 部分的结合，可算是完成。</p><p>整体功能上相比于 10s 的第一版，补充了我第一版中最缺失的关于贷款利息、机会成本的计算，可以说是能够满足我最初的预期了</p><h2 id="Cursor-的其他问题"><a href="#Cursor-的其他问题" class="headerlink" title="Cursor 的其他问题"></a>Cursor 的其他问题</h2><p>下面是我在 Cursor 使用中，发现 AI 这个”剑灵”(claude-3.5-sonnet)的一些其他问题：</p><h3 id="AI-也很固执-像极了那个刚入职的小伙子"><a href="#AI-也很固执-像极了那个刚入职的小伙子" class="headerlink" title="AI 也很固执(像极了那个刚入职的小伙子)"></a>AI 也很固执(像极了那个刚入职的小伙子)</h3><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503060937646.png" alt="AI的代码" loading="lazy"></p><p>上图是 AI 的代码，可以看到它通过 slot 来覆盖 form 的 label 标签，由于样式上的问题，label 看起来就很不搭。</p><p>我试图让 AI 自行调整样式，但是它多次调整，都还是存在 label 超出换行的问题，同时样式也比较丑。</p><p>经过我自己查阅文档，替换了 slot 的写法，转为借助 form 组件的 label 属性，如下：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503060939415.png" alt="我的代码" loading="lazy"></p><p>至此，form 组件上的一些 label 参数才生效。样式也比较顺眼</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503060941193.png" alt="我的代码" loading="lazy"></p><p>但是很快，我又让它修改一个功能时，AI 在它上一次修改的基础上而不是我改动之后的基础上对代码进行了调整。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503061957722.png" alt="固执" loading="lazy"></p><p>同样的问题不止发生一次：</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503061958462.png" alt="固执" loading="lazy"></p><p>说他固执，不过分吧？</p><h3 id="AI-私自篡改代码"><a href="#AI-私自篡改代码" class="headerlink" title="AI 私自篡改代码"></a>AI 私自篡改代码</h3><p>在 form 生成之后，由于 组件都有一个 clear 的 x 符号，我个人不喜欢，就手动修改去除了clear(现在想想自己真该打，让 AI 干就行了，你为啥手贱)。</p><p>只是令我没想到的是，在后续下达其他命令时，AI 又偷偷把我的代码给改回去了！</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503061959180.png" alt="改我代码" loading="lazy"></p><h3 id="AI-其实挺烦的"><a href="#AI-其实挺烦的" class="headerlink" title="AI 其实挺烦的"></a>AI 其实挺烦的</h3><p>不知道我开启了什么开关，AI 在每次调整好之后，都会问我，您觉得这样修改合适吗？</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503062006433.png" alt="合适吗？" loading="lazy"></p><p>对此，刚开始我觉得AI还挺有礼貌，但是后来想想，这话我真的很难回。</p><p>如果接受，我就点 apply 了，如果不接受，我就自己改了，那你这么问，是想让我说接受？</p><p>关键是我回不回，你都会固执的在下一次修改直接用你这一版的代码啊！！！</p><p>而且我是来写代码的，不是跟您聊天啊！</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>以上便是我和 Cursor 的初体验。</p><p>整体来讲，开发速度远超程序员。</p><p>但是在逻辑的理解和样式的debug上，稍微复杂一点的逻辑和bug，都会带来比较多无效的沟通。</p><p>像极了仙侠小说中刚刚生出灵智的剑灵，没有精确的指令就只能做些批量相对简单的工作。</p><p>复杂的逻辑交互，必须主人梳理清楚，才有可能完整无误的实现。</p><p>而对于相对难找的 bug，大概率是解决不了的(可能会给你很多方案，但是大概率无效)。</p><p>当然，在使用过程中也发现有 cursorrules 这个东西的存在，后期也会尝试加入自己的 rules，比如不要再问我接不接受啦，烦死了。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503071104024.jpeg" alt="烦死了" loading="lazy"></p><p>最后，欢迎大家来体验我的小程序，房来财(原名房亏亏，房价在跌，但是祝大家发财)，还在备案中，只能通过小程序码来访问啦。</p><p><img src="https://gitee.com/martin_bo/images/raw/master/image/202503071058291.jpg" alt="房来财" loading="lazy"></p><p>AI程序员时代(饭碗随时会丢)，我也组建了一个前端抱团取暖群，欢迎加我微信来撩：mxb151，加群备注简单的自我介绍和加群哦。</p>]]>
    </content>
    <id>https://blog.maxiaobo.com.cn/posts/28a6f7e6/</id>
    <link href="https://blog.maxiaobo.com.cn/posts/28a6f7e6/"/>
    <published>2025-03-07T12:04:00.000Z</published>
    <summary>
      <![CDATA[<p>如果说 IDE 是利剑，那么 AI 可能更多扮演的是剑灵的角色 – 芋仔</p>]]>
    </summary>
    <title>Cursor初体验，2小时上线房来财小程序</title>
    <updated>2026-07-08T10:35:50.886Z</updated>
  </entry>
</feed>
