用 LangGraph 构建可持久化的 AI 智能租房助手
当一个 AI 应用从"回答问题"走向"完成任务",难点就不再只是把提示词写得更好。它需要理解用户意图、查询外部数据、在关键步骤等待人工确认、记住跨会话信息,还要能够被调试、部署和持续迭代。
本文以一个智能租房助手为例,梳理如何用 LangGraph 把这些能力组织成一套可执行、可恢复、可扩展的工作流。这个思路同样适用于客服、订单处理、审批流、数据分析等有状态 Agent 场景。
先把需求翻译成工作流
租房助手通常包含四类请求:
- 房源推荐:根据城市、区域、预算、户型、朝向等条件筛选房源。
- 房源预定:收集必要信息,生成预定工单,并保存历史记录。
- 查询我的信息:读取用户过去的预算、偏好和预定信息。
- 常规问答:处理与业务无关的普通对话。
如果把这些能力全部塞进一个超长的 Agent 函数,代码很快会变成难以测试的条件分支。更稳妥的方式是先画出工作流,再把每个步骤实现为节点:
#mermaid-svg-oxGg3Mzw0CpTtd85{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-oxGg3Mzw0CpTtd85 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oxGg3Mzw0CpTtd85 .error-icon{fill:#552222;}#mermaid-svg-oxGg3Mzw0CpTtd85 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oxGg3Mzw0CpTtd85 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .marker.cross{stroke:#333333;}#mermaid-svg-oxGg3Mzw0CpTtd85 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oxGg3Mzw0CpTtd85 p{margin:0;}#mermaid-svg-oxGg3Mzw0CpTtd85 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .cluster-label text{fill:#333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .cluster-label span{color:#333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .cluster-label span p{background-color:transparent;}#mermaid-svg-oxGg3Mzw0CpTtd85 .label text,#mermaid-svg-oxGg3Mzw0CpTtd85 span{fill:#333;color:#333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .node rect,#mermaid-svg-oxGg3Mzw0CpTtd85 .node circle,#mermaid-svg-oxGg3Mzw0CpTtd85 .node ellipse,#mermaid-svg-oxGg3Mzw0CpTtd85 .node polygon,#mermaid-svg-oxGg3Mzw0CpTtd85 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oxGg3Mzw0CpTtd85 .rough-node .label text,#mermaid-svg-oxGg3Mzw0CpTtd85 .node .label text,#mermaid-svg-oxGg3Mzw0CpTtd85 .image-shape .label,#mermaid-svg-oxGg3Mzw0CpTtd85 .icon-shape .label{text-anchor:middle;}#mermaid-svg-oxGg3Mzw0CpTtd85 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oxGg3Mzw0CpTtd85 .rough-node .label,#mermaid-svg-oxGg3Mzw0CpTtd85 .node .label,#mermaid-svg-oxGg3Mzw0CpTtd85 .image-shape .label,#mermaid-svg-oxGg3Mzw0CpTtd85 .icon-shape .label{text-align:center;}#mermaid-svg-oxGg3Mzw0CpTtd85 .node.clickable{cursor:pointer;}#mermaid-svg-oxGg3Mzw0CpTtd85 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .arrowheadPath{fill:#333333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oxGg3Mzw0CpTtd85 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oxGg3Mzw0CpTtd85 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oxGg3Mzw0CpTtd85 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oxGg3Mzw0CpTtd85 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oxGg3Mzw0CpTtd85 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oxGg3Mzw0CpTtd85 .cluster text{fill:#333;}#mermaid-svg-oxGg3Mzw0CpTtd85 .cluster span{color:#333;}#mermaid-svg-oxGg3Mzw0CpTtd85 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-oxGg3Mzw0CpTtd85 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oxGg3Mzw0CpTtd85 rect.text{fill:none;stroke-width:0;}#mermaid-svg-oxGg3Mzw0CpTtd85 .icon-shape,#mermaid-svg-oxGg3Mzw0CpTtd85 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oxGg3Mzw0CpTtd85 .icon-shape p,#mermaid-svg-oxGg3Mzw0CpTtd85 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oxGg3Mzw0CpTtd85 .icon-shape .label rect,#mermaid-svg-oxGg3Mzw0CpTtd85 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oxGg3Mzw0CpTtd85 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oxGg3Mzw0CpTtd85 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oxGg3Mzw0CpTtd85 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 推荐房源
预定房源
查询信息
其它问题
需要
不需要
用户消息
读取用户记忆
识别意图
推荐子图
预定子图
个人信息子图
通用问答子图
是否继续预定
结束
主图负责路由和跨模块协作,子图负责相对独立的业务逻辑。这样做有三个直接好处:每个模块可以单独调试;复杂流程能够复用;未来新增业务时不必改动所有旧逻辑。
状态设计决定了系统能否长期运行
LangGraph 的核心不是"让模型多调用几次工具",而是让节点围绕共享状态协作。状态应当只保存跨步骤真正需要的数据,例如:
python
from langgraph.graph import MessagesState
class State(MessagesState):
user_intent: str
user_preferences: dict
推荐子图可以拥有更细的私有状态:
python
class RecommendState(MessagesState):
user_preferences: dict
city: str
district: str
budget_min: float
budget_max: float
room_type: str
orientation: str
room_count: int
others: str
设计状态时可以遵循两条原则:
- 只存原始数据,不把提示词模板、格式化文本和可推导结果塞进状态。
- 只有跨节点、跨重试或跨恢复需要的数据才进入状态。
例如,用户的城市和预算应当保留,供信息收集、SQL 查询和结果解释共同使用;而"给模型的最终提示词"可以在节点内部即时构造。
主图:先识别意图,再做智能分流
意图识别适合使用结构化输出,而不是让模型返回一段自由文本。把结果限制为有限枚举,路由逻辑会更稳定:
python
from typing import Literal
from pydantic import BaseModel, Field
class UserMessage(BaseModel):
type: Literal[
"recommend_house",
"reserve_house",
"get_info",
"others",
] = Field(description="用户请求的业务类型")
得到 user_intent 后,用条件边把流程送往对应子图。推荐流程结束后,再通过一个中断节点询问用户是否需要预定;用户确认后才进入预定流程。这个路由方式比让一个 Agent 自由决定所有动作更容易观测,也更容易限制风险。
推荐子图:让模型查询数据库,但不要让它直接"裸奔"
房源推荐的关键是把自然语言条件转换为可靠的 SQL。一个可控的查询链路可以拆成:
- 从当前消息和历史偏好中提取城市、预算等结构化条件。
- 信息不完整时,通过中断向用户追问。
- 列出数据库表,确认可用的数据范围。
- 获取相关表的 schema 和示例数据。
- 由模型生成查询 SQL。
- 执行前先检查 SQL,再调用数据库。
- 将查询结果交给模型整理为自然语言答案。
LangChain 的 SQLDatabaseToolkit 提供了列出表、获取 schema、执行查询和检查查询等工具。推荐子图可以用条件边形成一个小循环:模型生成查询后,如果产生了工具调用,就先进入 check_query;检查通过后再执行;执行结果返回模型,模型可以修正查询或给出最终答复。
这里有两个必须坚持的边界:
- 系统提示词明确禁止
INSERT、UPDATE、DELETE、DROP等 DML/DDL 操作。 - 数据库账号使用最小权限,推荐场景最好只授予只读权限。
这样即使模型生成了错误 SQL,也会在执行前多一道校验,降低数据泄露和误操作风险。
预定子图:用中断实现真正的人机协作
预定房源涉及电话、身份证和房源名称等信息,不能假设模型可以替用户完成确认。LangGraph 的 interrupt() 能让工作流在指定节点暂停,并把当前状态交给外部应用:
python
from langgraph.types import interrupt
def get_phone(state):
prompt = "请输入预定手机号"
while True:
phone = interrupt(prompt)
if phone and str(phone).isdigit():
return {"phone_number": str(phone)}
prompt = "手机号格式不正确,请重新输入"
恢复执行时,节点会从头重新运行,因此中断前的代码必须是幂等的。不要在中断前创建订单、扣款或发送不可逆通知;这些副作用应放在获得有效输入之后,并尽量使用唯一业务 ID 防止重复提交。
一个典型的预定流程是:收集房源名称 -> 收集手机号 -> 收集证件号 -> 组装消息 -> 调用生成工单工具 -> 保存预定记录。工具完成后,可以把订单号、房源标题和联系方式写入长期存储,供后续"查询我的信息"使用。
两层记忆:线程状态和跨会话存储
智能助手需要区分两种记忆:
- 线程级状态:保存一次会话中的消息和中间结果,支持暂停、恢复和故障重启。
- 跨会话存储:保存用户偏好、历史预定等长期信息,让用户下次打开对话时仍然可以被识别。
跨会话数据适合按用户 ID 划分命名空间:
python
namespace = (user_id, "preferences")
result = store.search(namespace)
store.put(namespace, record_id, {
"budget_min": 1000,
"budget_max": 3000,
})
用户 ID 等运行时信息不应硬编码到图中,可以通过运行时上下文传入:
python
class ContextSchema(TypedDict):
user_id: str
def node(state, runtime, *, store):
user_id = runtime.context["user_id"]
开发阶段可以使用内存存储,生产环境则应切换到 PostgreSQL 等持久化后端,并为检查点和长期记忆设置清理策略。
流式输出让过程可见
长流程如果只在最后返回一个结果,用户会误以为系统没有响应。LangGraph 支持多种流模式:
values:每一步输出完整状态。updates:只输出状态变化。messages:实时输出模型生成的 token。custom:输出自定义进度、日志或业务事件。debug/events:开发和迁移阶段查看更完整的执行信息。
实际应用中可以组合多种模式,例如同时发送节点状态和模型 token。通过 SSE 返回时,每个事件通常包含事件类型、数据负载和事件 ID,前端可以据此分别更新聊天区、进度条和调试面板。
从本地项目到可部署服务
一个可部署的 LangGraph 项目至少需要四部分:图代码、langgraph.json、项目依赖配置,以及可选的环境变量文件。推荐的目录结构如下:
text
my-app/
├── .env
├── langgraph.json
├── pyproject.toml
└── src/
└── agent/
├── graph.py
├── recommend.py
├── reserve.py
└── extend.py
常用的 CLI 工作流是:
bash
pip install -U "langgraph-cli[inmem]"
langgraph dev # 本地开发和调试
langgraph build # 构建镜像
langgraph dockerfile Dockerfile
langgraph up # 通过 Docker 启动
本地开发时可以使用 Studio 查看图结构、运行线程和状态变化。部署到独立服务器时,需要准备 PostgreSQL 保存检查点和配置,Redis 负责任务队列及实时事件流;生产环境可以进一步放到 Docker 或 Kubernetes 中运行。
部署选择通常有三种:完全托管的云部署、带控制平面的混合部署,以及直接运行 Agent Server 的独立部署。选择哪一种取决于团队对基础设施、数据合规、扩缩容和运维成本的要求。
需要提前处理的工程问题
实践中,以下问题值得在上线前单独验证:
- 依赖版本要锁定。CLI、API Server、检查点后端之间存在版本耦合,升级前应在隔离环境回归测试。
- 数据库要隔离。迁移阶段不要复用结构不兼容的旧库,尤其要确认检查点表和线程表的主键类型一致。
- 状态要控制大小。长时间对话需要定期裁剪历史消息或生成摘要,避免上下文无限增长。
- 中断顺序要稳定。同一个节点每次恢复时,中断调用的数量和顺序必须一致。
- 所有外部副作用都要可追踪。订单、支付、通知等动作应记录幂等键、执行结果和重试次数。
- SQL 和用户隐私要分开治理。日志中不要输出身份证号、完整手机号和数据库密码,敏感字段应脱敏或加密。
结语
一个真正可用的 AI 助手,既需要模型的理解能力,也需要工作流框架提供状态管理、路由、持久化、中断和部署能力。LangGraph 的价值正在于把这些能力放进一张可观察的图里:主图负责分流,子图负责业务,状态负责协作,检查点负责恢复,存储负责记忆,中断负责把人重新带回流程。
从一个租房助手开始,你可以继续扩展消息摘要、结构化字段与数据库列的精确映射、SQL 人工审核、自定义进度流,以及更多需要审批和长期记忆的业务流程。这样构建出来的 Agent,才更接近一个可以被测试、被维护、被部署的应用系统。