Skip to content

Latest commit

 

History

History
382 lines (245 loc) · 21.3 KB

File metadata and controls

382 lines (245 loc) · 21.3 KB

技术方案与开发计划|智能旅游规划与可视化行程 Agent(v1·本地跑通)

文档定位:本文是配套《主 PRD v3.0》的技术方案文档,把 PRD 的产品约束(SLA、SSOT、增量重算、降级、数据模型、验收标准)翻译为可落地的技术栈、架构设计、风险登记与开发排期。

本期目标:单人开源项目,第一阶段只求"本地跑通"核心闭环(路径 B:目的地→POI 推荐→顺路线路→双栏联动→保存);云部署 / Nginx / HTTPS / Redis / 验证码等均为上线阶段事项,本期不纳入。

技术栈一句话:前端 React + Vite + TypeScript(Zustand 做 SSOT),后端 Python + FastAPI(SQLModel + SQLite),LLM 走 OpenAI 兼容接口 + 自研轻量 Workflow,实时用 SSE 流式,缓存用进程内缓存。

1. 文档信息

1.1 基本信息

字段 内容
文档类型 技术方案 + 开发计划(配套主 PRD v3.0)
文档版本 v1.0(本地跑通阶段定稿)
对应 PRD 主 PRD|智能旅游规划与可视化行程 Agent(v3.0)
项目性质 个人开源项目,单人开发,按企业级流程推进
本期范围 P0 核心闭环本地跑通(路径 B 主链路),不含云部署与外部资质能力
读者对象 开发者本人、未来社区贡献者

1.2 范围边界(本期做 / 不做)

本期做(P0 本地跑通)

  • 混合输入 → 路径 B 目的地/体验优先

  • 高德 POI 推荐 + 顺路线路

  • 卡片流勾选/剔除、日程表、地图双栏联动

  • SSOT 状态机 + 拖拽 + 撤销

  • 增量交通重算 + 进程内缓存 + 防抖

  • 邮箱+密码登录、行程保存(SQLite)

  • LLM 流式草案(SSE)+ 契约校验

本期不做(上线 / 二期)

  • OTA 比价与返佣跳转(需资质,P1)

  • 小红书/抖音定向抓取(合规,延后)

  • 交通耗时预估(移至 P1)

  • 图形验证码 / 防刷(上线再规范)

  • Nginx / HTTPS / 域名 / CDN(上线)

  • Redis / 多实例 / 负载均衡(扩展期)

  • 云部署(Vercel/VPS/Neon)

2. 技术选型总览

2.1 技术栈总表

层 选型 本期理由 上线演进
前端框架 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/云

2.2 关键选型决策记录(ADR 摘要)

决策 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 纯前端、热更新极快,前后端清晰解耦,更适合本架构。

3. 系统架构设计

3.1 总体架构

系统沿用 PRD §8 的五层结构。前端维护唯一权威的 Itinerary Store(SSOT),卡片流/日程表/地图均为其只读投影;BFF 负责并行聚合外部数据、缓存与局部降级;Agent 编排层走确定性 Workflow;模型接入层统一 OpenAI 兼容协议;数据源为高德 API 与通用搜索。

3.2 数据流转(一次规划请求)

主链路:浏览器前端发起规划请求 → BFF 先查进程内缓存(Cache-Aside)→ 未命中则并行调用高德 POI / 搜索 / LLM → LLM 经 Workflow 生成结构化草案并经 SSE 逐块流式回传 → 前端边到边填充骨架屏 → 用户编排(勾选/拖拽)只变更 SSOT → SSOT 单向驱动三视图重渲染 → 保存写入 SQLite。

3.3 本地运行拓扑

组件 本地形态 地址/端口
前端 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)。

4. 前端方案:SSOT 与三视图投影

4.1 SSOT 单一数据源设计

前端唯一权威状态是 Itinerary Store(用 Zustand 实现)。任何用户交互(勾选/剔除/拖拽/删除/撤销)都只修改 Store,再由 Store 单向驱动卡片流、日程表、地图三个视图重渲染。三视图自身不持有业务状态,杜绝多视图状态不一致与竞态。

Store 结构对齐 PRD §10 数据模型:Itinerary → Day[] → Stop[] → (POI, Transit)。Store 持有当前行程树 + 一个用于撤销的快照栈。

4.2 三视图投影

视图 从 Store 投影什么 关键交互
卡片流 Feed 候选 POI 列表(吃/住/玩) 勾选→加入 Store;剔除→触发该类目重生成
日程表 Schedule 按 Day/Stop 排序的时间轴 dnd-kit 拖拽改 order_index
地图 Map Stop 经纬度打点 + Transit 连线 点选/挪动联动 Store

4.3 拖拽与撤销(PRD §13.4)

  • 拖拽库:dnd-kit。每个 Stop 卡片左侧拖拽手柄(命中区 ≥24px);拖拽中目标插入位高亮分隔线;临近合法落点自动吸附。

  • 撤销:Zustand 配 zundo 中间件,每次结构变更入快照栈;支持 Ctrl+Z 与 Toast 撤销按钮,回退到上一 Store 快照。

  • 拖拽后果:仅改变 order_index,随即触发"增量交通重算"(详见 §5.1),而非全量。

