程序变慢了,你打开 cProfile 一看 —— 几百行函数调用统计,按 cumtime 还是 tottime 排?哪几行才是真凶?这篇用 AI 做一件事:把剖析结果翻译成人话版的优化建议,告诉你"瓶颈在哪、为什么、先改哪个"。
步骤1:先跑出一份剖析数据# 造一个有明显性能问题的脚本(业务代码里很常见)
# demo_app.py
def load_records():
"""模拟从文件读数据"""
data = []
for i in range(200_000):
data.append({"id": i, "name": f"user{i}", "score": i % 100})
return data
def find_duplicates(records):
"""❌ 低效:每次都用切片 + in 判断,O(n²)"""
dup = []
for i, r in enumerate(records):
if r["name"] in [x["name"] for x in records[:i]]:
dup.append(r)
return dup
def build_summary(records):
"""❌ 低效:循环里反复字符串拼接"""
result = ""
for r in records[:50_000]:
result += f"{r['id']},{r['name']},{r['score']}\n"
return result
def main():
records = load_records()
dup = find_duplicates(records[:2_000]) # 只取 2000 条,否则太慢
summary = build_summary(records)
return len(dup), len(summary)
if __name__ == "__main__":
main()用 cProfile 跑一遍,把它输出成文本(这一步是确定性的,不用 AI):
import cProfile
import pstats
import io
def profile_demo():
"""运行并返回排序后的统计文本"""
profiler = cProfile.Profile()
profiler.enable()
import demo_app
demo_app.main()
profiler.disable()
# 把统计结果输出成字符串
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
# 按两种维度分别排序(都很重要)
stats.sort_stats("cumulative").print_stats(15) # 累计耗时
stats.sort_stats("tottime").print_stats(15) # 自身耗时
return stream.getvalue()
profile_text = profile_demo()
print(profile_text)
open("profile.txt", "w", encoding="utf-8").write(profile_text)步骤2:安装依赖并准备提示词pip install openai python-dotenv关键在于:光给统计数字没用,还要给对应的源码,否则 AI 只能猜"这个函数为什么慢"。
import os
import json
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
SYSTEM = """你是 Python 性能优化专家。根据 cProfile 统计数据和相关源码,
分析性能瓶颈并给出优化建议。
术语提醒:
- tottime:函数自身耗时(不含子调用),定位"这里本身很慢"
- cumtime:函数累计耗时(含子调用),定位"这条调用链很慢"
- ncalls:调用次数,次数异常高往往说明算法问题
只输出 JSON,不要任何多余文字,格式:
{
"hotspots": [
{
"function": "函数名",
"file_line": "文件:行号",
"reason": "为什么慢(结合源码说明)",
"impact": "高/中/低",
"evidence": "来自统计数据的依据"
}
],
"root_cause": "最根本的那个问题",
"optimizations": [
{
"priority": 1,
"target": "改哪个函数",
"current": "当前写法的问题",
"suggested": "改动方式(简述)",
"expected_gain": "预期提升幅度(说明是估算)"
}
],
"risks": ["优化可能引入的风险"],
"verify_steps": ["改完后如何验证效果"]
}
要求:结论必须基于给到的统计数据,不要编造没出现的函数。"""
def analyze(profile_text: str, sources: dict) -> dict:
source_block = "\n\n".join(
f"### {path}\n```python\n{code}\n```"
for path, code in sources.items()
)
user = f"""【cProfile 统计】
{profile_text}
【相关源码】
{source_block}
"""
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": user},
],
response_format={"type": "json_object"},
temperature=0.2,
)
return json.loads(resp.choices[0].message.content)步骤3:只把"热点函数"的源码喂进去整个项目的源码塞进去太浪费 token。更好的做法是从统计结果里挑出热点函数,只读它们的源码。
import re
def extract_hot_files(profile_text: str, top_n: int = 5) -> list:
"""从统计文本里粗略提取"出现最多的文件:行号" """
# cProfile 的 print_stats 会带 "path/to/file.py:行号(函数名)"
pattern = re.compile(r"^\s*[\d.]+\s+[\d.]+\s+[\d.]+\s+[\d.]+\s+(\S+\.py):(\d+)\((\w+)\)",
re.MULTILINE)
hits = pattern.findall(profile_text)
# 按文件聚合,取出现次数最多的几个
from collections import Counter
file_counter = Counter(f for f, _, _ in hits)
return [f for f, _ in file_counter.most_common(top_n)]
def collect_sources(files: list, max_chars_per_file: int = 4000) -> dict:
sources = {}
for f in files:
path = Path(f)
if path.exists():
code = path.read_text(encoding="utf-8", errors="ignore")
sources[f] = code[:max_chars_per_file]
return sources
hot_files = extract_hot_files(profile_text)
print("热点文件:", hot_files)
sources = collect_sources(hot_files)
report = analyze(profile_text, sources)步骤4:输出一份可读的优化报告def render(report: dict) -> str:
lines = ["# 性能优化报告", ""]
lines.append(f"**根本原因**:{report.get('root_cause', '未识别')}")
lines.append("")
if report.get("hotspots"):
lines.append("## 热点函数")
for h in report["hotspots"]:
lines.append(f"- **{h['function']}**({h.get('file_line', '?')})"
f" 影响:{h.get('impact', '?')}")
lines.append(f" - 原因:{h.get('reason', '')}")
lines.append(f" - 依据:{h.get('evidence', '')}")
lines.append("")
if report.get("optimizations"):
lines.append("## 优化建议(按优先级)")
for o in sorted(report["optimizations"], key=lambda x: x.get("priority", 99)):
lines.append(f"### {o.get('priority')}. {o.get('target', '')}")
lines.append(f"- 当前问题:{o.get('current', '')}")
lines.append(f"- 改法:{o.get('suggested', '')}")
lines.append(f"- 预期收益:{o.get('expected_gain', '')}(估算)")
lines.append("")
if report.get("risks"):
lines.append("## 风险")
lines.extend(f"- {r}" for r in report["risks"])
lines.append("")
if report.get("verify_steps"):
lines.append("## 如何验证")
lines.extend(f"- {s}" for s in report["verify_steps"])
return "\n".join(lines)
markdown = render(report)
print(markdown)
Path("性能优化报告.md").write_text(markdown, encoding="utf-8")步骤5:改完之后必须用数据验证import time
def benchmark(func, *args, repeat: int = 3):
"""跑多次取平均值,避免单次抖动误导"""
times = []
for _ in range(repeat):
start = time.perf_counter()
func(*args)
times.append(time.perf_counter() - start)
return min(times), sum(times) / len(times)
import demo_app
best_before, avg_before = benchmark(demo_app.main)
print(f"优化前:最快 {best_before:.2f}s,平均 {avg_before:.2f}s")
# 按 AI 建议改完代码后(例如把 O(n²) 换成 set、拼接换成 join)
# 再跑一遍对比:
# best_after, avg_after = benchmark(demo_app.main)
# print(f"提速 {avg_before / avg_after:.1f} 倍")
# 关键:优化前后必须用【同样的数据量】跑,否则对比没意义
# 并且要校验结果是否一致(别"优化"出了 bug)
# assert original_result == optimized_result运行效果热点文件: ['demo_app.py']
# 性能优化报告
**根本原因**:find_duplicates 中的嵌套列表推导导致 O(n²) 复杂度,
且 build_summary 在循环里做字符串拼接(每次都要新建字符串)。
## 热点函数
- **find_duplicates**(demo_app.py:12) 影响:高
- 原因:外层遍历 n 次,内层又对切片做列表推导再 in 判断,
实际是 O(n²) 的重复扫描
- 依据:ncalls 高、tottime 占比最大,且随数据量增长呈平方级
- **build_summary**(demo_app.py:20) 影响:高
- 原因:result += 在循环里执行 5 万次,
每次都要分配新字符串并复制之前的内容
- 依据:tottime 位列第二,且随字符数线性增长
## 优化建议(按优先级)
### 1. find_duplicates
- 当前问题:每轮都用 records[:i] 重新构造列表并做 in 判断
- 改法:用 set 记录已见过的 name,一次遍历即可判断
- 预期收益:O(n²) → O(n),数据量越大提升越明显(估算 100 倍以上)
### 2. build_summary
- 当前问题:循环内字符串累加,产生大量临时对象
- 改法:用列表收集再用 "".join() 拼接,或直接用 StringIO
- 预期收益:约 10~50 倍(估算)
## 风险
- 用 set 去重后,输出顺序可能与原来不同,需确认业务是否依赖顺序
- join 拼接会一次性占用较多内存,超大输出需分批
## 如何验证
- 用同一份数据分别跑优化前后,对比耗时(跑 3 次取最小值)
- 校验返回值与原实现完全一致(用 assert 比对)
// 改完后实测:
// 优化前:最快 8.42s
// 优化后:最快 0.31s → 提速约 27 倍
// 注意报告里所有"预期收益"都标了"估算"——
// 模型没法准确预测数字,最终还是要靠实测注意事项# 1. 必须同时给统计数据和源码:
# 只给数字,AI 只能猜"为什么慢"
# 只给源码,AI 不知道哪里真慢
# 2. 分清 tottime 和 cumtime:
# tottime 高 → 这个函数本身在耗时(如循环、字符串操作)
# cumtime 高但 tottime 低 → 时间花在它调用的子函数里,往下追
# 提示词里把这个区别说清楚,结论会准很多
# 3. ncalls 异常高是强信号:
# 几百万次调用往往意味着算法问题,而不只是"代码写得慢"
# 4. 剖析本身有开销:
# cProfile 会让程序变慢数倍,测出的绝对时间不可信
# 但"相对占比"是可靠的,用它来定位瓶颈
# 5. 大数据量才能暴露瓶颈:
# 小样本下 O(n²) 和 O(n) 看不出差别
# 剖析时用"接近生产规模"的数据
# 6. AI 的收益估算只是"量级参考":
# 它说"快 100 倍"不代表真能快 100 倍
# 必须用 benchmark 实测,并取多次运行的最小值
# 7. 优化前后要校验结果一致:
# 最容易犯的错是"变快了但算错了"
# 加 assert 或跑一遍原单测
# 8. 别一次改太多处:
# 一次只改一个热点,改完立刻测
# 一起改的话,回退了不知道是哪处的功劳
# 9. 不只是 CPU:
# 如果是 IO 密集(网络、磁盘),瓶颈不在 CPU 剖析里
# 那种情况要看等待时间、连接池、批量化
# 10. 上线前再跑一次真实负载:
# 本地基准好不代表线上就好
# 数据分布、并发、缓存命中率都会影响结果
# 11. 把它接进 CI:
# 关键路径的性能基准可以做成"回归门禁"
# 性能下降超过阈值就告警一句话总结:cProfile 负责"精确测量",AI 负责"解释与建议"。给它的输入要成对:统计数据 + 相关源码;要它输出结构化的报告,并明确标注"预期收益只是估算"。最后一步永远是自己动手改、用 benchmark 实测、并校验结果一致 —— 别让"优化"变成"改出了 bug 还更快了"。
需要 Python 3.9+,安装 openai 与 python-dotenv;cProfile 与 pstats 为标准库,无需安装。