Skip to main content
ai voice11 de marzo de 202616 min de lectura

Cómo Automatizamos el Matching de Socios con IA para más de 17.000 Usuarios

La ingeniería detrás del motor de matching de socios con IA de onSpark. Desde el onboarding por voz hasta el matching basado en vectores, sirviendo a más de 17.000 profesionales.

Loic Bachellerie

Senior Product Engineer

El Problema que Nadie Me Advirtió

Cuando comencé a construir onSpark, pensé que lo difícil era lograr que los profesionales se registraran. Estaba equivocado. Lo difícil era hacer que su primera coincidencia se sintiera como si la hubiera hecho alguien que realmente leyó su perfil.

La curación manual no escala. Con 200 usuarios se puede evaluar la compatibilidad a simple vista. Con 2.000 se está adivinando. Con 17.000 se está ahogando. El equipo fundador intentó con etiquetas, categorías y búsqueda de texto libre. Los tres produjeron el mismo resultado: presentaciones de baja calidad, bajas tasas de respuesta y usuarios que abandonaban diciendo que la plataforma "no los entendía".

Ese fue el brief que heredé cuando me trajeron para reconstruir la capa de matching. Este artículo es un recorrido técnico completo de lo que construimos: un sistema de onboarding por voz respaldado por embeddings de OpenAI, búsqueda vectorial con Pinecone y un scorer de compatibilidad multifactorial. El stack se ejecuta sobre un frontend en Angular con un backend en Node.js desplegado en Google Cloud Run.

Para cuando lanzamos la versión 2, las tasas de aceptación de matches habían aumentado del 14% al 61%. Así es como lo logramos.


El Desafío: Matching de Socios a Escala

El matching de socios es más difícil que el matching laboral o de citas por una razón específica: la superficie de compatibilidad es enorme. Dos profesionales pueden compartir una industria, un tamaño de audiencia y un objetivo de crecimiento, pero uno quiere un acuerdo de co-promoción mientras el otro quiere un revenue share. Son socios incompatibles independientemente de cualquier otra señal.

El enfoque clásico, filtros por facetas más un ordenamiento por relevancia, falla porque:

  • Los usuarios se describen de manera inconsistente. Una persona dice "fundador SaaS", otra dice "emprendedor de software B2B". Ambas son válidas. Ninguna encuentra a la otra mediante búsqueda por palabras clave.
  • Los objetivos cambian. Alguien que se registró buscando un invitado para podcast ahora quiere un socio para webinars conjuntos. Los perfiles estáticos se vuelven obsoletos inmediatamente.
  • La confianza es asimétrica. Un usuario nuevo con 500 seguidores en LinkedIn y un lanzamiento de producto verificado es mejor socio que una cuenta antigua con 50.000 seguidores y sin activos verificables.

Necesitábamos un sistema que entendiera significado, no palabras clave, y que pudiera clasificar candidatos por un puntaje compuesto en lugar de una sola métrica de similitud. Eso nos llevó hacia embeddings y búsqueda vectorial desde el primer día.


Onboarding Basado en Voz con Vapi.ai

El primer insight que lo cambió todo: los datos de perfil más ricos no provienen de formularios. Provienen de conversaciones.

Cuando cambiamos de un formulario de registro de 12 campos a una entrevista de voz con IA de 7 minutos, la completitud promedio del perfil pasó del 38% al 91%. Más importante aún, los datos que extrajimos eran cualitativamente diferentes. Las personas le contaron a la IA cosas que nunca escribirían en un campo de texto.

Por Qué la Voz Supera a los Formularios

Los formularios optimizan para velocidad de completado. Los usuarios abrevian, omiten campos opcionales y pegan texto genérico. Una entrevista conversacional es diferente porque una buena pregunta de seguimiento saca a la luz el detalle que el usuario no pensó en ofrecer voluntariamente.

Por ejemplo: un usuario escribe "Dirijo un newsletter" en un campo de formulario. El asistente de Vapi.ai escucha eso y pregunta: "¿Qué temas cubre y quién es su lector típico?" La respuesta - "Escribo sobre operaciones para marcas de Shopify que facturan entre uno y cinco millones en ingresos" - es infinitamente más útil para el matching.

