1. 从“前端”到“智能体”:为什么你的代码需要连接大模型?

最近和几个做前端的朋友聊天,发现一个挺有意思的现象:大家聊起AI大模型,要么是觉得那是后端或者算法工程师的“魔法”,自己用用ChatGPT写写注释、生成点测试数据就差不多了;要么就是被各种复杂的API文档、SDK配置、网络请求和流式响应搞得头大,试了一下就放弃了。这让我想起几年前,前端刚开始处理WebSocket、处理复杂的表单状态管理时的状态,既兴奋又有点无从下手。

但我想说的是,现在把AI能力集成到前端应用里,已经不再是“魔法”或者“高门槛”的事情了。它正在变得像当年引入 axios 处理HTTP请求,或者用 WebSocket 实现实时聊天一样,成为一项可以标准化、流程化的工程能力。你不再需要去理解Transformer的每一层结构,也不需要自己去部署一个动辄几十GB的模型。你需要做的,是理解如何作为一个“调用者”,去和这些强大的“智能体”对话,并把它们的“思考”结果优雅地呈现在你的界面上。

这背后的核心驱动力,是应用交互范式的转变。传统的Web应用,交互逻辑是确定的:用户点击按钮,前端发送请求,后端处理数据并返回,前端渲染结果。整个过程是“请求-响应”的闭环。而接入了大模型的应用,交互变成了“发起对话-持续思考-流式反馈”。用户输入一个问题或指令,大模型可能需要进行多步“思考”(推理),并以一种持续、渐进的方式将结果“流”回来。这对前端的挑战在于,你需要处理的不再是一个简单的JSON响应,而是一个可能持续数秒甚至更长的数据流,并且要在这个过程中保持界面的流畅和即时反馈。

所以,今天我们不谈高深的算法,就从一个纯粹的前端开发者视角出发,拆解将你的代码与AI大模型连接起来的五个关键步骤。无论你是想做一个智能客服对话框、一个代码辅助生成工具,还是一个能理解用户自然语言指令的仪表盘,这套思路都能帮你快速上手。我们会聚焦在最通用、最核心的链路:如何发起请求、如何处理响应、如何管理状态,以及如何应对那些“坑”。你会发现,用到的技术栈可能就是你每天都在用的 fetch EventSource ,或者一个封装好的SDK。

2. 第一步:明确目标与选择接口——你要调用谁的“大脑”?

在动手写第一行代码之前,最重要的一步是明确:你到底要做什么?以及,你准备调用哪个“大脑”(大模型服务)来实现它?这一步的选择,直接决定了后续所有技术方案的成本、复杂度和效果。

2.1 需求定义:从“聊天”到“工具调用”

别一上来就想做“下一个ChatGPT”。先从具体的、小的功能点切入:

  • 智能文本补全与润色 :在富文本编辑器里,用户选中一段文字,点击“优化表达”或“扩写”,前端调用大模型并原地替换。
  • 基于自然语言的查询 :在一个数据报表页面,用户输入“帮我找出上个月销售额最高的三个产品”,前端解析后转换为对后端API的查询,或者直接由大模型生成SQL/查询语句(需谨慎)。
  • 内容分类与摘要 :用户上传或输入一段长文本(如新闻、报告),前端调用大模型生成摘要或打上标签。
  • 简单的对话交互 :一个固定领域的问答机器人,比如电商售前咨询、产品功能解答。

明确需求的核心是 确定输入和输出的格式 。输入是纯文本?还是包含图片的Multipart FormData?输出你期望是纯文本流,还是一个结构化的JSON(例如 {action: 'query', parameters: {...}} )?这决定了你后续调用API的方式。

2.2 服务商选型:开源、云服务与自托管

这是技术选型的核心。目前主要有三条路径:

