LangGraph智能租房助手实践

用 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

设计状态时可以遵循两条原则:

  1. 只存原始数据,不把提示词模板、格式化文本和可推导结果塞进状态。
  2. 只有跨节点、跨重试或跨恢复需要的数据才进入状态。

例如,用户的城市和预算应当保留,供信息收集、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。一个可控的查询链路可以拆成:

  1. 从当前消息和历史偏好中提取城市、预算等结构化条件。
  2. 信息不完整时,通过中断向用户追问。
  3. 列出数据库表,确认可用的数据范围。
  4. 获取相关表的 schema 和示例数据。
  5. 由模型生成查询 SQL。
  6. 执行前先检查 SQL,再调用数据库。
  7. 将查询结果交给模型整理为自然语言答案。

LangChain 的 SQLDatabaseToolkit 提供了列出表、获取 schema、执行查询和检查查询等工具。推荐子图可以用条件边形成一个小循环:模型生成查询后,如果产生了工具调用,就先进入 check_query;检查通过后再执行;执行结果返回模型,模型可以修正查询或给出最终答复。

这里有两个必须坚持的边界:

  • 系统提示词明确禁止 INSERTUPDATEDELETEDROP 等 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 的独立部署。选择哪一种取决于团队对基础设施、数据合规、扩缩容和运维成本的要求。

需要提前处理的工程问题

实践中,以下问题值得在上线前单独验证:

  1. 依赖版本要锁定。CLI、API Server、检查点后端之间存在版本耦合,升级前应在隔离环境回归测试。
  2. 数据库要隔离。迁移阶段不要复用结构不兼容的旧库,尤其要确认检查点表和线程表的主键类型一致。
  3. 状态要控制大小。长时间对话需要定期裁剪历史消息或生成摘要,避免上下文无限增长。
  4. 中断顺序要稳定。同一个节点每次恢复时,中断调用的数量和顺序必须一致。
  5. 所有外部副作用都要可追踪。订单、支付、通知等动作应记录幂等键、执行结果和重试次数。
  6. SQL 和用户隐私要分开治理。日志中不要输出身份证号、完整手机号和数据库密码,敏感字段应脱敏或加密。

结语

一个真正可用的 AI 助手,既需要模型的理解能力,也需要工作流框架提供状态管理、路由、持久化、中断和部署能力。LangGraph 的价值正在于把这些能力放进一张可观察的图里:主图负责分流,子图负责业务,状态负责协作,检查点负责恢复,存储负责记忆,中断负责把人重新带回流程。

从一个租房助手开始,你可以继续扩展消息摘要、结构化字段与数据库列的精确映射、SQL 人工审核、自定义进度流,以及更多需要审批和长期记忆的业务流程。这样构建出来的 Agent,才更接近一个可以被测试、被维护、被部署的应用系统。

相关推荐
zqrgkjyxgs2 小时前
GEO垂直行业实战:医疗、制造、教育、金融的分行业差异化优化策略
大数据·人工智能·搜索引擎
winrisef2 小时前
ChatGPT/Codex最新版本出错了,无法进入解决方案
人工智能·语言模型·chatgpt·codex
rain_sxr2 小时前
部分 JSON 的增量解析:Function Calling 流式输出的前端执行策略
人工智能
OpenApi.cc2 小时前
Mocode 开发文档平台
人工智能·深度学习·目标检测·自然语言处理·语音识别
Promise微笑2 小时前
电力电缆故障分类与精准定位:物理机制、诊断挑战及前沿技术
人工智能·分类·数据挖掘
GrepowTattu2 小时前
智能护膝与外骨骼设备为什么需要异形定制电池?
人工智能·智能穿戴
北京晶数信息科技2 小时前
加油站成品油智慧监管系统项目建设实施汇报(一)
大数据·物联网·产品经理·需求分析
夜影风2 小时前
我国AI智能体产业发展洞察:为何能在AI应用层实现“换道超车“
大数据·人工智能
码银2 小时前
放弃了豆包,我使用Python做了一个桌面宠物
python·microsoft·宠物