0. 系列闭环(不公开源码也能跟读)
端到端链路:Vue 前端 → api/routes/chat.py → Guide 多轮 SSE → run_analysis_pipeline(解析→分析→匹配→报告)→ tools/pdf_exporter PDF。
本篇:第 17/17 篇 · 配置环 · settings
| 阶段 | 用户可见 | 代码入口 | 对应篇 |
|---|---|---|---|
| 建会话 | 欢迎语 | POST /api/sessions | 09 |
| 多轮对话 | SSE 流式 | chat/stream → run_guide_single_turn | 06, 14 |
| 信息充分 | 开始分析 | _run_analysis_background | 05, 07 |
| 履历解析 | 进度 30% | run_resume_parser | 12 |
| 画像/RIASEC | 进度 50% | run_profile_analyzer | 03, 13 |
| 职业匹配 | 进度 70% | run_career_matcher | 02 |
| 报告 | 进度 90% | run_reporter | 11 |
| 下载 PDF | 文件 | GET …/report/pdf | 11, 15 |
| 说明 | |
|---|---|
| 读本篇前 | 第 08/15 篇环境变量 |
| 读完本篇 | 能写出 .env 如何覆盖 Settings 默认值 |
| 下一环 | 回到第 01 篇:闭环复盘(回到第 01 篇复盘) |
全系列闭环索引:SERIES-LOOP.md
1. 要解决什么问题
iCan 的配置分散在多处会迅速失控:LLM 密钥、MySQL URL、Embedding 路径、Chroma 目录、对话轮次上限——本地 .env、Docker environment、CI 注入的变量可能同时存在。还要避免把完整 LLM_API_KEY 打进日志。
项目把所有项收敛到 config.py 一个 Settings 类,配合模块级 settings 单例,供 llm/providers.py、Agent、Docker compose 统一读取。本文只讲 iCan 这份实现,不是 pydantic-settings 通用教程。
2. 实现位置
| 模块 | 与配置的关系 |
|---|---|
config.py |
_ENV_FILE 定位、dotenv 预加载、Settings、get_settings()、settings 单例 |
llm/providers.py |
from ican.config import settings,读 LLM_* 创建 ChatOpenAI |
agents/resume_parser.py |
间接通过 get_light_model() 读 LLM_MODEL_LIGHT |
.env.example |
仓库推荐的本地/DeepSeek 示例值 |
docker-compose.yml |
容器内环境变量覆盖(默认 Ollama + MySQL DSN) |
3. _ENV_FILE 查找:两层路径
config.py 在 import Settings 之前就计算 .env 绝对路径:
1 | |
解释(__file__ 在 src/ican/config.py):
- 首选
项目根/.env(向上两级到iCan/仓库根); - 备选 再向上一级(兼容
src外层还有一层目录的 checkout 布局)。
若两个路径都不存在,_ENV_FILE 仍指向最后一个候选路径;pydantic 读不到文件时不会报错,字段走代码默认值。
Settings.model_config 显式绑定该路径:
1 | |
case_sensitive=True 表示环境变量名必须与字段名一致(LLM_API_KEY,不是 llm_api_key)。extra="ignore" 允许 .env 里有多余键而不 ValidationError。
4. dotenv 预加载:先于 pydantic 写入 os.environ
在 Settings 类定义之前,若找到 .env,会用 python-dotenv 手动灌进 os.environ:
1 | |
设计意图:
- 不覆盖已有环境变量(
k not in os.environ)——Docker/K8s 注入的值优先级高于.env文件; - 在 pydantic 解析前就让
os.environ可见,避免部分库只读 environ、不读env_file的边缘问题; except Exception: pass:缺python-dotenv或文件损坏时静默跳过,仍可用纯环境变量启动。
有效优先级(与注释一致):
1 | |
5. Settings 字段:代码默认 vs .env 示例
代码里的默认值(未设环境变量时生效):
| 字段 | 默认值 | 用途 |
|---|---|---|
LLM_MODEL_CHAT |
gpt-4o |
Guide / 分析 / 匹配 / Reporter |
LLM_MODEL_LIGHT |
gpt-4o-mini |
ResumeParser 等 |
LLM_BASE_URL |
https://api.openai.com/v1 |
OpenAI 兼容根路径 |
LLM_API_KEY |
"" |
空字符串,本地需自行填写 |
LLM_MAX_TOKENS |
4096 |
最大生成 token(compose 默认 8192 会覆盖) |
DB_URL |
sqlite:///./ican.db |
开发 SQLite |
DEBUG |
False |
生产模式 |
MAX_CHAT_ROUNDS |
15 |
Guide 循环上限 |
EMBEDDING_MODEL_PATH |
"" |
空表示需外部挂载或本地配置 |
CHROMA_PERSIST_DIR |
"" |
空表示使用项目相对路径逻辑 |
.env.example 展示的是常见 DeepSeek 部署示例,不是代码默认:
1 | |
文档里写「默认 DeepSeek」会与源码不符;应以 config.py 为准,.env / compose 为覆盖层。
Docker docker-compose.yml 又是第三套默认(Ollama qwen3.5:9b、LLM_API_KEY=ollama),适合离线演示,与 .env.example 的云端 API 并不矛盾——都是环境覆盖,不是改 Settings 类默认值。
6. 便捷属性与 llm_config_dict 脱敏
Settings 上几个 @property 供业务侧使用:
is_debug/is_production:基于DEBUG;log_level_value:把LOG_LEVEL字符串映射到logging.DEBUG等整数;app_info:应用名、版本、调试开关字典。
LLM 相关聚合在 llm_config_dict:
1 | |
要点:
- 日志走
safe_result,只露末四位; - 返回值仍是完整
result(含明文api_key),调用方若再print需自行脱敏; - key 长度 ≤4 时日志显示
***,避免短 key 泄露过多。
llm/providers.py 不经过 llm_config_dict,直接读 settings.LLM_API_KEY 等字段构造 ChatOpenAI——配置入口单一,但日志脱敏只在访问 llm_config_dict 属性时触发。
7. 模块级单例:get_settings() 与 settings
1 | |
模块 import 时执行一次,全进程共享 from ican.config import settings。FastAPI 若需要依赖注入,可包装 get_settings(),但当前代码库普遍直接 import settings。
注意:get_settings() 每次调用都会 Settings() 新建实例;只有模块级 settings 是单例。业务代码应 import settings,不要反复 get_settings() unless 测试隔离。
启动日志会打印 DB_URL 明文(含密码),与 API Key 不同,数据库连接串目前未做脱敏——生产环境应靠 LOG_LEVEL 控制或外层日志过滤。
8. 多环境怎么切换
| 场景 | 做法 |
|---|---|
| 本地开发 | 复制 .env.example → .env,填 DeepSeek/OpenAI |
| Docker | compose environment 块覆盖;宿主机 .env 可通过 ${VAR} 传入 |
| 仅 CI 密钥 | 不设 .env,流水线注入 LLM_API_KEY 等环境变量 |
| SQLite → MySQL | 改 DB_URL,compose 默认已是 MySQL DSN |
EMBEDDING_MODEL_PATH、CHROMA_PERSIST_DIR 在 compose 里默认 /app/models/bge-m3、/app/chroma_data,与第 15 篇 volume 挂载对应。空字符串默认值在代码里表示「需外部指定路径」(Chroma 注释写明默认可落在项目根 chroma_data)。
类型校验由 pydantic 完成:DEBUG=false 字符串会转为 False;DEBUG=abc 启动即 ValidationError。LLM_TEMPERATURE、LLM_MAX_TOKENS 同理。
9. 在流水线中的位置
1 | |
换模型不改 Agent 代码,只改 LLM_BASE_URL + LLM_MODEL_*;这与第 8 篇「OpenAI 兼容接口 + 环境变量切换」一致。llm/providers.py 的 check_ollama_available 同样读 settings.LLM_BASE_URL 和 settings.LLM_MODEL_CHAT 做探活。
10. 踩坑与边界
踩坑 1:误以为 .env.example 即运行时默认。 源码默认是 gpt-4o + OpenAI URL;DeepSeek 只是示例文件。写文档或截图时需区分「类默认值」和「部署示例」。
踩坑 2:llm_config_dict 脱敏只保护 logger。 返回 dict 仍含明文 key;若把返回值序列化到 API 响应会泄密。脱敏逻辑不应复制到对外接口,应单独设计 DTO。
踩坑 3:_ENV_FILE 路径与 cwd 无关。 配置按 config.py 位置算绝对路径,从 /app/src 启动 uvicorn 仍能找到仓库根 .env(若挂载进容器)。但若 .env 未 COPY 进镜像、又未在 compose 设变量,则只用代码默认 + compose 注入项。
踩坑 4:dotenv 预加载 silence。 except Exception: pass 吞掉 dotenv 失败,排查「变量未生效」时要同时检查 os.environ、compose、以及 case_sensitive 拼写。
踩坑 5:双模型字段只配一个。 .env.example 里 chat/light 常设相同 DeepSeek 模型;agents/resume_parser.py 仍走 LLM_MODEL_LIGHT。只改 LLM_MODEL_CHAT 不改 light 会导致两路模型不一致。
11. 小结
_ENV_FILE两级查找 +SettingsConfigDict(env_file=...)绑定唯一.env路径。- import 前 dotenv 预加载,且不覆盖已有环境变量,适配 Docker 注入。
- 代码默认 OpenAI 系;DeepSeek/Ollama 通过
.env或 compose 覆盖,勿写死进Settings类。 llm_config_dict日志脱敏末四位,返回体仍含明文,调用方慎用。- 使用模块级
settings单例;get_settings()主要用于启动日志与异常兜底。
本系列第 16 篇中间件、第 15 篇 Docker 中的环境变量,均以此模块为源头。
附录:关键源码(逐行注释)
以下代码摘自 iCan 实现,每行上方均有中文注释,不公开仓库也可跟读。
生成命令:python3 bin/build-ican-annotated-snippets.py
Settings LLM/DB 字段
1 | |
llm_config_dict 脱敏
1 | |
get_settings 单例
1 | |
系列导航
| 篇 | 主题 |
|---|---|
| 1 | 系统全景 |
| 2 | 五 Agent 协作 |
| 3 | 霍兰德 RIASEC |
| 4–7 | 状态 · 路由 · 嵌套 · 容错 |
| 8–11 | LLM 层 · SSE/WS · DB 迁移 · PDF |
| 12–14 | JSON Prompt · RIASEC Prompt · Guide Prompt |
| 15–17 | Docker · 中间件 · 配置 |