02 · 独立开发 · 微信小程序 + FastAPI · 2026.06 – 至今
AI 食材识别与菜谱推荐助手
拍一张现有食材,识别、确认后推荐能做的菜,再跟着步骤完成烹饪。难点不在"接了个模型",而在三个判断:模型选型用数据说话、所有模型调用走统一出口、确定性链路与受限 Agent 各司其职。
概览
家里有食材但不知道做什么,是个高频但一直没被解决好的问题。传统菜谱搜索要求先知道菜名,再手动比对食材、时间和口味。项目把入口反过来:先拍现有食材,由视觉模型提取候选,用户确认修正后,结合结构化菜谱事实给出推荐,再用步骤页和语音降低实际烹饪成本。
我独立负责产品流程、前后端实现、模型评测、数据库与 Tool/Agent 设计、RAG 导入层及测试。
问题
-
食材识别没有公开 benchmark,选型等于赌
要把食材图片识别成结构化 JSON,但没有现成基准告诉我哪个模型 Recall 够高、延迟可接受。不同模型差异巨大——实测 Recall 从 0.53 到 0.63、延迟从 1.6 秒到 56 秒不等。凭感觉选会选错。
-
文本推荐模型普遍太慢,同步场景不可用
推荐菜谱的 LLM 在实测里平均要 12 秒到 49 秒。用户在"看看能做什么"这一步干等几十秒,体验上是不可用的。需要先量化延迟边界,再决定取舍。
-
开放式"问厨"里,Agent 可能失控
用户可能同时给出食材、时间、口味、缺失容忍度等模糊约束,系统需要动态决定搜索、查详情还是补问。但开放式的 Agent 可能无限调用 Tool、绕圈子。需要一个明确的收口机制。
架构
方案
-
01自建评测集横评视觉模型
建立 50 张图片、98 个目标食材标签的视觉评测集,横评 4 个多模态模型,统计命中、漏检、额外预测、JSON 合法率与平均延迟。最终选 qwen3.5-omni-flash——它的 Recall(0.5714)不是最高,但平均延迟 1.63s 最低、额外预测最少,配合"用户确认"节点用纠错兜底。
为什么不是 Recall 最高的:kimi-k2.6 的 Recall 有 0.6327,但平均 55.8 秒的等待不适合同步拍照。选型要的是"足够效果 + 更低延迟 + 人机协同纠错"的折中,而不是单一指标最优。
-
02AI Gateway 统一异常契约
所有模型调用都收敛到一个
call_ai():把 OpenAI SDK 的 APITimeout / APIConnection / RateLimit / APIStatus 四类异常,映射成项目自己的 AIServiceTimeout / Connection / Busy / Provider 领域错误,业务路由只依赖领域异常,不感知供应商异常类型。为什么收敛到一个出口:散落的模型调用会各自长出一套超时和重试逻辑,行为不一致且无法统一观测。收敛之后,加一条降级策略只改一个地方,所有调用的延迟、失败类型、重试次数都在同一层可查。
-
03TTS 预取 + Redis Cache-Aside
菜谱步骤是固定文本,提前把语音生成好。缓存键由供应商、模型、音色、格式、采样率、语速和规范化文本序列化后做 SHA-256,不暴露原文;TTL 用供应商过期时间减 300 秒安全窗口。读写失败一律 fail-open,不阻断语音。同文本命中延迟从 2045.59ms 降到 17.02ms。
为什么成立:"热锅倒油"对所有用户是同一段音频,没必要每次实时合成。这是空间换时间,前提是内容重复率足够高——菜谱步骤恰好满足。
-
04双链路 + 受限 KitchenAgent
拍照做饭是固定路径(识别→确认→推荐→步骤→TTS),用确定性 Workflow 承载;只有"现有食材 + 时间 + 口味 + 缺失容忍度"这类无法预先写死步骤的开放式问题,才进入受限 KitchenAgent。Agent 通过 Tool Registry 只暴露白名单能力,受 4 轮、6 次 Tool 调用、60 秒总超时约束,每次执行落持久化 trace。
为什么不全用 Agent:把确定性主链路塞进 Agent,不会提升识别质量,只会增加轮次、延迟、成本和不可预测跳转。先用上限保证最坏情况下系统一定停下来,再谈效果——开放式场景枚举不了所有跑偏,但一定能枚举资源边界。
关键决策
为什么选 Recall 不是最高的视觉模型?
评测不是只看 Recall。kimi-k2.6 的 Recall 最高(0.6327),但平均 55.8 秒的等待对"拍照识别"这个同步场景是致命的;qwen3.5-omni-flash 平均 1.63 秒,Recall 0.5714,且额外预测最少。产品在识别后有用户确认/修改节点,可以用人机协同把 Recall 的差距补回来。所以选了"足够效果 + 更低延迟 + 可纠错"的方案,而不是纸面 Recall 最高的那个。
为什么菜谱事实不交给模型自由生成?
菜名、食材、用量、步骤、来源是可验证的事实,交给 LLM 自由生成会有幻觉且无法追溯。所以 PostgreSQL 作为唯一事实来源,模型只负责理解用户目标、选择工具、组织回答。硬约束(过敏、禁忌、耗时上限)由代码强制过滤,Agent 无权放宽——这同时降低幻觉,也让每一条推荐有据可查。
为什么 Tool 和 Repository 要分开?
模型不能直接调用 Repository:它不能创建 AsyncSession、ORM 对象不是稳定的跨边界 JSON 契约、数据库字段变更不该直接打破 Agent 契约。所以 Tool 是"受控 AI Adapter"——声明名称和用途、校验模型生成的 JSON 参数、调用 Repository、把 ORM 转成最小化 JSON、隐藏内部字段。Repository 仍可被 API、Service、测试复用。
为什么给 Agent 加边界,而不是"让它更聪明"?
开放式场景枚举不了所有跑偏,但一定能枚举资源边界:4 轮、6 次 Tool 调用、60 秒总超时、白名单、终止条件、持久化 trace。还有两个细节:同轮多个 Tool Call 要么整批获准要么整批拒绝(不留半轮中间态);重复调用用规范化签名识别并计数(否则模型可以无限重复同一请求)。运行状态与质量评测分开——HTTP 200 不等于任务成功。
结果
0.5714
视觉识别 Recall
qwen3.5-omni-flash,平均延迟 1.63s
2046ms → 17ms
TTS 同文本命中延迟
单次本地验证,降低约 99.2%
15 / 15
推荐硬约束通过
deepseek-v4-pro,15 类用例 100%
15 / 15
推荐硬约束通过
deepseek-v4-pro,15 类用例 100%
视觉模型横评(50 图 · 98 标签)
| 模型 | Recall | 平均延迟 | JSON 合法率 |
|---|---|---|---|
| qwen3.5-omni-flash(选用) | 0.5714 | 1.63s | 100% |
| kimi-k2.6 | 0.6327 | 55.83s | 100% |
| qwen3.7-plus | 0.6020 | 16.83s | 100% |
| gpt-5.2 | 0.5306 | 7.44s | 100% |
推荐模型横评(15 类固定场景,硬约束通过率均为 100%)
| 模型 | 用例通过 | 平均延迟 | P95 延迟 |
|---|---|---|---|
| deepseek-v4-pro(选用) | 15 / 15 | 12.76s | 19.22s |
| glm-5.2-fast-preview | 15 / 15 | 11.35s | 17.10s |
| qwen3.7-plus | 15 / 15 | 39.12s | 46.77s |
| kimi-k3 | 15 / 15 | 49.33s | 87.87s |
推荐延迟的初始在线目标为平均 ≤ 5s、P95 ≤ 10s。当前候选模型仍未达标(deepseek-v4-pro 平均 12.76s),这是明确的后续优化项。
已知边界
-
01
视觉 Recall 约 0.57。小众食材(香茅、罗勒这类)识别率明显偏低,仍是整个链路里最弱的一环。
-
02
RAG 检索闭环尚未完成。文档导入层(统一 ParsedDocument、PDF 文本/图片/bbox/hash 提取、Docling 离线解析器选型)已完成,但 Chunk / Embedding / pgvector 混合检索 / RAG Tool 仍在建设中。
-
03
推荐延迟未达标。deepseek-v4-pro 平均 12.76s,仍高于"平均 ≤ 5s"的目标,需要缓存或更快的模型通道。
-
04
未做并发压测。TTS 缓存只有单次本地验证数据(2045.59ms → 17.02ms),暂无命中率、P95 和线上吞吐口径。