跳到主要内容

项目介绍稿(面试口述版)

自我介绍(1 分钟版)

面试官你好,我叫李文杰,9 年前端开发经验,目前在找 Web 开发工程师的岗位。

我的技术栈以 React + TypeScript 为主,Next.js、Node.js 也用得比较多,最近两年做了不少全栈的工作。

职业经历大概分三个阶段:

最早在凡科做了四年,从前端开发做到前端组长,主要负责一个在线图片设计平台,就是类似 Canva 那种产品,核心是 Canvas 和 SVG 的可视化编辑器开发。这段经历让我在可视化、图形计算这块积累比较深。

第二段在虎牙做了三年前端,主要做直播 M 站和视频站的 React SSR 重构,还有视频播放器 SDK 的开发维护。这段经历让我对性能优化、SDK 架构设计和音视频领域有了比较系统的理解。后期的话加入了一个Web3 创新产品的研发。

然后作为初创员工和当时那个团队一起出来加入了一家创业公司,担任 Web 开发负责人。先后主导了两个海外平台的 Web 端架构设计和核心开发——一个是 Web3 直播社交平台,一个是 AI 社交娱乐平台。因为是创业公司,前后端都是我在做,用 NestJS 写后端服务,也做了埋点系统、Prompt 模板管理系统这些内部工具。这段经历让我从纯前端成长为能独立负责全栈交付的工程师。

技术上,我比较关注 AI 应用开发这块,LLM API 集成、流式渲染、Prompt 工程这些在实际项目中都有落地。平时也在用 Claude Code 这类 AI 工具辅助开发,对研发效率提升帮助很大。

以上就是我的简单介绍,接下来我可以详细聊聊项目经历。


按时间倒序介绍。每个项目先用 1-2 句话概括,再展开关键技术点。「追问预备」是面试官可能追问的方向,平时记住要点,面试时自然展开即可。


一、Swee — AI 社交娱乐平台(2024 - 至今)

开场概括(30 秒版)

Swee 是一个 AI 驱动的社交娱乐平台。我在里面负责的比较多,主要做了四块:移动端 PWA、一个 Prompt 模板管理系统、NestJS 后端服务、还有事件埋点系统。前后端都是我做的。 系统采用单仓模式,用vercel提供的Turborepo做前后端代码管理,所有项目都在同一个仓库里,公共组件和工具函数放在 packages 目录下,前端项目和后端项目分别在 apps 目录下。 系统部署方案是:GitHub Actions CI/CD , PM2,Docker、Kubernetes,静态资源部署到阿里云oss。 鉴权方案是通过调用同一个k8s集群的java服务的内部dns服务发现地址(K8s Service 名称 + 命名空间 + 集群内部域名 + 服务端口)。

遇到的坑

  • Nest应用不支持引入未编译的内部包,Turborepo官方其实是建议应用层直接引用内部包的源码,也就是内部包的package.json写 "main": "src/index.ts",这样应用层引入时会直接引入源码,Turborepo会在构建时自动编译内部包的源码并缓存编译结果,构建速度很快。但是 Nest 的模块解析机制不支持直接引入ts,只编译当前项目 src中的ts,不编译外部packages,会报错。官方推荐方法是使用 Nest 原生 Monorepo,但是不想用。解决方案是:内部包的 package.json 里 main 字段指向编译后的文件(比如 dist/index.js),应用层正常引入,Turborepo 构建时先编译内部包生成 dist/index.js,再编译 Nest 应用,这样就兼容了 Nest 的模块解析。

1. AI 移动端 PWA

一句话:Next.js做的 PWA 应用,以真人直播为基础,包含比较完整的直播体系,比如观看,送礼,弹幕。然后主播可以创建数字人,用户跟 AI 数字人聊天互动,有好感度系统、剧情任务、角色扮演,换装等玩法。核心是 AI 对话界面,需要处理流式渲染、多内容类型混排和复杂的聊天状态管理。

技术栈:Turborepo + Next.js + Shadcn/ui + Zustand + Tailwind CSS + next-intl

展开讲

这个应用的核心页面是 AI 对话,跟普通聊天不一样,AI 回复是流式返回的,而且消息类型很多——文本、Markdown、代码块、图片、音频、卡片(好感度变化 +5、任务触发卡片、对话次数超过次数引导充值)这些都要混排展示。示例如下:

"今天的约会真开心呢~下次我们去水族馆吧?"
[structured:affinity]{"type":"affinity","value":5,"current":75}[/structured]
[structured:choices]{"type":"choices","items":[{"id":"1","label":"好呀!"},{"id":"2","label":"我再想想"}]}[/structured]

核心思路是把流式文本拆成"纯 Markdown 段"和"结构化标记段"两类,分别渲染。

具体做法

后端通过 SSE 推流式返回 AI 响应,前端用 ReadableStream 逐 chunk 读取。后端在 LLM 输出中约定了一种标记格式:[structured:类型名]JSON数据[/structured]。前端写了一个增量解析器,每次收到新 chunk 拼接到累积文本后,用正则扫描整段文本:

  1. 已闭合的结构化标记(开标签和闭标签都有)→ 提取 JSON,JSON.parse 后分发给对应的 React 组件渲染
  2. 纯文本段(标记之间或之外的内容)→ 交给 react-markdown 实时渲染,支持 GFM 表格、代码高亮
  • 不完整的 Markdown 语法(比如 **加粗 只来了一半、代码块只有开头的 ``` 没有结尾)会导致渲染错乱,我在传给 react-markdown 之前做了预处理,检测未闭合的标记并自动补全闭合(如追加 **```)。
  1. 未闭合的标记(只有开标签,闭标签还没到)→ 跳过,显示加载占位符,等后续 chunk 补全。 详情见ai-对话的流式渲染管线是怎么实现的文本和结构化内容怎么混排

性能优化

  • requestAnimationFrame 批量更新:流式渲染时每个 chunk 都会触发一次 React re-render,LLM 流式输出的频率约 30-100 token/秒,可能比浏览器正常的渲染频率要高。消息列表组件会重新计算布局,性能开销比较大。我用 requestAnimationFrame 做了一个批量更新机制,流式过程中收到 chunk 先更新一个 ref 里的最新消息内容,然后在下一帧统一调用 setState 更新状态,这样无论流式数据多快都保证每帧最多更新一次 DOM,大大降低了 CPU 占用和卡顿感。
  • 分块渲染: 拆分为多个独立组件,只有最后一个 chunk 在流式更新,前面的 chunk 内容已稳定,用 React.memo 跳过重渲染。
  • 长消息列表用虚拟滚动(只渲染可视区域 ± buffer 的消息),避免几百条消息时 DOM 节点爆炸,核心思路是先估算,渲染后测量,缓存真实高度,动态调整占位元素高度实现无缝滚动。使用现成的库react-virtuoso。
  • 图片消息懒加载 + 骨架屏占位,滚动到可视区域时才加载
  • IndexedDB 存储历史消息,实现离线查看和快速加载
  • Service Worker 缓存策略:通过 next-pwa(底层基于 Workbox)对不同资源配置差异化缓存策略——Next.js 构建产物(_next/static/ 下带 hash 的 JS/CSS)走 Cache First(命中缓存直接返回,避免重复请求),API 走 Network First(优先网络保证数据新鲜,断网降级到缓存),图片走 Stale While Revalidate(先返回缓存保证速度,后台静默更新)。next-pwa 在构建时根据产物自动生成预缓存清单和 SW 文件,发版后 SW 检测到资源 hash 变化自动拉取新版本

另外,流式过程中列表要自动滚动到底部,但用户手动上滑查看历史时不能打断,我用一个 isUserScrolling 标记来区分,基于 IntersectionObserver 检测底部哨兵元素的可见性


2. Prompt 模板管理系统(独立设计,前后端全栈)

一句话:接下来是Prompt 模板管理系统,作用是把 公司内部平时会反复使用的,不管是员工使用,还是业务上(比如agent)使用的60 多个 AI Prompt 集中管理起来,用户可以在线编辑、调试、发布 Prompt,全程不需要开发介入。

技术栈:Ant Design + Tailwind CSS + CodeMirror 6(前端)/ NestJS + PostgreSQL + Redis(后端)+ 阿里云 OSS(存储)

展开讲

做这个系统的原因是——我们平台有 100 多个 Prompt,分散在 Java 后端和 NestJS 后端代码里,运营想改个措辞都要找开发改代码、部署,至少半天。这明显不合理,Prompt 应该和代码解耦。

核心功能

3张核心表: prompt_templates 存模板内容和元信息,template_versions 版本快照表,每次保存/回滚创建一条,和prompt_templates是多对一的关系,execution_logs 执行日志表,Playground 调试和 SDK 正式调用都记录。执行日志超过 90 天的会归档到 OSS,PostgreSQL 中只保留近 90 天的数据。

模板编辑器是三栏布局——左边变量面板(定义变量名、类型、是否必填、最大长度)、中间 CodeMirror 6 代码编辑器写 Prompt、右边实时预览渲染结果。编辑器支持变量 {{xxx}} 语法高亮(黄色)和条件块 {{#if}} 高亮(蓝色),输入 {{ 会自动弹出已定义的变量列表补全。

变量引擎是核心——我设计了一个三步渲染管线:先处理条件块 {{#if}}(决定哪些文本段保留),再处理循环块 {{#each}}(展开数组),最后做变量插值 {{variable}}。顺序不能反,因为先插值会破坏条件块的语法结构。举个例子,数字人人格生成的模板里有 {{#if favoriteCharacter}},如果用户没填这个字段,整段"最喜欢的角色"就不会出现在最终 Prompt 里。

Playground 调试是运营用得最多的功能——改完 Prompt 填入测试变量,直接调 LLM 看效果。关键设计是渲染追踪,面板会展示三步管线的每步中间结果,还会高亮未匹配变量和未使用变量。运营自己就能定位 80% 的问题。

执行引擎 执行引擎连接模板和 LLM,核心是在调 LLM 之前把能拦截的问题全部拦截。执行管线:校验 → 限流 → 渲染 → Token 预检 → 调用 LLM

版本管理每次保存都是完整快照(模板通常 < 5KB,存储成本忽略不计),回滚是创建一个"内容等于旧版本的新版本",保留完整的版本线。并发编辑用乐观锁(WHERE version = currentVersion)。

SDK 集成提供 render-only 模式给 Java 后端和 NestJS 后端,只渲染模板返回 messages,LLM 调用由业务方自己完成。多级缓存:SDK 内存 60s → Redis 5min → PostgreSQL。迁移是分 4 批灰度的,每批都有双写兼容逻辑,SDK 调不通就回退到硬编码,全程零事故。


3. 事件埋点分析服务

一句话:自己设计的埋点系统 + BI 分析,包含一个事件上报SDK,后台管理系统和后端服务,NestJS + PostgreSQL + Kafka + Redis + Grafana ,负责全端事件数据的实时采集、缓冲写入和多维度分析查询,覆盖用户全链路行为追踪和 25 项核心 KPI,支撑运营数据看板和定时报表推送。

展开讲

SDK: 合并请求(事件先进缓冲队列,满10条或者5秒定时器触发1次上报,哪个先到就批量写入,减少请求数,减轻服务器压力)、多级降级(sendBeacon -> fetch -> xhr)、页面卸载兜底(visibilitychange + beforeunload 事件触发最后一次发送,保证数据尽可能完整)、内置用户行为追踪(pv,uv,页面停留时长等)。

管理后台:一套完整的埋点事件定义与上报数据查询系统,支持事件集合管理、事件定义(含自定义属性和枚举值(String(字符串)、Int(数值)、Enum(枚举)))、事件发布、上报数据查看和埋点代码自动生成。

后端:校验后投递到 Kafka 解耦采集与入库,应对前端突发流量,Consumer 端攒批后批量 INSERT 到 PostgreSQL((满 500 条或 5s 超时,先到先刷),避免高峰期单条写入打满连接池,低峰期数据滞留。事件属性用 PostgreSQL JSONB 存储,加 GIN 索引支持灵活的自定义字段查询。

聚合计算 25 项核心 KPI:日活、注册转化率、内容生成量、VIP 转化、充值金额等,通过定时任务聚合后推送到飞书看板。


4. NestJS 后端服务

一句话:基于 NestJS 10 的 BFF 层,负责多语言文案管理、文件管理、配置管理、AI 对话、素材库等功能。

展开讲

  • 多语言文案管理:支持 8 种语言,文案以 JSON 格式存储在 MySQL 中,集成微软翻译器 API 做自动翻译,运营编辑中文后一键翻译为其他 7 种语言,也支持各端的开发一键下载国际化相关文件到项目中,同时也提供脚本给各端直接执行更新最新文案。有完整审计日志:操作前后内容对比 + IP + 操作人
  • 素材库:树形文件夹结构,用 TypeORM 的 Materialized Path(物化路径) 模式存储层级关系(多一个mpath字段,如1.2.4,表示层级路径),支持多级文件夹嵌套、多类型文件上传/预览/编辑/替换。阿里云 OSS 直传 + CDN 自动刷新。上传后自动生成 CDN 地址,旧文件覆盖时主动调 CDN 刷新 API 清缓存
  • 配置管理:相当于配置一个json,作为一个通用配置。可视化编辑器,根据数据内容自动推断列类型,展示对应类型的组件(图片显示图片预览和上传,链接显示链接样式,其他使用输入框)。JSON 配置存 MySQL,多级缓存(内存 → Redis → MySQL),支持版本控制和一键回滚。每次修改自动创建版本快照。允许授权给其他用户查看和编辑。

二、RYZZ — Web3 直播社交平台(2023 - 2024)

开场概括(20 秒版)

RYZZ 是一个 Web3 直播社交平台,融合直播互动(送礼打赏弹幕)、预测市场、Token 对战、NFT 等功能,是一个融合 Web2 社交直播与 Web3 去中心化金融的平台。


1. 主站

技术栈:Next.js + React + TypeScript + Recoil + WebSocket + Protobuf + Wagmi + Ethers.js + Tailwind CSS

展开讲

主站的架构设计有两个核心难点——实时通信Web3 集成

WebSocket + Protobuf 实时通信:直播间的弹幕、礼物特效、投注通知、在线人数这些都是实时推送的,web3的交互也有大量异步操作,需要异步通知。一开始是用 JSON,但是数据量大的时候带宽消耗明显,后来换成 Protobuf 二进制协议,定义了 90 多个 Proto 消息类型,相比 JSON 大概节省 50% 带宽。前端封装了 WebSocket 管理层,做了自动重连(指数退避),心跳保活,消息队列缓冲(实例初始化前发送的消息队列,需要在初始化后发送)和状态重放(断连后重连时先重新发送登录和进房的消息)。事件分发基于 EventEmitter 的事件驱动架构,解耦消息处理与业务逻辑

Web3 钱包连接:用 Wagmi + Ethers.js 做钱包连接,支持 MetaMask 等外部(去中心化)钱包和 DAuth MPC(通过邮箱号/第三方/手机号) 托管钱包。链上交互包括投注、Token 对战、NFT 铸造,前端负责构造交易、发起签名、监听链上确认。


三、虎牙直播 & 虎牙视频(2020 - 2023)

开场概括(15 秒版)

在虎牙主要做两块——直播 M 站和视频站的 SSR 重构,还有视频播放器 SDK。


1. 直播 M 站 & 视频站 SSR 重构

一句话:把虎牙直播 M 站和视频站从 PHP + Smarty 模板引擎重构为 React + TypeScript SSR 架构。

技术栈:React + TypeScript + Redux 展开讲

基于团队自研的 SSR 框架(Koa 2 内核),实现文件系统路由、getServerProps 服务端数据预取、Redux Store 序列化注入和客户端 Hydration 的完整链路。

有几个比较重要的设计:

  • 在迁移的过程中,除了用新技术还原原有功能,还做了性能优化和架构改进,比如为组件设置优先级(高优先级组件先渲染,低优先级组件延迟渲染),图片优化(图标使用svg sprites,webp),缓存策略(静态资源长缓存,HTML 页面短缓存),数据聚合(利用Promise.allSettled 聚合多个异步请求,并根据情况设置缓存
  • 重构后页面秒开率平均提升约 41%,白屏时间 1s 内占比平均提升约 34%

遇到的坑

  • 通过ua判断跳转移动端。移动端触发了回源,302 被缓存,PC 用户也会拿到这个 302。解决方案是:CDN 边缘做 UA 判断,不在源站做重定向(另一个方案是改为前端js跳转)

2. 视频播放器 SDK

一句话:虎牙视频播放器核心 SDK,四层分层架构,插件化设计,支持多格式播放和自适应码率。

技术栈:TypeScript + EventEmitter + MSE + HLS.js

展开讲

播放器采用四层分层架构

  • UI 层:控制栏、进度条(缩略图预览)、弹幕层、与播放逻辑完全解耦
  • 播放器核心层:Player Core 负责播放流程编排,PluginManager 管理插件生命周期(install → init → ready → running → destroy),EventBus 事件总线做层间通信,StateStore 管理播放状态
  • 引擎层:抽象了 PlayEngine 接口,三种实现——HTML5 Video(MP4 渐进式)、MSE Engine(HLS/DASH 流式)、WebCodecs Engine(低延迟场景)。自动降级策略:优先 MSE,iOS Safari 降级到原生 HLS,最后兜底 WebCodecs + Canvas
  • 解码层:优先浏览器原生硬解,H.265/AV1 格式用 WASM 软解兜底

插件系统是扩展性核心——弹幕、字幕、广告、数据上报、快捷键、画中画、清晰度切换全都是插件,不是硬编码在核心里。插件通过 EventBus 订阅播放事件(play / pause / timeupdate / error 等),核心包 < 30KB gzipped,插件按需加载。

自适应码率(ABR):实现了混合算法——结合带宽估计(滑动窗口平均下载速度)和 Buffer 水位(高于上阈值升档、低于下阈值降档),在画质和流畅度之间取平衡。切换清晰度时通过 MSE 的 SourceBuffer 无缝切分片,用户感知不到切换过程。

弹幕系统:Canvas 渲染(比 DOM 性能好一个量级),实现了轨道分配算法做碰撞检测、弹幕密度控制、滚动/顶部/底部三种弹幕类型。直播弹幕和录播弹幕用不同的时间线同步策略。

可观测性:内置数据上报插件,采集首帧时间、卡顿率、Buffer 健康度、seek 耗时等播放质量指标,用于质量监控和调优。


四、凡科快图(2016 - 2020)

一句话:在线图片设计平台,Web 端 PS 的定位。

简单提

  • Web 端轻量级 PS,基于 Vue + Canvas + SVG 构建可视化编辑器,支持图层管理、滤镜特效、拖拽操作、旋转拉伸、文本编辑、矢量图形等功能。
  • 重构编辑器技术架构,从canvas转向SVG,减少40%开发成本,提升性能和可维护性。
  • 编辑器内包含大量计算密集型操作(变换矩阵、碰撞检测、边界判断、滤镜等),通过 Web Worker 多线程处理,主线程保持 UI 响应流畅。

五、面试官追问题库

按项目分类整理面试官可能追问的问题。每个问题标注了可参考的文档链接,面试前翻阅对应文档即可准备答案。

Swee — AI 移动端 PWA

#追问方向具体问题参考文档
1流式渲染SSE 和 WebSocket 有什么区别?为什么选 SSE?WebSocket 与 SSE
2流式渲染ReadableStream 怎么用的?流式数据怎么逐步解析?流式渲染与 Markdown
3流式渲染增量 Markdown 解析具体怎么实现的?遇到不完整语法怎么处理?流式渲染与 Markdown
4流式渲染流式渲染过程中怎么控制自动滚动?用户上滑时怎么判断?AI 聊天界面设计
5性能优化虚拟滚动怎么实现的?用的什么库还是自己写的?长列表优化
6性能优化requestAnimationFrame 批量更新具体怎么做的?和直接 setState 有什么区别?requestAnimationFrame
7性能优化图片懒加载用什么方案?IntersectionObserver 还是 scroll 事件?图片优化
8状态管理为什么选 Zustand 而不是 Redux 或 Jotai?状态管理方案
9状态管理Zustand 的 subscribeWithSelector 怎么避免不必要的重渲染?React 性能优化
10PWAService Worker 缓存策略是怎么设计的?有哪些坑?Web Workers
11PWAPWA 的离线体验怎么做的?离线时用户能做什么?PWA 渐进式 Web 应用
12React消息列表组件的 key 怎么设计的?为什么不能用 index?列表渲染优化
13React对话页面有哪些组件?组件之间怎么通信的?组件通信方案
14Next.js为什么用 Next.js?哪些页面用了 SSR,哪些用了 CSR?Next.js 核心知识
15AILLM API 怎么调的?前端直接调还是走后端代理?前端接入大模型 API
16AIFunction Calling 用过吗?怎么处理工具调用的?Function Calling 与 Agent
17多内容类型消息渲染管线具体是怎么设计的?怎么识别不同类型的段落?AI 聊天界面设计
18MonorepoTurborepo 的 monorepo 怎么组织的?packages 目录怎么拆分?Monorepo 管理
19MonorepoTurborepo 的缓存机制是什么?对构建速度提升有多大?Monorepo 管理
20部署GitHub Actions CI/CD 流程是什么?怎么和 K8s 配合的?CI/CD 与自动化部署
21鉴权K8s 内部 DNS 服务发现鉴权具体怎么做的?为什么不直接调外网地址?Kubernetes 基础
22离线存储IndexedDB 存历史消息具体怎么设计的?数据结构是什么?和服务端怎么同步?前端存储技术
23多语言next-intl 多语言方案怎么做的?SSR 下多语言路由怎么处理?设计多语言管理系统
24分块渲染流式消息按换行符拆分为多个组件,具体怎么切分的?切分粒度怎么定?流式渲染与 Markdown
25协议设计[structured:xxx] 这种自定义标记协议是怎么设计的?为什么不用 JSON 包裹整条消息?AI 聊天界面设计

Swee — Prompt 模板管理系统

#追问方向具体问题参考文档
1架构设计为什么不用 LangSmith / Humanloop?Build vs Buy 怎么决策的?Swee-Prompt 模板管理系统 Q2
2变量引擎三步渲染管线的顺序为什么不能变?举例说明反了会出什么问题?Swee-Prompt 模板管理系统 Q3
3变量引擎变量的 maxLength 有什么用?不设会出什么问题?设计 Prompt 模板管理系统 六、变量引擎
4编辑器CodeMirror 6 自定义高亮怎么做的?为什么不用 Lezer Grammar?Swee-Prompt 模板管理系统 7.2
5编辑器变量自动补全怎么实现的?数据源从哪来?Swee-Prompt 模板管理系统 3.3
6版本管理为什么用完整快照而不是增量 Diff?Swee-Prompt 模板管理系统 Q4
7版本管理回滚怎么实现的?为什么是"创建新版本"而不是覆盖?Swee-Prompt 模板管理系统 Q4
8版本管理乐观锁怎么实现的?冲突了前端怎么处理?事务与并发控制
9缓存SDK 的多级缓存怎么设计的?发布后怎么保证一致性?Swee-Prompt 模板管理系统 Q5
10缓存Redis 缓存的 TTL 为什么加随机偏移?服务端缓存策略
11迁移60 多个 Prompt 怎么迁移过来的?迁移过程中最大的坑是什么?Swee-Prompt 模板管理系统 Q6
12迁移双写兼容具体怎么做的?降级逻辑是什么?Swee-Prompt 模板管理系统 7.1
13安全Prompt 注入攻击了解吗?你的系统怎么防的?AI 应用安全
14数据库为什么用 PostgreSQL 而不是 MySQL?JSONB 有什么优势?PostgreSQL 特性
15系统设计如果让你重新设计,会加什么功能?Swee-Prompt 模板管理系统 Q11
16系统设计系统挂了怎么办?线上 AI 功能会受影响吗?Swee-Prompt 模板管理系统 Q8
17Prompt你对 Prompt Engineering 有什么了解?Few-shot 用过吗?Prompt 工程
18效果评估怎么知道改完 Prompt 效果好不好?有没有做 A/B 测试?Swee-Prompt 模板管理系统 Q7

Swee — NestJS 后端 & 埋点服务

#追问方向具体问题参考文档
1NestJSNestJS 的 Module / Controller / Service 是怎么组织的?NestJS 框架深入
2NestJSGuard 和 Middleware 有什么区别?你的鉴权用的是哪个?NestJS 框架深入
3NestJSNestJS 的请求处理管道是什么顺序?异常怎么统一处理?错误处理
4数据库TypeORM 的 Materialized Path 是什么?怎么查询子孙节点?ORM 框架对比
5数据库多语言文案用 JSON 存,查询性能怎么样?有没有考虑过其他方案?MySQL 基础与查询
6文件管理OSS 直传是怎么做的?前端直传还是后端中转?文件存储与 OSS
7文件管理CDN 缓存刷新是怎么做的?什么时候触发?CDN 原理
8埋点为什么要用缓冲策略?直接逐条写入不行吗?前端监控与埋点
9埋点双触发缓冲(600 条 OR 3 秒)的参数怎么定的?前端监控与埋点
10埋点JSONB + GIN 索引是什么?为什么适合埋点数据?PostgreSQL 特性
11埋点异步非阻塞写入怎么实现的?如果写入失败怎么办?消息队列
12埋点IP 地理位置解析为什么要限制在缓冲区 < 45 条时执行?限流与熔断
13认证调用外部 Java 认证服务,网络延迟怎么处理?有没有缓存 Token?JWT 认证
14埋点执行日志超过 90 天归档到 OSS,这个定时任务怎么实现的?归档后还能查吗?定时任务与后台作业
15埋点25 项 KPI 聚合计算是实时的还是离线的?定时任务多久跑一次?定时任务与后台作业
16埋点飞书看板推送怎么实现的?数据格式是什么?Webhook 设计与实现
17埋点异步非阻塞写入如果 DB 挂了,内存缓冲区的数据怎么办?会丢数据吗?消息队列
18埋点埋点 SDK 前端怎么上报的?用 sendBeacon 还是 fetch?页面关闭时数据怎么保证不丢?前端监控与埋点
19多语言微软翻译器 API 的翻译质量怎么样?运营需要人工校对吗?设计多语言管理系统
20素材库Materialized Path 和嵌套集(Nested Set)、闭包表(Closure Table)有什么区别?为什么选它?ORM 框架对比

RYZZ — Web3 直播社交平台

#追问方向具体问题参考文档
1WebSocketWebSocket 的自动重连怎么做的?指数退避具体是什么策略?WebSocket 与 SSE
2WebSocket心跳保活是怎么实现的?多久发一次?服务端超时怎么处理?WebSocket 服务端
3WebSocketWebSocket 断开重连后,丢失的消息怎么补回来?断线重连与离线处理
4ProtobufProtobuf 和 JSON 有什么区别?为什么能节省 50% 带宽?TCP 与 UDP
5Protobuf前端怎么编译 Proto 文件?类型怎么和 TypeScript 对齐?JavaScript 模块化
6Web3钱包连接的流程是什么?怎么判断用户有没有安装 MetaMask?Web3 钱包连接方案
7Web3链上交易的前端签名流程是什么?怎么监听交易确认?Web3 钱包连接方案
8状态管理为什么选 Recoil 而不是 Redux?15 层 Provider 不会有性能问题吗?状态管理方案
9直播追帧策略是什么?为什么要 1.1-1.2 倍速?设计视频播放器 SDK
10直播FLV 和 HLS 有什么区别?什么时候用哪个?设计视频播放器 SDK
11架构Monorepo 的 7 个共享包是怎么拆分的?用什么工具管理?Monorepo 管理
12图片处理Sharp 的性能怎么样?和 ImageMagick 比呢?设计图片处理 CDN 服务
13图片处理URL 参数 DSL 怎么解析的?多个操作怎么链式执行?设计图片处理 CDN 服务
14DevOpsDocker 多阶段构建是什么?为什么能减小镜像体积?Docker 与容器化
15监控OpenTelemetry 是什么?怎么做分布式链路追踪?日志与监控体系

虎牙 — SSR 重构

#追问方向具体问题参考文档
1SSRSSR 和 CSR 有什么区别?SSR 的优缺点是什么?SSR 与 SSG
2SSRHydration 是什么?Hydration 失败(mismatch)怎么排查?SSR 与 SSG
3SSRgetServerProps 里的数据怎么传给客户端的?序列化有什么坑?Server Components 深入
4SSR内存泄漏在 SSR 场景下更容易出现,你遇到过吗?内存泄漏排查
5性能秒开率提升 41% 具体做了哪些优化?最有效的是哪个?首屏优化
6性能懒加载、预渲染、懒执行分别是什么?怎么做的?懒加载与代码分割
7SEOSSR 对 SEO 有什么帮助?Meta 标签怎么动态注入的?meta 标签与 SEO
8路由文件系统路由是怎么实现的?和 Next.js 的有什么异同?React Router 原理
9状态管理Redux Store 在 SSR 场景下怎么处理?服务端和客户端的 Store 怎么同步?状态管理方案
10部署Serverless 部署和传统 Node 部署有什么区别?冷启动怎么处理?Serverless 与边缘计算
11监控Sentry 怎么区分 SSR 端和客户端的错误?前端监控与埋点
12缓存踩坑302 被 CDN 缓存导致 PC 用户跳移动端,这个问题怎么发现的?排查过程是什么?CDN 原理
13缓存踩坑CDN 边缘做 UA 判断和前端 JS 跳转,两种方案各有什么优缺点?CDN 原理
14迁移PHP + Smarty 迁移到 React SSR,怎么保证迁移过程中不影响线上?灰度策略是什么?技术栈升级迁移
15性能组件优先级渲染是怎么实现的?高低优先级怎么定义?渲染优化

虎牙 — 视频播放器 SDK

#追问方向具体问题参考文档
1架构四层架构每层的职责是什么?为什么这样分?设计视频播放器 SDK
2架构播放器的核心包怎么做到 < 30KB 的?打包优化
3插件系统插件的生命周期是什么?插件之间怎么通信?设计视频播放器 SDK
4插件系统EventBus 和 React 的事件系统有什么区别?观察者模式与发布订阅
5插件系统如果两个插件有依赖关系,怎么保证加载顺序?设计视频播放器 SDK
6引擎MSE 和 HTML5 Video 有什么区别?SourceBuffer 怎么用的?设计视频播放器 SDK
7引擎HLS 的 m3u8 文件格式是什么?前端怎么解析的?设计视频播放器 SDK
8引擎引擎自动降级策略是什么?怎么检测浏览器能力?浏览器兼容性
9ABR混合 ABR 算法的带宽估计怎么做的?滑动窗口多大?设计视频播放器 SDK
10ABR清晰度切换时怎么做到无缝?用户感知不到切换过程?设计视频播放器 SDK
11弹幕为什么用 Canvas 而不是 DOM 渲染弹幕?性能差多少?设计直播弹幕系统
12弹幕轨道分配算法怎么做碰撞检测的?设计直播弹幕系统
13弹幕直播弹幕和录播弹幕的时间线同步有什么区别?设计直播弹幕系统
14性能首帧时间怎么优化的?目标是多少?首屏优化
15性能播放卡顿怎么监控?Buffer 健康度怎么定义的?Web 性能指标与监控系统
16SDK 设计SDK 的 API 怎么设计的?考虑过向后兼容吗?前端 SDK 通用架构设计
17WebCodecsWebCodecs 是什么?什么场景下用?和 MSE 什么区别?设计视频播放器 SDK

凡科快图 — 在线图片设计平台

#追问方向具体问题参考文档
1架构重构为什么从 Canvas 转向 SVG?减少 40% 开发成本体现在哪?Canvas 与 SVG
2架构重构Canvas 和 SVG 在图片编辑器场景下各有什么优缺点?Canvas 与 SVG
3架构重构SVG 处理复杂滤镜(模糊、色彩调整)性能怎么样?有没有不如 Canvas 的地方?SVG 进阶与动画
4图层管理图层的数据结构怎么设计的?怎么处理图层的层级关系和 z-index?设计在线图片编辑器
5图层管理撤销/重做怎么实现的?用的命令模式还是状态快照?命令模式
6Web Worker哪些计算放到 Web Worker 里了?主线程和 Worker 之间怎么通信?Web Worker 优化
7Web Worker变换矩阵在编辑器里怎么用的?旋转+缩放+平移怎么组合计算?Transform 变换与矩阵
8碰撞检测碰撞检测和边界判断怎么做的?旋转后的元素碰撞怎么计算?碰撞检测与边界判断
9交互拖拽操作怎么实现的?拖拽过程中的吸附对齐(辅助线)怎么做?拖拽排序
10性能编辑器里元素很多时(比如几百个图层)性能怎么保证?大数据量渲染优化
11文本编辑SVG 里的文本编辑怎么实现的?富文本还是纯文本?设计在线图片编辑器
12导出编辑完成后怎么导出图片?SVG 转 PNG/JPG 怎么做的?Canvas 与 SVG

通用 / 跨项目追问

#追问方向具体问题参考文档
1职业9 年前端,为什么后面转全栈?NestJS 后端是自学的吗?前端工程师的职业发展路线
2职业你做的项目里最有挑战的是什么?本文档「通用追问准备」
3职业如果让你重新设计,会改什么?本文档「通用追问准备」
4架构你怎么理解前端架构?做架构设计时考虑哪些因素?如何理解前端架构
5工程化你怎么理解前端工程化?你在项目中做了哪些工程化的事?如何理解前端工程化
6性能你做过哪些性能优化?用什么工具分析性能?常见性能问题与排查
7测试你的项目有写测试吗?怎么保证代码质量?前端测试策略
8CI/CD你的项目怎么部署的?CI/CD 流程是什么?CI/CD 与自动化部署
9团队你怎么做 Code Review?关注哪些点?如何做好 Code Review
10技术选型你怎么做技术选型?有什么方法论?前端技术选型方法论
11AI你怎么看 AI 对前端的影响?如何看待 AI 对前端的影响
12AI你用 AI 辅助开发吗?怎么用的?AI 辅助开发
13学习你怎么持续学习前端技术的?如何持续学习前端技术
14趋势你觉得前端未来 3-5 年的趋势是什么?前端未来 3-5 年的趋势
15管理你有带团队的经验,怎么做技术管理的?如何带领前端团队
16重构如果让你重构一个遗留项目,你会怎么做?接手遗留项目重构策略
17安全你的项目怎么处理 XSS / CSRF 安全问题的?XSS/CSRF 漏洞修复实战
18错误处理线上出了 Bug 你怎么排查?有什么方法论?线上故障应急
19Docker/K8s你的 Docker 镜像怎么构建的?多阶段构建用了吗?Docker 与容器化
20Docker/K8sK8s 的 Pod、Service、Deployment 分别是什么?你的服务怎么编排的?Kubernetes 基础
21协作一个人做前后端,你怎么保证代码质量和进度?如何在有限时间内交付高质量代码
22数据库MySQL 和 PostgreSQL 你都用过,什么场景选哪个?PostgreSQL 特性

六、通用追问准备

"你做的项目里最有挑战的是什么?"

Prompt 模板管理系统的迁移过程。60 多个 Prompt 散落在 Java 和 Node 两个后端,不可能一次切换。我分 4 批灰度迁移,每批都做了双写兼容——SDK 调不通就回退硬编码。最大的坑是变量名不统一,同一个字段 Java 叫 userName、别的地方叫 user_name,最后在 SDK 层加了 fieldMapping 做映射。全程零事故。

"如果让你重新设计,会改什么?"

Prompt 系统会加 LLM-as-Judge 自动评分(哪怕最简单的让 GPT-4o-mini 打个 1-5 分),还会做模板继承/组合——数字人相关的 10 多个模板共享大段 System Prompt,现在每个都复制一遍,改一个地方要同步改十几个。

"为什么从纯前端转全栈?"

在 Swee 项目中,团队 Node.js 后端最开始没有专职后端负责,只有 Java 后端。很多运营工具类的需求(文案管理、配置管理、文件管理)不适合让 Java 团队做——太轻量了,排不上优先级。我自己用 NestJS 搭了一套,后来越做越多,Prompt 管理系统、埋点系统也一起做了。不是刻意转全栈,是业务需要驱动的自然扩展。NestJS 的模块化思想和 Angular 类似,对前端来说学习成本不高。

"AI 对话的流式渲染管线是怎么实现的?文本和结构化内容怎么混排?"

我们的 AI 对话不只是纯文本,模型输出里会混着结构化内容——好感度变化动画、剧情任务卡片、选择按钮这些。所以我基于 react-markdown 设计了一套流式消息渲染管线,核心思路是把流式文本拆成"纯 Markdown 段"和"结构化标记段"两类,分别渲染。

具体做法

后端在 LLM 输出中约定了一种标记格式:[structured:类型名]JSON数据[/structured]。前端写了一个增量解析器,每次收到新 chunk 拼接到累积文本后,用正则扫描整段文本:

  1. 已闭合的结构化标记(开标签和闭标签都有)→ 提取 JSON,JSON.parse 后分发给对应的 React 组件渲染
  2. 纯文本段(标记之间或之外的内容)→ 交给 react-markdown 实时渲染,支持 GFM 表格、代码高亮
  3. 未闭合的标记(只有开标签,闭标签还没到)→ 跳过,显示加载占位符,等后续 chunk 补全
LLM 输出流: "你好!\n[structured:affection]{"delta":5}[/structured]\n继续聊..."

解析结果:
├─ TextBlock("你好!") → react-markdown
├─ StructuredBlock("affection") → AffectionAnimation 组件
└─ TextBlock("继续聊...") → react-markdown

流式 Markdown 的坑:流式输出时内容随时可能截断——代码块只有开头的 ``` 没有结尾、粗体 ** 没闭合。我在传给 react-markdown 之前做了预处理:检测奇数个围栏/粗体标记时自动追加闭合。只对流式中的消息做预处理,已完成的消息直接渲染原始内容。

核心思路:把流式文本拆成"纯 Markdown 段"和"结构化标记段"两类,分别用不同方式渲染

SSE chunk 到达 → 拼接到累积文本 → 增量解析器扫描

┌─────────────────────────────┐
│ 纯文本段 → react-markdown │
│ [structured:xxx] 已闭合 │
│ → JSON.parse → React 组件 │
│ [structured:xxx] 未闭合 │
│ → 跳过,等后续 chunk 补全 │
└─────────────────────────────┘

1. 增量解析器:把累积文本拆成 Block 数组

lib/stream-parser.ts
interface TextBlock {
type: 'text';
content: string;
}

interface StructuredBlock {
type: 'structured';
name: string; // 如 'affection', 'task_card', 'choices'
data: unknown; // JSON.parse 后的数据
}

interface PendingBlock {
type: 'pending'; // 未闭合的结构化标记,暂不渲染
raw: string;
}

type ContentBlock = TextBlock | StructuredBlock | PendingBlock;

/**
* 将累积的流式文本解析为 Block 数组
* 每次新 chunk 到达后,对整段累积文本重新解析
*
* 标记格式:[structured:name]{"key":"value"}[/structured]
*/
function parseStreamContent(accumulated: string): ContentBlock[] {
const blocks: ContentBlock[] = [];
// 匹配已闭合的结构化标记
const pattern = /\[structured:(\w+)\]([\s\S]*?)\[\/structured\]/g;

let lastIndex = 0;
let match: RegExpExecArray | null;

while ((match = pattern.exec(accumulated)) !== null) {
// match 前面的纯文本
if (match.index > lastIndex) {
const text = accumulated.slice(lastIndex, match.index).trim();
if (text) blocks.push({ type: 'text', content: text });
}

// 已闭合的结构化块 → 解析 JSON
try {
const data = JSON.parse(match[2]);
blocks.push({ type: 'structured', name: match[1], data });
} catch {
// JSON 不合法,当普通文本处理
blocks.push({ type: 'text', content: match[0] });
}

lastIndex = match.index + match[0].length;
}

// 剩余部分:可能包含未闭合的标记
const remaining = accumulated.slice(lastIndex);
if (remaining) {
// 检测是否有未闭合的 [structured:xxx]
const unclosedMatch = remaining.match(/\[structured:(\w+)\][\s\S]*$/);

if (unclosedMatch) {
// 未闭合标记前面的纯文本正常渲染
const textBefore = remaining.slice(0, unclosedMatch.index!).trim();
if (textBefore) blocks.push({ type: 'text', content: textBefore });
// 未闭合部分标记为 pending,不渲染
blocks.push({ type: 'pending', raw: unclosedMatch[0] });
} else {
// 没有未闭合标记,全部是纯文本
blocks.push({ type: 'text', content: remaining });
}
}

return blocks;
}

2. 流式消息组件:根据 Block 类型分发渲染

components/StreamMessage.tsx
import { useMemo, memo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';

interface StreamMessageProps {
content: string; // 累积的流式文本
isStreaming: boolean;
}

export const StreamMessage = memo(function StreamMessage({
content,
isStreaming,
}: StreamMessageProps) {
const blocks = useMemo(() => parseStreamContent(content), [content]);

return (
<div className="stream-message">
{blocks.map((block, i) => {
switch (block.type) {
case 'text':
return (
<MarkdownBlock
key={i}
content={block.content}
isStreaming={isStreaming && i === blocks.length - 1}
/>
);
case 'structured':
return (
<StructuredRenderer key={i} name={block.name} data={block.data} />
);
case 'pending':
// 未闭合的标记:显示加载占位
return <div key={i} className="loading-placeholder" />;
}
})}
</div>
);
});

3. Markdown 渲染(处理流式未闭合语法)

components/MarkdownBlock.tsx
const MarkdownBlock = memo(function MarkdownBlock({
content,
isStreaming,
}: {
content: string;
isStreaming: boolean;
}) {
// 流式中需要补全未闭合的 Markdown 语法
const processed = useMemo(() => {
if (!isStreaming) return content;
return closeOpenMarkdown(content);
}, [content, isStreaming]);

return (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// 代码块:语法高亮 + 复制按钮
code({ className, children, ...props }) {
const language = className?.replace('language-', '');
const codeString = String(children).replace(/\n$/, '');

if (!language) {
return <code {...props}>{children}</code>;
}

return (
<div className="code-block">
<div className="code-header">
<span>{language}</span>
<CopyButton text={codeString} />
</div>
<SyntaxHighlighter language={language} style={oneDark}>
{codeString}
</SyntaxHighlighter>
</div>
);
},
}}
>
{processed}
</ReactMarkdown>
);
});

/** 补全流式中未闭合的 Markdown 标记 */
function closeOpenMarkdown(content: string): string {
let result = content;

// 未闭合的代码块
const fences = result.match(/```/g);
if (fences && fences.length % 2 !== 0) {
result += '\n```';
}

// 未闭合的粗体
const bolds = result.match(/\*\*/g);
if (bolds && bolds.length % 2 !== 0) {
result += '**';
}

// 未闭合的行内代码(排除代码块内的)
const inlineCode = result.match(/(?<!`)`(?!`)/g);
if (inlineCode && inlineCode.length % 2 !== 0) {
result += '`';
}

return result;
}

4. 结构化内容渲染器

components/StructuredRenderer.tsx
/** 根据 name 分发到对应的 React 组件 */
function StructuredRenderer({ name, data }: { name: string; data: unknown }) {
switch (name) {
case 'affection':
// 好感度变化动画
return <AffectionAnimation data={data as AffectionData} />;
case 'task_card':
// 剧情任务卡片
return <TaskCard data={data as TaskData} />;
case 'choices':
// 选择按钮组
return <ChoiceButtons data={data as ChoiceData} />;
case 'gift':
// 礼物特效
return <GiftEffect data={data as GiftData} />;
default:
// 未知类型,降级为 JSON 展示
return (
<pre className="unknown-block">{JSON.stringify(data, null, 2)}</pre>
);
}
}

// 示例:好感度动画组件
function AffectionAnimation({ data }: { data: AffectionData }) {
return (
<div className="affection-change">
<span className="affection-icon">💗</span>
<span className={data.delta > 0 ? 'positive' : 'negative'}>
{data.delta > 0 ? '+' : ''}
{data.delta}
</span>
<span className="affection-total">好感度: {data.total}</span>
</div>
);
}

5. 整合到流式接收流程

hooks/useStreamChat.ts
function useStreamChat() {
const [content, setContent] = useState('');
const bufferRef = useRef('');
const rafRef = useRef<number>();

const onToken = useCallback((token: string) => {
// RAF 批量更新,避免每个 token 都触发重渲染
bufferRef.current += token;

if (!rafRef.current) {
rafRef.current = requestAnimationFrame(() => {
setContent((prev) => prev + bufferRef.current);
bufferRef.current = '';
rafRef.current = undefined;
});
}
}, []);

// content 变化 → StreamMessage 重新解析 → 自动拆分渲染
return { content, onToken };
}

数据流总结

LLM 输出: "你好!\n[structured:affection]{"delta":5,"total":80}[/structured]\n继续聊..."

▼ SSE chunk 到达,拼接到累积文本

▼ parseStreamContent() 解析

├─ TextBlock("你好!") → react-markdown 渲染
├─ StructuredBlock("affection") → AffectionAnimation 组件
└─ TextBlock("继续聊...") → react-markdown 渲染