返回 AI大模型从0到1——理论与实操

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 大家族的位置

Plain Text
1
2
3
4
5
6
7
┌────────────────────────────────────────────────┐
│  Deep Agents —— "开箱即用的完整 Agent"          │  一行 create_deep_agent 起步
├────────────────────────────────────────────────┤
│  LangGraph —— "画流程图的运行时"                 │  节点、边、状态、子图、Command
├────────────────────────────────────────────────┤
│  LangChain —— "基础积木盒"                       │  Models / Tools / Prompts / Messages
└────────────────────────────────────────────────┘

学习顺序:先吃透 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 个问题:

  1. 这个 Agent 叫什么
  2. 做什么(输入 / 输出是什么)?
  3. 它的边界在哪里(什么不做)?

这一句话决定了你后面所有节点、工具、状态的设计。

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

Plain Text
 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
RESEARCH_AGENT_NAME = "ResearchAgent"

RESEARCH_AGENT_SYSTEM_PROMPT = """
你是一名 ResearchAgent。你的职责:
- 接收一个研究问题
- 调用合适的工具收集资料
- 最终输出结构化报告

你不做的事:
- 不执行代码
- 不直接修改文件
- 不回答与研究无关的问题(如闲聊)
"""

# 在 LangGraph 中体现为:State 里有一个字段标记当前 agent 名字,
# 每个节点的 system_prompt 都引用 RESEARCH_AGENT_SYSTEM_PROMPT。
```text
# 三、第二步:定义 Agent 需要的工具、技能

## 3.1 什么是"工具"

工具是 Agent 能调用的**普通 Python 函数**LangGraph 通过 `@tool` 装饰器把函数包装成 LLM "看见"的工具 schema)。

## 3.2 工具的 3 个要素

1. **函数名**LLM 用它来调用snake_case
2. **文档字符串**告诉 LLM 这个工具能干嘛何时该用
3. **类型注解**参数和返回值的类型自动生成 schema

## 3.3 具体例子:3 个常用工具

### 例子 1:天气查询工具

```python
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """查询指定城市的当前天气。

    Args:
        city: 城市名,如 "北京" / "上海" / "Tokyo"。
    """
    # 真实场景:调天气 API。这里用伪数据演示。
    return json.dumps({"city": city, "temperature": 24, "condition": "晴"})

要点:docstring 不是装饰用的,是给 LLM 看的"说明书"。写得越清楚,LLM 用得越准。

例子 2:数学计算工具

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@tool
def calculate(expression: str) -> str:
    """对数学表达式求值。

    支持 + - * / () 和数字。不支持变量、函数、负数。
    Args:
        expression: 如 "(25+17)*3"。
    """
    # 安全:白名单只允许数字和运算符
    allowed = set("0123456789+-*/().% ")
    if not all(ch in allowed for ch in expression):
        return "错误:表达式包含非法字符"
    try:
        return str(eval(expression, {"__builtins__": {}}, {}))
    except Exception as e:
        return f"计算失败: {e}"

要点:工具有副作用风险时(执行代码、调 API、写文件)要做安全校验。

例子 3:时间查询工具

Plain Text
1
2
3
4
5
6
import datetime as _dt

@tool
def get_current_time() -> str:
    """获取当前的服务器时间,返回格式 'YYYY-MM-DD HH:MM:SS'。"""
    return _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

3.4 把工具"告诉" LLM

bind_tools 把工具列表绑给 LLM。这一步之后,LLM 在回复时就能输出 tool_calls

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="MiniMax-M3", ...)

# 1) 定义工具
TOOLS = [get_weather, calculate, get_current_time]

# 2) 把工具告诉 LLM
llm_with_tools = llm.bind_tools(TOOLS)

# 3) LLM 看到用户问题时,会自动选工具
response = llm_with_tools.invoke("北京天气怎么样?")
print(response.tool_calls)
# 输出:[{'name': 'get_weather', 'args': {'city': '北京'}, 'id': '...'}]

3.5 工具设计的 4 条原则

原则 原因
一个工具只做一件事 LLM 才能精确选择
docstring 写清楚何时该用 / 不该用 LLM 选错工具的常见原因是 docstring 不清
参数尽量少 参数越多 LLM 越容易填错
返回值用字符串(JSON 更佳) LLM 处理字符串最稳

3.6 反例:写一个"坏"工具

Plain Text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@tool
def do_everything(action: str, data1: str = "", data2: str = "", data3: str = "") -> str:
    """万能工具,能做各种事"""
    if action == "weather":
        ...
    elif action == "calc":
        ...
    elif action == "time":
        ...
    # 50 行 if-elif ...

