ChefMate RAG — 智能食谱 RAG 问答系统
RAGFastAPIDeepSeekFAISSPythonAstro
从 CLI 到 Web,一个完整 RAG 系统的工程化实践。基于 362 个真实菜谱,涵盖数据准备、向量索引、混合检索、查询路由、LLM 生成、指标可视化与生产部署的全链路实现。已上线运行于 vincentbuilds.fun。
系统架构
用户输入 (vincentbuilds.fun/chefmate)
│
├──► 多轮对话解析 (指代消解 + 上下文注入)
│
├──► 查询约束分析 (份量/分类/难度提取)
│
├──► 查询路由 (LLM 分类: list/detail/general)
│
├──► 混合检索
│ ├── FAISS 向量检索 (bge-small-zh-v1.5, 384d)
│ ├── BM25 关键词检索
│ ├── 元数据筛选 (份量/分类/难度)
│ └── RRF 融合排序 (k=60)
│
├──► 父文档还原 (子块 → 完整菜谱)
│
├──► 上下文预算管理 (10,000 tokens)
│
├──► LLM 生成 (DeepSeek V4, 三种 Prompt 模板)
│ ├── list → JSON 约束输出 (防幻觉)
│ ├── detail → 结构化步骤教学
│ └── general → 知识推理
│
└──► 答案验证 → 指标计算 → 流式返回
部署架构
GitHub (allinweb)
│
├── git push → GitHub Actions CI
│ ├── npm build → dist/
│ ├── rsync dist/ → Alibaba Cloud
│ └── rsync chefmate/ + data/ → Alibaba Cloud
│ └── docker compose up -d --build
│
└── Alibaba Cloud ECS
├── Nginx (443, SSL)
│ ├── / → /var/www/vincentbuilds (static)
│ └── /api/* → proxy_pass localhost:8000
└── Docker
└── FastAPI :8000 (127.0.0.1 only)
核心技术
技术栈
| 层级 | 技术 | 用途 |
|---|---|---|
| 向量检索 | FAISS + bge-small-zh-v1.5 (384d) | 语义相似度搜索 |
| 关键词检索 | BM25 (rank-bm25) | 精确关键词匹配 |
| 融合排序 | RRF (Reciprocal Rank Fusion, k=60) | 消除分数尺度差异 |
| LLM | DeepSeek V4 (deepseek-chat) | 路由 + 查询改写 + 生成 |
| 后端 | FastAPI + slowapi + Pydantic | API 服务 + 限流 + 校验 |
| 前端 | Astro v6 + Tailwind CSS v4 | 静态站 + ChefMate SPA |
| 部署 | Docker + Nginx + GitHub Actions | 容器化 + HTTPS + CI/CD |
混合检索策略
# retriever.py — 核心检索逻辑
def hybrid_search(query, top_k=20, filters=None):
fetch_k = max(top_k * 3, 15) # 放大检索池
vec_docs = FAISS.similarity_search(query, k=fetch_k) # 语义
bm25_docs = BM25.retrieve(query, k=fetch_k) # 关键词
# 元数据筛选(份量/分类/难度)
if filters:
vec_docs = apply_filters(vec_docs, filters)
bm25_docs = apply_filters(bm25_docs, filters)
# RRF 融合排序
return rrf_rerank(vec_docs, bm25_docs)[:top_k]
为什么用 RRF? 向量检索 (0~1 相似度) 和 BM25 (非归一化分数) 的分数尺度不同,无法直接相加。RRF 基于排名而非原始分数,天然消除了尺度差异。
查询约束分析
# query_analyzer.py — 从自然语言提取结构化约束
"推荐3个简单的素菜,适合5个人吃"
→ servings=5, category="素菜", difficulty="简单"
"推荐适合十个人吃的量的菜"
→ servings=10 # 中文数字自动识别
"适合两个人吃的菜"
→ servings=2 # 小份量场景
约束同时用于:
- 检索阶段(元数据筛选,优先返回份量匹配的菜谱)
- 生成阶段(Prompt 注入约束提示)
关键功能
1. 幻觉防御
问题:LLM 在列表推荐时经常编造库中不存在的菜名。
方案:三重防线
| 防线 | 实现 | 效果 |
|---|---|---|
| Prompt 约束 | JSON 结构化输出 + 白名单菜品列表 | LLM 只能从给定列表中选择 |
| 候选过滤 | 从 valid_names 中物理剔除,LLM 看不到 | 纯技术手段杜绝 |
| 后校验 | AnswerValidator 模糊匹配 (get_close_matches) | 容错 “宫宝鸡丁” → “宫保鸡丁” |
2. 多轮对话
用户:推荐两个素菜
系统:素炒豆角 + 凉拌豆腐
用户:有没有不一样的 ← 上下文理解
系统:奶油蘑菇汤 + 上汤娃娃菜 ← 自动排除已推荐
指代消解 + 已推荐菜品追踪 + 上下文注入到 Prompt。
3. 份量感知
菜谱数据自动提取份量信息(“一份正好够 2 人吃”、“4-6 人份”),检索和推荐时作为硬约束。
4. RAG 指标可视化
每次回答展示:
- 路由类型 — LLM 分类结果(步骤教学/列表推荐/通用问答)
- 检索来源 — 匹配菜谱、语义得分、命中子块数
- 耗时 — 端到端响应时间
- 置信度 — 基于检索命中数计算
安全防护
| 防线 | 措施 | 配置 |
|---|---|---|
| 频率限制 | slowapi IP 限流 | 10/min(chat) |
| 日配额 | SQLite 计数 | DAILY_QUOTA=200 |
| 输入校验 | Pydantic + XSS 检测 | max_length=500 |
| 端口安全 | Docker 绑定 127.0.0.1 | 公网不可达 |
| CORS | 仅允许 vincentbuilds.fun | 同源策略 |
| HTTPS | Nginx + Let’s Encrypt | TLS 1.2/1.3 |
| API Key | 服务端 .env chmod 600 | 前端不可见 |
本地开发
git clone https://github.com/8BitcloudBot/allinweb.git
cd allinweb
# 后端
cp .env.example .env # 填入 DEEPSEEK_API_KEY
uv sync
uv run uvicorn chefmate.server:app --reload --port 8000
# 前端(另一个终端)
npm install && npm run dev # http://localhost:4321
V2 演进
ChefMate GraphRAG 版本已上线,基于 Neo4j 知识图谱支持多跳推理、食材搭配发现和相似菜品推荐。
注意:向量索引各环境独立,首次启动会自动构建(约 20 秒)。不要跨环境复制 vector_index/。