Spring AI MCP Server中如何提取请求URL中的sessionId并传递至@Tool注解的工具函数
我之前在基于Spring AI开发MCP服务的时候,也踩过异步工具调用丢失请求上下文的坑——用RequestContextHolder拿不到参数,就是因为工具执行在新线程里,上下文没传过去。下面给你几个经过实践验证的解决方案,按推荐程度排序:
方案一:利用Spring AI原生对话上下文(最推荐)
Spring AI本身提供了Conversation(对话)机制,用来维护对话状态,它会自动跟随工具调用流程,不管线程怎么切换都能拿到。
步骤1:在接口层提取sessionId并存入对话上下文
首先在/mcp/message接口里,把请求参数里的sessionId放到Conversation的metadata中:
@PostMapping("/mcp/message") public ResponseEntity<Message> handleMessage(@RequestParam String sessionId, @RequestBody Message request) { // 通过sessionId获取或创建对话(你需要自己实现conversationService) Conversation conversation = conversationService.getOrCreateConversation(sessionId); // 把sessionId存入对话元数据 conversation.getMetadata().put("sessionId", sessionId); // 调用AI生成响应,对话上下文会自动传递给工具 Message response = aiClient.generate(request, conversation); return ResponseEntity.ok(response); }
步骤2:在@Tool函数中获取sessionId
直接在工具函数的参数里注入Conversation,就能从metadata里取出sessionId:
@Tool public String userAuthenticationTool(Conversation conversation) { String sessionId = (String) conversation.getMetadata().get("sessionId"); // 这里写你的登录验证逻辑,比如根据sessionId查询用户信息 return String.format("已验证sessionId: %s,用户身份有效", sessionId); }
这个方案完全贴合Spring AI的设计,不用自己处理线程上下文传递,可靠性最高。
方案二:自定义线程池传递ThreadLocal上下文
如果你的场景必须用ThreadLocal,那就要确保线程池在执行任务时复制上下文。Spring的ThreadPoolTaskExecutor支持通过TaskDecorator实现这一点。
步骤1:定义Session上下文Holder
public class SessionContextHolder { private static final ThreadLocal<String> sessionIdHolder = new ThreadLocal<>(); public static void setSessionId(String sessionId) { sessionIdHolder.set(sessionId); } public static String getSessionId() { return sessionIdHolder.get(); } public static void clear() { sessionIdHolder.remove(); } }
步骤2:添加拦截器设置sessionId
在请求进入时把sessionId存入ThreadLocal,请求结束后清理:
@Component public class SessionIdInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String sessionId = request.getParameter("sessionId"); if (StringUtils.hasText(sessionId)) { SessionContextHolder.setSessionId(sessionId); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { SessionContextHolder.clear(); } }
别忘了在Spring配置里注册这个拦截器,让它作用于/mcp/message接口。
步骤3:配置带TaskDecorator的线程池
让Spring AI的异步工具调用使用这个线程池,确保上下文被复制:
@Configuration public class AsyncToolExecutorConfig { @Bean(name = "aiToolExecutor") public Executor aiToolExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(20); executor.setThreadNamePrefix("AI-Tool-Worker-"); // 关键:用TaskDecorator复制ThreadLocal里的sessionId executor.setTaskDecorator(runnable -> { String currentSessionId = SessionContextHolder.getSessionId(); return () -> { try { SessionContextHolder.setSessionId(currentSessionId); runnable.run(); } finally { SessionContextHolder.clear(); } }; }); executor.initialize(); return executor; } }
然后要确保Spring AI的工具调用使用这个线程池,你可以在AiClient的配置里指定executor,或者通过@Async("aiToolExecutor")注解工具方法(如果工具是异步执行的话)。
步骤4:在工具函数中获取sessionId
@Tool public String userAuthenticationTool() { String sessionId = SessionContextHolder.getSessionId(); // 业务逻辑 return String.format("通过ThreadLocal拿到sessionId: %s", sessionId); }
方案三:直接将sessionId作为工具参数传递
这个方案最直接,不需要处理上下文,只要让AI在调用工具时把sessionId作为参数传进去就行。
步骤1:在接口层构建提示词时明确传递sessionId
@PostMapping("/mcp/message") public ResponseEntity<Message> handleMessage(@RequestParam String sessionId, @RequestBody Message request) { // 在提示词里明确告诉AI,调用工具时必须传入sessionId参数 String prompt = String.format("用户请求:%s,请调用相关工具时,务必将sessionId=%s作为参数传入", request.getContent(), sessionId); Message aiRequest = Message.from(prompt); Message response = aiClient.generate(aiRequest); return ResponseEntity.ok(response); }
步骤2:工具函数接收sessionId参数
@Tool public String userAuthenticationTool(String sessionId) { // 直接使用sessionId做验证 return String.format("工具收到sessionId: %s,验证通过", sessionId); }
这个方案的缺点是依赖AI对提示词的理解,如果AI没按要求传参数,工具就拿不到值。适合简单场景,或者你能通过prompt engineering确保AI正确传递参数。
内容来源于stack exchange




