- FastAPI backend with SQLAlchemy ORM and SQLite - AI chatbot with OpenAI-compatible LLM integration (SSE streaming) - Admin panel for content management, LLM config, token management - Anonymous access with 3-question limit, token-based access control - Recruiter intent detection with admin notification - Resume generator (JD-based, Markdown to Word export) - Chinese localized public interface Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
699 lines
33 KiB
Markdown
699 lines
33 KiB
Markdown
# CareerBot 项目设计文档
|
||
|
||
## 1. 项目概述
|
||
|
||
CareerBot 是一个个人职业展示网站,集成了 AI 智能对话助手。主要面向两类用户:
|
||
|
||
- **访问者(招聘方)**:浏览候选人的职业背景,通过 AI 对话深入了解候选人,上传 JD 进行匹配分析
|
||
- **管理员(候选人)**:管理个人资料、教育/工作经历、技能等内容,配置 LLM,管理访问令牌,查看招聘意向消息,生成定制简历
|
||
|
||
### 技术栈
|
||
|
||
| 层级 | 技术 |
|
||
|------|------|
|
||
| 后端框架 | Python / FastAPI |
|
||
| 模板引擎 | Jinja2 |
|
||
| 数据库 | SQLite + SQLAlchemy ORM |
|
||
| LLM 集成 | OpenAI 兼容 API (httpx) |
|
||
| 实时通信 | Server-Sent Events (SSE) |
|
||
| 认证 | JWT (管理员) / Cookie (访问者) |
|
||
| 密码加密 | bcrypt |
|
||
| 文件处理 | python-docx, PyPDF2, Pillow |
|
||
|
||
---
|
||
|
||
## 2. 项目结构
|
||
|
||
```
|
||
CareerBot/
|
||
├── app/
|
||
│ ├── __init__.py
|
||
│ ├── main.py # FastAPI 应用入口,启动初始化
|
||
│ ├── config.py # Pydantic Settings 配置
|
||
│ ├── database.py # SQLAlchemy 引擎、Session、init_db
|
||
│ ├── models.py # 全部 ORM 模型定义
|
||
│ ├── routers/
|
||
│ │ ├── auth.py # 认证:管理员登录、访问令牌验证、匿名访问
|
||
│ │ ├── public.py # 公开页面路由和数据 API
|
||
│ │ ├── admin.py # 管理后台 CRUD、简历生成、消息管理
|
||
│ │ └── chat.py # 聊天 API:对话、历史、招聘意图检测
|
||
│ ├── services/
|
||
│ │ ├── chat_service.py # 聊天业务逻辑:构建 prompt、处理消息
|
||
│ │ ├── llm_service.py # LLM 调用封装:流式/非流式、视觉降级
|
||
│ │ └── file_parser.py # 文件解析:txt/docx/pdf/image
|
||
│ └── static/
|
||
│ ├── css/style.css # 全局样式
|
||
│ └── js/
|
||
│ ├── main.js # 主页数据加载(个人信息、技能、经历)
|
||
│ ├── chat.js # 聊天组件(SSE 流式、提问限制、匿名模式)
|
||
│ └── admin.js # 管理后台 CRUD 操作、消息管理
|
||
├── templates/
|
||
│ ├── index.html # 主页(职业展示 + 聊天组件)
|
||
│ ├── login.html # 访问者登录/匿名入口
|
||
│ └── admin/
|
||
│ ├── login.html # 管理员登录
|
||
│ ├── dashboard.html # 仪表盘
|
||
│ ├── profile.html # 个人资料管理
|
||
│ ├── education.html # 教育经历管理
|
||
│ ├── experience.html # 工作经历管理
|
||
│ ├── skills.html # 技能管理
|
||
│ ├── tokens.html # 访问令牌管理
|
||
│ ├── messages.html # 招聘消息查看
|
||
│ ├── llm-config.html # LLM 配置
|
||
│ └── resume.html # 简历生成
|
||
├── init_data.py # 数据库初始化种子数据
|
||
├── requirements.txt # Python 依赖
|
||
├── start.bat # 启动脚本
|
||
└── stop.bat # 停止脚本
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 系统架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Browser │
|
||
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||
│ │ index.html │ │ login.html │ │ admin/*.html │ │
|
||
│ │ main.js │ │ (Token/匿名入口) │ │ admin.js │ │
|
||
│ │ chat.js │ │ │ │ │ │
|
||
│ └──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘ │
|
||
└─────────┼──────────────────┼──────────────────────┼─────────────┘
|
||
│ HTTP/SSE │ HTTP │ HTTP
|
||
▼ ▼ ▼
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ FastAPI Application │
|
||
│ │
|
||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
|
||
│ │ public.py │ │ auth.py │ │ chat.py │ │ admin.py │ │
|
||
│ │ 页面路由 │ │ 认证鉴权 │ │ 聊天API │ │ 管理CRUD │ │
|
||
│ │ 数据API │ │ JWT/Cookie │ │ SSE流式 │ │ 简历生成 │ │
|
||
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └────┬─────┘ │
|
||
│ │ │ │ │ │
|
||
│ │ ┌──────┴──────────┐ │ │ │
|
||
│ │ │ get_current_ │ │ │ │
|
||
│ │ │ admin/visitor │◄───┤ │ │
|
||
│ │ └─────────────────┘ │ │ │
|
||
│ │ │ │ │
|
||
│ ┌─────┴───────────────────────────────┴──────────────┴─────┐ │
|
||
│ │ Services Layer │ │
|
||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||
│ │ │chat_service │ │ llm_service │ │ file_parser │ │ │
|
||
│ │ │System Prompt │ │ OpenAI API │ │ txt/docx/pdf │ │ │
|
||
│ │ │消息处理/历史 │─▶│ 流式/非流式 │ │ image/base64 │ │ │
|
||
│ │ └──────────────┘ └──────┬───────┘ └──────────────┘ │ │
|
||
│ └───────────────────────────┼──────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌───────────────────────────┼──────────────────────────────┐ │
|
||
│ │ SQLAlchemy ORM │ │
|
||
│ │ Profile │ Skill │ Education │ WorkExperience │ AdminUser │ │
|
||
│ │ AccessToken │ LLMConfig │ ChatHistory │ RecruiterMessage │ │
|
||
│ └───────────────────────────┼──────────────────────────────┘ │
|
||
└──────────────────────────────┼──────────────────────────────────┘
|
||
│
|
||
┌──────────▼──────────┐
|
||
│ SQLite (WAL模式) │
|
||
│ careerbot.db │
|
||
└─────────────────────┘
|
||
|
||
│ httpx (异步)
|
||
┌──────────▼──────────┐
|
||
│ LLM API Provider │
|
||
│ (DeepSeek/Qwen/ │
|
||
│ OpenAI/Zhipu) │
|
||
└─────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 数据模型(类图)
|
||
|
||
```
|
||
┌─────────────────────┐ ┌─────────────────────────┐
|
||
│ Profile │ │ AdminUser │
|
||
├─────────────────────┤ ├─────────────────────────┤
|
||
│ id: int (PK) │ │ id: int (PK) │
|
||
│ name: str │ │ email: str (unique) │
|
||
│ phone: str │ │ password_hash: str │
|
||
│ location: str │ └─────────────────────────┘
|
||
│ birthday: str │
|
||
│ party: str │ ┌─────────────────────────┐
|
||
│ education_level: str│ │ AccessToken │
|
||
│ email: str │ ├─────────────────────────┤
|
||
│ photo_url: str? │ │ id: int (PK) │
|
||
│ self_summary: text │ │ token: str (unique) │
|
||
└─────────────────────┘ │ created_at: datetime │
|
||
│ is_active: bool │
|
||
┌─────────────────────┐ │ note: str? │
|
||
│ Skill │ │ max_questions: int [=9] │
|
||
├─────────────────────┤ │ used_questions: int [=0] │
|
||
│ id: int (PK) │ └─────────────────────────┘
|
||
│ category: str │
|
||
│ content: text │ ┌─────────────────────────┐
|
||
│ sort_order: int │ │ LLMConfig │
|
||
└─────────────────────┘ ├─────────────────────────┤
|
||
│ id: int (PK) │
|
||
┌─────────────────────┐ │ api_url: str │
|
||
│ Education │ │ api_key: str │
|
||
├─────────────────────┤ │ model_name: str │
|
||
│ id: int (PK) │ │ max_tokens: int [=2048] │
|
||
│ start_date: str │ │ temperature: float [=0.7]│
|
||
│ end_date: str │ │ system_prompt: text? │
|
||
│ school: str │ │ is_active: bool │
|
||
│ major: str │ │ max_questions_per_session│
|
||
│ degree: str │ └─────────────────────────┘
|
||
│ details: text │
|
||
│ sort_order: int │ ┌─────────────────────────┐
|
||
└─────────────────────┘ │ ChatHistory │
|
||
├─────────────────────────┤
|
||
┌─────────────────────┐ │ id: int (PK) │
|
||
│ WorkExperience │ │ session_id: str (索引) │
|
||
├─────────────────────┤ │ role: str │
|
||
│ id: int (PK) │ │ content: text │
|
||
│ start_date: str │ │ created_at: datetime │
|
||
│ end_date: str │ └─────────────────────────┘
|
||
│ company: str │
|
||
│ position: str │ ┌─────────────────────────┐
|
||
│ company_intro: text │ │ RecruiterMessage │
|
||
│ responsibilities: t │ ├─────────────────────────┤
|
||
│ achievements: text │ │ id: int (PK) │
|
||
│ sort_order: int │ │ session_id: str (索引) │
|
||
└─────────────────────┘ │ visitor_label: str │
|
||
│ company: str │
|
||
│ contact: str │
|
||
│ intent: str │
|
||
│ summary: text │
|
||
│ is_read: bool │
|
||
│ created_at: datetime │
|
||
└─────────────────────────┘
|
||
```
|
||
|
||
**模型分组关系:**
|
||
|
||
```
|
||
候选人内容数据: Profile ──┬── Skill (1:N)
|
||
├── Education (1:N)
|
||
└── WorkExperience (1:N)
|
||
|
||
访问控制: AdminUser (管理员)
|
||
AccessToken (访问者令牌) ── 关联 max_questions / used_questions
|
||
|
||
AI 配置: LLMConfig (LLM连接配置 + 默认提问次数)
|
||
|
||
对话数据: ChatHistory (按 session_id 分组)
|
||
RecruiterMessage (招聘意图检测结果)
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 认证与访问控制
|
||
|
||
### 5.1 三种访问身份
|
||
|
||
```
|
||
┌───────────────┬───────────────────┬──────────────┬──────────────┐
|
||
│ 身份 │ 认证方式 │ 浏览内容 │ 对话限制 │
|
||
├───────────────┼───────────────────┼──────────────┼──────────────┤
|
||
│ 管理员 │ JWT (Bearer Token)│ 管理后台 │ 无限制 │
|
||
│ Token 访问者 │ Cookie (visitor_ │ 主页+对话 │ 按token配额 │
|
||
│ │ token = 实际token)│ │ (默认9次) │
|
||
│ 匿名访问者 │ Cookie (visitor_ │ 主页+对话 │ 3次 │
|
||
│ │ token=__anonymous__)│ │ │
|
||
└───────────────┴───────────────────┴──────────────┴──────────────┘
|
||
```
|
||
|
||
### 5.2 访问者认证流程
|
||
|
||
```
|
||
访问 / (主页)
|
||
│
|
||
▼
|
||
有 visitor_token Cookie?
|
||
│
|
||
┌────┴────┐
|
||
│ No │ Yes
|
||
▼ ▼
|
||
重定向 token == "__anonymous__"?
|
||
/login │
|
||
│ ┌────┴────┐
|
||
▼ │ Yes │ No
|
||
登录页 │ ▼
|
||
│ │ DB查询token有效?
|
||
│ │ │
|
||
│ │ ┌────┴────┐
|
||
│ │ │ Yes │ No
|
||
│ │ │ ▼
|
||
│ │ │ 重定向 /login
|
||
│ ▼ ▼
|
||
│ 渲染 index.html
|
||
│
|
||
├─ 输入Token → POST /api/verify-token → set cookie → 跳转 /
|
||
└─ 匿名访问 → POST /api/anonymous-entry → set cookie(__anonymous__) → 跳转 /
|
||
```
|
||
|
||
### 5.3 管理员认证
|
||
|
||
- **登录**: `POST /api/admin/login` → 验证 email + bcrypt 密码 → 返回 JWT
|
||
- **鉴权**: 所有 `/api/admin/*` 接口通过 `get_current_admin` 依赖校验 JWT
|
||
- **JWT 存储**: 前端 `localStorage.admin_token`,请求时放入 `Authorization: Bearer` Header
|
||
- **有效期**: 480 分钟(8 小时)
|
||
|
||
---
|
||
|
||
## 6. 核心流程
|
||
|
||
### 6.1 智能对话流程
|
||
|
||
```
|
||
用户发送消息 (chat.js)
|
||
│
|
||
│ POST /api/chat (FormData: session_id, message, file?)
|
||
▼
|
||
┌─── chat.py ─────────────────────────────────────────┐
|
||
│ │
|
||
│ 1. 身份识别: get_current_visitor │
|
||
│ ├─ AccessToken记录 → 检查 used_questions < max │
|
||
│ └─ None (匿名) → 查 ChatHistory 计数 < 3 │
|
||
│ │
|
||
│ 2. 超限 → 返回限制提示的 SSE │
|
||
│ 未超限 → 继续 │
|
||
│ │
|
||
│ 3. Token用户: used_questions += 1, db.commit() │
|
||
│ │
|
||
│ 4. 调用 chat_service.process_message() │
|
||
│ │ │
|
||
│ ┌──▼── chat_service.py ──────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ a. 解析上传文件 (file_parser) │ │
|
||
│ │ ├─ txt/docx/pdf → 提取文本 │ │
|
||
│ │ └─ 图片 → base64编码 │ │
|
||
│ │ │ │
|
||
│ │ b. 查询最近20条 ChatHistory │ │
|
||
│ │ │ │
|
||
│ │ c. 构建 messages: │ │
|
||
│ │ [system_prompt, ...history, user_msg] │ │
|
||
│ │ │ │
|
||
│ │ d. 调用 llm_service.chat_completion_stream │ │
|
||
│ │ │ │
|
||
│ │ e. 保存 user/assistant 消息到 ChatHistory │ │
|
||
│ │ (错误消息不保存) │ │
|
||
│ └─────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 5. SSE 流式返回 → chat.js 逐块渲染 │
|
||
│ │
|
||
│ 6. BackgroundTask: 招聘意图检测 │
|
||
│ (独立DB Session, 不影响主流程) │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 6.2 LLM 调用流程(含视觉降级)
|
||
|
||
```
|
||
chat_completion_stream(messages, db)
|
||
│
|
||
▼
|
||
读取 LLMConfig (is_active=True)
|
||
│
|
||
├─ 无配置 → yield "AI助手尚未配置"
|
||
│
|
||
▼
|
||
构建请求:
|
||
URL: {api_url}/v1/chat/completions
|
||
Headers: Authorization: Bearer {api_key}
|
||
Body: { model, messages, max_tokens, temperature, stream: true }
|
||
│
|
||
▼
|
||
发送流式请求 (httpx.AsyncClient.stream)
|
||
│
|
||
┌────┴────────────────┐
|
||
│ 成功 │ 400 错误 且 消息含图片
|
||
│ │
|
||
▼ ▼
|
||
逐块 yield 视觉降级处理:
|
||
content _strip_images(messages)
|
||
├─ 移除 image_url 内容
|
||
├─ 保留文本部分
|
||
└─ 追加提示: "模型不支持图片, 请改用txt/docx/pdf"
|
||
│
|
||
▼
|
||
重试 (不含图片的messages)
|
||
│
|
||
┌────┴────┐
|
||
│ 成功 │ 失败
|
||
▼ ▼
|
||
逐块 yield 错误信息
|
||
yield
|
||
```
|
||
|
||
### 6.3 招聘意图检测流程
|
||
|
||
```
|
||
聊天 SSE 流结束
|
||
│
|
||
▼ BackgroundTask (独立DB Session)
|
||
_check_recruiter_intent()
|
||
│
|
||
▼
|
||
构建检测 prompt:
|
||
System: "你是一个精确的信息提取助手,只输出JSON格式。"
|
||
User: "分析以下对话,判断访问者是否表达了:
|
||
1.招聘意愿 2.面试意愿 3.公司/联系方式"
|
||
+ 用户消息(截取500字) + AI回复(截取500字)
|
||
│
|
||
▼
|
||
调用 chat_completion (非流式) → LLM 返回 JSON
|
||
│
|
||
▼
|
||
解析 JSON:
|
||
{"detected": true/false, "intent": "...",
|
||
"company": "...", "contact": "...", "summary": "..."}
|
||
│
|
||
┌────┴────┐
|
||
│detected │ not detected
|
||
│= true │
|
||
▼ ▼
|
||
创建 忽略
|
||
RecruiterMessage
|
||
记录到DB
|
||
│
|
||
▼
|
||
管理后台 Messages 页面显示
|
||
侧边栏红色未读数徽章
|
||
```
|
||
|
||
### 6.4 简历生成流程
|
||
|
||
```
|
||
管理员上传 JD (文件或文本)
|
||
│
|
||
▼ POST /api/admin/resume/generate
|
||
解析 JD 内容 (file_parser)
|
||
│
|
||
▼
|
||
从 DB 读取候选人全部资料:
|
||
Profile + Skills + Education + WorkExperience
|
||
│
|
||
▼
|
||
构建 messages:
|
||
System: "你是专业简历撰写顾问..." (详见 §7.3)
|
||
User: "=== JD === ... === 候选人背景 === ..."
|
||
│
|
||
▼
|
||
SSE 流式生成 Markdown 格式简历
|
||
│
|
||
▼
|
||
前端渲染 Markdown 预览
|
||
│
|
||
▼ POST /api/admin/resume/download
|
||
Markdown → Word 文档 (python-docx)
|
||
- 解析 #/##/### 标题
|
||
- 解析 -/* 列表
|
||
- 解析 **粗体**
|
||
- 设置页边距、对齐
|
||
│
|
||
▼
|
||
下载 resume.docx
|
||
```
|
||
|
||
---
|
||
|
||
## 7. LLM 集成详细设计
|
||
|
||
### 7.1 架构设计
|
||
|
||
CareerBot 采用 **OpenAI 兼容 API 格式** 调用 LLM,通过 `httpx` 发送 HTTP 请求,**不使用官方 SDK**。这样可以灵活切换国内外各种兼容 OpenAI 格式的 LLM 提供商。
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────┐
|
||
│ llm_service.py │
|
||
│ │
|
||
│ get_active_config(db) ← 从DB读取LLM配置 │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ _build_url_and_headers() │
|
||
│ URL自动补全: {base_url}/v1/chat/completions │
|
||
│ Headers: Authorization: Bearer {api_key} │
|
||
│ │ │
|
||
│ _build_body() │
|
||
│ { model, messages, max_tokens, temperature } │
|
||
│ │ │
|
||
│ ┌────┴────────────────┐ │
|
||
│ │ │ │
|
||
│ ▼ ▼ │
|
||
│ chat_completion chat_completion_stream │
|
||
│ (非流式,同步返回) (流式,AsyncGenerator) │
|
||
│ │ │ │
|
||
│ │ ┌────────────────┤ │
|
||
│ │ │ 视觉降级逻辑 │ │
|
||
│ │ │ _has_image_content() 检测 │
|
||
│ │ │ _strip_images() 移除图片 │
|
||
│ │ │ 自动重试 │
|
||
│ │ └────────────────┘ │
|
||
└──────┴──────────────────────────────────────────┘
|
||
```
|
||
|
||
**支持的 LLM 提供商(管理后台预设按钮):**
|
||
|
||
| 提供商 | Base URL | 默认模型 |
|
||
|--------|----------|----------|
|
||
| 通义千问 | `https://dashscope.aliyuncs.com/compatible-mode` | qwen-plus |
|
||
| DeepSeek | `https://api.deepseek.com` | deepseek-chat |
|
||
| 智谱 AI | `https://open.bigmodel.cn/api/paas` | glm-4-flash |
|
||
| OpenAI | `https://api.openai.com` | gpt-4o |
|
||
|
||
### 7.2 LLM 配置参数
|
||
|
||
在管理后台 **LLM Config** 页面配置,存储在 `llm_config` 表:
|
||
|
||
| 参数 | 说明 | 默认值 |
|
||
|------|------|--------|
|
||
| `api_url` | LLM API 基础地址 | - |
|
||
| `api_key` | API 密钥 | - |
|
||
| `model_name` | 模型名称 | - |
|
||
| `max_tokens` | 单次最大生成 token 数 | 2048 |
|
||
| `temperature` | 生成随机性 (0~1) | 0.7 |
|
||
| `system_prompt` | 自定义额外系统指令 | 空 |
|
||
| `max_questions_per_session` | 新Token默认提问次数 | 9 |
|
||
|
||
### 7.3 Prompt 设计
|
||
|
||
#### 7.3.1 智能对话 System Prompt
|
||
|
||
由 `chat_service.build_system_prompt()` 动态构建,**每次对话请求实时从数据库读取**,确保内容始终最新。
|
||
|
||
**结构:**
|
||
|
||
```
|
||
你是{候选人姓名}的个人AI职业助手,专门帮助招聘者了解{姓名}的职业背景、技能和工作经验。
|
||
|
||
=== 个人基本信息 ===
|
||
姓名: ... 学历: ... 所在地: ... 邮箱: ...
|
||
个人总结: ...
|
||
|
||
=== 技能特长 ===
|
||
- 类别1: 内容...
|
||
- 类别2: 内容...
|
||
|
||
=== 教育经历 ===
|
||
时间 | 学校 | 专业 | 学位
|
||
详情: ...
|
||
|
||
=== 工作经历 ===
|
||
时间 | 公司 | 职位
|
||
公司简介: ...
|
||
工作职责: ...
|
||
工作成就: ...
|
||
|
||
=== 回答规则 ===
|
||
1. 只回答与招聘、职业、工作能力、技术背景相关的问题
|
||
2. 对于无关问题,礼貌拒绝并引导回职业话题
|
||
3. 严格基于以上信息回答,不编造经历
|
||
4. 使用专业、友好的语气
|
||
5. 如果招聘者上传了JD,详细分析岗位匹配度,突出亮点/优势,客观指出不足
|
||
6. 根据招聘者使用的语言选择中文或英文回答
|
||
|
||
=== 额外指令 === ← 仅当管理员配置了自定义 system_prompt 时追加
|
||
{自定义内容}
|
||
```
|
||
|
||
#### 7.3.2 招聘意图检测 Prompt
|
||
|
||
由 `chat.py._check_recruiter_intent()` 使用,**非流式调用**,后台静默执行:
|
||
|
||
```
|
||
System: "你是一个精确的信息提取助手,只输出JSON格式。"
|
||
|
||
User:
|
||
你是一个信息提取助手。分析以下对话,判断访问者是否表达了以下任何意愿:
|
||
1. 招聘意愿(想要招聘候选人)
|
||
2. 面试意愿(想要邀请候选人面试)
|
||
3. 留下了公司信息或联系方式
|
||
|
||
如果检测到以上任何一项,请用以下JSON格式回复:
|
||
{"detected": true, "intent": "...", "company": "...", "contact": "...", "summary": "..."}
|
||
|
||
如果没有检测到,回复:
|
||
{"detected": false}
|
||
|
||
访问者消息: {截取前500字}
|
||
AI助手回复: {截取前500字}
|
||
```
|
||
|
||
#### 7.3.3 简历生成 Prompt
|
||
|
||
由 `admin.py.generate_resume()` 使用,**流式调用**:
|
||
|
||
```
|
||
System:
|
||
你是一位专业的简历撰写顾问。请根据提供的JD和候选人的完整背景信息,
|
||
生成一份针对该职位量身定制的中文简历。
|
||
|
||
要求:
|
||
1. 必须包含候选人的全部教育经历和工作经历,不得遗漏
|
||
2. "个人优势"和"自我评价"部分要针对JD定制
|
||
3. 每段工作经历重点突出与JD相关的职责和成就
|
||
4. 技能部分优先展示JD要求的技能
|
||
5. 使用专业、简洁的语言
|
||
6. 输出格式使用Markdown
|
||
|
||
简历结构:
|
||
# 个人信息
|
||
# 求职意向(根据JD推断)
|
||
# 个人优势(3-5条,针对JD定制)
|
||
# 核心技能
|
||
# 工作经历(全部,按时间倒序)
|
||
# 教育背景(全部,按时间倒序)
|
||
|
||
User:
|
||
=== 目标职位描述(JD) ===
|
||
{JD内容}
|
||
|
||
=== 候选人完整背景 ===
|
||
{从DB读取的全部资料}
|
||
```
|
||
|
||
### 7.4 LLM 能力说明
|
||
|
||
> **当前版本没有为 LLM 配置 Function Calling / Tool Use 能力。**
|
||
|
||
LLM 在本项目中扮演的角色是**纯文本生成**,有三个使用场景:
|
||
|
||
| 场景 | 调用方式 | 输入 | 输出 |
|
||
|------|---------|------|------|
|
||
| 智能对话 | 流式 (SSE) | system prompt + 对话历史 + 用户消息(可含文件文本) | 自然语言回复 |
|
||
| 招聘意图检测 | 非流式 | 当轮对话(用户+AI各500字) | JSON 结构化数据 |
|
||
| 简历生成 | 流式 (SSE) | JD + 候选人全部资料 | Markdown 格式简历 |
|
||
|
||
**没有使用的 LLM 高级能力:**
|
||
- Function Calling / Tool Use
|
||
- RAG (检索增强生成)
|
||
- Agent 多步推理
|
||
- 图片生成
|
||
|
||
**已实现的降级策略:**
|
||
- 视觉模型不可用时自动降级:检测到 400 错误 + 消息含图片 → 移除图片内容 → 用文本重试 → 提示用户改用 txt/docx/pdf 格式
|
||
|
||
---
|
||
|
||
## 8. 提问次数限制机制
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ 提问次数限制 │
|
||
├─────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Token 用户: │
|
||
│ 限制存储: AccessToken.max_questions (默认9) │
|
||
│ 已用计数: AccessToken.used_questions │
|
||
│ 检查方式: 服务端每次请求 check + increment │
|
||
│ 持久性: 跨会话、跨页面刷新持久有效 │
|
||
│ 管理: 管理后台可修改 max/used,可重置 │
|
||
│ │
|
||
│ 匿名用户: │
|
||
│ 限制: 固定 3 次 (ANONYMOUS_MAX_QUESTIONS) │
|
||
│ 计数: 服务端查 ChatHistory 中 session_id 的 │
|
||
│ user 消息数量 │
|
||
│ 持久性: 绑定 session_id (sessionStorage) │
|
||
│ │
|
||
│ 默认值来源: │
|
||
│ 新Token的 max_questions │
|
||
│ ← LLMConfig.max_questions_per_session │
|
||
│ ← 若未配置则 fallback 到 9 │
|
||
│ │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 9. SSE 流式通信协议
|
||
|
||
前后端通过 **Server-Sent Events** 实现流式对话,使用 `POST` + `fetch ReadableStream`(非 EventSource,因为需要 POST 发送 FormData)。
|
||
|
||
### 数据格式
|
||
|
||
```
|
||
data: {"content": "回复文本块..."} ← 正常内容块
|
||
data: {"content": "...", "limit_reached": true} ← 超限提示
|
||
data: {"content": "", "done": true} ← 结束标记
|
||
```
|
||
|
||
### 前端处理逻辑 (chat.js)
|
||
|
||
```javascript
|
||
// 1. POST FormData (session_id + message + file?)
|
||
const response = await fetch('/api/chat', { method: 'POST', body: formData });
|
||
|
||
// 2. 读取 ReadableStream
|
||
const reader = response.body.getReader();
|
||
while (true) {
|
||
const { value, done } = await reader.read();
|
||
if (done) break;
|
||
// 3. 解析 SSE 行,提取 data: JSON
|
||
// 4. 逐块拼接到气泡元素的 textContent
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. 文件处理能力
|
||
|
||
`file_parser.py` 支持以下格式:
|
||
|
||
| 格式 | 处理方式 | 返回 | 大小限制 |
|
||
|------|---------|------|---------|
|
||
| `.txt` | 多编码尝试 (utf-8 → gbk → gb2312 → utf-16) | `{"text": "..."}` | 10MB |
|
||
| `.docx` | python-docx 提取段落文本 | `{"text": "..."}` | 10MB |
|
||
| `.pdf` | PyPDF2 逐页提取文本 | `{"text": "..."}` | 10MB |
|
||
| `.png/.jpg/.gif/.webp` | base64 编码 | `{"base64_image": "...", "mime_type": "..."}` | 10MB |
|
||
| 其他 | - | `{"error": "不支持的文件格式"}` | - |
|
||
|
||
---
|
||
|
||
## 11. 运维脚本
|
||
|
||
### start.bat
|
||
1. 检查端口 8000 是否被占用
|
||
2. 首次运行自动执行 `init_data.py` 初始化数据库
|
||
3. 启动 `uvicorn --host 0.0.0.0 --port 8000 --reload`
|
||
|
||
### stop.bat
|
||
1. 查找占用端口 8000 的所有进程
|
||
2. 逐个终止进程
|
||
|
||
---
|
||
|
||
## 12. 关键设计决策
|
||
|
||
| 决策 | 原因 |
|
||
|------|------|
|
||
| 直接使用 bcrypt 而非 passlib | passlib 与 bcrypt 5.0 存在不兼容问题 |
|
||
| httpx 而非 openai SDK | 灵活支持国内 LLM 厂商,无需依赖特定 SDK |
|
||
| SQLite + WAL | 轻量部署,WAL 模式支持并发读取 |
|
||
| 无 Alembic 迁移 | 项目初期用 `create_all` 自动建表,新增列手动 `ALTER TABLE` |
|
||
| Jinja2 < 3.1.6 | 3.1.6 存在模板缓存 `unhashable dict` bug |
|
||
| POST + ReadableStream 而非 EventSource | EventSource 仅支持 GET,无法发送 FormData 附件 |
|
||
| BackgroundTask 做意图检测 | 避免阻塞 SSE 流或导致连接异常 |
|
||
| 意图检测用独立 DB Session | BackgroundTask 执行时请求级 Session 已关闭 |
|
||
| FAB 按钮用内层 span flex | Chrome 对 button 元素的 flex 布局有渲染 bug |
|