Skip to content

7.1 环境变量管理

🎯 小白理解:什么是"环境变量"?

想象你开了一家餐厅,有些信息不能写在菜单上:

  • 菜单(代码):写"使用 API 获取数据"
  • 保险箱(环境变量):存放真正的 API 密钥 sk-xxxxx

为什么不能直接把密钥写在代码里?

python
# ❌ 错误做法!密钥会被上传到 GitHub
api_key = "sk-abc123xyz456"

# ✅ 正确做法!密钥存在环境变量里
api_key = os.getenv("OPENAI_API_KEY")

环境变量的好处

优点解释
安全密钥不会被提交到 Git
灵活不改代码就能换配置
分离开发/测试/生产用不同的值

类比:环境变量就像密码管理器——你的程序知道去哪里取密钥,但密钥本身不会暴露在代码里。

环境变量基础

🎯 小白理解os.getenv() 怎么用?

python
os.getenv("变量名")           # 获取变量,没有则返回 None
os.getenv("变量名", "默认值")  # 获取变量,没有则返回默认值

就像从保险箱取东西:有就拿出来,没有就用备用方案。

python
import os
from pathlib import Path

# 读取环境变量
api_key = os.getenv("OPENAI_API_KEY")
model = os.getenv("MODEL_NAME", "gpt-4")  # 带默认值

# 设置环境变量(当前进程)
os.environ["DEBUG"] = "true"

# 检查环境变量是否存在
if "OPENAI_API_KEY" in os.environ:
    print("API Key 已配置")
else:
    print("警告: API Key 未配置")

.env 文件管理

🎯 小白理解:什么是 .env 文件?

.env 文件就是一个专门存放环境变量的"配置文件":

文件名: .env(以点开头,是隐藏文件)
位置: 放在项目根目录
格式: 变量名=值(一行一个)

为什么用 .env 文件?

  • 不用每次在终端设置环境变量
  • 方便团队共享配置(但不共享真实密钥!)
  • 使用 python-dotenv 库自动加载

