上周三晚上 11 点,老王给我发消息:他们的桌面 Agent 上线刚满 30 天,DAU 爬到 8000,团队 4 个人。他翻了一周用户行为日志,发现一个反直觉的事实——用过 3 次以上的用户里,62% 只把它当"自然语言版的快捷启动器"用,真正让它做跨应用编排的不到 13%。但他们的技术栈正按"复杂编排"在搭:每个 query 直接扔给 GPT-4o 做 function calling,P50 延迟 1.6 秒,P99 干到 3.8 秒。
老王问:“我看了你之前那个四层意图漏斗,要不要现在就全套上?团队就 4 个人,老板说三个月内要把日活做到 5 万,怕 over-engineering。”
我的答案:要上,但不是一次上四层。1 个月的 Agent 团队最缺的不是模型层数,是能让你做对决策的数据。
本质问题:你不缺架构,你缺标注数据
我看到太多团队在月 1 就照搬"规则 + 缓存 + 微调小模型 + LLM"的完整方案,最后掉进同一个坑:为了那个还没有训练数据的微调小模型,提前搭了一堆基础设施,结果两个月过去线上跑的还是单 LLM,团队精力全耗在调框架上。
桌面 Agent 这类系统,决定演进顺序的不是技术复杂度,是数据成熟度:
阶段 Query 日志量 标注覆盖 分布稳定性 可做的事
─────────────────────────────────────────────────────────────
月 1-2 < 50k 0% 很不稳定 规则 + LLM
月 3-4 50k-500k > 60% 逐步收敛 + 缓存 + 小模型
月 6+ > 1M > 80% 接近稳定 + 自进化闭环
老王的团队卡在月 1-2,但他想按月 6+ 的架构搭。这就是问题。
四层漏斗:方向对,但要分批上
先把目标架构摆在这里——这是未来 6 个月的终态,不是 1 个月的起点:
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%,处理真正的复杂推理 │
└──────────────────────────────────────┘
加权后平均延迟从 1800ms 压到 100ms 左右。但这是有了 50 万条标注数据之后的样子。月 1 团队上来就建 Stage 3 小模型,相当于在没有水的河床上修水电站。
三阶段实施路径
我给老王画了一张时间轴。每个阶段只做这个阶段最该做的事:
月 1-2 (现在) 月 3-4 月 6+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
现在该做 │ 规则引擎 │ │ Embedding │ │ 微调小模型 │
│ + 数据管道 │ │ 缓存 │ │ + 自进化 │
│ + LLM 兜底 │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
触发条件 上线即做 日 query > 5k 日 query > 50k
已经有用户 + 标注 > 30% + 分布稳定
接下来一节一节讲:每一阶段做什么、为什么是这个顺序、什么时候可以进下一阶段。
阶段 A(月 1-2):规则 + LLM + 数据管道
做这三件事,别的先别碰。
A.1 规则引擎:覆盖头部 30% 就够
不要追求"覆盖 50%"。月 1 的 query 分布还在剧烈变化,你今天写的规则下个月可能就过时。只兜那些绝对不会有歧义的 pattern:
import re
from dataclasses import dataclass
from enum import Enum
from typing import Optional
class Intent(Enum):
APP_LAUNCH = "app_launch"
FILE_OPEN = "file_open"
WEB_SEARCH = "web_search"
@dataclass
class RuleMatch:
intent: Intent
confidence: float
raw_query: str
# 月 1 只需要 5-10 条规则,覆盖头部场景
RULES: list[tuple[str, Intent]] = [
(r"^(?:打开|启动|open)\s+(VS Code|Chrome|Terminal|Excel|Word|微信)", Intent.APP_LAUNCH),
(r"^(?:搜索|搜|google|百度)\s+\S+", Intent.WEB_SEARCH),
(r"^(?:打开|查看)\s+.{1,20}\.(pdf|docx?|xlsx?|md|txt)$", Intent.FILE_OPEN),
]
def rule_classify(query: str) -> Optional[RuleMatch]:
for pattern, intent in RULES:
if re.search(pattern, query.strip(), re.IGNORECASE):
return RuleMatch(intent=intent, confidence=0.98, raw_query=query)
return None
# generated by hugo AI
为什么不写更多? 月 1 你不知道哪些 pattern 会持续存在。写 50 条规则,3 个月后可能 30 条已经不再匹配真实分布,维护成本反而高。规则要后置补充,不是前置铺满。
A.2 数据管道:这才是月 1 最值钱的事
老王听到这一步说:“就埋点呗,能有多复杂?” 我让他打开自己的代码看了一眼——他们现在只记了 query + response,没有记意图标注、tool 选择、执行结果、用户后续行为。这种日志后面想做小模型微调,全得人工重标。
月 1-2 必须先把这套结构落下来:
import time
import uuid
from dataclasses import dataclass, asdict, field
from typing import Optional
@dataclass
class QueryTrace:
trace_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: float = field(default_factory=time.time)
# 输入侧
query: str = ""
active_app: str = ""
user_id_hash: str = ""
# 路由决策
classifier_stage: str = "" # "rule" | "llm"
predicted_intent: str = ""
intent_confidence: float = 0.0
selected_tool: str = ""
# 执行结果
tool_success: Optional[bool] = None
latency_ms: int = 0
# 隐式反馈(最重要!)
user_undid: bool = False # 用户是否撤销
user_retried_within_30s: bool = False
user_rephrased: bool = False # 30s 内换了说法重问
def log_trace(trace: QueryTrace) -> None:
# 写入你现有的日志系统:Loki / ClickHouse / S3 都行
# 关键是 schema 必须从月 1 就稳定下来
pass
# generated by hugo AI
为什么这是月 1 最重要的事? 因为月 3 你想训小模型时,能不能省掉 80% 的人工标注成本,全看月 1-2 这套日志结构合不合格。隐式反馈(undo、重试、改述)是最廉价、最准确的标注源——用户用脚投票了。
A.3 LLM 兜底:分类与执行解耦
老王现在的做法是把 query 直接交给 GPT-4o 做 function calling。我让他改成两步:
原方案:query → GPT-4o(function calling) → 直接执行
─────────────────────────
一个调用同时做:意图识别 + 参数抽取 + tool 选择
问题:决策不可缓存、不可审计、换模型成本高
新方案:query → GPT-4o(intent classification) → Router(规则+成功率) → tool
────────────────────── ──────────────
只做意图分类,结果可缓存 选 tool 的逻辑独立可测
为什么要拆?因为 Stage 2(embedding 缓存)能缓存的是"意图",不是"执行"。query “把这个 PDF 翻译成中文” 和 “翻译这份 PDF 成中文” 是同一个意图,但参数(文件路径)不同,缓存意图就能命中,缓存执行结果就不行。这一步不拆,月 3 想加缓存层时整个链路都得重写。
阶段 B(月 3-4):加 Embedding 缓存
进入这一阶段的触发条件:日 query 量稳定 > 5k,且 A 阶段日志里至少 30% 的 query 已经被规则或 LLM 自动打上了高置信度意图标签。
为什么是这两个条件?
- 日 query < 5k 时,缓存命中率会很低(毕竟用户说法千奇百怪),加这一层 ROI 不划算。
- 标注覆盖 < 30% 时,缓存里塞的都是 LLM 标注的"伪标签",错误会扩散。
import time
import faiss
import numpy as np
from dataclasses import dataclass
@dataclass
class CachedClassification:
query: str
intent: Intent
timestamp: float
hit_count: int = 0
class EmbeddingCache:
def __init__(self, embedding_dim: int = 384, max_size: int = 20000):
self.index = faiss.IndexFlatIP(embedding_dim)
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)
if scores[0][0] > threshold:
entry = self.entries[indices[0][0]]
entry.hit_count += 1
return entry.intent
return 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))
if len(self.entries) > self.max_size:
# 淘汰策略:LFU 优于 LRU,因为高频 query 才是缓存的本体价值
victim = min(range(len(self.entries)), key=lambda i: self.entries[i].hit_count)
self.index.remove_ids(np.array([victim]))
del self.entries[victim]
# generated by hugo AI
月 1 团队常踩的坑:用 OpenAI text-embedding-3-small。每次缓存查询都要 API 调用,延迟 50-150ms,比缓存本身的 20ms 慢得多。月 3 上 embedding 缓存时一定要本地化,用 all-MiniLM-L6-v2(22M 参数,CPU 上 5ms)或 BGE-small。
这一阶段还有一件事可以同步做:把规则引擎从 5-10 条扩到 30-50 条。这时候你已经有 2 个月日志,能看清哪些 pattern 是真实存在的高频长尾。
阶段 C(月 6+):微调小模型 + 自进化闭环
进入这一阶段的触发条件:累计 query 量 > 50 万,标注覆盖 > 60%,且头部 100 个意图的分布在最近一个月波动 < 15%。
第三个条件最容易被忽视。分布还在剧烈变化时去微调小模型,等于追着尾巴跑——刚训好模型,分布又变了。
到这一阶段,微调数据的构造可以走自动化:
A 阶段日志(已有意图标签)
├── LLM 高置信度标注(confidence > 0.9)→ 直接进训练集
├── 规则命中标注 → 直接进训练集
└── 用户隐式反馈纠正过的 → 高权重进训练集
↓
DistilBERT / ModernBERT 微调
↓
线上 shadow mode 跑 1 周(不影响用户,对比 Stage 4 LLM 的结果)
↓
一致率 > 92% → 切流量到 Stage 3
自进化闭环的核心是让规则引擎自己长大。每当某个 LLM 分类结果连续 N 次(比如 10 次)出现在相似 query 上,就提议生成一条新规则,人工 review 后入库。
from collections import defaultdict
class RuleProposer:
def __init__(self, threshold: int = 10):
self.pattern_counter: defaultdict[tuple[str, Intent], int] = defaultdict(int)
self.threshold = threshold
def observe(self, query: str, llm_intent: Intent) -> Optional[str]:
# 提取 query 的关键词模板(去除文件名、URL 等变量)
template = self._extract_template(query)
key = (template, llm_intent)
self.pattern_counter[key] += 1
if self.pattern_counter[key] >= self.threshold:
return self._propose_rule(template, llm_intent)
return None
def _extract_template(self, query: str) -> str:
# 实现细节:用 spaCy / jieba 提取动词 + 名词骨架
# 例如 "打开 Chrome 浏览器" → "打开 ${APP}"
return query # 简化示意
def _propose_rule(self, template: str, intent: Intent) -> str:
return f"建议规则: {template} → {intent.value}"
# generated by hugo AI
为什么是这个顺序:三个反直觉点
老王看完路线图问了三个问题,正好对应三个常见误区。
误区一:先上小模型,能省 LLM 钱
错。月 1-2 你的 LLM 调用量大概率不到 100 万次/月,按 GPT-4o $5/M token 算,意图分类那点 token,总成本不超过 $300/月。一个工程师一周的工资足够烧三个月 LLM。
但维护一个微调模型——数据清洗、训练、评估、版本管理、回滚机制——是一个长期占用 0.5 个工程师的活。月 1 团队 4 个人,掏不起这 12.5% 的人力。
误区二:缓存月 1 就能上,反正不复杂
也错。Embedding 缓存的命中率取决于 query 重复度。日活 8000、日 query 1.5 万的产品,前 100 个高频 query 大概只能覆盖 8-12% 的总流量,剩下 88% 是各种低频长尾。这种分布下缓存命中率不到 15%,加进来还要扛额外的 embedding 调用延迟,划不来。
要等用户量上去、行为模式收敛后,缓存才有 ROI。
误区三:先把规则铺满,能少调 LLM
最容易栽的一个。规则的真实成本不在写,而在维护。30 条规则的回归测试集合大概有 200-500 条 case,每改一条规则都要全跑一遍。你愿意为了省 30% 的 LLM 调用,多养一份持续维护的回归测试吗?
正确顺序是:先用 LLM 兜底,让数据告诉你哪些 pattern 真的高频且稳定,再把规则精准地加上去。
路由决策的两条经验(不分阶段,从月 1 就该做)
有两件事不分阶段,从第一天就要做对:
1. 上下文感知
同样的"打开那个文件",在 VS Code 里和 Finder 里指的不是同一个东西。任何阶段的 Router 都必须感知 active_app + 最近 5 次操作。这个上下文上线第一天就要采集,否则后面所有路由决策都是瞎子。
2. API 优先,GUI 兜底
Action selection:
├── 有原生 API?→ 直接调用(< 50ms,确定性 100%)
├── 有 URL Scheme / deep link?→ scheme 调用
└── 都没有 → GUI 自动化(500-2000ms,非确定性)
UFO 团队的实践经验:能用 AppleScript / Win32 COM / D-Bus 完成的,绝不用 GUI 点击。这条原则月 1 就要立,因为如果你前期所有 tool 都是 GUI 自动化封装的,等到月 6 想换成 API 时,整个 tool 库都要重写。
总结:这才是 1 个月团队的实施清单
月 1-2 必做:
□ 规则引擎(5-10 条高置信度规则)
□ 数据管道(QueryTrace schema + 隐式反馈)
□ LLM 调用拆成 "意图分类 → Router → tool"
□ 上下文采集(active_app + 操作历史)
□ tool 抽象层(API 优先 GUI 兜底的接口)
月 3-4 触发条件达成后做:
□ Embedding 缓存(本地模型,LFU 淘汰)
□ 规则扩展到 30-50 条(基于真实日志)
月 6+ 触发条件达成后做:
□ 微调小模型(DistilBERT / ModernBERT)
□ Shadow mode 验证
□ 规则自动提议机制
□ 完整自进化闭环
四层漏斗是终态,不是起点。月 1 团队的核心决策不是"要不要上完整架构",而是"现在该把 1 个月后能加架构的地基打哪些"。地基打错(比如 LLM 调用没解耦、日志没结构化),后面每一阶段都要付重写代价。
老王的团队最后选了这条路径:第一周补完了 QueryTrace 日志,第二周拆了 LLM 调用,规则只写了 6 条。延迟从 P50 1.6 秒降到 1.2 秒(拆解后并行了一些操作),但更重要的是——他们月 3 想上小模型时,有干净的 30 万条标注数据可以直接用。
你的桌面 Agent 现在在哪个阶段?最大的瓶颈是延迟、数据,还是工程人力?欢迎留言讨论。