Foto de icon0.com

Estoy seguro que ya ha probado e investigado las capacidades del modelo ChatGPT y puede que usaras la Interfaz de Programación de Aplicaciones (API) de OpenAI (ver mi articulo «La AI es mi compañero de trabajo«).

Como profesionales en programación tradicional, es probable que te esté asaltando esta cuestión: «¿Este será el futuro de la programación?» La respuesta es sí, lo es. Independientemente de las opiniones personales, este es y será el nuevo estándar: texto entra, texto sale. La aceptación de esta realidad debe ser el primer paso. Una vez asumida esta nueva realidad, es cuando podemos hacer la siguiente reflexión: «¿Cómo podemos optimizar aún más este proceso?»

¿Qué es Semantic Kernel?

Semantic Kernel es un Kit de Desarrollo de Software (SDK) de código abierto que te permite combinar fácilmente prompts de IA con lenguajes de programación tradicionales como C#. Simplifica el desarrollo de aplicaciones de IA ofreciendo un conjunto de abstracciones para crear y gestionar prompts, funciones nativas, memorias y conectores. Estos componentes pueden ser orquestados usando las canalizaciones de Semantic Kernel para satisfacer las solicitudes de los usuarios o automatizar tareas. El SDK se puede utilizar para orquestar la IA de cualquier proveedor como OpenAI o Azure OpenAI Service, por ejemplo.

Caso de uso

Vamos a crear una aplicación que nos permita crear codigo en C# para una aplicación de tipo ERP (por ejemplo). Esta herramienta tiene como objetivo que el cambio sea de la calidad suficiente que podría usarse directamente en nuestra refactorización de la aplicación legacy.

¿Qué criterios se deben cumplir para que podamos considerar la migración con calidad suficiente?

  • Que el codigo tenga buenas prácticas.
  • Que sea muy legible.
  • Tenga test.
el proyecto
  • Aplicación de consola con .NET 7.0 desde Visual Studio
  • Con los siguientes NuGet: Microsoft.SemanticKernel Preview y  Microsoft.Extensions.Configuration.Json
  • Tener una OpenAI API Key disponible.
  • Y descargarse este código de GitHub.

A continuación os pongo el código correspondiente:

using Microsoft.Extensions.Configuration;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.SemanticFunctions;

namespace AzureAIOpenAISemanticKernel;

internal class Program
{
    private static async Task Main(string[] args)
    {
        var config = new ConfigurationBuilder()
            .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
            .AddJsonFile("appsettings.json", false, true)
            .Build();

        var kernel = Kernel.Builder
            .WithAzureChatCompletionService(
                config.GetSection("DeploymentName").Value,
                config.GetSection("Endpoint").Value,
                config.GetSection("apiKey").Value
            )
            .Build();

        Console.Write("Enter requirements: ");
        var requirements = Console.ReadLine();
        var prompt = $"""
            Write a program with the following requirements:
            {requirements}
            """;

        var promptConfig = new PromptTemplateConfig
        {
            Completion =
            {
                MaxTokens = 1000, Temperature = 0.2, TopP = 0.5
            }
        };

        var promptTemplate = new PromptTemplate(
            prompt, promptConfig, kernel
        );

        var functionConfig = new SemanticFunctionConfig(promptConfig, promptTemplate);

        var function = kernel.RegisterSemanticFunction("WriteMyCode", functionConfig);

        var outputCode = await kernel.RunAsync(function);

        Console.WriteLine(outputCode);
    }
}

La configuración:

{
  "DeploymentName": "[YOU_APP_NAME]",
  "Endpoint": "https://[YOU_APP_NAME].openai.azure.com/",
  "apiKey": "[YOUR_APP_KEY]"
}

Y un ejemplo sobre el código anterior:

Si has probado los chat de OpenAI o cualquier otro habrás sisto que no difiere mucho de lo que te da el chat. Veamos que nos da realmente Semantic Kernel.

