在昨天的文章中,我們透過有限狀態機(FSM)實作了多輪對話機制,讓 Agent 能夠在關鍵資訊不足時主動追問,補齊使用者的地點或預算。
然而,美食推薦領域最常見的使用情境往往不是「條件太明確」,而是每到用餐時間最讓人頭痛的終極難題:
「今天中午到底要吃什麼?隨便啦,你幫我抽一個!」
面對選擇障礙(Decision Paralysis)的使用者,一口氣丟出 5 間餐廳只會讓對方更糾結。這時候,如果能提供一個「命運盲盒 / 美食大轉盤」模式,直接隨機盲抽一家,並搭配 LINE 貼心的 Quick Reply(快速回覆按鈕),使用者的互動體驗將會變得極其有趣又解痛!
今天 Day 18,我們將在微服務中實作隨機抽籤演算法,並在 LINE 回覆中加入 Quick Reply 按鈕選單,打造一鍵重抽與快速決策體驗!
Quick Reply 是 LINE 提供的一種懸浮在聊天室底部的快速按鈕選單:
app/routers/recommendation.py)我們在推薦模組中新增一隻專屬的盲盒隨機抽選端點 POST /api/v1/recommend/random。
app/schemas/food.py)在 app/schemas/food.py 新增抽籤專屬結構:
from pydantic import BaseModel, Field
from typing import Optional
from app.schemas.food import RestaurantItem
class RandomDrawRequest(BaseModel):
user_id: str
location: str = Field(default="板橋區", description="搜尋範圍或商圈")
budget_max: int = Field(default=300, description="預算上限")
class RandomDrawResponse(BaseModel):
status: str
drawn_restaurant: Optional[RestaurantItem] = None
remaining_count: int = 0
利用 Python 的 random.choice 從符合條件的真實 Google Places 候選名單中,隨機抽出一位「天選店家」:
import random
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.database import get_db
from app.schemas.food import RandomDrawRequest, RandomDrawResponse
from app.services.places import search_places
router = APIRouter(
prefix="/api/v1/recommend",
tags=["Food Recommendation"]
)
@router.post("/random", response_model=RandomDrawResponse)
async def draw_random_food(req: RandomDrawRequest, db: Session = Depends(get_db)):
"""
美食大轉盤:從指定區域符合預算的店家名單中,隨機盲抽一間餐廳
"""
# 呼叫 Places API 搜尋候選餐廳(分類設定為全部,以擴大候選池)
candidates = await search_places(
query_location=req.location,
query_cuisine="全部",
budget_max=req.budget_max
)
if not candidates:
return RandomDrawResponse(status="empty", drawn_restaurant=None, remaining_count=0)
# 隨機抽取一間
chosen_one = random.choice(candidates)
return RandomDrawResponse(
status="success",
drawn_restaurant=chosen_one,
remaining_count=len(candidates) - 1
)
回到 Dify 的系統提示詞(System Prompt),新增一個 mode 標籤,讓大腦自動辨識使用者是否正處於「選擇困難」模式:
你是一個專業的美食推薦助理前置解析器。請從對話中提取以下欄位並輸出 JSON:
- mode: 對話意圖(若使用者提及「隨便」、「幫我抽」、「選擇困難」、「不知道吃什麼」、「命運轉盤」,請填 "random";一般查詢填 "search")
- cuisine: 料理類型(未提及填 "全部")
- budget_max: 每人預算(未提及預設 300)
- location: 地區或捷運站(未提及填 "板橋區")
輸出範例:
{
"mode": "random",
"cuisine": "全部",
"budget_max": 300,
"location": "板橋"
}
當後端回傳抽籤結果時,我們使用 LINE Quick Reply 按鈕,讓使用者能一鍵進行「再抽一次」或「滿意導航」。
在 n8n 的訊息格式化節點中,編寫 JavaScript 邏輯:
const responseData = $input.first().json;
const replyToken = $('Parse AI JSON').first().json.reply_token;
// 1. 若為抽籤模式 (Random Mode)
if (responseData.drawn_restaurant) {
const shop = responseData.drawn_restaurant;
const mapUri = `[https://www.google.com/maps/search/?api=1&query=$](https://www.google.com/maps/search/?api=1&query=$){encodeURIComponent(shop.location + ' ' + shop.name)}`;
const resultText = `🎲 命運的美食轉盤轉動中...\n\n` +
`今日午餐就是它了:【${shop.name}】!✨\n` +
`📍 地址:${shop.location}\n` +
`💰 均消:約 ${shop.price_avg} 元\n` +
`⭐️ 評分:${shop.rating}\n\n` +
`不滿意嗎?可以點下方按鈕重新抽籤哦!`;
// 2. 構造底部 Quick Reply 互動按鈕
const quickReply = {
items: [
{
type: "action",
action: {
type: "message",
label: "🎲 再抽一次",
text: "幫我隨便抽一間"
}
},
{
type: "action",
action: {
type: "uri",
label: "🗺️ 查看地圖",
uri: mapUri
}
},
{
type: "action",
action: {
type: "message",
label: "🍜 改吃日式",
text: "推薦西門町日式拉麵"
}
}
]
};
return {
reply_token: replyToken,
messages: [
{
type: "text",
text: resultText,
quickReply: quickReply
}
]
};
}
// 若是一般推薦模式,則維持昨天的 Flex Carousel 回傳
return $input.first().json;
確保 FastAPI 伺服器與 n8n 工作流均正常處於 Active 狀態,拿起手機打開 LINE 測試:
1.發送選擇困難口語:
使用者:「中午不知道吃什麼,隨便幫我抽一個」
2.機器人秒抽回覆:
美食小幫手:
🎲 命運的美食轉盤轉動中...
今日午餐就是它了:【港式燒臘大王】!✨
📍 地址:新北市板橋區...
💰 均消:約 120 元
⭐️ 評分:4.4
3.聊天室底部懸浮按鈕:
手機螢幕底部整齊彈出三顆膠囊狀的 Quick Reply 選項:
[ 🎲 再抽一次 ]
[ 🗺️ 查看地圖 ]
[ 🍜 改吃日式 ]
4.一鍵互動:
直接點擊 [ 🎲 再抽一次 ],無需動手打字,系統立刻觸發下一輪隨機盲盒抽籤!
今日結語與明日預告
今天我們為美食 Agent 注入了趣味感與極高黏著度的互動體驗:
設計了專屬的 隨機盲盒抽籤演算法,直接化解使用者的選擇障礙。
整合了 LINE Quick Reply 快速選單,消除了在手機上反覆打字的繁瑣。
透過 Dify 意圖標籤切換,實現普通推薦與轉盤模式的無縫流轉!
抽到喜歡的餐廳之後,很多使用者希望能把口袋名單保存起來,下次隨時調閱。
明天 Day 19,我們將要實作個人化收藏夾:擴充 SQLite 資料庫,實現「加入我的最愛」與「口袋名單歷史回顧」功能! 敬請期待!