--- id: cs.algorithms.scenario.business-algorithms title: 业务中的算法:热门问题 Top K、任务依赖排序、相似问题去重和缓存淘汰 type: scenario domain: cs stack: [algorithms, redis, langgraph] tags: [Top K, 拓扑排序, 编辑距离, LRU, 去重, 复杂度] level: intermediate prerequisites: [cs.algorithms.complexity-patterns] est_hours: 1.5 related: [cs.algorithms.interview] week: [16] --- # 业务中的算法:热门问题 Top K、任务依赖排序、相似问题去重和缓存淘汰 ## 业务背景与问题 AI 客服运营后台需要: 1. 每小时展示用户最常问的 20 个问题(每小时几十万条消息); 2. 知识库导入任务有依赖(先解析、再分块、再嵌入、再建索引),要按正确顺序执行,并发现配置中的循环依赖; 3. 标注 Bad Case 时,大量问题只是错别字或说法略有不同,需要聚合去重; 4. 进程内缓存最近查询的文档,内存有限。 ## 约束 - 统计要在秒级完成。 - 去重不能误合并意思不同的问题。 ## 方案设计 1. **Top K**:先对问题文本做规范化(去空白、统一全半角),用哈希表计数,再用大小为 K 的堆取前 K,O(n log K);多实例时用 Redis 有序集合 `ZINCRBY` + `ZREVRANGE` 汇总。 2. **任务依赖**:用拓扑排序得到执行顺序,检测到环时拒绝保存配置;同层没有依赖关系的任务可以并行。 3. **相似去重**:先按规范化文本精确去重;再对候选对计算编辑距离(短文本)或向量相似度(语义),超过阈值的合并为一组,人工确认后生效。编辑距离是 O(mn),只对长度相近的候选对计算,避免 O(n²) 两两比较全部。 4. **缓存**:LRU 淘汰;Python 中可直接用 `functools.lru_cache` 或 `OrderedDict` 实现。 ## 关键代码 ```python from patterns import edit_distance, top_k_frequent, topo_order hot = top_k_frequent((normalize(m.text) for m in messages), 20) order = topo_order({"index": ["embed"], "embed": ["chunk"], "chunk": ["parse"]}) if order is None: raise ValueError("任务配置存在循环依赖") def similar(a: str, b: str) -> bool: if abs(len(a) - len(b)) > 2: # 先用长度剪枝 return False return edit_distance(a, b) <= max(1, len(a) // 10) ``` 完整实现和测试:[examples/patterns](../examples/patterns/)。 ## 取舍 | 选择 | 优点 | 缺点 | | --- | --- | --- | | **精确计数 + 堆(本方案)** | 结果准确 | 内存随不同问题数增长 | | 近似算法(Count-Min Sketch 等) | 内存固定 | 有误差 | | 编辑距离去重 | 适合错别字 | 识别不了同义改写,需要配合向量相似度 | ## 易错点 - 没有规范化就计数,同一问题被拆成多个。 - 对全部问题两两计算相似度,O(n²) 跑不完。 - 去重阈值过松,把“退货”和“退款”合并(它们的编辑距离只有 1,但业务含义不同)——相似度只是候选,需要人工确认或结合业务规则。 ## 面试怎么讲 > “运营后台的热门问题,我先做文本规范化,用哈希表计数加大小为 K 的堆取 Top K,复杂度 O(n log K),多实例用 Redis 有序集合汇总。导入任务的依赖用拓扑排序确定顺序并检测环。Bad Case 去重先精确去重,再按长度剪枝后算编辑距离,阈值只产生候选,避免把‘退货’和‘退款’这种只差一个字但意思不同的问题合并。”