Agent调用流程

一次 agent.invoke 到底发生了什么?------拆解 LangGraph Agent 的完整调用链路

技术栈:LangChain / LangGraph · 中间件(Middleware)· Human-in-the-loop

关键词:thread、SummarizationMiddleware、@wrap_model_call、interrupt、Command(resume)

很多人第一次用 LangGraph 写 Agent,代码跑通了,但脑子里是一团浆糊:中间件到底是按什么顺序执行的?@wrap_model_call 是在模型调用之后才套的一层,还是它本身就是那层包裹?interrupt 暂停之后,Command(resume=...) 又是喂给谁的?

这篇文章不讲怎么装环境、怎么配 API Key,只回答一个问题:从 agent.invoke() 被按下去的那一刻,到返回那个状态字典,中间到底走了哪些步骤,每一步在改什么。


一、先分清两层钩子:节点钩子 vs 模型钩子

这是整条链路里最容易搞混的地方。LangGraph 的中间件不是"平铺的一排",而是分层的 ,而分层的分界线就是"是否真的要去调模型了"。

lua 复制代码
agent.invoke()
    ↓
【全部 before_model 钩子依次执行】      <-- 第一层:图/节点层面的前置钩子
    ↓
【wrap 洋葱包裹层开始】
    wrap2 before
    wrap3 before
    wrap1 before
        ↓
        调用 LLM 模型
        ↓
    wrap1 after
    wrap3 after
    wrap2 after
【wrap 洋葱包裹层结束】
    ↓
【全部 after_model 钩子依次执行】       <-- 节点层面的后置钩子
    ↓
返回 AIMessage

读这张图要注意三个点:

第一,before_model 跑在图层面,不是每次模型调用前都跑。 像 SummarizationMiddleware 这种摘要中间件,它属于"图状态层"------它面对的是整个 thread 的消息列表,干的事情是压缩上下文。它不属于"模型调用"这件事。

第二,@wrap_model_call 只在真正要发 LLM 请求的那一刻才进入。 而且它是包裹钩子本身,不是"模型调用之后再额外套一层"。这一点后面单独展开。

第三,洋葱的进入顺序和退出顺序严格相反。 上图里是 wrap2 → wrap3 → wrap1 进去,wrap1 → wrap3 → wrap2 出来。谁在最外层取决于注册顺序,但规则是不变的:先进后出。这跟栈是一回事。


二、@wrap_model_call 的真相:它不是一个"事后钩子"

看到 wrap_model_call 这个名字,很容易理解成"包装了模型调用的钩子",然后顺手理解成"模型调用完之后再执行一下"。不对。

它的正确结构是这样的:

python 复制代码
def wrap_model_call_middleware(request, handler):
    # before:发给模型之前,修改请求消息
    request.messages[-1].content += "before标记"

    response = handler(request)      # handler 才是真正调用 model_out

    # after:拿到模型返回后,修改 AI 输出
    response.result[0].content += "after标记"
    return response

关键在于 handler 这个参数:真正发起 LLM 请求的代码不是你写的,是 handler。你的中间件只负责"把请求递给 handler 之前动一下"和"从 handler 拿回结果之后动一下"。

所以要建立的直觉是:

  • model_out 是主模型,负责决策、生成文本、生成工具调用请求。
  • wrap_model_call 这一层不会新增任何模型调用 ,它只是对同一次 LLM 请求做前后加工。
  • 如果 Agent 进入循环、多次调用模型,那么每一轮 LLM 调用都会重新走一遍这个 wrap 钩子。

把它想成 HTTP 的拦截器或者 AOP 的切面就通了------拦的是"这一次调用",不是"整个流程"。


三、端到端流程:从 invoke 到 response

接下来是完整链路。先看流程图,后面逐步拆。

arduino 复制代码
用户调用 agent.invoke(输入消息, config=config)
        ↓
【1】读取 config,取出 thread_id → 加载对应 thread 会话(历史消息、状态)
        ↓
【2】Graph 级别中间件(SummarizationMiddleware 摘要中间件)
        ├─ 读取当前 thread 里全部 messages
        ├─ 判断是否超长,触发摘要压缩,更新消息列表
        ↓
【3】准备调用大模型 → 进入 @wrap_model_call 模型包裹中间件
        ├─ before 阶段:修改模型请求 request(改消息)
        ├─ handler(request):真正发起 LLM 请求(model_out 主模型)
        └─ after 阶段:拿到模型返回,修改 AIMessage 结果
        ↓
【4】解析模型输出:
        ├─ 情况A:纯文本回答,无工具调用 → 直接跳到【9】更新会话
        └─ 情况B:模型输出 action_requests(想要调用工具)
                ↓
【5】触发 interrupt 人工中断(暂停 Agent 图)
        ├─ 提取所有工具请求 action_requests
        ├─ 遍历工具,生成审批 decisions(同意/拒绝)
        ↓
【6】外部调用 agent.invoke(Command(resume=decisions), config=config)
        ├─ Command:唤醒被暂停的图
        ├─ resume:把人工审批结果喂回中断节点
        ↓
【7】执行工具(如果有 LLMToolEmulator,用 model_in 模拟工具返回结果)
        ↓
【8】将工具返回包装成 ToolMessage,追加进 thread 的 messages 列表
        └─ 回到【2】,再次循环:摘要中间件 → wrap 钩子 → 调用 model_out(Agent 循环)
        ↓
【9】全部任务完成,更新 thread 会话状态:追加新消息存入 thread_id 对应的会话
        ↓
【10】返回完整状态字典 response(包含所有 messages:Human / AI / ToolMessage)

1. agent.invoke(输入, config=config):拿到"钥匙"

python 复制代码
config = {"configurable": {"thread_id": "1"}}

这里必须把两个概念分开:

  • config 不是会话本身 ,它只是找到会话的钥匙 。它携带 thread_id,告诉你"这次操作哪一条会话"。
  • thread 才是会话档案,里面保存着整套历史消息和 Agent 状态。

框架拿到 thread_id,去把这条会话的历史消息加载出来,作为后续所有步骤的上下文。

2. SummarizationMiddleware:在碰 LLM 之前先瘦身

摘要中间件跑在图状态层面,此时还完全没碰模型。它做三件事:

  1. 读取当前 thread 里的 messages 列表;
  2. 计算总 token,判断是否超过阈值;
  3. 超长就把旧消息压缩成摘要,替换消息列表,减小上下文长度。

要记住的是:它修改的是"会话的消息列表",这个修改后的列表会传给后面的模型。所以它虽然不直接参与模型调用,但实实在在地决定了模型看到什么。

3. @wrap_model_call:贴着 LLM 的那一层

到了这一步,才真的要发网络请求了。钩子逻辑就是第二节那段代码:before 改 request,handler(request) 真正调 model_out,after 改返回的 AIMessage。

这一层只包裹单次 LLM 调用,不新增调用。

4. 解析模型输出:两条分岔路

模型返回之后立刻分两条路:

  • 分支一:普通文本回答,没有工具请求。 直接往下走,更新会话状态,返回结果。整个流程结束。
  • 分支二:模型输出了工具调用请求 action_requests。 模型在说:"我要调用 get_weather / send_email_tool。" 于是进入人工审批流程。

5. interrupt:Agent 主动踩刹车

这一步里,Agent 会主动暂停整个图 ,不再往下执行工具,而是把 action_requests(模型想调用的工具清单)暴露给外部代码。

外部代码要做的是:遍历所有工具请求,生成 decisions 审批列表(同意 / 拒绝)。此时 Agent 卡在断点上,等外部输入------这就是 human-in-the-loop 的落点。

6. Command(resume=decisions):把审批结果喂回去

python 复制代码
agent.invoke(Command(resume=decisions), config=config)
  • Command:LangGraph 的图控制指令,作用是把被暂停的 Agent 唤醒。
  • resume :传给断点的返回值,也就是人工审批结果 decisions。
  • 必须带上相同的 config ,这样框架才知道操作的是同一个 thread_id 会话,否则就跑到另一条会话上去了。

