一次提问,图上到底跑了几步
flowchart TB
A[START<br/>messages 0 条] --> B[超步 1 model<br/>决定调用 get_current_time]
B --> C[超步 2 tools<br/>执行 拿到时间]
C --> D[超步 3 model<br/>决定调用 web_search]
D --> E[超步 4 tools<br/>执行 拿到搜索结果]
E --> F[超步 5 model<br/>写出最终回答]
F --> G[END<br/>messages 6 条]
模型不是「问一句、答一句」,而是「想一步 → 调个工具 → 拿着结果接着想」,直到不用再调了才开口答。而且每跑完一个超步,都会落一个检查点。
就拿「今天上海的天气如何?」这一句,实测下来:
| 项目 | 实测 |
|---|---|
| 超步 | 5 个 |
| model 跑了几次 | 3 次,不是 1 次 |
| tools 跑了几次 | 2 次,一次查时间、一次搜天气 |
| messages | 从 0 条长到 6 条 |
下面把这一趟按顺序走一遍。
整体:图长什么样
先看这张图,就是之前学过的那个循环:
START 进 model,model 出来分两条路:上一轮还带着工具调用就去 tools,没有工具调用了就直接 END。tools 干完再回到 model —— 就是这条回边,让模型能「想一步、看一眼、接着想」。
真跑起来之后,trace 里能一行一行数出节点执行的顺序:
__start__ → model → tools → model → tools → model,model 和 tools 交替出现。这就是上面那 5 个超步的本体。
最后在 chat 里看到的回答:
messages 是怎么一条条长出来的
每跑完一个超步就落一个检查点,检查点里存的是那一刻完整的 state。 所以把一串检查点按顺序看下来,messages 是怎么长出来的就一目了然 —— 这也是检查点最实用的地方:过程可以倒着回看。
起点:检查点里什么都没有
messages 是空的 []。这是这一趟开始之前的模样。
输入进来:reducer 把 HumanMessage 接上去
START 收到用户的问题,检查点里就多了一条 Human。注意节点只负责「产生这条新消息」,是 reducer 把它接到数组末尾的,不是把整个数组重写一遍。
第一次调用工具:model 要查时间
model 这一轮没有直接回答,而是产出了一条带工具调用的 AI 消息 —— 它要调 get_current_time。(右边那个 ID 是这次工具调用的编号,展开才看得到。)
这一轮跑完,检查点里 AI 那条也进去了:
现在是 [Human, AI] 两条,只增不减。
第一次执行:工具节点跑工具
tools 按这条调用去执行 get_current_time,返回一个时间字符串。跑完照样落检查点:
现在是 [Human, AI, Tool] 三条。
第二次调用工具:model 拿到时间,又决定要搜天气
这一步最值得看:模型先拿到了时间,然后才判断「光有时间不够,还得搜一下天气」,于是又一次发起工具调用,参数也自己写好了 —— query = 上海今天天气 2026年9月29日。(下半张是这个超步之后的检查点,[Human, AI, Tool, AI] 四条。)
第二次执行:搜索
tools 执行 web_search,检查点里又多一条 Tool,变成 5 条。
收尾:这次不用调工具了,直接回答
数据齐了,model 这一轮没有再产出工具调用,而是直接写回答。
最后这条 AI 消息进 messages,凑齐 6 条。model 发现自己不用再调工具了,这一次就走 END,一趟结束。
第二轮:上下文只会越来越大
第二轮我又问了一句「那今天杭州的天气如何?」,先看这一轮开始之前的 messages:
上一轮的 6 条原封不动还在 —— 因为 state 存在检查点里,第二轮接着往下用,不用重新来。
新的一句 Human 追加到末尾:
第 7 条就是新问题。这一轮的机制和第一轮一模一样(查时间、搜天气、回答),就不重复走了。
要留意的是:messages 只进不出,聊得越久上下文越长。 这不是 bug —— 好处是模型记得住前面聊过什么,代价是每一轮都得把全部历史重新发给模型。
之前学的概念,都落在这条 trace 的哪里
| 之前学的 | 在这条 trace 里对应什么 |
|---|---|
| 图(graph) | 那张 START → model ↔ tools 的循环图 |
| 消息(message) | 检查点里 messages 数组的每一项 |
| reducer | 每个超步跑完,messages 比上一步多一条 |
| 超步(super step) | trace 里 model、tools 各占一行,一行就是一个超步 |
| 检查点(checkpointer) | 每个超步之后新落的那一条,点开就能看到当时的 state |
| 工具节点(ToolNode) | trace 里的 tools 那一行 |
容易踩的坑
- 以为「问一句 = 模型答一次」。 实测这一趟 model 跑了 3 次 —— 每拿到一批工具结果,它都要重新想一遍。
- 以为工具是模型一口气调完的。 实测是分两批:先查时间,拿到时间之后才决定要搜天气。模型是看着上一步的结果决定下一步的,不是开局就把两步都计划好。
- 以为 messages 会被新消息覆盖。 实测只增不减,第二轮跑的时候第一轮那 6 条还在,新问题接在第 7 条。
小结
- 模型是「想一步、调一次、再想」,不是一问一答。 这一趟 5 个超步:model 3 次、tools 2 次。
- 节点只负责产生新消息,追加是 reducer 干的。 所以 messages 只增不减。
- 一个超步落一个检查点,检查点存的是当时完整的 state。 整个过程可以倒着回看。
- 上下文只进不出。 第二轮直接用第一轮的 messages,聊得越久每轮发给模型的东西越多。