随着大语言模型(LLM)能力的边界不断拓展,前端开发的范式正在发生微妙的变化。过去我们需要后端提供结构化的数据接口,现在前端可以直接与多模态模型对话,让应用具备“看”和“说”的能力。
今天我想分享一个小型的全栈实践案例:一个“拍照记单词”的应用。它的核心逻辑很简单:用户拍摄或上传一张生活照片,系统识别图片内容,提取一个适合初学者的英文单词,生成例句,并朗读出来。
虽然功能看似简单,但在实现过程中,涉及到了文件处理、多模态 API 调用、音频流处理以及 Prompt 工程等多个技术点。本文将剥离出核心代码逻辑,探讨其中的实现细节、设计考量以及潜在的优化空间。
一、核心交互与文件处理
在传统的文件上传场景中,我们通常将文件直接提交给后端。但在这个应用中,图片需要同时做两件事:
- 本地预览:让用户确认上传的内容。
- 发送给 LLM:作为多模态模型的输入。
1. 无障碍与样式控制的平衡
在 PictureCard 组件中,文件上传的实现采用了经典的 input + label 组合模式:
<input type="file" id="selecteImage" class="input" accept="image/*" @change="updateImageData">
<label for="selecteImage" class="upload">
<img :src="imgPreview" alt="camera" class="img">
</label>
这里有两个细节值得注意:
首先是无障碍访问(Accessibility)。原生的 input[type="file"] 样式难以定制,且在不同浏览器上表现不一。通过 display: none 隐藏 input,并使用 label 关联 id,我们既获得了完全自由的样式控制权,又保留了语义化。当用户点击美观的相机图标时,实际上触发的是原生文件选择器。对于使用读屏器的视障用户,label 标签能准确传达“上传图片”的意图,这是开发中容易忽视但至关重要的细节。
其次是文件读取机制。为了将图片发送给 LLM,我们需要将其转换为 Base64 格式。这里使用了 HTML5 提供的 FileReader API:
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const data = reader.result as string;
imgPreview.value = data;
emit('update-image', data);
}
readAsDataURL 会将文件内容读取为一个包含 MIME 类型的 Base64 字符串(例如 data:image/png;base64,...)。
- 优点:格式统一,可以直接嵌入 JSON 发送给大多数多模态 API,同时也方便直接赋值给
img标签的src进行预览。 - 缺点:Base64 编码会使文件体积增加约 33%。如果图片过大,不仅影响传输速度,还可能超出 LLM 的 Token 限制。在实际生产中,通常需要在读取前对图片进行压缩或尺寸限制。
二、与大模型的对话:Prompt 工程与多模态
应用的核心智能来源于对 Kimi(Moonshot)多模态接口的调用。在 App.vue 中,我们构建了请求体。
1. 多模态输入的标准格式
目前主流的多模态模型(如 GPT-4V, Moonshot-v1-vision)在接收图片时,通常要求 messages 中的 content 字段是一个数组,分别包含文本和图片对象:
messages: [
{
role: 'user',
content: [{
type: 'image_url',
image_url: { url: imageDate } // 这里是 Base64 或 HTTP URL
}, {
type: 'text',
text: userPrompt
}]
}
]
这种设计允许模型同时“看”到图片并“读”到指令。需要注意的是,虽然代码中直接使用了 Base64,但如果图片较大,建议先上传至对象存储(OSS),将 HTTP URL 传给模型,以减少请求包体大小。
2. 结构化输出的重要性
在 userPrompt 的设计上,我们没有让模型自由发挥,而是严格限制了输出格式:
返回 JSON 数据:
{
"representative_word": "图片代表的英文单词",
"example_sentence": "结合英文单词和图片描述,给出一个简单的例句",
"explaination": "...",
...
}
这是开发 AI 应用的一个关键原则:机器与人对话可以自然,但机器与代码对话必须严谨。
通过要求模型返回 JSON,我们可以直接 JSON.parse 结果,将单词、句子、解释分发到不同的 UI 区域。如果让模型自由返回文本,前端就需要编写复杂的正则去提取单词,这不仅脆弱,而且容易出错。此外,Prompt 中明确了词汇难度(A1~A2),这是产品价值的体现——我们不是在做一个翻译工具,而是在做一个适合初学者的教育工具。
三、音频生成与播放机制
当模型返回例句后,应用需要调用 TTS(Text-to-Speech)服务将文本转为音频。这里涉及到了二进制数据的处理。
1. Base64 到 Blob URL 的转换
TTS 接口返回的通常是音频文件的 Base64 数据。在 audio.ts 中,我们实现了一个 createBlobURL 函数:
const byteCharacters = atob(base64AudioData);
// ... 转换为 Uint8Array
const audioBlob = new Blob([new Uint8Array(byteArrays)], { type: 'audio/mp3' });
const blobURL = URL.createObjectURL(audioBlob);
这里有一个常见的疑问:为什么不直接使用 data:audio/mp3;base64,... 赋值给 audio 标签?
虽然 Data URI 可以直接播放,但在处理较长音频或高频调用时,Blob URL 方案更具优势:
- 性能:Blob URL 指向的是内存中的二进制对象,浏览器解码效率通常更高。
- 内存管理:
URL.createObjectURL创建的引用是可以被显式释放的(通过URL.revokeObjectURL)。虽然示例代码中为了简洁未展示释放逻辑,但在组件卸载时调用释放,可以有效防止内存泄漏。 - 类型安全:显式创建 Blob 可以确保 MIME 类型被浏览器正确识别,避免某些移动端浏览器对 Data URI 音频支持不佳的问题。
2. 音频格式的潜在风险
在代码审查中,我发现了一个值得注意的细节:
- TTS 请求参数中设置的是
encoding: 'ogg_opus'。 - 但在创建 Blob 时,MIME 类型指定的是
audio/mp3。
这可能会导致部分浏览器播放失败或无法识别时长。严谨的做法是根据 API 实际返回的音频流格式来设定 Blob 的 type,或者在 API 请求时直接要求返回 MP3 格式。这提醒我们在对接第三方服务时,必须严格核对输入输出的格式规范。
四、架构思考与安全隐患
在复盘整个项目时,除了功能实现,还有几个架构层面的问题需要深入探讨。
1. 前端密钥的安全风险
在 App.vue 中,我们看到了这样的代码:
'Authorization': `Bearer ${import.meta.env.VITE_KIMI_API_KEY}`
这是一个严重的安全隐患。 将 LLM 的 API Key 直接暴露在前端代码中,意味着任何查看网页源码的用户都可以获取你的密钥,从而盗用你的额度。
改进方案:
README 中提到了技术栈包含 NestJS。正确的架构应该是:
- 前端发起请求到自有的 NestJS 后端。
- 后端在服务器端存储 API Key,并转发请求给 Kimi 和 TTS 服务。
- 后端可以做一层代理,同时实现限流、鉴权和日志记录。
目前的实现仅适合本地学习或内部演示,绝不可直接部署到公网。
2. 状态管理的解耦
当前逻辑集中在 App.vue 中,包括图片状态、单词状态、音频状态等。随着功能增加(例如历史记录、生词本),组件会变得臃肿。
建议引入状态管理库(如 Pinia),将“学习会话”作为一个 Store 管理。同时,将 generateAudio 和 fetchLLM 封装为独立的 Service 层,与 UI 组件彻底解耦。这样不仅便于测试,也方便后续将 API 调用迁移到后端时,前端只需修改 Service 层的请求地址。
3. 用户体验的细腻处理
代码中实现了基础的加载状态(如“分析中..."),但在网络波动或 API 报错时,用户体验还可以更好:
- 重试机制:LLM 接口偶尔会超时,提供“重试”按钮比直接报错更友好。
- 音频预加载:在生成音频 URL 后,可以实例化
new Audio(url)进行预加载,确保用户点击播放时无延迟。 - 图片压缩:如前所述,在
FileReader读取前,使用 Canvas 对图片进行压缩,能显著提升上传和解析速度。
五、总结
通过这个“拍照记单词”的小应用,我们实践了 Vue3 组合式 API 的组件通信,探索了 FileReader 与 Blob 的二进制处理,并深入体验了多模态大模型的接入流程。
技术本身并不是目的,解决用户痛点才是。在这个案例中,技术的价值在于将“生活中的任意场景”瞬间转化为“可学习的语言素材”,降低了语言学习的门槛。
对于前端开发者而言,拥抱 AI 不仅仅是学会调用 API,更在于理解如何设计 Prompt 以获得稳定的输出,如何处理多媒体数据流,以及如何在享受 AI 便利的同时,守住安全与性能的底线。希望这个案例能为你构建自己的 AI 应用提供一些实在的参考。