RAG 后端接口设计:为什么 ingest 要异步,query 要同步?

RAG 后端接口设计:为什么 ingest 要异步,query 要同步?

做 RAG 后端时,很容易把接口设计想得太简单:

一个接口负责导入文档,一个接口负责提问回答。

看起来没问题,但真正落到工程里,ingestquery 其实是两类完全不同的 API。

ingest 面对的是长任务。它要扫描文件、解析 Markdown、切 chunk、生成 embedding、写入索引或数据库。这个过程可能很慢,也可能部分失败。

query 面对的是一次即时回答。用户发出问题后,系统要完成检索、构建上下文、生成答案,并返回引用证据。

所以这篇文章想讲清楚一个核心判断:

ingest API 管后台任务生命周期,query API 管本次回答的可信结果。

如果把这两个接口都当成普通同步接口来写,后面很容易遇到超时、重复导入、错误码混乱、无证据乱答等问题。

1. 常见误区:让 ingest 接口一直等到导入完成

很多人第一版会把导入接口设计成这样:

http 复制代码
POST /api/ingest

然后希望它做完整件事:

  1. 接收文档路径或上传文件;
  2. 扫描文件;
  3. 解析 Markdown;
  4. 切分 chunk;
  5. 生成 embedding;
  6. 写入数据库或向量索引;
  7. 返回导入成功。

这个设计在 demo 里可能能跑,但一旦文档多一点,就会暴露问题。

第一,HTTP 请求会超时。客户端、网关、后端服务、反向代理都可能有超时限制。

第二,客户端无法知道任务进度。它不知道系统卡在扫描、解析、embedding,还是写库。

第三,失败很难表达。如果 100 个文件里 95 个成功、5 个失败,单纯返回一个 HTTP 200 或 500 都不够准确。

第四,重试会变危险。如果客户端没收到响应,它不知道后台任务是否已经创建成功。再次请求可能会制造一个重复导入任务。

所以,ingest 不适合设计成"请求一直挂着,直到全部导入完成"。

它更适合设计成异步 job。

2. ingest API:先创建 job,再查询状态

一个更稳的设计是:

http 复制代码
POST /api/ingest/jobs

这个接口只负责一件事:

接收导入请求,创建后台任务,返回 jobId。

比如请求:

json 复制代码
{
  "projectId": "agent-runtime",
  "sourceType": "markdown_directory",
  "sourcePath": "docs/"
}

响应:

http 复制代码
202 Accepted
json 复制代码
{
  "jobId": "ingest_20260523_001",
  "status": "accepted"
}

这里用 202 Accepted200 OK 更准确。

它表达的是:请求已经被接受,但后台处理还没有完成。

然后再提供一个状态查询接口:

http 复制代码
GET /api/ingest/jobs/{jobId}

返回:

json 复制代码
{
  "jobId": "ingest_20260523_001",
  "status": "running",
  "stage": "embedding",
  "totalFiles": 120,
  "processedFiles": 76,
  "failedFiles": 2
}

这样,导入任务就从一次 HTTP 请求,变成了一个可追踪的生命周期。

3. job 状态比 HTTP 状态更适合表达后台进度

导入任务可以有一组明确的状态:

状态 含义
accepted 请求已接收,任务已创建
scanning 正在扫描文件
parsing 正在解析文档
embedding 正在生成向量
storing 正在写入索引或数据库
completed 全部完成
partial_failed 部分文件失败
failed 任务整体失败
cancelled 任务被取消

这样做的好处是:HTTP 状态码只表达"这次接口调用是否正常",job 状态表达"后台任务执行到哪里了"。

这两个层次不要混在一起。

比如:

场景 推荐表达
请求缺少 sourcePath POST /api/ingest/jobs 返回 400 Bad Request
用户没有项目权限 POST /api/ingest/jobs 返回 403 Forbidden
任务创建成功 POST /api/ingest/jobs 返回 202 Accepted
后台 embedding 服务挂了 job 状态变成 failed,错误记录在 job 里
部分 Markdown 文件解析失败 job 状态变成 partial_failed

也就是说,后台执行阶段失败,不应该倒过来让最初的 POST /api/ingest/jobs 返回 500。

因为最初的接口调用已经完成了它的职责:创建任务。

4. 超时不等于任务失败

ingest 设计里最容易出错的是重试。

假设客户端调用:

http 复制代码
POST /api/ingest/jobs

但是因为网络抖动或网关超时,客户端没有拿到响应。

这时客户端不能简单认为:

请求失败了,所以我再创建一个新任务。

因为后台可能已经成功创建了 job,只是响应没有传回客户端。

如果客户端直接重试,就可能创建两个导入任务:

  • ingest_001
  • ingest_002

它们处理同一批文档,浪费资源,还可能造成重复 chunk、重复索引或版本冲突。

所以 ingest API 需要幂等设计。

