
1. 为什么 Manus 智能体需要 askUser 与 ApplicationRunner 协作做 Agent 开发时很多人第一反应是把工具链堆满搜索、爬虫、文件操作、终端命令全挂上然后丢一句“帮我清理下项目目录”就期待它自己跑完。我试过结果往往是模型在“删除文件”这一步直接执行连个确认都没有——因为工具描述里没写“高风险操作要先问人”Agent 也没有暂停等待用户输入的机制。Manus 这类智能体的核心是 ReAct 循环Think → Act → Observe。它默认是“自主执行”的遇到模糊需求会自己猜遇到危险操作会自己扛。但真实业务里删除文件、覆盖数据、执行系统命令、请求 API 密钥这些动作必须有人来拍板。于是就需要一个askUser工具让 Agent 在关键节点主动暂停把决策权交还给终端前的你。问题在于askUser工具本身只是“问”它需要一个能“答”的入口。单元测试里控制台是只读的你没法在测试方法里敲键盘而 Spring Boot 应用启动后如果没有一个常驻的交互循环Agent 问完就卡死了。这就是ApplicationRunner出场的地方——它让程序启动时直接进入终端交互模式Scanner读取你的输入回传给askUserAgent 拿到答案后继续执行。所以整条链路是ApplicationRunner 启动交互循环 → 用户输入任务 → Manus 执行 → 遇到高风险操作调用 askUser → 终端打印问题 → 用户输入答案 → askUser 返回答案 → Manus 继续执行 → 结果回传终端。这条链路打通了人机交互闭环才算真正跑起来。而在这条链路里模型调用是绕不开的一环。Manus 的 ReAct 循环每一步都要请求大模型如果 Key 管理混乱、每个工具各配一套凭证调试成本会非常高。用 TaoToken 统一 Key 的好处是一个 API Key 覆盖模型对话、工具调用、多轮推理Base URL 和 Model ID 集中配置换模型只改一个字段。下面我会先给出 TaoToken 的接入配置再完整复现 askUser ApplicationRunner 的终端交互链路。2. TaoToken 统一 Key 接入配置与模型通道准备在写 askUser 之前先把模型通道打通。Manus 的每一步 Think 都要调模型如果这一步不通后面工具注册得再漂亮也跑不起来。TaoToken 的接入方式兼容 OpenAI 风格的 Base URL API Key Model ID 三件套Spring AI 的OpenAiChatModel可以直接对接。先拿 Key。访问 API Keys 管理页https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys 创建一个新 Key复制保存。注意 Key 只在创建时显示一次丢了就重新建。然后在application.yml里配置模型通道。Spring AI 的 OpenAI starter 默认读spring.ai.openai前缀我们把 base-url 指向 TaoToken 的 API 地址api-key 填刚拿到的 Keymodel 填你要用的模型 IDspring: ai: openai: base-url: https://taotoken.net/api api-key: sk-你的TaoToken密钥 chat: options: model: claude-sonnet-4-20250514 temperature: 0.7如果你用的是dashScopeChatModel这类自定义 Bean也可以在Bean方法里显式构造Bean(dashScopeChatModel) public ChatModel dashScopeChatModel() { OpenAiApi openAiApi OpenAiApi.builder() .baseUrl(https://taotoken.net/api) .apiKey(System.getenv(TAOTOKEN_API_KEY)) .build(); return new OpenAiChatModel(openAiApi, OpenAiChatOptions.builder() .model(claude-sonnet-4-20250514) .temperature(0.7) .build()); }这里有个细节不要把 Key 硬编码进代码提交到仓库。用环境变量TAOTOKEN_API_KEY或者配置中心注入。本地调试可以在 IDE 的 Run Configuration 里加环境变量或者用.env文件配合spring-dotenv。模型通道验证很简单写一个最小测试SpringBootTest class ModelChannelTest { Resource(name dashScopeChatModel) private ChatModel chatModel; Test void testModelReachable() { String reply chatModel.call(用一句话说明什么是 ReAct 循环); System.out.println(reply); assertNotNull(reply); } }如果这一步能打印出模型回复说明 Base URL、Key、Model ID 三件套都对了。如果报 401检查 Key 是否复制完整如果报 model not found检查 Model ID 拼写如果连接超时检查 base-url 是否写成了https://taotoken.net/api注意结尾不要多加/v1具体以接入文档为准。模型通道通了之后Manus 的 Think 步骤就有了稳定的推理后端。接下来才是工具注册和 askUser 的实现。3. askUser 工具与 ApplicationRunner 的可复制配置这一节是核心。我会按“工具类 → 注册中心 → SystemPrompt → yml 开关 → ApplicationRunner”的顺序给出可复制片段你照着贴就能跑。3.1 AskUserTool 工具类askUser的本质是暂停当前线程用Scanner读一行用户输入返回给模型。关键是Tool的 description 要写清楚“什么时候必须调用”否则模型不会主动用。package com.example.manus.tools; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import java.util.Scanner; Component public class AskUserTool { Tool(description 【能力】暂停当前流程并向用户请求输入用于需要人工决策、确认或提供额外信息的场景。 【限制】此工具会阻塞当前线程直到用户输入请确保在交互式环境中使用。 【强制调用场景】1)删除/覆盖文件前确认 2)选择报告类型 3)请求API密钥 4)澄清模糊需求。 问题必须清晰明确包含可操作的选项。) public String askUser( ToolParam(description 需要用户回答的问题描述示例请选择下一步操作A)继续 B)终止 C)保存) String question) { System.out.println(\n[系统需要您的决策] question); System.out.print( 请输入您的选择: ); Scanner scanner new Scanner(System.in); String answer scanner.nextLine().trim(); if (answer.isEmpty()) { answer 用户未提供输入启用默认处理; } System.out.println([用户回答] answer); return answer; } }注意Scanner每次调用都新建避免上一次的缓冲残留。生产环境如果换成 WebSocket 或 HTTP 回调把Scanner替换成阻塞队列的take()即可工具签名不变。3.2 工具注册中心Spring AI 用ToolCallbacks.from()扫描带Tool注解的方法包装成ToolCallback数组。把askUserTool加进去Configuration Slf4j public class ToolRegistration { Resource private AskUserTool askUserTool; Resource private TerminalOperationTool terminalOperationTool; // ... 其他工具注入 Bean public ToolCallback[] allTools() { ToolCallback[] normalCallbacks ToolCallbacks.from( askUserTool, terminalOperationTool // ... 其他工具 ); log.info(Registered {} normal tools, normalCallbacks.length); return normalCallbacks; } Bean public FunctionCallback[] allFunctionCallbacks(ToolCallback[] allTools) { return Arrays.stream(allTools) .filter(cb - cb instanceof FunctionCallback) .map(cb - (FunctionCallback) cb) .toArray(FunctionCallback[]::new); } }如果你还有 MCP 工具用Stream.concat合并即可。注册完在启动日志里能看到Registered 2 normal tools说明 askUser 已被发现。3.3 SystemPrompt 决策规则模型不会无缘无故调 askUser必须在 SystemPrompt 里写死“强制调用场景”。这段直接抄String SYSTEM_PROMPT You are Manus, an all-capable AI assistant. **关键决策规则** 当且仅当遇到以下情况时必须调用 askUser 工具 1. 高风险操作删除文件、覆盖数据、执行系统命令→ 询问即将执行[操作]请确认是否继续 2. 关键选择报告类型、处理策略→ 询问请选择[A/B/C]您的偏好是 3. 信息缺失API密钥、文件路径→ 询问请提供[缺失信息] 4. 意图不明处理一下这个文件→ 询问请明确您的具体需求 **调用规范** 问题必须清晰明确包含可操作选项获得回答后立即继续执行不确定时务必询问。 ;3.4 yml 交互开关加一个开关避免生产环境误启动交互循环tool: enable-manus-interaction: true3.5 ApplicationRunner 交互入口ApplicationRunner的run方法在 Spring Boot 启动完成后执行正好用来开终端循环Component Slf4j public class ManusInteractionRunner implements ApplicationRunner { Resource private ManusAgent manusAgent; Value(${tool.enable-manus-interaction:false}) private boolean enableInteraction; Override public void run(ApplicationArguments args) { if (!enableInteraction) { log.info(Manus 交互模式未启用); return; } System.out.println(); System.out.println(Manus 用户交互模式已启动输入 exit 退出); System.out.println(); Scanner scanner new Scanner(System.in); while (true) { System.out.print(\n请输入任务描述: ); String input scanner.nextLine().trim(); if (exit.equalsIgnoreCase(input)) { System.out.println(再见); break; } if (input.isEmpty()) { System.out.println(输入不能为空); continue; } System.out.println(\n[执行中...]); String result manusAgent.run(input); System.out.println(\n[执行结果]:\n result); } } }到这里配置片段全部给完。启动类跑起来终端就会进入交互模式。4. 终端命令触发与 askUser 回调的完整验证配置写完了现在验证链路是否真的通。启动AiSuperLoveAgentApplication看到“Manus 用户交互模式已启动”就说明ApplicationRunner生效了。第一步输入一个会触发高风险操作的任务请输入任务描述: 删除当前目录下的所有 .pdf 和 .png 文件Manus 的 ReAct 循环开始Think 阶段模型分析任务判断这是“高风险操作”决定调用askUser。终端会打印[系统需要您的决策] 即将执行删除操作将删除当前目录下所有 .pdf 和 .png 文件请确认是否继续(yes/no) 请输入您的选择:这就是askUser工具被调用的证据。此时 Agent 线程阻塞在Scanner.nextLine()等待你输入。第二步输入yes 请输入您的选择: yes [用户回答] yesaskUser返回yes模型拿到这个 Observe 结果继续 Think决定调用executeCommand工具执行删除命令。终端打印[执行中...] [执行结果]: [command exitCode0] 已删除 file1.pdf 已删除 file2.png第三步验证文件确实被删。另开一个终端ls或dir确认.pdf和.png已消失。第四步输入exit退出交互循环。整个链路的时序是阶段触发方动作终端表现启动ApplicationRunner进入 while 循环打印交互模式已启动任务输入用户输入删除任务打印执行中Think模型判断高风险无输出Act模型调用 askUser打印决策问题暂停askUserScanner 阻塞等待输入用户回答用户输入 yes打印用户回答Observe模型拿到 yes无输出Act模型调用 executeCommand打印命令输出结果回传Agent返回最终结果打印执行结果如果这条链路跑通说明 askUser、ApplicationRunner、工具注册、模型通道全部对齐了。你可以把任务换成“生成一份报告让我选类型”验证 askUser 在“关键选择”场景下也能触发。5. 常见报错排查401、local proxy failed、reading choices链路跑不通时报错通常集中在几个地方。我按真实遇到的顺序列出来。401 Unauthorized模型通道的 Key 不对。检查spring.ai.openai.api-key是否填了完整的 TaoToken Key有没有多余空格。如果 Key 是从环境变量读的确认环境变量名拼写一致。还有一种情况是 base-url 写错比如写成了https://taotoken.net/api/v1而实际不需要/v1具体以接入文档为准。local proxy failed / connection refused本地网络到 TaoToken 的连接不通。先curl https://taotoken.net/api看能否返回如果 curl 也不通检查本机网络配置。注意不要配置任何非官方的网络转发工具直接用系统默认网络即可。reading choices / choices is null模型返回体解析失败。常见原因是 Model ID 写错比如把claude-sonnet-4-20250514写成了claude-sonnet-4服务端返回了错误结构。检查spring.ai.openai.chat.options.model是否与 TaoToken 支持的模型 ID 完全一致。另一个原因是 temperature 等参数超出了模型支持范围。OAuth / token expired如果你用的是需要 OAuth 的通道token 过期会导致 401。TaoToken 的 API Key 是长期有效的不存在 OAuth 刷新问题所以这个报错通常出现在混用了其他通道的情况下。统一用 TaoToken 的 Key 就能规避。askUser 不触发模型没有调用 askUser直接执行了删除。检查 SystemPrompt 里的“强制调用场景”是否写进去了以及Tool的 description 是否包含“高风险操作必须调用”。模型对工具描述很敏感描述里没写“强制”它就可能跳过。ApplicationRunner 不执行启动后没看到交互模式。检查tool.enable-manus-interaction是否为 true以及ApplicationRunner实现类是否加了Component。如果用了SpringBootTest跑测试ApplicationRunner默认不执行必须用mvn spring-boot:run或 IDE 直接运行主类。Scanner 读不到输入在 IDE 的 Run 窗口里确保焦点在控制台且没有其他进程占用 stdin。如果用了redirectErrorStream把 stderr 合并到 stdout有时会干扰 Scanner检查ProcessBuilder的配置。排障时建议先单独验证模型通道第 2 节的测试再验证工具注册看启动日志的 tools 数量最后验证 ApplicationRunner看交互模式是否打印。分层定位比一把梭快得多。6. 从终端交互到长期 Agent 的接入路径终端交互适合快速验证但生产环境不会让用户守着控制台。把askUser的Scanner换成阻塞队列ApplicationRunner换成 WebSocket 端点整条链路就能搬到 Web 端。工具签名和 SystemPrompt 都不用改这是这套设计的好处。如果你要长期跑 Agent 任务比如让 Manus 持续处理文件、定时执行命令、多轮对话建议用 Coding Plan 这类长期通道避免按次调用带来的额度波动。模型对话调试可以在模型对话页直接试接入文档在接入文档页API Key 管理在 API Keys 页。先把终端闭环跑通再往生产形态迁移路径会清晰很多。