主动性 Agent 的第一道工程题:Fast/Slow Path 两级过滤,附 80 行可跑模拟器

主动性 Agent 工程周 · Day 2(周二)。昨天拆了 dsh-superagent 的整体架构,今天进入最容易被做歪的一环:一条环境事件从进来到"开工"或"闭嘴",中间到底发生了什么。

本文含一个 80 行、纯标准库、可离线复跑的两级过滤模拟器,文末输出经实际运行验证。

一、为什么不能每条消息都过模型

接入真实群聊和代码平台后,事件流有个残酷特征:高噪音、低触发率。一个研发群一天几百条消息,闲聊占九成以上;真正该让 Agent 动手的触发,一天可能一两次。

如果每条消息都调 LLM 从头判断"要不要动",延迟和成本都不可接受。DeepSeek 在《Super-agent 设计探索》里搬来了计算机系统的经典思路 Fast Path / Slow Path:

  • Fast Path(程序层策略):把明确、稳定的高频判断固化成确定性代码------事件来源、群组范围、用户/Bot 白名单,直接代码准入检查。毫秒级、零模型成本,在调模型前滤掉大部分噪音。
  • Slow Path(语义层策略):通过程序层准入的候选事件,才交给 LLM,按提示词里的策略判断"随口吐槽还是真问题"。

工程上的好处很实际:Fast Path 改代码,Slow Path 改提示词。想调整语义策略,改一句话就行,不用发版。

二、准入矩阵:连"谁有资格 @ 它"都是策略

程序层不只是关键词过滤。dsh-superagent 的授权文档把"谁能触发它"写成三列映射(按官方 docs/002-authorization.md):

来源 默认情况 特殊情况
名单内用户 不 @ 时进入推断候选 @ 则算正式请求
陌生人(群聊) @ 也不理 ------
Bot 不接任何请求 仅名单内 Bot 可产生推断候选

两个容易漏的细节:

  1. 陌生人 @ 它也默认不接------显式呼叫不等于授权,防止任何人拉群就能指挥你的 Agent。
  2. 鉴权贯穿任务全生命周期:挂起的任务唤醒时重新拉取主体状态,触发者若被移出名单,执行权限即刻失效。授权不是一次性门票。

三、订阅直通:Router 不能成为堵点

过滤之后是路由。如果已分配任务的每个后续事件都要经过全局 Router 转发,这个常驻节点就是单点瓶颈。DeepSeek 的方案是 Thread 订阅 + 直通:

  • 同一个 GitHub PR、同一封邮件往来、同一个 IM 话题下的连续事件构成一条 Thread;
  • Worker 订阅 Thread 后,后续事件由程序直接送达对应 Worker,不再绕 Router;
  • 全局 Router 只管两件事:新任务冷启动、跨任务协调。

代价账目(官方自报的模拟实验,未经独立审计):去掉订阅直通后,Router 决定数增加约 30%,读缓存 token 从 2323 万增至 3177 万;批处理能把单个决定分摊的推理时间从 6.4 秒压到 1.6 秒。结论直白:架构省下的每一秒,都是用 token 买的。

四、可跑模拟器:两级过滤 + observe/auto 双模式

下面是本次的复现代码(gate_sim.py),纯标准库。Slow Path 用确定性关键词规则"扮演"LLM,以便离线复跑;真实系统里换成模型调用即可:

python 复制代码
# gate_sim.py ------ 两级过滤 + 订阅直通 + 观察模式 最小可跑模拟
# 用法: python gate_sim.py
from dataclasses import dataclass, field

@dataclass
class Event:
    source: str        # 渠道: dev群 / 闲聊群 / github / ci
    sender: str        # 发件人
    sender_type: str   # human / bot / ci
    text: str
    thread_id: str = ""  # Thread 订阅: 同一话题的连续事件直通 Worker

# ---------- 程序层策略 (Fast Path, 对应 docs/002 的三列准入映射) ----------
ALLOW_USERS = {"张三", "李四"}          # 名单内用户: 不@也进推断候选
ALLOW_BOTS = set()                      # 名单内 Bot: 允许产生推断候选
CHANNEL_POLICY = {                      # 渠道策略: infer=推断候选 / ignore=直接丢弃
    "dev群": "infer",
    "github": "infer",
    "ci": "ignore",                     # 无明确发件人的 CI 信号只开兜底槽位
}

