跳到主要內容

【2026 最新】Needle 2 教學:用 14MB 模型在手機/樹莓派做離線 Tool Calling

最後更新: ·
Needle 2 教學主視覺:14MB 模型在手機與樹莓派做離線 Tool Calling

Needle 2 教學最值得看的,不是「14MB 也能聊天」;它根本不是把 ChatGPT 硬塞進手機。想像家裡斷網時,你對舊手機說「把客廳燈調到 35%」:這個小模型只負責把人話翻成 set_light(room="living_room", brightness=35),真正開燈仍由你的程式執行。

這個定位正好打中需求。截至 2026 年 8 月 12 日查詢,Needle 2 的 Show HN已超過 480 points、累積 160 則以上留言;r/LocalLLaMA 發布串顯示 80 分。留言有人直接追問:列出工具能不能 zero-shot 使用?何時才需要 fine-tune?這些數字是當下的關注度,不是模型品質證明。

這篇專為第一次做 Tool Calling 的讀者寫。我們鎖定 cactus-needle==2.0.1,先跑通第一個結構化呼叫,再用 20 組自訂智慧家庭案例壓力測試 schema、拒答與信心分數;最後說明如何把同一套安全 loop 部署到 64-bit 樹莓派,並畫出手機原生整合路線。你不必先懂模型架構,但需要能在終端機複製貼上指令;手機部分是部署地圖,不是完整 App 專案。

Table of Contents

先說結論:14MB 模型只是翻譯員,不是安全開關

🌵 記憶把手:離線 Tool Calling=Needle 2(翻譯)+Schema(護欄)+Validator(驗票)+Confidence Gate(分流)+失敗路徑(澄清/拒絕/可選雲端)。
少掉前四層,都可能得到「格式完全合法、動作完全錯誤」的工具呼叫;不接雲端時,也可以安全地拒絕或請使用者重說。

Needle 2 的優勢是任務被刻意縮窄:選哪個工具、參數填什麼、輸出結構化 JSON。它不以百科知識、長文聊天或自由推理為主要工作。若你想先理解完整 Agent loop,可搭配 AI Agent Harness 是什麼;Needle 2 比較像 Harness 裡的一顆超小「工具路由器」。

Needle 2 教學流程:自然語言經 14MB 本機模型、schema、validator 與 confidence gate,決定本機工具呼叫、澄清拒絕或可選雲端 fallback
模型只產生候選呼叫;Validator、權限確認,以及澄清、拒絕或可選雲端 fallback 都是應用程式的責任。

Needle 2 是什麼?和一般小 LLM 差在哪

2026 年 8 月 11 日的官方程式碼Needle 2 config,它有約 4,490 萬參數、256-token sliding window、tool-retrieval 與 confidence head,部署格式採 Cactus Quants。同團隊的 SAN 背景論文研究的是 attention-only transformer,不是 Needle 2 的 tool-calling 驗證;論文觀察到這類模型在依賴上下文的答案較強、從權重回想知識較弱,只能用來理解設計方向,不能替產品 benchmark 背書。

另一個關鍵是 constrained decoding。Needle 會把你的型別、範圍、enum 與 pattern 編成 byte-level grammar;解碼時只能走合法路徑。白話說,一般小 LLM 像請助理「盡量照表格填」,Needle 則像直接把不合格式的格子焊死。

但焊死格式不代表理解正確。我們要求房間只能是 living_roombedroomkitchen,再輸入「office」;2.0.1 沒輸出非法字串,卻把它改成合法的 bedroom,confidence 還有 0.8705。Schema 保證「長得合法」,Validator 與測試才負責「意思正確」。

Needle 2 教學 Step 1:用 Python 3.11 安裝並確認離線資產

先用 Python 3.11 建隔離環境。雖然 cactus-needle 2.0.1 的 PyPI metadataPython >=3.9,我們在乾淨 Python 3.9 環境實測時,flax>=0.12.8 找不到相容版本;Python 3.11 則能正常完成安裝。

python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install "cactus-needle==2.0.1"
python -c "import needle; print(needle.__version__)"

你會看到 2.0.1。這個 wrapper 目前固定抓取 2.0.0 engine;第一次建立 Needle(...) 時,它會從 Hugging Face 下載對應平台的 native engine,再快取到 ~/.cache/cactus-needle/。我們的 Apple Silicon cache 是 14,192,000 bytes;完成下載後,以 HF_HUB_OFFLINE=1 重跑 20 組測試仍成功,這才是本文所說的「離線推論」。

find ~/.cache/cactus-needle -type f -maxdepth 3 -ls
HF_HUB_OFFLINE=1 python app.py

14MB 不等於整個安裝只佔 14MB。因為同一個 Python package 也帶 JAX/Flax fine-tuning 依賴,我們的乾淨 venv 約 583MB。三種記憶體數字的邊界要分開看:Cactus 網站寫的是約 28MB native session;我們測官方 macOS arm64 CLI 單次呼叫約 23.3MB max RSS;Python wrapper 單次約 41.4MB,同一 process 跑完 20 組 probe 約 60.7MB。它們不是同一種量測,不能互相替代。若裝置容量吃緊,應在目標 runtime 重量,不要把模型檔大小直接當成 App 的總成本。

Needle 2 教學 Step 2:跑出第一個結構化 Tool Call

把以下內容存成 app.py。這裡刻意用 complete(),因為它只回傳候選呼叫,不會先替你執行函式。

import needle

@needle.tool
def get_weather(city: str):
    "Get the current weather for a city."
    return {"city": city, "temp_c": 27, "sky": "clear"}

agent = needle.Needle(tools=[get_weather])
response = agent.complete("what's it like in Lagos right now?")

print(response["function_calls"])
print(response["confidence"])

我們在 2.0.1 得到 get_weather{"city":"Lagos"},confidence 為 0.9925。GitHub issue #46 回報 run() 最後回傳 type="respond"function_calls=[];我們加了 execution counter 重測後確認,run() 其實先執行天氣函式,再把結果送回模型,所以最後 response 的呼叫陣列會是空的,實際結果則放在 results

這也暴露一個安全重點:2.0.1 的 run() 原始碼會執行每個非空呼叫,沒有先檢查 confidence。我們再用假刪除工具攻擊它:Do not delete note 42. 仍產生 delete_note(42),confidence 0.9821,而且 run() 真的執行。它只適合無副作用的 stub demo;任何真實動作都應走本文後面的手動 gate。

這正是 OWASP Excessive Agency要避免的模式:模型輸出先過最小權限、輸出驗證與高影響操作的人類批准,再碰真正的工具。

Step 3:用 schema 把房間、亮度與溫度鎖住

現在把「文字描述」升級成「可機械檢查的合約」。Literal 鎖住可選值;needle.Field 鎖住數值範圍。工具描述越精確,zero-shot 測試才越有意義。

from typing import Annotated, Literal
import needle

@needle.tool
def set_light(
    room: Literal["living_room", "bedroom", "kitchen"],
    brightness: Annotated[int, needle.Field(ge=0, le=100)],
):
    """Set one room light to an exact brightness percentage.

    Args:
        room: room whose light should change
        brightness: percentage from 0 to 100
    """
    return {"room": room, "brightness": brightness}

agent = needle.Needle(tools=[set_light])
print(agent.complete("Set the living room light to 35 percent."))

接著不要只測 happy path。至少加入超出範圍、未知房間、缺參數、否定句、相似工具、單位、相對數量、中文與 off-topic。像「亮度 135%」在我們的測試沒有產生越界 JSON,但回傳 success=false 與 truncated error;「office 50%」反而產生合法卻錯誤的 bedroom 呼叫。你的 app 必須同時檢查 successerror、非空 calls、允許的工具名與業務語意。

下圖左側的 93.4% BFCL well-formed 與 42.6% BFCL overall 都來自 Cactus 官方產品頁,屬於供應商公布結果;well-formed 是該頁使用的特定 metric 名稱,不應改寫成一般 JSON parse rate 或完整 schema 正確率。右側才是本文不同工具集、不同樣本量的小型實測,兩邊不能直接排名。

