Spring AI 智能体模式(第二部分):AskUserQuestionTool —— 行动前先澄清的智能体

预估阅读时长:8分钟

Spring AI 智能体模式(第二部分):AskUserQuestionTool —— 行动前先澄清的智能体

传统的 AI 交互遵循一种常见模式:你提供提示词,AI 做出假设,然后生成回复。当这些假设与你的需求不匹配时,你只能反复修正。每一次假设都会造成返工——浪费时间和上下文。

如果你的 AI 智能体能在给出答案之前,主动向你提出澄清性问题,会怎样?

AskUserQuestionTool 正是为此而生。它允许 AI 智能体在回答之前提出澄清性问题,以交互方式收集需求,并从一开始就创建符合你实际需求的规格说明。

Spring AI 的实现将这种交互模式带入了 Java 生态,并确保了大语言模型的可移植性——你只需定义一次问题处理器,就能与 OpenAI、Anthropic、Google Gemini 或任何其他支持的模型一起使用。

这是我们 Spring AI 智能体模式系列的第二部分。在第一部分中,我们探讨了智能体技能——那些能扩展 AI 功能的模块化能力。现在,我们来研究 AskUserQuestionTool,它将 AI 智能体转变为可以交互式收集需求的协作伙伴。

🚀 想直接开始?跳到入门指南部分。

AskUserQuestionTool 的工作原理

AskUserQuestionTool 是 spring-ai-agent-utils 工具包的一部分,它是 Claude Code 的 AskUserQuestion 工具的一种可移植、基于 Spring AI 的实现,能让 AI 智能体在执行过程中向用户提出选择题。

该工具遵循一个”提问-回答”的工作流:

  • AI 生成问题 —— 智能体确定需要输入信息,并构造问题(每个问题包含问题文本、标题、2-4 个选项以及 multiSelect 标志),然后调用 askUserQuestion 工具函数。
  • 用户提供答案 —— 你自定义的处理器接收这些问题,通过你的用户界面展示它们,收集答案,并将答案返回给 AI。
  • 提出更多问题 —— 如有必要,重复第 1 步和第 2 步,以收集更多用户反馈。
  • AI 结合上下文继续 —— 智能体利用这些答案来提供量身定制的解决方案。

每个问题都支持:

  • 单选或多选 —— 可选择一个选项,也可组合多个选项。
  • 自由文本输入 —— 用户始终可以提供预定义选项之外的自由文本。
  • 丰富的上下文 —— 每个选项都包含描述,解释其影响和权衡。

💡 可移植且与模型无关 —— 无供应商锁定。与那些绑定特定大语言模型平台的实现不同,这个 Spring AI 实现可以跨众多大语言模型提供商工作,让你可以在不重写代码或问题处理器的情况下切换模型。

💡 与 MCP 启发式交互的关系 —— AskUserQuestionTool 充当了一种在智能体内部实现交互式用户输入的本地化方法,在概念上类似于 MCP 启发式交互能力。MCP 启发式交互允许 MCP 服务器通过 JSON schema 请求结构化的用户输入,而 AskUserQuestionTool 则直接在你的智能体内部提供了相同的交互模式,无需依赖 MCP 服务器。对于由服务器驱动的场景,Spring AI 也通过 @McpElicitation 注解提供了完整的 MCP 启发式交互支持。

示例:旅行推荐助手

以下是该工具在旅行推荐用例(来自 ask-user-question-demo)中的实际工作方式:

USER: 下一个该去欧盟哪个国家旅行?

Interests: 旅行时你的主要兴趣是什么?
  1. 历史与文化 - 博物馆、历史遗迹、建筑
  2. 自然与户外 - 徒步、海滩、山脉、国家公园
  3. 美食与饮品 - 烹饪体验、葡萄酒产区、美食之旅
  4. 城市与都市 - 都市探索、购物、夜生活
  (输入以逗号分隔的数字,或键入自定义文本)
1,2

Season: 你计划什么时间旅行?
  1. 春季 - 3月至5月 - 温和气温,游客较少
  2. 夏季 - 6月至8月 - 温暖天气,旅游旺季
  3. 秋季 - 9月至11月 - 更凉爽的气温,秋色
  4. 冬季 - 12月至2月 - 圣诞集市,冬季运动
  (输入一个数字,或键入自定义文本)
3

Budget: 你这次旅行的大致预算是多少?
  1. ...
...

ASSISTANT: 基于你希望在秋季旅行,对历史、文化和自然感兴趣,
并选择中等预算,同时寻求热门景点与小众体验相结合,以下是一些
可以考虑的欧盟国家:

