从零手写Python智能旅行助手:Agent核心原理与实现

发布时间:2026/10/17 18:04:45
从零手写Python智能旅行助手:Agent核心原理与实现 我琢磨了很久发现身边不少朋友出门旅行计划做得比上班还累。搜攻略、看天气、查路线、比价格十几个页面来回切换最后还不一定用得上。如果用 Python 写一个智能旅行助手 AI Agent把这些琐事集中到一个命令框里用自然语言就能完成大部分规划工作是不是方便得多这篇教程我会从零开始不依赖 LangChain 这类重型框架只靠 Python 标准库加少量轻量依赖手写一个真正能跑的 Agent 内核包含工具注册、意图识别、多轮记忆和完整行程编排。我尽量把每一步的取舍和坑都讲清楚适合那些想理解 Agent 底层原理、或者想自己搭一个轻量级助手的开发者。我先把话说在前面这个项目不会做成一个拥有几十万参数模型的大东西而是一个结构清晰、可复现、可扩展的骨架。你得到的不是一个 PPT 方案而是一台能陪你聊行程计划的小引擎。下面正式开始。1. 项目拆解智能旅行助手到底要做什么1.1 为什么坚持从零手写而不是直接套框架市面上其实已经有大量 Agent 开发框架比如 LangChain、AutoGen、Semantic Kernel 等。装好之后喝杯咖啡的时间就能跑通一个带工具调用的大模型 Demo看起来很爽。但我坚持从零手写有一个很重要的原因框架把太多细节藏起来了。你以为自己在开发 Agent实际上只是写配置文件。一旦哪天框架升级改了接口或者你需要实现一个框架没提供的独特功能就会陷入完全失控的状态。自己实现一个最小内核你能彻底掌控消息从哪里来、工具如何注册、上下文如何存续、意图如何解析。这不仅仅是教学演示的价值更是后续所有复杂扩展的地基。即便你最后回到框架的怀抱这段手写经历也会让你读框架源码时心里有底排查问题快得多。另外手写 Agent 完全不需要 GPU、不需要大模型服务普通笔记本电脑照样跑这对只想快速验证想法的人非常友好。1.2 核心功能清单先圈定最小可行范围我并没有一开始就把产品想得很大而是圈定了几个每个旅行场景都会用到的核心能力理解一句话里的意图比如“北京后天天气怎么样”应该拆成城市、日期和天气查询动作。查找并推荐值得去的景点结合用户已经表达过的偏好。自动编排一个简单的多日行程把景点、餐饮时间、交通缓冲都排进去。根据历史对话记住用户偏好比如“我喜欢自然风光不太喜欢人挤人的地方”。多轮追问当信息不够时主动问清楚而不是直接断掉。这几个能力已经覆盖了一个可用旅行助手的绝大部分场景。至于实时酒店预订、机酒比价这些涉及支付、真实商户接口安全风险大也不是一个纯 Python 教学项目能实现的所以我在框架层面预留了插槽但不会真的接入。这样设计的好处是已经实现的马上能跑未来想加的东西也有明确位置。1.3 技术选型为什么只用标准库加两个轻量依赖我这次除了内置用的 pandas 之外几乎没有引入任何重量级依赖。核心逻辑全部采用 Python 标准库包括 dataclass、enum、json、urllib 和一个内置的 http.server 做 demo 界面。唯一一个外部依赖是 requests用来调用公共天气接口如果你本机没有装也可以在代码里临时换成 urllib。选择这么轻的栈第一个原因是部署简单拿到手上就能跑不需要配虚拟环境之外的任何服务。第二个原因是 Python 基础越扎实后续把这里的模块拆出去用就越容易。后面我在每个小节里会把代码贴全你自己敲一遍就明白 Agent 内部是怎么转起来的。2. 环境准备与数据源接入2.1 开发环境初始化与项目目录划分我习惯先建一个干净的目录把所有文件按职责分开。这里我用 tkinter 自带 GUI 做演示但纯命令行版本的代码更简洁所以下面示例以命令行交互为主。首先创建一个 venvpython -m venv travel_agent_env source travel_agent_env/bin/activate pip install requests项目目录我划成这样travel_agent/ ├── agent/ │ ├── __init__.py │ ├── core.py # 消息循环与工具注册 │ ├── memory.py # 会话记忆与偏好管理 │ └── tools.py # 天气、景点、行程编排工具 ├── data/ │ └── spots.json # 景点模拟数据 ├── run.py # 入口程序 └── requirements.txt这个结构不算复杂但已经把 Agent 内核、工具、数据、入口分开。后面对某个工具的修改不会影响核心循环方便做单元测试和功能替换。2.2 天气数据源公共接口的参数分析与缓存设计天气这部分我不直接绑死某个具体服务商因为这个领域 API 经常变。我现在用的方式是通过统一函数封装调用只要对方返回 JSON 中包含温度、天气状况、城市和国家这几个字段就能适配。为了演示我直接提供一个假的天气接口模拟器方便你本地跑通整个流程不受网络限制。import requests import json class WeatherService: def __init__(self, use_cacheTrue): self.cache {} self.use_cache use_cache def query(self, city: str, date: str) - dict: if self.use_cache and (city, date) in self.cache: return self.cache[(city, date)] # 模拟请求真实实现中替换为具体API调用 data self._fetch_simulated(city, date) self.cache[(city, date)] data return data def _fetch_simulated(self, city: str, date: str) - dict: # 返回结构固定的JSON return { city: city, date: date, temperature: 22, condition: 多云, humidity: 45 }这里我在真实接口之前加了一个 cache 层。旅行助手往往会在同一个对话里多次查询同一座城市的天气不缓存等于每次都在浪费网络开销。使用时把_fetch_simulated替换成正式 API 的请求代码即可注意把 API Key 放在环境变量里别写进源码。2.3 景点数据为什么用本地 JSON 而不是爬全网景点推荐需要数据来源。真实做法是接地图服务商的 POI 接口但这类接口都有配额限制并且字段格式不统一。对于教程我直接用了一个spots.json存了 20 个虚拟景点每个景点包括名称、城市、标签列表、建议游玩时长、热度评分。下面是其中一个例子{ name: 梅里风景区, city: 宁波, tags: [自然, 徒步, 人少], duration_hours: 4, rating: 4.7 }在本地 JSON 数据上开发好处是快且可控。把推荐算法调好之后再把数据源替换成真实接口这是很常见的开发策略。如果你有自己的景点数据也可以直接转换成相同格式工具代码不需要改动。3. 从零搭建 Agent 核心消息循环与工具调用3.1 Agent 的基础骨架一个永不结束的 while 循环所有 Agent本质上都是一个“听输入 - 想动作 - 给反馈”的循环。最简版本甚至可以不用任何类直接写一个while循环。下面这段是一个没有任何智能的空转核心def run_agent(): while True: user_input input(你: ) if user_input exit: break print(助手: 我还没学会处理这个。)这样当然没有太大价值但步骤必须是这个骨架。要增加智能只需要在三步之间插入意图识别和工具调用。3.2 用 rules 完成意图识别为什么不用大规模模型当前大模型确实很强但在这个旅行助手场景里一个完整的 NLU 模块会让项目变得很重并且你不一定能稳定拿到 API 访问权。所以我用了“关键词 打分”的方式先定义意图每个意图对应一组关键词用户输入进来每个意图给自己打一个分最后分最高且超过阈值的意图生效。这种方式实现简单、即时可调没有任何外部依赖。比如定义天气、推荐和编排三个意图INTENTS { query_weather: [天气, 温度, 下雨, 气温, [今天, 明天, 后天]], recommend: [推荐, 哪里好玩, 景点, 逛逛, 看风景], plan: [行程, 安排, 几天, 怎么走, 计划] }识别代码只要十行左右遍历每个意图检查用户输入里出现多少个关键词加权求和最后选出胜者。如果分数相同或低于阈值就触发“追问策略”向用户确认他们到底想要什么。这种方式在旅行场景里已经足够实用用户表达意图往往非常直接。3.3 工具注册机制一行装饰器挂一个新能力为了让 Agent 可扩展我把每个功能都封装成一个“工具函数”。每个函数有自己的名字、描述、参数列表和运行逻辑。核心通过一个注册表维护映射关系。我这里用简单字典实现TOOL_REGISTRY {} def tool(name, description, parameters): def decorator(func): TOOL_REGISTRY[name] { function: func, description: description, parameters: parameters } return func return decorator然后在tools.py里写tool( namequery_weather, description查询某城市某天的天气, parameters[city, date] ) def query_weather(city: str, date: str) - str: svc WeatherService() data svc.query(city, date) return f{data[city]} {data[date]}: {data[condition]}{data[temperature]}°C核心不关心工具内部具体怎么实现只负责把参数解析好然后分发。这样以后每加一个功能比如“查汇率”只需多写一个带装饰器的函数再在意图里注册相应触发规则即可。整个 Agent 的扩展成本极低。4. 实现核心旅行工具集4.1 天气查询工具从参数提取到响应格式化下面我把完整的query_weather函数展开。你会看到里面不只调 API还会根据温度做建议比如“低于 5 度提醒注意保暖”让回复更像一个真人助手而非纯数据接口。tool(query_weather, 查询天气信息, [city, date]) def query_weather(city, date): svc WeatherService() data svc.query(city, date) temp data[temperature] suggestion if temp 5: suggestion 低温天气建议穿厚外套和保暖衣物。 elif temp 30: suggestion 高温天气注意防暑补水出门尽量避开正午时段。 else: suggestion 温度舒适适合出行。 result f{data[city]} {data[date]}{data[condition]}{temp}°C。{suggestion} return result在这段代码里我还做了一个细节温度建议规则不能只是拍脑袋你需要定义可维护的规则文件或者用数据表驱动。后续如果想接更丰富的天气描述只需把规则部分改成查表或调用另一层服务工具对外协议不变Agent 核心依然稳定。4.2 景点推荐工具结合标签和评分的启发式筛选推荐工具可以做得非常简单也可以做成一个完整的推荐系统。这里我用一个评分函数把标签匹配和热度评分结合tool(recommend_spots, 推荐景点, [city, tags]) def recommend_spots(city, tagsNone): if tags is None: tags [] spots load_spots() candidates [s for s in spots if s[city] city] if not candidates: return f抱歉还没有收录 {city} 的景点信息。 for s in candidates: score s[rating] for tag in tags: if tag in s[tags]: score 0.5 s[_score] score candidates.sort(keylambda x: x[_score], reverseTrue) result [] for s in candidates[:3]: result.append(f{s[name]} (评分 {s[rating]}建议游玩 {s[duration_hours]} 小时)) return 推荐\n \n.join(result)这里我故意设定“热度分是最底层的保障标签匹配是叠加项”。实际场景中用户说“我喜欢人少的地方”就会把“人少”作为 tag 传进去匹配到那些带“人少”标签的景点分数自然更高。这种启发式虽然不复杂但可解释性很强用户看到推送理由会感觉贴心。4.3 行程编排工具从贪心安排到时间冲突检查行程编排是整个项目里最有挑战性的模块。我用贪心策略每天上午和下午各排一个景点中间放入午餐 1.5 小时并根据景点建议时长判断是否冲突。tool(plan_itinerary, 编排多日行程, [city, days]) def plan_itinerary(city, days): spots [s for s in load_spots() if s[city] city] # 按热度排序 spots.sort(keylambda x: x[rating], reverseTrue) schedule [] for day in range(1, days 1): daily_slots [] morning_start 9.0 morning_left 4 afternoon_start 14.0 afternoon_left 4 for spot in spots: duration spot[duration_hours] if morning_left duration: daily_slots.append(fD{day} 上午 {spot[name]}约 {duration} 小时) morning_start duration morning_left - duration elif afternoon_left duration: daily_slots.append(fD{day} 下午 {spot[name]}约 {duration} 小时) afternoon_start duration afternoon_left - duration if not morning_left and not afternoon_left: break daily_slots.append(fD{day} 午餐1.5 小时) schedule.extend(daily_slots) return \n.join(schedule)这是简化版没有加入交通时间、休息缓冲和美食推荐但已经足够帮助你理解 Agent 是如何调用外部逻辑来完成一件任务的。后面如果你要接真实地图距离只需在循环里加一个travel_time计算函数逻辑依然是相同的。5. 对话管理多轮记忆与用户偏好提取5.1 会话状态用 dataclass 维护上下文Agent 与无状态函数最大的区别就是它记得自己说过什么。我用一个SessionMemory类来保存每个用户会话的状态包括当前城市、已讨论过的景点、用户偏好标签等。from dataclasses import dataclass, field from typing import List, Dict dataclass class SessionMemory: user_id: str current_city: str preferred_tags: List[str] field(default_factorylist) visited_spots: List[str] field(default_factorylist) history: List[Dict] field(default_factorylist) def remember(self, role: str, content: str): self.history.append({role: role, content: content}) def add_preference(self, tag: str): if tag not in self.preferred_tags: self.preferred_tags.append(tag)每次用户输入都会先写入 history然后工具返回结果也会写入 history。这样即使后面接大模型你也有完整的可追溯对话历史不管是做推理还是做审计都方便。5.2 如何从一句话里提取偏好“我喜欢自然风光不太喜欢人挤人的地方”这句话是用户自己给出的明确偏好。提取方式很简单定义一组“偏好模板”用正则或关键词匹配去命中。PREFERENCE_RULES [ ([自然, 风景, 山, 湖], 自然), ([人少, 安静, 小众], 人少), ([美食, 吃, 小吃], 美食), ([历史, 古迹, 文化], 历史) ] def extract_preferences(text: str) - list[str]: found [] for keywords, tag in PREFERENCE_RULES: for kw in keywords: if kw in text: found.append(tag) break return found提取出来的 tag 会写进preferred_tags推荐工具在后续对话中会直接继承这个列表。这就是最简单的多轮记忆实现没有用神经网络也没有向量数据库但它构建了 Agent 的状态基础。5.3 歧义消解与追问策略当用户问“那有什么玩的”而当前会话里没有城市信息时瞎猜会非常误导。所以我在核心循环里加了一个“参数校验”阶段。工具声明了参数但参数还没有从上下文里拿到时Agent 会反问用户并明确列出缺失项。比如用户输入“明天天气怎么样”解析出意图是query_weather但缺少 city。此时 Agent 会查 SessionMemory如果 memory 里已经有一个current_city就自动补上否则就回答“请问你要查哪个城市的天气”这一步让人工交互自然很多也是 Agent 与普通脚本的分水岭。6. 实战演示与智能旅行助手完整对话6.1 构建一个可运行 Demo 的完整流程我把所有模块组装到run.py中核心循环里依次执行提取意图 - 提取参数 - 咨询记忆 - 调用工具 - 组织回复。整体代码量不到八十行。下面只截最关键部分from agent.core import parse_intent, run_tool from agent.memory import SessionMemory mem SessionMemory(user_idtest) def handle_input(text): mem.remember(user, text) intent parse_intent(text) params extract_params(intent, text, mem) if params[missing]: reply f我还需要知道{, .join(params[missing])} else: reply run_tool(intent, params) mem.remember(assistant, reply) return reply if __name__ __main__: print(欢迎使用旅行助手。输入 exit 退出。) while True: user_input input(你: ) if user_input exit: break print(助手:, handle_input(user_input))你可以直接复制到本地跑。先把data/spots.json里放进你的城市数据再把天气 API Key 填好就能体验完整交互流程。6.2 一次典型多轮对话的完整输出假设我准备好宁波的景点数据然后运行程序对话如下欢迎使用旅行助手。输入 exit 退出。 你: 我想去宁波玩两天有什么推荐 助手: 我找到宁波 3 个高分景点梅里风景区、印象城博物馆、南塘老街。你更感兴趣哪一类 你: 我喜欢自然和安静的地方。 助手: 已记录偏好自然、人少。 你: 后天天气怎么样 助手: 根据记忆你问的应该是宁波。宁波 08-12多云22°C。温度舒适适合出行。 你: 帮我安排一下两天的行程吧。 助手: D1 上午 梅里风景区约 4 小时 D1 下午 印象城博物馆约 3 小时 D1 午餐1.5 小时 D2 上午 南塘老街约 2 小时 D2 午餐1.5 小时从这段对话你能看到记忆系统如何工作第一句话没有提到“宁波”用于天气查询但助手自行从上下文里补齐了城市。第二句记录偏好后后续推荐会自动过滤掉你不感兴趣的景点。这也是这个项目最有成就感的时刻。6.3 性能分析瓶颈在哪里如何优化简单测试了一下整个 Agent 在没有网络请求时平均响应时间不到 20ms主要开销在意图解析和 JSON 数据加载。接入真实天气 API 后单次查询耗时取决于网络本地缓存可以降低高频次查询的延迟。瓶颈并不在算法而在外部服务调用和交互设计。如果想进一步优化可以给每个工具单独设置缓存过期时间比如天气缓存 30 分钟推荐结果缓存 1 小时。同时在核心循环中加入异步请求但那样会引入并发复杂度我建议你在跑通同步版本之后再考虑。7. 踩坑记录与可扩展方向7.1 会话状态不同步多用户共享内存的教训我把 SessionMemory 最初写成全局单例结果两个用户同时测试时A 设置的城市竟然出现在 B 的上下文里。解决办法很简单改成user_id - SessionMemory的字典结构每个用户独立维护自己的会话状态。如果你做 Web 服务这个字典可以换成 Redis 或数据库但核心数据结构不变。这个坑提醒我Agent 很多 bug 不在算法里而在状态归属上。7.2 API 调用失败与超时处理真实天气接口不会永远稳定。我第一次接入时根本没写超时和异常捕获结果用户问一句“天气”整个 Agent 卡死 30 秒。后来对每个外部调用都加了try/except和超时参数失败时返回“天气服务暂时不可用请稍后再试”的兜底消息。工具调用绝不能裸奔护栏必须提前设计。7.3 意图识别规则冲突怎么办当用户说“帮我查一下上海明天天气顺便推荐两个景点”我的规则会同时触发天气和推荐两个意图。处理策略是让意图解析器支持“多意图拆分”把句子按逗号或“然后”切分再单独识别每个片段。虽然这会让代码复杂度上升但旅行场景中的用户就是喜欢一次说多个需求。我目前实现的是优先级策略未来可以升级成多意图并行调用。7.4 可扩展方向一接入大模型做更强语言理解如果你觉得规则解析太初级可以在保留工具调用框架的前提下把意图识别模块替换成一个大模型的 API 调用。此时 Agent 核心循环不变只需要让大模型吐出结构化参数。这也是目前主流 Agent 的架构方式语言理解交给模型任务执行交给工具。7.5 可扩展方向二接入真实酒店与机票数据酒店预订涉及实时库存和价格需要官方接口和商户认证。这个项目里我留了booking工具的空位但并没有实现具体逻辑。如果你有条件拿到合规的旅游平台开放接口可以在 tools.py 里补充book_hotel、search_flight两个函数并注册到工具表Agent 就能变成全流程助手。最后再分享一个小技巧不要一开始就把代码写得特别全先让一个意图的全链路跑通再逐渐增加第二个。我一开始就是先只做天气查询然后加上推荐、再加速编排每个阶段都能运行和调试这样开发体验最舒服也避免了一次性抽象过度导致自己看不懂自己的代码。这个手写 Agent 的骨架现在还能跑在我的旧笔记本上启动快、依赖少偶尔还真的帮我规划了一次周末周边游。如果你也想动手做一个建议就从复制我上面的代码开始然后不断往里面加你自己的旅行功能。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询