LangGraph 完结篇:手搓一个自动查单发货的工单 Agent
生命不息,折腾不止。前四课攒下的三件套(工具手、落盘、审批闸门),今天一把焊成一个真的能干活的工单 Agent。
LangGraph 实战这个系列写到今天,总算可以收尾了。前面四课我们分别攒了三样硬货:第 2 课学会用 interrupt() 给 Agent 装人工审批闸门,第 3 课用 SqliteSaver 让 Agent 能落盘「倒带重来」,第 4 课用 @tool + ToolNode 给 Agent 装了会干活的「手」。
但光有零件不算本事,把它们串起来才叫 Agent。今天我们就手搓一个客服工单 Agent:用户报个订单号,它自动查单、算优惠、起草发货单,然后——重点来了——卡在发货前等人点头,人回一句「发货」才真正往下走。全程断了电、关了机,隔天开回来还能接着上次的进度继续跑。这就是把三节课焊成一个完整闭环。
一、先盘一遍手里的三件套
动手之前,把三个零件各归各位,写代码的时候心里才有数:
- 工具手(第 4 课):
@tool装饰器把普通 Python 函数变成模型能调的工具,bind_tools把工具「塞给」模型,ToolNode负责真的去执行工具并回传结果。 - 落盘倒带(第 3 课):
SqliteSaver把每一步的状态存进 SQLite 数据库,thread_id是「这一单」的唯一标识,随时能停、随时能续。 - 人工闸门(第 2 课):
interrupt()在节点里暂停整张图,把要审批的内容甩给人;人回个结果,用Command(resume=...)让它接着往下跑。
串起来的流程张这样:大脑节点(调工具)→ 工具节点(执行)→ 大脑节点(继续想)→ … → 走路到审批节点(卡住等人)→ 人身点头 → 收工。前四课的活儿,全在这一条链子里。
二、先把工单 Agent 的骨架想清楚
写代码前,先想清楚这 Agent 得管哪些事、状态长啥样。我们做的是「自动查单 + 算优惠 + 人工确认发货」三步。状态用一个 TypedDict 装下整单信息:
1 | import sqlite3 |
messages 用 add_messages 这个 reducer 是 LangGraph 的规矩:模型每轮说的话往上追加,而不是覆盖。这是第 4 课就讲过的基础,今天照用。
接着定义两个工具。教程里查单、算优惠用模拟数据顶一下,生产环境你把 fake_db 换成真库查询就行,工具的口子是完全一样的:
1 |
|
工具函数的 docstring 别偷懒——模型就是靠它来理解「这工具是干嘛的、参数传什么」。写得越清楚,模型调得越准。
三、把整张图拼起来
零件齐了,上模型。这种中文客服场景,DeepSeek 便宜又够用,是本篇首选;我直接走中转站 ai.aklibk.com(一个 key 通吃 DeepSeek / Claude / GPT 多家模型,按量付费,比每家单独开账号省心):
1 | llm = ChatOpenAI( |
然后定义三个节点——大脑、手、闸门:
1 | def agent(state: OrderState): |
最后拼图 + 落盘,compile(checkpointer=...) 这一步是关键——不挂 checkpointer,interrupt() 直接报错:
1 | graph = StateGraph(OrderState) |
整条链你看一眼就懂:大脑想 → 有工具要调就去「手」那儿执行 → 回来继续想 → 直到想完出结论,走进审批闸门 → 等人点头 → 结束。一个标准的 ReAct「想—做—再想」循环,末尾接了一道人肉闸门。
四、跑一遍完整流程
开工。第一段调用,Agent 会自动查单、算优惠,走到发货前主动停下等人:
1 | config = {"configurable": {"thread_id": "order-1001"}} |
看到没,它自己把订单查了、折扣算了,最后把「是否发货」这件事交还给人,而不是闷头就发货。这就是人工闸门的价值——不可逆的动作,永远别让它自己拍板。
人回一句「approve」,接着跑完:
1 | final = app.invoke(Command(resume="approve"), config=config) |
流程跑通了。再加一层好处:因为挂了 SqliteSaver 落盘,你可以现在就关电脑,明天用同一个 thread_id 再 invoke 一次,它照样记得这单卡在哪儿。断点续跑,免费白送。
五、几个最容易踩的坑
收尾前把血泪坑再钉一遍,省得你回头翻:
interrupt()必须配 checkpointer +thread_id。少了任何一个,不是报错就是新建了个空线程,根本恢复不了。Command(resume=...)是唯一该当invoke输入的命令。别手滑传成Command(update=...)或普通 dict,否则图看着像「卡死」了。- 节点恢复时是从头重跑。所以发邮件、扣款这类副作用,一定要写在
interrupt()之后,别写前面,否则每次恢复都会重复执行。 thread_id用业务 ID(订单号、工单号),别用随机 uuid。出了事你要靠它在数据库里扒这条线程的 checkpoint,业务 ID 一眼定位。
本篇把查单、算优惠用模拟数据顶上了,是个能跑通的闭环骨架;你接上真库、真支付,它就是一个小型生产级客服工单 Agent。LangGraph 实战系列到这儿收尾,前五课连起来看,你已经能把「工具调用 + 状态持久化 + 人工审批」这三块硬骨头啃下来了。
生命不息,折腾不止。LangGraph 系列收官,下一篇开个新坑——DeepSeek Harness 进阶玩法系列第 1 篇:手写一个 dsh 插件,搞懂「一切皆插件」的 Cordis 架构,教你给这个国产 Agent 框架装上一个自定义 provider,接进任何 OpenAI 兼容接口。