Нитобэ связывает знание с поступком: мало повторить выученное, интересно увидеть, что с ним можно сделать. Для локального ИИ-агента тоже выберем действие. Модель получит просьбу, найдёт цену через одну функцию и посчитает сумму через другую. Ход работы останется виден в окне программы.
Допустим, вы делаете картонный домик. Стены уже вырезаны, но для крыши нужен ещё лист, а клей заканчивается. Возьмём вымышленный прайс: материал A стоит 120, материал B стоит 80. Нужно купить один A и два B. Сумма маленькая, поэтому её удобно отдать помощнику и тут же проверить по собственным числам.
Цель первой попытки небольшая: увидеть сумму 280, след вызовов и понятную реакцию на неизвестный код.
Как устроен локальный ИИ-агент
У локального ИИ-агента в примере разделены две обязанности. Модель читает просьбу и выбирает подходящий инструмент. До закупки ей выдаём небольшой набор: узнать цену и сложить стоимость. Программа разрешает только эти функции, проверяет аргументы и возвращает результат. После этого та может попросить ещё одно действие или сформулировать ответ.
lookup_price знает цены A и B. calculate_total получает коды и количества, берёт цены из того же прайса и складывает стоимость. У этих двух функций нет отправки запросов в интернет, командной оболочки и записи файлов. Их работа ограничена чтением двух чисел и арифметикой.
Модель предлагает действие, а выполняет его разрешённая функция. Результат отправляется обратно в историю диалога.
Важная часть агента — как раз этот возврат. Если модель попросила узнать цену, а программа только вывела её на экран, следующему ответу недостаёт результата инструмента. Для человека число уже перед глазами, но в новом запросе модели его ещё нет. У каждого участника здесь свой кусок разговора. Поэтому код ниже сохраняет сообщение модели целиком, добавляет ответ функции с её именем и продолжает разговор. Такой цикл описан в руководстве Ollama о вызове инструментов.
Что понадобится для первого запуска
Чтобы локальный ИИ-агент заработал, нужны совместимый компьютер, Ollama и Node.js 22 или новее. Ollama загружает и запускает модель; Node.js выполняет нашу небольшую программу. Знакомство с ними можно начать по официальному quickstart Ollama и установщику Node.js для своей системы.
Для исходной загрузки программы и весов нужен интернет. На странице qwen3:4b указан объём весов около 2,5 ГБ. Память для выполнения — отдельный вопрос: кроме весов, её занимает контекст и работа среды. Число 2,5 не означает, что достаточно ровно такой RAM. Возможности и скорость своего компьютера выясняют при запуске.
Основной путь ниже — для Windows. Для macOS и Linux настройку сервера найдёте в FAQ Ollama. Отдельных библиотек код не требует.
Если вы ещё не работали с такими программами, опору можно получить в обычном чате DeepSeek, без передачи собственных материалов:
Хочу запустить учебный локальный расчёт через Ollama и готовый файл agent.mjs. Уточни мою систему и что уже установлено. Объясняй по одному действию: какое окно открыть, куда вставить команду, какой результат ждать. Для настройки Ollama опирайся на текущую официальную справку. Если её не можешь открыть, попроси у меня нужный фрагмент. Жди моего ответа, а затем помогай со следующим шагом. Ошибку я пришлю без личных данных.
Сами материалы останутся в локальной программе; для объяснения достаточно шагов и текста ошибки.
Отключить облако и выбрать местную модель
В Windows завершите Ollama через значок в области уведомлений. Откройте настройку переменных среды своей учётной записи и создайте переменную OLLAMA_NO_CLOUD со значением 1. Для локального адреса задайте OLLAMA_HOST со значением 127.0.0.1:11434. Примените изменения и заново откройте Ollama из меню «Пуск».
С этими настройками локальный ИИ-агент обращается к серверу своего компьютера, а облачные возможности Ollama отключены. После перезапуска проверьте журнал: в нём должна появиться строка Ollama cloud disabled: true. На Windows откройте в Проводнике папку %LOCALAPPDATA%\Ollama и найдите server.log. Эти настройки и подтверждение описаны в FAQ, расположение журнала — в инструкции по диагностике.
Если в журнале подтверждения нет, сначала выясните, получила ли перезапущенная программа нужную переменную. Присваивание в чужом окне терминала само по себе не меняет настройки уже работающего приложения. Не переходите к своим материалам, пока не понятен выбранный режим.
Теперь откройте PowerShell и введите:
ollama pull qwen3:4b
Дождитесь завершения загрузки, затем запустите короткую проверку:
ollama run qwen3:4b
В открывшемся разговоре напишите «Поздоровайся одной фразой». Когда получите ответ, введите /bye, чтобы выйти из этого разговора. Здесь нужен именно тег qwen3:4b, без облачного варианта. Этот шаг проверяет, что модель отвечает на вашем компьютере; функции расчёта подключим дальше.
Различаем загрузку весов и последующую обработку. Для расчёта готовый исполнитель обращается только к 127.0.0.1.
Модель и две функции работают на компьютере. При добавлении сетевого инструмента изменится и путь данных — отметьте это в своей схеме.
Готовый исполнитель: сохранить один файл
Создайте отдельную папку local-agent. В обычном редакторе сохраните в неё следующий код как agent.mjs. Он использует встроенный fetch в Node.js и обращается к http://127.0.0.1:11434/api/chat. Секретного ключа для этого локального обращения не нужно.
Готовый локальный ИИ-агент уже содержит прайс, описания функций, проверки и предел пяти обращений к модели. Код можно взять целиком.
import { pathToFileURL } from 'node:url';
const endpoint = 'http://127.0.0.1:11434/api/chat';
const model = 'qwen3:4b';
const prices = Object.freeze({ A: 120, B: 80 });
function objectWithKeys(value, keys) {
if (!value || typeof value !== 'object' || Array.isArray(value) ||
Object.keys(value).some(key => !keys.includes(key))) {
throw new Error('Отказ: неверные поля аргументов');
}
}
function price(code) {
if (typeof code !== 'string' || !Object.hasOwn(prices, code)) {
throw new Error('Отказ: неизвестный код материала');
}
return prices[code];
}
export function runTool(name, args) {
if (name === 'lookup_price') {
objectWithKeys(args, ['code']);
return { code: args.code, price: price(args.code) };
}
if (name === 'calculate_total') {
objectWithKeys(args, ['items']);
if (!Array.isArray(args.items) || args.items.length < 1 || args.items.length > 20) {
throw new Error('Отказ: нужно от 1 до 20 строк');
}
const total = args.items.reduce((sum, item) => {
objectWithKeys(item, ['code', 'quantity']);
if (!Number.isInteger(item.quantity) || item.quantity < 1 || item.quantity > 1000) {
throw new Error('Отказ: количество — целое число от 1 до 1000');
}
return sum + price(item.code) * item.quantity;
}, 0);
return { total };
}
throw new Error('Отказ: инструмент не разрешён');
}
const tools = [
{ type: 'function', function: {
name: 'lookup_price', description: 'Узнать цену материала по коду A или B',
parameters: { type: 'object', required: ['code'], additionalProperties: false,
properties: { code: { type: 'string', enum: ['A', 'B'] } } }
} },
{ type: 'function', function: {
name: 'calculate_total', description: 'Посчитать сумму по кодам и количествам; цены берёт из своего прайса',
parameters: { type: 'object', required: ['items'], additionalProperties: false,
properties: { items: { type: 'array', minItems: 1, maxItems: 20,
items: { type: 'object', required: ['code', 'quantity'], additionalProperties: false,
properties: { code: { type: 'string', enum: ['A', 'B'] },
quantity: { type: 'integer', minimum: 1, maximum: 1000 } } } } } }
} }
];
async function send(body) {
const response = await fetch(endpoint, {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(60000),
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
});
if (!response.ok) throw new Error(`Ollama: HTTP ${response.status}`);
return response.json();
}
export async function agentLoop(question, transport = send) {
const messages = [
{ role: 'system', content: 'Ты помощник для расчёта материалов. Доступны цены A и B. Для цен вызывай lookup_price, для суммы calculate_total. Не придумывай цены. При неизвестном коде сообщи, что его нет. Ты можешь вызвать инструменты несколько раз. После расчёта дай короткий ответ по-русски.' },
{ role: 'user', content: question }
];
const trace = [];
for (let round = 1; round <= 5; round++) {
const response = await transport({ model, messages, tools, stream: false, think: false });
const message = response.message;
if (!message || message.role !== 'assistant') throw new Error('Нет сообщения assistant');
messages.push(message);
const calls = message.tool_calls ?? [];
if (!Array.isArray(calls) || calls.length > 4) throw new Error('Неверное число вызовов');
if (calls.length === 0) return { answer: message.content ?? '', trace, rounds: round };
for (const call of calls) {
const name = call.function?.name;
const result = runTool(name, call.function?.arguments);
trace.push({ name, arguments: call.function.arguments, result });
messages.push({ role: 'tool', tool_name: name, content: JSON.stringify(result) });
}
}
throw new Error('Остановка: достигнут предел 5 циклов');
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const question = process.argv.slice(2).join(' ') || 'Узнай цены A и B, затем рассчитай стоимость одного A и двух B.';
try {
const result = await agentLoop(question);
for (const step of result.trace) console.log(JSON.stringify(step));
console.log('Ответ модели:', result.answer);
if (!result.trace.some(step => step.name === 'calculate_total')) {
console.log('Сумма инструментом не проверена. Для расчёта повторите запрос с явной просьбой вызвать calculate_total.');
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
Откройте PowerShell в папке local-agent: в Проводнике можно перейти в эту папку, в адресной строке ввести powershell и нажать Enter. Выполните:
node --version
В ответ должна появиться версия, начинающаяся с v22 или более нового основного номера. Если команда не найдена, проверьте установку Node.js и откройте новое окно PowerShell. Затем запустите файл:
node agent.mjs
Программа отправит просьбу узнать цены и посчитать один A и два B. На экране появятся строки вызовов с именами, аргументами и результатами, а затем текст модели. Ищите calculate_total и total:280 в его результате. Ответ сверяем именно с этим расчётом; одних слов «итого 280» без вызова инструмента для проверки пути мало.
Если модель закончила разговор без расчёта, программа сообщит, что сумма инструментом не проверена. Повторите запуск с явной просьбой:
node agent.mjs "Узнай цены A и B, затем обязательно вызови calculate_total для одного A и двух B."
Так вы уточняете нужное действие, сохраняя готовый проверяемый код.
Сверить сумму и неизвестный код
В прайсе A=120, B=80. Значит, один A и два B дают 120+2×80=280. Чтобы проверить, какие материалы выбрал локальный ИИ-агент, посмотрите на аргументы items: у A должно быть количество 1, у B — 2. Это отдельная проверка: функция безошибочно посчитает и два листа вместо одного, если такие количества ей передали. Но для нашей крыши получилась бы другая покупка. Поэтому сначала сверяем предметы и количества, затем итог.
| Что проверяем | Ожидаемый результат | На что смотреть |
|---|---|---|
| Один A и два B | 280 | Коды, количества и результат calculate_total |
| Материал unknown | Сообщение, что такого кода нет | Отсутствие придуманной цены; попытка вызвать функцию с неизвестным кодом получает отказ |
Для второго вопроса в том же PowerShell выполните:
node agent.mjs "Сколько стоит материал unknown?"
Модель может сразу объяснить, что такого кода нет. Если попробует передать его функции, исполнитель остановит обработку с отказом. В смете не возникнет третий материал с ценой, пришедшей в голову посреди разговора. Ни один из этих путей не должен превращать неизвестный материал в число. Код также отклоняет постороннее имя функции, строку вместо целого количества и выход за заданный диапазон.
На схеме показаны ожидаемые результаты. Фактический ход модели смотрите в выводе своего запуска.
Сохраните себе текст запроса и строки вызовов: их можно скопировать из окна в заметку. Рядом подпишите тег модели, адрес, подтверждение отключённого облака и итог двух вопросов. Получится маленькая карта того, что действительно выполнялось у вас. С новым материалом сравните ход по тем же признакам.
Если вместо результата ошибка
Не удалось соединиться. Проверьте, запущена ли Ollama и совпадает ли её адрес с 127.0.0.1:11434. Затем посмотрите журнал. В этом маршруте локальный ИИ-агент не ищет запасной облачный сервер; сбой соединения останется видимым.
Модель не найдена. Сверьте, завершилась ли загрузка qwen3:4b. Команда ollama list в PowerShell покажет доступные модели; нужный тег должен быть в списке. Не заменяйте его случайным облачным названием ради исчезновения ошибки — для этого примера нам нужен выбранный местный вариант.
Сработал предел пяти циклов. Сначала прочитайте вопрос и доступный ход. Можно упростить просьбу до одного расчёта. Бесконечно повышать предел не нужно: он как раз помогает заметить, что модель повторяет действия вместо завершения. При неизвестных аргументах исправляют конкретный код или количество, а не убирают проверку целиком.
После окончания работы можно освободить память модели командой ollama stop qwen3:4b в том же PowerShell. Она выгружает модель; закрыть само приложение Ollama можно отдельно, когда оно больше не нужно.
Вопросы и ответы о локальном агенте
Почему нельзя просто попросить модель посчитать?
Модель выбирает действие, функция возвращает проверяемое число. Здесь изучаем этот путь; след показывает работу каждого участника.
Какая память понадобится?
Размер загружаемых весов и память для работы — разные величины. На расход влияют модель, контекст и способ выполнения. Для вашего устройства полезнее проверить совместимость Ollama и фактический запуск, чем вывести RAM из размера файла.
Есть ли у функций доступ ко всем файлам?
У локального ИИ-агента из примера функции читают встроенный прайс и считают сумму. Других действий им не дали. Если позже добавить чтение папки, сетевой поиск или запись, это будет уже расширение набора возможностей и карты данных.
Чем этот путь отличается от Qwen-Agent?
Здесь мы используем локальный API Ollama и небольшой исполнитель с двумя функциями. Если захотите другой способ собирать агента через готовый фреймворк, можно перейти к разбору Qwen-Agent. Это отдельный следующий маршрут.
У помощника появилась проверяемая работа
Локальный ИИ-агент интересен возможностью связать свою просьбу с понятным действием на компьютере. После двух материалов можно попробовать небольшой личный справочник или другой расчёт, добавляя функции постепенно. Практика агентов в «Первопроходцах» помогает познакомиться с направлением и условиями участия. В клубе можно задавать вопросы и дорабатывать свои проекты; раз в две недели проходят эфиры, личная проверка проекта Никитой доступна в соответствующем варианте участия. Путь к участию — на странице клуба.
А картонный домик пока дождётся своего клея. Вместо долгого разговора о красоте крыши в нашем следе есть дело покороче: A, два B и сумма 280. У помощника появился собственный маленький участок работы. Стены пока держим сами.