← Главная

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 Completions
  • tools.py — получение summary-страницы из Wikipedia REST API
  • cli.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. Приоритет такой:

  1. Переменные окружения (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY, PROXY)
  2. Системный прокси 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, после чего переменная подхватывается из файла.