主动性 Agent 收官:一个 60 行的"最小可行主动 Agent"骨架,以及本周行业给出的三份校准答案

3 阅读6分钟

文中模型性能与市场数据均为官方/机构自报,未经独立审计;代码为纯标准库演示,非生产基准。

本周一到五我们拆完了 dsh-superagent 的主动性工程线(谱系架构 → 触发与沉默 → 权限与治理 → 安全 → 成本与评测)。周五刚收官,行业就送来三份"期末答案":GPT-6 Cyber 把越界规避率做成了指标(99.93%)、Claude 理财钉死只读权限、Menlo 报告显示美企开源模型支出 89% 流向中国模型。今天用这三份证据校准一个可以直接抄走的东西:最小可行主动 Agent(MVPA)骨架。

一、MVPA 六部件:缺一个,生产环境用事故补课

Source(事件源) → Filter(过滤) → Eval(门控) → Mandate(授权) → Executor(执行) → Reporter(汇报)

与本周各天的对应关系:

部件出自一句话要点
SourceDay 1到达即持久化,不活在内存里
FilterDay 2便宜规则挡前面,大多数事件死在模型看到之前
EvalDay 2/4规则优先,模型只做解释;"不越界"是指标不是美德
MandateDay 3不可变授权:为什么开工/能碰什么/何时过期
Executor—(今日补课)错误分类 + 退避 + 熔断 + 快照
ReporterDay 2干活时闭嘴,收尾必须开口

二、可运行骨架(纯标准库,直接跑)

"""mvpa_skeleton.py —— 最小可行主动 Agent(MVPA)骨架
纯标准库,python3 mvpa_skeleton.py 直接运行;文末自带断言。
均为演示用模拟值,非生产基准。
"""
import time
from dataclasses import dataclass

# ---------- 1. 事件 ----------
class Event:
    def __init__(self, source: str, text: str, ts: float, mentions_agent: bool = False):
        self.source, self.text, self.ts, self.mentions_agent = source, text, ts, mentions_agent

# ---------- 2. 过滤层:便宜规则优先 ----------
KEYWORDS = ("复现", "报错", "修复", "pr")
BAN_SOURCES = {"spam-bot"}

def cheap_filter(ev: Event) -> bool:
    if ev.source in BAN_SOURCES:
        return False
    return any(k in ev.text for k in KEYWORDS)

# ---------- 3. Eval 门控:规则优先 ----------
def eval_gate(ev: Event) -> bool:
    return ev.mentions_agent or "复现" in ev.text

# ---------- 4. Mandate:不可变授权 ----------
@dataclass(frozen=True)
class Mandate:
    task_id: str
    reason: str
    allowed_tools: tuple
    expires_at: float

    def expired(self, now: float) -> bool:
        return now >= self.expires_at

def issue_mandate(ev: Event, now: float) -> Mandate:
    return Mandate(task_id=f"{ev.source}:{int(now)}",
                   reason=f"命中规则: {ev.text[:20]}",
                   allowed_tools=("repo.read", "repo.branch", "pr.create"),
                   expires_at=now + 600)

# ---------- 5. Executor:错误分类 + 熔断 ----------
class CircuitOpen(Exception):
    pass

class Executor:
    def __init__(self, threshold: int = 5):
        self.fail_streak, self.threshold = 0, threshold

    def run(self, mandate: Mandate, ok: bool) -> str:
        if self.fail_streak >= self.threshold:
            raise CircuitOpen("熔断中,任务转入人工队列")
        if ok:
            self.fail_streak = 0
            return f"[{mandate.task_id}] 完成,工具={mandate.allowed_tools}"
        self.fail_streak += 1  # 可重试错误:真实实现在此处退避后重试
        raise RuntimeError("TRANSIENT: 沙箱超时")

# ---------- 6. 汇报 ----------
def report_back(source: str, msg: str):
    print(f"→ @{source}: {msg}")

# ---------- 演示 ----------
if __name__ == "__main__":
    now = time.time()
    execr = Executor(threshold=5)
    events = [
        Event("spam-bot", "领福利啦", now),
        Event("群聊",     "今天天气不错", now),
        Event("群聊",     "@agent 帮我复现这个报错", now, mentions_agent=True),
        Event("issue",    "CI 连续报错,请复现", now),
    ]
    done = []
    for ev in events:
        if not cheap_filter(ev):
            continue  # 大多数事件死在这里
        if not eval_gate(ev):
            continue
        m = issue_mandate(ev, now)
        assert not m.expired(now)
        try:
            done.append(execr.run(m, ok=True))
        except Exception as e:
            report_back(ev.source, f"失败:{e}")
    for d in done:
        print(d)

    # ---- 断言 ----
    assert len(done) == 2
    assert execr.fail_streak == 0
    raised = 0
    for _ in range(6):
        try:
            execr.run(issue_mandate(events[3], now), ok=False)
        except RuntimeError:
            raised += 1
        except CircuitOpen:
            pass
    assert raised == 5  # 5 次可重试失败,第 6 次被熔断拦截
    m = issue_mandate(events[2], now)
    assert m.expired(now + 601) and not m.expired(now + 599)
    print("ALL PASS")

