Skip to content

第 1 章 · Python 异步编程与 FastAPI 生产进阶

本章目标:

  • 掌握 asyncio 核心概念:event loop、Task、gather、async with、async for
  • 理解 aiohttp 异步 HTTP 客户端,用于 LLM API 调用
  • 实现生产级限流、重试(tenacity)和熔断模式
  • 区分 BackgroundTasks 与消息队列的适用边界

1.1 asyncio 核心概念

Python 的 asyncio 是单线程并发模型,通过 event loop 调度协程。理解以下概念是构建高并发 Agent 服务的基础:

python
import asyncio

async def fetch_data(name: str, delay: float) -> str:
    """模拟异步数据获取"""
    await asyncio.sleep(delay)
    return f"{name}: {delay}s"

async def main():
    # 创建多个任务并发执行
    tasks = [fetch_data("A", 0.5), fetch_data("B", 0.3), fetch_data("C", 0.8)]
    
    # gather 并发运行所有任务,返回结果列表
    results = await asyncio.gather(*tasks)
    for r in results:
        print(r)
    
    # 单个任务创建与等待
    task = asyncio.create_task(fetch_data("D", 1.0))
    result = await task
    print(result)

asyncio.run(main())

关键 API

  • asyncio.run(coro):运行顶层协程
  • asyncio.create_task(coro):调度协程并发执行
  • asyncio.gather(*coros):并发运行多个协程,收集结果
  • asyncio.wait_for(coro, timeout):带超时的等待
  • async with / async for:异步上下文管理与迭代

1.2 aiohttp:异步 HTTP 客户端

Agent 服务需要异步调用 LLM API(如 Vercel AI Gateway),aiohttp 是首选:

python
import aiohttp
import asyncio

async def call_llm_async(prompt: str) -> str:
    """异步调用 LLM(示例用 httpbin 替代实际 API)"""
    async with aiohttp.ClientSession() as session:
        async with session.post(
            "https://httpbin.org/post",
            json={"prompt": prompt},
            timeout=aiohttp.ClientTimeout(total=30)
        ) as resp:
            data = await resp.json()
            return data.get("json", {}).get("prompt", "")

# 并发调用多个 LLM
async def main():
    prompts = ["你好", "解释量子计算", "写一首诗"]
    tasks = [call_llm_async(p) for p in prompts]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for r in results:
        print(r if isinstance(r, str) else f"错误: {r}")

asyncio.run(main())

1.3 生产级限流:Token Bucket 实现

python
import asyncio
import time
from typing import Dict

class RateLimiter:
    """Token Bucket 限流器"""
    
    def __init__(self, rate: float, burst: int):
        self.rate = rate  # 每秒允许的请求数
        self.burst = burst  # 最大突发量
        self._tokens = burst
        self._last_refill = time.monotonic()
        self._lock = asyncio.Lock()
    
    async def acquire(self) -> None:
        async with self._lock:
            now = time.monotonic()
            elapsed = now - self._last_refill
            self._tokens = min(self.burst, self._tokens + elapsed * self.rate)
            self._last_refill = now
            
            if self._tokens < 1:
                wait_time = (1 - self._tokens) / self.rate
                await asyncio.sleep(wait_time)
                self._tokens = 0
            else:
                self._tokens -= 1

async def limited_llm_call(prompt: str, limiter: RateLimiter) -> str:
    await limiter.acquire()  # 等待令牌
    return f"response to: {prompt}"

async def main():
    limiter = RateLimiter(rate=5.0, burst=10)  # 5 req/s,最大突发 10
    tasks = [limited_llm_call(f"prompt-{i}", limiter) for i in range(20)]
    results = await asyncio.gather(*tasks)
    print(f"完成 {len(results)} 个请求")

asyncio.run(main())

1.4 重试策略:tenacity

