搭建好检索和生成流程后,要让业务系统真正用起来,必须将整个 RAG 问答能力封装为可调用的接口。这一步看似简单,但设计得当与否,直接影响前端集成效率、用户体验和后续维护成本。以下内容基于真实落地经验,聚焦最核心的设计要点。
1. 接口设计原则
在封装 RAG 接口时,遵循几个实用原则可以避免后期大量返工:
- 单一职责:一个接口只做一件事。通常分为“提问接口”和“反馈/评价接口”,不要混在一起。
- 前端友好:返回结构清晰,字段命名一致,错误信息可读,方便前端直接渲染。
- 可观测性:每个请求带上唯一标识,便于追踪整条调用链(从用户问题到最终回答)。
- 向后兼容:字段只增不减,老版本前端调用新接口不会报错。
2. 核心接口:提交问题并获取流式回答
现代 AI 对话产品几乎都采用流式输出(SSE,Server-Sent Events),让回答逐字返回,避免用户长时间等待空白页面。接口设计如下:
请求方式:POST /api/v1/chat/completions
Content-Type:application/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取值positive或negative。comment和correct_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 能力交付给前端团队,让他们关注用户交互和界面体验,而不必深入理解内部检索和生成的复杂性。这样的对接方式已在多个实际项目中验证有效,并能随着系统迭代灵活扩展。