批量工具调用与并发处理

一句话定义:当 Agent 需要处理一批同质任务(如批量导入多个 Excel)时,在工具层用 Python 并发机制替代串行逐个执行,提升吞吐并汇总结果。

1. 问题场景

典型场景:Agent 调用 Skill 创建航线模块并上传 Excel,每个 Excel 内含大量航线数据,需要调 Python 脚本执行导入。

用户:"把这十几个 Excel 的航线都导进去"
  → Agent 调用 Skill(航线导入)
    → Skill 触发 Tool(import_routes)
      → Tool 调 Python 脚本
        → Python 读取 Excel → 解析航线 → 调 API 逐条创建

串行痛点:十几个 Excel 逐个跑,每个内部还要逐条调 API 创建航线,总耗时 = Σ(每个Excel处理时间)。如果单个 Excel 需要 30 秒,10 个就是 5 分钟,Agent 一直阻塞等待。

串行:  [Excel1 30s] → [Excel2 30s] → [Excel3 30s] → ... → 总计 300s
并发:  [Excel1 30s]
        [Excel2 30s]
        [Excel3 30s]
        ...                                    → 总计 ~35s(取决于线程数)

2. 并发架构选择

关键决策点:在哪一层做并发? 有三种方案,适用场景不同。

方案 A:LLM 层并行调用(Parallel Function Calling)

依赖模型的并行 Function Calling 能力,LLM 一次输出多个 tool call,宿主并行执行:

[
  {"name": "import_routes", "arguments": {"file": "routes_01.xlsx"}},
  {"name": "import_routes", "arguments": {"file": "routes_02.xlsx"}},
  {"name": "import_routes", "arguments": {"file": "routes_03.xlsx"}}
]

宿主并行执行三个调用,汇总结果后一次性回传。

优点 缺点
实现简单,宿主用 Promise.all / asyncio.gather 依赖模型支持并行调用
每个调用独立,天然隔离 文件多了 LLM 可能漏调或参数填错
并发度不可控(LLM 输出几个就几个)
每次调用独立拉起 Python 进程,进程启动开销大

适用:文件数量少(3-5 个)、已验证模型并行调用稳定、不想改工具内部逻辑。

方案 B:工具内部并发(推荐)

LLM 只调一次工具,把所有文件路径作为数组传入,工具内部用 Python 并发处理:

{
  "name": "import_routes_batch",
  "arguments": {
    "files": ["routes_01.xlsx", "routes_02.xlsx", "routes_03.xlsx", "..."],
    "concurrency": 5
  }
}

工具内部 Python 用线程池/进程池并发处理所有文件,汇总后一次性返回。

优点 缺点
只一次 LLM 交互,减少往返 工具内部需实现并发逻辑
并发度可控(线程池大小) 单个文件失败需设计容错
进程只拉起一次,无重复启动开销
结果汇总在工具内完成,返回结构化汇总
不依赖模型的并行调用能力

适用:文件数量多(10+)、需要精确控制并发度、需要统一汇总结果。本场景首选。

方案 C:混合模式

LLM 层并行调用 + 每个调用内部再并发。适用于超大规模(100+ 文件),LLM 分批输出 tool call,每个 tool call 处理一批文件。复杂度高,一般不需要。

决策流程

文件数 ≤ 5 且模型支持并行调用?
  ├─ 是 → 方案 A(LLM 层并行)
  └─ 否 → 文件数 ≤ 50?
           ├─ 是 → 方案 B(工具内部并发)✓
           └─ 否 → 方案 C(混合模式)

3. Python 并发机制选择

方案 B 确定后,工具内部用 Python 实现并发。Python 有三套并发机制:

机制 适用场景 航线导入场景
ThreadPoolExecutor I/O 密集(网络请求、文件读写) ✅ 调 API 创建航线是 I/O 密集
ProcessPoolExecutor CPU 密集(大量计算、数据转换) ⚠️ 如果 Excel 解析极重可考虑
asyncio 高并发 I/O、已有异步代码库 ✅ 适合,但与同步 API 混用较复杂

航线导入场景分析

单个 Excel 处理流程:
  1. 读 Excel 文件        → I/O(磁盘读取)
  2. 解析航线数据          → CPU(pandas/openpyxl 解析)
  3. 数据校验              → CPU
  4. 调 API 逐条创建航线    → I/O(网络请求)← 瓶颈在这
  5. 记录结果              → I/O