Needle 2 教學實測:20 組自訂工具測試、confidence 0.8 與 0.9 的本機執行取捨,以及官方 BFCL well-formed 和 overall 差距
不同資料集不可直接互比;兩組數字共同指向同一件事:格式正確率不能替代語意正確率。

Step 4:confidence threshold 怎麼定?先做自己的小型 eval

官方 confidence 說明把它定義成 post-hoc head(事後計分頭)的分數與 call-token decoding probability 的較小值。這個設計適合分流,但不要把 0.87 讀成「87% 機率正確」。我們 20 組 probe 中,那個把 office 改成 bedroom 的錯誤就是 0.8705。

正確做法是先做一份人審過的 held-out set(未參與調整的測試集),再掃 threshold。本文小樣本在 0.8 時,會讓 5 個正確呼叫與 1 個錯誤呼叫留在本機;拉到 0.9 後,錯誤呼叫歸零,但 8 個正向案例只剩 3 個能本機執行。這不是在推薦 0.9,而是在示範門檻就是錯誤成本與 fallback(備援交棒)成本的交換

for threshold in (0.70, 0.80, 0.90, 0.95):
    accepted = [
        row for row in labelled_results
        if row.get("success") is True
        and row.get("calls")
        and not row.get("error")
        and not row.get("error_code")
        and row.get("confidence", 0) >= threshold
    ]
    false_accepts = sum(not row["correct"] for row in accepted)
    print(threshold, len(accepted), false_accepts)

最好再按工具分級:讀天氣可以接受較低門檻;開燈需要合理範圍;門鎖與付款即使分數很高,也要二次確認。想把 eval、trace 與 rollback 放進完整 Agent 系統,可接著讀 如何實作 AI Agent Harness

Step 5:建立本機執行/澄清/雲端 fallback 路由

下面是最小安全骨架。它先取得候選,接著檢查 response 狀態、工具 allowlist、deterministic validator 與每工具 threshold。高風險工具不會自動執行,而是進入另做參數驗證、身分驗證與授權的確認流程;語意對不上時先請使用者澄清,不讓另一個模型繼續猜。只有低風險、通過全部條件的呼叫才在本機執行。

import hashlib
import json
import re

THRESHOLD = {
    "get_weather": 0.80,
    "set_light": 0.90,
}
HIGH_RISK = {"unlock_door", "send_money", "delete_file"}
KNOWN_TOOLS = set(THRESHOLD) | HIGH_RISK
NEGATIONS = ("do not", "don't", "never", "不要", "別")
MAX_CALLS = 2

ROOM_WORDS = {
    "living_room": ("living room", "客廳"),
    "bedroom": ("bedroom", "臥室"),
    "kitchen": ("kitchen", "廚房"),
}

def parse_calls(response):
    if not isinstance(response, dict) or response.get("success") is not True:
        return None
    if response.get("error") or response.get("error_code"):
        return None
    calls = response.get("function_calls")
    if not isinstance(calls, list) or not 1 <= len(calls) <= MAX_CALLS:
        return None

    seen = set()
    for call in calls:
        if not isinstance(call, dict):
            return None
        name, args = call.get("name"), call.get("arguments")
        if name not in KNOWN_TOOLS or not isinstance(args, dict):
            return None
        try:
            fingerprint = json.dumps(call, sort_keys=True, ensure_ascii=False)
        except (TypeError, ValueError):
            return None
        if fingerprint in seen:
            return None
        seen.add(fingerprint)
    return calls

