docs: update content and components across multiple files
- Refine chapter introductions in zh-cn docs for clarity and conciseness - Update navigation links to include '/easy-vibe' prefix - Simplify UI components (ChapterIntroduction, ContextWindowVisualizer) - Add new agent-related demo components (AgentMemoryDemo, AgentToolUseDemo) - Improve context compression demo with better visuals and metrics - Adjust styling and layout across various components
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
<template>
|
||||
<div class="memory-demo">
|
||||
<div class="header">
|
||||
<div class="title">💾 Agent 的记忆系统</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<div class="quick-actions">
|
||||
<button
|
||||
v-for="action in quickActions"
|
||||
:key="action"
|
||||
class="action-btn"
|
||||
@click="sendMessage(action)"
|
||||
:disabled="isTyping"
|
||||
>
|
||||
{{ action }}
|
||||
</button>
|
||||
<button class="action-btn reset" @click="resetConversation">🔄 重置</button>
|
||||
</div>
|
||||
|
||||
<!-- 主区域 -->
|
||||
<div class="main-area">
|
||||
<!-- 对话区 -->
|
||||
<div class="chat-box">
|
||||
<div class="box-header">💬 对话</div>
|
||||
<div class="messages" ref="chatContainer">
|
||||
<div v-for="(msg, i) in messages.slice(-4)" :key="i" class="msg-row" :class="msg.role">
|
||||
<span class="avatar">{{ msg.role === 'user' ? '👤' : '🤖' }}</span>
|
||||
<span class="text">{{ msg.content }}</span>
|
||||
</div>
|
||||
<div v-if="isTyping" class="msg-row assistant typing">
|
||||
<span class="avatar">🤖</span>
|
||||
<span class="dots"><span></span><span></span><span></span></span>
|
||||
</div>
|
||||
<div v-if="messages.length === 0" class="empty-msg">点击上方按钮开始对话</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 三种记忆并排 -->
|
||||
<div class="memory-row">
|
||||
<div class="memory-card">
|
||||
<div class="card-header">
|
||||
<span>⏱️ 短期记忆</span>
|
||||
<span class="count">{{ shortTermMemory.length }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-for="(item, i) in shortTermMemory.slice(-3)" :key="i" class="mem-item">
|
||||
<span class="role">{{ item.role === 'user' ? 'U' : 'A' }}</span>
|
||||
<span class="content">{{ truncate(item.content, 20) }}</span>
|
||||
</div>
|
||||
<div v-if="shortTermMemory.length === 0" class="empty">空</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="memory-card">
|
||||
<div class="card-header">
|
||||
<span>📝 工作记忆</span>
|
||||
<span class="count">{{ Object.keys(workingMemory).length }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-for="(v, k) in workingMemory" :key="k" class="mem-item kv">
|
||||
<span class="key">{{ k }}</span>
|
||||
<span class="value">{{ v }}</span>
|
||||
</div>
|
||||
<div v-if="Object.keys(workingMemory).length === 0" class="empty">空</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="memory-card">
|
||||
<div class="card-header">
|
||||
<span>🗄️ 长期记忆</span>
|
||||
<span class="count">{{ longTermMemory.length }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-for="(item, i) in longTermMemory.slice(-2)" :key="i" class="mem-item">
|
||||
<span class="tag">{{ item.category }}</span>
|
||||
<span class="content">{{ item.content }}</span>
|
||||
</div>
|
||||
<div v-if="longTermMemory.length === 0" class="empty">空</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 记忆操作提示 -->
|
||||
<div v-if="lastOp" class="op-bar">
|
||||
<span>{{ lastOp.icon }}</span>
|
||||
<span>{{ lastOp.text }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<div class="tip-bar">
|
||||
<span>💡</span>
|
||||
<span><strong>短期</strong>=当前对话,<strong>工作</strong>=临时变量,<strong>长期</strong>=跨会话知识</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from 'vue'
|
||||
|
||||
const messages = ref([])
|
||||
const shortTermMemory = ref([])
|
||||
const workingMemory = ref({})
|
||||
const longTermMemory = ref([])
|
||||
const isTyping = ref(false)
|
||||
const lastOp = ref(null)
|
||||
|
||||
const quickActions = [
|
||||
'我叫张三',
|
||||
'我喜欢 Python',
|
||||
'推荐编程书',
|
||||
'我叫什么?'
|
||||
]
|
||||
|
||||
const responses = {
|
||||
'我叫张三': {
|
||||
reply: '好的,我记住了你叫张三。',
|
||||
op: { icon: '💾', text: '长期记忆: 姓名=张三' },
|
||||
update: () => longTermMemory.value.push({ category: '身份', content: '姓名: 张三' })
|
||||
},
|
||||
'我喜欢 Python': {
|
||||
reply: '收到!记录了你偏好 Python。',
|
||||
op: { icon: '💾', text: '工作记忆: 偏好=Python | 长期记忆: 技术偏好' },
|
||||
update: () => {
|
||||
workingMemory.value['偏好'] = 'Python'
|
||||
longTermMemory.value.push({ category: '偏好', content: '编程语言: Python' })
|
||||
}
|
||||
},
|
||||
'推荐编程书': {
|
||||
reply: '基于你偏好 Python,推荐《流畅的Python》。',
|
||||
op: { icon: '🔍', text: '检索工作记忆: 偏好=Python → 生成推荐' }
|
||||
},
|
||||
'我叫什么?': {
|
||||
reply: '你叫张三。',
|
||||
op: { icon: '🔍', text: '检索长期记忆: 姓名=张三' }
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async (text) => {
|
||||
messages.value.push({ role: 'user', content: text })
|
||||
shortTermMemory.value.push({ role: 'user', content: text })
|
||||
isTyping.value = true
|
||||
scrollToBottom()
|
||||
|
||||
await wait(600)
|
||||
|
||||
const config = responses[text] || { reply: '收到', op: null, update: () => {} }
|
||||
config.update()
|
||||
lastOp.value = config.op
|
||||
|
||||
messages.value.push({ role: 'assistant', content: config.reply })
|
||||
shortTermMemory.value.push({ role: 'assistant', content: config.reply })
|
||||
isTyping.value = false
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
const resetConversation = () => {
|
||||
messages.value = []
|
||||
shortTermMemory.value = []
|
||||
workingMemory.value = {}
|
||||
longTermMemory.value = []
|
||||
lastOp.value = null
|
||||
isTyping.value = false
|
||||
}
|
||||
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
const container = document.querySelector('.messages')
|
||||
if (container) container.scrollTop = container.scrollHeight
|
||||
}
|
||||
|
||||
const truncate = (text, len) => text.length > len ? text.slice(0, len) + '...' : text
|
||||
const wait = (ms) => new Promise(r => setTimeout(r, ms))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.memory-demo {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(120deg, var(--vp-c-brand), #9c27b0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* 快捷操作 */
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 16px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover:not(:disabled) {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.action-btn.reset {
|
||||
background: #fee2e2;
|
||||
border-color: #fecaca;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
/* 主区域 */
|
||||
.main-area {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-area { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* 对话区 */
|
||||
.chat-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
padding: 10px 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 12px;
|
||||
min-height: 120px;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.msg-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.msg-row.user { flex-direction: row-reverse; }
|
||||
|
||||
.avatar {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.msg-row.user .text {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.dots span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: var(--vp-c-text-3);
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.4s infinite;
|
||||
}
|
||||
|
||||
.dots span:nth-child(1) { animation-delay: 0s; }
|
||||
.dots span:nth-child(2) { animation-delay: 0.2s; }
|
||||
.dots span:nth-child(3) { animation-delay: 0.4s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% { transform: scale(0); }
|
||||
40% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.empty-msg {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
padding: 40px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 记忆行 */
|
||||
.memory-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.memory-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.memory-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.count {
|
||||
padding: 2px 8px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 10px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.mem-item {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mem-item .role {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
min-width: 14px;
|
||||
}
|
||||
|
||||
.mem-item .content {
|
||||
color: var(--vp-c-text-1);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mem-item.kv .key {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.mem-item.kv .value {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.mem-item .tag {
|
||||
padding: 1px 6px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-3);
|
||||
padding: 20px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 操作提示 */
|
||||
.op-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: #dcfce7;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 12px;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
/* 提示 */
|
||||
.tip-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,837 @@
|
||||
<template>
|
||||
<div class="multi-tool-principle">
|
||||
<div class="header">
|
||||
<div class="title">🔧 多工具调用原理:Agent 如何"串联"工具完成任务</div>
|
||||
<div class="subtitle">理解 Agent 的链式思考(Chain-of-Thought)和工具编排机制</div>
|
||||
</div>
|
||||
|
||||
<!-- 场景选择 -->
|
||||
<div class="scenario-tabs">
|
||||
<button
|
||||
v-for="s in scenarios"
|
||||
:key="s.id"
|
||||
:class="['tab-btn', { active: currentScenario === s.id }]"
|
||||
@click="selectScenario(s.id)"
|
||||
>
|
||||
<span>{{ s.icon }}</span>
|
||||
<span>{{ s.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 用户意图 -->
|
||||
<div class="intent-box">
|
||||
<div class="intent-label">👤 用户意图</div>
|
||||
<div class="intent-text">{{ currentData.intent }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行流程可视化 -->
|
||||
<div class="execution-flow">
|
||||
<div class="flow-title">🔄 工具调用执行流程</div>
|
||||
|
||||
<!-- 思考阶段 -->
|
||||
<div class="phase thinking-phase" :class="{ active: currentPhase >= 0 }">
|
||||
<div class="phase-header">
|
||||
<span class="phase-icon">🧠</span>
|
||||
<span class="phase-name">思考规划</span>
|
||||
<span class="phase-status">{{ currentPhase > 0 ? '✅ 完成' : currentPhase === 0 ? '🔄 进行中' : '⏳ 等待' }}</span>
|
||||
</div>
|
||||
<div v-if="currentPhase >= 0" class="phase-content">
|
||||
<div class="thought-steps">
|
||||
<div v-for="(step, idx) in currentData.planningSteps" :key="idx" class="thought-step">
|
||||
<span class="step-num">{{ idx + 1 }}</span>
|
||||
<span class="step-text">{{ step }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具执行阶段 -->
|
||||
<div class="phase tools-phase" :class="{ active: currentPhase >= 1 }">
|
||||
<div class="phase-header">
|
||||
<span class="phase-icon">🔧</span>
|
||||
<span class="phase-name">工具执行</span>
|
||||
<span class="phase-status">{{ currentPhase > 1 ? '✅ 完成' : currentPhase === 1 ? '🔄 进行中' : '⏳ 等待' }}</span>
|
||||
</div>
|
||||
<div v-if="currentPhase >= 1" class="phase-content">
|
||||
<div class="tools-chain">
|
||||
<div
|
||||
v-for="(tool, idx) in currentData.tools"
|
||||
:key="idx"
|
||||
class="tool-node"
|
||||
:class="{
|
||||
completed: currentTool > idx,
|
||||
executing: currentTool === idx,
|
||||
pending: currentTool < idx
|
||||
}"
|
||||
>
|
||||
<div class="node-connector" v-if="idx > 0">
|
||||
<div class="connector-line" :class="{ active: currentTool >= idx }"></div>
|
||||
</div>
|
||||
<div class="node-content">
|
||||
<div class="node-icon">{{ tool.icon }}</div>
|
||||
<div class="node-name">{{ tool.name }}</div>
|
||||
<div class="node-status">
|
||||
<span v-if="currentTool > idx" class="status-done">✓</span>
|
||||
<span v-else-if="currentTool === idx" class="status-running">
|
||||
<span class="pulse"></span>
|
||||
</span>
|
||||
<span v-else class="status-wait">○</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具详情 -->
|
||||
<div v-if="currentTool >= idx" class="tool-detail-popup">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">输入:</span>
|
||||
<code class="detail-code">{{ tool.input }}</code>
|
||||
</div>
|
||||
<div v-if="currentTool > idx" class="detail-row">
|
||||
<span class="detail-label">输出:</span>
|
||||
<span class="detail-output">{{ truncate(tool.output, 50) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据流转示意 -->
|
||||
<div v-if="currentPhase === 1" class="data-flow-hint">
|
||||
<div class="flow-arrow">⬇️ 数据在工具间流转,上一步的输出成为下一步的输入</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果整合阶段 -->
|
||||
<div class="phase result-phase" :class="{ active: currentPhase >= 2 }">
|
||||
<div class="phase-header">
|
||||
<span class="phase-icon">📝</span>
|
||||
<span class="phase-name">结果整合</span>
|
||||
<span class="phase-status">{{ currentPhase > 2 ? '✅ 完成' : currentPhase === 2 ? '🔄 进行中' : '⏳ 等待' }}</span>
|
||||
</div>
|
||||
<div v-if="currentPhase >= 2" class="phase-content">
|
||||
<div class="integration-steps">
|
||||
<div class="integration-step" :class="{ done: integrationStep >= 0 }">
|
||||
<span class="check">{{ integrationStep >= 0 ? '✓' : '○' }}</span>
|
||||
<span>收集所有工具输出</span>
|
||||
</div>
|
||||
<div class="integration-step" :class="{ done: integrationStep >= 1 }">
|
||||
<span class="check">{{ integrationStep >= 1 ? '✓' : '○' }}</span>
|
||||
<span>去重与验证</span>
|
||||
</div>
|
||||
<div class="integration-step" :class="{ done: integrationStep >= 2 }">
|
||||
<span class="check">{{ integrationStep >= 2 ? '✓' : '○' }}</span>
|
||||
<span>结构化整理</span>
|
||||
</div>
|
||||
<div class="integration-step" :class="{ done: integrationStep >= 3 }">
|
||||
<span class="check">{{ integrationStep >= 3 ? '✓' : '○' }}</span>
|
||||
<span>生成自然语言回复</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最终输出 -->
|
||||
<div class="phase output-phase" :class="{ active: currentPhase >= 3 }">
|
||||
<div class="phase-header">
|
||||
<span class="phase-icon">💬</span>
|
||||
<span class="phase-name">最终输出</span>
|
||||
<span class="phase-status">{{ currentPhase >= 3 ? '✅ 完成' : '⏳ 等待' }}</span>
|
||||
</div>
|
||||
<div v-if="currentPhase >= 3" class="phase-content">
|
||||
<div class="final-output">
|
||||
<div class="output-bubble">{{ currentData.finalOutput }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="controls">
|
||||
<button v-if="!isRunning && currentPhase === -1" class="control-btn primary" @click="startDemo">
|
||||
▶ 开始演示
|
||||
</button>
|
||||
<button v-else-if="isRunning" class="control-btn" disabled>
|
||||
⏳ 执行中...
|
||||
</button>
|
||||
<button v-else class="control-btn secondary" @click="reset">
|
||||
🔄 重新演示
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 原理说明 -->
|
||||
<div class="principle-explanation">
|
||||
<div class="explanation-title">📚 核心原理</div>
|
||||
<div class="explanation-grid">
|
||||
<div class="explanation-card">
|
||||
<div class="card-icon">🧩</div>
|
||||
<div class="card-title">任务分解</div>
|
||||
<div class="card-desc">Agent 将复杂任务拆解为多个子任务,每个子任务对应一个工具调用</div>
|
||||
</div>
|
||||
<div class="explanation-card">
|
||||
<div class="card-icon">🔗</div>
|
||||
<div class="card-title">链式调用</div>
|
||||
<div class="card-desc">工具按依赖关系串联执行,前一个工具的输出成为后一个工具的输入</div>
|
||||
</div>
|
||||
<div class="explanation-card">
|
||||
<div class="card-icon">🔄</div>
|
||||
<div class="card-title">动态调整</div>
|
||||
<div class="card-desc">根据中间结果,Agent 可以动态决定下一步调用哪个工具</div>
|
||||
</div>
|
||||
<div class="explanation-card">
|
||||
<div class="card-icon">🎯</div>
|
||||
<div class="card-title">结果整合</div>
|
||||
<div class="card-desc">将所有工具输出整合为连贯、有用的最终回复</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 与 LLM 对比 -->
|
||||
<div class="comparison-section">
|
||||
<div class="comparison-title">⚖️ 为什么需要多工具调用?</div>
|
||||
<div class="comparison-table">
|
||||
<div class="comparison-row header">
|
||||
<div class="col scenario">场景</div>
|
||||
<div class="col llm">普通 LLM</div>
|
||||
<div class="col agent">Agent + 多工具</div>
|
||||
</div>
|
||||
<div v-for="(item, idx) in comparisons" :key="idx" class="comparison-row">
|
||||
<div class="col scenario">{{ item.scenario }}</div>
|
||||
<div class="col llm">{{ item.llm }}</div>
|
||||
<div class="col agent">{{ item.agent }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
id: 'travel',
|
||||
icon: '✈️',
|
||||
name: '旅行规划',
|
||||
intent: '规划一个3天2晚的东京旅行,预算1万元',
|
||||
planningSteps: [
|
||||
'分析需求:东京、3天2晚、预算1万',
|
||||
'确定需要查询:机票、酒店、景点、路线、预算',
|
||||
'规划工具调用顺序:机票→酒店→景点→路线→预算汇总'
|
||||
],
|
||||
tools: [
|
||||
{ icon: '✈️', name: '查机票', input: '{from:上海, to:东京, date:3.15}', output: '往返¥3,200' },
|
||||
{ icon: '🏨', name: '查酒店', input: '{city:东京, nights:2, budget:3000}', output: '新宿酒店¥1,200/晚' },
|
||||
{ icon: '📍', name: '查景点', input: '{city:东京, days:3}', output: '推荐5个景点' },
|
||||
{ icon: '🗺️', name: '规划路线', input: '{spots:[...], days:3}', output: '3天路线规划' },
|
||||
{ icon: '💰', name: '算预算', input: '{items:[...]}', output: '总计¥8,400' }
|
||||
],
|
||||
finalOutput: '✈️ 东京3天2晚行程已规划好!\n• 机票:¥3,200\n• 酒店:¥2,400\n• 餐饮交通:¥2,000\n• 门票购物:¥1,000\n• 总计:¥8,400(剩余¥1,600)'
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
icon: '📊',
|
||||
name: '行业研究',
|
||||
intent: '生成2024年新能源汽车行业分析报告',
|
||||
planningSteps: [
|
||||
'分析需求:行业报告需要市场数据、厂商信息、技术趋势、政策',
|
||||
'确定数据来源:市场数据库、公司信息、技术文献、政策文件',
|
||||
'规划工具调用:市场数据→厂商排名→技术趋势→政策→可视化→报告生成'
|
||||
],
|
||||
tools: [
|
||||
{ icon: '📈', name: '市场数据', input: '{industry:NEV, year:2024}', output: '销量1700万辆,+35%' },
|
||||
{ icon: '🏢', name: '厂商信息', input: '{industry:NEV, top:10}', output: '比亚迪302万,特斯拉181万...' },
|
||||
{ icon: '🔋', name: '技术趋势', input: '{field:NEV, tech:[电池,智驾]}', output: '固态电池、L2+智驾普及' },
|
||||
{ icon: '📋', name: '政策查询', input: '{region:全球, topic:NEV}', output: '中国减免购置税至2027' },
|
||||
{ icon: '📊', name: '数据可视化', input: '{type:饼图, data:市场份额}', output: '生成6个图表' },
|
||||
{ icon: '📝', name: '报告生成', input: '{sections:[...]}', output: '12页完整报告' }
|
||||
],
|
||||
finalOutput: '📊 2024新能源汽车行业分析报告已完成!\n• 全球销量1700万辆(+35%)\n• 比亚迪领先(302万辆)\n• 技术趋势:固态电池、800V快充\n• 完整报告:12页,6个图表'
|
||||
},
|
||||
{
|
||||
id: 'shopping',
|
||||
icon: '🛒',
|
||||
name: '智能购物',
|
||||
intent: '买5000元笔记本,编程+轻度游戏',
|
||||
planningSteps: [
|
||||
'分析需求:5000元、编程、轻度游戏',
|
||||
'确定评估维度:机型、规格、价格、评价、性能跑分',
|
||||
'规划工具调用:搜索→查规格→比价格→看评价→跑分对比'
|
||||
],
|
||||
tools: [
|
||||
{ icon: '🔍', name: '搜索机型', input: '{category:笔记本, budget:5000}', output: '找到6款候选机型' },
|
||||
{ icon: '⚙️', name: '查规格', input: '{products:[...]}', output: 'CPU/内存/屏幕参数' },
|
||||
{ icon: '💰', name: '比价格', input: '{products:[...]}', output: '价格对比表' },
|
||||
{ icon: '⭐', name: '看评价', input: '{products:[...], source:电商}', output: '好评率96% vs 94%' },
|
||||
{ icon: '📊', name: '跑分对比', input: '{products:[...], tests:[CPU,GPU]}', output: 'R7>i5,续航8h vs 6.5h' }
|
||||
],
|
||||
finalOutput: '💻 笔记本推荐结果\n🥇 首选:联想小新Pro16(¥4,999)\n• R7-7840HS/16G/1TB/2.5K\n• 性能强、屏幕好、存储大\n\n🥈 备选:ThinkBook14+(¥5,299)\n• 做工好、续航长、接口全'
|
||||
}
|
||||
]
|
||||
|
||||
const comparisons = [
|
||||
{ scenario: '查天气+穿衣建议', llm: '只能推测,无法获取实时数据', agent: '调用天气API获取实时数据,再给出穿衣建议' },
|
||||
{ scenario: '股票分析', llm: '无法获取股价,只能泛泛而谈', agent: '股价+新闻+技术分析,三个工具串联完成深度分析' },
|
||||
{ scenario: '旅行规划', llm: '只能给建议,无法查询实时价格', agent: '机票+酒店+景点+路线+预算,5个工具完成完整规划' },
|
||||
{ scenario: '数据分析', llm: '无法访问数据,只能讲分析方法', agent: '查询+分组+计算+可视化,6个工具完成完整分析' }
|
||||
]
|
||||
|
||||
const currentScenario = ref('travel')
|
||||
const currentPhase = ref(-1)
|
||||
const currentTool = ref(-1)
|
||||
const integrationStep = ref(-1)
|
||||
const isRunning = ref(false)
|
||||
|
||||
const currentData = computed(() => scenarios.find(s => s.id === currentScenario.value))
|
||||
|
||||
const selectScenario = (id) => {
|
||||
currentScenario.value = id
|
||||
reset()
|
||||
}
|
||||
|
||||
const startDemo = async () => {
|
||||
isRunning.value = true
|
||||
currentPhase.value = 0
|
||||
currentTool.value = -1
|
||||
integrationStep.value = -1
|
||||
|
||||
// 思考阶段
|
||||
await wait(1500)
|
||||
|
||||
// 工具执行阶段
|
||||
currentPhase.value = 1
|
||||
const tools = currentData.value.tools
|
||||
|
||||
for (let i = 0; i < tools.length; i++) {
|
||||
currentTool.value = i
|
||||
await wait(1200)
|
||||
}
|
||||
currentTool.value = tools.length
|
||||
|
||||
await wait(500)
|
||||
|
||||
// 结果整合阶段
|
||||
currentPhase.value = 2
|
||||
for (let i = 0; i < 4; i++) {
|
||||
integrationStep.value = i
|
||||
await wait(600)
|
||||
}
|
||||
|
||||
// 最终输出
|
||||
await wait(300)
|
||||
currentPhase.value = 3
|
||||
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
currentPhase.value = -1
|
||||
currentTool.value = -1
|
||||
integrationStep.value = -1
|
||||
isRunning.value = false
|
||||
}
|
||||
|
||||
const wait = (ms) => new Promise(r => setTimeout(r, ms))
|
||||
const truncate = (str, len) => str.length > len ? str.slice(0, len) + '...' : str
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.multi-tool-principle {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(120deg, var(--vp-c-brand), #9c27b0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 场景标签 */
|
||||
.scenario-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
/* 用户意图 */
|
||||
.intent-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.intent-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.intent-text {
|
||||
font-size: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* 执行流程 */
|
||||
.execution-flow {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.flow-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 14px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
/* 阶段 */
|
||||
.phase {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.phase.active {
|
||||
opacity: 1;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.phase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.phase-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.phase-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.phase-status {
|
||||
font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.phase-content {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
/* 思考步骤 */
|
||||
.thought-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.thought-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: #fef3c7;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.step-num {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 12px;
|
||||
color: #92400e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 工具链 */
|
||||
.tools-chain {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tool-node {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
border: 2px solid transparent;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tool-node.completed {
|
||||
border-color: #86efac;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.tool-node.executing {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.tool-node.pending {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.node-connector {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: -14px;
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.connector-line {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--vp-c-divider);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.connector-line.active {
|
||||
background: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.node-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.node-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.node-status {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-done {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.status-running .pulse {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--vp-c-brand);
|
||||
border-radius: 50%;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.5; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.status-wait {
|
||||
color: var(--vp-c-text-3);
|
||||
}
|
||||
|
||||
/* 工具详情 */
|
||||
.tool-detail-popup {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.detail-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--vp-c-text-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-code {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.detail-output {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.data-flow-hint {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
/* 整合步骤 */
|
||||
.integration-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.integration-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.integration-step.done {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.check {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 最终输出 */
|
||||
.final-output {
|
||||
padding: 12px;
|
||||
background: #dcfce7;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.output-bubble {
|
||||
font-size: 13px;
|
||||
color: #166534;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 控制按钮 */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 10px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.control-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.control-btn.primary:hover {
|
||||
background: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.control-btn.secondary {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.control-btn.secondary:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.control-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 原理解释 */
|
||||
.principle-explanation {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.explanation-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.explanation-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.explanation-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.explanation-card {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 11px;
|
||||
color: var(--vp-c-text-2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 对比表格 */
|
||||
.comparison-section {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.comparison-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
background: var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comparison-row {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--vp-c-bg);
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comparison-row.header {
|
||||
background: var(--vp-c-bg-soft);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.col.scenario {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.col.llm {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.col.agent {
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,541 @@
|
||||
<template>
|
||||
<div class="planning-demo">
|
||||
<div class="header">
|
||||
<div class="title">📋 Agent 的规划能力</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务选择 -->
|
||||
<div class="task-tabs">
|
||||
<button
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
:class="['task-btn', { active: currentTask === task.id }]"
|
||||
@click="selectTask(task.id)"
|
||||
>
|
||||
<span>{{ task.icon }}</span>
|
||||
<span>{{ task.name }}</span>
|
||||
<span class="complexity" :class="task.complexity">{{ task.complexityLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 目标 -->
|
||||
<div class="goal-bar">
|
||||
<span class="label">🎯</span>
|
||||
<span class="text">{{ currentTaskData.goal }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 执行区域 -->
|
||||
<div class="execution-area">
|
||||
<!-- 步骤进度条 -->
|
||||
<div class="steps-progress">
|
||||
<div
|
||||
v-for="(step, index) in currentTaskData.steps"
|
||||
:key="index"
|
||||
class="step-node"
|
||||
:class="{ completed: stepStatus[index] === 'completed', running: stepStatus[index] === 'running' }"
|
||||
>
|
||||
<div class="node-circle">{{ index + 1 }}</div>
|
||||
<div class="node-name">{{ step.name }}</div>
|
||||
<div v-if="index < currentTaskData.steps.length - 1" class="node-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志和思考 -->
|
||||
<div class="info-row">
|
||||
<div class="log-box">
|
||||
<div class="box-header">
|
||||
<span>📝 执行日志</span>
|
||||
<span v-if="executionStatus === 'running'" class="status running">执行中</span>
|
||||
<span v-else-if="executionStatus === 'completed'" class="status completed">已完成</span>
|
||||
</div>
|
||||
<div class="log-content">
|
||||
<div v-if="logs.length === 0" class="empty">点击"开始执行"查看过程</div>
|
||||
<div v-for="(log, i) in logs.slice(-4)" :key="i" class="log-line" :class="log.type">
|
||||
<span class="time">{{ log.time }}</span>
|
||||
<span class="icon">{{ log.icon }}</span>
|
||||
<span class="msg" v-html="log.message"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentThought" class="thought-box">
|
||||
<div class="box-header">🧠 正在思考</div>
|
||||
<div class="thought-content">{{ currentThought }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制栏 -->
|
||||
<div class="control-bar">
|
||||
<button v-if="executionStatus === 'idle'" class="ctrl-btn primary" @click="startExecution">
|
||||
▶ 开始执行
|
||||
</button>
|
||||
<button v-else-if="executionStatus === 'running'" class="ctrl-btn" disabled>
|
||||
⏳ 执行中...
|
||||
</button>
|
||||
<button v-else class="ctrl-btn" @click="reset">
|
||||
🔄 重置
|
||||
</button>
|
||||
|
||||
<div v-if="executionStatus === 'completed'" class="stats">
|
||||
<span class="stat">{{ currentTaskData.steps.length }} 步骤</span>
|
||||
<span class="stat">{{ executionTime }}s</span>
|
||||
<span class="stat">{{ toolCalls }} 调用</span>
|
||||
</div>
|
||||
|
||||
<div class="step-dots">
|
||||
<span v-for="n in currentTaskData.steps.length" :key="n" :class="['dot', { active: stepStatus[n-1] === 'completed' }]"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<div class="tip-bar">
|
||||
<span>💡</span>
|
||||
<span>规划核心:将复杂任务分解为<strong>原子操作</strong>,根据上一步结果<strong>动态调整</strong>后续计划</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
|
||||
const tasks = [
|
||||
{
|
||||
id: 'simple',
|
||||
icon: '🌤️',
|
||||
name: '查天气',
|
||||
complexity: 'easy',
|
||||
complexityLabel: '简单',
|
||||
goal: '查询北京今天的天气',
|
||||
steps: [
|
||||
{ name: '调用天气 API', tool: 'weather_api' },
|
||||
{ name: '格式化结果', tool: 'formatter' }
|
||||
],
|
||||
logs: [
|
||||
{ type: 'think', icon: '🧠', message: '需要查询北京天气' },
|
||||
{ type: 'action', icon: '🔧', message: 'weather_api(city="北京")' },
|
||||
{ type: 'result', icon: '📥', message: '晴, 25°C, 空气质量良' },
|
||||
{ type: 'complete', icon: '✅', message: '北京今天天气晴朗' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
icon: '📊',
|
||||
name: '数据分析',
|
||||
complexity: 'medium',
|
||||
complexityLabel: '中等',
|
||||
goal: '分析销售 CSV,找出销售额最高月份',
|
||||
steps: [
|
||||
{ name: '读取 CSV', tool: 'file_reader' },
|
||||
{ name: '解析数据', tool: 'data_parser' },
|
||||
{ name: '聚合计算', tool: 'calculator' },
|
||||
{ name: '生成报告', tool: 'report_generator' }
|
||||
],
|
||||
logs: [
|
||||
{ type: 'think', icon: '🧠', message: '读取销售数据文件' },
|
||||
{ type: 'action', icon: '🔧', message: 'file_reader(path="sales.csv")' },
|
||||
{ type: 'result', icon: '📥', message: '读取 1200 行数据' },
|
||||
{ type: 'think', icon: '🧠', message: '解析数据结构' },
|
||||
{ type: 'action', icon: '🔧', message: 'data_parser(data)' },
|
||||
{ type: 'result', icon: '📥', message: '解析完成' },
|
||||
{ type: 'think', icon: '🧠', message: '按月份聚合销售额' },
|
||||
{ type: 'action', icon: '🔧', message: 'calculator.aggregate(by="month")' },
|
||||
{ type: 'result', icon: '📥', message: '11月销售额最高 ¥320K' },
|
||||
{ type: 'complete', icon: '✅', message: '分析完成' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'complex',
|
||||
icon: '🔬',
|
||||
name: '研究报告',
|
||||
complexity: 'hard',
|
||||
complexityLabel: '复杂',
|
||||
goal: '调研 AI Agent 进展,撰写完整报告',
|
||||
steps: [
|
||||
{ name: '搜索资讯', tool: 'web_search' },
|
||||
{ name: '阅读文章', tool: 'web_reader' },
|
||||
{ name: '提取信息', tool: 'extractor' },
|
||||
{ name: '搜索厂商', tool: 'web_search' },
|
||||
{ name: '生成大纲', tool: 'planner' },
|
||||
{ name: '撰写报告', tool: 'writer' }
|
||||
],
|
||||
logs: [
|
||||
{ type: 'think', icon: '🧠', message: '搜索最新 AI Agent 资讯' },
|
||||
{ type: 'action', icon: '🔧', message: 'web_search("AI Agent 2024")' },
|
||||
{ type: 'result', icon: '📥', message: '找到 15 篇文章' },
|
||||
{ type: 'action', icon: '🔧', message: 'web_reader(urls=[...])' },
|
||||
{ type: 'result', icon: '📥', message: '成功读取内容' },
|
||||
{ type: 'action', icon: '🔧', message: 'extractor(fields=[...])' },
|
||||
{ type: 'result', icon: '📥', message: '提取 45 个数据点' },
|
||||
{ type: 'action', icon: '🔧', message: 'web_search("AI Agent companies")' },
|
||||
{ type: 'result', icon: '📥', message: 'OpenAI, Anthropic, Microsoft...' },
|
||||
{ type: 'action', icon: '🔧', message: 'planner.generate_outline()' },
|
||||
{ type: 'result', icon: '📥', message: '大纲生成完成' },
|
||||
{ type: 'action', icon: '🔧', message: 'writer.generate_content()' },
|
||||
{ type: 'complete', icon: '✅', message: '报告生成完成,2500字' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const currentTask = ref('simple')
|
||||
const executionStatus = ref('idle')
|
||||
const stepStatus = ref([])
|
||||
const logs = ref([])
|
||||
const currentThought = ref('')
|
||||
const executionTime = ref(0)
|
||||
const toolCalls = ref(0)
|
||||
|
||||
const currentTaskData = computed(() => tasks.find(t => t.id === currentTask.value))
|
||||
|
||||
const selectTask = (id) => {
|
||||
currentTask.value = id
|
||||
reset()
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
executionStatus.value = 'idle'
|
||||
stepStatus.value = new Array(currentTaskData.value.steps.length).fill('pending')
|
||||
logs.value = []
|
||||
currentThought.value = ''
|
||||
executionTime.value = 0
|
||||
toolCalls.value = 0
|
||||
}
|
||||
|
||||
const startExecution = async () => {
|
||||
executionStatus.value = 'running'
|
||||
stepStatus.value = new Array(currentTaskData.value.steps.length).fill('pending')
|
||||
logs.value = []
|
||||
toolCalls.value = 0
|
||||
|
||||
const startTime = Date.now()
|
||||
const taskLogs = currentTaskData.value.logs
|
||||
|
||||
for (let i = 0; i < taskLogs.length; i++) {
|
||||
const log = taskLogs[i]
|
||||
|
||||
if (log.type === 'think') currentThought.value = log.message
|
||||
if (log.type === 'action') {
|
||||
const stepIndex = Math.min(toolCalls.value, currentTaskData.value.steps.length - 1)
|
||||
stepStatus.value = stepStatus.value.map((s, idx) => {
|
||||
if (idx < stepIndex) return 'completed'
|
||||
if (idx === stepIndex) return 'running'
|
||||
return 'pending'
|
||||
})
|
||||
toolCalls.value++
|
||||
}
|
||||
if (log.type === 'complete') currentThought.value = ''
|
||||
|
||||
logs.value.push({ ...log, time: new Date().toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) })
|
||||
await wait(700)
|
||||
}
|
||||
|
||||
stepStatus.value = stepStatus.value.map(() => 'completed')
|
||||
executionTime.value = ((Date.now() - startTime) / 1000).toFixed(1)
|
||||
executionStatus.value = 'completed'
|
||||
}
|
||||
|
||||
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms))
|
||||
reset()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.planning-demo {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(120deg, var(--vp-c-brand), #9c27b0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* 任务标签 */
|
||||
.task-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.task-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.complexity {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.complexity.easy { background: #dcfce7; color: #166534; }
|
||||
.complexity.medium { background: #fef3c7; color: #92400e; }
|
||||
.complexity.hard { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
/* 目标 */
|
||||
.goal-bar {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.goal-bar .label { margin-right: 8px; }
|
||||
.goal-bar .text { font-weight: 600; }
|
||||
|
||||
/* 步骤进度 */
|
||||
.steps-progress {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.node-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg);
|
||||
border: 2px solid var(--vp-c-divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.step-node.running .node-circle {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
.step-node.completed .node-circle {
|
||||
border-color: #22c55e;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.1); }
|
||||
}
|
||||
|
||||
.node-name {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-node.completed .node-name,
|
||||
.step-node.running .node-name {
|
||||
color: var(--vp-c-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.node-line {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: -16px;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.step-node.completed + .step-node .node-line {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
/* 信息行 */
|
||||
.info-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.info-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.log-box, .thought-box {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.box-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-bottom: 1px solid var(--vp-c-divider);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status.running { background: #fef3c7; color: #92400e; }
|
||||
.status.completed { background: #dcfce7; color: #166534; }
|
||||
|
||||
.log-content {
|
||||
padding: 10px 12px;
|
||||
min-height: 100px;
|
||||
max-height: 140px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--vp-c-text-3);
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.log-line .time {
|
||||
color: var(--vp-c-text-3);
|
||||
font-size: 10px;
|
||||
min-width: 55px;
|
||||
}
|
||||
|
||||
.log-line .icon {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.log-line .msg {
|
||||
color: var(--vp-c-text-1);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.log-line.think .msg { color: #3b82f6; }
|
||||
.log-line.action .msg { color: #f59e0b; }
|
||||
.log-line.result .msg { color: #10b981; }
|
||||
.log-line.complete .msg { color: #8b5cf6; font-weight: 600; }
|
||||
|
||||
.thought-content {
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--vp-c-text-1);
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 控制栏 */
|
||||
.control-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
padding: 8px 18px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctrl-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 4px 10px;
|
||||
background: var(--vp-c-bg);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.step-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.dot.active { background: #22c55e; }
|
||||
|
||||
/* 提示 */
|
||||
.tip-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,666 @@
|
||||
<template>
|
||||
<div class="agent-chat-demo">
|
||||
<div class="header">
|
||||
<div class="title">🤖 Agent 初体验:从"能说"到"能做"</div>
|
||||
<div class="subtitle">体验 Agent 如何自动调用工具完成任务</div>
|
||||
</div>
|
||||
|
||||
<!-- 场景选择 -->
|
||||
<div class="scenario-tabs">
|
||||
<button
|
||||
v-for="s in scenarios"
|
||||
:key="s.id"
|
||||
:class="['tab-btn', { active: currentScenario === s.id }]"
|
||||
@click="selectScenario(s.id)"
|
||||
>
|
||||
<span>{{ s.icon }}</span>
|
||||
<span>{{ s.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 聊天窗口 -->
|
||||
<div class="chat-window">
|
||||
<!-- 用户消息 -->
|
||||
<div class="message user">
|
||||
<div class="avatar">👤</div>
|
||||
<div class="bubble">{{ currentScenarioData.query }}</div>
|
||||
</div>
|
||||
|
||||
<!-- LLM 回复(对比) -->
|
||||
<div class="message llm">
|
||||
<div class="avatar">🤖</div>
|
||||
<div class="bubble llm-bubble">
|
||||
<div class="llm-label">普通 LLM</div>
|
||||
<div class="llm-content">{{ currentScenarioData.llmResponse }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent 回复 -->
|
||||
<div class="message agent">
|
||||
<div class="avatar agent-avatar">🦾</div>
|
||||
<div class="bubble agent-bubble">
|
||||
<div class="agent-label">Agent 智能体</div>
|
||||
|
||||
<!-- 思考过程(可折叠) -->
|
||||
<div v-if="showThinking" class="thinking-section">
|
||||
<div class="thinking-header" @click="toggleThinking">
|
||||
<span>🧠 思考过程</span>
|
||||
<span class="toggle-icon">{{ thinkingExpanded ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="thinkingExpanded" class="thinking-content">
|
||||
<div class="thought-item">{{ currentScenarioData.thinking }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具调用(可折叠) -->
|
||||
<div v-if="showTools" ref="toolsSection" class="tools-section">
|
||||
<div class="tools-header" @click="toggleTools">
|
||||
<span>🔧 工具调用 ({{ currentScenarioData.tools.length }}个)</span>
|
||||
<span class="toggle-icon">{{ toolsExpanded ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="toolsExpanded" class="tools-list">
|
||||
<div
|
||||
v-for="(tool, idx) in currentScenarioData.tools"
|
||||
:key="idx"
|
||||
:ref="el => setToolRef(el, idx)"
|
||||
class="tool-item"
|
||||
:class="{ completed: toolExecuted > idx, executing: toolExecuting === idx }"
|
||||
>
|
||||
<div class="tool-status">
|
||||
<span v-if="toolExecuted > idx">✅</span>
|
||||
<span v-else-if="toolExecuting === idx" class="spinner">⏳</span>
|
||||
<span v-else>⏸️</span>
|
||||
</div>
|
||||
<div class="tool-info">
|
||||
<div class="tool-name">{{ tool.name }}</div>
|
||||
<div v-if="toolExecuted > idx || toolExecuting === idx" class="tool-detail">
|
||||
<code class="tool-params">{{ tool.params }}</code>
|
||||
<div v-if="toolExecuted > idx" class="tool-result">{{ tool.result }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最终回复 -->
|
||||
<div v-if="showResponse" class="final-response">
|
||||
<div class="response-header">💬 最终回复</div>
|
||||
<div class="response-content">{{ currentScenarioData.agentResponse }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 执行按钮 -->
|
||||
<button v-if="!isExecuting && !executionComplete" class="execute-btn" @click="startExecution">
|
||||
▶ 让 Agent 执行
|
||||
</button>
|
||||
<button v-else-if="executionComplete" class="execute-btn reset" @click="reset">
|
||||
🔄 重置对话
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心区别 -->
|
||||
<div class="insight-bar">
|
||||
<span class="insight-label">💡 核心区别:</span>
|
||||
<span class="insight-text">{{ currentScenarioData.insight }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, nextTick } from 'vue'
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
id: 'weather',
|
||||
icon: '🌤️',
|
||||
name: '查天气',
|
||||
query: '北京今天天气怎么样?适合穿什么衣服?',
|
||||
llmResponse: '我无法获取实时天气信息。北京一般在春季比较温和,建议穿薄外套。',
|
||||
thinking: '用户想知道北京今天的天气和穿衣建议。我需要:1) 查询实时天气 2) 根据温度给出穿衣建议',
|
||||
tools: [
|
||||
{ name: 'weather_api', params: '{"city": "北京", "date": "today"}', result: '☀️ 晴,15-25°C,空气质量良' }
|
||||
],
|
||||
agentResponse: '北京今天天气晴朗,15-25°C,空气质量良。建议穿薄外套或长袖T恤,早晚温差较大,可以带件薄外套。',
|
||||
insight: 'Agent 调用天气 API 获取实时数据,LLM 只能基于训练数据推测。'
|
||||
},
|
||||
{
|
||||
id: 'calculate',
|
||||
icon: '🧮',
|
||||
name: '复杂计算',
|
||||
query: '帮我算一下:如果贷款100万,年利率4.2%,30年等额本息,每月还多少?总利息多少?',
|
||||
llmResponse: '根据公式计算,月供大约5000元左右,总利息约80万。这只是估算,建议用专业计算器。',
|
||||
thinking: '这是房贷计算问题,需要用到等额本息公式。月供 = 贷款本金 × 月利率 × (1+月利率)^还款月数 / [(1+月利率)^还款月数 - 1]',
|
||||
tools: [
|
||||
{ name: 'calculator', params: '{"principal": 1000000, "rate": 0.042, "years": 30}', result: '月供: ¥4,890.19, 总利息: ¥760,468.40' }
|
||||
],
|
||||
agentResponse: '计算结果:\n• 每月还款:¥4,890.19\n• 还款总额:¥1,760,468.40\n• 总利息:¥760,468.40\n\n30年下来利息约占本金的76%。',
|
||||
insight: 'Agent 调用计算器确保 100% 准确,LLM 心算可能出错。'
|
||||
},
|
||||
{
|
||||
id: 'stock',
|
||||
icon: '📈',
|
||||
name: '股票分析',
|
||||
query: '分析一下特斯拉股票最近的表现,并预测明天走势',
|
||||
llmResponse: '我无法获取实时股票数据。特斯拉是知名电动车公司,股价波动较大,建议查看专业财经网站。',
|
||||
thinking: '用户需要特斯拉股票的近期表现分析和预测。我需要:1) 获取最新股价 2) 获取历史数据 3) 进行技术分析',
|
||||
tools: [
|
||||
{ name: 'stock_api', params: '{"symbol": "TSLA", "period": "1mo"}', result: '当前价: $248.50, 月涨幅: +12.3%, 成交量: 1.2亿' },
|
||||
{ name: 'news_search', params: '{"query": "Tesla stock news", "limit": 5}', result: '找到5条相关新闻:财报超预期、新车型发布...' },
|
||||
{ name: 'technical_analysis', params: '{"data": "TSLA_price_data", "indicators": ["MA", "RSI"]}', result: 'RSI: 68(接近超买), MA20: $235, 趋势: 上升' }
|
||||
],
|
||||
agentResponse: '特斯拉(TSLA)最近表现:\n📈 月涨幅 +12.3%,现报 $248.50\n📊 技术指标:RSI 68(接近超买),站上20日均线\n📰 消息面:财报超预期、新车型发布利好\n\n预测:短期可能回调,中长期看好。',
|
||||
insight: 'Agent 串联多个工具(股价+新闻+技术分析)完成复杂分析任务。'
|
||||
},
|
||||
{
|
||||
id: 'travel',
|
||||
icon: '✈️',
|
||||
name: '旅行规划',
|
||||
query: '帮我规划一个3天2晚的东京旅行,预算1万人民币,包含机票、酒店、景点',
|
||||
llmResponse: '东京是个好地方!我可以给你一些建议:浅草寺、东京塔、银座购物区都很值得去。预算1万可能有点紧张,建议提前预订。',
|
||||
thinking: '用户需要完整的东京3天2晚旅行规划。我需要:1) 查询机票价格 2) 搜索酒店 3) 推荐景点 4) 规划路线 5) 计算总预算',
|
||||
tools: [
|
||||
{ name: 'flight_search', params: '{"from": "上海", "to": "东京", "depart": "2024-03-15", "return": "2024-03-17"}', result: '往返机票: ¥3,200 (春秋航空)' },
|
||||
{ name: 'hotel_search', params: '{"city": "东京", "checkin": "2024-03-15", "nights": 2, "budget": 3000}', result: '新宿华盛顿酒店: ¥1,200/晚, 评分4.5' },
|
||||
{ name: 'attractions_search', params: '{"city": "东京", "days": 3}', result: '推荐景点: 浅草寺、东京塔、涩谷十字路口、明治神宫、秋叶原' },
|
||||
{ name: 'route_planner', params: '{"spots": ["浅草寺", "东京塔", "涩谷", "秋叶原"], "days": 3}', result: 'Day1: 浅草寺→东京塔, Day2: 涩谷→明治神宫, Day3: 秋叶原→银座' },
|
||||
{ name: 'budget_calculator', params: '{"flight": 3200, "hotel": 2400, "food": 1500, "transport": 500, "tickets": 800}', result: '总预算: ¥8,400 (剩余¥1,600用于购物)' }
|
||||
],
|
||||
agentResponse: '✈️ 东京3天2晚行程规划\n\n📅 Day1: 浅草寺→东京塔\n📅 Day2: 涩谷→明治神宫\n📅 Day3: 秋叶原→银座\n\n💰 预算明细:\n• 往返机票: ¥3,200\n• 酒店2晚: ¥2,400\n• 餐饮: ¥1,500\n• 交通: ¥500\n• 门票: ¥800\n• 总计: ¥8,400 (剩余¥1,600购物)',
|
||||
insight: 'Agent 调用5个工具完成机票、酒店、景点、路线、预算的完整规划。'
|
||||
},
|
||||
{
|
||||
id: 'shopping',
|
||||
icon: '🛒',
|
||||
name: '智能购物',
|
||||
query: '我想买一台5000元左右的笔记本电脑,主要用于编程和轻度游戏,推荐几款并对比',
|
||||
llmResponse: '5000元预算可以买到不错的笔记本。推荐联想小新Pro、华为MateBook、小米RedmiBook。具体配置建议16GB内存、512GB SSD。',
|
||||
thinking: '用户需要5000元价位的编程+游戏笔记本推荐。我需要:1) 搜索当前热门机型 2) 获取详细规格参数 3) 查询实时价格 4) 查看用户评价 5) 进行性能对比',
|
||||
tools: [
|
||||
{ name: 'product_search', params: '{"category": "laptop", "budget": 5000, "usage": "programming,gaming"}', result: '找到6款: 联想小新Pro16、ThinkBook14+、华为MateBook14、RedmiBook Pro15、荣耀MagicBook14、机械革命极光Pro' },
|
||||
{ name: 'specs_query', params: '{"products": ["联想小新Pro16", "ThinkBook14+", "华为MateBook14"]}', result: '小新Pro16: R7-7840HS/16G/1TB/2.5K; ThinkBook14+: i5-13500H/16G/512G/2.8K; MateBook14: i5-1340P/16G/512G/2K触控' },
|
||||
{ name: 'price_compare', params: '{"products": ["联想小新Pro16", "ThinkBook14+", "华为MateBook14"]}', result: '小新Pro16: ¥4,999; ThinkBook14+: ¥5,299; MateBook14: ¥5,499' },
|
||||
{ name: 'reviews_search', params: '{"products": ["联想小新Pro16", "ThinkBook14+"], "source": "jd,tmall"}', result: '小新Pro16: 好评率96%, praised for screen and performance; ThinkBook14+: 好评率94%, praised for build quality' },
|
||||
{ name: 'benchmark_compare', params: '{"products": ["联想小新Pro16", "ThinkBook14+"], "tests": ["cpu", "gpu", "battery"]}', result: 'CPU: 小新Pro16(R7) > ThinkBook14+(i5); GPU: 两者均为核显; 续航: ThinkBook14+ 8h > 小新Pro16 6.5h' }
|
||||
],
|
||||
agentResponse: '💻 5000元笔记本推荐对比\n\n🥇 首选: 联想小新Pro16\n• 配置: R7-7840HS/16G/1TB/2.5K 120Hz\n• 价格: ¥4,999\n• 优势: 性能强、屏幕好、存储大\n• 适合: 编程+轻度游戏\n\n🥈 备选: ThinkBook14+\n• 配置: i5-13500H/16G/512G/2.8K\n• 价格: ¥5,299\n• 优势: 做工好、续航长、接口全\n• 适合: 商务办公+编程',
|
||||
insight: 'Agent 调用5个工具完成搜索、查规格、比价格、看评价、跑分对比的完整购物决策。'
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
icon: '📊',
|
||||
name: '研究报告',
|
||||
query: '帮我生成一份2024年新能源汽车行业分析报告,包含市场规模、主要玩家、技术趋势',
|
||||
llmResponse: '新能源汽车行业正在快速发展。特斯拉、比亚迪是行业领导者。市场规模预计持续增长,电池技术是关键。建议查阅专业行业报告获取详细数据。',
|
||||
thinking: '用户需要一份完整的新能源汽车行业分析报告。我需要:1) 查询最新市场数据 2) 搜索主要厂商信息 3) 获取技术发展趋势 4) 查找政策法规 5) 生成数据可视化 6) 整理成报告格式',
|
||||
tools: [
|
||||
{ name: 'market_data', params: '{"industry": "NEV", "year": 2024, "metrics": ["size", "growth", "penetration"]}', result: '2024年全球NEV销量: 1,700万辆(+35%), 中国占比60%, 渗透率: 全球18%, 中国35%' },
|
||||
{ name: 'company_search', params: '{"industry": "NEV", "ranking": "top10"}', result: 'TOP5: 比亚迪(302万辆)、特斯拉(181万辆)、大众(77万辆)、吉利(68万辆)、上汽(63万辆)' },
|
||||
{ name: 'tech_trends', params: '{"field": "NEV", "technologies": ["battery", "autonomous", "charging"]}', result: '电池: 固态电池量产在即, 能量密度>400Wh/kg; 智驾: L2+普及率>50%, L4开始商用; 充电: 800V高压平台普及, 5分钟补能200km' },
|
||||
{ name: 'policy_search', params: '{"region": "global", "topic": "NEV_policy", "year": 2024}', result: '中国: 购置税减免延续至2027年; 欧盟: 2035年禁售燃油车; 美国: IRA法案补贴本土化生产' },
|
||||
{ name: 'data_visualization', params: '{"type": "chart", "data": "NEV_market_share", "chart_type": "pie"}', result: '生成市场份额饼图: 比亚迪35%, 特斯拉21%, 其他44%' },
|
||||
{ name: 'report_generator', params: '{"title": "2024新能源汽车行业分析报告", "sections": ["market", "players", "tech", "policy"], "format": "markdown"}', result: '报告已生成: 12页, 包含6个图表, 3个数据表格' }
|
||||
],
|
||||
agentResponse: '📊 2024新能源汽车行业分析报告\n\n📈 市场规模\n• 全球销量: 1,700万辆 (+35%)\n• 中国市场占比: 60%\n• 全球渗透率: 18% (中国35%)\n\n🏆 主要玩家\n1. 比亚迪: 302万辆 (35%)\n2. 特斯拉: 181万辆 (21%)\n3. 大众: 77万辆\n\n🔋 技术趋势\n• 固态电池能量密度>400Wh/kg\n• L2+智驾普及率超50%\n• 800V高压快充普及\n\n📋 报告已生成: 12页完整分析',
|
||||
insight: 'Agent 调用6个工具完成数据收集、分析、可视化和报告生成的完整研究流程。'
|
||||
},
|
||||
{
|
||||
id: 'code-run',
|
||||
icon: '💻',
|
||||
name: '代码执行',
|
||||
query: '生成一个1-100的随机数列表,找出其中的质数,并计算平均值',
|
||||
llmResponse: '我可以写出代码,但无法实际执行。质数是指只能被1和自身整除的数,比如2,3,5,7...',
|
||||
thinking: '用户需要:1) 生成随机数列表 2) 筛选质数 3) 计算平均值。我需要在沙箱中执行Python代码',
|
||||
tools: [
|
||||
{ name: 'python_executor', params: '{"code": "import random; nums = [random.randint(1,100) for _ in range(20)]; primes = [n for n in nums if all(n%i!=0 for i in range(2,int(n**0.5)+1)) and n>1]; print(f\"随机数: {nums}\"); print(f\"质数: {primes}\"); print(f\"平均值: {sum(nums)/len(nums):.2f}\")"}', result: '随机数: [42, 17, 89, 23, 56, 71, 34, 91, 13, 67...]\n质数: [17, 89, 23, 71, 13, 67, 47, 79]\n平均值: 52.35' }
|
||||
],
|
||||
agentResponse: '执行结果:\n🎲 生成20个随机数:42, 17, 89, 23, 56, 71...\n🔢 其中质数:17, 89, 23, 71, 13, 67, 47, 79 (共8个)\n📊 所有数平均值:52.35',
|
||||
insight: 'Agent 在沙箱中实际执行代码,LLM 只能推理无法验证结果。'
|
||||
},
|
||||
{
|
||||
id: 'data-analysis',
|
||||
icon: '📉',
|
||||
name: '数据分析',
|
||||
query: '分析一下我们公司上季度的销售数据,找出增长最快的产品线和地区',
|
||||
llmResponse: '我无法访问您公司的内部销售数据。建议您使用Excel或专业BI工具进行分析,关注同比和环比增长率。',
|
||||
thinking: '用户需要分析公司销售数据。我需要:1) 连接数据库获取销售数据 2) 按产品线分组统计 3) 按地区分组统计 4) 计算增长率 5) 生成可视化图表 6) 输出分析报告',
|
||||
tools: [
|
||||
{ name: 'database_query', params: '{"table": "sales", "period": "Q4_2023", "fields": ["product", "region", "amount", "quantity"]}', result: '获取Q4销售记录: 15,230条, 涵盖12个产品线, 8个地区' },
|
||||
{ name: 'data_groupby', params: '{"data": "sales_q4", "group": "product", "agg": ["sum", "count", "avg"]}', result: '产品线统计: 电子产品¥2.1M(+45%), 服装¥1.8M(+12%), 家居¥1.2M(+28%)...' },
|
||||
{ name: 'data_groupby', params: '{"data": "sales_q4", "group": "region", "agg": ["sum", "growth"]}', result: '地区统计: 华东¥3.2M(+38%), 华南¥2.1M(+52%), 华北¥1.5M(+15%)...' },
|
||||
{ name: 'growth_calculator', params: '{"current": "Q4_2023", "previous": "Q3_2023", "dimensions": ["product", "region"]}', result: '增长最快产品线: 电子产品+45%; 增长最快地区: 华南+52%' },
|
||||
{ name: 'chart_generator', params: '{"type": "bar", "data": "product_growth", "title": "产品线增长率对比"}', result: '生成柱状图: 电子产品45%, 家居28%, 服装12%' },
|
||||
{ name: 'chart_generator', params: '{"type": "map", "data": "region_sales", "title": "地区销售热力图"}', result: '生成中国地图热力图: 华东、华南为深色高值区' }
|
||||
],
|
||||
agentResponse: '📊 Q4销售数据分析报告\n\n🏆 增长最快产品线\n1. 电子产品: +45% (¥2.1M)\n2. 家居用品: +28% (¥1.2M)\n3. 服装: +12% (¥1.8M)\n\n🌍 增长最快地区\n1. 华南: +52% (¥2.1M)\n2. 华东: +38% (¥3.2M)\n3. 西南: +22% (¥0.8M)\n\n💡 建议: 加大电子产品投入,重点拓展华南市场',
|
||||
insight: 'Agent 调用6个工具完成数据查询、分组统计、增长率计算、可视化的完整分析流程。'
|
||||
}
|
||||
]
|
||||
|
||||
const currentScenario = ref('weather')
|
||||
const isExecuting = ref(false)
|
||||
const executionComplete = ref(false)
|
||||
const toolExecuting = ref(-1)
|
||||
const toolExecuted = ref(0)
|
||||
const showThinking = ref(false)
|
||||
const showTools = ref(false)
|
||||
const showResponse = ref(false)
|
||||
const thinkingExpanded = ref(true)
|
||||
const toolsExpanded = ref(true)
|
||||
const toolsSection = ref(null)
|
||||
const toolRefs = ref([])
|
||||
|
||||
const currentScenarioData = computed(() => scenarios.find(s => s.id === currentScenario.value))
|
||||
|
||||
const setToolRef = (el, idx) => {
|
||||
if (el) {
|
||||
toolRefs.value[idx] = el
|
||||
}
|
||||
}
|
||||
|
||||
const selectScenario = (id) => {
|
||||
currentScenario.value = id
|
||||
reset()
|
||||
}
|
||||
|
||||
const startExecution = async () => {
|
||||
isExecuting.value = true
|
||||
executionComplete.value = false
|
||||
toolExecuting.value = -1
|
||||
toolExecuted.value = 0
|
||||
showThinking.value = true
|
||||
showTools.value = false
|
||||
showResponse.value = false
|
||||
thinkingExpanded.value = true
|
||||
toolsExpanded.value = true
|
||||
|
||||
// 显示思考
|
||||
await wait(800)
|
||||
|
||||
// 显示工具调用
|
||||
showTools.value = true
|
||||
toolsExpanded.value = true
|
||||
|
||||
await nextTick()
|
||||
|
||||
const tools = currentScenarioData.value.tools
|
||||
|
||||
for (let i = 0; i < tools.length; i++) {
|
||||
toolExecuting.value = i
|
||||
|
||||
// 滚动到当前执行的工具
|
||||
await nextTick()
|
||||
const toolEl = toolRefs.value[i]
|
||||
if (toolEl && toolsSection.value) {
|
||||
toolEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}
|
||||
|
||||
await wait(1000)
|
||||
toolExecuted.value = i + 1
|
||||
toolExecuting.value = -1
|
||||
await wait(300)
|
||||
}
|
||||
|
||||
// 显示最终回复
|
||||
await wait(500)
|
||||
showResponse.value = true
|
||||
isExecuting.value = false
|
||||
executionComplete.value = true
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
isExecuting.value = false
|
||||
executionComplete.value = false
|
||||
toolExecuting.value = -1
|
||||
toolExecuted.value = 0
|
||||
showThinking.value = false
|
||||
showTools.value = false
|
||||
showResponse.value = false
|
||||
}
|
||||
|
||||
const toggleThinking = () => {
|
||||
thinkingExpanded.value = !thinkingExpanded.value
|
||||
}
|
||||
|
||||
const toggleTools = () => {
|
||||
toolsExpanded.value = !toolsExpanded.value
|
||||
}
|
||||
|
||||
const wait = (ms) => new Promise(r => setTimeout(r, ms))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agent-chat-demo {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(120deg, var(--vp-c-brand), #9c27b0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 场景标签 */
|
||||
.scenario-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 20px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
/* 聊天窗口 */
|
||||
.chat-window {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 消息 */
|
||||
.message {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar.agent-avatar {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 75%;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.message.user .bubble {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message.llm .bubble {
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.message.agent .bubble {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-bottom-left-radius: 4px;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.llm-label, .agent-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.agent-label {
|
||||
color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.llm-content {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* 思考过程 */
|
||||
.thinking-section {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thinking-header, .tools-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.thinking-header:hover, .tools-header:hover {
|
||||
background: var(--vp-c-bg-alt);
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
|
||||
.thinking-content {
|
||||
padding: 10px 12px;
|
||||
background: #fef3c7;
|
||||
font-size: 12px;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.thought-item {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 工具调用 */
|
||||
.tools-section {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tools-list {
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg);
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tool-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tool-item.completed {
|
||||
border-color: #86efac;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.tool-item.executing {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.tool-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tool-params {
|
||||
display: block;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-family: monospace;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tool-result {
|
||||
font-size: 11px;
|
||||
color: #16a34a;
|
||||
padding: 6px 8px;
|
||||
background: #dcfce7;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 最终回复 */
|
||||
.final-response {
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background: #dcfce7;
|
||||
border: 1px solid #86efac;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #166534;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.response-content {
|
||||
font-size: 13px;
|
||||
color: #166534;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* 执行按钮 */
|
||||
.execute-btn {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.execute-btn:hover {
|
||||
background: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.execute-btn.reset {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
.execute-btn.reset:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
/* 核心区别 */
|
||||
.insight-bar {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.insight-label {
|
||||
font-weight: 600;
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
.insight-text {
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="tool-use-demo">
|
||||
<div class="header">
|
||||
<div class="title">🔧 揭秘:Agent 如何调用工具?</div>
|
||||
</div>
|
||||
|
||||
<!-- 场景选择 -->
|
||||
<div class="scenario-tabs">
|
||||
<button
|
||||
v-for="s in scenarios"
|
||||
:key="s.id"
|
||||
:class="['tab-btn', { active: currentScenario === s.id }]"
|
||||
@click="selectScenario(s.id)"
|
||||
>
|
||||
<span>{{ s.icon }}</span>
|
||||
<span>{{ s.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 用户输入 -->
|
||||
<div class="user-input-bar">
|
||||
<span class="label">👤</span>
|
||||
<span class="text">"{{ currentData.userInput }}"</span>
|
||||
</div>
|
||||
|
||||
<!-- 横向流程 -->
|
||||
<div ref="flowRowRef" class="flow-row">
|
||||
<!-- 步骤1: 理解 -->
|
||||
<div class="flow-card" :class="{ active: currentStep >= 1 }">
|
||||
<div class="card-num">1</div>
|
||||
<div class="card-body">
|
||||
<div class="card-title">分析需求</div>
|
||||
<div v-if="currentStep >= 1" class="card-content">
|
||||
<div class="intent-box">
|
||||
<div class="intent-label">用户想要:</div>
|
||||
<div class="intent-value">{{ currentData.intent.type }}</div>
|
||||
</div>
|
||||
<div class="extract-box">
|
||||
<div class="extract-label">提取信息:</div>
|
||||
<div class="extract-tags">
|
||||
<span v-for="(e, i) in currentData.intent.entities" :key="i" class="entity">{{ e }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow" :class="{ active: currentStep >= 2 }">→</div>
|
||||
|
||||
<!-- 步骤2: 选工具 -->
|
||||
<div class="flow-card" :class="{ active: currentStep >= 2 }">
|
||||
<div class="card-num">2</div>
|
||||
<div class="card-body">
|
||||
<div class="card-title">选择工具</div>
|
||||
<div v-if="currentStep >= 2" class="card-content">
|
||||
<div class="tool-list">
|
||||
<div
|
||||
v-for="tool in currentData.availableTools.slice(0, 2)"
|
||||
:key="tool.name"
|
||||
class="tool-mini"
|
||||
:class="{ selected: tool.selected }"
|
||||
>
|
||||
<span>{{ tool.icon }}</span>
|
||||
<span class="tool-name">{{ tool.name }}</span>
|
||||
<span v-if="tool.selected" class="check">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow" :class="{ active: currentStep >= 3 }">→</div>
|
||||
|
||||
<!-- 步骤3: 构造参数 -->
|
||||
<div class="flow-card" :class="{ active: currentStep >= 3 }">
|
||||
<div class="card-num">3</div>
|
||||
<div class="card-body">
|
||||
<div class="card-title">构造参数</div>
|
||||
<div v-if="currentStep >= 3" class="card-content">
|
||||
<code class="params-code">{{ JSON.stringify(currentData.finalParams.params) }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow" :class="{ active: currentStep >= 4 }">→</div>
|
||||
|
||||
<!-- 步骤4: 执行 -->
|
||||
<div class="flow-card" :class="{ active: currentStep >= 4 }">
|
||||
<div class="card-num">4</div>
|
||||
<div class="card-body">
|
||||
<div class="card-title">执行返回</div>
|
||||
<div v-if="currentStep >= 4" class="card-content">
|
||||
<div class="exec-flow">
|
||||
<span class="from">Agent</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="to">{{ currentData.selectedTool }}</span>
|
||||
<span class="arrow">→</span>
|
||||
<span class="from">结果</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最终结果 -->
|
||||
<div v-if="currentStep >= 4" class="final-result">
|
||||
<span class="result-label">💬 回复:</span>
|
||||
<span class="result-text">{{ currentData.finalResponse }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 控制栏 -->
|
||||
<div class="control-bar">
|
||||
<button v-if="currentStep === 0" class="ctrl-btn primary" @click="nextStep">
|
||||
▶ 开始演示
|
||||
</button>
|
||||
<button v-else-if="currentStep < 4" class="ctrl-btn primary" @click="nextStep">
|
||||
下一步 →
|
||||
</button>
|
||||
<button v-else class="ctrl-btn" @click="reset">
|
||||
🔄 重置
|
||||
</button>
|
||||
|
||||
<div class="step-dots">
|
||||
<span v-for="n in 4" :key="n" :class="['dot', { active: currentStep >= n }]"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示 -->
|
||||
<div class="tip-bar">
|
||||
<span>💡</span>
|
||||
<span>Tool Calling 本质:LLM 生成结构化文本(JSON),外部系统执行后返回结果</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
|
||||
const scenarios = [
|
||||
{
|
||||
id: 'weather',
|
||||
icon: '🌤️',
|
||||
name: '查天气',
|
||||
userInput: '明天上海需要带伞吗?',
|
||||
intent: { type: '天气查询', entities: ['明天', '上海'], confidence: 95 },
|
||||
availableTools: [
|
||||
{ name: 'weather_api', icon: '🌤️', description: '获取天气', selected: true, score: 95 },
|
||||
{ name: 'calculator', icon: '🧮', description: '数学计算', selected: false, score: 10 },
|
||||
],
|
||||
selectedTool: 'weather_api',
|
||||
finalParams: { tool: 'weather_api', params: { city: '上海', date: 'tomorrow' } },
|
||||
finalResponse: '明天上海有小雨,建议带伞。气温 8-15°C。'
|
||||
},
|
||||
{
|
||||
id: 'calculate',
|
||||
icon: '🧮',
|
||||
name: '计算',
|
||||
userInput: '1250 除以 25 乘以 8 等于多少',
|
||||
intent: { type: '数学计算', entities: ['1250', '25', '8'], confidence: 98 },
|
||||
availableTools: [
|
||||
{ name: 'weather_api', icon: '🌤️', description: '获取天气', selected: false, score: 5 },
|
||||
{ name: 'calculator', icon: '🧮', description: '数学计算', selected: true, score: 98 },
|
||||
],
|
||||
selectedTool: 'calculator',
|
||||
finalParams: { tool: 'calculator', params: { expression: '(1250/25)*8' } },
|
||||
finalResponse: '计算结果:400。'
|
||||
},
|
||||
{
|
||||
id: 'search',
|
||||
icon: '🔍',
|
||||
name: '搜索',
|
||||
userInput: '搜索最近关于人工智能的新闻',
|
||||
intent: { type: '信息检索', entities: ['AI', '新闻'], confidence: 92 },
|
||||
availableTools: [
|
||||
{ name: 'web_search', icon: '🔍', description: '网络搜索', selected: true, score: 92 },
|
||||
{ name: 'calculator', icon: '🧮', description: '数学计算', selected: false, score: 5 },
|
||||
],
|
||||
selectedTool: 'web_search',
|
||||
finalParams: { tool: 'web_search', params: { query: 'AI news', max: 5 } },
|
||||
finalResponse: '为您找到 5 条最新 AI 新闻...'
|
||||
}
|
||||
]
|
||||
|
||||
const currentScenario = ref('weather')
|
||||
const currentStep = ref(0)
|
||||
|
||||
const currentData = computed(() => scenarios.find(s => s.id === currentScenario.value))
|
||||
|
||||
const selectScenario = (id) => {
|
||||
currentScenario.value = id
|
||||
reset()
|
||||
}
|
||||
|
||||
const flowRowRef = ref(null)
|
||||
|
||||
const nextStep = () => {
|
||||
if (currentStep.value < 4) {
|
||||
currentStep.value++
|
||||
// 自动滚动到当前步骤
|
||||
nextTick(() => {
|
||||
if (flowRowRef.value) {
|
||||
const cards = flowRowRef.value.querySelectorAll('.flow-card')
|
||||
const currentCard = cards[currentStep.value - 1]
|
||||
if (currentCard) {
|
||||
currentCard.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const reset = () => { currentStep.value = 0 }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tool-use-demo {
|
||||
background: var(--vp-c-bg-soft);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(120deg, var(--vp-c-brand), #9c27b0);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
/* 场景标签 */
|
||||
.scenario-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 16px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
border-color: var(--vp-c-brand);
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-dark);
|
||||
}
|
||||
|
||||
/* 用户输入 */
|
||||
.user-input-bar {
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-input-bar .label { margin-right: 8px; }
|
||||
.user-input-bar .text { font-weight: 600; color: var(--vp-c-text-1); }
|
||||
|
||||
/* 横向流程 */
|
||||
.flow-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.flow-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
.flow-arrow {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.flow-card {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
background: var(--vp-c-bg);
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
opacity: 0.4;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flow-card.active {
|
||||
opacity: 1;
|
||||
border-color: var(--vp-c-brand);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.card-num {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-bg-mute);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.flow-card.active .card-num {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 意图内容 */
|
||||
.intent-box {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.intent-label {
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.intent-value {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
color: var(--vp-c-brand-dark);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.extract-box {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.extract-label {
|
||||
font-size: 10px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.extract-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.entity {
|
||||
padding: 3px 8px;
|
||||
background: #fef3c7;
|
||||
border: 1px solid #fde68a;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
color: #92400e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 工具列表 */
|
||||
.tool-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tool-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
background: var(--vp-c-bg-soft);
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tool-mini.selected {
|
||||
background: #dcfce7;
|
||||
border: 1px solid #86efac;
|
||||
}
|
||||
|
||||
.tool-name { flex: 1; }
|
||||
.check { color: #16a34a; font-weight: 700; }
|
||||
|
||||
/* 参数代码 */
|
||||
.params-code {
|
||||
display: block;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 10px;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 执行流程 */
|
||||
.exec-flow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.from, .to {
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.from { background: var(--vp-c-brand-soft); color: var(--vp-c-brand-dark); }
|
||||
.to { background: #fef3c7; color: #92400e; }
|
||||
.arrow { color: var(--vp-c-text-3); }
|
||||
|
||||
/* 箭头 */
|
||||
.flow-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--vp-c-divider);
|
||||
font-size: 18px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.flow-arrow.active { color: var(--vp-c-brand); }
|
||||
|
||||
/* 最终结果 */
|
||||
.final-result {
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-left: 3px solid var(--vp-c-brand);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-label { font-weight: 600; margin-right: 8px; }
|
||||
.result-text { color: var(--vp-c-text-1); }
|
||||
|
||||
/* 控制栏 */
|
||||
.control-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
padding: 8px 18px;
|
||||
border: 1px solid var(--vp-c-divider);
|
||||
border-radius: 6px;
|
||||
background: var(--vp-c-bg);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ctrl-btn.primary {
|
||||
background: var(--vp-c-brand);
|
||||
color: white;
|
||||
border-color: var(--vp-c-brand);
|
||||
}
|
||||
|
||||
.step-dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--vp-c-divider);
|
||||
}
|
||||
|
||||
.dot.active { background: var(--vp-c-brand); }
|
||||
|
||||
/* 提示 */
|
||||
.tip-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: var(--vp-c-brand-soft);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-1);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user