瓶颈在第 4 步——调 API 是网络 I/O,等待响应时 CPU 空闲。ThreadPoolExecutor 在等待 API 响应期间切换到其他线程处理别的 Excel,是最佳选择。

Python 的 GIL(全局解释器锁)在 I/O 等待时会释放,所以多线程对 I/O 密集任务有效。纯 CPU 密集任务才需要多进程绕过 GIL。

线程数选择

# 经验公式
import os
# I/O 密集:线程数可以远大于 CPU 核数
# 常见经验值:min(文件数, CPU核数 × 5 ~ 10)
optimal_threads = min(len(files), (os.cpu_count() or 4) * 5)
文件数 建议线程数 说明
1-5 1-5 一一对应即可
6-20 5-10 控制在 API 限流范围内
20-50 10-15 注意 API 限流,必要时加限速
50+ 15-20 配合分批 + 限速

线程数不是越大越好——后端 API 有并发限制(如 QPS 限制),线程太多会触发限流反而变慢。

4. 完整实现

4.1 工具定义

{
  "name": "import_routes_batch",
  "description": "批量导入多个 Excel 文件中的航线数据,并发处理并返回汇总结果",
  "parameters": {
    "type": "object",
    "properties": {
      "files": {
        "type": "array",
        "items": {"type": "string"},
        "description": "Excel 文件路径列表"
      },
      "concurrency": {
        "type": "integer",
        "description": "并发线程数,默认 5",
        "default": 5
      },
      "module_name": {
        "type": "string",
        "description": "航线模块名称"
      }
    },
    "required": ["files", "module_name"]
  }
}

4.2 Python 并发导入脚本

import concurrent.futures
import time
import traceback
from dataclasses import dataclass, field
from typing import Optional

# ============================================================
# 数据结构定义
# ============================================================

@dataclass
class FileResult:
    """单个文件的处理结果"""
    file: str
    success: bool
    total: int = 0          # 该文件中航线总数
    imported: int = 0       # 成功导入数
    failed: int = 0         # 失败数
    duration: float = 0.0   # 耗时(秒)
    errors: list = field(default_factory=list)  # 失败详情


@dataclass
class BatchResult:
    """批量处理汇总结果"""
    total_files: int = 0
    success_files: int = 0
    failed_files: int = 0
    total_routes: int = 0
    imported_routes: int = 0
    failed_routes: int = 0
    total_duration: float = 0.0
    details: list = field(default_factory=list)  # 每个文件的 FileResult


# ============================================================
# 单文件处理函数(每个线程执行一次)
# ============================================================

def import_single_excel(file_path: str, module_name: str) -> FileResult:
    """
    处理单个 Excel 文件:读取 → 解析 → 逐条调 API 创建航线
    这个函数会被线程池并发调用
    """
    result = FileResult(file=file_path, success=False)
    start = time.time()

    try:
        # 1. 读取 Excel
        import openpyxl
        wb = openpyxl.load_workbook(file_path, read_only=True)
        ws = wb.active
        routes = []
        for row in ws.iter_rows(min_row=2, values_only=True):  # 跳过表头
            if row[0] is not None:
                routes.append({
                    "name": row[0],
                    "origin": row[1],
                    "destination": row[2],
                    "departure": row[3],
                })
        wb.close()

        result.total = len(routes)
        print(f"[{file_path}] 解析到 {result.total} 条航线")

        # 2. 逐条调 API 创建航线
        # 这里用 create_route_via_api 模拟 API 调用
        for i, route in enumerate(routes):
            try:
                ok = create_route_via_api(module_name, route)
                if ok:
                    result.imported += 1
                else:
                    result.failed += 1
                    result.errors.append(f"第 {i+2} 行: API 返回失败")
            except Exception as e:
                result.failed += 1
                result.errors.append(f"第 {i+2} 行: {str(e)}")

        result.success = result.failed == 0
        if result.failed > 0 and result.imported > 0:
            result.success = True  # 部分成功也算成功,详情看 errors

    except Exception as e:
        result.errors.append(f"文件处理异常: {traceback.format_exc()}")
    finally:
        result.duration = round(time.time() - start, 2)

    return result


