Mapper|Map-Interactive Travel Agent(demo)

2025-10-03 · Junyi Yan、Claude Sonnet 4.5

本项目受 MapScroll 启发。

地图是数字化时代重要的基础设施。Agent时代,无论作为Tools还是通过MCP调用,在各类智能场景下的地图数据与使用交互,成为了产品设计新的思考命题。


项目概述

「Mapper」是一款基于 AI Agent 自主规划能力、且地图交互联动的智能旅游规划系统,此 demo 聚焦在川西旅游规划。通过先进的 AI 技术和地图交互,为用户提供个性化、智能化的旅行规划服务。

  • AI 自主决策 - Agent 自动识别意图、提取景点、规划路线
  • 实时地图交互 - 双向联动,所见即所得
  • 极简设计 - 专注核心体验,75% 地图 + 25% 对话
  • 流式响应 - 思考过程可视化,实时反馈
  • 多模式切换 - 单点查询 / 行程规划 / 路线导航


技术架构

技术栈

前端

框架: Next.js 15 (App Router)
语言: TypeScript
状态管理: Zustand
UI 组件: Tailwind CSS
地图引擎: 高德地图 2.0 (@amap/amap-jsapi-loader)
实时通信: Socket.io-client
Markdown 渲染: react-markdown

后端

运行时: Node.js 20+ / Bun
框架: Fastify
语言: TypeScript
AI 引擎: Claude Sonnet 4.5 / DeepSeek
实时通信: Socket.io
地图服务: 高德地图 API
日志: Pino

架构设计

┌─────────────────────────────────────────────────────────┐
│                    前端 (Next.js)                        │
│  ┌─────────────────┐        ┌──────────────────────┐     │
│  │  MapContainer   │◄─────►│  PlanningPanel        │     │
│  │  (地图 75%)      │  双向  │  (对话 25%)           │     │
│  │  - 高德地图      │  联动  │  - 流式对话            │     │
│  │  - 标记/路线     │        │  - 思考过程            │     │
│  └─────────────────┘        └──────────────────────┘     │
└────────────────────┬─────────────────────────────────────┘
                     │ WebSocket (Socket.io)

┌─────────────────────────────────────────────────────────┐
│                 后端 Agent 引擎 (Fastify)                │
│  ┌──────────────────────────────────────────────────┐  │
│  │  PlanningAgent (核心 AI 编排)                     │  │
│  │  - 意图识别 (单点/多点/路线)                        │  │
│  │  - 景点提取 (LLM + 正则)                           │  │
│  │  - 实时流式响应                                    │  │
│  │  - 思考步骤发送                                    │  │
│  └──────────────────┬───────────────────────────────┘  │
│                     ↓                                  │
│  ┌──────────────────────────────────────────────────┐  │
│  │  工具层 (Tools)                                   │  │
│  │  - AmapSearchTool (POI 搜索 + 地理过滤)            │  │
│  │  - RoutePlanningTool (路线规划)                   │  │
│  └──────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

工作流程

用户输入

PlanningAgent.generateStreamResponse()

├─→ LLM 生成回复(流式) ──→ 前端显示

└─→ extractAttractions() 提取景点

    AmapSearchTool.batchSearch() 搜索坐标

    WebSocket: map:addMarkers ──→ 前端地图添加标记

    detectRouteIntent() 检测路线意图

    RoutePlanningTool.planRoute() 规划路线

    WebSocket: map:drawRoute ──→ 前端地图绘制路线

核心功能一:Agent 自主规划

功能描述

AI Agent 能够理解用户的自然语言输入,自主分析意图并生成执行计划,无需预定义规则即可完成复杂的多步骤任务。

核心实现

1. 意图分析与任务规划

文件:backend/src/agent/PlanningAgent.ts

