写点什么

重塑 AI Agent 架构:摒弃“上帝 Prompt”,基于状态驱动与多智能体协作的复杂旅游规划实践

  • 2026-03-18
    北京
  • 本文字数:4492 字

    阅读完需:约 15 分钟

导语

在过去的一两年里,大语言模型(LLM)的发展让 AI Agent 迎来了寒武纪大爆发。然而,当我们将 Agent 从简单的“单点对话”推向诸如“端到端旅游规划”这样长链路、多异构数据源的复杂生产场景时,传统基于单体 LLM 的 ReAct (Reason+Act) 模式迅速暴露出其工程瓶颈:响应极慢、状态易崩、逻辑与执行深度耦合

本文将分享我们团队如何跳出“用一个无所不能的上帝 Prompt 解决所有问题”的思维陷阱,通过引入确定性的代码编排中枢(Orchestrator)内容感知(Content-Aware)的调度决策以及多智能体异步协作,构建出千万级并发下的新一代旅游规划系统架构。

一、 为什么单体 LLM Agent 在复杂场景下会“死”?

在重构之前,我们和业界许多团队一样,试图构建一个“全能旅行管家”。我们给 GPT-4 塞入了一个长达几千 Token 的系统提示词(涵盖了意图识别、规划逻辑、各种 Search 工具的使用、数据聚合格式等)。

结果在生产环境中,我们遭遇了“灾难级”的体验:

  1. 串行阻塞导致的性能灾难:规划一次旅行需要查天气、订酒店、排景点。LLM 只能“思考-调用天气-等待-思考-调用酒店-等待”。这种纯串行机制使得总耗时等于所有外部 API 耗时的简单叠加,用户往往需要等待几分钟才能看到结果。

  2. 状态丢失与幻觉:在长达几十轮的内部 Tool Calling 循环中,LLM 极易忘记最初的用户约束(如“不坐飞机”),或者在最后一步整合数据时发生幻觉。

  3. “知识重复抓取”带来的算力浪费:用户问“故宫的历史背景”,Agent 跑去实时抓取网页;下一个用户问同样的请求,Agent 依然去抓网页。LLM 无法区分哪些是高时效的个性化数据(如今天希尔顿的房价),哪些是高通用的静态知识

我们的核心洞察是:LLM 极其擅长自然语言理解和非结构化数据的抽取,但它绝对不是一个合格的“状态机”或“任务调度器”。

二、 架构演进:让 LLM 做它该做的事,把控制权还给代码

为了解决上述问题,我们彻底重构了系统,提出了一种基于中心化编排 + 多智能体协作的架构。

在这个架构中,我们定义了五类核心组件,明确了各自的边界:

  1. 需求分析智能体 (Requirement Analysis Agent):一个极轻量级的 LLM 节点,唯一职责是将用户的自然语言(“想去海边玩三天,预算三千”)提取并补全为结构化的任务简报 (Task Brief) JSON。

  2. 旅行规划编排器 (Travel Orchestrator) [核心]:一个基于 Java 和 RxJava 构建的纯确定性代码服务。它不依赖 LLM 生成执行路径,而是根据 Task Brief 动态生成任务执行图 (DAG),进行并发调度。

  3. 任务类型决策模块与旅游知识库 (Task Decision & Knowledge Base):编排器内的智能拦截器,负责缓存命中与异构任务的路由。

  4. 专业任务智能体 (Specialized Task Agents):被剥离了复杂规划逻辑的“打工人”,分为基于 MCP 协议的直连型 Agent 和基于浏览器自动化的抓取型 Agent。

  5. 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 在高并发业务场景下的工程化落地。