def create_route_via_api(module_name: str, route: dict) -> bool:
    """调用 API 创建单条航线(模拟)"""
    import requests
    resp = requests.post(
        "https://api.example.com/routes",
        json={"module": module_name, **route},
        timeout=30,
    )
    return resp.status_code == 200


# ============================================================
# 并发调度 + 结果汇总(核心)
# ============================================================

def import_routes_batch(files: list, module_name: str, concurrency: int = 5) -> BatchResult:
    """
    批量并发导入航线,汇总结果
    """
    batch = BatchResult(total_files=len(files))
    overall_start = time.time()

    # 使用线程池并发处理
    with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
        # 提交所有任务
        future_to_file = {
            executor.submit(import_single_excel, f, module_name): f
            for f in files
        }

        # as_completed:哪个任务先完成就先处理,不按提交顺序等待
        for future in concurrent.futures.as_completed(future_to_file):
            file_path = future_to_file[future]
            try:
                result: FileResult = future.result()
                batch.details.append(result)

                # 汇总计数
                if result.success:
                    batch.success_files += 1
                else:
                    batch.failed_files += 1
                batch.total_routes += result.total
                batch.imported_routes += result.imported
                batch.failed_routes += result.failed

                # 实时进度(输出到 stderr 供宿主捕获)
                done = len(batch.details)
                print(f"进度: {done}/{batch.total_files} | "
                      f"成功 {batch.imported_routes} / 失败 {batch.failed_routes}")

            except Exception as e:
                batch.failed_files += 1
                batch.details.append(FileResult(
                    file=file_path, success=False,
                    errors=[f"线程异常: {traceback.format_exc()}"]
                ))

    batch.total_duration = round(time.time() - overall_start, 2)

    # 排序:按文件名排序,让结果可读
    batch.details.sort(key=lambda x: x.file)

    return batch


# ============================================================
# 结果格式化(返回给 LLM)
# ============================================================

def format_batch_result(batch: BatchResult) -> dict:
    """将汇总结果格式化为 LLM 可读的结构化返回"""
    return {
        "summary": {
            "total_files": batch.total_files,
            "success_files": batch.success_files,
            "failed_files": batch.failed_files,
            "total_routes": batch.total_routes,
            "imported_routes": batch.imported_routes,
            "failed_routes": batch.failed_routes,
            "duration_seconds": batch.total_duration,
        },
        "details": [
            {
                "file": r.file,
                "success": r.success,
                "total": r.total,
                "imported": r.imported,
                "failed": r.failed,
                "duration": r.duration,
                "errors": r.errors[:5],  # 每个文件最多保留 5 条错误,避免过长
            }
            for r in batch.details
        ],
    }


# ============================================================
# 入口
# ============================================================

if __name__ == "__main__":
    files = [
        "routes_01.xlsx", "routes_02.xlsx", "routes_03.xlsx",
        "routes_04.xlsx", "routes_05.xlsx", "routes_06.xlsx",
        "routes_07.xlsx", "routes_08.xlsx", "routes_09.xlsx",
        "routes_10.xlsx", "routes_11.xlsx", "routes_12.xlsx",
    ]

    batch = import_routes_batch(files, module_name="华东航线", concurrency=5)
    report = format_batch_result(batch)

    # 打印汇总(实际场景中由工具框架返回给 LLM)
    import json
    print("\n========== 汇总结果 ==========")
    print(json.dumps(report["summary"], ensure_ascii=False, indent=2))

    print("\n========== 各文件详情 ==========")
    for d in report["details"]:
        status = "✓" if d["success"] else "✗"
        print(f"  {status} {d['file']}: {d['imported']}/{d['total']} 成功, "
              f"{d['failed']} 失败, {d['duration']}s")
        for err in d["errors"]:
            print(f"      → {err}")

4.3 执行流程图

LLM 调用 import_routes_batch(files=[12个文件], concurrency=5)
  │
  ▼
Python: ThreadPoolExecutor(max_workers=5)
  │
  ├── 线程1: import_single_excel("routes_01.xlsx") ──→ FileResult
  ├── 线程2: import_single_excel("routes_02.xlsx") ──→ FileResult
  ├── 线程3: import_single_excel("routes_03.xlsx") ──→ FileResult
  ├── 线程4: import_single_excel("routes_04.xlsx") ──→ FileResult
  ├── 线程5: import_single_excel("routes_05.xlsx") ──→ FileResult
  │   ... (前5个完成后,线程空出继续处理剩余文件)
  ├── 线程N: import_single_excel("routes_12.xlsx") ──→ FileResult
  │
  ▼
