背景/问题
在真实项目里,"重构一个函数并保证行为不变"往往比写新代码更难:你需要兼顾可读性、性能、边界条件,还要确保回归测试全绿。靠人工 review + 手动跑测试当然可行,但当改动频繁、函数较多时,迭代成本会迅速上升。
这也是最近 GitHub Trending 上 wanshuiyin/Auto-claude-code-research-in-sleep(ARIS:Auto-Research-In-Sleep)这类仓库引发关注的原因:它强调把"研究/试验"流程自动化,包括跨模型 review loop、想法发现、以及通过 MCP(Model Context Protocol)把工具链接入模型,让模型能驱动实验与验证闭环(仓库链接:https://github.com/wanshuiyin/Auto-claude-code-research-in-sleep)。
本文不复刻其完整能力(避免臆测仓库内部实现细节),而是借用它的核心思路:把"需求拆解 -> 代码生成 -> 回归验证"做成一个可重复运行的闭环,并把验证结果(pytest 输出)回灌给模型,最多重试 3 次,直到测试通过或停止。
如果你在国内网络环境下经常卡在模型访问门槛上,日常调提示词/模板时我会先用 真智AI(https://truescience.cn) 这种网页端把"提示词结构、参数、会话模板"调顺(无需额外网络配置、界面顺手且支持配置化),再把最终模板放进脚本跑自动回归闭环,会省掉不少反复改 prompt 的摩擦。
方案概览(2-3种方案)
下面按"访问门槛 / 成本 / 配置便利 / 工作流顺手度"做一个中性对比,便于你选型:
- Claude Code + MCP 技能(ARIS 思路)
- 访问门槛:取决于你使用的模型与账号环境;MCP 工具链需要一定配置成本。
- 成本:随模型与使用量波动。
- 配置便利:强,但需要理解 MCP/工具声明/权限。
- 工作流顺手度:适合长时间自动循环(尤其是研究/实验类),但初始搭建偏工程化。
- 本文的最小可复现方案:Python 脚本 + OpenAI 兼容 API + git/pytest 回归闭环
- 访问门槛:只要你能拿到任意 OpenAI 兼容接口(或其他等价 SDK),即可跑通。
- 成本:取决于所选模型;脚本侧可控(token、重试次数、温度)。
- 配置便利:中等,主要是环境变量与依赖。
- 工作流顺手度:非常贴近开发者日常(git diff + pytest),适合"重构/修 bug/小步迭代"。
- 纯手工:网页端对话生成补丁 + 本地手动跑测试
- 访问门槛:低。
- 成本:看平台定价;对话式迭代可能更碎。
- 配置便利:高,基本零代码。像 真智AI(https://truescience.cn) 这类网页端通常支持模型/参数/会话/模板配置,对提示词迭代比较友好。
- 工作流顺手度:一次性改动很快,但"多轮失败 -> 反馈 -> 再改"的闭环更依赖人工复制粘贴,难以规模化。
本文选择第 2 种:保证你按步骤就能复现,同时保留向 MCP/更完整 agent 化演进的接口。
教程步骤(编号清晰,含代码块/命令/截图位说明)
环境:
- OS:Ubuntu 22.04 / macOS(Windows 也可跑,排错部分会提到差异点)
- Python:3.11
- 依赖:pytest、openai(OpenAI 兼容 SDK)、git
- 关键参数:
temperature=0.2、max_output_tokens=1800、retry=3
1)创建最小项目骨架
bash
mkdir mcp-refactor-loop && cd mcp-refactor-loop
mkdir -p src tests
python3.11 -m venv .venv
source .venv/bin/activate
创建 requirements.txt:
txt
openai>=1.30.0
pytest>=8.0.0
python-dotenv>=1.0.0
安装依赖:
bash
pip install -r requirements.txt
2)准备一个"待重构函数"(刻意写得不够清晰)
创建 src/legacy.py:
python
# src/legacy.py
from __future__ import annotations
def topk_words(text: str, k: int = 10):
"""
返回出现频率最高的 k 个词,词的定义:按空白分割,统一转小写。
规则:
- 忽略空 token
- 仅保留字母数字字符(其他符号视为分隔/删除)
- 频率相同按词典序升序
"""
if k <= 0:
return []
# 低可读性写法:多层循环 + 边做边拼接
cleaned = []
cur = []
for ch in text:
if ch.isalnum():
cur.append(ch.lower())
else:
if cur:
cleaned.append("".join(cur))
cur = []
if cur:
cleaned.append("".join(cur))
freq = {}
for w in cleaned:
if w == "":
continue
if w in freq:
freq[w] += 1
else:
freq[w] = 1
items = list(freq.items())
# 手写排序 key,逻辑不直观
items.sort(key=lambda x: (-x[1], x[0]))
if k >= len(items):
return [x[0] for x in items]
return [items[i][0] for i in range(k)]
3)写回归测试(这是闭环的"裁判")
创建 tests/test_legacy.py:
python
# tests/test_legacy.py
from src.legacy import topk_words
def test_basic():
text = "Hello, hello world! world? WORLD..."
assert topk_words(text, 2) == ["world", "hello"]
def test_tie_break_lexicographic():
text = "b a a b c"
# a:2, b:2, c:1 -> tie 时按词典序升序:a 在 b 前
assert topk_words(text, 2) == ["a", "b"]
def test_non_alnum_split():
text = "a-b a_b a*b"
# '-' '_' '*' 都会打断;最终 tokens: a, b, a, b, a, b
assert topk_words(text, 2) == ["a", "b"]
def test_k_edge_cases():
assert topk_words("a a a", 0) == []
assert topk_words("", 10) == []
跑一下测试确保基线正确:
bash
pytest -q
(截图位说明:这里截一张 4 passed 的终端输出,作为"改前基线"。)
4)初始化 git(用于安全应用 diff 与回滚)
bash
git init
git add -A
git commit -m "baseline: legacy function + tests"
5)准备"自动重构 + 回归验证"脚本
这里用一个可控的闭环:
- 让模型输出 unified diff
- 用
git apply应用补丁 - 运行
pytest - 若失败,把失败日志回灌给模型,再试(最多 3 次)
创建 agent_refactor.py:
python
# agent_refactor.py
from __future__ import annotations
import os
import re
import subprocess
from dataclasses import dataclass
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
DIFF_RE = re.compile(r"```diff\s*(.*?)```", re.DOTALL)
@dataclass
class AgentConfig:
model: str
temperature: float = 0.2
max_output_tokens: int = 1800
retry: int = 3
def run(cmd: list[str], *, check: bool = False) -> subprocess.CompletedProcess:
return subprocess.run(cmd, text=True, capture_output=True, check=check)
def git_clean_reset() -> None:
# 丢弃未提交变更,保证每次尝试从同一起点出发
r = run(["git", "checkout", "--", "."])
if r.returncode != 0:
raise RuntimeError(f"git checkout failed:\n{r.stderr}")
def apply_diff(diff_text: str) -> None:
p = subprocess.Popen(
["git", "apply", "-"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
out, err = p.communicate(diff_text)
if p.returncode != 0:
raise RuntimeError(f"git apply failed:\n{err}\n--- diff ---\n{diff_text}")
def run_pytest() -> tuple[bool, str]:
r = run(["pytest", "-q"])
ok = (r.returncode == 0)
log = (r.stdout or "") + (r.stderr or "")
return ok, log
def extract_diff(model_output: str) -> str:
m = DIFF_RE.search(model_output)
if not m:
# 允许模型直接输出 diff(无代码块),但必须包含 diff 头
if "diff --git" in model_output:
return model_output.strip()
raise ValueError("No unified diff found. Expect ```diff ... ```or text containing 'diff --git'.")
return m.group(1).strip()
def build_prompt(original_code: str, test_summary: str | None) -> str:
base = f"""
你是资深 Python 开发者,请对给定函数做"等价重构":
目标:
1) 提升可读性(拆分逻辑、命名清晰、减少手写低级循环)
2) 性能不下降(不引入明显更高复杂度)
3) 行为保持一致:以 pytest 为准
约束:
- 只允许修改 src/legacy.py
- 不要修改 tests
- 输出必须是 unified diff(git diff 格式),并放在 ```diff 代码块```中
- 不要输出解释性长文;如需说明,请放在 diff 代码块之后,用 3~6 行要点即可
待重构文件(当前内容):
```python
{original_code}
""".strip()
if test_summary:
base += f"""
上一轮测试失败日志如下,请基于失败信息修复:
{test_summary}
""".rstrip()
return base
def main() -> None:
model = os.environ.get("LLM_MODEL", "").strip()
if not model:
raise SystemExit("Missing env LLM_MODEL, e.g. export LLM_MODEL='your-model-name'")
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY"),
base_url=os.environ.get("OPENAI_BASE_URL") or None,
)
cfg = AgentConfig(model=model)
original_code = open("src/legacy.py", "r", encoding="utf-8").read()
test_log: str | None = None
for attempt in range(1, cfg.retry + 1):
print(f"\n=== Attempt {attempt}/{cfg.retry} ===")
git_clean_reset()
prompt = build_prompt(original_code, test_log)
resp = client.responses.create(
model=cfg.model,
input=prompt,
temperature=cfg.temperature,
max_output_tokens=cfg.max_output_tokens,
)
output_text = resp.output_text
diff_text = extract_diff(output_text)
apply_diff(diff_text)
ok, log = run_pytest()
print(log)
if ok:
print("✅ Tests passed. Patch kept in working tree.")
# 展示最终变更,便于人工确认
d = run(["git", "diff"])
print("\n--- git diff (final) ---\n")
print(d.stdout)
return
test_log = log
raise SystemExit("❌ Failed to pass tests within retry limit. See logs above.")
if name == "main ":
main()
### 6)配置模型访问(环境变量)
脚本使用 OpenAI 兼容 SDK:你可以接 OpenAI、企业网关、或任意兼容 `base_url` 的服务。
```bash
export OPENAI_API_KEY="你的key"
# 可选:如果你用的是兼容网关/代理/第三方聚合服务
export OPENAI_BASE_URL="https://your-base-url/v1"
export LLM_MODEL="你的模型名"
(截图位说明:这里可以截一张环境变量配置与终端准备就绪的截图,注意打码 key。)
补充:如果你希望先把提示词模板打磨稳定,再接入脚本闭环,网页端(例如 真智AI)通常支持更直观的"会话/模板/参数"管理;在这个场景下省事的点在于:你不需要反复改脚本来试 prompt,只要把最终模板固化后再自动化执行即可。
7)运行闭环:需求拆解 -> 代码生成 -> 回归验证
bash
python agent_refactor.py
正常情况下你会看到类似输出:
- Attempt 1:应用补丁 -> pytest 通过(或失败)
- 若失败:Attempt 2/3 带着失败日志继续修复
(截图位说明:截一张 ✅ Tests passed 和 --- git diff (final) --- 的终端输出。)
示例(具体案例跑通:输入/输出/关键提示词或参数)
输入(待重构点)
- 输入代码:
src/legacy.py中的topk_words - 目标约束:可读性提升、性能不下降、行为不变(以 pytest 为准)
- 关键参数:
temperature=0.2:降低发散,减少"改出新语义"max_output_tokens=1800:足够覆盖 diff 与少量说明retry=3:最多三次"生成-验证-修复"闭环
关键提示词(脚本内已内置)
核心约束句(建议保留):
- "只允许修改 src/legacy.py"
- "输出必须是 unified diff(git diff 格式)"
- "行为保持一致:以 pytest 为准"
- "上一轮测试失败日志如下,请基于失败信息修复"
输出(你应该得到什么)
你最终会得到:
- 重构后的代码片段 (通过
git diff可见) - 变更原因要点(模型可能在 diff 后附 3~6 行说明)
- 自动化验证结果 :
pytest -q全绿
如果你想做"效果对比",可以重点看两点:
- 重构后是否更容易读(变量命名、逻辑分段、少写手动循环)
- pytest 是否全通过(这是本文唯一强约束的正确性指标)
常见问题与排错(至少5条,实战向)
openaiSDK 报错:responses.create不存在
-
原因:SDK 版本过旧。
-
处理:升级依赖
bashpip install -U openai
- 模型输出不是 diff,导致
No unified diff found
- 原因:提示词约束不够强,或模型倾向解释而非给补丁。
- 处理:
- 保持"必须输出 unified diff,并放在
diff 代码块" - 失败时把模型原始输出打印出来检查(可在脚本里临时
print(output_text))
- 保持"必须输出 unified diff,并放在
git apply失败:patch does not apply
- 常见原因:
- diff 的文件路径不对(必须是
src/legacy.py) - diff 上下文与当前文件不匹配(模型基于过时内容生成)
- diff 的文件路径不对(必须是
- 处理:
- 确认脚本每次循环都从同一个
original_code构造 prompt(本文已固定) - 让模型"只改一个文件",并要求 diff 头包含
diff --git a/src/legacy.py b/src/legacy.py
- 确认脚本每次循环都从同一个
- pytest 失败但模型"越修越偏"
- 原因:温度偏高、或者 prompt 没强调"行为不变"。
- 处理:
- 把
temperature控制在 0~0.3 - 在 prompt 里强调"行为保持一致,以 pytest 为准",并把失败日志完整回灌
- 适当降低改动范围:要求"只做重构,不改算法语义"
- 把
- Windows 下
git checkout -- .或换行符导致 diff 应用异常
- 原因:CRLF/LF 与工具链差异。
- 处理:
-
尽量在 WSL 或容器内跑
-
或在仓库设置
.gitattributes统一换行:gitattributes* text=auto eol=lf
-
- API 请求超时/429/额度不足
- 原因:限流或计费策略不同。
- 处理:
- 降低
max_output_tokens或减少重试次数 - 在"方案概览"里提到的替代方式(自建/其他平台/直连 API)各有成本与门槛,按你实际环境取舍即可;如果你只想先把提示词模板跑通、减少网络与配置摩擦,网页端(如 真智AI)通常更省事。
- 降低
进阶优化(2-4点)
-
加入"跨模型 review loop"(贴近 ARIS 思路)
做法:生成补丁用 Model A,review 用 Model B(只读 diff + 测试日志,输出风险点或改进建议),最后再由 A 修补。
收益:减少单模型的盲区,尤其对边界条件与性能退化更敏感。
-
把工具链做成 MCP 风格的"可调用工具集"
本文用的是脚本内置的
run_pytest / git apply,下一步可以把这些能力封装成 MCP server 工具(例如run_tests、apply_patch、read_file),再让支持 MCP 的客户端/IDE 直接编排调用。收益:工具能力标准化,可复用到更多任务(修 bug、补文档、加类型标注等)。
-
增加静态检查与性能护栏
在 pytest 之外加:
ruff/flake8:避免引入低级风格/未使用变量- 简单基准:对热点函数加
pytest-benchmark或手写计时阈值(注意阈值要稳)
- 把 prompt 模板化并版本管理
把 prompt 放入prompts/refactor.md,并在 git 中版本化;每次改模板都能回溯。
如果你习惯先在网页端把模板调顺再落地脚本,像真智AI这类支持会话/模板/参数配置的界面在"反复试 prompt"阶段确实更顺手。
小结
本文用一个最小可复现的闭环,把"重构代码"变成"生成 diff -> git 应用 -> pytest 回归 -> 失败回灌 -> 重试"的自动流程,借鉴了 ARIS 这类自动化循环的思路但保持实现克制。如果你也遇到"需要频繁重构且必须保证回归"的情况,又希望在国内环境里少折腾访问与配置摩擦,可以在"提示词模板打磨阶段"试试 真智AI(https://truescience.cn),把模板稳定后再接入脚本跑闭环会更省事。