Stay informed and never miss an Core update!
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Por la naturaleza no determinística de los modelos generativos de IA, los agentes inteligentes deben ser guiados y controlados para que su resultado sea el esperado.
En la Parte I de esta serie de artículos describimos un primer enfoque en el uso de multiagentes de IA utilizando el marco de trabajo AutoGen para automatizar la gestión de un trámite de “informe de dominio” y mejorar la capacidad de atención a ciudadanos de un organismo del sector público.
Debido a que según las pruebas realizadas, ciertos resultados no fueron satisfactorios en cuanto a la precisión de la información extraída de documentos de baja resolución, emprendimos una serie de mejoras y estrategias para guiar y controlar el resultado de los multiagentes de IA.
En esta parte II describimos dichas estrategias.
Estrategia #1 - Uso de diferentes patrones de coordinación entre agentes
Un patrón de coordinación multiagente es un modelo específico que describe cómo los agentes interactúan entre sí para resolver problemas y cómo se define el alcance y el flujo del contexto entre los agentes. Algunos artículos interesantes describen en detalle los patrones de coordinación, específicamente en AutoGen 1, y en general, con cierta analogía con los modelos de concurrencia según Akka 2. Adicionalmente, hay un curso en línea3 que explica cómo usar cada patrón de coordinación de AutoGen.
Como vimos en la parte I, en nuestro primer ejemplo usamos el patrón Secuencial donde el control se transfiere entre agentes, por ejemplo, del user_proxy al OCR_agent y éste vuelve al user_proxy. Solo un agente está activo a la vez. El contexto se acumula o transforma a medida que avanza por la cadena, hasta llegar al resultado final.
Debido a que el marco de trabajo AutoGen nos permite distintos patrones de coordinación entre los agentes inteligentes, realizamos distintas pruebas utilizando cada uno de ellos.
El segundo patrón que usamos es el de Delegación, donde un coordinador asigna subtareas a otros agentes trabajadores. Los trabajadores operan en contextos aislados. Los resultados se procesan para su síntesis.
Dentro de este patrón, podemos mencionar, el uso de GroupChat o chat grupal, el cual es un patrón de diseño donde un grupo de agentes comparte un hilo de mensajes común: todos se suscriben y publican en el mismo tópico. Cada agente participante está especializado en una tarea específica. Los participantes se turnan para publicar mensajes, y el proceso es secuencial: solo un agente trabaja a la vez. Internamente, el orden de los turnos lo mantiene un agente coordinador del chat grupal, que selecciona al siguiente agente para hablar al recibir un mensaje. El algoritmo exacto para seleccionar al siguiente agente puede variar según los requisitos de la aplicación. En nuestro caso, utilizamos el algoritmo de turno rotatorio (round robin).
A continuación, la porción de código donde definimos el agente coordinador del grupo de chat:
self.manager = GroupChatManager(
groupchat=GroupChat(
agents=[self.user_proxy, self.ocr_agent, self.formatter_agent],
messages=[],
max_round=5,
speaker_selection_method="round_robin",
),
llm_config=self.llm_config,
system_message=(
"""You are the group manager responsible for coordinating a two-step process to convert pdf or images files to structured JSON responses.
Step 1: Ask the OCR_Agent to extract the text from the image or PDF file.
Step 2: Once the OCR_Agent provides the extracted text, if confidence > 0.8 send that content to the Formatter_Agent to generate response in JSON format.
Make sure the process follows this order strictly:
1. OCR_Agent → extract raw text.
2. Formatter_Agent → create JSON output from the extracted text.
"""
)
)
Y la definición de la función que inicia el llamado al mismo:
def process_file(self, file_path: str):
message = f"extracts text from file '{file_path}', and provides it in raw text format."
response = self.user_proxy.initiate_chat(self.manager, message=message)
return response.chat_history[-1]['content']
También probamos otros patrones de coordinación de Delegación tales como:
Estos patrones los implementamos usando la librería de AutoGen llamada Agentchat4, una API de alto nivel para la creación de aplicaciones multiagentes. Está construida sobre el paquete autogen-core. Al final de la serie de artículos, compartimos el repositorio con los ejemplos.
Con estos cambios, pudimos comprobar cómo los mensajes se intercambian entre los agentes, las conversaciones explícitas entre ellos, con sus idas y vueltas. Observamos que, dependiendo del patrón usado, puede que la conversación sea más extensa que en otro, y en algunos casos, incluso, la conversación no terminaba y debíamos cortarla desde afuera, o con la intervención de una persona (human-in-the-loop).
En resumen, con esta estrategia de cambio no logramos mejorar o resolver el problema de la precisión en los resultados, esto es, porque el problema de la precisión, se encontraba en el reconocimiento de los textos, no en la interacción entre agentes.
Estrategia #2 - Uso de generación aumentada de recuperación (RAG)
Debido a que uno de los datos más importante a extraer es el número de identificación del titular (CUIL/CUIT), que éste era un problema con imágenes de baja resolución, y que podíamos contar previamente con una lista de CUIL/CUIT y DNI (datos de identificación personal), se nos ocurrió intentar mejorar la recuperación de datos de los modelos con datos aumentados mediante la técnica de RAG (Retrieval Augmented Generation)5.
Para implementar esta estrategia usamos una base de datos vectorial de código abierto y estándar en el mundo python: ChromaDB6, es la base de datos vectorial predeterminada que se utiliza para las aplicaciones de Generación Aumentada por Recuperación (RAG) dentro del marco AutoGen, lo que permite a los agentes de IA tener memoria a largo plazo y acceder a conocimientos personalizados.
Previamente, poblamos la nueva base de datos vectorial con los valores de CUIL/CUIT, mediante un proceso de ingesta desde un archivo .csv, para hacerlo simple, usamos sólo 3 datos o columnas: CUIL/CUIT, DNI, Nombre y Apellido. El proceso de ingesta, convierte estos valores en vectores a través de una técnica de embeddings usando un modelo específico “all-MiniLM-L6-v2”, cuyo detalle está fuera del alcance de este artículo, pero al final de la serie, se puede consultar el código fuente en el repositorio compartido.
Una vez poblada la base vectorial, con sólo tres líneas instanciamos la misma para su uso:
from autogen.agentchat.contrib.vectordb.chromadb import ChromaVectorDB
chroma = ChromaVectorDB(path="./chroma_db")
collection = chroma.get_collection("cuits_collection")
Luego, reemplazamos la definición del agente user_proxy por rag_proxy_agent de la siguiente manera:
rag_proxy_agent = RetrieveUserProxyAgent(
name="Rag_Proxy_Agent",
is_termination_msg=lambda x: x.get("content", "").rstrip().endswith("TERMINATE"),
human_input_mode="NEVER",
code_execution_config=False,
system_message = "Context retrieval assistant.",
retrieve_config={
"task": "qa",
"docs_path": None,
"vector_db": chroma,
"model": "qwen2.5vl:7b",
"embedding_model": "all-MiniLM-L6-v2",
"collection_name": "cuits_collection",
"update_context": True,
"context_max_tokens": 10000,
"get_or_create": False
}
)
Como resultados de la aplicación de esta estrategia, podemos destacar lo siguiente:
Lo anterior, nos llevó a continuar explorando otras alternativas y estrategias.
Estrategia #3 - Uso de reglas de validación + herramientas de extracción
Continuando con la búsqueda de mejorar la precisión en la extracción de información clave desde archivos de imágenes, encontramos algunos caminos que nos acercan a dicho objetivo.
En primer lugar, es posible validar un CUIL/CUIT mediante un algoritmo10, esto nos permite verificar, de una manera rápida y sencilla, aunque no perfecta, si la extracción de dicha información, es válida.
A continuación, la porción de código para implementar el algoritmo, gentileza de la comunidad Python de Argentina11:
# CUIL/CUIT Validation function (without dashes)
# Python Community from Argentina - https://wiki.python.org.ar/recetario/validarcuit/
def validar_cuit(cuil: str) -> bool:
# minimum validations
if len(cuil) != 11:
return False
base = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2]
# calculation of the check digit:
aux = 0
for i in range(10):
aux += int(cuil[i]) * base[i]
aux = 11 - (aux - (int(aux / 11) * 11))
if aux == 11:
aux = 0
if aux == 10:
aux = 9
return aux == int(cuil[10])
Dentro de la función run_ocr que realiza la extracción de los datos, implementamos la lógica de parseo del valor de CUIL/CUIT y lo validamos con el algoritmo mediante una función.
Teniendo la validación de este valor de identificación nos permitía inferir, si la extracción fue correcta, al menos para los valores más críticos, y con ello, proseguir con la entrega de resultados. En caso que la validación no fuera exitosa, teníamos la opción de abortar el flujo de trabajo enviando un error para un procesamiento manual posterior, o bien, usar para esos casos, alguna otra herramienta de extracción más precisa.
Seguimos este último enfoque, mediante el empleo de una potente herramienta de extracción de documentos, basada en agentes inteligentes, llamada ADE - Agentic Document Extraction12 de la firma Landing.ai.
Las pruebas realizadas con la solución de ADE mostraron resultados muy satisfactorios, tanto en precisión en la extracción de distintos formatos de documentos, como en tiempos de respuesta.
Si bien, ADE, al ser un servicio de nube consumido a través de una API, no cumple uno de los requerimientos no funcionales: “no revelar información personal (PII) del Ciudadano”, y además, la misma tiene un costo por procesamiento de documentos, el uso de la misma quedó condicionado exclusivamente a los casos de una extracción no válida con VLMs, es decir, para las excepciones donde la validación del CUIL/CUIT no es superada. Adicionalmente y como atenuante, la firma asegura que no conserva información personal privada, a través de la opción de ZDR - Zero Data Retention13.
Como un elemento de control adicional, previo a verificar el CUIL y a llamar a la función de ADE, se agregó en el prompt de la función run_ocr() la verificación de que en el documento existe un párrafo que comienza con “El Registro Nacional de la Propiedad del Automotor”, esto es, para evitar llamar al servicio ADE, que tiene costo, si el documento no se corresponde con el indicado.
En cuanto a los cambios en nuestro código, es muy simple el uso y llamado al servicio.
A continuación, un fragmento del código modificado de la función run_ocr que incluye el agregado de la lógica de parseo del dato de CUIL/CUIT a validar, y en caso de no ser válido el mismo, el llamado a la API de ADE.
def run_ocr(file_path: str) -> str:
ocr = OCRProcessor(model_name='qwen2.5vl:7b')
result = ocr.process_image(
image_path=file_path,
format_type="markdown",
custom_prompt= """ Extract all visible text from this image in Spanish **without any changes**.
- **Do not summarize, paraphrase, or infer missing text.**
- Retain all spacing, punctuation, and formatting exactly as in the image.
- If the text is unclear or partially visible, extract as much as possible without guessing.
- **Include all text, even if it seems irrelevant or repeated.**
- Check that there exists a paragraph that starts with 'El Registro Nacional de la Propiedad del Automotor'. If it is not present or readable, reply "TERMINATE".
- The value for the key 'Dominio' is located between 'número de dominio' and 'el Automotor'.
- The value for the key 'Nombre' is located after 'Nombre:' in the section "TITULAR" only.
- The value for the key 'Cuil' is located after 'Cuil:'.
- The value for the key 'Nro. Doc.:' is located after 'Nro. Doc.:" in the section "TITULAR" only.
- Extract the rest of the keys-values you can find in the full text.""", # Optional custom prompt
language="Spanish"
)
data = {}
for line in result.strip().split('\n'):
match = re.match(r'^(.*?):\s*(.*)$', line)
if match:
key = match.group(1).strip()
value = match.group(2).strip()
data[key] = value
print(data['Cuil'])
is_cuil = validar_cuit(data['Cuil'])
if not is_cuil:
result = agentic_doc_extraction(file_path, ExtractedFields) # print ("Using ADE services for better extraction")
return result
# Agentic Document Extraction
# Landing.ai - https://landing.ai/
def agentic_doc_extraction(file_path: str, extraction_model: BaseModel) -> str:
result = parse(file_path)
print(result[0].markdown)
return result[0].markdown
Resultados
Las pruebas con ADE - Agentic Document Extraction de Landing.ai lograron alcanzar una mayor precisión ~0.9999%, y esto nos permite reducir considerablemente la posibilidad de error en el procesamiento, y la consecuente devolución del trámite para un procesamiento manual.
Por otro lado, teniendo en cuenta un costo de usd 0.03 por página procesada, el costo de usar esta herramienta es razonable para un estimado de error del 10% del volumen total de trámites de alrededor de 1.500 diarios, esto es, sólo para aquellos casos donde los VLMs locales de Ollama no reconozcan correctamente la información de los documentos.
Luego, continuamos explorando otras librerías y soluciones alternativas de extracción de documentos, entre ellas, la solución de código abierto desarrollada por IBM para el procesamiento de documentos llamada docling14 15, sin embargo, los resultados obtenidos con ADE de Landing.ai, continuaron siendo superiores.
A continuación, el flujo completo implementado durante esta prueba de concepto, que incluye la interacción de distintos agentes inteligentes, llamadas a herramientas y servicios, y la aplicación de distintos controles para guiar y acotar la naturaleza no determinística de los modelos:
%207.38.31%E2%80%AFp.%C2%A0m..png)
Resumen y lecciones aprendidas
Podemos concluir que con esta prueba de concepto se logró el objetivo planteado del caso de uso de automatizar el procesamiento de trámites de “Informe de transferencia de dominio” para evitar la intervención humana en la lectura y análisis de título del automotor. Para ello, se utilizaron agentes inteligentes del marco de trabajo de AutoGen, se implementaron diferentes estrategias con varias iteraciones, cada una, con sus pros y sus contras, como se observa en el cuadro que sigue:
%207.33.04%E2%80%AFp.%C2%A0m..png)
Como lecciones aprendidas de esta prueba de concepto, podemos indicar:
En el próximo artículo, Parte III, describimos algunas consideraciones importantes y frameworks disponibles para entornos productivos que brindan garantías de resiliencia, disponibilidad, seguridad, etc.
Créditos: Foto de portada Shubham Dhage en Unsplash
Referencias:
1-Patrones de conversación de AutoGen - AutoGen Conversation Patterns, https://www.gettingstarted.ai/autogen-conversation-patterns-workflows/
2-Patrones de coordinación multiagentes - Akka Multi-agent Patterns,
https://www.linkedin.com/posts/tylerjewell_agenticai-ai-akka-activity-7434604332383272960-Z7Gk/
3-Patrones de diseño de agentes de IA con AutoGen: https://learn.deeplearning.ai/courses/ai-agentic-design-patterns-with-autogen/lesson/pcet5/introduction
4-API Agenchat: https://microsoft.github.io/autogen/stable//user-guide/agentchat-user-guide/index.html
5-RAG: https://cloud.google.com/use-cases/retrieval-augmented-generation
6-ChromaDB: https://www.trychroma.com/
7- Vision RAG sucks - Text is Still King: https://chunkr.ai/blog/vision-rag-sucks-text-is-still-king
8- OCRBench: On the Hidden Mystery of OCR in Large Multimodal Models: https://arxiv.org/abs/2305.07895
9- Losing Visual Needles in Image Haystacks: Vision Language Models are Easily Distracted in Short and Long Contexts: https://arxiv.org/abs/2406.16851
10-Clave Única de Identificación Tributaria: https://es.wikipedia.org/wiki/Clave_%C3%9Anica_de_Identificaci%C3%B3n_Tributaria
11-Algoritmo python para validar CUIL/CUIT: https://wiki.python.org.ar/recetario/validarcuit/
12- Agentic Document Extraction: https://landing.ai/ade
13- ZDR - Zero Data Retention: https://docs.landing.ai/ade/zdr
14- Docling documentación: https://www.docling.ai/
15- Docling Repo: https://github.com/docling-project/docling
16- Microsoft Semantic Kernel: https://learn.microsoft.com/en-us/semantic-kernel/overview/