在 OpenRouter 上做嵌入与重排:RAG 那一段怎么拼
做 RAG 最容易翻车的地方,往往不是”要不要上重排”这种路线问题,而是三段接口拼接时字段对不上:嵌入接口返回的数组该按顺序取还是按 index 取、重排接口收的 documents 到底是字符串还是对象、重排返回的结果直接丢给聊天模型时该取哪一层。这篇只做一件事——把 OpenRouter 官方文档 openrouter.ai/docs/api_reference/embeddings 和 openrouter.ai/docs/cookbook/evaluate-and-optimize/rag 这两页里写明的字段与拼接顺序对齐一遍,顺便指出这两页示例代码之间对不上的两处。
一、它解决的是哪一段的问题
如果你已经在用 OpenRouter 发 chat completions,那么 RAG 链路里缺的是前面两段:把文档和问题变成向量,以及把召回结果重新打一遍分。官方文档在 RAG 那一页把这条链路写成四步:Index(切块并嵌入)→ Retrieve(嵌入查询并找相似块)→ Rerank(可选,用 cross-encoder 重新打分)→ Generate(把选中的文档作为上下文交给聊天模型)。文档明确把第三步标为 optional,这一点后面还要说。
三段各有各的端点,都挂在同一个 base 下:
| 用途 | 端点 | 方法 |
|---|---|---|
| 生成嵌入 | https://openrouter.ai/api/v1/embeddings | POST |
| 列出可用嵌入模型 | https://openrouter.ai/api/v1/embeddings/models | GET |
| 重排 | https://openrouter.ai/api/v1/rerank | POST |
| 生成回答 | https://openrouter.ai/api/v1/chat/completions | POST |
RAG 那一页开门见山的说法是:嵌入、重排、聊天补全这三块积木都由同一套 API 提供。落到写代码上,就是这三段共用同一个 base 和同一套 Authorization / Content-Type 请求头,不必为向量这一段再单独接一家服务商的 SDK。
二、前置条件
这一段最容易被跳过,但它决定了你第一次请求是 200 还是 401。
鉴权与请求头。 文档里所有示例都是两个头:Authorization: Bearer <你的 key> 和 Content-Type: application/json。文档正文里的密钥占位写作 <OPENROUTER_API_KEY>,shell 示例里用的是环境变量 $OPENROUTER_API_KEY。写进代码库时按你自己的密钥管理方式来,不要把值直接写进仓库。
能力前提。 嵌入这一段需要的是一个输出模态为 embeddings 的模型,重排这一段需要的是输出模态为 rerank 的模型——这两类模型和你平时调的聊天模型不是同一批,写错模型名时文档给的是 404(下面错误码那一节会讲)。平台上具体有哪些嵌入与重排模型随时在变,文档给的自查方式是调 GET /api/v1/embeddings/models,或者去官网模型页按输出模态筛。本文不列任何模型清单。
SDK 还是裸 HTTP。 文档的代码组里给了 TypeScript SDK(@openrouter/sdk,用 openRouter.embeddings.generate(...) 和 openRouter.embeddings.listModels())、Python(requests)、TypeScript 的 fetch,以及 shell 的 curl 四种写法。RAG 那一页只给了 Python、TypeScript fetch 和 shell 三种。
Windows 侧要注意的一点。 官方文档的 shell 示例是 POSIX shell 写法:JSON 体用单引号整段包住,密钥用 $OPENROUTER_API_KEY 展开。在 Windows 的 PowerShell 里,单引号不做变量展开、curl 默认还是 Invoke-WebRequest 的别名,这段命令不能照抄就跑——官方文档没有给 Windows 侧的等价写法。这属于通用的命令行差异,不是 OpenRouter 官方文档的内容;实际做法上,在 Windows 上更省事的路径是直接用文档里的 Python 或 TypeScript 示例,或者在 WSL / Git Bash 里跑那段 curl。
三、四段的字段与拼接顺序
Index:input 接单条也接数组
最基础的请求体只有两个字段,model 和 input:
curl https://openrouter.ai/api/v1/embeddings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-d '{
"model": "openai/text-embedding-3-small",
"input": "The quick brown fox jumps over the lazy dog"
}'
input 传字符串数组就是批量。文档在《Embeddings》和 RAG 两页都把”批量发一次而不是循环发多次”列为 best practice,理由文档自述是减少延迟与开销。
响应结构是 data 数组,每项带 embedding(向量)。这里有本文要指出的第一处不一致:《Embeddings》那一页的示例是按位置取的(data["data"][0]["embedding"],语义搜索例子里更是直接 response.data.slice(1) 拿文档部分);而 RAG 那一页在建索引时是按字段取的:
document_embeddings = [
{"text": chunks[item["index"]], "embedding": item["embedding"]}
for item in data["data"]
]
同一个响应,两页用了两种取法。按 item["index"] 回指原始 chunks 显然更稳,因为它不依赖返回顺序;但要注意文档并没有在任何一处写明”响应顺序保证与输入顺序一致”——官方文档没有说明这一点,所以按 index 对齐是文档里就有依据的写法,别自己想当然。
多模态输入:嵌入和重排的形状不一样
《Embeddings》页写明”部分嵌入模型支持图像输入”。它的多模态形状是:input 里放对象,对象里有 content 数组,数组项是 {"type": "image_url", "image_url": {"url": ...}} 或 {"type": "text", "text": ...},两者可以放在同一个 content 里生成联合嵌入,示例还额外带了 "encoding_format": "float"。
而 RAG 页里的多模态重排是另一套形状:documents 里放的是扁平对象,字段就是可选的 text 和可选的 image,文档写明 image 的值可以是 http/https 远程 URL,也可以是 base64 的 data URI(data:image/...),并且每个文档至少要有 text 或 image 之一。
"documents": [
{"text": "AI enables robots to perceive, plan, and act autonomously."},
{"image": "https://example.com/robot-diagram.png"},
{
"text": "A robotic arm assembling components.",
"image": "https://example.com/robot-arm.png",
},
],
嵌入侧用 content 数组套 image_url 对象,重排侧用扁平的 text / image 字符串字段——同一条链路上两个接口的多模态写法不通用,把一边的形状抄到另一边去是这两页里最容易犯的错。文档在嵌入侧给 400 的定义就是「输入格式非法或缺必填参数」,形状抄串了首先要往这个方向查;重排接口的错误码文档在这两页里没有单独列,官方文档没有说明这一点。另外文档在提示框里写明:纯文本文档仍然可以直接传字符串,也就是说同一个 documents 数组里可以混着放 "一段纯字符串" 和 { "image": "..." }。
Retrieve:查询和文档拼在同一次请求里
RAG 页的检索这一步是把查询单独嵌入一次,再和已存的文档向量算余弦相似度。《Embeddings》页那个语义搜索例子则更省一次请求:把查询放在数组第一位,文档接在后面(input: [query, ...documents]),响应里 data[0] 就是查询向量,其余是文档向量。这个顺序是人为约定的,不是接口语义——你要是把查询放最后,取值那两行也得跟着改。
至于比较方法,文档的建议是用余弦相似度而不是欧氏距离,理由文档自述是余弦相似度对尺度不敏感、在高维向量上表现更好。
Rerank:documents 进,results 出
重排请求体四个字段:model、query、documents、top_n。
def rerank(query, documents, top_n=3):
response = requests.post(
"https://openrouter.ai/api/v1/rerank",
headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": "cohere/rerank-v3.5",
"query": query,
"documents": documents,
"top_n": top_n,
},
)
data = response.json()
return data["results"]
返回的是 results。它每项有哪些字段,文档在 TypeScript 版本里写得最全,标注的类型是 { index: number; relevance_score: number; document: { text: string } }。三个字段各有用处:relevance_score 是重排后的分数,index 回指你传进去的 documents 数组下标(多模态那个例子打印的就是 relevance_score 加 index,压根没碰 document.text——因为那批文档里有的只有图没有文),document.text 是文本本身。
上面代码里的 openai/text-embedding-3-small、cohere/rerank-v3.5 都是官方文档当时用的示例值,平台上有哪些模型随时在变,别当清单用。以上片段按官方文档中的参数语义组合,未经实测,以官方文档最新内容为准。
Generate:拼上下文时取哪一层
这是本文要指出的第二处不一致,也是最容易踩的一处。RAG 页 Step 4 的 generate_answer 直接吃 rerank 的原始结果,拼接时要往下钻两层:
context = "\n\n".join(
f"[{i+1}] {doc['document']['text']}"
for i, doc in enumerate(context_docs)
)
但同一页的完整示例里,generate 收的已经是抽好的字符串列表(调用前先做了 context_texts = [r["document"]["text"] for r in reranked]),拼接写的是 f"[{i+1}] {doc}"。两个函数同名不同签名,连中间那个 retrieve 的 top_n 默认值也不一样。照着页面往下抄、抄串了行,报错会出在字符串拼接这一步而不是 API 调用上,排查时容易找错方向。
拼接顺序本身两处是一致的:每条上下文前面加 [n] 序号、条与条之间用空行(\n\n)分隔,然后 system 提示要求模型基于给定上下文回答并用方括号标注来源、上下文不足时直说,user 消息则是 Context:\n{context}\n\nQuestion: {query} 这个顺序——上下文在前、问题在后。文档另外建议把来源元数据(标题、章节、URL)一并放进上下文,好让模型能给出可核对的引用。
四、边界:文档明说不支持与没说的部分
- 嵌入不支持流式。文档在 Limitations 里写明:不同于 chat completions,嵌入是一次性返回完整响应,不支持 streaming。
- 嵌入输出是确定性的。同一段文本每次返回的向量相同,没有 temperature 之类的随机性。文档据此建议把嵌入结果缓存下来。
- 有最大输入长度。文档写明每个模型都有输入长度上限,超出的文本会被截断或拒绝,需要先切块。具体数值随模型而异,本文不写。
- 语言支持看模型。文档写明部分模型是针对特定语言优化的,要看模型自身说明。
- 重排是可选步骤。文档把它列为 optional,并给了取舍:知识库大、召回噪声多、精度比延迟更重要时值得加;知识库小、要最低延迟、或者只是搭原型时可以跳过——因为它多一次 API 调用。
- 多模态是”部分模型”的能力。嵌入与重排两侧文档用的都是 “Some models” 的措辞,并没有在这两页给出支持清单,只指向官网模型页按输出模态筛。
- 换模型就得重建索引。文档把”索引和查询用同一个嵌入模型”列为 best practice,写明混用会产生不兼容的向量空间、检索结果会变差。
- 这两页没有标 beta / preview / deprecated。我们在这两页的落盘文本里没有检索到这类标记,但这不等于其中每个字段都长期稳定,仍以官方文档最新内容为准。
五、怎么验证拼对了
分层验证,别等到最后看回答质量再回头猜。
第一层,链路通不通。 先发一条最短的单字符串嵌入请求。文档列出的错误码含义是现成的诊断表:
| 状态码 | 文档给的含义 |
|---|---|
| 400 | 输入格式非法或缺必填参数,检查 input 与 model |
| 401 | 密钥无效或缺失,检查 Authorization 头 |
| 402 | 账户额度不足 |
| 404 | 模型不存在,或该模型不是嵌入模型 |
| 429 | 触发限流,文档建议做指数退避重试 |
| 529 | 上游供应商过载,文档建议开 allow_fallbacks: true 走备选 |
404 这一条尤其值得记住:拿聊天模型去打 /embeddings,症状就是 404 而不是 400。
第二层,索引对没对齐。 检查 data 数组长度与你送进去的块数一致,再抽一条按 item["index"] 回指原文看看文本对得上。文档示例里打印的是向量维度(len(embedding)),批量时打印每一项的维度也能顺手发现有没有哪条返回异常。
第三层,重排接上没有。 看 results 的条数是否不超过你给的 top_n,再用 index 回指原 documents 数组核对一下——index 对得上,说明你传进去的顺序和取回来的映射没错位。顺带一句:文档的示例是直接按 results 的返回顺序往下打印的,但它并没有写明 results 一定按 relevance_score 降序返回——要靠顺序取「最相关那条」的话,自己按分数排一遍更稳。文档还建议设一个相关性阈值,把分数过低的文档滤掉,免得把不相干的内容塞进上下文。
第四层,上下文真的进 prompt 了。 在发 chat completions 之前把拼好的 context 字符串打出来看一眼:序号是不是 [1] 起、条与条之间是不是空行、有没有出现 {'document': {'text': ...}} 这种没钻到底层的字典字面量。上面说的那处签名不一致,症状通常就在这里现形。
顺带一个可用的自检技巧:嵌入是确定性的,同一段文本发两次应该拿到同样的向量。这条来自文档的 Limitations 一节,可以拿来确认你的缓存层没有把不同文本的向量串错。
嵌入与重排这两段接口本身不复杂,真正花时间的是 data / results / document.text 这几层壳,以及两页示例之间的写法差异。把上面这几处字段和顺序钉死,剩下的调优就回到切块策略和召回数量上了——那属于另一个话题。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。