《零基础 AI 入门》第 6 篇|作者:梯度下降的咖啡师 | 8 年 Java 后端(京东、字节)+ 2 年 AI 应用开发
前五篇写完,你手上有了一个手写的 Agent:while 循环里调模型、执行工具、压缩记忆,一百来行代码,改个 key 就能跑。上一篇我们给它做了生产体检,列了三个坑——持久化、多用户隔离、工具权限,还说了这些 LangGraph 都有现成答案。
今天就干一件事:把前面这一百行重写成 LangGraph 版。但不是推翻重来——你的工具函数一个字不改,你的 system prompt 不改,你对 Agent 循环的理解不改。只是把 while True 换成图,把全局 messages 换成带 thread_id 的状态。
01 你的 while 循环,其实就是一张图
回顾一下第三篇你写的 Agent,核心就是这段:
| |
你仔细看这段代码:先调模型,然后判断有没有 tool_calls——有就执行工具再循环回去,没有就退出。这其实就是一个有循环、有分支的流程图。你手写的时候已经在画流程图了,只是用 while 和 if 表达的。
LangGraph 做的事,就是把这个流程图变成一等公民:你不再手写 while 和 if,而是声明"这里有个节点叫 agent,这里有个节点叫 tools,agent 执行完看情况决定下一步去哪"。
02 三个概念,没有第四个
别被 LangGraph 的文档吓到,入门只需要三个概念:
State:就是你的 messages 列表。你用 TypedDict 声明一下这个状态里有什么字段。和你之前的全局 messages 本质一样,只是现在框架帮你管它怎么更新。
Node:就是你的函数。接收当前 State,返回 State 要更新的部分。你的 agent_node 就是调模型,你的 tools_node 就是执行工具——和你之前写的函数一模一样。
Edge:就是你 while 循环里的 if 判断。声明"执行完这个节点后,下一步去哪个节点"。有固定边(无条件跳转)和条件边(根据 State 内容决定去哪)。
就这三个。没有 Chain、没有 Runnable、没有 LCEL。你在前面四篇手写的东西,全对应得上。

03 完整代码,一百行不到
先装依赖:
| |
这里 LLM 客户端从之前的 OpenAI SDK 换成了 ChatOpenAI——别紧张,它就是个发 HTTP 请求的客户端,和你之前用 client.chat.completions.create 本质一样。区别只是它自动处理消息格式转换,省得你自己在 dict 和模型消息对象之间来回转。工具函数和 schema 直接从第三篇复制,一个字不改:
| |
注意最后两行:同一个 thread_id 调了两次 invoke。第二次问"那空气质量呢",模型知道"那"指的是北京——因为 checkpointer 自动把第一次的 messages 存下来了,第二次 invoke 时按 thread_id 自动加载。你不需要自己 json.load、不需要自己用 dict 按 thread_id 存。
04 手写版 vs LangGraph 版,对照一下
把你手写时自己管的事,和 LangGraph 替你做的事拉个表:
| 你手写时自己管的事 | LangGraph 替你做了什么 |
|---|---|
| while True 循环 | 图的边自动流转,tools 节点执行完回 agent |
| if not tool_calls: break | 条件边 should_continue 返回 END 就停 |
| messages.append(新消息) | add_messages reducer 自动追加,不用手动 append |
| for tc in tool_calls: dispatch | tools_node 里照样手写 dispatch,这部分没替你省 |
| messages 存内存 dict,重启就丢 | checkpointer 自动存快照,传 thread_id 自动加载 |
| 多用户按 thread_id 手动隔离 | 框架按 config 里的 thread_id 自动存取状态 |
看出来了吗?LangGraph 没有魔法。它只是把你手写时"本来就该这么做"的事,变成了框架的默认行为。你手写时最容易漏的边角情况——tool_call 没有对应 tool result 怎么办、不同 thread 的状态怎么隔离——框架替你兜住了。
有一点它没替你省:工具 dispatch 那段(for tc in tool_calls 然后调函数),还是你自己写的。因为工具逻辑是你自己的业务,框架不知道你要调什么函数。但 tc.args 已经是解析好的 dict 了——不用你自己 json.loads(tc.function.arguments)。这个 JSON 字符串解析和校验的活儿,如果你调试时需要看工具参数长什么样,可以丢到 ForJSON(forjson.com)里格式化一下,比在终端 print 清楚。
05 你现在还缺什么
跑通这段代码,你的 Agent 已经有了:图结构、自动状态管理、thread_id 隔离、内存级记忆。但上一篇体检说的三件事,你还只解决了两件半:
持久化:现在用的是 InMemorySaver,进程退了还是丢。下一篇换成 SqliteSaver,代码只改一行——checkpointer = SqliteSaver.from_conn_string(“session.db”)。
多用户隔离:thread_id 已经能用了,但你还没真把它包成 Web 服务试过并发。这个等部署篇再说。
工具权限:现在工具还是全暴露给模型,高风险操作没有人工审批。下一篇加 Interrupt——在 tools_node 之前设个断点,要发邮件、删数据之前暂停等人确认。这是 LangGraph 另一个核心能力。
06 写在最后
从第一篇"什么是 Function Calling"到现在,你手写了一个完整的 Agent,又用 LangGraph 把它重写了一遍。你不是在学一个框架 API,你是在看一个框架的每个设计决策,怎么对应你手写时踩过的坑。
这就是为什么我一直说别上来就用 LangChain——你没手写过,直接看 StateGraph、add_messages、reducer 这些词会觉得莫名其妙。但你手写了五篇之后再看,会发现它们每个都在解决你刚经历过的问题。
下一篇,给这个 LangGraph 版 Agent 加人工审批:遇到高风险操作自动暂停,等人点确认再继续。顺便把内存 checkpointer 换成 SQLite,真正做到重启不丢。
旧炉未冷,新火已燃。关注公众号「梯度下降的咖啡师」,或访问 gradcoffee.com。下一篇给 Agent 装个人工刹车,看看 Interrupt 到底怎么用。
聊聊两个问题:
① 你手写 while 循环时,最想吐槽 LangGraph 哪点——还是觉得它省得不够多?
② 你现在的 Agent 有几个工具?换图结构的时候哪个部分最绕?
咖啡续命,梯度下降。| 作者:梯度下降的咖啡师
