docs: update Chinese documentation and add Vue components
- Update AI capability dictionary by removing redundant mention of Baidu's model - Add new Vue components for context engineering visualization (IntroProblemReasonSolution, MemoryPalaceDemo, MemoryPalaceActionDemo, KVCacheDemo, LostInMiddleDemo) - Register new components in theme index.js - Enhance audio introduction with new interactive demos (AudioQuickStartDemo, MelSpectrogramDemo, TTSPipelineDemo, VoiceCloningDemo, ASRvsTTSDemo, AudioTokenizationDemo, EmotionControlDemo) - Improve existing context engineering demos with Chinese localization and better tokenization - Fix Japanese documentation layout by properly closing NavGrid components
This commit is contained in:
+131
-465
@@ -1,531 +1,197 @@
|
||||
<!--
|
||||
* Component: AgentContextFlow.vue
|
||||
* Description: Visualizes the data flow in an agentic system, showing how context is built, used, and updated during interactions.
|
||||
* Features:
|
||||
* - Step-by-step visualization of the Agent Loop (Input -> Context -> Decision -> Action -> Observation -> Update)
|
||||
* - Animation of data flowing between components
|
||||
* - Metrics display for context usage and cache hits
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const steps = [
|
||||
{ id: 'input', label: '用户输入', icon: '👤', desc: '用户提出问题或指令' },
|
||||
{ id: 'context', label: '构建上下文', icon: '📚', desc: '检索历史消息与相关知识' },
|
||||
{ id: 'reasoning', label: '模型推理', icon: '🧠', desc: 'LLM 分析意图并规划行动' },
|
||||
{ id: 'action', label: '工具调用', icon: '🔧', desc: '执行外部工具或 API' },
|
||||
{ id: 'observation', label: '观察结果', icon: '👁️', desc: '获取工具执行的返回结果' },
|
||||
{ id: 'update', label: '更新上下文', icon: '📝', desc: '将结果追加到记忆中' }
|
||||
]
|
||||
const round = ref(1)
|
||||
const minRound = 1
|
||||
const maxRound = 5
|
||||
|
||||
const currentStepIndex = ref(-1)
|
||||
const isAutoPlaying = ref(false)
|
||||
const iteration = ref(1)
|
||||
const contextTokens = ref(120)
|
||||
const cacheHitRate = ref(0)
|
||||
const autoPlayInterval = ref(null)
|
||||
const contextTokens = computed(() => 120 + (round.value - 1) * 80)
|
||||
|
||||
// Simulation data
|
||||
const currentScenario = computed(() => {
|
||||
const scenarios = [
|
||||
{ input: "查询北京天气", action: "WeatherAPI.get('Beijing')", result: "晴, 25°C", response: "北京今天晴,气温25度。" },
|
||||
{ input: "计算 123 * 456", action: "Calculator.mul(123, 456)", result: "56088", response: "结果是 56088。" },
|
||||
{ input: "搜索最新的 AI 新闻", action: "Search.query('AI news')", result: "Found 5 articles...", response: "最近的 AI 新闻包括..." }
|
||||
]
|
||||
return scenarios[(iteration.value - 1) % scenarios.length]
|
||||
const cacheHitRate = computed(() =>
|
||||
round.value === 1 ? 0 : Math.min(80, (round.value - 1) * 20)
|
||||
)
|
||||
|
||||
const baseCostPerRound = 0.025
|
||||
|
||||
const currentCost = computed(() => {
|
||||
const rate = cacheHitRate.value / 100
|
||||
const cost = baseCostPerRound * (1 - rate * 0.9)
|
||||
return cost.toFixed(4)
|
||||
})
|
||||
|
||||
const currentStep = computed(() => {
|
||||
if (currentStepIndex.value === -1) return null
|
||||
return steps[currentStepIndex.value]
|
||||
const savedPercent = computed(() => {
|
||||
const cost = Number(currentCost.value)
|
||||
const saved = ((baseCostPerRound - cost) / baseCostPerRound) * 100
|
||||
return saved.toFixed(1)
|
||||
})
|
||||
|
||||
const progress = computed(() => {
|
||||
if (currentStepIndex.value === -1) return 0
|
||||
return ((currentStepIndex.value + 1) / steps.length) * 100
|
||||
})
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStepIndex.value < steps.length - 1) {
|
||||
currentStepIndex.value++
|
||||
// Update metrics based on step
|
||||
if (steps[currentStepIndex.value].id === 'context') {
|
||||
contextTokens.value += 50
|
||||
} else if (steps[currentStepIndex.value].id === 'update') {
|
||||
contextTokens.value += 30
|
||||
cacheHitRate.value = Math.min(95, cacheHitRate.value + 15)
|
||||
}
|
||||
} else {
|
||||
// Loop finished, prepare next iteration
|
||||
currentStepIndex.value = -1
|
||||
iteration.value++
|
||||
setTimeout(() => {
|
||||
if (isAutoPlaying.value) nextStep()
|
||||
}, 500)
|
||||
}
|
||||
const increaseRound = () => {
|
||||
if (round.value < maxRound) round.value += 1
|
||||
}
|
||||
|
||||
const toggleAutoPlay = () => {
|
||||
isAutoPlaying.value = !isAutoPlaying.value
|
||||
if (isAutoPlaying.value) {
|
||||
if (currentStepIndex.value === steps.length - 1) {
|
||||
currentStepIndex.value = -1
|
||||
iteration.value++
|
||||
}
|
||||
runAutoPlay()
|
||||
} else {
|
||||
clearTimeout(autoPlayInterval.value)
|
||||
}
|
||||
const decreaseRound = () => {
|
||||
if (round.value > minRound) round.value -= 1
|
||||
}
|
||||
|
||||
const runAutoPlay = () => {
|
||||
if (!isAutoPlaying.value) return
|
||||
|
||||
nextStep()
|
||||
|
||||
// Determine delay based on current step
|
||||
const delay = currentStepIndex.value === -1 ? 500 : 1500
|
||||
|
||||
autoPlayInterval.value = setTimeout(() => {
|
||||
runAutoPlay()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
isAutoPlaying.value = false
|
||||
clearTimeout(autoPlayInterval.value)
|
||||
currentStepIndex.value = -1
|
||||
iteration.value = 1
|
||||
contextTokens.value = 120
|
||||
cacheHitRate.value = 0
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
clearTimeout(autoPlayInterval.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="agent-context-flow">
|
||||
<!-- Control Panel -->
|
||||
<div class="control-panel">
|
||||
<div class="controls-left">
|
||||
<button
|
||||
class="action-btn primary"
|
||||
@click="toggleAutoPlay"
|
||||
>
|
||||
{{ isAutoPlaying ? '⏸ 暂停' : '▶ 自动运行' }}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn secondary"
|
||||
@click="nextStep"
|
||||
:disabled="isAutoPlaying || currentStepIndex === steps.length - 1"
|
||||
>
|
||||
下一步 ➝
|
||||
</button>
|
||||
<button
|
||||
class="action-btn text"
|
||||
@click="reset"
|
||||
>
|
||||
重置
|
||||
</button>
|
||||
<div class="agent-context-intro">
|
||||
<div class="header">
|
||||
<h3>三个关键数字:轮次、上下文长度、缓存命中率</h3>
|
||||
<p>拖动轮次,看看这三个数字是怎么一起变化的。</p>
|
||||
</div>
|
||||
|
||||
<div class="round-control">
|
||||
<button class="round-btn" @click="decreaseRound" :disabled="round === minRound">
|
||||
-
|
||||
</button>
|
||||
<div class="round-text">
|
||||
当前假设:我们已经聊到了
|
||||
<strong>第 {{ round }} 轮</strong>。拖动右侧滑块,看看聊多几轮之后,黑板会写满到什么程度,背课文本比例会涨到多高。
|
||||
</div>
|
||||
<div class="status-indicator">
|
||||
<span class="status-dot" :class="{ active: isAutoPlaying }"></span>
|
||||
{{ isAutoPlaying ? '运行中' : '等待中' }}
|
||||
<input
|
||||
class="round-slider"
|
||||
type="range"
|
||||
:min="minRound"
|
||||
:max="maxRound"
|
||||
v-model.number="round"
|
||||
/>
|
||||
<button class="round-btn" @click="increaseRound" :disabled="round === maxRound">
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="metrics-row">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">聊了几轮</div>
|
||||
<div class="metric-value">第 {{ round }} 轮</div>
|
||||
<div class="metric-desc">对话轮次</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">记了多少字</div>
|
||||
<div class="metric-value">{{ contextTokens }}</div>
|
||||
<div class="metric-desc">大致对应 token 数</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">背课文本比例</div>
|
||||
<div class="metric-value">{{ cacheHitRate }}%</div>
|
||||
<div class="metric-desc">前缀复用比例</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">这轮大概多少钱</div>
|
||||
<div class="metric-value">${{ currentCost }}</div>
|
||||
<div class="metric-desc">比不做优化便宜了 {{ savedPercent }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Visualization Area -->
|
||||
<div class="visualization-area">
|
||||
<!-- Central Flow Diagram -->
|
||||
<div class="flow-container">
|
||||
<div
|
||||
v-for="(step, index) in steps"
|
||||
:key="step.id"
|
||||
class="flow-step"
|
||||
:class="{
|
||||
active: index === currentStepIndex,
|
||||
completed: index < currentStepIndex,
|
||||
pending: index > currentStepIndex
|
||||
}"
|
||||
>
|
||||
<div class="step-connector" v-if="index > 0"></div>
|
||||
<div class="step-node">
|
||||
<div class="step-icon">{{ step.icon }}</div>
|
||||
<div class="step-label">{{ step.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detail View -->
|
||||
<div class="detail-view">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="currentStep" :key="currentStep.id" class="step-detail">
|
||||
<div class="detail-header">
|
||||
<h3>{{ currentStep.icon }} {{ currentStep.label }}</h3>
|
||||
<p>{{ currentStep.desc }}</p>
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="scenario-info" v-if="currentStep.id === 'input'">
|
||||
<strong>输入:</strong> {{ currentScenario.input }}
|
||||
</div>
|
||||
<div class="scenario-info" v-else-if="currentStep.id === 'action'">
|
||||
<strong>执行:</strong> <code>{{ currentScenario.action }}</code>
|
||||
</div>
|
||||
<div class="scenario-info" v-else-if="currentStep.id === 'observation'">
|
||||
<strong>结果:</strong> {{ currentScenario.result }}
|
||||
</div>
|
||||
<div class="scenario-info" v-else-if="currentStep.id === 'update'">
|
||||
上下文已更新,准备下一轮对话。
|
||||
</div>
|
||||
<div class="scenario-info" v-else>
|
||||
正在处理...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="step-detail placeholder">
|
||||
<div class="empty-state">
|
||||
<span class="empty-icon">👋</span>
|
||||
<p>点击"自动运行"或"下一步"开始 Agent 流程演示</p>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics/Info Section -->
|
||||
<div class="metrics-panel">
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">迭代轮次</div>
|
||||
<div class="metric-value">#{{ iteration }}</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">上下文长度</div>
|
||||
<div class="metric-value">{{ contextTokens }} tokens</div>
|
||||
</div>
|
||||
<div class="metric-item">
|
||||
<div class="metric-label">KV 缓存命中</div>
|
||||
<div class="metric-value highlight">{{ cacheHitRate }}%</div>
|
||||
</div>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" :style="{ width: `${progress}%` }"></div>
|
||||
</div>
|
||||
<div class="summary-line">
|
||||
参考基准:一轮完全不做优化大约 {{ baseCostPerRound.toFixed(4) }} 美元。
|
||||
在当前轮次下,通过复用前缀,这轮的成本约为 {{ currentCost }} 美元。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.agent-context-flow {
|
||||
.agent-context-intro {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Control Panel */
|
||||
.control-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.controls-left {
|
||||
.header {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.header h3 {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.round-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
.round-btn {
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid transparent;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
.action-btn.primary:hover {
|
||||
background-color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
color: var(--vp-c-text-1);
|
||||
border-color: var(--vp-c-divider);
|
||||
}
|
||||
.action-btn.secondary:hover:not(:disabled) {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.action-btn.secondary:disabled {
|
||||
opacity: 0.5;
|
||||
.round-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-btn.text {
|
||||
background: none;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.action-btn.text:hover {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vp-c-divider);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.status-dot.active {
|
||||
background-color: var(--vp-c-green);
|
||||
box-shadow: 0 0 4px var(--vp-c-green);
|
||||
}
|
||||
|
||||
/* Visualization Area */
|
||||
.visualization-area {
|
||||
padding: 2rem 1rem;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.round-text {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.step-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: 500;
|
||||
transition: color 0.3s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: -50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: var(--vp-c-divider);
|
||||
z-index: 1;
|
||||
transform: translateY(-50%);
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Active State */
|
||||
.flow-step.active .step-icon {
|
||||
background-color: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px var(--vp-c-brand-dimm);
|
||||
}
|
||||
.flow-step.active .step-label {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Completed State */
|
||||
.flow-step.completed .step-icon {
|
||||
background-color: var(--vp-c-brand-dimm);
|
||||
border-color: var(--vp-c-brand);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
.flow-step.completed .step-connector {
|
||||
background-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
/* Detail View */
|
||||
.detail-view {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-height: 120px;
|
||||
background-color: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.detail-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--vp-c-text-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.detail-header p {
|
||||
margin: 0.25rem 0 0.75rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
.round-slider {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.scenario-info code {
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-family: var(--vp-font-mono);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--vp-c-text-3);
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
.empty-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
/* Metrics Panel */
|
||||
.metrics-panel {
|
||||
.metrics-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
position: relative;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
.metric-card {
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.2rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-mono);
|
||||
}
|
||||
.metric-value.highlight {
|
||||
color: var(--vp-c-brand);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: transparent;
|
||||
.metric-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-brand);
|
||||
transition: width 0.3s linear;
|
||||
.summary-line {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.flow-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.step-connector {
|
||||
width: 2px;
|
||||
height: 20px;
|
||||
top: -20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.flow-step {
|
||||
width: 100%;
|
||||
min-width: unset;
|
||||
}
|
||||
.controls-left span {
|
||||
display: none;
|
||||
@media (max-width: 768px) {
|
||||
.metrics-row {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+175
-180
@@ -1,10 +1,6 @@
|
||||
<!--
|
||||
* Component: ContextCompressionDemo.vue
|
||||
* Description: Demonstrates various context compression techniques to save tokens.
|
||||
* Features:
|
||||
* - Strategies: Summarization, Extraction, Structured Format (JSON)
|
||||
* - Real-time comparison of original vs compressed text
|
||||
* - Metrics display (Token count, Compression Ratio)
|
||||
* Description: Demonstrates various context compression techniques with a clear vertical flow.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
@@ -15,9 +11,9 @@ const originalText = ref(
|
||||
)
|
||||
|
||||
const strategies = [
|
||||
{ id: 'summary', label: '📝 摘要生成', desc: '保留大意,缩减篇幅' },
|
||||
{ id: 'extract', label: '🔑 关键词提取', desc: '提取核心要点' },
|
||||
{ id: 'json', label: '⚙️ 结构化数据', desc: '转换为 JSON 格式' }
|
||||
{ id: 'summary', label: '📝 摘要生成', desc: '保留大意' },
|
||||
{ id: 'extract', label: '🔑 关键词', desc: '提炼要点' },
|
||||
{ id: 'json', label: '⚙️ 结构化', desc: '转 JSON' }
|
||||
]
|
||||
|
||||
const currentMode = ref('')
|
||||
@@ -39,7 +35,7 @@ const compress = async (mode) => {
|
||||
compressedText.value = ''
|
||||
|
||||
// Simulate API delay
|
||||
await new Promise(r => setTimeout(r, 600))
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
|
||||
if (mode === 'summary') {
|
||||
compressedText.value = '上下文工程旨在优化 LLM 提示词以解决上下文窗口限制。主要技术包括摘要生成(浓缩关键信息)、RAG(按需检索相关片段)以及结构化数据转换(提高信息密度)。'
|
||||
@@ -59,8 +55,9 @@ const compress = async (mode) => {
|
||||
|
||||
<template>
|
||||
<div class="context-compression-demo">
|
||||
<!-- Control Panel -->
|
||||
<div class="control-panel">
|
||||
<!-- 1. Strategy Selection -->
|
||||
<div class="section control-panel">
|
||||
<div class="section-label">1. 选择压缩策略</div>
|
||||
<div class="strategy-group">
|
||||
<button
|
||||
v-for="s in strategies"
|
||||
@@ -68,7 +65,6 @@ const compress = async (mode) => {
|
||||
class="strategy-btn"
|
||||
:class="{ active: currentMode === s.id }"
|
||||
@click="compress(s.id)"
|
||||
:title="s.desc"
|
||||
>
|
||||
<div class="btn-label">{{ s.label }}</div>
|
||||
<div class="btn-desc">{{ s.desc }}</div>
|
||||
@@ -76,92 +72,102 @@ const compress = async (mode) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Comparison Area -->
|
||||
<div class="comparison-area">
|
||||
<!-- Original -->
|
||||
<div class="text-column original">
|
||||
<div class="column-header">
|
||||
<span class="badge">原始文本</span>
|
||||
<span class="meta">{{ originalTokens }} Tokens</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="originalText"
|
||||
class="text-content"
|
||||
placeholder="在此输入长文本..."
|
||||
></textarea>
|
||||
<!-- 2. Input Area -->
|
||||
<div class="section input-area">
|
||||
<div class="section-header">
|
||||
<span class="label">原始文本 (Original)</span>
|
||||
<span class="token-count">{{ originalTokens }} tokens</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="originalText"
|
||||
class="text-content original-input"
|
||||
placeholder="在此输入长文本..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Arrow -->
|
||||
<div class="process-arrow">
|
||||
<div class="arrow-icon" :class="{ compressing: isCompressing }">
|
||||
{{ isCompressing ? '⚙️' : '➡️' }}
|
||||
<!-- Connector / Process -->
|
||||
<div class="flow-connector">
|
||||
<div class="line"></div>
|
||||
<div class="process-icon" :class="{ spinning: isCompressing }">
|
||||
{{ isCompressing ? '⚙️' : '⬇️' }}
|
||||
</div>
|
||||
<div class="badge-container" v-if="compressedText && !isCompressing">
|
||||
<span class="ratio-badge">-{{ compressionRatio }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Output Area -->
|
||||
<div class="section output-area" :class="{ 'has-result': compressedText }">
|
||||
<div class="section-header">
|
||||
<span class="label">压缩后 (Compressed)</span>
|
||||
<span class="token-count" v-if="compressedText">{{ compressedTokens }} tokens</span>
|
||||
</div>
|
||||
|
||||
<div class="text-content result-box">
|
||||
<div v-if="isCompressing" class="loading-state">
|
||||
<span class="spinner"></span> 正在压缩...
|
||||
</div>
|
||||
<div class="ratio-badge" v-if="compressedText && !isCompressing">
|
||||
-{{ compressionRatio }}%
|
||||
<pre v-else-if="compressedText">{{ compressedText }}</pre>
|
||||
<div v-else class="placeholder">
|
||||
请点击上方按钮开始压缩
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compressed -->
|
||||
<div class="text-column compressed">
|
||||
<div class="column-header">
|
||||
<span class="badge success">压缩后</span>
|
||||
<span class="meta" v-if="compressedText">{{ compressedTokens }} Tokens</span>
|
||||
<!-- Mini Metrics (Inside Output Area) -->
|
||||
<div class="mini-metrics" v-if="compressedText && !isCompressing">
|
||||
<div class="metric-item">
|
||||
<span class="metric-label">节省空间</span>
|
||||
<span class="metric-val highlight">{{ compressionRatio }}%</span>
|
||||
</div>
|
||||
<div class="text-content result-box" :class="{ empty: !compressedText }">
|
||||
<div v-if="isCompressing" class="loading-state">
|
||||
<span class="spinner"></span> 压缩中...
|
||||
</div>
|
||||
<pre v-else-if="compressedText">{{ compressedText }}</pre>
|
||||
<div v-else class="placeholder">
|
||||
请选择一种压缩策略
|
||||
<br>
|
||||
<small>点击上方按钮开始</small>
|
||||
</div>
|
||||
<div class="metric-bar">
|
||||
<div class="bar-fill" :style="{ width: (100 - compressionRatio) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics Bar -->
|
||||
<div class="metrics-bar" v-if="compressedText && !isCompressing">
|
||||
<div class="progress-bg">
|
||||
<div class="progress-fill" :style="{ width: (100 - compressionRatio) + '%' }"></div>
|
||||
<div class="progress-label">占用空间: {{ 100 - compressionRatio }}%</div>
|
||||
</div>
|
||||
<div class="saved-label">节省了 {{ compressionRatio }}% 的 Token</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.context-compression-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 12px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
max-width: 600px;
|
||||
margin: 1.5rem auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
.section {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* Control Panel */
|
||||
.strategy-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.strategy-btn {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
padding: 0.8rem;
|
||||
padding: 0.6rem 0.4rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
@@ -177,176 +183,165 @@ const compress = async (mode) => {
|
||||
border-color: var(--vp-c-brand);
|
||||
background-color: var(--vp-c-brand-dimm);
|
||||
color: var(--vp-c-brand-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.btn-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.btn-desc {
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
/* Comparison Area */
|
||||
.comparison-area {
|
||||
display: flex;
|
||||
padding: 1.5rem;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.text-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
/* Text Areas */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
color: var(--vp-c-text-2);
|
||||
.label {
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge.success {
|
||||
background-color: var(--vp-c-green-dimm);
|
||||
color: var(--vp-c-green-dark);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--vp-c-text-2);
|
||||
.token-count {
|
||||
font-family: var(--vp-font-mono);
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.75rem;
|
||||
background: var(--vp-c-bg-mute);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-1);
|
||||
font-family: var(--vp-font-mono);
|
||||
}
|
||||
.text-content:focus {
|
||||
|
||||
.original-input {
|
||||
min-height: 100px;
|
||||
padding: 0.75rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.original-input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.result-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
background-color: #f6f8fa; /* Light code bg style */
|
||||
}
|
||||
.result-box.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.dark .result-box {
|
||||
background-color: #161b22;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--vp-c-text-3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Process Arrow */
|
||||
.process-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 50px;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 80px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 1.5rem;
|
||||
transition: transform 0.5s;
|
||||
/* Connector */
|
||||
.flow-connector {
|
||||
position: relative;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.arrow-icon.compressing {
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.process-icon {
|
||||
z-index: 1;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 4px;
|
||||
font-size: 1.2rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.spinning {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.ratio-badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-green);
|
||||
background-color: var(--vp-c-green-dimm);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Metrics Bar */
|
||||
.metrics-bar {
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
background-color: var(--vp-c-bg-mute);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background-color: var(--vp-c-brand);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
.badge-container {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.ratio-badge {
|
||||
background: var(--vp-c-green-dimm);
|
||||
color: var(--vp-c-green-dark);
|
||||
font-size: 0.75rem;
|
||||
color: white;
|
||||
text-shadow: 0 0 2px rgba(0,0,0,0.5);
|
||||
font-weight: bold;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Metrics */
|
||||
.mini-metrics {
|
||||
margin-top: 1rem;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
padding-top: 0.8rem;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--vp-c-green);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.saved-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-green);
|
||||
.metric-bar {
|
||||
height: 6px;
|
||||
background: var(--vp-c-bg-mute);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
background: var(--vp-c-brand);
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.comparison-area {
|
||||
flex-direction: column;
|
||||
}
|
||||
.process-arrow {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+33
-32
@@ -18,12 +18,12 @@
|
||||
<div class="stat-group">
|
||||
<div class="stat-item">
|
||||
<span class="value" :class="{ error: isOverflow }">{{ usedTokens }}</span>
|
||||
<span class="label">Used Tokens</span>
|
||||
<span class="label">已经写了多少个 token</span>
|
||||
</div>
|
||||
<div class="stat-divider">/</div>
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ maxTokens }}</span>
|
||||
<span class="label">Limit</span>
|
||||
<span class="label">黑板最多能写几个 token</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="window-frame" :class="{ overflow: isOverflow }">
|
||||
<div class="window-header">
|
||||
<span class="icon">🧠</span>
|
||||
<span>Model Context Window</span>
|
||||
<span>模型能看到的“小黑板”(上下文窗口)</span>
|
||||
</div>
|
||||
|
||||
<div class="token-stream">
|
||||
@@ -63,23 +63,23 @@
|
||||
|
||||
<div v-if="isOverflow" class="overflow-indicator">
|
||||
<div class="overflow-line"></div>
|
||||
<span class="overflow-text">⚠️ Context Limit Reached (Truncated)</span>
|
||||
<span class="overflow-text">⚠️ 达到上下文上限 (已截断)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-section">
|
||||
<div class="input-header">
|
||||
<label>Input Text / 输入内容</label>
|
||||
<label>输入内容(看黑板怎么被一点点写满)</label>
|
||||
<div class="actions">
|
||||
<button class="action-btn" @click="fillLorem(30)">+ Short</button>
|
||||
<button class="action-btn" @click="fillLorem(120)">+ Overflow</button>
|
||||
<button class="action-btn outline" @click="clear">Clear</button>
|
||||
<button class="action-btn" @click="fillLorem(10)">填一段短文本</button>
|
||||
<button class="action-btn" @click="fillLorem(60)">一下子塞满黑板</button>
|
||||
<button class="action-btn outline" @click="clear">清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="inputText"
|
||||
placeholder="Type here to see how tokens fill up the memory..."
|
||||
placeholder="在这里输入几句话,看看小黑板是怎么逐渐被写满的..."
|
||||
rows="4"
|
||||
></textarea>
|
||||
</div>
|
||||
@@ -87,9 +87,9 @@
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>Note:</strong>
|
||||
Context Window 是模型的短期记忆。就像黑板只有那么大,写满了就必须擦掉旧的才能写新的。
|
||||
一旦溢出,模型不仅会"忘记"前面的内容,甚至可能无法处理新的请求。
|
||||
<strong>说明:</strong>
|
||||
上下文窗口可以理解成模型的“小黑板”。黑板只有这么大,写满了就必须擦掉旧的才能写新的。
|
||||
一旦溢出,最早写的那部分内容就会被擦掉,模型会完全“看不见”它们。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,16 +99,18 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const maxTokens = 100
|
||||
const inputText = ref('Context engineering is the art of managing information.')
|
||||
const inputText = ref('上下文工程(Context Engineering)是指优化提供给大语言模型(LLM)的提示词。')
|
||||
|
||||
// Simple mock tokenizer: split by space for demonstration
|
||||
// In reality, tokens are subwords, but space-split is good enough for concept
|
||||
const tokenizedText = computed(() => {
|
||||
if (!inputText.value) return []
|
||||
return inputText.value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((t) => t)
|
||||
// Improved tokenizer:
|
||||
// 1. Matches continuous English words/numbers ([a-zA-Z0-9]+)
|
||||
// 2. OR matches any other single character (including Chinese, punctuation)
|
||||
// This provides a better visual approximation for mixed Chinese/English text
|
||||
const matches = inputText.value.match(/[a-zA-Z0-9]+|./g) || []
|
||||
return matches.filter(t => t.trim().length > 0)
|
||||
})
|
||||
|
||||
const usedTokens = computed(() => tokenizedText.value.length)
|
||||
@@ -128,9 +130,8 @@ const getTokenClass = (index) => {
|
||||
|
||||
const fillLorem = (count) => {
|
||||
const words = [
|
||||
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur',
|
||||
'adipiscing', 'elit', 'sed', 'do', 'eiusmod', 'tempor',
|
||||
'incididunt', 'ut', 'labore', 'et', 'dolore', 'magna', 'aliqua'
|
||||
'人工智能', '深度学习', '神经网络', '大模型', 'Transformer', '注意力机制',
|
||||
'上下文窗口', 'Token', 'Embedding', '微调', '预训练', '推理', '生成', 'RAG'
|
||||
]
|
||||
const newText = Array.from({ length: count }, () => words[Math.floor(Math.random() * words.length)]).join(' ')
|
||||
inputText.value = newText
|
||||
@@ -146,7 +147,7 @@ const clear = () => {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
@@ -154,10 +155,10 @@ const clear = () => {
|
||||
.control-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
@@ -175,7 +176,7 @@ const clear = () => {
|
||||
}
|
||||
|
||||
.stat-item .value {
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -223,14 +224,14 @@ const clear = () => {
|
||||
}
|
||||
|
||||
.visualization-area {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.window-frame {
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg);
|
||||
min-height: 120px;
|
||||
min-height: 100px;
|
||||
position: relative;
|
||||
transition: border-color 0.3s;
|
||||
overflow: hidden;
|
||||
@@ -242,8 +243,8 @@ const clear = () => {
|
||||
|
||||
.window-header {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
@@ -252,11 +253,11 @@ const clear = () => {
|
||||
}
|
||||
|
||||
.token-stream {
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
max-height: 200px;
|
||||
gap: 2px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="intro-prs">
|
||||
<div class="prs-item">
|
||||
<div class="prs-title">问题</div>
|
||||
<ul>
|
||||
<li><strong>上下文难以保持一致</strong>:对话一长,前后语义容易脱节。</li>
|
||||
<li><strong>关键事实容易丢失</strong>:早期给出的信息在后续轮次中难以被准确引用。</li>
|
||||
<li><strong>调用成本持续上升</strong>:每一轮都要重新处理大量历史内容。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="prs-item">
|
||||
<div class="prs-title">可能的成因</div>
|
||||
<ul>
|
||||
<li><strong>视野仅限当前调用</strong>:模型只能依赖这一轮提供的上下文。</li>
|
||||
<li><strong>信息缺乏结构化组织</strong>:重要信息与次要细节混在一起,难以形成稳定记忆。</li>
|
||||
<li><strong>历史内容反复计算</strong>:大量固定前缀在多轮对话中被一遍遍重新处理。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="prs-item">
|
||||
<div class="prs-title">带来的影响</div>
|
||||
<ul>
|
||||
<li><strong>回答质量不稳定</strong>:对话越长,模型越难保持一致性和可追溯性。</li>
|
||||
<li><strong>成本难以预估</strong>:每轮上下文大小高度波动,调用费用不可控。</li>
|
||||
<li><strong>难以工程化落地</strong>:缺乏明确的上下文管理策略,系统在生产环境中难以维护与扩展。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.intro-prs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 0.75rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.prs-item {
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.prs-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 1.1rem;
|
||||
font-size: 0.82rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
li + li {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.intro-prs {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<div class="kv-cache-demo">
|
||||
<div class="control-panel">
|
||||
<div class="control-group">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" v-model="isCacheEnabled" :disabled="isProcessing">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="label">开启“背课文加速”(前缀复用 / KV Cache)</span>
|
||||
</div>
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="isProcessing"
|
||||
@click="sendRequest"
|
||||
>
|
||||
{{ isProcessing ? '生成中...' : '发送新请求' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="visualization-area">
|
||||
<div class="memory-blocks">
|
||||
<!-- System Prompt Block -->
|
||||
<div
|
||||
class="memory-block system"
|
||||
:class="{ 'cached': isCacheEnabled && hasCache, 'processing': processingStep === 'system' }"
|
||||
>
|
||||
<div class="block-header">
|
||||
<span class="icon">⚙️</span>
|
||||
<span>固定开场白(System Prompt)</span>
|
||||
<span class="badge" v-if="isCacheEnabled && hasCache">已背过</span>
|
||||
</div>
|
||||
<div class="block-content">
|
||||
你是一个乐于助人的 AI 助手... (大约 500 个 token)
|
||||
</div>
|
||||
<div class="process-indicator" v-if="processingStep === 'system'">
|
||||
计算中...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Block -->
|
||||
<div
|
||||
class="memory-block history"
|
||||
:class="{ 'processing': processingStep === 'history' }"
|
||||
>
|
||||
<div class="block-header">
|
||||
<span class="icon">💬</span>
|
||||
<span>最近几轮聊天记录</span>
|
||||
</div>
|
||||
<div class="block-content">
|
||||
用户:你好... (大约 200 个 token)
|
||||
</div>
|
||||
<div class="process-indicator" v-if="processingStep === 'history'">
|
||||
计算中...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Query Block -->
|
||||
<div
|
||||
class="memory-block query"
|
||||
:class="{ 'processing': processingStep === 'query' }"
|
||||
>
|
||||
<div class="block-header">
|
||||
<span class="icon">❓</span>
|
||||
<span>这一次的新问题</span>
|
||||
</div>
|
||||
<div class="block-content">
|
||||
{{ currentQuery }} (大约 50 个 token)
|
||||
</div>
|
||||
<div class="process-indicator" v-if="processingStep === 'query'">
|
||||
计算中...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics-panel">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{{ metrics.ttft }}ms</div>
|
||||
<div class="metric-label">开口速度(首字延迟 TTFT)</div>
|
||||
<div class="metric-diff" :class="{ 'good': metrics.savedTime > 0 }" v-if="metrics.savedTime > 0">
|
||||
节省 {{ metrics.savedTime }}ms
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{{ metrics.processedTokens }}</div>
|
||||
<div class="metric-label">这次一共算了多少个 token</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{{ metrics.cost }}</div>
|
||||
<div class="metric-label">大致算力消耗(越少越省钱)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p v-if="isCacheEnabled">
|
||||
<span class="icon">⚡</span>
|
||||
<strong>命中时在干嘛:</strong>前面的固定开场白不再重复计算,直接用“上一次背过的结果”,所以又快又省。
|
||||
</p>
|
||||
<p v-else>
|
||||
<span class="icon">🐌</span>
|
||||
<strong>没开缓存时:</strong>每次都要从头把所有 token 重新算一遍注意力,就像每次都从第一页开始重读课文,又慢又费钱。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const isCacheEnabled = ref(false)
|
||||
const hasCache = ref(false)
|
||||
const isProcessing = ref(false)
|
||||
const processingStep = ref('') // 'system', 'history', 'query'
|
||||
const currentQuery = ref('帮我写一段 Python 代码')
|
||||
|
||||
const metrics = reactive({
|
||||
ttft: 0,
|
||||
processedTokens: 0,
|
||||
cost: 0,
|
||||
savedTime: 0
|
||||
})
|
||||
|
||||
const sendRequest = async () => {
|
||||
if (isProcessing.value) return
|
||||
isProcessing.value = true
|
||||
|
||||
// Reset metrics display
|
||||
metrics.ttft = 0
|
||||
metrics.processedTokens = 0
|
||||
metrics.cost = 0
|
||||
metrics.savedTime = 0
|
||||
|
||||
const systemTokens = 500
|
||||
const historyTokens = 200
|
||||
const queryTokens = 50
|
||||
|
||||
// Step 1: System Prompt
|
||||
processingStep.value = 'system'
|
||||
const systemDelay = (isCacheEnabled.value && hasCache.value) ? 100 : 800
|
||||
await new Promise(r => setTimeout(r, systemDelay))
|
||||
|
||||
// Step 2: Chat History
|
||||
processingStep.value = 'history'
|
||||
await new Promise(r => setTimeout(r, 400))
|
||||
|
||||
// Step 3: New Query
|
||||
processingStep.value = 'query'
|
||||
await new Promise(r => setTimeout(r, 200))
|
||||
|
||||
// Calculate final metrics
|
||||
processingStep.value = ''
|
||||
isProcessing.value = false
|
||||
|
||||
// Logic:
|
||||
// Without Cache: Process all (500 + 200 + 50) = 750 tokens
|
||||
// With Cache: Process only (200 + 50) = 250 tokens (System is reused)
|
||||
|
||||
if (isCacheEnabled.value && hasCache.value) {
|
||||
metrics.ttft = 150 // Fast
|
||||
metrics.processedTokens = historyTokens + queryTokens
|
||||
metrics.cost = 3 // Low cost
|
||||
metrics.savedTime = 650
|
||||
} else {
|
||||
metrics.ttft = 800 // Slow
|
||||
metrics.processedTokens = systemTokens + historyTokens + queryTokens
|
||||
metrics.cost = 10 // High cost
|
||||
|
||||
// First run with cache enabled establishes the cache
|
||||
if (isCacheEnabled.value) {
|
||||
hasCache.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update query for next run to simulate conversation
|
||||
currentQuery.value = currentQuery.value === '帮我写一段 Python 代码'
|
||||
? '这段代码怎么运行?'
|
||||
: '帮我写一段 Python 代码'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kv-cache-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: .4s;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 1px;
|
||||
bottom: 1px;
|
||||
background-color: var(--vp-c-text-2);
|
||||
transition: .4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(20px);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 0.4rem 0.8rem;
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: opacity 0.2s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.action-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.visualization-area {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.memory-blocks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.memory-block {
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.memory-block.system { border-left: 4px solid var(--vp-c-green-1); }
|
||||
.memory-block.history { border-left: 4px solid var(--vp-c-yellow-1); }
|
||||
.memory-block.query { border-left: 4px solid var(--vp-c-brand-1); }
|
||||
|
||||
.memory-block.cached {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: var(--vp-c-green-1);
|
||||
}
|
||||
|
||||
.memory-block.processing {
|
||||
box-shadow: 0 0 10px var(--vp-c-brand);
|
||||
border-color: var(--vp-c-brand);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.block-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.block-content {
|
||||
color: var(--vp-c-text-2);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: var(--vp-c-green-1);
|
||||
color: white;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.process-indicator {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.metrics-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-diff {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.metric-diff.good {
|
||||
background: var(--vp-c-green-1);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="lost-in-middle-demo">
|
||||
<div class="control-panel">
|
||||
<div class="control-group">
|
||||
<label>关键信息大概在整段话的哪个位置:{{ needlePosition }}%</label>
|
||||
<input
|
||||
type="range"
|
||||
v-model.number="needlePosition"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
class="slider-input"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visualization-area">
|
||||
<!-- Context Window Bar -->
|
||||
<div class="context-bar">
|
||||
<div class="context-label start">Start (System)</div>
|
||||
<div class="context-label end">End (Query)</div>
|
||||
|
||||
<!-- Attention Heatmap Background -->
|
||||
<div class="attention-heatmap"></div>
|
||||
|
||||
<!-- Needle Marker -->
|
||||
<div
|
||||
class="needle-marker"
|
||||
:style="{ left: `${needlePosition}%` }"
|
||||
>
|
||||
<div class="needle-icon">📍</div>
|
||||
<div class="needle-tooltip">关键事实</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Probability Curve Chart -->
|
||||
<div class="chart-container">
|
||||
<svg viewBox="0 0 100 60" preserveAspectRatio="none" class="chart-svg">
|
||||
<!-- U-Curve Path -->
|
||||
<path
|
||||
d="M 0 5 Q 50 55 100 5"
|
||||
fill="none"
|
||||
stroke="var(--vp-c-divider)"
|
||||
stroke-width="2"
|
||||
stroke-dasharray="4"
|
||||
/>
|
||||
<!-- Active Dot -->
|
||||
<circle
|
||||
:cx="needlePosition"
|
||||
:cy="60 - (retrievalProb * 0.5 + 5)"
|
||||
r="3"
|
||||
fill="var(--vp-c-brand)"
|
||||
/>
|
||||
</svg>
|
||||
<div class="chart-label y-axis">被记住的概率</div>
|
||||
<div class="chart-label x-axis">在上下文里的位置</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metrics-panel">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value" :class="getScoreClass(retrievalProb)">
|
||||
{{ retrievalProb.toFixed(1) }}%
|
||||
</div>
|
||||
<div class="metric-label">检索成功率</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">{{ positionLabel }}</div>
|
||||
<div class="metric-label">位置描述</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">🔍</span>
|
||||
<strong>实验观察:</strong>当关键信息藏在整段话的<strong>中间位置</strong>时,模型最容易“漏看掉”(Lost in the Middle)。
|
||||
<br>
|
||||
最靠谱的做法:把重要指令放在<strong>最前面的 System Prompt</strong>,或者<strong>最后的用户问题里</strong>。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const needlePosition = ref(50) // 0 to 100
|
||||
|
||||
// Parabolic curve calculation: Vertex at (50, 40), passing through (0, 95) and (100, 95)
|
||||
// y = a(x-h)^2 + k
|
||||
// a = 0.022
|
||||
const retrievalProb = computed(() => {
|
||||
const x = needlePosition.value
|
||||
const prob = 0.022 * Math.pow(x - 50, 2) + 40
|
||||
return Math.min(99.9, Math.max(0, prob))
|
||||
})
|
||||
|
||||
const positionLabel = computed(() => {
|
||||
const p = needlePosition.value
|
||||
if (p < 20) return '偏开头'
|
||||
if (p > 80) return '偏结尾'
|
||||
return '中间区域(最危险)'
|
||||
})
|
||||
|
||||
const getScoreClass = (score) => {
|
||||
if (score > 85) return 'text-success'
|
||||
if (score > 60) return 'text-warning'
|
||||
return 'text-danger'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lost-in-middle-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
margin-bottom: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.control-group label {
|
||||
font-weight: bold;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
width: 100%;
|
||||
accent-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.visualization-area {
|
||||
margin-bottom: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.context-bar {
|
||||
height: 40px;
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
margin-bottom: 0.75rem;
|
||||
background: var(--vp-c-bg);
|
||||
overflow: visible; /* Allow needle to stick out */
|
||||
}
|
||||
|
||||
.attention-heatmap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(16, 185, 129, 0.2) 0%,
|
||||
rgba(239, 68, 68, 0.1) 50%,
|
||||
rgba(16, 185, 129, 0.2) 100%
|
||||
);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.context-label {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.context-label.start { left: 0; }
|
||||
.context-label.end { right: 0; }
|
||||
|
||||
.needle-marker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
cursor: grab;
|
||||
transition: left 0.1s ease;
|
||||
}
|
||||
|
||||
.needle-icon {
|
||||
font-size: 1.25rem;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
|
||||
}
|
||||
|
||||
.needle-tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--vp-c-text-1);
|
||||
color: var(--vp-c-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.needle-marker:hover .needle-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 60px;
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.chart-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.chart-label {
|
||||
position: absolute;
|
||||
font-size: 0.7rem;
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
.chart-label.y-axis { top: 0; left: 0; }
|
||||
.chart-label.x-axis { bottom: -1rem; width: 100%; text-align: center; }
|
||||
|
||||
.metrics-panel {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
flex: 1;
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.text-success { color: var(--vp-c-success-1); }
|
||||
.text-warning { color: var(--vp-c-warning-1); }
|
||||
.text-danger { color: var(--vp-c-danger-1); }
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.info-box .icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
+521
@@ -0,0 +1,521 @@
|
||||
<!--
|
||||
* Component: MemoryPalaceActionDemo.vue
|
||||
* Description: Interactive simulation of the "Memory Palace" in action.
|
||||
* Features:
|
||||
* - Scenario selection (Coding vs Support)
|
||||
* - Chat interface simulation
|
||||
* - Real-time visualization of the 4 context layers
|
||||
* - Step-by-step walkthrough of the context construction process
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
|
||||
const scenarios = {
|
||||
coding: {
|
||||
name: '👨💻 代码助手场景',
|
||||
steps: [
|
||||
{
|
||||
user: '帮我写一个 Python 贪吃蛇游戏',
|
||||
action: '初始化',
|
||||
layers: {
|
||||
base: 'System: 你是资深 Python 工程师...',
|
||||
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
|
||||
chat: [],
|
||||
rag: []
|
||||
},
|
||||
desc: '初始化:装载地基(System)和任务(Task)。此时 Layer 1 & 2 建立。'
|
||||
},
|
||||
{
|
||||
user: null,
|
||||
ai_thinking: '需要查询 Pygame 的最新初始化代码...',
|
||||
action: '检索',
|
||||
layers: {
|
||||
base: 'System: 你是资深 Python 工程师...',
|
||||
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
|
||||
chat: [],
|
||||
rag: ['Docs: Pygame.init() usage...', 'Docs: Game loop pattern...']
|
||||
},
|
||||
desc: '思考与检索:发现需要知识补充,临时调取 RAG 资料到 Layer 4。'
|
||||
},
|
||||
{
|
||||
user: null,
|
||||
ai: '好的,这是一个基于 Pygame 的贪吃蛇基础代码...',
|
||||
action: '生成',
|
||||
layers: {
|
||||
base: 'System: 你是资深 Python 工程师...',
|
||||
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
|
||||
chat: ['User: 写贪吃蛇', 'AI: [Code Block]'],
|
||||
rag: [] // RAG cleared after generation to save space
|
||||
},
|
||||
desc: '生成回答:RAG 资料用完即扔(节省空间),对话写入 Layer 3 (Chat)。'
|
||||
},
|
||||
{
|
||||
user: '蛇移动得太快了,怎么调慢点?',
|
||||
action: '追问',
|
||||
layers: {
|
||||
base: 'System: 你是资深 Python 工程师...',
|
||||
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
|
||||
chat: ['User: 写贪吃蛇', 'AI: [Code Block]', 'User: 调慢点'],
|
||||
rag: []
|
||||
},
|
||||
desc: '用户追问:新对话追加到 Layer 3。Layer 1 & 2 保持不变(0成本)。'
|
||||
},
|
||||
{
|
||||
user: null,
|
||||
ai: '你可以调整 clock.tick(15) 中的数值...',
|
||||
action: '回复',
|
||||
layers: {
|
||||
base: 'System: 你是资深 Python 工程师...',
|
||||
task: 'Task: 编写贪吃蛇游戏,使用 Pygame 库...',
|
||||
chat: ['User: 写贪吃蛇', 'AI: [Code Block]', 'User: 调慢点', 'AI: 调整 tick 值...'],
|
||||
rag: []
|
||||
},
|
||||
desc: '持续对话:Layer 3 增长。如果太长,最上面的对话会被挤出去(滑动窗口)。'
|
||||
}
|
||||
]
|
||||
},
|
||||
support: {
|
||||
name: '👩💼 客服助手场景',
|
||||
steps: [
|
||||
{
|
||||
user: '我的订单发货了吗?单号 12345',
|
||||
action: '接收',
|
||||
layers: {
|
||||
base: 'System: 你是电商客服,语气温柔...',
|
||||
task: 'Task: 处理订单查询请求...',
|
||||
chat: [],
|
||||
rag: []
|
||||
},
|
||||
desc: '接收消息:加载地基(System)。'
|
||||
},
|
||||
{
|
||||
user: null,
|
||||
ai_thinking: '查询订单系统 API...',
|
||||
action: '工具调用',
|
||||
layers: {
|
||||
base: 'System: 你是电商客服,语气温柔...',
|
||||
task: 'Task: 处理订单查询请求...',
|
||||
chat: ['User: 查单号 12345'],
|
||||
rag: ['API_Result: {id:12345, status:"shipped", loc:"Beijing"}']
|
||||
},
|
||||
desc: '调用工具/RAG:获取实时订单状态,放入 Layer 4。'
|
||||
},
|
||||
{
|
||||
user: null,
|
||||
ai: '亲,查到了哦!您的包裹已经在北京中转了。',
|
||||
action: '回复',
|
||||
layers: {
|
||||
base: 'System: 你是电商客服,语气温柔...',
|
||||
task: 'Task: 处理订单查询请求...',
|
||||
chat: ['User: 查单号 12345', 'AI: 在北京中转'],
|
||||
rag: []
|
||||
},
|
||||
desc: '完成回复:Layer 4 清空,对话保留在 Layer 3。'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const currentScenarioKey = ref('coding')
|
||||
const currentStepIndex = ref(0)
|
||||
|
||||
const currentScenario = computed(() => scenarios[currentScenarioKey.value])
|
||||
const currentStep = computed(() => currentScenario.value.steps[currentStepIndex.value])
|
||||
const isLastStep = computed(() => currentStepIndex.value === currentScenario.value.steps.length - 1)
|
||||
|
||||
const setScenario = (key) => {
|
||||
currentScenarioKey.value = key
|
||||
currentStepIndex.value = 0
|
||||
}
|
||||
|
||||
const nextStep = () => {
|
||||
if (!isLastStep.value) {
|
||||
currentStepIndex.value++
|
||||
} else {
|
||||
currentStepIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStepIndex.value > 0) {
|
||||
currentStepIndex.value--
|
||||
}
|
||||
}
|
||||
|
||||
// Visual helpers
|
||||
const getLayerStyle = (layerId) => {
|
||||
const isActive = (layer) => {
|
||||
// Logic to highlight active layer based on step action could go here
|
||||
// For now, simple static colors
|
||||
return true
|
||||
}
|
||||
return {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="action-demo">
|
||||
<!-- Scenario Selector -->
|
||||
<div class="scenario-tabs">
|
||||
<button
|
||||
v-for="(s, key) in scenarios"
|
||||
:key="key"
|
||||
class="tab-btn"
|
||||
:class="{ active: currentScenarioKey === key }"
|
||||
@click="setScenario(key)"
|
||||
>
|
||||
{{ s.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-grid">
|
||||
<!-- Left: Chat Simulator -->
|
||||
<div class="chat-panel">
|
||||
<div class="panel-header">📱 用户视角 (Chat)</div>
|
||||
<div class="chat-window">
|
||||
<div v-for="(msg, idx) in currentStep.layers.chat" :key="idx" class="chat-bubble" :class="msg.startsWith('User') ? 'user' : 'ai'">
|
||||
{{ msg.split(': ')[1] || msg }}
|
||||
</div>
|
||||
<div v-if="currentStep.user && !currentStep.layers.chat.some(m => m.includes(currentStep.user))" class="chat-bubble user pending">
|
||||
{{ currentStep.user }}...
|
||||
</div>
|
||||
<div v-if="currentStep.ai_thinking" class="chat-bubble thinking">
|
||||
💭 {{ currentStep.ai_thinking }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="step-info">步骤 {{ currentStepIndex + 1 }} / {{ currentScenario.steps.length }}</div>
|
||||
<div class="btn-group">
|
||||
<button class="nav-btn" @click="prevStep" :disabled="currentStepIndex === 0">⬅️ 上一步</button>
|
||||
<button class="nav-btn primary" @click="nextStep">
|
||||
{{ isLastStep ? '🔄 重新演示' : '下一步 ➡️' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Memory Palace Internals -->
|
||||
<div class="palace-panel">
|
||||
<div class="panel-header">🧠 AI 视角 (Context Construction)</div>
|
||||
<div class="context-visualizer">
|
||||
|
||||
<!-- Layer 1: Base -->
|
||||
<div class="layer-box base">
|
||||
<div class="layer-label">
|
||||
<span class="icon">🏛️</span>
|
||||
<span class="title">Layer 1: 地基 (System)</span>
|
||||
<span class="badge">KV Cached</span>
|
||||
</div>
|
||||
<div class="layer-content">{{ currentStep.layers.base }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer 2: Task -->
|
||||
<div class="layer-box task">
|
||||
<div class="layer-label">
|
||||
<span class="icon">📌</span>
|
||||
<span class="title">Layer 2: 支柱 (Task)</span>
|
||||
<span class="badge">Pinned</span>
|
||||
</div>
|
||||
<div class="layer-content">{{ currentStep.layers.task }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer 3: Chat -->
|
||||
<div class="layer-box chat">
|
||||
<div class="layer-label">
|
||||
<span class="icon">💬</span>
|
||||
<span class="title">Layer 3: 客厅 (Chat)</span>
|
||||
<span class="badge">Sliding</span>
|
||||
</div>
|
||||
<div class="layer-content">
|
||||
<div v-for="(m, i) in currentStep.layers.chat" :key="i" class="mini-line">{{ m }}</div>
|
||||
<div v-if="currentStep.layers.chat.length === 0" class="empty-hint">(暂无对话历史)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer 4: RAG -->
|
||||
<div class="layer-box rag" :class="{ active: currentStep.layers.rag.length > 0 }">
|
||||
<div class="layer-label">
|
||||
<span class="icon">📚</span>
|
||||
<span class="title">Layer 4: 图书馆 (RAG)</span>
|
||||
<span class="badge ephemeral">Temp</span>
|
||||
</div>
|
||||
<div class="layer-content">
|
||||
<div v-for="(r, i) in currentStep.layers.rag" :key="i" class="rag-item">{{ r }}</div>
|
||||
<div v-if="currentStep.layers.rag.length === 0" class="empty-hint">(当前无需检索)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Explanation Footer -->
|
||||
<div class="step-desc">
|
||||
<strong>💡 这一步发生了什么:</strong>
|
||||
{{ currentStep.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.action-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
margin: 1.5rem 0;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scenario-tabs {
|
||||
display: flex;
|
||||
background: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--vp-c-brand);
|
||||
border-bottom-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.demo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chat Panel */
|
||||
.chat-panel {
|
||||
border-right: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chat-window {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
overflow-y: auto;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.dark .chat-window {
|
||||
background: #1e1e20;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
max-width: 85%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.chat-bubble.user {
|
||||
align-self: flex-end;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
|
||||
.chat-bubble.ai {
|
||||
align-self: flex-start;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
|
||||
.chat-bubble.thinking {
|
||||
align-self: center;
|
||||
background: transparent;
|
||||
color: var(--vp-c-text-2);
|
||||
font-style: italic;
|
||||
font-size: 0.85em;
|
||||
border: 1px dashed var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.chat-bubble.pending {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: 15px;
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.step-info {
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
flex: 1;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-size: 0.9em;
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-btn:hover:not(:disabled) {
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
.nav-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.nav-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
.nav-btn.primary:hover {
|
||||
background: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
/* Palace Panel */
|
||||
.palace-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.context-visualizer {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.layer-box {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.layer-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge {
|
||||
margin-left: auto;
|
||||
font-size: 0.7em;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-divider);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.badge.ephemeral {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.layer-content {
|
||||
font-family: var(--vp-font-mono);
|
||||
font-size: 0.8em;
|
||||
color: var(--vp-c-text-2);
|
||||
background: var(--vp-c-bg-alt);
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mini-line {
|
||||
margin-bottom: 2px;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.rag-item {
|
||||
color: #27ae60;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
/* Layer specific styling */
|
||||
.base .layer-label { color: var(--vp-c-brand); }
|
||||
.base .badge { background: var(--vp-c-brand); color: white; }
|
||||
|
||||
.task .layer-label { color: #8e44ad; }
|
||||
.task .badge { background: #8e44ad; color: white; }
|
||||
|
||||
.chat .layer-label { color: #e67e22; }
|
||||
|
||||
.rag { border-style: dashed; opacity: 0.6; }
|
||||
.rag.active { opacity: 1; border-color: #27ae60; background: rgba(39, 174, 96, 0.05); }
|
||||
.rag .layer-label { color: #27ae60; }
|
||||
|
||||
.step-desc {
|
||||
padding: 12px;
|
||||
background: #fff9c4;
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.dark .step-desc {
|
||||
background: #333322;
|
||||
color: #ddd;
|
||||
border-top-color: #444;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,337 @@
|
||||
<!--
|
||||
* Component: MemoryPalaceDemo.vue
|
||||
* Description: Visualizes the "Memory Palace" 4-layer context structure.
|
||||
* Features:
|
||||
* - Step-by-step assembly of the context layers
|
||||
* - Visual distinction between Static (Cached) and Dynamic parts
|
||||
* - Explains the purpose of each layer
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const currentStep = ref(0)
|
||||
const steps = [
|
||||
{
|
||||
id: 'base',
|
||||
title: '第一层:地基 (System)',
|
||||
desc: '系统设定、身份、原则',
|
||||
detail: '✅ 永远不变,利用 KV Cache 实现 0 成本背诵',
|
||||
color: 'var(--vp-c-brand)',
|
||||
icon: '🏛️'
|
||||
},
|
||||
{
|
||||
id: 'task',
|
||||
title: '第二层:支柱 (Task)',
|
||||
desc: '当前任务目标、用户画像',
|
||||
detail: '📌 任务期内“钉死”,保证方向不偏',
|
||||
color: '#8e44ad',
|
||||
icon: '📌'
|
||||
},
|
||||
{
|
||||
id: 'chat',
|
||||
title: '第三层:客厅 (Chat)',
|
||||
desc: '最近 5-10 轮对话',
|
||||
detail: '🔄 滑动窗口,旧的自动腾出空间',
|
||||
color: '#e67e22',
|
||||
icon: '💬'
|
||||
},
|
||||
{
|
||||
id: 'rag',
|
||||
title: '第四层:图书馆 (RAG)',
|
||||
desc: '按需检索的知识',
|
||||
detail: '📚 不占脑子,用时再查,无限扩展',
|
||||
color: '#27ae60',
|
||||
icon: '🔍'
|
||||
}
|
||||
]
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < 4) {
|
||||
currentStep.value++
|
||||
} else {
|
||||
currentStep.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const isComplete = computed(() => currentStep.value === 4)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="memory-palace-demo">
|
||||
|
||||
<!-- Visual Area -->
|
||||
<div class="palace-container">
|
||||
<div class="palace-stack">
|
||||
<!-- Layer 4: RAG (Top/Side) -->
|
||||
<div
|
||||
class="layer-block rag-layer"
|
||||
:class="{ visible: currentStep >= 4 }"
|
||||
>
|
||||
<div class="layer-content">
|
||||
<span class="icon">{{ steps[3].icon }}</span>
|
||||
<div class="text">
|
||||
<div class="layer-title">{{ steps[3].title }}</div>
|
||||
<div class="layer-desc">{{ steps[3].desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer-detail" v-if="currentStep >= 4">{{ steps[3].detail }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer 3: Chat -->
|
||||
<div
|
||||
class="layer-block chat-layer"
|
||||
:class="{ visible: currentStep >= 3 }"
|
||||
>
|
||||
<div class="layer-content">
|
||||
<span class="icon">{{ steps[2].icon }}</span>
|
||||
<div class="text">
|
||||
<div class="layer-title">{{ steps[2].title }}</div>
|
||||
<div class="layer-desc">{{ steps[2].desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer-detail" v-if="currentStep >= 3">{{ steps[2].detail }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer 2: Task -->
|
||||
<div
|
||||
class="layer-block task-layer"
|
||||
:class="{ visible: currentStep >= 2 }"
|
||||
>
|
||||
<div class="layer-content">
|
||||
<span class="icon">{{ steps[1].icon }}</span>
|
||||
<div class="text">
|
||||
<div class="layer-title">{{ steps[1].title }}</div>
|
||||
<div class="layer-desc">{{ steps[1].desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer-detail" v-if="currentStep >= 2">{{ steps[1].detail }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Layer 1: Base -->
|
||||
<div
|
||||
class="layer-block base-layer"
|
||||
:class="{ visible: currentStep >= 1 }"
|
||||
>
|
||||
<div class="layer-content">
|
||||
<span class="icon">{{ steps[0].icon }}</span>
|
||||
<div class="text">
|
||||
<div class="layer-title">{{ steps[0].title }}</div>
|
||||
<div class="layer-desc">{{ steps[0].desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer-detail" v-if="currentStep >= 1">{{ steps[0].detail }}</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State Placeholder -->
|
||||
<div class="empty-placeholder" v-if="currentStep === 0">
|
||||
🚧 空地:点击下方按钮开始建造记忆宫殿
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Control Area -->
|
||||
<div class="control-area">
|
||||
<div class="step-indicator">
|
||||
当前进度: {{ currentStep }}/4
|
||||
</div>
|
||||
<button class="build-btn" @click="nextStep" :class="{ 'reset-mode': isComplete }">
|
||||
{{ isComplete ? '🔄 重置重建' : (currentStep === 0 ? '🏗️ 开始建造' : '➕ 添加下一层') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Explanation Box -->
|
||||
<div class="explanation-box" v-if="currentStep > 0">
|
||||
<div class="exp-title">为什么这样设计?</div>
|
||||
<div class="exp-content" v-if="currentStep === 1">
|
||||
**地基最稳**:把 System Prompt 放在最前面,利用 KV Cache 机制,让 AI "背下来",后续请求**速度快且免费**。
|
||||
</div>
|
||||
<div class="exp-content" v-if="currentStep === 2">
|
||||
**目标明确**:无论聊得多嗨,任务目标(如“写一个 Python 爬虫”)必须**钉死**,防止 AI 聊偏了。
|
||||
</div>
|
||||
<div class="exp-content" v-if="currentStep === 3">
|
||||
**保持鲜活**:最近的对话最重要,用滑动窗口保留,**旧的自动忘掉**,给新信息腾地方。
|
||||
</div>
|
||||
<div class="exp-content" v-if="currentStep === 4">
|
||||
**无限外脑**:遇到不懂的,不要瞎编,去“图书馆”查资料。**用完即走**,不占宝贵的脑容量。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.memory-palace-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
margin: 1.5rem 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.palace-container {
|
||||
padding: 2rem;
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
align-items: flex-end; /* Stack from bottom */
|
||||
justify-content: center;
|
||||
background: linear-gradient(to top, var(--vp-c-bg-alt), var(--vp-c-bg));
|
||||
}
|
||||
|
||||
.palace-stack {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse; /* Stack from bottom */
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.layer-block {
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-block.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
/* Layer Specific Styles */
|
||||
.base-layer {
|
||||
border-color: var(--vp-c-brand);
|
||||
border-bottom-width: 6px; /* Heavy foundation */
|
||||
background: var(--vp-c-brand-dimm);
|
||||
}
|
||||
|
||||
.task-layer {
|
||||
border-color: #8e44ad;
|
||||
background: rgba(142, 68, 173, 0.1);
|
||||
margin: 0 10px; /* Slightly narrower */
|
||||
}
|
||||
|
||||
.chat-layer {
|
||||
border-color: #e67e22;
|
||||
background: rgba(230, 126, 34, 0.1);
|
||||
margin: 0 20px; /* Narrower */
|
||||
}
|
||||
|
||||
.rag-layer {
|
||||
border-color: #27ae60;
|
||||
border-style: dashed;
|
||||
background: rgba(39, 174, 96, 0.1);
|
||||
margin: 0 30px; /* Narrowest */
|
||||
}
|
||||
|
||||
.layer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.layer-title {
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.layer-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.layer-detail {
|
||||
font-size: 0.75rem;
|
||||
background: rgba(255,255,255,0.5);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--vp-c-text-1);
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
}
|
||||
.dark .layer-detail {
|
||||
background: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.empty-placeholder {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
padding: 2rem;
|
||||
border: 2px dashed var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.control-area {
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
font-family: var(--vp-font-mono);
|
||||
font-size: 0.9rem;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.build-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.build-btn:hover {
|
||||
background: var(--vp-c-brand-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.build-btn.reset-mode {
|
||||
background: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* Explanation */
|
||||
.explanation-box {
|
||||
padding: 1rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.exp-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.exp-content {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
color: var(--vp-c-text-1);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
+308
-413
@@ -1,11 +1,6 @@
|
||||
<!--
|
||||
* Component: RAGSimulationDemo.vue
|
||||
* Description: Demonstrates the Retrieval-Augmented Generation (RAG) process.
|
||||
* Features:
|
||||
* - Interactive search simulation
|
||||
* - Visual representation of Vector DB and Document retrieval
|
||||
* - Step-by-step animation of the RAG pipeline
|
||||
* - Visualization of context augmentation
|
||||
* Description: Demonstrates the Retrieval-Augmented Generation (RAG) process with a vertical, intuitive flow.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
@@ -14,65 +9,48 @@ import { ref, computed } from 'vue'
|
||||
const query = ref('如何重置密码?')
|
||||
const lastQuery = ref('')
|
||||
const isSearching = ref(false)
|
||||
const currentStep = ref(0)
|
||||
const searchTime = ref(0)
|
||||
const currentStep = ref(0) // 0: Idle, 1: Searching/Scanning, 2: Retrieved/Assembling, 3: Done
|
||||
|
||||
const documents = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '密码重置指南',
|
||||
content: '用户可以通过点击设置页面的"忘记密码"链接来重置密码。系统会发送验证邮件。',
|
||||
vector: [0.12, 0.88, 0.05],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '定价策略',
|
||||
content: '基础版每月 $10,专业版每月 $29。企业版需要联系销售团队获取报价。',
|
||||
vector: [0.85, 0.15, 0.10],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'API 文档',
|
||||
content: '所有 API 请求都需要在 Header 中包含 Bearer Token 进行身份验证。',
|
||||
vector: [0.30, 0.20, 0.95],
|
||||
score: 0
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '账户安全',
|
||||
content: '为了账户安全,建议开启双重认证 (2FA)。定期修改密码也是好习惯。',
|
||||
vector: [0.15, 0.85, 0.12],
|
||||
score: 0
|
||||
}
|
||||
])
|
||||
|
||||
const steps = [
|
||||
{ id: 1, label: 'Embedding', desc: '将问题转换为向量' },
|
||||
{ id: 2, label: 'Similarity Search', desc: '计算向量相似度' },
|
||||
{ id: 3, label: 'Retrieval', desc: '提取 Top-K 相关文档' },
|
||||
{ id: 4, label: 'Augmentation', desc: '注入上下文窗口' }
|
||||
]
|
||||
|
||||
const retrievedDocs = computed(() => {
|
||||
return documents.value
|
||||
.filter(doc => doc.score > 0.7)
|
||||
.filter(doc => doc.score > 0.6)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
})
|
||||
|
||||
const calculateSimilarity = (q, docVector) => {
|
||||
// Mock similarity calculation based on keywords for demo purposes
|
||||
// In reality, this would be a vector dot product
|
||||
if (q.includes('密码') || q.includes('安全')) {
|
||||
if (docVector[1] > 0.8) return 0.92 + (Math.random() * 0.05)
|
||||
if (docVector[0] > 0.8) return 0.15
|
||||
return 0.4 + (Math.random() * 0.1)
|
||||
}
|
||||
if (q.includes('价格') || q.includes('多少钱')) {
|
||||
if (docVector[0] > 0.8) return 0.95
|
||||
return 0.1
|
||||
}
|
||||
const calculateSimilarity = (q, docContent) => {
|
||||
// Simple keyword matching simulation
|
||||
if (q.includes('密码') && (docContent.includes('密码') || docContent.includes('安全'))) return 0.95
|
||||
if (q.includes('价格') && docContent.includes('价')) return 0.9
|
||||
if (q.includes('API') && docContent.includes('API')) return 0.9
|
||||
|
||||
// Random noise for non-matches
|
||||
return Math.random() * 0.3
|
||||
}
|
||||
|
||||
@@ -82,470 +60,387 @@ const search = async () => {
|
||||
isSearching.value = true
|
||||
lastQuery.value = query.value
|
||||
currentStep.value = 1
|
||||
searchTime.value = 0
|
||||
|
||||
// Reset scores
|
||||
documents.value.forEach(d => d.score = 0)
|
||||
|
||||
// Step 1: Embedding (Simulated)
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
currentStep.value = 2
|
||||
|
||||
// Step 2: Search
|
||||
const startTime = performance.now()
|
||||
documents.value.forEach(doc => {
|
||||
doc.score = calculateSimilarity(query.value, doc.vector)
|
||||
})
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
searchTime.value = Math.round(performance.now() - startTime) + 45 // Add base latency
|
||||
currentStep.value = 3
|
||||
|
||||
// Step 3: Retrieval
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
currentStep.value = 4
|
||||
// Step 1: Simulate Scanning (1.5s)
|
||||
await new Promise(r => setTimeout(r, 600))
|
||||
|
||||
// Calculate scores
|
||||
documents.value.forEach(doc => {
|
||||
doc.score = calculateSimilarity(query.value, doc.content + doc.title)
|
||||
})
|
||||
|
||||
await new Promise(r => setTimeout(r, 800)) // Wait for scan animation to finish visual impact
|
||||
|
||||
currentStep.value = 2 // Transition to retrieval
|
||||
|
||||
// Step 2: Assemble Context (1s)
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
currentStep.value = 3 // Done
|
||||
|
||||
// Step 4: Complete
|
||||
await new Promise(r => setTimeout(r, 800))
|
||||
isSearching.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rag-simulation-demo">
|
||||
<!-- Control Panel -->
|
||||
<div class="control-panel">
|
||||
<div class="search-bar">
|
||||
<div class="rag-demo">
|
||||
|
||||
<!-- Step 1: User Input -->
|
||||
<div class="step-section input-section">
|
||||
<div class="step-label">
|
||||
<span class="step-num">1</span>
|
||||
<span class="step-text">用户提问 (User Query)</span>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<input
|
||||
v-model="query"
|
||||
type="text"
|
||||
placeholder="输入问题 (例如: 怎么重置密码?)"
|
||||
placeholder="输入问题..."
|
||||
@keyup.enter="search"
|
||||
:disabled="isSearching"
|
||||
/>
|
||||
<button
|
||||
class="search-btn"
|
||||
class="action-btn"
|
||||
@click="search"
|
||||
:disabled="isSearching || !query"
|
||||
>
|
||||
{{ isSearching ? '检索中...' : '🔍 开始检索' }}
|
||||
{{ isSearching ? '检索中...' : '🚀 开始检索' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="step-indicator">
|
||||
<div
|
||||
v-for="s in steps"
|
||||
:key="s.id"
|
||||
class="step-dot"
|
||||
:class="{ active: currentStep >= s.id, current: currentStep === s.id }"
|
||||
:title="s.label"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Visualization -->
|
||||
<div class="viz-container">
|
||||
<!-- Arrow Connection -->
|
||||
<div class="flow-arrow" :class="{ active: currentStep >= 1 }">
|
||||
<div class="line"></div>
|
||||
<div class="icon">🔍</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Library Scanning -->
|
||||
<div class="step-section library-section" :class="{ 'is-scanning': currentStep === 1 }">
|
||||
<div class="step-label">
|
||||
<span class="step-num">2</span>
|
||||
<span class="step-text">图书馆检索 (Retrieval)</span>
|
||||
<span class="status-badge" v-if="currentStep === 1">正在扫描...</span>
|
||||
<span class="status-badge success" v-if="currentStep >= 2">命中 {{ retrievedDocs.length }} 条</span>
|
||||
</div>
|
||||
|
||||
<!-- Left: Vector Database -->
|
||||
<div class="panel vector-db" :class="{ dimmed: currentStep === 4 }">
|
||||
<div class="panel-header">
|
||||
<span class="icon">🗄️</span> 向量数据库 (Knowledge Base)
|
||||
</div>
|
||||
<div class="doc-list">
|
||||
<div
|
||||
v-for="doc in documents"
|
||||
:key="doc.id"
|
||||
class="doc-card"
|
||||
:class="{
|
||||
'scanning': currentStep === 2,
|
||||
'matched': doc.score > 0.7 && currentStep >= 3,
|
||||
'rejected': doc.score <= 0.7 && currentStep >= 3
|
||||
}"
|
||||
:style="{ '--score': doc.score }"
|
||||
>
|
||||
<div class="doc-icon">📄</div>
|
||||
<div class="doc-info">
|
||||
<div class="doc-title">{{ doc.title }}</div>
|
||||
<div class="doc-preview">{{ doc.content.substring(0, 20) }}...</div>
|
||||
</div>
|
||||
<div class="doc-score" v-if="currentStep >= 2 && doc.score > 0">
|
||||
{{ (doc.score * 100).toFixed(0) }}%
|
||||
</div>
|
||||
<div class="vector-visual">
|
||||
<span v-for="(v,i) in doc.vector" :key="i" :style="{ height: v * 10 + 'px' }"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: Pipeline Visuals -->
|
||||
<div class="pipeline-arrow">
|
||||
<div class="arrow-line" :class="{ active: isSearching }"></div>
|
||||
<div class="pipeline-status" v-if="currentStep > 0">
|
||||
{{ steps[currentStep - 1]?.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Augmented Context -->
|
||||
<div class="panel context-window" :class="{ active: currentStep === 4 }">
|
||||
<div class="panel-header">
|
||||
<span class="icon">🤖</span> 增强后的上下文 (Final Prompt)
|
||||
</div>
|
||||
<div class="prompt-content">
|
||||
<div class="prompt-section system">
|
||||
<span class="tag">System</span>
|
||||
<p>你是一个帮助用户的 AI 助手。请基于以下上下文回答用户的问题。</p>
|
||||
<div class="docs-grid">
|
||||
<div
|
||||
v-for="doc in documents"
|
||||
:key="doc.id"
|
||||
class="doc-card"
|
||||
:class="{
|
||||
'matched': doc.score > 0.6 && currentStep >= 2,
|
||||
'ignored': doc.score <= 0.6 && currentStep >= 2
|
||||
}"
|
||||
>
|
||||
<div class="doc-header">
|
||||
<span class="doc-icon">📄</span>
|
||||
<span class="doc-title">{{ doc.title }}</span>
|
||||
<span class="doc-score" v-if="currentStep >= 2 && doc.score > 0.6">
|
||||
{{ (doc.score * 100).toFixed(0) }}% 相关
|
||||
</span>
|
||||
</div>
|
||||
<div class="doc-content">{{ doc.content }}</div>
|
||||
|
||||
<div class="prompt-section context" v-if="currentStep >= 3">
|
||||
<span class="tag">Context (RAG)</span>
|
||||
<div v-if="retrievedDocs.length > 0">
|
||||
<div v-for="doc in retrievedDocs" :key="doc.id" class="retrieved-item">
|
||||
<span class="bullet">•</span> {{ doc.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-context">
|
||||
(暂无相关文档)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prompt-section user" v-if="lastQuery">
|
||||
<span class="tag">User</span>
|
||||
<p>{{ lastQuery }}</p>
|
||||
</div>
|
||||
<div class="placeholder-text" v-else>
|
||||
等待查询...
|
||||
</div>
|
||||
<!-- Visual effect for scanning -->
|
||||
<div class="scan-line" v-if="currentStep === 1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Metrics Footer -->
|
||||
<div class="metrics-footer">
|
||||
<div class="metric">
|
||||
<span class="label">检索耗时:</span>
|
||||
<span class="value">{{ searchTime }} ms</span>
|
||||
<!-- Arrow Connection -->
|
||||
<div class="flow-arrow" :class="{ active: currentStep >= 2 }">
|
||||
<div class="line"></div>
|
||||
<div class="icon">✂️ 复制粘贴</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Context Assembly -->
|
||||
<div class="step-section context-section" :class="{ active: currentStep >= 3 }">
|
||||
<div class="step-label">
|
||||
<span class="step-num">3</span>
|
||||
<span class="step-text">最终上下文 (Final Prompt)</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">命中数量:</span>
|
||||
<span class="value">{{ retrievedDocs.length }} docs</span>
|
||||
|
||||
<div class="blackboard">
|
||||
<div class="chalk-text system">
|
||||
<span class="role-badge">SYSTEM</span>
|
||||
你是一个专业的 AI 助手。请基于下方【检索到的资料】回答用户的提问。
|
||||
</div>
|
||||
|
||||
<div class="retrieved-block" v-if="currentStep >= 2">
|
||||
<div class="block-header">📚 检索到的资料 (Context)</div>
|
||||
<div v-if="retrievedDocs.length > 0">
|
||||
<div v-for="doc in retrievedDocs" :key="doc.id" class="retrieved-item">
|
||||
{{ doc.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
(未找到相关资料)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chalk-text user">
|
||||
<span class="role-badge">USER</span>
|
||||
{{ lastQuery || '等待提问...' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-simulation-demo {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
.rag-demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
max-width: 600px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
.step-section {
|
||||
position: relative;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
.step-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.8rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.step-num {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Input Section */
|
||||
.search-box {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.8rem;
|
||||
padding: 0.6rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
input:focus {
|
||||
border-color: var(--vp-c-brand);
|
||||
outline: none;
|
||||
background: var(--vp-c-bg-alt);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: var(--vp-c-brand);
|
||||
.action-btn {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.search-btn:hover:not(:disabled) {
|
||||
background-color: var(--vp-c-brand-dark);
|
||||
}
|
||||
.search-btn:disabled {
|
||||
.action-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.step-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.step-dot.active {
|
||||
background-color: var(--vp-c-brand);
|
||||
}
|
||||
.step-dot.current {
|
||||
transform: scale(1.4);
|
||||
box-shadow: 0 0 4px var(--vp-c-brand);
|
||||
}
|
||||
|
||||
/* Viz Container */
|
||||
.viz-container {
|
||||
display: flex;
|
||||
padding: 1.5rem;
|
||||
gap: 1rem;
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
min-height: 350px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
background-color: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.panel.dimmed {
|
||||
opacity: 0.6;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
.panel.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 0 15px rgba(var(--vp-c-brand-rgb), 0.1);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 0.8rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.doc-list {
|
||||
padding: 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
overflow-y: auto;
|
||||
max-height: 300px;
|
||||
/* Library Section */
|
||||
.docs-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.doc-card {
|
||||
padding: 0.6rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.doc-card.matched {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-dimm);
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.doc-card.ignored {
|
||||
opacity: 0.4;
|
||||
filter: grayscale(0.8);
|
||||
}
|
||||
|
||||
.doc-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.doc-score {
|
||||
color: var(--vp-c-brand);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.doc-content {
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Scanning Animation */
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
animation: scan 1s infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { left: -100%; }
|
||||
100% { left: 200%; }
|
||||
}
|
||||
|
||||
/* Context Section */
|
||||
.blackboard {
|
||||
background: #2b2b2b;
|
||||
color: #e0e0e0;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
border: 2px solid #444;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
display: inline-block;
|
||||
background: #444;
|
||||
color: #aaa;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chalk-text {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.retrieved-block {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
padding: 0.6rem;
|
||||
margin: 0.5rem 0 1rem 0;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.block-header {
|
||||
color: var(--vp-c-brand);
|
||||
font-weight: bold;
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.4rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.retrieved-item {
|
||||
margin-bottom: 0.4rem;
|
||||
padding-left: 0.8rem;
|
||||
position: relative;
|
||||
}
|
||||
.retrieved-item::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
/* Arrows */
|
||||
.flow-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
color: var(--vp-c-divider);
|
||||
position: relative;
|
||||
transition: all 0.3s;
|
||||
background-color: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.doc-card.scanning {
|
||||
animation: pulse 1s infinite;
|
||||
border-color: var(--vp-c-brand-dimm);
|
||||
}
|
||||
.doc-card.matched {
|
||||
border-color: var(--vp-c-green);
|
||||
background-color: var(--vp-c-green-dimm);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
.doc-card.rejected {
|
||||
opacity: 0.5;
|
||||
.flow-arrow .line {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.doc-icon {
|
||||
.flow-arrow .icon {
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 4px;
|
||||
z-index: 1;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.doc-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.flow-arrow.active .line {
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
.doc-title {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-1);
|
||||
.flow-arrow.active .icon {
|
||||
animation: bounce 1s infinite;
|
||||
}
|
||||
.doc-preview {
|
||||
color: var(--vp-c-text-2);
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.doc-score {
|
||||
font-family: var(--vp-font-mono);
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
.vector-visual {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: flex-end;
|
||||
height: 15px;
|
||||
width: 20px;
|
||||
}
|
||||
.vector-visual span {
|
||||
width: 4px;
|
||||
background-color: var(--vp-c-text-3);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* Pipeline Arrow */
|
||||
.pipeline-arrow {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.arrow-line {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background-color: var(--vp-c-divider);
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.arrow-line.active {
|
||||
background: linear-gradient(90deg, var(--vp-c-brand), var(--vp-c-brand-light));
|
||||
background-size: 200% 100%;
|
||||
animation: flow 1s linear infinite;
|
||||
}
|
||||
.pipeline-status {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--vp-c-brand);
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Context Window */
|
||||
.prompt-content {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
font-family: var(--vp-font-mono);
|
||||
font-size: 0.85rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.prompt-section {
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 0.8rem;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.prompt-section.system {
|
||||
border-left-color: var(--vp-c-yellow);
|
||||
}
|
||||
.prompt-section.context {
|
||||
border-left-color: var(--vp-c-green);
|
||||
background-color: rgba(var(--vp-c-green-rgb), 0.1);
|
||||
}
|
||||
.prompt-section.user {
|
||||
border-left-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.4rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
.retrieved-item {
|
||||
margin-top: 0.4rem;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
.empty-context {
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
.placeholder-text {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-top: 2rem;
|
||||
|
||||
.status-badge.success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
/* Metrics Footer */
|
||||
.metrics-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0.8rem;
|
||||
background-color: var(--vp-c-bg);
|
||||
border-top: 1px solid var(--vp-c-divider);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.metric .label {
|
||||
color: var(--vp-c-text-2);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.metric .value {
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
@keyframes flow {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.viz-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.pipeline-arrow {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
flex-direction: row;
|
||||
}
|
||||
.arrow-line {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(3px); }
|
||||
}
|
||||
</style>
|
||||
|
||||
+44
-43
@@ -17,12 +17,12 @@
|
||||
<div class="stat-group">
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ totalMessages }}</span>
|
||||
<span class="label">Total Messages</span>
|
||||
<span class="label">现在一共记了几条</span>
|
||||
</div>
|
||||
<div class="stat-divider">/</div>
|
||||
<div class="stat-item">
|
||||
<span class="value">{{ maxSlots }}</span>
|
||||
<span class="label">Window Capacity</span>
|
||||
<span class="label">黑板最多能记几条</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-bar">
|
||||
@@ -39,8 +39,8 @@
|
||||
<div class="context-section pinned-section">
|
||||
<div class="section-header">
|
||||
<span class="icon">📌</span>
|
||||
<span class="title">Pinned Context (Protected)</span>
|
||||
<span class="count">{{ pinnedMessages.length }} items</span>
|
||||
<span class="title">钉住区(永远保留的重要信息)</span>
|
||||
<span class="count">当前 {{ pinnedMessages.length }} 条</span>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<transition-group name="list">
|
||||
@@ -56,10 +56,10 @@
|
||||
class="pin-btn active"
|
||||
@click="togglePin(msg)"
|
||||
:disabled="msg.role === 'System'"
|
||||
title="Unpin message"
|
||||
title="取消钉住"
|
||||
>
|
||||
<span v-if="msg.role === 'System'">🔒 Fixed</span>
|
||||
<span v-else>📌 Unpin</span>
|
||||
<span v-if="msg.role === 'System'">🔒 系统信息固定在这</span>
|
||||
<span v-else>📌 取消钉住</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content">{{ msg.content }}</div>
|
||||
@@ -72,8 +72,8 @@
|
||||
<div class="context-section scrolling-section">
|
||||
<div class="section-header">
|
||||
<span class="icon">📜</span>
|
||||
<span class="title">Scrolling Context (FIFO)</span>
|
||||
<span class="count">{{ scrollingMessages.length }} items</span>
|
||||
<span class="title">会被“挤走”的普通对话(先进先出)</span>
|
||||
<span class="count">当前 {{ scrollingMessages.length }} 条</span>
|
||||
</div>
|
||||
<div class="message-list">
|
||||
<transition-group name="list">
|
||||
@@ -85,15 +85,15 @@
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="role-badge">{{ msg.role }}</span>
|
||||
<button class="pin-btn" @click="togglePin(msg)" title="Pin message">
|
||||
📌 Pin
|
||||
<button class="pin-btn" @click="togglePin(msg)" title="把这条钉在黑板上">
|
||||
📌 钉住这条
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-content">{{ msg.content }}</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="scrollingMessages.length === 0" class="empty-state">
|
||||
No scrolling messages...
|
||||
这里是“普通对话区”,暂时还空着
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,24 +104,24 @@
|
||||
<input
|
||||
v-model="newMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="Add a new fact or message..."
|
||||
placeholder="在这里输入一条新的信息,比如“我叫小明”"
|
||||
/>
|
||||
<button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim()">
|
||||
Add
|
||||
添加到黑板
|
||||
</button>
|
||||
</div>
|
||||
<div class="presets">
|
||||
<button class="preset-btn" @click="addPreset('My name is Alice.')">User: My name is Alice</button>
|
||||
<button class="preset-btn" @click="addPreset('The password is 1234.')">User: Password is 1234</button>
|
||||
<button class="preset-btn" @click="addPreset('我的名字叫 Alice。')">用户:我的名字叫 Alice</button>
|
||||
<button class="preset-btn" @click="addPreset('系统密码是 1234。')">用户:系统密码是 1234</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>Note:</strong>
|
||||
"选择性保留" 解决了滑动窗口的遗忘问题。
|
||||
System Prompt 通常被永久钉住。用户也可以通过某些机制(如 RAG 或 记忆模块)将重要信息(如名字、密码)钉在窗口中,防止被挤出。
|
||||
<strong>说明:</strong>
|
||||
“选择性保留”就是:重要的就钉在黑板上,普通的让它自己滑走。
|
||||
系统提示通常会永久钉住,用户的关键信息(比如名字、账号、重要偏好)也可以通过记忆模块或 RAG 钉在这里,避免被新对话挤掉。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -194,7 +194,7 @@ const togglePin = (msg) => {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
@@ -202,10 +202,10 @@ const togglePin = (msg) => {
|
||||
.control-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
@@ -259,8 +259,8 @@ const togglePin = (msg) => {
|
||||
.visualization-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.context-section {
|
||||
@@ -276,7 +276,7 @@ const togglePin = (msg) => {
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
@@ -298,15 +298,15 @@ const togglePin = (msg) => {
|
||||
}
|
||||
|
||||
.message-list {
|
||||
padding: 1rem;
|
||||
min-height: 80px;
|
||||
padding: 0.5rem;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.message-card {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: var(--vp-c-bg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
@@ -327,11 +327,11 @@ const togglePin = (msg) => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.role-badge {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
padding: 2px 6px;
|
||||
@@ -344,8 +344,8 @@ const togglePin = (msg) => {
|
||||
background: transparent;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 6px;
|
||||
font-size: 0.7rem;
|
||||
cursor: pointer;
|
||||
color: var(--vp-c-text-2);
|
||||
transition: all 0.2s;
|
||||
@@ -369,19 +369,19 @@ const togglePin = (msg) => {
|
||||
}
|
||||
|
||||
.card-content {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@@ -392,7 +392,7 @@ const togglePin = (msg) => {
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
@@ -405,13 +405,14 @@ input:focus {
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 0 1.5rem;
|
||||
padding: 0 1rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
@@ -442,10 +443,10 @@ input:focus {
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
|
||||
+56
-55
@@ -15,15 +15,15 @@
|
||||
<div class="sliding-window-demo">
|
||||
<div class="control-panel">
|
||||
<div class="info-stat">
|
||||
<span class="label">Window Size / 窗口大小</span>
|
||||
<span class="value">{{ windowSize }} Messages</span>
|
||||
<span class="label">窗口里最多能记住几条对话</span>
|
||||
<span class="value">最多 {{ windowSize }} 条</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="action-btn" @click="autoPlay" :disabled="isAutoPlaying">
|
||||
▶ Auto Play
|
||||
▶ 自动演示
|
||||
</button>
|
||||
<button class="action-btn outline" @click="reset">
|
||||
↺ Reset
|
||||
↺ 重新开始
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
<!-- Forgotten / History Zone -->
|
||||
<div class="zone history-zone">
|
||||
<div class="zone-label">
|
||||
<span class="icon">🗑️</span> Forgotten (History)
|
||||
<span class="icon">🗑️</span> 已被遗忘的内容
|
||||
</div>
|
||||
<transition-group name="fade-list">
|
||||
<div
|
||||
@@ -50,21 +50,21 @@
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="historyMessages.length === 0" class="empty-placeholder">
|
||||
No history yet...
|
||||
这里暂时还没有被“挤出去”的对话
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="window-divider">
|
||||
<span>⬆ Out of Context</span>
|
||||
<span>⬆ 窗口外(模型已经看不到)</span>
|
||||
<div class="divider-line"></div>
|
||||
<span>⬇ In Context</span>
|
||||
<span>⬇ 窗口内(模型还能看到)</span>
|
||||
</div>
|
||||
|
||||
<!-- Active Window Zone -->
|
||||
<div class="zone active-zone">
|
||||
<div class="zone-label">
|
||||
<span class="icon">🖼️</span> Active Context Window
|
||||
<span class="icon">🖼️</span> 当前还在记忆里的对话
|
||||
</div>
|
||||
<transition-group name="slide-list">
|
||||
<div
|
||||
@@ -81,7 +81,7 @@
|
||||
</div>
|
||||
</transition-group>
|
||||
<div v-if="activeMessages.length === 0" class="empty-placeholder">
|
||||
Start the conversation...
|
||||
从这里开始聊天,看看旧对话是怎么被“挤出去”的
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,20 +91,20 @@
|
||||
<input
|
||||
v-model="newMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="Type a message..."
|
||||
placeholder="在这里输入一条消息,然后点发送"
|
||||
:disabled="isAutoPlaying"
|
||||
/>
|
||||
<button class="send-btn" @click="sendMessage" :disabled="!newMessage.trim() || isAutoPlaying">
|
||||
Send
|
||||
发送消息
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
<span class="icon">💡</span>
|
||||
<strong>Note:</strong>
|
||||
滑动窗口是最简单的记忆管理策略。它保证了 Token 永远不会溢出,但代价是"健忘"。
|
||||
一旦消息滑出窗口(进入上方灰色区域),模型就完全不知道它的存在了。
|
||||
<strong>说明:</strong>
|
||||
滑动窗口是最简单的记忆管理方式:新的进来,旧的出去。
|
||||
好处是永远不会“撑爆脑子”,代价就是——一旦滑出窗口(上面灰色区域),模型就完全忘了它存在过。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -151,25 +151,25 @@ const addMessage = (role, content) => {
|
||||
const autoPlay = async () => {
|
||||
isAutoPlaying.value = true
|
||||
const script = [
|
||||
"Hello there!",
|
||||
"Hi! I'm an AI assistant.",
|
||||
"What is your name?",
|
||||
"I am Model GPT-X.",
|
||||
"Do you remember my first message?",
|
||||
"Yes, you said 'Hello there!'.",
|
||||
"Tell me a joke.",
|
||||
"Why did the chicken cross the road?",
|
||||
"To get to the other side!",
|
||||
"Haha, classic.",
|
||||
"Wait, what was my name again?",
|
||||
"I... I don't remember. It fell out of my context window!"
|
||||
'你好,我是张三。',
|
||||
'你好呀,我是你的 AI 助手。',
|
||||
'我今天有点累,帮我记录一下待办吧。',
|
||||
'没问题,你可以把待办一条条发给我。',
|
||||
'第一件事:给客户发邮件。',
|
||||
'好的,已经记下来了。',
|
||||
'第二件事:晚上去买菜做饭。',
|
||||
'收到,也帮你记住了。',
|
||||
'第三件事:记得给女朋友买花。',
|
||||
'这条也帮你写在“小黑板”上了。',
|
||||
'现在还记得我第一句话说了什么吗?',
|
||||
'呃……我只看得到窗口里的几条,最早那句已经被挤出去了。'
|
||||
]
|
||||
|
||||
for (const line of script) {
|
||||
if (!isAutoPlaying.value) break
|
||||
const role = messages.value.length % 2 === 0 ? 'User' : 'AI'
|
||||
addMessage(role, line)
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
await new Promise((r) => setTimeout(r, 1500))
|
||||
}
|
||||
isAutoPlaying.value = false
|
||||
}
|
||||
@@ -186,7 +186,7 @@ const reset = () => {
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
background-color: var(--vp-c-bg-soft);
|
||||
padding: 1.5rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
font-family: var(--vp-font-family-mono);
|
||||
}
|
||||
@@ -195,9 +195,9 @@ const reset = () => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--vp-c-bg);
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
@@ -245,10 +245,10 @@ const reset = () => {
|
||||
}
|
||||
|
||||
.visualization-area {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--vp-c-bg-alt);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ const reset = () => {
|
||||
}
|
||||
|
||||
.zone {
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
@@ -276,14 +276,14 @@ const reset = () => {
|
||||
border: 2px solid var(--vp-c-brand);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
margin-top: 0.5rem;
|
||||
min-height: 150px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.zone-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
@@ -306,9 +306,9 @@ const reset = () => {
|
||||
|
||||
.message-bubble {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
margin-bottom: 0.8rem;
|
||||
padding: 0.6rem;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
@@ -334,9 +334,9 @@ const reset = () => {
|
||||
}
|
||||
|
||||
.avatar {
|
||||
font-size: 1.2rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-size: 1rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -347,37 +347,37 @@ const reset = () => {
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 80%;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.role-name {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
color: var(--vp-c-text-3);
|
||||
margin-bottom: 0.2rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.empty-placeholder {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
font-style: italic;
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
@@ -390,7 +390,7 @@ input:focus {
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 0 1.5rem;
|
||||
padding: 0 1rem;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
@@ -398,6 +398,7 @@ input:focus {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
@@ -411,10 +412,10 @@ input:focus {
|
||||
|
||||
.info-box {
|
||||
background-color: var(--vp-c-bg-alt);
|
||||
padding: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user