ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

DeepSeek智能菜单推荐:餐饮大模型API落地实战

2026/9/18 13:48:33 拓冰建站 浏览量
DeepSeek智能菜单推荐:餐饮大模型API落地实战 简介这是一份面向餐饮行业技术开发人员与AI应用实践者的落地型技术文档围绕DeepSeek驱动的智能菜单推荐系统展开共30页聚焦从需求分析到部署上线的完整链路。内容覆盖餐饮业现状与推荐需求剖析、DeepSeek技术原理及适用性说明以及系统分层架构、用户与菜品等模块划分和交互流程设计进而延伸到数据收集与预处理、模型选择与训练调参、接口设计与开发、服务器部署与性能优化最后以真实应用案例给出业务与技术指标的效果评估和总结展望可作为落地参考与项目实施思路。包内共1个PDF文件压缩包约2.17MB目录分十章、条理清晰文字与图表显示正常具备完整的电子版阅读体验。目前已有64人学习下载。对算法工程师、餐饮数字化从业者与相关专业学生而言可据此梳理推荐系统的工程环节与关键决策点。1. 从「菜单点不动」到 DeepSeek 推荐餐饮场景真正需要的是什么一家开了七八家门店的连锁快餐老板最常问的问题不是「AI 能不能帮我写文案」而是「为什么我的菜单越做越厚客单价反而涨不上去」。后厨有 120 道 SKU前厅点单率排前 20 的菜贡献了 70% 的营收剩下 100 道菜压在菜单上既占库存又拖慢出餐。这不是菜单设计问题是缺少一个能把「谁在点、几点点、点什么配什么」串起来的推荐链路。所谓 DeepSeek 驱动的智能菜单推荐系统本质是用 DeepSeek 的 APIdeepseek-chat这类对话补全模型把结构化的订单数据、菜单数据、时段数据转成自然语言的推荐理由再叠一层业务规则做兜底。它解决三件事给顾客一份千人千面的推荐排序、给店长一份可解释的搭配逻辑、给运营一份能被 A/B 验证的调参入口。适合谁看手里已经有 POS 或扫码点餐系统的餐饮 IT、做餐饮 SaaS 的研发、以及想在门店里落地一个真实大模型应用的工程师。下面从数据准备一路讲到接口实现、提示词调参和上线后的排错代码可以抄参数可以改。2. DeepSeek 智能菜单推荐系统的数据层与推荐链路设计推荐系统做得烂八成不是模型问题是数据没整理干净。DeepSeek 这类大模型对输入噪声非常敏感你给它一份字段混乱的订单表它会给你一段看似合理、实则胡说八道的推荐理由。所以先把数据层立住再谈接口。2.1 菜单表、订单表、时段表的最小字段设计餐饮数据有个特点SKU 少但维度多。一份菜单条目至少要能表达口味标签、辣度、烹饪方式、出餐时长、成本、毛利率、库存状态、是否主推。订单明细要有下单时间戳、桌号、人数、菜品、数量、金额。时段表则是把营业时间切成早餐、午市、下午茶、晚市、夜宵每段有不同的推荐策略。常见做法是用三张宽表 一张标签关联表别一上来就上图数据库。下面是最小可用的 SQL 结构。-- 菜单主表每道菜一行标签用逗号分隔的字符串存方便喂给大模型 CREATE TABLE menu_item ( item_id VARCHAR(32) PRIMARY KEY, item_name VARCHAR(64) NOT NULL, price DECIMAL(8,2) NOT NULL, cost DECIMAL(8,2) NOT NULL, -- 用于算毛利率主推高毛利菜 taste_tags VARCHAR(128), -- 如 香辣,重口,下饭 spice_level TINYINT DEFAULT 0, -- 0-30 不辣 cook_minutes SMALLINT DEFAULT 10, -- 出餐时长午高峰硬约束 stock_status TINYINT DEFAULT 1, -- 1 有货 0 售罄 is_featured TINYINT DEFAULT 0 ); -- 订单明细推荐模型的训练与上下文来源 CREATE TABLE order_detail ( order_id BIGINT, item_id VARCHAR(32), qty INT, created_at DATETIME, -- 精确到分钟用来切时段 table_no VARCHAR(16), party_size TINYINT -- 就餐人数决定推荐份数 ) PARTITION BY RANGE (TO_DAYS(created_at));cook_minutes和stock_status是两个容易被忽略但极其关键的字段。午市高峰期如果推荐了出餐 25 分钟的菜顾客等餐体验直接崩再好的推荐理由都白搭。party_size则决定你是推单品还是推套餐组合2 人桌推「一荤一素一汤」6 人桌推「三荤两素」。2.2 推荐链路的四段式拆解召回 → 过滤 → 排序 → 生成理由不要把 DeepSeek 直接当排序模型用。正确的分工是本地数据库或轻量规则做召回和过滤DeepSeek 负责排序的语义增强和推荐理由的自然语言生成。四段式的职责划分阶段执行方输入输出召回SQL / 协同过滤用户历史、时段、人数候选 30~50 道菜过滤规则引擎库存、出餐时长、过敏原候选 15~20 道排序DeepSeek 业务权重候选集 用户画像Top 5 排序理由DeepSeekTop 5 门店语境每条一句推荐语召回阶段用一张简单 SQL 就能跑起来按「该顾客近 30 天点过的菜的同标签菜」「同时段同人数其他桌的高频菜」合并去重。-- 召回同标签热销 本人复购各取前 25 条共约 50 条候选 (SELECT m.item_id, m.item_name, m.price, m.taste_tags, COUNT(*) AS freq FROM order_detail o JOIN menu_item m ON o.item_id m.item_id WHERE o.created_at DATE_SUB(NOW(), INTERVAL 7 DAY) AND HOUR(o.created_at) BETWEEN 11 AND 13 GROUP BY m.item_id ORDER BY freq DESC LIMIT 25) UNION (SELECT m.item_id, m.item_name, m.price, m.taste_tags, COUNT(*) AS freq FROM order_detail o JOIN menu_item m ON o.item_id m.item_id WHERE o.table_no :table_no AND o.created_at DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY m.item_id ORDER BY freq DESC LIMIT 25);逻辑说明一句第一段是「同时段全店热销」做泛化召回第二段是「本桌历史复购」做个性召回两段 UNION 去重后进入过滤。BETWEEN 11 AND 13这个时段窗口要和后面时段表定义对齐不然午市和下午茶会串。参数上INTERVAL 7 DAY控制热度衰减旺季可以缩到 3 天淡季放到 14 天。过滤阶段千万不能省。库存为 0、出餐时长超过当前时段阈值、命中顾客过敏原的全部剔除。# 过滤硬约束一次过完别交给大模型判断 def hard_filter(candidates, ctx): ok [] for c in candidates: if c[stock_status] 0: continue if c[cook_minutes] ctx[max_cook_minutes]: # 午市设 15晚市放宽到 25 continue if set(c[allergens]) set(ctx[user_allergens]): continue ok.append(c) return ok[:20] # 控制在 20 条内提示词不要太长max_cook_minutes按时段配置午市 15 分钟晚市 25 分钟夜宵 30 分钟。这个参数是餐饮推荐里最容易被产品经理拍脑袋定的建议直接拉出餐监控的真实 P90 出餐时长来设比拍数字靠谱。过滤完控制在 20 条以内是因为候选集越大模型注意力越分散推荐质量反而下降。3. 用 DeepSeek API 跑通推荐排序与推荐理由生成链路设计完核心问题变成怎么调 DeepSeek 的接口让它稳定输出结构化结果而不是一段没法解析的小作文。答案是强制 JSON 输出 明确的字段约束。3.1 DeepSeek API 调用的最小可运行代码DeepSeek 的 API 走 OpenAI 兼容协议base_url 用https://api.deepseek.com模型填deepseek-chat。如果你习惯用 httpx 或 requests 直接发也行下面用官方 SDK 的写法能直接跑。import os, json from openai import OpenAI client OpenAI( api_keyos.environ[DEEPSEEK_API_KEY], base_urlhttps://api.deepseek.com # 兼容 OpenAI 协议SDK 直接复用 ) SYSTEM_PROMPT 你是餐饮门店的点餐推荐助手。根据候选菜品和顾客上下文 选出最合适的 5 道并按推荐优先级排序。只输出 JSON不要任何解释文字。 JSON 结构{picks:[{item_id:,reason:,score:0.0}]} reason 必须是一句不超过 30 字的中文推荐语结合口味、时段、人数。 def recommend(candidates, ctx): user_payload { 时段: ctx[period], 人数: ctx[party_size], 已点: ctx[ordered_names], 候选: [ {id: c[item_id], 名: c[item_name], 价: c[price], 标签: c[taste_tags], 辣度: c[spice_level]} for c in candidates ] } resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: json.dumps(user_payload, ensure_asciiFalse)} ], temperature0.3, # 推荐要稳定别给太高的随机性 response_format{type: json_object}, # 强制 JSON省去正则解析 max_tokens800 ) return json.loads(resp.choices[0].message.content)逻辑说明response_format{type: json_object}是让模型稳定吐 JSON 的关键少了这个参数你会收到带 markdown 代码块的文本。temperature0.3是推荐场景的甜点值太高会把相似候选的顺序随机化太低0在候选质量接近时容易反复推同一道菜。max_tokens800对 5 条推荐够用设太大反而增加无谓成本。参数调整有几个方向候选集变大时同步抬max_tokens如果发现输出 JSON 泄漏解释文字把 system prompt 里「只输出 JSON」加粗再说一遍如果模型老是忽略辣度可以在候选里把spice_level直接转成「微辣/中辣/特辣」的中文模型对中文标签更敏感。3.2 强制 JSON 输出与字段校验避免模型自由发挥即使加了response_format生产环境里也要做二次校验。模型可能返回不存在的item_id或者把score写成字符串。落地时用一层 pydantic 或手写校验兜住。VALID_IDS {c[item_id] for c in candidates} def validate(raw, candidates): picks raw.get(picks, []) cleaned [] for p in picks: if p.get(item_id) not in VALID_IDS: # 防幻觉菜名 continue try: score float(p.get(score, 0)) except (TypeError, ValueError): score 0.0 reason str(p.get(reason, )).strip()[:30] if not reason: continue cleaned.append({item_id: p[item_id], score: score, reason: reason}) return cleaned[:5]VALID_IDS做的是「幻觉拦截」大模型偶尔会编出一个菜单里没有的菜名如果直接透传给前厅顾客点不到就尴尬了。score强转 float 是防止模型把分数写成0.9分。最后cleaned[:5]保证前端最多只拿到 5 条防止模型多输出打乱 UI 布局。提示DeepSeek 的响应在高峰期可能超过 3 秒别在前端点单主流程里同步等待。把推荐结果缓存到 Redis键用table_no periodTTL 设 10 分钟顾客扫码时秒出客服加菜时再触发一次增量推荐。3.3 提示词里必须写清的三类上下文推荐质量差往往不是模型不行是提示词没给够上下文。我在实际项目里固定塞三类顾客上下文、门店上下文、约束上下文。顾客上下文的写法要具体「本桌 2 人已点 1 道香辣牛蛙顾客历史偏好重口、拒绝香菜」。门店上下文是让推荐落地的关键「本店位于写字楼商圈午市顾客赶时间主推出餐 10 分钟内的快手菜」。约束上下文则是给模型划红线「不要推荐超过 88 元的单品不要在同一桌重复推荐同一主料」。把这三类拼进 user message 的开头再跟候选列表效果比只丢候选好很多。可以按下面这个模板组织。【顾客】2人已点香辣牛蛙。偏好重口忌香菜。 【门店】写字楼商圈午市主推快手菜客单目标 65 元。 【约束】单价 ≤ 88 元不与已点菜同主料最多 5 道。 【候选】[{...}, {...}, ...]这套模板的好处是模型能同时做「语义排序」和「业务约束」两件事省掉你在代码里再写一堆 if-else。缺点是提示词变长token 成本上升所以候选集才要压到 20 条以内平衡成本和效果。4. 门店落地实战从接口到扫码点餐页的集成与调参接口能跑通不代表门店能用。餐饮的场景是「顾客扫码 → 3 秒内出推荐 → 点了菜 → 后厨收到单」任何一环卡住都会影响翻台率。这一章讲从接口到前端的集成细节以及上线后怎么调参。4.1 扫码点餐触发推荐的前后端时序时序上最忌讳在顾客打开菜单页时才去调 DeepSeek那 3 秒的等待足够让顾客放弃扫码。正确做法是「预生成 增量刷新」顾客落座扫码、点第一道菜这两个时点各触发一次其余时间读缓存。时点触发动作调用方式预期耗时扫码进页生成初始推荐异步预热写 Redis2~4s点第一道菜增量推荐扣掉同主料异步刷新1~3s会话内再次打开读缓存直接读 Redis50ms加菜按钮重新生成同步 loading2~4s前端拿到推荐后不要一次性铺满屏幕按「猜你喜欢」卡片形式放 3 条用户滑一下再加载剩余 2 条。这样即使推荐整体一般也不会压迫主菜单的浏览。// 扫码进页先渲染主菜单推荐位异步填充 async function loadMenuPage(tableNo) { renderFullMenu(await fetchMenu()); // 推荐走异步失败就隐藏推荐位不能阻塞主流程 try { const rec await fetch(/api/recommend?table${tableNo}, { timeout: 5000 }); const data await rec.json(); renderRecommendCards(data.picks.slice(0, 3)); } catch (e) { document.getElementById(rec-slot).style.display none; } }这段代码的核心是「推荐失败不能阻塞主菜单」。餐饮系统里主菜单渲染必须 1 秒内出来推荐是加分项不是必需项。timeout: 5000给足 DeepSeek 的响应时间超时就隐藏推荐位用户完全无感。4.2 冷启动新店没有历史订单时怎么推荐新开门店最尴尬没有订单数据召回阶段一片空白。这时候靠两层兜底。第一层是门店预设的主推菜单运营在后台手工标 10 道「招牌菜」系统冷启动时直接返回这 10 道第二层是品类通用搭配规则用「荤素汤」组合模板生成推荐比如「任选一荤 一素 一汤」。具体做法是把通用搭配规则也喂给 DeepSeek让它在这个框架里填菜名。COLD_START_RULES { 2人: 一荤一素一汤总价控制在 80-120 元, 4人: 两荤两素一汤总价 180-260 元, 6人: 三荤三素一汤加一份主食总价 300-450 元 } def cold_start_recommend(menu, party_size): rule COLD_START_RULES.get(party_size, COLD_START_RULES[2人]) featured [m for m in menu if m[is_featured] 1] # 把招牌菜 搭配规则交给模型组单 return recommend(featured, { period: 冷启动, party_size: party_size, ordered_names: [], combo_rule: rule })冷启动阶段temperature可以稍微调到 0.5让推荐组合更多样避免所有新店都推同样几道菜。等到一家店积累了两周订单就切回正常的召回链路。4.3 推荐结果埋点与 A/B 调参的落地参数推荐上线后一定要埋点否则你不知道推的东西顾客点没点。最少埋四个事件推荐曝光、推荐点击、推荐下单、推荐跳过。用这四个算 CTR 和推荐转化率作为调参依据。-- 推荐效果日报CTR 和转化率 SELECT DATE(event_time) AS dt, COUNT_IF(event impression) AS imp, COUNT_IF(event click) AS clk, COUNT_IF(event order) AS ord, ROUND(clk / NULLIF(imp, 0), 4) AS ctr, ROUND(ord / NULLIF(clk, 0), 4) AS cvr FROM rec_event WHERE event_time DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY dt ORDER BY dt DESC;调参时盯三个数CTR 低于 8% 说明推荐和顾客意图不匹配要回头改召回CVR 低于 30% 说明推荐理由没说服力要改提示词曝光到点击的衰减太快说明推荐位太靠下是 UI 问题不是算法问题。参数上temperature从 0.3 起步CTR 上不去就往 0.5 调CVR 上不去就往 0.2 调每次只动一个参数跑够一周再改。注意A/B 测试要按桌号哈希分流不要按顾客分流否则同一桌两个人看到不同推荐会打架。分流比例从 10% 灰度起步稳定后再扩大。5. 智能菜单推荐的进阶技巧成本控制、超长上下文与幻觉兜底前面几章把系统跑起来了这一章讲三个上线后必然会碰到的硬骨头也是决定这套系统能不能长期跑下去的关键。5.1 把 token 成本压到每单 0.1 元以内的三个手段DeepSeek 的定价相对友好但餐饮是薄利行业每单推荐成本必须算清楚。假设一次推荐输入 800 token、输出 300 token一天 500 单一个月下来也不是小数目。压成本有三个实招。第一是候选集瘦身。20 条候选改到 12 条输入 token 直接降四成实测推荐质量下降不到 5%。判断哪些菜可以砍把「近 7 天全店零点击」的菜直接从候选里去掉。第二是提示词复用。System prompt 固定不变不要每次把整段规则重发可以精简到 150 token 以内只保留「输出 JSON、字段结构、推荐语长度」这三条硬约束其余的细节约束挪到 user message 里按需拼。第三是缓存命中。同一个门店、同一个时段、同样是「2 人桌已点香辣牛蛙」的场景推荐结果高度相似直接命中缓存返回不重复调接口。缓存键设计成store_id:period:party_size:ordered_hash命中率通常能做到 40% 以上。优化手段输入 token 变化成本降幅质量影响候选集 20→12-40%约 35%5%System 精简-60%约 15%可忽略缓存命中 40%—约 40%无三个手段叠加单次推荐成本能压到原来的三成左右。按当前 DeepSeek 的价格量级每单推荐成本控制在 0.1 元以内是完全可行的。5.2 DeepSeek 达到对话长度上限时推荐会话怎么续接餐饮系统里有个真实痛点一个扫码会话如果反复加菜、反复推荐消息会越堆越长最后触发「达到对话长度上限请开启新对话」。这不是模型故障是会话管理没设计好。解决方案是「推荐接口无状态化」。每次推荐都把必要的上下文重新组织进 user message而不是把历史消息全带上。也就是说不要把推荐做成一个持续增长的对话而是每次调用都是独立的、自包含的请求。def build_messages(ctx, candidates): # 每次只带当前状态不带历史对话彻底避开长度上限 return [ {role: system, content: SYSTEM_PROMPT}, # 固定精简版 {role: user, content: json.dumps({ 时段: ctx[period], 人数: ctx[party_size], 已点: ctx[ordered_names], # 只传菜名列表不传完整历史 候选: slim(candidates, 12) }, ensure_asciiFalse)} ]关键在ordered_names只传一个菜名字符串数组而不是把之前的每轮推荐和回复都塞进去。这样无论顾客加几次菜请求长度都保持稳定不会随会话增长而增长。如果确实需要多轮记忆比如顾客说「不要太辣」把这条约束抽出来存成一个user_preference字段下次请求带上即可同样不累积历史。5.3 用规则兜底模型幻觉推荐菜名必须存在于菜单表上线后我遇到过一次事故模型推荐了一道三个月前就下架的菜顾客点单失败投诉到店长。这类幻觉不可能完全靠提示词消除必须用规则兜死。核心原则是「模型只做排序不做选品」——候选集由数据库出模型只能从候选里挑绝不允许它生成候选外的菜名。在第 3 章的validate函数里已经用VALID_IDS拦了一道但还要加两层。第一层是候选集本身必须带stock_status1售罄菜根本不进候选。第二层是推荐结果落库前再过一次菜单表校验防止缓存里的旧数据把下架菜带出来。def final_check(picks, store_id): live_ids query_live_items(store_id) # 实时查菜单表不读缓存 safe [p for p in picks if p[item_id] in live_ids] if len(safe) 3: # 兜底不足 3 条就补主推菜 safe fallback_featured(store_id, need3 - len(safe)) return safe[:5]query_live_items每次都实时查库不读缓存代价是一次轻量 SQL换来的是绝不会推出下架菜。fallback_featured是最后的安全网万一过滤完剩不到 3 条直接用门店主推菜补齐保证推荐位永远有内容不会开天窗。这套「模型排序 规则兜底」的组合才是餐饮这种容错率低的场景里大模型推荐能真正跑稳的前提。本文还有配套的精品资源点击获取