Turning spoken commands into safe actions: regex first, the language model second

本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。

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.

我为自己的网站构建了一个微型语音控制家庭自动化服务器。目标很简单:说出一个指令,让服务器执行一个安全的操作(打开灯、开始备份等)。棘手的部分在于确保语音文本不会被转化为任意的 shell 命令。我的解决方案是一个两步过滤器——首先是严格的正则表达式白名单,然后由语言模型(LLM)来填充细节。为什么要先用正则表达式?正则表达式为我提供了一个确定性的门控。任何不匹配已知模式的内容在到达 LLM 之前就会被拒绝。这可以防止用户试图将恶意指令潜入自然语言提示词中的提示词注入攻击。根据我的经验,与 OpenAI API 的往返时间相比,几次正则表达式检查的开销微不足道。

Designing the command grammar

I started by listing the actions I actually want to expose: light on|off, backup now, status . 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.

设计命令语法

我首先列出了我真正想要公开的操作:light on|off、backup now、status 。根据该列表,我编写了一个单一的正则表达式来捕获意图和参数: // commandRegex.js export const commandRegex = /^(light)\s+(living|kitchen|bedroom)\s+(on|off)$|^(backup)\s+now$|^(status)\s+(nginx|postgres)$/i; 该模式被刻意设计得很简单——每个分支匹配一个允许的命令。如果语音短语不匹配,请求将被丢弃并返回 400 响应。

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. The server receives {"text":"light kitchen on"} and runs it through the regex.

连接语音识别器

在客户端,我使用 Web Speech API,因为它可以在 Chrome 中运行而无需任何额外的库。识别器通过 POST 请求将原始转录文本发送到我的 Node 服务器。服务器接收到 {"text":"light kitchen on"} 并通过正则表达式运行它。

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. 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.

使用语言模型处理意图

一旦正则表达式验证了格式,我仍然需要将捕获的组转换为具体的参数(例如,哪个 GPIO 引脚控制厨房的灯)。我将该映射保存在一个 JSON 文件中,并要求 LLM 生成最终的命令字符串。提示词是静态的,从不直接包含用户提供的文本。由于提示词中从不包含原始转录文本,LLM 无法被诱骗回显恶意文本。它看到的唯一可变数据是意图名称和一个经过清理的 JSON 对象。

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. If isSafe returns false, the request is logged and rejected.

安全检查

即使在 LLM 返回命令后,我还会运行一个允许的 shell 代码片段的最终白名单。这是防止模型产生幻觉的第二道防线。如果 isSafe 返回 false,请求将被记录并拒绝。

Putting it together

The Express route ties all pieces together. All the heavy lifting is deterministic: regex → intent → LLM prompt → safety whitelist → exec.

整合

Express 路由将所有部分整合在一起。所有的繁重工作都是确定性的:正则表达式 → 意图 → LLM 提示词 → 安全白名单 → 执行。

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.

部署注意事项

我在家里的树莓派上运行此服务器,但相同的代码可以在任何 Linux 机器上运行。几个实用建议:环境隔离——使用以非 root 用户身份运行 Node 进程的 systemd 服务。速率限制——Web Speech API 可能会产生噪音;简单的令牌桶中间件可以防止滥用。日志记录——存储原始转录、解析后的命令和安全检查结果。这使得调试提示词注入尝试变得更容易。模型成本——因为提示词很小且温度为 0,每次请求的成本仅为几分之一美分。

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.

结论

通过预先设置严格的正则表达式过滤器,我将攻击面保持在足够小的范围内,从而可以安全地将 LLM 用于自然语言扩展。这种模式适用于任何有限的命令集:定义语法,将生成的 shell 加入白名单,并让模型填充枯燥的细节。我已经运行了几个月,没有出现误报。