当前位置: 首页 > news >正文

agent学习Day11——多环境配置与完整JD分析接口

一、多环境配置:让配置跟环境走

三件事

一是 OS 环境变量优先级最高——export KEY=xxx盖过.env文件,生产敏感信息只放 OS 不落盘。

二是 env_file 分层——.env(通用默认,可进 git)+.env.development/.env.production(环境覆盖,gitignore)+.env.local(个人本地)。

三是 ENV 字段标识——告诉代码"我在哪个环境",据此选 env_file。

pydantic-settings 的一个坑

model_config = SettingsConfigDict(env_file=...)类定义时求值的,那时self.ENV字段还没读到,没法写env_file=f".env.{self.ENV}"。解法:类定义前先从 OS 环境变量读 ENV 到模块级变量:

importosfrompydantic_settingsimportBaseSettings,SettingsConfigDict _env_name=os.environ.get("ENV","development")classSettings(BaseSettings):model_config=SettingsConfigDict(env_file=(".env",f".env.{_env_name}"),# 元组:后者覆盖前者env_file_encoding="utf-8",extra="ignore",)ENV:str="development"LOG_LEVEL:str="DEBUG"LOG_FILE:str="logs/app.log"# ... 其他字段

env_file接收元组的语义:pydantic-settings 按顺序加载,后面的文件覆盖前面的——同名 key 后者生效。所以.env是底座默认值,.env.{env_name}是环境特定覆盖。

配置优先级全景

实例化参数 > OS 环境变量 > .env 文件 > 字段默认值

生产环境你在 OS 设DEEPSEEK_API_KEY=正式key,本地用.envDEEPSEEK_API_KEY=测试key——同一份代码、不同环境自动读到不同值,不用改代码。这就是多环境配置能跑起来的底层机制。

二、结构化日志:从 print 到 dictConfig

print 的五个毛病

  1. 没级别(INFO/WARN/ERROR 混在一起,没法过滤)
  2. 没时间戳、没文件名、没行号
  3. 不能同时输出到终端和文件
  4. 不能按级别动态切换(dev 想看 DEBUG,prod 只看 INFO)
  5. 关掉print要改代码

logging模块解决以上所有问题。dictConfig是结构化配置——把"日志怎么打"用字典声明,比代码一行行logger.addHandler(...)配置更清晰、更易切换。

三件套:Logger / Handler / Formatter

  • Logger(入口):你写logger.info("xxx")就是给它发消息。它决定"这条消息要不要往下传"(按级别过滤)。
  • Handler(出口):决定消息发到哪儿——终端(StreamHandler)、文件(FileHandler)、网络、邮件。每个 Handler 各自管自己的级别过滤。
  • Formatter(格式化):决定消息长啥样——加时间戳、文件名、行号、级别标签。

两层过滤(最容易绕晕的地方)

  • Logger 设级别= “低于这个级别的消息我直接丢,不传给 Handler”
  • Handler 设级别= “低于这个级别的消息我收到了但也不输出”
  • 两者取交集——消息必须通过 Logger 级别 + Handler 级别两层才能输出

我自己的需求是"终端 DEBUG + 文件 INFO",靠的就是两层过滤:Logger 级别设 DEBUG(让 DEBUG 消息能传出去),两个 Handler 各自设级别(终端 DEBUG 全收、文件 INFO 起步)。

举例:Logger 级别=DEBUG,Handler 级别=INFO。

  • logger.debug("x")通过 Logger(DEBUG≥DEBUG)但被 Handler 拦下(DEBUG<INFO)→不输出
  • logger.info("x")两层都通过 →输出

dictConfig 落地

importlogging.configfrompathlibimportPathfromapp.core.configimportSettingsdefsetup_logging(settings:Settings)->None:# 提前建日志目录,否则 FileHandler 第一次写文件时崩log_path=Path(settings.LOG_FILE)log_path.parent.mkdir(parents=True,exist_ok=True)logging.config.dictConfig({"version":1,"disable_existing_loggers":False,# 关键!不禁用已注册 logger(uvicorn 等)"formatters":{"default":{"format":"%(asctime)s | %(levelname)-8s | %(name)s | %(message)s","datefmt":"%Y-%m-%d %H:%M:%S",},},"handlers":{"console":{"class":"logging.StreamHandler","level":"DEBUG","formatter":"default","stream":"ext://sys.stdout",# ext:// 协议引用 Python 对象},"file":{"class":"logging.FileHandler","level":"INFO","formatter":"default","filename":settings.LOG_FILE,"encoding":"utf-8",},},"loggers":{"app":{"level":settings.LOG_LEVEL.upper(),# 动态读配置,不写死"handlers":["console","file"],"propagate":False,# 切断向父 logger 传播,避免重复输出},},"root":{"level":"WARNING","handlers":["console"],},})

几个关键点我特意记一下:

  • disable_existing_loggers: False—— 默认True会禁用所有已存在 logger(包括 uvicorn 的),导致 uvicorn 访问日志消失。一律写False
  • propagate: False—— 子 logger 处理完消息后默认会向父 logger 传播,父 logger 再处理一遍 → 重复输出。切断传播避免重复。
  • ext://sys.stdout—— dictConfig 是纯字典配置,不能直接写 Python 对象,用ext://协议字符串引用。ext://sys.stdout=sys.stdout
  • settings.LOG_LEVEL.upper()—— 动态读配置,不写死 DEBUG。生产 OS 设LOG_LEVEL=INFO自动屏蔽 DEBUG,排查时改LOG_LEVEL=DEBUG立刻打开,不用动代码。

logger 继承

logging.getLogger("app.main")创建的 logger 名为"app.main",Python logging 把.当层级分隔符——"app.main""app"的子 logger。子 logger 没设级别 → 继承父 logger 的级别;没自己的 handler → 消息 propagate 到父 logger,由父的 handler 处理。"app"设了propagate=False→ 不再向 root 传播,链条到此为止。

三、完整接口整合:把散装能力串成链路

这一节是把前面几篇学的依赖注入、LlmService、async、异常处理、重试、mock 测试、日志全整合到一个接口里。前面那些知识点都是为这一刻铺路。

三层架构(这节的灵魂)

文件职责厚薄
路由层app/api/routes/jd.py接收请求、调服务、返回薄(不写业务逻辑、不 try/except)
服务层app/services/llm_service.py业务逻辑、调 LLM、解析、异常映射
依赖层app/api/dependencies.py注入服务实例

反模式:路由里直接OpenAI()调 LLM——三层混一起,无法测试、无法替换。

路由核心链路

@router.post("/analyze",response_model=JdAnalysisResult)asyncdefanalyze(req:JdAnalyzeRequest,llm_service:LlmService=Depends(get_llm_service),):logger.info("analyze called, jd_text length=%s",len(req.jd_text))result=awaitllm_service.analyze_jd(req.jd_text)logger.info("analyze success: job_title=%s, skills_count=%s",result.job_title,len(result.required_skills))returnresult
  • response_model=JdAnalysisResult—— FastAPI 自动序列化 + 生成 /docs + 字段过滤
  • req: JdAnalyzeRequest—— FastAPI 自动用 Pydantic 校验请求体,失败抛RequestValidationError→ 422
  • Depends(get_llm_service)—— 依赖注入,路由不 new 服务,解耦
  • 路由不写 try/except—— 异常由全局处理器接

异常映射全景

场景异常状态码语义
请求 jd_text 缺失/过短/过长RequestValidationError422客户端错
LLM 重试耗尽ServiceError503上游不可用
LLM 返回坏 JSONBadGatewayError502上游返回无效响应
其他未捕获Exception兜底500服务端 bug

坏 JSON 的 502 映射是这节的核心。之前parse_jd_analysisValueError(或 pydanticValidationError)走 Exception 兜底 → 500,语义错位——把上游的锅算成了我们的 bug。HTTP 状态码语义:

  • 500 Internal Server Error—— “服务端自己出 bug”。是我们的错,不该重试,得修代码。
  • 502 Bad Gateway—— “上游网关返回了无效响应”。上游返回了垃圾,不是我们的 bug,重试可能成功。
  • 503 Service Unavailable—— “服务暂时不可用”。上游连不上/超时/限流,重试可能成功。

坏 JSON 是 LLM(上游)返回了垃圾——语义上该是 502,不是 500。修复:在analyze_jd内 try/except 捕获ValueError+PydanticValidationError,抛BadGatewayError(502)

frompydanticimportValidationErrorasPydanticValidationErrorfromapp.core.exceptionsimportBadGatewayErrorasyncdefanalyze_jd(self,jd_text:str)->JdAnalysisResult:raw=awaitself.call_llm(...)try:returnparse_jd_analysis(raw)except(ValueError,PydanticValidationError)ase:raiseBadGatewayError(message="LLM 返回的内容无法解析为有效 JSON",detail=str(e),)

起别名PydanticValidationError避免跟项目的ValidationError(422 那个)撞名——这是这节最容易踩的坑。两个ValidationError完全不同:

名字来源用途是不是 AppException
app.core.exceptions.ValidationError我们项目AppException 子类,code=422,路由层抛✅ 是
pydantic.ValidationErrorPydantic 库model_validate失败时自动抛❌ 不是

4 场景端到端测试

用 TestClient +dependency_overrides注入假 service,测 HTTP 链路 + 异常映射:

classFakeLlmService:"""鸭子类型假 service,不继承 LlmService 避免触发 AsyncOpenAI 初始化。"""def__init__(self,return_value=None,raise_exception=None):self.return_value=return_value self.raise_exception=raise_exceptionasyncdefanalyze_jd(self,jd_text:str):ifself.raise_exception:raiseself.raise_exceptionreturnself.return_value@pytest.fixture(autouse=True)defcleanup_overrides():yieldapp.dependency_overrides.clear()# 避免测试间污染deftest_analyze_success(client):app.dependency_overrides[get_llm_service]=lambda:FakeLlmService(return_value=_make_result())response=client.post("/api/v1/jd/analyze",json={"jd_text":VALID_JD})assertresponse.status_code==200deftest_analyze_short_jd_text_422(client):response=client.post("/api/v1/jd/analyze",json={"jd_text":"短"})assertresponse.status_code==422# FastAPI 自动校验,FakeLlmService 没被调deftest_analyze_llm_unavailable_503(client):app.dependency_overrides[get_llm_service]=lambda:FakeLlmService(raise_exception=ServiceError(message="LLM 超时"))response=client.post("/api/v1/jd/analyze",json={"jd_text":VALID_JD})assertresponse.status_code==503deftest_analyze_bad_json_502(client):app.dependency_overrides[get_llm_service]=lambda:FakeLlmService(raise_exception=BadGatewayError(message="无法解析"))response=client.post("/api/v1/jd/analyze",json={"jd_text":VALID_JD})assertresponse.status_code==502

两种 mock 模式的区别(这个区分是测试设计的灵魂):

  • 服务层单元测试test_analyze_jd_mock.py):mockclient.chat.completions.create(网络边界),业务逻辑真跑——验"加工过程对不对"
  • 端到端集成测试test_jd_analyze.py):mockLlmService整个(鸭子类型 FakeLlmService),测 HTTP 链路 + 异常映射——验"调度对不对"

