<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>软件工程 on 张欣耕的个人博客</title><description>Recent content in 软件工程 on 张欣耕的个人博客</description><link>https://shanechang.com/zh-cn/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/</link><language>zh-cn</language><lastBuildDate>Mon, 09 Mar 2026 00:00:00 GMT</lastBuildDate><atom:link href="https://shanechang.com/zh-cn/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/index.xml" rel="self" type="application/rss+xml"/><item><title>Temporal 时间快进:你没见过的那只&quot;钟&quot;</title><link>https://shanechang.com/zh-cn/p/temporal-time-skipping-the-clock-you-didnt-know-existed/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/temporal-time-skipping-the-clock-you-didnt-know-existed/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.D09JL2C2_Z1BbfYs.webp&quot; alt=&quot;Featured image of post Temporal 时间快进:你没见过的那只&amp;quot;钟&amp;quot;&quot; /&gt;&lt;p&gt;有那么一阵子,我在调试一套测试,生产上跑得溜溜的,测试环境下却炸成烟花。工作流没问题,活动都完成了,信号也收到了——可每个测试都挂了个 &lt;code&gt;TimeoutError&lt;/code&gt;,工作流还没跟测试代码”打个招呼”就一命呜呼。&lt;/p&gt;
&lt;p&gt;背后凶手是一只我从来没注意过的”钟”。&lt;/p&gt;
&lt;p&gt;今天这篇文章,就来聊聊那个终于让我理解 Temporal 时间快进测试服务器的心智模型。如果你也曾被 &lt;code&gt;WorkflowEnvironment.start_time_skipping()&lt;/code&gt; 搞晕,或者遇到过生产没问题,测试却老是超时的离谱现象,欢迎入座!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;什么是-temporal-30秒极简介绍&quot;&gt;什么是 Temporal? (30秒极简介绍)&lt;/h2&gt;
&lt;p&gt;Temporal 是一个工作流编排引擎。你把业务流程写成一个”工作流”(比如一连串步骤),Temporal 保证帮你靠谱地跑完。哪怕有一步挂了,它也会自动重试,服务器崩了还能恢复进度。所有让人抓狂的东西——重试, 超时, 状态保存, 分布式协调——它全包了。&lt;/p&gt;
&lt;p&gt;这篇文章只需要知道一件事:&lt;strong&gt;你的工作流不是在普通的 Python 环境里跑的,而是”住”在 Temporal 的专属运行时里。&lt;/strong&gt; Temporal 负责掌控什么时候发生什么,这一点非常重要。后面一切的谜题都从这里展开。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;temporal-是时间管理员&quot;&gt;Temporal 是时间管理员&lt;/h2&gt;
&lt;p&gt;改变我认知的关键点:&lt;strong&gt;Temporal 不会跳过你的代码,它只控制自己的”钟”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我以前总觉得 Temporal 是个加速器,会让我的代码嗖嗖地快进。其实不是。Temporal 就像内核(kernel)一样,是个大管家,维护着一张”定时事件表”。&lt;/p&gt;
&lt;p&gt;比如你的工作流里有:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;3600&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 睡1小时&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Temporal 并不会让 Python 代码飞快执行,而是悄悄在自己的时间表上记一笔:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;在当前时间 + 3600秒时叫醒工作流ABC&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;又比如你启动一个30秒超时的活动:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;如果活动XYZ在当前时间 + 30秒还没回,算失败&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整体超时10分钟:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&quot;如果工作流ABC在当前时间 + 600秒还没完,直接掐灭&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论是 sleep, 超时, 活动期限还是心跳检查,对 Temporal 来说本质都是”定时事件”:到点了,干某事。区别只是干什么——恢复, 失败, 杀掉——机制完全一样,就是”时间戳+动作”。&lt;/p&gt;
&lt;p&gt;这,就是理解时间快进的基础。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;时间快进无聊时就快进&quot;&gt;时间快进:无聊时就快进&lt;/h2&gt;
&lt;p&gt;Temporal 的 Python SDK 有两种测试环境:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WorkflowEnvironment.start_time_skipping()&lt;/code&gt;&lt;/strong&gt; —— 下载一个超轻量的 Rust 测试服务器(二进制直接跑,无需 Docker)。这个服务器有一块”虚拟钟表”,能随时快进。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WorkflowEnvironment.start_local()&lt;/code&gt;&lt;/strong&gt; —— 跑一个完整的 Temporal 服务,和生产一模一样,时钟也是实打实的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;时间快进服务器的聪明之处在于:&lt;strong&gt;只要它发现啥也没在发生,它就会把自己的时钟跳到下一个事件。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如你的流程是:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;3600&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 睡1小时&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; do_some_activity()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;7200&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)   &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 睡2小时&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生产上,光这俩 sleep 就得熬3小时。时间快进模式下是这样:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;调用 &lt;code&gt;sleep(3600)&lt;/code&gt;,时间表登记:“3600秒后恢复”。&lt;/li&gt;
&lt;li&gt;发现没别的事干,咻——时钟直接跳到3600秒后。&lt;/li&gt;
&lt;li&gt;定时器响了,继续执行,开始活动。&lt;/li&gt;
&lt;li&gt;活动在跑——这时服务器&lt;strong&gt;不能快进&lt;/strong&gt;,得等真实的结果。&lt;/li&gt;
&lt;li&gt;活动完了,再来个 &lt;code&gt;sleep(7200)&lt;/code&gt;,时间表又记一笔。&lt;/li&gt;
&lt;li&gt;没事干? 再咻——时钟跳7200秒。&lt;/li&gt;
&lt;li&gt;定时器响,流程结束。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整个流程花的真实时间,只取决于活动(比如 mock 活动可能只耗几毫秒)。那3小时的 sleep? 一秒没浪费,直接”快进到片尾”。&lt;/p&gt;
&lt;p&gt;这有个非常关键的结论:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果你的代码里从不 sleep, 不等, 不设超时,时间快进模式根本不会提速。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;想想看:时间快进只会跳过”无聊时间”——即 Temporal 的时钟处于空闲时。如果整个流程全是计算和逻辑,没有任何等待,时间快进就一点用没有。&lt;/p&gt;
&lt;p&gt;当然,现实里的工作流总有各种等待, 重试, sleep。关键在于:&lt;strong&gt;时间快进不是让代码更快,而是消灭事件之间的”等待时间”。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;handle你的遥控器&quot;&gt;Handle:你的遥控器&lt;/h2&gt;
&lt;p&gt;每当你启动一个工作流,Temporal 会给你返回个”handle”。你可以把它想象成餐厅的取餐小票——下单后这张票就是你和厨房沟通的唯一渠道。&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;handle &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; client.start_workflow(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    MyWorkflow.run,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;    inputs,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;    id&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;order-123&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;    task_queue&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#0A3069;--shiki-dark:#A5D6FF&quot;&gt;&quot;kitchen&quot;&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会让工作流在 Temporal 里独立运行。handle 就是你的遥控器,有几个常用”按钮”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.result()&lt;/code&gt;&lt;/strong&gt; —— “等我饭好通知我。” 你一直等着,直到结果出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.query()&lt;/code&gt;&lt;/strong&gt; —— “我的饭现在咋样啦? ” 问一句,立马得到当前状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.signal()&lt;/code&gt;&lt;/strong&gt; —— “再加点芝士! ” 给正在跑的工作流发个消息,立刻送达。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;handle.cancel()&lt;/code&gt;&lt;/strong&gt; —— “不吃了,取消订单。”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;handle 其实就是 &lt;code&gt;temporalio.client.WorkflowHandle&lt;/code&gt;,没啥神秘的。关键是你点哪个”按钮”,Temporal 的时间快进服务器会有不同的反应。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;result-vs-query--你掉的那个坑&quot;&gt;&lt;code&gt;result()&lt;/code&gt; vs &lt;code&gt;query()&lt;/code&gt; — 你掉的那个坑&lt;/h2&gt;
&lt;p&gt;这里是我理解彻底反转的地方——也是让我卡壳最久的关键。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;handle.result()&lt;/code&gt;&lt;/strong&gt; 就像打电话:“我等着,饭好了一定通知我! ” 你在那儿死等。更关键的是,你等的方式会让时间快进服务器理解为:“用户在等结果,我得帮忙快进到流程结束。”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;handle.query()&lt;/code&gt;&lt;/strong&gt; 则是打个电话:“现在饭好了吗? 没好啊? 那挂了哈! ” 一次性问答。服务器没理由快进——你没让它等啥。&lt;/p&gt;
&lt;p&gt;这里的坑在于:&lt;strong&gt;&lt;code&gt;result()&lt;/code&gt; 是事件驱动(等”完成”事件),&lt;code&gt;query()&lt;/code&gt; 是轮询(问一下返回当前状态)。&lt;/strong&gt; 按常理,事件驱动总是”更好”,但在时间快进模式下,轮询反而更安全,因为它不会授权 Temporal 快进时钟。&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;操作&lt;/th&gt;&lt;th&gt;Temporal 理解&lt;/th&gt;&lt;th&gt;是否触发快进&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;handle.result()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;”等我做好叫你”&lt;/td&gt;&lt;td&gt;&lt;strong&gt;是&lt;/strong&gt;(服务会快进到结果)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;handle.query()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;”现在状态咋样? “&lt;/td&gt;&lt;td&gt;&lt;strong&gt;否&lt;/strong&gt;(只回答,不快进)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;handle.signal()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;”传个消息”&lt;/td&gt;&lt;td&gt;&lt;strong&gt;否&lt;/strong&gt;(立刻送达)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;中间那一列就是全部真相。&lt;code&gt;result()&lt;/code&gt; 给了服务器快进的许可,&lt;code&gt;query()&lt;/code&gt; 没给。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;竞态条件死于时间旅行&quot;&gt;竞态条件:死于时间旅行&lt;/h2&gt;
&lt;p&gt;拼图已经齐了,下面说说这些原理怎么凑一起,制造了一个让人抓狂的 bug。&lt;/p&gt;
&lt;h3 id=&quot;场景设定&quot;&gt;场景设定&lt;/h3&gt;
&lt;p&gt;我们的工作流有个环节需要用户反馈。每跑完一步(比如分析文档),工作流会暂停,等用户点”继续”。生产环境下,这可能要等几分钟, 几小时,用户思考, 编辑, 喝咖啡。&lt;/p&gt;
&lt;p&gt;代码里等待是这样的:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# backend/src/genai/temporal/workflow_executor.py, line 149&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; workflow.wait_condition(&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;lambda&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;: waiter.signal_received)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;意思就是:“等到 &lt;code&gt;signal_received&lt;/code&gt; 变 True 再继续。” 没有超时,爱等多久等多久。生产上OK,用户点了继续,信号发到,流程继续跑。&lt;/p&gt;
&lt;h3 id=&quot;考试监考的比喻&quot;&gt;考试监考的比喻&lt;/h3&gt;
&lt;p&gt;想象你是监考老师,考试2小时,墙上有钟。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生产环境(真钟):&lt;/strong&gt; 学生举手:“老师,我要去拿计算器。” 你等着,计算器送到,学生继续考试,钟上显示45分钟过去,一切正常。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时间快进(魔法钟):&lt;/strong&gt; 学生举手:“我要拿计算器。” 你环顾教室,没人写字,没动静,于是咻地一下把钟拨到1小时, 1.5小时, 2小时——“时间到,交卷! ” 学生还没回来,考试挂了。&lt;/p&gt;
&lt;p&gt;那边计算器刚进门,钟已经跳到2小时,晚了。&lt;/p&gt;
&lt;h3 id=&quot;真正发生了什么&quot;&gt;真正发生了什么&lt;/h3&gt;
&lt;p&gt;来看测试里的实际时间线:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 0.00s&lt;/strong&gt; —— 测试启动工作流。Mock 活动瞬间完成(没真I/O)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 ~0.02s&lt;/strong&gt; —— 所有活动结束,工作流进入 &lt;code&gt;wait_condition()&lt;/code&gt;,等反馈信号。这时 Temporal 的”时间表”里&lt;strong&gt;啥也没在等&lt;/strong&gt;:没活动, 没定时器,只有个等信号的工作流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 ~0.02s&lt;/strong&gt; —— 测试调用了 &lt;code&gt;handle.result()&lt;/code&gt;:“等流程结束告诉我。”&lt;/p&gt;
&lt;p&gt;时间快进服务器一听:“用户等结果? 查查时间表……啥都没挂着,只剩下600秒后的整体超时。那我直接快进600秒吧! ”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;服务器时钟跳跃:&lt;/strong&gt; 0s → 600s。&lt;/p&gt;
&lt;p&gt;工作流超时,挂了,报 &lt;code&gt;TimeoutError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真实时间 ~0.03s&lt;/strong&gt; —— &lt;code&gt;handle.result()&lt;/code&gt; 报错返回。&lt;/p&gt;
&lt;p&gt;这时,测试里本来有个轮询循环,计划 &lt;code&gt;asyncio.sleep(0.1)&lt;/code&gt; 后去 poll 状态, 发信号。但这会儿,工作流尸体早凉了,信号只能送给”亡灵”。&lt;/p&gt;
&lt;p&gt;整个过程真实时间不到30毫秒,服务器一键”快进到大结局”。&lt;/p&gt;
&lt;h3 id=&quot;解决方法&quot;&gt;解决方法&lt;/h3&gt;
&lt;p&gt;明白原理后,解决办法反而很朴素。别在等信号时调用 &lt;code&gt;handle.result()&lt;/code&gt;。改用 &lt;code&gt;handle.query()&lt;/code&gt; 轮询进度,根据需要发信号,&lt;strong&gt;只有确认流程完了再调 &lt;code&gt;result()&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code astro-code-themes github-light-default github-dark-default&quot; style=&quot;background-color:#ffffff;--shiki-dark-bg:#0d1117;color:#1f2328;--shiki-dark:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# backend/tests/integration/genai/test_temporal_workflow.py, lines 49-77&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#8250DF;--shiki-dark:#D2A8FF&quot;&gt; _run_workflow_with_feedback&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;(handle):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;    while&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt; True&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; asyncio.sleep(&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;0.1&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;)    &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 测试进程的真实 sleep&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;        # query() 不触发快进,时钟保持原地&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;        progress &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; handle.query(JurorAnalysisWorkflow.get_progress)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;        # 给所有等待反馈的步骤发信号&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; step_id &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; progress.pending_feedback_steps:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;            await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; handle.signal(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;                JurorAnalysisWorkflow.submit_step_feedback,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;                StepFeedbackSignal(&lt;/span&gt;&lt;span style=&quot;color:#953800;--shiki-dark:#FFA657&quot;&gt;release_step_id&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;step_id),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;            )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;        # 确认流程真正结束后再调 result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; progress.status &lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; WorkflowStatus.&lt;/span&gt;&lt;span style=&quot;color:#0550AE;--shiki-dark:#79C0FF&quot;&gt;COMPLETED&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#CF222E;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#1F2328;--shiki-dark:#E6EDF3&quot;&gt; handle.result()  &lt;/span&gt;&lt;span style=&quot;color:#6E7781;--shiki-dark:#8B949E&quot;&gt;# 这时已经完事,result 立刻返回&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理很简单:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;query()&lt;/code&gt; 不动时钟&lt;/strong&gt;,工作流静止在 &lt;code&gt;wait_condition&lt;/code&gt; 时刻,服务器不快进。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轮询循环用真实时间跑&lt;/strong&gt;,&lt;code&gt;asyncio.sleep(0.1)&lt;/code&gt; 是测试进程自己的等待,不受 Temporal 支配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;信号无视时钟,立刻送达。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只有确定流程完了才调 &lt;code&gt;result()&lt;/code&gt;,这时无需再快进,result 也会瞬间返回。&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只要你不给服务器快进的机会,工作流的时钟就老老实实等着。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;总结几条硬核法则&quot;&gt;总结:几条硬核法则&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;心智模型:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Temporal 是时间管理员,不是加速器。它维护一张”定时事件表”。&lt;/li&gt;
&lt;li&gt;时间快进只”加速”空闲时,无事可跳就不跳。&lt;/li&gt;
&lt;li&gt;没有定时器, 超时, sleep,时间快进也无用武之地。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实操守则:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;千万别在等信号时 &lt;code&gt;await handle.result()&lt;/code&gt;,否则 Temporal 会直接快进到流程超时,信号都来不及送到。&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;handle.query()&lt;/code&gt; 轮询工作流状态,查询是被动的,不会影响时钟。&lt;/li&gt;
&lt;li&gt;信号任何时候都能送达,不受时钟影响。&lt;/li&gt;
&lt;li&gt;只有确认流程结束,再调 &lt;code&gt;handle.result()&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;选哪个测试环境:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;start_time_skipping()&lt;/code&gt;&lt;/strong&gt; —— 适合自闭型工作流(只有定时器, 活动,没有外部交互)。或者依赖信号的流程,但用轮询+信号模式也OK。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;start_local()&lt;/code&gt;&lt;/strong&gt; —— 需要流程中途信号/查询的,建议用这个,所有等待都是真实时间,但会慢一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们最终还是选了 &lt;code&gt;start_time_skipping()&lt;/code&gt;,因为轮询+信号模式能完美兼容,还能免费享受所有 “sleep/重试/抖动” 的快进。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;声明:本文由人类创作,部分内容参考 AI 建议,最终拍板归作者。&lt;/em&gt;&lt;/p&gt;</description><pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate></item><item><title>Cursor 真厉害——但我为什么还是用得抓狂</title><link>https://shanechang.com/zh-cn/p/cursor-windsurf-comparison-review/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/cursor-windsurf-comparison-review/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.DGRCuqQK_Z19ffmq.webp&quot; alt=&quot;Featured image of post Cursor 真厉害——但我为什么还是用得抓狂&quot; /&gt;&lt;p&gt;先给大家讲个$600的”爆肝”故事。&lt;/p&gt;
&lt;p&gt;这个月刚过三分之二, 我的 Cursor 配额就烧光了——不是因为我浪费, 而是认真八小时写功能, 查代码, 调结构, 做着资深工程师该做的事: 追求类型安全, 模块化, 可维护。&lt;/p&gt;
&lt;p&gt;盯着”升级为按量付费”的弹窗时, 我的心情有点复杂——累, 还有点小委屈。Cursor 真香, 检索系统就像魔法师, 能把分散在各处的依赖都揪出来。上下文理解力, 甩开一大票竞品。出错?几乎没有。&lt;/p&gt;
&lt;p&gt;可偏偏, 这几周我一直在和它较劲。&lt;/p&gt;
&lt;p&gt;本文不是常规测评, 不打算给你推荐”买谁”。我是全栈工程师, 搞 AI 创业, ML 硕士, 对代码质量有点洁癖。我需要的是能尊重我判断力的工具, 而不是替我下决定的 AI。用了几个月 Cursor, Windsurf 还有 Claude Code, 我发现了比”哪个更好”更有意思的东西——两种截然不同的 AI 编程哲学。&lt;/p&gt;
&lt;h2 id=&quot;那些没人提的真问题-认知可预期性&quot;&gt;那些没人提的真问题: 认知可预期性&lt;/h2&gt;
&lt;p&gt;市面上测评都喜欢谈”价格透明”, Cursor 就很自豪地实时展示 token 消耗, 看着挺明了是吧?可我根本看不懂这数字到底意味着啥——重构一块代码要多少 token?加个功能要烧多少?大改架构是不是得卖肾?数字虽然在那, 实际没什么参考价值。&lt;/p&gt;
&lt;p&gt;于是, 我总在犹豫: 要不要让它帮我试个新思路, 会不会一下子额度见底?这种踟蹰, 其实才是最大成本。真正消耗的不是钱, 而是你脑子里不停算”这操作值不值”的额外负担。&lt;/p&gt;
&lt;p&gt;再试试 Windsurf, 定价直接让人犯困 (褒义) 。每种模型给你明码标价, Opus 就是 3 倍信用点, 百万 token 的 Sonnet 10 倍。用前就心里有底: 每天开发大概十美元。自动充值一开, 心无旁骛。&lt;/p&gt;
&lt;p&gt;心理差异巨大。不是 Windsurf 便宜多少, 而是——你能”预判”自己的消耗, 脑子终于能专心搞事, 背景焦虑感直接消失。Cursor 这种”雾里看花”的价格, 不只是掏空钱包, 更是榨干注意力。&lt;/p&gt;
&lt;h2 id=&quot;当聪明工具遇上老练工程师&quot;&gt;当聪明工具遇上老练工程师&lt;/h2&gt;
&lt;p&gt;必须说句公道话: Cursor 很少犯低级错误, 代码靠谱, 建议也聪明。可问题不在技术失误, 而是更细腻的体验。&lt;/p&gt;
&lt;p&gt;来举个栗子:&lt;/p&gt;
&lt;p&gt;我的代码库, 所有函数, 类的 docstring 都是一行简洁描述——类型提示只写在签名里, 绝不往 docstring 塞 type。Pydantic 模型全家桶风格也统一, 整个项目都在”明示”这些约定。&lt;/p&gt;
&lt;p&gt;Windsurf 识趣, 一切照旧。你用 Pydantic, 它就用;docstring 一行到底, 它也跟着写;风格完全贴合现有代码。&lt;/p&gt;
&lt;p&gt;Cursor 则爱走自己的路。&lt;/p&gt;
&lt;p&gt;除非我每次都在 prompt 里手动强调”docstring 只写一行”, 否则它总爱写多行说明。就算前后左右所有函数都单行, 它也要自作主张。它想写测试, 想补文档, 想按自己喜好的格式来, 完全不管你项目的规矩。&lt;/p&gt;
&lt;p&gt;你可以用 prompt 跟它”battle”, 每次都得加一句”按现有风格, 单行 docstring, 用 Pydantic, 类型提示写签名”, 仿佛有个超强但总忘记你是谁的同事, 你得不断提醒他”哥们儿, 这不是你家实验室”。&lt;/p&gt;
&lt;p&gt;于是, 沟通变成死循环。你反复精心措辞, Cursor 却自顾自改代码, 套默认风格。你不得不打断它, 重申明明写在代码里的规范, 工程师和 AI 的”心理内耗”飙升。&lt;/p&gt;
&lt;p&gt;讽刺的是, 这些行为对新手其实很友好——有”意见”的工具能帮你养成好习惯。问题是, Cursor 优化的是”平均开发者”, 需要”护栏”的那批人。可对资深工程师来说, 这些护栏反而成了”手铐”。&lt;/p&gt;
&lt;p&gt;我不想工具替我定风格, 我只想它学会我的。&lt;/p&gt;
&lt;h2 id=&quot;cursor-的一技之长&quot;&gt;Cursor 的一技之长&lt;/h2&gt;
&lt;p&gt;说句良心话, Cursor 的检索 (RAG) 系统真的厉害。&lt;/p&gt;
&lt;p&gt;你问 Cursor 代码问题时, 它会显示”searching…”的动画, 真不是摆样子, 而是用强大的上下文检索系统帮你搜最相关的内容。直观, 透明, 频繁出现, 你几乎能目睹它”思考”的过程。&lt;/p&gt;
&lt;p&gt;这让 Cursor 在全局规划时如虎添翼。你要实现涉及多服务的功能?Cursor 能把分散的依赖都扒出来, 理清前后关系, 给出整套方案。像有个把你代码库倒背如流的搭档。&lt;/p&gt;
&lt;p&gt;Windsurf 虽然也有”Quick Context”, 但基本得你自己手动触发, 且更像关键词检索而非语义级 RAG。快归快, 检索深度差点意思, 有时还只是遍历文件。&lt;/p&gt;
&lt;p&gt;可惜, Cursor 的强项也是软肋。它自动检索, 思路很主观, 你也很难判断它的依据。有几次 Cursor 直接删了重要代码, 只因为没搜到相关功能 (其实明明存在) 。我得手动提醒它”读完整个文件”, 它有时还会无视指令。&lt;/p&gt;
&lt;p&gt;这就是 Cursor 的哲学: 既聪明又坚持己见。探索, 架构阶段超好用, 关键代码实现时就有点危险。&lt;/p&gt;
&lt;h2 id=&quot;你没发现的脑内负担&quot;&gt;你没发现的”脑内负担”&lt;/h2&gt;
&lt;p&gt;有种”细水长流”的烦躁, 很难具体描述, 不是愤怒, 就是像有点小电流一直嗡嗡作响。&lt;/p&gt;
&lt;p&gt;用 Cursor, 总是意识到自己在”用 Cursor”——这条 prompt 会不会很贵?它会不会乱改风格?会不会又自动加测试?我是不是得即时叫停它?&lt;/p&gt;
&lt;p&gt;用 Windsurf, 脑子只想着代码。&lt;/p&gt;
&lt;p&gt;仅此而已, 这就是区别。&lt;/p&gt;
&lt;p&gt;Windsurf 偶尔会工具调用卡死——并发命令时前几个命令永远停不下来, 纯粹是执行引擎的 bug。但说实话, 这种失败我反而不那么恼火。&lt;/p&gt;
&lt;p&gt;因为 Windsurf 的 bug, 至少是”按我说的做时”崩的;而 Cursor 的问题, 是”它自作主张时”还一副得意洋洋。&lt;/p&gt;
&lt;p&gt;结论很简单: 技术可靠但不尊重你, 不如偶尔犯错但始终把你当主角的工具。给我选, 完美但处处掣肘的 IDE 和偶尔磕磕绊绊但高度理解我的 IDE, 我选后者。&lt;/p&gt;
&lt;h2 id=&quot;真正决定代码质量的-其实是模型不是壳&quot;&gt;真正决定代码质量的, 其实是”模型”不是”壳”&lt;/h2&gt;
&lt;p&gt;这点我反应了好几个月才明白: 代码质量, 取决于 LLM 模型本身, 而不是 IDE 包了几层皮。&lt;/p&gt;
&lt;p&gt;Claude Opus 生成代码清晰, 结构合理, 类型严谨, 边界情况考虑周全。只要 prompt 明确, 最多两轮迭代就能搞定需求 (顺便说一句, 再高级的 LLM 也救不了糟糕的 prompt) 。&lt;/p&gt;
&lt;p&gt;Claude Sonnet 虽然快, 但遇到复杂类型系统就有点”掉链子”, 比如 Python 泛型经常出错, 语法也更容易写错。做方案和规划还行, 真要写严谨的工程代码就力不从心。&lt;/p&gt;
&lt;p&gt;所以, IDE 最好的做法其实很简单: 别挡着模型发挥。Opus 就让它自由发挥, Sonnet 也一样。IDE 只需喂好上下文, 搞定工具链, 然后闪开。&lt;/p&gt;
&lt;p&gt;Cursor 偏不, 非得包一堆 prompt orchestration, 上来先按自己”家法”整风格, 补文档, 写测试。新手可能高兴, 老司机只会觉得”原本更高级的模型被 IDE 拖了后腿”。&lt;/p&gt;
&lt;p&gt;所以, 认真做生产代码时我总回到 Windsurf。不是 Windsurf 更聪明, 而是它懂得”放权”——让 LLM 自己发挥, 你能拿到最接近 Opus 上限的输出, 不被 IDE 风格”驯化”。&lt;/p&gt;
&lt;h2 id=&quot;为什么我两个都用-你也可能一样&quot;&gt;为什么我两个都用 (你也可能一样)&lt;/h2&gt;
&lt;p&gt;折腾了几个月, 实验, 吐槽, 复盘, 得出一个很现实的结论:&lt;/p&gt;
&lt;p&gt;探索需求, 理清全局, 搞清各服务关系时, 我用 Cursor——RAG 系统几乎无敌, 哪怕多点心理负担也值, 能快速画清”地图”。&lt;/p&gt;
&lt;p&gt;等到真正动手实现, 注重类型, 风格, 结构一致性时, 我切回 Windsurf——它尊重我项目的规律, 生成代码无缝融入现有体系。&lt;/p&gt;
&lt;p&gt;两家额度都烧光?那就用 Claude Code, 虽然不能增量改, 但遇到架构决策, 复杂推理, 还是很顶用。&lt;/p&gt;
&lt;p&gt;这不是摇摆不定, 而是实用主义。不同阶段, 不同工具。&lt;/p&gt;
&lt;p&gt;不管 Cursor 还是 Windsurf, 都没有绝对”优劣”。它们分别代表两种 AI 编程哲学: Cursor 偏产品化智能, 重 orchestration, 强调全局一致, 强意见;Windsurf 偏模型原教旨, 轻量抽象, 以代码库为准, 少设限。&lt;/p&gt;
&lt;p&gt;你是新手或常做陌生领域, Cursor 的”意见”会帮你。你是资深工程师, 有一套自己惯例和审美, 那些”意见”就成了障碍。&lt;/p&gt;
&lt;h2 id=&quot;理想中的五年后-ide-长啥样&quot;&gt;理想中的五年后 IDE 长啥样?&lt;/h2&gt;
&lt;p&gt;现在正是 AI 编程工具的”热炒期”, 大家都在鼓吹”AI agent”, 把 prompt 当商业机密。Cursor 把 orchestration 全封死, Windsurf 虽然通透点, 但也没完全开源。厂商都希望你把这些黑盒当成”魔法”盲信。&lt;/p&gt;
&lt;p&gt;但热度总会过去。等开发者们更关心效率而非新奇时, 格局必然变。&lt;/p&gt;
&lt;p&gt;最后赢的 IDE, 不会是最聪明的, 而是最”坦诚”的。&lt;/p&gt;
&lt;p&gt;我想要的 IDE 能完全暴露 agent 的 prompt, 不是让你 hack, 而是 UI 里一目了然。你能看到”我传了啥给模型”, “注入了什么上下文”, “加了什么风格指令”, 所有 orchestration 都能自定义。&lt;/p&gt;
&lt;p&gt;项目级偏好, 用户级风格, 甚至 AI 辅助 prompt 调优——我可以直接和 IDE 说”像 Windsurf 一点”, 它就调整 orchestration。&lt;/p&gt;
&lt;p&gt;这不是什么激进想法, 而是对开发者最基本的尊重。我们不是小白, 我们懂 prompt, 我们能驾驭复杂性。别把我们当需要被”扶上马送一程”的用户, 得把我们当工程师, 给我们掌控权。&lt;/p&gt;
&lt;p&gt;最好的 AI IDE, 不会有自己的”意见”, 只会学你的。&lt;/p&gt;
&lt;h2 id=&quot;你该怎么选&quot;&gt;你该怎么选?&lt;/h2&gt;
&lt;p&gt;如果你正在评估这些工具, 最该看的是:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;选 Cursor 的理由:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&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;/ul&gt;
&lt;p&gt;&lt;strong&gt;选 Windsurf 的理由:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&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;/ul&gt;
&lt;p&gt;&lt;strong&gt;两者都用的理由:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作内容复杂, 分阶段&lt;/li&gt;
&lt;li&gt;能接受切换带来的”脑内切换成本”&lt;/li&gt;
&lt;li&gt;重视探索 (Cursor) 和落地实现 (Windsurf) 的差异&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但最重要的洞见是: AI IDE 不该像选语言或框架那样选, 而是像选”合伙人”——它尊重你的专业吗?是减轻还是增加你的认知负担?是放大你的思维还是和你作对?&lt;/p&gt;
&lt;p&gt;因为我们选的不仅仅是工具, 更是”共事伙伴”。最好的搭档, 不一定最聪明, 而是懂得什么时候该带头, 什么时候该随队。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;我还在用 Cursor, 还在为它抓狂, 也还在按时交钱。&lt;/p&gt;
&lt;p&gt;但我现在知道自己买的是什么: 智能很强, 但要时刻”讨价还价”的体验。有时候, 这值得;有时候, 我只想要个安安静静, 老老实实帮我把事办好的工具。&lt;/p&gt;
&lt;p&gt;AI 辅助开发的未来, 不在于造更聪明的 agent, 而在于造更坦诚的伙伴。愿这些工具能暴露假设, 尊重偏好, 让”可控”成为信任的基石。&lt;/p&gt;
&lt;p&gt;在那之前, 我两个 IDE 都开着, 继续为自己想要的代码和 AI 斗智斗勇。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(人类原创, 有 AI 友情润色)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate></item><item><title>为什么AI无法成为工程师</title><link>https://shanechang.com/zh-cn/p/why-ai-cannot-engineer/</link><guid isPermaLink="true">https://shanechang.com/zh-cn/p/why-ai-cannot-engineer/</guid><description>&lt;img src=&quot;https://shanechang.com/_astro/cover.CTct8cc3_1GfOkr.webp&quot; alt=&quot;Featured image of post 为什么AI无法成为工程师&quot; /&gt;&lt;p&gt;上个月, 我让Claude帮我撸了一个客户后台。十分钟后, 漂亮的React组件摆在我面前, API结构清晰, 登录验证在我本地跑得飞快。那一刻我差点以为自己是哈利波特。&lt;/p&gt;
&lt;p&gt;两天后, 我们把这玩意儿给真实用户用, 灾难现场就上演了: API在并发下直接跪了, 登录令牌一半时间在用户操作中间就过期, 数据库查询在我测试数据只有10条时还很快, 真数据一上来, 30秒都查不出来。&lt;/p&gt;
&lt;p&gt;代码美得像艺术品。用?也能用——勉强算是。但本质上, 这就是定时炸弹, 我只能怪自己。需要建筑师的时候, 我却只找了个瓦匠。&lt;/p&gt;
&lt;h2 id=&quot;看起来像摩天大楼-没人敢住&quot;&gt;看起来像摩天大楼, 没人敢住&lt;/h2&gt;
&lt;p&gt;想象一下建一栋摩天大楼。你得有两波人:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;建筑师和结构工程师&lt;/strong&gt;负责设计。他们要琢磨承重墙放哪, 电线水管怎么绕, 什么材料能抗住高空风, 地震来了楼会不会晃歪。还得考虑消防通道, 电梯井, 甚至人怎么进出都要细细盘算。画一大堆图纸, 标一堆标准。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;施工队&lt;/strong&gt;才是真正搬砖的。他们会搅拌混凝土, 焊钢筋, 装玻璃窗。只要给他们详细蓝图, “这儿用几号钢筋, 间距6英寸, 混凝土48小时别动”, 他们就能盖出结实安全的楼。&lt;/p&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;
&lt;p&gt;你真的需要工程师和建筑师。&lt;/p&gt;
&lt;h2 id=&quot;我们把两份工混成了一份&quot;&gt;我们把两份工混成了一份&lt;/h2&gt;
&lt;p&gt;回到软件圈。其实我们也有两种角色, 但现在大家已经傻傻分不清。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;软件工程师&lt;/strong&gt;应该主导系统设计, 琢磨扩展性, 可靠性, 安全性, 可维护性。定规范, 立规矩, 想各种情况: 一万用户同时访问会咋样?部分功能挂了怎么办?数据一致性怎么保证?凌晨3点出bug怎么排查?这些人给出”蓝图”, 决定你的代码到底能不能上线, 能不能扛住大场面。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;程序员&lt;/strong&gt;则负责写代码。他们擅长语法, 算法, 设计模式, 能把需求转成具体实现。只要有好规范和架构, 他们能写出高质量, 维护方便的代码。&lt;/p&gt;
&lt;p&gt;但问题来了: 工程师也得写代码啊!不会写代码的建筑师, 设计出来的楼根本站不住。同理, 不懂代码运行原理的软件工程师, 也架不住”代码打脸”。&lt;/p&gt;
&lt;p&gt;于是, 两份工混成了一份: 大多数公司说”我们招软件工程师”, 其实是招”设计+实现一肩挑”的全能型选手。大家脑袋里画蓝图, 手上撸代码。这么搞了好多年, 竟然还行。&lt;/p&gt;
&lt;h2 id=&quot;欢迎ai程序员闪亮登场&quot;&gt;欢迎AI程序员闪亮登场&lt;/h2&gt;
&lt;p&gt;现在, 大语言模型 (LLM) 能写代码了。我的感受是: &lt;strong&gt;LLM是顶级程序员, 但完全不是工程师。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LLM像那位啥都能盖的施工大哥, 你说”给我做个Todo List”, 它立马给你写好。代码优雅, 结构清晰, 变量名感人, 跑起来贼顺溜。&lt;/p&gt;
&lt;p&gt;但工程的部分全没了:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不考虑负载&lt;/strong&gt;: 用内存数组存Todo, 测着快, 用户一多直接爆炸&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没处理异常&lt;/strong&gt;: 数据库断了咋办?代码八成直接挂&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全感人&lt;/strong&gt;: 有登录验证, 但防不住常见攻击, SQL注入一查一个准&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能盲点&lt;/strong&gt;: 每次都遍历所有用户的todo?等着内存爆炸吧&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运维无视&lt;/strong&gt;: 日志, 监控几乎没有, 出事了只能靠烧香&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码看起来像模像样, 规范也全对, 理想情况下能跑。但一旦遇到真实世界的幺蛾子——并发, 网络断, 奇葩输入, 恶意攻击——它就跟那座没人敢住的摩天楼一样, 外表光鲜, 里头一团糟。&lt;/p&gt;
&lt;h2 id=&quot;这事儿到底意味着啥&quot;&gt;这事儿到底意味着啥&lt;/h2&gt;
&lt;p&gt;以前我觉得”工程师”和”程序员”的区分纯粹是咬文嚼字。现在我明白了, 这区别比你想的要命。&lt;/p&gt;
&lt;p&gt;你让LLM”写个认证系统”, 其实就是让施工队”盖个楼”, 却没给任何要求。比如:&lt;/p&gt;
&lt;ul&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;/ul&gt;
&lt;p&gt;LLM肯定给你盖出来点啥, 样子还挺唬人, 但根本不靠谱。因为, 没人做”工程师”的活。&lt;/p&gt;
&lt;p&gt;举个我司Empath Legal的真实例子。我们帮律师做陪审团筛选工具。我要是让LLM来: “写个API, 分析陪审员问卷, 给出洞察。”&lt;/p&gt;
&lt;p&gt;LLM能写出能用的东西:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解析问卷&lt;/li&gt;
&lt;li&gt;调用LLM分析文本&lt;/li&gt;
&lt;li&gt;格式化结果返回&lt;/li&gt;
&lt;li&gt;代码结构合理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但它绝对不会主动考虑这些:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;业务需求&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问卷50多页, 律师要30秒内出结果&lt;/li&gt;
&lt;li&gt;用户是律师, 一切都得有出处, 不能瞎编&lt;/li&gt;
&lt;li&gt;分析结果必须有确定性, 法庭上要靠得住&lt;/li&gt;
&lt;li&gt;按用量计费, 得严格追踪消耗&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;技术护栏&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;问卷得按章节分块, 不能随便拆&lt;/li&gt;
&lt;li&gt;得有流式输出, 不能让律师30秒看个空白&lt;/li&gt;
&lt;li&gt;需要缓存, 防止重复分析白烧钱&lt;/li&gt;
&lt;li&gt;LLM API偶尔抽风, 异常要兜底&lt;/li&gt;
&lt;li&gt;全链路审计, 合规要查&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;系统设计&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用同步还是异步? (异步, 30秒要求)&lt;/li&gt;
&lt;li&gt;LLM中途挂了怎么办? (幂等重试)&lt;/li&gt;
&lt;li&gt;LLM供应商限流咋办? (排队+背压)&lt;/li&gt;
&lt;li&gt;数据一致性怎么保证? (部分完成咋处理)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LLM能写”通路”, 但如果我不自己做工程设计, 梳理约束, 定架构, 那代码上线一秒钟就翻车。&lt;/p&gt;
&lt;h2 id=&quot;新的分工时代&quot;&gt;新的分工时代&lt;/h2&gt;
&lt;p&gt;这不是说LLM不行——它们太厉害了。但我们得搞清楚它们在干啥。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLM是超级程序员&lt;/strong&gt;: 给定明确需求和约束, 能写出一手好代码。会用模式, 能优化, 能调试, 能写文档。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;人类要做工程师&lt;/strong&gt;: 理解业务, 设计系统, 搭好护栏, 预判边界和异常。要问: “我们到底要解决什么?会出啥乱子?怎么扩展?”&lt;/p&gt;
&lt;p&gt;想开点: 繁琐的底层活可以丢给AI, 我们只用搞定”人性化需求, 系统设计, 架构取舍”这些有趣的活儿。&lt;/p&gt;
&lt;p&gt;但前提是: 你得先做工程师的活。如果只是”LLM, 给我造个X”, 不给上下文, 不提约束, 不画蓝图, 出来的就是只可远观的危楼。&lt;/p&gt;
&lt;h2 id=&quot;跟ai编程工具打交道的正确姿势&quot;&gt;跟AI编程工具打交道的正确姿势&lt;/h2&gt;
&lt;p&gt;我的经验:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;烂提示:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“给我写个SaaS的支付系统”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;优质提示:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“写个支付系统, 满足这些需求:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支持按积分计费 (用户买积分, 功能扣积分)&lt;/li&gt;
&lt;li&gt;必须安全处理并发扣除 (两次API不能都扣成功, 积分只够一次)&lt;/li&gt;
&lt;li&gt;需要账单审计, 方便对账&lt;/li&gt;
&lt;li&gt;支付渠道挂了要优雅降级 (走缓存积分, 记录日志后续补算)&lt;/li&gt;
&lt;li&gt;性能要求: 每次扣积分延迟&amp;#x3C;50ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据库用PostgreSQL行级锁扣积分。操作需幂等, 重试不重复扣。全链路日志方便排查。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;第二种提示才是”工程师干的活”:&lt;/p&gt;
&lt;ul&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;技术方案 (PostgreSQL行锁)&lt;/li&gt;
&lt;li&gt;运维考虑 (可排查)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样, LLM才能写出上线无压力的代码, 因为你给了它”蓝图”。&lt;/p&gt;
&lt;h2 id=&quot;一点扎心的真相&quot;&gt;一点扎心的真相&lt;/h2&gt;
&lt;p&gt;很多老开发, 其实早就习惯了”脑中设计+手上实现”一条龙。每次写代码, 都是先做工程师, 再做程序员, 帽子换得飞快。&lt;/p&gt;
&lt;p&gt;但有了LLM, 编程部分AI包了。你如果不认真做工程师的活, 只会下”魔法提示”, 最后得到的, 是个只在demo里能用, 真环境就翻车的项目。&lt;/p&gt;
&lt;p&gt;扎心的是: &lt;strong&gt;很多自称工程师的, 其实只是程序员&lt;/strong&gt;。代码写得贼溜, 但从没真正搞过系统设计, 要么一直在实现别人的架构, 要么项目本身太小没啥复杂度。&lt;/p&gt;
&lt;p&gt;在LLM时代, 这个区别终于变得重要了。单纯的写代码技能正在被AI取代。真正有价值的是”工程思维”: 理解需求, 设计架构, 权衡取舍, 预判故障。&lt;/p&gt;
&lt;h2 id=&quot;该怎么进化你自己&quot;&gt;该怎么进化你自己&lt;/h2&gt;
&lt;p&gt;如果你正在学编程, 不要只学语法和框架。学会像工程师一样思考:&lt;/p&gt;
&lt;ul&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;/ul&gt;
&lt;p&gt;如果你已经在开发岗位, 注意分清自己干的是哪一头。只是对着规范写功能?那你就是程序员, 未来LLM会把这工作抢走。赶紧加强工程能力: 系统设计, 需求分析, 架构思维。&lt;/p&gt;
&lt;p&gt;如果你在面试招人, 别再考”能不能写FizzBuzz”, 改问”设计个URL短链系统, 能扛1000请求/秒, 故障咋办?”写代码的活儿越来越自动化了, 剩下的全靠脑子。&lt;/p&gt;
&lt;h2 id=&quot;最后的笑点&quot;&gt;最后的笑点&lt;/h2&gt;
&lt;p&gt;说个小秘密: 这篇文章, 其实是我和Claude合写的。不是我不会写, 而是Claude作为”写作程序员”比我还专业。我给它核心思路, 写作架构, 风格要求 (要有建筑比喻, 要举案例), 它写得比我快还顺。&lt;/p&gt;
&lt;p&gt;Claude写了优美的段落。但那些最关键的决定——讲什么, 怎么安排, 用什么例子, 读者能不能get到——全是”工程师”我来定的。&lt;/p&gt;
&lt;p&gt;未来的软件开发, 不会是”全自动无人区”。而是人类做擅长的事——理解复杂需求, 做取舍, 设计健壮系统;AI做擅长的事——把规范变成精美, 可靠的代码。&lt;/p&gt;
&lt;p&gt;但前提是: 你得记得自己是工程师。否则, 盖出来的, 还是那种没人敢住的高楼。&lt;/p&gt;
&lt;p&gt;这种故事, 咱都不想再听第二遍吧。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;你用AI编程工具踩过什么坑?你发现自己开始更多地思考工程问题, 还是还停留在”写个X就行”?欢迎留言分享——翻车现场往往比成功故事更有料!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(本文由人类原创, 部分内容参考AI协助优化。)&lt;/em&gt;&lt;/p&gt;</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate></item></channel></rss>