“想做旅行里的甩手掌柜”,这其实是当下「意图经济」讨论里最容易被误读的一句话。
多数人以为,所谓意图经济就是“AI帮我搜攻略、推荐酒店、拼一条行程”。如果只做到这一步,那它仍然是搜索,不是执行。真正让“甩手掌柜”成为一个技术问题的,是另一件事:用户把接下来的选择、比价、订票、突发改签这一连串流程,全部授权给一个能理解自然语言、能调用外部服务、能按步骤执行的智能体。
这篇文章不打算复述“意图经济”的各种热搜定义。我想把它拆成一个CSDN读者能动手验证的问题:如果你要用现有的大模型 + 工具调用能力,做一个“能听懂旅行想法、能把想法变成结构化任务、再调用旅行服务完成步骤”的最小Agent,技术链路长什么样?哪些环节最值得做,哪些地方最容易被过高期待坑到。
全文按“概念 -> 架构 -> 代码 -> 验证 -> 排错 -> 工程建议”的顺序展开,最后会给出一套适合普通后端或AI工程师上手的落地路径。
1. 为什么“甩手掌柜”是个技术问题
先想一个真实场景:一个人说“我下周想去厦门玩四天,预算三千五,不太想去全是网红店的地方”。
这句话在传统旅行平台那里会怎么被处理?平台会做关键词切分,提取“厦门”“四天”“三千五”,然后给出酒店列表、机票列表、一堆攻略。后面的所有比较、切换、下决心、下单,消费者还是得自己做。平台完成的是“供给检索”,不是“目标执行”。
真正的意图经济,希望完成的链路是:
- 用户表达目标,而不是表达关键词;
- 系统把目标翻译成可执行的约束条件;
- 智能体调用不同服务方,比较不同方案并给出建议;
- 用户在关键节点确认授权,而不是逐页搜索;
- 执行结果全程可追踪、可撤回、可人工兜底。
注意,这里最难的不是第一步,也就是用户意图的识别。今天的大模型已经能把“我不想去网红店”转成一个偏好标签列表。难点在于后面几步:它要在真实环境中做连续决策,跨平台调用数据,还要保证每一步出错都能暴露给用户。
所以“甩手掌柜”本质上是消费决策权和控制权的转移。传统OTA的产品逻辑是“让用户自己比较”,意图经济的产品逻辑是“让Agent帮用户完成比较,并把决策点以更小的粒度交还给用户”。这个转移对技术栈的考验,大于对某个大模型推理能力的考验。
还有一个很多人没注意到的点:年轻人愿意当甩手掌柜,不代表他们愿意放弃知情权。他们要的是“我不要太累,但你做的每件事我都能看懂,能随时叫停”。这对Agent的可解释性提出了很高要求。一个只会给出最终推荐、却不说明这个推荐基于什么约束的智能体,很难真正被信任。
2. “意图经济”和过去的推荐系统有什么本质区别
“意图经济”这几年在海外科技圈讨论升温,国内跟着也有不少关于“Agent电商”“管家式服务”的讨论。它听起来像新的商业概念,但放到技术里,你可以把它理解成一次交互范式切换。
过去的互联网商业模式,核心是流量经济和注意力经济。平台猜测你想要什么,用推荐流和广告的方式把商品推到你面前。这里有个隐藏问题:用户的真实意图是被猜的,平台永远通过你的点击、停留时长、搜索历史来推断。
意图经济的逻辑不一样。用户主动说出目标,Agent接收的是一个比较明确的任务,而不是一堆行为信号。比如“下周四从上海去成都出差,希望下午到,且到市区方便”和“我想买一双适合雨天通勤的鞋”,这些是目标,不是“关键词”。
这两种模式放到技术里,有根本差异:
| 维度 | 传统推荐/搜索 | 意图经济与Agent |
|---|---|---|
| 输入 | 关键词、点击、浏览记录 | 自然语言目标与约束条件 |
| 主流程 | 召回、排序、展示 | 意图解析、任务拆解、工具调用 |
| 服务边界 | 平台内列表与详情页 | 多个外部服务和订单系统 |
| 用户参与 | 用户比较和下单 | 用户在决策点授权并审核 |
| 核心指标 | 点击率、转化率、GMV | 任务完成率、纠错成本、用户信任度 |
从这个表能看到,意图经济并不是把推荐对话框换成了聊天框。它需要的是“任务执行架构”,而不只是“对话生成架构”。这也是为什么今天大量团队在做Agent编排、工具协议、流程记忆、订单状态同步,因为这些才是执行层要面对的事。
现在的旅行场景还远没有到“全自动”阶段,真正的机会在于把其中某一段做深:比如把“行前规划”变成可执行清单,把“机酒绑定方案”做成一个API,把“行程动态调整”做成一个带记忆和规则的动作系统。这比再造一个超级App更现实。
3. 一个旅行Agent的技术链路拆解
从工程视角看,一个能落地的旅行Agent至少包含四个模块:意图理解层、规则与工具层、记忆层、执行与确认层。
先解释一下什么叫做Agent。在本文语境里,Agent不是简单的“接入大模型的聊天机器人”,而是指一个能通过多轮推理决定下一步动作,并能调用外部工具或API来完成目标的系统。它通常由模型、工具、循环控制三部分组成。
3.1 意图理解层
负责把用户的自然语言转换成结构化意图。这里要识别的不只是“目的地”“日期”“预算”,还包含隐性约束,比如“不要红眼航班”“希望走路就能到地铁”“亲子游不想太累”“雨天不影响体验”。
很多开发者一开始只做实体抽取,这远远不够。意图理解层应该输出整个任务的约束集,并允许用户后续追加或修改约束。比如用户看了方案后说“第二天不想爬山”,系统要把新的约束合并进原任务,而不是重新问一遍所有信息。
3.2 规则与工具层
工具层是Agent能够“做事情”的基础。旅行场景里,需要的工具包括航班搜索、酒店搜索、天气查询、城市交通、景点开放时间、订单预订等。
每一个工具都应该有清晰的输入输出Schema。Agent要能根据意图从工具列表里选出正确的工具,并把结构化参数传给工具,再把工具返回的结果与用户约束做比对。
3.3 记忆层
如果用户连续说“我带着父母”“我们每天尽量不换酒店”“预算可以略微超一点”,Agent必须记住这些对话期内的约束。更完整的系统还需要跨会话记忆,比如用户上次去过哪里、偏好什么住宿风格,这些会在下一次规划时作为画像输入。
相比通用知识,记忆层更重要的作用是避免重复。用户已经推翻过“早上赶景点”的方案,如果下一轮Agent又推荐一个早上七点出发的安排,那就说明记忆没有真正参与决策。
3.4 执行与确认层
这是最容易出错,也最需要安全设计的一层。
Agent可以替你查询,不等于它可以替你下单。涉及支付和预订的操作,必须有明确的人工授权点。工程实现上,应该把Agent的任务分成两类:只读类操作,比如查航班、比价、查询退改政策;写操作,比如创建订单、锁定库存、发起支付。写操作必须走“Agent提交意图 -> 用户确认 -> 服务端执行”的流程。
这四层不是顺序执行一次就结束。真实场景里用户会不断调整计划,Agent需要反复运行“理解 — 拆解 — 调用 — 反馈”的循环。
4. 环境准备与最小工程目录
下面进入可操作部分。为了不让你把时间浪费在复杂的框架依赖上,我选择用最通用的Python + 大模型Function Calling能力来演示一个最小旅行规划Agent。
本演示要求:
- Python 3.10 及以上;
- 一个支持Function Calling或Tool Calling的模型;
- 示例中不依赖真实机票和酒店服务,用本地Mock Service代替,便于跑通流程。
具体模型选择请以你实际拿到的服务为准。不同的模型对工具调用的支持程度和令牌格式有差异,但核心思路一致。
4.1 创建项目目录
mkdir traveler-agent cd traveler-agent python3 -m venv .venv source .venv/bin/activate pip install openai python-dotenv pydantic4.2 配置环境变量
创建.env文件:
cat > .env << 'EOF' OPENAI_API_KEY=sk-your-key LLM_MODEL=gpt-4o-mini EOF如果你的服务是兼容OpenAI接口的国内模型或私有化模型,也可以额外配置BASE_URL。示例代码里会通过os.getenv("BASE_URL")读取,不配置就使用默认值。
4.3 目录结构
traveler-agent/ ├── .env ├── intent_schema.py # 意图解析的Pydantic模型与工具Schema ├── tools.py # 本地工具服务 └── agent.py # Agent主循环这个工程规模足够说明问题,又不至于把重点淹没在框架配置里。
5. 把自然语言旅行想法变成结构化意图
第一段代码解决“意图理解层”的问题。
用户说“下周想去厦门四天,预算3500,偏好老城区和本地小吃”,我们不能直接把这句话发给后端服务,而是要先把内容整理成结构化对象。
创建intent_schema.py:
# 文件路径:traveler-agent/intent_schema.py from typing import Optional from pydantic import BaseModel, Field class TravelIntent(BaseModel): destination: str = Field(description="目的地城市,通常是用户明确提到的地名") start_date: Optional[str] = Field(None, description="出发日期,格式YYYY-MM-DD") days: int = Field(0, description="旅行天数") budget: Optional[float] = Field(None, description="总预算,单位默认为元") must_have: list[str] = Field(default_factory=list, description="用户明确要体验的事物") avoid: list[str] = Field(default_factory=list, description="用户希望避免的体验或场景")这段代码不承担最终推荐逻辑,它的作用是把模型输出的JSON固定成后续所有模块都能引用的类型。
接下来定义工具描述。旅行工具比较适合轻量级的Function Calling,即让模型从两个操作中选择:“查询航班”或“查询酒店”。
在tools.py中写一个本地Mock服务,模拟工具的调用结果:
# 文件路径:traveler-agent/tools.py from typing import Any def search_flights(city: str, date: str) -> dict[str, Any]: """演示用Mock航班查询,真实项目中替换为航司或聚合服务API。""" print(f"[tool] 查询 {city} 在 {date} 的航班") return { "city": city, "date": date, "flights": [ {"flight_no": "MF8101", "depart": "08:20", "arrive": "10:05", "price": 860}, {"flight_no": "MU5883", "depart": "14:40", "arrive": "16:30", "price": 720}, ], } def search_hotels(city: str, check_in: str, nights: int) -> dict[str, Any]: """演示用Mock酒店查询,返回符合位置的酒店列表。""" print(f"[tool] 查询 {city} 从 {check_in} 起 {nights} 晚酒店") return { "city": city, "check_in": check_in, "hotels": [ {"name": "老城区附近舒适型酒店", "avg_price": 420, "tags": ["老城区", "交通便利"]}, {"name": "海景度假酒店", "avg_price": 890, "tags": ["海景", "偏度假"]}, ], }为什么先写成Mock?因为这样能让你完全聚焦在Agent控制逻辑上。等整条链路跑通后,再替换成真实接口,排查范围会小很多。
6. Function Calling:让Agent学会“调用工具”
Intention模型和工具函数都有了,现在是核心环节:让大模型根据用户输入的旅行意图,决定调用哪个工具、传什么参数。
我们使用OpenAI SDK的Function Calling机制。它做的事情可以简单概括为:告诉模型系统里存在哪些工具,模型根据对话内容返回一个“请求调用工具”的结构化指令,而不是直接生成一段人类自然语言。
创建agent.py:
# 文件路径:traveler-agent/agent.py import json import os from dotenv import load_dotenv from openai import OpenAI from intent_schema import TravelIntent from tools import search_flights, search_hotels load_dotenv() client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("BASE_URL") or None, ) MODEL = os.getenv("LLM_MODEL", "gpt-4o-mini") def build_tools(): return [ { "type": "function", "function": { "name": "search_flights", "description": "查询某城市某一天的航班信息。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "目的地城市"}, "date": {"type": "string", "description": "出发日期,格式YYYY-MM-DD"}, }, "required": ["city", "date"], }, }, }, { "type": "function", "function": { "name": "search_hotels", "description": "查询某城市的酒店列表。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "目的地城市"}, "check_in": {"type": "string", "description": "入住日期"}, "nights": {"type": "integer", "description": "入住晚数"}, }, "required": ["city", "check_in", "nights"], }, }, }, ] def extract_intent(user_input: str) -> TravelIntent: """第一轮:让模型把自然语言解析为结构化约束。""" resp = client.chat.completions.create( model=MODEL, response_format={"type": "json_object"}, messages=[ {"role": "system", "content": "你是旅行意图解析器,只输出JSON。"}, {"role": "user", "content": user_input}, ], ) return TravelIntent.model_validate_json(resp.choices[0].message.content) def run_agent(user_input: str): intent = extract_intent(user_input) print("结构化意图:", intent.model_dump()) tool_map = { "search_flights": search_flights, "search_hotels": search_hotels, } messages = [ { "role": "system", "content": "你是一个旅行管家Agent。请分析用户当前问题,选择需要的工具来查询信息。", }, {"role": "user", "content": user_input}, ] for step in range(3): resp = client.chat.completions.create( model=MODEL, messages=messages, tools=build_tools(), tool_choice="auto", ) msg = resp.choices[0].message if not msg.tool_calls: print("Agent最终回答:", msg.content) return messages.append(msg) for tool_call in msg.tool_calls: func_name = tool_call.function.name func_args = json.loads(tool_call.function.arguments) print(f"[step {step + 1}] 调用工具:{func_name},参数:{func_args}") if func_name not in tool_map: content = json.dumps({"error": f"未知工具 {func_name}"}) else: result = tool_map[func_name](**func_args) content = json.dumps(result, ensure_ascii=False) messages.append( { "role": "tool", "tool_call_id": tool_call.id, "content": content, } ) print("Agent达到最大步骤数,需要人工介入或追问信息。") if __name__ == "__main__": demo_input = "我想下周四去厦门玩4天,预算3500,住在老城区附近,尽量不赶早班飞机。" run_agent(demo_input)这段代码最关键的地方在于messages列表的循环追加逻辑。每次模型请求一个工具调用,我们就执行工具,并把工具结果以role="tool"的消息返回给模型。模型看到真实结果后,才会继续规划下一步。没有这一步,模型只能“猜测”工具能返回什么,就会出现大量幻觉。
运行命令:
python agent.py在真实模型可用的情况下,你会看到类似这样的执行轨迹:
结构化意图: {'destination': '厦门', 'days': 4, 'budget': 3500.0, 'must_have': ['老城区', '本地小吃'], 'avoid': ['早班飞机']} [step 1] 调用工具:search_flights,参数:{'date': '2025-XX-XX', 'city': '厦门'} [tool] 查询 厦门 在 2025-XX-XX 的航班 ... Agent最终回答: 我建议选择下午到达厦门的航班...这里你应该注意一件事:模型要用工具,需要把意图层解析出来的日期、城市等字段作为工具参数传给后端。如果意图解析环节丢字段,后面工具调用就会缺参数或传错值。
7. 运行结果验证:不要只看“回答是否漂亮”
不少初学者跑通一次Agent对话,看到模型说出了一段完整回答,就以为成功了。但在真实场景里,我们要验证的不只是回答内容,而是整个执行链路是否正确。
建议按三个层次验证。
第一层是意图解析的准确性。打印intent.model_dump(),人工检查“目的地、天数、预算、偏好、规避项”是否都对。最容易错的是日期,模型可能根据当前时间推算错“下周”;其次是用户表达偏好时会说反话,比如“不要太赶”,解析器需要把“不要太赶”当成一个真实约束。
第二层是工具调用序列的合理性。检查Agent是先查机票还是先查酒店?查询时是否把用户偏好放进了约束?一个糟糕的Agent可能会在用户没说不考虑价格时,直接忽略价格信息。验证方法很简单:把整个调用过程的JSON日志保存下来,人工看每一步的参数。
第三层是最终答案的可执行性。Agent给用户输出的结果必须包含可操作信息,比如航班时间、航班号、酒店区域、大致的价格区间。如果模型只回答了“我建议你乘坐下午的航班,住在老城区”,却没有提供任何可预订的对象,那它只是把用户的话换了一种说法。
为了便于观察,建议在agent.py里把每一步的工具调用结果都写入日志文件:
with open("trace.jsonl", "a", encoding="utf-8") as f: f.write(json.dumps({"user": user_input, "intent": intent.model_dump()}, ensure_ascii=False) + "\n")生产环境通常会把trace.jsonl换成日志平台,用request_id串起一次Agent会话的全部事件。
如果在实际运行时出现结果异常,先不要急着换模型,按下面这个方向排查:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 工具没有被调用 | 模型不支持Function Calling,或工具描述不清晰 | 查看API返回是否有tool_calls字段 | 换支持工具调用的模型,简化工具名称和描述 |
| 工具参数明显错误 | 意图解析没有保留用户约束 | 打印结构化意图,检查字段是否缺失 | 在系统提示词中要求模型补充缺失参数并二次确认 |
| 模型反复调用同一个工具 | 缺少终止条件或最大步骤限制 | 检查消息历史和调用次数 | 设置max_steps,并让模型在信息充足时直接回答 |
| 工具返回内容模型看不懂 | 返回纯JSON但缺少说明 | 阅读模型下一轮回答 | 在工具返回内容中加入简单中文摘要字段 |
| Agent回答不基于工具结果 | 代码里把工具结果丢弃了 | 检查role=tool消息是否确实追加进了对话 | 把工具结果以tool消息传回,而不是只打印 |
| 涉及支付时存在误下单风险 | 缺少人工确认流程 | 审查代码全部调用点 | 写操作一律拆到独立模块,并增加用户确认接口 |
这个表的最后一行最容易忽略。工具调用在Demo里是查询,到了生产里就会变成“创建订单、锁定库存、发起支付”。任何写操作都必须与Agent推理主循环做物理隔离,不能因为模型多轮对话“表现得聪明”就让它直接执行。
8. 旅行场景里做Agent最容易踩的坑
现在不少团队做旅行助手失败,失败点不在模型不够聪明,而在工程上低估了旅行场景特有的复杂性。下面几个坑,是这个领域比较典型的。
第一个坑是“把动态信息当静态知识”。大模型的知识截止时间是有限的。航班时刻、酒店价格、景区开放状态全部是时效性数据,Agent必须走实时查询接口。如果你依赖模型的内部记忆去回答“明天厦门的天气”“最近大兴机场的航班变化”,系统永远不可靠。
第二个坑是“低估约束冲突处理”。用户口中的偏好经常互相冲突:预算三千五,又要求海景房;时间很短,又希望所有景点都覆盖。朴素实现会把用户约束都塞进搜索条件,然后发现搜不到结果。实际情况里需要Agent能主动跟用户澄清冲突,提出“海景房和预算只能先满足一个,你要哪个优先”。现在的对话Agent通常缺少这种“主动暴露冲突”的设计。
第三个坑是“退改和取消没有纳入执行链”。旅行订单不是一手交钱一手交货,航班延误、酒店不可取消、天气突变,这些都是执行中的常态。意图经济里,Agent要能对已经做过的决策做回滚或改期操作。这要求后端提供完整的取消与改期API,而不是只提供一个预订接口。
第四个坑是“不设计人工兜底”。即使在技术上允许Agent自动完成支付,真实业务也不建议一上来就全自动。稳妥的落地方式是“Agent建议 + 用户确认”,先让Agent处理信息搜集和方案生成,把决策权保留给用户。等运行数据和用户信任积累起来,再逐步开放更多自动化权限。
9. 给开发者和产品团队的三条工程建议
如果你正在考虑做一款“旅游甩手掌柜”类产品,下面几条经验值得留在方案评审里。
第一,把“意图”和“操作”分两个系统实现。不要让同一个大模型既负责高层的行程规划,又负责低层的航班预订。可以用“规划模型”负责拆解和推荐,用“执行服务”负责和真实旅行服务商打交道。这条边界能最大程度减少模型犯错带来的业务风险。
第二,所有写操作接口都要做到幂等。Agent在超时后很可能会重试一次请求。如果接口没有幂等机制,用户可能收到两笔相同订单。工程上可以使用订单号或请求ID去重,每笔操作在进入支付前必须生成唯一业务编号。具体代码如下:
def create_order_with_idempotency(user_id: str, idempotency_key: str, order_data: dict): """根据 idempotency_key 保证同一请求只创建一个订单。""" existing = order_store.get(idempotency_key) if existing: return existing, False order = order_store.create(user_id, order_data) order_store.save_with_key(order.id, idempotency_key) return order, True幂等逻辑不复杂,但它是自动化交易场景的安全底线。
第三,用“可被Agent调用的API”思维改造旅行服务。如果你不准备做面向用户的Agent,也可以做面向Agent的供应商。比如把酒店预订整理成统一Schema,支持JSON输入输出、稳定返回“可订/不可订/价格”、把退改规则结构化。未来一定会有大量Agent需要调用这类服务,谁能提供稳定、低幻觉、带明确错误码的工具接口,谁就能在意图经济里占据位置。
对后端开发者的启发是:意图经济带来的不一定只是“对话式App”,也可能是新一轮API经济。Agent是消费方,你的服务是供给方。要提前考虑接口被机器调用时的行为:错误信息要结构化,字段命名要稳定,API版本要兼容。
10. 总结
回到开头那道题。年轻人想当旅行里的甩手掌柜,这句话真正的含义是:用户愿意把目标表达清楚,也愿意在关键节点做确认,但不希望被搜索列表和比价过程消耗掉所有精力。这背后对应的不是更聪明的聊天机器人,而是一整套“意图解析 -> 任务拆解 -> 工具调用 -> 结果反馈 -> 人工确认”的系统能力。
本文用不到一百行代码,跑通了一个最小旅行Agent的完整链路。它能做的事情还非常有限,但用来理解意图经济的技术骨架已经足够。你可以在这个基础上继续往下做:把Mock工具换成真实供应商API,加入跨会话记忆,增加用户确认界面,再逐步把行程动态调整、订单状态同步等能力接进来。
如果要用一句话给这个方向定调,我会说:意图经济真正的门槛不是“让AI听懂人话”,而是“让AI做对事,并在做错前停下来问你”。谁先把后者做好,谁才能真正赢得想做甩手掌柜的那群年轻人。