4.4 地图集成

  • 本期:高德 JS API 2.0 基础能力——加载地图、Marker 打点、Polyline 画线、路径规划。

  • P1 增强:Loca 做轨迹/路况可视化;MarkerCluster 点聚合(单日 POI >15 触发,对应 PRD §5.3.3)。

  • 边界降级:POI 经纬度缺失 → 仅列表展示、地图不打点并提示;海上/无路网 → 仅打点标"无可达路线";跨城 → 分段不强连。

4.5 三态与骨架屏(PRD §13.3)

状态 实现
空态 插画 + 引导文案 + "描述你的行程"主入口
加载态 骨架屏占位 + SSE 流式逐块填充;禁止整屏 loading 转圈
错误/部分失败 局部错误局部提示(地图挂了只提示地图区),不拖垮整页

5. 后端 BFF:增量重算 / 缓存 / 降级

5.1 增量交通重算(核心难点)

拖拽改变 Stop 顺序时,只有"被移动节点"的前驱段与后继段失效,最多重算 2~3 段 Transit,而非全量 O(N²)。每段 Transit 以 from_stop_id_to_stop_id_mode 为缓存键独立存储与复用。

5.2 缓存策略(进程内,对齐 PRD §8.2)

数据类型 缓存键 TTL 过期口径
POI 详情 poi_id 24h 直接使用
两点路径/耗时 起点_终点_交通方式 1h 命中即复用,免重复请求
LLM 推荐结果 意图哈希 会话级 同输入复用,避免重复推理

本期用 cachetools.TTLCache 实现,存在 FastAPI 进程内存中。单实例够用;上线开多实例后,将这层替换为 Redis(接口抽象一致,仅换实现)。

5.3 防抖与竞态控制(PRD §12.4)

竞态根因:1 秒内连续拖拽多次会发出多个异步重算请求,网络不保证按序返回,旧请求的慢响应可能覆盖新结果。

解法:① 防抖——停手约 300ms 才发请求,合并高频触发;② 请求带单调递增 seq;③ 响应回来时比对 Store 当前版本,过期响应直接丢弃。最终态以 SSOT 为唯一裁决。

5.4 BFF 并行聚合与降级(PRD §8.3)

故障 降级行为
地图接口超时/限流(429) 展示上次缓存轨迹 + "显示历史数据"提示,不白屏
POI 搜索返回空 空状态引导,提示调整关键词/扩大范围
LLM 超时/失败 降级为高德 POI 热门排序的非个性化推荐
返回脏数据(坐标越界/缺字段) BFF 层校验过滤,单条丢弃不阻塞整体

BFF 用 asyncio.gather 并行发起高德/搜索/LLM 调用,单条失败不影响其余结果,实现"局部失败局部降级"。

6. LLM 编排与数据库设计

6.1 模型接入层(OpenAI 兼容,PRD §7.2)

  • 统一采用 OpenAI chat/completions 格式(messages、tools、stream 字段),通过 OpenAI 官方 Python SDK 接入。

  • 通过环境变量 base_url / api_key / model 切换 Claude 4.8 / DeepSeek V4 Pro,切换零代码改动,避免供应商锁定。

  • 必需能力:流式输出(stream)、结构化输出 / function calling;不满足的模型仅作降级备选。

6.2 Agent 编排(自研轻量 Workflow,PRD §7.3)

链路高度确定,采用确定性 Workflow 而非全自主 Agent loop。每个节点可独立测试、可降级,统一通过模型接入层调模型。

6.3 契约测试设计(应对 LLM 非确定性,PRD §12.5)

不校验 LLM 具体文案,改为契约校验:① 输出 JSON 符合预定义 schema;② 每个推荐 POI 真实存在于高德(反查 amap_id);③ 推荐语字数合规(≤50 Unicode 码点);④ 坐标在合法经纬度范围内。校验不通过则走降级。

6.4 数据库设计(SQLModel + SQLite,对齐 PRD §10 ER)

实体 关键约束
Itinerary status ∈ draft/saved;day_count ≥ 1
Stop order_index 决定排序与连线顺序
POI lng/lat 缺失则降级列表展示
Transit 增量重算仅更新受影响段
Source 仅存公开 url + 摘要(限定来源域)

用 SQLModel 定义模型即同时获得 Pydantic 校验 + 表结构;本地 SQLite,上线改连接串切 PostgreSQL,模型代码不变。

7. 鉴权安全与性能 SLA 落地

7.1 鉴权与安全(PRD §5.4)

项 本期实现
注册 邮箱+密码+二次确认,前后端校验邮箱格式与两次一致后写库;无邮箱验证码
登录 邮箱+密码校验,签发 JWT
密码存储 禁止明文,argon2id(或 bcrypt)加盐哈希入库
会话 JWT 设合理过期;HTTP 无状态,每次请求带 Token 证明身份;登出前端清除
密码强度 最小长度 ≥8,前端即时校验
行程归属 登录后 Itinerary 与 user_id 绑定持久化

