跳到正文

TECH NOTE

2638 字 13 分钟 系列 13 / 22

附录:Subagents 为什么是 4 次模型调用——抓包实录

ON THIS PAGE / 目录13 节

一句话结论:Subagents 的 4 次 = 主管派活(1)+ 子智能体干活(它自己也要”决定调工具→拿结果再答”,2 次)+ 主管把结果说成人话(1)。第 4 次不是多余的,它是”工具结果必须由主管变成用户可读的回答”这一步——也就是官方文档说的”集中管控的过路费”。

本篇是 03 性能对比 里「Subagents:4 次调用」的实证附录 | 实测日期:2026-08-30 | 模型:DeepSeek(deepseek-v4-flash-vision-exp,接口回报 deepseek-flash)| 协议:OpenAI Responses API(/v1/responses)

复现实验说明:demo.py / capture.py / summarize.py / captured.jsonl


一、这次抓包到底跑了什么#

一个最小的 Subagents 系统(实验脚本为 demo/demo.py,运行方式见复现实验说明,只有 3 个部件):

部件代码手里有什么
主管 Supervisorcreate_agent(model, tools=[call_calendar_agent], system_prompt="你是个人助理主管…")只有 1 个工具 schedule_event(它就是子智能体的包装壳)
子智能体 Calendar Agentcreate_agent(model, tools=[create_calendar_event], system_prompt="你是日历助手…")只有 1 个工具 create_calendar_event(桩函数,代替真实日历 API)
包装工具@tool("schedule_event") → calendar_agent.invoke(...),只返回 messages[-1]——

用户输入一句话:

帮我约明天下午 3 点和张三开产品评审会,1 小时

二、4 次调用的时序(每条都标注了抓包序号与耗时)#

用户请求
│
│ ① 11:35:03 主管第一次调模型(in=347 tokens)
│ 上下文 = [主管的 system prompt, 用户原话]
│ 产出 → function_call: schedule_event({"request":"…时长 1 小时"})
▼
┌─ 工具 schedule_event 被调用(进程内,不是 HTTP):主管把活转给日历子智能体
│ │
│ │ ② 11:35:06 日历子智能体调模型(in=398 tokens)
│ │ 上下文 = [日历助手的 system prompt, 主管转交的那句话] ← 注意:看不到主对话历史
│ │ 产出 → function_call: create_calendar_event({"title":"产品评审会", …})
│ ▼
│ ┌─ 工具 create_calendar_event 执行:返回 "OK: created '产品评审会' at 2025-06-11T15:00:00 for 60 min"
│ │ │
│ │ │ ③ 11:35:07 日历子智能体再调模型(in=1020 tokens)
│ │ │ 上下文 = [system, 主管转交的话, 它自己的 function_call, 工具结果]
│ │ │ 产出 → 文本:"已为你创建「产品评审会」,时间:明天(2025-06-11)15:00,时长 1 小时。"
│ │ └─ 这句文本作为 schedule_event 的返回值,回到主管的消息列表里
│ └───┘
│
│ ④ 11:35:08 主管第二次调模型(in=462 tokens) ← 这就是你问的"第 4 次"
│ 上下文 = [主管 system prompt, 用户原话, ①的 function_call, ③返回的工具结果]
│ 产出 → 面向用户的最终回答:"已经帮你安排好了:明天(2025-06-11)下午 3 点和张三的产品评审会,时长 1 小时。"
▼
最终回复

三、4 次模型调用的账(来自 captured.jsonl 的真实数字)#

#时间谁在调请求上下文里有什么产出inout其中思考
①11:35:03主管system(主管) + 用户原话function_call: schedule_event3477518
②11:35:06子智能体system(日历) + 转交的请求function_call: create_calendar_event398584494
③11:35:07子智能体上面 4 项 + 它自己的工具调用 + 工具结果文本(给主管看)1020300
④11:35:08主管system(主管) + 用户原话 + ①的调用 + ③的结果文本(给用户看)462310
合计2227720512

端到端耗时约 5 秒(4 次调用串行:1 次主管决策 + 2 次子智能体 + 1 次主管收尾)。


四、原始报文(Responses API,节选真实内容)#

① 主管第一次调用:请求#

{
"model": "deepseek-v4-flash-vision-exp",
"input": [
{"type": "message", "role": "system",
"content": "你是个人助理主管。遇到日程类请求时,调用 schedule_event 交给日历助手,拿到结果后用一句自然语言回复用户。"},
{"type": "message", "role": "user",
"content": "帮我约明天下午 3 点和张三开产品评审会,1 小时"}
],
"tools": [
{"type": "function", "name": "schedule_event",
"description": "把用户的日程安排请求交给日历助手处理。入参 request 应是完整的一句话请求。",
"parameters": {"type": "object", "properties": {"request": {"type": "string"}}, "required": ["request"]}}
],
"max_output_tokens": 2048
}

注意:tools 里只有 schedule_event——主管根本不知道 create_calendar_event 的存在。这就是”子智能体即工具”的报文级证据。

① 主管第一次调用:响应#

{
"id": "713dda7a-2723-4f52-b22a-26483d8ee7e9",
"model": "deepseek-flash",
"status": "completed",
"output": [
{"type": "reasoning", "status": "completed",
"content": [{"type": "reasoning_text",
"text": "The user wants to schedule a meeting. I should call schedule_event with the complete request."}],
"encrypted_content": "713dda7a-2723-4f52-b22a-26483d8ee7e9-0"},
{"type": "function_call", "status": "completed",
"name": "schedule_event",
"arguments": "{\"request\": \"帮我约明天下午 3 点和张三开产品评审会,时长 1 小时\"}",
"call_id": "call_00_Fsa6XTMl6dbQxpR4TFIb4432"}
],
"usage": {"input_tokens": 347, "output_tokens": 75,
"output_tokens_details": {"reasoning_tokens": 18}, "total_tokens": 422}
}

关键:这一次响应里没有任何面向用户的文本(output 只有 thinking + function_call)。所以此刻主管还没有能力回答用户——它连”会到底开成没有”都不知道。这就是必须还有第 4 次调用的根本原因。

② 子智能体的上下文:只有两句话(上下文隔离的实证)#

"input": [
{"type": "message", "role": "system", "content": "你是日历助手。收到日程请求后,必须调用 create_calendar_event 创建事件,然后用一句话确认结果(包含标题与时间)。"},
{"type": "message", "role": "user", "content": "帮我约明天下午 3 点和张三开产品评审会,时长 1 小时"}
]

子智能体看不到主管的 system prompt,也看不到用户与主管的完整历史;它只拿到主管”转交的那一句话”(而且主管把原话改写成了带”时长 1 小时”的版本)。反过来,主管也看不到子智能体内部的推理与工具调用——只有 ③ 的最后一条文本会回去。

③ 子智能体的第二次调用:请求(工具结果回灌)#

"input": [
{"type": "message", "role": "system", "content": "你是日历助手。…"},
{"type": "message", "role": "user", "content": "帮我约明天下午 3 点和张三开产品评审会,时长 1 小时"},
{"type": "function_call", "name": "create_calendar_event",
"arguments": "{\"title\": \"产品评审会\", \"start_iso\": \"2025-06-11T15:00:00\", \"duration_minutes\": 60}",
"call_id": "call_00_…"},
{"type": "function_call_output", "call_id": "call_00_…",
"output": "OK: created '产品评审会' at 2025-06-11T15:00:00 for 60 min"}
]

响应(③):output_text = "已为你创建「产品评审会」,时间:明天(2025-06-11)15:00,时长 1 小时。" → 这句就是 schedule_event 工具的返回值。

④ 主管第二次调用:请求(工具结果回到主管手里)#

"input": [
{"type": "message", "role": "system", "content": "你是个人助理主管。…"},
{"type": "message", "role": "user", "content": "帮我约明天下午 3 点和张三开产品评审会,1 小时"},
{"type": "reasoning", "content": [{"type": "reasoning_text", "text": "The user wants to schedule a meeting. I should call schedule_event…"}],
"encrypted_content": "713dda7a-…-0"},
{"type": "function_call", "name": "schedule_event",
"arguments": "{\"request\": \"帮我约明天下午 3 点和张三开产品评审会,时长 1 小时\"}",
"call_id": "call_00_Fsa6XTMl6dbQxpR4TFIb4432"},
{"type": "function_call_output", "call_id": "call_00_Fsa6XTMl6dbQxpR4TFIb4432",
"output": "已为你创建「产品评审会」,时间:明天(2025-06-11)15:00,时长 1 小时。"}
]

这里的 function_call_output 就是第 4 次调用的理由:主管的消息列表现在以”工具结果”结尾,而不是以”助手对用户说的话”结尾。模型必须再跑一次,才能把这条工具结果变成一条面向用户的 assistant 消息。

④ 主管第二次调用:响应(最终回答)#

