重排
POST /v1/rerank —— 给一个检索式和一批候选文档打相关性分并排序。
什么时候需要重排#
典型场景是检索增强生成(RAG)。向量检索能从十万篇文档中快速筛出五十篇「大致相关」的结果,但它比较的是向量距离,精度有限;重排模型会将检索式与每篇文档成对读取后再打分,精度显著更高,代价是耗时与成本也显著更高。
因此标准做法是两段式:先用向量化粗筛出数十篇,再用重排从中选出最相关的三至五篇提供给模型。直接对全量文档做重排,在耗时与成本上都不可行。
与向量化的关键区别在于:向量化对每段文本单独编码,结果可以缓存复用;重排的输入是「检索式与文档」这一组合,更换检索式即需重新计算,不存在可缓存的中间产物。
一次完整调用#
curl:三篇候选,只要最相关的两篇
bash
curl https://ogrouter.ai/v1/rerank \ -H "Authorization: Bearer $AIROUTER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "cohere/rerank-v3.5", "query": "如何申请退款", "documents": [ "我们的办公地址位于香港中环。", "订单在支付后 7 天内可以申请全额退款,请在订单详情页点击退款。", "退款到账时间取决于发卡行,通常为 3 到 5 个工作日。" ], "top_n": 2 }'响应:按分数降序,index 指回你传进来的下标
json
{ "model": "cohere/rerank-v3.5", "results": [ { "index": 1, "relevance_score": 0.981 }, { "index": 2, "relevance_score": 0.774 } ], "usage": { "prompt_tokens": 96, "total_tokens": 96 }}index 是原始下标,不是名次
results[0].index 等于 1,意思是「你传入的第 2 篇(下标 1)最相关」。名次即数组顺序本身。可直接用 documents[r.index] 映射回自己的数据,无需回传文档原文。请求字段#
modelstring必填- 重排模型名。规则与对话端点一致,见模型与路由。
querystring必填- 检索式,至少一个字符。
documentsarray必填- 候选文档,1 到 1000 篇。每项可以是字符串,也可以是带
text字段的对象;对象形态的原始 JSON 会被完整保留,return_documents为真时原样回带。只有text会发送给上游。 top_ninteger- 只返回前 N 条。不传表示全部返回;超过文档数时自动钳到文档数。传入负数返回
invalid_request,不会当作 0 处理。 return_documentsboolean- 默认
false。为真时在每条结果中带回文档原文。平台回带的是请求中提交的那一份,而非上游回显的内容——详见下一节。
能力边界#
documents单次最多 1000 篇;字符串或{"text": ...}对象均可。- 只有
text会发给上游;对象上的其它字段靠return_documents由平台回带。 top_n出站前钳到文档数,响应前平台再截一次,避免「按 top_n 收费却回全量」。- 计费按输入 token:
query只算一次 + 全部documents文本。
两处与直觉不符的设计#
以下两点若不了解,容易写出表面正确、实际有误的代码。
排序与 top_n 截断都在平台完成。上游返回的顺序不保证,拿未排序的结果直接截断会丢掉最相关的文档。
接进检索流程#
Python:粗筛加精排的两段式
python
import os, requests
API = "https://ogrouter.ai/v1"HEADERS = {"Authorization": f"Bearer {os.environ['AIROUTER_API_KEY']}"}
def rerank(query, docs, top_n=3): body = { "model": "cohere/rerank-v3.5", "query": query, "documents": docs, "top_n": top_n, } r = requests.post(API + "/rerank", json=body, headers=HEADERS, timeout=60) r.raise_for_status() # index 指回 docs 的下标,直接拿它取回原文 return [docs[item["index"]] for item in r.json()["results"]]
candidates = vector_search(question, limit=50) # 你自己的向量检索context = rerank(question, candidates, top_n=3) # 精排后只留 3 篇Node.js:对象形态的文档,原样拿回自己的字段
javascript
const res = await fetch('https://ogrouter.ai/v1/rerank', { method: 'POST', headers: { Authorization: `Bearer ${process.env.AIROUTER_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'cohere/rerank-v3.5', query: '如何申请退款', // 带上你自己的字段:只有 text 会发给上游,其余原样保留 documents: [ { id: 'kb-17', text: '订单在支付后 7 天内可以申请全额退款。', url: '/help/refund' }, { id: 'kb-42', text: '我们的办公地址位于香港中环。', url: '/help/contact' }, ], return_documents: true, top_n: 1, }),});
const { results } = await res.json();console.log(results[0].document.id); // kb-17,你传进去的字段还在Java:粗筛后精排,index 指回原始下标
java
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.util.List;import com.fasterxml.jackson.databind.JsonNode;import com.fasterxml.jackson.databind.ObjectMapper;
// JSON 解析用 Jackson(com.fasterxml.jackson.core:jackson-databind)ObjectMapper mapper = new ObjectMapper();List<String> documents = List.of("我们的办公地址位于香港中环。", "订单在支付后 7 天内可以申请全额退款。");
String body = mapper.writeValueAsString(java.util.Map.of( "model", "cohere/rerank-v3.5", "query", "如何申请退款", "documents", documents, "top_n", 1));
HttpResponse<String> response = HttpClient.newHttpClient().send( HttpRequest.newBuilder(URI.create("https://ogrouter.ai/v1/rerank")) .header("Authorization", "Bearer " + System.getenv("AIROUTER_API_KEY")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(), HttpResponse.BodyHandlers.ofString());
// index 指回 documents 的下标,直接拿它取回原文for (JsonNode item : mapper.readTree(response.body()).get("results")) { System.out.println(documents.get(item.get("index").asInt()));}计费口径#
只按输入计费,没有输出那一半——重排的产物是分数,不是文本。计费的 token 数是检索式加上全部文档的文本量。
检索式只算一次
模型内部会将检索式与每篇文档配对,但平台按一次计量。1 个检索式配 50 篇文档,计量的是「检索式加 50 篇文档」的总量,而非 50 遍检索式。
这意味着成本几乎完全由文档总长度决定。在粗筛阶段减少候选数量,或将长文档切分变短,比更换模型更能有效降低成本。完整规则见 计费口径。