Vue 3 中大模型流式输出的最佳实践
本文介绍了Vue 3组合式函数useLLMChat的设计与实现,该函数封装了大语言模型的流式输出功能。核心功能包括SSE协议处理、多种聊天模式、状态管理和中断控制。通过createSSEReader方法实现流式解码和缓冲区管理,采用双重状态标志(isLoading/isStreaming)确保交互流畅性。设计上注重错误处理的多级回调、中断机制的可靠性和资源的规范清理,但存在复用性不足、缺乏超时控制
前言
在当今 AI 应用蓬勃发展的时代,与大语言模型(LLM)的交互已成为很多应用的核心功能之一。流式输出(Server-Sent Events,SSE)能够带来更佳的用户体验——用户无需等待模型生成完整回复,而是像打字一样逐字看到AI的思考过程。
今天要分析的是一个用于封装大模型流式输出的 Vue 3 Composable:useLLMChat。它优雅地解决了流式聊天中的各种问题,让我们一起来深入理解它的设计哲学。
功能概述
基本功能
useLLMChat 是一个 Vue 3 的组合式函数(Composable),主要提供以下核心能力:
- 流式响应处理 - 支持 SSE 协议,实时接收并处理大模型的流式输出
- 多种聊天模式 - 支持直接聊天和会话聊天两种模式
- 生命周期管理 - 提供 loading、streaming、error 等状态
- 中断控制 - 支持随时停止流式输出
- 状态重置 - 提供重置功能,清空所有状态
核心 API
export function useLLMChat(options: UseLLMChatOptions = {}) {
return {
isLoading: readonly(isLoading), // 是否正在加载
isStreaming: readonly(isStreaming), // 是否正在流式输出
error: readonly(error), // 错误信息
fullResponse: readonly(fullResponse), // 完整响应内容
chunkCount: readonly(chunkCount), // 收到的 chunk 数量
chat, // 直接聊天方法
conversationChat, // 会话聊天方法
stop, // 停止流式输出
reset, // 重置状态
};
}
使用示例
const {
isLoading,
isStreaming,
fullResponse,
chat,
stop,
reset
} = useLLMChat({
onSuccess: () => console.log('Chat completed'),
onError: (err) => console.error('Error:', err),
onComplete: () => console.log('Done'),
});
// 直接调用
const response = await chat([
{ role: 'user', content: '你好,请介绍一下自己' }
], {
model: 'gpt-3.5-turbo',
provider: 'openai'
});
核心原理
SSE 数据流解析
useLLMChat 的核心在于 createSSEReader 方法,它实现了对 SSE 流的完整解析:
async function* createSSEReader(stream: ReadableStream<Uint8Array>) {
const reader = stream.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const result = await reader.read();
if (result.done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('event:')) {
currentEvent = line.slice(6).trim();
continue;
}
if (line.startsWith('data:')) {
const dataContent = line.slice(5).trim();
yield { event: currentEvent, data: dataContent };
}
}
}
}
这段代码有以下几个精妙之处:
- 流式解码 - 使用
{ stream: true }参数,避免 UTF-8 多字节字符被截断 - 缓冲区管理 - 将未处理完的剩余数据保留到下一次循环
- 事件解析 - 同时解析
event和data字段,支持多种事件类型
状态管理策略
const isLoading = ref(false); // 请求是否开始
const isStreaming = ref(false); // 是否正在流式输出
const error = ref<string | null>(null); // 错误状态
const fullResponse = ref(''); // 完整响应
const chunkCount = ref(0); // chunk 计数
状态设计遵循以下原则:
- isLoading vs isStreaming - loading 表示请求发出了,streaming 表示正在接收数据
- readonly 暴露 - 防止外部直接修改状态,保证内部一致性
- fullResponse - 累加式的响应存储,方便展示完整内容
两种聊天模式的差异
| 特性 | chat 方法 |
conversationChat 方法 |
|---|---|---|
| 用途 | 直接调用 LLM API | 维护会话上下文的聊天 |
| 配置 | 每次调用传入 | 从 llmConfig 读取 |
| 中断支持 | 无 | 使用 AbortController |
| 回调参数 | 通过 options 传入 | 通过 params 传入 |
设计理念
为什么这样设计?
-
统一的错误处理
} catch (err: any) { const errorMessage = err.message || 'Chat failed'; error.value = errorMessage; options.onError?.(errorMessage); // 调用全局错误回调 params.onError?.(errorMessage); // 调用方法级错误回调 throw err; }错误会同时触发全局和方法级的回调,让调用方有最大的灵活性。
-
中断机制的优雅实现
abortController = new AbortController(); for await (const { event, data } of reader) { if (!isStreaming.value) { // 检查中断标志 break; } // ...处理数据 } function stop() { if (abortController) { abortController.abort(); // 触发 AbortError } isStreaming.value = false; }使用
isStreaming标志位 +AbortController双重保障,确保中断及时生效。 -
资源清理的 finally 块
finally { isLoading.value = false; isStreaming.value = false; abortController = null; options.onComplete?.(); } }无论成功还是失败,都会在 finally 中重置状态,这是非常规范的做法。
优缺点分析
优点:
- 状态设计清晰,readonly 防止意外修改
- 错误处理完善,支持多级回调
- 支持中断流式输出,用户体验好
- 代码结构规范,易于维护和扩展
缺点/局限:
createSSEReader是内部方法,无法复用于其他场景- 缺乏请求超时控制
- 缺少重试机制
最佳实践
典型场景:AI 辅助生成文章摘要
在实际项目中,useLLMChat 最常见的用途之一就是 AI 辅助内容生成。下面是一个真实的业务场景——自动生成经验文章的摘要:
import { watch } from 'vue';
import { useLLMChat } from '@/composables/useLLMChat';
import { llmConfig } from '@/utils/llmConfig';
// 1. 初始化 hook,传入回调
const llmChatState = useLLMChat({
onComplete: () => {
console.log('LLM生成摘要完成');
},
});
// 2. 定义生成摘要的方法
const handleGenerateSummary = async (content: string) => {
// 每次调用前先重置状态
llmChatState.reset();
// 清理 Markdown 内容,移除图片和格式标记
const cleanContent = content
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '')
.replace(/[#*`]/g, '')
.slice(0, 2000);
const messages = [
{
role: 'system',
content: '你是一个专业的经验文章摘要生成助手。请根据提供的经验文章内容生成一段简洁的摘要,大约50-100字。',
},
{
role: 'user',
content: `请为以下经验文章生成摘要:\n\n${cleanContent}`,
},
];
// 3. 调用 chat 方法,传入配置
await llmChatState.chat(messages, {
baseUrl: llmConfig.value.baseUrl,
model: llmConfig.value.model,
apiKey: llmConfig.value.apiKey,
provider: llmConfig.value.provider,
temperature: 0.7,
maxTokens: 200,
});
};
// 4. 通过 watch 监听流式输出,实时更新 UI
watch(
() => llmChatState.fullResponse.value,
(newVal) => {
if (newVal) {
form.value.summary = newVal;
}
}
);
这个示例展示了实际开发中的几个关键点:
注意事项
-
每次调用前务必调用
reset()在实际使用中,每次发起新的聊天请求前,必须调用
reset()重置状态,否则fullResponse会累积之前的输出。llmChatState.reset(); // 清空之前的状态 await llmChatState.chat(messages, config); -
使用 watch 监听流式输出
chat方法返回的是完整响应,而非流式过程中的数据。如果需要实时显示 AI 正在输入的内容,应该使用 watch 监听fullResponse:watch( () => llmChatState.fullResponse.value, (newVal) => { if (newVal) { // 实时更新 UI currentContent.value = newVal; } } ); -
中断后需要手动重置
调用
stop()后,isStreaming会变为 false,但fullResponse会保留已接收的内容。如果需要清空,请调用reset()。 -
处理 AbortError
} catch (err: any) { if (err.name === 'AbortError') { return fullResponse.value; // 中断时返回已接收的内容 } // ...其他错误处理 }中断是正常行为,不是错误,需要特殊处理。
总结
useLLMChat 是一个设计精良的 Vue 3 Composable,它:
- 完整实现了 SSE 协议的解析
- 提供了清晰的状态管理和错误处理
- 支持流式输出的中断控制
- 代码结构规范,易于理解和扩展
通过分析这个 Hook,我们可以学到很多实用的技巧:流式解码、缓冲区管理、AbortController 中断、finally 资源清理等。这些技术在处理任何流式数据时都非常有用。
希望这篇文章能帮助你更好地理解大模型流式输出的实现原理。如果有任何问题,欢迎在评论区讨论!
更多推荐

所有评论(0)