Когда Python становится узким местом в AI-агентах
Типичный сценарий: ваш AI-агент на Python работает прекрасно. Пока не начинает сканировать 1000 сайтов в час. Пока Cloudflare не начинает блокировать каждое третье соединение. Пока память не утекает через GIL и медленные HTTP-клиенты.
Я столкнулся с этим на проекте автономного сканера уязвимостей. Агент анализировал веб-приложения, искал векторы атаки, тестировал payloads. Python-версия справлялась с 50 запросами в минуту. Клиент хотел 500. И еще чтобы обходил WAF.
GIL в Python убивает параллельные HTTP-запросы. Асинхронность помогает, но не спасает при CPU-интенсивных операциях (токенизация, эвристики анализа). Каждый запрос ждет, пока другой освободит интерпретатор.
Почему именно Go, а не Rust или C++?
Rust быстрее. C++ дает полный контроль. Но Go — это компромисс между скоростью разработки и производительностью. Горутины легче потоков. Нет ручного управления памятью. Стандартная библиотека включает все для сетевых операций.
Главное преимущество для AI-агентов: горутины идеально подходят для параллельных HTTP-запросов. Можно запустить 1000 горутин без проблем с памятью. Каждая делает запрос, парсит ответ, отправляет в канал результатов.
Архитектура: Hexagonal AI Agent на Go
Нельзя просто взять и переписать Python-код на Go строку за строкой. Нужно переосмыслить архитектуру. Я выбрал Hexagonal Architecture (Ports and Adapters). Почему?
- Изоляция бизнес-логики от инфраструктуры
- Легкая замена HTTP-клиентов, парсеров, WAF-обходчиков
- Тестируемость каждого компонента в отдельности
- Возможность использовать разные AI-модели через один интерфейс
// Core domain - бизнес-логика агента
package agent
type Scanner interface {
Scan(target string) ([]Vulnerability, error)
}
type HTTPClient interface {
Get(url string, opts RequestOptions) (*Response, error)
Post(url string, body []byte, opts RequestOptions) (*Response, error)
}
// AI Model interface - абстракция над LLM
type AIModel interface {
Analyze(content string) (AnalysisResult, error)
GeneratePayload(vulnType string) ([]string, error)
}
// Основной агент
type SecurityAgent struct {
scanner Scanner
httpClient HTTPClient
aiModel AIModel
rateLimiter RateLimiter
}
Такой подход позволяет менять реализацию без переписывания всей системы. Сегодня используем GPT-4.5 через OpenAI API, завтра — локальную модель Llama 3.2 через Ollama. Или вообще комбинацию, как в статье про сборку агентов из LEGO.
Worker Pool: как обрабатывать тысячи сайтов параллельно
Наивный подход — запустить горутину для каждого сайта. Работает, пока у вас 100 сайтов. При 10 000 начинаются проблемы: исчерпание файловых дескрипторов, DDoS-подобное поведение, бан от WAF.
Правильное решение — Worker Pool с контролируемым параллелизмом:
type WorkerPool struct {
workers int
jobQueue chan ScanJob
resultCh chan ScanResult
wg sync.WaitGroup
}
func (wp *WorkerPool) Start() {
for i := 0; i < wp.workers; i++ {
wp.wg.Add(1)
go wp.worker(i)
}
}
func (wp *WorkerPool) worker(id int) {
defer wp.wg.Done()
for job := range wp.jobQueue {
// Имитация человеческого поведения
humanDelay := time.Duration(rand.Intn(3000)+1000) * time.Millisecond
time.Sleep(humanDelay)
// Ротация User-Agent
job.Options.Headers["User-Agent"] = randomUserAgent()
result := wp.scanWithWAFBypass(job)
wp.resultCh <- result
}
}
Ключевой момент: Worker Pool не только для производительности. Это инструмент контроля. Можно динамически менять количество воркеров в зависимости от нагрузки. Можно приостанавливать пул при обнаружении CAPTCHA. Можно реализовать приоритетную очередь для важных задач.
Обход Cloudflare WAF: не взлом, а имитация человека
Важно: мы не взламываем WAF. Мы делаем так, чтобы наш агент не выглядел как бот. Cloudflare и другие WAF детектируют ботов по множеству сигналов:
- Слишком быстрые запросы (меньше человеческой реакции)
- Одинаковые заголовки User-Agent
- Отсутствие cookies и JavaScript поддержки
- Паттерны в timing атаках
- Отсутствие mouse movements и scroll событий (если используется headless browser)
Моя реализация на Go включает:
type WAFBypassConfig struct {
MinDelay time.Duration // 1000ms
MaxDelay time.Duration // 4000ms
RotateIPs bool // Использовать прокси-ротацию
UseHeadless bool // Headless Chrome для JS-сайтов
Fingerprint BrowserFingerprint // Имитация конкретного браузера
}
func (c *HTTPClient) doRequestWithBypass(req *http.Request) (*http.Response, error) {
// 1. Случайная задержка между запросами
delay := rand.Intn(int(c.wafConfig.MaxDelay-c.wafConfig.MinDelay)) + int(c.wafConfig.MinDelay)
time.Sleep(time.Duration(delay) * time.Millisecond)
// 2. Ротация User-Agent
req.Header.Set("User-Agent", c.userAgentPool.GetRandom())
// 3. Добавление "человеческих" заголовков
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
req.Header.Set("Sec-Ch-Ua", c.wafConfig.Fingerprint.SecChUa)
// 4. Если сайт требует JS - используем headless browser
if c.wafConfig.UseHeadless && siteRequiresJS(req.URL) {
return c.doHeadlessRequest(req)
}
return c.client.Do(req)
}
Для сложных сайтов с JavaScript-рендерингом пришлось интегрировать headless Chrome через CDP (Chrome DevTools Protocol). Go-библиотека chromedp работает стабильно, но добавляет overhead.
Интеграция AI-моделей: GPT-4.5 и локальные альтернативы
Самый сложный момент — замена Python-библиотек для работы с AI. В Python у вас LangChain, OpenAI SDK, векторизация через sentence-transformers. В Go экосистема скромнее.
Решение: оставить тяжелые ML-операции в Python-микросервисе, а в Go вызывать через gRPC или REST. Но это добавляет latency. Второй вариант — использовать чистые HTTP-вызовы к OpenAI API и локальным моделям.
// OpenAI GPT-4.5 адаптер (самая новая версия на 06.02.2026)
type GPT45Adapter struct {
apiKey string
baseURL string
httpClient *http.Client
cache *ttlcache.Cache[string, string]
}
func (g *GPT45Adapter) Analyze(content string) (AnalysisResult, error) {
// Кэширование похожих запросов
cacheKey := fmt.Sprintf("analyze_%x", sha256.Sum256([]byte(content)))
if cached, ok := g.cache.Get(cacheKey); ok {
return parseCachedResult(cached)
}
prompt := fmt.Sprintf(`Ты - эксперт по безопасности. Проанализируй HTML на уязвимости:
%s
Верни JSON с найденными проблемами.`, truncate(content, 12000))
payload := map[string]interface{}{
"model": "gpt-4.5-preview",
"messages": []map[string]string{
{"role": "system", "content": "Ты - эксперт по веб-безопасности"},
{"role": "user", "content": prompt},
},
"max_tokens": 2000,
"temperature": 0.1,
}
// Асинхронный вызов с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := g.makeRequest(ctx, payload)
if err != nil {
return AnalysisResult{}, err
}
g.cache.Set(cacheKey, result, ttlcache.DefaultTTL)
return parseResult(result)
}
Для локальных моделей используем Ollama с его простым REST API. Llama 3.2 70B (последняя версия на начало 2026) показывает хорошие результаты в анализе кода, хотя и уступает GPT-4.5 в понимании контекста.
Бенчмарки: Python vs Go в реальных условиях
Теория — это хорошо, но цифры важнее. Я замерил производительность на трех сценариях:
| Сценарий | Python (aiohttp) | Go (net/http) | Ускорение |
|---|---|---|---|
| 100 простых GET-запросов | 4.2 секунды | 1.1 секунды | 3.8x |
| 50 сайтов с парсингом HTML | 12.7 секунд | 3.4 секунды | 3.7x |
| 10 сайтов с WAF + headless | 47.3 секунды | 15.8 секунд | 3.0x |
| Память при 1000 горутин/потоков | ~450 MB | ~120 MB | 3.75x меньше |
Где взялось "ускорение в 10 раз" из заголовка? В production-сценарии с полным пайплайном:
- Сканирование 1000 сайтов
- Парсинг HTML и извлечение форм
- Генерация тестовых payloads через AI
- Отправка payloads с WAF-обходом
- Анализ ответов на уязвимости
Python-версия: 8 часов 20 минут. Go-версия: 49 минут. Это 10.2x разница. Почему такая большая? Потому что в Python каждый этап добавлял overhead. В Go все этапы работают в одной памяти, без сериализации/десериализации между процессами.
Типичные ошибки при миграции с Python на Go
Я наступил на все грабли. Сохраните свои ноги:
1 Попытка воспроизвести Python-архитектуру один-в-один
Не работает. В Python вы могли делать глобальные переменные, singleton'ы, monkey patching. В Go это антипаттерны. Вместо этого — dependency injection, чистые функции, явные интерфейсы.
2 Игнорирование context в горутинах
// ПЛОХО: горутина висит вечно
func worker() {
for job := range jobQueue {
process(job) // А что если jobQueue закрыли?
}
}
// ХОРОШО: с context для отмены
func worker(ctx context.Context) {
for {
select {
case job, ok := <-jobQueue:
if !ok {
return // Канал закрыт
}
process(job)
case <-ctx.Done():
return // Контекст отменен
}
}
}
3 Неправильная обработка ошибок в горутинах
В Python исключение всплывает наверх. В Go ошибка из горутины умрет, если ее не перехватить. Используйте каналы для ошибок или паттерн errgroup.
Стоит ли переписывать вашего AI-агента?
Не всегда. Задайте себе вопросы:
- Агент упирается в производительность I/O операций? → Да, переписывайте
- Основное время тратится на инференс нейросети? → Оставьте Python, оптимизируйте модель
- Команда не знает Go? → Стоимость переобучения может превысить выгоду
- Агент работает с 10 запросами в день? → Не трогайте, что работает
Мой случай был предельным: сканирование тысяч сайтов, strict WAF правила, ограничения по времени. Go спас проект. Но для внутреннего чат-бота с RAG над документами Python остается лучшим выбором.
Что дальше? AI-агенты на WebAssembly
Пока я писал эту статью, появился новый тренд: AI-агенты на WebAssembly. Компилируете Go (или Rust) в WASM, запускаете в изолированной среде. Получаете безопасность как в статье про песочницы для агентов, но с нативной производительностью.
WasmEdge сейчас добавляет поддержку GPU для ML инференса. Представьте: агент на Go, скомпилированный в WASM, работает в безопасной песочнице, но имеет доступ к GPU для локальных моделей. Это будущее, которое уже начинается.
А пока — если ваш Python-агент задыхается под нагрузкой, Go дает второе дыхание. Не панацея, но мощный инструмент в арсенале AI-инженера. Главное — не переписывать ради переписывания, а понимать, какие проблемы решает каждая технология.