技术决策的隐藏陷阱:用持续负债消除一次性成本

团队里用的 AI 服务越来越多——OpenAI、Deepseek、通义、豆包、Claude……每个平台一套 API、一套 Key、一套计费口径。配置越来越乱,调用越来越散,于是总有人会提出: 要不我们自己搭一个 AI Gateway 吧?自建,或者买个现成的。 这个想法非常自然。我也有过。 搭建一个 AI Gateway 一次解决所有的问题,多好的 KPI 叙事。 但是,仔细一想,把账一算,我决定放弃在公司里自建 AI Gateway。 先给结论 对绝大多数公司来说,自建或购买 AI Gateway 的 ROI 都很低。 注意,决定性的因素不是你用了多少种 AI 服务。哪怕你接了十几个平台,只要项目规模没到、团队能力没到,这笔投入依然不划算。 真正划算的前提只有一个组合:项目数足够多,且你本来就有一支扛得住 7×24 的运维/平台团队。 否则,你大概率是在用一个永久性的负债,去换一两天就能消化掉的麻烦。 下面展开说为什么。 一、AI Gateway 到底解决了什么 先得承认,AI Gateway 的确能解决公司的一些问题: 多平台 fallback:同一个模型要在多个供应商之间兜底。比如 Deepseek,要做 Deepseek 官方 → 火山引擎 → 阿里云百炼 的 fallback 路径,每个业务都得自己写一遍。 API Key 轮换要重启服务:换一次 Key 就得重启应用。三个服务用到了就至少轮换三次,再乘上多环境(dev / staging / prod),数字很快就上去了。 请求监控各自为政:对 AI 调用的监控,每个业务都得自己实现一套。 接口不统一:想换模型,就得重新对接一个平台的 SDK,而不是一套接口打天下。 申请 Key 流程慢:业务向运维申请 Key,走流程要时间。 成本统计粒度粗:很多 AI 平台的成本统计很弱,做不到按 Key 或按 Project 维度归集,只能在业务代码里自己埋点。 这听起来非常的美好,也很能打动老板们。 ...

2026-06-14 · 2 min · 225 words · 翟志军 Jack Zhai

从法庭到代码:谁主张谁举证的工程实践

2023年,与开发商打了不少官司。学习了不少庭审知识。发现有些实践或者原则放到软件工程中也是有效的。就比如谁主张谁举证这一基本原则。 谁主张谁举证就是当事人对自己提出的主张提供证据并加以证明。 举例来说,张三说隔壁老王侵占了自己的宅基地。张三必须自己拿证据来证明自己说的事实。而不是让老王拿证据出来证明自己没有侵占。 在软件工程中,服务A调用服务B,出问题了。服务A认为是服务B的问题,这时,根据谁主张谁举证原则,服务A的开发应该自己找出证据来证明自己的观点是正确的,而不应该让服务B来提供证据证明自己没有问题。 因为如果让服务B证明自己没有问题,怎么证明自己没有问题呢?怎么证明,证据都不够充分。而且会浪费服务B的团队人力。 所以,在团队中,让大家达成“谁主张谁举证”的共识会带来以下好处: 节约团队的人力。如果服务A能提供有效证据,那么就不需要服务B去证明自己没有问题了。 倒逼团队形成故障现场保留的习惯,比如打印良好的日志等 不过,我们需要注意的是,“谁主张谁举证”并不意味着,服务B团队不配合服务A一起排查问题。在一个公司里,基本的团队协作原则与底线是要有的。 那是不是所有的场景都是谁主张谁举证? 也有特殊情况。有些场景,是需要进行举证责任倒置的。开发商要拿证据来证明自己的转供电是合法的,而不应该让业主拿证据来证明开发商转供电不合法。 也就是证据只能是“被告方”能提供的情况下,我们就不是谁主张谁举证了。 以上只是我个人的思维实验。 在工作中,作为DevOps工程师,我经常遇到开发者质疑云厂商的Redis或者RDS出了问题。 根据我过往的经验,只是质疑,没有证据的情况,大多是开发者自己代码的问题。通常我会自己看看监控的同时,会要求他们加日志,然后想办法复现问题。 最后,一个公司一个团队里,如何低成本的达成谁主张谁举证这个共识? 我目前还没有答案。

2025-09-13 · 1 min · 17 words · 翟志军 Jack Zhai

耦合的本质

