My Blog
Agent 学习

Agent 学习第 14 周:自进化 Agent,和一次差点写进报告的错误结论

让模型写 skill 再优化它,token 降了 45%;但一次「完全符合预期」的对比结论,真相是我自己的量尺有 bug。

发布于 2026/9/11更新于 2026/9/11阅读约 27 分钟
只问这篇文章 →

第十四周讲自进化 Agent,案例是 Nous Research 的 Hermes。作业是:让大模型写一个 skill,再让模型优化它,从执行效率或 token 消耗的角度对比优化前后。

这周我做出了一组数字,它完全符合我的预期——「模型的优化把 skill 搞坏了,这是帕累托权衡」。差一点就写进报告了。

真实原因是我自己的量尺有 bug。这篇后半部分主要讲这个。


一、两层进化:个体学习 + 种群进化

这是这周的主结构:

个体学习(运行时)种群进化(GEPA 离线)
触发每 10 次工具调用后,响应交付之后异步触发手动运行或定时批处理
执行派生独立的后台回顾 AgentDSPy GEPA 遗传算法 + 帕累托排序
输出skill_manage(create/patch)验证过的更优 SKILL.md
范围单次会话即时生效跨会话系统性改良

Nudge 机制的四个设计决策

① 响应交付后触发     标志位在 final_response 返回之后才激活,不抢占任务注意力
② 后台 Agent 完全隔离 review Agent 的 nudge_interval = 0,禁止嵌套,防无限递归
③ 主动调用立刻归零   Agent 自己用了 skill_manage 就清零计数
④ Memory Nudge 同步  独立计数,与 Skill Nudge 共用同一个后台 Agent

回顾 Agent 的提示词核心就三问:

刚才是否重复了类似步骤?有没有可复用的模式?写一份 SKILL.md 会不会帮到以后的任务?


二、GEPA:把技能文本当作「基因」

这个比喻我觉得很妙:

生物概念GEPA 对应
基因SKILL.md 文本,每条技能是一个「个体」
种群多版本变体集合
突变LLM 改写指令步骤
交叉取 A 的前半 + B 的后半
自然选择帕累托排序淘汰

SKILL.md 里每一句自然语言指令 = 一个可被突变的基因片段;整个技能文件 = 一条染色体。

帕累托优化:为什么没有单一「最优」

V0 基线  72% / 320 tok / 中    被 V4 支配 → 淘汰
V1 突变  78% / 290 tok / 中    前沿
V2 突变  70% / 210 tok / 低    被 V1 支配 → 淘汰
V3 交叉  80% / 310 tok / 高    前沿
V4 精英  83% / 280 tok / 中高  前沿

前沿上的变体「各有所长」——提升准确率往往要多花 token,降低 token 又可能损失鲁棒性。GEPA 不求唯一最优解,而是保留前沿上的多个优质变体,按任务侧重灵活选用。

这一点直接决定了我这次作业怎么设计:作业要求我优化 token,那正确率就必须一起量,否则很容易把另一个维度搞坏。


三、Hermes 的其他设计

六级渐进式披露(比 Week13 的三层更细):

L1  系统提示摘要      ~50 tok/skill    名称 + 描述,自动注入
L2  skills_list()     ~100 tok/skill   名称 + 触发关键词
L3  skill_view(name)  500~2000 tok     完整 SKILL.md
L4  skill_view(file)  技能引用的附属文件
L5  斜杠命令激活
L6  Cron 自动执行

纵深防御五层,核心哲学一句话:

检测自动化,决策留给人类——危险命令是暂停等确认,而不是直接拒绝。

六大设计原则里有一条数字很惊人:

韧性优先——11,000+ 行里绝大部分是韧性工程,纯循环逻辑不到 100 行。

这句话值得记很久。ReAct 的核心算法在 PPT 上就三行,工程实现里 99% 的代码在处理它不工作的时候该怎么办。

Loop Engineering 作为延伸:范式的四层进化是 Prompt → Context → Harness → Loop。关键差异是「继续还是停」由系统自动判断,而不是靠人工逐轮提示。


四、作业设计:三个必须钉死的前提

① 优化目标要多维

老师给的 self_evolving_agent/ 优化的是准确率,作业要求我优化效率 / token——这是另一个维度。

所以我把「正确率不许降」写成优化提示词里的硬约束,跑分时两个维度一起量。

② 判分必须零 LLM 成本

老师 evaluator.py 用关键词判分,教学重点写的就是「零 LLM 成本」。这次尤其重要:

本实验比的就是 token,如果判分本身也烧 token,测出来的数就混进了裁判的开销。

