一个永远收不了尾的计划
去年底我开始搭一套计划执行系统——把项目拆成任务队列,按优先级逐步推进。架构不复杂:一个 YAML 文件定义任务列表,一个调度脚本按顺序执行,每完成一个任务就推进到下一个。
听起来很常规,但跑起来之后我发现了一个系统性的问题:队列里有任务执行了三轮还在"优化中",整个计划始终无法收尾。 周一早上我对着任务列表,看到"优化数据处理模块"状态栏写着"执行中",旁边有个小备注"P99 延迟从 800ms 降到了 350ms,还需要再压"。周二再看,变成了 280ms,备注变成了"试试换个序列化库可能还能再降 50ms"。周三,320ms——换了库反而更慢了,于是又滚回去,继续调参。一个本该两小时搞定的模块,因为反复优化拖了三天。后面五个任务全在等着,整个计划卡在第一个节点动弹不得。
这不是某个人的拖延,不是 AI agent 的"沉迷",而是一个结构性陷阱。当你完成一个任务时,总有一些"可以更好"的方向冒出来——性能可以再提 20%、代码可以再精简几十行、测试可以再补几个边界 case。每一个建议单独看都合理,但它们合在一起的效果是:任务永远无法被标记为"完成"。
根因:缺少"完成"的定义
我复盘了那些卡住的任务,发现一个共性:它们都没有明确的完成标准。 任务描述往往是这样的:
- "优化数据处理模块"
- "完善错误处理"
- "提升接口响应速度"
这些描述只说了方向,没说做到什么程度算完。执行者在面对模糊目标时,自然倾向于"做到更好"——因为"更好"没有上限,优化没有终点。这个问题的根源有两层:语言层面的模糊性——"优化""完善""提升"这类动词天然没有边界,从 800ms 到 200ms 是提升,从 180ms 到 179ms 依然是提升,没有锚点就没有终点;心理层面的损失厌恶——已经花了三小时,现在停下来意味着"承认这三小时还不够完美",于是"再改半小时"变成了一直改下去,经济学里叫沉没成本谬误。
这两层叠加的结果是:你以为是执行力的问题,其实是定义的问题。 没有在计划阶段回答"什么是完成",执行阶段就只能凭感觉判断,而凭感觉的结果几乎一定是"还能再改改"。
一个更惨的真实案例
在 finish gate 机制出现之前,我的项目里发生过一个典型案例,至今记忆犹新。
任务是"重写配置加载模块"。原始模块用了 Python 的 configparser,功能够用但代码有些陈旧。任务描述写着"重写配置加载,使用更现代的方式"。执行者花了第一天搭好了新模块,用 Pydantic 做验证,用 TOML 替代 INI 格式,写了 40 个测试 case。到这为止一切正常——功能覆盖了旧模块的所有场景,测试全绿。然后"优化"开始了。第二天:觉得 TOML 的嵌套结构不够优雅,改成 YAML。随之而来的是重写所有测试,因为测试数据格式变了。第三天:觉得 Pydantic 的错误提示不够友好,自己封装了一层,加了中文错误码映射表。这个功能其实没人要求。第四天:发现 YAML 库在处理某些特殊字符时有安全隐患,又换回 TOML——绕了一圈。第五天:觉得配置项应该支持环境变量覆盖,加了一个 env_prefix 功能。这个需求确实合理,但它完全不在原始任务范围内。
五天过去了,新模块比旧模块"好"了很多,但任务始终没有关闭的迹象。因为每完成一个改进,总会冒出下一个"应该也顺便做了"的想法。最终这个任务花了整整一周才被我手动强制关闭——不是因为做完了,而是因为我实在等不下去了。一个"重写"任务,预算一天,实际花了一周。而同一周里,原本排在它后面的六个任务一个都没启动。
设计:finish gate——强制定义完成条件
经历了这些之后,我给系统加了一层机制,叫 finish gate:每个任务在进入执行队列之前,必须定义一组可量化的完成条件,当条件满足时系统判定任务已完成,不再接受新的优化请求。加上 finish gate 后,"优化数据处理模块"变成了:
- id: optimize-data-pipeline
desc: 优化数据处理模块的性能和稳定性
finish_gate:
- metric: p99_latency
condition: "<= 300ms"
verify: "curl -w '%{time_total}' localhost:8080/api/process"
- metric: test_coverage
condition: ">= 80%"
verify: "pytest --cov=src tests/"
- metric: error_handling
condition: "all_known_exceptions_covered"
- metric: code_review
condition: "approved"
现在"完成"有了客观标准——P99 延迟到了 280ms?达标了,关掉;测试覆盖率 82%?达标了,不再加 case。finish gate 的三条设计原则:可判定性(条件必须可机器校验或人工快速确认)、完整性(覆盖"不做就不算完成"的硬性要求,不含锦上添花项)、最小化(3-5 个条件,太多等于没有条件)。
失败案例一:过早关闭的代价
finish gate 刚上线时,我踩了一个坑:任务是"编写用户认证模块文档",gate 只要求"覆盖所有公开接口"和"包含至少 2 个使用示例"。任务很快达标并被系统自动关闭。但两天后,有新同事发现文档里缺少错误码对照表——他收到 AUTH_4003 后完全不知道什么意思,翻遍了文档都没找到,最后花了两小时翻源码才弄明白。
问题出在哪?finish gate 定义得太窄了。 "覆盖所有公开接口"只保证了接口文档的存在,没保证文档的实用程度——我把"必须完成"的条件(错误码表)漏掉了,却把"可以延后"的条件(更多示例)当作了门槛。教训:gate 里的条件必须是"不做就不算完成"的硬性要求,而不是"做了更好"的优化建议。 混淆两者,要么导致过早关闭(漏掉必要条件),要么导致无限打磨(把增强建议也塞进 gate)。
失败案例二:把优化建议塞进 gate
另一个方向也翻车过。有一个任务"重构日志模块",吸取了前面的教训,把 gate 定义得很全面,七个条件:
finish_gate:
- "所有日志调用迁移到 structured logging 框架"
- "支持 JSON 和纯文本两种输出格式"
- "支持日志级别动态调整(无需重启)"
- "性能开销不超过旧模块的 120%"
- "添加日志采样功能"
- "支持按 request_id 链路追踪"
- "编写完整的迁移指南文档"
执行者花了两天完成了前四个,后三个全卡住了——日志采样需要设计新算法,链路追踪需要和上游服务协调,迁移指南要等所有模块迁移完才能写。结果:一个本该两天搞定的任务,卡了一周多,心态崩了——"永远有新条件冒出来"。这次的教训正好相反:不要把"理想状态"当成"完成标准"。 把增强功能塞进 finish gate,本质上又回到了无限打磨的老路。
迭代:finish gate + deferred list
吸取两边的教训后,我引入了组合方案:finish gate + deferred list。每个任务有两个部分:finish gate 是硬性完成条件,只放"不做就不算完成"的底线要求;deferred list 是优化建议,记录但不阻塞,留给下一个迭代周期——存放"做了更好但不必现在"的增强项。修正后的用户认证文档任务如下:
- id: auth-module-docs
desc: 编写用户认证模块文档
finish_gate:
- "覆盖所有公开接口"
- "包含错误码对照表"
- "包含至少 2 个使用示例"
deferred:
- "补充 OAuth 流程的详细步骤图"
- "添加多语言 SDK 示例(Python、Go、JS)"
- "制作 API 调用速查卡片(PDF 版)"
日志模块重构同理:finish gate 只放迁移框架、格式支持、性能基准三条硬性要求,其余"日志采样""链路追踪""仪表盘"全部进 deferred list。这个组合方案让计划的节奏感变好了——以前是"一个任务磨到完美再进下一个",现在是"每个任务做到够用就推进,定期回头批量优化"。在 AI agent 的场景下,这个机制尤为重要。Agent 没有人类的"累了不想干了"的止损机制——给它一个模糊目标,它可以无限生成"改进建议"然后逐个执行。finish gate 就是给 agent 装上了一个"够了,停下来"的开关。
下次我会怎么做
回头看这段经历,如果我从头开始设计这个系统,我会这么做:
1. 第一天就要求每个任务带 finish gate,零容忍。 缺 gate 的任务不进队列。这不是官僚主义,这是纪律。大多数情况下,花两分钟问自己"我怎么知道这个做完了"就足够了。回答不上来,说明任务定义本身有问题,需要先拆细。
2. gate 条件在计划评审时就确认,而不是执行时临时想。 计划评审的核心问题应该是"你怎么知道它做完了"——答不上来,回去改描述。gate 条件是计划的一部分,不是执行的附属品。3. deferred list 从一开始就设好,不要幻想一轮迭代能把所有事做到完美。 承认"够用就行"比假装"这次一定做全"更务实。每个任务写 finish gate 的同时,顺手把脑子里的"还想做"写进 deferred list;每个迭代周期开始时再扫一遍,挑出 ROI 最高的 2-3 条升级为新任务的 gate 条件。这样 deferred list 是一个活的优化池,而不是没人看的垃圾箱。
4. 对"优化型"任务特别警惕。 如果任务的关键动词是"优化""完善""提升""改进",几乎一定需要 finish gate,否则它就是无限打磨的温床。更激进的做法是:所有任务都要求 gate,不区分类型。因为即使是"实现 XX 功能"这种看起来明确的目标,也可能因为"顺便重构一下周边代码"而悄悄膨胀。
写在最后
不是所有优化都该马上做。
这个道理听起来简单,实践起来却需要刻意为之。因为在执行的过程中,"再改改"的诱惑永远存在——而且每一个"再改改"在孤立看的时候都合理。正是这种"每个都合理,合起来要命"的局部最优,让无数项目死在原地打磨上。
finish gate 不是要降低质量标准,而是要给质量一个可落地的定义。把"什么算完成"提前说清楚,把"可以更好但不必现在"的东西放进 deferred list,定期回头批量处理——你的计划才能跑起来,你的系统才能迭代前进,而不是在一个任务上打转,把热情和 momentum 全磨光。
说到底,完成比完美更重要。先把东西交出去,再根据真实反馈来改进。这不只是工程方法论,也是一种对待复杂系统的健康态度。