La Arquitectura de la Entrevista

Construimos el asistente de onboarding sobre Vapi.ai porque nos proporcionó una integración limpia de function-calling con nuestro propio backend. El asistente sigue una guía de entrevista estructurada pero conversacional que cubre cuatro dominios:

  1. Identidad principal - rol, industria, etapa del negocio
  2. Activos para partnerships - audiencia, alcance, relaciones existentes, canales de contenido
  3. Objetivos de partnerships - qué quieren obtener de las colaboraciones en los próximos 90 días
  4. Preferencias de acuerdos - disposición para revenue share, compromiso de tiempo, requisitos de exclusividad

La configuración de Vapi para el asistente se ve así:

// vapi-onboarding-assistant.config.ts
import type { CreateAssistantDTO } from "@vapi-ai/server-sdk";
 
export const onboardingAssistantConfig: CreateAssistantDTO = {
  name: "onSpark Onboarding",
  firstMessage:
    "Hi, I'm here to build your partnership profile. " +
    "This takes about seven minutes and everything you share helps us find you better matches. " +
    "Let's start - what does your business do, and who do you serve?",
  model: {
    provider: "openai",
    model: "gpt-4o",
    temperature: 0.3,
    systemPrompt: ONBOARDING_SYSTEM_PROMPT,
    tools: [
      {
        type: "function",
        function: {
          name: "save_profile_section",
          description:
            "Save a completed section of the user's partnership profile. " +
            "Call this after gathering sufficient information for each domain.",
          parameters: {
            type: "object",
            properties: {
              section: {
                type: "string",
                enum: ["identity", "assets", "goals", "deal_preferences"],
              },
              data: {
                type: "object",
                description: "Structured data extracted from the conversation",
              },
              confidence: {
                type: "number",
                description: "0-1 confidence score for the extracted data",
              },
            },
            required: ["section", "data", "confidence"],
          },
        },
      },
      {
        type: "function",
        function: {
          name: "complete_onboarding",
          description:
            "Mark onboarding as complete and trigger profile embedding generation.",
          parameters: {
            type: "object",
            properties: {
              summary: {
                type: "string",
                description:
                  "A 2-3 sentence plain-language summary of the user's partnership profile.",
              },
            },
            required: ["summary"],
          },
        },
      },
    ],
  },
  voice: {
    provider: "11labs",
    voiceId: "21m00Tcm4TlvDq8ikWAM",
  },
  endCallFunctionEnabled: true,
  recordingEnabled: true,
  transcriptPlan: {
    enabled: true,
  },
};

La función save_profile_section se ejecuta incrementalmente durante la llamada. Para cuando el usuario cuelga, tenemos JSON estructurado cubriendo los cuatro dominios. Sin necesidad de post-procesamiento.

Extracción de Datos Estructurados del Transcript

Incluso con function calling, los datos crudos de cada sección necesitaban normalización antes del embedding. Ejecutamos un segundo pase de LLM para producir un objeto de perfil canónico:

// profile-extractor.service.ts
import OpenAI from "openai";
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 
interface RawProfileSection {
  section: string;
  data: Record<string, unknown>;
  confidence: number;
}
 
interface NormalizedProfile {
  userId: string;
  industry: string;
  businessStage: "idea" | "mvp" | "growth" | "scale";
  audienceSize: number;
  audienceDescription: string;
  contentChannels: string[];
  partnershipGoals: string[];
  dealTypes: string[];
  timeCommitmentHoursPerMonth: number;
  revenueShareWilling: boolean;
  summary: string;
  rawSections: RawProfileSection[];
  extractedAt: string;
}
 
