Перевод статьи 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_led48829678
How hot is this Raspberry Pi?get_temperature487303149
Blink the LED 2 times.blink_led47531483
Take a photo.take_photo46124876
Save a note that says the cooler is working.save_note475305107
What is the capital of France?(нет)47029792

Замеры сделаны на 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.