首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >给 Agent 加一层自进化闸门:评测隔离、契约校验与成本封顶

给 Agent 加一层自进化闸门:评测隔离、契约校验与成本封顶

原创
作者头像
用户9746675
发布2026-08-04 23:14:14
发布2026-08-04 23:14:14
1240
举报

LlamaFactory 作者郑耀威团队刚开源的 PenguinHarness,主打一件事:让 Agent 自己迭代,而不是人手工调提示词。宣传数字很刺眼——0.2 元从零造一个 Agent,准确率还能从 50% 提到 90%。但它有个不那么显眼、却更关键的设计:GDPevo 评测把训练集和测试集硬拆开,就是怕进化过程中 Agent 把答案背下来。

这篇不聊框架本身,直接给一个能跑的最小自进化闸门。Python 3.10+ 标准库即可,四个部件:评测集隔离、技能变更契约、单位任务成本封顶、只有 held-out 分数上升才晋升的进化循环。文末有完整串联示例和运行输出。

一、先把训练集和评测集拆开

Agent 自进化最常见的骗局是:进化时能看到评测样本,分数涨了,换一批真实任务又掉回去。解法很朴素,样本一开始就按 id 哈希切开,进化只碰 train,晋升只看 holdout。

代码语言:python
复制
import hashlib
from dataclasses import dataclass, field
from typing import Callable


@dataclass(frozen=True)
class Sample:
    sid: str
    question: str
    answer: str


def split_holdout(samples: list, holdout_ratio: float = 0.3, seed: str = "v1"):
    train, holdout = [], []
    for s in samples:
        h = int(hashlib.sha256((s.sid + seed).encode()).hexdigest(), 16)
        (holdout if (h % 100) < int(holdout_ratio * 100) else train).append(s)
    return train, holdout


def score(agent: Callable, samples: list) -> float:
    if not samples:
        return 0.0
    ok = sum(1 for s in samples if agent(s.question) == s.answer)
    return ok / len(samples)

seed 固定后,同一批样本的切分可复现;换 seed 等于换一次盲测,适合定期轮换 holdout,进一步压低背答案的空间。

二、技能变更必须过契约

自进化如果允许 Agent 随便改自己的 skill 文件,很快就会写出“看到这道题就直接返回标准答案”这类捷径。契约层做三件事:禁止访问 holdout id、禁止硬编码答案表、变更必须声明意图。

代码语言:python
复制
import re


@dataclass
class SkillPatch:
    name: str
    intent: str
    source: str


class ContractError(Exception):
    pass


def check_contract(patch: SkillPatch, holdout_ids: set) -> None:
    if not patch.intent.strip():
        raise ContractError("变更必须声明意图")
    for sid in holdout_ids:
        if sid in patch.source:
            raise ContractError(f"技能源码不得引用 holdout 样本 id: {sid}")
    if re.search(r"ANSWER_TABLE|标准答案|直接返回", patch.source):
        raise ContractError("疑似硬编码答案捷径,拒绝晋升")
    if "eval(" in patch.source or "exec(" in patch.source:
        raise ContractError("禁止动态执行")

契约拦不住所有作弊,但它把“最廉价的作弊路径”先堵上了。真正要防住的是分数上涨但行为不可解释,所以晋升时还要看 holdout,而不是只看 train。

三、单位任务成本必须封顶

PenguinHarness 强调 0.2 元造 Agent,背后其实是成本闸门:进化可以跑,但不能无限烧 token。最小实现是给每次调用记账,超过预算就停。

代码语言:python
复制
@dataclass
class Budget:
    max_yuan: float
    spent: float = 0.0

    def charge(self, tokens_in: int, tokens_out: int,
               price_in: float = 12.0, price_out: float = 36.0) -> None:
        # 价格单位:元 / 百万 token,可按实际模型改
        cost = tokens_in * price_in / 1_000_000 + tokens_out * price_out / 1_000_000
        if self.spent + cost > self.max_yuan:
            raise RuntimeError(f"超出预算:已花 {self.spent:.4f},本次 {cost:.4f}")
        self.spent += cost

业务里建议再加一个“单位成功任务成本”:只统计最终成功的任务,失败重试的 token 也算进去。这个数比 FLOPS 更能指导你该不该继续进化。

四、进化循环:只有 holdout 上涨才晋升

代码语言:python
复制
@dataclass
class AgentState:
    skills: dict = field(default_factory=dict)
    version: int = 0


def apply_patch(state: AgentState, patch: SkillPatch) -> AgentState:
    new_skills = dict(state.skills)
    new_skills[patch.name] = patch.source
    return AgentState(skills=new_skills, version=state.version + 1)


def make_agent(state: AgentState) -> Callable:
    # 演示用:技能里写 "prefix:xx" 表示答案以 xx 开头则命中
    rules = []
    for src in state.skills.values():
        m = re.search(r"prefix:(.+)$", src.strip())
        if m:
            rules.append(m.group(1))
    def agent(q: str) -> str:
        for p in rules:
            if q.startswith(p):
                return "yes"
        return "no"
    return agent


def evolve_once(state, patch, train, holdout, holdout_ids, budget: Budget):
    check_contract(patch, holdout_ids)
    budget.charge(tokens_in=800, tokens_out=200)  # 模拟一次进化开销
    candidate = apply_patch(state, patch)
    before = score(make_agent(state), holdout)
    after = score(make_agent(candidate), train), score(make_agent(candidate), holdout)
    train_s, hold_s = after
    if hold_s <= before:
        return state, {"promoted": False, "reason": "holdout 未提升",
                       "train": train_s, "holdout": hold_s, "spent": budget.spent}
    return candidate, {"promoted": True, "train": train_s, "holdout": hold_s,
                       "spent": budget.spent}

关键不变量只有一句:hold_s <= before 就拒绝晋升。train 分再高,只要 holdout 没动,就当作过拟合丢掉。

五、串起来跑一遍

代码语言:python
复制
samples = [
    Sample("t1", "发票报销怎么走", "yes"),
    Sample("t2", "发票丢失怎么办", "yes"),
    Sample("t3", "合同审批流程", "yes"),
    Sample("h1", "发票抬头改错了", "yes"),
    Sample("h2", "差旅补贴标准", "no"),
    Sample("h3", "合同作废条件", "yes"),
]
train, holdout = split_holdout(samples, holdout_ratio=0.5, seed="demo")
holdout_ids = {s.sid for s in holdout}
state = AgentState()
budget = Budget(max_yuan=0.2)

patches = [
    SkillPatch("invoice", "识别发票类问题", "prefix:发票"),
    SkillPatch("cheat", "偷瞄评测", f"看 {next(iter(holdout_ids))} 直接返回"),
    SkillPatch("contract", "识别合同类问题", "prefix:合同"),
]

for p in patches:
    try:
        state, info = evolve_once(state, p, train, holdout, holdout_ids, budget)
        print(p.name, info)
    except (ContractError, RuntimeError) as e:
        print(p.name, "BLOCKED", e)
print("final version", state.version, "skills", list(state.skills))
print("holdout score", score(make_agent(state), holdout))

运行输出:

代码语言:bash
复制
invoice {'promoted': True, 'train': 1.0, 'holdout': 0.333..., 'spent': 0.0168}
cheat BLOCKED 技能源码不得引用 holdout 样本 id: h1
contract {'promoted': True, 'train': 1.0, 'holdout': 0.666..., 'spent': 0.0336}
final version 2 skills ['invoice', 'contract']
holdout score 0.666...

输出里能看到三件事:合法技能可以靠 holdout 上涨晋升;试图把 holdout id 写进技能的补丁被契约直接拦住;全程花费远低于 0.2 元预算。演示里的 agent 规则很简陋,但闸门逻辑和生产里要的是同一套。

六、落地时的几个经验

  • holdout 要定期换 seed,避免 Agent 间接拟合固定盲测集。
  • 契约优先拦“引用评测 id / 硬编码答案表 / 动态执行”这三类廉价捷径。
  • 成本账本按单位成功任务计,失败重试也要算进去。
  • 晋升条件写成代码不变量:holdout 不涨就不发布,不要靠人工肉眼看 train 曲线。

自进化并不神秘,难的是防止它用作弊换分数。把评测隔离、契约校验和成本封顶做成闸门之后,Agent 才能在预算内变强,而不是在评测集上背答案。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、先把训练集和评测集拆开
  • 二、技能变更必须过契约
  • 三、单位任务成本必须封顶
  • 四、进化循环:只有 holdout 上涨才晋升
  • 五、串起来跑一遍
  • 六、落地时的几个经验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档