当 AI 成为开发标配,前端工程师早已不满足于 "调用后端接口" 的被动角色 —— 如今我们可以直接通过 HTTP 请求与大模型对话,让网页具备原生智能。但从零散的 API 调用到工程化落地,藏着不少值得深究的细节。本文结合原生 JS 与 Vite 全栈方案,带你吃透前端调用大模型的核心逻辑与最佳实践。
一、认知破局:前端为何能直接调用大模型?
很多人会疑惑:"大模型调用不该是后端的活吗?" 其实核心逻辑很简单 ——大模型服务商本质是提供了标准化的 HTTP API,就像我们调用天气接口、地图接口一样,前端只需遵循协议规范发送请求,就能获得 AI 响应。
以 DeepSeek 为例,其聊天接口https://api.deepseek.com/chat/completions本质是一个接受 POST 请求的 HTTP 端点,前端通过 Fetch API 即可直接与之交互。这种方式的优势显而易见:
- 减少链路损耗:无需后端中转,直接前后端 "对话" 大模型
- 降低开发成本:前端独立完成智能功能开发,无需跨团队协作
- 提升响应速度:省去服务器转发的额外耗时
但优势背后也藏着挑战:API 密钥安全、请求逻辑复用、多环境适配等问题,都需要工程化方案来解决。
二、项目初始化:从原生到 Vite 全栈的两种方案
根据项目复杂度,我们可以选择不同的初始化方式,从简单到工程化逐步升级。
方案 1:原生 HTML/CSS/JS 快速验证
适合快速原型验证,无需任何构建工具,3 分钟即可启动:
- 创建基础目录结构
plaintext
web-llm-demo/
├─ index.html # 页面结构
├─ style.css # 样式美化
└─ app.js # 核心调用逻辑
- 编写基础页面在
index.html中创建输入框、发送按钮和响应区域,核心是引入脚本并预留 DOM 节点:
html
预览
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>前端直连大模型</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="chat-container">
<div id="response-area" class="chat-content"></div>
<div class="input-area">
<input type="text" id="prompt-input" placeholder="请输入问题...">
<button id="send-btn">发送</button>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
这种方式的优点是零配置、启动快,但随着项目迭代,会逐渐暴露代码冗余、依赖管理混乱等问题,此时就需要升级到工程化方案。
方案 2:Vite 全栈脚手架搭建(推荐)
Vite 的极速构建、热更新和环境配置能力,完美适配大模型调用的工程化需求。
第一步:初始化 Vite 项目
执行命令创建项目,选择 "Vanilla JavaScript" 模板(如需框架可选 Vue/React):
bash
运行
npm create vite@latest web-llm-fullstack -- --template vanilla
cd web-llm-fullstack
npm install
第二步:核心 Vite 配置(vite.config.js)
结合大模型调用场景,我们需要配置环境变量加载、接口代理等关键功能:
javascript
运行
import { defineConfig, loadEnv } from 'vite'
// 支持多环境配置
export default defineConfig(({ mode }) => {
// 加载对应环境的.env文件,第三个参数""表示读取所有前缀的变量
const env = loadEnv(mode, process.cwd(), '')
return {
// 开发服务器配置
server: {
port: 3000,
// 接口代理:解决开发环境跨域问题
proxy: {
'/api/llm': {
target: env.VITE_LLM_API_BASE, // 从环境变量获取API地址
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/llm/, '') // 路径重写
}
}
},
// 构建配置
build: {
target: 'es2015', // 兼容现代浏览器
outDir: 'dist',
assetsDir: 'static'
}
}
})
第三步:环境变量配置
创建.env相关文件管理不同环境的配置,关键是区分公共配置与敏感信息:
- .env.development(开发环境)
env
# 大模型API基础地址
VITE_LLM_API_BASE=https://api.deepseek.com
# 模型版本
VITE_LLM_MODEL=deepseek-chat
# API路径(配合代理使用)
VITE_LLM_API_PATH=/api/llm/chat/completions
- .env.production(生产环境)
env
VITE_LLM_API_BASE=https://api.deepseek.com
VITE_LLM_MODEL=deepseek-chat
VITE_LLM_API_PATH=/api/llm/chat/completions
- .env.local(本地私密配置,不提交 Git)
env
# 注意:生产环境API密钥应放在后端,此处仅用于开发调试
VITE_LLM_API_KEY=sk-your-private-key-here
⚠️ 安全提醒:Vite 中以VITE_开头的变量会暴露到前端,生产环境的 API 密钥必须通过后端转发,禁止直接在前端存储!
三、核心实现:Fetch 复杂请求与工程化封装
大模型的 HTTP 请求包含严格的格式要求,直接手写 Fetch 会导致代码重复且难以维护,我们需要从基础调用到工程化封装逐步实现。
1. 吃透大模型 HTTP 请求结构
大模型 API 的 POST 请求遵循固定格式,就像一封结构严谨的 "信件" :
| 组成部分 | 核心内容 |
|---|---|
| 请求行 | POST /chat/completions HTTP/1.1 (方法 + 路径 + 协议) |
| 请求头 | Content-Type: application/json(数据格式)、Authorization: Bearer ${key}(身份认证) |
| 请求体 | JSON 字符串格式,包含模型名、对话消息等关键参数 |
特别注意:请求体不能直接发送 JSON 对象,必须用JSON.stringify()转换为字符串。
2. 原生 Fetch 调用实现(基础版)
先看最基础的调用逻辑,理解核心原理:
javascript
运行
// app.js
async function callLLM(prompt) {
// 1. 从环境变量获取配置
const apiKey = import.meta.env.VITE_LLM_API_KEY
const apiPath = import.meta.env.VITE_LLM_API_PATH
const model = import.meta.env.VITE_LLM_MODEL
// 2. 准备请求参数
const payload = {
model: model,
messages: [
{ role: 'system', content: '你是一个帮助前端开发者的助手' },
{ role: 'user', content: prompt }
]
}
try {
// 3. 发送Fetch请求
const response = await fetch(apiPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify(payload) // 必须转换为JSON字符串
})
// 4. 处理响应
if (!response.ok) {
throw new Error(`请求失败:${response.status} ${response.statusText}`)
}
const data = await response.json()
// 提取AI回复(不同模型的响应结构可能略有差异)
return data.choices[0].message.content
} catch (error) {
console.error('大模型调用失败:', error)
throw error // 抛出错误供上层处理
}
}
// 绑定DOM事件
document.getElementById('send-btn').addEventListener('click', async () => {
const input = document.getElementById('prompt-input')
const prompt = input.value.trim()
if (!prompt) return
const responseArea = document.getElementById('response-area')
responseArea.innerHTML += `<div class="user-msg">${prompt}</div>`
try {
// 调用大模型
const reply = await callLLM(prompt)
responseArea.innerHTML += `<div class="ai-msg">${reply}</div>`
} catch (err) {
responseArea.innerHTML += `<div class="error-msg">调用失败:${err.message}</div>`
}
input.value = ''
})
3. Trae 介入:工程化请求封装
Trae 是一个轻量级的 HTTP 客户端,基于 Fetch 封装,能大幅提升代码的可维护性和复用性。
第一步:安装 Trae
bash
运行
npm install trae
第二步:创建统一的 API 客户端(src/api/llmClient.js)
javascript
运行
import trae from 'trae'
// 1. 初始化Trae实例
const llmClient = trae.create({
baseURL: import.meta.env.VITE_LLM_API_BASE,
headers: {
'Content-Type': 'application/json'
}
})
// 2. 请求拦截器:统一添加认证信息
llmClient.interceptors.request.use((config) => {
const apiKey = import.meta.env.VITE_LLM_API_KEY
if (apiKey) {
config.headers.Authorization = `Bearer ${apiKey}`
}
// 开发环境使用代理路径,生产环境可能直接使用真实路径
if (import.meta.env.DEV) {
config.url = `/api/llm${config.url}`
}
return config
})
// 3. 响应拦截器:统一处理响应与错误
llmClient.interceptors.response.use(
(response) => {
// 统一提取有用数据
return response.data.choices[0].message.content
},
(error) => {
// 分类处理不同错误
let errorMsg = '大模型调用失败'
if (error.response) {
// 服务器返回错误
if (error.response.status === 401) {
errorMsg = 'API密钥无效,请检查配置'
} else if (error.response.status === 429) {
errorMsg = '请求过于频繁,请稍后再试'
}
} else if (error.request) {
// 无响应(网络问题)
errorMsg = '网络异常,无法连接到服务器'
}
console.error(errorMsg, error)
return Promise.reject(new Error(errorMsg))
}
)
// 4. 封装大模型调用方法
export const llmApi = {
/**
* 调用大模型生成响应
* @param {string} prompt 用户输入
* @param {Array} history 对话历史
* @returns {Promise<string>} AI回复
*/
async chat(prompt, history = []) {
const messages = [
{ role: 'system', content: '你是一个帮助前端开发者的助手' },
...history,
{ role: 'user', content: prompt }
]
return llmClient.post('/chat/completions', {
model: import.meta.env.VITE_LLM_MODEL,
messages
})
}
}
第四步:使用封装后的 API
在业务代码中调用变得异常简洁:
javascript
运行
import { llmApi } from './api/llmClient.js'
// 调用示例
async function handleSendMessage(prompt, history) {
try {
const loading = document.getElementById('loading')
loading.style.display = 'block'
// 只需一行代码调用
const reply = await llmApi.chat(prompt, history)
loading.style.display = 'none'
// 处理回复...
} catch (error) {
alert(error.message)
}
}
这种封装的优势在项目迭代中会愈发明显:
- 统一配置:修改 API 地址、模型版本只需改一处
- 统一错误处理:避免重复编写错误提示逻辑
- 易于扩展:新增模型、添加请求缓存都很方便
四、工程化进阶:从开发到生产的关键考量
前端调用大模型绝不是 "调通接口就完事",工程化落地需要解决这些核心问题:
1. API 密钥安全:后端转发是必选项
开发环境可以临时在.env.local存放密钥,但生产环境必须通过后端转发:
- 前端请求自己的后端接口(如
/api/proxy/llm) - 后端从安全存储(如配置中心、环境变量)获取 API 密钥
- 后端转发请求到大模型 API,返回结果给前端
这样既能保护密钥,又能在后端实现限流、日志等额外功能。
2. 性能优化:提升用户体验的 3 个技巧
- 加载状态反馈:调用期间显示加载动画,避免用户重复点击
- 请求防抖:输入框使用防抖(如 500ms),避免频繁触发请求
- 流式响应:大模型支持的话,使用
ReadableStream实现打字机效果,减少等待感
3. 错误处理:覆盖所有异常场景
除了网络错误,还要处理大模型特有的错误:
- 401:密钥无效或过期
- 429:请求频率超限(可提示用户稍后重试)
- 503:服务暂时不可用(可实现自动重试)
- 响应格式异常:添加数据校验逻辑
五、总结:前端调用大模型的核心心法
从原生 Fetch 到 Vite 全栈工程化,前端调用大模型的演进本质是 "从快速验证到体系化落地" 的过程:
- 基础层:理解 HTTP 请求三要素(行、头、体),掌握 Fetch 异步处理
- 工程层:用 Vite 管理环境配置,用 Trae 封装请求逻辑
- 安全层:生产环境必须通过后端转发 API 密钥
- 体验层:优化加载状态,完善错误处理
如今大模型 API 的标准化程度越来越高,掌握前端直连技术的开发者,能更快地将 AI 能力融入产品,创造出更智能的 Web 体验。你准备好给你的网页加上 "大脑" 了吗?