很多人第一次看到 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 里,核心定义其实非常直接:
staticconst 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.\nIMPORTANT!!!: 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,描述就可以写成:
Controlarelay connectedtothe board.Usethis tool whenever the user askstoturnadevice 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,
然后具体执行函数长这样:
staticesp_err_tec_tool_gpio_control_execute(constchar *input_json,char*output,size_toutput_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: gpio21action=onlevel=1
因此 Tool 的输出建议做到:
稳定
简洁
可读
尽量结构化
这样后续模型再根据工具结果组织自然语言回复时,会更稳。
09
第五步:把 Tool 注册进系统
只写了 tools_gpio.c 还不够,系统还得知道有这么个 Tool。
esp_err_tec_tools_gpio_control(void){ ec_tools_register(&s_gpio_control); returnESP_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);returnESP_OK;}
3
第三步:写执行逻辑
你的执行函数里重点做三件事:
解析参数
校验参数
写入执行结果
4
注册
在 components/embed_claw/tools/ec_tools_reg.inc 里补一行:
EC_TOOLS_REG(demo)
5
测试
你最好至少验证这些场景:
合法参数能正常执行
缺少必填参数时返回明确错误
非法参数不会误操作硬件
Tool 输出格式稳定
这样你这个 Tool 才算真的能给模型用。
这样,你就能自己DIY定义各种各样的功能并通过大模型进行调用了。
-
硬件
+关注
关注
12文章
3626浏览量
69160 -
AI
+关注
关注
91文章
40941浏览量
302520 -
大模型
+关注
关注
2文章
3750浏览量
5268
发布评论请先 登录
一句话让大模型控制硬件:手把手教你给 EmbedClaw 添加自己的 Tool!
评论