Módulo 2: Code Review Automático en PRs
Manejar PRs Grandes con Chunking
Manejar PRs Grandes con Chunking
Descripción
Tu bot funciona perfecto en PRs típicos (5-20 archivos). Pero tarde o temprano llega el PR grande: 50, 80, 200 archivos. ¿Qué hace el bot ahora?
Tres opciones malas:
- Pasar todo al modelo → context window saturado, calidad degrada, costos explotan
- Truncar arbitrariamente → review parcial sin transparencia, los issues importantes quedan afuera
- Skipear silenciosamente → el equipo no recibe ningún feedback, justo cuando más lo necesita
Esta cápsula cubre el camino correcto: chunking inteligente. Vas a aprender a partir el PR en chunks que sí caben, priorizar qué chunks revisar, agregar resultados de múltiples chunks en un único review coherente, y comunicar transparentemente cuando un PR excede los límites.
Al terminar, tu bot va a manejar PRs de cualquier tamaño con calidad consistente — o decir explícitamente "este PR es demasiado grande para review automático" cuando corresponda.
Cuándo Chunkear
Definir umbrales claros antes de implementar:
PR CHICO (default review en 1 pasada):
- ≤20 archivos relevantes
- ≤2,000 líneas de diff
- Costo estimado: $0.01-0.03
PR MEDIANO (review chunkeado):
- 21-50 archivos relevantes
- 2,001-8,000 líneas de diff
- Costo estimado: $0.05-0.15
- Estrategia: chunking por archivo o por feature
PR GRANDE (review parcial + warning):
- 51-100 archivos relevantes
- 8,001-20,000 líneas de diff
- Costo estimado: $0.20-0.50
- Estrategia: chunking + priorización agresiva, marcar como
"review parcial" en el summary
PR DEMASIADO GRANDE (skipear con mensaje):
- >100 archivos relevantes
- >20,000 líneas de diff
- Estrategia: postear comment explicando, sugerir dividir el PR
Los umbrales son ajustables según el modelo y presupuesto. Con Haiku, puedes ser más permisivo; con Sonnet/Opus, más conservador.
Estrategia 1: Chunking por Archivo
La más simple: revisar un archivo por llamada al modelo.
"""Review por archivo."""
import json
from anthropic import Anthropic
client = Anthropic()
def review_single_file(file_data: dict, conventions: str) -> dict:
"""Review de un solo archivo. Retorna {summary, comments}."""
patch = file_data.get("patch", "")
if not patch:
return {"summary": "", "comments": []}
prompt = f"""Review este archivo del PR.
CONVENCIONES:
{conventions}
ARCHIVO: {file_data['filename']}
DIFF:
{patch}
Devuelve JSON con:
{{
"summary": "1 oración con verdict del archivo",
"comments": [
{{"line": N, "severity": "critical|warning|suggestion", "body": "..."}}
]
}}
"""
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text.strip()
# Limpiar code fences
if text.startswith("```"):
text = "\n".join(text.split("\n")[1:-1])
try:
return json.loads(text)
except json.JSONDecodeError:
return {"summary": "Error parsing review", "comments": []}
# Review todos los archivos relevantes
all_results = []
for f in relevant_files:
result = review_single_file(f, conventions)
# Agregar path a cada comment
for c in result["comments"]:
c["path"] = f["filename"]
all_results.append({
"filename": f["filename"],
"summary": result["summary"],
"comments": result["comments"],
})
Ventajas:
- Cada llamada es chica → context window holgado, calidad alta
- Paralelizable (con cuidado de rate limits)
- Costos predecibles (linear con archivos)
Desventajas:
- El modelo no ve cómo los archivos se relacionan entre sí
- Issues cross-file (ej. cambio en un archivo rompe asunción en otro) se pierden
- Más llamadas API = más latencia (a menos que paralelices)
Estrategia 2: Chunking por Feature
Para PRs medianos donde los archivos están relacionados, agrupar por feature:
def group_by_feature(files: list) -> dict:
"""Agrupar archivos por feature inferido del path."""
groups = {}
for f in files:
# Heurística: primer dir significativo
parts = f["filename"].split("/")
if len(parts) > 2:
feature = f"{parts[0]}/{parts[1]}" # ej. "src/payments"
else:
feature = parts[0]
groups.setdefault(feature, []).append(f)
return groups
groups = group_by_feature(relevant_files)
for feature, files_in_feature in groups.items():
print(f"Reviewing feature: {feature} ({len(files_in_feature)} archivos)")
review = review_feature(files_in_feature, conventions, feature)
all_results.append(review)
Función review_feature: combina los patches de los archivos del grupo y los pasa juntos. El modelo ve el contexto completo del feature.
Cuándo usar: PRs que tocan varias áreas distintas. Cada área se revisa por separado, cada una con sus archivos relacionados juntos.
Estrategia 3: Priorización + Top N
Para PRs grandes, no puedes revisar todos los archivos. Prioriza los más importantes:
def prioritize_files(files: list, max_files: int = 30) -> list:
"""Devolver los top N archivos más relevantes."""
def priority_score(f):
score = 0
# Más cambios = más relevante
score += f["changes"] * 1
# Archivos críticos pesan más
critical_paths = ["payments", "auth", "security", "database"]
if any(p in f["filename"] for p in critical_paths):
score += 100
# Archivos nuevos pesan más (más superficie de bugs nuevos)
if f["status"] == "added":
score += 50
# Tests pesan menos (es una decisión opinable)
if "test" in f["filename"]:
score *= 0.5
return score
sorted_files = sorted(files, key=priority_score, reverse=True)
return sorted_files[:max_files]
prioritized = prioritize_files(relevant_files, max_files=30)
Heurísticas comunes para priority_score:
- Cantidad de cambios (líneas modificadas)
- Path crítico (auth, payments, security, etc.)
- Status (
added>modified>renamed) - Tipo de archivo (código fuente > tests > config)
- Tamaño del archivo (cambios chicos en archivos grandes son riesgosos)
Agregando Resultados Multi-Chunk
Después de revisar todos los chunks, necesitas un único review publicado en el PR (no varios).
def aggregate_reviews(per_chunk_results: list) -> dict:
"""Combinar reviews de chunks en uno solo."""
# Aplanar todos los comments
all_comments = []
for r in per_chunk_results:
all_comments.extend(r["comments"])
# Conteo por severidad
by_severity = {"critical": 0, "warning": 0, "suggestion": 0}
for c in all_comments:
by_severity[c.get("severity", "suggestion")] += 1
# Summary agregado: combinar summaries por feature
feature_summaries = "\n".join(
f"- **{r['filename']}**: {r['summary']}"
for r in per_chunk_results
if r.get("summary")
)
overall = f"""# 🤖 Code Review (Claude Code)
Este PR fue revisado en **{len(per_chunk_results)} chunks** debido al tamaño.
## Resumen por área
{feature_summaries}
## Severidad total
- 🚨 Critical: {by_severity['critical']}
- ⚠️ Warning: {by_severity['warning']}
- 💡 Suggestion: {by_severity['suggestion']}
Detalles inline en cada archivo.
"""
return {
"summary": overall,
"comments": all_comments,
}
aggregated = aggregate_reviews(all_results)
Ahora aggregated tiene la estructura del review API y se puede publicar como un único review.
Caso Extremo: PR Demasiado Grande
Cuando el PR excede los umbrales (>100 archivos, >20K líneas), revisarlo automáticamente no aporta valor proporcional al costo. Mejor: comunicar transparentemente.
def post_too_large_message(pr_number, repo, token, file_count, line_count):
"""Postear comment al PR explicando por qué no se revisa."""
body = f"""# 🤖 Code Review
Este PR tiene **{file_count} archivos** y **{line_count} líneas** modificadas — excede los límites para review automático.
## Sugerencias
1. **Considera dividir el PR** en cambios más chicos. PRs grandes son difíciles de revisar (humano y automatizado).
2. Si el PR es necesariamente grande (ej. migración masiva), pide review manual a un humano del equipo.
3. Si quieres review parcial automatizado de archivos críticos, agrega la label `review-critical-only`.
## Para activar review parcial
Agrega label `review-critical-only` y los archivos en paths críticos (auth, payments, etc.) se revisarán automáticamente.
"""
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}
requests.post(url, headers=headers, json={"body": body})
Resultado: transparencia. El equipo sabe por qué no recibió review automático y tiene opciones (dividir el PR, review manual, label para review parcial).
Implementación Integrada
Combinando todo en el script principal:
"""Script principal: review con chunking inteligente."""
import os
import requests
# 1. Obtener archivos del PR
files = get_pr_files() # función de cápsula 02
relevant_files = filter_relevant(files)
file_count = len(relevant_files)
total_changes = sum(f["changes"] for f in relevant_files)
# 2. Decidir estrategia según tamaño
if file_count == 0:
print("No hay archivos relevantes.")
sys.exit(0)
elif file_count <= 20 and total_changes <= 2000:
# PR chico: review en 1 pasada
print(f"PR chico ({file_count} archivos). Review único.")
review = review_combined(relevant_files, conventions)
elif file_count <= 50 and total_changes <= 8000:
# PR mediano: chunking por feature
print(f"PR mediano ({file_count} archivos). Chunking por feature.")
groups = group_by_feature(relevant_files)
per_chunk_results = [review_feature(files_g, conventions, name)
for name, files_g in groups.items()]
review = aggregate_reviews(per_chunk_results)
elif file_count <= 100 and total_changes <= 20000:
# PR grande: priorización + top 30
print(f"PR grande ({file_count} archivos). Priorización + top 30.")
prioritized = prioritize_files(relevant_files, max_files=30)
per_chunk_results = [review_single_file(f, conventions) for f in prioritized]
review = aggregate_reviews(per_chunk_results)
# Marcar como review parcial
review["summary"] = "**⚠️ Review parcial** — solo top 30 archivos.\n\n" + review["summary"]
else:
# PR demasiado grande
print(f"PR demasiado grande ({file_count} archivos, {total_changes} líneas).")
post_too_large_message(pr_number, repo, github_token, file_count, total_changes)
sys.exit(0)
# 3. Validar y publicar el review
valid_comments = validate_all(review["comments"], files)
publish_review(pr_number, repo, github_token, commit_sha, review["summary"], valid_comments)
Resultado: un script que escala gracefully de PRs chicos a gigantes, con feedback apropiado en cada caso.
Optimizaciones Adicionales
Paralelización con cuidado
Si revisas archivos en paralelo, respetar rate limits de Anthropic:
import concurrent.futures
from anthropic import Anthropic
client = Anthropic()
def review_file_safe(file_data, conventions):
"""Wrapper con retry."""
for attempt in range(3):
try:
return review_single_file(file_data, conventions)
except APIError as e:
if e.status_code == 429: # rate limit
time.sleep(2 ** attempt) # exponential backoff
else:
raise
return {"summary": "Failed after retries", "comments": []}
# Paralelizar con cuidado: max 5 concurrent
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(
lambda f: review_file_safe(f, conventions),
prioritized,
))
Beneficio: review de 30 archivos pasa de ~5 minutos a ~1 minuto. Riesgo: si te pasas del rate limit, todo falla.
Cache de resultados
Si el PR no cambió en algunas líneas pero hubo push, puedes cachear reviews previos por commit SHA + archivo. No es trivial pero ahorra llamadas en re-ejecuciones.
Trampas Comunes
Error 1: Pasar el PR completo siempre
Síntoma: En PRs grandes, calidad de review degrada visiblemente. El bot da comments genéricos o se contradice.
Por qué pasa: Context window saturado.
Cómo corregir: Implementar umbrales y chunking. La regla del 70% del context window aplica acá.
Error 2: Chunking sin agregación
Síntoma: El bot postea N reviews separados (uno por chunk) en el mismo PR.
Por qué pasa: El script publica cada chunk inmediatamente en lugar de acumular y agregar.
Cómo corregir: Acumular resultados y publicar un único review al final con aggregate_reviews.
Error 3: Priorización opaca
Síntoma: El bot revisó 30 archivos. ¿Cuáles 30 de los 80? El equipo no sabe.
Por qué pasa: El summary no comunica qué se revisó vs qué no.
Cómo corregir: En el summary del review parcial, listar explícitamente: "Se revisaron 30 archivos prioritarios. No se revisó: [lista de los 50 restantes con razón breve]".
Error 4: No respetar rate limits al paralelizar
Síntoma: Reviews fallan con 429 cuando el PR es grande.
Por qué pasa: ThreadPoolExecutor con max_workers=20 lanza 20 llamadas simultáneas. Anthropic rate-limits.
Cómo corregir: max_workers=5 típicamente seguro. Con backoff exponencial en caso de 429.
Error 5: Skipear silenciosamente
Síntoma: El bot no comenta en PRs grandes. El equipo no sabe si es bug o intencional.
Por qué pasa: El script hace sys.exit(0) sin comentar.
Cómo corregir: Siempre postear un comment explicando por qué no se revisó. Transparencia sobre silencio.
Diagnóstico
Pregunta 1: ¿Tienes umbrales claros para "PR chico/mediano/grande/demasiado grande"?
Si no, vas a tomar decisiones inconsistentes. Definir umbrales basados en tu modelo y presupuesto.
Pregunta 2: ¿Tu chunking publica un review único o varios separados?
Varios separados es ruido. Aggregar y publicar uno solo es la práctica.
Pregunta 3: Cuando haces priorización, ¿comunicas qué archivos se omitieron?
Sin comunicarlo, el equipo no sabe qué tan completa es la review.
Pregunta 4: ¿Paralelizas reviews respetando rate limits?
Si paralelizas sin límite, vas a chocarte con 429s. max_workers=5 + backoff es razonable.
Pregunta 5: Cuando un PR es demasiado grande, ¿posteas un mensaje explicativo o skipeas silenciosamente?
Silencio = mala UX. Mensaje explicativo + sugerencias = profesional.
Ejercicios
Ejercicio 1: Implementar umbrales (Fácil)
Define los 4 umbrales (chico, mediano, grande, demasiado grande) en tu script con valores ajustados a tu uso. Documéntalos en CLAUDE.md o el README del workflow.
Ejercicio 2: Chunking por archivo con agregación (Medio)
Implementa:
- Función
review_single_fileque revisa un archivo - Loop sobre archivos prioritizados
- Función
aggregate_reviewsque combina resultados - Publicación de un único review al final
Ver checklist
- Cada archivo se revisa en una llamada API separada
- Cada comment tiene
pathcorrecto - El summary agregado lista resultados por archivo/feature
- Solo se publica un review (no N)
Ejercicio 3: Priorización con score function (Difícil)
Implementa priority_score que considera:
- Cantidad de cambios
- Path crítico (basado en lista configurable)
- Status del archivo (
added>modified) - Tamaño absoluto del archivo
Verifica con un PR de prueba que los archivos críticos quedan en el top.
Resumen
- Umbrales claros evitan decisiones ad-hoc en PRs de tamaño variable
- 4 estrategias según tamaño: review único, chunking por feature, priorización + top N, mensaje explicativo
- Aggregar resultados en un único review evita ruido en el PR
- Priorización transparente comunica qué se revisó y qué se omitió
- Paralelización con cuidado acelera reviews grandes sin chocar rate limits
- Mensaje explícito en PRs demasiado grandes mantiene transparencia
Próxima cápsula: Módulo 3 — GitLab CI/CD y SDK Headless. Tu bot funciona en GitHub Actions, maneja PRs chicos y grandes, opera con convenciones del equipo. La siguiente pregunta natural: ¿qué pasa si tu equipo (o tu cliente) usa GitLab? El SDK headless es la respuesta — escribes la lógica una vez, corre en cualquier plataforma.
Recursos Adicionales
- Anthropic Rate Limits — Límites por tier
- Python concurrent.futures — Paralelización segura
- GitHub PR Files endpoint — Hasta 3000 archivos por PR
- Effective batch processing with LLMs — Anthropic batch API para casos avanzados
- Code review at Google: large CL — Cómo Google maneja PRs grandes (referencia conceptual)
- The 200-line rule — Argumento sobre tamaño ideal de PRs