// 核心方法:plan() - 分析用户意图并生成任务计划
async plan(userInput: string, context: ExecutionContext): Promise<Task[]> {
  // 构建规划提示词
  const prompt = buildPlanningPrompt(userInput, context);
  const systemPrompt = PLANNING_SYSTEM_PROMPT;

  // 调用 LLM(Claude/DeepSeek)生成任务计划
  const response = await this.anthropicClient.messages.create({
    model: this.model,
    max_tokens: 2000,
    temperature: 0.7,
    system: systemPrompt,
    messages: [{ role: 'user', content: prompt }],
  });

  // 提取并解析 JSON 格式的任务列表
  const planJson = this.extractJSON(content);
  return this.parseTasks(planJson);
}

关键点:

  • 使用 LLM 进行意图理解和任务分解
  • 返回结构化的任务列表(Task[]),每个任务包含类型、描述、参数、依赖关系等
  • 支持 Claude 和 DeepSeek 两种 LLM 提供商

2. 任务执行引擎

文件:backend/src/agent/ExecutionEngine.ts

// 核心方法:execute() - 按依赖关系执行任务
async execute(tasks: Task[], context: ExecutionContext): Promise<ExecutionResult> {
  const results = new Map<string, ToolResult>();

  // 按优先级排序任务
  const sortedTasks = [...tasks].sort((a, b) => a.priority - b.priority);

  for (const task of sortedTasks) {
    // 检查依赖是否满足
    if (!this.checkDependencies(task, results)) {
      throw new Error(`Dependencies not met for task: ${task.id}`);
    }

    // 发送思考步骤到前端
    context.websocket.emit('thinking:step', {
      taskId: task.id,
      content: task.description,
    });

    // 获取对应工具并执行
    const tool = this.getToolForTask(task);
    const result = await tool.execute(task.params, context);
    results.set(task.id, result);
  }

  return { success: true, results, completedTasks, failedTasks };
}

关键点:

  • 依赖检查:确保任务按正确顺序执行
  • 工具映射:根据任务类型选择合适的工具
  • 实时反馈:通过 WebSocket 向前端发送执行状态

3. 智能景点提取

文件:backend/src/agent/PlanningAgent.ts:199-241

// 从 AI 回复中自动提取景点名称
private async extractAttractions(text: string): Promise<string[]> {
  const systemPrompt = ATTRACTION_EXTRACTION_PROMPT;

  // 使用专门的提示词让 LLM 提取景点
  const response = await this.anthropicClient.messages.create({
    model: this.model,
    max_tokens: 500,
    temperature: 0.1,  // 低温度确保准确性
    system: systemPrompt,
    messages: [{ role: 'user', content: text }],
  });

  // 解析提取的景点列表
  return content.split('\n')
    .map(line => line.trim())
    .filter(line => line.length > 0 && line.length < 20);
}

关键点:

  • 低温度(0.1)提高提取准确性
  • 专用提示词优化提取效果
  • 自动过滤无效结果

核心功能二:地图双向交互

功能描述

前端地图与后端 AI 通过 WebSocket 实现双向实时同步,AI 可以在地图上添加标记、绘制路线,用户也可以通过点击地图触发 AI 响应。

核心实现

1. 后端 → 前端:智能地图标记

文件:backend/src/agent/PlanningAgent.ts:246-431

// 流式响应中自动添加地图标记
async *generateStreamResponse(userInput: string, context: ExecutionContext) {
  // 1. 生成 AI 回复(流式)
  for await (const event of stream) {
    const content = event.delta.text;
    fullResponse += content;
    yield content;  // 流式返回给前端
  }

  // 2. 提取景点名称
  const attractions = await this.extractAttractions(fullResponse);

  // 3. 批量搜索 POI(地点信息)
  const pois = await this.amapTool.batchSearch(attractions);

  // 4. 发送地图标记事件到前端
  context.websocket.emit('map:addMarkers', {
    markers: pois.map(poi => ({
      id: poi.id,
      name: poi.name,
      position: poi.location,
      address: poi.address,
      category: poi.category,
    })),
  });

  // 5. 检测路线规划意图
  const hasRouteIntent = this.detectRouteIntent(userInput, fullResponse);
  if (hasRouteIntent && pois.length >= 2) {
    // 规划多点路线
    const routes = await this.routeTool.planMultiPointRoute(waypoints);

    // 发送路线绘制事件
    context.websocket.emit('map:drawRoute', {
      path: fullPath,
      color: '#10b981',
      width: 6,
    });
  }
}