as_completed 逐个收集结果 → 汇总到 BatchResult
  │
  ▼
format_batch_result → 返回结构化 JSON 给 LLM
  │
  ▼
LLM 基于汇总结果继续推理(告知用户导入完成情况)

5. 结果汇总策略

5.1 as_completed vs map

# 方式一:as_completed(推荐)—— 谁先完成谁先处理
with ThreadPoolExecutor(max_workers=5) as executor:
    futures = {executor.submit(fn, f): f for f in files}
    for future in as_completed(futures):
        result = future.result()  # 拿到已完成任务的结果
        # 可以实时输出进度

# 方式二:map —— 按提交顺序返回结果
with ThreadPoolExecutor(max_workers=5) as executor:
    results = executor.map(fn, files)  # 返回迭代器,按 files 顺序 yield
    for r in results:
        # 即使第3个先完成,也要等第1、2个完成才能拿到
对比 as_completed map
返回顺序 完成顺序(随机) 提交顺序
实时进度 ✅ 可实时输出 ❌ 必须按序等
异常处理 ✅ 逐个 try/catch ❌ 一个异常中断迭代
推荐度 ✅ 批量场景首选 简单场景可用

5.2 部分失败的容错策略

# 每个文件独立容错,一个失败不影响其他
for future in concurrent.futures.as_completed(future_to_file):
    try:
        result = future.result(timeout=300)  # 单文件超时 5 分钟
    except concurrent.futures.TimeoutError:
        # 该文件处理超时,标记失败,继续处理其他
        result = FileResult(file=file_path, success=False,
                           errors=["处理超时(300s)"])
    except Exception as e:
        # 该文件处理异常,标记失败,继续处理其他
        result = FileResult(file=file_path, success=False,
                           errors=[f"异常: {str(e)}"])

    batch.details.append(result)  # 无论成败都记录

5.3 返回给 LLM 的结果裁剪

def format_batch_result(batch: BatchResult) -> dict:
    return {
        "summary": {
            "total_files": batch.total_files,
            "success_files": batch.success_files,
            "failed_files": batch.failed_files,
            "total_routes": batch.total_routes,
            "imported_routes": batch.imported_routes,
            "failed_routes": batch.failed_routes,
            "duration_seconds": batch.total_duration,
        },
        # 只返回失败的详情,成功的只计数量
        # 避免上下文膨胀
        "failed_details": [
            {"file": r.file, "errors": r.errors[:5]}
            for r in batch.details if not r.success
        ],
    }

返回裁剪原则:LLM 不需要看每个文件的每条成功记录。汇总数字 + 失败详情即可。成功详情让用户看工具日志。

6. 进阶:单文件内部也并发

上面的方案是「文件级并发」——每个 Excel 一个线程。如果单个 Excel 内航线很多(如 1000 条),可以在文件内部再做一层并发:

def import_single_excel(file_path: str, module_name: str) -> FileResult:
    # ... 解析 Excel 得到 routes 列表 ...

    # 文件内部:用线程池并发调 API
    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as api_executor:
        futures = {
            api_executor.submit(create_route_via_api, module_name, route): i
            for i, route in enumerate(routes)
        }
        for future in concurrent.futures.as_completed(futures):
            idx = futures[future]
            try:
                if future.result():
                    result.imported += 1
                else:
                    result.failed += 1
                    result.errors.append(f"第 {idx+2} 行: API 返回失败")
            except Exception as e:
                result.failed += 1
                result.errors.append(f"第 {idx+2} 行: {str(e)}")

    return result

两层并发的线程数协调

外层:5 个线程处理文件
内层:每个文件 10 个线程调 API
总并发:5 × 10 = 50 个 API 请求

→ 需确认后端 API 能承受 50 并发,否则触发限流
# 建议:总并发线程数控制在 API 限流范围内
# 假设 API 限制 30 QPS
file_concurrency = 3    # 外层 3 个文件并发
api_concurrency = 10    # 每个文件内 10 个 API 并发
# 总并发 = 3 × 10 = 30,刚好不超限

