面灵AI→

美的全栈开发一面:自研Agent与Spring原理

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 自我介绍
  2. Agent 是自研的,还是用 LangGraph 做的?
  3. 为什么选择自研 Agent 框架?
  4. Harness 是什么?
  5. Harness 具体治理哪些内容?你怎么做的?
  6. 百万级数据有没有做分库分表?
  7. 实时排行榜用什么技术实现?
  8. Redis 的排行榜数据丢失了,怎么恢复?
  9. 同步靠定时任务,还是消息通知?
  10. 摘要的输入、输出是什么?提示词怎么写?
  11. 智能摘要具体解决什么问题?
  12. 你如何选择 Python 或 Java?
  13. Spring Boot 自动装配的原理是什么?
  14. Bean 的生命周期大致是什么?
  15. Spring AOP 怎么实现?
  16. MySQL 的 B+ 树索引有什么优缺点?
  17. 百万级数据做分页,遇到了什么性能瓶颈?
  18. 总数缓存在 Redis 中,多久更新一次?
  19. 哪段项目技术含量最高,或者让你成长最多?
  20. 在学校学习与实习中学习,有什么区别和体会?

反问:部门业务、后续流程。

《参考解析》

为什么要自研 Agent 框架:这题一定要给具体动机,不能只说「可控」。可以按四类理由组织:一是编排需求特殊——业务链路需要「固定步骤 + 模型决策」混合,LangGraph 这类图式框架表达条件分支和重试不够灵活,或者反过来,我们的流程太简单、引入整套依赖得不偿失;二是可观测与治理——需要按业务维度记录每次调用的 token 消耗、工具调用轨迹、失败原因并做成本归集,通用框架的埋点粒度不够,自己写能精确控制;三是模型与渠道管理——需要多模型路由、降级、限流、缓存复用(前缀缓存)和密钥池管理,这些与公司内部基础设施强耦合;四是工程约束——依赖体积、Python/Java 技术栈统一、部署环境、许可证与安全合规。反面也要提:自研的代价是生态和稳定性要自己扛,所以如果需求能用现成框架满足,优先用现成的。这样的取舍表达比站队更有说服力。

Harness 是什么、治理什么:在 Agent 语境里,Harness 指包在模型外面的那层「运行时脚手架」——它负责给模型组装上下文、注入工具、驱动循环、校验输出、记录轨迹,模型本身只是其中一个组件。要治理的内容可以列成清单:模型与渠道(选型、路由、降级、超时与重试、密钥与配额);上下文(系统提示词模板、历史裁剪与摘要、检索片段注入、前缀缓存命中率);工具(注册与权限、参数校验、执行超时、危险操作二次确认);输出(结构化 schema 校验、失败重试、拒答与转人工);成本与观测(token 与费用归集、链路 trace、错误分类);以及安全(提示注入防护、敏感信息脱敏、审计日志)。落地做法是把它做成一层中间件,所有 Agent 调用都走同一个入口,策略用配置下发而不是写死在业务代码里。

分库分表与实时排行榜:百万级数据先给判断——单表百万行对 MySQL 完全不是瓶颈,做好索引和归档即可,不需要分库分表;真正要分的时候通常是单表过千万且写入 QPS 高。分片键选择是第一原则:要保证高频查询能带分片键(否则要扫全部分片),且分片要均匀(避免热点),常用用户 ID 取模或一致性哈希,按时间分片只适合日志类场景。分表后必须处理的问题:跨分片分页与聚合(引入 ES 或汇总表)、分布式主键(雪花算法)、扩容时的数据迁移(成倍扩容或一致性哈希)。实时排行榜用 Redis 的 ZSET:ZADD 更新分数、ZREVRANGE 取榜单、ZREVRANK 查名次,O(log N) 复杂度,日榜/周榜按 key 加时间后缀,用 EXPIRE 自动清理;注意大 key 问题(千万级成员要按业务再切分,比如按城市/品类分桶),以及热点榜单可以用本地缓存 + 定时刷新降低 Redis 压力。

排行榜数据丢失怎么恢复:先按「丢了什么、能容忍丢多久」定策略。恢复手段按优先级排列:一是持久化——开 AOF(appendfsync everysec 最多丢 1 秒)或 RDB 定时快照,重启后从文件恢复;二是主从 + 哨兵/Cluster 做故障切换,避免单点;三是事实表重建——排行榜本质是聚合结果,最可靠的做法是从业务库(或消费 binlog 的日志表)按时间窗口重算并 ZADD 全量覆盖,所以关键前提是原始明细必须落库且可回溯;四是补偿——切换期间如果有双写,先比对再合并。要主动补一句设计上的预防:把 Redis 当缓存而不是唯一数据源,任何榜单数据都要有一份可重算的源,并给榜单加版本号或重建开关,出了事能一键重算。

数据同步方式:定时任务和消息通知不是二选一。定时任务简单、适合对时效要求不高的对账与批量刷新,缺点是延迟和空跑;消息(或 binlog 订阅如 Canal/Debezium)实时、增量、延迟低,缺点是链路复杂、要处理乱序、重复和失败重试。实践口径通常是「增量走消息保时效 + 定时全量对账保正确性」的双轨,并让对账任务在发现不一致时自动修复。

