人说过,电脑是人脑的延伸;如今,大模型则是人脑的近似。它能像人一样思考任务:先分析子目标,再进行规划,最后按部就班地执行。这正是我想实现的——基于大模型和 Skill 的智能业务流生成。

本文旨在验证上述想法,核心目标是将用户的自然语言指令转换为可执行的硬件操作序列。把业务逻辑交给大模型,让它自行编排工作流,而我不再编写繁琐的业务代码,仅需定义好板子的技能和执行引擎,便可做到“换板子即换技能”。

准备工作
硬件环境
本次实验使用的是英飞凌 OK1126BJ-S 开发板,其核心板为 FET1126BJ-S。

SoC:基于 Rockchip RV1126BJ 处理器

CPU:集成 4 个 ARM Cortex-A53 核心

NPU:内置独立 NPU,具备 3 TOPS @ INT8​ 算力

软件环境
使用瑞芯微官方提供的 rknn-llm 开发套件。

仓库地址:GitHub - airockchip/rknn-llm

模型获取
可直接下载预转换的 RKLLM 模型文件:

下载地址:rkllm_model_zoo
提取码:rkllm
image

兼容性说明
支持平台:

RK3588 Series

RK3576 Series

RK3562 Series

RV1126B Series

支持模型:

✅ LLAMA / TinyLLAMA

✅ Qwen2 / Qwen2.5 / Qwen3 / Qwen3.5

✅ Phi2 / Phi3

✅ ChatGLM3-6B

✅ Gemma 系列

✅ InternLM2

✅ MiniCPM3 / MiniCPM4

✅ TeleChat2

✅ Qwen2-VL / Qwen3-VL

✅ MiniCPM-V-2_6

✅ DeepSeek-R1-Distill

✅ Janus-Pro-1B

✅ InternVL 系列

✅ SmolVLM / SmolLM3

✅ RWKV7

✅ DeepSeekOCR

部署大模型 (LLM)

  1. 确认板端基础信息
    在部署前,需确认板子的架构、内存及 NPU 驱动版本,以便选择合适的模型。

查看架构,应为 aarch64

uname -m

查看内存容量

cat /proc/meminfo | head -2

检查 NPU 驱动版本 (RV1126B 需 v0.9.8,配合 toolkit2 v2.3.2)

dmesg | grep -i npu
cat /sys/kernel/debug/rknpu/version

查看 RKNN Runtime 版本

strings /usr/lib/librknnrt.so | grep “librknnrt version”
经验之谈:硬件资源越丰富,可承载的模型体量就越大。鉴于 RV1126B 的资源限制,我选择了 Qwen3.5-0.8B 量化版本。

对应的模型文件是Qwen3.5-0.8B_w4a16_g128_rv1126b.rkllm,当然如果需要使用其他的模型,也可以做转换部署

  1. PC 端环境搭建 (Ubuntu 22.04)
    为了方便管理依赖,我选择在 Docker 容器中构建环境。

克隆仓库

git clone https://github.com/airockchip/rknn-llm.git
cd rknn-llm/rkllm-toolkit/packages

安装 Python 依赖

pip install -r requirements.txt

安装 Toolkit (选择与 Python 版本匹配的 wheel 文件)

pip install rkllm_toolkit-*.whl
3. 编译 Demo
修改官方例程 rkllm_api_demo 中的 llm_demo.cpp,优化解码参数:

/* embed_flash: 0 表示将权重加载至 DRAM,资源充足时更稳定 */
param.extend_param.embed_flash = 0;

param.top_k = 10; // 贪心解码,过小会导致输出呆板
param.top_p = 0.9;
param.temperature = 0.8;
修改交叉编译脚本 build-linux.sh,指定编译器路径:

C_COMPILER=/usr/bin/aarch64-linux-gnu-gcc
CXX_COMPILER=/usr/bin/aarch64-linux-gnu-g++
STRIP_COMPILER=/usr/bin/aarch64-linux-gnu-strip
执行编译:

./build-linux.sh
4. 板端部署与测试
将编译产物拷贝至开发板:

llm_demo (可执行文件)

Qwen3.5-0.8B_w4a16_g128_rv1126b.rkllm (模型文件)

librkllmrt.so (Runtime 库,注意:Qwen3.5 需要 RKLLM v1.3.0 支持)

调整系统参数并运行:

增大文件句柄上限,防止 NPU 内存管理报错

ulimit -HSn 102400

启动 Demo (max_new_tokens + max_context)

./llm_demo Qwen3.5-0.8B_w4a16_g128_rv1126b.rkllm 1024 1024
实测日志与分析
rkllm init start
I rkllm: rkllm-runtime version: 1.3.0, rknpu driver version: 0.9.8
I rkllm: loading model… max_context_limit: 4096, npu_core_num: 1
rkllm init success

user: 你是一个严谨的数学助手… 鸡兔同笼问题…
robot: ### 1. 设定变量
设鸡的数量为 x x x,兔子的数量为 y y y。
…(省略推导过程)…
答:鸡有 9 只,兔子有 5 只。
踩坑记录:

截断问题:初始参数设为 256+512 时,解题过程常被截断。需调整为 1024+1024(需满足 32 对齐)。

逻辑错误:小模型容易啰嗦且中途改答案。通过优化 Prompt(如要求“精简逻辑”)和增加输出长度解决。

端侧 LLM + Function Calling(Tool Use)
设计理念:Skill 调度与 LLM Agent
本项目的核心是将大模型作为“大脑”,Skill 作为“手脚”。通过组合多个 Skill 实现复杂的业务流。这在嵌入式领域常被称为 “Skill 调度”​或 “Edge Agent”。

由于 RV1126B 只能运行 0.8B 级别的小模型,其逻辑推理能力有限。为了保证系统的稳定性,我采用了“分阶段架构”,将复杂的自然语言理解任务拆解,结合代码的确定性逻辑,确保最终执行结果的可靠性。

之前尝试依赖大模型实现大部分的功能,包括意图识别,技能映射以及任务编排。只有执行引擎是纯代码实现。可是效果很不理想,做了很多优化,调整prompt,堆示例,分解模型调用层级,减小每次模型任务难度等,但最终仍不达标

为了增加模型输出稳定性,同时保证通用性,经过调整,意图识别这一块一定是LLM占主导,所以怎么调用LLM也是一个可设计的点,同时在其他地方增加约束,这样才能达到一个良好的效果。

技术栈概览
层次 技术 版本 说明
语言​ C/C++ C++11 主程序 C++,核心模块 C
JSON库​ cJSON - 轻量级解析
LLM框架​ rkllm v1.3.0 瑞芯微推理框架
构建工具​ CMake 3.28+ 跨平台构建
编译目标​ Linux aarch64 - RV1126B 平台
核心设计原则
数据驱动:Skill 定义、作用域规则全部通过 skills.json 配置,代码仅负责解析和执行。

输入制式:以逗号分割输入,每个切片对应一个动作序列,降低模型分割难度。

LLM 做选择题:阶段一让 LLM 从给定索引列表中选择,而非自由生成文本,大幅降低输出难度,这些索引列表从skills.json中动态生成。

确定性操作:阶段二、三完全由代码实现,杜绝 LLM 幻觉。

栈式作用域:通过栈算法处理控制流,支持嵌套和切换。

整体架构:三阶段流水线
为了克服小模型的局限性,系统被拆分为三个严格的阶段:

用户输入 → 阶段一(意图识别) → 阶段二(技能映射) → 阶段三(结构解析) → 执行引擎
整体架构框图如下,展示各层次的组件和数据流:

image

阶段一:意图识别(LLM 主导)
目标:将用户的自然语言切片转换为 索引|文本切片,并提取参数。

实现逻辑:

预处理:代码层按逗号(支持中英文)切分用户输入。

索引识别:针对每个切片,调用 LLM 识别对应的 Skill 索引(数字)。

参数提取:针对识别出的索引,调用 LLM 提取参数,采用“填空题”格式(key=value)。

关键代码逻辑:

🔎索引识别流程:

// 1. 预处理:清理不可见字符
char *input_copy = strdup(user_input);
// … 字符过滤 …

// 2. 按逗号切片
while (remaining && *remaining) {
char slice_buf[256];
// … 提取切片 …

// 3. 构造 Prompt 并调用 LLM
std::string prompt = "<|im_start|>system\n" + system_prompt + ...;
llm_run_with_prompt(handle, prompt.c_str(), params);

// 4. 解析索引
skill_index = atoi(strip_markdown(g_collected_text));

}
设计要点:

根据skills.json动态生成prompt

