本文 AI 產出,尚未審核

LangChain 課程 – LLM 評估(Evaluation)

主題:RAG Benchmarks


簡介

Retrieval‑Augmented Generation(RAG) 的工作流程中,LLM(大型語言模型)不再只是單純產生文字,而是會結合外部知識庫的檢索結果,提供更具事實性的回應。隨著 RAG 應用越來越多,如何有效評估 這樣的系統成為關鍵。

傳統的生成模型評估指標(BLEU、ROUGE、BLEURT 等)只能衡量語言流暢度或與參考答案的相似度,卻無法直接捕捉檢索與生成之間的交互效果。為此,社群提出了 RAG Benchmarks——一套專門設計來測試檢索、證據整合與生成品質的基準測試。

本篇文章將帶你了解 RAG Benchmarks 的核心概念、在 LangChain 中如何實作、常見陷阱與最佳實踐,並提供完整的程式碼範例,協助你快速上手並在實務專案中驗證 RAG 系統的效能。


核心概念

1. 為什麼需要 RAG Benchmarks?

  • 證據依賴度(Evidence Dependency):RAG 系統的答案是否真的使用了檢索到的文件作為依據?
  • 檢索正確率(Retrieval Accuracy):檢索階段是否找到了正確且相關的文件?
  • 生成真實性(Factuality):即使檢索成功,生成的文字仍可能出現「幻覺」(hallucination),需要額外檢測。

RAG Benchmarks 透過 多層次評分(檢索、證據引用、生成)提供更細緻的診斷資訊,讓開發者能針對瓶頸進行優化。

2. 主流 RAG Benchmark 介紹

Benchmark 主要特點 資料來源 評分維度
RAG‑QA QA 形式,要求模型引用檔案頁碼 Wikipedia、ArXiv Retrieval@k、Citation‑F1、Answer‑Exact
MIRAGE 多輪對話,測試檢索在上下文中的持續性 科技新聞、法律文書 Context‑Recall、Fact‑Consistency
LLM‑Eval‑RAG (LangChain 官方) 直接整合到 LangChain 評估框架 自建測試集 Retrieval‑Score、Citation‑Score、BLEU/ROUGE

本文將以 LangChain 官方的 LLM‑Eval‑RAG 為例,示範如何在程式碼層面實作完整的評估流程。

3. LangChain 中的 RAG 評估流程

  1. 建構測試資料集:每筆測試包含問題、期望答案、以及參考文件(或文件 ID)。
  2. 執行檢索:使用 Retriever(如 FAISS, Pinecone, ElasticSearch)取得前 k 個相關文件。
  3. 生成答案:把檢索結果(或其摘要)作為 PromptTemplate 的上下文,呼叫 LLM(OpenAI, Anthropic, etc.)。
  4. 比對與計分
    • Retrieval Score:檢查正確文件是否在前 k 名。
    • Citation Score:解析 LLM 回答中的引用(如 【1】(source: doc_id)),比對是否對應到檢索結果。
    • Answer Score:使用 ROUGE、BLEU、或 LLM‑based 評分(如 GPT‑4 評分)測量答案與參考答案的相似度。
  5. 匯總報告:產出每個維度的平均分數與詳細錯誤案例,方便後續改進。

以下章節將提供實作範例,帶你一步步完成上述流程。


程式碼範例

⚠️ 注意:以下範例以 Node.js 為基礎,使用 langchainjs(v0.0.200 以上)與 openai SDK。若使用 Python,概念相同,只是套件與語法略有差異。

1. 安裝必要套件

npm install langchain openai @pinecone-database/pinecone-client

2. 建立測試資料集

// data/test_set.js
/**
 * 每筆測試案例的結構
 * @typedef {Object} RAGTestCase
 * @property {string} question   使用者的提問
 * @property {string} answer     參考答案(完整敘述)
 * @property {string[]} docIds   正確文件的 ID(可在向量資料庫中查找)
 */

