本文记录我利用业余时间,给自己做的一个「旅行秘书」Agent:知道我的位置、周围有什么、天气如何、行程怎么安排,然后用自然语言主动发到我的手机上。下面先是技术部分,最后是感受部分。
技术篇
一、前提:位置数据是现成的
最近在使用 Traccar 这个应用,简单来说就是时刻记录自己的 GPS 位置,然后发送到设定好的服务器里面,我已经使用了近一年的时间,基本上每天 24 小时都开着,所以这次旅行我也会一直开着。

因为位置 24 小时都在记录,且 Traccar 服务端和 agent 都在我自己的服务器上,获取位置非常简单且迅速,Traccar 服务端有现成的 API 可以使用。但是需要注意坐标系转化。
请求返回类似:
[
{
"id": 123456,
"deviceId": 1,
"protocol": "osmand",
"deviceTime": "2026-05-30T08:15:42.000+00:00",
"fixTime": "2026-05-30T08:15:42.000+00:00",
"serverTime": "2026-05-30T08:15:45.123+00:00",
"valid": true,
"latitude": 35.681236,
"longitude": 139.767125,
"altitude": 24.0,
"speed": 0.54,
"course": 187.0,
"address": null,
"accuracy": 12.0,
"attributes": {
"motion": false,
"batteryLevel": 78,
"distance": 0.0,
"totalDistance": 152340.55
}
}
]
| 字段 | 含义 | 代码里怎么用 |
|---|---|---|
latitude / longitude | 经纬度 | 直接取 |
deviceTime | 设备上报时间(ISO8601) | 当前位置时间戳 |
speed | 速度,单位是节(knots) | × 1.852 → km/h |
attributes.motion | 是否正在移动(bool) | 乘车短路规则的关键 |
attributes.batteryLevel | 设备电量 | 可选,没用但有 |
valid | 定位是否有效 | 可做兜底过滤 |
整个旅行助手的数据基础获取很简单。
二、整体思路与数据源
agent 的大概功能是提醒我周边有什么美食和景点、接下来的日程是怎么样的,以及未来日程的天气状况。
日程:我认为我的日程可能会经常变动,所以把日程放在了 notion 上,然后去 notion 申请了Token,可以读取日程文件,这样我可以随时改动,agent 也能够读取到最新的文件了。

天气:这块踩了一个坑。开始的时候打算用和风天气的 API,数据详细而且有免费额度,但测试的时候才发现和风天气对海外天气提供的参数比较少。因此国外改用 Open-Meteo(免费、无需绑卡),国内坐标再自动切回和风。Open-Meteo的可调参数非常多,API使用也非常简单,官方文档里有调用示例。


周边与坐标(谷歌):真正花谷歌钱的是另外两处——查周边地点用 Google Places (New),把日程地名解析成坐标用 Google Geocoding。这两个绑定银行卡还能获取 300 刀的免费额度,不过我每天的请求量也不会触碰到收费标准。

交通:由于日本交通实在复杂,导航不如直接用谷歌地图了。交通状况我没找到免费的 API,收费的 API 也十分复杂就放弃了。让我没想到的是谷歌(包括谷歌地图)也没有提供日本交通状况的 API,上网搜了下发现谷歌也是用的第三方的数据,并且不提供 API,那就在地图上一块看吧。
一次完整的运行(run_once)大致是这样一条链路:

代码结构上做了模块拆分,主程序负责「编排」,脏活累活下沉到 trip_bot/ 包里:
| 文件 | 职责 |
|---|---|
location_nearby_bot.py | 编排入口,run_once / run_loop / 早晚提醒 |
trip_bot/utils.py | haversine 距离、HTTP 重试、JSONL 读取、文本清理 |
trip_bot/weather.py | 天气服务层(Open-Meteo / 和风天气,含 JWT 签名) |
trip_bot/geocoding.py | 景点坐标解析、把行程地点匹配到坐标 |
trip_bot/notion.py | Notion 日程的读取与回写 |
三、设计取舍(几个我觉得有意思的点)
1. 在车上直接本地规则跳过
旅行里有大量时间是在移动——坐电车、新干线。这种时候查周边毫无意义,还会让 agent 发送大量垃圾消息。所以加了一条纯本地规则判断是否「乘车状态」,命中后直接跳过周边查询和 LLM。
不过获取「速度」实际情况下比较复杂,Traccar 直接报的 speed 不够可靠(因为信号),所以速度判定做了两层:
第一层:GPS 报速(speed_kmh)
Traccar 给的 speed 字段(节 → km/h)。两个问题:
- 隧道、电车、新干线车厢里 GPS 信号差,会报成
speed=0; - 上报不及时,两次采集间隔会被拉长,采不到瞬时速度。
第二层:位移反推速度(computed_speed_kmh)
循环模式里,每轮拿到新位置时跟上一轮的位置比,自己算实际速度(location_nearby_bot.py:1734):
# 计算基于位移的实际速度(兜底 GPS speed=0 的情况,如高铁隧道)
if _last_prefetch_pos is not None:
_prev_time = parse_iso_time(_last_prefetch_pos.time_raw)
_cur_time = parse_iso_time(cur_pos.time_raw)
if _prev_time and _cur_time:
_elapsed_s = (_cur_time - _prev_time).total_seconds()
if _elapsed_s > 30: # 至少隔 30 秒才算,避免噪声
_dist_m = haversine_m(_last_prefetch_pos.lat, _last_prefetch_pos.lon,
cur_pos.lat, cur_pos.lon)
_disp_speed = (_dist_m / _elapsed_s) * 3.6 # m/s → km/h
_loop_computed_speed = _disp_speed
即 速度 = 两点间 haversine 距离 ÷ 时间差 × 3.6。两个关键点:
- 用的是
deviceTime的时间差,所以上报不及时也没关系——间隔变长,距离也会跟着变大,反推出的速度依然成立; - 要求间隔 > 30 秒才算,否则间隔太短时 GPS 抖动会把噪声放大成假高速。
两者取 max
最后在 decide_activity 里两个速度取较大值当判定依据(location_nearby_bot.py:371):
gps_speed = position.speed_kmh or 0.0
effective_speed = max(gps_speed, computed_speed_kmh) # 谁高信谁
if effective_speed >= transit_speed_threshold_kmh: # 默认 18 km/h
...判定为乘车中
所以这条规则更完整的表述是:
乘车判定速度 = max(GPS 报速, 位移反推速度)
- 有效速度 ≥ 18 km/h;或
- motion=true 且 GPS 报速 ≥ 12 km/h
这样即使在高铁隧道里 GPS 报 0,只要两次定位之间挪了足够远,位移反推速度照样能识别出「在乘车」,正确短路掉周边查询和 LLM。
2. 多设备择优:手机和平板谁更可信
我同时带了手机和平板(都接在 Traccar 上报位置)。两个设备的定位时间、精度、移动状态经常打架。select_best_position 做的就是择优:
- 只有一个设备有数据 → 直接用;
- 两个都有 → 比较上报时间新旧、距离是否异常跳变、谁在移动谁静止;
- 输出时附带一个
meta,说明「为什么选了这个设备、用了哪条规则」,方便事后在快照里复盘。
另外发现了一个 traccar iOS 客户端的 bug,有的情况下即使开了后台刷新,也会不上报位置,只能手动点击发送位置,然后才会把寄存的数据一口气全发送,卸载手机 app 重装后解决。
3. 防止记忆文件膨胀
travel_history.jsonl 是给后续 LLM 看的「旅行记忆」,但如果每 15 分钟写一条,文件很快膨胀,而且全是废话。所以 should_append_history 定了一套写入门槛,只在状态真正变化时才落盘:
- 活动状态切换(比如从静止变移动);
- 移动距离超过阈值(默认 120m);
- 距上次记录时间够久(默认 20 分钟);
- 同一地点的「重访」(同点 100m 内、但间隔 ≥ 180 分钟)。
反过来,同地点短间隔的重复、距离和时间都没到阈值且状态没变,就静默丢弃。再加一个自动裁剪(默认最多 2000 条,超了只留最近的)。
实际上的travel_history.jsonl用量没有想象的那么多,结束后统计了一下总条数:1081 条,时间跨度:约 21.3 天(510 小时)。
4. 静默轮询:移动不够就跳过
循环模式下,如果跟上次调用 LLM 的位置相比移动不足 120m,就跳过 LLM 和 Telegram——人还在原地,没必要重复推送。调度本身也分时段:
- 白天 6:00–23:00:每 15 分钟;
- 夜间 23:00–6:00:每 60 分钟;
- 检测到正在移动:加密到每 8 分钟。
不然这种主动推送的 Agent 很快就会变成骚扰。
5. LLM 这层:身份固定 + 多模型兜底
发给本地 URL LLM 的 payload 信息量其实不小,包含:位置、时区、timeAnalysis(现在时间 / 设备时间 / 设备时钟偏差 / 距上次记录多久)、周边地点、今天和明天天气、Notion 行程、最近几条旅行记忆和摘要。
LLM 使用的是 CPA 反代的 claude,但是会有封号的风险,请谨慎使用,根据我个人经验少量使用问题不大。

