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:
sanbuphy
2026-02-24 18:22:58 +08:00
parent b5a55811cc
commit 3af119a598
86 changed files with 20311 additions and 340 deletions
+8
View File
@@ -573,6 +573,14 @@ export default defineConfig({
text: '一、计算机是怎么回事',
collapsed: false,
items: [
{
text: 'Vibe Coding 时代下的全栈开发',
link: '/zh-cn/appendix/1-computer-fundamentals/vibe-coding-fullstack'
},
{
text: '从按下电源到访问网站发生了什么',
link: '/zh-cn/appendix/1-computer-fundamentals/power-on-to-web'
},
{
text: '从晶体管到 CPU',
link: '/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu'
+2 -2
View File
@@ -466,7 +466,7 @@ watch(sidebarCollapsed, (collapsed) => {
font-size: 18px;
font-weight: 500;
white-space: pre-wrap;
color: #000000;
color: var(--vp-c-text-2);
min-height: 28px;
display: flex;
/* 居中对齐 */
@@ -488,7 +488,7 @@ watch(sidebarCollapsed, (collapsed) => {
margin-right: auto;
}
.VPHomeHero .text {
color: #000000 !important;
color: var(--vp-c-text-1) !important;
}
.VPHomeHero .actions {
justify-content: center;
@@ -1970,7 +1970,7 @@ a {
.nav-title {
font-weight: 600;
font-size: 19px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
flex-shrink: 0;
letter-spacing: -0.01em;
}
@@ -1987,7 +1987,7 @@ a {
background: none;
border: none;
font-size: 12px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
cursor: pointer;
transition: opacity 0.2s;
padding: 0;
@@ -2000,7 +2000,7 @@ a {
.nav-links button:hover,
.nav-links button.active,
.nav-link-item:hover {
color: #000 !important;
color: var(--vp-c-text-1) !important;
opacity: 1;
}
@@ -2021,7 +2021,7 @@ a {
background: none;
border: none;
padding: 0;
color: #000 !important;
color: var(--vp-c-text-1) !important;
cursor: pointer;
display: flex;
align-items: center;
@@ -2038,7 +2038,7 @@ a {
background: none;
border: none;
padding: 0;
color: #000 !important;
color: var(--vp-c-text-1) !important;
cursor: pointer;
display: flex;
align-items: center;
@@ -2060,13 +2060,13 @@ a {
.button .option-icon {
width: 20px;
height: 20px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
}
.button .text-icon {
width: 14px;
height: 14px;
color: #000 !important;
color: var(--vp-c-text-1) !important;
}
/* GitHub Stars Styles */
@@ -2076,7 +2076,7 @@ a {
}
:deep(.nav-github-stars .github-stars-link) {
color: #000 !important;
color: var(--vp-c-text-1) !important;
display: flex;
align-items: center;
gap: 4px;
@@ -2134,8 +2134,8 @@ a {
}
.buy-btn {
background: #000;
color: #fff !important;
background: var(--vp-c-text-1);
color: var(--vp-c-bg) !important;
padding: 4px 12px;
border-radius: 980px;
font-size: 12px;
@@ -0,0 +1,245 @@
<template>
<div class="flow-demo">
<div class="header">
<div class="title">AI 应用请求处理流程</div>
<div class="subtitle">点击"发送请求"观察一次 AI 请求的完整生命周期</div>
</div>
<div class="pipeline">
<div
v-for="(step, idx) in steps"
:key="step.id"
:class="['pipe-step', {
active: currentStep === idx,
done: currentStep > idx
}]"
>
<div class="step-icon">{{ currentStep > idx ? '✅' : step.icon }}</div>
<div class="step-info">
<div class="step-name">{{ step.name }}</div>
<div class="step-en">{{ step.en }}</div>
</div>
<div v-if="idx < steps.length - 1" class="arrow"></div>
</div>
</div>
<div class="control-bar">
<button
v-if="!isRunning && currentStep < 0"
class="action-btn"
@click="startFlow"
>
发送请求
</button>
<button
v-else-if="!isRunning && currentStep >= steps.length"
class="action-btn reset"
@click="resetFlow"
>
🔄 重置
</button>
<div v-else-if="isRunning" class="running-hint">
处理中...
</div>
</div>
<div v-if="currentStep >= 0" class="detail-area">
<div class="detail-card">
<div class="detail-title">
{{ activeStep.icon }} {{ activeStep.name }}
</div>
<div class="detail-desc">{{ activeStep.detail }}</div>
<div class="io-section">
<div class="io-block">
<div class="io-label">输入</div>
<pre class="io-code"><code>{{ activeStep.input }}</code></pre>
</div>
<div class="io-block">
<div class="io-label">输出</div>
<pre class="io-code"><code>{{ activeStep.output }}</code></pre>
</div>
</div>
<div class="latency-bar">
<span class="latency-label">耗时</span>
<div class="latency-track">
<div
class="latency-fill"
:style="{ width: activeStep.latencyPct + '%' }"
/>
</div>
<span class="latency-val">{{ activeStep.latency }}</span>
</div>
</div>
</div>
<div class="insight-bar">
<span class="insight-label">💡 关键洞察</span>
<span class="insight-text">
AI 应用的请求链路比传统应用更长模型推理通常占总耗时的 60-80%
优化重点在于Prompt 缓存流式输出异步处理
</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const steps = [
{
id: 'input', icon: '👤', name: '用户输入', en: 'User Input',
detail: '用户通过自然语言输入请求。系统需要处理多种输入形式:文本、语音转文字、图片描述等。与传统应用的表单提交不同,输入是开放式的、非结构化的。',
input: '"帮我总结这篇文章的核心观点"',
output: '{ text: "帮我总结...", type: "text", lang: "zh" }',
latency: '~0ms', latencyPct: 2
},
{
id: 'preprocess', icon: '🔧', name: '预处理', en: 'Preprocessing',
detail: '对用户输入进行清洗和增强:意图识别、关键词提取、上下文拼接、RAG 检索相关文档片段、构建完整的 Prompt。这一步决定了模型能获得多少有效信息。',
input: '{ text: "帮我总结...", context: [...历史对话] }',
output: '{ system_prompt: "你是...", user_prompt: "...", retrieved_docs: [...] }',
latency: '~200ms', latencyPct: 15
},
{
id: 'model', icon: '🧠', name: '模型推理', en: 'Model Inference',
detail: '将构建好的 Prompt 发送给大语言模型进行推理。这是整个链路中耗时最长的环节。模型会根据 Prompt 中的指令、上下文和检索到的知识,生成回答。',
input: '{ messages: [...], model: "gpt-4", temperature: 0.7 }',
output: '{ content: "这篇文章的核心观点有三个...", tokens: 256 }',
latency: '~2-8s', latencyPct: 75
},
{
id: 'postprocess', icon: '🛡️', name: '后处理', en: 'Post-processing',
detail: '对模型输出进行安全检查和格式化:内容审核过滤、幻觉检测、格式转换(Markdown 渲染)、引用来源标注、敏感信息脱敏等。',
input: '{ raw_output: "这篇文章的核心观点有三个..." }',
output: '{ safe: true, formatted: "## 核心观点\\n1. ...", sources: [...] }',
latency: '~100ms', latencyPct: 8
},
{
id: 'response', icon: '💬', name: '响应输出', en: 'Response',
detail: '将处理后的结果以流式方式返回给用户。前端逐步渲染 Markdown 内容,同时展示引用来源和置信度。用户可以在生成过程中随时中断或追问。',
input: '{ formatted: "## 核心观点\\n1. ...", stream: true }',
output: '用户看到逐字出现的回答 + 来源引用',
latency: '~50ms (首字节)', latencyPct: 5
}
]
const currentStep = ref(-1)
const isRunning = ref(false)
const activeStep = computed(() => {
const idx = Math.min(currentStep.value, steps.length - 1)
return idx >= 0 ? steps[idx] : steps[0]
})
const startFlow = async () => {
isRunning.value = true
for (let i = 0; i < steps.length; i++) {
currentStep.value = i
await new Promise(r => setTimeout(r, 1200))
}
currentStep.value = steps.length
isRunning.value = false
}
const resetFlow = () => {
currentStep.value = -1
isRunning.value = false
}
</script>
<style scoped>
.flow-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 20px; margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #10b981, #3b82f6);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.pipeline {
display: flex; align-items: center; justify-content: center;
gap: 4px; flex-wrap: wrap; margin-bottom: 16px;
}
.pipe-step {
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); transition: all 0.3s;
font-size: 12px;
}
.pipe-step.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.pipe-step.done {
border-color: #86efac; background: #f0fdf4;
}
.step-icon { font-size: 18px; }
.step-name { font-weight: 600; font-size: 12px; }
.step-en { font-size: 10px; color: var(--vp-c-text-3); }
.arrow { color: var(--vp-c-text-3); font-size: 14px; margin: 0 2px; }
.control-bar { text-align: center; margin-bottom: 16px; }
.action-btn {
padding: 10px 28px; background: var(--vp-c-brand);
color: white; border: none; border-radius: 8px;
font-size: 13px; cursor: pointer; transition: background 0.2s;
}
.action-btn:hover { background: var(--vp-c-brand-dark); }
.action-btn.reset { background: #6b7280; }
.action-btn.reset:hover { background: #4b5563; }
.running-hint { color: var(--vp-c-brand); font-size: 13px; }
.detail-area { margin-bottom: 16px; }
.detail-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 16px;
}
.detail-title { font-weight: 700; font-size: 15px; margin-bottom: 8px; }
.detail-desc {
color: var(--vp-c-text-2); font-size: 13px;
line-height: 1.7; margin-bottom: 12px;
}
.io-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 10px; margin-bottom: 12px;
}
.io-label { font-weight: 600; font-size: 11px; margin-bottom: 4px; color: var(--vp-c-text-2); }
.io-code {
margin: 0; background: #0b1221; color: #e5e7eb;
border-radius: 8px; padding: 10px;
font-family: var(--vp-font-family-mono);
font-size: 11px; overflow-x: auto; white-space: pre-wrap;
}
.latency-bar {
display: flex; align-items: center; gap: 10px;
}
.latency-label { font-size: 11px; font-weight: 600; color: var(--vp-c-text-2); }
.latency-track {
flex: 1; height: 8px; background: var(--vp-c-bg-soft);
border-radius: 4px; overflow: hidden;
}
.latency-fill {
height: 100%; border-radius: 4px;
background: var(--vp-c-brand); transition: width 0.5s;
}
.latency-val { font-size: 11px; font-weight: 600; min-width: 80px; text-align: right; }
.insight-bar {
padding: 12px 16px; background: var(--vp-c-brand-soft);
border-radius: 6px; font-size: 13px;
}
.insight-label { font-weight: 600; color: var(--vp-c-brand-dark); }
.insight-text { color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,222 @@
<template>
<div class="principle-demo">
<div class="header">
<div class="title">AI 原生设计原则</div>
<div class="subtitle">点击卡片深入了解每条设计原则</div>
</div>
<div class="principle-grid">
<div
v-for="p in principles"
:key="p.id"
:class="['principle-card', { active: selected === p.id }]"
@click="selected = p.id"
>
<div class="p-icon">{{ p.icon }}</div>
<div class="p-name">{{ p.name }}</div>
<div class="p-brief">{{ p.brief }}</div>
</div>
</div>
<div v-if="selected" class="detail-panel">
<div class="detail-header">
<span>{{ currentPrinciple.icon }} {{ currentPrinciple.name }}</span>
</div>
<div class="detail-body">
<div class="detail-desc">{{ currentPrinciple.detail }}</div>
<div class="example-section">
<div class="example-title">实践对比</div>
<div class="compare-grid">
<div class="compare-bad">
<div class="compare-label bad-label"> 反面示例</div>
<div class="compare-text">{{ currentPrinciple.bad }}</div>
</div>
<div class="compare-good">
<div class="compare-label good-label"> 正确做法</div>
<div class="compare-text">{{ currentPrinciple.good }}</div>
</div>
</div>
</div>
<div class="checklist">
<div class="checklist-title">检查清单</div>
<div
v-for="(item, idx) in currentPrinciple.checklist"
:key="idx"
:class="['check-item', { checked: checkedItems[selected]?.[idx] }]"
@click="toggleCheck(idx)"
>
<span class="check-box">
{{ checkedItems[selected]?.[idx] ? '☑' : '☐' }}
</span>
<span>{{ item }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive } from 'vue'
const principles = [
{
id: 'graceful',
icon: '🛡️',
name: '优雅降级',
brief: 'AI 失败时,系统仍然可用',
detail: 'AI 模型可能超时、返回错误、产生幻觉。优雅降级意味着:当 AI 不可用时,系统应该有兜底方案,而不是直接崩溃。这是 AI 原生应用与玩具项目的分水岭。',
bad: '模型 API 超时后,页面显示空白错误页,用户只能刷新重试。',
good: '模型超时后,显示缓存的上一次回答或推荐相关文档,同时后台自动重试。',
checklist: [
'设置合理的 API 超时时间(通常 30-60s',
'准备降级方案:缓存、规则引擎、人工转接',
'向用户透明地展示当前状态',
'记录失败日志用于后续优化'
]
},
{
id: 'human',
icon: '🤝',
name: '人机协作',
brief: '关键决策由人类确认',
detail: 'AI 擅长生成和建议,但不应该在高风险场景中自主决策。人机协作(Human-in-the-Loop)模式让 AI 负责草稿和推荐,人类负责审核和确认。',
bad: 'AI 自动发送邮件给客户,内容未经人工审核,导致错误信息传播。',
good: 'AI 生成邮件草稿并高亮不确定的部分,用户审核修改后手动发送。',
checklist: [
'识别哪些操作是"高风险"的(发送、删除、支付)',
'高风险操作前必须有人工确认步骤',
'AI 输出标注置信度,低置信内容需人工复核',
'提供便捷的编辑和修改界面'
]
},
{
id: 'transparent',
icon: '🔍',
name: '透明可解释',
brief: '让用户理解 AI 的推理过程',
detail: 'AI 不是黑盒魔法。用户需要知道 AI 为什么给出这个回答、依据了哪些信息、有多大把握。透明性建立信任,也帮助用户判断何时该相信 AI、何时该质疑。',
bad: 'AI 直接给出一个结论,没有任何解释或来源引用,用户无法判断可靠性。',
good: '回答附带推理过程、引用来源链接、置信度指示,用户可以追溯验证。',
checklist: [
'展示 AI 的推理链路或思考过程',
'标注信息来源和引用',
'显示置信度或不确定性指标',
'提供"为什么这样回答"的解释入口'
]
},
{
id: 'feedback',
icon: '🔄',
name: '反馈闭环',
brief: '用户反馈驱动持续改进',
detail: '每一次用户交互都是改进的机会。通过收集用户对 AI 输出的评价(点赞/点踩、修改记录、追问模式),持续优化 Prompt、微调模型、改进检索策略。',
bad: 'AI 回答错误后,没有任何反馈渠道,同样的错误会反复出现。',
good: '用户可以标记错误回答,系统自动收集并用于优化 Prompt 和检索策略。',
checklist: [
'提供简单的反馈机制(👍👎 按钮)',
'记录用户的修改和追问作为隐式反馈',
'定期分析反馈数据,优化 Prompt 模板',
'建立 A/B 测试机制验证改进效果'
]
}
]
const selected = ref('graceful')
const checkedItems = reactive({})
const currentPrinciple = computed(() =>
principles.find(p => p.id === selected.value) || principles[0]
)
const toggleCheck = (idx) => {
if (!checkedItems[selected.value]) {
checkedItems[selected.value] = {}
}
checkedItems[selected.value][idx] = !checkedItems[selected.value][idx]
}
</script>
<style scoped>
.principle-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 20px; margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #ef4444, #f59e0b);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.principle-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px; margin-bottom: 16px;
}
.principle-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 10px; padding: 14px; cursor: pointer;
transition: all 0.2s; text-align: center;
}
.principle-card:hover { background: var(--vp-c-bg-alt); }
.principle-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.p-icon { font-size: 24px; margin-bottom: 6px; }
.p-name { font-weight: 600; font-size: 13px; }
.p-brief { font-size: 11px; color: var(--vp-c-text-2); margin-top: 4px; }
.detail-panel {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; overflow: hidden;
}
.detail-header {
padding: 14px 16px; font-weight: 700; font-size: 15px;
border-bottom: 1px solid var(--vp-c-divider);
}
.detail-body { padding: 16px; }
.detail-desc {
color: var(--vp-c-text-2); font-size: 13px;
line-height: 1.7; margin-bottom: 16px;
}
.example-section { margin-bottom: 16px; }
.example-title { font-weight: 600; font-size: 13px; margin-bottom: 8px; }
.compare-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
}
.compare-bad, .compare-good {
padding: 12px; border-radius: 8px; font-size: 13px; line-height: 1.6;
}
.compare-bad { background: #fef2f2; border: 1px solid #fecaca; }
.compare-good { background: #f0fdf4; border: 1px solid #bbf7d0; }
.compare-label {
font-weight: 600; font-size: 11px; margin-bottom: 6px;
}
.bad-label { color: #dc2626; }
.good-label { color: #16a34a; }
.compare-text { color: var(--vp-c-text-1); }
.checklist-title { font-weight: 600; font-size: 13px; margin-bottom: 8px; }
.check-item {
display: flex; align-items: center; gap: 8px;
padding: 8px 10px; border-radius: 6px; font-size: 13px;
cursor: pointer; transition: background 0.2s;
border: 1px solid transparent;
}
.check-item:hover { background: var(--vp-c-bg-soft); }
.check-item.checked {
background: #f0fdf4; border-color: #bbf7d0;
text-decoration: line-through; color: var(--vp-c-text-3);
}
.check-box { font-size: 16px; flex-shrink: 0; }
</style>
@@ -0,0 +1,237 @@
<template>
<div class="arch-demo">
<div class="header">
<div class="title">传统应用 vs AI 原生应用</div>
<div class="subtitle">切换视图对比两种架构的核心差异</div>
</div>
<div class="toggle-bar">
<button
:class="['toggle-btn', { active: mode === 'traditional' }]"
@click="mode = 'traditional'"
>
<span>🏗</span>
<span>传统应用</span>
</button>
<button
:class="['toggle-btn', { active: mode === 'ai-native' }]"
@click="mode = 'ai-native'"
>
<span>🤖</span>
<span>AI 原生应用</span>
</button>
</div>
<div class="arch-grid">
<div class="stack">
<div class="stack-title">{{ currentArch.label }}</div>
<div
v-for="(layer, idx) in currentArch.layers"
:key="idx"
:class="['layer', { highlight: selectedLayer === idx }]"
:style="{ borderLeftColor: layer.color }"
@click="selectedLayer = idx"
>
<div class="layer-icon">{{ layer.icon }}</div>
<div class="layer-info">
<div class="layer-name">{{ layer.name }}</div>
<div class="layer-desc">{{ layer.brief }}</div>
</div>
</div>
</div>
<div class="detail-panel">
<div v-if="selectedLayer !== null" class="detail-content">
<div class="detail-title">
{{ currentArch.layers[selectedLayer].icon }}
{{ currentArch.layers[selectedLayer].name }}
</div>
<div class="detail-desc">
{{ currentArch.layers[selectedLayer].detail }}
</div>
<div class="detail-example">
<div class="example-label">典型技术</div>
<div class="tech-tags">
<span
v-for="t in currentArch.layers[selectedLayer].techs"
:key="t"
class="tech-tag"
>{{ t }}</span>
</div>
</div>
</div>
<div v-else class="detail-placeholder">
👆 点击左侧层级查看详情
</div>
</div>
</div>
<div class="comparison-bar">
<span class="compare-label">💡 核心区别</span>
<span class="compare-text">{{ mode === 'traditional'
? '传统应用的逻辑由开发者用 if/else 硬编码,行为完全确定。'
: 'AI 原生应用的核心逻辑由模型驱动,行为具有概率性,需要全新的设计思维。' }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const mode = ref('traditional')
const selectedLayer = ref(0)
const architectures = {
traditional: {
label: '传统应用架构',
layers: [
{
icon: '🖥️', name: '前端 UI', color: '#3b82f6',
brief: '用户界面与交互',
detail: '基于确定性的表单、按钮、页面路由。用户操作触发固定的业务流程,所有交互路径在开发时已经确定。',
techs: ['React', 'Vue', 'HTML/CSS']
},
{
icon: '⚙️', name: '业务逻辑层', color: '#8b5cf6',
brief: '硬编码的规则引擎',
detail: '开发者用 if/else、switch/case 编写所有业务规则。每一条路径都需要人工预设,无法处理规则之外的情况。',
techs: ['Node.js', 'Java', 'Python']
},
{
icon: '🗄️', name: '数据存储', color: '#06b6d4',
brief: '结构化数据管理',
detail: '关系型数据库存储结构化数据,Schema 固定。数据的读写遵循严格的 CRUD 模式。',
techs: ['MySQL', 'PostgreSQL', 'Redis']
},
{
icon: '🔌', name: 'API 接口', color: '#10b981',
brief: '固定的请求/响应',
detail: '每个 API 端点返回确定性的结果。相同的输入永远产生相同的输出,行为完全可预测。',
techs: ['REST', 'GraphQL', 'gRPC']
}
]
},
'ai-native': {
label: 'AI 原生应用架构',
layers: [
{
icon: '💬', name: '自然语言交互层', color: '#f59e0b',
brief: '对话式 + 流式输出',
detail: '用户通过自然语言表达意图,系统以流式方式逐步生成响应。交互不再是固定的表单,而是开放式的对话。',
techs: ['Streaming UI', 'Markdown 渲染', 'SSE']
},
{
icon: '🧠', name: '模型推理层', color: '#ef4444',
brief: 'LLM 驱动的决策引擎',
detail: '核心逻辑不再是 if/else,而是由大语言模型根据 Prompt 和上下文进行推理。输出具有概率性,同样的输入可能产生不同的结果。',
techs: ['GPT-4', 'Claude', 'Prompt 工程']
},
{
icon: '🔗', name: '编排与工具层', color: '#8b5cf6',
brief: 'Agent 编排 + 工具调用',
detail: '模型可以调用外部工具(搜索、数据库、API)来获取实时信息。编排层负责管理多步推理、工具选择和结果整合。',
techs: ['LangChain', 'Function Calling', 'RAG']
},
{
icon: '📦', name: '上下文管理层', color: '#06b6d4',
brief: '向量数据库 + 记忆系统',
detail: '使用向量数据库存储和检索非结构化知识。通过 Embedding 将文本转化为语义向量,实现基于含义的搜索而非关键词匹配。',
techs: ['Pinecone', 'ChromaDB', 'Embedding']
},
{
icon: '🛡️', name: '安全与护栏层', color: '#10b981',
brief: '输出过滤 + 幻觉检测',
detail: 'AI 输出不可完全信任,需要护栏机制:内容过滤、事实核查、幻觉检测、敏感信息脱敏等。这是传统应用不需要的全新层级。',
techs: ['Guardrails', '内容审核', '事实校验']
}
]
}
}
const currentArch = computed(() => architectures[mode.value])
</script>
<style scoped>
.arch-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, var(--vp-c-brand), #f59e0b);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.toggle-bar {
display: flex; gap: 8px; justify-content: center; margin-bottom: 16px;
}
.toggle-btn {
display: flex; align-items: center; gap: 6px;
padding: 8px 18px; border: 1px solid var(--vp-c-divider);
border-radius: 20px; background: var(--vp-c-bg);
cursor: pointer; transition: all 0.2s; font-size: 13px;
}
.toggle-btn:hover { background: var(--vp-c-bg-alt); }
.toggle-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.arch-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.stack {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 12px;
display: flex; flex-direction: column; gap: 8px;
}
.stack-title { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
.layer {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border-radius: 8px;
border: 1px solid var(--vp-c-divider);
border-left: 3px solid; background: var(--vp-c-bg);
cursor: pointer; transition: all 0.2s;
}
.layer:hover { background: var(--vp-c-bg-alt); }
.layer.highlight {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.layer-icon { font-size: 20px; flex-shrink: 0; }
.layer-name { font-weight: 600; font-size: 13px; }
.layer-desc { font-size: 11px; color: var(--vp-c-text-2); margin-top: 2px; }
.detail-panel {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 16px;
}
.detail-title { font-weight: 700; font-size: 15px; margin-bottom: 10px; }
.detail-desc { color: var(--vp-c-text-2); line-height: 1.7; font-size: 13px; margin-bottom: 12px; }
.example-label { font-weight: 600; font-size: 12px; margin-bottom: 6px; }
.tech-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.tech-tag {
padding: 3px 10px; border-radius: 12px; font-size: 11px;
background: var(--vp-c-brand-soft); color: var(--vp-c-brand-dark);
border: 1px solid var(--vp-c-brand-dimm);
}
.detail-placeholder {
color: var(--vp-c-text-3); text-align: center; padding: 40px 0; font-size: 13px;
}
.comparison-bar {
margin-top: 16px; padding: 12px 16px;
background: var(--vp-c-brand-soft); border-radius: 6px; font-size: 13px;
}
.compare-label { font-weight: 600; color: var(--vp-c-brand-dark); }
.compare-text { color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,301 @@
<template>
<div class="ux-demo">
<div class="header">
<div class="title">AI 原生交互模式</div>
<div class="subtitle">点击卡片体验每种 AI 交互模式的效果</div>
</div>
<div class="pattern-grid">
<div
v-for="p in patterns"
:key="p.id"
:class="['pattern-card', { active: activePattern === p.id }]"
@click="activatePattern(p.id)"
>
<div class="card-icon">{{ p.icon }}</div>
<div class="card-name">{{ p.name }}</div>
<div class="card-desc">{{ p.brief }}</div>
</div>
</div>
<div v-if="activePattern" class="preview-area">
<div class="preview-header">
<span>{{ currentPattern.icon }} {{ currentPattern.name }} 演示</span>
<button class="replay-btn" @click="replayDemo">🔄 重播</button>
</div>
<!-- 流式输出演示 -->
<div v-if="activePattern === 'streaming'" class="demo-box">
<div class="chat-bubble ai">
<span class="stream-text">{{ streamText }}</span>
<span v-if="isStreaming" class="cursor-blink">|</span>
</div>
<div class="demo-note">逐字输出用户无需等待完整响应</div>
</div>
<!-- 加载状态演示 -->
<div v-if="activePattern === 'loading'" class="demo-box">
<div class="loading-stages">
<div
v-for="(s, idx) in loadingStages"
:key="idx"
:class="['stage', { done: loadingStep > idx, current: loadingStep === idx }]"
>
<span class="stage-icon">
{{ loadingStep > idx ? '✅' : loadingStep === idx ? '⏳' : '⬜' }}
</span>
<span>{{ s }}</span>
</div>
</div>
<div class="demo-note">分阶段展示进度而非单一的"加载中"</div>
</div>
<!-- 置信度指示器演示 -->
<div v-if="activePattern === 'confidence'" class="demo-box">
<div class="confidence-list">
<div v-for="c in confidenceItems" :key="c.text" class="conf-item">
<div class="conf-bar-wrap">
<div
class="conf-bar"
:style="{ width: c.score + '%', background: c.color }"
/>
</div>
<div class="conf-score">{{ c.score }}%</div>
<div class="conf-label">{{ c.level }}</div>
<div class="conf-text">{{ c.text }}</div>
</div>
</div>
<div class="demo-note">让用户知道 AI 对自己的回答有多"确定"</div>
</div>
<!-- 降级处理演示 -->
<div v-if="activePattern === 'fallback'" class="demo-box">
<div class="fallback-flow">
<div :class="['fb-step', { active: fallbackStep >= 0 }]">
<span class="fb-icon">🤖</span>
<span>AI 尝试回答...</span>
</div>
<div class="fb-arrow" v-if="fallbackStep >= 1"> 检测到不确定</div>
<div :class="['fb-step warn', { active: fallbackStep >= 1 }]">
<span class="fb-icon"></span>
<span>提示用户此回答可能不准确</span>
</div>
<div class="fb-arrow" v-if="fallbackStep >= 2"> 提供替代方案</div>
<div :class="['fb-step safe', { active: fallbackStep >= 2 }]">
<span class="fb-icon">🔄</span>
<span>转接人工 / 推荐文档 / 换个方式提问</span>
</div>
</div>
<div class="demo-note">AI 不确定时优雅降级而非强行回答</div>
</div>
<div class="pattern-detail">
<div class="detail-label">设计要点</div>
<div class="detail-text">{{ currentPattern.detail }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const patterns = [
{
id: 'streaming', icon: '💬', name: '流式输出',
brief: '逐字生成,即时反馈',
detail: '流式输出让用户在 AI 思考时就能看到部分结果,大幅降低感知等待时间。技术上通过 SSEServer-Sent Events)或 WebSocket 实现,前端逐步渲染 Markdown 内容。'
},
{
id: 'loading', icon: '⏳', name: '智能加载态',
brief: '分阶段展示处理进度',
detail: 'AI 请求通常需要数秒,传统的转圈加载会让用户焦虑。智能加载态将处理过程拆解为可见的步骤(理解问题 → 检索知识 → 生成回答),让等待变得可预期。'
},
{
id: 'confidence', icon: '📊', name: '置信度指示',
brief: '展示 AI 的确定程度',
detail: 'AI 的输出具有概率性,不同回答的可靠程度不同。通过置信度指示器,用户可以判断哪些信息可以直接采纳,哪些需要二次验证。这是 AI 原生应用透明性的核心体现。'
},
{
id: 'fallback', icon: '🛡️', name: '优雅降级',
brief: '不确定时的兜底策略',
detail: '当 AI 无法给出可靠回答时,不应该硬编一个答案。优雅降级策略包括:坦诚告知不确定性、提供替代信息源、转接人工服务、引导用户换个方式提问。'
}
]
const activePattern = ref('')
const currentPattern = computed(() => patterns.find(p => p.id === activePattern.value) || {})
// Streaming demo
const streamText = ref('')
const isStreaming = ref(false)
const fullText = 'React 是一个用于构建用户界面的 JavaScript 库。它采用组件化的开发模式,让你可以将复杂的 UI 拆分成独立的、可复用的小模块。'
// Loading demo
const loadingStages = ['理解用户意图...', '检索相关知识...', '组织回答内容...', '生成最终响应']
const loadingStep = ref(-1)
// Confidence demo
const confidenceItems = [
{ text: 'React 由 Meta 开发', score: 98, level: '高置信', color: '#10b981' },
{ text: '全球约 40% 的网站使用 React', score: 72, level: '中置信', color: '#f59e0b' },
{ text: 'React 19 将在下月发布', score: 35, level: '低置信', color: '#ef4444' }
]
// Fallback demo
const fallbackStep = ref(-1)
let timer = null
const clearTimers = () => {
if (timer) { clearInterval(timer); timer = null }
}
const activatePattern = (id) => {
clearTimers()
activePattern.value = id
replayDemo()
}
const replayDemo = () => {
clearTimers()
if (activePattern.value === 'streaming') {
streamText.value = ''
isStreaming.value = true
let i = 0
timer = setInterval(() => {
if (i < fullText.length) {
streamText.value += fullText[i]
i++
} else {
isStreaming.value = false
clearTimers()
}
}, 50)
} else if (activePattern.value === 'loading') {
loadingStep.value = 0
let step = 0
timer = setInterval(() => {
step++
loadingStep.value = step
if (step >= loadingStages.length) clearTimers()
}, 900)
} else if (activePattern.value === 'fallback') {
fallbackStep.value = 0
let step = 0
timer = setInterval(() => {
step++
fallbackStep.value = step
if (step >= 2) clearTimers()
}, 1000)
}
}
</script>
<style scoped>
.ux-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 20px; margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #06b6d4, #8b5cf6);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.pattern-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px; margin-bottom: 16px;
}
.pattern-card {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 10px; padding: 14px; cursor: pointer;
transition: all 0.2s; text-align: center;
}
.pattern-card:hover { background: var(--vp-c-bg-alt); }
.pattern-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.card-icon { font-size: 24px; margin-bottom: 6px; }
.card-name { font-weight: 600; font-size: 13px; }
.card-desc { font-size: 11px; color: var(--vp-c-text-2); margin-top: 4px; }
.preview-area {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 16px;
}
.preview-header {
display: flex; justify-content: space-between; align-items: center;
font-weight: 700; font-size: 14px; margin-bottom: 12px;
}
.replay-btn {
padding: 4px 12px; border: 1px solid var(--vp-c-divider);
border-radius: 6px; background: var(--vp-c-bg-soft);
cursor: pointer; font-size: 12px;
}
.demo-box {
background: var(--vp-c-bg-soft); border-radius: 8px;
padding: 16px; margin-bottom: 12px;
}
.demo-note {
font-size: 11px; color: var(--vp-c-text-3);
text-align: center; margin-top: 10px;
}
/* Streaming */
.chat-bubble.ai {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 10px; padding: 12px; font-size: 13px; line-height: 1.7;
}
.cursor-blink { animation: blink 0.8s infinite; color: var(--vp-c-brand); }
@keyframes blink { 50% { opacity: 0; } }
/* Loading */
.loading-stages { display: flex; flex-direction: column; gap: 8px; }
.stage {
display: flex; align-items: center; gap: 8px;
padding: 8px 12px; border-radius: 6px; font-size: 13px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
opacity: 0.4; transition: all 0.3s;
}
.stage.current { opacity: 1; border-color: var(--vp-c-brand); background: var(--vp-c-brand-soft); }
.stage.done { opacity: 1; border-color: #86efac; background: #f0fdf4; }
/* Confidence */
.confidence-list { display: flex; flex-direction: column; gap: 10px; }
.conf-item {
display: grid; grid-template-columns: 1fr 40px 60px 1fr;
align-items: center; gap: 8px; font-size: 12px;
}
.conf-bar-wrap {
height: 8px; background: var(--vp-c-bg);
border-radius: 4px; overflow: hidden;
}
.conf-bar { height: 100%; border-radius: 4px; transition: width 0.6s; }
.conf-score { font-weight: 600; text-align: right; }
.conf-label { font-size: 11px; color: var(--vp-c-text-2); }
.conf-text { color: var(--vp-c-text-1); }
/* Fallback */
.fallback-flow { display: flex; flex-direction: column; align-items: center; gap: 6px; }
.fb-step {
display: flex; align-items: center; gap: 8px; width: 100%;
padding: 10px 14px; border-radius: 8px; font-size: 13px;
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
opacity: 0.3; transition: all 0.4s;
}
.fb-step.active { opacity: 1; }
.fb-step.warn.active { border-color: #fbbf24; background: #fef3c7; }
.fb-step.safe.active { border-color: #86efac; background: #f0fdf4; }
.fb-arrow { font-size: 12px; color: var(--vp-c-text-3); }
.pattern-detail { margin-top: 12px; }
.detail-label { font-weight: 600; font-size: 12px; margin-bottom: 4px; }
.detail-text { font-size: 13px; color: var(--vp-c-text-2); line-height: 1.7; }
</style>
@@ -0,0 +1,260 @@
<template>
<div class="prompt-demo">
<div class="header">
<div class="title">Prompt 工程实验室</div>
<div class="subtitle">修改 Prompt 结构观察输出质量的变化</div>
</div>
<div class="template-tabs">
<button
v-for="t in templates"
:key="t.id"
:class="['tab-btn', { active: currentTemplate === t.id }]"
@click="selectTemplate(t.id)"
>
<span>{{ t.icon }}</span>
<span>{{ t.name }}</span>
</button>
</div>
<div class="editor-grid">
<div class="editor-panel">
<div class="panel-label">System Prompt系统指令</div>
<textarea
v-model="systemPrompt"
class="prompt-input"
rows="3"
placeholder="设定 AI 的角色和行为规则..."
/>
<div class="panel-label">User Prompt用户输入</div>
<textarea
v-model="userPrompt"
class="prompt-input"
rows="3"
placeholder="用户的具体问题或指令..."
/>
<button class="run-btn" @click="runPrompt">
模拟生成
</button>
</div>
<div class="output-panel">
<div class="panel-label">模拟输出</div>
<div class="output-box">
<div v-if="isGenerating" class="generating">
<span class="dot-anim"></span> 生成中...
</div>
<div v-else-if="output" class="output-text">
{{ output }}
</div>
<div v-else class="output-placeholder">
点击"模拟生成"查看效果
</div>
</div>
<div v-if="output" class="quality-bar">
<div class="quality-label">输出质量评估</div>
<div class="quality-metrics">
<div
v-for="m in currentQuality"
:key="m.name"
class="metric"
>
<div class="metric-name">{{ m.name }}</div>
<div class="meter">
<div
class="meter-fill"
:style="{ width: m.score + '%', background: m.color }"
/>
</div>
<div class="metric-score">{{ m.score }}%</div>
</div>
</div>
</div>
</div>
</div>
<div class="tips-bar">
<span class="tips-label">💡 Prompt 技巧</span>
<span class="tips-text">{{ currentTip }}</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const templates = [
{ id: 'bad', icon: '❌', name: '模糊提问' },
{ id: 'basic', icon: '📝', name: '基础结构' },
{ id: 'good', icon: '✅', name: '最佳实践' }
]
const currentTemplate = ref('bad')
const systemPrompt = ref('')
const userPrompt = ref('')
const output = ref('')
const isGenerating = ref(false)
const templateData = {
bad: {
system: '',
user: '帮我写个介绍',
output: '好的,这是一个介绍:\n\n大家好,我是一个人,我喜欢很多东西。我觉得生活很有趣。谢谢大家。\n\n(输出模糊、缺乏方向、没有实际价值)',
quality: [
{ name: '相关性', score: 20, color: '#ef4444' },
{ name: '具体性', score: 10, color: '#ef4444' },
{ name: '可用性', score: 15, color: '#ef4444' }
],
tip: '没有 System Prompt,没有上下文,问题过于模糊 —— AI 只能猜测你的意图。'
},
basic: {
system: '你是一个技术文档写手。',
user: '帮我写一段关于 React 的介绍,大约100字。',
output: 'React 是由 Meta(原 Facebook)开发的开源 JavaScript 库,用于构建用户界面。它采用组件化开发模式和虚拟 DOM 技术,能够高效地更新和渲染页面。React 的声明式编程风格让开发者可以更直观地描述 UI 状态,广泛应用于现代 Web 开发中。',
quality: [
{ name: '相关性', score: 70, color: '#f59e0b' },
{ name: '具体性', score: 65, color: '#f59e0b' },
{ name: '可用性', score: 60, color: '#f59e0b' }
],
tip: '有了角色设定和基本约束(字数),输出质量明显提升,但仍缺少格式和受众定义。'
},
good: {
system: '你是一位资深前端技术博主。写作风格:简洁专业,善用类比。目标读者:编程初学者。输出格式:先用一句话总结,再展开说明,最后给出一个生活类比。',
user: '请介绍 React 是什么,以及为什么它如此流行。控制在150字以内。',
output: '一句话总结:React 是一个帮你高效搭建网页界面的 JavaScript 工具库。\n\n展开说明:React 由 Meta 团队开发,核心理念是"组件化"——把复杂页面拆成独立的小积木,每个积木管理自己的状态和外观。它的虚拟 DOM 机制能智能计算最小更新范围,避免不必要的页面重绘。\n\n生活类比:如果网页是一面乐高墙,React 就是那套标准化的乐高积木系统——你可以独立替换任何一块,而不用推倒重来。',
quality: [
{ name: '相关性', score: 95, color: '#10b981' },
{ name: '具体性', score: 90, color: '#10b981' },
{ name: '可用性', score: 95, color: '#10b981' }
],
tip: '角色 + 风格 + 受众 + 格式 + 约束 = 高质量输出。好的 Prompt 就是好的需求文档。'
}
}
const currentQuality = ref([])
const currentTip = computed(() => templateData[currentTemplate.value].tip)
const selectTemplate = (id) => {
currentTemplate.value = id
const data = templateData[id]
systemPrompt.value = data.system
userPrompt.value = data.user
output.value = ''
currentQuality.value = []
}
const runPrompt = async () => {
isGenerating.value = true
output.value = ''
currentQuality.value = []
await new Promise(r => setTimeout(r, 1200))
const data = templateData[currentTemplate.value]
output.value = data.output
currentQuality.value = data.quality
isGenerating.value = false
}
// Initialize
selectTemplate('bad')
</script>
<style scoped>
.prompt-demo {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 20px;
margin: 20px 0;
}
.header { text-align: center; margin-bottom: 16px; }
.title {
font-size: 17px; font-weight: 700;
background: linear-gradient(120deg, #8b5cf6, var(--vp-c-brand));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.subtitle { font-size: 12px; color: var(--vp-c-text-2); margin-top: 4px; }
.template-tabs {
display: flex; gap: 8px; justify-content: center;
margin-bottom: 16px; flex-wrap: wrap;
}
.tab-btn {
display: flex; align-items: center; gap: 6px;
padding: 8px 14px; border: 1px solid var(--vp-c-divider);
border-radius: 20px; background: var(--vp-c-bg);
cursor: pointer; transition: all 0.2s; font-size: 13px;
}
.tab-btn:hover { background: var(--vp-c-bg-alt); }
.tab-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.editor-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.editor-panel, .output-panel {
background: var(--vp-c-bg); border: 1px solid var(--vp-c-divider);
border-radius: 12px; padding: 14px;
}
.panel-label {
font-weight: 600; font-size: 12px; margin-bottom: 6px;
color: var(--vp-c-text-2);
}
.prompt-input {
width: 100%; padding: 10px; border: 1px solid var(--vp-c-divider);
border-radius: 8px; background: var(--vp-c-bg-soft);
font-size: 13px; line-height: 1.5; resize: vertical;
font-family: var(--vp-font-family-mono);
color: var(--vp-c-text-1); margin-bottom: 10px;
box-sizing: border-box;
}
.prompt-input:focus {
outline: none; border-color: var(--vp-c-brand);
}
.run-btn {
width: 100%; padding: 10px; background: var(--vp-c-brand);
color: white; border: none; border-radius: 8px;
font-size: 13px; cursor: pointer; transition: background 0.2s;
}
.run-btn:hover { background: var(--vp-c-brand-dark); }
.output-box {
background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider);
border-radius: 8px; padding: 14px; min-height: 120px;
font-size: 13px; line-height: 1.7;
}
.output-text { white-space: pre-wrap; color: var(--vp-c-text-1); }
.output-placeholder { color: var(--vp-c-text-3); text-align: center; padding: 30px 0; }
.generating { color: var(--vp-c-brand); text-align: center; padding: 30px 0; }
.dot-anim { animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.3; } }
.quality-bar { margin-top: 12px; }
.quality-label { font-weight: 600; font-size: 12px; margin-bottom: 8px; }
.quality-metrics { display: flex; flex-direction: column; gap: 6px; }
.metric { display: flex; align-items: center; gap: 8px; }
.metric-name { font-size: 11px; width: 50px; color: var(--vp-c-text-2); }
.meter {
flex: 1; height: 8px; background: var(--vp-c-bg-soft);
border-radius: 4px; overflow: hidden;
}
.meter-fill {
height: 100%; border-radius: 4px;
transition: width 0.6s ease;
}
.metric-score { font-size: 11px; font-weight: 600; width: 36px; text-align: right; }
.tips-bar {
margin-top: 16px; padding: 12px 16px;
background: var(--vp-c-brand-soft); border-radius: 6px; font-size: 13px;
}
.tips-label { font-weight: 600; color: var(--vp-c-brand-dark); }
.tips-text { color: var(--vp-c-text-1); }
</style>
@@ -0,0 +1,373 @@
<template>
<div class="ai-vs-traditional-demo">
<div class="demo-header">
<span class="icon">🤖</span>
<span class="title">AI 工程师 vs 传统工程师</span>
<span class="subtitle">工作方式的差异</span>
</div>
<div class="comparison-container">
<div class="comparison-column traditional">
<div class="column-header">
<span class="col-icon">👨💻</span>
<span class="col-title">传统工程师</span>
</div>
<div class="work-flow">
<div v-for="(step, index) in traditionalSteps" :key="index" class="flow-step">
<span class="step-num">{{ index + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
</div>
<div class="column-stats">
<div class="stat-item">
<span class="stat-label">编码时间占比</span>
<span class="stat-value">60-70%</span>
</div>
<div class="stat-item">
<span class="stat-label">思考时间占比</span>
<span class="stat-value">30-40%</span>
</div>
</div>
</div>
<div class="vs-divider">
<span class="vs-text">VS</span>
</div>
<div class="comparison-column ai">
<div class="column-header">
<span class="col-icon">🤖</span>
<span class="col-title">AI 工程师</span>
</div>
<div class="work-flow">
<div v-for="(step, index) in aiSteps" :key="index" class="flow-step">
<span class="step-num">{{ index + 1 }}</span>
<span class="step-text">{{ step }}</span>
</div>
</div>
<div class="column-stats">
<div class="stat-item">
<span class="stat-label">编码时间占比</span>
<span class="stat-value">20-30%</span>
</div>
<div class="stat-item">
<span class="stat-label">思考时间占比</span>
<span class="stat-value">70-80%</span>
</div>
</div>
</div>
</div>
<div class="skill-shift">
<div class="shift-title">📊 能力重心转移</div>
<div class="shift-grid">
<div v-for="item in skillShift" :key="item.from" class="shift-item">
<div class="shift-from">
<span class="arrow"></span>
<span class="text">{{ item.from }}</span>
<span class="trend down">重要性下降</span>
</div>
<div class="shift-to">
<span class="arrow"></span>
<span class="text">{{ item.to }}</span>
<span class="trend up">重要性上升</span>
</div>
</div>
</div>
</div>
<div class="ai-insight">
<div class="insight-icon">💡</div>
<div class="insight-text">
<strong>AI 时代的核心竞争力</strong>不是"会写代码"而是"会描述需求、会判断对错、会设计方案"AI 是你的编程助手<strong>决策者永远是你</strong>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const traditionalSteps = ref([
'理解需求',
'查阅文档学习语法',
'手写代码实现',
'调试修复 Bug',
'优化代码性能',
'编写测试用例'
])
const aiSteps = ref([
'理解需求',
'用自然语言描述给 AI',
'审核 AI 生成的代码',
'判断是否符合预期',
'调整需求重新生成',
'整合到项目中'
])
const skillShift = ref([
{ from: '语法记忆', to: '需求描述能力' },
{ from: '手写代码速度', to: '代码审核能力' },
{ from: '查文档能力', to: '架构设计能力' },
{ from: '调试技巧', to: '问题定位能力' }
])
</script>
<style scoped>
.ai-vs-traditional-demo {
background: linear-gradient(135deg, #fff7ed 0%, #ffedd5 100%);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #fed7aa;
}
.icon {
font-size: 28px;
}
.title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
}
.subtitle {
font-size: 14px;
color: #64748b;
margin-left: auto;
}
.comparison-container {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 16px;
margin-bottom: 24px;
}
.comparison-column {
background: white;
border-radius: 12px;
padding: 16px;
}
.column-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #e2e8f0;
}
.col-icon {
font-size: 24px;
}
.col-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.work-flow {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.flow-step {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #f8fafc;
border-radius: 6px;
}
.step-num {
width: 20px;
height: 20px;
background: #e2e8f0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
color: #64748b;
}
.traditional .step-num {
background: #dbeafe;
color: #1d4ed8;
}
.ai .step-num {
background: #dcfce7;
color: #15803d;
}
.step-text {
font-size: 13px;
color: #475569;
}
.column-stats {
display: flex;
flex-direction: column;
gap: 8px;
padding-top: 12px;
border-top: 1px solid #e2e8f0;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.stat-label {
font-size: 12px;
color: #64748b;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.vs-divider {
display: flex;
align-items: center;
justify-content: center;
}
.vs-text {
font-size: 18px;
font-weight: 700;
color: #f97316;
background: white;
padding: 8px 12px;
border-radius: 8px;
}
.skill-shift {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
}
.shift-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 16px;
}
.shift-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.shift-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
background: #f8fafc;
border-radius: 8px;
}
.shift-from,
.shift-to {
display: flex;
align-items: center;
gap: 8px;
}
.arrow {
font-size: 14px;
font-weight: 700;
}
.shift-from .arrow {
color: #ef4444;
}
.shift-to .arrow {
color: #22c55e;
}
.text {
font-size: 13px;
color: #475569;
}
.trend {
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
margin-left: auto;
}
.trend.down {
background: #fee2e2;
color: #dc2626;
}
.trend.up {
background: #dcfce7;
color: #16a34a;
}
.ai-insight {
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
gap: 12px;
border-left: 4px solid #f97316;
}
.insight-icon {
font-size: 20px;
}
.insight-text {
font-size: 14px;
color: #475569;
line-height: 1.6;
}
.insight-text strong {
color: #1e293b;
}
@media (max-width: 768px) {
.comparison-container {
grid-template-columns: 1fr;
}
.vs-divider {
padding: 8px 0;
}
.shift-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,139 @@
<template>
<div class="bios-post-demo">
<div class="demo-label">BIOS POST 硬件自检 点击查看检测项目</div>
<div class="post-items">
<div
v-for="item in postItems"
:key="item.name"
class="post-item"
:class="{ passed: item.passed, error: item.error }"
@click="item.passed = !item.passed"
>
<div class="item-status">{{ item.passed ? '✅' : item.error ? '❌' : '⏳' }}</div>
<div class="item-info">
<div class="item-name">{{ item.name }}</div>
<div class="item-desc">{{ item.desc }}</div>
</div>
</div>
</div>
<div class="post-result">
<span v-if="allPassed" class="result-pass"> 自检通过准备启动</span>
<span v-else class="result-pending"> 点击项目模拟检测状态</span>
</div>
<div class="tap-hint">👆 点击模拟检测结果</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const postItems = ref([
{ name: 'CPU', desc: '处理器完整性检测', passed: false, error: false },
{ name: '内存', desc: 'RAM 容量和可用性检测', passed: false, error: false },
{ name: '显卡', desc: '显示适配器初始化', passed: false, error: false },
{ name: '硬盘', desc: '存储设备识别', passed: false, error: false },
{ name: '键盘', desc: '键盘接口检测', passed: false, error: false },
{ name: '鼠标', desc: '鼠标接口检测', passed: false, error: false }
])
const allPassed = computed(() => postItems.value.every(item => item.passed))
</script>
<style scoped>
.bios-post-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.post-items {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.4rem;
}
.post-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
transition: all 0.3s;
}
.post-item.passed {
border-color: #22c55e;
background: rgba(34, 197, 94, 0.1);
}
.post-item.error {
border-color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.item-status {
font-size: 1rem;
flex-shrink: 0;
}
.item-info {
flex: 1;
min-width: 0;
}
.item-name {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.item-desc {
font-size: 0.62rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.post-result {
margin-top: 0.75rem;
text-align: center;
padding: 0.4rem;
border-radius: 4px;
font-size: 0.75rem;
}
.result-pass {
color: #22c55e;
font-weight: 600;
}
.result-pending {
color: var(--vp-c-text-3);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
}
</style>
@@ -0,0 +1,167 @@
<template>
<div class="backend-demo">
<div class="demo-header">
<span class="title">后端核心概念</span>
<span class="subtitle">服务器端的核心职责</span>
</div>
<div class="core-grid">
<div v-for="core in coreConcepts" :key="core.name" class="core-card">
<div class="core-name">{{ core.name }}</div>
<div class="core-desc">{{ core.desc }}</div>
<div class="core-examples">
<span v-for="ex in core.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
<div class="flow-section">
<div class="flow-title">请求处理流程</div>
<div class="flow-steps">
<span v-for="(step, i) in flowSteps" :key="step">
<span class="flow-step">{{ step }}</span>
<span v-if="i < flowSteps.length - 1" class="flow-arrow"></span>
</span>
</div>
</div>
<div class="info-box">
<strong>后端的核心价值</strong>不是写代码而是设计系统如何让系统稳定安全高效可扩展才是后端工程师的真正能力
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const coreConcepts = ref([
{ name: 'API 设计', desc: '定义客户端如何与服务端交互', examples: ['RESTful', 'GraphQL'] },
{ name: '业务逻辑', desc: '处理核心业务规则和流程', examples: ['订单处理', '支付流程'] },
{ name: '数据存储', desc: '数据的持久化和查询', examples: ['MySQL', 'Redis'] },
{ name: '认证授权', desc: '用户身份验证和权限控制', examples: ['JWT', 'OAuth'] },
{ name: '性能优化', desc: '缓存、异步、并发处理', examples: ['缓存', '消息队列'] },
{ name: '安全防护', desc: '防止攻击和数据泄露', examples: ['SQL注入防护', 'HTTPS'] }
])
const flowSteps = ref(['接收请求', '路由解析', '业务处理', '数据操作', '返回响应'])
</script>
<style scoped>
.backend-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.core-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.core-card {
padding: 0.6rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.core-name {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.core-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.35rem;
}
.core-examples {
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
}
.example-tag {
font-size: 0.65rem;
padding: 0.1rem 0.3rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.flow-section {
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
margin-bottom: 1rem;
}
.flow-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.5rem;
}
.flow-steps {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem;
}
.flow-step {
font-size: 0.72rem;
padding: 0.2rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
color: var(--vp-c-text-2);
}
.flow-arrow {
color: var(--vp-c-text-3);
font-size: 0.8rem;
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.core-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,137 @@
<template>
<div class="boot-demo">
<div class="demo-label">操作系统启动流程 点击逐步启动</div>
<div class="timeline">
<div
v-for="(step, index) in steps"
:key="step.name"
class="timeline-item"
:class="{ active: currentStep >= index }"
@click="currentStep = index"
>
<div class="timeline-marker">
<span class="marker-dot" :class="{ filled: currentStep >= index }">{{ index + 1 }}</span>
<span v-if="index < steps.length - 1" class="marker-line" :class="{ filled: currentStep >= index }"></span>
</div>
<div class="timeline-content">
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.desc }}</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各步骤</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{ name: '引导程序', desc: '从硬盘读取 bootloader' },
{ name: '内核加载', desc: '加载操作系统内核' },
{ name: '系统服务', desc: '启动各种后台服务' },
{ name: '桌面环境', desc: '显示登录界面和桌面' }
]
</script>
<style scoped>
.boot-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.timeline {
display: flex;
flex-direction: column;
}
.timeline-item {
display: flex;
gap: 0.6rem;
padding: 0.3rem 0;
opacity: 0.4;
transition: opacity 0.3s;
}
.timeline-item.active {
opacity: 1;
}
.timeline-marker {
display: flex;
flex-direction: column;
align-items: center;
width: 1.5rem;
}
.marker-dot {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.marker-dot.filled {
background: var(--vp-c-brand);
border-color: var(--vp-c-brand);
color: white;
}
.marker-line {
flex: 1;
width: 2px;
background: var(--vp-c-divider);
min-height: 1.5rem;
}
.marker-line.filled {
background: var(--vp-c-brand);
}
.timeline-content {
flex: 1;
padding-bottom: 0.8rem;
}
.step-name {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.step-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
}
</style>
@@ -0,0 +1,130 @@
<template>
<div class="browser-demo">
<div class="demo-label">浏览器架构 点击查看各模块</div>
<div class="browser-schema">
<div class="browser-layers">
<div
v-for="layer in layers"
:key="layer.name"
class="layer"
:class="{ active: activeLayer === layer.name }"
@click="activeLayer = activeLayer === layer.name ? '' : layer.name"
>
<div class="layer-header">
<span class="layer-name">{{ layer.name }}</span>
<span class="layer-icon">{{ layer.icon }}</span>
</div>
<div v-if="activeLayer === layer.name" class="layer-detail">
<div class="detail-text">{{ layer.desc }}</div>
<div class="detail-examples">
<span v-for="ex in layer.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各模块详情</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeLayer = ref('')
const layers = [
{ name: '用户界面', icon: '🎨', desc: '你看到的地址栏、标签页、书签等', examples: ['地址栏', '标签页', '书签栏', '工具栏'] },
{ name: '浏览器引擎', icon: '⚙️', desc: '协调 UI 和渲染引擎的桥梁', examples: ['Chrome: Blink', 'Firefox: Gecko', 'Safari: WebKit'] },
{ name: '渲染引擎', icon: '📄', desc: '解析 HTML/CSS,把网页显示出来', examples: ['Blink', 'Gecko', 'WebKit'] },
{ name: 'JavaScript 引擎', icon: '⚡', desc: '执行页面中的 JavaScript 代码', examples: ['V8', 'SpiderMonkey', 'JavaScriptCore'] },
{ name: '网络模块', icon: '🌐', desc: '发送 HTTP/HTTPS 请求', examples: ['HTTP', 'HTTP/2', 'HTTP/3', 'WebSocket'] },
{ name: '数据存储', icon: '💾', desc: '保存 Cookie、缓存等数据', examples: ['Cookie', 'LocalStorage', 'Cache'] }
]
</script>
<style scoped>
.browser-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.browser-layers {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.layer {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.7rem;
transition: all 0.3s;
}
.layer.active {
border-color: var(--vp-c-brand);
}
.layer-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.layer-name {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.layer-icon {
font-size: 0.9rem;
}
.layer-detail {
margin-top: 0.5rem;
padding-top: 0.5rem;
border-top: 1px solid var(--vp-c-divider);
}
.detail-text {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.4rem;
}
.detail-examples {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
}
.example-tag {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,286 @@
<template>
<div class="career-path-demo">
<div class="demo-header">
<span class="icon">🚀</span>
<span class="title">工程师成长路径</span>
<span class="subtitle">从入门到精通的技能演进</span>
</div>
<div class="path-container">
<div
v-for="(stage, index) in stages"
:key="stage.name"
class="stage-card"
:class="{ active: activeStage === index }"
@click="activeStage = index"
>
<div class="stage-header">
<span class="stage-icon">{{ stage.icon }}</span>
<span class="stage-name">{{ stage.name }}</span>
<span class="stage-time">{{ stage.time }}</span>
</div>
<div class="stage-content">
<div class="stage-desc">{{ stage.desc }}</div>
<div class="stage-skills">
<span class="skill-label">核心技能</span>
<div class="skill-tags">
<span v-for="skill in stage.skills" :key="skill" class="skill-tag">
{{ skill }}
</span>
</div>
</div>
<div class="stage-output">
<span class="output-label">典型产出</span>
<span class="output-text">{{ stage.output }}</span>
</div>
</div>
<div v-if="index < stages.length - 1" class="stage-arrow"></div>
</div>
</div>
<div class="path-insight">
<div class="insight-icon">💡</div>
<div class="insight-content">
<div class="insight-title">成长关键点</div>
<ul class="insight-list">
<li> 1-2 打基础建立"能独立完成任务"的能力</li>
<li>2-3 选方向在某个领域建立深度</li>
<li>3-5 横向扩展培养架构思维和团队协作能力</li>
<li>5 +技术决策团队带领技术影响力</li>
</ul>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeStage = ref(0)
const stages = ref([
{
name: '入门期',
icon: '🌱',
time: '0-1 年',
desc: '学习基础语法和工具,能完成简单任务',
skills: ['一门语言基础', 'Git 使用', '调试技巧', '阅读文档'],
output: '能独立完成小功能、修复简单 Bug'
},
{
name: '成长期',
icon: '🌿',
time: '1-2 年',
desc: '熟悉常用框架和最佳实践,能独立负责模块',
skills: ['框架熟练', '代码规范', '单元测试', 'API 设计'],
output: '独立负责一个功能模块,代码质量稳定'
},
{
name: '进阶期',
icon: '🌳',
time: '2-3 年',
desc: '深入某个领域,开始有技术选型能力',
skills: ['领域深入', '性能优化', '架构设计', '技术选型'],
output: '主导技术方案设计,解决复杂问题'
},
{
name: '成熟期',
icon: '🌲',
time: '3-5 年',
desc: '全栈能力或领域专家,能带领小团队',
skills: ['全栈能力', '团队协作', '技术分享', '项目管理'],
output: '负责核心系统,指导新人成长'
},
{
name: '专家期',
icon: '🏔️',
time: '5 年+',
desc: '技术决策者,有行业影响力',
skills: ['技术战略', '团队建设', '行业洞察', '创新引领'],
output: '技术方向决策,培养技术团队'
}
])
</script>
<style scoped>
.career-path-demo {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 16px;
padding: 24px;
margin: 20px 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #e2e8f0;
}
.icon {
font-size: 28px;
}
.title {
font-size: 20px;
font-weight: 700;
color: #1e293b;
}
.subtitle {
font-size: 14px;
color: #64748b;
margin-left: auto;
}
.path-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.stage-card {
background: white;
border-radius: 12px;
padding: 16px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
position: relative;
}
.stage-card:hover {
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stage-card.active {
border-color: #3b82f6;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
}
.stage-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.stage-icon {
font-size: 24px;
}
.stage-name {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.stage-time {
font-size: 12px;
color: #64748b;
background: #f1f5f9;
padding: 2px 8px;
border-radius: 4px;
margin-left: auto;
}
.stage-content {
padding-left: 36px;
}
.stage-desc {
font-size: 14px;
color: #475569;
margin-bottom: 12px;
}
.stage-skills {
margin-bottom: 8px;
}
.skill-label,
.output-label {
font-size: 12px;
color: #64748b;
margin-right: 8px;
}
.skill-tags {
display: inline-flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
}
.skill-tag {
font-size: 12px;
background: #e0f2fe;
color: #0369a1;
padding: 2px 8px;
border-radius: 4px;
}
.stage-output {
font-size: 13px;
color: #475569;
}
.output-text {
color: #1e293b;
}
.stage-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
color: #cbd5e1;
}
.path-insight {
margin-top: 24px;
background: white;
border-radius: 12px;
padding: 16px;
display: flex;
gap: 16px;
border-left: 4px solid #f59e0b;
}
.insight-icon {
font-size: 24px;
}
.insight-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin-bottom: 8px;
}
.insight-list {
margin: 0;
padding-left: 20px;
font-size: 13px;
color: #475569;
line-height: 1.8;
}
.insight-list li {
margin-bottom: 4px;
}
@media (max-width: 640px) {
.stage-content {
padding-left: 0;
}
.stage-arrow {
display: none;
}
}
</style>
@@ -0,0 +1,164 @@
<template>
<div class="field-map-demo">
<div class="demo-header">
<span class="title">计算机领域全景图</span>
<span class="subtitle">点击查看详情</span>
</div>
<div class="field-grid">
<div
v-for="field in fields"
:key="field.name"
class="field-card"
:class="{ active: activeField === field.name }"
@click="activeField = field.name"
>
<div class="field-name">{{ field.name }}</div>
<div class="field-desc">{{ field.desc }}</div>
<div class="field-techs">
<span v-for="tech in field.techs" :key="tech" class="tech-tag">{{ tech }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>建议</strong>不要试图一次学完所有方向先选一个方向深入建立"根据地"再横向扩展
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeField = ref('前端')
const fields = ref([
{
name: '前端',
desc: '用户能看到、能交互的一切',
techs: ['HTML/CSS', 'JavaScript', 'React/Vue']
},
{
name: '后端',
desc: '服务器端的业务逻辑和数据处理',
techs: ['Node.js', 'Go', 'Java', 'Python']
},
{
name: '移动端',
desc: '手机上的应用体验',
techs: ['Swift', 'Kotlin', 'Flutter']
},
{
name: 'AI/算法',
desc: '让系统变"聪明"',
techs: ['PyTorch', 'TensorFlow', '机器学习']
},
{
name: '运维/DevOps',
desc: '保证系统稳定运行',
techs: ['Docker', 'K8s', 'CI/CD']
},
{
name: '数据工程',
desc: '数据采集、存储、分析',
techs: ['SQL', 'Spark', '数据仓库']
}
])
</script>
<style scoped>
.field-map-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.field-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.field-card {
padding: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.field-card:hover {
border-color: var(--vp-c-brand-1);
}
.field-card.active {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-bg-soft);
}
.field-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.field-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.field-techs {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.tech-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.field-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
@@ -0,0 +1,172 @@
<template>
<div class="desktop-demo">
<div class="demo-label">桌面是如何出现的 点击加载</div>
<div class="desktop-view" @click="loading = !loading">
<div class="screen" :class="{ loaded: loading }">
<div class="boot-screen" v-if="!loading">
<div class="boot-logo"></div>
<div class="boot-text">正在启动...</div>
</div>
<div class="desktop" v-else>
<div class="desktop-icons">
<div class="desktop-icon">📁</div>
<div class="desktop-icon">🗑</div>
<div class="desktop-icon"></div>
<div class="desktop-icon">🌐</div>
</div>
<div class="taskbar">
<div class="taskbar-icon">🏠</div>
<div class="taskbar-icon">📊</div>
<div class="taskbar-time">{{ time }}</div>
</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击切换开机状态</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const loading = ref(false)
const time = ref('')
let timer = null
const updateTime = () => {
const now = new Date()
time.value = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
}
onMounted(() => {
updateTime()
timer = setInterval(updateTime, 1000)
})
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style scoped>
.desktop-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.desktop-view {
display: flex;
justify-content: center;
}
.screen {
width: 14rem;
height: 9rem;
background: #1a1a1a;
border-radius: 6px;
overflow: hidden;
transition: all 0.5s;
}
.screen.loaded {
background: linear-gradient(180deg, #2d5af5 0%, #1e3a8a 100%);
}
.boot-screen {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #666;
}
.boot-logo {
font-size: 2rem;
margin-bottom: 0.5rem;
animation: spin 2s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.boot-text {
font-size: 0.75rem;
}
.desktop {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0.5rem;
}
.desktop-icons {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.desktop-icon {
width: 2.2rem;
height: 2.2rem;
background: rgba(255,255,255,0.15);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
}
.taskbar {
display: flex;
align-items: center;
gap: 0.5rem;
background: rgba(0,0,0,0.3);
padding: 0.3rem 0.5rem;
border-radius: 4px;
}
.taskbar-icon {
width: 1.5rem;
height: 1.5rem;
background: rgba(255,255,255,0.2);
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
}
.taskbar-time {
margin-left: auto;
font-size: 0.7rem;
color: white;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,162 @@
<template>
<div class="skill-shift-demo">
<div class="demo-header">
<span class="title">能力重要性变化</span>
<span class="subtitle">AI 时代哪些能力更重要了</span>
</div>
<div class="comparison-grid">
<div class="column">
<div class="column-title">传统时代更重要</div>
<div class="skill-list">
<div v-for="skill in beforeSkills" :key="skill.name" class="skill-item">
<span class="skill-name">{{ skill.name }}</span>
<div class="skill-bar">
<div class="bar-fill before" :style="{ width: skill.level + '%' }"></div>
</div>
<span class="skill-desc">{{ skill.desc }}</span>
</div>
</div>
</div>
<div class="column">
<div class="column-title">AI 时代更重要</div>
<div class="skill-list">
<div v-for="skill in afterSkills" :key="skill.name" class="skill-item">
<span class="skill-name">{{ skill.name }}</span>
<div class="skill-bar">
<div class="bar-fill after" :style="{ width: skill.level + '%' }"></div>
</div>
<span class="skill-desc">{{ skill.desc }}</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>关键洞察</strong>AI 能帮你写代码但判断力架构思维领域知识调试能力是 AI 替代不了的
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const beforeSkills = ref([
{ name: '语法记忆', level: 90, desc: '熟记 API 和语法细节' },
{ name: '手写代码速度', level: 85, desc: '快速敲代码的能力' },
{ name: '查文档能力', level: 80, desc: '快速找到 API 用法' }
])
const afterSkills = ref([
{ name: '需求描述能力', level: 95, desc: '用自然语言准确描述需求' },
{ name: '代码审核能力', level: 90, desc: '判断 AI 生成代码的对错' },
{ name: '架构设计能力', level: 85, desc: '设计系统整体结构' },
{ name: '问题定位能力', level: 80, desc: '出问题时知道从哪排查' }
])
</script>
<style scoped>
.skill-shift-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.column-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px dashed var(--vp-c-divider);
}
.skill-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.skill-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.skill-name {
font-size: 0.82rem;
font-weight: 500;
color: var(--vp-c-text-1);
}
.skill-bar {
height: 6px;
background: var(--vp-c-divider);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease;
}
.bar-fill.before {
background: var(--vp-c-text-3);
}
.bar-fill.after {
background: var(--vp-c-brand-1);
}
.skill-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.comparison-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,190 @@
<template>
<div class="framework-demo">
<div class="demo-header">
<span class="title">前端框架演进</span>
<span class="subtitle"> jQuery 到现代框架</span>
</div>
<div class="timeline">
<div v-for="(era, index) in eras" :key="era.name" class="era-item">
<div class="era-marker">
<span class="era-dot"></span>
<span v-if="index < eras.length - 1" class="era-line"></span>
</div>
<div class="era-content">
<div class="era-header">
<span class="era-name">{{ era.name }}</span>
<span class="era-time">{{ era.time }}</span>
</div>
<div class="era-desc">{{ era.desc }}</div>
<div class="era-techs">
<span v-for="tech in era.techs" :key="tech" class="tech-tag">{{ tech }}</span>
</div>
</div>
</div>
</div>
<div class="info-box">
<strong>框架的本质</strong>解决"数据变化后如何高效更新 UI"的问题现代框架让你只需关注"数据是什么"框架自动处理"UI 怎么变"
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const eras = ref([
{
name: '原生时代',
time: '1990s',
desc: '直接操作 DOM,一切从零开始',
techs: ['HTML', 'CSS', 'JavaScript']
},
{
name: 'jQuery 时代',
time: '2006-2015',
desc: '简化 DOM 操作,跨浏览器兼容',
techs: ['jQuery', 'Bootstrap']
},
{
name: 'MVVM 时代',
time: '2010-2015',
desc: '数据驱动视图,双向绑定',
techs: ['Angular.js', 'Knockout']
},
{
name: '组件化时代',
time: '2013-至今',
desc: '声明式、组件化、虚拟 DOM',
techs: ['React', 'Vue', 'Angular']
},
{
name: '新时代',
time: '2020-至今',
desc: '编译时优化,更少运行时开销',
techs: ['Svelte', 'Solid']
}
])
</script>
<style scoped>
.framework-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.timeline {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.era-item {
display: flex;
gap: 0.75rem;
}
.era-marker {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.era-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vp-c-brand-1);
border: 2px solid var(--vp-c-bg);
}
.era-line {
width: 2px;
flex: 1;
background: var(--vp-c-divider);
margin: 2px 0;
}
.era-content {
flex: 1;
padding: 0.5rem 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
margin-bottom: 0.25rem;
}
.era-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.era-name {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.era-time {
font-size: 0.7rem;
color: var(--vp-c-text-3);
background: var(--vp-c-bg-soft);
padding: 0.1rem 0.35rem;
border-radius: 3px;
}
.era-desc {
font-size: 0.75rem;
color: var(--vp-c-text-2);
margin-bottom: 0.35rem;
}
.era-techs {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.tech-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,158 @@
<template>
<div class="triad-demo">
<div class="demo-header">
<span class="title">前端三件套</span>
<span class="subtitle">网页开发的三大基石</span>
</div>
<div class="triad-grid">
<div
v-for="tech in triad"
:key="tech.name"
class="tech-card"
:class="{ active: activeTech === tech.name }"
@click="activeTech = tech.name"
>
<div class="tech-name">{{ tech.name }}</div>
<div class="tech-role">{{ tech.role }}</div>
<div class="tech-analogy">{{ tech.analogy }}</div>
<div class="tech-examples">
<span v-for="ex in tech.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>协作关系</strong>HTML 搭骨架CSS 穿衣服JavaScript 让它动起来三者缺一不可
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeTech = ref('HTML')
const triad = ref([
{
name: 'HTML',
role: '结构层',
analogy: '房子的骨架:墙、门、窗',
examples: ['div', 'span', 'form', 'input']
},
{
name: 'CSS',
role: '表现层',
analogy: '房子的装修:颜色、位置、大小',
examples: ['color', 'flex', 'grid', 'animation']
},
{
name: 'JavaScript',
role: '行为层',
analogy: '房子的智能:开关灯、开门',
examples: ['事件', 'DOM操作', '网络请求']
}
])
</script>
<style scoped>
.triad-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.triad-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.tech-card {
padding: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.tech-card:hover {
border-color: var(--vp-c-brand-1);
}
.tech-card.active {
border-color: var(--vp-c-brand-1);
}
.tech-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.25rem;
}
.tech-role {
font-size: 0.75rem;
color: var(--vp-c-brand-1);
margin-bottom: 0.25rem;
}
.tech-analogy {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.tech-examples {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.example-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.triad-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,120 @@
<template>
<div class="full-process-demo">
<div class="demo-label">完整流程概览 点击逐步演示</div>
<div class="process-flow">
<div
v-for="(phase, index) in phases"
:key="phase.name"
class="phase"
:class="{ active: currentPhase >= index }"
@click="currentPhase = index"
>
<div class="phase-icon">{{ phase.icon }}</div>
<div class="phase-name">{{ phase.name }}</div>
<div class="phase-steps">
<span v-for="step in phase.steps" :key="step" class="step-tag">{{ step }}</span>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各阶段</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentPhase = ref(0)
const phases = [
{ icon: '🔌', name: '硬件启动', steps: ['电源', '主板', 'CPU', 'BIOS'] },
{ icon: '💻', name: '系统启动', steps: ['引导程序', '内核', '服务', '桌面'] },
{ icon: '🌐', name: '浏览器', steps: ['双击图标', '加载程序', '显示窗口'] },
{ icon: '📡', name: '网络请求', steps: ['URL解析', 'DNS', 'TCP', 'HTTP'] },
{ icon: '🎨', name: '页面渲染', steps: ['HTML解析', 'CSS', '布局', '绘制'] }
]
</script>
<style scoped>
.full-process-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.process-flow {
display: flex;
justify-content: space-between;
gap: 0.5rem;
}
.phase {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.6rem 0.4rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
opacity: 0.4;
transition: all 0.3s;
}
.phase.active {
opacity: 1;
border-color: var(--vp-c-brand);
background: var(--vp-c-bg-soft);
}
.phase-icon {
font-size: 1.4rem;
margin-bottom: 0.3rem;
}
.phase-name {
font-size: 0.7rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.4rem;
}
.phase-steps {
display: flex;
flex-direction: column;
gap: 0.15rem;
width: 100%;
}
.step-tag {
font-size: 0.58rem;
text-align: center;
padding: 0.15rem 0;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-3);
}
.phase.active .step-tag {
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,134 @@
<template>
<div class="fullstack-demo">
<div class="demo-header">
<span class="title">全栈技能树</span>
<span class="subtitle">前后端通吃的核心能力</span>
</div>
<div class="skill-sections">
<div class="skill-section">
<div class="section-title">前端能力</div>
<div class="skill-list">
<div v-for="skill in frontendSkills" :key="skill" class="skill-item">{{ skill }}</div>
</div>
</div>
<div class="skill-section bridge">
<div class="section-title">全栈核心</div>
<div class="skill-list">
<div v-for="skill in bridgeSkills" :key="skill" class="skill-item highlight">{{ skill }}</div>
</div>
</div>
<div class="skill-section">
<div class="section-title">后端能力</div>
<div class="skill-list">
<div v-for="skill in backendSkills" :key="skill" class="skill-item">{{ skill }}</div>
</div>
</div>
</div>
<div class="info-box">
<strong>全栈不等于全部精通</strong>核心是打通前后端能独立完成一个完整功能不需要在每个领域都达到专家级别
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const frontendSkills = ref(['HTML/CSS', 'JavaScript', '框架使用', '响应式设计'])
const backendSkills = ref(['API 设计', '数据库操作', '业务逻辑', '服务器部署'])
const bridgeSkills = ref(['HTTP 协议', 'Git 协作', '调试能力', '系统设计'])
</script>
<style scoped>
.fullstack-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.skill-sections {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.75rem;
}
.skill-section {
padding: 0.6rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.skill-section.bridge {
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-brand-1);
}
.section-title {
font-size: 0.78rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.5rem;
padding-bottom: 0.35rem;
border-bottom: 1px dashed var(--vp-c-divider);
}
.skill-list {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.skill-item {
font-size: 0.72rem;
padding: 0.25rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.skill-item.highlight {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.skill-sections {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,128 @@
<template>
<div class="language-selection-demo">
<div class="demo-header">
<span class="title">语言选择指南</span>
<span class="subtitle">根据目标选语言</span>
</div>
<div class="selection-grid">
<div v-for="item in selections" :key="item.goal" class="selection-card">
<div class="goal-name">{{ item.goal }}</div>
<div class="goal-desc">{{ item.desc }}</div>
<div class="goal-langs">
<span class="lang-label">推荐</span>
<span v-for="lang in item.langs" :key="lang" class="lang-tag">{{ lang }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>核心原则</strong>语言只是工具重要的是解决问题的能力先精通一门再触类旁通
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selections = ref([
{ goal: 'Web 前端', desc: '网页、小程序、H5', langs: ['JavaScript', 'TypeScript'] },
{ goal: 'Web 后端', desc: 'API 服务、业务系统', langs: ['Node.js', 'Go', 'Java', 'Python'] },
{ goal: '移动端', desc: 'iOS / Android 应用', langs: ['Swift', 'Kotlin', 'Flutter'] },
{ goal: 'AI / 数据科学', desc: '机器学习、数据分析', langs: ['Python'] },
{ goal: '系统编程', desc: '操作系统、嵌入式', langs: ['C', 'C++', 'Rust'] },
{ goal: '快速原型', desc: '脚本、自动化、小工具', langs: ['Python', 'Shell'] }
])
</script>
<style scoped>
.language-selection-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.selection-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.selection-card {
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
}
.goal-name {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.15rem;
}
.goal-desc {
font-size: 0.7rem;
color: var(--vp-c-text-3);
margin-bottom: 0.35rem;
}
.goal-langs {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25rem;
}
.lang-label {
font-size: 0.68rem;
color: var(--vp-c-text-3);
}
.lang-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-brand-1);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
@media (max-width: 640px) {
.selection-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -0,0 +1,134 @@
<template>
<div class="strategy-demo">
<div class="demo-header">
<span class="title">Vibe Coding 学习策略</span>
<span class="subtitle">AI 时代怎么学更高效</span>
</div>
<div class="strategy-list">
<div v-for="(strategy, index) in strategies" :key="index" class="strategy-item">
<div class="strategy-num">{{ index + 1 }}</div>
<div class="strategy-content">
<div class="strategy-title">{{ strategy.title }}</div>
<div class="strategy-desc">{{ strategy.desc }}</div>
</div>
</div>
</div>
<div class="info-box">
<strong>核心原则</strong>AI 是你的编程助手但决策者永远是你学会提问学会判断学会整合比学会写代码更重要
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const strategies = ref([
{
title: '先理解,再让 AI 写',
desc: '不要一上来就让 AI 写代码。先理解问题是什么,想清楚解决方案,再用 AI 加速实现。'
},
{
title: '把 AI 当结对编程伙伴',
desc: '遇到不懂的概念,问 AI 解释。遇到复杂问题,和 AI 讨论方案。AI 是你的知识渊博的同事。'
},
{
title: '学会审核 AI 的输出',
desc: 'AI 生成的代码不一定对。你需要有能力判断:逻辑对不对?有没有安全隐患?性能如何?'
},
{
title: '建立自己的知识体系',
desc: 'AI 能帮你查漏补缺,但核心知识框架要自己建立。知道"有什么",才能问出"怎么用"。'
},
{
title: '在实践中学习',
desc: '做真实的项目,解决真实的问题。AI 帮你扫清语法障碍,你专注于解决业务问题。'
}
])
</script>
<style scoped>
.strategy-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.strategy-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-item {
display: flex;
gap: 0.75rem;
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.strategy-num {
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
background: var(--vp-c-brand-1);
border-radius: 50%;
color: white;
flex-shrink: 0;
}
.strategy-content {
flex: 1;
}
.strategy-title {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.strategy-desc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
line-height: 1.5;
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,386 @@
<template>
<div class="cpu-internal-demo">
<div class="demo-title">CPU 内部微架构剖析</div>
<div class="demo-subtitle">点击下方各个模块查看其内部由哪些子电路构成以及工作原理</div>
<div class="demo-container">
<div class="cpu-chip">
<div class="chip-title">CPU 核心 (Central Processing Unit)</div>
<div class="bus-top address-bus" :class="{ active: currentModule === 'address_bus' }" @click="selectModule('address_bus')">地址总线 (Address Bus)</div>
<div class="bus-top data-bus" :class="{ active: currentModule === 'data_bus' }" @click="selectModule('data_bus')">数据总线 (Data Bus)</div>
<div class="cpu-layout">
<!-- 左侧控制单元 -->
<div class="cu-section section-box" :class="{ active: currentModule === 'cu' }" @click.stop="selectModule('cu')">
<h4 class="section-title">控制单元 (Control Unit)</h4>
<div class="sub-modules">
<div class="sub-mod" :class="{ active: currentModule === 'pc' }" @click.stop="selectModule('pc')">程序计数器 (PC)</div>
<div class="sub-mod" :class="{ active: currentModule === 'ir' }" @click.stop="selectModule('ir')">指令寄存器 (IR)</div>
<div class="sub-mod" :class="{ active: currentModule === 'decoder' }" @click.stop="selectModule('decoder')">指令译码器</div>
<div class="sub-mod" :class="{ active: currentModule === 'clock' }" @click.stop="selectModule('clock')">时钟发生器</div>
</div>
<div class="control-lines">控制信号线 </div>
</div>
<!-- 右侧数据通道ALU + 寄存器 -->
<div class="datapath-section">
<!-- 寄存器组 -->
<div class="reg-section section-box" :class="{ active: currentModule === 'reg' }" @click.stop="selectModule('reg')">
<h4 class="section-title">寄存器组 (Register File)</h4>
<div class="sub-modules grid-2">
<div class="sub-mod">通用寄存器 R0-R3</div>
<div class="sub-mod">累加器 (ACC)</div>
</div>
</div>
<!-- ALU -->
<div class="alu-section section-box" :class="{ active: currentModule === 'alu' }" @click.stop="selectModule('alu')">
<h4 class="section-title">算术逻辑单元 (ALU)</h4>
<div class="sub-modules">
<div class="sub-mod" :class="{ active: currentModule === 'adder' }" @click.stop="selectModule('adder')">加法器电路</div>
<div class="sub-mod" :class="{ active: currentModule === 'flags' }" @click.stop="selectModule('flags')">状态标志 (Flags)</div>
</div>
</div>
</div>
</div>
<div class="bus-bottom control-bus" :class="{ active: currentModule === 'control_bus' }" @click="selectModule('control_bus')">控制总线 (Control Bus)</div>
</div>
<!-- 右侧/下方详细说明面板 -->
<div class="details-panel" v-if="currentModuleData">
<h3>{{ currentModuleData.title }}</h3>
<p class="desc">{{ currentModuleData.description }}</p>
<div class="circuit-impl" v-if="currentModuleData.subCircuit">
<h4><span class="icon">🔌</span> 底层子电路实现</h4>
<p>{{ currentModuleData.subCircuit }}</p>
</div>
</div>
<div class="details-panel empty" v-else>
<div class="empty-icon">🖱</div>
<p>点击左侧 CPU 内部结构图的各个模块<br>深入探索其微观电路实现</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const currentModule = ref(null)
const moduleInfo = {
// ALU 相关
alu: {
title: '算术逻辑单元 (ALU)',
description: 'ALU 是 CPU 中负责进行数学运算(加减法)和逻辑运算(与、或、非、异或)的核心引擎。所有的高级数学计算最终都会被分解为 ALU 能够执行的这些基础操作。',
subCircuit: '由海量的逻辑门组成。核心是半加器和全加器的级联(如行波进位加法器或超前进位加法器),并结合多路选择器(MUX)来决定当前是输出加法结果还是某种逻辑运算结果。'
},
adder: {
title: '加法器电路 (Adder)',
description: '负责执行二进制加法。',
subCircuit: '底层由异或门(XOR)负责本位相加,与门(AND)和或门(OR)负责产生对高位的进位信号。几十个全加器串联即可实现 32/64 位数的加法。'
},
flags: {
title: '状态标志寄存器 (Flags)',
description: '记录上一次 ALU 运算的“副作用”特征,例如结果是否为零(Z)、是否产生进位(C)、符号是正还是负(S)、是否溢出(O)。它是实现 `if/else` 等条件跳转指令的核心物理依据。',
subCircuit: '一组特定的触发器(Flip-Flops),每个触发器通过逻辑门直接连接在 ALU 的输出端电路上。'
},
// 寄存器相关
reg: {
title: '寄存器组 (Register File)',
description: 'CPU 内部的高速“草稿本”。由于直接嵌在指令执行的数据通路中,其读写速度和 CPU 主频几乎一致。用来暂存 ALU 需要的输入数据和刚刚算出的输出结果。',
subCircuit: '本质上是由成千上万个 D 型触发器(D Flip-Flop)按位宽(如 64 位)并列组合而成。配合多路选择器和地址译码电路,实现对特定“草稿本”的数据寻址读写。'
},
// 控制单元相关
cu: {
title: '控制单元 (Control Unit, CU)',
description: '整个 CPU 的“大脑和总指挥”。它并不直接参与运算,而是负责从内存读取指令,翻译指令,并像“拉线木偶”一样向全芯片发出各种导通和关断电信号,指挥其余部件开始工作。',
subCircuit: '通常存在有限状态机(FSM)或微程序的实现方式。本质上是一组庞大复杂的逻辑门网络和触发器组合,将输入的二进制指令(如 0x01)映射为激活对应模块的控制电平。'
},
pc: {
title: '程序计数器 (Program Counter, PC)',
description: '永远指向“下一条要执行的指令”在内存中的具体地址。每次成功执行完一条指令,它就会自动递增。当程序发生函数调用或循环跳转时,它的值会被强行改写。',
subCircuit: '一个带有“自增电路(Incrementer)”的寄存器。通过内部的简单半加器加上时钟脉冲边界的触发来同步更新地址值。'
},
ir: {
title: '指令寄存器 (Instruction Register, IR)',
description: '暂存刚刚从内存中读出、当前正在处于“译码”阶段的那条二进制机器指令。',
subCircuit: '同样是一排带写使能(Write-Enable)控制端的触发器(Flip-Flop),在"取指"周期时,写使能为1,锁存进指令数据。'
},
decoder: {
title: '指令译码器 (Instruction Decoder)',
description: '负责破译 IR 中的一长串 0 和 1 到底是什么意思。把二进制的机器码切分成“操作码”(做什么,如做加法)和“操作数”(对谁做,如寻址寄存器)。',
subCircuit: '由大量的与门和非门组成的组合电路网络。比如输入操作码 0010,只有代表“ADD操作”的那根特定输出管脚会被置 1,其他管脚保持 0。'
},
clock: {
title: '时钟发生器 (Clock)',
description: 'CPU 的心脏节拍器。发出持续的方波信号,同步全系统各个部件的工作节奏。每一次时钟波形的上升沿,所有的触发器才会统一改变锁存状态(即节拍)。',
subCircuit: '外部主板上的石英晶振产生极准的基础震荡信号,结合 CPU 内部的锁相环(PLL)倍频电路生成极高频率的脉冲方波。'
},
// 总线
address_bus: {
title: '地址总线 (Address Bus)',
description: '单向传输总线。CPU 通过这组电线,将它想访问的内存单元或 I/O 设备地址发送出去。地址总线的宽度决定了 CPU 最大能寻址多少内存(比如 32 位地址总线最多覆盖 4GB 寻址)。',
subCircuit: '物理上就是一块芯片引出的几十根极其细微的平行导线。通常受到三态门缓冲器(Tri-state Buffer)所驱动。'
},
data_bus: {
title: '数据总线 (Data Bus)',
description: '双向传输总线。在这组电线上,数据可以从 CPU 流向内存,也可以从内存流回 CPU。它的宽度就是我们平常所说的 32位/64位 处理器一次性处理的数据通路宽度。',
subCircuit: '同样是平行的导电线路,但两端接有方向控制引脚的三态门,确保不会由于多方同时施加高低电平导致设备短路。'
},
control_bus: {
title: '控制总线 (Control Bus)',
description: '混合传输总线,承载各种不同类型的核心控制信号:例如“我要读(Read)”、“我要写(Write)”、“外设的中断请求”、“等待反馈”等。',
subCircuit: '每一条线路一般都有单独而明确的功能分配,直接由控制单元(CU)的逻辑组合端引出,连接并支配外部的所有硬件。'
}
}
const currentModuleData = computed(() => moduleInfo[currentModule.value])
function selectModule(mod) {
if (currentModule.value === mod) {
currentModule.value = null
} else {
currentModule.value = mod
}
}
</script>
<style scoped>
.cpu-internal-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1.5rem 0;
}
.demo-title {
text-align: center;
font-weight: 800;
font-size: 1.25rem;
color: var(--vp-c-text-1);
margin-bottom: 0.2rem;
}
.demo-subtitle {
text-align: center;
font-size: 0.9rem;
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
}
.demo-container {
display: flex;
gap: 1.5rem;
align-items: stretch;
}
.cpu-chip {
flex: 3;
background: var(--vp-c-bg);
border: 3px solid #64748b;
border-radius: 12px;
padding: 1rem;
position: relative;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.chip-title {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: #64748b;
color: #fff;
padding: 2px 12px;
border-radius: 4px;
font-weight: bold;
font-size: 0.85rem;
white-space: nowrap;
}
.bus-top, .bus-bottom {
text-align: center;
padding: 0.4rem;
font-size: 0.8rem;
font-weight: bold;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
border: 2px dashed var(--vp-c-text-3);
background: var(--vp-c-bg-alt);
}
.bus-top:hover, .bus-bottom:hover, .bus-top.active, .bus-bottom.active {
border-style: solid;
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.cpu-layout {
display: flex;
gap: 1rem;
flex: 1;
min-height: 280px;
}
.section-box {
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.8rem;
display: flex;
flex-direction: column;
transition: all 0.2s;
cursor: pointer;
background: var(--vp-c-bg-soft);
}
.section-box:hover, .section-box.active {
border-color: var(--vp-c-brand-1);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.cu-section { margin-top: 0; }
.cu-section:hover, .cu-section.active { border-color: #3b82f6; }
.reg-section:hover, .reg-section.active { border-color: #8b5cf6; }
.alu-section:hover, .alu-section.active { border-color: #f59e0b; }
.section-title {
margin: 0 0 0.8rem 0;
font-size: 0.95rem;
text-align: center;
color: var(--vp-c-text-1);
}
.cu-section {
flex: 5;
}
.datapath-section {
flex: 7;
display: flex;
flex-direction: column;
gap: 1rem;
}
.sub-modules {
display: flex;
flex-direction: column;
gap: 0.4rem;
flex: 1;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
}
.sub-mod {
font-size: 0.8rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
padding: 0.5rem;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.sub-mod:hover, .sub-mod.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.cu-section .sub-mod:hover, .cu-section .sub-mod.active { background: #3b82f6; border-color: #3b82f6; }
.alu-section .sub-mod:hover, .alu-section .sub-mod.active { background: #f59e0b; border-color: #f59e0b; }
.reg-section .sub-mod:hover, .reg-section .sub-mod.active { background: #8b5cf6; border-color: #8b5cf6; }
.control-lines {
text-align: center;
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-top: 0.5rem;
font-family: monospace;
}
.details-panel {
flex: 2;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 1.5rem;
box-shadow: inset 0 0 0 1px var(--vp-c-divider);
display: flex;
flex-direction: column;
}
.details-panel h3 {
margin: 0 0 1rem 0;
color: var(--vp-c-brand-1);
font-size: 1.2rem;
border-bottom: 1px solid var(--vp-c-divider);
padding-bottom: 0.5rem;
}
.details-panel .desc {
font-size: 0.95rem;
line-height: 1.6;
color: var(--vp-c-text-2);
margin-bottom: 1.5rem;
}
.circuit-impl {
background: var(--vp-c-bg-soft);
border-left: 4px solid var(--vp-c-brand-2);
padding: 1rem;
border-radius: 0 4px 4px 0;
margin-top: auto;
}
.circuit-impl h4 {
margin: 0 0 0.5rem 0;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.circuit-impl p {
margin: 0;
font-size: 0.9rem;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.empty {
align-items: center;
justify-content: center;
background: transparent;
box-shadow: none;
border: 1px dashed var(--vp-c-divider);
text-align: center;
color: var(--vp-c-text-3);
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
@media (max-width: 800px) {
.demo-container {
flex-direction: column;
}
.cpu-layout {
flex-direction: column;
}
}
</style>
@@ -0,0 +1,115 @@
<template>
<div class="power-on-demo">
<div class="demo-label">电脑启动第一步 点击通电</div>
<div class="sequence" @click="powerOn = !powerOn">
<div class="step" :class="{ active: powerOn }">
<div class="step-icon">🔌</div>
<div class="step-name">电源</div>
<div class="step-status">{{ powerOn ? '✅ 供电中' : '⬜ 等待' }}</div>
</div>
<div class="arrow"></div>
<div class="step" :class="{ active: powerOn }">
<div class="step-icon">🔋</div>
<div class="step-name">主板</div>
<div class="step-status">{{ powerOn ? '✅ 唤醒' : '⬜ 等待' }}</div>
</div>
<div class="arrow"></div>
<div class="step" :class="{ active: powerOn }">
<div class="step-icon"></div>
<div class="step-name">CPU</div>
<div class="step-status">{{ powerOn ? '✅ 复位' : '⬜ 等待' }}</div>
</div>
<div class="arrow"></div>
<div class="step" :class="{ active: powerOn }">
<div class="step-icon">💾</div>
<div class="step-name">BIOS</div>
<div class="step-status">{{ powerOn ? '✅ 启动' : '⬜ 等待' }}</div>
</div>
</div>
<div class="tap-hint">👆 点击切换电源状态</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const powerOn = ref(false)
</script>
<style scoped>
.power-on-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.sequence {
display: flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.6rem;
border-radius: 6px;
opacity: 0.4;
transition: all 0.3s;
}
.step.active {
opacity: 1;
background: var(--vp-c-bg);
}
.step-icon {
font-size: 1.3rem;
margin-bottom: 0.2rem;
}
.step-name {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.step-status {
font-size: 0.65rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.arrow {
font-size: 0.9rem;
color: var(--vp-c-text-3);
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,170 @@
<template>
<div class="language-map-demo">
<div class="demo-header">
<span class="title">编程语言分类</span>
<span class="subtitle">不同维度看语言</span>
</div>
<div class="classification-tabs">
<button
v-for="tab in tabs"
:key="tab.key"
class="tab-btn"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</div>
<div class="classification-content">
<div v-for="item in currentItems" :key="item.name" class="item-card">
<div class="item-name">{{ item.name }}</div>
<div class="item-desc">{{ item.desc }}</div>
<div class="item-examples">
<span v-for="ex in item.examples" :key="ex" class="example-tag">{{ ex }}</span>
</div>
</div>
</div>
<div class="info-box">
<strong>选择建议</strong>先学一门主流语言深入理解编程思想再学其他语言会容易很多
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('type')
const tabs = [
{ key: 'type', label: '按类型系统' },
{ key: 'level', label: '按抽象层级' },
{ key: 'paradigm', label: '按编程范式' }
]
const classifications = {
type: [
{ name: '静态类型', desc: '变量类型在编译时确定', examples: ['Java', 'C++', 'Go', 'TypeScript'] },
{ name: '动态类型', desc: '变量类型在运行时确定', examples: ['Python', 'JavaScript', 'Ruby'] }
],
level: [
{ name: '低级语言', desc: '接近硬件,执行效率高', examples: ['C', '汇编'] },
{ name: '高级语言', desc: '接近人类语言,开发效率高', examples: ['Python', 'Java', 'JavaScript'] }
],
paradigm: [
{ name: '面向对象', desc: '以对象为中心组织代码', examples: ['Java', 'C++', 'Python'] },
{ name: '函数式', desc: '以函数为中心,强调不可变', examples: ['Haskell', 'Elixir', 'Clojure'] },
{ name: '多范式', desc: '支持多种编程风格', examples: ['Python', 'JavaScript', 'Rust'] }
]
}
const currentItems = computed(() => classifications[activeTab.value])
</script>
<style scoped>
.language-map-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--vp-c-divider);
}
.title {
font-size: 0.95rem;
font-weight: 600;
color: var(--vp-c-text-1);
}
.subtitle {
font-size: 0.78rem;
color: var(--vp-c-text-3);
}
.classification-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.tab-btn {
padding: 0.35rem 0.75rem;
font-size: 0.78rem;
border: 1px solid var(--vp-c-divider);
border-radius: 4px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
transition: all 0.2s;
}
.tab-btn:hover {
border-color: var(--vp-c-brand-1);
}
.tab-btn.active {
background: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-1);
color: white;
}
.classification-content {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item-card {
padding: 0.6rem 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
}
.item-name {
font-size: 0.82rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 0.15rem;
}
.item-desc {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 0.35rem;
}
.item-examples {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.example-tag {
font-size: 0.68rem;
padding: 0.15rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 3px;
color: var(--vp-c-text-2);
}
.info-box {
margin-top: 1rem;
padding: 0.75rem;
background: var(--vp-c-bg);
border-radius: 6px;
font-size: 0.8rem;
color: var(--vp-c-text-2);
border-left: 3px solid var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,131 @@
<template>
<div class="rendering-demo">
<div class="demo-label">浏览器渲染过程 点击逐步演示</div>
<div class="pipeline">
<div
v-for="(stage, index) in stages"
:key="stage.name"
class="pipeline-stage"
:class="{ active: currentStage >= index }"
@click="currentStage = index"
>
<div class="stage-header">
<span class="stage-num">{{ index + 1 }}</span>
<span class="stage-name">{{ stage.name }}</span>
</div>
<div class="stage-desc">{{ stage.desc }}</div>
<div v-if="currentStage >= index" class="stage-detail">
{{ stage.detail }}
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各阶段详情</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStage = ref(0)
const stages = [
{ name: 'HTML 解析', desc: '把 HTML 变成 DOM 树', detail: '<div> → Document Object Model 树结构' },
{ name: 'CSS 解析', desc: '把 CSS 变成样式规则', detail: '选择器 + 属性 → 样式规则表' },
{ name: '渲染树', desc: 'DOM + CSS = 渲染树', detail: '哪些节点显示、什么样式' },
{ name: '布局计算', desc: '计算每个元素的位置', detail: '宽高、坐标、层级' },
{ name: '绘制', desc: '把内容画到像素缓冲区', detail: '文字、颜色、图片、边框' },
{ name: '合成', desc: '多层合成一张图', detail: 'GPU 合成,显示到屏幕' }
]
</script>
<style scoped>
.rendering-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.pipeline {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.pipeline-stage {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.6rem;
opacity: 0.4;
transition: all 0.3s;
}
.pipeline-stage.active {
opacity: 1;
border-color: var(--vp-c-brand);
}
.stage-header {
display: flex;
align-items: center;
gap: 0.4rem;
}
.stage-num {
width: 1.2rem;
height: 1.2rem;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.6rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.pipeline-stage.active .stage-num {
background: var(--vp-c-brand);
color: white;
}
.stage-name {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.stage-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.stage-detail {
font-size: 0.65rem;
color: var(--vp-c-brand);
margin-top: 0.3rem;
padding: 0.25rem 0.4rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,133 @@
<template>
<div class="url-demo">
<div class="demo-label">URL 访问流程 点击逐步演示</div>
<div class="flow">
<div
v-for="(step, index) in steps"
:key="step.name"
class="flow-step"
:class="{ active: currentStep >= index }"
@click="currentStep = index"
>
<div class="step-num">{{ index + 1 }}</div>
<div class="step-content">
<div class="step-name">{{ step.name }}</div>
<div class="step-desc">{{ step.desc }}</div>
<div v-if="currentStep >= index && step.detail" class="step-detail">
{{ step.detail }}
</div>
</div>
</div>
</div>
<div class="tap-hint">👆 点击查看各步骤详情</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const currentStep = ref(0)
const steps = [
{ name: 'URL 解析', desc: '提取协议、域名、路径', detail: 'https://www.example.com → 协议:https, 域名:www.example.com' },
{ name: 'DNS 解析', desc: '域名转换为 IP 地址', detail: 'www.example.com → 93.184.216.34' },
{ name: 'TCP 连接', desc: '建立与服务器的连接', detail: '三次握手完成' },
{ name: 'TLS 握手', desc: '建立加密通道(HTTPS', detail: '交换密钥,验证证书' },
{ name: '发送请求', desc: '发送 HTTP 请求', detail: 'GET /index.html HTTP/1.1' },
{ name: '服务器处理', desc: '后端处理请求', detail: '查询数据库,返回 HTML' },
{ name: '返回响应', desc: '接收服务器响应', detail: 'HTTP/1.1 200 OK, Content-Type: text/html' },
{ name: '浏览器渲染', desc: '解析并显示页面', detail: '构建 DOM 树,计算样式,绘制页面' }
]
</script>
<style scoped>
.url-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
cursor: pointer;
user-select: none;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 0.75rem;
letter-spacing: 0.2px;
}
.flow {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.flow-step {
display: flex;
gap: 0.6rem;
padding: 0.4rem;
border-radius: 6px;
opacity: 0.4;
transition: all 0.3s;
background: var(--vp-c-bg);
}
.flow-step.active {
opacity: 1;
}
.step-num {
width: 1.3rem;
height: 1.3rem;
background: var(--vp-c-divider);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
font-weight: 600;
color: var(--vp-c-text-2);
flex-shrink: 0;
}
.flow-step.active .step-num {
background: var(--vp-c-brand);
color: white;
}
.step-content {
flex: 1;
}
.step-name {
font-size: 0.75rem;
font-weight: 600;
color: var(--vp-c-text-2);
}
.step-desc {
font-size: 0.68rem;
color: var(--vp-c-text-3);
margin-top: 0.1rem;
}
.step-detail {
font-size: 0.65rem;
color: var(--vp-c-brand);
margin-top: 0.3rem;
padding: 0.3rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-family: monospace;
}
.tap-hint {
text-align: center;
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-top: 0.75rem;
}
</style>
@@ -0,0 +1,307 @@
<template>
<div class="cert-chain-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔗 证书信任链可视化
</h4>
<p class="intro-text">
点击每一层证书查看它的详细信息和在信任链中的角色
</p>
<div class="chain-container">
<div
v-for="(cert, idx) in certs"
:key="idx"
class="cert-node"
:class="{ selected: selectedIdx === idx }"
:style="{ '--level-color': cert.color }"
@click="selectedIdx = idx"
>
<div class="cert-icon">{{ cert.icon }}</div>
<div class="cert-title">{{ cert.title }}</div>
<div class="cert-subtitle">{{ cert.subtitle }}</div>
<div v-if="idx < certs.length - 1" class="chain-arrow">
<span class="arrow-text">签发</span>
<span class="arrow-symbol"></span>
</div>
</div>
</div>
<div v-if="selectedIdx >= 0" class="detail-panel">
<div
class="detail-header"
:style="{ borderColor: certs[selectedIdx].color }"
>
<span class="detail-icon">{{ certs[selectedIdx].icon }}</span>
<span class="detail-name">{{ certs[selectedIdx].title }}</span>
</div>
<div class="detail-body">
<div class="detail-row" v-for="(item, i) in certs[selectedIdx].details" :key="i">
<span class="detail-label">{{ item.label }}</span>
<span class="detail-value">{{ item.value }}</span>
</div>
</div>
<div class="detail-explain">
{{ certs[selectedIdx].explain }}
</div>
</div>
<div class="verify-box">
<div class="verify-title">🔍 浏览器验证流程</div>
<div class="verify-steps">
<div v-for="(s, i) in verifySteps" :key="i" class="verify-step">
<span class="verify-num">{{ i + 1 }}</span>
<span class="verify-text">{{ s }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selectedIdx = ref(0)
const certs = [
{
icon: '🏛️',
title: '根证书(Root CA',
subtitle: '信任的起点',
color: '#c62828',
explain:
'根证书是整个信任链的锚点。它由根证书颁发机构自签名,预装在操作系统和浏览器中。全球只有少数几十个根 CA,它们的安全性由严格的审计和物理安全措施保障。根 CA 的私钥通常存储在离线的硬件安全模块(HSM)中。',
details: [
{ label: '签发者', value: 'DigiCert Global Root G2(自签名)' },
{ label: '有效期', value: '25 年(2013 - 2038' },
{ label: '密钥长度', value: 'RSA 2048 位' },
{ label: '存储位置', value: '操作系统 / 浏览器内置信任库' },
{ label: '数量级', value: '全球约 150 个受信根证书' }
]
},
{
icon: '🏢',
title: '中间证书(Intermediate CA',
subtitle: '信任的桥梁',
color: '#e65100',
explain:
'中间证书由根 CA 签发,作为根证书和服务器证书之间的桥梁。这种分层设计的好处是:即使中间证书被泄露,也可以单独吊销它而不影响根证书。中间 CA 负责日常的证书签发工作,根 CA 的私钥因此可以保持离线状态。',
details: [
{ label: '签发者', value: 'DigiCert Global Root G2' },
{ label: '持有者', value: 'DigiCert SHA2 Extended Validation Server CA' },
{ label: '有效期', value: '10 年' },
{ label: '用途', value: '签发终端实体(服务器)证书' },
{ label: '可吊销', value: '是(通过 CRL 或 OCSP' }
]
},
{
icon: '🌐',
title: '服务器证书(Server Certificate',
subtitle: '网站的身份证',
color: '#1565c0',
explain:
'服务器证书是网站向浏览器证明自己身份的凭证。它由中间 CA 签发,包含网站的域名、公钥和有效期等信息。当浏览器收到这张证书后,会沿着信任链向上验证,直到找到一个已经信任的根证书为止。',
details: [
{ label: '签发者', value: 'DigiCert SHA2 Extended Validation Server CA' },
{ label: '持有者', value: 'www.example.com' },
{ label: '有效期', value: '1 年(行业标准)' },
{ label: '包含公钥', value: 'ECDSA P-256 公钥' },
{ label: '验证级别', value: 'EV(扩展验证)/ DV(域名验证)' }
]
}
]
const verifySteps = [
'浏览器收到服务器证书,读取其签发者信息',
'找到中间证书,用中间 CA 的公钥验证服务器证书的签名',
'再用根 CA 的公钥验证中间证书的签名',
'确认根证书在本地信任库中 → 整条链验证通过'
]
</script>
<style scoped>
.cert-chain-demo {
background: linear-gradient(135deg, #fce4ec 0%, #fff3e0 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.intro-text {
font-size: 13px;
color: #666;
margin: 0 0 16px 0;
}
.chain-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
margin-bottom: 18px;
}
.cert-node {
position: relative;
background: #fff;
border: 2px solid #e0e0e0;
border-radius: 12px;
padding: 14px 24px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
width: 280px;
max-width: 100%;
}
.cert-node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.cert-node.selected {
border-color: var(--level-color);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: scale(1.03);
}
.cert-icon {
font-size: 30px;
}
.cert-title {
font-weight: 700;
font-size: 14px;
color: #1a1a2e;
margin-top: 4px;
}
.cert-subtitle {
font-size: 12px;
color: #888;
}
.chain-arrow {
display: flex;
flex-direction: column;
align-items: center;
padding: 6px 0;
color: #999;
}
.arrow-text {
font-size: 11px;
}
.arrow-symbol {
font-size: 20px;
line-height: 1;
}
.detail-panel {
background: #fff;
border-radius: 10px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid #e0e0e0;
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 3px solid;
}
.detail-icon {
font-size: 24px;
}
.detail-name {
font-weight: 700;
font-size: 16px;
color: #1a1a2e;
}
.detail-body {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
}
.detail-row {
display: flex;
gap: 8px;
font-size: 13px;
}
.detail-label {
color: #888;
min-width: 80px;
flex-shrink: 0;
}
.detail-value {
color: #333;
font-weight: 500;
word-break: break-all;
}
.detail-explain {
font-size: 13px;
color: #555;
line-height: 1.7;
background: #f5f5f5;
padding: 10px 14px;
border-radius: 8px;
}
.verify-box {
background: #e8f5e9;
border-radius: 10px;
padding: 14px 18px;
}
.verify-title {
font-weight: 700;
color: #2e7d32;
margin-bottom: 10px;
font-size: 14px;
}
.verify-steps {
display: flex;
flex-direction: column;
gap: 8px;
}
.verify-step {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: #333;
}
.verify-num {
background: #4caf50;
color: #fff;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
.verify-text {
line-height: 1.5;
padding-top: 1px;
}
</style>
@@ -0,0 +1,391 @@
<template>
<div class="comparison-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔐 HTTP vs HTTPS 数据传输对比
</h4>
<div class="control-row">
<button
class="mode-btn"
:class="{ active: mode === 'http' }"
@click="mode = 'http'"
>
HTTP明文
</button>
<button
class="mode-btn https"
:class="{ active: mode === 'https' }"
@click="mode = 'https'"
>
HTTPS加密
</button>
<button class="send-btn" :disabled="isSending" @click="sendData">
{{ isSending ? '传输中...' : '发送数据' }}
</button>
</div>
<div class="flow-area">
<div class="endpoint">
<div class="ep-icon">💻</div>
<div class="ep-label">浏览器</div>
<div class="ep-data original">
<div class="data-title">原始数据</div>
<code>{{ originalData }}</code>
</div>
</div>
<div class="transmission">
<div class="wire" :class="mode">
<div class="wire-label">
{{ mode === 'http' ? '🔓 明文传输' : '🔒 加密传输' }}
</div>
<div
class="packet"
:class="{ moving: isSending, done: sendDone }"
>
<code class="packet-text">{{ transmittedData }}</code>
</div>
</div>
<div v-if="mode === 'http'" class="hacker-box">
<div class="hacker-icon">🕵</div>
<div class="hacker-label">中间人可窃听</div>
<div v-if="sendDone" class="hacker-sees">
<code>{{ originalData }}</code>
</div>
</div>
<div v-else class="hacker-box blocked">
<div class="hacker-icon">🕵</div>
<div class="hacker-label">中间人无法解密</div>
<div v-if="sendDone" class="hacker-sees encrypted">
<code>{{ encryptedData }}</code>
</div>
</div>
</div>
<div class="endpoint">
<div class="ep-icon">🖥</div>
<div class="ep-label">服务器</div>
<div v-if="sendDone" class="ep-data received">
<div class="data-title">收到数据</div>
<code>{{ originalData }}</code>
</div>
</div>
</div>
<div class="compare-table">
<table>
<thead>
<tr>
<th>对比项</th>
<th>HTTP</th>
<th>HTTPS</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in compareRows" :key="i">
<td class="row-label">{{ row.label }}</td>
<td class="http-cell">{{ row.http }}</td>
<td class="https-cell">{{ row.https }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const mode = ref('http')
const isSending = ref(false)
const sendDone = ref(false)
const originalData = 'password=MySecret123&user=zhangsan'
const encryptedData = 'a7f2c9...3b8e1d(密文)'
const transmittedData = ref('')
const compareRows = [
{ label: '端口', http: '80', https: '443' },
{ label: '数据加密', http: '无(明文传输)', https: 'TLS 对称加密' },
{ label: '身份验证', http: '无', https: 'CA 证书验证服务器身份' },
{ label: '数据完整性', http: '无保障', https: 'MAC 校验防篡改' },
{ label: 'SEO 影响', http: '搜索引擎降权', https: '搜索引擎优先收录' },
{ label: '性能开销', http: '无额外开销', https: 'TLS 握手增加约 1-2 RTT' }
]
async function sendData() {
if (isSending.value) return
isSending.value = true
sendDone.value = false
if (mode.value === 'http') {
transmittedData.value = originalData
} else {
transmittedData.value = encryptedData
}
await sleep(1500)
sendDone.value = true
isSending.value = false
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
</script>
<style scoped>
.comparison-demo {
background: linear-gradient(135deg, #e8eaf6 0%, #fce4ec 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.control-row {
display: flex;
gap: 8px;
margin-bottom: 18px;
flex-wrap: wrap;
}
.mode-btn {
padding: 7px 18px;
border: 2px solid #ef5350;
border-radius: 8px;
background: #fff;
color: #c62828;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.mode-btn.https {
border-color: #43a047;
color: #2e7d32;
}
.mode-btn.active {
background: #c62828;
color: #fff;
}
.mode-btn.https.active {
background: #2e7d32;
color: #fff;
}
.send-btn {
padding: 7px 18px;
background: #1565c0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
}
.send-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.flow-area {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 18px;
flex-wrap: wrap;
justify-content: center;
}
.endpoint {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
}
.ep-icon {
font-size: 32px;
}
.ep-label {
font-size: 13px;
font-weight: 600;
color: #333;
margin: 4px 0 8px;
}
.ep-data {
background: #fff;
border-radius: 8px;
padding: 8px 12px;
font-size: 11px;
max-width: 160px;
word-break: break-all;
}
.ep-data.original {
border: 1px solid #90caf9;
}
.ep-data.received {
border: 1px solid #a5d6a7;
}
.data-title {
font-size: 10px;
color: #888;
margin-bottom: 4px;
font-weight: 600;
}
.transmission {
flex: 1;
min-width: 180px;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.wire {
width: 100%;
padding: 10px;
border-radius: 8px;
text-align: center;
position: relative;
min-height: 60px;
}
.wire.http {
background: #ffebee;
border: 2px dashed #ef5350;
}
.wire.https {
background: #e8f5e9;
border: 2px solid #43a047;
}
.wire-label {
font-size: 12px;
font-weight: 600;
margin-bottom: 6px;
}
.packet {
font-size: 11px;
opacity: 0;
transition: opacity 0.5s;
}
.packet.moving {
opacity: 1;
animation: slide 1.2s ease-in-out;
}
.packet.done {
opacity: 1;
}
@keyframes slide {
0% {
transform: translateX(-30px);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.packet-text {
font-size: 10px;
word-break: break-all;
}
.hacker-box {
background: #fff3e0;
border: 1px solid #ffcc80;
border-radius: 8px;
padding: 8px 12px;
text-align: center;
width: 100%;
}
.hacker-box.blocked {
background: #f1f8e9;
border-color: #aed581;
}
.hacker-icon {
font-size: 24px;
}
.hacker-label {
font-size: 11px;
font-weight: 600;
color: #e65100;
}
.hacker-box.blocked .hacker-label {
color: #558b2f;
}
.hacker-sees {
margin-top: 4px;
font-size: 10px;
color: #c62828;
word-break: break-all;
}
.hacker-sees.encrypted {
color: #558b2f;
}
.compare-table {
overflow-x: auto;
}
.compare-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
background: #fff;
border-radius: 8px;
overflow: hidden;
}
.compare-table th {
background: #37474f;
color: #fff;
padding: 8px 12px;
text-align: left;
font-weight: 600;
}
.compare-table td {
padding: 8px 12px;
border-bottom: 1px solid #eee;
}
.row-label {
font-weight: 600;
color: #333;
}
.http-cell {
color: #c62828;
}
.https-cell {
color: #2e7d32;
}
</style>
@@ -0,0 +1,249 @@
<template>
<div class="dns-record-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
📋 DNS 记录类型速查
</h4>
<div class="tab-row">
<button
v-for="rec in records"
:key="rec.type"
class="tab-btn"
:class="{ active: selected === rec.type }"
@click="selected = rec.type"
>
{{ rec.type }}
</button>
</div>
<div v-if="current" class="detail-card">
<div class="detail-header">
<span class="type-badge">{{ current.type }}</span>
<span class="type-name">{{ current.name }}</span>
</div>
<p class="type-desc">{{ current.desc }}</p>
<div class="example-block">
<div class="example-title">示例记录</div>
<code class="example-code">{{ current.example }}</code>
</div>
<div class="usage-block">
<div class="usage-title">常见用途</div>
<ul class="usage-list">
<li v-for="(u, i) in current.usages" :key="i">{{ u }}</li>
</ul>
</div>
</div>
<div class="info-box">
<strong>小贴士</strong>
DNS 不只是把域名翻译成 IP它还承载了邮件路由域名验证负载均衡等多种功能全靠不同的记录类型来实现
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selected = ref('A')
const records = [
{
type: 'A',
name: 'Address 记录',
desc: '将域名映射到一个 IPv4 地址。这是最常见的 DNS 记录类型,浏览器访问网站时最终需要的就是这条记录。',
example: 'example.com. IN A 93.184.216.34',
usages: [
'网站域名指向服务器 IP',
'子域名指向不同的服务器',
'配合负载均衡返回多个 IP'
]
},
{
type: 'AAAA',
name: 'IPv6 Address 记录',
desc: '将域名映射到一个 IPv6 地址。随着 IPv4 地址耗尽,AAAA 记录变得越来越重要。',
example: 'example.com. IN AAAA 2606:2800:220:1:248:1893:25c8:1946',
usages: [
'支持 IPv6 网络的设备访问',
'双栈部署(同时配置 A 和 AAAA',
'面向未来的网络架构'
]
},
{
type: 'CNAME',
name: 'Canonical Name 记录',
desc: '将一个域名指向另一个域名(别名)。浏览器会继续解析目标域名,直到找到 A 记录。',
example: 'www.example.com. IN CNAME example.com.',
usages: [
'www 子域名指向主域名',
'CDN 加速(指向 CDN 提供商域名)',
'多个域名指向同一服务'
]
},
{
type: 'MX',
name: 'Mail Exchange 记录',
desc: '指定负责接收该域名邮件的邮件服务器地址和优先级。数字越小优先级越高。',
example: 'example.com. IN MX 10 mail.example.com.',
usages: [
'配置企业邮箱(如 Gmail、Outlook',
'设置邮件服务器优先级',
'邮件备份和容灾'
]
},
{
type: 'TXT',
name: 'Text 记录',
desc: '存储任意文本信息。常用于域名所有权验证、邮件安全策略(SPF/DKIM/DMARC)等场景。',
example: 'example.com. IN TXT "v=spf1 include:_spf.google.com ~all"',
usages: [
'SPF 记录防止邮件伪造',
'SSL 证书申请时的域名验证',
'第三方服务的域名所有权确认'
]
},
{
type: 'NS',
name: 'Name Server 记录',
desc: '指定该域名由哪些 DNS 服务器负责解析。这是 DNS 委派机制的核心。',
example: 'example.com. IN NS ns1.exampledns.com.',
usages: [
'将域名托管到指定 DNS 服务商',
'子域名委派给不同团队管理',
'DNS 服务迁移'
]
}
]
const current = computed(() => records.find((r) => r.type === selected.value))
</script>
<style scoped>
.dns-record-demo {
background: linear-gradient(135deg, #f3e5f5 0%, #ede7f6 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.tab-row {
display: flex;
gap: 6px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.tab-btn {
padding: 6px 16px;
border: 2px solid #ce93d8;
border-radius: 20px;
background: #fff;
color: #7b1fa2;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.tab-btn.active {
background: #7b1fa2;
color: #fff;
border-color: #7b1fa2;
}
.tab-btn:hover:not(.active) {
background: #f3e5f5;
}
.detail-card {
background: #fff;
border-radius: 10px;
padding: 18px;
margin-bottom: 14px;
border: 1px solid #e1bee7;
}
.detail-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.type-badge {
background: #7b1fa2;
color: #fff;
padding: 3px 12px;
border-radius: 6px;
font-weight: 700;
font-size: 14px;
font-family: monospace;
}
.type-name {
font-size: 15px;
color: #555;
font-weight: 500;
}
.type-desc {
font-size: 14px;
color: #333;
line-height: 1.7;
margin: 0 0 14px 0;
}
.example-block {
background: #263238;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 14px;
}
.example-title {
font-size: 11px;
color: #80cbc4;
margin-bottom: 6px;
font-weight: 600;
}
.example-code {
color: #e0f7fa;
font-size: 13px;
font-family: 'Fira Code', monospace;
word-break: break-all;
}
.usage-block {
background: #f3e5f5;
border-radius: 8px;
padding: 12px 16px;
}
.usage-title {
font-size: 12px;
font-weight: 700;
color: #7b1fa2;
margin-bottom: 6px;
}
.usage-list {
margin: 0;
padding-left: 18px;
font-size: 13px;
color: #444;
line-height: 1.8;
}
.info-box {
margin-top: 14px;
padding: 10px 14px;
background: #fff3e0;
border-radius: 8px;
font-size: 13px;
color: #5d4037;
line-height: 1.6;
}
</style>
@@ -0,0 +1,357 @@
<template>
<div class="dns-resolution-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🔍 DNS 解析过程模拟器
</h4>
<div class="input-row">
<input
v-model="domain"
type="text"
placeholder="输入域名,如 www.example.com"
class="domain-input"
@keyup.enter="startResolve"
/>
<button class="resolve-btn" :disabled="isResolving" @click="startResolve">
{{ isResolving ? '解析中...' : '开始解析' }}
</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="resolve-flow">
<div
v-for="(step, idx) in steps"
:key="idx"
class="step-card"
:class="{
active: currentStep === idx,
done: currentStep > idx,
pending: currentStep < idx
}"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-label">{{ step.label }}</div>
<div v-if="currentStep > idx" class="step-result">
{{ step.result }}
</div>
<div v-if="currentStep === idx && isResolving" class="step-spinner">
</div>
<div
v-if="idx < steps.length - 1"
class="arrow"
:class="{ 'arrow-active': currentStep > idx }"
>
</div>
</div>
</div>
<div v-if="resolved" class="result-box">
<div class="result-title"> 解析完成</div>
<div class="result-detail">
<span class="result-domain">{{ domain }}</span>
<span class="result-ip">{{ resolvedIp }}</span>
</div>
<div class="result-time">总耗时 {{ totalTime }}ms模拟</div>
</div>
<div class="info-box">
<strong>解析流程说明</strong>
浏览器访问网站时需要先将域名翻译成 IP
地址这个过程会依次查询多级缓存和服务器直到找到对应的 IP
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const domain = ref('www.example.com')
const isResolving = ref(false)
const resolved = ref(false)
const currentStep = ref(-1)
const resolvedIp = ref('')
const totalTime = ref(0)
const steps = reactive([
{
icon: '🌐',
label: '浏览器缓存',
result: '未命中,继续查询...'
},
{
icon: '💻',
label: '操作系统缓存',
result: '未命中,继续查询...'
},
{
icon: '🔄',
label: '递归解析器',
result: '向根服务器发起查询...'
},
{
icon: '🌍',
label: '根域名服务器',
result: '返回 .com TLD 服务器地址'
},
{
icon: '📂',
label: 'TLD 服务器',
result: '返回权威服务器地址'
},
{
icon: '🏠',
label: '权威 DNS 服务器',
result: ''
}
])
function generateIp() {
const a = 93 + Math.floor(Math.random() * 60)
const b = Math.floor(Math.random() * 256)
const c = Math.floor(Math.random() * 256)
const d = 1 + Math.floor(Math.random() * 254)
return `${a}.${b}.${c}.${d}`
}
async function startResolve() {
if (isResolving.value || !domain.value.trim()) return
isResolving.value = true
resolved.value = false
currentStep.value = -1
const ip = generateIp()
resolvedIp.value = ip
steps[5].result = `找到记录!IP = ${ip}`
const delays = [200, 300, 400, 500, 400, 300]
let total = 0
for (let i = 0; i < steps.length; i++) {
currentStep.value = i
await sleep(delays[i])
total += delays[i]
currentStep.value = i + 1
}
totalTime.value = total
resolved.value = true
isResolving.value = false
}
function reset() {
isResolving.value = false
resolved.value = false
currentStep.value = -1
resolvedIp.value = ''
totalTime.value = 0
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
</script>
<style scoped>
.dns-resolution-demo {
background: linear-gradient(135deg, #f0f4ff 0%, #e8f0fe 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.input-row {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.domain-input {
flex: 1;
min-width: 200px;
padding: 8px 14px;
border: 2px solid #c5cae9;
border-radius: 8px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.domain-input:focus {
border-color: #5c6bc0;
}
.resolve-btn {
padding: 8px 20px;
background: #5c6bc0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.resolve-btn:hover:not(:disabled) {
background: #3f51b5;
}
.resolve-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
padding: 8px 16px;
background: #e0e0e0;
color: #333;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.reset-btn:hover {
background: #bdbdbd;
}
.resolve-flow {
display: flex;
align-items: flex-start;
gap: 4px;
overflow-x: auto;
padding: 10px 0;
flex-wrap: wrap;
justify-content: center;
}
.step-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 10px;
border-radius: 10px;
background: #fff;
min-width: 100px;
max-width: 120px;
text-align: center;
transition: all 0.3s;
border: 2px solid transparent;
position: relative;
opacity: 0.5;
}
.step-card.active {
border-color: #ff9800;
background: #fff8e1;
opacity: 1;
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(255, 152, 0, 0.3);
}
.step-card.done {
border-color: #4caf50;
background: #e8f5e9;
opacity: 1;
}
.step-card.pending {
opacity: 0.4;
}
.step-icon {
font-size: 28px;
margin-bottom: 6px;
}
.step-label {
font-size: 12px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.step-result {
font-size: 10px;
color: #666;
line-height: 1.3;
}
.step-spinner {
font-size: 20px;
animation: pulse 0.8s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.arrow {
position: absolute;
right: -14px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
color: #ccc;
font-weight: bold;
z-index: 1;
}
.arrow-active {
color: #4caf50;
}
.result-box {
margin-top: 16px;
padding: 14px 18px;
background: #e8f5e9;
border-radius: 10px;
border: 1px solid #a5d6a7;
}
.result-title {
font-weight: 700;
color: #2e7d32;
margin-bottom: 6px;
}
.result-detail {
font-size: 15px;
color: #333;
}
.result-domain {
color: #5c6bc0;
font-weight: 600;
}
.result-ip {
color: #e65100;
font-weight: 600;
font-family: monospace;
}
.result-time {
font-size: 12px;
color: #888;
margin-top: 4px;
}
.info-box {
margin-top: 14px;
padding: 10px 14px;
background: #fff3e0;
border-radius: 8px;
font-size: 13px;
color: #5d4037;
line-height: 1.6;
}
</style>
@@ -0,0 +1,298 @@
<template>
<div class="https-handshake-demo">
<h4 style="margin: 0 0 12px 0; color: #1a1a2e">
🤝 TLS 握手过程演示
</h4>
<div class="control-row">
<button class="start-btn" :disabled="isRunning" @click="startHandshake">
{{ isRunning ? '握手进行中...' : '开始 TLS 握手' }}
</button>
<button class="reset-btn" @click="reset">重置</button>
</div>
<div class="handshake-area">
<div class="side client-side">
<div class="side-icon">💻</div>
<div class="side-label">客户端浏览器</div>
</div>
<div class="message-lane">
<div
v-for="(msg, idx) in messages"
:key="idx"
class="msg-row"
:class="{
active: currentStep === idx,
done: currentStep > idx,
pending: currentStep < idx
}"
>
<div
class="msg-arrow"
:class="msg.direction === 'right' ? 'arrow-right' : 'arrow-left'"
>
<span class="arrow-line"></span>
<span class="arrow-head">{{ msg.direction === 'right' ? '→' : '←' }}</span>
</div>
<div class="msg-content">
<div class="msg-name">{{ msg.name }}</div>
<div class="msg-desc">{{ msg.desc }}</div>
</div>
</div>
</div>
<div class="side server-side">
<div class="side-icon">🖥</div>
<div class="side-label">服务器</div>
</div>
</div>
<div v-if="currentStep >= 0 && currentStep < messages.length" class="detail-box">
<div class="detail-title">
{{ messages[currentStep].name }}
</div>
<div class="detail-text">
{{ messages[currentStep].detail }}
</div>
</div>
<div v-if="handshakeDone" class="success-box">
TLS 握手完成后续所有 HTTP 数据都将通过对称加密传输第三方无法窃听
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isRunning = ref(false)
const currentStep = ref(-1)
const handshakeDone = ref(false)
const messages = [
{
name: 'Client Hello',
direction: 'right',
desc: '发送支持的 TLS 版本、加密套件列表、随机数',
detail:
'浏览器向服务器发起连接请求,告知自己支持的 TLS 版本(如 TLS 1.3)、可用的加密算法列表(如 AES-256-GCM)以及一个客户端随机数(Client Random)。这就像自我介绍:"我会这些加密方式,你选一个吧。"'
},
{
name: 'Server Hello',
direction: 'left',
desc: '选定 TLS 版本、加密套件、服务器随机数',
detail:
'服务器从客户端提供的列表中选择一个最优的加密套件,并返回自己的随机数(Server Random)。相当于回应:"好的,我们就用 TLS 1.3 + AES-256-GCM 来通信。"'
},
{
name: 'Certificate',
direction: 'left',
desc: '服务器发送数字证书(含公钥)',
detail:
'服务器将自己的数字证书发送给浏览器。证书中包含服务器的公钥、域名信息以及 CA 的签名。浏览器会验证证书是否由受信任的 CA 签发、是否过期、域名是否匹配。'
},
{
name: 'Key Exchange',
direction: 'right',
desc: '双方协商生成会话密钥',
detail:
'在 TLS 1.3 中,客户端和服务器通过 ECDHE(椭圆曲线 Diffie-Hellman)算法交换密钥材料。双方各自生成临时密钥对,交换公钥后独立计算出相同的"预主密钥",再结合之前的随机数推导出最终的对称会话密钥。'
},
{
name: 'Finished',
direction: 'right',
desc: '双方确认握手成功,开始加密通信',
detail:
'双方各自发送 Finished 消息,其中包含之前所有握手消息的摘要(用刚协商好的密钥加密)。如果对方能正确解密并验证,说明密钥协商成功,后续所有数据都将使用对称加密传输。'
}
]
async function startHandshake() {
if (isRunning.value) return
isRunning.value = true
handshakeDone.value = false
currentStep.value = -1
for (let i = 0; i < messages.length; i++) {
currentStep.value = i
await sleep(1200)
}
handshakeDone.value = true
isRunning.value = false
}
function reset() {
isRunning.value = false
currentStep.value = -1
handshakeDone.value = false
}
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms))
}
</script>
<style scoped>
.https-handshake-demo {
background: linear-gradient(135deg, #e3f2fd 0%, #e8eaf6 100%);
border-radius: 12px;
padding: 20px;
margin: 16px 0;
font-family: system-ui, sans-serif;
}
.control-row {
display: flex;
gap: 8px;
margin-bottom: 18px;
}
.start-btn {
padding: 8px 20px;
background: #1565c0;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.start-btn:hover:not(:disabled) {
background: #0d47a1;
}
.start-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
padding: 8px 16px;
background: #e0e0e0;
color: #333;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
}
.handshake-area {
display: flex;
gap: 12px;
align-items: flex-start;
}
.side {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
padding-top: 10px;
}
.side-icon {
font-size: 36px;
}
.side-label {
font-size: 12px;
font-weight: 600;
color: #333;
margin-top: 4px;
text-align: center;
}
.message-lane {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.msg-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 8px;
background: #fff;
border: 2px solid transparent;
opacity: 0.35;
transition: all 0.4s;
}
.msg-row.active {
opacity: 1;
border-color: #ff9800;
background: #fff8e1;
transform: scale(1.02);
box-shadow: 0 3px 10px rgba(255, 152, 0, 0.2);
}
.msg-row.done {
opacity: 1;
border-color: #4caf50;
background: #e8f5e9;
}
.msg-arrow {
display: flex;
align-items: center;
min-width: 36px;
font-size: 18px;
font-weight: bold;
}
.arrow-right {
color: #1565c0;
}
.arrow-left {
color: #e65100;
}
.msg-name {
font-weight: 700;
font-size: 14px;
color: #1a1a2e;
}
.msg-desc {
font-size: 12px;
color: #666;
margin-top: 2px;
}
.detail-box {
margin-top: 14px;
padding: 14px 18px;
background: #fff;
border-radius: 10px;
border-left: 4px solid #1565c0;
}
.detail-title {
font-weight: 700;
color: #1565c0;
margin-bottom: 6px;
font-size: 15px;
}
.detail-text {
font-size: 13px;
color: #444;
line-height: 1.7;
}
.success-box {
margin-top: 14px;
padding: 12px 18px;
background: #e8f5e9;
border-radius: 10px;
border: 1px solid #a5d6a7;
color: #2e7d32;
font-weight: 600;
font-size: 14px;
}
</style>
@@ -0,0 +1,308 @@
<!--
EmbeddingConceptDemo.vue
嵌入概念可视化组件
用途
将词语/句子嵌入可视化为二维空间中的点展示语义相似的概念如何聚集在一起
交互功能
- 切换不同词组类别查看聚类效果
- 悬停查看词语详情和坐标
- 动态展示语义空间的分布
-->
<template>
<div class="embedding-demo">
<div class="demo-header">
<h4>词嵌入空间可视化</h4>
<p class="desc">语义相近的词语在向量空间中距离更近形成自然聚类</p>
</div>
<div class="controls">
<button
v-for="cat in categories"
:key="cat.key"
class="cat-btn"
:class="{ active: activeCategory === cat.key }"
@click="activeCategory = cat.key"
>
{{ cat.label }}
</button>
</div>
<div class="canvas-wrap">
<svg
ref="svgRef"
viewBox="0 0 500 400"
class="embed-svg"
>
<!-- 坐标轴 -->
<line x1="50" y1="370" x2="480" y2="370" stroke="var(--vp-c-divider)" stroke-width="1" />
<line x1="50" y1="370" x2="50" y2="20" stroke="var(--vp-c-divider)" stroke-width="1" />
<text x="265" y="395" text-anchor="middle" fill="var(--vp-c-text-3)" font-size="12">维度 1</text>
<text x="15" y="195" text-anchor="middle" fill="var(--vp-c-text-3)" font-size="12" transform="rotate(-90, 15, 195)">维度 2</text>
<!-- 聚类椭圆 -->
<ellipse
v-for="cluster in currentClusters"
:key="cluster.label"
:cx="cluster.cx"
:cy="cluster.cy"
:rx="cluster.rx"
:ry="cluster.ry"
:fill="cluster.color"
fill-opacity="0.08"
:stroke="cluster.color"
stroke-opacity="0.3"
stroke-width="1.5"
stroke-dasharray="4 3"
/>
<!-- 数据点 -->
<g
v-for="(point, idx) in currentPoints"
:key="point.word"
class="point-group"
@mouseenter="hoveredPoint = idx"
@mouseleave="hoveredPoint = -1"
>
<circle
:cx="point.x"
:cy="point.y"
:r="hoveredPoint === idx ? 8 : 6"
:fill="point.color"
stroke="#fff"
stroke-width="1.5"
class="data-point"
/>
<text
:x="point.x"
:y="point.y - 12"
text-anchor="middle"
:fill="point.color"
font-size="12"
font-weight="600"
>
{{ point.word }}
</text>
</g>
<!-- 聚类标签 -->
<text
v-for="cluster in currentClusters"
:key="'label-' + cluster.label"
:x="cluster.cx"
:y="cluster.cy + cluster.ry + 16"
text-anchor="middle"
:fill="cluster.color"
font-size="11"
font-weight="500"
opacity="0.7"
>
{{ cluster.label }}
</text>
</svg>
<!-- 悬停信息 -->
<div v-if="hoveredPoint >= 0" class="hover-info">
<span class="hw">{{ currentPoints[hoveredPoint].word }}</span>
<span class="hc">向量: [{{ currentPoints[hoveredPoint].vec.join(', ') }}]</span>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">&#x1F4A1;</span>
嵌入模型将文本映射到高维向量空间通常 768~1536 这里我们将其简化为二维来展示核心思想<strong>语义相近的词语向量距离也更近</strong>
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeCategory = ref('animals-royalty')
const hoveredPoint = ref(-1)
const categories = [
{ key: 'animals-royalty', label: '动物 vs 皇室' },
{ key: 'food-tech', label: '食物 vs 科技' },
{ key: 'emotions', label: '情感词汇' }
]
const dataMap = {
'animals-royalty': {
clusters: [
{ label: '动物', cx: 150, cy: 160, rx: 80, ry: 65, color: '#10b981' },
{ label: '皇室', cx: 370, cy: 200, rx: 75, ry: 60, color: '#8b5cf6' }
],
points: [
{ word: '猫', x: 120, y: 140, color: '#10b981', vec: [0.21, 0.68] },
{ word: '狗', x: 160, y: 180, color: '#10b981', vec: [0.28, 0.55] },
{ word: '老虎', x: 185, y: 130, color: '#10b981', vec: [0.35, 0.72] },
{ word: '兔子', x: 130, y: 195, color: '#10b981', vec: [0.22, 0.48] },
{ word: '国王', x: 350, y: 175, color: '#8b5cf6', vec: [0.82, 0.58] },
{ word: '王后', x: 390, y: 195, color: '#8b5cf6', vec: [0.88, 0.52] },
{ word: '王子', x: 360, y: 225, color: '#8b5cf6', vec: [0.84, 0.42] },
{ word: '公主', x: 395, y: 215, color: '#8b5cf6', vec: [0.89, 0.45] }
]
},
'food-tech': {
clusters: [
{ label: '食物', cx: 140, cy: 240, rx: 85, ry: 70, color: '#f59e0b' },
{ label: '科技', cx: 360, cy: 120, rx: 80, ry: 65, color: '#3b82f6' }
],
points: [
{ word: '苹果(水果)', x: 110, y: 220, color: '#f59e0b', vec: [0.15, 0.38] },
{ word: '面包', x: 155, y: 260, color: '#f59e0b', vec: [0.25, 0.28] },
{ word: '牛奶', x: 130, y: 280, color: '#f59e0b', vec: [0.20, 0.22] },
{ word: '蛋糕', x: 175, y: 230, color: '#f59e0b', vec: [0.30, 0.35] },
{ word: '电脑', x: 340, y: 100, color: '#3b82f6', vec: [0.78, 0.82] },
{ word: '手机', x: 375, y: 130, color: '#3b82f6', vec: [0.85, 0.75] },
{ word: '芯片', x: 355, y: 150, color: '#3b82f6', vec: [0.82, 0.70] },
{ word: '算法', x: 390, y: 110, color: '#3b82f6', vec: [0.88, 0.80] }
]
},
emotions: {
clusters: [
{ label: '积极情感', cx: 150, cy: 130, rx: 90, ry: 70, color: '#10b981' },
{ label: '消极情感', cx: 360, cy: 270, rx: 85, ry: 65, color: '#ef4444' },
{ label: '中性情感', cx: 260, cy: 200, rx: 60, ry: 45, color: '#6b7280' }
],
points: [
{ word: '快乐', x: 120, y: 110, color: '#10b981', vec: [0.15, 0.78] },
{ word: '幸福', x: 155, y: 130, color: '#10b981', vec: [0.22, 0.72] },
{ word: '兴奋', x: 180, y: 100, color: '#10b981', vec: [0.28, 0.82] },
{ word: '悲伤', x: 340, y: 250, color: '#ef4444', vec: [0.78, 0.30] },
{ word: '愤怒', x: 380, y: 270, color: '#ef4444', vec: [0.85, 0.25] },
{ word: '恐惧', x: 360, y: 295, color: '#ef4444', vec: [0.82, 0.18] },
{ word: '平静', x: 245, y: 190, color: '#6b7280', vec: [0.50, 0.52] },
{ word: '淡然', x: 275, y: 210, color: '#6b7280', vec: [0.55, 0.48] }
]
}
}
const currentClusters = computed(() => dataMap[activeCategory.value].clusters)
const currentPoints = computed(() => dataMap[activeCategory.value].points)
</script>
<style scoped>
.embedding-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.cat-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.cat-btn:hover {
background: var(--vp-c-bg-alt);
}
.cat-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.canvas-wrap {
position: relative;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.embed-svg {
width: 100%;
height: auto;
display: block;
}
.data-point {
cursor: pointer;
transition: r 0.2s;
}
.point-group {
transition: opacity 0.2s;
}
.hover-info {
position: absolute;
top: 12px;
right: 12px;
background: var(--vp-c-bg-soft);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 8px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.hw {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.hc {
font-size: 0.75rem;
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box .icon {
margin-right: 4px;
}
.info-box p {
margin: 0;
}
@media (max-width: 640px) {
.embedding-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,453 @@
<!--
EmbeddingPipelineDemo.vue
嵌入生成流水线演示组件
用途
展示从原始文本到向量存储的完整嵌入流水线
Text Tokenize Model Vector Store Query
交互功能
- 输入自定义文本
- 逐步执行流水线
- 每一步展示中间结果
-->
<template>
<div class="pipeline-demo">
<div class="demo-header">
<h4>嵌入生成流水线</h4>
<p class="desc">逐步体验从文本到向量的完整转换过程</p>
</div>
<div class="input-area">
<label>输入文本</label>
<input
v-model="inputText"
type="text"
placeholder="输入一段文本,观察嵌入生成过程..."
class="text-input"
/>
<button class="run-btn" @click="runPipeline">
{{ running ? '处理中...' : '开始处理' }}
</button>
</div>
<!-- 流水线步骤 -->
<div class="pipeline-steps">
<div
v-for="(step, idx) in steps"
:key="step.key"
class="step"
:class="{
active: currentStep >= idx,
current: currentStep === idx && running
}"
>
<div class="step-header">
<div class="step-num" :style="{ background: currentStep >= idx ? step.color : '' }">
{{ idx + 1 }}
</div>
<div class="step-title">{{ step.title }}</div>
<div v-if="currentStep > idx" class="step-check">&#x2713;</div>
</div>
<div v-if="currentStep >= idx" class="step-content">
<div class="step-desc">{{ step.desc }}</div>
<div class="step-output" v-if="stepOutputs[step.key]">
<code>{{ stepOutputs[step.key] }}</code>
</div>
</div>
<!-- 箭头连接 -->
<div v-if="idx < steps.length - 1" class="step-arrow">
<span :class="{ visible: currentStep > idx }">&#x2193;</span>
</div>
</div>
</div>
<!-- 最终结果 -->
<div v-if="currentStep >= steps.length - 1 && !running" class="final-result">
<div class="result-title">嵌入向量已生成</div>
<div class="vector-viz">
<div
v-for="(val, i) in finalVector"
:key="i"
class="vec-bar"
:style="{
height: Math.abs(val) * 60 + 'px',
background: val >= 0 ? '#3b82f6' : '#ef4444',
opacity: 0.4 + Math.abs(val) * 0.6
}"
>
<span class="vec-val">{{ val.toFixed(2) }}</span>
</div>
</div>
<p class="vec-note">
实际嵌入向量通常有 768~1536 个维度这里仅展示前 16 维的模拟值
</p>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const inputText = ref('今天天气真不错,适合出去散步')
const currentStep = ref(-1)
const running = ref(false)
const stepOutputs = reactive({})
const finalVector = ref([])
const steps = [
{
key: 'tokenize',
title: '分词 (Tokenize)',
desc: '将文本拆分为模型可处理的 Token 序列',
color: '#3b82f6'
},
{
key: 'encode',
title: '编码 (Encode)',
desc: '将 Token 映射为数字 ID',
color: '#8b5cf6'
},
{
key: 'model',
title: '模型推理 (Model)',
desc: '通过 Transformer 模型生成上下文感知的向量表示',
color: '#10b981'
},
{
key: 'pool',
title: '池化 (Pooling)',
desc: '将多个 Token 向量聚合为单一句子向量',
color: '#f59e0b'
},
{
key: 'normalize',
title: '归一化 (Normalize)',
desc: '将向量缩放到单位长度,便于余弦相似度计算',
color: '#ef4444'
}
]
function simulateTokenize(text) {
const tokens = []
const zhRegex = /[\u4e00-\u9fa5]/g
const enRegex = /[a-zA-Z]+/g
let i = 0
while (i < text.length) {
if (/[\u4e00-\u9fa5]/.test(text[i])) {
tokens.push(text[i])
i++
} else if (/[a-zA-Z]/.test(text[i])) {
let word = ''
while (i < text.length && /[a-zA-Z]/.test(text[i])) {
word += text[i]
i++
}
tokens.push(word)
} else if (/\s/.test(text[i])) {
i++
} else {
tokens.push(text[i])
i++
}
}
return tokens
}
function hashCode(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return Math.abs(hash)
}
function generateVector(text, dim = 16) {
const vec = []
for (let i = 0; i < dim; i++) {
const seed = hashCode(text + i)
vec.push(((seed % 2000) - 1000) / 1000)
}
// 归一化
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0))
return vec.map((v) => v / (mag || 1))
}
async function runPipeline() {
if (running.value) return
running.value = true
currentStep.value = -1
Object.keys(stepOutputs).forEach((k) => delete stepOutputs[k])
finalVector.value = []
const text = inputText.value || '你好世界'
// Step 1: Tokenize
await delay(400)
currentStep.value = 0
const tokens = simulateTokenize(text)
stepOutputs.tokenize = `[${tokens.map((t) => '"' + t + '"').join(', ')}]`
// Step 2: Encode
await delay(500)
currentStep.value = 1
const ids = tokens.map((t) => hashCode(t) % 50000)
stepOutputs.encode = `[${ids.join(', ')}]`
// Step 3: Model
await delay(600)
currentStep.value = 2
stepOutputs.model = `${tokens.length} 个 Token -> ${tokens.length} x 768 维隐藏状态矩阵`
// Step 4: Pool
await delay(500)
currentStep.value = 3
stepOutputs.pool = `Mean Pooling: ${tokens.length} 个向量 -> 1 个 768 维句子向量`
// Step 5: Normalize
await delay(400)
currentStep.value = 4
finalVector.value = generateVector(text)
stepOutputs.normalize = `L2 归一化: ||v|| = 1.0000`
running.value = false
}
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
</script>
<style scoped>
.pipeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.input-area {
display: flex;
gap: 0.5rem;
align-items: flex-end;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.input-area label {
width: 100%;
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-weight: 600;
margin-bottom: 0.25rem;
}
.text-input {
flex: 1;
min-width: 200px;
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-1);
font-size: 0.9rem;
}
.text-input:focus {
outline: none;
border-color: var(--vp-c-brand);
}
.run-btn {
padding: 8px 18px;
border: none;
border-radius: 6px;
background: var(--vp-c-brand);
color: #fff;
cursor: pointer;
font-size: 0.85rem;
white-space: nowrap;
transition: background 0.2s;
}
.run-btn:hover {
opacity: 0.9;
}
.pipeline-steps {
display: flex;
flex-direction: column;
gap: 0;
}
.step {
padding: 0.75rem;
border-radius: 8px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
opacity: 0.4;
transition: all 0.3s;
}
.step.active {
opacity: 1;
}
.step.current {
border-color: var(--vp-c-brand);
box-shadow: 0 0 0 2px var(--vp-c-brand-soft);
}
.step-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.step-num {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
color: #fff;
background: var(--vp-c-text-3);
flex-shrink: 0;
transition: background 0.3s;
}
.step-title {
font-weight: 600;
font-size: 0.9rem;
color: var(--vp-c-text-1);
}
.step-check {
margin-left: auto;
color: #10b981;
font-weight: 700;
}
.step-content {
margin-top: 0.5rem;
padding-left: 2rem;
}
.step-desc {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 0.4rem;
}
.step-output {
background: var(--vp-c-bg-alt);
border-radius: 4px;
padding: 6px 10px;
overflow-x: auto;
}
.step-output code {
font-size: 0.75rem;
color: var(--vp-c-text-2);
font-family: var(--vp-font-family-mono);
word-break: break-all;
}
.step-arrow {
text-align: center;
height: 24px;
line-height: 24px;
color: var(--vp-c-text-3);
font-size: 1rem;
}
.step-arrow span {
opacity: 0;
transition: opacity 0.3s;
}
.step-arrow span.visible {
opacity: 0.5;
}
.final-result {
margin-top: 1rem;
padding: 1rem;
background: var(--vp-c-bg);
border: 1px solid #10b981;
border-radius: 8px;
}
.result-title {
font-weight: 600;
color: #10b981;
margin-bottom: 0.75rem;
font-size: 0.9rem;
}
.vector-viz {
display: flex;
align-items: flex-end;
gap: 4px;
height: 80px;
padding: 0 4px;
}
.vec-bar {
flex: 1;
min-width: 0;
border-radius: 2px 2px 0 0;
position: relative;
transition: height 0.3s;
}
.vec-val {
position: absolute;
top: -16px;
left: 50%;
transform: translateX(-50%);
font-size: 0.6rem;
color: var(--vp-c-text-3);
font-family: var(--vp-font-family-mono);
white-space: nowrap;
}
.vec-note {
margin: 0.75rem 0 0;
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
@media (max-width: 640px) {
.pipeline-demo {
padding: 1rem;
}
.input-area {
flex-direction: column;
align-items: stretch;
}
.run-btn {
width: 100%;
}
}
</style>
@@ -0,0 +1,401 @@
<!--
VectorDatabaseDemo.vue
向量数据库对比组件
用途
交互式对比主流向量数据库的特性适用场景和架构差异
交互功能
- 点击卡片查看详情
- 对比不同数据库的核心指标
- 场景推荐
-->
<template>
<div class="vdb-demo">
<div class="demo-header">
<h4>主流向量数据库对比</h4>
<p class="desc">点击卡片查看详细信息了解不同向量数据库的特点与适用场景</p>
</div>
<div class="db-grid">
<div
v-for="db in databases"
:key="db.name"
class="db-card"
:class="{ active: selected === db.name }"
@click="selected = selected === db.name ? null : db.name"
>
<div class="card-header">
<span class="db-icon" :style="{ background: db.color }">{{ db.icon }}</span>
<div>
<div class="db-name">{{ db.name }}</div>
<div class="db-type">{{ db.type }}</div>
</div>
</div>
<div class="card-tags">
<span
v-for="tag in db.tags"
:key="tag"
class="tag"
>{{ tag }}</span>
</div>
<div v-if="selected === db.name" class="card-detail">
<div class="detail-row">
<span class="detail-label">开源协议</span>
<span class="detail-val">{{ db.license }}</span>
</div>
<div class="detail-row">
<span class="detail-label">索引算法</span>
<span class="detail-val">{{ db.index }}</span>
</div>
<div class="detail-row">
<span class="detail-label">最大维度</span>
<span class="detail-val">{{ db.maxDim }}</span>
</div>
<div class="detail-row">
<span class="detail-label">适用场景</span>
<span class="detail-val">{{ db.useCase }}</span>
</div>
<p class="detail-desc">{{ db.description }}</p>
</div>
<div class="card-metrics">
<div class="metric">
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: db.perf + '%', background: db.color }"></div>
</div>
<span class="metric-label">性能</span>
</div>
<div class="metric">
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: db.ease + '%', background: db.color }"></div>
</div>
<span class="metric-label">易用性</span>
</div>
<div class="metric">
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: db.scale + '%', background: db.color }"></div>
</div>
<span class="metric-label">扩展性</span>
</div>
</div>
</div>
</div>
<div class="scenario-section">
<h5>场景推荐</h5>
<div class="scenario-grid">
<div
v-for="s in scenarios"
:key="s.title"
class="scenario-card"
>
<div class="scenario-icon">{{ s.icon }}</div>
<div class="scenario-title">{{ s.title }}</div>
<div class="scenario-rec">{{ s.recommend }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const selected = ref(null)
const databases = [
{
name: 'Pinecone',
type: '全托管云服务',
icon: 'P',
color: '#3b82f6',
tags: ['云原生', 'Serverless'],
license: '商业',
index: 'Proprietary ANN',
maxDim: '20,000',
useCase: '快速上线的 AI 应用',
description: '全托管向量数据库,无需运维,按用量付费。适合初创团队和快速原型开发。',
perf: 85,
ease: 95,
scale: 80
},
{
name: 'Milvus',
type: '开源分布式',
icon: 'M',
color: '#10b981',
tags: ['开源', '分布式', '高性能'],
license: 'Apache 2.0',
index: 'IVF / HNSW / DiskANN',
maxDim: '32,768',
useCase: '大规模企业级检索',
description: '支持十亿级向量的分布式数据库,提供丰富的索引类型和混合查询能力。',
perf: 95,
ease: 65,
scale: 95
},
{
name: 'Weaviate',
type: '开源 AI 原生',
icon: 'W',
color: '#8b5cf6',
tags: ['开源', 'GraphQL', '模块化'],
license: 'BSD-3',
index: 'HNSW',
maxDim: '65,536',
useCase: '语义搜索与多模态',
description: '内置向量化模块,支持文本、图像等多模态数据的自动嵌入和检索。',
perf: 80,
ease: 85,
scale: 80
},
{
name: 'Chroma',
type: '轻量级嵌入式',
icon: 'C',
color: '#f59e0b',
tags: ['开源', '轻量', 'Python'],
license: 'Apache 2.0',
index: 'HNSW',
maxDim: '无限制',
useCase: '本地开发与 RAG 原型',
description: '极简 API 设计,几行代码即可集成。非常适合 LangChain / LlamaIndex 生态。',
perf: 60,
ease: 98,
scale: 40
},
{
name: 'pgvector',
type: 'PostgreSQL 扩展',
icon: 'pg',
color: '#ef4444',
tags: ['SQL', 'PostgreSQL', '扩展'],
license: 'PostgreSQL',
index: 'IVFFlat / HNSW',
maxDim: '16,000',
useCase: '已有 PG 基础设施的团队',
description: '在现有 PostgreSQL 中添加向量能力,无需引入新的数据库。支持 SQL 混合查询。',
perf: 65,
ease: 80,
scale: 60
}
]
const scenarios = [
{ icon: '&#x1F680;', title: '快速原型', recommend: 'Chroma / Pinecone' },
{ icon: '&#x1F3E2;', title: '企业级部署', recommend: 'Milvus / Weaviate' },
{ icon: '&#x1F4BE;', title: '已有 PG 数据库', recommend: 'pgvector' },
{ icon: '&#x1F916;', title: 'RAG 应用', recommend: 'Chroma / Weaviate' }
]
</script>
<style scoped>
.vdb-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.db-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.db-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.db-card:hover {
border-color: var(--vp-c-text-3);
}
.db-card.active {
border-color: var(--vp-c-brand);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.6rem;
}
.db-icon {
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 0.8rem;
flex-shrink: 0;
}
.db-name {
font-weight: 600;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.db-type {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.card-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 0.6rem;
}
.tag {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 3px;
background: var(--vp-c-bg-alt);
color: var(--vp-c-text-3);
}
.card-detail {
border-top: 1px solid var(--vp-c-divider);
padding-top: 0.6rem;
margin-bottom: 0.6rem;
}
.detail-row {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
padding: 3px 0;
}
.detail-label {
color: var(--vp-c-text-3);
}
.detail-val {
color: var(--vp-c-text-1);
font-weight: 500;
text-align: right;
}
.detail-desc {
font-size: 0.8rem;
color: var(--vp-c-text-2);
margin: 0.5rem 0 0;
line-height: 1.5;
}
.card-metrics {
display: flex;
flex-direction: column;
gap: 4px;
}
.metric {
display: flex;
align-items: center;
gap: 6px;
}
.metric-bar-wrap {
flex: 1;
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
overflow: hidden;
}
.metric-bar {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.metric-label {
font-size: 0.7rem;
color: var(--vp-c-text-3);
width: 40px;
text-align: right;
}
.scenario-section h5 {
margin: 0 0 0.75rem;
font-size: 0.95rem;
color: var(--vp-c-text-1);
}
.scenario-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 0.5rem;
}
.scenario-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
}
.scenario-icon {
font-size: 1.5rem;
margin-bottom: 4px;
}
.scenario-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 2px;
}
.scenario-rec {
font-size: 0.75rem;
color: var(--vp-c-brand);
font-weight: 500;
}
@media (max-width: 640px) {
.db-grid {
grid-template-columns: 1fr;
}
.scenario-grid {
grid-template-columns: repeat(2, 1fr);
}
.vdb-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,443 @@
<!--
VectorIndexDemo.vue
向量索引策略可视化组件
用途
展示暴力搜索与近似最近邻(ANN)搜索的对比可视化不同索引策略的工作原理
交互功能
- 切换暴力搜索和ANN搜索模式
- 点击查询点触发搜索动画
- 展示搜索过程中访问的节点数量对比
-->
<template>
<div class="index-demo">
<div class="demo-header">
<h4>向量索引策略对比</h4>
<p class="desc">对比暴力搜索与近似最近邻搜索的效率差异</p>
</div>
<div class="controls">
<button
v-for="mode in modes"
:key="mode.key"
class="mode-btn"
:class="{ active: activeMode === mode.key }"
@click="switchMode(mode.key)"
>
{{ mode.label }}
</button>
<button class="search-btn" @click="runSearch">
{{ searching ? '搜索中...' : '开始搜索' }}
</button>
</div>
<div class="canvas-area">
<svg viewBox="0 0 500 380" class="index-svg">
<!-- 数据点 -->
<circle
v-for="(p, i) in points"
:key="'p' + i"
:cx="p.x" :cy="p.y" r="5"
:fill="pointColor(i)"
:opacity="pointOpacity(i)"
stroke="#fff"
stroke-width="0.8"
class="data-pt"
/>
<!-- ANN 分区线 ( ANN 模式) -->
<template v-if="activeMode === 'ann'">
<line
v-for="(line, i) in partitionLines"
:key="'line' + i"
:x1="line.x1" :y1="line.y1"
:x2="line.x2" :y2="line.y2"
stroke="var(--vp-c-brand)"
stroke-width="1"
stroke-dasharray="4 3"
opacity="0.3"
/>
</template>
<!-- 查询点 -->
<g>
<circle
:cx="query.x" :cy="query.y" r="9"
fill="none" stroke="#ef4444" stroke-width="2.5"
/>
<circle :cx="query.x" :cy="query.y" r="3" fill="#ef4444" />
<text
:x="query.x + 14" :y="query.y + 4"
fill="#ef4444" font-size="12" font-weight="600"
>查询点</text>
</g>
<!-- 搜索连线 -->
<line
v-for="(idx, i) in visitedOrder"
:key="'visit' + i"
:x1="query.x" :y1="query.y"
:x2="points[idx].x" :y2="points[idx].y"
:stroke="resultIndices.includes(idx) ? '#10b981' : '#94a3b8'"
:stroke-width="resultIndices.includes(idx) ? 2 : 0.8"
:opacity="resultIndices.includes(idx) ? 0.8 : 0.25"
/>
<!-- 结果高亮 -->
<circle
v-for="idx in resultIndices"
:key="'res' + idx"
:cx="points[idx].x" :cy="points[idx].y" r="8"
fill="none" stroke="#10b981" stroke-width="2.5"
/>
</svg>
</div>
<!-- 统计面板 -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">数据点总数</div>
<div class="stat-val">{{ points.length }}</div>
</div>
<div class="stat-card">
<div class="stat-label">访问节点数</div>
<div class="stat-val" :class="{ good: visitedOrder.length < points.length }">
{{ visitedOrder.length }}
</div>
</div>
<div class="stat-card">
<div class="stat-label">搜索效率</div>
<div class="stat-val efficiency">
{{ points.length > 0 ? ((visitedOrder.length / points.length) * 100).toFixed(0) : 0 }}%
</div>
</div>
<div class="stat-card">
<div class="stat-label">找到最近 K </div>
<div class="stat-val">{{ resultIndices.length }}</div>
</div>
</div>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>策略</th>
<th>时间复杂度</th>
<th>精确度</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr :class="{ 'row-active': activeMode === 'brute' }">
<td>暴力搜索</td>
<td><code>O(n)</code></td>
<td>100%</td>
<td>小数据集 (&lt;10K)</td>
</tr>
<tr :class="{ 'row-active': activeMode === 'ann' }">
<td>ANN (IVF)</td>
<td><code>O(n/k)</code></td>
<td>~95%</td>
<td>大数据集 (>100K)</td>
</tr>
<tr>
<td>HNSW</td>
<td><code>O(log n)</code></td>
<td>~98%</td>
<td>高性能检索</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const activeMode = ref('brute')
const searching = ref(false)
const visitedOrder = ref([])
const resultIndices = ref([])
const modes = [
{ key: 'brute', label: '暴力搜索' },
{ key: 'ann', label: 'ANN 近似搜索' }
]
const query = reactive({ x: 250, y: 190 })
// 生成随机数据点
function generatePoints() {
const pts = []
const rng = (seed) => {
let s = seed
return () => { s = (s * 16807) % 2147483647; return s / 2147483647 }
}
const rand = rng(42)
for (let i = 0; i < 60; i++) {
pts.push({
x: 40 + rand() * 420,
y: 30 + rand() * 320
})
}
return pts
}
const points = ref(generatePoints())
const partitionLines = [
{ x1: 250, y1: 10, x2: 250, y2: 370 },
{ x1: 30, y1: 190, x2: 470, y2: 190 },
{ x1: 140, y1: 10, x2: 140, y2: 370 },
{ x1: 360, y1: 10, x2: 360, y2: 370 }
]
function dist(a, b) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
}
function switchMode(mode) {
activeMode.value = mode
visitedOrder.value = []
resultIndices.value = []
}
function runSearch() {
if (searching.value) return
searching.value = true
visitedOrder.value = []
resultIndices.value = []
const K = 3
const allDists = points.value.map((p, i) => ({ i, d: dist(query, p) }))
allDists.sort((a, b) => a.d - b.d)
const trueTopK = allDists.slice(0, K).map((x) => x.i)
if (activeMode.value === 'brute') {
// 暴力搜索:逐个访问
const order = allDists.map((x) => x.i)
let step = 0
const timer = setInterval(() => {
if (step < order.length) {
visitedOrder.value = order.slice(0, step + 1)
step++
} else {
clearInterval(timer)
resultIndices.value = trueTopK
searching.value = false
}
}, 30)
} else {
// ANN:只搜索查询点所在分区附近
const qPartX = query.x < 140 ? 0 : query.x < 250 ? 1 : query.x < 360 ? 2 : 3
const qPartY = query.y < 190 ? 0 : 1
const nearby = points.value
.map((p, i) => {
const pPartX = p.x < 140 ? 0 : p.x < 250 ? 1 : p.x < 360 ? 2 : 3
const pPartY = p.y < 190 ? 0 : 1
const samePart = Math.abs(pPartX - qPartX) <= 1 && pPartY === qPartY
return { i, d: dist(query, p), samePart }
})
.filter((x) => x.samePart)
.sort((a, b) => a.d - b.d)
const order = nearby.map((x) => x.i)
let step = 0
const timer = setInterval(() => {
if (step < order.length) {
visitedOrder.value = order.slice(0, step + 1)
step++
} else {
clearInterval(timer)
resultIndices.value = nearby.slice(0, K).map((x) => x.i)
searching.value = false
}
}, 50)
}
}
function pointColor(i) {
if (resultIndices.value.includes(i)) return '#10b981'
if (visitedOrder.value.includes(i)) return '#3b82f6'
return '#94a3b8'
}
function pointOpacity(i) {
if (resultIndices.value.includes(i)) return 1
if (visitedOrder.value.includes(i)) return 0.8
return 0.4
}
</script>
<style scoped>
.index-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.controls {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.mode-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.mode-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.search-btn {
padding: 6px 16px;
border: 1px solid #10b981;
border-radius: 6px;
background: #10b981;
color: #fff;
cursor: pointer;
font-size: 0.85rem;
margin-left: auto;
transition: all 0.2s;
}
.search-btn:hover {
background: #059669;
}
.canvas-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.index-svg {
width: 100%;
height: auto;
display: block;
}
.data-pt {
transition: fill 0.3s, opacity 0.3s;
}
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.stat-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
text-align: center;
}
.stat-label {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 2px;
}
.stat-val {
font-size: 1.2rem;
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.stat-val.good {
color: #10b981;
}
.stat-val.efficiency {
color: var(--vp-c-brand);
}
.comparison-table {
overflow-x: auto;
}
.comparison-table table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
.comparison-table th,
.comparison-table td {
padding: 8px 12px;
border: 1px solid var(--vp-c-divider);
text-align: left;
}
.comparison-table th {
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
font-weight: 600;
}
.comparison-table td {
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-1);
}
.row-active {
background: var(--vp-c-brand-soft) !important;
}
.row-active td {
background: var(--vp-c-brand-soft) !important;
border-color: var(--vp-c-brand);
}
code {
font-family: var(--vp-font-family-mono);
font-size: 0.8rem;
background: var(--vp-c-bg);
padding: 2px 4px;
border-radius: 3px;
}
@media (max-width: 640px) {
.stats-row {
grid-template-columns: repeat(2, 1fr);
}
.index-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,398 @@
<!--
VectorSimilarityDemo.vue
向量相似度交互演示组件
用途
交互式展示余弦相似度和欧氏距离的计算过程用户可拖动点观察相似度变化
交互功能
- 拖动两个向量端点
- 实时计算余弦相似度和欧氏距离
- 切换度量方式查看差异
-->
<template>
<div class="similarity-demo">
<div class="demo-header">
<h4>向量相似度计算器</h4>
<p class="desc">拖动向量端点观察不同相似度指标的实时变化</p>
</div>
<div class="metric-tabs">
<button
v-for="m in metrics"
:key="m.key"
class="metric-btn"
:class="{ active: activeMetric === m.key }"
@click="activeMetric = m.key"
>
{{ m.label }}
</button>
</div>
<div class="canvas-area">
<svg
ref="svgEl"
viewBox="0 0 460 360"
class="sim-svg"
@mousemove="onDrag"
@mouseup="stopDrag"
@mouseleave="stopDrag"
>
<!-- 网格 -->
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="var(--vp-c-divider)" stroke-width="0.5" opacity="0.5" />
</pattern>
</defs>
<rect x="30" y="10" width="400" height="320" fill="url(#grid)" />
<!-- 坐标轴 -->
<line x1="230" y1="10" x2="230" y2="330" stroke="var(--vp-c-divider)" stroke-width="1" />
<line x1="30" y1="170" x2="430" y2="170" stroke="var(--vp-c-divider)" stroke-width="1" />
<!-- 夹角弧线 (余弦相似度模式) -->
<path
v-if="activeMetric === 'cosine'"
:d="anglePath"
fill="none"
stroke="var(--vp-c-brand)"
stroke-width="1.5"
stroke-dasharray="3 2"
opacity="0.6"
/>
<!-- 距离线 (欧氏距离模式) -->
<line
v-if="activeMetric === 'euclidean'"
:x1="vecA.x" :y1="vecA.y"
:x2="vecB.x" :y2="vecB.y"
stroke="#ef4444"
stroke-width="2"
stroke-dasharray="6 3"
opacity="0.7"
/>
<!-- 向量 A -->
<line x1="230" y1="170" :x2="vecA.x" :y2="vecA.y" stroke="#3b82f6" stroke-width="2.5" />
<polygon :points="arrowHead(230, 170, vecA.x, vecA.y)" fill="#3b82f6" />
<circle
:cx="vecA.x" :cy="vecA.y" r="10"
fill="#3b82f6" stroke="#fff" stroke-width="2"
class="drag-handle"
@mousedown.prevent="startDrag('A')"
/>
<text :x="vecA.x + 14" :y="vecA.y - 8" fill="#3b82f6" font-size="13" font-weight="600">A</text>
<!-- 向量 B -->
<line x1="230" y1="170" :x2="vecB.x" :y2="vecB.y" stroke="#10b981" stroke-width="2.5" />
<polygon :points="arrowHead(230, 170, vecB.x, vecB.y)" fill="#10b981" />
<circle
:cx="vecB.x" :cy="vecB.y" r="10"
fill="#10b981" stroke="#fff" stroke-width="2"
class="drag-handle"
@mousedown.prevent="startDrag('B')"
/>
<text :x="vecB.x + 14" :y="vecB.y - 8" fill="#10b981" font-size="13" font-weight="600">B</text>
<!-- 原点标记 -->
<circle cx="230" cy="170" r="3" fill="var(--vp-c-text-3)" />
</svg>
</div>
<!-- 结果面板 -->
<div class="results">
<div class="result-card" :class="{ highlight: activeMetric === 'cosine' }">
<div class="result-label">余弦相似度</div>
<div class="result-value">{{ cosineSim.toFixed(4) }}</div>
<div class="result-bar">
<div class="bar-fill cosine-bar" :style="{ width: ((cosineSim + 1) / 2 * 100) + '%' }"></div>
</div>
<div class="result-range">-1 (相反) ~ 1 (相同)</div>
</div>
<div class="result-card" :class="{ highlight: activeMetric === 'euclidean' }">
<div class="result-label">欧氏距离</div>
<div class="result-value">{{ euclideanDist.toFixed(2) }}</div>
<div class="result-bar">
<div class="bar-fill euclidean-bar" :style="{ width: Math.min(euclideanDist / 5 * 100, 100) + '%' }"></div>
</div>
<div class="result-range">0 (完全重合) ~ &#x221E; (无穷远)</div>
</div>
<div class="result-card">
<div class="result-label">点积</div>
<div class="result-value">{{ dotProduct.toFixed(2) }}</div>
<div class="result-hint">dot(A, B) = |A||B|cos&#x3B8;</div>
</div>
</div>
<div class="info-box">
<p>
<span class="icon">&#x1F4A1;</span>
<strong>余弦相似度</strong>只关注方向不关注长度适合文本语义比较<strong>欧氏距离</strong>同时考虑方向和大小适合需要绝对距离的场景
</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeMetric = ref('cosine')
const dragging = ref(null)
const metrics = [
{ key: 'cosine', label: '余弦相似度' },
{ key: 'euclidean', label: '欧氏距离' }
]
const vecA = ref({ x: 350, y: 80 })
const vecB = ref({ x: 370, y: 250 })
const svgEl = ref(null)
function toVec(p) {
return { x: (p.x - 230) / 100, y: (170 - p.y) / 100 }
}
const cosineSim = computed(() => {
const a = toVec(vecA.value)
const b = toVec(vecB.value)
const dot = a.x * b.x + a.y * b.y
const magA = Math.sqrt(a.x * a.x + a.y * a.y)
const magB = Math.sqrt(b.x * b.x + b.y * b.y)
if (magA === 0 || magB === 0) return 0
return dot / (magA * magB)
})
const euclideanDist = computed(() => {
const a = toVec(vecA.value)
const b = toVec(vecB.value)
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
})
const dotProduct = computed(() => {
const a = toVec(vecA.value)
const b = toVec(vecB.value)
return a.x * b.x + a.y * b.y
})
const anglePath = computed(() => {
const r = 40
const ax = vecA.value.x - 230
const ay = vecA.value.y - 170
const bx = vecB.value.x - 230
const by = vecB.value.y - 170
const angA = Math.atan2(ay, ax)
const angB = Math.atan2(by, bx)
const x1 = 230 + r * Math.cos(angA)
const y1 = 170 + r * Math.sin(angA)
const x2 = 230 + r * Math.cos(angB)
const y2 = 170 + r * Math.sin(angB)
const diff = angB - angA
const large = Math.abs(diff) > Math.PI ? 1 : 0
const sweep = diff > 0 ? 1 : 0
return `M ${x1} ${y1} A ${r} ${r} 0 ${large} ${sweep} ${x2} ${y2}`
})
function arrowHead(x1, y1, x2, y2) {
const dx = x2 - x1
const dy = y2 - y1
const len = Math.sqrt(dx * dx + dy * dy)
if (len < 1) return ''
const ux = dx / len
const uy = dy / len
const px = -uy
const py = ux
const tipX = x2
const tipY = y2
const s = 8
const p1x = tipX - ux * s + px * s * 0.4
const p1y = tipY - uy * s + py * s * 0.4
const p2x = tipX - ux * s - px * s * 0.4
const p2y = tipY - uy * s - py * s * 0.4
return `${tipX},${tipY} ${p1x},${p1y} ${p2x},${p2y}`
}
function startDrag(which) {
dragging.value = which
}
function stopDrag() {
dragging.value = null
}
function onDrag(e) {
if (!dragging.value || !svgEl.value) return
const svg = svgEl.value
const rect = svg.getBoundingClientRect()
const scaleX = 460 / rect.width
const scaleY = 360 / rect.height
const x = Math.max(30, Math.min(430, (e.clientX - rect.left) * scaleX))
const y = Math.max(10, Math.min(330, (e.clientY - rect.top) * scaleY))
if (dragging.value === 'A') {
vecA.value = { x, y }
} else {
vecB.value = { x, y }
}
}
</script>
<style scoped>
.similarity-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1.5rem;
margin: 1rem 0;
}
.demo-header h4 {
margin: 0 0 0.25rem;
font-size: 1rem;
color: var(--vp-c-text-1);
}
.desc {
margin: 0 0 1rem;
font-size: 0.85rem;
color: var(--vp-c-text-3);
}
.metric-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.metric-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.metric-btn.active {
border-color: var(--vp-c-brand);
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-dark);
}
.canvas-area {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
margin-bottom: 1rem;
overflow: hidden;
}
.sim-svg {
width: 100%;
height: auto;
display: block;
user-select: none;
}
.drag-handle {
cursor: grab;
}
.drag-handle:active {
cursor: grabbing;
}
.results {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.result-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
text-align: center;
transition: border-color 0.2s;
}
.result-card.highlight {
border-color: var(--vp-c-brand);
}
.result-label {
font-size: 0.8rem;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.result-value {
font-size: 1.4rem;
font-weight: 700;
color: var(--vp-c-text-1);
font-family: var(--vp-font-family-mono);
}
.result-bar {
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
margin: 6px 0 4px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
.cosine-bar {
background: #3b82f6;
}
.euclidean-bar {
background: #ef4444;
}
.result-range {
font-size: 0.7rem;
color: var(--vp-c-text-3);
}
.result-hint {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 6px;
font-family: var(--vp-font-family-mono);
}
.info-box {
padding: 0.75rem;
background: var(--vp-c-bg-alt);
border-radius: 6px;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.info-box p {
margin: 0;
}
.info-box .icon {
margin-right: 4px;
}
@media (max-width: 640px) {
.results {
grid-template-columns: 1fr;
}
.similarity-demo {
padding: 1rem;
}
}
</style>
@@ -0,0 +1,444 @@
<!--
AlertEscalationDemo.vue
告警升级流程演示展示告警如何根据严重程度和时间逐级升级
-->
<template>
<div class="alert-escalation-demo">
<div class="header">
<div class="title">告警升级流程 (Alert Escalation)</div>
<div class="subtitle">选择一个场景观察告警如何逐级升级</div>
</div>
<div class="scenario-select">
<button
v-for="s in scenarios"
:key="s.id"
:class="['scenario-btn', { active: activeScenario === s.id }]"
@click="startScenario(s.id)"
>
{{ s.name }}
</button>
</div>
<div class="escalation-flow">
<div
v-for="(step, index) in escalationSteps"
:key="step.id"
:class="[
'esc-step',
{
active: currentStep === index,
completed: currentStep > index,
pending: currentStep < index
}
]"
>
<div class="esc-left">
<div class="esc-icon" :style="{ background: step.color }">
{{ step.icon }}
</div>
<div v-if="index < escalationSteps.length - 1" class="esc-line">
<div
class="esc-line-fill"
:class="{ filled: currentStep > index }"
></div>
</div>
</div>
<div class="esc-content">
<div class="esc-header">
<span class="esc-title">{{ step.title }}</span>
<span class="esc-time">{{ step.time }}</span>
</div>
<div class="esc-desc">{{ step.desc }}</div>
<div v-if="step.action && currentStep >= index" class="esc-action">
{{ step.action }}
</div>
</div>
</div>
</div>
<div v-if="activeScenario" class="timer-bar">
<div class="timer-label">
升级进度 {{ currentStep + 1 }} / {{ escalationSteps.length }}
</div>
<div class="timer-track">
<div
class="timer-fill"
:style="{
width: ((currentStep + 1) / escalationSteps.length) * 100 + '%'
}"
></div>
</div>
<div class="timer-controls">
<button
class="ctrl-btn"
@click="prevStep"
:disabled="currentStep <= 0"
>
上一级
</button>
<button
class="ctrl-btn"
@click="nextStep"
:disabled="currentStep >= escalationSteps.length - 1"
>
下一级升级
</button>
</div>
</div>
<div class="rule-box">
<div class="rule-title">升级规则说明</div>
<div class="rules">
<div class="rule-item">
<span class="rule-dot" style="background: #22c55e"></span>
<span>P3/P4 告警仅通知值班工程师无需升级</span>
</div>
<div class="rule-item">
<span class="rule-dot" style="background: #eab308"></span>
<span>P2 告警15 分钟未响应则升级至团队负责人</span>
</div>
<div class="rule-item">
<span class="rule-dot" style="background: #f59e0b"></span>
<span>P1 告警5 分钟未响应升级30 分钟未解决升级至总监</span>
</div>
<div class="rule-item">
<span class="rule-dot" style="background: #ef4444"></span>
<span>P0 告警立即通知全链路15 分钟未缓解升级至 VP/CTO</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeScenario = ref(null)
const currentStep = ref(0)
const scenarios = [
{ id: 'p0', name: 'P0 数据库宕机' },
{ id: 'p1', name: 'P1 接口超时' },
{ id: 'p2', name: 'P2 性能下降' }
]
const scenarioSteps = {
p0: [
{
id: 1,
icon: '📡',
color: '#3b82f6',
title: '监控系统检测',
time: 'T+0s',
desc: 'Prometheus 检测到数据库连接池耗尽,所有查询超时',
action: '自动触发 P0 级别告警'
},
{
id: 2,
icon: '📱',
color: '#f59e0b',
title: '值班工程师',
time: 'T+30s',
desc: '电话 + 短信 + 即时通讯同时通知值班 DBA',
action: '值班工程师确认告警,开始排查'
},
{
id: 3,
icon: '👥',
color: '#ef4444',
title: '团队负责人',
time: 'T+5min',
desc: '自动升级至数据库团队负责人和后端团队负责人',
action: '团队负责人召集紧急会议'
},
{
id: 4,
icon: '🎖️',
color: '#8b5cf6',
title: '技术总监',
time: 'T+15min',
desc: '问题未缓解,自动升级至技术总监',
action: '总监协调跨团队资源,启动应急预案'
},
{
id: 5,
icon: '🏢',
color: '#1e293b',
title: 'VP / CTO',
time: 'T+30min',
desc: '重大事故升级至高管层,准备对外沟通',
action: 'CTO 决策是否启动灾备切换'
}
],
p1: [
{
id: 1,
icon: '📡',
color: '#3b82f6',
title: '监控系统检测',
time: 'T+0s',
desc: 'API 网关检测到 P99 延迟超过 3 秒阈值',
action: '触发 P1 级别告警'
},
{
id: 2,
icon: '📱',
color: '#f59e0b',
title: '值班工程师',
time: 'T+1min',
desc: '即时通讯 + 短信通知值班后端工程师',
action: '工程师开始查看监控面板和日志'
},
{
id: 3,
icon: '👥',
color: '#ef4444',
title: '团队负责人',
time: 'T+15min',
desc: '15 分钟未解决,自动升级至团队负责人',
action: '负责人评估是否需要更多人力支援'
},
{
id: 4,
icon: '🎖️',
color: '#8b5cf6',
title: '技术总监',
time: 'T+30min',
desc: '30 分钟未缓解,升级至技术总监',
action: '总监决定是否升级为 P0'
}
],
p2: [
{
id: 1,
icon: '📡',
color: '#3b82f6',
title: '监控系统检测',
time: 'T+0s',
desc: '检测到页面加载时间从 1.2s 上升到 2.8s',
action: '触发 P2 级别告警'
},
{
id: 2,
icon: '📱',
color: '#eab308',
title: '值班工程师',
time: 'T+5min',
desc: '即时通讯通知值班前端工程师',
action: '工程师确认问题,记录工单'
},
{
id: 3,
icon: '👥',
color: '#f59e0b',
title: '团队负责人',
time: 'T+30min',
desc: '30 分钟未响应时升级至团队负责人',
action: '负责人安排当天修复'
}
]
}
const escalationSteps = computed(() => {
if (!activeScenario.value) return scenarioSteps.p0
return scenarioSteps[activeScenario.value]
})
const startScenario = (id) => {
activeScenario.value = id
currentStep.value = 0
}
const nextStep = () => {
if (currentStep.value < escalationSteps.value.length - 1) {
currentStep.value++
}
}
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--
}
}
</script>
<style scoped>
.alert-escalation-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1.5rem; }
.title { font-weight: 700; font-size: 1.1rem; margin-bottom: 0.25rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.scenario-select {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.scenario-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.scenario-btn:hover { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.scenario-btn.active { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.escalation-flow {
display: flex;
flex-direction: column;
margin-bottom: 1.5rem;
}
.esc-step {
display: flex;
gap: 1rem;
opacity: 0.4;
transition: all 0.3s;
}
.esc-step.active,
.esc-step.completed { opacity: 1; }
.esc-left {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
}
.esc-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: #fff;
z-index: 1;
}
.esc-line {
width: 3px;
flex: 1;
min-height: 20px;
background: var(--vp-c-divider);
margin: 4px 0;
}
.esc-line-fill {
width: 100%;
height: 0;
background: var(--vp-c-brand);
transition: height 0.5s;
}
.esc-line-fill.filled { height: 100%; }
.esc-content {
padding-bottom: 1rem;
flex: 1;
}
.esc-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.25rem;
}
.esc-title { font-weight: 600; font-size: 0.95rem; }
.esc-time { font-size: 0.8rem; color: var(--vp-c-text-3); font-family: monospace; }
.esc-desc { font-size: 0.85rem; color: var(--vp-c-text-2); margin-bottom: 0.3rem; }
.esc-action {
font-size: 0.85rem;
padding: 0.4rem 0.6rem;
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.08);
border-radius: 4px;
border-left: 3px solid var(--vp-c-brand);
color: var(--vp-c-text-1);
}
.timer-bar {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.timer-label { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.5rem; }
.timer-track {
height: 6px;
background: var(--vp-c-divider);
border-radius: 3px;
margin-bottom: 0.75rem;
overflow: hidden;
}
.timer-fill {
height: 100%;
background: var(--vp-c-brand);
border-radius: 3px;
transition: width 0.3s;
}
.timer-controls { display: flex; gap: 0.5rem; }
.ctrl-btn {
padding: 0.4rem 0.8rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.ctrl-btn:hover:not(:disabled) { border-color: var(--vp-c-brand); color: var(--vp-c-brand); }
.ctrl-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.rule-box {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 0.75rem;
border: 1px solid var(--vp-c-divider);
}
.rule-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
.rules { display: flex; flex-direction: column; gap: 0.5rem; }
.rule-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
}
.rule-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
@media (max-width: 768px) {
.scenario-select { flex-direction: column; }
.scenario-btn { width: 100%; }
}
</style>
@@ -0,0 +1,356 @@
<!--
IncidentCommandDemo.vue
事故指挥体系演示展示事故响应中的角色分工和协作关系
-->
<template>
<div class="incident-command-demo">
<div class="header">
<div class="title">事故指挥体系 (Incident Command System)</div>
<div class="subtitle">点击角色卡片了解各角色的职责和协作关系</div>
</div>
<div class="org-chart">
<div class="org-level org-top">
<div
:class="['role-card', 'commander', { active: activeRole === 'ic' }]"
@click="selectRole('ic')"
>
<div class="role-icon">🎖</div>
<div class="role-name">事故指挥官</div>
<div class="role-eng">Incident Commander</div>
</div>
</div>
<div class="org-connector">
<div class="connector-line"></div>
</div>
<div class="org-level org-middle">
<div
v-for="role in middleRoles"
:key="role.id"
:class="['role-card', { active: activeRole === role.id }]"
@click="selectRole(role.id)"
>
<div class="role-icon">{{ role.icon }}</div>
<div class="role-name">{{ role.name }}</div>
<div class="role-eng">{{ role.eng }}</div>
</div>
</div>
</div>
<div v-if="currentRole" class="role-detail">
<div class="detail-header" :style="{ background: currentRole.color }">
<span class="detail-icon">{{ currentRole.icon }}</span>
<span class="detail-name">{{ currentRole.name }}</span>
</div>
<div class="detail-body">
<div class="detail-section">
<div class="section-label">核心职责</div>
<div class="responsibilities">
<div
v-for="(r, i) in currentRole.responsibilities"
:key="i"
class="resp-item"
>
<span class="resp-num">{{ i + 1 }}</span>
<span>{{ r }}</span>
</div>
</div>
</div>
<div class="detail-section">
<div class="section-label">关键能力</div>
<div class="skills">
<span
v-for="skill in currentRole.skills"
:key="skill"
class="skill-tag"
>
{{ skill }}
</span>
</div>
</div>
<div class="detail-section">
<div class="section-label">常见话术</div>
<div class="quote-box">
"{{ currentRole.quote }}"
</div>
</div>
</div>
</div>
<div class="scenario-box">
<div class="scenario-title">模拟场景支付系统 P0 事故</div>
<div class="scenario-timeline">
<div
v-for="(event, i) in scenarioEvents"
:key="i"
class="event-item"
>
<span class="event-time">{{ event.time }}</span>
<span
class="event-role"
:style="{ background: event.color }"
>
{{ event.role }}
</span>
<span class="event-text">{{ event.text }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeRole = ref('ic')
const allRoles = {
ic: {
id: 'ic',
icon: '🎖️',
name: '事故指挥官',
eng: 'Incident Commander',
color: '#8b5cf6',
responsibilities: [
'统筹协调整个事故响应过程',
'做出关键决策(回滚、切流、降级等)',
'确保各角色高效协作,避免混乱',
'控制事故响应节奏,定时同步进展'
],
skills: ['全局视野', '决策能力', '沟通协调', '压力管理'],
quote: '当前状态:支付服务不可用。运维组排查数据库,后端组准备回滚方案,通讯组每 10 分钟同步一次。'
},
comm: {
id: 'comm',
icon: '📢',
name: '通讯协调员',
eng: 'Communications Lead',
color: '#3b82f6',
responsibilities: [
'对内:定时向管理层和相关团队通报进展',
'对外:更新状态页面,通知受影响客户',
'记录事故时间线,为复盘提供素材',
'过滤噪音信息,确保指挥官专注决策'
],
skills: ['文字表达', '信息整理', '多方沟通', '时间管理'],
quote: '状态更新:我们已识别到支付服务异常,团队正在紧急处理中,预计 30 分钟内恢复。'
},
ops: {
id: 'ops',
icon: '🔧',
name: '运维负责人',
eng: 'Operations Lead',
color: '#ef4444',
responsibilities: [
'执行具体的技术操作(回滚、重启、扩容等)',
'监控系统指标变化,判断操作效果',
'管理基础设施层面的应急响应',
'向指挥官汇报技术层面的进展'
],
skills: ['系统运维', '故障排查', '脚本自动化', '监控分析'],
quote: '数据库主节点 CPU 100%,正在执行主从切换,预计 2 分钟完成。'
},
dev: {
id: 'dev',
icon: '💻',
name: '开发负责人',
eng: 'Development Lead',
color: '#22c55e',
responsibilities: [
'分析代码层面的问题根因',
'准备和执行代码级别的修复或回滚',
'评估变更风险,提供技术方案',
'协调开发团队成员参与排查'
],
skills: ['代码分析', '快速调试', '风险评估', '版本管理'],
quote: '定位到问题:昨天上线的批量查询没有加分页,导致全表扫描拖垮数据库。准备回滚到上一版本。'
}
}
const middleRoles = [
allRoles.comm,
allRoles.ops,
allRoles.dev
]
const currentRole = computed(() => {
return allRoles[activeRole.value] || null
})
const selectRole = (id) => {
activeRole.value = id
}
const scenarioEvents = [
{ time: '14:02', role: '监控', color: '#3b82f6', text: '支付成功率从 99.9% 骤降至 12%,触发 P0 告警' },
{ time: '14:03', role: '指挥官', color: '#8b5cf6', text: '确认 P0 事故,开启事故频道,召集各角色' },
{ time: '14:05', role: '通讯', color: '#3b82f6', text: '通知管理层,更新状态页为"服务降级"' },
{ time: '14:08', role: '运维', color: '#ef4444', text: '发现数据库主节点 CPU 100%,连接池耗尽' },
{ time: '14:10', role: '开发', color: '#22c55e', text: '定位到昨日上线的慢查询是根因' },
{ time: '14:12', role: '指挥官', color: '#8b5cf6', text: '决策:立即回滚昨日变更 + 数据库主从切换' },
{ time: '14:15', role: '运维', color: '#ef4444', text: '数据库主从切换完成,连接恢复' },
{ time: '14:18', role: '开发', color: '#22c55e', text: '代码回滚部署完成' },
{ time: '14:20', role: '通讯', color: '#3b82f6', text: '支付成功率恢复至 99.8%,通知各方服务恢复' }
]
</script>
<style scoped>
.incident-command-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1.5rem; }
.title { font-weight: 700; font-size: 1.1rem; margin-bottom: 0.25rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.org-chart {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 1.5rem;
}
.org-level { display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; }
.org-connector {
display: flex;
justify-content: center;
padding: 0.5rem 0;
}
.connector-line {
width: 2px;
height: 24px;
background: var(--vp-c-divider);
}
.role-card {
padding: 0.75rem 1rem;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
cursor: pointer;
text-align: center;
transition: all 0.2s;
min-width: 130px;
}
.role-card:hover { border-color: var(--vp-c-brand); transform: translateY(-2px); }
.role-card.active { border-color: var(--vp-c-brand); box-shadow: 0 2px 12px rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.15); }
.role-card.commander { border-width: 3px; }
.role-icon { font-size: 1.5rem; margin-bottom: 0.25rem; }
.role-name { font-weight: 600; font-size: 0.9rem; }
.role-eng { font-size: 0.75rem; color: var(--vp-c-text-3); }
.role-detail {
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #fff;
}
.detail-icon { font-size: 1.3rem; }
.detail-name { font-weight: 700; font-size: 1rem; }
.detail-body { padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
.detail-section { display: flex; flex-direction: column; gap: 0.3rem; }
.section-label { font-weight: 600; font-size: 0.85rem; color: var(--vp-c-text-2); }
.responsibilities { display: flex; flex-direction: column; gap: 0.3rem; }
.resp-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.85rem;
}
.resp-num {
width: 20px; height: 20px; border-radius: 50%;
background: var(--vp-c-bg-soft);
display: flex; align-items: center; justify-content: center;
font-size: 0.7rem; font-weight: 700; flex-shrink: 0;
}
.skills { display: flex; gap: 0.4rem; flex-wrap: wrap; }
.skill-tag {
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
}
.quote-box {
font-size: 0.85rem;
padding: 0.6rem 0.8rem;
background: var(--vp-c-bg-soft);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
font-style: italic;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.scenario-box {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.scenario-title { font-weight: 700; font-size: 0.95rem; margin-bottom: 0.75rem; }
.scenario-timeline { display: flex; flex-direction: column; gap: 0.4rem; }
.event-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
padding: 0.3rem 0;
border-bottom: 1px solid var(--vp-c-divider);
}
.event-item:last-child { border-bottom: none; }
.event-time {
font-family: monospace;
font-size: 0.8rem;
color: var(--vp-c-text-3);
min-width: 40px;
}
.event-role {
padding: 0.1rem 0.4rem;
border-radius: 4px;
color: #fff;
font-size: 0.75rem;
font-weight: 600;
min-width: 45px;
text-align: center;
}
.event-text { color: var(--vp-c-text-1); }
@media (max-width: 768px) {
.org-level { flex-direction: column; align-items: center; }
.event-item { flex-wrap: wrap; }
}
</style>
@@ -0,0 +1,471 @@
<!--
IncidentTimelineDemo.vue
事故响应时间线演示展示从发现到复盘的完整事故响应流程
-->
<template>
<div class="incident-timeline-demo">
<div class="header">
<div class="title">事故响应时间线 (Incident Timeline)</div>
<div class="subtitle">点击各阶段了解每个环节的关键动作</div>
</div>
<div class="timeline">
<div class="timeline-track">
<div
class="timeline-progress"
:style="{ width: progressWidth }"
></div>
</div>
<div class="timeline-nodes">
<div
v-for="(phase, index) in phases"
:key="phase.id"
:class="[
'timeline-node',
{
active: activePhase === phase.id,
completed: completedPhases.includes(phase.id)
}
]"
@click="selectPhase(phase.id)"
>
<div class="node-dot">
<span v-if="completedPhases.includes(phase.id)">&#10003;</span>
<span v-else>{{ index + 1 }}</span>
</div>
<div class="node-label">{{ phase.name }}</div>
<div class="node-time">{{ phase.timeHint }}</div>
</div>
</div>
</div>
<div v-if="currentPhase" class="phase-detail">
<div class="phase-header" :style="{ background: currentPhase.color }">
<span class="phase-icon">{{ currentPhase.icon }}</span>
<span class="phase-name">{{ currentPhase.name }}</span>
<span class="phase-duration">{{ currentPhase.duration }}</span>
</div>
<div class="phase-body">
<div class="phase-desc">{{ currentPhase.description }}</div>
<div class="phase-actions">
<div class="actions-title">关键动作</div>
<div
v-for="(action, i) in currentPhase.actions"
:key="i"
class="action-item"
>
<span class="action-bullet">{{ i + 1 }}</span>
<span>{{ action }}</span>
</div>
</div>
<div class="phase-roles">
<span class="roles-label">参与角色</span>
<span
v-for="role in currentPhase.roles"
:key="role"
class="role-tag"
>
{{ role }}
</span>
</div>
</div>
</div>
<div class="auto-controls">
<button class="play-btn" @click="autoPlay" :disabled="isPlaying">
{{ isPlaying ? '播放中...' : '自动演示完整流程' }}
</button>
<button class="reset-btn" @click="resetAll">重置</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activePhase = ref(null)
const completedPhases = ref([])
const isPlaying = ref(false)
const phases = [
{
id: 'detect',
name: '发现',
timeHint: 'T+0',
icon: '🔍',
color: '#ef4444',
duration: '目标 < 5 分钟',
description:
'通过监控告警、用户反馈或自动化检测发现系统异常。越早发现,损失越小。',
actions: [
'监控系统触发告警(CPU、延迟、错误率等)',
'值班人员收到通知并确认',
'初步判断影响范围',
'在事故频道发出第一条通报'
],
roles: ['值班工程师', '监控系统']
},
{
id: 'triage',
name: '分级',
timeHint: 'T+5min',
icon: '📋',
color: '#f59e0b',
duration: '目标 < 10 分钟',
description:
'快速评估事故严重程度,确定优先级(P0-P4),决定响应规模和升级路径。',
actions: [
'评估用户影响面(多少用户受影响?)',
'确定业务影响(核心功能是否不可用?)',
'分配事故等级(P0/P1/P2/P3/P4',
'根据等级启动对应的响应流程'
],
roles: ['值班工程师', '事故指挥官']
},
{
id: 'mitigate',
name: '止血',
timeHint: 'T+15min',
icon: '🚑',
color: '#3b82f6',
duration: '目标 < 1 小时',
description:
'采取紧急措施恢复服务,优先止血而非根治。回滚、降级、限流都是常见手段。',
actions: [
'回滚最近的变更(代码、配置、基础设施)',
'启用降级方案或备用系统',
'实施限流保护核心链路',
'持续监控恢复进度并通报状态'
],
roles: ['事故指挥官', '运维工程师', '开发工程师']
},
{
id: 'resolve',
name: '解决',
timeHint: 'T+1h',
icon: '🔧',
color: '#22c55e',
duration: '视复杂度而定',
description:
'在服务恢复后,定位根本原因并实施永久修复,确保同类问题不再发生。',
actions: [
'深入分析日志、监控数据定位根因',
'编写并审核修复代码',
'在预发布环境验证修复效果',
'灰度发布修复,确认问题彻底解决'
],
roles: ['开发工程师', '架构师', 'QA 工程师']
},
{
id: 'postmortem',
name: '复盘',
timeHint: 'T+48h',
icon: '📝',
color: '#8b5cf6',
duration: '事故后 48 小时内',
description:
'召开无责复盘会议,分析根因,提炼经验教训,制定改进措施防止再次发生。',
actions: [
'撰写事故复盘报告(时间线、影响、根因)',
'召开复盘会议,全员参与讨论',
'使用"五个为什么"深挖根本原因',
'制定并跟踪改进行动项(Action Items'
],
roles: ['事故指挥官', '全体相关人员', '管理层']
}
]
const currentPhase = computed(() => {
if (!activePhase.value) return null
return phases.find((p) => p.id === activePhase.value)
})
const progressWidth = computed(() => {
if (completedPhases.value.length === 0 && !activePhase.value) return '0%'
const activeIndex = phases.findIndex((p) => p.id === activePhase.value)
if (activeIndex === -1) {
const lastCompleted = completedPhases.value.length
return `${(lastCompleted / phases.length) * 100}%`
}
return `${((activeIndex + 0.5) / phases.length) * 100}%`
})
const selectPhase = (id) => {
activePhase.value = id
}
const autoPlay = async () => {
isPlaying.value = true
completedPhases.value = []
activePhase.value = null
for (let i = 0; i < phases.length; i++) {
activePhase.value = phases[i].id
await new Promise((r) => setTimeout(r, 1800))
completedPhases.value.push(phases[i].id)
}
isPlaying.value = false
}
const resetAll = () => {
activePhase.value = null
completedPhases.value = []
isPlaying.value = false
}
</script>
<style scoped>
.incident-timeline-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.timeline {
position: relative;
margin-bottom: 1.5rem;
}
.timeline-track {
position: absolute;
top: 16px;
left: 5%;
right: 5%;
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
}
.timeline-progress {
height: 100%;
background: var(--vp-c-brand);
border-radius: 2px;
transition: width 0.5s ease;
}
.timeline-nodes {
display: flex;
justify-content: space-between;
position: relative;
}
.timeline-node {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
flex: 1;
transition: all 0.2s;
}
.node-dot {
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--vp-c-bg);
border: 3px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.8rem;
transition: all 0.3s;
z-index: 1;
}
.timeline-node.active .node-dot {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
transform: scale(1.2);
box-shadow: 0 0 0 4px rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.2);
}
.timeline-node.completed .node-dot {
border-color: #22c55e;
background: #22c55e;
color: #fff;
}
.node-label {
margin-top: 0.5rem;
font-size: 0.85rem;
font-weight: 600;
text-align: center;
}
.node-time {
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-top: 0.15rem;
}
.phase-detail {
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 1rem;
border: 1px solid var(--vp-c-divider);
}
.phase-header {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #fff;
}
.phase-icon {
font-size: 1.3rem;
}
.phase-name {
font-weight: 700;
font-size: 1rem;
flex: 1;
}
.phase-duration {
background: rgba(255, 255, 255, 0.2);
padding: 0.2rem 0.6rem;
border-radius: 999px;
font-size: 0.8rem;
}
.phase-body {
padding: 1rem;
}
.phase-desc {
font-size: 0.9rem;
color: var(--vp-c-text-1);
margin-bottom: 1rem;
line-height: 1.6;
}
.actions-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.action-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.85rem;
margin-bottom: 0.4rem;
color: var(--vp-c-text-2);
}
.action-bullet {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--vp-c-bg-soft);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 700;
flex-shrink: 0;
color: var(--vp-c-text-1);
}
.phase-roles {
margin-top: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.roles-label {
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.role-tag {
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.auto-controls {
display: flex;
gap: 0.5rem;
}
.play-btn,
.reset-btn {
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
border: 1px solid var(--vp-c-divider);
transition: all 0.2s;
}
.play-btn {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.play-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.reset-btn {
background: var(--vp-c-bg);
}
.reset-btn:hover {
border-color: var(--vp-c-brand);
}
@media (max-width: 768px) {
.timeline-nodes {
flex-direction: column;
gap: 0.75rem;
}
.timeline-track {
display: none;
}
.timeline-node {
flex-direction: row;
gap: 0.75rem;
}
.node-label {
margin-top: 0;
}
.node-time {
margin-top: 0;
margin-left: auto;
}
}
</style>
@@ -0,0 +1,412 @@
<!--
PostmortemDemo.vue
事后复盘演示交互式展示"五个为什么"分析法和复盘报告模板
-->
<template>
<div class="postmortem-demo">
<div class="header">
<div class="title">事后复盘五个为什么 (5 Whys Analysis)</div>
<div class="subtitle">点击"继续追问"层层深入挖掘根本原因</div>
</div>
<div class="case-select">
<button
v-for="c in cases"
:key="c.id"
:class="['case-btn', { active: activeCase === c.id }]"
@click="selectCase(c.id)"
>
{{ c.name }}
</button>
</div>
<div v-if="currentCase" class="whys-chain">
<div
v-for="(why, index) in visibleWhys"
:key="index"
class="why-item"
>
<div class="why-header">
<span class="why-badge">
{{ index === 0 ? '现象' : '第 ' + index + ' 个为什么' }}
</span>
<span class="why-depth">
深度 {{ index }} / {{ currentCase.whys.length - 1 }}
</span>
</div>
<div class="why-question" v-if="index > 0">
为什么{{ currentCase.whys[index - 1].answer }}
</div>
<div class="why-answer">
<span class="answer-icon">{{ index === currentCase.whys.length - 1 && revealedCount >= currentCase.whys.length ? '🎯' : '💡' }}</span>
<span>{{ why.answer }}</span>
</div>
<div
v-if="index < visibleWhys.length - 1"
class="why-arrow"
>
继续追问
</div>
</div>
<div class="why-controls" v-if="revealedCount < currentCase.whys.length">
<button class="ask-btn" @click="revealNext">
继续追问为什么
</button>
</div>
<div v-else class="root-cause-box">
<div class="root-label">根本原因已找到</div>
<div class="root-content">{{ currentCase.rootCause }}</div>
<div class="root-actions">
<div class="actions-label">改进措施</div>
<div
v-for="(action, i) in currentCase.actions"
:key="i"
class="action-item"
>
<span class="action-check">&#10003;</span>
<span>{{ action }}</span>
</div>
</div>
</div>
</div>
<div class="template-box">
<div class="template-title">复盘报告模板</div>
<div class="template-sections">
<div
v-for="(section, i) in templateSections"
:key="i"
class="template-item"
:class="{ expanded: expandedSection === i }"
@click="expandedSection = expandedSection === i ? -1 : i"
>
<div class="template-item-header">
<span class="template-num">{{ i + 1 }}</span>
<span class="template-name">{{ section.name }}</span>
<span class="template-toggle">
{{ expandedSection === i ? '' : '+' }}
</span>
</div>
<div v-if="expandedSection === i" class="template-item-body">
{{ section.desc }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeCase = ref('payment')
const revealedCount = ref(1)
const expandedSection = ref(-1)
const casesData = {
payment: {
id: 'payment',
name: '支付系统宕机',
whys: [
{ answer: '支付系统在高峰期完全不可用,持续 18 分钟' },
{ answer: '数据库连接池被耗尽,所有新请求排队超时' },
{ answer: '一条慢查询占用连接长达 30 秒不释放' },
{ answer: '新上线的对账功能执行了全表扫描,没有使用索引' },
{ answer: '代码审查时没有检查 SQL 执行计划,也没有慢查询测试环节' }
],
rootCause: '研发流程缺陷:代码审查清单中缺少 SQL 性能审查项,CI/CD 流水线中没有慢查询检测环节。',
actions: [
'代码审查清单增加"SQL 执行计划检查"必选项',
'CI 流水线增加慢查询自动检测(阈值 100ms)',
'数据库连接池增加单查询超时限制(5s 强制断开)',
'建立大表变更审批流程'
]
},
deploy: {
id: 'deploy',
name: '部署导致服务中断',
whys: [
{ answer: '新版本部署后,用户登录功能完全失效,持续 25 分钟' },
{ answer: '新版本的认证服务无法连接 Redis 缓存集群' },
{ answer: '部署脚本使用了错误的 Redis 集群地址(指向了测试环境)' },
{ answer: '环境配置是硬编码在部署脚本中的,没有使用配置中心' },
{ answer: '团队没有统一的配置管理规范,每个服务自行管理配置' }
],
rootCause: '基础设施缺陷:缺乏统一的配置管理平台和规范,环境配置散落在各处,容易出错且难以审计。',
actions: [
'引入配置中心(如 Consul/Nacos),统一管理所有环境配置',
'部署流水线增加配置校验步骤(连通性检查)',
'禁止在代码和脚本中硬编码环境地址',
'建立部署前 Checklist,包含配置确认环节'
]
}
}
const cases = [
{ id: 'payment', name: '支付系统宕机' },
{ id: 'deploy', name: '部署导致服务中断' }
]
const currentCase = computed(() => casesData[activeCase.value] || null)
const visibleWhys = computed(() => {
if (!currentCase.value) return []
return currentCase.value.whys.slice(0, revealedCount.value)
})
const selectCase = (id) => {
activeCase.value = id
revealedCount.value = 1
}
const revealNext = () => {
if (currentCase.value && revealedCount.value < currentCase.value.whys.length) {
revealedCount.value++
}
}
const templateSections = [
{ name: '事故概述', desc: '简要描述事故发生的时间、持续时长、影响范围和严重程度。例如:"2024年3月15日 14:02-14:20,支付服务完全不可用,影响约 12 万笔交易。"' },
{ name: '时间线', desc: '按时间顺序记录从发现到解决的每一个关键事件,精确到分钟。包括:告警触发、人员响应、排查过程、修复操作、服务恢复等。' },
{ name: '影响评估', desc: '量化事故影响:受影响用户数、失败请求数、经济损失估算、SLA 影响等。用数据说话,避免模糊描述。' },
{ name: '根因分析', desc: '使用"五个为什么"等方法深入分析根本原因。区分直接原因(触发因素)和根本原因(系统性缺陷)。' },
{ name: '改进措施', desc: '列出具体的改进行动项,每项必须有负责人和截止日期。分为短期(本周)、中期(本月)、长期(本季度)三个层次。' },
{ name: '经验教训', desc: '总结哪些做得好(值得保持)、哪些做得不好(需要改进)、哪些是意外发现(新的风险点)。' }
]
</script>
<style scoped>
.postmortem-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header { margin-bottom: 1.5rem; }
.title { font-weight: 700; font-size: 1.1rem; margin-bottom: 0.25rem; }
.subtitle { color: var(--vp-c-text-2); font-size: 0.9rem; }
.case-select {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.case-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.case-btn:hover {
border-color: var(--vp-c-brand);
color: var(--vp-c-brand);
}
.case-btn.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.whys-chain {
margin-bottom: 1.5rem;
}
.why-item {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 0.75rem;
margin-bottom: 0.25rem;
}
.why-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.4rem;
}
.why-badge {
font-weight: 700;
font-size: 0.8rem;
padding: 0.15rem 0.5rem;
background: var(--vp-c-brand);
color: #fff;
border-radius: 4px;
}
.why-depth {
font-size: 0.75rem;
color: var(--vp-c-text-3);
}
.why-question {
font-size: 0.85rem;
color: var(--vp-c-text-2);
font-style: italic;
margin-bottom: 0.3rem;
padding-left: 0.5rem;
border-left: 2px solid var(--vp-c-divider);
}
.why-answer {
display: flex;
align-items: flex-start;
gap: 0.4rem;
font-size: 0.9rem;
line-height: 1.5;
}
.answer-icon { flex-shrink: 0; }
.why-arrow {
text-align: center;
color: var(--vp-c-text-3);
font-size: 0.8rem;
padding: 0.25rem 0;
}
.why-controls {
text-align: center;
margin-top: 0.75rem;
}
.ask-btn {
padding: 0.6rem 1.5rem;
background: var(--vp-c-brand);
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
font-weight: 600;
transition: all 0.2s;
}
.ask-btn:hover { opacity: 0.9; transform: translateY(-1px); }
.root-cause-box {
background: rgba(34, 197, 94, 0.08);
border: 2px solid #22c55e;
border-radius: 10px;
padding: 1rem;
margin-top: 0.75rem;
}
.root-label {
font-weight: 700;
font-size: 0.95rem;
color: #22c55e;
margin-bottom: 0.5rem;
}
.root-content {
font-size: 0.9rem;
line-height: 1.6;
margin-bottom: 0.75rem;
}
.actions-label {
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 0.4rem;
}
.action-item {
display: flex;
align-items: flex-start;
gap: 0.4rem;
font-size: 0.85rem;
margin-bottom: 0.3rem;
}
.action-check {
color: #22c55e;
font-weight: 700;
flex-shrink: 0;
}
.template-box {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.template-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.template-sections {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.template-item {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
overflow: hidden;
}
.template-item:hover {
border-color: var(--vp-c-brand);
}
.template-item.expanded {
border-color: var(--vp-c-brand);
}
.template-item-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
}
.template-num {
width: 22px; height: 22px; border-radius: 50%;
background: var(--vp-c-bg-soft);
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; font-weight: 700; flex-shrink: 0;
}
.template-name {
flex: 1;
font-weight: 600;
font-size: 0.9rem;
}
.template-toggle {
font-size: 1.1rem;
color: var(--vp-c-text-3);
font-weight: 700;
}
.template-item-body {
padding: 0 0.75rem 0.6rem;
font-size: 0.85rem;
color: var(--vp-c-text-2);
line-height: 1.6;
}
@media (max-width: 768px) {
.case-select { flex-direction: column; }
.case-btn { width: 100%; }
}
</style>
@@ -0,0 +1,483 @@
<!--
SeverityLevelDemo.vue
事故严重程度分级演示交互式展示 P0-P4 各级别的定义示例和响应要求
-->
<template>
<div class="severity-level-demo">
<div class="header">
<div class="title">事故严重程度分级 (Severity Levels)</div>
<div class="subtitle">点击各级别了解对应的响应要求和真实案例</div>
</div>
<div class="level-tabs">
<button
v-for="level in levels"
:key="level.id"
:class="['level-tab', level.id, { active: activeLevel === level.id }]"
@click="activeLevel = level.id"
>
<span class="tab-badge">{{ level.id.toUpperCase() }}</span>
<span class="tab-name">{{ level.shortName }}</span>
</button>
</div>
<div v-if="current" class="level-detail">
<div class="detail-header" :style="{ background: current.color }">
<div class="detail-level">{{ current.id.toUpperCase() }}</div>
<div class="detail-name">{{ current.name }}</div>
</div>
<div class="detail-body">
<div class="detail-section">
<div class="section-label">定义</div>
<div class="section-content">{{ current.definition }}</div>
</div>
<div class="detail-section">
<div class="section-label">响应时间</div>
<div class="section-content response-time">
{{ current.responseTime }}
</div>
</div>
<div class="detail-section">
<div class="section-label">通知方式</div>
<div class="channels">
<span
v-for="ch in current.channels"
:key="ch"
class="channel-tag"
>
{{ ch }}
</span>
</div>
</div>
<div class="detail-section">
<div class="section-label">真实案例</div>
<div class="examples">
<div
v-for="(ex, i) in current.examples"
:key="i"
class="example-item"
>
{{ ex }}
</div>
</div>
</div>
<div class="detail-section">
<div class="section-label">响应要求</div>
<div class="requirements">
<div
v-for="(req, i) in current.requirements"
:key="i"
class="req-item"
>
<span class="req-check">&#10003;</span>
<span>{{ req }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="comparison-table">
<div class="table-title">各级别对比一览</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>级别</th>
<th>用户影响</th>
<th>响应时间</th>
<th>值班要求</th>
</tr>
</thead>
<tbody>
<tr
v-for="level in levels"
:key="level.id"
:class="{ highlight: activeLevel === level.id }"
@click="activeLevel = level.id"
>
<td>
<span class="table-badge" :class="level.id">
{{ level.id.toUpperCase() }}
</span>
</td>
<td>{{ level.userImpact }}</td>
<td>{{ level.responseTime }}</td>
<td>{{ level.oncallReq }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeLevel = ref('p0')
const levels = [
{
id: 'p0',
shortName: '致命',
name: '致命事故 (Critical)',
color: '#ef4444',
definition: '核心业务完全不可用,大面积用户受影响,造成严重经济损失或数据丢失风险。',
responseTime: '立即响应,5 分钟内到位',
userImpact: '全部用户',
oncallReq: '全员到位',
channels: ['电话', '短信', '即时通讯', '邮件'],
examples: [
'主数据库宕机,所有读写请求失败',
'支付系统完全不可用,用户无法下单',
'用户数据大规模泄露'
],
requirements: [
'事故指挥官必须在 5 分钟内就位',
'每 15 分钟向管理层通报进展',
'所有相关团队取消休假立即支援',
'事后 24 小时内完成复盘报告'
]
},
{
id: 'p1',
shortName: '严重',
name: '严重事故 (Major)',
color: '#f59e0b',
definition: '核心功能部分受损,大量用户体验降级,但系统未完全不可用。',
responseTime: '15 分钟内响应',
userImpact: '大量用户',
oncallReq: '核心团队',
channels: ['即时通讯', '短信', '邮件'],
examples: [
'搜索功能返回结果严重延迟(>5s)',
'部分地区用户无法登录',
'订单处理队列严重积压'
],
requirements: [
'值班工程师 15 分钟内开始排查',
'每 30 分钟通报一次进展',
'必要时升级为 P0',
'事后 48 小时内完成复盘'
]
},
{
id: 'p2',
shortName: '中等',
name: '中等事故 (Moderate)',
color: '#eab308',
definition: '非核心功能异常,部分用户受影响,不影响主要业务流程。',
responseTime: '1 小时内响应',
userImpact: '部分用户',
oncallReq: '值班工程师',
channels: ['即时通讯', '邮件'],
examples: [
'用户头像加载失败',
'报表导出功能超时',
'非关键页面 CSS 样式错乱'
],
requirements: [
'值班工程师在工作时间内处理',
'当天给出修复方案',
'不需要全员响应',
'在周报中记录'
]
},
{
id: 'p3',
shortName: '轻微',
name: '轻微问题 (Minor)',
color: '#84cc16',
definition: '边缘功能小问题,极少数用户受影响,不影响正常使用。',
responseTime: '当天确认,本周处理',
userImpact: '极少用户',
oncallReq: '正常排期',
channels: ['邮件', '工单系统'],
examples: [
'某个按钮在特定浏览器下对齐偏移',
'日志中出现非关键性警告',
'文案有错别字'
],
requirements: [
'记录到缺陷跟踪系统',
'纳入正常迭代排期',
'不需要紧急响应',
'修复后正常发布'
]
},
{
id: 'p4',
shortName: '建议',
name: '改进建议 (Suggestion)',
color: '#64748b',
definition: '非故障类问题,属于优化建议或技术债务,不影响任何用户。',
responseTime: '按优先级排期',
userImpact: '无直接影响',
oncallReq: '无需值班',
channels: ['工单系统'],
examples: [
'代码中存在可优化的性能瓶颈',
'依赖库版本过旧需要升级',
'监控覆盖率不足需要补充'
],
requirements: [
'记录到技术债务清单',
'季度规划时评估优先级',
'作为团队改进项跟踪',
'无时间压力'
]
}
]
const current = computed(() => {
return levels.find((l) => l.id === activeLevel.value)
})
</script>
<style scoped>
.severity-level-demo {
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
border-radius: 12px;
padding: 1.5rem;
margin: 1.5rem 0;
}
.header {
margin-bottom: 1.5rem;
}
.title {
font-weight: 700;
font-size: 1.1rem;
margin-bottom: 0.25rem;
}
.subtitle {
color: var(--vp-c-text-2);
font-size: 0.9rem;
}
.level-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.level-tab {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 0.9rem;
border: 2px solid var(--vp-c-divider);
background: var(--vp-c-bg);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9rem;
}
.level-tab:hover {
border-color: var(--vp-c-text-3);
}
.level-tab.active.p0 { border-color: #ef4444; background: rgba(239,68,68,0.08); }
.level-tab.active.p1 { border-color: #f59e0b; background: rgba(245,158,11,0.08); }
.level-tab.active.p2 { border-color: #eab308; background: rgba(234,179,8,0.08); }
.level-tab.active.p3 { border-color: #84cc16; background: rgba(132,204,22,0.08); }
.level-tab.active.p4 { border-color: #64748b; background: rgba(100,116,139,0.08); }
.tab-badge {
font-weight: 700;
font-size: 0.8rem;
padding: 0.1rem 0.4rem;
border-radius: 4px;
color: #fff;
}
.p0 .tab-badge { background: #ef4444; }
.p1 .tab-badge { background: #f59e0b; }
.p2 .tab-badge { background: #eab308; }
.p3 .tab-badge { background: #84cc16; }
.p4 .tab-badge { background: #64748b; }
.tab-name {
font-weight: 500;
}
.level-detail {
background: var(--vp-c-bg);
border-radius: 10px;
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--vp-c-divider);
}
.detail-header {
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
color: #fff;
}
.detail-level {
font-weight: 800;
font-size: 1.2rem;
}
.detail-name {
font-weight: 600;
font-size: 1rem;
}
.detail-body {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-section {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.section-label {
font-weight: 600;
font-size: 0.85rem;
color: var(--vp-c-text-2);
}
.section-content {
font-size: 0.9rem;
line-height: 1.6;
}
.response-time {
font-weight: 700;
color: var(--vp-c-brand);
}
.channels {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.channel-tag {
padding: 0.15rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
font-size: 0.8rem;
}
.examples {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.example-item {
font-size: 0.85rem;
padding: 0.3rem 0.5rem;
background: var(--vp-c-bg-soft);
border-radius: 4px;
border-left: 3px solid var(--vp-c-divider);
}
.requirements {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.req-item {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.85rem;
}
.req-check {
color: #22c55e;
font-weight: 700;
}
.comparison-table {
background: var(--vp-c-bg);
border-radius: 10px;
padding: 1rem;
border: 1px solid var(--vp-c-divider);
}
.table-title {
font-weight: 700;
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.table-wrapper {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
th {
text-align: left;
padding: 0.5rem;
border-bottom: 2px solid var(--vp-c-divider);
font-weight: 600;
color: var(--vp-c-text-2);
}
td {
padding: 0.5rem;
border-bottom: 1px solid var(--vp-c-divider);
}
tr.highlight {
background: rgba(var(--vp-c-brand-rgb, 100, 108, 255), 0.06);
}
tr {
cursor: pointer;
transition: background 0.2s;
}
tr:hover {
background: var(--vp-c-bg-soft);
}
.table-badge {
padding: 0.15rem 0.4rem;
border-radius: 4px;
font-weight: 700;
font-size: 0.75rem;
color: #fff;
}
.table-badge.p0 { background: #ef4444; }
.table-badge.p1 { background: #f59e0b; }
.table-badge.p2 { background: #eab308; }
.table-badge.p3 { background: #84cc16; }
.table-badge.p4 { background: #64748b; }
@media (max-width: 768px) {
.level-tabs {
flex-direction: column;
}
.level-tab {
width: 100%;
justify-content: center;
}
}
</style>
@@ -0,0 +1,360 @@
<template>
<div class="config-drift-demo">
<div class="demo-label">交互演示 配置漂移无声的定时炸弹</div>
<div class="timeline">
<div class="timeline-track">
<div
v-for="(event, i) in events"
:key="i"
:class="['timeline-node', event.type, { active: step >= i }]"
@click="goToStep(i)"
>
<div class="node-dot"></div>
<div class="node-label">{{ event.label }}</div>
</div>
</div>
</div>
<div class="scene-area">
<div class="infra-visual">
<div class="server-group">
<div class="group-title">期望状态代码定义</div>
<div class="server-cards">
<div v-for="s in expectedServers" :key="s.name" class="server-card expected">
<div class="server-icon">🖥</div>
<div class="server-name">{{ s.name }}</div>
<div class="server-config">{{ s.config }}</div>
</div>
</div>
</div>
<div class="drift-indicator">
<div :class="['drift-status', driftLevel]">
<span class="drift-icon">{{ driftIcon }}</span>
<span class="drift-text">{{ driftText }}</span>
</div>
</div>
<div class="server-group">
<div class="group-title">实际状态线上环境</div>
<div class="server-cards">
<div
v-for="s in actualServers"
:key="s.name"
:class="['server-card', 'actual', { drifted: s.drifted }]"
>
<div class="server-icon">{{ s.drifted ? '⚠️' : '🖥️' }}</div>
<div class="server-name">{{ s.name }}</div>
<div class="server-config">{{ s.config }}</div>
<div v-if="s.driftReason" class="drift-reason">{{ s.driftReason }}</div>
</div>
</div>
</div>
</div>
<div class="event-desc">
<div class="event-title">{{ events[step].title }}</div>
<p class="event-detail">{{ events[step].detail }}</p>
</div>
</div>
<div class="controls">
<button class="ctrl-btn" :disabled="step === 0" @click="goToStep(step - 1)"> 上一步</button>
<button class="ctrl-btn reset" @click="goToStep(0)">重置</button>
<button class="ctrl-btn primary" :disabled="step >= events.length - 1" @click="goToStep(step + 1)">
下一步
</button>
</div>
<div class="lesson-box">
<div class="lesson-title">关键教训</div>
<div class="lesson-items">
<div v-for="(lesson, i) in lessons" :key="i" class="lesson-item">
<span class="lesson-icon">{{ lesson.icon }}</span>
<span class="lesson-text">{{ lesson.text }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const step = ref(0)
const events = [
{
label: '初始部署',
type: 'good',
title: '第 0 步:通过 IaC 初始部署',
detail: '团队使用 Terraform 部署了 3 台 Web 服务器,配置完全一致:Nginx 1.24、端口 443、2GB 内存。代码和实际状态完美匹配。'
},
{
label: '手动修改',
type: 'warn',
title: '第 1 步:深夜紧急手动修改',
detail: '凌晨 3 点,Server-B 出现性能问题。值班工程师直接 SSH 登录,手动将内存从 2GB 升级到 4GB,并修改了 Nginx 配置。没有更新 IaC 代码。'
},
{
label: '又一次修改',
type: 'warn',
title: '第 2 步:另一位同事的"临时"调整',
detail: '一周后,另一位工程师为了调试,在 Server-C 上开放了 22 端口(SSH),并安装了调试工具。同样没有更新代码。'
},
{
label: '漂移加剧',
type: 'bad',
title: '第 3 步:配置漂移已经失控',
detail: '此时 3 台"相同"的服务器实际配置已经各不相同。代码描述的状态和线上真实状态严重脱节,没有人能说清楚线上到底是什么配置。'
},
{
label: 'IaC 检测',
type: 'fix',
title: '第 4 步:terraform plan 发现漂移',
detail: '运行 terraform plan 后,Terraform 对比 State 文件和实际资源,清晰列出所有差异。团队决定将手动变更回退,统一通过代码管理。'
}
]
const expectedServers = [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB' },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB' },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB' }
]
const actualServers = computed(() => {
if (step.value === 0) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
}
if (step.value === 1) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.25 | 443 | 4GB', drifted: true, driftReason: '手动升级内存和 Nginx' },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
}
if (step.value === 2 || step.value === 3) {
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.25 | 443 | 4GB', drifted: true, driftReason: '手动升级内存和 Nginx' },
{ name: 'Server-C', config: 'Nginx 1.24 | 22+443 | 2GB', drifted: true, driftReason: '开放了 SSH 端口' }
]
}
// step 4: fix
return [
{ name: 'Server-A', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-B', config: 'Nginx 1.24 | 443 | 2GB', drifted: false },
{ name: 'Server-C', config: 'Nginx 1.24 | 443 | 2GB', drifted: false }
]
})
const driftLevel = computed(() => {
if (step.value === 0 || step.value === 4) return 'ok'
if (step.value <= 2) return 'warning'
return 'danger'
})
const driftIcon = computed(() => {
if (driftLevel.value === 'ok') return '✅'
if (driftLevel.value === 'warning') return '⚠️'
return '🔥'
})
const driftText = computed(() => {
if (step.value === 0) return '状态一致'
if (step.value === 4) return '漂移已修复'
if (step.value === 1) return '1 台漂移'
if (step.value === 2) return '2 台漂移'
return '严重漂移!'
})
const lessons = [
{ icon: '🚫', text: '禁止手动修改线上环境,所有变更必须通过代码' },
{ icon: '🔍', text: '定期运行 terraform plan 检测漂移' },
{ icon: '🔒', text: '限制生产环境的 SSH 权限,减少人为干预' },
{ icon: '📋', text: '建立变更审批流程(PR → Review → Merge → Apply' }
]
function goToStep(i) {
step.value = Math.max(0, Math.min(i, events.length - 1))
}
</script>
<style scoped>
.config-drift-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
text-align: center;
}
.timeline { margin-bottom: 1rem; overflow-x: auto; }
.timeline-track {
display: flex;
align-items: flex-start;
gap: 0;
min-width: max-content;
position: relative;
padding: 0 0.5rem;
}
.timeline-node {
flex: 1;
min-width: 90px;
text-align: center;
cursor: pointer;
position: relative;
padding-top: 20px;
}
.timeline-node::before {
content: '';
position: absolute;
top: 8px;
left: 0;
right: 0;
height: 2px;
background: var(--vp-c-divider);
}
.timeline-node:first-child::before { left: 50%; }
.timeline-node:last-child::before { right: 50%; }
.node-dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
margin: 0 auto 4px;
position: relative;
z-index: 1;
transition: all 0.3s;
}
.timeline-node.active .node-dot { transform: scale(1.3); }
.timeline-node.active.good .node-dot { background: #10b981; border-color: #10b981; }
.timeline-node.active.warn .node-dot { background: #f59e0b; border-color: #f59e0b; }
.timeline-node.active.bad .node-dot { background: #ef4444; border-color: #ef4444; }
.timeline-node.active.fix .node-dot { background: #3b82f6; border-color: #3b82f6; }
.node-label { font-size: 0.68rem; color: var(--vp-c-text-3); }
.timeline-node.active .node-label { font-weight: 600; color: var(--vp-c-text-1); }
.scene-area { margin-bottom: 1rem; }
.infra-visual {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 0.8rem;
}
.group-title {
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
margin-bottom: 0.3rem;
text-align: center;
}
.server-cards {
display: flex;
gap: 0.4rem;
justify-content: center;
flex-wrap: wrap;
}
.server-card {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.5rem 0.6rem;
background: var(--vp-c-bg);
text-align: center;
min-width: 120px;
transition: all 0.3s;
font-size: 0.73rem;
}
.server-card.expected { border-color: #10b981; }
.server-card.drifted {
border-color: #ef4444;
background: #fef2f210;
box-shadow: 0 0 0 1px #fca5a540;
}
.server-icon { font-size: 1.2rem; }
.server-name { font-weight: 600; font-size: 0.75rem; }
.server-config { font-size: 0.68rem; color: var(--vp-c-text-2); }
.drift-reason {
font-size: 0.62rem;
color: #ef4444;
margin-top: 2px;
font-style: italic;
}
.drift-indicator { text-align: center; }
.drift-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 14px;
border-radius: 16px;
font-size: 0.78rem;
font-weight: 600;
}
.drift-status.ok { background: #d1fae5; color: #065f46; }
.drift-status.warning { background: #fef3c7; color: #92400e; }
.drift-status.danger { background: #fee2e2; color: #991b1b; }
:root.dark .drift-status.ok { background: #022c2240; color: #6ee7b7; }
:root.dark .drift-status.warning { background: #451a0340; color: #fcd34d; }
:root.dark .drift-status.danger { background: #450a0a40; color: #fca5a5; }
.event-desc {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.8rem;
background: var(--vp-c-bg);
}
.event-title { font-weight: 600; font-size: 0.88rem; margin-bottom: 4px; }
.event-detail { font-size: 0.8rem; color: var(--vp-c-text-2); line-height: 1.6; margin: 0; }
.controls {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.ctrl-btn {
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.78rem;
transition: all 0.2s;
}
.ctrl-btn:disabled { opacity: 0.4; cursor: default; }
.ctrl-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.ctrl-btn.reset { color: var(--vp-c-text-3); }
.lesson-box {
border: 1px solid #3b82f640;
border-radius: 6px;
padding: 0.8rem;
background: #dbeafe10;
}
.lesson-title {
font-weight: 600;
font-size: 0.82rem;
margin-bottom: 0.5rem;
color: #2563eb;
}
:root.dark .lesson-title { color: #93c5fd; }
.lesson-items { display: flex; flex-direction: column; gap: 0.3rem; }
.lesson-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.78rem;
}
</style>
@@ -0,0 +1,329 @@
<template>
<div class="iac-best-practice-demo">
<div class="demo-label">交互演示 IaC 最佳实践</div>
<div class="practice-tabs">
<button
v-for="(tab, i) in practices"
:key="tab.key"
:class="['practice-tab', { active: activeTab === i }]"
@click="activeTab = i"
>
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-name">{{ tab.name }}</span>
</button>
</div>
<Transition name="fade" mode="out-in">
<div :key="activeTab" class="practice-content">
<div class="practice-header">
<span class="practice-icon">{{ currentPractice.icon }}</span>
<div>
<div class="practice-title">{{ currentPractice.title }}</div>
<div class="practice-subtitle">{{ currentPractice.subtitle }}</div>
</div>
</div>
<div class="do-dont-grid">
<div class="do-card">
<div class="card-label good-label"> 推荐做法</div>
<div class="card-items">
<div v-for="(item, i) in currentPractice.dos" :key="i" class="card-item">
{{ item }}
</div>
</div>
</div>
<div class="dont-card">
<div class="card-label bad-label"> 反面模式</div>
<div class="card-items">
<div v-for="(item, i) in currentPractice.donts" :key="i" class="card-item">
{{ item }}
</div>
</div>
</div>
</div>
<div v-if="currentPractice.code" class="code-example">
<div class="code-header">
<span>{{ currentPractice.codeTitle }}</span>
</div>
<pre class="code-body"><code>{{ currentPractice.code }}</code></pre>
</div>
<div class="maturity-bar">
<div class="maturity-label">实践成熟度</div>
<div class="maturity-track">
<div
v-for="(level, i) in maturityLevels"
:key="i"
:class="['maturity-segment', { filled: i <= currentPractice.maturity }]"
>
<span class="maturity-text">{{ level }}</span>
</div>
</div>
</div>
</div>
</Transition>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref(0)
const maturityLevels = ['入门', '基础', '进阶', '成熟', '卓越']
const practices = [
{
key: 'vcs', icon: '📂', name: '版本控制',
title: '实践一:基础设施代码纳入版本控制',
subtitle: '像管理应用代码一样管理基础设施代码',
dos: [
'所有 .tf 文件提交到 Git 仓库',
'使用分支策略(main / dev / feature',
'通过 Pull Request 进行代码审查',
'在 CI 中自动运行 terraform plan'
],
donts: [
'在本地执行 apply 后不提交代码',
'直接在 main 分支上修改',
'将 .tfstate 文件提交到 Git',
'跳过 Code Review 直接部署'
],
codeTitle: '.gitignore 示例',
code: `# 忽略本地状态文件
*.tfstate
*.tfstate.backup
.terraform/
# 忽略敏感变量文件
*.tfvars
!example.tfvars`,
maturity: 1
},
{
key: 'modules', icon: '🧩', name: '模块化',
title: '实践二:使用模块实现代码复用',
subtitle: '避免复制粘贴,通过模块封装通用基础设施模式',
dos: [
'将通用模式抽取为可复用模块',
'模块使用语义化版本号',
'为模块编写 README 和使用示例',
'通过 variables 暴露可配置参数'
],
donts: [
'在多个项目中复制粘贴相同代码',
'创建过于庞大的"万能"模块',
'模块内硬编码环境特定的值',
'不写文档直接发布模块'
],
codeTitle: '模块调用示例',
code: `module "web_server" {
source = "./modules/ec2-instance"
version = "2.1.0"
instance_type = "t3.micro"
environment = "production"
app_name = "my-web-app"
}`,
maturity: 2
},
{
key: 'state', icon: '💾', name: '状态管理',
title: '实践三:远程状态存储与锁定',
subtitle: 'State 文件是 IaC 的核心,必须安全可靠地管理',
dos: [
'使用远程后端(S3 + DynamoDB',
'启用状态文件加密',
'配置状态锁防止并发冲突',
'按环境/项目隔离状态文件'
],
donts: [
'将 State 存储在本地文件系统',
'多人共享同一个 State 无锁机制',
'手动编辑 terraform.tfstate',
'所有环境共用一个 State 文件'
],
codeTitle: '远程后端配置',
code: `terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "tf-lock"
}
}`,
maturity: 2
},
{
key: 'env', icon: '🌍', name: '环境管理',
title: '实践四:多环境一致性管理',
subtitle: '开发、测试、生产环境使用相同代码,仅参数不同',
dos: [
'使用 Workspace 或目录隔离环境',
'通过 .tfvars 文件区分环境参数',
'保持环境间代码结构完全一致',
'先在 dev 验证,再推广到 prod'
],
donts: [
'为每个环境维护独立的代码副本',
'在代码中硬编码环境名称',
'跳过测试环境直接部署生产',
'不同环境使用不同的模块版本'
],
codeTitle: '多环境目录结构',
code: `environments/
├── dev/
│ ├── main.tf # 引用相同模块
│ └── dev.tfvars # 开发环境参数
├── staging/
│ ├── main.tf
│ └── staging.tfvars
└── prod/
├── main.tf
└── prod.tfvars`,
maturity: 3
}
]
const currentPractice = computed(() => practices[activeTab.value])
</script>
<style scoped>
.iac-best-practice-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
text-align: center;
}
.practice-tabs {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 1rem;
justify-content: center;
}
.practice-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
}
.practice-tab.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.practice-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 1rem;
}
.practice-icon { font-size: 1.5rem; }
.practice-title { font-weight: 600; font-size: 0.95rem; }
.practice-subtitle { font-size: 0.78rem; color: var(--vp-c-text-3); }
.do-dont-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem;
margin-bottom: 1rem;
}
@media (max-width: 540px) {
.do-dont-grid { grid-template-columns: 1fr; }
}
.do-card, .dont-card {
border-radius: 6px;
padding: 0.7rem;
border: 1px solid var(--vp-c-divider);
}
.do-card { background: #d1fae508; border-color: #6ee7b740; }
.dont-card { background: #fee2e208; border-color: #fca5a540; }
.card-label {
font-size: 0.75rem;
font-weight: 600;
margin-bottom: 0.4rem;
}
.good-label { color: #10b981; }
.bad-label { color: #ef4444; }
.card-items { display: flex; flex-direction: column; gap: 0.25rem; }
.card-item {
font-size: 0.75rem;
color: var(--vp-c-text-2);
padding-left: 0.5rem;
border-left: 2px solid var(--vp-c-divider);
}
.code-example {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
margin-bottom: 1rem;
}
.code-header {
background: var(--vp-c-bg-alt);
padding: 4px 10px;
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-body {
background: #1a1a2e;
color: #e0e0e0;
padding: 0.8rem;
font-size: 0.73rem;
font-family: 'Menlo', 'Consolas', monospace;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.maturity-bar { margin-top: 0.5rem; }
.maturity-label {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.maturity-track {
display: flex;
gap: 2px;
}
.maturity-segment {
flex: 1;
height: 24px;
border-radius: 4px;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.65rem;
color: var(--vp-c-text-3);
transition: all 0.3s;
}
.maturity-segment.filled {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
@@ -0,0 +1,301 @@
<template>
<div class="iac-concept-demo">
<div class="demo-label">交互演示 手动运维 vs 基础设施即代码</div>
<div class="toggle-bar">
<button
v-for="mode in modes"
:key="mode.key"
:class="['toggle-btn', { active: current === mode.key }]"
@click="current = mode.key"
>
{{ mode.icon }} {{ mode.label }}
</button>
</div>
<div class="scene-container">
<Transition name="fade" mode="out-in">
<div v-if="current === 'manual'" key="manual" class="scene manual-scene">
<div class="scene-title">手动运维流程</div>
<div class="steps">
<div
v-for="(step, i) in manualSteps"
:key="i"
:class="['step-card', { done: manualProgress > i, current: manualProgress === i }]"
>
<div class="step-num">{{ i + 1 }}</div>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-text">{{ step.text }}</div>
<div class="step-risk">{{ step.risk }}</div>
</div>
</div>
<button class="action-btn manual-btn" @click="advanceManual" :disabled="manualProgress >= manualSteps.length">
{{ manualProgress >= manualSteps.length ? '全部完成(耗时约 2 小时)' : '点击控制台按钮...' }}
</button>
<div v-if="manualProgress >= manualSteps.length" class="result-box warning">
手动操作完成但存在风险步骤不可重复无法审计容易遗漏配置
</div>
</div>
<div v-else key="iac" class="scene iac-scene">
<div class="scene-title">IaC 代码驱动流程</div>
<div class="code-block">
<div class="code-header">main.tf</div>
<pre class="code-content"><code>{{ iacCode }}</code></pre>
</div>
<div class="iac-steps">
<div
v-for="(step, i) in iacSteps"
:key="i"
:class="['iac-step', { done: iacProgress > i, current: iacProgress === i }]"
>
<span class="iac-arrow" v-if="i > 0"></span>
<span class="iac-badge">{{ step.icon }}</span>
<span class="iac-label">{{ step.text }}</span>
</div>
</div>
<button class="action-btn iac-btn" @click="advanceIac" :disabled="iacProgress >= iacSteps.length">
{{ iacProgress >= iacSteps.length ? '全部完成(耗时约 30 秒)' : '执行下一步' }}
</button>
<div v-if="iacProgress >= iacSteps.length" class="result-box success">
代码即文档可重复可审计可版本控制团队协作无忧
</div>
</div>
</Transition>
</div>
<div class="comparison-table">
<table>
<thead>
<tr>
<th>对比维度</th>
<th>手动运维</th>
<th>基础设施即代码</th>
</tr>
</thead>
<tbody>
<tr v-for="row in comparisonRows" :key="row.dim">
<td class="dim-cell">{{ row.dim }}</td>
<td class="bad-cell">{{ row.manual }}</td>
<td class="good-cell">{{ row.iac }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const current = ref('manual')
const manualProgress = ref(0)
const iacProgress = ref(0)
const modes = [
{ key: 'manual', icon: '🖱️', label: '手动运维' },
{ key: 'iac', icon: '📝', label: '基础设施即代码' }
]
const manualSteps = [
{ icon: '🌐', text: '登录云控制台', risk: '需要记住密码' },
{ icon: '🖥️', text: '手动创建服务器', risk: '配置可能遗漏' },
{ icon: '🔧', text: '配置安全组规则', risk: '容易开放过多端口' },
{ icon: '💾', text: '挂载存储卷', risk: '大小可能选错' },
{ icon: '🔗', text: '配置负载均衡', risk: '路由规则易出错' },
{ icon: '📋', text: '手动记录到文档', risk: '文档很快过时' }
]
const iacSteps = [
{ icon: '📝', text: 'Write(编写代码)' },
{ icon: '🔍', text: 'Plan(预览变更)' },
{ icon: '🚀', text: 'Apply(自动执行)' },
{ icon: '✅', text: 'Done(状态记录)' }
]
const iacCode = `resource "aws_instance" "web" {
ami = "ami-0c55b159"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Env = "production"
}
}
resource "aws_security_group" "web_sg" {
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}`
const comparisonRows = [
{ dim: '可重复性', manual: '每次操作可能不同', iac: '代码保证完全一致' },
{ dim: '速度', manual: '分钟到小时级', iac: '秒到分钟级' },
{ dim: '审计追踪', manual: '依赖人工记录', iac: 'Git 历史自动记录' },
{ dim: '协作', manual: '口头传达、截图', iac: 'Code Review、PR 流程' },
{ dim: '回滚', manual: '几乎不可能', iac: 'git revert 一键回滚' }
]
function advanceManual() {
if (manualProgress.value < manualSteps.length) {
manualProgress.value++
}
}
function advanceIac() {
if (iacProgress.value < iacSteps.length) {
iacProgress.value++
}
}
</script>
<style scoped>
.iac-concept-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
text-align: center;
}
.toggle-bar {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.toggle-btn {
padding: 6px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.85rem;
transition: all 0.2s;
}
.toggle-btn.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.scene-container { min-height: 200px; }
.scene-title {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 0.8rem;
text-align: center;
}
.steps {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.5rem;
margin-bottom: 1rem;
}
.step-card {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
padding: 0.6rem;
background: var(--vp-c-bg);
text-align: center;
transition: all 0.3s;
opacity: 0.5;
}
.step-card.done { opacity: 1; border-color: #f59e0b; background: #fef3c710; }
.step-card.current { opacity: 1; border-color: var(--vp-c-brand); box-shadow: 0 0 0 2px var(--vp-c-brand-light); }
.step-num { font-size: 0.65rem; color: var(--vp-c-text-3); }
.step-icon { font-size: 1.4rem; margin: 4px 0; }
.step-text { font-size: 0.75rem; font-weight: 600; }
.step-risk { font-size: 0.65rem; color: #ef4444; margin-top: 2px; }
.action-btn {
display: block;
margin: 0 auto;
padding: 8px 20px;
border: none;
border-radius: 6px;
font-size: 0.82rem;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:disabled { opacity: 0.6; cursor: default; }
.manual-btn { background: #fbbf24; color: #78350f; }
.iac-btn { background: var(--vp-c-brand); color: #fff; }
.code-block {
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
overflow: hidden;
margin-bottom: 1rem;
}
.code-header {
background: var(--vp-c-bg-alt);
padding: 4px 10px;
font-size: 0.72rem;
font-weight: 600;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.code-content {
padding: 0.8rem;
font-size: 0.75rem;
line-height: 1.5;
margin: 0;
overflow-x: auto;
background: var(--vp-c-bg);
}
.iac-steps {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.3rem;
margin-bottom: 1rem;
}
.iac-step {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 16px;
font-size: 0.78rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
opacity: 0.4;
transition: all 0.3s;
}
.iac-step.done { opacity: 1; border-color: #10b981; background: #d1fae510; }
.iac-step.current { opacity: 1; border-color: var(--vp-c-brand); box-shadow: 0 0 0 2px var(--vp-c-brand-light); }
.iac-arrow { color: var(--vp-c-text-3); font-size: 0.8rem; }
.result-box {
margin-top: 0.8rem;
padding: 0.6rem 1rem;
border-radius: 6px;
font-size: 0.8rem;
text-align: center;
}
.result-box.warning { background: #fef3c7; color: #92400e; border: 1px solid #fcd34d; }
.result-box.success { background: #d1fae5; color: #065f46; border: 1px solid #6ee7b7; }
:root.dark .result-box.warning { background: #451a0320; color: #fcd34d; }
:root.dark .result-box.success { background: #022c2220; color: #6ee7b7; }
.comparison-table { margin-top: 1rem; overflow-x: auto; }
.comparison-table table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
.comparison-table th, .comparison-table td {
padding: 6px 10px;
border: 1px solid var(--vp-c-divider);
text-align: center;
}
.comparison-table th { background: var(--vp-c-bg-alt); font-weight: 600; }
.dim-cell { font-weight: 600; }
.bad-cell { color: #ef4444; }
.good-cell { color: #10b981; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
@@ -0,0 +1,374 @@
<template>
<div class="iac-tool-comparison-demo">
<div class="demo-label">交互演示 主流 IaC 工具对比</div>
<div class="tool-selector">
<span class="selector-hint">选择要对比的工具至少选 2 </span>
<div class="tool-chips">
<button
v-for="tool in tools"
:key="tool.name"
:class="['tool-chip', { selected: selectedTools.includes(tool.name) }]"
:style="selectedTools.includes(tool.name) ? { background: tool.color, borderColor: tool.color, color: '#fff' } : {}"
@click="toggleTool(tool.name)"
>
{{ tool.icon }} {{ tool.name }}
</button>
</div>
</div>
<div v-if="selectedTools.length >= 2" class="comparison-grid">
<table>
<thead>
<tr>
<th class="feature-col">特性</th>
<th v-for="name in selectedTools" :key="name" class="tool-col">
<span class="tool-header-icon">{{ getToolByName(name).icon }}</span>
<span>{{ name }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="feature in features" :key="feature.key">
<td class="feature-cell">{{ feature.label }}</td>
<td v-for="name in selectedTools" :key="name" class="value-cell">
<span :class="getCellClass(name, feature.key)">
{{ getToolByName(name).features[feature.key] }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="empty-hint">
请至少选择 2 个工具进行对比
</div>
<Transition name="fade">
<div v-if="selectedDetail" class="detail-card">
<div class="detail-header">
<span class="detail-icon">{{ selectedDetail.icon }}</span>
<span class="detail-name">{{ selectedDetail.name }}</span>
<button class="close-btn" @click="detailName = ''"></button>
</div>
<p class="detail-desc">{{ selectedDetail.desc }}</p>
<div class="detail-code">
<div class="code-label">示例代码片段</div>
<pre class="code-block"><code>{{ selectedDetail.example }}</code></pre>
</div>
</div>
</Transition>
<div class="detail-hint" v-if="selectedTools.length >= 2 && !detailName">
点击下方工具名称查看详细介绍和代码示例
</div>
<div class="tool-detail-btns" v-if="selectedTools.length >= 2">
<button
v-for="name in selectedTools"
:key="name"
:class="['detail-btn', { active: detailName === name }]"
@click="detailName = detailName === name ? '' : name"
>
{{ getToolByName(name).icon }} {{ name }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const selectedTools = ref(['Terraform', 'CloudFormation'])
const detailName = ref('')
const selectedDetail = computed(() => {
if (!detailName.value) return null
return tools.find(t => t.name === detailName.value)
})
const features = [
{ key: 'vendor', label: '厂商' },
{ key: 'language', label: '配置语言' },
{ key: 'style', label: '声明式/命令式' },
{ key: 'multiCloud', label: '多云支持' },
{ key: 'stateManagement', label: '状态管理' },
{ key: 'learning', label: '学习曲线' },
{ key: 'community', label: '社区生态' },
{ key: 'bestFor', label: '最佳场景' }
]
const tools = [
{
name: 'Terraform',
icon: '🟣',
color: '#7c3aed',
features: {
vendor: 'HashiCorp',
language: 'HCL',
style: '声明式',
multiCloud: '原生多云',
stateManagement: 'State 文件',
learning: '中等',
community: '非常活跃',
bestFor: '多云/混合云'
},
desc: 'Terraform 是目前最流行的开源 IaC 工具,由 HashiCorp 开发。它使用自研的 HCL 语言,通过 Provider 机制支持几乎所有主流云平台。',
example: `resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
tags = { Env = "prod" }
}
resource "aws_instance" "web" {
ami = "ami-0c55b159"
instance_type = "t3.micro"
}`
},
{
name: 'CloudFormation',
icon: '🟠',
color: '#ea580c',
features: {
vendor: 'AWS',
language: 'YAML / JSON',
style: '声明式',
multiCloud: '仅 AWS',
stateManagement: 'AWS 托管',
learning: '中等偏高',
community: 'AWS 生态',
bestFor: '纯 AWS 环境'
},
desc: 'CloudFormation 是 AWS 原生的 IaC 服务,与 AWS 服务深度集成。状态由 AWS 自动管理,无需额外维护 State 文件。',
example: `Resources:
WebServer:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0c55b159
InstanceType: t3.micro
Tags:
- Key: Name
Value: web-server`
},
{
name: 'Pulumi',
icon: '🔵',
color: '#2563eb',
features: {
vendor: 'Pulumi',
language: 'TypeScript/Python/Go',
style: '命令式 + 声明式',
multiCloud: '原生多云',
stateManagement: 'Pulumi Cloud / 自管',
learning: '低(熟悉编程语言)',
community: '快速增长',
bestFor: '开发者友好场景'
},
desc: 'Pulumi 允许使用真正的编程语言(TypeScript、Python、Go 等)来定义基础设施,对开发者非常友好,支持条件判断、循环等编程特性。',
example: `import * as aws from "@pulumi/aws"
const bucket = new aws.s3.Bucket("data", {
tags: { Env: "prod" }
})
const server = new aws.ec2.Instance("web", {
ami: "ami-0c55b159",
instanceType: "t3.micro",
})`
},
{
name: 'Ansible',
icon: '🔴',
color: '#dc2626',
features: {
vendor: 'Red Hat',
language: 'YAML (Playbook)',
style: '命令式',
multiCloud: '通过模块支持',
stateManagement: '无状态(幂等)',
learning: '低',
community: '非常活跃',
bestFor: '配置管理 + 编排'
},
desc: 'Ansible 是一个无代理的自动化工具,擅长配置管理和应用部署。它通过 SSH 连接目标机器执行任务,无需安装客户端。',
example: `- name: 部署 Web 服务器
hosts: webservers
tasks:
- name: 安装 Nginx
apt:
name: nginx
state: present
- name: 启动服务
service:
name: nginx
state: started`
}
]
function getToolByName(name) {
return tools.find(t => t.name === name)
}
function toggleTool(name) {
const idx = selectedTools.value.indexOf(name)
if (idx >= 0) {
if (selectedTools.value.length > 2) {
selectedTools.value.splice(idx, 1)
}
} else {
selectedTools.value.push(name)
}
}
function getCellClass(toolName, featureKey) {
const val = getToolByName(toolName).features[featureKey]
if (featureKey === 'multiCloud') {
if (val.includes('原生多云')) return 'cell-good'
if (val.includes('仅')) return 'cell-warn'
return ''
}
if (featureKey === 'learning') {
if (val === '低') return 'cell-good'
if (val.includes('高')) return 'cell-warn'
return ''
}
return ''
}
</script>
<style scoped>
.iac-tool-comparison-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
text-align: center;
}
.selector-hint {
font-size: 0.78rem;
color: var(--vp-c-text-3);
display: block;
margin-bottom: 0.5rem;
}
.tool-chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 1rem;
}
.tool-chip {
padding: 5px 14px;
border: 1px solid var(--vp-c-divider);
border-radius: 20px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
}
.tool-chip:hover { transform: scale(1.05); }
.comparison-grid { overflow-x: auto; margin-bottom: 1rem; }
.comparison-grid table {
width: 100%;
border-collapse: collapse;
font-size: 0.78rem;
}
.comparison-grid th,
.comparison-grid td {
padding: 8px 10px;
border: 1px solid var(--vp-c-divider);
text-align: center;
}
.comparison-grid th {
background: var(--vp-c-bg-alt);
font-weight: 600;
}
.feature-col { text-align: left; min-width: 80px; }
.feature-cell { font-weight: 600; text-align: left; }
.tool-header-icon { margin-right: 4px; }
.cell-good { color: #10b981; font-weight: 600; }
.cell-warn { color: #f59e0b; }
.empty-hint {
text-align: center;
padding: 2rem;
color: var(--vp-c-text-3);
font-size: 0.85rem;
}
.detail-hint {
text-align: center;
font-size: 0.75rem;
color: var(--vp-c-text-3);
margin-bottom: 0.5rem;
}
.tool-detail-btns {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.detail-btn {
padding: 4px 12px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.78rem;
transition: all 0.2s;
}
.detail-btn.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.detail-card {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
padding: 1rem;
background: var(--vp-c-bg);
margin-top: 0.5rem;
}
.detail-header {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 0.5rem;
}
.detail-icon { font-size: 1.2rem; }
.detail-name { font-weight: 600; font-size: 1rem; flex: 1; }
.close-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: var(--vp-c-text-3);
}
.detail-desc {
font-size: 0.82rem;
color: var(--vp-c-text-2);
line-height: 1.6;
margin-bottom: 0.8rem;
}
.code-label {
font-size: 0.72rem;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.code-block {
background: #1a1a2e;
color: #e0e0e0;
padding: 0.8rem;
border-radius: 6px;
font-size: 0.73rem;
font-family: 'Menlo', 'Consolas', monospace;
line-height: 1.5;
overflow-x: auto;
margin: 0;
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.25s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
@@ -0,0 +1,333 @@
<template>
<div class="terraform-workflow-demo">
<div class="demo-label">交互演示 Terraform 工作流四阶段</div>
<div class="stage-nav">
<div
v-for="(stage, i) in stages"
:key="stage.key"
:class="['stage-tab', { active: currentStage === i, completed: i < currentStage }]"
@click="goToStage(i)"
>
<span class="stage-icon">{{ stage.icon }}</span>
<span class="stage-name">{{ stage.name }}</span>
<span v-if="i < stages.length - 1" class="stage-arrow"></span>
</div>
</div>
<div class="stage-content">
<Transition name="slide" mode="out-in">
<div :key="currentStage" class="stage-panel">
<div class="panel-header">
<span class="panel-icon">{{ stages[currentStage].icon }}</span>
<span class="panel-title">{{ stages[currentStage].title }}</span>
</div>
<p class="panel-desc">{{ stages[currentStage].desc }}</p>
<div class="terminal-block">
<div class="terminal-header">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
<span class="terminal-title">Terminal</span>
</div>
<div class="terminal-body">
<div v-for="(line, li) in visibleLines" :key="li" class="terminal-line">
<span :class="line.cls">{{ line.text }}</span>
</div>
<span v-if="isTyping" class="cursor-blink">_</span>
</div>
</div>
<div class="key-points">
<div v-for="(point, pi) in stages[currentStage].points" :key="pi" class="point-item">
<span class="point-bullet">{{ point.icon }}</span>
<span class="point-text">{{ point.text }}</span>
</div>
</div>
</div>
</Transition>
</div>
<div class="nav-buttons">
<button class="nav-btn" :disabled="currentStage === 0" @click="goToStage(currentStage - 1)">
上一步
</button>
<span class="stage-indicator">{{ currentStage + 1 }} / {{ stages.length }}</span>
<button class="nav-btn primary" :disabled="currentStage === stages.length - 1" @click="goToStage(currentStage + 1)">
下一步
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const currentStage = ref(0)
const typingIndex = ref(0)
const isTyping = ref(false)
const stages = [
{
key: 'write', icon: '📝', name: 'Write', title: 'Write ── 编写基础设施代码',
desc: '用声明式语言(HCL)描述你期望的基础设施状态。代码就是文档,可以提交到 Git 进行版本管理和 Code Review。',
lines: [
{ text: '$ vim main.tf', cls: 'cmd' },
{ text: '', cls: '' },
{ text: 'resource "aws_instance" "app" {', cls: 'code' },
{ text: ' ami = "ami-0c55b159"', cls: 'code' },
{ text: ' instance_type = "t3.micro"', cls: 'code' },
{ text: ' tags = { Name = "my-app" }', cls: 'code' },
{ text: '}', cls: 'code' },
{ text: '', cls: '' },
{ text: '# 文件已保存 ✓', cls: 'success' }
],
points: [
{ icon: '📄', text: '使用 .tf 文件描述资源' },
{ icon: '🔧', text: 'HCL 语法简洁易读' },
{ icon: '📦', text: '支持模块化复用' }
]
},
{
key: 'plan', icon: '🔍', name: 'Plan', title: 'Plan ── 预览变更计划',
desc: 'Terraform 会对比当前状态和期望状态,生成一份详细的执行计划。这一步不会做任何实际变更,是安全的"预演"。',
lines: [
{ text: '$ terraform plan', cls: 'cmd' },
{ text: '', cls: '' },
{ text: 'Terraform will perform the following actions:', cls: 'info' },
{ text: '', cls: '' },
{ text: ' + aws_instance.app', cls: 'add' },
{ text: ' ami: "ami-0c55b159"', cls: 'detail' },
{ text: ' instance_type: "t3.micro"', cls: 'detail' },
{ text: '', cls: '' },
{ text: 'Plan: 1 to add, 0 to change, 0 to destroy.', cls: 'success' }
],
points: [
{ icon: '🛡️', text: '变更前先预览,避免意外' },
{ icon: '', text: '绿色 + 表示新增资源' },
{ icon: '🔄', text: '~ 表示修改,- 表示删除' }
]
},
{
key: 'apply', icon: '🚀', name: 'Apply', title: 'Apply ── 执行变更',
desc: '确认计划无误后,Terraform 调用云平台 API 创建/修改/删除资源,并将最终状态写入 State 文件。',
lines: [
{ text: '$ terraform apply', cls: 'cmd' },
{ text: '', cls: '' },
{ text: 'aws_instance.app: Creating...', cls: 'info' },
{ text: 'aws_instance.app: Still creating... [10s elapsed]', cls: 'info' },
{ text: 'aws_instance.app: Creation complete after 32s', cls: 'success' },
{ text: '', cls: '' },
{ text: 'Apply complete! Resources: 1 added, 0 changed, 0 destroyed.', cls: 'success' },
{ text: '', cls: '' },
{ text: 'Outputs:', cls: 'info' },
{ text: ' public_ip = "54.123.45.67"', cls: 'output' }
],
points: [
{ icon: '☁️', text: '自动调用云平台 API' },
{ icon: '💾', text: '状态保存到 terraform.tfstate' },
{ icon: '📤', text: '输出关键信息(IP、域名等)' }
]
},
{
key: 'destroy', icon: '🗑️', name: 'Destroy', title: 'Destroy ── 销毁资源',
desc: '不再需要时,一条命令即可安全销毁所有资源。Terraform 会按照依赖关系的逆序逐一清理,避免残留。',
lines: [
{ text: '$ terraform destroy', cls: 'cmd' },
{ text: '', cls: '' },
{ text: 'Terraform will perform the following actions:', cls: 'info' },
{ text: '', cls: '' },
{ text: ' - aws_instance.app', cls: 'remove' },
{ text: '', cls: '' },
{ text: 'Plan: 0 to add, 0 to change, 1 to destroy.', cls: 'warn' },
{ text: 'aws_instance.app: Destroying...', cls: 'info' },
{ text: 'aws_instance.app: Destruction complete after 15s', cls: 'success' },
{ text: '', cls: '' },
{ text: 'Destroy complete! Resources: 1 destroyed.', cls: 'success' }
],
points: [
{ icon: '🧹', text: '按依赖逆序安全清理' },
{ icon: '💰', text: '避免资源遗忘产生费用' },
{ icon: '♻️', text: '环境可随时重建' }
]
}
]
const visibleLines = computed(() => {
return stages[currentStage.value].lines.slice(0, typingIndex.value)
})
function goToStage(i) {
currentStage.value = i
}
watch(currentStage, () => {
typingIndex.value = 0
isTyping.value = true
typeNext()
})
function typeNext() {
const total = stages[currentStage.value].lines.length
if (typingIndex.value < total) {
setTimeout(() => {
typingIndex.value++
typeNext()
}, 120)
} else {
isTyping.value = false
}
}
// Initialize first stage
typeNext()
</script>
<style scoped>
.terraform-workflow-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
background: var(--vp-c-bg-soft);
padding: 1rem 1.2rem;
margin: 1rem 0;
}
.demo-label {
font-size: 0.78rem;
font-weight: bold;
color: var(--vp-c-text-2);
margin-bottom: 1rem;
text-align: center;
}
.stage-nav {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.2rem;
margin-bottom: 1rem;
}
.stage-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
transition: all 0.2s;
}
.stage-tab.active {
background: var(--vp-c-brand);
color: #fff;
border-color: var(--vp-c-brand);
}
.stage-tab.completed {
border-color: #10b981;
background: #d1fae510;
}
.stage-arrow { color: var(--vp-c-text-3); margin: 0 2px; }
.stage-content { min-height: 280px; }
.panel-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 0.5rem;
}
.panel-icon { font-size: 1.3rem; }
.panel-title { font-weight: 600; font-size: 0.95rem; }
.panel-desc {
font-size: 0.82rem;
color: var(--vp-c-text-2);
margin-bottom: 0.8rem;
line-height: 1.6;
}
.terminal-block {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
margin-bottom: 0.8rem;
}
.terminal-header {
background: #1e1e1e;
padding: 6px 10px;
display: flex;
align-items: center;
gap: 6px;
}
.terminal-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.terminal-dot.red { background: #ff5f57; }
.terminal-dot.yellow { background: #febc2e; }
.terminal-dot.green { background: #28c840; }
.terminal-title {
font-size: 0.7rem;
color: #888;
margin-left: 6px;
}
.terminal-body {
background: #1a1a2e;
padding: 0.8rem;
font-family: 'Menlo', 'Consolas', monospace;
font-size: 0.73rem;
line-height: 1.6;
min-height: 160px;
color: #e0e0e0;
}
.terminal-line .cmd { color: #7dd3fc; }
.terminal-line .code { color: #a5b4fc; }
.terminal-line .info { color: #94a3b8; }
.terminal-line .add { color: #4ade80; }
.terminal-line .remove { color: #f87171; }
.terminal-line .detail { color: #cbd5e1; padding-left: 1rem; }
.terminal-line .success { color: #34d399; font-weight: 600; }
.terminal-line .warn { color: #fbbf24; }
.terminal-line .output { color: #c084fc; }
.cursor-blink {
animation: blink 1s step-end infinite;
color: #7dd3fc;
}
@keyframes blink { 50% { opacity: 0; } }
.key-points {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.point-item {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 16px;
font-size: 0.75rem;
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-divider);
}
.nav-buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
margin-top: 1rem;
}
.nav-btn {
padding: 6px 16px;
border: 1px solid var(--vp-c-divider);
border-radius: 6px;
background: var(--vp-c-bg);
cursor: pointer;
font-size: 0.8rem;
transition: all 0.2s;
}
.nav-btn:disabled { opacity: 0.4; cursor: default; }
.nav-btn.primary { background: var(--vp-c-brand); color: #fff; border-color: var(--vp-c-brand); }
.stage-indicator { font-size: 0.75rem; color: var(--vp-c-text-3); }
.slide-enter-active, .slide-leave-active { transition: all 0.3s ease; }
.slide-enter-from { opacity: 0; transform: translateX(20px); }
.slide-leave-to { opacity: 0; transform: translateX(-20px); }
</style>
@@ -0,0 +1,333 @@
<template>
<div class="finetuning-pipeline-demo">
<div class="pipeline-header">
<h4>微调流水线演示</h4>
<p class="subtitle">点击每个阶段了解微调的完整流程</p>
</div>
<div class="pipeline-steps">
<div
v-for="(step, index) in steps"
:key="step.id"
class="pipeline-step"
:class="{ active: activeStep === index, completed: index < activeStep }"
@click="setStep(index)"
>
<div class="step-icon">{{ step.icon }}</div>
<div class="step-label">{{ step.label }}</div>
<div v-if="index < steps.length - 1" class="step-arrow">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</div>
<div class="step-detail" v-if="activeStep >= 0">
<div class="detail-title">
{{ steps[activeStep].icon }} {{ steps[activeStep].label }}
</div>
<p class="detail-desc">{{ steps[activeStep].description }}</p>
<div class="detail-points">
<div v-for="(point, i) in steps[activeStep].points" :key="i" class="point-item">
<span class="point-bullet">{{ i + 1 }}</span>
<span>{{ point }}</span>
</div>
</div>
<div class="detail-example" v-if="steps[activeStep].example">
<div class="example-label">示例</div>
<code>{{ steps[activeStep].example }}</code>
</div>
</div>
<div class="pipeline-controls">
<button class="ctrl-btn" :disabled="activeStep <= 0" @click="prevStep">上一步</button>
<span class="step-indicator">{{ activeStep + 1 }} / {{ steps.length }}</span>
<button class="ctrl-btn primary" :disabled="activeStep >= steps.length - 1" @click="nextStep">下一步</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const activeStep = ref(0)
const steps = [
{
id: 'base',
icon: '🧠',
label: '选择基座模型',
description: '微调的第一步是选择一个合适的预训练基座模型。基座模型已经在海量数据上学习了通用的语言能力,我们要做的是在此基础上进行"专业化训练"。',
points: [
'根据任务需求选择模型规模(7B、13B、70B 等)',
'考虑开源许可证(Apache 2.0、Llama 许可等)',
'评估模型的基础能力是否匹配目标场景',
'常见选择:Llama、Qwen、Mistral、DeepSeek 等'
],
example: 'model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2-7B")'
},
{
id: 'data',
icon: '📊',
label: '准备训练数据',
description: '高质量的训练数据是微调成功的关键。数据的质量远比数量重要——1000 条精心标注的数据,往往胜过 10 万条噪声数据。',
points: [
'收集与目标任务相关的数据样本',
'清洗数据:去重、过滤低质量内容',
'格式化为模型要求的输入格式(如 instruction-response 对)',
'划分训练集、验证集(通常 9:1)'
],
example: '{"instruction": "翻译成英文", "input": "你好世界", "output": "Hello World"}'
},
{
id: 'train',
icon: '⚙️',
label: '执行微调训练',
description: '使用准备好的数据对模型进行训练。现代微调通常采用参数高效方法(如 LoRA),只更新模型的一小部分参数,大幅降低计算成本。',
points: [
'配置训练超参数(学习率、批次大小、训练轮数)',
'选择微调策略(全量微调 / LoRA / QLoRA',
'监控训练损失曲线,防止过拟合',
'通常需要 1-4 个 GPU,训练数小时到数天'
],
example: 'trainer = SFTTrainer(model, train_dataset, peft_config=lora_config)'
},
{
id: 'eval',
icon: '📈',
label: '评估与测试',
description: '训练完成后,需要全面评估模型的表现。不仅要看自动化指标,更要进行人工评测,确保模型在真实场景中表现良好。',
points: [
'在验证集上计算损失和困惑度(Perplexity',
'使用任务特定指标(BLEU、ROUGE、准确率等)',
'人工评测:流畅度、准确性、安全性',
'与基座模型对比,确认微调带来了提升'
],
example: 'eval_results = trainer.evaluate(eval_dataset)'
},
{
id: 'deploy',
icon: '🚀',
label: '部署上线',
description: '将微调好的模型部署到生产环境,对外提供服务。部署前通常需要进行模型优化(量化、蒸馏等)以降低推理成本。',
points: [
'导出模型权重,合并 LoRA 适配器',
'应用量化技术压缩模型体积',
'选择部署方案(API 服务、边缘部署等)',
'配置监控和日志,持续跟踪线上表现'
],
example: 'model.merge_and_unload().save_pretrained("my-finetuned-model")'
}
]
function setStep(index) {
activeStep.value = index
}
function prevStep() {
if (activeStep.value > 0) activeStep.value--
}
function nextStep() {
if (activeStep.value < steps.length - 1) activeStep.value++
}
</script>
<style scoped>
.finetuning-pipeline-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.pipeline-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 20px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.pipeline-steps {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.pipeline-step {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
transition: all 0.2s;
}
.step-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
transition: all 0.3s;
}
.pipeline-step.active .step-icon {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
transform: scale(1.1);
}
.pipeline-step.completed .step-icon {
border-color: #10b981;
background: #d1fae5;
}
.step-label {
font-size: 12px;
color: var(--vp-c-text-2);
max-width: 64px;
text-align: center;
line-height: 1.3;
}
.pipeline-step.active .step-label {
color: var(--vp-c-brand-1);
font-weight: 600;
}
.step-arrow {
color: var(--vp-c-text-3);
display: flex;
align-items: center;
margin: 0 2px;
}
.step-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
border: 1px solid var(--vp-c-divider);
}
.detail-title {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.detail-desc {
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 12px;
}
.detail-points {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.point-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.point-bullet {
min-width: 20px;
height: 20px;
border-radius: 50%;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
}
.detail-example {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
}
.example-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-example code {
font-size: 12px;
color: var(--vp-c-brand-1);
word-break: break-all;
}
.pipeline-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.ctrl-btn {
padding: 6px 16px;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.ctrl-btn:hover:not(:disabled) {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.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);
}
.ctrl-btn.primary:hover:not(:disabled) {
opacity: 0.9;
}
.step-indicator {
font-size: 13px;
color: var(--vp-c-text-3);
}
</style>
@@ -0,0 +1,512 @@
<template>
<div class="lora-demo">
<div class="demo-header">
<h4>LoRA 低秩适配原理演示</h4>
<p class="subtitle">理解 LoRA 如何用极少参数实现高效微调</p>
</div>
<div class="tabs">
<button
v-for="tab in tabs"
:key="tab.id"
class="tab-btn"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>{{ tab.label }}</button>
</div>
<!-- 核心原理 -->
<div v-if="activeTab === 'principle'" class="tab-content">
<div class="matrix-visual">
<div class="matrix-row">
<div class="matrix-box frozen">
<div class="matrix-label">原始权重 W</div>
<div class="matrix-size">{{ matrixSize }}x{{ matrixSize }}</div>
<div class="matrix-grid">
<div v-for="i in 16" :key="i" class="cell frozen-cell"></div>
</div>
<div class="param-count">{{ (matrixSize * matrixSize).toLocaleString() }} 参数</div>
<div class="status-badge frozen-badge">冻结不动</div>
</div>
<div class="plus-sign">+</div>
<div class="matrix-box trainable">
<div class="matrix-label">LoRA 适配器</div>
<div class="lora-decompose">
<div class="small-matrix a-matrix">
<div class="sm-label">A</div>
<div class="sm-size">{{ matrixSize }}x{{ loraRank }}</div>
<div class="sm-grid">
<div v-for="i in 8" :key="i" class="cell a-cell"></div>
</div>
</div>
<div class="multiply-sign">x</div>
<div class="small-matrix b-matrix">
<div class="sm-label">B</div>
<div class="sm-size">{{ loraRank }}x{{ matrixSize }}</div>
<div class="sm-grid">
<div v-for="i in 8" :key="i" class="cell b-cell"></div>
</div>
</div>
</div>
<div class="param-count lora-count">{{ loraParams.toLocaleString() }} 参数</div>
<div class="status-badge train-badge">可训练</div>
</div>
</div>
</div>
<div class="savings-bar">
<div class="savings-label">参数节省比例</div>
<div class="bar-track">
<div class="bar-fill" :style="{ width: savingsPercent + '%' }"></div>
</div>
<div class="savings-value">节省 {{ savingsPercent.toFixed(1) }}% 参数</div>
</div>
<div class="rank-control">
<label>LoRA (Rank): <strong>{{ loraRank }}</strong></label>
<input type="range" min="1" max="64" v-model.number="loraRank" />
<div class="rank-hints">
<span>秩越小 = 参数越少训练越快</span>
<span>秩越大 = 表达力越强效果越好</span>
</div>
</div>
</div>
<!-- 直觉类比 -->
<div v-if="activeTab === 'analogy'" class="tab-content">
<div class="analogy-card">
<div class="analogy-icon">🎨</div>
<div class="analogy-text">
<p><strong>想象你有一幅巨大的油画预训练模型</strong></p>
<p>传统微调就像把整幅画重新画一遍费时费力还可能破坏原作的精髓</p>
<p> LoRA 的做法是<strong>在原画上覆盖一层薄薄的透明贴纸</strong>只在贴纸上做修改原画完好无损贴纸又轻又薄随时可以换</p>
</div>
</div>
<div class="comparison-table">
<div class="comp-row header">
<div class="comp-cell">对比维度</div>
<div class="comp-cell">全量微调</div>
<div class="comp-cell highlight">LoRA 微调</div>
</div>
<div v-for="row in comparisonRows" :key="row.dim" class="comp-row">
<div class="comp-cell dim">{{ row.dim }}</div>
<div class="comp-cell">{{ row.full }}</div>
<div class="comp-cell highlight">{{ row.lora }}</div>
</div>
</div>
</div>
<!-- 实际应用 -->
<div v-if="activeTab === 'usage'" class="tab-content">
<div class="usage-steps">
<div v-for="(step, i) in usageSteps" :key="i" class="usage-step">
<div class="usage-num">{{ i + 1 }}</div>
<div class="usage-body">
<div class="usage-title">{{ step.title }}</div>
<div class="usage-code">
<code>{{ step.code }}</code>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeTab = ref('principle')
const matrixSize = ref(4096)
const loraRank = ref(8)
const tabs = [
{ id: 'principle', label: '核心原理' },
{ id: 'analogy', label: '直觉类比' },
{ id: 'usage', label: '实际应用' }
]
const loraParams = computed(() => {
return matrixSize.value * loraRank.value + loraRank.value * matrixSize.value
})
const fullParams = computed(() => matrixSize.value * matrixSize.value)
const savingsPercent = computed(() => {
return ((1 - loraParams.value / fullParams.value) * 100)
})
const comparisonRows = [
{ dim: '训练参数量', full: '100%(数十亿)', lora: '0.1%~1%(数百万)' },
{ dim: '显存需求', full: '4x A100 80GB', lora: '1x RTX 4090 24GB' },
{ dim: '训练时间', full: '数天~数周', lora: '数小时~1天' },
{ dim: '存储开销', full: '完整模型副本(~14GB', lora: '适配器文件(~几十MB' },
{ dim: '多任务切换', full: '需要多个完整模型', lora: '共享基座 + 切换适配器' },
{ dim: '训练效果', full: '理论上限最高', lora: '接近全量微调(90%+' }
]
const usageSteps = [
{
title: '配置 LoRA 参数',
code: `lora_config = LoraConfig(\n r=8, # 秩\n lora_alpha=16, # 缩放因子\n target_modules=["q_proj", "v_proj"],\n lora_dropout=0.05\n)`
},
{
title: '应用到模型',
code: `model = get_peft_model(base_model, lora_config)\nmodel.print_trainable_parameters()\n# 可训练参数: 4,194,304 / 6,738,415,616 (0.06%)`
},
{
title: '训练完成后合并',
code: `merged_model = model.merge_and_unload()\nmerged_model.save_pretrained("my-model")`
}
]
</script>
<style scoped>
.lora-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 16px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.tab-btn {
padding: 6px 16px;
border-radius: 6px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.tab-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.tab-content {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.matrix-visual {
margin-bottom: 20px;
}
.matrix-row {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.matrix-box {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
text-align: center;
border: 2px solid var(--vp-c-divider);
min-width: 160px;
}
.matrix-box.frozen {
border-color: #94a3b8;
}
.matrix-box.trainable {
border-color: var(--vp-c-brand-1);
}
.matrix-label {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
color: var(--vp-c-text-1);
}
.matrix-size {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.matrix-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 2px;
margin: 0 auto 8px;
max-width: 80px;
}
.cell {
width: 16px;
height: 16px;
border-radius: 2px;
}
.frozen-cell {
background: #cbd5e1;
}
.param-count {
font-size: 12px;
color: var(--vp-c-text-2);
margin-bottom: 6px;
}
.lora-count {
color: var(--vp-c-brand-1);
font-weight: 600;
}
.status-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
}
.frozen-badge {
background: #e2e8f0;
color: #64748b;
}
.train-badge {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.plus-sign, .multiply-sign {
font-size: 24px;
font-weight: 700;
color: var(--vp-c-text-3);
}
.lora-decompose {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 8px 0;
}
.small-matrix {
text-align: center;
}
.sm-label {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-brand-1);
}
.sm-size {
font-size: 10px;
color: var(--vp-c-text-3);
}
.sm-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2px;
margin: 4px auto;
max-width: 40px;
}
.a-cell {
background: #818cf8;
}
.b-cell {
background: #f472b6;
}
.savings-bar {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
}
.savings-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 6px;
}
.bar-track {
height: 8px;
background: var(--vp-c-divider);
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--vp-c-brand-1), #10b981);
border-radius: 4px;
transition: width 0.3s;
}
.savings-value {
font-size: 13px;
font-weight: 600;
color: #10b981;
}
.rank-control {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 12px 16px;
}
.rank-control label {
font-size: 13px;
color: var(--vp-c-text-2);
}
.rank-control input[type="range"] {
width: 100%;
margin: 8px 0;
}
.rank-hints {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--vp-c-text-3);
}
.analogy-card {
display: flex;
gap: 16px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border-left: 4px solid var(--vp-c-brand-1);
}
.analogy-icon {
font-size: 36px;
flex-shrink: 0;
}
.analogy-text p {
margin: 0 0 8px;
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.7;
}
.comparison-table {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--vp-c-divider);
}
.comp-row {
display: grid;
grid-template-columns: 1fr 1.2fr 1.2fr;
}
.comp-row.header {
background: var(--vp-c-bg-alt);
font-weight: 600;
font-size: 13px;
}
.comp-cell {
padding: 10px 12px;
font-size: 13px;
color: var(--vp-c-text-2);
border-bottom: 1px solid var(--vp-c-divider);
}
.comp-cell.dim {
font-weight: 600;
color: var(--vp-c-text-1);
}
.comp-cell.highlight {
background: var(--vp-c-brand-soft);
}
.usage-steps {
display: flex;
flex-direction: column;
gap: 16px;
}
.usage-step {
display: flex;
gap: 12px;
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
}
.usage-num {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--vp-c-brand-1);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.usage-title {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.usage-code {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 10px;
}
.usage-code code {
font-size: 12px;
color: var(--vp-c-brand-1);
white-space: pre-wrap;
}
</style>
@@ -0,0 +1,345 @@
<template>
<div class="quantization-demo">
<div class="demo-header">
<h4>模型量化演示</h4>
<p class="subtitle">拖动滑块直观感受不同精度下的模型体积速度与质量变化</p>
</div>
<div class="precision-selector">
<div
v-for="(p, i) in precisions"
:key="p.id"
class="precision-card"
:class="{ active: activePrecision === i }"
@click="activePrecision = i"
>
<div class="prec-badge" :style="{ background: p.color }">{{ p.label }}</div>
<div class="prec-bits">{{ p.bits }} bit</div>
</div>
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">💾</div>
<div class="metric-label">模型体积</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.sizePercent + '%', background: currentPrecision.color }"></div>
</div>
<div class="metric-value">{{ currentPrecision.size }}</div>
</div>
<div class="metric-card">
<div class="metric-icon"></div>
<div class="metric-label">推理速度</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.speedPercent + '%', background: '#10b981' }"></div>
</div>
<div class="metric-value">{{ currentPrecision.speed }}</div>
</div>
<div class="metric-card">
<div class="metric-icon">🎯</div>
<div class="metric-label">输出质量</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.qualityPercent + '%', background: '#818cf8' }"></div>
</div>
<div class="metric-value">{{ currentPrecision.quality }}</div>
</div>
<div class="metric-card">
<div class="metric-icon">🖥</div>
<div class="metric-label">显存需求</div>
<div class="metric-bar-wrap">
<div class="metric-bar" :style="{ width: currentPrecision.vramPercent + '%', background: '#f59e0b' }"></div>
</div>
<div class="metric-value">{{ currentPrecision.vram }}</div>
</div>
</div>
<div class="detail-section">
<div class="detail-title">{{ currentPrecision.label }} 详解</div>
<p class="detail-desc">{{ currentPrecision.description }}</p>
<div class="bit-visual">
<div class="bit-label">单个参数存储示意</div>
<div class="bit-row">
<div
v-for="i in currentPrecision.bits"
:key="i"
class="bit-cell"
:style="{ background: currentPrecision.color }"
>{{ i % 2 === 0 ? '1' : '0' }}</div>
</div>
<div class="bit-info">每个参数占用 {{ currentPrecision.bits }} = {{ currentPrecision.bytes }} 字节</div>
</div>
<div class="use-case">
<span class="use-label">适用场景</span>
<span>{{ currentPrecision.useCase }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activePrecision = ref(0)
const precisions = [
{
id: 'fp32',
label: 'FP32',
bits: 32,
bytes: 4,
color: '#ef4444',
size: '~28 GB (7B 模型)',
sizePercent: 100,
speed: '1x (基准)',
speedPercent: 25,
quality: '100% (无损)',
qualityPercent: 100,
vram: '~32 GB',
vramPercent: 100,
description: 'FP32(32位浮点数)是模型训练时的默认精度。每个参数用 32 位存储,精度最高但体积最大。通常只在训练阶段使用,推理时很少直接使用 FP32。',
useCase: '模型训练、科研实验、精度敏感的任务'
},
{
id: 'fp16',
label: 'FP16',
bits: 16,
bytes: 2,
color: '#f59e0b',
size: '~14 GB (7B 模型)',
sizePercent: 50,
speed: '2x',
speedPercent: 50,
quality: '~99.5%',
qualityPercent: 99,
vram: '~16 GB',
vramPercent: 50,
description: 'FP16(16位浮点数)将精度减半,模型体积直接缩小一半。在绝大多数场景下,FP16 的输出质量与 FP32 几乎无差别,是目前最主流的推理精度。',
useCase: '标准推理部署、GPU 服务器、大多数生产环境'
},
{
id: 'int8',
label: 'INT8',
bits: 8,
bytes: 1,
color: '#10b981',
size: '~7 GB (7B 模型)',
sizePercent: 25,
speed: '3-4x',
speedPercent: 75,
quality: '~98%',
qualityPercent: 96,
vram: '~8 GB',
vramPercent: 25,
description: 'INT8(8位整数)量化将浮点数映射为整数,体积仅为 FP32 的四分之一。质量损失很小,但推理速度显著提升。适合在消费级 GPU 上运行大模型。',
useCase: '消费级 GPU 部署(RTX 4090)、成本敏感场景'
},
{
id: 'int4',
label: 'INT4',
bits: 4,
bytes: 0.5,
color: '#818cf8',
size: '~3.5 GB (7B 模型)',
sizePercent: 12.5,
speed: '5-6x',
speedPercent: 90,
quality: '~93-95%',
qualityPercent: 90,
vram: '~4 GB',
vramPercent: 12.5,
description: 'INT4(4位整数)是目前最激进的量化方案。模型体积压缩到 FP32 的八分之一,甚至可以在笔记本电脑上运行 7B 模型。质量有一定损失,但对于大多数应用仍然可用。',
useCase: '笔记本/手机端部署、边缘计算、离线场景'
}
]
const currentPrecision = computed(() => precisions[activePrecision.value])
</script>
<style scoped>
.quantization-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 20px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.precision-selector {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.precision-card {
flex: 1;
min-width: 80px;
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 8px;
padding: 12px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.precision-card.active {
border-color: var(--vp-c-brand-1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.prec-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
color: #fff;
font-size: 12px;
font-weight: 700;
margin-bottom: 4px;
}
.prec-bits {
font-size: 11px;
color: var(--vp-c-text-3);
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 20px;
}
@media (max-width: 640px) {
.metrics-grid {
grid-template-columns: 1fr;
}
}
.metric-card {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 14px;
}
.metric-icon {
font-size: 18px;
margin-bottom: 4px;
}
.metric-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.metric-bar-wrap {
height: 6px;
background: var(--vp-c-divider);
border-radius: 3px;
overflow: hidden;
margin-bottom: 6px;
}
.metric-bar {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease;
}
.metric-value {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.detail-section {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--vp-c-divider);
}
.detail-title {
font-size: 15px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 8px;
}
.detail-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 16px;
}
.bit-visual {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
}
.bit-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.bit-row {
display: flex;
gap: 2px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.bit-cell {
width: 20px;
height: 20px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 10px;
font-weight: 600;
font-family: monospace;
}
.bit-info {
font-size: 12px;
color: var(--vp-c-text-3);
}
.use-case {
font-size: 13px;
color: var(--vp-c-text-2);
padding: 8px 12px;
background: var(--vp-c-brand-soft);
border-radius: 6px;
}
.use-label {
font-weight: 600;
color: var(--vp-c-brand-1);
}
</style>
@@ -0,0 +1,332 @@
<template>
<div class="model-serving-demo">
<div class="demo-header">
<h4>模型服务架构演示</h4>
<p class="subtitle">点击不同部署方案对比其特点与适用场景</p>
</div>
<div class="serving-options">
<div
v-for="(opt, i) in options"
:key="opt.id"
class="option-card"
:class="{ active: activeOption === i }"
@click="activeOption = i"
>
<div class="opt-icon">{{ opt.icon }}</div>
<div class="opt-name">{{ opt.name }}</div>
<div class="opt-brief">{{ opt.brief }}</div>
</div>
</div>
<div class="option-detail" v-if="currentOption">
<div class="detail-header">
<span class="detail-icon">{{ currentOption.icon }}</span>
<span class="detail-name">{{ currentOption.name }}</span>
</div>
<p class="detail-desc">{{ currentOption.description }}</p>
<div class="arch-flow">
<div class="flow-label">架构流程</div>
<div class="flow-steps">
<div v-for="(node, i) in currentOption.flow" :key="i" class="flow-node">
<div class="node-box">{{ node }}</div>
<div v-if="i < currentOption.flow.length - 1" class="flow-arrow-h"></div>
</div>
</div>
</div>
<div class="specs-grid">
<div v-for="spec in currentOption.specs" :key="spec.label" class="spec-item">
<div class="spec-label">{{ spec.label }}</div>
<div class="spec-value">{{ spec.value }}</div>
<div class="spec-bar-wrap">
<div class="spec-bar" :style="{ width: spec.score + '%', background: spec.color }"></div>
</div>
</div>
</div>
<div class="tools-section">
<div class="tools-label">常用工具</div>
<div class="tools-list">
<span v-for="tool in currentOption.tools" :key="tool" class="tool-tag">{{ tool }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeOption = ref(0)
const options = [
{
id: 'api',
icon: '🌐',
name: 'API 服务',
brief: '最常见的在线部署方式',
description: '将模型封装为 RESTful API 或 gRPC 服务,通过 HTTP 请求调用。适合需要实时响应的在线应用,如聊天机器人、智能客服、内容生成等。是目前最主流的部署方式。',
flow: ['客户端请求', '负载均衡', '推理服务器', 'GPU 推理', '返回结果'],
specs: [
{ label: '响应延迟', value: '100ms - 2s', score: 70, color: '#10b981' },
{ label: '并发能力', value: '高(可水平扩展)', score: 85, color: '#818cf8' },
{ label: '部署成本', value: '中高(需 GPU 服务器)', score: 50, color: '#f59e0b' },
{ label: '运维复杂度', value: '中等', score: 55, color: '#ef4444' }
],
tools: ['vLLM', 'TGI', 'Triton', 'FastAPI', 'Ollama']
},
{
id: 'edge',
icon: '📱',
name: '边缘部署',
brief: '在终端设备上本地运行',
description: '将量化后的模型部署到手机、笔记本、嵌入式设备等终端上,无需网络连接即可运行。适合隐私敏感、离线场景或需要极低延迟的应用。',
flow: ['模型量化', '格式转换', '设备加载', '本地推理', '即时输出'],
specs: [
{ label: '响应延迟', value: '50ms - 5s', score: 60, color: '#10b981' },
{ label: '并发能力', value: '低(单设备)', score: 20, color: '#818cf8' },
{ label: '部署成本', value: '低(无服务器费用)', score: 90, color: '#f59e0b' },
{ label: '运维复杂度', value: '低', score: 85, color: '#ef4444' }
],
tools: ['llama.cpp', 'MLC LLM', 'ONNX Runtime', 'MediaPipe']
},
{
id: 'batch',
icon: '📦',
name: '批量处理',
brief: '离线批量推理大量数据',
description: '将大量请求收集后统一处理,不要求实时响应。适合数据标注、文档摘要、批量翻译等离线任务。通过批处理可以最大化 GPU 利用率,显著降低单条推理成本。',
flow: ['数据队列', '批量收集', 'GPU 批推理', '结果存储', '异步通知'],
specs: [
{ label: '响应延迟', value: '分钟~小时级', score: 20, color: '#10b981' },
{ label: '吞吐量', value: '极高(批处理优化)', score: 95, color: '#818cf8' },
{ label: '部署成本', value: '低(GPU 利用率高)', score: 85, color: '#f59e0b' },
{ label: '运维复杂度', value: '中等', score: 55, color: '#ef4444' }
],
tools: ['Ray Serve', 'Spark', 'Celery', 'AWS Batch']
}
]
const currentOption = computed(() => options[activeOption.value])
</script>
<style scoped>
.model-serving-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 20px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.serving-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
}
@media (max-width: 640px) {
.serving-options {
grid-template-columns: 1fr;
}
}
.option-card {
background: var(--vp-c-bg);
border: 2px solid var(--vp-c-divider);
border-radius: 10px;
padding: 16px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.option-card.active {
border-color: var(--vp-c-brand-1);
background: var(--vp-c-brand-soft);
}
.opt-icon {
font-size: 28px;
margin-bottom: 6px;
}
.opt-name {
font-size: 14px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 4px;
}
.opt-brief {
font-size: 11px;
color: var(--vp-c-text-3);
}
.option-detail {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--vp-c-divider);
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.detail-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.detail-icon {
font-size: 22px;
}
.detail-name {
font-size: 16px;
font-weight: 600;
color: var(--vp-c-text-1);
}
.detail-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 16px;
}
.arch-flow {
background: var(--vp-c-bg-soft);
border-radius: 8px;
padding: 14px;
margin-bottom: 16px;
}
.flow-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.flow-steps {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
justify-content: center;
}
.flow-node {
display: flex;
align-items: center;
gap: 4px;
}
.node-box {
padding: 6px 12px;
border-radius: 6px;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.flow-arrow-h {
color: var(--vp-c-text-3);
font-size: 16px;
}
.specs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
@media (max-width: 640px) {
.specs-grid {
grid-template-columns: 1fr;
}
}
.spec-item {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 10px 12px;
}
.spec-label {
font-size: 11px;
color: var(--vp-c-text-3);
margin-bottom: 4px;
}
.spec-value {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.spec-bar-wrap {
height: 4px;
background: var(--vp-c-divider);
border-radius: 2px;
overflow: hidden;
}
.spec-bar {
height: 100%;
border-radius: 2px;
transition: width 0.5s ease;
}
.tools-section {
border-top: 1px solid var(--vp-c-divider);
padding-top: 12px;
}
.tools-label {
font-size: 12px;
color: var(--vp-c-text-3);
margin-bottom: 8px;
}
.tools-list {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tool-tag {
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
border: 1px solid var(--vp-c-divider);
}
</style>
@@ -0,0 +1,369 @@
<template>
<div class="training-data-demo">
<div class="demo-header">
<h4>训练数据格式演示</h4>
<p class="subtitle">切换不同格式了解微调数据的组织方式</p>
</div>
<div class="format-tabs">
<button
v-for="fmt in formats"
:key="fmt.id"
class="fmt-btn"
:class="{ active: activeFormat === fmt.id }"
@click="activeFormat = fmt.id"
>
<span class="fmt-icon">{{ fmt.icon }}</span>
<span>{{ fmt.label }}</span>
</button>
</div>
<div class="format-detail">
<div class="format-info">
<div class="info-title">{{ currentFormat.label }}</div>
<p class="info-desc">{{ currentFormat.description }}</p>
<div class="info-tags">
<span class="tag" v-for="tag in currentFormat.tags" :key="tag">{{ tag }}</span>
</div>
</div>
<div class="data-preview">
<div class="preview-header">
<span class="preview-label">数据样例</span>
<button class="switch-btn" @click="nextExample">
换一条
</button>
</div>
<div class="json-block">
<div v-for="(line, i) in currentExample" :key="i" class="json-line">
<span class="json-key" v-if="line.key">{{ line.key }}</span>
<span class="json-colon" v-if="line.key">: </span>
<span :class="'json-value ' + (line.type || '')">{{ line.value }}</span>
</div>
</div>
</div>
<div class="quality-tips">
<div class="tips-title">数据质量要点</div>
<div class="tips-list">
<div v-for="(tip, i) in currentFormat.tips" :key="i" class="tip-item">
<span class="tip-check"></span>
<span>{{ tip }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const activeFormat = ref('instruction')
const exampleIndex = ref(0)
const formats = [
{
id: 'instruction',
icon: '📝',
label: '指令跟随',
description: '最常见的微调数据格式。每条数据包含一个指令(instruction)、可选的输入(input)和期望的输出(output)。适合训练通用助手类模型。',
tags: ['通用助手', 'ChatGPT 风格', '最常用'],
tips: [
'指令要清晰明确,避免歧义',
'输出要完整、准确、格式规范',
'覆盖多种任务类型(翻译、摘要、问答等)',
'数据量建议:1,000 ~ 50,000 条'
],
examples: [
[
{ key: '"instruction"', value: '"请将以下中文翻译成英文"', type: 'string' },
{ key: '"input"', value: '"人工智能正在改变世界"', type: 'string' },
{ key: '"output"', value: '"AI is changing the world"', type: 'string' }
],
[
{ key: '"instruction"', value: '"用一句话总结以下段落"', type: 'string' },
{ key: '"input"', value: '"深度学习是机器学习的一个分支..."', type: 'string' },
{ key: '"output"', value: '"深度学习通过多层神经网络自动学习数据特征"', type: 'string' }
],
[
{ key: '"instruction"', value: '"解释什么是 API"', type: 'string' },
{ key: '"input"', value: '""', type: 'string' },
{ key: '"output"', value: '"API 是应用程序编程接口,它定义了..."', type: 'string' }
]
]
},
{
id: 'conversation',
icon: '💬',
label: '多轮对话',
description: '模拟真实的多轮对话场景。每条数据包含一组对话消息,包括系统提示、用户消息和助手回复。适合训练聊天机器人。',
tags: ['聊天机器人', '多轮交互', '上下文理解'],
tips: [
'对话要自然流畅,符合真实交互模式',
'保持角色一致性(系统提示贯穿始终)',
'包含上下文引用和追问场景',
'数据量建议:5,000 ~ 100,000 条对话'
],
examples: [
[
{ key: '"messages"', value: '[', type: 'bracket' },
{ key: ' {"role"', value: '"system", "content": "你是一个编程助手"}', type: 'string' },
{ key: ' {"role"', value: '"user", "content": "Python 怎么读取文件?"}', type: 'string' },
{ key: ' {"role"', value: '"assistant", "content": "使用 open() 函数..."}', type: 'string' },
{ key: '', value: ']', type: 'bracket' }
],
[
{ key: '"messages"', value: '[', type: 'bracket' },
{ key: ' {"role"', value: '"system", "content": "你是一个医疗顾问"}', type: 'string' },
{ key: ' {"role"', value: '"user", "content": "感冒了怎么办?"}', type: 'string' },
{ key: ' {"role"', value: '"assistant", "content": "建议多休息多喝水..."}', type: 'string' },
{ key: ' {"role"', value: '"user", "content": "需要吃药吗?"}', type: 'string' },
{ key: ' {"role"', value: '"assistant", "content": "如果症状较轻..."}', type: 'string' },
{ key: '', value: ']', type: 'bracket' }
]
]
},
{
id: 'classification',
icon: '🏷️',
label: '分类标注',
description: '用于训练文本分类任务。每条数据包含输入文本和对应的类别标签。适合情感分析、意图识别、内容审核等场景。',
tags: ['情感分析', '意图识别', '内容审核'],
tips: [
'类别标签要统一规范,避免拼写差异',
'各类别样本数量尽量均衡',
'包含边界案例和易混淆样本',
'数据量建议:每个类别至少 100 条'
],
examples: [
[
{ key: '"text"', value: '"这家餐厅的菜品非常好吃,服务也很周到"', type: 'string' },
{ key: '"label"', value: '"positive"', type: 'label' }
],
[
{ key: '"text"', value: '"等了一个小时还没上菜,太失望了"', type: 'string' },
{ key: '"label"', value: '"negative"', type: 'label' }
],
[
{ key: '"text"', value: '"餐厅环境一般,价格中等"', type: 'string' },
{ key: '"label"', value: '"neutral"', type: 'label' }
]
]
}
]
const currentFormat = computed(() => {
return formats.find(f => f.id === activeFormat.value)
})
const currentExample = computed(() => {
const examples = currentFormat.value.examples
return examples[exampleIndex.value % examples.length]
})
function nextExample() {
exampleIndex.value++
}
</script>
<style scoped>
.training-data-demo {
border: 1px solid var(--vp-c-divider);
border-radius: 12px;
padding: 24px;
margin: 16px 0;
background: var(--vp-c-bg-soft);
}
.demo-header h4 {
margin: 0 0 4px;
font-size: 16px;
color: var(--vp-c-text-1);
}
.subtitle {
margin: 0 0 16px;
font-size: 13px;
color: var(--vp-c-text-3);
}
.format-tabs {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.fmt-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.fmt-btn.active {
background: var(--vp-c-brand-1);
color: #fff;
border-color: var(--vp-c-brand-1);
}
.fmt-icon {
font-size: 16px;
}
.format-detail {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.format-info {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.info-title {
font-size: 15px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 6px;
}
.info-desc {
font-size: 13px;
color: var(--vp-c-text-2);
line-height: 1.7;
margin: 0 0 10px;
}
.info-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.tag {
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand-1);
}
.data-preview {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
border: 1px solid var(--vp-c-divider);
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.preview-label {
font-size: 12px;
color: var(--vp-c-text-3);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.switch-btn {
padding: 4px 10px;
border-radius: 4px;
border: 1px solid var(--vp-c-divider);
background: var(--vp-c-bg-soft);
color: var(--vp-c-text-2);
cursor: pointer;
font-size: 12px;
}
.switch-btn:hover {
border-color: var(--vp-c-brand-1);
color: var(--vp-c-brand-1);
}
.json-block {
background: var(--vp-c-bg-soft);
border-radius: 6px;
padding: 12px;
font-family: 'Fira Code', monospace;
}
.json-line {
font-size: 12px;
line-height: 1.8;
}
.json-key {
color: #818cf8;
}
.json-colon {
color: var(--vp-c-text-3);
}
.json-value.string {
color: #10b981;
}
.json-value.label {
color: #f59e0b;
font-weight: 600;
}
.json-value.bracket {
color: var(--vp-c-text-3);
}
.quality-tips {
background: var(--vp-c-bg);
border-radius: 8px;
padding: 16px;
border-left: 3px solid #10b981;
}
.tips-title {
font-size: 13px;
font-weight: 600;
color: var(--vp-c-text-1);
margin-bottom: 10px;
}
.tips-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.tip-item {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: var(--vp-c-text-2);
}
.tip-check {
color: #10b981;
font-weight: 600;
flex-shrink: 0;
}
</style>
@@ -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 RAGAdvanced RAGModular 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>
+140
View File
@@ -120,6 +120,7 @@ import AdderChainDemo from './components/appendix/computer-fundamentals/AdderCha
import CompleteAdderDemo from './components/appendix/computer-fundamentals/CompleteAdderDemo.vue'
import FunctionalUnitDemo from './components/appendix/computer-fundamentals/FunctionalUnitDemo.vue'
import CpuArchitectureDemo from './components/appendix/computer-fundamentals/CpuArchitectureDemo.vue'
import MinCpuDemo from './components/appendix/computer-fundamentals/MinCpuDemo.vue'
import RegisterDemo from './components/appendix/computer-fundamentals/RegisterDemo.vue'
import FlipFlopDemo from './components/appendix/computer-fundamentals/FlipFlopDemo.vue'
// import EvolutionFlowDemo from './components/appendix/computer-fundamentals/EvolutionFlowDemo.vue'
@@ -171,6 +172,26 @@ import GraphStructureDemo from './components/appendix/computer-fundamentals/Grap
import LanguageTypeModelDemo from './components/appendix/computer-fundamentals/LanguageTypeModelDemo.vue'
import CompilationPracticeDemo from './components/appendix/computer-fundamentals/CompilationPracticeDemo.vue'
// Vibe Coding Fullstack Components
import DeveloperSkillShiftDemo from './components/appendix/computer-fundamentals/DeveloperSkillShiftDemo.vue'
import ComputerFieldMapDemo from './components/appendix/computer-fundamentals/ComputerFieldMapDemo.vue'
import FrontendTriadDemo from './components/appendix/computer-fundamentals/FrontendTriadDemo.vue'
import FrontendFrameworkDemo from './components/appendix/computer-fundamentals/FrontendFrameworkDemo.vue'
import BackendCoreDemo from './components/appendix/computer-fundamentals/BackendCoreDemo.vue'
import ProgrammingLanguageMapDemo from './components/appendix/computer-fundamentals/ProgrammingLanguageMapDemo.vue'
import LanguageSelectionDemo from './components/appendix/computer-fundamentals/LanguageSelectionDemo.vue'
import FullstackSkillDemo from './components/appendix/computer-fundamentals/FullstackSkillDemo.vue'
import AIvsTraditionalDemo from './components/appendix/computer-fundamentals/AIvsTraditionalDemo.vue'
import CareerPathDemo from './components/appendix/computer-fundamentals/CareerPathDemo.vue'
import LearningStrategyDemo from './components/appendix/computer-fundamentals/LearningStrategyDemo.vue'
import PowerOnDemo from './components/appendix/computer-fundamentals/PowerOnDemo.vue'
import BootProcessDemo from './components/appendix/computer-fundamentals/BootProcessDemo.vue'
import DesktopDemo from './components/appendix/computer-fundamentals/DesktopDemo.vue'
import BrowserArchitectureDemo from './components/appendix/computer-fundamentals/BrowserArchitectureDemo.vue'
import URLRequestDemo from './components/appendix/computer-fundamentals/URLRequestDemo.vue'
import RenderingDemo from './components/appendix/computer-fundamentals/RenderingDemo.vue'
import FullProcessDemo from './components/appendix/computer-fundamentals/FullProcessDemo.vue'
// Data Encoding Components
import GarbledTextDemo from './components/appendix/data-encoding/GarbledTextDemo.vue'
import CharacterEncodingExplorer from './components/appendix/data-encoding/CharacterEncodingExplorer.vue'
@@ -668,6 +689,55 @@ import ABTestingDemo from './components/appendix/data/ABTestingDemo.vue'
import DataAnalysisDemo from './components/appendix/data/DataAnalysisDemo.vue'
import DataTrackingDemo from './components/appendix/data/DataTrackingDemo.vue'
// RAG Components
import RAGPipelineDemo from './components/appendix/rag/RAGPipelineDemo.vue'
import ChunkingStrategyDemo from './components/appendix/rag/ChunkingStrategyDemo.vue'
import RetrievalDemo from './components/appendix/rag/RetrievalDemo.vue'
import RAGArchitectureDemo from './components/appendix/rag/RAGArchitectureDemo.vue'
import RAGvsFineTuningDemo from './components/appendix/rag/RAGvsFineTuningDemo.vue'
// Embedding & Vector Components
import EmbeddingConceptDemo from './components/appendix/embedding-vector/EmbeddingConceptDemo.vue'
import VectorSimilarityDemo from './components/appendix/embedding-vector/VectorSimilarityDemo.vue'
import VectorIndexDemo from './components/appendix/embedding-vector/VectorIndexDemo.vue'
import VectorDatabaseDemo from './components/appendix/embedding-vector/VectorDatabaseDemo.vue'
import EmbeddingPipelineDemo from './components/appendix/embedding-vector/EmbeddingPipelineDemo.vue'
// AI Native App Components
import AINativeArchDemo from './components/appendix/ai-native-app/AINativeArchDemo.vue'
import AIDesignPrincipleDemo from './components/appendix/ai-native-app/AIDesignPrincipleDemo.vue'
import PromptDesignDemo from './components/appendix/ai-native-app/PromptDesignDemo.vue'
import AIUXPatternDemo from './components/appendix/ai-native-app/AIUXPatternDemo.vue'
import AIAppFlowDemo from './components/appendix/ai-native-app/AIAppFlowDemo.vue'
// Infrastructure as Code Components
import IaCConceptDemo from './components/appendix/infrastructure-as-code/IaCConceptDemo.vue'
import TerraformWorkflowDemo from './components/appendix/infrastructure-as-code/TerraformWorkflowDemo.vue'
import IaCToolComparisonDemo from './components/appendix/infrastructure-as-code/IaCToolComparisonDemo.vue'
import ConfigDriftDemo from './components/appendix/infrastructure-as-code/ConfigDriftDemo.vue'
import IaCBestPracticeDemo from './components/appendix/infrastructure-as-code/IaCBestPracticeDemo.vue'
// DNS & HTTPS Components
import DnsResolutionDemo from './components/appendix/dns-https/DnsResolutionDemo.vue'
import DnsRecordTypeDemo from './components/appendix/dns-https/DnsRecordTypeDemo.vue'
import HttpsHandshakeDemo from './components/appendix/dns-https/HttpsHandshakeDemo.vue'
import CertificateChainDemo from './components/appendix/dns-https/CertificateChainDemo.vue'
import DnsHttpsComparisonDemo from './components/appendix/dns-https/DnsHttpsComparisonDemo.vue'
// Model Finetuning Components
import FinetuningPipelineDemo from './components/appendix/model-finetuning/FinetuningPipelineDemo.vue'
import TrainingDataDemo from './components/appendix/model-finetuning/TrainingDataDemo.vue'
import LoRADemo from './components/appendix/model-finetuning/LoRADemo.vue'
import ModelQuantizationDemo from './components/appendix/model-finetuning/ModelQuantizationDemo.vue'
import ModelServingDemo from './components/appendix/model-finetuning/ModelServingDemo.vue'
// Incident Response Components
import SeverityLevelDemo from './components/appendix/incident-response/SeverityLevelDemo.vue'
import IncidentTimelineDemo from './components/appendix/incident-response/IncidentTimelineDemo.vue'
import IncidentCommandDemo from './components/appendix/incident-response/IncidentCommandDemo.vue'
import AlertEscalationDemo from './components/appendix/incident-response/AlertEscalationDemo.vue'
import PostmortemDemo from './components/appendix/incident-response/PostmortemDemo.vue'
export default {
extends: DefaultTheme,
Layout,
@@ -787,6 +857,7 @@ export default {
app.component('CompleteAdderDemo', CompleteAdderDemo)
app.component('FunctionalUnitDemo', FunctionalUnitDemo)
app.component('CpuArchitectureDemo', CpuArchitectureDemo)
app.component('MinCpuDemo', MinCpuDemo)
app.component('RegisterDemo', RegisterDemo)
app.component('FlipFlopDemo', FlipFlopDemo)
// app.component('EvolutionFlowDemo', EvolutionFlowDemo)
@@ -844,6 +915,26 @@ export default {
app.component('LanguageTypeModelDemo', LanguageTypeModelDemo)
app.component('CompilationPracticeDemo', CompilationPracticeDemo)
// Vibe Coding Fullstack Components Registration
app.component('DeveloperSkillShiftDemo', DeveloperSkillShiftDemo)
app.component('ComputerFieldMapDemo', ComputerFieldMapDemo)
app.component('FrontendTriadDemo', FrontendTriadDemo)
app.component('FrontendFrameworkDemo', FrontendFrameworkDemo)
app.component('BackendCoreDemo', BackendCoreDemo)
app.component('ProgrammingLanguageMapDemo', ProgrammingLanguageMapDemo)
app.component('LanguageSelectionDemo', LanguageSelectionDemo)
app.component('FullstackSkillDemo', FullstackSkillDemo)
app.component('AIvsTraditionalDemo', AIvsTraditionalDemo)
app.component('CareerPathDemo', CareerPathDemo)
app.component('LearningStrategyDemo', LearningStrategyDemo)
app.component('PowerOnDemo', PowerOnDemo)
app.component('BootProcessDemo', BootProcessDemo)
app.component('DesktopDemo', DesktopDemo)
app.component('BrowserArchitectureDemo', BrowserArchitectureDemo)
app.component('URLRequestDemo', URLRequestDemo)
app.component('RenderingDemo', RenderingDemo)
app.component('FullProcessDemo', FullProcessDemo)
// Data Encoding Components Registration
app.component('GarbledTextDemo', GarbledTextDemo)
app.component('CharacterEncodingExplorer', CharacterEncodingExplorer)
@@ -1353,6 +1444,55 @@ export default {
app.component('LicenseComparisonDemo', LicenseComparisonDemo)
app.component('TechRadarDemo', TechRadarDemo)
app.component('DecisionMatrixDemo', DecisionMatrixDemo)
// RAG Components Registration
app.component('RAGPipelineDemo', RAGPipelineDemo)
app.component('ChunkingStrategyDemo', ChunkingStrategyDemo)
app.component('RetrievalDemo', RetrievalDemo)
app.component('RAGArchitectureDemo', RAGArchitectureDemo)
app.component('RAGvsFineTuningDemo', RAGvsFineTuningDemo)
// Embedding & Vector Components Registration
app.component('EmbeddingConceptDemo', EmbeddingConceptDemo)
app.component('VectorSimilarityDemo', VectorSimilarityDemo)
app.component('VectorIndexDemo', VectorIndexDemo)
app.component('VectorDatabaseDemo', VectorDatabaseDemo)
app.component('EmbeddingPipelineDemo', EmbeddingPipelineDemo)
// AI Native App Components Registration
app.component('AINativeArchDemo', AINativeArchDemo)
app.component('AIDesignPrincipleDemo', AIDesignPrincipleDemo)
app.component('PromptDesignDemo', PromptDesignDemo)
app.component('AIUXPatternDemo', AIUXPatternDemo)
app.component('AIAppFlowDemo', AIAppFlowDemo)
// Infrastructure as Code Components Registration
app.component('IaCConceptDemo', IaCConceptDemo)
app.component('TerraformWorkflowDemo', TerraformWorkflowDemo)
app.component('IaCToolComparisonDemo', IaCToolComparisonDemo)
app.component('ConfigDriftDemo', ConfigDriftDemo)
app.component('IaCBestPracticeDemo', IaCBestPracticeDemo)
// DNS & HTTPS Components Registration
app.component('DnsResolutionDemo', DnsResolutionDemo)
app.component('DnsRecordTypeDemo', DnsRecordTypeDemo)
app.component('HttpsHandshakeDemo', HttpsHandshakeDemo)
app.component('CertificateChainDemo', CertificateChainDemo)
app.component('DnsHttpsComparisonDemo', DnsHttpsComparisonDemo)
// Model Finetuning Components Registration
app.component('FinetuningPipelineDemo', FinetuningPipelineDemo)
app.component('TrainingDataDemo', TrainingDataDemo)
app.component('LoRADemo', LoRADemo)
app.component('ModelQuantizationDemo', ModelQuantizationDemo)
app.component('ModelServingDemo', ModelServingDemo)
// Incident Response Components Registration
app.component('SeverityLevelDemo', SeverityLevelDemo)
app.component('IncidentTimelineDemo', IncidentTimelineDemo)
app.component('IncidentCommandDemo', IncidentCommandDemo)
app.component('AlertEscalationDemo', AlertEscalationDemo)
app.component('PostmortemDemo', PostmortemDemo)
},
setup() {
const route = useRoute()
@@ -1,9 +1,24 @@
# 算法思维入门
::: tip 🎯 核心问题
::: tip 前言
**如何高效地解决问题?** 你可能遇到过这样的困惑:同一个问题,有人写的代码跑几秒就出结果,有人写的跑几分钟还在转。差别往往在于算法。本章带你理解算法的核心思维方式。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **问题拆解能力**:面对复杂问题,能想到用分治、递归等策略拆解,而不是一上来就写代码
- **效率判断能力**:用大 O 表示法判断两种解法哪个更高效,而不是凭感觉猜测
- **复杂度思维**:写代码前先估算数据规模和时间要求,选择合适的算法级别
- **后续学习基础**:为高级数据结构、分布式系统、机器学习打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 二分查找 | 分治思想、O(log n) |
| **第 2 章** | 排序算法 | 冒泡、快排、归并 |
| **第 3 章** | 复杂度分析 | 时间复杂度、空间复杂度 |
---
## 0. 全景图:算法是什么?
@@ -16,6 +16,8 @@
**这篇文章会带你学什么?**
学完这章后,你将掌握从输入网址到页面显示的完整技术流程,理解浏览器与服务器如何协同工作。这些知识是后续学习 API、接口、网络安全等技术的基石,也是排查"网页打不开"、"加载慢"等日常问题的关键。
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | URL 解析 | 网址的结构和作用 |
@@ -1,10 +1,23 @@
# 什么是数据的编码与传输?
> 💡 **学习指南**:当你给朋友发一张照片、发一条微信,或者下载一个几 GB 的游戏时,这些信息是怎么穿过大半个地球、完好无损地出现在你的屏幕上的?
>
> 本章节会围绕一个经常困扰新手的问题展开:**为什么我收到的文件变成了乱码?**
>
> 顺着这个问题,我们将彻底揭开计算机底层最核心的三大基石:**编码、存储与传输**
::: tip 前言
当你给朋友发一张照片、发一条微信,或者下载一个几 GB 的游戏时,这些信息是怎么穿过大半个地球、完好无损地出现在你的屏幕上的?本章节会围绕一个经常困扰新手的问题展开:**为什么我收到的文件变成了乱码?** 顺着这个问题,我们将彻底揭开计算机底层最核心的三大基石:**编码、存储与传输**。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **乱码排查能力**:遇到"文件打开是乱码"时,能从编码角度分析原因,而不是简单认为"文件坏了"
- **跨平台意识**:处理数据交换时,知道为什么要关注编码格式和字节序
- **编码世界观**:理解计算机如何用 0 和 1 表示世间万物——从文字到图像到复杂对象
- **后续学习基础**:为网络协议、文件格式、序列化技术打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 字符编码 | ASCII、UTF-8、GBK |
| **第 2 章** | 数据存储 | 二进制、字节序 |
| **第 3 章** | 数据传输 | 序列化、压缩 |
在开始之前,我们需要先明确一个经常被新手忽略的物理事实:
@@ -1,9 +1,25 @@
# 数据结构
::: tip 🎯 核心问题
::: tip 前言
**如何高效地组织和存储数据?** 你可能遇到过这样的困惑:为什么有些程序处理几万条数据很快,有些处理几百条就卡住了?答案往往在于数据结构的选择。本章带你理解常见数据结构的特点和适用场景。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **选型决策能力**:知道什么时候用数组快速访问,什么时候用链表灵活插入
- **性能分析视角**:能判断性能问题是数据结构选择不当,还是算法效率低下
- **权衡思维**:理解"空间换时间"与"时间换空间",知道没有完美的数据结构
- **后续学习基础**:为数据库、缓存系统、搜索引擎等技术打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 线性结构 | 数组、链表、栈、队列 |
| **第 2 章** | 哈希结构 | 哈希表、冲突处理 |
| **第 3 章** | 树形结构 | 二叉树、B树、堆 |
| **第 4 章** | 图结构 | 有向图、无向图、遍历算法 |
---
## 0. 全景图:数据结构是什么?
@@ -1,12 +1,27 @@
# 操作系统:给电脑请个"大管家"
::: tip 🎯 核心问题
::: tip 前言
**有了完美的 CPU 和无限的内存,电脑就能直接用了吗?**
在上一章,我们见证了晶体管如何组合成强大的 CPU。但即使你拥有最顶级的硬件,如果直接让它们工作,连在屏幕上显示一个字母都需要写几百行晦涩的机器指令。不仅麻烦,还极其危险——稍有差池,你的代码就可能把别人的数据覆盖掉。
为了解决这些噩梦,**操作系统(Operating System, 简称 OS)**诞生了。它是挡在你和冰冷硬件之间的一层最伟大的"软件"。本章我们将抛开深奥的代码,用通俗的比喻,看看这个"超级管家"是如何把杂乱无章的硬件调教得服服帖帖的。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **问题排查能力**:遇到"程序卡死"、"内存不足"时,能从操作系统层面分析原因
- **术语理解深度**:理解"多进程"、"虚拟内存"、"文件权限"解决的是什么问题
- **系统观思维**:理解程序不是孤立运行的,而是与操作系统、其他进程、硬件资源密切交互
- **后续学习基础**:为并发编程、系统调优、容器技术打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 进程管理 | CPU 时分复用、时间片轮转 |
| **第 2 章** | 内存管理 | 虚拟内存、分页机制 |
| **第 3 章** | 文件系统 | 文件组织、目录结构 |
---
## 0. 全景图:没有操作系统会怎样?
@@ -0,0 +1,452 @@
# 从按下电源到访问网站发生了什么
::: tip 前言
你有没有想过,当你按下电脑电源键,到最终在浏览器中看到网页,这中间到底发生了什么?
这个过程涉及**硬件启动**、**操作系统加载**、**网络通信**等多个环节。理解这个过程,能帮助你建立对计算机系统的整体认知,也是成为全栈工程师的必经之路。
:::
**你会学到什么?**
- 电脑从通电到显示桌面的完整过程
- 操作系统是如何启动的
- 浏览器是如何工作的
- 当你访问一个 URL 时,网络请求是如何完成的
---
## 1. 按下电源:硬件的觉醒
### 1.1 电源启动
当你按下电源键,**电源单元(PSU)** 开始工作,把交流电(220V)转换成直流电(12V、5V、3.3V 等),为各个硬件部件供电。
```
电源按钮 → 电源单元(PSU) → 直流电输出 → 供给主板各部件
```
### 1.2 主板芯片组唤醒
电源稳定后,**主板芯片组**开始工作,它就像电脑的"总调度员",负责协调各个硬件部件。
### 1.3 CPU 复位
CPU 接收到复位信号后,把内部所有寄存器和缓存清零,从一个预设的地址开始执行指令。这个地址通常指向 **BIOS/UEFI** 芯片。
<PowerOnDemo />
---
## 2. BIOS/UEFI:硬件的自检
### 2.1 什么是 BIOS/UEFI
**BIOSBasic Input/Output System** 是电脑启动后第一个运行的程序,存储在主板的一个**只读芯片**中。
**UEFIUnified Extensible Firmware Interface** 是 BIOS 的升级版,更安全、更现代。现在的电脑大多使用 UEFI。
### 2.2 BIOS/UEFI 做了什么?
1. **硬件自检(POST**:检查内存、显卡、键盘等部件是否正常
2. **初始化硬件**:设置硬件工作模式
3. **启动顺序**:按照设定顺序,尝试从硬盘/U 盘/网络启动
```
BIOS/UEFI 工作流程:
┌─────────────────────────────────────┐
│ 1. 硬件自检 (POST) │
│ - 检查内存是否正常 │
│ - 检查显卡是否正常 │
│ - 检查键盘/鼠标是否正常 │
├─────────────────────────────────────┤
│ 2. 初始化硬件 │
│ - 设置硬件工作模式 │
│ - 配置中断向量表 │
├─────────────────────────────────────┤
│ 3. 寻找启动设备 │
│ - 按启动顺序查找可启动设备 │
│ - 读取启动扇区 │
└─────────────────────────────────────┘
```
如果发现问题,主板会发出**蜂鸣声**(不同次数代表不同错误)。
### 2.3 启动顺序
BIOS/UEFI 会按照设定的**启动顺序**查找启动设备:
1. 硬盘(最常见)
2. U 盘/光盘(重装系统时用)
3. 网络( PXE 启动,企业批量部署用)
找到第一个可启动设备后,读取它的**启动扇区(Boot Sector)**,把控制权交给操作系统。
---
## 3. 操作系统启动:从内核到桌面
### 3.1 什么是操作系统?
**操作系统(Operating System,简称 OS)** 是管理计算机硬件和软件资源的程序集合。它就像一个"大管家",帮我们管理内存、CPU、文件等资源,让我们不需要直接和硬件打交道。
常见的操作系统:
| 操作系统 | 特点 | 典型设备 |
|---------|------|---------|
| **Windows** | 生态丰富,兼容性好 | 桌面电脑、笔记本 |
| **macOS** | 苹果生态,流畅稳定 | Mac 电脑 |
| **Linux** | 开源免费,服务器首选 | 服务器、嵌入式设备 |
| **Android** | 移动端 Linux | 手机、平板 |
| **iOS** | 苹果移动端 | iPhone、iPad |
### 3.2 操作系统的启动过程
当你从硬盘启动时,操作系统的启动过程如下:
<BootProcessDemo />
#### 第一步:引导程序(Bootloader)
硬盘的第一个扇区存放着**引导程序(Bootloader)**,它的任务是把操作系统内核加载到内存中。
- **Windows**Bootloader 叫 `bootmgr`
- **Linux**:常见的引导程序有 `GRUB``rEFInd`
```
引导程序工作流程:
┌─────────────────────────────────────┐
│ 1. 读取硬盘分区表 │
│ 2. 找到操作系统分区 │
│ 3. 加载操作系统内核到内存 │
│ 4. 跳转到内核入口点 │
└─────────────────────────────────────┘
```
#### 第二步:内核加载(Kernel)
操作系统**内核(Kernel)** 是操作系统的核心,负责管理内存、CPU、进程等核心功能。
```
内核的主要功能:
┌─────────────────────────────────────┐
│ • 进程管理 - 创建/调度进程 │
│ • 内存管理 - 分配/回收内存 │
│ • 文件系统 - 管理文件存储 │
│ • 设备驱动 - 控制硬件设备 │
│ • 网络通信 - 处理网络协议 │
└─────────────────────────────────────┘
```
#### 第三步:系统服务启动
内核加载后,会启动各种**系统服务**:
- **Windows 服务**:更新服务、安全中心、打印机服务
- **Linux 服务**:SSH 服务、网络服务、图形界面(GNOME、KDE)
```
Windows 启动过程:
BIOS → MBR → bootmgr → winload.exe → ntoskrnl.exe → 系统服务 → 桌面
Linux 启动过程:
BIOS → GRUB → vmlinuz (内核) → systemd → 系统服务 → 桌面环境
```
#### 第四步:显示桌面
最后,操作系统启动**图形界面(GUI)**,显示桌面:
- **Windows**explorer.exe(资源管理器)显示桌面
- **Linux**GNOME、KDE、XFCE 等桌面环境
- **macOS**Finder 显示桌面
```
桌面出现的过程:
┌─────────────────────────────────────┐
│ 1. 显卡驱动加载 │
│ 2. 显示服务器启动 │
│ (Windows: Desktop Window Manager)│
│ (Linux: X Server / Wayland) │
│ 3. 桌面环境启动 │
│ 4. 显示桌面背景和图标 │
└─────────────────────────────────────┘
```
<DesktopDemo />
---
## 4. 打开浏览器:应用程序的启动
### 4.1 应用程序的启动过程
当你双击浏览器图标时,操作系统会:
1. **查找可执行文件**:根据文件关联,找到浏览器的 `.exe`Windows)或可执行文件
2. **创建进程**:为浏览器创建一个新的**进程**
3. **加载程序**:把浏览器的代码从硬盘加载到内存
4. **初始化**:启动浏览器的主线程、渲染引擎、网络引擎等
```
浏览器启动过程:
┌─────────────────────────────────────┐
│ 1. 双击图标 │
│ 2. 操作系统查找浏览器可执行文件 │
│ 3. 创建浏览器进程 │
│ 4. 加载浏览器代码到内存 │
│ 5. 初始化各模块(渲染、网络、JS) │
│ 6. 显示浏览器窗口 │
└─────────────────────────────────────┘
```
### 4.2 浏览器的主要组成部分
现代浏览器是一个复杂的"操作系统",主要由以下部分组成:
| 模块 | 功能 |
|-----|------|
| **用户界面** | 地址栏、标签页、书签等 |
| **浏览器引擎** | 协调 UI 和渲染引擎 |
| **渲染引擎** | 解析 HTML/CSS,显示网页 |
| **JavaScript 引擎** | 执行 JavaScript 代码 |
| **网络模块** | 发送 HTTP 请求 |
| **UI 后端** | 绘制基础 UI 组件 |
| **数据存储** | Cookie、LocalStorage 等 |
<BrowserArchitectureDemo />
---
## 5. 访问 URL:网络请求的全过程
### 5.1 什么是 URL
**URLUniform Resource Locator** 是资源的地址,就像生活中的地址一样,用来定位互联网上的资源。
```
URL 的结构:
┌─────────────────────────────────────────────────────────┐
│ https:// │ www.example.com │ /path/to/page │ ?query=1 │
│ 协议 │ 域名 │ 路径 │ 查询 │
└─────────────────────────────────────────────────────────┘
```
- **协议(Protocol**:用什么方式访问(http、https、ftp 等)
- **域名(Domain**:服务器的地址
- **路径(Path**:资源在服务器上的位置
- **查询(Query**:额外的参数
### 5.2 访问 URL 的完整过程
当你访问 `https://www.example.com` 时,发生了这些事情:
<URLRequestDemo />
#### 第一步:URL 解析
浏览器首先**解析 URL**,提取出协议、域名、路径等信息。
```
URL 解析过程:
https://www.example.com/index.html
协议: https
域名: www.example.com
路径: /index.html
```
#### 第二步:DNS 解析
计算机通过网络访问服务器,但网络用的是 **IP 地址**(如 93.184.216.34),而不是域名。所以需要把域名转换成 IP 地址,这个过程叫 **DNS 解析**
```
DNS 解析流程:
┌─────────────────────────────────────────────────────────┐
│ 浏览器缓存 → hosts 文件 → 本地 DNS 缓存 → DNS 服务器 │
└─────────────────────────────────────────────────────────┘
实际过程:
1. 浏览器检查缓存(最近访问过吗?)
2. 操作系统检查 DNS 缓存
3. 向 DNS 服务器发送查询请求
4. DNS 服务器返回 IP 地址
```
#### 第三步:建立 TCP 连接
拿到 IP 地址后,浏览器要与服务器建立 **TCP 连接**。TCP 是传输层协议,保证数据可靠传输。
```
TCP 三次握手:
┌─────────────────────────────────────────────────────────┐
│ 客户端 → 服务器:SYN(同步请求) │
│ 服务器 → 客户端:SYN-ACK(确认并同步) │
│ 客户端 → 服务器:ACK(确认) │
│ ↓ │
│ 连接建立完成! │
└─────────────────────────────────────────────────────────┘
```
如果是 **HTTPS**,还需要进行 **TLS/SSL 握手**,建立加密通道。
#### 第四步:发送 HTTP 请求
连接建立后,浏览器向服务器发送 **HTTP 请求**
```
HTTP 请求格式:
┌─────────────────────────────────────────────────────────┐
│ GET /index.html HTTP/1.1 │
│ Host: www.example.com │
│ User-Agent: Mozilla/5.0... │
│ Accept: text/html │
│ │
│ (空行) │
└─────────────────────────────────────────────────────────┘
```
常见的 HTTP 方法:
| 方法 | 含义 | 用途 |
|-----|------|-----|
| **GET** | 获取资源 | 浏览网页 |
| **POST** | 提交数据 | 登录、提交表单 |
| **PUT** | 上传资源 | 文件上传 |
| **DELETE** | 删除资源 | 删除数据 |
#### 第五步:服务器处理请求
服务器(通常是 **Web 服务器** 如 Nginx、Apache)收到请求后:
1. **解析请求**:理解客户端想要什么
2. **处理业务**:调用后端程序(如 Python、Node.js、Java
3. **查询数据库**:获取需要的数据
4. **生成响应**:把数据组装成 HTML、JSON 等格式
```
服务器处理流程:
┌─────────────────────────────────────────────────────────┐
│ 1. Web 服务器接收请求 (Nginx/Apache) │
│ 2. 根据路径找到对应的处理程序 │
│ 3. 执行后端代码 (API、业务逻辑) │
│ 4. 如需查询数据库,获取数据 │
│ 5. 组装响应 (HTML/JSON/CSS/JS) │
│ 6. 返回 HTTP 响应 │
└─────────────────────────────────────────────────────────┘
```
#### 第六步:返回 HTTP 响应
服务器返回 **HTTP 响应**,包含状态码、响应头和响应体:
```
HTTP 响应格式:
┌─────────────────────────────────────────────────────────┐
│ HTTP/1.1 200 OK │
│ Content-Type: text/html │
│ Content-Length: 1234 │
│ │
│ <!DOCTYPE html> │
│ <html>...</html> │
└─────────────────────────────────────────────────────────┘
```
常见的状态码:
| 状态码 | 含义 |
|-------|------|
| **200** | 成功 |
| **301/302** | 重定向 |
| **404** | 资源未找到 |
| **500** | 服务器错误 |
#### 第七步:浏览器渲染页面
浏览器收到响应后,开始**渲染页面**:
<RenderingDemo />
1. **解析 HTML**:构建 DOM 树
2. **解析 CSS**:计算样式,构建渲染树
3. **执行 JavaScript**:执行页面中的 JS 代码
4. **绘制页面**:把内容显示到屏幕上
```
浏览器渲染过程:
┌─────────────────────────────────────────────────────────┐
│ 1. HTML 解析 → DOM 树 │
│ 2. CSS 解析 → 样式规则 │
│ 3. DOM + CSS → 渲染树 │
│ 4. 布局计算 → 每个元素的大小位置 │
│ 5. 绘制 → 像素显示到屏幕 │
│ 6. 合成 → 多层合并显示 │
└─────────────────────────────────────────────────────────┘
```
---
## 6. 完整流程回顾
让我们把整个过程串起来:
<FullProcessDemo />
```
从按下电源到访问网站的完整流程:
┌──────────────────────────────────────────────────────────────────┐
│ 1. 按下电源 │
│ └── 电源启动 → 主板唤醒 → CPU 复位 → 执行 BIOS/UEFI │
├──────────────────────────────────────────────────────────────────┤
│ 2. BIOS/UEFI 启动 │
│ └── 硬件自检 → 寻找启动设备 → 读取引导程序 │
├──────────────────────────────────────────────────────────────────┤
│ 3. 操作系统启动 │
│ └── 引导程序 → 加载内核 → 启动服务 → 显示桌面 │
├──────────────────────────────────────────────────────────────────┤
│ 4. 打开浏览器 │
│ └── 双击图标 → 创建进程 → 加载程序 → 显示窗口 │
├──────────────────────────────────────────────────────────────────┤
│ 5. 访问 URL │
│ └── URL 解析 → DNS 解析 → TCP 连接 → HTTP 请求 │
│ → 服务器处理 → HTTP 响应 → 浏览器渲染 → 显示网页 │
└──────────────────────────────────────────────────────────────────┘
```
---
## 7. 知识地图
这一章涉及的知识领域:
```
计算机系统概览
├── 硬件基础
│ ├── 电源 (PSU)
│ ├── 主板芯片组
│ └── CPU
├── BIOS/UEFI
│ ├── POST 自检
│ ├── 启动顺序
│ └── 引导程序
├── 操作系统
│ ├── 内核 (Kernel)
│ ├── 系统服务
│ └── 桌面环境
├── 应用程序
│ ├── 进程管理
│ └── 程序加载
└── 网络通信
├── DNS 解析
├── TCP/IP 协议
├── HTTP 协议
└── 浏览器渲染
```
::: tip 继续学习
如果你想深入了解某个环节,可以继续学习:
- **从晶体管到 CPU**:了解计算机硬件基础
- **操作系统(进程/内存/文件系统)**:深入理解操作系统
- **计算机网络**:深入理解网络协议
:::
@@ -1,6 +1,23 @@
# 编程语言图谱
> 💡 **学习指南**:为什么有这么多编程语言?该学哪个?本章带你从"语言演化"到"编程范式"到"如何选择",建立对编程语言全景的理解。**结论先行:没有最好的语言,只有最适合场景的语言。**
::: tip 前言
为什么有这么多编程语言?该学哪个?本章带你从"语言演化"到"编程范式"到"如何选择",建立对编程语言全景的理解。**结论先行:没有最好的语言,只有最适合场景的语言。**
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **理性选型能力**:面对"学什么语言"时,能根据项目需求做出判断,而不是盲目跟风
- **范式理解深度**:理解"面向对象"、"函数式编程"是不同的思维方式,而不仅仅是语法差异
- **历史演进视角**:看到 70 多年语言演化——从手写 0 和 1 到自然语言生成代码
- **后续学习基础**:为理解新语言设计理念、技术选型决策打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 语言演化 | 从机器语言到高级语言 |
| **第 2 章** | 编程范式 | 命令式、面向对象、函数式 |
| **第 3 章** | 语言选择 | 场景驱动的选型方法 |
---
@@ -1,9 +1,25 @@
# 从晶体管到 CPU
::: tip 核心问题
::: tip 前言
**计算机是怎么"思考"的?** 你可能知道 CPU 是电脑的"大脑",但这个大脑到底是怎么工作的?它怎么从一堆金属和塑料变成能执行程序、处理数据的智能设备?本章带你从最底层的晶体管开始,一步步理解 CPU 的构造原理。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **术语理解能力**:听到"CPU 主频"、"多核"、"指令集"不再一头雾水,能理解背后的物理原理
- **代码执行视角**:看到一行代码如何经过取指、解码、执行、写回,最终变成屏幕上的像素点
- **抽象层次思维**:理解每一层如何向上层提供服务,又如何隐藏下层的复杂性
- **后续学习基础**:为计算机体系结构、嵌入式开发、性能优化打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 晶体管 | 数字世界的开关 |
| **第 2 章** | 逻辑门 | 布尔运算的物理实现 |
| **第 3 章** | 功能单元 | 加法器、寄存器、多路选择器 |
| **第 4 章** | CPU 核心 | 取指、解码、执行、写回 |
---
## 0. 全景图:从沙子到智能
@@ -201,6 +217,8 @@
- **内部总线 (Internal Bus)**:系统里的传送带,负责在各个模块之间搬运数据和信号。
- **控制单元 (Control Unit)**:总指挥。它的使命就是从内存中读取用 0 和 1 组成的指令,解析出应该做什么,并向其他模块传达具体的控制信号,调度它们各司其职。
<MinCpuDemo />
### 4.2 CPU 是如何执行指令的?
不管写下的高级编程语言有多么复杂,最终都会变成内存中的一条条底层指令。CPU 执行任何指令的过程,本质上都在重复以下典型的四个步骤:
@@ -1,6 +1,22 @@
# 类型系统与编译原理入门
> 💡 **学习指南**:当你写下 `int x = 10 + 5;` 时,编译器是如何理解每个字符、检查类型是否正确、最终生成机器指令的?本章用两个核心概念——**类型系统**和**编译流程**——帮你理解编程语言背后的"翻译机制"。
::: tip 前言
当你写下 `int x = 10 + 5;` 时,编译器是如何理解每个字符、检查类型是否正确、最终生成机器指令的?本章用两个核心概念——**类型系统**和**编译流程**——帮你理解编程语言背后的"翻译机制"。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **问题诊断能力**:看到 `TypeError` 报错时,能快速定位是类型不匹配还是隐式转换惹的祸
- **语言选择依据**:理解为什么 TypeScript 适合大型项目、Python 适合快速原型开发
- **类型安全思维**:在写代码时就预见可能的类型错误,而不是等到运行时才发现
- **后续学习基础**:为编译原理、语言设计等高级话题打下基础
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 类型系统 | 静态/动态类型、强/弱类型 |
| **第 2 章** | 编译流程 | 词法分析、语法分析、代码生成 |
---
@@ -0,0 +1,413 @@
# Vibe Coding 时代下的全栈开发
::: tip 前言
**什么是 Vibe Coding** 简单说,就是"用自然语言写代码"——你用中文或英文描述想要什么,AI 帮你生成代码。这彻底改变了软件开发的游戏规则。
但这里有个关键问题:**AI 能帮你写代码,但 AI 不能替你思考。** 你仍然需要知道"要写什么"、"为什么这么写"、"怎么判断对错"。这正是本章要帮你建立的基础认知框架。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **领域全景认知**:知道前端、后端、AI 算法等方向分别做什么
- **技术选型能力**:面对"学什么语言/框架"时,能做出理性判断
- **成长路径清晰**:了解从零基础到 3-5 年经验工程师的技能演进
- **Vibe Coding 思维**:理解在 AI 辅助时代,哪些能力变得更重要
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 计算机领域全景 | 前端、后端、移动端、AI、运维 |
| **第 2 章** | 什么是前端 | 用户能感知的界面层 |
| **第 3 章** | 什么是后端 | 幕后的服务器逻辑 |
| **第 4 章** | 编程语言图谱 | 与计算机沟通的工具 |
| **第 5 章** | 全栈工程师 | 前后端通吃的多面手 |
| **第 6 章** | AI 算法工程师 | 让机器学会思考 |
| **第 7 章** | 成长路径 | 从入门到精通的路线图 |
---
## 0. Vibe Coding:软件开发的新范式
### 0.1 什么是 Vibe Coding
想象一下以前的软件开发:
```
传统开发流程:
你 → 学习语法 → 写代码 → 调试 → 查文档 → 修改 → 运行
↑___________________反复循环___________________↓
```
现在有了 AI 辅助:
```
Vibe Coding 流程:
你 → 用自然语言描述需求 → AI 生成代码 → 你审核修改 → 运行
↑____________快速迭代____________↓
```
**核心变化**:从"怎么写代码"变成"怎么描述需求"。
### 0.2 Vibe Coding 时代,什么能力更重要?
<DeveloperSkillShiftDemo />
::: tip 💡 关键洞察
AI 能帮你写代码,但以下能力 AI 替代不了:
- **判断力**:知道 AI 生成的代码对不对、好不好
- **架构思维**:知道系统该怎么设计、模块该怎么划分
- **领域知识**:理解业务逻辑,知道"要做什么"
- **调试能力**:出问题时知道从哪里排查
:::
---
## 1. 计算机领域全景图
在深入各个方向之前,先建立一个全局认知。
<ComputerFieldMapDemo />
### 1.1 用"餐厅"比喻理解各领域
把一个软件系统想象成一家**餐厅**
| 领域 | 餐厅角色 | 做什么 | 产出物 |
|-----|---------|--------|--------|
| **前端** | 装修 + 菜单 + 服务员 | 用户能看到、能交互的一切 | 网页、小程序、App 界面 |
| **后端** | 厨房 + 仓库 | 处理业务逻辑、存储数据 | API、数据库、服务器程序 |
| **移动端** | 外卖窗口 | 手机上的应用体验 | iOS/Android App |
| **AI/算法** | 研发部 | 让系统变"聪明" | 推荐模型、图像识别、智能对话 |
| **运维/DevOps** | 物业 + 安保 | 保证系统稳定运行 | 部署脚本、监控系统、安全防护 |
| **数据工程** | 财务 + 分析师 | 数据采集、存储、分析 | 数据管道、报表、仪表盘 |
### 1.2 各领域的技术栈速览
不要被这些名词吓到,这里只是让你"见过"它们:
| 领域 | 核心语言 | 常用框架/工具 | 典型产出 |
|-----|---------|--------------|---------|
| 前端 | JavaScript, TypeScript | React, Vue, CSS | 网页、管理后台 |
| 后端 | Node.js, Go, Java, Python | Express, Gin, Spring | API 服务 |
| 移动端 | Swift, Kotlin, Dart | SwiftUI, Jetpack, Flutter | 手机 App |
| AI/算法 | Python | PyTorch, TensorFlow | 模型、算法 |
| 运维 | Shell, Python | Docker, Kubernetes | 部署方案 |
::: tip 💡 给新手的建议
不要试图一次学完所有东西。先选一个方向深入,建立"根据地",再横向扩展。全栈不是"什么都懂一点",而是"有一个核心强项,其他方向能用"。
:::
---
## 2. 什么是前端?
### 2.1 一句话定义
**前端 = 用户能直接看到、点击、交互的部分。**
当你打开一个网页:
- 页面的布局、颜色、字体 → 前端
- 点击按钮后的动画效果 → 前端
- 表单输入、数据展示 → 前端
- 页面怎么适配手机屏幕 → 前端
### 2.2 前端三件套
<FrontendTriadDemo />
**用"装修房子"来比喻**
| 技术 | 装修角色 | 职责 |
|-----|---------|------|
| **HTML** | 房屋结构 | 墙在哪、门在哪、房间怎么划分 |
| **CSS** | 装饰风格 | 墙什么颜色、家具怎么摆、灯光效果 |
| **JavaScript** | 智能家居 | 开关灯、窗帘自动开合、安防系统 |
### 2.3 前端框架:为什么要用?
原生 HTML/CSS/JS 能写网页,为什么还要学 React、Vue 这些框架?
<FrontendFrameworkDemo />
**核心原因**:当页面变得复杂(比如淘宝、微信网页版),直接操作 DOM 会变得非常混乱。框架帮你"管理复杂性"。
### 2.4 前端工程师的一天
```
9:00 查看设计稿,理解要做什么功能
10:00 用 React/Vue 写组件代码
12:00 午休
14:00 和后端对接 API,调试数据展示
16:00 修复 bug,优化页面性能
18:00 代码评审,和团队讨论技术方案
```
---
## 3. 什么是后端?
### 3.1 一句话定义
**后端 = 用户看不到,但支撑整个系统运转的逻辑。**
当你网购下单:
- 验证你的账号密码 → 后端
- 检查商品库存 → 后端
- 计算优惠价格 → 后端
- 生成订单、扣款 → 后端
- 通知仓库发货 → 后端
### 3.2 后端的核心职责
<BackendCoreDemo />
**用"餐厅厨房"来比喻**
| 后端职责 | 厨房类比 | 具体内容 |
|---------|---------|---------|
| **API 设计** | 菜单设计 | 定义"用户能点什么菜"、"怎么点" |
| **业务逻辑** | 烹饪过程 | 处理订单、计算价格、验证权限 |
| **数据存储** | 仓库管理 | 把数据存进数据库、查询数据 |
| **性能优化** | 厨房效率 | 缓存、异步处理、负载均衡 |
| **安全防护** | 食品安全 | 防止 SQL 注入、权限控制 |
### 3.3 后端语言怎么选?
| 语言 | 特点 | 适合场景 |
|-----|------|---------|
| **Node.js** | 前端友好,JavaScript 全栈 | 中小型项目、快速原型 |
| **Go** | 高性能、并发强 | 高并发服务、微服务架构 |
| **Java** | 生态成熟、企业级 | 大型企业系统、银行 |
| **Python** | 简洁、AI 生态好 | 数据处理、AI 服务 |
::: tip 💡 新手建议
如果你已经会 JavaScript(前端基础),Node.js 是最自然的后端入门选择。一套语言,前后端都能写。
:::
### 3.4 后端工程师的一天
```
9:00 查看 API 需求文档
10:00 设计数据库表结构
11:00 写 API 接口代码
14:00 和前端联调,修复接口问题
16:00 优化慢查询,处理线上问题
18:00 代码评审,写技术文档
```
---
## 4. 编程语言图谱
### 4.1 编程语言是什么?
**编程语言 = 人类和计算机沟通的桥梁。**
计算机只认识 0 和 1,人类习惯说自然语言。编程语言是中间层:
- 人类用编程语言写代码(比 0/1 好理解)
- 计算机把编程语言翻译成机器指令
### 4.2 语言分类
<ProgrammingLanguageMapDemo />
**按运行方式分类**
| 类型 | 原理 | 代表语言 | 特点 |
|-----|------|---------|------|
| **编译型** | 先翻译成机器码,再运行 | C, C++, Go, Rust | 运行快,编译慢 |
| **解释型** | 边翻译边运行 | Python, JavaScript, Ruby | 开发快,运行慢 |
| **字节码型** | 折中方案 | Java, Kotlin, C# | 平衡性能和开发效率 |
**按类型系统分类**
| 类型 | 特点 | 代表语言 |
|-----|------|---------|
| **静态类型** | 变量类型写代码时确定 | Java, TypeScript, Go |
| **动态类型** | 变量类型运行时确定 | Python, JavaScript, Ruby |
| **强类型** | 类型检查严格,不自动转换 | Python, Java |
| **弱类型** | 类型检查宽松,会自动转换 | JavaScript, PHP |
### 4.3 该学哪门语言?
<LanguageSelectionDemo />
::: tip 💡 选择原则
没有"最好的语言",只有"最适合场景的语言"。新手建议:
1. **先学一门,学深**:建立编程思维
2. **再学第二门,对比**:理解语言设计差异
3. **按需学习**:根据项目需求选择
:::
---
## 5. 全栈工程师:前后端通吃
### 5.1 什么是全栈?
**全栈工程师 = 能独立完成前端 + 后端开发的工程师。**
<FullstackSkillDemo />
### 5.2 全栈的优势
| 优势 | 说明 |
|-----|------|
| **独立完成项目** | 从需求到上线,一个人搞定 |
| **沟通成本低** | 不需要前后端来回扯皮 |
| **技术视野广** | 理解整个系统如何运作 |
| **创业友好** | 快速验证想法,MVP 开发 |
### 5.3 全栈的挑战
| 挑战 | 说明 |
|-----|------|
| **深度 vs 广度** | 容易"什么都懂一点,什么都不精" |
| **技术更新快** | 前后端技术都在快速演进 |
| **精力分散** | 需要同时关注多个领域 |
### 5.4 全栈成长建议
```
第 1 阶段:建立根据地
└── 选一个方向深入(建议从前端或后端开始)
└── 达到能独立完成项目的水平
第 2 阶段:横向扩展
└── 学习另一个方向的基础
└── 能完成简单的全栈项目
第 3 阶段:融会贯通
└── 理解前后端如何协作
└── 能设计完整的技术架构
第 4 阶段:持续精进
└── 在某个领域保持深度
└── 其他领域保持"能用"水平
```
---
## 6. AI 算法工程师:让机器学会思考
### 6.1 AI 工程师 vs 传统开发
<AIvsTraditionalDemo />
| 维度 | 传统开发 | AI 算法工程师 |
|-----|---------|--------------|
| **核心任务** | 实现确定性的业务逻辑 | 训练模型、优化算法 |
| **思维方式** | "如果 A 则执行 B" | "让机器从数据中学习规律" |
| **代码产出** | 功能模块、系统 | 模型、训练脚本 |
| **调试方式** | 断点、日志 | 看指标、调超参 |
| **成功标准** | 功能正确、无 bug | 准确率、召回率达标 |
### 6.2 AI 工程师的技能树
```
AI 算法工程师
├── 数学基础
│ ├── 线性代数(矩阵运算)
│ ├── 概率统计(分布、期望)
│ └── 微积分(梯度、优化)
├── 编程能力
│ ├── Python(主力语言)
│ ├── PyTorch / TensorFlow(深度学习框架)
│ └── 数据处理(Pandas, NumPy
├── 机器学习
│ ├── 监督学习(分类、回归)
│ ├── 无监督学习(聚类、降维)
│ └── 模型评估方法
└── 深度学习
├── 神经网络基础
├── CNN(图像)
├── RNN / Transformer(序列)
└── 大模型(LLM)
```
### 6.3 AI 工程师的一天
```
9:00 查看模型训练结果,分析指标
10:00 数据预处理,清洗训练数据
12:00 午休
14:00 调整模型结构,尝试新方案
16:00 跑实验,对比不同方案效果
18:00 写实验报告,和团队讨论下一步
```
### 6.4 Vibe Coding 时代的 AI 工程师
AI 辅助开发对 AI 工程师的影响:
| 变化 | 说明 |
|-----|------|
| **代码生成** | AI 能生成训练脚本、数据处理代码 |
| **论文阅读** | AI 能帮你总结论文要点 |
| **实验记录** | AI 能帮你整理实验结果 |
| **不变的是** | 对问题的理解、对结果的判断、对方向的把握 |
---
## 7. 成长路径:从入门到精通
### 7.1 3-5 年成长路线图
<CareerPathDemo />
### 7.2 各阶段能力要求
| 阶段 | 时间 | 核心能力 | 典型产出 |
|-----|------|---------|---------|
| **入门** | 0-1 年 | 掌握一门语言 + 基础工具 | 能完成简单功能模块 |
| **进阶** | 1-2 年 | 熟悉一个技术栈 + 工程化 | 能独立完成中型项目 |
| **高级** | 2-3 年 | 深入一个领域 + 架构能力 | 能设计系统方案 |
| **资深** | 3-5 年 | 技术深度 + 业务理解 + 团队协作 | 能主导大型项目 |
### 7.3 Vibe Coding 时代的学习策略
<LearningStrategyDemo />
::: tip 💡 核心建议
1. **基础比工具重要**:语言特性、数据结构、算法思维是根基
2. **实践比理论重要**:做项目是最好的学习方式
3. **思考比记忆重要**:理解"为什么"比记住"怎么做"更有价值
4. **AI 是工具不是拐杖**:用 AI 加速学习,不要用 AI 替代思考
:::
---
## 8. 总结:Vibe Coding 时代的核心竞争力
回顾本章,我们建立了计算机领域的全局认知:
1. **领域划分**:前端、后端、移动端、AI、运维、数据——各有侧重
2. **技术选型**:没有最好的技术,只有最适合场景的技术
3. **成长路径**:先深后广,建立根据地再横向扩展
4. **AI 时代**AI 能帮你写代码,但不能替你思考
### Vibe Coding 时代的三层能力
```
┌─────────────────────────────────────────┐
│ 第 3 层:判断力(AI 替代不了) │
│ - 知道什么是对的 │
│ - 知道什么是好的 │
│ - 知道该往哪个方向走 │
├─────────────────────────────────────────┤
│ 第 2 层:架构思维(AI 辅助) │
│ - 系统设计能力 │
│ - 模块划分能力 │
│ - 技术选型能力 │
├─────────────────────────────────────────┤
│ 第 1 层:代码实现(AI 擅长) │
│ - 语法编写 │
│ - API 调用 │
│ - 常见模式实现 │
└─────────────────────────────────────────┘
```
@@ -0,0 +1,566 @@
# 领域特定语言(DSL):后端世界中那些"不像代码的代码"
::: tip 前言
在一个真实案例中,工程师 Armin 在新公司用 AI 构建了一套基础设施服务,总计约 4 万行代码(Go + YAML + Pulumi + SDK 胶水代码),其中超过 90% 由 AI 生成。这个案例中出现了许多初学者不熟悉的术语:YAML、Pulumi、HCL、Lua、SDK 胶水代码……它们既不是 Python,也不是 JavaScript,却在后端项目中无处不在。本文将从一个统一的视角——**领域特定语言(DSL)**——来系统地介绍这些技术。
:::
**本文的学习目标**
在后端开发中,除了用通用编程语言(Python、Go、Java 等)编写的业务逻辑之外,还存在大量**用途各异、语法各异、但都不属于通用编程语言**的文件和代码。它们有一个共同的上位概念:**DSLDomain-Specific Language,领域特定语言)**。
学完本文后,你将能够:
- 理解 DSL 与通用编程语言(GPL)的本质区别
- 掌握 DSL 的分类体系:数据序列化格式、嵌入式脚本语言、基础设施定义语言
- 区分 XML、JSON、YAML、TOML、CSV、Protobuf 等数据格式的适用场景
- 理解 Lua 等嵌入式脚本语言的设计目的
- 解释 TerraformHCL)和 Pulumi 的原理与区别
- 理解 OpenAPI 规范与 SDK 自动生成的工作原理
- 判断哪些类型的代码适合交给 AI 生成
| 章节 | 主题 | 核心概念 |
|-----|------|---------|
| **第 1 章** | DSL 总论 | DSL vs GPL 的定义、分类体系与全景图 |
| **第 2 章** | 数据序列化格式 | XML、JSON、YAML、TOML、CSV、Protobuf 等 |
| **第 3 章** | 嵌入式脚本语言 | Lua 等语言的设计哲学与典型应用 |
| **第 4 章** | 基础设施即代码 | TerraformHCL)、Pulumi 的原理与对比 |
| **第 5 章** | 胶水代码与 SDK 生成 | OpenAPI 规范与客户端代码自动生成 |
| **第 6 章** | AI 与 DSL 的关系 | 为什么 AI 特别擅长生成 DSL 代码 |
---
## 1. DSL 总论:通用语言之外的另一个世界
### 1.1 什么是 DSL
**DSLDomain-Specific Language,领域特定语言)** 是为某个特定领域或特定任务设计的语言。与之相对的是 **GPLGeneral-Purpose Language,通用编程语言)**,如 Python、Java、Go、C++ 等——它们被设计为可以解决任意计算问题。
两者的核心区别:
| 维度 | GPL(通用编程语言) | DSL(领域特定语言) |
|------|-------------------|-------------------|
| **设计目标** | 解决任意计算问题 | 解决某个特定领域的问题 |
| **表达范围** | 图灵完备,理论上可以计算任何东西 | 通常有意限制表达范围 |
| **学习成本** | 较高,需要理解完整的语言体系 | 较低,只需理解该领域的概念 |
| **典型代表** | Python、Java、Go、C++、JavaScript | SQL、HTML/CSS、正则表达式、YAML、HCL |
你其实早就在使用 DSL 了:
- **SQL** 是数据库查询领域的 DSL——你用 `SELECT * FROM users WHERE age > 18` 来查数据,而不是用 Python 手写遍历逻辑
- **HTML/CSS** 是网页结构与样式领域的 DSL——你用标签和属性描述页面,而不是用 C++ 操作像素
- **正则表达式** 是文本模式匹配领域的 DSL——你用 `\d{3}-\d{4}` 匹配电话号码,而不是手写字符比较循环
### 1.2 DSL 的分类
DSL 可以按照"是否具备图灵完备性"分为两大类:
**外部 DSLExternal DSL**
拥有独立的语法和解析器,不依附于任何通用编程语言。用户编写的代码由专用的解释器或编译器处理。
- 纯数据描述型:JSON、YAML、XML、TOML、CSV、Protobuf(不含任何逻辑)
- 查询/操作型:SQL、GraphQL、正则表达式(有限的逻辑能力)
- 领域建模型:HCLTerraform)、Dockerfile、Nginx 配置语法(声明式描述特定领域的状态)
**内部 DSLInternal DSL / Embedded DSL**
寄生在某门通用编程语言内部,利用宿主语言的语法来构建领域专用的表达方式。代码本身是合法的宿主语言代码,但读起来像是一门专用语言。
- Pulumi(用 TypeScript/Python/Go 编写,但 API 设计得像声明式配置)
- Ruby on Rails 的路由定义(`get '/users', to: 'users#index'`,合法的 Ruby 代码,但读起来像配置)
- 测试框架中的断言语法(`expect(value).toBe(42)`,合法的 JavaScript,但读起来像自然语言)
### 1.3 后端项目中的 DSL 全景图
在一个典型的后端项目中,你会遇到以下几类 DSL:
```
后端项目中的 DSL
├── 数据序列化格式(描述数据结构)
│ ├── 文本格式:JSON、YAML、XML、TOML、CSV、INI
│ └── 二进制格式:Protobuf、MessagePack、Avro、BSON
├── 嵌入式脚本语言(可编程的配置层)
│ ├── Lua(游戏引擎、Nginx、Redis
│ ├── GDScriptGodot 引擎)
│ └── Jsonnet(配置模板生成)
├── 基础设施与运维 DSL(声明式描述系统状态)
│ ├── HCLTerraform
│ ├── Dockerfile / Docker Compose YAML
│ └── Nginx / Apache 配置语法
└── 接口描述语言(描述 API 契约)
├── OpenAPI / Swagger
├── Protocol Buffers.proto 文件)
└── GraphQL Schema
```
理解了这张全景图,后续章节将逐一展开每个分支。
---
## 2. 数据序列化格式:用文本描述结构化数据
### 2.1 什么是数据序列化?
**序列化(Serialization)** 是指将内存中的数据结构(对象、字典、数组等)转换为一种可存储或可传输的文本/字节流的过程。反过来,从文本/字节流还原为内存中的数据结构,称为**反序列化(Deserialization**。
数据序列化格式是 DSL 中最基础的一类——它们属于纯数据描述型外部 DSL,不具备任何逻辑能力,只负责静态地描述"值是什么"。
### 2.2 为什么需要这些格式?
假设你开发了一个后端服务,数据库地址为 `localhost:5432`。如果将这个地址硬编码在源代码中,本地开发没有问题,但部署到生产环境时,数据库地址变为 `db.prod.company.com:5432`,你就需要修改源代码并重新编译。
工程实践中的通用做法是:**将可变的参数从代码中分离出来,存放在独立的配置文件中。** 程序在启动时读取配置文件,根据其中的值来决定行为。
除了配置之外,数据序列化格式还广泛用于:系统间的数据交换(API 请求/响应)、数据持久化存储、跨语言通信等场景。
### 2.3 人类可读的文本格式
以下是工程中最常见的文本序列化格式,按历史顺序介绍。
**INI**
最早期的配置格式,起源于 Windows 系统。结构简单,由节(section)和键值对组成:
```ini
[database]
host = localhost
port = 5432
[server]
debug = true
```
优点是可读性强。局限在于不支持嵌套结构和数组类型,无法表达复杂配置。目前主要出现在遗留系统和部分 Linux 配置中(如 `php.ini``my.cnf`)。
**CSV**
**CSVComma-Separated Values,逗号分隔值)** 是最简单的表格数据格式:
```csv
name,age,city
Alice,30,Beijing
Bob,25,Shanghai
```
每行是一条记录,字段之间用逗号分隔。CSV 广泛用于数据导入导出、电子表格交换、数据分析管道。它的局限是只能表达扁平的二维表格,不支持嵌套结构,且没有类型信息(所有值都是字符串)。
**XML**
**XMLeXtensible Markup Language,可扩展标记语言)** 诞生于 1998 年,曾经是数据交换的主流标准:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<config>
<database>
<host>localhost</host>
<port>5432</port>
</database>
<server>
<debug>true</debug>
<allowed_origins>
<origin>https://example.com</origin>
<origin>https://app.example.com</origin>
</allowed_origins>
</server>
</config>
```
XML 的表达力非常强,支持嵌套、属性、命名空间、Schema 验证等高级特性。但它的语法冗长——大量的开闭标签导致信噪比低,手动编写和阅读的体验较差。
XML 在以下领域仍然广泛使用:
- Java 生态(Maven 的 `pom.xml`、Spring 配置、Android 布局文件)
- 企业级 Web 服务(SOAP 协议)
- 办公文档格式(`.docx``.xlsx` 本质上是 ZIP 压缩的 XML 文件集合)
- RSS/Atom 订阅源、SVG 矢量图形
**JSON**
**JSONJavaScript Object Notation** 诞生于 2001 年,因其简洁性迅速取代 XML 成为 Web API 数据交换的事实标准:
```json
{
"database": {
"host": "localhost",
"port": 5432
},
"server": {
"debug": true
}
}
```
优点是结构清晰,几乎所有编程语言都有原生解析支持。主要缺点是**不支持注释**,且大量的括号和引号在手动编写时容易出错。JSON 同时也是前端项目配置的标准格式(`package.json``tsconfig.json`)。
**YAML**
**YAMLYAML Ain't Markup Language** 同样诞生于 2001 年,是目前后端和 DevOps 领域使用最广泛的配置格式。Docker Compose、Kubernetes、GitHub Actions 等工具均采用 YAML
```yaml
# 数据库配置
database:
host: localhost
port: 5432
# 服务器配置
server:
debug: true
allowed_origins:
- https://example.com
- https://app.example.com
```
优点是支持注释、语法简洁、可表达复杂嵌套结构。缺点是**依赖缩进来表示层级关系**,缩进错误会导致解析失败,这是初学者最常遇到的问题。
> 补充:YAML 的全称 "YAML Ain't Markup Language" 是一个递归缩写。
**TOML**
**TOMLTom's Obvious Minimal Language** 诞生于 2013 年,被 Rust 的包管理器 Cargo 和 Python 的 `pyproject.toml` 采用:
```toml
[database]
host = "localhost"
port = 5432
[server]
debug = true
allowed_origins = [
"https://example.com",
"https://app.example.com"
]
```
TOML 试图兼顾 INI 的简洁性和 YAML 的表达力,同时避免缩进敏感带来的问题。
### 2.4 二进制序列化格式
上述格式都是人类可读的文本。在对性能和体积有更高要求的场景中,还存在一类**二进制序列化格式**——它们牺牲可读性,换取更小的体积和更快的解析速度。
| 格式 | 开发方 | 特点 | 典型使用场景 |
|------|-------|------|------------|
| **Protocol Buffers (Protobuf)** | Google | 需要预定义 `.proto` Schema 文件,强类型,体积极小 | gRPC 通信、Google 内部服务、高性能微服务 |
| **MessagePack** | 社区 | 类似 JSON 的二进制版本,无需 Schema | Redis 内部编码、跨语言高性能通信 |
| **Avro** | Apache | 支持 Schema 演进,适合大数据场景 | Hadoop / Kafka 生态的数据序列化 |
| **BSON** | MongoDB | JSON 的二进制扩展,支持更多数据类型 | MongoDB 数据库内部存储格式 |
以 Protocol Buffers 为例,需要先定义 Schema
```protobuf
// user.proto
syntax = "proto3";
message User {
string name = 1;
int32 age = 2;
string email = 3;
}
```
然后通过编译器(`protoc`)自动生成各语言的序列化/反序列化代码。这种"先定义 Schema,再生成代码"的模式与后文将介绍的 OpenAPI SDK 生成思路一致。
### 2.5 完整对比
| 格式 | 类型 | 诞生年代 | 可读性 | 支持注释 | 典型使用场景 |
|------|------|---------|--------|---------|------------|
| **INI** | 文本 | 1980s | 高 | ✅ | 系统配置、遗留项目 |
| **CSV** | 文本 | 1972 | 高 | ❌ | 数据导入导出、表格交换 |
| **XML** | 文本 | 1998 | 中 | ✅ | Java 生态、企业级 Web 服务、文档格式 |
| **JSON** | 文本 | 2001 | 高 | ❌ | Web API 数据交换、前端配置 |
| **YAML** | 文本 | 2001 | 高 | ✅ | Docker、K8s、CI/CD、后端服务配置 |
| **TOML** | 文本 | 2013 | 高 | ✅ | Rust / Python 项目配置 |
| **Protobuf** | 二进制 | 2008 | 无 | — | gRPC、高性能微服务通信 |
| **MessagePack** | 二进制 | 2008 | 无 | — | 高性能跨语言通信 |
| **Avro** | 二进制 | 2009 | 无 | — | Hadoop / Kafka 大数据管道 |
| **BSON** | 二进制 | 2009 | 无 | — | MongoDB 内部存储 |
**要点**:所有这些格式的本质功能相同——**将结构化数据转换为可存储、可传输的形式**。文本格式优先考虑人类可读性和易编辑性;二进制格式优先考虑解析性能和传输体积。选择哪种格式取决于具体场景的需求权衡。
---
## 3. 嵌入式脚本语言:可编程的配置层
### 3.1 概念定义
Python、JavaScript、Go 等语言是通用编程语言(General-Purpose Language),它们可以独立运行,构建完整的应用程序。
与之不同,还有一类语言**专门设计为嵌入到其他宿主程序中运行**,为宿主程序提供可编程的扩展能力。这类语言被称为**嵌入式脚本语言(Embedded Scripting Language**。
它们解决的核心问题是:**当静态配置文件(YAML/JSON)的表达力不够,需要引入条件判断、循环等逻辑时,如何在不修改宿主程序源码的前提下实现动态行为。**
### 3.2 Lua:最具代表性的嵌入式脚本语言
Lua(葡萄牙语中"月亮"的意思)是一门极其轻量的脚本语言,整个解释器编译后仅几百 KB。它的设计目标不是独立运行,而是作为可嵌入的扩展层。
Lua 的典型应用场景:
- **游戏引擎**:《魔兽世界》的插件系统、《Roblox》的游戏脚本均使用 Lua。游戏引擎用 C/C++ 实现核心渲染和物理计算,将关卡逻辑、NPC 对话等频繁变动的部分交给 Lua 脚本。这样,策划人员修改游戏内容时不需要重新编译引擎。
- **Web 服务器**OpenResty 将 Lua 嵌入 Nginx 内部,使运维人员可以用 Lua 脚本实现请求过滤、限流、鉴权等逻辑,而无需修改 Nginx 的 C 源码。
- **数据库**:Redis 支持将 Lua 脚本发送到服务端执行,用于实现需要原子性保证的复合操作(如"先读后写")。
以下是一段嵌入在 NginxOpenResty)中的 Lua 脚本示例:
```lua
-- 功能:对 /api/secret 路径进行 token 鉴权
local uri = ngx.var.uri
local token = ngx.req.get_headers()["Authorization"]
if uri == "/api/secret" and token ~= "Bearer my-secret-token" then
ngx.status = 403
ngx.say("Access denied")
return ngx.exit(403)
end
```
### 3.3 其他嵌入式脚本语言
| 语言 | 宿主环境 | 典型用途 |
|------|---------|---------|
| **Lua** | 游戏引擎、NginxOpenResty)、Redis | 游戏逻辑、网关策略、缓存操作 |
| **VimScript / Lua** | Vim / Neovim 编辑器 | 编辑器插件开发 |
| **Emacs Lisp** | Emacs 编辑器 | 编辑器行为自定义 |
| **GDScript** | Godot 游戏引擎 | 游戏逻辑脚本 |
| **Jsonnet** | Kubernetes 生态 / 配置生成工具 | 模板化生成大量相似的 JSON/YAML 配置 |
**要点**:嵌入式脚本语言在 DSL 分类中属于**内部 DSL 与外部 DSL 的交界地带**——它们是独立的语言(有自己的语法和解释器),但设计目标是嵌入宿主程序运行,而非独立构建应用。它们填补了"静态配置文件"(纯数据描述型 DSL)与"通用编程语言"(GPL)之间的空白:当配置需要表达逻辑(条件判断、循环、函数调用)时,嵌入一门轻量脚本语言是工程上的标准解决方案。
---
## 4. 基础设施即代码(Infrastructure as Code
### 4.1 什么是"基础设施"
在后端工程中,"基础设施"Infrastructure)指的是应用程序运行所依赖的底层资源:
- 计算资源:服务器(虚拟机或容器)
- 数据存储:数据库实例、对象存储桶
- 网络:防火墙规则、负载均衡器、DNS 配置
- 中间件:消息队列、缓存集群
在云计算时代,这些资源通过云服务商(如 AWS、阿里云、腾讯云)的控制台以图形界面的方式创建和管理。
### 4.2 手动管理的局限性
通过控制台手动操作在小规模项目中可行,但随着项目规模增长,会暴露以下问题:
1. **不可重复**:操作步骤没有记录,无法精确复现同一套环境
2. **不可审计**:无法追溯"谁在什么时间修改了什么配置"
3. **不可协作**:操作过程无法纳入版本控制,无法进行代码审查
4. **容易出错**:手动操作在生产环境中存在误操作风险
**基础设施即代码(Infrastructure as Code,简称 IaC)** 的核心思想是:**用代码来声明式地定义基础设施资源,使其具备版本控制、自动化执行和可重复部署的能力。**
### 4.3 Terraform
Terraform 是目前使用最广泛的 IaC 工具,由 HashiCorp 公司开发。它使用专用的 **HCLHashiCorp Configuration Language** 语言。
Terraform 采用**声明式**范式:用户描述期望的最终状态,Terraform 自动计算从当前状态到目标状态所需的操作。
```hcl
# 定义一台云服务器
resource "aws_instance" "my_server" {
ami = "ami-0c55b159cbfafe1f0" # 操作系统镜像
instance_type = "t3.micro" # 实例规格
tags = {
Name = "my-first-server"
}
}
# 定义一个 PostgreSQL 数据库实例
resource "aws_db_instance" "my_database" {
engine = "postgres"
instance_class = "db.t3.micro"
username = "admin"
password = "please-use-secrets-manager"
}
```
执行流程:
```bash
terraform plan # 预览将要执行的变更
terraform apply # 确认并执行,自动在云平台创建资源
```
### 4.4 Pulumi
Pulumi 提供了另一种思路:**直接使用通用编程语言(TypeScript、Python、Go 等)来定义基础设施**,而非学习专用的 HCL 语法。
同样的服务器定义,用 Pulumi + TypeScript 表达如下:
```typescript
import * as aws from "@pulumi/aws";
const server = new aws.ec2.Instance("my-server", {
ami: "ami-0c55b159cbfafe1f0",
instanceType: "t3.micro",
tags: { Name: "my-first-server" },
});
const bucket = new aws.s3.Bucket("my-bucket", {
acl: "private",
});
export const serverIp = server.publicIp;
```
由于使用的是通用编程语言,开发者可以利用循环、条件判断、函数抽象等语言特性来处理复杂的基础设施逻辑。
### 4.5 Terraform 与 Pulumi 的对比
| 维度 | Terraform | Pulumi |
|------|-----------|--------|
| **语言** | HCL(专用语言) | TypeScript / Python / Go 等通用语言 |
| **学习成本** | 需要学习 HCL 语法 | 使用已掌握的编程语言,学习成本较低 |
| **社区生态** | 非常成熟,几乎覆盖所有云服务商 | 快速增长中,但规模小于 Terraform |
| **适用场景** | 运维团队主导的标准化基础设施管理 | 开发者主导的项目,需要复杂逻辑的场景 |
| **AI 代码生成适配度** | 高(模式固定) | 很高(本质是通用编程语言代码) |
**要点**:IaC 工具中的 HCL 是一种典型的外部 DSL——它有独立的语法和解析器,专门用于声明式描述基础设施状态。而 Pulumi 则采用内部 DSL 的策略——用通用编程语言的语法来表达领域特定的概念。两者目标一致(将基础设施管理从手动操作转为代码驱动),路径不同(专用语言 vs 通用语言)。代码可以纳入 Git 版本控制、进行团队审查、自动化执行和回滚。
---
## 5. 胶水代码与 SDK 自动生成
### 5.1 什么是胶水代码
在软件工程中,**胶水代码(Glue Code)** 指的是本身不包含业务逻辑,仅用于连接两个系统或模块的代码。
典型的胶水代码包括:
- 前端调用后端 API 时编写的 HTTP 请求代码(URL 拼接、请求头设置、响应解析)
- 后端服务 A 调用服务 B 接口时编写的 HTTP 客户端代码
- 不同编程语言之间的接口适配代码
这类代码的特征是:**高度重复、模式固定、但不可省略。**
### 5.2 OpenAPI 规范与代码自动生成
既然胶水代码具有高度的模式化特征,工程界的解决方案是:**先用标准格式描述 API 接口,再用工具自动生成客户端代码。**
**OpenAPI 规范**(前身为 Swagger)是描述 REST API 的行业标准。它使用 YAML 或 JSON 格式,精确定义 API 的路径、参数、请求体和响应结构:
```yaml
openapi: 3.0.0
info:
title: 邮件服务 API
version: 1.0.0
paths:
/emails:
post:
summary: 发送邮件
requestBody:
content:
application/json:
schema:
type: object
properties:
to:
type: string
example: "user@example.com"
subject:
type: string
body:
type: string
responses:
'200':
description: 发送成功
```
基于这份规范文件,使用 `openapi-generator` 等工具可以自动生成多种语言的客户端 SDK:
- **Python**`client.emails.send(to="user@example.com", subject="Hi", body="Hello")`
- **TypeScript**`client.emails.send({ to: "user@example.com", subject: "Hi", body: "Hello" })`
- **Go**`client.Emails.Send(ctx, &SendEmailRequest{To: "user@example.com", ...})`
生成的 SDK 封装了 HTTP 请求的所有细节,调用方无需关心 URL 路径、请求方法、序列化格式等底层实现。
### 5.3 重新理解 Armin 的案例
回到本文开头的案例,现在可以准确理解其中每个组成部分:
| 组成部分 | 性质 | 说明 |
|---------|------|------|
| **Go** | 业务逻辑代码 | 邮件收发服务的核心功能实现 |
| **YAML** | 配置文件 | 服务配置、CI/CD 流水线定义、OpenAPI 规范文件 |
| **Pulumi** | 基础设施代码 | 用 Go/TypeScript 定义云资源(服务器、数据库、网络) |
| **SDK 胶水代码** | 自动生成的客户端库 | 从 OpenAPI 规范自动生成的 Python 和 TypeScript SDK |
其中 YAML 配置、Pulumi 资源定义、SDK 胶水代码这三类均属于高度模式化、有明确规范约束的代码,这正是 AI 代码生成能力最强的领域。因此"4 万行代码中 90% 由 AI 生成"是合理的。
---
## 6. AI 与 DSL 的关系
### 6.1 AI 代码生成的适用性分析
| 特征维度 | 适合 AI 生成 | 不适合 AI 生成 |
|---------|-------------|---------------|
| **模式化程度** | 高度重复,存在固定模板 | 需要创造性设计,无先例可循 |
| **规范约束** | 有明确的 schema 或语法规范 | 需求模糊,边界不清晰 |
| **上下文依赖** | 局部自洽,单个定义不依赖全局理解 | 需要理解整个系统的架构意图 |
| **可验证性** | 可被工具自动校验(如 `terraform validate` | 只能依靠人工判断设计合理性 |
本文介绍的四类技术——配置文件、嵌入式脚本、IaC 代码、SDK 胶水代码——均具备左列的特征。这解释了为什么 AI 在这些领域的代码生成效果显著优于业务逻辑代码。
### 6.2 评估框架
在判断某段代码是否适合交给 AI 生成时,可以参考以下三个标准:
1. **是否存在现成的规范或 schema?** —— 存在则 AI 友好
2. **是否属于大量重复的模式?** —— 是则 AI 友好
3. **生成结果能否被工具自动验证?** —— 能则 AI 友好
三项均满足的代码(如从 OpenAPI 规范生成 SDK、用 Terraform 批量定义同构资源),可以高度依赖 AI 生成。三项均不满足的代码(如设计一个新的分布式一致性协议),仍需要工程师自行完成。
---
## 7. 术语表
| 术语 | 全称 / 中文 | 定义 |
|------|------------|------|
| **DSL** | Domain-Specific Language / 领域特定语言 | 为特定领域设计的语言,与通用编程语言相对 |
| **GPL** | General-Purpose Language / 通用编程语言 | 可解决任意计算问题的编程语言,如 Python、Java、Go |
| **外部 DSL** | External DSL | 拥有独立语法和解析器的领域特定语言,如 SQL、HCL、YAML |
| **内部 DSL** | Internal DSL / Embedded DSL | 寄生在通用编程语言内部、利用宿主语法构建的领域专用表达,如 Pulumi |
| **数据序列化** | Data Serialization | 将内存中的数据结构转换为可存储或可传输的格式的过程 |
| **INI** | Initialization | 最早期的键值对配置格式,起源于 Windows 系统 |
| **CSV** | Comma-Separated Values / 逗号分隔值 | 用逗号分隔字段的纯文本表格格式 |
| **XML** | eXtensible Markup Language / 可扩展标记语言 | 基于标签的文本数据格式,表达力强但语法冗长 |
| **JSON** | JavaScript Object Notation | 基于键值对的轻量数据交换格式,Web API 的事实标准 |
| **YAML** | YAML Ain't Markup Language | 基于缩进的配置文件格式,后端和 DevOps 领域广泛使用 |
| **TOML** | Tom's Obvious Minimal Language | 显式语法的配置格式,Rust 和 Python 生态常用 |
| **Protobuf** | Protocol Buffers | Google 开发的二进制序列化格式,需预定义 Schema,体积小、速度快 |
| **MessagePack** | — | 类似 JSON 的二进制序列化格式,无需 Schema |
| **Lua** | — | 轻量级嵌入式脚本语言,常用于游戏引擎、Web 服务器和数据库扩展 |
| **IaC** | Infrastructure as Code / 基础设施即代码 | 用代码定义和管理云计算资源的工程实践 |
| **Terraform** | — | HashiCorp 开发的 IaC 工具,使用 HCL 声明式语言 |
| **HCL** | HashiCorp Configuration Language | Terraform 使用的专用配置语言 |
| **Pulumi** | — | 支持通用编程语言的 IaC 工具 |
| **OpenAPI** | — | 描述 REST API 接口的行业标准规范(前身为 Swagger) |
| **SDK** | Software Development Kit / 软件开发工具包 | 封装了 API 调用细节的客户端库 |
| **胶水代码** | Glue Code | 不含业务逻辑,仅用于连接两个系统的适配代码 |
---
## 总结
后端工程中存在大量非业务逻辑代码。它们有一个共同的上位概念:**DSL(领域特定语言)**——为特定领域设计的、与通用编程语言相对的语言。
本文介绍的 DSL 可以归为四个类别:
1. **数据序列化格式**XML / JSON / YAML / TOML / CSV / Protobuf 等)—— 纯数据描述型外部 DSL,将结构化数据转换为可存储、可传输的形式
2. **嵌入式脚本语言**(Lua 等)—— 介于配置与通用语言之间,为宿主程序提供可编程的扩展能力
3. **基础设施定义语言**HCL / Dockerfile 等)—— 声明式外部 DSL,描述系统期望状态;Pulumi 则以内部 DSL 的方式实现同一目标
4. **接口描述语言与胶水代码生成**OpenAPI / .proto)—— 通过规范描述自动生成系统间的连接代码
理解 DSL 这一分类框架后,面对后端项目中各类"不像代码的代码"时,可以快速识别其性质:它属于哪类 DSL、解决什么领域的问题、为什么不用通用编程语言来写。
同时,由于 DSL 代码具有高度模式化、规范驱动、可自动验证的特征,它们也是当前 AI 代码生成技术最有效的应用领域。
@@ -1,3 +1,173 @@
# 域名、DNS 与 HTTPS
> 待实现
::: tip 前言
**当你在浏览器输入 `www.google.com` 并按下回车,背后发生了什么?** 这个看似简单的动作,背后涉及域名解析、DNS 查询、TLS 加密握手等一系列精密的协作过程。理解这些机制,是每个开发者的必修课——它直接关系到你的网站能不能被访问、数据会不会被窃取。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **DNS 原理**:理解域名如何被翻译成 IP 地址的完整过程
- **记录类型**:掌握 A、CNAME、MX 等常见 DNS 记录的用途
- **HTTPS 机制**:理解 TLS 握手如何建立安全连接
- **证书体系**:了解数字证书的信任链和验证机制
- **安全意识**:明白为什么 HTTPS 是现代 Web 的底线要求
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | DNS 解析 | 递归查询、迭代查询 |
| **第 2 章** | DNS 记录 | A、CNAME、MX、TXT |
| **第 3 章** | HTTPS 与 TLS | 握手过程、加密通信 |
| **第 4 章** | 证书信任链 | CA、根证书、中间证书 |
| **第 5 章** | HTTP vs HTTPS | 明文 vs 加密、安全对比 |
---
## 0. 全景图:从域名到安全连接
互联网的通信基于 IP 地址(如 142.250.80.46),但人类记不住这些数字。于是我们发明了**域名系统(DNS)**——互联网的"电话簿",把人类可读的域名翻译成机器可读的 IP 地址。
但光能找到服务器还不够。如果通信内容是明文传输的,任何中间人都能窃听、篡改你的数据。**HTTPS** 就是解决这个问题的——它在 HTTP 之上加了一层 TLS 加密,确保数据在传输过程中的机密性和完整性。
::: tip 一次完整的网页访问
1. **域名解析**:浏览器问 DNS "www.google.com 的 IP 是多少?"DNS 回答 "142.250.80.46"
2. **TCP 连接**:浏览器与服务器建立 TCP 三次握手
3. **TLS 握手**:双方协商加密算法、验证证书、交换密钥
4. **加密通信**:所有 HTTP 数据通过加密通道传输
:::
---
## 1. DNS 解析:互联网的"电话簿"
DNSDomain Name System)的工作原理就像查电话簿:你知道对方的名字(域名),需要查到对方的电话号码(IP 地址)。但互联网的"电话簿"不是一本,而是一个分层的分布式系统。
<DnsResolutionDemo />
::: tip DNS 解析的四个步骤
1. **浏览器缓存**:先查本地缓存,如果之前访问过这个域名,直接用缓存的 IP
2. **递归解析器**:缓存没命中,请求发给 ISP 的递归解析器(如 8.8.8.8
3. **逐级查询**:递归解析器依次询问根域名服务器 → 顶级域服务器(.com)→ 权威域名服务器(google.com)
4. **返回结果**:权威服务器返回最终 IP,递归解析器缓存结果并返回给浏览器
:::
| 层级 | 服务器 | 职责 | 数量 |
|------|-------|------|------|
| 根域 | Root Server | 知道所有顶级域的地址 | 全球 13 组 |
| 顶级域 | TLD Server | 管理 .com、.cn、.org 等 | 每个后缀一组 |
| 权威域 | Authoritative | 存储具体域名的 DNS 记录 | 每个域名至少 2 个 |
| 递归解析器 | Resolver | 代替用户完成整个查询过程 | ISP 或公共 DNS |
---
## 2. DNS 记录类型:域名背后的"配置表"
DNS 不只是把域名翻译成 IP。通过不同类型的 DNS 记录,你可以控制邮件投递、域名跳转、服务发现等多种行为。理解这些记录类型,是配置域名和排查网络问题的基础。
<DnsRecordTypeDemo />
| 记录类型 | 用途 | 示例 |
|---------|------|------|
| A | 域名 → IPv4 地址 | `example.com → 93.184.216.34` |
| AAAA | 域名 → IPv6 地址 | `example.com → 2606:2800:220:1:...` |
| CNAME | 域名 → 另一个域名(别名) | `www.example.com → example.com` |
| MX | 指定邮件服务器 | `example.com → mail.example.com` |
| TXT | 存储文本信息 | SPF 验证、域名所有权验证 |
| NS | 指定权威域名服务器 | `example.com → ns1.example.com` |
::: tip 实际场景中的 DNS 配置
- **部署网站**:添加 A 记录指向服务器 IP,或 CNAME 指向 CDN 域名
- **配置邮箱**:添加 MX 记录指向邮件服务器,TXT 记录配置 SPF/DKIM 防垃圾邮件
- **验证域名所有权**:云服务商要求你添加特定 TXT 记录来证明你拥有这个域名
- **负载均衡**:同一域名配置多条 A 记录,DNS 轮询分发流量
:::
---
## 3. HTTPS 与 TLS:给数据穿上"防弹衣"
HTTP 协议的数据是明文传输的——就像寄明信片,邮递员(中间人)可以随意阅读内容。HTTPS 在 HTTP 之上加了一层 TLSTransport Layer Security)加密,相当于把明信片装进了密封信封。
TLS 握手是建立安全连接的关键步骤,它在正式传输数据之前,完成身份验证和密钥协商。
<HttpsHandshakeDemo />
::: tip TLS 1.3 握手的核心步骤
1. **Client Hello**:客户端发送支持的加密算法列表和一个随机数
2. **Server Hello**:服务器选择加密算法,返回数字证书和随机数
3. **证书验证**:客户端验证服务器证书是否可信(检查 CA 签名、有效期、域名匹配)
4. **密钥交换**:双方通过 ECDHE 算法协商出一个共享密钥(不在网络上传输密钥本身)
5. **加密通信**:后续所有数据使用协商好的对称密钥加密传输
:::
| 特性 | TLS 1.2 | TLS 1.3 |
|------|---------|---------|
| 握手往返次数 | 2-RTT | 1-RTT(首次)/ 0-RTT(恢复) |
| 密钥交换 | RSA 或 ECDHE | 仅 ECDHE(前向安全) |
| 加密算法 | 支持较多旧算法 | 仅保留安全算法 |
| 性能 | 较慢 | 更快 |
---
## 4. 证书信任链:凭什么相信这个网站?
TLS 握手中最关键的一步是"证书验证"。浏览器怎么判断一个网站的证书是真的,而不是攻击者伪造的?答案是**证书信任链**——一个层层背书的信任体系。
<CertificateChainDemo />
::: tip 证书信任链的三层结构
1. **根证书(Root CA**:由受信任的证书颁发机构签发,预装在操作系统和浏览器中。这是信任的"锚点"。
2. **中间证书(Intermediate CA**:由根 CA 签发,用于签发终端证书。根 CA 不直接签发网站证书,是为了安全隔离。
3. **终端证书(Leaf Certificate**:你的网站实际使用的证书,由中间 CA 签发,包含域名、公钥、有效期等信息。
:::
| 证书类型 | 验证级别 | 颁发速度 | 适用场景 |
|---------|---------|---------|---------|
| DV(域名验证) | 仅验证域名所有权 | 分钟级 | 个人网站、博客 |
| OV(组织验证) | 验证组织身份 | 数天 | 企业官网 |
| EV(扩展验证) | 严格验证组织 | 数周 | 银行、金融机构 |
| 通配符证书 | 覆盖所有子域名 | 视类型而定 | 多子域名场景 |
---
## 5. HTTP vs HTTPS:为什么加密是底线?
2024 年,全球超过 95% 的网页流量已经通过 HTTPS 传输。Chrome 浏览器会对 HTTP 网站标记"不安全"警告,搜索引擎也会降低 HTTP 网站的排名。HTTPS 不再是"可选项",而是现代 Web 的底线要求。
<DnsHttpsComparisonDemo />
| 维度 | HTTP | HTTPS |
|------|------|-------|
| 数据传输 | 明文,可被窃听 | 加密,无法被窃听 |
| 身份验证 | 无,无法确认服务器身份 | 有,通过证书验证服务器 |
| 数据完整性 | 无保护,可被篡改 | 有保护,篡改会被检测 |
| 端口 | 80 | 443 |
| SEO 影响 | 搜索排名降低 | 搜索排名加分 |
| 浏览器表现 | 显示"不安全"警告 | 显示锁图标 |
::: tip 免费获取 HTTPS 证书
**Let's Encrypt** 是一个免费、自动化的证书颁发机构,让任何网站都能零成本启用 HTTPS。配合 Certbot 工具,可以一键申请和自动续期证书。大多数云平台和 CDN 服务商也提供免费的 SSL 证书。
:::
---
## 总结
域名、DNS 和 HTTPS 是互联网基础设施的三大支柱。DNS 让我们用人类可读的名字访问网站,HTTPS 确保通信过程安全可信。
回顾本章的关键要点:
1. **DNS 是分层系统**:根域 → 顶级域 → 权威域,逐级查询,缓存加速
2. **记录类型各有用途**:A 记录指向 IP,CNAME 做别名,MX 管邮件,TXT 做验证
3. **TLS 握手建立信任**:证书验证 + 密钥协商,TLS 1.3 只需 1-RTT
4. **证书信任链**:根 CA → 中间 CA → 终端证书,层层背书
5. **HTTPS 是底线**:免费证书(Let's Encrypt)让加密零门槛
## 延伸阅读
- [How DNS Works](https://howdns.works/) - 漫画形式讲解 DNS 工作原理
- [Let's Encrypt 文档](https://letsencrypt.org/docs/) - 免费 SSL 证书申请指南
- [Cloudflare Learning Center](https://www.cloudflare.com/learning/dns/what-is-dns/) - DNS 和网络安全系统教程
- [TLS 1.3 RFC 8446](https://datatracker.ietf.org/doc/html/rfc8446) - TLS 1.3 协议规范
- [SSL Labs](https://www.ssllabs.com/ssltest/) - 在线检测网站 HTTPS 配置质量
@@ -1,3 +1,158 @@
# 故障排查与应急响应
> 待实现
::: tip 前言
**凌晨三点,手机疯狂震动,线上服务全面瘫痪——你该怎么办?** 对于任何互联网团队来说,故障不是"会不会发生"的问题,而是"什么时候发生"的问题。优秀的团队不是不出故障,而是出了故障能快速响应、高效恢复,并从中学习避免重蹈覆辙。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **分级意识**:掌握 P0~P4 事故严重程度分级标准
- **响应流程**:理解从发现到恢复的完整事故响应时间线
- **组织协作**:了解事故指挥体系中的角色分工和协作机制
- **告警体系**:掌握告警升级策略,确保关键问题不被遗漏
- **复盘方法**:学会用"五个为什么"挖掘根因,写出有价值的复盘报告
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 严重程度分级 | P0~P4、影响范围评估 |
| **第 2 章** | 响应时间线 | 发现→响应→恢复→复盘 |
| **第 3 章** | 指挥体系 | IC、通信官、技术负责人 |
| **第 4 章** | 告警升级 | 分级告警、逐级升级 |
| **第 5 章** | 事后复盘 | 五个为什么、无责文化 |
---
## 0. 全景图:故障是最好的老师
Netflix 有一个著名的工具叫 Chaos Monkey——它会随机杀掉生产环境的服务器。听起来疯狂,但背后的逻辑很清晰:**与其等故障找上门,不如主动制造故障来锻炼团队的应急能力**。
应急响应不是靠临场发挥,而是靠**流程、角色、工具**三位一体的体系化建设。就像消防队不是火灾发生时才组建的——他们平时就在训练、演练、维护装备。
::: tip 应急响应的四个核心要素
- **快速发现**:完善的监控和告警体系,确保问题在用户感知之前被发现
- **高效协作**:清晰的角色分工和沟通机制,避免混乱中的重复劳动
- **快速恢复**:优先恢复服务,而不是优先找根因。先止血,再治病
- **持续改进**:每次故障都是学习机会,通过复盘不断完善系统和流程
:::
---
## 1. 严重程度分级:不是所有故障都要"全员出动"
一个按钮颜色显示错误和整个支付系统瘫痪,显然不是同一个级别的问题。**事故分级**的目的是让团队用合适的力度响应合适级别的问题——既不过度反应浪费资源,也不轻视问题导致损失扩大。
<SeverityLevelDemo />
| 级别 | 名称 | 影响范围 | 响应要求 | 示例 |
|------|------|---------|---------|------|
| P0 | 致命 | 核心业务完全不可用 | 立即响应,全员待命 | 支付系统瘫痪、数据泄露 |
| P1 | 严重 | 核心功能严重受损 | 15 分钟内响应 | 登录失败率 > 50%、API 大面积超时 |
| P2 | 重要 | 部分功能异常 | 1 小时内响应 | 搜索结果不准确、部分页面 500 |
| P3 | 一般 | 非核心功能异常 | 工作时间处理 | 头像加载失败、非关键通知延迟 |
| P4 | 轻微 | 体验问题 | 排入迭代计划 | UI 错位、文案错误 |
::: tip 分级的关键原则
- **影响用户数**:影响 100% 用户的 P2 可能比影响 1% 用户的 P1 更紧急
- **业务损失**:直接影响收入的问题(支付、下单)优先级更高
- **可降级处理**:如果有临时方案可以缓解影响,可以适当降级处理
- **动态调整**:随着排查深入,级别可能上调或下调
:::
---
## 2. 响应时间线:从发现到复盘的完整流程
一次事故响应就像一场接力赛,每个阶段都有明确的目标和交接点。清晰的时间线能让团队在混乱中保持有序。
<IncidentTimelineDemo />
::: tip 事故响应的五个阶段
1. **检测(Detection**:通过监控告警、用户反馈或内部巡检发现异常。目标:尽早发现,缩短 MTTD(平均检测时间)。
2. **响应(Response**:确认事故、评估严重程度、召集响应团队、建立沟通频道。目标:快速组织起有效的响应力量。
3. **缓解(Mitigation**:采取临时措施恢复服务,如回滚部署、切换备用节点、限流降级。目标:先止血,恢复用户体验。
4. **修复(Resolution**:找到根本原因并彻底修复。目标:消除隐患,防止复发。
5. **复盘(Postmortem**:回顾整个过程,分析根因,制定改进措施。目标:从故障中学习,让系统更健壮。
:::
| 指标 | 含义 | 优化方向 |
|------|------|---------|
| MTTD | 平均检测时间 | 完善监控覆盖、降低告警阈值 |
| MTTR | 平均恢复时间 | 自动化恢复、预案演练 |
| MTBF | 平均故障间隔 | 提升系统可靠性、消除单点故障 |
---
## 3. 指挥体系:谁来指挥这场"战斗"?
大型事故中最怕的不是技术难题,而是**混乱**——十几个人同时在排查,没人知道别人在做什么,关键信息在各个群里碎片化传播。事故指挥体系(Incident Command System)就是为了解决这个问题。
<IncidentCommandDemo />
::: tip 三个核心角色
1. **事故指挥官(Incident Commander, IC**:整个事故响应的总负责人。负责决策、协调资源、把控节奏。IC 不一定是技术最强的人,但必须是最冷静、最有全局观的人。
2. **通信官(Communication Lead**:负责对外沟通——更新状态页、通知客户、同步管理层。让 IC 和技术人员专注于解决问题,不被沟通事务打断。
3. **技术负责人(Tech Lead**:负责技术层面的排查和修复。组织技术人员分工协作,向 IC 汇报进展和方案。
:::
---
## 4. 告警升级:确保关键问题不被遗漏
告警系统是事故响应的"眼睛"。但告警太少会漏报,告警太多会导致"告警疲劳"——当每天收到几百条告警时,真正重要的那条很容易被淹没。**告警升级策略**就是解决这个问题的关键。
<AlertEscalationDemo />
::: tip 告警升级的三层机制
1. **一线响应(L1**:告警触发后,先通知值班工程师。如果 15 分钟内未确认,自动升级。
2. **二线升级(L2**:通知团队负责人和相关领域专家。如果 30 分钟内未缓解,继续升级。
3. **三线升级(L3**:通知技术总监和管理层,启动全面应急响应。
:::
| 告警级别 | 通知方式 | 响应时限 | 升级条件 |
|---------|---------|---------|---------|
| Warning | IM 消息 | 工作时间处理 | 持续 30 分钟未恢复 |
| Critical | 电话 + IM | 15 分钟内确认 | 未确认或未缓解 |
| Fatal | 电话轰炸 + 短信 | 5 分钟内响应 | 自动升级至管理层 |
---
## 5. 事后复盘:从故障中学习
事故恢复后,最重要的一步是**复盘(Postmortem)**。复盘不是为了追责,而是为了找到系统性的改进机会。Google、Meta 等公司都奉行"无责复盘"文化——关注"系统为什么允许这个错误发生",而不是"谁犯了这个错误"。
<PostmortemDemo />
::: tip "五个为什么"分析法
从表面现象出发,连续追问"为什么",直到找到根本原因:
1. **为什么服务挂了?** → 数据库连接池耗尽
2. **为什么连接池耗尽?** → 慢查询占用连接不释放
3. **为什么有慢查询?** → 缺少索引,全表扫描
4. **为什么缺少索引?** → 新表上线时没有 DBA 审核
5. **为什么没有审核?** → 没有强制的 SQL 审核流程
根因不是"某个人忘了加索引",而是"缺少 SQL 审核流程"。修复根因才能防止复发。
:::
---
## 总结
故障排查与应急响应是每个技术团队的必备能力。它不是靠英雄主义的个人发挥,而是靠体系化的流程、清晰的角色分工和持续的复盘改进。
回顾本章的关键要点:
1. **分级响应**:P0~P4 分级确保用合适的力度应对合适级别的问题
2. **时间线清晰**:检测→响应→缓解→修复→复盘,每个阶段目标明确
3. **指挥体系**:IC + 通信官 + 技术负责人,分工协作避免混乱
4. **告警升级**:分级告警 + 自动升级,确保关键问题不被遗漏
5. **无责复盘**:用"五个为什么"挖掘根因,关注系统改进而非个人追责
## 延伸阅读
- [Google SRE Book - Incident Response](https://sre.google/sre-book/managing-incidents/) - Google 的事故管理实践
- [PagerDuty Incident Response Guide](https://response.pagerduty.com/) - PagerDuty 开源的应急响应指南
- [Atlassian Incident Management](https://www.atlassian.com/incident-management) - Atlassian 的事故管理最佳实践
- [Learning from Incidents](https://www.learningfromincidents.io/) - 从事故中学习的社区资源
- [Chaos Engineering (O'Reilly)](https://www.oreilly.com/library/view/chaos-engineering/9781492043850/) - 混沌工程原理与实践
@@ -1,3 +1,173 @@
# 基础设施即代码
> 待实现
::: tip 前言
**你有没有经历过这种噩梦:线上服务器挂了,但没人记得当初是怎么配置的?** 手动登录服务器、凭记忆敲命令、祈祷不要敲错——这就是传统运维的日常。基础设施即代码(Infrastructure as CodeIaC)彻底改变了这一切:用代码来定义和管理基础设施,让服务器配置像软件一样可版本控制、可复现、可审计。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **核心概念**:理解 IaC 是什么,为什么它是现代运维的基石
- **工作流认知**:掌握 Terraform 的 Write → Plan → Apply → Destroy 四阶段流程
- **工具选型**:了解 Terraform、Pulumi、CloudFormation 等主流工具的优劣
- **风险意识**:理解配置漂移的危害和检测方法
- **最佳实践**:掌握 IaC 项目的工程化管理方法
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | IaC 概念 | 手动运维 vs 代码化管理 |
| **第 2 章** | Terraform 工作流 | Write → Plan → Apply |
| **第 3 章** | 工具对比 | Terraform、Pulumi、CDK |
| **第 4 章** | 配置漂移 | 检测、预防、修复 |
| **第 5 章** | 最佳实践 | 模块化、状态管理、CI/CD |
---
## 0. 全景图:为什么基础设施也需要"源代码"?
想象你是一个厨师。如果每道菜都凭感觉做,今天放一勺盐、明天放两勺,味道永远不稳定。但如果你把配方写下来——精确到每种调料的克数——任何人都能复现同样的味道。
基础设施管理面临的是同样的问题。一台服务器的配置可能涉及操作系统、网络规则、安全组、存储卷、环境变量等几十个参数。手动配置不仅容易出错,而且**不可复现、不可审计、不可回滚**。
::: tip IaC 的核心价值
- **可复现**:同一份代码,无论执行多少次,结果都一样(幂等性)
- **可版本控制**:基础设施变更通过 Git 管理,谁改了什么、为什么改,一目了然
- **可审计**:所有变更都有记录,满足合规要求
- **可自动化**:通过 CI/CD 流水线自动部署,消除人为操作风险
- **可协作**:团队成员通过 Pull Request 审查基础设施变更,就像审查代码一样
:::
---
## 1. IaC 概念:从"手动点击"到"代码声明"
传统运维的工作方式是:登录云平台控制台,手动点击创建服务器、配置网络、设置安全组。这种方式在管理几台服务器时还能应付,但当规模扩大到几十、几百台时,就变成了噩梦。
IaC 的核心思想是:**用声明式代码描述你想要的基础设施状态,让工具自动帮你实现**。你不需要告诉工具"先创建 VPC,再创建子网,再创建安全组"(命令式),只需要说"我要一个这样的网络环境"(声明式),工具会自动计算出需要执行的步骤。
<IaCConceptDemo />
| 维度 | 手动运维 | 基础设施即代码 |
|------|---------|--------------|
| 操作方式 | 登录控制台点击 | 编写代码文件 |
| 可复现性 | 依赖文档和记忆 | 代码即文档,100% 可复现 |
| 变更追踪 | 无记录或记录不全 | Git 版本控制,完整历史 |
| 协作方式 | 口头沟通、文档传递 | Pull Request 审查 |
| 回滚能力 | 手动逆向操作 | git revert + 重新 apply |
| 一致性 | 环境间差异大 | 开发/测试/生产完全一致 |
::: tip 声明式 vs 命令式
- **声明式(Declarative**:描述"我要什么",工具自动计算"怎么做"。Terraform、CloudFormation 采用这种方式。优点是幂等性好,缺点是灵活性受限。
- **命令式(Imperative**:描述"怎么做",一步步执行。Ansible、Shell 脚本采用这种方式。优点是灵活,缺点是难以保证幂等性。
- **混合式**Pulumi、AWS CDK 用通用编程语言编写,兼具声明式的状态管理和命令式的灵活性。
:::
---
## 2. Terraform 工作流:Write → Plan → Apply
Terraform 是目前最流行的 IaC 工具,由 HashiCorp 开发。它的工作流程清晰直观,分为四个阶段,就像软件开发的"编码→审查→部署→清理"。
<TerraformWorkflowDemo />
::: tip 四阶段工作流
1. **Write(编写)**:用 HCLHashiCorp Configuration Language)编写基础设施定义文件(.tf)。声明你需要的资源:服务器、数据库、网络等。
2. **Plan(计划)**:运行 `terraform plan`,Terraform 会对比当前状态和目标状态,生成一份"执行计划"——告诉你它打算创建、修改、删除哪些资源。这是安全网,让你在真正执行前确认变更。
3. **Apply(执行)**:确认计划无误后,运行 `terraform apply`,Terraform 按计划创建或修改资源。执行完成后,当前状态会保存到状态文件(terraform.tfstate)。
4. **Destroy(销毁)**:不再需要时,运行 `terraform destroy` 清理所有资源,避免产生不必要的费用。
:::
| 命令 | 作用 | 是否修改基础设施 | 使用场景 |
|------|------|----------------|---------|
| `terraform init` | 初始化项目,下载 Provider | 否 | 首次使用或添加新 Provider |
| `terraform plan` | 预览变更,生成执行计划 | 否 | 每次变更前必须执行 |
| `terraform apply` | 执行变更,创建/修改资源 | 是 | 确认 plan 后执行 |
| `terraform destroy` | 销毁所有资源 | 是 | 清理测试环境、下线服务 |
| `terraform state` | 查看/管理状态文件 | 视操作而定 | 状态迁移、资源导入 |
---
## 3. 工具对比:选择适合你的 IaC 工具
IaC 领域有多种工具,各有侧重。选择工具时需要考虑团队技术栈、云平台、项目规模等因素。没有"最好"的工具,只有最适合你场景的工具。
<IaCToolComparisonDemo />
| 工具 | 语言 | 云平台支持 | 学习曲线 | 适用场景 |
|------|------|-----------|---------|---------|
| Terraform | HCL | 多云(AWS/Azure/GCP | 中等 | 多云环境、团队协作 |
| Pulumi | Python/TS/Go | 多云 | 低(熟悉编程语言) | 开发者友好、复杂逻辑 |
| AWS CloudFormation | JSON/YAML | 仅 AWS | 中等 | 纯 AWS 环境 |
| AWS CDK | Python/TS/Java | 仅 AWS | 低 | AWS + 编程语言偏好 |
| Ansible | YAML | 多云 + 裸机 | 低 | 配置管理、混合环境 |
::: tip 如何选择?
- **初创团队 / 单云**CloudFormation(AWS)或对应云平台原生工具,生态集成最好
- **多云 / 中大型团队**Terraform,社区最大、Provider 最丰富、招聘最容易
- **开发者主导的团队**:Pulumi 或 CDK,用熟悉的编程语言写基础设施,IDE 支持好
- **需要配置管理**:Ansible,擅长服务器内部配置(安装软件、修改配置文件)
:::
---
## 4. 配置漂移:无声的定时炸弹
配置漂移(Configuration Drift)是 IaC 实践中最隐蔽的敌人。它指的是**实际基础设施状态与代码定义的状态之间逐渐产生偏差**。
这种偏差通常是怎么产生的?有人为了"快速修复"一个线上问题,直接登录控制台手动改了安全组规则;有人为了调试,临时加大了某台服务器的配置但忘了改回来。这些"小改动"日积月累,最终导致代码和实际环境严重脱节。
<ConfigDriftDemo />
::: tip 配置漂移的危害
1. **不可复现**:代码描述的环境和实际环境不一致,新建环境时会出问题
2. **回滚失败**:以为回滚到上一版本就能恢复,但实际环境已经被手动修改过
3. **安全隐患**:手动开放的端口、放宽的权限可能被遗忘,成为攻击入口
4. **审计失效**:合规审计基于代码,但代码不反映真实状态
:::
| 预防措施 | 说明 |
|---------|------|
| 禁止手动变更 | 通过 IAM 策略限制控制台操作权限 |
| 定期漂移检测 | 定时运行 `terraform plan` 检查差异 |
| 自动修复 | 检测到漂移后自动执行 apply 恢复一致性 |
| 变更审计 | 开启 CloudTrail 等审计日志,追踪所有变更来源 |
---
## 5. 最佳实践:让 IaC 项目可持续演进
IaC 代码和应用代码一样,需要良好的工程实践来保证可维护性。随着基础设施规模增长,没有章法的 IaC 代码会变成另一种形式的"技术债"。
<IaCBestPracticeDemo />
::: tip 六条核心最佳实践
1. **模块化**:将可复用的基础设施抽象为模块(如 VPC 模块、数据库模块),避免复制粘贴。就像写函数一样,一处定义、多处调用。
2. **环境隔离**:开发、测试、生产使用独立的状态文件和变量文件,通过 workspace 或目录结构隔离。
3. **远程状态管理**:状态文件(tfstate)存储在远程后端(S3 + DynamoDB),支持团队协作和状态锁定,避免并发冲突。
4. **敏感信息管理**:密码、密钥等敏感信息不要写在代码里,使用 Vault、AWS Secrets Manager 等工具管理。
5. **CI/CD 集成**:将 terraform plan 集成到 PR 流程,apply 通过流水线自动执行,杜绝本地手动操作。
6. **代码审查**:基础设施变更和应用代码一样需要 Code Review,尤其是涉及安全组、IAM 策略的变更。
:::
---
## 总结
基础设施即代码是现代云原生运维的基石。它把"不可描述的手动操作"变成了"可版本控制的代码",让基础设施管理从"艺术"变成了"工程"。
回顾本章的关键要点:
1. **IaC 的本质**:用代码声明基础设施的期望状态,让工具自动实现
2. **Terraform 工作流**Write → Plan → Apply 三步走,Plan 是安全网
3. **工具选型**:多云选 Terraform,单云选原生工具,开发者团队选 Pulumi
4. **配置漂移**:最隐蔽的风险,需要通过流程和工具双重防护
5. **工程化管理**:模块化、环境隔离、远程状态、CI/CD 集成缺一不可
## 延伸阅读
- [Terraform 官方教程](https://developer.hashicorp.com/terraform/tutorials) - 从零开始学 Terraform
- [Pulumi 文档](https://www.pulumi.com/docs/) - 用编程语言写基础设施
- [AWS CDK Workshop](https://cdkworkshop.com/) - AWS CDK 实战教程
- [Infrastructure as Code (O'Reilly)](https://www.oreilly.com/library/view/infrastructure-as-code/9781098114664/) - IaC 领域的经典书籍
- [Spacelift Blog](https://spacelift.io/blog) - IaC 最佳实践和行业趋势
@@ -1,3 +1,164 @@
# AI 原生应用设计
> 待实现
::: tip 前言
**为什么有些 AI 产品让人惊艳,而有些只是"套壳 ChatGPT"?** 差别不在于用了多强的模型,而在于产品是否从底层就围绕 AI 的特性来设计。AI 原生应用不是在传统应用上"加个聊天框",而是重新思考用户交互、系统架构和产品逻辑的全新范式。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **范式认知**:理解 AI 原生应用与传统应用的本质区别
- **设计原则**:掌握 AI 原生产品设计的核心原则
- **Prompt 工程**:了解如何设计高质量的 Prompt 来驱动 AI 能力
- **交互模式**:认识 AI 时代的新型用户交互范式
- **架构思维**:理解 AI 应用的请求处理流程和系统架构
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 架构对比 | 传统应用 vs AI 原生应用 |
| **第 2 章** | 设计原则 | AI-First 思维、不确定性设计 |
| **第 3 章** | Prompt 工程 | 系统提示词、模板设计 |
| **第 4 章** | 交互模式 | 流式输出、多模态、Agent |
| **第 5 章** | 请求流程 | AI 应用的完整生命周期 |
---
## 0. 全景图:从"加个 AI"到"AI 原生"
过去几年,很多产品的 AI 化路径是这样的:有一个现成的应用,然后在某个角落加一个"AI 助手"按钮。这种做法就像在马车上装一个引擎——能跑,但远不如从头设计一辆汽车。
**AI 原生应用**是一种全新的产品思维:从第一行代码开始,就把 AI 作为核心能力来设计,而不是事后附加的功能。
::: tip 传统应用 vs AI 原生应用
- **传统应用**:用户操作 → 确定性逻辑 → 确定性结果。每次点击"提交订单",流程完全一样。
- **AI 原生应用**:用户意图 → AI 理解 → 概率性结果。同样的问题,每次回答可能略有不同。
- **核心转变**:从"编写规则"到"描述意图",从"确定性"到"概率性",从"操作界面"到"对话界面"。
:::
---
## 1. 架构对比:两种完全不同的世界
传统应用的架构是"请求-响应"模型:用户点击按钮,后端执行确定性逻辑,返回确定性结果。整个过程可预测、可测试、可复现。
AI 原生应用则引入了一个全新的角色——**大语言模型**。它像一个"智能中间层",接收自然语言输入,输出自然语言结果。这带来了架构上的根本性变化。
<AINativeArchDemo />
| 维度 | 传统应用 | AI 原生应用 |
|------|---------|------------|
| 输入方式 | 表单、按钮、下拉框 | 自然语言、图片、语音 |
| 处理逻辑 | if-else、规则引擎 | LLM 推理、Prompt 驱动 |
| 输出特性 | 确定性、可复现 | 概率性、每次可能不同 |
| 延迟特征 | 毫秒级 | 秒级(需要流式输出) |
| 错误处理 | 明确的错误码 | 幻觉、拒绝回答、答非所问 |
| 成本模型 | 固定计算资源 | 按 token 计费,成本波动大 |
::: tip 架构演进的三个阶段
1. **AI 增强型**:在现有应用中嵌入 AI 功能(如自动补全、智能推荐)
2. **AI 协作型**:AI 作为核心交互方式,但仍有传统 UI 兜底(如 Notion AI、GitHub Copilot
3. **AI 原生型**:整个产品围绕 AI 构建,去掉 AI 产品就不成立(如 ChatGPT、Cursor、Midjourney
:::
---
## 2. 设计原则:AI 原生产品的"宪法"
设计 AI 原生应用不能照搬传统软件的设计思路。AI 的概率性、延迟性和不可预测性,要求我们建立一套全新的设计原则。
<AIDesignPrincipleDemo />
::: tip 五大核心设计原则
1. **拥抱不确定性**:AI 的输出不是 100% 可靠的,产品设计必须考虑"AI 可能出错"的情况。提供编辑、重试、反馈机制,让用户始终拥有控制权。
2. **渐进式信任**:不要一开始就让 AI 做高风险决策。先从低风险场景建立用户信任,再逐步扩展 AI 的自主权。
3. **透明可解释**:让用户知道 AI 在做什么、为什么这么做。展示推理过程、引用来源、标注置信度。
4. **人机协作**:AI 不是替代人,而是增强人。最好的设计是让 AI 做初稿,人做终审。
5. **优雅降级**:当 AI 服务不可用或结果不理想时,产品仍然可用。永远有 Plan B。
:::
---
## 3. Prompt 工程:AI 应用的"编程语言"
在传统应用中,你用代码告诉计算机做什么。在 AI 原生应用中,你用 Prompt 告诉模型做什么。**Prompt 就是 AI 时代的编程语言**——写得好,AI 表现惊艳;写得差,AI 胡说八道。
<PromptDesignDemo />
::: tip Prompt 设计的四层结构
1. **系统提示词(System Prompt**:定义 AI 的角色、能力边界和行为规范。这是"宪法"级别的指令,用户看不到但始终生效。
2. **上下文注入(Context**:通过 RAG 检索到的相关文档、用户历史记录等,为 AI 提供回答所需的背景信息。
3. **用户输入(User Message**:用户的实际问题或指令。
4. **输出格式约束(Format**:指定 AI 的输出格式(JSON、Markdown、特定模板),确保结果可被程序解析。
:::
| Prompt 技巧 | 说明 | 效果 |
|------------|------|------|
| 角色设定 | "你是一个资深前端工程师" | 提升专业领域回答质量 |
| Few-shot 示例 | 给出 2-3 个输入输出示例 | 让模型理解期望的格式和风格 |
| 思维链(CoT | "请一步步思考" | 提升复杂推理的准确性 |
| 输出约束 | "用 JSON 格式回答" | 确保输出可被程序解析 |
| 负面指令 | "不要编造不确定的信息" | 减少幻觉和错误信息 |
---
## 4. 交互模式:AI 时代的用户体验
AI 原生应用催生了一批全新的交互模式。传统应用的交互是"点击-等待-查看",而 AI 应用的交互更像是"对话-观察-调整"。
<AIUXPatternDemo />
::: tip 四种核心交互模式
1. **流式输出(Streaming**:AI 生成内容时逐字显示,而不是等全部生成完再展示。这大幅降低了用户的感知等待时间,也让用户可以在生成过程中判断方向是否正确。
2. **多轮对话(Multi-turn**:通过上下文记忆实现连续对话,用户可以逐步细化需求。关键挑战是上下文窗口管理和对话历史压缩。
3. **多模态交互(Multimodal**:支持文本、图片、语音、文件等多种输入方式,AI 也能输出图片、代码、表格等多种格式。
4. **Agent 模式(Agentic**:AI 不只是回答问题,而是自主规划、执行多步骤任务。用户给出目标,AI 自行拆解步骤并逐一完成。
:::
---
## 5. 请求流程:一次 AI 调用的完整生命周期
当用户在 AI 应用中发送一条消息,背后发生了什么?理解这个完整流程,是构建可靠 AI 应用的基础。
<AIAppFlowDemo />
::: tip 请求处理的六个阶段
1. **输入预处理**:校验用户输入、内容安全审核、敏感信息脱敏
2. **上下文组装**:拼接系统提示词、检索相关文档(RAG)、加载对话历史
3. **模型调用**:将组装好的 Prompt 发送给 LLM API,开启流式响应
4. **输出后处理**:格式化输出、内容安全过滤、结构化数据提取
5. **结果缓存**:对常见问题缓存结果,降低成本和延迟
6. **监控记录**:记录 token 用量、响应时间、用户反馈,用于持续优化
:::
| 阶段 | 关键考量 | 常见问题 |
|------|---------|---------|
| 输入预处理 | 注入攻击防护、长度限制 | Prompt 注入、越狱攻击 |
| 上下文组装 | token 预算分配、信息优先级 | 上下文溢出、关键信息被截断 |
| 模型调用 | 超时处理、重试策略、流式传输 | API 限流、网络超时 |
| 输出后处理 | 格式校验、幻觉检测 | 输出格式不符预期 |
| 缓存策略 | 语义缓存 vs 精确缓存 | 缓存命中率低 |
| 监控告警 | 成本监控、质量评估 | token 成本失控 |
---
## 总结
AI 原生应用设计不是简单地在传统应用上叠加 AI 功能,而是从架构、交互、工程实践等维度进行全面重构。
回顾本章的关键要点:
1. **架构转变**:从确定性逻辑到概率性推理,AI 原生应用需要全新的架构思维
2. **设计原则**:拥抱不确定性、渐进式信任、透明可解释、人机协作、优雅降级
3. **Prompt 是核心**Prompt 工程是 AI 应用的"编程语言",直接决定产品质量
4. **交互革新**:流式输出、多轮对话、多模态、Agent 模式重新定义了用户体验
5. **全链路思维**:从输入预处理到监控告警,每个环节都需要针对 AI 特性专门设计
## 延伸阅读
- [Google PAIR Guidelines](https://pair.withgoogle.com/) - Google 的人机交互 AI 设计指南
- [OpenAI Prompt Engineering Guide](https://platform.openai.com/docs/guides/prompt-engineering) - 官方 Prompt 工程最佳实践
- [Anthropic Prompt Engineering](https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering) - Claude 的 Prompt 设计指南
- [Nielsen Norman Group: AI UX](https://www.nngroup.com/topic/artificial-intelligence/) - AI 用户体验研究
- [Building LLM Applications](https://www.oreilly.com/library/view/building-llm-powered/9781835462317/) - 构建 LLM 应用的实战指南
@@ -1,3 +1,193 @@
# Embedding 与向量检索
> 待实现
::: tip 前言
**计算机怎么理解"猫和狗很像,但和汽车不像"这件事?** 对人类来说这是常识,但对计算机来说,"猫"、"狗"、"汽车"不过是三个毫无关联的字符串。Embedding(嵌入)技术就是解决这个问题的关键——它把文字变成数字向量,让计算机也能理解语义上的"远近亲疏"。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **直觉理解**:明白 Embedding 是什么,为什么"猫"和"狗"的向量会靠近
- **相似度计算**:掌握余弦相似度、欧氏距离等核心度量方法
- **索引原理**:理解向量数据库如何在百万级数据中毫秒级检索
- **技术选型**:了解主流向量数据库的特点和适用场景
- **端到端流程**:掌握从文本到向量到检索的完整 Pipeline
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | Embedding 概念 | 语义空间、向量表示 |
| **第 2 章** | 相似度计算 | 余弦相似度、欧氏距离 |
| **第 3 章** | 向量索引 | 暴力搜索 vs ANN |
| **第 4 章** | 向量数据库 | Pinecone、Milvus、Chroma |
| **第 5 章** | 端到端 Pipeline | 文本→向量→存储→查询 |
---
## 0. 全景图:从文字到数字的桥梁
在自然语言处理的世界里,有一个根本性的挑战:**计算机只认识数字,不认识文字**。
早期的做法是给每个词分配一个编号(One-Hot 编码),比如"猫"=001"狗"=010"汽车"=100。但这样做有个致命问题:**所有词之间的距离都一样远**。"猫"到"狗"的距离和"猫"到"汽车"的距离完全相同——这显然不符合我们的直觉。
Embedding 的革命性在于:它把每个词映射到一个**稠密的低维向量空间**,让语义相近的词自然聚集在一起。在这个空间里,"猫"和"狗"靠得很近,而"汽车"则在远处——计算机终于能"理解"语义了。
::: tip 从 One-Hot 到 Embedding 的飞跃
- **One-Hot**:维度 = 词表大小(可能几万维),每个向量只有一个 1,其余全是 0,稀疏且无语义
- **Embedding**:维度通常 768~1536,每个数字都有意义,稠密且富含语义信息
- **关键突破**Word2Vec2013)证明了"词的含义可以用它的上下文来定义",开启了 Embedding 时代
:::
---
## 1. Embedding 概念:把文字变成坐标
Embedding 的核心思想可以用一句话概括:**用一组数字(向量)来表示一个词或句子的含义**。
想象一个二维坐标系。我们把"猫"放在坐标 (0.2, 0.7)"狗"放在 (0.3, 0.6)"汽车"放在 (0.9, 0.1)。你会发现"猫"和"狗"的坐标很接近,而"汽车"离它们很远。这就是 Embedding 的直觉——**语义相似度变成了空间距离**。
<EmbeddingConceptDemo />
::: tip Embedding 的三个关键特性
1. **语义聚类**:相似含义的词会自动聚集在一起(动物一簇、食物一簇、科技一簇)
2. **类比关系**:向量运算可以表达语义关系,经典例子:king - man + woman ≈ queen
3. **维度含义**:每个维度隐式编码了某种语义特征(如"是否是动物"、"大小"、"情感倾向"等)
:::
| 编码方式 | 维度 | 语义信息 | 典型应用 |
|---------|------|---------|---------|
| One-Hot | 词表大小(~50000 | 无 | 传统 NLP |
| Word2Vec | 100~300 | 词级语义 | 词相似度、类比推理 |
| BERT Embedding | 768 | 上下文语义 | 句子理解、问答 |
| OpenAI text-embedding-3 | 1536~3072 | 深层语义 | RAG、语义搜索 |
---
## 2. 相似度计算:向量之间有多"近"?
有了向量表示,下一个问题自然是:**怎么衡量两个向量有多相似?** 这就像在地图上衡量两个城市有多近——你可以量直线距离,也可以看方向是否一致。
<VectorSimilarityDemo />
::: tip 两种核心度量
- **余弦相似度(Cosine Similarity**:衡量两个向量的**方向**是否一致,值域 [-1, 1]。1 表示方向完全相同,0 表示正交(无关),-1 表示完全相反。文本语义比较的首选,因为它不受向量长度影响。
- **欧氏距离(Euclidean Distance**:衡量两个向量端点之间的**直线距离**,值域 [0, ∞)。0 表示完全重合,值越大越不相似。适合需要考虑"绝对大小"的场景。
:::
| 度量方式 | 公式直觉 | 值域 | 适用场景 |
|---------|---------|------|---------|
| 余弦相似度 | 看方向,忽略长度 | [-1, 1] | 文本语义搜索、推荐系统 |
| 欧氏距离 | 看端点直线距离 | [0, ∞) | 图像特征、聚类分析 |
| 点积 | 方向 × 长度 | (-∞, +∞) | 归一化向量的快速计算 |
| 曼哈顿距离 | 沿坐标轴走的距离 | [0, ∞) | 高维稀疏向量 |
---
## 3. 向量索引:如何在百万向量中毫秒检索?
假设你有 100 万条文档,每条都转成了 1536 维的向量。用户提了一个问题,你需要找到最相似的 10 条。最直接的方法是逐一计算相似度——但这意味着要做 100 万次 1536 维的向量运算,太慢了。
这就是**向量索引**要解决的问题:**用空间换时间,通过预处理建立索引结构,让检索速度从 O(n) 降到近似 O(log n)**。
<VectorIndexDemo />
::: tip 暴力搜索 vs 近似最近邻(ANN)
- **暴力搜索(Flat**:逐一比较,100% 准确但速度慢。适合数据量小(< 10 万)的场景。
- **IVF(倒排文件索引)**:先把向量空间划分成若干区域(聚类),查询时只搜索最近的几个区域。像是把图书馆按主题分区,找书时只去相关区域。
- **HNSW(分层可导航小世界图)**:构建多层图结构,从粗粒度到细粒度逐层导航。像是先看世界地图定位到国家,再看省级地图,最后看街道地图。
- **PQ(乘积量化)**:把高维向量压缩成短编码,牺牲少量精度换取大幅内存节省。适合超大规模数据集。
:::
| 索引类型 | 构建速度 | 查询速度 | 召回率 | 内存占用 | 适用规模 |
|---------|---------|---------|-------|---------|---------|
| Flat(暴力) | 无需构建 | 慢 | 100% | 高 | < 10 万 |
| IVF | 中等 | 快 | 95%+ | 中 | 10 万~1000 万 |
| HNSW | 慢 | 很快 | 99%+ | 高 | 10 万~1000 万 |
| PQ | 中等 | 快 | 90%+ | 很低 | > 1000 万 |
| IVF-PQ | 中等 | 快 | 92%+ | 低 | > 1 亿 |
---
## 4. 向量数据库:专为向量而生的存储引擎
有了向量和索引算法,你需要一个地方来存储和管理它们。传统数据库(MySQL、PostgreSQL)擅长处理结构化数据,但对高维向量的相似度搜索力不从心。**向量数据库**就是为这个场景专门设计的。
<VectorDatabaseDemo />
::: tip 向量数据库的核心能力
1. **高效存储**:针对高维浮点向量优化的存储格式
2. **ANN 检索**:内置多种近似最近邻索引算法(HNSW、IVF 等)
3. **元数据过滤**:支持在向量搜索的同时按标签、时间等条件过滤
4. **实时更新**:支持动态增删改向量,无需重建整个索引
5. **水平扩展**:分布式架构支持亿级向量规模
:::
| 数据库 | 类型 | 特点 | 适用场景 |
|-------|------|------|---------|
| Pinecone | 全托管云服务 | 零运维、开箱即用 | 快速原型、中小规模生产 |
| Milvus | 开源分布式 | 高性能、可扩展 | 大规模生产环境 |
| Chroma | 开源轻量 | 嵌入式、API 简洁 | 本地开发、小型项目 |
| Weaviate | 开源云原生 | 内置向量化、GraphQL | 需要自动向量化的场景 |
| Qdrant | 开源高性能 | Rust 实现、过滤强 | 需要复杂过滤的场景 |
| pgvector | PG 扩展 | 复用现有 PG 基础设施 | 已有 PostgreSQL 的团队 |
---
## 5. 端到端 Pipeline:从文本到检索的完整流程
理解了各个组件后,让我们把它们串起来,看看一个完整的向量检索系统是怎么工作的。
整个流程分为两条线:**离线写入**(把文档变成向量存起来)和**在线查询**(把问题变成向量去搜索)。
<EmbeddingPipelineDemo />
::: tip 离线写入流程
1. **文档加载**:从各种来源(PDF、网页、数据库)读取原始文本
2. **文本预处理**:清洗、去噪、标准化(去掉 HTML 标签、特殊字符等)
3. **文本分块**:按策略将长文本切分为合适大小的片段(200~500 tokens
4. **向量化**:调用嵌入模型(如 OpenAI text-embedding-3-small)将每个片段转为向量
5. **存入向量数据库**:将向量和原始文本、元数据一起写入数据库
:::
::: tip 在线查询流程
1. **接收查询**:用户输入自然语言问题
2. **查询向量化**:用同一个嵌入模型将问题转为向量
3. **相似度检索**:在向量数据库中搜索 Top-K 最相似的文档片段
4. **后处理**:重排序、去重、元数据过滤
5. **返回结果**:将最相关的文档片段返回给调用方(或交给 LLM 生成回答)
:::
| 环节 | 关键选择 | 推荐方案 |
|------|---------|---------|
| 嵌入模型 | 精度 vs 成本 vs 速度 | OpenAI text-embedding-3-small(性价比高) |
| 分块策略 | 粒度 vs 语义完整性 | 递归分块,200~500 tokens |
| 向量数据库 | 规模 vs 运维成本 | 小项目用 Chroma,生产用 Pinecone/Milvus |
| 相似度度量 | 语义 vs 精确 | 余弦相似度(文本场景首选) |
| Top-K 值 | 召回率 vs 噪音 | 先检索 20 条,重排序后取 Top 5 |
---
## 总结
Embedding 与向量检索是连接"人类语言"和"机器理解"的桥梁,也是 RAG、语义搜索、推荐系统等 AI 应用的基础设施。
回顾本章的关键要点:
1. **Embedding 的本质**:把文本映射到高维向量空间,让语义相似度变成空间距离
2. **相似度度量**:余弦相似度关注方向(适合文本),欧氏距离关注绝对距离
3. **索引是性能关键**:HNSW 和 IVF 让百万级向量的检索降到毫秒级
4. **向量数据库选型**:小项目用 Chroma/pgvector,生产环境用 Pinecone/Milvus
5. **端到端思维**:从文档加载到最终检索,每个环节的选择都会影响最终效果
## 延伸阅读
- [OpenAI Embeddings 文档](https://platform.openai.com/docs/guides/embeddings) - 官方嵌入模型使用指南
- [Pinecone Learning Center](https://www.pinecone.io/learn/) - 向量数据库和检索的系统教程
- [FAISS Wiki](https://github.com/facebookresearch/faiss/wiki) - Facebook 开源的向量检索库文档
- [Word2Vec 原始论文](https://arxiv.org/abs/1301.3781) - Embedding 时代的开山之作
- [MTEB 排行榜](https://huggingface.co/spaces/mteb/leaderboard) - 嵌入模型性能对比排行榜
@@ -1,3 +1,173 @@
# 模型微调与部署
> 待实现
::: tip 前言
**大模型很强,但它不懂你的业务。** GPT-4 能写诗、能编程,但它不知道你公司的产品术语、不了解你行业的专业规范。微调(Fine-tuning)就是让通用大模型"学会"你的专业知识的过程——就像给一个博学的通才做岗前培训,让它变成你的领域专家。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **流程认知**:掌握从数据准备到模型上线的完整微调流水线
- **数据工程**:了解微调数据的格式要求和质量标准
- **高效微调**:理解 LoRA 等参数高效微调技术的原理和优势
- **模型压缩**:掌握量化技术如何让大模型在消费级硬件上运行
- **部署实践**:了解模型服务的主流架构和选型策略
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | 微调流水线 | 数据→训练→评估→部署 |
| **第 2 章** | 训练数据 | 数据格式、质量控制 |
| **第 3 章** | LoRA 微调 | 低秩适配、参数高效 |
| **第 4 章** | 模型量化 | FP16、INT8、INT4 |
| **第 5 章** | 模型部署 | 推理服务、API 网关 |
---
## 0. 全景图:为什么需要微调?
大语言模型的训练分为两个阶段:**预训练**和**微调**。预训练是在海量通用数据上学习语言能力,微调是在特定任务数据上学习专业能力。
打个比方:预训练像是上大学——学习通识知识,什么都懂一点;微调像是入职培训——针对具体岗位学习专业技能。
::: tip 什么时候需要微调?
- **特定输出格式**:需要模型始终以固定 JSON 格式输出
- **专业领域知识**:医疗、法律、金融等领域的专业术语和规范
- **语言风格迁移**:让模型用特定的语气、风格回答(如客服话术)
- **小众语言支持**:提升模型在特定语言上的表现
- **成本优化**:用小模型微调替代大模型调用,降低推理成本
:::
---
## 1. 微调流水线:从数据到上线的完整旅程
微调不是"把数据丢给模型就完事"。它是一个严谨的工程流程,每个环节都会影响最终效果。
<FinetuningPipelineDemo />
::: tip 微调的五个阶段
1. **数据准备**:收集、清洗、标注训练数据,这是最耗时也最关键的环节
2. **模型选择**:选择合适的基座模型(Base Model),如 Llama 3、Qwen、Mistral
3. **训练配置**:设置学习率、batch size、epoch 数等超参数
4. **训练执行**:在 GPU 上运行训练,监控 loss 曲线和评估指标
5. **评估上线**:在测试集上评估效果,通过后部署为 API 服务
:::
| 阶段 | 关键动作 | 常见陷阱 |
|------|---------|---------|
| 数据准备 | 清洗、去重、格式化 | 数据质量差导致模型"学坏" |
| 模型选择 | 评估基座模型能力 | 模型太大训练不动,太小效果差 |
| 训练配置 | 调整超参数 | 学习率过高导致灾难性遗忘 |
| 训练执行 | 监控 loss 和指标 | 过拟合、训练不收敛 |
| 评估上线 | A/B 测试、灰度发布 | 测试集泄漏导致评估虚高 |
---
## 2. 训练数据:微调效果的天花板
在微调中有一句老话:**"Garbage in, garbage out"**。训练数据的质量直接决定了微调效果的上限。100 条高质量数据的效果,往往好过 10000 条低质量数据。
<TrainingDataDemo />
::: tip 微调数据的三种常见格式
1. **指令格式(Instruction**:最常用的格式,包含 instruction(指令)、input(输入)、output(期望输出)三个字段。适合训练模型遵循指令。
2. **对话格式(Chat**:多轮对话形式,包含 system、user、assistant 角色的消息列表。适合训练聊天机器人。
3. **补全格式(Completion**:简单的 prompt-completion 对,适合文本生成、代码补全等场景。
:::
| 数据质量维度 | 说明 | 检查方法 |
|------------|------|---------|
| 准确性 | 答案必须正确无误 | 人工审核、专家校验 |
| 一致性 | 相似问题的回答风格一致 | 抽样对比检查 |
| 多样性 | 覆盖足够多的场景和变体 | 统计问题类型分布 |
| 去重 | 避免重复样本导致过拟合 | 文本去重、语义去重 |
| 数据量 | 通常 500~5000 条高质量数据即可 | 从少量开始,逐步增加 |
---
## 3. LoRA:用 1% 的参数实现 90% 的效果
全量微调(Full Fine-tuning)需要更新模型的所有参数——对于一个 70B 参数的模型,这意味着需要数百 GB 的显存和大量的 GPU 算力。对大多数团队来说,这不现实。
LoRALow-Rank Adaptation)提供了一个优雅的解决方案:**冻结原始模型参数,只训练一小组新增的低秩矩阵**。这些矩阵的参数量通常只有原模型的 0.1%~1%,但能达到接近全量微调的效果。
<LoRADemo />
::: tip LoRA 的核心思想
原始模型的权重矩阵 W 是一个巨大的矩阵(如 4096×4096)。LoRA 不直接修改 W,而是在旁边加一个"旁路"W' = W + BA,其中 B 和 A 是两个小矩阵(如 4096×8 和 8×4096)。训练时只更新 B 和 A,原始 W 保持不变。
- **秩(Rank**:r 值越大,表达能力越强,但参数量也越多。通常 r=8~64 就够用
- **合并部署**:训练完成后,可以把 BA 合并回 W,推理时零额外开销
:::
| 微调方式 | 可训练参数 | 显存需求 | 训练速度 | 效果 |
|---------|-----------|---------|---------|------|
| 全量微调 | 100% | 极高 | 慢 | 最好 |
| LoRA | 0.1%~1% | 低 | 快 | 接近全量 |
| QLoRA | 0.1%~1% | 更低 | 中等 | 略低于 LoRA |
| Prompt Tuning | < 0.01% | 极低 | 很快 | 有限 |
---
## 4. 模型量化:让大模型"瘦身"
一个 70B 参数的模型,如果用 FP32(32 位浮点数)存储,需要 280GB 显存——没有几块顶级 GPU 根本跑不起来。量化(Quantization)技术通过降低数值精度来压缩模型体积,让大模型能在消费级硬件上运行。
<ModelQuantizationDemo />
::: tip 量化的核心权衡
量化本质上是**精度换空间**的权衡。FP32 → FP16 几乎无损,INT8 有轻微损失,INT4 会有明显但通常可接受的质量下降。关键是找到你场景下的最佳平衡点。
- **FP16(半精度)**:体积减半,质量几乎无损,是训练和推理的默认选择
- **INT88 位整数)**:体积再减半,质量损失很小,适合大多数推理场景
- **INT44 位整数)**:体积仅为 FP32 的 1/8,质量有一定损失,适合资源受限场景
:::
| 精度 | 每参数字节 | 70B 模型体积 | 质量损失 | 适用场景 |
|------|-----------|-------------|---------|---------|
| FP32 | 4 字节 | ~280 GB | 无 | 训练基准 |
| FP16 | 2 字节 | ~140 GB | 几乎无 | 标准训练和推理 |
| INT8 | 1 字节 | ~70 GB | 很小 | 生产推理 |
| INT4 | 0.5 字节 | ~35 GB | 可接受 | 边缘设备、本地部署 |
---
## 5. 模型部署:从实验室到生产环境
模型训练好了,量化压缩了,最后一步是把它部署成可供调用的服务。模型部署不只是"把模型跑起来",还涉及并发处理、负载均衡、成本控制等工程问题。
<ModelServingDemo />
::: tip 三种主流部署方案
1. **API 服务商**:直接使用 OpenAI、Anthropic 等厂商的 API。零运维,按 token 付费,适合快速验证和中小规模使用。
2. **自托管推理服务**:用 vLLM、TGI 等框架在自己的 GPU 服务器上部署。成本可控,数据不出域,适合有隐私要求或大规模调用的场景。
3. **Serverless 推理**:使用 AWS SageMaker、Replicate 等平台,按请求付费,自动扩缩容。适合流量波动大的场景。
:::
| 部署方案 | 成本模型 | 延迟 | 运维复杂度 | 适用场景 |
|---------|---------|------|-----------|---------|
| API 服务商 | 按 token 计费 | 中等 | 零 | 快速原型、中小规模 |
| vLLM 自部署 | GPU 租赁费用 | 低 | 高 | 大规模、隐私敏感 |
| Serverless | 按请求计费 | 冷启动较高 | 低 | 流量波动大 |
| 边缘部署 | 硬件一次性投入 | 极低 | 中 | 离线场景、IoT |
---
## 总结
模型微调与部署是让大模型从"通用工具"变成"专业助手"的关键环节。从数据准备到模型上线,每一步都需要工程化的思维和实践。
回顾本章的关键要点:
1. **微调是岗前培训**:让通用模型学会特定领域的知识和行为模式
2. **数据质量决定上限**:100 条高质量数据胜过 10000 条低质量数据
3. **LoRA 是效率之王**:用不到 1% 的参数实现接近全量微调的效果
4. **量化是部署利器**:INT4 量化让 70B 模型在单卡上运行成为可能
5. **部署方案因地制宜**:快速验证用 API,大规模用自部署,波动大用 Serverless
## 延伸阅读
- [Hugging Face PEFT 文档](https://huggingface.co/docs/peft) - 参数高效微调库官方文档
- [vLLM 文档](https://docs.vllm.ai/) - 高性能 LLM 推理引擎
- [Unsloth](https://github.com/unslothai/unsloth) - 2x 加速的 LoRA 微调框架
- [GGUF 格式说明](https://github.com/ggerganov/ggml/blob/master/docs/gguf.md) - llama.cpp 使用的量化模型格式
- [OpenAI Fine-tuning Guide](https://platform.openai.com/docs/guides/fine-tuning) - OpenAI 官方微调指南
@@ -1,3 +1,161 @@
# RAG 架构
# RAG:检索增强生成
> 待实现
::: tip 前言
**为什么 ChatGPT 有时候会"一本正经地胡说八道"?** 大语言模型的知识来自训练数据,但训练数据有截止日期,也不包含你公司的内部文档。RAGRetrieval-Augmented Generation,检索增强生成)就是解决这个问题的核心技术——让 AI 在回答之前,先去"查资料"。
:::
**这篇文章会带你学什么?**
学完这章后,你将获得:
- **核心概念理解**:明白 RAG 是什么、为什么需要它,以及它如何解决大模型的"幻觉"问题
- **完整流程认知**:掌握从文档加载、分块、向量化到检索、生成的端到端流程
- **技术选型能力**:了解不同分块策略、检索方法的优劣,能根据场景做出选择
- **架构演进视角**:理解 RAG 从 Naive 到 Advanced 再到 Modular 的演进路线
- **实践决策能力**:知道什么时候该用 RAG、什么时候该用微调
| 章节 | 内容 | 核心概念 |
|-----|------|---------|
| **第 1 章** | RAG 基础流程 | 索引、检索、生成三阶段 |
| **第 2 章** | 文本分块策略 | 固定分块、语义分块、递归分块 |
| **第 3 章** | 检索技术 | 向量检索、关键词检索、混合检索 |
| **第 4 章** | 架构演进 | Naive RAG → Advanced RAG → Modular RAG |
| **第 5 章** | RAG vs 微调 | 两种方案的适用场景对比 |
---
## 0. 全景图:为什么大模型需要"查资料"?
想象你是一个博学的教授,读过无数书籍。但如果有人问你"昨天公司的销售数据是多少",你肯定答不上来——因为这些信息不在你读过的书里。
大语言模型面临的就是同样的困境:
- **知识有截止日期**:GPT-4 的训练数据截止到某个时间点,之后发生的事它不知道
- **缺乏私有知识**:你公司的内部文档、产品手册、客户数据,模型从未见过
- **容易产生幻觉**:当模型不确定答案时,它倾向于"编造"一个看起来合理的回答
::: tip RAG 的核心思想
RAG 的解决方案非常直觉:**在让模型回答之前,先帮它找到相关的参考资料**。就像开卷考试——你不需要记住所有知识,只需要知道去哪里找、怎么找。
RAG = 检索(Retrieval+ 增强(Augmented+ 生成(Generation
:::
---
## 1. RAG 基础流程:索引、检索、生成
RAG 的工作流程可以分为两个阶段:**离线索引**和**在线查询**。
离线阶段就像图书馆的编目工作——把所有书籍分类、编号、上架,方便日后查找。在线阶段则是读者来图书馆查资料的过程——根据问题找到相关书籍,然后综合信息给出回答。
<RAGPipelineDemo />
::: tip 三个核心阶段
1. **索引阶段(Indexing**:将原始文档加载、清洗、分块,然后通过嵌入模型转化为向量,存入向量数据库。这是一次性的准备工作。
2. **检索阶段(Retrieval**:用户提问时,将问题也转化为向量,在向量数据库中搜索最相似的文档片段。
3. **生成阶段(Generation**:将检索到的文档片段和用户问题一起拼接为 Prompt,交给大模型生成最终回答。
:::
| 阶段 | 输入 | 输出 | 关键技术 |
|------|------|------|---------|
| 索引 | 原始文档 | 向量数据库 | 文本分块、嵌入模型 |
| 检索 | 用户问题 | Top-K 文档片段 | 向量相似度、重排序 |
| 生成 | 问题 + 上下文 | 最终回答 | Prompt 工程、LLM |
---
## 2. 文本分块:把大象装进冰箱
文本分块是 RAG 中最容易被忽视、却对效果影响最大的环节。为什么需要分块?因为大模型的上下文窗口有限,我们不可能把整本书塞进去。更重要的是,**分块的质量直接决定了检索的质量**。
想象你在图书馆找一本书的某个知识点。如果整本书是一个"块",检索到了也没用——你还是得翻遍全书。但如果按章节甚至段落分块,就能精准定位到你需要的内容。
<ChunkingStrategyDemo />
::: tip 分块策略的选择
- **固定大小分块**:按字符数或 token 数切分,简单粗暴但可能切断语义
- **递归分块**:先按段落分,段落太长再按句子分,保持语义完整性
- **语义分块**:用嵌入模型判断语义边界,相似度突变处切分
- **文档结构分块**:利用 Markdown 标题、HTML 标签等结构信息分块
没有"最好"的分块策略,只有最适合你数据的策略。一般建议从递归分块开始,chunk 大小 200-500 tokensoverlap 10-20%。
:::
---
## 3. 检索技术:如何找到最相关的内容?
分块完成后,下一个关键问题是:**用户提了一个问题,怎么从成千上万个文档片段中找到最相关的那几个?**
这就像在一个巨大的图书馆里找书。你可以按书名关键词搜索(关键词检索),也可以描述你想要的内容让图书管理员帮你找(语义检索),最好的方式是两者结合(混合检索)。
<RetrievalDemo />
| 检索方式 | 原理 | 优势 | 劣势 |
|---------|------|------|------|
| 关键词检索(BM25) | 基于词频和逆文档频率 | 精确匹配、速度快 | 无法理解语义、同义词失效 |
| 向量检索 | 基于嵌入向量的余弦相似度 | 理解语义、支持模糊匹配 | 对专有名词不敏感 |
| 混合检索 | 融合关键词和向量检索结果 | 兼顾精确和语义 | 需要调权重、复杂度高 |
::: tip 重排序(Reranking
检索到候选文档后,通常还需要一步"重排序"。初始检索追求召回率(尽量不遗漏),重排序追求精确率(把最相关的排到最前面)。常用的重排序模型有 Cohere Rerank、BGE Reranker 等,它们使用交叉编码器对 query-document 对进行精细打分。
:::
---
## 4. 架构演进:从简单到智能
RAG 技术在短短两年内经历了三代演进,每一代都在解决上一代的痛点。
<RAGArchitectureDemo />
::: tip 三代 RAG 架构对比
- **Naive RAG2023**:最基础的"索引→检索→生成"流程,实现简单但效果有限。问题包括:检索质量不稳定、无法处理复杂查询、容易引入噪音上下文。
- **Advanced RAG2024**:在 Naive RAG 基础上增加了查询改写、混合检索、重排序、上下文压缩等优化环节,显著提升了检索精度和生成质量。
- **Modular RAG2025**:将 RAG 拆解为可插拔的模块,支持路由判断、自适应检索、自我反思等高级能力。可根据查询类型动态选择最优处理流程。
:::
---
## 5. RAG vs 微调:该选哪个?
当你想让大模型掌握特定领域的知识时,通常有两条路:RAG 和微调(Fine-tuning)。它们不是互斥的,而是互补的。
打个比方:**微调像是让学生上培训班**,把知识内化到大脑里;**RAG 像是给学生发参考书**,考试时可以翻阅。两种方式各有优劣,关键看你的具体需求。
<RAGvsFineTuningDemo />
| 维度 | RAG | 微调 |
|------|-----|------|
| 知识更新 | 实时更新,改文档即可 | 需要重新训练 |
| 成本 | 低(无需 GPU 训练) | 高(需要训练资源) |
| 可解释性 | 高(可追溯来源) | 低(知识内化在权重中) |
| 适用场景 | 知识库问答、文档检索 | 风格迁移、特定任务优化 |
| 幻觉控制 | 较好(有参考依据) | 一般(仍可能幻觉) |
::: tip 实践建议
大多数场景下,**先试 RAG**。RAG 的优势在于:不需要训练、知识可实时更新、回答可追溯来源。只有当你需要改变模型的"行为模式"(比如输出格式、语言风格、推理方式)时,才考虑微调。最强的方案往往是 **RAG + 微调** 的组合。
:::
---
## 总结
RAG 是当前让大模型"落地"最实用的技术之一。它的核心价值在于:让模型的回答有据可查、知识可实时更新、幻觉可有效控制。
回顾本章的关键要点:
1. **RAG 解决的核心问题**:大模型知识过时、缺乏私有数据、容易幻觉
2. **三阶段流程**:索引(离线准备)→ 检索(在线查找)→ 生成(综合回答)
3. **分块是基础**:分块质量直接决定检索质量,选择合适的分块策略至关重要
4. **检索是关键**:混合检索 + 重排序是目前效果最好的组合
5. **架构在演进**:从 Naive RAG 到 Modular RAG,系统越来越智能和灵活
6. **RAG 和微调互补**:大多数场景先试 RAG,需要改变模型行为时再考虑微调
## 延伸阅读
- [LangChain RAG 教程](https://python.langchain.com/docs/tutorials/rag/) - 最流行的 RAG 框架实战指南
- [LlamaIndex 文档](https://docs.llamaindex.ai/) - 专注于 RAG 的框架,提供丰富的数据连接器
- [RAG Survey 论文](https://arxiv.org/abs/2312.10997) - 全面的 RAG 技术综述
- [Chunking Strategies](https://www.pinecone.io/learn/chunking-strategies/) - Pinecone 的分块策略详解
- [向量数据库对比](https://superlinked.com/vector-db-comparison) - 主流向量数据库的功能对比
@@ -214,7 +214,55 @@ function getPayAmount(employee) {
---
## 5. 总结
## 5. AI 助力:用大模型提升代码质量
大模型在代码质量领域已经非常实用,它可以充当你的"24 小时在线的代码审查员"。
### 5.1 识别代码坏味道
> **提示词**
> ```
> 请审查以下代码,识别其中的代码坏味道(Code Smell),包括但不限于:
> 过长函数、魔法数字、重复代码、过深嵌套、过长参数列表。
> 对每个问题给出具体位置、问题描述和改进建议。
>
> [粘贴你的代码]
> ```
### 5.2 自动重构
> **提示词**
> ```
> 请对以下代码进行重构,要求:
> 1. 不改变外部行为
> 2. 使用提炼函数、卫语句替代嵌套等手法
> 3. 改善命名,消除魔法数字
> 4. 解释每一步重构的理由
>
> [粘贴你的代码]
> ```
### 5.3 模拟 Code Review
> **提示词**
> ```
> 请以资深开发者的视角审查这段代码,从以下维度给出反馈:
> - 正确性:逻辑是否有 Bug?边界条件是否处理?
> - 可读性:命名是否清晰?结构是否易懂?
> - 性能:是否有明显的性能问题?
> - 安全性:是否有注入或数据泄露风险?
> 用"建议"而非"命令"的语气,给出改进方案。
>
> [粘贴你的代码]
> ```
::: tip AI 使用建议
AI 的重构建议需要你自己验证——跑测试确认行为没变。把 AI 当作"提建议的同事",而不是"无条件信任的权威"。
:::
---
## 6. 总结
回顾这一路,我们从识别问题到解决问题,建立了一套完整的代码质量改进体系:
@@ -207,7 +207,55 @@ calculatePrice(100, 'svip') // 60
---
## 5. 总结
## 5. AI 助力:用大模型学习和应用设计模式
大模型可以帮你识别代码中适合使用设计模式的场景,并给出具体的重构方案。
### 5.1 识别适用模式
> **提示词**
> ```
> 分析以下代码,判断是否存在可以用设计模式改进的地方。
> 如果有,请说明:
> 1. 当前代码的问题
> 2. 推荐使用哪种设计模式
> 3. 重构后的代码示例
> 4. 为什么这个模式适合这个场景
>
> [粘贴你的代码]
> ```
### 5.2 用具体场景学习模式
> **提示词**
> ```
> 用一个"外卖点餐系统"的真实场景,分别演示以下设计模式的应用:
> - 工厂模式:创建不同类型的订单
> - 观察者模式:订单状态变化通知
> - 策略模式:不同的配送费计算规则
>
> 用 JavaScript 代码示例,每个模式先展示不用模式的问题,
> 再展示用模式后的改进。
> ```
### 5.3 判断是否过度设计
> **提示词**
> ```
> 审查以下代码,判断是否存在过度设计的问题。
> 是否有不必要的抽象、用不到的设计模式、或过早的优化?
> 如果有,请建议如何简化,遵循 KISS 原则。
>
> [粘贴你的代码]
> ```
::: tip AI 使用建议
让 AI 用你熟悉的业务场景来解释设计模式,比看抽象的 UML 图有效得多。但记住:AI 可能倾向于推荐更复杂的方案,你需要自己判断是否真的需要。
:::
---
## 6. 总结
1. **创建型模式**:解决"如何创建对象"的问题,让创建过程更灵活
2. **结构型模式**:解决"如何组织代码"的问题,让结构更清晰
@@ -142,7 +142,55 @@ git commit -m "fix: 修复 README 中的安装命令拼写错误"
---
## 5. 总结
## 5. AI 助力:用大模型加速开源贡献
大模型可以帮你快速理解陌生代码库、写出高质量的 PR 描述、甚至辅助 Code Review。
### 5.1 快速理解陌生代码库
> **提示词**
> ```
> 我刚 clone 了一个开源项目,请帮我分析以下目录结构,
> 说明每个目录/文件的职责,以及代码的整体架构和数据流向。
> 我想修复一个登录相关的 Bug,应该从哪里开始看?
>
> [粘贴 tree 命令输出或目录结构]
> ```
### 5.2 写 PR 描述
> **提示词**
> ```
> 根据以下 git diff,帮我写一份 Pull Request 描述,包括:
> - 标题(简洁,说明改了什么)
> - 改动说明(为什么改、改了什么)
> - 测试方法(如何验证改动正确)
> - 关联 Issue(如果有)
> 用英文撰写,语气专业友好。
>
> [粘贴 git diff 输出]
> ```
### 5.3 辅助翻译文档
> **提示词**
> ```
> 将以下中文技术文档翻译为英文,要求:
> 1. 技术术语使用业界通用的英文表达
> 2. 代码注释和变量名不翻译
> 3. 保持 Markdown 格式不变
> 4. 语气自然流畅,不要机翻感
>
> [粘贴中文文档]
> ```
::: tip AI 使用建议
用 AI 写 PR 描述时,确保你自己理解了每一行改动。审查者可能会问你为什么这么改——如果你答不上来,说明你还没真正理解。
:::
---
## 6. 总结
1. **流程**Fork → Branch → Commit → PR → Review → Merge
2. **许可证**:MIT 最宽松,GPL 最严格,根据需求选择
@@ -148,7 +148,55 @@ Strict-Transport-Security: max-age=31536000
---
## 4. 总结
## 4. AI 助力:用大模型提升安全防护
大模型可以充当你的"安全顾问",帮你审计代码漏洞、生成安全方案。
### 4.1 代码安全审计
> **提示词**
> ```
> 请对以下代码进行安全审计,检查是否存在:
> - XSS 漏洞(未转义的用户输入)
> - SQL 注入(字符串拼接查询)
> - CSRF 风险(缺少 Token 验证)
> - 敏感数据泄露(硬编码密钥、明文密码)
> 对每个问题给出风险等级、具体位置和修复方案。
>
> [粘贴你的代码]
> ```
### 4.2 生成安全配置
> **提示词**
> ```
> 我的项目使用 Express.js + PostgreSQL,即将部署上线。
> 请生成一份完整的安全配置清单,包括:
> - HTTP 安全头配置代码
> - CORS 配置
> - 数据库连接的安全设置
> - 环境变量管理方案
> 给出可直接使用的代码片段。
> ```
### 4.3 解释漏洞原理
> **提示词**
> ```
> 用一个具体的例子,解释 CSRF 攻击的完整流程:
> 1. 攻击者如何构造恶意页面
> 2. 为什么浏览器会自动携带 Cookie
> 3. 服务端如何用 CSRF Token 防御
> 用代码演示攻击和防御的完整过程。
> ```
::: tip AI 使用建议
AI 的安全审计不能替代专业的安全测试。把它当作第一道筛查,关键系统仍需专业安全团队审计。
:::
---
## 5. 总结
1. **安全思维**:永不信任外部输入,最小权限,纵深防御
2. **常见攻击**XSS、SQL 注入、CSRF 是最高频的 Web 安全威胁
@@ -151,7 +151,58 @@ for (let i = items.length - 1; i >= 0; i--) { ... }
---
## 5. 总结
## 5. AI 助力:用大模型提升文档质量
大模型在技术写作领域几乎是"天赋异禀"——生成文档、改善表达、翻译内容都是它的强项。
### 5.1 生成 API 文档
> **提示词**
> ```
> 根据以下 Express 路由代码,生成完整的 API 文档,包括:
> - 端点路径和方法
> - 请求参数(路径参数、查询参数、请求体)及类型
> - 成功和错误的响应示例
> - 使用 curl 的调用示例
>
> [粘贴你的路由代码]
> ```
### 5.2 改善技术写作
> **提示词**
> ```
> 请改善以下技术文档的表达,要求:
> 1. 语言简洁清晰,去掉冗余表述
> 2. 用主动语态替代被动语态
> 3. 专业术语保持准确
> 4. 添加必要的代码示例
> 保持原意不变,只改善表达质量。
>
> [粘贴你的文档内容]
> ```
### 5.3 生成 README
> **提示词**
> ```
> 根据以下项目信息,生成一份高质量的 README.md:
> - 项目名称:[名称]
> - 一句话描述:[描述]
> - 技术栈:[列出]
> - 核心功能:[列出]
>
> 要求包含:项目简介、快速开始、功能特性、
> 安装步骤(含代码)、使用示例、贡献指南、许可证。
> ```
::: tip AI 使用建议
AI 生成的文档要检查技术细节是否准确——它可能编造不存在的 API 参数或错误的返回值。始终对照实际代码验证。
:::
---
## 6. 总结
1. **类型匹配**:不同文档有不同的结构和写法
2. **清晰优先**:具体、准确、面向读者
@@ -119,7 +119,55 @@
---
## 4. 总结
## 4. AI 助力:用大模型辅助技术选型
大模型可以帮你快速调研技术方案、对比优劣、生成决策报告。
### 4.1 技术方案对比
> **提示词**
> ```
> 我需要为一个电商项目选择数据库,候选方案:
> MySQL、PostgreSQL、MongoDB。
> 项目特点:读多写少、需要复杂查询、数据量预计千万级。
>
> 请从以下维度对比三个方案:
> 性能、生态、学习曲线、运维成本、扩展性。
> 用表格形式呈现,并给出最终推荐和理由。
> ```
### 4.2 生成架构决策记录(ADR)
> **提示词**
> ```
> 帮我写一份架构决策记录(ADR),格式如下:
> - 标题:选择 Vue 3 作为前端框架
> - 背景:[项目背景和需求]
> - 候选方案:React, Vue 3, Svelte
> - 决策:Vue 3
> - 理由:[基于团队能力、生态、性能等维度]
> - 后果:[选择后的影响和风险]
> ```
### 4.3 调研新技术
> **提示词**
> ```
> 我在考虑是否在项目中引入 Bun 替代 Node.js,请帮我分析:
> 1. Bun 相比 Node.js 的核心优势和劣势
> 2. 当前生态成熟度(npm 兼容性、主流框架支持)
> 3. 生产环境使用的风险点
> 4. 适合和不适合使用 Bun 的场景
> 给出客观评估,不要只说优点。
> ```
::: tip AI 使用建议
AI 的知识有时效性——它可能不了解最新版本的变化。对于快速迭代的技术,用 AI 做初步调研后,务必查阅官方文档确认最新信息。
:::
---
## 5. 总结
1. **技术雷达**:了解技术的成熟度,区分采纳/试验/评估/暂缓
2. **选型维度**:团队能力 > 社区生态 > 性能需求 > 维护状态
@@ -149,7 +149,53 @@ TDD 适合逻辑密集的代码(算法、业务规则、数据转换),但
---
## 5. 总结
## 5. AI 助力:用大模型提升测试效率
大模型在测试领域的能力已经非常强大——它可以帮你生成测试用例、发现边界条件、甚至写出完整的测试代码。
### 5.1 生成单元测试
> **提示词**
> ```
> 请为以下函数编写单元测试,使用 Vitest 框架,要求:
> 1. 遵循 AAA 模式(Arrange-Act-Assert
> 2. 覆盖正常路径、边界条件和错误路径
> 3. 每个测试用例有清晰的中文描述
>
> [粘贴你的函数代码]
> ```
### 5.2 发现边界条件
> **提示词**
> ```
> 分析以下函数,列出所有可能的边界条件和极端输入场景,
> 包括:空值、零、负数、超大数、特殊字符、并发情况等。
> 对每个场景说明预期行为和可能的风险。
>
> [粘贴你的函数代码]
> ```
### 5.3 从需求生成测试(TDD 辅助)
> **提示词**
> ```
> 我要实现一个购物车模块,需求如下:
> - 添加商品、删除商品、修改数量
> - 自动计算总价(含折扣)
> - 库存不足时提示错误
>
> 请按照 TDD 思路,先写出测试用例(不写实现),
> 使用 Vitest,覆盖所有核心场景。
> ```
::: tip AI 使用建议
AI 生成的测试要检查断言是否有意义——避免 `expect(true).toBe(true)` 这种无效测试。好的测试应该在代码出错时真的能失败。
:::
---
## 6. 总结
1. **测试金字塔**:底层多、顶层少,平衡速度与真实度
2. **单元测试**:遵循 FIRST 原则和 AAA 模式,测试核心逻辑
+5
View File
@@ -8,6 +8,11 @@
从晶体管到操作系统,深入了解计算机如何工作:
<NavGrid>
<NavCard
href="/zh-cn/appendix/1-computer-fundamentals/vibe-coding-fullstack"
title="Vibe Coding 时代下的全栈开发"
description="AI 辅助时代,前端、后端、编程语言、全栈工程师的成长路径全景图"
/>
<NavCard
href="/zh-cn/appendix/1-computer-fundamentals/transistor-to-cpu"
title="从晶体管到 CPU"