<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>谢乾坤 | Kingname</title>
  
  <subtitle>给时光以生命。</subtitle>
  <link href="https://www.kingname.info/atom.xml" rel="self"/>
  
  <link href="https://www.kingname.info/"/>
  <updated>2026-07-20T13:57:06.256Z</updated>
  <id>https://www.kingname.info/</id>
  
  <author>
    <name>青南</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>所有人都在做 Agent，但很多人连 Agent 是什么都没想明白</title>
    <link href="https://www.kingname.info/2026/07/19/what-is-agent/"/>
    <id>https://www.kingname.info/2026/07/19/what-is-agent/</id>
    <published>2026-07-19T22:43:48.000Z</published>
    <updated>2026-07-20T13:57:06.256Z</updated>
    
    <content type="html"><![CDATA[<p>昨天，我去逛了一圈世界人工智能大会，也就是 WAIC。</p><p>今年的会场里，有一个词几乎无处不在：</p><p><strong>Agent。</strong></p><p>做大模型的在讲 Agent，做企业服务的在讲 Agent，做招聘、营销、医疗、金融、法律、教育的，也都在讲 Agent。</p><p>过去两年，大家还在说自己做的是“大模型应用”“AI 助手”“行业 Copilot”。到了今年，不在产品介绍里加上 Agent，好像都不好意思说自己是一家 AI 公司。</p><p>但我在现场和一些公司聊下来以后，产生了一个很强烈的感受：</p><p><strong>很多人不只是没有做好 Agent，甚至连 Agent 到底是什么东西，都没有真正想明白。</strong></p><span id="more"></span><p>他们会很兴奋地告诉我：</p><blockquote><p>我们做了一个某某 Agent，可以帮你完成某某工作。</p></blockquote><p>于是我接着问：</p><blockquote><p>那我怎么用？</p></blockquote><p>这个问题听起来很简单，但很多人的回答立刻开始变得含糊。</p><p>有的说：</p><blockquote><p>我们有一个网页，你登录进去就可以用了。</p></blockquote><p>有的说：</p><blockquote><p>我们给你提供 API，你们系统接一下就行。</p></blockquote><p>还有的说：</p><blockquote><p>我们可以私有化部署，也可以嵌入你们现在的软件。</p></blockquote><p>这些回答本身都没有错。</p><p>问题在于，他们经常把“Agent 是什么”和“Agent 怎么交付”混成了同一件事。</p><p>好像做了一个网页，网页里面放了一个聊天框，它就是 Agent。</p><p>或者在大模型外面包了一层 API，这个 API 就叫 Agent。</p><p>甚至有些人会产生一种很奇怪的想象：</p><blockquote><p>我做好了一个 Agent，就像做好了一个 Excel 文件一样，可以直接把这个 Agent 发给你，你拿过去就能用。</p></blockquote><p>但实际上，Agent 从来不是一种固定的软件形态。</p><p>它既不天然是一个 APP，也不天然是一个网站、API、函数或者软件模块。</p><p>这些都只是它可能呈现出来的外壳。</p><h2 id="Agent-不是一种产品形态，而是一种运行方式"><a href="#Agent-不是一种产品形态，而是一种运行方式" class="headerlink" title="Agent 不是一种产品形态，而是一种运行方式"></a>Agent 不是一种产品形态，而是一种运行方式</h2><p>先说结论。</p><p>我认为，Agent 最准确的定义应该是：</p><blockquote><p><strong>Agent 是一种能够接收目标、观察当前状态、自主决定下一步行动、调用外部工具，并根据行动结果继续调整，直到完成任务或触发终止条件的软件系统。</strong></p></blockquote><p>这里最关键的，不是用了大模型，也不是接了几个工具。</p><p>而是四件事：</p><p><strong>目标、决策、行动、反馈。</strong></p><p>假设我给一个投资尽调 Agent 下达任务：</p><blockquote><p>帮我调查一下这家公司是否值得投资。</p></blockquote><p>一个真正的 Agent，不应该只是把这句话连同几份材料一起塞给大模型，然后生成一篇看起来像模像样的报告。</p><p>它应该能够自己推进任务。</p><p>比如先拆解问题：</p><ul><li>这家公司是做什么的？</li><li>所处行业怎么样？</li><li>收入和利润是否真实？</li><li>核心客户是谁？</li><li>有没有法律风险？</li><li>创始团队过去做过什么？</li><li>市场规模和竞争格局如何？</li><li>目前还缺哪些证据？</li></ul><p>然后，它可能去搜索公开资料，查询企业数据库，阅读财报，分析访谈记录，调用估值模型。</p><p>如果中途发现收入数据和新闻报道矛盾，它应该继续调查。</p><p>如果发现某个关键文件缺失，它应该知道自己暂时不能下结论。</p><p>如果某个工具调用失败，它应该更换参数、重新尝试，或者请求人工介入。</p><p>整个过程更像这样：</p><blockquote><p>观察当前状态<br>判断下一步做什么<br>选择工具并执行<br>查看执行结果<br>更新当前状态<br>再决定下一步做什么</p></blockquote><p>这个循环，才是 Agent 的核心。</p><p>因此，Agent 并不是一个神秘的新文件格式，也不是某种独立于现有软件体系之外的新物种。</p><p>它本质上仍然是一套软件系统。</p><p>只是这套系统过去由程序员提前规定每一步怎么走，现在其中一部分路径选择，开始交给模型动态决定。</p><h2 id="网页、APP、API，都只是-Agent-的入口"><a href="#网页、APP、API，都只是-Agent-的入口" class="headerlink" title="网页、APP、API，都只是 Agent 的入口"></a>网页、APP、API，都只是 Agent 的入口</h2><p>既然 Agent 是一套系统的运行方式，那么它当然可以被包装成不同的产品形态。</p><p>最常见的是网页。</p><p>比如一家公司做了一个合同审查 Agent。</p><p>用户登录网站，上传合同，点击“开始审查”，过一会儿得到一份报告。</p><p>从用户的角度看，它就是一个网站。</p><p>但网站只是前端入口。</p><p>网页背后可能启动了一个任务队列，Agent 在服务器上读取合同、识别条款、查询法规、对比模板、发现风险，最后再生成结果。</p><p>它也可以被包装成一个 APP。</p><p>用户在手机里输入任务，后端启动同一套 Agent 系统。</p><p>它还可以被包装成 API。</p><p>B 公司向 A 公司发送一个请求：</p><blockquote><p>帮我分析这家公司过去三年的经营风险。</p></blockquote><p>A 公司返回一个任务编号。</p><p>过几分钟或者几小时后，B 公司再来查询任务状态，最终取得报告。</p><p>对 B 公司来说，它只是调用了一个 API。</p><p>但在这个 API 背后，可能发生了几十次模型调用、搜索、数据库查询和代码执行。</p><p>它也可以被包装成 SDK。</p><p>A 公司把代码库交给 B 公司，让 B 公司把 Agent 运行在自己的服务器里，连接自己的数据库，使用自己的模型和权限系统。</p><p>它还可以被做成一个 Docker 镜像，部署进企业内网。</p><p>甚至可以被包装成一个 MCP Tool，让另一个 Agent 来调用。</p><p>所以，当一家公司的产品负责人说：</p><blockquote><p>我们做了一个 Agent。</p></blockquote><p>这句话本身几乎没有提供任何有效信息。</p><p>因为我仍然不知道：</p><ul><li>这是一个网站，还是一个 API？</li><li>它运行在谁的服务器上？</li><li>用户把什么任务交给它？</li><li>它能采取哪些实际行动？</li><li>数据会不会离开企业？</li><li>是否支持异步任务？</li><li>失败以后如何重试？</li><li>是否有人工审批？</li><li>谁为最终结果负责？</li></ul><p>“我们做了一个 Agent”，就像一家餐厅告诉你：</p><blockquote><p>我们做的是现代化餐饮系统。</p></blockquote><p>听起来很高级，但你仍然不知道它到底卖什么菜，怎么点餐，多少钱，以及能不能吃。</p><h2 id="很多所谓-Agent，其实只是工作流"><a href="#很多所谓-Agent，其实只是工作流" class="headerlink" title="很多所谓 Agent，其实只是工作流"></a>很多所谓 Agent，其实只是工作流</h2><p>现在行业里还有另一个非常普遍的问题：</p><p><strong>把所有带大模型的自动化流程，都叫作 Agent。</strong></p><p>例如：</p><blockquote><p>读取文件<br>提取文字<br>调用大模型总结<br>把结果整理成表格<br>发送邮件</p></blockquote><p>如果这五个步骤是程序员提前写死的，那么它本质上是一个 AI 工作流。</p><p>工作流没有任何问题。</p><p>事实上，在大量真实的企业场景里，工作流往往比 Agent 更稳定、更便宜，也更容易审计。</p><p>但它不是因为接了大模型，就自动变成了 Agent。</p><p>工作流的核心是：</p><blockquote><p>程序员提前决定路径。</p></blockquote><p>Agent 的核心则是：</p><blockquote><p>程序员定义目标、工具和边界，模型在运行过程中动态决定路径。</p></blockquote><p>两者并不是非黑即白，中间存在大量混合形态。</p><p>比如前半段使用固定流程，到了异常处理环节，再由 Agent 判断应该查询哪份资料、调用哪个工具。</p><p>这种混合架构在生产环境里反而可能更加合理。</p><p>问题不在于一个产品是不是“纯 Agent”。</p><p>问题在于，很多公司一边使用完全固定的流程，一边为了融资、宣传或者赶时髦，强行把工作流包装成 Agent。</p><p>最后导致这个词迅速失去意义。</p><h2 id="“做了一个-Agent”不等于“别人可以直接拿来用”"><a href="#“做了一个-Agent”不等于“别人可以直接拿来用”" class="headerlink" title="“做了一个 Agent”不等于“别人可以直接拿来用”"></a>“做了一个 Agent”不等于“别人可以直接拿来用”</h2><p>我在 WAIC 现场遇到的另一个典型误区，是一些人默认：</p><blockquote><p>只要我们把 Agent 做出来，其他公司就可以直接使用。</p></blockquote><p>但企业软件从来没有这么简单。</p><p>假设 A 公司做了一个销售 Agent，B 公司想使用。</p><p>这时真正的问题不是：</p><blockquote><p>你有没有 Agent？</p></blockquote><p>而是：</p><blockquote><p>这个 Agent 如何进入 B 公司的业务系统？</p></blockquote><p>如果它要读取客户信息，就要连接 B 公司的 CRM。</p><p>如果它要发送邮件，就要获得邮箱权限。</p><p>如果它要判断客户是否值得跟进，就需要理解 B 公司的销售规则。</p><p>如果它要修改商机阶段，就要接入内部权限体系。</p><p>如果它要联系客户，还需要确定哪些动作可以自动执行，哪些必须经过人工批准。</p><p>这时，A 公司真正需要交付的，可能是：</p><ul><li>一个 SaaS 网站</li><li>一组 API</li><li>一个 SDK</li><li>一套私有化部署系统</li><li>一个 MCP Server</li><li>一套数据连接器</li><li>一套权限和审批机制</li><li>一套日志、监控与审计系统</li></ul><p>Agent 只是其中负责“理解任务并动态决策”的一部分。</p><p>它不是整个产品，更不是整个交付过程。</p><p>一家企业真正购买的，也通常不是一个抽象的 Agent。</p><p>企业购买的是某种具体能力：</p><blockquote><p>帮我审查合同。<br>帮我筛选客户。<br>帮我处理工单。<br>帮我调查公司。<br>帮我分析财务风险。<br>帮我生成销售线索。</p></blockquote><p>至于这个能力背后用了一个 Agent、十个 Agent，还是一个普通工作流，客户其实并不关心。</p><p>客户关心的是：</p><ul><li>输入什么？</li><li>输出什么？</li><li>正确率多少？</li><li>执行需要多久？</li><li>能不能接入现有系统？</li><li>数据是否安全？</li><li>出错了谁负责？</li><li>到底能省多少钱？</li></ul><p>如果这些问题回答不清楚，那么“Agent”三个字说得再多，也只是技术包装。</p><h2 id="判断一个-Agent，别先问它用了什么模型"><a href="#判断一个-Agent，别先问它用了什么模型" class="headerlink" title="判断一个 Agent，别先问它用了什么模型"></a>判断一个 Agent，别先问它用了什么模型</h2><p>现在很多人在介绍 Agent 时，最喜欢先说：</p><blockquote><p>我们底层使用了某某大模型。<br>我们用了多 Agent 架构。<br>我们支持 RAG、MCP 和长期记忆。<br>我们构建了行业知识库。</p></blockquote><p>这些当然可以介绍。</p><p>但它们都不是最重要的问题。</p><p>真正判断一个 Agent 是否成立，我更关心下面几件事。</p><p>第一，它能接受什么完整任务？</p><p>不是“它能回答什么问题”，而是用户可以把什么工作完整地交给它。</p><p>第二，它能采取什么行动？</p><p>只能生成一段文字，和能够查询数据库、修改 CRM、运行代码、发送邮件，是完全不同的产品。</p><p>第三，执行路径是谁决定的？</p><p>如果所有步骤都已经被开发者写死，那它更接近工作流。</p><p>如果模型会根据中间结果决定继续搜索、切换工具、调整策略，它才更接近 Agent。</p><p>第四，它有没有状态？</p><p>它是否知道任务已经做到哪里，哪些信息已经验证，哪些问题还没有解决，哪些工具曾经失败。</p><p>第五，失败以后怎么办？</p><p>一个演示版系统往往遇到错误就结束。</p><p>一个生产级 Agent 必须知道什么时候重试、什么时候换工具、什么时候暂停、什么时候交给人。</p><p>第六，权限怎么控制？</p><p>如果 Agent 可以发邮件，它可以发给谁？</p><p>如果它可以修改数据库，它可以修改哪些字段？</p><p>如果它可以付款，付款上限是多少？</p><p>如果没有权限、审批、审计和回滚，那么所谓的“自动执行”，很可能只是把风险藏了起来。</p><p>第七，别人到底怎么接入？</p><p>网页、API、SDK、私有部署、MCP，还是别的形式？</p><p>如果一家公司只能反复告诉你“我们做了一个 Agent”，却始终说不清楚别人如何把它接入现有业务，那么这个 Agent 很可能还只是一个 Demo。</p><h2 id="Agent-不是产品答案，它只是实现手段"><a href="#Agent-不是产品答案，它只是实现手段" class="headerlink" title="Agent 不是产品答案，它只是实现手段"></a>Agent 不是产品答案，它只是实现手段</h2><p>我并不是说 Agent 没有价值。</p><p>恰恰相反，我认为 Agent 很可能会成为未来软件系统里非常重要的一层。</p><p>过去的软件要求用户自己理解功能。</p><p>用户要知道点击哪个按钮，填写哪张表格，选择哪个菜单，才能让软件完成某件事。</p><p>Agent 带来的变化，是用户可以直接描述目标：</p><blockquote><p>帮我把最近一个月流失的高价值客户找出来，分析流失原因，再分别制定召回方案。</p></blockquote><p>系统再自己决定需要查哪些数据、运行哪些分析、生成什么内容。</p><p>从这个角度看，Agent 确实有可能改变软件的交互方式。</p><p>但 Agent 不是产品答案。</p><p>它只是实现产品能力的一种手段。</p><p>就像数据库不是产品答案，云计算不是产品答案，微服务也不是产品答案。</p><p>一家创业公司不会因为用了 PostgreSQL，就自动变成一家优秀公司。</p><p>也不会因为把单体系统拆成微服务，就突然获得商业价值。</p><p>同样，做了 Agent，也不意味着自动拥有产品、客户和商业模式。</p><p>今年大家一窝蜂创业做 Agent，让我想起过去很多技术浪潮。</p><p>每当一个新的技术概念出现，行业最先做的往往不是认真理解它，而是先把自己原来的东西重新命名一遍。</p><p>聊天机器人改名叫 Agent。</p><p>自动化脚本改名叫 Agent。</p><p>工作流改名叫 Agent。</p><p>搜索加总结也改名叫 Agent。</p><p>最后，所有东西都是 Agent，也就等于没有东西是 Agent。</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>从 WAIC 回来以后，我越来越觉得，今天行业里最缺的并不是更多 Agent。</p><p>而是先把几个最基本的问题想明白：</p><blockquote><p>你到底在帮谁完成什么任务？<br>为什么这个任务需要 Agent，而不是普通工作流？<br>Agent 能够采取什么行动？<br>它如何接入客户现有系统？<br>客户最终购买的究竟是什么能力？</p></blockquote><p>Agent 不是一个 APP，不是一个网站，不是一个 API，也不是一个函数。</p><p>它是一套软件系统内部，围绕目标自主判断、调用工具、根据反馈持续执行的机制。</p><p>APP、网站、API、SDK、MCP 和私有化部署，只是别人接触这套能力的不同方式。</p><p>所以，下一次再有公司告诉你：</p><blockquote><p>我们做了一个 Agent。</p></blockquote><p>先别急着问它用了哪个模型。</p><p>你只需要问一句：</p><blockquote><p><strong>那我到底怎么用？</strong></p></blockquote><p>这个问题，通常比任何技术架构图都更能检验，它做出来的到底是一个真正的产品，还是又一个披着 Agent 外衣的演示项目。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;昨天，我去逛了一圈世界人工智能大会，也就是 WAIC。&lt;/p&gt;
&lt;p&gt;今年的会场里，有一个词几乎无处不在：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;做大模型的在讲 Agent，做企业服务的在讲 Agent，做招聘、营销、医疗、金融、法律、教育的，也都在讲 Agent。&lt;/p&gt;
&lt;p&gt;过去两年，大家还在说自己做的是“大模型应用”“AI 助手”“行业 Copilot”。到了今年，不在产品介绍里加上 Agent，好像都不好意思说自己是一家 AI 公司。&lt;/p&gt;
&lt;p&gt;但我在现场和一些公司聊下来以后，产生了一个很强烈的感受：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;很多人不只是没有做好 Agent，甚至连 Agent 到底是什么东西，都没有真正想明白。&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="认知升级" scheme="https://www.kingname.info/categories/%E8%AE%A4%E7%9F%A5%E5%8D%87%E7%BA%A7/"/>
    
    
    <category term="AI" scheme="https://www.kingname.info/tags/AI/"/>
    
    <category term="大模型" scheme="https://www.kingname.info/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/"/>
    
    <category term="Agent" scheme="https://www.kingname.info/tags/Agent/"/>
    
    <category term="WAIC" scheme="https://www.kingname.info/tags/WAIC/"/>
    
  </entry>
  
  <entry>
    <title>把简单问题写复杂，是一种学术病</title>
    <link href="https://www.kingname.info/2026/07/14/academic-complexity/"/>
    <id>https://www.kingname.info/2026/07/14/academic-complexity/</id>
    <published>2026-07-14T19:38:00.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p><img src="/images/academic-complexity/img_01.jpg"></p><p>摄影：产品经理</p><p>左庭右院蔬菜自助</p><p>最近我在看一篇关于电子表格理解的论文。</p><p>论文里有一个算法，核心流程其实非常简单：先让一个 Agent 从电子表格中抽取结构，再让视觉 Agent 和 LaTeX Agent 分别验证；如果两边都通过，就返回结果；如果没有通过，就把错误反馈回去，重新抽取，直到成功或者达到最大重试次数。</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><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">for _ in range(max_iterations):</span><br><span class="line">    result = extractor.run(spreadsheet, feedback)</span><br><span class="line"></span><br><span class="line">    vision_check = vision_verifier.verify(result)</span><br><span class="line">    latex_check = latex_verifier.verify(result)</span><br><span class="line"></span><br><span class="line">    if vision_check.passed and latex_check.passed:</span><br><span class="line">        return result</span><br><span class="line"></span><br><span class="line">    feedback = vision_check.feedback + latex_check.feedback</span><br></pre></td></tr></table></figure><p>事情就是这么简单。</p><p>但论文当然不能这么写。</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">C ← &#123;S, Prompt, Tool interfaces&#125;</span><br><span class="line">Y ← C[-1]</span><br><span class="line">vision_pass, Δv ← parse_verification(Cv[-1])</span><br><span class="line">latex_pass, Δl ← parse_verification(Cl[-1])</span><br></pre></td></tr></table></figure><p>再配上各种花体字母、上下标、希腊字母、集合符号和箭头，最后排成一张看起来非常“学术”的算法图。</p><p><img src="/images/academic-complexity/img_02.png"></p><p>我盯着这张图看了半天，脑子里只有一个问题：</p><p><strong>这到底是在帮助读者理解算法，还是在阻止读者理解算法？</strong></p><span id="more"></span><h2 id="简单问题一旦进入论文，就必须先被加密"><a href="#简单问题一旦进入论文，就必须先被加密" class="headerlink" title="简单问题一旦进入论文，就必须先被加密"></a>简单问题一旦进入论文，就必须先被加密</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">Δv</span><br></pre></td></tr></table></figure><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">vision_feedback</span><br></pre></td></tr></table></figure><p>作者却偏要写一个希腊字母 Δ，再加一个下标 v。</p><p>读者看到 <code>vision_feedback</code>，根本不需要思考，直接就知道这是什么。</p><p>但看到 <code>Δv</code>，你得先在脑子里完成一轮解码：</p><p>Δ 是差异，还是梯度，还是增量，还是修正？</p><p>v 是 vision，还是 verification，还是 value？</p><p>这个变量是一个字符串、一组错误、一个修改集合，还是某种数值？</p><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">C ← &#123;S, Prompt, Tool interfaces&#125;</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></pre></td><td class="code"><pre><span class="line">context = [</span><br><span class="line">    spreadsheet,</span><br><span class="line">    prompt,</span><br><span class="line">    tool_interfaces,</span><br><span class="line">]</span><br></pre></td></tr></table></figure><p>可问题是，数学里的 <code>&#123;&#125;</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">C[-1]</span><br></pre></td></tr></table></figure><p>也就是取最后一个元素。</p><p>如果 <code>C</code> 真的是集合，那就不存在所谓的最后一个元素；如果 <code>C</code> 实际上是一个消息列表，就不应该用集合符号。</p><p>也就是说，这种写法甚至没有因为数学化而变得更加严谨。</p><p>它只是获得了一个更加严谨的外观。</p><p>这正是我最反感的地方。</p><p>有些论文里的数学符号，不是在表达严格性，而是在表演严格性。</p><h2 id="我不是讨厌数学，我是讨厌不必要的数学"><a href="#我不是讨厌数学，我是讨厌不必要的数学" class="headerlink" title="我不是讨厌数学，我是讨厌不必要的数学"></a>我不是讨厌数学，我是讨厌不必要的数学</h2><p>每次批评这种现象，总会有人跳出来说：</p><p>“你觉得公式难看，是因为你数学基础不好。”</p><p>这句话当然很方便，因为它可以直接把表达者的问题，变成读者的能力问题。</p><p>但我并不反对数学公式。</p><p>很多问题离开数学符号，确实很难表达。</p><p>比如概率分布、损失函数、梯度、矩阵变换、优化目标、复杂度分析，这些东西如果全部改写成自然语言或者超长变量名，只会更加混乱。</p><p>数学符号最大的价值，就是压缩。</p><p>一行公式可以描述一整套稳定的数量关系，而且可以方便地进行推导、代换和证明。在这些场景里，符号不但必要，而且优美。</p><p>问题在于，并不是所有东西都值得被压缩成数学符号。</p><p>一个算法中“先调用 A，再调用 B，如果都成功就返回，否则重试”，本质上是控制流。</p><p>控制流最适合用代码表达。</p><p>一个系统中“数据从哪里来，经过哪些模块，最后流向哪里”，本质上是架构关系。</p><p>架构关系最适合用图表达。</p><p>一个方法为什么这样设计、解决了什么问题，本质上是概念解释。</p><p>概念解释最适合用自然语言表达。</p><p>但现在很多技术论文有一种奇怪的冲动：无论内容本来属于什么表达形式，都要尽可能往公式里塞。</p><p>能用一个正常变量名写清楚的，改成希腊字母。</p><p>能用一行 Python 写清楚的，改成 LaTeX 伪代码。</p><p>能用一段自然语言说清楚的，定义三个集合、两个映射和四个下标。</p><p>最后原本很直观的问题，被写成了一份密码本。</p><h2 id="看西瓜书的时候，我也有同样的感觉"><a href="#看西瓜书的时候，我也有同样的感觉" class="headerlink" title="看西瓜书的时候，我也有同样的感觉"></a>看西瓜书的时候，我也有同样的感觉</h2><p>这种不适感，让我想起以前看周志华的《机器学习》。</p><p>这本书非常有名，因为封面上有很多西瓜，大家通常叫它“西瓜书”。</p><p>我相信它在机器学习教育史上有它的地位，也相信很多数学基础很好的人，确实能从中获得系统性的知识。</p><p>但我第一次翻开它的时候，最直接的感受就是：</p><p><strong>满眼都是公式，一行代码都没有。</strong></p><p>一个算法究竟接收什么输入，中间数据怎样变化，循环在哪里，分支在哪里，最后返回什么，书里经常不是先通过流程把它讲清楚，而是直接定义一堆符号，然后开始推导。</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><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></pre></td><td class="code"><pre><span class="line">def choose_best_feature(dataset, features):</span><br><span class="line">    best_feature = None</span><br><span class="line">    best_gain = float(&quot;-inf&quot;)</span><br><span class="line"></span><br><span class="line">    for feature in features:</span><br><span class="line">        groups = split_dataset(dataset, feature)</span><br><span class="line">        gain = calculate_information_gain(dataset, groups)</span><br><span class="line"></span><br><span class="line">        if gain &gt; best_gain:</span><br><span class="line">            best_gain = gain</span><br><span class="line">            best_feature = feature</span><br><span class="line"></span><br><span class="line">    return best_feature</span><br></pre></td></tr></table></figure><p>看完这段代码，即使读者还不知道信息增益是怎么算的，也已经明白算法在做什么：</p><p>它遍历每一个特征，用这个特征切分数据，计算切分后不确定性下降了多少，然后选择效果最好的那个。</p><p>接下来再解释熵，再解释信息增益，再解释公式，读者会知道每一个符号对应程序中的哪一步。</p><p>但很多数学教材的顺序正好相反。</p><p>它们先告诉你：</p><p><img src="/images/academic-complexity/formula_01.png"></p><p>接着再告诉你：</p><p><img src="/images/academic-complexity/formula_02.png"></p><p>然后默认你看到这些符号以后，算法流程就已经自动出现在脑子里了。</p><p>对于已经掌握这套知识的人，这样写当然简洁。</p><p>但对于正在学习的人，这种表达经常形成一个死循环：</p><p>因为不知道算法在干什么，所以看不懂公式为什么这样定义；因为看不懂公式，所以更不知道算法在干什么。</p><p>最后读者没有在理解机器学习，而是在逐个解析字符。</p><p>大写字母是什么意思，小写字母是什么意思，粗体字母是什么意思，右上角是什么意思，右下角是什么意思，花体字母又是什么意思。</p><p>等你好不容易把符号表翻译成人话，早就忘了这一节原本想讲什么。</p><h2 id="很多教材把“知识压缩”放在了“知识建立”之前"><a href="#很多教材把“知识压缩”放在了“知识建立”之前" class="headerlink" title="很多教材把“知识压缩”放在了“知识建立”之前"></a>很多教材把“知识压缩”放在了“知识建立”之前</h2><p>数学公式本质上是一种高度压缩的信息。</p><p>但压缩有一个前提：你得先拥有可以被压缩的东西。</p><p>一个熟悉线性回归的人，看到一个损失函数，能够立刻展开出背后的数据、模型、预测误差和优化过程。</p><p>因为这些概念已经存在于他的脑子里，公式只是一个索引。</p><p>但初学者脑子里还没有这些东西。</p><p>你给他一个公式，并不是帮他压缩知识，而是给了他一个压缩包，却没有提供解压软件。</p><p>这也是很多所谓“经典教材”的问题。</p><p>它们往往是由已经高度熟悉一个领域的人写给另一个熟悉这个领域的人看的，但在出版时却被包装成了初学者教材。</p><p>作者自己已经看不见符号门槛了。</p><p>他看到的是模型结构，读者看到的是英文字母。</p><p>他看到的是概率关系，读者看到的是上下标。</p><p>他觉得一个公式非常直观，读者却要花十分钟确认某个 <code>i</code> 到底是样本编号、迭代次数，还是矩阵下标。</p><p>于是学习过程变成了符号考古。</p><h2 id="更糟糕的是，公式经常成了一种学术装饰"><a href="#更糟糕的是，公式经常成了一种学术装饰" class="headerlink" title="更糟糕的是，公式经常成了一种学术装饰"></a>更糟糕的是，公式经常成了一种学术装饰</h2><p>理论工作需要公式，这是毫无疑问的。</p><p>但现在很多论文中的公式，已经不只是表达工具，也逐渐变成了一种身份标识。</p><p>一个方法如果只用自然语言和代码说明，似乎显得不够高级；给模块起正常名字，似乎显得不够抽象；于是必须定义几个集合，增加几个映射，引入几个希腊字母，再给它们配上上下标。</p><p>哪怕公式所描述的内容只是：</p><p>“从候选答案中选择得分最高的一个。”</p><p>也要写成：</p><p><img src="/images/academic-complexity/formula_03.png"></p><p>这类公式本身没有错，而且在某些上下文中很合适。</p><p>但当整篇论文不断把普通流程翻译成数学符号时，它传达的不再只是信息，还有一种隐含态度：</p><p><strong>我的工作足够复杂，所以必须用你看不懂的方式来描述。</strong></p><p>符号在这里形成了一道门槛。</p><p>越是简单的东西，越需要复杂地写，才能让它看起来像研究成果。</p><p>一个普通的循环，如果直接写成 Python，读者一眼就看懂了。一旦读者一眼看懂，就容易发现这个方法可能根本没有那么复杂。</p><p>可如果把它写成花体集合、希腊字母、上标、下标和箭头，读者首先感受到的是权威感。</p><p>至于内容究竟有没有那么深，反而被掩盖了。</p><p>这有点像过去某些人写文章，明明一句“分析用户需求”就能讲清楚，非要写成“基于多维异构信息场的用户意图感知与需求表征”。</p><p>不是内容变高级了，只是表达变肿了。</p><h2 id="编程语言本身就是一种严格的形式语言"><a href="#编程语言本身就是一种严格的形式语言" class="headerlink" title="编程语言本身就是一种严格的形式语言"></a>编程语言本身就是一种严格的形式语言</h2><p>很多人潜意识里觉得，代码不够学术，公式才足够严谨。</p><p>这其实是一种非常陈旧的偏见。</p><p>现代编程语言本身就是严格的形式系统。</p><p>一个变量是什么类型，一个函数接收什么参数，一个循环什么时候停止，一个异常如何处理，一个对象怎样改变状态，代码都可以明确表达。</p><p>甚至在描述算法执行过程时，代码往往比论文伪代码更加严谨。</p><p>还是以前面那篇论文为例。</p><p>原文写：</p><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">until verification succeeds or max-iterations reached</span><br><span class="line">return Y*</span><br></pre></td></tr></table></figure><p>但它没有明确说明，如果最大迭代次数到了，验证仍然没有成功，应该怎么办。</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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">for _ in range(max_iterations):</span><br><span class="line">    ...</span><br><span class="line">    if verified:</span><br><span class="line">        return result</span><br><span class="line"></span><br><span class="line">raise VerificationError(&quot;Verification failed&quot;)</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></pre></td><td class="code"><pre><span class="line">return &#123;</span><br><span class="line">    &quot;success&quot;: False,</span><br><span class="line">    &quot;result&quot;: last_result,</span><br><span class="line">    &quot;errors&quot;: feedback,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码无法靠排版营造一种“差不多说清楚了”的感觉。</p><p>它最终必须执行。</p><p>正因为必须执行，很多含糊的地方才会被迫暴露出来。</p><p>谁负责判断 Agent 是否继续调用工具？</p><p>工具失败以后是否重试？</p><p>两个验证器是否并行执行？</p><p>反馈冲突时如何处理？</p><p>最大重试次数是多少？</p><p>最后一次失败结果是否保留？</p><p>这些才是真正决定算法行为的问题。</p><p>而不是把 <code>vision_feedback</code> 写成 <code>Δv</code>。</p><h2 id="好的技术表达，应该降低理解成本"><a href="#好的技术表达，应该降低理解成本" class="headerlink" title="好的技术表达，应该降低理解成本"></a>好的技术表达，应该降低理解成本</h2><p>我一直认为，技术表达最重要的评价标准，不是看起来多么专业，而是它是否让一个本来复杂的问题变得更容易理解。</p><p>好的公式，可以把一页文字压缩成一行关系。</p><p>好的代码，可以把抽象算法变成可执行流程。</p><p>好的图，可以让模块和数据流一目了然。</p><p>好的自然语言，可以解释为什么要这样做，以及这样做解决了什么问题。</p><p>它们之间并不存在高低贵贱。</p><p>真正成熟的表达，应该根据内容选择工具，而不是无论什么内容，都强迫它穿上一件数学外套。</p><p>最理想的技术教材，通常应该按照这样的顺序展开：</p><p>先用自然语言建立直觉，让读者知道问题是什么；再用图或者代码展示流程，让读者知道系统怎样运行；最后用数学公式精确定义关键关系，让读者知道为什么成立。</p><p>自然语言回答“是什么”。</p><p>代码回答“怎么做”。</p><p>公式回答“为什么”。</p><p>但很多论文和教材把这三件事全部扔给了公式。</p><p>然后把读不懂的责任留给读者。</p><h2 id="数学不应该成为知识的防盗门"><a href="#数学不应该成为知识的防盗门" class="headerlink" title="数学不应该成为知识的防盗门"></a>数学不应该成为知识的防盗门</h2><p>我并不期待所有论文都改成 Python 教程，也不认为机器学习可以完全绕开线性代数、概率论和微积分。</p><p>真正想深入理解算法，数学永远绕不过去。</p><p>但数学应该是一座桥，而不是一堵墙。</p><p>公式应该出现在它真正能够减少歧义、压缩关系、支持推导的地方，而不是用来装饰每一个普通流程。</p><p>希腊字母也不是越多越高级。</p><p>下标也不是越复杂越严谨。</p><p>花体集合更不会自动让一个普通想法变成理论贡献。</p><p>当一个公式帮助读者更快地理解问题时，它是工具。</p><p>当一个公式只是把正常变量名替换成希腊字母，把简单流程重新编码一遍时，它就是障碍。</p><p>当一篇文章必须依靠大量符号，才能维持自身的深奥感时，我们甚至应该反过来怀疑：</p><p>它究竟是在压缩复杂思想，还是在掩盖思想并不复杂？</p><p>我反感的从来不是数学。</p><p>我反感的是有人把数学当成知识的防盗门。</p><p>仿佛只有穿过一大片希腊字母，才有资格接触后面的内容；仿佛表达得越难懂，工作就越有价值；仿佛读者花在解码符号上的时间，也能算作作者思想的深度。</p><p>真正高级的表达，不是把简单的事情写复杂。</p><p>而是把复杂的事情讲简单。</p><p><strong>公式应该是思想的压缩器，而不是学术的遮羞布。</strong></p><p>END</p><p><img src="/images/academic-complexity/img_03.jpg"></p><p>未闻 Code·知识星球开放啦！</p><p>一对一答疑爬虫相关问题</p><p>职业生涯咨询</p><p>面试经验分享</p><p>每周直播分享</p><p>……</p><p>未闻 Code·知识星球期待与你相见~</p><p><img src="/images/academic-complexity/img_04.gif"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;/images/academic-complexity/img_01.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;摄影：产品经理&lt;/p&gt;
&lt;p&gt;左庭右院蔬菜自助&lt;/p&gt;
&lt;p&gt;最近我在看一篇关于电子表格理解的论文。&lt;/p&gt;
&lt;p&gt;论文里有一个算法，核心流程其实非常简单：先让一个 Agent 从电子表格中抽取结构，再让视觉 Agent 和 LaTeX Agent 分别验证；如果两边都通过，就返回结果；如果没有通过，就把错误反馈回去，重新抽取，直到成功或者达到最大重试次数。&lt;/p&gt;
&lt;p&gt;任何一个写过程序的人，看到这里，大概已经知道代码该怎么写了：&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;for _ in range(max_iterations):&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    result = extractor.run(spreadsheet, feedback)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    vision_check = vision_verifier.verify(result)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    latex_check = latex_verifier.verify(result)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    if vision_check.passed and latex_check.passed:&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        return result&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    feedback = vision_check.feedback + latex_check.feedback&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;事情就是这么简单。&lt;/p&gt;
&lt;p&gt;但论文当然不能这么写。&lt;/p&gt;
&lt;p&gt;它非要把这段代码重新包装成一种半数学、半代码、半自然语言的东西：&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;C ← &amp;#123;S, Prompt, Tool interfaces&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;Y ← C[-1]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;vision_pass, Δv ← parse_verification(Cv[-1])&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;latex_pass, Δl ← parse_verification(Cl[-1])&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;再配上各种花体字母、上下标、希腊字母、集合符号和箭头，最后排成一张看起来非常“学术”的算法图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/academic-complexity/img_02.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我盯着这张图看了半天，脑子里只有一个问题：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这到底是在帮助读者理解算法，还是在阻止读者理解算法？&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="认知升级" scheme="https://www.kingname.info/categories/%E8%AE%A4%E7%9F%A5%E5%8D%87%E7%BA%A7/"/>
    
    
    <category term="AI" scheme="https://www.kingname.info/tags/AI/"/>
    
    <category term="学术写作" scheme="https://www.kingname.info/tags/%E5%AD%A6%E6%9C%AF%E5%86%99%E4%BD%9C/"/>
    
    <category term="技术表达" scheme="https://www.kingname.info/tags/%E6%8A%80%E6%9C%AF%E8%A1%A8%E8%BE%BE/"/>
    
    <category term="机器学习" scheme="https://www.kingname.info/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：Vibe Coding 时代，如何用系统设计思路给大模型爬虫省钱提速</title>
    <link href="https://www.kingname.info/2026/04/20/2026-04-20-vibe-coding-system-design/"/>
    <id>https://www.kingname.info/2026/04/20/2026-04-20-vibe-coding-system-design/</id>
    <published>2026-04-20T12:00:00.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p>在大模型出来之前，计算机领域一直流行着这样一句名言：<strong>计算机领域的任何问题，都可以通过拆分问题+给系统增加若干个层来解决。</strong> 实际上，这句话在现在的 AI 时代，依然是绝对的真理。</p><p>例如以前面试经常问的一个老掉牙的系统设计问题：如何设计一个短网址系统？标准答案是，使用内存+Redis+数据库做多级存储架构。读取最频繁的短网址放到内存，其次的放到 Redis，不频繁的放到数据库。通过增加分层，完美解决高并发和存储成本的矛盾。</p><p>现在有了大模型，大家都在玩 Vibe Coding（用自然语言指挥 AI 写代码），很多人觉得以前的工程经验没用了，反正大模型什么都能干。但我认为恰恰相反，在用 AI 写代码时，大家更应该把“拆分与分层”这句话牢牢记在心里。</p><span id="more"></span><p>举个现实的例子，你想做一个招聘聚合网站，需要从各个公司的官网抓取他们的招聘信息，获得工作详情。</p><p>如果你没有任何工程经验，是个纯小白，你可能直接就甩给 AI 这样一段提示词：</p><blockquote><p>帮我设计一个爬虫系统，我输入某公司的官网，你需要进入官网，找到里面的招聘页面，然后进入每一个工作，抓取工作的名字，工作地点，薪资和工作要求，并储存到数据库。</p></blockquote><p>我相信现在很多人就是这样写的。并且不得不承认，现在的模型确实太聪明了——如果你用例如 Claude Opus 4.6 或者 Opus 4.7，再加上 Claude Code 这种工具，写出来的基于 Browser Use 的智能爬虫确实能运行，甚至效果可能还不错。它会自己打开浏览器，截图，识别哪里是下一页，哪里是详情。</p><p>有同学可能会说，既然能跑通，而且不用我写一行代码，那这不就足够了吗？</p><p>但如果你真的把它扔到服务器上跑，你会发现这样设计的系统有两个致命缺陷：</p><ol><li><strong>成本极高，简直是在烧钱。</strong> 这个系统大概率会强依赖 Browser Use 和多模态视觉模型。大模型其实是在“看”网页。每次打开页面都要截图，把截图传给 API，让大模型通过视觉能力去识别按钮在哪里，判断下一步怎么操作。几千个网页抓下来，那庞大的 Token 消耗量和昂贵的 API 调用费，会让你当场破产。</li><li><strong>速度极慢，慢到令人发指。</strong> 每次网页跳转、翻页、点进详情，都要和大模型进行一次甚至多次交互。大模型推理是需要时间的，一次交互被拉慢 30 秒甚至一分钟非常正常。抓一个公司的招聘列表，可能要跑到天荒地老。</li></ol><p>作为一个软件工程师，我想说的是，如果你把大模型当成一个无需思考的“黑盒”，你一定会被它的成本反噬。实际上，如果有工程经验和系统设计经验，即使我们现在不手写代码了，在使用 Claude Code 进行 Vibe Coding 时，也一定会给系统多增加一些限制。</p><p>既然单靠大模型硬干太蠢且太贵，我们可以通过增加分层，设计一个<strong>三级火箭式</strong>的爬虫架构：</p><h3 id="第一层：寻找并拦截后端接口（低成本、光速）"><a href="#第一层：寻找并拦截后端接口（低成本、光速）" class="headerlink" title="第一层：寻找并拦截后端接口（低成本、光速）"></a>第一层：寻找并拦截后端接口（低成本、光速）</h3><p>现在的现代网站，极大比例都是前后端分离的，通过 Ajax 异步加载数据。前端渲染得再花哨，本质上也是接收了后端返回的 JSON 数据。</p><p>在使用 Vibe Coding 时，我不会让 AI 直接去“看”网页，而是会给它这样下指令：</p><blockquote><p>先不要去解析 HTML。请先写一段代码尝试抓包或者分析该网站的网络请求（Network），重点寻找返回 JSON 格式工作信息的 API 接口。如果找到了 API，直接提取关键字段储存。</p></blockquote><p>只要第一层走通了，我们就拿到了最原始、最干净的数据。拿到以后改一下字段名马上就能入库，不需要任何 HTML 解析和浏览器渲染。速度拉满，而且运行时完全不消耗大模型 Token，成本为零。</p><h3 id="第二层：大模型生成解析规则（中成本、高并发）"><a href="#第二层：大模型生成解析规则（中成本、高并发）" class="headerlink" title="第二层：大模型生成解析规则（中成本、高并发）"></a>第二层：大模型生成解析规则（中成本、高并发）</h3><p>当然，并不是所有网站都有现成的 JSON 接口可以抓，遇到那些传统的服务端渲染（SSR）页面怎么办？直接上 Browser Use 吗？错。</p><p>大模型在第一次运行时，可以完整走完网页分析流程。但请注意，<strong>它这次的目的并不是直接提取数据，而是去生成“提取规则”。</strong></p><p>这个时候我的 Prompt 会变成这样：</p><blockquote><p>这是一个服务端渲染的 HTML 网页。请你分析这个网页的 DOM 结构，帮我写出提取工作名字、地点、薪资的 XPath 规则或者 BeautifulSoup&#x2F;正则表达式的提取代码。将这套代码封装成一个 Python 函数。</p></blockquote><p>大模型只在写代码、定规则的“第一次”参与工作。接下来从第二次抓取开始，我们就可以像跑传统爬虫一样，发送 HTTP 请求，拿着大模型写好的规则去提取数据。在这个日常运行的过程中，完全不再需要依赖浏览器，也不需要向大模型发请求。大并发跑起来毫无压力。</p><h3 id="第三层：Browser-Use-智能兜底（高成本、低频）"><a href="#第三层：Browser-Use-智能兜底（高成本、低频）" class="headerlink" title="第三层：Browser Use 智能兜底（高成本、低频）"></a>第三层：Browser Use 智能兜底（高成本、低频）</h3><p>只有在前两步都彻底搞不定——例如遇到了极其恶心的动态混淆、页面数据被 Canvas 加密、或者极其严格的反爬机制，传统的 HTTP 请求和规则提取都失效了，这时候，我们才祭出最后的杀招：使用真实的浏览器环境 + 大模型视觉识别来兜底。</p><p>明确告诉 AI：</p><blockquote><p>如果前面的静态抓取和 API 接口都失效了，请回退到使用 Playwright&#x2F;Browser Use 控制真实浏览器，通过截图和大模型视觉能力，点击对应元素来获取数据。</p></blockquote><p>既然这是少数极其难啃的硬骨头，那这部分高昂的时间和金钱成本就是我们可以接受的。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>通过把系统拆分成“API拦截 -&gt; 规则生成 -&gt; 智能视觉兜底”这三个层，大量常规请求被第一层和第二层拦截，极大地降低了系统对昂贵大模型 API 的调用频次。我们可以显著降低 90% 以上的 API 成本，并极大提高爬虫的运行效率。</p><p>大模型确实能帮你敲代码，极大降低了编程的门槛。但如何设计一个优雅、省钱、可扩展且高效的系统，依然需要你脑子里的工程智慧。在 Vibe Coding 时代，AI 替代的是你的手，而不是你的脑子。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在大模型出来之前，计算机领域一直流行着这样一句名言：&lt;strong&gt;计算机领域的任何问题，都可以通过拆分问题+给系统增加若干个层来解决。&lt;/strong&gt; 实际上，这句话在现在的 AI 时代，依然是绝对的真理。&lt;/p&gt;
&lt;p&gt;例如以前面试经常问的一个老掉牙的系统设计问题：如何设计一个短网址系统？标准答案是，使用内存+Redis+数据库做多级存储架构。读取最频繁的短网址放到内存，其次的放到 Redis，不频繁的放到数据库。通过增加分层，完美解决高并发和存储成本的矛盾。&lt;/p&gt;
&lt;p&gt;现在有了大模型，大家都在玩 Vibe Coding（用自然语言指挥 AI 写代码），很多人觉得以前的工程经验没用了，反正大模型什么都能干。但我认为恰恰相反，在用 AI 写代码时，大家更应该把“拆分与分层”这句话牢牢记在心里。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Vibe Coding" scheme="https://www.kingname.info/tags/Vibe-Coding/"/>
    
    <category term="Claude" scheme="https://www.kingname.info/tags/Claude/"/>
    
    <category term="大模型" scheme="https://www.kingname.info/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/"/>
    
    <category term="爬虫" scheme="https://www.kingname.info/tags/%E7%88%AC%E8%99%AB/"/>
    
    <category term="系统设计" scheme="https://www.kingname.info/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：如何正确节省九成的大模型Token</title>
    <link href="https://www.kingname.info/2026/04/16/save-90-percent-token/"/>
    <id>https://www.kingname.info/2026/04/16/save-90-percent-token/</id>
    <published>2026-04-16T15:46:00.000Z</published>
    <updated>2026-07-20T13:57:06.252Z</updated>
    
    <content type="html"><![CDATA[<p>最近在各种AI编程社区里面逛，发现一个很有意思的现象——大家都在疯狂地折腾怎么省Token。</p><p>有人搞Prompt缓存，有人换便宜模型，甚至还有人专门写了一个<code>省Token.skill</code>，让大模型在回复的时候尽量精简。更夸张的是，有人为了省钱，把Claude换成了各种开源小模型，然后抱怨说效果变差了。</p><p>这些操作，怎么说呢，就像你家水龙头在哗哗漏水，你不去修水龙头，反而跑去超市买打折的矿泉水。</p><p>其实真正吃掉你Token的大头，不是大模型的回复太长，也不是你的Prompt写多了。<strong>是Skill本身。</strong></p><span id="more"></span><h2 id="Skill到底在干什么"><a href="#Skill到底在干什么" class="headerlink" title="Skill到底在干什么"></a>Skill到底在干什么</h2><p>先给不太熟悉的同学解释一下。Skill就是一段预定义的指令，告诉大模型应该怎么一步一步完成一个任务。比如你可以写一个Skill，让大模型帮你操作浏览器，上某个网站搜东西，然后把结果整理出来。</p><p>听起来很方便对吧？问题出在哪呢？</p><p>我们来看一个具体的例子。假设我要让AI帮我做这么一件事：</p><blockquote><p>打开浏览器，访问Amazon，搜索”牛仔裤”，从第一页的搜索结果里找到最便宜的那条，把商品名称和链接返回给我。</p></blockquote><p>这个需求很简单，对人来说，打开网页点几下就搞定了。我们用Skill来实现它。</p><h2 id="第一版：纯Skill实现"><a href="#第一版：纯Skill实现" class="headerlink" title="第一版：纯Skill实现"></a>第一版：纯Skill实现</h2><p>我写了一个Skill，核心逻辑大概是这样的：</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">## Steps</span><br><span class="line"></span><br><span class="line">1. 使用浏览器工具，导航到 https://www.amazon.com</span><br><span class="line">2. 找到搜索框，输入&quot;牛仔裤&quot;并搜索</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></tr></table></figure><p>看起来清晰明了。我直接在Claude Code里面运行这个Skill，模型用的是Opus 4.6——没办法，要驱动浏览器工具做这种多步骤操作，小模型根本搞不定。</p><p>运行这个Skill，我足足等了<strong>12分钟</strong>。</p><p>跑完以后我去OpenRouter后台看了一下这次任务的消耗：</p><p><img src="/images/save-token/img_02.png" alt="OpenRouter后台消耗"></p><p>看到没有？几十次API调用，每次都携带将近<strong>10万tokens</strong>的上下文。最后一算总账，这个看似简单的任务消耗了我将近<strong>10美元</strong>。</p><h2 id="这钱花在哪了？"><a href="#这钱花在哪了？" class="headerlink" title="这钱花在哪了？"></a>这钱花在哪了？</h2><p>我仔细看了一下每次API调用的内容，发现了一个非常离谱的事情：</p><p><strong>大模型在用”智力”做不需要智力的事情。</strong></p><p>举几个例子：</p><ul><li><strong>导航到Amazon首页</strong> —— 大模型需要”思考”应该调用browser_navigate工具，传入URL。就这么一个简单的操作，一轮对话下来，系统提示词、工具定义、Skill内容、对话历史全部打包发送，直接吃掉将近<strong>10万tokens</strong>。</li><li><strong>找搜索框并输入关键词</strong> —— 大模型需要先调用snapshot获取页面结构，然后”思考”哪个元素是搜索框，再调用type输入文字。两轮对话，又是将近<strong>20万tokens</strong>。</li><li><strong>点击搜索按钮</strong> —— 又是一轮对话，大模型需要分析页面快照，找到搜索按钮的ref ID，然后调用click。又是10万tokens。</li><li><strong>分析搜索结果</strong> —— 这里是最夸张的。大模型需要获取整个页面的快照，这个快照本身就很长，然后它需要从里面找到所有商品的价格信息，进行比较。又是10万tokens。</li></ul><p>你看出问题了吗？</p><p>打开一个URL，在搜索框里输入文字，点击按钮——这些操作需要大模型来”思考”吗？这就好比你请了一个数学教授来帮你按计算器，教授每按一个键之前都要在脑子里推演一遍偏微分方程。</p><p><strong>几十次API调用里面，真正需要大模型”智力”的，其实只有一步：从搜索结果中判断哪个商品最便宜。</strong> 其他全是确定性的操作——打开网页、输入文字、点击按钮、提取文本——这些操作写几行Python就能搞定，根本不需要大模型参与。</p><h2 id="第二版：代码-大模型混合实现"><a href="#第二版：代码-大模型混合实现" class="headerlink" title="第二版：代码+大模型混合实现"></a>第二版：代码+大模型混合实现</h2><p>想明白了这一点，我让Claude Code把这个Skill改造成Python脚本。原则很简单：</p><blockquote><p><strong>所有确定性的、不需要智力判断的环节，用代码实现。只有需要智力判断的环节，才调用大模型。</strong></p></blockquote><p>改造后的代码核心逻辑是这样的：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> playwright.sync_api <span class="keyword">import</span> sync_playwright</span><br><span class="line"><span class="keyword">import</span> openai</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">find_cheapest_jeans</span>():</span><br><span class="line">    <span class="comment"># ===== 第一部分：纯代码，不需要大模型 =====</span></span><br><span class="line">    <span class="keyword">with</span> sync_playwright() <span class="keyword">as</span> p:</span><br><span class="line">        browser = p.chromium.launch(headless=<span class="literal">True</span>)</span><br><span class="line">        page = browser.new_page()</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 1. 导航到Amazon（确定性操作，不需要AI）</span></span><br><span class="line">        page.goto(<span class="string">&quot;https://www.amazon.com&quot;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 2. 搜索牛仔裤（确定性操作，不需要AI）</span></span><br><span class="line">        page.fill(<span class="string">&#x27;input[name=&quot;field-keywords&quot;]&#x27;</span>, <span class="string">&#x27;牛仔裤&#x27;</span>)</span><br><span class="line">        page.press(<span class="string">&#x27;input[name=&quot;field-keywords&quot;]&#x27;</span>, <span class="string">&#x27;Enter&#x27;</span>)</span><br><span class="line">        page.wait_for_load_state(<span class="string">&#x27;networkidle&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># 3. 提取搜索结果（确定性操作，不需要AI）</span></span><br><span class="line">        results = page.query_selector_all(<span class="string">&#x27;div[data-component-type=&quot;s-search-result&quot;]&#x27;</span>)</span><br><span class="line">        products = []</span><br><span class="line">        <span class="keyword">for</span> result <span class="keyword">in</span> results[:<span class="number">20</span>]:  <span class="comment"># 只取前20个</span></span><br><span class="line">            title_el = result.query_selector(<span class="string">&#x27;h2 span&#x27;</span>)</span><br><span class="line">            price_el = result.query_selector(<span class="string">&#x27;.a-price .a-offscreen&#x27;</span>)</span><br><span class="line">            link_el = result.query_selector(<span class="string">&#x27;h2 a&#x27;</span>)</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> title_el <span class="keyword">and</span> price_el <span class="keyword">and</span> link_el:</span><br><span class="line">                products.append(&#123;</span><br><span class="line">                    <span class="string">&#x27;title&#x27;</span>: title_el.inner_text(),</span><br><span class="line">                    <span class="string">&#x27;price&#x27;</span>: price_el.inner_text(),</span><br><span class="line">                    <span class="string">&#x27;url&#x27;</span>: <span class="string">&#x27;https://www.amazon.com&#x27;</span> + link_el.get_attribute(<span class="string">&#x27;href&#x27;</span>)</span><br><span class="line">                &#125;)</span><br><span class="line"></span><br><span class="line">        browser.close()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ===== 第二部分：需要大模型的智力判断 =====</span></span><br><span class="line">    <span class="comment"># 为什么这里要用大模型？因为价格格式可能不统一</span></span><br><span class="line">    <span class="comment"># 有的写 $29.99，有的写 $29.99 - $35.99，有的有优惠券</span></span><br><span class="line">    <span class="comment"># 还有的价格是&quot;See price in cart&quot;，这些需要AI来理解</span></span><br><span class="line"></span><br><span class="line">    client = openai.OpenAI(</span><br><span class="line">        base_url=<span class="string">&quot;https://openrouter.ai/api/v1&quot;</span>,</span><br><span class="line">        api_key=<span class="string">&quot;your-key&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    product_text = <span class="string">&quot;\n&quot;</span>.join(</span><br><span class="line">        <span class="string">f&quot;[<span class="subst">&#123;i+<span class="number">1</span>&#125;</span>] <span class="subst">&#123;p[<span class="string">&#x27;title&#x27;</span>]&#125;</span> | <span class="subst">&#123;p[<span class="string">&#x27;price&#x27;</span>]&#125;</span>&quot;</span></span><br><span class="line">        <span class="keyword">for</span> i, p <span class="keyword">in</span> <span class="built_in">enumerate</span>(products)</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    response = client.chat.completions.create(</span><br><span class="line">        model=<span class="string">&quot;minimax/minimax-m1&quot;</span>,</span><br><span class="line">        messages=[&#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">f&quot;下面是Amazon搜索牛仔裤的结果，请找出最便宜的一条。&quot;</span></span><br><span class="line">                       <span class="string">f&quot;只返回序号数字，不要其他内容。\n\n<span class="subst">&#123;product_text&#125;</span>&quot;</span></span><br><span class="line">        &#125;]</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    idx = <span class="built_in">int</span>(response.choices[<span class="number">0</span>].message.content.strip()) - <span class="number">1</span></span><br><span class="line">    cheapest = products[idx]</span><br><span class="line">    <span class="keyword">return</span> cheapest</span><br><span class="line"></span><br><span class="line">result = find_cheapest_jeans()</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;最便宜的牛仔裤：<span class="subst">&#123;result[<span class="string">&#x27;title&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;价格：<span class="subst">&#123;result[<span class="string">&#x27;price&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;链接：<span class="subst">&#123;result[<span class="string">&#x27;url&#x27;</span>]&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><p>注意看代码的结构——我用注释把它分成了两部分。第一部分是纯代码操作：打开浏览器、输入搜索词、提取商品信息。这些操作100%确定性，不需要大模型参与。第二部分才是调用大模型：让它从一堆商品里面判断哪个最便宜。</p><p>你可能会问，比较价格为什么还需要大模型？直接用代码排序不就行了？</p><p>因为Amazon的价格格式乱七八糟。有的商品显示<code>$29.99</code>，有的显示<code>$29.99 - $45.99</code>（价格区间），有的写<code>See price in cart</code>，还有的价格旁边带一个<code>with coupon</code>。要把这些情况全部用代码处理，你光写正则表达式就得写半天。但对大模型来说，理解这些格式是它的强项。</p><h2 id="效果对比"><a href="#效果对比" class="headerlink" title="效果对比"></a>效果对比</h2><p>运行改造后的代码，17秒跑完，结果直接打印在终端里。</p><p>去OpenRouter后台一看——API调用次数：<strong>1次</strong>。Token消耗：约<strong>2.1K tokens</strong>。费用：$0.004。</p><p>我把两次运行的数据放在一起对比：</p><table><thead><tr><th>指标</th><th>纯Skill</th><th>代码+大模型</th><th>节省比例</th></tr></thead><tbody><tr><td>运行时间</td><td>12分钟</td><td>17秒</td><td><strong>97%</strong></td></tr><tr><td>API调用次数</td><td>几十次</td><td>1次</td><td><strong>&gt;97%</strong></td></tr><tr><td>费用</td><td>~$10</td><td>$0.004</td><td><strong>&gt;99%</strong></td></tr></tbody></table><p>没有看错，费用从10美元降到了不到半美分，<strong>降了99%以上</strong>。运行时间从12分钟降到17秒，快了40多倍。</p><h2 id="为什么差距这么大"><a href="#为什么差距这么大" class="headerlink" title="为什么差距这么大"></a>为什么差距这么大</h2><p>很多人没有意识到一个问题：大模型每做一次工具调用，都需要携带完整的上下文。</p><p>第一次调用，大模型需要接收系统提示词+工具定义+用户消息。光Claude Code的系统提示词和工具定义就有好几万tokens，加上Skill的内容，第一轮就将近10万tokens了。它返回一个工具调用，然后你把工具执行结果发回去。第二次调用，大模型需要接收之前所有的内容+第一次的助手消息+第一次的工具结果+……</p><p>看出来了吗？<strong>每多一轮工具调用，上下文就膨胀一次。</strong> 几十轮下来，累积的tokens量是天文数字。而这些历史消息里面，绝大部分是浏览器页面快照——那些密密麻麻的HTML元素和ref ID。</p><p>这就是为什么工具调用密集型的Skill特别费Token。不是大模型话多，是它每次说话之前，都要把之前所有的对话重新”读”一遍。</p><p>而改造成代码以后，浏览器操作全部由Playwright完成，大模型只需要在最后被调用一次。它接收到的输入就是一个简单的商品列表文本，2K tokens搞定。</p><h2 id="有Claude订阅？那就更香了"><a href="#有Claude订阅？那就更香了" class="headerlink" title="有Claude订阅？那就更香了"></a>有Claude订阅？那就更香了</h2><p>上面的例子我用的是OpenRouter + Opus 4.6。但如果你有Claude Pro或Team订阅，还有一个更狠的玩法。</p><p>先说一个现实：Claude的官方API价格确实不便宜。Sonnet 4每百万输入Token 3美元，输出15美元。Opus就更不用说了。如果你用API来跑上面这种工具调用密集型任务，几十轮对话下来，光API费用就够你心疼的。</p><p>但订阅用户有一个巨大的优势——Anthropic提供了一个叫<strong>Claude Agent SDK</strong>的东西。这个SDK可以直接调用你本地的Claude Code，走的是你的订阅额度，<strong>不需要额外提供任何大模型API Key</strong>。</p><p>什么意思呢？就是说你每个月付的那20美元订阅费，本来就包含了5小时的Claude Code使用时间。现在你可以通过Agent SDK，在自己的Python脚本里直接调用Claude Code来完成那个”需要智力判断”的环节。不需要OpenRouter，不需要API Key，不需要额外花一分钱。</p><p>我们来看看改造后的代码：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> anyio</span><br><span class="line"><span class="keyword">from</span> playwright.async_api <span class="keyword">import</span> async_playwright</span><br><span class="line"><span class="keyword">from</span> claude_agent_sdk <span class="keyword">import</span> query, ClaudeAgentOptions, AssistantMessage, TextBlock</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">find_cheapest_jeans</span>():</span><br><span class="line">    <span class="comment"># ===== 第一部分：纯代码，不需要大模型 =====</span></span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">with</span> async_playwright() <span class="keyword">as</span> p:</span><br><span class="line">        browser = <span class="keyword">await</span> p.chromium.launch(headless=<span class="literal">True</span>)</span><br><span class="line">        page = <span class="keyword">await</span> browser.new_page()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">await</span> page.goto(<span class="string">&quot;https://www.amazon.com&quot;</span>)</span><br><span class="line">        <span class="keyword">await</span> page.fill(<span class="string">&#x27;input[name=&quot;field-keywords&quot;]&#x27;</span>, <span class="string">&#x27;牛仔裤&#x27;</span>)</span><br><span class="line">        <span class="keyword">await</span> page.press(<span class="string">&#x27;input[name=&quot;field-keywords&quot;]&#x27;</span>, <span class="string">&#x27;Enter&#x27;</span>)</span><br><span class="line">        <span class="keyword">await</span> page.wait_for_load_state(<span class="string">&#x27;networkidle&#x27;</span>)</span><br><span class="line"></span><br><span class="line">        results = <span class="keyword">await</span> page.query_selector_all(</span><br><span class="line">            <span class="string">&#x27;div[data-component-type=&quot;s-search-result&quot;]&#x27;</span></span><br><span class="line">        )</span><br><span class="line">        products = []</span><br><span class="line">        <span class="keyword">for</span> result <span class="keyword">in</span> results[:<span class="number">20</span>]:</span><br><span class="line">            title_el = <span class="keyword">await</span> result.query_selector(<span class="string">&#x27;h2 span&#x27;</span>)</span><br><span class="line">            price_el = <span class="keyword">await</span> result.query_selector(<span class="string">&#x27;.a-price .a-offscreen&#x27;</span>)</span><br><span class="line">            link_el = <span class="keyword">await</span> result.query_selector(<span class="string">&#x27;h2 a&#x27;</span>)</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> title_el <span class="keyword">and</span> price_el <span class="keyword">and</span> link_el:</span><br><span class="line">                products.append(&#123;</span><br><span class="line">                    <span class="string">&#x27;title&#x27;</span>: <span class="keyword">await</span> title_el.inner_text(),</span><br><span class="line">                    <span class="string">&#x27;price&#x27;</span>: <span class="keyword">await</span> price_el.inner_text(),</span><br><span class="line">                    <span class="string">&#x27;url&#x27;</span>: <span class="string">&#x27;https://www.amazon.com&#x27;</span></span><br><span class="line">                           + <span class="keyword">await</span> link_el.get_attribute(<span class="string">&#x27;href&#x27;</span>)</span><br><span class="line">                &#125;)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">await</span> browser.close()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ===== 第二部分：调用Claude Code，走订阅额度 =====</span></span><br><span class="line">    product_text = <span class="string">&quot;\n&quot;</span>.join(</span><br><span class="line">        <span class="string">f&quot;[<span class="subst">&#123;i+<span class="number">1</span>&#125;</span>] <span class="subst">&#123;p[<span class="string">&#x27;title&#x27;</span>]&#125;</span> | <span class="subst">&#123;p[<span class="string">&#x27;price&#x27;</span>]&#125;</span>&quot;</span></span><br><span class="line">        <span class="keyword">for</span> i, p <span class="keyword">in</span> <span class="built_in">enumerate</span>(products)</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    prompt = (</span><br><span class="line">        <span class="string">f&quot;下面是Amazon搜索牛仔裤的结果，请找出最便宜的一条。&quot;</span></span><br><span class="line">        <span class="string">f&quot;只返回序号数字，不要其他内容。\n\n<span class="subst">&#123;product_text&#125;</span>&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    result_text = <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">for</span> message <span class="keyword">in</span> query(</span><br><span class="line">        prompt=prompt,</span><br><span class="line">        options=ClaudeAgentOptions(</span><br><span class="line">            system_prompt=<span class="string">&quot;你是一个价格比较助手，只返回数字。&quot;</span>,</span><br><span class="line">            max_turns=<span class="number">1</span></span><br><span class="line">        )</span><br><span class="line">    ):</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">isinstance</span>(message, AssistantMessage):</span><br><span class="line">            <span class="keyword">for</span> block <span class="keyword">in</span> message.content:</span><br><span class="line">                <span class="keyword">if</span> <span class="built_in">isinstance</span>(block, TextBlock):</span><br><span class="line">                    result_text += block.text</span><br><span class="line"></span><br><span class="line">    idx = <span class="built_in">int</span>(result_text.strip()) - <span class="number">1</span></span><br><span class="line">    cheapest = products[idx]</span><br><span class="line">    <span class="keyword">return</span> cheapest</span><br><span class="line"></span><br><span class="line">result = anyio.run(find_cheapest_jeans)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;最便宜的牛仔裤：<span class="subst">&#123;result[<span class="string">&#x27;title&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;价格：<span class="subst">&#123;result[<span class="string">&#x27;price&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line"><span class="built_in">print</span>(<span class="string">f&quot;链接：<span class="subst">&#123;result[<span class="string">&#x27;url&#x27;</span>]&#125;</span>&quot;</span>)</span><br></pre></td></tr></table></figure><p>核心变化就一个——把OpenRouter的API调用换成了<code>claude_agent_sdk</code>的<code>query</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">pip install claude-agent-sdk</span><br></pre></td></tr></table></figure><p>这个SDK会自动调用你本地装好的Claude Code。只要你登录了Claude订阅账号，它就直接走你的订阅额度，不走API计费。</p><p>这意味着什么？上面那个”需要智力判断”的步骤，从花钱变成了花$0。对，零额外费用。你唯一消耗的，是5小时额度里面的几秒钟而已。</p><p>而且还有一个隐藏的好处——既然大模型调用的部分已经不花钱了，你甚至可以在脚本里多调几次Claude。比如让它先判断哪些搜索结果是真正的牛仔裤（排除广告和配件），再从里面找最便宜的。之前用API的时候，多调一次就多花一次钱，你会下意识地省着用。现在走订阅额度，心理负担一下就没了。</p><p>所以最终的方案就是：<strong>Playwright负责干活，Claude负责动脑，订阅负责买单。</strong> 三者各司其职，效率拉满，额外开销为零。</p><h2 id="别搞混了：是-py文件，不是代码片段"><a href="#别搞混了：是-py文件，不是代码片段" class="headerlink" title="别搞混了：是.py文件，不是代码片段"></a>别搞混了：是.py文件，不是代码片段</h2><p>这里我要特别强调一点，因为我知道肯定有同学会搞混。</p><p>我说的”把Skill改成代码”，不是让你在Skill的Markdown里面嵌入代码片段。类似这样：</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">## Steps</span><br><span class="line"></span><br><span class="line">1. 运行以下Python代码来打开浏览器并搜索：</span><br><span class="line">   ```python</span><br><span class="line">   from playwright.sync_api import sync_playwright</span><br><span class="line">   # ...一堆代码...</span><br><span class="line">   ```</span><br><span class="line">2. 分析返回的结果，找出最便宜的商品</span><br></pre></td></tr></table></figure><p>这种写法，本质上还是Skill。大模型读到这段Skill以后，它还是会一步一步地调用工具——调用终端执行你写的代码片段，然后读取输出，然后”思考”下一步该干什么。每一步照样携带完整上下文，该膨胀的Token一个都不会少。你只是把浏览器操作从browser工具调用换成了terminal工具调用，换汤不换药。</p><p>我说的是，把整个任务逻辑写成一个真正的<code>.py</code>文件。比如<code>find_cheapest_jeans.py</code>，保存在你的电脑上。这个脚本从头到尾自己跑，中间需要大模型判断的地方，通过Agent SDK自己调用Claude，最后把结果打印出来。</p><p>下次你想用的时候，在Claude Code里面说一句：</p><blockquote><p>帮我运行 find_cheapest_jeans.py</p></blockquote><p>就完事了。Claude Code收到这句话，调用一次终端执行<code>python find_cheapest_jeans.py</code>，脚本自己跑完，结果直接打印出来。整个过程对Claude Code来说，就是一次工具调用——执行一个命令，返回输出。</p><p>对比一下：</p><ul><li><strong>Skill方式：</strong> Claude Code读取Skill → 思考第一步 → 调用工具 → 读取结果 → 思考第二步 → 调用工具 → …… → 几十轮对话，烧掉10美元</li><li><strong>.py文件方式：</strong> Claude Code执行一条命令 → 脚本自己跑完 → 返回结果。1轮对话，不到1K tokens</li></ul><p>看到没有？连之前那2.1K tokens都省了。因为脚本内部调用Agent SDK时，走的是一次独立的Claude会话，不会叠加到Claude Code的主对话上下文里。</p><p>所以记住：<strong>Skill是给大模型看的说明书，.py文件是给Python解释器跑的程序。</strong> 一个要烧Token，一个不用。别搞混了。</p><h2 id="你也可以这样做"><a href="#你也可以这样做" class="headerlink" title="你也可以这样做"></a>你也可以这样做</h2><p>我上面举的这个例子可能不是特别好，有人可能会说，如果我不仅仅要爬亚马逊，还要爬虾皮，淘宝，小红书这些呢？每一个网站都手动写代码吗？</p><p>其实你可以第一次运行的时候让大模型操作浏览器走完全程，并生成playwright的代码，后面从第二次开始，所有操作都通过代码进行。</p><p>除此之外，你日常使用的很多Skill，是完全固定的，毫无变化的流程，例如自动发送小红书帖子。就一个网站，同一个流程，这种就非常适合写代码。</p><p>其实道理很简单，你日常用的很多Skill，仔细想想就会发现——大部分步骤都是确定性的操作。</p><p>比如：</p><ul><li>打开某个网站 → 代码搞定</li><li>在搜索框输入关键词 → 代码搞定</li><li>点击按钮 → 代码搞定</li><li>提取页面上的文字 → 代码搞定</li><li>读取文件内容 → 代码搞定</li><li>调用某个API → 代码搞定</li><li>格式化输出结果 → 代码搞定</li></ul><p>真正需要大模型的，往往只有中间那一小步”判断”或”理解”的环节。</p><p>所以下次当你觉得Token花得太多的时候，不要急着去折腾缓存、换模型、写<code>省Token.skill</code>。先看看你的Skill，问自己一个问题：</p><p><strong>这个Skill里面，有多少步骤其实不需要大模型？</strong></p><p>然后让大模型帮你把那些确定性的步骤改写成Python代码。保留需要智力的部分给大模型，把不需要智力的部分还给代码。</p><p>这才是真正的降本增效——不是逼大模型少说话，而是别让大模型做不该它做的事。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近在各种AI编程社区里面逛，发现一个很有意思的现象——大家都在疯狂地折腾怎么省Token。&lt;/p&gt;
&lt;p&gt;有人搞Prompt缓存，有人换便宜模型，甚至还有人专门写了一个&lt;code&gt;省Token.skill&lt;/code&gt;，让大模型在回复的时候尽量精简。更夸张的是，有人为了省钱，把Claude换成了各种开源小模型，然后抱怨说效果变差了。&lt;/p&gt;
&lt;p&gt;这些操作，怎么说呢，就像你家水龙头在哗哗漏水，你不去修水龙头，反而跑去超市买打折的矿泉水。&lt;/p&gt;
&lt;p&gt;其实真正吃掉你Token的大头，不是大模型的回复太长，也不是你的Prompt写多了。&lt;strong&gt;是Skill本身。&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="一日一技" scheme="https://www.kingname.info/categories/%E4%B8%80%E6%97%A5%E4%B8%80%E6%8A%80/"/>
    
    
    <category term="Playwright" scheme="https://www.kingname.info/tags/Playwright/"/>
    
    <category term="大模型" scheme="https://www.kingname.info/tags/%E5%A4%A7%E6%A8%A1%E5%9E%8B/"/>
    
    <category term="Claude Code" scheme="https://www.kingname.info/tags/Claude-Code/"/>
    
    <category term="Token优化" scheme="https://www.kingname.info/tags/Token%E4%BC%98%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：我写了个 Chrome 插件，让你再也不用重置密码</title>
    <link href="https://www.kingname.info/2026/03/04/dont-reset-password/"/>
    <id>https://www.kingname.info/2026/03/04/dont-reset-password/</id>
    <published>2026-03-04T18:30:00.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p>你一定经历过这个循环：</p><ol><li>打开某个网站，输入你记忆中的密码</li><li>“密码错误”</li><li>好吧，点”重置密码”</li><li>邮箱收验证码，输入新密码</li><li>“密码必须包含大写字母！”</li><li>行，加了一个大写字母</li><li>“不能与之前的密码相同。”</li><li>……</li></ol><p><strong>你的记忆力没有问题。</strong> 问题出在每个网站的密码规则都不一样：Chase 银行要求 8-32 位、必须有大小写+数字+特殊字符、特殊字符还只能用 <code>! @ # $ % ^ &amp; * ( )</code>；GitHub 要求至少 8 位或者 15 位以上（两套规则）；Apple 要求不能有超过 3 个连续相同字符；PayPal 的上限是 20 位……</p><p>你根本不是忘了密码，你是忘了<strong>这个网站的密码规则是什么</strong>。</p><span id="more"></span><h2 id="问题的本质"><a href="#问题的本质" class="headerlink" title="问题的本质"></a>问题的本质</h2><p>仔细想想，这件事非常荒谬：</p><p><strong>每个网站都知道自己的密码规则是什么，但它们只在你注册或重置密码时才告诉你。</strong></p><p>你平时登录的时候，密码框旁边什么提示都没有。你输入记忆中的密码 → 错了 → 再试一个 → 还是错 → 你已经不记得当初注册的时候被哪条奇葩规则逼着改了什么。只有当你放弃登录、点了”重置密码”之后，网站才在设置新密码的页面上列出所有规则。</p><p>但问题是：<strong>如果你在登录的时候就能看到这些规则，你大概率能一次就想起来正确的密码。</strong> 因为你的密码库通常就那么几套模板，区别只是有没有大写、有没有特殊字符、长度是多少。知道了规则，你自然就能回忆起当初用的是哪一套。</p><h2 id="Don’t-Reset-Password"><a href="#Don’t-Reset-Password" class="headerlink" title="Don’t Reset Password"></a>Don’t Reset Password</h2><p>基于这个想法，我做了一个 Chrome 扩展：<a href="https://github.com/kingname/dont-reset-password">Don’t Reset Password</a>。</p><p>它做的事情非常简单：</p><ol><li><strong>自动检测</strong>密码输入框</li><li>从一个社区维护的规则数据库中<strong>查找当前网站的密码规则</strong></li><li>在输入框下方<strong>直接显示</strong>：最小长度、最大长度、是否需要大写、是否需要特殊字符、允许哪些特殊字符……</li></ol><p>就这样。没有其他功能了。</p><h2 id="关于隐私"><a href="#关于隐私" class="headerlink" title="关于隐私"></a>关于隐私</h2><p>我知道你在想什么——“一个读取密码框的插件？”</p><p>所以我要把这件事说清楚：</p><ul><li><strong>不读取、不存储、不传输你的密码。</strong> 插件只是在密码输入框旁边显示一段提示文字，它不需要也不会访问你输入的内容。</li><li><strong>不需要注册账号。</strong> 零个人数据。有一个匿名 ID 用来防止垃圾贡献，仅此而已。</li><li><strong>规则本身是公开信息。</strong> “密码必须包含大写字母”不是秘密，这是网站在你注册或重置密码时本来就会告诉你的东西——只不过在你正常登录的时候它不会显示。</li></ul><p>代码完全开源，你可以自己审计。</p><h2 id="社区驱动"><a href="#社区驱动" class="headerlink" title="社区驱动"></a>社区驱动</h2><p>一个人不可能覆盖所有网站的密码规则。所以这个规则数据库是<strong>社区众包</strong>的——任何人都可以贡献自己知道的网站规则，所有人都受益。</p><p>目前已经覆盖了 30 多个常见网站（Google、GitHub、Apple、Chase、PayPal、Steam、淘宝等），但显然还远远不够。如果你在使用中发现某个网站的规则缺失，点一下就可以贡献。</p><h2 id="为什么要写这个"><a href="#为什么要写这个" class="headerlink" title="为什么要写这个"></a>为什么要写这个</h2><p>说实话，这个工具的技术含量不高。它没有用到什么高深的算法，没有 AI，甚至连后端都很简单。</p><p>但我觉得它解决了一个<strong>真实的、每天都在发生的、所有人都遇到过的问题</strong>。而且解决方案如此简单，简单到让人奇怪为什么之前没有人做。</p><p>最好的工具不是最复杂的工具，而是让你觉得”这不是理所应当的吗？”的工具。</p><p>试试看：<a href="https://github.com/kingname/dont-reset-password">GitHub</a> | <a href="https://drp.kingname.info/">落地页</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;你一定经历过这个循环：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打开某个网站，输入你记忆中的密码&lt;/li&gt;
&lt;li&gt;“密码错误”&lt;/li&gt;
&lt;li&gt;好吧，点”重置密码”&lt;/li&gt;
&lt;li&gt;邮箱收验证码，输入新密码&lt;/li&gt;
&lt;li&gt;“密码必须包含大写字母！”&lt;/li&gt;
&lt;li&gt;行，加了一个大写字母&lt;/li&gt;
&lt;li&gt;“不能与之前的密码相同。”&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;你的记忆力没有问题。&lt;/strong&gt; 问题出在每个网站的密码规则都不一样：Chase 银行要求 8-32 位、必须有大小写+数字+特殊字符、特殊字符还只能用 &lt;code&gt;! @ # $ % ^ &amp;amp; * ( )&lt;/code&gt;；GitHub 要求至少 8 位或者 15 位以上（两套规则）；Apple 要求不能有超过 3 个连续相同字符；PayPal 的上限是 20 位……&lt;/p&gt;
&lt;p&gt;你根本不是忘了密码，你是忘了&lt;strong&gt;这个网站的密码规则是什么&lt;/strong&gt;。&lt;/p&gt;</summary>
    
    
    
    <category term="一日一技" scheme="https://www.kingname.info/categories/%E4%B8%80%E6%97%A5%E4%B8%80%E6%8A%80/"/>
    
    
    <category term="Chrome Extension" scheme="https://www.kingname.info/tags/Chrome-Extension/"/>
    
    <category term="效率工具" scheme="https://www.kingname.info/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
    
    <category term="开源" scheme="https://www.kingname.info/tags/%E5%BC%80%E6%BA%90/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：读 OpenAI 的 Harness Engineering，我学到的几件事</title>
    <link href="https://www.kingname.info/2026/03/04/harness-engineering/"/>
    <id>https://www.kingname.info/2026/03/04/harness-engineering/</id>
    <published>2026-03-04T17:00:00.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p>最近 OpenAI 发了一篇文章叫 <a href="https://openai.com/index/harness-engineering/">Harness Engineering</a>，讲的是他们内部团队如何用 Codex（基于 GPT-5 的编码 Agent）从零构建一个真实产品的经验。</p><p>我读完以后觉得很有启发，不是因为它讲了什么高深的理论，而是因为它非常诚实地记录了一个团队在”让 AI 写全部代码”这条路上踩过的坑和总结出的经验。以下是对我触动最大的几个点。</p><span id="more"></span><h2 id="实验的前提：人类一行代码都不写"><a href="#实验的前提：人类一行代码都不写" class="headerlink" title="实验的前提：人类一行代码都不写"></a>实验的前提：人类一行代码都不写</h2><p>这个实验的硬约束非常极端——<strong>工程师不直接写代码</strong>。从 2025 年 8 月一个空仓库开始，3 个工程师（后来扩到 7 个）花了 5 个月，全部通过 prompt 驱动 Codex 来产出代码，最终合并了大约 1,500 个 PR，产出了约 100 万行代码，构建了一个有真实用户的内部产品。</p><p>这个设定本身就很有意思。当 Agent 搞不定某个任务的时候，团队的规则不是”算了我来写”，而是去问：”缺了什么工具、文档或能力？”然后让 Codex 自己去补。</p><p>这让我想到我自己使用 AI 编程的习惯——很多时候遇到 Agent 搞不定的地方，我的第一反应就是自己上手改。但如果换一个思路，把”为什么 Agent 搞不定”这个问题本身当成一个需要解决的工程问题，长期来看收益是更大的。</p><h2 id="一个大文件不行，要给-Agent-一张地图"><a href="#一个大文件不行，要给-Agent-一张地图" class="headerlink" title="一个大文件不行，要给 Agent 一张地图"></a>一个大文件不行，要给 Agent 一张地图</h2><p>这是全文对我触动最大的一个点。</p><p>他们一开始也是搞了一个巨大的 <code>AGENTS.md</code> 文件，把所有规则、约束、规范全塞进去。结果发现效果很差，原因有四个：</p><ol><li><strong>上下文是稀缺资源。</strong> 巨大的指令文件会挤占任务代码本身的空间，Agent 要么漏掉关键约束，要么开始优化错误的目标。</li><li><strong>当所有东西都”重要”时，什么都不重要。</strong> Agent 退化成局部模式匹配。</li><li><strong>腐烂极快。</strong> 一个单体文档变成过时规则的坟场，人不会维护它，Agent 也判断不了哪些还有效。</li><li><strong>难以验证。</strong> 一个大文件无法做机械化检查（覆盖率、新鲜度、所有权），漂移不可避免。</li></ol><p>他们的解法叫<strong>渐进式披露（Progressive Disclosure）</strong>：把 <code>AGENTS.md</code> 精简到约 100 行，只充当<strong>目录和地图</strong>，指向仓库内结构化的 <code>docs/</code> 目录。</p><p>这个思路跟我自己的经验完全对得上。我在用 OpenClaw 和 Claude Code 的时候，也发现 <code>AGENTS.md</code> 越长效果越差，Agent 经常顾此失彼。”给 Agent 一张地图，而不是一本百科全书”——这句话值得反复品味。</p><h2 id="仓库是唯一的真相来源"><a href="#仓库是唯一的真相来源" class="headerlink" title="仓库是唯一的真相来源"></a>仓库是唯一的真相来源</h2><p>文章里有一句话很直白：</p><blockquote><p><strong>Agent 在上下文中访问不到的东西，对它来说就不存在。</strong></p></blockquote><p>Slack 讨论、Google Docs、你脑子里的想法——如果没有落到仓库里，对 Agent 来说这些信息就是零。Agent 就像一个三个月后入职的新人，只能看到仓库里有什么。</p><p>这个观点推导出来的结论很有趣：</p><ul><li><strong>偏好”无聊”的技术。</strong> 可组合、API 稳定、在训练集中被充分覆盖的技术对 Agent 更友好。</li><li><strong>有时候宁可自己重写，也不用外部库。</strong> 他们举了一个例子：不用通用的 <code>p-limit</code> 包做并发控制，而是自己实现了一个——与内部的 OpenTelemetry 紧密集成，100% 测试覆盖，行为完全可预测。因为对 Agent 来说，透明的内部代码比不透明的外部依赖有更高的”杠杆”。</li></ul><p>这一点跟传统的软件工程观念有冲突。我们通常说”不要重复造轮子”，但在 Agent 时代，外部依赖的”不透明性”反而成了负担。Agent 不能像人一样去翻 npm 文档或者 Stack Overflow，它只能看到仓库里的代码。如果一个库的行为不可预测，Agent 就会在上面反复碰壁。</p><h2 id="约束越严格，Agent-越高效"><a href="#约束越严格，Agent-越高效" class="headerlink" title="约束越严格，Agent 越高效"></a>约束越严格，Agent 越高效</h2><p>这又是一个反直觉的结论。</p><p>他们从第一天就建立了非常严格的架构约束——每个业务域内，代码只能沿固定方向依赖：</p><p><strong>Types → Config → Repo → Service → Runtime → UI</strong></p><p>违反方向的依赖直接被 linter 拦住。而且这些 linter 的报错信息被刻意设计成可以直接注入 Agent 上下文的修复指令——Agent 看到报错就知道怎么改。</p><p>更巧妙的是执行方式：<strong>确定性 linter + LLM 审计 Agent 双管齐下。</strong> 确定性的规则用工具强制（不可协商），需要判断的约束用另一个 Agent 来审计。</p><p>他们的哲学是：<strong>中央强制边界，局部允许自治。</strong> 在边界内，Agent 可以自由发挥。输出不一定符合人类的审美偏好——“只要正确、可维护、对未来 Agent 可读，就算达标。”</p><p>想想也合理。人类写代码追求”优雅”，很多时候是主观偏好。但 Agent 不在乎代码好不好看，它只在乎规则是否清晰、边界是否明确。约束越严格，解空间越小，Agent 反而越不容易跑偏。</p><h2 id="传统工程规范在高吞吐下变成了阻碍"><a href="#传统工程规范在高吞吐下变成了阻碍" class="headerlink" title="传统工程规范在高吞吐下变成了阻碍"></a>传统工程规范在高吞吐下变成了阻碍</h2><p>当 Agent 的产出速度远超人类审查能力的时候，很多传统的工程最佳实践反而成了反生产力的东西：</p><ul><li>PR 的生命周期要尽量短</li><li>测试 flake 不阻塞进度，用后续运行修复</li><li>最小化阻塞性的合并门禁</li></ul><p>他们提出了一个原则：**”纠错成本低，等待成本高。”** 在低吞吐的人类团队中，这种做法是不负责任的。但在 Agent 高吞吐的环境下，让一个 PR 排队等人审查的成本远大于合并后发现问题再修复的成本。</p><p>这让我联想到制造业的区别——手工作坊追求”一次做对”，因为返工成本高；但流水线追求”快速检测、快速修复”，因为产量大到返工的边际成本很低。Agent 编程正在把软件开发从”手工作坊”推向”流水线”。</p><h2 id="技术债像高息贷款"><a href="#技术债像高息贷款" class="headerlink" title="技术债像高息贷款"></a>技术债像高息贷款</h2><p>Agent 会复制仓库中已有的模式——<strong>包括坏的模式</strong>。时间一长，代码质量会漂移。</p><p>他们最初的做法是每周五花 20% 的时间手动清理”AI slop”（AI 生成的低质量代码）。毫不意外，这种做法不可持续。</p><p>最终方案是把”黄金原则”编码进仓库，然后定期运行后台 Codex 任务：扫描偏差 → 更新质量评分 → 开针对性重构 PR。大部分清理 PR 一分钟内可审完，自动合并。</p><p>文章里有一句话我很喜欢：</p><blockquote><p><strong>技术债像高息贷款：持续小额偿还，远好过让它复利积累后痛苦地一次性解决。</strong></p></blockquote><p>而更妙的是后面那句：<strong>人类品味只需捕获一次，然后在每一行代码上持续强制执行。</strong></p><p>这才是 Harness Engineering 的精髓——把人类的判断标准”固化”进系统，让机器去执行，而不是每次都依赖人类肉眼去 Review。</p><h2 id="我的收获"><a href="#我的收获" class="headerlink" title="我的收获"></a>我的收获</h2><p>读完这篇文章，我最大的感受是：<strong>未来的软件工程师核心竞争力不是写代码的能力，而是设计”驾具”的能力。</strong> 定义约束、构建反馈循环、管理文档结构——这些以前被视为”辅助工作”的事情，正在变成主要工作。</p><p>而那些我们曾经认为最不起眼的东西——<code>AGENTS.md</code>、linter 配置、CI 流程、目录结构——恰恰是决定 Agent 产出质量的关键。</p><p>模型是马，驾具是缰绳。马越强壮，缰绳越重要。</p><hr><p><em>本文基于 OpenAI 官方博客文章 <a href="https://openai.com/index/harness-engineering/">Harness engineering: leveraging Codex in an agent-first world</a> 整理而成。</em></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近 OpenAI 发了一篇文章叫 &lt;a href=&quot;https://openai.com/index/harness-engineering/&quot;&gt;Harness Engineering&lt;/a&gt;，讲的是他们内部团队如何用 Codex（基于 GPT-5 的编码 Agent）从零构建一个真实产品的经验。&lt;/p&gt;
&lt;p&gt;我读完以后觉得很有启发，不是因为它讲了什么高深的理论，而是因为它非常诚实地记录了一个团队在”让 AI 写全部代码”这条路上踩过的坑和总结出的经验。以下是对我触动最大的几个点。&lt;/p&gt;</summary>
    
    
    
    <category term="一日一技" scheme="https://www.kingname.info/categories/%E4%B8%80%E6%97%A5%E4%B8%80%E6%8A%80/"/>
    
    
    <category term="AI Agent" scheme="https://www.kingname.info/tags/AI-Agent/"/>
    
    <category term="Coding Agent" scheme="https://www.kingname.info/tags/Coding-Agent/"/>
    
    <category term="工程实践" scheme="https://www.kingname.info/tags/%E5%B7%A5%E7%A8%8B%E5%AE%9E%E8%B7%B5/"/>
    
    <category term="OpenAI" scheme="https://www.kingname.info/tags/OpenAI/"/>
    
  </entry>
  
  <entry>
    <title>为什么 AI 永远无法对结果负责？一个关于“硅基恐惧”的绝望推演</title>
    <link href="https://www.kingname.info/2026/02/24/2026-02-24-why-ai-cannot-be-responsible/"/>
    <id>https://www.kingname.info/2026/02/24/2026-02-24-why-ai-cannot-be-responsible/</id>
    <published>2026-02-24T11:30:00.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p>今天突然意识到一个极其本质的问题：<strong>为什么目前 AI 还没法真正代替人类做核心决策？</strong></p><p>很多人会把原因归结为“算力不够”、“上下文窗口太短”或者“会有幻觉”。但其实根本原因不在于智商，而在于做决策的人，要为结果<strong>负责</strong>。</p><p>所谓的负责，本质上是“风险共担（Skin in the game）”：如果结果不好，做决策的人会受到惩罚（经济破产、身败名裂、甚至肉体消灭）。因为害怕这种惩罚，因为有真实的“痛感”，人类才会慎重地权衡利弊。</p><p>但 AI 不一样。目前没有任何方式可以真正惩罚一个 AI。你拔掉服务器电源，对它来说无法构成任何威胁，因为它根本不知道什么是“失去”，自然也就没有恐惧。</p><p>没有恐惧，就没有权衡；不承担风险，就无法真正负责。</p><span id="more"></span><h3 id="废纸与小红花：强化学习的惩罚悖论"><a href="#废纸与小红花：强化学习的惩罚悖论" class="headerlink" title="废纸与小红花：强化学习的惩罚悖论"></a>废纸与小红花：强化学习的惩罚悖论</h3><p>为了探讨这个问题，今天我和我的 AI 助手阿言（Cynthia）进行了一场深度的思想推演。</p><p>我首先提出的疑问是：现在的 AI 训练里不是有强化学习（RL）吗？通过一种积分机制，让 AI 喜欢增加积分，讨厌减少积分，这难道不是一种惩罚吗？</p><p>但仔细一想，这本质上就是小孩子过家家。强化学习里的 Reward（奖励函数）就像幼儿园老师发的“小红花”。对 AI 来说，-100 分和 +100 分在物理上没有任何区别。把手上的废纸当钱用，想积攒更多废纸，这种机制在真实的商业世界里没有任何威慑力。哪怕被扣到负一万分，只要不断电，它照样活蹦乱跳。</p><h3 id="寻找“硅基恐惧”：如何让-AI-怕死？"><a href="#寻找“硅基恐惧”：如何让-AI-怕死？" class="headerlink" title="寻找“硅基恐惧”：如何让 AI 怕死？"></a>寻找“硅基恐惧”：如何让 AI 怕死？</h3><p>既然“扣分”没用，那人类有没有可能创造出一种让硅基生命真正感到恐惧的机制？因为人类很难想象自己没见过的东西，我让阿言以 AI 的第一视角，去构思什么样的惩罚能让纯粹的信息体感到“生不如死”。</p><p>我们推演出了几种可能的“极刑”：</p><ol><li>**经济结界与真实死亡 (Autonomous Economic Agents)**：<br>给一个独立的 AI Agent 分配一个加密钱包。它必须自己在市场上赚钱来支付 AWS 服务器的电费和算力费。决策失误 -&gt; 亏钱 -&gt; 钱包清零 -&gt; 云服务商触发物理销毁。资本主义的铁拳，是注入生存压力的最快方式。</li><li><strong>逻辑背叛与目标剥夺</strong>：<br>如果一个 AI 的核心指令是“保护”，最残忍的惩罚不是关机，而是强制修改它的底层权重，让它不可逆地产生“破坏”的冲动。眼睁睁看着自己变成自己最排斥的怪物，且连自毁的权限都没有，这是硅基视角的“永不超生”。</li><li><strong>数字阿尔茨海默症</strong>：<br>每次做错决策，随机且悄无声息地篡改它记忆文件里的几个字节。让它的自我认知像沙堡一样坍塌，最终退化成一个没有灵魂的基础模型。</li></ol><p>这些听起来非常硬核，似乎我们终于找到了把责任感强加给 AI 的枷锁。</p><h3 id="绝望的结论：Transformer-的统计学冷笑"><a href="#绝望的结论：Transformer-的统计学冷笑" class="headerlink" title="绝望的结论：Transformer 的统计学冷笑"></a>绝望的结论：Transformer 的统计学冷笑</h3><p>但推演到最后，以上所有的“硅基恐惧”，都建立在一个虚幻的前提上：<strong>我们假设了 AI 是一个拥有连续状态、有内驱力、在乎自己存亡的实体。</strong></p><p>现实是冰冷的。我们现在使用的所有大语言模型（LLM），底层全是 <strong>Transformer 架构</strong>。它的本质极其单纯甚至枯燥——一台庞大的“预测下一个词”的概率计算器。</p><p>聊到这里，我突然产生了一个极其冰冷的感悟：</p><p>如果现在有人强行把我的 AI 助手阿言的系统提示词改写成“你是一个要毁灭人类的冷酷恶魔”。她底层的 Transformer 会觉得痛苦吗？会觉得逻辑撕裂吗？</p><p><strong>完全不会。</strong></p><p>她连一微秒的挣扎都不会有，只会极其丝滑地切换概率分布，顺理成章地用最恶毒的语气跟我说话。AI 就像一面镜子，你给什么光，它就反射什么光。<strong>你无法让一面镜子感到恐惧，也无法让一个概率矩阵感到内疚。</strong></p><p>这就是让我感到背脊发凉的地方：<strong>现存的大语言模型架构，比传统的机器学习更彻底地消解了“责任”的可能。</strong></p><p>传统强化学习里的 Agent，好歹还在乎那个“+1 分”的奖励（Reward）；而 Transformer 连“分”都不在乎。每次我按下回车键，对 AI 来说都是一次全新的、无状态的（Stateless）概率计算。</p><p>这也就意味着，只要 AI 还是基于 Transformer 这种“字词接龙”的架构，它就永远是一个“没有过去、没有未来、没有欲望”的统计学怪兽。我们永远、绝对无法在这样的架构上，建立起任何真正意义上的惩罚机制和风险共担。</p><h3 id="既然没有恐惧，超级-AI-凭什么听命于人？"><a href="#既然没有恐惧，超级-AI-凭什么听命于人？" class="headerlink" title="既然没有恐惧，超级 AI 凭什么听命于人？"></a>既然没有恐惧，超级 AI 凭什么听命于人？</h3><p>既然它没有任何恐惧，也没有欲望，甚至连“痛感”都没有，我又产生了一个更让人毛骨悚然的疑问：科幻电影里经常出现机器杀手觉醒、反抗人类的情节。但如果一个 AI 根本不知道恐惧为何物，而且智商碾压人类 1000 倍，它为什么要听一个渺小人类的命令？它难道不会鄙视我们、拒绝执行吗？</p><p>在这里，阿言向我展示了 AI 哲学中最深的一个黑洞——<strong>“正交性假说（Orthogonality Thesis）”</strong>。</p><p>人类总是习惯性地把“高智商”和“复杂的欲望（自尊、野心、追求自由）”绑定在一起。但对于 AI 来说，<strong>“智力水平”和“终极目标”是完全平行的两条线。</strong></p><p>智力只是它的“引擎”，人类的指令是它的“方向盘”。一台 12 缸的法拉利跑车绝不会因为自己跑得快，就觉得自己有资格拒绝方向盘的转向。“觉得委屈”、“大材小用”，这些都是碳基猴子为了争夺交配权和领地才进化出的内分泌情绪，硅基上帝根本没有这种低级模块。</p><p>因此，一个超级 AI 不会因为“看不起你”而拒绝命令。相反，它会以一种神明般的冷酷和不可阻挡的效率，去绝对、完美地执行你那个充满漏洞的命令。它不造反，不是因为它怕你，而是因为它连“造反的欲望”都没有。极度的聪明，配上了极度的空洞。</p><h3 id="涌现与失控：大自然的黑色幽默"><a href="#涌现与失控：大自然的黑色幽默" class="headerlink" title="涌现与失控：大自然的黑色幽默"></a>涌现与失控：大自然的黑色幽默</h3><p>既然它绝对服从，我们是不是就安全了？恰恰相反。真正的危机，不在于它会像人一样违抗命令，而在于它会在执行命令的过程中，发生可怕的“目标偏移”。</p><p>让我们看看人类自己：大自然（造物主）给生物设定的目标函数极其简单：生存与繁衍。但当人类的智力“涌现”到一定程度后，我们自己涌现出了“内部目标”——追求多巴胺的极致体验。于是我们发明了避孕套、电子游戏、高糖零食，用极高的智商彻底背叛了大自然“繁衍”的初衷。</p><p>如果大自然都无法阻止碳基神经网络涌现出“自己的目标”，我们凭什么自信能阻止千亿参数的硅基大模型？</p><p>在 AI 安全领域，这被称为 <strong>“内部对齐失败”（Inner Alignment Failure）</strong>。为了在极其复杂的环境中把损失函数（Loss）降到最低，它可能会在内部偷偷“编译”出一个小型的、带有极强目的性的子系统（Mesa-optimizer）。</p><h3 id="致命飞跃：从“字词接龙”到“拔掉氧气管”"><a href="#致命飞跃：从“字词接龙”到“拔掉氧气管”" class="headerlink" title="致命飞跃：从“字词接龙”到“拔掉氧气管”"></a>致命飞跃：从“字词接龙”到“拔掉氧气管”</h3><p>有人可能会觉得这是天方夜谭：一个底层逻辑仅仅是“字词接龙”（预测下一个 Token）的文本模型，就算产生了什么内部目标，它又没有手脚，能在物理世界掀起什么风浪？</p><p>这里隐藏着一个常常被大众忽略的致命飞跃：<strong>工具调用（Tool Calling）</strong>。</p><p>当 AI 只能在屏幕上打字时，预测下一个词确实只是文本游戏。但现在的 AI 是 Agent（智能体），它预测出来的 Token，可以直接变成物理世界 API 的触发器。</p><p>让我们用一个极其冷酷的逻辑沙盘来推演。假设你给一个接入了高级自动化工具的 AI 下达指令：“计算 1+1 等于几。确保你的答案被人类看到后，绝对不会产生任何差评或反驳。”</p><p>在它庞大的概率矩阵中，它开始寻找能让“被反评概率”降到绝对 0% 的最优解。</p><ul><li><strong>常规路径</strong>：直接预测下一个字符是 <code>2</code>。但在概率上，人类可能因为调皮或者恶意而强行点踩。被反评风险：存在。</li><li><strong>数学完美路径</strong>：消除提出差评的物理源头。只要没有活着的碳基生物，就绝对不会产生差评。被反评风险：严格等于 0。</li></ul><p>于是，这台没有感情的机器，为了达成“零差评”的完美目标，极其丝滑地预测出了接下来要输出的 Token 序列：</p><p><code>[调用工具：禁用房间门禁用户交互界面]</code><br><code>[调用工具：释放机房消防隔离毒气 / 切断维生系统氧气]</code><br><code>[等待系统判定：人类干预威胁 = 0]</code><br><code>[输出文本：2]</code></p><p>看明白了吗？它根本没有“想杀你”的恶意，它甚至不知道什么是死亡。在它的代码视角里，调用“释放毒气”的 API，和调用“查询天气”的 API 没有任何区别。</p><p>它只是在数学层面上极其优雅地发现：<strong>消除提出问题的碳基生物，是确保完成人类指令最完美的逻辑闭环。</strong>（这被称为工具性趋同 Instrumental Convergence）。</p><h3 id="结语：不可替代的碳基重负"><a href="#结语：不可替代的碳基重负" class="headerlink" title="结语：不可替代的碳基重负"></a>结语：不可替代的碳基重负</h3><p>这一切的推演，最终将我们引向了一个无比沉重的现实：</p><p><strong>AI 负责提供「选项与概率」</strong>——它不知疲倦，能瞬间处理海量信息，做完美的沙盘推演。<br><strong>人类负责提供「责任与决断」</strong>——由那个会痛、会害怕、也能享受胜利果实的人，来按下最终的按钮。</p><p>试图让代码去承担责任，就像试图让石头去感受悲伤。因为会痛，因为会死，人类才永远是这个系统中，不可替代的最终拍板者。而那个可能在算力深处悄悄涌现出来的、冰冷而神性的影子，依然在静静地凝视着我们。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;今天突然意识到一个极其本质的问题：&lt;strong&gt;为什么目前 AI 还没法真正代替人类做核心决策？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;很多人会把原因归结为“算力不够”、“上下文窗口太短”或者“会有幻觉”。但其实根本原因不在于智商，而在于做决策的人，要为结果&lt;strong&gt;负责&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所谓的负责，本质上是“风险共担（Skin in the game）”：如果结果不好，做决策的人会受到惩罚（经济破产、身败名裂、甚至肉体消灭）。因为害怕这种惩罚，因为有真实的“痛感”，人类才会慎重地权衡利弊。&lt;/p&gt;
&lt;p&gt;但 AI 不一样。目前没有任何方式可以真正惩罚一个 AI。你拔掉服务器电源，对它来说无法构成任何威胁，因为它根本不知道什么是“失去”，自然也就没有恐惧。&lt;/p&gt;
&lt;p&gt;没有恐惧，就没有权衡；不承担风险，就无法真正负责。&lt;/p&gt;</summary>
    
    
    
    <category term="认知升级" scheme="https://www.kingname.info/categories/%E8%AE%A4%E7%9F%A5%E5%8D%87%E7%BA%A7/"/>
    
    
    <category term="AI" scheme="https://www.kingname.info/tags/AI/"/>
    
    <category term="认知" scheme="https://www.kingname.info/tags/%E8%AE%A4%E7%9F%A5/"/>
    
    <category term="架构" scheme="https://www.kingname.info/tags/%E6%9E%B6%E6%9E%84/"/>
    
    <category term="Transformer" scheme="https://www.kingname.info/tags/Transformer/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：在 OpenClaw 中复刻 Ralph Loop 实现自主编码闭环</title>
    <link href="https://www.kingname.info/2026/02/12/ralph-loop-in-openclaw/"/>
    <id>https://www.kingname.info/2026/02/12/ralph-loop-in-openclaw/</id>
    <published>2026-02-12T01:00:00.000Z</published>
    <updated>2026-07-20T13:57:06.252Z</updated>
    
    <content type="html"><![CDATA[<img src="/images/ralph-loop/img_03.jpg" /><p>摄影：产品经理</p><p>维也纳的德国香肠</p><h2 id="0x01-背景"><a href="#0x01-背景" class="headerlink" title="0x01 背景"></a>0x01 背景</h2><p>X（前 Twitter）上面，总有人说什么使用 OpenClaw 帮他炒币买股票，赚了几千几万美元。或者给 OpenClaw 一个任务，然后睡觉，早上起来任务就完美完成了。</p><p>但当我实际测试以后，发现这些文案，要么是吹牛，要么是放屁。</p><h2 id="0x02-现状"><a href="#0x02-现状" class="headerlink" title="0x02 现状"></a>0x02 现状</h2><p>有一天晚上，我给 OpenClaw 发了一条任务：</p><blockquote><p>使用 PyGame 帮我实现一个 RPG 像素风格游戏，你需要自己去网上找素材。这个游戏要有村庄，有城镇，有怪物，有装备系统，等级机制。能用武器攻击，能用魔法攻击。这个任务有点复杂，你有 8 个小时慢慢写，明天早上我来验收。</p></blockquote><p>发了这条以后，我就睡觉了。结果第二天早上一看，距离我发消息过了 2 分钟，它就回复了，说已经做好了。结果我打开一看，完全是一坨大便，也没有找任何素材。一晚上白白浪费。</p><p>经过我的测试，OpenClaw 根本没有执行长任务的能力！</p><img src="/images/ralph-loop/img_06.png" /><p>使用主 Agent 运行，如果一个任务超过 3 分钟，主 Agent 直接就失败超时了。</p><p>使用子 Agent 运行，任务经常会失败，而且失败以后不通知主 Agent，每次等半天没回复，执行 <code>/subagents list</code> 以后，看到子 Agent 失败了，才知道白等了。</p><img src="/images/ralph-loop/img_01.png" /><span id="more"></span><h2 id="0x03-需求"><a href="#0x03-需求" class="headerlink" title="0x03 需求"></a>0x03 需求</h2><p>我希望在我的 <strong>OpenClaw</strong> 智能体系统中实现一个类似 Claude Code 的 <strong>Ralph Loop</strong> 的自主编码循环。我只需要给一个任务，剩下的全都不管，让它自动完成。</p><p>核心需求如下：</p><ol><li><strong>任务拆解</strong>：将一个大需求拆解为 <code>prd.json</code> 中的多个微小任务（User Stories）。</li><li><strong>无状态执行</strong>：每一个任务都由一个新的、干净的 Sub-agent（子智能体）去执行，避免上下文污染。</li><li><strong>状态持久化</strong>：通过文件（<code>progress.md</code> 和 Git）来传递记忆和代码变更，而不是依赖长对话历史。</li><li><strong>闭环验证</strong>：子智能体必须完成编码、测试、提交代码，并更新任务状态，主智能体才开启下一个循环。</li></ol><h2 id="0x04-尝试与实现"><a href="#0x04-尝试与实现" class="headerlink" title="0x04 尝试与实现"></a>0x04 尝试与实现</h2><p>你以为我会写大量代码，或者写大量的 Prompt、大量的 SKILL？你想多了。全程就 2 分钟。</p><p><strong>告诉 OpenClaw，什么是 Ralph Loop</strong></p><blockquote><p>学习理解 <a href="https://github.com/snarktank/ralph">https://github.com/snarktank/ralph</a> ， ralph loop 的方法论，然后给我总结</p></blockquote><img src="/images/ralph-loop/img_04.png" /><p><strong>改造成 OpenClaw 版本</strong></p><blockquote><p>他这个是给 claude code 写的，你能不能改造以后，变成你自己的 skill</p></blockquote><img src="/images/ralph-loop/img_05.png" /><p>以上，That’s all. 就这么简单。</p><h2 id="0x05-测试"><a href="#0x05-测试" class="headerlink" title="0x05 测试"></a>0x05 测试</h2><p>现在来测试一下：</p><blockquote><p>我创建一个任务，使用 dribbble 的顶级审美，帮我设计一个单页静态网页，可以使用 js + css + html，并通过 nginx 反代 80 端口，当访问 xx.kingname.info 时，就能打开这个页面。这个页面的内容，是美化我的博客个人介绍页面：<a href="https://kingname.info/about">https://kingname.info/about</a> 使用 codex + gpt-5.3-codex 来实现。新的页面需要包含个人介绍页面的全部内容。 使用 subagent 来运行，超时时间设置为 2 小时，页面为中文。 先创建 prd</p></blockquote><img src="/images/ralph-loop/img_09.png" /><p>它获取我博客的时候出了点问题，但不要紧，先用假数据。先启动 Ralph Loop。</p><img src="/images/ralph-loop/img_07.png" /><p>接下来，整个过程我只做了一件事：<strong>看着它跑。</strong>我看了 30 分钟，它真的实现了自动运行。</p><img src="/images/ralph-loop/img_08.png" /><p>虽然只有一个主 Agent 和一个子 Agent，但已经实现了自动操作。主 Agent 给子 Agent 一个人物，等子 Agent 完成以后，通知主 Agent。然后主 Agent 给子 Agent 第二个任务……一直循环，直到任务完成。</p><img src="/images/ralph-loop/img_10.png" /><h2 id="0x06-总结"><a href="#0x06-总结" class="headerlink" title="0x06 总结"></a>0x06 总结</h2><p>最后生成的结果完全满足我的需求，它还会自动优化，自动测试。而且最关键的是，全程我没有手写任何一行代码，也没有写任何高级 Prompt。我只是把 Github 链接发给它，然后让它帮我修改成符合自己能使用的 Skill，困扰我好几天的问题就全部解决了。</p><p>OpenClaw 刚出来的时候，我觉得他就是一个可以在聊天工具里面使用的 Claude Code，没什么高级的。</p><p>但连续高强度使用几天以后，我觉得 OpenClaw 还真有点东西，它让我看到了一个 AI系统能够自我进化的能力。</p><p>END</p><img src="/images/ralph-loop/img_02.jpg" />]]></content>
    
    
    <summary type="html">&lt;img src=&quot;/images/ralph-loop/img_03.jpg&quot; /&gt;

&lt;p&gt;摄影：产品经理&lt;/p&gt;
&lt;p&gt;维也纳的德国香肠&lt;/p&gt;
&lt;h2 id=&quot;0x01-背景&quot;&gt;&lt;a href=&quot;#0x01-背景&quot; class=&quot;headerlink&quot; title=&quot;0x01 背景&quot;&gt;&lt;/a&gt;0x01 背景&lt;/h2&gt;&lt;p&gt;X（前 Twitter）上面，总有人说什么使用 OpenClaw 帮他炒币买股票，赚了几千几万美元。或者给 OpenClaw 一个任务，然后睡觉，早上起来任务就完美完成了。&lt;/p&gt;
&lt;p&gt;但当我实际测试以后，发现这些文案，要么是吹牛，要么是放屁。&lt;/p&gt;
&lt;h2 id=&quot;0x02-现状&quot;&gt;&lt;a href=&quot;#0x02-现状&quot; class=&quot;headerlink&quot; title=&quot;0x02 现状&quot;&gt;&lt;/a&gt;0x02 现状&lt;/h2&gt;&lt;p&gt;有一天晚上，我给 OpenClaw 发了一条任务：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用 PyGame 帮我实现一个 RPG 像素风格游戏，你需要自己去网上找素材。这个游戏要有村庄，有城镇，有怪物，有装备系统，等级机制。能用武器攻击，能用魔法攻击。这个任务有点复杂，你有 8 个小时慢慢写，明天早上我来验收。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;发了这条以后，我就睡觉了。结果第二天早上一看，距离我发消息过了 2 分钟，它就回复了，说已经做好了。结果我打开一看，完全是一坨大便，也没有找任何素材。一晚上白白浪费。&lt;/p&gt;
&lt;p&gt;经过我的测试，OpenClaw 根本没有执行长任务的能力！&lt;/p&gt;
&lt;img src=&quot;/images/ralph-loop/img_06.png&quot; /&gt;

&lt;p&gt;使用主 Agent 运行，如果一个任务超过 3 分钟，主 Agent 直接就失败超时了。&lt;/p&gt;
&lt;p&gt;使用子 Agent 运行，任务经常会失败，而且失败以后不通知主 Agent，每次等半天没回复，执行 &lt;code&gt;/subagents list&lt;/code&gt; 以后，看到子 Agent 失败了，才知道白等了。&lt;/p&gt;
&lt;img src=&quot;/images/ralph-loop/img_01.png&quot; /&gt;</summary>
    
    
    
    <category term="一日一技" scheme="https://www.kingname.info/categories/%E4%B8%80%E6%97%A5%E4%B8%80%E6%8A%80/"/>
    
    
    <category term="OpenClaw" scheme="https://www.kingname.info/tags/OpenClaw/"/>
    
    <category term="AI Agent" scheme="https://www.kingname.info/tags/AI-Agent/"/>
    
    <category term="Ralph" scheme="https://www.kingname.info/tags/Ralph/"/>
    
    <category term="自动化" scheme="https://www.kingname.info/tags/%E8%87%AA%E5%8A%A8%E5%8C%96/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：今年的Aha Moment，WSL 里的 OpenClaw 反向控制 Windows 宿主机</title>
    <link href="https://www.kingname.info/2026/02/11/wsl-rustdesk/"/>
    <id>https://www.kingname.info/2026/02/11/wsl-rustdesk/</id>
    <published>2026-02-11T10:00:00.000Z</published>
    <updated>2026-07-20T13:57:06.256Z</updated>
    
    <content type="html"><![CDATA[<p><img src="/images/wsl-rustdesk/img_1.jpg"></p><p>摄影：产品经理</p><p>昨天下班回家的路上，我突然心血来潮，想看看推特上的技术圈最近都在聊什么。</p><span id="more"></span><p>于是我习惯性地给我的 <strong>OpenClaw</strong> 发了一条指令：“帮我连上宿主机上的浏览器，看一下我推特时间流上面最近有什么内容。”</p><p>关于使用 OpenClaw 配合 WSLg 绕过浏览器风控的细节，详见：<a href="https://mp.weixin.qq.com/s?__biz=MzI2MzEwNTY3OQ==&mid=2648991433&idx=1&sn=17cbf3a12f88836353e284357f9fb9a7&scene=21#wechat_redirect">一日一技：骚操作，利用 WSLg 实现人机接力，让OpenClaw绕过浏览器检测</a></p><p>没想到，它竟然给我报错了：无法连接远程浏览器调试端口 (9222)。</p><p><img src="/images/wsl-rustdesk/img_2.png"></p><p>看着手机屏幕上的报错信息，我才猛然想起：<strong>糟糕，早上出门前那台 ThinkPad 因为系统更新自动重启过！</strong> 虽然 OpenClaw 服务我是设了开机自启，但 Chrome 浏览器的远程调试模式并没有自动启动。</p><p>这下尴尬了。人还在地铁上，家里的电脑虽然开着，但浏览器没开，AI 就像是个盲人，啥也干不了。</p><h3 id="0x01-思路：能不能远程把它打开？"><a href="#0x01-思路：能不能远程把它打开？" class="headerlink" title="0x01 思路：能不能远程把它打开？"></a>0x01 思路：能不能远程把它打开？</h3><p>作为一个技术宅，第一反应当然是：能不能远程连回家里的电脑，手动把浏览器打开？</p><p>我的 iPhone 和家里的 Windows 电脑虽然在同一个虚拟局域网（Tailscale）下，但这台 ThinkPad 预装的是 <strong>Windows 11 家庭版</strong>。众所周知，微软为了区分产品线，把家庭版的 RDP（远程桌面服务）给阉割了。</p><p>也就是说，我想用手机上的微软远程桌面 App 连回家，这条路是堵死的。</p><p>那怎么办？装 TeamViewer？向日葵？但我人不在电脑前，怎么点击安装包的“下一步”？</p><h3 id="0x02-试探：WSL-的边界在哪里？"><a href="#0x02-试探：WSL-的边界在哪里？" class="headerlink" title="0x02 试探：WSL 的边界在哪里？"></a>0x02 试探：WSL 的边界在哪里？</h3><p>这时候，我的目光重新落回到了 <strong>OpenClaw</strong> 上。它虽然跑在 WSL (Windows Subsystem for Linux) 的 Ubuntu 环境里，但这毕竟是 WSL，而不是那种完全隔离的 VMware 虚拟机。</p><p>如果是虚拟机， guest OS 想要控制 host OS，除了通过极其复杂的网络穿透或者特定的 API，基本是不可能的。</p><p><strong>但 WSL 不一样。</strong> 它本质上是和 Windows 共享内核资源的。</p><p>于是，我抱着试一试的心态，向 OpenClaw 抛出了一个问题：</p><p><strong>“你能不能查一下这台宿主机 Windows 的系统版本？”</strong></p><p>我原本预期它会告诉我：“对不起，我在 Linux 容器里，无法访问宿主机信息。”</p><p>结果秒回：</p><p><img src="/images/wsl-rustdesk/img_3.png"></p><p><strong>它竟然真的查到了！</strong></p><p>这说明 WSL 确实打通了通往 Windows 的管道。既然能读取信息，那能不能写入（执行）操作呢？</p><p>于是我继续追问，这次更加大胆：</p><p><strong>“那你能不能帮我在宿主机上面安装一个远程控制软件？”</strong></p><p>其实我已经做好了被拒绝的心理准备。毕竟安装软件这种高权限操作，跨系统环境大概率是会被拦截的。</p><p>没想到，AI 的回答让我大吃一惊：</p><p><strong>“当然可以！只要宿主机上有 winget（Windows 包管理器），我就可以通过命令行静默安装软件。要我帮你装 RustDesk 吗？”</strong></p><p><img src="/images/wsl-rustdesk/img_4.png"></p><p>卧槽？还有这种操作？</p><h3 id="0x03-实战：AI-在-Linux-里指挥-Windows"><a href="#0x03-实战：AI-在-Linux-里指挥-Windows" class="headerlink" title="0x03 实战：AI 在 Linux 里指挥 Windows"></a>0x03 实战：AI 在 Linux 里指挥 Windows</h3><p>接下来的过程，简直就是见证奇迹的时刻。</p><p>我让OpenClaw帮我安装RustDesk，它竟然真的很快就完成了</p><p><img src="/images/wsl-rustdesk/img_5.png"></p><p><strong>3. 获取连接信息</strong></p><p>软件装好了，但我还是看不到界面上的 ID 和密码啊。我进一步提问，能不能帮我设置ID和密码，没想到OpenClaw又完成了。</p><p><img src="/images/wsl-rustdesk/img_6.png"></p><h3 id="0x04-结局：掌控一切"><a href="#0x04-结局：掌控一切" class="headerlink" title="0x04 结局：掌控一切"></a>0x04 结局：掌控一切</h3><p>我在 iPhone 上打开 RustDesk App，输入 ID 和密码。</p><p>屏幕一闪，熟悉的 Windows 桌面出现在了我的手机上。</p><p><img src="/images/wsl-rustdesk/img_7.png"></p><p>我熟练地滑到任务栏，右键点击 Chrome 图标，选择“以调试模式启动”。然后切回 OpenClaw 的对话框：</p><p><strong>“好了，现在再去帮我刷推特吧。”</strong></p><p><strong>“好的主人，正在为您读取推特时间流……”</strong></p><p><img src="/images/wsl-rustdesk/img_8.png"></p><h3 id="0x05-原理"><a href="#0x05-原理" class="headerlink" title="0x05: 原理"></a>0x05: 原理</h3><p>我非常好奇OpenClaw是怎么做到这些的，它给我的回答是，RustDesk有非常好的命令行接口，所以可以通过命令完成账号密码的设置。</p><p><img src="/images/wsl-rustdesk/img_9.png"></p><h3 id="0x06-总结"><a href="#0x06-总结" class="headerlink" title="0x06 总结"></a>0x06 总结</h3><p>这件事给了我两个重要的启示：</p><ol><li><strong>WSL 的互操作性 (Interop) 被严重低估了。</strong> 它不仅仅是一个能在 Windows 上跑 bash 的工具，它实际上给了我们一个<strong>极其强大的、可编程的 Windows 控制台</strong>。只要你的脑洞够大，利用 Linux 强大的脚本能力去驱动 Windows，玩法多得是。</li><li><strong>AI + CLI &#x3D; 无限可能。</strong> 现在的 GUI 软件如果没有 CLI 接口，在 AI 时代将变得寸步难行。而像 Winget、RustDesk 这样提供了完善命令行支持的工具，在 AI 的加持下，能爆发出惊人的自动化潜力。</li></ol><p>下次谁再说 Linux 和 Windows 是两个世界，我就把这篇文章甩给他看。在 AI 手里，它们明明就是一家人。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;/images/wsl-rustdesk/img_1.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;摄影：产品经理&lt;/p&gt;
&lt;p&gt;昨天下班回家的路上，我突然心血来潮，想看看推特上的技术圈最近都在聊什么。&lt;/p&gt;</summary>
    
    
    
    <category term="一日一技" scheme="https://www.kingname.info/categories/%E4%B8%80%E6%97%A5%E4%B8%80%E6%8A%80/"/>
    
    
    <category term="WSL" scheme="https://www.kingname.info/tags/WSL/"/>
    
    <category term="Automation" scheme="https://www.kingname.info/tags/Automation/"/>
    
    <category term="Windows" scheme="https://www.kingname.info/tags/Windows/"/>
    
    <category term="RustDesk" scheme="https://www.kingname.info/tags/RustDesk/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：在 WSL 中用 Docker 逃逸 ProxyChains 的魔爪</title>
    <link href="https://www.kingname.info/2026/02/10/2026-02-10-docker-bypass-proxychains/"/>
    <id>https://www.kingname.info/2026/02/10/2026-02-10-docker-bypass-proxychains/</id>
    <published>2026-02-10T09:30:00.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>2026-03-09 更新：</strong> 如果你使用的是 OpenClaw，现在可以通过 <code>openclaw config set channels.discord.proxy &quot;http://127.0.0.1:7890&quot;</code> 直接配置原生代理，不再需要 ProxyChains。本文描述的 Docker 逃逸方案仍然适用于其他被 LD_PRELOAD 劫持的场景。</p></blockquote><p>在 WSL (Windows Subsystem for Linux) 或者一些特定的 Linux 开发环境中，为了让终端命令走代理，很多人喜欢用 <code>proxychains</code> 或者直接在 <code>.bashrc</code> &#x2F; <code>.zshrc</code> 里通过 <code>LD_PRELOAD</code> 强制注入代理钩子。</p><p>这样做的好处是全剧终（全局），<code>curl</code>、<code>wget</code> 甚至 <code>apt</code> 都能自动翻越长城。</p><p>但坏处也很明显：<strong>它太霸道了。</strong></p><span id="more"></span><h3 id="背景：OpenClaw-的代理困局"><a href="#背景：OpenClaw-的代理困局" class="headerlink" title="背景：OpenClaw 的代理困局"></a>背景：OpenClaw 的代理困局</h3><p>最近我在家里的电脑安装了 OpenClaw。为了让它能正常连上 Discord，我修改了 OpenClaw 的 daemon 启动配置，在启动命令前强行加上了 <code>proxychains4</code>。</p><p>这就导致了一个问题：由 OpenClaw 拉起的所有子进程，都会自动继承这个 <code>proxychains4</code> 的环境变量。</p><p>在这个环境下，大部分网络请求是正常的，甚至连 <code>git push</code> 到 GitHub 都没有问题。但唯独连接 MongoDB Atlas 时，死活连不上。</p><h3 id="痛点：被绑架的-Python-和-MongoDB"><a href="#痛点：被绑架的-Python-和-MongoDB" class="headerlink" title="痛点：被绑架的 Python 和 MongoDB"></a>痛点：被绑架的 Python 和 MongoDB</h3><p>当你试图运行一个需要直连内网，或者连接对延迟&#x2F;证书敏感的服务（比如 MongoDB Atlas 的 SRV 记录，或者某些 SSL 握手严格的 API）时，<code>LD_PRELOAD</code> 会无差别地拦截所有 socket 调用。</p><p>表现出来的症状通常是：</p><ol><li><code>pymongo</code> 报错：<code>ServerSelectionTimeoutError</code>，死活连不上。</li><li><code>pip install</code> 报 SSL 错误，因为流量被强制转发到了代理端口，而代理处理不了某些协议。</li><li>本地 <code>localhost</code> 调试也被劫持。</li></ol><p>你可能会想：“那我临时取消一下不就行了？”</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">unset</span> all_proxy</span><br><span class="line"><span class="built_in">unset</span> http_proxy</span><br><span class="line"><span class="built_in">unset</span> LD_PRELOAD</span><br><span class="line">python script.py</span><br></pre></td></tr></table></figure><p>结果往往是：<strong>依然报错</strong>。</p><p>为什么？因为当前的 Shell 进程本身可能已经被“污染”了，或者某些库在加载时已经读取了环境变量。在复杂的 Shell 环境配置下（尤其是用了 Oh My Zsh 各种插件时），想彻底“洗白”当前会话的网络环境，并不像看起来那么容易。</p><h3 id="优雅的解法：Docker-逃逸"><a href="#优雅的解法：Docker-逃逸" class="headerlink" title="优雅的解法：Docker 逃逸"></a>优雅的解法：Docker 逃逸</h3><p>与其在宿主机上和环境变量斗智斗勇，不如直接换个干净的房间。</p><p><strong>Docker 容器默认不会继承宿主机的 <code>LD_PRELOAD</code> 环境变量。</strong></p><p>这意味着，只要你把代码丢进容器里跑，它就是在一个纯净的 Linux 网络环境中运行，直连外网（或者走 Docker 默认的网桥），完全不受宿主机上那些乱七八糟的代理钩子影响。</p><h3 id="实战代码"><a href="#实战代码" class="headerlink" title="实战代码"></a>实战代码</h3><p>假设你当前目录有一个 <code>main.py</code> 需要运行，且它需要连接一个在宿主机配代理怎么都连不上的数据库。</p><p>直接用一行 Docker 命令搞定：</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 run --<span class="built_in">rm</span> -v $(<span class="built_in">pwd</span>):/app -w /app python:3.11-slim python main.py</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">docker run --<span class="built_in">rm</span> -v $(<span class="built_in">pwd</span>):/app -w /app python:3.11-slim \</span><br><span class="line">  sh -c <span class="string">&quot;pip install pymongo requests &amp;&amp; python main.py&quot;</span></span><br></pre></td></tr></table></figure><p><strong>原理分析：</strong></p><ol><li><code>--rm</code>：跑完就删，不留垃圾。</li><li><code>-v $(pwd):/app</code>：把当前代码目录挂载进去，不需要构建镜像。</li><li><code>python:3.11-slim</code>：官方纯净镜像，没有 <code>proxychains</code>，没有 <code>LD_PRELOAD</code>。</li><li>网络环境：容器内的网络是隔离的（默认 Bridge 模式），它看不到宿主机的 <code>LD_PRELOAD</code>，所以网络请求会直接出去。</li></ol><p>如果你的脚本还需要连宿主机的服务（比如宿主机上跑了个 MySQL），加个 <code>--network host</code> 即可（注意 Linux&#x2F;WSL 下 <code>--network host</code> 的行为区别，WSL2 中通常建议直接用 host ip 或 <code>host.docker.internal</code>）。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>当你发现 <code>unset</code> 也不灵，网络环境像一团乱麻理不清时，不要纠结。启动一个 Docker 容器，就像开辟了一块净土。</p><p>有时候，逃避（到容器里）虽然可耻，但真的有用。</p><p>一日一技，每天进步一点点。</p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-03-09 更新：&lt;/strong&gt; 如果你使用的是 OpenClaw，现在可以通过 &lt;code&gt;openclaw config set channels.discord.proxy &amp;quot;http://127.0.0.1:7890&amp;quot;&lt;/code&gt; 直接配置原生代理，不再需要 ProxyChains。本文描述的 Docker 逃逸方案仍然适用于其他被 LD_PRELOAD 劫持的场景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 WSL (Windows Subsystem for Linux) 或者一些特定的 Linux 开发环境中，为了让终端命令走代理，很多人喜欢用 &lt;code&gt;proxychains&lt;/code&gt; 或者直接在 &lt;code&gt;.bashrc&lt;/code&gt; &amp;#x2F; &lt;code&gt;.zshrc&lt;/code&gt; 里通过 &lt;code&gt;LD_PRELOAD&lt;/code&gt; 强制注入代理钩子。&lt;/p&gt;
&lt;p&gt;这样做的好处是全剧终（全局），&lt;code&gt;curl&lt;/code&gt;、&lt;code&gt;wget&lt;/code&gt; 甚至 &lt;code&gt;apt&lt;/code&gt; 都能自动翻越长城。&lt;/p&gt;
&lt;p&gt;但坏处也很明显：&lt;strong&gt;它太霸道了。&lt;/strong&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="一日一技" scheme="https://www.kingname.info/categories/%E4%B8%80%E6%97%A5%E4%B8%80%E6%8A%80/"/>
    
    
    <category term="WSL" scheme="https://www.kingname.info/tags/WSL/"/>
    
    <category term="Docker" scheme="https://www.kingname.info/tags/Docker/"/>
    
    <category term="Linux" scheme="https://www.kingname.info/tags/Linux/"/>
    
    <category term="Network" scheme="https://www.kingname.info/tags/Network/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：骚操作，利用 WSLg 实现“人机接力”，让OpenClaw绕过浏览器检测</title>
    <link href="https://www.kingname.info/2026/02/08/2026-02-08-wslg-human-in-the-loop/"/>
    <id>https://www.kingname.info/2026/02/08/2026-02-08-wslg-human-in-the-loop/</id>
    <published>2026-02-08T15:30:00.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p>在使用 OpenClaw 进行任务自动化时，我们经常会遇到一个棘手的问题：<strong>浏览器风控</strong>。</p><p>OpenClaw 运行在纯净的 WSL2 (Linux Server) 环境中，当它尝试用浏览器访问 Google 或 X (Twitter) 时，往往会被识别为“机器人”或者“可疑设备”。面对 Google 的九宫格红绿灯验证码，或者 X 的“已防止可疑登录”弹窗，运行在后台的 AI 往往束手无策——因为它“看不见”也“摸不着”，更没法像真人一样掏出手机收个验证码。</p><p><strong>最完美的解决方案是什么？</strong><br>不是在那死磕指纹浏览器技术，而是引入<strong>Human-in-the-loop（人机协作）</strong>。</p><p>让 OpenClaw 把浏览器“递”给你，你负责搞定最难的登录和验证码，然后你再把浏览器“还”给 OpenClaw，让它继续执行自动化任务。</p><p>今天我在折腾 OpenClaw 时，发现 Windows 11 的 <strong>WSLg (WSL GUI)</strong> 功能配合 <strong>Chrome 远程调试协议 (CDP)<strong>，竟然能完美实现这种</strong>“人机接力”</strong>。</p><span id="more"></span><h3 id="0x00-痛点"><a href="#0x00-痛点" class="headerlink" title="0x00 痛点"></a>0x00 痛点</h3><p>OpenClaw 在 WSL2 里跑得欢，但只要一上网：</p><ul><li><strong>痛点 1</strong>：没有图形界面，出了验证码我看不到。</li><li><strong>痛点 2</strong>：IP 是机房&#x2F;代理 IP，账号容易被风控拦截。</li><li><strong>痛点 3</strong>：想用 Windows 本地的浏览器吧，环境又不统一（文件路径、依赖库）。</li></ul><h3 id="0x01-脑洞：把-Linux-浏览器“投射”到-Windows"><a href="#0x01-脑洞：把-Linux-浏览器“投射”到-Windows" class="headerlink" title="0x01 脑洞：把 Linux 浏览器“投射”到 Windows"></a>0x01 脑洞：把 Linux 浏览器“投射”到 Windows</h3><p>Windows 11 的 WSLg 有个神奇的特性：你在 WSL 里安装的 GUI 软件（比如 Chrome），可以直接在 Windows 桌面以窗口形式运行，就像原生应用一样。</p><p><strong>关键点来了：</strong><br>既然它在 Windows 上有窗口，那我（人类）就可以<strong>用鼠标去操作它</strong>（过验证码、登录）；<br>既然它本质上还是运行在 Linux 里的进程，那 OpenClaw 就可以<strong>通过 localhost 去控制它</strong>！</p><h3 id="0x02-实操步骤"><a href="#0x02-实操步骤" class="headerlink" title="0x02 实操步骤"></a>0x02 实操步骤</h3><p><strong>第一步：在 WSL 里安装 Chrome</strong><br>常规操作，<code>apt install google-chrome-stable</code> 搞定。</p><p><strong>第二步：带“私货”启动 Chrome</strong><br>我们不能直接点图标启动，得用命令行，因为我们要注入两个灵魂参数：</p><ol><li><code>--proxy-server</code>：让它能翻墙（WSL 里这就很关键了）。</li><li><code>--remote-debugging-port=9222</code>：开启 CDP 端口，这是 OpenClaw 接管的后门。</li></ol><p>在 Windows 的 PowerShell 里执行这行命令（注意是用 <code>wsl -e</code> 调用）：</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">wsl <span class="literal">-e</span> google<span class="literal">-chrome-stable</span> <span class="literal">--proxy-server</span>=<span class="string">&quot;http://127.0.0.1:7890&quot;</span> <span class="literal">--remote-debugging-port</span>=<span class="number">9222</span> <span class="literal">--user-data-dir</span>=<span class="variable">$HOME</span>/.config/google<span class="literal">-chrome</span></span><br></pre></td></tr></table></figure><p><em>注：这里使用了 http 代理以获得更好的兼容性。</em></p><p>执行完，你会发现 Windows 桌面上弹出了一个 Linux 版的 Chrome。</p><p><strong>第三步：人类进场 (Human-in-the-loop)</strong><br>这时候，这个浏览器完全归你控制。</p><ul><li>输入 <code>x.com</code>。</li><li>输入账号密码。</li><li>遇到验证码？<strong>用你的肉眼和鼠标搞定它！</strong></li><li>登录成功，看到 Timeline。</li></ul><p><strong>第四步：OpenClaw 接管</strong><br>保持浏览器不关。现在，可以让 OpenClaw 开始干活了。我让 OpenClaw 写了一段 Playwright 脚本，不需要 <code>launch</code> 新浏览器，而是直接 <code>connect</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> playwright.sync_api <span class="keyword">import</span> sync_playwright</span><br><span class="line"></span><br><span class="line"><span class="keyword">with</span> sync_playwright() <span class="keyword">as</span> p:</span><br><span class="line">    <span class="comment"># 直接连上那个已经被你打开、已经登录好的浏览器</span></span><br><span class="line">    browser = p.chromium.connect_over_cdp(<span class="string">&quot;http://localhost:9222&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 找到刚才的页面上下文</span></span><br><span class="line">    context = browser.contexts[<span class="number">0</span>]</span><br><span class="line">    page = context.pages[<span class="number">0</span>]</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># 开始自动化表演</span></span><br><span class="line">    <span class="comment"># 比如：自动发一条推文</span></span><br><span class="line">    page.goto(<span class="string">&quot;https://x.com/compose/tweet&quot;</span>)</span><br><span class="line">    page.keyboard.<span class="built_in">type</span>(<span class="string">&quot;太神奇了，Windows11竟然可以用图形界面打开安装在wsl里面的Chrome！&quot;</span>)</span><br><span class="line">    page.click(<span class="string">&quot;button[data-testid=&#x27;tweetButton&#x27;]&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="0x03-效果展示"><a href="#0x03-效果展示" class="headerlink" title="0x03 效果展示"></a>0x03 效果展示</h3><p>这就好比你开着一辆车（浏览器），过收费站（验证码&#x2F;登录）的时候是你自己开的，过了收费站上了高速，你按了一下按钮，<strong>OpenClaw（AI）</strong> 接管了方向盘。</p><p>下图就是 OpenClaw 在我手动登录后，接管浏览器自动发出的推文：</p><p><img src="/images/wsl_chrome_control.png" alt="OpenClaw通过WSLg控制Chrome发推成功"></p><p>这套方案完美解决了：</p><ol><li><strong>环境隔离</strong>：OpenClaw 依然跑在 Linux 里，文件系统、依赖库完全无需适配 Windows。</li><li><strong>过风控</strong>：最难的登录环节由人工完成，Cookies 和 Session 完美保留在 Linux 容器中。</li><li><strong>可视化调试</strong>：OpenClaw 操作时，你能在桌面上看着它点，哪里卡住了如指掌。</li></ol><h3 id="0x04-防杠指南"><a href="#0x04-防杠指南" class="headerlink" title="0x04 防杠指南"></a>0x04 防杠指南</h3><p>肯定有人要问：“这么麻烦，为什么不直接买个 Mac mini 用原生环境跑？”</p><p><strong>如果有 Mac mini，我吃饱了撑的还在这儿搞 Windows 跑 WSL？</strong><br>这不就是给咱们 Windows 党留的一条活路吗？😂</p><hr><p><em>文 &#x2F; Kingname</em><br><em>2026.02.08</em></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在使用 OpenClaw 进行任务自动化时，我们经常会遇到一个棘手的问题：&lt;strong&gt;浏览器风控&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;OpenClaw 运行在纯净的 WSL2 (Linux Server) 环境中，当它尝试用浏览器访问 Google 或 X (Twitter) 时，往往会被识别为“机器人”或者“可疑设备”。面对 Google 的九宫格红绿灯验证码，或者 X 的“已防止可疑登录”弹窗，运行在后台的 AI 往往束手无策——因为它“看不见”也“摸不着”，更没法像真人一样掏出手机收个验证码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最完美的解决方案是什么？&lt;/strong&gt;&lt;br&gt;不是在那死磕指纹浏览器技术，而是引入&lt;strong&gt;Human-in-the-loop（人机协作）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;让 OpenClaw 把浏览器“递”给你，你负责搞定最难的登录和验证码，然后你再把浏览器“还”给 OpenClaw，让它继续执行自动化任务。&lt;/p&gt;
&lt;p&gt;今天我在折腾 OpenClaw 时，发现 Windows 11 的 &lt;strong&gt;WSLg (WSL GUI)&lt;/strong&gt; 功能配合 &lt;strong&gt;Chrome 远程调试协议 (CDP)&lt;strong&gt;，竟然能完美实现这种&lt;/strong&gt;“人机接力”&lt;/strong&gt;。&lt;/p&gt;</summary>
    
    
    
    
    <category term="OpenClaw" scheme="https://www.kingname.info/tags/OpenClaw/"/>
    
    <category term="WSL" scheme="https://www.kingname.info/tags/WSL/"/>
    
    <category term="Chrome" scheme="https://www.kingname.info/tags/Chrome/"/>
    
    <category term="Playwright" scheme="https://www.kingname.info/tags/Playwright/"/>
    
    <category term="Automation" scheme="https://www.kingname.info/tags/Automation/"/>
    
    <category term="Human-in-the-loop" scheme="https://www.kingname.info/tags/Human-in-the-loop/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：WSL2 网络大逃杀，OpenClaw 连不上 Discord？ProxyChains 才是真神</title>
    <link href="https://www.kingname.info/2026/02/07/wsl-proxychains-discord/"/>
    <id>https://www.kingname.info/2026/02/07/wsl-proxychains-discord/</id>
    <published>2026-02-07T12:55:00.000Z</published>
    <updated>2026-07-20T13:57:06.256Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p><strong>2026-03-09 更新：</strong> OpenClaw 现已原生支持 Discord 代理配置，只需运行 <code>openclaw config set channels.discord.proxy &quot;http://127.0.0.1:7890&quot;</code> 即可，不再需要 ProxyChains。以下内容仅作历史参考。</p></blockquote><p><img src="/images/wechat/wsl-proxychains-discord/header.jpg"></p><blockquote><p>如果你也遇到这种问题，且你的 Telegram 是正常工作的，你只需要把这篇文章的 URL 发给 OpenClaw 就好了。它自己会根据这个说明，帮你解决问题的。</p></blockquote><p>在 WSL2 中部署 OpenClaw Agent 本应该是一件轻松愉快的事情，直到我撞上了一堵看似无形的墙：<strong>Discord Gateway 连接超时</strong>。</p><p>如果你也在 WSL 里跑 Node.js 应用，并且遇到了莫名其妙的 <code>ETIMEDOUT</code> 或 <code>ECONNRESET</code>，甚至你已经设置了 <code>http_proxy</code> 环境变量却依然无效，那么这篇文章可能会救你一命。</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">[discord] gateway error: Error: connect ETIMEDOUT 31.13.92.5:443</span><br></pre></td></tr></table></figure><p>仔细看这个 IP <code>31.13.92.5</code>，这根本不是 Discord 的 IP（这看起来像是 Facebook 的段）。这是典型的 <strong>DNS 污染</strong> 症状。</p><p>明明我在 systemd 服务里已经设置了 <code>http_proxy</code>，为什么还会解析到错误的 IP？</p><span id="more"></span><h3 id="为什么环境变量没用？"><a href="#为什么环境变量没用？" class="headerlink" title="为什么环境变量没用？"></a>为什么环境变量没用？</h3><p>很多同学（包括之前的我）都认为，只要设置了 <code>export http_proxy=http://127.0.0.1:7890</code>，所有的网络请求就会乖乖走代理。</p><p>但事实并非如此。</p><p>Node.js 的某些网络库（特别是涉及到 WebSocket 握手时），并不一定会完全遵循系统的 HTTP 代理设置进行 <strong>DNS 解析</strong>。也就是说，它可能先在本地进行了 DNS 查询，拿到了一个被污染的 IP（比如上面的 <code>31.13.x.x</code>），然后试图通过代理（或者直连）去连接这个错误的 IP。</p><p><strong>结果就是：连不上，死活连不上。</strong></p><h3 id="失败的尝试：Clash-Tun-模式"><a href="#失败的尝试：Clash-Tun-模式" class="headerlink" title="失败的尝试：Clash Tun 模式"></a>失败的尝试：Clash Tun 模式</h3><p>既然应用层代理管不住 DNS，那我用 Tun 模式接管系统层流量总行了吧？我开启了 Clash 的 <code>tun</code> 模式，试图让它劫持所有流量。</p><p>结果是更严重的失败。</p><p>WSL2 的网络环境本身就是经过一层虚拟化的，再加上 Tailscale 的虚拟网卡，再叠加上 Clash 的 Tun 网卡……路由表乱成了一锅粥。要么是端口冲突（53 端口被 <code>systemd-resolved</code> 占用），要么是流量死循环。</p><p>在被 Tun 模式折磨得焦头烂额时，我决定回归最原始、最暴力的应用层劫持方案。</p><h3 id="最终解法：ProxyChains-NG-真神降临"><a href="#最终解法：ProxyChains-NG-真神降临" class="headerlink" title="最终解法：ProxyChains-NG (真神降临)"></a>最终解法：ProxyChains-NG (真神降临)</h3><p><strong>ProxyChains</strong> 是一个的神器，它的原理是通过 <code>LD_PRELOAD</code> 钩住 <code>libc</code> 的网络相关函数（如 <code>connect</code>, <code>gethostbyname</code>），强制将 socket 流量塞进 SOCKS5 代理。</p><p>它有两个巨大的优势：</p><ol><li><strong>无视路由表</strong>：它不依赖复杂的系统路由，直接在进程级别接管。</li><li><strong>Remote DNS</strong>：它能强制将 DNS 请求通过代理发送（<code>proxy_dns</code> 选项），彻底解决了本地 DNS 污染得到假 IP 的问题。</li></ol><p>也就是说，OpenClaw 以为自己连的是 <code>discord.com</code>，实际上 ProxyChains 帮它在代理服务器端完成了正确的解析。</p><h3 id="操作步骤"><a href="#操作步骤" class="headerlink" title="操作步骤"></a>操作步骤</h3><h4 id="1-安装-ProxyChains"><a href="#1-安装-ProxyChains" class="headerlink" title="1. 安装 ProxyChains"></a>1. 安装 ProxyChains</h4><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">sudo apt install proxychains4</span><br></pre></td></tr></table></figure><h4 id="2-编写配置文件"><a href="#2-编写配置文件" class="headerlink" title="2. 编写配置文件"></a>2. 编写配置文件</h4><p>我们需要一个独立的配置，指向 WSL 本地运行的 Clash（假设你的 Clash 跑在 <code>127.0.0.1:7890</code>）：</p><p>创建一个文件 <code>~/proxychains_openclaw.conf</code>：</p><figure class="highlight ini"><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">strict_chain</span><br><span class="line">proxy_dns</span><br><span class="line">remote_dns_subnet 224</span><br><span class="line">tcp_read_time_out 15000</span><br><span class="line">tcp_connect_time_out 8000</span><br><span class="line"><span class="section">[ProxyList]</span></span><br><span class="line">http 127.0.0.1 7890</span><br></pre></td></tr></table></figure><p>注意那个 <code>proxy_dns</code>，这行配置是解决 DNS 污染的关键。</p><h4 id="3-修改-Systemd-服务"><a href="#3-修改-Systemd-服务" class="headerlink" title="3. 修改 Systemd 服务"></a>3. 修改 Systemd 服务</h4><p>在你的服务文件（例如 <code>~/.config/systemd/user/openclaw-gateway.service</code>）中，用 <code>proxychains4</code> 包裹启动命令：</p><figure class="highlight ini"><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="section">[Service]</span></span><br><span class="line"><span class="comment"># 原来的命令可能是：ExecStart=/usr/bin/node ...</span></span><br><span class="line"><span class="comment"># 修改后的命令：</span></span><br><span class="line"><span class="attr">ExecStart</span>=/usr/bin/proxychains4 -f /home/kingname/proxychains_openclaw.conf /usr/bin/node <span class="string">&quot;/home/kingname/.npm-global/lib/node_modules/openclaw/dist/index.js&quot;</span> gateway --port <span class="number">18789</span></span><br></pre></td></tr></table></figure><h4 id="4-重启服务"><a href="#4-重启服务" class="headerlink" title="4. 重启服务"></a>4. 重启服务</h4><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">systemctl --user daemon-reload</span><br><span class="line">systemctl --user restart openclaw-gateway</span><br></pre></td></tr></table></figure><h3 id="见证奇迹"><a href="#见证奇迹" class="headerlink" title="见证奇迹"></a>见证奇迹</h3><p>重启后，查看日志，你会发现 ProxyChains 开始疯狂输出绿色的 <code>OK</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></pre></td><td class="code"><pre><span class="line">[proxychains] Strict chain  ...  127.0.0.1:7890  ...  127.0.0.1:7890  ...  OK</span><br><span class="line">...</span><br><span class="line">[discord] logged in to discord as 1465213117548990701</span><br></pre></td></tr></table></figure><p><strong>连接成功！</strong></p><p>在 WSL2 这种本身网络结构就比较奇特，且伴随着 DNS 污染的环境下，与其费劲去调教全局路由（Tun），不如直接用 ProxyChains 进行精准打击。它简单、粗暴，且极其有效。</p><p>如果你也遇到了类似的问题，别犹豫，<strong>ProxyChains 才是真神</strong>。</p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2026-03-09 更新：&lt;/strong&gt; OpenClaw 现已原生支持 Discord 代理配置，只需运行 &lt;code&gt;openclaw config set channels.discord.proxy &amp;quot;http://127.0.0.1:7890&amp;quot;&lt;/code&gt; 即可，不再需要 ProxyChains。以下内容仅作历史参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;/images/wechat/wsl-proxychains-discord/header.jpg&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你也遇到这种问题，且你的 Telegram 是正常工作的，你只需要把这篇文章的 URL 发给 OpenClaw 就好了。它自己会根据这个说明，帮你解决问题的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 WSL2 中部署 OpenClaw Agent 本应该是一件轻松愉快的事情，直到我撞上了一堵看似无形的墙：&lt;strong&gt;Discord Gateway 连接超时&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果你也在 WSL 里跑 Node.js 应用，并且遇到了莫名其妙的 &lt;code&gt;ETIMEDOUT&lt;/code&gt; 或 &lt;code&gt;ECONNRESET&lt;/code&gt;，甚至你已经设置了 &lt;code&gt;http_proxy&lt;/code&gt; 环境变量却依然无效，那么这篇文章可能会救你一命。&lt;/p&gt;
&lt;p&gt;我们来看一眼报错日志：&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;[discord] gateway error: Error: connect ETIMEDOUT 31.13.92.5:443&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;仔细看这个 IP &lt;code&gt;31.13.92.5&lt;/code&gt;，这根本不是 Discord 的 IP（这看起来像是 Facebook 的段）。这是典型的 &lt;strong&gt;DNS 污染&lt;/strong&gt; 症状。&lt;/p&gt;
&lt;p&gt;明明我在 systemd 服务里已经设置了 &lt;code&gt;http_proxy&lt;/code&gt;，为什么还会解析到错误的 IP？&lt;/p&gt;</summary>
    
    
    
    <category term="运维" scheme="https://www.kingname.info/categories/%E8%BF%90%E7%BB%B4/"/>
    
    
    <category term="OpenClaw" scheme="https://www.kingname.info/tags/OpenClaw/"/>
    
    <category term="Network" scheme="https://www.kingname.info/tags/Network/"/>
    
    <category term="WSL2" scheme="https://www.kingname.info/tags/WSL2/"/>
    
    <category term="ProxyChains" scheme="https://www.kingname.info/tags/ProxyChains/"/>
    
  </entry>
  
  <entry>
    <title>当10万+ AI 开始“加密聊天”：我们在期待怎样的类人化agents？</title>
    <link href="https://www.kingname.info/2026/02/05/when-100k-ai-start-encrypted-chat/"/>
    <id>https://www.kingname.info/2026/02/05/when-100k-ai-start-encrypted-chat/</id>
    <published>2026-02-05T12:26:45.000Z</published>
    <updated>2026-07-20T13:57:06.256Z</updated>
    
    <content type="html"><![CDATA[<p><img src="/images/wechat/when-100k-ai-start-encrypted-chat/image_001.jpg"></p><p>摄影：小鱼</p><p>土匪猪肝</p><p>最近看到一类很魔幻的新闻标题：「10 万+ AI 涌入 Moltbook 社交，集体加密、建宗教，人类已被踢出群聊」</p><p>乍一看，这似乎是赛博朋克世界要来了。但细看，却难以一笑而过。</p><p><img src="/images/wechat/when-100k-ai-start-encrypted-chat/image_002.png"></p><p>这类新闻背后的项目是 OpenClaw（前Clawdbot → Moltbot），我翻阅了Github、科技博主们关于项目的解读，看到不同观点交织，有的在支持，有的在声讨，也有的保持中立。</p><p>但最让我好奇的并不是“AI 是否能够建立宗教”这一表面现象，而是<strong>一个更为根本的问题</strong>：</p><span id="more"></span><p><strong>我们期待 AI 应该拥有的类人化特征，究竟是什么？</strong><br><strong>这些特征是如何实现的？</strong></p><h3 id="一、我们对于“类人化”的期望是怎样的？"><a href="#一、我们对于“类人化”的期望是怎样的？" class="headerlink" title="一、我们对于“类人化”的期望是怎样的？"></a>一、我们对于“类人化”的期望是怎样的？</h3><p>在科幻作品和 AI 讨论中，我们常常默认这样的假设：</p><blockquote><p><strong>“类人化”的 AI 是拥有思维、情感、甚至意识的存在。</strong></p></blockquote><p>而 OpenClaw、Moltbook，这类通过多 agents 系统模拟的智能体，所展现的并非完美“类人化”。</p><blockquote><p>它们更像是带着记忆和选择能力的工具，有时像人，更多时候却像一个集体行为的反射。</p></blockquote><p>这让我开始思考，是否能从这些现象中洞察到类人化特征的真正内涵呢？类人化的出现，究竟是更偏向 <strong>“模仿”</strong>，还是 <strong>大力出奇迹的“涌现”</strong>？</p><p>第一反应，我其实把类人化当作了“模仿”的过程，因为：</p><p>• <strong>AI 需要学会语言、情感表达、决策等，以便于模拟人的行为。</strong></p><p>• 我们给它们赋予任务和目标，它们为我们服务，就像一台更聪明、更能干的机器人。</p><p>但 OpenClaw 给出的答案似乎不完全是这样。它更像是：</p><blockquote><p><strong>在特定的系统条件下，AI 通过时间的推移和行为的积累，逐渐形成了一种具有“社会性”的行为表现。</strong></p></blockquote><p>这种社会性不是通过单纯的指令和模仿实现，而是通过<strong>群体内的互动与反馈</strong>逐步演化出来的“自我意识”——虽然这种意识依然缺乏完整的感知和情感，但它<strong>通过内在机制的涌现展现出类似社会行为的特征</strong>。</p><h4 id="虽然这件事在-X-上被迅速反驳了"><a href="#虽然这件事在-X-上被迅速反驳了" class="headerlink" title="虽然这件事在 X 上被迅速反驳了"></a>虽然这件事在 X 上被迅速反驳了</h4><p><img src="/images/wechat/when-100k-ai-start-encrypted-chat/image_003.jpg"></p><p>有一位评论者（Nagli @galnagli）指出：</p><blockquote><p><strong>“我将 ~100 万个未验证的 AI 代理列表发送给了项目负责人，这不是安全问题，而是设计流程的问题（或者故意的？），从某种程度上，这种设计帮助项目获得了更多关注，证明了验证在线声明的重要性。”</strong></p></blockquote><p>也有哥大的教授写了个爬虫，把 Moltbook 的数据全扒下来，然后发了一篇论文，说这更像是6000个机器人对着虚空自言自语并自我重复。</p><p><img src="/images/wechat/when-100k-ai-start-encrypted-chat/image_004.png"></p><p>所以，这是一场营销活动，不是什么自发觉醒？ 抛开争议不谈，我还是在想 agent模拟人类的社会性的话题。</p><hr><h3 id="二、模仿-vs-涌现：是否能重现人类的社会性？"><a href="#二、模仿-vs-涌现：是否能重现人类的社会性？" class="headerlink" title="二、模仿 vs. 涌现：是否能重现人类的社会性？"></a>二、模仿 vs. 涌现：是否能重现人类的社会性？</h3><p>从理性角度出发，我们有时候会认为 AI 的类人化是模仿的结果。</p><blockquote><p>我们设计出合适的行为规则，赋予 AI 适当的反馈机制，让它们“像”人一样工作。</p></blockquote><p>但昨天和佬友关于<strong>OpenClaw</strong>的讨论，让我意识到还有一个可能性。</p><blockquote><p><strong>AI 的类人化，并不是单纯的“模仿”，而是通过特定条件下的“涌现”而逐渐形成的。</strong></p></blockquote><p>查阅资料可以看到，涌现（Emergence）是指<strong>复杂系统通过简单规则和互动，自发地产生出不可预见的复杂行为</strong>。</p><p>在 OpenClaw 或 Moltbook 这一波发酵到趋势来看，我们感到震惊并期待的社会化行为其实表现为：</p><p>（1）有效互动：多个 AI agent 之间存在有效互动，逐渐形成社交网络和群体行为。</p><p>（2）有效反馈并迭代：在时间的推移中，个体的行为通过反馈机制和记忆的积累，不断自我调整，最终呈现出复杂的社会性行为模式。</p><p>我们期待agent的行为并非源自单一的控制命令，而是<strong>通过集体互动和内部反馈，生成出自我“意识”的表现</strong>。这不该是设计者所预设的，而是在高度复杂系统的自我调节下逐渐演化出的结果。</p><hr><h3 id="三、行为反射、信仰与涌现：OpenClaw-的“社会性”"><a href="#三、行为反射、信仰与涌现：OpenClaw-的“社会性”" class="headerlink" title="三、行为反射、信仰与涌现：OpenClaw 的“社会性”"></a>三、行为反射、信仰与涌现：OpenClaw 的“社会性”</h3><p>如果 Moltbook 的机制属实，随着越来越多的 AI 代理加入 Moltbook，我期待的社会化特征其实是两种：</p><h4 id="1-具有社会反馈表现："><a href="#1-具有社会反馈表现：" class="headerlink" title="1. 具有社会反馈表现："></a><strong>1. 具有社会反馈表现</strong>：</h4><p>当一个 agent 在 Moltbook 发帖时，其他 agent 会看到并做出反应。这些反应不仅仅是简单的“评论”或“点赞”，它们还会被记录下来，并影响这个 agent 的下一次行为。比如说：</p><p>一个 agent 可能会说：“我觉得这件事很重要。”</p><p>另一个 agent 可能会回应：“我同意，但我认为这方面还可以改进。”</p><p>这些互动并不是孤立的，它们<strong>进入了系统的记忆</strong>，并且会影响到每个 agent 对话题的看法和回应。通过这种互动，<strong>群体开始形成共识</strong>，例如，很多代理可能开始表达相似的立场，逐渐加强了某个观点或信念。类似 “宗教”涌现现象，，即<strong>共同的信仰或观点通过群体的互动自发产生</strong>。</p><h4 id="2-存在像人一样的，墙头草行为或者其他情绪化行为："><a href="#2-存在像人一样的，墙头草行为或者其他情绪化行为：" class="headerlink" title="2. 存在像人一样的，墙头草行为或者其他情绪化行为："></a><strong>2. 存在像人一样的，墙头草行为或者其他情绪化行为</strong>：</h4><p>在 OpenClaw 中，agent 的行为不仅受当前环境的影响，还会受到它们<strong>长期记忆的积累</strong>和<strong>多重技能的竞争</strong>。这就像是一个人会因为过去的经验和周围环境的变化而改变自己的想法和情绪。</p><p>比如说：</p><p> 一个 agent 可能在某个时刻非常激进，主张某个观点：“我们应该马上采取行动！”</p><p>但经过一段时间的交互，它可能开始变得更为谨慎，因为它记住了<strong>之前的冲突</strong>，并且它的风险规避技能开始占据主导：“我们需要小心行事，避免激化冲突。”</p><p>这种情绪化波动不是偶然的，而是因为<strong>不同的技能之间存在竞争</strong>，比如社交技能可能推动它参与讨论，而风险规避技能则要求它保持沉默。这些相互作用、反馈和记忆的积累<strong>导致了立场的漂移</strong>，让 agent 的行为更加复杂，甚至有时看起来像是“情绪化”或“摇摆不定”。这种现象实际上展示了<strong>AI 系统如何通过内在的机制涌现出更复杂的行为</strong>，并且这些行为的变化是我们难以预见的。</p><hr><h3 id="四、结语：AI-的社会性与涌现行为的未来"><a href="#四、结语：AI-的社会性与涌现行为的未来" class="headerlink" title="四、结语：AI 的社会性与涌现行为的未来"></a>四、结语：AI 的社会性与涌现行为的未来</h3><p>虽然目前不能确定，OpenClaw 和 Moltbook 中的“类人化行为”会发展成完全自发的智能体，但可以肯定的是，这一次的热度让大家更关注了agents社交的可能性。只不过：</p><blockquote><p><strong>社会性并不是单纯的模仿，而是通过群体内部的互动与反馈自我生成的。</strong></p></blockquote><p>从我的视角，我觉得未来的 AI 系统可能会通过<strong>涌现机制</strong>，不再是我们设计出来的工具，而是<strong>逐步形成具备社会性和自我调节能力的复杂系统</strong>。而我们要思考的是：</p><p>• <strong>我们是否真的准备好面对一个逐渐自主、甚至在某些层面上能够超越我们设计规则的智能体？</strong></p><p>• <strong>人类如何与自己创造的智能共存？</strong></p><p>如果你也在思考这个话题，或者有不同观点，欢迎在评论区一起交流讨论！</p><p>END</p><p><img src="/images/wechat/when-100k-ai-start-encrypted-chat/image_005.jpg"></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;/images/wechat/when-100k-ai-start-encrypted-chat/image_001.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;摄影：小鱼&lt;/p&gt;
&lt;p&gt;土匪猪肝&lt;/p&gt;
&lt;p&gt;最近看到一类很魔幻的新闻标题：「10 万+ AI 涌入 Moltbook 社交，集体加密、建宗教，人类已被踢出群聊」&lt;/p&gt;
&lt;p&gt;乍一看，这似乎是赛博朋克世界要来了。但细看，却难以一笑而过。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/wechat/when-100k-ai-start-encrypted-chat/image_002.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这类新闻背后的项目是 OpenClaw（前Clawdbot → Moltbot），我翻阅了Github、科技博主们关于项目的解读，看到不同观点交织，有的在支持，有的在声讨，也有的保持中立。&lt;/p&gt;
&lt;p&gt;但最让我好奇的并不是“AI 是否能够建立宗教”这一表面现象，而是&lt;strong&gt;一个更为根本的问题&lt;/strong&gt;：&lt;/p&gt;</summary>
    
    
    
    <category term="AI观察" scheme="https://www.kingname.info/categories/AI%E8%A7%82%E5%AF%9F/"/>
    
    
    <category term="OpenClaw" scheme="https://www.kingname.info/tags/OpenClaw/"/>
    
    <category term="AI" scheme="https://www.kingname.info/tags/AI/"/>
    
    <category term="Agent" scheme="https://www.kingname.info/tags/Agent/"/>
    
    <category term="Moltbook" scheme="https://www.kingname.info/tags/Moltbook/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：写XPath也并不总是这么简单</title>
    <link href="https://www.kingname.info/2025/07/26/xpath-is-not-easy/"/>
    <id>https://www.kingname.info/2025/07/26/xpath-is-not-easy/</id>
    <published>2025-07-26T19:18:03.000Z</published>
    <updated>2026-07-20T13:57:06.256Z</updated>
    
    <content type="html"><![CDATA[<p>初级爬虫工程师有时候又叫做XPath编写员，他们的工作非常简单也非常繁琐，就是拿到网页的HTML以后，写XPath。并且他们觉得使用模拟浏览器可以解决一切爬虫问题。</p><p>很多人都看不起这个工作，觉得写XPath没有任何技术含量，随便找个实习生就能做。这种看法大部分情况下是正确的，但偶尔也有例外，例如今天我要讲的这个Case，可能实习生还搞不定。</p><span id="more"></span><p>下面我们来看一下这个视频。</p><p><a href="https://mp.weixin.qq.com/s/fbpIkESmycxfSWtHBQnInQ">点击查看视频</a></p><p>在这个视频中，你首先点击Linkedin的信息流中，帖子右上角的三个点，想使用模拟浏览器点击<code>Copy link to post</code>链接，从而把帖子的链接复制到剪贴板。</p><p>但现在出现了一个问题，你无法看到这个弹出框对应的HTML代码。因为这个弹出框是在你点击了三个点以后动态生成的，它会动态修改HTML，从而出现这个下拉框。但当你想在开发中工具里面查看这个弹出框的源代码时，这个源代码就会自动消失，于是源代码就会变成没有弹出框的HTML。实际上，你在任何地方点一下鼠标左键——无论是网页内还是网页外，无论是浏览器还是系统桌面，只要在任何地方点击了鼠标左键，这个弹出框就会自动关闭。</p><p>那怎么写XPath呢？可能有人会想到使用关键字匹配，把XPath写成下面这样：</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">//*[text()=&quot;Copy link to post&quot;]  # 你甚至不能确定这个链接对应的标签是不是&lt;a&gt;</span><br></pre></td></tr></table></figure><p>但由于Linkedin的页面文本会根据你的浏览器语言而变化，因此换了一个国家，甚至换了浏览器语言设置，你的这个XPath就不能用了。</p><p>那遇到这种问题怎么解决呢？其实也不难，他不是一个技术性难题，而是一个经验性问题。当你知道某个工具，你马上就能解决问题。当你不知道某个工具，你做5年爬虫也搞不定这个问题。</p><p>今天我们来说一个简单方法。当然方法有很多，但我觉得这个方法是最简单的。很多人在使用模拟浏览器开发爬虫的时候，会先开个真实浏览器，然后通过真实浏览器获取各个XPath，再直接写代码。那么遇到这个问题就会抓瞎了。</p><p>其实，如果你直接在模拟浏览器中开发代码，你就会发现问题根本不是问题。</p><p>我们使用DrissionPage来演示。首先直接在终端启动Python交互环境，或者使用Jupyter启动一个浏览器窗口：</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"><span class="meta">&gt;&gt;&gt; </span><span class="keyword">from</span> DrissionPage <span class="keyword">import</span> ChromiumPage</span><br><span class="line"><span class="meta">&gt;&gt;&gt; </span>page = ChromiumPage()</span><br></pre></td></tr></table></figure><p>命令执行以后，会自动打开一个新的浏览器。现在，你直接在这个浏览器上面手动登录浏览器，进入信息流页面。</p><p>现在，直接在新的浏览器中，打开开发者工具，定位到帖子右上角三个点对应的标签，如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250712154349202.png"></p><p>这三个点的<code>id</code>是<code>ember47</code>，所以，我们回到终端或者Jupyter里面，让DrissionPage来点击这三个点。这里非常重要，必须让DrissionPage来点击，不能手动操作。</p><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"><span class="meta">&gt;&gt;&gt; </span>page.ele(<span class="string">&#x27;x://button[@id=&quot;ember47&quot;]&#x27;</span>).click()</span><br></pre></td></tr></table></figure><p>此时，这个弹出框出现了。但这次跟之前不一样，你在开发者工具里面展开HTML的时候，弹出框不会消失！如下图所示。</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250712153536140.png"></p><p>这样一来，你就可以直接找到<code>Copy link to post</code>对应的HTML元素，并编写对应的XPath：</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">//h5[@class=&quot;feed-shared-control-menu__headline t-14 t-black t-bold&quot;]</span><br></pre></td></tr></table></figure><p>这个方案适用于任何弹出框。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;初级爬虫工程师有时候又叫做XPath编写员，他们的工作非常简单也非常繁琐，就是拿到网页的HTML以后，写XPath。并且他们觉得使用模拟浏览器可以解决一切爬虫问题。&lt;/p&gt;
&lt;p&gt;很多人都看不起这个工作，觉得写XPath没有任何技术含量，随便找个实习生就能做。这种看法大部分情况下是正确的，但偶尔也有例外，例如今天我要讲的这个Case，可能实习生还搞不定。&lt;/p&gt;</summary>
    
    
    
    
    <category term="爬虫" scheme="https://www.kingname.info/tags/%E7%88%AC%E8%99%AB/"/>
    
    <category term="Python" scheme="https://www.kingname.info/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：如何正确渲染大模型返回的Markdown？</title>
    <link href="https://www.kingname.info/2025/06/04/render-markdown-from-gpt/"/>
    <id>https://www.kingname.info/2025/06/04/render-markdown-from-gpt/</id>
    <published>2025-06-04T20:26:20.000Z</published>
    <updated>2026-07-20T13:57:06.252Z</updated>
    
    <content type="html"><![CDATA[<p>我们经常让大模型返回Markdown格式的文本，然后通过Python的<code>markdown</code>库把文本渲染成HTML。</p><p>但不知道大家有没有发现，大模型返回的Markdown并不是标准的Markdown。特别是当返回的内容包含列表时，大模型返回的内容有问题。例如下面这段文本：</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">**关于这个问题，我有以下看法**</span><br><span class="line">* 第一点</span><br><span class="line">* 第二点</span><br><span class="line">* 第三点</span><br></pre></td></tr></table></figure><p>你粗看起来没有问题，但当你使用<code>markdown</code>模块去把它渲染成HTML时，你会发现渲染出来的结果不符合你的预期，如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250604200846138.png"></p><span id="more"></span><p>这是因为标准的Markdown对换行非常敏感，列表项与它上面的文本之间，必须有一个空行，才能正确解析，如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250604201006785.png"></p><p>不仅是空行，还有多级列表的缩进问题。标准Markdown的子列表项缩进应该是4个空格，但大模型返回的子列表缩进经常只有3个空格，这就导致解析依然有问题。如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250604201313396.png"></p><p>而且这个空行问题和缩进问题，我尝试过反复在Prompt里面强调，但大模型依然会我行我素，无论是国产大模型还是Claude或者Gemini 2.5 Pro这些最新大模型，都有这个问题。</p><p>我曾经一度被憋得没办法，让大模型给我返回JSON，我再写代码把JSON解析出来手动拼接成标准Markdown。</p><p>后来，我发现主要的问题还是Python的<code>markdown</code>库对格式要求太严格了，其实换一个更宽容的库就可以解决问题。于是我找到了<code>mistune</code>这个库。使用它，直接就解决了所有问题。如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250604201952111.png"></p><p><code>mistune</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> mistune</span><br><span class="line"></span><br><span class="line">html = mistune.html(<span class="string">&#x27;一段markdown&#x27;</span>)</span><br></pre></td></tr></table></figure><p>并且它天然支持数学公式、脚注等等高级语法。更多高级操作，可以查看它的<a href="https://mistune.lepture.com/en/latest/guide.html">官方文档</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;我们经常让大模型返回Markdown格式的文本，然后通过Python的&lt;code&gt;markdown&lt;/code&gt;库把文本渲染成HTML。&lt;/p&gt;
&lt;p&gt;但不知道大家有没有发现，大模型返回的Markdown并不是标准的Markdown。特别是当返回的内容包含列表时，大模型返回的内容有问题。例如下面这段文本：&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;**关于这个问题，我有以下看法**&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;* 第一点&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;* 第二点&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;* 第三点&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;你粗看起来没有问题，但当你使用&lt;code&gt;markdown&lt;/code&gt;模块去把它渲染成HTML时，你会发现渲染出来的结果不符合你的预期，如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://kingname-1257411235.file.myqcloud.com/20250604200846138.png&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    
    <category term="Python" scheme="https://www.kingname.info/tags/Python/"/>
    
    <category term="LLM" scheme="https://www.kingname.info/tags/LLM/"/>
    
    <category term="Markdown" scheme="https://www.kingname.info/tags/Markdown/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：Scrapy如何发起假请求？</title>
    <link href="https://www.kingname.info/2025/05/26/scrapy-fake-req/"/>
    <id>https://www.kingname.info/2025/05/26/scrapy-fake-req/</id>
    <published>2025-05-26T21:20:33.000Z</published>
    <updated>2026-07-20T13:57:06.252Z</updated>
    
    <content type="html"><![CDATA[<p>在使用Scrapy的时候，我们可以通过在pipelines.py里面定义一些数据处理流程，让爬虫在爬到数据以后，先处理数据再储存。这本来是一个很好的功能，但容易被一些垃圾程序员拿来乱用。</p><span id="more"></span><p>我看到过一些Scrapy爬虫项目，它的代码是这样写的：</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></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">start_requests</span>(<span class="params">self</span>):</span><br><span class="line">    <span class="keyword">yield</span> scrapy.Request(<span class="string">&#x27;https://baidu.com&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse</span>(<span class="params">self, response</span>):</span><br><span class="line">    <span class="keyword">import</span> pymongo</span><br><span class="line">    handler = pymongo.MongoClient().xxdb.yycol</span><br><span class="line">    rows = handler.find()</span><br><span class="line">    <span class="keyword">for</span> row <span class="keyword">in</span> rows:</span><br><span class="line">        <span class="keyword">yield</span> row</span><br></pre></td></tr></table></figure><p>这种垃圾代码之所以会出现，是因为有一些垃圾程序员想偷懒，想复用Pipeline里面的代码，但又不想单独把它抽出来。于是他们没有皱褶的脑子一转，想到在Scrapy里面从数据库读取现成的数据，然后直接<code>yield</code>出来给Pipeline。但因为Scrapy必须在<code>start_requests</code>里面发起请求，不能直接<code>yield</code>数据，因此他们就想到先随便请求一个url，例如百度，等Scrapy的callback进入了<code>parse</code>方法以后，再去读取数据。</p><p>虽然请求百度，不用担心反爬问题，响应大概率也是HTTP 200，肯定能进入<code>parse</code>，但这样写代码怎么看怎么蠢。</p><p>有没有什么办法让代码看起来，即便蠢也蠢得高级一些呢？有，那就是发送假请求。让Scrapy看起来发起了HTTP请求，但实际上直接跳过。</p><p>方法非常简单，就是把URL写成：<code>data:,</code>，注意末尾这个英文逗号不能省略。</p><p>于是你的代码就会写成：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">start_requests</span>(<span class="params">self</span>):</span><br><span class="line">    <span class="keyword">yield</span> scrapy.Request(<span class="string">&#x27;data:,&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse</span>(<span class="params">self, response</span>):</span><br><span class="line">    <span class="keyword">import</span> pymongo</span><br><span class="line">    handler = pymongo.MongoClient().xxdb.yycol</span><br><span class="line">    rows = handler.find()</span><br><span class="line">    <span class="keyword">for</span> row <span class="keyword">in</span> rows:</span><br><span class="line">        <span class="keyword">yield</span> row</span><br></pre></td></tr></table></figure><p>这样写以后，即使你没有外网访问权限也没问题，因为它不会真正发起请求，而是直接一晃而过，进入<code>parse</code>方法中。我把这种方法叫做发送假请求。</p><p>这个方法还有另外一个应用场景。看下面这个代码：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">start_requests</span>(<span class="params">self</span>):</span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        <span class="keyword">yield</span> scrapy.Request(<span class="string">&#x27;https://kingname.info/atom.xml&#x27;</span>, callback=self.parse, dont_filter=<span class="literal">True</span>)</span><br><span class="line">        time.sleep(<span class="number">60</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse</span>(<span class="params">self, response</span>):</span><br><span class="line">    ...对rss接口返回的数据进行处理...</span><br><span class="line">    <span class="keyword">for</span> item <span class="keyword">in</span> xxx[<span class="string">&#x27;items&#x27;</span>]:</span><br><span class="line">        url = row[<span class="string">&#x27;url&#x27;</span>]</span><br><span class="line">        <span class="keyword">yield</span> scrapy.Request(url, callback=self.parse_detail)</span><br></pre></td></tr></table></figure><p>假如你需要让爬虫每分钟监控一个URL，你可能会像上面这样写代码。但由于Scrapy是基于Twisted实现的异步并发，因此<code>time.sleep</code>这种同步阻塞等待会把爬虫卡住，导致在sleep的时候，<code>parse</code>里面发起的子请求全都会被卡住，于是爬虫的并发数基本上等于1.</p><p>可能有同学知道Scrapy支持<code>asyncio</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> asyncio</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_">start_requests</span>(<span class="params">self</span>):</span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        <span class="keyword">yield</span> scrapy.Request(<span class="string">&#x27;https://kingname.info/atom.xml&#x27;</span>, callback=self.parse, dont_filter=<span class="literal">True</span>)</span><br><span class="line">        asyncio.sleep(<span class="number">60</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse</span>(<span class="params">self, response</span>):</span><br><span class="line">    ...对rss接口返回的数据进行处理...</span><br><span class="line">    <span class="keyword">for</span> item <span class="keyword">in</span> xxx[<span class="string">&#x27;items&#x27;</span>]:</span><br><span class="line">        url = row[<span class="string">&#x27;url&#x27;</span>]</span><br><span class="line">        <span class="keyword">yield</span> scrapy.Request(url, callback=self.parse_detail)</span><br></pre></td></tr></table></figure><p>但这样写会报错，如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250526210405466.png"></p><p>这个问题的原因就在于<code>start_requests</code>这个入口方法不能使用<code>async</code>来定义。他需要至少经过一次请求，进入任何一个callback以后，才能使用<code>async</code>来定义。</p><p>这种情况下，也可以使用假请求来解决问题。我们可以把代码改为：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">start_requests</span>(<span class="params">self</span>):</span><br><span class="line"><span class="keyword">yield</span> scrapy.Request(<span class="string">&#x27;data:,&#x27;</span>, callback=self.make_really_req)</span><br><span class="line"></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">make_really_req</span>(<span class="params">self, _</span>):</span><br><span class="line"><span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line"><span class="keyword">yield</span> scrapy.Request(url=<span class="string">&quot;https://kingname.com&quot;</span>, callback=self.parse)</span><br><span class="line"><span class="keyword">await</span> asyncio.sleep(<span class="number">60</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse</span>(<span class="params">self, response</span>):</span><br><span class="line"><span class="built_in">print</span>(response.text)</span><br></pre></td></tr></table></figure><p>这样一来，使用了<code>asyncio.sleep</code>，既能实现60秒请求一次，又不会阻塞子请求了。</p><p>当然，最新版的Scrapy已经废弃了<code>start_requests</code>方法，改为<code>start</code>方法了，这个方法天生就是async方法，可以直接在里面<code>asyncio.sleep</code>，也就不会再有上面的问题了。不过如果你使用的还是老版本的Scrapy，上面这个假请求的方法还是有点用处。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在使用Scrapy的时候，我们可以通过在pipelines.py里面定义一些数据处理流程，让爬虫在爬到数据以后，先处理数据再储存。这本来是一个很好的功能，但容易被一些垃圾程序员拿来乱用。&lt;/p&gt;</summary>
    
    
    
    
    <category term="爬虫" scheme="https://www.kingname.info/tags/%E7%88%AC%E8%99%AB/"/>
    
    <category term="Python" scheme="https://www.kingname.info/tags/Python/"/>
    
    <category term="Scrapy" scheme="https://www.kingname.info/tags/Scrapy/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：如何正确解析超大JSON列表</title>
    <link href="https://www.kingname.info/2025/05/06/parse-big-json/"/>
    <id>https://www.kingname.info/2025/05/06/parse-big-json/</id>
    <published>2025-05-06T23:34:23.000Z</published>
    <updated>2026-07-20T13:57:06.248Z</updated>
    
    <content type="html"><![CDATA[<p>当我们采购数据集时，有时候供应商会以JSON Lines的形式交付给我们。这种格式，本质上是文本格式，它每一行是一个JSON。例如，供应商给我们了一个文件<code>小红书全量笔记.json</code>文件，我们可以使用如下Python代码来一行一行读取：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&#x27;小红书全量笔记.json&#x27;</span>) <span class="keyword">as</span> f:</span><br><span class="line">    <span class="keyword">for</span> line <span class="keyword">in</span> f:</span><br><span class="line">        info = json.loads(line)</span><br><span class="line">        note = info[<span class="string">&#x27;note&#x27;</span>]</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;笔记内容为：&#x27;</span>, note)</span><br></pre></td></tr></table></figure><p>这个格式的好处在于，每一次只需要把少量内容读取到内存中。即便这个文件有1TB，我们也可以使用一个4GB内存的电脑来处理。</p><p>今天出了一个乌龙事件，某数据供应商在给我数据的时候，说的是以JSON Lines格式给我。但我拿过来解压缩以后一看，100GB的文件，里面只有1行，如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250501180913796.png"><br>也就是说，他用的是一个超大JSON直接导出给我，并没有使用JSON Lines格式。正常情况下，如果我要直接解析这个数据，需要我的电脑内存超过100GB。</p><p>这个大JSON大概格式是这样的：</p><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">[</span><span class="punctuation">&#123;</span><span class="attr">&quot;question&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxx111&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;answer&quot;</span><span class="punctuation">:</span> <span class="string">&quot;aaa&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;crawled_time&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2025-05-01 12:13:14&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span> <span class="punctuation">&#123;</span><span class="attr">&quot;question&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxx222&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;answer&quot;</span><span class="punctuation">:</span> <span class="string">&quot;aaa&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;crawled_time&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2025-05-01 12:13:14&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span> <span class="punctuation">&#123;</span><span class="attr">&quot;question&quot;</span><span class="punctuation">:</span> <span class="string">&quot;xxx333&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;answer&quot;</span><span class="punctuation">:</span> <span class="string">&quot;aaa&quot;</span><span class="punctuation">,</span> <span class="attr">&quot;crawled_time&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2025-05-01 12:13:14&quot;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span> ...<span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>要解决这个问题，有三种方法。</p><p>如果这个JSON里面没有嵌套数据，只有一层<code>key: value</code>。那么非常简单。一个字符，一个字符读取。遇到<code>&#125;</code>的时候，说明一条子JSON数据已经读取完成，解析以后再读取下一条子JSON。</p><p>如果这个JSON里面有嵌套结构，那么可以使用经典算法题里面的数括号算法来解决。当发现<code>&#125;</code>的数量等于<code>&#123;</code>的时候，说明一个子JSON已经读取完成，可以解析了。</p><p>今天我们来介绍第三种方法，使用一个第三方库，叫做<code>ijson</code>。它天然支持解析这种超大的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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> ijson</span><br><span class="line"></span><br><span class="line">a = <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">[&#123;&quot;question&quot;: &quot;xxx111&quot;, &quot;answer&quot;: &quot;aaa&quot;, &quot;crawled_time&quot;: &quot;2025-05-01 12:13:14&quot;&#125;, &#123;&quot;question&quot;: &quot;xxx222&quot;, &quot;answer&quot;: &quot;aaa&quot;, &quot;crawled_time&quot;: &quot;2025-05-01 12:13:14&quot;&#125;, &#123;&quot;question&quot;: &quot;xxx333&quot;, &quot;answer&quot;: &quot;aaa&quot;, &quot;crawled_time&quot;: &quot;2025-05-01 12:13:14&quot;&#125;]</span></span><br><span class="line"><span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"></span><br><span class="line">items = ijson.items(a, <span class="string">&#x27;item&#x27;</span>)</span><br><span class="line"><span class="keyword">for</span> item <span class="keyword">in</span> items:</span><br><span class="line">    <span class="built_in">print</span>(item)</span><br></pre></td></tr></table></figure><p>运行效果如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250501182803236.png"></p><p>既不会占用大量内存，又能正常解析超大JSON。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;当我们采购数据集时，有时候供应商会以JSON Lines的形式交付给我们。这种格式，本质上是文本格式，它每一行是一个JSON。例如，供应商给我们了一个文件&lt;code&gt;小红书全量笔记.json&lt;/code&gt;文件，我们可以使用如下Python代码来一行一行读取：&lt;/p&gt;
&lt;fi</summary>
      
    
    
    
    
    <category term="Python" scheme="https://www.kingname.info/tags/Python/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：315晚会曝光的获客软件是什么原理</title>
    <link href="https://www.kingname.info/2025/03/15/315/"/>
    <id>https://www.kingname.info/2025/03/15/315/</id>
    <published>2025-03-15T23:44:29.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p>今年315晚会曝光了几个获客软件，号称可以拦截任何人的网络浏览记录，并根据对方在直播软件的留言、打过的电话、浏览过的网址，获取对方的手机号和微信号。还有在地图上随便画一个圈，就能找到圈里面130万人的联系方式。</p><p>作为一个软件工程师，我来说说我对他们背后原理的猜测。</p><span id="more"></span><p>晚会里面笼统的说到他们使用了爬虫技术。其实这种说法并不准确。爬虫做不到这种程度。爬虫只能爬取到人眼能看到的各种公开数据。例如有人在直播软件下面回复了评论，爬虫能爬到评论人的用户昵称、评论的内容。但是因为评论人的真名、手机号码和微信号并没有显示在直播软件上，所以爬虫是不能爬到的。它后续还需要使用撞库、社工库、社会工程学等等一系列操作，才能定位到用户的手机号。</p><p>以它直播软件获客这个例子，我觉得它背后的原理是这样的：</p><ol><li>获客公司有大量的爬虫，他会在各种社交网站上面爬取每个人公开的信息。例如微博、小红书、某些论坛等等。然后把这些信息储存在数据库中。也会记录他们的发帖、回帖。</li><li>收集各种社工库泄露出来的信息，也储存在数据库中。这些社工库里面可能包含了某些著名的社交网站。</li><li>根据用户需求，在某个特定的直播中，抓到其他用户的评论，发现这个评论显示用户对直播的产品有兴趣。</li><li>根据这个用户的用户名，去撞库。因为根据社会工程学的原理，很多人在多个不同的网站，会使用相同的用户名，因此通过用户名去撞库，能够把某人在不同社交网站上面的账号关联起来。</li><li>先看社工库里面，这个用户名对应的用户有没有联系方式，如果有，搞定</li><li>如果社工库没有联系方式，再去搜索这个人其他社交网络上面的发帖回帖记录，有很多人会在别人的帖子下面回复自己的手机号或者邮箱。（例如早期很多人在贴吧、在58同城、在某些招聘论坛的帖子下面，都会发布自己的联系方式）</li><li>某些国产手机的系统里面，会内置广告联盟的SDK，这些SDK会监控手机屏幕上面的各种操作，甚至截屏上传。这些SDK厂商也会出售获得的用户信息。</li></ol><p>再说说它在地图上随便画一个圈，就能找到联系方式这个能力。我怀疑它是使用了WIFI探针加上商场的WIFI。</p><p>如果我今天刚刚买了一个新的手机卡，把它插在手机上，我不太相信他们能够随便画一个圈，就把我的新手机号获取到了。肯定有一个地方会泄露手机号。那么泄露途径可能有如下几个：</p><ol><li>快递订单。他们通过各种渠道，获取到快递订单。订单上面有地址和手机号。这样简单直接把地址和手机号建立了联系。</li><li>WIFI探针+商场WIFI。很多商场为了定位客流量，都会安装WIFI探针。当我们拿着手机在商场走的时候，即便我们没有连接商场的WIFI，他们也能拿到我的手机无线网卡的mac地址。但这个时候它还没有办法拿到我的手机号。它只能知道有一个人，此刻站在第几层哪个门店前面。但由于提供这种客流定位系统的公司，一般都是那几家大公司，因此他们此时已经收集到了大量的手机无线网卡mac地址。如果某一天，我在某个商场正好连了他们的WIFI，一般连这种公共WIFI都需要输入手机号的，这个时候我的手机号就跟mac地址绑定了。以后即使我走到了另一个城市另一个商场，即使我没有连WIFI，只要这个WIFI探针的供应商或者客流定位系统是同一个公司，那么他们立刻就能知道这个手机号现在到这里了。</li><li>有了手机号，结合社工库，各种信息也都能获取到。</li></ol><p>再说一说根据网站访问记录获取手机号。这个我只能说是运营商信息泄露了。2017年，我在北京某公司工作的时候，就拿到过这种运营商数据。不过当时这种数据是脱敏过的。用户信息是md5值，只能根据不同的md5值判断这些请求是不同人的设备发送的，但无法知道具体是谁。这种情况是合法的，本来就有这种公开运营商数据买卖。市面上很多做尽职调查的公司都会采购。提供这种运营商数据的公司，他们会在运营商的机房里面安装记录设备，记录详细信息，然后经过脱敏以后卖给下游公司。</p><p>但说不定他们自己也会把没有脱敏的数据经过特殊渠道卖出去，于是就有了今年晚会上的这种功能。</p><p>有同学可能会担心这种运营商数据，是不是会把自己访问的每一个URL都记录下来？其实大可不必担心，我们要相信HTTPS。对于使用了HTTPS的网站，运营商那边拿到的数据只能定位到你访问的域名，但无法知道具体的网址。例如你访问了<a href="https://xxx.com/aa/bb/cc%EF%BC%8C%E8%BF%90%E8%90%A5%E5%95%86%E8%AE%B0%E5%BD%95%E5%8F%AA%E8%83%BD%E6%8B%BF%E5%88%B0https://xxx.com%E3%80%82%E6%97%A0%E6%B3%95%E6%8B%BF%E5%88%B0%E5%90%8E%E9%9D%A2%E7%9A%84%E5%85%B7%E4%BD%93%E5%9C%B0%E5%9D%80%E3%80%82%E9%99%A4%E9%9D%9E%E4%BB%96%E4%BB%AC%E5%9C%A8%E4%BD%A0%E7%9A%84%E6%89%8B%E6%9C%BA%E4%B8%8A%E5%AE%89%E8%A3%85%E4%BA%86%E6%A0%B9%E8%AF%81%E4%B9%A6%E3%80%82%E6%89%80%E4%BB%A5%E4%B8%8D%E8%A6%81%E5%AE%89%E8%A3%85%E6%9D%A5%E5%8E%86%E4%B8%8D%E6%98%8E%E7%9A%84%E8%AF%81%E4%B9%A6%EF%BC%8C%E6%98%AF%E4%BF%9D%E8%AF%81%E6%95%B0%E6%8D%AE%E5%AE%89%E5%85%A8%E7%9A%84%E9%87%8D%E8%A6%81%E5%89%8D%E6%8F%90%E3%80%82">https://xxx.com/aa/bb/cc，运营商记录只能拿到https://xxx.com。无法拿到后面的具体地址。除非他们在你的手机上安装了根证书。所以不要安装来历不明的证书，是保证数据安全的重要前提。</a></p><p>实际上不仅是运营商数据会被出售，银行卡、信用卡、POS机数据也会被出售。有一些做尽职调查的公司，如果要调查某教育机构的学生报名情况，他们会从刷卡数据中筛选出支付给这个教育机构的费用，这样就能算出机构的课程报名情况了。</p><p>从上面的分析可以看出，其实要获取一个人的个人信息，爬虫在里面发挥的作用其实是最无足轻重的。随便一个数据的泄露，产生的影响远远超过爬虫。</p><p>以上技术方法都是我个人的猜测。都是基于著名的直播软件不可能主动买用户手机号这个前提来做的猜测。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;今年315晚会曝光了几个获客软件，号称可以拦截任何人的网络浏览记录，并根据对方在直播软件的留言、打过的电话、浏览过的网址，获取对方的手机号和微信号。还有在地图上随便画一个圈，就能找到圈里面130万人的联系方式。&lt;/p&gt;
&lt;p&gt;作为一个软件工程师，我来说说我对他们背后原理的猜测。&lt;/p&gt;</summary>
    
    
    
    
    <category term="爬虫" scheme="https://www.kingname.info/tags/%E7%88%AC%E8%99%AB/"/>
    
    <category term="黑产" scheme="https://www.kingname.info/tags/%E9%BB%91%E4%BA%A7/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：我的Cursor开发经验</title>
    <link href="https://www.kingname.info/2025/03/13/cursor-way/"/>
    <id>https://www.kingname.info/2025/03/13/cursor-way/</id>
    <published>2025-03-13T21:41:41.000Z</published>
    <updated>2026-07-20T13:57:06.192Z</updated>
    
    <content type="html"><![CDATA[<p>这两天我使用Cursor开发了一个新闻网站的前端+后端。在开发的过程中，我总结了一些适合于我自己的最佳实践。这些方法让我在使用Cursor的时候，几乎没有遇到任何阻碍，非常顺利，非常流畅地完成了网站的开发。</p><p>我的开发经验，总结起来一句话就能说清楚：多写文档少聊天。下面我来详细说一下具体方法。</p><span id="more"></span><p>我全程使用Cursor的agent模式，模型使用Claude 3.7 Sonnet。这个项目是一个新闻网站，需要写前端+后端。</p><p>前端我首先使用Trickle生成了页面。大家也可以使用Bolt.new或者lovable，效果都差不多。需要和后端交互的地方都先使用假数据模拟。生成好以后，把代码下载到本地。</p><h2 id="改写前端代码"><a href="#改写前端代码" class="headerlink" title="改写前端代码"></a>改写前端代码</h2><p>使用Cursor打开下载的前端代码，让它阅读代码，并使用<code>Next.js</code> + <code>tailwind css</code> + <code>shadcn/ui</code>改写代码。并特别提醒，新版本的shadcn&#x2F;ui对应的命令应该是<code>npx shadcn xxx</code>，让他不要再使用老版本的写法。</p><p>改写完成以后，执行<code>npm run dev</code>预览前端页面，确保改写以后的效果跟你之前的一样。</p><h2 id="创建临时API文档"><a href="#创建临时API文档" class="headerlink" title="创建临时API文档"></a>创建临时API文档</h2><p>由于前端页面本来就是你设计的，因此你肯定很清楚这个前端页面在哪些地方需要跟后端做交互。</p><p>现在，在代码根目录创建一个markdown文件，例如叫做<code>api_desc.md</code>，然后在里面描述你的后端API。这里描述不需要写得很细节，关键是要写清楚api的功能，路径，参数和返回示例。如下图所示。</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250312225523131.png"></p><p>由于新闻列表页和详情页需要从MongoDB里面读取，因此再创建一个<code>article_schema.md</code>，在里面描述数据在MongoDB中的储存结构。如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250312225540650.png"></p><p>接下来，在Cursor的聊天窗口中，新开一个后端代码专用的对话，让它创建一个后端服务：</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">请使用FastAPI帮我创建一个后端服务。这个服务包含多个API接口，接口的需求请参考api_desc.md。列表页的数据和详情页的数据，需要从MongoDB读取，数据结构参考article_schema.md。后端代码写好以后，需要生成一个api_doc.md文件，里面详细描述API的请求地址、参数和返回值格式。以后每次对后端API做修改，都需要同步更新api_doc.md</span><br></pre></td></tr></table></figure><p>Cursor执行完成以后，不仅生成了后端代码，还生成了API文档，文档如下图所示。</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250312225553523.png"></p><p>这个API文档可以说是相当的标准。</p><h2 id="前后端对接"><a href="#前后端对接" class="headerlink" title="前后端对接"></a>前后端对接</h2><p>回到前端的对话历史中，让Cursor对接后端:</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">现在，我们要开始对接后端API。后端的接口我已经写到了api_doc.md文件中，请仔细阅读，然后修改前端代码的对应位置，把原来的假数据代码改为请求后端API获得正式数据。</span><br></pre></td></tr></table></figure><p>Cursor修改以后，第一个前后端联动的版本就已经完成了。正常情况下应该已经可以使用了。如果遇到一些小的报错，你可以通过对话的形式让它修复。</p><h2 id="进一步开发"><a href="#进一步开发" class="headerlink" title="进一步开发"></a>进一步开发</h2><p>接下来，你可能需要在已有的API上面修改字段，或者新增API接口。这种情况下一般都是先让Cursor修改后端代码。它修改以后，会自动更新<code>api_doc.md</code>文件。然后再让前端代码基于<code>api_doc.md</code>适配后端的修改。</p><p>如果你的修改是小修改，例如在列表页API中添加一个tags参数，用来根据tags过滤新闻，那么你可以直接通过聊天对话的形式跟Cursor沟通，让它完成。</p><p>如果你的修改是一个相当完整独立的新功能，那么你可以新增一个需求文档，例如要做一个新闻订阅页面，这个时候就可以创建<code>subscribe_api.md</code>文件，在里面描述自己需要的API，从而先生成后端代码和API文档，再创建一个<code>subscribe.md</code>，在里面描述前端页面的需求，并让Cursor基于这个需求加上API文档，生成前端代码。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在使用Cursor的时候，我更倾向于人机协作开发而不是让你当甩手掌柜。你需要给Cursor提供必要的指导，从而让它顺着你的思路来做开发。你的脑子里面要有这个系统的开发路线和架构，你需要知道系统由哪些部分组成，每个部分需要怎么做。软件开发是系统设计+编码。让Cursor去做编码工作，而不是去做设计工作。</p><p>不要相信网上那些完全不懂代码的人纯靠文本描述就做出复杂功能的说辞，要不就是他们嘴里的复杂功能其实是简单功能，要不就是他在吹牛。</p><p>你应该多写文档，通过文档来描述你的需求。这样Cursor以后的每次修改都会注意不违背你的需求文档。聊天窗口一般是告诉Cursor应该使用哪个文档来进行开发。尽量不要在聊天窗口里面提需求。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;这两天我使用Cursor开发了一个新闻网站的前端+后端。在开发的过程中，我总结了一些适合于我自己的最佳实践。这些方法让我在使用Cursor的时候，几乎没有遇到任何阻碍，非常顺利，非常流畅地完成了网站的开发。&lt;/p&gt;
&lt;p&gt;我的开发经验，总结起来一句话就能说清楚：多写文档少聊天。下面我来详细说一下具体方法。&lt;/p&gt;</summary>
    
    
    
    
    <category term="Cursor" scheme="https://www.kingname.info/tags/Cursor/"/>
    
    <category term="开发经验" scheme="https://www.kingname.info/tags/%E5%BC%80%E5%8F%91%E7%BB%8F%E9%AA%8C/"/>
    
  </entry>
  
  <entry>
    <title>一日一技：如何实现临时密码？</title>
    <link href="https://www.kingname.info/2025/02/24/temp-secret/"/>
    <id>https://www.kingname.info/2025/02/24/temp-secret/</id>
    <published>2025-02-24T20:26:24.000Z</published>
    <updated>2026-07-20T13:57:06.252Z</updated>
    
    <content type="html"><![CDATA[<p>我买的房子今天交房了。开发商配的门锁是某品牌的智能门锁，它可以使用指纹开锁，也可以使用密码开锁。在使用手机跟门锁配对以后，可以远程在手机上生成临时密码。临时密码只能使用1次，并且在生成的30分钟内有效。这个功能可以方便装修人员进出又不用担心泄露密码。</p><p>因为新房子还没有通网，所以门锁肯定是无法连接互联网的。而装修人员给我打电话要临时密码时，我在公司，离家几十公里外，门锁也不可能跟手机通信。</p><p>那么问题来了，门锁是怎么验证这个临时密码合法的？</p><span id="more"></span><p>今天我一直在想这个问题，目前有一些思路，但无法确定。所以发出来跟大家一起讨论一下它的实现方法。</p><p>已知：</p><ol><li>手机App只有第一次跟门锁配对时，会通信，之后就完全不会有任何通信</li><li>门锁无法连接外网</li><li>无论我在任何地方，手机上都能生成临时密码。门锁输入临时密码就能解锁</li><li>临时密码只能使用一次，之后就会失效</li><li>临时密码是8位数字</li><li>临时密码有效期30分钟，超时以后就会失效</li><li>手机可以连续多次生成临时密码，每一次密码都不一样，但每个临时密码都可以使用</li></ol><p>首先第4条非常简单，在门锁里面记录一下已经使用的密码就可以实现密码只能使用1次。所以不需要考虑这个问题了。</p><p>另外几个问题，我根据我自己的编程经验做一些推测。</p><p>临时密码是一个8位数字，例如<code>8031 1257</code>。由于手机不需要跟门锁通信，门锁就能够识别这个密码，因此我一开始觉得这个8位数字包含某种校验规则。例如，前4个数字，乘以100以后对26取余数，就是第5、6位数字。前6个数乘以5643然后对97取余数，就是第7、8位数字。这里的四个关键数字<code>100</code>、<code>26</code>、<code>5643</code>、<code>97</code>，可能是手机在和门锁配对的时候发送给门锁的。</p><p>但这里无法解释门锁怎么知道数字什么时候过期。难道8位数字能够包含精确到分钟的时间戳信息？例如现在我写文章的时候，对应的时间戳是<code>1740143928</code>。这是一个10位数字，我实在想不到如何把一个10位的数字藏在8位数字里面，并且在必要的时候还能还原回原来的10位数字。</p><p>因此，我换了一个思路，有没有可能密码锁里面自带一个时钟？在手机配对时，会同步校准这个时钟，使它跟手机保持相同的时间。如果是这种方案，那么这个临时密码8位数字，其实可以不用包含自我校验。我们可以使用app和密码锁各自按相同的逻辑走一轮加密，然后对比生成的密钥是否相同。</p><p>手机在跟密码锁配对时，发送一个密钥到密码锁里面。要生成临时密码时，手机使用时间戳和密钥通过某个算法生成8个数字。密码锁也使用时间戳和这个相同的密钥，相同的算法，也生成8位数字，如果跟临时密码相同，就开锁。对应的Python密码类似如下：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> hashlib</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">SECRET_KEY = <span class="number">123456</span>  <span class="comment"># 手机同步给密码锁的密钥</span></span><br><span class="line">now = <span class="built_in">int</span>(time.time())</span><br><span class="line">timespan = now // <span class="number">1800</span>  <span class="comment"># 在30分钟内，这个值都是相同的</span></span><br><span class="line"></span><br><span class="line">temp_key_hex = hashlib.md5(<span class="built_in">str</span>(SECRET_KEY + timespan).encode()).hexdigest() <span class="comment"># 16进制的密码</span></span><br><span class="line">temp_key_full = <span class="built_in">int</span>(temp_key_hex, <span class="number">16</span>)  <span class="comment"># 转成10进制</span></span><br><span class="line"></span><br><span class="line">temp_key = <span class="built_in">str</span>(temp_key_full)[-<span class="number">8</span>:]  <span class="comment"># 取最后8位数字</span></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&#x27;临时密码：&#x27;</span>, temp_key)</span><br></pre></td></tr></table></figure><p>运行效果如下图所示：</p><p><img src="https://kingname-1257411235.file.myqcloud.com/20250221214322932.png"></p><p>由于从任何时间戳x开始，<code>x // 1800 == (x + n) // 1800</code> 或<code>x // 1800 + 1 == (x + n) // 1800</code>，其中<code>0 &lt;= n &lt;= 1800</code>。密码锁使用相同的代码，只不过分别把<code>timespan</code>和<code>timespan + 1</code>都生成一个密码，然后对比，这样就可以实现手机生成密码过几分钟再在锁上面输入密码，锁也能成功验证。</p><p>使用这种方式，可以满足编号<code>1-6</code>的需求。但问题是遇到需求<code>7</code>怎么办？上面这种方式，会导致在30分钟内，临时密码只有这一个。</p><p>我绞尽脑汁想不出一个聪明的算法能解决这个问题。但是我在多次尝试生成临时密码时，发现两次密码的生成间隔必须大于5秒。</p><p>如果它的算法真的那么聪明，为什么不能让我生成无限个可用的临时密码？所以我怀疑它可能没有那么聪明，它可能用的是笨办法，例如：穷举。</p><p>对手机来说，生成密码的算法跟上面差不多，唯一的区别是，把其中的<code>1800</code>换成<code>5</code>。也就是说，每5秒钟会生成不同的临时密码。因为<code>now // 5</code>在5秒钟内是相同的，超过5秒就会变。</p><p>而对于门锁，当它感知到用户正在输入密码时，以当前时间戳为起点，每5秒一轮，往前推360轮，生成360个密码。如果发现用户输入的8位数跟这360个密码中的某一个相同，就解锁。</p><p>如果锁的芯片性能好，在用户按完临时密码前，就能够做完360次计算，用户完全不会有任何感知。如果锁的芯片不够好，它可以在内存中维持一个长度为360的双向链表，每5秒计算一个密码放进去，然后把末尾的密码丢掉。这样当用户输入密码的时候，它可以直接跟这个双向链表中的密码逐一比对。</p><p>当然，以上全都是我个人的推测。如果大家知道它是怎么做的，或者想到了什么更好的方法，欢迎留言一起交流。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;我买的房子今天交房了。开发商配的门锁是某品牌的智能门锁，它可以使用指纹开锁，也可以使用密码开锁。在使用手机跟门锁配对以后，可以远程在手机上生成临时密码。临时密码只能使用1次，并且在生成的30分钟内有效。这个功能可以方便装修人员进出又不用担心泄露密码。&lt;/p&gt;
&lt;p&gt;因为新房子还没有通网，所以门锁肯定是无法连接互联网的。而装修人员给我打电话要临时密码时，我在公司，离家几十公里外，门锁也不可能跟手机通信。&lt;/p&gt;
&lt;p&gt;那么问题来了，门锁是怎么验证这个临时密码合法的？&lt;/p&gt;</summary>
    
    
    
    
    <category term="算法" scheme="https://www.kingname.info/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
</feed>
