前端调用大语言模型:基于 Vite 的工程化实践与 HTTP 请求详解
随着人工智能技术的迅猛发展,大语言模型已逐渐从科研实验室走向工业应用。本文将围绕"前端如何以 HTTP 请求方式调用大语言模型"这一核心主题,结合现代前端工程化工具 Vite,详细讲解项目初始化、环境变量配置、fetch 请求封装、安全注意事项等关键环节,帮助读者掌握从前端发起 LLM 调用的全流程。
一、为什么前端可以直接调用 LLM?
传统观点认为,AI 模型应由后端服务代理调用,前端仅负责展示结果。然而,在某些场景下,前端直连 LLM API 是可行且高效的,前提是:
- API 支持 CORS(跨域资源共享) :如 DeepSeek、OpenRouter 等部分服务商允许浏览器直接请求。
- 安全性可控:通过短期有效的 API Key、IP 白名单、请求频率限制等方式降低风险。
- 无需敏感数据处理:用户输入不涉及隐私或机密信息。
以 DeepSeek 为例,其官方 API 支持 CORS,允许前端通过 fetch 直接发起 POST 请求,这为快速原型开发和轻量级应用提供了极大便利。
二、项目初始化:使用 Vite 搭建全栈友好型前端项目
Vite 是新一代前端构建工具,以其极速的冷启动和热更新能力著称。虽然 Vite 本身是前端构建器,但其对环境变量、TypeScript、ESM 模块的原生支持,使其成为调用 LLM 的理想脚手架。
1. 创建项目
sql
npm create vite@latest llm-frontend-demo -- --template vanilla
cd llm-frontend-demo
npm install
选择 vanilla 模板即可获得一个纯净的 HTML/CSS/JS 项目结构,适合教学和快速验证。
2. 配置环境变量
出于安全考虑,API Key 绝不能硬编码在源码中 。Vite 提供了 .env 文件机制,所有以 VITE_ 开头的变量会被注入到客户端代码中。
创建 .env.local 文件(该文件通常加入 .gitignore,避免提交到版本控制):
ini
VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
⚠️ 注意:此方法仅适用于 API 服务商允许前端直连的场景。若服务商禁止 CORS 或要求更高安全级别,则必须通过后端代理。
三、HTTP 请求详解:如何正确调用 LLM API
LLM API 通常遵循 RESTful 设计,使用 JSON 格式通信。以 DeepSeek 的 /chat/completions 接口为例,一次完整的请求包含三个部分:请求行、请求头、请求体。
1. 请求行(Request Line)
- Method :
POST(因为需要发送消息内容) - URL :
https://api.deepseek.com/chat/completions - HTTP 版本: 通常由浏览器自动处理,无需显式指定
2. 请求头(Headers)
必须包含两个关键字段:
javascript
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
};
Content-Type: application/json告知服务器请求体为 JSON 格式。Authorization: Bearer <token>是 OAuth 2.0 标准的认证方式,Bearer为固定前缀。
3. 请求体(Body)
LLM 接口通常要求结构化的消息数组:
css
const payload = {
model: 'deepseek-chat', // 指定模型名称
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "你好 DeepSeek" }
]
};
注意:body 必须是字符串 ,不能直接传入 JavaScript 对象。需使用 JSON.stringify() 序列化:
css
body: JSON.stringify(payload)
为什么 body 必须是字符串?
这是因为 fetch API 的底层实现遵循 HTTP 协议规范 ,而 HTTP 协议规定:请求体(request body)只能是字节流(即二进制数据) 。在浏览器环境中,JavaScript 无法直接发送对象、数组等高级数据结构------这些结构只存在于运行时内存中,网络传输必须将其转换为可序列化的格式。
当你调用 fetch 并设置 body 字段时,浏览器期望你提供以下几种类型之一:
string(如 JSON 字符串)FormDataURLSearchParamsBlob/ArrayBuffer/ReadableStream等二进制类型
如果你直接传入一个 JavaScript 对象(例如 { key: "value" }),浏览器会尝试将其隐式转换为字符串,结果通常是 [object Object] ------ 这显然不是服务器期望的 JSON 格式,会导致 API 返回解析错误(如 400 Bad Request)。
因此,必须显式使用 JSON.stringify() 将对象转换为标准的 JSON 字符串 ,确保服务端能正确反序列化并理解你的请求内容。同时,配合设置请求头 'Content-Type': 'application/json',告知服务器:"我发送的是 JSON 格式的文本"。
✅ 正确做法:
cssbody: JSON.stringify({ message: "hello" })❌ 错误做法:
cssbody: { message: "hello" } // 实际发送的是 "[object Object]"
这一细节看似微小,却是前后端数据通信可靠性的关键保障。
四、使用 fetch 发起异步请求
现代浏览器原生支持 fetch API,它是发起 HTTP 请求的标准方式。
完整调用示例
javascript
// main.js
const endpoint = 'https://api.deepseek.com/chat/completions';
async function callDeepSeek(userMessage) {
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
};
const payload = {
model: 'deepseek-chat',
messages: [
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: userMessage }
]
};
try {
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
console.error('调用 DeepSeek 失败:', error);
return '抱歉,暂时无法获取回复。';
}
}
// 绑定按钮点击事件
document.getElementById('send-btn').addEventListener('click', async () => {
const input = document.getElementById('user-input');
const replyEl = document.getElementById('reply');
const userMsg = input.value.trim();
if (!userMsg) return;
replyEl.textContent = '思考中...';
const reply = await callDeepSeek(userMsg);
replyEl.textContent = reply;
input.value = '';
});
关键点说明
- 使用
async/await使异步代码更易读,避免回调地狱。 - 对
response.ok进行判断,防止非 2xx 响应被误认为成功。 - 错误处理必不可少,网络波动或配额耗尽可能导致请求失败。
五、工程化思维:代码如钢筋水泥
在 Trae 所倡导的"工程化"理念中,代码不仅是功能的载体,更是可维护、可扩展、可协作的"建筑材料"。调用 LLM 不应只是复制粘贴一段 fetch 代码,而应思考:
- 可复用性:将 LLM 调用封装为独立函数或模块。
- 可配置性:模型名称、系统提示词可通过参数传入。
- 可测试性:模拟 API 响应进行单元测试。
- 用户体验:加载状态、错误提示、输入限制等细节。
例如,可进一步抽象为:
kotlin
class LLMClient {
constructor(apiKey, model = 'deepseek-chat') {
this.apiKey = apiKey;
this.model = model;
this.endpoint = 'https://api.deepseek.com/chat/completions';
}
async chat(messages, systemPrompt = "You are a helpful assistant.") {
const fullMessages = [
{ role: "system", content: systemPrompt },
...messages
];
const response = await fetch(this.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({ model: this.model, messages: fullMessages })
});
const data = await response.json();
return data.choices[0].message.content;
}
}
这种面向对象的设计更利于大型项目集成。
六、安全与最佳实践
尽管前端直连 LLM 便捷高效,但也存在风险:
-
API Key 泄露 :一旦
.env中的 Key 被提取,可能被滥用产生高额费用。- 解决方案:使用短期 Token、设置 IP 白名单、监控调用量。
-
CORS 限制:并非所有 LLM 服务商都开放 CORS。
- 替代方案:通过 Vite 的代理功能(仅开发环境)或部署轻量后端(如 Cloudflare Workers)中转。
-
速率限制:频繁请求可能触发限流。
- 建议:添加防抖、队列机制或用户提示。
结语
前端调用大语言模型不再是遥不可及的概念,而是触手可及的工程实践。借助 Vite 的现代化开发体验和浏览器原生的 fetch 能力,我们可以快速构建具备 AI 能力的 Web 应用。然而,技术便利的背后是对工程规范、安全意识和用户体验的更高要求。
未来,随着 WebAssembly、WebGPU 等技术的发展,甚至可能在浏览器本地运行小型 LLM,实现完全离线的智能交互。但无论技术如何演进,"工程化"始终是高质量软件开发的基石------正如钢筋水泥之于摩天大楼,代码结构之于数字世界。
代码不是魔法,而是精心设计的工程。