Spring AI Alibaba如何使用Jmanus? ---- 高德地图出行应用规划

Spring AI集成高德地图实现路线规划

最近社区列了一些Jmanus的todo list. 以下是个人的一些思路

实现方案

  1. 创建高德地图工具集成:参考现有的百度地图工具实现
  2. 设计MCP协议接口:使用MCP服务器配置来处理地理位置请求
  3. 实现路线规划代理:创建专门的代理来处理用户输入并生成路线
  4. 构建用户交互界面:提供简单的Web界面让用户输入地点并查看规划路线
具体实现1. 高德地图工具实现

首先,我们需要创建一个类似于BaiDuMapTools的高德地图工具类:


java

体验AI代码助手

代码解读

复制代码

package com.alibaba.cloud.ai.toolcalling.amap; import com.alibaba.cloud.ai.toolcalling.common.CommonToolCallUtils; import com.alibaba.cloud.ai.toolcalling.common.JsonParseTool; import com.alibaba.cloud.ai.toolcalling.common.WebClientTool; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.core.type.TypeReference; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import java.util.List; import java.util.Objects; public final class AMapTools { private final AMapProperties amapProperties; private final WebClientTool webClientTool; private final JsonParseTool jsonParseTool; public AMapTools(AMapProperties amapProperties, WebClientTool webClientTool, JsonParseTool jsonParseTool) { this.amapProperties = amapProperties; this.webClientTool = webClientTool; this.jsonParseTool = jsonParseTool; if (Objects.isNull(amapProperties.getApiKey())) { throw new RuntimeException("请在application.yml文件中配置高德地图API密钥"); } } /** * 获取地点详细信息 * @param keyword 地点关键词 * @return 地点详细信息 */ public String getPlaceInfo(String keyword) { String path = "/v3/place/text"; MultiValueMap<String, String> params = CommonToolCallUtils.<String, String>multiValueMapBuilder() .add("key", amapProperties.getApiKey()) .add("keywords", keyword) .add("output", "json") .build(); try { return webClientTool.get(path, params).block(); } catch (Exception e) { throw new RuntimeException("获取地点信息失败", e); } } /** * 规划路线 * @param origin 起点坐标(经度,纬度) * @param destination 终点坐标(经度,纬度) * @param type 路线类型:driving(驾车)、walking(步行)、transit(公交)、riding(骑行) * @return 路线规划结果 */ public String planRoute(String origin, String destination, String type) { String path = "/v3/direction/" + type; MultiValueMap<String, String> params = CommonToolCallUtils.<String, String>multiValueMapBuilder() .add("key", amapProperties.getApiKey()) .add("origin", origin) .add("destination", destination) .add("output", "json") .build(); try { return webClientTool.get(path, params).block(); } catch (Exception e) { throw new RuntimeException("路线规划失败", e); } } }

对应的属性配置类:


typescript

体验AI代码助手

代码解读

复制代码

package com.alibaba.cloud.ai.toolcalling.amap; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.ai.tool.amap") public class AMapProperties { private String apiKey; private String baseUrl = "https://restapi.amap.com"; public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getBaseUrl() { return baseUrl; } public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } }

2. 创建路线规划服务

接下来,我们创建一个路线规划服务,用于处理用户输入并调用高德地图API:


less

体验AI代码助手

代码解读

复制代码

package com.alibaba.cloud.ai.toolcalling.amap; import com.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.function.Function; @JsonClassDescription("使用高德地图API规划从起点到终点的路线") public class AMapRouteService implements Function<AMapRouteService.Request, AMapRouteService.Response> { private final AMapTools amapTools; private final ObjectMapper objectMapper = new ObjectMapper(); public AMapRouteService(AMapTools amapTools) { this.amapTools = amapTools; } @Override public Response apply(Request request) { try { // 1. 获取起点坐标 String originInfo = amapTools.getPlaceInfo(request.origin()); JsonNode originNode = objectMapper.readTree(originInfo); String originCoord = extractCoordinates(originNode); // 2. 获取终点坐标 String destInfo = amapTools.getPlaceInfo(request.destination()); JsonNode destNode = objectMapper.readTree(destInfo); String destCoord = extractCoordinates(destNode); // 3. 规划路线 String routeInfo = amapTools.planRoute(originCoord, destCoord, request.travelMode()); // 4. 处理结果 JsonNode routeNode = objectMapper.readTree(routeInfo); String routeSummary = extractRouteSummary(routeNode, request.travelMode()); return new Response(routeSummary); } catch (Exception e) { return new Response("路线规划失败: " + e.getMessage()); } } private String extractCoordinates(JsonNode placeNode) { if (placeNode.has("pois") && placeNode.get("pois").size() > 0) { JsonNode firstPoi = placeNode.get("pois").get(0); if (firstPoi.has("location")) { return firstPoi.get("location").asText(); } } throw new RuntimeException("无法获取地点坐标"); } private String extractRouteSummary(JsonNode routeNode, String travelMode) { StringBuilder summary = new StringBuilder(); if (routeNode.has("route")) { JsonNode route = routeNode.get("route"); // 提取基本信息 if (route.has("paths") && route.get("paths").size() > 0) { JsonNode path = route.get("paths").get(0); // 总距离和时间 if (path.has("distance") && path.has("duration")) { double distance = path.get("distance").asDouble() / 1000.0; // 转换为公里 int duration = path.get("duration").asInt() / 60; // 转换为分钟 summary.append(String.format("总距离: %.1f公里, 预计用时: %d分钟\n\n", distance, duration)); } // 提取路线步骤 if (path.has("steps") && path.get("steps").size() > 0) { summary.append("路线指引:\n"); JsonNode steps = path.get("steps"); for (int i = 0; i < steps.size(); i++) { JsonNode step = steps.get(i); if (step.has("instruction")) { summary.append(i + 1).append(". ") .append(step.get("instruction").asText()) .append("\n"); } } } } } return summary.toString(); } @JsonClassDescription("路线规划请求") public record Request( @JsonProperty(required = true) @JsonPropertyDescription("起点位置,如:北京市海淀区") String origin, @JsonProperty(required = true) @JsonPropertyDescription("终点位置,如:北京市朝阳区") String destination, @JsonProperty(required = false) @JsonPropertyDescription("出行方式:driving(驾车)、walking(步行)、transit(公交)、riding(骑行)") String travelMode) { } public record Response(String routePlan) { } }

3. 配置自动装配

创建自动配置类,使工具可以被Spring自动装配:


typescript

体验AI代码助手

代码解读

复制代码

package com.alibaba.cloud.ai.toolcalling.amap; import com.alibaba.cloud.ai.toolcalling.common.JsonParseTool; import com.alibaba.cloud.ai.toolcalling.common.WebClientTool; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @Configuration @EnableConfigurationProperties(AMapProperties.class) public class AMapAutoConfiguration { @Bean @ConditionalOnMissingBean public WebClientTool amapWebClientTool(AMapProperties amapProperties) { return new WebClientTool(WebClient.builder() .baseUrl(amapProperties.getBaseUrl()) .build()); } @Bean @ConditionalOnMissingBean public JsonParseTool amapJsonParseTool() { return new JsonParseTool(); } @Bean @ConditionalOnMissingBean public AMapTools amapTools(AMapProperties amapProperties, WebClientTool webClientTool, JsonParseTool jsonParseTool) { return new AMapTools(amapProperties, webClientTool, jsonParseTool); } @Bean @ConditionalOnMissingBean public AMapRouteService amapRouteService(AMapTools amapTools) { return new AMapRouteService(amapTools); } }

4. 创建路线规划代理

基于OpenManus框架创建一个专门的路线规划代理:


typescript

体验AI代码助手

代码解读

复制代码

package com.alibaba.cloud.ai.example.manus.agent; import com.alibaba.cloud.ai.example.manus.config.ManusProperties; import com.alibaba.cloud.ai.example.manus.llm.LlmService; import com.alibaba.cloud.ai.example.manus.recorder.PlanExecutionRecorder; import org.springframework.ai.model.tool.ToolCallingManager; import java.util.Map; public class RouteAgent extends BaseAgent { private static final String AGENT_NAME = "ROUTE_AGENT"; private static final String AGENT_DESCRIPTION = "专门处理地点查询和路线规划的智能体"; public RouteAgent(LlmService llmService, ToolCallingManager toolCallingManager, PlanExecutionRecorder recorder, ManusProperties manusProperties) { super(llmService, toolCallingManager, recorder, manusProperties); } @Override public String getName() { return AGENT_NAME; } @Override public String getDescription() { return AGENT_DESCRIPTION; } @Override protected String getThinkPrompt() { return """ 你是一个专门处理地点查询和路线规划的智能体。 你需要理解用户的出行需求,包括起点、终点以及可能的出行方式偏好。 然后使用高德地图API来规划最佳路线。 请按照以下步骤思考: 1. 分析用户请求,确定起点和终点 2. 确定合适的出行方式(驾车、步行、公交或骑行) 3. 使用高德地图API查询路线 4. 整理路线信息并提供给用户 你可以使用以下工具: - AMapRouteService:规划从起点到终点的路线 """; } @Override protected String getNextPrompt() { return """ 请根据用户的需求,使用高德地图API规划路线。 确保提供清晰的路线指引,包括总距离、预计时间以及详细的路线步骤。 如果用户没有明确指定出行方式,请默认使用驾车方式。 """; } }

5. 修改PlanningFlow配置

typescript

体验AI代码助手

代码解读

复制代码

@Bean @Scope("prototype") public PlanningFlow planningFlow(LlmService llmService, ToolCallingManager toolCallingManager, DynamicAgentLoader dynamicAgentLoader) { List<BaseAgent> agentList = new ArrayList<>(); Map<String, ToolCallBackContext> toolCallbackMap = new HashMap<>(); // 添加路线规划代理 RouteAgent routeAgent = new RouteAgent(llmService, toolCallingManager, recorder, manusProperties); toolCallbackMap = toolCallbackMap(routeAgent); routeAgent.setToolCallbackMap(toolCallbackMap); agentList.add(routeAgent); // 添加所有动态代理 for (DynamicAgentEntity agentEntity : dynamicAgentLoader.getAllAgents()) { DynamicAgent agent = dynamicAgentLoader.loadAgent(agentEntity.getAgentName()); toolCallbackMap = toolCallbackMap(agent); agent.setToolCallbackMap(toolCallbackMap); agentList.add(agent); } Map<String, Object> data = new HashMap<>(); return new PlanningFlow(agentList, data, recorder, toolCallbackMap); } // 工具回调映射方法 private Map<String, ToolCallBackContext> toolCallbackMap(BaseAgent agent) { Map<String, ToolCallBackContext> toolCallbackMap = new HashMap<>(); // 添加高德地图路线规划工具 AMapRouteService amapRouteService = context.getBean(AMapRouteService.class); ToolCallBiFunctionDef amapRouteTool = new ToolCallBiFunctionDef() { @Override public String getName() { return "amap_route"; } @Override public String getDescription() { return "使用高德地图API规划从起点到终点的路线"; } @Override public String getParameters() { return """ { "type": "object", "properties": { "origin": { "type": "string", "description": "起点位置,如:北京市海淀区" }, "destination": { "type": "string", "description": "终点位置,如:北京市朝阳区" }, "travelMode": { "type": "string", "enum": ["driving", "walking", "transit", "riding"], "description": "出行方式:driving(驾车)、walking(步行)、transit(公交)、riding(骑行)", "default": "driving" } }, "required": ["origin", "destination"] } """; } @Override public Class<?> getInputType() { return String.class; } @Override public boolean isReturnDirect() { return true; } @Override public void setAgent(BaseAgent agent) { // 设置关联的Agent } @Override public String getCurrentToolStateString() { return "Ready"; } @Override public void cleanup(String planId) { // 清理资源 } @Override public ToolExecuteResult apply(String input, ToolContext context) { try { ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree(input); String origin = node.get("origin").asText(); String destination = node.get("destination").asText(); String travelMode = node.has("travelMode") ? node.get("travelMode").asText() : "driving"; AMapRouteService.Request request = new AMapRouteService.Request( origin, destination, travelMode); AMapRouteService.Response response = amapRouteService.apply(request); return new ToolExecuteResult(response.routePlan()); } catch (Exception e) { return new ToolExecuteResult("路线规划失败: " + e.getMessage()); } } }; // 添加终止工具 TerminateTool terminateTool = new TerminateTool(agent); // 将工具添加到回调映射中 toolCallbackMap.put(amapRouteTool.getName(), new ToolCallBackContext( FunctionToolCallback.builder(amapRouteTool.getName(), amapRouteTool) .description(amapRouteTool.getDescription()) .inputSchema(amapRouteTool.getParameters()) .inputType(amapRouteTool.getInputType()) .build(), amapRouteTool )); toolCallbackMap.put("terminate", new ToolCallBackContext( FunctionToolCallback.builder("terminate", terminateTool) .description("终止当前任务执行") .inputSchema(terminateTool.getParameters()) .inputType(String.class) .build(), terminateTool )); return toolCallbackMap; }

  1. PlanningFlow Bean配置:
    1. 创建了一个prototype作用域的PlanningFlow Bean
    2. 添加了自定义的RouteAgent到代理列表中
    3. 为每个代理配置了工具回调映射
    4. 加载了所有动态代理
  2. 工具回调映射方法:
    1. 创建了一个专门用于配置工具回调的方法
    2. 实现了高德地图路线规划工具的ToolCallBiFunctionDef接口
    3. 添加了终止工具,用于结束代理执行
    4. 将工具添加到回调映射中,以便代理可以调用

这个配置确保了路线规划代理可以使用高德地图API工具来处理用户的路线规划请求。当用户输入起点和终点时,代理会通过工具调用高德地图API,获取路线信息并返回给用户。

6.前端界面

xml

体验AI代码助手

代码解读

复制代码

<!DOCTYPE html> <html> <head> <title>智能路线规划</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input, textarea { width: 100%; padding: 8px; box-sizing: border-box; } button { background-color: #4CAF50; color: white; padding: 10px 15px; border: none; cursor: pointer; } #result { margin-top: 20px; border: 1px solid #ddd; padding: 15px; white-space: pre-wrap; } </style> </head> <body> <h1>智能路线规划</h1> <div class="form-group"> <label for="userInput">请描述您的出行需求(包含起点和终点):</label> <textarea id="userInput" rows="4" placeholder="例如:我想从北京南站到北京故宫,请帮我规划路线"></textarea> </div> <button onclick="planRoute()">规划路线</button> <div id="result"></div> <script> async function planRoute() { const userInput = document.getElementById('userInput').value; const resultDiv = document.getElementById('result'); resultDiv.innerHTML = "正在规划路线,请稍候..."; try { const response = await fetch('/api/route/plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userInput }) }); const data = await response.json(); resultDiv.innerHTML = data.route; } catch (error) { resultDiv.innerHTML = "规划路线时出错:" + error.message; } } </script> </body> </html>

7.创建Controller接口

创建一个REST接口,用于接收用户输入并返回规划结果:


kotlin

体验AI代码助手

代码解读

复制代码

package com.alibaba.cloud.ai.example.manus.controller; import com.alibaba.cloud.ai.example.manus.agent.RouteNavigationAgent; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/route") public class RouteNavigationController { private final RouteNavigationAgent routeNavigationAgent; public RouteNavigationController(RouteNavigationAgent routeNavigationAgent) { this.routeNavigationAgent = routeNavigationAgent; } @PostMapping("/plan") public RouteResponse planRoute(@RequestBody RouteRequest request) { String result = routeNavigationAgent.planRoute(request.userInput); return new RouteResponse(result); } public record RouteRequest(String userInput) {} public record RouteResponse(String route) {} }

8. 配置MCP与高德地图集成

application.yml中添加必要的配置:


yaml

体验AI代码助手

代码解读

复制代码

spring: ai: alibaba: functioncalling: amap: web-api-key: ${AMAP_API_KEY:your_amap_api_key} mcp: client: model-name: qwen enabled: true

非常好的问题! 目前(截至 2025 年 4 月),**`spring-ai-alibaba-jmanus` 并不是 Spring 官方或 Alibaba 官方发布的正式依赖库**,它 **极有可能是社区开发者自行封装的第三方适配模块**,用于将 Spring AI 与阿里云的通义千问(Qwen)大模型对接。 下面我们从几个维度来深入解析: --- ### ✅ 1. `spring-ai-alibaba-jmanus` 是什么? #### 🔍 名称分析 - `spring-ai`: 表示这是基于 Spring AI 框架的扩展 - `alibaba`: 阿里相关服务 - `jmanus`: 这个名字 **并非阿里官方术语**,也不是公开 API 的命名。可能是拼写错误或代号,比如: - 可能是 “qwen” 的误写? - 或者是某个内部项目代号? - 也可能是社区昵称(如 GitHub 用户自定义命名) > ❗结论:**该依赖很可能是非官方、社区维护的实验性模块,不建议在生产环境使用。** --- ### ✅ 2. 它是如何使用 Spring AI 组件进行封装的?(通用原理) 尽管 `jmanus` 不是标准名称,但我们可以通过观察类似项目(如 `spring-ai-qwen-spring-boot-starter`)来理解这类封装的一般方式。 #### 🧱 封装结构(典型实现模式) ```java public class QwenChatClient implements ChatClient { private final RestTemplate restTemplate; private final String apiKey; private final String model; // 如 qwen-max, qwen-plus, qwen-turbo private final String endpoint = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation"; @Override public ChatResponse call(Prompt prompt) { // 1. 转换 Prompt -> 对应 JSON 请求体 QwenRequest request = buildRequest(prompt); // 2. 设置 Header: Authorization: Bearer ${apiKey} HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("Authorization", "Bearer " + apiKey); // 3. 发起 POST 请求 HttpEntity<QwenRequest> entity = new HttpEntity<>(request, headers); ResponseEntity<QwenResponse> responseEntity = restTemplate.postForEntity( endpoint, entity, QwenResponse.class); // 4. 解析响应 QwenResponse qwenResp = responseEntity.getBody(); String content = qwenResp.getOutput().getText(); return new ChatResponse(new Generation(content)); } } ``` #### 📦 注册为 Spring Bean ```java @Configuration @EnableConfigurationProperties(QwenProperties.class) public class QwenAutoConfiguration { @Bean @ConditionalOnMissingBean(ChatClient.class) public ChatClient qwenChatClient(QwenProperties properties) { return new QwenChatClient(properties.getApiKey(), properties.getModel()); } } ``` #### ✅ 实现了 Spring AI 的统一接口 - 实现 `ChatClient` 接口 → 支持 `.call()` - 可选实现 `StreamingChatClient` → 支持 `.stream()` 流式输出 - 使用标准 `Prompt`, `Message`, `Generation` 模型 👉 这样就能做到:“**换模型不改业务代码**”。 --- ### ✅ 3. 底层调用的是阿里的收费大模型吗? ✅ **是的!绝大多数情况下,底层调用的是阿里云【通义千问】系列大模型,且属于收费服务。** #### 🔗 调用的真实接口 通常指向阿里云 **DashScope(灵积模型服务平台)**: > 🔗 https://dashscope.aliyun.com/ 这个平台提供: | 模型名 | 类型 | 是否收费 | 说明 | |----------------|------------|----------|------| | `qwen-max` | 强推理 | ✅ 收费 | 类似 GPT-4 | | `qwen-plus` | 均衡性能 | ✅ 收费 | 中等成本 | | `qwen-turbo` | 快速响应 | ✅ 收费 | 便宜快速 | | `qwen-local` | 本地部署 | ❌ 免费 | 需下载模型文件 | #### 💰 计费方式 - 按 Token 收费(输入 + 输出) - 示例价格(参考): - qwen-max: ¥0.02 / 1K tokens 输入,¥0.06 / 1K tokens 输出 - 免费额度:新用户有少量免费额度(如 100 万 tokens) #### 🔐 认证方式 使用 **API Key** 进行鉴权: ```http POST https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation Header: Authorization: Bearer <YOUR_API_KEY> ``` 所以:只要你用了真实 API,就会产生调用记录和费用(除非在免费额度内)。 --- ### ✅ 4. 正确推荐的做法:使用官方支持的方式接入阿里模型 虽然没有叫 `spring-ai-alibaba-jmanus` 的官方包,但你可以通过以下方式安全集成: #### ✅ 方案一:自己实现 `QwenChatClient`(推荐) 参考上面结构,对接 DashScope API。 GitHub 上已有多个开源实现可参考: - https://github.com/realZhangChi/spring-ai-qwen-demo - https://gitee.com/dromara/spring-ai-alibaba-dashscope #### ✅ 方案二:等待 Spring AI 官方支持(已在计划中) Spring AI 社区正在积极增加对国产 LLM 的支持: - 已有 PR 提交支持 Qwen(见 GitHub #789) - 未来可能发布 `spring-ai-qwen-spring-boot-starter` #### ✅ 方案三:使用 Alibaba Cloud SDK + 自定义桥接 ```xml <!-- 阿里云官方 SDK --> <dependency> <groupId>com.alibaba.dashscope</groupId> <artifactId>sdk-java</artifactId> <version>1.5.0</version> </dependency> ``` 然后包装成 `ChatClient` 实现。 --- ### ✅ 总结:如何看待 `spring-ai-alibaba-jmanus`? | 项目 | 答案 | |------|------| | 是否官方? | ❌ 否,非阿里或 Spring 官方发布 | | 是否可靠? | ⚠️ 视具体来源而定,建议查看源码 | | 是否收费? | ✅ 是,底层调用 DashScope API 属于收费服务 | | 如何正确对接? | 实现 `ChatClient` 接口 + 调用 `dashscope.aliyuncs.com` API | | 有没有更好选择? | ✅ 用开源社区成熟方案 or 自研 Starter | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值