本期暂不做:图形验证码 / 防暴力破解限频 / 找回密码 / 第三方 OAuth。这些作为上线前的安全加固项(垃圾注册风险评估见 PRD §15)。本地跑通只验证登录态主链路。传输加密(HTTPS)也属上线事项,本地用 http://localhost。

7.2 性能 SLA 落地(PRD §8.1 / §9.1)

指标 目标 技术手段
纯数据聚合 ≤ 2s asyncio 并行聚合 + 进程内缓存命中
LLM 首字 ≤ 3s 流式首 token 经 SSE 立即推送
首份可见内容 TTFP ≤ 5s 骨架屏先行 + 流式逐块填充;POI 先用高德搜索垫底,LLM 异步精修
完整草案 TTFI ≤ 30s 分块流式渲染,不等全部算完
交互帧率 60fps SSOT 单向更新 + 局部重渲染,避免全量 diff

说明:本地跑通阶段以"功能正确 + 主观流畅"为先,上述 P95 量化口径(并发 50 用户等)属上线前压测目标。压测同时回填高德接口实际配额(PRD §7.1 待实测项)。

7.3 高可用与合规

  • 所有外部依赖具备降级(§5.4);单点失败不拖垮整页。

  • 内容溯源仅引用限定来源域的公开链接 + 摘要,遵守 robots 与版权;用户位置等数据最小化采集。

8. 风险登记册与测试方案

8.1 风险登记册

# 风险 影响 缓解
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 冒烟

8.2 P0 功能验收标准(GWT,PRD §12.1)

功能 验收标准
路径 B 生成草案 输入「成都耍三天」提交 → ≤5s 出骨架并流式填充,≤30s 生成含 ≥3 POI 完整草案
卡片勾选/剔除 勾选→POI 入日程表且地图打点;剔除→该类目重生成
拖拽增量重算 拖动改顺序 → 仅相邻 transit 重算、地图同步,无全量请求
空行程状态 无 POI → 展示空状态引导与添加入口
接口超时降级 地图超时 → 展示缓存数据+「显示历史数据」,不白屏
保存行程 草案含 ≥1 天 ≥3 POI → 点保存置 saved,可二次编辑
登录鉴权 正确邮箱密码 → 签发凭证,行程与账号绑定;错误密码提示失败

8.3 专项测试用例

  • 外部依赖降级(高德/LLM × 超时/429/空/脏数据):逐一注入故障,验证降级行为正确。

  • 地图边界:跨城分段、海上仅打点、经纬度缺失降级列表、单日 POI =15/=16 验证 Clustering 临界。

  • 竞态:1 秒内连续拖拽 ≥3 次,验证后发慢响应不覆盖新结果,最终态与 Store 一致。

  • 契约测试:LLM 输出走 schema 校验 + amap_id 反查 + 字数 + 坐标范围四关。

8.4 测试技术栈

层 工具
后端单测/契约 pytest + httpx(异步客户端)
前端单测 Vitest(与 Vite 同生态)
前端组件/交互 React Testing Library
端到端(可选) Playwright(拖拽/竞态场景)

9. 开发排期与本地跑通指南

9.1 排期总览(单人 · P0 本地跑通)

目标:用最短路径把"输入意图 → 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 项剔出本期。比价、抓取、交通耗时预估均不在本期。

9.2 关键路径与并行建议

  • 串行硬依赖:M0→M1→M2 必须按序,数据模型不定,Store 与 LLM 输出 schema 都无从对齐。

  • 可穿插并行:M3 的地图集成(高德 JS API 接入、Key 申请、基础打点)与 M2 的后端链路可并行预研,降低后期阻塞。

  • 尽早冒烟:M2 完成时就跑一次 PG 连接冒烟(仅建表+读写),提前暴露 SQLite→PostgreSQL 迁移隐患(对应 R8)。

  • 测试左移:契约校验(§6.3)随 M2 一起写,不要堆到 M6;竞态用例随 M4 落地。

9.3 本地跑通环境准备

依赖 版本建议 用途
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。

9.4 启动步骤

后端(终端 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。

9.5 联调自检清单

  • 后端 /health 返回 200;前端控制台无跨域报错(CORS 已在 FastAPI 放行 localhost)。

  • 输入「成都耍三天」→ 骨架屏先出,SSE 逐块填充,最终 ≥3 POI。

  • 勾选 POI → 日程表入项 + 地图打点;剔除 → 该类目重生成。

  • 拖拽改序 → 仅相邻 transit 变化、地图同步、无整页 loading。

  • 1 秒内连点拖拽 ≥3 次 → 最终态与 Store 一致,无错乱。

  • 关闭网络模拟高德超时 → 展示「显示历史数据」,不白屏。

  • 注册→登录→保存→刷新,行程仍在且归属正确。

本期完成定义(DoD):§8.2 七条 P0 GWT 全部通过 + §8.3 专项用例(降级/边界/竞态/契约)跑通 + 一份能让新环境照着起服务的 README。达成即视为"本地跑通"目标完成,后续再进入 P1 与上线加固。

(注:内容由 AI 生成,请谨慎参考)