5. 用 Idempotency-Key 避免重复导入

客户端创建导入任务时,可以带一个幂等键:

http 复制代码
POST /api/ingest/jobs
Idempotency-Key: agent-runtime-docs-20260523-v1

服务端逻辑可以是:

  1. 如果这个 key 第一次出现,创建新的 job;
  2. 如果这个 key 已经出现过,返回之前创建的 job;
  3. 如果同一个 key 对应的请求参数不同,返回冲突错误。

比如第二次请求时,服务端可以返回:

json 复制代码
{
  "jobId": "ingest_20260523_001",
  "status": "running"
}

而不是再创建一个新的 job。

这里的关键是:

超时不等于后台任务失败,重试不应该制造重复任务。

对 RAG 导入来说,幂等不是锦上添花,而是基础能力。

6. 后台失败应该记录在 job 里

假设导入任务执行到 embedding 阶段时,embedding 服务暂时不可用。

这时不应该让最初的 POST /api/ingest/jobs 返回 500,因为那个请求早就结束了。

更合理的是让状态查询接口返回:

json 复制代码
{
  "jobId": "ingest_20260523_001",
  "status": "failed",
  "failedStage": "embedding",
  "errorCode": "EMBEDDING_SERVICE_UNAVAILABLE",
  "message": "Embedding service is temporarily unavailable.",
  "retryable": true
}

这里的 retryable 很重要。

它告诉客户端或调度系统:这个失败是否适合重试。

比如:

失败原因 是否适合重试
embedding 服务暂时不可用
数据库临时连接失败
用户没有权限
文件格式不支持
请求参数缺失

这样,系统才能区分"临时故障"和"请求本身有问题"。

7. query API 为什么通常适合同步返回

ingest 不同,query 通常是用户正在等待的一次回答。

一个典型接口可以是:

http 复制代码
POST /api/query

请求:

json 复制代码
{
  "projectId": "agent-runtime",
  "question": "ingest API 为什么要异步?",
  "topK": 5,
  "filters": {
    "sourceType": "docs"
  }
}

响应:

json 复制代码
{
  "status": "success",
  "answer": "ingest API 适合异步设计,因为导入包含扫描、解析、embedding 和写库等长任务,需要用 jobId 管理进度、失败和重试。",
  "citations": [
    {
      "sourcePath": "docs/rag-api-design.md",
      "heading": "ingest API",
      "chunkId": "chunk_001"
    }
  ]
}

这个接口通常要在一次请求里完成:

  1. 根据问题生成 query embedding;
  2. 使用 metadata filter 限定范围;
  3. 检索 top-k chunks;
  4. 构建上下文;
  5. 调用模型生成回答;
  6. 返回 answer 和 citations。

它是同步的,不是因为它没有内部步骤,而是因为用户需要的是"这次问题的结果"。

8. query 的核心不是"尽量回答",而是"可信回答"

RAG 的 query API 有一个底线:

没有证据时,不要装作知道。

如果没有检索到足够相关的 chunk,接口不应该编一个看起来合理的答案。

更合理的返回是:

json 复制代码
{
  "status": "insufficient_evidence",
  "answer": null,
  "citations": [],
  "reason": "No retrieved chunks met the relevance threshold."
}

这里要注意:insufficient_evidence 不应该直接返回 HTTP 500。

因为查询过程本身是正常完成的,只是业务结果是"没有足够证据"。

HTTP 500 表示服务内部错误,比如程序异常、依赖服务异常、数据库异常。

而没有检索到证据,属于正常业务结果。

所以更合理的是:

http 复制代码
200 OK

配合:

json 复制代码
{
  "status": "insufficient_evidence",
  "answer": null,
  "citations": []
}

这样客户端可以提示用户:

  • 换一个问题;
  • 放宽过滤条件;
  • 先导入更多资料;
  • 检查文档是否已经完成索引。

9. query 的几类典型结果

query API 不应该只有成功和失败两种结果。

更实用的设计是至少区分三类:

状态 含义
success 找到足够证据,并生成完整答案
partial_answer 有部分证据,只能回答一部分
insufficient_evidence 没有足够证据,不生成最终答案

比如部分回答可以是:

json 复制代码
{
  "status": "partial_answer",
  "answer": "目前只能确认 ingest API 适合用 jobId 管理后台任务,但没有找到关于重试策略的完整说明。",
  "citations": [
    {
      "sourcePath": "docs/rag-api-design.md",
      "heading": "ingest API",
      "chunkId": "chunk_003"
    }
  ]
}

这里最重要的是:

answer 不能比 citations 支持的内容走得更远。

这也是 RAG 和普通聊天接口的区别之一。

RAG 的回答不只是"语言上通顺",还要能被证据支撑。

10. query 超时怎么处理

query 也可能超时,但它的处理要看 API 合同。