我做得更彻底:老师是写死关键词,我是把标准答案算出来(词频这种任务答案唯一且可复算)。

③ 任务选型的三条硬要求

① 可执行      产物是真文件,不是一段文字 —— 才能测执行效率
② 可客观判分  正确性能用代码算出来 —— 不用 LLM 当裁判
③ 有优化空间  朴素写法有明显浪费 —— 模型才有东西可优化

选了「给一段中文文本生成 HTML 词频报告」。分词方法和排序规则必须写进任务规格——不钉死的话,模型自己发明一套切法,数字和标准答案对不上,那是「标准不一致」不是「算错了」,实验就废了。

(排序规则是第二次才想起来补的:有段语料 Top-10 的末位次数是 1,并列一堆词,不定 tie-break 的话第 10 名是谁每次都可能不同。)


五、第一步:让模型写 skill(v1)

★ 生成提示词里一个字都不能提效率和 token。

v1 要的就是未经引导、模型自然会写出来的版本。第一版就提示注意效率,等于我替模型做了优化,后面的对比是自欺欺人。

中途遇到的一个坑:推理模型把 token 全烧在思考上

deepseek-v4-flash 连试两次都是零输出:

max_tokens=4,096   → completion 4,096 tok(reasoning 4,096),content 空
max_tokens=16,384  → completion 16,384 tok(reasoning 16,384),content 空,跑了 125 秒

finish_reason=length,但 content 是空字符串——看起来像「模型不回答」,实际是还没轮到输出就被截断了。它是推理模型,reasoning_tokens 也计入 completion_tokens。

v1 长什么样

出乎意料的是:模型自己就写了脚本,没有走「在对话里数词」的朴素路线。

SKILL.md                   2,211 B
scripts/generate_report.py 5,180 B

所以 v1 不是个稻草人,它本身就是个像样的版本——这让后面的优化更有说服力。


六、第二步:让模型优化(v2)

喂给优化器的不只是 SKILL.md,还有实测数据——每次跑的轮数、完整的工具调用链、token 数。

没有调用链,模型只能凭感觉改写措辞;有了它才能看见具体浪费。

模型自己写的改动说明:

- SKILL.md 瘦身:由详细步骤+注意事项压缩为直接执行流程
- 明确"先 write_file 后 run_script",消除实测中先 run_script 导致的失败重试
- 明确不要用 read_reference 读取脚本,减少多次源码读取
- 脚本简化:移除大段 CSS/HTML 装饰和多余候选输入路径
- 统计口径与 v1 完全一致,未改变分词、过滤、排序规则
              v1        v2      变化
SKILL.md   2,211B    1,284B    -42%
脚本       5,180B    2,940B    -43%

七、结果

3 段语料 × 2 次 × 2 个版本:

版本    满分数   平均分    prompt   completion    总tok    轮数   调用   耗时s
v1      5/6     0.967    16,553     2,230     18,783    5.0   4.3   19.9
v2      6/6     1.000     8,851     1,496     10,348    4.0   3.0   13.4
                +0.033     -47%      -33%       -45%   -20%  -30%   -32%

省的不是「SKILL.md 变短了」

看调用链就清楚了:

v1(6 次里有 4 次)  load_skill → list_skill_files → read_reference → write_file → run_script
v2(6 次全部)       load_skill → write_file → run_script

v1 那个 read_reference 读的是 5,180 字节的脚本源码——模型为了搞清楚脚本怎么调用,把整个源码读进了 context。

粗算 7,702 token 的差额从哪来:

SKILL.md 正文变短    ≈ 2,000 tok
不再读 5KB 脚本源码   一次读进来 ≈1,800 tok,
                     但它在后续每一轮都要重发   ≈ 5,400 tok
                                              ─────────
                                               ≈ 7,400 tok

主要的省不是正文瘦身,是那次 read_reference 没有发生。

而它之所以值这么多,是因为 context 是按轮累计重发的——一次多读 5KB,后面每一轮都要为它付钱。

更值得注意的是方差没了

v1  轮数 4~6,调用 3~5,prompt 8,269 ~ 22,838(差 2.8 倍)
v2  轮数 4,  调用 3,  prompt 6,935 ~ 10,373(差 1.5 倍)

v2 六次跑的调用链一模一样。 v1 则要看模型这次心情好不好——同一段语料两次跑,一次 3 个调用 8,853 tok,一次 5 个调用 22,804 tok。

原因是 v2 把话说死了(「无需先查看脚本内容」)。v1 没说,模型就自己判断要不要看,判断结果不稳定。

