RAG系列:系統(tǒng)評(píng)估 - 基于LLM-as-judge實(shí)現(xiàn)評(píng)估系統(tǒng)
引言
在 系統(tǒng)評(píng)估 - 五個(gè)主流評(píng)估指標(biāo)詳解 中,我們了解了 RAG 系統(tǒng)評(píng)估的 5 個(gè)主流指標(biāo),它們分別是 上下文召回率(Context Recall)、上下文相關(guān)性(Context Relevance)、答案忠實(shí)度(Faithfulness)、答案相關(guān)性(Answer Relevance)以及答案正確性(Answer Correctness),也簡(jiǎn)單了解了一些 RAG 系統(tǒng)的評(píng)估方法以及主流的評(píng)估系統(tǒng)。
今天我們將基于 LLM-as-judge 自己實(shí)現(xiàn)一套 RAG 系統(tǒng)評(píng)估系統(tǒng),然后通過該評(píng)估系統(tǒng)評(píng)估我們?cè)?nbsp;基于 DeepSeek + Chroma + LangChain 開發(fā)一個(gè)簡(jiǎn)單 RAG 系統(tǒng) 中搭建好的基礎(chǔ)版 RAG 系統(tǒng),以基礎(chǔ)版 RAG 系統(tǒng)這 5 個(gè)評(píng)估指標(biāo)值作為基準(zhǔn),通過學(xué)習(xí)不同的優(yōu)化方法來提升這 5 個(gè)指標(biāo)。
因?yàn)槭峭ㄟ^ LLM 來評(píng)估,所以評(píng)估 LLM 的能力越強(qiáng),理論上評(píng)估就會(huì)越準(zhǔn)確,因此在實(shí)際的業(yè)務(wù)場(chǎng)景中,盡可能選用能力更強(qiáng)的 LLM。
為了學(xué)習(xí)方便,本文采用的評(píng)估 LLM 是 Ollama 部署的 qwen2.5 14b。
本文完整代碼地址[1]:https://github.com/laixiangran/ai-learn/blob/main/src/app/rag/03_rag_evaluator/route.ts
下面具體講解下每個(gè)指標(biāo)評(píng)估器的代碼實(shí)現(xiàn):
上下文召回率評(píng)估器實(shí)現(xiàn)
衡量檢索到的上下文是否覆蓋參考答案所需的所有關(guān)鍵信息,避免遺漏關(guān)鍵信息。取值在 0 到 1 之間,數(shù)值越高表示檢索到的上下文覆蓋越全面。計(jì)算公式:上下文召回率 = 上下文覆蓋的關(guān)鍵信息數(shù)量 / 參考答案中關(guān)鍵信息總數(shù)量。例如,參考答案需要 5 個(gè)關(guān)鍵信息,若檢索到的上下文覆蓋其中 4 個(gè)關(guān)鍵信息,則上下文召回率為 0.8。
將參考答案(referenceAnswer)拆分成多個(gè)句子/關(guān)鍵信息(referenceAnswerStatements)。
舉個(gè)例子:
{
"referenceAnswer": "2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣,未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。",
"referenceAnswerStatements": [
"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣。",
"未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。"
]
}
代碼實(shí)現(xiàn)如下:
/**
* 將文本拆分為多個(gè)句子(關(guān)鍵信息)
* @param text 待拆分的文本
* @param evaluateLLM 拆分的 LLM
* @returns
*/
async function statementSplit(text, evaluateLLM) {
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是將以下文本拆分為多個(gè)獨(dú)立的句子,每個(gè)句子獨(dú)立表達(dá)一個(gè)完整含義,同時(shí)保留原意的邏輯連貫性。
說明:
1. 嚴(yán)格按以下JSON格式返回:["句子1", "句子2", ...],不能輸出其他無關(guān)內(nèi)容。
文本:
${text}
回答:
`;
const res = await evaluateLLM.invoke(prompt);
const data = formatToJson(res) || [];
console.log('statements: ', data);
return data;
}
逐個(gè)分析每個(gè)句子/關(guān)鍵信息(referenceAnswerStatements)是否可歸因于給定的上下文(retrievedContext)并計(jì)算上下文召回率(contextRecall)。
舉個(gè)例子:
{
"retrievedContext":[
"2016 年,美國(guó)教育部撥款40億\n美元用以計(jì)算機(jī)科學(xué)教育普及\n五年計(jì)劃。\n英國(guó):滲透率9.3%\n2024 年,英國(guó)把編程納入5-12 歲\n少兒的必修課。\n芬蘭\n2016年,芬蘭將編程納入小學(xué)教學(xué)大綱",
...
],
"referenceAnswer":"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣,未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。",
"referenceAnswerStatements":[
"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣。",
"未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。"
]
"contextRecall":{
"score":1,
"data":[
{"score":1,"statement":"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣。"},
{
"score":1,
"statement":"未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。"
}
]
}
}
代碼實(shí)現(xiàn)如下:
/**
* 上下文召回率評(píng)估器。
* 實(shí)現(xiàn)步驟:
* 1. 將參考答案拆分成多個(gè)句子(關(guān)鍵信息);
* 2. 逐個(gè)分析每個(gè)句子(關(guān)鍵信息)是否可歸因于給定的上下文;
* 3. 根據(jù)每個(gè)句子(關(guān)鍵信息)的得分,計(jì)算上下文召回率。
* @param evaluateData 評(píng)估數(shù)據(jù)
* @param evaluateLLM 評(píng)估 LLM
* @returns
*/
asyncfunctioncontextRecallEvaluator(evaluateData, evaluateLLM) {
// retrievedContext 檢索到的上下文
// referenceAnswer 參考答案
// referenceAnswerStatements 參考答案拆分出的多個(gè)句子(關(guān)鍵信息)
const { retrievedContext, referenceAnswer, referenceAnswerStatements } =
evaluateData;
let newStatements = [];
if (!referenceAnswerStatements) {
newStatements = awaitstatementSplit(referenceAnswer, evaluateLLM);
} else {
newStatements = [...referenceAnswerStatements];
}
const allRes = [];
// 逐個(gè)分析每個(gè)句子(關(guān)鍵信息)是否可歸因于給定的上下文
while (newStatements.length > 0) {
const statement = newStatements.shift();
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是分析句子是否可歸因于給定的上下文。
說明:
1. 如果句子不能歸因于上下文,則得分為0;
2. 如果句子能夠歸因于上下文,則得分為1;
3. 嚴(yán)格按以下JSON格式返回:{"score": "得分"},不能輸出其他無關(guān)內(nèi)容。
句子:
${statement}
上下文:
${retrievedContext.join('\n')}
回答:
`;
const llmRes = await evaluateLLM.invoke(prompt);
const data = formatToJson(llmRes);
allRes.push({
score: data?.score ? +data.score : 0,
statement,
});
console.log('contextRecallEvaluator: ', allRes);
}
// 根據(jù)每個(gè)句子(關(guān)鍵信息)的得分,計(jì)算上下文召回率
const score =
allRes.reduce((score, cur) => {
score += +cur.score;
return score;
}, 0) / allRes.length;
return {
score,
data: allRes,
};
}
上下文相關(guān)性評(píng)估器實(shí)現(xiàn)
衡量檢索到的上下文與問題之間的相關(guān)性,避免包含無關(guān)冗余內(nèi)容。取值在 0 到 1 之間,數(shù)值越高表示檢索到的上下文相關(guān)性越高。計(jì)算公式:上下文相關(guān)性 = 上下文中與問題相關(guān)的片段數(shù)量 / 上下文中片段總數(shù)量。例如,檢索到的上下文總共有 5 個(gè)片段,與問題相關(guān)的片段有 4 個(gè),則上下文相關(guān)性為 0.8。
逐個(gè)分析每個(gè)上下文片段(retrievedContext)是否與問題(question)相關(guān)并計(jì)算上下文相關(guān)性(contextRelevance)。
舉個(gè)例子:
{
"question":"預(yù)計(jì)2024年少兒編程教育市場(chǎng)規(guī)模是多少?未來潛力如何?",
"retrievedContext":[
"2016 年,美國(guó)教育部撥款40億\n美元用以計(jì)算機(jī)科學(xué)教育普及\n五年計(jì)劃。\n英國(guó):滲透率9.3%\n2024 年,英國(guó)把編程納入5-12 歲\n少兒的必修課。\n芬蘭\n2016年,芬蘭將編程納入小學(xué)教學(xué)大綱",
...
],
"contextRelevance":{
"score":0.7,
"data":[
{
"score":0,
"context":"2016 年,美國(guó)教育部撥款40億\n美元用以計(jì)算機(jī)科學(xué)教育普及\n五年計(jì)劃。\n英國(guó):滲透率9.3%\n2024 年,英國(guó)把編程納入5-12 歲\n少兒的必修課。\n芬蘭\n2016年,芬蘭將編程納入小學(xué)教學(xué)大綱"
},
...
]
}
}
代碼實(shí)現(xiàn)如下:
/**
* 上下文相關(guān)性評(píng)估器
* 實(shí)現(xiàn)步驟:
* 1. 逐個(gè)分析每個(gè)上下文片段是否與問題相關(guān);
* 2. 根據(jù)每個(gè)上下文片段的得分,計(jì)算上下文相關(guān)性。
* @param evaluateData 評(píng)估數(shù)據(jù)
* @param evaluateLLM 評(píng)估 LLM
* @returns
*/
asyncfunctioncontextRelevanceEvaluator(evaluateData, evaluateLLM) {
// question 問題
// retrievedContext 檢索到的上下文
const { question, retrievedContext } = evaluateData;
const newRetrievedContext = [...retrievedContext];
const allRes = [];
// 逐個(gè)分析每個(gè)上下文片段是否與問題相關(guān)
while (newRetrievedContext.length > 0) {
const context = newRetrievedContext.shift();
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是確定上下文是否與問題有關(guān)。
說明:
1. 如果上下文與問題無關(guān),則得分為0;
2. 如果上下文與問題有關(guān),則得分為1;
3. 嚴(yán)格按以下JSON格式返回:{"score": "得分"},不能輸出其他無關(guān)內(nèi)容。
問題:
${question}
上下文:
${context}
回答:
`;
const llmRes = await evaluateLLM.invoke(prompt);
const data = formatToJson(llmRes);
allRes.push({
score: data?.score ? +data.score : 0,
context,
});
console.log('contextRelevanceEvaluator: ', allRes);
}
// 根據(jù)每個(gè)上下文片段的得分,計(jì)算上下文相關(guān)性
const score =
allRes.reduce((score, cur) => {
score += +cur.score;
return score;
}, 0) / allRes.length;
return {
score,
data: allRes,
};
}
答案忠實(shí)度評(píng)估器實(shí)現(xiàn)
衡量實(shí)際答案是否嚴(yán)格基于檢索到的上下文,避免幻覺。取值在 0 到 1 之間,數(shù)值越高表示實(shí)際答案越嚴(yán)格基于檢索到的上下文。計(jì)算公式:答案忠實(shí)度 = 上下文能夠推斷出事實(shí)的數(shù)量 / 實(shí)際答案拆解出的事實(shí)總數(shù)量。例如,實(shí)際答案拆解出 5 個(gè)事實(shí),若檢索到的上下文覆蓋其中 4 個(gè)事實(shí),則答案忠實(shí)度為 0.8。
將實(shí)際答案(answer)拆分成多個(gè)句子(answerStatements)。
舉個(gè)例子:
{
"answer": "根據(jù)提供的上下文信息,2024年少兒編程教育市場(chǎng)的規(guī)模約為488億元人民幣。從長(zhǎng)遠(yuǎn)來看,隨著越來越多家長(zhǎng)認(rèn)同編程教育的重要性以及其逐漸滲透進(jìn)入教學(xué)體系內(nèi),該市場(chǎng)有望在未來5到10年內(nèi)發(fā)展成為一個(gè)千億級(jí)別的大市場(chǎng)。這表明少兒編程教育具有巨大的發(fā)展?jié)摿涂臻g。",
"answerStatements": [
"2024年少兒編程教育市場(chǎng)的規(guī)模約為488億元人民幣。",
"從長(zhǎng)遠(yuǎn)來看,隨著越來越多家長(zhǎng)認(rèn)同編程教育的重要性以及其逐漸滲透進(jìn)入教學(xué)體系內(nèi),該市場(chǎng)有望在未來5到10年內(nèi)發(fā)展成為一個(gè)千億級(jí)別的大市場(chǎng)。",
"這表明少兒編程教育具有巨大的發(fā)展?jié)摿涂臻g。"
]
}
代碼實(shí)現(xiàn)如下:
/**
* 將文本拆分為多個(gè)句子(關(guān)鍵信息)
* @param text 待拆分的文本
* @param evaluateLLM 拆分的 LLM
* @returns
*/
async function statementSplit(text, evaluateLLM) {
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是將以下文本拆分為多個(gè)獨(dú)立的句子,每個(gè)句子獨(dú)立表達(dá)一個(gè)完整含義,同時(shí)保留原意的邏輯連貫性。
說明:
1. 嚴(yán)格按以下JSON格式返回:["句子1", "句子2", ...],不能輸出其他無關(guān)內(nèi)容。
文本:
${text}
回答:
`;
const res = await evaluateLLM.invoke(prompt);
const data = formatToJson(res) || [];
console.log('statements: ', data);
return data;
}
逐個(gè)分析每個(gè)句子(answerStatements)是否可歸因于給定的上下文(retrievedContext)并計(jì)算答案忠實(shí)度(faithfulness)。
舉個(gè)例子:
{
"retrievedContext":[
"2016 年,美國(guó)教育部撥款40億\n美元用以計(jì)算機(jī)科學(xué)教育普及\n五年計(jì)劃。\n英國(guó):滲透率9.3%\n2024 年,英國(guó)把編程納入5-12 歲\n少兒的必修課。\n芬蘭\n2016年,芬蘭將編程納入小學(xué)教學(xué)大綱",
...
],
"answer":"根據(jù)提供的上下文信息,2024年少兒編程教育市場(chǎng)的規(guī)模約為488億元人民幣。從長(zhǎng)遠(yuǎn)來看,隨著越來越多家長(zhǎng)認(rèn)同編程教育的重要性以及其逐漸滲透進(jìn)入教學(xué)體系內(nèi),該市場(chǎng)有望在未來5到10年內(nèi)發(fā)展成為一個(gè)千億級(jí)別的大市場(chǎng)。這表明少兒編程教育具有巨大的發(fā)展?jié)摿涂臻g。",
"answerStatements":[
"2024年少兒編程教育市場(chǎng)的規(guī)模約為488億元人民幣。",
"從長(zhǎng)遠(yuǎn)來看,隨著越來越多家長(zhǎng)認(rèn)同編程教育的重要性以及其逐漸滲透進(jìn)入教學(xué)體系內(nèi),該市場(chǎng)有望在未來5到10年內(nèi)發(fā)展成為一個(gè)千億級(jí)別的大市場(chǎng)。",
"這表明少兒編程教育具有巨大的發(fā)展?jié)摿涂臻g。"
],
"faithfulness":{
"score":1,
"data":[
{
"score":1,
"statement":"2024年少兒編程教育市場(chǎng)的規(guī)模約為488億元人民幣。"
},
{
"score":1,
"statement":"從長(zhǎng)遠(yuǎn)來看,隨著越來越多家長(zhǎng)認(rèn)同編程教育的重要性以及其逐漸滲透進(jìn)入教學(xué)體系內(nèi),該市場(chǎng)有望在未來5到10年內(nèi)發(fā)展成為一個(gè)千億級(jí)別的大市場(chǎng)。"
},
{
"score":1,
"statement":"這表明少兒編程教育具有巨大的發(fā)展?jié)摿涂臻g。"
}
]
}
}
代碼實(shí)現(xiàn)如下:
/**
* 答案忠實(shí)度評(píng)估器
* 實(shí)現(xiàn)步驟:
* 1. 將實(shí)際答案拆分成多個(gè)句子(事實(shí));
* 2. 逐個(gè)分析每個(gè)句子(事實(shí))是否可歸因于給定的上下文;
* 3. 根據(jù)每個(gè)句子(事實(shí))的得分,計(jì)算答案忠實(shí)度。
* @param evaluateData 評(píng)估數(shù)據(jù)
* @param evaluateLLM 評(píng)估 LLM
* @returns
*/
asyncfunctionfaithfulnessEvaluator(evaluateData, evaluateLLM) {
// retrievedContext 檢索到的上下文
// answer 實(shí)際答案
// answerStatements 實(shí)際答案拆分出的多個(gè)句子(事實(shí))
const { retrievedContext, answer, answerStatements } = evaluateData;
let newStatements = [];
if (!answerStatements) {
newStatements = awaitstatementSplit(answer, evaluateLLM);
} else {
newStatements = [...answerStatements];
}
const allRes = [];
// 逐個(gè)分析每個(gè)句子(事實(shí))是否可歸因于給定的上下文
while (newStatements.length > 0) {
const statement = newStatements.shift();
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是分析句子是否可歸因于給定的上下文。
說明:
1. 如果句子不能歸因于上下文,則得分為0;
2. 如果句子能夠歸因于上下文,則得分為1;
3. 嚴(yán)格按以下JSON格式返回:{"score": "得分"},不能輸出其他無關(guān)內(nèi)容。
句子:
${statement}
上下文:
${retrievedContext.join('\n')}
回答:
`;
const llmRes = await evaluateLLM.invoke(prompt);
const data = formatToJson(llmRes);
allRes.push({
score: data?.score ? +data.score : 0,
statement,
});
console.log('faithfulnessEvaluator: ', allRes);
}
// 根據(jù)每個(gè)句子(事實(shí))的得分,計(jì)算答案忠實(shí)度
const score =
allRes.reduce((score, cur) => {
score += +cur.score;
return score;
}, 0) / allRes.length;
return {
score,
data: allRes,
};
}
答案相關(guān)性評(píng)估器實(shí)現(xiàn)
衡量實(shí)際答案是否直接完整回答用戶問題,排除冗余或跑題。取值在 0 到 1 之間,數(shù)值越高表示實(shí)際答案更直接完整回答用戶問題。計(jì)算公式:答案相關(guān)性 = 與實(shí)際問題相關(guān)的模擬問題數(shù)量 / 實(shí)際答案推導(dǎo)出的模擬問題總數(shù)量。例如,實(shí)際答案推導(dǎo)出 5 個(gè)模擬問題,若其中 4 個(gè)與實(shí)際問題相關(guān),則答案相關(guān)性為 0.8。
根據(jù)實(shí)際答案(answer)推導(dǎo)出多個(gè)模擬問題(simulationQuestions)。
舉個(gè)例子:
{
"answer": "根據(jù)提供的上下文信息,2024年少兒編程教育市場(chǎng)的規(guī)模約為488億元人民幣。從長(zhǎng)遠(yuǎn)來看,隨著越來越多家長(zhǎng)認(rèn)同編程教育的重要性以及其逐漸滲透進(jìn)入教學(xué)體系內(nèi),該市場(chǎng)有望在未來5到10年內(nèi)發(fā)展成為一個(gè)千億級(jí)別的大市場(chǎng)。這表明少兒編程教育具有巨大的發(fā)展?jié)摿涂臻g。",
"simulationQuestions": [
"2024年少兒編程教育市場(chǎng)的規(guī)模是多少?",
"未來五年到十年,少兒編程教育市場(chǎng)規(guī)模預(yù)計(jì)能達(dá)到多少?",
"為什么說少兒編程教育有巨大的發(fā)展空間?"
]
}
代碼實(shí)現(xiàn)如下:
/**
* 根據(jù)答案推導(dǎo)出多個(gè)模擬問題
* @param text
* @param evaluateLLM
* @returns
*/
async function simulationQuestion(text, evaluateLLM) {
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是根據(jù)以下答案的核心內(nèi)容來生成3個(gè)用戶可能問的問題。
說明:
1. 嚴(yán)格按以下JSON格式返回:["問題1", "問題2", ...],不能輸出其他無關(guān)內(nèi)容。
答案:
${text}
回答:
`;
const res = await evaluateLLM.invoke(prompt);
const data = formatToJson(res) || [];
console.log('questions: ', data);
return data;
}
逐個(gè)分析每個(gè)模擬問題(simulationQuestions)是否與原問題(question)相似并計(jì)算答案相關(guān)性(answerRelevance)。
舉個(gè)例子:
{
"question":"預(yù)計(jì)2024年少兒編程教育市場(chǎng)規(guī)模是多少?未來潛力如何?",
"answer":"根據(jù)提供的上下文信息,2024年少兒編程教育市場(chǎng)的規(guī)模約為488億元人民幣。從長(zhǎng)遠(yuǎn)來看,隨著越來越多家長(zhǎng)認(rèn)同編程教育的重要性以及其逐漸滲透進(jìn)入教學(xué)體系內(nèi),該市場(chǎng)有望在未來5到10年內(nèi)發(fā)展成為一個(gè)千億級(jí)別的大市場(chǎng)。這表明少兒編程教育具有巨大的發(fā)展?jié)摿涂臻g。",
"simulationQuestions":[
"2024年少兒編程教育市場(chǎng)的規(guī)模是多少?",
"未來五年到十年,少兒編程教育市場(chǎng)規(guī)模預(yù)計(jì)能達(dá)到多少?",
"為什么說少兒編程教育有巨大的發(fā)展空間?"
],
"answerRelevance":{
"score":1,
"data":[
{
"score":1,
"simulationQuestion":"2024年少兒編程教育市場(chǎng)的規(guī)模是多少?"
},
{
"score":1,
"simulationQuestion":"未來五年到十年,少兒編程教育市場(chǎng)規(guī)模預(yù)計(jì)能達(dá)到多少?"
},
{
"score":1,
"simulationQuestion":"為什么說少兒編程教育有巨大的發(fā)展空間?"
}
]
}
}
代碼實(shí)現(xiàn)如下:
/**
* 答案相關(guān)性評(píng)估器
* 實(shí)現(xiàn)步驟:
* 1. 根據(jù)實(shí)際答案推導(dǎo)出多個(gè)模擬問題;
* 2. 逐個(gè)分析每個(gè)模擬問題是否與原問題相似;
* 3. 根據(jù)每個(gè)模擬問題的得分,計(jì)算答案相關(guān)性。
* @param evaluateData 評(píng)估數(shù)據(jù)
* @param evaluateLLM 評(píng)估 LLM
* @returns
*/
asyncfunctionanswerRelevanceEvaluator(evaluateData, evaluateLLM) {
// question 問題
// answer 實(shí)際答案
// simulationQuestions 根據(jù)實(shí)際答案推導(dǎo)出的多個(gè)模擬問題
const { question, answer, simulationQuestions } = evaluateData;
let newSimulationQuestions = [];
if (!simulationQuestions) {
newSimulationQuestions = awaitsimulationQuestion(answer, evaluateLLM);
} else {
newSimulationQuestions = [...simulationQuestions];
}
const allRes = [];
// 逐個(gè)分析每個(gè)模擬問題是否與原問題相似
while (newSimulationQuestions.length > 0) {
const simulationQuestion = newSimulationQuestions.shift();
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是分析模擬問題和實(shí)際問題是否相似。
說明:
1. 如果模擬問題與實(shí)際問題不相似,則得分為0;
2. 如果模擬問題與實(shí)際問題相似,則得分為1;
3. 嚴(yán)格按以下JSON格式返回:{"score": "相似度"},不能輸出其他無關(guān)內(nèi)容。
模擬問題:
${simulationQuestion}
實(shí)際問題:
${question}
回答:
`;
const llmRes = await evaluateLLM.invoke(prompt);
const data = formatToJson(llmRes);
allRes.push({
score: data?.score ? +data.score : 0,
simulationQuestion,
});
console.log('answerRelevanceEvaluator: ', allRes);
}
// 根據(jù)每個(gè)模擬問題的得分,計(jì)算答案相關(guān)性
const score =
allRes.reduce((score, cur) => {
score += +cur.score;
return score;
}, 0) / allRes.length;
return {
score,
data: allRes,
};
}
答案正確性評(píng)估器實(shí)現(xiàn)
衡量實(shí)際答案的準(zhǔn)確性,需與參考答案對(duì)比。取值在 0 到 1 之間,數(shù)值越高表示實(shí)際答案與參考答案匹配度越高,準(zhǔn)確性也就越高。計(jì)算公式:答案準(zhǔn)確性 = 實(shí)際答案覆蓋的關(guān)鍵信息數(shù)量 / 參考答案中關(guān)鍵信息總數(shù)量。例如,參考答案需要 5 個(gè)關(guān)鍵信息,若實(shí)際答案覆蓋其中 4 個(gè)關(guān)鍵信息,則答案正確性為 0.8。
將參考答案(referenceAnswer)拆分成多個(gè)句子/關(guān)鍵信息(referenceAnswerStatements)。
舉個(gè)例子:
{
"referenceAnswer": "2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣,未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。",
"referenceAnswerStatements": [
"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣。",
"未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。"
]
}
代碼實(shí)現(xiàn)如下:
/**
* 將文本拆分為多個(gè)句子(關(guān)鍵信息)
* @param text 待拆分的文本
* @param evaluateLLM 拆分的 LLM
* @returns
*/
async function statementSplit(text, evaluateLLM) {
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是將以下文本拆分為多個(gè)獨(dú)立的句子,每個(gè)句子獨(dú)立表達(dá)一個(gè)完整含義,同時(shí)保留原意的邏輯連貫性。
說明:
1. 嚴(yán)格按以下JSON格式返回:["句子1", "句子2", ...],不能輸出其他無關(guān)內(nèi)容。
文本:
${text}
回答:
`;
const res = await evaluateLLM.invoke(prompt);
const data = formatToJson(res) || [];
console.log('statements: ', data);
return data;
}
逐個(gè)分析每個(gè)句子/關(guān)鍵信息(referenceAnswerStatements)是否可歸因于給定的實(shí)際答案(question)并計(jì)算答案正確性(answerCorrectness)。
舉個(gè)例子:
{
"question":"預(yù)計(jì)2024年少兒編程教育市場(chǎng)規(guī)模是多少?未來潛力如何?",
"referenceAnswer":"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣,未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。",
"referenceAnswerStatements":[
"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣。",
"未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。"
]
"answerCorrectness":{
"score":1,
"data":[
{"score":1,"statement":"2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣。"},
{
"score":1,
"statement":"未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。"
}
]
}
}
代碼實(shí)現(xiàn)如下:
/**
* 答案正確性評(píng)估器
* 實(shí)現(xiàn)步驟:
* 1. 將參考答案拆分成多個(gè)句子(關(guān)鍵信息);
* 2. 逐個(gè)分析每個(gè)句子(關(guān)鍵信息)是否可歸因于給定的實(shí)際答案;
* 3. 根據(jù)每個(gè)句子(關(guān)鍵信息)的得分,計(jì)算答案正確性。
* @param evaluateData 評(píng)估數(shù)據(jù)
* @param evaluateLLM 評(píng)估 LLM
* @returns
*/
asyncfunctionanswerCorrectnessEvaluator(evaluateData, evaluateLLM) {
// answer 實(shí)際答案
// referenceAnswer 參考答案
// referenceAnswerStatements 參考答案拆分出的多個(gè)句子(關(guān)鍵信息)
const { answer, referenceAnswer, referenceAnswerStatements } = evaluateData;
let newStatements = [];
if (!referenceAnswerStatements) {
newStatements = awaitstatementSplit(referenceAnswer, evaluateLLM);
} else {
newStatements = [...referenceAnswerStatements];
}
const allRes = [];
// 逐個(gè)分析每個(gè)句子(關(guān)鍵信息)是否可歸因于給定的實(shí)際答案
while (newStatements.length > 0) {
const statement = newStatements.shift();
const prompt = `
你是一個(gè)語言專家,你的任務(wù)是分析句子是否可歸因于給定的實(shí)際答案。
說明:
1. 如果句子不能歸因于實(shí)際答案,則得分為0;
2. 如果句子能夠歸因于實(shí)際答案,則得分為1;
3. 嚴(yán)格按以下JSON格式返回:{"score": "得分"},不能輸出其他無關(guān)內(nèi)容。
句子:
${statement}
實(shí)際答案:
${answer}
回答:
`;
const llmRes = await evaluateLLM.invoke(prompt);
const data = formatToJson(llmRes);
allRes.push({
score: data?.score ? +data.score : 0,
statement,
});
console.log('answerCorrectnessEvaluator: ', allRes);
}
// 根據(jù)每個(gè)句子(關(guān)鍵信息)的得分,計(jì)算答案正確性
const score =
allRes.reduce((score, cur) => {
score += +cur.score;
return score;
}, 0) / allRes.length;
return {
score,
data: allRes,
};
}
基礎(chǔ)版 RAG 系統(tǒng)評(píng)估
至此,我們基于 LLM-as-judge 自己實(shí)現(xiàn)了一套 RAG 系統(tǒng)評(píng)估系統(tǒng),下面我們通過該評(píng)估系統(tǒng)來評(píng)估基礎(chǔ)版 RAG 系統(tǒng)。
以下每一步的詳細(xì)代碼就不貼了,評(píng)估詳細(xì)代碼地址[2]:https://github.com/laixiangran/ai-learn/blob/main/src/app/rag/03_rag_evaluator/route.ts
準(zhǔn)備 QA 測(cè)試數(shù)據(jù)
這里我準(zhǔn)備了 20 個(gè) 測(cè)試 QA(含參考答案),QA 測(cè)試數(shù)據(jù)文件地址[3]:https://github.com/laixiangran/ai-learn/blob/main/src/app/data/qa_test_20.json
[
...
{
"question": "預(yù)計(jì)2024年少兒編程教育市場(chǎng)規(guī)模是多少?未來潛力如何?",
"referenceAnswer": "2024年市場(chǎng)規(guī)模預(yù)計(jì)約488億人民幣,未來有望達(dá)到千億級(jí)別,顯示行業(yè)持續(xù)增長(zhǎng)的潛力。"
},
...
]
知識(shí)庫(kù)構(gòu)建
文檔切分配置:
- 用于分割文本的字符或字符串(separator):["\n\n", "\n", " ", ""];
- 每個(gè)文本塊的最大字符數(shù)(chunk_size):500;
- 文本塊之間的重疊字符數(shù)(chunk_overlap):50。
Embedding 模型:
- nomic-embed-text。
代碼實(shí)現(xiàn)如下:
// 1. 文件解析
const docs = awaitloadPdf(
'src/app/data/2024少兒編程教育行業(yè)發(fā)展趨勢(shì)報(bào)告.pdf'
);
// 2. 文件切分
const texts = awaitsplitDocuments(docs);
// 3. 初始化向量模型和向量數(shù)據(jù)庫(kù),并將文檔存儲(chǔ)到向量數(shù)據(jù)庫(kù)
awaitaddDocuments(texts);
指標(biāo)評(píng)估
檢索上下文 TopK: 3;
評(píng)估 LLM: qwen2.5:14b(Ollama 部署)。
代碼實(shí)現(xiàn)如下:
// 單個(gè)指標(biāo)評(píng)估
asyncfunctionbathEvaluator(indexName: string) {
const evaluatorMap = {
contextRecall: contextRecallEvaluator,
contextRelevance: contextRelevanceEvaluator,
faithfulness: faithfulnessEvaluator,
answerRelevance: answerRelevanceEvaluator,
answerCorrectness: answerCorrectnessEvaluator,
};
const evaluator = evaluatorMap[indexName];
const inputPath = 'src/app/data/qa_test_20.json';
const outputPath = 'src/app/data/qa_test_20_base_evaluate.json';
const qaDatas = awaitreadJsonFile(inputPath);
const llm = initOllamaLLM('qwen2.5:14b');
const res = [];
while (qaDatas.length > 0) {
const data = qaDatas.shift();
if (!data.answer) {
const { retrievedContext, answer } = awaitllmAnswerByQaData(
data.question
);
data.retrievedContext = retrievedContext;
data.answer = answer;
}
const evaluateRes = awaitevaluator(data, llm);
res.push({
...data,
[indexName]: evaluateRes,
});
}
// 將 LLM 回答結(jié)果保存到文件中
awaitsaveJsonFile(JSON.stringify(res), outputPath);
// 計(jì)算最終指標(biāo)數(shù)據(jù)
const score =
res.reduce((score, cur) => {
score += cur[indexName].score;
return score;
}, 0) / res.length;
return { [indexName]: +score.toFixed(1) };
}
// 指標(biāo)評(píng)估
const indexs = [
'contextRecall',
'contextRelevance',
'faithfulness',
'answerRelevance',
'answerCorrectness',
];
constdata: any = {};
while (indexs.length > 0) {
const indexName = indexs.shift() || '';
const indexRes = awaitbathEvaluator(indexName);
data[indexName] = indexRes[indexName];
}
基礎(chǔ)版 RAG 系統(tǒng)(V1.0)各指標(biāo)的評(píng)估結(jié)果如下:
版本 | 優(yōu)化描述 | 上下文召回率(contextRecall) | 上下文相關(guān)性(contextRelevance) | 答案忠實(shí)度(faithfulness) | 答案相關(guān)性(answerRelevance) | 答案正確性(answerCorrectness) |
基礎(chǔ) RAG 系統(tǒng)(V1.0) | 無 | 0.6 | 0.5 | 0.8 | 0.5 | 0.6 |
結(jié)語
至此,我們基于 LLM-as-judge 自己實(shí)現(xiàn)一套 RAG 系統(tǒng)評(píng)估系統(tǒng),用該評(píng)估系統(tǒng)評(píng)估了基礎(chǔ)版 RAG 系統(tǒng)(V1.0)的 5 個(gè)評(píng)估指標(biāo)的表現(xiàn),可以看到基礎(chǔ)版 RAG 系統(tǒng)(V1.0)這 5 個(gè)指標(biāo)的值都是偏低的,所以后面我將通過講解不同的優(yōu)化方法來提升基礎(chǔ)版 RAG 系統(tǒng)(V1.0)這 5 個(gè)指標(biāo),敬請(qǐng)期待。
引用鏈接
[1]
本文完整代碼地址: https://github.com/laixiangran/ai-learn/blob/main/src/app/rag/03_rag_evaluator/route.ts
[2]
評(píng)估詳細(xì)代碼地址: https://github.com/laixiangran/ai-learn/blob/main/src/app/rag/03_rag_evaluator/route.ts
[3]
QA 測(cè)試數(shù)據(jù)文件地址: https://github.com/laixiangran/ai-learn/blob/main/src/app/data/qa_test_20.json