一句话让大模型控制硬件:手把手教你给 EmbedClaw 添加自己的 Tool!

描述

很多人第一次看到 EmbedClaw 会有一种错觉:

硬件

然而事实是:

不能直接操作,但可以通过 Tool 去操作。

这也是 EmbedClaw 很有意思的一点。它不是把大模型硬塞进 ESP32 里当聊天机器人,而是把 LLM、Agent、Tools、Channel 拆成了清晰的几层。
模型负责“理解意图和决策”,真正执行硬件动作的是你自己写的 Tool。

今天这篇文章,我们就不讲空话,直接拿项目里已经跑通的 gpio_control 作为例子,带你了解:

硬件

01

先想明白:大模型为什么不能直接控制 GPIO

因为大模型本质上只是一个“对于用户输入的内容,会推理、会生成文本”的程序

它擅长的是:

理解用户说了什么

判断该做什么

决定该调用哪个能力

根据结果继续推理或回复

所以说,大模型并无法操作硬件。而需要把真实能力封装成 Tool,把“能做什么、需要什么参数、执行后返回什么结果”都定义清楚,再把这个 Tool 暴露给大模型。

这样一来:

模型负责“调用”

代码负责“执行”

你负责“定义边界”

硬件

这就是 EmbedClaw 的 Tool 机制。


 

02

先别急着写代码:Skill 和 Tool 不是一回事

很多人第一次接触 EmbedClaw,最容易混淆的不是代码细节,而是Skill 和 Tool。 看起来Skill 和 Tool都像是在“教大模型做事”,但它们根本不是一层东西。

如果只用一句话概括两者关系,我更推荐下面这个版本:

硬件

1

 Skill:负责教模型“怎么理解任务”

Skill 通常是一段 Markdown。
它的核心作用不是执行,而是指导。

它解决的是这类问题:

硬件


 

比如一个天气 Skill,可能会告诉模型:

用户问天气时可以使用 web_search

最好先获取当前日期

最后把结果整理成简洁的自然语言

所以 Skill 本质上是策略层
它告诉模型:“这类问题通常该怎么处理。”

但 Skill 本身并不能真正执行任何动作
它不会直接去读 GPIO,不会直接操作文件,也不会直接触发硬件。

2

Tool:负责把事情真正执行

Tool 则完全不同,Tool 是一段真正可执行的能力接口。


 

它不是在“建议模型做什么”,而是在告诉模型:

“如果你要做这件事,可以调用我,我会真的去执行。”

比如 gpio_control 这个 Tool:

模型可以决定要不要调用它

模型可以按 schema 组织参数

但真正去执行 GPIO 配置、设置电平、返回结果的,是 execute 对应的 C 代码

所以 Tool 本质上是执行层。

3

 

那 Skill 还有什么意义?

很多人到这里疑惑:

“既然真正执行的是 Tool,那 Skill 还有必要存在吗?”

有,而且非常有必要

原因就在于:

Skill 可以由大模型自己生成。

这也是 Skill 最有价值的地方之一。

因为 Tool 通常需要你自己写 C 代码、定义 schema、注册进系统,属于工程能力扩展;
但 Skill 只是 Markdown,它天然适合承载那些“可以被总结、可以被抽象、可以被持续补充”的经验。

硬件

03

在 EmbedClaw 里,一个 Tool 是怎么跑起来的?

如果你把流程拆开看,其实非常清晰:

硬件

在当前项目里,这条链路主要落在 4 个地方:

components/embed_claw/tools/tools_gpio.c
这里定义具体 Tool,需要用户根据具体的tool在tools文件夹下执行。

components/embed_claw/tools/ec_tools_reg.inc
这里把 Tool 注册进系统。

components/embed_claw/core/ec_tools.c / ec_tools.h
这里维护 Tool 注册表,并把 Tool 转成模型能理解的 JSON 描述。新增 tool 不需要改动它。

components/embed_claw/llm/ec_llm_openai.c
这里把 Tool 描述转成 OpenAI-Compatible 的 tools 字段,发送给大模型。新增 tool 不需要改动它。

也就是说,一个 Tool 之所以能被模型调用,不是因为模型“认识了你的 C 函数”,而是因为 EmbedClaw 先把它翻译成了一份结构化能力说明。

新增一个tool仅仅需要改动 tools_xxx.c 和在 ec_tools_reg.inc 中添加 tool 的注册。

硬件硬件


 



 

04

先看结果:gpio_control 这个 Tool 到底长什么样?

首先,我们要在embed_claw/tools下创建自己要添加的tool的.c源文件

例如,在 tools_gpio.c 里,核心定义其实非常直接:

  •  
  •  