对生产系统来说,这比省 45% token 更有价值:成本可预测。

v1 唯一那次失分,不算数

[v1] rag#1  0.80   字符× 词数√ Top 10/10

词数和 Top-10 全对、只有总字符数错。离线复现验证了原因:输入文件多了一个结尾换行。

写入原文              → 总字符数 182   总分 1.00
写入原文 + 一个换行符  → 总字符数 183   总分 0.80

所以 v2 那 +0.033 的平均分优势基本是运气,不能算优化成果。

诚实的结论是:token 和轮数的改进是实打实且稳定的(6 次调用链完全一致),正确率的差异在噪声范围内。


八、一次跑反的对比:三个 bug 让我得出了相反的结论

LLM 生成的 skill 一上来就把我 Week13 写的 harness 跑挂了,连跑三段语料全是 0 分。

修完三个 bug 之前,第一轮对比跑出来是这样的:

v2 全面崩溃:正确率 6/6 → 1/6,总 token +259%,耗时 +405%

看着就是「模型的优化把 skill 搞坏了」。 而且这个解释还很符合预期——课上讲的帕累托权衡正好能拿来解释「只优化 token 就掉了准确率」。

真实原因:

  • v1「没事」是因为它的 SKILL.md 没禁止读脚本,模型发现路径不对之后自己写探测脚本、在产物目录里把 skill 重建了一遍,硬绕过去了
  • v2「崩了」恰恰因为它的优化写了「不要读取脚本」——把这条自救路堵死了

那组数字测的根本不是「优化好不好」,是「哪一版更能容忍我的 bug」。

两层教训

① 跑分异常时先怀疑量尺。

我的第一反应是「模型优化失败了」,而且这个解释很符合课上的理论。

一个符合预期的错误结论,比一个明显离谱的结论更难被发现。

② 「能跑通一个例子」离「能跑通任意 skill」很远。

三个 bug 的共同点是:那套 harness 只在 flash-card 一个样本上验证过,而它是老师精心写的、恰好满足所有隐含假设的 skill。换一个来源的立刻全崩。

bugflash-card 为什么不触发
用 frontmatter 的 name 拼路径,沙箱按目录名解析它的 name 恰好等于目录名
裸文件名落进 skills/,而脚本 cwd 是产物目录它的写入路径都带 flash-card/ 前缀
建 ExecContext 时没传 skills_dir,用了默认值Week13 只有一个 skills 目录,默认值恰好对

(反过来说 Week13 的交付本身没问题:它的作业范围就是 flash-card,三个 bug 在那个范围内一次都不发作。)


九、模型的「优化」里有一半是在减容错

v2 把脚本从 5,180 B 砍到 2,940 B,砍掉的东西里包括:

v1: def find_input_file(report_name):        # 4 个候选路径
    if len(sys.argv) < 2:                    # 概括是可选的

v2: input_path = Path.cwd() / f"{name}.input.txt"   # 只剩 1 个
    if len(sys.argv) < 3:                    # 概括变成必填

这些不是浪费,是容错。 模型只看到「脚本源码占 token」,没看到「这些分支是在第一次调用姿势不对时兜底的」。

这次没出事,因为 v2 的 SKILL.md 同时把调用姿势写得更死了,两个改动刚好配套。但如果只砍容错、不同时把说明写清楚,就是纯粹的退化——上面那次跑反的对比里,v2 恰好因为没有容错而全军覆没,v1 靠着容错活了下来。


这周的收获

1. Agent 场景的「浪费」定义和普通程序不一样。

普通程序   优化热点循环、减少 IO 次数
Agent      优化"什么东西进了 context、进得多早、待了多久"

读一个 5KB 文件,在普通程序里是一次性开销;在 Agent 循环里,成本 = 大小 × 剩余轮数。这条重新定义了什么叫优化。

2. 方差比均值更值得优化。

v2 省了 45% token,但真正让我觉得它「更好」的是六次跑的调用链完全一致。可预测的成本,比更低但抖动的成本更有工程价值。

3. 符合预期的错误结论最危险。

这周最大的收获不是技术,是这个:我拿到一组数据,它完美印证了课上刚学的帕累托权衡理论,于是我几乎没有质疑它。理论给了我一个现成的解释,让我停止了追问。

真正救回来的动作很朴素——先怀疑量尺,再怀疑被测对象。

4. 优化器需要看见数据,不是看见代码。

喂给模型的是完整的工具调用链和逐轮 token,它才能指出「这次 read_reference 是浪费的」。只给它 SKILL.md,它只会改措辞。

下周是知识图谱与大模型。

相关文章