{
"model": "deepseek-flash",
"status": "completed",
"output": [
{"type": "message", "role": "assistant", "status": "completed", "phase": "final_answer",
"content": [{"type": "output_text",
"text": "已经帮你安排好了:明天(2025-06-11)下午 3 点和张三的产品评审会,时长 1 小时。"}]}
],
"usage": {"input_tokens": 462, "output_tokens": 31,
"input_tokens_details": {"cached_tokens": 256}, "total_tokens": 493}
}

五、第 4 次调用到底买到了什么#

把 ① 和 ④ 对比着看最清楚——同一个主管、同一个 system prompt,唯一的差别是上下文末尾多了工具结果:

① 派活前④ 收结果后
上下文末尾用户原话用户原话 + function_call + function_call_output
产出function_call(没有文本)面向用户的文本
它知道”办成了吗”❌ 不知道✅ 知道(“已为你创建…”)
能不能对用户说话❌ 不能✅ 能

所以第 4 次买到的是四样东西(前三样是本例直接体现的,第四样是多智能体场景的关键):

  1. 把内部结果翻译成”人话”:子智能体返回的是给程序看的结果文本,主管负责组织成面向用户的一句话;
  2. 闭环:让消息列表以 assistant 消息结尾(前端”可以输入了”、对话历史合法);
  3. 决定要不要继续:主管看到结果后可以选择再派一个子智能体、重试、或追问用户——这个”下一步判断”必须有模型参与;
  4. 合并多个子智能体的结果:并行派了 3 个子智能体时,只有主管能把 3 份结果合成一个回答。

六、次数公式(和官方文档的数字对得上)#

一次用户请求的模型调用次数 ≈
1(主管派活)
+ Σ 每个被调用的子智能体(1 次"决定调不调工具" + 1 次"拿结果作答",或最少 1 次)
+ 1(主管收尾)
场景公式次数与官方文档对照
本例:1 个子智能体 + 1 次工具调用1 + 2 + 14官方「一次性请求」Subagents = 4 calls ✅
3 个子智能体,各自直接作答(无工具)1 + 3 + 15官方「多领域」Subagents = 5 calls / ~9K tokens ✅
子智能体每多一次工具调用每多 1 次工具调用 +1 次模型调用4 → 6 …官方没写,但机制决定
对照:Handoffs 一次性请求交接 1 + 调工具 1 + 作答 13官方「一次性请求」Handoffs = 3 calls ✅

结论:Subagents 的第 4 次(推广到 N 个子智能体就是最后那 1 次)是这个模式的”结构成本”:只要”所有结果都必须回到主管、由主管对用户说话”,就必须多一次汇总调用。它换来的是集中管控、结果合并和一致口径——当这些正好是你需要的,这 1 次就值;当任务简单、单一领域时,它就是纯浪费,该换成 Handoffs / Skills / Router(都是 3 次)。


七、自己复现(三步)#

Terminal window
# 0) 准备:一个 Python 3.11 环境(需 langchain 1.x + langchain-openai + httpx)+ 一个隔离环境装 mitmproxy
python -m venv /tmp/mitmenv && /tmp/mitmenv/bin/pip install mitmproxy
# 1) 起代理(CA 生成在 /tmp/subagents-demo/.mitmproxy/)
/tmp/mitmenv/bin/mitmdump -q -s demo/capture.py -p 8888 --set confdir=/tmp/subagents-demo/.mitmproxy
# 2) 让 demo 走代理跑(模型配置用环境变量,勿写进代码)
export CLOUD_MODEL_BASE_URL=https://api.deepseek.com
export CLOUD_MODEL_API_KEY=<你的 key>
export CLOUD_MODEL_NAME=deepseek-v4-flash-vision-exp
export CLOUD_MODEL_PROTOCOL=RESPONSES
HTTPS_PROXY=http://127.0.0.1:8888 SSL_CERT_FILE=/tmp/subagents-demo/.mitmproxy/mitmproxy-ca-cert.pem \
python demo/demo.py
# 3) 把抓包汇总成人能读的清单
python demo/summarize.py demo/captured.jsonl

说明:SSL_CERT_FILE 指向 mitmproxy 的 CA 证书,httpx/OpenAI SDK 才会信任代理的中间人证书;不加这个会报 CERTIFICATE_VERIFY_FAILED。 本次抓包文件 demo/captured.jsonl 只保存请求/响应正文,不含任何请求头与密钥。

附录:Subagents 为什么是 4 次模型调用——抓包实录
https://aicooleo.cn/blog/posts/multiagent-13-appendix-4-calls/
作者
aicooleo
发布于
2026-08-30
许可协议
CC BY-NC-SA 4.0
多 Agent 精讲 · 第 13 / 22 篇 返回合集