← Back to 文字

Ruff、日志与环境变量配置

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 使用 .env 管理本地敏感配置。
  • 使用 python-dotenv 加载环境变量。
  • 建立最小日志输出。
  • 使用 ruff 做静态检查。

前置知识

  • 已初始化 llm_api_lab

1. 环境变量配置

创建 .env.example

DEEPSEEK_API_KEY=
DEEPSEEK_BASE_URL=https://api.deepseek.com
DEEPSEEK_MODEL=deepseek-chat

复制为 .env

cp .env.example .env

然后在 .env 中填入真实 Key。

2. 使用 python-dotenv

示例:

import os

from dotenv import load_dotenv


load_dotenv()

api_key = os.getenv("DEEPSEEK_API_KEY")
if not api_key:
    raise ValueError("DEEPSEEK_API_KEY 未设置")

要点:

  • load_dotenv() 负责把 .env 读进当前进程环境变量
  • 读取后仍然使用 os.getenv

3. 为什么不能把配置散落在代码里

错误示例:

API_KEY = "hard-coded-key"
BASE_URL = "https://api.deepseek.com"

问题:

  • 不安全
  • 不方便切环境
  • 后续切模型、切厂商会变得混乱

4. 最小日志

在月份 1,你不需要复杂日志系统,但需要最小可读日志。

示例:

import logging


logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)

logger = logging.getLogger(__name__)

logger.info("application started")

用途:

  • 记录模型请求开始与结束
  • 记录工具调用
  • 记录错误

5. Ruff 最小使用方式

执行:

uv run ruff check .

作用:

  • 发现未使用导入
  • 发现明显代码风格问题
  • 发现一些低级错误

月份 1 的目标不是研究所有规则,而是养成“先检查再提交”的习惯。

6. 推荐的配置组织

建议在 app/config.py 中收口配置逻辑,在 app/logger.py 中收口日志逻辑。

好处:

  • 入口文件更干净
  • 后续 CLI 和 FastAPI 都能复用

7. 实操任务

  1. .env.example
  2. config.py 中读取 DeepSeek 相关配置
  3. logger.py 中创建最小日志器
  4. 故意引入一个未使用变量,运行 ruff check 观察输出

8. 自测题

  1. .env.example.env 的区别是什么?
  2. 为什么日志不能只靠 print
  3. 为什么配置加载逻辑应该集中管理?

9. 作业与验收

验收标准:

  • .env.example 已存在
  • .env 已能被成功加载
  • 项目运行时能输出最小日志
  • ruff check . 没有基础错误

10. 常见错误

  • 把真实 .env 提交到仓库
  • 代码里混用 print 和日志但没有边界
  • 报错后只看终端最后一行,不看上下文日志

11. 本章与前文关系

上一章解决的是“项目怎么初始化”;这一章解决的是“项目初始化后,最先应该放进哪些工程基础设施”。

对月份 1 来说,优先级最高的三件事是:

  • 配置收口
  • 日志可见
  • 静态检查可跑

这三件事一旦缺失,后面所有模型调用、Tool Calling 和 FastAPI 代码都更容易变乱。

12. 本章在研发助手项目中的位置

研发助手项目最终至少会有:

  • 一套稳定配置
  • 一套最小日志
  • 一套最小代码检查流程

这三者看似“基础设施”,但它们会直接影响:

  • 你能不能快速排 API Key 问题
  • 你能不能定位工具调用失败的阶段
  • 你能不能在代码越来越多时维持可读性

13. 为什么 .env、日志和 ruff 要一起学

因为这三者分别对应项目工程中的三条底线:

.env

解决“配置不能散落”的问题。

日志

解决“运行时到底发生了什么”的问题。

ruff

解决“代码还没跑就能发现一批低级问题”的问题。

月份 1 不是要你成为日志系统专家或代码规范专家,而是要建立“每个项目都默认带这些底盘”的习惯。

14. 错误示例 vs 正确示例

错误示例:配置写死 + 全程 print

API_KEY = "hard-coded-key"
BASE_URL = "https://api.deepseek.com"

print("start calling model")
print("call failed")

