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 个时,依然能保持快速、准确的意图识别能力。
浙公网安备 33010602011771号