RAG 查询意图分类:用轻量模型做前置路由,节省大模型调用
一、每条查询都走 RAG + LLM,成本爆炸
一个知识库问答系统,用户每天发起 10 万次查询。每条查询都要经过 Embedding 检索 + LLM 生成答案,日均 API 费用 3000 元。
分析日志后发现:
- 40% 的查询是"你好"、"谢谢"、"在吗"之类的闲聊,根本不需要检索
- 15% 的查询是可以用 FAQ 直接匹配的常见问题,也不需要 LLM 推理
- 25% 的查询可以通过关键词搜索返回文档列表,不需要 LLM 总结
- 只有 20% 的查询真正需要通过 RAG + LLM 生成答案
如果能在查询入口处做一个意图路由器,将不同类型的查询分发到不同的处理链路,可以大幅降低大模型调用成本。
二、意图路由架构
flowchart TB Query["用户查询"] --> Router["意图路由器\n轻量分类模型"] Router -->|"闲聊类\n(40%)"| Chat["闲聊处理器\n规则 + 小模型回复\n成本: 0"] Router -->|"FAQ 类\n(15%)"| FAQ["FAQ 匹配\nES 精确匹配\n成本: ¥0.001"] Router -->|"搜索类\n(25%)"| Search["文档搜索\nBM25 + Embedding 检索\n返回 Top-K 文档\n成本: ¥0.01"] Router -->|"推理类\n(20%)"| RAG["完整 RAG\n检索 + LLM 生成\n成本: ¥0.1"] subgraph Cost["每查询成本对比"] C1["闲聊: ¥0"] C2["FAQ: ¥0.001"] C3["搜索: ¥0.01"] C4["RAG: ¥0.1"] end Router --> Feedback["反馈回路\n用户行为标注\n持续优化路由准确率"]关键设计原则:分类模型必须比被节省的成本更便宜。不能为了解决 1 块钱的问题,投入 10 块钱的路由器。
三、Python 实现轻量级意图路由器
意图分类器
import re from typing import List, Tuple, Optional from enum import Enum from dataclasses import dataclass import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression import joblib class IntentType(Enum): """查询意图类型""" CHAT = "chat" # 闲聊:你好、谢谢 FAQ = "faq" # 常见问题:怎么退款 SEARCH = "search" # 文档搜索:Go 并发模型有哪些 RAG = "rag" # 需要推理:为什么 Go 的 channel 比 mutex 更适合这个场景 @dataclass class IntentResult: """意图分类结果""" intent: IntentType confidence: float # 置信度 [0, 1] keywords: List[str] # 提取的关键词 fallback_reason: Optional[str] = None class IntentRouter: """ 轻量级意图路由器 ——使用规则 + 轻量 ML 模型,处理延迟 < 5ms """ def __init__(self, model_path: Optional[str] = None): # 规则层:快速匹配常见模式 self.chat_patterns = [ r"^(你好|hi|hello|在吗|在不在|谢谢|thanks|好的|ok|知道了)$", r"^(你是谁|你叫什么|你能做什么)", ] self.faq_pattern_keywords = [ "退款", "退货", "发货", "物流", "价格", "优惠", "密码", "登录", "注册", "支付", "运费", "地址", ] # ML 层:用于区分 SEARCH 和 RAG self.vectorizer: Optional[TfidfVectorizer] = None self.classifier: Optional[LogisticRegression] = None if model_path: self.load_model(model_path) else: # 使用默认的轻量模型 self._init_default_model() def _init_default_model(self): """初始化默认 TF-IDF + 逻辑回归(训练样本在下面)""" self.vectorizer = TfidfVectorizer( max_features=5000, ngram_range=(1, 2), min_df=2, ) self.classifier = LogisticRegression( max_iter=1000, C=1.0, class_weight="balanced", ) def classify(self, query: str) -> IntentResult: """ 分类查询意图 先走规则(快速),再走 ML(精确) """ query = query.strip() if not query: return IntentResult(IntentType.CHAT, 1.0, []) # 第一层:规则匹配(延 < 1ms) rule_result = self._rule_based_classify(query) if rule_result.confidence > 0.8: return rule_result # 第二层:ML 分类(延迟 2-5ms) return self._ml_classify(query) def _rule_based_classify(self, query: str) -> IntentResult: """基于规则的快速分类""" # 检测闲聊 for pattern in self.chat_patterns: if re.match(pattern, query, re.IGNORECASE): return IntentResult( IntentType.CHAT, 0.95, keywords=[], ) # 短查询(< 5 个字符)默认归类为闲聊 if len(query) < 5: return IntentResult( IntentType.CHAT, 0.7, keywords=[], fallback_reason="查询过短", ) # 检测 FAQ 关键词 matched_keywords = [ kw for kw in self.faq_pattern_keywords if kw in query ] if matched_keywords and len(query) < 30: return IntentResult( IntentType.FAQ, 0.85, keywords=matched_keywords, ) # 检测搜索特征:包含书名号、技术术语等 search_patterns = [ r"《.+》", # 文档/书名引用 r"(什么是|怎么用|如何使用)", # 知识查询 r"(文档|手册|API|SDK|协议)", # 技术资料 ] for pattern in search_patterns: if re.search(pattern, query): return IntentResult( IntentType.SEARCH, 0.75, keywords=[], ) # 无法通过规则确定,返回低置信度 return IntentResult( IntentType.RAG, 0.5, keywords=[], fallback_reason="规则无法分类,转入 ML 分类", ) def _ml_classify(self, query: str) -> IntentResult: """基于 ML 的分类(SEARCH vs RAG)""" if self.classifier is None or self.vectorizer is None: return IntentResult(IntentType.RAG, 0.6, []) try: vec = self.vectorizer.transform([query]) proba = self.classifier.predict_proba(vec)[0] pred = self.classifier.predict(vec)[0] confidence = max(proba) intent_map = {0: IntentType.SEARCH, 1: IntentType.RAG} intent = intent_map.get(pred, IntentType.RAG) return IntentResult(intent, confidence, []) except Exception as e: # ML 分类失败,默认走 RAG(保守策略) return IntentResult( IntentType.RAG, 0.4, keywords=[], fallback_reason=f"ML 分类异常: {e}", ) def train(self, samples: List[Tuple[str, IntentType]]): """训练 SEARCH vs RAG 分类器""" texts = [s[0] for s in samples] labels = [1 if s[1] == IntentType.RAG else 0 for s in samples] X = self.vectorizer.fit_transform(texts) self.classifier.fit(X, labels) # 输出训练统计 accuracy = self.classifier.score(X, labels) print(f"意图路由器训练完成,准确率: {accuracy:.2%}") def save(self, path: str): joblib.dump( {"vectorizer": self.vectorizer, "classifier": self.classifier}, path, ) def load_model(self, path: str): data = joblib.load(path) self.vectorizer = data["vectorizer"] self.classifier = data["classifier"]完整查询管线
from dataclasses import dataclass from typing import Any, Optional @dataclass class QueryResult: """查询结果""" answer: str source: str # 来源:chat/faq/search/rag cost: float # 成本(元) latency_ms: float # 延迟(毫秒) class QueryPipeline: """查询管线——根据意图分发到不同处理器""" def __init__(self, router: IntentRouter): self.router = router self.chat_handler = ChatHandler() self.faq_handler = FAQHandler() self.search_handler = SearchHandler() self.rag_handler = RAGHandler() # 统计信息 self.stats = { "total": 0, "chat": 0, "faq": 0, "search": 0, "rag": 0, "total_cost": 0.0, } def process(self, query: str) -> QueryResult: """ 处理用户查询——先路由,再分发 """ import time start = time.time() # 意图分类 intent_result = self.router.classify(query) # 按意图分发 if intent_result.intent == IntentType.CHAT: answer = self.chat_handler.handle(query) cost = 0.0 source = "chat" elif intent_result.intent == IntentType.FAQ: answer = self.faq_handler.handle(query, intent_result.keywords) cost = 0.001 source = "faq" elif intent_result.intent == IntentType.SEARCH: answer = self.search_handler.handle(query) cost = 0.01 source = "search" else: # RAG answer = self.rag_handler.handle(query) cost = 0.1 source = "rag" latency = (time.time() - start) * 1000 # 更新统计 self.stats["total"] += 1 self.stats[source] += 1 self.stats["total_cost"] += cost # 如果置信度低,做异步二次验证 if intent_result.confidence < 0.7: self._async_verify(query, intent_result) return QueryResult( answer=answer, source=source, cost=cost, latency_ms=latency, ) def get_cost_savings(self) -> dict: """计算成本节省""" no_router_cost = self.stats["total"] * 0.1 # 全部走 RAG actual_cost = self.stats["total_cost"] savings = no_router_cost - actual_cost return { "total_queries": self.stats["total"], "without_router": f"¥{no_router_cost:.2f}", "with_router": f"¥{actual_cost:.2f}", "savings": f"¥{savings:.2f}", "savings_pct": f"{savings/no_router_cost*100:.1f}%", }四、边界分析与 Trade-offs
路由器自身的成本:
- TF-IDF + 逻辑回归分类:CPU 推理,延迟 < 5ms,成本接近于 0
- 但需要维护训练数据——建议从线上日志中抽样标注
误分类的代价:
- 将 RAG 意图误判为 FAQ:用户体验下降(得到不完整的回答)
- 将闲聊误判为 RAG:成本浪费
- 建议对低置信度分类做二次校验(规则 + 小模型交叉验证)
路由拓扑的适应性:
- 随着业务变化,意图分布可能改变
- 需要定期分析分类准确率和意图分布,更新规则和模型
何时不需要路由器:
- 查询量 < 1000 次/天(成本节省不明显)
- 绝大部分查询都需要 RAG(设置路由器反而增加延迟)
五、总结
用轻量级意图路由器做前置分发,是提高 RAG 系统性价比的有效手段:
- 规则层处理高频简单意图(< 1ms 延迟)
- ML 层区分搜索和推理意图(< 5ms 延迟)
- 统计层监控成本节省效果,持续优化路由策略
在 10 万次/天的查询量下,40% 的闲聊 + FAQ 查询被拦截,日均成本从 3000 元降到约 1200 元——路由器本身的开发成本(约 3 人天)在一周内就回本了。