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