将 Cactus Compute Needle 2 —— 一个 45M 参数、14MB、专为边缘设备设计的工具调用模型 —— 完整集成到 Android 应用中的实战示例。
Needle 2 是什么:一个超小型 transformer,把自然语言转成结构化 JSON(function calling / 信息抽取)。手机上 150–500 tok/s,session 内存 ~300MB。仅支持英文。不是通用聊天模型。
| Tab | 输入示例 | 展示能力 |
|---|---|---|
| 🏠 智能家居 | dim the living room to 60 |
function calling → set_lights / set_thermostat |
| 📋 信息抽取 | GreenMart receipt: oat milk 3.50, total 7.75 |
自由文本 → 结构化 JSON 字段 |
| 💳 约束动作 | send 50 dollars to @alice for dinner |
JSON Schema 约束(pattern/range)+ 多轮 loop |
每个场景的推理结果通过统一的 ToolCallCard 组件输出:紫色标题条 → 工具名 + 参数键值对 → 绿色执行结果。
# 自动下载(需要 curl)
./download_native.sh或手动从 HuggingFace 下载这 3 个文件:
| 文件 | 大小 | 来源 → 目标位置 |
|---|---|---|
libneedle.a |
~20MB | android-arm64/libneedle.a → app/src/main/jniLibsStatic/arm64-v8a/ |
needle2.cact |
~14MB | needle2.cact → app/src/main/assets/ |
needle.h |
~2KB | android-arm64/needle.h → app/src/main/cpp/include/ |
用 Android Studio 打开项目 → Sync Gradle → Run 'app' 到 arm64 真机。
状态栏显示绿色「XXX tok/s」即表示引擎就绪。点击示例 chip 自动运行推理。
Needle 2 是 45M 参数小模型,存在固有的不稳定性(随机失败、非常规输入 confidence 暴跌)。本 Demo 实现了三层防御机制确保可靠性:
用户输入
↓
第 1 层:引擎推理(Needle native engine)
├── 返回 function_call → 执行 handler → 输出结果
└── 返回 respond(空调用)→ 进入重试
↓
第 2 层:fullReinit + 重试(最多 4 次)
├── 每次重试:reset + reload 权重 + reinit + complete
└── 仍然失败 → 进入回退
↓
第 3 层:规则回退(fallback)
└── 从输入中正则提取 room + brightness → 构造 function call
NeedleEngine.fullReinit(toolsJson) — 每次推理前完全重置引擎:
fun fullReinit(toolsJson: String? = null) {
nativeReset() // 清空状态
nativeLoad(cactBytes!!) // 重新加载权重 ← 关键!
nativeInit(lastSystemPrompt, toolsJson, ...) // 恢复 tools schema
}
⚠️ 实战发现:needle_reset不仅清空上下文,还清空模型权重。只做 reset + init(不重新 load)会导致引擎在无权重状态下运行,complete 总是返回空 function_calls。必须 reset + load + init。
NeedleAgent.completeWithRetry(input) — 重试 + 回退:
private fun completeWithRetry(input: String, maxRetries: Int = 4): NeedleResponse {
var resp = parse(NeedleEngine.complete(input))
if (resp.isCall) return resp
for (retry in 1..maxRetries) {
NeedleEngine.fullReinit() // 重新加载权重
resp = parse(NeedleEngine.complete(input))
if (resp.isCall) return resp
}
fallback?.invoke(input)?.let { return it } // 规则回退兜底
return resp
}在真机调试过程中发现的关键问题及解决方案:
needle_load 返回 0 ← 不是"成功"
needle_init 返回 227 ← 不是"失败",是内部计数
needle_complete 返回 26 ← 不是"失败",是 token 计数
误区:最初用 rc == 0 判断成功,导致 init 被误判为失败。
解决:不检查返回值,靠输出 JSON 的 success / function_calls 字段判断。
needle_reset 不仅清空对话上下文,还清空模型权重。只做 reset + init 不够——必须 reset + load + init。
system_prompt 中 locale: zh-CN 让引擎困惑(模型只支持英文但被告知 locale 是中文),confidence 下降,频繁返回 respond。
解决:改为 locale: en-US。修改后 5 次测试 100% 成功(之前约 80%)。
"dim to 30" → confidence 0.5127 ✓
"dim to 31" → confidence 0.0004 ✗(训练数据偏差)
解决:规则回退从输入中提取数字构造 function call。
引擎返回 "reasoning": null(JSON null),Android 的 JSONObject.optString() 返回字符串 "null"(而非 fallback "")。
解决:.let { if (it == "null") "" else it }。
bootstrap 时只用第一个场景的 tools.json。切换 tab 时需要用当前场景的 tools 重新 init。
解决:fullReinit(toolsJson) 接受当前场景的 tools 参数。
needle-android-demo/
├── app/src/main/
│ ├── assets/
│ │ ├── tools/{smart_home,receipt,payment}.json ← 三个场景的工具 schema
│ │ ├── system_prompt.txt
│ │ └── needle2.cact ← 模型权重(用户下载)
│ ├── cpp/
│ │ ├── CMakeLists.txt ← 链接 libneedle.a 为 .so
│ │ ├── needle_jni.cpp ← JNI 桥接(4 个函数)
│ │ └── include/needle.h ← C API 头文件
│ ├── jniLibsStatic/arm64-v8a/
│ │ └── libneedle.a ← 静态库(用户下载)
│ └── java/com/example/needledemo/
│ ├── NeedleEngine.kt ← 引擎封装:bootstrap / fullReinit / complete
│ ├── NeedleAgent.kt ← Agent loop:重试 + fallback
│ ├── NeedleViewModel.kt ← ViewModel:状态管理 + 规则回退
│ ├── model/
│ │ ├── NeedleResponse.kt ← JSON 解析(含 null 处理)
│ │ └── Scenario.kt ← 场景定义 + 示例输入
│ ├── ui/
│ │ ├── components/
│ │ │ ├── ToolCallCard.kt ← 统一工具调用输出
│ │ │ ├── ScenarioScaffold.kt ← 共享脚手架 + 自动滚动
│ │ │ ├── StatusBadge.kt ← 引擎状态徽标
│ │ │ ├── MetricRow.kt ← 性能指标
│ │ │ └── JsonCard.kt ← Section 容器
│ │ ├── screens/
│ │ │ ├── SmartHomeScreen.kt
│ │ │ ├── ExtractionScreen.kt
│ │ │ └── PaymentScreen.kt
│ │ └── theme/ ← Material3 配色
│ └── MainActivity.kt ← Scaffold + 底部导航
├── download_native.sh ← 一键下载原生文件
└── README.md
object NeedleEngine {
init { System.loadLibrary("needle_jni") }
fun bootstrap(context, toolsJson, systemPrompt): Boolean {
cactBytes = context.assets.open("needle2.cact").use { it.readBytes() }
nativeLoad(cactBytes) // 加载权重
nativeInit(systemPrompt, toolsJson, toolIndexPath) // 初始化会话
nativeInitialized = true
}
fun fullReinit(toolsJson: String? = null) {
nativeReset() // 清空(含权重!)
nativeLoad(cactBytes) // 必须重新加载
nativeInit(...) // 恢复 tools schema
}
}class NeedleAgent(
handlers: Map<String, (Map<String, Any?>) -> String>,
fallback: ((String) -> NeedleResponse?)? = null, // 规则回退
) {
fun run(input, maxRounds = 1): Result { // 单轮模式
val resp = completeWithRetry(input)
// ... 执行 handler 或终止
}
}val nativeEnabled = file(".../libneedle.a").exists()
if (nativeEnabled) {
externalNativeBuild { cmake { ... } }
}结合 Cactus Compute 官网 和实测数据:
-
可穿戴设备离线语音指令(如 Pebble Index 01 手表)
- 工具集小而固定(运动控制、音乐、消息模板)
- 必须离线(蓝牙断连时仍可用)
- 英文指令解析
-
边缘-云协作路由
- Needle 评估 confidence,高置信本地执行,低置信升级到云端
- 空调用(
function_calls:[])= "超出能力,请交给云端"
-
MCU 上的自然语言配置(ESP32-S3 / STM32H7)
- 物联网设备的语音/文本配置
- 替代复杂表单 UI
| 指标 | 实测值 |
|---|---|
| 解码速度 | 150–500 tok/s(取决于输入复杂度) |
| Session 内存 | ~300MB(含 Android runtime) |
| 首次加载 | ~1s(读 14MB .cact + load + init) |
| 单次推理 | ~500ms(fullReinit + complete) |
| 推理成功率 | >99%(含重试 + fallback) |
| 泛化类型 | 能力 | 示例 |
|---|---|---|
| 表述泛化 | ✅ 强 | make it brighter → set_lights(brightness=100) |
| 实体泛化 | ✅ 中 | bedroom/kitchen/bathroom 正确提取 |
| 数值泛化 | 30/60 能理解,31/34/35 需规则兜底 |
- 必须用 arm64 真机(
libneedle.a只编译了 arm64-v8a) - 仅支持英文输入(vocab_size=8192,无中文能力)
needle_complete是阻塞调用——必须在Dispatchers.Default执行- 每次推理前需要
fullReinit——needle_reset会清空权重 - 这不是聊天模型——问"你好"会返回空调用,专长是 NL → JSON
- License:Needle 模型 Apache-2.0,可商用
- 模型主页:huggingface.co/Cactus-Compute/needle2
- 官网:cactuscompute.com/needle
- GitHub:github.com/cactus-compute/needle
- Python 库:
pip install cactus-needle