公司动态
【从 LLM Demo 到 Agent 后端】从 0 到 1 构建一个智能旅行 Agent 后端
摘要本文是《从 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 ↓ 负责执行流程 CheckpointLangGraph Checkpointer 持久化的执行快照 ↓ 负责恢复状态 Repository ↓ 负责业务元数据分工可以概括成一句话Agent 负责「想什么」流程系统负责「下一步做什么」。Agent 读用户输入、产出结构化需求和路线候选——它只管「理解与生成的领域内容」。用户点了 CONFIRM 还是 MODIFY当前 stage 能不能进路线规划选中 id 是否合法这些由Transition和LangGraph的确定性控制来管。进程重启后从哪恢复LangGraph Checkpoint管执行态interrupt 停在哪、next 节点是谁Repository管业务元数据Session 列表、active 分支、用户可见的快照——两者各存各的不混写。我曾差点违反这条原则让 Mock Agent 顺便写stage、在 wait 节点里直接做 transition、Router 里直接Command(resumeTrue)——每一种都让短期 demo 更快但测试立刻变成「看实现细节心情」。最终收口的约束是Domain 归 AgentControl 归 transitionExecution 归 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 PlannerLangGraph HITL双确认门、interrupt / resumeSession Fork独立 thread、多方案时间线FastAPI HTTP 层SQLite PersistenceBusiness DB SqliteSaver LangGraph CheckpointRestart Recovery容器销毁重建后可从 interrupt 继续未完成Vue3 前端联调真实 LLM 接入MCP 工具集成鉴权与多用户多 worker / 高并发部署测试覆盖了 HTTP、分支隔离、失败补偿和重启恢复等主链路但测试通过不等于生产就绪。当前 Agent 仍是规则 Mock——换真实模型之前架构边界必须先站住。Mock 不是偷懒而是把「模型会不会胡说」和「系统能不能管住流程」拆开验证前者等 LLM 接入再测后者现在就能用规则和测试固定行为。如果你也在做 Agent 后端建议先问自己你的产品是需要「一次出答案」还是「长期管状态」我的答案是后者所以这套骨架值得先搭好再换真实模型。7. 总结做完这一圈我留下三个观点第一Agent 应该先解决状态和流程问题而不是只关注 Prompt。模型写得再漂亮没有确认门、没有 stage 约束、没有恢复能力撑不住真实交互。第二复杂 Agent 的核心不是一次生成而是长期交互过程中的状态管理。用户会改、会比、会暂停、会回来——后端要管理的是一个持续演进的状态而不只是一段输出文本。第三工程化 Agent 需要明确哪些事情交给模型哪些事情必须由系统控制。理解与生成交给 Agent流程、权限、持久化、HTTP 边界交给确定性系统。混在一起短期像 Agent长期一定失控。下一篇《为什么我没有直接写 Agent而是先设计状态机》