Skip to main content
ai voice11 mars 202616 min de lecture

Comment nous avons automatise le matching de partenariats par IA pour 17K+ utilisateurs

Les coulisses techniques du moteur de matching par IA d'onSpark. De l'onboarding vocal au matching vectoriel, au service de plus de 17 000 professionnels.

Loic Bachellerie

Senior Product Engineer

Le probleme que personne ne m'avait signale

Quand j'ai commence a developper onSpark, je pensais que le plus dur serait de convaincre les professionnels de s'inscrire. Je me trompais. Le vrai defi, c'etait de faire en sorte que leur premier match donne l'impression d'avoir ete fait par quelqu'un ayant reellement lu leur profil.

La curation manuelle ne passe pas a l'echelle. A 200 utilisateurs, on peut evaluer la compatibilite a l'oeil. A 2 000, on devine. A 17 000, on se noie. L'equipe fondatrice avait essaye les tags, les categories et la recherche en texte libre. Les trois approches produisaient le meme resultat : des mises en relation de faible qualite, des taux de reponse bas, et des utilisateurs qui partaient en disant que la plateforme "ne les comprenait pas".

C'est le brief que j'ai herite quand on m'a fait venir pour reconstruire la couche de matching. Cet article est un guide technique complet de ce que nous avons construit : un systeme d'onboarding vocal adosse aux embeddings OpenAI, a la recherche vectorielle Pinecone et a un scorer de compatibilite multi-facteurs. La stack repose sur un frontend Angular avec un backend Node.js deploye sur Google Cloud Run.

Au moment du lancement de la v2, le taux d'acceptation des matchs etait passe de 14 % a 61 %. Voici comment nous y sommes parvenus.


Le defi : matcher des partenaires a grande echelle

Le matching de partenariats est plus difficile que le matching d'emploi ou de rencontres pour une raison precise : la surface de compatibilite est enorme. Deux professionnels peuvent partager un secteur, une taille d'audience et un objectif de croissance, mais l'un veut un accord de co-promotion tandis que l'autre veut un partage de revenus. Ce sont des partenaires incompatibles, quels que soient tous les autres signaux.

L'approche classique, filtres a facettes plus tri par pertinence, echoue parce que :

  • Les utilisateurs se decrivent de maniere incoherente. L'un dit "fondateur SaaS", l'autre dit "entrepreneur logiciel B2B". Les deux sont valides. Aucun ne trouve l'autre par recherche par mots-cles.
  • Les objectifs changent. Quelqu'un qui s'est inscrit pour trouver un invite de podcast veut maintenant un partenaire pour un webinaire commun. Les profils statiques deviennent obsoletes immediatement.
  • La confiance est asymetrique. Un nouvel utilisateur avec 500 abonnes LinkedIn et un lancement de produit verifie est un meilleur partenaire qu'un compte plus ancien avec 50 000 abonnes et aucun actif verifiable.

Nous avions besoin d'un systeme qui comprenne le sens, pas les mots-cles, et qui puisse classer les candidats par un score composite plutot que par une seule metrique de similarite. Cela nous a orientes vers les embeddings et la recherche vectorielle des le premier jour.


Onboarding vocal avec Vapi.ai

La premiere intuition qui a tout change : les donnees de profil les plus riches ne viennent pas des formulaires. Elles viennent de la conversation.

Quand nous sommes passes d'un formulaire d'inscription a 12 champs a un entretien vocal IA de 7 minutes, la completude moyenne des profils est passee de 38 % a 91 %. Plus important encore, les donnees extraites etaient qualitativement differentes. Les gens disaient a l'IA des choses qu'ils n'auraient jamais tapees dans un champ texte.

Pourquoi la voix surpasse les formulaires

Les formulaires optimisent la vitesse de completion. Les utilisateurs abregent, sautent les champs optionnels et collent du texte generique. Un entretien conversationnel est different parce qu'une bonne question de relance fait emerger le detail que l'utilisateur n'avait pas pense a mentionner.

Par exemple : un utilisateur tape "Je gere une newsletter" dans un champ de formulaire. L'assistant Vapi.ai entend cela et demande : "Quels sujets couvrez-vous, et qui est votre lecteur type ?" La reponse - "J'ecris sur les operations pour les marques Shopify realisant entre un et cinq millions de chiffre d'affaires" - est infiniment plus utile pour le matching.

L'architecture de l'entretien

