很多人把“AI 做旅游规划”理解成“把景点、餐厅、路线拼成一张表”,但真正到了出发前一晚,最容易让人心里发虚的往往不是景点顺序,而是那些不写在行程单首页的小事:进寺庙要不要脱鞋,拍照前要不要先问,和陌生人合影哪些场合不合适,夜间公共交通是否默认安静,给不给小费、给多少、什么情况下给错反而显得冒犯。大模型擅长总结,却不天然擅长对这种“低频但关键、细碎却高风险”的文化礼仪场景做精细判断。它如果只靠一次大而全的提示词,常常会输出正确却无用的套话;如果把问题拆得太碎,又会让用户在真正需要提醒的时候看不到重点。所以我后来越来越倾向于把礼仪提示视为旅游规划里的一个独立执行层,而不是附着在攻略末尾的补充说明,因为目的地体验运营里最难的部分,恰恰是把“别出错”这件事做得不打扰人,却又能在临界时刻真正起作用。
这个思路成形以后,系统设计就不再是“生成一篇攻略”,而是“围绕出行过程逐段触发提示”。我当时把它放进一个 Agent 协同链路里:行程规划智能体先给出城市间移动、景点密度和停留时长,礼仪检索智能体再根据地点类型、时段、活动性质和用户画像补充文化注意事项,最后由一个合成智能体把结果压缩成用户真会看的短提醒。中间那层并不需要高调存在,哪怕团队把兼容 OpenAI 格式的统一出口挂在一个像 DМXΑРΙ 这样的中转层上,对产品体验来说真正重要的也不是入口名字,而是上层调用协议稳定、下层模型可替换、日志字段可追踪,这样我们才能把“礼仪提示”从一次性文案变成可观测、可复盘、可持续修正的服务能力,而不是一段写完就没人再看的说明文字。
我后来把这套链路拆成四个最小角色。第一层是 planner_agent,输入用户预算、交通偏好、体力阈值和可用时段,输出结构化行程;第二层是 context_agent,把每个 stop 变成上下文片段,例如“宗教场所”“高密度居民街区”“正式餐桌”“夜间市场”;第三层是 etiquette_agent,只负责生成礼仪和风险提示,不碰路线重排;第四层是 operator_agent,把前面三层结果按推送时机重写成适合 App、短信、导游后台或酒店前台协同系统的不同文案。这样做的好处是职责边界很硬,出了问题容易定位。比如用户说“为什么你在博物馆段落提醒我寺庙礼仪”,我们不用去怀疑整条链路,而是直接查 context_agent 的分类结果是否把场景标签打错了。
礼仪提示如果想做得像样,核心不是堆知识点,而是做“触发条件”。我自己踩过一个坑:早期把提示写成百科卡片,字段像下面这样。
{
"country": "JP",
"etiquette": [
"公共场合注意音量",
"部分宗教场所需脱鞋",
"拍照前先确认许可"
]
}
看起来没错,但几乎没有运营价值,因为用户在地铁里、餐桌边和神社门口看到的是同一组句子。后来我把结构改成“场景-动作-风险-替代表达”四段式,模型就开始像一个真正能协助出行的助手,而不是百科复读机。
{
"scene": "religious_site",
"trigger": "entering_before_10m",
"risk": "未经许可拍摄内部陈设可能被制止",
"advice": "入内前观察门口标识,鞋柜和静音要求优先执行",
"fallback_phrase": "请问这里可以拍照吗"
}
真正落地时,Agent 协同不是为了“显得高级”,而是为了让错误有隔离带。旅游规划一旦和目的地体验运营结合,就会面对两个相反的压力:一边要求提示足够具体,最好能告诉你此刻该做什么;另一边又要求提示不要越俎代庖,别把本地文化讲成僵硬清单。我的一个经验是,礼仪提示不能试图替代人的判断,它只应该在高风险处抬高手臂,在低风险处尽量退后。比如“进店后是否先点单再入座”这种会随城市、店型、语言环境变化的场景,模型不应该装作百分之百确定,而应该输出“先观察门口动线和前台指引,若无明确说明,优先询问”。这种保守不是能力不足,而是对真实旅行情境的尊重。
实现层面其实并不复杂,我用了一套很朴素的栈:FastAPI + Pydantic + 一个轻量任务队列 + OpenAI 格式请求封装。初始化时我只跑了几条命令:
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn pydantic openai
uvicorn app:app --reload
服务内部最关键的是别让“礼仪提示”直接吃原始用户文本,而是先吃已经抽象好的行程节点。下面这段是我当时的核心调度代码,短,但决定了系统是不是稳定。
from pydantic import BaseModel
from typing import Literal
class Stop(BaseModel):
city: str
venue_type: str
local_time: str
activity: str
class EtiquetteHint(BaseModel):
scene: Literal["religious_site", "restaurant", "street_market", "museum", "transit"]
advice: str
risk: str
confidence: float
def build_context(stop: Stop) -> str:
return f"{stop.city}|{stop.venue_type}|{stop.local_time}|{stop.activity}"
def route_to_agent(stop: Stop) -> str:
if stop.venue_type in {
"temple", "shrine", "mosque", "church"}:
return "etiquette_agent_religious"
if stop.venue_type in {
"restaurant", "tea_house"}:
return "etiquette_agent_dining"
return "etiquette_agent_general"
这类代码看起来普通,真正难的是字段约束。旅游场景里一旦 venue_type 和 activity 的边界模糊,后面所有礼仪提示都会发生漂移。举例说,“夜市吃东西”到底算 restaurant、street_market 还是 snack_stall,如果你只是为了省事把它们归并,模型后面就会把“排队秩序”“现金找零”“边走边吃是否失礼”混成一锅。我后来为此补了一层手写映射表,宁愿多维护几十条规则,也不让模型在这种细部上自由发挥,因为运营里最贵的不是多写几行代码,而是用户第一次觉得你“不懂当地”。
中后期我还真的因为一个很小的疏忽吃过亏,而且那次比任何成功案例都更能说明问题。那天我在做“提前十分钟触发礼仪提醒”的逻辑,想法很简单:如果用户即将进入宗教场所,就把穿着、拍照、音量、路线动线这些提示提前推送出去。我写了一个函数,把 stop 的本地时间减去十分钟,命中窗口就触发。上线前自测都没问题,可一到多城市联程测试里,提醒就开始莫名其妙地提前几个小时出现。最先看上去像是时区问题,我第一反应也是时区问题,于是直接去翻日志。
rg "entering_before_10m|local_time|timezone" logs/app.log
日志里最刺眼的一条是这样的:
trigger=entering_before_10m city=Kyoto local_time=2026-03-15T09:20:00+09:00 device_tz=+08:00 push_at=2026-03-15T09:10:00+08:00
第一眼我还觉得“这不就找到了吗,设备时区和目的地时区不一致”,但继续往下翻又不对,因为用户明明已经落地,设备时区早就切到了目的地。我又打了更多上下文日志,把 stop 原始对象和进入调度前的序列化内容一起输出,结果发现问题根本不在时区切换,而在我自己定义的字段名上。前一版模型里时间字段叫 local_time,新版为了让 planner 输出更明确,我新加了 arrival_local_time,结果 context_agent 适配时偷懒写了个兜底逻辑:没有 arrival_local_time 就回退到 generated_at。而多城市联程测试用的那批数据里,第二段 stop 恰好没有成功写入 arrival_local_time,所以礼仪提醒实际上是拿“行程生成时间”去减十分钟,当然越算越离谱。
更糟的是,我一开始还被自己的“经验判断”带偏了。因为旅游系统里时间错误太常见,脑子自然会先锁定时区、夏令时、客户端缓存、异步队列延迟这些大坑,反而忽略了最朴素的字段错配。我后来干脆把怀疑链路一段一段掐断:先用固定输入跑单元测试,再跳过队列直接同步执行,再把模型输出 JSON 存盘做 diff,最后才发现那行最该早看见的代码。
def resolve_arrival_time(stop: dict) -> str:
return stop.get("arrival_local_time") or stop.get("generated_at")
问题就出在这个“看起来很贴心”的回退上。generated_at 在别的地方是合法字段,但在礼仪调度里完全不该作为候选。我最后把逻辑改得非常保守,只要缺少 arrival_local_time 就直接标记不可触发,并把 stop 丢回补全队列,而不是继续装作系统还能猜。
def resolve_arrival_time(stop: dict) -> str:
if not stop.get("arrival_local_time"):
raise ValueError("missing arrival_local_time for etiquette scheduling")
return stop["arrival_local_time"]
改完以后我又补了两层防线。第一层是 schema 约束,planner 输出里 arrival_local_time 变成必填;第二层是集成测试,专门构造“跨城、跨时区、设备时区滞后”的场景。那次排查给我的教训很具体:做目的地礼仪提示,最怕的不是模型偶尔说得不够漂亮,而是系统在错误时间把正确的话说出来。错误内容用户也许会怀疑,错误时机用户往往只会觉得你烦,从此把通知全部关掉。产品一旦走到这里,再好的大模型也救不回来。
等调度和字段问题稳定以后,调用层反而成了最不值得炫耀的部分,因为它本质上只是一个兼容 OpenAI 格式的请求包装;当时我把这条请求接到团队统一的模型出口上,部署说明里顺手记了一句走的是 DМXΑРΙ,但对写业务代码的人来说,真正要紧的只是 messages 结构、超时策略、重试次数和返回 schema 是否可校验,像下面这样把“目的地礼仪提示”限定成结构化结果,远比争论模型名更有用。
curl <LLM API BASE URL>/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <LLM API KEY>" \
-d '{
"model": "<LLM MODEL NAME>",
"temperature": 0.2,
"response_format": { "type": "json_object" },
"messages": [
{
"role": "system",
"content": "你是目的地礼仪提示助手。只输出JSON,字段包括 scene、risk、advice、fallback_phrase、confidence。若信息不足,明确写出 uncertainty。"
},
{
"role": "user",
"content": "用户将在今晚19:20抵达一处宗教场所,计划停留40分钟,喜欢拍照,不懂当地语言,请生成简短但不武断的礼仪提示。"
}
]
}'
这种调用方式的好处,是你可以把 Agent 之间的接口设计得比模型本身更稳定。planner_agent 负责“去哪”,etiquette_agent 负责“怎么不出错”,operator_agent 负责“何时说、说多长”。一旦线上发现某类场景误判,比如把“传统市场”的礼仪写得像“高端餐厅”,你只需要回看场景标签和提示模板,不必把整套旅游规划逻辑推翻重来。更重要的是,这种拆法很适合目的地体验运营团队协作:内容运营可以维护场景词典,产品经理可以调整触发时机,工程侧则守住 schema 和调度边界。大模型在这里不是主角,它更像一个能被约束、被校验、被替换的语言组件。
我现在回头看,这类系统真正值得投入的地方,不是“让旅行规划看起来更聪明”,而是让它在细处显得更可靠。用户不会因为你知道十条礼仪常识就长期留下来,但会因为你在最容易尴尬的时刻给出一句刚刚好的提醒而减少一次不必要的冒犯。目的地文化礼仪这件事,说到底不是给系统加一层文化滤镜,而是承认旅行中的人与地方之间,本来就有一个需要被认真对待的边界。Agent 协同、结构化提示、严格字段校验,这些工程动作最终都服务于同一件朴素的小事:别让技术只会替人安排路线,却不会替人保留分寸。
本文包含AI生成内容