export async function normalizeProfileSections(
  userId: string,
  sections: RawProfileSection[],
  callSummary: string
): Promise<NormalizedProfile> {
  const completion = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    temperature: 0,
    response_format: { type: "json_object" },
    messages: [
      {
        role: "system",
        content:
          "You are a data normalization assistant. Given raw profile sections " +
          "from a voice interview, produce a single canonical JSON profile object " +
          "matching the specified schema exactly. Infer missing numeric fields " +
          "from context where possible.",
      },
      {
        role: "user",
        content: JSON.stringify({ sections, callSummary }),
      },
    ],
  });
 
  const parsed = JSON.parse(
    completion.choices[0].message.content ?? "{}"
  ) as Omit<NormalizedProfile, "userId" | "rawSections" | "extractedAt">;
 
  return {
    ...parsed,
    userId,
    rawSections: sections,
    extractedAt: new Date().toISOString(),
  };
}

Construcción del Motor de Matching

Con los perfiles normalizados almacenados en Firestore, la siguiente capa fue el motor de matching en sí. El objetivo de diseño era simple de enunciar y difícil de ejecutar: dado un usuario, devolver los top N socios compatibles clasificados por un puntaje que capture más que similitud textual.

Embedding de Perfiles con OpenAI

La entrada del embedding no es el JSON crudo del perfil. Eso codificaría nombres de campos y estructura en el vector, lo cual es ruido. En su lugar, renderizamos cada perfil en un pasaje rico en lenguaje natural antes de generar el embedding:

// profile-passage.builder.ts
export function buildProfilePassage(profile: NormalizedProfile): string {
  const goalsList = profile.partnershipGoals.join(", ");
  const channelsList = profile.contentChannels.join(", ");
  const dealsList = profile.dealTypes.join(", ");
 
  return [
    `${profile.businessStage} stage business in the ${profile.industry} industry.`,
    `Audience: ${profile.audienceDescription} (approximately ${profile.audienceSize.toLocaleString()} people).`,
    `Primary content channels: ${channelsList}.`,
    `Partnership goals for the next 90 days: ${goalsList}.`,
    `Open to the following deal types: ${dealsList}.`,
    `Available approximately ${profile.timeCommitmentHoursPerMonth} hours per month for partnership work.`,
    profile.revenueShareWilling
      ? "Open to revenue share arrangements."
      : "Prefers non-revenue-share arrangements.",
    `Summary: ${profile.summary}`,
  ]
    .filter(Boolean)
    .join(" ");
}

Luego generamos el embedding de este pasaje usando text-embedding-3-large, que produce vectores de 3.072 dimensiones. Para costos de almacenamiento, truncamos a 1.536 dimensiones - la primera mitad del vector retiene ~96% de la calidad de recuperación en nuestros benchmarks, y reduce a la mitad los costos de almacenamiento en Pinecone.

// embedding.service.ts
import OpenAI from "openai";
 
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 
const EMBEDDING_DIMENSIONS = 1536;
 
export async function generateProfileEmbedding(
  passage: string
): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: "text-embedding-3-large",
    input: passage,
    dimensions: EMBEDDING_DIMENSIONS,
  });
 
  return response.data[0].embedding;
}

Almacenamiento de Vectores en Pinecone

Cada registro de Pinecone almacena el embedding junto con un payload de metadata que permite pre-filtrado antes de la búsqueda por similitud. El pre-filtrado es crítico con más de 17.000 registros - sin él, cada consulta escanea el índice completo.

// pinecone.service.ts
import { Pinecone } from "@pinecone-database/pinecone";
 
const pinecone = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const index = pinecone.index(process.env.PINECONE_INDEX_NAME!);
 
interface ProfileMetadata {
  userId: string;
  industry: string;
  businessStage: string;
  audienceSize: number;
  revenueShareWilling: boolean;
  dealTypes: string[];
  updatedAt: number;
}
 
export async function upsertProfileVector(
  profile: NormalizedProfile,
  embedding: number[]
): Promise<void> {
  const metadata: ProfileMetadata = {
    userId: profile.userId,
    industry: profile.industry,
    businessStage: profile.businessStage,
    audienceSize: profile.audienceSize,
    revenueShareWilling: profile.revenueShareWilling,
    dealTypes: profile.dealTypes,
    updatedAt: Date.now(),
  };
 
  await index.upsert([
    {
      id: profile.userId,
      values: embedding,
      metadata,
    },
  ]);
}
 
