Биоинформатики тонут в данных. Белковые последовательности, структуры, функции - объём информации растёт экспоненциально, а инструменты для поиска остаются архаичными. BLAST, HMMER, командная строка. Учёный хочет спросить: "Покажи все рецепторы, похожие на этот пептид с уровнем гомологии > 80%", а не писать SQL-подобные запросы к базе Swiss-Prot.
Проблема не в отсутствии LLM, а в том, что обычный RAG не умеет работать с последовательностями аминокислот. Токенизатор GPT разобьёт "MVLSEGEWQLVLHVWAKVEADVAGHGQDILIRLFKSHPETLEKFDRFKHLKTEAEMKASEDLKKHGVTVLTALGAILKKKGHHEAELKPLAQSHATKHKIPIKYLEFISEAIIHVLHSRHPGNFGADAQGAMNKALELFRKDIAAKYKELGYQG" в мусор. Нужен специализированный эмбеддер - модель, обученная на миллионах белков. Таким стал ESM-C 300M от Meta, последняя версия на июнь 2026 года.
Суть подхода: объединяем кастомные эмбеддинги (ESM-C 300M) с векторным индексом pgvector, а поверх ставим агента на Amazon Bedrock AgentCore, который понимает естественный язык и умеет разбивать сложные запросы на подзадачи с помощью Strands Agents SDK. Никакого копирования последовательностей вручную.
Почему обычный RAG тут не взлетит
Типичный RAG с OpenAI embeddings или Amazon Titan Text Embeddings даёт осмысленные векторы для текста, но не для биологических последовательностей. Два белка могут иметь 50% идентичности, но их текстовые описания будут разными. Модели вроде ESM (Evolutionary Scale Modeling) кодируют эволюционный контекст: мутации, гомологию, структуру. ESM-C 300M - это контрастивная версия, оптимизированная под поиск похожих последовательностей. Она выдаёт эмбеддинги размером 1280, где расстояние L2 коррелирует с биологической близостью.
Второй нюанс - запросы. Биолог не пишет "similarity > 0.9". Он говорит: "Найди белки с доменом SH3, которые связывают протеинкиназу A". Тут нужен агент, способный распарсить естественный язык, извлечь ключевые сущности (домен, функция, организм) и скомбинировать векторный поиск с фильтрацией по метаданным. Гибридный RAG спасает, но в нашем случае векторный поиск идёт не по тексту, а по биологическим эмбеддингам.
Архитектура: от запроса до ответа
Вот как выглядит итоговый пайплайн (без лишних деталей, только суть):
- Пользователь пишет: "Покажи пять белков человека, похожих по структуре на этот пептид: MVLSEGEWQLVL..."
- Бедрок-агент (Amazon Bedrock AgentCore) получает запрос, определяет, что нужен action group для поиска белков.
- Strands Agents SDK разбивает задачу: извлечь последовательность, задать organism=human, top_k=5.
- Серверная функция (Lambda или контейнер) запускает инференс ESM-C 300M для эмбеддинга запроса.
- Выполняется ANN-поиск в pgvector (Amazon RDS for PostgreSQL с расширением pgvector). Фильтр по organism='human'.
- Результаты возвращаются агенту, он форматирует ответ с аннотациями UniProt.
Всё это работает в реальном времени за 2-3 секунды. Сравните с ручным запуском BLAST+.
Шаг за шагом: строим своими руками
1 Подготовка окружения и данные
Нам понадобятся: аккаунт AWS (разумеется), IAM роль с правами на Bedrock, Lambda, RDS. Локально ставим Python 3.12, esm (последняя версия от Meta, pip install esm), psycopg2-binary, boto3, strands-agents-sdk (pip install strands-agents-sdk). Версии на июнь 2026: esm 3.2, strands-agents-sdk 1.5.
Качаем подмножество Swiss-Prot (около 50 тысяч последовательностей) в формате FASTA. Можно взять из S3 публичного датасета. Для теста хватит.
2 Генерация эмбеддингов через ESM-C 300M
Используем предобученную модель esm_c_300m. Она принимает последовательность, возвращает тензор 1280 float. Важно: нормализуем векторы перед записью в pgvector (L2 нормализация, чтобы использовать косинусное расстояние через косинус).
import esm
model, alphabet = esm.pretrained.esm_c_300m()
batch_converter = alphabet.get_batch_converter()
def embed_sequence(seq: str):
data = [("protein", seq)]
_, _, tokens = batch_converter(data)
with torch.no_grad():
results = model(tokens, repr_layers=[36], return_contacts=False)
token_representations = results["representations"][36][0, 1:-1].mean(axis=0).numpy() # средний пулинг
norm = np.linalg.norm(token_representations)
return token_representations / norm if norm != 0 else token_representations
Подводный камень: ESM-C 300M требует GPU для быстрой работы. На CPU один эмбеддинг будет считаться 5-10 секунд. На Batch на g4dn.xlarge (T4) - около 100 последовательностей в секунду. Учитывайте это при масштабировании.
3 Наполняем pgvector
Создаём экземпляр RDS for PostgreSQL 16 с расширением pgvector (версия 0.7.4). Таблица:
CREATE EXTENSION vector;
CREATE TABLE proteins (
id SERIAL PRIMARY KEY,
uniprot_id TEXT UNIQUE,
sequence TEXT,
organism TEXT,
description TEXT,
embedding vector(1280)
);
CREATE INDEX ON proteins USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
Загружаем данные батчами. Код в Lambda: получает эмбеддинги из S3 (предварительно рассчитанные) и вставляет через executemany.
4 Создаём Agent в Amazon Bedrock AgentCore с action group
В Bedrock AgentCore определяем агента с инструкцией: "You are a protein research assistant. You can search for similar proteins, retrieve sequences, and filter by organism.". Создаём action group с OpenAPI-схемой, указывающей эндпоинт Lambda: POST /search с параметрами (sequence, top_k, organism). Как добавлять action groups с MCP - полезно для расширения.
Lambda-функция: принимает запрос, извлекает последовательность, вызывает ESM-C (можно на том же инстансе, если используем контейнер с GPU), выполняет pgvector запрос:
import psycopg2, json, numpy as np
def lambda_handler(event, context):
body = json.loads(event['body'])
query_vec = embed_sequence(body['sequence'])
conn = psycopg2.connect(...)
cur = conn.cursor()
cur.execute("""
SELECT uniprot_id, description, 1 - (embedding <=> %s::vector) AS similarity
FROM proteins
WHERE organism = %s
ORDER BY embedding <=> %s::vector
LIMIT %s
""", (query_vec.tolist(), body.get('organism', '%'), query_vec.tolist(), body['top_k']))
rows = cur.fetchall()
return {'statusCode': 200, 'body': json.dumps(rows)}
5 Интеграция со Strands Agents SDK для многозадачности
Сам по себе Bedrock AgentCore умеет вызывать один action group за раз. Но сложный запрос вроде "сравни три пептида и выведи, какой из них наиболее эволюционно консервативен" требует цепочки действий: найти гомологи для каждого, посчитать консервативность. Strands Agents SDK позволяет описать граф задач. Устанавливаем его на Lambda (или отдельный microservice).
from strands.agents import Agent, Task
class ProteinResearchAgent(Agent):
def build_plan(self, query):
tasks = []
if "compare" in query:
tasks.append(Task("extract_peptides"))
tasks.append(Task("find_orthologs"))
tasks.append(Task("compute_conservation"))
else:
tasks.append(Task("vector_search"))
return tasks
Агент на Bedrock вызывает Strands как action, Strands возвращает результат. Пример создания кино-агента показывает похожую логику с разбивкой.
Ошибки, которые я видел в каждом втором проекте
- Забыли нормализовать эмбеддинги. pgvector при cosine distance сам не нормализует, если вы сохраняете с нормализацией - всё ок. Но если не нормализовать, поиск будет по L2, а не по косинусу. Хуже того, некоторые модели выдают разные по длине векторы.
- Выбрали IVFFlat с неправильным lists. Для 50к записей lists=100 норм, но для >1M нужно пересчитывать. Используйте
ivfflatтолько для продакшна с настройкой probes. Для прототипаhnswбыстрее, но требует больше памяти. - Lambda холодный старт с моделью ESM-C. Загрузка модели 2-3 ГБ. Используйте Elastic Inference или контейнер на ECS/Fargate с сохранением тёплого пула. Либо предрасчёт эмбеддингов и хранение в таблице - поиск быстрее.
- Не настроили таймауты для агента. Bedrock AgentCore по умолчанию ждёт 30 секунд. Если инференс ESM-C занимает 40 секунд - получите ошибку. Увеличьте timeout до 120 секунд в конфигурации action group.
Совет: Для production используйте отдельный кластер RDS с read replica для векторного поиска, чтобы не тормозить основные транзакции. И кэшируйте эмбеддинги популярных запросов в ElastiCache Redis - ускоряет повторные запросы на 90%.
Неочевидный трюк: гибридный поиск + эмбеддинги ESM
Чисто векторный поиск хорош, но иногда нужна точная подстрока (например, найти конкретный мотив). Гибридный поиск решает: pgvector поддерживает полнотекстовый поиск (tsvector) одновременно с векторами.
SELECT uniprot_id, description
FROM proteins
WHERE to_tsvector('english', description) @@ plainto_tsquery('kinase')
AND organism = 'human'
ORDER BY embedding <=> %s::vector
LIMIT 10;
Тогда агент может ответить на запрос: "Найди человеческие киназы, похожие на этот белок". Комбинируем фильтр по тексту и векторную близость. Если хотите полный дашборд для биологов - FAST-шаблон даёт фронтенд и деплой одним стеком.
И последний совет, который сэкономит вам неделю: не пытайтесь воткнуть ESM-C 300M в Lambda напрямую. Используйте контейнер на ECS с GPU Spot - это в 4 раза дешевле, чем on-demand. И закэшируйте эмбеддинги всех последовательностей из Swiss-Prot в S3 при первом запуске. Поверьте, пересчитывать их каждый раз при редеплое - тот ещё мазохизм.