Creación de skills de agentes personalizados
Avanzado⏱65 min📖3 min de lecturaLección 7 de 10

Creación de skills de agentes personalizados

Aprende a diseñar, implementar, probar y desplegar skills personalizados para agentes de IA con integración de herramientas y gestión de recursos.

Ciclo de vida del desarrollo de skills

Construir una skill personalizada sigue un ciclo de vida estructurado desde el concepto hasta el despliegue. Cada fase garantiza que la skill sea fiable, comprobable y mantenible.

100%
ℹ️Nota

El ciclo de vida del desarrollo de skills es iterativo. Cada despliegue aporta retroalimentación que informa la siguiente iteración de diseño. Las skills mejoran con el tiempo a medida que se descubren y se gestionan los casos límite.


Estructura del manifiesto de una skill

Toda skill comienza con un archivo de manifiesto YAML que declara su identidad, requisitos y comportamiento.

yaml
# skills/api-client-generator/skill.yaml name: api-client-generator version: "1.0.0" description: | Generates type-safe API client libraries from OpenAPI/Swagger specifications. Supports Python, TypeScript, and Go targets. author: "Nuniversity" license: "MIT" instructions: - path: instructions/generate-client.md - path: instructions/validate-spec.md - path: instructions/handle-errors.md tools: required: - read - write - glob optional: - bash - websearch resources: - path: templates/python-client.py.hbs description: Python client template - path: templates/typescript-client.ts.hbs description: TypeScript client template - path: schemas/openapi-v3.json description: OpenAPI 3.0 schema reference - path: configs/lint-rules.yaml description: Generated client linting rules autoload: enabled: true matchPattern: "generate client|api client|openapi|swagger" constraints: maxTokens: 8192 temperature: 0.3 errorRecovery: onFailure: "report_and_stop" maxRetries: 2
💡Consejo

Declara los requisitos de herramientas con precisión. Marca las herramientas como required solo si la skill no puede funcionar sin ellas. Usa optional para las herramientas que mejoran los resultados pero tienen un comportamiento de respaldo. Esto evita que la skill falle cuando las herramientas opcionales no están disponibles.


Redacción de instrucciones efectivas

Las instrucciones son el núcleo de una skill. Deben ser claras, completas y gestionar los casos límite.