⚠️ 重要规则

  1. .env 文件绝对不能提交到 Git(加入 .gitignore
  2. 创建 .env.example 作为模板(只写变量名,不写真实值)

安装 python-dotenv

bash
pip install python-dotenv

创建 .env 文件

env
# .env 文件内容
OPENAI_API_KEY=sk-xxx
ANTHROPIC_API_KEY=sk-ant-xxx
MODEL_NAME=gpt-4
TEMPERATURE=0.7
MAX_TOKENS=2000
DEBUG=false
LOG_LEVEL=INFO
DATABASE_URL=postgresql://user:pass@localhost/db

加载环境变量

python
from dotenv import load_dotenv
import os
from pathlib import Path

# 方法 1: 从当前目录加载
load_dotenv()

# 方法 2: 指定 .env 文件路径
env_path = Path(".") / ".env"
load_dotenv(dotenv_path=env_path)

# 方法 3: 覆盖已有环境变量
load_dotenv(override=True)

# 使用环境变量
api_key = os.getenv("OPENAI_API_KEY")
model = os.getenv("MODEL_NAME")
temperature = float(os.getenv("TEMPERATURE", 0.7))
debug = os.getenv("DEBUG", "false").lower() == "true"

print(f"模型: {model}")
print(f"温度: {temperature}")
print(f"调试模式: {debug}")

配置管理类

🎯 小白理解:为什么要写"配置管理类"?

直接用 os.getenv() 有几个问题:

  1. 每次都要写 os.getenv("OPENAI_API_KEY"),很长
  2. 容易打错变量名(字符串没有代码补全)
  3. 类型不安全(os.getenv() 返回的都是字符串)

配置类的好处

python
# 没有配置类
api_key = os.getenv("OPENAI_API_KEY")  # 每次都写这么长
temp = float(os.getenv("TEMPERATURE", "0.7"))  # 还要手动转类型

# 有配置类
config = Config.from_env()
api_key = config.openai_api_key  # 代码补全!
temp = config.temperature        # 已经是 float 类型!

配置类就像一个"助手",帮你把所有环境变量整理好、转换好类型、验证好格式。

python
from typing import Optional, Dict, Any
from dataclasses import dataclass, field
from pathlib import Path
import os
from dotenv import load_dotenv

@dataclass
class Config:
    """应用配置管理"""
    
    # API 配置
    openai_api_key: str
    anthropic_api_key: Optional[str] = None
    
    # 模型配置
    model_name: str = "gpt-4"
    temperature: float = 0.7
    max_tokens: int = 2000
    
    # 应用配置
    debug: bool = False
    log_level: str = "INFO"
    
    # 数据库配置
    database_url: Optional[str] = None
    
    @classmethod
    def from_env(cls, env_file: str = ".env") -> "Config":
        """从环境变量加载配置"""
        # 加载 .env 文件
        if Path(env_file).exists():
            load_dotenv(env_file)
        
        # 读取配置
        return cls(
            openai_api_key=os.getenv("OPENAI_API_KEY", ""),
            anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
            model_name=os.getenv("MODEL_NAME", "gpt-4"),
            temperature=float(os.getenv("TEMPERATURE", "0.7")),
            max_tokens=int(os.getenv("MAX_TOKENS", "2000")),
            debug=os.getenv("DEBUG", "false").lower() == "true",
            log_level=os.getenv("LOG_LEVEL", "INFO"),
            database_url=os.getenv("DATABASE_URL")
        )
    
    def validate(self) -> bool:
        """验证配置"""
        if not self.openai_api_key:
            raise ValueError("OPENAI_API_KEY 未配置")
        
        if not self.openai_api_key.startswith("sk-"):
            raise ValueError("OPENAI_API_KEY 格式错误")
        
        if self.temperature < 0 or self.temperature > 2:
            raise ValueError("temperature 必须在 0-2 之间")
        
        return True
    
    def to_dict(self) -> Dict[str, Any]:
        """转换为字典(隐藏敏感信息)"""
        return {
            "openai_api_key": "sk-***" if self.openai_api_key else None,
            "anthropic_api_key": "sk-ant-***" if self.anthropic_api_key else None,
            "model_name": self.model_name,
            "temperature": self.temperature,
            "max_tokens": self.max_tokens,
            "debug": self.debug,
            "log_level": self.log_level,
            "database_url": "***" if self.database_url else None
        }

# 使用示例
config = Config.from_env()
config.validate()
print(config.to_dict())

多环境配置

python
from enum import Enum
from pathlib import Path
import os

class Environment(Enum):
    """环境类型"""
    DEVELOPMENT = "development"
    STAGING = "staging"
    PRODUCTION = "production"

class EnvironmentConfig:
    """多环境配置管理"""
    
    def __init__(self):
        self.env = self._detect_environment()
        self._load_config()
    
    def _detect_environment(self) -> Environment:
        """检测当前环境"""
        env_name = os.getenv("ENV", "development").lower()
        
        if env_name in ["prod", "production"]:
            return Environment.PRODUCTION
        elif env_name in ["stage", "staging"]:
            return Environment.STAGING
        else:
            return Environment.DEVELOPMENT
    
    def _load_config(self):
        """加载对应环境的配置"""
        # 加载基础配置
        base_env = Path(".env")
        if base_env.exists():
            load_dotenv(base_env)
        
        # 加载环境特定配置
        env_file = Path(f".env.{self.env.value}")
        if env_file.exists():
            load_dotenv(env_file, override=True)
    
    @property
    def is_production(self) -> bool:
        return self.env == Environment.PRODUCTION
    
    @property
    def is_development(self) -> bool:
        return self.env == Environment.DEVELOPMENT
    
    def get_config(self) -> Config:
        """获取配置"""
        return Config.from_env()

# 使用示例
env_config = EnvironmentConfig()
print(f"当前环境: {env_config.env.value}")
print(f"是否生产环境: {env_config.is_production}")

config = env_config.get_config()

Agent 配置系统

python
from typing import Optional, List, Dict, Any
from dataclasses import dataclass, field
from pathlib import Path
import os
from dotenv import load_dotenv

@dataclass
class AgentConfig:
    """Agent 配置"""
    
    # 基础配置
    name: str
    description: str
    
    # LLM 配置
    model: str = "gpt-4"
    temperature: float = 0.7
    max_tokens: int = 2000
    
    # 工具配置
    tools: List[str] = field(default_factory=list)
    
    # 系统提示词
    system_prompt: str = "你是一个有用的助手。"
    
    # API 配置
    api_key: Optional[str] = None
    api_base: Optional[str] = None
    
    # 高级配置
    streaming: bool = True
    retry_attempts: int = 3
    timeout: float = 30.0
    
    @classmethod
    def from_env(cls, agent_name: str) -> "AgentConfig":
        """从环境变量加载 Agent 配置"""
        load_dotenv()
        
        # 读取通用配置
        prefix = f"{agent_name.upper()}_"
        
        return cls(
            name=agent_name,
            description=os.getenv(f"{prefix}DESCRIPTION", ""),
            model=os.getenv(f"{prefix}MODEL", os.getenv("MODEL_NAME", "gpt-4")),
            temperature=float(os.getenv(f"{prefix}TEMPERATURE", 
                                       os.getenv("TEMPERATURE", "0.7"))),
            max_tokens=int(os.getenv(f"{prefix}MAX_TOKENS", 
                                     os.getenv("MAX_TOKENS", "2000"))),
            tools=os.getenv(f"{prefix}TOOLS", "").split(",") if os.getenv(f"{prefix}TOOLS") else [],
            system_prompt=os.getenv(f"{prefix}SYSTEM_PROMPT", "你是一个有用的助手。"),
            api_key=os.getenv("OPENAI_API_KEY"),
            api_base=os.getenv("OPENAI_API_BASE"),
            streaming=os.getenv(f"{prefix}STREAMING", "true").lower() == "true",
            retry_attempts=int(os.getenv(f"{prefix}RETRY_ATTEMPTS", "3")),
            timeout=float(os.getenv(f"{prefix}TIMEOUT", "30.0"))
        )

# .env 示例
"""
# 通用配置
OPENAI_API_KEY=sk-xxx
MODEL_NAME=gpt-4
TEMPERATURE=0.7

# Research Agent 配置
RESEARCH_DESCRIPTION=专业的研究助手
RESEARCH_MODEL=gpt-4
RESEARCH_TOOLS=search,calculator,wikipedia
RESEARCH_SYSTEM_PROMPT=你是一个专业的研究助手,擅长查找和分析信息。

# Writer Agent 配置
WRITER_DESCRIPTION=专业的写作助手
WRITER_MODEL=gpt-3.5-turbo
WRITER_TEMPERATURE=0.9
WRITER_SYSTEM_PROMPT=你是一个创意写作助手,风格优美简洁。
"""

# 使用示例
research_config = AgentConfig.from_env("research")
writer_config = AgentConfig.from_env("writer")

print(f"Research Agent: {research_config.model}, 工具: {research_config.tools}")
print(f"Writer Agent: {writer_config.model}, 温度: {writer_config.temperature}")

密钥管理最佳实践

🎯 小白理解:为什么需要"密钥管理"?

API 密钥就像你家的钥匙:

  • 泄露了 = 别人可以用你的钱调用 API
  • 格式错了 = 程序会报错
  • 没配置 = 程序无法运行

SecretManager 能帮你

功能作用
get_api_key()安全获取密钥,没有就报错
validate_api_key()检查密钥格式是否正确
mask_secret()隐藏密钥(打印日志时用)

为什么要隐藏密钥?

python
# ❌ 危险!密钥会出现在日志里
print(f"使用密钥: {api_key}")

# ✅ 安全!只显示前几位
print(f"使用密钥: {mask_secret(api_key)}")  # 输出: sk-a***
python
import os
from pathlib import Path
from typing import Optional
import secrets

class SecretManager:
    """密钥管理器"""
    
    @staticmethod
    def generate_secret_key(length: int = 32) -> str:
        """生成随机密钥"""
        return secrets.token_urlsafe(length)
    
    @staticmethod
    def get_api_key(key_name: str, required: bool = True) -> Optional[str]:
        """安全获取 API Key"""
        key = os.getenv(key_name)
        
        if required and not key:
            raise ValueError(f"环境变量 {key_name} 未配置")
        
        if key and not SecretManager.validate_api_key(key):
            raise ValueError(f"API Key 格式错误: {key_name}")
        
        return key
    
    @staticmethod
    def validate_api_key(key: str) -> bool:
        """验证 API Key 格式"""
        if not key:
            return False
        
        # OpenAI Key 格式
        if key.startswith("sk-"):
            return len(key) > 20
        
        # Anthropic Key 格式
        if key.startswith("sk-ant-"):
            return len(key) > 30
        
        return True
    
    @staticmethod
    def mask_secret(secret: str, visible_chars: int = 4) -> str:
        """隐藏密钥(仅显示前几位)"""
        if not secret:
            return "None"
        
        if len(secret) <= visible_chars:
            return "*" * len(secret)
        
        return f"{secret[:visible_chars]}***"

# 使用示例
manager = SecretManager()

# 获取 API Key
try:
    openai_key = manager.get_api_key("OPENAI_API_KEY", required=True)
    anthropic_key = manager.get_api_key("ANTHROPIC_API_KEY", required=False)
    
    print(f"OpenAI Key: {manager.mask_secret(openai_key)}")
    print(f"Anthropic Key: {manager.mask_secret(anthropic_key) if anthropic_key else 'Not configured'}")
except ValueError as e:
    print(f"配置错误: {e}")

# 生成新密钥
new_secret = manager.generate_secret_key()
print(f"新密钥: {manager.mask_secret(new_secret)}")

.gitignore 最佳实践

gitignore
# 环境变量文件
.env
.env.*
!.env.example

# Python
__pycache__/
*.py[cod]
*.so
.Python

# 虚拟环境
venv/
env/
ENV/

# IDE
.vscode/
.idea/
*.swp

# 日志
*.log
logs/

# 数据库
*.db
*.sqlite3

# 密钥和证书
*.pem
*.key
*.cert

关键要点

  1. 永远不要提交 .env 文件到 Git
  2. 使用 .env.example 作为模板
  3. 验证所有配置值
  4. 在日志中隐藏敏感信息
  5. 使用类型安全的配置类

下一节:7.2 HTTP 请求与 API 调用

基于 MIT 许可证发布。内容版权归作者所有。