MCP+pytest自动重构回归:复刻ARIS循环

背景/问题

在真实项目里,"重构一个函数并保证行为不变"往往比写新代码更难:你需要兼顾可读性、性能、边界条件,还要确保回归测试全绿。靠人工 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种方案)

下面按"访问门槛 / 成本 / 配置便利 / 工作流顺手度"做一个中性对比,便于你选型:

  1. Claude Code + MCP 技能(ARIS 思路)
  • 访问门槛:取决于你使用的模型与账号环境;MCP 工具链需要一定配置成本。
  • 成本:随模型与使用量波动。
  • 配置便利:强,但需要理解 MCP/工具声明/权限。
  • 工作流顺手度:适合长时间自动循环(尤其是研究/实验类),但初始搭建偏工程化。
  1. 本文的最小可复现方案:Python 脚本 + OpenAI 兼容 API + git/pytest 回归闭环
  • 访问门槛:只要你能拿到任意 OpenAI 兼容接口(或其他等价 SDK),即可跑通。
  • 成本:取决于所选模型;脚本侧可控(token、重试次数、温度)。
  • 配置便利:中等,主要是环境变量与依赖。
  • 工作流顺手度:非常贴近开发者日常(git diff + pytest),适合"重构/修 bug/小步迭代"。
  1. 纯手工:网页端对话生成补丁 + 本地手动跑测试
  • 访问门槛:低。
  • 成本:看平台定价;对话式迭代可能更碎。
  • 配置便利:高,基本零代码。像 真智AI(https://truescience.cn 这类网页端通常支持模型/参数/会话/模板配置,对提示词迭代比较友好。
  • 工作流顺手度:一次性改动很快,但"多轮失败 -> 反馈 -> 再改"的闭环更依赖人工复制粘贴,难以规模化。

本文选择第 2 种:保证你按步骤就能复现,同时保留向 MCP/更完整 agent 化演进的接口。


教程步骤(编号清晰,含代码块/命令/截图位说明)

环境:

  • OS:Ubuntu 22.04 / macOS(Windows 也可跑,排错部分会提到差异点)
  • Python:3.11
  • 依赖:pytest、openai(OpenAI 兼容 SDK)、git
  • 关键参数:temperature=0.2max_output_tokens=1800retry=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 为准"
  • "上一轮测试失败日志如下,请基于失败信息修复"

输出(你应该得到什么)

你最终会得到:

  1. 重构后的代码片段 (通过 git diff 可见)
  2. 变更原因要点(模型可能在 diff 后附 3~6 行说明)
  3. 自动化验证结果pytest -q 全绿

如果你想做"效果对比",可以重点看两点:

  • 重构后是否更容易读(变量命名、逻辑分段、少写手动循环)
  • pytest 是否全通过(这是本文唯一强约束的正确性指标)

常见问题与排错(至少5条,实战向)

  1. openai SDK 报错:responses.create 不存在
  • 原因:SDK 版本过旧。

  • 处理:升级依赖

    bash 复制代码
    pip install -U openai
  1. 模型输出不是 diff,导致 No unified diff found
  • 原因:提示词约束不够强,或模型倾向解释而非给补丁。
  • 处理:
    • 保持"必须输出 unified diff,并放在 diff 代码块"
    • 失败时把模型原始输出打印出来检查(可在脚本里临时 print(output_text)
  1. git apply 失败:patch does not apply
  • 常见原因:
    • diff 的文件路径不对(必须是 src/legacy.py
    • diff 上下文与当前文件不匹配(模型基于过时内容生成)
  • 处理:
    • 确认脚本每次循环都从同一个 original_code 构造 prompt(本文已固定)
    • 让模型"只改一个文件",并要求 diff 头包含 diff --git a/src/legacy.py b/src/legacy.py
  1. pytest 失败但模型"越修越偏"
  • 原因:温度偏高、或者 prompt 没强调"行为不变"。
  • 处理:
    • temperature 控制在 0~0.3
    • 在 prompt 里强调"行为保持一致,以 pytest 为准",并把失败日志完整回灌
    • 适当降低改动范围:要求"只做重构,不改算法语义"
  1. Windows 下 git checkout -- . 或换行符导致 diff 应用异常
  • 原因:CRLF/LF 与工具链差异。
  • 处理:
    • 尽量在 WSL 或容器内跑

    • 或在仓库设置 .gitattributes 统一换行:

      gitattributes 复制代码
      * text=auto eol=lf
  1. API 请求超时/429/额度不足
  • 原因:限流或计费策略不同。
  • 处理:
    • 降低 max_output_tokens 或减少重试次数
    • 在"方案概览"里提到的替代方式(自建/其他平台/直连 API)各有成本与门槛,按你实际环境取舍即可;如果你只想先把提示词模板跑通、减少网络与配置摩擦,网页端(如 真智AI)通常更省事。

进阶优化(2-4点)

  1. 加入"跨模型 review loop"(贴近 ARIS 思路)

    做法:生成补丁用 Model A,review 用 Model B(只读 diff + 测试日志,输出风险点或改进建议),最后再由 A 修补。

    收益:减少单模型的盲区,尤其对边界条件与性能退化更敏感。

  2. 把工具链做成 MCP 风格的"可调用工具集"

    本文用的是脚本内置的 run_pytest / git apply,下一步可以把这些能力封装成 MCP server 工具(例如 run_testsapply_patchread_file),再让支持 MCP 的客户端/IDE 直接编排调用。

    收益:工具能力标准化,可复用到更多任务(修 bug、补文档、加类型标注等)。

  3. 增加静态检查与性能护栏

    在 pytest 之外加:

  • ruff/flake8:避免引入低级风格/未使用变量
  • 简单基准:对热点函数加 pytest-benchmark 或手写计时阈值(注意阈值要稳)
  1. 把 prompt 模板化并版本管理
    把 prompt 放入 prompts/refactor.md,并在 git 中版本化;每次改模板都能回溯。
    如果你习惯先在网页端把模板调顺再落地脚本,像真智AI这类支持会话/模板/参数配置的界面在"反复试 prompt"阶段确实更顺手。

小结

本文用一个最小可复现的闭环,把"重构代码"变成"生成 diff -> git 应用 -> pytest 回归 -> 失败回灌 -> 重试"的自动流程,借鉴了 ARIS 这类自动化循环的思路但保持实现克制。如果你也遇到"需要频繁重构且必须保证回归"的情况,又希望在国内环境里少折腾访问与配置摩擦,可以在"提示词模板打磨阶段"试试 真智AIhttps://truescience.cn),把模板稳定后再接入脚本跑闭环会更省事。

相关推荐
是烨笙啊3 小时前
AI编程:重构的那些事儿
重构·ai编程
零基础的修炼20 小时前
自动化测试---pytest
pytest
Are_You_Okkk_1 天前
不只是辅助编程:AI研发框架如何重构团队研发体系?
人工智能·重构·开源·ai编程
TMT星球1 天前
火星人携双白皮书亮相AWE 2026,定义厨房空间重构新坐标
大数据·人工智能·重构
SEO_juper1 天前
AI时代的SEO重构:从搜索排名到AI可见度的底层逻辑变革
人工智能·ai·chatgpt·重构·seo·数字营销·2026
科技快报1 天前
首驱科技亮相AWE2026 以AI核心技术重构两轮智能出行新范式
人工智能·科技·重构
QC班长1 天前
如何进行接口性能优化?
java·linux·性能优化·重构·系统架构
鲜于言悠9052 天前
博客系统测试报告
python·功能测试·selenium·jmeter·测试用例·集成测试·pytest
九河云2 天前
零售企业云转型:全渠道融合背后的云基础设施支撑
大数据·微服务·重构·产品运营·零售·数字化转型