这次二面主要考察了什么?
面试围绕四个方向展开:
- 实习时间:12月29日到岗,持续6个月以上
- 项目深挖:EduAgentX(比赛)、心理医生知识库(实习)
- 中间件原理:Redis/RPC 序列化
- 手写算法:求N次方根(二分法/牛顿迭代)
面试官问了很多细节(JVM参数、Redis内存、数据量),这些我都没做过怎么办?
这叫"工程化细节拷打",考察项目是"玩具"还是"产品"。需要准备合理的参数和回答逻辑。
JVM 启动参数(JDK 17)
|
|
JDK 17 默认 G1 GC,适合大内存服务器,主要依赖 G1 的自适应能力。
Redis 内存规划
申请 8GB 是基于容量预估:
- 对话上下文(List):单条 1KB,几万条才几十 MB,设置 10分钟 TTL
- 预留 Buffer:避免后续频繁扩容
数据库文件大小
查看方式:
|
|
估算:1万条 × 1KB ≈ 10MB,加碎片约 50-100MB。256GB 内存服务器,数据库完全可加载进内存。
Redis序列化和Dubbo RPC序列化不懂怎么办?
Redis 序列化
Java 对象存入 Redis 必须序列化,Redis 只认字节/字符串。
| 方式 | 特点 | 场景 |
|---|---|---|
| JDK 序列化 | 字节流长,不可读 | 不推荐 |
| JSON 序列化 | 可读性强,方便调试 | 推荐 |
| Protobuf | 体积小,解析快 | 高并发微服务 |
话术:选 JSON 是因为需要在 Redis Desktop Manager 里直接看到清晰的 JSON 结构排查问题,可读性和开发效率优先。
RPC Protobuf 机制
之前的错误理解:.proto 文件在运行时传输
正确理解:
.proto是 IDL(接口定义语言),是服务契约- 编译期:
protoc生成 Java 代码,服务端和客户端都依赖这份代码 - 运行时:只传序列化后的二进制流,不传文件
- 优势:TLV 编码,不传字段名只传字段编号,体积比 JSON 小很多
话术:Redis 侧选 JSON(可读性第一),RPC 侧选 Protobuf(性能第一)。
应用程序和数据库部署在一块吗?数据库文件多大?
部署架构
单机部署是合理选择:
- 降低网络延迟(Localhost 通信比走网卡快)
- 方便维护
- Docker 容器进行逻辑隔离
话术:
“数据库文件只有几十 MB,服务器有 256GB 内存。操作系统 Page Cache 和 MySQL Buffer Pool 会将热点数据完全缓存,磁盘 I/O 不是瓶颈,所有查询本质上都是纯内存操作。”
大模型应用怎么做评测?没做过指标评测怎么办?
把"没做过"变成"我知道该怎么做"。
双层评测框架
| 维度 | 方法 | 指标 |
|---|---|---|
| 准确性 | 离线评估 + 黄金数据集 | Hit Rate, Faithfulness |
| 性能 | 用户体验延迟 | TTFT < 1.5s, Total < 15s |
| 反馈 | 专家验收 + 人在回路 | 采纳率, 专家评分 |
RAG 评测指标(RAGAS 框架)
检索层:
- Hit Rate@K:正确文档是否在前K个结果中
- MRR:正确文档的排名位置
生成层:
- Faithfulness(忠实度):答案是否基于检索文档,无幻觉
- Answer Relevance(相关性):是否答非所问
- Context Precision(精确度):检索内容的信噪比
性能评测
- TTFT (Time To First Token):首字延迟,目标 < 1.5秒
- E2E Latency:端到端耗时,目标 < 15秒
话术:
“当时受限于实习时间主要靠人工验收。如果复盘,我会引入 RAGAS 框架,重点考核 Faithfulness(防幻觉)和 Answer Relevance(防答非所问)。”
比赛项目没有用户反馈,怎么证明生成结果有用?
第一层防御:比赛本身就是测试
评委和竞争对手就是"种子用户",评委充当"领域专家"角色,给出定性反馈。
第二层防御:Human-in-the-Loop 机制
AI 生成只是初稿,老师的修改才是最终修正:
- 教师点击"采纳" = 正向评分
- 教师修改/重新生成 = 负向反馈
第三层防御:黄金数据集
找教材目录和课后习题作为 Ground Truth,人工核查知识点覆盖率。
话术:
“采取’离线评估 + 专家抽检’策略。以经典教材为基准确保知识点不跑偏,设计 Human-in-the-Loop 机制让老师把关最后一道防线。AI 负责提供灵感,人类负责把控质量。”
内部测试是怎么做的?怎么说是用公司服务器测试的?
把"宝塔面板"翻译成"Docker + Nginx + Shell脚本"。
环境搭建
“基于 Docker 的单机微服务部署。Docker Compose 编排中间件(MySQL、Redis、ES、Nacos),应用打包成 JAR 构建镜像,Volume 挂载日志目录。”
接口暴露
“Nginx 反向代理转发请求,集成 Swagger/Knife4j 提供接口文档,组员通过内网 IP 访问。”
代码更新
“写了简单的 Shell 脚本:git pull → mvn package → docker restart,实现半自动化部署。”
服务挂了怎么办
“Docker 启动参数配置了
--restart=always,守护进程会自动拉起崩溃的服务。”
怎么看日志
“登录服务器,去挂载的日志目录用
grep搜 Error,或tail -f实时盯日志配合前端复现 Bug。”
怎么体现做过接口评测?
把"开会演示"包装成"UAT 验收测试"。
演示前性能优化故事
“演示前冒烟测试发现历史记录接口响应超过 2秒。排查发现是深分页问题,引入时间游标分页 + 复合索引,响应时间从 2.1s 降至 0.7s。演示时业务方快速滑动历史记录非常丝滑。”
演示中实时监控
“演示过程中通过 SSH 连接服务器,
tail -f查看日志,top监控资源。观察 SSE 连接保持情况,验证心跳机制和断点续传逻辑正常工作。”
话术:
“业务方的现场验收就是最真实的评测。能扛住演示时的随机操作和现场网络环境,比单纯跑 Benchmark 更有说服力。”
AI生成内容的评测怎么做?医院有反馈过问题
医院的反馈就是最好的"UAT 验收测试"证据。
问题1:语气冰冷,像知识问答
痛点:直接把知识点甩给病人,缺乏共情,甚至输出表格
解决方案:
- 强化 System Prompt 角色设定:“你是有10年经验的心理咨询师,用温暖包容的口吻对话”
- Few-Shot Learning:找医生要 3-5 个优秀医患对话案例作为示例
结果:AI 会先说"我理解您的焦虑",再给建议
问题2:输出大量文本,Token 失控
痛点:一次性输出几百字"小作文",病人没耐心看
解决方案:
- 限制
max_tokens参数(300 token 以内) - Prompt 约束:“不要一次性解决所有问题,每次回复控制在3句话以内”
结果:AI 能一句一句有来有回,还节省了 40% Token 成本
话术:
“早期 Demo 阶段,医院医生提出 AI ‘太冷漠’且’废话太多’。我通过 Prompt Few-Shot 技术解决共情问题,通过 Token 限制和对话策略优化解决长文本问题。最终版本经医生试用通过。”
心理医生平台还会遇到什么问题?
安全风控(最致命)
危机干预熔断:
- 用户输入"我不想活了"时,必须熔断
- 请求发给大模型前,先经过关键词/语义分类器
- 检测到高危意图,立即绕过大模型,输出危机干预话术并触发人工介入
诱导性回复拦截:
- System Prompt 加入安全围栏
- 接入内容安全 API 二次校验
技术架构
长期记忆丢失:
- Redis List 只存最近几十条,一个月前的信息丢失
- 解决:Memory Summarization,每天对话结束后异步任务浓缩成"侧写"存入向量数据库
语义检索不准:
- 用户描述隐晦:“心里堵得慌”
- 解决:Query Rewrite,让大模型把口语翻译成标准医学术语再检索
隐私合规
PII 过滤器:
- 调用大模型前,正则 + NLP 工具将手机号、姓名替换为占位符
- 确保核心隐私数据不出域
话术:
“如果继续做,安全性和长期记忆是两个最大瓶颈。安全性上增加危机干预熔断机制,记忆上引入每日摘要功能解决长周期咨询问题。”
正常实习写代码的流程是什么样的?
写代码之前
- 需求评审:听懂需求,评估技术可行性
- 技术方案设计:写 TDD(接口定义、表结构、流程图)
- 方案评审:Mentor 审核技术选型
写代码之中
- 分支管理:基于最新代码拉
feature/xxx分支 - 单元测试:JUnit/Mockito,覆盖率 60%-80%
- 代码规范:Checkstyle/Sonar 扫描
提交代码
- 发起 MR/PR
- Code Review:逐行审查,打回修改 3-5 次正常
- 合并主干
测试上线
- 测试环境验收:QA 提 Bug 单
- 灰度发布:先发一台观察 CPU/内存/日志,再全量
- 监控告警:配置日志报警
话术:
“上一段实习团队规模较小,我主要锻炼了独立负责项目全生命周期的能力。但我也意识到在大型团队协作流程(Code Review、单元测试、CI/CD)上还缺乏实战经验,非常渴望加入规范的团队来补齐这块拼图。”
面试暴露的所有问题清单
算法基础(最致命)
- 求N次方根无法解出,放弃过早
- 需掌握:二分查找、牛顿迭代法
底层原理(概念混淆)
- RPC Protobuf 机制理解错误(以为运行时传 .proto 文件)
- 序列化选择逻辑薄弱
- Redis 数据结构理解不深
工程化细节(盲区最大)
- 不知道 JVM 启动参数
- 不知道 Redis 实际内存占用
- 不知道数据库文件大小
- 部署架构辩护不足
项目闭环(产品思维缺失)
- 缺乏量化指标(QPS、TP99、准确率)
- 缺乏用户反馈机制
沟通软技能
- 过度推卸责任(“不是我负责的"说太多)
- 主动暴露项目缺陷却没有补救方案
简历策略调整
把"水分较多"的实习经历转为"项目经历"展示,避免被问流程细节。
如果被问到:
“这其实是我在暑期实习期间主要负责的落地项目。因为团队规模精简,我拥有很大的技术自主权,作为核心开发者完整主导了从 RAG 架构设计到 Redis 异步通信的落地。虽然是实习期间做的,但我对底层逻辑和技术细节掌握得非常清楚。”
好处:
- 诚实承认是实习背景
- 把"公司小/流程水"转化为"权限大/独立负责/全栈锻炼”
Dubbo3 进行 RPC 调用经历了什么?
消费端(Consumer)—— 决策与封装
-
代理拦截 (Proxy):业务代码调用接口,被动态代理拦截,封装成
Invocation对象 -
集群容错 (Cluster):决定失败策略(Failover/Failfast),掩盖多个 Provider 的事实
-
服务目录 (Directory):从注册中心获取服务列表,Dubbo 3 采用应用级服务发现
-
路由选路 (Router):根据路由规则过滤机器(如标签路由、同机房优先)
-
负载均衡 (LoadBalance):从过滤后的列表选出一台(Random/RoundRobin/LeastActive/ConsistentHash)
-
过滤器链 (Filter):传递 Context、监控埋点、限流降级
-
协议与序列化:Triple 协议封装为 HTTP/2 Frame,默认 Protobuf 序列化
网络传输
- Netty 通过 TCP/IP 发送到 Provider
- Triple 协议支持 Stream 流式调用和背压
服务端(Provider)—— 接收与执行
-
解码与线程池派发:Netty 解码,请求扔给业务线程池
-
服务端过滤器:鉴权、超时检查、限流、解压 Context
-
真实调用:通过反射调用 ServiceImpl 方法
-
结果返回:序列化 -> 编码 -> Netty 发回 Consumer
关键路径记忆
|
|
Dubbo3 是怎么做序列化的?
两大流派
| 流派 | 协议 | 默认序列化 | 特点 |
|---|---|---|---|
| 经典流派 | Dubbo 协议 (TCP) | Hessian2 → Fastjson2 | Java 内部调用 |
| 云原生流派 | Triple 协议 (HTTP/2) | Protobuf | 跨语言、网关穿透 |
Dubbo 协议处理流程
- Consumer 将 POJO 通过 Hessian2/Fastjson2 序列化成字节数组
- 填充到 Dubbo TCP 报文的 Body 部分
- Provider 读取 Header 中的序列化 ID,找到反序列化器还原对象
Triple 协议两种模式
Wrapper 模式(兼容 Java 接口):
- 第一层:Hessian2 序列化业务对象
- 第二层:封装进 Protobuf 对象(TripleRequestWrapper)
- 缺点:双重序列化,性能有损耗
IDL 模式(原生 gRPC):
- 直接使用 Protobuf 序列化
- 性能最高,天然支持跨语言
配置方式
|
|
Dubbo 协议相比 Triple 协议差在哪?
| 维度 | Dubbo 协议 (TCP) | Triple 协议 (HTTP/2) |
|---|---|---|
| 穿透网关 | 差(私有协议) | 极佳(标准 HTTP/2) |
| 跨语言 | 困难 | 原生支持 |
| 流式通信 | 不支持 | 原生支持 (gRPC) |
| 性能 (直连) | 极高 | 较高 |
| Service Mesh | 难以治理 | 完美兼容 |
选择建议:
- 老系统、纯 Java 内部调用、追求极致速度 → Dubbo 协议
- 新业务、跨语言、上云上 Mesh → Triple 协议
.proto 文件什么时候交换?新服务上线多久能感知?
.proto 文件交换时机
答案:编译期,而非运行期
- 交换方式:Git 仓库或 Maven 依赖(api-jar)
- 原因:Protobuf 传输时完全剔除元数据(字段名、类型),客户端必须提前拿到 .proto 生成代码
新服务感知延迟
答案:通常 1 秒以内(Nacos 2.x),最慢 3-5 秒
流程(以 Nacos 为例):
- Provider 启动,向 Nacos 发送注册请求 (T0)
- Nacos 更新注册表 (T0 + 10ms)
- Nacos 通过 gRPC 长连接推送给 Consumer (T0 + 20ms)
- Consumer 更新本地服务目录 (T0 + 50ms)
- 下一个请求可能选中新机器 (T0 + 100ms)
HTTP/2 的序列化一般基于什么?
HTTP/2 本身不限制序列化方式,Body 里装什么都可以。
常见搭配
| 序列化 | 场景 | 说明 |
|---|---|---|
| JSON | Web 领域 | 浏览器访问后端,可读性好 |
| Protobuf | 微服务/RPC | gRPC/Dubbo3,体积小解析快 |
| Hessian2 | Dubbo 兼容 | Triple + Wrapper 模式 |
HTTP/2 + JSON 的优势是否发挥?
你说得对,JSON 确实拖后腿:
- 编解码 CPU 浪费(文本解析 vs 二进制复制)
- 体积膨胀(冗余字段名)
但 HTTP/2 核心优势依然生效:
- 多路复用:50 个请求瞬间并发,不再排队
- 头部压缩 (HPACK):Header 可能比 Body 还大,压缩 80%-90%
- 连接复用:避免 TCP 握手和慢启动
结论:
- 浏览器 → 服务器:HTTP/2 + JSON 是最佳实践
- 服务器 → 服务器:请用 HTTP/2 + Protobuf
介绍 Hessian2
核心特点
- 二进制传输:体积比 JSON 小
- Java 友好:无需 IDL,实现 Serializable 即可
- 性能均衡:比 Java 原生序列化快 10 倍,体积小 50%
三大坑
- Java 8 时间类型支持差:LocalDateTime 可能变 null 或 HashMap
- 方法重载混乱:Dubbo 接口严禁方法重载
- 父类字段丢失:父类未实现 Serializable 时行为不一致
对比
| 特性 | Hessian2 | Protobuf | JSON |
|---|---|---|---|
| 开发模式 | 代码优先 | IDL 优先 | 代码优先 |
| 性能 | 中等偏上 | 极高 | 中等 |
| 跨语言 | 差 | 完美 | 完美 |
| 可读性 | 不可读 | 不可读 | 可读 |
Hessian2 生成的字节流是怎样的?
结构:类定义 + 实例数据
假设 new User(1, "dubbo"):
第一部分:类定义
|
|
第二部分:实例数据
|
|
比 JSON 强在哪?
发送 100 个 User 对象:
- JSON:
"id"和"name"重复写 100 次 - Hessian2:类定义只发一次,后续只发值
比 Protobuf 弱在哪?
- Protobuf:连类名和字段名都不发(双方代码里已生成)
- Hessian2:第一次传输必须带元数据(为了不写 .proto 的代价)
Hessian2 每次服务器重启都要发送类结构吗?
答案:不是每次重启,而是每次 RPC 请求都重新发送
原因:请求级上下文
Hessian2 的"字典"是请求级的,不是连接级的:
- 每次 RPC 调用创建新的
Hessian2ObjectOutput - 保证无状态,请求可以打到任何机器
为什么不能跨请求复用?
- 网络抖动导致 TCP 断开重连
- 负载均衡转发到另一台机器
- 新机器不知道"定义 #0"是什么
感知重启的机制
- TCP 连接断开:Netty 监听
channelInactive事件 - 心跳机制:默认 60 秒心跳,3 次无响应判定死亡
- 注册中心通知:Nacos 推送下线事件
.proto 生成的 Java 代码是干什么的?
三件大事
- 造数据(DTO):超级数据传输对象
- 转数据(序列化):硬编码的位运算逻辑
- 传数据(RPC 桩):客户端和服务端对接
消息类代码特点
强制 Builder 模式:
|
|
- 不可变性:build() 后无法修改,多线程安全
- 链式调用:代码流畅
丰富的访问器:
getId(),hasAddress(),getRolesList(),getRolesCount()
序列化逻辑(硬编码)
|
|
优势:
- 去掉反射,运行时不查找字段
- 直接操作字节位,CPU 指令数最少
- 自动 Varint 压缩,小数字只占 1 字节
服务类代码(Stub)
方法描述符:
|
|
客户端调用:
|
|
gRPC 的工作流程总结
三步走
-
定义数据结构:编写
.proto文件(语言中立的公约) -
生成转换代码:
protoc编译器翻译成各语言代码- Java →
User.java+UserServiceGrpc.java - Go →
user.pb.go+user_grpc.pb.go
- Java →
-
调用生成代码:像调本地方法一样顺滑
1stub.getUser(request); // 感觉不到网络存在
跨语言示例
|
|
Java 客户端:
|
|
Go 服务端:
|
|
生成代码做的 4 件事
- 数据校验与存储:
setId(1)存入内存字段 - 二进制打包:
writeTo()转换成紧凑字节流 - 路由填单:定义
MethodDescriptor常量 - 交给运输队:
ClientCalls.blockingUnaryCall交给 Netty/HTTP2
一句话总结:根据 Java 对象,利用硬编码逻辑压缩进二进制缓冲区,贴上路由标签,交给 gRPC 核心层通过 HTTP/2 发送。