【大模型RAG生成式AI开发实战】《大模型RAG生成式AI开发实战》_230.[第23章 实战项目集] 项目9:多模态菜谱检索与推荐

当AI不仅能读懂你输入的文字,还能看懂你冰箱里那半根胡萝卜和两颗鸡蛋——多模态RAG菜谱推荐系统,正在让大模型从“纸上谈兵”的问答机器,进化成真正懂你口味、会看食材的“赛博厨师”。本文将手把手拆解项目9的完整实战链路,从多模态数据治理、统一向量空间构建,到混合检索、大模型生成与重排序优化,带你避开新手最容易踩的六个深坑,打通从Demo到工程落地的“最后一公里”。
文字目录
一、数据治理与架构设计:你的“地基”决定了楼能盖多高
二、多模态Embedding对齐:让文字和图片说同一种语言
三、RAG检索链路搭建:从“大海捞针”到“精准定位”
四、大模型生成与个性化推荐:检索是备菜,生成是烹饪
五、融合与重排优化:召回靠广度,排序靠精度
六、工程落地与性能调优:从Jupyter Notebook到线上服务
嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《大模型RAG生成式AI开发实战》230.[第23章 实战项目集] 项目9:多模态菜谱检索与推荐
俗话说“巧妇难为无米之炊”,但咱们程序员更难的是:米有了,锅也有了,就是不知道这顿饭该怎么做出花来。你是不是也这样?学了大模型、学了RAG、学了向量检索,感觉自己武装到了牙齿,可一到实战项目——比如做一个能看图片、能读文字、能给你推荐今晚吃啥的“多模态菜谱系统”——瞬间就懵了。数据怎么处理?图片和文字怎么揉到一块儿?检索准了生成却不准怎么办?别慌,今天咱们就把这个项目掰开了、揉碎了,一点一点讲清楚。
一、数据治理与架构设计:你的“地基”决定了楼能盖多高
点题
多模态菜谱系统的第一步不是怼模型,是搞数据。你得有菜谱文本(菜名、食材、步骤、口味标签),还得有菜品图片(成图、步骤图)。这两样东西怎么存、怎么洗、怎么管,决定了后面所有环节是否顺畅。一个合理的架构,至少应该分出数据层、Embedding层、检索层和生成层,层与层之间通过标准接口交互,而不是揉成一坨。
痛点分析
新手最容易犯的错就是“数据拿来主义”。网上爬下来的菜谱JSON,里面图片链接是404的;食材描述写的是“少许”“适量”,模型根本没法理解;图片大小不一,有的带水印,有的甚至是GIF。你二话不说,直接把这些脏数据往向量库里一塞,回头检索出来的结果惨不忍睹。更隐蔽的坑是单位不统一,比如有的菜谱用“克”,有的用“勺”,检索关键词匹配时直接漏召。
错误案例:
# 错误示范:直接加载原始脏数据,不做任何清洗
raw_data = load_json("recipes_dirty.json")
# 文本里还有HTML标签,图片链接已失效
for item in raw_data:
text_vec = embedding_model.encode(item["text"])
# 链接404,返回None或报错,却被当成正常向量插入
image_vec = clip_model.encode(item["image_url"])
vector_db.insert(text_vec, image_vec)
# 结果:垃圾进,垃圾出
想象一下,用户搜“清淡汤品”,结果出来个“麻辣火锅”。为啥?因为原始网页里带了句“吃完火锅来碗清汤解腻”的推荐语,向量一编码,这俩居然离得很近。图片更是张冠李戴,点进去全是广告图。这种体验,用户不骂娘才怪。
解决方案
先把架构画清楚,四层分离。文本侧做标准化清洗:去HTML标签、统一单位(把“少许”映射成标准克数或枚举值)、分词、去重。图像侧做预处理:下载到本地做校验(排除GIF、纯黑图、过小的图)、统一Resize到224x224、用感知哈希去重。存储上,原始数据放对象存储或CDN,元数据放PostgreSQL,向量放Milvus或pgvector。
正确案例:
# 正确示范:数据清洗流水线
def clean_recipe(raw):
text = strip_html(raw["desc"])
text = normalize_quantity(text) # 把“适量”标准化
image_path = download_and_verify(raw["img_url"])
if image_path and not is_duplicate(image_path):
return {
"id": raw["id"],
"clean_text": text,
"image_path": image_path,
"tags": raw.get("tags", [])
}
return None
cleaned = [c for c in map(clean_recipe, raw_data) if c]
batch_embed_and_index(cleaned)
这样做的好处是:检索侧拿到的素材都是“可信的”,生成侧不会胡编乱造。而且分层架构让你后面换模型、换向量库都不伤筋动骨。记住,数据是地基,地基不牢,地动山摇。
小结
数据治理是苦活累活,但它是整个系统的地基。多花两小时洗数据,能省后面两天的调参时间。
二、多模态Embedding对齐:让文字和图片说同一种语言
点题
有了干净数据,下一步是把“文字描述”和“菜品图片”映射到同一个向量空间里。这样用户上传一张冰箱照片,系统才能找到对应的菜谱;或者用户输入“红彤彤的肉菜”,系统也能召回色泽相符的菜品图。这一步的核心叫“多模态对齐”。
痛点分析
新手最大的误区是“各扫门前雪”。文本用OpenAI的text-embedding-ada-002,图像用ResNet50提特征,然后把两个向量直接拼在一起。大哥,这俩向量的维度、分布、语义空间完全不一样啊!ada-002是1536维,ResNet是2048维,拼起来3584维,你以为你在拼乐高呢?更重要的是,文本空间和图像空间没有对过齐,“红烧肉”的文本向量离“红烧肉”的图片向量可能十万八千里。
错误案例:
# 错误示范:异构Embedding强行拼接
text_vec = openai_embed("红烧肉的做法") # 1536维,语义空间A
image_vec = resnet.predict("hongshaorou.jpg") # 2048维,语义空间B
combined = np.concatenate([text_vec, image_vec])
vector_db.insert(combined)
检索的时候,用户输入一个文本Query,你只用前半段1536维去比,图片信息完全没用上;或者强行全量比,噪音把信号全淹没了。最后系统变成“瞎子摸象”,图文各说各话。
解决方案
用真正的多模态Embedding模型,比如CLIP、Chinese CLIP或者SigLIP。这类模型的核心思想就是“对比学习”:让匹配的图文对在向量空间里靠近,不匹配的原理。文本和图像过同一个模型的不同塔(Text Tower & Vision Tower),出来的向量维度一致、语义对齐。对于中文菜谱场景,强烈建议用Chinese CLIP,它对“鱼香肉丝”“宫保鸡丁”这种中文语义的的理解更到位。
正确案例:
# 正确示范:使用CLIP进行多模态统一编码
from transformers import CLIPProcessor, CLIPModel
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
# 编码文本
inputs_text = processor(text=["红烧肉"], return_tensors="pt", padding=True)
text_vec = model.get_text_features(**inputs_text)
# 编码图像
inputs_img = processor(images=[image], return_tensors="pt")
image_vec = model.get_image_features(**inputs_img)
# 归一化后计算相似度
similarity = cosine_similarity(text_vec, image_vec)
# 结果:0.89,高度相关
这样做的好处是:用户的文字Query和上传的菜品图片,都在同一个“语义坐标系”里比距离。你甚至可以做多模态Query:用户拍了一张青椒炒肉的图,问“这道菜怎么做?”系统把图和文联合编码,直接命中菜谱。同一个世界,同一个向量空间,别让文本和图片各说各话。
小结
多模态检索的灵魂是“对齐”。选一个能同时理解图文的多模态Encoder,比把两个异构向量硬拼要靠谱一百倍。
三、RAG检索链路搭建:从“大海捞针”到“精准定位”
点题
向量对齐了,接下来要搭建检索链路。用户可能输入文字“清淡一点的肉菜”,可能上传一张冰箱里的剩菜照片,也可能两者结合。系统怎么从百万菜谱里快速、准确地捞出候选集?答案是:多路召回、分层过滤,别指望单一向量检索包打天下。
痛点分析
新手的检索链路往往“单兵作战”:只搞一个向量Top-K召回。结果要么漏(用户要“清淡”,向量相似度高的却是“红烧”),要么杂(k值设成100,啥都往里装,后面生成模型直接过载)。更坑的是,多模态Query来了不知道该怎么拆:用户给了图又给了文字,到底以谁为主?还有新手完全不做metadata过滤,把烹饪时长、辣度、忌口这些信息统统浪费掉。
错误案例:
# 错误示范:单一向量检索,不设过滤
results = vector_db.search(
vector=user_image_vec,
top_k=100
)
# 结果里既有“清蒸鱼”(对),也有“麻辣香锅”(错)
# 用户说“不要辣”,系统完全没理会
prompt = build_prompt(results)
response = llm.generate(prompt)
# 100条结果塞进Prompt,Token爆炸,模型直接懵圈
100条候选塞给大模型,上下文窗口占满,生成速度慢得像蜗牛,质量还差。用户等了三秒,看到一堆驴唇不对马嘴的推荐,关掉页面,你的日活没了。
解决方案
设计“多路召回 + 粗排 + 精排”的分层检索链路。
第一步,多路召回:向量一路(CLIP语义相似)、关键词一路(BM25匹配食材名,比如“鸡蛋”“西红柿”)、metadata过滤一路(烹饪时间小于30分钟、口味等于清淡)。
第二步,粗排:把多路结果去重融合,按业务规则筛掉明显不匹配的(比如用户过敏食材、宗教禁忌)。
第三步,精排:只把最相关的10到20条送给大模型。
正确案例:
# 正确示范:多路召回+过滤
vec_results = vector_db.search(query_vec, top_k=50)
kw_results = elasticsearch.search(user_query, top_k=50)
candidates = merge_and_deduplicate(vec_results, kw_results)
# Metadata硬过滤:用户勾选“清淡”“无辣”
filtered = [
c for c in candidates
if c["tag"] == "清淡" and "辣" not in c["taste"]
]
# 取Top10给大模型
final_context = filtered[:10]
prompt = build_rag_prompt(user_query, user_image, final_context)
这样做,候选集从100个噪音变成10个精品,大模型上下文够用了,生成质量直接起飞。
小结
检索不是一锤子买卖。多路召回保全面,分层过滤保精准,别让大模型在垃圾堆里找答案。
四、大模型生成与个性化推荐:检索是备菜,生成是烹饪
点题
检索回来的10个候选菜谱,怎么变成用户眼前一份“千人千面”的推荐报告?这就是大模型生成的活儿。它不是简单罗列菜谱,而是要根据用户的食材、口味、烹饪水平,给出个性化建议。检索给你备好料了,但怎么炒这盘菜,全靠Prompt工程和生成策略。
痛点分析
新手写Prompt像在开盲盒。有的直接扔原始JSON给模型,模型看得眼花缭乱;有的Prompt里没给明确角色,模型输出像说明书一样生硬;更惨的是,没约束输出格式,模型一会儿Markdown,一会儿纯文本,前端根本没法渲染。最致命的是:没有把用户的“上下文”(比如冰箱里有什么、忌口什么、是不是新手)传进去,导致推荐完全不个性化。用户只有鸡蛋和西红柿,你给他推荐佛跳墙,不是找骂吗?
错误案例:
# 错误示范:Prompt过于简单,缺乏约束
prompt = f"""
用户问:{user_query}
检索结果:{json.dumps(raw_results)}
请推荐。
"""
response = llm.chat(prompt)
# 输出:"以下是一些菜谱:1. 红烧肉 2. 糖醋排骨..."
# 完全没考虑用户只有鸡蛋和西红柿
解决方案
Prompt工程要结构化、模板化。明确四要素:
第一,角色:你是精通家常菜的AI营养师。
第二,任务:根据用户现有食材和口味,推荐并改良菜谱。
第三,输入:结构化检索结果(菜名、食材、步骤、图片URL)加上用户画像。
第四,约束:输出必须包含“推荐菜名”“所需食材(标注用户已有/缺货)”“简化步骤”“预计耗时”。
正确案例:
# 正确示范:结构化Prompt模板
prompt_template = """
【角色】你是一位擅长“就地取材”的家常菜大厨。
【用户情况】
- 现有食材:{user_ingredients}
- 口味偏好:{user_taste}
- 烹饪水平:{user_level}
【候选菜谱】
{formatted_recipes}
【任务】
请从候选菜谱中挑选最适合的1-2道,并针对用户现有食材给出调整建议。
若缺个别食材,请给出可替代方案。
【输出格式】
### 今日推荐:{菜名}
**匹配度**:{高/中/低} | **耗时**:{X}分钟
**食材清单**:
- 用户已有:...
- 还需准备:...
**步骤简化版**:...
"""
prompt = prompt_template.format(
user_ingredients="鸡蛋2个、西红柿1个、青椒1个",
user_taste="偏清淡,不吃辣",
user_level="新手",
formatted_recipes=format_for_llm(final_context)
)
response = llm.chat(prompt)
这样做,模型输出稳定、结构清晰、真正贴合用户需求。前端拿到Markdown直接渲染,用户体验拉满。检索是给模型备好弹药,Prompt是教它怎么开枪。结构化Prompt,才能让大模型从“胡说八道”变成“私人厨师”。
小结
检索是给模型备好弹药,Prompt是教它怎么开枪。两者缺一不可,结构化Prompt是生成质量的保险栓。
五、融合与重排优化:召回靠广度,排序靠精度
点题
多路召回回来一堆候选,谁排第一、谁排最后?文本检索说“麻婆豆腐”最相关,图像检索说“红烧豆腐”最像,你信谁?这一步叫Rerank(重排序),是多模态RAG里最容易被忽视、却最能体现技术深度的环节。融合与重排,直接决定了用户第一眼看到的是不是他想要的。
痛点分析
新手最常见的做法是“拍脑袋加权”:文本相似度占0.5,图像相似度占0.5,一平均完事。问题是,不同Query的场景不一样。用户拍了一张图来问菜名,这时候图像权重应该高;用户输入详细文字描述“少油少盐的晚餐”,文本权重应该高。一刀切加权,结果就是“红烧豆腐”因为图像颜色像麻婆豆腐,被错误地排到了第一。还有一种坑是直接用向量相似度当最终排序,忽略了业务逻辑,导致用户明明忌口海鲜,海鲜类菜品却因为向量分数高而置顶。
错误案例:
# 错误示范:简单加权融合
for candidate in candidates:
score = 0.5 * candidate["text_score"] + 0.5 * candidate["image_score"]
# 用户搜"麻婆豆腐"
# 麻婆豆腐文本分0.95,图像分0.60 -> 0.775
# 红烧豆腐文本分0.70,图像分0.90 -> 0.800(反而排第一!)
用户点进去发现完全不辣,一脸懵。这种“看起来很像,实际上不对”的体验,比直接搜不到更伤用户信任。
解决方案
引入“多模态交叉编码器”做精细重排。比如用ColBERT、ColPali或ColQwen这类多模态Reranker,它们能把Query文本、Query图片和候选文档的图文做交叉注意力计算,打出更准的分数。如果算力有限,也可以设计业务规则后处理:用户Query含图片时图像分权重上调20%;或者用大模型做Listwise Reranking,把Top20候选丢给多模态LLM,让它直接排出1到20名并说明理由。
正确案例:
# 正确示范:轻量级多模态Reranker
# 假设使用ColPali风格的交叉编码
rerank_scores = multimodal_reranker.score(
query_text=user_query,
query_image=user_image,
candidates=candidate_list # 包含图文
)
reranked = sorted(
zip(candidates, rerank_scores),
key=lambda x: x[1],
reverse=True
)
# 业务规则兜底:过敏食材强制降权
final_results = apply_business_rules(reranked, user_profile)
这样做的好处是:文本和图像不是各打各的分再硬凑,而是在模型内部做深度融合。麻婆豆腐的“麻”“辣”文本特征和“红油”图像特征被联合理解,不会被红烧豆腐“长得像”而干扰。召回靠广度,排序靠精度,加权平均是偷懒,交叉编码才是对多模态的尊重。
小结
召回靠广度,排序靠精度。加权平均是偷懒,交叉编码才是对多模态的尊重。
六、工程落地与性能调优:从Jupyter Notebook到线上服务
点题
前面五步你在Jupyter Notebook里跑通了,感觉世界真美好。但要把这个系统搬到线上,让用户能并发访问、秒级响应、稳定运行,还有一大段路要走。工程化,是区分“玩具Demo”和“可用产品”的分水岭。这一步不过,前面全白搭。
痛点分析
新手最容易在工程化上栽跟头。向量库用内存版FAISS,重启服务数据全丢;图片没做CDN和缩略图,用户一多带宽炸掉;Embedding服务同步调用,一张高清图编码3秒钟,用户等得想关APP;没有评估体系,上线两周了不知道推荐到底准不准,全靠产品经理拍脑袋。最惨的是不做限流和降级,大模型服务一抖动,整个推荐链路全挂,页面直接报502。
错误案例:
# 错误示范:同步阻塞+内存向量库
@app.post("/recommend")
def recommend(image: UploadFile):
img_bytes = image.read()
vec = clip_encode(img_bytes) # 同步编码,阻塞主线程
results = faiss_index.search(vec, 10) # 内存索引,重启清零
recipes = [load_full_recipe(r) for r in results]
prompt = build_prompt(recipes)
text = llm_generate(prompt) # 再次同步等待
return text # 耗时5-8秒,并发一高直接OOM
上线第一天,10个用户同时传图,服务直接OOM,因为图片全部加载进内存做Resize。这种崩溃,老板可不会听你解释“我在本地跑得好好的”。
解决方案
工程化三板斧:持久化、异步化、缓存与评估。
第一,向量库持久化。生产环境用Milvus、Qdrant或PostgreSQL with pgvector,支持分布式和持久化,别再用内存FAISS了,除非你只做轻量本地版。
第二,异步流水线。图片上传后先入队列(Celery/RQ),后台做Resize、Embedding、入库;检索时向量库查Top-K可以毫秒级返回。大模型生成用异步IO或流式输出,别让HTTP连接干等着。
第三,多级缓存。热门Query结果缓存到Redis,命中率能到60%以上;图片走CDN加缩略图,别每次都传原图。
第四,评估体系。离线用Recall@K、NDCG评估检索质量;在线做A/B测试,看点击率、收藏率。生成侧用人工标注加自动指标(BERTScore、GPT-4打分)做回归测试。
正确案例:
# 正确示范:异步+缓存+持久化架构
@app.post("/recommend")
async def recommend(request: RecommendRequest):
# 1. 查缓存
if cache_hit(request.cache_key):
return get_cache(request.cache_key)
# 2. 异步检索持久化向量库
vec = await async_clip_encode(request.image_url)
candidates = await milvus_client.search(vec, top_k=20)
# 3. 轻量级Rerank
top5 = await local_reranker.rerank(request, candidates)
# 4. 异步调用大模型
prompt = build_prompt(top5, request.user_profile)
response = await async_llm.generate(prompt)
set_cache(request.cache_key, response, ttl=3600)
return response
架构上,检索服务、Embedding服务、生成服务解耦,各自水平扩容。图片处理单独抽微服务,支持批量和限流。大模型挂了?走规则兜底,直接返回热门菜谱,保证用户体验不崩盘。
小结
Demo和产品的距离,隔着一整片工程化的海洋。只有过了性能、稳定性、可评估这三关,你的“赛博厨师”才能真正上桌。
写在最后
写到这儿,咱们这个项目9的六个关键环节就算串起来了。你看,从对着脏数据发愁,到看着多模态向量对齐;从单一检索的无力感,到多路召回加重排的从容;从Jupyter里的玩具,到能扛住并发的线上服务——每一步都是新手成长为实战派必经的坎。
我知道,很多兄弟看到“多模态”“RAG”“向量检索”这些词堆在一起,第一反应是“这也太卷了,我能学会吗?” 但你想啊,谁不是从“把ResNet和文本向量硬拼”的傻操作里爬出来的呢?犯错不可怕,可怕的是在同一个坑里躺平。数据治理做得细一点,Embedding选型想得深一点,Prompt写得结构化一点,工程化想得全面一点——这些“一点点”累积起来,就是你简历上最硬的底气。
编程之路不易,但每一步成长都算数。保持好奇,别怕踩坑,多动手把项目从Notebook里搬出来。下次当你打开冰箱,对着那半根胡萝卜发呆的时候,希望你能笑着想:没关系,我的AI已经学会怎么把它变成一盘好菜了。加油,咱们下个项目见!
关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如:
《课程:2026 年多模态大模型实战训练营》
《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》
《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》
《课程:2026 年 AGI 大模型系统课 23 期》
《课程:2026 年 AGI 大模型系统课 21 期》
《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》
《课程:AI 大模型系统实战课三期》
《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》
《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》
《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》
《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》
《课程:LLM 多模态视觉大模型系统课》
《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》
《课程:大模型智能体线上速成班 V2.0》
《课程:Java+AI 大模型智能应用开发全阶课》
《课程:Python+AI 大模型实战视频教程》
《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》
《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》
《课程:AI 大模型零基础到商业实战全栈课第五期》
《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》
《课程:AI 大模型实战训练营 从入门到实战轻松上手》
《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》
《课程:大模型训练营配套补充资料》
更多推荐




所有评论(0)