def fast_path(ev: Event, mentioned: bool) -> str:
    """返回: direct=正式请求 / infer=推断候选 / ignore=丢弃"""
    if ev.sender_type == "human" and ev.sender in ALLOW_USERS:
        return "direct" if mentioned else CHANNEL_POLICY.get(ev.source, "ignore")
    if mentioned and ev.sender_type == "human" and ev.sender not in ALLOW_USERS:
        return "ignore"                  # 陌生人@也默认不接
    if ev.sender_type == "bot" and ev.sender in ALLOW_BOTS:
        return "infer"
    return "ignore"                      # 名单外Bot / CI 等一律丢弃

# ---------- 语义层策略 (Slow Path, 真实系统=LLM 按提示词裁决) ----------
TRIGGER_WORDS = ("bug", "报错", "复现", "崩溃")

def slow_path(ev: Event) -> bool:
    """团队预写的『静默复现并修复』策略的最简替身"""
    return any(w in ev.text.lower() for w in TRIGGER_WORDS)

# ---------- 接受模式: observe 只记录, auto 才派工 ----------
@dataclass
class Gate:
    mode: str = "observe"                # observe / auto
    would_accept: list = field(default_factory=list)  # observe 模式的『假定接受』日志
    dispatched: list = field(default_factory=list)

    def eval_ticket(self, ev: Event, verdict: str) -> str:
        if verdict != "infer":
            return verdict               # direct=正式请求直接开工; ignore 丢弃
        if self.mode == "auto":
            self.dispatched.append(ev)
            return "dispatch"
        self.would_accept.append(ev)     # observe: 结论可行也只记录, 绝不执行
        return "observe"

# ---------- 主流程: 一批事件过闸门 ----------
def run(events, mentioned_flags, mode="observe"):
    gate = Gate(mode=mode)
    stats = {"总事件": len(events), "FastPath丢弃": 0, "SlowPath拒绝": 0,
             "正式请求": 0, "派发/假定接受": 0}
    for ev, mentioned in zip(events, mentioned_flags):
        v = fast_path(ev, mentioned)
        if v == "ignore":
            stats["FastPath丢弃"] += 1
            continue
        if v == "direct":
            stats["正式请求"] += 1
            gate.eval_ticket(ev, "direct")
            continue
        if slow_path(ev):                # infer: 过 Slow Path 再裁决
            result = gate.eval_ticket(ev, "infer")
            if result in ("dispatch", "observe"):
                stats["派发/假定接受"] += 1
        else:
            stats["SlowPath拒绝"] += 1   # 随口吐槽, 模型判不动
    return gate, stats

if __name__ == "__main__":
    events = [
        Event("dev群",   "张三", "human", "这个接口在并发下会报错, 谁看下?"),
        Event("dev群",   "王五", "human", "今天中午吃什么"),              # 陌生人@
        Event("dev群",   "李四", "human", "周五团建报名接龙啦"),           # 名单内但纯闲聊
        Event("闲聊群",  "张三", "human", "有人养猫吗"),                   # 渠道策略 ignore
        Event("github",  "张三", "human", "PR#42 的 CI 挂了, 疑似 bug"),   # 不@也进候选
        Event("dev群",   "helper_bot", "bot", "构建失败, 请检查"),         # 名单外Bot
    ]
    mentioned = [False, True, False, False, False, False]

    gate_ob, stats_ob = run(events, mentioned, mode="observe")
    gate_au, stats_au = run(events, mentioned, mode="auto")

    print("== observe 模式 (默认) ==")
    for k, v in stats_ob.items():
        print(f"  {k}: {v}")
    print("  假定接受清单:", [f"{e.sender}:{e.text[:12]}" for e in gate_ob.would_accept])

    print("== auto 模式 (灰度后) ==")
    for k, v in stats_au.items():
        print(f"  {k}: {v}")
    print("  派发清单:", [f"{e.sender}:{e.text[:12]}" for e in gate_au.dispatched])

    # 断言: 行为契约
    assert stats_ob["FastPath丢弃"] == 3, "陌生人@/闲聊群/名单外Bot 共 3 条应被 Fast Path 滤掉"
    assert stats_ob["SlowPath拒绝"] == 1, "团建接龙应被语义层拒绝"
    assert len(gate_ob.would_accept) == 2, "observe 下应有 2 条假定接受(报错+PR bug)"
    assert len(gate_au.dispatched) == 2, "auto 下应有 2 条真派发"
    assert gate_au.dispatched == gate_ob.would_accept, "auto 派发与 observe 记录应一一对应"
    print("全部断言 PASS")

