用户问「西安三天怎么玩」,纯大模型给你的答案永远是兵马俑、大雁塔、回民街三件套——因为它读过的训练语料里,西安就是这三个景点。但如果你真的按它说的走一遍,会发现它不知道兵马俑现在要预约、不知道回民街本地人不去、不知道冬天华山封不封。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 回答「这个地方怎么玩」才不会停留在「背得出来、但走不通」的阶段。