Python 装饰器:LLM API 的安全与可观测性增强

AgenticCoding·十二月创作之星挑战赛 10w+人浏览 550人参与

(专栏:Python 从真零基础到纯文本 LLM 全栈实战・第 9 篇 | 字数:13000 字 | 零基础友好 | LLM 场景深度绑定 | 代码可运行)


开篇:LLM API 的 “裸奔” 困境

你有没有遇到过这样的问题?

  • 直接暴露 LLM API,被恶意调用导致费用飙升
  • LLM API 调用出现问题,不知道是谁调用的、什么时候调用的、调用了什么内容
  • 想要给 LLM API 添加身份验证、日志记录、限流等功能,却要修改大量原有代码
  • 不同的 LLM API 需要重复添加相同的功能,代码冗余

这些问题的核心是LLM API 缺乏安全防护和可观测性,而Python 装饰器就是解决这些问题的 “魔术函数”—— 它能帮你:

  1. 增强安全性:添加身份验证、API 密钥验证、请求限流等
  2. 提升可观测性:记录请求日志、响应时间、Token 消耗等
  3. 减少代码冗余:将公共功能封装为装饰器,一次定义,多次使用
  4. 不修改原有代码:在不改变原有 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 装饰器的工作原理

装饰器的工作原理是函数的嵌套和闭包

  1. 当定义@decorator时,Python 会将被装饰的函数hello作为参数传递给decorator函数
  2. decorator函数返回wrapper函数
  3. 当调用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 开发中,装饰器主要用于以下场景

  1. API 密钥验证:确保只有合法的用户才能调用 API
  2. 身份验证:验证用户的身份信息
  3. 请求限流:限制 API 的调用频率,防止恶意请求
  4. 日志记录:记录 API 的请求和响应信息
  5. Token 统计:统计 LLM API 的 Token 消耗
  6. 异常处理:统一处理 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 装饰器系统,包含以下功能:

  1. API 密钥验证
  2. 请求日志记录
  3. 请求限流
  4. Token 统计
  5. 异常处理

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 装饰器的顺序

装饰器的顺序非常重要,应该 ** 按照 “安全→限流→日志→业务→统计→异常”** 的顺序装饰:

  1. 安全装饰器:先验证 API 密钥和身份
  2. 限流装饰器:在验证后限制请求频率
  3. 日志装饰器:记录完整的请求信息
  4. 业务函数:执行核心业务逻辑
  5. 统计装饰器:统计业务执行的结果
  6. 异常处理装饰器:最后处理所有异常

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 装饰器的性能问题

问题:装饰器导致函数运行速度变慢。解决

  1. 简化装饰器的逻辑
  2. 避免在装饰器中执行耗时的操作
  3. 对于频繁调用的函数,使用缓存装饰器

八、总结:装饰器与 LLM API 开发的「对应关系」

装饰器功能LLM API 场景
API 密钥验证确保只有合法用户才能调用 API
身份验证验证用户的身份信息
请求限流防止恶意请求,保护 API
日志记录记录 API 的请求和响应信息,便于故障排查
Token 统计统计 LLM API 的 Token 消耗,控制成本
异常处理统一处理 API 的异常,提供友好的错误信息

Python 装饰器是LLM API 开发的核心技术,掌握它能帮你在不修改原有代码的情况下,为 API 添加安全防护和可观测性功能,提高 API 的可靠性和可维护性。在实际开发中,要注意:

  1. 合理安排装饰器的顺序
  2. 保留原函数的元信息
  3. 避免过度装饰
  4. 使用参数化装饰器提高灵活性
  5. 注意装饰器的性能

下一篇我们将学习《Python 异步 IO:LLM 批量推理的性能优化》,讲解如何使用异步 IO 提高 LLM 批量推理的性能。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值