markdown
<!-- skills/api-client-generator/instructions/generate-client.md --> # Generar cliente de API ## Paso 1: Leer la especificación de OpenAPI - Usa la herramienta `read` para cargar el archivo OpenAPI/Swagger - Valida la estructura de la especificación: - Debe tener el campo de versión `openapi` o `swagger` - Debe tener `info.title` y `info.version` - Debe tener al menos una entrada en `paths` - Si la validación falla, reporta los problemas específicos del esquema ## Paso 2: Extraer los metadatos de la API - Analiza la sección `info` para el nombre y la versión de la API - Identifica la URL base desde el campo `servers` o `host` - Extrae los esquemas de seguridad de `components.securitySchemes` ## Paso 3: Generar el código del cliente Para cada ruta y método de la especificación: 1. Genera la firma de la función con parámetros tipados 2. Añade la lógica de construcción de la solicitud (URL, cabeceras, cuerpo) 3. Añade el análisis de la respuesta con gestión de errores 4. Genera docstrings completos ## Paso 4: Generar los tipos auxiliares - Genera las interfaces de solicitud/respuesta - Genera los tipos enum para enumeraciones de cadenas - Genera los tipos de error para las respuestas de error ## Paso 5: Generar pruebas - Crea pruebas unitarias para cada método del cliente - Simula la capa HTTP - Prueba el manejo de respuestas correctas y de error - Prueba los casos límite (respuestas vacías, errores de red) ## Paso 6: Verificar - Ejecuta el linter sobre el código generado - Verifica la compilación de TypeScript (o la importación en Python) - Reporta cualquier problema de generación
python
# Programmatic skill implementation import json import os from typing import Optional class ApiClientGeneratorSkill: def __init__(self, agent): self.agent = agent self.language_handlers = { "python": self._generate_python, "typescript": self._generate_typescript, "go": self._generate_go, } async def execute(self, spec_path, language="python", output_dir="generated"): spec_content = await self.agent.read(spec_path) spec = json.loads(spec_content) errors = self._validate_spec(spec) if errors: return {"status": "error", "validation_errors": errors} os.makedirs(output_dir, exist_ok=True) handler = self.language_handlers.get(language) if not handler: return {"status": "error", "message": f"Unsupported language: {language}"} client_code = handler(spec, spec_path) client_path = os.path.join(output_dir, self._client_filename(language)) await self.agent.write(client_path, client_code) test_code = self._generate_tests(spec, language) test_path = os.path.join(output_dir, self._test_filename(language)) await self.agent.write(test_path, test_code) return { "status": "success", "client_file": client_path, "test_file": test_path, "endpoints_generated": len(spec.get("paths", {})) } def _validate_spec(self, spec): errors = [] if "openapi" not in spec and "swagger" not in spec: errors.append("Missing version field (openapi or swagger)") if "info" not in spec: errors.append("Missing info section") elif "title" not in spec.get("info", {}): errors.append("Missing info.title") if "paths" not in spec or not spec["paths"]: errors.append("No paths defined") return errors def _generate_python(self, spec, spec_path): name = spec.get("info", {}).get("title", "ApiClient") lines = [ f'"""Auto-generated client for {name}"""', "import requests", "from typing import Optional, Any, Dict", "", f"class {name.replace(' ', '')}Client:", f' """Client for the {name} API."""', " def __init__(self, base_url, api_key=None):", " self.base_url = base_url.rstrip('/')", " self.session = requests.Session()", " if api_key:", ' self.session.headers["Authorization"] = f"Bearer {api_key}"', "", ] for path, methods in spec.get("paths", {}).items(): for method in methods: func_name = self._path_to_function(method, path) lines.append(f" def {func_name}(self):") lines.append(f' """Call {method.upper()} {path}."""') lines.append(f' url = f"{{self.base_url}}{path}"') lines.append(f" response = self.session.{method}(url)") lines.append(" response.raise_for_status()") lines.append(" return response.json()") lines.append("") return "\n".join(lines) def _generate_typescript(self, spec, spec_path): name = spec.get("info", {}).get("title", "ApiClient") lines = [ f"// Auto-generated client for {name}", "import axios, { AxiosInstance } from 'axios';", "", f"export class {name.replace(' ', '')}Client {{", " private client: AxiosInstance;", " constructor(baseURL: string, apiKey?: string) {", " this.client = axios.create({ baseURL });", " if (apiKey) {", " this.client.defaults.headers.common['Authorization'] = `Bearer ${apiKey}`;", " }", " }", "", ] for path, methods in spec.get("paths", {}).items(): for method in methods: func_name = self._path_to_function(method, path) lines.append(f" async {func_name}(): Promise<any> {{") lines.append(f" const response = await this.client.{method}('{path}');") lines.append(" return response.data;") lines.append(" }") lines.append("") lines.append("}") return "\n".join(lines) def _generate_go(self, spec, spec_path): name = spec.get("info", {}).get("title", "ApiClient") safe_name = name.replace(" ", "") lines = [ f"// Package client provides auto-generated client for {name}", "package client", "", 'import (', ' "net/http"', ' "encoding/json"', ")", "", f"type {safe_name}Client struct {{", " baseURL string", " httpClient *http.Client", " apiKey string", "}", "", f"func New{safe_name}Client(baseURL string, apiKey string) *{safe_name}Client {{", f" return &{safe_name}Client{{", " baseURL: baseURL,", " httpClient: &http.Client{},", " apiKey: apiKey,", " }", "}", "", ] for path, methods in spec.get("paths", {}).items(): for method in methods: func_name = self._path_to_function(method, path) lines.append(f"func (c *{safe_name}Client) {func_name}() (map[string]interface{{}}, error) {{") lines.append(f' url := c.baseURL + "{path}"') lines.append(f' req, _ := http.NewRequest("{method.upper()}", url, nil)') lines.append(' req.Header.Set("Authorization", "Bearer " + c.apiKey)') lines.append(" resp, err := c.httpClient.Do(req)") lines.append(" if err != nil { return nil, err }") lines.append(" defer resp.Body.Close()") lines.append(" var result map[string]interface{}") lines.append(" json.NewDecoder(resp.Body).Decode(&result)") lines.append(" return result, nil") lines.append("}") lines.append("") return "\n".join(lines) def _generate_tests(self, spec, language): if language == "python": name = spec.get("info", {}).get("title", "ApiClient") safe = name.replace(" ", "") lines = [ '"""Tests for auto-generated API client."""', "import pytest", f"from client import {safe}Client", "", "@pytest.fixture", "def client():", f' return {safe}Client("https://api.example.com")', "", ] for path, methods in spec.get("paths", {}).items(): for method in methods: func_name = self._path_to_function(method, path) lines.append(f"def test_{func_name}(client, requests_mock):") url = f"https://api.example.com{path}" lines.append(f' requests_mock.{method}("{url}", json={{"status": "ok"}})') lines.append(f" result = client.{func_name}()") lines.append(' assert result["status"] == "ok"') lines.append("") return "\n".join(lines) return "# Tests not yet implemented for this language" def _path_to_function(self, method, path): parts = path.strip("/").replace("/", "_").replace("-", "_").replace("{", "").replace("}", "") return f"{method}_{parts}" def _client_filename(self, language): return {"python": "client.py", "typescript": "client.ts", "go": "client.go"}[language] def _test_filename(self, language): return {"python": "test_client.py", "typescript": "client.test.ts", "go": "client_test.go"}[language]