/** @type {RAGTestCase[]} */
const TEST_SET = [
  {
    question: "什麼是量子糾纏?請舉例說明。",
    answer:
      "量子糾纏是指兩個或多個量子系統的狀態彼此關聯,即使相隔遙遠也會即時影響。舉例而言,若兩顆電子的自旋被糾纏,測量其中一顆的自旋會立即決定另一顆的自旋方向。",
    docIds: ["wiki_quantum_entanglement"],
  },
  {
    question: "請說明 Linux 中的 `inode` 概念。",
    answer:
      "`inode` 是檔案系統中用來描述檔案屬性的資料結構,包含檔案大小、擁有者、權限、資料塊位置等資訊。每個檔案在磁碟上都有唯一的 inode 編號。",
    docIds: ["linux_inode_manual"],
  },
  // …更多案例
];

module.exports = { TEST_SET };

說明:測試資料集以 JSON 形式儲存,docIds 用來驗證檢索正確性。

3. 設定 Retriever(以 Pinecone 為例)

// services/retriever.js
const { PineconeClient } = require("@pinecone-database/pinecone-client");
const { OpenAIEmbeddings } = require("langchain/embeddings/openai");
const { PineconeStore } = require("langchain/vectorstores/pinecone");

/**
 * 建立 Pinecone 向量資料庫的 Retriever
 * @returns {Promise<PineconeStore>}
 */
async function initRetriever() {
  const pinecone = new PineconeClient();
  await pinecone.init({
    environment: process.env.PINECONE_ENV,
    apiKey: process.env.PINECONE_API_KEY,
  });

  const index = pinecone.Index(process.env.PINECONE_INDEX);
  const embeddings = new OpenAIEmbeddings({ openAIApiKey: process.env.OPENAI_API_KEY });

  // PineconeStore 同時提供 asRetriever() 方法
  const vectorStore = await PineconeStore.fromExistingIndex(embeddings, {
    pineconeIndex: index,
    namespace: "rag-benchmarks",
  });

  // 只取前 5 個最相似的文件
  return vectorStore.asRetriever(5);
}

module.exports = { initRetriever };

重點asRetriever(k) 會回傳一個符合 LangChain BaseRetriever 介面的物件,方便後續直接呼叫 getRelevantDocuments

4. 建立 LLM 生成器與 Prompt

// services/llmGenerator.js
const { OpenAI } = require("langchain/llms/openai");
const { PromptTemplate } = require("langchain/prompts");
const { ConversationalRetrievalQAChain } = require("langchain/chains");

// 1. LLM 設定
const llm = new OpenAI({
  temperature: 0.0, // 評估時盡量保持 deterministic
  openAIApiKey: process.env.OPENAI_API_KEY,
  modelName: "gpt-4o-mini",
});

// 2. Prompt 範本(含檔案引用格式)
const QA_PROMPT = PromptTemplate.fromTemplate(`
以下是從檔案中取得的相關資訊,請根據這些資訊回答使用者的問題,並在答案末尾以「[來源: <doc_id>]」的形式標註引用的檔案。

相關文件:
{context}

問題: {question}
答案:
`);

/**
 * 建立一條結合檢索與生成的 Chain
 * @param {BaseRetriever} retriever
 * @returns {ConversationalRetrievalQAChain}
 */
function buildRAGChain(retriever) {
  return ConversationalRetrievalQAChain.fromLLM(
    llm,
    retriever,
    {
      prompt: QA_PROMPT,
      // 讓 Chain 回傳檢索到的文件與產生的答案
      returnSourceDocuments: true,
    }
  );
}

module.exports = { buildRAGChain };

說明:此 Prompt 強制模型在答案中加入 [來源: <doc_id>],方便後續 Citation Score 的計算。

5. 評分函式:Retrieval、Citation、Answer

// evaluation/metrics.js
const { rougeL } = require("langchain/evaluation/metrics");
const { getEmbedding } = require("langchain/embeddings/openai");

/**
 * 檢查正確文件是否出現在檢索結果中
 * @param {string[]} retrievedIds
 * @param {string[]} goldIds
 * @returns {number} 1 = 命中, 0 = 未命中
 */
