From 18589adf8caca1a1771ef2e79f76de1347e5e948 Mon Sep 17 00:00:00 2001 From: zk Date: Tue, 26 May 2026 15:10:21 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=8A=BD=E8=B1=A1=20?= =?UTF-8?q?=E5=B0=81=E8=A3=85=20=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .kiro/steering/代码开发风格文档.md | 25 ++++++++++- .kiro/steering/项目结构说明.md | 28 ++++++++++++- app/ai/browser_plug/form_filler.py | 5 +-- app/ai/job_agent/chat.py | 5 +-- app/ai/job_agent/resume_optimizer.py | 6 +-- app/ai/model_config.py | 60 +++++++++++++++++++++++++++ app/ai/nova_chat/chat.py | 5 +-- app/ai/resume_diagnoser/diagnoser.py | 8 ++-- app/ai/resume_extractor/extractor.py | 6 +-- app/ai/skill_gap_analyzer/analyzer.py | 14 +++---- 10 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 app/ai/model_config.py diff --git a/.kiro/steering/代码开发风格文档.md b/.kiro/steering/代码开发风格文档.md index f753dc2..43ccbc4 100644 --- a/.kiro/steering/代码开发风格文档.md +++ b/.kiro/steering/代码开发风格文档.md @@ -135,11 +135,32 @@ inclusion: manual ## AI 调用规范 -- 通过 `LLM` 枚举创建模型实例:`LLM.DEEPSEEK_V3.create(temperature=0)` -- kwargs 透传给 LangChain `ChatOpenAI`(temperature、max_tokens 等) +- 业务代码**不直接使用** `LLM` 枚举,而是从 `app.ai.model_config` 中引用对应模块的场景配置类 +- `model_config.py` 中每个模块一个 class,每个场景一个类属性,属性值为预创建的 `ChatOpenAI` 实例 +- 修改模型或调整参数只需改 `model_config.py` 一个文件,业务代码不动 - AI 调用应做好异常捕获和容错,单次失败不应影响整体流程 - 长耗时 AI 调用考虑异步执行 +### 模型引用示例 +```python +from app.ai.model_config import SkillGapModel + +# chain 中直接使用配置类属性(已经是 ChatOpenAI 实例) +_plan_chain = ( + ChatPromptTemplate.from_messages([...]) + | SkillGapModel.AGENT_PLAN + | StrOutputParser() +) + +# 非 chain 场景直接 await 调用 +result = await JobAgentModel.CHAT.ainvoke(messages) +``` + +### 新增 AI 场景步骤 +1. 在 `app/ai/model_config.py` 对应模块的 class 中新增一个类属性,指定模型和参数 +2. 在业务代码中 `from app.ai.model_config import XxxModel`,引用该属性 +3. 如需新增模块,在 `model_config.py` 中新建一个 class + ### AI 输出 JSON 解析 - LLM 返回的 JSON 经常被 markdown 代码块(` ```json ... ``` `)包裹,**禁止**直接使用 LangChain 的 `JsonOutputParser` - 统一使用 `app.tool.json_helper.parse_llm_json` 解析 AI 输出的 JSON 文本 diff --git a/.kiro/steering/项目结构说明.md b/.kiro/steering/项目结构说明.md index c65a1e1..1db00bf 100644 --- a/.kiro/steering/项目结构说明.md +++ b/.kiro/steering/项目结构说明.md @@ -31,7 +31,8 @@ offerpie_python_ai/ │ └─ responses.py # 统一响应模型 StandardResponse(code/msg/data/timestamp/uuid) │ ├─ ai/ # **AI 能力层** - │ ├─ models.py # LLM 模型枚举(LLM.DOUBAO_PRO_256K、DEEPSEEK_V3、GPT_4O 等),基于 LangChain ChatOpenAI + │ ├─ models.py # LLM 模型枚举(LLM.DOUBAO_PRO_32K、DEEPSEEK_V3、GPT_4O 等),基于 LangChain ChatOpenAI + │ ├─ model_config.py # AI 模型场景配置(集中管理各模块的模型选择与参数,业务代码引用此文件而非直接使用 LLM 枚举) │ ├─ resume_extractor/ # 简历 AI 提取模块 │ │ ├─ prompts.py # 5 个提取任务的 System Prompt(个人信息/教育/工作+实习/项目/竞赛) │ │ └─ extractor.py # AI 并行提取(extract_all 入口,asyncio.gather 5 路并行) @@ -101,7 +102,7 @@ offerpie_python_ai/ |------|----------|-------------| | **config** | 统一配置管理,基于 Pydantic Settings,支持 .env 文件加载 | `Settings`(数据库、Redis、LLM供应商、JWT、CORS、日志等全部配置项) | | **core** | 核心基础设施:数据库连接、Redis连接、鉴权、日志、中间件、异常处理、统一响应 | `database.py`、`redis.py`、`auth.py`、`middleware.py`、`exceptions.py`、`logger.py`、`StandardResponse` | -| **ai** | AI 模型管理 + 业务 AI 能力 | `LLM` 枚举、`resume_extractor/`(简历并行提取)、`resume_diagnoser/`(简历诊断)、`skill_gap_analyzer/`(技能差距分析 + 定制简历优化 + Agent 原子化规划 + 单条记录修改/新增)、`job_agent/`(求职助手对话 + 岗位简历优化)、`nova_chat/`(Nova 对话助手,纯对话) | +| **ai** | AI 模型管理 + 业务 AI 能力 | `LLM` 枚举(models.py)、`model_config.py`(场景模型配置)、`resume_extractor/`(简历并行提取)、`resume_diagnoser/`(简历诊断)、`skill_gap_analyzer/`(技能差距分析 + 定制简历优化 + Agent 原子化规划 + 单条记录修改/新增)、`job_agent/`(求职助手对话 + 岗位简历优化)、`nova_chat/`(Nova 对话助手,纯对话) | | **api** | REST API 路由定义 | `health.py`(健康检查)、`resume.py`(简历上传解析)、`resume_diagnose.py`(简历诊断)、`skill_gap.py`(技能差距分析 + 生成定制简历 + AI对话编辑)、`customize_resume.py`(定制简历查询/修改/回滚)、`job_agent_chat.py`(求职助手对话 + 岗位简历优化)、`nova_chat.py`(Nova 对话助手) | | **models** | SQLAlchemy ORM 模型,与 Java 端共享同一数据库 | `FuncPermission`、`UserFuncPermissionStock`、`UserFuncUsageLog`、`UserResume`、`UserResumeEducation`/`Work`/`Internship`/`Project`/`Competition`、`ResumeDiagnosisReport`、`ResumeDiagnosisIssue`、`Job`(只读)、`JobAgentConfig`、`UserJobCustomizeResume` | | **tool** | 无状态通用工具,不依赖数据库/Redis/用户上下文 | `file_parser.py`(PDF/Word/TXT 文件解析为纯文本)、`json_helper.py`(AI 输出 JSON 解析,去 markdown 代码块 + json_repair 容错)、`snowflake.py`(雪花ID生成) | @@ -141,11 +142,15 @@ offerpie_python_ai/ 4. 业务异常自动回退 ## 6️⃣ AI 模型配置 + +### 模型定义(`app/ai/models.py`) | 供应商 | 模型 | 枚举值 | |--------|------|--------| | 火山引擎 | doubao-1-5-pro-32k-250115 | `LLM.DOUBAO_PRO_32K` | +| 火山引擎 | doubao-1-5-lite-32k-250115 | `LLM.DOUBAO_LITE_32K` | | 火山引擎 | deepseek-v3-250324 | `LLM.DEEPSEEK_V3` | | 火山引擎 | deepseek-r1-250528 | `LLM.DEEPSEEK_R1` | +| 火山引擎 | doubao-seed-2-0-mini-260215 | `LLM.DOUBAO_SEED_MINI` | | 火山引擎 | doubao-seed-2-0-lite-260215 | `LLM.DOUBAO_SEED_LITE` | | 火山引擎 | doubao-seed-2-0-pro-260215 | `LLM.DOUBAO_SEED_PRO` | | 加鱼 | gpt-4o | `LLM.GPT_4O` | @@ -157,9 +162,28 @@ offerpie_python_ai/ | 加鱼 | glm-5 | `LLM.JIAYU_GLM_5` | | 加鱼 | qwen3-coder-next | `LLM.JIAYU_QWEN3_CODER_NEXT` | | 加鱼 | minimax-m2.5 | `LLM.JIAYU_MINIMAX_M2_5` | +| ZM | gpt-5.5 | `LLM.ZM_GPT_5_5` | +| ZM | gpt-5.4 | `LLM.ZM_GPT_5_4` | +| ZM | gpt-5.4-mini | `LLM.ZM_GPT_5_4_MINI` | +| ZM | gpt-5.2 | `LLM.ZM_GPT_5_2` | 所有模型通过 `LLM.XXX.create(**kwargs)` 创建 LangChain `ChatOpenAI` 实例,kwargs 透传 temperature、max_tokens 等参数。 +### 场景模型配置(`app/ai/model_config.py`) + +业务代码**不直接使用** `LLM` 枚举,而是通过 `model_config.py` 中的场景配置类引用预创建的模型实例。修改模型或参数只需改此文件。 + +| 配置类 | 场景 | 说明 | +|--------|------|------| +| `SkillGapModel` | ANALYSIS / SUMMARY / EXPERIENCE / AGENT_PLAN / AGENT_EDIT / AGENT_ADD | 技能差距分析模块 | +| `JobAgentModel` | CHAT / SUMMARY / EXPERIENCE | 求职助手Agent模块 | +| `NovaChatModel` | CHAT | Nova智能聊天模块 | +| `ResumeExtractorModel` | PARSE | 简历解析模块 | +| `DiagnoserModel` | MODULE / SUMMARY / POLISH | 简历诊断模块 | +| `BrowserPlugModel` | FORM_FILL | 浏览器插件模块 | + +业务代码引用示例:`from app.ai.model_config import SkillGapModel`,然后在 chain 中直接使用 `SkillGapModel.ANALYSIS`。 + ## 7️⃣ 与 Java 后端的关系 - **共享数据库**:Python 端与 Java 端(back-end)连接同一 MySQL 数据库(offerpie),ORM 模型对应相同的表 - **共享 Redis**:共享登录态(`login:token:{userId}`),JWT Secret 一致 diff --git a/app/ai/browser_plug/form_filler.py b/app/ai/browser_plug/form_filler.py index 2a456be..c33add2 100644 --- a/app/ai/browser_plug/form_filler.py +++ b/app/ai/browser_plug/form_filler.py @@ -5,7 +5,7 @@ """ from app.ai.browser_plug.prompts import FORM_FILL_SYSTEM_PROMPT -from app.ai.models import LLM +from app.ai.model_config import BrowserPlugModel from app.core.logger import log @@ -27,8 +27,7 @@ async def generate_form_answer(resume_text: str, job_text: str, agent_config_tex messages = [("system", system_content), ("human", user_message)] try: - llm = LLM.DOUBAO_PRO_32K.create(temperature=0.3) - result = await llm.ainvoke(messages) + result = await BrowserPlugModel.FORM_FILL.ainvoke(messages) return result.content.strip() except Exception as e: log.error(f"表单填写AI调用失败: {e}") diff --git a/app/ai/job_agent/chat.py b/app/ai/job_agent/chat.py index d5994ac..0cfcd84 100644 --- a/app/ai/job_agent/chat.py +++ b/app/ai/job_agent/chat.py @@ -5,7 +5,7 @@ """ from app.ai.job_agent.prompts import SYSTEM_PROMPT -from app.ai.models import LLM +from app.ai.model_config import JobAgentModel from app.core.logger import log from app.tool.json_helper import parse_llm_json @@ -32,8 +32,7 @@ async def agent_chat(resume_text: str, message: str, history: list[dict], # 3. 调 LLM try: - llm = LLM.ZM_GPT_5_4.create(temperature=0.7) - result = await llm.ainvoke(messages) + result = await JobAgentModel.CHAT.ainvoke(messages) raw = result.content except Exception as e: log.error(f"求职助手AI调用失败: {e}") diff --git a/app/ai/job_agent/resume_optimizer.py b/app/ai/job_agent/resume_optimizer.py index 94d0b17..24af689 100644 --- a/app/ai/job_agent/resume_optimizer.py +++ b/app/ai/job_agent/resume_optimizer.py @@ -10,7 +10,7 @@ from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from app.ai.job_agent.prompts import RESUME_SUMMARY_OPTIMIZE_PROMPT, RESUME_EXPERIENCE_OPTIMIZE_PROMPT -from app.ai.models import LLM +from app.ai.model_config import JobAgentModel from app.core.logger import log from app.tool.json_helper import parse_llm_json @@ -18,7 +18,7 @@ from app.tool.json_helper import parse_llm_json _summary_chain = ( ChatPromptTemplate.from_messages([("system", RESUME_SUMMARY_OPTIMIZE_PROMPT), ("human", "请开始优化。")]) - | LLM.ZM_GPT_5_4_MINI.create(temperature=0.3) + | JobAgentModel.SUMMARY | StrOutputParser() ) @@ -42,7 +42,7 @@ async def optimize_summary(job_title: str, job_description: str, original_summar _experience_chain = ( ChatPromptTemplate.from_messages([("system", RESUME_EXPERIENCE_OPTIMIZE_PROMPT), ("human", "请开始优化。")]) - | LLM.ZM_GPT_5_4_MINI.create(temperature=0.3) + | JobAgentModel.EXPERIENCE | StrOutputParser() ) diff --git a/app/ai/model_config.py b/app/ai/model_config.py new file mode 100644 index 0000000..ca0436c --- /dev/null +++ b/app/ai/model_config.py @@ -0,0 +1,60 @@ +"""AI 模型场景配置 + +集中管理各业务模块的模型选择与参数,修改模型只需改此文件。 +""" + +from app.ai.models import LLM + + +class SkillGapModel: + """技能差距分析模块""" + # 技能差距识别:对比简历与岗位技能标签,输出缺失技能列表 + ANALYSIS = LLM.DOUBAO_LITE_32K.create(temperature=0) + # 个人概述优化:将缺失技能关键词融入 summary + SUMMARY = LLM.DOUBAO_LITE_32K.create(temperature=0.3) + # 经历描述优化:针对目标岗位优化单条经历的 description + EXPERIENCE = LLM.DOUBAO_LITE_32K.create(temperature=0.3) + # Agent规划:解析用户自然语言指令,拆解为原子编辑操作列表 + AGENT_PLAN = LLM.ZM_GPT_5_4.create(temperature=0) + # Agent执行-修改:按指令修改简历中的单条记录 + AGENT_EDIT = LLM.ZM_GPT_5_4.create(temperature=0.3) + # Agent执行-新增:按指令生成一条新的简历记录 + AGENT_ADD = LLM.ZM_GPT_5_4.create(temperature=0.3) + + +class JobAgentModel: + """求职助手Agent模块""" + # 多轮对话:理解用户求职意图,返回结构化回复(message+tool调用) + CHAT = LLM.ZM_GPT_5_4.create(temperature=0.7) + # 岗位简历-summary优化:针对具体岗位JD优化个人概述 + SUMMARY = LLM.ZM_GPT_5_4_MINI.create(temperature=0.3) + # 岗位简历-经历优化:针对具体岗位JD优化单条经历描述 + EXPERIENCE = LLM.ZM_GPT_5_4_MINI.create(temperature=0.3) + + +class NovaChatModel: + """Nova智能聊天模块""" + # 通用对话:基于简历和岗位上下文的自由问答,返回Markdown文本 + CHAT = LLM.ZM_GPT_5_2.create(temperature=0.7) + + +class ResumeExtractorModel: + """简历解析模块""" + # 简历结构化提取:两阶段并行提取简历文本为JSON结构 + PARSE = LLM.DOUBAO_LITE_32K.create(temperature=0) + + +class DiagnoserModel: + """简历诊断模块""" + # 模块诊断:逐条分析经历记录的问题(错别字/无量化/弱相关等) + MODULE = LLM.ZM_GPT_5_4_MINI.create(temperature=0) + # 整体评价:汇总所有诊断结果生成总结性评语 + SUMMARY = LLM.ZM_GPT_5_4_MINI.create(temperature=0.3) + # 内容润色:用户编辑后的文本做专业润色 + POLISH = LLM.JIAYU_CLAUDE_SONNET_4_5.create(temperature=0.3) + + +class BrowserPlugModel: + """浏览器插件模块""" + # 表单自动填写:根据简历+岗位信息生成招聘网站表单字段的回答 + FORM_FILL = LLM.DOUBAO_PRO_32K.create(temperature=0.3) diff --git a/app/ai/nova_chat/chat.py b/app/ai/nova_chat/chat.py index a84ec16..7f6b666 100644 --- a/app/ai/nova_chat/chat.py +++ b/app/ai/nova_chat/chat.py @@ -4,7 +4,7 @@ 依赖:LLM 枚举、nova_chat/prompts """ -from app.ai.models import LLM +from app.ai.model_config import NovaChatModel from app.ai.nova_chat.prompts import SYSTEM_PROMPT from app.core.logger import log @@ -24,8 +24,7 @@ async def nova_chat(resume_text: str, message: str, history: list[dict], messages.append(("human", message)) try: - llm = LLM.ZM_GPT_5_2.create(temperature=0.7) - result = await llm.ainvoke(messages) + result = await NovaChatModel.CHAT.ainvoke(messages) return result.content.strip() except Exception as e: log.error(f"Nova Chat AI 调用失败: {e}") diff --git a/app/ai/resume_diagnoser/diagnoser.py b/app/ai/resume_diagnoser/diagnoser.py index 67c3cd5..8b8c999 100644 --- a/app/ai/resume_diagnoser/diagnoser.py +++ b/app/ai/resume_diagnoser/diagnoser.py @@ -5,7 +5,7 @@ import asyncio from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate -from app.ai.models import LLM +from app.ai.model_config import DiagnoserModel from app.ai.resume_diagnoser.prompts import DIAGNOSE_MODULE_PROMPT, SUMMARY_PROMPT, POLISH_PROMPT from app.core.logger import log from app.tool.json_helper import parse_llm_json @@ -14,14 +14,14 @@ from app.tool.json_helper import parse_llm_json # 诊断链(StrOutputParser 拿原始文本,再手动解析 JSON,避免 markdown 代码块导致解析失败) _diagnose_chain = ( ChatPromptTemplate.from_messages([("system", DIAGNOSE_MODULE_PROMPT), ("human", "请开始诊断。")]) - | LLM.ZM_GPT_5_4_MINI.create(temperature=0) + | DiagnoserModel.MODULE | StrOutputParser() ) # 汇总评价链(纯文本输出) _summary_chain = ( ChatPromptTemplate.from_messages([("system", SUMMARY_PROMPT), ("human", "请生成整体评价。")]) - | LLM.ZM_GPT_5_4_MINI.create(temperature=0.3) + | DiagnoserModel.SUMMARY | StrOutputParser() ) @@ -55,7 +55,7 @@ async def generate_summary(grade: str, urgent_total: int, important_total: int, _polish_chain = ( ChatPromptTemplate.from_messages([("system", POLISH_PROMPT), ("human", "请开始优化。")]) - | LLM.JIAYU_CLAUDE_SONNET_4_5.create(temperature=0.3) + | DiagnoserModel.POLISH | StrOutputParser() ) diff --git a/app/ai/resume_extractor/extractor.py b/app/ai/resume_extractor/extractor.py index de28b18..da5d3d9 100644 --- a/app/ai/resume_extractor/extractor.py +++ b/app/ai/resume_extractor/extractor.py @@ -11,7 +11,7 @@ import time from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate -from app.ai.models import LLM +from app.ai.model_config import ResumeExtractorModel from app.ai.resume_extractor.prompts import ( OVERVIEW_PROFILE_PROMPT, OVERVIEW_EDUCATION_PROMPT, OVERVIEW_WORK_PROMPT, OVERVIEW_PROJECT_PROMPT, OVERVIEW_COMPETITION_PROMPT, @@ -21,14 +21,12 @@ from app.ai.resume_extractor.prompts import ( from app.core.logger import log from app.tool.json_helper import parse_llm_json -_LLM_MODEL = LLM.DOUBAO_LITE_32K - # ==================== LLM 调用工具 ==================== def _build_chain(prompt: str): """构建提取链:prompt → LLM → 文本输出""" - return ChatPromptTemplate.from_messages([("system", prompt), ("human", "{text}")]) | _LLM_MODEL.create(temperature=0) | StrOutputParser() + return ChatPromptTemplate.from_messages([("system", prompt), ("human", "{text}")]) | ResumeExtractorModel.PARSE | StrOutputParser() async def _safe_invoke(chain, inp: dict, label: str): diff --git a/app/ai/skill_gap_analyzer/analyzer.py b/app/ai/skill_gap_analyzer/analyzer.py index bd3a7fe..a620574 100644 --- a/app/ai/skill_gap_analyzer/analyzer.py +++ b/app/ai/skill_gap_analyzer/analyzer.py @@ -10,7 +10,7 @@ import time from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate -from app.ai.models import LLM +from app.ai.model_config import SkillGapModel from app.ai.skill_gap_analyzer.prompts import ( SKILL_GAP_PROMPT, SUMMARY_OPTIMIZE_PROMPT, EXPERIENCE_OPTIMIZE_PROMPT, AGENT_PLAN_PROMPT, AGENT_MODULE_EDIT_PROMPT, AGENT_MODULE_ADD_PROMPT, MODULE_SCHEMAS, @@ -23,7 +23,7 @@ from app.tool.json_helper import parse_llm_json _skill_gap_chain = ( ChatPromptTemplate.from_messages([("system", SKILL_GAP_PROMPT), ("human", "请开始分析。")]) - | LLM.DOUBAO_LITE_32K.create(temperature=0) + | SkillGapModel.ANALYSIS | StrOutputParser() ) @@ -45,7 +45,7 @@ async def analyze_skill_gap(skill_tags: list[str], resume_json: str) -> list[str _summary_optimize_chain = ( ChatPromptTemplate.from_messages([("system", SUMMARY_OPTIMIZE_PROMPT), ("human", "请开始优化。")]) - | LLM.DOUBAO_LITE_32K.create(temperature=0.3) + | SkillGapModel.SUMMARY | StrOutputParser() ) @@ -69,7 +69,7 @@ async def optimize_summary(job_title: str, add_skills: list[str], original_summa _experience_optimize_chain = ( ChatPromptTemplate.from_messages([("system", EXPERIENCE_OPTIMIZE_PROMPT), ("human", "请开始优化。")]) - | LLM.DOUBAO_LITE_32K.create(temperature=0.3) + | SkillGapModel.EXPERIENCE | StrOutputParser() ) @@ -94,7 +94,7 @@ async def optimize_module(job_title: str, job_description: str, module_data: str _plan_chain = ( ChatPromptTemplate.from_messages([("system", AGENT_PLAN_PROMPT), ("human", "请分析用户指令。")]) - | LLM.ZM_GPT_5_4.create(temperature=0) + | SkillGapModel.AGENT_PLAN | StrOutputParser() ) @@ -119,7 +119,7 @@ async def plan_edit(job_title: str, job_description: str, resume_json: str, _record_edit_chain = ( ChatPromptTemplate.from_messages([("system", AGENT_MODULE_EDIT_PROMPT), ("human", "请执行修改。")]) - | LLM.ZM_GPT_5_4.create(temperature=0.3) + | SkillGapModel.AGENT_EDIT | StrOutputParser() ) @@ -144,7 +144,7 @@ async def execute_record_edit(job_title: str, job_description: str, instruction: _record_add_chain = ( ChatPromptTemplate.from_messages([("system", AGENT_MODULE_ADD_PROMPT), ("human", "请生成新记录。")]) - | LLM.ZM_GPT_5_4.create(temperature=0.3) + | SkillGapModel.AGENT_ADD | StrOutputParser() )