旅迹:把一句旅行愿望,变成真正可编辑的行程
一个由 AI 对话驱动,同时保留结构化编辑、版本管理与人工确认的旅行规划工作台。
旅行攻略从来不缺,但把散落在攻略、地图、天气、车票和酒店页面里的信息,整理成一份真正能执行的逐日计划,依然很费时间。
这也是我做「旅迹」的起点:我希望用户只需要告诉 AI“想去哪、什么时候去、几个人、预算多少、喜欢什么”,就能得到一份清晰的行程;同时,它又不能只是一次性生成一大段文字,而应该允许继续修改、锁定重要安排、查看地图与天气,并在出发前随时调整。
- 在线体验:https://drfccv.github.io/lvji-travel/
- 项目源码:GitHub - lvji-travel
- 开源协议:Apache License 2.0
它不只是“让 AI 写一篇攻略”
旅迹的核心是一份可以持续操作的结构化行程。目的地、日期、人数、预算和旅行偏好是规划输入;每天的景点、交通、住宿、用餐、时间与费用则会落到具体安排中。
用户既可以手动编辑,也可以直接对 AI 说:
第二天别排得太满,把博物馆换到上午,晚上想吃当地菜,人均控制在 150 元以内。
AI 会理解这次修改,但不会立刻覆盖原计划。所有变更先进入预览,用户确认后才会正式应用。对于旅行计划这种会不断调整、又可能包含重要预订的内容,这一步很有必要。
1 | flowchart LR |
目前项目主要包含这些能力:
- 根据目的地、日期、人数、预算和偏好生成行程;
- 按天管理景点、交通、住宿、餐饮、时间和费用;
- 通过自然语言继续确认、修订、取消或重试规划;
- 展示地图与天气信息,辅助判断路线和出行条件;
- 将带有明确日期和时间的安排导出到日历;
- 接入 12306、搜索、酒店、机票等 MCP Server;
- 保存版本快照,保护已经锁定的重要安排;
- 隔离不同用户的数据与服务凭证;
- 按预算自动匹配经济型/舒适型/高品质档位;
- 出发地、货币、节奏、交通偏好等个性化设置;
- 匿名用户无需登录即可体验完整流程;
- 支持 DeepSeek、SiliconFlow、Volcengine、OpenRouter 等多家 AI 提供商;
- 思维链推理(Thinking)模式,兼容不同提供商的 API 差异;
- 深色模式切换。
从“能生成”到“敢修改”
这次开发中,我花了不少精力处理 AI 产品里不太显眼、却很影响可靠性的部分。
1. 所有修改都经过 preview → apply
AI 和手工操作最终走同一套变更流程,并且预览和实际应用分别有独立的 API。/api/trips/[id]/operations/preview 可以先计算操作结果并展示差异,确认后才调用 /api/trips/[id]/operations/apply 正式写入。这样既能降低误操作,也让 AI 的行为更容易理解。
2. 用 revision 处理并发冲突
每次提交都要携带当前行程的 revision。如果用户在另一个页面或请求中已经更新过行程,旧版本的操作会收到 HTTP 409,而不是悄悄覆盖新内容。
3. 用 idempotencyKey 避免重复提交
网络重试、连续点击或任务恢复,都可能让同一操作被发送多次。幂等键在数据库中建立唯一约束,确保一次修改只生效一次。
4. 锁定真正不能动的安排
已经买好的车票、订好的酒店不应该被 AI 随意挪动。旅迹允许锁定安排,锁定后不能修改、移动或删除;较重要的变更还会生成版本快照,方便恢复。
调度器:先判断用户想做什么,再决定调用哪条链路
对话式产品有一个很容易被忽略的问题:用户发来的每句话,并不都意味着“重新生成行程”。
例如,“酒店多少钱?”只是一个问题;“把第二天的景点换掉”是在修订方案;“不用了,就这样吧”在存在待确认方案时通常表示接受现有方案;而“不要这个方案”才是在拒绝方案。如果把这些表达全部交给同一个生成接口,很容易出现答非所问或误写入行程。
旅迹为此增加了一层对话调度器。它把判断拆成两个互相独立的维度:
pendingPlanDecision:用户对待确认方案的态度,可能是accept、revise、reject或undecided;requestKind:用户除此以外想做的事,可能是普通问答answer、规划plan或没有额外请求none。
随后,程序再把这两个判断映射成三种动作:
| 判断结果 | 调度动作 | 后续行为 |
|---|---|---|
| 接受待确认方案 | apply |
把方案送入结构化生成与应用流程 |
| 创建或修订计划 | plan |
生成新的候选方案,等待用户确认 |
| 普通问题或尚未决定 | reply |
只回复消息,不改动行程 |
1 | flowchart TD |
这里采用的是“确定性规则优先,语义模型兜底”的混合方式。像“确认”“就按这个”“不要这个方案”“把行程调整一下”这类高置信度控制语句,会直接在本地完成判断,响应更快,也更可预测;问题、否定和转折较复杂时,才交给模型结合待确认方案和最近几轮对话做语义分析。
调度器还会尝试从最近完成的 AI 任务中找回尚未应用的候选方案。这样即使前端没有继续携带 pendingPlan,用户随后说“就这样吧”,系统仍有机会恢复正确的上下文。同时,代码会检查较长回复是否具有“完整行程”“逐日行程”、多个 Day 标题和确认提示等特征,避免一份实际的规划结果被误当成普通聊天消息。
AI 任务本身也是一个可恢复的状态机
确定动作之后,耗时的 AI 与 MCP 工作不会挤在一次黑盒请求中完成,而是由任务调度器按两套模式分阶段推进。
当用户在对话中要求调整方案时,任务会以 conversation 模式运行,重点是与模型交互并输出待确认的候选方案:
1 | flowchart LR |
当用户确认采用某个方案后,任务则以 format 模式执行——先生成符合当前日期、锁定安排和既有条目的精确结构化变更,再通过工具补全地点描述与图片,最后输出可执行的 JSON:
1 | flowchart LR |
format 模式中,模型会使用一个独有的 finish_research 哨兵工具,在收集足够地点和图片信息后主动终止工具调用、进入最终结构化阶段。最终输出必须通过内容校验:JSON 被截断、为空或不符合可执行格式时,任务会进入 repairing_response 做一次严格纠错。校验通过后还会异步检查方案中图片 URL 的可达性(HTTP GET + content-type 验证),确保前端展示的都是有效图片。
模型在多次工具调用中可能收集大量返回数据——搜索、酒店列表、路线详情等。调度器会在提交给最终结构化模型之前,对历史工具证据做一次智能压缩:去重相同调用、裁剪过深的对象嵌套、省略非关键字段(如 polyline、raw_data),确保上下文窗口不会被冗余信息挤占。
每个阶段都使用数据库状态进行抢占:只有 queued 任务能原子地变为 running,因此重复请求不会同时执行同一阶段。阶段失败会自动重试两次,仍然失败才把任务标为 failed。前端因此可以展示真实进度和工具轨迹,任务也能在短暂网络故障后从当前阶段继续,而不是每次都从头生成。
用 MCP 把旅行信息接进来
单靠语言模型无法保证实时车次、酒店价格、天气或搜索结果准确,因此旅迹把外部能力设计成可扩展的 MCP 工具。
目前已内置以下 Streamable HTTP MCP Server:
| 标识 | 服务 | 提供的能力 |
|---|---|---|
rail12306 |
12306 | 车站查询、车次搜索、中转查询、列车路线 |
searxng |
SearXNG 搜索 | Web 搜索、搜索建议、网页正文读取 |
amap |
高德地图 | 地理编码、地点详情、公共交通路线规划、天气 |
tavily |
Tavily 搜索 | 智能搜索与网页内容提取 |
dida |
RollingGo 酒店 | 酒店搜索、详情、标签 |
didaFlight |
RollingGo 机票 | 机场查询、航班搜索 |
应用负责工具发现、调用和轨迹记录,模型则根据规划任务选择合适的工具。为了避免在大量工具中选错,系统还会根据当前任务关键词做语义工具过滤:酒店相关的任务只暴露酒店工具,交通相关的只暴露交通工具,核心旅行工具(地图、路线、天气)则始终可用。
1 | flowchart TB |
允许用户填写外部 Server 地址也带来了安全问题。项目中的 MCP Gateway 会限制目标为公开 HTTPS 地址,阻止回环、私网、链路本地和云元数据地址,同时控制重定向次数、请求超时(12 秒)与响应体积。AI/MCP 凭证使用 AES-256-GCM 加密后存储,仅在服务端解密和发送,列表接口只展示掩码,尽量缩小 SSRF 与密钥泄露风险。
每次 MCP 调用都会记录到审计日志(内存环形缓冲区,保留最近 100 条),包含调用方、工具名、耗时和状态。系统还按 IP 实施了速率限制(每分钟最多 30 次),防止单个客户端的意外或恶意高频调用。对 RollingGo 酒店和机票等外部服务,网关还会在校验参数合法性后,在响应中附加数据使用提示(如"displayRate 仅为参考展示价"),帮助模型准确理解返回结果。
技术选型
Web 主分支目前采用以下技术栈:
| 部分 | 技术 |
|---|---|
| 前端框架 | Next.js 16(App Router)、React 19、TypeScript 5.9 |
| 桌面端 | Electron 43、better-sqlite3(drfccv/electron-local 分支) |
| 样式与界面 | Tailwind CSS 4、Lucide React、React Markdown |
| 服务端与数据 | Node.js 22、PostgreSQL 18、Drizzle ORM |
| AI 与外部服务 | OpenAI-compatible API、MCP(Streamable HTTP)、Headroom 上下文压缩 |
| 校验与工程化 | Zod 4、ESLint 9、Node.js Test Runner |
| 部署 | Docker、Docker Compose |
代码按职责拆分:app/ 负责页面、组件与 API Routes,lib/ai/ 处理 AI 规划和任务分发,lib/mcp/ 实现 MCP 注册、网关、安全和审计,lib/trips/ 则集中管理行程操作规则。数据库 Schema 与迁移分别位于 db/ 和 drizzle-pg/。
上下文压缩(Headroom)
AI 对话中,反复携带完整的工具调用历史会迅速撑满上下文窗口。旅迹集成了可选的 Headroom 代理,在请求到达 AI 提供商之前自动压缩对话上下文,通常可节省 10–35% 的 Token 消耗。部署时只需设置 HEADROOM_PROXY 环境变量并在 Docker Compose 中启用 headroom Profile 即可,功能透明、无需修改代码。
预算与偏好:不仅是数字,更是档位与风格
输入预算后,系统会按人均每天花费自动划分档位:
- 经济型(< 500 元/人/天):推荐公共交通、平价餐饮、免费或低票价景点;
- 舒适型(500–1500 元/人/天):平衡性价比与体验,中高档住宿加特色正餐;
- 高品质型(≥ 1500 元/人/天):升级到高档酒店、精品餐饮,加入私享导览、演出等深度体验。
如果未设预算,系统默认采用舒适但不浪费的中档方案。预算指导在每次生成行程时作为系统提示的一部分注入,确保模型从一开始就理解费用约束。
此外,用户还可以设置出发地、货币种类、旅行节奏(紧凑/适中/宽松)和交通偏好(公共交通/打车/自驾),这些偏好持久化在浏览器 localStorage 中,下次创建行程时自动填充。
多提供商与思维链推理
旅迹的 AI 层不仅兼容 OpenAI,还支持 DeepSeek、SiliconFlow(硅基流动)、Volcengine(火山引擎方舟)和 OpenRouter。系统会检测提供商类型,自动调整 API 参数差异:
max_tokens与max_completion_tokens的选择;parallel_tool_calls的启用;tool_choice: required的兼容性处理;- 思考(Thinking)模式的请求格式适配。
这意味着用户可以根据自己的模型偏好和成本选择提供商,DeepSeek 的深度推理能力在复杂的多步行程规划中尤其有帮助。
匿名用户:零门槛开始使用
为了让用户在没有配置登录的情况下也能体验,项目内置了一个轻量匿名用户系统。中间件通过 HMAC 签名的 Cookie 管理匿名会话,自动生成稳定的用户标识,无需注册即可创建行程。后端对匿名用户也有速率限制,避免滥用。等用户决定注册后,数据可以平滑迁移到正式账户。
顺带聊聊 Electron 分支
除了 Web 版,仓库还保留了一个独立的 drfccv/electron-local 分支,用来维护 Windows Electron 单机版。
它并不是把网页直接塞进一个窗口这么简单。桌面版复用了现有 React 界面和 Trips、AI Jobs、MCP、天气、版本等业务能力,同时增加了 Electron Main、preload、Renderer transport、本地数据库、备份恢复和安装包构建链路。
| Web 主分支 | Electron 本地分支 |
|---|---|
| Next.js 服务端运行 | Electron Main 进程承载本地运行时 |
| PostgreSQL 18 | better-sqlite3 + Drizzle |
| 数据保存在服务器 | 数据保存在 %APPDATA%\Lvji\data\trip-planner.db |
| 服务端加密保存凭证 | Electron safeStorage 绑定当前 Windows 账户加密 |
| Docker / Node 服务部署 | electron-builder 生成 Windows NSIS 安装程序 |
| 适合在线访问和多用户 | 适合个人离线管理本地行程数据 |
Electron 侧采用了尽量收敛的权限边界:
1 | flowchart LR |
窗口启用了 contextIsolation: true、nodeIntegration: false 和 sandbox: true,并设置 CSP;Renderer 只能通过最小化的 preload 白名单调用可信 Main 进程。生产版本默认关闭 DevTools,外部访问限制为经过允许的 HTTPS 域名,MCP 仍沿用原有的 SSRF 防护。
本地数据库会在启动时自动执行迁移,并启用外键、WAL 和 busy timeout。普通 JSON 备份不会包含 AI/MCP 凭证;导入前会先校验格式,并在数据目录中保留当前数据库副本。安装后的应用已经包含 Electron 与 SQLite 运行时,普通用户无需再安装 Node.js、pnpm 或 Wrangler。
当然,桌面分支目前仍有一些明确的限制:没有配置自动更新服务;普通备份不能迁移凭证,换电脑后需要重新填写;AI、地图、天气与 MCP 仍依赖网络和用户自己的服务配置;正式分发前也还需要准备代码签名证书。
如果你想体验或参与桌面版开发,可以查看 GitHub 仓库的 drfccv/electron-local 分支。
详细的安装与运行说明请参阅 GitHub README。
写在最后
旅迹想解决的并不是“让 AI 多写一篇攻略”,而是如何把模型生成、实时工具和用户确认组合成一套可信、可编辑、可恢复的旅行规划流程。
Web 版让它可以随时访问,Electron 分支则尝试把数据和运行时留在用户自己的电脑里。两条路线面向不同场景,但共享同一个目标:让 AI 负责繁琐的信息整理,同时让最终决定始终掌握在旅行者手中。
如果这个项目对你有帮助,欢迎在 GitHub 提交 Issue、参与改进,或者分享你希望加入的旅行规划能力。
