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