1. 从 Hyperagents 论文到本地可跑的最小闭环
Meta 那篇 HYPERAGENTS 论文刷屏的时候,我第一反应不是「AI 要觉醒了」,而是——这套「自己写代码、自己改自己」的机制,能不能在本地用现成工具复现一个最小版本?论文里提到的达尔文哥德尔机(DGM)在 SWE-bench 上把性能从 20.0% 拉到 50.0%,Polyglot 上从 14.2% 涨到 30.7%,核心动作其实就一句话:让智能体读自己的代码、提改进方案、写回文件、再跑一遍验证。这个循环,开发者完全可以在自己机器上搭出来。
所谓超级智能体(Hyperagents),论文的定义是「既能修改任务执行行为,也能修改生成未来改进建议的过程」,也就是元认知自我修改。落到工程上,它需要一个能稳定调用大模型的入口、一套读写本地代码的脚本、一个能判断「这轮改动到底有没有变好」的验证器。这三件事里,最容易被卡住的是模型调用——不同模型不同 Key、不同 Base URL、不同参数格式,智能体每换一个模型就要改一次配置,自我进化循环根本跑不顺。
这篇就按「本地复现」的场景来写:用 TaoToken 统一 Key 把模型调用收敛成一个入口,然后写一个能自读代码、自提改进、自回写的脚本,最后跑一轮进化前后的能力对比。适合已经会写 Python、想让自己的 Agent 真正「动起来改自己」的开发者。全程不需要 GPU 集群,一台能联网的开发机就够。
我试过把模型调用散落在各个脚本里的写法,改一次模型要翻五个文件,后来统一到一个配置层才顺过来。下面按步骤走。
2. TaoToken 统一 Key 前置:把模型入口收敛成一个 Base URL
在写自我进化脚本之前,先把「模型怎么调」这件事固定下来。TaoToken 的作用是提供一个统一的 API 入口,你用同一个 Key、同一个 Base URL,就能切换不同模型,不用为每个模型单独维护一套鉴权和地址。对自我进化循环来说这点很关键——智能体在迭代过程中可能会换模型来提改进方案,如果每次换模型都要改代码里的 endpoint,循环就断了。
先拿 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台里创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 列表在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。创建完先复制存好,后面脚本里要用。
这里有个容易踩的坑:很多人拿到 Key 之后直接写死在脚本里,结果一提交就泄露。正确做法是放到环境变量或者本地.env文件,并且把.env加进.gitignore。我一般用环境变量,跨脚本复用最省事。
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"注意 Base URL 是https://taotoken.net/api,不带任何查询参数。有些教程会让你在 Base URL 后面拼/v1,那是另一套兼容格式的写法,用 TaoToken 的时候按官方给的地址来,别自己加后缀,否则容易出现 404 或者路径重复。
模型 ID 这块,TaoToken 支持多种主流模型,你在调用时通过model字段指定。建议先想清楚自我进化循环里要用哪几个模型:一个负责「提改进方案」(推理强一点的),一个负责「执行具体改动」(速度快一点的)。两个角色可以用同一个模型,也可以分开,统一 Key 的好处就是切换只改一个字符串。
验证 Key 是否可用,最直接的方式是发一个最小请求。用 curl 试一下:
curl https://taotoken.net/api/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-5-sonnet", "messages": [{"role": "user", "content": "回复 ok 两个字母"}], "max_tokens": 16 }'如果返回里能看到choices字段和内容,说明 Key 和地址都通了。如果返回 401,先检查 Key 有没有复制完整、有没有多余空格;如果返回 404,检查 Base URL 是不是写成了带/v1的形式。这一步通了,再往下写脚本。
如果你更习惯在对话界面里先试模型效果,可以打开模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,选一个模型发几句话,确认账号状态正常。这一步不是必须,但对第一次接入的人来说能快速排除「Key 本身有问题」这种情况。
3. 可复制配置:settings.json 与智能体回写脚本
配置层我建议单独放一个文件,不要混在业务脚本里。下面这套结构在本地复现时最省心:一个settings.json管模型入口,一个evolve_agent.py管自我进化循环。
先写settings.json,路径放在项目根目录:
{ "provider": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "timeout": 120 }, "models": { "planner": "claude-3-5-sonnet", "executor": "claude-3-5-sonnet" }, "evolution": { "target_file": "./agent_core.py", "max_rounds": 3, "backup_dir": "./.evolve_backup", "score_threshold": 0.05 } }这里planner负责读代码、提改进方案,executor负责把方案落成具体代码改动。target_file是智能体要改的那个文件——也就是它自己的核心逻辑。max_rounds控制最多迭代几轮,本地跑建议先设 3,别一上来就放开。score_threshold是「这轮改动至少要比上轮好多少才算数」,防止无意义的抖动被当成进化。
然后是主脚本。核心逻辑分四步:读当前代码 → 让模型提改进 → 写回文件 → 跑验证打分。下面是一个可运行的最小版本:
import json import os import shutil import subprocess from pathlib import Path from openai import OpenAI CFG = json.loads(Path("settings.json").read_text()) client = OpenAI( base_url=CFG["provider"]["base_url"], api_key=os.environ[CFG["provider"]["api_key_env"]], ) def read_target(): return Path(CFG["evolution"]["target_file"]).read_text() def backup(round_idx): src = Path(CFG["evolution"]["target_file"]) dst_dir = Path(CFG["evolution"]["backup_dir"]) dst_dir.mkdir(exist_ok=True) shutil.copy(src, dst_dir / f"round_{round_idx}_{src.name}") def propose_improvement(code, history): prompt = f"""你是一个自我改进智能体。下面是当前代码: {code} 历史尝试记录: {history} 请提出一处能提升该代码任务成功率的改进,直接输出完整的修改后代码, 不要解释,不要用 markdown 代码块包裹。""" resp = client.chat.completions.create( model=CFG["models"]["planner"], messages=[{"role": "user", "content": prompt}], max_tokens=4096, ) return resp.choices[0].message.content.strip() def write_back(new_code): Path(CFG["evolution"]["target_file"]).write_text(new_code) def run_validation(): result = subprocess.run( ["python", "-m", "pytest", "tests/", "-q"], capture_output=True, text=True, ) passed = result.stdout.count(" passed") return passed, result.stdout def evolve(): history = [] best_score = 0 for i in range(CFG["evolution"]["max_rounds"]): backup(i) code = read_target() new_code = propose_improvement(code, "\n".join(history)) write_back(new_code) score, log = run_validation() delta = score - best_score history.append(f"round {i}: score={score}, delta={delta}") if delta < CFG["evolution"]["score_threshold"]: print(f"round {i} 提升不足,回滚") shutil.copy( Path(CFG["evolution"]["backup_dir"]) / f"round_{i}_agent_core.py", CFG["evolution"]["target_file"], ) else: best_score = score print(f"round {i} 进化成功,score={score}") if __name__ == "__main__": evolve()这段脚本里,propose_improvement是核心——它把当前代码和历史尝试一起喂给模型,让模型输出完整的新代码。注意 prompt 里明确要求「直接输出完整代码,不要 markdown 包裹」,否则模型很容易给你一段带 ```python 的文本,写回文件后直接语法错误。这个坑我在第一次跑的时候踩过,回写后 pytest 报SyntaxError,排查了半天才发现是代码块标记混进去了。
run_validation用 pytest 的通过数当分数,这是最朴素的验证器。真实场景里你可以换成自己的评测集,比如一组固定任务的成功率。关键是这个分数要能自动算出来,不能靠人眼看。
如果你打算把这个循环跑成长期任务,比如让它每天自己迭代几轮,那用 Coding Plan 会更合适,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它面向的就是这种持续编码、Agent 长跑的用量场景,比按次调用更划算。
4. 验证请求与成功结果:跑一轮进化前后对比
配置和脚本都就位后,先别急着放开迭代,做一次「单轮进化」的对照实验,确认整条链路是通的。
准备一个待改进的agent_core.py,故意留一个可优化的点。比如下面这个版本,它做字符串处理时没有处理空输入:
def normalize(text): return text.strip().lower().replace(" ", "_")再写一个测试文件tests/test_core.py:
from agent_core import normalize def test_basic(): assert normalize("Hello World") == "hello_world" def test_empty(): assert normalize("") == "" def test_none(): assert normalize(None) == ""跑一次pytest tests/ -q,你会看到test_none失败,因为None.strip()会抛AttributeError。当前通过数是 2。
现在跑python evolve_agent.py。脚本会读agent_core.py,把代码和空历史发给模型,模型大概率会提出加一个空值判断,比如:
def normalize(text): if text is None: return "" return text.strip().lower().replace(" ", "_")写回后再跑 pytest,通过数变成 3。脚本输出round 0 进化成功,score=3。这就是一轮完整的「自读代码 → 自提改进 → 自回写 → 自验证」闭环。
如果你想更直观地看进化前后的差异,可以在跑之前先手动记录一次分数,跑完再记录一次,用表格对照:
| 轮次 | 通过用例数 | 改动内容 | 是否保留 |
|---|---|---|---|
| 初始 | 2 | 无 | - |
| 第 0 轮 | 3 | 增加 None 判断 | 是 |
| 第 1 轮 | 3 | 尝试重构但无提升 | 回滚 |
第二轮如果模型提的改动没有让通过数继续涨,脚本会按score_threshold回滚,保证agent_core.py始终停在当前最优版本。这个「只保留有提升的改动」的机制,就是论文里说的「从经验上提升性能的方案」在工程上的简化版。
验证模型本身是否正常工作,也可以单独发一个请求确认返回结构:
resp = client.chat.completions.create( model="claude-3-5-sonnet", messages=[{"role": "user", "content": "输出一行 python 代码:打印 hello"}], ) print(resp.choices[0].message.content)能正常打印出代码,说明模型调用没问题。如果这里就报错,先回到第 2 步检查 Key 和 Base URL,别往下走。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
本地复现这类自我进化循环,报错基本集中在模型调用和文件读写两块。下面按真实遇到的报错逐个说。
401 Unauthorized。最常见的原因是 Key 没读到。脚本里用的是os.environ["TAOTOKEN_API_KEY"],如果你在另一个终端窗口跑的脚本,而环境变量只在当前窗口 export 过,就会读不到。解决办法是把 export 写进~/.bashrc或~/.zshrc,或者用python-dotenv从.env读。还有一种情况是 Key 复制时带了首尾空格,Bearer后面多一个空格也会 401,用echo $TAOTOKEN_API_KEY | wc -c看一下长度对不对。
local proxy failed。这个报错通常出现在你本机设置了系统级网络配置、但脚本运行环境没继承到的时候。检查方式是看你的 shell 里有没有HTTP_PROXY/HTTPS_PROXY这类变量,如果有,确认它们指向的地址是通的。另一种可能是 Base URL 写错了,比如写成了https://taotoken.net/api/v1,多出来的路径导致请求打到了不存在的端点。按第 2 步给的地址https://taotoken.net/api来,不要自己拼后缀。
reading 'choices'。典型报错是TypeError: Cannot read properties of undefined (reading 'choices'),或者 Python 里resp.choices报AttributeError。这说明返回体里根本没有choices字段,通常是请求本身失败了但你没检查状态码。在脚本里加一层判断:
resp = client.chat.completions.create(...) if not resp.choices: raise RuntimeError(f"空返回: {resp}")更常见的原因是模型 ID 写错了。比如你写了一个 TaoToken 不支持的模型名,接口可能返回一个错误结构而不是标准 completion。解决办法是先用第 2 步的 curl 命令确认模型 ID 可用,再写进settings.json。
OAuth 相关报错。如果你用的是某些需要 OAuth 授权的客户端(比如 Claude Code 这类工具),报错里可能出现OAuth token expired或invalid_grant。这类工具接入 TaoToken 时,需要在配置里把 Base URL 指向https://taotoken.net/api,Key 用 API Key 而不是 OAuth token。以 Claude Code 为例,它的配置文件里要同时写全三件套:Base URL、API Key、Model ID。缺任何一个都会导致鉴权失败。具体接入方式可以看文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各客户端的配置示例。
还有一个不报错但很坑的情况:模型返回的代码带了 markdown 代码块标记,写回文件后 pytest 报SyntaxError。解决办法是在 prompt 里明确要求「不要用 markdown 包裹」,或者在写回前做一次清洗:
def strip_fence(text): if text.startswith("```"): lines = text.splitlines() return "\n".join(lines[1:-1]) return text这个清洗函数建议直接加进write_back之前,能省掉很多莫名其妙的语法错误。
6. 把循环跑长:从单轮进化到持续迭代
单轮跑通之后,你可以把max_rounds调大,让它连续迭代。但连续迭代会暴露一个新问题:模型容易陷入局部最优,反复提类似的改动。论文里 DGM 用「开放式进化搜索」解决这个问题——从现有智能体库里采样,保留表现稍差但方向不同的「祖先」,避免早熟收敛。本地复现时可以用一个简化策略:每轮把历史尝试记录完整传给模型,并且在 prompt 里加一句「不要重复历史中已经尝试过的改动方向」。
历史记录我建议存成 JSON,而不是拼成字符串。这样你可以记录每轮的分数、改动摘要、是否保留,下一轮提改进时把最近几轮的历史一起喂进去:
history.append({ "round": i, "score": score, "delta": delta, "kept": delta >= CFG["evolution"]["score_threshold"], "summary": new_code[:200], }) Path("evolution_history.json").write_text(json.dumps(history, ensure_ascii=False, indent=2))这样跑十几轮之后,你能清楚看到智能体在哪些方向上试过、哪些有效、哪些被回滚。这个记录本身就是「元学习」的雏形——它不只是在改代码,还在积累「怎么改才有效」的经验。
如果你想让这个循环长期跑下去,比如挂在一个定时任务里每天迭代,那模型调用的稳定性和成本就变成主要矛盾。Coding Plan 面向的就是这种长期编码场景,地址 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,适合把自我进化循环当成一个持续运行的 Agent 来用。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,API Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。
最后说一个实际跑下来的经验:验证器的质量决定了进化的方向。如果你的 pytest 只覆盖了三个用例,智能体很快就会把这三个用例全过掉,然后开始提一些「为了过测试而改」的无效改动。真正让自我进化有意义的前提,是验证器能代表你真实想要的能力。所以与其急着把max_rounds调到 100,不如先把测试集写扎实。循环本身不难搭,难的是让每一轮迭代都朝着你真正想要的方向走。