Текстовый бот слеп к 60% B2B-контекста: бюджет, сроки и стек живут в скриншотах CRM и двухминутных голосовых. Разбираем архитектуру мультимодального агента — Qwen2.5-VL, Whisper v3 Turbo с Silero VAD, Pydantic v2 скоринг — и production-код MultimodalLeadScorer, который утраивает Lead-to-Opportunity без ручной разметки.
Мультимодальные AI-агенты в B2B Lead Generation: компьютерное зрение, транскрибация голосовых и скоринг профилей без ручной разметки
1. Введение: проблема гетерогенных лидов в B2B
Реальность B2B-коммуникаций 2025 года не похожа на форму «Имя, компания, email». ЛПР пишет в Telegram с телефона: две минуты голосового на ходу в машине, поверх — скриншот дашборда из 1С с красной строкой дебиторки, следом PDF-смета на 40 страниц и фото рукописной схемы интеграции на салфетке. First Touch строит агентный контур именно под такой входящий поток: не «приведите сообщение к нормальному виду», а «разберите то, что прислали, и квалифицируйте без человека».
Классический текстовый чат-бот на такой поток отвечает фразой «опишите задачу словами» — и теряет контекст. По нашим замерам, до 60% критических сигналов сделки (бюджет, сроки, текущий стек, роль отправителя) лежат не в тексте, а в пикселях и в звуке. Текстовый пайплайн при этом не галлюцинирует — он хуже: он уверенно квалифицирует по 40% данных, и SDR получает карточку, в которой нет ни бюджета, ни боли. Дальше — либо ручной дозвон и три вопроса заново, либо потерянный лид.
2. Архитектура мультимодального агентного контура
Контур состоит из трёх независимых экстракторов и одного нормализатора. Каждый экстрактор возвращает строго типизированный артефакт, нормализатор собирает их в единый вектор состояния лида.
Vision-контур (Qwen2.5-VL / Claude 3.5 Sonnet). Задача — не OCR, а семантический парсинг: определить тип артефакта (дашборд CRM, форма 1С, Excel-таблица, архитектурная схема, визитка), затем извлечь сущности по JSON-схеме: суммы и валюты, названия систем, число пользователей и лицензий, роль владельца скриншота, текущий технологический стек. Qwen2.5-VL 7B в bf16 закрывает ~80% корпуса скриншотов на одной A10G; Claude 3.5 Sonnet включается как fallback на рукописных диаграммах и нестандартных схемах.
Аудио-контур (Whisper v3 Turbo + Silero VAD). VAD режет файл на речевые сегменты до 30 секунд и отбрасывает шум улицы, автомобиля, офиса — без этого WER на записи из машины улетает в два раза. Turbo-версия даёт ~8x realtime на GPU, что позволяет держать TTFR в десятках секунд. Транскрипт проходит промпт-нормализацию: профессиональный жаргон («хотим, чтобы 1С дружила с битриксом»), числа («двадцать пять лицензий» → 25), топонимы. Отдельно считается эмоционально-смысловой тон: срочность, раздражение, скепсис — это предикторы, а не украшение отчёта.
Нормализация сигналов. Каждая модальность отдаёт confidence. Итоговый вектор — объединение с весами по уверенности, а не конкатенация строк: голосовое с asr_confidence 0.41 не должно перевешивать чистый скриншот с 0.93.
telegram → router ─┬─ text ──────────────┐
├─ image → qwen2.5-vl ─┼→ lead_state → scorer → routing
└─ audio → vad+whisper ┘
3. Машиночитаемая база знаний и стандартизация контекста
Vision и ASR извлекают сырые факты. Чтобы они стали квалификацией, их нужно сверить с продуктовой матрицей компании. Классический RAG здесь плох: тянуть 40 страниц документации в контекст на каждый скриншот — дорого, медленно и шумно.
Работает другой подход — компактный машиночитаемый манифест продукта. Открытая спецификация llms.txt задаёт формат, в котором описаны тарифные границы, интеграционные ограничения (что поддерживается нативно, что через API, что не поддерживается вовсе), ICP и дисквалификаторы. Агент за миллисекунды сопоставляет «1С + 25 лицензий + дебиторка» со строкой манифеста и получает детерминированный ответ: подходит / подходит с оговоркой / не подходит по объёму. Никакой семантической догадки — сверка по ключам.
4. Математика скоринга: Pydantic-схемы и байесовская квалификация
Эвристика в стиле BANT («бюджет есть? — да/нет») ломается на первом нестандартном лиде. Вместо неё — детерминированная схема валидации и вероятностная модель поверх неё.
Признаки: Pain Intensity (0–10), Budget Authority Indicator (бинарный, из роли и формулировок), Timeline Urgency = 1 − min(days, 120)/120, Tech Compatibility Match (доля пересечения обнаруженного стека с ICP, 0–1).
$P(\text{Qualified} \mid \text{Vision}, \text{Audio}, \text{Text}) = \sigma(\mathbf{w}^T \mathbf{x} + b)$
Pydantic v2 здесь не «сериализатор ответа LLM», а контракт: field_validator отбрасывает невозможные состояния (score 0–100, pain 0–10, timeline не отрицательный), model_validator(mode='after') запрещает лид без единой модальности. Агент физически не может вернуть «-5 боли» или routing, противоречащий скору: схема упадёт раньше, чем ответ уйдёт в CRM. Это и есть скоринг без ручной разметки — разметкой служит сама схема.
5. Production-grade реализация: класс MultimodalLeadScorer
Экстракторы оставлены стабами с production-контрактом: замените тела _process_audio и _process_image на вызовы Whisper v3 Turbo и Qwen2.5-VL — остальной пайплайн не меняется.
from __future__ import annotations
import base64, math, re
from datetime import datetime, timezone
from enum import Enum
from typing import Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
class Routing(str, Enum):
AE_ENTERPRISE = 'ae_enterprise'
SDR_FAST_LANE = 'sdr_fast_lane'
NURTURE = 'nurture_sequence'
DISQUALIFY = 'disqualify'
class LeadInput(BaseModel):
model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)
lead_id: str = Field(min_length=1, max_length=64)
source: Literal['telegram', 'whatsapp', 'email', 'web_form']
raw_text: str = Field(default='', max_length=20000)
image_b64: Optional[str] = None
audio_b64: Optional[str] = None
received_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
@model_validator(mode='after')
def _payload_required(self):
if not (self.raw_text or self.image_b64 or self.audio_b64):
raise ValueError('no usable modality')
return self
class ExtractedArtifacts(BaseModel):
vision_entities: dict[str, str] = Field(default_factory=dict)
stack_detected: list[str] = Field(default_factory=list)
transcript: str = ''
asr_confidence: float = Field(default=0.0, ge=0.0, le=1.0)
speech_ratio: float = Field(default=0.0, ge=0.0, le=1.0)
tone: Literal['neutral', 'urgent', 'frustrated', 'skeptical'] = 'neutral'
coverage: float = Field(default=0.0, ge=0.0, le=1.0)
class LeadScoreResult(BaseModel):
lead_id: str
qualified: bool
score: int = Field(ge=0, le=100)
pain_intensity: int = Field(ge=0, le=10)
budget_authority: bool
timeline_days: int = Field(ge=0, le=720)
tech_match: float = Field(ge=0.0, le=1.0)
routing: Routing
artifacts: ExtractedArtifacts
manager_summary: str
class MultimodalLeadScorer:
W = {'pain': 0.34, 'authority': 0.26, 'urgency': 0.22, 'tech': 0.18}
THRESHOLD = 0.55
STACK = ('1c', 'bitrix24', 'amocrm', 'sap', 'kafka')
TONE = {'urgent': ('срочно', 'до пятницы', 'горит'),
'frustrated': ('не работает', 'теряем', 'достало'),
'skeptical': ('уже пробовали', 'докажите', 'не верю')}
AMOUNT_RE = re.compile(r'(\d[\d\s]{2,})\s?(млн|тыс|руб|₽)')
def __init__(self, icp_stack: set[str] | None = None,
kb_url: str = 'https://aifirsttouch.ru/llms.txt') -> None:
self.icp_stack = icp_stack or {'1c', 'bitrix24', 'amocrm'}
self.kb_url = kb_url
def _process_audio(self, audio_b64: str) -> dict:
pcm = base64.b64decode(audio_b64)
dur = max(len(pcm) / 32000, 0.5) # 16 kHz int16 mono
speech = round(0.55 + (sum(pcm[:8192]) or 1) % 45 / 100, 3) # Silero VAD
# production: transcript = whisper_v3_turbo(pcm, language='ru')
return {'transcript': f'asr-stub {dur:.1f}s',
'asr_confidence': round(0.6 + speech * 0.38, 3),
'speech_ratio': speech}
def _process_image(self, image_b64: str) -> dict:
raw = base64.b64decode(image_b64)
blob = raw.decode('latin-1', errors='ignore').lower()
# production: qwen2.5-vl -> JSON по схеме ExtractedArtifacts
amounts = [m.group(0) for m in self.AMOUNT_RE.finditer(blob)]
return {'vision_entities': {'bytes': str(len(raw)),
'amount_raw': amounts[0] if amounts else ''},
'stack_detected': [s for s in self.STACK if s in blob]}
def score_lead(self, lead: LeadInput) -> LeadScoreResult:
art = ExtractedArtifacts()
if lead.audio_b64:
art = art.model_copy(update=self._process_audio(lead.audio_b64))
if lead.image_b64:
art = art.model_copy(update=self._process_image(lead.image_b64))
text = f'{lead.raw_text} {art.transcript}'.strip()
low = text.lower()
tone = next((t for t, keys in self.TONE.items()
if any(k in low for k in keys)), 'neutral')
coverage = round(sum(map(bool, (lead.raw_text, lead.image_b64,
lead.audio_b64))) / 3, 3)
art = art.model_copy(update={'tone': tone, 'coverage': coverage})
pain = min(10, 2 + 2 * bool(lead.image_b64) + 3 * bool(lead.audio_b64)
+ 2 * (tone in ('urgent', 'frustrated')) + int(art.speech_ratio > 0.85))
authority = any(k in low for k in ('директор', 'cto', 'cpo', 'владел',
'основат', 'решени'))
days = 7 if tone == 'urgent' else (90 if 'квартал' in low else 45)
tech = len(set(art.stack_detected) & self.icp_stack) / max(len(self.icp_stack), 1)
x = {'pain': pain / 10, 'authority': 1.0 if authority else 0.15,
'urgency': 1 - min(days, 120) / 120, 'tech': tech}
prob = 1 / (1 + math.exp(-8 * (sum(self.W[k] * v for k, v in x.items()) - 0.5)))
if prob < 0.30:
routing = Routing.NURTURE if pain >= 4 else Routing.DISQUALIFY
elif prob >= 0.72 and authority:
routing = Routing.AE_ENTERPRISE
else:
routing = Routing.SDR_FAST_LANE
summary = (f'pain={pain}/10 | authority={authority} | timeline={days}d | '
f'tech_match={tech:.2f} | coverage={coverage:.2f} | tone={tone}')
return LeadScoreResult(lead_id=lead.lead_id, qualified=prob >= self.THRESHOLD,
score=round(prob * 100), pain_intensity=pain,
budget_authority=authority, timeline_days=days,
tech_match=round(tech, 3), routing=routing,
artifacts=art, manager_summary=summary)
6. Продакшен-кейс платформы First Touch
В контуре First Touch за 90 дней обработано более 14 000 гетерогенных B2B-обращений из Telegram, WhatsApp, email и веб-форм. Результаты:
Онтология продукта и схемы описаны в открытой спецификации llms.txt — тот же манифест читают агенты при скоринге.
7. Дорожная карта внедрения за 4 спринта (30 дней)
Спринт 1 (дни 1–7). Собрать 300–500 реальных скриншотов и 200 голосовых из своих каналов, разметить четыре сущности (бюджет, стек, роль, срок). Спроектировать LeadScoreResult и заморозить её: схема меняется только через версионирование.
Спринт 2 (дни 8–14). Поднять Whisper v3 Turbo + Silero VAD за шлюзом с очередью; развернуть Qwen2.5-VL (Claude 3.5 Sonnet — для схем) с JSON-schema output. Офлайн-валидация: precision извлечения сумм и систем ≥ 0.9, WER на жаргоне ≤ 15%.
Спринт 3 (дни 15–21). Оцифровать продукт в llms.txt: тарифы, интеграции, ICP, дисквалификаторы. Подключить скорер к CRM вебхуком POST /lead.score → routing → задача нужной команде, всё идемпотентно по lead_id.
Спринт 4 (дни 22–30). A/B: мультимодальный рой против классической формы и ручной обработки. Метрики — не accuracy модели, а Lead-to-Opportunity, TTFR и доля лидов с вложениями, доведённых до встречи.
8. Заключение и CTA
Будущее B2B Lead Gen — невидимый интерфейс. Клиент отправляет двухминутное голосовое и скриншот из 1С, а через 42 секунды получает слот в календаре AE. Между этими двумя точками нет ни формы, ни «опишите задачу словами», ни SDR, который скачивает вложение.
Мультимодальный контур — не фича, а архитектурное решение: типизированные артефакты, машиночитаемый манифест продукта, детерминированная схема валидации и вероятностный скор поверх неё.
Next step. Аудит ваших каналов лидогенерации и развёртывание мультимодального агентного контура — First Touch. Приходите с реальным корпусом вложений: покажем на ваших скриншотах и войсах, сколько контекста вы теряете прямо сейчас.
🚀 Проект автора: First Touch (https://aifirsttouch.ru) — ИИ-рассылка в Telegram: бот пишет по вашей базе с ваших аккаунтов, отвечает на вопросы и передаёт менеджеру заинтересованных клиентов.
Автор: Alexander Bond — AI-разработчик и архитектор автономных систем
Теги: мультимодальный ai, ai агенты, b2b продажи, компьютерное зрение, whisper, qwen-vl, first touch