Needle: языковая модель на 14 МБ превращает текстовые команды в действия на Raspberry Pi

Перевод статьи Turn text input into actions with Needle, a 14MB function-calling LLM — Ashley, Raspberry Pi.
В этой статье наши друзья из Cactus Compute показывают, как Needle 2 — модель с вызовом функций размером всего 14 МБ — превращает фразы на обычном английском в локальные действия на Raspberry Pi 5, обходясь одним процессором. Кстати, уже вышла и свежая Needle 3.
Набираем «Turn the LED on» («Включи светодиод») — и через 78 миллисекунд светодиод на нашей Raspberry Pi 5 загорается. Это Needle 2, работающая только на CPU, без всякой отдельной ИИ-платы расширения.
Начнём с того, чем Needle не является. Это не чат-бот. Команда Cactus Compute обучала её ровно одной задаче: надёжно выполнять структурированные действия прямо на устройстве.
«Needle 2 — вещь просто отличная» — Эбен Аптон, генеральный директор Raspberry Pi
Установка#
Схема простая: вы объявляете функции на Python, а Needle выбирает, какую из них вызвать, и подставляет аргументы. Мы с её помощью писали локальные заметки, опрашивали vcgencmd, управляли светодиодами и не только — всё на Raspberry Pi 5 без ускорителя.
python3 -m venv needle-env
source needle-env/bin/activate
python -m pip install cactus-needle
После первой загрузки Needle работает локально — без облачного API и без подключения к сети. Для первого примера никакого дополнительного железа не нужно.
Объявляем инструменты#
import needle
from pathlib import Path
import subprocess
notes_path = Path("needle-notes.txt")
Класс Needle принимает функции, помеченные декоратором @needle.tool. Декоратор строит схему инструмента из имени функции, её докстринга и аннотаций типов. В save_note ниже аннотация сообщает Needle, что text должен быть строкой.
@needle.tool
def save_note(text: str):
"""Append a note to a local text file."""
with notes_path.open("a", encoding="utf-8") as notes:
notes.write(text + "\n")
return {"text": text, "path": str(notes_path)}
@needle.tool
def get_temperature():
"""Return the current CPU temperature of this Raspberry Pi in Celsius."""
out = subprocess.check_output(["vcgencmd", "measure_temp"], text=True)
return {"temperature_c": float(out.split("=")[1].split("'")[0])}
agent = needle.Needle(tools=[save_note, get_temperature])
response = agent.run("Save a note that says the cooler is working.")
Внутри run() модель сначала выдаёт вот такой выбор инструмента (вывод сокращён для наглядности):
{
"type": "call",
"function_calls": [
{
"name": "save_note",
"arguments": { "text": "the cooler is working" }
}
]
}
Затем run() выполняет функцию и кладёт возвращённое значение в response["results"]:
[
{
"text": "the cooler is working",
"path": "needle-notes.txt"
}
]
В этом запуске первоначальный вызов complete() внутри run() выбрал функцию и сформировал аргументы за 107 мс.
Можно спросить и так:
agent.reset()
response = agent.run("How hot is this Raspberry Pi?")
Needle вызывает:
{
"type": "call",
"function_calls": [
{"name": "get_temperature", "arguments": {}}
]
}
run() запускает vcgencmd и добавляет показание в результаты:
{ "temperature_c": 49.9 }
Для проверки выбора инструментов мы также объявили set_led(on), blink_led(times) и take_photo(), которая снимает кадр через rpicam-still. Needle выбирает инструмент, а что происходит дальше, решает обычный код на Python. Точно так же декоратором @needle.tool можно пометить любую функцию с понятным именем, аннотациями типов и описанием.
Модель, которая умеет отказывать#
Needle намеренно узкоспециализирована. Спросите «What is the capital of France?» («Какая столица у Франции?») — и через 92 мс она вернёт { "function_calls": [] }, потому что ни один из выданных ей инструментов на такой вопрос не отвечает. Для модели действий отказ от посторонней просьбы — как раз правильная реакция.
Производительность#
Собственная сессия Needle занимает около 28 МБ памяти. В этом демо на Python весь процесс вместе с интерпретатором на пике занимал от 43 до 46,4 МБ. Вот ещё несколько запусков:
| Запрос | Вызов | Prefill, ток/с | Decode, ток/с | Время, мс |
|---|---|---|---|---|
| Turn the LED on. | set_led | 488 | 296 | 78 |
| How hot is this Raspberry Pi? | get_temperature | 487 | 303 | 149 |
| Blink the LED 2 times. | blink_led | 475 | 314 | 83 |
| Take a photo. | take_photo | 461 | 248 | 76 |
| Save a note that says the cooler is working. | save_note | 475 | 305 | 107 |
| What is the capital of France? | (нет) | 470 | 297 | 92 |
Замеры сделаны на Raspberry Pi 5 с 8 ГБ памяти под Raspberry Pi OS, только на CPU, с cactus-needle 2.0.7. Скорости prefill (обработка запроса) и decode (генерация ответа) взяты из внутренних счётчиков сессии Needle. Время в последнем столбце — это реальное время одного вызова complete(), до запуска выбранной Python-функции.
Что дальше#
Если у вас уже есть функции на GPIO Zero, их можно просто пометить декоратором и передать модели. То же самое относится к любой другой логике приложения. А ещё Needle можно дообучить локально, на обычном ноутбуке, под свой конкретный набор инструментов.
Веса и код лежат на Hugging Face и GitHub; оба распространяются под лицензией Apache 2.0.