1. 为什么 Manus 智能体需要 askUser 与 ApplicationRunner 协作
做 Agent 开发时,很多人第一反应是把工具链堆满:搜索、爬虫、文件操作、终端命令全挂上,然后丢一句“帮我清理下项目目录”就期待它自己跑完。我试过,结果往往是模型在“删除文件”这一步直接执行,连个确认都没有——因为工具描述里没写“高风险操作要先问人”,Agent 也没有暂停等待用户输入的机制。
Manus 这类智能体的核心是 ReAct 循环:Think → Act → Observe。它默认是“自主执行”的,遇到模糊需求会自己猜,遇到危险操作会自己扛。但真实业务里,删除文件、覆盖数据、执行系统命令、请求 API 密钥这些动作,必须有人来拍板。于是就需要一个askUser工具,让 Agent 在关键节点主动暂停,把决策权交还给终端前的你。
问题在于:askUser工具本身只是“问”,它需要一个能“答”的入口。单元测试里控制台是只读的,你没法在测试方法里敲键盘;而 Spring Boot 应用启动后,如果没有一个常驻的交互循环,Agent 问完就卡死了。这就是ApplicationRunner出场的地方——它让程序启动时直接进入终端交互模式,Scanner读取你的输入,回传给askUser,Agent 拿到答案后继续执行。
所以整条链路是: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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys ,创建一个新 Key,复制保存。注意 Key 只在创建时显示一次,丢了就重新建。
然后在application.yml里配置模型通道。Spring AI 的 OpenAI starter 默认读spring.ai.openai前缀,我们把 base-url 指向 TaoToken 的 API 地址,api-key 填刚拿到的 Key,model 填你要用的模型 ID:
spring: 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 exitCode=0] 已删除 file1.pdf 已删除 file2.png第三步,验证文件确实被删。另开一个终端ls或dir,确认.pdf和.png已消失。第四步,输入exit退出交互循环。
整个链路的时序是:
| 阶段 | 触发方 | 动作 | 终端表现 |
|---|---|---|---|
| 启动 | ApplicationRunner | 进入 while 循环 | 打印交互模式已启动 |
| 任务输入 | 用户 | 输入删除任务 | 打印执行中 |
| Think | 模型 | 判断高风险 | 无输出 |
| Act | 模型 | 调用 askUser | 打印决策问题 |
| 暂停 | askUser | Scanner 阻塞 | 等待输入 |
| 用户回答 | 用户 | 输入 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 页。先把终端闭环跑通,再往生产形态迁移,路径会清晰很多。