从零实现 AI 聊天助手:可直接复用的前端核心方案

从零实现 AI 聊天助手:可直接复用的前端核心方案

引言

项目目标:使用 Vue2 + TypeScript 构建 AI 聊天助手核心功能,实现「用户发送→AI逐字流输出」(类似 ChatGPT 的流式返回)效果,提供可落地的实现方案。

核心要求:

  • 视觉一致(左对齐、黑色、不斜体)
  • 输入锁定(避免并发推送)
  • 自动滚到底部
  • 支持请求真实API(SSE/ReadableStream)

1. 效果展示

本组件实现类 ChatGPT 的流式聊天体验。

核心效果

  • 用户消息实时展示
  • AI 回复逐字流式输出(打字机效果)

交互体验

  • 聊天区域自动滚动到底部
  • 流式过程中禁用输入,防止重复发送

UI规范

  • 统一干净的 UI 样式(左对齐、黑色常规字体)

完整流程

  • 输入消息 → 发送 → 顶部展示用户消息
  • 底部立即出现 AI 占位消息 → 逐字输出回复内容
  • 输出完成后恢复输入框,可继续对话

效果动图


2. 项目文件结构

核心文件说明,明确各文件作用与修改注意事项

  • chat.vue:聊天组件核心文件(重点开发文件)

  • chat-vue-technical-doc.md:技术说明文档(自动生成,无需手动修改)

  • main.ts / App.vue:全局路由与入口文件(无需改动,仅作为项目入口)


3. Chat 组件(核心功能组件)

3.1 基础配置与核心定义

3.1.1 数据结构定义
  • 定义Message接口,明确角色、内容、流式状态等核心字段。

  • 接口详情:role(区分user/assistant)、content(消息完整内容)、streaming(流式状态标识)、displayedContent(流式增量展示内容)。

  • 字段作用说明:用户消息直接渲染,AI消息先占位("空内容content='' + 流状态streaming=true + displayedContent=''")再在循环里逐字补充内容

ts 复制代码
interface Message {
  role: "user" | "assistant";
  content: string;
  streaming?: boolean;
  displayedContent?: string;
}
3.1.2 关键状态变量
  • messages: Message[] 存储所有聊天消息,管理消息状态
  • inputMessage: string 绑定输入框,存储用户待发送消息
  • isStreaming: boolean 控制UI输入锁定,保证一次仅一个流式输出

3.2 聊天流程:逐字打字机模拟

3.2.1 sendMessage 逻辑
  • 输入校验:检查空输入,流式过程中禁用发送功能

  • 消息处理:推送用户消息到messages数组,同时推送AI空占位消息

  • 流式调用:调用simulateStreaming方法,传入AI模拟响应和消息索引

  • 状态重置:模拟流式输出完成后,将isStreaming设为false,恢复输入功能

3.2.2 simulateStreaming 动画机制
  • 核心逻辑:循环遍历AI响应字符串,每50ms追加一个字符到displayedContent

  • 交互优化:每次追加字符后调用scrollToBottom(),保证消息实时可见

  • 状态更新:流式输出结束后,切换streaming为false,将完整响应内容写入content字段

  • 可扩展性说明:该逻辑支持后续替换为真实SSE/fetch stream/websocket,无需修改前端核心结构


3.3 模板与样式:可控 UI

3.3.1 模板渲染逻辑
  • 角色区分渲染:根据message.role判断渲染用户/AI消息

  • 流式状态渲染:AI消息流式输出时,渲染displayedContent;非流式状态渲染content

  • 核心模板代码示例:区分用户、AI流式、AI非流式三种场景的渲染逻辑

html 复制代码
<span v-if="message.role === 'user'">{{ message.content }}</span>
<span v-else-if="message.streaming" class="streaming-text">{{ message.displayedContent }}</span>
<span v-else>{{ message.content }}</span>
3.3.2 样式调整
  • 基础样式:.message类设置text-align:left,保证所有消息左对齐

  • 流式文本样式:.streaming-text设置color:#000、font-style:normal、white-space:pre-wrap,保证视觉一致

  • 样式效果:避免流式文本灰色、斜体,提升阅读体验