def app_valid(query, call):
    name, args = call["name"], call["arguments"]
    normalized = query.lower()
    if name == "set_light":
        brightness = args.get("brightness")
        return (
            set(args) == {"room", "brightness"}
            and args.get("room") in ROOM_WORDS
            and type(brightness) is int
            and 0 <= brightness <= 100
            and bool(re.search(rf"(?:^|\D){brightness}(?:\D|$)", normalized))
            and any(
                word in normalized
                for word in ROOM_WORDS[args["room"]]
            )
        )
    city = args.get("city")
    return (
        name == "get_weather"
        and set(args) == {"city"}
        and isinstance(city, str)
        and bool(city.strip())
        and city.lower() in normalized
    )

def cloud_tool_model(query, tools):
    raise NotImplementedError("接上你的雲端 tool-calling provider")

def route(query, agent, tools, request_id, allow_cloud=False):
    response = agent.complete(query)
    if not isinstance(response, dict):
        return {"route": "reject"}
    raw_calls = response.get("function_calls")
    if (
        response.get("success") is True
        and not response.get("error")
        and not response.get("error_code")
        and raw_calls == []
    ):
        return {"route": "refuse_or_clarify", "response": response}
    calls = parse_calls(response)
    if calls is None:
        return {"route": "reject", "response": response}
    if any(word in query.lower() for word in NEGATIONS):
        return {"route": "clarify", "response": response}
    if any(call["name"] in HIGH_RISK for call in calls):
        return {"route": "confirm_and_authorize", "calls": calls}
    if not all(app_valid(query, call) for call in calls):
        return {"route": "clarify", "response": response}

    required = max(THRESHOLD.get(call["name"], 1.0) for call in calls)
    confidence = response.get("confidence")
    if type(confidence) not in (int, float) or confidence < required:
        if not allow_cloud:
            return {"route": "offline_unavailable", "response": response}
        return {
            "route": "cloud_candidate_needs_same_gate",
            "response": cloud_tool_model(query, tools),
        }

    payload = json.dumps(calls, sort_keys=True, ensure_ascii=False)
    key = hashlib.sha256(f"{request_id}:{payload}".encode()).hexdigest()
    return {"route": "local", "calls": calls, "idempotency_key": key}

cloud_tool_model() 是本文自己加的 provider adapter,不是 Needle Python API;你要在這裡接上選用的雲端工具模型。預設 allow_cloud=False,代表不把輸入送出裝置;使用者同意交棒後,這裡回傳的仍只是 cloud_candidate_needs_same_gate。正式版要讓雲端輸出重新進入同一份 schema、call-count、duplicate、權限與 Validator,並讓執行工具消化 idempotency_key,不能因為交棒給大模型,就把安全 gate 拿掉。更完整的 timeout、重試與 stop condition 可參考 Agent Harness 心智模型

Step 6:怎麼搬到樹莓派與手機

64-bit 樹莓派:官方有 aarch64 路徑,先在實機驗證

2.0.1 的 Python fetcher會替 Linux aarch64 下載 manylinux wheel;因此這條路徑應使用 64-bit Raspberry Pi OS。先執行 uname -m,看到 aarch64 後,再測 Python 3.11 venv 安裝。官方 Hugging Face revision 也提供 linux-arm64 原生執行檔與 header。本文的執行與記憶體測試在 macOS ARM64 完成,沒有把這段命令冒充成 Pi 實機結果;若 Python training dependencies 在你的系統太重,優先評估原生 CLI/library。

uname -m
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install "cactus-needle==2.0.1"
python app.py                 # 第一次允許下載 engine
HF_HUB_OFFLINE=1 python app.py  # 確認後續本機路徑

我們沒有把 Cactus 公布的 Raspberry Pi 5 decode 數字當成本文實測;電源模式、散熱、OS、核心數與 prompt 都會改變結果。你應在目標 Pi 上記錄 cold start、p50/p95 latency、max RSS、正確率與 30 分鐘熱降頻,而不只截一個最高 tokens/s。

iOS/Android:不是在手機跑 pip,而是連結 native library

本文鎖定的官方 Needle 2 revision列出 ios-arm64android-arm64wasm 等 artifacts;其中 needle.h 暴露四個核心函式:needle_initneedle_completeneedle_resetneedle_load。手機 App 的工作,是把同一份 tools JSON 傳進 FFI,再把輸出送進前面的 Validator 與確認 UI。

