RAG 在旅行场景的实战:让 AI 回答「这个地方怎么玩」更靠谱

0 阅读5分钟

用户问「西安三天怎么玩」,纯大模型给你的答案永远是兵马俑、大雁塔、回民街三件套——因为它读过的训练语料里,西安就是这三个景点。但如果你真的按它说的走一遍,会发现它不知道兵马俑现在要预约、不知道回民街本地人不去、不知道冬天华山封不封。RAG 的价值就在这里:让模型基于你喂给它的、最新、最具体的语料来回答,而不是凭训练记忆瞎编。今天用旅行场景拆解 RAG 的实战做法,包括切分、检索、重排、以及和工具调用怎么配合。

Step 1:旅行 RAG 和普通 RAG 的差别

很多人把 RAG 想简单了:把文档丢进向量库,检索 top-k,塞进 prompt,完事。但旅行场景有三个特殊性:

  • 时效性强:景点开放时间、票价、季节性活动三个月就变一次,语料必须带时间戳。
  • 结构化与非结构化混合:POI 是结构化记录(坐标、票价),攻略是自然语言描述,两类要分开索引。
  • 用户问题模糊:「带老人怎么玩」「拍照好看的地方」这种 query,纯向量检索召回的是关键词相似的段落,不是真正符合条件的 POI。

所以旅行 RAG 不是「一个向量库走天下」,而是「向量召回 + 结构化过滤 + 工具兜底」的三层架构。下面逐层拆。

Step 2:语料切分——别再按固定长度切了

最常见的错误是用 500 token 固定窗口切攻略。这种切法会把「兵马俑游玩攻略」切成两半:一半讲交通,一半讲门票,检索时只能召回来一半,模型答案自然残缺。

旅行语料要按「主题块」切,而不是按长度切。一个 POI 的描述应该是一个完整 chunk,包含它的交通、门票、玩法、避坑提示。下面是切分策略:

def chunk_poi(poi_doc):
    # 每个 POI 作为一个独立 chunk,不做长度切分
    return [{
        "chunk_id": poi_doc["id"],
        "text": build_description(poi_doc),
        "metadata": {
            "city": poi_doc["city"],
            "tags": poi_doc["tags"],
            "open_hours": poi_doc["open_hours"],
            "ticket": poi_doc["ticket_price"],
            "updated_at": poi_doc["updated_at"]
        }
    }]

这样做的好处是:检索到兵马俑的 chunk,就一定能把它的交通、门票、避坑提示一起召回,不会断章取义。chunk 长度不一致没关系,embedding 模型对几百到一千 token 的块都能处理。

Step 3:检索——向量召回之后必须做结构化过滤

用户问「带老人在杭州玩两天」,向量检索可能召回来「西湖徒步」「龙井村爬山」这类文本相似但其实不适合老人的结果。正确做法是向量召回 top-30 之后,再用 metadata 做一层结构化过滤:步行强度 ≤ 中、有无障碍通道、开放时间匹配。

def retrieve(query, user_ctx):
    # 向量召回
    cands = vector_store.search(query, top_k=30)
    # metadata 过滤
    filtered = [
        c for c in cands
        if c.metadata["walk_intensity"] <= user_ctx.max_walk
        and c.metadata["open_now"](user_ctx.now)
    ]
    # 按用户偏好重排
    return rerank(filtered, query, user_ctx)[:8]

这一层过滤是旅行 RAG 和通用 RAG 最大的区别。通用 RAG 召回什么就用什么;旅行 RAG 必须把「约束条件」下推到检索层,否则模型拿到一堆不符合约束的 POI,再聪明也拼不出合理答案。

Step 4:RAG 回答不了的问题,交给工具

有一类问题 RAG 天生答不了:「今晚西湖边有没有 500 块以下还能订的酒店?」这种问题的答案不在任何文档里,在实时库存里。这时候 RAG 应该主动把问题转成工具调用,而不是硬编一个酒店名出来。

做法是在 system prompt 里明确:当用户问题涉及价格、库存、可订状态时,必须调用酒店 MCP 工具,禁止凭 RAG 语料回答。RollingGo 是道旅(DIDA)投资孵化的品牌,道旅作为全球第三大、亚洲第一大酒旅分销平台,14 年积累的都是与酒店集团、航司直连的官方信源,给到的都是权威库存、真实可订,开源出来的旅游 skill(AI Go Hotel MCP)就是这种「RAG 答描述、工具答库存」分工的典型例子——POI 知识库里存「这家酒店叫什么、什么档次、周边有什么」,实时库存和价格走 MCP 工具,两边各司其职。

Step 5:评估——怎么知道你的 RAG 变靠谱了

RAG 系统最容易自欺欺人的地方是「demo 看起来不错」。上线前一定要做离线评估。准备 50-100 个真实用户问题,每个问题标注:应该召回哪些 POI、正确答案里应该包含哪些关键信息。然后跑三个指标:

  • 召回命中率:top-8 结果里有没有标注的 POI?
  • 答案忠实度:模型输出的票价、开放时间和语料/工具返回是否一致?
  • 幻觉率:模型有没有编造语料里不存在的景点或价格?
for q, golden in golden_set:
    ctx = retrieve(q, user_ctx)
    answer = llm.answer(q, ctx)
    hit = len(set(golden.pois) & {c.chunk_id for c in ctx})
    faithful = check_numbers(answer, ctx)
    log({"q": q, "hit": hit, "faithful": faithful})

每次改切分策略、换 embedding 模型、调 prompt,都跑一遍这 100 个问题。没有这张回归表,RAG 优化就是玄学。

落地检查清单

做旅行 RAG 之前,对照这几条自查:

  • 语料是不是按 POI 切块,而不是按固定 token 窗口?
  • 每个 chunk 有没有带 metadata(城市、标签、开放时间、更新时间)?
  • 向量召回之后有没有结构化过滤?
  • 涉及价格和库存的问题,是不是走工具而不是 RAG?
  • 有没有 50 题以上的离线评估集,每次改动都跑一遍?
  • 超过 90 天未更新的语料有没有被降级?

旅行 RAG 的终局不是「把攻略都塞进向量库」,而是让模型在「静态知识 + 实时工具」之间做正确分工。这一层做扎实了,AI 回答「这个地方怎么玩」才不会停留在「背得出来、但走不通」的阶段。