实际运行输出(Python 3, 两次运行结果逐字一致):

less 复制代码
== observe 模式 (默认) ==
  总事件: 6
  FastPath丢弃: 3
  SlowPath拒绝: 1
  正式请求: 0
  派发/假定接受: 2
  假定接受清单: ['张三:这个接口在并发下会报错,', '张三:PR#42 的 CI 挂']
== auto 模式 (灰度后) ==
  总事件: 6
  FastPath丢弃: 3
  SlowPath拒绝: 1
  正式请求: 0
  派发/假定接受: 2
  派发清单: ['张三:这个接口在并发下会报错,', '张三:PR#42 的 CI 挂']
全部断言 PASS

看输出:6 条事件里,3 条死在 Fast Path(零模型成本),1 条死在 Slow Path,只有 2 条有资格进入"派发或记录"------这就是"高噪音、低触发率"下的漏斗形状。observe 与 auto 的唯一差别是最后一行的动作,两份清单一一对应,这正是官方"先 observe 看日志、顺眼了再开 auto"灰度路径的可运行版本。

五、踩坑记录(本次实测真实踩到)

  1. 断言里的期望值是拍脑袋写的 。初版我预设"Fast Path 应滤掉 4 条",跑完才发现实际是 3 条------写用例时没逐条推演 fast_path 的分支。教训:先手推每个用例的预期路径,再写断言,否则测试只是"复述代码"。
  2. 关键词规则会误伤语义 。TRIGGER_WORDS 里如果放"挂","团建报名接龙啦"里含"接龙"倒没事,但"进度挂了没"这类闲聊会被误触发。真实系统里 Slow Path 是 LLM,门槛高一些,但误报率的压力会转嫁给提示词质量------这就是官方说"规则是人写的,写歪了它就照着歪做"的代码级验证。
  3. @ 不是授权 。模拟器里王五 @ 了 Agent 但被 ignore,对应官方准入矩阵"陌生人 @ 也不理"。很多自建 Agent 的第一版漏洞就是把 mentioned 当唯一门控。
  4. CI 信号没有发件人 。sender_type == "ci" 的事件在准入映射里没有自然归属,官方也只给兜底槽位。自建系统里要显式决定:CI 信号走白名单仓库过滤,还是直接忽略。

六、局限说明

  • 模拟器的 Slow Path 是关键词替身,不含真实 LLM 的语义泛化能力;误报率数字不具外推性。
  • 官方所有性能数字(批处理 6.4→1.6 秒、直通 +30% 决策、token 2323 万→3177 万等)为团队自报的模拟/合成负载测试结果,未经独立审计。
  • dsh-superagent-prototype 是研究原型,README 明言非官方产品、非生产就绪,部分论文设计未实现。

七、延伸

明天(Day 3)讲权限与治理:Mandate 为什么必须不可变、"消息是决策输入不能变更权限"这条防注入的架构级答案。给读者的作业:把 slow_path 换成你们团队的触发词表,跑一遍自己群的真实消息导出,数数漏斗每层的量------你的 Agent 该沉默的时刻,比你想的多。

相关推荐
米小虾1 小时前
Agent 省钱工程:七成的钱不是模型花掉的
人工智能·agent
用户117910488331 小时前
AI输出满屏井号星号?hmharness把Markdown渲染成终端表格 GitHub:swsgbl/hmharness
ai编程·harmonyos
高调的涵涵1 小时前
不换模型,只改 Agent 外壳:多考 11.5 分
ai编程
不一样的少年_2 小时前
不花一分额外 API 钱,用现成的 Claude Code / Codex / Cursor 搭建 AI 个人知识库
前端·后端·ai编程
aiks_yuan2 小时前
WorkBuddy 怎么做网页小游戏:深海钓鱼游戏 43 次对话、301 积分从一句话做到发布(附手机横屏踩坑)
ai编程
阿里云云原生2 小时前
欢迎报名丨Alibaba Cloud AgentCore 企业智能体构建与治理八城巡回沙龙 · 杭州站开启
agent
掘黄金的打工人2 小时前
LangChain PromptTemplate 提示词模板
langchain·ai编程
谟松3 小时前
Claude Code 上下文爆了、/compact 也报 400:18MB 会话的抢救全过程
ai编程