將語音指令轉化為安全操作:先用正規表達式,再用語言模型
Turning spoken commands into safe actions: regex first, the language model second
作者以正規表達式白名單先行、語言模型其後的兩階段流程,將語音指令轉化為受控的家居自動化操作。系統只把已驗證的意圖和整理後的 JSON 參數交予 GPT-4o-mini,並在執行前再以 shell 指令白名單檢查輸出;不合規指令會被拒絕。
Introduction
I built a tiny voice‑controlled home automation server for my own website. The goal was simple: speak a command, have the server execute a safe action (turn a light on, start a backup, etc.). The tricky part is making sure the spoken text can’t be turned into an arbitrary shell command. My solution is a two‑step filter – a strict regular‑expression whitelist first, then a language‑model (LLM) to fill in the details.
Why regex first?
Regular expressions give me a deterministic gate. Anything that doesn’t match a known pattern is rejected before it ever reaches the LLM. This prevents prompt injection attacks where a user tries to sneak a malicious instruction into the natural‑language prompt. In my experience the overhead of a few regex checks is negligible compared to the round‑trip to the OpenAI API.
Designing the command grammar
I started by listing the actions I actually want to expose:
light <room> on|offbackup nowstatus <service>
From that list I wrote a single regex that captures the intent and the arguments:
// commandRegex.js
export const commandRegex = /^(light)\s+(living|kitchen|bedroom)\s+(on|off)$|^(backup)\s+now$|^(status)\s+(nginx|postgres)$/i;
The pattern is deliberately simple – each branch matches one of the allowed commands. If a spoken phrase doesn’t match, the request is dropped with a 400 response.
Hooking up the speech recognizer
On the client side I use the Web Speech API because it works in Chrome without any extra libraries. The recognizer sends the raw transcript to my Node server via a POST request.
// public/voice.js
const btn = document.getElementById('talk');
const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
recognition.lang = 'en-US';
recognition.interimResults = false;
btn.onclick = () => recognition.start();
recognition.onresult = e => {
const transcript = e.results[0][0].transcript.trim();
fetch('/api/command', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: transcript})
})
.then(r => r.json())
.then(console.log);
};
The server receives {"text":"light kitchen on"} and runs it through the regex.
Using a language model for intent
Once the regex validates the shape, I still need to translate the captured groups into concrete parameters (e.g., which GPIO pin controls the kitchen light). I keep that mapping in a JSON file and ask the LLM to produce the final command string. The prompt is static and never includes user‑provided text directly.
// intentResolver.js
import {Configuration, OpenAIApi} from 'openai';
import {commandRegex} from './commandRegex.js';
import pinMap from './pinMap.json';
const openai = new OpenAIApi(new Configuration({apiKey: process.env.OPENAI_API_KEY}));
export async function resolveIntent(rawText) {
const match = rawText.match(commandRegex);
if (!match) throw new Error('Invalid command');
// Extract intent and args from the regex groups
const [_, intent, room, state, backup, statusService] = match;
// Build a deterministic prompt – no user text interpolation
const prompt = `You are a helper that converts structured intent data into a shell command.
Intent: ${intent || backup || statusService}
Parameters: ${JSON.stringify({room, state, service: statusService})}
Return only the command, nothing else.`;
const response = await openai.createChatCompletion({
model: 'gpt-4o-mini',
messages: [{role: 'user', content: prompt}],
temperature: 0,
});
const command = response.data.choices[0].message.content.trim();
return command;
}
Because the prompt never contains the raw transcript, the LLM cannot be tricked into echoing back malicious text. The only variable data it sees are the intent name and a sanitized JSON object.
Safety checks
Even after the LLM returns a command, I run a final whitelist of allowed shell snippets. This is a second line of defense in case the model hallucinates.
// safeCommands.js
export const safeCommands = [
/^gpio write \d+ (1|0)$/, // turn a pin on/off
/^rsync -avz .* backup$/, // run a backup
/^systemctl status (nginx|postgres)$/
];
export function isSafe(cmd) {
return safeCommands.some(rx => rx.test(cmd));
}
If isSafe returns false, the request is logged and rejected.
Putting it together
The Express route ties all pieces together:
// server.js
import express from 'express';
import {resolveIntent} from './intentResolver.js';
import {isSafe} from './safeCommands.js';
import {exec} from 'child_process';
const app = express();
app.use(express.json());
app.post('/api/command', async (req, res) => {
const {text} = req.body;
try {
const cmd = await resolveIntent(text);
if (!isSafe(cmd)) throw new Error('Command failed safety check');
exec(cmd, (err, stdout, stderr) => {
if (err) return res.status(500).json({error: err.message});
res.json({output: stdout || stderr});
});
} catch (e) {
console.error(e);
res.status(400).json({error: e.message});
}
});
app.listen(3000, () => console.log('Voice command server listening on :3000'));
All the heavy lifting is deterministic: regex → intent → LLM prompt → safety whitelist → exec.
Deployment considerations
I run this server on a Raspberry Pi at home, but the same code works on any Linux box. A few practical tips:
- Environment isolation – use a systemd service that runs the Node process as a non‑root user.
- Rate limiting – the Web Speech API can be noisy; a simple token bucket middleware prevents abuse.
- Logging – store the raw transcript, the resolved command, and the safety check result. This makes debugging prompt‑injection attempts easier.
- Model cost – because the prompt is tiny and temperature is 0, the cost per request is a few fractions of a cent.
Conclusion
By front‑loading a strict regex filter, I keep the attack surface small enough that the LLM can be used safely for natural‑language expansion. The pattern works for any limited command set: define a grammar, whitelist the resulting shell, and let the model fill in the boring details. I’ve been running this for months with no false positives, and the approach scales nicely when I add new devices – just extend the regex and the safe‑command list.
I build sites and automations for small businesses at dreamfuturestech.com.
來源:DEV Community · dev.to