python
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import asyncio

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10),
    retry=retry_if_exception_type((asyncio.TimeoutError, ConnectionError)),
    before_sleep=lambda retry_state: print(f"重试 {retry_state.attempt_number}...")
)
async def call_llm_with_retry(prompt: str, timeout: float = 5.0) -> str:
    """带指数退避重试的 LLM 调用"""
    try:
        return await asyncio.wait_for(_actual_call(prompt), timeout=timeout)
    except asyncio.TimeoutError:
        raise  # 超时触发重试

async def _actual_call(prompt: str) -> str:
    await asyncio.sleep(0.1)
    return f"result for: {prompt}"

async def main():
    try:
        result = await call_llm_with_retry("hello")
        print(result)
    except Exception as e:
        print(f"最终失败: {e}")

asyncio.run(main())

1.5 熔断器模式

python
import asyncio
import time
from enum import Enum

class CircuitState(Enum):
    CLOSED = "closed"      # 正常
    OPEN = "open"          # 熔断
    HALF_OPEN = "half_open" # 半开

class CircuitBreaker:
    def __init__(self, failure_threshold: int = 5, reset_timeout: float = 30.0):
        self.failure_threshold = failure_threshold
        self.reset_timeout = reset_timeout
        self._state = CircuitState.CLOSED
        self._failures = 0
        self._last_failure_time = 0.0
        self._lock = asyncio.Lock()
    
    @property
    def state(self) -> CircuitState:
        return self._state
    
    async def call(self, func, *args, **kwargs):
        async with self._lock:
            if self._state == CircuitState.OPEN:
                if time.monotonic() - self._last_failure_time > self.reset_timeout:
                    self._state = CircuitState.HALF_OPEN
                    print("熔断器:半开状态,允许一次试探请求")
                else:
                    raise RuntimeError("熔断器已打开,服务不可用")
        
        try:
            result = await func(*args, **kwargs)
            async with self._lock:
                if self._state == CircuitState.HALF_OPEN:
                    self._state = CircuitState.CLOSED
                    self._failures = 0
                    print("熔断器:恢复关闭状态")
            return result
        except Exception as e:
            async with self._lock:
                self._failures += 1
                self._last_failure_time = time.monotonic()
                if self._failures >= self.failure_threshold:
                    self._state = CircuitState.OPEN
                    print(f"熔断器:达到 {self.failure_threshold} 次失败,打开熔断")
            raise

async def main():
    cb = CircuitBreaker(failure_threshold=3, reset_timeout=2.0)
    
    async def flaky_service():
        raise ConnectionError("服务不可用")
    
    for i in range(5):
        try:
            await cb.call(flaky_service)
        except Exception as e:
            print(f"请求 {i+1} 失败: {e}")
        await asyncio.sleep(0.1)

asyncio.run(main())

1.6 BackgroundTasks vs 消息队列的边界

场景方案原因
轻量异步任务(发送通知邮件)FastAPI BackgroundTasks同进程内,简单快速
长时间 LLM 调用Celery Worker + Redis Queue进程隔离、可重试、持久化
高并发 Agent 任务SQS + Fargate Worker水平扩展、分布式协调

💡 详见 FastAPI ch16 关于 BackgroundTasks 的完整讲解。

本章小结

  • asyncio 是单线程并发模型,核心 API 包括 gathercreate_tasksleep
  • aiohttp 用于异步 HTTP 调用,适合 LLM API 并发请求
  • 生产级限流用 Token Bucket,重试用 tenacity(指数退避),熔断用三态机
  • 区分 BackgroundTasks(轻量)与消息队列(重任务)的使用边界

🛠️ 动手实践

  1. 实现一个异步 LLM 调用服务,使用 aiohttp + 自定义 RateLimiter,限制每秒最多 3 个请求
  2. 给上述服务加上 tenacity 重试(最多 3 次,指数退避 1-8 秒)
  3. 实现 CircuitBreaker 熔断器,连续 3 次失败后打开,30 秒后自动半开

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. asyncio.gather() 的主要作用是什么?

2. Token Bucket 限流器中,burst 参数表示什么?

3. tenacity 的 wait_exponential 如何实现退避?

4. CircuitBreaker 的 HALF_OPEN 状态用于什么场景?