基于阿里云 Milvus 复刻“高德扫街榜”

作者:周弘懿(锦琛)

业务背景

「附近有什么好吃的?」------这是本地生活场景里最高频的一句发问。高德「扫街榜」这类产品之所以有说服力,核心在于两点:它离你近,且它是被真实吃过的人夸出来的,而不是靠商家刷分堆出来的。

把这类榜单拆开看,它需要回答三个层层递进的问题:

  • 就近:以我当前位置为圆心,方圆几公里内、或者我正身处的这个商圈范围内,有哪些店?

  • 对味 :这些店里,哪些符合我此刻的需求?比如「适合亲子的川菜馆」「安静能聊事的咖啡馆」「深夜还开着的苍蝇馆子」------这是一种语义需求,无法用标签精确枚举。

  • 靠谱:在符合前两条的店里,真实评价的口碑如何?好评率多少?大家反复提到的优点/槽点是什么?

它的业务价值也就落在这三层上:帮用户发现附近的好店 、把「近 + 对味 + 口碑好」融合成一张可解释的榜单 、并且基于真实评价的语义与情感而非刷分数据。要把这三层能力捏合到一次查询里,传统技术栈会相当吃力。

技术挑战

要做出这样一张榜单,系统必须同时具备以下能力,缺一不可:

  1. 地理空间过滤。既要支持「距离我某坐标 N 公里内」的半径查询,也要支持「是否落在某商圈多边形内」的范围判断,还要能判断门店与配送区域是否相交。这需要把 POI 的经纬度当作真正的几何对象来存储与运算。

  2. 评价文本的语义检索。用户输入「适合亲子的川菜馆」,系统要能理解语义、召回语义相近的评价与门店,而不是做关键词精确匹配------「带孩子」「亲子友好」「有儿童椅」都应该被命中。

  3. 海量评价的情感与摘要理解 。要在店铺维度上把成千上万条评价聚合成一个好评率 ,并生成一段店铺评价摘要,让榜单结果可读、可信。

  4. 相关性精排。语义召回的候选集里,排序还需要按 Query 的真实相关性重排,把「最对味」的店顶到前面。

传统方案通常是三套系统拼接:用 PostGIS / 空间数据库做地理过滤,用向量数据库做语义召回,再用一套 NLP 服务(情感分类、摘要)做评价理解。三套系统各存一份数据,跨系统 JOIN、数据同步、一致性维护的成本极高,一次榜单查询要串起多个服务、多轮网络往返,工程复杂度和延迟都难以接受。

解决方案

阿里云 Milvus 可以把上面四件事收敛到一个 Collection、一次检索里完成:

  • 地理 :POI 经纬度用 Milvus 的地理空间类型 GEOMETRY 存储、商圈为 POLYGON。检索时把地理约束写进 filter 表达式,用 ST_WITHIN / ST_CONTAINS / ST_INTERSECTS 等空间关系函数完成范围/相交过滤。Milvus 2.6 为 GEOMETRY 字段提供了基于 R-Tree 的地理位置索引来加速空间查询。

  • 语义 :评价文本通过 AI_EMBEDDING在写入时自动向量化,检索时对同一模型生成的 Query 向量做 ANN 检索。

  • 精排 :召回候选用 AI_RERANK,按 Query 相关性重排。

  • 口碑 :用 AI_SENTIMENT 对评价做情感分类,应用侧聚合出好评率 ;用 AI_SUMMARIZE 把门店评价压缩成一段评价摘要

最关键的一点:地理过滤与语义检索融合在同一次 search() 调用里 ------ANN 向量检索负责「对味」,filter 里的 ST_ 空间函数负责「就近」,二者在一次请求内联合求解,再叠加 rerank 精排,直接产出榜单候选。

先对齐一下文中几个 AI Function 的作用:

函数 作用 在扫街榜里
AI_EMBEDDING 写入时自动把评价文本转成向量(无需应用侧先调模型) 让「适合亲子的川菜馆」这类语义需求能被向量检索命中
AI_RERANK 对召回候选按与查询的相关性重新排序(精排) 把「最对味」的店顶到榜单前面
AI_SENTIMENT 判断每条评价是好评 / 差评 / 中性 聚合出门店「好评率」
AI_SUMMARIZE 把成百上千条评价压成一句话摘要 生成榜单里「一句话点评」

