
最近在给产品搭一个统一的AI问答入口要求能同时跑在微信小程序、H5网页和App三个端上。核心功能其实不复杂一个聊天式的智能问答助手能渲染Markdown和数学公式还要支持上传图片做多模态识别交互。这种需求在Vue生态里做单端很容易但一旦要求全端覆盖很多坑就冒出来了。折腾了几天我决定用Vue3 UniApp来实现这套方案把之前的Web端经验直接复用过去整体落地的过程虽然有不少摩擦但结果还是值得的。这篇文章就把完整技术方案、选型思考、实现细节和踩坑实录记录下来给打算做类似全端AI助手的同学一个可以直接抄作业的参考。先交代一下项目边界目标是做一个面对用户的智能问答助手形态是沉浸式聊天界面要求支持流式输出、Markdown渲染、LaTeX公式展示、图片上传并参与多模态理解。平台覆盖微信小程序、H5以及iOS/Android App。由于产品设计希望三个端交互完全一致所以没有选择分别开发原生或Web版本而是直接押注在UniApp上。下面从架构设计开始逐步拆解整个实现过程。1. 项目背景与整体设计1.1 为什么选择Vue UniApp而不是其他方案这几乎是每个全端项目都要先回答的问题。对比过React Native、Flutter和原生三端开发最终选UniApp的核心原因有三条第一团队已经熟悉VueVue3的组合式API写起来效率很高第二UniApp对小程序、H5、App的编译支持成熟一套代码能覆盖大部分业务场景第三AI问答助手这类应用以文本交互为主不依赖重原生能力跨端性能瓶颈不明显。当然也要诚实说它的短板复杂的自定义原生组件、高性能画布渲染、独立的原生动画这些在UniApp里做起来会有些别扭。但我们的业务核心是文本流、Markdown渲染和图片上传UniApp完全能接住。如果你做的是类似ChatGPT客服这类界面型应用选UniApp是性价比很高的路线。1.2 沉浸式交互的定义与功能拆解“沉浸式”这个词说起来虚但在产品层面是可落地的。我把它拆成了四个可度量的子需求视觉统一三端微信小程序、H5、App界面看起来几乎没有差异使用同一套设计语言、动效和暗黑模式。交互连续输入、发送、流式接收、停止生成整个过程流畅不闪烁键盘弹出不遮挡输入框。内容表现力AI返回的Markdown、代码块、公式、表格都能漂亮地呈现而不是一堆乱七八糟的符号。多模态入口用户可以用图片参与对话AI能理解图片内容并基于图片继续回复。基于这些拆解后端接口需要支持文本对话和图片理解两条链路前端则重点解决跨端渲染和交互一致性。后面每个部分我都会沿着这条主线展开。1.3 技术栈清单与工程准备我使用的技术栈清单如下框架Vue 3.4 Vite UniApp最新稳定版。状态管理Pinia用来存会话列表、当前对话上下文和全局设置。CSS方案SCSS CSS变量方便做暗黑模式切换。Markdown渲染mp-html 自定义解析补丁后面细说。公式渲染H5端用KaTeX小程序端走towxml的公式组件。请求层封装uni.request流式部分使用uni.connectSocket。图片处理uni.chooseImage 本地压缩后转Base64或临时文件路径。工程准备上如果团队有Vite经验建议直接用CLI方式创建项目而不是HBuilderX的可视化创建。原因很简单CLI项目文件结构标准Webpack/Vite配置可见方便集成本地依赖和自动化流水线。当然如果你只是个人快速体验HBuilderX上手更简单但后面做多人协作和CI/CD会麻烦一些。2. 工程初始化与多端配置细节2.1 用CLI还是HBuilderX创建UniApp项目这一步属于“看起来是小事、踩起坑来能急死人”的典型环节。我推荐Vue3 Vite的CLI方式创建命令如下npx degit dcloudio/uni-preset-vue#vite-ts my-ai-assistant cd my-ai-assistant npm install npm run dev:mp-weixin # 微信小程序运行 npm run dev:h5 # H5运行 npm run dev:app # App运行CLI的好处是可以用npm管理插件比如Pinia、sass、mp-html都能在package.json里统一锁版本。HBuilderX方式更适合不熟悉命令行的朋友但它的依赖都在HBuilderX内部一旦团队协作或者换设备环境还原比较痛苦。这里有个很关键的细节UniApp的Vue3版本在App端编译时使用Vite小程序端还是复用uni自己的编译器。所以遇到一些第三方库兼容性问题时要先分清是编译期还是运行期的问题否则会白折腾很久。2.2 manifest.json里必须关注的配置项manifest.json是UniApp的全端配置文件很多人会忽略它直到真机预览出问题才回来翻。这里列几个我实际调过的关键项微信小程序appid在mp-weixin节点配置不填的话很多权限和域名配置没法生效。应用名称和logo在App端打包时需要名称不能太随意审核会查。网络超时配置socket请求一定要设置合理的超时时间流式问答经常有长时间不返回的场景。权限配置图片上传和语音输入需要声明相机、相册、麦克风权限尤其App端必须写在原生manifest里。H5路由模式默认是hash模式如果想用history模式需要后端配SEO或伪静态一般建议AI助手用hash省心。实际配置中我还在commonStyle节点做了全局主题色和导航栏颜色这样三端从视觉上就先统一了一半。2.3 页面结构、路由与状态管理页面结构我用了最常规的分层方案页面目录pages/home欢迎页和会话列表。页面目录pages/chat主聊天交互页。页面目录pages/settings设置页包含暗黑模式、字体大小、清空上下文等。组件目录components消息气泡、输入工具栏、图片预览等组件。路由方面UniApp沿用pages.json配置我用的是普通uni.navigateTo跳转没有开pages.json里的tabBar因为更希望聊天页有全屏沉浸式体验而不是被底部导航固定住。如果你需要多个平级功能模块再考虑tabBar但AI助手这种工具类应用单入口反而更聚焦。状态管理使用Pinia创建了一个useChatStore来缓存会话消息数组、当前会话ID和全局配置。特别注意一个小程序端的限制全局状态在小程序被切换到后台一段时间后可能会被回收所以长对话一定要做本地缓存我用uni.setStorageSync把最近20条消息持久化回到页面时再恢复。这部分不复杂但能显著提升使用体验。3. 聊天核心消息模型与流式响应3.1 消息数据结构和聊天页布局先定义消息模型这是整个聊天功能的地基。我用的结构是interface ChatMessage { id: string; role: user | assistant | system; type: text | image | loading; content: string; images?: string[]; // 用户上传的图片本地路径 createdAt: number; status?: pending | streaming | completed | error; }这里有几个设计点需要解释role用来区分消息归属type除了文本还加了image类型用户如果只发图片不带文字也能单独占一条消息status非常关键UI要根据它显示加载占位、流式光标、错误提示等不同状态。聊天页布局比标准IM简单因为不用考虑好友列表和群组。核心就是三个区域顶部自定义导航栏、中间可滚动消息列表、底部固定输入工具栏。输入工具栏放了一个textarea和两个按钮一个是图片上传一个是发送/停止切换。为了沉浸式效果我把消息列表的背景做成了渐变底色消息气泡刻意弱化边框用轻阴影区分整体更接近极简笔记风格而不是传统IM气泡。3.2 流式响应的几种方案对比与选型AI问答助手最核心的体验就是流式输出也就是AI说的是“打出来的”不是等很久一下全部返回。唯有多端统一我才选了WebSocket方案。先说我在调研时遇到的三个方案fetch ReadableStreamH5很好用但小程序不支持原生的fetch流式读取而且小程序没有ReadableStream。uni.request轮询简单但要不停轮询延迟高、响应慢体验差。uni.connectSocket WebSocket小程序、H5、App都支持是真正的“一套代码三端跑”虽然需要后端额外提供WebSocket接口但这是目前最合理的全端流式方案。我们后端的协议设计大致是客户端发送一个JSON消息到WebSocket包含会话ID和用户输入。后端收到后开始调用大模型将回包按token拆分成多个消息帧推送回来。每帧格式{ type: token, content: 用户问, done: false }。最后发一个{ type: done, id: xxx }帧标记结束。WebSocket连接要注意心跳包。我每30秒发一次ping后端要回pong否则自动重连。还有错误处理如果网络断掉必须在上层做断线重连并在UI给出提示否则用户会以为AI卡住了。3.3 打字机效果的实现和性能优化打字机效果是让流式输出看起来更“自然”的关键。如果每收到一个token就立刻更新整个消息内容在小程序端会出现两个问题一是频繁setData导致视图频繁刷新页面卡顿二是用户视觉上会觉得文字跳来跳去不连贯。我的做法是在状态管理中维护一个displayContent每次收到新token时马上更新真实content但视图层用一个定时器来“逐步放出”内容每100ms渲染新增的35个字符。// 简化版打字机逻辑 let timer null; function typewriter(message) { const target message.content; let len message.displayContent.length; timer setInterval(() { len Math.ceil((target.length - len) / 10); message.displayContent target.slice(0, len); if (len target.length) clearInterval(timer); }, 60); }这个方案的好处是视觉效果平滑而且原始终端频繁setData的压力被限制在了每秒10次左右。实测iPhone中端机型和安卓千元机都能稳定不掉帧。不过需要注意定时器在App端切换到后台会被挂起所以我还在页面onHide时清除定时器等回到前台再重新恢复渲染。另外一个细节消息列表一定要用scroll-view的scroll-into-view属性在流式输出过程中实时滚动到底部。直接用scrollTop在Web端没问题但在小程序端有渲染层和逻辑层通信延迟会造成“追不上文字”的滞后感。用scroll-into-view绑到最后一条消息的id实测更跟手。4. Markdown、代码高亮与LaTeX公式的全端渲染4.1 小程序里不能直接用v-html那怎么办这是全端AI助手遇到的最大坑。在H5端v-html可以直接渲染服务端下发的HTML但小程序没有DOM根本没有v-html这个概念。第三方库必须专门处理成模板字符串或自定义组件。常见的跨端Markdown方案有mp-html一个支持小程序、H5、App的富文本组件可以渲染解析后的HTML字符串内置了很多扩展节点。towxml专门为小程序设计的MD解析库支持MathJax和代码高亮但对H5支持稍弱。uni-app的rich-text组件只支持少量节点和样式不能满足复杂Markdown和公式。自己写解析器只适合极简场景渲染数学公式和表格时工程量太大。经过权衡我选择了“mp-html 自定义补丁”的方案。理由是通过mp-html渲染基础Markdown转HTML同时我用calculator扩展组件去处理公式标签这样能兼顾三端兼容性和开发效率。4.2 选型对比mp-html还是towxml为了讲清楚差异性我做了一个小表格对比方案平台支持Markdown支持LaTeX公式代码高亮自定义节点社区活跃度mp-html小程序/H5/App较弱需转HTML需扩展内置一部分强高towxml主要小程序/H5较好支持支持中中rich-text小程序/H5极弱不支持不支持无官方我为什么没无脑选towxml虽然它公式支持好但它在App端偶尔会出现样式错乱而且对最新Vue3的响应式处理不太友好。mp-html更像一个基础组件灵活性更高我可以控制整个渲染管线的每一个环节。具体思路是后端返回Markdown源码前端用markdown-it统一转成HTML字符串再对这个HTML做两层处理。第一层把$$...$$和$...$公式提取出来替换成自定义标签formula内容/formula第二层用mp-html渲染时对formula标签进行自定义插件处理。这样公式既能正确显示又不影响其余Markdown渲染。4.3 公式渲染的兼容方案与H5/小程序差异公式渲染是很多AI问答项目里最容易被低估的部分。如果AI返回的内容涉及数学、物理或金融公式普通文本渲染会完全乱套。在H5端我直接用KaTeX的DOM渲染性能很好渲染速度快。但到了小程序端问题就来了小程序不能动态执行JavaScript库去操作DOM所以我用自定义组件模拟了一个公式渲染器后端在返回Markdown时如果识别到公式直接返回一个svg图片地址或base64图片前端放进image标签渲染。这个“把公式变成图片”的方案在小程序端非常稳定虽然不如图形化渲染灵活但胜在兼容性强而且对用户来说视觉上很清晰。App端我也沿用这套思路统一通过mp-html的formula标签解析成图片URL。如果你有更极致的公式排版需求可以考虑在小程序端集成wx-towxml的公式功能但要做好样式调试的准备。我的经验是先保证能显示再考虑效果公式图片是性价比最高的起步方案。4.4 自定义样式让渲染结果更有“沉浸感”Markdown渲染成HTML后默认样式往往不美观。沉浸式体验需要一套精心调过的排版风格。我定义了一套全局CSS变量包括正文颜色、背景色、链接色、代码块配色、表格边框色等。对于代码块我用了highlight.js提供的深色主题“Atom One Dark”并调整了字体和圆角。对于表格加了横向滚动容器防止小屏溢出。对于引用块统一使用左侧竖线加浅色背景开起来更柔和。还有一个容易被忽略的是“内容里的图片”。AI有时会在回复中插入图片如果直接用html渲染小程序里的图片容易被压缩变形。我通过mp-html的img扩展节点再配合modewidthFix来控制图片宽度自适应。如果你要做到极致的沉浸感可以对AI回复里的标签进行白名单过滤比如禁止script、iframe这类危险标签避免XSS问题。在小程序端mp-html默认会做安全过滤但在H5端需要自己额外加一层DOMPurify处理。这段折腾下来我的体会是渲染层是整个项目最耗时的地方不要上来就追求全功能先把基础Markdown跑通再逐步叠加公式、代码高亮和自定义组件迭代推进会稳很多。5. 多模态交互图片上传与视觉理解5.1 交互入口拍照、相册、拖拽多模态交互是AI助手提升体验的重要方式用户可能想直接拍一道数学题、一张产品截图或者一份手写笔记。UniApp封装了uni.chooseImage可以同时支持拍照和相册选择uni.chooseImage({ count: 1, sizeType: [compressed, original], sourceType: [camera, album], success: (res) { const tempFilePath res.tempFilePaths[0]; // 进入下一步处理 } });在H5端除了点击按钮上传我还额外做了一个拖拽上传的交互允许用户直接把图片拖进聊天窗口。这个体验在不熟悉的用户眼里很加分。At App和小程序端只能点按钮触发不过已经是足够了。还需要注意iOS小程序和App对相册权限的提示策略不太一样如果用户首次拒绝要引导去设置页打开权限。UniApp提供了uni.authorize可以预检权限建议在进入聊天页时就检查一次。5.2 图片压缩和上传封装图片不能无脑传原图尤其小程序包体有限制网络传输也要考虑流量。UniApp的uni.compressImage可以把图片压缩到指定质量或尺寸。我的压缩策略是宽高最大不超过1280px超了就等比缩放。质量压缩到0.7对于视觉问答已经足够清晰。压缩后体积超过1MB时再降一档质量重压一次。支持多张图片时串行压缩避免内存暴涨。压缩完成后我使用uni.uploadFile上传到自己的服务器。这里有个并行经验不要直接拿临时文件路径去调用AI接口因为临时路径在App端是原生临时目录在H5端又是blob URL直接转发给后端迟早会出兼容性问题。稳妥做法是先上传到自己的OSS或对象存储拿到一个稳定的URL再把这个URL发送给AI服务。5.3 把图片和文本组装成多模态消息上传完成后消息结构里会带上图片URL数组。发送给后端的JSON大致是{ sessionId: abc-123, messages: [ { role: user, content: [ { type: text, text: 帮我看看这道题怎么解 }, { type: image_url, image_url: { url: https://cdn.example.com/xxx.jpg } } ] } ] }这个结构参考了目前主流多模态大模型的输入格式。后端收到后根据是否有图片字段决定走多模态模型还是纯文本模型然后在回复中直接说“根据您发的图片我的分析是...”。前端展示时用户消息气泡里如果包含图片我会在小气泡缩略图基础上再配一个点击查看原图的大图预览。用uni.previewImage可以在三端统一起到预览效果不用自己写弹窗。多模态交互目前最容易被忽略的问题是历史消息中的图片处理。如果你把整段图片URL都存进上下文再传给AI很容易让token量暴涨、响应变慢。因此我在发送前会做一轮优化只保留当前消息和最近一轮相关图片之前的图片只保留“用户发过图片”的标记不携带全量URL。这个策略在真实使用时能显著降低接口费用和延迟。6. 体验优化与沉浸式细节打磨6.1 输入框自适应与键盘安全区聊天输入框做得不好会直接摧毁整个聊天体验。我用了textarea做输入开启了auto-height属性让它随内容自适应高度最大到6行。同时给textarea加上adjust-positionfalse避免键盘弹出时页面整体上移。键盘弹出的遮挡问题是重灾区。在App和小程序端输入框底部需要加上安全区高度通常用uni.getSystemInfoSync().safeArea来获取。H5端移动浏览器里用visualViewport监听键盘高度。我还加了一个“按住说话”的语音扩展入口这个稍后讲。输入工具栏底部我固定一行按钮加号图片、输入框、发送/停止按钮。按钮和输入框颜色都用了主题色渐变需要和整体视觉保持统一。6.2 自动滚动、消息缓存与虚拟列表聊天页的消息列表在长时间对话后可能超过100条直接用scroll-view一个个渲染也会卡。我做了几个优化消息按时间分批渲染每批最多30条滚动到底部时自动加载更早的消息。使用scroll-view的enhanced属性和show-scrollbar为false提升小程序滚动性能。每一条消息组件内部用memo优化避免无关消息更新导致整体重渲染。搜索场景下限制历史加载条数以降低首屏渲染压力。对于缓存我是这样做的每次会话结束时将最近20条消息压缩后写入storage进入页面先读缓存再根据服务端数据做增量更新。如果服务端数据异常至少用户还能看到历史对话不会白屏。这个问题在弱网环境下尤其重要。自动滚动我提过用scroll-into-view但这里有个细节新消息进来时只有当用户停在底部时才自动滚动到底部如果用户正在往上翻历史就别强制拉下去。这个判断可以通过监听scroll事件计算是否接近底部来实现。6.3 暗黑模式和极简UI设计沉浸式设计的另一个关键是暗黑模式。AI问答场景通常发生在夜间或弱光环境下一个柔和的暗黑主题能极大提升使用舒适度。我用了CSS变量来管理主题色:root { --bg-primary: #f7f8fa; --bg-chat: #eef1f5; --text-primary: #1a1d21; --text-secondary: #6b7280; --accent: #4f6ef7; } .dark { --bg-primary: #16181d; --bg-chat: #0f1115; --text-primary: #e5e7eb; --text-secondary: #9ca3af; --accent: #6b8afe; }在小程序端 uni-app的页面根节点不是html所以dark类不能直接挂在html上。我是在根组件外层套一个view用它绑定class这样所有子组件通过CSS变量自动切换主题色不用每个组件单独处理。极简UI方面我去掉了传统聊天的气泡尾巴改用细圆角矩形头像从圆形改成了小巧的圆角方形更贴近工具属性。技术上的效果就是每屏视觉噪音更少用户会更关注AI回复本身。6.4 语音扩展让问答助手“能听会说”虽然不是最初的硬性需求但加上语音可以让整体体验更称得上“沉浸式”。UniApp内置了录音和音频播放能力。我用uni.getRecorderManager()录制用户语音录完后调用语音识别接口转成文字再塞入输入框让用户确认后发送。这样可以避免语音识别错词直接发给AI。TTS语音播放则是在AI回复流输出完成后调用uni.createInnerAudioContext()播放服务端返回的合成音频。这个扩展最麻烦的是权限和格式。App端需要声明麦克风权限H5端浏览器必须用HTTPS才能录音小程序的录音格式在iOS上是m4a、在安卓上是mp3后端解析时需要兼容不同编码格式。如果团队没有语音处理经验建议先做文本图片语音放到第二阶段。7. 打包上线与常见问题排查实录7.1 微信小程序打包与域名配置微信小程序是目前流量最大、审核也相对严格的端。在manifest.json里填好appid后需要到微信公众平台配置服务器域名。如果后端API是https://api.example.comWebSocket是wss://api.example.com都要加入request和socket合法域名否则真机调试直接失败。小程序包体也有体积限制主包不能超过2MB。我会把依赖进行分包处理聊天页是核心放主包设置和引导页放分包。如果使用了mp-html代码量会增加一些注意tree-shaking尽量只引入需要的扩展组件。上传到微信后台时还需要做代码依赖分析。如果发现uniapp自身带的某些模块用不到可以在manifest的optimization里开启subpackage和treeShaking能明显缩小包体。实测开启后主包从1.8MB降到1.3MB减少了不少。7.2 H5部署的跨域与路由模式H5端部署相对简单但跨域是绕不开的题。开发环境我使用Vite的proxy代理将/api转发到后端服务。生产环境如果后端支持CORS那么前端可以直接请求如果不支持就得在Nginx层配置反向代理。做法是在Nginx的location /指向前端静态文件location /api反代到后端服务。这样可以避免暴露服务端真实地址同时解决跨域问题。路由模式我在前面提过H5端如果启用history模式需要Nginx把所有非静态资源请求都rewrite到index.html否则刷新页面会404。如果不想折腾直接用hash模式最稳妥。AI助手这类单页工具hash模式的URL虽然多一个#但几乎不影响SEO完全可以接受。7.3 App云打包痛点App端我用了UniApp的云打包功能不需要本地的Android Studio和Xcode。云打包前需要准备好Android证书或iOS证书iOS还需要苹果开发者账号。证书和描述文件的配置一定要认真对待不然上传App Store时会卡得很久。云打包的坑主要集中在原生权限和插件冲突上。比如同时使用摄像头、麦克风、相册权限时如果配置不当在Android 13上可能会出现运行时权限崩溃。我建议在打包前先完整测试三轮H5、微信开发者工具、App自定义调试基座。尤其自定义基座要用起来它能在不正式打包的情况下调试原生API。另外App端推送、版本更新等能力需要额外模块这会增加包体积。AI问答助手可以暂时不做推送把精力放在核心体验上。7.4 高频踩坑问题速查表为了帮大家避坑我把整个开发周期里遇到的高频问题整理成了表格方便对照问题现象原因分析解决方案小程序中AI回复的HTML不渲染小程序没有DOMv-html不可用使用mp-html组件替换v-htmlMarkdown表格在小屏端溢出表格默认宽度过大给表格包裹横向滚动容器公式渲染乱码或显示源码后端返回的LaTeX被转义统一转成HTML标签或图片渲染WebSocket连接App端经常断缺少心跳包每30秒发送一次ping断线自动重连上传图片后其他端无法访问在不同端拿到了本地临时路径先上传到服务器统一使用CDN URL键盘弹起遮挡输入框未适配安全区监听safeAreatextarea设置adjust-position长列表滚动卡顿一次渲染过多消息分批渲染 虚拟滚动暗黑模式切换后子组件不更新变量挂载位置不对根节点挂载变量子组件全部继承App云打包后相机权限崩溃原生权限声明不完整检查manifest的本机权限配置H5刷新404history模式未配伪静态改用hash模式或配置Nginx rewrite这些坑有些看起来很小但每个都能消耗半天时间。希望这个表格能帮你少走弯路。在我个人实操过程中最深的体验是跨端AI问答助手的难度不在“调用大模型”而在“渲染和交互的一致性”。你需要在三端做大量兼容适配才能让用户感受不到平台差异。如果你在做一个垂直领域的AI问答产品可以把Markdown渲染和流式输出作为核心优先实现多模态放在第二优先级。我最初的版本先做到了文本流式就足够验证业务流程了后面再陆续加上公式渲染和图片理解。最后再分享一个小技巧在小程序端用mp-html渲染Markdown时最好把后端返回的Markdown字符串先做一次“清洗”过滤掉可能包含的脚本标签同时把代码块的换行符处理好。很多诡异的样式错乱根源都出在特殊字符和嵌套标签上。抓住这几个关键点整个项目的稳定性会提升一个档次。