
我去年年底接手了一个很有意思的项目给一家做行程规划工具的团队重构他们的核心能力把原来一个单体大Prompt驱动的问答机器人改造成一个基于multi-agent架构的智能旅行助手。整个过程从架构设计、角色拆分到踩坑调试、效果评测前后折腾了将近两个月。这篇博文就是把这段实战经历完整复盘一遍从需求拆解到系统落地再到那些文档里不会写的坑一次性讲清楚。不管你是被多Agent概念吸引想动手试试还是已经在做类似场景但被各种奇葩bug折磨这篇应该都能给你一些参考。1. 为什么传统旅行App解决不了“真问题”1.1 你搜索到的不是答案而是一堆待处理的碎片先从一个很朴素的问题说起我们真的缺旅行信息吗完全不缺。打开任何一款主流旅行应用你能查到航班、酒店、景点评分、天气预报、餐厅推荐信息量巨大。但问题是这些信息全部是“碎片”需要用户自己去拼装。查航班要去机票模块查酒店要去酒店模块查景点要切到社区笔记查天气还得跳出App打开另一个工具。一趟行程涉及到十几个约束条件用户实际上在脑子里跑着一个非常复杂的整合程序。我在接手这个项目前做了不少用户访谈。印象最深的一位用户说她过去三年每次家庭出游都要花四五个晚上做行程不是因为不知道去哪儿而是因为“要把机票、酒店、孩子午睡时间、老人走路速度全都揉在一起每次都会漏掉一两项”。这句话基本戳中了传统旅行工具的命门它们擅长提供选项不擅长做决策擅长展示信息不擅长处理约束。而多Agent架构天然适合解决的问题恰恰就是这种多约束、多来源、多子任务的整合型任务。1.2 旅行计划本质上是一个多约束优化问题如果把一次旅行计划当成一个工程问题来看它其实是一个典型的多约束优化预算上限、日期区间、人员构成、体力阈值、兴趣偏好、天气条件、交通衔接、用餐需求。这些约束互相耦合比如选了某个航班就会影响落地时间和第一天行程酒店位置选得偏远每天的交通成本就会上升景点安排在户外就得看天气预报来定顺序。传统的单体大模型Prompt方案在处理这个问题时有个结构性缺陷所有约束都被塞进同一个上下文里模型需要在一次推理中同时记住预算、天气、家庭状况、交通时刻表等大量事实还要做多步规划。一旦上下文变长模型很容易“忘掉”早期约束或者为了输出一个看似完整的行程而做出局部最优选择。我此前用一个超大上下文模型做单体Prompt原型实测到了第三天行程经常出现预算超支或者把两小时车程的景点排进同一上午的情况。这不是模型不行而是任务结构本身就该拆开处理。1.3 为什么多Agent架构天然适配旅行规划场景多Agent系统的核心思路不是“一个超级智能体处理所有事情”而是“让多个专注的智能体分别处理自己最擅长的子任务再通过协作机制整合结果”。这恰好对应了旅行规划这种高复杂度、高耦合、强流程性的场景机票比价是一个子任务酒店筛选是另一个子任务景点路线规划又是一个子任务。每个子任务的判断逻辑、工具依赖、评价指标完全不同让同一个模型角色通吃所有子任务必然导致能力和注意力被摊薄。更重要的是多Agent架构在工程上给了我们一个关键优势可观测性和可干预性。单体Prompt是一个黑盒你很难知道行程出错是哪里出的错但多Agent框架下每个子任务有独立模块、独立输出哪一环出错一目了然。我们后续做调试和效果优化几乎全是靠这个特性才得以推进的。2. 把“旅行管家”拆成一支Agent小队角色设计与编排机制2.1 角色划分不是所有任务都该由同一个Agent做这是整个项目里最核心的设计决策到底拆成几个Agent、每个Agent负责什么。最初我们团队内部讨论过很多方案有主张极简的“规划师执行者”两个角色也有主张把航班、酒店、餐厅全部拆成独立Agent的“全家桶”方案。最后落地的方案折中成了五个角色分别覆盖一次行程规划的核心链路。Agent角色核心职责关键工具依赖Planner规划师理解用户需求拆解约束生成整体行程框架会话记忆、偏好画像Researcher研究员检索目的地信息、景点开放时间、交通衔接方案搜索API、知识库Bookkeeper预订管家查询比价航班、酒店、门票给出可预订方案票务API、比价工具WeatherGuard天气顾问获取并解释天气预报评估对行程的影响天气APIReviewer质检员检查生成行程的冲突、超预算、节奏不合理校验规则引擎每个角色都保持“职能隔离”Planner不直接调用搜索APIResearcher不直接生成最终行程。这样设计的直接好处是Prompt可以做得非常专注不需要在一长串系统提示里告诉模型“你既要做规划又要调用工具还要检查错误”角色职责越窄行为越稳定。2.2 通信协议Agent之间说什么语言拆完角色后的第一个问题是Agent之间怎么通信最省事的方式是让Agent之间直接用自然语言传消息就像真人团队在群里聊天一样。但这个方案在实测中立刻暴露了问题Agent之间的闲聊式沟通会产生大量冗余信息而且每个Agent对同一句自然语言的理解可能不一致。举个例子Previous Agent说“后天下午的航班价格有点贵”Next Agent会无法判断“有点贵”到底贵了多少、需不需要触发改价。我们的解决方式是定义一套结构化的通信协议所有消息都带两层结构自然语言摘要结构化数据字段。摘要给大模型看方便理解意图结构化数据给系统层面做校验和路由。{ from: Researcher, to: Planner, summary: 已确认目标景点本周开放时间周六9:00-18:00无闭馆安排, data: { poi_id: poi_101, open_date: 2025-03-15, open_time: 09:00, close_time: 18:00, status: confirmed }, intent: update_poi_info }这样做的好处是当Planner读到这条消息时既可以依赖摘要快速理解内容又可以调用data字段里的结构化数据做精确判断避免“可能”“大概”这类模糊表述对下游造成污染。2.3 编排器与共享状态谁说了算有了角色和通信协议还得有个东西来协调它们。这个角色我们称为编排器Orchestrator在实现上它不是一个独立的Agent模型而是一个运行时分发与状态管理组件。它负责三件事把用户的初始需求分发给合适的Agent维护一份全局共享的“当前规划状态”对象在每轮Agent输出后进行冲突检测和流程推进决策。这里要特别强调一点编排器不应该接管用户的对话意图理解。我们第一版尝试让编排器自己做一次“理解用户→生成全局计划→分发子任务”的流程结果发现编排器变成了一个隐形的单体大脑其他Agent变成了它调用的函数本质上又退化成了大Prompt方案。后来调整策略编排器只做轻量路由和状态维护对话理解和全局规划仍然由Planner负责。这个调整对系统的模块化程度影响非常大。3. 从零实现一个可运行的Multi-Agent旅行助手3.1 技术选型与架构思路真正动手写代码之前我们先定了两个原则。第一不是必须用现成的多Agent框架关键是团队能理解和维护跑通流程比赶时髦重要。市面上确实有一些现成的多Agent编排框架抽象程度高拿出来就能起一个多Agent演示但真要接入旅行场景的各种工具API会发现封装太厚底层细节被遮住之后出了问题反而难排查。我们最终的自研架构很轻量消息总线用异步队列实现注册表记录每个Agent的职责和工具列表共享状态存储内存态JSON后续可替换为Redis运行时调度逻辑共四部分。模型层优先选了通用对话模型后来根据各Agent的侧重做了差异化部署Planner和Reviewer用强推理型号其余Agent用兼顾成本和速度的标准型号。3.2 核心模块的实现要点先看Agent的基类定义。我们把Agent设计成“一个接收消息、调用工具、产出消息的循环单元”每个Agent内部由Prompt模板、工具列表、记忆片段三部分组成。class TravelAgent: def __init__(self, role_name, system_prompt, toolsNone, memory_storeNone): self.role_name role_name self.system_prompt system_prompt self.tools tools or [] self.memory_store memory_store async def handle_message(self, message): context self._build_context(message) tool_results await self._dispatch_tools(context) response await self._call_llm(context, tool_results) return self._format_response(response) async def _dispatch_tools(self, context): results {} for tool_request in context.get(tool_calls, []): tool self._find_tool(tool_request[name]) # 统一记录工具调用日志用于后续调试 results[tool_request[name]] await tool.execute(**tool_request[args]) return results代码本身不复杂但有三个细节值得说明一是工具调用不直接写在Prompt里让模型自由发挥而是通过一个独立的ToolCaller中间层统一调度。模型只需要输出“我想查询航班条件是XX”系统解析后把它变成结构化工具调用。这样可以防止模型跳过工具、直接编造数据。二是共享状态存储必须带上版本号。每次修改共享行程状态都会生成新版本Agent引用状态时带版本号防止读到旧数据导致行程信息前后矛盾。三是消息总线里的每一条消息都记录完整的调用链消息ID、来源、目标、时间戳这个last-mile日志能力在排障时价值巨大。3.3 一个完整的请求处理链路从用户输入到行程生成用一个实际案例来看整体链路。假设用户输入“清明节假期想带爸妈去某古城预算50004天3晚不要太累爸妈喜欢历史”。链路第一步编排器把请求路由给Planner。Planner通过共享状态做约束分析抽取核心约束日期范围、人数2大1老、预算上限、偏好类型历史文化、节奏慢。然后Planner生成初期框架拆出三个子任务交通查询、住宿筛选、景点路线规划。第二步编排器把三个子任务依次分发给对应Agent。交通衔接交给Bookkeeper它调用票务API获取往返车次与价格回传选项列表。住宿查询同样由Bookkeeper处理调用酒店搜索工具。景点规划则分给Researcher它检索古城主要人文景点、开放时间、建议游览时长。第三步各个Agent的回传消息汇聚到共享状态Planner基于整体状态做整合生成一份包含交通、住宿、每日行程的完整草稿。草稿不会直接输出而是先送到Reviewer做冲突检查。Reviewer在检查时发现了两个问题一是第一天安排了三个景点总游览时间加交通衔接时间会超过十小时和“不要太累”约束冲突二是酒店报价加上交通支出后预算余量已经不多但第三天计划里的特色餐厅人均消费偏高。Reviewer把这两个冲突标注回去由Planner修正方案后再次送审通过后才正式输出给用户。这整条链路在单体Prompt方案里无法显式存在因为所有动作都发生在一次推理中中间步骤不可观测更不可能在输出前做独立的冲突校验。这也是我们后来在宣传这个项目价值时最常拿来说明的技术差异点。4. 实测踩坑与调试心得那些文档里不会写的坑4.1 Agent之间“信息打架”根因在共享上下文设计错误第一个大坑出现在联调阶段TravelPlanner已经规划好第二天的行程但Researcher在一次搜索后回传了关于第三天天气的特殊提醒结果Planner把两天的信息搞混把第三天的调整应用到了第二天。查日志发现根本原因是共享状态里存的是“一个完整的每日行程对象”而非按日期索引的原子对象Agent在更新时容易整段覆盖。后来我们把共享状态改成按日期内容类型双维度存储每次更新只允许写入最小粒度的字段比如day2.weather_alert不涉及day2.attractions的内容就无权写入。这个改动完全消除了覆盖型串改问题。经验是多Agent系统中的共享状态一定要设计成“按最小可变单元锁定”的存储模型而不是一个粗粒度的大JSON否则越往后改越疼。4.2 工具调用失败后Agent学会了“编答案”这是大模型应用里一个经典陷阱工具API调用失败时如果返回值是空的模型倾向于在回复里保持流畅性从而“脑补”出看似合理的数据。我们的Bookkeeper在某个票务接口超时后竟然输出了一张不存在的“早鸟特价机票信息”震惊了全组人。解决这些办法上我们分了三层保障。第一层工具层所有工具调用必须返回结构化状态字段包括success/failure/error_code处理第二层Agent的系统Prompt里显式写入“未收到真实数据时必须声明数据缺失不得补全”第三层编排器在收集到工具失败消息时自动触发“重试或换源”策略而不只是把失败信息传给下一个Agent必要时会中断当前链路向用户说明“这个信息暂时没查到”。4.3 记忆污染上一次的行程计划影响了下一次的规划有用户连续两次使用助手做旅行查询中间间隔很短结果第二次查询时Planner推荐的景点和餐厅里混入了第一次行程中没有清理干净的偏好痕迹。排查后确认是记忆存储的一个低级设计失误记忆片段的过期策略没有生效旧会话的摘要被错误覆盖到了新会话。我们的修复方案是引入会话级隔离每个用户session持有独立记忆空间其中通用偏好如“用户喜欢历史类景点”可以跨会话保留但具体行程细节只能在当前会话内有效。这个“层级化记忆”的设计比简单地在缓存里加个时间戳要可靠得多因为不是所有记忆都该同等对待。4.4 评估系统的建立不能只看生成文本动不动听多Agent系统最大的评估难点在于最终输出是一个行程用户的“满意”由非常多的隐性指标组成。只请人来打分效率低且主观。我们建立了一个半自动评估流水线对每条测试样例自动计算几类量化指标。评估维度指标说明约束满足率预算、日期、人数、偏好是否全部满足核心指标不达标的用例不进入人工评审信息真实性工具调用成功率、数据缺失率检查Agent是否在无数据时编造信息规划合理性景点地理距离、交通衔接时间、节奏密度用规则引擎对生成的行程做合理性校验用户交互效率交互轮数、平均等待时长不希望用户为一次查询等太久这个评估体系上线前我们的测试方式是肉眼扫答案很多问题容易被有吸引力的行程文本掩盖上线后约束满足率从未达标组的82%提升到92%以上信息缺失率从11%降到3%效果非常显著。任何在认真做多Agent系统的人都应该尽早建立量化评测机制这能帮你绕开“看起来很好但细看全漏洞”的巨大陷阱。5. 进阶优化与实际效果从能跑到好用5.1 效果对比单体Prompt vs Multi-Agent交付前的验收测试里我们用60个旅行规划测试样本对比了原单体Prompt方案和新的多Agent方案。样本覆盖不同人数、不同预算区间、不同兴趣组合既有周末短途也有跨省长途。对比维度单体Prompt方案Multi-Agent方案约束满足率79%93%行程冲突产生率34%7%平均交付时间15.2秒24.8秒用户后续追问修正率41%23%数据很清楚多Agent方案在规划质量上有明显优势代价是推理链路变长导致响应时间增加但可以用模型选型、并行调度等方式抵消一部分。这个借鉴关系也印证了我一贯的看法多Agent并不是为了炫技存在的它的价值体现在当任务复杂度足够高时把误差缩小到可控范围。5.2 低成本进阶技巧从Mock数据到真实API很多个人开发者或小团队想复现类似项目最卡人的是API成本。我们的建议是先全部用Mock数据跑通系统闭环。票务工具返回固定的模拟航班表天气工具返回预设的几套天气场景酒店工具返回一个固定候选池。多Agent系统的核心逻辑在于角色拆分、通信协议和编排调度这些环节不依赖真实数据的复杂度。用Mock跑通后再逐个替换真实API。每替换一个就多跑一遍全部回归测试确保新的真实数据源不会引入协议不一致。这样的渐进式替换方式有两个好处一是系统始终处于可运行状态不会出现过长的空窗期二是能定位到每个真实API接入时引起的独立问题排查范围被限制到最小。5.3 后续还能怎么扩展目前这套系统已经稳定运行了一个多月下一步计划的重点有三个方向。一是用户画像的长期沉淀。现在通用偏好已经能跨会话保留但还没有形成结构化的用户画像模型比如“带着老人的家庭出行”“情侣打卡游”“独自特种兵式旅行”这些细分场景如果画像素能被自动构建和复用后续规划质量会再上一个台阶。二是实时动态规划。目前的天气和交通信息是查询时快照式的但真实旅行中会有航班晚点、景点临时关闭、天气突变等情况。让Agent系统在行程执行期间持续监听事件流并主动触发重规划是多Agent架构非常自然的延展方向也是单体方案最难实现的能力。三是多语言和跨文化场景适配。目前主要支持中文场景但如果把Planner、Reviewer的系统Prompt切换到不同地域文化模板同一套Agent框架可以很方便地迁移到东南亚、欧洲等热门旅行目的地。这个项目做下来我最深的体会是多Agent系统的难点从来不在“多”而在“Agent”本身有没有被设计清楚。角色边界模糊、通信协议混乱、共享状态粗放数量再多也只会互相制造噪音。反过来只要把每个Agent的职责边界、消息格式和状态管理彻底想明白了系统的能力边界会变得非常清晰。做技术选型或者写代码之前先把这几个问题回答好后面自然会顺利很多。