Bash REPL для LLM с curl и jq: туториал 2026 | AiManual
AiManual Logo Ai / Manual.
25 Июн 2026 Гайд

Создаём REPL для LLM на чистом Bash: туториал с curl, jq и pipes

Создайте интерактивный REPL для LLM из стандартных Unix-инструментов: curl, jq, pipes. Минимум зависимостей, максимум контроля для DevOps.

Реклама
cliv2

Скажите честно: сколько раз вы запускали тяжелый Python-скрипт или разворачивали очередную Node.js поделку только чтобы поболтать с LLM в терминале? А потом смотрели на pip install очередной тысячи зависимостей и думали: да ну нафиг.

Я тоже устал. Поэтому родился план: сделать REPL для LLM из того, что есть в любом Linux по умолчанию. curl, jq, read, pipes. И голова, конечно.

Да, есть куча готовых решений — от Aider до TUI Chat. Но они требуют Python, Node, а иногда и целый GPU-кластер. А что если у вас только SSH-доступ к серверу с минимальным софтом? Или вы хотите понять, как LLM API работают изнутри, без чёрного ящика SDK?

Вот туториал, который превращает терминал в полноценный AI-чат. Без sudo, без virtualenv, без головной боли.

💡
Кстати, если вам нужно сохранять историю разговоров — загляните в статью «Терминальный амнезиак». Там 50 строк Python делают то же самое, но с кучей зависимостей. Мы обойдёмся меньшим.

Проблема: почему я ненавижу SDK для LLM

Откройте любой гайд по работе с LLM. Везде одно и то же:

  • Python + requests + OpenAI library → 20 мегабайт зависимостей.
  • JavaScript + axios + npm install → 500 пакетов в node_modules.
  • А если надо stream? Ещё две библиотеки.

А базовый REPL — это просто цикл: read input + send to API + print response. Почему для этого нужен целый SDK? Не нужен.

И да, я знаю про локальные LLM. Но сегодня фокус на любой API, совместимый с OpenAI (а таких 99% на 2026 год — включая Llama 4, Mistral, DeepSeek).

Решение: bash-скрипт за 50 строк

Идея простая: мы пишем скрипт, который:

  • хранит историю сообщений в JSON-массиве;
  • читает ввод пользователя;
  • отправляет POST-запрос к /v1/chat/completions через curl;
  • парсит ответ jq и выводит текст;
  • добавляет ответ ассистента в историю.

И всё это в одном файле, без единой pip install.

Предупреждение: Если ваш босс требует тикеты в Jira и мониторинг через Grafana, этот скрипт не заменит enterprise-решения. Но для быстрого прототипа или обучения — идеально.

1Каркас: переменные и история