7. 进阶:API 限速控制

当并发量大时,需要加限速避免打崩后端:

import threading
import time

class RateLimiter:
    """简单的令牌桶限速器"""
    def __init__(self, max_calls: int, period: float = 1.0):
        self.max_calls = max_calls  # 每周期最大调用数
        self.period = period         # 周期(秒)
        self.calls = []              # 最近调用的时间戳
        self.lock = threading.Lock()

    def acquire(self):
        with self.lock:
            now = time.time()
            # 清理过期的时间戳
            self.calls = [t for t in self.calls if now - t < self.period]
            if len(self.calls) >= self.max_calls:
                # 需要等待
                sleep_time = self.period - (now - self.calls[0])
                if sleep_time > 0:
                    time.sleep(sleep_time)
            self.calls.append(time.time())


# 使用
rate_limiter = RateLimiter(max_calls=30, period=1.0)  # 30 QPS

def create_route_via_api(module_name: str, route: dict) -> bool:
    rate_limiter.acquire()  # 限速
    import requests
    resp = requests.post("https://api.example.com/routes",
                         json={"module": module_name, **route}, timeout=30)
    return resp.status_code == 200

8. 进阶:用 asyncio 替代线程池

如果整个工具链是异步的,用 asyncio 更高效(协程比线程更轻量):

import asyncio
import aiohttp

async def create_route_via_api_async(session, module_name, route):
    async with session.post(
        "https://api.example.com/routes",
        json={"module": module_name, **route},
        timeout=aiohttp.ClientTimeout(total=30),
    ) as resp:
        return resp.status == 200


async def import_single_excel_async(file_path, module_name, session):
    # ... 解析 Excel ...
    result = FileResult(file=file_path, success=False)
    # 用 semaphore 控制单文件内 API 并发度
    sem = asyncio.Semaphore(10)
    async def limited_create(route):
        async with sem:
            return await create_route_via_api_async(session, module_name, route)

    tasks = [limited_create(r) for r in routes]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    # ... 汇总 ...
    return result


async def import_routes_batch_async(files, module_name, concurrency=5):
    # 用 semaphore 控制文件级并发度
    sem = asyncio.Semaphore(concurrency)
    async with aiohttp.ClientSession() as session:
        async def limited_import(file_path):
            async with sem:
                return await import_single_excel_async(file_path, module_name, session)

        tasks = [limited_import(f) for f in files]
        results = await asyncio.gather(*tasks, return_exceptions=True)
    # ... 汇总 ...
    return results


# 入口
if __name__ == "__main__":
    results = asyncio.run(import_routes_batch_async(files, "华东航线", concurrency=5))
对比 ThreadPoolExecutor asyncio
学习成本 低(同步代码) 中(异步编程)
性能 好(I/O 密集) 更好(协程更轻量)
库支持 所有同步库 需 async 版库(aiohttp 等)
混用 可直接调用同步/异步 同步库需 run_in_executor 包装
推荐场景 已有同步代码、快速实现 全新建、高并发

如果已有同步的 requests 代码,先用 ThreadPoolExecutor 最快落地。如果从头建且追求性能,用 asyncio

9. 与 Agent/Skill 的集成

完整调用链

Agent (LLM)
  │
  │  tool_call: import_routes_batch(files=[...], concurrency=5)
  │
  ▼
Skill 执行器
  │
  │  调用 Python 脚本:python import_routes.py --files ... --concurrency 5
  │
  ▼
Python 脚本
  │
  │  ThreadPoolExecutor 并发处理
  │  ├── 线程1: 读Excel → 解析 → 调API
  │  ├── 线程2: 读Excel → 解析 → 调API
  │  └── ...
  │
  │  as_completed 收集 → 汇总 → format_batch_result
  │
  ▼
返回结构化 JSON → Skill 执行器 → 回传 LLM
  │
  ▼
LLM 基于汇总结果回复用户:
  "12 个文件全部处理完成,共导入 3,600 条航线,其中 15 条失败。
   失败详情:routes_03.xlsx 第 5 行 API 超时,routes_07.xlsx 第 12 行..."

Skill 配置示例(SKILL.md 片段)

---
name: route-import
description: 批量导入航线数据,支持多 Excel 并发处理
---

