岗位自动投递agent 对话功能
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
# 求职助手 Agent 对话接口设计方案
|
||||||
|
|
||||||
|
## 1. 接口概述
|
||||||
|
|
||||||
|
`POST /job-agent/chat` — 求职助手对话接口,Python 端实现。
|
||||||
|
|
||||||
|
用户通过对话与求职助手交互,AI 根据用户简历和求职意向提供求职建议,识别用户意图后触发前端工具调用(岗位推荐 / 调整偏好)。
|
||||||
|
|
||||||
|
## 2. 入参 Schema
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ChatMessage(BaseModel):
|
||||||
|
role: Literal["user", "assistant"]
|
||||||
|
content: str
|
||||||
|
|
||||||
|
class JobAgentChatParam(BaseModel):
|
||||||
|
message: str = Field(..., description="用户输入的消息")
|
||||||
|
resume_id: int = Field(..., alias="resumeId", description="简历ID")
|
||||||
|
history: list[ChatMessage] = Field(default_factory=list, description="对话历史")
|
||||||
|
job_categories: list[str] = Field(default_factory=list, alias="jobCategories", description="意向岗位类型名称")
|
||||||
|
regions: list[str] = Field(default_factory=list, alias="regions", description="意向城市名称")
|
||||||
|
industries: list[str] = Field(default_factory=list, alias="industries", description="意向行业名称")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 出参 Schema
|
||||||
|
|
||||||
|
```python
|
||||||
|
class ToolParams(BaseModel):
|
||||||
|
preference: str = Field(default="", description="用户岗位偏好描述,仅tool=recommend时有值")
|
||||||
|
|
||||||
|
class JobAgentChatDto(BaseModel):
|
||||||
|
message: str = Field(..., description="AI回复文本,不超过200字")
|
||||||
|
tool: str | None = Field(default=None, description="前端需执行的工具:recommend / editPreference / null")
|
||||||
|
tool_params: ToolParams | None = Field(default=None, alias="toolParams", description="工具参数")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. AI 上下文构造
|
||||||
|
|
||||||
|
每次对话请求,后端自动查询以下数据拼入 system prompt:
|
||||||
|
|
||||||
|
1. **简历信息**:根据 resumeId 查 bg_user_resume 主表 + 5张子表(教育/工作/实习/项目/竞赛),序列化为文本
|
||||||
|
2. **求职意向**:直接使用前端传入的 jobCategories / regions / industries(中文名称)
|
||||||
|
|
||||||
|
## 5. System Prompt 设计
|
||||||
|
|
||||||
|
```
|
||||||
|
你是 OfferPie 求职助手,帮助用户找到合适的工作。
|
||||||
|
|
||||||
|
【用户简历】
|
||||||
|
{resume_text}
|
||||||
|
|
||||||
|
【求职意向】
|
||||||
|
意向岗位:{job_categories}
|
||||||
|
意向城市:{regions}
|
||||||
|
意向行业:{industries}
|
||||||
|
|
||||||
|
【你的能力】
|
||||||
|
1. 回答求职相关问题(面试技巧、简历建议、行业分析等),回复不超过200字
|
||||||
|
2. 当用户想看岗位推荐时,提取用户的偏好描述,调用岗位推荐工具
|
||||||
|
3. 当用户想修改求职偏好/设置时,调用偏好设置工具
|
||||||
|
|
||||||
|
【输出格式】
|
||||||
|
严格返回 JSON,不要其他内容:
|
||||||
|
{"message":"回复内容","tool":null,"toolParams":null}
|
||||||
|
|
||||||
|
tool 可选值:
|
||||||
|
- null:普通对话,不触发工具
|
||||||
|
- "recommend":岗位推荐,toolParams 必须包含 {"preference":"用户偏好描述"}
|
||||||
|
- "editPreference":调整偏好,toolParams 为 null
|
||||||
|
|
||||||
|
【规则】
|
||||||
|
1. 只聊求职相关话题,其他话题礼貌拒绝
|
||||||
|
2. 回复简洁,不超过200字
|
||||||
|
3. 用户表达想看岗位、推荐岗位、帮我找工作等意图时,从对话中提取偏好描述,返回 recommend
|
||||||
|
4. 用户表达想改设置、调整偏好、修改意向等意图时,返回 editPreference
|
||||||
|
5. 偏好描述要准确概括用户的岗位偏好,如"更偏技术方向的产品岗"、"大厂优先"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 处理流程
|
||||||
|
|
||||||
|
1. 校验登录态,获取 user_id
|
||||||
|
2. 根据 resume_id 查简历主表 + 5张子表,序列化为文本
|
||||||
|
3. 构造 system prompt(简历 + 求职意向 + 规则)
|
||||||
|
4. 构造 messages 列表:system prompt + history + 当前 message
|
||||||
|
5. 调用 LLM(使用 LLM.DEEPSEEK_V3 或配置的模型)
|
||||||
|
6. 用 parse_llm_json 解析 AI 返回的 JSON
|
||||||
|
7. 构造 JobAgentChatDto 返回
|
||||||
|
|
||||||
|
## 7. 前端交互流程
|
||||||
|
|
||||||
|
1. 前端发消息 → 调 `POST /job-agent/chat`
|
||||||
|
2. 拿到返回后判断 tool 字段:
|
||||||
|
- `tool=null` → 直接展示 message
|
||||||
|
- `tool="recommend"` → 展示 message + 用 toolParams.preference 调 Java 端 `POST /job/agent/recommend` 拿岗位列表展示
|
||||||
|
- `tool="editPreference"` → 展示 message + 打开偏好设置页面
|
||||||
|
|
||||||
|
## 8. 涉及文件
|
||||||
|
|
||||||
|
| 文件 | 位置 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `job_agent_chat.py` | app/schemas/ | Pydantic 请求/响应 Schema |
|
||||||
|
| `job_agent_chat.py` | app/api/ | 路由定义 |
|
||||||
|
| `job_agent_chat_service.py` | app/services/ | Service:查简历 + 构造 prompt + 调 LLM |
|
||||||
|
| `prompts.py` | app/ai/job_agent/ | System Prompt 模板 |
|
||||||
|
| `main.py` | app/ | 注册新路由 |
|
||||||
|
|
||||||
|
## 9. 依赖的现有模块
|
||||||
|
|
||||||
|
- `app/models/user_resume.py` + 5张子表 ORM — 查简历数据
|
||||||
|
- `app/ai/models.py` — LLM 枚举,创建模型实例
|
||||||
|
- `app/tool/json_helper.py` — parse_llm_json 解析 AI 输出
|
||||||
|
- `app/core/context.py` — RequestContext 获取 user_id
|
||||||
|
- `app/core/database.py` — get_db 获取数据库会话
|
||||||
|
|
||||||
|
## 10. 不需要新增的 ORM 模型
|
||||||
|
|
||||||
|
- 求职意向由前端传入中文名称,不查 bg_user_job_intention 表
|
||||||
|
- 简历相关 ORM 模型已全部存在
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""求职助手 Agent 对话 AI 模块"""
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""求职助手 Agent 对话 AI 引擎
|
||||||
|
|
||||||
|
构造 prompt + 调 LLM + 解析返回。
|
||||||
|
依赖:LLM 枚举、job_agent/prompts、parse_llm_json
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app.ai.job_agent.prompts import SYSTEM_PROMPT
|
||||||
|
from app.ai.models import LLM
|
||||||
|
from app.core.logger import log
|
||||||
|
from app.tool.json_helper import parse_llm_json
|
||||||
|
|
||||||
|
|
||||||
|
async def agent_chat(resume_text: str, message: str, history: list[dict],
|
||||||
|
job_categories: list[str], regions: list[str],
|
||||||
|
industries: list[str]) -> dict:
|
||||||
|
"""求职助手对话
|
||||||
|
1. 构造 system prompt 2. 拼 messages 3. 调 LLM 4. 解析返回
|
||||||
|
"""
|
||||||
|
# 1. 构造 system prompt
|
||||||
|
system_content = SYSTEM_PROMPT.format(
|
||||||
|
resume_text=resume_text,
|
||||||
|
job_categories="、".join(job_categories) if job_categories else "未设置",
|
||||||
|
regions="、".join(regions) if regions else "未设置",
|
||||||
|
industries="、".join(industries) if industries else "未设置",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. 拼 messages
|
||||||
|
messages = [("system", system_content)]
|
||||||
|
for msg in history:
|
||||||
|
messages.append((msg["role"], msg["content"]))
|
||||||
|
messages.append(("human", message))
|
||||||
|
|
||||||
|
# 3. 调 LLM
|
||||||
|
try:
|
||||||
|
llm = LLM.JIAYU_CLAUDE_SONNET_4_5.create(temperature=0.7)
|
||||||
|
result = await llm.ainvoke(messages)
|
||||||
|
raw = result.content
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"求职助手AI调用失败: {e}")
|
||||||
|
return {"message": "抱歉,我暂时无法回复,请稍后再试。", "tool": None, "toolParams": None}
|
||||||
|
|
||||||
|
# 4. 解析返回
|
||||||
|
try:
|
||||||
|
parsed = parse_llm_json(raw)
|
||||||
|
return {
|
||||||
|
"message": parsed.get("message", ""),
|
||||||
|
"tool": parsed.get("tool"),
|
||||||
|
"toolParams": parsed.get("toolParams"),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"求职助手AI返回解析失败, raw={raw}, error={e}")
|
||||||
|
return {"message": raw.strip() if raw else "抱歉,我暂时无法回复。", "tool": None, "toolParams": None}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""求职助手 Agent 对话 Prompt 模板"""
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """你是 OfferPie 求职助手,帮助用户找到合适的工作。
|
||||||
|
|
||||||
|
【用户简历】
|
||||||
|
{resume_text}
|
||||||
|
|
||||||
|
【求职意向】
|
||||||
|
意向岗位:{job_categories}
|
||||||
|
意向城市:{regions}
|
||||||
|
意向行业:{industries}
|
||||||
|
|
||||||
|
【你的能力】
|
||||||
|
1. 回答求职相关问题(面试技巧、简历建议、行业分析等),回复不超过200字
|
||||||
|
2. 当用户想看岗位推荐时,提取用户的偏好描述,调用岗位推荐工具
|
||||||
|
3. 当用户想修改求职偏好/设置时,调用偏好设置工具
|
||||||
|
|
||||||
|
【输出格式】
|
||||||
|
严格返回 JSON,不要其他内容:
|
||||||
|
{{"message":"回复内容","tool":null,"toolParams":null}}
|
||||||
|
|
||||||
|
tool 可选值:
|
||||||
|
- null:普通对话,不触发工具
|
||||||
|
- "recommend":岗位推荐,toolParams 必须包含 {{"preference":"用户偏好描述"}}
|
||||||
|
- "editPreference":调整偏好,toolParams 为 null
|
||||||
|
|
||||||
|
【规则】
|
||||||
|
1. 只聊求职相关话题,其他话题礼貌拒绝
|
||||||
|
2. 回复简洁,不超过200字
|
||||||
|
3. 用户表达想看岗位、推荐岗位、帮我找工作等意图时,从对话中提取偏好描述,返回 recommend
|
||||||
|
4. 用户表达想改设置、调整偏好、修改意向等意图时,返回 editPreference
|
||||||
|
5. 偏好描述要准确概括用户的岗位偏好,如"更偏技术方向的产品岗"、"大厂优先"
|
||||||
|
6. 如果用户没有明确偏好,preference 填"无特殊偏好"
|
||||||
|
"""
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""求职助手 Agent 对话接口"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.core.context import RequestContext
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.schemas.job_agent_chat import JobAgentChatParam
|
||||||
|
from app.services.job_agent_chat_service import JobAgentChatService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/job-agent", tags=["求职助手agent"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/chat", summary="求职助手对话")
|
||||||
|
async def chat(param: JobAgentChatParam):
|
||||||
|
"""求职助手对话,根据用户简历和意向提供求职建议、触发岗位推荐或偏好调整"""
|
||||||
|
user_id = RequestContext.user_id.get()
|
||||||
|
async for session in get_db():
|
||||||
|
service = JobAgentChatService(session)
|
||||||
|
result = await service.chat(
|
||||||
|
user_id, param.resume_id, param.message,
|
||||||
|
[msg.model_dump() for msg in param.history],
|
||||||
|
param.job_categories, param.regions, param.industries,
|
||||||
|
)
|
||||||
|
return result
|
||||||
@@ -34,11 +34,13 @@ from app.api.health import router as health_router
|
|||||||
from app.api.resume import router as resume_router
|
from app.api.resume import router as resume_router
|
||||||
from app.api.resume_diagnose import router as resume_diagnose_router
|
from app.api.resume_diagnose import router as resume_diagnose_router
|
||||||
from app.api.skill_gap import router as skill_gap_router
|
from app.api.skill_gap import router as skill_gap_router
|
||||||
|
from app.api.job_agent_chat import router as job_agent_chat_router
|
||||||
|
|
||||||
app.include_router(health_router)
|
app.include_router(health_router)
|
||||||
app.include_router(resume_router)
|
app.include_router(resume_router)
|
||||||
app.include_router(resume_diagnose_router)
|
app.include_router(resume_diagnose_router)
|
||||||
app.include_router(skill_gap_router)
|
app.include_router(skill_gap_router)
|
||||||
|
app.include_router(job_agent_chat_router)
|
||||||
# ==============================
|
# ==============================
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""求职助手 Agent 对话 Schema
|
||||||
|
|
||||||
|
请求参数 Param、响应 Dto。
|
||||||
|
字段命名使用 camelCase alias,与前端 JSON 对齐。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
ChatRole = Literal["user", "assistant"]
|
||||||
|
|
||||||
|
|
||||||
|
class ChatMessage(BaseModel):
|
||||||
|
role: ChatRole
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class JobAgentChatParam(BaseModel):
|
||||||
|
message: str = Field(..., description="用户输入的消息")
|
||||||
|
resume_id: int = Field(..., alias="resumeId", description="简历ID")
|
||||||
|
history: list[ChatMessage] = Field(default_factory=list, description="对话历史")
|
||||||
|
job_categories: list[str] = Field(default_factory=list, alias="jobCategories", description="意向岗位类型名称")
|
||||||
|
regions: list[str] = Field(default_factory=list, alias="regions", description="意向城市名称")
|
||||||
|
industries: list[str] = Field(default_factory=list, alias="industries", description="意向行业名称")
|
||||||
|
|
||||||
|
|
||||||
|
class ToolParams(BaseModel):
|
||||||
|
preference: str = Field(default="", description="用户岗位偏好描述")
|
||||||
|
|
||||||
|
|
||||||
|
class JobAgentChatDto(BaseModel):
|
||||||
|
message: str = Field(..., description="AI回复文本,不超过200字")
|
||||||
|
tool: str | None = Field(default=None, description="前端需执行的工具:recommend / editPreference / null")
|
||||||
|
tool_params: ToolParams | None = Field(default=None, alias="toolParams", description="工具参数")
|
||||||
|
|
||||||
|
model_config = {"populate_by_name": True}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""求职助手 Agent 对话 Service
|
||||||
|
|
||||||
|
主要功能:查询简历数据,调用 AI 模块完成对话。
|
||||||
|
依赖:UserResume + 5张子表 ORM、job_agent.chat AI 模块
|
||||||
|
使用表:bg_user_resume、bg_user_resume_education/work/internship/project/competition(查询简历)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.ai.job_agent.chat import agent_chat
|
||||||
|
from app.models.user_resume import UserResume
|
||||||
|
from app.models.user_resume_education import UserResumeEducation
|
||||||
|
from app.models.user_resume_work import UserResumeWork
|
||||||
|
from app.models.user_resume_internship import UserResumeInternship
|
||||||
|
from app.models.user_resume_project import UserResumeProject
|
||||||
|
from app.models.user_resume_competition import UserResumeCompetition
|
||||||
|
|
||||||
|
|
||||||
|
class JobAgentChatService:
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
async def chat(self, user_id: int, resume_id: int, message: str,
|
||||||
|
history: list[dict], job_categories: list[str],
|
||||||
|
regions: list[str], industries: list[str]) -> dict:
|
||||||
|
"""求职助手对话:查简历 → 序列化 → 调 AI 模块"""
|
||||||
|
resume = await self._get_resume(resume_id, user_id)
|
||||||
|
edu, work, intern, proj, comp = await self._load_sub_tables(resume_id)
|
||||||
|
resume_text = self._build_resume_text(resume, edu, work, intern, proj, comp)
|
||||||
|
return await agent_chat(resume_text, message, history, job_categories, regions, industries)
|
||||||
|
|
||||||
|
async def _get_resume(self, resume_id: int, user_id: int) -> UserResume:
|
||||||
|
"""查指定简历"""
|
||||||
|
result = await self.session.execute(select(UserResume).where(UserResume.id == resume_id, UserResume.user_id == user_id))
|
||||||
|
resume = result.scalar_one_or_none()
|
||||||
|
if not resume:
|
||||||
|
raise ValueError("简历不存在")
|
||||||
|
return resume
|
||||||
|
|
||||||
|
async def _load_sub_tables(self, resume_id: int):
|
||||||
|
"""查询简历5张子表"""
|
||||||
|
edu = (await self.session.execute(select(UserResumeEducation).where(UserResumeEducation.resume_id == resume_id))).scalars().all()
|
||||||
|
work = (await self.session.execute(select(UserResumeWork).where(UserResumeWork.resume_id == resume_id))).scalars().all()
|
||||||
|
intern = (await self.session.execute(select(UserResumeInternship).where(UserResumeInternship.resume_id == resume_id))).scalars().all()
|
||||||
|
proj = (await self.session.execute(select(UserResumeProject).where(UserResumeProject.resume_id == resume_id))).scalars().all()
|
||||||
|
comp = (await self.session.execute(select(UserResumeCompetition).where(UserResumeCompetition.resume_id == resume_id))).scalars().all()
|
||||||
|
return edu, work, intern, proj, comp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_resume_text(resume: UserResume, edu_list, work_list, intern_list, proj_list, comp_list) -> str:
|
||||||
|
"""将简历数据序列化为文本供 AI 使用"""
|
||||||
|
parts = []
|
||||||
|
if resume.name:
|
||||||
|
parts.append(f"姓名:{resume.name}")
|
||||||
|
if resume.target_position:
|
||||||
|
parts.append(f"目标岗位:{resume.target_position}")
|
||||||
|
if resume.skills:
|
||||||
|
parts.append(f"技能:{'、'.join(resume.skills)}")
|
||||||
|
if resume.certificates:
|
||||||
|
parts.append(f"证书:{'、'.join(resume.certificates)}")
|
||||||
|
if resume.summary:
|
||||||
|
parts.append(f"个人概述:{resume.summary}")
|
||||||
|
if edu_list:
|
||||||
|
parts.append("教育经历:")
|
||||||
|
for r in edu_list:
|
||||||
|
parts.append(f" - {r.school or ''} {r.major or ''} {r.degree or ''}")
|
||||||
|
if work_list:
|
||||||
|
parts.append("工作经历:")
|
||||||
|
for r in work_list:
|
||||||
|
parts.append(f" - {r.company_name or ''} {r.position or ''}")
|
||||||
|
if intern_list:
|
||||||
|
parts.append("实习经历:")
|
||||||
|
for r in intern_list:
|
||||||
|
parts.append(f" - {r.company_name or ''} {r.position or ''}")
|
||||||
|
if proj_list:
|
||||||
|
parts.append("项目经历:")
|
||||||
|
for r in proj_list:
|
||||||
|
parts.append(f" - {r.project_name or ''} {r.role or ''}")
|
||||||
|
if comp_list:
|
||||||
|
parts.append("竞赛经历:")
|
||||||
|
for r in comp_list:
|
||||||
|
parts.append(f" - {r.competition_name or ''} {r.award or ''}")
|
||||||
|
return "\n".join(parts) if parts else "暂无简历信息"
|
||||||
Reference in New Issue
Block a user