美的全栈开发一面:自研Agent与Spring原理
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 自我介绍
- Agent 是自研的,还是用 LangGraph 做的?
- 为什么选择自研 Agent 框架?
- Harness 是什么?
- Harness 具体治理哪些内容?你怎么做的?
- 百万级数据有没有做分库分表?
- 实时排行榜用什么技术实现?
- Redis 的排行榜数据丢失了,怎么恢复?
- 同步靠定时任务,还是消息通知?
- 摘要的输入、输出是什么?提示词怎么写?
- 智能摘要具体解决什么问题?
- 你如何选择 Python 或 Java?
- Spring Boot 自动装配的原理是什么?
- Bean 的生命周期大致是什么?
- Spring AOP 怎么实现?
- MySQL 的 B+ 树索引有什么优缺点?
- 百万级数据做分页,遇到了什么性能瓶颈?
- 总数缓存在 Redis 中,多久更新一次?
- 哪段项目技术含量最高,或者让你成长最多?
- 在学校学习与实习中学习,有什么区别和体会?
反问:部门业务、后续流程。
《参考解析》
为什么要自研 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 分钟,既能挡掉大部分查询也不会长期不一致)。
成长最大的项目与校内外的区别:这两题是给面试官留下印象的机会。选项目时挑「技术上有取舍、结果可量化、你独立负责过一部分」的那个,按背景—方案对比—实现—结果讲。校园学习与实习的区别可以落到三点:校园是「知识点驱动、边界清晰、可以重做」,实习是「目标驱动、信息不全、要在约束下取舍、结果要对线上负责」;校园作业的正确性由自己判定,实习的正确性由监控和用户判定;校园更多是个人完成,实习更强调协作、沟通和文档。