具体AI Function用法参考官方文档

架构流程如下:

关键概念:GEOMETRY、WKT 与 ST_ 空间函数

在进入实战代码前,先把几个贯穿全文的地理概念讲清楚。

GEOMETRY ------ 把「地点」当成几何图形存起来。 普通做法是把经纬度存成两个 double 字段,数据库只把它当两个数字,并不理解「谁在谁里面、两块区域是否相交」。GEOMETRY 是 Milvus 2.6 的地理空间类型,它把「点 / 线 / 面」当作真正的几何对象存储,于是就能用 ST_* 函数做空间运算;Milvus 2.6 还为它提供了基于 R-Tree 的空间索引来加速。

WKT ------ 描述几何图形的标准文本写法。 GEOMETRY 字段里存的值用 WKT 表示,常见几种:

WKT 类型 写法示例 通俗含义 扫街榜里对应
POINT POINT(116.4612 39.9088) 一个点 一家门店的经纬度
LINESTRING LINESTRING(116.46 39.90, 116.47 39.91) 一条折线 一段道路 / 配送路径
POLYGON POLYGON((116.448 39.905, ..., 116.448 39.905)) 一片闭合区域(首尾坐标相同) 一个商圈 / 配送范围
MULTIPOINT / MULTIPOLYGON MULTIPOLYGON(((...)),((...))) 多个点 / 多片区域 一个品牌多家门店、多块行政区

坐标顺序是「经度在前、纬度在后」(POINT(lng lat)),和口语「北纬...东经...」相反。本文坐标均为 WGS84(GPS 经纬度)。

ST_ 开头的都是空间关系函数 :写进 filter 里判断门店几何与给定区域的关系,返回真 / 假。本文重点用到三个:

函数 一句话解释 扫街榜里的用途
ST_WITHIN(A, B) A 是否完全落在 B 内部 门店是否在商圈多边形内 / 是否在 3km 半径圆内
ST_CONTAINS(A, B) A 是否完全包住 B(与 WITHIN 相反) 某商圈是否包含这家店,做归属分析
ST_INTERSECTS(A, B) A 与 B 是否有任意交集 / 接触 门店配送范围是否与用户所在网格相交

Milvus 2.6 还有其余几个函数,可按需选用:ST_EQUALS(完全相同,去重)、ST_TOUCHES(只碰边界,如相邻商圈接壤)、ST_OVERLAPS(部分重叠,如配送区交叉)、ST_CROSSES(穿越式相交,如道路穿过商圈)。一句话记区别:WITHIN=在里面、CONTAINS=把它装住、INTERSECTS=有任何交集(最宽松)、TOUCHES=只碰边、OVERLAPS=叠一部分、CROSSES=穿过去、EQUALS=一模一样。

为了让 POINTPOLYGONST_WITHIN 更直观,下面把上面用到的示意坐标画到真实地图 上(底图为高德地图,正文的 WGS84 坐标已做坐标系转换以精确对齐)。图一 为 3km 半径圆与商圈范围的总览;图二 放大到商圈级别,可见三家门店都落在商圈 POLYGON 内------正是 ST_WITHIN 判定为「真」的情形。

实战操作

下面用代码表示「建库 → 入库 → 地理过滤 → 语义融合 → 精排 → 情感/摘要」全流程。

  1. 建带地理字段 + embedding Function 的 CollectionlocationGEOMETRY 存 POI 点,reviewAI_EMBEDDING,写入即向量化。

  2. POI 与评价数据入库 :写入门店 POINT 与评价原文,review_embedding 由 Function 自动生成,无需手工传向量。

  3. 地理过滤查询 :用 ST_WITHIN 判断门店是否落在商圈 POLYGON 或 3km 近似圆内(半径圆由 circle_to_wkt 近似成多边形)。

  4. 语义检索 + 地理 filter 融合 :把「就近」(filter 里的 ST_)与「对味」(ANN 向量检索)融合进同一次 search()

  5. AI_RERANK 重排候选 :按 Query 相关性精排,给出 search() 内挂 ranker 与 REST 即时接口两种写法。

  6. AI_SENTIMENT + AI_SUMMARIZE:情感分类聚合出好评率,摘要生成店铺一句话点评。

