← Главная

213 просм. 5 мин чтения

Mini QA Agent: почему запросы не шли через прокси и как это исправить

Konstantin Anisimoff

Я экспериментировал с примером из книги «Построение LLM-агентов с помощью RAG, графов знаний и осмысления» (Building LLM Agents with RAG, Knowledge Graphs & Reflection, ISBN 978-5-93700-481-9, издательство «ДМК Пресс»).

Mini QA Agent — минимальный question-answering-агент на Python. Он принимает вопрос пользователя, решает, нужна ли внешняя информация, при необходимости запрашивает Wikipedia, а затем формирует ответ через OpenAI API.

Исходный код я взял из первой главы книги без изменений. Проект состоит из пяти основных модулей:

  • agent.py — оркестрация: контроллер решает, нужен ли внешний источник, а композер формирует финальный ответ;
  • llm.py — вызов OpenAI Chat Completions;
  • tools.py — получение краткой информации из Wikipedia REST API;
  • cli.py — интерфейс командной строки;
  • serve.py — HTTP API на FastAPI.

Проблема

Проблема возникла при первом же запуске:

Bash
python cli.py "Кто изобрёл телефон?"

Агент должен был обратиться к api.openai.com и en.wikipedia.org. Оба запроса выполнялись через httpx, но прямое соединение с OpenAI из России сети не работает. Запросы требовалось направлять через прокси, однако агент пытался подключиться напрямую и завершался с ошибкой соединения.

Диагностика

Сначала я посмотрел, как создаётся HTTP-клиент:

Text
with httpx.Client(timeout=60) as client:
    ...

Параметр proxy здесь не указан. При этом httpx по умолчанию использует trust_env=True и читает стандартные переменные окружения:

  • HTTP_PROXY;
  • HTTPS_PROXY;
  • ALL_PROXY;
  • NO_PROXY.

Я не экспортировал их в текущую сессию оболочки, поэтому httpx не получил адрес прокси. Чтобы проверить, какие настройки доступны Python, я выполнил:

Text
>>> import urllib.request
>>> urllib.request.getproxies()
{}

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

Решение

Я вынес определение прокси в отдельный модуль и стал явно передавать результат при создании каждого httpx.Client.

Порядок поиска получился таким:

  1. Стандартные переменные окружения: HTTPS_PROXY, HTTP_PROXY, ALL_PROXY.
  2. Дополнительная переменная PROXY.
  3. Системные настройки GNOME через gsettings.

Новый файл http_utils.py

Text
import os
import shutil
import subprocess

def get_proxy() -> str | None:
    for key in (
        "HTTPS_PROXY",
        "https_proxy",
        "HTTP_PROXY",
        "http_proxy",
        "ALL_PROXY",
        "all_proxy",
        "PROXY",
    ):
        if value := os.getenv(key):
            return value

    return _gnome_proxy()

def _gnome_proxy() -> str | None:
    if shutil.which("gsettings") is None:
        return None

    try:
        def gget(schema: str, key: str) -> str:
            result = subprocess.run(
                ["gsettings", "get", schema, key],
                capture_output=True,
                text=True,
                timeout=2,
                check=True,
            )
            return result.stdout.strip().strip("'")

        if gget("org.gnome.system.proxy", "mode") != "manual":
            return None

        host = gget("org.gnome.system.proxy.http", "host")
        port = gget("org.gnome.system.proxy.http", "port")

        if host and port:
            return f"http://{host}:{port}"
    except (OSError, subprocess.SubprocessError):
        pass

    return None

Функция сначала проверяет переменные окружения. Если они не заданы, _gnome_proxy() читает режим, адрес и порт HTTP-прокси из настроек GNOME. Если gsettings недоступен, режим не равен manual или команда завершается с ошибкой, функция возвращает None.

Изменение llm.py

Text
from http_utils import get_proxy

# ...

with httpx.Client(timeout=60, proxy=get_proxy()) as client:
    response = client.post(url, headers=headers, json=payload)

Изменение tools.py

Text
from http_utils import get_proxy

# ...

with httpx.Client(timeout=20, proxy=get_proxy()) as client:
    response = client.get(
        url,
        headers={"accept": "application/json"},
    )

agent.py, cli.py и serve.py менять не потребовалось: настройка применяется только там, где создаются HTTP-клиенты.

Проверка

Сначала я убедился, что функция находит адрес прокси:

Bash
python -c "from http_utils import get_proxy; print(get_proxy())"
# http://127.0.0.1:12334

Затем повторно запустил агента:

Bash
python cli.py "Кто изобрёл телефон?"

На этот раз соединение прошло через прокси, и агент сформировал ответ.

Почему я не ограничился переменными окружения

Самый простой вариант — экспортировать адрес прокси перед запуском:

Bash
export HTTP_PROXY="http://127.0.0.1:12334"
export HTTPS_PROXY="http://127.0.0.1:12334"

python cli.py "Кто изобрёл телефон?"

Для большинства проектов это даже предпочтительнее: приложение остаётся независимым от конкретной среды рабочего стола. В моём эксперименте хотелось автоматически использовать уже настроенный прокси GNOME и не повторять команды в каждой новой сессии, поэтому я добавил fallback через gsettings.

Ограничения

Текущая реализация решает конкретную локальную задачу, но не является универсальным менеджером прокси:

  • fallback через gsettings работает только в GNOME;
  • читаются настройки HTTP-прокси, но отдельно не обрабатываются HTTPS- и SOCKS-схемы GNOME;
  • не предусмотрены логин и пароль, однако в моем случае это вообще лишнее;
  • адрес определяется при каждом создании клиента;
  • ошибки чтения системных настроек намеренно скрываются, и функция возвращает None.

Для production-приложения я бы предпочёл явную конфигурацию через переменные окружения или файл настроек. Можно еще добавить логирование и тесты для разных источников конфигурации.

Дополнение: запуск в Windows

В Windows соединение установилось без дополнительной настройки прокси, но агент завершился с другой ошибкой:

Text
RuntimeError: OPENAI_API_KEY is not set

Ключ уже находился в .env, однако исходный код не загружал этот файл. Я добавил зависимость python-dotenv и вызов load_dotenv() в llm.py, после чего OPENAI_API_KEY стал доступен приложению.

Итог

Причина ошибки была в конфигурации HTTP-клиентов. Python не видел системный прокси GNOME, нужные переменные окружения отсутствовали.

После добавления http_utils.py с функцией get_proxy(), которая сначала проверяет переменные окружения, а затем использует настройки GNOME как запасной источник. В llm.py и tools.py результат функции явно передаётся в httpx.Client.

Файл Изменение
http_utils.py Добавлено определение прокси из переменных окружения и настроек GNOME
llm.py В httpx.Client передаётся proxy=get_proxy()
tools.py В httpx.Client передаётся proxy=get_proxy()

Теперь Mini QA Agent успешно обращается к OpenAI.