Mini QA Agent: почему запросы не шли через прокси и как я это исправил
Konstantin Anisimoff · · 7 просм.Преамбула
Сегодня читал книгу «Построение LLM-агентов с помощью RAG, графов знаний и осмысления» (оригинальное название: Building LLM Agents with RAG, Knowledge Graphs & Reflection, ISBN 978-5-93700-481-9, издательство «ДМК Пресс»). Книга обещает научить строить кластеры LLM-агентов с общей памятью и циклами осмысления.
Mini QA Agent — минимальный question-answering агент на Python. Он принимает вопрос пользователя, решает, нужна ли внешняя информация, при необходимости запрашивает Wikipedia и формирует ответ через OpenAI API. По сути — умный собеседник, который сначала думает «надо ли гуглить», потом гуглит, потом красиво пересказывает.
Исходный код агента я взял из книги без изменений — файлы agent.py, llm.py, tools.py, cli.py и serve.py. Здесь я их не привожу: он полностью совпадает с листингами из 1-й главы. Ctrl+C, Ctrl+V — древнейшая инженерная практика.
Перечислим зоны ответственности этих модулей:
agent.py— оркестрация: контроллер (нужен ли API) и композер (финальный ответ)llm.py— вызов OpenAI Chat Completionstools.py— получение summary-страницы из Wikipedia REST APIcli.py— CLI-обёрткаserve.py— HTTP API на FastAPI
Неудача постигла на первом ходу. Когда я запустил:
python cli.py "Кто изобрел телефон?"
агент обращается к api.openai.com и en.wikipedia.org. Все сетевые запросы идут через httpx. Ясен день, что доступ из России к OpenAI возможен только через прокси, а без явной настройки запросы шли напрямую — и агент падал с ошибкой соединения.
Проблема: ошибка при выполнении
Причина
Смотрим код и видим, что HTTP-клиент создаётся без параметра proxy:
with httpx.Client(timeout=60) as client: ...
httpx по умолчанию (trust_env=True) читает только переменные окружения: HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY. Я их не экспортировал в shell — поэтому прокси не использовался. Классика: настроил прокси в системе, и полагаешь что Python об этом знает.
Дальше я проверил, что видит Python на Linux:
>>> import urllib.request >>> urllib.request.getproxies() {}
Пустой словарь. В общем философский вопрос «нужна ли внешняя информация?» остался без ответа — потому что внешний мир был недоступен.
Предложенное решение
Идея
Я решил вынести логику определения прокси в отдельный модуль и явно передавать его в каждый httpx.Client. Приоритет такой:
- Переменные окружения (
HTTPS_PROXY,HTTP_PROXY,ALL_PROXY,PROXY) - Системный прокси GNOME через
gsettings(fallback для Linux)
Не самое элегантное решение в истории DevOps, зато работает.
Новый файл: 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
Да, я написал код, который дергает gsettings из Python. Возможно, когда-нибудь архитекторы многоагентных систем будут строить self-reflecting agent clusters.
Изменения в 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: r = client.get(url, headers={"accept": "application/json"})
Что я не менял
agent.py, cli.py и serve.py я оставил без изменений — прокси подключается на уровне HTTP-клиентов в llm.py и tools.py. Агент по-прежнему не знает, что ходит через прокси.
Проверка
После правок я проверил, что прокси определяется корректно:
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 "Кто изобрел телефон?"
Но мне не хотелось настраивать это вручную в каждой сессии. Решение через http_utils.py работает автоматически.
Резюме
Я внёс минимальное исправление: добавил модуль http_utils.py с функцией get_proxy(), которая читает прокси из env-переменных или из настроек GNOME. В llm.py и tools.py прокси теперь передаётся явно через proxy=get_proxy(). Остальная логика агента не затронута.
| Файл | Что я изменил |
|---|---|
http_utils.py |
создал |
llm.py |
добавил proxy=get_proxy() |
tools.py |
добавил proxy=get_proxy() |
Проблема с proxy решена и пожалуй, этого на сегодня достаточно.
P.S. настройка llm_agent в Windows
В Windows соединение прошло успешно без дополнительных приседаний, но агент снова упал — на этот раз с RuntimeError: OPENAI_API_KEY is not set. Ключ у меня уже лежал в .env, однако код его не видел.
Добавил python-dotenv и вызов load_dotenv() в llm.py, после чего переменная подхватывается из файла.