技术面应答稿2
面试官您好,我是韦业,有三年前端开发经验,目前负责智慧停车业务的前端开发。我参与搭建了覆盖车主端 H5、小程序和运营管理后台的平台,负责停车缴费、欠费追缴、订单和设备运维等核心模块,推动停车欠费查询、在线缴费和运营跟进的业务闭环落地。此外,我是 Wot UI 组件库核心成员,主导开发过高扩展性的瀑布流组件,也参与了 Wot Starter 脚手架建设。我具备 C 端小程序、H5 及 B 端复杂业务开发经验,与贵公司的岗位场景比较匹配,期待进一步交流。谢谢。
一、开源贡献
1. 介绍一下你在 Wot UI 中的角色和贡献
我是 Wot UI 的核心成员之一。Wot UI 是一个面向 UniApp 的移动端组件库,目前有 2.3k+ Star,今年还获得了 Gitee 平台年度开源项目第一名。
我重点主导的是高拓展性瀑布流组件。这个组件在 UniApp 跨端下的挑战很大——各平台渲染机制和 DOM API 都不一致,而瀑布流又依赖精确的高度测量和异步加载。
我设计了一套基于队列驱动的增量排版引擎,核心机制是:
第一,排版队列 + 最短列优先算法。子项挂载后不立即计算位置,而是进入一个等待队列,由排版控制器按序逐个消费。每次从队列头部取出一个加载完成的项目,找到当前高度最短的那一列追加到它后面。做到「加载一个、排版一个」,而不是等全部加载完再统一排,首屏速度更快。
第二,异步加载编排。子项挂载时注册到父组件,通过 Vue 的 watch + Promise 等待它的内容加载完毕、拿到 DOM 精确高度后才计算位置。如果超时,有最大等待时长兜底,使用默认高度继续排,不会阻塞整列。所有正在等待的 watcher 统一管理,页面切到后台时批量清理,防止内存泄漏。
第三,四级错误处理策略。暴露四种异常处理模式:失败即结束,用默认高度兜底、失败后显示占位图、自动重试指定次数、三层兜底——先重试、再占位图、最后文字兜底。使用者按业务场景选,不需要自己写容错逻辑。
第四,完整的布局生命周期。支持 reflow(列数/间距变化后保留数据重新计算位置,防抖 16ms)和 reset(清空所有数据准备接收新列表)。支持通过 order 属性在任意位置插入项目,插入后自动全量重排。页面切后台时中断排版并清理资源,切回前台时自动恢复。
另外我也参与了代码 Review 和 Issue 维护,帮助社区 contributor 更好地合入代码。
2. Wot Starter 脚手架做了什么?
Wot Starter 是 Wot UI 的官方项目脚手架。我参与建设时,重点做的是编写完整的最佳实践示例代码和文档——因为 UniApp 项目的配置链路比较长(Vite 插件、主题定制、按需加载、跨端适配等),很多新用户第一步就卡住了。我把这些典型场景做成开箱即用的模板工程,配合文档说明,用户 clone 下来就能跑起一个带真实业务结构的项目。目前项目 1k+ Star,社区反馈说确实帮他们省了不少踩坑时间。
二、工作经历总览
3. 你在稳安特科技担任前端负责人,具体负责什么?
我在稳安特科技担任前端负责人,负责智慧停车和 AI 方向核心项目的前端架构设计与落地。具体来说有几块:
第一,主导了停车生态核心产品的建设,打通了停车缴费、商户转化、欠费追缴到清结算的完整业务闭环。
第二,负责欠费追缴体系的产品化落地,推动追缴率提升了 18%,累计追缴欠费超过 200 万元。
第三,统一了公司多个项目的技术栈和工程体系,沉淀了一套内部脚手架,让新业务能快速启动。
第四,负责了企业运维知识库 RAG 平台的方案设计和研发,这是 AI 能力在内部工具中的工程化落地尝试。
整体来说,我既要负责技术选型和架构方案,也要参与核心模块的编码交付,同时协调团队资源。
3.1 最难的点
停车综合运营平台是我现在负责的核心项目,包含 PC 运营端、公众号 H5 和微信小程序,主要覆盖停车运营、欠费追缴、设备运维和车主缴费等场景。我在里面主要负责前端架构设计、核心模块开发和工程化建设,像运营工作台、设备 GIS 监控、道路泊位远程值守、车主缴费链路这些都是我重点参与的模块。 这个项目最难的点我觉得有三个:一是业务链路长,涉及订单、设备、泊位、支付、回传、运营处理多个环节;二是多端并存,PC、H5、小程序既要复用又要处理差异;三是设备运维类页面数据量大、实时性高,对性能要求很高。针对这些问题,我做了配置化工作台、按需渲染和移动端性能优化,也把追缴和支付链路做了统一封装。最后平台支撑了 18 个车场点位、3000 多个泊位、2800 多台设备统一管理,追缴率提升了 18% 以上。
三、停车综合运营平台(核心项目)
4. 停车运营平台整体架构是什么样的?服务哪些用户?
这是一个典型的多端统一平台。PC 端面向停车运营管理人员,覆盖车场管理、订单收费、客服处理、财务结算、设备运维和经营分析这些日常运营工作。移动客户端面向车主,以公众号 H5 和微信小程序为载体,提供停车缴费、欠费追缴、附近车场查询、停车券、商户优惠券等功能。
目前支撑了 18 个车场/道路点位、3000+ 道路泊位和 2800+ 台设备的统一运营管理。技术栈以 Vue 3 + UniApp 为核心,配合 Element Plus 做 PC 端、Wot UI 做移动端,可视化部分用 ECharts 和高德 SDK。
5. 运营工作台的可配置能力是怎么做的?
运营工作台首页涉及不同岗位的差异化需求——运营人员关注数据看板和待办,客服关注工单,财务关注结算,运维关注设备告警。如果每个岗位单独开发首页,改版和维护成本很高。
我设计了一套 Widget 编排引擎:把数据看板、待办列表、快捷入口等模块抽象为可注册卡片,每张卡片用 JSON Schema 描述它的布局、数据源和权限要求。运营人员通过后台配置,按岗位角色自由组合卡片,不需要前端发版。
底层结合 RBAC 做权限过滤,4 类岗位各有默认配置模板,也支持个性化调整。覆盖了 10+ 个高频入口,后续新增卡片只需要注册新的 Widget,首页框架不需要改动。
6. 智能渲染容器是怎么实现的?解决了什么问题?
这个需求的背景是:有些页面有大量数据看板、实时图表和长列表,全部同时渲染时主线程负载很高,低端设备卡顿明显。
我把它做成了一个通用懒渲染容器组件,基于 IntersectionObserver 监听可见性。核心思路分三段:
组件首次进入视口时渲染真实内容并缓存 VNode。离开视口时不销毁组件——而是劫持组件实例的 render 函数,让它直接返回当前的 subTree,跳过之后的 patch 和 update,相当于把组件"冻住"。同时记录冻结期间有没有触发过更新请求。重新进入视口时恢复原始 render,如果冻结期间有过更新请求就立即执行一次 component.update(),把积压的状态同步回来。
这个方案比 v-if 好在哪里?v-if 离开视口会销毁组件实例、丢失状态,回来要重建;我这个只会暂停更新,状态保持、不反复走 mount/unmount 生命周期,回来时恢复代价也低很多。
7. 设备 GIS 监控和道路泊位远程值守是怎么做的?
设备 GIS 监控这部分,我们是基于高德地图 SDK 做的。把地磁、ETC、相机、手持终端这些设备按经纬度标注在地图上,支持按设备类型、在线状态、告警级别做筛选。点击设备标记可以查看设备详情和最近的告警记录,实现 2800+ 台设备的集中监控。
道路泊位远程值守的思路是:每个泊位对应的设备定时抓拍上传图片,前端通过轮询接口获取泊位状态和抓拍图片。我们构建了一个按时间轴回溯的图片稽核界面,人工可以远程查看某个泊位在某个时间段的照片,判断是否存在异常占位、设备故障等。这样一来,以前需要现场巡检 30 分钟才能发现的异常,现在远程 5 分钟内就能定位和处理。
7. 设备监控显示渲染性能优化
分析瓶颈在哪,是数据层面还是渲染层面? 因为这类场景真正的瓶颈通常不只是数据量,还有大量节点同时渲染、频繁更新和地图联动带来的主线程压力。 在我的项目里,优化一般会分多层优化。
从前端到网络协议到服务端到数据库
展示层不承接全量 | 网络传输尽量降本 | 缓存体系分层建设 | 服务端做聚合与解耦 | 数据库针对查询模型优化 | 实时链路从轮询升级
第一层是渲染层优化。我不会把 2800 台设备一次性按最重的形态全部渲染出来,而是先做视口裁剪和分级展示。比如只渲染当前可见区域的数据,缩放层级较低时先用聚合点,放大后再展开成具体设备。设备点位的展示也尽量轻量化,能用地图原生能力或更轻的标记就不用复杂 DOM,避免大量自定义节点挂在页面上。
第二层是数据层优化。首次加载时会拉基础数据,但后续不会每次全量刷新,而是按当前视口范围、筛选条件去增量获取。更新策略上我会把高频变化和低频变化的数据拆开,比如位置信息、在线状态、告警数量分开处理,避免一次轮询导致整棵视图重算。对变化不频繁的数据会做本地缓存或内存缓存,减少重复请求和重复计算。
第三层是交互和更新策略优化。大屏里很多卡顿不是首次渲染,而是筛选、缩放、拖拽地图时反复触发重绘。所以我会对筛选和地图事件做节流、防抖,避免短时间内连续触发请求和重新渲染。对于告警闪烁、状态变化这类局部更新,我会尽量做局部刷新,而不是整批设备重新挂载。
8. 停车场地图与寻车场服务是怎么做的?
这些车场是我们运营平台自己的数据,后端接口返回车场列表(含坐标、车位余量、收费标准这些),前端只负责在地图上标注展示。
H5 端:AMapLoader.load() 加载完 JSAPI 后 new AMap.Map('container', {...}) 初始化地图。调后端接口拿到车场列表,遍历 new AMap.Marker({position, content}) 创建标注点,map.add(marker) 添加到地图上。点击标注弹窗展示详情,导航按钮调用 AMap.Driving.search() 规划路线。
小程序端:渲染靠微信原生 map 经纬度 markers="" 组件。同样调后端接口取数据,把返回的车场列表映射成 markers 数组(latitude、longitude、iconPath、width、height),设置 data 让模板渲染。点击标注用 map bindmarkertap="onMarkerTap",导航用 wx.openLocation 调起微信内置导航。
封装:渲染层(地图初始化、标注交互)两端 SDK 完全不同,各自独立实现。数据层抽了一个 parkingMapService,车场查询和路线规划封装成统一 async 接口,内部用条件编译判断 H5 还是小程序,切换底层 SDK 调用,上层页面代码只调 service 不感知环境。
9. 微信支付流程是怎么对接的?
完整链路分四步:
第一步,用户点击下单后,小程序调 wx.login() 获取临时 code,传给后端去换 openId。 第二步,前端把商品信息加上 openId 一起发给购买接口,后端生成订单后调微信支付统一下单接口拿到 prepay_id,同时根据微信 SDK 规则生成预付款签名,一并返回给前端。 第三步,前端拿到 prepay_id 和签名后调 wx.requestPayment(),微信内部唤醒支付窗口,用户输密码或指纹完成支付。 第四步,微信后台处理结果会异步通知两端——同时推送给小程序的回调和后端配置的支付通知地址,两边各自更新订单状态。这样即使前端切走页面,后端也能收到结果,防止丢单。
支付防重复缴费,核心一定是后端通过幂等设计来保证,前端更多是做体验和兜底,两边配合才能把风险降到最低。
首先是后端的核心幂等:每一笔支付请求都会生成一个业务唯一ID(比如 payment_id),后端会用数据库唯一索引或者 Redis 锁来保证同一笔支付只会被处理一次;
支付流水表里会记录状态机:INIT → PAYING → PAID/FAILED,同一个订单只允许成功一次;支付本身也要求 out_trade_no 唯一,回调时后端先做幂等校验,再更新订单状态。
前端在这一层主要做三件事:UI 防重:支付按钮点击后立即置灰 + loading,防止用户手抖连点;
请求防重:对同一个订单的支付请求做短时锁(比如 5 秒内不允许重复发请求),避免弱网下的重复提交;
结果兜底:支付完成后强制走「查询订单状态」的逻辑,而不是直接信任前端跳转参数,避免用户重复发起支付。
极端场景兜底:如果出现网络闪断、页面关闭、用户重复扫码等情况,后端会通过订单状态判断是否允许再次发起支付;前端也会提示用户『该订单已支付,请勿重复操作』。
所以整体策略是:后端做绝对安全,前端做体验防重,两边一起兜底
10. 追缴率提升 18%+ 是怎么做到的?
追缴率的提升是技术和业务配合的结果。从技术角度看,我主要做了几件事:
第一,打通了追缴全流程的线上化闭环——以前欠费查询、缴费、处理回传、运营跟进是割裂的,车主和运营人员之间需要电话沟通,效率很低。我把这个流程全部搬到了线上,车主在公众号或小程序上就能查看欠费记录并在线缴费。
第二,封装了可嵌入的追缴模块——因为我们的停车场涉及多个商户,每个商户的计费模型和数据权限都不一样。我设计了一套统一的参数传递和结果回调机制,把这个追缴模块做成了可以快速接入不同商户端的组件,在公众号 H5 和小程序上都能复用。
第三,在运营端建设了追缴看板和跟进功能,运营人员可以实时看到哪些欠费单已经推送、哪些已缴费、哪些需要人工介入,形成了完整的运营闭环。
最终追缴率提升了 18% 以上,累计追缴欠费超过 200 万元。
11. 性能优化从 4.2s 优化到 1.5s 以内,具体做了哪些?
我是按先发现、再分析、后逐步优化的流程做的。
发现问题:通过 Lighthouse 加 Performance API 做生产采样,发现首屏加载平均 4.2s,FCP 和 LCP 都偏高。
分析瓶颈:用 Network panel 配合 Bundle Analyzer,定位到三个核心问题——一是静态资源 6.8M 没压缩;二是路由没做懒加载,首屏载入了大量非首屏模块;三是图片全部同步加载,首屏请求数过多。
逐步优化:优先做收益最高的——先上 Gzip 和 Brotli 压缩,资源降到 4.1M;再做路由级代码分割,按需加载;然后加图片懒加载。配套上了 CDN 和强缓存,二次访问直接命中缓存。三步下来首屏稳定在 1.5s 以内。
首屏之外也做了虚拟滚动、智能渲染容器和图片预加载,这些不贡献首屏指标,但整体体感提升明显。
12. 说说这个图片预加载插件是怎么设计的
这是一个 Vite 构建时插件,可以扫描指定目录下的图片文件,自动注入预加载。
核心是路径映射——开发环境直接使用源码路径,生产环境通过构建产物的 originalFileName 匹配到带 hash 的最终路径,还做了字节级内容比对作为兜底,保证路径解析可靠。
我做了两种策略:一是注入 <link rel="preload" as="image"> 标签到 head,让浏览器自己调度优先级;二是注入一段内联 script,用 new Image() 配合并发控制参数做运行时预热,避免一次性抢占太多浏览器连接。
配置上只需要指定要预加载的图片目录,其他路径映射逻辑插件内部自动处理。
13. 全局反馈体系解决了 UniApp 的什么问题?
UniApp 有一个典型的痛点:在一些非 Vue 上下文中——比如请求拦截器、路由守卫、支付回调——你没法直接调用 UI 组件,因为没有组件实例。但恰恰是这些场景最需要 toast、loading 这样的反馈。
我的方案是:基于 Pinia 维护全局 Toast 状态,在布局容器中注入 GlobalToast 组件监听状态变化。任何地方——无论是 Vue 组件还是纯 JS 逻辑——只需要调用 toast.show() 更新 Pinia 状态,布局层的 GlobalToast 就能响应式地展示出来。
这样我们在请求封装里统一处理网络错误提示、在路由拦截里处理登录过期提示、在支付回调里处理支付结果展示,整个应用的异常反馈体验就统一了。
四、多模态 RAG 设备运维知识库
14. 介绍一下你做的 RAG 项目
这个项目的背景是:停车设备的现场运维人员查手册、找故障方案平均要花 40 分钟,尤其是故障照片这种图片信息以前根本没法检索。
我们做的多模态 RAG 系统核心要解决几个问题:图片搜不了、单路检索不稳、知识不能累积。具体来说:
多模态知识建模——图文分开处理。图片走多模态模型做向量化,支持上传故障照片召回相似案例;纯文本用独立文本向量索引,兼顾效果和成本。
混合检索——构建了稠密向量、稀疏检索和元数据过滤三路召回,用加权融合策略合并结果,再用 qwen3-rerank 做精排重排序,保证高相关片段进入 LLM 上下文窗口。
上下文工程——Redis 维护短期会话,异步任务把有价值的交互摘要沉淀为长期记忆。检索层设 distance 阈值过滤低质量结果,避免无关上下文污染生成链路。
评估闭环——参考 RAGAS 设计离线评测,检索侧看 Context Precision/Recall,生成侧看 Answer Relevancy/Faithfulness。每次迭代跑指标对比,形成量化优化闭环。
最终完成 1100 份文档入库,故障定位从 40 分钟缩到 10 分钟,自助解决率提升 68%。
15. 多模态建模怎么做的?
这块我们其实是分几个阶段做的。第一步不是解析,而是先做数据清洗,因为原始资料里会有很多噪声,比如重复文档、旧版本手册、空白页、页眉页脚干扰,甚至有些附件本身就没什么检索价值。如果这些内容不先清掉,后面召回结果会很杂,所以我们会先做去重、去噪、版本筛选和基础规范化。
第二步才是文档分类和解析。我们会先判断它是纯文本文档、扫描件,还是图文混排的设备手册和故障图解。纯文本内容就走常规文本解析,扫描件先做 OCR,像故障照片、结构图、接线图、截图标注这类视觉信息比较重的内容,就走多模态解析。解析过程中我们会尽量保留原文结构,比如标题层级、段落边界、图片和图注关系、页码这些信息,而不是简单按页拆。
第三步是分流建模。像 FAQ、日志、纯说明文字这类内容,我们走独立的文本向量索引,成本低、效果也稳定;像故障图解、结构示意、现场照片这类强依赖图片理解的内容,才进入多模态链路,图片做视觉向量化,同时和对应的文字说明、设备型号、故障类型、处理步骤绑定成统一知识单元。
第四步每个知识单元都会带上文档 ID、页码、章节、标题、设备型号、故障类型这些 metadata。这样后面不管是文字提问还是图片检索,系统召回的都不是孤立片段,而是带上下文、能回溯原文位置的完整知识块。这也是我们做多模态 RAG 时比较看重的一点,就是不仅要能检索到,还要能定位、能引用、能校验。
到了agent的层,不建议把整份PDF塞进上下文,这样不仅浪费token,而且很容易让模型抓不住重点,那更好的方式是把文档的不同能力拆成工具 tools,让 agent 按需调用,比如search PDF,负责检索相关片段。Read page负责读取指定页内容,extract table 负责抽取表格,alice chart 负责理解图表,code source负责返回原文引用和页码。
后面检索时,如果用户是文字提问,就优先走纯文本索引;如果上传故障照片,就可以直接召回相似案例,并把相关说明和处理步骤一起带出来。整体上就是先把数据洗干净,再按内容类型选择纯文本还是多模态索引,这样能在效果和成本之间做比较好的平衡。
16. 混合检索的三路召回具体是怎么做的?
我们这个项目里的混合检索,本质上是多路召回。因为设备运维场景的问题比较复杂,单靠一种检索方式效果不稳定。比如向量检索擅长理解语义,适合处理口语化描述和同义表达;但像设备型号、错误码、专业术语这种内容,关键词检索往往更准。所以我们最后做的是稠密向量检索、稀疏检索和元数据过滤三路结合。
具体做法上,第一路是向量检索。我们把文档切成 chunk 之后做 embedding,用户问题也转成向量,到向量库里召回语义相近的片段。第二路是稀疏检索,也就是全文检索或 BM25,主要保设备型号、故障码、术语这些精确命中。第三路是元数据过滤,我们在入库时给文档片段打上设备类型、型号、文档类别、故障标签这些信息,检索时如果 query 里能识别出这些条件,就先做过滤,减少无关内容进来。
两路结果回来之后,我们没有直接拿原始分数做加权,而是用了 RRF,也就是倒数排名融合。原因是向量相似度和 BM25 分数不是一个量纲,直接相加不太稳定。RRF 的思路是看一个结果在不同召回列表里的排名,如果一个片段在向量检索里排得靠前,在 BM25 里也排得靠前,那它最终综合排序就会更高。这样可以比较稳地把语义相关和关键词精确命中结合起来。RRF 融合完之后,我们再把候选结果交给 rerank 模型做精排,因为前面的召回和融合更偏"尽量别漏",而 rerank 更适合做细粒度相关性判断,最后再选出最适合放进上下文的几个片段给大模型生成答案。
所以整个链路可以理解成:向量检索负责找语义,BM25 负责保精确命中,RRF 负责把多路结果稳健融合,rerank 负责最后精排。这样整体效果会比单一路召回稳定很多,尤其适合设备型号、错误码和自然语言故障描述混在一起的场景。
17. RAG 评估体系是怎么搭建的?具体用什么指标?
RAG 评估我会分成离线和线上两层。离线先建评测集,每条样本不只有 query,还会带标准答案、期望召回片段和场景标签。检索层主要看 Hit@K、Context Precision、Context Recall,生成层主要看 Answer Relevancy 和 Faithfulness。这样可以区分到底是没召回来,还是模型答偏了、编造了。上线以后我会重点看踩率、追问率、空回答率和会话解决率。整体闭环就是离线测评发现问题,调整 chunk、召回、rerank 和 Prompt,小流量上线后看线上反馈,再把差案例回灌到离线测试集继续优化。
我们当时是按离线评估 + 线上观测两层来搭 RAG 评估体系的,因为 RAG 不是只看模型答得像不像,还要拆开看究竟是检索有问题,还是生成有问题。
先说离线。第一步是先建一套评测集,不是只收集问题,而是把每条样本拆成 query + 标准答案 + 期望召回文档/片段 + 场景标签。场景标签一般会标设备型号查询、故障定位、操作步骤、图片相关问题这些,这样后面指标波动时能知道到底是哪一类问题退化了。
第二步是分层评。检索层我会重点看 Hit@K、Context Precision、Context Recall。
- Hit@K:前 K 个召回结果里有没有命中标准答案所在片段。这个指标最直观,适合看召回链路有没有把正确资料捞出来。
- Context Recall 上下文召回率:目标值是 > 0.7,低了说明检索层没召回到正确内容,优化方向是换更强的 Embedding 模型、调整 Chunking 策略、或者加多路召回来补充覆盖面。
- Context Precision 上下文精确率: 目标值是 > 0.8。低了,说明检索召回了太多噪音,相关内容是找到了,但不相关的内容也混进来了,把 LLM 的注意力稀释掉了,优化方向是加强 Rerank 模型、调低最终送给 LLM 的 chunk 数量。
第三步是看生成层,这里我主要参考 RAGAS 的两个指标:
- Faithfulness 忠实度:目标值是 > 0.8。低了,说明 LLM 在编造,幻觉问题多,你回答里说的东西在参考资料里找不到依据,优化方向是加强 Prompt 约束、引入引用核查、或者做检索质量门控,防止低质量上下文进入生成阶段。
- Answer Relevancy 答案相关性:目标值是 > 0.8。低了,说明答案跑题了,没有聚焦在用户问的问题上,通常是 Prompt 的指令不够明确,告诉 LLM「请严格回答问题本身,不要展开无关内容」往往就能改善。
线上这块,我认为才是最终验收标准。离线评估更多是为了快速迭代、快速定位问题,上线后还是要看真实用户反馈。我会重点看四类线上指标:
- 踩率:最直接的负反馈,能反映回答质量是否让用户不满意。
- 追问率:如果用户紧接着继续追问,尤其是重复追问同一个问题,通常说明上一次回答没有真正解决问题。
- 空回答率:系统直接说"不知道"或者没有给出有效结论的比例,这个指标高通常意味着知识库覆盖不足,或者召回阈值设得太保守。
- 会话解决率:一次会话里用户的问题有没有真正被解决,这是最接近业务价值的指标。
所以整个评估闭环其实是:离线测评发现问题 -> 调整 chunk、召回策略、rerank、Prompt -> 小流量上线 -> 看线上踩率和追问率 -> 把线上差案例回灌到离线测试集继续复现。这样优化才是闭环的。
但需要注意,离线指标好,不代表线上一定好;线上好,也不代表离线体系没问题。 常见原因是离线测试集覆盖不到真实用户分布,或者某个离线指标优化过头了,比如为了提升 Faithfulness 把回答限制得太死,结果模型虽然更保守,但用户会觉得不好用。所以离线和线上一定要定期交叉校验,必要时更新测试集和指标权重。
如果是混合检索场景,我还会按向量检索、BM25、RRF 融合、rerank 后几个阶段分别看 Hit@K,这样能快速定位问题到底出在召回、融合还是精排。 有时候我还会补一个人工抽检,因为像故障处理这类场景,纯自动指标不一定能完全覆盖"回答是否真的可执行"。
18. 上下文工程中短期会话和长期记忆是怎么设计的?
我的做法一般是把上下文工程拆成两层:短期会话上下文和长期记忆。 短期这层主要放在 Redis 里,维护当前会话的热状态,比如最近几轮对话、当前设备或故障对象、已经命中的知识片段,以及一些运行时状态。真正注入模型时,不会把所有历史都带进去,而是用“最近窗口 + 摘要压缩”的方式,保留最近高价值内容,压缩老内容,控制 token 和响应延迟。
长期记忆这层我会异步做,不放在 Redis。每轮交互后,把有价值的信息,比如故障现象、处理步骤、最终方案、适用机型,做摘要和结构化,再向量化存到向量库。后面遇到类似问题时,通过语义检索把高相关记忆召回进 prompt,而不是直接把历史聊天整段回放。
在召回阶段,而是先判断当前问题是否真的需要历史经验支撑;如果需要,再结合当前短期上下文构造查询,通过语义检索 + 全文检索 + 过滤重排去召回,最后再用阈值控制把低相关记忆挡掉。
所以整体上,短期上下文解决的是“当前怎么答”,长期记忆解决的是“以后类似问题还能不能复用经验”,两层拆开以后,响应速度、成本和回答质量会更容易平衡。
具体做法Redis会话热状态。主要包括 4 类信息:
- 最近几轮原始对话,用来保证多轮问答连续性。
- 当前问题上下文,比如当前设备型号、故障对象、错误码、用户上传过的图片或命中的文档片段 ID。
- 运行时状态,比如这轮已经检索过哪些知识、是否已经触发过图片检索、当前是不是在追问某个步骤。
- 压缩后的阶段性摘要,比如“当前讨论的是 XX 机型落杆异常,已排查电源,待确认控制板”。 这样设计是因为 Redis 更适合放高频读写、随会话变化的上下文,读取快,适合支撑当前轮 prompt 组装;但不会把所有历史都长期堆在 Redis 里,不然成本和维护都不划算。
长期记忆怎么处理? 做了一层价值提炼,每轮交互结束后,会异步抽取真正值得沉淀的信息,比如:
- 故障现象 | 设备型号 | 部件名称 | 错误码 | 排查步骤 | 最终处理方案
- 是否需要更换零件| 适用范围和限制条件
然后把这些内容整理成更稳定的知识单元,再做摘要和向量化,写入向量库。 这样后面检索时召回的是“有效经验”或者“结构化案例”,不是充满噪声的聊天流水。
19. 召回记忆的触发条件和召回链路
先判断要不要召回,再讲怎么召回,最后讲怎么控制副作用 什么时候判断需要召回记忆:
- 问题是否依赖历史上下文 比如用户说“这个故障上次怎么处理的”“刚才那个报码还有别的可能吗”“这个机型之前是不是换过主板”,这类明显是在引用前文或历史经验,就需要召回。
- 当前问题是否信息不完整 比如用户只说“这个报码怎么处理”或者“这个情况还有别的原因吗”,如果只靠当前一句话信息不足,我会优先看短期会话上下文,如果还不够,再触发长期记忆召回。
- 问题是否属于经验复用型 像设备运维场景里,很多问题不是标准 FAQ,而是“某机型 + 某故障现象 + 某处理步骤”的组合,这类问题天然适合从历史案例里找相似经验,所以我会触发长期记忆检索。
实际实现上,可以是规则判断,也可以做一个轻量分类器。
比如先把用户问题分成“纯事实问答 / 当前多轮追问 / 历史经验型问题 / 信息不足需补全”几类,只有后两类才触发长期记忆召回。这样可以避免每轮都查向量库。
召回具体怎么做
- 先构造检索查询 不是只拿用户这一句话直接查,而是会把当前短期上下文一起参与构造 query。
比如把“当前设备型号、错误码、故障现象、上一轮确认过的部件名”拼进去,形成一个更完整的查询表达。因为长期记忆检索如果只看单句,很容易召回偏。 - 再做混合检索 长期记忆这层我通常不是只做纯向量检索,而是:
- 语义检索,用来找相似故障案例和处理经验
- 全文检索,用来匹配错误码、机型、部件编号这类精确词
- 如果有需要,再加 metadata filter,比如只看同机型、同设备类别、同部件范围内的记忆
召回之后不会直接拿 topK 就喂模型,我还会做一层 rerank 或相关性过滤,最后只保留少量高价值片段进入 prompt。
怎么避免召回错记忆
这里我一般会加 3 个控制:
- 相似度阈值 如果 top 结果相似度不够,就不召回,宁可不给也不乱给。
- 召回数量限制 不会把很多历史记忆全塞进去,通常只保留 topN,避免噪声太多污染当前回答。
- 记忆类型约束 比如把长期记忆分成“故障案例”“排查步骤”“处理结论”几类,按当前问题类型只召回相关类别,而不是所有记忆混查。
20. 检索到了正确内容,模型还是编造了信息,怎么处理幻觉?
我们至少做了三个层面的组合。
Prompt 层面,明确要求只基于检索结果回答,没有就说不知道。
生成层面,用输出自校验,生成后再过一遍模型检查每条信息是否有依据。
参数层面,把 temperature 调低到 0.1 到 0.3 之间,降低随机性。
另外我们还有一个兜底机制——把生成的回答和检索到的上下文做一次相似度对比,如果语义距离太大,说明模型很可能在胡编,这时候直接返回"抱歉,未找到相关信息",不让答非所问的东西出去。
这一套组合下来,我们项目的幻觉率从 30% 降到了 12% 左右。
21. RAG 知识库如何实现动态与持续更新?
我理解知识库更新的核心挑战是,文档变了,对应的 chunk 和向量都要跟着变,而且要做到增量处理,不能每次全量重建。我们的通用方案是给每个文档算一个内容 hash,通过轮询或者监听数据源变更,检测到文档新增、修改、删除的时候,先清掉旧的向量,再重新切割入库。对于实时性要求比较高的场景,我会用消息队列比如 Kafka 做变更事件驱动,实现秒级的入库。
22. RAG 最难的地方是哪里?
我觉得 RAG 最难的不是把它跑起来,一个基础的 Demo 一两天就能搭起来,难的是把它调好。工程上最让我头疼的有三块。
第一是文档预处理,原始数据的格式五花八门,PDF 里面的表格、图片、嵌套的格式,处理不好就是一堆乱码进了知识库,进去的是垃圾出来的也是垃圾。
第二是检索质量的调优,向量召回不准是整个系统效果的天花板,但问题来源很多,Chunking、Embedding、Query 改写,任何一个环节出问题都会影响结果,排查起来很费劲。
第三是效果评估,答案对不对很难系统性地衡量,不知道是哪个环节出了问题,优化就变成了瞎猜
总结来看,RAG 落地最大的感受是:原型 Demo 一两天能跑起来,但把它调到生产可用的质量水平,往往需要几周甚至几个月的迭代。每个环节都可以是瓶颈,文档预处理、Chunking 策略、Embedding 选型、检索方式、Rerank、Prompt 设计,任何一个做得差都会拖累整体效果,而且各环节之间还相互影响,没有捷径。
五、通用高频问题
23. 你为什么从上一家公司离职?
在稳安特这两年多,我主导了停车生态核心产品的全链路前端建设,从停车缴费、欠费追缴到运营管理后台,再到多模态 RAG 知识库的 AI 落地,做了不少从 0 到 1 的项目,也积累了完整的技术架构经验。但到后期,核心业务已经基本建设完成,进入了相对稳定的维护期,新的业务需求和复杂场景越来越少,我能接触到的技术挑战和成长空间也自然收窄了。
我现在处于职业上升期,更希望去一个业务还在快速增长、技术挑战更大的平台继续成长。所以选择离开,不是因为现在公司不好——事实上我很感谢这段经历——而是我觉得自己的成长节奏和业务的发展阶段已经有了一些错位,想换个环境突破一下。我换工作比较看重技术成长和业务复杂度。
24. 你 Vue 3 熟悉的程度如何,能说说响应式原理吗?
Vue 3 的响应式基于 Proxy 实现,不同于 Vue 2 的 defineProperty。核心是 reactive 和 ref 两个 API。
reactive 用 Proxy 拦截 get/set 操作——get 时通过 track 函数收集依赖(当前正在运行的 effect),set 时通过 trigger 函数触发所有依赖的 effect 重新执行。
ref 本质上是把值包装成 { value: ... } 的结构,在模板中自动解包。如果 ref 传入的是对象,底层还是会走 reactive。
依赖收集的核心数据结构是一个 WeakMap:key 是目标对象,value 是一个 Map,这个 Map 的 key 是属性名,value 是 Set,存的是 effect 函数。这样当属性变化时,就能精确地找到哪些 effect 需要重新执行,不会触发不必要的更新。
25. 你有带团队的经验吗?
在稳安特我是前端负责人,团队规模不算大,但职责覆盖了技术决策、任务拆解和代码质量把控。我主要负责核心架构的设计和难点攻坚,日常开发中会做 Code Review 和技术方案评审。我不属于传统意义上的"管人"式管理,更多是技术引领 + 把事情做规范——比如统一技术栈、沉淀脚手架、建立编码规范,让团队协作更顺畅。遇到技术难题时我会和大家一起讨论方案,也鼓励团队成员参与开源项目。
