news 2026/10/9 2:48:09

明天几点涨潮?我用华为云码道 Agent 写了个_会算潮的老黄历_,跟 NOAA 对拍误差不到一个指甲盖

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
明天几点涨潮?我用华为云码道 Agent 写了个_会算潮的老黄历_,跟 NOAA 对拍误差不到一个指甲盖

明天几点涨潮?我用华为云码道 Agent 写了个"会算潮的老黄历",跟 NOAA 对拍误差不到一个指甲盖

一、这玩意儿是干嘛的

先说人话。你去海边赶海、垂钓、或者码头装卸货,最想知道的一件事就是:今天几点涨潮、几点满潮、几点落潮、几点干潮。老一辈翻纸质潮汐表,年轻人要么记不住要么查半天。

我做了个网页,选个港口、选个日子,它给你画一条 24 小时的潮位曲线,下面四张卡片直接告诉你涨落潮的时刻和潮高。看着不难,对吧?

难的是下面这件事——这条曲线是我自己算出来的,不是抄谁的。我用六个人文分潮(M2、S2、N2、K1、O1、K2)自己合成潮位,然后拿它去跟美国国家海洋和大气管理局 NOAA 的官方预测逐点对拍。结果是这样的:

站点与 NOAA 留出窗预测 RMS
旧金山6.62 cm
波士顿8.20 cm
纽约 Battery8.28 cm
西雅图13.66 cm
查尔斯顿(潮差 4m+)28.73 cm

误差最小的三个站,基本就是"一个指甲盖厚度"以内。整个项目从头到尾是喂给华为云码道 CodeArts Agent写的,我只负责出题、验收、和一轮一轮被它挖坑再填坑。

这里得把"对拍"两个字说清楚,不然容易吹牛。NOAA 每天把全球几百个验潮站的天文潮预测(就是纯算月亮太阳那套)和实测水位(潮位计真读出来的)都挂在公开 API 上,不要钱、不要注册。我做的事是:只用它某几天的预测数据,反解出这个站的六个分潮常数,然后拿这组常数去算另外几天它没见过的潮水,再跟 NOAA 那几天的预测逐点比。两个完全独立的实现,在同一片天上算同一件事,误差小到厘米级——这才叫护城河,不然就是个玩具。

二、为什么做这个

我家老爷子退休后被拉去海边钓鱼,天天掐着手机算潮水。有次他问我"这潮水到底咋算的,是不是就是月亮拉的",我随口嗯了一声,回头一想:不对啊,我讲不清。月亮只是其中一环,太阳也掺和,还有地球自转、海岸地形、浅水摩擦……真正的潮汐预报是调和分析——把潮位拆成一堆不同周期、不同振幅、不同相位的正弦波,叠起来。

这事儿有意思就有意思在:它是个"看起来玄学、其实全是数学"的东西。而且它有客观真值——NOAA 每天把各站的预测和实测都挂在公开 API 上。也就是说,我算得准不准,不是我说了算,是拿尺子量的。

参赛选题我给自己定了三条铁律:一秒能看懂、有客观真值可对拍、别跟别人撞车。升旗、国庆头像框、版图、中秋月相、日地月 3D、会算题的烟花这些获奖过的方向全被我划掉了。潮汐这条线,我翻遍了获奖名单没见有人做,行,就它了。

三、先跑起来看看长啥样

别急着看代码,先看东西能不能跑。整个项目零运行时依赖,纯 Node 20+ ES Module,不用装任何第三方包,clone 下来三条命令就能起:

gitclone https://atomgit.com/lskcode/tidal-almanac.gitcdtidal-almanacnode--test# 8 项测试全绿nodebin/dev.mjs# 起本地预览 http://localhost:8080

浏览器一开,深底冷蓝一张脸,选站、看曲线、看四张涨落潮卡片。切到潮差最大的查尔斯顿,曲线振幅肉眼可见地胖了一圈:

四、我是怎么"钉"码道 Agent 的

这里得先交代整个工作方式。码道 Agent 是跑在华为云上的,它只能在我已登录的 AtomGit 账号里干活——所以参赛截图带着我的账号,是它亲手建仓库、亲手 push 的。

