预算 160,AI 算成了 163.5

先说个挺离谱的事。
我想做个东西:帮一群人点麦当劳。
五个人,预算 160,一个不吃牛肉,一个不能吃辣,一个在控热量。
做完之后我发现,真正难的根本不是"吃什么",是协调。😮💨
你肯定经历过这个场面:
群里喊一句"点麦当劳了,报一下忌口",然后等。
有人说"我都行",有人三小时后才回,有人回"随便"——但你记得他上次不吃香菜。
你打开小程序翻菜单,算价格,发现超了,删掉一份薯条,再算一遍。
然后有人说:"等等,我其实不吃牛肉。"
十分钟过去了,群里还在问"到底谁不吃啥"。
这不就是该让 AI 干的活吗?
我把它交出去了。然后它翻车了。
一、AI 第一次算账,就错了
我把需求原封不动丢给模型,让它出方案。
方案出来了,看着还挺像样:谁吃哪个堡、配什么小食、总价多少。
我核对了一遍账:
- 它报的总价是 163.5 元
- 我的预算是 160
- 五个人里有个"不吃牛肉"的,它给人家排了一份巨无霸
我以为是个偶然,又问了一遍同样的问题。
这次价格变成了 155.8。
第三次,又是另一个数。
同一句话,问三遍,三个价格。而且我完全看不出 163.5 是怎么加出来的——它只给了结果,没给过程。
到这一步,很多人会得出一个结论:模型不够强,换个更好的。
但我后来想明白了,这个结论是错的。
二、不是它不会算,是这个任务不该它算
大模型擅长的是语言。而这件事的核心,是搜索 + 约束满足。
两个完全不同的活。
AI 硬干这件事,会翻在三个地方:
🔹 不可复现:同一输入,三次三个价格。因为它是在"生成"一个答案,不是在"计算"一个答案。
🔹 不可验证:163.5 错在哪?你没法指着一行代码说"这儿错了"。你只能整体重算,然后继续猜。
🔹 不可解释:它会告诉你"这款最受欢迎"。
但这不是解释。这叫形容词。🙃
真正的解释必须能回答两个问题:为什么是它?它牺牲了什么?
第三条最致命。因为用户不同意方案的时候,需要一个能下手的地方。如果理由是一句形容词,那他只能全盘推翻重来。
三、于是我做了个分工
我把活儿切开了,切得挺狠:
大模型:只听懂人话。谁不吃什么、预算多少、要不要辣。缺什么就问。最后把方案讲成人话。
代码:只算账。能不能满足、怎么组合最优、总价多少、有没有违规。
MCP 接口:只提供事实。门店有什么、多少钱、有没有货。
听起来很啰嗦,但它解决了一个特别实际的问题——出问题我能定位。
是接口给的价不对?还是我枚举漏了?还是展示时算错了?
三条路,分得清清楚楚。这套分法后来成了我所有 Agent 项目的默认指南,也是这次工程里最关键的一个经验分享:让该算的算,让该说的说。
后来我给自己定了一条死规矩:
页面上、对话里出现的每一个数字,都必须能追溯到代码的那一次输出。没有一处是模型算的。
四、第二版,我又翻车了
给用户 2-3 套方案,是这类产品的标配。
我第一版很聪明:加权打分。
价格占 40%,多样性占 40%,热量占 20%。跑三组权重,起了三个名字:省钱版、均衡版、健康版。
然后我自己点了一下切换。
三套方案几乎一模一样。
我把权重从 0.4 调到 0.5,结果没变。调到 0.7,才换了一个菜。
用户只要点两下就会发现:这是个障眼法。💀
这一瞬间,"可解释"这三个字就已经死了。
改法:不调权重了,换目标函数。
让每套方案有自己真正的优化目标,各自求最优:
🔸 最省钱:把总价压到最低(附带"至少 3 种主餐",防止五个人全吃一样)
🔸 最不重样:主餐种类尽量多
🔸 最低热量:整桌热量最小(低卡名额优先给有热量要求的人)
5 个人 160 块,实测出来是这样:
- 最省钱:117.5 元,3 种主餐,2480 大卡
- 最不重样:145.5 元,5 种主餐,2880 大卡
- 最低热量:127.5 元,3 种主餐,2190 大卡
差别一下子就"结构性"了:
便宜的方案有三个人吃同款;不重样的方案最贵;低卡方案比不重样少了 690 大卡。
用户选哪套,是在做真实的权衡,不是在看我摇骰子。
五、"可解释"到底长什么样
这是我觉得整个项目最值钱的部分。
「可解释」很容易被写成形容词堆砌。我给自己定的标准是:每条理由必须有数字,而且必须承认牺牲了什么。
反面例子(我第一版真这么写的):
这款性价比最高,符合您的需求。
正面例子(现在实际输出的):
避开牛肉,该店只筛出 8 项可选;比全场更低的脆鸡排蛋堡贵 6 块,用来凑够 3 种不同主餐,避免整桌一个味。
差别在哪?
📌 有数字:8 项、6 块、3 种。你可以自己核对。
📌 有对比:说的是"比 X 贵多少",不是孤立地夸自己。
📌 有取舍:直说"我多花了 6 块,是为了不让你五个人吃一样的东西"。
看到这句,你可以直接怼回来:"那别凑种类了,就吃便宜的。"
一个能被反驳的方案,才是可解释的方案。
"性价比最高"没法反驳,你只能接受或者关掉。
所以可解释性的价值,从来不是让 AI 显得有道理——是让你有地方下手。

