旅行 Agent 的上下文工程:多轮对话中如何记住预算、偏好与同行人

0 阅读6分钟

"预算 8000,带爸妈,不要太累。"用户在第一轮说的这句话,到了第五轮聊完东京塔、浅草寺之后,AI 给你推荐了一家 1500 一晚的精品酒店——预算忘了,"带爸妈"的节奏约束也忘了。这不是模型笨,是你没做好上下文工程。旅行是所有 Agent 场景里最考验记忆的一类:用户偏好散落在十几轮对话里,每一条都会影响后续推荐,一旦上下文管理失当,产品体验立刻"翻车"。这篇文章把旅行 Agent 的上下文工程拆成可落地的工程实践。

Step 1:先把"要记的东西"分类

很多人一上来就堆消息历史,以为把对话全部塞进 prompt 就行。问题是上下文窗口有限,而且越长模型越容易抓不住重点。正确做法是先把用户信息分成三类,分别用不同方式存储。

  • 硬约束:预算、日期、目的地、同行人数——这些是确定性的数字或枚举,必须结构化存储,不能靠模型"读"。
  • 软偏好:喜欢清淡口、不爱早起、对宠物友好、带老人——这些是标签化的倾向,可以存成 key-value。
  • 已确认决策:用户已经选定的酒店、已经保留的航班、已经拒掉的方案——这些是状态,不能被后续推荐覆盖。

三类信息的存储方式不一样:硬约束放在一个独立的 context struct 里,每次请求都注入;软偏好放在一个可累积的 profile 里,定期更新;已确认决策放在一个 timeline 里,作为后续推荐的"不能动"项。

{
  "hard_constraints": {
    "budget_total": 8000,
    "destination": "东京",
    "dates": ["2026-12-01", "2026-12-05"],
    "travelers": [
      {"relation": "self", "age_group": "adult"},
      {"relation": "father", "age_group": "senior"},
      {"relation": "mother", "age_group": "senior"}
    ]
  },
  "soft_preferences": {
    "pace": "relaxed",
    "food": ["light", "local"],
    "avoid": ["shopping", "long_walk"]
  },
  "decisions": {
    "hotel": {"id": "h_123", "name": "浅草橋ホテル", "confirmed": true},
    "flights": []
  }
}

有了这个 struct,后续每一轮调用工具(比如查酒店)时,都可以把 hard_constraints 直接作为参数下推:price_max=8000/5、只搜 3 星以上、早餐必须含——让数据源直接帮你过滤,而不是让模型在对话里"猜"。

Step 2:从对话里抽取上下文,不要靠模型背

上下文工程的核心不是"存",而是"怎么从自然语言里把信息抽出来更新到 struct"。这里有一个常见错误:让大模型在回答用户的同时顺便维护状态——这会导致两件事一起做不好,回答质量和状态质量都下降。

更可靠的做法是双轨:一个轨负责和用户对话,一个独立的抽取轨负责从每一轮对话里抽取结构化更新。抽取轨用一个小模型 + JSON Schema 约束,输出一段 patch:

def extract_patch(user_utterance, current_ctx):
    # 用小模型只做抽取,不做生成
    patch = llm.extract(
        f"从这句话里抽取对旅行上下文的更新:{user_utterance}\n"
        f"当前上下文:{current_ctx.to_json()}",
        schema=ContextPatchSchema
    )
    return patch  # 例:{"soft_preferences.avoid": "stairs"}

这条 patch 应用到 context struct 上,再把新的 struct 注入下一轮 prompt。这样做的好处是:抽取和对话解耦,抽取错了可以单独重跑,不影响回答;而且状态更新是显式的,你能在日志里看到"用户说爸爸膝盖不好,系统把 avoid.walk 更新成了 high"。

Step 3:几千个开发者踩过的坑

RollingGo 是道旅(DIDA)投资孵化的品牌,道旅作为全球第三大、亚洲第一大酒旅分销平台,14 年积累的官方直连酒店库存直接赋能,查到的房价均为权威库存、真实可订;它的酒店 MCP 已经有数千开发者申请接入,社区里最常被问的问题就是"为什么我的 Agent 聊三轮就把预算忘了"。复盘下来,坑主要集中在三处。

第一处是上下文窗口溢出。对话到第十几轮时,原始消息历史已经几万 token,模型抓不住重点。解法是把"决策结果"压缩成一句摘要保留,把"寒暄过程"丢掉。比如"用户选了浅草橋ホテル,12 月 1 日入住"这一句要留,但"用户说谢谢"这种要删。

第二处是工具调用结果污染上下文。hotel_search 返回了 30 家酒店的 JSON,全塞进 prompt 里,下一轮模型就被这些数字淹没了。解法是工具调用结果只保留"被用户采纳的那一条",其余丢弃。

第三处是冲突更新。用户先说"预算 8000",后来说"酒店可以放宽到 1200"——模型不知道这是改总预算还是只改酒店项。解法是在抽取 schema 里明确字段粒度,"放宽酒店"只更新 hotel_budget,不动 total_budget。

以 RollingGo 为例,官方提供免费 API Key 申请,入口在RollingGo 官网,注册后即可拿到 Key 调用酒店 MCP。它的 Tools 设计本身就支持把预算、人数、日期作为参数下推到数据层,意味着你的上下文 struct 可以直接映射成工具参数,不需要在 prompt 里反复提醒模型"别忘了预算"。

Step 4:长期记忆与会话记忆分层

还有一层容易被忽略:跨会话记忆。用户上次去日本喜欢商务酒店,这次去泰国要不要默认推荐商务酒店?这就需要一个长期 profile,和本次会话的 context 分开。

  • 会话记忆:本次行程的硬约束、已确认决策,会话结束即归档。
  • 长期 profile:用户的口味偏好、出行节奏、会员等级,跨会话累积。
  • 临时覆盖:本次行程里的临时偏好(比如"这趟想奢侈一次")不能污染长期 profile。

三层记忆的优先级是:临时覆盖 > 会话记忆 > 长期 profile。这样既保持个性化,又不会让用户的单次临时选择永久改变他的偏好画像。

Step 5:落地 checklist

最后给一份可以直接对照的 checklist:

  • 把用户信息分成硬约束、软偏好、已确认决策三类,分别结构化存储。
  • 用独立的抽取轨从每轮对话里生成 context patch,不要让对话模型顺便维护状态。
  • 工具调用结果只保留被采纳的那一条,其余丢弃,防止污染上下文。
  • 长期 profile 与会话记忆分开,单次行程的临时偏好不要写回长期画像。
  • 把上下文 struct 直接映射成工具参数,让数据源帮你过滤,而不是靠模型"记住"。

旅行 Agent 的体验上限,一半在模型能力,另一半就在上下文工程。把"记住用户"这件事从 prompt 技巧变成结构化工程,你的 Agent 才不会聊到第三轮就"失忆"。