这种"瑞士军刀"工具的坏处:LLM 不知道 action 该传什么值,docstring 一句话讲不清能力,扩展性为零。拆成 3 个独立工具才对。


四、第三步:设计循环 / 多 Agent 协作

这一步是 LangGraph 最核心的部分。我们分两种模式讲:

  • 单 Agent 循环(条件边实现 ReAct)
  • 多 Agent 协作(Supervisor 模式)

4.1 模式 A:单 Agent ReAct 循环

4.1.1 流程图

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
        ┌─────────────┐
        │   START     │
        └──────┬──────┘
               ▼
        ┌─────────────┐
        │  llm_call   │  调用 LLM,让它决定要不要调工具
        └──────┬──────┘
               │
       ┌───────┴───────┐
  有 tool_calls?      没 tool_calls?
       │                    │
       ▼                    ▼
  ┌─────────┐         ┌─────────┐
  │tool_node│         │   END   │
  │执行工具 │         │ 结束流程│
  └────┬────┘         └─────────┘
       │
       └─ 把结果回灌给 llm_call ─┐
                                 │
                            (回到 llm_call)

4.1.2 完整代码

代码位置:L4-agent/single_agent.py

4.1.3 实测运行

用户问:"北京天气怎么样?顺便帮我算一下 (25+17)*3 等于多少?"

Plain Text
1
2
3
4
5
6
7
8
[HumanMessage]    北京天气怎么样?顺便帮我算一下 (25+17)*3 等于多少?
[AIMessage]      tool_calls=[
                   get_weather(city="北京"),
                   calculate(expression="(25+17)*3")
                 ]
[ToolMessage]    {"city": "北京", "temperature": 21, "condition": "晴"}
[ToolMessage]    126
[AIMessage]      北京现在 21 度,天气晴朗;(25+17)*3 = 126。还有别的需要吗?

注意 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 流程图

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
                        用户
                         │
                         ▼
                ┌────────────────┐
                │  Supervisor    │
                │  (决定派给谁)  │
                └────────┬───────┘
                         │
       ┌─────────┬───────┴───────┬─────────┐
       ▼         ▼               ▼         ▼
  researcher  writer          reviewer   END
  (查资料)    (写文章)        (评审)    (结束)
       │         │               │
       └─────────┴───────┬───────┘
                         │
                  (回灌给 Supervisor)

4.2.4 Supervisor 节点代码

代码位置:L4-agent/supervisor_multi_agent.py

Plain Text
 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
from langgraph.types import Command

# 把"派活"建模成 4 个工具
@tool
def transfer_to_researcher(task: str) -> str:
    """派给 Researcher"""
    return f"派给 Researcher:{task}"

@tool
def transfer_to_writer(task: str) -> str:
    """派给 Writer"""
    return f"派给 Writer:{task}"

@tool
def transfer_to_reviewer(task: str) -> str:
    """派给 Reviewer"""
    return f"派给 Reviewer:{task}"

@tool
def finish(answer: str) -> str:
    """任务完成,结束流程"""
    return f"FINAL::{answer}"

supervisor_llm = base_llm.bind_tools([
    transfer_to_researcher, transfer_to_writer,
    transfer_to_reviewer,   finish,
])

def supervisor_node(state) -> Command:
    """Supervisor:看一眼白板,决定派给谁"""
    ai = supervisor_llm.invoke([
        SystemMessage(content="你是 Supervisor..."),
        *state["messages"]
    ])

    # 把 LLM 选的"工具名"翻译成"跳转指令"
    goto = END
    if ai.tool_calls:
        name = ai.tool_calls[0]["name"]
        goto = {
            "transfer_to_researcher": "researcher",
            "transfer_to_writer":     "writer",
            "transfer_to_reviewer":   "reviewer",
            "finish":                 END,
        }[name]

    return Command(goto=goto, update={"messages": [ai]})

4.2.5 Worker 节点代码

每个 Worker 内部都是一个独立的 ReAct 子图。

Python
 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
