尧图精选

基于Vue3和UniApp的全端AI问答助手开发实践

🕒 发布时间:2026/9/9 18:46:26 📁 来源:尧图网络
做 AI 问答助手这两年已经不是新鲜事了但真正做起来会发现要把它跑在微信小程序、H5 网页、Android 和 iOS App 上还要支持流式打字输出、Markdown 富文本、数学公式、图片语音多模态输入工作量远远超出做一个普通聊天页面。我这段时间用 Vue3 UniApp 把一个相对完整的问答助手从零重构并落地到了全端中间踩了不少坑也沉淀了一套可以复用的方案这篇就把整个项目的设计思路、关键技术选型和实操细节完整梳理一遍给正在做类似 AI 应用、跨端开发或者准备用 UniApp 承接复杂交互的团队和个人做个参考。1. 项目到底在做什么先拆清需求再动手1.1 “沉浸式”这三个字怎么落地标题里最容易被忽略的词是“沉浸式”。很多人以为这是视觉层面的美化需求做套暗色 UI 就完事但实际操作下来“沉浸式”至少要解决四个维度的体验问题第一是视觉沉浸。深色主题、自定义导航栏、沉浸式状态栏、消息气泡的圆角过渡、背景渐变这些是基础。App 端还要处理 iOS 底部安全区和 Android 状态栏的显示一致性否则一个刘海屏一个挖孔屏页面观感会差很多。第二是交互沉浸。AI 回答不是一次性加载出来的而是像在聊天软件里收到对方正在输入一样逐字或逐段流式出现。这里的核心是消息列表跟随输出自动滚动且滚动节奏要跟输出速度匹配太快会让用户看不到内容变化太慢又显得拖沓。实测下来每秒 30 到 60 个字的输出速度配合平滑滚动体感最接近真人对话。第三是功能沉浸。多轮对话要能记住上下文不能每次回答都像失忆。用户问“那第二个方案呢”系统得知道“那”指的是上一轮的哪个方案。这要求前端在组织请求时维护好上下文窗口后端再根据需求做会话记忆或摘要压缩。第四是场景沉浸。问答助手不该只是冷冰冰的对话框还要考虑快捷指令、历史会话、语音播报、多模态输入等附加能力。这些功能一旦拆出来消息模型和状态管理就会变得复杂必须在项目一开始就规划好。说白了沉浸式不是一句包装词而是贯穿数据模型、渲染层、交互层的设计约束。1.2 为什么选 Vue3 UniApp不直接写原生这个项目立项时对比过原生开发、Flutter、React Native 和 UniApp最终选了 Vue3 UniApp 的组合核心原因是团队要覆盖的端太多了。如果三个端各写一套原生代码App 端 Kotlin/Swift、小程序端原生 WXML、H5 端又是另一套工程量至少是跨端方案的 3 倍。Flutter 和 React Native 在 App 端的表现确实不错但都绕不开小程序这道坎。Flutter 要额外内嵌小程序容器React Native 则需要再套一层小程序框架链路长了维护成本不降反升。UniApp 的优势在于从底层就把“编译到多端”这件事内置了。一套 Vue3 代码通过 HBuilderX 或 CLI 编译能同时产出微信小程序、H5、Android/iOS App 包。Vue3 的 Composition API 对复杂状态管理、逻辑复用非常友好配合 Pinia 做会话级状态管理代码组织比 Vue2 的 Options API 清晰得多。但也要说清楚边界。UniApp 适合逻辑密集型、偏业务的应用像我们的问答助手、电商、工具类都很好使。如果产品核心是视频剪辑、3D 渲染、重度游戏这类需要极度贴近原生能力的场景跨端框架的插件桥接成本和性能损耗会让你很难受那时候就应该认真评估纯原生或 Flutter 的方案。1.3 全端产品矩阵与工程结构规划这个项目的端侧清单是微信小程序、H5、Android App、iOS App。四端共用一个技术栈但各自的工程配置、权限声明、发布流程完全不同。工程结构上我建议按功能模块而不是按端来划分目录条件编译只下沉到最底层。项目跑起来的目录结构是这样src/ ├── pages │ ├── index/index.vue # 聊天主页面 │ ├── history/history.vue # 历史会话列表 │ └── settings/settings.vue # 设置与模型配置 ├── components │ ├── message-item.vue # 单条消息展示 │ ├── markdown-renderer.vue # Markdown 渲染封装 │ ├── input-bar.vue # 底部输入栏文字/图片/语音 │ └── audio-player.vue # 语音播报组件 ├── api │ ├── chat.js # 对话接口 │ └── upload.js # 文件上传接口 ├── store │ └── chat.js # Pinia 会话状态 ├── utils │ ├── markdown.js # markdown-it 配置 │ ├── stream.js # SSE 流式读取封装 │ └── theme.js # 主题切换 └── static状态管理用 Pinia它比 Vuex 轻、对 TypeScript 支持更好而且 Vue3 配合script setup写起来很顺手。每个会话的消息列表、流式输出状态、页面滚动位置都放在 store 里页面组件只负责展示和派发事件这样多开聊天窗口、切后台回来恢复消息等需求都会好处理很多。2. 会话引擎与消息流AI问答的核心架构2.1 消息数据模型从单条消息到完整会话AI 问答助手的前端数据模型我不会用很简单的content字符串去糊弄。实际项目里每条消息至少要带身份、内容类型、状态、附件等字段否则后面扩展图片对话、语音输入、错误重试时改表结构能改到崩溃。我常用的消息结构是这样interface ChatMessage { id: string role: user | assistant | system contentType: text | image | audio | file | markdown content: string createdAt: number status: pending | streaming | done | error reasoning?: string // 如果后端支持思维链单独存 attachments?: Attachment[] // 多模态附件列表 error?: string // 错误提示信息 } interface Attachment { type: image | audio | file name: string url: string size: number ext?: string }status字段是流式输出的关键。用户在界面上发一条消息后这条消息先进入pending状态展示一个气泡加 loading后端把连接建立起来、开始吐字后变成streaming内容不断累加输出完成切到done连接中断或接口报错则标记为error允许用户点击重试。会话模型就简单一些interface ChatSession { id: string title: string messages: ChatMessage[] createdAt: number updatedAt: number }历史会话通过uni.setStorageSync或后端的会话接口持久化前端只维护当前会话的消息数组。2.2 流式输出SSE接入与渲染时序AI 问答的流式输出市面上主流方案是 SSEServer-Sent Events或 WebSocket。我最终选了 SSE原因很简单对话场景是“用户发一条服务端流式回一条”方向比较单一SSE 天然支持断线重连服务端实现也简单一个 POST 请求就能建立。前端核心是用 Fetch API 结合ReadableStream来读取流不能用普通的await response.json()因为响应体不会一次性结束。完整读取逻辑大致是const response await fetch(/api/chat/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ messages: contextMessages }) }) const reader response.body.getReader() const decoder new TextDecoder(utf-8) let buffer while (true) { const { done, value } await reader.read() if (done) break buffer decoder.decode(value, { stream: true }) const lines buffer.split(\n) buffer lines.pop() for (const line of lines) { if (line.startsWith(data: )) { const payload JSON.parse(line.slice(6)) handleChunk(payload) } } }这里有一个非常容易踩的坑底层数据是按字节流的一个中文字符在 UTF-8 里占 3 个字节可能被切到两个 chunk 里。如果不给TextDecoder传{ stream: true }中文会显示成乱码。我最初没注意结果接口输出英文正常、中文隔三差五出现一个“”字符排查半天才发现是这个原因。拿到 chunk 后的渲染时序更重要。每次收到内容片段不是立刻重渲染 Markdown而是先把文本累积到消息的content字段用节流函数控制渲染频率每隔 100 毫秒左右把累积内容交给 Markdown 渲染器转换成 HTML再更新到页面。这样既能保证打字机效果连贯又不至于因为渲染太频繁导致页面卡顿。2.3 上下文管理与多轮对话策略对话助手能“记得”你说过什么并不是靠前端硬撑而是依赖后端对上下文的理解能力。前端要做的是把合适的上下文信息传过去。实现方式是维护一个消息数组每次请求时把最近若干条消息拼成一个上下文数组发送给后端。这里有几个策略滑动窗口只取最近 N 条消息比如 10 条超出部分丢弃。简单但长对话中早期信息会丢失。Token 数限制估算消息的 token 量中文大概 1.5 到 2 个字符算一个 token超出上限时删最旧的消息。摘要压缩对话真的很长时可以把早期消息交给模型生成一段摘要作为 system 消息注入。这个我放在后端的会话服务里做前端只负责透传 sessionId。前端还要注意 system 指令的透传。比如产品要求“你是一位资深代码架构师回答请使用中文”这类 system 消息要始终放在上下文数组的最前面不能被滑动窗口挤掉。做法是拆分systemMessages和historyMessages两个数组请求时把 system 消息放在头部拼接。3. Markdown 与公式渲染跨端方案全对比3.1 H5/App 端markdown-it KaTeX highlight.jsAI 回答很少是纯文本代码块、表格、列表都很常见。渲染方案我选了 markdown-it、KaTeX 和 highlight.js 的组合这也是社区使用最广的一套搭配。markdown-it 的插件生态很好支持自定义渲染规则。相比 markedmarkdown-it 输出结构更规范性能也更好。基本配置import MarkdownIt from markdown-it import hljs from highlight.js const md new MarkdownIt({ html: false, linkify: true, breaks: false, highlight(str, lang) { if (lang hljs.getLanguage(lang)) { try { return pre classhljscode${hljs.highlight(str, { language: lang }).value}/code/pre } catch (__) {} } return pre classhljscode${md.utils.escapeHtml(str)}/code/pre } })注意html: false要设置。因为 AI 的输出是模型生成的万一模型输出了恶意的script或者带事件的标签前端直接v-html渲染就是安全漏洞。这点在嵌入公网的产品里尤其重要。公式渲染选了 KaTeX 而不是 MathJax核心原因是 KaTeX 体积小、渲染快对移动端更友好。MathJax 功能更强但也重得多在小程序环境里没法直接用。KaTeX 的使用方式是在 Markdown 解析前处理数学块或者通过 markdown-it 的插件扩展语法。我用的方案是 markdown-it-texmath 插件它能把$$...$$和$...$分别渲染成块级和行内公式。3.2 小程序端towxml 与受限环境下的替代方案小程序端的 Markdown 渲染是整个项目最头疼的部分没有之一。原因是小程序没有真正的 DOM不能直接用v-html而微信自带的rich-text对 HTML 的支持极其有限不支持 class 选择器样式必须内联标签白名单也有限。AI 回答里的复杂 HTML、表格、代码块用rich-text基本会乱。社区里比较主流的方案是 towxml。它是一个专门给小程序解析 Markdown 和富文本的库底层先把 Markdown 解析成 JSON 结构再用小程序的 template 递归渲染这样能绕开rich-text的限制。我们最后也是基于 towxml 做了二开增加了暗色主题样式和代码块复制按钮。但如果你的项目对渲染结果有强要求、或者不想依赖第三方库我建议用更可控的方案后端把 Markdown 转成结构化的 JSONAST前端拿 JSON 去模板递归渲染。优点是渲染完全可控、体积小、暗色主题容易做缺点是前后端都要投入人力而且后端得维护 Markdown 解析服务。还有一个退而求其次的方案公式和复杂表格转图片。后端用工具把 LaTeX 公式渲染成 SVG 或 PNG前端用image标签加载。这个方案兼容性最好但渲染出来的文字不能选中复制体验打折扣。对于公式量不大的场景可以只对少见场景做降级处理。3.3 代码高亮与主题样式的沉浸式适配代码高亮有两个选型highlight.js 和 Prism.js。我选了 highlight.js因为语言支持丰富、默认主题多配合 markdown-it 的highlight回调很顺。暗色主题下要特别注意代码块配色。我用的 highlight.js 的github-dark主题同时在消息组件的样式中做了统一代码块背景用#1e1e2e这种深蓝灰文字用浅灰白色关键字和字符串分别给到不同的亮色保证对比度符合无障碍标准。还有一个体验细节是代码块右上角加复制按钮。这个在 H5 和 App 端容易实现小程序端需要利用uni.setClipboardData来做我给 towxml 的代码块渲染模板里加了点击事件点击text就复制代码内容。公式的配色也要调。KaTeX 默认文字颜色继承父元素在暗色背景上如果没设置颜色公式会变成深色看不见。解决办法是在 KaTeX 输出的根节点上加 CSS.katex { color: #e0e0e0 !important; }这里优先级要够高否则会被组件内部样式覆盖。3.4 流式输出过程中的增量渲染优化流式输出最大的性能矛盾是AI 每吐几个字消息内容就变一次Markdown 解析和渲染如果每次全量跑消息多了以后页面会越来越卡。我的优化思路是“节流 全量重新渲染”而不是“增量拼 HTML”。为什么不直接拼 HTML因为 Markdown 是结构化的一段代码块可能被分成好几个 chunk 才闭合如果只渲染新增片段很容易出现pre没闭合、列表序号错误、表格错位这类问题。从完整 content 重新渲染虽然 CPU 开销大一些但结果永远是对的。节流的实现很粗暴let renderTimer null function scheduleRender() { if (renderTimer) return renderTimer setTimeout(() { renderCurrentMessage() renderTimer null }, 100) }实测下来一个 2000 词的 Markdown 回答100ms 节流足够保持页面 60 帧。如果回答特别长还可以在renderCurrentMessage内部判断如果当前累计字数超过 5000就改成 250ms 节流给移动端更多喘息空间。4. 多模态交互让问答助手从“文字对文字”升级4.1 图片输入链路全流程多模态是 AI 问答助手的加分项但接入链路比文字输入长很多。图片输入的完整链路是选择图片 - 本地压缩 - 上传到对象存储或后端 - 后端把图片交给视觉模型生成描述或直接参与对话 - 前端拿到结果。选择图片用uni.chooseImage这是 UniApp 封装好的跨端 APIconst res await uni.chooseImage({ count: 1, sizeType: [compressed], sourceType: [album, camera] })如果要支持用户拍照Android 端记得在manifest.json的 App 模块配置里勾选相机权限iOS 要在Info.plist中声明NSCameraUsageDescription和NSPhotoLibraryUsageDescription否则真机调试时会闪退。上传前最好做一次本地压缩。图片直接传原图网络差的时候等待时间很长用户体验非常差。用uni.compressImage把图片压缩到 1280px 宽度以内、质量 80%在保证 OCR 和视觉模型可用性的同时上传体积至少能缩小 60%。上传之后的处理有两种一种是把图片 URL 和文字描述一起放进消息内容里发给后端另一种是把图片转成 base64 直接拼在 Prompt 里。前者对 token 消耗更友好后者实现简单但会让请求体变大我倾向第一种。4.2 语音输入的接入方案语音输入分两步前端录音 后端转写。录音用uni.getRecorderManager()const recorderManager uni.getRecorderManager() recorderManager.onStart(() { recording.value true }) recorderManager.onStop((res) { uploadAudio(res.tempFilePath) }) recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: mp3 })这里要注意采样率。16000Hz 单声道是大多数 ASR 服务的标准输入格式用 44100Hz 的立体声反而会让后端处理慢。录音完成后通过uni.uploadFile上传音频后端转写出文字后再把文字填入输入框用户确认后发送。这种“先转文字再发送”的设计比直接发语音更符合用户预期也方便编辑。iOS 端录音权限要提前在manifest.json里配置好描述文案否则首次调用录音时系统弹窗文案会显示默认英文很影响观感。4.3 文件与其他输入类型的扩展设计除了图片和语音问答助手还可以支持 PDF、Word、Excel、TXT 等文件输入。后端的解析能力我们做了限制单个文件不超过 20MBPDF 和 Word 统一转成纯文本后参与对话。前端扩展新输入类型时不需要改消息组件只需要扩展Attachment的类型和输入栏的按钮。这个插件化设计让我在后续加“知识库导入”功能时省了很多事。新增一个文件类型在contentType里加一个枚举值、在输入栏加一个入口、在消息组件里加一个渲染分支三步搞定。4.4 多模态输出卡片的渲染策略多模态不只是输入输出也可以多样化。AI 回答可能不只有文本还会返回图片、表格、图表代码甚至语音播报。我的实现是让后端返回结构化消息而不只是一个字符串。格式类似{ type: text, content: 这是回答的文字部分 }或者带卡片的组合{ type: mixed, blocks: [ { type: text, content: 先说明结论 }, { type: table, data: { headers: [日期, 数值], rows: [] } }, { type: image, url: https://example.com/chart.png } ] }前端根据blocks数组用动态组件渲染不同类型的卡片。流式输出时文字部分的卡片持续累积更新图片表格这类静态卡片等数据完整后一次性渲染。这样的渲染策略比把所有内容拼成一个大 Markdown 字符串更容易控制样式也方便用户交互比如表格可以加“复制”、“下载 CSV”按钮。5. 全端体验与性能调优实录5.1 自定义导航栏与沉浸式状态栏适配沉浸式体验的第一步就是干掉默认导航栏。在pages.json里给聊天主页设置navigationStyle: custom然后页面上自己画一个自定义导航栏。自定义导航栏的核心是适配状态栏高度。不同机型的statusBarHeight不一样用uni.getSystemInfoSync()获取const systemInfo uni.getSystemInfoSync() const statusBarHeight systemInfo.statusBarHeight || 20 const safeAreaBottom systemInfo.safeAreaInsets?.bottom || 0页面顶部用一个动态高度的占位视图撑开避免内容被状态栏遮挡。App 端 iOS 底部还有 home indicator也需要用safeAreaBottom给底部输入栏留出安全距离。这里有一个经验状态栏高度在不同端获取的时机不一样。小程序端uni.getSystemInfoSync()在页面 onLoad 时一定能拿到正确值App 端偶尔会因为 WebView 初始化时序问题拿到默认值所以我在App.vue的onLaunch里提前获取并存到全局变量页面再读。5.2 消息列表长列表优化聊天消息如果一直往上堆页面节点会越来越多滚动手感越来越差。我的经验是把长列表优化分成两层。第一层是数量限制。只保留当前会话最近 100 条消息在内存里超过后最早的消息从数组中摘除但仍然展示一个“加载更早消息”的按钮。这一层能解决 90% 的问题实现简单稳定性好。第二层是渲染范围的虚拟化。UniApp 里实现真正的虚拟滚动没有现成的通用方案不同端差异很大。App 端可以尝试list组件或原生插件但小程序端scroll-view的virtual属性在部分场景下会跟动态高度冲突。我的建议是如果消息总量控制在 100 条以内用scroll-viewscroll-into-view完全够用超过 100 条再做虚拟列表否则收益不大但复杂度飙升。每次新消息或流式输出时自动滚动到底部可以用scroll-into-view绑定最后一条消息的 id注意别在用户往上翻看历史时强制跳走那体验会很差。5.3 小程序分包与包体积控制微信小程序主包有 2MB 限制一个markdown-it加highlight.js就吃掉不少体积分包是必须做的。按功能拆分分包主包只留聊天主页面和核心公共组件历史和设置页面放进分包{ pages: [ { path: pages/index/index, style: { navigationStyle: custom } } ], subPackages: [ { root: pages/history, pages: [index] }, { root: pages/settings, pages: [index] } ] }highlight.js默认引入所有语言体积很大。我按需引入 Java、Python、JavaScript、TypeScript、Shell、JSON、SQL 这些常用语言体积能减掉一大半import hljs from highlight.js/lib/core import javascript from highlight.js/lib/languages/javascript import python from highlight.js/lib/languages/python import java from highlight.js/lib/languages/java import typescript from highlight.js/lib/languages/typescript import shell from highlight.js/lib/languages/shell import json from highlight.js/lib/languages/json import sql from highlight.js/lib/languages/sql hljs.registerLanguage(javascript, javascript) hljs.registerLanguage(python, python) hljs.registerLanguage(java, java) hljs.registerLanguage(typescript, typescript) hljs.registerLanguage(shell, shell) hljs.registerLanguage(json, json) hljs.registerLanguage(sql, sql)CSS 也要按需抽取。KaTeX 和 highlight.js 的 CSS 体积不小如果直接用全量样式光样式文件就接近 100KB建议使用压缩版并按需加载。5.4 暗色主题切换的细节处理暗色主题是沉浸式助手的外衣但它不只是把背景改成黑色那么简单。我把所有颜色抽成 CSS 变量在page选择器上定义两套主题page { --bg-primary: #f5f5f7; --bg-message-user: #007aff; --text-primary: #1a1a1e; --code-bg: #f6f8fa; } page.dark { --bg-primary: #111118; --bg-message-user: #1e1e2e; --text-primary: #e4e4e7; --code-bg: #1e1e2e; }切换主题时只需要在根节点上增删darkclass。小程序端用page标签做根选择器H5 端用html。系统主题变化用uni.onThemeChange监听App 和小程序都支持。但要注意用户手动选择的主题优先级要高于系统主题否则用户选了浅色、系统切到暗色页面会“跳变”。代码块、公式、表格在暗色下的样式要专项检查。我踩过最典型的坑是H5 端暗色正常小程序端代码块背景还是白色原因是 towxml 内部组件用了固定 classCSS 作用域隔离导致变量没传进去最后用!important覆盖解决的。6. 发布前后的常见问题与排查手册6.1 小程序、App、H5 的差异化 BUG 清单跨端开发最大的成本不是写代码是处理各端差异。我把这几个月踩过的坑整理成了一份清单写在这里供大家对照排查。问题现象原因解决方案小程序报not found: pagepages.json里未注册页面或路径大小写不一致检查pages.json的 pages/subPackages 配置确保路径与实际文件一致H5 端 CSS 变量不生效部分旧浏览器不支持 CSS Variables用webpack的 postcss 插件降级或避免在关键位置使用变量App 端v-html中图片不显示远程图片域名未配置白名单App 端在 manifest 的联网权限中配置如果图片是 webp 格式还要确认渲染引擎支持iOS 键盘弹起遮住输入框键盘弹起高度未计算用uni.onKeyboardHeightChange监听键盘高度手动调整输入框位置Android 部分机型输入框被顶起软键盘adjustResize行为不一致在 manifest 里配置softinputMode: adjustResize并配合uni.onKeyboardHeightChange小程序富文本图片点击无法预览rich-text不绑定点击事件用image标签替换img或者用 towxml 的图片点击事件扩展这里面最隐蔽的是 Android 键盘问题。不同厂商的系统键盘弹起策略不一样有的会触发页面 resize有的只是覆盖。最终我采用uni.onKeyboardHeightChange来统一处理在键盘高度变化时动态给底部输入栏加padding-bottom在 App 端实测稳定。6.2 打包流程中的版本与证书问题UniApp 打包最常翻车的是版本匹配。HBuilderX 打包时需要用对应版本的本地 SDK否则会出现“SDK 版本与 HBuilderX 版本不匹配”的报错。我的做法是固定 HBuilderX 版本所有协作成员统一使用同一个版本升级时先把本地 SDK 也一并升级到对应版本。Android 上架应用市场需要签名证书。用 Android Studio 或keytool生成正式签名的.keystore文件然后在 manifest.json 的 App 打包配置里关联打包时勾选“使用本地签名”。第一次生成证书后一定要把密码和别名记下来丢了就得上架后换包名重新来。iOS 需要开发者账号和描述文件。测试阶段用开发描述文件上架用发布描述文件测试设备还要把设备 UDID 加到账号里。正常的流程是在 Apple Developer 后台创建 App ID、生成描述文件、一键打包出 ipa再用 TestFlight 或 Xcode 上传到 App Store Connect。整个过程最容易卡住的是描述文件的 Bundle Identifier 和 manifest.json 里的 AppID 不一致打包前检查一遍能省很多时间。6.3 体验细节键盘弹起、安全区、横屏等除了功能细节体验决定产品质感。键盘弹起的问题在前面提过这里再补充一个细节当键盘弹起时输入框要跟随键盘上移但页面背景不能被顶得错位。在 H5 端直接把输入框定位成fixed并用padding-bottom补偿App 端监听键盘高度小程序端设置adjust-position属性为 true部分场景下用cursor-spacing控制输入框与键盘间距。安全区适配除了底部env(safe-area-inset-bottom)页面顶部的自定义导航栏和状态栏之间也会存在隔断。我推荐把导航栏背景色和页面背景色统一视觉上融为一体这样刘海屏和挖孔屏的观感都能接受。横屏适配看产品需求决定。AI 问答助手大多时间使用竖屏但如果支持从微信分享链接打开时部分平板设备会自动横屏。简单做法是在页面onLoad里锁定竖屏uni.pageScrollTo不处理这个问题需要在 App 端用plus.screen.lockOrientation(portrait-primary)。但这类强制锁屏操作会限制用户体验如果产品没有强需求尽量保持自适应。结尾一些个人的经验体会跑完这个项目我最大的感受是跨端方案真正的复杂度不在写页面而在于把不同端的差异收敛到同一个抽象层。消息模型和渲染层一定要做稳这是整个 AI 问答助手的地基。流式输出、Markdown、公式、多模态这些能力都是在地基上长出来的分支基础不稳后面任何一端出问题都会牵连全局。如果让我重新做一遍我会建议从消息模型和渲染层开始多模态往后再扩展。很多团队上来就堆功能结果消息数据结构定得不够灵活做到图片对话时被迫重构那才是最伤筋动骨的事。最后分享一个小技巧线上环境一定要保留一份“明文文本对话”的降级模式。AI 回答偶尔会出现 Markdown 渲染崩坏的情况比如代码块没有正确闭合导致整页错乱。我加了一个“原文/渲染”切换开关用户遇到渲染异常可以一键切回纯文本既保住了阅读底线又给了我排查问题的时间。这个开关看着不起眼但运营反馈它救了很多次“看起来像 bug 的体验问题”。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →