实习经历话术

会赢的

智识神工实习经历 - 面试话术指南

你是怎么设计你们的技术选型的

拿到项目的那几天,先我先进行技术选型,考虑用户的数量,模型的价格,知识库的选型,前端工程的选型,组内分工,前后端对接方式,代码提交方式等等,我让跟我一起去做后端的同事去调研相关技术,当时我跟后端同事对接出来的结果是前期用户量较少,可以选择先用单体架构,然后先使用SpringBoot进行项目快速搭建,这块我们都比较熟悉,然后我花了大概一天的时间大概搭出了个框架,包括了统一结果返回,统一的异常处理,讨论出有移动端的存在的话,使用jwt鉴权比较好,AI应用开发方面我比较熟,这块就交给我了,我当时比较熟练使用SpringAI去搭建项目,所以就选择了这套技术栈,后面我对整个后端部分进行了分工,我来开发AI应用,另一个同事开发用户模块,我们都会把代码先push到feature分支,然后自己进行单元测试和接口集成测试,测试通过放入test分支,然后写入接口文档,当时我是把接口文档的编写任务交给了另外一个同事,规定了一套标准,接口信息,请求方法,请求路径,样例请求,请求参数,返回参数,样例返回,注意事项等等,pr给前端,前端同学对接完接口后我们把代码放入main分支。

“首先是技术调研。考虑到项目处于 MVP(最小可行性产品) 阶段,我们的首要目标是快速验证业务价值,同时兼顾成本控制。

  • 架构决策:虽然微服务很火,但考虑到初期用户量级较小,且团队规模精简,为了避免分布式事务等复杂性带来的开发成本,我拍板决定采用 SpringBoot 单体架构。这也符合’架构演进’的原则,先跑通业务,后期再拆分。

  • 核心技术栈:鉴于业务核心是 AI 对话,而我对 Spring AI 生态非常熟悉,它能极大简化 LLM 的接入成本,因此后端定栈为 JDK 17 + SpringBoot 3 + Spring AI。

  • 鉴权方案:考虑到未来有多端适配(移动端/H5)的需求,我选择了无状态的 JWT + Spring Security 方案。”

第二阶段:基建搭建与规范制定 (Infrastructure & Standardization)

“技术选型确定后,我没有急着写业务代码,而是花了一天时间搭建了项目脚手架(Scaffolding)。 这不仅是建个工程,而是制定了整个后端的开发规范

  1. 统一响应结构:封装了全局的 Result<T> 对象和全局异常处理器 (GlobalExceptionHandler),确保前后端交互的一致性。

  2. 标准化配置:集成了 Swagger/Knife4j 自动生成接口文档,确立了 ‘API First’(接口先行) 的开发模式。

  3. 模块划分:虽然是单体,但我按业务领域划分了清晰的包结构(Package Structure),防止代码耦合,为未来可能的微服务拆分留好口子。”

第三阶段:团队分工与协作流 (Workflow & CI/CD)

“基建完成后,我对后端开发进行了分工:

  • 职责划分:我负责攻坚核心的 AI 应用开发(RAG、SSE、Prompt 工程);另一位同事负责用户体系等基础业务模块。

  • Git 工作流:为了保证代码质量,我制定了一套适合我们小团队的 Gitflow 规范

    • 每人基于 feature 分支开发,本地完成单元测试

    • 代码合并前必须进行 Self-Review,推送到 test 分支进行接口集成测试。

    • 测试通过并更新接口文档后,通过 PR (Pull Request) 流程合并入 main 分支交付前端。

这套流程虽然简单,但有效避免了代码冲突,保证了主分支的稳定性。

为什么移动端推荐用 JWT 而不是 Session?

  1. 客户端兼容性:Cookie 是浏览器的机制,原生 App(Android/iOS)对 Cookie 的支持不如浏览器友好,需要大量额外的代码去管理 Cookie 的生命周期;而 JWT 通过 HTTP Header 传输,对任何客户端(包括小程序、IoT 设备)都通用。

  2. 服务端无状态(核心):Session 需要服务端存储状态,在高并发场景下会增加服务器内存或 Redis 的压力。JWT 是无状态的,用户信息自包含在 Token 中,服务端只需验签,更易于水平扩展(扩充机器)。

开发过程中有遇到需求变更吗?怎么处理的?

“有的,印象最深的是语音对话功能的变更。

背景是这样: 我们的 AI 对话接口原本已经定稿了,只支持文本和图片文件,还有pdf解析。但后来医院那边提出,很多患者打字不方便,强烈要求增加发送语音的功能。

我的处理思路是: 我没有去重写一套‘语音对话’的独立接口,而是把语音视为一种预处理步骤。

  1. 接入语音识别模型: 我调研并接入了qwen3的paraformer-realtime-v2的语音转文字API。

  2. 增加适配层: 我在 Controller 层新增了一个处理音频的入口。当接收到音频文件(MultipartFile)时,先调用 语音转文字服务把它转录成 Text

  3. 逻辑复用: 拿到转录后的 Text 之后,我直接复用了之前写好的 chat(String text) 核心业务方法。

结果: 这样相当于给核心逻辑加了一个‘翻译官’,底层的 RAG 检索、对话记忆、SSE 流式响应这些复杂逻辑完全不用动,只花了一天时间就平滑支持了语音功能。”

你是怎么拆分产品的需求的

理解产品目标,划分核心模块

“产品给我们的需求是做一个’智能心理健康评估与咨询系统’。拿到需求后,我并没有急着写代码,之前听别人分享时说过,开发和架构分析设计应该花费相同的时间,一开始设计的系统的时候下功夫了,后面开发起来才不会一直因为一些细节问题频繁打断开发流程,我们经过仔细分析后,先把它拆成了 5 个核心业务模块:”

  • 用户体系:患者端 + 医生端,双角色设计。

  • 心理量表评估:标准化量表作答、自动评分与生成报告。

  • AI 智能咨询:基于大模型的心理对话核心功能。

  • 知识库系统:文档上传、向量化处理、RAG 检索增强。

  • 专家对接:专家信息管理和留言预约功能。

医生端主要负责上传知识库文档,还有管理知识库文档,还有管理心理测评的量纲。

患者端主要负责的是AI对话机器人,知识库搭建,文件和音频的解析等部分,我比较熟悉AI的开发,所以这块我来做,我的让另外一个同事去做一些用户信息校验,还有答题系统的开发,还有把集成测试的工作交给了他。

我们设计了一下后面数据库的表结构,还有一些代码细节,比如返回格式,异常处理,接口文档规范等等。

你们的部署细节是怎样的?

“为了保证各个服务在共享资源时互不干扰,我们采用了 Docker Compose 进行容器化编排。 核心策略是:在 Docker 层面设置’硬上限’,在应用层面设置’软上限’,并预留足够的 Buffer(缓冲空间)。

2. 具体配置策略

我主要针对 MySQL、Redis 和 SpringBoot 做了精细化的内存划分:

🐬 MySQL 配置(800MB 方案)

  • Docker 限制(硬限): 我们在 docker-compose.yml 中限制了 MySQL 容器最大内存为 800MB。如果超过这个值,容器会被宿主机的 OOM Killer 杀掉,保护整机稳定。

  • 内部配置(软限): 我将 InnoDB 的核心参数 innodb_buffer_pool_size 设置为 500MB

  • 设计思考

    “为什么留了 300MB 空隙?因为 MySQL 除了 Buffer Pool,每个连接线程(Thread Stack)、排序缓冲(Sort Buffer)以及系统库都需要内存。如果 Buffer Pool 占满 800MB,一来并发请求时容器必挂,二来没有空间给操作系统缓存。500MB 存数据,300MB 留给连接和系统,这是经过计算的安全比例。”

🔴 Redis 配置(256MB 方案)

  • Docker 限制: 容器内存限制为 256MB

  • 内部配置

    • Max Memory:在 redis.conf 中设置 maxmemory 200mb

    • 淘汰策略:配置 maxmemory-policy allkeys-lru

  • 设计思考

    “Redis 不是只存数据就行的,它还需要内存来处理客户端缓冲区、AOF 重写缓冲以及内存碎片。所以我**预留了 56MB(约 20%)的 Headroom(头部空间)**给这些后台任务。 同时,配合 LRU 策略,一旦数据量达到 200MB,自动剔除最近最少用的数据,保证 Redis 永远不会因为数据写满而撑破 Docker 的 256MB 限制导致崩溃。”

☕ SpringBoot 配置(512MB 方案)

  • JVM 参数: 设置 -Xms512m -Xmx512m

  • 设计思考

    “我们将初始堆(Xms)和最大堆(Xmx)设置为相等。 这样做有两个好处:第一,避免 JVM 在运行时动态调整堆大小带来的性能抖动;第二,明确告知 JVM 就用这么多,防止它贪婪申请内存导致容器 OOM。 配合 Docker 容器大约 600MB-700MB 的限制(堆外内存+MetaSpace),对于我们目前的测试量级,服务运行非常稳定。”

3. 结果验证

“通过这套内外结合的配置,在目前的测试环境下,我们从未出现过服务因为 OOM 被 Docker 强杀的情况,资源利用率也控制在非常合理的范围内。”

有经历过接口评审吗?

设计阶段:技术合同评审 (Design Review)

这是你写代码之前的阶段。如果你当时和队友讨论过“接口返回什么格式”、“Redis 存什么数据”,其实就已经在做评审了。

  • 关注点: 接口路径(URL)、请求参数(Input)、返回结构(Output)。

  • 你的情况: 你确定了用 List 结构存 Redis,用 max_tokens 限制长度等,这些都属于技术方案评审

开发阶段:代码走查 (Code Review)

这是在你把代码推送到服务器之前的自查或互查。

  • 关注点: 内存分配(-Xms / -Xmx)、数据库连接是否关闭、SQL 是否有性能问题。

  • 你的情况: 比如你考虑给 MySQL 分配 800MB 内存,给 Redis 限制 256MB,这就是在进行资源配额评审

交付阶段:业务与验收评审 (Acceptance Review)

当时医院的人看到我们生成的结果,发现了问题,我们输出的内容都没有问题,AI 返回的内容过于‘冰冷’且带有‘表格’,不符合患者的心理预期。我们生成的内容甚至直接告诉患者你大概得的是什么病,这是不好的,我们在这个对话中主要做的还是陪伴患者,给患者做心理疏导,最后医生可以通过总结患者对话来总结出患者到底是什么问题,后续进行人工介入。

ES你们是怎么部署的。

是的,ES 确实非常吃内存。如果为了我们这个新项目单独部署一套 ES,起步就要占用 4G+ 内存,在我们的共享服务器上不太现实,维护成本也高。

也就是在这个时候,我了解到公司内部(或组内)已经有一套正在运行的 ES 集群(可能是给日志系统或者旧搜索业务用的)。

所以我采取了逻辑隔离,物理共享的策略:

  1. 复用基础设施:直接连接现有的 ES 集群,省去了部署和运维成本。

  2. 索引隔离 (Index Isolation):我没有把数据混在一起,而是为我们的项目单独建立了专属的索引,命名为 psy_chat_vector_index

讲讲你们这个断点续传的接口实现。

问:能详细说说你是怎么实现的吗?

好的,我从整体架构说起。

传统方案的问题是:AI响应直接通过SSE推给客户端,一旦连接断开,数据就丢了。

我的方案是把AI响应和客户端推送解耦:

  1. 用户发起请求时,后端生成一个唯一的taskId返回给前端

  2. 后端异步调用AI,把每个响应块按顺序写入Redis List

  3. 前端用这个taskId建立SSE连接,后端从Redis读取数据推送

  4. 每个数据块都有一个索引id,前端记录最后收到的id

  5. 如果连接断开,前端带着taskId和lastId重连,后端从lastId+1的位置继续推送

 这样就实现了断点续传,用户刷新页面也不怕。

问:如果Redis挂了怎么办?

目前是单点Redis,如果挂了会影响服务。生产环境可以:

用Redis Sentinel或Cluster保证高可用

加一层本地缓存作为降级方案

或者直接降级到传统模式,不支持断点续传但基本功能可用

什么情况下会导致断点续传失败?

1. 正常运行(数据不会丢失)

场景 状态 原因及表现
网络闪断 localStorage 已保存当前状态,网络恢复后重连即可续传。
用户误刷新页面 页面加载时自动从 localStorage 读取并恢复进度。
关闭标签页后重新打开 localStorage 属于持久化存储,数据依然存在。
浏览器正常关闭再打开 只要不清理缓存,数据在物理磁盘上是安全的。

