作者在GroundCheck应用中集成语音填写功能,通过Supabase Edge Function调用Claude将口述内容映射到表单字段,严格限定作用域避免了过度工程化。
我刚为 GroundCheck 添加了语音填写表单的功能,而最有用的决定其实很无聊:我没有构建语音助手。
这个功能的范围非常局限。检查员已经在某个检查表单内了。他们点击"语音填写",然后口述类似这样的内容:
all four guys had hard hats and vests on, scaffold tags were current, one trip hazard by the east gate, no incidents
应用将这份转录文本加上可见的表单架构发送给 Supabase Edge Function。Claude 将口述内容映射到匹配的字段上。应用显示一张审核表。检查员接受或拒绝每个建议的值。
就这样。没有聊天界面。没有跨报告的记忆。没有"全天的 AI 助手"。只有语音笔记 → 结构化表单字段。
客户端的规则故意做得很小:
const FILLABLE_TYPES = new Set(["text", "checkbox", "multiple_choice", "rating"]);
export function fillableQuestions(section, responses) {
return section.questions.filter(
(q) =>
FILLABLE_TYPES.has(q.type) &&
(!q.show_when || evaluateCondition(q.show_when, responses))
);
}
照片和签名被排除在外,因为口述无法产生它们。因 show_when 条件而隐藏的字段也会被排除。这在整张表单填写时很重要:如果用户说背心检查通过了,Claude 根本不应该看到"为什么 hi-vis 失败?"这个后续字段。
Edge Function 根据范围内的字段动态构建 JSON schema。选择字段变成 enums。评分字段根据分数映射变成 enums。未知字段和非法值在模型返回后再次在服务器端被丢弃,因为这些数据最终会进入安全合规记录,"架构大概处理了"是不够的。
我还让 API key 完全保持在服务器端。移动应用从不接触 Anthropic key。函数在发起付费调用之前检查认证、组织层级和每个组织的每月限额,然后记录带有输入/输出 token 数量的使用行。
巧妙之处在于:语音捕获使用的是 iOS 键盘上的听写按钮,放在一个自动对焦的多行文本字段里。不需要语音识别依赖,不需要 native 模块,不需要新的权限字符串,不需要为了测试第一个版本而重新构建。如果现场测试证明人们需要一个大的应用内按住说话按钮,以后可以替换转录框。下游的契约仍然只是文本。
审核步骤是不可协商的。Claude 提议;人类应用。已经回答过的字段显示为变更而不是静默覆盖。相同的值被丢弃。所有内容都按表单顺序显示,所以感觉像是在审核检查,而不是在调试 JSON。
我最喜欢的部分是整个功能更小了,因为它拒绝成为一个通用助手。它只知道屏幕上的表单,只看到当前可以回答的字段,只在人类说 yes 之后才写入。
这种形态通常是 LLM 功能变得有用的地方:狭窄的输入、受约束的输出、无聊的关卡、人在边界的审核。