function retrievalScore(retrievedIds, goldIds) {
  return goldIds.some((id) => retrievedIds.includes(id)) ? 1 : 0;
}

/**
 * 解析模型答案中的引用標記,回傳引用的 docId 陣列
 * 例: "......[來源: wiki_quantum_entanglement]" → ["wiki_quantum_entanglement"]
 * @param {string} answer
 * @returns {string[]}
 */
function parseCitations(answer) {
  const regex = /\[來源:\s*([^\]\s]+)\]/g;
  const ids = [];
  let match;
  while ((match = regex.exec(answer)) !== null) {
    ids.push(match[1]);
  }
  return ids;
}

/**
 * 計算 Citation Score:模型引用的文件是否與檢索結果相符
 * @param {string[]} citedIds
 * @param {string[]} retrievedIds
 * @returns {number} 1 = 正確引用, 0 = 錯誤或未引用
 */
function citationScore(citedIds, retrievedIds) {
  if (citedIds.length === 0) return 0;
  // 至少有一個引用在檢索結果內即算通過
  return citedIds.some((id) => retrievedIds.includes(id)) ? 1 : 0;
}

/**
 * 使用 ROUGE‑L 計算答案相似度
 * @param {string} pred
 * @param {string} gold
 * @returns {Promise<number>} 0~1 的分數
 */
async function answerScore(pred, gold) {
  const { rougeLScore } = await rougeL(pred, gold);
  return rougeLScore; // 直接回傳 F1 分數
}

module.exports = {
  retrievalScore,
  citationScore,
  parseCitations,
  answerScore,
};

提示:若想要更精細的事實性評估,可改用 GPT‑4 評分LLMChain 呼叫評分模型),但在本教學中保持簡潔,使用 ROUGE‑L 作為基礎指標。

6. 主評估腳本

// run_evaluation.js
require("dotenv").config();
const { TEST_SET } = require("./data/test_set");
const { initRetriever } = require("./services/retriever");
const { buildRAGChain } = require("./services/llmGenerator");
const {
  retrievalScore,
  citationScore,
  parseCitations,
  answerScore,
} = require("./evaluation/metrics");

async function evaluateOneCase(chain, testCase) {
  // 1. 執行 RAG
  const { answer, sourceDocuments } = await chain.call({
    question: testCase.question,
  });

  // 2. 取得檢索到的 docId 列表
  const retrievedIds = sourceDocuments.map((doc) => doc.metadata.doc_id);

  // 3. 計算各項分數
  const rScore = retrievalScore(retrievedIds, testCase.docIds);
  const citedIds = parseCitations(answer);
  const cScore = citationScore(citedIds, retrievedIds);
  const aScore = await answerScore(answer, testCase.answer);

  return {
    question: testCase.question,
    answer,
    retrievedIds,
    citedIds,
    scores: {
      retrieval: rScore,
      citation: cScore,
      answer: aScore,
    },
  };
}

async function main() {
  const retriever = await initRetriever();
  const ragChain = buildRAGChain(retriever);

  const results = [];
  for (const tc of TEST_SET) {
    const res = await evaluateOneCase(ragChain, tc);
    results.push(res);
    console.log(`✅ ${tc.question} → Retrieval:${res.scores.retrieval} Citation:${res.scores.citation} Answer:${res.scores.answer.toFixed(3)}`);
  }

  // 匯總統計
  const avg = (arr, key) => arr.reduce((sum, r) => sum + r.scores[key], 0) / arr.length;
  console.log("\n=== 總體報告 ===");
  console.log(`Retrieval Avg : ${avg(results, "retrieval").toFixed(2)}`);
  console.log(`Citation   Avg : ${avg(results, "citation").toFixed(2)}`);
  console.log(`Answer     Avg : ${avg(results, "answer").toFixed(3)}`);
}

main().catch(console.error);

執行方式

node run_evaluation.js

執行後會看到每筆測試的個別分數與最終的平均指標,這就是 RAG Benchmarks 在 LangChain 中的完整實作流程。