问题:

  • 密钥不安全
  • 配置不可替换
  • 日志不可分类
  • 后续 API 层和 service 层很难共享统一输出

正确示例:配置收口 + 日志最小化规范

import logging
import os

from dotenv import load_dotenv


load_dotenv()

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
)

logger = logging.getLogger(__name__)

api_key = os.getenv("DEEPSEEK_API_KEY")
logger.info("configuration loaded")

15. 完整文件级示例:config.py + logger.py

config.py

"""项目配置读取。"""

from __future__ import annotations

import os
from dataclasses import dataclass

from dotenv import load_dotenv


load_dotenv()


@dataclass
class AppSettings:
    """月份 1 最小配置对象。"""

    deepseek_api_key: str
    deepseek_base_url: str
    deepseek_model: str


def load_settings() -> AppSettings:
    """从环境变量中加载配置。

    当前阶段先用数据类承载,后面会自然升级到 Pydantic。
    """

    api_key = os.getenv("DEEPSEEK_API_KEY")
    if not api_key:
        raise ValueError("DEEPSEEK_API_KEY 未设置")

    return AppSettings(
        deepseek_api_key=api_key,
        deepseek_base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
        deepseek_model=os.getenv("DEEPSEEK_MODEL", "deepseek-chat"),
    )

logger.py

"""项目日志初始化。"""

from __future__ import annotations

import logging


def get_logger(name: str) -> logging.Logger:
    """返回一个最小可用的 logger。

    月份 1 不做复杂 logging 配置,只先保证:
    - 有时间
    - 有级别
    - 有模块名
    """

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    )
    return logging.getLogger(name)

16. 逐段解释这组完整示例

为什么 load_dotenv() 放在配置模块里

因为月份 1 希望把“配置读取入口”收口。你后面不应该在每个脚本里都复制一遍 load_dotenv()

为什么 AppSettings 要存在

因为即使当前只有三个字段,把它们收口成对象,也比到处传裸字符串更稳定。

为什么 logger 不复杂化

初学者常见错误是:一上来就想设计完整日志系统。月份 1 只需要最小可见性,不需要追求过度工程。

17. ruff 在月份 1 的真实价值

很多人把 ruff 理解为“格式工具”或“规范工具”。对月份 1 来说,它更像:

  • 语法和导入层面的早期预警器
  • 防止代码越来越乱的低成本手段

例如它能帮你发现:

  • 未使用导入
  • 显而易见的低级问题
  • 某些不一致的代码习惯

月份 1 不是研究所有 lint 规则,而是建立一个底线:代码改完先检查。

18. 一个更接近后续项目的增强版用法

你可以在入口文件中这样组合使用:

from app.config import load_settings
from app.logger import get_logger


logger = get_logger(__name__)


def main() -> None:
    settings = load_settings()
    logger.info("application_started")
    logger.info("current_model=%s", settings.deepseek_model)

这其实已经在为后面的 llm_client.pychat_service.py 做铺垫。

19. 调试与排错:本章最常见问题

问题一:.env 改了但程序似乎没读到

先确认:

  • 当前工作目录是否正确
  • .env 是否真的存在
  • 代码是否调用了 load_dotenv()

问题二:日志没输出

检查:

  • 是否真的调用了 logger.info(...)
  • logging level 是否为 INFO

问题三:ruff 报错看不懂

不要一上来全部忽略。月份 1 的策略是:

  • 先看它指出的是什么文件、哪一行
  • 优先修明显错误和未使用导入
  • 不在本阶段陷入复杂规则定制

20. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能用 .env 管理本地配置。
  2. 能通过配置对象读取模型设置。
  3. 能输出最小日志。
  4. 能运行 ruff 做基础检查。

21. 如果你卡在这里,先回看哪几章

22. 从本章过渡到下一章的桥接说明

接下来进入 03-pydantic与配置建模.md

这一章已经让你学会“配置应该被收口”,下一章会进一步升级这个思路:不仅配置要结构化,消息、请求、响应、错误这些对象也要结构化。也就是从“配置工程化”进入“数据工程化”。

Fin