基于LLM的Python脚本自我进化:构建AI驱动的代码优化框架
1. 项目概述:当Python脚本学会自我进化
几年前,如果有人告诉我,我写的Python脚本能在我喝咖啡的时候自己给自己“打补丁”、优化逻辑,我肯定会觉得这是科幻小说里的情节。但今天,这已经是我日常工作流的一部分。这个项目的核心,就是探索如何让大型语言模型(LLM)成为Python脚本的“贴身教练”,实现脚本的自我迭代与改进。
简单来说,它解决了一个非常实际的痛点:代码写完后的维护与优化。我们都有过这样的经历——写了一个数据处理脚本,跑了几次后发现性能瓶颈,或者逻辑有瑕疵,又或者需求微调了。传统的做法是,我们手动去读日志、分析性能、修改代码、重新测试。这个过程耗时耗力,尤其是在处理复杂脚本或遗留代码时。而这个项目,就是构建一个框架,让脚本能够自动分析自己的运行结果(日志、性能数据、错误信息),然后调用LLM(比如GPT-4、Claude 3或者开源的Llama 3)来生成改进建议,甚至直接应用补丁。
它适合谁呢?首先是像我这样的数据工程师、自动化脚本开发者,每天要和大量临时性、一次性的脚本打交道。其次是DevOps工程师,需要维护部署和监控脚本。最后,任何对AI辅助编程和自动化代码质量提升感兴趣的人,都能从中获得启发。这不是要取代程序员,而是提供一个强大的“副驾驶”,把我们从重复性的代码调试和微调中解放出来,去关注更核心的业务逻辑和架构设计。
2. 核心架构设计:构建一个闭环的自我改进系统
让脚本自我改进,听起来很玄乎,但拆解开来,其核心是一个经典的“观察-思考-行动”闭环,或者说是强化学习中的智能体(Agent)架构。只不过,这里的“智能体”是LLM,“环境”是脚本的运行上下文。
2.1 系统工作流与组件拆解
整个系统的骨架可以概括为以下五个核心组件,它们协同工作,形成一个自动化的改进管道:
脚本执行与监控器:这是系统的“眼睛”和“手”。它负责以安全、可控的方式运行目标Python脚本。更重要的是,它需要全面监控脚本的执行过程,捕获标准输出、标准错误、执行时间、内存消耗、CPU使用率等关键指标,并将所有信息结构化地记录下来。这里的一个关键设计是“沙箱化”执行,确保被改进的脚本不会对主系统造成破坏。
上下文收集与构建器:这是系统的“记忆”。仅仅有运行日志是不够的。LLM需要更丰富的上下文才能做出明智的判断。这个组件负责收集四类关键信息:
- 代码本身:目标脚本的完整源代码。
- 运行环境:Python版本、已安装的包及其版本、操作系统信息。
- 执行结果:上一步监控器捕获的所有日志、错误、性能数据。
- 改进目标:我们期望脚本朝哪个方向优化?是提高速度(性能)、减少内存占用(资源)、修复bug(正确性)、增强可读性(代码质量),还是增加新功能?这个目标需要以清晰、可量化的方式定义。
LLM集成与提示工程引擎:这是系统的“大脑”。它负责与选定的LLM API(如OpenAI、Anthropic或本地部署的模型)进行通信。其核心价值在于“提示工程”——如何将前面收集的庞杂上下文,组织成一段清晰、具体、可操作的指令(Prompt),引导LLM产出高质量的代码改进建议。提示词的质量直接决定了改进效果。
改进分析与应用器:这是系统的“决策手”。LLM返回的可能是自然语言描述的建议,也可能是直接的代码差异(Diff)。这个组件需要解析LLM的输出,判断改进建议是否安全、合理。对于简单的、低风险的改进(如修正一个明显的语法错误、优化一个循环结构),它可以自动生成补丁并应用。对于复杂的、有潜在破坏性的更改(如重构核心算法),它可能只是生成一份详细的改进报告,供开发者审核。
迭代控制与评估模块:这是系统的“调度中心”。它控制整个改进循环:运行脚本 -> 收集数据 -> 请求LLM -> 应用改进 -> 再次运行。它还需要定义停止条件:是达到性能目标后停止?是连续N次迭代没有显著改进后停止?还是遇到无法自动修复的错误时停止?同时,它要负责评估每次改进的效果(例如,执行时间缩短了百分之多少),为整个流程提供反馈。
提示:安全第一。在架构设计之初,就必须把“安全”刻在骨子里。自动修改代码是高风险操作。我的核心原则是:永远不在生产环境直接运行自我改进脚本;对于任何修改,都必须先经过一个“模拟运行”或“代码审查”阶段;并且为原始代码保留完整的版本备份和回滚机制。
2.2 技术选型背后的逻辑
为什么用Python?因为项目本身就是关于Python脚本的,用Python来构建这个框架有天然优势:丰富的库支持(如subprocess运行脚本、psutil监控资源、diff-match-patch处理代码差异)、与LLM API交互方便(openai,anthropic等库),并且最终改进的代码也是Python,同构性让分析更准确。
在LLM的选择上,我经历了从封闭到开放的探索。初期我使用GPT-4 Turbo,因为它代码能力强、指令跟随好,提示工程相对省心。但成本和对网络的依赖是问题。后来我转向了本地部署的Llama 3 70B(或更小的8B版本配合量化)。虽然提示工程需要更精细,且首次响应可能慢一些,但数据完全私有、无使用成本的优势对于长期、批量的脚本改进任务来说是决定性的。对于企业内部或对数据敏感的场景,开源模型是必选项。
监控工具上,我放弃了简单的time模块,采用了cProfile进行性能剖析,它能告诉我具体是哪个函数耗时最多。结合memory-profiler来定位内存泄漏点。这些工具产生的报告,是提供给LLM的、极具价值的“诊断书”。
3. 从零搭建:一个最小可行产品的实现
理论说再多,不如一行代码。下面我将带你一步步搭建一个最基础的自我改进脚本框架。这个MVP能自动发现并修复脚本中的简单错误和性能问题。
3.1 基础环境与依赖准备
首先,创建一个干净的虚拟环境并安装核心依赖。这里我选择uv作为包管理器,它速度极快,当然用pip也一样。
# 创建项目目录 mkdir self-improving-python && cd self-improving-python # 使用uv初始化(或 python -m venv venv && source venv/bin/activate) uv venv source .venv/bin/activate # Windows: .venv\Scripts\activate # 安装核心依赖 uv add openai # 如果你用OpenAI API # 或者,如果你用本地LLM,例如通过Ollama # uv add ollama uv add psutil uv add pytest # 用于运行测试(作为改进正确性的验证) uv add black # 可选,用于代码格式化,让LLM输出更规范我们的项目结构初步规划如下:
self-improving-python/ ├── agent/ # 核心智能体模块 │ ├── __init__.py │ ├── executor.py # 脚本执行与监控器 │ ├── context_builder.py # 上下文收集器 │ ├── llm_client.py # LLM集成客户端 │ └── patcher.py # 代码分析与应用器 ├── scripts/ # 待改进的目标脚本存放处 │ └── target_script.py ├── config.yaml # 配置文件(API密钥、模型参数等) ├── main.py # 主循环入口 └── requirements.txt3.2 核心模块实现详解
1. 脚本执行与监控器 (agent/executor.py)
这个模块的任务是安全地运行脚本并捕获一切。我使用subprocess模块,因为它可以更好地隔离子进程环境。
import subprocess import sys import time import psutil import traceback from typing import Dict, Any, Tuple class ScriptExecutor: def __init__(self, script_path: str, timeout: int = 30): self.script_path = script_path self.timeout = timeout def run(self) -> Dict[str, Any]: """执行脚本并返回完整结果字典""" result = { 'success': False, 'return_code': None, 'stdout': '', 'stderr': '', 'execution_time': 0.0, 'memory_peak_mb': 0.0, 'cpu_percent': 0.0, } start_time = time.time() try: # 使用psutil监控资源 process = psutil.Popen( [sys.executable, self.script_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) stdout, stderr = process.communicate(timeout=self.timeout) end_time = time.time() # 获取资源使用情况(注意:communicate后进程已结束,需在过程中监控,此处简化) # 更精确的做法是使用psutil.Process(process.pid)在循环中采样 try: ps_process = psutil.Process(process.pid) memory_info = ps_process.memory_info() result['memory_peak_mb'] = memory_info.rss / 1024 / 1024 # 转MB result['cpu_percent'] = ps_process.cpu_percent(interval=0.1) except (psutil.NoSuchProcess, psutil.AccessDenied): pass result.update({ 'success': process.returncode == 0, 'return_code': process.returncode, 'stdout': stdout, 'stderr': stderr, 'execution_time': end_time - start_time, }) except subprocess.TimeoutExpired: result['stderr'] = f"Error: Script execution timed out after {self.timeout} seconds." result['success'] = False except Exception as e: result['stderr'] = f"Error executing script: {traceback.format_exc()}" result['success'] = False return result实操心得:
subprocess的timeout参数至关重要,它能防止有bug的脚本(如死循环)拖垮整个改进系统。另外,对于资源监控,在生产级应用中,我会在另一个线程中定时采样psutil.Process的数据,以获得更准确的峰值内存和CPU曲线,而不是像上面这样只在结束时抓取一个近似值。
2. LLM集成与提示工程 (agent/llm_client.py)
这是与LLM对话的核心。我设计了一个包含系统指令和用户上下文的提示模板。
import openai # 或 from ollama import Client import yaml from typing import List, Dict class LLMClient: def __init__(self, config_path: str = 'config.yaml'): with open(config_path, 'r') as f: config = yaml.safe_load(f) self.model = config.get('llm', {}).get('model', 'gpt-4-turbo-preview') self.api_key = config.get('llm', {}).get('api_key') # 初始化客户端,这里以OpenAI为例 self.client = openai.OpenAI(api_key=self.api_key) def build_prompt(self, code: str, execution_result: Dict, improvement_goal: str) -> List[Dict]: """构建发送给LLM的消息列表""" system_message = { "role": "system", "content": """你是一个资深的Python代码优化专家。你的任务是分析给定的Python脚本及其运行结果,并提出具体、可操作的改进建议。 请严格按照以下步骤思考: 1. 首先,判断脚本是否运行成功。如果失败,精准定位错误原因(语法、运行时、逻辑错误)。 2. 如果成功,分析其性能(执行时间、内存)和代码质量(可读性、符合PEP8、潜在bug)。 3. 根据用户提供的“改进目标”,给出最优先的改进方案。 你的输出必须是纯文本,并且包含以下两个部分: - **分析摘要**:用简短几句话总结核心问题。 - **具体改进建议**:列出1-3条最关键的改进点,每条建议需说明理由。如果可能,直接提供修改后的代码片段(用```python包裹)。 不要输出与代码改进无关的内容。""" } user_content = f""" ## 需要改进的Python脚本 ```python {code} ``` ## 脚本运行结果 - 是否成功: {execution_result['success']} - 返回码: {execution_result['return_code']} - 标准输出: {execution_result['stdout'][:500]}... # 截断长输出 - 标准错误: {execution_result['stderr']} - 执行时间: {execution_result['execution_time']:.2f}秒 - 内存峰值: {execution_result['memory_peak_mb']:.1f} MB ## 本次改进的核心目标 {improvement_goal} 请根据以上信息,提供代码改进建议。""" return [system_message, {"role": "user", "content": user_content}] def get_improvement_suggestions(self, prompt_messages: List[Dict]) -> str: """调用LLM API获取改进建议""" try: response = self.client.chat.completions.create( model=self.model, messages=prompt_messages, temperature=0.2, # 低温度,保证输出稳定、可重复 max_tokens=1500, ) return response.choices[0].message.content except Exception as e: return f"Error calling LLM API: {e}"注意事项:提示工程是成败的关键。我花了大量时间调整系统指令,让LLM扮演一个“严谨的代码审查员”角色,而不是天马行空的创造者。
temperature参数设为较低值(如0.2),是为了让改进建议更确定、更少“胡言乱语”。另外,在用户上下文中,我结构化了所有输入信息,并明确了输出格式,这大大提高了LLM返回结果的可用性。
3. 主循环与迭代控制 (main.py)
最后,我们把所有模块串联起来,形成一个循环。
import os from agent.executor import ScriptExecutor from agent.llm_client import LLMClient from agent.context_builder import ContextBuilder # 假设有这个模块来收集环境信息 import difflib def main(): script_path = "./scripts/target_script.py" improvement_goal = "优先修复任何导致运行失败的错误,然后优化执行速度。" max_iterations = 5 llm_client = LLMClient() context_builder = ContextBuilder() for iteration in range(1, max_iterations + 1): print(f"\n=== 迭代第 {iteration} 次 ===") # 1. 执行并监控 executor = ScriptExecutor(script_path) result = executor.run() print(f"执行结果: 成功={result['success']}, 耗时={result['execution_time']:.2f}s, 内存={result['memory_peak_mb']:.1f}MB") if result['stderr']: print(f"错误输出: {result['stderr'][:200]}") # 2. 收集上下文(代码、环境) with open(script_path, 'r') as f: current_code = f.read() env_info = context_builder.get_environment_info() # 3. 构建Prompt并调用LLM prompt = llm_client.build_prompt(current_code, result, improvement_goal) suggestions = llm_client.get_improvement_suggestions(prompt) print(f"\nLLM改进建议:\n{suggestions}") # 4. 解析并应用改进(这里简化:手动审核) # 在实际系统中,可以集成自动解析diff和应用,但初期强烈建议手动审核。 print("\n--- 本次迭代结束。请手动审核以上建议并修改脚本。---") # 假设我们手动修改了脚本,这里就进入下一次迭代 # 自动应用代码需要更复杂的 diff 解析和代码合并逻辑,风险较高。 user_input = input("按回车继续下一轮迭代,或输入 'q' 退出: ") if user_input.lower() == 'q': break if __name__ == "__main__": main()这个MVP已经具备了核心功能:运行脚本、诊断问题、获取AI建议。虽然最后一步“应用改进”是手动的,但这恰恰是最安全的做法。你可以根据LLM的建议手动修改target_script.py,然后开始下一轮迭代,观察问题是否被解决,性能是否提升。
4. 进阶实战:处理复杂场景与提升改进质量
有了基础框架,我们就可以挑战更复杂的场景了。自我改进脚本的真正威力,体现在处理那些令开发者头疼的“灰色地带”问题上。
4.1 场景一:优化算法时间复杂度
假设我们有一个脚本slow_script.py,功能是计算一个列表中所有两数之和等于目标值的配对。初版代码可能使用了最直观的双重循环。
# scripts/slow_script.py (初始版本) def find_pairs_naive(nums, target): """时间复杂度 O(n^2) 的暴力解法""" pairs = [] for i in range(len(nums)): for j in range(i+1, len(nums)): if nums[i] + nums[j] == target: pairs.append((nums[i], nums[j])) return pairs if __name__ == "__main__": # 用一个较大的列表测试 import random test_nums = [random.randint(1, 1000) for _ in range(2000)] target = 100 result = find_pairs_naive(test_nums, target) print(f"Found {len(result)} pairs.")当我们运行这个脚本,并将improvement_goal设置为“显著优化执行速度,特别是对于大数据集”,执行监控器会报告一个较长的执行时间(比如2秒以上)。LLM在接收到代码、慢速的运行结果以及这个目标后,很可能会给出以下建议:
分析摘要:脚本使用了O(n^2)时间复杂度的双重循环,在处理2000个元素的列表时效率低下。具体改进建议:
- 使用哈希集合优化查找:将内层循环的查找操作从O(n)降至O(1)。算法思路是遍历一次列表,对于每个元素
num,计算其补数complement = target - num,然后检查complement是否在之前遍历过的元素集合中。这能将总体时间复杂度从O(n^2)降至O(n)。- 提供修改后的代码:
def find_pairs_optimized(nums, target): """时间复杂度 O(n) 的优化解法""" pairs = [] seen = set() for num in nums: complement = target - num if complement in seen: pairs.append((complement, num)) # 注意顺序,与之前输出一致 seen.add(num) return pairs
系统在后续迭代中运行优化后的版本,执行时间通常会从秒级降至毫秒级。这个案例展示了LLM如何将“优化速度”的模糊目标,转化为具体的算法重构方案。
4.2 场景二:修复隐蔽的逻辑Bug
有些Bug不会导致程序崩溃,但会产生错误结果。例如,一个计算平均值的脚本,但忽略了空列表的情况。
# scripts/buggy_script.py def calculate_average(numbers): """计算平均值,但有潜在除零错误""" total = sum(numbers) average = total / len(numbers) # 如果numbers为空,这里会抛出ZeroDivisionError return average data = [10, 20, 30] print(f"The average is: {calculate_average(data)}") # 但如果我们不小心传入空列表 empty_data = [] print(f"The average is: {calculate_average(empty_data)}") # 这里会崩溃当脚本因为未处理的空列表而崩溃时,执行结果中的stderr会包含ZeroDivisionError: division by zero。LLM在分析这个错误时,不仅能定位到出错行,还常常能推断出业务逻辑的意图,从而给出更健壮的修复方案:
分析摘要:脚本在第4行因除零错误而失败。根本原因是函数
calculate_average没有处理输入列表可能为空的情况。具体改进建议:
- 添加输入验证:在计算前检查列表长度。如果列表为空,应返回一个合理的值(如0或None)或抛出一个更清晰的异常。
- 修改后的代码:
def calculate_average(numbers): """计算平均值,增加空列表处理""" if not numbers: # 检查列表是否为空 return 0.0 # 或者 raise ValueError("Cannot calculate average of an empty list") total = sum(numbers) average = total / len(numbers) return average
- 补充单元测试建议:建议为这个函数添加测试用例,覆盖空列表、正常列表、负数列表等边界情况。
这个例子体现了LLM在代码审查和防御性编程方面的价值。它不仅能“打补丁”,还能引导我们写出更健壮的代码。
4.3 提升改进质量的工程化技巧
要让这个系统稳定可靠地工作,不能只靠一个简单的Prompt。以下是我在实践中总结的几个关键技巧:
1. 为LLM提供“工具”和“范例”LLM不擅长精确计算或记忆所有API。我们可以把关键信息以“工具”的形式提供给它。例如,在上下文中附带一份简短的“性能优化备忘单”:
可考虑的优化方向: - 数据结构:用集合(set)替代列表(list)进行成员检查(O(1) vs O(n))。 - 循环:避免在循环内重复计算不变的值;考虑使用列表推导式。 - 内置函数:优先使用map/filter/sum等内置函数,它们由C实现,速度更快。 - 算法:对于查找问题,考虑使用哈希表(dict/set);对于排序问题,评估是否真需要全局排序。同时,在Prompt中提供一两个成功的改进范例,能显著提升LLM输出的格式和质量。
2. 实施多轮对话与自我验证单次LLM调用可能考虑不周。我们可以设计一个多轮对话流程:
- 第一轮:LLM给出初步改进建议和代码Diff。
- 第二轮:系统自动将修改后的代码放入一个简单的语法检查器(如
py_compile)或风格检查器(如flake8)中运行,将检查结果反馈给LLM:“你提供的修改在代码行X存在语法错误[错误信息]。请修正。” - 第三轮:系统用修改后的代码跑一遍核心的单元测试(如果有的话),将测试结果反馈给LLM。 这种“提出方案-验证反馈-修正方案”的循环,能极大提高最终代码的正确率。
3. 量化评估与停止条件不能无限改进下去。我们需要定义明确的、可量化的成功标准。
- 对于性能优化:目标可以是“执行时间减少50%”或“内存占用低于100MB”。
- 对于Bug修复:目标就是“所有预定义的测试用例通过”。
- 通用停止条件:
- 连续3次迭代,关键指标(如运行时间)改进幅度小于5%。
- 达到了预设的最大迭代次数(如10次)。
- LLM连续两次给出的建议被认为是“无实际改进”或“无法应用”。 将这些条件编码到迭代控制模块中,系统就能在适当的时候自动停止,避免无意义的循环。
5. 避坑指南与局限性反思
在近一年的实践中,我踩过不少坑,也深刻认识到这项技术的边界在哪里。分享出来,希望能帮你少走弯路。
5.1 常见问题与实战解决方案
问题1:LLM的改进建议“隔靴搔痒”或脱离实际。
- 现象:LLM总是建议一些无关痛痒的修改,比如把变量名
a改成amount,但对真正的性能瓶颈视而不见。 - 根因:上下文信息不足或改进目标不明确。LLM看不到性能剖析的细节。
- 解决方案:将
cProfile的输出作为关键上下文提供给LLM。例如:
然后把import cProfile, pstats, io pr = cProfile.Profile() pr.enable() # ... 运行目标函数 ... pr.disable() s = io.StringIO() ps = pstats.Stats(pr, stream=s).sort_stats('cumulative') ps.print_stats(20) # 打印最耗时的前20个函数 profile_result = s.getvalue()profile_result这个字符串放进Prompt里:“以下是性能剖析结果,请重点关注耗时最多的函数:[profile_result]”。这样LLM就能精准定位热点。
问题2:自动应用的修改引入了新的Bug。
- 现象:系统自动合并了LLM提供的Diff,结果脚本直接无法运行,或者产生了更隐蔽的逻辑错误。
- 根因:缺乏可靠的自动化测试作为安全网。
- 解决方案:没有测试,就不要自动应用。这是铁律。对于任何试图自我改进的脚本,必须为其配备一套哪怕是最基本的“冒烟测试”(Smoke Tests)。在每次应用修改前,先在一个隔离的、安全的环境中运行这些测试。只有测试全部通过,修改才能被提交。测试可以很简单,比如:
# test_target_script.py import subprocess import sys def test_script_runs_without_error(): """最基本测试:脚本能正常启动并退出""" result = subprocess.run([sys.executable, 'scripts/target_script.py'], capture_output=True, text=True, timeout=10) assert result.returncode == 0, f"Script failed with stderr: {result.stderr}" print("基础运行测试通过。") def test_core_functionality(): """核心功能测试:导入主要函数并验证输入输出""" from scripts.target_script import calculate_average assert calculate_average([1,2,3]) == 2.0 assert calculate_average([]) == 0.0 # 根据我们的设计 print("核心功能测试通过。")
问题3:迭代陷入局部最优或开始“胡言乱语”。
- 现象:改进了几轮后,代码变得冗长奇怪,或者为了微小的性能提升牺牲了所有可读性。
- 根因:LLM没有“大局观”和“审美”,它只针对当前回合的反馈进行优化,可能过度拟合。
- 解决方案:引入“代码质量”作为硬性约束指标。可以使用
radon库计算代码的圈复杂度,或者用pylint进行代码风格评分。在Prompt中明确要求:“在保持或提升代码可读性(pylint分数不低于X)的前提下进行优化。”同时,设置一个“回溯”机制:如果连续两次迭代的代码质量评分下降超过阈值,则自动回滚到上一个最佳版本。
5.2 认清局限性:它不是什么银弹
在兴奋之余,我们必须清醒地认识到当前技术的边界:
无法进行深度的架构重构:LLM是基于已有代码的“模式匹配”和“组合创新”。它可以把双重循环改成哈希查找,但它无法将一个庞大的单体脚本拆分成微服务,也无法引入一个全新的设计模式来根本性解决技术债。这需要人类开发者的架构洞察力。
对业务逻辑的理解是表面的:LLM通过代码和注释来“猜测”业务意图。如果代码本身逻辑混乱、注释缺失,LLM很可能会误解意图,提出南辕北辙的“改进”。它无法替代产品经理或业务分析师。
存在“幻觉”风险:LLM可能会推荐使用一个不存在的库函数,或者引用一个过时的API。这就是为什么任何重要的、自动应用的修改,都必须经过测试验证。
成本与延迟问题:频繁调用高性能的LLM(如GPT-4)API,成本不容小觑。而使用本地大模型,则对计算资源有要求,且响应延迟较高。这决定了它更适合作为“离线优化工具”或“代码审查助手”,而非实时编程环境。
所以,我最常使用这个系统的场景是:在本地开发环境中,对刚刚写完的、或者从别处拷贝来的、不太熟悉的脚本,进行一轮“AI辅助的代码审查和快速优化”。把它当作一个不知疲倦、见多识广的实习生,它能快速指出明显的错误、低效的写法,并给出不错的改进草案。但最终拍板、理解业务、把握架构方向的,仍然是我自己。
这个项目的旅程让我明白,最强大的工具,永远是那些能扩展人类能力而非取代人类的工具。自我改进的Python脚本不是一个全自动的代码工厂,而是一面更智能的镜子,让我们能更清晰地看到自己代码的瑕疵,从而成长为更优秀的开发者。