将MILVUS_HOST和MILVUS_TOKEN替换成自己的集群就可以直接看到效果

python 复制代码
from __future__ import annotations

import json
import math
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from pymilvus import DataType, Function, FunctionType, MilvusClient

# ==================== 连接配置 ====================
MILVUS_HOST = "http://c-xxx.milvus.aliyuncs.com"
MILVUS_TOKEN = "root:xxx"
# RESTful 接口与 gRPC 同在 19530 端口,需显式带端口(否则默认 80 端口会连接超时)
MILVUS_REST_BASE_URL = f"{MILVUS_HOST}:19530"

client = MilvusClient(uri=MILVUS_HOST, token=MILVUS_TOKEN)
collection_name = "shop_reviews"


def post_json(path: str, body: dict[str, Any], timeout: int = 120, retries: int = 3) -> tuple[int, dict[str, Any]]:
    """AI_RERANK / AI_SENTIMENT / AI_SUMMARIZE 的 REST 即时接口统一封装(走大模型,加重试更稳)。"""
    last = None
    for _ in range(retries):
        request = Request(
            f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
            data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
            headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},
            method="POST",
        )
        try:
            with urlopen(request, timeout=timeout) as response:
                status, data = response.status, json.loads(response.read().decode("utf-8"))
        except HTTPError as exc:
            status, data = exc.code, json.loads(exc.read().decode("utf-8"))
        last = (status, data)
        if status == 200 and data.get("code") == 0:
            return status, data
        time.sleep(1)
    return last


def circle_to_wkt(lng: float, lat: float, radius_km: float, num_seg: int = 32) -> str:
    """把以 (lng,lat) 为圆心、radius_km 为半径的圆,近似成正多边形的 WKT(用于半径粗过滤)。
    注意:经纬度是角度,此处按平面近似换算,纬度越高误差越大。"""
    d_lat = radius_km / 111.0
    d_lng = radius_km / (111.0 * math.cos(math.radians(lat)))

    pts = [ ]

    for i in range(num_seg + 1):
        theta = 2 * math.pi * i / num_seg
        pts.append(f"{lng + d_lng * math.cos(theta):.6f} {lat + d_lat * math.sin(theta):.6f}")
    return f"POLYGON(({', '.join(pts)}))"


# ==================== 步骤 1:建带地理字段 + embedding Function 的 Collection ====================
# location 用 DataType.GEOMETRY 存 POI 点;review 挂 AI_EMBEDDING,写入时自动生成 1024 维向量
if client.has_collection(collection_name):
    client.drop_collection(collection_name)

schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("shop_id", DataType.INT64)
schema.add_field("shop_name", DataType.VARCHAR, max_length=256)
schema.add_field("category", DataType.VARCHAR, max_length=64)
schema.add_field("location", DataType.GEOMETRY)                       # POI 经纬度点(WKT: POINT(lng lat))
schema.add_field("review", DataType.VARCHAR, max_length=4096)         # 评价原文,作为 embedding 输入
schema.add_field("review_embedding", DataType.FLOAT_VECTOR, dim=1024)  # 评价向量,维度需与 dim 一致

# 写入时自动把 review 向量化
schema.add_function(
    Function(
        name="embed_review",
        function_type=FunctionType.TEXTEMBEDDING,
        input_field_names=["review"],
        output_field_names=["review_embedding"],
        params={
            "provider": "aliyun_milvus",
            "model_name": "qwen3.7-text-embedding",
            "dim": 1024,
            "max_client_batch_size": 20,   # qwen3.7-text-embedding 百炼官方文档批量大小为 20
            "max_concurrency": 1,
        },
    )
)

index_params = client.prepare_index_params()
index_params.add_index(
    field_name="review_embedding",
    index_type="HNSW",
    metric_type="COSINE",
    params={"M": 16, "efConstruction": 200},
)
client.create_collection(
    collection_name=collection_name,
    schema=schema,
    index_params=index_params,
)


