上周和一位做企业桌面 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 calling | 500-2000ms | 85-95% | 低 | tool < 20,低并发 |
| Embedding 相似度 | 10-50ms | 75-85% | 中 | 高并发简单 query |
| 规则引擎 | < 1ms | 90-99%* | 高 | 高频确定性 query |
| 微调小模型 | 20-100ms | 92-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-small | all-MiniLM-L6-v2(本地跑免费) |
| 向量索引 | FAISS 内存模式 | Chroma(持久化需求) |
| LLM 兜底 | Claude Sonnet 4.6 / GPT-4o | Qwen2.5-VL(多模态支持) |
| 屏幕解析 | OmniParser v2 | Windows UIA / macOS Accessibility |
| Agent 编排 | LangGraph | OpenAI Agents SDK |
| 可观测性 | LangSmith / 自建 tracing | 自研埋点系统 |
总结
这套方案的本质是把“快、准、省”拆解到不同层级上——规则和缓存负责“快”,微调小模型负责“准”,LLM 负责“处理复杂”。所谓智能调度,不是选一个最聪明的模型处理所有问题,而是让每个问题找到恰好够用的执行路径。
如果你在构建桌面 Agent,一个具体的迁移路径是:
- 先把现有 LLM 分类日志攒起来,分析 query 分布
- 从顶部高频 pattern 开始建立规则引擎和 embedding 缓存
- 用 LLM 日志作为标注数据微调小模型
- 接上反馈闭环,让它自己持续优化
你实际做桌面 Agent 的时候,最大的延迟瓶颈在哪一层?欢迎留言讨论。