我一开始也吃过亏。第一版提示词写得跟聊天似的:“帮我做个潮汐预测的小工具”。结果它给我吐了一大段"我们可以这样设计、那样设计"的说明文档,代码一行没有。我盯着屏幕愣了三秒:这不行。

后来我摸清了套路,提示词必须钉到函数级,而且结尾永远挂一句铁律:

直接建文件并git add -A && git commit -m "..." && git push,不要只描述、不要贴代码正文,只回复 git log + 文件树 + 测试 pass/fail。

这句话一挂,它就老实了。下面是我第一轮喂给它的核心提示词(原样):

【R1 · 渔历 tidal-almanac 核心潮汐调和常数引擎】 在我 AtomGit 账号下建 public 仓库 tidal-almanac。技术栈:Node 20+ ES Module, 无任何运行时依赖,测试用 node:test。 - src/constituents.mjs 六条 {name, speed_deg_per_hr}: M2=28.984104, S2=30.000000, N2=28.439730, K1=15.041069, O1=13.943035, K2=30.085141 - src/epochs.mjs astroArgs(date, lonEast) 返回六分潮天文初始角 - src/predict.mjs h(t)=z0 + Σ A_i·cos(arg_i(t)+φ_i) - src/extrema.mjs 滑窗找局部极值 + 打 HIGH/LOW 标签 - test/harmonic.test.mjs 三条硬断言(不许自证) 直接建文件并 git add -A && git commit && git push,只回复 git log + 文件树 + 测试 pass/fail

我把整个项目切成 5 轮喂:R1 核心算法 + 测试,R2 数据层 + 谐波拟合,R3 前端页面 + 本地预览,R4 可验证护城河对拍,R5 README + 集成测试。每轮一个能独立交付的东西,跑完我就git clone到本地自己node --test复核——码道说绿不算绿,我本地跑绿才算绿。

五、架构:四层单向,一个交叉窗

代码分得很干净,一条链走到底:

constituents(角速度表) → epochs(天文初始角) → predict(谐波合成) → extrema(高低潮) ↑ fit_harmonics(从 NOAA 数据反解常数)

方案上我对比过三条路,最后选了调和分析:

方案精度可解释性是否需联网我的取舍
直接调 NOAA API 返回最高无(黑盒)是❌ 那还做什么项目
纯天文力学数值积分高中否❌ 太重,海岸效应进不去
六分潮调和分析高强(每个分潮有物理意义)拟合时需真值✅ 就它

调和分析的妙处在于,它把"潮水"这个模糊的东西拆成了六个能叫出名字的正弦波:M2 是主太阴半日潮(月亮引起的、半天一次),S2 是主太阳半日潮,K1/O1 是全日潮,N2/K2 是修正项。每个波一个振幅、一个相位,叠起来就是潮位。

六、核心算法,掰开揉碎

先解释一下那串看着像乱码的角速度数字是怎么来的。月亮绕地球转,但因为地球同时在自转、月亮自己也在往前跑,所以同一个地方连续两次正对月亮,间隔的不是 12 小时整,而是约12.42 小时——这就是 M2 主半日潮的周期。把 360° 除以 12.42 小时,就得到 M2 每小时转约28.984°。太阳那套同理,正 30.0°/小时。剩下 N2、K1、O1、K2 是月亮轨道倾角、偏心率带来的修正项。这六个数字一旦定死,整个模型的时间轴就锁住了,剩下的振幅和相位才是每个港口自己的"指纹"。

第一步,六个分潮的角速度,写死成常量表:

// src/constituents.mjsexportconstSPEED_DEG_PER_HR=Object.freeze({M2:28.984104,S2:30.000000,N2:28.439730,K1:15.041069,O1:13.943035,K2:30.085141});

第二步,给定任意时刻和经度,算每个分潮的"天文初始角"。这里有个坑,待会儿第九节专门说:

// src/epochs.mjsexportfunctionastroArgs(date,lonEast=0){constt=hoursSinceJ2000(date);// 半日潮每 12h 转一圈 → -2.0°/度经度;全日潮每 24h → -1.0°/度return{M2:mod360(BASE.M2+SPEED_DEG_PER_HR.M2*t-2.0*lonEast),K1:mod360(BASE.K1+SPEED_DEG_PER_HR.K1*t-1.0*lonEast),// ... 六个};}

第三步,谐波合成。潮位 = 平均海面 + 六个分潮余弦之和:

// src/predict.mjsexportfunctionpredict({constituents,z0_m,startDate,hours,stepMin=60,lonEast=0}){constout=[];for(lett=0;t<=hours;t+=stepMin/60){constdate=newDate(startDate.getTime()+t*3_600_000);constargs=astroArgs(date,lonEast);leth=z0_m;for(constcofconstituents){h+=c.amplitude_m*Math.cos((args[c.name]+c.phase_deg)*Math.PI/180);}out.push({t_ms:date.getTime(),height_m:h});}returnout;}

第四步,找高低潮。滑窗三点比较,相邻 6 小时内同类极值去重:

// src/extrema.mjsexportfunctionfindExtrema(series){constout=[];for(leti=1;i<series.length-1;i++){consta=series[i-1].height_m,b=series[i].height_m,c=series[i+1].height_m;constkind=(b>=a&&b>c)?'HIGH':(b<=a&&b<c)?'LOW':null;if(kind&&(!out.length||series[i].t_ms-out.at(-1).t_ms>6*3_600_000))out.push({...series[i],kind});}returnout;}

七、可验证护城河:差点栽在"自证"上

这一节是整篇文章我最想写的,因为这里我被自己的评审狠狠上了一课。

先交代下评审怎么玩。我并行开了三个 Agent,分别扮演 UI 专家、技术专家、产品专家,各自读真实代码 + 运行截图,独立打 0–100 分并挑 top5 毛病。第一轮分数是这样的:UI 62、技术 84、产品 82。看着挺高对吧?但产品专家那 82 分里藏着一条能致命的批评。

第一版对拍脚本我是这么写的:拉 NOAA 10 月 5–8 号的预测,用这 4 天数据拟合出六分潮常数,然后再用这组常数去重建这同一段 4 天,跟 NOAA 比。RMS 出来 2.86 cm,我美滋滋,觉得稳了。

然后产品专家一眼看穿,原话是:

RMS<5cm 是用 NOAA 10.05–08 预测段拟合常数后对同一段重建,属 in-sample 自洽,非"独立对拍"。懂行评委一击即中。

我盯着这句话看了大概十秒,后背有点凉。它说得对。用同一批数据拟合又用同一批数据验证,这叫自证,就像考试时答案抄题目。真正的独立验证应该是:用 A 窗拟合,去预测完全没见过的 B 窗。

我当场把 verify.mjs 推倒重写,改成交叉窗:

// evidence/verify.mjs —— 只在 FIT 窗拟合,去预测完全留出的 HOLDOUT 窗exportconstFIT_BEGIN='20261005',FIT_END='20261008';// 拟合窗exportconstHOLDOUT_BEGIN='20261009',HOLDOUT_END='20261012';// 留出窗,拟合时没见过constfit=fitHarmonics(fitSeries,NAMES,{ridge:0.01,ridgeNames:['S2','K2']});constourHoldout=predict({constituents:fit.constituents,z0_m:fit.z0_m,startDate:newDate(holdoutSeries[0].t_ms),hours:holdoutHours,stepMin:6});constvsHoldout=compare(holdoutSeries,ourHoldout);// ← 这才是真 out-of-sample

改完再跑,数字"变差"了——从 2.86 cm 变成 6.62 cm(旧金山)。但这 6.62 cm 是真的,是拿拟合窗之外的数据量出来的,我睡觉都踏实了。第二轮复审,产品专家把这条从"致命伤"划掉,分数补到了 90+。


谐波拟合本身也不复杂,就是把每个采样点的六个分潮角算成 cos/sin 两列,拼一个设计矩阵,解最小二乘:

// src/fit_harmonics.mjs —— 设计矩阵 [cos(v_i), sin(v_i), ..., 1],正规方程高斯消元functionbuildDesign(series,names,lonEast){returnseries.map(({t_ms})=>{constargs=astroArgs(newDate(t_ms),lonEast);constrow=[];for(constnofnames){row.push(Math.cos(rad(args[n])),Math.sin(rad(args[n])));}row.push(1);// z0returnrow;});}

拟合的时候还撞上一个物理细节:S2 和 K2 的角速度只差 0.085°/小时,在 3–4 天的短窗里两条曲线几乎重合(数学上叫近共线),最小二乘会疯。码道第一版直接崩,我让它对 S2/K2 加了个 Tikhonov ridge 正则 λ=0.01 压住,代价是这俩的振幅估计有偏。这个我如实写进了 README,没藏。

顺带说个有意思的现象:正因为 M2(月)和 S2(日)周期差一点点,每逢农历初一十五两者同相叠加,就是大潮;到初八二十三两者反相抵消,就是小潮。我这六分潮一合成,屏幕上那条曲线的高低起伏,其实就是月亮和太阳在天上较劲的结果。老爷子要是看到自己钓了半辈子鱼的潮水,原来是这么几道正弦波打架打出来的,估计得乐。

八、测试:8 项,而且不许自证

我给码道定的规矩是测试必须锚定第三方真值,不能"我算出来多少就断言多少"。举几个:

// test/harmonic.test.mjstest('astroArgs: M2 初始角在纪元时刻 = 211.8028',()=>{assert.ok(Math.abs(astroArgs(newDate(Date.UTC(2001,0,1,12)),0).M2-211.8028)<0.001);});test('predict: 旧金山 24h 序列落在 [-0.5, 4.0] m',()=>{/* 用真实 SF 常数 */});test('findExtrema: 24h 内 ≥2 高 2 低,相邻极值间隔 5–8h',()=>{/* 半日潮特征 */});

fit 的测试更狠一点:我拿一组已知的常数合成 72 小时数据,再让 fitHarmonics 去反解,断言它能把这些常数几乎原样恢复出来(振幅误差 <0.02m、相位 <2°)。这验证的是"拟合器本身是对的"。

九、真实的坑,一个没删

写技术文章我最烦那种"一切顺利"的叙事。真实情况是坑一堆:

坑 1:经度系数写错,还是个休眠 bug。码道第一版astroArgs里经度订正全用了-0.5 * lonEast。评审专家指出来:半日潮一个周期 12 小时转 360°,经度每差 1 度应该是 2° 相位差,全日潮才是 1°。为啥之前没炸?因为我所有调用都传lonEast=0,那个错误系数乘 0 等于没乘,藏得死死的。技术评审把它揪出来,我改成了半日潮 -2.0、全日潮 -1.0。

坑 2:一个站号是编的。我让码道选 5 个真实 NOAA 站,它给了个 Kahului Maui9672907。跑 verify 直接 HTTP 400。我拿 curl 挨个试,发现这站号 NOAA 根本不认——AI 编数据编到站号上了。换成实测能用的查尔斯顿8410140才通。

坑 3:网页一片空白。R3 做完我截图,曲线区全黑。查了半天是serve.mjs里把/直接映射到index.html,但浏览器解析./app.mjs时基准是根路径,变成/app.mjs404,模块加载链整个断了。改成/302 重定向到/web/index.html就好了。

坑 4:码道配额中途耗尽。跑到 R5,deepseek-v4-flash 模型直接弹"当前模型配额不足 [APIError]"。我切到 GLM-5.2 才把收尾推完。这个截图我留着了,挺真实的:

十、提效数据:哪些是它干的,哪些是我干的

环节码道 Agent我
建仓库 + 写 20 个文件✅ 全程出题
5 轮代码 push✅每轮本地 clone 复核
交叉窗对拍脚本初版重写(评审后)
找 bug(经度/站号/路径)❌✅ 评审 + 手动
真实 NOAA 数据✅ fetch 代码跑通 + 校验
写这篇文章❌✅

一句话总结体验:码道是个非常快的"初级工程师",你给的需求越具体它写得越准;但它会一本正经地编站号、会留休眠 bug、会把自证当验证。这些得靠人(或者靠你自己搭的评审 Agent)兜底。