# ==================== 步骤 2:POI 与评价数据入库(review_embedding 由 Function 自动生成,无需手工传向量) ====================
rows = [
    {"shop_id": 1001, "shop_name": "巴蜀人家·亲子川菜", "category": "川菜",
     "location": "POINT(116.4612 39.9088)",
     "review": "带娃来的,有儿童座椅和小碗餐具,毛血旺不算太辣,服务员很有耐心,环境干净。"},
    {"shop_id": 1001, "shop_name": "巴蜀人家·亲子川菜", "category": "川菜",
     "location": "POINT(116.4612 39.9088)",
     "review": "周末人多要等位,但菜品稳定,适合一家人聚餐,孩子吃得开心。"},
    {"shop_id": 1002, "shop_name": "麻辣江湖", "category": "川菜",
     "location": "POINT(116.4501 39.9101)",
     "review": "重口味爱好者天堂,水煮鱼够味,但空间偏小、比较吵,不太适合带小孩。"},
    {"shop_id": 1003, "shop_name": "静巷咖啡", "category": "咖啡",
     "location": "POINT(116.4550 39.9075)",
     "review": "环境安静适合谈事,手冲不错,不过跟川菜需求无关。"},
]
client.insert(collection_name, rows)
client.flush(collection_name)
client.load_collection(collection_name)


# ==================== 步骤 3:地理过滤查询(ST_WITHIN 商圈多边形 / 半径近似圆) ====================
# 3.1 查询某商圈多边形范围内的门店
cbd_polygon = "POLYGON((116.448 39.905, 116.465 39.905, 116.465 39.914, 116.448 39.914, 116.448 39.905))"
hits = client.query(
    collection_name=collection_name,
    filter=f"ST_WITHIN(location, '{cbd_polygon}')",
    output_fields=["shop_name", "category", "location"],
    limit=100,
)
print("\n" + "=" * 64)
print("步骤 3 | 地理过滤查询  ST_WITHIN(location, 多边形 / 半径圆)")
print("=" * 64)
print("[3.1] CBD 商圈多边形范围内的门店:")
for h in hits:
    print(f"      · {h['shop_name']}({h['category']})  {h['location']}")

# 3.2 附近 3km 半径查询:把圆近似成多边形,再用 ST_WITHIN 判断 POI 是否落在圈内
center_lng, center_lat = 116.4600, 39.9090
circle = circle_to_wkt(center_lng, center_lat, radius_km=3.0)
nearby = client.query(
    collection_name=collection_name,
    filter=f"ST_WITHIN(location, '{circle}')",
    output_fields=["shop_name", "category", "location"],
    limit=200,
)
print(f"[3.2] 中心点 ({center_lng}, {center_lat}) 附近 3km 半径内的门店:")
for n in nearby:
    print(f"      · {n['shop_name']}({n['category']})  {n['location']}")


# ==================== 步骤 4:语义检索 + 地理 filter 融合进一次 search() ====================
# ANN 走评价向量负责「对味」,filter 里的 ST_ 空间函数负责「就近」,一次请求联合求解
QUERY = "附近适合亲子的川菜馆"
geo_filter = f"category == '川菜' && ST_WITHIN(location, '{circle}')"
results = client.search(
    collection_name=collection_name,
    data=[QUERY],
    anns_field="review_embedding",
    filter=geo_filter,
    limit=20,
    output_fields=["shop_id", "shop_name", "category", "location", "review"],
)
print("\n" + "=" * 64)
print("步骤 4 | 语义检索 + 地理过滤 融合查询(一次 search 联合求解)")
print("=" * 64)
print(f"查询语句:{QUERY}")
print("过滤条件:category == '川菜'  且落在附近 3km 圈内")
print("-" * 64)
for rank, hit in enumerate(results[0], 1):
    e = hit["entity"]
    print(f"  {rank}. [相似度 {hit['distance']:.4f}]  {e['shop_name']} | {e['review'][:24]}")


# ==================== 步骤 5:AI_RERANK 重排候选 ====================
RERANK_MODEL = "qwen3-rerank"

