知识库问答实战(2):关键接口设计

一、按主链开接口

上一篇把 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一步一步推导出完整的表设计。