不让模型直接输出技能名称,而是输出数字索引,极大降低了拼写错误的概率。

输入切片,不仅保留了自然语言的输入,同时减少了大模型任务难度

🔎参数提取实现

const char *skill_registry_build_param_extract_prompt(int skill_index, const char *text_slice) {
// 动态生成参数提取Prompt
// 格式:填空题形式,如 “device=[ ]”

skill = get_skill_by_index(skill_index);
append "从输入切片中提取技能 " + skill.skill_id + " 的参数值。\n";

if (skill.pattern) {
    append "该技能的模式是:" + skill.pattern + "\n";
}

// 列出参数选项
for each param in skill.input_schema:
    if param.type is "enum":
        append "- " + param.name + ": [" + join(param.values, ", ") + "]\n";
    else:
        append "- " + param.name + ": 数字\n";

// 生成填空题
append "输入切片:" + text_slice + "\n";
for each param in skill.input_schema:
    append "- " + param.name + "=[ ]\n";

}
设计要点:

针对单一技能的参数提取,复用性高,通用性强

以技能的input_schema字段作为提取准则,字段包含了参数类型,可以时字符串,数字以及枚举

阶段二:技能映射(代码确定性执行)
目标:将阶段一得到的索引,100% 准确地映射为具体的技能名称和数据结构。

核心数据结构:

typedef struct {
char skill_id[64]; // 技能名
int index; // 索引号
char category[32]; // action / structural
// … 作用域管理字段 (scope_action, scope_type等)
} skill_info_t;
映射逻辑:

const char *skill_registry_find_by_index(int index) {
for (int i = 0; i < g_skill_count; i++) {
if (g_skill_table[i].index == index) {
return g_skill_table[i].skill_id; // 精确查找
}
}
return NULL;
}
设计要点:

解耦:技能定义(JSON)与技能实现(Handler 函数)分离。

注册机制:支持动态注册新的技能 Handler,无需修改核心逻辑。

阶段三:结构解析(数据驱动的树构建)
目标:将扁平的技能列表转换为树状的 AST(抽象语法树),以支持 Loop 和 If-Else 等控制流。

核心算法:栈式作用域管理

初始化:栈顶为 ROOT
遍历技能列表:
├─ action技能 → 压入当前栈顶的任务数组
└─ structural技能:
├─ open (如 loop, if) → 创建父节点,Push 新作用域
├─ switch (如 else) → 切换当前作用域 (IF_THEN -> IF_ELSE)
└─ close (如 end) → Pop 作用域
结束:自动 Pop 所有未关闭的作用域
IF-ELSE 切换逻辑:

// 采用 Pop-then-Push 策略
parser->stack_top–; // 退出 IF_THEN
// 创建 ELSE 分支数组
parser->stack_top++; // 进入 IF_ELSE
else分支的作用域切换采用"pop-then-push"策略,先退出IF_THEN作用域,再进入IF_ELSE作用域,共享同一个parent_node(condition对象)。

变量自动绑定:

当 Action 技能(如 read)执行完毕后,系统会根据 output_schema 自动将其结果存入变量池。

例如:read 读取电压后,自动生成变量 $read_电压,供后续的 if 条件判断使用。

前三阶段流程示例
以输入"先关灯,每隔3秒,读取电压,若大于1V,再开灯,否则,关灯,若小于0.5V,退出任务"为例:

技能列表(阶段二输出):

  1. close (action) → 添加到ROOT作用域
  2. loop (structural, open) → 创建loop节点,push LOOP_BODY作用域
  3. read (action) → 添加到LOOP_BODY作用域,自动绑定 read_电压
  4. if (structural, open) → 创建condition节点,push IF_THEN作用域
  5. open (action) → 添加到IF_THEN作用域
  6. else (structural, switch) → pop IF_THEN,push IF_ELSE作用域
  7. close (action) → 添加到IF_ELSE作用域
  8. if (structural, open) → 创建condition节点,push IF_THEN作用域
  9. exit_task (action) → 添加到IF_THEN作用域

遍历结束,自动关闭所有作用域:

  • pop IF_THEN (深度3 → 2)
  • pop IF_ELSE (深度2 → 1)
  • pop LOOP_BODY (深度1 → 0)

