章末自测

题目 1:为什么 README.md、app.py、requirements.txt、config.py 通常适合并行读取?

请从数据依赖、副作用和共享资源三个角度分析,同时说明什么情况下即使都是 read_file,也不应该无限并发。

这四个读取动作通常不依赖彼此结果,也不会修改文件,因此从数据依赖和副作用角度适合并行。但只读并不等于无限并发。如果文件来自远程存储、单次读取很大、底层文件服务有限流或连接限制,仍然需要控制 max_workers。所以“能不能并发”要同时看逻辑依赖和资源约束,而不是只看 Tool 名称。

题目 2:判断题——read_file → edit_file → run_test 可以并行执行,因为它们是三个不同 Tool,对吗?

请解释具体依赖关系。

不对。edit_file 往往依赖 read_file 看到的真实代码和上下文,而 run_test 又应该在修改完成后验证新代码。如果三者同时执行,测试可能在修改之前启动,读取也可能得到修改前或修改后的不确定版本,产生竞态。它们虽然是三个不同 Tool,但逻辑上存在明显 read → edit → verify 依赖,因此必须串行。

题目 3:代码阅读——为什么并行执行时最好给 ToolCall 增加 call_id?

假设一轮同时执行两个 read_file,参数分别是 A.py 和 B.py,而 B 比 A 更早完成。请解释没有 call_id 会有什么问题。

并行任务完成顺序不保证等于提交顺序,而且同一个 Tool Name 可以被多次调用。只记录 read_file 无法明确某个 Result 对应 A.py 还是 B.py。唯一 call_id 可以从模型生成 Tool Call 开始一直传到 ToolResult、Trace 和 Provider 回传,确保请求与响应稳定关联。尤其在错误恢复中,我们必须知道到底是哪一个调用失败。

题目 4:编程实现——使用 ThreadPoolExecutor 并行执行三个同步任务

要求完整可运行,并保留输入顺序输出结果。

@可以这样实现:
from concurrent.futures import (
    ThreadPoolExecutor,
)
import time


def work(
    name: str,
    delay: float,
) -> str:
    time.sleep(
        delay
    )

    return (
        f"{name} finished"
    )


if __name__ == "__main__":
    inputs = [
        ("A", 0.5),
        ("B", 0.3),
        ("C", 0.4),
    ]

    with ThreadPoolExecutor(
        max_workers=3
    ) as executor:
        futures = [
            executor.submit(
                work,
                name,
                delay,
            )
            for (
                name,
                delay,
            ) in inputs
        ]

        results = [
            future.result()
            for future in futures
        ]

    print(results)
#线程会并发运行,但因为最终按 futures 原提交顺序调用 result(),输出仍然保持 A、B、C 的输入顺序。若希望按完成顺序处理,可以使用 as_completed()。

题目 5:编程实现——使用 asyncio.gather 并发执行三个异步 Tool

要求每个 Tool 使用 asyncio.sleep() 模拟 I/O,并最终一次得到全部结果。

题目 6:综合应用——一批四个 Tool 有一个失败,应该怎么处理?

假设三个 read_file 成功,一个返回 FileNotFoundError。请说明为什么不应该直接丢掉整批结果,以及下一轮 Agent 应该看到什么。

其他三个文件内容仍然是有效 Observation,丢掉会浪费已经完成的工作。更合理的是 Batch Result 中逐条记录 call_id、arguments、ok、result 或 error,例如 3 success、1 failed。下一轮模型既看到三个文件内容,也知道某个路径不存在,然后可以对失败项执行 search_files 或 list_files。这样 Parallel Execution 与 Error Recovery 可以自然结合。

题目 7:概念理解——为什么并行 Tool Calling 不等于“把所有可能动作一次性发出去”?

请结合数据分析 Agent 解释“分阶段并行”的意义。

Agent 的价值在于根据 Observation 动态决定下一步。如果一开始把所有可能查询全部并行执行,会产生大量无意义工作,也失去动态下钻价值。更合理的是在当前阶段,对已经确定彼此独立的动作并行,例如同时查 request、impression、click、eCPM;拿到结果后发现 eCPM 异常,再进入下一阶段按媒体查询。并发优化的是一个阶段内部的独立动作,而不是替代 Agent 的反馈驱动决策。

进一步阅读

  • Python concurrent.futures 官方文档:重点理解 ThreadPoolExecutor、submit()、Future、as_completed()。
  • Python asyncio 官方文档:重点理解 coroutine、task、event loop、gather()、Semaphore。
  • I/O Bound 与 CPU Bound 相关资料:理解线程、asyncio、进程池为什么适合不同任务类型。
  • Race Condition 相关资料:理解多个有副作用 Tool 同时修改共享资源为什么危险。
  • Semaphore / Rate Limit 相关资料:并发能力必须与资源上限一起设计。
  • Structured Concurrency 相关资料:理解并发任务生命周期、取消和错误传播。
  • Fan-out / Fan-in Pattern:并行 Tool Calling 本质上常见为“扇出多个任务,再汇总结果”。
  • Distributed Task Execution 相关资料:后续如果 Agent Tool 进入 Celery、任务队列或远程 Worker,会继续遇到类似问题。

术语表

  • 并行 Tool Calling(Parallel Tool Calling):在同一轮 Agent 决策中,对多个互不依赖的 Tool Call 同时调度执行,以降低 I/O 等待时间的执行方式。
  • 线程池(Thread Pool):复用固定数量线程执行多个同步任务的机制。Python 中可通过 ThreadPoolExecutor 实现,特别适合 I/O 型同步 Tool。
  • 协程(Coroutine):可在等待 I/O 时主动让出执行权的异步函数单元。Python 中通常使用 async def 定义,并由 asyncio 事件循环调度。
  • 事件循环(Event Loop):asyncio 负责调度多个协程执行的核心机制,当一个协程等待 I/O 时,可以切换到其他可运行协程。
  • 并发上限(Concurrency Limit):系统允许同时执行的最大 Tool 数量,例如线程池 max_workers 或 asyncio.Semaphore,用于避免资源失控。
  • 竞态条件(Race Condition):多个并发任务同时访问或修改共享资源时,最终结果依赖不可控执行时序的现象,例如两个 edit_file 同时修改同一文件。
  • 批量结果(Batch Result):一轮并行 Tool Calls 完成后形成的结果集合,通常需要保存每个 call_id、成功状态、结果或错误,再统一转成 Observation。
  • Fan-out / Fan-in:先把一个决策扇出成多个独立并行任务,再把结果汇总回一个处理节点的模式,非常适合描述并行 Tool Calling。

本章总结

这一节,我们让 Agent 的 Tool Execution 从:

一个一个执行

升级成:

多个独立动作可以并发执行

最开始的例子非常直观:

             ┌→ README.md
             ├→ app.py
Agent ───────┼→ requirements.txt
             └→ config.py

四个文件读取互不依赖。

如果串行执行,总耗时接近每个 Tool 耗时之和。

如果并发执行,总耗时更接近最慢那个 Tool 的耗时。

但这一节真正最重要的不是“快”。

而是我们建立了一个明确边界,只有互不依赖、没有副作用冲突、资源允许的 Tool Call 才适合并行。

所以:

read README
read app.py
read requirements

通常可以并行。

而:

read_file
 ↓
edit_file
 ↓
run_test

必须保持串行。

我们先使用:

concurrent.futures

解决同步 Tool 的并发。

尤其是:

ThreadPoolExecutor

非常适合已有 I/O 型同步工具。

然后又学习:

asyncio

处理本来就是异步实现的 HTTP、数据库等 Tool。

这两者没有谁“更高级”。

选择标准应该是:

已有同步 Tool
→ ThreadPoolExecutor

原生 async Tool
→ asyncio

我们还特别强调了三个工程问题。

第一,必须有 call_id。

因为并发完成顺序不确定,同一个 Tool 又可能被调用多次,必须稳定关联 Tool Call 与 Tool Result。

第二,必须错误隔离。

四个 Tool 一个失败,不应该自动丢掉其他三个成功结果。

第三,必须限制并发。

无论线程池还是 asyncio,都不能让模型决定无限并发。Harness 必须控制:

max_workers
Semaphore
Rate Limit

最后我们也看到,并行并没有改变 Agent Main Loop 的本质。

串行:

Reason
 ↓
Action
 ↓
Observation
 ↓
Reason

并行只是变成:

Reason
 ↓
Actions
 ↓
Observations
 ↓
Reason

模型仍然要等当前阶段结果回来,再决定下一阶段。

这也是为什么“并行 Tool Calling”不是“把所有可能动作一次发出去”。

真正成熟的 Agent 更常见的是,分阶段并行。

例如数据分析:

第一阶段:
request / impression / click / ecpm
并行查询

 ↓

发现 ecpm 异常

 ↓

第二阶段:
按媒体继续查询

这样既保留 Agent 的动态决策,又减少每个阶段内部的等待时间。

到这里,第2部分关于 Tool 的基础能力已经比较完整了:

Tool Calling
 ↓
Tool Registry
 ↓
Tool Schema
 ↓
基础 Tool
 ↓
Error Recovery
 ↓
Parallel Tool Calling

下一部分我们会正式进入整门课另一个非常关键的主题,Context Engineering。

Agent 已经会做越来越多事情,新的问题马上出现,每一轮到底应该把哪些信息交给模型?

如果什么都塞进 messages,上下文很快会爆。

如果塞得太少,Agent 又会失忆。

所以接下来我们会开始构建真正的,Context Builder。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发
头像-大赛人网
欢迎您留下宝贵的见解!
提交
头像-大赛人网

昵称

取消
昵称

    请登录后查看评论内容