并行工具调用:同时等待,按编号收回结果
正在读取学习记录…
1. 哪些调用可以同时开始
并行工具调用是把多个互相独立的请求一起发出,让等待时间重叠。例如行程助手同时查询上海天气和汇率,这两项都已有完整输入,天气结果也不会改变汇率参数,可以一起开始。
相反,“先查订单编号,再用编号查物流”有数据依赖,需要先得到编号。并行前先问:每项输入是否已经齐全、是否会修改同一份数据、结果是否依赖另一项?
异步程序在等待时把执行机会交给其他任务。信号量是控制同时工作的“名额”:设为2,最多两项一起进入工具调用;名额满时,后续任务等待。这里的并发主要让等待重叠,示例里的工具结果来自固定字典。
2. 运行天气和汇率两个请求
在学习文件夹新建 parallel_lab.py,保存下面完整程序。从该文件夹打开 PowerShell,运行代码后的命令。
先在 PowerShell 创建本篇的独立目录并进入;路径可换成你自己的非系统盘位置:
mkdir -Force "F:\学习\agentdepth-l2-parallel-tools"
cd "F:\学习\agentdepth-l2-parallel-tools"将本篇的 parallel_lab.py、test_parallel_lab.py 保存到这个目录。保存全部文件后运行 dir *.py,核对上述文件名,再执行下文命令;新开窗口时先回到该目录,并按 Python 环境篇核对 python 的解释器路径。
"""限制并发数量,按调用编号收集固定工具结果。"""
import asyncio
import json
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class Call:
call_id: str
tool: str
argument: str
delay: float
_RESULTS = {("weather", "上海"): "上海:晴", ("fx", "CNY"): "CNY:1.00"}
_ALLOWED = {"weather", "fx"}
async def run_calls(calls: List[Call], *, limit: int = 2, timeout: float = 0.1) -> dict:
"""调度独立读取,将每项最终结果绑定到它的调用编号。"""
if type(limit) is not int or limit < 1:
raise ValueError("invalid_limit")
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or timeout <= 0:
raise ValueError("invalid_timeout")
ids = [call.call_id for call in calls]
if any(not isinstance(call_id, str) or not call_id.strip() for call_id in ids):
raise ValueError("missing_call_id")
if len(ids) != len(set(ids)):
raise ValueError("duplicate_call_id")
for call in calls:
if call.tool not in _ALLOWED:
raise ValueError("tool_not_allowed")
if not isinstance(call.argument, str) or isinstance(call.delay, bool) or not isinstance(call.delay, (int, float)) or call.delay < 0:
raise ValueError("invalid_call")
semaphore = asyncio.Semaphore(limit)
completion_order = []
active = 0
peak = 0
async def one(call: Call):
nonlocal active, peak
async with semaphore:
active += 1
peak = max(peak, active)
try:
await asyncio.wait_for(asyncio.sleep(call.delay), timeout=timeout)
value = _RESULTS.get((call.tool, call.argument))
result = {"status": "ok", "value": value} if value is not None else {
"status": "error", "code": "bad_argument"}
except asyncio.TimeoutError:
result = {"status": "timeout"}
finally:
active -= 1
completion_order.append(call.call_id)
return call.call_id, result
completed = await asyncio.gather(*(one(call) for call in calls))
return {"completion_order": completion_order,
"by_call_id": dict(completed), "peak": peak}
if __name__ == "__main__":
sample = [Call("A", "weather", "上海", 0.03), Call("B", "fx", "CNY", 0.0)]
print(json.dumps(asyncio.run(run_calls(sample)), ensure_ascii=False, indent=2))python parallel_lab.py输出:
{
"completion_order": [
"B",
"A"
],
"by_call_id": {
"A": {
"status": "ok",
"value": "上海:晴"
},
"B": {
"status": "ok",
"value": "CNY:1.00"
}
},
"peak": 2
}A模拟等待0.03秒,B模拟等待0秒,因此完成顺序为B、A。by_call_id 仍把天气放在A下、汇率放在B下。peak: 2 表示运行中同时工作的数量最多为2。
Call 保存编号、工具、参数和等待时间。run_calls 先检查编号唯一、工具允许和参数有效,随后建立信号量。one 为每项请求执行等待、组装结果或记录超时,最后归还工作名额。gather 等待这些任务全部结束,再按编号合并。
每项结果带状态,所以一项超时后,另一项成功的结果仍可保存。应用程序可以据用户目标决定展示部分结果、重试读取或结束。
3. 改并发数和超时值
把末尾调用改为 run_calls(sample, limit=1),再运行。峰值变1,A先占名额,因此完成顺序为A、B。结果内容保持相同。
恢复并发2,把调用改为 run_calls(sample, timeout=0.01)。A等待0.03秒,超过0.01秒上限,状态为 timeout;B仍为 ok。这两个改动分别展示“调度数量”和“每项等待上限”。
| 条件 | 完成顺序 | 峰值 | 结果 |
|---|---|---|---|
| limit=2,默认超时 | B、A | 2 | 两项成功 |
| limit=1,默认超时 | A、B | 1 | 两项成功 |
| limit=2,timeout=0.01 | B后A超时 | 2 | B成功,A超时 |
4. 运行检查用例
把下方测试文件保存到程序所在目录:
下面的测试检查同时运行的任务数量、结果编号和超时后的收尾。
测试代码:test_parallel_lab.py
# 核对并行工具调用的正常结果和错误分支;断言失败时会显示对应的测试。
import asyncio
import unittest
from parallel_lab import Call, run_calls
class ParallelToolsTests(unittest.TestCase):
def run_lab(self, calls, *, limit=2, timeout=0.1):
return asyncio.run(run_calls(calls, limit=limit, timeout=timeout))
def test_fast_second_call_finishes_first_but_ids_stay_bound(self):
report = self.run_lab([Call("A", "weather", "上海", 0.03),
Call("B", "fx", "CNY", 0.0)])
self.assertEqual(report["completion_order"], ["B", "A"])
self.assertEqual(report["by_call_id"]["A"], {"status": "ok", "value": "上海:晴"})
self.assertEqual(report["by_call_id"]["B"], {"status": "ok", "value": "CNY:1.00"})
self.assertEqual(report["peak"], 2)
def test_one_call_fails_without_corrupting_another_result(self):
report = self.run_lab([Call("A", "weather", "未知城市", 0.01),
Call("B", "fx", "CNY", 0.0)])
self.assertEqual(report["by_call_id"]["A"], {"status": "error", "code": "bad_argument"})
self.assertEqual(report["by_call_id"]["B"], {"status": "ok", "value": "CNY:1.00"})
def test_limit_one_serializes_even_if_second_delay_is_shorter(self):
report = self.run_lab([Call("A", "weather", "上海", 0.02),
Call("B", "fx", "CNY", 0.0)], limit=1)
self.assertEqual(report["completion_order"], ["A", "B"])
self.assertEqual(report["peak"], 1)
def test_timeout_is_recorded_for_its_own_id(self):
report = self.run_lab([Call("A", "weather", "上海", 0.04),
Call("B", "fx", "CNY", 0.0)], timeout=0.005)
self.assertEqual(report["by_call_id"]["A"], {"status": "timeout"})
self.assertEqual(report["by_call_id"]["B"], {"status": "ok", "value": "CNY:1.00"})
def test_duplicate_id_and_unauthorized_tool_are_rejected_before_dispatch(self):
with self.assertRaisesRegex(ValueError, "duplicate_call_id"):
self.run_lab([Call("A", "weather", "上海", 0), Call("A", "fx", "CNY", 0)])
with self.assertRaisesRegex(ValueError, "tool_not_allowed"):
self.run_lab([Call("X", "delete_account", "any", 0)])
if __name__ == "__main__":
unittest.main()python -m unittest test_parallel_lab -v测试同时检查正常和失败路径。先恢复刚才的改动,再运行;若出现导入错误,核对文件位置与运行目录。
5. 结果合并与并发代价
调用编号由应用程序生成,用于配对请求和返回。重复编号会覆盖结果,示例在调度前检查;多个工具同名也需要不同调用编号,因为名称只说明做哪类操作。
真实服务有连接数和速率限制,同时发十个请求可能触发限流。并发数还应结合工具延迟、服务额度和任务总时间选择。可先从低并发开始,观察完成时间、超时数和错误率,再调整。
写工具并行时需要额外检查共享资源:两个更新可能相互覆盖,查询和删除可能产生竞态。依赖明确的写入适合顺序执行,独立只读请求通常更容易合并。结果部分成功时,保留每项状态和编号,让用户看清哪一项需要补查。
asyncio 的任务说明介绍 gather 和取消,信号量说明介绍并发名额。总结:先找独立任务,限制同时数量,按编号收回,逐项保留状态。下一篇 幂等处理重复写入。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“并行工具调用与结果回注”,哪种做法更稳妥?
对应知识点:L2-parallel-tools
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L2-parallel-tools
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L2-parallel-tools
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L2-parallel-tools
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。