Скажите честно: сколько раз вы запускали тяжелый 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, без головной боли.
Проблема: почему я ненавижу 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 или передавать при запуске.
--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 limiting | API возвращает 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"
fi3Интеграция с шеллом
Хотите выполнить команду, сгенерированную 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, где лишние зависимости запрещены политикой безопасности.
Вместо заключения
Я не призываю всех переходить на bash-REPL. Но иногда простота — это фича. Следующий раз, когда захотите «быстро потестить новую модель» или «написать бота для себя» — попробуйте обойтись curl и jq. Удивитесь, как много можно сделать без единого SDK.
А если надумаете усложнить — готовый код лежит в репозитории (ссылка в конце статьи). Но лучше напишите свой. Процесс важнее результата.