十一、五维自检

写完回头量一量这个项目:

  • 眼前一亮:会算潮的老黄历,赶海的人一秒懂。

  • 可验证护城河:交叉窗 out-of-sample,跟 NOAA 公开预测对拍 RMS 6.6–13.7 cm,任何人 clone 下来node bin/verify.mjs一键复现。

  • 原创度:调和分析 + 独立反解常数 + 交叉窗,不是套模板。

  • 工程质量:8 项测试、零依赖、分层清晰。

  • 诚实度:S2/K2 有偏、Charleston 潮差大误差放大、观测 RMS 0.2m 是天气残差不是模型误差——全写进 README 了。

十二、本地跑 + 写在最后

数据源都是 NOAA CO-OPS 公开 API,一条命令重新拉数对拍:

nodebin/verify.mjs# 站 RMS@holdout(m) RMS@obs(m)# San Francisco 0.0662 0.2138# Boston 0.0820 0.1909# ...

最后说点感受。这个项目真正有价值的部分,不是"码道帮我写了个潮汐网站"——那种东西谁都能糊一个。有价值的是那 6.62 厘米:一个独立算出来的天文模型,跟地球上最权威的潮汐机构,在互相没见过的数据上,量到了同一个数字。

而这条从"2.86 厘米的自嗨"到"6.62 厘米的踏实"的路,是评审 Agent 一巴掌把我扇醒走出来的。AI 写代码很快,但让 AI 的产出变得可信,这件事暂时还得靠人较真。我后来把这套"三个专家并行评审 + 交叉窗验真"的流程固化了下来,发现它揪出来的问题,比我自己盯着屏幕看半小时还准。工具越强,越需要一只能挑刺的手。

仓库在这,欢迎 clone 下来跑,也欢迎拿你城市的潮水来打我的脸:https://atomgit.com/lskcode/tidal-almanac


如果这篇对你有启发,点个赞收藏一下,是我继续写"会算 XX 的老黄历"系列的最大动力。下一篇我打算算"奶瓶几点能凉到 40 度"。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 2:47:37

VS Code高效技巧:Code Action

不知道你写代码的时候有没有这种经历。文件写了一大半,顶部一堆乱七八糟的import,有没用的、顺序乱掉的;Java实体类写完字段之后,还需要批量生成get、set方法。 很多刚从IDEA转过来的小伙伴,习惯性按Alt + Enter,点灯泡修复。但是在VS Code里面,有两类动作:普通Code A…

作者头像 李华
网站建设 2026/10/9 2:47:33

手把手教你学Simulink——基于小波神经网络的电机转子温度软测量仿真

目录 手把手教你学Simulink——基于小波神经网络的电机转子温度软测量仿真 一、研发目标与系统架构 1.1 研发目标 1.2 系统架构 二、机理分析与特征选择 2.1 热传递路径 2.2 输入特征选择 三、小波神经网络设计 3.1 网络结构 3.2 前向传播 3.3 训练算法 四、Simulin…

作者头像 李华
网站建设 2026/10/9 2:47:13

苦等11年,IDEA 2026.3终于决定加上这个新特性了

大家用IDEA写Java是不是也会经常遇到我遇到的这种情况 昨天我在写一段 Java 代码&#xff0c;大概是这样的&#xff1a; X.getBar().hashCode();getBar() 是可能返回 null 的。IDEA 很贴心地飘了个黄&#xff0c;提示我“这玩意儿可能为空”。 我心想&#xff0c;行吧&#xff…

作者头像 李华
网站建设 2026/10/9 2:44:47

轻量化后,产品结构树和PMI还在吗?SpinFire Convert让关键信息随模型保留

一份设备总装模型要交给车间和质量部门&#xff0c;设计人员通常会先准备便于查看的轻量化文件。模型变轻&#xff0c;主要是因为转换时会根据查看用途处理几何表示和设计数据。这样通常更便于打开和共享&#xff1b;相应的风险是&#xff0c;如果转换时只关注外形&#xff0c;…

作者头像 李华