主题
14.18-RAG接口设计
要点
- RAG 接口覆盖三个核心流程:文档管理(上传/删除/状态查询)、知识库检索、对话集成
- 文档上传是异步流程——接口返回 202,前端轮询状态
- 检索接口暴露 topK、阈值、过滤等参数,但控制默认行为避免前端过度配置
- 对话接口整合检索 + 上下文拼接 + LLM 生成 + 引用提取,关键在上下文窗口预算分配
- 流式响应先返回检索来源,再流式生成回答——用户体验显著提升
- 错误处理和降级策略在接口层统一处理:不同组件故障对应不同降级路径
1. 接口设计要解决的三个冲突
前 17 讲逐一拆解了 RAG 的每个组件——索引、检索、查询、评估。现在要把它们拼成一个对外可用的 API。
接口设计的核心难点不是写 CRUD,而是在三组冲突中找到适合你的平衡点:
检索 vs 对话,一个端点还是两个? 合并成一个看起来简洁,但「只检索不生成」是高频需求——前端展示引用来源、调试检索质量、让用户选择上下文,都只需要检索结果。拆成两个端点,各自职责清楚,也便于独立演进。
同步 vs 流式。 检索结果通常在 1-3 秒内返回,同步 JSON 足够。对话接口涉及 LLM 生成,可能需要 5-30 秒,纯同步会让用户面对一个漫长的加载状态。流式响应(SSE)让用户先看到检索到的来源,再逐字看到回答生成——体验差距明显,但实现成本也更高。
异步文档处理:轮询还是推送? 文档上传后要经历解析、切分、向量化,可能需要几十秒到几分钟。WebSocket 推送看似实时,但对这种低频长时任务来说,连接管理成本远超收益。轮询状态接口是更务实的选择。
这三个选择没有标准答案,取决于你的用户规模和前端复杂度。本章的实现按「检索和对话分离、对话同时提供同步和流式、文档处理用 202 + 轮询」来组织,你可以根据实际需求调整。
1.1 接口全景
文档管理:
POST /api/documents 上传文档(异步处理)
GET /api/documents 列出文档(分页)
GET /api/documents/:id 文档详情
DELETE /api/documents/:id 删除文档
GET /api/documents/:id/status 处理状态查询
检索:
POST /api/rag/search 纯检索(返回 chunk,不生成)
对话:
POST /api/rag/chat 同步对话(检索 + 生成)
POST /api/rag/chat/stream 流式对话(SSE)
知识库管理:
POST /api/knowledge-bases 创建知识库
GET /api/knowledge-bases 列出知识库
DELETE /api/knowledge-bases/:id 删除知识库1.2 RAG 请求链路
每个 RAG 请求经历的链路比普通 API 长得多——认证、权限、输入校验、Embedding、向量检索、Rerank、上下文拼接、LLM 生成、引用提取。每个环节都可能超时或失败。
接口设计的本质是为这条链路上的每个环节设计延迟策略、失败策略和降级策略。
2. 文档管理接口
文档是 RAG 系统的输入,但上传 ≠ 可用。文档要经过解析、切分、向量化才能被检索到,整个过程可能需要几十秒甚至几分钟。这意味着文档接口必须设计成异步的。
2.1 上传接口:202 + 异步队列
typescript
// src/routes/documents.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const documentsApp = new Hono()
const uploadSchema = z.object({
title: z.string().min(1).max(256),
knowledgeBaseId: z.string().uuid(),
requestId: z.string().uuid().optional(), // 幂等键
})
documentsApp.post('/', zValidator('form', uploadSchema), async (c) => {
const user = c.get('user')
const { title, knowledgeBaseId, requestId } = c.req.valid('form')
const file = c.req.valid('form').file
if (!file) {
return c.json({ error: 'No file provided' }, 400)
}
// 幂等检查:同一 requestId 直接返回已有结果
if (requestId) {
const existing = await documentService.getByRequestId(requestId)
if (existing) {
return c.json({
id: existing.id,
status: existing.status,
message: '文档已上传(幂等返回)',
}, 202)
}
}
// 1. 验证知识库权限
const kb = await knowledgeBaseService.getById(knowledgeBaseId)
if (!kb || kb.tenantId !== user.tenantId) {
return c.json({ error: 'Knowledge base not found' }, 404)
}
if (!hasPermission(user, kb, 'write')) {
return c.json({ error: 'Permission denied' }, 403)
}
// 2. 保存文件到对象存储
const fileKey = `docs/${user.tenantId}/${Date.now()}-${file.name}`
await objectStorage.put(fileKey, await file.arrayBuffer())
// 3. 创建文档记录(状态:pending)
const document = await documentService.create({
title,
knowledgeBaseId,
fileKey,
fileName: file.name,
fileSize: file.size,
mimeType: file.type,
status: 'pending',
requestId,
userId: user.id,
tenantId: user.tenantId,
})
// 4. 推入处理队列(异步)
await queue.publish('document.process', {
documentId: document.id,
fileKey,
mimeType: file.type,
})
return c.json({
id: document.id,
status: document.status,
message: '文档已上传,正在处理',
}, 202)
})这里有个容易忽略的点:幂等性。网络抖动、用户重复点击都可能导致重复上传。给前端一个 requestId 字段,服务端用它做去重——相同的 requestId 直接返回已有记录,而不是创建新文档。
2.2 状态查询接口
前端轮询文档处理进度。状态查询是只读的,适合高频轮询。
typescript
documentsApp.get('/:id/status', async (c) => {
const user = c.get('user')
const { id } = c.req.param()
const document = await documentService.getById(id)
if (!document || document.tenantId !== user.tenantId) {
return c.json({ error: 'Not found' }, 404)
}
return c.json({
id: document.id,
status: document.status, // pending | processing | completed | failed
progress: document.progress, // 0-100
error: document.error, // 失败时的错误信息
chunks: document.chunkCount, // 处理完成后的 chunk 数量
createdAt: document.createdAt,
updatedAt: document.updatedAt,
})
})文档处理的完整状态机:
pending → processing → completed
↘ failed → processing (重试)重试策略:处理失败后不自动重试,由前端触发。原因很清楚——有些失败是内容问题(不支持的格式、文件损坏),自动重试只会反复失败。建议前端限制最多重试 3 次,间隔递增(5s → 15s → 45s)。
2.3 删除接口
删除文档需要清理三个地方:向量数据、原始文件、文档记录。
typescript
documentsApp.delete('/:id', async (c) => {
const user = c.get('user')
const { id } = c.req.param()
const document = await documentService.getById(id)
if (!document || document.tenantId !== user.tenantId) {
return c.json({ error: 'Not found' }, 404)
}
if (!hasPermission(user, document, 'delete')) {
return c.json({ error: 'Permission denied' }, 403)
}
// 三步清理,每步独立容错
const errors: string[] = []
try {
await vectorStore.deleteDocument(document.knowledgeBaseId, id)
} catch (e) {
errors.push('Vector data cleanup failed')
}
try {
await objectStorage.delete(document.fileKey)
} catch (e) {
errors.push('File cleanup failed')
}
await documentService.updateStatus(id, 'deleted')
return c.json({
success: errors.length === 0,
warnings: errors,
})
})三步清理不放在一个事务里——向量数据库和对象存储通常不支持跨系统事务。每步独立容错,部分失败时返回警告,由运维或定时任务兜底清理残留数据。
3. 检索接口
检索接口只返回相关 chunk——不生成回答。这个接口用于前端展示引用来源、调试检索质量,或让用户在对话前选择上下文范围。
设计检索接口的关键决策是:暴露多少参数? 参数太少,前端无法针对不同场景调整;参数太多,接口变得复杂且难以维护。折中方案是暴露核心参数(topK、阈值、过滤),其余用合理默认值。
3.1 检索参数设计
typescript
const ragApp = new Hono()
const searchSchema = z.object({
query: z.string().min(1).max(2000),
knowledgeBaseIds: z.array(z.string()).optional(),
topK: z.number().int().min(1).max(50).optional().default(5),
threshold: z.number().min(0).max(1).optional().default(0.7),
enableRerank: z.boolean().optional().default(false),
filters: z.object({
documentIds: z.array(z.string()).optional(),
dateRange: z.object({
start: z.string().datetime().optional(),
end: z.string().datetime().optional(),
}).optional(),
}).optional(),
})enableRerank 默认 false 是有意的。Rerank 增加 200-500ms 延迟,对调试场景来说太慢;对话场景下再默认开启。
3.2 检索实现
typescript
ragApp.post('/search', zValidator('json', searchSchema), async (c) => {
const user = c.get('user')
const params = c.req.valid('json')
// 1. 查询 embedding
const queryVector = await embeddingService.embed(params.query)
// 2. 构建过滤条件
const filter = buildSearchFilter(user, params)
// 3. 向量检索
// 启用 rerank 时多召回,让 reranker 有足够候选
let results = await vectorStore.search(
params.knowledgeBaseIds ?? user.accessibleKnowledgeBases,
queryVector,
{
topK: params.enableRerank ? params.topK * 5 : params.topK,
filter,
threshold: params.threshold,
}
)
// 4. Rerank
if (params.enableRerank) {
results = await rerankService.rerank(params.query, results, {
topK: params.topK,
})
}
// 5. 格式化返回
return c.json({
results: results.map((r) => ({
id: r.id,
content: r.content,
score: r.score,
metadata: {
documentId: r.metadata.documentId,
documentTitle: r.metadata.documentTitle,
sectionTitle: r.metadata.sectionTitle,
chunkIndex: r.metadata.chunkIndex,
},
})),
total: results.length,
})
})3.3 过滤条件构建
过滤条件的第一优先级永远是租户隔离——SaaS 场景下不能跨租户检索。
typescript
function buildSearchFilter(
user: User,
params: z.infer<typeof searchSchema>
): Filter {
const conditions: FilterCondition[] = [
// 租户隔离——不可覆盖
{ key: 'tenant_id', match: { value: user.tenantId } },
]
// 知识库范围
if (params.knowledgeBaseIds?.length) {
conditions.push({
key: 'knowledge_base_id',
match: { any: params.knowledgeBaseIds },
})
} else {
conditions.push({
key: 'knowledge_base_id',
match: { any: user.accessibleKnowledgeBases },
})
}
// 文档过滤
if (params.filters?.documentIds?.length) {
conditions.push({
key: 'document_id',
match: { any: params.filters.documentIds },
})
}
// 时间范围
if (params.filters?.dateRange?.start) {
conditions.push({
key: 'created_at',
range: { gte: params.filters.dateRange.start },
})
}
if (params.filters?.dateRange?.end) {
conditions.push({
key: 'created_at',
range: { lte: params.filters.dateRange.end },
})
}
return { must: conditions }
}这个 buildSearchFilter 函数在检索接口和对话接口中都会用到。提取成公共函数避免重复实现,同时确保过滤逻辑一致。
4. 对话接口
对话接口把检索和生成整合在一起。它比检索接口复杂得多,因为涉及一个检索接口不需要考虑的问题:上下文窗口预算分配。
4.1 上下文窗口预算
这是 RAG 对话最容易踩的坑。模型的上下文窗口是有限的——GPT-4o 是 128K token,但大多数场景用不到这么多。问题不在于窗口够不够大,而在于如何把有限的空间分配给系统 Prompt、检索到的上下文、对话历史和用户输入。
一个常见的错误:把 topK 设为 10,每个 chunk 平均 500 token,加上系统 Prompt 和历史,轻松超出上下文限制。
typescript
// 上下文预算分配策略
function calculateContextBudget(maxTokens: number) {
// 系统 Prompt + 用户输入:固定预留
const systemAndUserTokens = 1500
// 检索上下文:占比 55%(可调)
const contextTokens = Math.floor(maxTokens * 0.55)
// 历史对话:剩余空间
const historyTokens = maxTokens - systemAndUserTokens - contextTokens
return { systemAndUserTokens, contextTokens, historyTokens }
}经验值:检索上下文占 55%、历史占 25%、系统 Prompt 和用户输入占 20%。这个比例适用于大多数 FAQ 和知识查询场景。如果你的场景涉及长文档分析,可以提高上下文占比到 70%,相应压缩历史。
4.2 对话实现
typescript
const chatSchema = z.object({
message: z.string().min(1).max(5000),
knowledgeBaseIds: z.array(z.string()).optional(),
topK: z.number().int().min(1).max(20).optional().default(5),
enableRerank: z.boolean().optional().default(true), // 对话默认开启 rerank
history: z.array(z.object({
role: z.enum(['user', 'assistant']),
content: z.string(),
})).optional().default([]),
})
ragApp.post('/chat', zValidator('json', chatSchema), async (c) => {
const user = c.get('user')
const params = c.req.valid('json')
// 1. 计算上下文预算
const budget = calculateContextBudget(8000) // 以 8K token 为例
// 2. 检索上下文
const queryVector = await embeddingService.embed(params.message)
const filter = buildSearchFilter(user, params)
let chunks = await vectorStore.search(
params.knowledgeBaseIds ?? user.accessibleKnowledgeBases,
queryVector,
{ topK: params.enableRerank ? params.topK * 5 : params.topK, filter }
)
if (params.enableRerank) {
chunks = await rerankService.rerank(params.message, chunks, {
topK: params.topK,
})
}
// 3. 按预算截断上下文
const context = contextBuilder.build(chunks, {
maxTokens: budget.contextTokens,
})
// 4. 按预算截断历史
const truncatedHistory = truncateHistory(
params.history,
budget.historyTokens
)
// 5. 组装消息
const messages: Message[] = [
{ role: 'system', content: buildSystemPrompt(context) },
...truncatedHistory,
{ role: 'user', content: params.message },
]
// 6. 调用 LLM
const response = await llm.chat({ messages, temperature: 0.3 })
// 7. 提取引用
const { cleanAnswer, citations } = extractCitations(response.content)
const references = buildReferences(citations, chunks)
return c.json({
answer: cleanAnswer,
citations,
references,
sources: chunks.map((c) => ({
id: c.id,
documentTitle: c.metadata.documentTitle,
sectionTitle: c.metadata.sectionTitle,
})),
})
})注意对话接口的 enableRerank 默认是 true——和检索接口相反。对话场景对回答质量要求更高,用户愿意多等 200-500ms 换来更好的排序。
4.3 历史截断
对话历史不能无限保留。简单的截断策略:从最近的对话开始往前保留,直到超出 token 预算。
typescript
function truncateHistory(
history: { role: string; content: string }[],
maxTokens: number
): Message[] {
const result: Message[] = []
let usedTokens = 0
// 从最近的消息开始,往前保留
for (let i = history.length - 1; i >= 0; i--) {
const msg = history[i]
const tokens = estimateTokens(msg.content)
if (usedTokens + tokens > maxTokens) break
result.unshift({
role: msg.role as 'user' | 'assistant',
content: msg.content,
})
usedTokens += tokens
}
return result
}这个策略有一个隐含特性:它优先保留最近的对话。对于多轮追问场景足够用。如果你的场景需要更长的上下文记忆,可以考虑摘要压缩——把早期对话总结成一段摘要塞进系统 Prompt。
4.4 系统 Prompt 设计
系统 Prompt 的质量直接影响回答质量。核心原则:明确约束模型行为,而不是泛泛地说「请好好回答」。
typescript
function buildSystemPrompt(context: string): string {
return `你是一个知识库助手。请根据以下参考资料回答用户的问题。
## 回答要求
1. 只基于参考资料回答,不要编造信息
2. 在每个事实陈述后面用 [编号] 标注来源
3. 如果参考资料中没有相关信息,明确说明「根据已有资料无法回答」
4. 保持回答简洁、准确
## 参考资料
${context}`
}第三条是关键:当知识库没有答案时,要求模型明确说出来而不是编造。这能有效减少幻觉,但代价是回答率会降低——用户可能得到「无法回答」。这是质量 vs 覆盖率的权衡,根据你的业务场景调整。
5. 流式对话接口
流式响应让用户体验显著提升——不再等待 5-30 秒的空白加载,而是先看到检索到的来源,再逐字看到回答生成。
实现流式响应的关键是选择合适的协议。SSE(Server-Sent Events)是最务实的选择:基于 HTTP、单向推送、浏览器原生支持。WebSocket 支持双向通信,但对 RAG 对话来说,用户发送消息是低频操作,不需要双向通道。
5.1 SSE 协议设计
流式响应使用 SSE 格式,包含三种事件类型:
data: {"type":"sources","sources":[...]} ← 先发送检索来源
data: {"type":"token","content":"根据"} ← 逐 token 发送回答
data: {"type":"token","content":"知识库"}
data: {"type":"citations","citations":[...]} ← 最后发送引用
data: [DONE] ← 结束标记先发送 sources 的价值:用户在前 1-3 秒就能看到检索到了哪些文档,而不是盯着一个空白页面等回答生成完。
5.2 流式实现
typescript
ragApp.post('/chat/stream', zValidator('json', chatSchema), async (c) => {
const user = c.get('user')
const params = c.req.valid('json')
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
const send = (data: unknown) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
)
}
try {
// 1. 检索
const queryVector = await embeddingService.embed(params.message)
const filter = buildSearchFilter(user, params)
let chunks = await vectorStore.search(
params.knowledgeBaseIds ?? user.accessibleKnowledgeBases,
queryVector,
{ topK: params.topK * 3, filter }
)
if (params.enableRerank) {
chunks = await rerankService.rerank(params.message, chunks, {
topK: params.topK,
})
}
// 2. 先发送来源信息
send({
type: 'sources',
sources: chunks.map((c) => ({
id: c.id,
documentTitle: c.metadata.documentTitle,
sectionTitle: c.metadata.sectionTitle,
score: c.score,
})),
})
// 3. 构建上下文和消息
const context = contextBuilder.build(chunks)
const messages: Message[] = [
{ role: 'system', content: buildSystemPrompt(context) },
...params.history.map((h) => ({
role: h.role as 'user' | 'assistant',
content: h.content,
})),
{ role: 'user', content: params.message },
]
// 4. 流式调用 LLM
const llmStream = await llm.stream({ messages, temperature: 0.3 })
let fullAnswer = ''
for await (const chunk of llmStream) {
const text = chunk.content ?? ''
fullAnswer += text
send({ type: 'token', content: text })
}
// 5. 发送引用信息
const { citations } = extractCitations(fullAnswer)
send({ type: 'citations', citations })
// 6. 结束
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
controller.close()
} catch (err) {
send({ type: 'error', error: String(err) })
controller.close()
}
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
})5.3 前端消费流式响应
SSE 的标准消费方式是通过 fetch + ReadableStream。注意 POST 请求不能用浏览器原生的 EventSource(只支持 GET),需要用 fetch 手动解析。
typescript
const response = await fetch('/api/rag/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: '退款多久到账?' }),
})
const reader = response.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value)
const lines = text.split('\n').filter((l) => l.startsWith('data: '))
for (const line of lines) {
const data = line.replace('data: ', '')
if (data === '[DONE]') continue
const event = JSON.parse(data)
switch (event.type) {
case 'sources':
renderSources(event.sources)
break
case 'token':
appendAnswer(event.content)
break
case 'citations':
renderCitations(event.citations)
break
case 'error':
showError(event.error)
break
}
}
}一个实际开发中需要注意的点:客户端可能在回答生成过程中关闭页面或取消请求。服务端应该在 send() 中检测连接状态,如果客户端已断开就提前终止 LLM 调用,避免浪费计算资源。
6. 错误处理和降级策略
RAG 接口比普通 API 多了一层复杂度:链路涉及四个外部服务——Embedding、向量数据库、Rerank、LLM,每个都可能超时或故障。降级策略不是笼统的「返回 500」,而是根据故障组件选择不同的降级路径。
6.1 降级路径设计
Embedding 故障 → 无法向量化查询 → 完全无法检索 → 返回 503
向量数据库故障 → 无法检索 → 返回友好提示(200),不生成回答
Rerank 故障 → 跳过 rerank,用原始向量检索结果继续
LLM 故障 → 检索成功但无法生成 → 返回检索结果,answer 为 null为什么向量数据库故障返回 200 而不是 500? 因为前端可以展示友好提示(「知识库暂时不可用」),而不是触发全局错误处理。对用户体验来说,一个带解释的空结果比一个红色错误弹窗好得多。
6.2 超时控制
RAG 链路上的每个外部调用都需要超时。不设超时意味着一个慢服务可能拖垮整个请求。
typescript
// 通用超时包装:超时后返回降级值而非抛异常
async function withTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number,
fallback: T
): Promise<T> {
return Promise.race([
fn(),
new Promise<T>((resolve) =>
setTimeout(() => resolve(fallback), timeoutMs)
),
])
}
// 使用示例
const chunks = await withTimeout(
() => vectorStore.search(queryVector, { topK: 20 }),
3000, // 3 秒超时
[] // 超时返回空数组
)各组件的推荐超时值(经验值,根据你的基础设施调整):
| 组件 | 推荐超时 | 超时后行为 |
|---|---|---|
| Embedding | 2s | 返回 503 |
| 向量检索 | 3s | 返回空结果 |
| Rerank | 2s | 跳过,用原始结果 |
| LLM 首 token | 10s | 返回超时错误 |
| LLM 完整响应 | 60s | 中断,返回已生成部分 |
6.3 统一错误中间件
typescript
// src/middleware/rag-error-handler.ts
import { createMiddleware } from 'hono/factory'
export const ragErrorHandler = createMiddleware(async (c, next) => {
try {
await next()
} catch (err) {
const error = err as Error
// 向量数据库不可用 → 返回友好提示
if (isVectorDBError(error)) {
return c.json({
answer: '抱歉,知识库暂时不可用,请稍后重试。',
sources: [],
error: 'Knowledge base temporarily unavailable',
}, 200)
}
// LLM 不可用 → 只返回检索结果
if (isLLMError(error)) {
return c.json({
answer: null,
sources: c.get('ragResults') ?? [],
error: 'Answer generation temporarily unavailable',
}, 200)
}
// Embedding 服务不可用 → 503
if (isEmbeddingError(error)) {
return c.json({ error: 'Embedding service unavailable' }, 503)
}
// 其他错误 → 500
return c.json({ error: 'Internal server error' }, 500)
}
})这个中间件覆盖三种最常见的故障场景。生产环境还需要补充:对错误分类统计(哪个组件故障率最高)、告警通知(连续失败 N 次触发告警)、以及限流保护(某个用户的异常请求不应影响其他用户)。
7. 接口聚合与生产化
7.1 路由挂载
把所有 RAG 相关的接口注册到主应用。中间件的顺序很重要——认证最先,权限其次,错误处理最后(兜底所有上游错误)。
typescript
// src/app.ts
import { Hono } from 'hono'
import { ragPermissionMiddleware } from './middleware/rag-permission'
import { ragErrorHandler } from './middleware/rag-error-handler'
import { documentsApp } from './routes/documents'
import { ragApp } from './routes/rag'
const app = new Hono()
// 全局中间件(按顺序执行)
app.use('/api/*', authMiddleware)
app.use('/api/rag/*', ragPermissionMiddleware)
app.use('/api/rag/*', ragErrorHandler)
// 路由挂载
app.route('/api/documents', documentsApp)
app.route('/api/rag', ragApp)
export default app7.2 示例可用 vs 生产可用
本章的代码是示例可用——核心逻辑完整,但生产部署还需要补充以下能力:
| 生产化需求 | 说明 | 优先级 |
|---|---|---|
| 限流 | 上传和搜索接口需要限流,防止滥用 | 高 |
| 结构化日志 | 每个 RAG 链路的耗时、token 消耗、失败原因 | 高 |
| 缓存 | 高频相同查询缓存检索结果(注意缓存失效策略) | 中 |
| 熔断器 | 外部服务连续失败时快速失败,不反复等待超时 | 中 |
| 请求取消 | 客户端断开后及时中止 LLM 调用 | 中 |
| 审计日志 | 谁在什么时候搜了什么、得到了什么结果 | 低 |
限流的实现取决于你的部署环境。Cloudflare Workers 用 KV 或 Durable Objects 做滑动窗口;Node.js 环境用 rate-limiter-flexible。关键原则是区分接口——上传接口按次数限,搜索接口按并发限,对话接口按 token 消耗限。
到此,RAG 的接口蓝图已经完整:五类端点、同步与流式并存、异步文档处理、上下文预算分配、四级降级策略。
这些设计决策不是唯一的,但每一个都对应了具体的冲突和权衡。你可以根据自己的业务场景调整参数默认值、降级策略和上下文预算比例。
下一章用 Cloudflare Vectorize + Workers AI 把这套接口落地成一个最小可运行的 RAG 系统——从设计到实现,走完最后一步。