最终任务结构:
{
“tasks”: [
{“type”:“action”,“skill_name”:“close”,…},
{“type”:“loop”,“interval”:3,“tasks”:[
{“type”:“action”,“skill_name”:“read”,“store”:“read_电压”,…},
{“type”:“condition”,“condition”:{“a”:“KaTeX parse error: Expected 'EOF', got '}' at position 25: …"op":">=","b":1}̲, "then…read_电压”,“op”:“<”,“b”:0.5},
“then”:[{“type”:“action”,“skill_name”:“exit_task”,…}]}
]
}
]}
]
}
执行引擎(递归下降执行)
目标:遍历 AST,执行具体的硬件操作。

执行上下文:

typedef struct {
cJSON *vars; // 变量池 (键值对)
int loop_depth; // 循环深度
int should_exit; // 退出标志
} exec_context_t;
任务执行主流程

static int execute_task_internal(cJSON *tasks_array, exec_context_t *ctx, cJSON **results_array) {
cJSON *task_item;
cJSON_ArrayForEach(task_item, tasks_array) {
cJSON *type_item = cJSON_GetObjectItemCaseSensitive(task_item, “type”);
const char *type = type_item ? type_item->valuestring : “”;

    if (strcmp(type, "action") == 0) {
        execute_action(task_item, ctx, results_array);
    } else if (strcmp(type, "loop") == 0) {
        execute_loop(task_item, ctx, results_array);
    } else if (strcmp(type, "condition") == 0) {
        execute_condition(task_item, ctx, results_array);
    }
    
    // 检测退出标志
    if (ctx->should_exit) {
        break;
    }
}
return 0;

}
三大执行器:

Action Executor:调用具体的技能 Handler(如点亮 LED)。

Loop Executor:循环执行子任务,支持 sleep 和 exit_task 中断。

Condition Executor:解析 $variable,比较数值,决定执行 Then 还是 Else 分支。

工作流程
主要流程伪代码如下:

// 阶段一:索引识别
prompt = skill_registry_build_phase_one_prompt();
rkllm_run(llmHandle, prompt, callback);
// 解析输出:索引|文本切片

// 阶段一:参数提取
for each intent:
prompt = skill_registry_build_param_extract_prompt(index, text_slice);
rkllm_run(llmHandle, prompt, callback);
// 解析参数

// 阶段二:技能映射
for each intent:
skill_name = skill_registry_find_by_index(index);
category = skill_registry_get_category(skill_name);
// 构建技能列表

// 阶段三:结构解析
struct_parser_execute(skills, &tasks);

// 执行引擎
execute_tasks(tasks);
下面是一张完整时序图:

image

这张时序图,描述从用户输入到硬件操作的完整执行流程

核心模块详解
文件 职责 语言
llm_demo.cpp 主流程控制,串联三阶段 C++
skill_registry.c/h 技能加载、Prompt 生成、索引映射 C
skill_struct_parser.c/h 栈式解析,AST 构建 C
skill_task.cpp/h 任务执行引擎 C++
skill_*.cpp 具体技能实现 (LED, ADC等) C++
skills.json 技能定义与配置 JSON
接下来挑几个重点内容介绍一下。

技能定义 (skills.json)
为了让 0.8B 模型更好地理解,Skill 定义需遵循“对 LLM 友好”的原则:动词化命名、枚举化参数、口语化描述。

{
“skill_id”: “read”,
“index”: 3,
“category”: “action”,
“trigger_keywords”: [“读取”, “获取”],
“input_schema”: {
“target”: { “type”: “enum”, “values”: [“电压”, “电流”] }
},
“output_schema”: { “value”: “float” }
}
技能分类

action技能(实际执行操作):

open/close:打开/关闭
delay:延时等待
read:读取
write:参数设置
compare:数值比较
exit_task:退出任务
structural技能(控制流结构,由结构解析器处理):

loop:循环控制
if:条件判断
else:条件分支
end:作用域结束
技能注册中心(skill_registry.c)
职责:加载技能定义、动态生成Prompt、索引到技能映射

核心函数:

函数 说明
skill_registry_init() 加载skills.json,初始化技能表
skill_registry_build_phase_one_prompt() 动态生成阶段一索引识别Prompt
skill_registry_build_param_extract_prompt() 动态生成参数提取Prompt
skill_registry_find_by_index() 通过索引查找技能名称
skill_registry_dispatch() 分发技能执行,设置exit_task标志
设计要点:

Prompt完全动态生成,基于skills.json配置
支持category和scope规则字段的加载
skill_registry_dispatch新增exec_ctx参数,用于设置退出标志
下面给出skill_registry_dispatch函数的代码:

/**

  • @brief 技能分发器:根据技能名称查找并执行对应的注册函数

  • @param skill_name 技能名称(可能是别名,需经 map 转换)

  • @param params_json 技能参数字符串(JSON格式)

  • @param output_result 输出参数,用于接收技能执行结果(JSON对象)

  • @param exec_ctx 执行上下文,包含变量池、循环深度、退出标志等

  • @return 0表示成功,非0表示失败
    */
    int skill_registry_dispatch(const char *skill_name, const char *params_json,
    cJSON **output_result, exec_context_t *exec_ctx)
    {
    // …

    // 技能名称映射:支持使用别名调用,获取真实的技能ID
    // 例如:将 “led_on” 映射为标准的 “open”
    const char *mapped_name = skill_registry_map_id(skill_name);
    // 使用映射后的标准名称进行后续查找
    skill_name = mapped_name;

    // 4. 遍历全局技能表,查找匹配的技能
    for (int i = 0; i < g_skill_count; i++) {
    // 比较技能表中的 ID 与目标技能名
    if (strcmp(g_skill_table[i].skill_id, skill_name) == 0) {
    // 4.1 检查该技能是否已注册具体的处理函数(handler)
    if (g_skill_table[i].handler) {
    // 打印执行日志,便于调试
    printf(“[执行技能: %s]\n”, skill_name);
    printf(“参数: %s\n”, params_json);

             // 4.2 获取该技能的私有上下文(用于存储状态或硬件句柄)
             skill_context_t *ctx = skill_registry_get_context(skill_name);
    
             // 4.3 特殊处理:如果是 exit_task 技能,设置上下文的退出标志
             //     这是一个信号,通知外层循环(如 execute_loop)终止执行
             if (strcmp(skill_name, "exit_task") == 0 && exec_ctx) {
                 exec_ctx->should_exit = 1;
             }
    
             // 4.4 调用注册的处理函数,执行具体硬件操作
             return g_skill_table[i].handler(params_json, output_result, ctx);
         } else {
             // 技能存在但未绑定实现函数
             fprintf(stderr, "技能 [%s] 未注册处理器\n", skill_name);
             return -1;
         }
     }
    

    }

    // 5. 遍历完整个表仍未找到,返回错误
    fprintf(stderr, “未找到技能: %s\n”, skill_name);
    return -1;
    }
    下面是阶段一意图识别需要用到的prompt片段:

“你是一个指令解析器,从输入切片中识别技能索引。\n\n”

“输出格式:只输出数字索引,不要输出其他任何内容\n”

“示例:\n”

“输入:开灯\n输出:0\n\n”

“输入:等待3秒\n输出:2\n\n”

“输入:若电压大于12V\n输出:7\n\n”

“输入:否则\n输出:8\n\n”

“输入:结束\n输出:9\n\n”

“规则:\n”

“1. 索引必须从下方技能索引列表中选择\n”

“2. 如果找不到匹配的触发词,输出-1\n”

“3. 只输出一个数字,不要输出其他文字\n\n”);
结构解析器(skill_struct_parser.c)
作用域规则(在skills.json中配置):

技能 scope_action scope_type child_scope_type 说明
loop open LOOP LOOP_BODY 打开循环作用域
if open IF IF_THEN 打开条件作用域(then分支)
else switch IF - 切换到else分支
end close - - 关闭作用域
作用域栈数据结构

typedef struct {
cJSON *tasks_array; // 当前作用域的任务数组(action技能添加到这里)
char scope_type[32]; // 作用域类型:ROOT, LOOP_BODY, IF_THEN, IF_ELSE
cJSON *parent_node; // 父节点(loop/condition对象)
} scope_stack_item_t;

typedef struct {
scope_stack_item_t stack[MAX_SCOPE_DEPTH]; // 作用域栈(最大深度8)
int stack_top; // 栈顶指针
cJSON *root_tasks; // 根任务数组
} struct_parser_t;
举例:IF作用域打开实现代码:

static void handle_scope_open(struct_parser_t *parser, const char *scope_type,
const char *child_scope_type, const char *skill_name, cJSON *params) {
cJSON *parent_node = cJSON_CreateObject();

if (strcmp(scope_type, "IF") == 0) {
    // 创建condition任务节点
    cJSON_AddStringToObject(parent_node, "type", "condition");
    cJSON_AddItemToObject(parent_node, "condition", build_condition_json(params));
    
    // 创建then分支任务数组
    cJSON *then_tasks = cJSON_CreateArray();
    cJSON_AddItemToObject(parent_node, "then", then_tasks);
    
    // 将父节点添加到当前作用域
    cJSON_AddItemToArray(parser->stack[parser->stack_top].tasks_array, parent_node);
    
    // push新作用域(IF_THEN)
    parser->stack_top++;
    parser->stack[parser->stack_top].tasks_array = then_tasks;
    strncpy(parser->stack[parser->stack_top].scope_type, "IF_THEN", ...);
    parser->stack[parser->stack_top].parent_node = parent_node;
}
// ... LOOP类型类似

}
变量自动绑定:

static const char *auto_bind_variable(const char *skill_name, cJSON *params) {
// 获取技能的output_schema
cJSON *output_schema = skill_registry_get_output_schema(skill_name);
if (!output_schema) return NULL;

// 检查是否包含value字段(表示有数值输出)
cJSON *value_field = cJSON_GetObjectItemCaseSensitive(output_schema, "value");
if (!value_field) return NULL;

// 生成变量名:skill_name_target
cJSON *target_item = cJSON_GetObjectItemCaseSensitive(params, "target");
const char *target = target_item ? target_item->valuestring : "";

static char var_name[64];
snprintf(var_name, sizeof(var_name), "%s_%s", skill_name, target);
return var_name;  // 如:read_电压

}
当 Action 技能(如 read)执行完毕后,系统会根据 output_schema 自动将其结果存入变量池。

例如:read 读取电压后,自动生成变量 $read_电压,供后续的 if 条件判断使用。

任务执行引擎(skill_task.cpp)
职责:执行任务流,管理执行上下文

执行器类型:

执行器 处理任务类型 说明
execute_action() action 调用技能处理器,存储结果
execute_loop() loop 循环执行子任务,检测退出标志
execute_condition() condition 条件判断,执行then/else分支
举例:循环执行实现

static int execute_loop(cJSON *loop_node, exec_context_t *ctx, cJSON **results_array) {
cJSON *interval_item = cJSON_GetObjectItemCaseSensitive(loop_node, “interval”);
cJSON *body_tasks = cJSON_GetObjectItemCaseSensitive(loop_node, “tasks”);

int interval = interval_item && cJSON_IsNumber(interval_item) 
               ? cJSON_GetNumberValue(interval_item) : 3;

ctx->loop_depth++;

int loop_count = 0;
while (1) {
    // 检测退出标志
    if (ctx->should_exit) {
        printf("[循环执行] 收到退出信号,退出循环\n");
        break;
    }
    
    loop_count++;
    printf("[循环执行] 第 %d 轮\n", loop_count);
    
    // 执行循环体任务
    cJSON *round_results = cJSON_CreateArray();
    int ret = execute_task_internal(body_tasks, ctx, &round_results);
    
    // 再次检测退出标志(循环体内部可能调用exit_task)
    if (ctx->should_exit) {
        cJSON_Delete(round_results);
        break;
    }
    
    // 等待interval秒
    if (interval > 0) {
        printf("[循环执行] 等待 %d 秒...\n", interval);
        sleep(interval);
    }
}

ctx->loop_depth--;
return 0;

}
变量引用机制

static double get_value(cJSON *item, exec_context_t *ctx) {
if (!item) return 0;

if (cJSON_IsString(item) && item->valuestring) {
    const char *str_val = item->valuestring;
    
    // 变量引用格式:$var_name
    if (str_val[0] == '$') {
        const char *var_name = str_val + 1;
        cJSON *var_item = exec_context_get_var(ctx, var_name);
        if (var_item) {
            if (cJSON_IsNumber(var_item)) {
                return cJSON_GetNumberValue(var_item);
            } else if (var_item->valuestring) {
                return atof(var_item->valuestring);
            }
        }
        return 0;
    }
    
    // 字符串形式的数字
    return atof(str_val);
} else if (cJSON_IsNumber(item)) {
    return cJSON_GetNumberValue(item);
}

return 0;

}
整个变量机制分为三个阶段: 命名 → 赋值 → 使用

