一、按主链开接口
上一篇把 MVP 主链走通了,这篇按主链节点说一下关键的接口设计。
| 主链这一步 | 接口 | 说明 |
|---|---|---|
| 建库 | POST /knowledge-bases | 资源容器 |
| 上传 | POST /documents | 202 + document_id + job_id(KB 级) |
| 等处理 | GET /index-jobs/{job_id} | 用户等进度;前端轮询此接口 |
| 提问 | POST /chat/stream | SSE 流式 |
| 看答案(含引用) | POST /chat/stream(answer + citations) |
有命中时答案与引用同响应返回 |
可选(反馈/历史):MVP 可不做。
二、三个关键接口
1. POST /documents:异步 + 幂等
容易踩坑 :返回 200 + document_id,前端立刻放开提问------文档还不能被搜到。
建议 :multipart 上传 PDF,异步处理,返回 202。
POST /api/v1/documents?kb_id=kb_001&idempotency_key=upload-xxx
Content-Type: multipart/form-data
Body: file=公司制度手册.pdf
→ 202 { document_id, job_id, should_enqueue }
要点:202 表异步;job_id 是 KB 级 ,轮询它;idempotency_key 防连点(Query 或 Header 都行)。
响应包在统一结构里:{ "request_id", "data": { ... } }。
2. GET /index-jobs/{job_id}:状态 + 进度
主链「等处理」那一步。任务状态跟整库索引重建对齐:
queued → running → succeeded
→ failed
「能问了」别只看 job :job succeeded 后,还要看相关 document.status 是否已到 ready(或库内没有还在处理的文档)。
json
{
"data": {
"job_id": "job_456",
"kb_id": "kb_001",
"status": "running",
"progress": 45,
"error_message": null
},
"request_id": "req_002"
}
3. POST /chat/stream:门禁 + 没搜到别硬答
空库 → 库里还没有 ready 的文档,别硬答。
索引还没好 → 409 + INDEX_NOT_READY,前端继续轮询或提示。
搜不到 → 别硬编。推荐 200 + 固定拒答文案 + 空 citations (也可加 error_code: NO_HIT 方便前端分支):
json
{
"data": {
"answer": "未检索到足够相关内容,无法基于文档回答。",
"citations": [],
"conversation_id": "conv_789",
"message_id": "msg_101"
},
"request_id": "req_003"
}
有命中时 :流式逐字出答案,finished: true 那条带上 citations (文档名、页码、snippet)和 message_id。
三、联调前再补三件事
接口能调通,只是第一步。下面三条是「用户真用起来、出了问题能查」时最常缺的------前后端联调前对一遍,省得上线后扯皮。
1. 可追溯:用户问「凭啥这么答?」你得说得清
用户看到「年假 10 天」,下一句往往是:哪份文档写的?
所以每条回答除了 answer,还要:
citations:至少带文档名、页码、原文片段(snippet)。前端能做成「点击查看原文」,排障时也能核对「模型是不是瞎编的」。message_id:这一条问答在库里的唯一编号。用户点「这条不对」、要反馈或人工复核,都靠它定位。request_id:这一次 HTTP 请求的全局编号(见下一条),和message_id一起用,能从界面一路追到日志。
json
{
"data": {
"answer": "根据制度手册,年假为......",
"message_id": "msg_101",
"citations": [{
"document_title": "公司制度手册",
"page": 15,
"snippet": "员工享有带薪年假......"
}]
},
"request_id": "req_20240101_001"
}
2. 可排障:用户说「刚才提问没反应」,别靠猜时间对日志
同一次请求会经过网关、业务服务、检索、模型调用,日志散在好几个文件里。如果只靠「大概 14:32 左右」,排查很痛苦。
做法 :从收到请求起生成一个 request_id ,写进响应体(或响应头 X-Request-Id),并且后面所有日志、用量记录都带同一个 id。
- 客户端可以传自己的 id(比如前端生成的 UUID),服务端优先用;没传再生成。
- 用户截图里只要有
request_id,后端就能一把捞出:进了哪个接口、检索命中了几条、模型回了啥。
3. 失败说人话:别只扔一个 failed
索引失败时,如果只返回 status: failed,用户只知道有问题,不知道问题出在哪里。
拆开两个字段:
| 字段 | 给谁看 | 干啥 |
|---|---|---|
error_code |
程序 / 前端 | 决定展示啥按钮、走啥分支 |
error_message |
人 | 可以直接贴在页面上 |
常见几种:
| error_code | 用户该咋办 | 要不要「重试」按钮 |
|---|---|---|
| UNSUPPORTED_FORMAT | 换 PDF/Word/TXT | 不要 |
| FILE_TOO_LARGE | 压缩或拆文件 | 不要 |
| EMPTY_CONTENT | 检查文件是不是空的 | 不要 |
| PARSE_ERROR / 超时类 | 稍后再传一次 | 要 → POST /index-jobs/{id}/retry |
| VECTOR_SERVICE_ERROR | 服务暂时不可用,晚点再试 | 要 |
接口契约
| 规矩 | 大白话 |
|---|---|
| kb 归属 | 带 kb_id 的接口都要查:这个库是不是当前用户的?不是就 403,别悄悄让别人库的数据漏出来。 |
| 分页 | 列表统一 page + page_size + total (总数)。没有 total,前端做不了「共 47 条、第 2 页」。 |
| 幂等 | 写接口(上传、建库)支持 Idempotency-Key:用户连点两次上传,别入库两份一样的 PDF。 |
总结与预告
本篇主要讲关键接口的设计,下篇讲怎么用5维框架从0到1一步一步推导出完整的表设计。