## 工具

### import_routes_batch
批量导入多个 Excel 文件中的航线数据。

**参数:**
- `files`(array<string>, 必填):Excel 文件路径列表
- `module_name`(string, 必填):航线模块名称
- `concurrency`(int, 默认 5):并发线程数

**执行:**
```bash
python scripts/import_routes.py \
  --files "{{files}}" \
  --module "{{module_name}}" \
  --concurrency {{concurrency}}

返回:

{
  "summary": {
    "total_files": 12,
    "success_files": 11,
    "failed_files": 1,
    "total_routes": 3600,
    "imported_routes": 3585,
    "failed_routes": 15
  },
  "failed_details": [...]
}

## 10. 注意事项

### 线程安全
- `requests` / `openpyxl` / `pandas` 读操作都是线程安全的。
- **写操作注意**:多线程写同一个文件需加锁;写不同文件无需加锁。
- 全局计数器需加锁或用 `threading.Lock`:
  ```python
  counter_lock = threading.Lock()
  with counter_lock:
      batch.imported_routes += 1

实际上 as_completed 在主线程汇总,子线程只返回自己的 FileResult,不需要全局锁。

超时与重试

  • 每个文件设独立超时,避免一个卡死全部等待:
    result = future.result(timeout=300)  # 单文件最多 5 分钟
    
  • API 调用加重试(网络抖动常见):
    from tenacity import retry, stop_after_attempt, wait_exponential
    
    @retry(stop=stop_after_attempt(3), wait=wait_exponential(min=1, max=10))
    def create_route_via_api(...):
        ...
    

资源控制

  • 线程数不是越大越好:受 API 限流、数据库连接池、内存限制约束。
  • 大文件注意内存read_only=True 模式读 Excel 可减少内存占用。
  • 进程数 vs 线程数:I/O 密集用线程(共享内存、轻量);CPU 密集用进程(绕过 GIL)。

错误隔离

  • 每个文件的处理用 try/except 包裹,一个文件失败不影响其他。
  • as_completed + future.result() 的异常不会传播到其他 future。
  • 汇总时区分「文件级失败」(Excel 损坏/格式错)和「航线级失败」(单条 API 调用失败)。

日志与可观测

  • 每个线程输出带文件名的日志,便于排查:
    import logging
    logger = logging.getLogger(f"import.{file_path}")
    logger.info(f"开始处理,共 {total} 条航线")
    
  • 实时进度输出到 stderr(宿主可捕获显示),最终结果输出到 stdout。

幂等性

  • 如果可能重试(如 Agent 重新调用),确保导入操作幂等:
    • 导入前检查航线是否已存在(按唯一标识查)。
    • API 层面用 upsert(存在则更新,不存在则创建)。
    • 或用批次 ID 标记,重试时先清理上次的部分导入。

11. 方案对比总结

维度 串行 LLM 层并行 (方案A) 工具内部并发 (方案B) asyncio
实现复杂度 最低 中高
LLM 交互次数 N 次 1 次 1 次 1 次
并发可控
结果汇总 手动 宿主处理 工具内自动 工具内自动
依赖模型能力 ✅ 需并行调用
适用文件数 1-3 3-5 5-50+ 50+
推荐度 ✅ 首选 △ 高性能场景

12. 学习要点

  • 并发在哪一层做是关键决策:LLM 层(并行 Function Calling)vs 工具层(Python 线程池)。文件多时工具层并发更可控。
  • I/O 密集用线程池:航线导入的瓶颈在调 API(网络 I/O),ThreadPoolExecutor 是最简单有效的方案。
  • as_completed 收集结果:谁先完成谁先处理,可实时输出进度,异常隔离好。
  • 结果裁剪返回 LLM:汇总数字 + 失败详情,不要把所有成功记录塞回 LLM 上下文。
  • 控制总并发量:外层文件并发 × 内层 API 并发 ≤ 后端 API 限制,必要时加限速器。
  • 每个任务独立容错:一个文件/一条航线失败不影响其他,失败记录在汇总结果中。

13. 参考资料

  • Python concurrent.futures 官方文档
  • Python asyncio 官方文档
  • GIL 与 Python 并发模型
  • tenacity 重试库文档
  • 本系列《01-Function Calling》第 5 节「多函数调用」