export interface MatchCandidate {
  userId: string;
  cosineSimilarity: number;
  metadata: ProfileMetadata;
}
 
export async function findSimilarProfiles(
  queryEmbedding: number[],
  requestingUserId: string,
  filters: Partial<ProfileMetadata>,
  topK = 50
): Promise<MatchCandidate[]> {
  const queryResponse = await index.query({
    vector: queryEmbedding,
    topK,
    includeMetadata: true,
    filter: buildPineconeFilter(filters),
  });
 
  return (queryResponse.matches ?? [])
    .filter((m) => m.id !== requestingUserId)
    .map((m) => ({
      userId: m.id,
      cosineSimilarity: m.score ?? 0,
      metadata: m.metadata as ProfileMetadata,
    }));
}
 
function buildPineconeFilter(
  filters: Partial<ProfileMetadata>
): Record<string, unknown> {
  const conditions: Record<string, unknown>[] = [];
 
  if (filters.industry) {
    conditions.push({ industry: { $eq: filters.industry } });
  }
 
  if (filters.audienceSize) {
    conditions.push({
      audienceSize: {
        $gte: filters.audienceSize * 0.1,
        $lte: filters.audienceSize * 10,
      },
    });
  }
 
  if (!conditions.length) return {};
 
  return conditions.length === 1 ? conditions[0] : { $and: conditions };
}

Puntuación y Clasificación

La similitud coseno cruda de Pinecone es una señal fuerte pero no la historia completa. Agregamos tres dimensiones adicionales en un puntaje final de compatibilidad: alineación de objetivos, compatibilidad de acuerdos e índice de confianza.

La Arquitectura del Sistema

Arquitectura del Motor de Matching de onSpark

Desde la llamada de voz hasta los resultados de matches clasificados

Capa 1: Ingesta
Entrevista de voz con Vapi.ai
Extracción por function call
Normalización con GPT-4o-mini
Almacenamiento de perfiles en Firestore
Capa 2: Embedding
Constructor de pasajes
text-embedding-3-large
Truncamiento a 1.536 dimensiones
Upsert en Pinecone
Capa 3: Clasificación
Consulta de similitud en Pinecone
Scorer de alineación de objetivos
Scorer de compatibilidad de acuerdos
Scorer de índice de confianza
Angular Frontend - Cloud Run API - Firebase Auth

Cálculo del Puntaje de Compatibilidad

El puntaje final es una suma ponderada de cuatro componentes:

// compatibility-scorer.ts
interface ScoringWeights {
  semanticSimilarity: number;
  goalAlignment: number;
  dealCompatibility: number;
  trustIndex: number;
}
 
const DEFAULT_WEIGHTS: ScoringWeights = {
  semanticSimilarity: 0.4,
  goalAlignment: 0.3,
  dealCompatibility: 0.2,
  trustIndex: 0.1,
};
 
interface CompatibilityResult {
  userId: string;
  finalScore: number;
  breakdown: {
    semantic: number;
    goals: number;
    deals: number;
    trust: number;
  };
  explanation: string;
}
 
export function computeCompatibilityScore(
  requester: NormalizedProfile,
  candidate: MatchCandidate,
  candidateProfile: NormalizedProfile,
  trustScore: number,
  weights: ScoringWeights = DEFAULT_WEIGHTS
): CompatibilityResult {
  const semantic = candidate.cosineSimilarity;
  const goals = computeGoalAlignment(requester, candidateProfile);
  const deals = computeDealCompatibility(requester, candidateProfile);
  const trust = trustScore;
 
  const finalScore =
    semantic * weights.semanticSimilarity +
    goals * weights.goalAlignment +
    deals * weights.dealCompatibility +
    trust * weights.trustIndex;
 
  return {
    userId: candidate.userId,
    finalScore,
    breakdown: { semantic, goals, deals, trust },
    explanation: buildExplanation(requester, candidateProfile, {
      semantic,
      goals,
      deals,
      trust,
    }),
  };
}
 
