人人都会AI编程

13.5 接口封装与前端对接

更新时间:2026-07-12

搭建好检索和生成流程后,要让业务系统真正用起来,必须将整个 RAG 问答能力封装为可调用的接口。这一步看似简单,但设计得当与否,直接影响前端集成效率、用户体验和后续维护成本。以下内容基于真实落地经验,聚焦最核心的设计要点。

1. 接口设计原则

在封装 RAG 接口时,遵循几个实用原则可以避免后期大量返工:

  • 单一职责:一个接口只做一件事。通常分为“提问接口”和“反馈/评价接口”,不要混在一起。
  • 前端友好:返回结构清晰,字段命名一致,错误信息可读,方便前端直接渲染。
  • 可观测性:每个请求带上唯一标识,便于追踪整条调用链(从用户问题到最终回答)。
  • 向后兼容:字段只增不减,老版本前端调用新接口不会报错。

2. 核心接口:提交问题并获取流式回答

现代 AI 对话产品几乎都采用流式输出(SSE,Server-Sent Events),让回答逐字返回,避免用户长时间等待空白页面。接口设计如下:

请求方式POST /api/v1/chat/completions
Content-Typeapplication/json
请求体结构(精简版):

{
  "message": "员工年假怎么计算?",
  "conversation_id": "conv_20250115_001",
  "stream": true
}

字段说明:

  • message:用户当前输入的问题。
  • conversation_id:可选,用于关联历史对话。多轮对话时前端应传同一个 ID,后端据此取出历史上下文进行改写或拼接。
  • stream:布尔值,告知后端采用流式还是非流式返回。

流式响应(SSE 格式):
后端设置 Content-Type: text/event-stream,每次推送一个数据块:

data: {"token": "根据", "type": "text"}

data: {"token": "《员工手册》", "type": "text"}

data: {"token": "第", "type": "text"}

...

data: {"type": "source", "sources": [{"doc_name": "员工手册2025版.pdf", "chunk_id": "chunk_103", "excerpt": "年假天数按司龄...", "page": 3}]}

data: [DONE]

设计要点:

  • 回答正文通过连续的 type: "text" 事件逐 token 推送,前端累加显示。
  • 在所有正文 token 发送完毕后,推送一个 type: "source" 事件,携带本次回答引用的来源列表。这样前端可以在回答结束时一次性展示“参考来源”区域,而不会在正文中间插入干扰。
  • [DONE] 标记流结束,前端可据此关闭连接、启用输入框等。

非流式响应(备用,用于调试或需要一次性获取完整回答的场景):
返回标准 JSON:

{
  "code": 0,
  "data": {
    "answer": "根据《员工手册》...",
    "sources": [
      {
        "doc_name": "员工手册2025版.pdf",
        "chunk_id": "chunk_103",
        "excerpt": "年假天数按司龄分为...",
        "page": 3
      }
    ],
    "conversation_id": "conv_20250115_001"
  },
  "trace_id": "req_8a3f2b"
}
  • trace_id 用于全链路日志追踪,排查问题时可以据此快速定位。
  • code 为 0 表示正常,非 0 时附带 message 字段说明错误原因。

3. 辅助接口:用户反馈

为了让系统持续优化,前端通常提供“点赞/点踩”或“纠错”入口。接口设计简单直接:

请求方式POST /api/v1/chat/feedback
请求体

{
  "conversation_id": "conv_20250115_001",
  "message_id": "msg_101",
  "rating": "negative",
  "comment": "回答中引用的政策已过期",
  "correct_answer": "实际应为15天"
}
  • message_id 是系统为每次助手回复分配的唯一 ID,在回答响应中一并返回,方便前端关联。
  • rating 取值 positivenegative
  • commentcorrect_answer 为可选,供用户补充说明,这些数据可以定期导出用于优化知识库或提示词。

4. 前端集成的关键考虑

连接管理与重试
流式连接可能因网络波动中断,前端需要实现自动重连或降级策略:

  • 中断时弹出“请求超时,正在重试”提示,并自动调用非流式接口获取完整回答作为兜底。
  • 或者通过 conversation_id 向服务端查询当前对话的最新完整内容,补全已显示部分之后的内容。

来源的呈现方式
收到 source 事件后,前端在回答底部的折叠区域或侧边栏展示来源列表。每条来源应包含:

  • 文档名称(可点击跳转到预览页或原生文件链接)
  • 片段摘要(展示高亮的关键句)
  • 如果系统支持,可附带页码

这样的设计既不过度打断阅读流,又让有需要的用户能快速追溯原文。

会话状态的保存
前端应持久化 conversation_id 和对话历史到本地存储或服务端,使得用户刷新页面或切换设备后仍能继续当前会话。服务端一般负责保存完整历史,前端按需拉取即可。

安全与鉴权
所有 API 调用必须携带认证信息(如 Bearer Token),且后端需校验该用户是否有权限访问对应的知识库。对于展示来源的文档链接,后端应生成带时效的临时访问链接,避免直接暴露内部存储桶地址。

5. 一个简化版前端集成代码示意

以下是使用 Fetch API 消费流式接口的极简示例,帮助理解对接方式:

async function askQuestion(message, conversationId) {
  const response = await fetch('/api/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + getToken()
    },
    body: JSON.stringify({
      message: message,
      conversation_id: conversationId,
      stream: true
    })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let answerBuffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const text = decoder.decode(value);
    // 解析 SSE 数据,此处简化,实际需按行分割并处理 data: 前缀
    if (text.trim().startsWith('data:')) {
      const payload = JSON.parse(text.trim().substring(5));
      if (payload.type === 'text') {
        answerBuffer += payload.token;
        updateAnswerDisplay(answerBuffer);
      } else if (payload.type === 'source') {
        renderSources(payload.sources);
      }
    }
  }
}

6. 常见问题与处理

  • 回答过长时用户体验:后端可设置 max_tokens 限制生成长度,同时前端应支持滚动跟随最新内容。
  • 多轮对话中的上下文管理:接口设计上让前端始终携带 conversation_id,后端通过这个 ID 读取最近几轮历史,自动完成“问题改写”或“上下文拼接”,前端无需手动拼接对话串。
  • 接口超时设置:流式接口建议使用较长的读取超时(如 60s),避免回答未完成就断开。
  • 版本管理与灰度发布:可在请求头中加入 X-API-Version,方便后端根据版本路由到不同的 RAG 配置(如不同的知识库快照或提示词),实现平滑上线。

通过上述清晰且精简的接口封装,可以快速将 RAG 能力交付给前端团队,让他们关注用户交互和界面体验,而不必深入理解内部检索和生成的复杂性。这样的对接方式已在多个实际项目中验证有效,并能随着系统迭代灵活扩展。