路径A:使用商业云API(最快捷) 这是绝大多数前端项目的起点。你不需要关心模型怎么部署、机器在哪,只需要一个API Key。

  • 代表 :OpenAI的GPT系列、Anthropic的Claude、国内各大厂的云服务(如百度文心、阿里通义、智谱GLM等)。
  • 优点 :开箱即用,稳定,性能有保障,通常提供完善的SDK和文档。适合快速验证产品想法。
  • 前端关注点 :你需要处理 网络请求 (通常是HTTPS)、 认证 (在请求头携带 Authorization: Bearer <api_key> )、 计费 (通常按Token数量)和 可能存在的网络延迟 (服务可能在海外)。 重要提示 :绝对不要在前端代码中硬编码API Key!这相当于把你的银行卡密码贴在网页上。必须通过你自己的后端服务器进行转发,后端负责鉴权、计费和可能的速度优化。

路径B:使用开源模型与本地/自有服务器 当你对数据隐私、成本有极高要求,或者需要定制化模型时,会考虑这条路。

  • 代表 :通过 Ollama vLLM Transformers 等框架在本地或公司内网服务器部署Llama、Qwen、ChatGLM等开源模型。
  • 优点 :数据完全私有,可深度定制,长期成本可能更低。
  • 前端关注点 :你需要与后端同事紧密合作,定义一套 内部API协议 。这套协议可能模仿商业API(如OpenAI格式),也可以是你们自定义的。前端的工作从“调用标准API”变成了“与自定义后端对接”,需要更关注 协议一致性 错误处理

路径C:使用“模型即服务”平台 这类平台聚合了多个模型,提供统一的API接口。

  • 代表 :国外如Together AI,国内也有一些类似平台。
  • 优点 :可以灵活切换不同模型,对比效果,有时能获得更好的性价比。
  • 前端关注点 :和路径A类似,但需要注意不同模型之间的 输入输出格式可能存在的细微差异 ,平台可能会做一层归一化。

对于前端开发者,我个人的建议是:在原型阶段,从路径A(商业API)开始,使用它们提供的官方或社区SDK,可以让你以最快速度跑通“前端-大模型”的完整链路,把精力集中在用户体验和交互逻辑上。当业务模型跑通后,再根据成本、隐私需求考虑是否迁移到路径B或C。

2.3 接口格式确认:RESTful vs. 流式(Server-Sent Events) 大模型API通常提供两种响应方式:

  1. 同步(阻塞)请求 :发送一个完整的请求,等待模型生成全部内容后,一次性返回。适用于生成内容较短、要求必须完整的场景。前端用普通的 fetch axios 处理即可。
  2. 流式(Streaming)请求 :请求发出后,服务器会以数据流的形式,逐步返回模型生成的每一个词元(Token)。这是实现“打字机”效果的关键。前端通常使用 EventSource (用于SSE协议)或处理 fetch 返回的 ReadableStream

在项目初期,我强烈建议 优先实现并测试流式接口 。因为这是大模型交互最典型、用户体验最好的方式,其技术方案也与同步请求不同,需要提前攻克。

3. 第二步:构建通信层——从 fetch 到“流”处理

选定模型服务并确定使用流式接口后,我们就来到了前端最核心的环节:构建一个健壮、易用的通信层。这个层负责处理所有与模型API的网络交互。

3.1 摒弃 XMLHttpRequest (XHR) ,拥抱 fetch ReadableStream

在流式场景下,古老的 XHR 对数据流的支持非常有限且笨拙。现代浏览器提供的 fetch API配合 ReadableStream ,是处理流式响应的标准答案。

一个最基础的、调用OpenAI兼容流式API的示例:

async function fetchStreamingResponse(prompt, apiKey, onChunkReceived, onCompletion) {
  // 注意:apiKey不应在前端硬编码,此处仅为示例。实际应由后端接口提供或代理。
  const response = await fetch('https://api.your-models.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}` // 危险!仅作演示
    },
    body: JSON.stringify({
      model: 'gpt-3.5-turbo',
      messages: [{ role: 'user', content: prompt }],
      stream: true // 关键参数,开启流式输出
    })
  });

  if (!response.ok || !response.body) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder('utf-8');
  let accumulatedText = '';

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        onCompletion?.(accumulatedText);
        break;
      }

      // 解码当前数据块
      const chunk = decoder.decode(value, { stream: true });
      // 处理可能的SSE格式:以 "data: " 开头,以 "\n\n" 分隔多个事件
      const lines = chunk.split('\n\n').filter(line => line.trim());

      for (const line of lines) {
        if (line.startsWith('data: ')) {
          const data = line.slice(6); // 去掉 "data: " 前缀
          if (data === '[DONE]') {
            onCompletion?.(accumulatedText);
            return;
          }
          try {
            const parsed = JSON.parse(data);
            const content = parsed.choices[0]?.delta?.content || '';
            if (content) {
              accumulatedText += content;
              onChunkReceived(content, accumulatedText); // 回调:单个词元和累积文本
            }
          } catch (e) {
            console.error('解析流式数据块失败:', e, '原始数据:', data);
          }
        }
      }
    }
  } finally {
    reader.releaseLock();
  }
}

