Valida automáticamente tus respuestas de AWS Bedrock LLM
Tags: llm, aws, ai, rag
Published: 2023-12-18 13:21:43

Author: [Martin Mueller](martinmueller.dev)
[English Version](https://dev.to/aws-builders/automatically-validate-your-aws-bedrock-llm-responses-4fhe)

##Introducción
Validar la respuesta de tu Modelo de Aprendizaje de Lenguaje (LLM) es un paso crítico en el proceso de desarrollo. Tienes que asegurar que la respuesta tenga el formato correcto y contenga los datos esperados. Si decides realizar la evaluación de forma manual, puedes encontrarte con una pesadilla, sobre todo si haces cambios frecuentes en tu LLM. 

El automatizar o semi-automatizar el proceso de validación es altamente recomendable para ahorrar tiempo y esfuerzo. En esta publicación os mostraré algunas ideas sobre cómo lograr esta automatización.

##Antes
Cuando hablamos de LLM, hablamos del uso de modelos fundacionales existentes a través de **AWS Bedrock**, como son Claude, LLama2 y otros. Puedes aprender más sobre AWS Bedrock [aquí](https://aws.amazon.com/bedrock/). Hay técnicas que puedes usar para mejorar la respuesta, como fine-tuning de prompts o RAG (generación de aumento de recuperación usando Bases de Datos Vectoriales).

Las respuestas de los Modelos de Aprendizaje de Lenguaje (LLMs) a menudo son no deterministas, lo que significa que se pueden generar diferentes respuestas incluso con el mismo prompt. Sin embargo, este comportamiento puede ajustarse hasta cierto punto utilizando parámetros de LLM como la temperatura.

##Ideas
En las siguientes secciones presentaré algunas ideas que te permitirán crear pruebas automatizadas para las respuestas de tus LLM. También veremos algunos ejemplos de cómo implementé estas ideas en mis propios proyectos.

##Validar la Forma
En muchos casos, la respuesta puede contener partes deterministas que se pueden usar para validar la respuesta de forma parcial. Por ejemplo, utilizo **Claude** para proporcionar una respuesta en JSON. He enseñado a Claude el esquema que debería tener el JSON, y he realizado una prueba de validación de esquema, verificando así si Claude se adhiere a dicho esquema. Como veréis a continuación, verificar un esquema JSON es muy simple.

Cada lenguaje de programación tiene una biblioteca que se puede usar para validar el esquema. Por ejemplo, en TypeScript, uso la biblioteca **[zod](https://github.com/colinhacks/zod)** para crear y validar el esquema. Que se ve así:

```
import { z } from 'zod';

export const NinoxFieldSchema = z.strictObject({
  base: z
    .enum([
      'string',
      'boolean',
      ...
    ])
    .optional(),
  caption: z.string().optional(),
  captions: z.record(z.string()).optional(),
  required: z.boolean().optional(),
  order: z.number().optional(),
  ...
});

export type NinoxField = z.infer<typeof NinoxFieldSchema>;

export const NinoxTableSchema = z.strictObject({
  nextFieldId: z.number().optional(),
  caption: z.string().optional(),
  captions: z.record(z.string()).optional(),
  hidden: z.boolean().optional(),
  ...
});

export type NinoxTable = z.infer<typeof NinoxTableSchema>;
```
Y como parte de mis pruebas unitarias:

```
test('check schema', async () => {
    ...

    const body = JSON.parse(response.body);

    const validationResult = NinoxTableSchema.safeParse(
        JSON.parse(body.json),
    );
    if (!validationResult.success) {
        console.log(validationResult.error.message);
    }
    expect(validationResult.success).toBeTruthy();
});
```
##Validar Sub-Respuestas

En mi aplicación de IA actual, utilizo múltiples llamadas a LLM para generar la respuesta final. Aunque validar toda la respuesta puede ser un desafío, lo que sí puedo hacer es validar fácilmente algunas de las sub-respuestas. Por ejemplo, tengo una respuesta determinista para la cual puedo verificar la respuesta; dicha respuesta clasifica la intención de un usuario en una categoría específica. Por ejemplo, si el usuario pide crear una tabla, la intención se clasifica como "crear_tabla". Eso generará una sub-respuesta determinista en mi **AWS Lambda** para la intención de "crear_tabla". Para probar la precisión de la clasificación, puedes usar métodos conocidos como división de entrenamiento-validación-prueba. Divide los datos de entrenamiento en subconjuntos para entrenamiento y validación. Describiré esta técnica más en la siguiente sección.

##División de Entrenamiento-Validación-Prueba
La división de **[entrenamiento-validación-prueba](https://medium.com/@evertongomede/the-significance-of-train-validation-test-split-in-machine-learning-91ee9f5b98f3)** es crucial para medir el rendimiento del LLM. Uno de los métodos de esas divisiones es la validación cruzada **k-fold**. Voy a intentar explicar este enfoque en palabras sencillas. Asegúrate de revisar el artículo técnico de Everton Gomede, PhD, [¡La Importancia de la División de Entrenamiento-Validación-Prueba en el Aprendizaje Automático!](https://medium.com/@evertongomede/the-significance-of-train-validation-test-split-in-machine-learning-91ee9f5b98f3) para más información.

Por ejemplo, podrías usar el 90 por ciento de los datos para entrenamiento y el 10 por ciento para validación. Luego, puedes usar los datos de validación para probar la precisión de la clasificación. Además, puedes usar permutación para cambiar el 10 por ciento de los datos de validación. Implementé un algoritmo simple en TypeScript que me ayuda a calcular la precisión de la clasificación:

```
import { test } from "@jest/globals"
import * as ArcbotStackStream from "../src/arcbot-stack.stream"
import {
 call_bedrock,
 generate_intent_identification_prompt,
 generate_table_identification_prompt,
 modify_table_prompt,
 relationship_json_prompt,
} from "../src/arcbot-stack.stream"
import {
 intentTrainingsData,
 modifyTableTrainingsData,
 oneToManyTrainingsData,
 tableIdentificationData,
} from "../src/training-data"

const runEvaluation = async <T extends { [s: string]: string[] }>(
 trainingData: T,
 jestSpy: jest.SpyInstance<T, [], any>,
 promptRefinement: (userInput: string) => Promise<string>,
 jsonResponse?: boolean
) => {
 const getTrainingAndEvaluationPermutations = (trainingsData: T) => {
  // Split trainings data into training and evaluation data
  const sliceTrainingsData = (fromPercentage: number, toPercentage: number) =>
   Object.entries(trainingsData).reduce(
    (acc, data) => {
     const evaluationSlice = data[1].slice(
      data[1].length * fromPercentage,
      data[1].length * toPercentage
     )
     const trainingSlice = data[1].filter((d) => !evaluationSlice.includes(d))
     return {
      training: { ...acc.training, [data[0]]: trainingSlice } as T,
      evaluations: {
       ...acc.evaluations,
       [data[0]]: evaluationSlice,
      } as T,
     }
    },
    {
     training: {} as T,
     evaluations: {} as T,
    }
   )
  const trainingPercentage = 0.9
  const validationPercentage = 1 - trainingPercentage

  // permute the training and evaluation data
  const trainingValidationPermutations = [
   sliceTrainingsData(0, 0 + validationPercentage),
  ]
  for (let i = 0 + validationPercentage; i < 1; i = i + validationPercentage) {
   trainingValidationPermutations.push(
    sliceTrainingsData(i, i + validationPercentage)
   )
  }

  console.log(
   `trainingValidationPermutations: ${JSON.stringify(
    trainingValidationPermutations
   )}`
  )
  return trainingValidationPermutations
 }

 let correctResponses = 0
 let wrongResponses = 0

 const trainingRecords = getTrainingAndEvaluationPermutations(trainingData)

 for (const trainingPermutation of trainingRecords) {
  console.log(`trainingPermutation=${JSON.stringify(trainingPermutation)}`)

  jestSpy.mockImplementation(() => trainingPermutation.training)

  for (const evaluationRecords of Object.entries(
   trainingPermutation.evaluations
  )) {
   for (const input of evaluationRecords[1]) {
    const intent_prompt = await promptRefinement(input)
    const response = await call_bedrock(intent_prompt, jsonResponse)

    let received = response

    // trim to JSON string
    if (jsonResponse) {
     received = JSON.stringify(JSON.parse(received))
    }

    console.log(`Expected ${evaluationRecords[0]}\nReceived ${received}`)

    if (evaluationRecords[0] === received) {
     correctResponses++
    } else {
     wrongResponses++
    }
   }
  }
 }
 console.log(
  ` correctResponses: ${correctResponses}\n wrongResponses: ${wrongResponses} \n ${
   correctResponses / (correctResponses + wrongResponses)
  } accuracy`
 )
}

test("evaluate one to many", async () => {
 const mockTrainingsData = jest.spyOn(
  ArcbotStackStream,
  "getOneToManyTrainingData"
 )

 const oneToManyPromptRefinement = async (userInput: string) =>
  relationship_json_prompt(
   {
    CUSTOMER: { caption: "Customer" },
    EMPLOYEE: { caption: "Employee" },
    INVOICE: { caption: "Invoice" },
   },
   userInput
  )

 await runEvaluation(
  oneToManyTrainingsData,
  mockTrainingsData,
  oneToManyPromptRefinement,
  true
 )
})

test("evaluate intent", async () => {
 const mockTrainingsData = jest.spyOn(
  ArcbotStackStream,
  "getIntentTrainingData"
 )

 const generate_intent_identification_promptRefinement = async (
  userInput: string
 ) => generate_intent_identification_prompt(userInput)

 await runEvaluation(
  intentTrainingsData,
  mockTrainingsData,
  generate_intent_identification_promptRefinement
 )
})

test("evaluate table identification", async () => {
 const mockTrainingsData = jest.spyOn(
  ArcbotStackStream,
  "getTableIdentificationData"
 )

 const generate_table_identification_promptRefinement = async (
  userInput: string
 ) => {
  const prompt = generate_table_identification_prompt(
   userInput,
   Object.keys(tableIdentificationData)
  )

  return prompt
 }

 await runEvaluation(
  tableIdentificationData,
  mockTrainingsData,
  generate_table_identification_promptRefinement
 )
})

test("evaluate modify table", async () => {
 const mockTrainingsData = jest.spyOn(
  ArcbotStackStream,
  "getModifyTableTrainingsData"
 )

 modifyTableTrainingsData

 const modify_table_promptRefinement = async (userInput: string) => {
  const prompt = await modify_table_prompt({}, userInput)

  return prompt
 }

 await runEvaluation(
  modifyTableTrainingsData,
  mockTrainingsData,
  modify_table_promptRefinement,
  true
 )
})
```

Creo que la parte más interesante aquí es la interfaz del método `getTrainingAndEvaluationPermutations(trainingData)`, ya que siempre espera el mismo formato como entrada y te devuelve una permuta de la división de validación de prueba de los datos de entrenamiento de entrada. Estos datos de entrenamiento deben estar en forma de lista de cadenas de registro:

```
<T extends { [s: string]: string[] }>
```
Donde **la clave** representa el resultado esperado / clase de clasificación / salida del LLM y **el valor** representa las posibles entradas que conducen al resultado. El resultado será de este tipo:

```
{
 training: T
 evaluations: T
}
;[]
```
Es un array que representa las permutaciones. Cada permutación tiene una sección de entrenamiento y evaluaciones.

Un ejemplo de datos de entrenamiento sería:

```
export const intentTrainingsData: { [key: string]: string[] } = {
 create_new_table: [
  "Create table to store invoices",
  "I need to store my customers information",
  "I need a table for my employees",
 ],
 modify_existing_table: [
  "Customers table should also have an address",
  "Add address to the customer table",
  "Invoice should have a date",
 ],
 link_two_tables: [
  "Customer should have multiple invoices",
  "Each employee should be responsible for multiple customers",
 ],
 do_not_know: [
  "How are you today?",
  "What is your name?",
  "What is the weather today?",
 ],
}
```
Este conjunto de entrenamiento es para enseñar al modelo el reconocimiento de la intención del usuario. La validación de entrenamiento permutada se vería así:

```
[
 {
  training: {
   create_new_table: [
    "Create table to store invoices",
    "I need to store my customers information",
   ],
   modify_existing_table: [
    "Customers table should also have an address",
    "Add address to the customer table",
   ],
   link_two_tables: [
    "Customer should have multiple invoices",
   ],
   do_not_know: [
    "How are you today?",
    "What is your name?",
    "What is the weather today?",
    "2 + 3",
   ],
  },
  evaluations: {
   create_new_table: [
     "I need a table for my employees",
   ],
   modify_existing_table: [
     "Invoice should have a date",
   ],
   link_two_tables: [
    "Customer should have multiple invoices",
    "Each employee should be responsible for multiple customers",
   ],
   do_not_know: [
    "How are you today?",
    "What is your name?",
   ],
  },
 },
 {
  training: {
   create_new_table: [
    "Create table to store invoices",
    "I need to store my customers information",
    "I need a table for my employees",
   ],
   modify_existing_table: [
    "Customers table should also have an address",
    "Add address to the customer table",
    "Invoice should have a date",
   ],
   link_two_tables: [
    "Each employee should be responsible for multiple customers",
   ],
   do_not_know: [
    "How are you today?",
    "What is your name?",
    "What is the weather today?",
    "2 + 3",
   ],
  },
  evaluations: {
   create_new_table: [
    "Create table to store invoices",
    "I need to store my customers information",
    "I need a table for my employees",
   ],
   modify_existing_table: [
    "Customers table should also have an address",
    "Add address to the customer table",
    "Invoice should have a date",
   ],
   link_two_tables: [
    "Customer should have multiple invoices",
    "Each employee should be responsible for multiple customers",
   ],
   do_not_know: [
    "What is the weather today?",
   ],
  },
 },
]
```
##Respuesta dorada
Esta es una idea de la comunidad de IA que promete. Aunque personalmente aún no la he probado, el concepto es comparar la respuesta con una "respuesta dorada" para asegurar su corrección. La respuesta dorada puede compararse con la respuesta real utilizando el mismo Modelo de Aprendizaje de Lenguaje (LLM). Podemos determinar entonces si son idénticas o muy similares. Este enfoque tiene potencial y estoy ansioso por probarlo pronto.

##Agradecimientos
Quisiera expresar mi gratitud a la **Comunidad de AWS** por su incasable apoyo y ayuda.

Quisiera dar un agradecimiento especial a [Chris Miller](https://www.linkedin.com/in/chris-t-miller) por echarme un cable con el enfoque de validación. A [Neylson Crepalde](https://www.linkedin.com/in/neylsoncrepalde/) por explicarme el método de validación de respuesta dorada.

Una vez más, gracias a todos por su apoyo y contribuciones.

##Conclusión
Trabajar con **AWS Bedrock** es increíblemente placentero. El campo de la IA está en constante evolución, y siempre hay algo nuevo que aprender. En esta publicación, hemos explicado cómo validar parcialmente tus respuestas de LLM.

Espero que hayáis encontrado útil esta publicación y si queréis saber más, podéis encontrar info en mi GitHub: https://github.com/mmuller88. 

Si te gusta lo que ves, y te apetece apoyarme:

[Buy Me a Coffee](https://ko-fi.com/T6T1BR59W)
[Become My Patron](https://www.patreon.com/bePatron?u=29010217)
[Web Martin Mueller](https://martinmueller.dev/resume)

Nos vemos en la siguiente!