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

903 阅读7分钟

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

实现方案

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

具体实现1. 高德地图工具实现

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

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);  
        }  
    }  
}

对应的属性配置类:

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:

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自动装配:

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框架创建一个专门的路线规划代理:

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配置

@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.前端界面

<!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接口,用于接收用户输入并返回规划结果:

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中添加必要的配置:

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