RAG 检索为什么找不准?用 JavaScript 从零实现中文 BM25 检索

举报
yd_241537900 发表于 2026/10/10 00:28:14 2026/10/10
【摘要】 RAG 知识库的效果与文档检索质量密切相关。本文使用原生 JavaScript 实现一个轻量级中文 BM25 检索器,通过字符二元组分词、词频统计和相关性评分,展示文档检索与排序的基本原理,帮助开发者理解 RAG 系统中的关键检索技术。

一、前言

随着大模型应用的发展,RAG(检索增强生成)已经成为构建智能知识库的常见方案。

但在实际开发中,我们经常遇到一个问题:知识库里明明存在相关文档,检索系统却没有把它排在前面。

这说明,RAG 的效果不仅取决于大模型,还与文档检索和相关性排序密切相关。

本文不依赖向量数据库,也不需要调用大模型 API,而是通过 JavaScript 实现一个轻量级 BM25 检索器,了解知识库检索背后的基本原理。

二、BM25 是什么?

BM25 是一种经典的信息检索排序算法,主要根据以下因素计算文档与查询的相关性:

  • 词频(TF): 查询词在文档中出现的次数。

  • 逆文档频率(IDF): 查询词在整个文档集合中的区分能力。

  • 文档长度: 对不同长度的文档进行归一化处理。

简单来说,BM25 不只是判断某个关键词是否出现,还会综合考虑它的出现频率与文档长度,从而计算相关性得分。

在中文场景下,由于文本没有天然的空格分词,我们可以先采用字符二元组(Bigram)进行简化处理。

例如:

输入:中文检索

分词结果:
["中文", "文检", "检索"]

这种方式不需要额外安装中文分词库,适合学习和简单的技术演示。

三、JavaScript 实战

下面使用原生 JavaScript 构建一个包含四篇技术文档的小型知识库,并实现 BM25 检索。

运行环境:Node.js 18 及以上版本,无需安装第三方依赖。

完整代码

const docs = [
  {
    title: "RAG 知识库基础",
    text: "RAG 通过检索知识库文档为大模型提供参考内容"
  },
  {
    title: "BM25 排序实践",
    text: "BM25 根据词频和文档长度计算关键词相关性"
  },
  {
    title: "AI Agent 安全控制",
    text: "AI Agent 工具调用需要重复检测和次数限制"
  },
  {
    title: "中文检索方法",
    text: "中文知识库检索可以使用字符二元组与 BM25 排序"
  }
];

// 中文字符二元组分词
function tokenize(text) {
  const blocks = text.toLowerCase()
    .match(/[\u4e00-\u9fff]+|[a-z0-9]+/g) || [];

  return blocks.flatMap(w => {
    if (!/[\u4e00-\u9fff]/.test(w)) return [w];
    return w.length < 2 ? [w] :
      Array.from(
        { length: w.length - 1 },
        (_, i) => w.slice(i, i + 2)
      );
  });
}

// 建立文档词频统计
const tokens = docs.map(d => tokenize(d.text));

const counts = tokens.map(ts => {
  const m = {};
  for (const t of ts) {
    m[t] = (m[t] || 0) + 1;
  }
  return m;
});

const avgLen = tokens.reduce(
  (s, ts) => s + ts.length, 0
) / docs.length;

// BM25 评分函数
function bm25(query, i) {
  let score = 0;
  const k1 = 1.5;
  const b = 0.75;
  const N = docs.length;

  for (const term of new Set(tokenize(query))) {
    const freq = counts[i][term] || 0;
    if (!freq) continue;

    const df = counts.filter(
      c => c[term] > 0
    ).length;

    const idf = Math.log(
      1 + (N - df + 0.5) / (df + 0.5)
    );

    score += idf * (freq * (k1 + 1)) /
      (freq + k1 * (
        1 - b + b * tokens[i].length / avgLen
      ));
  }

  return score;
}

// 执行检索并排序
const query = "中文知识库 BM25 检索";

const results = docs.map((d, i) => ({
  title: d.title,
  score: bm25(query, i)
}))
.filter(r => r.score > 0)
.sort((a, b) => b.score - a.score);

console.table(results.map(r => ({
  title: r.title,
  score: r.score.toFixed(3)
})));

四、运行结果

将代码保存为 bm25.js,执行:

node bm25.js

本示例的实际运行结果如下:

排名 文档标题 BM25 得分
1 中文检索方法 5.115
2 RAG 知识库基础 2.002
3 BM25 排序实践 0.702

可以看到,包含多个查询关键词的「中文检索方法」获得了最高的相关性得分。

而「AI Agent 安全控制」没有匹配到查询中的关键词,因此不会出现在结果中。

需要注意的是,BM25 得分并不是匹配概率,而是用于当前查询下文档排序的相关性指标。

五、BM25 的局限性

虽然 BM25 简单高效,但它主要依赖词项匹配,因此存在一些不足。

例如,当用户搜索「如何降低接口响应时间」时,如果文档使用的是「API 性能优化」等不同表达,关键词检索可能无法充分识别它们在语义上的关联。

实际 RAG 系统可以结合向量检索与 BM25,构建混合检索方案,提高文档召回的覆盖范围。

此外,字符二元组只是一个简化方案,实际项目中还可以引入更完善的中文分词、停用词处理与查询扩展机制。

六、总结

本文通过原生 JavaScript 实现了一个简化的中文 BM25 检索器,完成了分词、词频统计、相关性评分与结果排序。

整个示例无需复杂框架,也不依赖外部 API,适合初学者理解 RAG 系统中的检索环节。

RAG 不仅要让大模型能够回答问题,更需要在回答之前找到真正相关的知识。

需要说明的是,本文实现的是 RAG 的检索组件,而非包含大模型生成能力的完整 RAG 系统。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。