Logo image of Arcanum12thArcanum12th

Building a Resume Matcher with tRPC, NLP, and Vertex AI

I built a resume matcher app with tRPC, TypeScript, NLP, and Google Vertex AI. Learn how it compares to REST and GraphQL and why it’s great for fast MVPs.

  1. Авторы
  2. Курсы
Количество просмотров8
прочтений8 прочтений
Building a Resume Matcher with tRPC, NLP, and Vertex AI

Я недавно создал небольшое приложение для сопоставления резюме на TypeScript, которое сравнивает PDF-резюме с вакансиями. Я хотел быстро прототипировать API, поэтому выбрал tRPC для бэкенда. tRPC — это фреймворк RPC с приоритетом на TypeScript, который обещает «безопасные для типов API от конца до конца», что означает, что я мог делиться типами между клиентом и сервером, не написав схемы OpenAPI или SDL GraphQL. На практике это означало, что я мог сосредоточиться на написании логики, а не на шаблонах. В отличие от REST или GraphQL, tRPC не раскрывает общую схему — он просто раскрывает процедуры (по сути функции) на сервере, которые клиент может вызывать, делясь типами ввода/вывода напрямую.

Почему это полезно? Короче говоря, я создавал внутренний инструмент (MVP), и я уже использовал TypeScript на обоих концах. Модель tRPC с нулевым шагом сборки и безопасностью типов подошла идеально. Официальная документация tRPC даже хвалит автоматическую безопасность типов: если я изменю ввод или вывод функции сервера, TypeScript предупредит меня на клиенте, прежде чем я даже отправлю запрос. Это было большим успехом для раннего выявления ошибок. В отличие от этого, с REST или GraphQL мне пришлось бы вручную синхронизировать или генерировать схемы. С другой стороны, я знал, что tRPC тесно связывает мой API и код клиента (это не язык-независимый API) — поэтому это лучше всего подходит для проектов «TypeScript-first», таких как этот, а не для публичных кросс-платформенных API.

Определение маршрутизатора tRPC и валидация ввода

С настроенным tRPC я написал простой маршрутизатор для основной операции: анализа двух загруженных PDF-файлов (резюме и описание вакансии). Используя tRPC с Zod-form-data, я мог легко валидировать загрузки файлов. Вот упрощенная версия кода маршрутизатора:

экспорт конст matchRouter = маршрутизатор({
анализироватьPdfs: baseProcedure
.ввод(zfd.formData({
vacancyPdf: zfd.файл().уточнить(файл => файл.тип === "application/pdf", {
сообщение: "Разрешены только PDF-файлы",
}),
cvPdf: zfd.файл().усовершенствовать(файл => файл.тип === "application/pdf", {
сообщение: "Разрешены только PDF файлы",
}),
}))
.мутация(асинхронный ({ input }) => {
константа [cvText, vacancyText] = ожидать Обещание.все([
PDFService.извлечьТекст(input.cvPdf),
PDFService.извлечьТекст(input.vacancyPdf),
]);
константа результат = ожидать MatcherService.совпадение(cvText, vacancyText);
возврат { matchRequestId,...результат };
}),
});

Выше, анализироватьPDF мутация принимает многокомпонентную форму с двумя PDF-файлами. zfd.file().refine(...) вызывает проверку, чтобы каждый файл был PDF. Как только файлы проверены и загружены (через вспомогательный FileService), я использую PDFService.extractText(...) чтобы извлечь сырой текст из каждого PDF. Затем я вызываю MatcherService.match(cvText, vacancyText), который выполняет фактический анализ. Поскольку tRPC знает типы входных/выходных данных, мой фронтенд получает полностью типизированные результаты без необходимости писать дополнительные DTO. Эта быстрая настройка и строгая типовая безопасность сэкономили много времени на MVP.

Извлечение навыков с помощью Basic NLP

Как только у меня был простой текст резюме и описания работы, мне нужно было извлечь значимые ключевые слова или навыки из них. Я сделал это просто: я использовал комбинацию естественный (для токенизации), компромисс (для частей речи, таких как существительные), и фильтр стоп-слов. Например, в MatcherService У меня есть помощник вроде этого:

приватный статический извлечьНавыки(текст: строка): Множество<строка> {
константа док = нлп(текст);
константа существительные = док.существительные().выход("массив"); // существительные часто являются навыками или ключевыми словами
константа заглавныеСлова = текст.совпадение(/\b[A-Z][a-zA-Z0-9.-]+\b/g) || [];
// также захватывать заглавные слова (например, фреймворки или собственные имена)
возврат новый Установить([...существительные,...заглавныеСлова].карта(с => с.вНижнемРегистре()));
}

Проще говоря, этот код переводит текст в нижний регистр, обрабатывает его через компромисс NLP для извлечения существительных, а также использует регулярные выражения для поиска любых заглавных слов (что часто захватывает названия технологий). Объединение этих данных и удаление дубликатов дает мне набор кандидатских «навыков» из каждого документа. Этот базовый процесс извлечения ключевых слов не является сложным ML — это просто эвристика — но он быстрый и хорошо подходит для выделения совпадающих навыков. (Это напоминает мне некоторые старые парсеры резюме.) Внешняя модель пока не нужна, только несколько удобных библиотек и немного регулярных выражений в общем классе сервиса.

Интеграция Vertex AI (Gemini 1.5 Flash) для Сопоставления

Для основной логики сопоставления я решил обратиться к Vertex AI от Google с новой моделью Gemini 1.5 Flash. Это в основном касалось получения структурированного результата сравнения (например, балла и предложений) без необходимости реализовывать сложную логику NLP. В MatcherService, после очистки текста и извлечения навыков, я создаю запрос и получаю данные от Vertex. Например:

константа aiPrompt = `
Анализируйте описание работы и резюме кандидата, чтобы предоставить структурированную оценку.

Описание работы:
${cleanedJD}

Резюме кандидата:
${cleanedCV}

Предоставьте структурированный анализ в формате JSON с полями "score", "strengths" и "suggestions".
`
;

константа response = ожидать получить(процесс.среда.AI_API_ENDPOINT!, {
метод: "POST",
заголовки: {
Авторизация: процесс.среда.AI_API_TOKEN,
"Content-Type": "application/json",
},
тело: JSON.строка({
содержимое: [
{ роль: "user", части: [{ текст: aiPrompt }] }
]
})
});

константа данные = ожидать ответ.json();
если (!данные?.кандидаты?.[0]?.содержимое?.части?.[0]?.текст) {
выбросить новый AIServiceError('Неверный формат ответа AI');
}
конст сыройОтвет = данные.кандидаты[0].содержимое.части[0].текст;
// Затем парсите сыройОтвет как JSON для оценки, сильных сторон, предложений...

Здесь я использую fetch для POST-запроса к конечной точке Vertex AI (настроенной в AI_API_ENDPOINT), передавая пользовательский запрос в теле запроса. Запрос говорит модели сравнить описание работы и резюме и вывести JSON с оценкой соответствия, сильными сторонами и т.д. Затем я парсирую текст JSON из данные.кандидаты[0].content.parts[0].text. Этот подход был очень полезен – Gemini выдал результат, не заставляя меня писать алгоритм ранжирования. Это похоже на то, что я рассматриваю модель как черный ящик для сравнения. Конечно, это означает, что я доверяю AI, и иногда вывод требовал очистки или проверки. Но в целом, внедрение Gemini в сервис позволило мне сосредоточиться на UI и потоке данных, а не на запросах к LLM. (Мне пришлось обработать некоторые ошибки и ограничения по скорости вокруг вызова.)

Почему tRPC был хорошим выбором (и его недостатки)

