# 接入 AI 大模型对话

更新日期：2026-10-25。当前生产版本：65ddec15。

本文面向调用知影平台大模型能力的 iOS、Web、UniApp 和外部应用。API 根地址为 `https://work.zingyang.com/api`。平台封装模型选择、会话、鉴权与权益计量；客户端不持有模型厂商密钥。

**这不是 OpenAI API 的直接兼容地址。** 请使用下文的平台请求、标准 Result 响应及命名 SSE 事件，不要将 OpenAI SDK 的 baseURL 直接改到这里并假定协议一致。

## 1. 调用前提

每次发起 AI 调用都必须同时满足：

1. 平台登录 token 有效，用户存在且未停用、未删除。
2. 当前租户存在、启用且在有效期内，租户 ID 为正数。
3. 当前用户在该租户内具有有效成员关系。
4. 租户具有有效的 `AI_CHAT_TOKENS` 权益，且剩余额度足以覆盖本次调用预算。

管理员也不豁免。服务端覆盖请求中的 userId/tenantId，不能通过 JSON 冒用用户、切换租户或指定免扣费。有效订阅、附加包、赠送及无限额度沿用平台现有权益计算；不能只判断“购买过套餐”就认定可以调用。

```http
X-Access-Token: <平台登录token>
X-Tenant-Id: <当前已确认租户ID>
Content-Type: application/json
```

不要传 Apple identity token，不要把模型厂商 API Key 放在这里，也不要假设 Bearer 头可以替代平台认证头。登录流程见 [Apple 登录与平台会话](/apple-sign-in-ios/)。

如果是 AI 工具代理用户发起调用，按平台 AI 调用规则先取得授权，并携带 `X-Zing-Ai-Context: 1`；涉及写入的调用还需 `X-Open-Confirm: confirmed`。对话会保存会话/消息并产生用量，不能把它当作无副作用的 GET 查询。普通客户端页面操作无需伪装成 AI 工具请求。

## 2. 模型选择

| 接口 | 用途 |
|---|---|
| `GET /ai/model/config/options` | 获取已启用模型选项；客户端选择平台已有配置 |
| `GET /ai/model/config/default` | 获取当前可用默认模型配置；无可用默认项时返回业务失败 |

两者使用相同认证与租户头。读取模型选项不等于已经通过推理额度检查。

首次接入建议省略模型字段，使用服务端默认配置。需要固定模型时，使用选项接口返回的配置 `id` 作为 `modelConfigId`，并沿用所选项的 `providerCode`、`modelCode` 对应填入 `provider`、`model`；不要猜测配置 ID 或拼装不匹配的组合。

模型配置和厂商密钥由平台管理。新增或编辑平台模型是另一个管理功能，有 `ai:model:*` 权限要求；不应由普通客户端通过新增配置来绕过现有模型选择或套餐限制。

## 3. 普通对话

```http
POST /ai/chat/completions
```

最小请求：

```json
{"prompt":"请用三句话介绍会议纪要应该包含什么。"}
```

| 字段 | 类型 | 用法 |
|---|---|---|
| prompt | string | 本轮用户问题；简单接入优先使用 |
| conversationId | string | 首轮省略，后续填服务端返回的会话 ID |
| modelConfigId | string | 可选，平台模型配置 ID |
| provider | string | 可选，所选配置的 providerCode |
| model | string | 可选，所选配置的 modelCode |
| messages | array | 高级调用的消息数组，成员包含 role、content；普通客户端无需重复发送整个已保存历史 |
| knowledgeBaseIds | string[] | 可选，仅传用户有权使用的知识库 ID |
| toolCallEnabled | boolean | 默认 false；接入工具调用前须完成权限及写操作确认交互 |

首次只发送 prompt；不要同时拼装重复的 prompt 和全量 messages。已有 conversationId 必须属于当前用户与租户，不能复用其他账号的会话 ID。平台会话入口会启用个人记忆处理，具体行为受服务端实现和用户配置影响。

不要把 `userId`、`tenantId`、`quotaExempt` 当成客户端可控制的授权参数。`quotaExempt` 不从 JSON 接收；`stream` 也不代替选择流式接口。本文不把内部 `hitsPayload`、`answerMeta`、`availableTools` 等字段作为普通客户端协议。

```bash
curl --request POST 'https://work.zingyang.com/api/ai/chat/completions' \
  --header 'X-Access-Token: <平台token>' \
  --header 'X-Tenant-Id: <当前租户ID>' \
  --header 'Content-Type: application/json' \
  --data '{"prompt":"请用三句话介绍会议纪要应该包含什么。"}'
```

成功响应示意（值仅为示例）：

```json
{
  "success": true,
  "code": 200,
  "message": "",
  "result": {
    "conversationId": "<会话ID>",
    "provider": "<实际提供方>",
    "model": "<实际模型>",
    "content": "会议纪要应包含议题、结论和行动项。",
    "finishReason": "stop",
    "promptTokens": 100,
    "completionTokens": 30,
    "totalTokens": 130,
    "ragContext": null,
    "citations": [],
    "toolCalls": []
  },
  "timestamp": 1792886400000
}
```

检查 HTTP 状态后，还必须检查 `success` 和 `code`。答案在 `result.content`，不是 `choices[0].message.content`。用量字段可能包含平台估算或厂商返回值，不保证等于套餐账本的预算占用或厂商最终账单；有剩余额度也可能因不足以覆盖本轮预算被拒绝。

## 4. 流式对话（SSE）

```http
POST /ai/chat/completions/stream
Accept: text/event-stream
```

请求体及认证头与普通对话一致。浏览器应使用支持 POST、JSON body 和认证头的 fetch 流式读取方式；原生 EventSource 不能直接满足这组请求条件。iOS 可使用 URLSession 的流式接收能力，UniApp 应选支持分块读取的平台适配。

