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 專案。
先說結論:14MB 模型只是翻譯員,不是安全開關
🌵 記憶把手:離線 Tool Calling=Needle 2(翻譯)+Schema(護欄)+Validator(驗票)+Confidence Gate(分流)+失敗路徑(澄清/拒絕/可選雲端)。
少掉前四層,都可能得到「格式完全合法、動作完全錯誤」的工具呼叫;不接雲端時,也可以安全地拒絕或請使用者重說。
Needle 2 的優勢是任務被刻意縮窄:選哪個工具、參數填什麼、輸出結構化 JSON。它不以百科知識、長文聊天或自由推理為主要工作。若你想先理解完整 Agent loop,可搭配 AI Agent Harness 是什麼;Needle 2 比較像 Harness 裡的一顆超小「工具路由器」。

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_room、bedroom 或 kitchen,再輸入「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 metadata寫 Python >=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 必須同時檢查 success、error、非空 calls、允許的工具名與業務語意。
下圖左側的 93.4% BFCL well-formed 與 42.6% BFCL overall 都來自 Cactus 官方產品頁,屬於供應商公布結果;well-formed 是該頁使用的特定 metric 名稱,不應改寫成一般 JSON parse rate 或完整 schema 正確率。右側才是本文不同工具集、不同樣本量的小型實測,兩邊不能直接排名。

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-arm64、android-arm64、wasm 等 artifacts;其中 needle.h 暴露四個核心函式:needle_init、needle_complete、needle_reset 與 needle_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 的差別。

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 個重點
- 先縮任務:讓 Needle 只做選工具+填參數,不要逼 14MB 模型當通用助理。
- 先用
complete():在執行前保留檢查與 fallback 的機會。 - Schema 管格式,Validator 管語意:合法 enum 仍可能是錯的 enum。
- Threshold 從自己的 eval 來:分工具、分語言、分風險,不抄一個神奇數字。
- 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 主題專區接著學。





