| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>RAGAS RAG 评估系统</title>
- <link rel="preconnect" href="https://fonts.googleapis.com">
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
- <style>
- :root {
- --primary: #3b82f6;
- --primary-dark: #2563eb;
- --success: #10b981;
- --warning: #f59e0b;
- --danger: #ef4444;
- --bg: #f8fafc;
- --surface: #ffffff;
- --text: #0f172a;
- --text-muted: #64748b;
- --border: #e2e8f0;
- --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
- }
-
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- background: var(--bg);
- color: var(--text);
- line-height: 1.6;
- }
-
- /* Header */
- .header {
- background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
- color: white;
- padding: 2rem 0;
- box-shadow: var(--shadow-lg);
- }
-
- .header-content {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 2rem;
- }
-
- .header h1 {
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 0.5rem;
- }
-
- .header p {
- font-size: 1rem;
- opacity: 0.9;
- }
-
- /* Container */
- .container {
- max-width: 1200px;
- margin: 2rem auto;
- padding: 0 2rem;
- }
-
- /* Card */
- .card {
- background: var(--surface);
- border-radius: 12px;
- padding: 2rem;
- box-shadow: var(--shadow);
- margin-bottom: 2rem;
- }
-
- .card-title {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 1.5rem;
- color: var(--text);
- }
-
- /* Upload Section */
- .upload-area {
- border: 2px dashed var(--border);
- border-radius: 12px;
- padding: 3rem 2rem;
- text-align: center;
- background: linear-gradient(to bottom, #ffffff, #f8fafc);
- transition: all 0.3s ease;
- }
-
- .upload-area:hover {
- border-color: var(--primary);
- background: linear-gradient(to bottom, #ffffff, #eff6ff);
- }
-
- .upload-icon {
- font-size: 3rem;
- margin-bottom: 1rem;
- }
-
- .upload-area input[type="file"] {
- display: none;
- }
-
- .upload-label {
- display: inline-block;
- padding: 0.75rem 2rem;
- background: var(--primary);
- color: white;
- border-radius: 8px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.3s ease;
- box-shadow: var(--shadow);
- }
-
- .upload-label:hover {
- background: var(--primary-dark);
- transform: translateY(-2px);
- box-shadow: var(--shadow-lg);
- }
-
- .upload-hint {
- margin-top: 1rem;
- color: var(--text-muted);
- font-size: 0.875rem;
- }
-
- /* Form */
- .form-group {
- margin-bottom: 1.5rem;
- }
-
- .form-label {
- display: block;
- margin-bottom: 0.5rem;
- font-weight: 600;
- color: var(--text);
- font-size: 0.875rem;
- }
-
- .form-input,
- .form-select {
- width: 100%;
- padding: 0.75rem 1rem;
- border: 1px solid var(--border);
- border-radius: 8px;
- font-size: 0.875rem;
- font-family: inherit;
- transition: all 0.3s ease;
- }
-
- .form-input:focus,
- .form-select:focus {
- outline: none;
- border-color: var(--primary);
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
- }
-
- .form-hint {
- margin-top: 0.5rem;
- font-size: 0.75rem;
- color: var(--text-muted);
- }
-
- /* Buttons */
- .btn-group {
- display: flex;
- gap: 1rem;
- margin-top: 2rem;
- }
-
- .btn {
- padding: 0.75rem 1.5rem;
- border: none;
- border-radius: 8px;
- font-weight: 600;
- font-size: 0.875rem;
- cursor: pointer;
- transition: all 0.3s ease;
- box-shadow: var(--shadow);
- }
-
- .btn-primary {
- background: var(--primary);
- color: white;
- }
-
- .btn-primary:hover:not(:disabled) {
- background: var(--primary-dark);
- transform: translateY(-2px);
- box-shadow: var(--shadow-lg);
- }
-
- .btn-secondary {
- background: var(--surface);
- color: var(--text);
- border: 1px solid var(--border);
- }
-
- .btn-secondary:hover:not(:disabled) {
- background: var(--bg);
- }
-
- .btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- /* Loading */
- .loading {
- display: none;
- text-align: center;
- padding: 2rem;
- }
-
- .loading.active {
- display: block;
- }
-
- .spinner {
- border: 4px solid var(--border);
- border-top: 4px solid var(--primary);
- border-radius: 50%;
- width: 40px;
- height: 40px;
- animation: spin 1s linear infinite;
- margin: 0 auto 1rem;
- }
-
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
-
- /* Results */
- .results {
- display: none;
- }
-
- .results.active {
- display: block;
- }
-
- .metrics-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 1rem;
- margin-bottom: 2rem;
- }
-
- .metric-card {
- background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
- color: white;
- padding: 1.5rem;
- border-radius: 12px;
- box-shadow: var(--shadow);
- }
-
- .metric-name {
- font-size: 0.875rem;
- opacity: 0.9;
- margin-bottom: 0.5rem;
- }
-
- .metric-value {
- font-size: 2rem;
- font-weight: 700;
- }
-
- .metric-bar {
- height: 6px;
- background: rgba(255, 255, 255, 0.3);
- border-radius: 3px;
- margin-top: 0.75rem;
- overflow: hidden;
- }
-
- .metric-bar-fill {
- height: 100%;
- background: white;
- border-radius: 3px;
- transition: width 0.5s ease;
- }
-
- /* Chart */
- .chart-container {
- margin: 2rem 0;
- padding: 1.5rem;
- background: var(--surface);
- border-radius: 12px;
- box-shadow: var(--shadow);
- position: relative;
- height: 400px;
- }
-
- .chart-wrapper {
- position: relative;
- height: 100%;
- width: 100%;
- }
-
- /* Table */
- .table-container {
- overflow-x: auto;
- margin-top: 2rem;
- }
-
- .table {
- width: 100%;
- border-collapse: collapse;
- background: var(--surface);
- border-radius: 12px;
- overflow: hidden;
- box-shadow: var(--shadow);
- }
-
- .table th,
- .table td {
- padding: 1rem;
- text-align: left;
- border-bottom: 1px solid var(--border);
- }
-
- .table th {
- background: var(--bg);
- font-weight: 600;
- color: var(--text);
- font-size: 0.875rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .table td {
- font-size: 0.875rem;
- }
-
- .table tr:hover td {
- background: var(--bg);
- }
-
- .table tr:last-child td {
- border-bottom: none;
- }
-
- /* Score Badge */
- .score {
- display: inline-block;
- padding: 0.25rem 0.75rem;
- border-radius: 6px;
- font-weight: 600;
- font-size: 0.75rem;
- }
-
- .score-high {
- background: #d1fae5;
- color: #065f46;
- }
-
- .score-medium {
- background: #fef3c7;
- color: #92400e;
- }
-
- .score-low {
- background: #fee2e2;
- color: #991b1b;
- }
-
- /* Alert */
- .alert {
- padding: 1rem;
- border-radius: 8px;
- margin-bottom: 1rem;
- }
-
- .alert-info {
- background: #dbeafe;
- color: #1e40af;
- border-left: 4px solid var(--primary);
- }
-
- .alert-success {
- background: #d1fae5;
- color: #065f46;
- border-left: 4px solid var(--success);
- }
-
- /* Responsive */
- @media (max-width: 768px) {
- .header h1 {
- font-size: 1.5rem;
- }
-
- .container {
- padding: 0 1rem;
- }
-
- .card {
- padding: 1.5rem;
- }
-
- .metrics-grid {
- grid-template-columns: 1fr;
- }
-
- .btn-group {
- flex-direction: column;
- }
-
- .chart-container {
- height: 300px;
- }
-
- .table-container {
- overflow-x: scroll;
- }
- }
- </style>
- </head>
- <body>
- <!-- Header -->
- <div class="header">
- <div class="header-content">
- <h1>🎯 RAGAS RAG 评估系统</h1>
- <p>智能评估 RAG 系统的检索和生成质量</p>
- </div>
- </div>
- <!-- Main Container -->
- <div class="container">
- <!-- Upload Card -->
- <div class="card">
- <h2 class="card-title">📤 上传评估数据</h2>
-
- <div class="upload-area">
- <div class="upload-icon">📄</div>
- <input type="file" id="fileInput" accept=".json">
- <label for="fileInput" class="upload-label">选择 JSON 文件</label>
- <p class="upload-hint">
- 上传包含 <code>question</code> 和 <code>ground_truth</code> 的 JSON 文件<br>
- 系统将自动通过 RAG 获取 contexts 和 answer
- </p>
- </div>
- <!-- Configuration -->
- <div style="margin-top: 2rem;">
- <h3 style="font-size: 1rem; font-weight: 600; margin-bottom: 1rem;">⚙️ RAG 配置</h3>
-
- <div class="form-group">
- <label class="form-label">知识库 ID *</label>
- <input
- type="text"
- id="knowledgeIds"
- class="form-input"
- placeholder="例如: a2963496869283893248 或 id1,id2,id3"
- required
- >
- <p class="form-hint">多个 ID 用英文逗号分隔</p>
- </div>
- <div class="form-group">
- <label class="form-label">嵌入模型</label>
- <select id="embeddingId" class="form-select">
- <option value="e5" selected>e5 (默认)</option>
- <option value="multilingual-e5-large-instruct">multilingual-e5-large-instruct</option>
- </select>
- <p class="form-hint">选择用于向量检索的嵌入模型</p>
- </div>
- <div class="form-group">
- <label class="form-label">LLM 模型</label>
- <select id="model" class="form-select">
- <option value="Qwen3-Coder-30B-loft" selected>Qwen3-Coder-30B-loft</option>
- <option value="Qwen3-30B">Qwen3-30B</option>
- </select>
- <p class="form-hint">选择用于生成答案的 LLM 模型</p>
- </div>
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
- <div class="form-group">
- <label class="form-label">温度 (Temperature)</label>
- <input
- type="number"
- id="temperature"
- class="form-input"
- value="0.6"
- min="0"
- max="1"
- step="0.1"
- >
- <p class="form-hint">控制生成随机性 (0.0-1.0)</p>
- </div>
- <div class="form-group">
- <label class="form-label">Top P</label>
- <input
- type="number"
- id="topP"
- class="form-input"
- value="0.7"
- min="0"
- max="1"
- step="0.1"
- >
- <p class="form-hint">核采样参数 (0.0-1.0)</p>
- </div>
- </div>
- <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
- <div class="form-group">
- <label class="form-label">最大 Token 数</label>
- <input
- type="number"
- id="maxTokens"
- class="form-input"
- value="4096"
- min="512"
- max="8192"
- step="512"
- >
- <p class="form-hint">生成答案的最大长度</p>
- </div>
- <div class="form-group">
- <label class="form-label">检索切片数</label>
- <input
- type="number"
- id="sliceCount"
- class="form-input"
- value="5"
- min="1"
- max="20"
- step="1"
- >
- <p class="form-hint">每次检索的文档切片数量</p>
- </div>
- </div>
- <div class="form-group">
- <label class="form-label" style="display: flex; align-items: center; gap: 0.5rem;">
- <input type="checkbox" id="enableThink" style="width: auto; margin: 0;">
- <span>启用思考模式</span>
- </label>
- <p class="form-hint">启用后 LLM 会先思考再回答(适用于 Qwen3-30B)</p>
- </div>
- </div>
- <!-- Actions -->
- <div class="btn-group">
- <button id="evaluateBtn" class="btn btn-primary" disabled>
- 🚀 开始评估
- </button>
- <button id="resetBtn" class="btn btn-secondary" disabled>
- 🔄 重置
- </button>
- </div>
- </div>
- <!-- Loading -->
- <div id="loading" class="loading">
- <div class="spinner"></div>
- <p style="color: var(--text-muted);">正在评估中,请稍候...</p>
- </div>
- <!-- Results -->
- <div id="results" class="results">
- <!-- Summary Card -->
- <div class="card">
- <h2 class="card-title">📊 评估结果</h2>
-
- <div class="alert alert-success" id="successAlert" style="display: none;">
- <strong>✅ 评估完成!</strong> 共评估 <span id="totalCount">0</span> 条数据
- </div>
- <div class="metrics-grid" id="metricsGrid"></div>
- <div class="chart-container">
- <div class="chart-wrapper">
- <canvas id="metricsChart"></canvas>
- </div>
- </div>
- </div>
- <!-- Details Card -->
- <div class="card">
- <h2 class="card-title">📋 详细结果</h2>
-
- <div class="table-container">
- <table class="table">
- <thead>
- <tr>
- <th>#</th>
- <th>问题</th>
- <th>Faithfulness</th>
- <th>Answer Correctness</th>
- <th>Answer Relevancy</th>
- <th>Context Precision</th>
- <th>Context Recall</th>
- </tr>
- </thead>
- <tbody id="resultsTable"></tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
- <script>
- // DOM Elements
- const fileInput = document.getElementById('fileInput');
- const knowledgeIdsInput = document.getElementById('knowledgeIds');
- const embeddingIdSelect = document.getElementById('embeddingId');
- const modelSelect = document.getElementById('model');
- const temperatureInput = document.getElementById('temperature');
- const topPInput = document.getElementById('topP');
- const maxTokensInput = document.getElementById('maxTokens');
- const sliceCountInput = document.getElementById('sliceCount');
- const enableThinkCheckbox = document.getElementById('enableThink');
- const evaluateBtn = document.getElementById('evaluateBtn');
- const resetBtn = document.getElementById('resetBtn');
- const loading = document.getElementById('loading');
- const results = document.getElementById('results');
- const metricsGrid = document.getElementById('metricsGrid');
- const resultsTable = document.getElementById('resultsTable');
- const successAlert = document.getElementById('successAlert');
- const totalCount = document.getElementById('totalCount');
- let chartInstance = null;
- // File Input Handler
- fileInput.addEventListener('change', () => {
- const hasFile = fileInput.files && fileInput.files.length > 0;
- evaluateBtn.disabled = !hasFile;
- resetBtn.disabled = !hasFile;
- });
- // Reset Handler
- resetBtn.addEventListener('click', () => {
- fileInput.value = '';
- evaluateBtn.disabled = true;
- resetBtn.disabled = true;
- results.classList.remove('active');
- metricsGrid.innerHTML = '';
- resultsTable.innerHTML = '';
- if (chartInstance) {
- chartInstance.destroy();
- chartInstance = null;
- }
- });
- // Evaluate Handler
- evaluateBtn.addEventListener('click', async () => {
- if (!fileInput.files || fileInput.files.length === 0) {
- alert('请选择文件');
- return;
- }
- const knowledgeIds = knowledgeIdsInput.value.trim();
- if (!knowledgeIds) {
- alert('请输入知识库 ID');
- knowledgeIdsInput.focus();
- return;
- }
- evaluateBtn.disabled = true;
- resetBtn.disabled = true;
- loading.classList.add('active');
- results.classList.remove('active');
- try {
- const formData = new FormData();
- formData.append('file', fileInput.files[0]);
- formData.append('knowledge_ids', knowledgeIds);
- formData.append('embedding_id', embeddingIdSelect.value);
- formData.append('model', modelSelect.value);
- formData.append('temperature', parseFloat(temperatureInput.value));
- formData.append('top_p', parseFloat(topPInput.value));
- formData.append('max_tokens', parseInt(maxTokensInput.value));
- formData.append('slice_count', parseInt(sliceCountInput.value));
- formData.append('enable_think', enableThinkCheckbox.checked);
- const response = await fetch('/api/evaluate', {
- method: 'POST',
- body: formData
- });
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.detail || '评估失败');
- }
- const data = await response.json();
- renderResults(data);
- } catch (error) {
- alert('评估失败: ' + error.message);
- console.error(error);
- } finally {
- loading.classList.remove('active');
- evaluateBtn.disabled = false;
- resetBtn.disabled = false;
- }
- });
- // Render Results
- function renderResults(data) {
- results.classList.add('active');
- successAlert.style.display = 'block';
- totalCount.textContent = data.count;
- // Render Metrics
- metricsGrid.innerHTML = '';
- const summary = data.summary || {};
-
- Object.keys(summary).forEach(key => {
- const value = summary[key];
- const percent = Math.round((value || 0) * 100);
-
- const card = document.createElement('div');
- card.className = 'metric-card';
- card.innerHTML = `
- <div class="metric-name">${formatMetricName(key)}</div>
- <div class="metric-value">${percent}%</div>
- <div class="metric-bar">
- <div class="metric-bar-fill" style="width: ${percent}%"></div>
- </div>
- `;
- metricsGrid.appendChild(card);
- });
- // Render Chart
- renderChart(summary);
- // Render Table
- renderTable(data.rows || []);
- }
- // Render Chart
- function renderChart(summary) {
- const ctx = document.getElementById('metricsChart');
- const labels = Object.keys(summary).map(formatMetricName);
- const values = Object.values(summary).map(v => Math.round((v || 0) * 100));
- if (chartInstance) {
- chartInstance.destroy();
- }
- chartInstance = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: labels,
- datasets: [{
- label: '评估分数 (%)',
- data: values,
- backgroundColor: [
- 'rgba(59, 130, 246, 0.8)',
- 'rgba(16, 185, 129, 0.8)',
- 'rgba(245, 158, 11, 0.8)',
- 'rgba(139, 92, 246, 0.8)',
- 'rgba(236, 72, 153, 0.8)'
- ],
- borderRadius: 8,
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: {
- display: false
- }
- },
- scales: {
- y: {
- beginAtZero: true,
- max: 100,
- ticks: {
- callback: value => value + '%'
- }
- }
- }
- }
- });
- }
- // Render Table
- function renderTable(rows) {
- resultsTable.innerHTML = '';
-
- rows.forEach((row, index) => {
- const tr = document.createElement('tr');
- tr.innerHTML = `
- <td>${index + 1}</td>
- <td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${row.question}">${row.question}</td>
- <td>${formatScore(row.faithfulness)}</td>
- <td>${formatScore(row.answer_correctness)}</td>
- <td>${formatScore(row.answer_relevancy)}</td>
- <td>${formatScore(row.context_precision)}</td>
- <td>${formatScore(row.context_recall)}</td>
- `;
- resultsTable.appendChild(tr);
- });
- }
- // Format Score
- function formatScore(value) {
- if (value === null || value === undefined || isNaN(value)) {
- return '<span class="score score-low">N/A</span>';
- }
-
- const percent = Math.round(value * 100);
- let className = 'score-low';
-
- if (percent >= 80) className = 'score-high';
- else if (percent >= 60) className = 'score-medium';
-
- return `<span class="score ${className}">${percent}%</span>`;
- }
- // Format Metric Name
- function formatMetricName(name) {
- const names = {
- 'faithfulness': 'Faithfulness',
- 'answer_correctness': 'Answer Correctness',
- 'answer_relevancy': 'Answer Relevancy',
- 'context_precision': 'Context Precision',
- 'context_recall': 'Context Recall'
- };
- return names[name] || name;
- }
- </script>
- </body>
- </html>
|