Урок 5. Сборка CLI-агента
Собираем всё вместе: цикл из урока 2, ограждения из урока 3, инструменты из урока 4. Получается CLI-агент — программа, которая в цикле думает, зовёт инструменты и отвечает. Ровно так дирижирует ZCode: задача → рассуждение → инструмент → наблюдение → снова, пока не готов ответ.
┌───────────────────────────────┐
│ задача пользователя │
└───────────────┬───────────────┘
│
▼
┌───────────────────────────────┐
▶│ КОНТЕКСТ (system + история) │
│└───────────────┬───────────────┘
│ │
│ ▼
│ ┌───────┐
│ │ LLM │ финальный текст
│ └───┬───┬─────────────────▼
вызов JSON │ ┌─────────┐
│ ▼ │ ОТВЕТ │
│┌───────────────────────────────┐│ (текст) │
││ ОГРАЖДЕНИЯ (белый список) │└─────────┘
││ перехват ошибок, проверка │
│└───────────────┬───────────────┘
│ │
│ ▼
│┌───────────────────────────────┐
││ ИНСТРУМЕНТ: calc / time │
│└───────────────┬───────────────┘
│ │
┐────────────────└наблюдение
Запуск
Три переменные окружения — подходит любой OpenAI-совместимый API (OpenAI, Groq, POLZA, локальный Ollama):
| Переменная | Что это | Пример |
|---|---|---|
LLM_URL |
эндпоинт chat completions | https://api.openai.com/v1/chat/completions |
LLM_KEY |
ключ API | sk-... |
LLM_MODEL |
имя модели | gpt-4o-mini или sao10k/l3-lunaris-8b |
python mini_agent.py "Сколько будет 123*45, и который час?"
node mini_agent.js "Сколько будет 123*45, и который час?"
Код целиком — Python (~60 строк)
# mini_agent.py — минималистичный CLI-агент
import datetime, json, os, re, urllib.request
LLM_URL = os.environ.get("LLM_URL", "https://api.openai.com/v1/chat/completions")
LLM_KEY = os.environ.get("LLM_KEY", "no-key")
LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-4o-mini")
MAX_STEPS = 6
SYSTEM = ("Ты — агент-помощник в терминале. Отвечай по-русски, кратко. "
"Если нужен инструмент, ответь ровно одним JSON-объектом: "
'{"tool": "calc" или "time", "args": {...}}. Иначе — обычный текст.')
def calc(expr):
return eval(expr, {"__builtins__": {}}, {}) # без импортов — ограждение
def time():
return datetime.datetime.now().strftime("%H:%M %d.%m.%Y")
TOOLS = {"calc": calc, "time": time}
def chat(messages):
req = urllib.request.Request(
LLM_URL,
data=json.dumps({"model": LLM_MODEL, "messages": messages,
"temperature": 0.2}).encode(),
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {LLM_KEY}"})
with urllib.request.urlopen(req, timeout=60) as r:
return json.load(r)["choices"][0]["message"]["content"]
def parse_tool(text):
try:
obj = json.loads(text)
except json.JSONDecodeError:
m = re.search(r"\{.*\}", text, re.DOTALL)
try:
obj = json.loads(m.group(0)) if m else None
except json.JSONDecodeError:
obj = None
return obj if isinstance(obj, dict) and "tool" in obj else None
def run_tool(call):
fn = TOOLS.get(call.get("tool"))
if not fn:
return "Инструмент не найден"
try:
result = fn(**(call.get("args") or {}))
except Exception as e:
return f"Инструмент упал: {e}"
if result is None:
return "Инструмент вернул пустоту"
return result
def run(task):
messages = [{"role": "system", "content": SYSTEM},
{"role": "user", "content": task}]
for _ in range(MAX_STEPS): # Harness: лимит шагов
reply = chat(messages)
call = parse_tool(reply)
if not call:
return reply # финальный ответ
messages.append({"role": "assistant", "content": reply})
messages.append({"role": "user",
"content": f"Наблюдение: {run_tool(call)}"})
return f"Остановлен лимитом шагов ({MAX_STEPS})"
if __name__ == "__main__":
task = input("агент> ").strip() or "Сколько будет 123*45, и который час?"
print(run(task))
Код целиком — JavaScript / Node (~55 строк)
// mini_agent.js — минималистичный CLI-агент (Node 18+, fetch встроен)
const LLM_URL = process.env.LLM_URL || "https://api.openai.com/v1/chat/completions";
const LLM_KEY = process.env.LLM_KEY || "no-key";
const LLM_MODEL = process.env.LLM_MODEL || "gpt-4o-mini";
const MAX_STEPS = 6;
const SYSTEM = "Ты — агент-помощник в терминале. Отвечай по-русски, кратко. " +
"Если нужен инструмент, ответь ровно одним JSON-объектом: " +
'{"tool": "calc" или "time", "args": {...}}. Иначе — обычный текст.';
const TOOLS = {
calc: (args) => Function('"use strict"; return (' + args.expr + ')')(),
time: () => new Date().toLocaleString("ru-RU"),
};
async function chat(messages) {
const r = await fetch(LLM_URL, {
method: "POST",
headers: { "Content-Type": "application/json",
Authorization: `Bearer ${LLM_KEY}` },
body: JSON.stringify({ model: LLM_MODEL, messages, temperature: 0.2 }),
});
const data = await r.json();
return data.choices[0].message.content;
}
function parseTool(text) {
try {
const obj = JSON.parse(text);
return obj && obj.tool ? obj : null;
} catch {
const m = String(text).match(/\{[\s\S]*\}/);
if (!m) return null;
try {
const obj = JSON.parse(m[0]);
return obj && obj.tool ? obj : null;
} catch { return null; }
}
}
function runTool(call) {
const fn = TOOLS[call.tool];
if (!fn) return "Инструмент не найден";
try {
const result = fn(call.args || {});
if (result === undefined || result === null)
return "Инструмент вернул пустоту";
return String(result);
} catch (e) {
return "Инструмент упал: " + e;
}
}
async function run(task) {
const messages = [{ role: "system", content: SYSTEM },
{ role: "user", content: task }];
for (let step = 0; step < MAX_STEPS; step++) {
const reply = await chat(messages);
const call = parseTool(reply);
if (!call) return reply;
messages.push({ role: "assistant", content: reply });
messages.push({ role: "user", content: "Наблюдение: " + runTool(call) });
}
return "Остановлен лимитом шагов (" + MAX_STEPS + ")";
}
const task = process.argv[2] || "Сколько будет 123*45, и который час?";
run(task).then(console.log);
Пример сессии
$ python mini_agent.py "Сколько будет 123*45, и который час?"
[шаг 1] LLM: {"tool": "calc", "args": {"expr": "123*45"}} ← вызов
наблюдение: 5535 ← в контекст
[шаг 2] LLM: {"tool": "time", "args": {}} ← вызов
наблюдение: 14:32 24.08.2026 ← в контекст
[шаг 3] LLM: 123 * 45 = 5535, сейчас 14:32 (24.08.2026). ← ответ
Практика
- Запусти агента на своём ключе (переменные
LLM_URL/LLM_KEY/LLM_MODEL). - Добавь инструмент из урока 4 (
diceилиflip) — правка в двух местах:TOOLSи системный промпт. - Задай задачу, в которой агент ошибётся и поправится через наблюдение — посмотри на траекторию в контексте.
Что дальше
Куда расти: память через RAG (гайд «RAG с нуля»), стандартные инструменты через MCP, оценка качества — темы следующих гайдов раздела. Теория под каждый шаг — в заметках: Агентный цикл в коде, Harness-инженерия, Код как инструмент мышления.
Связано: Урок 4. Инструменты, Обучение