Tool Execution Runtime
This document is not available in the current locale. Showing fallback: zh
When This Page Helps
How multiple tool calls in one turn get executed safely. Best read after s02.
这篇桥接文档解决的不是“工具怎么注册”,而是:
当模型一口气发出多个工具调用时,系统到底按什么规则执行、并发、回写、合并上下文?
这一篇为什么要存在
s02 先教你:
- 工具 schema
- dispatch map
- tool_result 回流
这完全正确。
因为工具调用先得成立,后面才谈得上复杂度。
但系统一旦长大,真正棘手的问题会变成下面这些:
- 多个工具能不能并行执行
- 哪些工具必须串行
- 工具执行过程中要不要先发进度消息
- 并发工具的结果应该按完成顺序回写,还是按原始出现顺序回写
- 工具执行会不会改共享上下文
- 多个并发工具如果都要改上下文,最后怎么合并
这些问题已经不是“工具注册”能解释的了。
它们属于更深一层:
工具执行运行时。
先解释几个名词
什么叫工具执行运行时
这里的运行时,不是指编程语言 runtime。
这里说的是:
当工具真正开始执行时,系统用什么规则去调度、并发、跟踪和回写这些工具。
什么叫 concurrency safe
你可以先把它理解成:
这个工具能不能和别的同类工具同时跑,而不会把共享状态搞乱。
例如很多只读工具常常是 concurrency safe:
read_file- 某些搜索工具
- 某些纯查询类 MCP 工具
而很多写操作不是:
write_fileedit_file- 某些会改全局状态的工具
什么叫 progress message
有些工具跑得慢,不适合一直静默。
progress message 就是:
工具还没结束,但系统先把“它正在做什么”告诉上层。
什么叫 context modifier
有些工具执行完不只是返回结果,还会修改共享环境。
例如:
- 更新通知队列
- 更新 app state
- 更新“哪些工具正在运行”
这种“对共享上下文的修改动作”,就可以理解成 context modifier。
最小心智模型
先不要把工具执行想成:
tool_use -> handler -> result
更接近真实可扩展系统的理解是:
tool_use blocks
->
按执行安全性分批
->
每批决定串行还是并行
->
执行过程中可能产出 progress
->
最终按稳定顺序回写结果
->
必要时再合并 context modifiers
这里最关键的升级点有两个:
- 并发不是默认全开
- 上下文修改不是谁先跑完谁先直接乱写
关键数据结构
1. ToolExecutionBatch
教学版最小可以先用这样一个概念:
batch = {
"is_concurrency_safe": True,
"blocks": [tool_use_1, tool_use_2, tool_use_3],
}
它的意义是:
- 不是每个工具都单独处理
- 系统会先把工具调用按可否并发分成一批一批
2. TrackedTool
如果你准备把执行层做得更稳、更清楚,建议显式跟踪每个工具:
tracked_tool = {
"id": "toolu_01",
"name": "read_file",
"status": "queued", # queued / executing / completed / yielded
"is_concurrency_safe": True,
"pending_progress": [],
"results": [],
"context_modifiers": [],
}
这类结构的价值很大。
因为系统终于开始能回答:
- 哪些工具还在排队
- 哪些已经开始
- 哪些已经完成
- 哪些已经先吐出了中间进度
3. MessageUpdate
工具执行过程中,不一定只有最终结果。
最小可以先理解成:
update = {
"message": maybe_message,
"new_context": current_context,
}
更完整的执行层里,一个工具执行运行时往往会产出两类更新:
- 要立刻往上游发的消息更新
- 只影响内部共享环境的 context 更新
4. Queued Context Modifiers
这是最容易被忽略、但很关键的一层。
在并发工具批次里,更稳的策略不是“谁先完成谁先改 context”,而是:
先把 context modifier 暂存起来,最后按原始工具顺序统一合并。
最小理解方式:
queued_context_modifiers = {
"toolu_01": [modify_ctx_a],
"toolu_02": [modify_ctx_b],
}
最小实现
第一步:先分清哪些工具能并发
def is_concurrency_safe(tool_name: str, tool_input: dict) -> bool:
return tool_name in {"read_file", "search_files"}
第二步:先分批,再执行
batches = partition_tool_calls(tool_uses)
for batch in batches:
if batch["is_concurrency_safe"]:
run_concurrently(batch["blocks"])
else:
run_serially(batch["blocks"])
第三步:并发批次先吐进度,再收最终结果
for update in run_concurrently(...):
if update.get("message"):
yield update["message"]
第四步:context modifier 不要乱序落地
queued_modifiers = {}
for update in concurrent_updates:
if update.get("context_modifier"):
queued_modifiers[update["tool_id"]].append(update["context_modifier"])
for tool in original_batch_order:
for modifier in queued_modifiers.get(tool["id"], []):
context = modifier(context)
这一步是整篇里最容易被低估,但其实最接近真实系统开始长出执行运行时的点之一。
一张真正应该建立的图
tool_use blocks
|
v
partition by concurrency safety
|
+-- read-only / safe batch -----> concurrent execution
| |
| +-- progress updates
| +-- final results
| +-- queued context modifiers
|
+-- exclusive batch ------------> serial execution
|
+-- direct result + direct context update
为什么这层比“dispatch map”更接近真实系统主脉络
最小 demo 里:
handlers[tool_name](tool_input)
就够了。
但在更完整系统里,真正复杂的不是“找到 handler”。
真正复杂的是:
- 多工具之间如何共存
- 哪些能并发
- 并发时如何保证回写顺序稳定
- 并发时如何避免共享 context 被抢写
- 工具报错时是否中止其他工具
所以这层讲的不是边角优化,而是:
工具系统从“可调用”升级到“可调度”的关键一步。
它和前后章节怎么接
s02先教你工具为什么能被调用s02a-tool-control-plane.md讲工具为什么会长成统一控制面- 这篇继续讲,工具真的开始运行以后,系统如何调度它们
s07、s13、s19往后都还会继续用到这层心智
尤其是:
- 权限系统会影响工具能不能执行
- 后台任务会影响工具是否立即结束
- MCP / plugin 会让工具来源更多、执行形态更复杂
初学者最容易犯的错
1. 看到多个工具调用,就默认全部并发
这样很容易把共享状态搞乱。
2. 只按完成顺序回写结果
如果你完全按“谁先跑完谁先写”,主循环看到的顺序会越来越不稳定。
3. 并发工具直接同时改共享 context
这会制造很多很难解释的隐性状态问题。
4. 认为 progress message 是“可有可无的 UI 装饰”
它其实会影响:
- 上层何时知道工具还活着
- 长工具调用期间用户是否困惑
- streaming 执行体验是否稳定
5. 只讲工具 schema,不讲工具调度
这样读者最后只会“注册工具”,却不理解真实 agent 为什么还要长出工具执行运行时。
教学边界
这篇最重要的,不是把工具调度层一次讲成一个庞大 runtime,而是先让读者守住三件事:
- 工具调用要先分批,而不是默认看到多个
tool_use就全部并发 - 并发执行和稳定回写是两件事,不应该混成一个动作
- 共享 context 的修改最好先排队,再按稳定顺序统一合并
只要这三条边界已经清楚,后面的权限、后台任务和 MCP 接入就都有地方挂。
更细的队列模型、取消策略、流式输出协议,都可以放到你把这条最小运行时自己手搓出来以后再补。
读完这一篇你应该能说清楚
至少能完整说出这句话:
工具系统不只是
tool_name -> handler,它还需要一层执行运行时来决定哪些工具并发、哪些串行、结果如何回写、共享上下文如何稳定合并。
如果这句话你已经能稳定说清,那么你对 agent 工具层的理解,就已经比“会注册几个工具”深一大层了。