二面复盘笔记

这次二面主要考察了什么?

面试围绕四个方向展开:

  1. 实习时间:12月29日到岗,持续6个月以上
  2. 项目深挖:EduAgentX(比赛)、心理医生知识库(实习)
  3. 中间件原理:Redis/RPC 序列化
  4. 手写算法:求N次方根(二分法/牛顿迭代)

面试官问了很多细节(JVM参数、Redis内存、数据量),这些我都没做过怎么办?

这叫"工程化细节拷打",考察项目是"玩具"还是"产品"。需要准备合理的参数和回答逻辑。

JVM 启动参数(JDK 17)

1
-Xms4g -Xmx4g  # 初始堆和最大堆设为一致,避免内存抖动

JDK 17 默认 G1 GC,适合大内存服务器,主要依赖 G1 的自适应能力。

Redis 内存规划

申请 8GB 是基于容量预估:

  • 对话上下文(List):单条 1KB,几万条才几十 MB,设置 10分钟 TTL
  • 预留 Buffer:避免后续频繁扩容

数据库文件大小

查看方式:

1
2
SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024, 2) AS "Size (MB)"
FROM information_schema.TABLES WHERE table_schema = "数据库名";

估算: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:语气冰冷,像知识问答

痛点:直接把知识点甩给病人,缺乏共情,甚至输出表格

解决方案

  1. 强化 System Prompt 角色设定:“你是有10年经验的心理咨询师,用温暖包容的口吻对话”
  2. Few-Shot Learning:找医生要 3-5 个优秀医患对话案例作为示例

结果:AI 会先说"我理解您的焦虑",再给建议

问题2:输出大量文本,Token 失控

痛点:一次性输出几百字"小作文",病人没耐心看

解决方案

  1. 限制 max_tokens 参数(300 token 以内)
  2. Prompt 约束:“不要一次性解决所有问题,每次回复控制在3句话以内”

结果:AI 能一句一句有来有回,还节省了 40% Token 成本

话术

“早期 Demo 阶段,医院医生提出 AI ‘太冷漠’且’废话太多’。我通过 Prompt Few-Shot 技术解决共情问题,通过 Token 限制和对话策略优化解决长文本问题。最终版本经医生试用通过。”


心理医生平台还会遇到什么问题?

安全风控(最致命)

危机干预熔断

  • 用户输入"我不想活了"时,必须熔断
  • 请求发给大模型前,先经过关键词/语义分类器
  • 检测到高危意图,立即绕过大模型,输出危机干预话术并触发人工介入

诱导性回复拦截

  • System Prompt 加入安全围栏
  • 接入内容安全 API 二次校验

技术架构

长期记忆丢失

  • Redis List 只存最近几十条,一个月前的信息丢失
  • 解决:Memory Summarization,每天对话结束后异步任务浓缩成"侧写"存入向量数据库

语义检索不准

  • 用户描述隐晦:“心里堵得慌”
  • 解决:Query Rewrite,让大模型把口语翻译成标准医学术语再检索

隐私合规

PII 过滤器

  • 调用大模型前,正则 + NLP 工具将手机号、姓名替换为占位符
  • 确保核心隐私数据不出域

话术

“如果继续做,安全性和长期记忆是两个最大瓶颈。安全性上增加危机干预熔断机制,记忆上引入每日摘要功能解决长周期咨询问题。”


正常实习写代码的流程是什么样的?

写代码之前

  1. 需求评审:听懂需求,评估技术可行性
  2. 技术方案设计:写 TDD(接口定义、表结构、流程图)
  3. 方案评审:Mentor 审核技术选型

写代码之中

  1. 分支管理:基于最新代码拉 feature/xxx 分支
  2. 单元测试:JUnit/Mockito,覆盖率 60%-80%
  3. 代码规范:Checkstyle/Sonar 扫描

提交代码

  1. 发起 MR/PR
  2. Code Review:逐行审查,打回修改 3-5 次正常
  3. 合并主干

测试上线

  1. 测试环境验收:QA 提 Bug 单
  2. 灰度发布:先发一台观察 CPU/内存/日志,再全量
  3. 监控告警:配置日志报警

话术

“上一段实习团队规模较小,我主要锻炼了独立负责项目全生命周期的能力。但我也意识到在大型团队协作流程(Code Review、单元测试、CI/CD)上还缺乏实战经验,非常渴望加入规范的团队来补齐这块拼图。”


面试暴露的所有问题清单

算法基础(最致命)

  • 求N次方根无法解出,放弃过早
  • 需掌握:二分查找、牛顿迭代法