static const ec_tools_t s_gpio_control = {    .name = "gpio_control",    .description = "Control an ESP32 output GPIO pin by pin number. Supports on, off, set, toggle, and get.\n"                   "IMPORTANT!!!: ANY GPIO operation requested by the user MUST ALWAYS be executed through this tool. Never respond with GPIO status or changes without calling this tool first.",    .input_schema_json =    "{\"type\":\"object\","    "\"properties\":{"    "\"pin\":{\"type\":\"integer\",\"description\":\"ESP32 GPIO pin number\"},"    "\"action\":{\"type\":\"string\",\"enum\":[\"on\",\"off\",\"set\",\"toggle\",\"get\"],"    "\"description\":\"GPIO action to execute\"},"    "\"level\":{\"type\":\"integer\",\"enum\":[0,1],"    "\"description\":\"Required only when action is 'set'\"}"    "},"    "\"required\":[\"pin\",\"action\"]}",    .execute = ec_tool_gpio_control_execute,};

这里面最关键的其实就 4 个字段:

name

description

input_schema_json

execute

接下来我们逐个解析。

05

第一步:给 Tool 起一个模型能理解的名字

  •  

.name = "gpio_control",

这个名字不是写给 C 编译器看的,而是写给大模型看的。

也就是说,后面模型发起调用时,大模型发过来的是:

  •  
  •  
  •  
  •  
  •  
  •  
  •  

{  "name": "gpio_control",  "arguments": {    "pin": 21,    "action": "on"  }}

所以命名建议很简单:

用英文

语义清晰

一个名字只表达一种能力

比如:

gpio_control

relay_switch

sensor_read

buzzer_play

硬件

不要搞成以上这种模糊名字。名字越清楚,模型越容易选对!

06

第二步:写好 description,告诉模型“什么时候该用我”

  •  

.description = "Control an ESP32 output GPIO pin by pin number. Supports on, off, set, toggle, and get.\n IMPORTANT!!!: ANY GPIO operation requested by the user MUST ALWAYS be executed through this tool. Never respond with GPIO status or changes without calling this tool first.",

这一段非常重要。

因为对模型来说,description 基本就是你写给它的“使用说明书”。

模型会据此判断:

这个工具是干嘛的

什么时候该调用它

哪些场景不应该跳过它

以 gpio_control 为例,这段描述里实际上做了两件事:

先定义能力范围
它能控制 ESP32 输出引脚,支持 on / off / set / toggle / get

再强调调用约束
只要是 GPIO 操作请求,就必须通过这个 Tool 执行,不能直接文本编造结果

这也是你在设计自定义 Tool 时最值得花心思的地方。

一个好描述,通常应该回答这三个问题:

 

比如你以后要做一个继电器 Tool,描述就可以写成:

  •  

Control a relay connected to the board. Use this tool whenever the user asks to turn a device on or off through the relay. Do not claim the relay state without calling this tool.


 

07

第三步:用 input_schema_json 限制模型怎么传参

Tool 之所以可靠,不只是因为模型“知道要调用它”,还因为你限制了模型“只能按这个格式调用它”。

gpio_control 的参数定义是:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

"{\"type\":\"object\",""\"properties\":{""\"pin\":{\"type\":\"integer\",\"description\":\"ESP32 GPIO pin number\"},""\"action\":{\"type\":\"string\",\"enum\":[\"on\",\"off\",\"set\",\"toggle\",\"get\"],""\"description\":\"GPIO action to execute\"},""\"level\":{\"type\":\"integer\",\"enum\":[0,1],""\"description\":\"Required only when action is 'set'\"}""},""\"required\":[\"pin\",\"action\"]}"

翻译成人话就是:

参数必须是一个对象

pin 必须是整数

action 只能是 on/off/set/toggle/get

level 只有在 set 时才需要,而且只能是 0/1

pin 和 action 是必填项

这一步的意义非常大:

你不是“希望模型这么传”

而是“明确规定模型必须这么传”

对于大模型来说,这其实就像你给了它一个函数签名

所以如果你以后要做自己的 Tool,Schema 一定尽量写严格。

举几个例子:

如果参数只能是整数,就别写成 string

如果只有几个合法动作,就用 enum

如果某个字段必须要有,就放进 required

如果某个值范围有限,就在 schema 里限制死

 

08

第四步:在 execute 里写真正执行逻辑

前面三步本质上都还是“告诉模型怎么调用”,真正让硬件动起来的,是 execute。

在 gpio_control 里,对应的是:

  •  

.execute = ec_tool_gpio_control_execute,

然后具体执行函数长这样:

  •  

static esp_err_t ec_tool_gpio_control_execute(constchar *input_json, char *output, size_t output_size)