```bash
curl --no-buffer --request POST 'https://work.zingyang.com/api/ai/chat/completions/stream' \
  --header 'X-Access-Token: <平台token>' \
  --header 'X-Tenant-Id: <当前租户ID>' \
  --header 'Accept: text/event-stream' \
  --header 'Content-Type: application/json' \
  --data '{"prompt":"帮我列出会议纪要的三个要点。"}'
```

协议示意（省略未使用的可选字段）：

```text
event: start
data: {"conversationId":"<会话ID>","provider":"<提供方>","model":"<模型>"}

event: delta
data: {"conversationId":"<会话ID>","content":"第一个要点"}

event: delta
data: {"conversationId":"<会话ID>","content":"是会议结论。"}

event: done
data: {"conversationId":"<会话ID>","messageId":"<消息ID>","finishReason":"stop","totalTokens":130}

```

| 事件 | 客户端处理 |
|---|---|
| start | 保存 conversationId，可包含模型及业务附加信息 |
| delta | 追加 `content`，不要覆盖之前收到的文本 |
| heartbeat | 保持连接，不追加到答案 |
| tool_start / tool_result / tool_error | 可选工具执行进度；按工具数据展示，不能当作文本增量 |
| done | 本轮完成，保存 messageId、用量、引用等信息；不保证携带完整答案 |
| error | 本轮失败，读取 `message`，停止成功状态展示 |

SSE `data` 是事件 JSON 本身，没有外层 Result，也没有 OpenAI 的 `choices[].delta` 或 `[DONE]` 约定。

解析时使用流式 UTF-8 解码；网络 chunk 可能截断汉字、JSON 或事件。按 SSE 空行分隔缓冲完整事件，支持 LF/CRLF、多行 data 和注释，不能每收到一个网络 chunk 就直接 JSON.parse。只有收到 done 才标记正常完成；连接结束不自动等于成功。

## 5. 错误与升级提示

普通 JSON 接口沿用标准结构：

| 业务码 | 含义与建议处理 |
|---|---|
| 401 | 登录或用户无效，重新登录 |
| 403 | 租户无效、未生效、停用、缺少成员关系，或调用确认条件不满足；按实际 message 提示 |
| 402 | 对应权益未开通、失效或额度不足；提示查看/续费/升级套餐，不自动无限重试 |
| 510 | 其他能力（例如知识库、工具、管理接口）的角色权限不足；不能通过换接口绕过 |
| 500 等 | 模型配置、参数、厂商或其他业务异常；保留实际错误信息，不全部提示为套餐不足 |

不能只看 HTTP 200：同步业务失败可能仍是 HTTP200 + `success=false`。

**流式错误需要单独处理**：认证过滤器等前置拒绝可能返回非 SSE 的 JSON/HTTP 错误；检查状态和 Content-Type 后再选择解析器。进入 SSE 后，当前控制器会把同步准备阶段错误转成 `error` 事件，数据只有 message 等事件字段，**不保证附带 401/402/403 业务码**：

```text
event: error
data: {"message":"<服务端实际错误信息>"}

```

因此不要对 SSE error 执行 `data.code === 402` 并假定一定能识别套餐状态，也不要靠匹配某一句中文消息作为稳定协议。需要准确区分升级场景时，应结合客户端现有套餐状态界面；流式错误本身按实际 message 展示。本文不虚构新的升级接口、错误结构或套餐购买地址。

## 6. 会话恢复、重命名和删除

| 接口 | 请求 |
|---|---|
| `GET /ai/chat/conversations` | 本人当前租户会话列表 |
| `GET /ai/chat/conversations/{id}` | 会话详情及已保存消息 |
| `PUT /ai/chat/conversations/{id}/rename` | JSON：`{"title":"新标题"}` |
| `DELETE /ai/chat/conversations/{id}` | 删除本人会话 |

以上接口仍要求有效身份及租户归属，但读取历史、重命名和删除不作为新的模型推理扣费。不要绕过归属校验。AI 工具执行 DELETE 还须采用 `X-Open-Confirm: text:<至少4字符的确认文本>`。

网络断开后，服务端可能继续生成并保存回答。客户端中止 HTTP 接收不等于厂商任务已取消，也不等于自动退还额度；先用会话详情核实结果，避免立即重试造成重复生成和用量。通用对话接口没有在本文承诺幂等请求键或独立停止接口；不要套用 Talk 的专用取消协议。

## 7. 知识库、工具与其他 AI 能力

- 知识库问答通过 `knowledgeBaseIds` 关联已授权资料，答案可带 citations；不要把普通模型回答伪装成有资料出处。
- 工具调用默认关闭。开启后仍受当前用户权限及数据写操作确认限制；模型决定调用工具不等于用户已经授权写入。
- 应用固定的人设、指令和输出模板应复用平台提示词管理；不要在客户端复制一套长期维护的系统提示词。用户当轮问题作为输入提交。
- 图片附件、录音问答、ASR/OCR、图片生成有各自业务协议与计量口径；不能仅修改本接口字段就假定调用了这些能力。文件上传见 [COS 上传文档](/ios-cos-upload/)。
- TTS 当前未核实到可发布的业务调用入口，本文不提供虚构路径。

## 8. 验证边界

源码依据：AiChatController、AiChatRequest/Response/StreamEvent、AiChatServiceImpl、AiInvocationAccess、AiModelConfigController；实际调用方为 Admin 的 ai.api.ts 和共享 SSE 解析链路。基础准入随65ddec15部署；本次文档编写没有发起新的生产模型推理或消耗客户额度，不将协议示例当作真实厂商联调结果。
