213 просм. 5 мин чтения
Mini QA Agent: почему запросы не шли через прокси и как это исправить
Я экспериментировал с примером из книги «Построение 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.
Проблема
Проблема возникла при первом же запуске:
python cli.py "Кто изобрёл телефон?"
Агент должен был обратиться к api.openai.com и en.wikipedia.org. Оба запроса выполнялись через httpx, но прямое соединение с OpenAI из России сети не работает. Запросы требовалось направлять через прокси, однако агент пытался подключиться напрямую и завершался с ошибкой соединения.
Диагностика
Сначала я посмотрел, как создаётся HTTP-клиент:
with httpx.Client(timeout=60) as client: ...
Параметр proxy здесь не указан. При этом httpx по умолчанию использует trust_env=True и читает стандартные переменные окружения:
HTTP_PROXY;HTTPS_PROXY;ALL_PROXY;NO_PROXY.
Я не экспортировал их в текущую сессию оболочки, поэтому httpx не получил адрес прокси. Чтобы проверить, какие настройки доступны Python, я выполнил:
>>> import urllib.request >>> urllib.request.getproxies() {}
Пустой словарь подтвердил причину: прокси был настроен в GNOME, но не был доступен через переменные окружения.
Решение
Я вынес определение прокси в отдельный модуль и стал явно передавать результат при создании каждого httpx.Client.
Порядок поиска получился таким:
- Стандартные переменные окружения:
HTTPS_PROXY,HTTP_PROXY,ALL_PROXY. - Дополнительная переменная
PROXY. - Системные настройки GNOME через
gsettings.
Новый файл http_utils.py
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
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
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-клиенты.
Проверка
Сначала я убедился, что функция находит адрес прокси:
python -c "from http_utils import get_proxy; print(get_proxy())"
# http://127.0.0.1:12334
Затем повторно запустил агента:
python cli.py "Кто изобрёл телефон?"
На этот раз соединение прошло через прокси, и агент сформировал ответ.
Почему я не ограничился переменными окружения
Самый простой вариант — экспортировать адрес прокси перед запуском:
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 соединение установилось без дополнительной настройки прокси, но агент завершился с другой ошибкой:
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.