2. 异常与边界(可能失效)

场景 状态 风险点 解决方案
浏览器崩溃 (Crash) ⚠️ 最后几条指令可能留在内存中,未触发写入磁盘。 风险可控。丢失的通常只是崩溃瞬间前极短时间的数据。
隐私模式 / 无痕浏览 浏览器关闭后,系统会强制清空 localStorage。 在初始化阶段检测存储权限,提示用户切换到正常模式。
手动清理浏览器缓存 用户主动清空 Cookie 及网站数据,导致物理删除。 属于用户主动行为,通常无需技术处理。
Redis 数据过期 后端设定的 TTL(如1小时)到期,导致数据被剔除。 延长过期时间,或在过期前提示用户任务即将超时。
后端/容器重启 Redis 若开启了 AOF/RDB 持久化,数据不会丢失。 确保 Redis 配置了基础的持久化策略。

断点续传流程图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
页面加载
    
    
从localStorage读取保存的状态
    
    ├── 没有保存的状态  正常显示等待用户发起新对话
    
    └── 有保存的状态taskId, lastId, content
            
            
        显示已接收的内容用户立即看到之前的文字
            
            
        调用 GET /chat/status/{taskId} 检查任务状态
            
            ├── 任务不存在404)→ 清除localStorage提示已过期
            
            ├── 任务已完成completed=true)→ 显示完整内容清除状态
            
            └── 任务进行中completed=false
                    
                    
                自动调用 GET /chat/stream/{taskId}?lastId=N
                    
                    
                从lastId+1位置继续接收数据

你们是怎么监控大模型 Token 用量的?

我们接入的是阿里云通义千问模型,Token 是按量计费的。测试阶段遇到了几个痛点:不知道每天消耗多少 Token,测试完才发现账单超预期;简单说就是:看不见、算不清、管不住

我对比了几个方案,自己写日志分析太简单但不实时,ELK 功能强但太重运维成本高,云厂商监控开箱即用但贵且和厂商绑定。最终选了 Prometheus + Grafana,原因是 Spring Boot Actuator + Micrometer 原生支持,几行配置就能暴露指标;Prometheus 专为时序指标设计,查询性能好;Grafana 图表丰富还能配置告警;而且这是云原生领域的事实标准,学习成本低。

“具体实现分三步:

第一步:埋点采集

  • 利用 Spring AI 的 ObservationHandler 机制,在每次 AI 调用完成后采集指标
  • 自定义了核心指标:Token 用量(输入/输出)、请求耗时、并发数、费用估算、错误率
  • 我们的 RAG 系统涉及三类模型,我按模型类型打标签分别统计:Chat 模型(对话生成,单价最高)、Embedding 模型(向量化,调用频次最高)、Rerank 模型(重排序,按次计费)

第二步:存储展示

  • Prometheus 每 5 秒抓取一次指标,存储时序数据
  • Grafana 做可视化 Dashboard,能看到 Token 消耗趋势、响应时间分布、成本曲线

第三步:告警通知

  • 配置 Alertmanager,Token 超过阈值自动发邮件
  • 用时间窗口告警,比如 increase(ai_token_total[1h]) > 3000,监控每小时增量”

在测试环境验证下来,这套方案能清晰看到每次测试的 Token 消耗和费用估算,通过响应时间指标也能快速定位是模型慢还是代码问题。

可能的追问

Q: RAG 系统里哪个模型消耗最大?

“从 Token 数量看,Embedding 模型调用最频繁,因为每次用户提问都要向量化,每次文档入库也要向量化。但从费用看,Chat 模型单价最高,占总成本 60% 以上。Rerank 模型虽然每次调用 Token 不多,但它是按调用次数计费的,也需要单独监控。”

Q: Prometheus 和日志系统有什么区别?

“Prometheus 是时序数据库,存的是数值指标,比如’发生了多少次、用了多少量’;日志系统存的是文本,记录’具体发生了什么’。两者互补:指标告诉你’有问题’,日志告诉你’什么问题’。”

Licensed under CC BY-NC-SA 4.0