基于Spring AI Alibaba构建智能点餐助手Agent

📅 2026-05-12 22:29:59阅读时间: 49分钟

手把手带你入门Agent开发,实现一个能理解自然语言、自动推荐菜品的AI点餐助手

一、背景:从规则到智能

传统的点餐推荐通常由硬编码逻辑实现——根据人数计算菜品数量,按荤素比例筛选,最后返回结果。这种方式虽然确定、高效,但缺乏灵活性:用户无法用自然语言表达“今晚4个人,想吃辣一点的,预算200左右”,更无法追问调整。

随着大模型和Agent技术的成熟,我们可以构建一个智能点餐Agent:用户用平常说话的方式提出需求,Agent自动调用工具函数完成推荐,并以友好的格式返回结果。

Spring AI Alibaba 提供了完整的 Agent 开发框架(基于 ReactAgent),结合阿里云 DashScope 大模型,可以快速搭建此类应用。

本文将带你从零开始,开发一个功能完整的智能点餐助手。


二、环境准备

2.1 基础要求

  • JDK 17+
  • Maven 3.6+
  • 阿里云 DashScope API Key(免费申请

2.2 创建Spring Boot项目

使用 Spring Initializr 或 IDE 创建项目,依赖如下:

xml 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

<properties>
    <java.version>17</java.version>
</properties>

<dependencies>
    <!-- Spring Boot Web(可选,测试用) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI Alibaba Agent Framework -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-agent-framework</artifactId>
        <version>1.1.2.0</version>
    </dependency>

    <!-- DashScope ChatModel 支持 -->
    <dependency>
        <groupId>com.alibaba.cloud.ai</groupId>
        <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
        <version>1.1.2.0</version>
    </dependency>
</dependencies>

2.3 配置文件 application.yml

yaml 复制代码
spring:
  ai:
    dashscope:
      api-key: ${QWEN_API_KEY}   # 替换为你的真实API Key,或配置环境变量
      chat:
        options:
          model: qwen-plus        # 推荐使用 qwen-plus,推理能力强
          temperature: 0.7        # 适度随机性,让回复更自然

三、项目代码实现

我们将分三步完成:

  1. 定义菜单数据与工具类MenuTools
  2. 配置AgentAgentConfig
  3. 测试运行Controller

3.1 定义菜品实体(Record)

MenuTools.java 内部定义一个 Dish record,包含 id、名称、菜系、辣度、适合人数、价格。

java 复制代码
public record Dish(
    int id,
    String name,
    String type,      // 川菜、粤菜等
    int spicy,        // 0-5 辣度
    int people,       // 建议份量对应人数
    int price
) { }

3.2 核心推荐工具 getRecommendDishes

这是Agent调用的主要工具。方法上使用 @Tool 注解,Spring AI 会自动生成工具描述,供大模型理解。

关键逻辑

  • 根据人数计算推荐菜品总数(经验公式:2人→3菜,4人→4菜,6人→5菜,8人→7菜,9人+→9菜)。
  • 根据口味偏好(如“辣”“清淡”“川菜”)预筛选菜单。
  • 将筛选后的菜品分为大菜(≥50元或硬菜)、普通荤菜素菜
  • 按荤素6:4比例 + 大菜数量规则,选取菜品。
  • 返回 JSON 数组,每个菜品包含 idnameprice 等信息(理想情况应包含 count,此处简化)。
java 复制代码
@Tool(name = "getRecommendDishes", description = "根据用餐人数和口味偏好(如:辣、清淡、粤菜、川菜等)从菜单中检索合适的菜品,并生成包含菜品ID、名称和份数的点餐清单")
    public String getRecommendDishes(@ToolParam(description = "用餐的人数", required = true) int numOfPeople, int peopleCount,
                                     @ToolParam(description = "口味偏好,如:辣、清淡、粤菜、川菜等", required = true) String tastePreference) {
        if (peopleCount <= 0) {
            return "[{\"error\":\"用餐人数必须大于0\"}]";
        }

        // 0. 根据口味偏好筛选菜单
        List<Dish> filteredMenu = filterMenuByTaste(menu, tastePreference);
        if (filteredMenu.isEmpty()) {
            // 完全没有匹配项时,降级为全量菜单
            filteredMenu = new ArrayList<>(menu);
        }

        // 1. 从筛选后的菜单中分离大菜、普通荤菜、素菜
        List<Dish> bigDishes = new ArrayList<>();
        List<Dish> normalMeat = new ArrayList<>();
        List<Dish> vegetarian = new ArrayList<>();

        for (Dish dish : filteredMenu) {
            if (isBigDish(dish)) {
                bigDishes.add(dish);
            } else if (isMeatDish(dish)) {
                normalMeat.add(dish);
            } else {
                vegetarian.add(dish);
            }
        }

        // 按价格降序排序,优先推荐高价位菜品
        bigDishes.sort((d1, d2) -> Double.compare(d2.price(), d1.price()));
        normalMeat.sort((d1, d2) -> Double.compare(d2.price(), d1.price()));
        vegetarian.sort((d1, d2) -> Double.compare(d2.price(), d1.price()));

        // 2. 确定推荐菜品总数(荤素合理、避免浪费)
        int totalCount = calculateTotalDishes(peopleCount);
        totalCount = Math.min(totalCount, filteredMenu.size());

        // 3. 确定大菜数量(根据人数调整)
        int bigCount = Math.min(bigDishes.size(), calculateBigDishCount(peopleCount, totalCount));

        // 4. 荤素比例(荤菜约占60%,素菜约占40%)
        int targetMeatCount = (int) Math.round(totalCount * 0.6);
        int normalMeatNeeded = Math.max(0, targetMeatCount - bigCount);
        normalMeatNeeded = Math.min(normalMeatNeeded, normalMeat.size());

        int vegNeeded = totalCount - bigCount - normalMeatNeeded;
        if (vegNeeded < 0) {
            normalMeatNeeded += vegNeeded;
            vegNeeded = 0;
        }
        vegNeeded = Math.min(vegNeeded, vegetarian.size());
        if (vegNeeded < totalCount - bigCount - normalMeatNeeded) {
            int extraMeat = totalCount - bigCount - normalMeatNeeded - vegNeeded;
            normalMeatNeeded = Math.min(normalMeatNeeded + extraMeat, normalMeat.size());
            vegNeeded = totalCount - bigCount - normalMeatNeeded;
        }

        // 5. 选取菜品
        List<Dish> recommendation = new ArrayList<>();
        recommendation.addAll(bigDishes.subList(0, Math.min(bigCount, bigDishes.size())));
        recommendation.addAll(normalMeat.subList(0, Math.min(normalMeatNeeded, normalMeat.size())));
        recommendation.addAll(vegetarian.subList(0, Math.min(vegNeeded, vegetarian.size())));

        // 6. 若数量不足,从剩余菜品中补充
        if (recommendation.size() < totalCount) {
            Set<Integer> selectedIds = recommendation.stream().map(Dish::id).collect(Collectors.toSet());
            List<Dish> remaining = filteredMenu.stream()
                    .filter(d -> !selectedIds.contains(d.id()))
                    .collect(Collectors.toList());
            int need = totalCount - recommendation.size();
            for (int i = 0; i < need && i < remaining.size(); i++) {
                recommendation.add(remaining.get(i));
            }
        }
        return JSONArray.toJSONString(recommendation);
    }

引用方法如下:

java 复制代码
/**
     * 根据口味偏好筛选菜单
     *
     * @param menu            原始菜单
     * @param tastePreference 口味偏好字符串,例如:"辣", "清淡", "川菜,湘菜", "不辣 粤菜"
     * @return 符合偏好的菜品列表(可能为空)
     */
    private List<Dish> filterMenuByTaste(List<Dish> menu, String tastePreference) {
        if (tastePreference == null || tastePreference.trim().isEmpty()) {
            return new ArrayList<>(menu);
        }

        String lowerPref = tastePreference.toLowerCase().trim();
        // 分割关键词(支持中英文逗号、空格)
        String[] keywords = lowerPref.split("[\\s,,]+");

        List<Dish> result = new ArrayList<>();
        for (Dish dish : menu) {
            if (matchesPreference(dish, keywords)) {
                result.add(dish);
            }
        }
        return result;
    }

    /**
     * 判断单个菜品是否匹配用户的偏好关键词
     */
    private boolean matchesPreference(Dish dish, String[] keywords) {
        for (String kw : keywords) {
            if (kw.isEmpty()) continue;
            switch (kw) {
                case "辣":
                case "微辣":
                case "中辣":
                case "重辣":
                    if (dish.spicy() > 0) return true;
                    break;
                case "不辣":
                case "清淡":
                    if (dish.spicy() == 0) return true;
                    break;
                case "川菜":
                case "粤菜":
                case "湘菜":
                case "京菜":
                case "浙菜":
                case "苏菜":
                case "鲁菜":
                case "家常菜":
                case "素菜":
                    if (dish.type().equals(kw)) return true;
                    break;
                default:
                    // 其他关键词尝试匹配菜名或分类
                    if (dish.name().toLowerCase().contains(kw) ||
                            dish.type().toLowerCase().contains(kw)) {
                        return true;
                    }
            }
        }
        return false;
    }

    // 判断是否为大菜(价格≥50 或 特定大菜)
    private boolean isBigDish(Dish dish) {
        return dish.price() >= 50 ||
                dish.name().contains("烤鸭") ||
                dish.name().contains("鱼头") ||
                dish.name().contains("烧鹅");
    }

    // 判断是否为荤菜(根据名称和分类)
    private boolean isMeatDish(Dish dish) {
        String name = dish.name();
        String cat = dish.type();
        // 素菜系列全部是素
        if ("素菜".equals(cat)) return false;
        // 明确素食菜名
        return !name.contains("生菜") && !name.contains("西兰花") && !name.contains("茄子") &&
                !name.contains("四季豆") && !name.contains("包菜") && !name.contains("黄瓜") &&
                !name.contains("豆腐") && !name.contains("土豆丝") && !name.contains("西红柿炒蛋") &&
                !name.contains("地三鲜") && !name.contains("臭豆腐");
        // 蛋类归为半荤,这里简单视为荤(丰富口感)
    }

    // 根据人数确定推荐菜品总数(经验公式)
    private int calculateTotalDishes(int peopleCount) {
        if (peopleCount <= 2) return 3;
        if (peopleCount <= 4) return 4;
        if (peopleCount <= 6) return 5;
        if (peopleCount <= 8) return 7;
        return 9;  // 9人以上最多推荐9道菜,避免浪费
    }

    // 根据人数和总菜数计算大菜数量
    private int calculateBigDishCount(int peopleCount, int totalCount) {
        if (peopleCount <= 2) return Math.min(1, totalCount);
        if (peopleCount <= 4) return Math.min(2, totalCount / 2);
        return Math.min(3, totalCount / 2);
    }

测试菜单列表数据如下:

java 复制代码
// 初始化菜单数据(35个菜品)
        this.menu = Arrays.asList(
                // 川菜系列 (8个)
                new Dish(1, "宫保鸡丁", "川菜", 3, 2, 38),
                new Dish(2, "麻婆豆腐", "川菜", 4, 1, 18),
                new Dish(3, "水煮牛肉", "川菜", 5, 3, 58),
                new Dish(4, "回锅肉", "川菜", 4, 2, 42),
                new Dish(5, "鱼香肉丝", "川菜", 2, 2, 32),
                new Dish(6, "夫妻肺片", "川菜", 3, 2, 36),
                new Dish(7, "辣子鸡", "川菜", 5, 3, 48),
                new Dish(8, "口水鸡", "川菜", 3, 2, 35),

                // 粤菜系列 (6个)
                new Dish(9, "清蒸鲈鱼", "粤菜", 0, 4, 68),
                new Dish(10, "白切鸡", "粤菜", 0, 3, 58),
                new Dish(11, "烧鹅", "粤菜", 0, 4, 78),
                new Dish(12, "叉烧", "粤菜", 0, 2, 45),
                new Dish(13, "蚝油生菜", "粤菜", 0, 2, 28),
                new Dish(14, "煲仔饭", "粤菜", 0, 1, 32),

                // 家常菜系列 (7个)
                new Dish(15, "西红柿炒蛋", "家常菜", 0, 1, 22),
                new Dish(16, "酸辣土豆丝", "家常菜", 2, 1, 16),
                new Dish(17, "地三鲜", "家常菜", 1, 2, 28),
                new Dish(18, "红烧肉", "家常菜", 1, 3, 52),
                new Dish(19, "糖醋排骨", "家常菜", 0, 2, 48),
                new Dish(20, "木须肉", "家常菜", 0, 2, 35),
                new Dish(21, "青椒肉丝", "家常菜", 1, 2, 30),

                // 素菜系列 (5个)
                new Dish(22, "蒜蓉西兰花", "素菜", 0, 2, 26),
                new Dish(23, "红烧茄子", "素菜", 1, 2, 24),
                new Dish(24, "干煸四季豆", "素菜", 2, 2, 25),
                new Dish(25, "手撕包菜", "素菜", 1, 2, 20),
                new Dish(26, "凉拌黄瓜", "素菜", 0, 1, 15),

                // 湘菜系列 (4个)
                new Dish(27, "剁椒鱼头", "湘菜", 4, 4, 72),
                new Dish(28, "辣椒炒肉", "湘菜", 4, 2, 38),
                new Dish(29, "农家小炒肉", "湘菜", 4, 2, 40),
                new Dish(30, "臭豆腐", "湘菜", 3, 1, 18),

                // 其他特色菜 (5个)
                new Dish(31, "北京烤鸭", "京菜", 0, 4, 128),
                new Dish(32, "东坡肉", "浙菜", 1, 3, 58),
                new Dish(33, "狮子头", "苏菜", 1, 3, 55),
                new Dish(34, "黄焖鸡", "鲁菜", 2, 2, 42),
                new Dish(35, "酸菜鱼", "川菜", 3, 3, 58)
        );

3.3 辅助工具(可选)

为了增强Agent能力,还可以提供获取全量菜单、按菜系筛选、按价格筛选等工具。示例:

java 复制代码
@Tool(description = "获取完整菜单")
public String getAllMenuItems() { return JSONArray.toJSONString(menu); }

@Tool(description = "根据菜系类型(如:川菜、粤菜、家常菜、素菜等)筛选菜品")
    public String getDishesByType(String cuisineType) {
        if (cuisineType == null || cuisineType.trim().isEmpty()) {
            return "请指定菜系类型";
        }

        List<Dish> filtered = menu.stream()
                .filter(dish -> dish.type().contains(cuisineType.trim()))
                .collect(Collectors.toList());

        if (filtered.isEmpty()) {
            return "未找到" + cuisineType + "类型的菜品";
        }

        return JSONArray.toJSONString(filtered);
    }

3.4 配置 ReactAgent

AgentConfig 中,使用 ReactAgent.builder() 创建一个能调用工具的 Agent。ReactAgent 是 Spring AI Alibaba 提供的实现 ReAct(Reasoning + Acting) 模式的智能体,它能根据用户问题自动决定调用哪个工具,并将结果整合后回复。

java 复制代码
@Configuration
public class AgentConfig {

    @Bean
    public ReactAgent orderingAgent(DashScopeChatModel chatModel, MenuTools menuTools) {
        // 注册工具
        ToolCallbackProvider toolProvider = MethodToolCallbackProvider.builder()
                .toolObjects(menuTools)
                .build();

        // 设定系统指令(prompt)
        String instruction = """
                你是点餐助手。直接调用 getRecommendDishes 工具(参数:人数、口味偏好),
                并将工具返回的 JSON 数组原样输出,不要添加任何解释或额外格式。
                """;

        return ReactAgent.builder()
                .name("restaurant_ordering_agent")
                .model(chatModel)
                .toolCallbackProviders(toolProvider)
                .instruction(instruction)
                .build();
    }
}

3.5 提供 HTTP 接口(便于测试)

创建 OrderController,接收用户自然语言请求,转发给 Agent 处理。

java 复制代码
@RestController
public class OrderController {
    private final ReactAgent agent;

    public OrderController(ReactAgent orderingAgent) {
        this.agent = orderingAgent;
    }

    @PostMapping("/recommend")
    public String recommend(@RequestBody String userInput) {
        // Agent 直接返回字符串结果
        return agent.chat(userInput);
    }
}

四、运行与测试

启动 Spring Boot 应用,用 curl 或 Postman 测试:

bash 复制代码
curl -X POST http://localhost:8080/recommend \
  -H "Content-Type: text/plain" \
  -d "3个人,想吃辣一点的,不要猪肉"

Agent 会调用 getRecommendDishes(3, "辣 不要猪肉"),工具内部会智能过滤含猪肉的菜品(如回锅肉、红烧肉等),返回类似这样的结果:

json 复制代码
[
  {"id":3,"name":"水煮牛肉","type":"川菜","spicy":5,"people":3,"price":58},
  {"id":27,"name":"剁椒鱼头","type":"湘菜","spicy":4,"people":4,"price":72},
  {"id":24,"name":"干煸四季豆","type":"素菜","spicy":2,"people":2,"price":25}
]

你也可以去掉“原样输出”的指令,让 Agent 对结果进行自然语言解释,例如:“根据您3人、辣味的偏好,为您推荐:水煮牛肉(58元)、剁椒鱼头(72元)、干煸四季豆(25元),总计155元。”


五、核心原理解析

5.1 ReAct Agent 工作流程

  1. 用户输入 → Agent 获得自然语言请求。
  2. 推理(Reasoning):大模型分析需要调用哪个工具以及参数。
  3. 行动(Acting):调用注册的工具函数(如 getRecommendDishes)并得到返回值。
  4. 整合输出:模型根据工具结果和原始指令生成最终回复。

整个过程完全自动化,无需手动编写 if-else 判断。

5.2 @Tool 注解的作用

  • 自动将 Java 方法转换为 OpenAI 兼容的 Function Calling 描述。
  • 方法参数上的 @ToolParam 会生成参数说明,帮助大模型准确提取信息。
  • 支持返回 String、Map、List 等类型,框架会自动序列化。

5.3 为什么需要传统逻辑代码?

有人可能会问:为什么有了大模型,还要写 calculateTotalDishesfilterMenuByTaste 这些规则?
因为大模型不擅长精确的数值计算和复杂的约束优化(比如保证荤素比例 60:40)。最佳实践是:

  • 大模型负责意图理解(提取人数、偏好、禁忌)
  • 传统代码负责确定性计算(搭配逻辑)
  • 二者通过工具调用无缝结合

六、进阶优化建议

  1. 支持份数控制
    getRecommendDishes 返回时给每个菜品加上 count 字段(大菜1份,小菜也可1份,人数过多可增加)。

  2. 多工具协作
    添加 calculateTotalPrice 工具,让 Agent 在推荐完菜品后询问用户是否预算超支,并自动调整。

  3. 对话记忆
    ReactAgent 支持 chatWithMemory 方法,配合 InMemoryChatMemory 实现多轮对话,例如用户说“把那个水煮牛肉去掉,换一个便宜的”。

  4. 自定义输出格式
    修改 instruction,让 Agent 以表格或自然语言返回,同时保留原始 JSON 给前端。


七、总结

通过本文,你已经掌握了:

  • 使用 Spring AI Alibaba 快速搭建 Agent 应用
  • 通过 @Tool 注解将业务逻辑暴露给大模型
  • 如何编写一个兼顾规则与智能的点餐推荐助手

整个开发过程不需要复杂的 prompt 工程,也不需要维护对话状态框架,Spring AI Alibaba 的 Agent 模块让我们可以像开发普通 Spring Bean 一样开发智能应用

你可以基于这个模板,轻松扩展到点餐、问药、推荐、客服等各类场景。快去试试吧!

项目完整代码已附于文中,若需获取最新依赖版本,请参考 Spring AI Alibaba 官方文档

作者简介:本文作者为一名Java开发者,热衷于探索AI工程化落地。欢迎交流。