agent学习Day5——Ruff、日志与结构化 LLM 输出
一、工程规范让问题在提交前暴露Ruff、pre-commit 和 logging 分别解决什么今天先重新学习了项目工程规范。Ruff 是静态检查工具主要检查未使用的 import、import 排序、代码格式等问题pre-commit 则把 Ruff 挂到git commit上提交前自动检查不通过就拒绝提交。git commit - pre-commit hook - ruff check - 通过才能创建 commit实际测试时我故意在main.py中加入未使用的import os。提交时 pre-commit 报出F401 os imported but unused I001 Import block is un-sorted or un-formatted说明 hook 已经生效。修复后git diff --check又发现了 Ruff 当前规则未覆盖的行尾空格问题。经验Ruff、pre-commit 和git diff --check不是重复工具。它们关注的点不同提交前一起跑更稳妥。logging不只是打印信息print()只能临时调试真正的项目应该使用 logging因为日志有级别、时间、模块来源也可以在以后接入文件或监控系统。logging.basicConfig(levellogging.INFO,format%(asctime)s - %(name)s - %(levelname)s - %(message)s,)loggerlogging.getLogger(__name__)这里basicConfig()只在main.py这类应用入口配置一次每个模块用getLogger(__name__)获取自己的 loggerINFO记录正常业务事件logger.exception()记录失败事件和完整异常堆栈。调用/health和/api/v1/jd/analyze-basic后可以同时看到业务日志和 Uvicorn 访问日志app.api.routes.jd - INFO - analyze-basic called, jd_text length33 app.api.routes.jd - INFO - analyze-basic response: word_count33, has_pythonTrue INFO: 127.0.0.1 - POST /api/v1/jd/analyze-basic HTTP/1.1 200 OK业务日志说明“代码内部做了什么”访问日志说明“请求最终返回了什么状态”。二、封装 JD 分析链路从脚本到服务层为什么要封装之前 Prompt、LLM 调用和 JSON 解析分散在scripts/llm_playground.py中。脚本能跑不代表项目可复用未来路由、Agent 或其他功能都不应该知道 Prompt 怎么拼、模型怎么调、JSON 怎么清洗。所以将职责拆成两层app/services/llm_service.py负责调用模型并返回原始字符串app/services/jd_analysis.py负责 JD Prompt、JSON 清洗、Pydantic 校验和结果返回。主链路最终变成JD 文本 - build_jd_user_prompt() - call_llm() - 原始模型文本 - clean_json_response() - json.loads() - JdAnalysisResult.model_validate() - 返回结构化结果核心函数如下defanalyze_jd(jd_text:str)-JdAnalysisResult:user_promptbuild_jd_user_prompt(jd_text)raw_responsecall_llm(promptuser_prompt,system_promptSYSTEM_PROMPT_JD,temperature0,)returnparse_jd_analysis(raw_response)temperature0的目的是让结构化抽取尽量稳定避免模型自由发挥。LLM 调用层的日志在call_llm()中记录 Prompt 长度、耗时、输出长度和异常状态start_timetime.perf_counter()try:responseclient.chat.completions.create(...)exceptException:logger.exception(LLM call failed: prompt_length%s, duration_ms%.2f,len(prompt),duration_ms,)raise这里使用time.perf_counter()而不是系统时间因为它更适合测量耗时精度高并且不会因为系统校时发生跳变。日志只记录长度、耗时和状态不记录完整 JD 或 API Key避免敏感信息泄露。三、固定假响应测试先验证链路再调用真实模型为什么不能只测真实 API真实模型输出有波动也会消耗额度。自动化测试如果依赖真实 API可能今天通过、明天失败无法稳定定位代码问题。因此先用 fake LLM 替换真实call_llm()deffake_call_llm(prompt:str,system_prompt:str,temperature:float,)-str:returnjson { job_title: Python后端工程师, required_skills: [Python, FastAPI], responsibilities: [开发和维护 REST API], keywords: [Python, FastAPI], difficulty: 中等 } 测试覆盖了五种情况正常 JSON带 Markdown 围栏的 JSONLLM 调用异常非法 JSONJSON 合法但字段类型错误。两类错误的区别JSONDecodeError - 模型输出不是合法 JSON无法转成 Python dict ValidationError - JSON 可以解析但字段缺失或类型不符合 JdAnalysisResult还有第三类内容理解错误。例如 JD 明确要求 Redis但模型返回的技能列表漏掉 Redis。此时 JSON 合法、类型也正确Pydantic 无法发现因为它只校验数据结构不理解原始 JD 内容。四、真实 JD 质量检查最后用 5 段固定 JD 测试了 Python 后端、机器学习、前端、数据分析和 AI 产品经理岗位。结构化解析成功率5/5每次真实调用都记录了 Prompt 长度、耗时和输出长度平均耗时约 1 到 2 秒。质量检查中发现一个值得继续优化的点FastAPI 或 Django、React 或 Vue这类候选技能会被扁平化为普通列表。模型没有编造技能但当前required_skills: list[str]无法表达“二选一”。后续可以增加类似字段alternative_skills skill_groups让结构化结果更准确地保留 JD 的原始语义。本次完成的内容包括 LLM 调用封装、JD 分析主链路、日志、固定假响应测试和真实 JD 质量检查。