Files
ai_job_chat_agent/API_DOCS.md
2026-01-12 11:33:43 +08:00

1756 lines
46 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 青岛岗位匹配系统 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
```
#### 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
```
**示例 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 | 初始版本 |