六、一个我改了四次的 bug
这段是我这次遇到的最大挑战,也最真实。
求解器做完、测试全过,我开始做可视化页面。
为了体现"低卡方案不配甜点"这个决策,我加了条热量堆叠条:主餐一段、小食一段、甜点单独一段。
然后截图一看,我愣住了:
「最低热量」方案,热量比「最省钱」方案还高。
2760 大卡 vs 2480 大卡。
名字叫"最低热量",结果它最高。
顺着查下去,原因特别朴素:
低卡方案的共享小食,排序键用的是价格(挑最便宜的)。于是补位时,补进了一份 280 大卡的麦旋风。
主餐确实选了热量更低的,但一份小食把优势全吃回去了。
修复只改了一行:低卡方案的共享小食,按热量排序。
这个 bug 教会我一件事:
目标函数不只是用来选主餐的。挑小食、挑饮料、要不要甜点——每一个子决策都得服从同一个目标。
只要有一个子决策按别的口径走,整个方案的"最优"就是假的。
更坑的是:这个 bug 永远不会被自动化测试抓到。
它不超预算、不违反忌口、不超热量上限,所有硬约束都通过。
它只违反了一件事:方案的名字,和方案的实际效果不一致。
最后是靠人眼看截图发现的。所以我现在的习惯是:每个版本都必须真的把页面打开看一眼。
七、关于接口,一句话
这个项目最初的想法,其实是"自己写一个麦当劳下单接口"。
查完资料我就放弃了:麦当劳 2025 年 12 月就上线了官方 MCP 服务,33 个工具,免费,限流 600 次/分钟。
自己再造一个,等于给官方服务套层壳。
所以我改去啃"用哪些、按什么顺序用"。
33 个工具里,我只用了 15 个。剩下的(积分商城、抽奖、活动预约)明确不用。
理由很简单:
工具面越宽,AI 乱调用的概率越高。
33 个全接一遍不是本事,知道哪些不该接才是。 😌
八、一个特别反直觉的发现
测试的时候我点了几十种开关组合,发现一个有意思的事。
预算 160 的时候,打开"陈昊也不吃牛肉"这个开关——结果完全不变。因为他本来就没吃到牛肉。
但预算调到 200,同一个开关会让方案整体重排。
这说明什么?
改一个人,不等于换一份菜。
需求一变,腾出来的预算会改变其他所有人的最优解。所以任何一条约束变化,都必须整体重算。
你要是手动改一份,大概率踩预算,或者让另一个人失去最优选择。
这是多人协调最反直觉的地方,也是我做这个项目最想证明的一件事。
最后
这类 AI 产品的价值,从来不在"能聊"。
在敢交付。
用户说"就这个,下单吧",意味着他信了这个方案的每一个数字。
这份信任不能建立在"AI 看起来很聪明"上,只能建立在"每一步都能被验证"上。
所以我做的事情很朴素:
把不确定的部分(听懂、表达)交给 AI,把确定的部分(计算、校验)全部交给代码。
AI 负责听懂"有个不吃牛的"。
代码负责保证,那个人面前不会出现巨无霸。
你们公司点外卖,谁负责统计忌口?
评论区说说,我猜大概率是同一个人,每次都是他。🙋
项目已开源,搜索 "FoodPilot 麦当劳 MCP" 可找到 本项目为参赛作品,非麦当劳官方产品
#AI #大模型 #MCP #麦当劳 #WorkBuddy #工程实践 #Agent开发 #约束求解 #技术复盘 #独立开发