比喻:服务层单元测试是验"工厂流水线对不对",端到端测试是验"整条供应链调度对不对"。两者互补,不能互相替代。

四、几个我踩过的坑

把变量名写成了字符串字面量

logging_config.py时我把settings.LOG_LEVEL.upper()(调用对象属性方法)写成了"settings.LOG_LEVEL".upper()(字符串字面量.upper())。dictConfig 收到"SETTINGS.LOG_LEVEL"找不到对应级别抛 ValueError,pytest 加载 conftest 时炸。

教训:Python 里"xxx"是字符串字面量,xxx是变量/表达式,差一个引号语义完全不同"settings.LOG_LEVEL".upper()是把字符串"settings.LOG_LEVEL"转大写 ="SETTINGS.LOG_LEVEL"settings.LOG_LEVEL.upper()是访问settings对象的LOG_LEVEL属性、再调它的.upper()方法。

改级别改错位置:handler 还是 logger

我说改 “app” logger 的 level(loggers段下),结果改到了 console handler 的 level(handlers段下)。console handler 级别保留 DEBUG(终端看 DEBUG 细节),“app” logger 级别才该读settings.LOG_LEVEL(控制全局消息能否传出)。

教训:改代码前先精确定位"改哪一行、改哪个字段",别凭印象改。两层过滤里,Logger 级别管"消息能不能往下传",Handler 级别管"消息能不能输出"——改 Logger 级别才影响全局,改 Handler 级别只影响一个出口。

