文档定位:本文是配套《主 PRD v3.0》的技术方案文档,把 PRD 的产品约束(SLA、SSOT、增量重算、降级、数据模型、验收标准)翻译为可落地的技术栈、架构设计、风险登记与开发排期。
本期目标:单人开源项目,第一阶段只求"本地跑通"核心闭环(路径 B:目的地→POI 推荐→顺路线路→双栏联动→保存);云部署 / Nginx / HTTPS / Redis / 验证码等均为上线阶段事项,本期不纳入。
技术栈一句话:前端 React + Vite + TypeScript(Zustand 做 SSOT),后端 Python + FastAPI(SQLModel + SQLite),LLM 走 OpenAI 兼容接口 + 自研轻量 Workflow,实时用 SSE 流式,缓存用进程内缓存。
| 字段 | 内容 |
|---|---|
| 文档类型 | 技术方案 + 开发计划(配套主 PRD v3.0) |
| 文档版本 | v1.0(本地跑通阶段定稿) |
| 对应 PRD | 主 PRD|智能旅游规划与可视化行程 Agent(v3.0) |
| 项目性质 | 个人开源项目,单人开发,按企业级流程推进 |
| 本期范围 | P0 核心闭环本地跑通(路径 B 主链路),不含云部署与外部资质能力 |
| 读者对象 | 开发者本人、未来社区贡献者 |
本期做(P0 本地跑通)
-
混合输入 → 路径 B 目的地/体验优先
-
高德 POI 推荐 + 顺路线路
-
卡片流勾选/剔除、日程表、地图双栏联动
-
SSOT 状态机 + 拖拽 + 撤销
-
增量交通重算 + 进程内缓存 + 防抖
-
邮箱+密码登录、行程保存(SQLite)
-
LLM 流式草案(SSE)+ 契约校验
本期不做(上线 / 二期)
-
OTA 比价与返佣跳转(需资质,P1)
-
小红书/抖音定向抓取(合规,延后)
-
交通耗时预估(移至 P1)
-
图形验证码 / 防刷(上线再规范)
-
Nginx / HTTPS / 域名 / CDN(上线)
-
Redis / 多实例 / 负载均衡(扩展期)
-
云部署(Vercel/VPS/Neon)
| 层 | 选型 | 本期理由 | 上线演进 |
|---|---|---|---|
| 前端框架 | React + Vite + TypeScript | Vite 启动快、配置少;本地跑通不需要 Next 的服务端能力 | 保持,构建产物上 CDN |
| 状态管理 | Zustand + Immer(+ zundo 撤销) | 实现 SSOT 单一数据源,代码量最少,单人心智负担低 | 保持 |
| 拖拽 | dnd-kit | 主流且持续维护,无障碍好,契合拖拽手柄/落点高亮/吸附 | 保持 |
| 地图 | 高德 JS API 2.0 | 无资质门槛,POI/路径规划齐全;Loca/MarkerCluster 移至 P1 | 叠加 Loca 可视化 + 点聚合 |
| 后端框架 | Python + FastAPI |
异步原生、自带 API 文档、原生支持 SSE 流式 | 保持,前置 Nginx |
| LLM 编排 | 自研轻量 Workflow + OpenAI 官方 Python SDK | 对齐 PRD §7.3,链路确定,零框架学习成本 | 复杂化后升级 LangGraph |
| ORM | SQLModel(基于 SQLAlchemy) |
FastAPI 作者出品,天生一对;屏蔽数据库差异 | 保持 |
| 数据库 | SQLite(本地文件) | 零安装、一个文件;ORM 屏蔽差异,切换零成本 | 切 PostgreSQL(地理/JSON 强) |
| 缓存 | 进程内缓存(cachetools TTLCache/LRU) | 单实例本地跑足够,无需额外部署 | 多实例时上 Redis |
| 实时通信 | SSE(Server-Sent Events) | LLM 单向流式推送,比 WebSocket 简单、自带重连 | 保持 |
| 运行方式 | 本地 npm run dev + uvicorn | 无需 Docker/云,最快起步 | Docker Compose → VPS/云 |
决策 1:语言路线 = 前端 TS + 后端 Python(双语言)
取舍:放弃"全 TS 单语言"的统一性,换取 Python 在 LLM/Agent 生态上的成熟度。代价是两套工具链(npm/uvicorn)、两套类型系统,需开发者在两种语言间切换。
决策 2:本地数据库 = SQLite(而非直接上 PostgreSQL)
本地跑通优先零运维。由 SQLModel 屏蔽差异,上线切 PG 仅改连接配置 + 跑一次迁移。注意:开发期避免使用 SQLite 不支持的 PG 特有类型,保持可迁移性。
决策 3:缓存 = 进程内(而非 Redis)
本期单实例运行,进程内 TTLCache 完全够用。Redis 仅在"开多实例、缓存需跨实例共享"时才引入,本期不背这个运维成本。
决策 4:实时 = SSE(而非 WebSocket)
核心实时需求是"LLM 草案流式下发",单向即可。SSE 基于普通 HTTP、浏览器原生 EventSource、自动重连,复杂度远低于 WebSocket,且不受 Serverless 双向连接限制影响。
决策 5:前端构建 = Vite(而非 Next.js)
Next 的服务端渲染/后端 API 能力与"后端已定 Python"重复且变重。Vite 纯前端、热更新极快,前后端清晰解耦,更适合本架构。
系统沿用 PRD §8 的五层结构。前端维护唯一权威的 Itinerary Store(SSOT),卡片流/日程表/地图均为其只读投影;BFF 负责并行聚合外部数据、缓存与局部降级;Agent 编排层走确定性 Workflow;模型接入层统一 OpenAI 兼容协议;数据源为高德 API 与通用搜索。
主链路:浏览器前端发起规划请求 → BFF 先查进程内缓存(Cache-Aside)→ 未命中则并行调用高德 POI / 搜索 / LLM → LLM 经 Workflow 生成结构化草案并经 SSE 逐块流式回传 → 前端边到边填充骨架屏 → 用户编排(勾选/拖拽)只变更 SSOT → SSOT 单向驱动三视图重渲染 → 保存写入 SQLite。
| 组件 | 本地形态 | 地址/端口 |
|---|---|---|
| 前端 dev server | Vite(npm run dev) | http://localhost:5173 |
| 后端 API | FastAPI(uvicorn) | http://localhost:8000 |
| 数据库 | SQLite 文件 | ./data/app.db |
| 缓存 | 后端进程内存 | 无独立进程 |
| 跨域 | FastAPI CORS 放行 5173 | 开发期配置 |
本地阶段不涉及 DNS、Nginx、HTTPS、公网 IP、负载均衡——前端用相对/环境变量配置后端地址为 localhost:8000,直连即可。上线时这些再由部署层补齐(见 §9.5)。
前端唯一权威状态是 Itinerary Store(用 Zustand 实现)。任何用户交互(勾选/剔除/拖拽/删除/撤销)都只修改 Store,再由 Store 单向驱动卡片流、日程表、地图三个视图重渲染。三视图自身不持有业务状态,杜绝多视图状态不一致与竞态。
Store 结构对齐 PRD §10 数据模型:Itinerary → Day[] → Stop[] → (POI, Transit)。Store 持有当前行程树 + 一个用于撤销的快照栈。
| 视图 | 从 Store 投影什么 | 关键交互 |
|---|---|---|
| 卡片流 Feed | 候选 POI 列表(吃/住/玩) | 勾选→加入 Store;剔除→触发该类目重生成 |
| 日程表 Schedule | 按 Day/Stop 排序的时间轴 | dnd-kit 拖拽改 order_index |
| 地图 Map | Stop 经纬度打点 + Transit 连线 | 点选/挪动联动 Store |
-
拖拽库:dnd-kit。每个 Stop 卡片左侧拖拽手柄(命中区 ≥24px);拖拽中目标插入位高亮分隔线;临近合法落点自动吸附。
-
撤销:Zustand 配 zundo 中间件,每次结构变更入快照栈;支持 Ctrl+Z 与 Toast 撤销按钮,回退到上一 Store 快照。
-
拖拽后果:仅改变 order_index,随即触发"增量交通重算"(详见 §5.1),而非全量。
-
本期:高德 JS API 2.0 基础能力——加载地图、Marker 打点、Polyline 画线、路径规划。
-
P1 增强:Loca 做轨迹/路况可视化;MarkerCluster 点聚合(单日 POI >15 触发,对应 PRD §5.3.3)。
-
边界降级:POI 经纬度缺失 → 仅列表展示、地图不打点并提示;海上/无路网 → 仅打点标"无可达路线";跨城 → 分段不强连。
| 状态 | 实现 |
|---|---|
| 空态 | 插画 + 引导文案 + "描述你的行程"主入口 |
| 加载态 | 骨架屏占位 + SSE 流式逐块填充;禁止整屏 loading 转圈 |
| 错误/部分失败 | 局部错误局部提示(地图挂了只提示地图区),不拖垮整页 |
拖拽改变 Stop 顺序时,只有"被移动节点"的前驱段与后继段失效,最多重算 2~3 段 Transit,而非全量 O(N²)。每段 Transit 以 from_stop_id_to_stop_id_mode 为缓存键独立存储与复用。
| 数据类型 | 缓存键 | TTL | 过期口径 |
|---|---|---|---|
| POI 详情 | poi_id | 24h | 直接使用 |
| 两点路径/耗时 | 起点_终点_交通方式 | 1h | 命中即复用,免重复请求 |
| LLM 推荐结果 | 意图哈希 | 会话级 | 同输入复用,避免重复推理 |
本期用 cachetools.TTLCache 实现,存在 FastAPI 进程内存中。单实例够用;上线开多实例后,将这层替换为 Redis(接口抽象一致,仅换实现)。
竞态根因:1 秒内连续拖拽多次会发出多个异步重算请求,网络不保证按序返回,旧请求的慢响应可能覆盖新结果。
解法:① 防抖——停手约 300ms 才发请求,合并高频触发;② 请求带单调递增 seq;③ 响应回来时比对 Store 当前版本,过期响应直接丢弃。最终态以 SSOT 为唯一裁决。
| 故障 | 降级行为 |
|---|---|
| 地图接口超时/限流(429) | 展示上次缓存轨迹 + "显示历史数据"提示,不白屏 |
| POI 搜索返回空 | 空状态引导,提示调整关键词/扩大范围 |
| LLM 超时/失败 | 降级为高德 POI 热门排序的非个性化推荐 |
| 返回脏数据(坐标越界/缺字段) | BFF 层校验过滤,单条丢弃不阻塞整体 |
BFF 用 asyncio.gather 并行发起高德/搜索/LLM 调用,单条失败不影响其余结果,实现"局部失败局部降级"。
-
统一采用 OpenAI
chat/completions格式(messages、tools、stream 字段),通过 OpenAI 官方 Python SDK 接入。 -
通过环境变量
base_url/api_key/model切换 Claude 4.8 / DeepSeek V4 Pro,切换零代码改动,避免供应商锁定。 -
必需能力:流式输出(stream)、结构化输出 / function calling;不满足的模型仅作降级备选。
链路高度确定,采用确定性 Workflow 而非全自主 Agent loop。每个节点可独立测试、可降级,统一通过模型接入层调模型。
不校验 LLM 具体文案,改为契约校验:① 输出 JSON 符合预定义 schema;② 每个推荐 POI 真实存在于高德(反查 amap_id);③ 推荐语字数合规(≤50 Unicode 码点);④ 坐标在合法经纬度范围内。校验不通过则走降级。
| 实体 | 关键约束 |
|---|---|
| Itinerary | status ∈ draft/saved;day_count ≥ 1 |
| Stop | order_index 决定排序与连线顺序 |
| POI | lng/lat 缺失则降级列表展示 |
| Transit | 增量重算仅更新受影响段 |
| Source | 仅存公开 url + 摘要(限定来源域) |
用 SQLModel 定义模型即同时获得 Pydantic 校验 + 表结构;本地 SQLite,上线改连接串切 PostgreSQL,模型代码不变。
| 项 | 本期实现 |
|---|---|
| 注册 | 邮箱+密码+二次确认,前后端校验邮箱格式与两次一致后写库;无邮箱验证码 |
| 登录 | 邮箱+密码校验,签发 JWT |
| 密码存储 | 禁止明文,argon2id(或 bcrypt)加盐哈希入库 |
| 会话 | JWT 设合理过期;HTTP 无状态,每次请求带 Token 证明身份;登出前端清除 |
| 密码强度 | 最小长度 ≥8,前端即时校验 |
| 行程归属 | 登录后 Itinerary 与 user_id 绑定持久化 |
本期暂不做:图形验证码 / 防暴力破解限频 / 找回密码 / 第三方 OAuth。这些作为上线前的安全加固项(垃圾注册风险评估见 PRD §15)。本地跑通只验证登录态主链路。传输加密(HTTPS)也属上线事项,本地用 http://localhost。
| 指标 | 目标 | 技术手段 |
|---|---|---|
| 纯数据聚合 | ≤ 2s | asyncio 并行聚合 + 进程内缓存命中 |
| LLM 首字 | ≤ 3s | 流式首 token 经 SSE 立即推送 |
| 首份可见内容 TTFP | ≤ 5s | 骨架屏先行 + 流式逐块填充;POI 先用高德搜索垫底,LLM 异步精修 |
| 完整草案 TTFI | ≤ 30s | 分块流式渲染,不等全部算完 |
| 交互帧率 | 60fps | SSOT 单向更新 + 局部重渲染,避免全量 diff |
说明:本地跑通阶段以"功能正确 + 主观流畅"为先,上述 P95 量化口径(并发 50 用户等)属上线前压测目标。压测同时回填高德接口实际配额(PRD §7.1 待实测项)。
-
所有外部依赖具备降级(§5.4);单点失败不拖垮整页。
-
内容溯源仅引用限定来源域的公开链接 + 摘要,遵守 robots 与版权;用户位置等数据最小化采集。
| # | 风险 | 影响 | 缓解 |
|---|---|---|---|
| R1 | 高德配额未实测 | 拖拽实时重算瞬间打爆 QPS,触发 429 | 增量+缓存+防抖三件套做扎实;上线前压测回填配额 |
| R2 | LLM 30s 出结构化草案 | 生成慢、可能不合 schema | function calling + JSON schema 校验 + 反查 amap_id;失败降级热门排序 |
| R3 | 单人维护全栈复杂度 | 前端三视图+后端+LLM+地图体量大 | 严格按 P0 砍范围;比价/抓取/耗时预估均已延后 |
| R4 | TTFP 5s 在 LLM 链路下偏紧 | 首字 3s+渲染余量小 | 骨架屏先行 + 高德搜索垫底,LLM 异步精修 |
| R5 | SSOT 与异步重算竞态 | 快速拖拽致地图/时间错乱 | 请求 seq 版本号 + 过期响应丢弃,Store 唯一裁决 |
| R6 | 通用搜索质量与延迟 | 结果噪声大、拖慢主链路 | 限定来源域 + 限结果数;溯源作增强非阻塞 |
| R7 | 双语言工具链心智负担 | 前端 TS/后端 Python 切换成本 | 清晰分层 + 统一接口契约;先跑通后优化 |
| R8 | SQLite→PG 迁移隐患 | 用了 PG 特有类型导致迁移失败 | 开发期只用可迁移类型;早期就跑一次 PG 冒烟 |
| 功能 | 验收标准 |
|---|---|
| 路径 B 生成草案 | 输入「成都耍三天」提交 → ≤5s 出骨架并流式填充,≤30s 生成含 ≥3 POI 完整草案 |
| 卡片勾选/剔除 | 勾选→POI 入日程表且地图打点;剔除→该类目重生成 |
| 拖拽增量重算 | 拖动改顺序 → 仅相邻 transit 重算、地图同步,无全量请求 |
| 空行程状态 | 无 POI → 展示空状态引导与添加入口 |
| 接口超时降级 | 地图超时 → 展示缓存数据+「显示历史数据」,不白屏 |
| 保存行程 | 草案含 ≥1 天 ≥3 POI → 点保存置 saved,可二次编辑 |
| 登录鉴权 | 正确邮箱密码 → 签发凭证,行程与账号绑定;错误密码提示失败 |
-
外部依赖降级(高德/LLM × 超时/429/空/脏数据):逐一注入故障,验证降级行为正确。
-
地图边界:跨城分段、海上仅打点、经纬度缺失降级列表、单日 POI =15/=16 验证 Clustering 临界。
-
竞态:1 秒内连续拖拽 ≥3 次,验证后发慢响应不覆盖新结果,最终态与 Store 一致。
-
契约测试:LLM 输出走 schema 校验 + amap_id 反查 + 字数 + 坐标范围四关。
| 层 | 工具 |
|---|---|
| 后端单测/契约 | pytest + httpx(异步客户端) |
| 前端单测 | Vitest(与 Vite 同生态) |
| 前端组件/交互 | React Testing Library |
| 端到端(可选) | Playwright(拖拽/竞态场景) |
目标:用最短路径把"输入意图 → LLM 出草案 → 三视图渲染 → 拖拽增量重算 → 保存行程"的核心闭环在本地跑通。单人开发,按"先骨架后血肉、先打通后优化"推进,每个里程碑结束都有一个可演示的可运行产物。
| 里程碑 | 周期 | 核心交付 | 验收锚点 | 依赖 |
|---|---|---|---|---|
| M0 脚手架 | 第 1 周 | 前后端工程初始化、环境变量打通、SQLite 建表、健康检查接口 | 前端访问后端 /health 返回 200;建表成功 | 无 |
| M1 数据骨架 | 第 2 周 | SQLModel 模型(User/Itinerary/Day/Stop/POI/Transit/Source)+ 基础 CRUD + Zustand Store 结构 | 能手工塞一条行程并读出渲染到日程表 | M0 |
| M2 LLM 草案链路 | 第 3~4 周 | 模型接入层 + 自研 Workflow 五节点 + SSE 流式 + 契约校验 + 高德垫底降级 | 「成都耍三天」→ ≤5s 出骨架,≤30s 出 ≥3 POI 草案 | M1 |
| M3 三视图与交互 | 第 5~6 周 | 卡片流勾选/剔除、日程表 dnd-kit 拖拽、高德地图打点连线、三态骨架屏 | 勾选入表+打点;拖拽改序;空态/加载态/错误态正确 | M2 |
| M4 增量重算与竞态 | 第 7 周 | 受影响 Transit 段重算 + 进程内缓存 + 防抖 + seq 版本号丢弃过期响应 + Undo/Redo | 拖拽仅重算相邻段;连续拖拽不错乱;Ctrl+Z 可回退 | M3 |
| M5 鉴权与保存 | 第 8 周 | 注册/登录(argon2id+JWT)、行程归属绑定、保存为 saved 可二次编辑 | 登录态主链路通;保存后刷新仍在 | M4 |
| M6 测试与收尾 | 第 9 周 | 契约/降级/竞态/边界用例补齐、P0 GWT 验收对照、README 跑通文档 | §8.2 七条 GWT 全绿;新机器照 README 能起服务 | M5 |
排期口径:以"工作日"投入估算,约 9 周。关键路径是 M2(LLM + Workflow + 契约校验),风险最高、建议预留 buffer;M3/M4 是交互体验密集区,可按需把 Loca/MarkerCluster 等 P1 项剔出本期。比价、抓取、交通耗时预估均不在本期。
-
串行硬依赖:M0→M1→M2 必须按序,数据模型不定,Store 与 LLM 输出 schema 都无从对齐。
-
可穿插并行:M3 的地图集成(高德 JS API 接入、Key 申请、基础打点)与 M2 的后端链路可并行预研,降低后期阻塞。
-
尽早冒烟:M2 完成时就跑一次 PG 连接冒烟(仅建表+读写),提前暴露 SQLite→PostgreSQL 迁移隐患(对应 R8)。
-
测试左移:契约校验(§6.3)随 M2 一起写,不要堆到 M6;竞态用例随 M4 落地。
| 依赖 | 版本建议 | 用途 |
|---|---|---|
| Node.js | ≥ 20 LTS | 前端构建运行时(Vite 依赖) |
| Python | ≥ 3.11 | 后端 FastAPI 运行时 |
| 包管理 | npm / uv | 前后端依赖安装 |
| 高德 Key | Web 端 + JS API | POI 搜索、路径规划、地图渲染 |
| LLM 凭证 | OpenAI 兼容端点 | base_url/api_key/model 三件套 |
环境变量(后端 .env):OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL(切换 Claude 4.8 / DeepSeek V4 Pro 零代码改动)、AMAP_KEY(可选,高德 Web 服务;不填则走本地兜底)、JWT_SECRET、DATABASE_URL(本地默认 sqlite:///./data/app.db)。前端 .env:VITE_API_BASE(指向后端,如 http://localhost:8000)、`VITE_AMAP_JS_KEY`(JS API Key)。密钥不入库、不进 Git。
后端(终端 1):
cd backend
cp .env.example .env # 按需填写 LLM / 高德凭证,可先留空
uv sync # 创建 .venv 并安装依赖
uv run uvicorn app.main:app --reload --port 8000前端(终端 2):
cd frontend
cp .env.example .env # VITE_API_BASE 默认 http://localhost:8000
npm install
npm run dev # Vite 启动,默认 http://localhost:5173浏览器访问 http://localhost:5173,前端通过 VITE_API_BASE 调后端 8000 端口;后端再去聚合高德与 LLM。本地全程 http,无需 Nginx / HTTPS / 域名。
受限沙箱或 CI 环境如果不能写用户级缓存,可使用项目内缓存:后端执行 UV_CACHE_DIR=.uv-cache uv sync,前端执行 npm install --cache .npm-cache。如果环境不能绑定本地端口,可用 FastAPI TestClient 对 /health 做进程内冒烟,具体命令见 docs/CONTRIBUTING.md。
-
后端
/health返回 200;前端控制台无跨域报错(CORS 已在 FastAPI 放行 localhost)。 -
输入「成都耍三天」→ 骨架屏先出,SSE 逐块填充,最终 ≥3 POI。
-
勾选 POI → 日程表入项 + 地图打点;剔除 → 该类目重生成。
-
拖拽改序 → 仅相邻 transit 变化、地图同步、无整页 loading。
-
1 秒内连点拖拽 ≥3 次 → 最终态与 Store 一致,无错乱。
-
关闭网络模拟高德超时 → 展示「显示历史数据」,不白屏。
-
注册→登录→保存→刷新,行程仍在且归属正确。
本期完成定义(DoD):§8.2 七条 P0 GWT 全部通过 + §8.3 专项用例(降级/边界/竞态/契约)跑通 + 一份能让新环境照着起服务的 README。达成即视为"本地跑通"目标完成,后续再进入 P1 与上线加固。
(注:内容由 AI 生成,请谨慎参考)