摘要:本文是《从 0 到 1 构建一个智能旅行 Agent 后端》系列开篇。记录了一个个人学习项目如何从简单的 LLM Demo 逐步演进为具备状态管理、HITL 人工确认、Session Fork 多方案对比、双存储持久化和 FastAPI 服务化能力的 Agent 后端。当前 Agent 使用规则 Mock 实现,尚未接入真实 LLM,重点探索 Agent 后端工程架构而非模型调优。
副标题:基于 LangGraph + FastAPI + SQLite 的 Agent 后端工程实践
本文是《从 0 到 1 构建一个智能旅行 Agent 后端》系列开篇。本系列记录一个个人学习项目如何从简单的 LLM 应用思路,逐步演进为具备状态管理、人工确认、分支执行和持久化能力的 Agent 后端。
这是一个个人学习项目(helloagents-trip-planner)。当前 Agent 使用规则 Mock实现,尚未接入真实 LLM。我刻意把「模型能力」和「工程架构」拆开练——本系列重点探索 Agent 后端该怎么搭,而不是 Prompt 怎么调。
如果你也做过「用户输入 → 调模型 → 输出一段文字」的 Demo,大概会和我有同样的起点。但当我把场景从「演示一次生成」推到「用户会改主意、会对比方案、会隔天回来继续」时,问题就不再是「怎么让模型写得更好」,而是:为什么一个旅行规划 Agent,不能只是 User → LLM → Answer 这么简单?
1. 我为什么开始做这个 Agent
我一开始的目标很朴素:
输入旅行需求 ↓ 生成旅行方案Mock 能稳定吐出结构化字段和几条示例路线后,我以为离「能用的 Agent」不远了。直到真实交互一个个冒出来,我才意识到:产品形态远比「生成内容」复杂。
用户会修改需求。不是每次换一句新 prompt 重开一局,而是在同一次规划里反复 tweak——改天数、改预算、补目的地。没有结构化状态,后端只能整段重跑,或假装「模型记住了」。
用户会比较不同路线。已有「悉尼 5 天」方案,又说「换墨尔本试试」。这不是改几个字段,而是产生新的探索方向;在同一时间线上原地改写,旧方案会被覆盖,无法并排对比。
用户希望保留历史方案。上周那条路线还想留着,从它再 fork 一条新分支继续改——而不是只能编辑当前这一条。
用户希望暂停后继续。需求还没确认完就关了页面,或者服务重启了,下次回来应该从确认门接着走,而不是从头再来。
我选旅行规划做练手,也是因为它天然带这些张力:先锁「要什么样的旅行」,再锁「选哪条路线」,还可能另开方案对比。换成「单次问答式攻略生成」,很多层根本不会出现——但那就不是我在练的产品形态了。
这些问题逐渐把核心从「生成一段攻略」推成了:管理一个持续演进的状态。今天的后端骨架——可暂停、可分支、可持久化、可接 HTTP——不是第一天画完架构图就有的,是被这些问题一步步逼出来的。如果一开始就宣称「架构设计完备」,那是在骗自己;真实路径是:Mock 能跑 → 用户改需求 → 加确认门 → 用户要比方案 → 加 Fork → 要跨重启 → 加持久化 → 要接前端 → 加 HTTP 边界。
2. 普通 LLM Demo 遇到的问题
一个最简单的 Agent 长这样:
User ↓ Prompt ↓ LLM ↓ Answer在 Demo 里它能跑。放进稍真实的交互,很快就会撞墙。
问题 1:模型不应该决定流程
例如:是否等待用户确认?是否进入路线规划阶段?用户点了「修改需求」后该回到哪一步?这些都不是「让模型再想想」能解决的——它们是产品规则,必须确定性执行。如果把流程控制权交给 LLM,短期看起来像 Agent,长期一定状态漂移:同一句用户输入,有时跳过确认,有时多问一轮。
问题 2:用户行为不是线性的
用户不会按你设计的脚本走。常见场景:
悉尼路线不错,但我想看看墨尔本版本。
这不是在同一份草稿上改几个字段,而是另开一条探索线。如果后端只有「当前方案」一个槽位,每次探索都会覆盖上一次的结果,用户就没法并排比较「悉尼版」和「墨尔本版」。
问题 3:执行过程需要恢复
用户关闭页面、容器重启、部署更新——内存里的执行位置会全丢。没有持久化,Agent 只能当一次性玩具:每次回来都是新会话,之前的确认门、选中的路线、fork 出来的分支全部归零。
这三类问题叠在一起,Agent 的核心逐渐变成三件事:状态、流程、恢复。生成内容只是其中一环,而且往往还不是最难的一环。把它们拆成五个更具体的问题,就是本系列后面几层架构的直接动机:
用户为什么需要确认?旅行规划不是「模型觉得可以就定稿」。需求还不完整时(比如缺天数),不应该进入路线规划;用户说「差不多就这样」和「我还要改预算」,后端必须能区分。确认是控制流事件,不是多调一次模型就能替代的。
为什么修改需求不是重新调用一次模型?用户在确认门上点 MODIFY,期望的是:保留会话上下文、回到需求草稿、重新生成后再停在同一确认门——而不是新开一个 HTTP 请求、丢失 stage、把之前选过的路线一并清掉。修改是状态迁移,不是「再 prompt 一遍」。
为什么多方案探索需要 Session Fork?「悉尼版」和「墨尔本版」是两条并行的时间线,用户可能来回切换对比。在同一 thread 上 update 几个字段,旧方案会被覆盖;Fork 让每条探索线有独立的执行上下文,父方案只读保留。
为什么 Agent 需要保存执行状态?图跑在 interrupt 处暂停时,内存里只有「停在哪、next 是谁、当前 TripState 是什么」这一整套执行快照。用户隔天回来、容器重建,如果没有 LangGraph Checkpoint + 业务元数据,只能从头再来。
为什么 HTTP 接入后需要重新设计边界?一旦暴露 REST 端点,Router 很容易直接调 Graph、直接写 Repository、绕过 confirmation_flow。短期能跑,长期一定破坏「Agent 不改 Control、transition 不外泄到 HTTP」这些不变量。HTTP 层需要 Application Service 收口,而不是把图当普通函数调。
3. 我的 Agent 后端最终解决了什么问题
回头看,这个项目是一层层补出来的,不是一次设计完备的:
简单 Agent | v LangGraph 工作流 | v HITL 人工确认 | v Session Fork 多方案 | v Persistence 持久化 | v FastAPI 服务化LangGraph 工作流——因为单次 pipeline 撑不住「需求解析 → 确认 → 路线规划 → 再确认」的多步编排。
HITL 人工确认——因为用户必须在关键节点显式 CONFIRM / MODIFY / BACK,而不是让模型「假装确认过了」。
Session Fork——因为「换墨尔本试试」需要独立时间线,不能在同一 thread 上覆盖旧方案。
Persistence——因为进程重启后还要能从 interrupt 位置继续,业务元数据和图执行态都得有地方存。
FastAPI 服务化——因为 HTTP 接入后,Router 若直接碰 Graph 和 Repository,边界会失控,不变量很难守住。
每一层都对应上面某个「为什么」:确认门回答「为什么要 HITL」;条件路由回答「为什么 MODIFY 不能当 CONFIRM 处理」;Fork 回答「为什么不能原地覆盖」;双存储回答「为什么 MemorySaver 不够」;Application Service 回答「为什么 HTTP 不能直接调 Graph」。
这些能力不是开会画一张大图就齐了的。真实顺序里,LangGraph 和 HITL 来得最早,Fork 和持久化是产品形态清晰之后才补,FastAPI 则在内部闭环跑通后才收口到 HTTP。博客系列按概念理解顺序组织,和代码提交顺序不完全一致——先建立心智模型,再讲踩坑细节,读者更容易跟住因果链。
4. 最终架构的核心思想
这里只做概念介绍,细节留给后续文章。
Agent ↓ 负责领域生成 Transition ↓ 负责用户动作和控制 LangGraph ↓ 负责执行流程 Checkpoint(LangGraph Checkpointer 持久化的执行快照) ↓ 负责恢复状态 Repository ↓ 负责业务元数据分工可以概括成一句话:
Agent 负责「想什么」,流程系统负责「下一步做什么」。
Agent 读用户输入、产出结构化需求和路线候选——它只管「理解与生成的领域内容」。用户点了 CONFIRM 还是 MODIFY?当前 stage 能不能进路线规划?选中 id 是否合法?这些由Transition和LangGraph的确定性控制来管。进程重启后从哪恢复?LangGraph Checkpoint管执行态(interrupt 停在哪、next 节点是谁),Repository管业务元数据(Session 列表、active 分支、用户可见的快照)——两者各存各的,不混写。
我曾差点违反这条原则:让 Mock Agent 顺便写stage、在 wait 节点里直接做 transition、Router 里直接Command(resume=True)——每一种都让短期 demo 更快,但测试立刻变成「看实现细节心情」。最终收口的约束是:Domain 归 Agent,Control 归 transition,Execution 归 LangGraph,持久化分双存储,HTTP 归 Application Service。
这条原则看起来简单,落地时要和 interrupt、条件路由、fork 权限、HTTP 边界一一对齐。本系列后面五篇就是围绕这些对齐过程展开的——这里不展开 conditional edge 怎么写、fork 怎么不复制父 LangGraph Checkpoint、metadata first 怎么投影,那些留给对应篇章。
5. 这个系列会讲什么
如果把五层能力写成一篇「架构大全」,读者很难跟住「当时为什么加这一层」。我把演进过程拆成五篇独立文章,每篇对应一类设计张力,按概念理解顺序组织——不是 API 文档,也不是 README,而是第一人称工程实践:先讲我遇到了什么,再讲我改了什么,以及我当时差点怎么走偏。
第一篇
为什么我没有直接写 Agent,而是先设计状态机
核心:为什么 Agent 首先应该设计状态,而不是 Prompt。Domain / Control / Execution 必须拆开。
第二篇
LangGraph 实战踩坑:为什么 interrupt 后 resume 会跑错节点
核心:状态正确,不代表流程正确。interrupt、transition、条件边各管什么,不能混。
第三篇
为什么 Agent 需要 Session Fork:从改几个字段到多方案时间线
核心:用户在探索新可能,不是原地改方案。Branch 是时间线,不是版本号。
第四篇
为什么 Agent 系统需要双存储:Business DB 和 Checkpoint 各管什么
核心:重启以后怎么办。业务事实与执行事实分存,禁止双写 TripState。
第五篇
FastAPI 接入 Agent 后端:为什么 Router 不能直接调用 Graph
核心:HTTP 接入后边界如何收口。Router → Application Service → Graph。
6. 当前项目边界
保持真实:这是一个用于学习 Agent 后端工程化的项目,不是生产系统。
已完成:
- Mock Requirement Agent(规则解析)
- Mock Route Planner
- LangGraph HITL(双确认门、interrupt / resume)
- Session Fork(独立 thread、多方案时间线)
- FastAPI HTTP 层
- SQLite Persistence(Business DB + SqliteSaver LangGraph Checkpoint)
- Restart Recovery(容器销毁重建后可从 interrupt 继续)
未完成:
- Vue3 前端联调
- 真实 LLM 接入
- MCP 工具集成
- 鉴权与多用户
- 多 worker / 高并发部署
测试覆盖了 HTTP、分支隔离、失败补偿和重启恢复等主链路,但测试通过不等于生产就绪。当前 Agent 仍是规则 Mock——换真实模型之前,架构边界必须先站住。Mock 不是偷懒,而是把「模型会不会胡说」和「系统能不能管住流程」拆开验证:前者等 LLM 接入再测,后者现在就能用规则和测试固定行为。
如果你也在做 Agent 后端,建议先问自己:你的产品是需要「一次出答案」,还是「长期管状态」?我的答案是后者,所以这套骨架值得先搭好,再换真实模型。
7. 总结
做完这一圈,我留下三个观点:
第一,Agent 应该先解决状态和流程问题,而不是只关注 Prompt。模型写得再漂亮,没有确认门、没有 stage 约束、没有恢复能力,撑不住真实交互。
第二,复杂 Agent 的核心不是一次生成,而是长期交互过程中的状态管理。用户会改、会比、会暂停、会回来——后端要管理的,是一个持续演进的状态,而不只是一段输出文本。
第三,工程化 Agent 需要明确:哪些事情交给模型,哪些事情必须由系统控制。理解与生成交给 Agent;流程、权限、持久化、HTTP 边界交给确定性系统。混在一起,短期像 Agent,长期一定失控。
下一篇:
《为什么我没有直接写 Agent,而是先设计状态机》