如果接口承诺必须返回最终答案,那么 LLM 生成阶段超时,可以返回:

http 复制代码
504 Gateway Timeout

因为系统没有完成本次 query 的最终目标。

但如果产品允许降级,也可以返回 evidence-only 结果:

json 复制代码
{
  "status": "only_evidence",
  "answer": null,
  "citations": [
    {
      "sourcePath": "docs/rag-api-design.md",
      "heading": "query API",
      "chunkId": "chunk_008"
    }
  ],
  "reason": "Generation timed out after retrieval succeeded."
}

这两种设计没有绝对对错,关键是接口合同要清楚。

如果调用方期待的是 answer,那么超时就是失败。

如果调用方可以接受"先给证据,稍后再生成",那么降级结果就是一种可用设计。

11. 三层状态不要混在一起

RAG 后端接口设计里,最重要的是分清三层状态:

层次 负责表达什么 例子
HTTP 状态码 请求/服务层是否正常 200202400403500503504
job 状态 后台任务生命周期 acceptedrunningcompletedfailed
业务结果 本次回答是否有证据、是否完整 successpartial_answerinsufficient_evidence

几个典型判断:

场景 推荐设计
创建导入任务成功 202 Accepted + jobId
导入任务后台失败 GET job 返回 failed
客户端重复提交导入 Idempotency-Key 返回同一个 job
query 没有证据 200 OK + insufficient_evidence
query 服务内部异常 500 Internal Server Error
query 依赖服务暂时不可用 503 Service Unavailable
query 生成阶段超时且不能降级 504 Gateway Timeout

如果这三层混在一起,接口会越来越难维护。

比如把"没有证据"返回成 500,调用方就会误以为服务挂了。

把"后台任务失败"塞回创建接口的 HTTP 状态码里,又会让异步任务的生命周期变得不可追踪。

12. 一个可复用的设计 checklist

设计 RAG 后端 API 时,可以用下面这组问题检查:

ingest API

  • 这个接口是否只负责创建导入任务,而不是同步完成全部导入?
  • 是否返回 jobId
  • 是否有 GET /api/ingest/jobs/{jobId} 查询状态?
  • job 状态是否能表达 acceptedrunningcompletedfailedpartial_failed
  • 是否记录 failedStageerrorCoderetryable
  • 是否支持 Idempotency-Key,避免重复导入?
  • 是否能区分请求参数错误和后台执行失败?

query API

  • 返回里是否包含 answercitations
  • 没有证据时,是否返回 insufficient_evidence,而不是编答案?
  • insufficient_evidence 是否被当作业务结果,而不是 HTTP 500?
  • 是否区分 successpartial_answerinsufficient_evidence
  • 如果 LLM 超时,接口合同是返回 504,还是允许 only_evidence 降级?
  • answer 是否只表达 citations 能支持的内容?

总结

ingestquery 看起来都是 RAG 系统的接口,但它们解决的问题完全不同。

ingest API 面对的是后台长任务,所以要异步化,用 jobId 管理状态、进度、失败和重试。

query API 面对的是用户的一次问题,所以通常同步返回 answer + citations,并在没有证据时明确返回 insufficient_evidence

更重要的是,不要把 HTTP 状态码、job 状态和业务结果混在一起。

HTTP 状态码表达请求和服务是否正常。

job 状态表达后台任务执行到哪里。

业务结果表达本次回答是否可信、是否有足够证据。

这三个边界清楚了,RAG 后端接口才不会在超时、重试、失败和无证据场景里乱掉。

相关推荐
一条泥憨鱼9 小时前
能够让AI做事的“Skill“有什么奥秘
人工智能·ai·agent·rag·skill·mcp
fengxin_rou11 小时前
【Spring AI 集成 DeepSeek 实现 AI 摘要与 RAG 问答】:从原理到落地实践
数据库·mysql·rag·deepseek
Terrence Shen16 小时前
Agent面试八股文(系列之二)
人工智能·大模型·agent·rag
Restart-AHTCM1 天前
AI时代大前端Agent开发LangChain.js
typescript·langchain·memory·rag·tools
ZGi.ai1 天前
采购部门用AI审供应商资质:从3天压缩到3小时的方案
大数据·人工智能·rag·供应商管理·企业ai·文档审核·采购ai
德思特2 天前
从 Dify 配置页理解 RAG 的重要参数
java·人工智能·llm·dify·rag
鼎道开发者联盟2 天前
跳出传统 RAG!用 LLM Wiki 构建闭环式产品 Agent 协作体系
agent·rag·hermes·llmwiki
Honey Ro2 天前
浅析大模型 Agent 的记忆(Memory)机制
深度学习·语言模型·llm·rag
YDS8293 天前
DeepSeek RAG&MCP + Agent智能体项目 —— RAG知识库的搭建和接口实现
java·ai·springboot·agent·rag·deepseek