通用桌面 Agent 的 Query 分类与智能调度:四层意图漏斗设计

Four-Stage Intent Funnel for Desktop Agent Routing

上周和一位做企业桌面 Agent 的朋友聊天,他甩过来一组数据:他们的系统接了 30+ 个 tools,用户 query 从“打开上周的周报”到“帮我把这 15 张截图里的表格数据提取出来汇总成 Excel”都有。当前方案是每个 query 直接扔给 GPT-4o 做 function calling 选 tool——平均延迟 1.8 秒,P99 干到 4 秒,用户抱怨“比我自己干还慢”。

他问我:“是不是换个更快的模型就行?”

不是模型的问题。是架构的问题。

问题不在于“模型不够快”

桌面 Agent 的 query 分类天然是一个长尾分布

  • 头部 50%:高度重复、意图明确的指令(“打开文件”、“搜索 XX”、“运行脚本”)
  • 中部 35%:有一些变化但语义清晰的指令(“帮我把 Downloads 里的 PDF 都移到 Documents”)
  • 尾部 15%:复合意图、模糊指令、跨应用编排(“从上周的会议纪要里提取 action items,发邮件给相关人,并在 Calendar 里创建 deadline”)

拿同一个 LLM 处理这三类 query,就像让一个大学教授去回答“1+1 等于几”还要收一样的钱——浪费且愚蠢。

核心问题是延迟预算的分配。桌面 Agent 的用户体验有一个硬性阈值:acknowledgment < 50ms,首动作 < 1000ms。超过这个阈值,用户感知不是“AI 在思考”,而是“卡住了”。

四层意图漏斗

我给他的建议是一套分层分类架构,我管它叫四层意图漏斗(Four-Stage Intent Funnel)

User Query
┌──────────────────────────────────────┐
│ Stage 1: 规则引擎                     │
│ 正则 + 关键词白名单,< 1ms              │
│ 命中率 ~50%,零 LLM 成本               │
└────────────┬─────────────────────────┘
             │ 未命中
┌──────────────────────────────────────┐
│ Stage 2: Embedding 缓存               │
│ FAISS 内存索引,< 20ms                 │
│ 命中率 ~25%(累计划 75%)              │
└────────────┬─────────────────────────┘
             │ 置信度 < 0.92
┌──────────────────────────────────────┐
│ Stage 3: 微调小模型                    │
│ DistilBERT 领域微调,< 50ms            │
│ 命中率 ~15%(累计划 90%)              │
└────────────┬─────────────────────────┘
             │ 模糊 / 复合意图
┌──────────────────────────────────────┐
│ Stage 4: 完整 LLM                     │
│ Claude Sonnet / GPT-4o,< 800ms       │
│ 覆盖率 ~10%,处理真正的复杂推理         │
└──────────────────────────────────────┘

每一层都是一个滤网,只把兜不住的问题往下传。关键不是“替代 LLM”,而是让 LLM 只做它值得做的事

延迟预算的实际分布

                        延迟(ms)   累计覆盖率   每次成本
Stage 1: 规则引擎         < 1         ~50%        $0
Stage 2: Embedding 缓存   < 20        ~75%        $0.0001
Stage 3: DistilBERT      < 50        ~90%        $0.0003
Stage 4: LLM 兜底         < 800       ~100%       $0.005

加权平均延迟:~100ms。对比单 LLM 方案的 ~1800ms——快了 18 倍

每一层怎么实现

Stage 1:规则引擎

规则引擎的价值在于零延迟封堵高频确定性 query。不要试图覆盖所有情况,只 cover 那些没有歧义的模式:

import re
from dataclasses import dataclass
from enum import Enum
from typing import Optional


class Intent(Enum):
    FILE_READ = "file_read"
    FILE_WRITE = "file_write"
    WEB_SEARCH = "web_search"
    CODE_EXECUTE = "code_execute"
    APP_LAUNCH = "app_launch"
    SYSTEM_CONTROL = "system_control"