1. **葡萄牙** - 里斯本的历史街区、杜罗河谷的葡萄酒收获季、
   阿尔加维海岸线。消费适中,秋季气温宜人。
2. **希腊** - 古迹遗址、美丽海岛,秋季仍然温暖适合游泳,
   且游客较少。
3. **斯洛文尼亚** - 布莱德湖、朱利安阿尔卑斯山、壮丽的秋色。
   比邻近的奥地利或意大利更实惠。

智能体通过交互式问题全面收集了需求,然后才提供个性化推荐——无需任何假设,也无需反复迭代。

入门指南

1. 添加依赖:

<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>spring-ai-agent-utils</artifactId>
    <version>0.3.0</version>
</dependency>

2. 配置你的智能体:

ChatClient chatClient = chatClientBuilder
    .defaultTools(AskUserQuestionTool.builder()
        .questionHandler(this::handleQuestions)
        .build())
    .build();

3. 参考下面的控制台或 Web 示例来实现你的 QuestionHandler

当智能体需要澄清时,它会自动调用该工具,并使用答案来提供量身定制的解决方案。

💡 演示项目:ask-user-question-demo

QuestionHandler 示例

基于控制台的 QuestionHandler

一个基于控制台的实现:

private static Map<String, String> handleQuestions(List<Question> questions) {
    Map<String, String> answers = new HashMap<>();
    Scanner scanner = new Scanner(System.in);

    for (Question q : questions) {
        System.out.println("\n" + q.header() + ": " + q.question());

        for (int i = 0; i < q.options().size(); i++) {
            Option opt = q.options().get(i);
            System.out.printf("  %d. %s - %s%n", i + 1, opt.label(), opt.description());
        }

        System.out.println(q.multiSelect()
            ? "  (输入以逗号分隔的数字,或键入自定义文本)"
            : "  (输入一个数字,或键入自定义文本)");

        String response = scanner.nextLine().trim();

        // 解析数字选择或将其作为自由文本使用
        try {
            String[] parts = response.split(",");
            List<String> labels = new ArrayList<>();
            for (String part : parts) {
                int index = Integer.parseInt(part.trim()) - 1;
                if (index >= 0 && index < q.options().size()) {
                    labels.add(q.options().get(index).label());
                }
            }
            answers.put(q.question(), labels.isEmpty() ? response : String.join(", ", labels));
        } catch (NumberFormatException e) {
            answers.put(q.question(), response);
        }
    }
    return answers;
}

该处理器显示选项,接受数字选择(如 “1,2”)或自由文本(如 “中等预算”),并将答案返回给智能体。

基于 Web 的 QuestionHandler

对于 Web 应用,可使用 CompletableFuture 来桥接异步 UI 交互与同步的 QuestionHandler API。通过 WebSocket/SSE 将问题发送到你的前端,并在 future.get() 上阻塞。当用户通过 REST 端点提交答案后,再完成这个 future。

结语

AskUserQuestionTool 将 AI 智能体从基于假设的响应者转变为协作伙伴,在行动之前收集需求,从而在第一次尝试时就给出符合你需求的答案。

本系列的后续内容:

  • TodoWriteTool —— 透明地跟踪多步骤工作流
  • 子智能体编排 —— 分层多智能体架构
  • A2A 集成 —— 使用 Agent2Agent 协议构建可互操作的智能体
  • 子智能体扩展框架(即将推出) —— 协议无关的智能体编排

立即开始实验:ask-user-question-demo。

资源

  • GitHub 仓库:spring-ai-agent-utils
  • AskUserQuestionTool 文档:AskUserQuestionTool
  • Spring AI 文档:docs.spring.io/spring-ai
  • 演示项目:ask-user-question-demo - 基于控制台的交互式提问(即本文)
  • Claude 智能体 SDK:用户输入文档

系列链接

  • 第一部分:智能体技能 - 模块化、可复用的能力
  • 第二部分:AskUserQuestion 工具(本文) - 交互式工作流
  • 第三部分:TodoWriteTool - 结构化规划
  • 第四部分:子智能体编排 - 分层智能体架构
  • 第五部分:A2A 集成 - 使用 Agent2Agent 协议构建可互操作的智能体
  • 即将推出:子智能体扩展框架(即将推出) - 协议无关的智能体编排

相关 Spring AI 博客

  • 动态工具发现 —— 实现 34-64% 的 token 节省
  • 工具参数增强 —— 在工具执行期间捕获大语言模型的推理过程

【注】本文译自:Spring AI Agentic Patterns (Part 2): AskUserQuestionTool - Agents That Clarify Before Acting