Doğal dil ile cihaz kontrolü, insanların dashboard'lar arasında arama yapmak yerine "ofisi biraz ısıt" veya "oturma odasındaki her şeyi kapat" gibi ne istediklerini söylemelerini sağlar. Bu eğitimde, bir LLM'in istekleri yapılandırılmış fonksiyon çağrılarına çevirdiği, bir doğrulama katmanının her çağrıyı bir cihaz kaydına göre kontrol ettiği ve yalnızca geçerli komutların MQTT üzerinden, komutları onaylayan cihazlara yayınlandığı bir Python servisi oluşturacaksınız.
Akış Nasıl Çalışır?
Temel fikir şu: dil modeli asla MQTT mesajı yazmaz. Yalnızca sizin tanımladığınız bir fonksiyon menüsünden seçim yapar; bu seçimleri kontrol ettikten sonra komutlara dönüştüren ise sizin kodunuzdur.
- Kullanıcı isteği. Bir sohbet kutusundan, bir sesli asistan dökümünden veya bir mesajlaşma botundan gelen serbest metin.
- Bağlam. Servis, modele cihazların, kullanıcı dostu adlarının ve yeteneklerinin kompakt bir listesini verir.
- Function calling. Model sıfır veya daha fazla araç çağrısı döndürür, örneğin
set_light(device="kitchen-ceiling", on=true, brightness=30). - Doğrulama. Kod, cihazın var olduğunu, yeteneği desteklediğini ve değerlerin aralık içinde olduğunu kontrol eder.
- Yayın. Geçerli komutlar benzersiz bir komut kimliğiyle MQTT üzerinden
.../<device>/settopic'ine gider. - Onay. Cihaz değişikliği uygular ve yeni durumunu yayınlar; servis bunu kullanıcıya bildirir.
| Katman | Güvenilir mi? | Görevi |
|---|---|---|
| LLM | Hayır | Niyeti yorumlamak, fonksiyonları ve argümanları seçmek |
| Doğrulayıcı | Evet | Kaydı, yetenekleri ve değer aralıklarını uygulamak |
| MQTT broker | Evet | Komutları ve durumu güvenilir şekilde iletmek |
| Cihaz | Evet | Değişikliği kendi limitleri içinde uygulamak ve durumu bildirmek |
Ön Koşullar
- Python 3.10 veya üzeri.
qwen2.5:7bveyallama3.1:8bgibi araç çağrısını destekleyen bir modelle yerel olarak çalışan Ollama. Function calling destekleyen her sağlayıcı aynı şekilde çalışır; yalnızca API çağrısı değişir.- Bir MQTT broker. Demo için broker.connectmq.com kullanıyoruz.
ollama pull qwen2.5:7b
pip install ollama "paho-mqtt>=2.0"
Adım 1: Cihaz Kaydını Tanımlayın
Kayıt, neyin var olduğu ve neler yapabildiği konusunda tek doğruluk kaynağıdır. Model bunun bir özetini görür; doğrulayıcı ise tamamını kullanır.
# registry.py
DEVICES = {
"kitchen-ceiling": {"name": "kitchen ceiling light", "room": "kitchen", "type": "light", "dimmable": True},
"kitchen-under": {"name": "kitchen under-cabinet light", "room": "kitchen", "type": "light", "dimmable": False},
"living-lamp": {"name": "living room floor lamp", "room": "living room", "type": "light", "dimmable": True},
"living-blinds": {"name": "living room blinds", "room": "living room", "type": "blinds"},
"office-thermo": {"name": "office thermostat", "room": "office", "type": "thermostat", "min_c": 16, "max_c": 26},
}
def describe_devices():
lines = []
for dev_id, d in DEVICES.items():
extra = ""
if d["type"] == "light":
extra = " (dimmable)" if d["dimmable"] else " (on/off only)"
if d["type"] == "thermostat":
extra = f" (range {d['min_c']}-{d['max_c']} C)"
lines.append(f"- {dev_id}: {d['name']}, room={d['room']}, type={d['type']}{extra}")
return "\n".join(lines)
Adım 2: Fonksiyonları Tanımlayın
Her fonksiyon bir yeteneğe karşılık gelir. Şemada cihaz kimliklerini enum olarak listelemek, modelin geçerli hedefleri seçmesine yardımcı olur.
# tools.py
from registry import DEVICES
LIGHTS = [k for k, d in DEVICES.items() if d["type"] == "light"]
BLINDS = [k for k, d in DEVICES.items() if d["type"] == "blinds"]
THERMOS = [k for k, d in DEVICES.items() if d["type"] == "thermostat"]
TOOLS = [
{"type": "function", "function": {
"name": "set_light",
"description": "Turn a light on or off and optionally set brightness in percent (dimmable lights only).",
"parameters": {"type": "object", "properties": {
"device": {"type": "string", "enum": LIGHTS},
"on": {"type": "boolean"},
"brightness": {"type": "integer", "minimum": 1, "maximum": 100}},
"required": ["device", "on"]}}},
{"type": "function", "function": {
"name": "set_blinds",
"description": "Set blinds position in percent, 0 is closed and 100 is fully open.",
"parameters": {"type": "object", "properties": {
"device": {"type": "string", "enum": BLINDS},
"position": {"type": "integer", "minimum": 0, "maximum": 100}},
"required": ["device", "position"]}}},
{"type": "function", "function": {
"name": "set_thermostat",
"description": "Set the target temperature in degrees Celsius.",
"parameters": {"type": "object", "properties": {
"device": {"type": "string", "enum": THERMOS},
"target_c": {"type": "number"}},
"required": ["device", "target_c"]}}},
]
Adım 3: Her Çağrıyı Doğrulayın
Şemalar modele yol gösterir ama onu bağlamaz. Özellikle küçük yerel modeller bazen sayıları metin olarak döndürür, cihaz kimlikleri uydurur veya aralıkları görmezden gelir. Doğrulayıcı, normalleştirilmesi güvenli olanı normalleştirir ve geri kalan her şeyi bir gerekçeyle reddeder.
# validate.py
from registry import DEVICES
class Invalid(Exception):
pass
def as_int(value, field):
try:
return int(round(float(value)))
except (TypeError, ValueError):
raise Invalid(f"{field} must be a number")
def validate(name, args):
dev_id = args.get("device")
dev = DEVICES.get(dev_id)
if dev is None:
raise Invalid(f"unknown device {dev_id!r}")
if name == "set_light":
if dev["type"] != "light":
raise Invalid(f"{dev_id} is not a light")
on = args.get("on")
if isinstance(on, str):
on = on.lower() in ("true", "on", "1")
cmd = {"on": bool(on)}
if "brightness" in args and args["brightness"] is not None:
if not dev["dimmable"]:
raise Invalid(f"{dev['name']} is not dimmable")
b = as_int(args["brightness"], "brightness")
if not 1 <= b <= 100:
raise Invalid("brightness must be 1-100")
cmd["brightness"] = b
return dev_id, cmd
if name == "set_blinds":
if dev["type"] != "blinds":
raise Invalid(f"{dev_id} is not blinds")
p = as_int(args.get("position"), "position")
if not 0 <= p <= 100:
raise Invalid("position must be 0-100")
return dev_id, {"position": p}
if name == "set_thermostat":
if dev["type"] != "thermostat":
raise Invalid(f"{dev_id} is not a thermostat")
try:
t = round(float(args.get("target_c")), 1)
except (TypeError, ValueError):
raise Invalid("target_c must be a number")
if not dev["min_c"] <= t <= dev["max_c"]:
raise Invalid(f"target must be {dev['min_c']}-{dev['max_c']} C")
return dev_id, {"target_c": t}
raise Invalid(f"unknown function {name}")
Adım 4: Doğal Dil ile Cihaz Kontrolü Servisi
Şimdi her şeyi birbirine bağlayın. Servis bir isteği okur, modelden araç çağrıları ister, bunları doğrular ve komutları yayınlar.
# nl_control.py
import json
import time
import uuid
import ollama
import paho.mqtt.client as mqtt
from registry import describe_devices
from tools import TOOLS
from validate import Invalid, validate
MODEL = "qwen2.5:7b"
PREFIX = "connectmq/demo/e3b90c/home" # use your own random id
SYSTEM = (
"You control smart home devices by calling functions. "
"Only use device IDs from this list:\n" + describe_devices() + "\n"
"If a request is ambiguous or no device matches, ask a short clarifying question instead of calling a function. "
"Never guess temperatures outside the allowed range."
)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="nl-control-e3b90c")
client.on_message = lambda c, u, m: print(f" state {m.topic.split('/')[-2]}: {m.payload.decode()}")
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.subscribe(f"{PREFIX}/+/state", qos=1)
client.loop_start()
def handle(request):
resp = ollama.chat(
model=MODEL,
messages=[{"role": "system", "content": SYSTEM}, {"role": "user", "content": request}],
tools=TOOLS,
options={"temperature": 0},
)
calls = resp.message.tool_calls or []
if not calls:
print("assistant:", resp.message.content)
return
for call in calls:
name, args = call.function.name, dict(call.function.arguments)
try:
dev_id, cmd = validate(name, args)
except Invalid as err:
print(f"rejected {name}({args}): {err}")
continue
cmd.update({"id": str(uuid.uuid4()), "ts": int(time.time() * 1000), "source": "nl"})
client.publish(f"{PREFIX}/{dev_id}/set", json.dumps(cmd), qos=1)
print(f"sent {dev_id}: {cmd}")
if __name__ == "__main__":
while True:
text = input("\nyou: ").strip()
if text:
handle(text)
Temperature değerini 0'a ayarlamak araç seçimini daha tekrarlanabilir kılar; kontrol görevlerinde istediğiniz de budur.
Adım 5: Simüle Edilmiş Bir Cihaz Ekleyin ve Test Edin
Küçük bir simülatör tüm set topic'lerine abone olur, komutları uygular ve retained durum yayınlar:
# sim_home.py
import json
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/e3b90c/home"
state = {}
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(f"{PREFIX}/+/set", qos=1)
def on_message(client, userdata, msg):
dev = msg.topic.split("/")[-2]
cmd = json.loads(msg.payload)
state.setdefault(dev, {}).update({k: v for k, v in cmd.items() if k not in ("id", "ts", "source")})
client.publish(f"{PREFIX}/{dev}/state", json.dumps({**state[dev], "lastCommand": cmd["id"]}), qos=1, retain=True)
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="sim-home-e3b90c")
c.on_connect = on_connect
c.on_message = on_message
c.connect("broker.connectmq.com", 1883, keepalive=60)
c.loop_forever()
Önce simülatörü, ardından servisi çalıştırın ve şu istekleri deneyin:
| İstek | Beklenen sonuç |
|---|---|
| Mutfak tavan ışığını yüzde 30'a kıs | Parlaklığı 30 olan tek bir set_light çağrısı |
| Oturma odasındaki her şeyi kapat | Lamba için set_light ve yoruma bağlı olarak bir soru ya da set_blinds |
| Ofisi 35 derece yap | Doğrulayıcı tarafından reddedilir veya modelden netleştirici bir yanıt gelir |
| Dolap altı ışığını kıs | Reddedilir: kısılabilir değil |
| Garaj ışığını aç | Netleştirici soru, çünkü böyle bir cihaz yok |
İlginç olan, başarısız durumlardır. Her biri ya modelin netleştirme istemesiyle ya da doğrulayıcı tarafından yakalanır ve geçersiz hiçbir şey broker'a ulaşmaz.
Model Neden Asla MQTT Payload'ı Yazmaz?
Fonksiyon katmanını atlayıp modelden doğrudan bir topic ve JSON payload üretmesini istemek cazip gelebilir. Bu yaklaşım ince şekillerde bozulur. Model doğru görünen ama var olmayan bir topic uydurabilir, cihaz türleri arasında anahtar adlarını karıştırabilir veya yanlış birimlerle geçerli bir JSON üretebilir. Bu hataların her biri broker'a ulaşır ve bazı cihazlar hatalı komutları sessizce görmezden gelerek kullanıcıyı değişikliğin gerçekleştiğine inandırır.
Function calling ile model, parametrelerini sizin tanımladığınız birkaç adlandırılmış işlem arasından seçim yapar. İşlemden topic'e ve payload biçimine eşlemenin sahibi sizin kodunuzdur; bu sayede bir cihazın firmware'ini veya topic düzenini değiştirmek asla prompt'ları değiştirmeyi gerektirmez. Doğrulayıcı da komutların oluşturulduğu tek yer hâline gelir; bu da onu hiç model kullanmadan sıradan birim testleriyle test etmeyi kolaylaştırır.
Takip İsteklerini Ele Almak
Gerçek konuşmalar nadiren tek seferliktir. İnsanlar "biraz daha parlak" veya "diğerini de" der. Bunu desteklemek için son birkaç kullanıcı ve asistan mesajını messages listesinde tutun ve yakın zamanda değiştirilen cihazların retained durum topic'lerinden okunan mevcut durumunu sistem prompt'una ekleyin. Böylece model "biraz daha parlak" ifadesini az önce değiştirdiği ışıkla eşleştirebilir ve mevcut değerden yeni bir parlaklık hesaplayabilir; doğrulayıcı ise sonucu yine kontrol eder. Eski bağlamın yeni isteklere sızmaması için geçmişi birkaç turla sınırlayın.
Üretime Hazır Hâle Getirmek
Demodan gerçek bir eve, binaya veya tesise geçerken şu noktaları aklınızda tutun:
- Özel kimlik bilgileri ve TLS kullanın. Herkese açık MQTT test broker'ı herkes tarafından okunabilir. Gerçek cihazlar için ConnectMQ portalında oluşturulan kimlik bilgileriyle 8883 portundan bağlanın.
- Yıkıcı veya olağandışı işlemleri teyit edin. Tüm ışıkları kapatmak sorun değildir; bir kapıyı açmak veya bir alarmı devre dışı bırakmak açık bir teyit gerektirmelidir.
- Yalnızca yayınlamakla kalmayın, durumu bekleyin. Başarıyı ancak cihaz durumu komut kimliğini doğruladıktan sonra bildirin.
- Kaydı elle yazmak yerine otomatik oluşturun. Yeni cihazların otomatik olarak görünmesi için kaydı retained cihaz duyuru topic'lerinden oluşturun.
- Her şeyi kaydedin. Sorun giderme için isteği, araç çağrılarını, doğrulama sonuçlarını ve onayları saklayın.
Aynı doğrulama disiplini endüstriyel ekipmanlara da ölçeklenir; IoT için LLM function calling ve IoT cihazları için LLM ajanları rehberlerimiz bunu şema kütüphaneleri ve insan onayıyla genişletiyor. Tüm yığını çevrimdışı çalıştırmak istiyorsanız bir gateway üzerinde Ollama'yı MQTT ile çalıştırma yazımıza bakın. Dil arayüzlerinin diğer yapay zeka teknikleri arasındaki yerini görmek için yapay zeka ve IoT rehberimizi okuyun.
Kontrol kanalını barındırılan bir MQTT broker üzerinde çalıştırmak; cihazlarınızın, dil servisinin ve dashboard'ların evde veya tesiste port açmadan TCP, TLS veya WebSocket üzerinden tek bir güvenli uç noktaya ulaşması anlamına gelir.
ConnectMQ ile Başlayın
ConnectMQ, prompt'ları test ederken komutları ve durum değişikliklerini canlı izleyebilmeniz için tarayıcı içi MQTT client'ı olan tam yönetilen bir broker sunar. Kredi kartı gerektirmeden ücretsiz Serverless broker'ınızı oluşturun ve insanların cihazları kendi sözcükleriyle kontrol etmesini sağlayın.
Sık Sorulan Sorular
Doğal dil ile cihaz kontrolü için büyük bir bulut modeline ihtiyacım var mı?
Hayır. Araç çağrısı destekleyen küçük yerel modeller, cihaz listesi kısa ve fonksiyonlar net olduğunda basit cihaz komutlarını iyi işler. Yanlış yaptıkları durumları koddaki doğrulama karşılar.
"Işığı aç" gibi belirsiz istekleri nasıl ele alırım?
Sistem prompt'unda modele, birden fazla cihaz eşleştiğinde netleştirici bir soru sormasını söyleyin. Servisiniz soruyu kullanıcıya gösterir ve yanıtı bir sonraki turda geri gönderir.
Metin yerine ses kullanabilir miyim?
Evet. Önce konuşmayı metne dönüştürün ve dökümü aynı akışa verin. Döküm hataları ek bir hata kaynağı oluşturduğundan, riskli işlemler için teyitleri koruyun.
Komutları neden bir set topic'ine yayınlayıp ayrı bir state topic'ini okuyoruz?
Komutları durumdan ayırmak, state topic'inin her zaman cihazın gerçekte ne yaptığını yansıtması anlamına gelir; retained durum da her client'a mevcut değeri anında verir.



