从 0 到 1 打造 AI 旅行规划平台:LangGraph 多智能体工作流 + RAG 的完整工程实践
一个让用户输入目的地和偏好、自动生成逐日行程的 AI 平台。本文完整拆解它的架构设计:LangGraph 8 节点状态图、ReAct 规划智能体、三域隔离 RAG、Outbox 可靠消息、多层幂等与 AI 额度计费。全部方案均已在生产级代码中落地。
一、项目概览
AI 旅行规划平台是一个面向自由行用户的行程生成系统:用户输入目的地、日期和偏好,系统自动完成知识检索、行程规划、POI 核验,生成一份逐日行程预览,用户确认后定稿;另配一个可流式对话的 AI 旅行助手。
先给一组数据感受一下规模:
| 维度 | 数量 |
|---|---|
| 后端模块 | 23 个 |
| API 接口 | 178 个 |
| 后端代码 | 约 2.1 万行 |
| 测试用例 | 507 项 |
| 数据库迁移 | 45 个 |
| 架构文档 | 75 篇 |
技术栈:FastAPI + Vue3 + LangGraph + Milvus + Elasticsearch + MySQL/PostgreSQL 双库 + Redis + RabbitMQ。
二、整体架构
系统按"业务/AI/基础设施"分库存储:
- MySQL:业务主库(用户、行程、生成任务、outbox 事件、额度周期)
- PostgreSQL(AI 库):AI 专属数据(对话消息、LangGraph Checkpoint、长期记忆、助手运行记录)
- Milvus:向量检索(RAG 稠密召回)
- Elasticsearch:BM25 检索(RAG 稀疏召回 + 社区内容搜索)
- RabbitMQ:异步任务管道(行程生成任务、outbox 事件分发)
- Redis:缓存与分布式锁
#mermaid-svg-vQmvypZSFEfljZRu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vQmvypZSFEfljZRu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vQmvypZSFEfljZRu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vQmvypZSFEfljZRu .error-icon{fill:#552222;}#mermaid-svg-vQmvypZSFEfljZRu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vQmvypZSFEfljZRu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vQmvypZSFEfljZRu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vQmvypZSFEfljZRu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vQmvypZSFEfljZRu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vQmvypZSFEfljZRu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vQmvypZSFEfljZRu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vQmvypZSFEfljZRu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vQmvypZSFEfljZRu .marker.cross{stroke:#333333;}#mermaid-svg-vQmvypZSFEfljZRu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vQmvypZSFEfljZRu p{margin:0;}#mermaid-svg-vQmvypZSFEfljZRu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vQmvypZSFEfljZRu .cluster-label text{fill:#333;}#mermaid-svg-vQmvypZSFEfljZRu .cluster-label span{color:#333;}#mermaid-svg-vQmvypZSFEfljZRu .cluster-label span p{background-color:transparent;}#mermaid-svg-vQmvypZSFEfljZRu .label text,#mermaid-svg-vQmvypZSFEfljZRu span{fill:#333;color:#333;}#mermaid-svg-vQmvypZSFEfljZRu .node rect,#mermaid-svg-vQmvypZSFEfljZRu .node circle,#mermaid-svg-vQmvypZSFEfljZRu .node ellipse,#mermaid-svg-vQmvypZSFEfljZRu .node polygon,#mermaid-svg-vQmvypZSFEfljZRu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vQmvypZSFEfljZRu .rough-node .label text,#mermaid-svg-vQmvypZSFEfljZRu .node .label text,#mermaid-svg-vQmvypZSFEfljZRu .image-shape .label,#mermaid-svg-vQmvypZSFEfljZRu .icon-shape .label{text-anchor:middle;}#mermaid-svg-vQmvypZSFEfljZRu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vQmvypZSFEfljZRu .rough-node .label,#mermaid-svg-vQmvypZSFEfljZRu .node .label,#mermaid-svg-vQmvypZSFEfljZRu .image-shape .label,#mermaid-svg-vQmvypZSFEfljZRu .icon-shape .label{text-align:center;}#mermaid-svg-vQmvypZSFEfljZRu .node.clickable{cursor:pointer;}#mermaid-svg-vQmvypZSFEfljZRu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vQmvypZSFEfljZRu .arrowheadPath{fill:#333333;}#mermaid-svg-vQmvypZSFEfljZRu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vQmvypZSFEfljZRu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vQmvypZSFEfljZRu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vQmvypZSFEfljZRu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vQmvypZSFEfljZRu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vQmvypZSFEfljZRu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vQmvypZSFEfljZRu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vQmvypZSFEfljZRu .cluster text{fill:#333;}#mermaid-svg-vQmvypZSFEfljZRu .cluster span{color:#333;}#mermaid-svg-vQmvypZSFEfljZRu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vQmvypZSFEfljZRu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vQmvypZSFEfljZRu rect.text{fill:none;stroke-width:0;}#mermaid-svg-vQmvypZSFEfljZRu .icon-shape,#mermaid-svg-vQmvypZSFEfljZRu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vQmvypZSFEfljZRu .icon-shape p,#mermaid-svg-vQmvypZSFEfljZRu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vQmvypZSFEfljZRu .icon-shape .label rect,#mermaid-svg-vQmvypZSFEfljZRu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vQmvypZSFEfljZRu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vQmvypZSFEfljZRu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vQmvypZSFEfljZRu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 存储
后端 FastAPI
前端
C端 Vue3
B端管理后台
API 层 178 接口
LangGraph 8节点工作流
旅行助手 ReAct Agent
RAG 检索服务
MySQL 业务库
PostgreSQL AI库
Milvus
Elasticsearch
Redis
RabbitMQ
三、核心设计一:LangGraph 8 节点行程生成流水线
这是整个项目最核心的设计。行程生成不是"一次大模型调用",而是一条确定性骨架 + 单点智能的流水线:
#mermaid-svg-1OC9GJzXgTurFvCP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1OC9GJzXgTurFvCP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1OC9GJzXgTurFvCP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1OC9GJzXgTurFvCP .error-icon{fill:#552222;}#mermaid-svg-1OC9GJzXgTurFvCP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1OC9GJzXgTurFvCP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1OC9GJzXgTurFvCP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1OC9GJzXgTurFvCP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1OC9GJzXgTurFvCP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1OC9GJzXgTurFvCP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1OC9GJzXgTurFvCP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1OC9GJzXgTurFvCP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1OC9GJzXgTurFvCP .marker.cross{stroke:#333333;}#mermaid-svg-1OC9GJzXgTurFvCP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1OC9GJzXgTurFvCP p{margin:0;}#mermaid-svg-1OC9GJzXgTurFvCP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1OC9GJzXgTurFvCP .cluster-label text{fill:#333;}#mermaid-svg-1OC9GJzXgTurFvCP .cluster-label span{color:#333;}#mermaid-svg-1OC9GJzXgTurFvCP .cluster-label span p{background-color:transparent;}#mermaid-svg-1OC9GJzXgTurFvCP .label text,#mermaid-svg-1OC9GJzXgTurFvCP span{fill:#333;color:#333;}#mermaid-svg-1OC9GJzXgTurFvCP .node rect,#mermaid-svg-1OC9GJzXgTurFvCP .node circle,#mermaid-svg-1OC9GJzXgTurFvCP .node ellipse,#mermaid-svg-1OC9GJzXgTurFvCP .node polygon,#mermaid-svg-1OC9GJzXgTurFvCP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1OC9GJzXgTurFvCP .rough-node .label text,#mermaid-svg-1OC9GJzXgTurFvCP .node .label text,#mermaid-svg-1OC9GJzXgTurFvCP .image-shape .label,#mermaid-svg-1OC9GJzXgTurFvCP .icon-shape .label{text-anchor:middle;}#mermaid-svg-1OC9GJzXgTurFvCP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1OC9GJzXgTurFvCP .rough-node .label,#mermaid-svg-1OC9GJzXgTurFvCP .node .label,#mermaid-svg-1OC9GJzXgTurFvCP .image-shape .label,#mermaid-svg-1OC9GJzXgTurFvCP .icon-shape .label{text-align:center;}#mermaid-svg-1OC9GJzXgTurFvCP .node.clickable{cursor:pointer;}#mermaid-svg-1OC9GJzXgTurFvCP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1OC9GJzXgTurFvCP .arrowheadPath{fill:#333333;}#mermaid-svg-1OC9GJzXgTurFvCP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1OC9GJzXgTurFvCP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1OC9GJzXgTurFvCP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1OC9GJzXgTurFvCP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1OC9GJzXgTurFvCP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1OC9GJzXgTurFvCP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1OC9GJzXgTurFvCP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1OC9GJzXgTurFvCP .cluster text{fill:#333;}#mermaid-svg-1OC9GJzXgTurFvCP .cluster span{color:#333;}#mermaid-svg-1OC9GJzXgTurFvCP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1OC9GJzXgTurFvCP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1OC9GJzXgTurFvCP rect.text{fill:none;stroke-width:0;}#mermaid-svg-1OC9GJzXgTurFvCP .icon-shape,#mermaid-svg-1OC9GJzXgTurFvCP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1OC9GJzXgTurFvCP .icon-shape p,#mermaid-svg-1OC9GJzXgTurFvCP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1OC9GJzXgTurFvCP .icon-shape .label rect,#mermaid-svg-1OC9GJzXgTurFvCP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1OC9GJzXgTurFvCP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1OC9GJzXgTurFvCP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1OC9GJzXgTurFvCP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不通过·回炉重规划
通过
1 validate_request 请求校验
2 retrieve_evidence 证据检索
3 planning_agent ReAct 规划智能体
4 validate_schema 结构校验
5 map_agent 高德 POI 核验
6 generation_review_agent 审核
7 预览落库·带引用
8 user_confirmation 人工确认
结束
对应的真实代码(workflow.py)非常短,因为复杂度被封装进了每个节点:
python
graph = StateGraph(_LangGraphState)
graph.add_node("validate_request", self._graph_validate_request)
graph.add_node("retrieve_evidence", self._graph_retrieve_evidence)
graph.add_node("planning_agent", self._graph_generate_structured_draft)
graph.add_node("validate_schema", self._graph_validate_schema)
graph.add_node("map_agent", self._graph_verify_pois_with_amap)
graph.add_node("generation_review_agent", self._graph_check_date_budget_route_constraints)
graph.add_node("save_immutable_preview_with_audit", self._graph_save_preview_with_citations)
graph.add_node("user_confirmation", self._graph_user_confirmation)
# 审核节点带条件边:不通过则回到规划节点重试
graph.add_conditional_edges(
"generation_review_agent", self._route_review,
{"planning_agent": "planning_agent",
"save_immutable_preview_with_audit": "save_immutable_preview_with_audit"},
)
graph = graph.compile(checkpointer=checkpointer)
为什么这样设计? 三个原因:
- 可控性 。8 个节点里 7 个是确定性的纯逻辑(校验、检索、Schema 检查、地图核验),只有
planning_agent一个节点交给大模型。AI 的自由度被严格限制在"规划"这一件事上,输出还要经过 Schema 校验和地图核验两道闸门。 - 可追溯性。每个节点执行完都会产出一条审计记录(耗时、工具调用、降级标记)落库,行程出了问题可以精确到节点定位。
- 可恢复性 。图的状态通过
AsyncPostgresSaver持久化,thread_id绑定生成任务 ID,任何一步中断都能接续。
关于"软 HITL"的取舍 :第 8 个节点 user_confirmation 只标记"等待确认",不写正式行程数据。第 7 步落库的是一份不可变的行程预览(带引用),正式定稿走单独的 apply 接口。这比传统"人在环中阻塞等待"的 interrupt 模式更实用------图跑完就结束,用户什么时候确认都行,超时不占资源。
四、核心设计二:规划节点是 ReAct 智能体,但被"关在笼子里"
planning_agent 节点用 create_agent 构建成 ReAct 智能体,模型可以自主决定调用哪些只读工具(实时天气、官方知识库、社区帖子、网页搜索)来补齐证据,再产出结构化行程草稿。
关键约束有三层:
- 工具全部只读------智能体不可能污染业务数据;
- 输出强制约束在已验证的 POI 候选池内------检索阶段验证过哪些景点,规划就只能用哪些,从机制上杜绝"编造景点";
- 递归上限兜底------模型再怎么折腾也有步数上限,不会无限循环。
一句话总结:自主性和可控性不是对立的,关键在于把自主权放在哪一层。让模型决定"查什么、怎么查",但"能用什么、能产出什么"由工程侧焊死。
五、核心设计三:三域隔离的 RAG 数据面
RAG 检索分三个互不渗透的域:
| 域 | 内容 | 可见性 |
|---|---|---|
| 官方知识域 | 目的地攻略、POI 权威信息 | 全员可见 |
| 社区内容域 | 用户发布的田野笔记、路线 | 全员可见 |
| 用户私人记忆域 | 个人出行画像、偏好沉淀 | 仅本人可见(绑定 user_id) |
每个域独立 collection 与索引,检索时按域过滤可见性。跨用户数据泄漏在 schema 层被拦截------不是靠应用层代码小心,而是数据结构上就查不到别人的记忆。用户私人记忆还会随对话沉淀(出发城市、兴趣标签、出行节奏、旅行者类型),越用越懂你。
检索本身是混合检索:Milvus 向量(语义)+ ES BM25(关键词)双路召回,RRF 融合排序:
python
def _rrf(self, dense_results, sparse_results):
merged = {}
for results in (dense_results, sparse_results):
for rank, result in enumerate(results, start=1):
key = (result.document_id, result.chunk_id)
previous = merged.get(key)
score = (previous[1] if previous else 0.0) + 1 / (self.config.rrf_k + rank)
merged[key] = (result, score)
# 对"两路都排第一"的理论最优分归一化,让 min_score 有意义
...
融合后取 top8 上下文,且带 citation(引用来源)------行程里每个建议都能溯源到知识库文档或网页。
抑制幻觉的机制 :设置 0.35 置信度门槛(rag_min_score),低于阈值时系统不编造行程,而是转入澄清提问,让用户补充信息。宁可多问一句,不可瞎编一段。
六、核心设计四:旅行助手------create_agent + 7 工具 + SSE 流式
行程生成之外还有一个独立的对话式旅行助手。它同样是 create_agent 构建的 ReAct 智能体,挂载 7 个工具:
| 工具 | 说明 |
|---|---|
| search_official_knowledge | 检索官方知识域 |
| search_community_posts | 检索社区内容域 |
| search_personal_memory | 检索用户私人记忆域 |
| get_weather | 实时天气 |
| get_weather_forecast | 多日天气预报 |
| web_search | 网页搜索(时效信息) |
| fetch_web_page | 抓取网页全文(搜索结果不够细时跟进) |
对话通过 astream_events 实现 SSE 流式输出,用户能实时看到模型的思考和工具调用过程,不用对着空白等待。
两个工程细节:
- 幂等去重 :每条消息以
client_message_id在ai_messages表做唯一索引,前端重试、网络重发都不会产生重复应答、重复扣费; - 消息级计费:助手对话按消息扣 AI 额度,行程生成按任务扣------两种能力两种计费粒度(下文详述)。
七、核心设计五:Outbox + 三层幂等,事件不丢不重
异步链路(生成任务从创建到执行)的可靠性靠三层防线:
第一层:接口幂等。 创建生成任务时以(user_id + idempotency_key)组合校验:同 key 同内容直接返回原任务、不重复扣费;同 key 不同内容抛 IDEMPOTENCY_CONFLICT。
第二层:Outbox + Publisher Confirms。 业务事务内写 outbox_events 表(与业务数据同库同事务,天然原子);独立发布器只投递 published_at IS NULL 的事件,RabbitMQ 确认送达后回写时间戳------没确认就不回写,进程崩了重启会重新投递,配合消费端幂等实现"至少一次送达 + 恰好一次效果"。失败消息进死信队列兜底。
第三层:消费端幂等认领。 Worker 收到消息先 FOR UPDATE 锁 generation_jobs 行,只有 queued 状态才执行 ------MQ 重复投递、延迟重投全部直接跳过。进度回写还要校验 trace_id 且阶段只进不退,防止一条延迟的旧投递把新状态覆盖回去。
这套设计的本质:把"不丢"交给 Outbox 的事务原子性,把"不重"交给数据库行锁 + 状态机的唯一性判断,中间的 MQ 只是运输管道,坏了重投也没关系。
写路径同样有保障:行程定稿接口要求 If-Match-Version(乐观锁防协作覆盖)+ X-Operation-ID(防重试重复定稿),配合第一层的 Idempotency-Key,形成多层幂等矩阵。
八、核心设计六:AI 额度计费------把 AI 能力做成可计费产品
这是很多 AI Demo 缺失的一环:AI 能力必须有商业闭环。系统实现了双粒度计费:
- 消息级:旅行助手每产生一条 AI 回复扣一次额度;
- 任务级:行程生成每个任务扣一次额度(不成功不扣)。
额度挂在会员权益上:会员购买立即生效,已有排队周期自动顺延到新周期之后。所有额度校验、扣减、会员周期计算都在服务端完成,前端只做展示。
九、社区与工程沉淀
除了 AI 主线,平台还有完整的社区闭环:行程定稿后可发布"田野笔记"沉淀为社区内容(反哺 RAG 的社区域),支持路线"沿用"计数。
工程侧的沉淀:
- 507 项测试 :核心链路(幂等、额度扣减、RAG 融合)全覆盖,数据库迁移用
alembic upgrade head → downgrade -1 → upgrade head验证可回滚; - 75 篇架构文档:8 节点工作流逐节点讲解、RAG 切片算法走查、错误复盘记录等,做到"每个关键决策都有文档可查";
- 45 个数据库迁移:全部通过 Alembic 管理,schema 变更从不手改库。
十、踩过的坑与思考
1. 短信通道的坑。 198 开头号段被运营商屏蔽导致验证码收不到,排查了很久才定位到是通道问题而不是代码问题。教训:外部依赖的故障要第一时间换通道验证,而不是反复查自己的代码。
2. LangGraph checkpoint 的取舍。 早期纠结要不要做"节点级断点续跑",后来算了一笔账:整张图跑完只要几十秒,除规划节点外全部是确定性、只读、可重入的,整图重跑比断点续跑简单一个数量级。最终采用 RabbitMQ 重投 + DB 幂等认领 + trace_id 对账实现任务级恢复。启示:分布式恢复的粒度不是越细越好,要匹配任务的耗时特征。
3. 降级优先于报错。 Dify、ES、天气 API、网页搜索,所有外部依赖都有降级路径:检索降级走单路召回、ES 降级走 MySQL LIKE、网页搜索不可用就跳过。AI 平台的价值恰恰体现在"坏一半还能用"。
十一、核心页面展示




十二、总结
这个项目给我的最大收获是:AI 应用的难点不在调通模型,而在工程化------怎么让大模型的输出可信(候选池约束 + 双重校验)、怎么让异步链路可靠(Outbox + 三层幂等)、怎么让 AI 能力可运营(双粒度计费 + 会员权益)。LangGraph 编排、RAG 混合检索这些"AI 味"的方案,最终都要落在数据库行锁、唯一索引、版本号这些朴素的工程手段上才真正可靠。
如果你也在做 AI 应用工程化落地,欢迎评论区交流。
文中截图为平台实际运行页面;架构图与流程图使用 Mermaid 绘制,代码片段摘自项目真实实现。