Flow 意图识别预筛:用向量检索缓解 LLM 注意力稀释

Flow 意图识别预筛:用向量检索缓解 LLM 注意力稀释

当对话系统的 Flow 数量从 5 个增长到 50 个,全量注入 Prompt 的方式会遇到两个天花板:Token 消耗飙升、LLM 注意力稀释导致识别准确率下降。本文解析一个"向量预筛 + LLM 精判"的两阶段方案。


一、全量匹配模式的问题

当前系统的 Flow 意图识别采用全量匹配模式:将所有 Flow 的 ID 和 description 注入 LLM Prompt,让 LLM 判断用户输入匹配哪个 Flow。

Prompt 片段示例:

可用 Flow 列表:
1. query_my_tasks: 查询当前用户的个人任务。触发词:查任务、我的任务、待办
2. query_my_bugs: 查询当前用户的缺陷列表。触发词:查缺陷、我的bug
3. query_my_stories: 查询当前用户的需求列表。触发词:查需求、我的需求
4. create_bug: 提交一个新的缺陷记录。触发词:创建缺陷、提bug
5. create_task: 创建一个新的开发任务。触发词:创建任务、新建任务
... (共 50 个 Flow)

当 Flow 数量为 5-10 个时,这种方式效果很好。但当 Flow 扩展到 30+ 个时,问题开始出现:

1.1 Token 消耗线性增长

每个 Flow 的 description 约 50-80 Token,50 个 Flow 就是 2500-4000 Token 的 Prompt 开销。如果用户消息只有 10 Token,实际有效信息占比不到 1%。

1.2 LLM 注意力稀释

Transformer 的注意力机制在处理长上下文时存在"注意力稀释"问题——当候选项过多时,模型对每个候选项的注意力权重下降,导致:

  • 语义相近的 Flow 之间互相干扰("查任务" 和 "查需求" 的注意力权重差距缩小)
  • 低频 Flow 被高频 Flow "淹没"
  • 模型更倾向于选择描述更长的 Flow(长度偏差)

1.3 延迟增加

Prompt 越长,LLM 的首 Token 延迟(TTFT)越高。对于实时对话场景(IM 中@机器人),用户期望 1-2 秒内得到回复,全量匹配在 Flow 数量多时无法满足延迟要求。

二、两阶段识别方案

解决方案是在 LLM 之前加一层向量预筛,用嵌入模型(推理速度远快于 LLM)快速缩小候选范围:

用户消息 → 向量编码 → 余弦相似度排序 → top-5 候选 → LLM 精确判断 → 最终 Flow

2.1 第一阶段:向量预筛(毫秒级)

FlowPreFilter 在系统启动时预计算所有 Flow 的嵌入向量,运行时只需编码用户消息并计算余弦相似度:

class FlowPreFilter:
    DEFAULT_FLOW_THRESHOLD = 30  # Flow 数量低于此值时跳过预筛

    def index(self, flows: List[Flow]) -> None:
        """启动时预计算所有 Flow 的嵌入向量"""
        texts = []
        for flow in flows:
            parts = [flow.name or flow.id]
            if flow.description:
                parts.append(flow.description)
            texts.append(" ".join(parts))

        # 批量编码,归一化(使点积等价于余弦相似度)
        embeddings = model.encode(texts, normalize_embeddings=True)
        for flow, emb in zip(flows, embeddings):
            self._flow_vectors.append(_FlowVector(
                flow_id=flow.id,
                embedding=emb,
            ))

    def pre_filter(self, user_message: str, all_flows: List[Flow]) -> List[Flow]:
        """运行时快速筛选"""
        # Flow 数量少 → 全量传 LLM
        if len(all_flows) <= self.flow_threshold:
            return all_flows

        # 编码用户消息
        query_emb = model.encode([user_message], normalize_embeddings=True)[0]

        # 计算余弦相似度(归一化后点积 = 余弦相似度)
        scored = []
        for fv in self._flow_vectors:
            score = float(np.dot(query_emb, fv.embedding))
            scored.append((fv.flow_id, score))

        # 取 top-5
        scored.sort(key=lambda x: x[1], reverse=True)
        top_ids = {s[0] for s in scored[:self.top_k]}
        return [f for f in all_flows if f.id in top_ids]

关键设计点:

  • 归一化编码normalize_embeddings=True 使点积运算等价于余弦相似度,省去除法运算
  • 批量预计算:启动时一次性编码所有 Flow 向量并缓存,运行时只需编码用户消息(1 条 vs N 条)
  • 阈值控制:Flow 数量 ≤ 30 时跳过预筛(全量传给 LLM 的开销可接受),避免在 Flow 数量少时引入不必要的延迟

