导语
在过去的一两年里,大语言模型(LLM)的发展让 AI Agent 迎来了寒武纪大爆发。然而,当我们将 Agent 从简单的“单点对话”推向诸如“端到端旅游规划”这样长链路、多异构数据源的复杂生产场景时,传统基于单体 LLM 的 ReAct (Reason+Act) 模式迅速暴露出其工程瓶颈:响应极慢、状态易崩、逻辑与执行深度耦合。
本文将分享我们团队如何跳出“用一个无所不能的上帝 Prompt 解决所有问题”的思维陷阱,通过引入确定性的代码编排中枢(Orchestrator)、内容感知(Content-Aware)的调度决策以及多智能体异步协作,构建出千万级并发下的新一代旅游规划系统架构。
一、 为什么单体 LLM Agent 在复杂场景下会“死”?
在重构之前,我们和业界许多团队一样,试图构建一个“全能旅行管家”。我们给 GPT-4 塞入了一个长达几千 Token 的系统提示词(涵盖了意图识别、规划逻辑、各种 Search 工具的使用、数据聚合格式等)。
结果在生产环境中,我们遭遇了“灾难级”的体验:
串行阻塞导致的性能灾难:规划一次旅行需要查天气、订酒店、排景点。LLM 只能“思考-调用天气-等待-思考-调用酒店-等待”。这种纯串行机制使得总耗时等于所有外部 API 耗时的简单叠加,用户往往需要等待几分钟才能看到结果。
状态丢失与幻觉:在长达几十轮的内部 Tool Calling 循环中,LLM 极易忘记最初的用户约束(如“不坐飞机”),或者在最后一步整合数据时发生幻觉。
“知识重复抓取”带来的算力浪费:用户问“故宫的历史背景”,Agent 跑去实时抓取网页;下一个用户问同样的请求,Agent 依然去抓网页。LLM 无法区分哪些是高时效的个性化数据(如今天希尔顿的房价),哪些是高通用的静态知识。
我们的核心洞察是:LLM 极其擅长自然语言理解和非结构化数据的抽取,但它绝对不是一个合格的“状态机”或“任务调度器”。
二、 架构演进:让 LLM 做它该做的事,把控制权还给代码
为了解决上述问题,我们彻底重构了系统,提出了一种基于中心化编排 + 多智能体协作的架构。