@dataclass
class RuleMatch:
    intent: Intent
    confidence: float
    extracted_args: dict


RULES: list[tuple[str, Intent, dict]] = [
    # 文件操作
    (r"\b(?:打开|读|查看|显示)\s*(?:一下)?\s*(?:这个|那个|的)?\s*文件\b", Intent.FILE_READ, {}),
    (r"\b(?:创建|新建|写入|保存)\s*(?:一个|新的)?\s*文件\b", Intent.FILE_WRITE, {}),
    # Web 搜索
    (r"\b(?:搜索|搜|查|百度|Google)\s+(?:一下|一下)?", Intent.WEB_SEARCH, {}),
    # 代码执行
    (r"\b(?:运行|执行|跑)\s*(?:一下|这个)?\s*(?:脚本|代码|python|bash|命令)\b", Intent.CODE_EXECUTE, {}),
    # 应用启动
    (r"\b(?:打开|启动|运行)\s+(?:VS Code|Chrome|Terminal|Excel|Word)\b", Intent.APP_LAUNCH, {}),
]


def rule_classify(query: str) -> Optional[RuleMatch]:
    for pattern, intent, args_template in RULES:
        if match := re.search(pattern, query, re.IGNORECASE):
            return RuleMatch(
                intent=intent,
                confidence=0.95,
                extracted_args=args_template | match.groupdict(),
            )
    return None
# generated by hugo AI

关键原则:规则只写高度确信的模式,宁可漏过也不要误判。规则引擎的 precision 必须接近 100%,recall 低一点没关系——漏掉的交给后面几层。

Stage 2:Embedding 缓存

用 FAISS 做内存级向量索引,将历史分类结果缓存下来。新的 query 如果和历史 query 的余弦相似度 > 0.92,直接复用分类结果:

import faiss
import numpy as np
from dataclasses import dataclass, field


@dataclass
class CachedClassification:
    query: str
    intent: Intent
    timestamp: float


class EmbeddingCache:
    def __init__(self, embedding_dim: int = 768, max_size: int = 10000):
        self.index = faiss.IndexFlatIP(embedding_dim)  # inner product for cosine
        self.entries: list[CachedClassification] = []
        self.max_size = max_size

    def search(self, query_embedding: np.ndarray, threshold: float = 0.92) -> Optional[Intent]:
        if self.index.ntotal == 0:
            return None
        scores, indices = self.index.search(query_embedding.reshape(1, -1), k=1)
        return self.entries[indices[0][0]].intent if scores[0][0] > threshold else None

    def add(self, query: str, query_embedding: np.ndarray, intent: Intent) -> None:
        self.entries.append(CachedClassification(query=query, intent=intent, timestamp=time.time()))
        self.index.add(query_embedding.reshape(1, -1))
        # LRU 淘汰
        if len(self.entries) > self.max_size:
            oldest_idx = min(range(len(self.entries)), key=lambda i: self.entries[i].timestamp)
            self.index.remove_ids(np.array([oldest_idx]))
            del self.entries[oldest_idx]
# generated by hugo AI

这个缓存有两层作用:一是直接加速重复 query,二是为后续的自进化提供数据基础——缓存里的 query 分布变化可以作为 drift 检测的输入。

Stage 3:微调小模型

DistilBERT 在 5k-10k 条桌面 Agent 领域数据上微调后,可以在 CPU 上跑到 ~20ms,5-10 分类场景的准确率能达到 92-95%。

微调数据的构造是关键。不是手工标注,而是用 Stage 4 的 LLM 产出自动标注:

生产环境 query 日志
    → Stage 4 LLM 分类(离线批量,不要求实时)
    → 人工抽样校验(抽 5% 检查准确率)
    → 达标后作为 DistilBERT 微调数据
    → 部署,持续监控 Stage 3 vs Stage 4 的一致率

如果一致率下降到 90% 以下,说明 query 分布发生了变化,触发重新微调。

Stage 4:LLM 兜底