关键点解析

  • stream: true :这是告诉服务端开启流式传输的关键请求参数。
  • response.body.getReader() :获取响应体的 ReadableStream 的读取器。
  • reader.read() :异步读取下一个数据块。
  • SSE格式解析 :许多流式API(如OpenAI)使用Server-Sent Events格式。每个数据块以 data: 开头,以两个换行符 \n\n 分隔。有效数据是JSON,结束标志是单独的 data: [DONE]
  • 错误处理 :网络错误、响应非200、数据解析失败都需要考虑。上面的示例只是一个骨架,生产环境需要更完善的错误处理和重试逻辑。

3.2 使用专用SDK简化开发

如果你不想手动处理这些底层的流解析和错误处理,使用官方或成熟的第三方SDK是明智之举。例如,对于OpenAI:

npm install openai
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: 'your-api-key', // 同样,这应该来自后端
  dangerouslyAllowBrowser: true // 注意:这个选项意味着你明确知道在前端暴露API Key的风险,仅用于原型或内部工具。生产环境必须用后端代理。
});

async function streamWithSDK() {
  const stream = await openai.chat.completions.create({
    model: 'gpt-3.5-turbo',
    messages: [{ role: 'user', content: 'Hello' }],
    stream: true,
  });

  for await (const chunk of stream) {
    const content = chunk.choices[0]?.delta?.content || '';
    if (content) {
      console.log('收到内容:', content);
      // 更新UI...
    }
  }
}

SDK帮你封装了所有的HTTP通信、认证、流解析和类型提示,让代码更简洁、更安全(在配合后端代理的前提下)。选择SDK时,注意其 浏览器兼容性 包大小 以及 是否支持Tree Shaking

3.3 至关重要的安全代理层

我必须要再次强调: 永远不要将你的大模型服务API Key直接放在前端代码或环境变量中 。任何部署到用户浏览器的代码都是公开的。正确的架构是:

[用户浏览器] --(请求)--> [你的前端服务器/云函数] --(携带API Key的请求)--> [大模型服务商]
                                ↑
                            (在这里进行鉴权、限流、日志记录、成本控制)

你的前端代码只与你自己的后端接口通信。这个后端接口负责:

  1. 验证用户身份(是否登录、是否有权限)。
  2. 将用户的请求转发给大模型服务商,并附上API Key。
  3. 可能对请求和响应进行预处理或后处理(如提示词工程、格式化输出)。
  4. 记录使用日志,用于分析和计费。

这样,即使API Key泄露,你也可以在服务商的控制台迅速撤销它,而不会影响主业务。前端与这个代理层的通信,可以使用普通的 fetch ,也可以使用GraphQL等,根据你的技术栈决定。

4. 第三步:设计状态与UI交互——管理“进行中”的对话

流式响应带来了一个新的前端状态:“正在生成”。UI需要实时反映这个状态,并提供良好的用户体验。

4.1 状态管理:超越简单的 useState

对于一个简单的聊天界面,你至少需要管理:

  • messages : Array - 历史消息列表,包含 {role: 'user'|'assistant', content: string}
  • currentInput : string - 用户正在输入框输入的内容。
  • isLoading : boolean - 是否正在等待响应(对于流式,这个状态可能更复杂)。
  • streamingText : string - 专门用于存储当前正在流式接收的助手消息内容。

当使用流式响应时, isLoading 可能在请求开始时设为 true ,但整个流式接收过程中它都是 true 吗?不一定。更精细的做法可能是:

const [messageStatus, setMessageStatus] = useState('idle'); // 'idle' | 'streaming' | 'error'
const [pendingMessage, setPendingMessage] = useState(''); // 用于累积流式内容

当开始流式接收时, messageStatus 设为 'streaming' pendingMessage 初始为空字符串。每收到一个数据块,就更新 pendingMessage 。流结束时,将 pendingMessage 作为一条完整消息存入 messages ,并重置状态。

4.2 UI反馈:实时显示与中断控制

  • “打字机”效果 :将 streamingText pendingMessage 绑定到UI上,每次更新都会触发重新渲染,自然形成逐字输出的效果。对于长内容,可以考虑使用 requestAnimationFrame 进行节流更新,避免过于频繁的DOM操作影响性能。
  • 加载指示器 :当 messageStatus 'streaming' 时,可以在消息气泡尾部显示一个闪烁的光标或“正在思考...”的提示。
  • 中断生成 :这是一个非常重要的用户体验功能。在流式请求中,中断意味着需要 主动取消当前的网络请求
    let abortController = new AbortController();
    
    async function sendMessage() {
      abortController = new AbortController(); // 每次发送前创建新的
      try {
        const response = await fetch('/api/chat-proxy', {
          method: 'POST',
          signal: abortController.signal, // 关联AbortSignal
          // ... 其他参数
        });
        // ... 处理流
      } catch (error) {
        if (error.name === 'AbortError') {
          console.log('请求被用户中断');
        } else {
          // 处理其他错误
        }
      }
    }
    
    function stopGenerating() {
      abortController.abort(); // 调用此函数中断请求
      // 同时更新UI状态,例如将`streamingText`固化为一条完整消息
    }
    
    提供一个清晰的“停止生成”按钮,并在中断后妥善更新UI状态。

4.3 处理多轮对话与上下文

大模型的能力依赖于上下文。你需要将历史对话也作为消息列表的一部分,在每次请求时发送给模型。

const messagesToSend = [
  { role: 'system', content: '你是一个有帮助的助手。' }, // 系统指令,设定角色
  ...historyMessages, // 之前的对话历史
  { role: 'user', content: currentInput } // 最新的用户消息
];

这里有两个 关键陷阱

  1. 上下文长度限制(Token Limit) :所有模型都有输入Token的上限(如4K, 16K, 128K)。当对话历史很长时,你需要一个“上下文窗口管理”策略。常见策略包括:
    • 滑动窗口 :只保留最近N条消息。
    • 摘要压缩 :当历史过长时,调用模型自身对之前的对话生成一个摘要,然后用摘要替代旧的历史。
    • 丢弃最早的消息 :简单粗暴,但可能丢失重要早期信息。
  2. 系统指令(System Prompt) :这是引导模型行为的关键。它应该放在消息列表的最开始。前端可以提供一个地方让用户或管理员配置这个系统指令,或者由后端根据业务场景固定。

5. 第四步:优化、容错与监控——让体验更可靠

基础功能跑通后,下一步是让它变得健壮、可用。

5.1 性能与用户体验优化

  • 节流(Throttling)与防抖(Debouncing) :如果用户输入触发自动补全或建议,务必使用防抖,避免对API的疯狂请求。
  • 请求合并 :对于快速连续的请求(比如用户连续点击“重试”),可以考虑合并或取消前一个请求。
  • 本地缓存 :对于一些常见的、确定性的查询(如“解释某个术语”),如果回答变化不大,可以考虑在前端或服务端缓存响应,减少对模型的调用和用户等待时间。
  • 渐进式加载 :对于生成代码、长文章等,在流式接收的同时就可以进行初步的语法高亮或格式渲染,不要等全部内容接收完再显示。

5.2 全面的错误处理

大模型服务可能因为多种原因失败,前端必须有相应的降级方案。

  • 网络超时 :设置合理的 timeout ,并提示用户“请求超时,请检查网络或稍后重试”。
  • 速率限制(Rate Limit) :API服务商通常会限制调用频率。当收到 429 Too Many Requests 错误时,前端应提示用户“操作过于频繁,请稍后再试”,并可能自动进行指数退避重试。
  • 模型过载或服务不可用 5xx 错误):提示“服务暂时不可用”,并引导用户稍后重试。
  • 上下文过长 400 Bad Request ):当提示词超过Token限制时,服务商会返回明确错误。前端捕获后,应提示用户“对话内容过长,请尝试简化问题或开启新对话”,并自动清空或压缩历史。
  • 内容过滤 :如果用户输入或模型生成的内容触发了服务商的安全策略,可能会被拦截。前端需要友好地提示“请求内容不符合政策”,而不是显示一个晦涩的错误码。

5.3 客户端日志与监控

为了排查问题,需要在客户端记录关键日志(注意不要记录敏感信息):

  • 用户发送的请求(脱敏后)。
  • 收到的流式数据块和时间戳。
  • 请求的最终状态(成功、失败、中断)。
  • 网络延迟。

这些日志可以通过 console.log 输出(开发环境),或通过 fetch / Beacon API发送到你的监控服务器。结合错误监控服务(如Sentry),可以快速定位前端侧的问题。

6. 第五步:进阶模式与模式扩展

当基础的单轮流式对话满足后,可以探索更复杂的交互模式。

6.1 处理复杂输出:JSON模式与函数调用

有时,我们不仅需要模型生成文本,更需要它输出结构化的数据,或者根据对话内容决定调用某个工具(函数)。主流API(如OpenAI)支持 response_format tools (原 functions )参数。

  • JSON模式 :你可以要求模型始终以合法的JSON格式回复。这对于前端解析数据、进行后续操作极其方便。你需要在前端定义好期望的JSON Schema,并在系统指令中明确说明。
  • 函数调用(Tool Calls) :这是实现AI Agent的关键。你可以在请求中定义一系列“工具”(函数),描述其作用和参数。模型在思考后,可能会决定需要调用某个工具来获取信息(如查询天气、搜索数据库),它会返回一个 tool_calls 的响应,表明它想调用哪个函数以及参数是什么。 前端或后端需要根据这个响应,真正去执行这个函数(如调用一个天气API),然后将执行结果作为一条新的消息( role: ‘tool’ )再次发送给模型,让模型基于工具返回的结果生成最终的回答给用户。 这实现了模型与外部世界的交互。

6.2 多模态输入:从文本到图像

越来越多的模型支持多模态输入(如GPT-4V)。前端需要处理用户上传的图片,并将其编码(如Base64)或转换为文件URL,放入消息体的适当位置。请求的 Content-Type 会变为 multipart/form-data ,而不是 application/json 。处理流程会变得更复杂,但核心的流式接收和状态管理逻辑不变。

6.3 构建前端AI SDK抽象层

当你的应用中有多处需要调用AI能力时,建议将通信层、状态管理、错误处理等逻辑抽象成一个独立的、项目内部的“AI SDK”或自定义Hook(如果使用React)。

// 假设一个React Hook: useAIChat
function useAIChat(options) {
  const [messages, setMessages] = useState([]);
  const [isStreaming, setIsStreaming] = useState(false);
  const { apiEndpoint, defaultSystemPrompt } = options;

  const sendMessage = async (userInput) => {
    // 封装所有逻辑:构建消息列表、发起流式请求、处理数据块、更新状态、错误处理
  };

  const stopStreaming = () => {
    // 封装中断逻辑
  };

  const clearContext = () => {
    // 封装清空上下文逻辑
  };

  return { messages, isStreaming, sendMessage, stopStreaming, clearContext };
}

这样,在具体的UI组件中,你只需要调用 sendMessage ,而不用关心底层是如何与 fetch ReadableStream 打交道的,极大提高了代码的复用性和可维护性。

走到这一步,你的前端代码已经不仅仅是在“调用一个API”,而是在系统地集成一个“智能体”。你会开始思考如何设计提示词(Prompt Engineering)来让模型更稳定地输出你想要的格式,如何管理越来越复杂的对话状态,以及如何将AI能力无缝地编织到你的产品交互流程中。这个过程充满了挑战,但每解决一个问题,你都在为你的应用注入一种全新的、理解与生成自然语言的能力,这种能力的潜力是巨大的。

更多推荐