让模型把预算拆成几项,再顺便算一个总价,几乎是创作助手的标配。可“解释得顺”与“算得对”是两个目标。对一款轻量产品,我会让模型提出计算步骤,把最终算术交给一个拒绝模糊输入的小计算器。
工具边界越小,越容易检查
10 月 22 日发布的 OpenAI Decisions API面向有边界的决策任务。这是一个产品方向的信号:不是所有工作都要由一个自由对话吞掉。我的计算器没有接入该 API,只借这个问题重新划分职责——模型可以解释,工具必须按契约计算。
示例采用逆波兰表达式,2 3 +表示 2 加 3。只有数字和+ - * /四个操作符,不调用eval,不执行模型返回的任意 JavaScript。每次操作前检查栈长度,结束时栈里必须恰好剩一个值;除零、非有限结果、未知符号和过长表达式都报错。
正确的失败,比漂亮的猜测有用
“材料费约两百”不是数字,“10%”也不是这里支持的词元。工具不能随便把它们转换为 200 和 0.1。界面应把错误原样呈现为“需要补充输入”,让用户或模型重新组织算式,不返回一个看似完成的结果。
二进制浮点仍有边界,0.1+0.2不能承诺精确到金融分账。例子适合教学与粗略预算;真要处理货币,应该明确币种、小数位和舍入规则,通常改用整数最小货币单位,另测上溢。公式合法也不能证明上游价格或税率真实。
运行node rpn.js,本地验证普通计算、栈残留、除零、非法代码和数值溢出。没有调用模型、外部价格服务或真实支付。这个小栈不是完整数学系统,却能把“模型可能说错的最后一步”变成可观察、可拒绝的计算。
可独立运行的核心例子
function rpn(text) { const tokens=text.trim().split(/\s+/); if(!text.trim()||tokens.length>100) throw Error('词元需1..100个'); const stack=[]; for(const t of tokens) { if(/^-?(?:\d+\.?\d*|\.\d+)$/.test(t)) { const n=Number(t);if(!Number.isFinite(n))throw Error('非有限输入'); stack.push(n);continue; } if(!['+','-','*','/'].includes(t)||stack.length<2)throw Error('非法词元或操作数不足'); const b=stack.pop(),a=stack.pop(); if(t==='/'&&b===0)throw Error('除零'); const n=t==='+'?a+b:t==='-'?a-b:t==='*'?a*b:a/b; if(!Number.isFinite(n))throw Error('结果非有限'); stack.push(n); } if(stack.length!==1)throw Error('栈中有未使用的数'); return stack[0]; } if(rpn('2 3 + 4 *')!==20||rpn('7 2 /')!==3.5)throw Error('计算失败'); for(const bad of ['2 3','1 0 /','2 +','process.exit()','9'.repeat(400)]) { let rejected=false;try{rpn(bad);}catch{rejected=true;} if(!rejected)throw Error('应拒绝:'+bad); } console.log('结果',rpn('120 35 + 3 /'));