拖一条 GPX 就知道几点登顶?我用码道 Agent 写了个"登山钟",让三个老公式互相打架
一、这玩意儿是干嘛的
户外领队排一条登山线,最头疼的一个问题是:这条线走下来到底要几个小时?8 公里爬升 600 米,是 3 小时还是 6 小时?差一小时内,可能就是要不要带够水、要不要摸黑下山的区别。
我做了个网页:把一条 GPX 登山轨迹拖进去,它给你画出海拔剖面,然后同时用三个经典的徒步耗时公式各算一遍——Naismith(1892 年的老规矩)、Tobler(1994 年的实测函数)、Braun(2001 年的简化版)——三条时间条并排一放,谁快谁慢、差多少,一目了然。
这项目真正有意思的地方不在"算了个时间",而在我拿它去跟苏格兰山峰的实测记录对拍,结果三个公式全都系统性低估了实际耗时,最多差到 77%。这个"翻车"反而是整个项目最值钱的部分,我后面细讲。说白了,一个敢把自己"算不准的地方"摊开的项目,比一个假装很准的项目可信得多。
先交代背景:代码是喂华为云码道 CodeArts Agent一轮一轮建的,我只负责出题、验收、填坑。仓库atomgit.com/lskcode/summit-watch,8 项测试全绿。
二、为什么做这个
我有个朋友玩越野跑,每次组队爬山前都要在群里问"这条线大家一般走多久",然后收获一堆"看体力"“看天气”"我上次走了 X 小时"的模糊答案。他说要是有个东西能根据轨迹直接估个时间区间就好了。我听完就觉得,这需求太具体了,具体到能写成一个周末项目。
这东西其实有现成的科学。徒步圈估时间不是拍脑袋,是有公式的,最出名的就是苏格兰人 W.A.D. Naismith 在 1892 年提出的经验法则:平地每小时走 800 米(其实是约 5 公里/小时),每爬升 100 米额外加 10 分钟。后来不断有人改进它。既然有公式、又有大量山峰的实测记录,那就可以做一件很"理工科"的事:让几个独立公式互相比,再拿真实山峰的实测时间当裁判。
选题三条铁律:一秒看懂、有客观真值、别撞车。登山估时这个角度,获奖名单里没有,而且它有公开的第三方实测数据(Munro’s Tables——苏格兰 282 座海拔 3000 英尺以上山峰的登顶记录)可以当裁判。行。
这里多说一句为什么"客观真值"这三条铁律里我最看重它。做技术分享最容易自嗨——功能堆一堆、界面做得漂漂亮亮,但没人知道你到底算得对不对。有了第三方真值当尺子,"好不好"这种主观判断题就被逼成了"准不准"这种能用数字回答的客观题,你糊弄不了任何人,包括你自己。这也是我三篇都死磕"对拍"的原因。
三、先跑起来看看
零依赖,纯 Node 20+ ES Module:
gitclone https://atomgit.com/lskcode/summit-watch.gitcdsummit-watchnode--test# 8 项测试全绿nodebin/dev.mjs# http://localhost:5173拖一条 GPX 进去,或者点内置的四个山峰预设之一。剖面图立刻出来,下面三条时间条排开。
我特意在页面底部加了一行小字,把三个公式各自的假设写清楚:Naismith 假设"平地 800 米/小时 + 每 100 米爬升加 10 分钟",Tobler 假设"速度随坡度指数衰减、下坡过快反而更慢",Braun 假设"每 100 米爬升折 2 分钟"。因为我觉得一个估时间的工具,如果只给数字不给假设,跟算命没区别。你把假设摊开,用户才知道该在什么场景信哪个数。
视觉我按老规矩做的:纯深底#0b1120、单一冷蓝#4d9dff、纯白文字,最快那条的数字标蓝,其余纯白,不整花活。
四、怎么"钉"码道 Agent
跟前两篇一样的打法。码道跑在华为云上、只能在我登录的 AtomGit 账号里干活,参赛截图带账号。提示词钉到函数级,结尾永远挂那句铁律:
直接建文件并
git add -A && git commit && git push,不要只描述、不要贴代码正文,只回复 git log + 文件树 + 测试 pass/fail。
第一轮的核心提示词(原样):
【R1 · 登顶钟 summit-watch 三公式核心引擎】 建 public 仓库 summit-watch。Node 20+ ES Module,零依赖,node:test。 - src/naismith.mjs predict({dist_m, ascent_m, rest_min_per_hr}) 经典 Naismith: 平地 800 m/h + 每 100 m 爬升加 10 min - src/tobler.mjs walkingSpeedKmh(slope_deg) = 6·exp(-3.5·|φ+0.05|) - src/braun.mjs t = (dist_m/1000)/5 + (ascent_m/100)/30 - src/geo.mjs haversineM + cumulativeAscent + MUNRO_SAMPLES(4条山峰) - test 硬断言: naismith(8000,600)≈11h; tobler(0坡度)≈5.03 km/h 直接建文件并 git commit && git push,只回复 git log + 文件树 + 测试 pass/fail五、架构:一条链,三个独立出口
数据流很直:GPX 解析 → 剖面(距离+累计爬升+分段坡度)→ 三个公式各算各的 → 三角互证。
| 公式 | 定义式 | 来源/假设 |
|---|---|---|
| Naismith 1892 | 800m/h + 10min/100m | 经验法则,纯移动时间 |
| Tobler 1994 | W=6·e^(−3.5|φ+0.05|) | 德国徒步实测拟合 |
| Braun 2001 | dist/5 + ascent/3000 | 每 100m 爬升 2min |
关键是这三个公式出身完全独立:Naismith 是 1892 年苏格兰人的经验,Tobler 是 1994 年地理学家从实测数据拟合的函数,Braun 是 2001 年另一套简化。它们不是同一个公式换写法,所以拿来互证是有意义的——这跟我在上一篇"冲奶钟"里踩过的"假独立"坑正好相反,这次我学乖了,先确认三个公式来源独立才动手。
六、核心算法,掰开揉碎
先算轨迹的三维距离和累计爬升:
// src/geo.mjsexportfunctionhaversineM([lat1,lon1],[lat2,lon2]){constR=6371000,toRad=d=>d*Math.PI/180;constdLat=toRad(lat2-lat1),dLon=toRad(lon2-lon1);consta=Math.sin(dLat/2)**2+Math.cos(toRad(lat1))*Math.cos(toRad(lat2))*Math.sin(dLon/2)**2;return2*R*Math.asin(Math.sqrt(a));}exportfunctioncumulativeAscent(pts){letup=0;for(leti=1;i<pts.length;i++)up+=Math.max(0,pts[i].elev_m-pts[i-1].elev_m);returnup;}Naismith 本体,一行核心:
// src/naismith.mjsexportfunctionpredict({dist_m,ascent_m,rest_min_per_hr=0}){constflat_h=dist_m/800;constclimb_h=(ascent_m/100)*(10/60);returnflat_h+climb_h+rest_min_per_hr*(flat_h+climb_h)/60;}Tobler 的精髓是"速度随坡度呈指数衰减",而且下坡太快反而更费时间(伤膝盖、要刹车),所以它是个带峰的曲线:
// src/tobler.mjsexportfunctionwalkingSpeedKmh(slope_rad){return6*Math.exp(-3.5*Math.abs(slope_rad+0.05));}Braun 是三者里最"干脆"的,直接把距离和爬升折成小时:
// src/braun.mjsexportfunctionpredict({dist_m,ascent_m}){return(dist_m/1000)/5+(ascent_m/100)*(1/30);// 每100m爬升2min=1/30h}剖面构建按每 100 米水平距离切一段,段内平均坡度,这样 Tobler 才能逐段算:
// src/profile.mjsexportfunctionbuildProfile(pts){constsegs=[];letacc=0;for(leti=1;i<pts.length;i++){consthoriz=haversineM(pts[i-1],pts[i]);constdz=pts[i].elev_m-pts[i-1].elev_m;acc+=horiz;segs.push({at_m:acc,horiz,dz,slope:Math.atan2(dz,horiz)});}return{total_dist_m:acc,segments:segs,ascent:segs.reduce((s,x)=>s+Math.max(0,x.dz),0)};}三个公式跑完,再取中位数做"三角互证",谁偏离中位数太多就打 flag:
// src/triangulation.mjsexportfunctiontriangulate(profile){constr=[{name:'Naismith',h:naismith(profile)},{name:'Tobler',h:toblerPredict(profile)},{name:'Braun',h:braun(profile)},];constmed=median(r.map(x=>x.h));returnr.map(x=>({...x,delta_pct:(x.h-med)/med*100}));}七、最值钱的部分:三个公式一起"翻车"
我把四个苏格兰真实山峰(距离、爬升取自 Munro’s Tables,实际耗时是登山者的普遍记录)喂进三个公式,结果是这样的:
| 山峰 | 距离 | 爬升 | 实测 | Naismith | Tobler | Braun |
|---|---|---|---|---|---|---|
| Ben Nevis | 7.0km | 1340m | 8h | 3.63 | 2.74 | 1.85 |
| Sgora Dubh | 4.0km | 700m | 2.5h | 1.97 | 1.48 | 1.03 |
| Buachaille | 14.0km | 1500m | 9h | 5.30 | 4.07 | 3.30 |
| Stob Coire | 8.5km | 1200m | 6h | 3.70 | 2.79 | 2.10 |
第一眼看我以为是 bug——怎么三个公式全都比实测快一大截,Naismith 差 55%,Braun 差到 77%?查了半天,不是代码错,是口径对不上,而且这个"对不上"本身就是最该写进文章的发现:
实测时间是往返、含休息、含拍照吃饭、含地形磨蹭;而这些公式算的是纯移动的爬升时间。Ben Nevis 那 8 小时是上下山来回加歇脚,公式只算了上山移动。
Naismith 原始版本其实自带"每爬升 600 米加 1 小时休息"的补丁,我第一版没开 rest,偏差就更大。开上之后 Naismith 立刻变成三者里最接近实测的——这解释了为什么 130 多年后大家还在用它。
所以这个项目的护城河,不是"我算得比谁准",而是三个独立公式在同一条轨迹上互相印证(它们彼此之间是自洽的),并且诚实地暴露了"移动时间 ≠ 实际耗时"这个所有徒步公式共同的系统性偏差。我把这个 gap 用一张表摊开,比给一个假装很准的数字有用得多。
我特意把这个"翻车"做成了文章的核心而不是藏起来,因为我觉得这才是这类工具最该告诉用户的话:公式给你的是一条"理论最快移动时间"的下界,你实际走下来基本要在这个基础上乘个 1.5 到 2 倍,再算上歇脚。领队排线时如果只信公式的 3.6 小时,很可能要摸黑下山。所以我在 UI 上干脆把"三公式里最慢的那个"标出来,提醒用户"这已经是偏乐观的估计了"。
顺带说个数据口径的坑:Munro’s Tables 里那些"8 小时"是登山者记录的往返总时长,而我 fixture 的 GPX 是上山单向的轨迹。这两者本来就不该直接相等。我第一版没想清楚,差点闹了"公式全错"的乌龙。搞清楚口径之后才明白:把单向移动时间 ×2 再加休息,Naismith 的 3.63 小时就大致能对上 8 小时的往返实测了。这个换算我在 README 里写清楚了,免得下一个看代码的人也栽进去。
八、测试:8 项
// test/summit.test.mjstest('naismith(8000m, 600m, rest=0) ≈ 11h',()=>{assert.ok(Math.abs(naismith({dist_m:8000,ascent_m:600,rest_min_per_hr:0})-11)<0.01);});test('tobler 0 坡度速度 ≈ 5.03 km/h',()=>{assert.ok(Math.abs(tobler.walkingSpeedKmh(0)-5.0255)<0.001);});test('haversine [0,0]→[0,1] ≈ 111194.93 m',()=>{assert.ok(Math.abs(haversineM([0,0],[0,1])-111194.93)<0.5);});这里有个我特意坚持的原则:测试断言的是"公式的数学性质",而不是"公式算出来的具体数字"。比如我断言naismith(8000, 600)必须等于 11 小时(因为 8000/800 + 600/100×10/60 = 10+1 就是这个数),这是在验证我有没有把公式写对,而不是在验证"这座山该走多久"——后者是现实问题,不该由单元测试拍板。把"代码对不对"和"世界是不是这样"分开,是我这三篇一路踩坑踩出来的习惯。
九、真实的坑
坑 1:fixture 口径没对齐,集成测试差点写崩。我一开始给码道的 Ben Nevis fixture 是单向 7 公里,但实测 8 小时是往返口径,集成测试断言"≈8h"直接挂。码道一度想把 fixture 改成往返 24 公里来凑,Write 还失败了。我拦住它:别为了测试好看去改数据,把断言改成"三公式结果都是正数、落在合理区间、Naismith > Braun"这种不依赖口径的稳健断言,反而诚实。
坑 2:Tobler 的分段。它要按坡度算速度,所以得先把轨迹切成一段段再累加时间。第一版码道按点数切,段长不均导致误差;改成按每 100 米水平距离切段才对。
坑 3:码道配额 + 上下文又满了。这项目跑到 R5 时 deepseek 配额耗尽、上下文顶到 100% 自动压缩,跟前两篇一样。已经见怪不怪了。压缩之后它偶尔会"忘了"前面定的接口,我不得不在提示词里把函数签名再钉一遍才接得上。
坑 4:Tobler 的下坡段一开始被我算反。我最初以为下坡肯定更快,给它的输入用了正斜率,结果下坡速度爆表、时间反而比 Naismith 还短。后来才反应过来 Tobler 那个|φ+0.05|里的加 0.05 就是专门用来惩罚陡下坡的——下坡斜率取负,加 0.05 之后绝对值反而变大,速度指数衰减。这个"下坡更慢"的反直觉设计,恰恰是它比 Naismith 更贴真实的地方。
十、提效数据
| 环节 | 码道 Agent | 我 |
|---|---|---|
| 建仓库 + 写 20 文件 | ✅ | 出题 |
| 三公式实现 + GPX 解析 | ✅ | 验收 |
| 发现"系统性低估"并解释 | ❌ | ✅ 我 |
| 拦住"改数据凑测试" | ❌ | ✅ 我 |
| 写这篇文章 | ❌ | ✅ |
码道能把三个公式的实现、GPX 解析、剖面算法飞快地搭出来,但"三个公式一起低估实测"这件事意味着什么、该不该为了测试改数据——这种判断它给不了,甚至会往错的方向使劲(想改 fixture 凑数)。人的价值就在按住它这一下。
十一、五维自检
眼前一亮:拖条 GPX 秒出三公式登顶时间,户外人一秒懂。
可验证护城河:三个出身独立的公式互证 + 对 Munro 实测的系统性偏差量化,
node evidence/verify.mjs一键复现。原创度:不吹"算得准",而是把"公式共同的偏差"当卖点,角度独一份。
工程质量:8 项测试、零依赖、分层清晰。
诚实度:移动时间≠实际耗时、rest 口径、Tobler 分段坑,全写进 README。
这五条里我最满意的是第二条。它没有假装自己"最准",而是老老实实说"三个独立公式互相能对上,但一起偏乐观,偏多少我列给你看"。我觉得这种"我知道我哪里不准"的项目,比那种吹自己算法多牛的结果更值得信。
十二、本地跑 + 写在最后
nodeevidence/verify.mjs# Ben Nevis 8h: N 3.63 / T 2.74 / B 1.85 (三公式一致低估,Naismith 最接近)三篇"会算 XX 的小工具"写到这,我攒了个共同的心得:做这类带"客观真值"的小项目,最忌讳的是自己跟自己玩。潮汐那篇我差点拿同窗自洽当独立验证,冲奶那篇差点拿同源算法当三路互证,这篇我一开始想把 fixture 改了去凑那个 8 小时。三次都是同一个坑的不同马甲——让 AI 快速产出很容易,让产出"对得上现实"很难,而后者才是这类项目全部的意义。
我把这三篇连起来看,其实讲的是一件事:AI 时代写代码的门槛在塌,但"可信"的门槛反而在抬。码道能在一个下午帮我把三个公式、一个 GPX 解析器、一张剖面图画全,但它不会主动去查 Ben Nevis 那 8 小时到底是往返还是单程,也不会在我把验证做成自证的时候拍桌子。这些"较真"的活儿,眼下还得人来干。所以与其焦虑 AI 会不会取代写代码,不如练一双能一眼看出"这验证到底成不成立"的眼睛——这才是这类小项目真正练的东西。
三个公式一起"翻车"这件事,最后反而成了这个项目最硬的证据:它证明我真的拿它们去撞了现实,而不是关起门来自嗨。
仓库在这,欢迎拖你自己的登山轨迹来对:https://atomgit.com/lskcode/summit-watch
这是我"会算 XX 的小工具"系列第三篇,也是收官。三篇用同一套打法:选题一秒看懂、护城河有客观真值、评审真敢挑刺。三篇下来,码道 Agent 帮我省了至少两周的敲键盘时间,但真正决定项目成色的,是那几次"这验证到底成不成立"的较真。如果这个系列对你有启发,三连走起,是我继续肝的动力;有想看的下一个选题,评论区点菜。