智能摘要的输入输出与提示词:要先说清解决的业务问题(比如客诉工单太长、客服需要 10 秒内抓住核心),再定义接口。输入通常是原始长文本 + 结构化元数据(工单类型、机型、时间、渠道),输出是受约束的结构化字段,比如 {问题现象, 可能原因, 用户诉求, 处理建议, 置信度},用 JSON Schema 约束格式以便下游直接入库。提示词写法上:角色与目标写清楚(你是谁、输出给谁看)、给 1~3 个 few-shot 示例锁定风格与粒度、明确「只依据原文、不许推测、信息缺失填 null」这条硬约束、限定字数上限、把稳定内容放前面以命中前缀缓存、并要求输出纯 JSON 便于解析。评测上固定一批人工标注样本,用「关键信息召回率 + 格式合法率 + 人工评分」三个指标做回归,改提示词必须跑一遍。

Python 还是 Java:给判断依据而不是偏好。Python 适合算法、数据处理、模型相关链路(生态在 AI/科学计算上无可替代,开发速度快),Java 适合高并发、强类型、长生命周期的业务服务(线程模型成熟、JVM 生态的中间件与可观测性完整、团队协作的工程约束强)。实际项目里常按「模型与数据用 Python、在线业务用 Java」分工,中间用 HTTP/RPC 或消息打通,并注意两边的序列化与错误码约定。

Spring Boot 自动装配原理:启动类上的 @SpringBootApplication 是组合注解,核心是 @EnableAutoConfiguration,它通过 @Import(AutoConfigurationImportSelector.class) 读入所有 jar 包 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 2.7 之前是 spring.factories)里声明的自动配置类;这些类上带着 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等条件注解,只有类路径存在对应依赖、容器里还没有同类 Bean、配置开关打开时才会生效。生效的配置类再通过 @EnableConfigurationProperties 把 application.yml 里的属性绑定成配置对象(如 DataSourceProperties),从而自动创建数据源、事务管理器等 Bean。用户自定义 Bean 优先级更高,因为 @ConditionalOnMissingBean 会退让——这是「约定优于配置 + 可覆盖」的实现方式。

Bean 生命周期:大致是实例化(反射调用构造器)→ 属性填充(依赖注入)→ Aware 回调(BeanNameAware、BeanFactoryAware、ApplicationContextAware)→ BeanPostProcessor#postProcessBeforeInitialization → @PostConstruct → InitializingBean#afterPropertiesSet → 自定义 init-method → BeanPostProcessor#postProcessAfterInitialization(AOP 代理就在这一步生成)→ 使用 → 容器关闭时 @PreDestroy → DisposableBean#destroy → 自定义 destroy-method。要能点出三个高频追问:循环依赖靠三级缓存解决(只对单例、且要求是 setter/字段注入,构造器注入的循环依赖直接报错);@PostConstruct 早于 afterPropertiesSet;AOP 代理对象在初始化后才替换原对象,所以自调用(this.method())不会走增强。

Spring AOP:基于动态代理实现,目标是解耦横切关注点(事务、日志、权限、缓存、限流)。接口用 JDK 动态代理(Proxy + InvocationHandler),没有接口时用 CGLIB 生成子类(Spring Boot 默认 proxyTargetClass=true,统一走 CGLIB,且要求类和方法非 final)。核心概念是切面(@Aspect)、切点(@Pointcut,支持 execution 表达式)、通知(@Before/@After/@AfterReturning/@AfterThrowing/@Around)和织入。@Around 最强,可以决定是否执行原方法、改参数和返回值,@Transactional 本质上就是一个 Around 增强。失效场景要熟:同类内部自调用、方法不是 public、异常被自己 catch 掉、多线程调用、以及代理对象没经过 Spring 容器。

B+ 树索引的优缺点:优点是查询稳定(树高低、等值与范围都是 O(log N) 级别的 IO)、范围扫描高效(叶子链表)、支持排序(沿索引顺序读可避免 filesort)、可以做成覆盖索引免回表。缺点是占空间(索引本身也是数据文件)、写入放大(每次增删改都要维护索引页,随机写入还可能引起页分裂)、维护成本(索引多了优化器选择变难,也可能选错索引)、以及不擅长低区分度列的等值查询(比如性别、状态这类只有几个值的列,全表扫描反而更快)。

百万级数据分页的性能瓶颈:瓶颈在「LIMIT offset, size 要扫描并丢弃 offset 行」,同时在 SELECT * 时每一行都要回表,offset 越大越慢。优化手段:延迟关联(先用覆盖索引子查询取主键再回表)、游标分页(WHERE id > last_id ORDER BY id LIMIT size,最优但只能顺序翻页)、强制时间范围过滤、以及把列表页的排序键建成联合索引。业务上要限制最大翻页深度,更深的查询走导出或搜索服务。另外 total 不要每次 COUNT(*) 全表算,而是缓存或维护计数表。

Redis 里的总数多久更新:要按一致性要求选策略——实时性要求高的用「计数表 + 增量更新」(每次增删同步 INCR/DECR,并接受短暂不一致);一般列表用「写时删除缓存 + 读时回填」或「定时任务每 N 分钟刷新一次」;对精确性要求高的直接保留 DB 的 COUNT 或维护计数表 + 对账。回答时给出选择依据并说明过期时间设置(缓存总数一般给短 TTL,比如 1~5 分钟,既能挡掉大部分查询也不会长期不一致)。

成长最大的项目与校内外的区别:这两题是给面试官留下印象的机会。选项目时挑「技术上有取舍、结果可量化、你独立负责过一部分」的那个,按背景—方案对比—实现—结果讲。校园学习与实习的区别可以落到三点:校园是「知识点驱动、边界清晰、可以重做」,实习是「目标驱动、信息不全、要在约束下取舍、结果要对线上负责」;校园作业的正确性由自己判定,实习的正确性由监控和用户判定;校园更多是个人完成,实习更强调协作、沟通和文档。