面试官问:“你的 Agent 平均每轮调几个工具?“
面试官问:“你的 Agent 平均每轮调几个工具?”
简历:“3 年后端开发,主导公司 AI Agent 平台建设,负责多工具编排与端到端延迟优化。”
看到简历上延迟优化这几个字,我问了个当场就能验证的问题:你的 Agent 平均每轮调用几个工具?
候选人想了几秒,说没专门统计过,一般是一个,复杂任务可能两三个。
前半句是实话,后半句是猜的。
这个数字就躺在他的调用日志里,五行代码能算出来。绝大多数自研 Agent 算出来的结果是 1.0,不多不少的 1.0,意味着这套系统从上线到现在,从来没有在一轮里同时调起过两个工具。而它用的模型,出厂设置本来就会并行。
Agent 为什么慢,多数团队的答案是模型推理慢、工具接口慢。今天这场面试聊的是第三种可能:慢的是自己写的那段拼装消息的代码。
Round 1:这个数,你算过吗
面试官:“你的 Agent 平均每轮调用几个工具?给个数。”
候选人:“这个……应该是一个吧?大部分任务一个工具就够了,复杂的会多调几次。”
正解:
这个指标在 Anthropic 官方文档里有正式名字,average tools per message,判断标准只有一条:大于 1.0 说明并行生效,等于 1.0 说明一次都没并行过。
算法很直白。遍历收到的所有 assistant message,数出其中含 tool_use block 的消息条数作分母,数出 tool_use block 的总数作分子,相除。日志里已经存着的响应对象直接就能跑,不用改任何线上代码:
`tool_call_messages = [
msg for msg in messages if any(block.type == “tool_use” for block in msg.content)
]
total_tool_calls = sum(
len([block for block in msg.content if block.type == “tool_use”])
for msg in tool_call_messages
)
avg = total_tool_calls / len(tool_call_messages) if tool_call_messages else 0.0
print(f"Average tools per message: {avg}")
大于 1.0 才说明并行在工作`
(来源:Anthropic 官方文档 Parallel tool use 页,2026-08 访问)
跑之前先猜一个数,跑完再看差多少。
关键在于 1.0 这个值本身就不正常。Claude 4 及以后的模型,在请求受益于多个工具时默认就会并行调用;OpenAI 侧的并行 function calling 在 GPT-5 及以后同样支持。什么都不配置,模型出厂自带这个能力。日志里出现一个精确的 1.0,说明这条链路上有东西在把它压回去,而且压得非常彻底,几千次调用里一次例外都没有。
这跟我的任务本来就只需要一个工具是两回事。用户说帮我看看这三个配置文件、端口是不是撞了,这是三个彼此独立、参数在提问那一刻就已经确定的读操作,模型完全有能力一次吐出三个 tool_use block。真实的 Agent 日志里,这类请求占比通常不低。如果它们也是 1.0,问题就不在任务复杂度上。
先把这个数算出来。后面几轮讲的所有事情,都是围绕怎么让它从 1.0 涨上去。
要点速记
- 指标名 average tools per message:tool_use block 总数 ÷ 含 tool_use 的 assistant message 条数
- 大于 1.0 并行生效,等于 1.0 说明一次都没并行过
- Claude 4 及以后默认并行,GPT-5 及以后支持并行 function calling,都不需要额外开关
- 五行代码在现有日志上就能算,不用改线上代码
Round 2:那三个工具,是谁在并发跑
面试官:“假设模型一次返回了 3 个 tool_use block,这 3 个工具是谁在并发执行?”
候选人:“SDK 吧?API 既然一次返回多个调用,应该是框架帮我并发跑掉,再把结果一起送回去。”
正解:
没有人帮你跑。Anthropic 文档在这点上写得毫不含糊:How you run those calls is your decision,The API doesn't prescribe an execution order。模型给你的只是一份清单,谁先谁后、并发还是串行,全在你自己的 runtime 里。
并行工具调用这个词其实盖着两件独立的事,任何一件掉链子,端到端都是串行。
生成侧的并行:模型在一次前向生成里连续产出多个 tool_use block,装在同一个stop_reason: tool_use的响应里。这一步由模型决定,你能影响,但替它做不了主。
执行侧的并行:拿到这 3 个 block 之后,是 for 循环挨个 await,还是asyncio.gather一把梭。这一步纯粹是你的代码。
大量团队卡在后面这层。模型老老实实吐了 3 个 block,业务代码一个 for 循环挨个跑,三个各花 800ms 的接口,硬生生跑成 2.4 秒。日志上看 average tools per message 是 3.0,体感却和串行没差别。
两层都通了,再看并行到底省下什么。墙钟时间是最直观的那部分,3 个 800ms 的独立接口,串行 2.4 秒,并行 800ms。但这只是表层。
真正的大头是往返次数。串行调 3 个工具意味着 3 次独立的 LLM 请求:第一次让模型决定调 A,拿到结果后第二次让它决定调 B,第三次调 C,最后还要一次生成最终回答,整整 4 次往返。而每一次往返,都要把 system prompt、全部工具定义、到目前为止的完整对话历史重新发一遍。并行的话,一次请求吐出 3 个调用,一次请求消化 3 个结果,2 次往返收工。
把这笔账按 token 算一遍。设固定前缀(system + 工具定义 + 已有历史)20K token,每个 tool_use block 约 50 token,每个工具返回约 800 token:
| LLM 请求次数 | 累计 input token | |
|---|---|---|
| 串行 3 个工具 | 4 | 85,100 |
| 并行 3 个工具 | 2 | 42,550 |
(按上述假设推算,非实测数据)
input token 砍掉一半。按 claude-opus-5 输入 $5/M 计(Anthropic 定价,该型号 2026-07-24 发布),单次任务从 $0.43 降到 $0.21。工具数量越多,两条曲线岔得越开,因为串行的往返次数随工具数线性增长,而每次往返都在重发那个越来越长的前缀。
要点速记
- API 不规定执行顺序,并发与否由你的 runtime 决定,SDK 不会替你 gather
- 并行分两层:模型愿不愿意一次吐多个 block,你的代码敢不敢同时跑
- 3 个工具串行需要 4 次 LLM 往返,并行只要 2 次
- 20K 前缀下 input token 从 85K 降到 42K,opus-5 单价 $5/M 时单次省约 $0.21
Round 3:avg 就是卡在 1.0,你怎么排查
面试官:“你回去跑了脚本,avg 精确等于 1.0。现在让你排查,第一步做什么?”
候选人:“那我在 system prompt 里加一句,要求它尽量并行调用工具,加完再看效果。”
正解:
第一步该看的不是 prompt,是上一轮把 tool_result 送回去时的消息结构。
这是整件事里最反直觉的地方:你的历史消息本身正在充当少样本示例,一轮一轮教模型不要并行。
Anthropic 把这条列为并行失效的头号原因,文档里用的动词是 teach,说错误的 tool_result 格式会teach Claude to avoid parallel calls。
机制不复杂。模型生成下一个 tool_use block 时,看到的是完整对话历史。假如你把两个工具结果拆成两条独立的 user message 发回去:
[ {"role": "assistant", "content": [tool_use_1, tool_use_2]}, {"role": "user", "content": [tool_result_1]}, {"role": "user", "content": [tool_result_2]} ]
历史里就留下了一个结构清晰的范例:一次调用配一个结果,一轮只办一件事。模型下一轮生成时,会照着历史里已有的样式来,于是只吐一个 block。你的代码看到只有一个 block,自然又只回一条 user message,历史里再添一条串行样本。
这是个自我强化的负反馈环。Agent 跑得越久,历史里串行的示范越密,模型越不并行,avg 在 1.0 上钉得越死。此时加 prompt 去扭转,等于用一句指令对抗几十条现成的反面示例,效果时有时无,而且随着会话变长越来越弱。
想快速确认是不是这个问题,在消息数组上数一下相邻的 user message 就行:
# 相邻两条都是 user,说明一批 tool_result 被拆开发了 splits = sum( 1 for a, b in zip(messages, messages[1:]) if a["role"] == "user" and b["role"] == "user" ) print(f"被拆开的 tool_result 批次: {splits}")
结果大于 0,问题基本就在这里。有些框架还会在每条 tool_result 后面自动补一条提示性的 user message,这种同样算拆开,而且更隐蔽。
正确写法是所有结果合并进同一条 user message,靠tool_use_id做配对:
[ {"role": "assistant", "content": [tool_use_1, tool_use_2]}, {"role": "user", "content": [tool_result_1, tool_result_2]} ]
还有一条顺序规则容易踩。这条 user message 里,所有 tool_result block 必须排在任何 text block 前面。想在结果后面追一句接下来该做什么是允许的,追在前面直接 400。官方给的报错特征是tool_use ids were found without tool_result blocks immediately after,日志里搜到这句,就去查 content 数组的元素顺序。
排查顺序很明确:先确认消息结构,再动 prompt。结构不修,prompt 加得再重也是在跟历史打架。
要点速记
- 并行失效的头号原因是 tool_result 回传格式,prompt 强度排在后面
- N 个结果拆成 N 条 user message,等于给模型做了 N 次串行示范
- 正确做法:所有 tool_result 合并进同一条 user message,用 tool_use_id 配对
- 同一条消息里 tool_result 必须排在 text 之前,反了报 400
- 修复顺序:先结构后 prompt,结构不修 prompt 白加
Round 4:格式改完了,还差什么
面试官:“消息格式改对了,接下来还要做什么?这套东西你线上跑过吗?”
候选人:“改完格式,再加个提示词,执行那边用 asyncio.gather 包一下,应该就可以了。”
正解:
方向没错,但这三步改完直接上线,大概率要在生产环境出事。
打开的部分确实是三件事。消息结构是前提,Round 3 已经说过。第二件是 system prompt,官方给了两个版本,弱版一句话:
For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
默认效果不够时换强版,包在一对名为use_parallel_tool_calls的 XML 标签里,额外给了具体例子:读 3 个文件就发 3 个并行调用,跑多个只读命令一律并行。第三件是执行层的asyncio.gather或Promise.all。
出事的地方在第三件。并行安全的前提是这批调用彼此独立,而模型判断独立性的能力并不可靠。官方文档专门开了一节讲批次里的调用看起来互相依赖该怎么办,给的缓解 prompt 是Only batch tool calls that are independent of each other。需要为它单独写一节,说明这情况够常见。
真正会出事故的是有副作用的工具。三个只读接口并发跑错了,顶多重试一次;三个写操作乱序执行,就是数据事故。可行的做法是给每个工具打标:只读的进并发池,带写、带事务、带外部状态的一律退回串行执行。这个判断放在你的代码里,别交给模型。
失败处理是第二个坑,而且是硬性 API 约束。你串行跑这批调用,第二个失败了,第三个决定不跑,那么第三个也必须回一个 tool_result,带上is_error: true和一句说明:
{ "type": "tool_result", "tool_use_id": "toolu_02", "is_error": true, "content": "Not executed: the preceding write_file call failed." }
少回一个,整个请求 400。规则是每个 tool_use block 都要有对应的 tool_result,一个都不能缺。
第三件要提前想清楚的是上下文膨胀速度。并行把 N 个工具的返回值一次性灌进上下文,而串行至少还留有在中间做裁剪的机会。Anthropic 在工程博客里提到,Claude Code 默认把单个工具响应限制在 25,000 token;同一篇里举的例子,某个 Slack 接口的详细返回占 206 token,精简版只要 72 token,差了将近三倍。并发度乘上这个差值,就是上下文被烧掉的速度。工具返回值的分页、字段过滤、截断,该在开并行之前做完。
最后把成本这笔账修正一下。Round 2 算出的省一半 input token,前提是没开 prompt cache。开了缓存之后,串行多出来的那几次往返里,重发的前缀绝大部分会命中,而缓存读取按基础 input 价的 0.1 倍计费(Anthropic 定价文档:5 分钟缓存写入 1.25 倍,1 小时写入 2 倍,读取 0.1 倍)。同样那笔账按缓存价重算,2 倍的差距会收窄到几成之内。具体收窄到多少,取决于缓存断点打在哪、首轮前缀是否已经命中,按不同假设算下来大致落在 15% 到 60% 这个区间。
并行能稳定兑现的收益是墙钟时间和往返次数,这两项跟缓存开不开没有关系。省钱那部分会被缓存吃掉一大截,拿能省一半 token 成本去立项,上线后大概率对不上账。
顺带一个高频踩坑:想关掉并行时,disable_parallel_tool_use藏在tool_choice对象里面,不在请求顶层。
tool_choice={"type": "auto", "disable_parallel_tool_use": True}
写在顶层不会报错,也不会生效。这个字段的语义还随tool_choice类型变化:auto下是至多调一个工具,也可以一个都不调,any或tool下是恰好调一个。OpenAI 侧对应参数叫parallel_tool_calls,置为 false 时保证一轮里零个或一个工具调用。
要点速记
- 三步:修消息结构 → 加 system prompt → 执行层 gather,缺第一步后两步无效
- 只读工具进并发池,带写/带事务的强制串行,独立性判断别交给模型
- 没执行的调用也要回 tool_result + is_error: true,少一个整个请求 400
- Claude Code 默认限制单个工具响应 25,000 token,并发度会成倍加快上下文消耗
- 缓存读取是基础 input 价的 0.1 倍,开 cache 后并行的成本优势从 2 倍收窄到 15%-60%,随缓存断点而变
- disable_parallel_tool_use 在 tool_choice 对象内,写在顶层静默失效
Round 5:什么时候你会主动把并行关掉
面试官:“你讲了一堆怎么打开。反过来,什么情况下你会主动关掉?”
候选人:“有副作用的工具吧,写操作不能并发。其他的……应该是越并行越好?”
正解:
副作用只是最表层那条。真正决定能不能并行的,是参数在这一刻是否已知。
模型一次吐出 3 个 tool_use block 时,这 3 组参数是在同一次生成里连续写完的。第 2 个调用的参数,是在完全看不到第 1 个调用返回值的情况下填出来的。
读这三个文件能并行,三个路径在用户提问时就写死了。而先列出目录,再读里面最大的那个文件没法并行,第二步的文件名要等第一步返回才知道。模型如果被 prompt 逼着并行,它会做的事情是猜一个文件名填进去,然后你在 tool_result 里收到一个 file not found,白烧一轮。
这条边界比工具有没有副作用更常被忽略。两个纯只读的接口,一样可能因为参数依赖而不该并行。
第二种该关的是逐步收敛的探索型任务。Agent 做故障排查、代码定位这类工作时,每一步的观察结果都会改变下一步该看哪里。并行等于强迫它在信息最少的时刻,一次性把后面几步全押出去,押错就是成倍的无效 token。这类任务串行反而更省,也更准。
第三种是模型侧的已知缺陷。OpenAI 文档点名过某个旧版快照在开启并行时会重复调用同一个工具,建议对该版本直接关闭;微调模型还有个额外约束,一轮里调多个函数时 strict 模式对这些调用会失效,schema 校验的保证就没了。上生产前值得对实际在用的型号跑一遍验证。
最后一条跟缓存有关。改动tool_choice参数本身会让 messages 层的缓存失效,tools 和 system 层保留(Anthropic 缓存失效规则文档)。别设计成按请求动态开关并行,每切换一次就要付一遍 message 缓存重建的钱。要么全局固定,要么按会话固定。
把这几条判据摊平成一张表,落到代码里就是给工具打标时的依据:
| 场景 | 并行 | 判据 |
|---|---|---|
| 读 3 个已知路径的文件 | ✅ | 参数在提问那一刻已确定 |
| 并发查 3 个只读 API | ✅ | 参数独立,失败可直接重试 |
| 先 list 目录再读最大的文件 | ❌ | 第二步参数依赖第一步的返回 |
| 批量写库 / 转账 / 发通知 | ❌ | 有副作用,乱序即事故 |
| 故障排查、代码定位 | ❌ | 每步观察都会改变下一步方向 |
| 微调模型一轮调多个函数 | 先验证 | strict 模式对这批调用失效 |
要点速记
- 能否并行的判据是参数此刻是否已知,工具有无副作用只是其中一条
- 只读工具也可能因参数依赖而不该并行,比如先 list 后 read
- 探索型任务(故障排查、代码定位)串行更省,并行是在信息最少时下注
- 微调模型一轮调多个函数时 strict 模式失效,schema 保证随之消失
- 动态改 tool_choice 会失效 messages 层缓存,并行开关按会话固定
面试官点评
这位候选人对 Agent 的理解停在模型加工具加循环这个层面,缺的是对循环里每一条消息如何反过来影响下一次生成的感知。他把 Agent 慢归因到模型和接口,唯独没想过自己那段拼装 message 的代码也是变量。
三条建议:
- 今晚就把 average tools per message 加进监控,跟 P99 延迟放在一张图上看。这个数从 1.0 涨到 2.0,收益通常比换个更快的模型更直接。
- 把 tool_result 的拼装逻辑翻出来看一眼,确认 N 个结果合并在同一条 user message 里,且排在所有 text block 之前。这是一个函数级的改动。
- 给工具加只读标记,只读的才进并发池。这件事的顺序是开并行之前做完。
写 Agent 的人大多在优化模型选型、prompt 措辞、工具设计,很少有人回头看自己送进去的那份消息历史长什么样。而模型每一轮都在读它,并且照着它的样子作答。
调了半年 Agent 延迟,最后发现拖慢它的是自己写的那个 for 循环。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~