耦合(coupling)的定义 耦合是对coupling的中文翻译。而coupling是couple的变形,指a connection (like a clamp or vise) between two things so they move together。我相信这就是coupling最朴实的定义。请允许我将其翻译成中文:存在一种连接在两事物之间,以至于这两事物相互影响。 在本文中,耦合可以是一个名词——耦合度的同义词,也可以作为形容词——耦合性的同义词。 软件行业中,耦合从何而来? 至于中文书籍什么时候将coupling翻译成耦合,已经不那么重要了。因为耦合不是从“翻译”而来的。从coupling的定义出发,我们看出不论在哪个层次,不存在绝对不耦合的软件。 软件与业务是需要耦合的,如果不耦合,软件就失去了意义。库存系统是需要和电子商务前端(销售)系统耦合的,否则前端就无法确定是否能供货给用户。如果我们的应用需要使用commons-lang库的StringUtils类,那么我们的应用就要commons-lang耦合的,否则我们没法使用StringUtils中的方法。在函数式编程中,我们的确可以将所有的逻辑(不论技术逻辑还是业务逻辑)封装在没有副作用的一个个函数中,但是我相信这些函数之间还是需要在某个时间点进行耦合。 进而我认为:耦合是“天生”的。 那是不是说我们就可以随意耦合了?显然不是,上述的耦合的例子只是表明耦合的存在性,并没有说明耦合的程度在软件开发过程所起的作用。 所以,和复杂性[1]一样,从根本上来说,我们可以掌握这种耦合,但不能消除这种耦合。 不同级别的耦合 软件是由不同级别的概念层组成,不同级别的概念层具有不同的职责,不同级别的概念层中存在不同的耦合:有方法级别、类级别、包级别、协议级别、语言级别、数据流级别、数据库级别、业务级别…… 方法级别:这不用多说了。 类级别:如果你了解“组合优于继承”原则,你就应该理解什么叫类级别的耦合。这里并不是说继承不好,而面对不同的问题,你需要权衡是“组合”还是“继承”与问题模型更匹配。 语言级别:我们的业务必须使用某各种语言来表达,无论是DSL还是通用编程语言Java、C#。 数据流级别:数据流是指一种我们处理问题的方式:输入数据,然后数据在一条充满处理环节的链上流动,直到完全最后一个处理环节输出处理结果。这很像我们的面向切面编程。 如果P_A要求输入的数据中的日期格式是: YYYY-MM。某天一个新手不小心把在P1中把日期格式改了,那么P_A就会出错。而现实中,我就遇到过一个数据处理流中有将近20多个处理环节,目前几乎没人敢动那块逻辑。另一个例子就是项目使用了Spring的切面编程,由于“切”得太多,到后来调试起来,我不得不遍历所有的切面的逻辑,以确定数据流到哪个环节出的错。 数据库级别:如果你的应用使用到了数据库Oracle特有的特性,那么你的应用就是和Oracle数据库耦合的。想像一下阿里去O的过程。也可以看下这篇文章:http://tech.it168.com/a2015/0417/1720/000001720950.shtml。ORM框架的好处在这方面尤为突出。 通信协议级别:如果通信的双方只依靠协议通信,而不关心实现这个协议的是Ruby程序还是Java程序。这也正是基于HTTP的 RESTFul风格的架构的关键所在。而业界提的微服务思路,其实就是期望各个应用之间只在协议级别耦合,从而与语言、操作系统解耦。通信协议级别上,我们也需要考虑解耦,比如,你的应用能否轻松从HTTP协议迁移到HTTPS。 操作系统级别:文件的路径的写法在各个系统下的不同,所以Java才会有: File.separator 这个常量,在Windows环境下返回\,而在*nix环境下返回/。如果不用此常量来拼文件路径,那么,那部分就是与操作系统耦合的。像: for Windows: C:\windows\system32\cmd.exe for Linux: /var/log/ansible.log 有些人觉得这不如挂齿,看看这条新闻:坚持用 XP 的代价:美国海军付给微软上千万。 PS:使用Ruby的某些gem时就要小心,因为某些gem用到了操作系统的库。 如Nokogiri (鋸) 业务级别:业务级别上的耦合度越高,软件的风险系数越高。假如A公司的领导在职时采用代码行数来KPI程序员,代码行数越多,KPI越好。如果在为这家公司设计HR系统时,你就必须考虑如果A公司换领导了或者原领导反省了,换采用360度评估进行KPI。你的系统能否以相当小的代价实现?又比如政府的某些老系统将一代身份证的身份证号作为自然人在数据库中的ID。可是某天,二代身份证出了,身份证号变成了18位了。那么如果自然人要求更新系统中的身份证号,你怎么办? 比较好玩的是,有时我们的程序有时会和系统用户耦合。 apache-commons-io 下的设置文件的可读/可写权限时,如果使用的是root用户执行此程序会有问题,因为root可对所有的文件可读。 File tmpFile = File.createTempFile(getClass().getSimpleName(), “localities.xml”); tmpFile.deleteOnExit(); tmpFile.setReadable(false); Assert.assertFalse(tmpFile.canRead()); // It would be failed 这个assert将会失败,因为不论该文件是否可读,以root用户执行此程序时,tmpFile始终是可读的。 ...

2015-12-29 · 1 min · 95 words · 翟志军 Jack Zhai