项目实训——大数据租房推荐智能体——地图通勤评分(八)
地图富化与评分模块的前后端集成实践整合记录。
一、整体数据流:一条房源信息要经过哪些关卡
爬虫竞速(安居客/链家/房天下) → 标准化 → 地址构造 → 地图富化 → 6维评分 → 流式推送前端
二、地图模块:不止是调 API
最核心的工程问题是高德 API 的 QPS 限制。免费版限制 30 次/秒,而每条房源需要调用 5 个接口(地理编码 + 4 类 POI 搜索 + 2 条步行路径)。如果不加控制,连续请求 20 条房源就是 140 次 API 调用,几乎必然触发限流。
# map_service.py — enrich_batch 方法
total = len(listings)
enriched: List[Dict[str, Any]] = []
for idx, listing in enumerate(listings):
result = await self.enrich_listing(listing)
enriched.append(result)
print(f"地图富化完成: ({idx + 1}/{total})")
if idx < len(listings) - 1:
await asyncio.sleep(delay) # delay 默认 0.15s
设计要点:
- 150ms 间隔:5 次调用/条 × 0.15s ≈ 33 次/秒峰值,在 30 QPS 边缘留有缓冲
- 进度可见:终端输出 `地图富化完成: (3/20)`,排查问题时一眼能看出卡在哪条
- 不设并发:顺序处理而非 asyncio.gather,原因是高德 API 限流按全局维度计算,并发反而容易触发阈值
返回值的"双格式"设计
# 通勤数据同时返回格式化字符串和数值
return {
"distance": "450米", # 给人看
"duration": "5分钟", # 给人看
"distance_meters": 450, # 给算法算分
"duration_minutes": 5, # 给算法算分
}
这个细节很重要:评分引擎不能消费字符串,前端不能消费裸数值。两种格式同时返回,下游各取所需,避免重复解析。
三、地图富化板块的数量与数据问题
为什么地图富化只做前 N 条
爬虫可能返回 100+ 条房源,全部做地图富化会消耗大量 API 调用(每条 7 次高德请求)且时间过长。`max_enrich` 默认 20 是一个经验值:
- 足够覆盖用户实际会查看的推荐数量(top_n 默认 10)
- 在高德免费额度内(每日 5000 次 → 够约 714 条房源,一天够用)
- 单次请求总耗时约 20 条 × (5 次 API × 200ms + 150ms) ≈ 23 秒,用户可接受
标准化与地址构造:为地图富化做数据准备
标准化器处理爬虫间的不一致:
# normalizer.py 处理的核心差异
"6200元/月" → 6200.0 # 价格:去"元/月"后缀
"42㎡" → 42.0 # 面积:去单位
"朝南" → "南" # 朝向:去"朝"前缀
"sh" → "上海" # 城市:拼音→中文
"jingan" → "静安" # 区域:拼音→中文
地址构造器把分散字段拼成可地理编码的地址:
# address_builder.py
# "上海" + "浦东新区" + "浦东新区-张江-张江路100号"
# → "上海市浦东新区张江张江路100号"(自动去重 district 前缀)
这两个步骤看起来简单,但在三平台对接中意义重大:三个爬虫的城市/区域字段格式各不相同(中文 vs 拼音 vs 英文缩写),没有标准化和地址构造,地图富化的地理编码会大量失败。
四、前端集成:从 JSON 到可视化
三层架构的"翻译"问题
FastAPI (Python) ──JSON──▶ Express (TypeScript) ──SSE──▶ Streamlit (Python)
数据在三层之间传递,每层的数据模型和字段命名习惯不同。核心挑战是让 Python 爬虫产出的 ScoreResult 最终能在前端正确渲染成卡片和地图标记。
TypeScript 中间层的路由职责
Express 服务器不只是转发请求的 proxy,它承担了关键的**结构化数据提取**工作:
// reactAgent.ts — 从 scoring_recommend 工具调用中提取房源列表
if (toolSucceeded && toolCall.name === "scoring_recommend") {
const persisted = await persistScoringRecommendToolResult(observation);
if (persisted) {
step.listings = persisted.payload.items; // 房源列表 → 前端
step.listingsPath = persisted.latestPath; // 持久化路径 → 调试用
}
}
房源列表以专门的结构化事件推送:
// server/index.ts
res.write(`data: ${JSON.stringify({
type: "listings",
items: listingsForStream,
storagePath: listingsPathForStream,
})}\n\n`);
这样前端不需要解析 LLM 的文本输出来提取房源信息,直接处理结构化listings事件即可。这一步设计避免了前端做脆弱的文本正则匹配。
更多推荐
所有评论(0)