深信服 AI应用开发 二面

1. 自我介绍

2. 详细讲一下你的实习经历,你具体负责哪些工作

3. 限量出包是什么逻辑,5 万条候选线索只出 3 万条怎么判断高价值用户

答案:

限量出包本质是一个带约束的排序和筛选问题。不能简单按分数从高到低取前 3 万,因为业务里还有很多约束,比如销售每天承接能力、行业配额、城市覆盖、客户冷却期、黑名单、重复企业、近期已触达、历史投诉、企业规模、潜在金额等。

我会先做硬过滤,再做评分排序,最后做配额调整。硬过滤去掉无权限、黑名单、近期已触达、关键信息缺失的线索;评分层综合行为分、画像分、意图分、转化概率分;配额层保证行业、区域和销售组之间不会极端倾斜。最终不是单纯 topN,而是 “topN + 业务约束 + 去重补位”。

type Lead struct {
    ID              string
    CompanyID       string
    Industry        string
    City            string
    LastContactDays int
    BehaviorScore   float64
    IntentScore      float64
    ProfileScore     float64
    ConvertScore     float64
    IsBlack          bool
}

func Score(l Lead) float64 {
    return 0.35*l.BehaviorScore +
        0.25*l.IntentScore +
        0.25*l.ProfileScore +
        0.15*l.ConvertScore
}

出包时会保留可解释信息:

{
  "leadId": "L1001",
  "score": 86.7,
  "reasons": [
    "近 7 天访问价格页 3 次",
    "企业规模符合重点客户画像",
    "历史同类行业转化率较高"
  ]
}

4. 限量出包里如果排序规则和业务配额冲突,怎么处理

答案:

排序规则和配额冲突很常见。比如金融行业线索分数普遍高,但业务要求当天制造业也要覆盖一定比例;或者某个城市客户很多,但销售团队覆盖不了。这个时候不能只追求全局分数最高,而要做约束优化。

工程上可以分两阶段。第一阶段按全局分数排序得到候选池,比如取 5 万里的前 4 万;第二阶段按行业、城市、销售组做配额分配。如果某个桶不够,就从其他桶补位;如果某个桶超额,就保留桶内高分线索。这样既保留排序质量,也满足业务覆盖。

func SelectByQuota(leads []Lead, quota map[string]int, total int) []Lead {
    grouped := make(map[string][]Lead)
    for _, l := range leads {
        grouped[l.Industry] = append(grouped[l.Industry], l)
    }

    var result []Lead
    for industry, list := range grouped {
        sort.Slice(list, func(i, j int) bool {
            return Score(list[i]) > Score(list[j])
        })

        limit := quota[industry]
        if len(list) < limit {
            limit = len(list)
        }
        result = append(result, list[:limit]...)
    }

    sort.Slice(result, func(i, j int) bool {
        return Score(result[i]) > Score(result[j])
    })

    if len(result) > total {
        return result[:total]
    }
    return result
}

5. 筛选流程里最复杂或者最困难的点是什么

答案:

最困难的是“看起来重复但业务上不完全重复”的线索合并。比如一个企业可能从官网留资、活动报名、广告点击和销售导入同时出现,手机号、企业名、域名、统一社会信用代码可能有的缺失,有的写法不一致。如果不合并,会重复触达;如果合并过度,又可能把不同分公司、不同联系人错误合并。

我会做三层去重。第一层强标识,比如企业 ID、统一社会信用代码、手机号;第二层弱标识,比如企业名标准化、域名、邮箱后缀;第三层相似度匹配,比如企业名编辑距离、地址相似度。强标识命中可以直接合并,弱标识命中需要置信度,低置信度进入人工或延迟合并。

func NormalizeCompanyName(name string) string {
    name = strings.ReplaceAll(name, "有限公司", "")
    name = strings.ReplaceAll(name, "有限责任公司", "")
    name = strings.ReplaceAll(name, "集团", "")
    name = strings.TrimSpace(name)
    return name
}

合并时不能丢信息,应该把行为和来源累加:

{
  "companyId": "C1001",
  "sources": ["website", "crm", "campaign"],
  "contacts": ["138****0001", "sales@example.com"],
  "latestBehavior": "visited_pricing_page"
}

6. Doris 是什么样的数据库,适合解决什么问题

答案:

Doris 是面向分析场景的 MPP 列式数据库,适合做实时或准实时 OLAP 查询。它比较适合报表分析、明细检索、聚合统计、多维分析这类场景,比如按渠道、行业、城市、日期统计线索数量、转化率、触达效果。

它和 MySQL 的定位不一样。MySQL 更适合事务和点查,Doris 更适合大宽表、聚合和分析查询。在线路由、订单状态这类强事务数据不适合放 Doris 当主库;但大量行为日志、线索明细、报表指标放 Doris 会更合适。

