Vue3 AI整合實戰:2026年5種大模型互動模式與串流回應完整方案
前端工程
Vue3 AI整合實戰:2026年5種大模型互動模式與串流回應完整方案
前端接大模型,還在用 fetch 等完整回應回來再渲染?使用者盯著空白等10秒?2026年了,AI應用的使用者體驗標準已經變了——串流輸出、即時回饋、智慧互動才是標配。本文將帶你實作5種Vue3大模型互動模式,每一種都可直接複製到專案中使用。
背景知識:大模型前端互動演進
| 階段 | 互動模式 | 使用者體驗 | 技術實作 |
|---|---|---|---|
| 1.0 | 請求-等待-回應 | 長時間空白等待 | HTTP fetch |
| 2.0 | 串流回應 | 逐字輸出,像打字 | SSE / WebSocket |
| 3.0 | Function Calling | AI主動呼叫工具 | 結構化輸出 + 前端路由 |
| 4.0 | 多模型路由 | 按場景選最優模型 | 智慧路由層 |
| 5.0 | Agent自主互動 | AI自主規劃執行 | 多輪對話 + 工具鏈 |
問題分析:為什麼傳統方案不夠?
傳統前端呼叫大模型的三大痛點:
- 等待焦慮:完整回應回來前使用者無法獲得任何回饋
- 超時崩潰:大模型回應時間不確定,長文本生成可能超過30秒
- 功能割裂:AI能力與UI互動脫節,無法實現智慧化的使用者體驗
模式一:SSE串流回應
Composable實作
// composables/useSSEChat.ts
import { ref, onUnmounted } from 'vue'
interface ChatMessage {
role: 'user' | 'assistant' | 'system'
content: string
timestamp: number
}
interface UseSSEChatOptions {
apiUrl: string
model?: string
onToken?: (token: string) => void
onError?: (error: Error) => void
onComplete?: (fullText: string) => void
}
export function useSSEChat(options: UseSSEChatOptions) {
const messages = ref<ChatMessage[]>([])
const currentResponse = ref('')
const isLoading = ref(false)
const error = ref<string | null>(null)
let abortController: AbortController | null = null
async function sendMessage(content: string) {
isLoading.value = true
error.value = null
currentResponse.value = ''
abortController = new AbortController()
messages.value.push({ role: 'user', content, timestamp: Date.now() })
try {
const response = await fetch(options.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${import.meta.env.VITE_AI_API_KEY}`,
},
body: JSON.stringify({
model: options.model || 'gpt-4',
messages: messages.value.map(({ role, content: c }) => ({ role, content: c })),
stream: true,
}),
signal: abortController.signal,
})
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`)
const reader = response.body?.getReader()
if (!reader) throw new Error('ReadableStream不可用')
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed || trimmed === 'data: [DONE]') continue
if (!trimmed.startsWith('data: ')) continue
try {
const json = JSON.parse(trimmed.slice(6))
const token = json.choices?.[0]?.delta?.content || ''
if (token) {
currentResponse.value += token
options.onToken?.(token)
}
} catch {}
}
}
messages.value.push({ role: 'assistant', content: currentResponse.value, timestamp: Date.now() })
options.onComplete?.(currentResponse.value)
} catch (e: any) {
if (e.name !== 'AbortError') {
error.value = e.message
options.onError?.(e)
}
} finally {
isLoading.value = false
abortController = null
}
}
function stopGeneration() {
abortController?.abort()
if (currentResponse.value) {
messages.value.push({ role: 'assistant', content: currentResponse.value, timestamp: Date.now() })
}
isLoading.value = false
}
function clearMessages() {
messages.value = []
currentResponse.value = ''
error.value = null
}
onUnmounted(() => { abortController?.abort() })
return { messages, currentResponse, isLoading, error, sendMessage, stopGeneration, clearMessages }
}
模式二:Function Calling前端整合
// composables/useFunctionCalling.ts
import { ref } from 'vue'
interface FunctionDefinition {
name: string
description: string
parameters: Record<string, any>
execute: (args: any) => Promise<any>
}
export function useFunctionCalling() {
const functions = ref<Map<string, FunctionDefinition>>(new Map())
const executionLog = ref<Array<{ name: string; args: any; result: any; timestamp: number }>>([])
function registerFunction(fn: FunctionDefinition) {
functions.value.set(fn.name, fn)
}
function getToolsSchema() {
return Array.from(functions.value.values()).map(fn => ({
type: 'function',
function: { name: fn.name, description: fn.description, parameters: fn.parameters },
}))
}
async function handleToolCalls(toolCalls: any[]) {
const results = []
for (const call of toolCalls) {
const fn = functions.value.get(call.function.name)
if (!fn) {
results.push({ tool_call_id: call.id, role: 'tool', content: JSON.stringify({ error: `未知函式: ${call.function.name}` }) })
continue
}
try {
const args = JSON.parse(call.function.arguments)
const result = await fn.execute(args)
executionLog.value.push({ name: call.function.name, args, result, timestamp: Date.now() })
results.push({ tool_call_id: call.id, role: 'tool', content: JSON.stringify(result) })
} catch (e: any) {
results.push({ tool_call_id: call.id, role: 'tool', content: JSON.stringify({ error: e.message }) })
}
}
return results
}
return { functions, executionLog, registerFunction, getToolsSchema, handleToolCalls }
}
模式三:多模型智慧路由
// composables/useModelRouter.ts
import { ref } from 'vue'
interface ModelConfig {
id: string
name: string
maxTokens: number
costPer1k: number
latencyMs: number
capabilities: string[]
}
const MODEL_REGISTRY: ModelConfig[] = [
{ id: 'gpt-4', name: 'GPT-4', maxTokens: 128000, costPer1k: 0.03, latencyMs: 2000, capabilities: ['reasoning', 'code', 'writing'] },
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', maxTokens: 128000, costPer1k: 0.00015, latencyMs: 500, capabilities: ['chat', 'summary'] },
{ id: 'claude-3.5-sonnet', name: 'Claude 3.5 Sonnet', maxTokens: 200000, costPer1k: 0.003, latencyMs: 1500, capabilities: ['reasoning', 'code', 'analysis'] },
{ id: 'deepseek-v3', name: 'DeepSeek V3', maxTokens: 128000, costPer1k: 0.00027, latencyMs: 800, capabilities: ['code', 'math', 'reasoning'] },
]
export function useModelRouter() {
const currentModel = ref<ModelConfig>(MODEL_REGISTRY[1])
const routingLog = ref<Array<{ input: string; model: string; reason: string }>>([])
function selectModel(input: string, options?: { preferSpeed?: boolean; preferQuality?: boolean }) {
const lower = input.toLowerCase()
if (options?.preferSpeed || lower.length < 50) {
currentModel.value = MODEL_REGISTRY[1]
routingLog.value.push({ input: input.slice(0, 50), model: currentModel.value.id, reason: '速度優先/短輸入' })
return currentModel.value
}
if (lower.includes('程式碼') || lower.includes('code') || lower.includes('debug')) {
currentModel.value = MODEL_REGISTRY[3]
routingLog.value.push({ input: input.slice(0, 50), model: currentModel.value.id, reason: '程式碼任務' })
return currentModel.value
}
if (options?.preferQuality || lower.length > 500) {
currentModel.value = MODEL_REGISTRY[0]
routingLog.value.push({ input: input.slice(0, 50), model: currentModel.value.id, reason: '品質優先/長輸入' })
return currentModel.value
}
currentModel.value = MODEL_REGISTRY[2]
routingLog.value.push({ input: input.slice(0, 50), model: currentModel.value.id, reason: '預設推理' })
return currentModel.value
}
return { currentModel, routingLog, selectModel, MODEL_REGISTRY }
}
模式四:AI對話元件(Markdown渲染)
<!-- components/AIMarkdownRenderer.vue -->
<template>
<div class="ai-markdown" v-html="renderedContent"></div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { marked } from 'marked'
const props = defineProps<{ content: string }>()
const renderedContent = computed(() => {
if (!props.content) return ''
return marked.parse(props.content, { async: false }) as string
})
</script>
模式五:Agent自主互動
// composables/useAIAgent.ts
import { ref } from 'vue'
import { useSSEChat } from './useSSEChat'
import { useFunctionCalling } from './useFunctionCalling'
interface AgentStep {
type: 'thinking' | 'tool_call' | 'tool_result' | 'response'
content: string
timestamp: number
}
export function useAIAgent(apiUrl: string) {
const steps = ref<AgentStep[]>([])
const isRunning = ref(false)
const maxIterations = 5
const chat = useSSEChat({ apiUrl })
const fc = useFunctionCalling()
fc.registerFunction({
name: 'search_web',
description: '搜尋網際網路取得最新資訊',
parameters: {
type: 'object',
properties: { query: { type: 'string', description: '搜尋關鍵字' } },
required: ['query'],
},
execute: async (args) => {
const resp = await fetch(`/api/search?q=${encodeURIComponent(args.query)}`)
return resp.json()
},
})
fc.registerFunction({
name: 'run_code',
description: '執行程式碼並回傳結果',
parameters: {
type: 'object',
properties: { code: { type: 'string', description: '要執行的程式碼' }, language: { type: 'string', description: '程式語言' } },
required: ['code'],
},
execute: async (args) => {
const resp = await fetch('/api/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args),
})
return resp.json()
},
})
async function run(task: string) {
isRunning.value = true
steps.value = []
let iteration = 0
let currentInput = task
while (iteration < maxIterations) {
iteration++
steps.value.push({ type: 'thinking', content: `第${iteration}輪思考...`, timestamp: Date.now() })
await chat.sendMessage(currentInput)
const lastMsg = chat.messages.value[chat.messages.value.length - 1]
if (!lastMsg || lastMsg.role !== 'assistant') break
steps.value.push({ type: 'response', content: lastMsg.content, timestamp: Date.now() })
const hasToolCall = lastMsg.content.includes('function_call') || lastMsg.content.includes('tool_call')
if (!hasToolCall) break
steps.value.push({ type: 'tool_call', content: '執行工具呼叫...', timestamp: Date.now() })
break
}
isRunning.value = false
}
return { steps, isRunning, run, fc }
}
避坑指南
| 序號 | 坑點 | 症狀 | 解決方案 |
|---|---|---|---|
| 1 | SSE連線未正確關閉 | 元件卸載後仍在接收資料,記憶體洩漏 | 在 onUnmounted 中呼叫 abortController.abort() |
| 2 | 串流解析buffer不完整 | 最後一行資料遺失 | 保留 buffer 殘留部分,下次read時拼接 |
| 3 | Function Calling參數解析失敗 | JSON.parse(arguments) 報錯 |
用 try-catch 包裹,降級為純文字回應 |
| 4 | 多模型路由死迴圈 | 模型A推薦模型B,模型B推薦模型A | 設定 maxIterations 上限,超過後強制使用預設模型 |
| 5 | Markdown XSS注入 | v-html 渲染惡意指令碼 |
使用 DOMPurify 清洗HTML,或使用 marked 的 sanitize 選項 |
報錯排查
| 報錯訊息 | 原因 | 解決方法 |
|---|---|---|
ReadableStream is not supported |
瀏覽器不支援串流API | 新增polyfill web-streams-polyfill,或降級為輪詢 |
net::ERR_INCOMPLETE_CHUNKED_ENCODING |
服務端SSE格式錯誤 | 確認 Content-Type: text/event-stream,每條訊息以 \n\n 結尾 |
AbortError: The user aborted a request |
使用者主動取消 | 正常行為,檢查 e.name === 'AbortError' |
429 Too Many Requests |
API呼叫頻率超限 | 實作指數退避重試,新增請求佇列 |
JSON.parse: unexpected character |
SSE資料行格式錯誤 | 檢查 data: 前綴,過濾空行和非資料行 |
CORS policy: No 'Access-Control-Allow-Origin' |
跨域請求被拒 | 服務端新增CORS標頭,或使用Vite proxy |
Cannot read property 'delta' of undefined |
SSE回應結構變化 | 新增可選鏈 json.choices?.[0]?.delta?.content |
Maximum call stack size exceeded |
Agent遞迴呼叫過深 | 限制 maxIterations,新增遞迴深度檢查 |
Failed to execute 'fetch' on 'Window' |
網路斷開 | 新增網路狀態偵測,實作離線提示和自動重連 |
TypeError: response.body is null |
回應體為空 | 檢查API端點是否支援串流,新增 response.body 空值檢查 |
進階最佳化
1. 請求佇列與限流
// utils/rateLimiter.ts
export class RequestQueue {
private queue: Array<() => Promise<any>> = []
private running = 0
private maxConcurrent: number
private minInterval: number
private lastRun = 0
constructor(maxConcurrent = 3, minInterval = 1000) {
this.maxConcurrent = maxConcurrent
this.minInterval = minInterval
}
async add<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const now = Date.now()
const wait = Math.max(0, this.minInterval - (now - this.lastRun))
if (wait > 0) await new Promise(r => setTimeout(r, wait))
this.lastRun = Date.now()
resolve(await fn())
} catch (e) { reject(e) }
})
this.process()
})
}
private async process() {
while (this.queue.length > 0 && this.running < this.maxConcurrent) {
this.running++
const fn = this.queue.shift()!
fn().finally(() => { this.running--; this.process() })
}
}
}
2. 回應快取
const responseCache = new Map<string, { content: string; timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000
export function getCachedResponse(input: string): string | null {
const cached = responseCache.get(input)
if (!cached) return null
if (Date.now() - cached.timestamp > CACHE_TTL) { responseCache.delete(input); return null }
return cached.content
}
export function setCachedResponse(input: string, content: string) {
responseCache.set(input, { content, timestamp: Date.now() })
}
對比分析
| 互動模式 | 即時性 | 複雜度 | 適用場景 | 使用者感知延遲 |
|---|---|---|---|---|
| SSE串流 | ★★★★★ | 低 | 通用對話、文字生成 | <100ms |
| Function Calling | ★★★★ | 中 | 工具呼叫、資料分析 | 200-500ms |
| 多模型路由 | ★★★ | 中 | 成本敏感、多場景 | 100-2000ms |
| Markdown渲染 | ★★★★★ | 低 | 程式碼展示、富文字 | <50ms |
| Agent自主互動 | ★★★ | 高 | 複雜任務、自動化 | 1-10s |
| 前端AI方案 | 包大小 | 串流支援 | SSR相容 | Vue3整合 |
|---|---|---|---|---|
| 自研Composable | 0KB | ★★★★★ | ★★★★★ | ★★★★★ |
| Vercel AI SDK | 12KB | ★★★★ | ★★★★ | ★★★ |
| LangChain.js | 200KB+ | ★★★ | ★★★ | ★★ |
| OpenAI SDK | 50KB | ★★★ | ★★ | ★★ |
總結:Vue3 + Composable是前端AI整合的最佳範式——SSE串流回應解決等待焦慮,Function Calling實現AI與UI的深度連動,多模型路由平衡成本與品質,Agent模式賦予AI自主執行能力。5種模式不是互斥的,而是可以組合使用的——一個生產級AI應用,往往同時需要串流輸出、工具呼叫和智慧路由。2026年,前端工程師不僅要會寫UI,更要會寫AI互動。
線上工具推薦
- JSON資料格式化:/zh-TW/json/format
- Base64編碼解碼:/zh-TW/encode/base64
- Curl轉程式碼:/zh-TW/dev/curl-to-code
本站提供瀏覽器本地工具,免註冊即可試用 →
#Vue3#AI集成#大模型#流式响应#SSE#Composable#前端AI#智能交互