feat(appendix): 添加多个交互式演示组件,完善 AI/Infra 等章节内容
- 新增 Vibe Coding 全栈相关演示组件 (DeveloperSkillShift, FrontendTriad, BackendCore 等) - 新增 RAG 相关组件 (RAGPipeline, ChunkingStrategy, Retrieval 等) - 新增 Embedding & Vector 相关组件 (EmbeddingConcept, VectorSimilarity 等) - 新增 AI Native App 设计组件 (AINativeArch, PromptDesign 等) - 新增 Infrastructure as Code 组件 (IaCConcept, TerraformWorkflow 等) - 新增 DNS & HTTPS 演示组件 (DnsResolution, HttpsHandshake 等) - 新增 Model Finetuning 组件 (FinetuningPipeline 等) - 更新多个章节的 markdown 内容,集成交互式演示
This commit is contained in:
@@ -0,0 +1,439 @@
|
||||
<!--
|
||||
ChunkingStrategyDemo.vue
|
||||
文本分块策略交互演示
|
||||
|
||||
用途:
|
||||
展示不同的文本分块策略(固定大小、按句子、语义、递归),
|
||||
用户可以输入文本并观察不同策略如何切分。
|
||||
|
||||
交互功能:
|
||||
- 输入自定义文本或使用预设文本
|
||||
- 切换不同分块策略
|
||||
- 可视化展示分块结果与边界
|
||||
-->
|
||||
<template>
|
||||
<div class="chunking-demo">
|
||||
<div class="input-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">输入文本</span>
|
||||
<button
|
||||
class="preset-btn"
|
||||
@click="usePreset"
|
||||
>
|
||||
使用示例文本
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
class="text-input"
|
||||
rows="4"
|
||||
placeholder="请输入要分块的文本,或点击「使用示例文本」..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="strategy-selector">
|
||||
<button
|
||||
v-for="s in strategies"
|
||||
:key="s.id"
|
||||
:class="['strategy-btn', { active: currentStrategy === s.id }]"
|
||||
@click="currentStrategy = s.id"
|
||||
>
|
||||
<span class="strategy-icon">{{ s.icon }}</span>
|
||||
<span class="strategy-name">{{ s.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="strategy-info">
|
||||
<div class="info-title">{{ activeStrategy.name }}</div>
|
||||
<div class="info-desc">{{ activeStrategy.desc }}</div>
|
||||
<div class="info-params">
|
||||
<span
|
||||
v-for="(p, i) in activeStrategy.params"
|
||||
:key="i"
|
||||
class="param-tag"
|
||||
>
|
||||
{{ p }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-section">
|
||||
<div class="result-header">
|
||||
分块结果
|
||||
<span class="chunk-count">共 {{ chunks.length }} 个块</span>
|
||||
</div>
|
||||
<div class="chunks-container">
|
||||
<div
|
||||
v-for="(chunk, i) in chunks"
|
||||
:key="i"
|
||||
class="chunk-item"
|
||||
:style="{ borderLeftColor: chunkColors[i % chunkColors.length] }"
|
||||
>
|
||||
<div class="chunk-meta">
|
||||
<span
|
||||
class="chunk-index"
|
||||
:style="{ background: chunkColors[i % chunkColors.length] }"
|
||||
>
|
||||
#{{ i + 1 }}
|
||||
</span>
|
||||
<span class="chunk-size">{{ chunk.length }} 字符</span>
|
||||
</div>
|
||||
<div class="chunk-text">{{ chunk }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="chunks.length === 0"
|
||||
class="empty-hint"
|
||||
>
|
||||
请输入文本后查看分块结果
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>策略</th>
|
||||
<th>优点</th>
|
||||
<th>缺点</th>
|
||||
<th>适用场景</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="s in strategies"
|
||||
:key="s.id"
|
||||
:class="{ highlight: currentStrategy === s.id }"
|
||||
>
|
||||
<td class="strategy-cell">{{ s.icon }} {{ s.name }}</td>
|
||||
<td>{{ s.pros }}</td>
|
||||
<td>{{ s.cons }}</td>
|
||||
<td>{{ s.useCase }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const chunkColors = ['#6366f1', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4']
|
||||
|
||||
const presetText = '人工智能(AI)是计算机科学的一个分支,致力于创建能够模拟人类智能的系统。机器学习是 AI 的核心方法之一,它让计算机能够从数据中学习规律。深度学习是机器学习的子集,使用多层神经网络来处理复杂任务。自然语言处理(NLP)使计算机能够理解和生成人类语言。大语言模型(LLM)如 GPT 和 Claude 通过海量文本训练,具备了强大的语言理解和生成能力。RAG(检索增强生成)技术通过在生成前检索相关文档,显著提升了 LLM 回答的准确性和时效性。向量数据库是 RAG 系统的关键组件,它能高效存储和检索文本的向量表示。'
|
||||
|
||||
const inputText = ref('')
|
||||
const currentStrategy = ref('fixed')
|
||||
|
||||
const strategies = [
|
||||
{
|
||||
id: 'fixed',
|
||||
name: '固定大小',
|
||||
icon: '📏',
|
||||
desc: '按照固定的字符数切分文本,是最简单直接的分块方式。通常会设置一定的重叠区域(overlap),避免在切分边界丢失上下文。',
|
||||
params: ['块大小: 80 字符', '重叠: 20 字符'],
|
||||
pros: '实现简单,块大小均匀',
|
||||
cons: '可能在句子中间截断',
|
||||
useCase: '结构化程度低的长文本'
|
||||
},
|
||||
{
|
||||
id: 'sentence',
|
||||
name: '按句子',
|
||||
icon: '📝',
|
||||
desc: '以句号、问号、感叹号等标点作为分隔符,按完整句子进行切分。保证每个块都是语义完整的句子集合。',
|
||||
params: ['每块: 2-3 句', '分隔符: 。?!'],
|
||||
pros: '保持句子完整性',
|
||||
cons: '块大小不均匀',
|
||||
useCase: '文章、报告等自然文本'
|
||||
},
|
||||
{
|
||||
id: 'semantic',
|
||||
name: '语义分块',
|
||||
icon: '🧠',
|
||||
desc: '根据文本的语义相似度进行分块。当相邻句子的语义差异超过阈值时,在此处切分。能更好地保持主题的连贯性。',
|
||||
params: ['相似度阈值: 0.7', '最小块: 50 字符'],
|
||||
pros: '主题连贯,语义完整',
|
||||
cons: '计算成本高,需要嵌入模型',
|
||||
useCase: '多主题混合的复杂文档'
|
||||
},
|
||||
{
|
||||
id: 'recursive',
|
||||
name: '递归分块',
|
||||
icon: '🔄',
|
||||
desc: '使用多级分隔符递归切分:先按段落分,段落太长则按句子分,句子太长则按固定大小分。LangChain 的默认策略。',
|
||||
params: ['分隔符: \\n\\n → 。→ 固定', '目标: 80 字符'],
|
||||
pros: '兼顾结构与大小',
|
||||
cons: '实现较复杂',
|
||||
useCase: '通用场景,推荐默认选择'
|
||||
}
|
||||
]
|
||||
|
||||
const activeStrategy = computed(() => strategies.find((s) => s.id === currentStrategy.value))
|
||||
|
||||
const chunks = computed(() => {
|
||||
const text = inputText.value.trim()
|
||||
if (!text) return []
|
||||
|
||||
switch (currentStrategy.value) {
|
||||
case 'fixed':
|
||||
return chunkFixed(text, 80, 20)
|
||||
case 'sentence':
|
||||
return chunkBySentence(text, 3)
|
||||
case 'semantic':
|
||||
return chunkSemantic(text)
|
||||
case 'recursive':
|
||||
return chunkRecursive(text, 80)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
function chunkFixed(text, size, overlap) {
|
||||
const result = []
|
||||
let start = 0
|
||||
while (start < text.length) {
|
||||
result.push(text.slice(start, start + size))
|
||||
start += size - overlap
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function chunkBySentence(text, perChunk) {
|
||||
const sentences = text.split(/(?<=[。?!.?!])/).filter((s) => s.trim())
|
||||
const result = []
|
||||
for (let i = 0; i < sentences.length; i += perChunk) {
|
||||
result.push(sentences.slice(i, i + perChunk).join(''))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function chunkSemantic(text) {
|
||||
const sentences = text.split(/(?<=[。?!.?!])/).filter((s) => s.trim())
|
||||
const result = []
|
||||
let current = ''
|
||||
const keywords = ['AI', 'LLM', 'RAG', 'NLP', '机器学习', '深度学习', '向量']
|
||||
let prevKeywords = new Set()
|
||||
|
||||
for (const s of sentences) {
|
||||
const curKeywords = new Set(keywords.filter((k) => s.includes(k)))
|
||||
const overlap = [...curKeywords].filter((k) => prevKeywords.has(k)).length
|
||||
const similarity = prevKeywords.size > 0 ? overlap / Math.max(prevKeywords.size, curKeywords.size) : 1
|
||||
|
||||
if (current && similarity < 0.5 && current.length > 50) {
|
||||
result.push(current)
|
||||
current = s
|
||||
} else {
|
||||
current += s
|
||||
}
|
||||
prevKeywords = curKeywords
|
||||
}
|
||||
if (current) result.push(current)
|
||||
return result
|
||||
}
|
||||
|
||||
function chunkRecursive(text, target) {
|
||||
const paragraphs = text.split(/\n\n+/).filter((p) => p.trim())
|
||||
const result = []
|
||||
for (const para of paragraphs) {
|
||||
if (para.length <= target) {
|
||||
result.push(para)
|
||||
} else {
|
||||
const sentences = para.split(/(?<=[。?!.?!])/).filter((s) => s.trim())
|
||||
let current = ''
|
||||
for (const s of sentences) {
|
||||
if ((current + s).length > target && current) {
|
||||
result.push(current)
|
||||
current = s
|
||||
} else {
|
||||
current += s
|
||||
}
|
||||
}
|
||||
if (current) result.push(current)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function usePreset() {
|
||||
inputText.value = presetText
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chunking-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.section-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
.preset-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--vp-c-brand-1);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.text-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
color: var(--vp-c-text-1);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.strategy-selector {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.strategy-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
}
|
||||
.strategy-btn.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.strategy-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
.strategy-info {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.info-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-brand-1);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.info-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.info-params {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.param-tag {
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
font-family: monospace;
|
||||
}
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.chunk-count {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-weight: 400;
|
||||
}
|
||||
.chunks-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.chunk-item {
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-left: 4px solid;
|
||||
}
|
||||
.chunk-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.chunk-index {
|
||||
padding: 1px 8px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.chunk-size {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.chunk-text {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
.empty-hint {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 13px;
|
||||
}
|
||||
.comparison-table {
|
||||
margin-top: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.comparison-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
text-align: left;
|
||||
}
|
||||
.comparison-table th {
|
||||
background: var(--vp-c-bg);
|
||||
font-weight: 600;
|
||||
}
|
||||
.comparison-table tr.highlight {
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
.strategy-cell {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,417 @@
|
||||
<!--
|
||||
RAGArchitectureDemo.vue
|
||||
RAG 架构演进交互演示
|
||||
|
||||
用途:
|
||||
展示三种 RAG 架构:Naive RAG、Advanced RAG、Modular RAG
|
||||
用户可以切换查看不同架构的流程图和特点。
|
||||
|
||||
交互功能:
|
||||
- 切换三种架构
|
||||
- 查看每种架构的流程节点
|
||||
- 对比各架构的优劣
|
||||
-->
|
||||
<template>
|
||||
<div class="rag-arch-demo">
|
||||
<div class="arch-tabs">
|
||||
<button
|
||||
v-for="(arch, i) in architectures"
|
||||
:key="i"
|
||||
:class="['arch-tab', { active: currentArch === i }]"
|
||||
@click="currentArch = i"
|
||||
>
|
||||
<span class="tab-badge">{{ arch.badge }}</span>
|
||||
<span class="tab-name">{{ arch.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="arch-desc">
|
||||
{{ activeArch.desc }}
|
||||
</div>
|
||||
|
||||
<div class="flow-diagram">
|
||||
<div
|
||||
v-for="(node, j) in activeArch.nodes"
|
||||
:key="j"
|
||||
class="flow-node-wrapper"
|
||||
>
|
||||
<div
|
||||
:class="['flow-node', node.type]"
|
||||
@click="selectedNode = selectedNode === j ? null : j"
|
||||
>
|
||||
<div class="node-icon">{{ node.icon }}</div>
|
||||
<div class="node-label">{{ node.label }}</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="j < activeArch.nodes.length - 1"
|
||||
class="flow-connector"
|
||||
>
|
||||
<span class="connector-arrow">→</span>
|
||||
<span
|
||||
v-if="node.connectorLabel"
|
||||
class="connector-label"
|
||||
>{{ node.connectorLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedNode !== null"
|
||||
class="node-detail"
|
||||
>
|
||||
<div class="node-detail-title">
|
||||
{{ activeArch.nodes[selectedNode].icon }}
|
||||
{{ activeArch.nodes[selectedNode].label }}
|
||||
</div>
|
||||
<div class="node-detail-desc">
|
||||
{{ activeArch.nodes[selectedNode].detail }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="node-hint"
|
||||
>
|
||||
点击流程节点查看详细说明
|
||||
</div>
|
||||
|
||||
<div class="arch-features">
|
||||
<div class="feature-title">架构特点</div>
|
||||
<div class="feature-grid">
|
||||
<div
|
||||
v-for="(f, i) in activeArch.features"
|
||||
:key="i"
|
||||
class="feature-item"
|
||||
>
|
||||
<span class="feature-icon">{{ f.icon }}</span>
|
||||
<span class="feature-text">{{ f.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="evolution-bar">
|
||||
<div class="evo-title">架构演进路线</div>
|
||||
<div class="evo-track">
|
||||
<div
|
||||
v-for="(arch, i) in architectures"
|
||||
:key="i"
|
||||
:class="['evo-node', { active: currentArch >= i }]"
|
||||
>
|
||||
<div class="evo-dot" />
|
||||
<div class="evo-label">{{ arch.name }}</div>
|
||||
<div class="evo-year">{{ arch.year }}</div>
|
||||
</div>
|
||||
<div class="evo-line">
|
||||
<div
|
||||
class="evo-line-fill"
|
||||
:style="{ width: (currentArch / (architectures.length - 1)) * 100 + '%' }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const currentArch = ref(0)
|
||||
const selectedNode = ref(null)
|
||||
|
||||
watch(currentArch, () => { selectedNode.value = null })
|
||||
|
||||
const architectures = [
|
||||
{
|
||||
name: 'Naive RAG',
|
||||
badge: 'v1',
|
||||
year: '2023',
|
||||
desc: '最基础的 RAG 架构,流程简单直接:索引 → 检索 → 生成。适合快速原型验证,但在复杂场景下效果有限。',
|
||||
nodes: [
|
||||
{ icon: '📄', label: '文档加载', type: 'input', detail: '将原始文档(PDF、网页、数据库等)加载到系统中,进行基本的文本提取和清洗。', connectorLabel: '' },
|
||||
{ icon: '✂️', label: '文本分块', type: 'process', detail: '将长文档按固定大小切分为较小的文本块(chunk),通常 200-500 个 token。', connectorLabel: '' },
|
||||
{ icon: '🔢', label: '向量化', type: 'process', detail: '使用嵌入模型将每个文本块转化为向量,存入向量数据库。', connectorLabel: '' },
|
||||
{ icon: '🔍', label: '检索', type: 'process', detail: '用户提问时,将问题向量化后在向量数据库中搜索最相似的文本块。', connectorLabel: '' },
|
||||
{ icon: '🤖', label: '生成', type: 'output', detail: '将检索到的文本块与问题拼接为 Prompt,交给 LLM 生成回答。' }
|
||||
],
|
||||
features: [
|
||||
{ icon: '✅', text: '实现简单,上手快' },
|
||||
{ icon: '✅', text: '适合结构化知识库' },
|
||||
{ icon: '⚠️', text: '检索质量依赖分块策略' },
|
||||
{ icon: '❌', text: '无法处理复杂查询' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Advanced RAG',
|
||||
badge: 'v2',
|
||||
year: '2024',
|
||||
desc: '在 Naive RAG 基础上增加了查询优化和检索后处理,显著提升检索质量和生成准确性。',
|
||||
nodes: [
|
||||
{ icon: '💬', label: '用户查询', type: 'input', detail: '接收用户的原始问题。', connectorLabel: '' },
|
||||
{ icon: '🔄', label: '查询改写', type: 'enhance', detail: '使用 LLM 对原始查询进行改写、扩展或分解。例如将模糊问题改写为更精确的检索查询,或生成多个子查询。', connectorLabel: '' },
|
||||
{ icon: '🔍', label: '混合检索', type: 'process', detail: '同时使用向量检索(语义)和关键词检索(BM25),融合两者的结果,兼顾语义理解和精确匹配。', connectorLabel: '' },
|
||||
{ icon: '📊', label: '重排序', type: 'enhance', detail: '使用交叉编码器对检索结果进行精细排序,过滤掉不相关的文档片段。', connectorLabel: '' },
|
||||
{ icon: '📋', label: '上下文压缩', type: 'enhance', detail: '从检索到的文档中提取与问题最相关的部分,去除冗余信息,节省上下文窗口。', connectorLabel: '' },
|
||||
{ icon: '🤖', label: '生成', type: 'output', detail: '基于优化后的上下文生成高质量回答。' }
|
||||
],
|
||||
features: [
|
||||
{ icon: '✅', text: '查询改写提升检索召回率' },
|
||||
{ icon: '✅', text: '混合检索兼顾语义和关键词' },
|
||||
{ icon: '✅', text: '重排序显著提升精度' },
|
||||
{ icon: '⚠️', text: '流程较长,延迟增加' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Modular RAG',
|
||||
badge: 'v3',
|
||||
year: '2025',
|
||||
desc: '将 RAG 拆解为可插拔的模块,支持灵活组合和路由。可根据查询类型动态选择最优流程。',
|
||||
nodes: [
|
||||
{ icon: '💬', label: '用户查询', type: 'input', detail: '接收用户的原始问题。', connectorLabel: '' },
|
||||
{ icon: '🧭', label: '路由判断', type: 'enhance', detail: '分析查询意图,决定走哪条处理路径:简单问题直接回答,复杂问题走检索流程,多步问题走分解流程。', connectorLabel: '' },
|
||||
{ icon: '🔀', label: '查询转换', type: 'enhance', detail: '根据路由结果选择:HyDE(假设文档嵌入)、Step-back(退一步提问)、子问题分解等策略。', connectorLabel: '' },
|
||||
{ icon: '🔍', label: '自适应检索', type: 'process', detail: '根据查询特征自动选择检索策略:向量检索、图检索、SQL 检索或多路检索融合。', connectorLabel: '' },
|
||||
{ icon: '🔄', label: '自我反思', type: 'enhance', detail: 'LLM 评估检索结果是否充分,不充分则触发二次检索或调整检索策略(Self-RAG / CRAG)。', connectorLabel: '' },
|
||||
{ icon: '🤖', label: '生成', type: 'output', detail: '基于充分验证的上下文生成最终回答,并附带置信度评分。' }
|
||||
],
|
||||
features: [
|
||||
{ icon: '✅', text: '模块化设计,灵活可扩展' },
|
||||
{ icon: '✅', text: '自适应路由,智能选择策略' },
|
||||
{ icon: '✅', text: '自我反思机制提升可靠性' },
|
||||
{ icon: '⚠️', text: '系统复杂度高,需要精心调优' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const activeArch = computed(() => architectures[currentArch.value])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rag-arch-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
.arch-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.arch-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
}
|
||||
.arch-tab.active {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
.tab-badge {
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-divider);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.arch-tab.active .tab-badge {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
}
|
||||
.tab-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.arch-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.flow-diagram {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
overflow-x: auto;
|
||||
padding: 12px 0;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.flow-node-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.flow-node {
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 70px;
|
||||
}
|
||||
.flow-node:hover {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.flow-node.input {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.flow-node.output {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
.flow-node.enhance {
|
||||
border-color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
}
|
||||
.flow-node.process {
|
||||
border-color: #8b5cf6;
|
||||
background: #f5f3ff;
|
||||
}
|
||||
.node-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.node-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.flow-connector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.connector-arrow {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.connector-label {
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.node-detail {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.node-detail-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-brand-1);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.node-detail-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.node-hint {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.feature-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 13px;
|
||||
}
|
||||
.feature-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.feature-text {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.evolution-bar {
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.evo-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.evo-track {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.evo-node {
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.evo-node.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.evo-dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-divider);
|
||||
margin: 0 auto 6px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.evo-node.active .evo-dot {
|
||||
background: var(--vp-c-brand-1);
|
||||
}
|
||||
.evo-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.evo-year {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.evo-line {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
.evo-line-fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand-1);
|
||||
transition: width 0.5s;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,349 @@
|
||||
<!--
|
||||
RAGPipelineDemo.vue
|
||||
RAG 完整流程可视化演示
|
||||
|
||||
用途:
|
||||
展示 RAG 的核心流程:用户提问 → 检索 → 上下文组装 → LLM 生成 → 返回结果
|
||||
用户可以逐步点击,观察每个阶段的数据流动。
|
||||
|
||||
交互功能:
|
||||
- 点击"下一步"逐步推进流程
|
||||
- 每个阶段高亮并展示说明
|
||||
- 可选择不同的示例问题
|
||||
-->
|
||||
<template>
|
||||
<div class="rag-pipeline-demo">
|
||||
<div class="query-selector">
|
||||
<span class="label">选择问题:</span>
|
||||
<button
|
||||
v-for="(q, i) in queries"
|
||||
:key="i"
|
||||
:class="['query-btn', { active: currentQuery === i }]"
|
||||
@click="selectQuery(i)"
|
||||
>
|
||||
{{ q.short }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pipeline">
|
||||
<div
|
||||
v-for="(stage, i) in stages"
|
||||
:key="i"
|
||||
:class="['stage', { active: currentStep >= i, current: currentStep === i }]"
|
||||
>
|
||||
<div class="stage-icon">{{ stage.icon }}</div>
|
||||
<div class="stage-name">{{ stage.name }}</div>
|
||||
<div
|
||||
v-if="currentStep >= i"
|
||||
class="stage-content"
|
||||
>
|
||||
{{ getStageContent(i) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="i < stages.length - 1"
|
||||
:class="['arrow', { active: currentStep > i }]"
|
||||
>
|
||||
→
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-panel">
|
||||
<div class="detail-title">{{ stages[currentStep]?.name }} — 详细说明</div>
|
||||
<div class="detail-desc">{{ stages[currentStep]?.desc }}</div>
|
||||
<div
|
||||
v-if="currentStep >= 1 && currentStep <= 2"
|
||||
class="retrieved-docs"
|
||||
>
|
||||
<div class="doc-title">检索到的文档片段:</div>
|
||||
<div
|
||||
v-for="(doc, i) in queries[currentQuery].docs"
|
||||
:key="i"
|
||||
:class="['doc-item', { visible: currentStep >= 2 }]"
|
||||
>
|
||||
<span class="doc-score">相关度 {{ doc.score }}</span>
|
||||
<span class="doc-text">{{ doc.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button
|
||||
class="ctrl-btn"
|
||||
:disabled="currentStep <= 0"
|
||||
@click="prevStep"
|
||||
>
|
||||
← 上一步
|
||||
</button>
|
||||
<span class="step-indicator">{{ currentStep + 1 }} / {{ stages.length }}</span>
|
||||
<button
|
||||
class="ctrl-btn primary"
|
||||
:disabled="currentStep >= stages.length - 1"
|
||||
@click="nextStep"
|
||||
>
|
||||
下一步 →
|
||||
</button>
|
||||
<button
|
||||
class="ctrl-btn"
|
||||
@click="reset"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const stages = [
|
||||
{
|
||||
name: '用户提问',
|
||||
icon: '💬',
|
||||
desc: '用户向系统提出一个自然语言问题。这个问题会被转化为向量表示,用于后续的语义检索。'
|
||||
},
|
||||
{
|
||||
name: '语义检索',
|
||||
icon: '🔍',
|
||||
desc: '系统将问题编码为向量,在向量数据库中搜索语义最相近的文档片段。通常使用余弦相似度或点积来衡量相关性。'
|
||||
},
|
||||
{
|
||||
name: '上下文组装',
|
||||
icon: '📋',
|
||||
desc: '将检索到的 Top-K 文档片段与原始问题拼接,构造成一个完整的 Prompt。这个 Prompt 会告诉 LLM:"请根据以下参考资料回答问题"。'
|
||||
},
|
||||
{
|
||||
name: 'LLM 生成',
|
||||
icon: '🤖',
|
||||
desc: '大语言模型接收组装好的 Prompt,基于检索到的上下文信息生成回答。因为有了真实的参考资料,模型的回答更加准确、可靠。'
|
||||
},
|
||||
{
|
||||
name: '返回结果',
|
||||
icon: '✅',
|
||||
desc: '系统将 LLM 生成的回答返回给用户。高级系统还会附带引用来源,方便用户验证答案的可靠性。'
|
||||
}
|
||||
]
|
||||
|
||||
const queries = [
|
||||
{
|
||||
short: '公司年假政策',
|
||||
question: '我们公司的年假政策是什么?',
|
||||
docs: [
|
||||
{ score: '0.95', text: '员工入职满一年后享有 10 天带薪年假,满五年后增至 15 天。' },
|
||||
{ score: '0.87', text: '年假需提前 3 个工作日申请,经直属主管审批后生效。' },
|
||||
{ score: '0.72', text: '未使用的年假可结转至次年第一季度,逾期作废。' }
|
||||
],
|
||||
answer: '根据公司规定,入职满一年享有 10 天带薪年假,满五年增至 15 天。需提前 3 个工作日申请并经主管审批,未用年假可结转至次年 Q1。'
|
||||
},
|
||||
{
|
||||
short: 'API 限流规则',
|
||||
question: '我们的 API 限流规则是怎样的?',
|
||||
docs: [
|
||||
{ score: '0.93', text: '免费用户每分钟限 60 次请求,付费用户限 600 次。' },
|
||||
{ score: '0.85', text: '超出限流后返回 HTTP 429 状态码,需等待 60 秒后重试。' },
|
||||
{ score: '0.68', text: '企业版用户可申请自定义限流配额,最高支持每分钟 10000 次。' }
|
||||
],
|
||||
answer: '免费用户每分钟限 60 次请求,付费用户 600 次。超限返回 429 状态码,需等 60 秒。企业版可申请最高 10000 次/分钟的自定义配额。'
|
||||
}
|
||||
]
|
||||
|
||||
const currentQuery = ref(0)
|
||||
const currentStep = ref(0)
|
||||
|
||||
function selectQuery(i) {
|
||||
currentQuery.value = i
|
||||
currentStep.value = 0
|
||||
}
|
||||
|
||||
function getStageContent(i) {
|
||||
const q = queries[currentQuery.value]
|
||||
if (i === 0) return q.question
|
||||
if (i === 1) return `找到 ${q.docs.length} 个相关片段`
|
||||
if (i === 2) return '问题 + 参考资料 → Prompt'
|
||||
if (i === 3) return '基于上下文生成回答...'
|
||||
if (i === 4) return q.answer
|
||||
return ''
|
||||
}
|
||||
|
||||
function nextStep() {
|
||||
if (currentStep.value < stages.length - 1) currentStep.value++
|
||||
}
|
||||
function prevStep() {
|
||||
if (currentStep.value > 0) currentStep.value--
|
||||
}
|
||||
function reset() {
|
||||
currentStep.value = 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rag-pipeline-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
.query-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.query-selector .label {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.query-btn {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.query-btn.active {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
.pipeline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
padding: 12px 0;
|
||||
}
|
||||
.stage {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
padding: 12px 8px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
.stage.active {
|
||||
opacity: 1;
|
||||
}
|
||||
.stage.current {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px rgba(var(--vp-c-brand-1-rgb, 100, 108, 255), 0.15);
|
||||
}
|
||||
.stage-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.stage-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.stage-content {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.arrow {
|
||||
position: absolute;
|
||||
right: -16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 18px;
|
||||
color: var(--vp-c-divider);
|
||||
z-index: 1;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
.arrow.active {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.detail-panel {
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
.detail-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.retrieved-docs {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
.doc-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.doc-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.doc-item.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.doc-score {
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.doc-text {
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.ctrl-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.ctrl-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ctrl-btn.primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
.step-indicator {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,409 @@
|
||||
<!--
|
||||
RAGvsFineTuningDemo.vue
|
||||
RAG vs 微调对比演示
|
||||
|
||||
用途:
|
||||
并排对比 RAG 和微调两种方案的优劣势,
|
||||
帮助用户理解何时选择哪种方案。
|
||||
|
||||
交互功能:
|
||||
- 切换不同维度的对比
|
||||
- 场景选择器:根据需求推荐方案
|
||||
-->
|
||||
<template>
|
||||
<div class="rag-vs-ft-demo">
|
||||
<div class="toggle-bar">
|
||||
<button
|
||||
:class="['toggle-btn', { active: view === 'compare' }]"
|
||||
@click="view = 'compare'"
|
||||
>
|
||||
维度对比
|
||||
</button>
|
||||
<button
|
||||
:class="['toggle-btn', { active: view === 'scenario' }]"
|
||||
@click="view = 'scenario'"
|
||||
>
|
||||
场景推荐
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="view === 'compare'"
|
||||
class="compare-view"
|
||||
>
|
||||
<div class="compare-header">
|
||||
<div class="col-label rag-label">RAG 检索增强生成</div>
|
||||
<div class="col-label vs-label">VS</div>
|
||||
<div class="col-label ft-label">Fine-tuning 微调</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(dim, i) in dimensions"
|
||||
:key="i"
|
||||
class="compare-row"
|
||||
>
|
||||
<div class="dim-name">{{ dim.name }}</div>
|
||||
<div class="dim-content">
|
||||
<div class="rag-side">
|
||||
<div class="score-bar">
|
||||
<div
|
||||
class="score-fill rag-fill"
|
||||
:style="{ width: dim.ragScore + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="side-text">{{ dim.ragText }}</div>
|
||||
</div>
|
||||
<div class="dim-icon">{{ dim.icon }}</div>
|
||||
<div class="ft-side">
|
||||
<div class="score-bar">
|
||||
<div
|
||||
class="score-fill ft-fill"
|
||||
:style="{ width: dim.ftScore + '%' }"
|
||||
/>
|
||||
</div>
|
||||
<div class="side-text">{{ dim.ftText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="view === 'scenario'"
|
||||
class="scenario-view"
|
||||
>
|
||||
<div class="scenario-question">你的需求是什么?</div>
|
||||
<div class="scenario-grid">
|
||||
<div
|
||||
v-for="(s, i) in scenarios"
|
||||
:key="i"
|
||||
:class="['scenario-card', { selected: selectedScenario === i }]"
|
||||
@click="selectedScenario = i"
|
||||
>
|
||||
<div class="scenario-icon">{{ s.icon }}</div>
|
||||
<div class="scenario-name">{{ s.name }}</div>
|
||||
<div class="scenario-desc">{{ s.desc }}</div>
|
||||
<div :class="['recommendation', s.recommend]">
|
||||
{{ s.recommend === 'rag' ? '推荐 RAG' : s.recommend === 'ft' ? '推荐微调' : '两者结合' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="selectedScenario !== null"
|
||||
class="scenario-detail"
|
||||
>
|
||||
<div class="detail-title">{{ scenarios[selectedScenario].name }} — 详细分析</div>
|
||||
<div class="detail-reason">{{ scenarios[selectedScenario].reason }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-box">
|
||||
<div class="summary-title">一句话总结</div>
|
||||
<div class="summary-text">
|
||||
RAG 像是给模型配了一个<strong>实时更新的参考书库</strong>,适合知识频繁变化的场景;
|
||||
微调像是让模型<strong>上了一门专业课</strong>,适合需要特定风格或领域深度的场景。
|
||||
实际项目中,两者常常结合使用。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const view = ref('compare')
|
||||
const selectedScenario = ref(null)
|
||||
|
||||
const dimensions = [
|
||||
{
|
||||
name: '知识更新速度',
|
||||
icon: '⚡',
|
||||
ragScore: 95,
|
||||
ragText: '实时更新,修改文档即生效',
|
||||
ftScore: 25,
|
||||
ftText: '需要重新训练,周期长'
|
||||
},
|
||||
{
|
||||
name: '实施成本',
|
||||
icon: '💰',
|
||||
ragScore: 80,
|
||||
ragText: '搭建检索系统,成本适中',
|
||||
ftScore: 35,
|
||||
ftText: '需要 GPU 资源和标注数据'
|
||||
},
|
||||
{
|
||||
name: '回答风格控制',
|
||||
icon: '🎨',
|
||||
ragScore: 40,
|
||||
ragText: '依赖 Prompt 工程',
|
||||
ftScore: 90,
|
||||
ftText: '可深度定制输出风格'
|
||||
},
|
||||
{
|
||||
name: '幻觉控制',
|
||||
icon: '🎯',
|
||||
ragScore: 85,
|
||||
ragText: '有据可查,可追溯来源',
|
||||
ftScore: 50,
|
||||
ftText: '仍可能产生幻觉'
|
||||
},
|
||||
{
|
||||
name: '推理延迟',
|
||||
icon: '⏱️',
|
||||
ragScore: 55,
|
||||
ragText: '需要额外的检索步骤',
|
||||
ftScore: 85,
|
||||
ftText: '直接生成,无额外开销'
|
||||
},
|
||||
{
|
||||
name: '私有数据安全',
|
||||
icon: '🔒',
|
||||
ragScore: 90,
|
||||
ragText: '数据留在本地,不进入模型',
|
||||
ftScore: 45,
|
||||
ftText: '数据融入模型权重'
|
||||
}
|
||||
]
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
icon: '📚',
|
||||
name: '企业知识库问答',
|
||||
desc: '内部文档、政策、FAQ 等频繁更新的知识',
|
||||
recommend: 'rag',
|
||||
reason: '企业知识库的内容更新频繁,使用 RAG 可以在文档更新后立即生效,无需重新训练。同时数据留在本地,满足企业数据安全要求。'
|
||||
},
|
||||
{
|
||||
icon: '🏥',
|
||||
name: '医疗报告生成',
|
||||
desc: '需要严格遵循特定格式和术语的专业文档',
|
||||
recommend: 'ft',
|
||||
reason: '医疗报告有严格的格式要求和专业术语规范,微调可以让模型深度学习这些模式,生成更符合行业标准的内容。'
|
||||
},
|
||||
{
|
||||
icon: '💬',
|
||||
name: '客服对话系统',
|
||||
desc: '需要准确回答产品问题,同时保持品牌语调',
|
||||
recommend: 'both',
|
||||
reason: '客服系统需要 RAG 来检索最新的产品信息和解决方案,同时需要微调来保持一致的品牌语调和对话风格。两者结合效果最佳。'
|
||||
},
|
||||
{
|
||||
icon: '📰',
|
||||
name: '实时新闻摘要',
|
||||
desc: '需要基于最新信息生成摘要',
|
||||
recommend: 'rag',
|
||||
reason: '新闻内容实时变化,RAG 可以检索最新的新闻源并生成摘要,而微调无法跟上信息更新的速度。'
|
||||
},
|
||||
{
|
||||
icon: '✍️',
|
||||
name: '特定风格写作',
|
||||
desc: '模仿特定作者或品牌的写作风格',
|
||||
recommend: 'ft',
|
||||
reason: '写作风格是一种内化的模式,通过微调让模型学习大量风格样本,能更自然地模仿目标风格,RAG 难以实现这种深层次的风格迁移。'
|
||||
},
|
||||
{
|
||||
icon: '🔬',
|
||||
name: '科研文献助手',
|
||||
desc: '基于海量论文回答学术问题',
|
||||
recommend: 'rag',
|
||||
reason: '科研文献数量庞大且持续增长,RAG 可以动态检索相关论文片段,并提供引用来源,便于研究者验证和追溯。'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rag-vs-ft-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
.toggle-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.toggle-btn {
|
||||
padding: 8px 20px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.toggle-btn.active {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
.compare-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.col-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.rag-label {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
.vs-label {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 16px;
|
||||
}
|
||||
.ft-label {
|
||||
background: #fce7f3;
|
||||
color: #db2777;
|
||||
}
|
||||
.compare-row {
|
||||
margin-bottom: 14px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.dim-name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.dim-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.rag-side,
|
||||
.ft-side {
|
||||
flex: 1;
|
||||
}
|
||||
.dim-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.score-bar {
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.score-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.rag-fill {
|
||||
background: #3b82f6;
|
||||
}
|
||||
.ft-fill {
|
||||
background: #ec4899;
|
||||
}
|
||||
.side-text {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.scenario-question {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.scenario-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.scenario-card {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
.scenario-card.selected {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
box-shadow: 0 0 0 3px rgba(100, 108, 255, 0.15);
|
||||
}
|
||||
.scenario-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.scenario-name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.scenario-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.recommendation {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.recommendation.rag {
|
||||
background: #dbeafe;
|
||||
color: #2563eb;
|
||||
}
|
||||
.recommendation.ft {
|
||||
background: #fce7f3;
|
||||
color: #db2777;
|
||||
}
|
||||
.recommendation.both {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
.scenario-detail {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.detail-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-brand-1);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.detail-reason {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.summary-box {
|
||||
margin-top: 16px;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border: 1px solid var(--vp-c-brand-1);
|
||||
}
|
||||
.summary-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-brand-1);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.summary-text {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,549 @@
|
||||
<!--
|
||||
RetrievalDemo.vue
|
||||
检索过程可视化演示
|
||||
|
||||
用途:
|
||||
展示 RAG 中的检索流程:查询编码 → 向量搜索 → 重排序 → Top-K 选择
|
||||
用户可以输入查询,观察检索过程。
|
||||
|
||||
交互功能:
|
||||
- 选择示例查询
|
||||
- 观察向量相似度计算过程
|
||||
- 查看重排序效果
|
||||
-->
|
||||
<template>
|
||||
<div class="retrieval-demo">
|
||||
<div class="query-section">
|
||||
<span class="label">选择查询:</span>
|
||||
<div class="query-options">
|
||||
<button
|
||||
v-for="(q, i) in queries"
|
||||
:key="i"
|
||||
:class="['q-btn', { active: currentQuery === i }]"
|
||||
@click="selectQuery(i)"
|
||||
>
|
||||
{{ q.text }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="process-steps">
|
||||
<div
|
||||
v-for="(step, i) in steps"
|
||||
:key="i"
|
||||
:class="['step', { active: currentStep >= i, current: currentStep === i }]"
|
||||
@click="currentStep = i"
|
||||
>
|
||||
<div class="step-num">{{ i + 1 }}</div>
|
||||
<div class="step-name">{{ step.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-detail">
|
||||
<div class="step-title">{{ steps[currentStep].name }}</div>
|
||||
<div class="step-desc">{{ steps[currentStep].desc }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Query Embedding -->
|
||||
<div v-if="currentStep === 0" class="embedding-viz">
|
||||
<div class="embed-label">查询文本</div>
|
||||
<div class="embed-text">{{ queries[currentQuery].text }}</div>
|
||||
<div class="embed-arrow">↓ 嵌入模型编码</div>
|
||||
<div class="embed-label">查询向量</div>
|
||||
<div class="vector-display">
|
||||
<span
|
||||
v-for="(v, i) in queries[currentQuery].vector"
|
||||
:key="i"
|
||||
class="vector-val"
|
||||
>{{ v }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Vector Search -->
|
||||
<div v-if="currentStep === 1" class="search-viz">
|
||||
<div class="doc-list">
|
||||
<div
|
||||
v-for="(doc, i) in activeQuery.candidates"
|
||||
:key="i"
|
||||
class="doc-row"
|
||||
>
|
||||
<div class="doc-text-col">{{ doc.text }}</div>
|
||||
<div class="similarity-col">
|
||||
<div class="sim-bar-bg">
|
||||
<div
|
||||
class="sim-bar-fill"
|
||||
:style="{
|
||||
width: (doc.similarity * 100) + '%',
|
||||
background: getSimColor(doc.similarity)
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<span class="sim-value">{{ doc.similarity.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Re-ranking -->
|
||||
<div v-if="currentStep === 2" class="rerank-viz">
|
||||
<div class="rerank-columns">
|
||||
<div class="rerank-col">
|
||||
<div class="col-title">初始排序(向量相似度)</div>
|
||||
<div
|
||||
v-for="(doc, i) in sortedBySimilarity"
|
||||
:key="'init-' + i"
|
||||
class="rerank-item"
|
||||
>
|
||||
<span class="rank-badge">#{{ i + 1 }}</span>
|
||||
<span class="rerank-text">{{ doc.text }}</span>
|
||||
<span class="rerank-score">{{ doc.similarity.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rerank-arrow-col">→</div>
|
||||
<div class="rerank-col">
|
||||
<div class="col-title">重排序后(交叉编码器)</div>
|
||||
<div
|
||||
v-for="(doc, i) in reranked"
|
||||
:key="'re-' + i"
|
||||
class="rerank-item"
|
||||
>
|
||||
<span class="rank-badge highlight">#{{ i + 1 }}</span>
|
||||
<span class="rerank-text">{{ doc.text }}</span>
|
||||
<span class="rerank-score">{{ doc.rerankScore.toFixed(2) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Top-K Selection -->
|
||||
<div v-if="currentStep === 3" class="topk-viz">
|
||||
<div class="topk-setting">
|
||||
<span>Top-K 值:</span>
|
||||
<button
|
||||
v-for="k in [1, 2, 3]"
|
||||
:key="k"
|
||||
:class="['k-btn', { active: topK === k }]"
|
||||
@click="topK = k"
|
||||
>
|
||||
K = {{ k }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="topk-results">
|
||||
<div
|
||||
v-for="(doc, i) in topKResults"
|
||||
:key="i"
|
||||
:class="['topk-item', { selected: i < topK }]"
|
||||
>
|
||||
<span class="topk-rank">#{{ i + 1 }}</span>
|
||||
<span class="topk-text">{{ doc.text }}</span>
|
||||
<span
|
||||
v-if="i < topK"
|
||||
class="topk-badge"
|
||||
>已选中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-controls">
|
||||
<button
|
||||
class="nav-btn"
|
||||
:disabled="currentStep <= 0"
|
||||
@click="currentStep--"
|
||||
>
|
||||
← 上一步
|
||||
</button>
|
||||
<button
|
||||
class="nav-btn primary"
|
||||
:disabled="currentStep >= steps.length - 1"
|
||||
@click="currentStep++"
|
||||
>
|
||||
下一步 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const currentQuery = ref(0)
|
||||
const currentStep = ref(0)
|
||||
const topK = ref(2)
|
||||
|
||||
const steps = [
|
||||
{ name: '查询编码', desc: '将用户的自然语言查询通过嵌入模型(如 text-embedding-ada-002)转化为高维向量表示。这个向量捕捉了查询的语义信息。' },
|
||||
{ name: '向量搜索', desc: '在向量数据库中计算查询向量与所有文档向量的余弦相似度,找出语义最接近的候选文档。' },
|
||||
{ name: '重排序', desc: '使用交叉编码器(Cross-Encoder)对候选文档进行精细排序。交叉编码器同时考虑查询和文档的交互信息,排序更准确。' },
|
||||
{ name: 'Top-K 选择', desc: '从重排序后的结果中选取前 K 个最相关的文档片段,作为 LLM 生成回答的上下文。K 值的选择需要平衡准确性和上下文长度。' }
|
||||
]
|
||||
|
||||
const queries = [
|
||||
{
|
||||
text: '如何申请年假?',
|
||||
vector: [0.12, -0.45, 0.78, 0.33, -0.21, 0.56, 0.89, -0.14],
|
||||
candidates: [
|
||||
{ text: '员工年假申请需提前 3 个工作日提交审批流程', similarity: 0.94, rerankScore: 0.97 },
|
||||
{ text: '年假天数根据工龄计算:1-5年10天,5年以上15天', similarity: 0.88, rerankScore: 0.91 },
|
||||
{ text: '病假需提供医院开具的诊断证明', similarity: 0.62, rerankScore: 0.35 },
|
||||
{ text: '未使用的年假可折算为工资补偿', similarity: 0.79, rerankScore: 0.82 },
|
||||
{ text: '公司茶水间提供免费咖啡和零食', similarity: 0.15, rerankScore: 0.05 }
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Redis 缓存穿透怎么解决?',
|
||||
vector: [0.67, 0.23, -0.89, 0.45, 0.11, -0.34, 0.72, 0.56],
|
||||
candidates: [
|
||||
{ text: '缓存穿透可通过布隆过滤器拦截不存在的 key', similarity: 0.96, rerankScore: 0.98 },
|
||||
{ text: '对空值也进行缓存,设置较短的 TTL', similarity: 0.89, rerankScore: 0.93 },
|
||||
{ text: '缓存雪崩是指大量 key 同时过期导致数据库压力骤增', similarity: 0.71, rerankScore: 0.42 },
|
||||
{ text: 'Redis 支持主从复制和哨兵模式实现高可用', similarity: 0.58, rerankScore: 0.28 },
|
||||
{ text: '接口限流可以使用令牌桶或漏桶算法', similarity: 0.43, rerankScore: 0.15 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const activeQuery = computed(() => queries[currentQuery.value])
|
||||
|
||||
const sortedBySimilarity = computed(() =>
|
||||
[...activeQuery.value.candidates].sort((a, b) => b.similarity - a.similarity)
|
||||
)
|
||||
|
||||
const reranked = computed(() =>
|
||||
[...activeQuery.value.candidates].sort((a, b) => b.rerankScore - a.rerankScore)
|
||||
)
|
||||
|
||||
const topKResults = computed(() => reranked.value)
|
||||
|
||||
function selectQuery(i) {
|
||||
currentQuery.value = i
|
||||
currentStep.value = 0
|
||||
}
|
||||
|
||||
function getSimColor(sim) {
|
||||
if (sim >= 0.8) return '#10b981'
|
||||
if (sim >= 0.5) return '#f59e0b'
|
||||
return '#ef4444'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.retrieval-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 16px 0;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
.query-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.query-section .label {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.query-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.q-btn {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.q-btn.active {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
.process-steps {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.step {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.step.active { opacity: 1; }
|
||||
.step.current {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
.step-num {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.step.current .step-num {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
}
|
||||
.step-name { font-size: 12px; }
|
||||
.step-detail {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-brand-1);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.step-desc {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
/* Embedding visualization */
|
||||
.embedding-viz {
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.embed-label {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.embed-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand-1);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.embed-arrow {
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin: 8px 0;
|
||||
}
|
||||
.vector-display {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.vector-val {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
/* Search visualization */
|
||||
.search-viz { margin-bottom: 16px; }
|
||||
.doc-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.doc-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
.doc-text-col {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.similarity-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 140px;
|
||||
}
|
||||
.sim-bar-bg {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sim-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.5s;
|
||||
}
|
||||
.sim-value {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
min-width: 32px;
|
||||
}
|
||||
/* Reranking visualization */
|
||||
.rerank-viz { margin-bottom: 16px; }
|
||||
.rerank-columns {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.rerank-col { flex: 1; }
|
||||
.rerank-arrow-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24px;
|
||||
color: var(--vp-c-brand-1);
|
||||
padding-top: 40px;
|
||||
}
|
||||
.col-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.rerank-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
font-size: 12px;
|
||||
}
|
||||
.rank-badge {
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-divider);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rank-badge.highlight {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
}
|
||||
.rerank-text {
|
||||
flex: 1;
|
||||
color: var(--vp-c-text-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rerank-score {
|
||||
font-family: monospace;
|
||||
color: var(--vp-c-text-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Top-K visualization */
|
||||
.topk-viz { margin-bottom: 16px; }
|
||||
.topk-setting {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.k-btn {
|
||||
padding: 4px 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.k-btn.active {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
.topk-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.topk-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.topk-item.selected {
|
||||
border-color: var(--vp-c-brand-1);
|
||||
opacity: 1;
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
.topk-rank {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
.topk-text {
|
||||
flex: 1;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.topk-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
}
|
||||
/* Navigation */
|
||||
.nav-controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.nav-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.nav-btn.primary {
|
||||
background: var(--vp-c-brand-1);
|
||||
color: #fff;
|
||||
border-color: var(--vp-c-brand-1);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user