Nous avons construit l'assistant d'onboarding sur Vapi.ai parce qu'il nous offrait une integration propre du function calling avec notre propre backend. L'assistant suit un guide d'entretien structure mais conversationnel couvrant quatre domaines :

  1. Identite principale - role, secteur, stade de l'entreprise
  2. Actifs de partenariat - audience, portee, relations existantes, canaux de contenu
  3. Objectifs de partenariat - ce qu'ils veulent tirer des collaborations dans les 90 prochains jours
  4. Preferences de deal - appetence pour le partage de revenus, engagement en temps, exigences d'exclusivite

La configuration Vapi de l'assistant ressemble a ceci :

// 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 fonction save_profile_section se declenche de maniere incrementale pendant l'appel. Au moment ou l'utilisateur raccroche, nous disposons de JSON structure couvrant les quatre domaines. Aucun post-traitement necessaire.

Extraction de donnees structurees a partir de la transcription

Meme avec le function calling, les donnees brutes de chaque section necessitaient une normalisation avant l'embedding. Nous avons effectue une seconde passe LLM pour produire un objet de profil canonique :

// 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(),
  };
}

Construction du moteur de matching

Avec les profils normalises stockes dans Firestore, la couche suivante etait le moteur de matching lui-meme. L'objectif de conception etait simple a formuler et difficile a realiser : etant donne un utilisateur, retourner les N partenaires les plus compatibles classes par un score capturant plus que la simple similarite textuelle.

Embedding des profils avec OpenAI

L'entree de l'embedding n'est pas le JSON brut du profil. Cela encoderait les noms de champs et la structure dans le vecteur, ce qui constitue du bruit. A la place, nous transformons chaque profil en un passage riche en langage naturel avant l'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(" ");
}

Nous generons ensuite l'embedding de ce passage avec text-embedding-3-large, qui produit des vecteurs a 3 072 dimensions. Pour optimiser les couts de stockage, nous tronquons a 1 536 dimensions - la premiere moitie du vecteur conserve environ 96 % de la qualite de retrieval dans nos benchmarks, et divise par deux les couts de stockage 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;
}

Stockage des vecteurs dans Pinecone

Chaque enregistrement Pinecone stocke l'embedding accompagne de metadonnees qui permettent le pre-filtrage avant la recherche par similarite. Le pre-filtrage est crucial avec plus de 17K enregistrements - sans lui, chaque requete parcourt l'index entier.

// 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 };
}

Scoring et classement

La similarite cosinus brute de Pinecone est un signal fort mais ne raconte pas toute l'histoire. Nous superposons trois dimensions supplementaires dans un score de compatibilite final : alignement des objectifs, compatibilite de deal et indice de confiance.

L'architecture du systeme

Architecture du moteur de matching onSpark

De l'appel vocal aux resultats de matching classes

Couche 1 : Ingestion
Entretien vocal Vapi.ai
Extraction par function call
Normalisation GPT-4o-mini
Stockage profil Firestore
Couche 2 : Embedding
Constructeur de passage
text-embedding-3-large
Troncature 1 536 dimensions
Upsert Pinecone
Couche 3 : Classement
Requete de similarite Pinecone
Scorer d'alignement des objectifs
Scorer de compatibilite de deal
Scorer d'indice de confiance
Angular Frontend - Cloud Run API - Firebase Auth

Calcul du score de compatibilite

Le score final est une somme ponderee de quatre composantes :

// 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, "_");
}

L'indice de confiance

L'indice de confiance n'est pas un score de reputation - c'est un score de completude des signaux et de verification. Un profil accumule des points de confiance pour :

  • Avoir complete l'onboarding vocal (40 points)
  • Avoir verifie un profil LinkedIn via OAuth (20 points)
  • Avoir au moins un partenariat acheve avec une evaluation positive (20 points)
  • Avoir fourni des metriques d'audience verifiables (nombre d'abonnes newsletter, telechargements de podcast) (20 points)

Nous normalisons entre 0 et 1 et stockons le resultat dans Firestore a cote du profil, actualise a chaque evenement de verification.


Le pipeline de deal flow

Le matching produit une liste classee. Convertir cette liste en partenariats actifs a necessite un pipeline de deal flow qui maintient l'elan sans intervention manuelle.

Le pipeline comporte quatre etapes : Suggere (match presente a l'utilisateur), Interesse (l'utilisateur signale son interet), Presente (interet mutuel, introduction envoyee), Actif (deal en cours).

