AI-система рефакторингу коду
Рефакторинг без AI впирається у вартість: senior-розробник розуміє, що потрібно зробити, але 80% роботи — механічне переписування. AI бере цю механіку на себе, залишаючи людині архітектурні рішення та ревю результату.
Види рефакторингу та підходи
Структурний рефакторинг (extract method, move class, rename) — добре піддається автоматизації, висока точність AI.
Патернний рефакторинг (перехід з callbacks на async/await, додавання dependency injection) — потребує розуміння контексту, AI справляється при правильному промпті.
Архітектурний рефакторинг (monolith → microservices, God Object → SRP) — AI генерує план та чорновик, фінальні рішення за людиною.
Система рефакторингу
from anthropic import Anthropic
from pathlib import Path
import subprocess
import ast
from typing import Literal
client = Anthropic()
REFACTORING_TYPES = {
"extract_function": "Видели повторюваний код в окремі функції",
"simplify_conditions": "Спрости складні умовні вирази",
"add_type_hints": "Додай анотації типів всюди де їх немає",
"async_migration": "Перетвори синхронний код у async/await",
"error_handling": "Додай явну обробку помилок",
"dataclass_conversion": "Перетвори dict-based структури у dataclasses/Pydantic",
"dependency_injection": "Додай dependency injection замість глобальних об'єктів",
"remove_duplication": "Усунь дублювання коду (DRY)",
}
class CodeRefactorer:
def refactor(
self,
source_code: str,
refactoring_type: str,
context: str = "",
preserve_interface: bool = True,
) -> dict:
"""Виконує рефакторинг і повертає diff"""
instructions = REFACTORING_TYPES.get(refactoring_type, refactoring_type)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=8096,
system=f"""Ти — senior розробник, який виконує рефакторинг Python коду.
Завдання рефакторингу: {instructions}
Правила:
- {"Зберігай публічний інтерфейс (імена функцій, аргументи, типи повернення)" if preserve_interface else "Інтерфейс можна змінювати"}
- Не змінюй логіку — тільки структуру коду
- Якщо додаєш нові допоміжні функції — розміщуй їх вище основних
- Зберігай всі існуючі коментарі та docstrings
- Додавай коментар # REFACTORED: <короткий опис> де були ключові зміни""",
messages=[{
"role": "user",
"content": f"""Виконай рефакторинг:
```python
{source_code}
{f"Контекст проекту:{chr(10)}{context}" if context else ""}
Поверни:
- Рефакторований код
- Список змін (bullet points)
- Потенційні ризики якщо є
Формат відповіді:
<рефакторований код>
Зміни:
- ...
Ризики:
-
...""" }] )
text = response.content[0].text # Парсимо відповідь refactored_code = "" if "```python" in text: refactored_code = text.split("```python")[1].split("```")[0].strip() changes = [] risks = [] if "**Зміни:**" in text: changes_section = text.split("**Зміни:**")[1].split("**Ризики:**")[0] changes = [line.strip("- ").strip() for line in changes_section.splitlines() if line.strip().startswith("-")] if "**Ризики:**" in text: risks_section = text.split("**Ризики:**")[1] risks = [line.strip("- ").strip() for line in risks_section.splitlines() if line.strip().startswith("-")] return { "original": source_code, "refactored": refactored_code, "changes": changes, "risks": risks, }
### Async migration — приклад реального рефакторингу
```python
def migrate_to_async(source_file: str) -> str:
"""Мігрує синхронний код на async/await"""
source = Path(source_file).read_text()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=8096,
system="""Мігруй Python код з синхронного на async/await.
Правила:
- requests → httpx.AsyncClient
- time.sleep(n) → asyncio.sleep(n)
- threading.Thread → asyncio.create_task
- queue.Queue → asyncio.Queue
- Додай async/await до функцій, які роблять I/O
- Залиш синхронними функції без I/O (чисті обчислення)
- Заміни for loop на asyncio.gather де функції незалежні""",
messages=[{
"role": "user",
"content": f"Мігруй на async/await:\n\n```python\n{source}\n```\n\nПоверни тільки код."
}]
)
text = response.content[0].text
if "```python" in text:
return text.split("```python")[1].split("```")[0].strip()
return text
def add_type_hints(source_file: str) -> str:
"""Додає анотації типів до функцій"""
source = Path(source_file).read_text()
# Знаходимо функції без анотацій
tree = ast.parse(source)
unannotated = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
has_annotations = (
any(arg.annotation for arg in node.args.args) or
node.returns is not None
)
if not has_annotations:
unannotated.append(node.name)
if not unannotated:
return source
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=8096,
messages=[{
"role": "user",
"content": f"""Додай анотації типів Python (PEP 484) до функцій: {', '.join(unannotated)}
Правила:
- Використовуй from __future__ import annotations для forward references
- Для Optional використовуй X | None (Python 3.10+)
- Для колекцій: list[str], dict[str, int], tuple[int, ...]
- Для невідомих типів використовуй Any з typing
```python
{source}
Поверни повний файл з додатими анотаціями.""" }] )
text = response.content[0].text
if "```python" in text:
return text.split("```python")[1].split("```")[0].strip()
return text
### Рефакторинг з тестовою страховкою
```python
def safe_refactor(
source_file: str,
refactoring_type: str,
test_file: str = None,
) -> dict:
"""Виконує рефакторинг тільки якщо тести проходять"""
source = Path(source_file).read_text()
refactorer = CodeRefactorer()
# Спочатку запускаємо існуючі тести
if test_file and Path(test_file).exists():
result = subprocess.run(
["python", "-m", "pytest", test_file, "-v", "--tb=short"],
capture_output=True, text=True
)
if result.returncode != 0:
return {"success": False, "error": "Tests failing before refactoring", "test_output": result.stdout}
# Виконуємо рефакторинг
refactoring = refactorer.refactor(source, refactoring_type)
# Створюємо backup
backup_file = source_file + ".bak"
Path(backup_file).write_text(source)
# Записуємо рефакторований код
Path(source_file).write_text(refactoring["refactored"])
# Запускаємо тести знову
if test_file and Path(test_file).exists():
result = subprocess.run(
["python", "-m", "pytest", test_file, "-v", "--tb=short"],
capture_output=True, text=True
)
if result.returncode != 0:
# Откатуємось
Path(source_file).write_text(source)
return {
"success": False,
"error": "Tests failing after refactoring — rolled back",
"changes": refactoring["changes"],
"test_output": result.stdout,
}
return {
"success": True,
"changes": refactoring["changes"],
"risks": refactoring["risks"],
"backup": backup_file,
}
Практичний кейс: Django-моноліт
Ситуація: Django-проект, 6 років розробки, 45 000 рядків. 3 проблеми: немає type hints (нові розробники витрачають багато часу на розуміння типів), багато дублювання у view-функціях, 12 God Object класів.
Застосовані рефакторинги (за 3 тижні):
-
add_type_hintsдля всіхviews.pyфайлів — автоматично, 2 години -
extract_functionдля view-функцій > 50 рядків — автоматично + ревю, 1 тиждень -
remove_duplicationу сервісному шарі — автоматично, 3 дні
Результати:
- Type annotations coverage: 12% → 87%
- Середня довжина функції: 68 рядків → 23 рядки
- Дублювання коду (SonarQube metric): -43%
- Час онбордингу нового розробника: 3 тижні → 1.5 тижня
Один God Object (OrderService, 1800 рядків) потребував ручного архітектурного рішення — AI запропонував 3 варіанти декомпозиції, розробники вибрали оптимальний.
Строки
- Базовий рефакторинг одного типу для файлу: 1–2 дні
- Система safe_refactor з тестовою страховкою: 3–5 днів
- Batch-рефакторинг всієї кодової бази: 2–3 тижні
- Інтеграція в IDE як команда: 1 тиждень