6. 早晚两条主动提醒
除了「到了新地方播报」,还有两条定时触发:
- 早间:读 Notion 当天日程 + 当天天气,生成「今日提醒」;

- 晚间:读次日日程 + 次日天气,生成「明日提醒」,顺便帮我把第二天的安排过一遍。

7. 热更新:时刻给运行中的 agent 打补丁
旅行途中经常想顺手改两行逻辑,或者通过LLM修改行程,但总不能为此 SSH 上服务器 kill 进程再重启。于是做了套极简热更新,本质就是 wrapper + 进程自重启 两层。
外层 run.sh:一个 while true 死循环,bot 一退出就重新拉起,且不判断退出原因:
- 正常退出(码 0)→ 隔 2 秒重启;
- 异常退出(非 0)→ 指数退避 2→4→…→封顶 30 秒再重启。
内层 run_loop:启动时记下主脚本和 config.env 的修改时间(location_nearby_bot.py:1692–1693);每轮循环末尾、sleep(interval) 之前重新比对一次(:1840–1850)。一旦发现文件变了,就打条日志、 return 退出,进程以 0 结束;外层 wrapper 2 秒后把它重新拉起,新代码在全新进程里自然加载。
bot 自己发现代码变了就主动退出,wrapper 负责复活。
配合 Claude 的 Telegram 插件:人在外面,发条消息让 Claude 改location_nearby_bot.py,mtime 改变,bot 在下个轮询周期自己退出、被 wrapper 拉起、加载新逻辑——全程没碰过终端。等于在地铁上发条消息,就给正在跑的 agent 打了热补丁。

四、附带产物:按地点自动合并旅行视频
旅途中经常会使用大疆的 nano 拍摄,回来想剪辑,文件名只有时间戳,而且有大量的视频文件。于是写了个配套脚本 windows_video_merge_by_place.py:
- 读视频文件名里的拍摄时间;
- 拿这个时间去匹配旅行轨迹(
travel_history)里最接近的位置点(容忍窗口 120 分钟); - 反查 Google Places 得到地点名;
- 把「相邻 300m 内、间隔不超过 90 分钟」的视频归为同一个地点,自动用 ffmpeg 合并。
相当于让前面攒下的位置记忆反哺了视频整理。还配了一个 windows_manual_video_merge.py 做纯手动合并兜底。
感受篇
一、缘起:为什么是这趟旅行
今年三月份我提了辞职,然后打算去日本旅行一段时间。这次去日本主要是进行拍照,我过去一年经常在网上搜索相关的拍照机位,也整理了不少资料,正好趁这次旅行一次性拍完(其实只能拍一部分)。本来打算是过年那段时间去北海道的,不过有事情耽误了没去成,三月份估计那边也没什么雪景了,所以把目的地改成了东京周围和广岛周围。
在规划行程的时候,我突然想到:既然我的位置 24 小时都会被记录,而且现在 vibe coding 这么方便,那我为什么不编写一个 agent 来作为我的旅行助手呢?
二、实际用下来
「在车上跳过」其实没怎么触发。 因为实际使用的时候,一般在电车中间基本上没有 GPS 信号,除非在窗边且手机拿在手里,否则也会没信号,所以这一点其实用的不多,但是也有必要存在….不然会有很多垃圾消息。
早晚两条提醒是最实用的。 早上读当天日程加天气、晚上把第二天的安排过一遍,这个我认为是比较有用的,我的行程安排是我首先规划好去哪里,然后让AI把根据我想去的地方来给我安排行程,AI安排的很详细,会精准到分钟,后来我修改为精确到半小时,这样就比较合理了,在实际用的时候,参考agent发的日程也比较容易安排时间。这样,我每天出门的时候,很容易就知道今天的安排。