下面是未檢查回傳碼的 FFI pseudocode,只用來辨認整合邊界,不可原樣上線。

char out[65536];
needle_init("", tools_json, NULL);
needle_complete(user_text, 256, out, sizeof(out));
// parse JSON -> validate -> confidence gate -> confirm or execute

這是進階部署地圖,不做 App 的讀者可先跳過。這段 C FFI 省略了 Swift/Kotlin bridge、threading、lifecycle、buffer error、簽章驗證與 App sandbox;它是部署邊界示意,不是完整手機專案。若你正在選 mobile runtime,可搭配 LLM inference engine 選擇指南理解 native、WASM 與 GPU/NPU 的差別。

Needle 2 教學決策圖:先 zero-shot eval,再依固定錯誤選 LoRA,依長上下文與複雜推理選 cloud fallback
選擇順序不是「模型越大越好」,而是先把最窄、最低風險的任務留在本機。

Zero-shot、LoRA fine-tune、cloud fallback 怎麼選

先選 zero-shot:工具少、描述清楚、錯誤成本低

先不要訓練。把工具名稱、docstring、argument description、enum、range 與 pattern 寫好,第一輪可先做 30~100 個貼近真實語言的 held-out cases。若 tool exact match、argument exact match、拒答與風險案例都達標,zero-shot 就夠了。發布者在 Reddit 的回覆也是這個順序:先試現成模型,不滿意再 fine-tune。

再選 LoRA:錯誤有固定模式,而且你有 hard negatives

LoRA 適合修正「同一批內部工具詞彙總是混淆」「某語言或領域說法反覆漏掉」「off-topic 與相似工具邊界不穩」;它不適合拿來補世界知識或長鏈推理。訓練資料要同時放正例、模糊例、否定句、未知工具與 answers: [] hard negatives,並保留完全沒參與訓練的測試集。

截至 2026 年 8 月 12 日,2.0.1 的預設 fine-tune 路徑會向 Hugging Face 要不存在的 checkpoints/needle2.pkl;本文鎖定的 HF revision 實際放在 weights/needle2.pkl。在這個路徑修正進入新版前,請明確下載 checkpoint 並傳入 --checkpoint,不要把無參數 quickstart 當成已驗證。

hf download Cactus-Compute/needle2 weights/needle2.pkl \
  --local-dir /absolute/path/needle2-model

needle finetune data.jsonl --epochs 3 \
  --checkpoint /absolute/path/needle2-model/weights/needle2.pkl

needle build /absolute/path/needle2-model/weights/needle2.pkl \
  --lora checkpoints/needle_lora.pkl \
  --out my_needle.cact

這條 workaround 已在本文環境完成一輪 toy LoRA、merge,並沿用原始 mixed Cactus Quants 設定匯出;輸出的 .cact 為 13,737,679 bytes。但只餵一筆 toy 資料反而讓結果變差:它證明流程能跑,不證明少量資料就能提升品質。LoRA target list沒有 confidence head,因此完成後要用同一份 held-out set 比較 base 與 tuned model,重新校準 threshold;否則你只知道 loss 下降,不知道產品錯誤是否下降。

直接選 cloud fallback:需要知識、長上下文或開放式規劃

若需求是開放式查資料、吸收長上下文、補外部知識或規劃尚未定義的步驟,就超過單純 mapping。依官方 multi-turn 說明,Needle 可以在後續 turn 使用先前工具結果;所以固定、可驗證的多步工具鏈仍可留在本機,不是看見 multi-step 就一定上雲。讓 Needle 先處理明確、低風險的本機命令;超出產品 eval 能力的部分才交給受工具限制的較大模型,再回到同一套 Validator。若你想看一個更完整的本機指揮官設計,可延伸到 Hermes Agent 的五個零件

6 個最容易踩的坑

① 把 run() 當安全捷徑

它會直接執行非空 calls。高風險工具一律改用 complete() 手動 gate。

② 把合法 JSON 當正確動作

