AI-генерация документации для кода
Документация устаревает на следующий день после написания — это константа разработки. AI-генерация документации решает проблему иначе: не "писать раз и поддерживать", а "регенерировать при каждом изменении". Docstrings, API-документация, README-файлы, архитектурные описания — всё это можно автоматизировать с качеством, которое превышает среднее по команде.
Docstring генератор
from anthropic import Anthropic
import ast
from pathlib import Path
client = Anthropic()
DOCSTRING_SYSTEM = """Ты — технический писатель, специализирующийся на Python документации.
Пиши docstrings в формате Google Style:
```python
def function(param: type) -> type:
\"\"\"Краткое описание в одну строку (повелительное наклонение).
Детальное описание при необходимости. Объясняй ЧТО делает функция,
не КАК. Упоминай нетривиальные алгоритмы или важные side effects.
Args:
param: Описание параметра. Не указывай тип — он есть в аннотации.
Returns:
Что возвращает. Не указывай тип.
Raises:
ValueError: Когда и почему возникает.
Example:
>>> result = function(value)
>>> assert result == expected
\"\"\"
Правила:
- Краткая строка: повелительное наклонение (Calculates/Returns/Creates, не Calculate/Return)
- Не повторяй имя функции и типы из аннотаций
- Добавляй Example только для нетривиальных функций
- Для простых getter/setter — минимальная документация"""
def generate_docstring(function_source: str) -> str: """Генерирует docstring для функции"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=DOCSTRING_SYSTEM,
messages=[{
"role": "user",
"content": f"Напиши docstring для этой функции. Верни только docstring (тройные кавычки), без кода функции:\n\n```python\n{function_source}\n```"
}]
)
return response.content[0].text.strip()
def add_docstrings_to_file(file_path: str, overwrite: bool = False) -> str: """Добавляет docstrings ко всем функциям в файле""" source = Path(file_path).read_text() tree = ast.parse(source) lines = source.splitlines()
# Собираем функции без docstrings
functions_to_document = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
has_docstring = (
node.body and
isinstance(node.body[0], ast.Expr) and
isinstance(node.body[0].value, ast.Constant) and
isinstance(node.body[0].value.value, str)
)
if not has_docstring or overwrite:
func_source = ast.unparse(node)
functions_to_document.append({
"name": node.name,
"line": node.lineno,
"source": func_source,
"indent": len(lines[node.lineno - 1]) - len(lines[node.lineno - 1].lstrip()),
})
# Генерируем docstrings
insertions = {} # line_number -> docstring_text
for func_info in functions_to_document:
docstring = generate_docstring(func_info["source"])
# Индентируем docstring
indent = " " * (func_info["indent"] + 4)
docstring_lines = docstring.strip().splitlines()
indented = "\n".join(indent + line if line.strip() else line for line in docstring_lines)
# Строка после def ... :
insertions[func_info["line"]] = indented
# Вставляем docstrings в исходный код
result_lines = []
for i, line in enumerate(lines, 1):
result_lines.append(line)
if i in insertions:
result_lines.append(insertions[i])
return "\n".join(result_lines)
### API документация (OpenAPI/Swagger)
```python
from pydantic import BaseModel
import json
def generate_api_docs(router_source: str, existing_models: str = "") -> str:
"""Генерирует OpenAPI-совместимую документацию для FastAPI роутера"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
messages=[{
"role": "user",
"content": f"""Проанализируй FastAPI роутер и добавь полную OpenAPI документацию:
- summary и description для каждого endpoint
- description для всех параметров (Path, Query, Body)
- response_model с примерами
- HTTPException с кодами и описаниями
FastAPI роутер:
```python
{router_source}
{f"Существующие модели:{chr(10)}python{chr(10)}{existing_models}{chr(10)}" if existing_models else ""}
Верни улучшенный роутер с полной документацией.""" }] )
return response.content[0].text
def generate_readme_section(module_path: str) -> str: """Генерирует README секцию для Python модуля""" source = Path(module_path).read_text()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
messages=[{
"role": "user",
"content": f"""Напиши README секцию для этого Python модуля.
Структура:
Название модуля
Краткое описание (1-2 предложения).
Использование
# Пример кода
API
Таблица публичных функций/классов с описанием.
Требования
Зависимости если есть.
Код модуля:
{source[:3000]}
```"""
}]
)
return response.content[0].text
Архитектурная документация
def generate_architecture_doc(project_root: str) -> str:
"""Генерирует архитектурное описание проекта"""
from pathlib import Path
import os
# Собираем структуру проекта
structure = []
for root, dirs, files in os.walk(project_root):
# Пропускаем vendor, cache
dirs[:] = [d for d in dirs if d not in {
".git", "__pycache__", "node_modules", ".venv", "migrations"
}]
level = root.replace(project_root, "").count(os.sep)
indent = " " * 2 * level
structure.append(f"{indent}{os.path.basename(root)}/")
for file in files:
if file.endswith((".py", ".ts", ".tsx")):
structure.append(f"{indent} {file}")
# Читаем ключевые файлы
key_files_content = {}
key_patterns = ["models.py", "urls.py", "routes.ts", "app.py", "main.py"]
for root, _, files in os.walk(project_root):
for file in files:
if file in key_patterns:
file_path = os.path.join(root, file)
try:
key_files_content[file_path] = Path(file_path).read_text()[:1500]
except Exception:
pass
if len(key_files_content) >= 5:
break
key_content = "\n\n".join([
f"### {path}\n```python\n{content}\n```"
for path, content in key_files_content.items()
])
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
messages=[{
"role": "user",
"content": f"""Напиши архитектурную документацию для проекта.
Структура проекта:
{chr(10).join(structure[:100])}
Ключевые файлы:
{key_content}
Включи:
1. Обзор архитектуры (2-3 абзаца)
2. Основные компоненты и их роли
3. Схема взаимодействия (текстовая)
4. Ключевые паттерны, используемые в коде
5. Точки входа и запуск
Формат: Markdown для README."""
}]
)
return response.content[0].text
Автообновление документации в CI/CD
# .github/workflows/docs.yml
name: Update Documentation
on:
push:
branches: [main]
paths:
- "src/**/*.py"
- "app/**/*.py"
jobs:
update-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Generate docstrings
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
python scripts/generate_docs.py \
--source src/ \
--check-missing-only \
--output-report docs/coverage.json
- name: Update API docs
run: |
python scripts/generate_api_docs.py \
--routers app/api/ \
--output docs/api/
- name: Commit if changed
run: |
git config user.email "[email protected]"
git config user.name "Docs Bot"
git add docs/
git diff --staged --quiet || git commit -m "docs: auto-update documentation"
git push
Практический кейс: микросервис с 0% документации
Задача: Python FastAPI сервис, 4200 строк, 67 endpoints, 0 docstrings. Онбординг нового разработчика занимал 3 недели.
Автоматизация:
- Batch-генерация docstrings для всех функций (182 функции, 45 мин)
- Генерация OpenAPI descriptions для всех endpoints
- Архитектурный README с описанием компонентов
Результаты:
- Docstring coverage: 0% → 91%
- Время онбординга нового разработчика: 3 недели → 1 неделя
- Вопросы в Slack "как работает X": -68%
- Качество оценка документации командой: 4.1/5.0
Нюанс: для 8% функций с нетривиальной бизнес-логикой AI-генерированная документация была неточной — потребовала ручной правки. Система помечает такие функции (cyclomatic complexity > 10) для ревью.
Docstring coverage как метрика CI
def check_docstring_coverage(source_dir: str, threshold: float = 0.8) -> bool:
"""Проверяет coverage docstrings, возвращает False если ниже порога"""
total = 0
documented = 0
for py_file in Path(source_dir).rglob("*.py"):
source = py_file.read_text()
try:
tree = ast.parse(source)
except SyntaxError:
continue
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if not node.name.startswith("_"): # Публичные функции
total += 1
if (node.body and isinstance(node.body[0], ast.Expr) and
isinstance(node.body[0].value, ast.Constant)):
documented += 1
coverage = documented / total if total > 0 else 1.0
print(f"Docstring coverage: {coverage:.1%} ({documented}/{total})")
return coverage >= threshold
Сроки
- Docstring генератор для существующей кодовой базы: 2–3 дня
- OpenAPI документация для FastAPI/Django REST: 3–5 дней
- Автоматическое обновление в CI/CD: 1 неделя
- Архитектурная документация + wiki: 1–2 недели







