Generación de Contenido Técnico con LLMs¶
Tiempo de lectura: 30 minutos | Dificultad: Intermedia | Categoría: Inteligencia Artificial
Resumen¶
Los LLMs pueden automatizar la creación de documentación técnica, generar posts de blog y resumir artículos complejos. Esta guía cubre técnicas prácticas para usar modelos locales en workflows de documentación empresarial, manteniendo calidad y consistencia.
🎯 Por Qué Automatizar la Documentación¶
Problemas Comunes en Documentación¶
- Documentación obsoleta: Código cambia más rápido que los docs
- Inconsistencia de estilo: Múltiples autores, múltiples estilos
- Falta de tiempo: Developers prefieren codear que documentar
- Barreras de idioma: Contenido en un solo idioma limita audiencia
Beneficios de LLMs para Documentación¶
- ✅ Generación automática de docstrings y README
- ✅ Traducción técnica precisa a múltiples idiomas
- ✅ Resumen automático de PRs y changelogs
- ✅ Consistencia de estilo con templates personalizados
- ✅ Actualización continua con CI/CD integration
📝 Caso de Uso 1: Generación de Documentación API¶
Automatización de Docstrings¶
import ast
import requests
def generate_docstring(function_code: str, model: str = "llama2:7b-chat-q4_0") -> str:
"""
Genera un docstring profesional para una función Python.
Args:
function_code: Código fuente de la función
model: Modelo Ollama a usar
Returns:
Docstring generado en formato Google Style
"""
prompt = f"""
Eres un experto en documentación Python. Genera un docstring profesional en formato Google Style para esta función:
```python
{function_code}
El docstring debe incluir: 1. Descripción breve (1 línea) 2. Descripción detallada (si es necesario) 3. Args: con tipos y descripciones 4. Returns: con tipo y descripción 5. Raises: si aplica 6. Examples: código de ejemplo
Responde SOLO con el docstring, sin explicaciones adicionales. """
response = requests.post("http://localhost:11434/api/generate", json={
"model": model,
"prompt": prompt,
"stream": False,
"temperature": 0.3 # Baja temperatura para consistencia
})
if response.status_code == 200:
return response.json()["response"].strip()
else:
raise Exception(f"Error generando docstring: {response.text}")
Ejemplo de uso¶
code = """ def calculate_metrics(data: list, threshold: float = 0.5): filtered = [x for x in data if x > threshold] avg = sum(filtered) / len(filtered) if filtered else 0 return {"count": len(filtered), "average": avg} """
docstring = generate_docstring(code) print(docstring)
### Resultado Esperado
```python
"""
Calculate metrics for filtered data above a threshold.
Processes a list of numeric values, filters out values below the specified
threshold, and computes aggregated statistics.
Args:
data (list): List of numeric values to process
threshold (float, optional): Minimum value to include in calculations.
Defaults to 0.5.
Returns:
dict: Dictionary with keys:
- 'count' (int): Number of values above threshold
- 'average' (float): Mean of filtered values, or 0 if none
Examples:
>>> calculate_metrics([0.3, 0.6, 0.8, 0.2], threshold=0.5)
{'count': 2, 'average': 0.7}
>>> calculate_metrics([0.1, 0.2], threshold=0.5)
{'count': 0, 'average': 0}
"""
Procesamiento por Lotes¶
import os
import ast
def document_python_file(filepath: str, output_dir: str = "docs_generated"):
"""Documenta todas las funciones en un archivo Python."""
with open(filepath, 'r') as f:
tree = ast.parse(f.read())
os.makedirs(output_dir, exist_ok=True)
documented_functions = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
# Extraer código de la función
function_code = ast.get_source_segment(open(filepath).read(), node)
# Generar docstring
docstring = generate_docstring(function_code)
documented_functions.append({
"name": node.name,
"docstring": docstring,
"code": function_code
})
# Guardar documentación en Markdown
output_file = os.path.join(output_dir, f"{os.path.basename(filepath)}.md")
with open(output_file, 'w') as f:
f.write(f"# Documentación: {filepath}\n\n")
for func in documented_functions:
f.write(f"## `{func['name']}`\n\n")
f.write(f"```python\n{func['docstring']}\n```\n\n")
f.write(f"**Código fuente:**\n```python\n{func['code']}\n```\n\n")
print(f"✅ Documentación generada: {output_file}")
return output_file
# Uso
document_python_file("my_module.py")
📄 Caso de Uso 2: Generación de README Automático¶
Template con Variables¶
from pathlib import Path
import json
def generate_readme(
repo_path: str,
project_name: str = None,
description: str = None,
model: str = "llama2:7b-chat-q4_0"
) -> str:
"""
Genera un README.md profesional analizando el repositorio.
Args:
repo_path: Ruta al repositorio
project_name: Nombre del proyecto (auto-detecta si None)
description: Descripción breve (LLM genera si None)
model: Modelo Ollama a usar
Returns:
Contenido del README.md generado
"""
repo = Path(repo_path)
# Auto-detectar información del proyecto
if not project_name:
project_name = repo.name
# Analizar estructura
files = list(repo.rglob("*.py"))
has_tests = any("test" in str(f) for f in files)
has_requirements = (repo / "requirements.txt").exists()
has_docker = (repo / "Dockerfile").exists()
# Leer archivos principales
main_files = []
for file in ["main.py", "app.py", "__init__.py", "setup.py"]:
filepath = repo / file
if filepath.exists():
with open(filepath, 'r') as f:
main_files.append({"name": file, "content": f.read()[:500]})
# Prompt para LLM
prompt = f"""
Genera un README.md profesional para este proyecto Python:
**Proyecto:** {project_name}
**Descripción:** {description or "Analiza el código y genera una descripción"}
**Estructura:**
- {len(files)} archivos Python
- Tests: {"Sí" if has_tests else "No"}
- Docker: {"Sí" if has_docker else "No"}
**Archivos principales:**
{json.dumps(main_files, indent=2)}
Genera un README con estas secciones:
1. # {project_name} - Título y badges
2. ## Descripción - Qué hace el proyecto (2-3 párrafos)
3. ## Características - Lista de features principales
4. ## Requisitos - Python version, dependencias
5. ## Instalación - Paso a paso
6. ## Uso - Ejemplos básicos
7. ## Desarrollo - Cómo contribuir
8. ## Licencia
Usa formato Markdown profesional. Sé conciso pero completo.
"""
response = requests.post("http://localhost:11434/api/generate", json={
"model": model,
"prompt": prompt,
"stream": False,
"temperature": 0.4
})
if response.status_code == 200:
readme_content = response.json()["response"]
# Guardar README
with open(repo / "README.md", 'w') as f:
f.write(readme_content)
print(f"✅ README generado en {repo / 'README.md'}")
return readme_content
else:
raise Exception(f"Error: {response.text}")
# Uso
readme = generate_readme(
"/path/to/my-project",
description="API REST para gestión de usuarios con autenticación JWT"
)
✍️ Caso de Uso 3: Generación de Posts de Blog¶
Pipeline Completo¶
from datetime import datetime
import frontmatter
class BlogPostGenerator:
def __init__(self, model: str = "llama2:13b-chat-q4_0"):
self.model = model
self.ollama_url = "http://localhost:11434/api/generate"
def generate_outline(self, topic: str, audience: str = "developers") -> list:
"""Genera un outline estructurado para el post."""
prompt = f"""
Crea un outline detallado para un post de blog técnico sobre: {topic}
Audiencia: {audience}
Objetivo: Educativo, práctico, con ejemplos de código
Genera una estructura con:
1. Título atractivo (H1)
2. Introducción (problema que resuelve)
3. 4-5 secciones principales (H2) con subsecciones (H3)
4. Sección de conclusiones
5. Referencias/recursos
Formato: Lista con viñetas, indicando nivel de heading.
"""
response = requests.post(self.ollama_url, json={
"model": self.model,
"prompt": prompt,
"stream": False,
"temperature": 0.6
})
outline_text = response.json()["response"]
# Parsear outline a estructura
return self._parse_outline(outline_text)
def generate_section(self, section_title: str, context: str = "") -> str:
"""Genera contenido para una sección específica."""
prompt = f"""
Escribe la sección "{section_title}" de un post técnico.
Contexto del artículo: {context}
Requisitos:
- 300-500 palabras
- Incluir ejemplos de código si es relevante
- Tono profesional pero accesible
- Formato Markdown
- Incluir enlaces a recursos externos si aplica
Contenido:
"""
response = requests.post(self.ollama_url, json={
"model": self.model,
"prompt": prompt,
"stream": False,
"temperature": 0.7
})
return response.json()["response"]
def generate_full_post(
self,
topic: str,
tags: list,
category: str = "DevOps",
audience: str = "developers"
) -> str:
"""Genera un post completo de blog."""
# 1. Generar outline
print("📝 Generando outline...")
outline = self.generate_outline(topic, audience)
# 2. Generar cada sección
print("✍️ Generando secciones...")
sections = []
for item in outline:
section_content = self.generate_section(
item["title"],
context=f"Post sobre {topic}"
)
sections.append({
"level": item["level"],
"title": item["title"],
"content": section_content
})
# 3. Ensamblar post completo
print("🔧 Ensamblando post...")
post_content = self._assemble_post(sections)
# 4. Agregar frontmatter
post = frontmatter.Post(post_content)
post.metadata = {
"title": outline[0]["title"] if outline else topic,
"date": datetime.now().strftime("%Y-%m-%d"),
"tags": tags,
"category": category,
"author": "AI Assistant",
"draft": True # Revisar antes de publicar
}
# 5. Guardar
filename = f"blog_{datetime.now().strftime('%Y%m%d')}_{topic.lower().replace(' ', '_')}.md"
with open(filename, 'w') as f:
f.write(frontmatter.dumps(post))
print(f"✅ Post generado: {filename}")
return filename
def _parse_outline(self, text: str) -> list:
"""Parsea texto de outline a estructura."""
# Implementación simplificada
lines = text.strip().split('\n')
outline = []
for line in lines:
if line.startswith('#'):
level = len(line.split()[0])
title = line.lstrip('#').strip()
outline.append({"level": level, "title": title})
return outline
def _assemble_post(self, sections: list) -> str:
"""Ensambla secciones en post completo."""
content = []
for section in sections:
heading = '#' * section["level"]
content.append(f"{heading} {section['title']}\n\n{section['content']}\n\n")
return '\n'.join(content)
# Uso
generator = BlogPostGenerator()
post_file = generator.generate_full_post(
topic="Optimización de Kubernetes en Producción",
tags=["kubernetes", "devops", "performance"],
category="Infrastructure"
)
📊 Caso de Uso 4: Resumen de Artículos Técnicos¶
Resumen Inteligente con Extracción de Conceptos¶
import requests
from bs4 import BeautifulSoup
class TechnicalSummarizer:
def __init__(self, model: str = "llama2:13b-chat-q4_0"):
self.model = model
self.ollama_url = "http://localhost:11434/api/generate"
def fetch_article(self, url: str) -> dict:
"""Extrae contenido de un artículo web."""
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
# Extraer título y contenido principal
title = soup.find('h1').get_text() if soup.find('h1') else "Sin título"
# Remover scripts y estilos
for script in soup(["script", "style"]):
script.decompose()
# Extraer texto
text = soup.get_text()
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = ' '.join(chunk for chunk in chunks if chunk)
return {"title": title, "content": text[:4000]} # Limitar a 4k chars
def summarize(
self,
text: str,
summary_type: str = "executive", # executive, technical, bullet_points
max_length: int = 500
) -> str:
"""Genera resumen del texto."""
prompts = {
"executive": f"""
Resume este artículo técnico en {max_length} palabras para ejecutivos no técnicos.
Enfócate en: problema, solución, beneficios, impacto de negocio.
Artículo:
{text}
Resumen ejecutivo:
""",
"technical": f"""
Resume este artículo técnico en {max_length} palabras para desarrolladores.
Incluye: conceptos clave, arquitectura, implementación, trade-offs.
Artículo:
{text}
Resumen técnico:
""",
"bullet_points": f"""
Extrae los puntos clave de este artículo técnico (máximo 10 puntos).
Formato: lista con viñetas, concisa y accionable.
Artículo:
{text}
Puntos clave:
"""
}
prompt = prompts.get(summary_type, prompts["technical"])
response = requests.post(self.ollama_url, json={
"model": self.model,
"prompt": prompt,
"stream": False,
"temperature": 0.3
})
return response.json()["response"]
def extract_code_examples(self, text: str) -> list:
"""Extrae snippets de código del texto."""
prompt = f"""
Identifica y extrae TODOS los ejemplos de código de este artículo.
Para cada snippet, indica:
1. Lenguaje de programación
2. Propósito/descripción breve
3. El código completo
Texto:
{text}
Ejemplos de código:
"""
response = requests.post(self.ollama_url, json={
"model": self.model,
"prompt": prompt,
"stream": False,
"temperature": 0.2
})
return response.json()["response"]
def generate_study_notes(self, url: str) -> dict:
"""Genera notas de estudio completas de un artículo."""
# 1. Fetch artículo
article = self.fetch_article(url)
# 2. Generar diferentes resúmenes
executive = self.summarize(article["content"], "executive", 300)
technical = self.summarize(article["content"], "technical", 500)
bullet_points = self.summarize(article["content"], "bullet_points")
# 3. Extraer código
code_examples = self.extract_code_examples(article["content"])
# 4. Ensamblar notas
notes = f"""# {article['title']}
## Resumen Ejecutivo
{executive}
## Resumen Técnico
{technical}
## Puntos Clave
{bullet_points}
## Ejemplos de Código
{code_examples}
---
*Fuente:* {url}
*Generado:* {datetime.now().strftime("%Y-%m-%d %H:%M")}
"""
# 5. Guardar
filename = f"notes_{datetime.now().strftime('%Y%m%d_%H%M')}.md"
with open(filename, 'w') as f:
f.write(notes)
print(f"✅ Notas generadas: {filename}")
return {
"executive": executive,
"technical": technical,
"bullet_points": bullet_points,
"code_examples": code_examples,
"filename": filename
}
# Uso
summarizer = TechnicalSummarizer()
notes = summarizer.generate_study_notes("https://blog.example.com/kubernetes-optimization")
🔄 Caso de Uso 5: Changelog Automático desde Git¶
Generación de Release Notes¶
import subprocess
import re
class ChangelogGenerator:
def __init__(self, model: str = "llama2:7b-chat-q4_0"):
self.model = model
self.ollama_url = "http://localhost:11434/api/generate"
def get_commits_since_tag(self, tag: str = None) -> list:
"""Obtiene commits desde el último tag."""
if not tag:
# Obtener último tag
result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"],
capture_output=True,
text=True
)
tag = result.stdout.strip() if result.returncode == 0 else None
# Obtener commits
cmd = ["git", "log", f"{tag}..HEAD", "--pretty=format:%H|%s|%b"] if tag else \
["git", "log", "--pretty=format:%H|%s|%b", "-20"]
result = subprocess.run(cmd, capture_output=True, text=True)
commits = []
for line in result.stdout.split('\n'):
if '|' in line:
hash_id, subject, body = line.split('|', 2)
commits.append({
"hash": hash_id[:7],
"subject": subject,
"body": body.strip()
})
return commits
def categorize_commits(self, commits: list) -> dict:
"""Categoriza commits por tipo (feat, fix, docs, etc.)."""
categories = {
"features": [],
"fixes": [],
"docs": [],
"refactor": [],
"other": []
}
for commit in commits:
subject = commit["subject"].lower()
if subject.startswith("feat"):
categories["features"].append(commit)
elif subject.startswith("fix"):
categories["fixes"].append(commit)
elif subject.startswith("docs"):
categories["docs"].append(commit)
elif subject.startswith("refactor"):
categories["refactor"].append(commit)
else:
categories["other"].append(commit)
return categories
def generate_changelog(
self,
version: str,
since_tag: str = None,
output_file: str = "CHANGELOG.md"
) -> str:
"""Genera changelog profesional."""
# 1. Obtener commits
commits = self.get_commits_since_tag(since_tag)
# 2. Categorizar
categorized = self.categorize_commits(commits)
# 3. Generar descripción con LLM
prompt = f"""
Genera un changelog profesional para la versión {version} basado en estos commits:
**Features:**
{chr(10).join([f"- {c['subject']}" for c in categorized['features']])}
**Fixes:**
{chr(10).join([f"- {c['subject']}" for c in categorized['fixes']])}
**Docs:**
{chr(10).join([f"- {c['subject']}" for c in categorized['docs']])}
Requisitos:
1. Agrupa cambios relacionados
2. Usa lenguaje user-friendly (no técnico)
3. Destaca breaking changes si los hay
4. Formato Markdown con secciones claras
Changelog:
"""
response = requests.post(self.ollama_url, json={
"model": self.model,
"prompt": prompt,
"stream": False,
"temperature": 0.4
})
changelog_content = response.json()["response"]
# 4. Agregar header
full_changelog = f"""# Changelog
## [{version}] - {datetime.now().strftime("%Y-%m-%d")}
{changelog_content}
### Commits Incluidos
{chr(10).join([f"- {c['hash']} - {c['subject']}" for c in commits])}
"""
# 5. Guardar
with open(output_file, 'w') as f:
f.write(full_changelog)
print(f"✅ Changelog generado: {output_file}")
return full_changelog
# Uso
generator = ChangelogGenerator()
changelog = generator.generate_changelog(
version="v2.1.0",
since_tag="v2.0.0"
)
🔧 Integración con CI/CD¶
GitHub Actions Workflow¶
name: Auto-generate Documentation
on:
push:
branches: [main]
pull_request:
jobs:
generate-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install Ollama
run: |
curl -fsSL https://ollama.ai/install.sh | sh
ollama pull llama2:7b-chat-q4_0
- name: Install dependencies
run: pip install requests python-frontmatter beautifulsoup4
- name: Generate API docs
run: python scripts/generate_docs.py --input src/ --output docs/api/
- name: Generate changelog
run: python scripts/generate_changelog.py --version ${{ github.ref_name }}
- name: Commit changes
run: |
git config --global user.name 'Documentation Bot'
git config --global user.email 'bot@example.com'
git add docs/ CHANGELOG.md
git commit -m "docs: Auto-generate documentation [skip ci]" || true
git push
⚠️ Mejores Prácticas¶
Validación de Contenido Generado¶
class ContentValidator:
def validate_markdown(self, content: str) -> dict:
"""Valida formato Markdown."""
issues = []
# Verificar headers
if not re.search(r'^# ', content, re.MULTILINE):
issues.append("Falta título principal (H1)")
# Verificar enlaces rotos
links = re.findall(r'\[([^\]]+)\]\(([^\)]+)\)', content)
for text, url in links:
if url.startswith('http'):
try:
response = requests.head(url, timeout=5)
if response.status_code >= 400:
issues.append(f"Enlace roto: {url}")
except:
issues.append(f"No se puede validar: {url}")
return {
"valid": len(issues) == 0,
"issues": issues
}
def check_code_syntax(self, content: str) -> dict:
"""Valida sintaxis de bloques de código."""
code_blocks = re.findall(r'```(\w+)\n(.*?)\n```', content, re.DOTALL)
issues = []
for lang, code in code_blocks:
if lang == "python":
try:
compile(code, '<string>', 'exec')
except SyntaxError as e:
issues.append(f"Error de sintaxis Python: {e}")
return {
"valid": len(issues) == 0,
"issues": issues
}
# Uso
validator = ContentValidator()
validation_result = validator.validate_markdown(generated_content)
if not validation_result["valid"]:
print("⚠️ Problemas encontrados:")
for issue in validation_result["issues"]:
print(f" - {issue}")
Control de Calidad¶
- ✅ Revisión humana siempre antes de publicar
- ✅ Validación de sintaxis de código generado
- ✅ Verificación de enlaces para evitar rotos
- ✅ Spell checking con herramientas como
aspell - ✅ Tone consistency validar con otro LLM
📊 Métricas y Monitoreo¶
class DocumentationMetrics:
def __init__(self):
self.metrics = {
"documents_generated": 0,
"total_words": 0,
"total_cost_saved_hours": 0,
"errors": 0
}
def record_generation(self, word_count: int, time_taken: float):
self.metrics["documents_generated"] += 1
self.metrics["total_words"] += word_count
# Estimar ahorro (promedio 500 palabras/hora manual)
hours_saved = word_count / 500
self.metrics["total_cost_saved_hours"] += hours_saved
def report(self):
return f"""
📊 Métricas de Generación de Documentación
Documentos generados: {self.metrics['documents_generated']}
Palabras totales: {self.metrics['total_words']:,}
Horas ahorradas: {self.metrics['total_cost_saved_hours']:.1f}h
Errores: {self.metrics['errors']}
"""
metrics = DocumentationMetrics()
🔗 Recursos Adicionales¶
📚 Próximos Pasos¶
Después de automatizar documentación, considera:
- Análisis de Logs - Troubleshooting asistido por IA
- Prompt Engineering - Técnicas para mejores resultados
- Fine-tuning - Personalizar modelos para tu dominio
¿Has automatizado tu documentación con LLMs? Comparte tus experiencias y mejores prácticas en los comentarios.