记得 2021 年夏天,我接手了一个维护了七年多的老服务。代码里有一行注释,原话大概是:// TODO: 这里偶发超时,很奇怪,别管它,重试就行。那行注释当时救了我——线上确实偶发超时,加个重试确实能扛过去。但三个月后我又恨透了它:没人知道它为什么奇怪,没人记录过试过哪些方案,谁都不敢动那段逻辑。它像一个没有说明书的炸弹,你知道它在,但不知道它什么时候响。
后来我慢慢想明白一个道理:工程里真正稀缺的不是把问题修好,因为很多问题根本没法在当前条件下修好。真正稀缺的是给那些尚未解决的问题留下清晰、可追溯、对后来人有用的标记。Marking solutions to problems that are not solved——给未解决的问题标记解决方案。这句话表面看有点矛盾:问题都没解决,哪来的解决方案?但恰恰是这种“承认未解决,同时记录下所有尝试过的路径、绕开的坑、暂时奏效的规避手段”的思维,才是一个团队或者一个人真正成熟的标志。
这篇文章不是讲某个具体框架的用法,而是讲一套我实践了几年、已经在多个团队验证有效的工作纪律。它适合后端开发、技术负责人、文档维护者,也适合任何一个需要跟“悬而未决”共处的人。读完之后你可以直接把那套注释模板和清单结构拿去用,不用改装。
1. 为什么说“标记解决方案”比“解决”更重要
- 在任何真实系统里,未解决都是常态 先聊一个反直觉的结论:一个正常运转的中型系统,同时存在的“已知但未根治”的问题,少说也有几十个。它们可能是某个偶发内存泄漏、某个只在特殊网络环境下出现的竞态、某个业务规则从来没跟产品对齐的模糊地带。你不可能把它们全部关掉,也很少有人能把它们全部关掉。
这不是团队懒散,而是工程常态。一个系统上线三年,如果要求所有已知问题全部根治,只有两种可能:要么系统规模太小,要么产品已经停止迭代。只要业务还在往前走,新功能就会不断暴露新的边界,旧问题就会在特定场景下被重新激活。换句话说,接受“有些问题暂时解决不了”,不是妥协,而是专业成熟度的体现。
但接受未解决,不代表放任自流。问题可以悬置,认知不能悬置。真正可怕的是:一个问题悬置了三个月,团队里没有留下任何关于它的上下文,某天它突然引爆,所有人一脸茫然。未解决的问题就像一个长期存在的盲区,你允许它存在,就必须为它建立相应的可见性。
- “解决”与“标记”的三个本质差别 我花了不少时间才意识到,“解决”和“标记”是两种完全不同的动作,不能混为一谈。
| 对比维度 | 解决问题 | 标记未解决问题 |
|---|---|---|
| 时间属性 | 终局性,问题被关闭 | 过程性,记录探索轨迹 |
| 作用对象 | 问题本身 | 我们与问题的关系 |
| 价值兑现 | 当前时刻立即生效 | 在未来每一个接手人身上兑现 |
| 失败代价 | 问题没修好,白费时间 | 标记不全,后人重复踩坑 |
| 对能力要求 | 需要技术判断力 | 需要诚实与整理能力 |
解决问题讲究“找到一个能跑的方案”,面向当下;标记未解决问题讲究“把局面说清楚”,面向未来。我见过太多团队为了证明自己“在做事”,拼命往未解决的问题上堆方案,结果越修越乱。反而是一些慢下来、认真把问题边界、失败路径、当前兜底逻辑写清楚的团队,后面越走越顺。他们的核心竞争力不是“修得快”,而是“在没修好的情况下,依然让系统可控、让团队可接力”。
- 标记的三个核心价值:止损、接力、信任 止损最好理解。两个不同的人在不同年份查同一个问题,各自花费两个星期,最后各自得出同一个“没解决”结论——这种浪费在我见过的项目里几乎每天发生。一份好的标记,等于把“已经证明走不通的路”提前画出来,后来的人不用再拿脚去丈量。
接力是标记最有魅力的地方。很多未解决问题,并不是永远无解,而是解法依赖系统演进到某个阶段。比如一个底层框架的 bug,可能因为业务规模没到而不值得修;但当你把标记写清楚,三年后系统重构、流量翻倍,下一任接手的人看到标记,会瞬间明白“原来这个问题在这里等着”,他甚至能直接把方案接上。标记就是留给未来的接力棒。
信任则更微妙。作为 leader,我最怕的不是下属说“这个问题我没搞定”,而是他默默改了一个 workaround,既不写注释也不提 issue,让问题彻底沉入海底。反过来,当一个同学在产品群里说“这个点目前无法根治,复现条件如下,我已经上了规避方案,后续跟进的 issue 在这里”,他不仅没有显得无能,反而让我觉得这个人极其靠谱。
提示:我这些年 review 文档和代码,最看重的不是“这段代码解决了什么”,而是“这段代码里有没有诚实地标记出那些它没有解决的问题”。
2. 从TODO注释到未决问题档案:我一直在用的一套标记法
- 最常见的标记灾难 很多人其实已经在做标记,最典型的就是
TODO、FIXME、HACK这三个标签。但我在代码评审里看到的常态是:
// TODO: 优化性能 // FIXME: 这里会崩 // HACK: 先这么写吧这种注释写了等于没写。TODO后面挂着“待优化”,一待三年,没人知道要优化什么、优化到什么标准;FIXME后面写着“会崩”,但没写怎么复现、崩在哪一行、有没有临时兜底;HACK更是重灾区,完全没有上下文,三个月后连作者自己都看不懂当初为什么要 hack。
我一直认为,注释不是给自己看的,是给六个月后那个半夜被报警叫起来的人看的。如果一条注释没有包含“现象、尝试过的路径、失败原因、当前规避手段、下一步方向”,它就不算一个合格的未决问题标记,它只是代码里的一句情绪宣泄。
- 我推行的“五要素”注释模板 后来我在团队里推行了一套注释结构,规则不复杂:凡是确认“暂时无法根治”的问题,必须按下面这种格式写透。
// @UNRESOLVED: scrap-timeout-review-20250315 // 现象: 抓取任务偶发超时,约 0.2%~0.4%,集中在大促高峰时段 // 已尝试: // - 客户端超时 5s -> 15s,无改善,排除应用层读超时 // - 连接池从 20 扩到 50,无改善,排除连接数瓶颈 // - 切换到备用 DNS,无效,排除单点 DNS 故障 // 当前规避: 失败自动重试一次 + 读缓存降级,线上可接受 // 下一步: 怀疑与网关同签名并发转发有关,需要压测复现 // 状态: OPEN / 负责人: 张工 / 最后更新: 2025-03-15不要小看这个结构。它的核心不是那几个字段,而是强迫写注释的人过一遍自己的思考链条:现象是什么?我到底试过什么?失败在哪一步?我现在靠什么兜底?下一步往哪走?
这五个问题一旦捋清楚,哪怕这个问题一辈子不修,后来的人也不会再在这个坑里浪费超过半天。我把这套东西叫“未决问题档案”,它不需要什么重量级系统支撑,它就是一套纪律:你承认这个问题还开着,就必须同时提交一份“关于这个问题的暂存知识”。
- 注释与issue的分工 注释是给代码旁边的人看的,但它不应该成为唯一的标记载体。真正的状态管理,还是得到 issue 或缺陷管理平台上去做。我在团队里定过三条分工规则:
| 容器 | 放什么 | 更新频率 | 谁负责 |
|---|---|---|---|
代码注释@UNRESOLVED | 现象、尝试、兜底、下一步 | 每次排查有结论就更新 | 最后触达问题的开发者 |
issue 标题[未解决] | 长期状态、版本影响范围、负责人 | 每两周过一遍 | 模块负责人 |
| 团队知识库文档 | 横跨多个服务的复杂问题全景图 | 月度复盘同步 | 技术负责人 |
注释负责“贴地气”,它必须出现在第一现场,让阅读代码的人立即感知风险;issue 负责“管状态”,确定优先级、排期、负责人;知识库文档负责“串全景”,当一个未解决问题牵扯多个服务时,单独的注释和 issue 都装不下它。
三条铁律需要特别记住:第一,注释里必须带 issue 编号,否则注释会变成孤岛;第二,issue 标题用[未解决]前缀统一标识,跟普通 bug 区分开,筛选时一眼就能看到;第三,每次有人对问题做了新尝试,必须回到原 issue 追加记录,而不是另开新坑。开新 issue 而不关联旧 issue,等于亲手切断了接力棒。
3. 一个真实案例:那个让我写下这篇博文的偶发超时
- 我第一次踩到坑,只花了十分钟 回到开头那个老服务。我当时刚接手,线上报警说抓取任务偶发超时,频率不高,但每天总有那么几次。我翻了代码,看到那行“别管它,重试就行”的注释,照做了——加了重试逻辑,观察了两天,报警确实少了。
整个过程也就十分钟。那行注释在当时确实“救”了我,让我不用从零开始排查。但注意,它只是告诉我“怎么回避”,没有告诉我“为什么回避”。我当时觉得足够了,毕竟线上稳了。
- 两个月后同事踩坑,痛苦了三天 两个月后,抓取任务的量级翻了一倍,光靠重试扛不住了,超时开始连续报警。另一个同事去排查,他没有我这么幸运——他相信那句“别管它”,但重试已经不灵了,所以他必须自己从头查。
那三天我看到他干了什么:先抓日志,发现超时集中在某几个上游节点;然后怀疑 DNS,换了库,无效;再怀疑连接池,扩了线程,无效;最后怀疑操作系统层连接复用,调了半天参数,依然无效。他每一步都踩在我们当时已经踩过的路上,但没有文本告诉他“前面有人走过了”。
更讽刺的是,他发现代码历史记录里,两年前就有人为这个问题做过一次很深的排查,结论写在一封已经没人翻的邮件里。那封邮件明确排除了三四个方向——正是他这三天反复验证的方向。
- 我们后来补上的未决问题档案 这个问题最终也没有被“根治”,我们找到的仍然是一个规避方案,只是比“无脑重试”更稳:失败重试一次 + 读缓存降级 + 对上游做熔断。但它从“地雷”变成了“可控风险”,关键变化就是我们补了一份真正的标记。
## [未解决] 抓取任务偶发超时 - 首次记录: 2023-11-02 | 最后更新: 2025-03-15 | 负责人: 张工 - 现状: 未彻底定位根因,当前依赖 重试+降级+熔断 三件套兜底 - 复现特征: 高峰期与上游节点网络抖动高度相关,概率 0.2%-0.4% - 已尝试且失败: 1. 客户端读超时 5s->15s(2023-11,陈工)无效 2. 连接池 20->50(2023-12,陈工)无效 3. 备用 DNS(2024-02,李工)无效 4. 操作系统层连接复用参数(2024-05,王工)无效 - 已排除方向: 应用层超时配置、连接池容量、DNS 单点、连接复用 - 当前最可疑: 网关层对同签名请求并发转发时的连接建立风暴 - 下一步动作: 大促前做网关层压测复现,若复现则推动网关团队介入 - 状态: OPEN这份档案贴在我们 wiki 的“未决问题”区,三个月里被引用了十几次。每次有人看到相关日志,都会先去翻它,而不是重新发明轮子。这就是标记的价值:它不解决问题本身,但它解决了“被同一个问题反复消耗”的问题。
4. 团队层面的未解决问题管理:从会议记录到知识库
- 最昂贵的流逝:讨论到一半的方案 代码注释解决的是“已知风险”,但团队里还有一类更隐蔽的损失,发生在会议和即时通讯里。
我参加过太多这样的会:大家在白板前讨论一个棘手问题,提出了四五个方向,分析到一半发现依赖某个前置条件,于是约定“下周三再对一下”。然后下周三来了,人到齐了,但没人记得上次到底聊到哪一步、谁负责验证哪个方向。上一次讨论产生的所有中间结论,全部归零。
IM 群聊更夸张。一个问题在群里反复拉扯,中途还穿插着“这个 bug 怎么复现”“我这边环境起不来”“老板刚进了个新需求”。一个小时后,结论淹没在 300 条消息里,过两天连当事人自己都翻不到。我们把这种情况叫“讨论即流失”:讨论了,但没留下任何可以被检索的东西。
- 一份轻量的未决问题清单如何运转 后来我们做了一件很小但极有效的事:建了一份“未决问题清单”,放在团队知识库固定位置,任何人在会上或群里讨论问题时,只要确认“当前无法根治”,就顺手在上面加一行。
字段设计如下:
| 字段 | 要求 |
|---|---|
| 问题摘要 | 一句话说清现象,不带背景故事 |
| 首次记录 | 日期 + 提出人 |
| 最后更新 | 每次追加必须改动这个日期 |
| 已尝试路径 | 哪怕失败也要写,失败比成功更有指导性 |
| 当前卡点 | “卡在什么上”比“卡在哪里”更有效 |
| 当前兜底 | 线上/当前阶段靠什么扛着 |
| 负责人 | 必须有人认领,哪怕只是维护记录 |
| 下一步动作 | 没有下一步动作的问题,视作非紧急备忘 |
这份清单不需要做成系统,用在线表格或者文档就行。关键是两个使用纪律:第一,任何没有“当前兜底”的问题,必须升为紧急项,因为说明团队正在裸奔;第二,任何超过一个月没有更新且没有负责人的条目,直接归档,不要让它继续污染清单。
- 防止标记本身腐化 标记也是一种资产,资产不维护就会贬值。我最怕看到的是清单建了三个月,里面躺着二十条记录,但没有一条进过周会讨论。这种清单很快会变成另一种形式主义的装饰品。
我们的维护节奏很固定:每周五下午的技术复盘会上,拿出清单,逐条过一遍。过的时候只问三个问题:这条有没有新的进展?当前兜底还成立吗?负责人有没有变化?没进展的条目不会被批评,但一定要有人能当场说明“为什么还没进展”。如果说不出来,说明这条标记已经腐化,应该重新评估优先级或者直接归档。
从实践效果看,这份清单最大的贡献不是“解决了问题”,而是把团队从“反复讨论同一个问题”里解放出来。任何新成员入职,先读一遍未决问题清单,他对系统风险的理解速度,比读两个月代码还快。
5. 假标记与低质标记:比不标记更危险
- 三类常见假标记 标记是好事,但我也见过不少“假标记”,它们比不标记更坑人。
第一类是位置标记。“这里有坑”“这里有问题”“这里别动”——只告诉你有石头,不告诉你石头长什么样。这种标记最大的问题是制造恐惧:后来的人看到“别动”两个字,连旁边的正常代码也不敢动了。我见过一段逻辑因为一行“这里很危险”的注释,三年没人敢重构,最后成了整个系统里最陈旧的角落。
第二类是情绪标记。“求求了谁来修一下”“这个 bug 真的离谱”“我不行了你们谁有空看看”——这种话在代码注释和工作群都经常出现。它不是记录,是情绪宣泄,对后续排查毫无帮助。
第三类是过期标记。半年前写了一条@UNRESOLVED,记录了当时的现象和兜底方案,但半年里系统重构过、依赖升级过、数据量翻倍过,这条标记已经完全不准确了。更危险的是,后来的人看到标记写着“有兜底”,就放心地没有继续排查,结果兜底方案早就失效了。过期标记等于给后人提供一个“看起来安全的假象”,比没有标记更可怕。
- 标记也会说谎:把“绕过”写成“解决” 还有一种特别隐蔽的污染,就是把 workaround 说成 solution。我见过不少代码注释,写的是“解决了偶发超时问题”,实际做的是 catch 异常后静默重试。这两者的性质完全不同:解决是问题消失,绕过只是把问题藏到更深的地方。
我后来在团队里立了一个规矩:凡是用了重试、降级、熔断、兜底数据这类手段,注释里不允许写“解决”这两个字,统一写“规避”。词汇上较真是有意义的,因为它强迫每个人诚实面对:“我没把它修好,我只是让它暂时不炸。”这个认知一旦模糊,团队就会对线上的真实风险失去判断。
- 我判断标记是否合格的三条标准 在代码评审和文档 review 里,我判断一条未解决标记是否合格,只看三件事。
第一,一个完全不了解前情的新人,能否靠这条标记判断出“下一步可以做哪个动作”?如果看完标记你依然不知道是该去压测、还是该去查日志、还是该去找某个团队,那这条标记的信息密度就不达标。
第二,失败路径是否被诚实记录?成功的尝试当然有价值,但对后续排查而言,失败的路径往往更值钱。“试过 DNS,没用”这几个字,能帮后来的人省下一天。很多低质标记只写“我在排查中”,不写“我排除了哪些选项”,这是对失败经验的浪费。
第三,有没有明确的“当前状态”和“最后更新者”?状态标记了 open、pending、stalled 都行,但不能缺失。最后更新者的名字则决定了一条标记在关键时刻是否可以找到活人问清楚。失去活人背书的标记,早晚会腐化成一句流言。
6. 把“标记思维”挪到个人复盘和团队文化里
- 每周复盘里的未决问题区 这套思维不只适用于代码,我后来把它搬进了个人复盘和工作日志。
以前我写周报,习惯只写“完成的三件事”,没完成的要么不好意思写,要么觉得下周再说。后来我改成:每周末尾固定留一个区块,叫“本周未决问题”。在这里写的不只是没做完的任务,还包括没想通的问题、尝试过但没奏效的方案、停留在猜想阶段的方向。
这个改动带来一个很实际的好处:我的周报不再只是“成就清单”,它变成了一本可以回溯的思维流水。三周后还能看到自己当时卡在哪个判断上、走过了哪条弯路,这种记录对个人成长的帮助,远大于只记录成果。
- 承认“没解决”不会让你显得无能,反而建立信任 很多同学不敢标记未解决问题,是怕暴露自己的局限。我刚带团队的时候也有这个误区,总觉得说“这个问题我搞不定”会被质疑能力。直到后来我想清楚一件事:评估一个工程师水平,看的不是他从未失败,而是他在失败和未解决的问题面前,能不能保持判断力、能不能把败局整理成对团队有用的信息。
敢于把“我没解决、我试过什么、我卡在哪”写清楚的人,通常才是真正理解系统的人。反而不写的人,让问题一次次在暗处吞噬团队的时间,那个代价是巨大的。
- 如果只做三个起步动作 如果你读完这篇文章,想改变现状,但又不想立刻搞一整套流程,那从这三个动作开始就够了。
第一,今天就在代码里找一个TODO或者FIXME,把它补成完整的五要素格式。你不需要一次改完所有,改一个就足以感受到两种注释之间的差别。当你看到一条写清楚“现象、已尝试、兜底、下一步”的注释时,会觉得整个人都踏实了。
第二,在团队文档库里建一个“未决问题清单”,就按上面那张表结构。拉两个最让你头疼的线上问题填进去,下次周会花十分钟过一下。这份清单不需要多漂亮,能跑起来就行。
第三,从下一次讨论会开始,安排一个人专门记录“悬而未决项”。会议结束时,念一遍“今天有三个问题没解决,分别卡在哪、谁负责下一步”,你会发现会议效率直接提升一个档次。
我自己在这几年里最大的感触是:一个团队真正的底气,从来不是“所有问题都解决了”,而是“每个没解决的问题都有一个清晰的标记”。那些标记见证了我们的反复尝试,也提醒着后来的人别重复踏入同一条河流。深夜加班排查问题时,如果翻代码能看到前人的一份完整档案,真的会感觉有人在黑暗中替你举了一盏灯。