La potencia de Semantic Kernel

¿Cómo podemos elaborar código de alta calidad que se parezca a lo que va a realizar un humano? Sin duda, nuestro conocimiento pesa mucho en este proceso. Sin embargo, también empleamos varias técnicas de programación que aseguran que el código sea correcto y este es el enfoque que adoptamos:

  • Seleccionar los requerimientos fundamentales y obligatorios que captan la funcionalidad mínima y suficiente para dar la funcionalidad.
  • Elaborar paso a paso en pseudo-código que queremos hacer y como (un indice es una buena opción).
  • Desarrollar el programa siguiendo el pseudo-codigo anteriormente descrito.
  • Escritura de test unitarios, si usas TDD, pues tu forma de trabajar será difente a la aquí expuesta.
  • Escrituta de test de integración.
  • etc.

En lugar de pedir a la IA que componga el código basandose en una única frase, deberíamos descomponerlo en varias subtareas. Debido el problema de la limitación de tokens en los modelos de IA, elaborar una salida de una sola vez, es un reto y aquí precisamente es donde Semantic Kernel puede porporcionarnos una valiosa ayuda.

Prompts reutilizables

El problema fundamental es que existe una brecha entre la programación tradicional y prompt engineering. La programación convencional sigue un enfoque determinista en el que el resultado es predecible en función de la entrada y la lógica escrita. Por otro lado, prompt engineering implica la creación de instrucciones para guiar el comportamiento de un modelo de IA, y el resultado no siempre es predecible.

Semantic Kernel intenta tender un puente entre la programación convencional y prompt engineering introduciendo dos abstracciones clave: Plugins y Funciones. En tu aplicación, es probable que tenga una variedad de promts, cada uno sirviendo a un propósito diferente en la interacción con los modelos de IA. En lugar de tener estos promts dispersos a través de su código, Semantic Kernel te permite organizarlos ordenadamente en Plugins y Funciones dentro de una estructura de carpetas. En este contexto, un prompt es tratado como una Función que puede ser invocada usando Semantic Kernel, mientras que un Plugin es un grupo de Funciones relacionadas (prompts) que colectivamente realizan una tarea compartida.

Para nuestro ejemplo, podemos organizar nuestros prompts en la siguiente estructura de carpetas:

Plugins/
└── WriteMyCode/
    ├── step1/
    │   ├── config.json
    │   └── skprompt.txt
    ├── test1/
    │   ├── config.json
    │   └── skprompt.txt
    └── step2/
        ├── config.json
        └── skprompt.txt

Hemos establecido un plugin llamado «WriteMyCode», que está formado por distintas funciones. Cada una de estas funciones posee su propio prompt almacenado en el archivo ‘skprompt.txt’, junto con una configuración dedicada detallada dentro de ‘config.json’.

Mediante la abstracción de los conceptos de Plugin y Función, Semantic Kernel pretende:

  • Establecer una clara separación de conceptos: Este es el principio fundamental de la ingeniería de software, ayuda a gestionar la complejidad de las grandes aplicaciones dividiéndolas en unidades más pequeñas y autocontenidas. En el contexto de Semantic Kernel, cada función representa una tarea u operación específica que el modelo de IA puede realizar. Al separar estas funciones de su programa, puedes desarrollar, probar y modificar cada una de ellas de forma independiente, lo que hace que el sistema global sea más manejable y menos propenso a errores.
  • Permitir la reutilización de las instrucciones: la programación tradicional, la reutilización es un factor clave para reducir la duplicación de código y mejorar la capacidad de mantenimiento. Esto no es diferente en prompt engineering con Semantic Kernel. Al definir los prompts en funciones, estos avisos pueden reutilizarse en diferentes partes de su aplicación o incluso en diferentes aplicaciones. Esto no sólo reduce la cantidad de escritura de avisos, sino que también garantiza la coherencia en las respuestas de la IA.
  • Orquestar las prácticas de programación convencionales: Semantic Kernel está diseñado para integrarse perfectamente en los entornos de programación tradicionales. Las funciones de Semantic Kernel pueden invocarse y manipularse como si fueran funciones normales del lenguaje de programación elegido. Esto significa que los desarrolladores podemos aprovechar los plugins y herramientas existentes al trabajar con Semantic Kernel, lo que facilita la creación y el despliegue de aplicaciones basadas en IA.