Nous avons mis en place un job Cloud Scheduler qui s'execute toutes les 6 heures et fait avancer ou expire les deals en fonction de seuils de temps de reponse. Si un utilisateur n'agit pas sur un match Suggere dans les 72 heures, le match disparait de son flux et est remplace. Cela maintient le flux a jour et cree un leger signal d'urgence sans recourir a des dark patterns.

Le message d'introduction est genere par GPT-4o et fait reference a des actifs partages specifiques entre les deux parties. Les premieres versions utilisaient un template generique. Le passage a une generation personnalisee a reduit le taux d'ignorance des introductions de 44 % a 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() ?? "";
}

Les defis de mise a l'echelle

Passer du prototype a 17 000 profils actifs n'a pas ete un chemin rectiligne. Voici les trois problemes qui nous ont coute le plus de temps d'ingenierie.

Latence de l'embedding a la fin de l'onboarding

Quand un appel vocal se termine, l'utilisateur s'attend a voir ses premiers matchs en quelques secondes. Le pipeline d'embedding - normalisation, construction du passage, embedding, upsert vers Pinecone - prenait 3 a 4 secondes de maniere synchrone, ce qui semblait lent dans l'interface Angular.

Nous avons resolu cela par une approche en deux phases. A la fin de l'appel, nous retournons immediatement un etat de chargement et affichons un ecran "construction de votre profil en cours". Un job Cloud Tasks execute le pipeline complet de maniere asynchrone. Le frontend Angular interroge un endpoint /onboarding-status toutes les 2 secondes et passe a la vue des matchs lorsque le job est termine. La latence percue est passee de 4 secondes a moins d'1 seconde.

Obsolescence de l'index vectoriel

Les profils evoluent. Un utilisateur ayant complete l'onboarding il y a six mois peut avoir des objectifs entierement differents. Nous avons ajoute un signal de fraicheur de profil a l'indice de confiance : les profils de plus de 90 jours sans mise a jour recoivent une penalite de fraicheur de 15 points. Cela fait remonter les utilisateurs recemment actifs dans les resultats et cree un signal naturel de re-engagement.

Nous avons egalement ajoute un flux de "mise a jour rapide" - un check-in vocal de 90 secondes utilisant le meme assistant Vapi - qui rafraichit uniquement les sections objectifs et preferences de deal, et declenche un re-embedding sans repeter l'onboarding complet.

Demarrage a froid pour les nouveaux utilisateurs

Un tout nouvel utilisateur sans historique et avec un profil tout neuf n'a aucun signal comportemental. Ses premiers matchs reposent entierement sur l'embedding semantique. Nous avons constate que la qualite des 3 premiers matchs est decisive pour la retention - les utilisateurs qui ont interagi avec au moins un match dans les 48 premieres heures avaient 4,7 fois plus de chances d'etre encore actifs a 30 jours.

Pour ameliorer la qualite du demarrage a froid, nous avons augmente le topK de Pinecone a 100 pour les utilisateurs de moins de 7 jours, puis re-classe agressivement en utilisant l'alignement des objectifs et la compatibilite de deal. Les nouveaux utilisateurs recoivent egalement un indicateur "porte de qualite" manuelle qui empeche les profils incomplets (confiance inferieure a 0,7 sur une section) d'apparaitre dans les flux des autres utilisateurs tant qu'ils n'ont pas complete le check-in de mise a jour rapide.


Resultats et metriques

Apres six mois d'exploitation de la v2 en production, les chiffres sont les suivants :

  • 17 400 profils actifs avec des embeddings complets dans Pinecone
  • Taux d'acceptation des matchs : 61 % (contre 14 % avec le systeme a base de filtres)
  • Taux de reponse aux introductions : 49 % (contre 22 %)
  • Partenariats actifs crees : plus de 3 200 sur l'ensemble de la base utilisateurs
  • Taux de completion de l'onboarding vocal : 84 % (contre 61 % pour le formulaire)
  • Latence mediane du matching : 340 ms pour une liste classee de 20 candidats (p99 : 820 ms)
  • Cout du pipeline d'embedding : 0,0023 $ par profil aux tarifs actuels d'OpenAI

La latence mediane de 340 ms nous a surpris. L'index HNSW de Pinecone sur un deploiement pod gere la recherche vectorielle en moins de 80 ms. Le temps restant se repartit entre la recuperation des profils candidats depuis Firestore, l'execution des fonctions de scoring et la generation du texte d'introduction.

