返回博客

用 LangGraph 六节点状态机做企业问答:我的四个架构决策

不是模型封装,是编排中间件。为什么强制用状态机而不是链式调用、为什么禁止用纯字典传参、为什么服务不可达要立刻中止启动,以及这个项目还没做完的部分。

2026/10/02 约 5 分钟
目录

这是我技术含量最高的项目,也是我唯一一个敢在面试里说「你随便问」的项目。

它的定位不是「一个能聊企业文档的机器人」,而是面向企业知识库的确定性 AI 编排中间件 —— 用工程化手段解决大模型在垂直领域落地的三个真问题:事实性幻觉、知识时效性、服务成本不可控。

技术栈:FastAPI 0.115 + LangGraph 0.2.57 + LangChain 0.3.x + Ollama + FAISS + Redis。

先看整体:六节点状态机

用户请求 → 追踪标识中间件 → /api/v1/chat
                              ↓
                    LangGraph 状态机
   ┌─────────────────────────────────────────────┐
   │ classify       意图识别                       │
   │   ├─ 敏感 / 超范围 → 结束(拒答)              │
   │   ├─ 闲聊       → 直接生成                    │
   │   └─ 事实性 / 推理类 ↓                        │
   │ rewrite_query  查询重写                       │
   │ retrieve       向量检索                       │
   │ route_model    动态模型路由                   │
   │ generate       溯源生成(+ 联网)              │
   │ validate       置信度评估                     │
   └─────────────────────────────────────────────┘

画出来就这么点东西,但每个节点该不该存在、怎么接、失败怎么办,才是真正花时间的地方。下面是四个我觉得值得讲的决策。

决策一:强制状态机,禁止顺序链

规则:流程控制必须用 LangGraph,禁止用 LangChain 旧版的顺序链(Chain)。

原因不是「新东西更好」,是业务形态根本不是线性的。

  • 检索回来的内容质量不行,得回到查询重写节点重来一遍 —— 这是循环。
  • 云端模型超时,得切到本地模型重试 —— 这是分支和回滚。
  • 置信度评估不过关,得换个路由策略再生成一次 —— 这是条件边。

顺序链表达不了这些。硬要用链来写,最后会变成一堆 if-else 套在回调函数里,调试的时候连「现在跑到哪一步了」都说不清。

状态机的好处是:每一步的状态变更都是显式的、可回放的。出了问题,我能拿到那次请求的状态快照,看到底在哪个节点拐错了弯。

另外给自己定了两条纪律:所有节点遵循输入不可变和副作用隔离。这两个约束在单节点看起来是啰嗦,到了要加重试和并发的时候,会救你的命。

决策二:禁止用纯字典传参

规则:全局上下文用 Pydantic 严格类型校验,禁止用纯字典(dict)传递参数。

这条是我被自己坑过之后加的。

早期图省事,节点之间传 dict。写到第五个节点的时候,我已经记不清 state["result"] 到底是字符串还是带元信息的对象了,只能往上翻代码。改一个字段名,要在六个文件里搜一遍字符串,还不敢确定搜全了。

换成 Pydantic 之后:

  • IDE 能自动提示,写错字段名当场报红
  • 类型不对在入口就被拦下,不会流到第三个节点才炸
  • 看一眼 schema 定义就知道整个流程在传什么,代码自文档化

代价是多写几十行模型定义。收益是这套东西我隔了两周回来还能改得动。

决策三:快速失败,而不是带病运行

规则:启动阶段做依赖健康检查,服务不可达或密钥认证失败,立即中止启动。

这条反直觉。大部分人会想「先起来,坏的那个功能降级就好」。

但生产环境里,「带病启动」是最难排查的故障来源 —— 服务起来了,日志里飘过一行警告,然后某个接口一直返回质量很差的结果,你要查三天才想到是启动时某个依赖没连上。

不如启动就死给你看。容器起不来,监控系统立刻告警,问题在一分钟内暴露。

配套的是另一条:运行时要优雅降级。云端依赖超时或返回 5xx,毫秒级切到本地轻量模型或兜底语料库,SLA 不低于 99.5%。

启动严格、运行宽容 —— 这两条不矛盾,针对的是不同阶段的不同目标。

决策四:错误响应遵循 RFC 7807

听起来是最无聊的一条,但它是「这是个工程还是个 demo」的分界线。

统一错误抽象之后,调用方拿到的一定是同样结构的问题详情:类型、标题、状态码、详情、以及这次请求的追踪标识。前端不用为每个接口写一套错误处理,排查问题的时候用户把追踪标识发过来,我就能拉出完整链路。

顺带说安全性:云端密钥一律环境变量注入,不进代码;用户输入做注入攻击防护和敏感词过滤。这两条没什么好展开的,但不做的后果很直接。

可观测性:每次请求一个追踪标识

请求进来的第一站就是中间件,生成全局唯一的 trace id,然后:

  • 记录每个节点的耗时
  • 记录令牌消耗
  • 记录状态变更快照
  • 集成 LangSmith 做可视化

有了这个,「回答质量变差了」就不再是玄学 —— 我能看出是检索召回率掉了,还是路由选错了模型,还是重写后的查询跑偏了。

还没做完的部分

这部分我一定会主动说,因为面试官问到的时候,装作没有会更难看。

优先级改进项状态
P0-1交叉编码器重排序(Rerank)未做
P0-2BM25 + 向量混合检索(召回率 65% → 82%+)未做
P0-3Ragas 评估体系(忠实度 / 答案相关性)未做
P1-1LoRA 微调管线未做
P2-1Self-RAG未做
P2-2Docker 完整部署未做

这是我目前最大的求职风险:简历上如果写「做过生产级 RAG 系统」,面试官第一个追问就是重排序和评估体系。这两块我确实还没做,补起来各要 2-3 天,性价比最高,排在最前面。

面试官真正会看的三条

后来我反过来想,评判一个 RAG 项目合不合格,其实就三件事:

  1. 能不能量化系统质量 —— 有评估指标,而不是「感觉还行」
  2. 能不能优雅降级 —— 依赖挂了不返回空值
  3. 能不能溯源 —— 每个回答能指出出自哪份文档的哪一段

第 2、3 条我做到了,第 1 条还没有。这个缺口我自己知道,也在补。

写在最后

这个项目给我最大的收获,不是学会了 LangGraph 的 API。

是意识到一件事:大模型应用做到后面,拼的不是提示词,是软件工程。

状态机、类型契约、错误处理、可观测性、优雅降级 —— 全是老掉牙的东西。但正是这些老掉牙的东西,决定了一个 Agent 系统是能上线,还是只能录个 demo 视频。

评论

评论区还没启用。在 src/consts.ts 里填好 Giscus 配置并把 enabled 改成 true 就能开启。