基于Multi-Agent的智能旅行助手:从架构拆解到落地实践

📅 发布时间:2026/10/17 7:42:24
基于Multi-Agent的智能旅行助手:从架构拆解到落地实践
上个月帮一个朋友做周末出行安排酒店、车票、餐厅、景点来回查了大半天最后还因为预算超支推倒重来。这种又琐碎又费神的事就是我动手做“基于multi-agent的智能旅行助手”的直接原因与其让人在一堆网页和App之间来回切不如让几个各司其职的AI角色分头查资料、做推荐、核算预算再由一个主控Agent把零散结果拼成完整行程。如果你正在研究Agent应用怎么落地或者单纯被做攻略折磨过这篇文章应该能给你一个看得懂、能改得动的参考。这个项目本质上解决的是一个典型问题旅行规划属于需求模糊、约束多、信息分散的场景。用户常常只说得出“想出去玩几天、预算大概多少”但说不清具体去哪个区域、住什么档次、吃什么东西。传统对话式助手在这种场景下很容易“聊着聊着就偏了”而Multi-Agent架构恰好能把大问题拆成几个小任务分给多个专职角色并行处理再由协调者统一裁决。接下来我会从架构拆解、通信设计、核心实现到完整实操把整个项目的关键思路和踩坑过程都梳理一遍。1. 一开始我为啥要自己搭一个旅行助手1.1 传统单体Prompt方案的三个硬伤在动手做Multi-Agent之前我其实先用“一个Prompt直接生成完整攻略”的老路子试过。方式是把目的地、天数、人数、预算、偏好一次性塞给模型让它直接输出一份行程单。表面上看省事实际用下来问题非常明显。第一个问题是上下文一长就开始“失忆”。当需求细节超过一定量模型在后半段生成时很容易忘记前文设定比如酒店推荐跑到另一个城市、第二天的行程和第一天的时间重叠、预算加总后明显超支但模型完全没察觉。这跟提示词写得好不好关系不大是单体上下文结构承受不住这么多信息的同时约束。第二个问题是改一个环节等于全部重来。出行日期一变机票价格、酒店空房、景点开放时间全都要重新推断用户中途说“预算收紧一点”前面生成的餐厅和住宿方案就失去参考意义。单体方案每一次需求变更都意味着整段对话重新生成一遍token成本和等待时间都不可接受。第三个问题最隐蔽用户需求一开始往往是模糊的。他说“想轻松一点”到底是指行程节奏慢还是住宿档次高还是不想走太多路单体对话虽然也能追问但追问多了之后早期那些对话内容很容易被后续的产出稀释掉模型渐渐搞不清哪些是最终约束哪些是已经被推翻的讨论。我当时拿一个“三天两晚、预算2500、带老人小孩”的真实需求试过模型给出的行程第二天下午连续排了四个景点老人体力根本扛不住。出现这种结果不是单个提示词的问题而是架构设计的问题——单体模型擅长处理单一目标明确的任务不适合同时协调大量约束冲突的复合任务。1.2 Multi-Agent架构带来的本质变化后来我把方案改成Multi-Agent本质上做了一件事把“一个全能选手”换成“一个明确分工的团队”。每个Agent只负责一件小事情比如住宿Agent只管找酒店、餐饮Agent只管挑餐厅每个Agent收到的上下文都短而聚焦模型注意力不会分散输出的稳定性明显提高。这里面有一个生活化的类比你不可能要求一个全科医生同时精通骨科、眼科和心内科但如果让一个门诊部里的几位专科医生分别看自己领域的问题再由一位全科医生汇总判断效率和准确率都会高很多。Multi-Agent旅行助手就是这个门诊部专科Agent各查各的规划协调者做最终汇总。这个架构带来的第二个好处是并行能力。住宿、交通、餐饮、景点这些子任务彼此相对独立可以同时启动查询不用像单体对话那样一步一步串行推进。我实测过四个研究员Agent并行跑一轮信息采集整体耗时大约只有单体对话的三分之一到四分之一。第三个好处是系统可维护性。想调整酒店推荐策略只需要改住宿Agent的Prompt或工具参数其他Agent完全不受影响。这在单体方案里是没法做到的——牵一发而动全身。对项目后续迭代来说这种解耦价值非常大。1.3 什么样的场景才值得上Multi-Agent这里要说句实话不是所有场景都值得上Multi-Agent。如果任务足够简单比如“把这段文字翻译成英文”或者“帮我写一封请假邮件”一个单体Prompt加一次调用就搞定了强行拆Agent只会增加延迟、成本和系统复杂度。我判断一个场景适不适合Multi-Agent主要看三个特征第一是任务有没有多个相对独立的子领域第二是子任务之间是否存在大量约束冲突需要裁决第三是用户需求是否需要多轮确认和动态调整。旅行规划三条全占所以它是一个非常典型的Multi-Agent落地场景。反过来如果你的需求只是单一领域的查询老老实实用单体Prompt反而是最理性的选择。2. Multi-Agent架构是怎么拆出来的2.1 角色规划一张Agent职责表拆解Multi-Agent架构第一步不是写代码而是确定要哪些角色。我在项目里最后定下来六个角色每个角色都有清晰的职责边界、工作内容和输出格式。Agent名称核心职责主要工具/数据源关键输出规划协调者 Planner澄清需求、拆解任务、汇总结果、裁决冲突无只做调度与汇总完整行程单交通研究员 Transport Agent查询往返交通方案、时间与价格车票/机票查询API交通方案列表住宿研究员 Stay Agent查询酒店/民宿、位置、价格、评分住宿搜索API、地图API住宿候选方案餐饮打卡研究员 Lifestyle Agent推荐餐厅、热门景点与体验项目点评数据源、景点热度数据餐饮与游玩清单预算合规员 Guardian校验总预算、检查时间可行性、识别冲突无对产出做规则校验冲突报告与调整建议验证员 Verifier复查关键数据价格、营业时间的真实性详情页访问工具、搜索API验证结果标记角色数量的选择是有讲究的。太少每个Agent职责过重又回到单体问题太多调度成本高通信复杂度爆炸。我试过把交通拆成“机票Agent”和“火车Agent”表面上看更细实际使用中发现两者的工作流程高度相似强行拆分只会增加重复代码和消息数量。对旅行助手来说六个角色是一个性价比比较高的配置。2.2 每个角色的输入输出契约角色定完之后下一步是给每个Agent定义输入输出格式。我用的是一张“任务卡”作为标准输入结构大概是这样的任务编号、用户核心目标、硬性约束列表、偏好权重字典、截止时间。每个Agent收到任务卡后只需要关注自己负责的那部分字段。输出格式统一采用JSON并且强制附带引用编号。拿住宿Agent举例它的输出大概是三到五个住宿方案每个方案包含名称、区域、价格、评分、到核心景点的距离以及信息引用来源。这个设计解决了两个问题一是后续Planner做汇总时不需要解析自由文本直接读字段就行二是每个推荐都带引用方便验证员复查避免模型凭空编造不存在的酒店。统一数据契约最大的价值在于后续想换掉任何一个Agent的底层模型只要它输入输出还是这套格式其他Agent完全不需要改动。我在项目中期把其中一个研究员Agent从自研的规则增强方案换成了更便宜的模型整个替换过程只改了一个配置文件这就是契约设计带来的工程红利。2.3 为什么不要用“让Agent们自由聊天”的方案这里必须说一个我自己踩过的坑。项目早期我为了追求“Agent之间像真人一样协作”设计了一套自由讨论机制让多个Agent像群聊一样互相发言Planner只是旁听。结果发现这条路完全走不通。首先Token消耗爆炸式增长。四个Agent围绕“预算有限但想吃好一点”这个话题来来回回讨论一轮下来消耗的Token是单体方案的五倍以上其中大部分是没有任何信息增量的废话。其次Agent们会陷入循环说服。每个Agent都基于自己的局部视角坚持意见没有全局裁决者讨论很容易演变成各说各话。最后这种自由讨论的产出完全不可控你永远不知道最终结论会在第几轮出现也没法保证结论质量。后来我把整个通信机制改成“星型结构”一切请求从Planner发出各子Agent只跟Planner通信子Agent之间不直接对话。所有消息都是结构化任务和结构化结果不再有自由发言。这个改动直接让系统从“看起来很酷”变成“真正能用”Token消耗降了大半输出稳定性也好了一个量级。3. Agent之间的协作机制与通信协议3.1 我用的消息格式与总线设计Multi-Agent系统能不能稳定运行通信协议是关键。我设计了一套轻量的消息总线每条消息包含六个字段from发送方、to接收方、trace_id链路追踪、type消息类型、payload消息内容、refs引用列表。trace_id非常关键。一次完整的行程规划可能涉及几十条消息如果没有链路ID排错时根本不知道该跟踪哪条消息是从哪个任务派生出来的。我在每条消息创建时生成一个UUID后续所有Agent在处理该任务时都复用这个ID日志里一搜trace_id就能看到整条处理链路的流转过程。消息总线本身就是一个带队列的内存容器Planner发出任务后等待对应结果研究员Agent完成后把结果写入总线并通知Planner。这里我做了两个保险措施一是每条任务消息都带超时时间超过时限Planner会先查缓存没有缓存就重新调度而不是无限等下去二是每个Agent的输出必须明确标记为“完整结果”或“信息不足”不允许不返回任何结果避免出现任务派发出去却石沉大海的情况。3.2 冲突裁决预算约束碰上用户偏好怎么办旅行规划里最常遇到的情况是约束冲突想住的区域很好但价格超预算想吃的餐厅评分很高但距离住宿地点太远。这些冲突如果指望用户自己一个个去权衡那这个系统的意义就少了一大半。我的处理办法是把冲突裁决全部收拢到Planner用一套可配置的权重规则来决策。默认权重是硬性预算 时间可行性 热门口碑 距离远近。预算作为硬约束计划生成阶段谁也不能超剩下的偏好指标按权重打分排序。但权重不是写死的而是保存在一个独立的配置项里用户可以在对话里随时调整比如强调“我对吃的重视程度超过距离”Planner就会重算分数、重新排序推荐。举一个实际例子预算合规员发现餐饮Agent推荐的三家餐厅都明显超预算于是触发降级规则。Planner保留其中口碑最高的一家同时从“预算内备选列表”里补两家补充方案并且在最终输出里明确标注“以下前两项已超出预算请确认是否接受”。这么做的好处是不因为硬约束直接抹掉用户偏好而是把决策权交还给用户同时保证大部分方案还在预算框架内。3.3 并行调度与超时控制系统既然用了多个Agent理论上可以并行跑但并行不能无限制。我实际操作时研究员类Agent的最大并发数控制在3到4个。原因有两个第一是外部搜索API有频控限制并发太高会触发限流第二是并行调用模型服务的成本是叠加的无脑并发容易让账单迅速变难看。Planner在派发任务时会先做一个依赖分析住宿、交通、餐饮三个子任务彼此无依赖可以并行验证员需要等研究员产出后才能启动属于第二阶段。整个调度过程我用了一个简单的线程池第一阶段三个研究员同时跑等所有结果回来后Planner做一轮汇总然后唤醒验证员和预算合规员做并行检查。实测下来这个两阶段流水线比完全串行快很多也比无脑并行稳定很多。超时控制方面我给每个子任务设置了30秒到60秒不等的超时阈值。超时后Planner先看本地缓存有没有相关历史数据有就直接用旧数据并在结果里标注“数据时效性较低”没有缓存则重新派发一次。这个机制很有用因为搜索API偶尔会长时间无响应没有超时控制整个调度会被一个慢请求卡死。4. 核心实现拆解Planner状态机与研究员Agent4.1 Planner的六状态状态机整个系统的核心是Planner它决定了一次行程规划从开始到结束要经历哪些阶段。我用状态机来控制Planner的行为总共六个状态INIT、CLARIFY、FAN_OUT、SYNC、REVIEW、OUTPUT。INIT阶段就是接收用户原始请求做需求解析CLARIFY阶段负责追问模糊信息比如人数、预算上限、节奏偏好FAN_OUT阶段做任务拆解并派发给各子AgentSYNC阶段等待收集所有子Agent的结果REVIEW阶段让预算合规员和验证员检查数据完整性和约束冲突OUTPUT阶段把校验通过的结果格式化成最终行程单输出。这套状态机最有价值的一点是它让“不可控的Agent行为”变成了“可控的流程步骤”。不管子Agent的输出有多发散Planner的行为始终沿着六个状态线性推进。项目调试时状态机日志可以清楚显示当前卡在哪一步、是哪条消息出了问题排查效率高很多。CLARIFY阶段我做了专门的上下文管理。用户多轮补充的信息会不断更新到一张“需求画像表”里而不是追加聊天记录。比如用户先说“预算3000”后来说“其实是2500”需求画像表里永远是2500不会因为上下文里有两处不同数字导致模型理解混乱。4.2 研究员Agent的实现要点研究员Agent是系统里数量最多的角色它们的实现直接影响最终推荐质量。我这里分享几个关键实现细节。第一是提示词模板化。我不用自由对话的方式让研究员Agent干活而是把它的指令固定成模板只留一个“当前待查主题”的插槽。比如住宿Agent的模板是“用户需求是___约束条件是___请返回三个住宿候选每个候选取名叫JSON对象必须包含价格、评分、距离字段”。模板化能让每个Agent的每次调用都保持行为一致不会因为对话历史积累而产出跑偏。第二是工具调用的返回必须落库。研究员Agent调用搜索API拿到数据后不会直接把原始数据塞给模型生成推荐而是先把参考资料存储到本地数据库并分配引用编号然后模型基于编号引用这些资料做推荐。这样做的目的是阻断模型“编造”的路径——它只能引用编号对应的真实数据不能自己凭空渲染一个不存在的酒店名。第三是温度参数设置要保守。给研究员Agent的模型调用我把temperature调到0.2甚至更低目的很简单这些角色做的是信息筛选和整理不是创意写作偏低温度能让输出更贴近检索到的真实信息减少幻觉。4.3 推荐热力图生成器是怎么加的项目做到中后期我发现单纯给用户列一串推荐列表用户对目的地“哪里更好玩”其实没有直观感知。于是我加了一个附加功能推荐热力图生成器在行程预览地图上展示各区域的热度分布。实现思路不复杂先把目标城市切成一堆网格坐标然后把所有候选POI餐厅、景点、商圈按坐标映射到网格统计每个网格里的POI数量、评分加权值生成一个热度分数最后在展示层按分数映射成不同的颜色深浅。工作流程上这块由餐饮打卡研究员在采集POI数据时顺便生成统计数据已经在那里了只是多一次聚合计算。这个功能看起来很轻量但在实际体验上提升非常明显。用户不用一个个查看推荐点位一眼就能看出“东部区域明显更热闹”或者“市区餐饮密度远高于南边”做区域选择的时候就有了依据。这也说明Multi-Agent系统的扩展逻辑新功能如果只是基于已有数据做加工整合成本很低。5. 实操记录从零搭出最小可用的Multi-Agent原型5.1 技术选型与运行环境这部分写给想照着做一遍的读者。整个原型我用Python实现核心依赖只有三个一个能通过API访问的对话模型服务、一个轻量级本地数据库用于缓存检索结果、一个通用的地图/搜索API用于真实数据获取。模型选型方面不需要一步到位上最贵的模型。实际测试下来一个推理能力中等偏上的通用模型已经足以支撑研究员Agent的日常工作Planner因为要做汇总裁决可以用能力稍强一档的模型。项目里我把模型选择做成了配置文件不同Agent可以走不同的模型方便在效果和成本之间取平衡。运行环境非常简单一台普通开发机能跑关键外部依赖就是搜索API的访问权限。数据库用来做两级缓存一级是任务级缓存同一行程内重复查询直接复用二级是历史级缓存同一城市的历史查询结果在数据未过期时可以直接取用省下大量外部API调用。5.2 代码骨架三十行跑通多Agent调度以下是整个多Agent调度最核心的骨架代码去掉业务细节后其实很短重点看消息派发和结果汇总的方式。from concurrent.futures import ThreadPoolExecutor import uuid, json class MessageBus: def __init__(self): self.inbox {} def send(self, to, msg): self.inbox.setdefault(to, []).append(msg) def receive(self, agent_name): return self.inbox.pop(agent_name, []) class Agent: def __init__(self, name, model_client): self.name name self.model model_client def run(self, task): raise NotImplementedError class Planner(Agent): def __init__(self, model_client, sub_agents): super().__init__(planner, model_client) self.sub_agents sub_agents def run(self, user_request): # 阶段1澄清需求生成任务卡 task_card self._clarify(user_request) # 阶段2并行派发子任务 with ThreadPoolExecutor(max_workers3) as pool: futures [ pool.submit(agent.run, task_card) for agent in self.sub_agents ] results [f.result(timeout60) for f in futures] # 阶段3汇总 冲突裁决 return self._synthesize(results) # 使用示例 stay_agent StayAgent(stay, model) sync_agent TransportAgent(transport, model) planner Planner(model, [stay_agent, sync_agent]) itinerary planner.run(某城市两天一晚预算2000)这段代码只有三个类消息总线、Agent基类、Planner。子Agent只需要继承Agent基类并实现run方法。run方法内部做什么完全自治Planner只关心结果。实战里我在这个骨架上补充了超时、重试、缓存、结果存储但核心调度逻辑几乎没有变过。如果你也想快速验证一个Multi-Agent想法先把这个骨架跑通再逐步加业务细节是最高效的路径。5.3 实测一个完整行程生成的现场记录下面记录一次完整的实测过程。用户输入原话是“周末两天一晚去某南方城市预算2000以内主要想吃点本地菜节奏不要太赶。”系统跑完后的产出流程如下。交通研究员返回往返高铁票两档总价约500元。住宿研究员返回市区三家评分4.6以上的酒店一晚价格集中在300到450元。餐饮打卡研究员返回五家本地菜餐厅其中两家在景区周边三家在老街区。预算合规员做了加总发现预算内可以覆盖。验证员复查了其中一家的营业时间确认标注的下午休息时段属实。最终Planner汇总出了一份行程单第一天上午出发中午抵达下午先去老街区逛两三个相邻的点位晚上吃其中一家本地菜餐厅第二天早上吃另一家特色餐厅上午再逛一个免费博物馆午后返程。行程节奏安排得比较松没有把景点塞满符合用户“节奏不要太赶”的偏好。这次完整规划消耗的Token大约在6500左右耗时约40秒。对比我早期用单体Prompt做相同需求经常要两三轮对话才能收敛而且过程中容易顾此失彼。40秒的成本换来一份跨多个信息源的完整行程这个性价比我是可以接受的。6. 常见问题与避坑手册实测踩坑实录6.1 Agent之间“踢皮球”互相推诿导致任务流产Multi-Agent系统最典型的问题就是“踢皮球”——A把任务推给BB说这不归我管又推给CC又让A来处理。我早期在自由讨论模式中遇到过好几次这种循环最终任务没有任何产出。出现这个问题的根子在于角色边界不清晰或者消息协议里没有强制返回值。解决办法是双管齐下一方面明确每个Agent的职责范围和它不能处理的情况比如住宿Agent如果发现目标区域没有可选酒店只能返回“区域无房源”加上周边区域替代建议不允许返回一个空结果另一方面Planner限制递归深度如果一条任务被转手超过两次直接升级给用户做人工选择不让系统在内部无限循环。6.2 上下文爆炸与记忆丢失项目初期我把所有Agent的通信记录都原样存储在Planner的上下文里结果跑到第五六个子任务时Token已经接近上限Planner开始“忘记”用户最开始提到的预算约束。这个问题在Multi-Agent系统里比单体对话更严重因为消息量是成倍增加的。我的解决方案是两层记忆管理。第一层Planner只保留每个子Agent最近一次正式产出的摘要原始的完整长文本存到外置数据库需要时按trace_id查询第二层需求画像等关键信息始终同步在任务的全局字段里每次汇总读取直接读全局字段不依赖对话历史也不依赖各Agent的返回文本。做了这个改动之后即使行程规划拉得再长Planner的核心约束也不会丢失。6.3 模型幻觉污染关键数据有一轮测试里餐饮Agent推荐了一家“评分4.9”的餐厅验证员复查发现这家餐厅在该城市根本没有分店。模型不是故意骗人而是在生成推荐时脱离了检索数据自己“脑补”了一个看起来合理的方案。这个问题在关注价格的场景里尤其危险价格差几十块可能直接影响用户决策。根治思路是前文提到的引用编号机制每个Agent的推荐必须绑定真实检索数据的引用ID没有引用ID的推荐在汇总阶段直接丢弃。同时我增加了一个验证员角色负责抽查关键字段价格、营业时间、地址是否和引用数据一致不一致的方案会被标记成“数据存疑”需要重新查询或降级处理。加了这道工序之后系统输出里的幻觉数据比例降到了可以接受的范围。6.4 并行调用多了账单很吓人Multi-Agent的并行特性是一把双刃剑。第一次全量并行运行四个Agent做完整规划时单轮消耗的Token比我预想高了一倍原因是有多个Agent在做重复的检索和模型调用。我在账单上踩过一次坑之后学乖了开始做成本和效果的平衡。控制成本最有效的手段是加缓存。同一城市、同一时间段的历史查询结果能复用就复用同一行程里多个Agent可能查询同一个景点信息我会做一个轻量的去重避免重复调用同样的搜索。还有一个技巧是给每个Agent设置输出长度上限研究员Agent的方案列表限制在三个到五个够用就行不需要列十个候选。这些调整对推荐质量的影响很小但账单数字能降下来不少。6.5 扩展节奏先做半自动再谈全自动规划项目后期时我一度想着做全自动行程生成用户一句话系统直接输出完整行程不做任何人工确认。试了几轮后发现这个目标在现阶段不太现实因为行程规划里的隐性约束太多了比如“室内和室外的平衡”“一天走路的强度”“雨天备选方案”这些问题自动识别和自动权衡很容易出错。后来我调整了产品形态采用半自动流程Planner生成的行程单会标出几个需要用户确认的关键决策点比如住宿区域选择、预算分配偏好。用户确认或修改之后系统再走一遍校验和微调。这个改动表面上少了“全自动”的酷炫感但实际转化率、满意度都高了很多。做Agent应用稳定可用比什么都重要。7. 后续还能怎么扩展项目告一段落后我发现几条值得继续做的线路。行程冲突检测是目前最值得深挖的方向现在的Planner虽然能做预算和时间的硬校验但对“路线绕路”“景点组合不合理”这类软性冲突判断还比较弱。另一个方向是多人偏好融合一家三口出行孩子、老人、成年人的偏好差异很大让Planner把不同成员的偏好权重分开处理会比现在单一需求画像更贴合真实场景。最后分享一个小技巧我把Planner的偏好权重和裁决规则全部做成了JSON配置文件调整某个权重或者加一条冲突处理规则不需要改一行业务代码。比如想让“距离”的优先级从第三名升到第一名只需要改两个数字。在实际使用中这个配置化思路让我能快速根据反馈调整系统行为不用每次改完都重新上线一整个版本。这个项目最让我满意的不是最终行程生成得多漂亮而是它证明了一件事Multi-Agent架构只要把角色边界切清楚、通信协议定规范、约束校验交给独立角色就能把原本一团乱麻的复合任务拆成一条稳定可控的生产流水线。旅行只是一个起点同样的架构思路放到其他多约束复杂场景里一样是成立的。