Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Needle 2 Android Demo

将 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 组件输出:紫色标题条 → 工具名 + 参数键值对 → 绿色执行结果。

🚀 快速开始

第 1 步:下载原生文件

# 自动下载(需要 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/

第 2 步:编译运行

用 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
}

🔬 实战踩坑记录

在真机调试过程中发现的关键问题及解决方案:

1. C API 返回值不是 0/非0 约定

needle_load   返回 0     ← 不是"成功"
needle_init   返回 227   ← 不是"失败",是内部计数
needle_complete 返回 26  ← 不是"失败",是 token 计数

误区:最初用 rc == 0 判断成功,导致 init 被误判为失败。 解决:不检查返回值,靠输出 JSON 的 success / function_calls 字段判断。

2. needle_reset 清空模型权重

needle_reset 不仅清空对话上下文,还清空模型权重。只做 reset + init 不够——必须 reset + load + init。

3. locale: zh-CN 导致推理不稳定

system_prompt 中 locale: zh-CN 让引擎困惑(模型只支持英文但被告知 locale 是中文),confidence 下降,频繁返回 respond。

解决:改为 locale: en-US。修改后 5 次测试 100% 成功(之前约 80%)。

4. 非常规数字 confidence 暴跌

"dim to 30" → confidence 0.5127 ✓
"dim to 31" → confidence 0.0004 ✗(训练数据偏差)

解决:规则回退从输入中提取数字构造 function call。

5. Android optString 对 JSON null 返回字符串 "null"

引擎返回 "reasoning": null(JSON null),Android 的 JSONObject.optString() 返回字符串 "null"(而非 fallback "")。

解决:.let { if (it == "null") "" else it }。

6. 多场景 tools schema 切换

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

🔑 关键代码导读

引擎初始化(NeedleEngine.kt)

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
    }
}

推理 + 重试 + 回退(NeedleAgent.kt)

class NeedleAgent(
    handlers: Map<String, (Map<String, Any?>) -> String>,
    fallback: ((String) -> NeedleResponse?)? = null,  // 规则回退
) {
    fun run(input, maxRounds = 1): Result {           // 单轮模式
        val resp = completeWithRetry(input)
        // ... 执行 handler 或终止
    }
}

动态 NDK 启用(build.gradle.kts)

val nativeEnabled = file(".../libneedle.a").exists()
if (nativeEnabled) {
    externalNativeBuild { cmake { ... } }
}

📊 应用场景分析

结合 Cactus Compute 官网 和实测数据:

最适合的场景

  1. 可穿戴设备离线语音指令(如 Pebble Index 01 手表)

    • 工具集小而固定(运动控制、音乐、消息模板)
    • 必须离线(蓝牙断连时仍可用)
    • 英文指令解析
  2. 边缘-云协作路由

    • Needle 评估 confidence,高置信本地执行,低置信升级到云端
    • 空调用(function_calls:[])= "超出能力,请交给云端"
  3. 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 需规则兜底

⚠️ 注意事项

  1. 必须用 arm64 真机(libneedle.a 只编译了 arm64-v8a)
  2. 仅支持英文输入(vocab_size=8192,无中文能力)
  3. needle_complete 是阻塞调用——必须在 Dispatchers.Default 执行
  4. 每次推理前需要 fullReinit——needle_reset 会清空权重
  5. 这不是聊天模型——问"你好"会返回空调用,专长是 NL → JSON
  6. License:Needle 模型 Apache-2.0,可商用

📚 进一步阅读

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages