Создание AI-агента с веб-поиском: Strands Agents SDK + Exa гайд | AiManual
AiManual Logo Ai / Manual.
12 Май 2026 Гайд

Как прикрутить веб-поиск к AI-агенту: Strands Agents SDK + Exa — пошаговое пособие для тех, кто устал от галлюцинаций

Полный гайд по интеграции Strands Agents SDK (AWS) и Exa для AI-агентов с веб-доступом. Код, примеры, ошибки и советы для production.

Ваш AI-агент — просто болванка без интернета

Я перестал считать, сколько раз видел демо "агента", который отвечает на вопросы про погоду данными 2023 года. LLM без веб-доступа — это энциклопедия, которую закрыли на реставрацию. В 2026 году, когда свежие данные обновляются каждые несколько секунд, заставлять модель полагаться на внутренние знания — преступление против пользователя.

Проблема в том, что просто дать агенту поисковый API — мало. Google вернёт 10 синих ссылок, а модель должна их распарсить, извлечь суть и решить, что делать дальше. Это путь к лапше из кода и потерянному контексту. Нужна связка: фреймворк для агентов (с состояниями, инструментами, ReAct) и поиск, который отдаёт структурированные данные, а не html-огрызки.

Здесь появляются Strands Agents SDK (AWS, версия 2.0.0) и Exa (AI-native поисковик). О первом я рассказывал в контексте LangSmith Agent Builder, но сегодня копаем глубже. Exa, в отличие от Tavily или обычного Google Custom Search, выдаёт тип объекта, эмбеддинги, авторов — всё, что нужно агенту для принятия решений без лишнего парсинга.

💡
Если вы ещё не читали сравнение Exa, Tavily и Playwright для deep research, сделайте это сейчас. Там я разбираю, почему Exa даёт меньше головной боли, когда нужно не просто найти, а понять.

Что конкретно мы построим

Агента, который:

  • Принимает естественно-языковый запрос (например, "Расскажи последние новости про квантовые вычисления и проверь, подтвердил ли IBM свой roadmap")
  • С помощью Exa ищет релевантные документы
  • Извлекает полные тексты статей через exa_get_contents
  • Формирует ответ с цитатами и ссылками
  • При неоднозначности — задаёт уточняющий вопрос пользователю

Вся логика упакована в инструменты Strands SDK. Режим — ReAct с loop до 5 шагов.

Почему не взять готовый Tavily Search Agent

Потому что Tavily возвращает summary, а не сырой контент. Для фактчекинга это смерть — вы не можете проверить первоисточник. Exa отдаёт вам полный текст, метаданные, даты, а в платной версии — score достоверности. Кроме того, Exa умеет искать похожие документы по эмбеддингу, что идеально для Research Agent (я писал про подход Яндекса к DeepResearch — там та же идея, но на своей инфраструктуре).

Пошаговый план (с кровью и кодом)

1 Установка и ключи

Убедитесь, что Python >= 3.11. Strands SDK 2.0.0 требует pydantic>=2.0.

pip install strands-agents-sdk==2.0.0 exa-py requests
# Настройка переменных окружения
export STRANDS_API_KEY="sk-..."
export EXA_API_KEY="exa-..."
export OPENAI_API_KEY="sk-..."  # или любой другой LLM провайдер

Кстати, Strands SDK теперь поддерживает не только AWS Bedrock, но и OpenAI, Anthropic, Google Gemini. В этом примере я использую GPT-4o-mini, потому что он дёшев и быстр.

2 Создаём инструменты Exa

Главная фишка — мы определяем два инструмента: exa_search (ищет ссылки) и exa_get_contents (грузит полный текст). Агент сам решает, когда какой вызывать.

from strands import Tool, Agent
from exa_py import Exa

exa = Exa(api_key="...")

def exa_search(query: str, num_results: int = 5) -> list[dict]:
    """Поиск в вебе через Exa. Возвращает список результатов с заголовком, url, snippet."""
    response = exa.search(query, num_results=num_results, type="neural")
    return [
        {
            "title": r.title,
            "url": r.url,
            "snippet": r.snippet,
            "published_date": r.published_date
        }
        for r in response.results
    ]

def exa_get_contents(urls: list[str]) -> list[dict]:
    """Получает полный текст страниц по списку URL."""
    response = exa.get_contents(urls)
    return [
        {
            "url": c.url,
            "text": c.text[:5000],  # обрезаем, чтобы не раздувать контекст
            "author": c.author
        }
        for c in response.contents
    ]

search_tool = Tool(name="exa_search", func=exa_search, description="Поиск в интернете. Используй, когда нужно найти свежие данные.")
contents_tool = Tool(name="exa_get_contents", func=exa_get_contents, description="Получить полный текст страницы по URL. Используй после поиска.")

⚠️ Частая ошибка: type="neural" в Exa даёт семантический поиск, но медленнее. Для новостей лучше type="keyword" — дешевле и быстрее. Экспериментируйте.

3 Собираем агента

Strands SDK использует declarative конфиг. Мы задаём LLM, список инструментов, максимальное число итераций и системный промпт.

agent = Agent(
    name="web_researcher",
    llm={
        "provider": "openai",
        "model": "gpt-4o-mini-2026-05-12",
        "api_key": "..."
    },
    tools=[search_tool, contents_tool],
    max_iterations=5,
    system_prompt="""Ты — исследовательский агент. Твоя задача — отвечать на вопросы пользователя, используя веб-поиск.
    Алгоритм:
    1. Если вопрос подразумевает свежие данные, сначала вызови exa_search.
    2. Если нужно проверить детали, вызови exa_get_contents для конкретных URL.
    3. Обязательно указывай источники в ответе в формате [1], [2] и ссылки.
    4. Если информации недостаточно, задай уточняющий вопрос."""
)