常見陷阱與最佳實踐

陷阱 為何會發生 解決方法 / 最佳實踐
檢索結果不穩定 向量資料庫的隨機抽樣或 temperature 影響 設定 temperature = 0,使用 deterministic embedding(如 text-embedding-3-large),固定 k 值。
引用格式不一致 Prompt 未明確要求模型使用統一標記,導致解析失敗 在 Prompt 中加入 嚴格的範例,如 "[來源: doc_id]",並在 output_parser 中驗證。
答案過長導致 token 截斷 LLM 輸出超過模型上限,部分內容被截斷,影響 ROUGE 計算 使用 maxTokensstop 參數限制長度,或在前端先 summarize 檢索結果。
測試集與實務資料不匹配 測試時使用的 docIds 與實際向量庫不一致,導致 retrievalScore 偏低 在建置測試集時,直接從向量庫抽樣取得 doc_id,確保一致性。
評分指標單一 只使用 ROUGE 可能忽略事實性錯誤 結合 LLM‑based fact‑checking(如 GPT‑4 判斷)或 FactScore,多指標加權。

最佳實踐總結

  1. 固定隨機種子:對於檢索向量化與 LLM 呼叫,都設定 deterministic 參數。
  2. 統一引用規範:Prompt 必須明確說明引用格式,並在後處理階段驗證。
  3. 多層次指標:同時追蹤 Retrieval、Citation、Answer,必要時加入 FactScore。
  4. 持續迭代測試集:隨著資料庫更新,定期抽樣新案例,避免過度擬合舊測試。
  5. 自動化報告:將評估結果寫入 JSON/CSV,結合 CI/CD 流程,讓每次模型升級都有可比較的基準。

實際應用場景

場景 為何需要 RAG Benchmark 可能的改進方向
客服聊天機器人 客服需引用產品手冊、FAQ,避免錯誤資訊。 透過 Benchmark 調整檢索 top‑k、增強文檔切分策略,提高 Citation Score。
法律文件分析 法律問答必須精準引用條文、案例。 使用長文本向量化(如 text-embedding-3-large)與多輪檢索,並在 Benchmark 中加入 Citation‑Precision
醫療知識庫查詢 醫師查詢必須根據最新臨床指南給出答案。 結合時間戳記過濾,Benchmark 中加入 Recency Score 以確保最新資訊被檢索。
研發文獻搜尋 研究人員需要快速定位相關論文並產出摘要。 在 Benchmark 中加入 Summarization Quality,使用 LLM 產生的摘要與原文比對。
企業內部搜尋 員工搜尋內部政策、報告,需要明確來源。 用 RAG Benchmark 監控內部向量庫的 Coverage,確保所有關鍵文件都有被檢索到的機會。

在這些場景中,持續的 RAG Benchmark 能夠幫助團隊量化改進效果,快速定位檢索或生成的瓶頸,最終提升使用者信任度與業務價值。


總結

  • RAG Benchmarks 為評估結合檢索與生成的系統提供了 多維度、可操作 的指標,解決了僅靠傳統 NLG 評分無法捕捉事實性與證據依賴的問題。
  • LangChain 中,我們只需要:
    1. 建立測試集(包含正確 docId),
    2. 設定 Retriever 與 LLM,
    3. 透過 Prompt 強制模型輸出引用,
    4. 使用簡易的 retrieval、citation、answer 計分函式,即可完成完整的基準測試。
  • 注意常見陷阱(檢索不穩、引用格式不統一、指標單一)並依照 最佳實踐(固定隨機種子、統一引用、結合多指標)調整,即可在 客服、法律、醫療、研發等 各種實務場景中快速驗證與優化 RAG 系統。

透過持續的 RAG Benchmark,開發者不僅能量化模型的事實性與證據引用能力,更能在迭代過程中快速定位瓶頸、證明改進成果,最終交付更可靠、更具信任感的 AI 服務。祝你在 LangChain 的 RAG 之路上跑出好成績! 🚀