Использование tRPC определенно ускорило разработку. Без необходимости писать схемы API, я мог создать конечную точку за считанные минуты. Полный стек TypeScript означает, что код маршрутизатора, который я написал выше, является общим кодом на клиенте (через генератор клиента tRPC), так что я получаю проверки на этапе компиляции. На практике, когда я изменил валидацию Zod или форму возвращаемого значения, мой интерфейс React сразу же перестал компилироваться, пока я не скорректировал типы интерфейса. Это ощущение «автозаполнения» именно то, что обещает сайт tRPC. И поскольку tRPC по сути не имеет никакого шаблона (нет классов контроллеров, нет генерации кода), код остался лаконичным.

С другой стороны, я осознаю ограничения tRPC. Он напрямую связывает мой фронтенд с этой реализацией сервера, так что если мне когда-либо понадобится публичный REST или мобильный клиент, мне придется пересмотреть это. Мне также пришлось самостоятельно подумать о кэшировании и лимитах скорости (tRPC не делает кэширование из коробки, как это может делать GraphQL). Блог Directus попал в точку: tRPC отлично подходит для внутренних инструментов с большим количеством TypeScript, но он «ограничивает ваши возможности», если вам нужна широкая совместимость. Для этого проекта — по сути, внутреннего демо — эти компромиссы были приемлемыми. Я даже реализовал простое промежуточное ПО для ограничения скорости на случай, если мои вызовы Vertex AI превысят квоту.

Уроки, извлеченные из опыта

Создание этого проекта с современными API TypeScript было довольно приятно. Я получил полную типизацию (клиент точно знает, какая форма { score: number } возвращается) и не нужно поддерживать отдельную клиентскую библиотеку. Код ощущается очень «похоже на SDK», просто вызывая функции на matchRouter, как если бы это был локальный код. С точки зрения NLP, я узнал, что даже простые эвристики (существительные + заглавные слова) могут неплохо справляться с извлечением ключевых слов в экстренных ситуациях. И наконец, интеграция Vertex AI напомнила мне, что много «магии ИИ» можно аутсорсить с помощью хорошо составленного запроса.

Тем не менее, ничто не является панацеей. Если бы у меня было больше времени, я бы улучшил обработку ошибок вокруг AI-сервиса и, возможно, добавил кэширование результатов (поскольку вызовы PDF-в-текст и AI дорогие). И если это приложение вырастет за пределы быстрого демо, я мог бы заменить tRPC на более традиционный REST/GraphQL API, если мне нужен был бы публичный интерфейс. Но пока что tRPC дал мне именно то, что мне нужно: быстрый MVP с полной безопасностью типов и минимальными формальностями.

Вы можете найти полный код для этого проекта здесь: GitHub Репозиторий.

Источники: Я опирался на несколько ресурсов, исследуя эту настройку. Сайт tRPC подчеркивает подход «двигайтесь быстро, не ломайте ничего» с полной безопасностью типов, а блоги сравнивают, как tRPC вписывается среди REST/GraphQL, отмечая его преимущества и ограничения, ориентированные на TypeScript.

  1. Официальный сайт tRPC — Двигайтесь быстро и не ломайте ничего. Полностью безопасные API с типами стало проще. (Показывает акцент tRPC на полном стеке TypeScript и безопасности типов).
  2. Вильями Куосманен, Сравнение REST, GraphQL и tRPC (dev.to, октябрь 2023) — Обсуждает, как tRPC открывает функции в стиле RPC и делится типами вместо общей схемы.
  3. Брайант Гиллеспи, REST против GraphQL против tRPC (блог Directus, февраль 2025) — Охватывает сильные стороны tRPC (минимальный шаблон, безопасность типов) и компромиссы (только TypeScript, ограниченный охват API).

Вам также может понравиться

+0

Видео контент

21

Уроки

+500

Студенты

Пожалуйста, оформите подписку, чтобы комментировать, или Войти, если у вас уже есть подписка.