function computeGoalAlignment(
  a: NormalizedProfile,
  b: NormalizedProfile
): number {
  // Jaccard similarity on goal token sets
  const setA = new Set(a.partnershipGoals.map(normalizeGoalToken));
  const setB = new Set(b.partnershipGoals.map(normalizeGoalToken));
  const intersection = new Set([...setA].filter((g) => setB.has(g)));
  const union = new Set([...setA, ...setB]);
  return union.size === 0 ? 0 : intersection.size / union.size;
}
 
function computeDealCompatibility(
  a: NormalizedProfile,
  b: NormalizedProfile
): number {
  const sharedDealTypes = a.dealTypes.filter((d) => b.dealTypes.includes(d));
 
  const revenueShareConflict =
    a.revenueShareWilling !== b.revenueShareWilling ? 0.3 : 0;
 
  const timeCompatibility =
    1 -
    Math.abs(
      a.timeCommitmentHoursPerMonth - b.timeCommitmentHoursPerMonth
    ) /
      Math.max(a.timeCommitmentHoursPerMonth, b.timeCommitmentHoursPerMonth, 1);
 
  const dealTypeScore =
    sharedDealTypes.length /
    Math.max(a.dealTypes.length, b.dealTypes.length, 1);
 
  return Math.max(
    0,
    dealTypeScore * 0.5 + timeCompatibility * 0.5 - revenueShareConflict
  );
}
 
function normalizeGoalToken(goal: string): string {
  return goal.toLowerCase().trim().replace(/\s+/g, "_");
}

El Índice de Confianza

El índice de confianza no es un puntaje de reputación - es un puntaje de completitud de señales y verificación. Un perfil gana puntos de confianza por:

  • Completar el onboarding por voz (40 puntos)
  • Verificar un perfil de LinkedIn a través de OAuth (20 puntos)
  • Tener al menos un partnership completado con calificación positiva (20 puntos)
  • Proporcionar métricas de audiencia verificables (cantidad de suscriptores al newsletter, descargas de podcast) (20 puntos)

Normalizamos a 0-1 y lo almacenamos en Firestore junto con el perfil, actualizado en cada evento de verificación.


El Pipeline de Flujo de Acuerdos

El matching produce una lista clasificada. Convertir esa lista en partnerships activos requirió un pipeline de flujo de acuerdos que mantuviera el impulso sin intervención manual.

El pipeline tiene cuatro etapas: Sugerido (match presentado al usuario), Interesado (el usuario señala intención), Presentado (interés mutuo, se envía la presentación), Activo (acuerdo en progreso).

Construimos un trabajo de Cloud Scheduler que se ejecuta cada 6 horas y avanza o expira acuerdos basándose en umbrales de tiempo de respuesta. Si un usuario no actúa sobre un match Sugerido dentro de 72 horas, el match desaparece de su feed y es reemplazado. Esto mantiene el feed fresco y crea una señal leve de urgencia sin patrones oscuros.

El mensaje de presentación es generado por GPT-4o y hace referencia a activos compartidos específicos entre ambas partes. Las versiones tempranas usaban una plantilla genérica. Cambiar a generación personalizada redujo las tasas de ignorar presentaciones del 44% al 18%.

// introduction-generator.service.ts
export async function generateIntroductionMessage(
  sender: NormalizedProfile,
  recipient: NormalizedProfile,
  compatibilityResult: CompatibilityResult
): Promise<string> {
  const prompt = [
    "Write a warm, professional partnership introduction message.",
    "The message is sent from the platform on behalf of the sender to the recipient.",
    "Reference one specific shared goal and one complementary asset.",
    "Keep it under 120 words. Do not use salesy language.",
    "",
    `Sender summary: ${sender.summary}`,
    `Recipient summary: ${recipient.summary}`,
    `Top compatibility reason: ${compatibilityResult.explanation}`,
  ].join("\n");
 
  const completion = await openai.chat.completions.create({
    model: "gpt-4o",
    temperature: 0.6,
    max_tokens: 200,
    messages: [{ role: "user", content: prompt }],
  });
 
  return completion.choices[0].message.content?.trim() ?? "";
}