到了这一层的 query,才是 LLM 真正值得发挥的场景——复合意图、跨应用编排、需要上下文理解。这时候 system prompt 的质量至关重要:

CLASSIFIER_SYSTEM_PROMPT = """You are an intent classifier for a desktop AI agent.
Given a user query, classify it into EXACTLY ONE primary intent.

Available intents and their definitions:
- file_read: Read or access file contents
- file_write: Create, write, or modify files
- file_organize: Move, rename, delete, or batch-process files
- web_search: Search or browse the internet
- web_action: Fill forms, submit, interact with web pages
- code_execute: Run scripts, execute commands, git operations
- app_automation: Launch and interact with desktop applications
- system_control: Change system settings, manage processes
- communication: Send emails, messages, schedule meetings
- media_process: Screenshots, image/video editing, OCR

For ambiguous queries, choose the most likely primary intent.
Reply with ONLY the intent name, nothing else.
"""
# generated by hugo AI

注意:这里不是让 LLM 直接做 function calling,而是先做意图分类,再交给专门的 Router 去选 tool。分类和选 tool 解耦的好处是:分类结果可以缓存和审计,选 tool 逻辑可以独立优化(如基于历史成功率做加权)。

路由:意图分类只是第一步

有了意图,下一步是把 query 派发到真正干活的模块。这里三条经验值得强调:

1. 上下文感知的路由

同样的“打开那个文件”,在 VS Code 里说的是当前项目文件,在 Finder 里说的是选中的文件。路由必须感知活跃应用

@dataclass
class RouteContext:
    active_app: str
    recent_actions: list[str]
    user_preferences: dict


class ContextAwareRouter:
    def route(self, intent: Intent, query: str, ctx: RouteContext) -> str:
        # 根据活跃应用微调工具选择
        tool_priority = self._compute_priority(intent, ctx.active_app)
        # 根据历史成功率加权
        best_tool = max(
            tool_priority,
            key=lambda t: self.tool_success_rates.get(t, {}).get(intent.value, 0.5),
        )
        return best_tool
# generated by hugo AI

2. 并行执行 vs 串行执行

不是所有子任务都必须串行。UFO3 Galaxy 的方案是用 DAG 描述子任务依赖,在依赖满足时立即并行执行:

场景执行方式原因
“打开三个文件”三个并行 file_read无依赖关系
“搜到结果 → 打开第一个链接”串行第二步依赖第一步结果
“从 Excel 提取数据 → 生成 PPT”串行产出物依赖
“同时搜 A 和搜 B,然后对比”先并行再串行搜索并行,对比依赖两路结果

3. API 优先,GUI 兜底

这是 UFO 的一个关键设计:能用原生 API(文件系统、AppleScript、Win32 COM)完成的,绝不用 GUI 点击。GUI 自动化只有在没有 API 可用时才介入:

Action selection:
    ├── 有 API?→ 直接调用(< 50ms,确定性 100%)
    ├── 有 URL Scheme?→ deep link 调用
    └── 都没有 → 走 GUI 自动化(500ms-2000ms,非确定性)

这个分层在不同平台上的 API 可用性差异很大——macOS 的 AppleScript + Shortcuts 覆盖率高,Windows 的 COM/UIA 次之,Linux 的 D-Bus 覆盖最弱。

分类方法的对比

方案延迟准确率维护成本适用场景
纯 LLM function calling500-2000ms85-95%tool < 20,低并发
Embedding 相似度10-50ms75-85%高并发简单 query
规则引擎< 1ms90-99%*高频确定性 query
微调小模型20-100ms92-95%高(需标注)中等复杂度
四层混合(推荐)< 100ms 平均90-95%+生产环境全场景

*规则引擎的准确率前提是只 cover 高确信 pattern,precision 优先于 recall

自进化:让系统从错误中学习

架构设计里最容易忽略的是反馈闭环。没有反馈的分类系统是一个注定腐烂的系统——query 分布在变,用户习惯在变,工具的成功率也在变。

我建议的实现分四步走:

┌─────────────────────────────────────────────┐
│              自进化反馈闭环                    │
│                                              │
│  用户纠错 / 隐式反馈(undo、重试)              │
│      │                                       │
│      ▼                                       │
│  记录 (query, 预测意图, 实际意图, 成功/失败)    │
│      │                                       │
│      ▼                                       │
│  每 N 次检查:准确率 < 85%?                   │
│      │                                       │
│      ├── 是 → 触发分析                        │
│      │      ├── 同 pattern 3+ 次 → 更新规则   │
│      │      ├── Embedding 漂移 → 重建缓存     │
│      │      └── 分布整体偏移 → 重新微调        │
│      │                                       │
│      └── 否 → 继续监控                        │
└─────────────────────────────────────────────┘

具体实现上,OS-Copilot 的双记忆模型是一个很好的参照:声明式记忆存用户的长期偏好(“我习惯用 VS Code 而不是 Vim”),程序性记忆存成功的执行轨迹(“处理 PDF 提取 → 汇总到 Excel 这条链路已验证”)。前者用于路由偏好,后者用于跳过重复推理。

from collections import defaultdict


class SelfEvolvingRouter:
    def __init__(self, drift_check_interval: int = 100, accuracy_threshold: float = 0.85):
        self.history: list[dict] = []
        self.drift_check_interval = drift_check_interval
        self.accuracy_threshold = accuracy_threshold
        # pattern → 纠错计数
        self.correction_patterns: defaultdict[str, int] = defaultdict(int)

    def record(self, query: str, predicted: Intent, actual: Intent, success: bool) -> None:
        self.history.append({
            "query": query,
            "predicted": predicted,
            "actual": actual,
            "success": success,
        })

        if not success and predicted != actual:
            pattern_key = f"{predicted.value}{actual.value}"
            self.correction_patterns[pattern_key] += 1

        if len(self.history) % self.drift_check_interval == 0:
            self._check_drift()

    def _check_drift(self) -> None:
        recent = self.history[-self.drift_check_interval:]
        accuracy = sum(1 for r in recent if r["success"]) / len(recent)

        if accuracy < self.accuracy_threshold:
            # 分析高频纠错 pattern
            for pattern, count in self.correction_patterns.items():
                if count >= 3:
                    predicted_intent, actual_intent = pattern.split("→")
                    self._update_rules(predicted_intent, actual_intent)
            self.correction_patterns.clear()

    def _update_rules(self, wrong: str, correct: str) -> None:
        # 基于高频纠错模式自动更新规则引擎的优先级
        # 实现细节取决于具体规则引擎的设计
        pass
# generated by hugo AI

推荐技术栈

组件推荐方案备选
快速分类器DistilBERT 领域微调ModernBERT-base(更准但更慢)
Embedding 模型text-embedding-3-smallall-MiniLM-L6-v2(本地跑免费)
向量索引FAISS 内存模式Chroma(持久化需求)
LLM 兜底Claude Sonnet 4.6 / GPT-4oQwen2.5-VL(多模态支持)
屏幕解析OmniParser v2Windows UIA / macOS Accessibility
Agent 编排LangGraphOpenAI Agents SDK
可观测性LangSmith / 自建 tracing自研埋点系统

总结

这套方案的本质是把“快、准、省”拆解到不同层级上——规则和缓存负责“快”,微调小模型负责“准”,LLM 负责“处理复杂”。所谓智能调度,不是选一个最聪明的模型处理所有问题,而是让每个问题找到恰好够用的执行路径。

如果你在构建桌面 Agent,一个具体的迁移路径是:

  1. 先把现有 LLM 分类日志攒起来,分析 query 分布
  2. 从顶部高频 pattern 开始建立规则引擎和 embedding 缓存
  3. 用 LLM 日志作为标注数据微调小模型
  4. 接上反馈闭环,让它自己持续优化

你实际做桌面 Agent 的时候,最大的延迟瓶颈在哪一层?欢迎留言讨论。


See also