实验室的预约群又炸了。
管理员早上刚发了一条“本周三下午可约”,不到十分钟,群里就刷了上百条消息,有人抢到了黄金时段,有人对着“已被占用”的红色提示骂骂咧咧,还有人直接私聊管理员说要走后门。这种混乱我实在太熟悉了,几乎所有靠人工登记的实验室都逃不过这一劫。所以当我决定自己动手做一个智能实验室预约系统时,第一反应不是去网上找一个现成的预约插件,而是把这个项目当成一次完整的 AI 工程实践来做:后端用 FastAPI 搭 RESTful 服务,核心调度逻辑用 LangGraph 编排成可感知上下文的 Agent,让系统不仅能“预约”,还能在时间冲突时主动给出备选方案。这套组合在当前的技术栈里非常能打,FastAPI 负责稳定地把 API 暴露给前端,LangGraph 负责把大模型能力嵌进业务流。这篇文章就把我从零开始搭这个项目的过程完整复盘一遍,包括选型思路、数据库设计、Agent 图谱搭建、并发控制,以及那些文档里查不到的坑。
1. 项目从哪来:一个真实的实验室预约痛点
1.1 为什么实验室预约总能吵起来
先说背景。我所在的实验室有 6 台设备、3 个独立房间,每周开放 40 个小时的预约窗口,但实际使用人数是设备位数的 5 倍以上。原先的预约方式是“微信群 + 共享表格”,管理员用 Excel 手动标时间段,谁先回复谁占坑,结果就是信息滞后、冲突频发、有人临时放了鸽子别人还补不上。表面上看是个管理问题,本质上是个“资源调度 + 信息对称”的技术问题。
传统改法是上个预约系统,表单填一下、时间选一下、提交入库,看起来能解决问题。但用一段时间你会发现三个硬伤:第一,用户输入不标准,“周三下午”、“3点左右”、“后天上午”这种模糊说法,普通表单根本无法处理;第二,时间冲突时系统只会冷冰冰说“不可预约”,不会主动给出相邻空闲时段,用户得换好几个条件反复试;第三,管理员想做一些规则调整(比如某台设备夜间限制使用、长时间预约必须审批),改起来非常痛苦。
这些痛点恰好是大模型 Agent 擅长的地方。自然语言理解可以交给 LLM,多轮对话状态管理可以用 LangGraph 的图结构来做,规则变更就改图节点,不用重构整个系统。这也是我在技术选型时坚持引入 LangGraph 的核心原因:它不是花架子,是真的能把自然语言预约变成现实。
1.2 技术选型:FastAPI + LangGraph 为什么是这个组合
先聊后端框架。实验室预约系统本质上是个 CRUD + 业务规则的 Web 服务,可选方案有 Django、Flask、FastAPI 三个主流。Django 太重,自带 Admin 和 ORM 确实方便,但异步支持是后补的,而且对于一个预约系统来说有点杀鸡用牛刀;Flask 轻量灵活,但原生不支持异步,后续对接流式响应、WebSocket 这类能力时要额外折腾。FastAPI 的好处非常突出:原生 async/await、基于 OpenAPI 自动生成接口文档、Pydantic 做参数校验和序列化,写起来快,跑起来稳。尤其是自动生成的 Swagger 文档,在前后端联调时帮了大忙,前端同事拿着/docs页面就能自己试接口,不用一遍遍来问参数格式。
再补一句热词里高频出现的 FastAPI CORS 问题,这个后面实操章节会细说。CORS 是前后端分离项目躲不开的一关,配置不对你前端 axios 发请求就报跨域错误,但这个跟框架本身没关系,是浏览器安全策略,FastAPI 提供了现成的 CORSMiddleware,用对就行。
LangGraph 这边,我要先泼一盆冷水:不是所有项目都需要上 LangGraph。如果你只是想在 FastAPI 里调用一次大模型做关键词抽取,用 LangChain 的 chain 或者直接调 SDK 就够了。但预约系统天然是“多轮状态型”场景:用户第一句话说“帮我预约明天下午的设备 A”,Agent 需要确认用户身份、解析时间意图、检索设备占用情况、判断与规则的冲突、返回可用时段或者发起二次确认。每一步的输入依赖上一步的输出,而且不同分支有跳转逻辑,这种场景用 LangGraph 的图式编排就是最合适的选择——节点是处理单元,边是状态转移条件,整个对话流程一目了然,出了问题也好定位。
下表整理了我在选型时做的对比,直接贴出来供参考:
| 对比维度 | FastAPI | Flask | Django |
|---|---|---|---|
| 异步支持 | 原生 async/await | 不支持(需额外库) | 3.1+ 逐步完善 |
| 自动文档 | OpenAPI + Swagger 自动生成 | 需手动配置 | 需第三方库 |
| 参数校验 | Pydantic 模型 | 手动校验 | Serializer 较重 |
| LangGraph 集成 | 自然,异步节点友好 | 可集成但不顺畅 | 可集成但偏重 |
| 适合体量 | 中小型服务优选 | 嵌入式/微服务 | 大型单体业务 |
2. 核心设计:预约系统怎么才算“智能”
2.1 从“查询空闲”到“主动调度”的设计升级
做预约系统的第一反应通常是:让用户选设备、选日期、选时间段,然后后端查一下有没有冲突,没有就写入。这个流程能跑通,但谈不上智能。我做的设计是这样升级的:把用户任意口语化的预约请求丢给 Agent,Agent 先做意图理解和槽位抽取,然后调后端接口查空闲,再把结果整理成自然语言响应。如果请求的时间段已经被占用,Agent 不直接拒绝,而是查询最近的两个可替代空闲时段返回给用户,由用户确认或修改。
这样一来,用户在小程序或网页里只需要输入“我要约明天下午两点到四点的设备 B”,系统就能自动完成一系列动作,而不是让用户在三个下拉框里挨个选。真正的“智能”不一定是大模型有多聪明,而是把大模型的语义理解能力嵌进业务流程,把原来需要人类管理员判断的事情交给程序自动完成。
2.2 数据库表结构设计:把预约业务吃透
讲真话,很多 AI 项目的数据库设计都特别敷衍,几张表一把梭。但预约系统不像聊天机器人,它要保证数据一致性,表结构直接决定后面的并发控制和冲突检测能不能写好。我设计了四张核心表:users、equipments、bookings、equipment_maintenance。
users表负责用户身份和权限,普通用户只能预约和取消自己的预约,管理员可以调整所有记录和设置规则。equipments表保存设备基础信息,包括设备名称、房间号、是否启用、单次最长占用时长、是否需要审批。bookings表是核心,字段包括预约人、设备 ID、开始时间、结束时间、状态(待确认/已确认/已取消/已完成)、备注、创建时间。关键是给(equipment_id, start_time, end_time)加复合索引,这是冲突检测效率的基石。
equipment_maintenance表用来记录设备维护计划,这块很多预约系统都会漏掉,但实际使用中维护周期和可预约时间必须联动,否则用户约了一个在检修的设备,现场体验极差。我在查询环节就把维护时间段过滤掉,Agent 返回的空闲时段也不会包含维护时间。
下面是核心bookings表的 SQLite 建表语句示例,我在项目里实际用的是 SQLAlchemy 2.0 的 ORM 写法,这里简化展示便于理解:
CREATE TABLE bookings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), equipment_id INTEGER NOT NULL REFERENCES equipments(id), start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status VARCHAR(20) DEFAULT 'confirmed', remark TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CHECK (end_time > start_time) ); CREATE INDEX idx_bookings_equipment_time ON bookings(equipment_id, start_time, end_time);2.3 权限与规则:给 Agent 加上边界意识
系统里不能什么东西都让大模型自由发挥。比如夜间预约规则:23:00 到次日 07:00 禁止预约(全职管理员值班场景除外),这个规则如果只是在 Prompt 里写一句“请遵守实验室规定”,效果很不稳定,模型偶尔会无视。我把规则做成了 FastAPI 里的一个校验函数,Agent 在调用预约节点前先把时间参数传给这个校验器,返回通过/不通过并附带原因,再决定是否走预约写库分支。
这样做的好处是:规则放在确定的代码里,保证 100% 执行;Agent 只负责自然语言理解和用户沟通,不负责拍板。所谓“智能系统”,是规则引擎和大模型的协同,而不是把一切交给大模型。
3. 实操一把梭:FastAPI 后端从零搭起来
3.1 用 uv 管理项目和虚拟环境
写 Python 项目最烦的是环境管理,以前用pip install+venv容易把系统环境搞乱,坑踩多了之后我现在全面切到了 uv。uv 是一个用 Rust 写的 Python 包管理器,速度比 pip 快一个量级,而且可以一条命令创建虚拟环境、安装依赖、锁定版本。用热词里提到的“uv包管理器创建虚拟环境与fastapi”,这套流程在 Windows 和 macOS 上都能跑,实测非常稳。
核心命令如下:
# 初始化项目 uv init lab-reservation cd lab-reservation # 创建虚拟环境并激活(uv 会自动识别 .python-version) uv venv --python 3.11 source .venv/bin/activate # Windows 下执行 .venv\Scripts\activate # 安装依赖 uv add fastapi uvicorn[standard] sqlalchemy aiosqlite pydantic-settings langgraph langchain-openai httpx提示:安装时不要用
pip install -r requirements.txt直接装,uv 的项目依赖写在pyproject.toml里,版本锁定更可靠。如果你在 PyCharm 里遇到“安装 FastAPI 失败”,大概率是虚拟环境解释器和项目解释器没配对,用 uv 创建好.venv后,手动在 PyCharm 里选该解释器即可解决。
3.2 FastAPI 项目骨架与配置拆分
项目目录我习惯按“路由-服务-模型”三层分,再加一个core放配置和依赖项:
lab-reservation/ ├── app/ │ ├── api/ │ │ ├── routes/ │ │ │ ├── auth.py │ │ │ ├── equipment.py │ │ │ └── booking.py │ │ └── deps.py │ ├── core/ │ │ ├── config.py │ │ └── database.py │ ├── models/ │ │ ├── user.py │ │ ├── equipment.py │ │ └── booking.py │ ├── schemas/ │ │ ├── booking.py │ │ └── user.py │ ├── services/ │ │ ├── booking_service.py │ │ └── slot_service.py │ └── main.py ├── agent/ │ ├── graph.py │ ├── nodes.py │ └── state.py └── pyproject.tomlFastAPI 初始化读取配置文件这个问题,热词里也提到了,我给出一套标准解法:使用pydantic-settings读取.env文件,把数据库连接、密钥、模型 API Key 全部放到环境变量里,不写死在代码中。
# app/core/config.py from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): app_name: str = "Lab Reservation API" database_url: str = "sqlite+aiosqlite:///./lab_reservation.db" openai_api_key: str = "" model_name: str = "gpt-4o-mini" jwt_secret: str = "change-me-in-prod" jwt_expire_minutes: int = 60 * 24 model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") settings = Settings() # app/main.py from fastapi import FastAPI from app.core.config import settings from app.api.routes import booking, equipment, auth app = FastAPI(title=settings.app_name) app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(equipment.router, prefix="/api/equipments", tags=["equipment"]) app.include_router(booking.router, prefix="/api/bookings", tags=["booking"])这种写法的好处是:不同环境(本地、测试、生产)只需要换.env文件内容,代码不用改一行。
3.3 CORS 和中间件:前后端联调避坑
CORS 是前后端分离项目里最常见的第一个坎。浏览器的同源策略会拦截跨域请求,FastAPI 的解决方案是添加 CORSMiddleware。很多新手在这里的困惑是:明明后端接口用 curl 调通了,前端 axios 一调就报Access-Control-Allow-Origin缺失,这不是后端接口有问题,是跨域策略没配好。
具体配置如下:
app.add_middleware( CORSMiddleware, allow_origins=[origin for origin in settings.cors_origins], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )这里有两个经验点。第一,allow_origins别图省事写["*"],因为如果你同时开了allow_credentials=True,浏览器规定通配符是不合法的,这在涉及登录态(Cookie/Session)时必炸。第二,如果你的前端跑在http://localhost:5173(Vite 默认端口),后端跑在http://localhost:8000,那么allow_origins里必须包含http://localhost:5173这个 Origin,否则就会被拦截。前端排查时候,打开浏览器 F12,看网络请求的响应头,如果请求报 CORS,响应头里一定没有Access-Control-Allow-Origin,对照这个就能确认是不是配置问题。
4. 核心环节:用 LangGraph 把预约 Agent 编排出来
4.1 LangGraph 和 LangChain 到底有啥区别
这是热词里出现率极高的问题,也是我在技术分享群被问烂了的问题。LangChain 的核心抽象是 Chain(链),就是把一连串的调用按顺序串起来,上一个的输出是下一个的输入。核心路由控制靠 chain 内部的 Prompt 模板和条件编码,但写复杂逻辑时很容易变成一堆 if-else。LangGraph 则引入了图(Graph)的概念,节点是处理函数,边是条件转移,天然支持环、分支和状态持久化,说白了就是给 AI 应用加了“状态机”能力。
对于预约系统来说,LangChain 的问题是:一次预约请求可能需要多轮对话才能拿到完整的槽位信息,你总不能让用户一次性把所有信息说全。LangGraph 可以做到:第一轮用户说“帮我约设备 A”,Agent 发现时间缺失,走collect_time节点向用户追问;第二轮用户说“明天下午3点”,Agent 再检查设备是否存在、是否空闲、是否冲突,整个过程的状态都保存在图的State对象里。这种能力用 Chain 做也不是不行,但代码会绕到怀疑人生。
注意:听到“LangChain 和 LangGraph 都过时了”这种说法,不必焦虑。作为个人项目和学习来说,LangGraph 目前依然是编排 AI Agent 的最主流选择。框架更替是必然的,但状态机和图编排的核心思想是通用的,学会一种就能快速迁移到新工具。
4.2 Agent 状态图设计:节点和边的决策逻辑
我先定义状态数据结构,也就是 LangGraph 里的GraphState:
# agent/state.py from typing import TypedDict, Annotated, Optional from langgraph.graph.message import add_messages class LabState(TypedDict): messages: Annotated[list, add_messages] user_id: Optional[int] intent: Optional[str] equipment_name: Optional[str] equipment_id: Optional[int] date: Optional[str] start_time: Optional[str] end_time: Optional[str] available_slots: list holidays: list step: str每个字段都有清晰职责:messages保存对话历史,intent保存解析出来的意图(预约/取消/查询/改期),equipment_name是设备名字,start_time/end_time是解析后的标准化时间,available_slots是查询到的可预约时段,step是当前状态。
节点设计上我拆了 5 个:
parse_intent_node:调用 LLM,从用户最新一条消息中抽取意图和设备/时间槽位。把结果写入 state。query_slots_node:调用 FastAPI 后端查询接口(或直接调数据库服务),查目标设备在指定日期哪些时段空闲。check_conflict_node:判断用户请求时间段是否落在空闲列表里,同时校验是否满足实验室规则(夜间禁用、维护中不可约)。book_slot_node:满足条件时直接写库,生成预约成功消息。handle_conflict_node:产生冲突时,从空闲列表里挑出两个最近的可替代时段,生成带选项的回复,引导用户重新选择。
条件边是图的核心。我用的转移逻辑是:
parse_intent_node解析后,如果equipment或time缺失,转移到collect_info_node(这个节点可以简单回复“请补充设备或时间段信息”),否则去query_slots_node。check_conflict_node如果通过,去book_slot_node;不通过则去handle_conflict_node。handle_conflict_node返回后,用户如果确认新的时间,流程回到check_conflict_node重新校验。
# agent/graph.py(核心骨架) from langgraph.graph import StateGraph, END from agent.nodes import ( parse_intent_node, query_slots_node, check_conflict_node, book_slot_node, handle_conflict_node, collect_info_node ) g = StateGraph(LabState) g.add_node("parse_intent", parse_intent_node) g.add_node("collect_info", collect_info_node) g.add_node("query_slots", query_slots_node) g.add_node("check_conflict", check_conflict_node) g.add_node("book_slot", book_slot_node) g.add_node("handle_conflict", handle_conflict_node) g.set_entry_point("parse_intent") g.add_conditional_edges( "parse_intent", lambda state: "collect_info" if not state.get("equipment_id") or not state.get("start_time") else "query_slots", {"collect_info": "collect_info", "query_slots": "query_slots"} ) g.add_conditional_edges( "check_conflict", lambda state: "book_slot" if state.get("available_slots") else "handle_conflict", {"book_slot": "book_slot", "handle_conflict": "handle_conflict"} ) g.add_edge("collect_info", "parse_intent") g.add_edge("query_slots", "check_conflict") g.add_edge("book_slot", END) g.add_edge("handle_conflict", END) app = g.compile()这套状态图跑起来之后,你拿中文自然语言请求去测,会发现整个对话流程非常清晰。LangGraph 最爽的地方在于:每个节点是普通 Python 函数,可以写任何业务逻辑,调试时只需要打印 state 就能看到每一步发生了什么。
4.3 用 LangGraph CLI 快速测试 Agent
LangGraph 提供了 LangGraph CLI 和 LangGraph Studio 可视化调试工具(网页版),可以在浏览器里直接对话测试 Agent,还能回放每一步的 state。安装和启动命令:
# 安装 CLI 工具 uv tool install langgraph-cli # 在项目根目录启动开发服务器(默认加载 langgraph.json 配置) langgraph dev如果你只有 API Key 没有本地模型,需要在.env里配置OPENAI_API_KEY(或者你用的其他兼容 API)。LangGraph 的StateGraph本身不绑定任何模型,你完全可以用任何 OpenAI 兼容接口,这在实际项目中非常实用,因为国内各家模型服务基本都提供 OpenAI 兼容的 SDK 接口。
5. 硬骨头:时间冲突检测和并发控制
5.1 时间重叠判断怎么写才不漏
时间冲突检测看着简单,写起来非常容易漏。两个时间段(a1, a2)和(b1, b2)重叠的完整条件不是“开始时间在对方范围内”那么简单,而是要判断四种情况:完全覆盖、部分重叠、被包含、完全相等。最保险的判断方法是用“不是不重叠”来反推:如果a2 <= b1或b2 <= a1,则两段时间不重叠;除此之外必重叠。
写成 SQL Alchemy 查询就是:
from sqlalchemy import and_, or_, select from app.models.booking import Booking stmt = select(Booking).where( Booking.equipment_id == equipment_id, Booking.status.in_(["confirmed", "pending"]), and_( Booking.start_time < new_end, Booking.end_time > new_start ) ) conflict = await session.execute(stmt).scalar_one_or_none()这个写法把四种重叠一网打尽,而且索引命中率高。如果你是直接用 SQLite 原生 SQL,同理。
5.2 SQLite 并发写问题与事务控制
SQLite 在低并发场景下非常香,零配置文件、单文件、好备份,但预约系统是个典型的多读多写场景,尤其是热门设备在周一早上九点的抢约高峰,多个请求同时写可能会报database is locked。解决方案有三个层次:
第一层:开启 WAL 模式,允许读写并发。执行PRAGMA journal_mode=WAL;,读操作不再阻塞写操作。第二层:设置busy_timeout,让连接等待锁释放而不是立即报错,比如设 3000ms。第三层:在写关键业务(比如预约提交)时,用事务的原子性保证不会出现脏写。
FastAPI 里我用 SQLAlchemy 异步引擎连接 SQLite,连接串加参数启用 WAL:
DATABASE_URL = "sqlite+aiosqlite:///./lab_reservation.db?check_same_thread=False" engine = create_async_engine( DATABASE_URL, connect_args={"timeout": 30}, )如果你预判未来并发量真的特别高(几十人同时抢),更稳妥的做法是把 SQLite 换成 PostgreSQL。但作为实验室内部系统,SQLite + WAL 扛住常规并发完全没问题,实测 30 人并发抢约也没再出现锁问题。
5.3 给 Agent 返回“智能”备选时段
冲突检测从来不是预约系统的全部,用户体验差异主要在冲突后的处理。普通系统说“对不起该时间段不可预约”,我这个系统会返回:“设备 A 在 2025-06-18 14:00-16:00 已被占用,当前最接近的空闲时段是 16:00-18:00 和次日 09:00-11:00,是否需要为您预订?”这个能力来自handle_conflict_node的逻辑:查到冲突后,按时间连续性对一天内所有可用时段进行排序,筛出与目标时间段起点差值最小的两个区间。
代码实现思路:查询目标日期内该设备所有已确认预约,得到“已被占用区间”列表,再拿整天时间线(比如 08:00-22:00)减去这些区间,剩余的就是空闲块,最后跟用户请求的时间段求最近差值排序。这样返回的选项不是随机的,而是真正“最近”的可用时间。
6. 填坑实录:我踩过的那些坑
6.1 pydantic v2 与 langchain-core 版本撕裂
这是目前 LangChain/LangGraph 初学者最容易遇到的坑。FastAPI 默认用 Pydantic v2,而早期版本的langchain-core用的是 Pydantic v1 的 API,升级到新版后虽然兼容了 v2,但如果你手动指定了旧版本的langchain-core或者混用了pydantic.v1和pydantic,经常会出现ValidationError或ImportError。我遇到的实际报错是:
ImportError: cannot import name 'ValidationError' from 'pydantic'排查了半天,最后发现是项目里同时存在两份 pydantic v1 和 v2,且 langchain 相关包引用了 pydantic 的别名。解决方案:不要手动锁langchain-core老版本,尽量让 uv 解析依赖关系,装最新版langgraph,它会自动带你所需的langchain-core。如果还有包依赖 pydantic v1,那就把整个虚拟环境删掉重建,避免历史依赖干扰。很多人在这一步浪费时间,实际上删.venv重建通常是最快的解法。
6.2 LangGraph 安装失败别慌,先看 Python 版本
LangGraph 要求 Python 3.9 以上,但在 3.9 上部分新版本特性表现一般,实测最稳的是 Python 3.11 和 3.12。如果你用 uv 创建项目时不小心用了系统默认的 Python 3.8,装langgraph大概率失败。解决办法是显式指定 Python 版本:
uv venv --python 3.11另外,Windows 上装langgraph时如果有 C 扩展编译错误(比如chromadb之类的伴随依赖),建议先装cmake或者直接使用 WSL 环境,能少很多折腾。这不是 LangGraph 本身的问题,是 Python 生态在 Windows 上常见的依赖编译问题。
6.3 FastAPI 初始化读取配置文件的正确姿势
这个热词点出了一个很容易被忽略的问题:FastAPI 项目多了之后,配置管理容易失控。我推荐的模式是前面提过的pydantic-settings+.env。但有一点坑:如果你用了docker-compose部署,.env文件的加载路径可能会漂移。保险做法是在启动命令里显式指定环境变量文件路径:
ENV_FILE_PATH=/path/to/.env uvicorn app.main:app --host 0.0.0.0 --port 8000在SettingsConfigDict里设置"env_file"的同时,也可以用os.environ.get("ENV_FILE_PATH")动态拼路径,这个方式在本地开发和 Docker 部署都能适配。另一个习惯是不要把密钥提交到 git 仓库,.env一定要加进.gitignore,不然一旦仓库公开,API Key 就泄露了。
6.4 前后端联调与线程问题
FastAPI 的async def路由天生跑在事件循环里,但如果你在 async 路由里直接调用requests.get这种同步阻塞库,整个请求会被卡住。预约系统里有一个需求是调用外部模型服务(LangGraph 节点里调 LLM),在 FastAPI 侧如果用同步客户端,高并发下会拖垮服务。我当时用httpx.AsyncClient替代requests,并在路由里await调用,响应时间在并发场景下稳定了很多。
如果你使用的 LangGraph 是同步的默认执行方式,可以考虑在节点函数里用asyncio.run()或者单独跑线程池,也可以直接用langgraph对异步节点的支持,在定义节点时用async def编写节点函数,状态图会以异步方式运行。这个细节在官方文档角落有提到,但不仔细看很容易忽略。
6.5 调试小技巧:打印 state 和 "中途回退" 的 AgentState
Agent 类系统调试起来比普通后端难,因为它跑在图上,问题可能出现在任意节点。我强烈建议在开发阶段不要一上来就写完整前端,而是先在 FastAPI 里加一个调试路由,往 LangGraph 的StateGraph传入初始消息,返回时打印每一步的 state 和中间产出:
@app.post("/api/agent/debug") async def debug_agent(payload: dict): initial_state = {"messages": [{"role": "user", "content": payload["message"]}]} result = await graph.ainvoke(initial_state) return resultgraph.ainvoke()是 LangGraph 里异步执行整张图的入口,它会按状态图跑完全部节点并返回最终 state。调试时直接把 state 打出来,看哪个节点没有正确写入字段,基本就能定位 90% 的问题。另一条经验是给每个节点加一段print(f"node: x, state: {state}"),虽然不优雅,但开发期极好用。线上再统一替换成 logging。
最后再分享一个小技巧
这个项目做完以后,我最大的体会是:FastAPI 和 LangGraph 的组合并不是什么高深莫测的新物种,它本质上是“把确定的业务逻辑用 FastAPI 写得干干净净,把不确定的语言交互用 LangGraph 写得灵活可控”。你在实际做的时候,也守住这个边界:Agent 负责理解,系统负责规则,不要指望大模型能替你保证数据一致性。
如果你也要上这类系统,建议第一版不要做太复杂,先跑通单设备、单用户、标准时间段,把图结构和接口跑顺,再逐步加多设备、审批流、模糊时间解析。按这个节奏推进,基本不会再踩我前面说的那些版本的坑。