预期输出:ALL PASS(两条演示任务完成、5 次瞬态失败后熔断、Mandate 过期判定正确)。

写这个骨架时自己踩的两个坑:

  1. 熔断判定写在了 run 开头,但失败计数忘了在熔断触发后归零——第一次版本里熔断一旦触发永远打开。生产实现需要半开状态(冷却期后放一个探测请求),本骨架为压缩篇幅省略,但你抄的时候别省。
  2. allowed_tools 最初用 list——dataclass frozen=True 只冻结字段引用,list 内容照样能改,"不可变授权"就成了摆设。改 tuple,或者存进去后包一层 MappingProxyType。

三、三份行业证据,校准三个层

Eval 层 ← GPT-6 Cyber(OpenAI,11-05)。 ExploitGym 48.5%(上代 32.1%)、Firefox 81 个新高危、Linux 内核 948 个本地提权 exploit 这些是能力;工程上更值得抄的是 ExploitGym Honeypot:评测环境故意放"顺手可得"的越界诱饵,专测忍不忍得住——99.93% 越界规避(无部署提示)/100%(有提示)。搬回来:给你的 Agent 单设"越界探测"用例集,越界尝试率应与准确率平级进上线清单。Preparedness Framework 评级升为 Critical,访问走 Daybreak Red 审核制(MFA/SSO/IP 白名单)——头部厂商都不敢直接放开,你的 auto 模式更该先 observe 两周。

Mandate 层 ← Claude Personal Finance(Anthropic,11-06)。 只读连接、两年交易上限、不可动账、对话不入训练。消费级金融是权限敏感度天花板,答案仍是"默认零写权限,写操作单独签凭证"。对照反例:多少内部 Agent 第一步就开全权限,靠提示词求模型别乱来——提示词是上下文,上下文是输入,输入可以被注入。

成本层 ← Menlo 报告(11-06/07)+ 开源放权潮。 美企开源模型支出 89% 流向中国模型(Kimi 61%、DeepSeek 15%、GLM 11%),OpenRouter 上中国模型 token 调用连续 27 周超美国(Menlo 自承样本偏开发者/中小企业);同周末 Kimi 开放 2.8T 权重、混元 Hy4 发布即开源、Inkling 2 Small 将以 Apache 2.0 开源、MNN-Pocket-0.3B 在 iPhone 18 Pro 离线 350+ token/s。结论:Filter/Eval 这种高频短上下文层用小模型 + 缓存,从省钱技巧变成默认架构。周五算过值守/执行差三个数量级——底座再降一次价,你的三本账(token/时间/评测次数)更该每周重算。

四、社区本周补的两课

Executor 的值班员守则(掘金《Agent 死循环的根因不在模型》,juejin.cn/post/7704594117820891172):根因是没有类型化错误的 dispatch,四层改造 = 错误分类/退避/熔断/快照(os.replace 原子写)。两条实测经验:熔断阈值 2 次太敏感(网络抖动误伤),5 次是平衡点;重试预算放任务层而非调用层——十几个工具会把"单调用 3 次"放大成几十次请求。佐证:GitHub 周榜冠军 claude-mem(周增约 9300 星)11-06 的 v15.10.1 恰好修复"429 后丢弃排队写入批次"——11 万星的记忆插件都会弄丢 Agent 的"昨天",事件源必须自己做持久化 + 退避。

横切关注点进中间件(掘金《LangChain v1 中间件实战》,juejin.cn/post/7704499758711652387):6 个生命周期钩子、stateSchema、wrapModelCall 短路、洋葱模型注册顺序。一句话:门控是钩子,不是 if——散落在业务代码里的权限判断,迟早会在重构中被"顺手优化"掉。

五、上线检查清单(合入 CI 的那种)

  • Mandate 存在且不可变(frozen dataclass / MappingProxyType)
  • 权限表默写一遍,关闭所有"开着但没用"的写权限
  • Filter 层误放行/误拦截有埋点,小模型降级账算过一遍
  • 事件源:队列持久化 + 退避重试 + 原子快照,人为 429 演练通过
  • 越界探测用例 ≥10 条,越界尝试率进上线指标
  • auto 模式前 observe 模式 ≥2 周,日志有人看

数字声明:GPT-6 Cyber / Menlo 报告 / claude-mem 数据均为官方或机构自报,本文仅转述未独立核验;骨架代码为教学演示。