第二次软件工程作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/SoftwareEngineering24 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/SoftwareEngineering24/homework/15646 |
| 这个作业的目标 | 构造一个能说会道,有记忆功能的智能体 |
| 学号 | 3124004565 3124005526 |
智能美食推荐助手 项目说明
一、项目简介
本项目为“智能美食推荐助手”,基于高德地图周边搜索 API 与 DeepSeek 大模型 API,实现基于用户地理位置、菜系偏好、场景需求(约会/聚餐/天气等)的个性化餐厅推荐。系统采用“真实 POI 数据 + 大模型生成推荐语”的双引擎架构,支持多维度筛选(距离、菜系、预算),并具备缓存机制与坐标兜底策略,确保服务稳定可用。配套 Web 前端提供直观交互,整体设计模块化、高容错。
项目仓库地址:https://github.com/dave66688/software-engeneering
项目运行成果:智能美食推荐助手(Web 端 http://127.0.0.1:2025/ ,网页相关端口尚未完善)
二、团队成员分工
成员1 钟启睿(后端与 API 集成)
负责 FastAPI 后端框架搭建(app.py);实现高德地图周边搜索 API 调用(amap_api.py、llm_api.py 中的 search_nearby_restaurants);集成 DeepSeek 大模型生成推荐语(llm_api.py 中的 chat_with_llm);设计缓存机制(10 分钟过期)与坐标兜底逻辑(默认广州坐标);处理跨域、异常捕获、服务降级(无餐厅时返回友好提示)。
成员2 盘嵘(前端与交互逻辑)
开发 HTML/CSS/JS 前端页面(index.html);实现浏览器 Geolocation 定位,支持超时/失败时降级为默认坐标;设计三模式交互界面:常规推荐(菜系+距离+预算)、场景推荐(自然语言描述)、天气推荐(按天气推荐);调用后端 /api/chat 与 /api/home_recommend 接口,动态渲染推荐结果;处理加载状态、错误提示及页面切换逻辑。
三、需求描述
基于用户地理位置(或默认坐标)搜索附近餐厅,按距离、评分排序展示。
支持用户指定菜系(火锅/日料/川菜等)、搜索半径(1~50 km)、人均预算。
支持自然语言场景描述(如“约会,2人,200元内”)生成个性化推荐。
支持根据天气(晴天/下雨/寒冷/炎热)智能推荐合适菜系和餐厅。
大模型生成推荐语时结合餐厅真实数据(名称、地址、距离、评分)。
服务异常(API Key 失效、网络超时)时返回明确错误信息,不崩溃。
首页自动加载 3 家附近餐厅作为快捷推荐,带缓存减少重复请求。
四、业务流描述
核心业务场景流
(1)场景1:首页附近餐厅推荐
步骤1:用户打开网页,前端调用浏览器 navigator.geolocation 获取经纬度(超时 10 秒或失败则降级为广州坐标 23.1200,113.3200)。
步骤2:前端 POST /api/home_recommend,携带 latitude、longitude。
步骤3:后端校验坐标,若为 0 则替换为默认广州坐标。
步骤4:检查缓存 cache["restaurants"][key],若存在且未过期(10 分钟内)直接返回前 3 家。
步骤5:缓存未命中则调用 search_nearby_restaurants(lat, lng, radius=10000, keywords="餐厅")。
内部请求高德周边搜索 API: https://restapi.amap.com/v3/place/around 。
参数包含 location(经度,纬度)、radius、types=050000(餐饮类)。
步骤6:解析返回的 POI 列表,提取名称、地址、距离、评分。
步骤7:将结果存入缓存,返回前 3 家给前端。
步骤8:前端渲染餐厅卡片(名称、评分、距离、地址)。
(2)场景2:常规推荐(菜系+距离+预算)
步骤1:用户在“常规推荐”页选择菜系(如“川菜”)、输入距离(如 10 km)、预算(如“100元”)。
步骤2:前端 POST /api/chat,参数包含 demand(组合文本“推荐10公里内的川菜,人均预算100元”)、latitude/longitude、radius(10)、cuisine(“川菜”)、scene="normal"。
步骤3:后端校验坐标并调用 search_nearby_restaurants(lat, lng, radius*1000, cuisine) 获取最多 20 家餐厅。
步骤4:将餐厅列表(前 10 家)格式化为文本(店名、地址、距离、评分)。
步骤5:构造 Prompt 调用 DeepSeek API(chat_with_llm)。
步骤6:DeepSeek 返回推荐语(如“1. 渝味轩:距离3.2km,评分4.5,人均80元,招牌水煮鱼”)。
步骤7:后端返回 {code:0, data: 推荐语},前端展示在结果区域。
(3)场景3:场景推荐(自然语言描述)
步骤1:用户在“场景推荐”页输入“约会,2人,200元内”,距离 15 km。
步骤2:前端 POST /api/chat,demand 直接为用户输入文本,scene 也传该文本。
步骤3:后端同样搜索附近餐厅(关键词默认“餐厅”,因未指定菜系)。
步骤4:DeepSeek 根据场景描述(约会)从餐厅列表中挑选环境好、适合约会的餐厅并给出理由。
步骤5:返回自然语言推荐结果。
(4)场景4:不知道吃什么(按天气推荐)
步骤1:用户输入当前天气(如“寒冷”)和搜索距离(如 50 km)。
步骤2:前端构造 demand = "当前天气寒冷,请根据天气推荐合适的餐厅和菜系。"。
步骤3:后端搜索餐厅后,DeepSeek 根据“寒冷”天气推荐热乎乎的食物(火锅、炖菜等)。
步骤4:返回推荐语。
(5)异常降级流程
定位失败:前端捕获错误,使用默认广州坐标,并打印日志。
高德 API 无结果:search_nearby_restaurants 返回空列表,后端返回 {"code": -1, "msg": "附近未找到符合条件的餐厅"}。
DeepSeek API 超时/错误:chat_with_llm 捕获异常,返回 "大模型服务异常,请稍后重试",前端显示该消息。
坐标兜底:若前端传的经纬度为 0(未定位成功),后端自动替换为广州坐标,保证功能可用。
前端交互流程(纯 HTML/JS)
步骤1:用户访问 http://localhost:2025 ,后端返回 index.html。
步骤2:页面加载时显示“正在获取你的位置…”,同时请求定位。
步骤3:定位成功/超时/失败后自动调用 /api/home_recommend 渲染首页推荐。
步骤4:用户可点击“跳过 → 更多选择”进入功能菜单页。
步骤5:在任一推荐页面填写参数,点击按钮发起异步 POST 请求。
步骤6:请求期间显示“正在生成推荐…”,收到响应后更新结果区域。
步骤7:提供“返回”按钮回到菜单页,整体无页面刷新。
五、实现说明
核心调度模块(app.py)
使用 FastAPI 构建 REST API,配置 CORS 允许所有来源(开发环境)。
全局缓存字典 cache,键为 "{lat}_{lng}",值为 (restaurants_list, timestamp),过期时间 600 秒。
两个主要接口:
POST /api/home_recommend:首页推荐(仅返回前 3 家原始 POI 数据)。
POST /api/chat:智能聊天推荐(调用大模型生成推荐语)。
坐标预处理:若 latitude0 或 longitude0,替换为广州默认坐标(23.1200, 113.3200)。
高德地图接口模块(llm_api.py 中的 search_nearby_restaurants)
异步函数,使用 aiohttp 请求周边搜索 API。
参数:lat(纬度)、lng(经度)、radius(米)、keywords(搜索关键词)。
固定 types=050000(餐饮类),page_size=20。
解析返回 JSON,提取 pois 列表,标准化为 {name, address, distance, score}。
异常时返回空列表,不中断主流程。
大模型调用模块(llm_api.py 中的 chat_with_llm)
异步函数,使用 aiohttp 请求 DeepSeek API。
构造 prompt:包含前 10 家餐厅的详细信息 + 用户需求 + 场景。
设置 temperature=0.7,max_tokens=1024。
成功则返回 choices[0].message.content,失败返回友好错误提示。
前端实现(index.html)
原生 HTML/CSS/JS,无第三方框架。
页面分为:加载页、首页推荐页、菜单页、常规推荐页、场景推荐页、天气推荐页、错误页。
使用 fetch 发起 POST 请求,Content-Type: application/json。
定位逻辑:navigator.geolocation.getCurrentPosition,超时 10 秒或失败时使用默认坐标。
六、关键代码说明
- 高德地图周边搜索(llm_api.py)
async def search_nearby_restaurants(lat, lng, radius, keywords):
url = "https://restapi.amap.com/v3/place/around"
params = {
"key": AMAP_KEY,
"location": f"{lng},{lat}", # 注意:经度在前,纬度在后
"radius": radius,
"keywords": keywords,
"types": "050000", # 餐饮类别代码
"output": "json",
"page_size": 20
}
async with aiohttp.ClientSession() as session:
async with session.get(url, params=params, timeout=10) as resp:
data = await resp.json()
pois = data.get("pois", [])
return [{"name": poi["name"], "address": poi["address"],
"distance": poi["distance"], "score": float(poi.get("rating", 4.0))}
for poi in pois]
说明:
使用高德周边搜索 API,location 参数格式为“经度,纬度”(易错点)。
types=050000 限定餐饮类 POI。
异步超时 10 秒,异常时返回空列表,保证主流程不崩溃。
将高德返回的评分(字符串)转为 float,缺失时默认 4.0。
- 缓存机制(app.py)
cache = {"restaurants": {}, "expire": 600}
key = f"{lat}_{lng}"
now = time.time()
if key in cache["restaurants"]:
data, t = cache["restaurants"][key]
if now - t < cache["expire"]:
return {"code": 0, "data": data[:3]} # 命中缓存,直接返回前3家
说明:
以经纬度组合作为缓存键,避免不同用户/不同位置相互污染。
缓存有效期 600 秒(10 分钟),平衡实时性与 API 调用成本。
首页推荐只取缓存中前 3 家,减少传输量。
- 坐标兜底策略(app.py 与前端配合)
后端兜底:
if request.latitude == 0 or request.longitude == 0:
lat = 23.1200 # 广州纬度
lng = 113.3200 # 广州经度
前端兜底:
navigator.geolocation.getCurrentPosition(
successCallback,
(error) => {
userLat = 23.1200;
userLon = 113.3200;
loadHomeRecommend(); // 降级继续
}
);
说明:
前端定位失败(用户拒绝、超时、不支持)时自动使用广州坐标。
后端再次校验,若收到的坐标为 0 也替换为广州坐标,双重保障。
- DeepSeek 大模型调用(llm_api.py)
async def chat_with_llm(demand, restaurants, scene="normal"):
rest_text = "\n".join([
f"店名:{r['name']},地址:{r['address']},距离:{r['distance']}米,评分:{r['score']}"
for r in restaurants[:10]
])
prompt = f"你是专业美食推荐官。\n周边餐厅信息:\n{rest_text}\n\n用户需求:{demand}\n场景:{scene}\n\n请直接给出推荐,分点列出,语言自然口语。"
async with aiohttp.ClientSession() as session:
async with session.post(
"https://api.deepseek.com/v1/chat/completions",
headers={"Authorization": f"Bearer {DEEPSEEK_KEY}", "Content-Type": "application/json"},
json={"model": "deepseek-chat", "messages": [{"role": "user", "content": prompt}],
"temperature": 0.7, "max_tokens": 1024},
timeout=30
) as resp:
res = await resp.json()
return res["choices"][0]["message"]["content"].strip()
说明:
只取前 10 家餐厅信息,避免 token 超限。
Prompt 中明确角色(美食推荐官)和输出格式(分点列出),提高生成质量。
设置 temperature=0.7 保持一定多样性但不至离谱。
异常捕获后返回友好错误信息,不影响前端展示。
- 前端异步请求与渲染(index.html)
async function searchNormal() {
const cuisine = document.getElementById("cuisine").value;
const distanceKm = parseFloat(document.getElementById("normalDist").value);
const budget = document.getElementById("normalBudget").value;
const demand = 推荐${distanceKm}公里内的${cuisine},人均预算${budget || "不限"};
const response = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
demand: demand,
latitude: userLat,
longitude: userLon,
radius: distanceKm,
cuisine: cuisine,
scene: "normal"
})
});
const data = await response.json();
document.getElementById("normalResult").innerText = data.data;
}
说明:
将用户输入的菜系、距离、预算拼接成自然语言需求,传给后端。
使用 fetch 异步提交,不刷新页面。
根据返回的 data.data 直接显示大模型生成的推荐语(纯文本)。
三个推荐模式(常规、场景、天气)复用同一个 /api/chat 接口,仅 demand 和 scene 不同。
- 首页推荐与缓存联动
@app.post("/api/home_recommend")
async def home_recommend(request: ChatRequest):
key = f"{lat}_{lng}"
if key in cache["restaurants"]:
data, t = cache["restaurants"][key]
if now - t < cache["expire"]:
return {"code": 0, "data": data[:3]} # 直接返回缓存
restaurants = await search_nearby_restaurants(lat, lng, 10000, request.cuisine or "餐厅")
cache["restaurants"][key] = (restaurants, now)
return {"code": 0, "data": restaurants[:3]}
说明:
首页推荐独立于聊天推荐,只返回原始 POI 数据(不调用大模型),速度更快。
搜索半径固定为 10 公里,关键词默认“餐厅”。
利用缓存减少高德 API 压力,同一位置的重复访问几乎无延迟。
七、开发问题与解决方案
-
高德 API 坐标顺序错误
问题:location 参数要求“经度,纬度”,但代码传了“纬度,经度”。
解决:修改 llm_api.py 中 params["location"] = f"{lng},{lat}"。 -
首页推荐缓存未命中时重复请求
问题:每次刷新页面都会重新请求高德 API。
解决:增加缓存机制,键基于经纬度,过期时间 10 分钟,显著降低 API 调用频率。 -
前端定位超时或拒绝后无默认坐标
问题:用户拒绝定位权限后,前端 userLat/userLon 仍为 null,导致请求体为 0。
解决:在定位失败回调中主动设置为广州坐标(23.1200, 113.3200),同时后端也做兜底处理。 -
DeepSeek API Key 环境变量加载失败
问题:.env 文件中的变量名不一致(DEEPSEEK_KEY vs DEEPSEEK_API_KEY)。
解决:统一使用 DEEPSEEK_KEY,并在 test_deepseek.py 中验证。 -
跨域请求被阻止(CORS)
问题:前端与后端不同端口时浏览器拦截。
解决:在 FastAPI 中添加 CORSMiddleware,设置 allow_origins=["*"]。 -
高德 API 返回的评分是字符串,比较时出错
解决:在 search_nearby_restaurants 中增加 float() 转换,异常时默认 4.0。
八、项目总结与心得体会
钟启睿(后端与 API 集成)
本次我负责后端框架搭建、高德地图与 DeepSeek API 集成以及缓存设计。从零开始实现了异步 API 调用、异常捕获、坐标兜底等关键功能。最大的收获是理解了真实业务系统中“容错”的重要性——不能假设所有上游服务都稳定,必须在每一层做好降级预案(如默认坐标、缓存、空列表处理)。此外,调试高德 API 参数(经纬度顺序、radius 单位)让我深刻体会到阅读官方文档和编写测试脚本的价值。通过本项目,我熟练掌握了 FastAPI + aiohttp 的高并发异步编程模式,也为后续开发更复杂的 Agent 系统积累了实战经验。
盘嵘(前端与交互逻辑)
我负责前端页面开发、定位集成与三个推荐模式的交互实现。从零开始用原生 JS 完成了多页面切换、异步请求、加载状态管理等功能。实践中遇到的最大挑战是定位 API 的超时处理和降级逻辑——需要保证用户即使在拒绝定位或网络慢的情况下也能正常使用。通过设计“定位失败自动使用广州坐标”的策略,并结合后端的双重兜底,最终实现了流畅的用户体验。此外,我还学会了如何构造适配大模型的 prompt(

浙公网安备 33010602011771号