Le facteur le plus determinant dans l'amelioration du taux d'acceptation n'etait pas le modele d'embedding. C'etait la qualite des donnees de l'onboarding vocal. Quand nous avons mene une ablation en remplacant les profils issus de la voix par des profils equivalents remplis par formulaire pour un sous-ensemble d'utilisateurs, le taux d'acceptation est passe de 61 % a 39 %. La profondeur conversationnelle des donnees vocales est le veritable avantage competitif.


Lecons apprises

La regle du "garbage in, garbage out" s'applique aussi aux embeddings

Un embedding est aussi bon que son entree. Le constructeur de passage n'est pas une couche cosmetique - c'est l'un des composants les plus importants du systeme. Nous avons passe deux semaines a iterer sur la structure du passage avant que la qualite des embeddings ne se stabilise. Si les matchs semblent incorrects, verifiez le passage avant de toucher au modele.

Pre-filtrez agressivement dans Pinecone

Sans filtres de metadonnees, une requete sur 17K enregistrements avec topK=50 retourne des resultats provenant de toute la population. Les utilisateurs qui construisent une audience de newsletter SaaS B2B n'ont aucun interet a etre matches avec des influenceurs e-commerce. Le pre-filtrage par secteur et tranche de taille d'audience avant la similarite semantique reduit les resultats non pertinents et ameliore la precision percue. Le cout d'une requete pre-filtree est quasi identique a celui d'une requete non filtree.

Les poids du score sont une decision produit, pas une decision d'ingenierie

Nous avons passe trop de temps a traiter les poids du score de compatibilite comme un probleme d'optimisation d'ingenierie. Ce n'en est pas un. Ils refletent les valeurs produit : quelle importance accorder a l'adequation semantique par rapport a l'adequation de la structure du deal ? La reponse change selon le type de partenariat que la plateforme cherche a optimiser. Parlez aux utilisateurs, choisissez des poids, mesurez les resultats, ajustez. Livrez plus vite.

La voix est sous-estimee comme canal de collecte de donnees

Le domaine de l'onboarding par IA est sur le point de changer significativement. L'abandon de formulaire est un probleme connu. L'abandon vocal a 84 % de completion ne l'est pas. Quand j'ai propose pour la premiere fois de passer l'onboarding a la voix, l'objection etait "les utilisateurs ne le feront pas". Ils l'ont fait. La duree modale de nos appels d'onboarding vocal est de 6 minutes et 42 secondes. Personne ne remplit un formulaire de 7 minutes.


Ce que nous construisons ensuite

Le systeme actuel est reactif : il matche les utilisateurs en fonction d'objectifs declares. La prochaine version sera proactive. Nous experimentons avec une couche de signaux qui ingere l'activite publique - posts LinkedIn, apparitions en podcast, contenu de newsletter - et infere des objectifs non exprimes a partir de patterns comportementaux. Un utilisateur qui commence a beaucoup ecrire sur la croissance YouTube pense probablement a des partenariats video meme si son profil indique encore "newsletter".

Nous explorons egalement les embeddings multi-modaux qui combinent la transcription vocale avec les caracteristiques prosodiques de l'audio - rythme, energie, pauses - comme signal supplementaire de compatibilite de style de communication. Les premiers tests suggerent que cela ajoute environ 4 points au taux d'acceptation a lui seul.


Conclusion

Construire un moteur de matching par IA a grande echelle est un probleme de systemes avant d'etre un probleme de machine learning. La qualite de votre collecte de donnees, le soin apporte a la construction de vos passages et l'honnetete de vos poids de scoring comptent plus que le choix du modele d'embedding.

La combinaison de Vapi.ai pour l'onboarding vocal, OpenAI pour les embeddings et la normalisation, et Pinecone pour le stockage vectoriel nous a donne une stack a la fois abordable et performante a 17K utilisateurs. L'architecture peut supporter 10 fois ce nombre sans changements structurels.

Si vous construisez une marketplace, un reseau professionnel, ou toute plateforme ou la qualite des introductions determine la retention, je suis convaincu que cette approche surpasse le matching par filtres a toute echelle au-dela de quelques centaines d'utilisateurs.

Si vous travaillez sur quelque chose de similaire et souhaitez echanger sur l'architecture, contactez-moi directement. Je suis particulierement interesse par les conversations autour du scoring de confiance, de l'onboarding multi-modal et du passage a l'echelle de Pinecone au-dela du seuil des 100K enregistrements.

Share:

Recevez des perspectives d'ingénierie pratiques

Agents vocaux IA, workflows d'automatisation et livraison rapide. Pas de spam, désabonnement à tout moment.