Gestión de recursos de skills

Las skills pueden empaquetar recursos como plantillas, archivos de referencia y configuración.

yaml
# skills/api-client-generator/resources/templates/python-client.py.hbs \"\"\" Auto-generated client for {{info.title}} v{{info.version}} \"\"\" import requests from typing import Optional, Any, Dict, List class {{info.title | replace(' ', '')}}Client: \"\"\"Client for the {{info.title}} API.\"\"\" def __init__(self, base_url, api_key=None): self.base_url = base_url.rstrip('/') self.session = requests.Session() self.session.headers.update({ "Content-Type": "application/json", "Accept": "application/json", }) if api_key: self.session.headers["Authorization"] = f"Bearer {api_key}" {% for path, methods in paths.items() %} {% for method in methods %} def {{method}}_{{path | to_snake}}(self, ...): \"\"\"{{methods[method].summary}}\"\"\" url = f"{self.base_url}{{path}}" response = self.session.{{method}}(url) response.raise_for_status() return response.json() {% endfor %} {% endfor %}
💡Consejo

Usa plantillas al estilo Handlebars (.hbs) para el código generado. Permiten separar limpiamente la lógica de la plantilla del código Python de la skill y hacen que las plantillas sean más fáciles de mantener y personalizar.


Pruebas de skills

Las skills deben probarse a fondo antes del despliegue.

python
import pytest import json from unittest.mock import AsyncMock @pytest.fixture def mock_agent(): agent = AsyncMock() agent.read.return_value = json.dumps({ "openapi": "3.0.0", "info": {"title": "Test API", "version": "1.0.0"}, "paths": { "/users": {"get": {"summary": "List users"}}, "/users/{id}": {"get": {"summary": "Get user"}} } }) return agent @pytest.mark.asyncio async def test_api_client_generator_python(mock_agent): skill = ApiClientGeneratorSkill(mock_agent) result = await skill.execute("spec.yaml", language="python") assert result["status"] == "success" assert result["endpoints_generated"] == 2 @pytest.mark.asyncio async def test_invalid_spec_returns_errors(mock_agent): mock_agent.read.return_value = json.dumps({ "openapi": "3.0.0" }) skill = ApiClientGeneratorSkill(mock_agent) result = await skill.execute("invalid.yaml") assert result["status"] == "error" assert len(result["validation_errors"]) > 0 @pytest.mark.asyncio async def test_unsupported_language(mock_agent): skill = ApiClientGeneratorSkill(mock_agent) result = await skill.execute("spec.yaml", language="rust") assert result["status"] == "error" assert "rust" in result["message"].lower() @pytest.mark.asyncio async def test_generated_code_structure(mock_agent): skill = ApiClientGeneratorSkill(mock_agent) result = await skill.execute("petstore.yaml", language="python") write_args = mock_agent.write.call_args_list[0][0] content = write_args[1] assert "class TestAPIClient:" in content assert "def get_users(self):" in content

Registro de skills en OpenCode

json
{ "skills": { "api-client-generator": { "manifest": "skills/api-client-generator/skill.yaml", "autoLoad": true, "matchPattern": "api client|openapi|swagger|generate client" }, "code-reviewer": { "manifest": "skills/code-reviewer/skill.yaml", "autoLoad": true, "matchPattern": "review|audit|inspect" }, "database-migration": { "manifest": "skills/database-migration/skill.yaml", "autoLoad": false } }, "agents": { "default": { "model": "gpt-4o", "description": "Primary coding assistant", "skills": ["api-client-generator", "code-reviewer"] } } }

Patrones de diseño de skills

Patrón
Descripción
Ideal para
Propósito único
Una skill = una tarea
Linting, pruebas, generación
Pipeline
Cadena de skills
Build -> Test -> Deploy
Basado en plantillas
Usa plantillas para la salida
Generación de código, andamiaje
Adaptativo
Ajusta el comportamiento según el contexto
Revisión de código, depuración
Interactivo
Requiere entrada del usuario en puntos clave
Despliegue, configuración
Sub-skill
Orquesta otras skills
Flujos de trabajo meta
⚠️Advertencia

Evita crear una "God Skill" que intente hacerlo todo. Si las instrucciones de una skill superan las 200 líneas o se necesitan más de 5 herramientas, probablemente debas dividirla en skills más pequeñas y especializadas que puedan componerse entre sí.


Estrategia de versionado de skills

yaml
# versioning-policy.yaml versioning: strategy: "semantic" rules: major: - "Breaking changes to skill interface" - "Removed required tools" - "Changed output format" minor: - "Added new instructions or resources" - "Added optional tool support" - "Enhanced error recovery" patch: - "Fixed instruction typos" - "Improved example code" - "Updated resource references" compatibility: - "Skills with same major version are compatible" - "Minor version upgrades are backward compatible" - "Patch versions require no changes from consumers"

Ejercicios de práctica

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

¿Cuáles son los tres componentes obligatorios de un manifiesto de skill?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

¿Cuándo se debe marcar una herramienta como 'required' frente a 'optional' en un manifiesto de skill?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

¿Cuál es el propósito de la sección 'autoload' en un manifiesto de skill?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

¿Qué patrón de pruebas se recomienda para las skills personalizadas?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

¿Qué es el antipatrón 'God Skill'?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

En el ciclo de vida del desarrollo de skills, ¿qué sucede después de la fase de despliegue?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

¿Qué formato de archivo se recomienda para las plantillas de generación de código incluidas con las skills?

Pregunta de Práctica
Cómo responder

Pregunta de opción múltiple con 4 opciones. Selecciona una opción y elige Comprobar respuesta.

¿Qué sucede cuando el matchPattern de una skill es demasiado amplio?


✅Conclusiones Clave (Key Takeaways):
  • Las skills siguen un ciclo de vida: Diseñar -> Implementar -> Probar -> Desplegar -> Monitorear -> Iterar
  • Los manifiestos declaran identidad, herramientas, recursos, patrones de autoload y restricciones
  • Las instrucciones son el núcleo de una skill; deben ser claras, completas y gestionar los casos límite
  • Prueba las skills con agentes simulados en las pruebas unitarias y con herramientas reales en las de integración
  • Evita las God Skills; sigue la responsabilidad única para skills componibles y mantenibles
  • Usa plantillas de Handlebars para el código generado en las skills
  • Registra las skills en opencode.json con autoload y matchPattern
  • Usa el versionado semántico para comunicar cambios incompatibles en las skills
  • Monitorea el uso de las skills para impulsar mejoras iterativas
Progreso70%