# Запуск
result = agent.run("Какие новые модели LLM выпустили в апреле 2026? Найди 3 источника и суммируй их.")
print(result)

4 Добавляем валидацию и кэширование (production-ready)

Чистый код из прошлого шага упадёт при rate limit Exa (50 req/min на бесплатном плане). Используем декоратор с повторными попытками и кэширование в Redis, чтобы не долбить API одинаковыми запросами.

from strands.cache import RedisCache
from tenacity import retry, stop_after_attempt, wait_exponential

cache = RedisCache(ttl=3600)  # час

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def exa_search_cached(query: str, num_results: int = 5) -> list[dict]:
    cache_key = f"exa_search:{query}:{num_results}"
    if cached := cache.get(cache_key):
        return cached
    result = exa_search(query, num_results)
    cache.set(cache_key, result)
    return result

# Переопределяем инструмент
search_tool_cached = Tool(name="exa_search", func=exa_search_cached, description=...)

Реальный сценарий: фактчекинг заявления

Допустим, пользователь говорит: "Я слышал, что Anthropic выпустила модель Claude 4.5 в мае 2026. Это правда?". Агент запускает поиск, получает 5 ссылок, среди них — официальный блог Anthropic и новость на TechCrunch. Затем вызывает exa_get_contents для этих двух URL, читает текст и отвечает: "Да, 12 мая 2026 Anthropic анонсировала Claude 4.5 (source: blog.anthropic.com [1]), но TechCrunch пишет [2], что это не замена Claude 4, а промежуточная версия."

💡
Для глубоких исследований такой подход незаменим. Сравните с агентами, которые пытаются перевести SQL в естественный язык — там другая боль, но принцип тот же: инструмент должен быть спроектирован под структуру данных.

Грабли, на которые я наступил (и вы наступите)

  1. Контекстное окно. Exa возвращает до 10 000 символов на страницу. После двух страниц контекст может переполниться. Решение: обрезайте текст до 2000-3000 символов в инструменте, а если нужно больше — вызывайте отдельно.
  2. Галлюцинации ссылок. Модель может выдумать URL, которых нет в результатах Exa. Всегда проверяйте: в инструменте возвращайте список {url, text}, а в промпте требуйте ссылаться только на реальные URL из вызова.
  3. Rate limit Exa. Бесплатный план — 50 запросов/мин. На бесплатном плане я получал 429 ошибки. Решение: кэширование (см. шаг 4) и очередь через asyncio для параллельных запросов.

Кстати, про rate limit отлично описано в статье о снижении латентности поиска — там показан подход с батчингом запросов, который легко адаптировать под Exa.

Сравнение: Strands + Exa vs. другие связки

Критерий Strands + Exa LangChain + Tavily n8n + SerpAPI
Контроль над контентом Полный текст (raw) Только summary Только snippet
Стоимость (1000 запросов) ~$3 (Exa) + LLM ~$5 (Tavily Pro) ~$10 (SerpAPI Business)
Простота интеграции с Strands Прямая (3 строки кода) Через LangChain Tool Через HTTP API

Как НЕ надо делать (мой утренний провал)

Я решил сэкономить и не обрезать текст в exa_get_contents. Результат: агент получил 25 000 токенов контекста, запутался и начал выдумывать факты из середины статьи. Пришлось добавить обрезание до 3000 символов и явно указать в промпте: "Используй только первые 3000 символов. Если нужно больше — запроси другой URL".

# Как НЕ надо
def exa_get_contents_no_limit(urls: list[str]):
    return exa.get_contents(urls)  # вернёт всё

# Как надо
def exa_get_contents_trimmed(urls: list[str]):
    contents = exa.get_contents(urls)
    trimmed = []
    for c in contents:
        if len(c.text) > 3000:
            c.text = c.text[:3000] + "... [обрезано]"
        trimmed.append(c)
    return trimmed

Бонус: деплой в production

Strands SDK из коробки поддерживает мониторинг через CloudWatch и X-Ray. Но для простых сценариев достаточно обернуть агента в FastAPI endpoint:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Query(BaseModel):
    text: str

@app.post("/ask")
def ask(query: Query):
    result = agent.run(query.text)
    return {"answer": result.output, "trace": [s.name for s in result.steps]}

И да, не забудьте добавить лимитер запросов на уровне API, чтобы один пользователь не сжёг ваш Exa budget. Я использую slowapi с 10 req/min на IP.

📌
Хотите увидеть, как то же самое можно реализовать на n8n без кода? Читайте гайд по AI-агенту 3-го уровня автономии на n8n. А если вам больше по душе концепция procedure memory для устойчивости к изменениям сайтов — посмотрите Exogram.

Что дальше?

Если вы дочитали до сюда, у вас есть работающий агент с веб-поиском. Но не останавливайтесь. Добавьте sub-agent, который будет ходить за дополнительными источниками — про это есть отдельная статья с тремя сценариями. Или подключите гибридный RAG с OpenSearch (мы разбирали здесь), чтобы агент умел искать не только в интернете, но и в вашей базе знаний.

А самое смешное, что через полгода выйдет Strands SDK 3.0 с нативной поддержкой Exa (судя по дорожной карте AWS), и весь этот код можно будет заменить двумя строками. Но пока — пишем руками и не жалуемся.

Подписаться на канал