在这个架构中,我们定义了五类核心组件,明确了各自的边界:
需求分析智能体 (Requirement Analysis Agent):一个极轻量级的 LLM 节点,唯一职责是将用户的自然语言(“想去海边玩三天,预算三千”)提取并补全为结构化的任务简报 (Task Brief) JSON。
旅行规划编排器 (Travel Orchestrator) [核心]:一个基于 Java 和 RxJava 构建的纯确定性代码服务。它不依赖 LLM 生成执行路径,而是根据 Task Brief 动态生成任务执行图 (DAG),进行并发调度。
任务类型决策模块与旅游知识库 (Task Decision & Knowledge Base):编排器内的智能拦截器,负责缓存命中与异构任务的路由。
专业任务智能体 (Specialized Task Agents):被剥离了复杂规划逻辑的“打工人”,分为基于 MCP 协议的直连型 Agent 和基于浏览器自动化的抓取型 Agent。
UI 渲染智能体 (UI Rendering Agent):专门负责将 JSON 转化为精美行程单 HTML,实现数据与表现层的彻底解耦。
三、 硬核解析:我们是如何用代码实现核心机制的?
下面,我们将深入代码底层,看看这些架构理念是如何落地的。
1. 异构任务的并行异步调度 (The Orchestrator)
在单体架构中,LLM 决定先查机票再查酒店。在我们的系统中,编排器接管了调度权。当接收到任务简报后,编排器会将其拆解为独立的子任务,利用 RxJava 的异步响应式流和远端 Agent 通信(A2A Client)实现并行抓取。总耗时从 $O(N)$ 锐减为 $O(\max(T))$。
以下是我们的核心调度代码片段缩影(基于 A2AClientService 与并发组合):
@Service@Slf4j@RequiredArgsConstructorpublic class TravelOrchestrator { private final A2AClientService a2aClient; private final TaskDecisionModule decisionModule; private final SupabaseManager supabaseManager; // 用于状态持久化 /** * 接收任务简报,构建执行图并异步并行拉取数据 */ public Single<TripDataJson> executeTripPlanAsync(TaskBrief brief, String threadId) { log.info("开始编排旅行计划, ThreadId: {}", threadId); // 1. 将简报拆解为原子的信息获取任务 (天气、交通、景点、住宿等) List<SubTask> subTasks = TaskDecomposer.decompose(brief); // 2. 将所有任务转化为异步的 CompletableFuture 流 List<CompletableFuture<JsonNode>> taskFutures = subTasks.stream() .map(task -> CompletableFuture.supplyAsync(() -> { // 3. 核心:通过决策模块路由任务 ExecutionStrategy strategy = decisionModule.determineStrategy(task); if (strategy.isCacheHit()) { return strategy.getCachedData(); // 毫秒级返回 } else { // 动态调用特定的远端专业 Agent String resultRaw = a2aClient.callAgentSync( strategy.getTargetAgentName(), // 例如: "scrape-webpage-agent" task.getPrompt(), threadId, brief.getUserId(), brief.getProjectId() ); // 异步回填通用知识到知识库 if (strategy.isCacheable()) { decisionModule.writeBackToKnowledgeBase(task, resultRaw); } return parseToJson(resultRaw); } })) .collect(Collectors.toList()); // 4. 并行等待所有异构数据源返回,并组合成最终的 JSON CompletableFuture<Void> allOf = CompletableFuture.allOf( taskFutures.toArray(new CompletableFuture[0]) ); return Single.fromCompletionStage(allOf.thenApply(v -> { TripDataJson finalData = new TripDataJson(); taskFutures.forEach(future -> finalData.merge(future.join())); return finalData; })); }}2. 突破效率瓶颈:内容感知(Content-Aware)的决策模块
这是竞争对手短期内难以逾越的壁垒。通用的编排框架(如 LangChain 的 SequentialChain)是“无感知”的,它不知道自己调度的内容是什么。
我们的 TaskDecisionModule 引入了领域知识。在分发任务前,它会拦截任务并进行判断:这究竟是一个需要实时挂载无头浏览器去抓取的“动态个性化任务”,还是一个可以直接走 Redis/Supabase 知识库的“静态通用任务”?
@Componentpublic class TaskDecisionModule { private final RedisSupplier redisSupplier; public ExecutionStrategy determineStrategy(SubTask task) { // 策略 1: 强时效性数据 (如:特定日期的酒店价格,机票) if (task.getCategory() == Category.REALTIME_PRICE) { return new ExecutionStrategy("browser-agent", false, null); } // 策略 2: 通用知识 (如:北京故宫的历史介绍) if (task.getCategory() == Category.STATIC_KNOWLEDGE) { String cacheKey = "kb:travel:" + task.getHash(); try (Jedis jedis = redisSupplier.get()) { String cachedInfo = jedis.get(cacheKey); if (cachedInfo != null) { log.info("🎯 知识库命中! 极速返回: {}", task.getQuery()); return new ExecutionStrategy(null, true, cachedInfo); } } // 缓存未命中,分发给便宜且快速的文本爬虫 Agent,并标记可缓存 return new ExecutionStrategy("scrape-webpage-agent", true, null); } // 默认走标准搜索 MCP return new ExecutionStrategy("general-search-agent", false, null); }}通过这种知识库前置拦截+按需执行的机制,对于热门城市的常规规划请求,我们的系统能将超过 60% 的子任务直接通过内存返回。这带来了意想不到的技术效果:复杂行程的生成速度从分钟级直接拉升到了秒级(Sub-Second 级别体验)。
3. MVC 模式的回归:UI 渲染智能体的独立
在早期版本中,我们要求核心 LLM 直接输出带有复杂交互逻辑的 HTML 页面。结果是灾难性的:HTML 标签极易闭合错误,样式混乱,而且极大地消耗了主流程的 Token 额度。
我们最终意识到,AI 应用也必须遵循传统软件工程的 MVC (Model-View-Controller) 原则。
在我们的架构中,编排器(Controller)产出的是一份纯粹的、严格结构化的 trip_data.json(Model)。最后,我们通过 A2AClient 将这份 JSON 通过任务转移(task_transfer)交给专门的 UI 渲染智能体(View Agent):
// 在 TravelAgentService.java 中的最后一步:数据与表现的分离private Flowable<SendStreamingMessageResponse> renderUI(String requestId, TripDataJson tripData, String invocationId) { // 1. 此时的数据已经是高度确定、结构化的 JSON String jsonData = objectMapper.writeValueAsString(tripData); // 2. 委派专门的渲染 Agent 进行前端代码生成 String htmlRendered = a2aClient.callAgentSync( "ui-rendering-agent", "使用提供的 JSON 数据,注入至行程单预设模板中,生成可交付的 HTML: " + jsonData, ... ); // 3. 将页面作为最终资产交付 (基于自定义的资源流视图协议) AdkPart part = Resources.HtmlView.createPart(UUID.randomUUID().toString(), Map.of("url", htmlRendered)); return Flowable.just(responseFactory.wrapEvent(requestId, part, invocationId, "assistant"));}将“数据整合的终点”和“视觉呈现的起点”进行硬切分,使得我们可以随时更换前端模板,且彻底杜绝了数据处理逻辑与 UI 代码混杂导致的崩溃。
四、 总结与展望 (2026 年的思考)
时至今日,随着 MCP (Model Context Protocol) 的普及以及各类小参数特化模型的成熟,AI Agent 的架构正在发生深刻的范式转移。
我们构建的这套多智能体旅游规划系统证明了一个观点:未来的复杂 AI 应用,绝不是靠一个巨大的模型加无数的 Prompt 堆砌出来的,而是靠优秀的软件工程架构设计出来的。
通过将不确定的规划逻辑剥离给确定性的代码编排器,将异构数据获取下放给多模态专业智能体,并引入基于知识库的智能拦截机制,我们成功解决了异构数据整合难、响应速度慢、系统扩展性差的痛点。
“不写代码”的纯 Prompt 工程师时代正在过去,懂得如何将 AI 能力(Agentic APIs)作为组件,完美融入分布式、高并发、基于状态机制的现代工程架构中的架构师,才是这个时代真正的主力军。
作者简介:深耕大后端与 AI 架构领域,致力于探索 LLM 在高并发业务场景下的工程化落地。





