在完成了知识库构建、检索链路和生成逻辑之后,最后一步是将整个问答流程封装成一个可供前端或业务系统调用的接口。封装的目标很明确:调用方只需传入问题,就能拿到附带来源引用的答案,完全不需要关心内部的向量检索、提示拼接等细节。
下面给出一个最精简但能直接上线的 RESTful 接口设计方案,同时兼顾可观测性和基本错误处理。
12.5.1 接口定义
采用 POST 方式,接受 JSON 请求体,返回 JSON 响应。
请求
POST /api/v1/qa/ask
Content-Type: application/json
{
"question": "今年的年假政策有哪些变化?",
"top_k": 3,
"temperature": 0.3
}
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| question | string | 是 | 用户自然语言问题 |
| top_k | int | 否 | 检索返回的片段数量,默认 3 |
| temperature | float | 否 | 生成多样性控制,默认 0.3 |
成功响应
{
"code": 0,
"data": {
"answer": "根据2025版《员工手册》第3.2条,年假天数由15天调整为18天;同时,未休年假的折算比例从200%提升至250%。",
"sources": [
{
"doc_name": "员工手册-2025版.pdf",
"chunk_id": "page_12_chunk_3",
"content": "3.2 年假规定:员工每年享有18个工作日带薪年假...",
"score": 0.92
},
{
"doc_name": "福利政策修订通知.docx",
"chunk_id": "page_2_chunk_1",
"content": "未休年假折算比例自2025年1月起调整为250%...",
"score": 0.87
}
],
"query_id": "qa_20250218_001"
},
"message": "success"
}
| 字段 | 说明 |
|------|------|
| answer | 最终生成的回答 |
| sources | 引用的原文片段列表,每项包含文档名、片段ID、内容摘要和相似度得分 |
| query_id | 本次请求的唯一标识,便于日志追踪 |
失败响应
当检索不到高相关度片段时,系统应明确告知而非强行回答:
{
"code": 404,
"data": null,
"message": "未在知识库中找到与问题相关的信息,请尝试更换问法。"
}
12.5.2 核心处理流程
接口内部的执行步骤应保持清晰,每一个环节都便于独立调试和监控:
- 参数校验
检查 question 是否为空,top_k 是否在合理范围(如 1–10),不合法直接返回 4xx 错误。
- 问题向量化
使用与索引构建阶段完全一致的嵌入模型,将问题文本转换为向量。这一步通常调用嵌入服务的 API 或本地模型推理。
- 向量检索
在向量数据库中按余弦相似度搜索 top_k 个最相关片段。建议设置一个相似度阈值(例如 0.7),低于阈值的片段直接丢弃。如果过滤后结果为空,返回“未找到相关信息”。
- 上下文拼接
将检索到的片段按顺序编号,填入预定义的提示模板。一个普适的模板示例:
请根据以下资料回答用户问题。如果资料不足以回答,请明确说明“无法从现有资料中得出答案”,不要编造信息。
资料:
【1】{chunk_1 内容}
【2】{chunk_2 内容}
问题:{question}
回答:
- 调用大模型生成
将拼接好的提示发送给 LLM,设置较低的温度值(0.1–0.3)以保证回答的确定性。
- 组装来源信息
在返回给调用方时,将引用的片段元数据(文档名、片段内容、相似度)一并附上。注意,sources 中的内容应是检索到的原文片段摘要,而不是模型生成的内容,以保持溯源的真实性。
- 日志记录与上报
记录 query_id、问题、检索耗时、生成耗时、调用 token 数等关键指标,方便后续优化和成本统计。
12.5.3 实现注意事项
关于相似度阈值
阈值设置没有绝对标准,需要根据嵌入模型和业务场景实测。很多时候 0.7 可以作为一个起点:过高可能导致漏召回,过低则可能引入噪声。可以在接口中预留一个扩展参数,供运营调整。
关于并发与缓存
如果某些问题是高频且答案相对稳定的(如公司固定政策咨询),可以在接口外增加一层缓存(Redis),以问题文本的哈希作为键,缓存回答和来源。这能显著降低 LLM 调用成本,同时加快响应速度。
关于错误重试
检索和生成环节都可能遇到临时故障(如向量库超时、LLM 限流)。应在代码中加入简单的重试逻辑,例如对检索失败重试 1–2 次,对 LLM 调用捕获限流异常后返回友好提示。
关于提示词管理
实际生产环境中,提示模板不应硬编码在接口代码里。可以维护在配置文件或数据库中,以便随时调整措辞而不必重新部署服务。
12.5.4 一个 Python 伪代码骨架
以下是该接口核心逻辑的简化实现,方便快速理解结构(实际使用需替换为具体库和组件):
def ask_question(question, top_k=3, temperature=0.3):
# 1. 校验
if not question or not question.strip():
return error_response(400, "问题不能为空")
# 2. 问题向量化
q_vector = embedding_model.encode(question)
# 3. 向量检索
raw_hits = vector_db.search(q_vector, top_k=top_k)
hits = [h for h in raw_hits if h.score >= 0.7]
if not hits:
return error_response(404, "未在知识库中找到与问题相关的信息")
# 4. 构建提示
context = "\n".join([f"【{i+1}】{h.content}" for i, h in enumerate(hits)])
prompt = f"请根据以下资料回答用户问题。如果资料不足以回答,请明确说明……\n\n资料:\n{context}\n\n问题:{question}\n回答:"
# 5. 调用 LLM
answer = llm.generate(prompt, temperature=temperature)
# 6. 组装来源
sources = [{
"doc_name": h.doc_name,
"chunk_id": h.chunk_id,
"content": h.content[:200], # 截取前200字符作为预览
"score": round(h.score, 4)
} for h in hits]
# 7. 返回
return {
"code": 0,
"data": {
"answer": answer,
"sources": sources,
"query_id": generate_query_id()
},
"message": "success"
}
这个接口封装虽然简单,但已经涵盖了生产级 RAG 问答系统的核心要素:检索、生成、溯源、异常处理。在此基础上,可以逐步增加流式输出(SSE)、多轮对话、用户反馈收集等功能,向上扩展为更完整的应用服务。