(专栏:Python 从真零基础到纯文本 LLM 全栈实战・第 9 篇 | 字数:13000 字 | 零基础友好 | LLM 场景深度绑定 | 代码可运行)
开篇:LLM API 的 “裸奔” 困境
你有没有遇到过这样的问题?
- 直接暴露 LLM API,被恶意调用导致费用飙升
- LLM API 调用出现问题,不知道是谁调用的、什么时候调用的、调用了什么内容
- 想要给 LLM API 添加身份验证、日志记录、限流等功能,却要修改大量原有代码
- 不同的 LLM API 需要重复添加相同的功能,代码冗余
这些问题的核心是LLM API 缺乏安全防护和可观测性,而Python 装饰器就是解决这些问题的 “魔术函数”—— 它能帮你:
- 增强安全性:添加身份验证、API 密钥验证、请求限流等
- 提升可观测性:记录请求日志、响应时间、Token 消耗等
- 减少代码冗余:将公共功能封装为装饰器,一次定义,多次使用
- 不修改原有代码:在不改变原有 API 代码的情况下增强功能
本文将从LLM API 的真实应用场景出发,系统讲解 Python 装饰器的核心技术,并结合身份验证、日志记录、限流、Token 统计等实战需求给出代码示例。
一、核心概念:装饰器的基础认知
1.1 什么是装饰器?
装饰器是一种用于修改或增强函数或类功能的函数,它接受一个函数作为参数,并返回一个新的函数,新函数包含了原函数的功能和装饰器添加的功能。
1.2 装饰器的基本语法
# 定义装饰器
def decorator(func):
def wrapper():
# 在原函数执行前添加的功能
print("Before function execution")
# 执行原函数
func()
# 在原函数执行后添加的功能
print("After function execution")
return wrapper
# 使用装饰器
@decorator
def hello():
print("Hello, LLM!")
# 调用函数
hello()
1.3 运行结果
Before function execution
Hello, LLM!
After function execution
1.4 装饰器的工作原理
装饰器的工作原理是函数的嵌套和闭包:
- 当定义
@decorator时,Python 会将被装饰的函数hello作为参数传递给decorator函数 decorator函数返回wrapper函数- 当调用
hello()时,实际上是调用wrapper()函数
二、核心操作:装饰器的基本使用
2.1 装饰器带参数
如果被装饰的函数带有参数,需要在wrapper函数中接收并传递这些参数。
def decorator(func):
def wrapper(*args, **kwargs):
print("Before function execution")
result = func(*args, **kwargs) # 传递参数
print("After function execution")
return result # 返回结果
return wrapper
@decorator
def add(a, b):
return a + b
result = add(1, 2)
print(f"Result: {result}")
2.2 装饰器带返回值
如果被装饰的函数有返回值,需要在wrapper函数中捕获并返回。
def decorator(func):
def wrapper(*args, **kwargs):
print("Before function execution")
result = func(*args, **kwargs)
print("After function execution")
return result
return wrapper
@decorator
def multiply(a, b):
return a * b
result = multiply(3, 4)
print(f"Result: {result}")
2.3 装饰器本身带参数
如果装饰器需要传递参数,需要在原装饰器外再嵌套一层函数。
def decorator_with_args(prefix):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"{prefix} Before function execution")
result = func(*args, **kwargs)
print(f"{prefix} After function execution")
return result
return wrapper
return decorator
@decorator_with_args("DEBUG:")
def divide(a, b):
return a / b
result = divide(10, 2)
print(f"Result: {result}")
2.4 类装饰器
除了函数装饰器,还可以使用类装饰器,将装饰器封装为一个类。
class Decorator:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("Before function execution")
result = self.func(*args, **kwargs)
print("After function execution")
return result
@Decorator
def subtract(a, b):
return a - b
result = subtract(5, 3)
print(f"Result: {result}")
三、进阶技巧:装饰器的高级应用
3.1 保留原函数的元信息
使用装饰器后,原函数的元信息(如函数名、文档字符串等)会丢失,可以使用functools.wraps保留这些信息。
from functools import wraps
def decorator(func):
@wraps(func) # 保留原函数的元信息
def wrapper(*args, **kwargs):
"""Wrapper function"""
result = func(*args, **kwargs)
return result
return wrapper
@decorator
def hello():
"""Hello function"""
print("Hello, LLM!")
print(f"Function name: {hello.__name__}")
print(f"Function docstring: {hello.__doc__}")
3.2 多个装饰器的执行顺序
当一个函数被多个装饰器装饰时,装饰器的执行顺序是从下到上。
def decorator1(func):
def wrapper():
print("Decorator 1 Before")
func()
print("Decorator 1 After")
return wrapper
def decorator2(func):
def wrapper():
print("Decorator 2 Before")
func()
print("Decorator 2 After")
return wrapper
@decorator1
@decorator2
def hello():
print("Hello, LLM!")
hello()
3.3 装饰器的执行结果
Decorator 1 Before
Decorator 2 Before
Hello, LLM!
Decorator 2 After
Decorator 1 After
四、LLM API 实战:装饰器的安全与可观测性增强
在 LLM API 开发中,装饰器主要用于以下场景:
- API 密钥验证:确保只有合法的用户才能调用 API
- 身份验证:验证用户的身份信息
- 请求限流:限制 API 的调用频率,防止恶意请求
- 日志记录:记录 API 的请求和响应信息
- Token 统计:统计 LLM API 的 Token 消耗
- 异常处理:统一处理 API 的异常
4.1 实战 1:API 密钥验证装饰器
4.1.1 需求
为 LLM API 添加 API 密钥验证,只有提供正确的 API 密钥才能调用。
4.1.2 实现代码
from functools import wraps
# 合法的API密钥
VALID_API_KEYS = {"sk-123456", "ak-789012"}
def api_key_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 假设API密钥是通过kwargs传递的
if "api_key" not in kwargs:
return {"error": "Missing API key"}, 401
api_key = kwargs.pop("api_key")
if api_key not in VALID_API_KEYS:
return {"error": "Invalid API key"}, 401
# 调用原函数
return func(*args, **kwargs)
return wrapper
# 使用装饰器
@api_key_required
def call_llm(prompt, model="gpt-3.5-turbo"):
"""调用LLM API"""
return {"response": f"LLM response to: {prompt}", "model": model}, 200
# 测试调用
print("测试1:缺少API密钥")
result, status = call_llm("什么是LLM?")
print(f"结果:{result},状态码:{status}")
print("\n测试2:无效API密钥")
result, status = call_llm("什么是LLM?", api_key="invalid")
print(f"结果:{result},状态码:{status}")
print("\n测试3:有效API密钥")
result, status = call_llm("什么是LLM?", api_key="sk-123456")
print(f"结果:{result},状态码:{status}")
4.1.3 测试结果
测试1:缺少API密钥
结果:{'error': 'Missing API key'},状态码:401
测试2:无效API密钥
结果:{'error': 'Invalid API key'},状态码:401
测试3:有效API密钥
结果:{'response': 'LLM response to: 什么是LLM?', 'model': 'gpt-3.5-turbo'},状态码:200
4.2 实战 2:请求日志记录装饰器
4.2.1 需求
为 LLM API 添加日志记录,记录请求的时间、API 路径、请求参数、响应内容等。
4.2.2 实现代码
from functools import wraps
import datetime
import logging
# 配置日志
logging.basicConfig(
filename="llm_api.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
def log_request(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 记录请求时间
request_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 记录请求信息
logging.info(f"Request at {request_time}: function={func.__name__}, args={args}, kwargs={kwargs}")
# 调用原函数
result, status = func(*args, **kwargs)
# 记录响应信息
logging.info(f"Response at {request_time}: result={result}, status={status}")
return result, status
return wrapper
# 使用装饰器
@log_request
@api_key_required
def call_llm(prompt, model="gpt-3.5-turbo"):
return {"response": f"LLM response to: {prompt}", "model": model}, 200
# 测试调用
result, status = call_llm("什么是LLM?", api_key="sk-123456")
print(f"结果:{result},状态码:{status}")
4.2.3 日志文件内容
2024-12-15 14:30:00 - INFO - Request at 2024-12-15 14:30:00: function=call_llm, args=('什么是LLM?',), kwargs={'api_key': 'sk-123456'}
2024-12-15 14:30:00 - INFO - Response at 2024-12-15 14:30:00: result={'response': 'LLM response to: 什么是LLM?', 'model': 'gpt-3.5-turbo'}, status=200
4.3 实战 3:请求限流装饰器
4.3.1 需求
为 LLM API 添加请求限流,限制每个 API 密钥每分钟最多调用 10 次。
4.3.2 实现代码
from functools import wraps
from collections import defaultdict
import time
# 限流字典:{api_key: [(请求时间1), (请求时间2)...]}
request_count = defaultdict(list)
# 限流配置:每分钟最多调用10次
RATE_LIMIT = 10
TIME_WINDOW = 60 # 秒
def rate_limit(func):
@wraps(func)
def wrapper(*args, **kwargs):
if "api_key" not in kwargs:
return {"error": "Missing API key"}, 401
api_key = kwargs["api_key"]
current_time = time.time()
# 清除过期的请求记录
request_count[api_key] = [t for t in request_count[api_key] if current_time - t < TIME_WINDOW]
# 检查请求次数是否超过限制
if len(request_count[api_key]) >= RATE_LIMIT:
return {"error": "Rate limit exceeded"}, 429
# 添加当前请求记录
request_count[api_key].append(current_time)
# 调用原函数
return func(*args, **kwargs)
return wrapper
# 使用装饰器
@rate_limit
@api_key_required
@log_request
def call_llm(prompt, model="gpt-3.5-turbo"):
return {"response": f"LLM response to: {prompt}", "model": model}, 200
# 测试限流
print("测试限流:")
for i in range(12):
result, status = call_llm("什么是LLM?", api_key="sk-123456")
print(f"第{i+1}次调用:结果={result}, 状态码={status}")
time.sleep(5) # 每5秒调用一次
4.3.3 测试结果
测试限流:
第1次调用:结果={'response': 'LLM response to: 什么是LLM?', 'model': 'gpt-3.5-turbo'}, 状态码=200
...
第10次调用:结果={'response': 'LLM response to: 什么是LLM?', 'model': 'gpt-3.5-turbo'}, 状态码=200
第11次调用:结果={'error': 'Rate limit exceeded'}, 状态码=429
第12次调用:结果={'error': 'Rate limit exceeded'}, 状态码=429
4.4 实战 4:Token 统计装饰器
4.4.1 需求
为 LLM API 添加 Token 统计,统计每个请求的 Token 消耗。
4.4.2 实现代码
from functools import wraps
from transformers import AutoTokenizer
# 加载Token计数器
tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-roberta-wwm-ext")
def token_counter(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 假设第一个参数是prompt
prompt = args[0]
# 统计Token数量
token_count = len(tokenizer.tokenize(prompt))
# 调用原函数
result, status = func(*args, **kwargs)
# 将Token数量添加到响应中
result["token_count"] = token_count
return result, status
return wrapper
# 使用装饰器
@token_counter
@rate_limit
@api_key_required
@log_request
def call_llm(prompt, model="gpt-3.5-turbo"):
return {"response": f"LLM response to: {prompt}", "model": model}, 200
# 测试Token统计
result, status = call_llm("什么是LLM?", api_key="sk-123456")
print(f"结果:{result},状态码:{status}")
4.4.3 测试结果
结果:{'response': 'LLM response to: 什么是LLM?', 'model': 'gpt-3.5-turbo', 'token_count': 7},状态码:200
4.5 实战 5:异常处理装饰器
4.5.1 需求
为 LLM API 添加异常处理,统一处理 API 的异常。
4.5.2 实现代码
from functools import wraps
def handle_exception(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
return {"error": str(e), "message": "Internal server error"}, 500
return wrapper
# 使用装饰器
@handle_exception
@token_counter
@rate_limit
@api_key_required
@log_request
def call_llm(prompt, model="gpt-3.5-turbo"):
# 模拟异常
if "error" in prompt:
raise ValueError("Prompt contains error")
return {"response": f"LLM response to: {prompt}", "model": model}, 200
# 测试异常处理
print("测试1:正常请求")
result, status = call_llm("什么是LLM?", api_key="sk-123456")
print(f"结果:{result},状态码:{status}")
print("\n测试2:异常请求")
result, status = call_llm("什么是error?", api_key="sk-123456")
print(f"结果:{result},状态码:{status}")
4.5.3 测试结果
测试1:正常请求
结果:{'response': 'LLM response to: 什么是LLM?', 'model': 'gpt-3.5-turbo', 'token_count': 7},状态码:200
测试2:异常请求
结果:{'error': 'Prompt contains error', 'message': 'Internal server error'},状态码:500
五、高级实战:完整的 LLM API 装饰器系统
5.1 系统需求
开发一个完整的 LLM API 装饰器系统,包含以下功能:
- API 密钥验证
- 请求日志记录
- 请求限流
- Token 统计
- 异常处理
5.2 系统结构
llm_api_decorators/
├── decorators.py # 装饰器核心模块
├── main.py # 主程序
5.3 核心代码实现
5.3.1 decorators.py
from functools import wraps
from collections import defaultdict
import datetime
import logging
import time
from transformers import AutoTokenizer
# 配置日志
logging.basicConfig(
filename="llm_api.log",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
# 加载Token计数器
tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-roberta-wwm-ext")
# 合法的API密钥
VALID_API_KEYS = {"sk-123456", "ak-789012"}
# 限流配置
RATE_LIMIT = 10
TIME_WINDOW = 60 # 秒
request_count = defaultdict(list)
def api_key_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if "api_key" not in kwargs:
return {"error": "Missing API key"}, 401
api_key = kwargs.pop("api_key")
if api_key not in VALID_API_KEYS:
return {"error": "Invalid API key"}, 401
return func(*args, **kwargs)
return wrapper
def log_request(func):
@wraps(func)
def wrapper(*args, **kwargs):
request_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
logging.info(f"Request at {request_time}: function={func.__name__}, args={args}, kwargs={kwargs}")
result, status = func(*args, **kwargs)
logging.info(f"Response at {request_time}: result={result}, status={status}")
return result, status
return wrapper
def rate_limit(func):
@wraps(func)
def wrapper(*args, **kwargs):
if "api_key" in kwargs:
api_key = kwargs["api_key"]
else:
# 如果API密钥在args中,需要根据实际情况提取
api_key = None
if api_key:
current_time = time.time()
request_count[api_key] = [t for t in request_count[api_key] if current_time - t < TIME_WINDOW]
if len(request_count[api_key]) >= RATE_LIMIT:
return {"error": "Rate limit exceeded"}, 429
request_count[api_key].append(current_time)
return func(*args, **kwargs)
return wrapper
def token_counter(func):
@wraps(func)
def wrapper(*args, **kwargs):
if args:
prompt = args[0]
token_count = len(tokenizer.tokenize(prompt))
else:
token_count = 0
result, status = func(*args, **kwargs)
result["token_count"] = token_count
return result, status
return wrapper
def handle_exception(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
return {"error": str(e), "message": "Internal server error"}, 500
return wrapper
5.3.2 main.py
from decorators import (
api_key_required,
log_request,
rate_limit,
token_counter,
handle_exception
)
@handle_exception
@token_counter
@rate_limit
@api_key_required
@log_request
def call_llm(prompt, model="gpt-3.5-turbo"):
"""调用LLM API"""
return {"response": f"LLM response to: {prompt}", "model": model}, 200
# 测试完整系统
print("测试1:缺少API密钥")
result, status = call_llm("什么是LLM?")
print(f"结果:{result},状态码:{status}")
print("\n测试2:无效API密钥")
result, status = call_llm("什么是LLM?", api_key="invalid")
print(f"结果:{result},状态码:{status}")
print("\n测试3:正常请求")
result, status = call_llm("什么是LLM?", api_key="sk-123456")
print(f"结果:{result},状态码:{status}")
print("\n测试4:Token统计")
result, status = call_llm("大语言模型是什么?", api_key="sk-123456")
print(f"结果:{result},状态码:{status}")
5.4 测试结果
测试1:缺少API密钥
结果:{'error': 'Missing API key'},状态码:401
测试2:无效API密钥
结果:{'error': 'Invalid API key'},状态码:401
测试3:正常请求
结果:{'response': 'LLM response to: 什么是LLM?', 'model': 'gpt-3.5-turbo', 'token_count': 7},状态码:200
测试4:Token统计
结果:{'response': 'LLM response to: 大语言模型是什么?', 'model': 'gpt-3.5-turbo', 'token_count': 10},状态码:200
六、性能优化与最佳实践
6.1 装饰器的顺序
装饰器的顺序非常重要,应该 ** 按照 “安全→限流→日志→业务→统计→异常”** 的顺序装饰:
- 安全装饰器:先验证 API 密钥和身份
- 限流装饰器:在验证后限制请求频率
- 日志装饰器:记录完整的请求信息
- 业务函数:执行核心业务逻辑
- 统计装饰器:统计业务执行的结果
- 异常处理装饰器:最后处理所有异常
6.2 避免过度装饰
虽然装饰器很强大,但不要过度装饰,过多的装饰器会增加代码的复杂度和运行时间。
6.3 使用缓存装饰器
对于频繁调用且结果不变的 LLM API,可以使用functools.lru_cache装饰器缓存结果。
from functools import lru_cache
@lru_cache(maxsize=128)
def call_llm(prompt):
return "LLM response"
6.4 装饰器的参数化
将装饰器的配置参数化,提高装饰器的灵活性。
def rate_limit(limit=10, window=60):
request_count = defaultdict(list)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 限流逻辑
pass
return wrapper
return decorator
# 使用参数化装饰器
@rate_limit(limit=5, window=30)
def call_llm(prompt):
pass
七、零基础避坑指南
7.1 忘记传递参数
问题:装饰器没有传递被装饰函数的参数。解决:在wrapper函数中使用*args和**kwargs接收和传递参数。
7.2 丢失原函数的元信息
问题:装饰器修改了原函数的名称、文档字符串等元信息。解决:使用functools.wraps保留原函数的元信息。
7.3 装饰器的执行顺序错误
问题:装饰器的执行顺序不符合预期。解决:注意装饰器的顺序,从下到上执行。
7.4 装饰器的参数错误
问题:装饰器本身的参数传递错误。解决:使用三层嵌套的装饰器处理装饰器本身的参数。
7.5 装饰器的性能问题
问题:装饰器导致函数运行速度变慢。解决:
- 简化装饰器的逻辑
- 避免在装饰器中执行耗时的操作
- 对于频繁调用的函数,使用缓存装饰器
八、总结:装饰器与 LLM API 开发的「对应关系」
| 装饰器功能 | LLM API 场景 |
|---|---|
| API 密钥验证 | 确保只有合法用户才能调用 API |
| 身份验证 | 验证用户的身份信息 |
| 请求限流 | 防止恶意请求,保护 API |
| 日志记录 | 记录 API 的请求和响应信息,便于故障排查 |
| Token 统计 | 统计 LLM API 的 Token 消耗,控制成本 |
| 异常处理 | 统一处理 API 的异常,提供友好的错误信息 |
Python 装饰器是LLM API 开发的核心技术,掌握它能帮你在不修改原有代码的情况下,为 API 添加安全防护和可观测性功能,提高 API 的可靠性和可维护性。在实际开发中,要注意:
- 合理安排装饰器的顺序
- 保留原函数的元信息
- 避免过度装饰
- 使用参数化装饰器提高灵活性
- 注意装饰器的性能
下一篇我们将学习《Python 异步 IO:LLM 批量推理的性能优化》,讲解如何使用异步 IO 提高 LLM 批量推理的性能。

2081

被折叠的 条评论
为什么被折叠?