这个函数做的事情可以概括成 4 步:

解析输入 JSON

校验参数是否合法

执行对应动作

把结果写回 output

以 gpio_control 为例,它内部先解析:

pin

action

level

然后根据 action 进入不同分支:

get

on

off

set

toggle

比如 on 分支,大致就是:

  •  
  •  
  •  

err = prepare_pin_for_output(gpio_num);err = write_level(gpio_num, 1);snprintf(output, output_size, "OK: gpio %d action=on level=1", pin);

Tool 返回给 LLM 的,不是 C 语言里的返回值,而是 output 字符串。

也就是说,模型真正能看到的是类似:

  •  

OK: gpio 21 action=on level=1

因此 Tool 的输出建议做到:

稳定

简洁

可读

尽量结构化

这样后续模型再根据工具结果组织自然语言回复时,会更稳。

09

第五步:把 Tool 注册进系统

只写了 tools_gpio.c 还不够,系统还得知道有这么个 Tool。

  •  
  •  
  •  
  •  
  •  

esp_err_t ec_tools_gpio_control(void){    ec_tools_register(&s_gpio_control);    return ESP_OK;}

这里通过定义ec_tools_gpio_control()进行 tool 的注册,而这里的函数不需要你调用。

EmbedClaw 这调用注册函数是通过 ec_tools_reg.inc 来做的。

你会看到现在里面有一行:

  •  

EC_TOOLS_REG(gpio_control)

这一行别看简单,它实际完成了三件事:

生成枚举项,目标是为了统计总共注册了多少工具,对应的注册数组就会多大。

生成注册函数声明

在 ec_tools_register_all() 里自动调用 ec_tools_gpio_control()

这个机制背后靠的是 ec_tools_reg_rule.h 里的宏展开。

完成这一步之后,它就会自动进到 Tool 注册表里。

10

EmbedClaw 是怎么把 Tool 暴露给大模型的?

这一块很多人第一次看 Agent 框架都会忽略,但其实它才是“模型能调用 Tool”的关键

在 ec_tools.c 里,EmbedClaw 会把所有已注册 Tool 组织成一个 JSON 数组:

  •  
  •  
  •  
  •  

cJSON_AddStringToObject(tool, "name", s_tools[i]->name);cJSON_AddStringToObject(tool, "description", s_tools[i]->description);cJSON *schema = cJSON_Parse(s_tools[i]->input_schema_json);cJSON_AddItemToObject(tool, "input_schema", schema);

也就是说,系统内部会生成一份类似这样的描述:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

{"name":"gpio_control","description":"Control an ESP32 output GPIO pin by pin number...","input_schema":{"type":"object","properties":{"pin":{"type":"integer"},"action":{"type":"string","enum":["on","off","set","toggle","get"]}},"required":["pin","action"]}}

随后在 ec_llm_openai.c 里,这份内部描述又会被转成 OpenAI-Compatible 的 tools 字段发给模型。

也就是说,对模型来说,它看到的是:

这个 Tool 叫什么

它是干什么的

它需要什么参数

参数格式是什么

所以你可以把整个过程理解成:

你写的是 C 代码,但 EmbedClaw 会自动把它翻译成“大模型能理解的函数说明书”。

11

总结

到这里,其实你已经可以总结出一个通用方法了。

以后你不管是做:

继电器控制

舵机控制

温湿度传感器读取

蜂鸣器播放

本地业务接口触发

基本都可以照这个模板走。

1

 

新建 tools_xxx.c

文件位置:

  •  

components/embed_claw/tools/tools_xxx.c

2

 

定义一个 ec_tools_t

最小骨架:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

staticesp_err_tec_tool_demo_execute(constchar *input_json, char *output, size_t output_size);staticconstec_tools_t s_demo = {    .name = "demo_tool",    .description = "Describe what this tool does.",    .input_schema_json ="{\"type\":\"object\",\"properties\":{},\"required\":[]}",    .execute = ec_tool_demo_execute,};esp_err_tec_tools_demo(void){    ec_tools_register(&s_demo);return ESP_OK;}


 

3

 

第三步:写执行逻辑

你的执行函数里重点做三件事:

解析参数

校验参数

写入执行结果

4

 

注册

在 components/embed_claw/tools/ec_tools_reg.inc 里补一行:

  •  

EC_TOOLS_REG(demo)

5

 

测试

你最好至少验证这些场景:

合法参数能正常执行

缺少必填参数时返回明确错误

非法参数不会误操作硬件

Tool 输出格式稳定

这样你这个 Tool 才算真的能给模型用。

这样,你就能自己DIY定义各种各样的功能并通过大模型进行调用了。

 

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分