咱 web 端也能跑本地知识库,RAG(傲娇)-篇章 2-数据预处理与匹配结果优化 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
doujiangjiyaozha
V2EX    程序员

咱 web 端也能跑本地知识库,RAG(傲娇)-篇章 2-数据预处理与匹配结果优化

  •  
  •   doujiangjiyaozha 146 天前 957 次点击
    这是一个创建于 146 天前的主题,其中的信息可能已经有所发展或是发生改变。

    众所周知,RAG 最基本的流程是:

    数据处理 → 向量化 → 存储 → 匹配文本 → 结果优化 → 最终的匹配结果

    其中,数据预处理与匹配结果优化尤为重要。

    1. 数据预处理:文本分块( Chunking )

    在处理一篇长篇幅的文章时,通常需要将整个文本切分为多个小块,每个小块分别向量化后再存储。

    拆分块的重要性

    如果分块方式不合理,可能会导致无法命中真正相关的内容。因此,合理的分块策略至关重要。

    如何拆分?

    看这里:

    Text Splitters Overview - LangChain

    • 文章类型数据:推荐使用 Text-structured
    • HTML 等结构化标签类型:推荐使用 Document-structured

    2. 弥补 RAG 匹配缺陷:大小块 + 关键词索引

    即使文本被拆成多个块,匹配结果依旧可能不准确。因为 RAG 本身存在局限性:若问题与任何文本块都不相关,匹配效果自然不佳。

    优化方案:参考 Danswer 架构

    简单来说,就是将文本拆分成不同大小的块再配上关键词索引

    • 大块文本:提高语义层面的相关性
    • 小块文本 + 关键词索引:提高细节命中率

    小块可以提供更多细节,但也可能带来噪音信息。


    3. 匹配结果优化

    多维度匹配后可能得到大量候选文本,因此需要做进一步排序:

    1. 基于向量匹配与关键词匹配的分数加权排序
    2. 使用轻量级的 rerank 模型
    3. 最终形成一组合理的匹配文本

    4. Web 端落地(纯浏览器端)

    文本块处理

    import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters" const getBaseTextRecursiveSplitter = () => { const bigSplitter = new RecursiveCharacterTextSplitter({ chunkSize: SPLITTER_BIG_CHUNK_SIZE, chunkOverlap: SPLITTER_BIG_CHUNK_OVERLAP, separators: SPLITTER_SEPARATORS }); const miniSplitter = new RecursiveCharacterTextSplitter({ chunkSize: SPLITTER_MINI_CHUNK_SIZE, chunkOverlap: SPLITTER_MINI_CHUNK_OVERLAP, separators: SPLITTER_SEPARATORS }); return { bigSplitter, miniSplitter } } 

    推荐参数配置

    // split chunk relate export const SPLITTER_BIG_CHUNK_SIZE = 1000; // 大块字符数 export const SPLITTER_BIG_CHUNK_OVERLAP = 200; // 大块重叠字符数 export const SPLITTER_MINI_CHUNK_SIZE = 150; // 小块字符数 export const SPLITTER_MINI_CHUNK_OVERLAP = 30; // 小块重叠字符数 export const SPLITTER_SEPARATORS = ["\n\n", "\n", "。", ";", ",", " ", ""] 

    关键词索引

    使用 lunr.js + jieba(处理中文更优)


    匹配结果的优化

    考虑到运行在客户端的性能问题没使用 rerank 模型,仅通过加权与归一化排序处理并优化了一点打分逻辑

    参考代码如下:

    let [lshRes, fullIndexResFromDB] = await Promise.all([ searchLshIndex(), searchFullTextIndex(), ]) as [Search.LshItemRes[], lunr.Index.Result[]] // 向量匹配排序 const sortedLshRes = lshRes.sort((a, b) => b.similarity - a.similarity) .slice(0, config.SEARCH_RESULT_HEADER_SLICE_SIZE) // 全文匹配排序 const sortedFullIndexResFromDB = fullIndexResFromDB.sort((a, b) => b.score - a.score) .slice(0, config.SEARCH_RESULT_HEADER_SLICE_SIZE) // 重新打分、归一化 await FullTextIndex.loadJieBa() const fullIndexFromDBTextChunkRes = await store.getBatch({ storeName: constant.TEXT_CHUNK_STORE_NAME, keys: sortedFullIndexResFromDB.map((item) => Number(item.ref)) }) FullTextIndex.add([{ field: 'text' }], fullIndexFromDBTextChunkRes.map(item => ({ id: item.id, text: item.text }))) let newFullIndexRes = FullTextIndex.search(question) newFullIndexRes = newFullIndexRes.sort((a, b) => b.score - a.score) const maxScore = newFullIndexRes[0]?.score || 1 const reRankFullIndexRes = newFullIndexRes.map(item => ({ ...item, score: item.score / maxScore })) // 合并向量和关键词匹配结果 let mixIndexSearchedRes: { id: number, score: number }[] = [] const vectorWeight = config.SEARCHED_VECTOR_WEIGHT const fullTextWeight = config.SEARCHED_FULL_TEXT_WEIGHT sortedLshRes.forEach(lshItem => { const match = reRankFullIndexRes.find(item => Number(item.ref) === lshItem.id) if (match) { mixIndexSearchedRes.push({ id: lshItem.id, score: lshItem.similarity * vectorWeight + match.score * fullTextWeight }) } else { mixIndexSearchedRes.push({ id: lshItem.id, score: lshItem.similarity }) } }) // 补充关键词匹配的尾部数据 const lshTailStartIndex = Math.floor(vectorWeight * sortedLshRes.length) const lshTailMaxScore = sortedLshRes.slice(lshTailStartIndex)?.[0]?.similarity || 1 reRankFullIndexRes.forEach(item => { if (!mixIndexSearchedRes.find(i => i.id === Number(item.ref))) { mixIndexSearchedRes.push({ id: Number(item.ref), score: item.score * lshTailMaxScore }) } }) mixIndexSearchedRes = mixIndexSearchedRes .sort((a, b) => b.score - a.score) .filter(item => item.score > config.SEARCH_SCORE_THRESHOLD) 

    啥,你觉得这一套不靠谱,看下面!!!

    成果

    代码已开源(有帮助的话来个 star 吧)

    github.com/Yoan98/Ncurator

    想看实际演示?这里

    www.ncurator.com

    有啥能证明这个玩意靠谱呢?这里

    1.上了阮一峰的周刊 科技爱好者周刊(第 337 期):互联网创业几乎没了

    2.上了 DeepSeek 的集成推荐 https://github.com/deepseek-ai/awesome-deepseek-integration

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana   &bsp; 3443 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 04:23 PVG 12:23 LAX 21:23 JFK 00:23
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86