CREATE TABLE lead_event_detail (
    event_date DATE,
    lead_id VARCHAR(64),
    company_id VARCHAR(64),
    channel VARCHAR(32),
    industry VARCHAR(64),
    event_type VARCHAR(64),
    event_time DATETIME
)
DUPLICATE KEY(event_date, lead_id)
PARTITION BY RANGE(event_date)()
DISTRIBUTED BY HASH(lead_id) BUCKETS 32;

7. Doris 表模型怎么选,明细表和聚合表怎么设计

答案:

Doris 常见有 Duplicate Key、Aggregate Key、Unique Key。明细行为日志一般用 Duplicate Key,因为同一个线索可能有多条访问、点击、报名记录,不需要覆盖。客户画像这类按 company_id 更新的数据,可以用 Unique Key。日报类指标可以用 Aggregate Key 或提前聚合表。

比如线索行为明细保留原始事件,用于排查和回溯;线索日报聚合用于高频查询,减少每次扫明细表聚合的成本。

CREATE TABLE lead_metric_daily (
    stat_date DATE,
    channel VARCHAR(32),
    industry VARCHAR(64),
    lead_count BIGINT SUM,
    valid_count BIGINT SUM,
    convert_count BIGINT SUM
)
AGGREGATE KEY(stat_date, channel, industry)
DISTRIBUTED BY HASH(channel) BUCKETS 16;

查询日报时直接查聚合表:

SELECT industry,
       SUM(lead_count) AS leads,
       SUM(convert_count) AS converts
FROM lead_metric_daily
WHERE stat_date >= '2026-04-01'
  AND stat_date < '2026-05-01'
GROUP BY industry;

8. 出数频率一般怎么设计,实时出数和离线出数怎么取舍

答案:

出数频率要看业务使用场景。销售每日分发名单一般可以 T+1 或小时级,不一定要秒级;但客户刚提交表单后的高意向提醒可能需要分钟级甚至实时。实时越强,链路越复杂,成本越高,也更容易受脏数据影响。

我会把出数分成三类:第一类是实时高价值事件,比如提交表单、预约演示,直接进实时队列;第二类是小时级行为聚合,比如访问价格页、下载资料;第三类是日级全量出包,用离线任务统一排序和配额控制。

实时链路:表单提交 -> MQ -> 实时评分 -> 销售提醒
小时链路:行为日志 -> 聚合任务 -> 更新意图分
日级链路:全量候选 -> 排序约束 -> 出包名单

这样不会为了所有场景都追求实时,导致系统复杂度失控。

9. Redis 缓存热点查询是怎么复用的,不是简单字符串匹配怎么做

答案:

热点查询缓存不应该只按原始字符串做 key,因为用户表达可能不同但语义相同,比如“近 7 天北京制造业线索”和“北京制造业最近一周线索”。如果完全按字符串缓存,命中率会很低。更好的方式是把查询条件结构化,再生成规范化 cache key。

比如先把查询语句解析成行业、城市、时间范围、渠道、指标等字段,然后按固定顺序生成 key。对于模糊关键词,可以先做关键词抽取和归一化,再参与缓存。

type QueryCondition struct {
    City      string
    Industry  string
    TimeRange string
    Metric    string
    Channel   string
}

func CacheKey(q QueryCondition) string {
    return fmt.Sprintf("lead:query:%s:%s:%s:%s:%s",
        q.City, q.Industry, q.TimeRange, q.Metric, q.Channel)
}

这样“最近一周”和“近 7 天”会被归一化成同一个 last_7_days,缓存才能复用。

10. 关键词提取是怎么做的,为什么要喂给大模型

答案:

关键词提取不是单纯分词,而是把用户输入变成后续工具和查询能理解的结构。比如用户问“帮我找最近对私域运营感兴趣的教育行业客户”,这里要提取行业是教育,兴趣关键词是私域运营,时间范围是最近,任务类型是线索筛选。

大模型适合处理模糊表达和语义归一化,但不能完全让模型自由输出。所以会给它固定 schema,让它输出结构化字段,再由后端校验。像行业、城市、渠道要映射到枚举;时间要转换成具体范围;关键词要做同义词归一。

{
  "task": "lead_filter",
  "industry": "education",
  "interestKeywords": ["private_domain_operation"],
  "timeRange": "last_30_days",
  "confidence": 0.88
}

后端校验:

var validIndustries = map[string]bool{
    "ed

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

AI-Agent面试实战专栏 文章被收录于专栏

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

全部评论

相关推荐

评论
1
收藏
分享

创作者周榜

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