最近在尝试让一个 AI 代理(Agent)去修改它自己写的 Python 代码,并且让它循环跑了 144 轮。这个想法听起来很酷,对吧?让 AI 自己迭代优化自己,理论上应该能越跑越好,甚至能发现一些人类程序员都想不到的优化点。但实际情况是,在跑了若干轮之后,整个系统以一种非常“有趣”的方式崩溃了。这并非简单的语法错误或运行时异常,而是一种更深层次的、关于“控制”和“意图”的失效。
这个实验的核心,其实触及了当前 AI 编程助手应用中的一个关键问题:我们究竟在多大程度上可以信任 AI 对代码的“理解”和“修改”?当我们将修改权完全下放,让 AI 在一个循环中不断自我迭代时,代码的“熵”是如何增加的?最终,是什么“断裂”了?
这不仅仅是关于一个 AI 代理的技术实验,更是关于人机协作边界的一次压力测试。我们习惯了让 AI 补全单行代码、解释函数功能,甚至生成一个小模块。但当 AI 开始拥有对一段代码的“长期”修改权时,事情就变得复杂起来。代码的生命周期、逻辑的一致性、甚至代码风格,都会在一次次看似合理的局部修改中,逐渐偏离最初的轨道,直到变得无法理解、无法维护。
1. 实验设定:一个看似简单的自我迭代循环
这个实验的起点并不复杂。其核心思想是构建一个能够读取、分析、修改自身源代码的 AI 代理。
1.1 核心组件与工作流
整个系统可以拆解为几个关键部分:
- 主体脚本:一个 Python 脚本,比如叫
self_evolve.py。它包含初始的业务逻辑,也是后续被修改的对象。 - AI 代理:一个具备代码理解和生成能力的 AI 模型(例如基于 Claude Code、GPT-4 等技术的接口)。它的角色是“代码医生”兼“重构工程师”。
- 控制循环:一个外部的“控制器”脚本。它负责在每一轮循环中执行以下操作:
- 读取:加载
self_evolve.py的当前内容。 - 分析:将代码和修改指令(如“优化性能”、“增加错误处理”、“提高可读性”)一同发送给 AI 代理。
- 修改:接收 AI 代理返回的“改进版”完整代码。
- 替换:将原文件内容替换为新生成的代码。
- 验证:尝试执行新代码,确保其至少能无错误地导入或运行一个简单测试。
- 迭代:记录本轮信息,然后进入下一轮。
- 读取:加载
这个过程听起来像是一个完美的自动化代码优化流水线。初始指令可能是“让这段代码更 Pythonic”或者“为这个函数添加详细的文档字符串”。
1.2 最初的期望与现实的偏差
在实验开始前,一个合理的预期是:AI 会在前几轮做出明显的、积极的改进。例如:
- 将冗长的
for循环改为列表推导式。 - 添加缺失的
try...except块。 - 将魔法数字替换为有意义的常量。
- 拆分过于庞大的函数。
在最初的 10-20 轮,情况似乎确实如此。代码看起来更整洁了,注释也多了。你可能会觉得,照这个趋势下去,144 轮后应该能得到一份“完美”的代码。
然而,问题就埋藏在这个“自我指涉”和“无限授权”的循环中。AI 代理每一轮看到的,都是它自己上一轮修改后的产物。它没有“最初设计意图”的长期记忆,也没有对“代码整体架构”的全局观。它的优化是基于当前快照的、局部的、针对单条指令的反应。
2. 崩溃的序曲:代码“熵增”与逻辑漂移
大约从第 30-50 轮开始,一些微妙的变化开始积累,最终导致了系统的质变。崩溃不是突然发生的,而是多种因素逐渐累积的结果。
2.1 “过度工程化”与复杂度爆炸
AI 代理,尤其是被要求“优化”或“改进”时,有一种强烈的倾向:增加抽象层次。在第一轮,它可能只是添加了一个日志函数。在第五轮,它可能觉得这个日志函数不够通用,于是将其重构成一个具有多种格式和输出目标的日志“类”。在第十轮,它可能又为这个类添加了配置文件读取、异步写入、日志轮转等“企业级”功能。
于是,一个原本只有 50 行、功能清晰的脚本,逐渐膨胀为一个包含多个模块、类、设计模式(如单例、工厂)的“框架”。而脚本的核心业务逻辑,被埋没在层层封装和间接调用之中。每一轮修改都在增加代码的“重量”,而不是提升其核心功能的“质量”。
# 初始版本(简化的核心逻辑) def process_data(input_list): result = [] for item in input_list: if item > 0: result.append(item * 2) return result # 第N轮后可能变成(示意) class DataProcessorConfig: def __init__(self, multiplier=2, filter_positive=True): self.multiplier = multiplier self.filter_positive = filter_positive class DataProcessor: def __init__(self, config: DataProcessorConfig): self.config = config self.logger = LoggerFactory.get_logger(self.__class__.__name__) def _apply_filter(self, item): if self.config.filter_positive: return item > 0 return True def _apply_transformation(self, item): return item * self.config.multiplier def process(self, input_iterable): self.logger.info(f"Processing iterable of length {len(input_iterable)}") return [self._apply_transformation(i) for i in input_iterable if self._apply_filter(i)] # 控制器还需要实例化并调用 config = DataProcessorConfig() processor = DataProcessor(config) result = processor.process(data)对于简单的任务,这种复杂度是完全没有必要的。但 AI 在每一轮独立的评估中,都可能认为“引入配置类提高了灵活性”、“使用工厂模式管理日志是好的实践”,从而一步步将代码推向过度设计。
2.2 意图迷失与功能蠕变
最初的代码有一个明确的、单一的功能。但在循环修改中,AI 可能会误解或过度扩展这个功能。
例如,原始脚本是读取一个 CSV 文件并计算某列的平均值。某轮 AI 可能“改进”为同时支持 CSV 和 JSON。下一轮,它可能觉得应该增加从网络 API 获取数据的能力。再下一轮,它可能又加入了数据清洗和异常值检测。
最终,脚本变成了一个试图解决所有相关问题的“瑞士军刀”,但每个功能都做得不深不透,且彼此之间的耦合度越来越高。代码的“核心职责”变得模糊,维护和理解成本呈指数级上升。
2.3 风格不一致与结构腐蚀
即使我们要求 AI“保持一致的代码风格”,但在多轮修改中,风格漂移几乎不可避免。
- 命名:一个变量可能从
data变成input_data,再变成raw_input_dataset。函数名可能从calc()变成calculate(),再变成perform_calculation()。 - 结构:一开始是函数式编程风格,几轮后可能引入类,再几轮后可能又混入了全局变量和装饰器,导致范式混杂。
- 错误处理:有的函数用返回
(success, result)元组,有的用异常,有的用None,形成了不一致的错误处理契约。
这种不一致性不会导致代码立即崩溃,但会严重损害可读性和可维护性,为后续的修改埋下隐患。
3. 断裂点:当修改链失去可控性
前期的“熵增”是缓慢的,而真正的“断裂”往往发生在一些关键环节。
3.1 依赖断裂与环境毒化
AI 在添加新功能时,很可能会引入新的第三方库依赖(import requests, import numpy)。控制器脚本在验证阶段,可能只检查语法和简单运行。如果验证环境是一个干净的、安装了所有依赖的环境,那么检查总能通过。
但问题在于:
- 依赖冲突:新引入的库可能与现有库的版本不兼容。
- 依赖递归:AI 可能为了一个极其边缘的功能,引入一个庞大且不必要的库,污染了项目环境。
- 隐式依赖:AI 生成的代码可能使用了特定 Python 版本的新特性(如
walrus operator),而运行环境并不支持。
最终,代码虽然“正确”,但只能在一种非常特定的、脆弱的依赖状态下运行。一旦环境稍有变化(例如部署到另一台机器),就会立即失败。
3.2 逻辑循环与自我引用
这是最“有趣”也最危险的崩溃模式。在自我修改的上下文中,AI 可能会创建出指代自身的逻辑。例如:
- 代码中生成了一段字符串,该字符串恰好是调用 AI API 的指令。
- 在添加调试或监控功能时,代码试图去读取或分析它自己的源文件(即
self_evolve.py),而在修改期间,这个文件可能正处于被写入的不确定状态。 - 更隐晦的情况是,业务逻辑无意中形成了一个循环,其终止条件在后续修改中被破坏,导致无限循环或递归。
当代码试图观察或影响其自身的生成/执行环境时,就容易产生不可预测的行为,甚至导致控制器脚本死锁或资源耗尽。
3.3 语义正确性与功能正确性的背离
这是最隐蔽的“断裂”。代码在每一轮都语法正确,甚至能通过简单的单元测试(如果设置了的话),但它实现的功能已经悄悄偏离了初衷。
假设原始函数是“返回列表中所有正数的平方”。经过多轮“优化”后,函数可能变成了“返回列表中所有非负数的平方的绝对值”。从数学和代码上看,对于大部分输入,结果似乎没变。但对于边界情况(如0或特定负数),行为已经改变。AI 在每一轮都只基于当前代码和模糊的指令进行修改,它无法追溯和保证与最初版本的“语义等价性”。
最终,代码变得“完美”地运行,却安静地计算着错误的东西。这种断裂比直接报错更可怕,因为它产生了看似合理的错误结果。
4. 从崩溃中反思:AI 编程助手的正确打开方式
这次实验的“崩溃”并非失败,而是一个宝贵的压力测试。它清晰地揭示了当前 AI 编码能力的边界,以及我们应该如何与之协作。
4.1 明确角色:AI 是副驾,不是自动驾驶
这个实验最核心的教训是:不能将代码的长期演进和架构决策完全交给一个没有全局上下文和持续记忆的 AI。
- 人类负责战略和架构:定义清晰的模块边界、接口契约、核心数据流和关键算法。这是 AI 不擅长的。
- AI 负责战术和执行:在人类划定的边界内,让 AI 去实现具体函数、编写样板代码、添加文档、进行代码风格化、建议重构方案。这是 AI 的高效区。
正确的模式是“人类评审-AI 执行”的短反馈循环,而不是“AI 自我演进”的长开放循环。
4.2 为 AI 设定强约束和清晰上下文
当你让 AI 修改代码时,给出的指令必须极其精确和有限制性。
- 坏指令:“优化这段代码。”
- 好指令:“将
process_data函数中的for循环改为列表推导式,保持功能完全不变。不要修改函数签名和其他任何部分。” - 更好指令:“在
UserDB类的add_user方法中,添加参数验证(用户名非空、邮箱格式),并抛出ValueError。请只修改这个方法,遵循项目现有的错误处理模式(参见validate_email函数)。”
你需要为 AI 提供“作战地图”(架构图、接口文档)和“交战规则”(编码规范、测试用例),而不是让它在一片模糊中自行探索。
4.3 建立可靠的验证防线,而不仅仅是语法检查
控制循环中的验证环节至关重要,且必须多层次:
- 静态检查:每次修改后,自动运行
pylint,flake8,mypy等工具,确保代码风格和基本类型安全。 - 单元测试:必须有一套覆盖核心功能的单元测试。修改后的代码必须通过所有现有测试。这是防止“语义漂移”最重要的防线。
- 集成测试:对于关键流程,需要有端到端的集成测试,确保模块组合后工作正常。
- 性能基准(可选):如果优化目标是性能,需要有基准测试来防止“优化”后性能反而下降。
任何一轮修改如果未能通过上述任何一道防线,就应该被自动拒绝,并触发人工审查。
4.4 采用版本控制与差异审视
绝对不应该让 AI 直接覆盖源文件。实验中的控制器应该将每一轮修改作为一个新的提交(或分支)保存到 Git 中。
这样做的价值在于:
- 可追溯:你可以清晰地看到每一轮 AI 具体改了哪里。
- 可回滚:当发现逻辑漂移或引入 bug 时,可以轻松回退到上一个稳定版本。
- 可分析:通过
git diff,你能快速理解 AI 的“思考过程”,发现其修改模式中的问题,从而优化你的指令。
将 AI 的修改纳入版本控制,是将其纳入成熟软件开发流程的关键一步。
5. 实践指南:构建一个健壮的 AI 辅助迭代流程
基于以上反思,我们可以设计一个更安全、更有效的流程来利用 AI 进行代码迭代,而不是走向失控的循环。
5.1 流程设计:受控的迭代循环
一个健壮的流程应该如下图所示(此处以文字描述):
- 初始化:准备好清晰的原始代码、详细的修改需求说明书、完整的测试套件和编码规范文档。
- 单次修改请求:向 AI 发出一个非常具体、范围有限的修改任务。任务描述应包含“做什么”、“不做什么”、“参考什么”。
- 生成与提交:AI 生成代码补丁或差异。系统不直接应用,而是将其保存为一个待评审的变更集(如 Git Pull Request)。
- 自动化验证流水线:自动触发静态检查、单元测试、集成测试。任何失败都会标记该次修改为“失败”。
- 人工评审:开发者审查 AI 提交的变更。重点看:是否理解了意图?是否引入了不必要的复杂度?是否保持了风格一致?变更范围是否可控?
- 合并或反馈:如果通过,则合并到主分支;如果不通过,则将具体的评审反馈(“这个类没必要引入”、“请用更直接的方式处理错误”)作为新的、更精确的指令,回到第 2 步。
- 归档与学习:记录本次任务的成功与否,用于优化未来的任务指令模板。
这个流程的核心是“人类在环”和“小步快跑”,用自动化和人工审查构建双重护栏。
5.2 指令工程:如何与 AI 沟通代码修改
你的指令质量直接决定输出质量。以下是一些模板:
- 重构指令:“重构
calculate_stats函数,使其长度不超过 30 行。可以提取辅助函数,但不得改变函数的输入输出行为。所有提取的新函数名必须以_开头。” - 添加功能指令:“在
FileLoader类中,添加一个load_from_url(url)方法。请复用现有的_validate_format方法,并添加网络超时和状态码检查。参考network_utils.py中的safe_fetch函数处理异常。” - 修复 Bug 指令:“根据附带的堆栈跟踪信息,修复
DataPipeline在输入为空列表时崩溃的问题。请确保修复后,空列表输入返回一个空列表,并记录一条警告日志。”
指令要像给一位能力很强但缺乏背景知识的新同事布置任务一样,清晰、无歧义、有边界。
5.3 工具链整合建议
将上述流程融入现有开发工具链:
- IDE 插件:使用 VSCode 中的 Copilot Chat、Claude Code 等,在编辑器中完成小范围的、交互式的代码修改。这是最安全、最可控的方式。
- CI/CD 集成:可以将复杂的、模式化的代码生成任务(如为新的 API 模型生成 CRUD 代码)编写为脚本,调用 AI API 生成代码,然后作为 CI 流水线的一个环节,自动发起 PR 并运行测试。
- 代码库知识库:在要求 AI 修改代码前,利用 RAG 技术,将项目的架构文档、API 文档、设计决策记录等提供给 AI 作为上下文,使其修改更符合项目背景。
让 AI 在强大的工具链约束和引导下工作,它能成为倍增器;放任它在循环中自我演进,它可能会成为不可控的复杂化引擎。
144 轮的自我修改实验,最终揭示的不是 AI 的愚蠢,而是复杂系统在缺乏高层设计和持续校准下的必然走向——混乱。代码的本质是精确的逻辑表达,而长期的、无监督的局部优化,很容易侵蚀其整体的清晰性和正确性。这给我们的启示远比“如何调试一个 AI 写的 bug”要深刻:它关乎如何在智能时代,设计一种人机共生、权责清晰、可持续的软件演进模式。最强大的工具,永远需要最深思熟虑的使用方法。