打造智能家居的AI代理
近十年来,我一直在研究智能家居 API。我购买了许多系统,构建了多个版本的集成 API、移动应用程序和聊天机器人。你可以在此处阅读我迄今为止构建的内容
虽然所有这些都是令人兴奋的项目,而且真的很有趣——但它们从未完全实现真正的目标——一个像钢铁侠系列中的 JARVIS 一样的真正的AI驱动的家。直到大型语言模型的出现。
1、旧世界:基础版助手
LLM极大地推动了聊天机器人对话体验的发展。几年前,我使用微软的 Luis.ai 构建了一个“聊天机器人”,当时它真的很前沿,但现在感觉像是一件遗物,我已将该软件包存档在 monorepo 中。该聊天机器人只能处理使用准确单词的查询,如果您尝试错误的短语,它就会崩溃。
这次我采用了自主代理方法。代理不使用决策树类型的逻辑,而是依靠LLM进行“推理”。这意味着它们可以处理更复杂的自然语言输入。一个很好的例子是,如果我要求旧的聊天机器人“关掉休息室的灯”——聊天机器人可能无法做到这一点,因为它不知道休息室是哪个房间——它只知道“客厅”。我发现,无论有多少种不同的表达方式—我的妻子都会尝试其他方式,而同义词并没有被编入 Luis AI 中。现在,LLM 只知道实际的房间名称是什么,并通过无数种命名房间的方式推理它。
2、创建 LangGraph.AI 代理
用真正的 AI 替换 LUIS.ai 并不太难—开始使用 LangGraph 真的很容易。这个概念很简单,有一个带有基本系统提示的代理,可以将任务提示作为输入。然后,代理会获得一些与提示相关的功能“工具”(例如“你可以使用此工具来控制灯光……”)。然后,代理使用底层 LLM 来决定应该使用哪些工具来回答任务提示。这就是范式转变的地方。不需要逻辑流程或决策树——旧的“AI 只是一大组 IF 语句”的陈词滥调已经消失—这些聊天机器人实际上能够独立推理。
async function callModel(state: typeof MessagesAnnotation.State) {
const response = await model.invoke(state.messages);
return {
messages: [
"Your personal name is KAREN. You are a friendly and conversational personal assistant that manages my smart home. The prompt is more complex than this but I'm not leaking that",
response
]
};
}
const workflow = new StateGraph(MessagesAnnotation)
.addNode("agent", callModel)
.addEdge("__start__", "agent")
.addNode("tools", toolNode)
.addEdge("tools", "agent")
.addConditionalEdges("agent", shouldContinue);
const memory = new MemorySaver();
const app = workflow.compile({ checkpointer: memory });
export const runAgent = async (message: string, conversationId: string) => {
const finalState = await app.invoke({
messages: [new HumanMessage(message)],
}, {
configurable: {
thread_id: conversationId
}
});
return finalState.messages[finalState.messages.length - 1].content;
};
对于一个完全可运行的代理来说,实际代码最初看起来非常简单。下面你可以看到它是多么简单,传入一组工具,创建一些内存并给它一个基本的系统提示,它就会处理请求。直到我开始创建工具时,我才意识到自治代理的复杂性所在。
3、为代理提供控制家庭的工具
在纸面上为代理提供一套管理房屋的工具似乎很容易。API 已经以某种形式存在了近十年—将工具提示包装在上面肯定是一项快速而干净的工作吗?不可能。
这是你真正需要考虑的地方,不仅仅是 API 平台中的“开发人员体验”,还有“代理体验”。我遇到了一些非常有趣的错误。目前,天气和供暖 API 返回的温度以摄氏度和华氏度表示。显然,UI 会在它们之间进行选择。LLM 确实很难保持一致,直到我调整了代理的主系统提示几次。
但是,有些工具几乎无缝地工作。我创建了一个 GetLighting 工具,它查询集成 API,然后告诉代理该灯组的状态。定义工具提示需要几次尝试,但最终我让它全部工作了。诀窍似乎是不要过于冗长,而是突出显示代理应该关注的关键项目——它在调用工具时已经传入了 json 输出。
export const changeLights = tool(async (input: { roomId: string, on: boolean, bri: number, color?: string }) => {
if (input.roomId && isNaN(Number(input.roomId)) || !Number.isInteger(Number(input.roomId))) {
throw new Error("The ID provided is a string and not a numerical ID. Please map this room name to the room ID.");
}
const lightGroupId = input.roomId;
let response;
try {
response = await updateLights({ lightGroupId, on: input.on, bri: input.bri, color: input.color });
} catch (err: AxiosError) {
throw new Error(err.message);
}
return response.data.data;
}, {
name: 'change_Lights',
description: 'Update the lights in a room. This can be whether it’s on or off, its brightness, or even its color.',
schema: z.object({
roomId: z.string().describe("The Light ID of the room to get the lighting status for."),
on: z.boolean().describe("Whether to turn the lights on or off."),
bri: z.number().describe("The brightness of the lights. This is a number between 0 and 254. 0 is off and 254 is full brightness."),
color: z.string().optional().describe("The color of the lights. This is a hex code. This is only needed for the Master Bedroom Light and is not supported in other rooms.")
})
});
改变状态同样简单(如上所示),主要是因为 Zod。Zod 通常用于验证 API 的输入。 LangGraph 利用这个库来验证和记录工具规范——这会以合同的形式输入代理,代理需要提供哪些值来执行工具。这一切都意味着对巧妙提示的依赖减少了,因为 Zod 会处理它。我几乎第一次就能够让代理处理灯光的开/关、亮度和颜色。大多数实际的 API 调用逻辑和验证都在 SDK 中,该 SDK 用于聊天机器人、UI 和 NFC 标签系统——但你会注意到我必须添加一小段额外的验证。
这里再次出现的巨大转变是,UI 很可能总是会向你传递房间的数字 ID——但 LLM 可能不会。顺便说一句,这种潜在错误的转变改变了我们在代理中处理错误的方式——该工具返回自然语言错误,而不是要处理的特定代码。
这才是我开始意识到使用我的 API 的 LLM 代理和 UI 之间的区别的地方——我构建的 API 拥有非常出色的开发人员和系统体验……但代理体验很差。
4、代理体验
开发者体验 (DX) 是我们熟悉的一个概念 — 它指的是软件工程师使用工具(在本例中为 API)的难易程度和愉悦程度。从消费角度来看,智能家居 API 一直都完全按照我的期望运行 — 因为我是唯一的消费者,所以当我发现从 UI 或聊天机器人的角度来看某些东西不起作用时,我会更改 API。我在构建此代理时发现,有些东西根本“不起作用”。
一个很好的例子就是房间。在 UI 应用程序中,UI 将调用 API 来获取所有房间及其 ID。然后它将使用这些 ID 来更新所述房间的状态 — 更改照明、暖气等。对于 LLM 代理来说,这种流程并不是很好用。我不希望每次需要确定“客厅”引用哪个 ID 时都必须调用获取所有 API。
我意识到在开发这个底层 API 的过程中,我从未考虑过我正在构建的“代理体验”。到目前为止,ID 一直都很好。我不想完全重写整个 API,所以我利用 LangGraph 来解决这个问题——只需构建另一个工具即可。
def get_room_mappings():
room_mappings = {
'Living Room': {'lights': 'LR001', 'heating': 'HTR001', 'blinds': 'BLD001'},
'Bedroom': {'lights': 'BR001', 'heating': 'HTR002', 'blinds': 'BLD002'},
# Add more rooms and their mappings as needed
}
return room_mappings
这个简单的工具只返回一组基本的房间映射,使代理能够做出一些真正明智的决定,如果房间已列出,它可以使用它,如果它能找到该房间的同义词,那也没问题。 LangGraph.JS 中的工具是填补代理体验空白的好方法,无需重新设计底层的所有内容,并在需要时为代理提供上下文。我最终构建了许多“上下文”工具,例如日期工具(因为基础 LLM 认为日期仍然是 2023 年末!),甚至还有一个“关于”工具,当代理被问到自我识别问题时,它可以为代理提供一些真实的角色。
5、添加内存
到目前为止,代理能够处理基本任务,与智能家居互动,但无法进行对话。没有流程,没有讨论——只有陈述和回应。传统的聊天机器人使用类似瀑布的决策树来处理这个问题,这种决策树很容易被打破,而且非常僵化。
LangGraph 允许你为对话创建基本记忆,只需向代理传递一个线程 ID 即可。这允许 LLM 引用“聊天”中的先前消息,为其提供额外的上下文。这就是我解决第一张图片中原始问题的方法;“你能关掉灯吗?当然!你能把它打开吗?……打开什么?”代理现在可以处理讨论线程——这不仅有助于将代理展示为面向用户的助手,还允许它处理与其他代理的更复杂的交互。
代理本身被包裹在一个简单的 Express.JS 服务器后面,因此如果未设置线程 ID 标头,则每个进入服务器的请求都会生成一个 UUID。这是一种非常简单的方法,只需利用现有的 HTTP 属性即可为代理添加实际的短期记忆。
6、将代理展示给用户
拥有代理很棒,但我实际上想使用它。鉴于 LangGraph 代理只是 javascript,我只是用 Typescript 构建了代理(因为我不是尼安德特人),将其包装在服务器框架中,然后将其容器化并部署到我的生产服务器(我前厅的 Raspberry Pi)。
2024 年,我花了一年时间开发了一个新的渐进式 Web 应用程序来实际控制智能家居,因此下一步只是添加一个与代理服务器对话的聊天界面。 我使用 React 和 Material UI 的“Joy-UI”组件库在一小时左右的时间里拼凑了一个非常简单的屏幕。
UI 非常基础,但这有点刻意。 我想发布它并让我和我的妻子每天使用,因为这是开发聊天机器人和 AI 代理的最佳方式——迭代。 完善体验可能需要数月时间,如果不使用它,你将错过宝贵的反馈。随着时间的推移,我会改进用户界面,但由于它已经上线,我也会同时改进代理。
7、触发 AI 代理
到目前为止,我实际上只创建了一个聊天机器人——但我想重新创建类似 Jarvis 的东西。AI 助手应该是自主的——这意味着它不需要用户输入即可运行。我需要一种方法让我的助手按时启动或处理不是来自我的输入。
我利用了自上次撰写本文以来为智能家居构建的新应用程序——自动化服务器。因为我想要处理的许多“作业”都是每周例行发生的,所以我构建了一个 cronjob 调度程序,它会通过触发 API 端点向我发送通知或提醒。这主要用于告诉我需要取出哪个垃圾箱或告诉我早上上班的火车是否被取消。
// Schedule Bin Alert for Tuesday at 18:00
schedule('0 18 * * 2', async () => {
console.log('Executing Bin Alert');
try {
await executeBinAutomation();
} catch (err) {
console.error('Unable to execute Bin Automation:', err);
}
});
// Schedule Train Alert for Monday and Tuesday at 6:30
schedule('30 6 * * 1,2', async () => {
console.log('Executing Train Alert');
try {
await executeTrainsAutomation();
} catch (err) {
console.error('Unable to execute Train Automation:', err);
}
});
我将 AI 代理放到调度程序上,并将其设置为o 运行各种不同的提示。其中一些与以前相同;“通过查看日历告诉我今晚要取出哪个垃圾桶,然后发送通知”或“检查我通常乘坐的 7:13am 开往伯明翰的火车是否被取消或延误”——它们效果很好——只是现在更加个性化。
我还利用了不同类型的预定提示,例如“今天是我的办公日,查看天气、火车、日历和新闻并向我发送早间更新”。这产生了一条可爱的早间消息,其中包含我需要了解的当天信息。我添加的另一个提示是当有人在门口时。几年前,我破解了我的 Ring Doorbell 进行面部识别(您可以在此处听到),所以我重新使用了一些现已失效的技术,让代理来处理该过程。
我重新指向我的所有 NFC 端点以发送提示。不久前,我在家里创建了一堆 NFC 标签,用于处理我不想登录 PWA 的任务——例如打开/关闭百叶窗。我在前门添加了一个新的 NFC 标签,这样当我离开家时,就会发送提示“我刚刚离开家,请降低所有房间的温度,关闭所有灯并拉上所有窗帘……”
8、最后……自主 AI
最后一步实际上是让代理真正控制房屋本身,而无需用户输入。除了在每个房间添加运动传感器(我可能会这样做)之外,我还在努力弄清楚 AI 如何知道我们是否在家。我最终 POC 了一个工具,让代理登录我的家庭路由器并检查我们的手机是否连接到 wifi——如果没有,我们可能不在家。每 30 分钟,AI 就会唤醒一次(由于调度程序),检查我们是否在家,如果不在家——它会关闭所有灯并降低暖气。如果我们确实在家,它会检查今天是否是在家办公日,并努力将家庭办公室的温度调到合适的水平等。
我仍在调整调度程序时间和提示,这就是我发现实际工作所在。构建一个自主 AI 代理来运行我的智能家居真的很容易——但构建一个完全按照我的要求运行的代理要困难得多!我原本以为提示工程是一门像软件工程一样的科学,但最终感觉更像是一门艺术。
原文链接:How I built an Autonomous AI Agent to run my Smart Home
汇智网翻译整理,转载请标明出处