7. 执行工具:真实调用与模拟器

  • 真实场景:调用真实 API,把数据拿回来。
  • LLMToolEmulator :工具模拟器中间件。它内部的 model_in 伪造工具返回结果 ,不需要对接真实接口,非常适合跑通链路或做测试。模拟结果一样会被包装成 ToolMessage。

注意 model_in 和 model_out 的分工:model_out 是主大脑,负责决策;model_in 是模拟器模型,负责假装工具执行完并返回内容。

8. 回到第 2 步:Agent 循环成型

工具的返回结果以 ToolMessage 的形式被追加进 thread 的 messages 列表,然后重新走一轮完整的 Agent 循环:

lua 复制代码
摘要中间件 → wrap 模型钩子 → 再次调用 model_out

模型会基于工具结果继续思考。这个循环会反复执行,直到模型输出纯文本、不再请求工具为止。

9. 落盘与返回

  • 把本轮所有新消息(AIMessage、ToolMessage)追加存入 thread_id 对应的会话,持久化记忆。
  • agent.invoke 返回的是完整状态字典 ,不是单独一条 AIMessage。
  • response["messages"] 里放着全部消息:HumanMessage、AIMessage、ToolMessage。

四、关键概念速查表

概念 作用
config 携带 thread_id,用来选定操作哪一条会话
thread 会话档案,存储聊天历史、Agent 状态
SummarizationMiddleware 图层面中间件,模型调用前压缩长消息
@wrap_model_call 模型调用钩子,包裹单次 LLM 请求,请求前后加工
model_out Agent 主模型,做决策、生成工具调用
model_in LLMToolEmulator 模拟器模型,伪造工具返回
interrupt 暂停 Agent,等待人工审批
Command(resume) 给断点传入审批结果,恢复 Agent 运行
action_requests 模型想要调用的工具请求列表

五、几句必须记住的提醒

第一,循环的触发条件是"有工具调用"。 只要 Agent 触发了工具调用,就会进入循环、多次调用 model_out 。而每一次 model_out 调用都会触发 wrap_model_call 钩子,并且每一轮开始都会执行摘要中间件。

第二,没有工具调用就是单轮。 像"你好"这种简单对话,只走一轮,仅调用一次 model_out,不会进循环。

第三,把顺序刻进脑子里: before_model(图层面)→ wrap_model_call 洋葱(模型层,可多轮)→ after_model(图层面)。分界线永远是"是不是真的要调模型了"。


写在最后

这套链路最反直觉的地方,其实就两个:一是中间件是分层的 ,摘要这类图层面中间件和 wrap_model_call 这种模型层钩子压根不在一个层级上;二是**wrap_model_call 本身就是包裹层**,它不是模型调用之后补的一层。

把这两点想通,再看 interrupt + Command(resume) 的人工审批,就只是"在循环中间插了一个断点,等待外部把结果送回来"而已------整条链路自然就串起来了。

本文基于个人学习笔记整理,结合自己的理解写成,重点是建立直觉而非穷举 API。如有偏差,欢迎指正。

相关推荐
知几蜗牛1 小时前
AI眼镜把记忆放上云,怎样证明云端也看不见?
人工智能
知几蜗牛1 小时前
训练数据越多越好吗?用LeRobot讲清数据质量与版本化
人工智能
知几蜗牛1 小时前
PR合并慢,别再只看平均时长:GitHub把等待拆成了三段
人工智能
知几蜗牛1 小时前
AI账单失控前,团队真正缺的不是更便宜的模型
人工智能
吴佳浩1 小时前
Agent 可观测性(Observability):分布式追踪、链路诊断与 Token 成本精细化核算
人工智能·agent·ai编程
武子康1 小时前
Agent 能接进 IDE,为什么还不能随意互换?
人工智能·llm·agent
IT_陈寒1 小时前
我的JavaScript代码为啥在forEach里没按预期执行?
前端·人工智能·后端
知几蜗牛1 小时前
AI修过一次漏洞还会再犯吗?关键不只是“记住”
人工智能
老马识码1 小时前
Function Calling 与工具设计
人工智能