基于端侧大模型的嵌入式 Skill 调度引擎实现智能业务生成的尝试
前言
近期在研究大模型(LLM) 与Skill(技能) 的结合应用,颇有感触。人类拥有绘画、编程等技能,依靠眼和手来完成;类比到嵌入式设备中,图像采集、数据传输同样是技能,只不过它们依靠板载硬件来完成。
通常,一块开发板的硬件资源是固定的,但上层应用是多变的。同一块板子,既可以在项目 A 中使用,也可以在项目 B 中使用,本质都是基于固定的硬件资源进行开发。因此,我们可以认为这块板子具备与生俱来的固有技能,而形形色色的应用,无非是对这些基础技能的组合与编排。
有人说过,电脑是人脑的延伸;如今,大模型则是人脑的近似。它能像人一样思考任务:先分析子目标,再进行规划,最后按部就班地执行。这正是我想实现的——基于大模型和 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](https://console.box.lenovo.com/l/l0tXb8)
提取码:rkllm

兼容性说明
支持平台:
-
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,当然如果需要使用其他的模型,也可以做转换部署
2. 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$,兔子的数量为 $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 幻觉。
-
栈式作用域:通过栈算法处理控制流,支持嵌套和切换。
整体架构:三阶段流水线
为了克服小模型的局限性,系统被拆分为三个严格的阶段:
用户输入 → 阶段一(意图识别) → 阶段二(技能映射) → 阶段三(结构解析) → 执行引擎
整体架构框图如下,展示各层次的组件和数据流:

阶段一:意图识别(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":"$read_电压","op":">=","b":1},
"then":[{"type":"action","skill_name":"open",...}],
"else":[
{"type":"action","skill_name":"close",...},
{"type":"condition","condition":{"a":"$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);
下面是一张完整时序图:

这张时序图,描述从用户输入到硬件操作的完整执行流程
核心模块详解
| 文件 | 职责 | 语言 |
|---|---|---|
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 -> [退出任务] 任务已退出
------------------------
[任务执行] 执行完成
总结
受限于 0.8B 小模型的推理能力,本次实践不得不牺牲部分灵活性,转而依赖代码层面的确定性逻辑来增强系统的鲁棒性。这是一种权衡:放弃纯粹的大模型调度,换取可控的执行结果。
然而,这也验证了工程落地的核心逻辑:同样的模型,不同的工程架构,结果天差地别。从自然语言理解到硬件执行,中间经历了意图识别、技能映射、结构解析和执行管控等多个环节。这种长链路的自动化,考验的不仅是模型的智商,更是工程的智慧。
随着端侧模型体量的不断增大(如更大的 1.5B、3B),有理由相信,模型的抽象推理能力将进一步解放代码逻辑,实现真正的“嵌入式AIGC”。
首发于21ic
微信公众号:软趴趴的工程师(一个乐于助人的工程师)

本文来自博客园,作者:pie_thn,转载请注明原文链接:https://www.cnblogs.com/pie-o/p/21825621

基于端侧大模型的嵌入式 Skill 调度引擎实现智能业务生成的尝试
浙公网安备 33010602011771号