MIT与Sakana AI的SIFT:低成本自我改进编码智能体实战指南
1. 从“模型越大越强”到“小步快跑自我进化”SIFT 到底在解决什么问题第一次看到“MIT 与 Sakana AI 提出 SIFT低成本实现自我改进的编码智能体”这个标题时我脑子里冒出来的第一个念头是终于有人把“自我改进”这件事从论文里的漂亮曲线拉回到工程可落地的成本账上了。过去两年编码智能体Coding Agent的叙事基本被两条路线主导——一条是堆参数、堆上下文、堆推理算力另一条是堆人工反馈、堆标注数据、堆评测集。两条路都有效但都贵得离谱。SIFT 的出现本质上是想回答一个非常朴素的问题如果我不重新训练一个大模型也不请一堆标注员天天打分能不能让一个编码智能体在真实任务里自己变强SIFT 的全称在不同资料里表述略有差异但核心思想是一致的Self-Improvement with Fine-grained Feedback and Trajectory即利用智能体自身执行任务时产生的轨迹trajectory和细粒度反馈fine-grained feedback构建一个低成本、可循环的自我改进闭环。它不是一个新模型而是一套方法论加工程框架。MIT 那边贡献的是对“自我改进”理论边界的拆解Sakana AI 那边贡献的是把进化算法和编码任务结合起来的工程直觉。两边凑在一起才有了 SIFT 这个听起来像图像处理、实际上是智能体训练范式的名字。这篇文章适合谁看如果你正在做编码助手、自动化测试生成、代码审查机器人或者你只是单纯好奇“不烧大钱能不能让 Agent 自己进步”那 SIFT 的思路值得你花时间拆一遍。我会从设计思路、核心细节、实操流程、踩坑排查四个层面把 SIFT 拆到你能直接抄作业的程度。需要提前说明的是SIFT 目前公开的完整实现细节有限下面涉及具体参数和步骤的部分我会基于编码智能体领域的常见实践做合理补全并明确标注哪些是推断、哪些是公开信息。2. SIFT 的整体设计思路为什么是“轨迹细粒度反馈”而不是“重新训练”2.1 编码智能体的传统改进路径及其成本瓶颈要理解 SIFT 为什么这么设计得先看清楚传统路径的钱花在哪了。一个编码智能体要变强通常有三条路第一条是继续预训练或指令微调。你需要收集大量高质量代码问答对清洗、去重、格式化然后跑分布式训练。这条路的效果上限最高但成本也最高。一次像样的微调光是数据标注和算力开销就不是小团队能承受的。而且编码任务有个特点版本迭代快、API 变化频繁你今天微调进去的知识三个月后可能就过时了。第二条是上下文学习加提示工程。你把任务描述、代码库结构、历史对话一股脑塞进上下文窗口靠模型自己的推理能力解决问题。这条路成本低、迭代快但天花板也低。上下文窗口再大也装不下整个代码库提示词再精巧也架不住模型在复杂多步任务里“走着走着就忘了目标”。第三条是基于人类反馈的强化学习。你让模型生成多个候选答案人类标注员排序训练一个奖励模型再用强化学习优化策略。这条路在对话任务上很成功但在编码任务上有个致命问题代码的正确性判断成本极高。一段代码能不能跑通、有没有边界 bug、性能是否达标不是标注员看一眼就能打分的。你需要实际执行、需要测试用例、需要静态分析这些都比“哪个回答更礼貌”贵得多。SIFT 的切入点就在这里既然人类反馈贵那能不能用智能体自己执行任务时产生的信号来替代代码任务有一个天然优势——它是可验证的。你写一段代码跑一下测试就知道对不对你改一个 bug回归测试通过就是通过不通过就是不通过。这种“执行结果即反馈”的特性让编码智能体比对话智能体更容易实现低成本的自我改进。2.2 SIFT 的核心循环生成、执行、筛选、进化SIFT 的整体流程可以概括为一个四步循环我把它叫做“生成-执行-筛选-进化”生成阶段智能体针对当前任务生成多个候选解决方案。这里的关键是多样性。如果所有候选都差不多筛选就没有意义。SIFT 通常会通过调整温度参数、改变提示模板、引入不同的推理路径来保证候选的多样性。执行阶段每个候选方案被实际运行。对于编码任务这意味着编译、执行测试用例、收集运行时信息。这一步产生的不是简单的“对/错”标签而是细粒度反馈哪几个测试通过了、哪几个失败了、失败的原因是什么、执行时间是多少、有没有抛出异常。这些信息比一个二元标签丰富得多也便宜得多——因为它们是机器自动产生的。筛选阶段根据执行结果对候选进行排序和过滤。SIFT 在这里的巧妙之处在于它不要求有一个完美的奖励模型。它可以用简单的规则通过测试多的排前面执行时间短的排前面代码改动小的排前面。当然也可以训练一个轻量级的排序模型但这不是必须的。进化阶段把筛选出来的优质候选作为“种子”进行变异和重组生成下一轮候选。这里的“进化”借用了遗传算法的思想好的代码片段被保留和组合坏的被淘汰。Sakana AI 在进化计算方面有深厚积累这部分应该是他们的主要贡献。这个循环跑起来之后智能体不需要外部标注也不需要重新训练大模型就能在特定任务分布上逐步提升。成本主要花在推理算力和执行环境上这两样都比人类标注便宜得多。2.3 为什么“低成本”是可行的算一笔粗账我们来粗略算一笔账看看 SIFT 到底省在哪。假设你有一个编码任务需要智能体生成一个函数并通过 20 个测试用例。传统 RLHF 路径生成 10 个候选请 3 个标注员对每个候选打分排序每人每个候选花 2 分钟。总人工时间 10 × 3 × 2 60 分钟。按每小时 30 元算单任务人工成本 30 元。这还不算标注员培训、质量控制、争议仲裁的成本。SIFT 路径生成 10 个候选自动执行测试收集通过率和执行时间。总机器时间 10 次编译执行假设每次 5 秒总共 50 秒。算力成本几乎可以忽略。筛选和进化阶段可以用规则完成也可以用一个小模型但不需要人类介入。当然SIFT 不是万能的。它的效果依赖于任务本身的可验证性。如果任务没有明确的测试用例或者正确性判断需要人类主观判断SIFT 的优势就不明显了。但在编码领域大量任务是有测试、有类型检查、有静态分析工具支持的这就给了 SIFT 很大的发挥空间。注意SIFT 的“低成本”是相对于“重新训练大模型”和“大规模人类标注”而言的。它仍然需要推理算力和执行环境只是把成本从人力和训练转移到了自动化和工程优化上。3. 核心细节拆解SIFT 的四个关键组件怎么实现3.1 轨迹采集不只是记录而是结构化提取SIFT 里的“轨迹”不是简单的日志堆砌。一个编码智能体在执行任务时会产生大量信息它读了哪些文件、调用了哪些工具、生成了哪些中间代码、遇到了什么错误、如何修正。这些信息如果原封不动地存下来就是一堆噪音。SIFT 的关键在于结构化提取。具体来说轨迹至少需要包含以下几个维度任务上下文任务描述、输入输出示例、相关代码库片段。动作序列智能体每一步做了什么是读文件、写代码、运行测试还是搜索文档。中间产物生成的代码片段、修改的 diff、执行的命令。执行结果测试通过情况、错误信息、性能指标。修正历史如果智能体进行了多轮尝试每一轮的变化是什么。为什么要这么细因为细粒度反馈是 SIFT 自我改进的燃料。如果只记录“最终通过了”或“最终失败了”那和二元标签没区别。只有知道“它在第三步改错了变量名导致两个测试失败第四步修正后通过”才能从中提取出可复用的改进信号。在实际工程中我建议用 JSON Lines 格式存储轨迹每条记录是一个完整的任务执行过程。字段设计可以参考下面这个结构{ task_id: fix-bug-001, task_description: 修复 calculate_total 函数在空列表输入时抛出异常的问题, trajectory: [ {step: 1, action: read_file, target: utils.py, result: success}, {step: 2, action: generate_patch, content: ..., result: generated}, {step: 3, action: run_tests, passed: 18, failed: 2, errors: [test_empty_list]}, {step: 4, action: generate_patch, content: ..., result: generated}, {step: 5, action: run_tests, passed: 20, failed: 0, errors: []} ], final_status: success, total_steps: 5 }这种结构化轨迹的好处是后续无论是做统计分析、训练排序模型还是做进化变异都有明确的数据接口。3.2 细粒度反馈的提取从测试结果到改进信号细粒度反馈是 SIFT 区别于普通“试错”方法的核心。普通试错只知道“这个候选不行”SIFT 要知道“为什么不行、哪里不行、怎么改可能行”。以测试失败为例反馈可以分层提取第一层通过率。20 个测试过了 18 个通过率 90%。这是最粗的粒度。第二层失败测试的分类。失败的 2 个测试是关于空列表输入的说明候选方案没有处理边界情况。第三层错误类型。是抛出了异常、返回了错误值还是超时了异常类型是什么第四层代码差异。失败候选和通过候选之间的 diff 是什么哪些改动导致了失败有了这四层信息智能体在下一轮生成时就可以有针对性地调整。比如它可以在提示里加入“注意处理空列表输入”或者在进化阶段把通过候选里处理边界的代码片段提取出来拼接到失败候选上。这里有个实操心得不要试图一次性提取所有反馈。反馈太细会导致噪音过大反而干扰筛选。我的经验是先抓通过率和失败测试名这两个信号最稳定、最便宜。等系统跑顺了再逐步加入错误类型和代码差异分析。3.3 筛选策略规则、模型还是混合筛选阶段决定了哪些候选能进入下一轮进化。SIFT 论文里提到的方法比较灵活但核心原则是筛选标准要可计算、可复现、低成本。我实际用过的筛选策略有三种各有适用场景纯规则筛选按通过测试数降序通过数相同按执行时间升序再相同按代码改动行数升序。这种策略零成本、完全可复现适合任务定义清晰、测试用例完备的场景。缺点是它无法区分“通过测试但代码质量差”的候选。轻量模型筛选训练一个小的排序模型输入是候选的轨迹特征通过率、错误类型、代码复杂度、改动大小等输出是排序分数。这个模型可以用历史数据训练也可以用启发式规则生成弱标签。成本比纯规则高但能捕捉更复杂的偏好。混合筛选先用规则过滤掉明显不合格的候选比如通过率低于 50% 的再用模型对剩余候选排序。这是我在生产环境里最常用的策略兼顾了效率和效果。下面这张表对比了三种策略的优缺点筛选策略成本可复现性效果上限适用场景纯规则极低完全可复现中等任务简单、测试完备轻量模型中等依赖模型版本较高任务复杂、需要质量排序混合中低规则部分可复现高生产环境通用提示筛选策略的选择不要一步到位。先用纯规则跑通闭环再根据实际效果决定是否引入模型。很多团队一上来就搞复杂排序模型结果发现规则筛选已经够用了。3.4 进化机制变异、重组与选择压力进化阶段是 SIFT 里最有“生命力”的部分。它的基本操作包括变异对候选代码进行随机修改。修改可以是改变量名、调整条件判断、增加边界处理、替换算法实现。变异的粒度要控制好——太大容易破坏已有正确逻辑太小则探索效率低。重组把两个或多个优质候选的代码片段拼接在一起。比如候选 A 通过了所有功能测试但性能差候选 B 性能好但有两个测试失败重组可以把 A 的功能逻辑和 B 的性能优化点结合起来。选择压力决定哪些候选能存活到下一轮。选择压力太大种群多样性会迅速丧失陷入局部最优选择压力太小进化速度会很慢。SIFT 里通常用精英保留策略每轮保留 top-k 个候选直接进入下一轮其余名额通过变异和重组产生。这里有个容易踩的坑变异操作必须保证代码语法正确。如果变异后的代码连编译都过不了那这一轮就白跑了。我的做法是在变异后加一个快速的语法检查不通过的直接丢弃不进入执行阶段。这个检查可以用语言自带的解析器成本极低。4. 实操流程从零搭建一个 SIFT 风格的编码智能体4.1 环境准备与工具选型要复现 SIFT 的思路你不需要 MIT 或 Sakana AI 的内部代码。核心组件都是公开可用的。下面是我推荐的一套工具栈按优先级排列基础模型任何有代码生成能力的模型都可以。开源的有 CodeLlama、DeepSeek-Coder、Qwen-Coder 等闭源的有各类代码助手 API。选择标准是生成质量够用、推理成本可接受、支持批量调用。如果你要做进化实验建议用开源模型本地部署因为需要大量生成候选API 成本会很快失控。执行环境Docker 是必须的。每个候选代码都要在隔离环境里执行防止恶意代码或意外错误影响宿主系统。Docker 镜像里预装好语言运行时、测试框架和依赖库。对于 Python 任务一个包含 pytest 的镜像就够了对于多语言任务可以用多阶段构建。轨迹存储SQLite 或 JSON Lines 文件都可以。数据量小的时候 JSON Lines 更灵活数据量大了再迁移到数据库。关键是要有统一的 schema方便后续分析。筛选与进化这部分可以自己写 Python 脚本。核心逻辑不复杂读轨迹、算分数、排序、变异、重组。如果要用模型筛选可以接一个轻量级的分类器或排序器。编排框架LangChain、AutoGen、CrewAI 这些都可以用但我的建议是不要过度依赖框架。SIFT 的核心循环很简单用原生 Python 加几个函数就能实现。框架带来的抽象层反而会增加调试难度。等你把核心循环跑通了再考虑用框架做工程化封装。4.2 任务定义与测试用例准备SIFT 的效果高度依赖任务和测试的质量。任务定义要清晰测试用例要覆盖主要边界情况。我通常按以下步骤准备第一步收集任务。可以从开源项目的 issue 里找 bug 修复任务从代码库的 commit 历史里找重构任务或者自己构造算法实现任务。每个任务要有明确的输入输出描述。第二步编写测试。每个任务至少配 5 到 20 个测试用例覆盖正常路径、边界条件、异常输入。测试要能自动运行、自动判断通过与否。如果任务本身没有现成测试可以用模型辅助生成但必须人工审核一遍确保测试本身是正确的。第三步划分数据集。把任务分成训练集和测试集。训练集用于跑 SIFT 循环、收集轨迹、优化筛选策略测试集用于评估最终效果。划分比例可以是 8:2 或 7:3取决于任务总量。第四步建立基线。在跑 SIFT 之前先测一下基础模型在测试集上的零样本表现。这个基线是你判断 SIFT 是否有效的参照物。如果 SIFT 跑完还不如零样本那说明流程有问题。4.3 核心循环的代码实现下面是一个简化版的 SIFT 核心循环实现用 Python 写成。这段代码不是生产级的但足以让你理解整个流程并跑通一个小规模实验。import json import subprocess import random from pathlib import Path class SIFTCodingAgent: def __init__(self, model_client, task, test_command, population_size10, generations5): self.model model_client self.task task self.test_command test_command self.population_size population_size self.generations generations self.trajectory_log [] def generate_candidates(self, prompt, n): candidates [] for i in range(n): response self.model.generate(prompt, temperature0.7 i * 0.05) candidates.append(response) return candidates def execute_candidate(self, code): # 将代码写入临时文件在 Docker 中执行测试 with open(candidate.py, w) as f: f.write(code) result subprocess.run( self.test_command, shellTrue, capture_outputTrue, textTrue, timeout30 ) passed result.stdout.count(PASSED) failed result.stdout.count(FAILED) return { passed: passed, failed: failed, stdout: result.stdout[-2000:], stderr: result.stderr[-2000:], returncode: result.returncode } def score_candidate(self, exec_result): total exec_result[passed] exec_result[failed] if total 0: return 0.0 return exec_result[passed] / total def select_survivors(self, candidates_with_scores, top_k): sorted_candidates sorted( candidates_with_scores, keylambda x: (-x[score], x[exec_result][returncode]) ) return sorted_candidates[:top_k] def mutate(self, code, mutation_rate0.1): lines code.split(\n) mutated [] for line in lines: if random.random() mutation_rate and line.strip(): # 简单变异注释掉一行或修改变量名 if random.random() 0.5: mutated.append(# line) else: mutated.append(line.replace(, )) else: mutated.append(line) return \n.join(mutated) def run(self): prompt f任务{self.task}\n请生成完整的 Python 代码解决方案。 population self.generate_candidates(prompt, self.population_size) for gen in range(self.generations): scored [] for code in population: exec_result self.execute_candidate(code) score self.score_candidate(exec_result) scored.append({code: code, score: score, exec_result: exec_result}) self.trajectory_log.append({ generation: gen, score: score, passed: exec_result[passed], failed: exec_result[failed] }) survivors self.select_survivors(scored, top_kself.population_size // 2) print(f第 {gen} 代最佳分数 {survivors[0][score]:.2f}) # 生成下一代 next_population [s[code] for s in survivors] while len(next_population) self.population_size: parent random.choice(survivors)[code] child self.mutate(parent) next_population.append(child) population next_population # 返回最终最佳候选 final_scored [] for code in population: exec_result self.execute_candidate(code) final_scored.append({code: code, score: self.score_candidate(exec_result)}) best max(final_scored, keylambda x: x[score]) return best这段代码的核心逻辑是生成候选、执行测试、按通过率排序、保留 top-k、对幸存者做变异生成下一代。你可以直接把它跑起来只需要替换model_client为你的模型调用接口设置好test_command为你的测试命令。4.4 参数选择与调优经验SIFT 循环里有几个关键参数选不好会导致效果差或成本高。下面是我踩过坑之后总结的经验值种群大小population_size太小小于 5多样性不足太大大于 50执行成本高。我的经验值是 10 到 20。如果任务简单10 个候选足够如果任务复杂、搜索空间大可以加到 20。进化代数generations3 到 5 代通常能看到明显提升。超过 5 代后收益递减而且容易过拟合到训练任务的测试用例上。我一般设 5 代然后看第 3 代到第 5 代的提升幅度如果小于 2%就提前停止。变异率mutation_rate0.05 到 0.15 之间比较合适。太低探索不足太高破坏性太强。对于代码任务我倾向于用较低的变异率0.05 到 0.1因为代码的语法结构比较脆弱大改容易改坏。温度参数temperature生成候选时用 0.7 到 1.0 之间的温度保证多样性。筛选后的精修阶段可以降到 0.3 到 0.5提高生成质量。精英保留比例保留 top 30% 到 50% 作为下一代的基础。保留太少好的基因容易丢失保留太多进化压力不足。注意这些参数不是绝对的。不同模型、不同任务分布下最优参数会变化。建议先用小规模实验比如 20 个任务快速扫一遍参数找到大致范围后再放大。5. 常见问题与排查技巧实录5.1 候选多样性不足怎么办这是 SIFT 跑起来之后最常见的问题。表现是所有候选的通过率都差不多筛选出来的幸存者代码几乎一样进化几代后没有提升。原因通常有三个一是温度参数太低模型生成时过于保守二是提示模板太单一所有候选都在同一个思路下生成三是任务本身太简单搜索空间小。解决办法第一提高温度到 0.9 甚至 1.0并在提示里明确要求“用不同的方法解决这个问题”。第二设计多个提示模板比如一个要求“用递归实现”一个要求“用迭代实现”一个要求“用内置函数实现”然后从不同模板各生成一部分候选。第三如果任务确实简单那就减少种群大小和代数把精力放在更难的任务上。我实测下来多提示模板是最有效的多样性提升手段。同一个任务用三种不同风格的提示各生成 5 个候选比用同一种提示生成 15 个候选的效果好得多。5.2 测试通过但代码质量差怎么处理SIFT 的筛选主要依赖测试通过率这会导致一个问题有些候选通过了所有测试但代码写得极其丑陋——变量名是 a、b、c函数没有注释逻辑嵌套十层。如果直接把这些候选作为下一代的种子进化出来的代码会越来越难维护。解决办法是在筛选分数里加入代码质量因子。我通常用三个指标代码行数越短越好但不能太短、圈复杂度越低越好、命名规范用简单的正则检查变量名长度和可读性。把这三个指标归一化后和通过率加权求和。权重可以设成通过率 0.7、代码质量 0.3。如果你不想自己实现代码质量评估可以用现成的静态分析工具比如 Python 的radon算圈复杂度pylint算代码规范分。这些工具的输出可以直接作为筛选特征。5.3 执行环境不稳定导致误判编码智能体的执行环境如果没配好会出现各种诡异问题测试偶尔超时、依赖库版本冲突、文件权限错误。这些都会导致候选被误判为失败影响筛选准确性。我的排查清单是这样的问题现象可能原因排查方法解决方案测试随机失败超时设置太短记录每次执行耗时超时时间设为平均耗时的 3 倍依赖导入错误镜像缺少库在容器内手动跑一次完善 Dockerfile固定依赖版本文件写入失败权限或路径问题检查工作目录权限统一用绝对路径容器内用非 root 用户结果不一致随机种子未固定对比多次执行输出在测试命令里固定随机种子提示执行环境的问题往往在实验初期被忽略等到筛选结果异常时才回头排查浪费大量时间。建议在跑正式实验前先用 5 个已知正确的候选和 5 个已知错误的候选做一次“环境校准”确认执行结果和预期一致。5.4 过拟合到测试用例的识别与缓解SIFT 的进化压力来自测试通过率这天然会导致候选过拟合到测试用例上。表现是训练集上的通过率很高但换一组新测试就崩了。识别方法很简单留出一部分测试用例不参与筛选只在最终评估时使用。如果训练测试通过率和留出测试通过率差距超过 20%就说明过拟合了。缓解手段有三个第一增加测试用例的多样性覆盖更多边界情况第二在筛选时加入正则化项惩罚代码改动过大的候选第三用交叉验证的方式每轮进化随机抽取一部分测试用例参与筛选而不是用全部测试。我个人的经验是留出测试集是必须的。没有留出测试集你根本不知道 SIFT 是真的让智能体变强了还是只是让它更会“背答案”。5.5 成本失控的预警信号虽然 SIFT 号称低成本但如果不加控制成本也会悄悄涨上去。以下信号出现时说明你需要检查成本了单个任务的候选生成次数超过 50 次执行阶段的总耗时超过生成阶段的 10 倍进化 5 代后通过率提升不到 5%轨迹日志文件大小每天增长超过 1GB控制成本的手段包括设置最大生成次数上限、对执行阶段做缓存相同代码不重复执行、提前停止条件连续两代无提升就停、定期清理无用轨迹。6. 影响范围与延展思考SIFT 思路还能用在哪SIFT 虽然是为编码智能体设计的但它的核心思想——用自动可验证的细粒度反馈驱动自我改进——可以迁移到很多其他领域。只要一个任务满足“执行结果可自动判断”和“候选可以批量生成”这两个条件SIFT 的循环就能套用。比如自动化测试生成智能体生成测试用例执行后看能否发现预设的 bug用 bug 发现率作为筛选信号。比如数据清洗脚本生成智能体生成清洗规则执行后看数据质量指标用指标提升作为筛选信号。再比如配置调优智能体生成配置参数执行后看性能指标用性能提升作为筛选信号。甚至在非代码领域只要你能构建一个自动评估环境SIFT 的思路也能用。比如数学题求解生成多个解题步骤用答案是否正确作为反馈。比如文案生成生成多个版本用点击率或转化率作为反馈虽然这个反馈有延迟但可以近似。MIT 和 Sakana AI 这次的工作最大的价值不在于提出了一个多复杂的算法而在于它把“自我改进”这件事从“需要大量人类反馈”的假设里解放出来证明了在特定条件下机器可以用自己产生的信号让自己变强。这个思路的延展空间比 SIFT 本身大得多。我在实际搭建类似系统时最大的体会是不要追求一步到位。先用最简单的规则筛选跑通闭环哪怕效果一般然后逐步加入细粒度反馈、代码质量因子、留出测试集最后再考虑用模型替代规则。很多团队卡在第一步就是因为一上来就想搞一个完美的奖励模型结果连最基本的循环都没跑起来。SIFT 给我们的启示与其说是技术上的不如说是工程上的先让循环转起来再让它转得好。