Files
ai_job_chat_agent/API_DOCS.md

1756 lines
46 KiB
Markdown
Raw Normal View History

2026-01-12 11:33:43 +08:00
# 青岛岗位匹配系统 API 调用文档
> **版本**: 1.0.0
> **更新日期**: 2025-11
> **协议**: 兼容 OpenAI Chat Completions API
---
## 目录
- [1. 系统概述](#1-系统概述)
- [2. 系统架构](#2-系统架构)
- [3. 快速开始](#3-快速开始)
- [4. API 端点列表](#4-api-端点列表)
- [5. 核心接口详解](#5-核心接口详解)
- [5.1 聊天补全接口](#51-聊天补全接口)
- [5.2 健康检查接口](#52-健康检查接口)
- [5.3 性能指标接口](#53-性能指标接口)
- [5.4 性能分析接口](#54-性能分析接口)
- [6. 内置工具说明](#6-内置工具说明)
- [7. 代码对照表](#7-代码对照表)
- [8. SDK 与代码示例](#8-sdk-与代码示例)
- [9. 错误处理](#9-错误处理)
- [10. 配置参考](#10-配置参考)
- [11. 部署指南](#11-部署指南)
- [12. 常见问题](#12-常见问题)
---
## 1. 系统概述
### 1.1 系统简介
青岛岗位匹配系统是一个基于 Go 语言开发的智能岗位推荐服务,提供与 OpenAI `/v1/chat/completions` 完全兼容的 API 接口。系统集成了多种智能工具,能够自动理解用户意图并调用相应功能。
### 1.2 核心功能
| 功能模块 | 说明 |
|---------|------|
| 🎯 **岗位推荐** | 根据用户需求、简历内容智能推荐青岛市岗位 |
| 📍 **地理位置查询** | 集成高德地图,支持按位置、区域搜索岗位 |
| 📄 **文件解析** | 支持 PDF、图片、Excel、PPT、Word 等文件的 OCR 智能解析 |
| 🖼️ **Vision API** | 兼容 OpenAI Vision API 格式,支持通过 URL 发送图片/PDF/Excel/PPT 等文件进行 OCR 识别 |
| 📋 **政策咨询** | 提供就业创业、社保医保、人才政策等咨询服务 |
| 🔄 **多轮对话** | 支持上下文连续对话 |
| ⚡ **流式输出** | 支持 SSE 流式响应,提升用户体验 |
### 1.3 技术特性
- **兼容性**: 100% 兼容 OpenAI Chat API 格式
- **高性能**: 基于 Gin 框架,支持高并发
- **限流保护**: 内置令牌桶限流200 容量/50 QPS
- **优雅关闭**: 支持信号处理和优雅停机
- **可观测性**: 内置 JSON 指标端点和 pprof 性能分析(可通过配置开关关闭)
---
## 2. 系统架构
### 2.1 架构图
```
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 (Client) │
│ (Web/App/第三方 OpenAI 兼容客户端) │
└─────────────────────────────┬───────────────────────────────────┘
│ HTTP/HTTPS
┌─────────────────────────────────────────────────────────────────┐
│ API 网关层 (Gateway) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ CORS │ │ Recovery│ │ Metrics │ │ Rate │ │ Logger │ │
│ │Middleware│ │Middleware│ │Middleware│ │ Limit │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └──────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 处理器层 (Handlers) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ ChatHandler │ │HealthHandler │ │MetricsHandler│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 服务层 (Services) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ChatService │ │ JobService │ │FileService │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │LocationServ │ │PolicyService│ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 客户端层 (Clients) │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ LLMClient │ │ JobClient │ │AmapClient │ │ OCRClient │ │
│ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│ │ │ │ │ │
└────────┼─────────────┼─────────────┼─────────────┼──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐
│ LLM API │ │ 岗位 API │ │ 高德地图 │ │ OCR 服务 │
│ (OpenAI兼容) │ │ │ │ API │ │ │
└──────────────┘ └──────────────┘ └──────────┘ └──────────────┘
```
### 2.2 数据流
```
用户请求 → 中间件处理 → ChatHandler → ChatService
识别意图 & 调用工具
┌────────────────┼────────────────┐
↓ ↓ ↓
LocationService JobService PolicyService
↓ ↓ ↓
高德地图API 岗位API 政策API
↓ ↓ ↓
└────────────────┼────────────────┘
整合结果 & 生成回复
流式/非流式响应 → 用户
```
---
## 3. 快速开始
### 3.1 环境要求
- Go 1.20+
- 或 Docker 20.10+
### 3.2 安装运行
#### 方式一:源码运行
```bash
# 克隆项目
git clone <repository-url>
cd qd-sc
# 安装依赖
go mod download
# 配置文件(编辑 config.yaml
cp config.yaml config.local.yaml
vim config.local.yaml
# 运行
go run cmd/server/main.go
# 或编译后运行
go build -o qd-sc-server cmd/server/main.go
./qd-sc-server -config=config.yaml
```
#### 方式二Docker 运行
```bash
# 使用 docker-compose
docker-compose up -d
# 或手动构建
docker build -t qd-sc-server .
docker run -d -p 8080:8080 \
-e LLM_API_KEY="sk-xxx" \
-e LLM_BASE_URL="https://your-api.com/v1" \
--name qd-sc-server \
qd-sc-server
```
### 3.3 验证服务
```bash
# 健康检查
curl http://localhost:8080/health
# 测试对话(流式)
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qd-job-turbo",
"messages": [{"role": "user", "content": "推荐城阳区的Java开发岗位"}],
"stream": true
}'
```
---
## 4. API 端点列表
| 端点 | 方法 | 说明 | 认证 |
|------|------|------|------|
| `/` | GET | API 信息和端点列表 | 无 |
| `/health` | GET | 健康检查 | 无 |
| `/metrics` | GET | 性能指标JSON需启用 `performance.enable_metrics` | 无 |
| `/v1/chat/completions` | POST | **核心接口** - OpenAI 兼容的聊天接口 | 无 |
| `/debug/pprof/*` | GET | pprof 性能分析(需启用 `performance.enable_pprof` | 无 |
---
## 5. 核心接口详解
### 5.1 聊天补全接口
#### 基本信息
| 属性 | 值 |
|------|-----|
| **端点** | `POST /v1/chat/completions` |
| **Content-Type** | `application/json` |
| **响应格式** | JSON 或 SSE (Server-Sent Events) |
#### 5.1.1 JSON 请求格式
```http
POST /v1/chat/completions HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"model": "qd-job-turbo",
"messages": [
{
"role": "system",
"content": "你是一个有帮助的助手"
},
{
"role": "user",
"content": "帮我找青岛城阳区的Java开发岗位"
}
],
"stream": true,
"temperature": 0.7,
"max_tokens": 2000
}
```
#### 5.1.2 请求参数说明
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `model` | string | ✅ | - | 模型名称,固定为 `qd-job-turbo` |
| `messages` | array | ✅ | - | 消息数组,见下表 |
| `stream` | boolean | ❌ | `false` | 是否流式输出(**推荐 `true`** |
| `temperature` | float | ❌ | `1.0` | 采样温度,范围 0-2 |
| `top_p` | float | ❌ | `1.0` | 核采样参数 |
| `max_tokens` | integer | ❌ | - | 最大生成 token 数 |
| `presence_penalty` | float | ❌ | `0.0` | 存在惩罚,范围 -2.0 到 2.0 |
| `frequency_penalty` | float | ❌ | `0.0` | 频率惩罚,范围 -2.0 到 2.0 |
| `user` | string | ❌ | - | 用户标识 |
#### 5.1.3 消息对象格式
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `role` | string | ✅ | 角色:`system``user``assistant` |
| `content` | string/array | ✅ | 消息内容(支持文本或多模态数组) |
| `name` | string | ❌ | 发送者名称 |
**role 说明**:
| 角色 | 说明 |
|------|------|
| `system` | 系统指令,设置 AI 行为 |
| `user` | 用户消息 |
| `assistant` | AI 回复 |
**content 格式说明**:
`content` 支持两种格式:
1. **字符串格式**(普通文本消息):
```json
{
"role": "user",
"content": "帮我推荐Java开发岗位"
}
```
2. **数组格式**(多模态消息,支持图片 - OpenAI Vision API 兼容):
```json
{
"role": "user",
"content": [
{"type": "text", "text": "根据这份简历帮我推荐合适的岗位"},
{"type": "image_url", "image_url": {"url": "https://example.com/resume.jpg"}}
]
}
```
**多模态内容类型**:
| type | 说明 | 字段 |
|------|------|------|
| `text` | 文本内容 | `text`: 文本字符串 |
| `image_url` | 文件URL兼容字段 | `image_url.url`: 文件地址 |
> **重要说明**:
> - `image_url` 是为兼容 OpenAI Vision API 而保留的字段名称
> - 实际上不仅支持图片,**还支持 PDF、Excel、PPT 等所有 OCR 服务支持的文件类型**
> - 系统会自动调用 OCR 服务进行识别解析
> - 支持的文件格式JPG、PNG、GIF、PDF、XLS、XLSX、PPT、PPTX 等
#### 5.1.4 带文件URL的请求Vision API 兼容格式)
完全兼容 OpenAI Vision API 格式,通过 `image_url` 字段发送文件 URL 进行 OCR 识别。
> **字段说明**: `image_url` 是 OpenAI Vision API 的标准字段名,为保持兼容性沿用此名称。但本系统通过 OCR 服务,**不仅支持图片,还支持 PDF、Excel、PPT 等多种文件格式**。
**示例 1发送图片简历**
```http
POST /v1/chat/completions HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"model": "qd-job-turbo",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "根据这份简历帮我推荐合适的岗位"},
{"type": "image_url", "image_url": {"url": "https://example.com/resume.jpg"}}
]
}
],
"stream": true
}
```
**示例 2发送 PDF 文件**
```json
{
"role": "user",
"content": [
{"type": "text", "text": "分析这份PDF简历"},
{"type": "image_url", "image_url": {"url": "https://example.com/resume.pdf"}}
]
}
```
**示例 3发送 Excel 文件**
```json
{
"role": "user",
"content": [
{"type": "text", "text": "帮我分析这个表格数据"},
{"type": "image_url", "image_url": {"url": "https://example.com/data.xlsx"}}
]
}
```
**示例 4发送多个文件**
```json
{
"role": "user",
"content": [
{"type": "text", "text": "分析这些文档内容"},
{"type": "image_url", "image_url": {"url": "https://example.com/resume.pdf"}},
{"type": "image_url", "image_url": {"url": "https://example.com/certificate.jpg"}},
{"type": "image_url", "image_url": {"url": "https://example.com/transcript.xlsx"}}
]
}
```
**支持的文件格式**:
| 类型 | 格式 |
|------|------|
| 图片 | JPG、JPEG、PNG、GIF |
| 文档 | PDF |
| 表格 | XLS、XLSX |
| 演示 | PPT、PPTX |
#### 5.1.6 流式响应格式 (SSE)
`stream: true` 时,响应为 Server-Sent Events 格式:
```
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qd-job-turbo","choices":[{"index":0,"delta":{"role":"assistant","content":"您好"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qd-job-turbo","choices":[{"index":0,"delta":{"content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qd-job-turbo","choices":[{"index":0,"delta":{"content":"我"},"finish_reason":null}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"qd-job-turbo","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
```
**Chunk 对象结构**:
```typescript
interface ChatCompletionChunk {
id: string; // 响应ID
object: "chat.completion.chunk";
created: number; // Unix 时间戳
model: string; // 模型名称
choices: Array<{
index: number;
delta: {
role?: "assistant"; // 仅首个 chunk 包含
content?: string; // 内容片段
};
finish_reason: string | null; // "stop" 或 null
}>;
}
```
#### 5.1.7 非流式响应格式
`stream: false` 时:
```json
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1234567890,
"model": "qd-job-turbo",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "您好,我可以帮您推荐岗位..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150
}
}
```
#### 5.1.8 岗位推荐特殊响应格式
当系统返回岗位推荐时,岗位信息会以特殊的 Markdown 代码块格式输出:
```
为您找到 3 个相关岗位:
``` job-json
{
"jobTitle": "Java开发工程师",
"companyName": "青岛XX科技有限公司",
"salary": "15000-25000元/月",
"location": "城阳区",
"education": "本科",
"experience": "3-5年",
"appJobUrl": "https://..."
}
```
``` job-json
{
"jobTitle": "高级Java工程师",
"companyName": "青岛YY信息技术有限公司",
"salary": "20000-35000元/月",
"location": "城阳区",
"education": "本科",
"experience": "5-10年",
"appJobUrl": "https://..."
}
```
```
**岗位对象结构**:
```typescript
interface FormattedJob {
jobTitle: string; // 职位名称
companyName: string; // 公司名称
salary: string; // 薪资范围
location: string; // 工作地点
education: string; // 学历要求
experience: string; // 经验要求
appJobUrl: string; // 职位详情链接
data?: any; // 额外数据(分页信息等)
}
```
---
### 5.2 健康检查接口
#### 请求
```http
GET /health HTTP/1.1
Host: localhost:8080
```
#### 响应
```json
{
"status": "ok",
"timestamp": "2024-01-01T12:00:00Z"
}
```
---
### 5.3 性能指标接口
> 该接口需要启用 `performance.enable_metrics`;关闭后将不会注册 `/metrics` 端点。
#### 请求
```http
GET /metrics HTTP/1.1
Host: localhost:8080
```
#### 响应
```json
{
"requests_total": 12345,
"requests_success": 12300,
"requests_failed": 45,
"avg_response_time_ms": 156.8,
"goroutines": 42,
"memory_alloc_mb": 45.6
}
```
| 指标 | 说明 |
|------|------|
| `requests_total` | 总请求数 |
| `requests_success` | 成功请求数 |
| `requests_failed` | 失败请求数 |
| `avg_response_time_ms` | 平均响应时间(毫秒) |
| `goroutines` | 当前 goroutine 数量 |
| `memory_alloc_mb` | 内存分配MB |
---
### 5.4 性能分析接口
> 该接口需要启用 `performance.enable_pprof`;关闭后将不会注册 `/debug/pprof/*` 端点。
支持 Go pprof 标准端点:
| 端点 | 说明 |
|------|------|
| `/debug/pprof/` | pprof 索引页面 |
| `/debug/pprof/profile?seconds=30` | CPU 性能分析 |
| `/debug/pprof/heap` | 堆内存分析 |
| `/debug/pprof/goroutine` | Goroutine 分析 |
| `/debug/pprof/allocs` | 内存分配分析 |
**使用示例**:
```bash
# 采集 30 秒 CPU 数据
curl http://localhost:8080/debug/pprof/profile?seconds=30 -o cpu.prof
# 分析
go tool pprof cpu.prof
```
---
## 6. 内置工具说明
系统内置了多种智能工具,会根据用户意图自动调用:
### 6.1 queryLocation - 地理位置查询
**功能**: 查询青岛具体地点的经纬度坐标
**触发场景**: 用户提到具体地点名称时(如"五四广场附近"、"青岛啤酒博物馆周边"
**参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `keywords` | string | ✅ | 地点名称,如"五四广场" |
**返回示例**:
```json
{
"keywords": "五四广场",
"latitude": "36.061892",
"longitude": "120.384428",
"message": "成功获取地点 五四广场 的坐标"
}
```
---
### 6.2 queryJobsByArea - 按区域查询岗位
**功能**: 根据青岛市区域代码查询岗位信息
**触发场景**: 用户指定区域名称时(如"城阳区"、"市北区"
**参数**:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `jobTitle` | string | ✅ | - | 岗位关键词 |
| `current` | integer | ✅ | 1 | 页码 |
| `pageSize` | integer | ✅ | 10 | 每页数量 |
| `jobLocationAreaCode` | string | ❌ | - | 区域代码(见代码表) |
| `order` | string | ❌ | "0" | 排序0-推荐1-最热2-最新 |
| `minSalary` | string | ❌ | - | 最低薪资(元/月) |
| `maxSalary` | string | ❌ | - | 最高薪资(元/月) |
| `experience` | string | ❌ | - | 经验要求代码 |
| `education` | string | ❌ | - | 学历要求代码 |
| `companyNature` | string | ❌ | - | 企业类型代码 |
---
### 6.3 queryJobsByLocation - 按坐标查询岗位
**功能**: 根据经纬度和半径查询附近岗位
**触发场景**: 用户指定具体地点后,需要查询附近岗位
**参数**:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `jobTitle` | string | ✅ | - | 岗位关键词 |
| `current` | integer | ✅ | 1 | 页码 |
| `pageSize` | integer | ✅ | 10 | 每页数量 |
| `latitude` | string | ✅ | - | 纬度 |
| `longitude` | string | ✅ | - | 经度 |
| `radius` | string | ✅ | "10" | 搜索半径千米最大50 |
| `order` | string | ❌ | "0" | 排序方式 |
| `minSalary` | string | ❌ | - | 最低薪资 |
| `maxSalary` | string | ❌ | - | 最高薪资 |
| `experience` | string | ❌ | - | 经验要求 |
| `education` | string | ❌ | - | 学历要求 |
| `companyNature` | string | ❌ | - | 企业类型 |
---
### 6.4 queryPolicy - 政策咨询
**功能**: 查询青岛市就业创业、社保医保、人才政策等
**触发场景**: 用户咨询政策相关问题
**参数**:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `message` | string | ✅ | - | 咨询问题 |
| `chatId` | string | ❌ | - | 会话ID多轮对话 |
| `conversationId` | string | ❌ | - | 流水号(多轮对话) |
| `realName` | boolean | ❌ | false | 是否实名咨询 |
| `aac001` | string | ❌* | - | 个人编号(实名时必填) |
| `aac147` | string | ❌* | - | 身份证号(实名时必填) |
| `aac003` | string | ❌* | - | 姓名(实名时必填) |
---
### 6.5 parsePDF - PDF 解析
**功能**: 使用 OCR 服务解析 PDF 文件内容(如简历)
**说明**: 通常在文件上传时自动触发,无需手动调用。文件会通过 OCR 服务进行识别解析。
**参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `fileUrl` | string | ✅ | PDF 文件 URL |
---
### 6.6 parseImage - 图片解析
**功能**: 使用 OCR 服务识别图片中的文本内容
**说明**: 通常在文件上传时自动触发,无需手动调用。文件会通过 OCR 服务进行识别解析。
**参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `imageUrl` | string | ✅ | 图片文件 URL |
---
## 7. 代码对照表
### 7.1 区域代码 (jobLocationAreaCode)
| 代码 | 区域 |
|------|------|
| 0 | 市南区 |
| 1 | 市北区 |
| 2 | 李沧区 |
| 3 | 崂山区 |
| 4 | 黄岛区 |
| 5 | 城阳区 |
| 6 | 即墨区 |
| 7 | 胶州市 |
| 8 | 平度市 |
| 9 | 莱西市 |
### 7.2 学历代码 (education)
| 代码 | 学历 |
|------|------|
| -1 | 学历不限 |
| 0 | 初中及以下 |
| 1 | 中专/中技 |
| 2 | 高中 |
| 3 | 大专 |
| 4 | 本科 |
| 5 | 硕士 |
| 6 | 博士 |
| 7 | MBA/EMBA |
| 8 | 留学-学士 |
| 9 | 留学-硕士 |
| 10 | 留学-博士 |
### 7.3 经验代码 (experience)
| 代码 | 经验 |
|------|------|
| 0 | 经验不限 |
| 1 | 实习生 |
| 2 | 应届毕业生 |
| 3 | 1年以下 |
| 4 | 1-3年 |
| 5 | 3-5年 |
| 6 | 5-10年 |
| 7 | 10年以上 |
### 7.4 企业类型代码 (companyNature)
| 代码 | 类型 |
|------|------|
| 1 | 私营企业 |
| 2 | 股份制企业 |
| 3 | 国有企业 |
| 4 | 外商及港澳台投资企业 |
| 5 | 医院 |
### 7.5 排序方式代码 (order)
| 代码 | 排序 |
|------|------|
| 0 | 推荐 |
| 1 | 最热 |
| 2 | 最新发布 |
---
## 8. SDK 与代码示例
### 8.1 cURL 示例
#### 基础对话
```bash
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qd-job-turbo",
"messages": [
{"role": "user", "content": "帮我推荐城阳区的Java开发岗位"}
],
"stream": true
}'
```
#### 带文件URLVision API 兼容格式)
通过 `image_url` 字段发送文件 URL支持图片、PDF、Excel、PPT 等):
```bash
# 发送图片
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qd-job-turbo",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "根据这份简历帮我推荐合适的岗位"},
{"type": "image_url", "image_url": {"url": "https://example.com/resume.jpg"}}
]
}
],
"stream": true
}'
# 发送 PDF 文件
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qd-job-turbo",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "分析这份PDF简历"},
{"type": "image_url", "image_url": {"url": "https://example.com/resume.pdf"}}
]
}
],
"stream": true
}'
```
#### 多轮对话
```bash
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qd-job-turbo",
"messages": [
{"role": "user", "content": "我想找Java开发岗位"},
{"role": "assistant", "content": "好的,请问您希望在青岛哪个区域工作?"},
{"role": "user", "content": "城阳区要求薪资15k以上"}
],
"stream": true
}'
```
#### 政策咨询
```bash
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qd-job-turbo",
"messages": [
{"role": "user", "content": "青岛市大学生就业补贴政策是怎样的?"}
],
"stream": true
}'
```
---
### 8.2 Python 示例
#### 使用 OpenAI SDK
```python
from openai import OpenAI
# 创建客户端,指向本地服务
client = OpenAI(
base_url="http://localhost:8080/v1",
api_key="not-needed" # 本系统不需要 API key
)
# 流式对话
def chat_stream(message: str):
stream = client.chat.completions.create(
model="qd-job-turbo",
messages=[{"role": "user", "content": message}],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
# 示例
chat_stream("帮我推荐青岛城阳区的Java开发岗位")
```
#### 使用 Vision API 兼容格式发送文件 URL
通过 `image_url` 字段发送文件 URL支持图片、PDF、Excel、PPT 等格式:
```python
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8080/v1",
api_key="not-needed"
)
# 带文件URL的对话Vision API 兼容格式)
# 注意image_url 是兼容字段名,实际支持多种文件格式
def chat_with_file_url(text: str, file_url: str):
stream = client.chat.completions.create(
model="qd-job-turbo",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": text},
{"type": "image_url", "image_url": {"url": file_url}}
]
}
],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()
# 示例1发送图片简历
chat_with_file_url(
"根据这份简历帮我推荐合适的岗位",
"https://example.com/resume.jpg"
)
# 示例2发送 PDF 文件
chat_with_file_url(
"分析这份PDF文档",
"https://example.com/document.pdf"
)
# 示例3发送 Excel 文件
chat_with_file_url(
"帮我分析这个表格",
"https://example.com/data.xlsx"
)
```
#### 使用 Requests 库
```python
import requests
import json
def chat_with_file(message: str, file_path: str = None):
url = "http://localhost:8080/v1/chat/completions"
if file_path:
# 带文件上传
files = {
'file': open(file_path, 'rb'),
'request': (None, json.dumps({
"model": "qd-job-turbo",
"messages": [{"role": "user", "content": message}],
"stream": True
}))
}
response = requests.post(url, files=files, stream=True)
else:
# 普通请求
response = requests.post(
url,
json={
"model": "qd-job-turbo",
"messages": [{"role": "user", "content": message}],
"stream": True
},
stream=True
)
# 处理流式响应
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
data = line[6:]
if data == '[DONE]':
break
chunk = json.loads(data)
if chunk['choices'][0]['delta'].get('content'):
print(chunk['choices'][0]['delta']['content'], end='', flush=True)
print()
# 示例
chat_with_file("根据我的简历推荐岗位", "resume.pdf")
```
---
### 8.3 JavaScript/TypeScript 示例
#### 使用 Fetch API
```typescript
interface ChatMessage {
role: 'system' | 'user' | 'assistant';
content: string;
}
async function chat(messages: ChatMessage[]): Promise<void> {
const response = await fetch('http://localhost:8080/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'qd-job-turbo',
messages,
stream: true,
}),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) return;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const chunk = JSON.parse(data);
const content = chunk.choices[0]?.delta?.content;
if (content) {
process.stdout.write(content);
}
} catch (e) {
// 忽略解析错误
}
}
}
}
}
// 使用示例
chat([
{ role: 'user', content: '帮我推荐城阳区的Java开发岗位' }
]);
```
#### 使用 OpenAI SDK (Node.js)
```typescript
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'http://localhost:8080/v1',
apiKey: 'not-needed',
});
async function chatStream(message: string) {
const stream = await client.chat.completions.create({
model: 'qd-job-turbo',
messages: [{ role: 'user', content: message }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content || '');
}
console.log();
}
chatStream('帮我推荐青岛城阳区的Java开发岗位');
```
---
### 8.4 Go 示例
```go
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
)
type ChatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Stream bool `json:"stream"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
func chat(message string) error {
req := ChatRequest{
Model: "qd-job-turbo",
Messages: []Message{
{Role: "user", Content: message},
},
Stream: true,
}
body, _ := json.Marshal(req)
resp, err := http.Post(
"http://localhost:8080/v1/chat/completions",
"application/json",
bytes.NewBuffer(body),
)
if err != nil {
return err
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
if data == "[DONE]" {
break
}
var chunk map[string]interface{}
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
continue
}
if choices, ok := chunk["choices"].([]interface{}); ok && len(choices) > 0 {
if choice, ok := choices[0].(map[string]interface{}); ok {
if delta, ok := choice["delta"].(map[string]interface{}); ok {
if content, ok := delta["content"].(string); ok {
fmt.Print(content)
}
}
}
}
}
}
fmt.Println()
return nil
}
func main() {
chat("帮我推荐城阳区的Java开发岗位")
}
```
---
### 8.5 Vue.js 前端示例
```vue
<template>
<div class="chat-container">
<div class="messages">
<div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]">
<div class="content" v-html="formatContent(msg.content)"></div>
</div>
<div v-if="loading" class="message assistant">
<div class="content">{{ currentResponse }}</div>
</div>
</div>
<div class="input-area">
<input
v-model="input"
@keyup.enter="sendMessage"
placeholder="输入消息..."
:disabled="loading"
/>
<button @click="sendMessage" :disabled="loading">发送</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const messages = ref([]);
const input = ref('');
const loading = ref(false);
const currentResponse = ref('');
async function sendMessage() {
if (!input.value.trim() || loading.value) return;
const userMessage = input.value;
messages.value.push({
id: Date.now(),
role: 'user',
content: userMessage
});
input.value = '';
loading.value = true;
currentResponse.value = '';
try {
const response = await fetch('http://localhost:8080/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qd-job-turbo',
messages: messages.value.map(m => ({
role: m.role,
content: m.content
})),
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const chunk = JSON.parse(data);
const content = chunk.choices[0]?.delta?.content;
if (content) {
currentResponse.value += content;
}
} catch (e) {}
}
}
}
messages.value.push({
id: Date.now(),
role: 'assistant',
content: currentResponse.value
});
} catch (error) {
console.error('Chat error:', error);
} finally {
loading.value = false;
currentResponse.value = '';
}
}
function formatContent(content) {
// 解析 job-json 代码块
return content.replace(
/```\s*job-json\n([\s\S]*?)```/g,
(_, json) => {
try {
const job = JSON.parse(json);
return `<div class="job-card">
<h3>${job.jobTitle}</h3>
<p class="company">${job.companyName}</p>
<p class="salary">${job.salary}</p>
<p class="info">${job.location} | ${job.education} | ${job.experience}</p>
<a href="${job.appJobUrl}" target="_blank">查看详情</a>
</div>`;
} catch {
return json;
}
}
);
}
</script>
<style scoped>
.chat-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.messages {
height: 500px;
overflow-y: auto;
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
}
.message {
margin-bottom: 10px;
padding: 10px;
border-radius: 8px;
}
.message.user {
background: #e3f2fd;
text-align: right;
}
.message.assistant {
background: #f5f5f5;
}
.input-area {
display: flex;
gap: 10px;
}
.input-area input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.input-area button {
padding: 10px 20px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.job-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
}
.job-card h3 {
margin: 0 0 8px 0;
color: #1976d2;
}
.job-card .company {
font-weight: bold;
color: #333;
}
.job-card .salary {
color: #e53935;
font-size: 1.1em;
}
.job-card .info {
color: #666;
font-size: 0.9em;
}
.job-card a {
display: inline-block;
margin-top: 10px;
color: #1976d2;
}
</style>
```
---
## 9. 错误处理
### 9.1 错误响应格式
```json
{
"error": {
"message": "错误描述信息",
"type": "错误类型",
"code": "错误代码"
}
}
```
### 9.2 常见错误码
| HTTP 状态码 | 错误类型 | 说明 |
|------------|---------|------|
| 400 | `invalid_request` | 请求格式错误 |
| 400 | `multipart_parse_error` | multipart 表单解析失败 |
| 400 | `file_processing_error` | 文件处理失败 |
| 429 | `rate_limit_exceeded` | 请求过于频繁 |
| 500 | `internal_error` | 服务器内部错误 |
### 9.3 错误处理建议
```python
import requests
def safe_chat(message: str) -> str:
try:
response = requests.post(
"http://localhost:8080/v1/chat/completions",
json={
"model": "qd-job-turbo",
"messages": [{"role": "user", "content": message}],
"stream": False
},
timeout=120
)
if response.status_code == 429:
# 限流,等待后重试
import time
time.sleep(2)
return safe_chat(message)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
except requests.exceptions.Timeout:
return "请求超时,请稍后重试"
except requests.exceptions.RequestException as e:
return f"请求失败: {e}"
```
---
## 10. 配置参考
### 10.1 完整配置文件 (config.yaml)
```yaml
# 服务器配置
server:
port: 8080 # 服务端口
host: "0.0.0.0" # 监听地址
read_timeout: 30s # 读取请求超时
write_timeout: 300s # 写入响应超时(流式响应需要更长时间)
# LLM配置
llm:
base_url: "https://api.openai.com/v1" # LLM API地址
api_key: "sk-xxx" # LLM API密钥
model: "gpt-4o" # 默认模型
timeout: 120s # 请求超时
max_retries: 3 # 最大重试次数
# 高德地图配置
amap:
api_key: "your-amap-key" # 高德地图API密钥
base_url: "https://restapi.amap.com/v3" # 高德API地址
timeout: 10s # 请求超时
# 岗位API配置
job_api:
base_url: "https://job-api.example.com" # 岗位API地址
timeout: 30s # 请求超时
# OCR服务配置文件解析
ocr:
base_url: "https://your-ocr-api.example.com" # OCR服务地址外网
# base_url: "http://127.0.0.1:9001" # OCR服务地址内网
timeout: 120s # 请求超时
# 政策咨询配置
policy:
base_url: "http://policy-api.example.com" # 政策API地址
login_name: "your_login_name" # 登录用户名
user_key: "your_user_key" # 用户密钥
service_id: "your_service_id" # 服务ID
timeout: 60s # 请求超时
# 日志配置
logging:
level: "info" # 日志级别debug, info, warn, error
format: "json" # 日志格式json, text
# 性能配置
performance:
max_goroutines: 10000 # 最大并发goroutine数
goroutine_pool_size: 5000 # goroutine池大小
task_queue_size: 10000 # 任务队列大小
enable_pprof: true # 启用pprof性能分析设为 false 可关闭 /debug/pprof/*
enable_metrics: true # 启用指标收集(设为 false 可关闭 /metrics 与指标中间件)
gc_percent: 100 # GC触发百分比
```
### 10.2 环境变量覆盖
环境变量会自动覆盖配置文件中的值:
| 环境变量 | 配置项 |
|---------|--------|
| `SERVER_PORT` | server.port |
| `SERVER_HOST` | server.host |
| `LLM_API_KEY` | llm.api_key |
| `LLM_BASE_URL` | llm.base_url |
| `LLM_MODEL` | llm.model |
| `AMAP_API_KEY` | amap.api_key |
| `OCR_BASE_URL` | ocr.base_url |
| `JOB_API_BASE_URL` | job_api.base_url |
| `POLICY_BASE_URL` | policy.base_url |
| `POLICY_LOGIN_NAME` | policy.login_name |
| `POLICY_USER_KEY` | policy.user_key |
| `POLICY_SERVICE_ID` | policy.service_id |
---
## 11. 部署指南
### 11.1 Docker Compose 部署
```yaml
# docker-compose.yml
version: '3.8'
services:
qd-sc-server:
build: .
ports:
- "8080:8080"
environment:
- LLM_API_KEY=sk-xxx
- LLM_BASE_URL=https://api.openai.com/v1
- AMAP_API_KEY=your-amap-key
- OCR_BASE_URL=https://your-ocr-api.example.com
volumes:
- ./config.yaml:/app/config.yaml:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
```
### 11.2 Kubernetes 部署
```yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: qd-sc-server
spec:
replicas: 3
selector:
matchLabels:
app: qd-sc-server
template:
metadata:
labels:
app: qd-sc-server
spec:
containers:
- name: qd-sc-server
image: qd-sc-server:latest
ports:
- containerPort: 8080
env:
- name: LLM_API_KEY
valueFrom:
secretKeyRef:
name: qd-sc-secrets
key: llm-api-key
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: qd-sc-server
spec:
selector:
app: qd-sc-server
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
```
### 11.3 Nginx 反向代理
```nginx
upstream qd_sc_backend {
server 127.0.0.1:8080;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://qd_sc_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE 支持
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
# 超时配置
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
# 文件上传大小限制
client_max_body_size 20M;
}
```
---
## 12. 常见问题
### Q1: 如何处理流式响应中的岗位数据?
岗位数据会以特殊的 ```` ``` job-json ```` 代码块格式返回。客户端需要解析这个格式:
```javascript
function parseJobCards(content) {
const jobRegex = /```\s*job-json\n([\s\S]*?)```/g;
const jobs = [];
let match;
while ((match = jobRegex.exec(content)) !== null) {
try {
jobs.push(JSON.parse(match[1]));
} catch (e) {}
}
return jobs;
}
```
### Q2: 流式响应中途断开怎么办?
建议实现重连机制:
```python
import time
def chat_with_retry(message, max_retries=3):
for i in range(max_retries):
try:
return chat_stream(message)
except Exception as e:
if i < max_retries - 1:
time.sleep(2 ** i) # 指数退避
else:
raise e
```
### Q3: 如何实现多轮对话?
保存历史消息并在每次请求中传递:
```python
conversation = []
def chat(user_input):
conversation.append({"role": "user", "content": user_input})
response = client.chat.completions.create(
model="qd-job-turbo",
messages=conversation,
stream=True
)
assistant_message = ""
for chunk in response:
content = chunk.choices[0].delta.content or ""
assistant_message += content
print(content, end="", flush=True)
conversation.append({"role": "assistant", "content": assistant_message})
print()
```
### Q4: 429 错误(限流)如何处理?
系统默认限流为:桶容量 200每秒补充 50 个令牌。建议:
1. 实现请求重试机制,遇到 429 后等待 1-2 秒重试
2. 控制客户端的请求频率
3. 如需更高 QPS联系管理员调整配置
### Q5: 如何调试 API 问题?
1. 启用 debug 日志级别
2. (可选)使用 `/metrics` 端点查看性能指标(需启用 `performance.enable_metrics`
3. (可选)使用 `/debug/pprof/` 进行性能分析(需启用 `performance.enable_pprof`
4. 检查服务器日志输出
---
## 附录
### A. 响应时间参考
| 操作 | 预期响应时间 |
|------|-------------|
| 健康检查 | < 10ms |
| 简单对话 | 2-5s |
| 岗位查询 | 3-8s |
| 文件解析 | 5-30s |
| 政策咨询 | 3-10s |
### B. 并发能力
| 指标 | 参考值 |
|------|--------|
| 最大并发连接 | 10,000 |
| 建议 QPS | 50 |
| 最大文件上传 | 10MB |
### C. 更新日志
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0.0 | 2025-11 | 初始版本 |