通过auto_bind_variable自动命名变量
变量名格式: {技能名}_{目标参数},只有当技能的 output_schema 包含 value 字段时才会自动绑定变量,这个变量名会写入任务结构的 store 字段

多种提取类型(voltage、result、value)

voltage技能输出 ( skill_voltage.cpp ):

cJSON_AddNumberToObject(*output_result, “voltage”,
voltage); // 输出 voltage 字段
compare技能输出 ( skill_compare.cpp ):

cJSON_AddBoolToObject(*output_result, “result”, result); //
输出 result 字段
赋值阶段 – execute_action 函数

if (store_item && cJSON_IsString(store_item) && step_result) {
// 按优先级查找:voltage → result → value
cJSON *result_val = cJSON_GetObjectItemCaseSensitive(step_result, “voltage”);
if (!result_val) {
result_val = cJSON_GetObjectItemCaseSensitive(step_result, “result”);
}
if (!result_val) {
result_val = cJSON_GetObjectItemCaseSensitive(step_result, “value”);
}

// 将值存储到exec_context
if (result_val && cJSON_IsNumber(result_val)) {
    exec_context_set_var(ctx, store_item->valuestring, result_val);
    printf("  - 存储变量: %s = %.2f\n", store_item->valuestring, cJSON_GetNumberValue(result_val));
}

}
执行流程 :

获取任务结构中的 store 字段值(如 read_电压 )
调用技能处理器,获取 step_result (如 {“status”:“success”,“voltage”:0.47} )
按优先级查找字段:先找 voltage ,找不到找 result ,再找不到找 value
将找到的值存入 ctx->vars[“read_电压”]
退出机制:

// skill_registry_dispatch中设置退出标志
int skill_registry_dispatch(const char *skill_name, const char *params_json,
cJSON **output_result, exec_context_t *exec_ctx) {
// … 查找技能handler

if (strcmp(skill_name, "exit_task") == 0 && exec_ctx) {
    exec_ctx->should_exit = 1;  // 设置退出标志
}

return g_skill_table[i].handler(params_json, output_result, ctx);

}
exit_task 技能会设置 ctx->should_exit = 1,各执行器在循环中检测该标志,实现优雅退出。

新增技能流程
定义:在 skills.json 中添加技能元数据。

实现:编写 skill_xxx.cpp,实现具体的硬件操作逻辑。

注册:在 Registry 中注册 Handler。

实战演示
启动应用,加载模型和技能库:

./llm_demo …/Qwen3.5-0.8B_w4a16_g128_rv1126b.rkllm 1024 1024 ./skills.json
输入指令:

先关灯,每隔3秒,读取电压,若大于1V,再开灯,否则,关灯,若小于0.5V,退出任务

系统执行流程:

[阶段一] 识别到 9 个意图

  • 索引: 1, 技能: close, 参数: device=灯
  • 索引: 6, 技能: loop, 参数: interval=3
  • … (省略中间过程)

[阶段二] 映射到 9 个技能

[阶段三] 结构解析完成,生成 AST
{
“tasks”: [
{“type”:“action”,“skill_name”:“close”,…},
{“type”:“loop”,“interval”:3,“tasks”:[
{“type”:“action”,“skill_name”:“read”,“store”:“read_电压”,…},
{“type”:“condition”,“condition”:{“a”:“$read_电压”,“op”:“>=”,“b”:1},
“then”:[{“type”:“action”,“skill_name”:“open”,…}],
“else”:[…]}
]}
]
}

[执行引擎] 开始运行

[任务执行] 子任务: close -> LED 0 已关闭
[循环执行] 第 1 轮
[任务执行] 子任务: read -> [电压] ADC原始值: 4882, 电压: 1.07V

  • 存储变量: read_电压 = 1.07
    [条件判断] 条件成立,执行then分支
    [任务执行] 子任务: open -> LED 0 已打开
    …
    [循环执行] 第 12 轮
    [任务执行] 子任务: read -> [电压] ADC原始值: 2115, 电压: 0.46V
  • 存储变量: read_电压 = 0.46
    [条件判断] 条件成立,执行then分支
    [任务执行] 子任务: exit_task -> [退出任务] 任务已退出

[任务执行] 执行完成

更多推荐