检索增强生成(RAG)的思路很早就在问答系统中出现,但直到大语言模型普遍暴露知识截止和编造问题后,它才成为生产环境的关键组件。Command R+ 具备长上下文与工具调用能力,单独使用已经能处理不少任务;一旦把企业文档、接口手册或内部知识库接入,回答质量会有明显提升。本文基于 Node.js 实现一个可运行的 RAG 服务,重点拆解嵌入、检索、提示词拼接和流式输出四个环节。

Command R+ 单独使用时的问题
Command R+ 是 Cohere 面向复杂推理和工具调用场景发布的对话模型,上下文窗口较大,也支持多步工具调用。但它和所有大模型一样,参数中保存的是训练阶段的知识,无法直接回答企业内部最新的政策、代码库或业务数据。直接把这些私有内容塞进长上下文虽然可行,但成本高、延迟大,而且无关信息会稀释模型注意力。检索增强的做法是只把与问题最相关的片段作为上下文,这样既控制了 token 消耗,也让回答有明确依据。
在 RAG 链路里,Command R+ 的角色是生成器,而不是检索器。真正决定答案上限的是前序检索质量:如果检索返回的片段不相关,再强的生成模型也只能基于错误材料作答。因此实现时要把精力放在文档切分、嵌入一致性和相似度过滤上,而不是只关心 prompt 写法。下面开始在 Node.js 中搭建这套流程。
构建嵌入与向量存储
Node.js 环境下处理嵌入请求并不复杂,Node 18 以上原生支持 fetch,可以直接调用 Cohere 的 Embed API。安装依赖只需要 dotenv 用于读取环境变量。嵌入模型建议固定为 embed-v4.0,它返回的向量维度一致,避免后续相似度计算出现维度错误。
文档切块是影响检索效果的关键步骤。chunk 太大会让一个片段包含多个主题,降低定位精度;chunk 太小又可能切断完整语义。一般可以从 400 到 800 个字符开始测试,并设置 10% 到 20% 的重叠,让相邻片段保留上下文。下面的函数按字符数切分,生产环境建议改成按句子边界切分。
function chunkText(text, chunkSize = 600, overlap = 80) {
const chunks = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length);
chunks.push(text.slice(start, end));
start = end - overlap;
}
return chunks;
}
嵌入函数需要区分索引和查询两种输入类型。Cohere Embed API 支持 search_document 和 search_query 两种 input_type,混用会导致向量空间偏移。给文档向量标记为 search_document,查询向量标记为 search_query,这样相似度排序更稳定。
async function embed(texts, inputType = 'search_document') {
const response = await fetch('https://api.cohere.com/v2/embed', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.COHERE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'embed-v4.0',
texts,
input_type: inputType
})
});
if (!response.ok) {
throw new Error(`Embed request failed: ${response.status}`);
}
const data = await response.json();
return data.embeddings;
}
向量存储可以用内存数组起步,不必一开始就引入 Pinecone 或 Weaviate。将每个文档块连同向量保存,检索时逐个计算余弦相似度,再按分数降序取前 K 个。数据量在几千条以内时,这种方案足够验证效果;规模上来后再切换向量数据库,检索接口不需要改动。
检索与 Command R+ 生成接口
查询到达服务端后,先用相同嵌入函数把问题转成向量,再与库中的文档向量计算相似度。为了减少低质量上下文进入 prompt,可以设置一个阈值,例如 0.35。只有超过阈值的片段才会传给模型;如果全部低于阈值,就明确告诉用户当前知识库中没有找到相关资料,避免模型自由发挥。
function cosineSimilarity(a, b) {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
function search(store, queryEmbedding, topK = 5, threshold = 0.35) {
return store
.map((item) => ({
...item,
score: cosineSimilarity(queryEmbedding, item.embedding)
}))
.filter((item) => item.score >= threshold)
.sort((a, b) => b.score - a.score)
.slice(0, topK);
}
提示词构造要把检索结果放在清晰的区域,并加入编号,让模型知道每段资料的边界。不要只把片段拼成一大段,那样模型容易混淆来源。可以要求模型只依据资料回答,找不到时直接说明,这样能进一步抑制幻觉。生成请求使用 command-r-plus 模型,开启流式输出可以降低首字延迟。
async function generateAnswer(chunks, question) {
const context = chunks
.map((c, i) => `[${i + 1}] ${c.text}`)
.join('\n');
const response = await fetch('https://api.cohere.com/v2/chat', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.COHERE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'command-r-plus',
messages: [
{
role: 'system',
content: '你是一个基于给定资料回答问题的助手。如果资料中没有答案,请直接说明。'
},
{
role: 'user',
content: `资料:\n${context}\n\n问题:${question}`
}
],
stream: true
})
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (payload === '[DONE]') continue;
const json = JSON.parse(payload);
const delta = json.delta?.message?.content?.text ?? '';
if (delta) process.stdout.write(delta);
}
}
}
上面的流式解析按行切割 SSE 数据,每次读取到 data: 后提取 JSON 中的增量文本。注意 Cohere 的流式响应字段层级与 OpenAI 不同,需要按 delta.message.content.text 取值,字段缺失时用空字符串兜底。可以将这个函数封装到 HTTP 服务中,在请求体里接收 question 和可选 topK,返回 text/event-stream 或直接写入响应流。
提升检索质量与工程化建议
单纯按字符切分和余弦相似度能跑通,但实际文档往往包含表格、代码片段和标题层级。建议在切分前先做轻量清洗,比如移除多余换行、保留标题与正文的关联。对于长文档,可以先生成摘要或提取章节标题,再与片段拼接,让检索向量携带更多语义信息。
相似度阈值不宜设得过高。实践中,不同嵌入模型的分值分布不同,固定阈值容易误杀相关片段。可以先在测试集上观察正负样本的分数区间,再确定阈值。更稳妥的方式是引入重排序模型,先召回 20 到 50 条候选,再用 rerank 模型精排前 5 条,这样能明显改善长尾问题。
工程化时还要考虑缓存和观测。对相同问题的嵌入结果做缓存,能显著降低重复请求成本。日志中记录每个查询的 topK 分数和返回片段 ID,方便定位检索失败的原因。Command R+ 支持工具调用,如果知识库没有命中,可以让模型触发搜索工具或改写问题,而不是直接生成猜测性回答。
Node.jsCommand R+检索增强生成修改时间:2026-09-21 16:22:26