4. 大模型 API 集成

以智谱 AI为例,前端用 fetch + ReadableStream 实现流式。

需要前往 智谱开放平台 注册并创建应用,获取 API Key.

4.1 实现目标

  • 调用智谱 GLM 大模型接口
  • 开启流式输出(stream: true)
  • 实时接收 AI 逐字返回的内容
  • 动态更新到页面上,实现打字机效果

4.2 实现细节

4.2.1 方法定义与基础配置
ts 复制代码
async fetchAIStream(aiMsgIdx: number) {
  const apiKey = "62e6df10d65d43b09c97bb4d3c340bce.xxxxxxxxxxxx";// 替换为你的 API Key 
  const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
  • async:标记这是异步函数,内部可以用 await 等待接口响应
  • aiMsgIdx: number:接收一个消息索引,用来定位要更新的 AI 消息
  • apiKey:智谱大模型的身份密钥(你自己的密钥)
  • url:智谱官方的对话接口地址
  • sendMessage()调用fetchAIStream()时需要await,等待全部
4.2.2 发送 POST 请求给大模型
ts 复制代码
const response = await fetch(url, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${apiKey}`, // 身份验证
  },
  body: JSON.stringify({
    model: "glm-4.5-flash", // 使用的模型版本
    stream: true, // ✅ 核心:开启流式输出
    messages: this.messages
      .map((msg) => ({
        role: msg.role,
        content: msg.content || msg.displayedContent,
      }))
      .filter((m) => m.role === "user" || m.role === "assistant"),
  }),
});

这部分是请求核心:

  • fetch:浏览器原生 API,发送网络请求
  • headers:请求头,携带身份凭证和数据格式
  • stream: true:最重要的参数!告诉大模型不要一次性返回全部答案,而是逐字流式返回
  • messages:对话上下文
    • 只保留 user(用户)和 assistant(AI)的消息
    • 取完整内容 content 或正在显示的内容 displayedContent
为什么用fetch而不是其他请求

一句话结论:可以用 axios,但不推荐!流式输出必须用 fetch.

因为 AI 流式输出(SSE / ReadableStream) 依赖一个核心能力:

读取原生数据流(stream / ReadableStream)。fetch 天生支持response.body.getReader()(可以逐块读取数据)。axios 目前完全不支持:axios 会等整个请求全部结束才把结果一次性给你,不能实时拿到每一段文字。也就是说:fetch = 边煮边喝汤(流式);axios = 煮完才能喝(一次性)。

Axios 官方文档写得很清楚:Axios does not support streaming responses. Axios 不支持流式响应。它会把整个返回缓存起来,直到结束才返回,无法实现打字机效果。

那能不能强行让 axios 支持?能,但非常麻烦:要装额外插件 axios-observable. 要配置复杂;兼容性差;不如 fetch 原生稳定、简洁。

一句话总结:AI 流式聊天 = 必须用 fetch, 普通接口(增删改查)= 随便用 axios /fetch.

4.2.3 初始化流式读取器
ts 复制代码
const reader = response.body?.getReader();
const decoder = new TextDecoder("utf-8");
  • response.body?.getReader():获取流式响应读取器,用来一点点接收数据
    • response.body → 是一个 ReadableStream 对象(浏览器自带)
    • .getReader() → 获取这个流的读取器(ReadableStreamDefaultReader)
    • const reader = response.body?.getReader();是浏览器原生 Fetch API + 流式响应 (ReadableStream) 标准用法
  • TextDecoder("utf-8"):把二进制流解码成我们能看懂的文字
4.2.4 循环读取流式数据
ts 复制代码
while (true) {
  const { done, value } = await reader!.read();
  if (done) break; // 数据接收完毕,退出循环
  • while(true):无限循环,持续读取流数据
  • reader.read():读取一段数据,返回两个值
    • done:是否读取完成
    • value:本次读取到的二进制数据
  • 读完就 break 跳出循环
  • 为什么你看不到 done?done: true 是 浏览器流 ReadableStream API 内部返回的状态,不是后端返回的文本,不会出现在 data: xxxx 里,只有流彻底关闭时,才会返回 { done: true }。所以你抓包看不到,但代码里能读到。
  • 整体代码流程
cpp 复制代码
1. 发送请求
2. 拿到 ReadableStream(response.body)
3. 拿读取器 reader
4. while 循环 reader.read()
   → 读一段数据
   → 解析显示
5. 后端最后发 data: [DONE]
6. 后端关闭连接
7. 浏览器自动返回 { done: true }
8. 循环 break,结束
  • 观察接口返参
4.2.5 解析每一段流式数据
ts 复制代码
const chunk = decoder.decode(value); // 二进制转文字
const lines = chunk.split("\n").filter((line) => line.trim());
  • 解码二进制数据为文本
  • 按换行符拆分(大模型流式返回的格式要求)
  • 过滤空行,避免无效数据
4.2.6 处理标准 SSE 格式数据
ts 复制代码
for (const line of lines) {
  if (!line.startsWith("data: ")) continue; // 只处理以 data: 开头的行
  const data = line.slice(6).trim();
  if (data === "[DONE]") continue; // 结束标记,跳过

这是大模型流式返回的固定格式(SSE)

  • 每一段数据都以 data: 开头
  • 最后会返回 data: [DONE] 表示传输结束
  • 这里做格式过滤,只保留有效内容
4.2.7 解析 JSON 并实时更新页面
ts 复制代码
try {
  const json = JSON.parse(data);
  const content = json.choices[0]?.delta?.content || "";
  this.messages[aiMsgIdx].displayedContent += content;
  this.$nextTick(() => this.scrollToBottom());
} catch (e) {}

这是页面实时显示的核心:

  • 解析 JSON 数据
  • 取出流式增量内容:choices[0].delta.content
  • 追加到对应 AI 消息的 displayedContent 上(不是覆盖!)
  • $nextTick:Vue 异步更新 DOM 后,自动滚动到页面底部
  • try/catch:捕获解析异常,避免页面报错

4.3 注意事项

  • API Key 直接写在代码里不安全(生产环境要放后端)
  • 依赖浏览器原生 fetch,不支持非常老的浏览器
  • 必须开启 stream: true,否则无法流式接收

4.4 调用方式

这个方法一般在发送消息后调用。

ts 复制代码
// 示例:发送用户消息 → 添加一条空的AI消息 → 调用流式方法填充内容
this.messages.push({ role: "user", content: "你好" });
const aiMsgIdx = this.messages.length;
this.messages.push({ role: "assistant", displayedContent: "" });
// 开始流式输出
try {
   await this.fetchAIStream(aiMsgIdx);
} catch (error) {
  console.error("请求出错", error);
  this.messages[aiMsgIdx].content = "请求失败,请稍后重试";
} finally {
  this.messages[aiMsgIdx].streaming = false;
  this.messages[aiMsgIdx].content =
    this.messages[aiMsgIdx].displayedContent || "没有回复内容";
  this.isStreaming = false;
}

4.5 实现效果

lz


5 Markdown 格式适配

实现目标

将消息内容中的 Markdown 语法(标题、列表、代码块、引用、表格、链接、加粗斜体等) 渲染为美观的富文本样式,并支持代码高亮。

技术选型

技术选型

  • marked:Markdown 解析渲染
  • highlight.js:代码块语法高亮

安装依赖

marked 依赖支持所有 Markdown 语法:标题、粗体、斜体、列表、表格、引用、链接、图片;代码块高亮(几十种语言);代码高亮主题可换; Vue2 样式穿透:用 ::v-deep 让样式作用于 v-html 渲染的内容。

bash 复制代码
npm install marked@4.3.0 highlight.js

Vue2 用 marked 4.x 最稳定,不会报错。

Markown渲染

html 复制代码
<script lang="ts">
/* eslint-disable */
import { Component, Vue } from "vue-property-decorator";
import { marked } from "marked";
import hljs from "highlight.js";
// 代码高亮主题(可替换)
import "highlight.js/styles/github-dark.css";

....
	// 渲染方法
  renderMarkdown(text: string): string {
    marked.setOptions({
      gfm: true,
      breaks: true,
      highlight: function (code: string, lang?: string) {
        try {
          if (lang && hljs.getLanguage(lang)) {
            return hljs.highlight(code, { language: lang }).value;
          }
          return hljs.highlightAuto(code).value;
        } catch (e) {
          return code;
        }
      },
    });
    return marked.parse(text) as string;
  }
}
</script>

实现效果

优化建议

  • 标题上下间距有点大,建议通过样式穿透重新调整间距大小。
  • 渲染方法封装

5 问题修复日志

5.1 视觉跳变

问题现象

流式过程中用的是 .streaming-text 样式,流式结束后用的是普通消息样式。两个样式不一样 → 视觉跳变。

解决思路

将流式输出过程的样式添加到流失输出结束后的div上即可。

修复效果


5. 项目要点总结

  • 核心原则:UI与数据分离,messages数组仅存储消息状态,不耦合渲染逻辑

  • 流式核心:通过增量更新displayedContent模拟打字机效果,实现优雅的流式体验

  • 交互规范:输入锁定、自动滚动、样式统一,保证用户体验一致

  • 可扩展性:前端结构与后端解耦,后续替换真实API无需修改前端核心代码

  • 适用场景:聊天交互、文案生成、关键进度可视化等需要流式输出的场景


6 迭代优化方向

  • 功能拓展

    • 新增表情、图片发送功能,丰富消息展示形式

    • 拆分核心子组件(ChatMessage、ChatInput),提升组件复用性

    • 抽取可复用打字机组件,单独封装为src/components/TypingMessage.vue

  • 体验优化

    • 添加加载动画、错误提示,提升异常场景体验

    • 优化打字机效果,采用分块渲染(每句独立显示),搭配动态光标

    • 完善异常处理,添加try/catch捕获接口报错,支持重试功能

  • 性能优化

    • 针对大量历史消息渲染优化,避免页面卡顿

    • 取消全局isStreaming锁,改为消息级独立状态,支持多消息并行流式输出

    • 优化渲染逻辑,减少DOM重绘与回流

  • 规范优化

    • 配合Vite/Vue CLI,统一代码规范,避免语法冲突

    • 完善项目注释,提升代码可读性与可维护性

相关推荐
码上生存指南3 小时前
我让 Claude、ChatGPT、Kimi 同时帮我写代码,差距有点大
ai·chatgpt
积跬步,慕至千里5 小时前
2026年2月读书笔记|AI大模型助你轻松搞定数据分析
语言模型·chatgpt
Agent产品评测局7 小时前
企业自动化项目,如何做好内部推广与员工培训?——企业级智能体落地与人才赋能实测指南
运维·人工智能·ai·chatgpt·自动化
向量引擎7 小时前
肝了三天三夜!四大AI模型(DeepSeek/Gemini/ChatGPT/豆包)深度横评,开发者该如何选?
人工智能·chatgpt·架构·开源·aigc·文心一言·api调用
AI-Ming9 小时前
程序员转行学习 AI 大模型: 模型微调| 附清晰概念分类
人工智能·pytorch·深度学习·机器学习·chatgpt·nlp·gpt-3
薛定猫AI1 天前
【深度解析】Claude Auto Dream:从“短期对话”到“项目级心智模型”的记忆系统升级
人工智能·chatgpt
树谷-胡老师1 天前
基于AI工具(ChatGPT、OpenClaw等)工作流的高强度论文写作实战
人工智能·chatgpt
承渊政道1 天前
从n-grams到Transformer:一文读懂语言模型基础
深度学习·学习·语言模型·自然语言处理·chatgpt·transformer·机器翻译
前端飞行手册1 天前
electron应用开发模板,集成多种解决方案
前端·javascript·学习·electron·前端框架·vue