虾皮 AI应用开发 实习 一面
1. 自我介绍
2. 最快到岗时间和实习时长
3. 讲讲你在字节的实习
4. 服务运行在 K8s 上,你了解哪些核心概念,线上排查会看什么
答案:
K8s 里最核心的是 Pod、Deployment、Service、ConfigMap、Secret、Ingress、HPA。Pod 是最小调度单元,Deployment 管理副本和滚动更新,Service 提供稳定访问入口,ConfigMap 管配置,Secret 管敏感信息,Ingress 做七层入口,HPA 根据指标自动扩缩容。
线上排查一般先看 Pod 状态、重启次数、事件、日志和资源使用。如果服务一直重启,看 kubectl describe pod 里的 event,再看容器日志;如果接口慢,看 CPU、内存、网络、连接池和下游依赖;如果配置不生效,看 ConfigMap 是否更新、Pod 是否重新加载。
kubectl get pods -n ai-platform kubectl describe pod ai-api-xxx -n ai-platform kubectl logs ai-api-xxx -n ai-platform --tail=200 kubectl top pod -n ai-platform kubectl get events -n ai-platform --sort-by=.metadata.creationTimestamp
如果怀疑服务实例没有收到流量,可以看 Service 和 Endpoint:
kubectl get svc -n ai-platform kubectl get endpoints -n ai-platform
5. 日志一般从哪里看,线上日志怎么设计才方便排查
答案:
本地开发可以直接看控制台或文件,线上一般会接入日志平台,比如 ELK、Loki、SLS 这类系统。K8s 环境下也可以先用 kubectl logs 快速定位,但真正排查复杂问题必须依赖集中式日志和 traceId。
日志设计最关键的是结构化和链路串联。每个请求要有 traceId,日志里要带接口、用户或租户、请求耗时、错误码、下游调用耗时、SQL 慢查询标识、模型调用耗时。不能只打一行“系统异常”,否则出了问题不知道是哪一层慢。
func TraceMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
traceID := c.GetHeader("X-Trace-Id")
if traceID == "" {
traceID = uuid.New().String()
}
c.Set("traceId", traceID)
c.Header("X-Trace-Id", traceID)
c.Next()
}
}
结构化日志示例:
{
"traceId": "9f2a1c",
"path": "/api/configs/1001",
"costMs": 38,
"status": 200,
"redisCostMs": 3,
"mysqlCostMs": 17
}
6. 讲一下从开发到上线的完整流程
答案:
完整流程一般是需求评审、技术方案、接口设计、数据库设计、编码、自测、联调、测试环境验证、代码 review、CI 构建、灰度发布、线上观察和回滚预案。实习生也要知道自己写的代码不是提交完就结束,后面还有测试、发布和监控。
如果是 AI 平台接口,还要额外关注模型调用是否可降级、prompt 版本是否可追踪、配置是否能热更新、缓存是否会出现脏读。上线前要准备回滚方案,比如镜像版本回退、配置开关关闭、缓存清理脚本和数据库变更回滚。
需求评审 -> 技术方案 -> 建表 / 接口定义 -> 编码 -> 单测 / 自测 -> 联调 -> Code Review -> CI 构建镜像 -> 部署测试环境 -> 灰度发布 -> 观察日志和指标 -> 全量发布
K8s 发布可以通过 Deployment 滚动更新:
kubectl set image deployment/ai-api ai-api=registry.example.com/ai-api:v20260507 -n ai-platform kubectl rollout status deployment/ai-api -n ai-platform
7. 多渠道数据是怎么处理的,已经结构化的数据还需要怎么建模
答案:
多渠道数据不能简单堆到一张大表里。不同渠道字段可能不一样,比如广告渠道有点击、曝光、消耗,内容渠道有播放、点赞、转化,搜索渠道有关键词、排名、点击率。要先抽象公共维度,比如渠道、场景、用户、时间、资源位、实验分组,再把渠道特有字段放到扩展表或 JSON 字段里。
如果数据已经结构化,也要看查询场景。报表分析通常按时间、渠道、场景聚合;在线接口通常按用户、渠道、配置版本查询。不同场景对应不同表设计,不能为了省事全部 join。明细表、汇总表、配置表、维度表要分开,避免线上接口扫大表。
CREATE TABLE channel_metric_daily (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
stat_date DATE NOT NULL,
channel_code VARCHAR(32) NOT NULL,
scene_code VARCHAR(32) NOT NULL,
exposure BIGINT NOT NULL DEFAULT 0,
click BIGINT NOT NULL DEFAULT 0,
cost DECIMAL(18,4) NOT NULL DEFAULT 0,
conversion BIGINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_date_channel_scene(stat_date, channel_code, scene_code)
);
对于在线查询,可以提前汇总:
SELECT channel_code, scene_code, exposure, click, cost FROM channel_metric_daily WHERE stat_date = ? AND channel_code = ?;
8. 大规模 SQL 优化具体做过哪些事情
答案:
大规模 SQL 优化首先看慢查询日志和执行计划,确认是全表扫描、索引失效、回表太多、排序临时表、join 顺序错误还是锁等待。不能只靠感觉加索引。优化动作一般包括补联合索引、改写条件、避免函数包裹索引列、分页优化、拆分大 SQL、用汇总表替代实时聚合、减少不必要字段查询。
比如原来有一个渠道数据报表接口,每次都从明细表按日期范围聚合,数据量大时很慢。后来改成每天离线汇总到 channel_metric_daily,在线查询只查汇总表,延迟从秒级降到几十毫秒。
-- 原始写法:在线扫明细表 SELECT channel_code, SUM(exposure), SUM(click), SUM(cost) FROM channel_event_log WHERE event_time >= '2026-05-01' AND event_time < '2026-05-08' GROUP BY channel_code; -- 优化后:查日汇总表 SELECT channel_code, SUM(exposure), SUM(click), SUM(cost) FROM channel_metric_daily WHERE stat_date >= '2026-05-01' AND stat_date < '2026-05-08' GROUP BY channel_code;
配合索引:
CREATE INDEX idx_date_channel ON channel_metric_daily(stat_date, channel_code);
9. 多渠道数据都在一个维度下,为什么还要 join,什么时候不应该 join
答案:
如果所有数据都已经在同一个维度,并且查询只需要这个维度上的指标,那确实不应该为了形式去 join。很多慢 SQL 就是因为把本来可以在宽表或汇总表里解决的问题,写成多表 join。在线接口更应该减少 join,尤其是大表 join 大表。
但有些场景必须 join,比如指标表只有 channel_id,展示时需要渠道名称、渠道类型、负责人,这些在维度表里;或者权限过滤需要 join 用户资源表。优化方向不是完全禁止 join,而是把高频查询需要的维度适当冗余到结果表,把低频管理类查询再走 join。
-- 高频在线查询,不建议每次 join 大表 SELECT channel_code, exposure, click FROM channel_metric_daily WHERE stat_date = ?; -- 管理后台低频查询,可以 join 维度表 SELECT m.channel_code, c.channel_name, m.exposure, m.click FROM channel_metric_daily m JOIN channel_dim c ON m.channel_code = c.channel_code WHERE m.stat_date = ?;
如果 join 不可避免,要保证关联字段类型一致、都有索引,并且控制驱动表大小。
10. 转化率不达标时,为什么要补一批数据给大模型做意图识别,具体怎么做
答案:
转化率不达标时,不能只看最终转化数,要判断用户卡在哪个环节,比如曝光少、点击低、落地页跳出高、商品不匹配、价格敏感、客服响应慢。大模型做意图识别不是凭空分析,而是基于用户行为、搜索词、点击路径、历史问法和渠道上下文,判断用户真实意图属于价格咨询、功能对比、售后担忧、购买犹豫还是无效流量。
具体做法是先把行为数据结构化,再抽样补充给模型。不能把全量日志直接塞进去,而是按用户会话聚合成摘要,包括入口渠道、关键词、浏览商品、停留时间、点击按钮、退出页面。模型输出意图标签和原因,再进入统计分析。
{
"sessionId": "s1001",
"channel": "search_ad",
"query": "工业相机怎么选",
"pageViews": ["camera_list", "camera_detail_A", "compare_page"],
"staySeconds": 96,
"actions": ["view_detail", "compare", "exit"],
"converted": false
}
模型输出要结构化:
{
"intent": "product_compare",
"dropReason": "用户在对比页退出,可能缺少核心参数差异说明",
"confidence": 0.82
}
11. 配置热更新基于 MySQL 和 Redis 怎么实现
答案:
配置的权威数据放 MySQL,Redis 做缓存和发布通知。本地服务可以再做一层本地缓存,减少每次请求都访问 Redis。更新配置时先写 MySQL,再删除或更新 Redis,然后通过 Redis Pub/Sub、消息队列或版本号轮询通知各个服务刷新本地缓存。
关键是配置要有版本号。服务加载配置时不仅看 key,还要看 version。如果 Redis 里版本比本地新,就刷新;如果 Redis 不可用,可以降级使用本地旧配置,但要打告警。
CREATE TABLE platform_config (
config_key VARCHAR(128) PRIMARY KEY,
config_value TEXT NOT NULL,
version BIGINT NOT NULL,
updated_at DATETIME NOT NULL
);
更新配置:
func UpdateConfig(key string, value string) error {
err := db.Transaction(func(tx *gorm.DB) error {
return tx.Exec(`
UPDATE platform_config
SET config_value = ?, version = version + 1, updated_at = NOW()
WHERE config_key = ?
`, value, key).Error
})
if err != nil {
return err
}
redis.Del(ctx, "config:"+key)
redis.Publish(ctx, "config_change", key)
return nil
}
本地服务订阅变更后刷新:
func WatchConfigChange() {
sub := redis.Subscribe(ctx, "config_change")
for msg := range sub.Channel() {
localCache.Delete(msg.Payload)
}
}
12. MySQL 和 Redis 分别存什么数据,为什么不能只存一份
答案:
MySQL 存权威数据和需要事务保证的数据,比如配置记录、渠道指标、用户权限、任务状态、审计日志。Redis 存热点数据和短生命周期数据,比如热点配置、接口缓存、分布式锁、限流计数、临时会话、幂等标记。
不能只存 Redis,因为 Redis 不适合作为所有业务事实源,内存成本高,复杂查询弱,持久化和事务能力也不是它的强项。也不能只用 MySQL,因为高频读、热点配置和短期状态都打到 MySQL,会让连接池和磁盘压力很大。两者分工是 MySQL 保正确性,Redis 提性能和削峰能力。
MySQL: - 配置权威数据 - 用户权限 - 渠道指标 - 审计日志 - 任务最终状态 Redis: - 热点配置缓存 - 本地缓存失效通知 - 限流计数 - 幂等 key - 短期会话状态
13. Redis 配置缓存多副本会有什么问题,怎么解决
答案:
多副本的问题主要是数据一致性和读到旧值。Redis 主从复制是异步的,如果刚写主节点,读从节点可能还没同步,服务就会读到旧配置。配置这种数据如果影响线上策略,旧值可能导致灰度不一致。
解决方式有几种。强一致要求高的配置只读主节点,或者通过版本号校验发现旧数据后回源 MySQL。读多写少的配置可以接受短暂延迟,但要保证最终一致。还可以在配置里加 version,服务本地缓存也保存版本,只有新版本才能覆盖旧版本。
type ConfigValue struct {
Key string `json:"key"`
Value string `json:"value"`
Version int64 `json:"version"`
}
func ApplyConfig(newCfg ConfigValue) {
old, ok := localCache.Get(newCfg.Key)
if ok && old.Version >= newCfg.Version {
return
}
localCache.Set(newCfg.Key, newCfg)
}
这样即使消息乱序,也不会用旧配置覆盖新配置。
14. 为什么不把配置直接缓存到本地,每次读本地内存就行
答案:
本地缓存速度最快,但最大问题是多实例一致性和失效通知。K8s 上可能有很多 Pod,每个 Pod 都有自己的本地缓存。如果配置更新后只改了 MySQL,没有通知所有 Pod,本地缓存会长期不一致。Redis 的作用是做集中缓存和变更通知。
比较合理的架构是三级读取:先读本地缓存,本地没有或版本过期再读 Redis,Redis 没有再读 MySQL。配置更新后通过 Redis Pub/Sub 或 MQ 通知所有实例清理本地缓存。这样既能减少 Redis 压力,又能保证配置变更可以传播。
func GetConfig(key string) (ConfigValue, error) {
if v, ok := localCache.Get(key);
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏聚焦 AI-Agent 面试高频考点,内容来自真实面试与项目实践。系统覆盖大模型基础、Prompt工程、RAG、Agent架构、工具调用、多Agent协作、记忆机制、评测、安全与部署优化等核心模块。以“原理+场景+实战”为主线,提供高频题解析、标准答题思路与工程落地方法,帮助你高效查漏补缺.