前端调用大模型实战:从 0 到 1 搭建工程化调用体系

117 阅读7分钟

当 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 分钟即可启动:

  1. 创建基础目录结构

plaintext

web-llm-demo/
├─ index.html  # 页面结构
├─ style.css   # 样式美化
└─ app.js      # 核心调用逻辑
  1. 编写基础页面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相关文件管理不同环境的配置,关键是区分公共配置与敏感信息

  1. .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
  1. .env.production(生产环境)

env

VITE_LLM_API_BASE=https://api.deepseek.com
VITE_LLM_MODEL=deepseek-chat
VITE_LLM_API_PATH=/api/llm/chat/completions
  1. .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存放密钥,但生产环境必须通过后端转发:

  1. 前端请求自己的后端接口(如/api/proxy/llm
  2. 后端从安全存储(如配置中心、环境变量)获取 API 密钥
  3. 后端转发请求到大模型 API,返回结果给前端

这样既能保护密钥,又能在后端实现限流、日志等额外功能。

2. 性能优化:提升用户体验的 3 个技巧

  • 加载状态反馈:调用期间显示加载动画,避免用户重复点击
  • 请求防抖:输入框使用防抖(如 500ms),避免频繁触发请求
  • 流式响应:大模型支持的话,使用ReadableStream实现打字机效果,减少等待感

3. 错误处理:覆盖所有异常场景

除了网络错误,还要处理大模型特有的错误:

  • 401:密钥无效或过期
  • 429:请求频率超限(可提示用户稍后重试)
  • 503:服务暂时不可用(可实现自动重试)
  • 响应格式异常:添加数据校验逻辑

五、总结:前端调用大模型的核心心法

从原生 Fetch 到 Vite 全栈工程化,前端调用大模型的演进本质是 "从快速验证到体系化落地" 的过程:

  1. 基础层:理解 HTTP 请求三要素(行、头、体),掌握 Fetch 异步处理
  2. 工程层:用 Vite 管理环境配置,用 Trae 封装请求逻辑
  3. 安全层:生产环境必须通过后端转发 API 密钥
  4. 体验层:优化加载状态,完善错误处理

如今大模型 API 的标准化程度越来越高,掌握前端直连技术的开发者,能更快地将 AI 能力融入产品,创造出更智能的 Web 体验。你准备好给你的网页加上 "大脑" 了吗?