def make_worker_node(name, system_prompt, tools, tool_node):
    """工厂函数:给每个 Worker 生成一个节点"""
    worker_llm = base_llm.bind_tools(tools)

    # Worker 内部子图:agent ↔ tools 循环
    subgraph = (
        StateGraph(MessagesState)
        .add_node("agent", lambda s: {"messages": [
            worker_llm.invoke([SystemMessage(system_prompt), *s["messages"]])
        ]})
        .add_node("tools", tool_node)
        .add_edge(START, "agent")
        .add_conditional_edges("agent",
            lambda s: "tools" if s["messages"][-1].tool_calls else END)
        .add_edge("tools", "agent")
        .compile()
    )

    def worker_node(state: MessagesState) -> Command[Literal["supervisor"]]:
        # 1) 从 Supervisor 的派活指令里取出 task
        supervisor_msg = next(m for m in reversed(state["messages"])
                              if isinstance(m, AIMessage))
        task_text = supervisor_msg.tool_calls[0]["args"].get("task", "")
        tool_call_id = supervisor_msg.tool_calls[0]["id"]

        # 2) 跑 Worker 子图
        result = subgraph.invoke({
            "messages": [HumanMessage(content=task_text)]
        })

        # 3) ToolMessage 紧跟在 Supervisor AIMessage 之后
        transfer_tm = ToolMessage(
            content=f"{name} 完成:{result['messages'][-1].content}",
            name=f"transfer_to_{name}",
            tool_call_id=tool_call_id,
        )
        return Command(
            goto="supervisor",
            update={"messages": [transfer_tm, *result["messages"]]},
        )

    return worker_node

# 生成 3 个 Worker
researcher_node = make_worker_node("researcher", "你是研究员...", RESEARCH_TOOLS, RESEARCH_TOOL_NODE)
writer_node     = make_worker_node("writer",     "你是写手...",   WRITER_TOOLS,     WRITER_TOOL_NODE)
reviewer_node   = make_worker_node("reviewer",   "你是评审员...", REVIEWER_TOOLS,   REVIEWER_TOOL_NODE)

# 装配主图
builder = StateGraph(MessagesState)
builder.add_node("supervisor", supervisor_node)
builder.add_node("researcher", researcher_node)
builder.add_node("writer",     writer_node)
builder.add_node("reviewer",   reviewer_node)
builder.add_edge(START, "supervisor")
graph = builder.compile()

4.2.6 实测运行片段

Plain Text
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Human]   请帮我写一篇 300 字左右的短文,介绍 LangGraph 在企业级 AI Agent 中的应用价值。

[Supervisor]   transfer_to_researcher(task="收集 LangGraph 资料...")
[Researcher]   web_search("LangGraph 是什么..."), web_search("LangGraph features...")...
[Researcher]   整理出关键要点...

[Supervisor]   transfer_to_writer(task="基于资料写 280~350 字短文")
[Writer]       # LangGraph:企业级 AI Agent 的图驱动编排框架 ...(约 624 字)

[Supervisor]   transfer_to_reviewer(task="检查字数和内容")
[Reviewer]     字数 624,超标!建议精简到 280~350。

[Supervisor]   transfer_to_writer(task="精简到 280~350 字")
[Writer]       ...(精简版)
[Reviewer]     字数 375,再精简。

[Supervisor]   transfer_to_writer(task="再精简到 350 以内")
[Writer]       ...(最终版)
[Reviewer]     PASS!350 字以内,结构清晰。

[Supervisor]   finish(answer="## 任务完成!...")

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 个层面

  1. 图结构调试:画图、看节点和边是否连对
  2. 运行轨迹调试:单步跟踪,看每一步状态变化
  3. 生产可观测性:用 LangSmith 看完整链路

5.2 图结构可视化

Python
1
2
3
4
# 编译后直接画图
agent = builder.compile()
from IPython.display import Image, display
display(Image(agent.get_graph().draw_mermaid_png()))

生成的是 Mermaid 图,能直观看到所有节点、边、条件分支。

5.3 单步跟踪:用 stream 模式观察每一步

Plain Text
1
2
3
# stream 模式:每执行完一个节点就返回一个 snapshot
for step in agent.stream({"messages": [HumanMessage(content="...")]}, stream_mode="values"):
    step["messages"][-1].pretty_print()

输出会显示每一步产生的消息,你可以清楚看到:

  • LLM 在哪一步决定调什么工具
  • 工具返回了什么
  • 状态是怎么变化的

5.4 LangSmith 集成

LangSmith 是 LangChain 团队的可观测性平台,能记录所有 Agent 调用的完整链路。

Plain Text
1
2
3
pip install langsmith
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=lsv2_xxx

设置环境变量后,所有 LangGraph 运行都会自动上报。在 https://smith.langchain.com 可以看到:

  • 每次运行的完整 trace
  • 每个节点的耗时
  • 每次 LLM 调用的输入输出
  • 每次工具调用的参数和返回

5.5 常见 Bug 与排查

Bug 1:ToolMessage 必须紧跟 AIMessage(tool_calls)

报错信息:

Plain Text
1
2
openai.BadRequestError: 400 - An assistant message with 'tool_calls' must be followed
by tool messages responding to each 'tool_call_id'.

原因:

  • Worker 节点 update 消息时,顺序错了(AIMessage 在前但 ToolMessage 被插入到后面)。
  • 或者 Supervisor 的 transfer AIMessage 没有对应的 ToolMessage。

修复:

Plain Text
1
2
3
4
5
# 错误写法:先放子图消息,再放 ToolMessage
update={"messages": [*result["messages"], ToolMessage(...)]}

# 正确写法:ToolMessage 必须紧跟在 Supervisor AIMessage 后
update={"messages": [ToolMessage(...), *result["messages"]]}

Bug 2:Worker LLM 拒绝接收 Supervisor 的派活 AIMessage

症状:

Plain Text
1
2
openai.BadRequestError: 400 - An assistant message with 'tool_calls' must be followed
by tool messages responding to each 'tool_call_id'.

原因:

Worker 节点把 Supervisor 的 state 原封不动喂给了 Worker 的 LLM,里面有 Supervisor 的 AIMessage(tool_calls=[transfer_to_X]),但 Worker LLM 看到的下一条不是 ToolMessage。

修复:

Plain Text
1
2
3
4
5
6
7
8
9
# Worker 启动时只构造一个新 HumanMessage,丢弃 Supervisor 的派活 AIMessage
def worker_node(state):
    supervisor_msg = next(m for m in reversed(state["messages"])
                          if isinstance(m, AIMessage))
    task_text = supervisor_msg.tool_calls[0]["args"].get("task", "")

    # 用全新 HumanMessage 启动子图,不传 state["messages"]
    result = subgraph.invoke({"messages": [HumanMessage(content=task_text)]})
    ...

Bug 3:Supervisor 派活后陷入无限循环

症状:Supervisor → Researcher → Supervisor → Researcher → ... 一直转。

原因:

  • Worker 的输出没让 Supervisor 满意,但又没触发 finish。
  • Supervisor 的 system_prompt 没明确"何时该 finish"。

修复:

  1. 在 Supervisor system_prompt 里加上硬性规则:"如果已收集到足够资料,必须调用 finish。"
  2. 给 Supervisor 加 max_iterations 限制。
  3. 让 Reviewer 在合格时明确返回 PASS,Supervisor 看到 PASS 才 finish。

Bug 4:Tool 没被 LLM 调用

症状:明明定义了工具,LLM 却不用。

排查顺序:

  1. 检查是否调用了 llm.bind_tools(TOOLS)
  2. 检查工具的 docstring 是否清楚
  3. 检查 system_prompt 里是否引导 LLM 使用工具
  4. llm.invoke("请用 calculate 算 1+1") 直接测试 LLM 是否识别工具

5.6 调试工作流(推荐)

  1. 画图:用 draw_mermaid_png() 看图结构对不对
  2. 单步跟踪:用 stream_mode="values" 看每一步
  3. 本地打日志:在节点函数里加 print 输出 state 关键字段
  4. 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

Plain Text
 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
from deepagents import create_deep_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
import json, os

# 1) 你的工具
@tool
def web_search(query: str) -> str:
    """联网搜索"""
    return json.dumps({"query": query, "results": [...]}, ensure_ascii=False)

@tool
def count_words(text: str) -> str:
    """统计字数"""
    return str(len(text))

# 2) LLM
llm = ChatOpenAI(model="MiniMax-M3", base_url=..., api_key=...)

# 3) 一行创建
agent = create_deep_agent(
    model=llm,
    tools=[web_search, count_words],
    system_prompt=(
        "你是研究员 + 写手。\n"
        "工作流:\n"
        "1) write_todos 拆任务\n"
        "2) web_search 查资料\n"
        "3) 写文章,用 count_words 自检字数\n"
    ),
)

# 4) 运行
result = agent.invoke({"messages": [{"role": "user", "content": "..."}]})
print(result["messages"][-1].content)

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 安装依赖

Python
1
pip install langgraph langchain-openai langchain-core deepagents python-dotenv

7.2 配置 .env

项目根目录的 D:\opc\llm-course-codes\.env

Plain Text
1
2
3
MINIMAX_API_KEY=sk-xxx
MINIMAX_BASE_URL=https://api.minimaxi.com/v1
MINIMAX_MODEL=MiniMax-M3

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. 项目 1:把案例 2 的 researcher 换成真实 RAG(复用 L3-rag 的 chromadb),对比"假搜索"vs"真检索"的效果。
  2. 项目 2:给单 Agent 加 MemorySaver,体验"对话断电后还能接着聊"。
  3. 项目 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 部署到生产环境,让它们真正为业务服务。