RAG系列:問題優(yōu)化 - 意圖識別&同義改寫&多視角分解&補充上下文
在實際業(yè)務(wù)場景中,知識庫不會只有單一領(lǐng)域的知識,可能會存在多個領(lǐng)域的知識,如果對用戶問題不提前做領(lǐng)域區(qū)分,在對基于距離的向量數(shù)據(jù)庫進行檢索時,可能會檢索出很多與用戶問題不屬于同一個領(lǐng)域的文檔片段,這樣的上下文會存在較多的噪音或者不準(zhǔn)確的信息,從而影響最終的回答效果。
另一方面知識庫中涵蓋的知識表達形式也是有限的,但用戶的提問方式卻是千人千面的,用戶遣詞造句的方式以及描述問題的角度可能會與向量數(shù)據(jù)庫中存儲的文檔片段存在差異,這就可能導(dǎo)致用戶問題和知識庫之間不能很好匹配,從而降低檢索效果。
為了解決此問題,我們可以對用戶問題進行查詢增強,比如對用戶問題進行意圖識別、同義改寫、多視角分解以及補充上下文,通過這幾個查詢增強方式來更好地匹配知識庫中的文檔片段,提升檢索效果和回答效果。
本文完整代碼地址[1]:https://github.com/laixiangran/ai-learn/blob/main/src/app/rag/04_question_optimize/route.ts
現(xiàn)在我們準(zhǔn)備兩份文檔《2024少兒編程教育行業(yè)發(fā)展趨勢報告.pdf》、《2021年低代碼行業(yè)研究報告.pdf》,將這兩份文檔同時存儲到向量數(shù)據(jù)庫中,同時我們在每個文檔片段的元數(shù)據(jù)中加上 category 這個字段,用來標(biāo)記每個片段所屬的領(lǐng)域,作為后續(xù)的檢索篩選條件。
代碼實現(xiàn)如下:
const pdfs = [
{
path: 'src/app/data/2024少兒編程教育行業(yè)發(fā)展趨勢報告.pdf',
category: '少兒編程',
},
{
path: 'src/app/data/2021年低代碼行業(yè)研究報告.pdf',
category: '低代碼',
},
];
for (const pdf of pdfs) {
const { path, category } = pdf;
const pdfContent = awaitloadPdf(path); // pdf 文件解析
const documents = awaitsplitDocuments(pdfContent); // 文檔切分
for (constdocumentof documents) {
// 添加元數(shù)據(jù)
document.metadata.category = category; // 少兒編程 or 低代碼
}
awaitaddDocuments(documents); // 保存文檔到向量數(shù)據(jù)庫
}
到這里,我們的知識庫就同時存在兩個不同領(lǐng)域的文檔,在不對問題進行任何優(yōu)化的前提下,我們將該 RAG 系統(tǒng)版本定為 V2.0,現(xiàn)在我們先通過RAG系列(五):系統(tǒng)評估 - 基于LLM-as-judge實現(xiàn)評估系統(tǒng)這一篇文章實現(xiàn)的評估系統(tǒng)來對 V2.0 進行評估,得分如下:
可以看到,相對于 V1.0,五個指標(biāo)得分都有不同程度的下降,主要的原因就是同樣的問題在 V2.0 中會檢索到不屬于“少兒編程”領(lǐng)域的文檔片段。比如用戶問題 “少兒編程教育行業(yè)當(dāng)前的發(fā)展動力主要來自哪些方面?”檢索出了“低代碼”相關(guān)的文檔片段,從而影響了各個指標(biāo)的得分:
"question": "少兒編程教育行業(yè)當(dāng)前的發(fā)展動力主要來自哪些方面?",
"retrievedContext": [
"低代碼行業(yè)發(fā)展趨勢-產(chǎn)品易用性提升路徑\n基礎(chǔ)能力\n易用性\n增加引擎和交付模塊數(shù)量,提升整個集成引擎\n的組合方式,覆蓋更多應(yīng)用場景。\n?架構(gòu)落地\n增加引擎和交付模塊數(shù)量,提升整個集成引擎\n的組合方式,覆蓋更多應(yīng)用場景。\n?平臺架構(gòu)設(shè)計\n客戶知道怎么設(shè)計才能發(fā)揮無代碼、低代碼平\n臺的真正能力,現(xiàn)階段國內(nèi)對這方面認知逐漸\n提升。\n低\n代\n碼\n/\n零\n代\n碼\n產(chǎn)\n品\n落地能力\n使用能力",
"低代碼商業(yè)模式-前后端開發(fā)平臺\n廠商角度\n?廠商角度:面向具有一定開發(fā)能力的專業(yè)人員,降低專業(yè)開發(fā)人員的使用門檻,減少對\n高級別研發(fā)人員的依賴,大大提升了開發(fā)效率和降低開發(fā)人員成本。\n軟件開發(fā)商\n產(chǎn)品提供方\n低代碼平臺廠商\n微服務(wù)架構(gòu)\n大多提供給一\n定開發(fā)能力的\n專業(yè)人員。\nDevOps\n控件、組件\n企業(yè)\n客戶\n元數(shù)據(jù)管理\n......\n中臺化能力",
"?多鯨是多鯨資本旗下教育行業(yè)垂直內(nèi)容平臺,專注產(chǎn)業(yè)視角下的教育行\(zhòng)n業(yè)研究,依托對教育產(chǎn)業(yè)的深度認知,通過原創(chuàng)圖文視頻等媒體內(nèi)容、\n鏈接一線教育從業(yè)者的線上線下活動,打造教育行業(yè)媒體影響力,與教\n育從業(yè)者同行,助力行業(yè)發(fā)展。\n掃\n\n描\n\n二\n\n維\n\n碼\n\n關(guān)\n\n多\n\n鯨\n獲\n\n取\n\n更\n\n多\n\n資\n\n訊"
],
本文將以 V2.0 作為基準(zhǔn)版本,以此來驗證對用戶問題進行查詢增強后的效果。
意圖識別
意圖識別是根據(jù)用戶問題來識別問題所屬的知識領(lǐng)域,這樣可以縮小或者準(zhǔn)確定位到需要檢索哪個知識庫,從而更精準(zhǔn)地檢索出相關(guān)的文檔片段。比如,對于這樣一個用戶問題:“少兒編程教育行業(yè)當(dāng)前的發(fā)展動力主要來自哪些方面?”,可以知道這個問題屬于“少兒編程”這個知識領(lǐng)域,那么在檢索的時候就要去檢索與“少兒編程”相關(guān)的知識庫,而不應(yīng)該去檢索其它知識庫。
對用戶問題的意圖識別有很多方法,比如規(guī)則匹配、傳統(tǒng)機器學(xué)習(xí)以及深度學(xué)習(xí)等等,這里先不做展開。
本文采用的方法是直接通過 LLM 來識別用戶問題的意圖,代碼實現(xiàn)如下:
/**
* 問題優(yōu)化 - 意圖識別
* @param question 原始問題
* @returns
*/
async function intentRecognition(question) {
const prompt = `
你是一個語言專家,你的任務(wù)是分析下面的問題是屬于哪個領(lǐng)域。
說明:
1. 無法判斷時,默認為“少兒編程”;
2. 只需要回答領(lǐng)域名稱,不要輸出其他內(nèi)容。
領(lǐng)域列表:
["少兒編程", "低代碼"]
問題:
${question}
回答:
`;
const res = await generateModel.invoke(prompt);
const data = res.content;
console.log('intentRecognition: ', data);
return data;
}
這里的意圖對應(yīng)的是每個文檔片段的元數(shù)據(jù)的 category,這樣我們就可以將 category 作為檢索條件來查詢對應(yīng)領(lǐng)域的知識庫:
// 意圖識別
const intent = await intentRecognition(evaluateData.question);
// 檢索條件
const vectorFilter = {
category: intent, // 將意圖作為篩選條件
};
// 文檔檢索
const docs = await chromadb.similaritySearchWithScore(
question,
topK,
vectorFilter
);
這樣我們檢索到的文檔片段都是屬于“少兒編程”領(lǐng)域,不會再有“低代碼”的文檔片段了。
"question": "少兒編程教育行業(yè)當(dāng)前的發(fā)展動力主要來自哪些方面?",
"retrievedContext": [
"?多鯨是多鯨資本旗下教育行業(yè)垂直內(nèi)容平臺,專注產(chǎn)業(yè)視角下的教育行\(zhòng)n業(yè)研究,依托對教育產(chǎn)業(yè)的深度認知,通過原創(chuàng)圖文視頻等媒體內(nèi)容、\n鏈接一線教育從業(yè)者的線上線下活動,打造教育行業(yè)媒體影響力,與教\n育從業(yè)者同行,助力行業(yè)發(fā)展。\n掃\n\n描\n\n二\n\n維\n\n碼\n\n關(guān)\n\n多\n\n鯨\n獲\n\n取\n\n更\n\n多\n\n資\n\n訊",
"的需求,具備快速成長和創(chuàng)造巨大價值的潛力。然而,曾經(jīng)的從業(yè)邏輯,已不再適配當(dāng)前大量新玩家涌入、市場\n競爭愈發(fā)激烈的全新格局。",
"具保持同步。\n【多鯨資本創(chuàng)始合伙人/姚玉飛 】\n?未來教育的關(guān)注點,是培養(yǎng)個性鮮明、獨立自強的大寫的“人”。我們希望孩子們面對一個繁雜多樣、極\n不確定的世界時,擁有高階的分析判斷力,能在給定條件下找到最優(yōu)選擇。作為世界公認的未來語言,編\n程已經(jīng)成為打造孩子們面向未來的核心競爭力的重要方式。"
],
我們將版本定為 V3.0,評估得分如下:
可以看到,相對于 V2.0,通過對問題的意圖識別實現(xiàn)知識庫的精準(zhǔn)檢索,五個指標(biāo)得分都恢復(fù)到 V1.0 的水平了。
接下來,我們在 V3.0 的基礎(chǔ)上再進一步對問題進行優(yōu)化。
同義改寫
同義改寫是通過將原始查詢改寫成相同語義下不同的表達方式,來解決用戶查詢單一的表達形式可能無法全面覆蓋到知識庫中多樣化表達的知識。比如,對于這樣一個用戶問題:“少兒編程教育行業(yè)當(dāng)前的發(fā)展動力主要來自哪些方面?”,可以改寫成下面幾種同義表達:1、“少兒編程教育行業(yè)的當(dāng)前發(fā)展是由哪些因素推動的?”;2、“是什么力量在驅(qū)動著少兒編程教育行業(yè)目前的發(fā)展?”;3、“目前推動少兒編程教育行業(yè)發(fā)展的重要因素有哪些?”。每個改寫后的問題都可獨立用于檢索相關(guān)文檔片段,隨后從這些不同問題中檢索到的文檔片段集合進行合并和去重處理,從而形成一個更大的相關(guān)文檔集合。
本文采用的方法是直接通過 LLM 來對用戶問題進行同義改寫,代碼實現(xiàn)如下:
/**
* 問題優(yōu)化 - 同義改寫
* @param question 原始問題
* @param num 同義改寫后的同義問題數(shù)量
* @returns
*/
async function synonymyRewritten(question, num = 3) {
const prompt = `
你是一個語言專家,你的任務(wù)是將給定的原始問題改寫成${num}個語義相同但表達方式不同的問題。
說明:
1. 嚴格按以下JSON格式返回:["問題1", "問題2", ...],不能輸出其他無關(guān)內(nèi)容。
原始問題:
${question}
回答:
`;
const res = await generateModel.invoke(prompt);
const data = formatToJson(res.content) || [];
console.log('synonymyRewritten: ', data);
return data;
}
然后將原始問題和同義改寫出來的問題同時用于檢索,再將每個問題檢索到的文檔片段組合起來,根據(jù)文檔 id 去重并按文檔片段相似度升序排列,最終取 topK(此時是 3) 個文檔片段作為最終的上下文,代碼實現(xiàn)如下:
const allQuestions = [evaluateData.question];
// 同義改寫的問題
allQuestions.push(...evaluateData.synonymyQuestions);
// 檢索條件
const vectorFilter = {
category: evaluateData.category,
};
const allDocs = [];
while (allQuestions.length > 0) {
const question = allQuestions.shift();
const docs = await chromadb.similaritySearchWithScore(
question,
topK,
vectorFilter
);
allDocs.push(...docs);
}
// 根據(jù)文檔 id 去重并按文檔相似度升序排列,最終取 topK 個文檔作為上下文
const uniqueDocs = Array.from(
newMap(allDocs.map((doc) => [doc[0].id, doc])).values()
);
uniqueDocs.sort((a, b) => a[1] - b[1]);
// 最終的上下文
const retrievedContext = uniqueDocs
.slice(0, topK)
.map((doc) => doc[0].pageContent);
我們將版本定為 V4.0,評估得分如下:
可以看到,相對于 V3.0,似乎效果提升的并不明顯,有的指標(biāo)(上下文召回率、上下文相關(guān)性、答案正確性)得分反而下降了。
出現(xiàn)這個現(xiàn)象的主要原因是雖然我們通過同義改寫的方式擴大了檢索文檔片段集合,但由于我們只是簡單做了去重和根據(jù)相似度排序,在 topK 不變的情況下,最終的上下文會有一些與同義問題相似度高但與原問題相似度低的文檔片段,從而影響部分指標(biāo)的下降。
解決這個問題的方法有很多,可以直接增大 topK,也可以通過重排序模型進行重排和篩選,這塊后面會單獨詳細介紹,這里先不做展開。
本文先直接增大 topK,將 topK 設(shè)置為 6,我們將版本定為 V4.1,評估得分如下:
此時我們可以看到,相對于 V3.0,除了上下文相關(guān)性得分下降了(因為檢索文檔片段多了,就可能會包含更多與問題無關(guān)的文檔片段),其他指標(biāo)都有提升來,基本復(fù)合預(yù)期。
多視角分解
多視角分解采用分而治之的方法來處理復(fù)雜問題,將復(fù)雜問題分解為來自不同視角的子問題,以檢索到問題相關(guān)的不同角度的文檔片段。比如,對于這樣一個問題:“少兒編程教育行業(yè)當(dāng)前的發(fā)展動力主要來自哪些方面?”,可以從多個視角分解為:1、“推動少兒編程教育行業(yè)發(fā)展的重要因素有哪些?”;2、“目前支撐少兒編程教育市場的關(guān)鍵力量是什么?”;3、“少兒編程教育領(lǐng)域發(fā)展的主要驅(qū)動來源有哪些?”等子問題。每個子問題能檢索到不同的相關(guān)文檔片段,這些文檔片段分別提供來自不同視角的信息。通過綜合這些文檔片段,LLM 能夠生成一個更加全面和深入的最終答案。
本文采用的方法是直接通過 LLM 來對用戶問題進行多視角分解,代碼實現(xiàn)如下:
/**
* 問題優(yōu)化 - 多視角分解
* @param question 原始問題
* @param num 多視角分解后的子問題數(shù)量
* @returns
*/
async function subRewritten(question, num = 3) {
const prompt = `
你是一個語言專家,你的任務(wù)是將給定的原始問題分解成${num}個不同視角的子問題。
說明:
1. 嚴格按以下JSON格式返回:["問題1", "問題2", ...],不能輸出其他無關(guān)內(nèi)容。
原始問題:
${question}
回答:
`;
const res = await generateModel.invoke(prompt);
const data = formatToJson(res.content) || [];
console.log('subRewritten: ', data);
return data;
}
然后將原始問題和多視角分解出來的子問題同時用于檢索,再將每個問題檢索到的文檔片段組合起來,根據(jù)文檔 id 去重并按文檔片段相似度升序排列,最終取 topK(此時是 3) 個文檔片段作為最終的上下文,代碼實現(xiàn)如下:
const allQuestions = [evaluateData.question];
// 多視角分解
allQuestions.push(...evaluateData.subQuestions);
// 檢索條件
const vectorFilter = {
category: evaluateData.category,
};
const allDocs = [];
while (allQuestions.length > 0) {
const question = allQuestions.shift();
const docs = await chromadb.similaritySearchWithScore(
question,
topK,
vectorFilter
);
allDocs.push(...docs);
}
// 根據(jù)文檔 id 去重并按文檔相似度升序排列,最終取 topK 個文檔作為上下文
const uniqueDocs = Array.from(
newMap(allDocs.map((doc) => [doc[0].id, doc])).values()
);
uniqueDocs.sort((a, b) => a[1] - b[1]);
// 最終的上下文
const retrievedContext = uniqueDocs
.slice(0, topK)
.map((doc) => doc[0].pageContent);
我們將版本定為 V5.0,評估得分如下:
這里的問題情況和解決方案與同義改寫的情況類似,這里就不重復(fù)講解了。
補充上下文
補充上下文旨在通過生成與原始問題相關(guān)的上下文信息,從而豐富查詢內(nèi)容,提高檢索的準(zhǔn)確性和全面性。比如,對于這樣一個問題:“少兒編程教育行業(yè)當(dāng)前的發(fā)展動力主要來自哪些方面?”,可以生成如下上下文信息:“少兒編程教育行業(yè)的快速發(fā)展得益于政策支持、家長對孩子未來競爭力的重視以及技術(shù)進步和市場需求的增長”。這些生成的上下文信息可以作為原始問題的補充信息,提供更多的上下文內(nèi)容,從而提高檢索結(jié)果的相關(guān)性和豐富性。
本文采用的方法是直接通過 LLM 來根據(jù)用戶問題生成補充的上下文,代碼實現(xiàn)如下:
/**
* 問題優(yōu)化 - 補充上下文
* @param question 原始問題
* @param maxLen 補充上下文的最大字符長度
* @returns
*/
async function contextSupplement(question, maxLen = 200) {
const prompt = `
你是一個語言專家,你的任務(wù)是根據(jù)給定的原始問題,生成一段與原始問題相關(guān)的背景信息。
說明:
1. 背景信息最大不超過${maxLen}個字符;
2. 只要輸出背景信息,不能輸出其他無關(guān)內(nèi)容。
原始問題:
${question}
回答:
`;
const res = await generateModel.invoke(prompt);
const data = res.content;
console.log('supplementContext: ', data);
return data;
}
然后將原始問題和生成的補充上下文同時用于檢索,再將每個問題檢索到的文檔片段組合起來,根據(jù)文檔 id 去重并按文檔片段相似度升序排列,最終取 topK(此時是 3) 個文檔片段作為最終的上下文,代碼實現(xiàn)如下:
const allQuestions = [evaluateData.question];
// 補充上下文
allQuestions.push(evaluateData.supplementaryContext);
// 檢索條件
const vectorFilter = {
category: evaluateData.category,
};
const allDocs = [];
while (allQuestions.length > 0) {
const question = allQuestions.shift();
const docs = await chromadb.similaritySearchWithScore(
question,
topK,
vectorFilter
);
allDocs.push(...docs);
}
// 根據(jù)文檔 id 去重并按文檔相似度升序排列,最終取 topK 個文檔作為上下文
const uniqueDocs = Array.from(
newMap(allDocs.map((doc) => [doc[0].id, doc])).values()
);
uniqueDocs.sort((a, b) => a[1] - b[1]);
// 最終的上下文
const retrievedContext = uniqueDocs
.slice(0, topK)
.map((doc) => doc[0].pageContent);
我們將版本定為 V6.0,評估得分如下:
這里的問題情況和解決方案與同義改寫的情況類似,這里就不重復(fù)講解了。
結(jié)語
通過本文的研究與實踐,我們系統(tǒng)驗證了在多領(lǐng)域知識庫場景下,通過意圖識別、同義改寫、多視角分解和補充上下文等查詢增強技術(shù)對 RAG 系統(tǒng)性能的提升作用。通過實踐驗證,意圖識別通過領(lǐng)域過濾可有效減少跨領(lǐng)域噪音,而后續(xù)的語義優(yōu)化策略進一步解決了表達差異問題,使系統(tǒng)在準(zhǔn)確率、相關(guān)性和完整性等關(guān)鍵指標(biāo)有一定程度的提升。
當(dāng)然我們也看到了,僅僅通過問題優(yōu)化還不夠,要想進一步提升 RAG 系統(tǒng)各個指標(biāo)的表現(xiàn),還需要通過更多的優(yōu)化,比如切分優(yōu)化、檢索優(yōu)化等等,這些后續(xù)都會一一講解,敬請期待。
引用鏈接
[1]
本文完整代碼地址: https://github.com/laixiangran/ai-learn/blob/main/src/app/rag/04_question_optimize/route.ts