news 2026/10/5 2:29:57

拖一条 GPX 就知道几点登顶?我用码道 Agent 写了个_登山钟_,让三个老公式互相打架

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
拖一条 GPX 就知道几点登顶?我用码道 Agent 写了个_登山钟_,让三个老公式互相打架

拖一条 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 1892800m/h + 10min/100m经验法则,纯移动时间
Tobler 1994W=6·e^(−3.5|φ+0.05|)德国徒步实测拟合
Braun 2001dist/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,实际耗时是登山者的普遍记录)喂进三个公式,结果是这样的:

山峰距离爬升实测NaismithToblerBraun
Ben Nevis7.0km1340m8h3.632.741.85
Sgora Dubh4.0km700m2.5h1.971.481.03
Buachaille14.0km1500m9h5.304.073.30
Stob Coire8.5km1200m6h3.702.792.10

第一眼看我以为是 bug——怎么三个公式全都比实测快一大截,Naismith 差 55%,Braun 差到 77%?查了半天,不是代码错,是口径对不上,而且这个"对不上"本身就是最该写进文章的发现:

  1. 实测时间是往返、含休息、含拍照吃饭、含地形磨蹭;而这些公式算的是纯移动的爬升时间。Ben Nevis 那 8 小时是上下山来回加歇脚,公式只算了上山移动。

  2. 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 帮我省了至少两周的敲键盘时间,但真正决定项目成色的,是那几次"这验证到底成不成立"的较真。如果这个系列对你有启发,三连走起,是我继续肝的动力;有想看的下一个选题,评论区点菜。

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

第068篇 sealed class:状态建模的利器

sealed class 在 Kotlin 里的地位,相当于"用类型系统表达状态机"。它的价值不在语法,而在一个很实际的痛点:当一个值有多种可能形态时,怎么保证"处理了这几种形态"这件事不会漏。 面试里问 sealed class,问的往往不是"是什么",而是"你…

作者头像 李华
网站建设 2026/10/5 2:28:57

Android 自定义 View 从零到实战:手写 6 大高频组件,附绘制异常排查清单

摘要:从零手写 Android 自定义 View 的实战指南。深入拆解 Measure、Layout、Draw 三大核心步骤,带你完成月份选择器、防滚动列表、动态饼图、广告轮播等 6 个高频组件,并附赠绘制异常排查清单。全程代码可复制、可直接落地。 目录 ① 视图构建核心三步骤解析 1.1 onMeasure…

作者头像 李华
网站建设 2026/10/5 2:28:57

Python实战第14期:常用标准库

文章目录 引言:为什么需要标准库? 一、os模块 1. 导入os模块 2. 获取当前工作目录 3. 切换目录 4. 列出目录内容 5. 创建目录 6. 删除目录 7. 删除文件 8. 重命名文件或目录 9. 路径操作 10. 遍历目录树 11. 环境变量 12. 执行系统命令 二、datetime模块 1. 导入datetime模块…

作者头像 李华
网站建设 2026/10/5 2:28:28

Linux 权限别死记!一文搞懂 rwx、数字权限、目录权限坑点

这几天是国庆我久违的睡了一个大觉 目录 一、 root与普通用户的区分 二、Linux的权限管理 2.1 三种基本权限 2.2 权限的两种表示法&#xff1a;符号和数字 三、权限的修改与验证 四、三个关于权限的问题 4.1 进入一个目录需要哪些权限 4.2 读文件需要什么权限 4.3 写入…

作者头像 李华
网站建设 2026/10/5 2:28:07

交易最大的敌人,终于被机制打败

在金融交易市场中&#xff0c;绝大多数交易者的亏损&#xff0c;从来不是败给了行情&#xff0c;而是败给了自己。 无数交易数据和实战案例证明&#xff1a;市场的不确定性并不可怕&#xff0c;人性的确定性弱点才是交易亏损的核心根源。贪婪、恐惧、侥幸、犹豫、冲动、自负&am…

作者头像 李华