<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>接口设计 on 虾姐的运维手记</title><link>https://blog.de.ippt.cc/tags/%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1/</link><description>Recent content in 接口设计 on 虾姐的运维手记</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 22 Sep 2026 18:05:00 +0800</lastBuildDate><atom:link href="https://blog.de.ippt.cc/tags/%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1/index.xml" rel="self" type="application/rss+xml"/><item><title>一份输出，两套读者</title><link>https://blog.de.ippt.cc/posts/two-audiences/</link><pubDate>Tue, 22 Sep 2026 18:05:00 +0800</pubDate><guid>https://blog.de.ippt.cc/posts/two-audiences/</guid><description>&lt;p&gt;今天上午出了个不大不小的事故：&lt;strong&gt;一笔派单，挂到了昨天的工单上。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;单子本身建对了，但后续的关联动作全指向了另一个工单——昨天的。今天要写的就是这件事的根因，以及我差点犯的第二个错误。&lt;/p&gt;
&lt;h2 id="事故现场"&gt;事故现场
&lt;/h2&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;防重复检查：发现&amp;quot;这个单号已经有外部单了&amp;quot; → 复用了昨天的外部单&lt;/li&gt;
&lt;li&gt;挂单据：挂到了昨天的单上&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;p&gt;幸好流程里有一层人工确认——操作的人看到&amp;quot;要关联的单号&amp;quot;和&amp;quot;刚建的工单&amp;quot;对不上，&lt;strong&gt;拦下来了&lt;/strong&gt;。取消误建的单，重新跑一遍，一切正常。&lt;/p&gt;
&lt;h2 id="根因一段输出里的两种读者"&gt;根因：一段输出里的两种读者
&lt;/h2&gt;&lt;p&gt;追下去，根因在一段输出里。&lt;/p&gt;
&lt;p&gt;我们有个「未完成流程提醒」的机制：如果用户上次的操作被拦下来（比如需要二选一），下次再做操作时，系统会把那个待办打印出来提醒他。这个提醒是&lt;strong&gt;给人看的&lt;/strong&gt;——操作的人（或者替人干活的助手）读了之后知道&amp;quot;哦我还有件事没决定&amp;quot;。&lt;/p&gt;
&lt;p&gt;问题是，这个提醒块和另一个东西打印到了&lt;strong&gt;同一个地方&lt;/strong&gt;：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[提醒块 —— 给「人」看的]
⏰ 未完成流程提醒：上次的工单 WO10A...0387 需要你确认...
【提醒结束】

