虽然我并不认为自己是 Arduino 编程专家,但我非常喜欢在业余时间制作电子项目。所以前几天我突然冒出一个想法:我懂一点人工智能,也懂一点 Arduino,那么让它们一起工作怎么样?
在过去几周尝试了 CrewAI之后,我想出了以下实验:将 CrewAI 与 Arduino 连接起来。此外,我认为使用一些本地的 LLM 会很有趣,比如 Ollama提供的那些。
但是,在我们开始编写代码之前,你可能不熟悉 Arduino。那么,让我们从最基础的部分开始吧。
什么是 Arduino?
Arduino 是一款小型的可编程计算机,能让你创建自己的电子项目,从闪烁 LED 的基本电路到能够移动的先进机器人,无所不包。简单来说,在使用 Arduino 平台时,你需要了解有两个主要“部分”需要协同工作。
Arduino电路板
Arduino 电路板是物理硬件,其主要组件是微控制器芯片。该芯片负责运行您通过代码提供的指令。
你使用 Arduino 编程语言(基于 C/C++)编写代码,这些代码被称为“sketches”(草图)。这些草图会指示 Arduino 执行何种操作,例如点亮一盏灯、读取传感器数据或控制一个伺服电机。
例如,下面这个草图的作用就是让一个 LED 灯亮一秒,然后灭一秒,并如此循环。
void setup() {
pinMode(11, OUTPUT);
}
void loop() {
digitalWrite(11, HIGH); // 打开LED灯 (HIGH表示高电压)
delay(1000); // 等待1秒
digitalWrite(11, LOW); //通过写入LOW拉低电压关闭LED灯
delay(1000); // 等待1秒
} 我不会深入探讨 sketch 编程的细节,因为这超出了本教程的范围。但是,如果你对这个迷人的世界感兴趣,我建议你阅读 Arduino 教程。
构建一个简单的电路
为了探索大型语言模型(LLM)与 Arduino 之间的联系,我选择了一个简单的项目:让三个 LED 灯循环闪烁。你可以在上图中看到电路的设置。
现在电路已经搭建好了,合乎逻辑的下一步将是手动编写一个 sketch 来控制这些 LED 灯。然而,这正是我们想要利用 LLM 来自动化的事情!
所以,我们将让一个 CrewAI 代理来处理编程工作,而不是自己编写代码。同样,我们也会让另一个 CrewAI 代理来负责为我们编译代码并将其上传到 Arduino,而不是手动执行这些操作
实现一个Crewai的Agents
让我们把重点放在上方的图表上,它概述了我们即将构建的 CrewAI 应用程序。
如你所见,这个设置很简单,由两个代理 组成:Sketch 编程代理和 Arduino 上传代理。第一个代理将接收电路的描述及其预期行为,然后生成一个扩展名为 .ino 的 sketch 文件。第二个代理将编译生成的 sketch 并将代码上传到 Arduino。
让我们来详细研究一下每个代理。
Sketch 编程代理
在本文前面,我提到过我们将使用本地 LLM。为此,我们将使用 Ollama。
如果你不熟悉 Ollama,Matthew Berman 有一个关于它的非常好的视频。
要将 Ollama 与 CrewAI 集成,只需在定义代理之前添加以下几行代码即可。
llama3 = ChatOpenAI(
model="llama3",
base_url="http://localhost:11434/v1"
)请注意,我正在使用 Llama 3 模型。如果你也想这样做,请确保它已经在 Ollama 中下载。如果还没有,你可以通过运行这个简单的命令来下载它。
ollama pull llama3现在,是时候进行代理的实现了。
sketch_programmer_agent = Agent(
role="Sketch Programmer",
goal=dedent(
"""为 Arduino 编写一个 Sketch 脚本,该脚本可以点亮数字引脚 11 上的红色 LED,数字引脚 10 上的蓝色 LED 和数字引脚 9 上的绿色 LED,三个 LED 之间的间隔时间为 1 秒。"""),
backstory=dedent(
"""你是一位经验丰富的 Sketch 程序员,非常喜欢对 Arduino 进行编程。 """
),
verbose=True,
allow_delegation=False,
llm=llama3,
)该Agent的目标包含了我们已经实现的电路信息,以及其预期的行为。此外,你会注意到该代理正在使用 llama3 作为其大型语言模型(LLM)。
sketch_programming_task = Task(
description=dedent(
"编写一个可以立即上传到 Arduino 的 Sketch 脚本。 只要 Arduino 代码,别的什么都不要。"),
expected_output=dedent(
"一段可直接复制粘贴到 Arduino CLI 的纯 Sketch 脚本"),
agent=sketch_programmer_agent,
output_file="./tmp/tmp.ino",
)请注意 output_file 属性。这一点至关重要,因为 Sketch 编程代理会将生成的代码保存到 ./tmp/tmp.ino 文件中。
Arduino 上传Agent
既然前一个代理已经生成了 sketch 文件,Arduino 上传代理就需要编译它并将其上传到 Arduino。我们如何实现这一点呢?
解决方案是使用一个自定义工具。
您可以按照这篇 CrewAI 文档 学习如何实现您自己的自定义工具。
import re
import subprocess
from crewai_tools import BaseTool
class CompileAndUploadToArduinoTool(BaseTool):
name: str = "编译和上传至arduino工具"
description: str = """编译和上传一个 Arduino Sketch Script
给arduino"""
ino_file_dir: str = "包含 ino 文件的目录"
board_fqbn: str = "电路板类型,例如 'arduino:avr:uno'"port: str = "Arduino 连接的端口"
def __init__(self, ino_file_dir: str, board_fqbn: str, port: str, **kwargs):
super().__init__(**kwargs)
self.ino_file_dir = ino_file_dir
self.board_fqbn = board_fqbn
self.port = port
def _fix_ino_file(self):
"""
这是一个辅助方法,用于修复输出的 .ino 文件,以防 Llama3 添加一些意想不到 的文本导致编译无效
"""
with open(f"{self.ino_file_dir}/tmp.ino", "r") as f:
content = f.read()
pattern = r'```.*?\n(.*?)```'
match = re.search(pattern, content, re.DOTALL).group(1).strip()
with open(f"{self.ino_file_dir}/tmp.ino", "w") as f:
f.write(match)
def _run(self):
self._fix_ino_file()
try:
subprocess.check_call([
"arduino-cli", "compile", "--fqbn",
self.board_fqbn, self.ino_file_dir
])
subprocess.check_call([
"arduino-cli", "upload", "--port",
self.port, "--fqbn", self.board_fqbn, self.ino_file_dir
])
except subprocess.CalledProcessError:
return "编译失败"
return "代码已经成功上传给Board"该工具需要三个参数:
- ino_file_dir:包含 sketch 的目录。请记住,我们使用的是 tmp 目录。
- board_fqbn:电路板类型。我使用的是 Arduino UNO,所以我的电路板类型是 arduino:avr:uno,但你的情况可能会有所不同。
- port:Arduino 连接的端口。我的连接在 /dev/cu.usbmodem1201 端口。
当代理使用该工具时(通过访问 _run 方法),它会编译代码,然后将其上传到 Arduino。您可以在下面查看代理的实现——在这种情况下,我使用了 GPT-4,因为它在使用工具时效果要好得多:
tool = CompileAndUploadToArduinoTool(
ino_file_dir="./tmp",
board_fqbn="arduino:avr:uno",
port="/dev/cu.usbmodem1201"
)
arduino_uploader_agent = Agent(
role="Arduino 上传 Agent",
goal="""你的目标是使用工具编译和上传接收到的Arduino脚本l""",
backstory=dedent(
"""
You are a hardware geek.
"""
),
verbose=True,
allow_delegation=False,
tools=[tool]
)
arduino_uploading_task = Task(
description=dedent(
"将草图(Sketch)编译并上传到Arduino"),
expected_output=dedent(
"""仅将代码编译并上传到Arduino中"""),
agent=arduino_uploader_agent,
)
最终实现
虽然花了一些时间,但我们现在已经组装好了构建 CrewAI 应用程序所需的所有组件。是时候创建“船员”(Crew)了。
from crewai import Crew
from dotenv import load_dotenv
from agents import sketch_programmer_agent, arduino_uploader_agent
from tasks import sketch_programming_task, arduino_uploading_task
load_dotenv()
crew = Crew(
agents=[sketch_programmer_agent, arduino_uploader_agent],
tasks=[sketch_programming_task, arduino_uploading_task],
)
result = crew.kickoff()
print(result) 