地图富化与评分模块的前后端集成实践整合记录。

一、整体数据流:一条房源信息要经过哪些关卡

爬虫竞速(安居客/链家/房天下) → 标准化 → 地址构造 → 地图富化 → 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事件即可。这一步设计避免了前端做脆弱的文本正则匹配。

更多推荐