Desafíos de Escalabilidad

Pasar del prototipo a 17.000 perfiles activos no fue un camino recto. Estos fueron los tres problemas que nos costaron más tiempo de ingeniería.

Latencia del Embedding al Completar el Onboarding

Cuando una llamada de voz termina, el usuario espera ver sus primeras coincidencias en segundos. El pipeline de embedding - normalizar, construir pasaje, generar embedding, hacer upsert en Pinecone - tomaba 3-4 segundos de forma sincrónica, lo cual se sentía lento en la interfaz de Angular.

Lo resolvimos con un enfoque de dos fases. Al finalizar la llamada, inmediatamente devolvemos un estado de carga y mostramos al usuario una pantalla de "construyendo su perfil". Un trabajo de Cloud Tasks ejecuta el pipeline completo de forma asincrónica. El frontend en Angular consulta un endpoint /onboarding-status cada 2 segundos y hace la transición a la vista de matches cuando el trabajo se completa. La latencia percibida bajó de 4 segundos a menos de 1 segundo.

Obsolescencia del Índice Vectorial

Los perfiles cambian. Un usuario que completó el onboarding hace seis meses puede tener objetivos completamente diferentes. Añadimos una señal de frescura de perfil al índice de confianza: los perfiles con más de 90 días sin actualización reciben una penalización de 15 puntos por frescura. Esto posiciona a los usuarios activos recientemente más arriba en los resultados y crea un incentivo natural de re-engagement.

También añadimos un flujo ligero de "actualización rápida" - un check-in de voz de 90 segundos usando el mismo asistente de Vapi - que actualiza solo las secciones de objetivos y preferencias de acuerdos y dispara un re-embedding sin repetir el onboarding completo.

Arranque en Frío para Nuevos Usuarios

Un usuario completamente nuevo sin historial y con un perfil recién creado no tiene señal de comportamiento. Sus primeras coincidencias dependen completamente del embedding semántico. Descubrimos que la calidad de las primeras 3 coincidencias es decisiva para la retención - los usuarios que interactuaron con al menos un match en las primeras 48 horas tenían 4.7 veces más probabilidades de seguir activos a los 30 días.

Para mejorar la calidad del arranque en frío, aumentamos el topK de Pinecone a 100 para usuarios con menos de 7 días, y luego reclasificamos agresivamente usando alineación de objetivos y compatibilidad de acuerdos. Los usuarios nuevos también reciben una bandera manual de "control de calidad" que impide que perfiles incompletos (confianza por debajo de 0.7 en cualquier sección) aparezcan en los feeds de otros usuarios hasta que completen el check-in de actualización rápida.


Resultados y Métricas

Después de seis meses ejecutando la versión 2 en producción, los números se ven así:

  • 17.400 perfiles activos con embeddings completos en Pinecone
  • Tasa de aceptación de matches: 61% (arriba del 14% con el sistema basado en filtros)
  • Tasa de respuesta a presentaciones: 49% (arriba del 22%)
  • Partnerships activos creados: más de 3.200 en toda la base de usuarios
  • Tasa de completación del onboarding por voz: 84% (versus 61% para el formulario)
  • Latencia mediana de matching: 340ms para una lista clasificada de 20 candidatos (p99: 820ms)
  • Costo del pipeline de embedding: $0,0023 por perfil a precios actuales de OpenAI

La latencia mediana de 340ms nos sorprendió. El índice HNSW de Pinecone en un despliegue basado en pods maneja la búsqueda vectorial en menos de 80ms. El tiempo restante se divide entre obtener perfiles de candidatos de Firestore, ejecutar las funciones de puntuación y generar el texto de presentación.

El factor individual más grande del aumento en la tasa de aceptación no fue el modelo de embedding. Fue la calidad de datos del onboarding por voz. Cuando ejecutamos una ablación donde reemplazamos perfiles derivados de voz con perfiles equivalentes llenados por formulario para un subconjunto de usuarios, la tasa de aceptación cayó del 61% al 39%. La profundidad conversacional de los datos de voz es la verdadera ventaja competitiva.


