从自然语言到物理配方:AI汽水机的软硬结合实践

从自然语言到物理配方:AI汽水机的软硬结合实践 1. 从一句来杯初恋的味道到一杯真汽水我花了大半年时间折腾出来一台能听懂人话的 AI 汽水机你对着它说一句来杯初恋的味道它在两秒内把它翻译成糖浆配比、甜度、气泡强度、温度四组参数然后泵开始工作八秒后一杯冒着细泡的汽水就递到你面前。它最大的价值不在于会做汽水而在于打通了自然语言到物理配方这一整条链路——把大模型的抽象理解能力落到计量泵的毫秒级脉冲上。这篇文章写给三类人想动手做硬件AI 融合项目但不清楚从哪下手的爱好者、做 AI 应用开发想找落地场景的工程师、以及单纯好奇50 万种口味这个数字怎么算出来的朋友。我从架构、硬件、软件、翻车复盘四条线把它拆开讲参数、代码、坑点都摆在明面上。1.1 这个项目真正难的地方在哪很多人第一反应是接个大模型不就行了但真上手就会发现模型能给你一段漂亮的描述却给不了泵该转几秒。中间隔着一层语义到物理量的映射这层怎么设计决定了整台机器的成败。我一开始也天真直接把用户的话丢给模型让它输出3 毫升柠檬糖浆结果它一会儿输出 3 毫升、一会儿输出 30 毫升单位都飘。第二个难点是约束。你可以让模型自由发挥说加点烟熏味但糖浆架上只有十六瓶真实存在的液体。模型必须被限制在这十六瓶的组合里选不能凭空发明一种原料。这就需要用结构化输出加枚举约束把它的想象力框在物理世界内。第三个难点是可控的重复性。同一句话说两遍出来的味道应该基本一致否则用户会觉得这机器神经病。这就要求我们在模型之外再做一层缓存和锚定把常见表达固定成稳定的味觉向量而不是每次都依赖模型随机采样。这三点想清楚了剩下的都是工程量。1.2 大家最关心的50 万种从哪来这个数字不是拍脑袋是实打实的组合数只不过口径要说明白——它是配方组合的数学上限不是每种都调过且都好喝。我先把可调维度列出来十六种基础糖浆每杯从中取 1 到 4 种混搭甜度 5 档气泡强度 6 档出杯温度 4 档是否加酸 2 档冰量 3 档。算一下风味组合本身取 1 种C(16,1) 16取 2 种C(16,2) 120取 3 种C(16,3) 560取 4 种C(16,4) 1820风味组合合计 2516 种。再乘上其他维度2516 × 5甜度× 6气泡× 4温度× 2酸 603,840。所以50 万其实是保守说法理论上限超过 60 万。但这里有个诚实的提醒大部分组合是难喝的真正好喝的映射集中在一个很小的子空间里这也正是需要 AI 做翻译而不是让用户手动调参的原因。可调维度档位数说明糖浆基底组合251616 选 1~4带各自占比权重甜度50%、25%、50%、75%、100% 糖浆基准量气泡强度6对应碳酸水压力与注入比例出杯温度4常温、微凉、冷、冰爽加酸2是否补柠檬酸平衡冰量3无冰、半冰、满冰1.3 为什么值得做一个实体 AI 项目纯软件的 AI 应用现在满地都是但把 AI 的结果落到一个能摸到、能喝到的东西上体验完全不一样。它逼着你处理延迟、容错、机械误差、卫生这些软件世界里不存在的问题。做完这一台我对AI 落地这四个字的理解比以前深得多——模型只是整条链路里最聪明、但也是最不可靠的一环真正决定体验的是它周围的工程。这也是我建议每个想做 AI 应用的人都至少动手做一次软硬结合项目的原因。2. 系统架构与口味数据模型设计整台机器我按感知—决策—执行三层来分层与层之间用明确的接口隔开。这样做的最大好处是任何一层出问题都能单独替换比如今天语音识别换成另一个引擎下游代码一行不用改明天换个大模型配比逻辑照样跑。定接口这件事我在项目早期偷懒省略过结果第三周返工了整整两天教训很硬。2.1 三层架构怎么切感知层负责把用户的语音转成文字。它对外只吐出一个字符串加一个置信度别的一概不管。决策层是核心负责把文字转成结构化的配方对象内部又分两步先把抽象表达映射到味觉维度再把味觉维度量化成具体配比。执行层拿到配方就干活把每种液体的体积换算成泵的运转时长按顺序出料、混合、出杯。三层之间只传数据不传状态这样调试的时候可以逐层打印中间结果定位问题极快。提示接口定义尽量用简单的 JSON 结构别一开始就上复杂的消息队列单机项目用不上反而增加调试成本。2.2 配方数据结构怎么定配方是整个系统的通用语言它的字段设计直接决定了后面好不好扩展。我最终定成下面这样包含原料清单、每种的体积、以及目标温度、气泡等级这些环境参数。体积用毫升温度用摄氏度气泡用 0~5 的整数等级全部是无量纲或标准单位。为什么强调这一点因为早期我用过少一点/多一点这种模糊描述在内部传递结果下位机根本没法处理被迫全部重写。{ name: 凌晨三点的柑橘, ingredients: [ {syrup_id: citrus_01, volume_ml: 18.0}, {syrup_id: tonic_07, volume_ml: 6.0}, {syrup_id: herb_03, volume_ml: 3.5} ], sweetness: 2, carbonation: 4, temperature_c: 4, acid_boost: true, ice_level: 2, soda_water_ml: 180.0 }这里有个设计取舍值得说我为什么把糖浆体积和碳酸水体积分开。因为糖浆是计量泵精确控制的碳酸水是定容注水两者误差来源完全不同。分开之后标定和排查都能对号入座不会互相甩锅。2.3 抽象表达怎么变成味觉维度这是整个项目最有意思的部分。用户说初恋的味道加班到凌晨三点的感觉下雨天配一首老歌这些话没有标准答案。我的做法是引入一个中间层六维味觉坐标——甜、酸、苦、气泡感、温度感、香气指向。模型的任务不是直接给配方而是先给这六个维度的打分再由确定性的代码映射成配方。这样做的理由是降维打击。让模型做读空气、猜情绪、翻译成味觉这件事它很擅长让它算毫升它会翻车。把两件事拆开各用最合适的工具做。香气指向我用了五个方向果香、花香、草本、奶香、焦香烟熏/烘焙。用一个长度 5 的权重向量表示。下雨天的感觉我映射成低甜、中酸、微弱苦、中气泡、低温、草本和花香各占一半。这个映射不是唯一解但它是稳定解说一百次结果都一样。2.4 锚定缓存让结果不飘纯靠模型每次现推同一个表达可能给出不同的味觉坐标这就是飘的来源。我在决策层加了一张锚定表把高频表达固定成写死的味觉坐标命中就走缓存不命中才叫模型。表里现在有 300 多条覆盖了大部分常见的形容词和场景词。实测下来命中率大概七成剩下三成走模型整体响应时间从 1.8 秒压到 0.6 秒左右。这招在成本和稳定性上都划算。处理方式平均延迟结果一致性适用场景锚定表命中约 0.1s完全一致常见表达、热门口味走大模型约 1.5s基本一致新奇表达、长句描述兜底默认约 0.05s一致解析失败、置信度过低3. 硬件实操泵、阀与管路的选型逻辑硬件这部分我踩的坑比软件还多。核心就三件事量要准、路要分开、洗得干净。量准靠计量泵和标定路分开是为了防止糖浆串味和碳酸水回流洗干净则是食品级设备的基本要求做不到这点机器做出来自己都不敢喝。3.1 计量泵怎么选蠕动泵 vs 电磁泵我最终选了蠕动泵理由有三。第一糖浆比较粘蠕动泵靠滚轮挤压软管输送不接触液体本身只换软管就干净卫生上省心。第二精度够用配合标定能做到 ±0.5 毫升的重复精度做汽水完全够了。第三不同糖浆之间只要换管就不串味。电磁泵便宜、流量大但精度差、清洗难、容易堵我试过一轮就放弃了。泵的流量别信标称值一定要自己标。我买的那批标称 12 毫升/秒实测在 12V 下只有 8.6 毫升/秒而且每台之间差异能到 15%。这个差异不管配比直接跑偏。3.2 碳酸水和糖浆必须是两条独立回路这是安全问题也是味道问题。碳酸水那一路我用了二氧化碳气瓶加调压阀配合一个带泄压阀的小型耐压罐做碳化。糖浆回路绝对不能和碳酸水回路混用一个泵或一根管因为碳酸水有压力一旦停泵会往回顶把糖浆污染甚至把管路顶爆。两条路分开走最后在出杯口用分流嘴汇合混合才发生。这样每一路都能独立控压、独立清洗。注意带压力的气体和液体系统务必装泄压阀管路承压能力要有余量接头定期检查。任何承压部件都不要私自改装到超出其标称范围。3.3 主控怎么分工我用树莓派 ESP32双主控。树莓派负责语音识别、跑大模型客户端、管业务逻辑因为它算力够、能跑 Linux。ESP32 只干一件事接收体积指令精确控制泵的启停时序。为什么不用树莓派直接控制泵因为 Linux 不是实时系统做毫秒级定时会抖实测误差能到几十毫秒对于 1.5 秒的脉冲来说误差就有 2%累积起来味道会偏。ESP32 用硬件定时器做这活误差在微秒级。两者之间用 USB 串口通信115200 波特率协议是简单的 ASCII 行协议好抓包、好调试。3.4 泵的流量标定用称重法最靠谱标定别用眼睛看刻度用电子秤。步骤是这样的先让泵空转把管内空气排净然后在出液口放一个空杯去皮让它按固定时长运行比如 3 秒称重。糖浆密度按 1.2 克/毫升换算体积。重复三次取平均算出这台泵的实际毫升/秒。把这个值写进配置文件配比换算时按各自泵的系数算。我建议每两三个月重新标一次因为软管会老化流量会慢慢衰减。泵编号对应糖浆实测流量(ml/s)标定日期P1柑橘8.62第 31 周P2草本8.05第 31 周P3焦香9.14第 31 周P4奶香8.41第 31 周4. 软件实操从语音到泵脉冲的完整链路软件这条链子有三段语音转文字、文字转配方、配方转脉冲。每段都有它的脾气我把实现和踩过的坑都摆出来。4.1 语音识别本地跑省心还是云端省事我两种都试过。云端识别效果更稳尤其在有环境噪声的场合但要联网、有延迟、有请求量限制。本地识别我用的是一个开源语音模型量化后在树莓派上跑识别一句话大概 0.8 秒离线可用隐私也好。我的取舍是本地为主、云端兜底本地识别置信度低于阈值时才走云端。这样日常用起来快遇到复杂语句也不至于彻底歇菜。def transcribe(audio_path): text, conf local_asr(audio_path) if conf 0.6: text cloud_asr(audio_path) return text提示语音识别对短句和方言口音很敏感。我在提示用户时有意引导说完整短句比如来一杯偏酸、带点草本味的气泡水比单说酸的识别率和理解准确率都高一截。4.2 提示词怎么写才不翻车提示词的核心思路是给定枚举 强制结构 明确边界。我先告诉模型有哪些糖浆可选、甜度分几档、气泡分几档然后要求它只输出 JSON且字段值必须在给定范围内。为了让它别乱来我在提示词里明确写了只输出 JSON不要解释不要 markdown 代码块。实测加不加这句解析成功率差很多。你是一台汽水机的配方引擎。可选糖浆有 citrus_01(柑橘), tonic_07(汤力), herb_03(草本), floral_02(花香), smoky_04(焦香), creamy_05(奶香) 等共16种。 用户描述{user_text} 请输出 JSON字段如下 - ingredients: 数组1~4 项每项含 syrup_id 和 volume_ml(0~25) - sweetness: 0~4 整数 - carbonation: 0~5 整数 - temperature_c: 从 [22, 12, 6, 3] 中选 - acid_boost: true/false - reason: 一句话说明不超过20字 只输出 JSON。这里有个经验让模型也输出一句 reason不是为了展示是为了排查。当某个口味离谱时看它给的理由就知道是哪里理解偏了调试效率高不少。4.3 配比换算和限幅必须有的安全网模型输出再约束也可能越界所以代码层必须做硬限幅。我定了三条规则单种糖浆体积不超过 25 毫升总糖浆体积不超过 60 毫升甜度、气泡等级超范围直接夹到边界值。同时做一个互斥检查比如焦香和奶香同时高占比会很难喝就自动压低次要项的占比。这层限幅不依赖模型纯确定性代码跑一万次结果都稳。def clamp_recipe(r): for ing in r[ingredients]: ing[volume_ml] max(0.0, min(25.0, ing[volume_ml])) total sum(i[volume_ml] for i in r[ingredients]) if total 60.0: k 60.0 / total for i in r[ingredients]: i[volume_ml] * k r[sweetness] max(0, min(4, r[sweetness])) r[carbonation] max(0, min(5, r[carbonation])) return r4.4 配方转脉冲一行公式背后的讲究体积转时长看着简单时长 体积 / 泵流量。但有几个细节。泵启动有加速段停止有惯性段真实出液量会比流量×时长略少我在标定时把这部分折算进流量系数里了。多种糖浆要按顺序出料不能同时开泵否则管路压力互相干扰。出料顺序按体积从小到大先出少的减少残留误差。最后是碳酸水注入它在糖浆之后用来冲刷管路并把糖浆带进杯子等于顺手做了次小清洗。def recipe_to_pulses(recipe, pump_map, water_pump): pulses [] items sorted(recipe[ingredients], keylambda x: x[volume_ml]) for ing in items: flow pump_map[ing[syrup_id]][flow_ml_s] dur ing[volume_ml] / flow pulses.append((pump_map[ing[syrup_id]][port], dur)) soda_dur recipe[soda_water_ml] / water_pump[flow_ml_s] pulses.append((water_pump[port], soda_dur)) return pulses5. 联调、翻车复盘与排查速查表联调阶段我大概有一周时间天天在喝奇怪的液体。下面挑几个最典型的翻车案例复盘都是花钱买来的经验。5.1 三个经典翻车案例案例一串味。做完一杯焦香口味下一杯柑橘里带烟熏味。排查发现是公共出液口的残液没冲干净。解决办法是每杯结束加一段清水冲洗脉冲把共用段管路冲一遍成本是每杯多十几毫升水但串味彻底消失。案例二气泡不足。用户反映气泡感 5 档还是像糖水。查下来是碳酸水温度太高气体溶解度低。溶解度和温度强相关水越冷溶得越多。我把碳化罐温度压到 2~4 摄氏度后气泡感明显上来了。所以气泡档位不只是压力问题温度是隐藏变量。案例三同一句话两种味道。用户连说两遍清爽的一杯偏甜一杯偏酸。原因是这句没进锚定表走了模型随机采样。加进锚定表固定坐标后问题解决。这也印证了前面说的模型负责新奇表达常见表达必须锚定。5.2 常见问题速查表现象可能原因排查方向出液量偏少软管老化、气泡、泵衰减重新标定流量检查管路密封出液量忽多忽少泵管磨损、接头漏气换管检查所有接头味道每次不同未命中锚定表走模型随机把表达加入锚定表气泡感弱碳酸水温度高、压力低降低水温检查调压阀串味公共管路残液增加清水冲洗脉冲语音识别错环境噪声、口音引导说完整短句提高信噪比响应慢走云端识别 走大模型启用本地识别扩充锚定表5.3 卫生维护这活儿不能偷懒食品级设备卫生是底线。糖浆含糖管路里是微生物的温床。我的维护节奏是每 48 小时用温水冲洗整个糖浆回路每次换糖浆瓶时单独冲洗对应管路每两周做一次食品级清洗剂循环。出杯嘴每天擦。所有接触液体的部件都要是食品级材料硅胶管定期更换一般一到两个月换一次。别嫌麻烦机器是给自己和家人用的这步省不得。注意清洗后要用清水充分冲洗避免清洗剂残留。碳酸水回路和糖浆回路分开清洗别混。6. 做完这一台之后我真正学到的东西如果说这个项目教会我一件事那就是AI 不是万能的解题器它是链路里最灵活、也最需要被约束的一环。让模型干它擅长的——理解语言、翻译情绪、做模糊映射把精确计算、边界控制、硬件时序交给确定性代码。这条分工线划清楚项目就顺了划不清就会陷入为什么它这次又变了的无尽调试。还有一个体会是先跑通最丑的版本。我最早那台机器树莓派搁在纸盒上管子用胶带固定语音识别经常听错但它能出汽水。正是这个丑版本让我验证了整条链路可行后面才有动力去优化泵精度、做锚定表、改管路。如果一开始就追求完美估计第三周就放弃了。最后分享一个我觉得有意思的扩展方向给这台机器加一个记录功能把每次点单的原话、模型解析结果、最终配方存下来攒够数据之后就能反过来分析人们描述味道的语言规律。这个数据集本身挺有价值也能用来做锚定表的自动扩充。我目前已经攒了两千多条等我跑一段时间再回来分享这批数据里发现的规律。