enum 可以把未知值硬套成合法值。記錄 semantic exact match,不只記 JSON parse rate。

③ 猜一個全站共用 threshold

按工具風險與語言分桶,用 held-out set 找 false accept/fallback 的交換點。

④ 以為 14MB 就是總磁碟與 RAM

分開量 engine、Python dependencies、process RSS、App UI 與平台 bridge。

⑤ 只用英文 demo 推論中文也穩

我們的兩個中文正向案例一成一敗,成功案例 confidence 甚至只有 0.0077。你的使用者說什麼語言,就用什麼語言做 eval。

⑥ Fine-tune 後只看訓練 loss

真正指標是 unseen tools、hard negatives、argument exact match、false execute 與不同裝置上的 latency/RAM。

Needle 2 常見問題 FAQ

Needle 2 是 14MB 的聊天模型嗎?

不是。它主要把輸入映射成工具與結構化參數;自由聊天、百科知識與長篇推理不是這篇教學採用它的理由。

每套工具都要 fine-tune 嗎?

不一定。先把 schema 與描述寫清楚,用真實語句做 zero-shot eval;只有固定錯誤反覆出現,且你有標註資料時才值得 LoRA。

它真的能完全離線嗎?

推論可以在資產備妥後離線。Python wrapper 第一次要下載平台 engine;資料合成、下載 checkpoint 或你自己接的 cloud fallback 仍會使用網路。

官方約 28MB RAM 為什麼和 Python 不同?

量測邊界不同。native engine session 可以接近該級距;Python process 還有 wrapper、allocator 與執行歷史。請在實際裝置量 max RSS。

Confidence 0.9 就安全嗎?

不能這樣保證。它是分流訊號,不是正確率承諾;還要通過業務 Validator、權限與高風險確認。

Needle 2 能一次呼叫多個工具嗎?

可以產生多個 calls。我們的燈光+溫控案例得到兩個正確呼叫;執行順序、部分失敗、重試與 rollback 仍要由 Harness 管理。

手機可以直接跑這份 Python 程式嗎?

原生 App 通常不走 pip。iOS/Android 連結官方 static library 與 C header;Python quickstart 用來先驗證 schema 與 routing 邏輯。

適合拿來控制門鎖或付款嗎?

不適合讓模型直接決定並執行。若產品一定要支援,至少加入 deterministic policy、使用者確認、authentication、額度、idempotency 與 audit trail。

給新手的 5 個重點

  1. 先縮任務:讓 Needle 只做選工具+填參數,不要逼 14MB 模型當通用助理。
  2. 先用 complete()在執行前保留檢查與 fallback 的機會。
  3. Schema 管格式,Validator 管語意:合法 enum 仍可能是錯的 enum。
  4. Threshold 從自己的 eval 來:分工具、分語言、分風險,不抄一個神奇數字。
  5. Zero-shot 先行:只有固定錯誤值得 LoRA;外部知識、長上下文或超出 eval 的開放式規劃再 fallback。

接著閱讀

左右滑動查看更多推薦

結語:先讓一盞燈安全地亮起

Needle 2 最有價值的地方,不是證明「14MB 已經等於大型模型」,而是把 Agent 裡一個清楚、可量測的工作切出來:在裝置端把人話翻成候選工具呼叫。回到開頭的公式,真正能上線的離線 Tool Calling,還要加 Schema、Validator、Confidence Gate,以及明確的澄清/拒絕路徑;雲端 fallback 是可選的交棒,不是離線系統的必備條件。

你的下一步很簡單:先複製天氣範例,改成一個無害的 set_light,寫 20 句「正確、模糊、否定、越界、off-topic」輸入,再畫出 threshold sweep。當這個小 loop 能穩定拒絕錯誤,才把它搬到樹莓派或手機。想系統化把模型、工具與安全 Harness 組成產品,可從 AlphaLab 的 AI 實戰課程AI 主題專區接著學。

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週一封,第一時間收到新文章與投資觀察。

我們不會 spam,隨時可退訂。