Como cualquier otro lenguajes de programación, podemos pasar parámetros a una función, en Semantic Kernel también se pueden diseñar prompts con parámetros. Por ejemplo nuestro ‘skprompt.txt’ para la función «step1» podría ser:

Generate code exactly as follows

The codig must be
- Easy to maintain
- Simple to understand
- Use .NET7
- The code must be used in .NET dependency injection standar 
- Using SOLID principles
- Use best praticies
- Be testable

Generate code on:
+++++
{{$requirements}}
+++++

Y nuestro ‘config.json’ sería:

{
  "schema": 1,
  "type": "completion",
  "description": "a function that generates step 1",
  "completion": {
    "max_tokens": 500,
    "temperature": 0.2
  },
  "input": {
    "parameters": [
      {
        "name": "requirements",
        "description": "The requirements to generate code for",
        "defaultValue": ""
      }
    ]
  }
}

Para poder probar esto, debes ejecutar este código:

Console.Write("Enter requirements: ");
string? requirements = Console.ReadLine();

var plugin = kernel.ImportSemanticSkillFromDirectory("Plugins", "WriteMyCode");

var variables = new ContextVariables();
variables.Set("requirements", requirements);

var stepOne = await kernel.RunAsync(variables, plugin["Step1"]);

Console.WriteLine(stepOne);

Y como resultado obtendremos:

Como podeis observa la cosa cambia, el mismo requerimiento pero con un contexto que se mantendrá con todo el código que me ponga a generar para mi nueva aplicación.

Ahora es cuando pongo esto: surprised

Esto es una muestra de la orientación de Semantic Kernel. Ya que si anidas un step2 (que genere una API y le cuentes como la quieres), step3 (que genere un front en React que use tu API), etc. podeis observar la potencia de Semantic Kernel.

¿Qué más podemos hacer con Sematic Kernel?

Prompts contextuales

Cuando enviamos una solicitud al LLM (Large Language Model), recibimos un texto de respuesta. Sin embargo, es importante tener en cuenta que ni los modelos ni las API backend recuedan los prompts historicos.

Es decir si le estoy pidiendo que consturya un sistema de login, el historico no se usa para ir dando tus siguientes respuestas.

Es posible que la respuesta no se ajuste a tus expectativas, ya que el modelo no conserva el historico que ha proporcionado. Para mantener una «conversación» coherente a lo largo de sucesivas llamadas a la API, debemos introducir continuamente algunos datos contextuales en la solicitud. Este proceso garantiza que el modelo se mantenga informado sobre las interacciones anteriores.

Si la información contextual es breve, es posible incorporarla a las solicitudes posteriores. Sin embargo, hay que tener en cuenta la longitud del texto de entrada en relación con el límite de tokens del modelo seleccionado.

Por ejemplo, GPT-4 (el que usado en mis ejemplos) puede procesar hasta 8.192 tokens por entrada, mientras que el límite de GPT-3 es de 4.096 tokens. Por tanto, si los textos superan la capacidad de tokens del modelo, es posible que no se procesen en su totalidad y se trunquen o pasen por alto.

Semantic Kernel ha agilizado todo este procedimiento introduciendo el concepto de memoria. Esta memoria nos oculta la magia negra de la tokenización.

Conclusión

Es obligatior algo de intervención humana para ajustar nuestros prompts y poder acercarnos un poco más al criterio de calidad del programa que deseamos entergar. Y con la salida, algo más de intervención.

Pero de no tener nada a tener algo o tener todos los test unitarios, ya es un gran avance.