2.2 第二阶段:LLM 精判

预筛后只有 5 个候选 Flow 传给 LLM,Prompt 从 ~4000 Token 缩减到 ~400 Token。LLM 在小范围内做精确的语义匹配,注意力不再稀释,识别准确率回升。

三、在 LangGraph 图中的集成位置

向量预筛集成在 understand 节点中,在调用 LLM 之前执行:

async def understand_node(state):
    flows_list = flows.flows if flows else []

    # Flow 向量预筛:先用嵌入模型筛出 top-k 候选
    if flow_pre_filter and flows_list:
        original_count = len(flows_list)
        flows_list = flow_pre_filter.pre_filter(input_message, flows_list)
        if len(flows_list) < original_count:
            logger.info(f"Flow 预筛: {original_count} → {len(flows_list)} 个候选")

    # 只传候选 Flow 给 LLM
    generation_result = await command_generator.generate(
        tracker, domain, flows_list  # ← 已筛选
    )

这个集成位置的选择是有意的:预筛在图的最前端执行,不改变后续节点的任何逻辑。Policy 节点和 Flow 引擎仍然只看到"当前活跃的 Flow",不感知预筛的存在。

四、性能分析

4.1 延迟对比

假设 50 个 Flow 的场景:

阶段 全量模式 预筛模式
用户消息编码 - ~5ms
余弦相似度计算 - ~1ms
LLM Prompt 构建 ~10ms ~2ms
LLM 推理 ~800ms(4000 Token Prompt) ~300ms(400 Token Prompt)
总延迟 ~810ms ~308ms

LLM 推理延迟与输入 Token 数近似线性关系,Prompt 缩减 10 倍带来约 2.5 倍的延迟改善。

4.2 准确率对比

在模拟 50 个 Flow 的测试中:

方法 Top-1 准确率 Top-5 召回率
全量 LLM ~82% 100%
向量预筛 top-5 - ~96%
预筛 + LLM ~91% ~96%

全量 LLM 的 Top-1 准确率低于"预筛 + LLM",验证了注意力稀释假设:候选项过多时 LLM 的判断力下降。预筛后 LLM 在小范围内做决策,准确率反而更高。

向量预筛的 Top-5 召回率为 96%,意味着 4% 的正确 Flow 被预筛阶段漏掉。这是用嵌入模型的召回率换取 LLM 精确度的 trade-off,可以通过调大 top_k(如从 5 调到 8)来改善。

五、与嵌入模型微调的协同

预筛效果直接依赖嵌入模型的质量。在通用 bge-base-zh 上,"查任务"和"查需求"的向量距离很近,预筛可能将两者都召回或都漏掉。

经过 LoRA 微调后(参见上一篇博客),嵌入模型能区分研发管理领域的细粒度语义,预筛的 Top-5 召回率从 ~88% 提升到 ~96%。

两个模块的协同关系:

LoRA 微调 → 嵌入模型质量 ↑ → 预筛召回率 ↑ → LLM 候选质量 ↑ → 意图识别准确率 ↑

六、扩展性考量

6.1 Flow 数量增长路径

Flow 数量 推荐方案
≤ 10 全量 LLM 匹配(简单可靠)
10-30 全量 LLM + 关注 Token 消耗
30-100 向量预筛 + LLM(当前方案)
100+ 多级预筛(粗筛 top-20 → 精筛 top-5)或聚类索引

6.2 索引更新策略

当前预筛器在系统启动时一次性构建索引。如果运行时动态添加 Flow(如热加载新的 YAML 配置),需要对新增 Flow 单独编码并追加到向量列表中。对于增量更新频繁的场景,可以考虑引入 FAISS 等向量索引库替代手动维护的 numpy 数组。

七、总结

向量预筛方案的核心思想是用嵌入模型的推理速度优势换取 LLM 的注意力集中度

  • 嵌入模型编码 50 条 Flow + 1 条查询 + 计算相似度:~6ms
  • LLM 处理 5 个候选 vs 50 个候选:延迟降低 2.5 倍,准确率提升 ~9%

这不是一个复杂的算法创新,而是一个工程上的务实选择——在 LLM 能力有天花板时,用传统检索技术缩小搜索空间,让 LLM 在它擅长的"小范围精确判断"上发挥最大价值。

对于任务型对话系统来说,Flow 数量会随着业务扩展持续增长。向量预筛提供了一条清晰的扩展路径,确保系统在 Flow 数量从 5 个增长到 50 个时,依然能保持快速、准确的意图识别能力。

posted @ 2026-06-07 20:52  黄忠  阅读(43)  评论(0)    收藏  举报