关键点:

  • 流式响应完成后自动提取景点
  • 调用高德地图 API 获取精确坐标
  • 智能检测是否需要绘制路线
  • 通过 WebSocket 事件驱动前端地图更新

2. 前端:WebSocket 事件监听

文件:frontend/hooks/useWebSocket.ts:92-149

// 批量添加地图标记
socketInstance.on('map:addMarkers', (data: any) => {
  console.log('📍 Received markers from backend:', data.markers);
  if (data.markers && Array.isArray(data.markers)) {
    data.markers.forEach((marker: any) => {
      addMarker({
        id: marker.id,
        position: marker.position,
        label: marker.name,
        data: {
          name: marker.name,
          address: marker.address,
          category: marker.category,
        },
      });
    });
  }
});

// 绘制路线
socketInstance.on('map:drawRoute', (data: any) => {
  addRoute({
    id: `route-${Date.now()}`,
    path: data.path,
    color: data.color || '#4285F4',
    width: data.width || 4,
  });
});

// 地图飞行到指定位置
socketInstance.on('map:flyTo', (data: any) => {
  if (data.center && data.zoom) {
    setViewport({
      center: data.center,
      zoom: data.zoom,
    });
  }
});

关键点:

  • 实时接收后端发送的地图操作指令
  • 通过 Zustand store 更新状态触发 UI 重绘
  • 支持标记、路线、视图控制等多种操作

3. 前端 → 后端:用户地图交互

文件:frontend/components/map/MapContainer.tsx:59-74

// 监听地图移动
map.on('moveend', () => {
  const center = map.getCenter();
  const zoom = map.getZoom();
  setViewport({
    center: [center.lng, center.lat],
    zoom,
  });
});

// 监听地图点击
map.on('click', (e: any) => {
  emitMapEvent('map:clicked', {
    location: [e.lnglat.lng, e.lnglat.lat],
  });
});

// 监听标记点击
marker.on('click', () => {
  emitMapEvent('map:markerClicked', {
    markerId: markerData.id,
    location: markerData.position,
    name: markerData.label,
  });
});

关键点:

  • 捕获用户地图操作事件
  • 通过 WebSocket 发送到后端
  • 后端可基于用户操作生成智能响应

4. 高德地图 POI 搜索工具

文件:backend/src/tools/AmapSearchTool.ts:62-103

// 批量搜索景点并过滤川西范围
async batchSearch(names: string[]): Promise<POIResult[]> {
  const chuanxiBounds = {
    minLng: 99.0,  maxLng: 104.0,  // 经度范围
    minLat: 27.0,  maxLat: 33.0    // 纬度范围
  };

  for (const name of names) {
    const pois = await this.searchPOI(name);

    // 只保留川西范围内的 POI
    const validPoi = pois.find(poi => {
      const [lng, lat] = poi.location;
      return lng >= chuanxiBounds.minLng && lng <= chuanxiBounds.maxLng &&
             lat >= chuanxiBounds.minLat && lat <= chuanxiBounds.maxLat;
    });

    if (validPoi) {
      results.push(validPoi);
    }
  }

  return results;
}

关键点:

  • 调用高德地图 Web API 搜索 POI
  • 地理围栏过滤:只返回川西地区的景点
  • 支持批量搜索提高效率

#人工智能#产品实践
https://junyiyan.xyz/posts/feed.xml