C3-13 开发 Agent —— 基于 LangGraph 开发框架
💡 回忆:上一节课我们手撕了一个 ReAct 循环 —— 用 while True + if else 让 LLM 决定要不要调用工具。代码能跑,但维护起来像在搬砖,每加一个工具就要改一遍主循环。
这节课我们用 LangGraph 把这套流程"画成图",结构化、可视化、可扩展。
开发一个 Agent 需要以下 4 步:
第一步:定义 Agent 名称、功能
第二步:定义 Agent 需要的工具、技能
第三步:设计循环 / 多 Agent 协作
第四步:调试
本节课的剩余章节会按这 4 步推进,每一步对应 LangGraph 的一个核心概念或一段代码。先看 LangGraph 是什么。
一、LangGraph 是什么
1.1 一句话定义
LangGraph 是一个 Python 库,让你把 Agent 的工作流程画成一张"有向图":节点(Node)做事,边(Edge)决定下一步去哪,状态(State)在所有节点之间共享。
1.2 它在 LangChain 大家族的位置
1 2 3 4 5 6 7 | |
学习顺序:先吃透 LangGraph(理解底层),再学 Deep Agents(学会偷懒)。
1.3 LangGraph 的 4 个核心概念
后面会用具体的代码示例分别演示,这里先列清单:
- State:节点之间共享的数据结构。常见是 TypedDict,消息字段用
Annotated[list, add_messages]实现"自动追加"。 - Node:一个普通 Python 函数,输入 State,输出 State 的子集。
- Edge:连接节点。分两种:
add_edge(固定路由)和add_conditional_edges(根据状态动态决定下一个节点)。 - Command:LangGraph v0.2+ 的 API。一个节点可以同时返回"跳转到哪个节点"和"更新什么状态"。
1.4 LangGraph 的 5 大特性
| 特性 | 具体能力 |
|---|---|
| Stateful 持久化 | 支持 Checkpoint,Agent 崩溃后可从断点恢复 |
| Human-in-the-loop | 任意节点可暂停,让人审批后再继续 |
| Streaming | 实时把中间状态 / Token 流式输出给前端 |
| Memory | 短期记忆(State)+ 长期记忆(Store) |
| Debugging | 图结构可视化、LangSmith 全链路追踪 |
1.5 LangGraph 与手写 ReAct 循环的对比
| 维度 | 手写 ReAct | LangGraph |
|---|---|---|
| 流程变更 | 改 if-else,容易动一处坏一处 | 改图,结构一目了然 |
| 分支 & 并行 | 写起来很丑 | 天生支持 |
| 状态追踪 | 手动维护一个 dict | State 自动管理 |
| 可观测性 | 靠 print | 可视化 + LangSmith |
| 长任务 | 容易爆上下文 | 原生支持 Checkpoint |
二、第一步:定义 Agent 名称、功能
2.1 什么是"定义 Agent"
在写代码前,先用一句话回答清楚 3 个问题:
- 这个 Agent 叫什么?
- 它做什么(输入 / 输出是什么)?
- 它的边界在哪里(什么不做)?
这一句话决定了你后面所有节点、工具、状态的设计。
2.2 具体例子:3 种典型 Agent
例子 A:单工具计算器 Agent
| 字段 | 定义 |
|---|---|
| 名称 | CalculatorAgent |
| 输入 | 用户自然语言,如 "3 加 5 等于多少?""(25+17)*3" |
| 输出 | 计算结果(数字) |
| 职责 | 把自然语言转成算式并计算 |
| 不做 | 不查天气、不联网、不写文章 |
例子 B:奶茶店服务员 Agent
| 字段 | 定义 |
|---|---|
| 名称 | MilkTeaAgent |
| 输入 | 顾客问句(菜单咨询 / 推荐 / 价格 / 优惠) |
| 输出 | 结构化答复(产品名、价格、推荐理由) |
| 职责 | 回答奶茶相关的所有问题 |
| 不做 | 不处理退款投诉(交给其他 Agent) |
例子 C:研究助手 Agent
| 字段 | 定义 |
|---|---|
| 名称 | ResearchAgent |
| 输入 | 一个研究问题,如 "LangGraph 在企业 AI Agent 中的应用价值" |
| 输出 | 一篇结构化报告 |
| 职责 | 联网检索 → 整理资料 → 撰写报告 |
| 不做 | 不执行代码(除非另有 code 工具) |
2.3 Agent 定义如何映射到 LangGraph 代码
把 Agent 名字、功能、边界想清楚后,对应到 LangGraph:
| Agent 设计 | LangGraph 落点 |
|---|---|
| 名称 | 命名空间 / 日志前缀 / LangSmith 项目名 |
| 输入 | State 里的 input 字段(通常是 messages[0]) |
| 输出 | State 里的 output 字段 |
| 职责 | 节点的 system_prompt(告诉 LLM 该做什么) |
| 不做 | system_prompt 里写清楚边界 + 不绑相关工具 |
2.4 示例:定义 ResearchAgent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
要点:docstring 不是装饰用的,是给 LLM 看的"说明书"。写得越清楚,LLM 用得越准。
例子 2:数学计算工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
要点:工具有副作用风险时(执行代码、调 API、写文件)要做安全校验。
例子 3:时间查询工具
1 2 3 4 5 6 | |
3.4 把工具"告诉" LLM
用 bind_tools 把工具列表绑给 LLM。这一步之后,LLM 在回复时就能输出 tool_calls。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
3.5 工具设计的 4 条原则
| 原则 | 原因 |
|---|---|
| 一个工具只做一件事 | LLM 才能精确选择 |
| docstring 写清楚何时该用 / 不该用 | LLM 选错工具的常见原因是 docstring 不清 |
| 参数尽量少 | 参数越多 LLM 越容易填错 |
| 返回值用字符串(JSON 更佳) | LLM 处理字符串最稳 |
3.6 反例:写一个"坏"工具
1 2 3 4 5 6 7 8 9 10 | |
这种"瑞士军刀"工具的坏处:LLM 不知道 action 该传什么值,docstring 一句话讲不清能力,扩展性为零。拆成 3 个独立工具才对。
四、第三步:设计循环 / 多 Agent 协作
这一步是 LangGraph 最核心的部分。我们分两种模式讲:
- 单 Agent 循环(条件边实现 ReAct)
- 多 Agent 协作(Supervisor 模式)
4.1 模式 A:单 Agent ReAct 循环
4.1.1 流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
4.1.2 完整代码
代码位置:L4-agent/single_agent.py
4.1.3 实测运行
用户问:"北京天气怎么样?顺便帮我算一下 (25+17)*3 等于多少?"
1 2 3 4 5 6 7 8 | |
注意 LLM 一次性发出 2 个 tool_calls,ToolNode 自动并行执行。
4.1.4 单 Agent 循环的 5 个关键点
- MessagesState 自带 add_messages reducer:新消息自动追加,不覆盖。
- ToolNode 是预置节点:不用手写工具调度循环。
- 条件边 should_continue 是 ReAct 的灵魂:LLM 选工具就执行,不选就结束。
- bind_tools 把工具的 schema 告诉 LLM:LLM 才能输出 tool_calls。
- 整个循环由图驱动:不需要写 while True。
4.1.5 单 Agent 适合什么场景
- 工具数量 ≤ 5 个
- 任务流程是"LLM 决定 → 执行 → LLM 再决定"的简单循环
- 不需要把任务拆给多个专家
一旦工具数量超过 5-7 个,或者任务明显需要"研究 → 写 → 评审"这种分工,就要考虑多 Agent。
4.2 模式 B:Supervisor 多 Agent 协作
4.2.1 为什么需要 Supervisor
单 Agent 遇到复杂任务时的问题:
- 工具太多 → LLM 选不准
- 任务太杂 → 一个 system_prompt 装不下所有职责
- 想要"研究 → 写作 → 评审"这种流水线 → 单 Agent 不会自动按顺序做
解决方案:招一个 Supervisor 当调度者,把任务拆给多个专家 Worker。
4.2.2 角色定义(第一步:定义 Agent 们)
| 角色 | 职责 | 能调的工具 |
|---|---|---|
| Supervisor | 读用户请求,决定派给谁 | transfer_to_researcher / transfer_to_writer / transfer_to_reviewer / finish |
| Researcher | 联网搜索资料 | web_search |
| Writer | 基于资料写文章 | summarize |
| Reviewer | 评审字数 / 内容 | count_words |
4.2.3 流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
4.2.4 Supervisor 节点代码
代码位置:L4-agent/supervisor_multi_agent.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | |
4.2.5 Worker 节点代码
每个 Worker 内部都是一个独立的 ReAct 子图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
4.2.6 实测运行片段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
4.2.7 Supervisor 模式的 5 个关键点
- 把"派活"建模为工具调用:不要写 if-else,让 LLM 自己决定调哪个 transfer_to_xxx。
- Command(goto, update) 同时完成跳转和状态更新。
- Worker 是嵌套子图:每个 Worker 内部是完整的 ReAct 循环。
- ToolMessage 必须紧跟 AIMessage(tool_calls):OpenAI API 的硬性要求。
- Worker 启动时构造新 HumanMessage:别把 Supervisor 的派活 AIMessage 喂给 Worker LLM。
4.2.8 Supervisor vs 硬编码 if-else
| 硬编码调度 | Supervisor 模式 |
|---|---|
if state.next == "researcher": ...<br/>``elif state.next == "writer": ... |
if LLM 调 transfer_to_X: goto "X" |
| 流程写死,加 worker 要改 if 链 | 流程由 LLM 决定,加 worker 只注册一个工具 |
| 多轮迭代要在代码里写 while | LLM 自己会反复派活直到满意 |
| 状态变化靠手动维护 | 状态由 State 自动累积 |
4.2.9 单 Agent vs Supervisor 的选择
| 维度 | 单 Agent | Supervisor 多 Agent |
|---|---|---|
| 工具数量 | ≤ 5 个 | 不限 |
| 任务复杂度 | 简单、单步 | 需要拆解的多步任务 |
| 代码复杂度 | 低 | 中 |
| 灵活度 | 一般 | 高(LLM 自主决定) |
| 可控性 | 高(图结构固定) | 中(LLM 可能"想太多") |
五、第四步:调试
实操:前面的agent测试改进(设计几个问题)
5.1 调试的 3 个层面
- 图结构调试:画图、看节点和边是否连对
- 运行轨迹调试:单步跟踪,看每一步状态变化
- 生产可观测性:用 LangSmith 看完整链路
5.2 图结构可视化
1 2 3 4 | |
生成的是 Mermaid 图,能直观看到所有节点、边、条件分支。
5.3 单步跟踪:用 stream 模式观察每一步
1 2 3 | |
输出会显示每一步产生的消息,你可以清楚看到:
- LLM 在哪一步决定调什么工具
- 工具返回了什么
- 状态是怎么变化的
5.4 LangSmith 集成
LangSmith 是 LangChain 团队的可观测性平台,能记录所有 Agent 调用的完整链路。
1 2 3 | |
设置环境变量后,所有 LangGraph 运行都会自动上报。在 https://smith.langchain.com 可以看到:
- 每次运行的完整 trace
- 每个节点的耗时
- 每次 LLM 调用的输入输出
- 每次工具调用的参数和返回
5.5 常见 Bug 与排查
Bug 1:ToolMessage 必须紧跟 AIMessage(tool_calls)
报错信息:
1 2 | |
原因:
- Worker 节点 update 消息时,顺序错了(AIMessage 在前但 ToolMessage 被插入到后面)。
- 或者 Supervisor 的 transfer AIMessage 没有对应的 ToolMessage。
修复:
1 2 3 4 5 | |
Bug 2:Worker LLM 拒绝接收 Supervisor 的派活 AIMessage
症状:
1 2 | |
原因:
Worker 节点把 Supervisor 的 state 原封不动喂给了 Worker 的 LLM,里面有 Supervisor 的 AIMessage(tool_calls=[transfer_to_X]),但 Worker LLM 看到的下一条不是 ToolMessage。
修复:
1 2 3 4 5 6 7 8 9 | |
Bug 3:Supervisor 派活后陷入无限循环
症状:Supervisor → Researcher → Supervisor → Researcher → ... 一直转。
原因:
- Worker 的输出没让 Supervisor 满意,但又没触发 finish。
- Supervisor 的 system_prompt 没明确"何时该 finish"。
修复:
- 在 Supervisor system_prompt 里加上硬性规则:"如果已收集到足够资料,必须调用 finish。"
- 给 Supervisor 加 max_iterations 限制。
- 让 Reviewer 在合格时明确返回 PASS,Supervisor 看到 PASS 才 finish。
Bug 4:Tool 没被 LLM 调用
症状:明明定义了工具,LLM 却不用。
排查顺序:
- 检查是否调用了
llm.bind_tools(TOOLS) - 检查工具的 docstring 是否清楚
- 检查 system_prompt 里是否引导 LLM 使用工具
- 用
llm.invoke("请用 calculate 算 1+1")直接测试 LLM 是否识别工具
5.6 调试工作流(推荐)
- 画图:用
draw_mermaid_png()看图结构对不对 - 单步跟踪:用
stream_mode="values"看每一步 - 本地打日志:在节点函数里加 print 输出 state 关键字段
- LangSmith 追踪:生产环境用,看完整链路
六、进阶:Deep Agents(高阶封装)
6.1 是什么
Deep Agents 是 LangChain 团队的"高阶 Agent 框架",底层还是 LangGraph,但把"Agent 应有的所有默认能力"都封装好了。
6.2 默认给你的能力
- 虚拟文件系统(ls / read_file / write_file / edit_file / glob / grep)
- 任务规划(write_todos 工具)
- 子 Agent 派活(task 工具,自动 spawn 并行 / 串行)
- 人机协同(interrupt_on)
- 长期记忆(AGENTS.md 文件)
- 自动总结 & 上下文卸载
6.3 与手写 Supervisor 的代码量对比
| 维度 | 手写 Supervisor | Deep Agents |
|---|---|---|
| 代码行数 | \~250 行 | \~30 行 |
| 节点定义 | 自己写 4 个 | 不要写 |
| 派活机制 | 手写 4 个 transfer 工具 | 内置 task 工具 |
| 任务追踪 | 自己维护 | 内置 write_todos |
| 文件系统 | 没有 | 内置 |
| 灵活性 | 高 | 中(通过 system_prompt / middleware / subagents 定制) |
6.4 完整代码(30 行)
代码位置:L4-agent/deepagents_demo.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
6.5 进阶定制
| 想要什么 | 怎么开 |
|---|---|
| 写文件前要人审批 | interrupt_on={"edit_file": True} |
| 跨会话记忆 | memory="./AGENTS.md" |
| 专属知识库 | skills=["./skills/"] |
| 自定义子 Agent | subagents=[{...}] |
| 文件权限 | permissions=[{...}] |
| 跑代码 | backend 换成 SandboxBackend |
6.6 何时用手写 LangGraph vs Deep Agents
| 用手写 LangGraph | 用 Deep Agents |
|---|---|
| 流程里有非常规路由 | 标准的研究 / 写作 / 数据分析 |
| 跨多个数据源深度协调 | 需要任务规划 + 子 Agent |
| 需要深度定制 State schema | 需要文件系统 / 长期记忆 |
| 想完全控制流程 | 想快速搭起来 |
学习建议:先用本课案例吃透原理,再学 Deep Agents 体会封装。生产中绝大多数场景用 Deep Agents 就够。
七、运行代码
7.1 安装依赖
1 | |
7.2 配置 .env
项目根目录的 D:\opc\llm-course-codes\.env:
1 2 3 | |
7.3 运行
```bash
cd D:\opc\llm-course-codes
python L4-agent/single_agent.py # 第一步 + 第二步 + 第三步(模式 A)
python L4-agent/supervisor_multi_agent.py # 第一步 + 第二步 + 第三步(模式 B)
python L4-agent/deepagents_demo.py # Deep Agents 进阶
```text
八、本章小结
8.1 对照开发 Agent 的 4 步
| 步骤 | 单 Agent(案例 1) | Supervisor 多 Agent(案例 2) | Deep Agents(案例 3) |
|---|---|---|---|
| 第一步 定义 Agent | CalculatorAgent | Supervisor + Researcher + Writer + Reviewer | 不显式定义,在 system_prompt 里描述 |
| 第二步 定义工具 | get_weather / calculate / get_current_time | 每个 Worker 一套独立工具 + Supervisor 的 transfer 工具 | 自己定义的工具 + 内置的 write_todos / task / 文件系统 |
| 第三步 设计循环 | llm_call ↔ tool_node 条件边 | Supervisor 派活 + Worker 子图 + Command 跳转 | 框架内置 |
| 第四步 调试 | draw_mermaid_png + stream + LangSmith | 同上 | 同上 |
8.2 推荐实战项目
- 项目 1:把案例 2 的 researcher 换成真实 RAG(复用 L3-rag 的 chromadb),对比"假搜索"vs"真检索"的效果。
- 项目 2:给单 Agent 加
MemorySaver,体验"对话断电后还能接着聊"。 - 项目 3:用 Deep Agents 重构案例 2,对比代码量、可维护性。
8.3 学习资源
- 本地文档:
D:\codes\langchain-docs\src\oss\langgraph\ - 多 Agent 模式对比:
D:\codes\langchain-docs\src\oss\langchain\multi-agent\ - Deep Agents 文档:
D:\codes\langchain-docs\src\oss\deepagents\ - 官方在线文档:
下一节课把这些 Agent 部署到生产环境,让它们真正为业务服务。