DeepSeek智能菜单推荐:餐饮大模型API落地实战
2026/9/18 13:48:27 网站建设 项目流程

简介:这是一份面向餐饮行业技术开发人员与AI应用实践者的落地型技术文档,围绕DeepSeek驱动的智能菜单推荐系统展开,共30页,聚焦从需求分析到部署上线的完整链路。内容覆盖餐饮业现状与推荐需求剖析、DeepSeek技术原理及适用性说明,以及系统分层架构、用户与菜品等模块划分和交互流程设计;进而延伸到数据收集与预处理、模型选择与训练调参、接口设计与开发、服务器部署与性能优化,最后以真实应用案例给出业务与技术指标的效果评估和总结展望,可作为落地参考与项目实施思路。包内共1个PDF文件,压缩包约2.17MB,目录分十章、条理清晰,文字与图表显示正常,具备完整的电子版阅读体验。目前已有64人学习下载。对算法工程师、餐饮数字化从业者与相关专业学生而言,可据此梳理推荐系统的工程环节与关键决策点。

1. 从「菜单点不动」到 DeepSeek 推荐:餐饮场景真正需要的是什么

一家开了七八家门店的连锁快餐,老板最常问的问题不是「AI 能不能帮我写文案」,而是「为什么我的菜单越做越厚,客单价反而涨不上去」。后厨有 120 道 SKU,前厅点单率排前 20 的菜贡献了 70% 的营收,剩下 100 道菜压在菜单上,既占库存又拖慢出餐。这不是菜单设计问题,是缺少一个能把「谁在点、几点点、点什么配什么」串起来的推荐链路。

所谓 DeepSeek 驱动的智能菜单推荐系统,本质是用 DeepSeek 的 API(deepseek-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-3,0 不辣 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_minutesstock_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_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://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( model="deepseek-chat", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": json.dumps(user_payload, ensure_ascii=False)} ], temperature=0.3, # 推荐要稳定,别给太高的随机性 response_format={"type": "json_object"}, # 强制 JSON,省去正则解析 max_tokens=800 ) return json.loads(resp.choices[0].message.content)

逻辑说明:response_format={"type": "json_object"}是让模型稳定吐 JSON 的关键,少了这个参数你会收到带 markdown 代码块的文本。temperature=0.3是推荐场景的甜点值,太高会把相似候选的顺序随机化,太低(0)在候选质量接近时容易反复推同一道菜。max_tokens=800对 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 + period,TTL 设 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
会话内再次打开读缓存直接读 Redis<50ms
加菜按钮重新生成同步 + 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_ascii=False)} ]

关键在ordered_names只传一个菜名字符串数组,而不是把之前的每轮推荐和回复都塞进去。这样无论顾客加几次菜,请求长度都保持稳定,不会随会话增长而增长。如果确实需要多轮记忆(比如顾客说「不要太辣」),把这条约束抽出来存成一个user_preference字段,下次请求带上即可,同样不累积历史。

5.3 用规则兜底模型幻觉:推荐菜名必须存在于菜单表

上线后我遇到过一次事故:模型推荐了一道三个月前就下架的菜,顾客点单失败,投诉到店长。这类幻觉不可能完全靠提示词消除,必须用规则兜死。核心原则是「模型只做排序,不做选品」——候选集由数据库出,模型只能从候选里挑,绝不允许它生成候选外的菜名。

在第 3 章的validate函数里已经用VALID_IDS拦了一道,但还要加两层。第一层是候选集本身必须带stock_status=1,售罄菜根本不进候选。第二层是推荐结果落库前再过一次菜单表校验,防止缓存里的旧数据把下架菜带出来。

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, need=3 - len(safe)) return safe[:5]

query_live_items每次都实时查库,不读缓存,代价是一次轻量 SQL,换来的是绝不会推出下架菜。fallback_featured是最后的安全网:万一过滤完剩不到 3 条,直接用门店主推菜补齐,保证推荐位永远有内容,不会开天窗。这套「模型排序 + 规则兜底」的组合,才是餐饮这种容错率低的场景里,大模型推荐能真正跑稳的前提。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询