Начинаем с настройки API. На 2026 год самый удобный вариант — OpenAI или любой совместимый endpoint (например, http://localhost:8080/v1 от llama.cpp).

#!/usr/bin/env bash

# Config
API_URL="${LLM_API_URL:-https://api.openai.com/v1/chat/completions}"
API_KEY="${LLM_API_KEY:-}"
MODEL="${LLM_MODEL:-gpt-4o}"
HISTORY_FILE="/tmp/llm_history.json"

# Initialize history if not exists
if [ ! -f "$HISTORY_FILE" ]; then
    echo '[]' > "$HISTORY_FILE"
fi

Обратите внимание: всё через переменные окружения. Никаких хардкоженных ключей в коде. LLM_API_KEY можно положить в ~/.bashrc или передавать при запуске.

💡
Для теста с локальной LLM (например, llama.cpp) запустите сервер с флагом --host 0.0.0.0 и укажите LLM_API_URL=http://localhost:8080/v1/chat/completions. Подробнее о запуске — в статье «Заставьте llama.cpp выйти в интернет».

2Функция добавления сообщения в историю

add_message() {
    local role="$1"
    local content="$2"
    # Escape content for JSON
    local escaped=$(echo "$content" | jq -Rs .)
    local new_entry=$(jq -n --arg role "$role" --arg content "$content" '{role: $role, content: $content}')
    local updated=$(jq --argjson new "$new_entry" '. + [$new]' "$HISTORY_FILE")
    echo "$updated" > "$HISTORY_FILE"
}

Здесь jq -Rs . — правильный способ экранирования произвольного текста в JSON. Если вы используете просто echo "..." | jq -sR . — это одно и то же. Запоминаем: без экранирования вы получите сломанный JSON от первой же кавычки или эмодзи.

3Функция запроса к LLM

ask_llm() {
    local user_input="$1"
    add_message "user" "$user_input"

    # Build request body from history
    local body=$(jq -c --arg model "$MODEL" '{model: $model, messages: .}' "$HISTORY_FILE")

    # Make API call
    response=$(curl -s -X POST "$API_URL" \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer $API_KEY" \
        -d "$body")

    # Extract assistant message
    assistant_msg=$(echo "$response" | jq -r '.choices[0].message.content // empty')
    if [ -z "$assistant_msg" ]; then
        echo "Error: $response" >&2
        return 1
    fi

    echo "$assistant_msg"
    add_message "assistant" "$assistant_msg"
}

Критический момент: jq -c выводит компактный JSON (без лишних пробелов), чтобы curl не споткнулся о переносы строк. -s у curl — silent mode, иначе каждый запрос будет выводить прогресс-бар в терминал.

Обработка ошибок минимальна: если jq не нашёл поле .content, выводим сырой ответ в stderr. В продакшне нужно парсить код ошибки, но для REPL достаточно.

4REPL-цикл

echo "LLM REPL started. Type 'exit' to quit."
while true; do
    printf "> "
    if ! read -r input; then
        break  # EOF
    fi
    if [ "$input" = "exit" ]; then
        break
    fi
    if [ -z "$input" ]; then
        continue
    fi
    echo ""
    result=$(ask_llm "$input")
    echo "$result"
    echo ""
done

Всё. Скрипт готов. Запускаем bash llm_repl.sh и общаемся.

Ошибка новичка: read -r — обязательный флаг. Без него обратная косая черта в сообщении будет интерпретироваться как escape. Хотите отправить \n — без -r он превратится в перевод строки.

Типичные грабли (и как их избежать)

ПроблемаСимптомРешение
Сломанный JSON при спецсимволахjq ругается на parse errorИспользуйте jq -Rs для экранирования
Пустой ответ от APIВыводится null или ошибкаПроверьте jq -r '.choices[0].message.content // empty'
Rate limitingAPI возвращает 429Добавьте sleep 2 после каждого запроса
История раздуваетсяФайл >10MB, медленные запросыОбрезать историю до N последних сообщений (например, tail -n 20)

С rate limiting можно поступить хитро: положить в переменную last_request_time и не отправлять запрос, если прошло меньше секунды. Но в bash с этим геморрой — проще sleep 1.

Расширения: делаем REPL умнее

Базовый скрипт работает. Но можно улучшить.

1Streaming (без зависимости от внеших утилит)

curl поддерживает --no-buffer и -N для вывод данных по мере поступления. Чтобы парсить SSE (Server-Sent Events), придётся написать небольшой парсер на awk:

curl -s -N -X POST "$API_URL" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $API_KEY" \
    -d "$body" | while IFS= read -r line; do
    case "$line" in
        "data: [DONE]"*) break ;;
        "data: "*)
            content=$(echo "${line#data: }" | jq -r '.choices[0].delta.content // empty' 2>/dev/null)
            printf "%s" "$content"
            ;;
    esac
done

Обратите внимание: мы не сохраняем в историю промежуточные токены, только финальный ответ. Это нормально.

2Системный промпт

Добавьте в начало файла SYSTEM_PROMPT и загружайте его как первое сообщение при создании истории:

SYSTEM_PROMPT="You are a helpful Unix assistant. Keep answers concise."
if [ ! -f "$HISTORY_FILE" ]; then
    jq -n --arg content "$SYSTEM_PROMPT" '[{role: "system", content: $content}]' > "$HISTORY_FILE"
fi

3Интеграция с шеллом

Хотите выполнить команду, сгенерированную LLM, не копируя её вручную? Добавьте команду /run:

if [[ "$input" == /run* ]]; then
    cmd="${input#/run }"
    echo "Executing: $cmd"
    eval "$cmd"
    continue
fi

Но предупреждаю: это опасно. LLM может сгенерировать rm -rf /. Лучше сначала выводить команду и спрашивать подтверждение. Можно почитать статью про встроенные знания LLM о Git — там похожая идея.

Почему это работает (и не сломается)

curl и jq — ветераны Unix. Первый появился в 1997, второй — в 2012. Они есть на каждом сервере. Более того, на 2026 год обе утилиты стабильны: curl 8.12, jq 1.7.1. Никаких breaking changes.

Да, скрипт не умеет в многопоточность, не поддерживает автоматическое обрезание контекста, не сохраняет сессии между перезапусками по-умному. Но он:

  • работает везде, где есть bash (включая Windows через WSL)
  • не требует интернета для установки
  • полностью прозрачен — вы видите каждый запрос и ответ

Полезно как educational tool: дайте этот скрипт джуниору, пусть разберётся, как работают API LLM. Или используйте в CI/CD, где лишние зависимости запрещены политикой безопасности.

💡
Если вам нужно что-то более серьёзное, но тоже без Python — посмотрите статью «Delegation Filter». Там чек-лист, когда вообще стоит подключать LLM к пайплайну, а когда лучше обойтись grep'ом.

Вместо заключения

Я не призываю всех переходить на bash-REPL. Но иногда простота — это фича. Следующий раз, когда захотите «быстро потестить новую модель» или «написать бота для себя» — попробуйте обойтись curl и jq. Удивитесь, как много можно сделать без единого SDK.

А если надумаете усложнить — готовый код лежит в репозитории (ссылка в конце статьи). Но лучше напишите свой. Процесс важнее результата.

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