Lecciones Aprendidas

Basura Entra, Basura Sale También Aplica a Embeddings

Un embedding es solo tan bueno como su entrada. El constructor de pasajes no es una capa cosmética - es uno de los componentes más importantes del sistema. Pasamos dos semanas iterando sobre la estructura del pasaje antes de que la calidad del embedding se estabilizara. Si las coincidencias se sienten incorrectas, revise el pasaje antes de tocar el modelo.

Filtre Agresivamente en Pinecone

Sin filtros de metadata, una consulta contra 17.000 registros con topK=50 devuelve resultados de toda la población. Los usuarios que construyen una audiencia de newsletter SaaS B2B no se benefician de ser emparejados con influencers de e-commerce. El pre-filtrado por industria y rango de tamaño de audiencia antes de la similitud semántica reduce los resultados irrelevantes y mejora la precisión percibida. El costo de una consulta pre-filtrada es casi idéntico al de una sin filtrar.

Los Pesos del Puntaje Son una Decisión de Producto, No de Ingeniería

Pasamos demasiado tiempo tratando los pesos del puntaje de compatibilidad como un problema de optimización de ingeniería. No lo son. Reflejan valores de producto: ¿cuánto debería importar el ajuste semántico versus el ajuste de estructura de acuerdo? La respuesta cambia según qué tipo de partnership la plataforma está optimizando. Hable con los usuarios, elija pesos, mida resultados, ajuste. Lance más rápido.

La Voz Está Subestimada como Canal de Recolección de Datos

El campo del onboarding con IA está a punto de cambiar significativamente. El abandono de formularios es un problema conocido. El abandono de voz con un 84% de completación no lo es. Cuando propuse por primera vez cambiar el onboarding a voz, la objeción fue "los usuarios no lo harán". Lo hicieron. La duración modal de nuestras llamadas de onboarding por voz es de 6 minutos y 42 segundos. Nadie llena un formulario de 7 minutos.


Lo Que Estamos Construyendo a Continuación

El sistema actual es reactivo: empareja usuarios basándose en objetivos declarados. La próxima versión será proactiva. Estamos experimentando con una capa de señales que ingesta actividad pública - publicaciones de LinkedIn, apariciones en podcasts, contenido de newsletters - e infiere objetivos no declarados a partir de patrones de comportamiento. Un usuario que comienza a escribir mucho sobre crecimiento en YouTube probablemente está pensando en partnerships de video aunque su perfil aún diga "newsletter".

También estamos explorando embeddings multimodales que combinan el transcript de voz con las características de prosodia del audio - ritmo, energía, pausas - como una señal adicional para compatibilidad de estilo de comunicación. Las pruebas tempranas sugieren que agrega ~4 puntos a la tasa de aceptación por sí solo.


Conclusión

Construir un motor de matching con IA a escala es un problema de sistemas antes de ser un problema de machine learning. La calidad de su recolección de datos, el cuidado en la construcción de sus pasajes y la honestidad de sus pesos de puntuación importan más que qué modelo de embedding elija.

La combinación de Vapi.ai para onboarding por voz, OpenAI para embeddings y normalización, y Pinecone para almacenamiento vectorial nos dio un stack que es tanto asequible como performante con 17.000 usuarios. La arquitectura escala a 10 veces ese número sin cambios estructurales.

Si está construyendo un marketplace, una red profesional o cualquier plataforma donde la calidad de las presentaciones determina la retención, estoy convencido de que este enfoque supera al matching basado en filtros en cualquier escala por encima de unos pocos cientos de usuarios.

Si está trabajando en algo similar y quiere comparar notas sobre la arquitectura, comuníquese directamente. Me interesan particularmente las conversaciones sobre puntuación de confianza, onboarding multimodal y escalar Pinecone más allá del umbral de 100.000 registros.

Share:

Recibe perspectivas prácticas de ingeniería

Agentes de voz con IA, flujos de automatización y lanzamientos rápidos. Sin spam, cancela cuando quieras.