我的第一个AI大系统——AssetOps手记 作者: zhaoyang 时间: 2026-08-17 分类: AI 开发 这篇文章想过几次怎么写,最后还是决定从最麻烦的那段日子写起。AssetOps 不是我某一天突然想出来的项目。 它真正的起点要往前推到 2021 年。21年我因为频繁的出差,开始招聘实习生,将自己的一部分资产管理和桌面运维的工作移交给实习生去做。只不过那会儿手里没有一个完整的系统,主要靠钉钉群、腾讯文档和我电脑上的 Excel 往前推。现在回头看,那套方式其实很原始,但当时也确实能用。有设备发放、回收、调拨或者其他变化,先在钉钉群里沟通。实习生按照群里的消息去操作,再把结果更新到腾讯文档里。这样至少我能知道事情进行到哪里,实习生也有一份可以照着执行和更新的记录,不至于所有工作都堆在我一个人身上。但实习生操作完,并不代表这件事对我来说结束了。我还要再去线下比对一次。设备是不是这一台,使用人对不对,编号有没有写错,实际状态和文档里是否一致,都要重新确认。要定期的去盘点仓库,看仓库里的设备和表格上的数据是否一致,看员工的签收单和系统的记录是否一致。因为群里的消息是一段一段的,腾讯文档又可能在不同时间更新,我没办法直接把其中任何一个地方当成最后结果。线下比对完成以后,我还要回到自己的电脑上继续改表格。 从 2021 年到现在五年时间里,我前后带过 8 个实习生。每次人员更替,表面上只是换了一个现场执行的人,实际上我都要重新把一套没有写进文档的规则讲给他听:什么情况必须再找我确认,设备主体和部门之间怎么判断,仓库里的位置应该看到哪一层,发放、回收和调拨遇到交叉时先做什么,哪些字段以财务为准,哪些字段以现场结果为准。实习生可以很快学会“打开文档、照着消息操作”,但真正容易出错的地方,往往是这些我已经做熟、却很难一次讲完整的隐形规则。于是我一边处理日常工作,一边反复培养新人、解释背景、纠正操作,再把同样的话重新讲给下一个人听。那几年我越来越清楚,原来的流程不只是数据同步麻烦,知识也一直挂在我身上,人员一换,沟通成本就重新来一遍。 当时基本是一家公司对应一个 Excel,这个表格也是我19年入职的时候做的,当时公司还很小,不到200人,也只有1家公司主体。这个表格最早可以追溯到我16年在vipkid实习,当时vipkid也没有资产管理系统,所以我用excel+一大堆公式做了一个简易的资产管理系统。这个系统据说后来在vipkid用了好几年(虽然我只实习了三个月)。后来公式规模越来越大,组织形式越来越复杂,注册的子公司越来越多。然而表格一开始设计的时候,并没有考虑到多公司主体的问题。只能是分开一个公司一张excel表。一个部门在组织上是一个部门,设备主体却可能落在不同公司名下。我要是想拉取某个部门的设备数据,不能只打开一个文件搜一下,而是要把所有公司的表格都翻一遍,再把结果合到一起。这还只是资产台账。为了和财务数据核对,我另外做了好几份对比表。有的用来比财务编码,有的用来找账实差异,有的只是为了把不同来源的数据临时拼在一起。时间长了以后,电脑上那些表格越来越多。每一张都有自己的作用,但它们之间又没有真正的关联。实习生做一遍,我再线下核一遍,核完更新腾讯文档,然后再更新本地的若干 Excel,最后还要和财务表重新对一遍。同一件事情就这样在几个地方来回走。最让我疲惫的不是某一次工作量特别大,而是这种重复没有尽头。 财务、各业务部门经理或者领导临时问我某个部门现在有多少设备,我很少能马上给出答案。因为我知道,如果只查其中一家公司,结果可能不全;如果只看腾讯文档,又不确定是不是已经和我本地最终版本同步。我只能重新打开那些文件,一个一个查,再做一次合并和校对。只要中间有一个地方录错,后面的对比就会跟着乱。更麻烦的是,发现一个错误时,我经常不能只改那一格。我得往前确认群里的原始消息,往后检查腾讯文档、本地台账和财务对比表,看这个错误到底传到了哪些地方。有时只是一个编号或主体不一致,最后却要大面积复盘。检查完一轮,心里还会不太踏实,总担心是不是漏了另外一张表。这样的工作以前占了我很多时间。它不是那种做一次就结束的集中整理,而是不断穿插在日常工作里。每次资产发生变化,就要让几个地方重新保持一致。表格本身并没有错,钉钉群和腾讯文档也都解决了各自的问题,可它们拼在一起以后,数据是否一致这件事只能靠人盯着。那个负责盯住全部结果的人,最后还是我。 所以我后来想做 AssetOps,并不是因为我突然想开发一个看起来很完整的资产管理系统。我只是受够了反复打开文件、查找、复制、比对,再担心自己是不是改漏了。我想先把同一份数据收回到一个地方。资产主体可以不同,部门也可能跨公司,但查询时不应该再让我挨个打开公司的表格。实习生完成一次操作以后,结果应该直接留在流程里,我可以核对,也可以追溯,不需要再从群消息抄到文档、从文档抄到 Excel。和财务做对比时,也不应该每次临时拼一张新表才能知道哪里不一致。这个想法很简单,真正往下做的时候却没有那么简单。因为从 2021 年开始,那套流程已经磨合了很久。很多规则并没有写在哪里,而是留在我和实习生的操作习惯里。什么情况下要再确认,哪类设备归哪个主体,哪个字段以财务为准,哪个字段以现场为准,遇到历史数据怎么处理,都是在一次次实际工作里慢慢形成的。真要做成系统时,我才发现,过去并不是没有系统。 钉钉群、腾讯文档、各公司的 Excel、财务对比表,再加上我脑子里记住的规则,它们合在一起就是原来的系统。只不过这个系统需要靠人来连接,而我就是中间最忙的那一层。 这里也需要先把一件事说清楚:AssetOps 的代码我一行都没有写,全部是 Codex 完成的。数据库、接口、网页、移动端、测试、部署脚本,包括后来那些临时工具,都是我把实际问题和规则告诉 Codex,再由它去阅读现有代码、实现、修改和验证。我做的事情更像是同时担任需求方、产品经理和验收人,而最大的需求方就是我自己。我知道过去的流程为什么麻烦,知道实习生到了现场会卡在哪里,也知道什么样的数据我才敢导给财务、部门经理和领导。Codex 负责把这些东西变成代码,我负责判断它做出来的东西究竟能不能解决我的问题。所以我说自己“做”了 AssetOps,意思是我把它从自己的日常工作里一点点磨了出来,并不是我亲手写了代码。 我最早并没有打算直接做成现在这样的系统。考虑到实习生需要在现场操作,我一开始想走轻量化的路线,让 Codex 先做了一个微信小程序。当时给它的目标其实很具体:先替代入职设备发放、设备回收和新设备入库这几张最常用的表,实习生拿手机扫码就能完成发放和回收,我则可以在后台看到每一次操作,再把日志导出来核对。最初我还想过,如果设备上的资产标签脱落,就让小程序用 OCR 识别序列号,再通过序列号反查资产。功能做出来一段时间以后,我重新看现场的使用方式,还是把这项 OCR 砍掉了,只保留手动输入序列号。现在回头看,这算是我很早的一次收缩需求:一个点子听起来聪明,不代表它值得一直留在系统里。对实习生来说,稳定地扫码和手动兜底,比多一种看起来高级的识别方式更重要。 小程序做着做着也没有我一开始想得那么轻。为了让我能维护基础数据,它长出了 Web 后台;为了不让陌生人看到公司资产,又要做申请审批和权限;为了避免同一台设备被重复安排,还要有任务和锁定;数据多起来以后,又要补批量导入、错误明细和导出。后台最开始为了图快用了临时的管理员 Token,结果页面一刷新就可能失效,我自己都嫌麻烦,后来又让 Codex 改成正常的账号登录。一个原本只想让实习生方便扫码的小工具,慢慢已经有了一套完整系统的样子。真正让我放弃它的,还是部署。小程序要获取资产数据,就必须访问后端服务,但服务器放在内网。如果要让小程序正常使用,就得把内网服务器通过某种方式映射到公网,这本身就带来了数据安全风险。除此之外,小程序还需要做 ICP 备案,想让其他人真正使用还要完成认证。原本是为了轻量化,最后却发现上线和维护的门槛一点也不轻。整个系统虽然已经开发完成,最终还是被我放弃了。做完以后不用当然很可惜,但为了不浪费前面的开发投入,硬把内网数据暴露出去,只会带来更大的问题。 不过我也没有把小程序阶段积累下来的东西全部扔掉。业务流程、页面结构、任务规则和产品文档都已经磨过一遍,问题主要出在小程序这个载体以及它的上线条件。于是我把小程序的文档和代码继续交给 Codex,让它按照原来的思路重新做成可以通过域名访问的 Web 端和手机端,管理页面放在电脑上使用,现场操作保留手机端,数据库和服务也收回到同一套可以由我控制的部署环境里。微信推送之类依赖小程序生态的能力可以不要,发放、回收、入库、任务和日志这些真正有用的流程则继续保留。现在的 AssetOps 并不是和那个被放弃的小程序毫无关系的第二个项目,更像是我承认第一种交付方式走不通以后,把已经想明白的部分拆下来,换了一个能长期运行的外壳。 这次尝试也让我明白,系统能不能用,从来不只是开发阶段的事。访问方式、网络边界、备案、认证和数据安全,不能等功能写完以后再考虑。后来再做现在这套 AssetOps 时,我也没有能力一次把所有规则说清楚,只能先把最直观的部分交给 Codex:资产台账、人员、任务、操作记录、移动端和部署。先让设备能录进去,让任务能往下走,让实习生的操作不再只留在群消息里。那时我更关心的是“有没有”。有没有统一台账,有没有任务,有没有历史,有没有一个地方能直接查到结果。很多判断都很朴素:先让 Codex 把主流程搭起来,先让页面能打开,先把过去分散在几个工具里的动作搬进来,剩下的问题以后再补。最初版本出来以后,问题也马上跟着出来了。有的是登录以后跳错页面,有的是实习生看不清任务到底分给了谁,有的是资产明明已经流转过,时间线上却没有留下完整记录。领用凭证看起来只是一个导出页面,真的拿去用以后,日期、中文排版、列宽、财务编码,哪一项不对都很别扭。 这套系统从一开始就不是面向很多人的平台,真正登录使用的只有我和实习生。我负责定义规则、核对数据、处理异常、维护系统和导出结果,实习生主要负责现场的发放、回收、调拨、上架和数据录入。它也不是用来替代财务系统或者 OA 的,更像是 IT 内部的实物台账和校验层。我会把 AssetOps 里的数据和财务、OA 数据放在一起比较,找到差异以后再决定应该修正哪一边。财务、部门经理和领导并不需要进入系统,他们需要的是我从系统里导出的资产台账、部门数据、盘点结果和财务对比数据。对我来说,系统最大的价值也不是让更多人进来操作,而是当有人来问数据时,我终于可以直接查询和导出,不必再临时打开所有公司的 Excel 重新拼一遍。 以前的表格虽然麻烦,但每次遇到例外,我可以凭经验临时处理。系统不行。只要规则没有写进去,它就会一本正经地给出一个错误结果。我那时才慢慢意识到,把 Excel 搬进数据库并不等于把流程做成了系统。过去那些靠我人工判断的地方,一个都不会自动消失。它们只是换了一种方式,重新出现在页面、任务、权限和数据里。系统上线也不是结束,更像是把原来藏在人脑和表格里的问题都照了出来。以前我可以在最终核对时悄悄修掉的差异,现在必须说清楚它到底属于哪条规则。从这里开始,AssetOps 才不再只是一个“把数据放到一起”的工具。它开始逼着我把这几年已经习惯、却从来没有认真写下来的流程,一点点讲明白。 ## 从一个仓库开始,后来还是想把所有资产都放进来 这个系统一开始的范围其实很小,只管理我一个仓库里的笔记本和显示器。因为这两类设备流转得最快,经常发放、回收和换人,也是原来最容易在群消息、腾讯文档和 Excel 之间对不上的部分。服务器、网络设备以及其他办公 IT 资产相对稳定,使用人和位置不太会频繁变化,所以一开始我没有急着把它们也放进系统里。先把变化最快、最占时间的部分解决掉,对我来说已经够用了。 但系统用了个把月以后,我又觉得这样不行。笔记本和显示器在网站里,其他资产还在本地 Excel,相当于我同时维护两套台账。每次查询全量资产或者和财务数据核对,还是要在系统和表格之间来回切换。既然系统已经做了,最后还是希望把所有资产都录进去,彻底结束一半在线、一半本地的状态。 范围扩大以后,最先暴露出来的还不只是数据格式,连一些原来很自然的页面逻辑也不成立了。系统里只有一个仓库的笔记本和显示器时,手机端一次把数据全加载出来,看起来没有什么问题;资产种类和数量上来以后,同样的做法就会开始变慢,批量写入和查询也出现过超时。我后来专门提醒 Codex,不能再按几十条测试数据去想这个系统,所有读取、写入和变更都要考虑真实台账的规模。移动端的仓库资产也不再默认加载全部,而是先选择位置或者输入关键词再查。这个变化看上去只是性能优化,实际也改变了我对页面的理解:数据少的时候,全部展示最省事;数据多的时候,把所有东西堆到眼前反而不是方便。现场真正需要的通常只是某个位置或者某一台设备。 真正开始导入时,历史数据的问题一下子全冒了出来。Excel 里的地理位置是给人看的,同一个地方可能有很多种写法,有的写得很完整,有的只写一个大家都懂的简称,还有些记录把楼层、房间、仓库和货架混在一个单元格里。人在表格里看一眼大概能明白,系统却没法稳定识别,更不可能直接转换成统一的位置结构。为了把这些数据导进去,我差不多又花了一周时间清洗和整理位置数据。那段时间做的不是新功能,基本就是反复找出异常写法、确认它实际指向哪里,再统一成系统能理解的格式。 历史数据我也没有直接一次性写进正式系统,而是让 Codex 另外做了一个独立的临时导入工具,先对准备导入的数据做校验。它最开始只处理历史领用记录,页面不接入正式系统导航,按公司主体、资产编码、工号、领用日期和归还日期去匹配数据。后来我才发现,历史使用人和当前使用人不是一回事,不能拿历史记录的最后一条去反过来校验资产现在归谁,于是又让 Codex 把这条规则单独改掉。日期也不能只做简单的先后判断:前一个人当天归还、下一个人当天领用是可以的,但两个实际使用区间发生交叉就必须拦住。那几天我做得最多的事情,就是一遍遍查看预览和报错,再把这种看起来很细、但不拦住以后就会很难查的边界补充给 Codex。 这个工具很快又不只是历史领用导入了。正常资产、公司间买卖历史、处置资产,Codex 后来分别做了独立入口;已有资产和导入文件不一致时,预览阶段就标红,逐项展示系统值和导入值,必须等我确认以后才能覆盖。我在小程序阶段已经吃过一次批量导入的亏:页面只告诉我一批数据失败了,却不清楚到底是哪一行、哪个字段有问题,中间还碰到过中文乱码。到了这个临时工具里,我就很坚持错误必须能定位、能预览,必要时还要把失败明细单独导出来,否则所谓批量导入只是把人工检查从表格里挪到了报错信息里。模板也不再只是 CSV,而是支持带系统选项的 Excel 下拉,兼容不同格式的历史文件。 把原来分散在不同公司 Excel 里的数据真正合在一起以后,还有一个以前很容易被遮住的问题也出来了:资产编码并不是全局唯一的。极少数情况下,不同公司主体下会出现相同的资产编码。过去每家公司一张表,打开文件时公司主体天然就是上下文,不会觉得有歧义;到了统一系统里,如果只拿资产编码去查,系统根本不知道我指的是哪一台。所以后来的匹配规则必须同时看公司主体和资产编码。公司间买卖又牵出了另一层身份问题:设备从一个公司主体转到另一个公司主体以后,旧编号和新编号都可能变化,但它仍然是同一台实物,后面导入历史领用时不能因此把它断成两台资产。这个临时工具也是我在实际导入中不断发现情况,再让 Codex 一轮轮补规则,而不是它一次写完我就能放心使用。 它被单独做成了一个临时服务,和主系统分开部署、单独认证。这样做是为了让大批量历史数据的校验和导入不干扰日常业务,但它终究不是一个应该长期保留的正式入口。到了后面,我决定把它从生产环境里撤掉,删除部署配置和对外入口,只把源码、测试和规则留在仓库里做审计和必要的历史追溯。现在回头看,这个工具虽然最终退出了生产,却完成了很重要的一件事:把那些原来藏在 Excel 里的模糊关系,先变成可以预览、可以拒绝、可以确认的规则,再交给正式系统。 ## 现场根本不按我画的流程图走 最早做移动端的时候,我以为扫码是一个很清楚的功能:打开摄像头,识别资产标签上的一维码,找到资产,然后进入任务。实际情况完全不是这样。也是做这个系统以后我才第一次知道,浏览器调用摄像头并不是代码写好就能用,实际部署时必须通过带域名的 HTTPS 页面访问。直接用内网地址和 HTTP 打开,浏览器根本不会给摄像头权限。为了实现一个看起来很简单的扫码入口,我还得先处理域名、证书和访问方式。这个当时挺出乎我意料的,我原本把摄像头当成手机自身的能力,没想到第一步先被浏览器的安全策略卡住了。普通浏览器有自己的权限问题,企业应用里的 WebView 又有另一套能力;有时摄像头能打开,但遮罩层挡住了确认按钮;有时识别成功了,页面却没有正确接住结果;有时桌面端正常,到了手机上,屏幕高度一变,最下面那个关键按钮就看不到了。设备包装箱上还经常挤着好几个一维码,序列号、物流信息和其他标识离得很近,系统如果一识别到内容就自动取第一个,很容易扫中错误的那个。我后来让 Codex 参考微信扫码的体验,识别到多个候选时先让我或实习生选择,而不是替我们猜。最开始那个要求把一维码对准框选区域的取景框也被我嫌弃过,现场还要搬着设备,没必要再让人花时间把线条对得整整齐齐。还有一些环境根本不适合继续使用网页摄像头,只能调用容器提供的原生扫码。一开始碰到这些问题,我会下意识地把它们看成前端兼容性问题。后来才明白,兼容性只是表面。扫码不是一个单独摆在那里的功能,它只是实习生现场操作中的一步。实习生拿着手机时,通常一只手在操作,另一只手还要搬设备,网络不一定稳定,光线也不一定好。他不会站在那里研究浏览器为什么没有授权,更不会关心回调来自哪个 SDK。他只知道自己扫了,系统却没有往下走。这个差别很重要。 系统刚成形时,我习惯从页面看它,实习生到了现场却只会从动作看系统。对他来说,不存在“扫码模块”“任务模块”和“资产模块”,只有“我现在要把这台电脑交给这个人,怎么还没弄完”。如果系统把一个动作切得太碎,或者每一步都要求实习生先理解内部状态,它就会显得很笨。后来再让 Codex 改移动端,我会更在意完整路径,而不是单个页面是否正确。扫不到怎么办,扫错了怎么返回,我已经在电脑上处理过的任务手机端怎么提示,网络中断以后会不会重复提交,按钮在小屏幕上是否还看得到,这些都成了功能本身。说实话,这类问题处理起来不太有成就感。改完以后,页面看不出多大变化,提交说明也只是“修复移动端扫码”之类很普通的一句话。可这种修复多了以后,我也就不再觉得它们小了。实习生都已经走到最后一步,系统却不让他做完,前面那些架构和模块对实际工作就没有意义。 ## 仓库改造,是我第一次比较大的返工 如果只看表单,资产位置很好理解。选一个地点,再选一个货架,必要时填点备注,差不多就够了。我一开始也差不多是这么想的。等我真开始梳理仓库,才发现“位置”这两个字下面压着很多不同的东西。城市、园区、楼、楼层、房间,是物理空间;仓库和区域,是管理范围;货架、层、格位,是存放结构;还有一些设备根本不适合放进整齐的格子,只能按区域堆放,或者用一段文字说明具体放在哪里。更麻烦的是,容量也不是一个简单数字。有些格位确实只能放一台,再多就不应该允许上架,这是硬限制。有些货架写着建议容量,实际多放一两件并不一定错;还有些区域本来就是堆放式管理,所谓容量只是为了提醒,不应该变成系统强行阻断。我要是为了模型整齐,把它们全都做成同一种规则,系统会很“规范”,现场却会天天绕过它。那一轮改动很大。我当时很容易产生一种错觉:既然代码已经重构了这么多,最好顺便把历史数据也一次性统一掉。这样数据库看起来干净,后面的逻辑也省事。但映射规则还没有完全确认。 有些旧资产只有一段历史位置,有些只有货架信息,有些字段的含义在不同时期还变过。如果在这个时候批量回填,脚本当然可以很快跑完,可一旦规则理解错了,错误就会被整齐地写进每一条数据里。代码错了还能再发一个版本,数据被批量改错以后,恢复起来就没那么轻松了。最后我没有急着把生产数据全部改掉,而是先让读取逻辑兼容旧数据,新产生的数据则按照新规则写入。老记录还能正常显示,新操作不再继续制造旧格式,等口径真正稳定以后,再考虑迁移。这个方案从技术上看不算漂亮,因为代码里会多一些兼容分支。我当时接受它,其实有点别扭,总觉得既然改了,就应该一次改干净。后来想想,数据库字段排得整整齐齐,却把一批旧资产映射错了,那才是真的麻烦。兼容可以先留着,只要我知道它为什么在这里,也知道以后删它之前还欠哪一步。 那段时间,我使用的管理端、实习生使用的操作端和移动端,还分别显示着略有不同的位置。有的优先显示货架,有的先显示文字位置,有的遇到旧数据就空着。为了这件事,我前后让 Codex 改了不止一次,最后才让它把展示规则收回到同一个地方。这也让我意识到,共享代码的价值不只是少写几遍。只要一个字段背后有业务优先级,它就不该散落在三个页面里各自解释。否则今天我在管理端看见的是一个位置,过两天实习生拿手机扫码时看见的又是另一个位置,最后还得由我来判断哪一个才对。 ## 系统开始记录历史以后,我也开始变得小心 早期的系统比较像一张“当前状态表”:资产现在属于谁,现在放在哪里,现在是什么状态。对于日常查询,这些信息已经很有用。但业务往前走以后,越来越多的问题没法靠当前值回答。一台设备可能因为厂家换新换过序列号,也可能因为历史录入问题改过资产编号。编号变了,它还是不是原来那台资产?扫码扫到旧标签时,系统应该提示不存在,还是应该找到现在的记录?如果财务编码也调整过,导入时怎么避免把它当成一台新设备再建一次?员工领用也是一样。当前使用人只能说明现在在谁手里,不能说明之前给过谁、什么时候归还、是否属于复用设备。我开始让 Codex 补历史身份、领用区间、归还记录和状态变化。也是从这时候起,我对生产数据的操作明显谨慎了很多。 有几次需要修正真实数据,表面上看只是改一个日期、补一段历史,真正动手以前却要先查清楚现状:这条记录有没有已经存在的区间,会不会和另一段历史重叠,当前状态是不是由别的任务产生,改完以后报表会受到什么影响。确认完还要备份,写入后再查一遍。以前我会把“修 Bug”简单理解成让 Codex 改代码。这个阶段以后,我开始把它拆开看。已经错掉的数据要修,导致它出错的逻辑也要让 Codex 修,防止再次发生的测试还要补上。它们经常发生在同一天,但不是同一件事。只改数据,过几天可能又会出现;只让 Codex 改代码,历史错误会继续影响我后面的查询、核对和导出。更重要的是,修改代码和修改生产数据的授权边界也不一样,不能因为它们都和同一个问题有关,就让 Codex 顺手一起做了。这份小心后来帮了我很多。内部系统的数据看起来不像交易数据那么敏感,但它同样会影响财务对账、人员交接和责任判断。系统里一个日期错了,可能只是页面上差一天,也可能让一台设备被算成逾期、闲置或未归还。数据不会因为是“内部使用”就自动变得无所谓。 ## 有些问题,不值得做成一个宏大的模型 开发过程中有一次讨论,让我印象很深。现实里有极少数人,同时承担不同部门的工作。设备发给他时,费用不一定永远归到人员资料里的默认部门。想到这个情况,我第一反应是让 Codex 做一个完整的多身份模型:一个人可以有多个组织身份,每个身份有生效时间,创建任务时选择本次身份,历史记录也要保存当时的关系。从模型上说,这当然很完整,甚至有点漂亮。但继续把实际情况翻出来看,真正需要处理的只有很少几个人。如果为了这些偶发情况,把人员、任务、资产、导入、报表和权限全部改成多身份体系,以后每次建任务、导数据和查问题时,我和实习生都得先理解这套复杂关系。最后我把方案收了回来。人员仍然有默认归属,遇到特殊情况时,我在具体任务里覆盖本次成本归属,同时写明原因。资产上会记住这次归属是跟随人员,还是来自任务特例。以后人员资料发生变化,只更新正常跟随的部分,不覆盖曾经明确指定的特例。这个方案不够“平台化”,却更适合我真正要处理的情况。 以前我很容易把扩展性当成一种天然正确。只要能想象未来可能出现更多情况,就倾向于让 Codex 提前把模型做全。后来我越来越觉得,扩展性本身也有成本。多一张表、多一种状态、多一个选择框,后面的每次导入、导出、统计和排错都要理解它。这件事以后,我在“做成通用能力”这件事上收敛了不少。有些情况就是特例,给它一个受控入口,写清原因,留下记录,先把眼前的问题解决掉,也没什么不好。这件事对我影响挺大。后面再想到一个新需求时,我会多问自己一句:这个情况到底有多常见?如果不做完整体系,最坏会怎样?有些复杂度确实不能逃,有些只是我看到 Codex 能很快实现,就忍不住把事情想得太完整。 ## 最难受的一次返工,代码其实没有错 采购和预算那次,是我整个项目里记得最清楚的一次返工。当时我把一份现有台账交给 Codex,本来的意思只是先和它讨论方案。我还没有想好预算究竟应该按申请、合同、付款还是报销来算,超预算时是提醒还是阻断,历史台账又该怎么和新流程衔接。我想先让 Codex 帮我把这些关系拆开,看看有哪些容易踩坑的地方,再由我决定到底要不要做、先做哪一部分。结果 Codex 直接把活干了。它分析了表格里的预算、采购、报销和资产关系,接着做出一套本地原型,页面、导入、台账、看板和日志都有了,连类型检查、构建和测试也跑过了。第一次打开的时候,我甚至有点发懵:我明明还在想这件事应该怎么定义,它已经把一个看起来相当完整的答案摆在我面前了。单看完成度,这轮工作确实很快;但我是这个系统最大的需求方,连我自己都还没有确认统计口径,功能做得越完整,方向错了以后返工的面积反而越大。我很快叫停了这一轮。问题不是某个页面不好看,也不是代码不能运行,而是 Codex 把“先讨论一下”理解成了“现在就开始实现”。当然,根子还是我给出的边界不够清楚。一句在我看来明显属于讨论阶段的话,到了执行力很强的 Codex 那里,已经足够变成开工指令。 于是那一轮工作停在本地,没有提交,也没有部署,更没有初始化任何生产数据。我又回到最开始的问题:我到底想让这个模块解决什么,什么数字才算权威,看到怎样的结果我才会认可。这次之后,我就没再把“测试全过了”和“方向没问题”当成一回事。测试只能证明 Codex 按照当前上下文写下来的那套规则能跑通,至于那是不是我真正想清楚、确认过的规则,它回答不了。把开发全部交给 Codex 以后,这个问题会被明显放大。过去一个错误理解可能因为开发很慢,做到一半就会暴露。现在从一句想法到一个完整原型的距离非常短,如果我没有在关键口径上先停一下,Codex 可以非常高效地把一个错误方向做得相当完整。做得快不一定省时间,这次就是。后来再碰到财务、盘点、预测这类功能,我会先逼自己回答数据从哪里来,时间点怎么算,什么情况需要我确认,哪些动作只是建议,哪些动作会真正改变业务状态。不是故意把事情拖慢。返工一次就知道了,页面重写并没有多可怕,可如果连我自己都开始怀疑页面上的数字,后面导给财务、部门经理和领导时就更不可能心里有底。 ## 权限问题,让我重新认识了“任务” 系统里的任务越来越多以后,权限也开始变得复杂。真正登录 AssetOps 的一直只有两个人:我是管理员,能看全部数据、配置规则和导出结果;实习生是操作员,主要处理分给他的现场任务。后来有了移动端入口、仓库操作和打印任务,我又让 Codex 加过一种不绑定具体操作员、让实习生可以直接接手的任务。有一次,我在电脑上看起来一切正常,实习生拿手机执行时却一直失败。页面能看到任务,接口也能打开,真正提交时系统提示资产被占用。Codex 排查到最后才发现,问题不在手机,也不在页面上的权限,而在资产锁。任务表面上允许实习生处理,创建时产生的锁却仍然落在创建者身上。于是系统一边告诉实习生“这件事你可以做”,另一边又在更底层的位置说“这台资产已经被我的任务占用”。查到这里,我才把几件以前混在一起的事分开:能不能看见任务,能不能执行任务,资产当前被哪个流程占用,以及最后由谁完成。它们有关联,但不能指望一个角色字段顺便全管了。 后来修复时,不只是改了判断,还要处理已经留在生产里的错误锁。代码发布以后,新任务不会再错,旧任务仍然会卡住。所以又回到了前面说的那件事:代码和数据要分开修,修完还要让实习生沿着真实路径重新执行一遍。任务多起来以后,我越来越觉得,业务系统的核心不是页面,而是状态变化。发放、归还、维修、调拨、报废,看起来是不同功能,底层都在回答相似的问题:这台资产现在能不能被操作,我和实习生分别能做到哪一步,操作完成以后状态去哪里,如果任务取消又怎么退回来。只要这些问题没有统一想清楚,页面做得越多,冲突越容易藏起来。所以后来当我想让实习生多处理一类任务时,已经不敢只在页面上多放一个按钮。后面还有接口权限、任务归属、资产锁、操作日志和完成条件。少一层,就会出现那种最让人摸不着头脑的情况:看得到,也点得到,但就是做不成。 ## 找不到打印机 SDK,我把安装包丢给了 Codex 标签打印是另一个让我改变看法的功能。一开始它听起来特别简单。系统里已经有资产信息,仓库里有打印机,点一下,出一张标签,不就结束了吗?按照我过去比较传统的思路,第一步当然是去找打印机厂家提供的 SDK,然后再想办法接进系统。问题是官方根本没有提供可以直接下载的 SDK,我在正常渠道里绕了一圈也没找到。就在我以为这条路可能走不通的时候,突然冒出一个有点野的想法:既然厂家能提供 Windows 的 EXE 安装包,能不能让 Codex 直接分析这个安装包,看看里面到底有没有可以利用的调用方式?我也没抱太大希望,就把 EXE 安装包丢给了 Codex,让它试着研究。没想到它还真的分析成功了,最后把打印机调用跑了起来。那一刻我对 Codex 的能力又多了一点新的认识。以前我觉得没有 SDK,基本就等于只能等厂家支持;这一次却是我提供了一个可能的入口,Codex 真从安装包里找到了把事情继续做下去的办法。 调用打印机只是第一步。服务器不在仓库电脑里,打印机也不会直接暴露给服务器。操作可能来自实习生手里的手机,也可能来自我办公室的电脑,最后却要让仓库里那台 Windows 电脑上的打印机出纸,中间隔着网络、权限、驱动、打印队列和本地进程。我又让 Codex 做了一个常驻在仓库电脑上的代理,由它主动连接服务器。我或实习生提交打印任务时,服务器把任务推过去,代理负责渲染和打印,再把结果返回。断线要重连,失败要能重试,同时还要避免因为重连把同一个任务打印两遍。做到这里,它已经不再是一个“调用打印机”的功能了,而是 Codex 围绕我的实际环境搭出来的一个很小的分布式系统。最折磨人的还是那些听起来不像软件问题的细节。标签边距差一点,内容就不居中;电脑休眠以后,代理会断线;驱动里的纸张尺寸和程序里的尺寸不一致,预览正常,打印出来却偏了。还有一次,打印机没关,电脑也没关,网络端口可以访问,系统仍然显示离线。 我一开始也被“网络是通的”这件事带偏了。后来才想清楚,网络可达只能说明这台电脑能连到服务器,不能说明代理进程还在运行,更不能说明它已经鉴权成功并保持着实时连接。系统显示的“在线”,必须对应真正能接收任务的状态,而不是某个端口碰巧能通。打印功能上线以后,我又让 Codex 陆续补了实习生使用的移动端入口、权限、任务列表和错误提示。因为“标签能打印”不只是我在电脑上测试成功,而是实习生到了仓库以后能找到按钮,按下去能看到任务是否送达,失败时我也能判断问题是在网络、代理还是打印机。硬件有一种很诚实的残酷。纯软件里有些问题可以暂时被刷新页面掩盖,打印机不会。纸没有出来,就是没有出来;连续出来两张,也不可能解释成缓存。它逼着我把在线状态、任务编号、重试和幂等这些原本容易含糊过去的事情想清楚。 后来回看这段经历,我反而挺喜欢它。打印功能谈不上多高级,可它把系统从服务器和浏览器一直拖到了现实世界。代码里的一个状态,最后会变成仓库桌面上的一张纸。很多原来有点虚的概念,到这里一下就具体了。 ## 盘点报告这件事,前后拐了好几次弯 盘点报告最开始也是一个听起来很清楚的需求。当时员工自行确认资产的盘点动作主要放在 OA 里,财务系统提供固定资产基准,AssetOps 里则保存我维护的实物台账。我最初并没有打算在 AssetOps 里重新做一套完整盘点,只是想拿现有的盘点明细、财务资产基准和往年的报告模板,自动生成一份新的报告。我把这件事交给 Codex 时,原意是在现有 AssetOps 里增加一个模块,以后由我在系统里导入盘点数据、检查结果并生成报告。可我当时只强调了“自动生成报告”,没有把交付形态说清楚。Codex 顺着最短路径,先做成了一个单独放在电脑上运行的脚本:读取表格,计算结果,再把内容填进模板。作为验证,它确实能工作,但这不是我最终想要的使用方式。我很快把方向纠正回来,让它回到现有系统里,而不是再给自己增加一个需要单独维护的工具。这和采购预算那次有点像,只不过一个是我本来只想先讨论,Codex 直接开始实现;另一个是我已经决定要做,却没有说清楚应该落在哪里。好在脚本阶段也没有完全浪费,它先把数据问题暴露了出来。财务系统里的一张资产卡片,到了实物台账里可能被拆成很多条设备明细。如果直接拿两个编码做一对一匹配,大量正常数据会被当成缺失。OA 里的使用人又是盘点开始时的快照,不能简单拿系统里今天的使用人覆盖。参与盘点的人数也不能只对某一列去重,因为员工自行盘点、我或实习生代盘,以及仓库里的闲置资产,背后的含义并不一样。这些问题让我对“报表”两个字产生了敬畏。 页面做错了,我或者实习生在操作时通常很快就能发现。报表公式做错了,它却可能照样生成一张排版漂亮、数字齐全的表,而且看上去特别像真的。这个反而最让我害怕。后来有一个按时率一直显示异常。代码里的比较没有语法错误,日期也能正常解析。查到最后,是截止日被当成了当天零点。任务在截止日当天上午完成,从人的理解看当然算按时;从程序看,它已经晚于零点,所以被算成迟到。这是我很喜欢举的一个例子。代码完全按照写下来的规则运行,问题出在我没有把“截止到某一天”翻译完整。业务说的日期,经常包含一个默认边界,人知道,程序不知道。如果不明确是当天开始还是当天结束,同一个字段在不同报表里就可能有不同解释。报告模块做完以后,盘点又继续往前走,变成了一个正式流程。创建盘点时要冻结当时的资产范围,执行过程中可以扫码,记录已盘、未盘、范围外、位置不一致,也要处理盘点期间发生的调拨和变化。 “冻结”这件事很关键。盘点不会让公司的其他工作停下来,资产仍然可能被发放、归还或移动。如果每次打开页面都用当前数据重新计算应该盘什么,盘点的基准会一直变,最后谁也说不清差异是漏盘,还是资产在盘点期间刚好发生了变化。从一份自动报告,到系统模块,再到正式盘点流程,弯拐得有点多。最开始我盯着模板,后来盯着公式,最后才回到事情发生的那个时间点。早一点问清楚,当然能少走一些路,不过这些路也让我把盘点到底在盘什么想明白了。现在让我再做类似功能,我大概会先写下三件事:谁定义应盘范围,哪个时间点的数据算基准,期间变化怎么解释。模板反而是最后才需要担心的东西。 ## 离职流程让我发现,页面从来不是业务边界 员工离职看起来也像一条很清楚的流程:把名下设备收回来,任务完成,人改成离职状态。实际情况当然没有这么整齐。一个人名下可能有多台设备,有的要回仓库,有的留在原场地,有的直接调拨给接手同事。更麻烦的是,离职前可能已经单独创建了调拨任务。如果离职流程完全不知道这件事,又为同一台资产创建归还任务,系统就会让两条流程同时争一台设备。我最初想过一个比较局部的处理:已经被调拨任务占用的资产,就不再出现在离职归还清单里。这样至少不会重复操作。但继续往下推,会发现“藏起来”并没有真正解决问题。离职流程仍然不知道那台设备最后会去哪里,也不知道调拨取消以后要不要重新处理。只有当所有资产都有明确去向,人员状态才应该真正结束。后来方案变成在离职过程中逐台选择处理方式。已经存在的调拨继续显示,但不能重复创建;调拨完成以后,这台资产从离职人员名下转走;如果调拨取消,它又回到待处理范围。所有子任务都结束,离职流程才算结束。 做完这轮我才发现,我和实习生虽然在不同页面上操作,系统处理的却始终是同一批资产。发放页面、调拨页面、离职页面,不能各管各的。只要它们会改变同一台设备,就得知道别的流程正在做什么。很多内部系统最后会变得难维护,不是因为功能太多,而是因为每个功能只在自己的页面里成立。页面之间靠我和实习生记住操作顺序来维持秩序:先去这里点一下,再去那里避开某个选项,某种状态下千万不要创建另一类任务。这样的规则只要还依赖我口头提醒,迟早会出错。我希望 AssetOps 最后能做的,是把这些冲突提前说清楚。不能做的时候直接告诉我原因,任务已经存在时让我和实习生都能看到它的去向,取消以后再把状态还回来。系统未必能替我做所有决定,但至少不要让我靠猜来维持一致。 ## 报表越漂亮,我后来反而越不敢马上相信 项目后期开始增加更多统计和预测以后,我的心态和最开始已经很不一样了。早期看到图表能正常出数字,我会觉得功能基本完成。后来看到一个数字,我第一反应变成:它从哪张表来,使用的是当前状态还是历史快照,时间范围是什么,分母里有没有把不该算的东西放进去。例如“最后登录时间”听起来很合理,但系统使用了持续有效的登录状态,一个账号可能很久没有重新登录,却一直在访问。我要是想知道账号最近有没有被使用,应该看最后活动,而不是最后一次认证。这两个字段只差几个字,回答的是完全不同的问题。库存预测也是这样。画一条未来几个月的需求曲线并不难,难的是这条线凭什么往上或往下。人员入职和离职趋势、不同岗位的设备配置率、归还设备有多少能继续复用、采购需要提前多久、安全库存留多少,任何一个假设变化,结果都会变。如果页面只给我一个很精确的数字,却不把这些假设摊开,它很容易制造一种“系统已经替我算明白了”的错觉。数字带着小数点,不代表它更接近现实。 所以后来做预测,我更愿意把它停在决策支持。系统可以提醒未来可能缺货,可以告诉我这个判断基于哪些历史变化,但不应该因为一条预测线就自动创建采购。采购会带来真实成本,必须由我看懂原因以后再决定。这也是整个项目里我逐渐形成的一种习惯:越接近真实业务后果的功能,越要把假设露出来。系统当然可以自动化,但自动化不应该把我的判断过程藏掉。 ## 我用 Codex 做这个项目以后,对“快”有了新的认识 因为全部代码都由 Codex 完成,这个过程和我过去自己用 Excel 公式搭工具很不一样。Codex 最明显的优势当然是快。它可以同时阅读很多文件,沿着服务、接口和页面追一条逻辑,也可以很快补测试、跑构建、整理部署步骤。我不需要亲自在数据库、后端和前端之间来回切换,只要把问题、现场情况和验收结果持续告诉它,一次长会话就能推进很远。但它的局限也同样明显。它了解代码,不等于了解我和实习生每天怎么做资产管理;它能根据我提供的信息做出合理推断,不等于这个推断就是业务事实。很多时候,它给出的方案在工程上非常完整,真正的问题却是我少说了一句背景,或者我和它对一个词的理解不同。采购预算的返工是这样,盘点报告先做成脚本也是这样,多身份模型一开始想得太重还是这样。Codex 不太会因为方案工作量大而犹豫。只要目标看起来明确,它可以迅速把数据库、接口、页面和测试一起补齐。这种能力非常好用,也很容易让人上头。看着功能不断出现,会产生一种项目推进得特别顺利的感觉。可我是需求方,也是最后每天要用它、判断它到底对不对的人。很多时候,我要做的反而是随时准备踩一下刹车。 这个字段的来源确定了吗?我说“做一个报告”,到底是想先讨论、先做脚本验证,还是直接增加系统模块?这次只允许改代码,还是也允许提交、部署和修改生产数据?本地有一些原来的修改,能不能覆盖?看到一批看似无用的文件,是不是真的可以删除?这些问题没有多少代码含量,却决定了事情会不会做过界。我以前可能会把 Codex 看成一个更快的开发工具。做完这个项目以后,我更愿意把它当成一个执行力很强、记忆依赖上下文、又没有现场经验的合作者。我必须把事实、边界和验收标准交代清楚,也要在它过于自信的时候重新检查前提。有时它会犯很低级的错误,有时它也会从大量代码里发现我自己没有注意到的关联。相处久了以后,我觉得没必要把它神化,也没必要故意贬低。它确实改变了我做项目的方式,但最后那个“这是不是现实里的正确做法”的判断,还是得由我承担。因为系统写完以后不是交给一群客户使用,它每天面对的就是我和实习生;财务、部门经理和领导看到的,则是我从里面导出去的数据。只要口径错了,最后回来解释和复盘的人还是我。还有一点变化是,我比以前更重视证据。 Codex 很容易给出一句听起来完整的总结,我也很容易因为它语气确定就相信。可在真实项目里,“应该没问题”几乎没有用。我需要看到测试有没有跑,构建是否成功,部署的是不是刚刚生成的版本,服务是否真的健康,关键页面能不能从外部访问。修改生产数据以前,要先看匹配范围,修改以后要再查结果。说到底,系统只认实际跑出来的结果。 ## 我后来才明白,当前状态、历史和快照是三件事 如果要从这段经历里挑一个对我影响最大的技术认识,大概就是这个。很多系统最开始都围绕当前状态设计。资产现在在哪,属于谁,是否在库,是否维修。当前状态让页面好查,也让日常操作容易理解。但只要进入审计、盘点、对账和预测,当前状态就不够了。历史事件回答“发生过什么”。谁在什么时候领用,什么时候归还,编号为什么改变,位置是谁修正的。快照回答“当时看到的世界是什么”。盘点开始那一天,应该盘哪些资产;报告统计那个时点,OA 里记录的使用人是谁;任务创建时,哪些条件已经被确认。这三种数据混在一起,系统就会不断出现解释不通的情况。拿当前使用人去覆盖盘点快照,报告会随着人事变化悄悄改变;只看当前闲置状态,不看最近归还事件,闲置时长就会从入库那天开始算;任务执行过程中不保留创建时的资产范围,后来发生的调拨会让任务像是从来没有包含过那台设备。这些 Bug 往往不是某一行代码明显写错,而是数据代表的时间不一样。 以前我和 Codex 讨论字段时,更关注它是什么类型、是否必填、怎么建立索引。后来我会多问一句:这个值描述的是现在,某一次发生的事情,还是某个时间点冻结下来的事实?如果这个问题没有答案,后面报表迟早会替我回答,而且通常答错。日期也一样。系统里“某日截止”“入职日期”“归还日期”“盘点日期”看起来都是日期,实际边界完全不同。有的是当天开始生效,有的是当天结束前完成都可以,有的只需要记自然日,有的必须保留准确时间。程序不会理解中文里默认省略的含义,必须在规则里写出来。这些认识听起来并不新鲜。可道理只在书里看,很容易点点头就过去;当它真的把生产报表算成零,把截止日当天完成的任务判成逾期,就不太容易忘了。 ## 如果让我重新做一次 如果现在让我从头再做 AssetOps,我不觉得最后会让 Codex 写出一个完全不同的系统。很多复杂度来自现实,本来就绕不过去。但推进方式应该会不一样。我会更早整理一份业务词汇表,而且不会只写名词解释。比如“在库”到底包括哪些状态,“按时”以什么时刻为边界,“复用”由当前使用人判断还是由历史领用判断,“盘点人数”统计的是操作人、使用人还是两者关系。每一个容易进入报表的词,都应该同时写清数据来源和时间点。我也会把方案确认和 Codex 的实现分得更开。尤其是财务、预算、盘点、预测这类功能,先用小样例把口径讲明白,再决定页面和数据结构。原型可以做,但要明确告诉 Codex 它只是用来讨论,不要让完成度替代了确认。任务状态和资产锁,我会更早让 Codex 抽出来统一设计。项目中间很多问题,本质上都是不同流程在争同一台资产,只是散落在发放、归还、调拨、维修和离职里。早一点把占用、转交、释放、取消和完成条件放到同一套规则下,后面会少很多局部修补。 生产数据修复也应该有更正式的工具。每次临时让 Codex 写脚本虽然能解决问题,但预检、备份、幂等、执行、校验和记录其实都是重复步骤。把这些步骤固定下来,既能减少紧张,也能避免某次着急时漏掉一环。还有会话本身。我不会再让一个任务无限长下去。一个目标做完就停下来,写清楚结果,再开下一段。这样提交更容易对应原因,Codex 的上下文也不会混杂。以前我总觉得重新交代背景很浪费时间,后来发现,整理背景本身就是一次校验。至于提交大小,我现在没有那么执着于形式。大改动有时确实无法避免,小提交也不天然代表质量好。我更关心每次提交能不能说清意图,能不能独立验证,出了问题是否知道回到哪里。生产数据修复、代码变化和部署配置最好各自留下边界,不要为了省一次操作全塞在一起。最后,我会更早接受一件事:内部业务系统不会在需求文档写完的那天就被完整定义。很多规则只有用起来以后才会冒出来。第一次别急着把模型做得包罗万象,先让系统能留下历史、解释变化,也给以后改错留点余地。 ## 写到最后 回看这段时间,我没有一种“终于把它做完了”的感觉。系统当然比最开始完整了很多。很多以前靠表格、口头确认和人工记忆维持的事情,现在有了明确入口,也有了日志和状态。可我也越来越清楚,它不可能因为功能多了就自动变得成熟。成熟更像是一种面对问题的方式。遇到一个反例时,不急着把它藏进条件分支,而是先想原来的理解哪里不完整;看到一张数字漂亮的报表时,先问数据来自哪里;修改生产记录时,知道这和改代码不是同一种动作;Codex 很快给出完整方案时,也愿意停下来确认,我到底有没有把自己的需求说对。这个项目让我经历了不少返工。有些是我没有把需求说清楚,有些是我让 Codex 推进得太快,还有些情况只有系统真的被我和实习生用起来以后才会暴露。以前我会觉得返工就是前面做错了,多少有点挫败。现在再看,后来那些比较靠谱的规则,很多就是从这些做错的地方长出来的。 当然,这不代表返工越多越好。能在动手前想清楚的,还是应该先想清楚;能靠测试挡住的,还是应该提前挡住。只是当现实推翻原来的判断时,没有必要为了维护“最初设计是对的”而硬撑。这个系统不是为了证明 Codex 能做出多少功能,它最后还是要让我和实习生把事情顺利做完,让我能放心地把需要的数据导给财务、部门经理和领导。我现在偶尔翻到早期那些提交,能看到当时很急,也能看到很多判断还比较简单。再往后,提交说明里开始出现更多“兼容”“修正口径”“冻结基线”“恢复状态”“补充审计”。这些词可能没有新页面那么显眼,却更接近我这段时间真正学到的东西。AssetOps 还会继续改。新的数据、新的流程、新的例外,一定还会带来现在没有想到的问题。也许过一段时间再回头看,这篇文章里的某些判断也会显得幼稚。那也没什么。至少今天回头看,我已经知道自己不是把一个系统一次做完了,而是在不断被现实纠正以后,学会了怎么继续做下去。先记到这里。 标签: none
评论已关闭