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

46 KiB
Raw Blame History

青岛岗位匹配系统 API 调用文档

版本: 1.0.0
更新日期: 2025-11
协议: 兼容 OpenAI Chat Completions API


目录


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 安装运行

方式一:源码运行

# 克隆项目
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 运行

# 使用 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 验证服务

# 健康检查
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 请求格式

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 角色:systemuserassistant
content string/array 消息内容(支持文本或多模态数组)
name string 发送者名称

role 说明:

角色 说明
system 系统指令,设置 AI 行为
user 用户消息
assistant AI 回复

content 格式说明:

content 支持两种格式:

  1. 字符串格式(普通文本消息):
{
  "role": "user",
  "content": "帮我推荐Java开发岗位"
}
  1. 数组格式(多模态消息,支持图片 - OpenAI Vision API 兼容):
{
  "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发送图片简历

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 文件

{
  "role": "user",
  "content": [
    {"type": "text", "text": "分析这份PDF简历"},
    {"type": "image_url", "image_url": {"url": "https://example.com/resume.pdf"}}
  ]
}

示例 3发送 Excel 文件

{
  "role": "user",
  "content": [
    {"type": "text", "text": "帮我分析这个表格数据"},
    {"type": "image_url", "image_url": {"url": "https://example.com/data.xlsx"}}
  ]
}

示例 4发送多个文件

{
  "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 对象结构:

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 时:

{
  "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://..."
}
{
  "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 健康检查接口

请求

GET /health HTTP/1.1
Host: localhost:8080

响应

{
  "status": "ok",
  "timestamp": "2024-01-01T12:00:00Z"
}

5.3 性能指标接口

该接口需要启用 performance.enable_metrics;关闭后将不会注册 /metrics 端点。

请求

GET /metrics HTTP/1.1
Host: localhost:8080

响应

{
  "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 内存分配分析

使用示例:

# 采集 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 地点名称,如"五四广场"

返回示例:

{
  "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 示例

基础对话

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 等):

# 发送图片
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
  }'

多轮对话

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
  }'

政策咨询

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

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 等格式:

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 库

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

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)

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 示例

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 前端示例

<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 错误响应格式

{
  "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 错误处理建议

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)

# 服务器配置
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 部署

# 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 部署

# 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 反向代理

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 代码块格式返回。客户端需要解析这个格式:

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: 流式响应中途断开怎么办?

建议实现重连机制:

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: 如何实现多轮对话?

保存历史消息并在每次请求中传递:

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 初始版本