虾皮 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面试实战专栏 文章被收录于专栏

本专栏聚焦 AI-Agent 面试高频考点,内容来自真实面试与项目实践。系统覆盖大模型基础、Prompt工程、RAG、Agent架构、工具调用、多Agent协作、记忆机制、评测、安全与部署优化等核心模块。以“原理+场景+实战”为主线,提供高频题解析、标准答题思路与工程落地方法,帮助你高效查漏补缺.

全部评论

相关推荐

逆流河上万仙退:你说的对 这就是我们27届双非领军人物 阿里->虾皮->百度->pdd 他的极限到底在哪 让我们秋招拭目以待
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务