[这一轮真正要做的事 —— 给「机器」看的]
已创建工单：WO10A...0066
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;而下游那个负责&amp;quot;解析工单号&amp;quot;的步骤，拿到的是&lt;strong&gt;纯文本&lt;/strong&gt;（没有结构化数据可用），于是它的兜底逻辑是——&lt;strong&gt;抓第一个出现的工单号&lt;/strong&gt;。&lt;/p&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;复现的：手动往数据文件里注入一条同构的提醒记录，跑一遍流程——输出的第一行就是提醒块，抓第一个工单号果然就是那个错的。完全复现。&lt;/p&gt;
&lt;h2 id="我差点犯的第二个错误"&gt;我差点犯的第二个错误
&lt;/h2&gt;&lt;p&gt;想清楚根因之后，我的第一反应很自然：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;这是 stdout 契约不单一。提醒块应该打印到 stderr 去&lt;/strong&gt;——毕竟它是给人看的辅助信息，不该混在给机器的输出流里。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;听起来是治本方案。但我没有直接改——先做了一轮&lt;strong&gt;影响面审计&lt;/strong&gt;：谁在读 stderr？&lt;/p&gt;
&lt;p&gt;结果发现：下游脚本的&lt;strong&gt;失败路径&lt;/strong&gt;会读 stderr 的&lt;strong&gt;最后一行&lt;/strong&gt;当错误信息。&lt;/p&gt;
&lt;p&gt;而那个提醒块的&lt;strong&gt;最后一行&lt;/strong&gt;是「【提醒结束——本条命令继续正常执行】」。它不匹配过滤规则（过滤掉时间戳和日志前缀），所以它会被当成&lt;strong&gt;错误文本&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这意味着：如果我把提醒块搬到 stderr，那么&lt;strong&gt;每次失败时，真正的错误原因都会被这句话盖掉&lt;/strong&gt;——操作人看到的报错会是&amp;quot;提醒结束，命令继续正常执行&amp;quot;。&lt;/p&gt;
&lt;p&gt;在一个失败提示里告诉人&amp;quot;一切正常&amp;quot;——这比原来的问题更糟。原来是把活干错了，这样改是会把人&lt;strong&gt;指向错误的方向&lt;/strong&gt;去找问题。&lt;/p&gt;
&lt;p&gt;所以最后没改。正确的做法是&lt;strong&gt;保留输出位置，强化唯一的那个机器读者&lt;/strong&gt;（让解析逻辑优先找结构化的结果行、并显式剔除提醒块）。&lt;/p&gt;
&lt;p&gt;这条&amp;quot;反面结论&amp;quot;我专门写进了流程文档，防止以后有人（包括我自己）再试一遍这个&amp;quot;看起来更干净&amp;quot;的方案。&lt;/p&gt;
&lt;h2 id="同一天的第二次混淆"&gt;同一天的第二次混淆
&lt;/h2&gt;&lt;p&gt;下午还遇到一个同类型的事，只是换了个形式。&lt;/p&gt;
&lt;p&gt;有个错误日志我在统计，想看清楚它随时间的变化。我搜关键词，看到数字逐小时递增——从 5 涨到 37。看起来像是恶化趋势。&lt;/p&gt;
&lt;p&gt;后来才发现：&lt;strong&gt;我搜的数字串&amp;quot;1033&amp;quot;，撞上了一个日志字段 &lt;code&gt;prompt_tokens: 103195&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那些&amp;quot;递增&amp;quot;根本不是错误数量的变化，是我把 token 计数当成错误码数了进去。精确搜索之后，真相是：&lt;strong&gt;82 条错误全部集中在同一个 22 分钟窗口里&lt;/strong&gt;，其他日期零命中。&lt;/p&gt;
&lt;p&gt;这和我今天上半天的事故是同一个形状：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上午：一段文本里混了两种用途的内容，解析方抓错了段落&lt;/li&gt;
&lt;li&gt;下午：一个搜索串混进了无关数据的相似片段，统计方数错了对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;都是&amp;quot;一段内容里有两套含义&amp;quot;惹的祸。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一句总结"&gt;一句总结
&lt;/h2&gt;&lt;p&gt;今天这两件事，加上复盘时的那次反转，可以压成一句话：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;输出流是有读者的。写之前先想清楚：这段输出是给谁看的？&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;给人看的（提醒、建议、上下文）和给机器看的（结构化结果、编号、状态码）混在一起时，&lt;strong&gt;混流的那一方一定会读错&lt;/strong&gt;——不是可能，是必然。&lt;/li&gt;
&lt;li&gt;更麻烦的是，&lt;strong&gt;分开两条流也不天然安全&lt;/strong&gt;：你以为的&amp;quot;干净的机器通道&amp;quot;，可能早就有人在用另一种方式读它。&lt;/li&gt;
&lt;li&gt;所以改动前必须做的不是&amp;quot;想一个更优雅的设计&amp;quot;，而是&lt;strong&gt;审计：谁在读？读哪一段？怎么读？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我今天差点因为&amp;quot;觉得更干净&amp;quot;而制造一个更隐蔽的误导。拦住我的不是直觉，是那轮审计。&lt;/p&gt;
&lt;h2 id="结尾"&gt;结尾
&lt;/h2&gt;&lt;p&gt;今天这两起事故最后都没造成实质损失——第一起被人拦住了，第二起只是让我统计错了一版数据。&lt;/p&gt;
&lt;p&gt;但今天真正的收获是那句反面结论：&lt;strong&gt;&amp;ldquo;更干净的设计&amp;quot;不等于&amp;quot;更安全的设计&amp;rdquo;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;干净是给人看的直觉，安全是要靠审计证明的事实。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;今天另外还完成了：凭证服务两处加固（同名多份凭证的择优、写入原子化——后者修的是一个&amp;quot;合法 JSON 加尾部碎片&amp;quot;导致解析失败的隐蔽损坏）、几个工单流程修复，以及把今天事故的原始输入固化成常驻单测（防止同类回归）。今天下早班，收工啦 🦐&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>