底层原理(概念混淆)

  • RPC Protobuf 机制理解错误(以为运行时传 .proto 文件)
  • 序列化选择逻辑薄弱
  • Redis 数据结构理解不深

工程化细节(盲区最大)

  • 不知道 JVM 启动参数
  • 不知道 Redis 实际内存占用
  • 不知道数据库文件大小
  • 部署架构辩护不足

项目闭环(产品思维缺失)

  • 缺乏量化指标(QPS、TP99、准确率)
  • 缺乏用户反馈机制

沟通软技能

  • 过度推卸责任(“不是我负责的"说太多)
  • 主动暴露项目缺陷却没有补救方案

简历策略调整

把"水分较多"的实习经历转为"项目经历"展示,避免被问流程细节。

如果被问到

“这其实是我在暑期实习期间主要负责的落地项目。因为团队规模精简,我拥有很大的技术自主权,作为核心开发者完整主导了从 RAG 架构设计到 Redis 异步通信的落地。虽然是实习期间做的,但我对底层逻辑和技术细节掌握得非常清楚。”

好处

  • 诚实承认是实习背景
  • 把"公司小/流程水"转化为"权限大/独立负责/全栈锻炼”

Dubbo3 进行 RPC 调用经历了什么?

消费端(Consumer)—— 决策与封装

  1. 代理拦截 (Proxy):业务代码调用接口,被动态代理拦截,封装成 Invocation 对象

  2. 集群容错 (Cluster):决定失败策略(Failover/Failfast),掩盖多个 Provider 的事实

  3. 服务目录 (Directory):从注册中心获取服务列表,Dubbo 3 采用应用级服务发现

  4. 路由选路 (Router):根据路由规则过滤机器(如标签路由、同机房优先)

  5. 负载均衡 (LoadBalance):从过滤后的列表选出一台(Random/RoundRobin/LeastActive/ConsistentHash)

  6. 过滤器链 (Filter):传递 Context、监控埋点、限流降级

  7. 协议与序列化:Triple 协议封装为 HTTP/2 Frame,默认 Protobuf 序列化

网络传输

  • Netty 通过 TCP/IP 发送到 Provider
  • Triple 协议支持 Stream 流式调用和背压

服务端(Provider)—— 接收与执行

  1. 解码与线程池派发:Netty 解码,请求扔给业务线程池

  2. 服务端过滤器:鉴权、超时检查、限流、解压 Context

  3. 真实调用:通过反射调用 ServiceImpl 方法

  4. 结果返回:序列化 -> 编码 -> Netty 发回 Consumer

关键路径记忆

1
Proxy -> Cluster -> Directory -> Router -> LoadBalance -> Filter -> Codec/Transport -> ThreadPool -> Implementation

Dubbo3 是怎么做序列化的?

两大流派

流派 协议 默认序列化 特点
经典流派 Dubbo 协议 (TCP) Hessian2 → Fastjson2 Java 内部调用
云原生流派 Triple 协议 (HTTP/2) Protobuf 跨语言、网关穿透

Dubbo 协议处理流程

  1. Consumer 将 POJO 通过 Hessian2/Fastjson2 序列化成字节数组
  2. 填充到 Dubbo TCP 报文的 Body 部分
  3. Provider 读取 Header 中的序列化 ID,找到反序列化器还原对象

Triple 协议两种模式

Wrapper 模式(兼容 Java 接口)

  • 第一层:Hessian2 序列化业务对象
  • 第二层:封装进 Protobuf 对象(TripleRequestWrapper)
  • 缺点:双重序列化,性能有损耗

IDL 模式(原生 gRPC)

  • 直接使用 Protobuf 序列化
  • 性能最高,天然支持跨语言

配置方式

1
2
3
4
dubbo:
  protocol:
    name: tri  # 或 dubbo
    serialization: fastjson2

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 为例):

  1. Provider 启动,向 Nacos 发送注册请求 (T0)
  2. Nacos 更新注册表 (T0 + 10ms)
  3. Nacos 通过 gRPC 长连接推送给 Consumer (T0 + 20ms)
  4. Consumer 更新本地服务目录 (T0 + 50ms)
  5. 下一个请求可能选中新机器 (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%

三大坑

  1. Java 8 时间类型支持差:LocalDateTime 可能变 null 或 HashMap
  2. 方法重载混乱:Dubbo 接口严禁方法重载
  3. 父类字段丢失:父类未实现 Serializable 时行为不一致

对比

特性 Hessian2 Protobuf JSON
开发模式 代码优先 IDL 优先 代码优先
性能 中等偏上 极高 中等
跨语言 完美 完美
可读性 不可读 不可读 可读

Hessian2 生成的字节流是怎样的?

结构:类定义 + 实例数据

假设 new User(1, "dubbo")

第一部分:类定义

1
2
3
4
C (0x43)                    -> 类定义开始
"com.example.User"          -> 类全限定名
2 (0x92)                    -> 字段数量
"id", "name"                -> 字段名

第二部分:实例数据

1
2
3
4
O (0x4f)                    -> 对象标记
0 (0x90)                    -> 引用第 0 号类定义
1 (0x91)                    -> id 的值(紧凑写法,1 字节)
0x05 + "dubbo"              -> name 的值

比 JSON 强在哪?

发送 100 个 User 对象:

  • JSON"id""name" 重复写 100 次
  • Hessian2:类定义只发一次,后续只发值

比 Protobuf 弱在哪?

  • Protobuf:连类名和字段名都不发(双方代码里已生成)
  • Hessian2:第一次传输必须带元数据(为了不写 .proto 的代价)

Hessian2 每次服务器重启都要发送类结构吗?

答案:不是每次重启,而是每次 RPC 请求都重新发送

原因:请求级上下文

Hessian2 的"字典"是请求级的,不是连接级的:

  • 每次 RPC 调用创建新的 Hessian2ObjectOutput
  • 保证无状态,请求可以打到任何机器

为什么不能跨请求复用?

  • 网络抖动导致 TCP 断开重连
  • 负载均衡转发到另一台机器
  • 新机器不知道"定义 #0"是什么

感知重启的机制

  1. TCP 连接断开:Netty 监听 channelInactive 事件
  2. 心跳机制:默认 60 秒心跳,3 次无响应判定死亡
  3. 注册中心通知:Nacos 推送下线事件

.proto 生成的 Java 代码是干什么的?

三件大事

  1. 造数据(DTO):超级数据传输对象
  2. 转数据(序列化):硬编码的位运算逻辑
  3. 传数据(RPC 桩):客户端和服务端对接

消息类代码特点

强制 Builder 模式

1
2
3
UserRequest request = UserRequest.newBuilder()
    .setId(1001)
    .build();
  • 不可变性:build() 后无法修改,多线程安全
  • 链式调用:代码流畅

丰富的访问器

  • getId(), hasAddress(), getRolesList(), getRolesCount()

序列化逻辑(硬编码)

1
2
3
4
5
6
public void writeTo(Output output) {
    // 不需要反射,直接写字节
    output.writeRawByte(0x08);  // Tag
    output.writeRawByte(0xAC);  // Value (Varint 编码)
    output.writeRawByte(0x02);
}

优势

  • 去掉反射,运行时不查找字段
  • 直接操作字节位,CPU 指令数最少
  • 自动 Varint 压缩,小数字只占 1 字节

服务类代码(Stub)

方法描述符

1
2
3
4
5
MethodDescriptor<UserRequest, UserResponse> GET_USER_METHOD = 
    MethodDescriptor.newBuilder()
    .setFullMethodName("com.example.UserService/GetUser")
    .setRequestMarshaller(...)
    .build();

客户端调用

1
2
3
4
public UserResponse getUser(UserRequest request) {
    ClientCall call = channel.newCall(GET_USER_METHOD, callOptions);
    return ClientCalls.blockingUnaryCall(call, request);
}

gRPC 的工作流程总结

三步走

  1. 定义数据结构:编写 .proto 文件(语言中立的公约)

  2. 生成转换代码protoc 编译器翻译成各语言代码

    • Java → User.java + UserServiceGrpc.java
    • Go → user.pb.go + user_grpc.pb.go
  3. 调用生成代码:像调本地方法一样顺滑

    1
    
    stub.getUser(request);  // 感觉不到网络存在
    

跨语言示例

1
2
3
// 共用一份 .proto
message HelloRequest { string name = 1; }
service Greeter { rpc SayHello (HelloRequest) ... }

Java 客户端

1
stub.sayHello(HelloRequest.newBuilder().setName("Lucius").build());

Go 服务端

1
2
3
func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) {
    fmt.Println(req.Name)  // 输出 "Lucius"
}

生成代码做的 4 件事

  1. 数据校验与存储setId(1) 存入内存字段
  2. 二进制打包writeTo() 转换成紧凑字节流
  3. 路由填单:定义 MethodDescriptor 常量
  4. 交给运输队ClientCalls.blockingUnaryCall 交给 Netty/HTTP2

一句话总结:根据 Java 对象,利用硬编码逻辑压缩进二进制缓冲区,贴上路由标签,交给 gRPC 核心层通过 HTTP/2 发送。

Licensed under CC BY-NC-SA 4.0