# 5.1 在 search() 中挂 RERANK ranker(reranker="model"),ANN 召回后由模型二次精排
reranker = Function(
    name="rerank_reviews",
    function_type=FunctionType.RERANK,
    input_field_names=["review"],
    params={
        "reranker": "model",
        "provider": "aliyun_milvus",
        "model_name": RERANK_MODEL,
        "queries": [QUERY],
        "timeout_sec": 10,
    },
)
results = client.search(
    collection_name=collection_name,
    data=[QUERY],
    anns_field="review_embedding",
    filter=geo_filter,
    limit=20,
    output_fields=["shop_id", "shop_name", "review"],
    ranker=reranker,
)
print("\n" + "=" * 64)
print("步骤 5 | AI_RERANK 大模型重排(qwen3-rerank)")
print("=" * 64)
print("[5.1] search() 内置 ranker:ANN 召回后由大模型二次精排")
print("-" * 64)
for rank, hit in enumerate(results[0], 1):
    e = hit["entity"]
    print(f"  {rank}. [重排分 {hit['distance']:.4f}]  {e['shop_name']} | {e['review'][:24]}")

# 5.2 也可用无状态 REST 即时接口 /v2/vectordb/ai/rerank
#     (documents 必须是 string 数组、query 为顶层 string、不支持 top_n,每个 doc 返回一个分)
rerank_docs = [
    "带娃来的,有儿童座椅和小碗餐具,服务员很有耐心。",
    "重口味爱好者天堂,水煮鱼够味,但空间偏小、比较吵,不太适合带小孩。",
]
status, data = post_json(
    "/v2/vectordb/ai/rerank",
    {
        "model_name": RERANK_MODEL,
        "query": QUERY,
        "documents": rerank_docs,
        "params": {"timeout_sec": 10},
    },
)
assert status == 200 and data.get("code") == 0, data
print(f"\n[5.2] REST 即时接口 /v2/vectordb/ai/rerank(query:{QUERY})")
print("-" * 64)
ranked = sorted(data["data"]["output"]["results"], key=lambda x: x["relevance_score"], reverse=True)
for rank, item in enumerate(ranked, 1):
    print(f"  {rank}. [相关度 {item['relevance_score']:.4f}]  {rerank_docs[item['index']][:28]}")


# ==================== 步骤 6:AI_SENTIMENT 算好评率 + AI_SUMMARIZE 生成店铺摘要 ====================
shop_id = 1001
reviews = [r["review"] for r in client.query(
    collection_name=collection_name,
    filter=f"shop_id == {shop_id}",
    output_fields=["review"],
    limit=1000,
)]

# 6.1 情感分类 → 好评率
status, sent = post_json("/v2/vectordb/ai/sentiment", {
    "model_name": "qwen3.7-max",
    "texts": reviews,
    "params": {"categories": ["positive", "negative", "neutral"], "temperature": 0},
})
assert status == 200 and sent.get("code") == 0, sent
labels = sent["data"]["output"]["outputs"]
valid = [x for x in labels if x]                      # null 计入「未判定」,不参与聚合
positive = sum(1 for x in valid if x == "positive")
good_rate = positive / len(valid) if valid else 0.0
print("\n" + "=" * 64)
print(f"步骤 6 | AI_SENTIMENT 好评率 + AI_SUMMARIZE 摘要(shop_id={shop_id})")
print("=" * 64)
print(f"[6.1] 好评率:{good_rate:.0%}   (有效样本 {len(valid)}/{len(labels)})")

# 6.2 摘要 → 店铺评价一句话
status, summ = post_json("/v2/vectordb/ai/summarize", {
    "model_name": "qwen3.7-max",
    "texts": ["\n".join(reviews)],
    "params": {"max_words": 40, "temperature": 0,
               "prompt": "用中文概括这家店的整体评价,突出招牌菜、适合人群与环境。"},
})
assert status == 200 and summ.get("code") == 0, summ
print(f"[6.2] 店铺摘要:{summ['data']['output']['outputs'][0]}")

实战效果

把上面的地理过滤 → 语义检索 → rerank 精排 → 情感/摘要 串起来,就是一次完整的扫街榜生成:地理过滤圈定「就近」候选 → 语义检索命中「对味」→ rerank 精排 → 情感/摘要补齐「口碑」。下面是一份示例榜单输出

扫街榜的排序逻辑由三路信号融合:

  • 地理分:在半径/商圈内为门槛(不满足直接过滤掉),再按距离由近到远给基础权重;

  • 语义相关分AI_RERANK 输出的 relevance_score,衡量门店评价与用户需求「适合亲子的川菜馆」的贴合度;

  • 口碑分AI_SENTIMENT 聚合出的好评率。