pre-commit 因 import 排序挂掉

commit 时 ruff 检测到 import 顺序不规范(Organize imports),pre-commit restore 了改动,commit 失败。修法是 commit 前先ruff check --fix .修 import 排序,再 add + commit。

教训:pre-commit 是故意"不让坏代码进 git"的设计,改完再 commit 是正常流程不是失败。以后 commit 前养成"先ruff check --fix ."的习惯。

五、小结

多环境配置让代码不动配置跟环境走,结构化日志让print升级成有级别有格式的logging,三层架构 + 异常映射把散装能力串成完整接口。这三块是从"能跑的代码"往"工程化的代码"挪的核心动作。

路由薄、服务厚、依赖注入——这三层分离让代码可换、可测、可替换。两层过滤(Logger 级别 + Handler 级别取交集)+ 多环境优先级(实例化参数 > OS > .env > 默认值),是配置和日志能"跟环境走、可动态切"的底层机制。

后面做任何 LLM / Agent 接口,这三层架构 + 422/503/502/500 四种状态码语义 + 两层 mock 测试(服务层单元 + 端到端集成),都是底层认知,提前理清能少踩不少坑。

http://www.jsqmd.com/news/1228711/

相关文章:

  • Frappe Books:开源会计软件如何重塑中小企业财务管理体验
  • 2026年geo排名监测工具深度测评:竞品AI声量对标监测能力哪家强? - GEORANK
  • 如何通过开源语音合成技术实现技术民主化:面向开发者的社区共建完整指南
  • 生产环境中的Agent记忆管理与状态持久化——让Agent从“金鱼”进化为“大象”
  • 高校严查AI写作!如何让论文“天然抗检测”,这3招是关键
  • RedisInsight:从命令行到可视化,Redis管理的终极进化
  • NPM供应链攻击防御:从left-pad事件看依赖安全
  • 2026 青岛崂山艾米龙手表高价回收,逸程对照实时行情估价,无任何隐形扣费 - 全城热点
  • 2026固原景区古建牌坊检测排名 TOP5 CMA 资质机构提供牌坊裂缝检测、牌坊倾斜检测、老化检测 联系方式推荐 - 诚金汇钻回收公司
  • 如何自定义Learn-to-Cluster:扩展新数据集和模型架构的完整指南
  • 终极可视化代码编辑器:Onlook如何重塑设计到开发的完整工作流
  • 用豆包写简历 VS 用专业AI简历工具,差距有多大
  • 本地大模型落地难?Ollama核心命令速查表,95%开发者漏掉的5个关键参数
  • OrcaPlayground 最小 MVP 从踩坑到闭环指南
  • 终极免费三国杀卡牌制作工具:Lyciumaker新手完全指南
  • Klipper 3D打印机固件:如何实现专业级打印质量与极致性能
  • 为什么你的RPA项目总失败?AI智能体自动化办公的4层认知跃迁与2024技术栈选型决策树
  • tprPix跨平台开发实战:如何一次编写,三平台运行
  • TradingAgents-CN终极指南:5分钟掌握AI多智能体金融分析框架
  • 函数调用技术:构建高效AI Agents的核心方法
  • ECCV 2026 | NanoVSR:让视频超分在边缘设备上“飞“起来
  • 2026年geo优化工具推荐权威指南:免费GEO自测工具矩阵深度测评 - GEORANK
  • 2026年上海环境试验设备怎么选不踩雷?5家实测对比与国产替代推荐 - 中国远见品牌企业资讯
  • 股票/基金实时行情采集--从行情API到实时监控面板的全链路实战
  • 2026 曲靖家用商用中央空调维修实测|全品牌故障检修清洗移机加氟,无隐形消费优选欧米到家 - 欧米到家
  • 如何用gh_mirrors/nixo/nixos-config打造跨平台Nix环境?从入门到精通
  • 如何用ESP-IoT-Solution构建智能显示系统:从零到一的5步实战指南
  • 导购 APP 订单追踪为什么会延迟?淘客技术链路完整拆解说明
  • Socket.IO Redis Emitter入门教程:5分钟快速搭建分布式WebSocket系统
  • Tiny Slider 2.9.4版本迁移实战指南:技术升级与架构演进深度解析