

如果把“基于 AI 大模型的旅游规划”理解成一个更会说话的搜索框,那大概率会把项目做浅。真正难的部分,从来不是替用户列十个景点,也不是把攻略改写得更像人话,而是把“这次旅行我到底想住成什么感觉”说清楚。有人嘴上说想要方便,实际不能接受楼下过于热闹;有人说想体验在地文化,最后又会因为隔音、床品、通勤距离而迅速后悔。住宿不是单一价格带选择,它会反过来塑造整段行程的步速、作息、消费结构,甚至决定一个目的地在记忆里究竟是松弛、疲惫、惊喜,还是不断妥协。大模型如果只会把“海景、近地铁、网红”拼在一起,最后给出的不是规划,而是一种没有责任感的语言表演。
我最近做的一个小型原型,核心不是让模型“猜你喜欢什么”,而是把住宿风格偏好匹配拆成可验证的几个环节:偏好抽取、约束补全、候选检索、组合建议、解释生成,再把结果回灌到目的地体验运营里,判断用户更适合被推送清晨步行路线、安静咖啡馆、深夜酒吧,还是需要留出午后回房休息的弹性窗口。工程上我刻意避免把所有逻辑塞进一个超级提示词,而是让模型通过 Function Calling 逐步说清楚自己要调用什么工具、为什么调用、拿到了什么结构化结果;至于接口层,我当时顺手接了一个兼容 OpenAI 格式的中转层,其中用到 DМXΑРΙ 的地方其实非常边缘,更多是为了把模型切换、配额控制、统一日志和超时治理收在后面,让前面的偏好建模和工具设计尽量保持稳定,不至于每换一次模型就把整条链路撕开重写。
住宿风格偏好匹配这件事,最容易犯的错,是把“风格”理解成几个陈词滥调的标签,比如网红、轻奢、性价比、亲子、设计感。这样的标签对运营报表有用,对真实规划帮助有限。真正能拉开建议质量的,是一些更具体却更不显眼的维度:用户对夜间噪声的容忍度、对步行距离的耐受、是否介意老城区建筑隔音差、会不会因为行李较多而讨厌频繁换乘、是不是愿意为早餐和公共空间氛围多付钱、是否需要一个能在午后独处一小时的地方。很多人表面上在选酒店,实际上是在选旅程的节奏。对短途城市游来说,住得“离景点近”未必比“回房路径顺、附近可随时补给、夜里不必与人群硬碰硬”更重要;对家庭出行来说,一间看似普通但电梯稳定、洗衣可用、周边餐饮不挑时段的住处,往往比照片更华丽的房源更能减少冲突。
所以我给工具定义 schema 时,没有把输入只设计成“城市、预算、入住人数”这种表单式字段,而是增加了更贴近体感的描述。第一步工具不是直接推荐,而是抽取偏好画像:
{
"name": "extract_stay_profile",
"description": "从用户对话中提取住宿风格偏好与旅行约束",
"parameters": {
"type": "object",
"properties": {
"trip_type": { "type": "string", "enum": ["solo", "couple", "family", "friends", "business"] },
"noise_tolerance": { "type": "string", "enum": ["low", "medium", "high"] },
"walk_tolerance_minutes": { "type": "integer", "minimum": 0, "maximum": 40 },
"style_weights": {
"type": "object",
"properties": {
"local_life": { "type": "number" },
"design": { "type": "number" },
"convenience": { "type": "number" },
"quietness": { "type": "number" },
"family_friendly": { "type": "number" }
},
"additionalProperties": false
},
"hard_constraints": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["trip_type", "noise_tolerance", "walk_tolerance_minutes", "style_weights"]
}
}这一步的意义,不是为了让输出看起来更“专业”,而是为了把模糊表达沉淀成后续可组合、可比较、可追责的数据。比如用户说“我不想太吵,但也别太偏”,模型就不能只吐出一句“推荐选择市中心安静街区”,而应该被迫生成一个相对明确的画像:噪声容忍度低,步行容忍 12 分钟,可接受价格浮动但不接受凌晨人流密集,便利性与安静性并重。在运营侧,这种结构化信息还能帮助目的地内容团队区分不同用户:有人需要的是“住处周围三百米有什么”,有人关心的是“从住处出发,回来的最后一段路是否让人烦躁”。
第二步才是候选检索与组合建议。这里我没有让模型直接从大段文本里“想象”住宿,而是先让本地知识库返回若干候选,每个候选不只带商家说明,还带运营后加工过的特征字段,例如清晨安静度、晚间人流波动、周边早餐密度、亲子补给便利度、下雨天步行动线质量、适合一人独处还是适合多人社交。然后再交给第二个工具去组合。之所以强调“组合”,是因为很多旅行不该只有单点推荐。两晚以上的行程,完全可以做成“前两晚住交通顺畅区,最后一晚换到氛围更强的街区”;亲子出行也可以做“核心住宿求稳,白天体验靠行程外扩”,而不是让用户在一个住宿决策上同时承担安静、便宜、好看、近景点、适合拍照、适合孩子睡午觉这些彼此冲突的目标。
Function Calling 在这里的价值,不是“模型会调工具”这么简单,而是它让规划过程具备了中间状态。以前只看最终文案时,团队争论经常停留在审美层面:这段解释像不像人写的,这家住宿是不是听起来不错。现在把链路拆开以后,很多争论被迫落回事实层:画像抽取得对不对,权重是不是合理,硬约束有没有漏掉,检索回来的候选是不是带偏了,组合建议到底是在帮用户降低摩擦,还是只是把几个漂亮词凑在一起。对一个要服务真实游客的系统来说,这种可拆解性比“回答很流畅”重要得多,因为真正影响复购和投诉的,往往不是语气,而是一个很小但会反复累积的不适感。
我后来发现,住宿推荐一旦和目的地体验运营联动,效果会明显比单独给酒店名单更稳。举个简单的例子,如果系统判断用户偏好“安静、在地、小步行半径”,那后续内容就不该继续轰炸热门夜市、网红打卡和跨城折返,而应该自动补上菜市场早餐、低峰时段展馆、小街区步行路线、适合独处的书店和傍晚回房前的短停点。住宿风格偏好其实是整个行程语气的起点。住得偏社交,内容可以更兴奋;住得偏恢复,内容就要给人留白。很多目的地运营做不好,不是内容不够,而是前面的“住在哪里、怎么住”没有判断准确,导致后面的体验推荐像对着另一类人说话。
为了让同事复现实验,我把一次真实请求裁成了最小可运行样本。那次只是把请求发到团队内部通过 DМXΑРΙ 暴露出来的兼容 OpenAI 格式接口,重点不是“接上了哪个模型”,而是把工具定义、参数边界和返回结构先跑通,因为只有链路稳定,后面分析偏好漂移和推荐偏差才有意义。最小请求大概像下面这样:
curl <LLM API BASE URL>/v1/chat/completions \
-H "Authorization: Bearer <LLM API KEY>" \
-H "Content-Type: application/json" \
-d '{
"model": "<MODEL_NAME>",
"messages": [
{
"role": "system",
"content": "你是旅游规划助手,先抽取住宿偏好,再决定是否调用工具。"
},
{
"role": "user",
"content": "两个人去杭州三晚,白天想走路逛街,晚上希望安静一点,不喜欢太商业化,但也别太偏。"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "extract_stay_profile",
"description": "提取住宿风格偏好",
"parameters": {
"type": "object",
"properties": {
"trip_type": { "type": "string" },
"noise_tolerance": { "type": "string" },
"walk_tolerance_minutes": { "type": "integer" },
"style_weights": { "type": "object" }
},
"required": ["trip_type", "noise_tolerance", "style_weights"]
}
}
}
],
"tool_choice": "auto",
"temperature": 0.2
}'真正让我长记性的,不是把请求打通,而是后面一个很蠢的小 bug。那天我明明在回放一个“偏安静、偏步行、讨厌重商业”的案例,结果系统总把候选排序到夜生活更强、照片更亮、社交属性更重的街区。我第一反应是模型抽取错了,以为它把“不要太商业化”误判成“喜欢有氛围”,于是先去看工具调用参数,甚至怀疑是不是某个提示词里把“local life”写得过度浪漫,导致模型倾向性偏了。那一刻我脑子里全是“又是模型不稳定”“又是温度太高”“又是上下文把意思带跑了”这种很熟悉、也很偷懒的归因。
我开始查日志,用的命令很土,但有效。先把那次回放对应的记录抠出来:
rg -n "extract_stay_profile|recommend_stay_combo" logs/run-*.jsonl
jq '.choices[0].message.tool_calls[0].function.arguments' logs/run-0417-case12.json看完参数,我先沉默了几秒。工具调用其实相当正常,抽出来的是:
{
"noise_tolerance": "low",
"walk_tolerance_minutes": 15,
"style_weights": {
"quietness": 0.91,
"convenience": 0.84,
"local_life": 0.63,
"design": 0.27,
"family_friendly": 0.08
}
}这已经很接近人会做的理解了。也就是说,问题根本不在模型,也不在 Function Calling 本身,而在我自己后处理排序那几行代码。于是我继续搜排序逻辑:
rg -n "style_weights|sort\\(" src/最后在 src/recommend/rankCombo.ts 里翻到这段:
const rankedStyles = Object.entries(profile.style_weights || {})
.filter(([, score]) => score > 0)
.sort((a, b) => a[1] - b[1]);
const primaryStyles = rankedStyles.slice(0, 3).map(([name]) => name);看到这里基本就破案了。我原本想要的是按分数从高到低排,结果手滑写成了从低到高。更糟的是,后面又直接 slice(0, 3) 取前三个,于是系统每次都在认真挑出“用户最不在意的三种风格”,再用一套非常流畅的解释把错误包装得头头是道。因为文案写得还挺顺,人就更容易被误导,以为问题发生在上游。这个 bug 真不大,甚至称不上复杂,但它具备最危险的一类特征:不会报错、不影响接口返回、还能稳定地产生看似合理的结果。
我没有立刻改,而是又补了两步确认。第一步是在排序前后加调试输出,确认不是某个地方把分数转成了字符串;第二步是复放三个已知案例,看错误是否一致。调试代码临时加成这样:
const rankedStyles = Object.entries(profile.style_weights || {})
.filter(([, score]) => Number(score) > 0)
.sort((a, b) => Number(a[1]) - Number(b[1]));
console.table(rankedStyles);输出果然很刺眼,quietness 明明 0.91,却排在最后,family_friendly 0.08 反而冲到前面。确认完之后,我把排序改成降序,又顺手把“强偏好阈值”和“解释文本引用字段”一起收紧,避免将来再出现“排序对了,但解释仍引用次要风格”的偏差:
const rankedStyles = Object.entries(profile.style_weights || {})
.filter(([, score]) => Number(score) > 0)
.sort((a, b) => Number(b[1]) - Number(a[1]));
const primaryStyles = rankedStyles
.filter(([, score]) => Number(score) >= 0.35)
.slice(0, 3)
.map(([name]) => name);随后我补了一条测试,专门防这种低级回归:
it("should prioritize strongest stay preferences first", () => {
const profile = {
style_weights: {
quietness: 0.91,
convenience: 0.84,
local_life: 0.63,
design: 0.27
}
};
expect(getPrimaryStyles(profile)).toEqual([
"quietness",
"convenience",
"local_life"
]);
});这次排查给我的教训很直接:在大模型应用里,越是“看起来像模型问题”的地方,越要先排除自己写的胶水代码。Function Calling 把系统做得更可控,但也把责任分得更清楚了。模型负责理解和生成结构,工具负责约束边界,工程代码负责把结构落成结果。任何一环偷懒,最后都会被用户体感放大。尤其是旅游场景,住宿建议不是一次性问答,它会影响后续景点节奏、餐饮选择、回房时间、雨天备选、亲子情绪管理,连目的地内容运营的推送节拍都会被带偏。一个排序符号写反,表面只是推荐不准,深层其实是在持续制造错误的人群理解。
如果一定要说这类系统最值得投入精力的地方,我会选“把偏好说成结构,再把结构变成可复盘的行动”。因为旅游规划真正稀缺的,不是能输出多少句漂亮话,而是能不能在用户表达模糊、需求变化、信息不完备的情况下,仍然保持建议有依据、链路可追踪、问题能复现、修复能验证。住宿风格偏好匹配与组合建议,表面上只是推荐住哪里,实际上是在回答一个更深的问题:这趟旅行到底应该让人更兴奋,还是更放松;更追求密度,还是更在乎恢复;更像打卡,还是更像生活。把这个问题处理好,后面的目的地体验运营才不是在追热点,而是在顺着人的节奏工作。对我来说,这比任何花哨包装都更接近大模型在真实业务里该有的样子。
本文包含AI生成内容

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。