最终榜位可用形如 w1·相关分 + w2·好评率 + w3·距离衰减 的加权融合,权重按业务调参。「巴蜀人家」虽然不是最辣,但因「亲子友好」的语义强命中 + 高好评率 + 距离最近,综合排到第一------这正是「扫街榜」区别于「按评分排序」的地方。

与传统多系统方案对比,简化非常直观:

维度 传统方案(PostGIS + 向量库 + NLP 服务) 阿里云 Milvus 一站式
系统数量 3+ 套,各存一份数据 1 套,数据同源
地理 + 语义融合 跨系统 JOIN / 多轮回捞 一次 search():ANN + ST_ filter
评价理解 自建/外接情感、摘要模型 AI_SENTIMENT / AI_SUMMARIZE 内置
向量化链路 应用侧调 embedding 再写入 写入即向量化(Collection Function)
一致性/同步 需自行维护 单库天然一致

总结

这套方案的价值可以收敛为一句话:把「空间」与「语义」放进同一个数据库、同一次检索里求解。地理空间类型让 Milvus 理解「哪里」,AI Function 让它理解「什么」和「好不好」,二者融合直接产出可解释的本地生活榜单,省掉了 PostGIS + 向量库 + NLP 三套系统的拼接与同步成本。

顺着这条路还有不少想象空间:

  • 多模态门店检索 :用 qwen3-vl-embedding 把门店图片/菜品图向量化,支持「附近有这种装修风格的店」的以图搜店,与地理过滤同样融合在一次检索里。

  • 批量口碑加工:门店评价体量大时,用批量方式(如 AI_BATCH / 批量摘要)离线把好评率、评价摘要预计算落库,榜单查询只读结果,兼顾成本与延迟。

  • 更丰富的空间关系 :用 ST_INTERSECTS 判断门店配送多边形与用户所在网格是否相交、用 ST_CONTAINS 做商圈归属分析,把「地理 + AI」的融合从「找店」扩展到调度、选址、履约等更多本地生活环节。

地理让 AI「落到地面」,AI 让地理「读懂内容」------当这两者在一个系统里协同,本地生活的每一次「附近有什么好吃的」,都能被更快、更准、更可信地回答。

让 AI 落地,不必再从复杂链路开始

阿里云 Milvus 是目前唯一支持 AI Native 的 Milvus 云服务。它不仅提供高性能、全托管的向量检索能力,更将 AI Function 与 AI-Gateway 原生嵌入数据链路,让模型能力真正成为数据库的一部分。

从数据进入阿里云Milvus 的那一刻起,理解、加工、检索和生成便可以在一条原生链路中完成。阿里云 Milvus 正在把"接入 AI"变成"原生拥有 AI"。

相关推荐
秦先生在广东1 小时前
Archify:让 AI Agent 直接在对话中生成可交互、可验证架构图的 Skill
人工智能
゛凌乱的记忆づ1 小时前
50元从零到成品:一套AI辅助的嵌入式实战入门教程——基础工程篇
人工智能
无凭1 小时前
DeerFlow 的可观测性(一):RunJournal 如何记录 Agent 运行过程
人工智能·开源
迷迭香yy1 小时前
行业板块轮动因子实战从板块资金到因子建模的本地化Python全流程
数据库·人工智能·python
牛奶咖啡132 小时前
AI助力运维——AIGC运维应用实践—Deepseek的介绍与本地部署选型
运维·人工智能·deepseek·deepseek能做什么·deepseek本地部署配置·本地部署选型避坑原则·本地部署的典型方案
阿里云大数据AI技术2 小时前
阿里云 Milvus 知识库开启邀测,助力客户构建企业级 Agent
人工智能·agent
正经教主2 小时前
AI提示词工程(进阶)第7课:角色设定与身份模拟
人工智能
lucky_syq2 小时前
第3篇 · S1·上:什么是大语言模型 + Transformer 架构深讲
人工智能·语言模型·架构·transformer
小玮看世界2 小时前
当“安全“变成“教训“:《拟人化暂行办法》时代,AI护栏的过度拒答之困与破局
大数据·人工智能