CHAPTER 052 / 100
第五十二章:南山高新园的夜,我和她一起 Debug
听这一章
微软云希 · AI 合成朗读 · 27:36
周五晚上十一点以后,办公室里最响的东西,不是键盘。
是空调。
白天几十个人在里面开会、说话、接电话,中央空调那点动静根本没人注意。等同事一个个走完,灯也只剩我们这一排还亮着,出风口的低鸣就突然清楚起来,像一台不知道谁忘了关的老服务器,在天花板里坚持值夜班。
我坐在工位上敲代码。
小璇坐在旁边画页面。
她面前开着批次详情页的设计稿,左边是状态和操作说明,右边摆着几张音乐结果卡片。我的屏幕则比她朴素得多,一边是代码,一边是接口日志,最下面还压着伟源下班前留下的最后三条 Bug。
第一条,失败原因展示不完整。
第二条,旧版本任务的时长偶尔为零。
第三条,也是最麻烦的一条:模型明明返回了两首音乐,企业端页面却只显示一首。
这是这一轮集中联调留下的最后三条问题。
按照下午定下来的计划,只要它们全部关闭,企业项目这一阶段就算正式收口。后面不是再也不改,而是从全员盯守的集中开发,转进稳定观察和正常迭代。客户继续用,监控继续看,新需求继续排,但大家不用再每天围着同一块屏幕,等下一条报警决定今晚几点回家。
前两条已经修完了。
最后一条还赖在屏幕上,像凌晨便利店门口那种怎么赶都不肯走的蚊子,体积不大,存在感极强。
小马哥和伟源本来想留下来,被我赶走了。小马哥明早还要跟客户对新一版接口,伟源已经连续跑了两天回归,再熬下去,测试报告可能没问题,人先进入异常状态。
小璇没走。
她给出的理由是,结果卡片的交互还要跟最终接口一起定,不能明早再靠聊天记录猜。
这个理由非常职业。
职业到我差点忘了,她十点半去茶水间的时候,顺手也给我泡了一杯热茶。
我把刚改完的代码重新启动,对她说:“再走一遍。”
“嗯。”
小璇把设计稿缩到一边,切到测试页面,输入企业测试账号,创建了一条音乐生成任务。
页面上的状态从“已提交”变成“生成中”。
几十秒后,模型回调进来。
我的日志窗口连续刷过几行绿色记录:回调验签通过,任务状态更新成功,结果保存成功,批次状态更新成功。
小璇刷新页面。
“还是一首。”
我没说话,先去查数据库。
任务是成功的,模型结果数量字段写的是二,结果表里却只有一条记录。接口返回也很诚实,resultCount 是二,真正的结果列表里只有一项。
一边说有两个,一边只掏出一个。
这已经不是接口 Bug 了,这是接口在跟用户玩藏私房钱。
“模型那边到底回了几个?”小璇问。
“日志里是两个。”
“数据库呢?”
“一个。”
“那就不是页面漏了。”
她说完,把刚准备打开的前端调试工具又关了。
动作很干脆。
换成我们刚开始一起做项目的时候,她大概还会先问我,是不是前端没解析对。现在不用了。页面拿到几条、接口返回几条、数据库存了几条,她已经能顺着这条链路一层层往回找。
我把模型回调的原始数据调出来,跟后端接收对象放在一起看。
问题很快露出了一半。
旧模型一次只返回一个结果,字段就是一组音频地址、封面和时长。我们最早接的时候,后端也按这个结构设计,一条任务对应一条结果。
新模型为了让企业客户有选择,一次会生成两个候选版本。回调里不再是单个结果,而是一个 outputs 数组。我们前面为了兼容旧接口,在适配层里先取了数组第一项,再映射成原来的结果对象。
当时大家想的是,先让老链路跑起来,后面再把多结果补完整。
这种“后面再补”,在程序员世界里跟“改天请你吃饭”差不多。
说的时候都很真诚,什么时候兑现全看事故挑哪天上门。
“找到了?”小璇把椅子往我这边滑了一点。
“适配层只拿了第一个。”我指给她看,“数量字段用的是模型返回的总数,所以显示二。落库和接口还是旧结构,只认识一个。”
她靠过来看屏幕。
她刚才嫌办公室冷,把外套披在了肩上,这会儿一俯身,长发从肩侧滑下来,发尾轻轻扫过我的手背。茶香里混着她身上很淡的洗发水味,我盯着那几行代码,注意力却很没骨气地偏了半秒。
小璇看了我一眼。
“代码在这边。”
“我知道。”
“那你看哪儿?”
“在定位问题。”
“问题定位到我脸上了?”
我清了清嗓子,把光标移回适配方法。
“刚才是链路追踪。”
她嘴角动了一下,没继续拆穿我,只把头发拢到耳后,手撑着桌边看那份回调数据。
“你准备怎么改?”
“把接口的单个结果改成数组,模型回几个就存几个,页面也按数组画。”
“然后呢?”
“什么然后?”
“页面上现在写的是‘任务生成成功’。”她指着测试页,“一个任务,两首结果。这里的成功到底是任务成功,还是两首都成功?”
我愣了一下。
她又点了点页面右上角的数字。
“这里显示‘成功 2’,用户会以为自己提交了两条任务。可他明明只提交了一条,只是模型给了两个候选结果。你把接口改成数组,数据能出来,意思还是乱的。”
我本来以为问题已经定位到了适配层。
她一句话,又把问题往上提了一层。
代码丢掉了第二条结果,只是表面现象。更深的原因是,我们从一开始就把“任务”和“结果”当成同一个数量来用了。旧模型一进一出,这两个概念长得一样,谁也没觉得有问题。新模型一进两出以后,原先藏在一比一关系里的偷懒,立刻全暴露出来。
我往椅背上一靠,看着页面上的“成功 2”。
“那应该显示一条任务完成,生成两个结果。”
“详情页可以这么写。”小璇说,“列表里不用一次塞这么多字。列表只告诉用户任务完成,进详情以后再看到两张候选卡。要是模型只回来一个,另一个还在生成,也别让整个页面一直转圈。哪个能听就先展示哪个,没回来的位置把状态说清楚。”
“模型最后是一次性回完整结果。”
“你确定所有版本都是?”
我下意识想点头,手都快动了,又停住。
旧版本是单结果回调。
现在这版是最终一次返回两个结果。
可模型同学前两天提过,后面可能会把生成完成的候选先推回来,不必等整组全结束。那句话当时只在对齐会上顺嘴提了一下,还没进入正式文档。
要是我现在把“两首一起回来”直接写死在页面和接口里,眼前这个 Bug 是能关,下一版模型一上,今晚的我们就会变成下一批人嘴里的历史包袱。
我把椅子往她那边转了点。
“你这个问题,问得有点贵。”
“多贵?”
“本来改一个对象,现在得重新确认一遍结果协议。”
“那你还改吗?”
“改。”我把模型文档重新打开,“贵也比上线以后让客户替我们验收便宜。”
小璇轻轻笑了一下。
“这句像负责人说的。”
“前面不像?”
“前面像一个偷看 UI 的后端。”
我没法反驳。
主要是证据链太完整。
十一点四十,我们把模型新旧两个版本的回调样例全拉了出来。
旧版本返回单个结果。
新版本返回结果数组,每个候选都有独立的结果编号、音频地址、封面、时长和生成状态。任务本身还有一层总状态,用来说明这一轮生成是否结束。
除此之外,新版本的时长单位是毫秒,旧接口一直用秒。
第三条 Bug 还没修,第二条“时长偶尔为零”倒是顺手找到了真正根因。之前那次修复只补了空值,没有解决单位不一致。某些很短的测试音频经过错误换算,最后就被页面四舍五入成了零。
我看着两份协议,忍不住叹气。
“你们 AI 模型是不是都不爱跟接口讲同一种语言?”
小璇坐在旁边喝了口茶。
“你们后端不是最爱说,外部变化要在边界消化吗?”
“你怎么连这个都记得?”
“因为你开会讲过三次。”
“我讲得有这么频繁?”
“有。”她说,“跟我提醒你少熬夜的频率差不多。区别是,你对前一句执行得比较好。”
我看了眼右下角的时间,决定暂时不接这张需求单。
我们重新定了数据边界。
模型回调先在适配层被翻译成统一结构。旧版本的单个结果,转成只含一项的结果列表;新版本的 outputs 数组,每一项按模型结果编号独立保存;毫秒统一转成秒,页面和业务接口都不再知道模型原始单位。
任务状态和结果状态也分开。
任务完成,只代表模型不再继续生成。具体有几首可用、哪一首失败,由结果列表自己说明。以后模型改成陆续回调,后端也按结果编号做新增或更新,不会因为第二次回调又插出一份重复数据。
接口则明确返回任务状态、结果总数和结果列表,不再拿一个“成功数量”同时扮演两种角色。
小璇把页面也跟着拆开。
批次列表看任务,告诉用户还有几条在生成、几条已经结束;进入任务详情以后,再看这一条任务生成了几个候选结果。已经完成的音乐直接试听,仍在生成的位置显示独立状态,失败的候选给出原因,不让一个红色提示把已经能用的结果全盖住。
她画得很快。
我偶尔抬头看一眼,她的屏幕上就多出一个状态、多一行文案,刚才还显得有点别扭的两张结果卡,慢慢有了清楚的层级。
我忽然发现,我们俩一起做事的时候,最舒服的地方不是她能看懂多少代码,也不是我能对她的设计提多少建议。
是她总能看见我容易忽略的那半边。
我盯着数据有没有存对。
她盯着用户会不会看懂。
我考虑系统怎样兼容旧版本。
她会追问新版本来了以后,页面是不是又要推倒重画。
两个人关注的东西不一样,却刚好能在同一个问题上合起来。
这比单纯有人陪着加班更让人上头。
陪伴只能让夜没那么长。
真正的配合,能让 Bug 没地方藏。
凌晨十二点二十,我开始改代码。
小璇没有一直盯着我。她戴上耳机,继续整理页面状态,偶尔把一张图拖到我这边,问一句这个字段能不能稳定返回。
能,我就点头。
不能,我就把原因说清楚。
有两次我差点顺口说“前端先判断一下”,话到嘴边,又想起第五十一章那只转个不停的加载图标,只能老老实实把判断收回后端。
人在同一个坑里摔过一次叫经验。
摔第二次,就只能算对坑有感情。
改到一半,小璇起身去接水。她坐久了,站起来时伸了个懒腰,针织上衣随着动作往上带了一点,露出一小截腰线。
我抬头的时候,视线正好撞上。
她伸到一半,低头看我。
我们对视了两秒。
“又在链路追踪?”她问。
“这次是自动发现异常。”
“要修吗?”
“这个异常建议保留。”
她拿起桌上的便签纸,轻轻拍在我额头上。
“先修你自己的。”
我把便签拿下来,上面正好是她刚写的一句话:
“任务完成,不等于所有结果成功。”
我把那张纸贴到显示器边上。
“这个归我。”
“本来就是给你的。”
她转身往茶水间走,走了两步又回头看我。
“别删。”
“放心,最高优先级。”
办公室里只剩她的脚步声,没一会儿又慢慢走回来。她把温水放到我手边,没有说“辛苦了”,也没催我快点。
她只是俯下身,看了一眼我已经改完的那部分代码。
“还要多久?”
“半小时。”
“你上次说半小时,最后用了一个半小时。”
“这次有需求方现场监督。”
“我是设计。”
“今晚兼任产品和监理。”
她站在我身后,手很自然地搭上我的肩,拇指在我僵硬的后颈上按了两下。
我整个人差点当场断开连接。
她刚洗过手,指尖有点凉,掌心却是暖的。按到最酸的那一块时,我下意识往后靠了一点,后脑勺碰到她身前,她的动作也跟着停了半秒。
那一下很轻。
轻得谁都可以假装没发生。
可深夜办公室太安静了,人的呼吸离得近一点,存在感都比白天强。
我转过椅子,抬头看她。
小璇没躲,只是手还搭在我肩上,低声问:“不写了?”
“程序在重启。”
“所以呢?”
“有二十秒空闲时间。”
她眼睛弯了一下。
“你准备怎么利用?”
我伸手轻轻揽住她的腰,把人往前带了一点。
她站在我膝间,肩上的外套滑下来一些,手也顺势扶住了我的肩。我们离得很近,近到我能看清她睫毛落下来的影子,也能感觉到她说话时很轻的呼吸。
“小璇。”
“嗯?”
“你今天这个优化建议,值不值得给奖励?”
“奖励不是应该给我吗?”
“我可以代领。”
她被我这句不要脸逗笑了,抬手捏了一下我的耳朵。
“你现在越来越会占便宜了。”
“主要是项目磨合得好。”
“少拿项目当借口。”
嘴上这么说,她却没从我怀里退开。
我抬头亲了她一下。
一开始只是很轻地碰了碰。她低头看着我,停了半秒,又俯下来回亲了一下。外套从她肩上滑到手臂,发丝落在我脸侧,我放在她腰后的手也不自觉收紧了一点。
这个吻慢慢有了点温度。
直到电脑发出一声启动完成的提示音。
小璇在我唇边笑了一下,额头抵着我,声音很轻。
“服务起来了。”
“让它再等会儿。”
“刚才谁说最后一个 Bug 不过,今晚不走?”
“负责人。”
“那你现在是什么?”
我看着她,没忍住又亲了一下。
“负责人短暂降级。”
她抬手推了推我肩膀,耳朵已经有点红,眼里却还带着笑。
“恢复服务。”
我只好松开她。
她把滑下来的外套重新拉好,坐回旁边的时候,还故意把椅子往外挪了十厘米。
“物理隔离。”她说。
“有必要吗?”
“防止某些人调着调着,把 Bug 调到设计师身上。”
我看着那十厘米的距离,心想这种隔离方案基本等于心理安慰。
不过我没说。
怕她继续扩容。
凌晨一点零七分,第一版修改完成。
我先拿旧模型任务跑了一遍。
单个结果被正常转成列表,历史接口数据没有丢,时长也显示正确。
接着切到新模型。
两个候选结果陆续落库,模型结果编号各自唯一,重复回调没有多插数据。任务结束以后,接口返回一条完成任务和两个可用结果。
小璇刷新页面。
两张结果卡同时出现。
第一首可以试听,第二首也可以。封面、时长、下载按钮都在正确的位置。页面顶部不再写容易误解的“成功 2”,而是清清楚楚地显示:
“任务已完成,生成 2 个候选结果。”
我还没来得及高兴,她已经点开其中一张卡片。
“时长不对。”
我心里刚冒出来的烟花,当场被她拿灭火器喷了。
“哪里不对?”
“音频三分十二秒,这里显示三分十一秒。”
我查了一下,模型返回的是毫秒,后端转秒时直接向下取整,播放器却按实际时长四舍五入。两边只差一秒,普通用户大概不会为这点误差投诉,但同一个页面两个地方显示不一样,看起来就像系统连自己生成了多长的音乐都没商量好。
“统一规则。”小璇说,“别一个地方向下,一个地方四舍五入。”
我点点头,重新补了转换逻辑和测试。
一点二十三分,第二轮。
旧版本,正常。
新版本,正常。
单结果,正常。
双结果,正常。
重复回调,数据没有增加。
其中一个候选失败时,任务详情保留成功结果,同时把失败原因单独展示出来。
我故意把接口延迟了两秒,小璇那边的页面先显示已完成的第一张卡,第二个位置仍然保留生成状态,没有整页锁死。
最后一次刷新,接口稳定返回 200。
日志从模型回调、结果转换、数据落库到详情查询,一路全绿。
屏幕上没有红字,没有空指针,也没有那种看起来像成功、点进去才发现少一半数据的半成品状态。
我表面上只很稳重地点了点头。
“可以了。”
心里其实已经敲锣打鼓。
那种快乐很难跟不写代码的人解释。它不是中了奖,也不是突然涨工资,就是一堆刚才还各说各话的数据,终于按同一套规则从模型走到数据库,再从接口走到页面。每一层都认得上一层说的是什么,最后用户看到的,也刚好是系统真实发生的事。
世界没有因此变好。
但至少今晚这一小块,终于讲道理了。
小璇点下播放按钮。
办公室里响起刚生成的音乐。
旋律很轻,音箱声音也不大,在凌晨一点多的办公室里慢慢铺开。窗外是南山高新园还没完全熄掉的灯,远处楼顶的标识亮着,深南大道上偶尔有车从高架下穿过去。
白天这里是会议、消息和进度表。
到了深夜,只剩一首 AI 刚生成的音乐,两个没来得及下班的人,还有一整晚终于变绿的日志。
小璇听了十几秒,点点头。
“这个版本还挺好听。”
“那说明我们修得好。”
“音乐是模型生成的。”
“播放链路是我修的。”
“页面是我画的。”
“那就共同版权。”
她笑着关掉播放器。
“提交吧。”
我把代码提交到测试分支,又在 Bug 记录后面补上根因、兼容方式和验证结果。小璇把最终页面标成可开发版本,顺手把刚才那句状态说明也写进交互文档。
最后一条 Bug 后面,终于被我改成了已解决。
我没有马上关掉问题单,又从头把这一轮的清单过了一遍。
接口幂等、回调顺序、模型版本、批次状态、多结果展示。
那些前几天还轮流把报警群点亮的问题,现在后面全有了负责人、处理记录和验证结果。页面右上角的“未解决”从三变成二,又从二变成一,最后停在了零。
我把完整回归记录发进项目群,写下最后一句:
“本轮遗留问题已清零,企业项目转入稳定观察。”
凌晨一点多,群里当然没人秒回。
小马哥和伟源大概已经睡了,产品也不会在这个时间突然跳出来发表获奖感言。屏幕上只有我刚发出去的消息,以及问题面板里一个安安静静的零。
可我知道,这一阶段到这里,是真的可以结束了。
不是系统以后不会再出 Bug。
是下一次问题再来时,我们已经不用重新认识彼此,也不用靠某一个人把整条链路扛回去。小马哥知道怎么守住外部口径,伟源知道该在哪些边界拦问题,小璇能把系统状态翻译成用户真正看得懂的页面,我也终于学会把精力放在边界和取舍上,而不是见到每一处红字都自己扑过去。
项目从“靠人顶住”,走到了“靠团队接住”。
对当时的我们来说,这比问题清零更像一次交付。
收工的时候,已经快凌晨两点。
我关掉日志窗口,合上电脑,把桌上几个空杯子摞到一起。小璇在旁边收数据线和画板,刚才贴在显示器边上的那张便签被她揭下来,准备扔掉。
我从她手里拿了回来。
“这个留着。”
“留它干什么?”
我看了一眼上面的字。
任务完成,不等于所有结果成功。
“提醒自己,别看见一个 200 就以为什么都好了。”
小璇看着我,轻轻挑了下眉。
“只提醒工作?”
我把便签折好,塞进电脑包侧袋。
“生活也适用。”
“比如?”
“比如两个人住到一起,不代表磨合完成。一起做项目,也不代表天然就懂对方。很多状态得慢慢对,很多问题得说出来。”
我说完以后,又回头看了一眼已经暗下去的屏幕。
刚进这家公司时,我还是那个遇到事情只会自己往前顶的人。后来做第一个 C 端项目,招人,拉老同事回来,再到企业项目第一次提测,我们几个人一路补代码、补流程,也补彼此之间没说清楚的边界。
小璇也从那个下班以后提醒我别看工作消息的人,变成了今晚坐在我旁边、能顺着一张页面把接口问题追到模型协议的人。
工作没有把我们推远。
它终于也成了我们能够并肩站住的一部分。
她安静了两秒,伸手替我把电脑包没拉好的拉链拉上。
“今天总结得还行。”
我看着她的手,笑了一下。
“主要是设计师教得好。”
“少来。”
她嘴上嫌弃,手却很自然地伸过来,牵住了我。
我们关掉工位上最后一盏灯,一起往电梯口走。
整层办公室暗下去,玻璃窗外的城市反而更亮。电梯从一楼慢慢往上爬,数字一层层变化。我和小璇站在门口,肩膀靠着肩膀,谁都没再说话。
门打开以后,她先走进去,又回身按住开门键等我。
我站到她旁边,电梯门缓缓合上。镜面里映出两张熬夜以后都有点疲惫的脸,头发乱了,衣服也不如白天平整,可牵在一起的手还没松。
到一楼,保安抬头看了我们一眼。
“又这么晚?”
“最后几个问题。”我说。
他笑了笑,替我们按开玻璃门。
走出公司,南山高新园的夜风迎面吹过来,带着一点深圳夏夜的热。地铁早停了,路边还有几辆出租车,写字楼之间的灯一块亮、一块暗,像城市自己也有跑不完的后台任务。
小璇往我身边靠了一点。
“这回算结束了吗?”
“这一阶段算。”
“以后还会有新需求,也还会有新 Bug。”
“那明天呢?”
“明天先睡醒。”
我握紧她的手,转头看她。
“睡醒以后,好好过周末。”
她笑了一下,牵着我往路边走。
我跟在她旁边,脑子里还留着刚才那片全绿的日志,也留着问题面板里那个零,以及她靠在我身边时的温度。
企业项目的这一轮结束了。
团队不用再靠救火证明默契,我和她也不用在工作与生活之间反复切换身份。我们还是会争,会改,会碰到新的问题,只是已经知道该怎么一起把它们调顺。
恋爱和 Debug 一样,越磨合,越稳定。