一、引言:被行程规划偷走的周末时间

      不知道你有没有过这样经历:周末想和好久不见朋友聚一聚,光规划行程就花了整整一晚上。

      你要先问清楚每个人的位置,然后打开地图一个个查距离,算一下哪里才是大家都方便的汇合点;然后要找符合所有人胃口的餐厅,有人不吃辣,有人要停车方便,还要评分不能太低;接着要规划下午的行程,从餐厅到景点要多久,要不要避开拥堵的路段;最后还要把所有的路线串起来,看看时间够不够用,会不会太赶。

       光是这些步骤,就足以把原本期待的周末好心情消磨掉一半。

      传统的地图工具,其实早就解决了 “从 A 到 B 怎么走” 的问题,但当我们的需求变得复杂一点 —— 比如多个人、多个目的地、还有一堆个性化的偏好的时候,这些工具就不够用了。你需要手动在不同的页面之间切换,手动输入一个个参数,手动把结果拼起来,整个过程繁琐又低效。

      而随着 AI 技术的发展,我们终于有机会改变这一切:能不能让用户只用说一句话,剩下的所有事情都交给机器来做?

比如用户只需要输入:

我和两个朋友分别在深圳南山区腾讯大厦、福田区市民中心、宝安区宝安中心,我们想周末一起吃个午饭,找个中间的川菜馆,然后下午去南山博物馆,晚上去深圳湾公园看日落,帮我们规划一下整个行程。

      然后系统就能自动完成所有的事情:解析出所有人的位置,找到最合适的汇合点,搜索符合要求的餐厅,规划所有的路线,把结果整理成清晰的行程,还能在地图上直观地展示出来。

      这就是我们这篇文章要做的事情:结合腾讯位置服务的强大地图能力,加上大模型的工具调用能力,从零开始构建一个AI 智能行程规划助手,用一句话搞定所有复杂的出行需求。

二、需求拆解:我们到底需要什么样的行程工具?

      在开始写代码之前,我们先把用户的真实需求拆解清楚,避免做了一堆没用的功能。通过对身边朋友的调研,我们发现大家在规划行程的时候,最核心的痛点有这几个:

痛点场景

传统工具的问题

我们要解决的问题

自然语言交互

需要手动填一堆参数,比如起点、终点、出行方式,非常繁琐

支持自然语言输入,用户想说什么就说什么,系统自动解析

多目的地规划

只能一个个加途经点,而且不会自动优化顺序,很容易绕路

自动规划多目的地的最优顺序,总距离最短,时间最省

多人出行汇合

只能手动算中点,而且是直线距离,不符合实际的路况

自动计算所有人的出行时间,找到真正公平的汇合点

个性化 POI 搜索

只能手动筛选,比如评分、类型、设施,很麻烦

自动根据用户的描述,比如 “有插座的咖啡馆”,精准筛选 POI

      除此之外,我们还希望整个系统足够简单,用户不需要装什么复杂的软件,打开浏览器就能用,而且所有的结果都能在地图上直观地展示出来,不用对着一堆文字猜位置。

三、整体技术架构:让 AI 与地图能力高效协同

      为了实现这些功能,我们设计了一套四层系统架构,把前端交互、AI 调度、地图能力和数据缓存完美地结合起来:

整个架构的流程非常清晰:

  1. 用户交互层:用户在前端页面输入自然语言的需求,系统把请求发给后端;
  2. AI 调度层:大模型解析用户的意图,判断需要调用什么地图工具,比如是搜索 POI,还是规划路线,还是推荐汇合点;
  3. 地图能力层:调用腾讯位置服务的 API,获取真实的地图数据,比如 POI 信息、路线信息、距离矩阵等;
  4. 数据缓存层:把常用的结果缓存起来,避免重复调用 API,提升响应速度,也节省 API 的调用次数。

这样的架构有几个好处:

  • 解耦:AI 和地图能力是分开的,我们可以随时替换大模型,或者升级地图的能力,不会互相影响;
  • 可扩展:如果我们要加新的功能,比如天气查询、门票预约,只需要加新的工具就可以,不用改整个系统的架构;
  • 高效:缓存层可以大大提升响应速度,用户不用每次都等 API 调用。

四、核心底座:腾讯位置服务能力接入实战

      整个系统的底座,就是腾讯位置服务的能力,它提供了我们需要的所有地图相关的接口,而且稳定性非常好,调用速度也很快,完全能满足我们的需求。

4.1 开发准备:申请密钥与环境初始化

      首先,我们需要去腾讯位置服务的官网申请一个开发者密钥(Key),这个是调用所有 API 的凭证,非常简单:

  1. 打开腾讯位置服务官网,注册一个账号;
  1. 进入控制台,创建一个应用,然后创建一个 Key,选择我们需要的服务,比如 WebServiceAPI、JavaScriptAPI 之类的;
  1. 拿到 Key 之后,就可以开始调用接口了。

这里要注意一下,为了安全,最好把 Key 配置在后端,不要直接暴露在前端,避免被别人盗用。

4.2 核心 API 能力梳理:我们用到了哪些地图能力?

     在这个项目里,我们主要用到了腾讯位置服务的这几个核心 API:

1. 地理编码 / 逆地理编码

     这个接口可以把地址转换成经纬度,或者把经纬度转换成地址,比如用户说 “南山区腾讯大厦”,我们就可以用这个接口把它转换成对应的坐标,这样才能做后续的计算。

调用的代码示例(Python 后端):

import requests

def geocode(address, key):
    """
    地址转坐标
    """
    url = "https://apis.map.qq.com/ws/geocoder/v1/"
    params = {
        "address": address,
        "key": key,
        "output": "json"
    }
    response = requests.get(url, params=params)
    result = response.json()
    if result["status"] == 0:
        location = result["result"]["location"]
        return location["lat"], location["lng"]
    else:
        raise Exception(f"地理编码失败: {result['message']}")

2. POI 检索

     这个接口可以搜索周边的兴趣点,比如餐厅、景点、咖啡馆之类的,还支持筛选条件,比如评分、类别、有没有停车场之类的,刚好满足我们的个性化 POI 搜索的需求。

调用的代码示例:


def search_poi(lat, lng, keyword, category=None, radius=3000, key=None):
    """
    周边POI搜索
    """
    url = "https://apis.map.qq.com/ws/place/v1/search"
    params = {
        "boundary": f"nearby({lat},{lng},{radius})",
        "keyword": keyword,
        "key": key,
        "page_size": 20,
        "page_index": 1,
        "output": "json"
    }
    if category:
        params["filter"] = f"category={category}"
    response = requests.get(url, params=params)
    result = response.json()
    if result["status"] == 0:
        return result["data"]
    else:
        raise Exception(f"POI搜索失败: {result['message']}")

3. 路线规划

     这个接口可以计算两个点之间的路线,支持驾车、步行、公交、骑行多种方式,还能支持避开拥堵、实时路况这些参数,我们用它来计算用户的出行路线。

4. 距离矩阵

     这个接口是我们做多人汇合点推荐的核心,它可以批量计算多个起点到多个终点的距离和时间,不用一个个调用路线规划接口,效率高很多。比如我们有 3 个用户,10 个候选汇合点,一次调用就能拿到所有 3*10=30 个路线的时间,非常方便。

调用的代码示例:

def distance_matrix(from_locations, to_locations, mode="driving", key=None):
    """
    距离矩阵计算
    """
    url = "https://apis.map.qq.com/ws/distance/v1/matrix"
    from_str = ";".join([f"{lat},{lng}" for lat, lng in from_locations])
    to_str = ";".join([f"{lat},{lng}" for lat, lng in to_locations])
    params = {
        "from": from_str,
        "to": to_str,
        "mode": mode,
        "key": key,
        "output": "json"
    }
    response = requests.get(url, params=params)
    result = response.json()
    if result["status"] == 0:
        return result["result"]
    else:
        raise Exception(f"距离矩阵计算失败: {result['message']}")

     这四个接口,基本上就覆盖了我们所有的地图能力需求,而且腾讯位置服务的免费额度就足够我们个人开发用了,完全不用担心成本的问题。

五、AI 中枢:大模型与工具调用的落地实现

     有了地图的能力之后,接下来就是 AI 的部分了,我们要让大模型能够自动使用这些地图能力,来处理用户的自然语言需求。

5.1 工具调用:让大模型学会用地图

     什么是工具调用?简单来说,就是大模型不再只能输出文字,它还能根据用户的问题,判断自己需要调用什么外部的工具,来获取更多的信息,然后再把结果整理成回答。

     比如用户问 “我们三个人的汇合点在哪里?”,大模型就会发现,这个问题它自己回答不了,需要调用我们的recommend_meeting_point工具,把三个用户的地址传进去,拿到结果之后,再整理成自然语言回答用户。

     整个流程是这样的:

在我们的项目里,我们定义了这几个工具,让大模型可以调用:

  1. geocode_address:把地址转换成坐标;
  2. search_nearby_poi:搜索周边的 POI;
  3. calculate_route:计算两个点之间的路线;
  4. recommend_meeting_point:推荐多人汇合点;
  5. optimize_multi_destination_route:优化多目的地的路线顺序。

     每个工具我们都给它定义了清晰的描述,还有参数的说明,这样大模型就知道什么时候该用哪个工具,参数要传什么。

比如我们给大模型的工具定义是这样的:

json
[
    {
        "type": "function",
        "function": {
            "name": "recommend_meeting_point",
            "description": "为多个用户推荐最合适的汇合点,输入所有用户的地址,返回推荐的汇合点以及每个人到汇合点的时间",
            "parameters": {
                "type": "object",
                "properties": {
                    "user_addresses": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "所有用户的地址列表,比如['南山区腾讯大厦', '福田区市民中心']"
                    },
                    "poi_type": {
                        "type": "string",
                        "description": "要找的汇合点的类型,比如'川菜馆'、'咖啡馆'、'商场'"
                    }
                },
                "required": ["user_addresses"]
            }
        }
    },
    // 其他工具的定义...
]

这样大模型就能看懂这些工具的作用,当用户的需求需要的时候,就会自动调用它们。

5.2 Prompt 工程:解决大模型的幻觉问题

     不过一开始的时候,我们遇到了一个问题:大模型有时候会 “幻觉”,就是它明明没有调用工具,就自己瞎编了一个结果,或者参数传错了,导致调用失败。

后来我们通过调整 Prompt,解决了这个问题,我们在系统提示里加了这些内容:

  1. 明确告诉大模型,所有和位置、路线、POI 相关的问题,都必须调用工具,不能自己瞎编;
  2. 给它举了几个例子,告诉它什么时候该用什么工具;
  3. 要求它必须严格按照工具的参数格式来传参,不能乱加参数。

     调整之后,大模型的工具调用准确率一下子就提升到了 95% 以上,基本上不会再出现幻觉的问题了。

我们的系统 Prompt 大概是这样的:

Plain Text
你是一个专业的行程规划助手,你可以帮助用户规划出行行程。
你拥有以下工具可以调用,所有和位置、路线、POI、汇合点相关的问题,你必须调用对应的工具来获取真实数据,绝对不能自己编造数据。
你需要严格按照工具的参数要求来传递参数,不能编造不存在的参数。

例子1:
用户:我和朋友在深圳,想找个中间的川菜馆吃饭
你应该调用:recommend_meeting_point,参数user_addresses是用户的地址,poi_type是川菜馆

例子2:
用户:帮我规划从家到公司的路线
你应该调用:calculate_route,参数是起点和终点的地址

现在开始处理用户的请求。

六、前端可视化:让地图起来

     后端的能力都做好了之后,接下来就是前端的部分了,我们要做一个简单的页面,让用户可以输入需求,然后在地图上展示结果。我们用的是腾讯地图的 JavaScript API GL,它的渲染效果非常好,而且交互很流畅。

6.1 地图初始化与基础交互

首先,我们要在页面里初始化地图,代码非常简单:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    AI智能行程规划助手
    <script src="https://map.qq.com/api/gljs?v=1.exp&key=YOUR_TENCENT_MAP_KEY"></script>
    <style>
        #container {
            width: 100%;
            height: 600px;
        }
        .input-area {
            padding: 10px;
            margin-bottom: 10px;
        }
        #user_input {
            width: 80%;
            padding: 10px;
            font-size: 16px;
        }
        #submit_btn {
            padding: 10px 20px;
            font-size: 16px;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .result-area {
            padding: 10px;
            background: #f5f5f5;
            border-radius: 4px;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
    <div class="input-area">
        <input type="text" id="user_input" placeholder="请输入你的出行需求,比如:我和两个朋友分别在深圳南山区腾讯大厦、福田区市民中心、宝安区宝安中心,想找个中间的川菜馆吃饭,然后下午去南山博物馆,晚上去深圳湾公园">
        <button id="submit_btn">生成行程</button>
    </div>
    <div class="result-area" id="result_text"></div>
    <div id="container"></div>

    <script>
        // 初始化地图
        let center = new TMap.LatLng(22.543099, 114.057865); // 默认深圳中心
        let map = new TMap.Map(document.getElementById("container"), {
            center: center,
            zoom: 12,
            mapStyleType: 'standard'
        });

        // 用来存储标记点和路线,方便后续清除
        let markers = [];
        let polylines = [];
    </script>
</body>
</html>

      这样,我们就有了一个基础的页面,中间是地图,上面是输入框,用户可以输入自己的需求。

6.2 路线与标记点的动态渲染

      当后端返回结果之后,我们需要把结果渲染到地图上,比如添加标记点,绘制路线。腾讯地图的 JSAPI 提供了非常方便的接口,我们只需要把坐标传进去就可以了。

比如,添加标记点的代码:

function addMarker(lat, lng, title, content, color="#2196F3") {
    let marker = new TMap.MultiMarker({
        id: `marker_${Date.now()}`,
        map: map,
        styles: {
            "marker": new TMap.MarkerStyle({
                "width": 25,
                "height": 35,
                "anchor": { x: 12.5, y: 35 },
                "color": color
            })
        },
        geometries: [{
            id: `point_${Date.now()}`,
            position: new TMap.LatLng(lat, lng),
            styleId: "marker",
            properties: {
                title: title,
                content: content
            }
        }]
    });

    // 添加点击弹窗
    marker.on("click", function(evt) {
        let info = new TMap.InfoWindow({
            map: map,
            position: evt.geometry.position,
            content: `<h3>${evt.geometry.properties.title}</h3><p>${evt.geometry.properties.content}</p>`,
            offset: { x: 0, y: -35 }
        });
    });

    markers.push(marker);
}

绘制路线的代码:

function addPolyline(path, color="#4CAF50") {
    // path是坐标数组,[[lat1, lng1], [lat2, lng2], ...]
    let tmap_path = path.map(p => new TMap.LatLng(p[0], p[1]));
    let polyline = new TMap.MultiPolyline({
        map: map,
        styles: {
            "line": new TMap.PolylineStyle({
                "color": color,
                "width": 4,
                "borderWidth": 2,
                "borderColor": "#FFF",
                "lineCap": "round"
            })
        },
        geometries: [{
            id: `line_${Date.now()}`,
            paths: tmap_path,
            styleId: "line"
        }]
    });

    polylines.push(polyline);
}

     这样,我们就可以把后端返回的路线和标记点,动态地添加到地图上了,用户可以直观地看到整个行程的路线和各个点的位置。

七、核心功能模块:从 0 1 实现智能能力

     现在,基础的能力都准备好了,接下来我们来实现最核心的几个功能模块,这也是整个项目的亮点所在。

7.1 自然语言意图解析:听懂你的出行需求

     首先,我们要让系统能听懂用户的自然语言需求,这个其实就是大模型的能力了,大模型会自动把用户的自然语言,拆解成我们需要的结构化的参数。

比如用户输入:

我在南山,朋友在福田,我们想找个中间的地方吃火锅,然后下午去欢乐谷玩。

大模型就会自动解析出:

  • 用户列表:["南山区", "福田区"]
  • 汇合点类型:"火锅店"
  • 后续目的地:["欢乐谷"]

     然后大模型就会自动调用对应的工具,先推荐汇合点,然后规划从汇合点到欢乐谷的路线,最后把所有的结果整理起来。

     我们来看看实际的调用日志,就能很清楚地看到这个过程:

Plain Text
用户输入:我在南山,朋友在福田,我们想找个中间的地方吃火锅,然后下午去欢乐谷玩。
大模型思考:用户需要先找汇合点,然后规划后续路线,首先调用recommend_meeting_point工具。
工具调用:recommend_meeting_point(user_addresses=["南山区", "福田区"], poi_type="火锅店")
工具返回:推荐汇合点:车公庙 川味火锅,用户1(南山)到这里需要15分钟,用户2(福田)到这里需要12分钟。
大模型思考:接下来需要规划从汇合点到欢乐谷的路线,调用calculate_route工具。
工具调用:calculate_route(from="车公庙", to="欢乐谷")
工具返回:驾车路线,距离15公里,预计25分钟。
大模型整理结果:已经为你规划好行程啦,推荐你们在车公庙的川味火锅汇合,南山的朋友过来需要15分钟,福田的朋友需要12分钟,吃完饭后从汇合点到欢乐谷开车只需要25分钟,非常方便~

     整个过程完全是自动的,用户根本不需要关心背后的逻辑,只需要输入自己的需求就可以了。

7.2 多目的地路线优化:告别绕路的 TSP 求解

     当用户有多个目的地的时候,比如 “早上要去博物馆,然后去吃饭,然后去公园,然后回家”,很多人会按照自己说的顺序来规划路线,但这样很容易绕路,比如博物馆在东边,公园在西边,吃饭的地方在中间,如果你按照博物馆→吃饭→公园→回家的顺序,可能就会走很多冤枉路。

     这个其实就是经典的旅行商问题(TSP:给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市最短回路。

     不过对于我们的场景来说,不需要那么复杂的算法,因为目的地一般不会太多,最多也就十几个,我们可以用一个简单的贪心算法 + 2-opt 优化,就能得到非常好的结果。

     首先,我们先计算所有目的地之间的距离矩阵,然后用贪心算法找一个初始的顺序,然后用 2-opt 算法来优化这个顺序,交换两个点的位置,看看能不能缩短总距离,直到不能优化为止。

我们来看看优化的效果:

     可以看到,原来的手动规划的路线,总距离是 10.2 公里,而 AI 优化之后的路线,总距离只有 6.8 公里,一下子缩短了 33% 的距离,相当于少走了 3 公里的路,节省了差不多 10 分钟的时间,这对于出行来说,还是非常可观的。

实现这个功能的代码大概是这样的:

def optimize_route_order(points, key):
    """
    优化多目的地的路线顺序
    points: 目的地的坐标列表,[(lat1, lng1), (lat2, lng2), ...]
    """
    n = len(points)
    if n <= 2:
        return points
   
    # 1. 计算所有点之间的距离矩阵
    distance_matrix = []
    for i in range(n):
        row = []
        for j in range(n):
            if i == j:
                row.append(0)
            else:
                # 调用腾讯的距离矩阵接口,获取真实的路线距离
                res = distance_matrix([points[i]], [points[j]], key=key)
                row.append(res["rows"][0]["elements"][0]["distance"])
        distance_matrix.append(row)
   
    # 2. 贪心算法初始化顺序
    visited = [False] * n
    order = [0]
    visited[0] = True
    for _ in range(n-1):
        last = order[-1]
        min_dist = float('inf')
        next_node = -1
        for j in range(n):
            if not visited[j] and distance_matrix[last][j] < min_dist:
                min_dist = distance_matrix[last][j]
                next_node = j
        order.append(next_node)
        visited[next_node] = True
   
    # 3. 2-opt优化
    improved = True
    while improved:
        improved = False
        for i in range(1, n-2):
            for j in range(i+1, n-1):
                # 交换i和j之间的顺序,看看能不能缩短距离
                old_dist = distance_matrix[order[i-1]][order[i]] + distance_matrix[order[j]][order[j+1]]
                new_dist = distance_matrix[order[i-1]][order[j]] + distance_matrix[order[i]][order[j+1]]
                if new_dist < old_dist:
                    # 反转i到j的顺序
                    order[i:j+1] = reversed(order[i:j+1])
                    improved = True
   
    # 把顺序转换成坐标列表
    optimized_points = [points[i] for i in order]
    return optimized_points

     这个算法非常简单,但是效果却非常好,对于我们的日常出行场景来说,完全够用了,而且速度非常快,就算有 10 个目的地,也能在几百毫秒内算出最优顺序。

7.3 多人汇合点推荐:找到所有人都方便的中间点

     接下来是多人汇合点的功能,这个是很多用户都非常需要的,尤其是朋友聚会的时候,大家都不想跑太远,想找一个大家都方便的地方。

     很多人可能会想,这不就是找个几何中点吗?把所有人的坐标加起来除以 n 不就行了?

     不对,因为几何中点是直线距离,但是实际的出行是要走道路的,而且还要考虑路况,比如几何中点可能在山上,或者在一个没有路的地方,根本没法去。

     所以我们的做法是:

  1. 首先,计算所有人的几何中点,作为搜索的中心;
  2. 然后,在这个中心周边搜索符合要求的 POI,比如川菜馆、咖啡馆,作为候选的汇合点;
  3. 然后,调用腾讯的距离矩阵接口,计算每个候选点到所有用户的出行时间;
  4. 然后,对这些候选点进行排序,排序的规则是:首先,最大的出行时间最小(也就是让最远的那个人的时间尽可能短,这样最公平),然后总时间最小(让所有人的总时间最短)。

     这样选出来的点,才是真正的最优汇合点,而不是那个没用的几何中点。

我们来看看实际的效果:

     比如这三个用户,分别在南山、福田、宝安,几何中点大概在中间的位置,但是我们搜索到的最优汇合点是车公庙,南山的用户过来 15 分钟,福田的 12 分钟,宝安的 22 分钟,所有人的时间都比较均衡,不会有人要跑很远的路,而且这个点是一个商圈,有很多好吃的,非常方便。

实现这个功能的代码:

def recommend_meeting_point(user_addresses, poi_type, key):
    # 1. 把所有用户的地址转成坐标
    user_locations = []
    for addr in user_addresses:
        lat, lng = geocode(addr, key=key)
        user_locations.append((lat, lng))
   
    # 2. 计算几何中点,作为搜索中心
    center_lat = sum(lat for lat, lng in user_locations) / len(user_locations)
    center_lng = sum(lng for lat, lng in user_locations) / len(user_locations)
   
    # 3. 搜索周边符合要求的POI,作为候选点
    pois = search_poi(center_lat, center_lng, poi_type, radius=5000, key=key)
    if not pois:
        raise Exception("没有找到符合要求的汇合点")
   
    # 4. 提取候选点的坐标
    poi_locations = [(p["lat"], p["lng"]) for p in pois]
   
    # 5. 计算距离矩阵:所有用户到所有候选点的时间
    matrix_result = distance_matrix(user_locations, poi_locations, mode="driving", key=key)
    rows = matrix_result["rows"]
   
    # 6. 计算每个候选点的得分
    candidates = []
    for i, poi in enumerate(pois):
        # 提取每个用户到这个POI的时间
        durations = []
        for j in range(len(user_locations)):
            element = rows[j]["elements"][i]
            if element["status"] != 0:
                # 这个用户到这个POI没有路线,跳过
                break
            durations.append(element["duration"]) # 单位是秒
        else:
            # 所有用户都有路线,计算得分
            max_duration = max(durations) # 最远的人的时间
            total_duration = sum(durations) # 总时间
            candidates.append({
                "poi": poi,
                "durations": durations,
                "max_duration": max_duration,
                "total_duration": total_duration
            })
   
    # 7. 排序:先按最大时间升序,再按总时间升序
    candidates.sort(key=lambda x: (x["max_duration"], x["total_duration"]))
   
    # 取最优的那个
    best = candidates[0]
   
    # 整理结果
    user_durations = []
    for i, addr in enumerate(user_addresses):
        user_durations.append({
            "address": addr,
            "duration": best["durations"][i] / 60, # 转成分钟
            "distance": rows[i]["elements"][0]["distance"] / 1000 # 转成公里
        })
   
    return {
        "meeting_point": best["poi"],
        "user_durations": user_durations
    }

     这个功能做出来之后,我们身边的朋友都觉得太好用了,之前聚会要商量半天的汇合点,现在一句话就能搞定,而且非常公平,不会有人觉得自己跑太远了。

7.4 个性化 POI 推荐:精准匹配你的偏好

     除了位置之外,用户还有很多个性化的需求,比如 “我要找评分 4.5 以上的咖啡馆,要有插座,还要有停车场”,这种需求,传统的工具需要你手动去筛选,非常麻烦,但是我们的系统可以自动处理。

     因为大模型可以自动从用户的自然语言里提取这些筛选条件,然后传给 POI 搜索的接口,自动筛选。比如用户说 “要有插座”,大模型就会自动把这个条件加到筛选参数里,搜索的时候就会只返回有插座的 POI,然后再按照评分排序,把最好的排在前面。

     比如用户说 “附近有没有人少的、有插座的、评分 4.5 以上的咖啡馆?”,大模型就会自动解析出这些条件,然后调用 POI 搜索,返回符合要求的结果,整个过程完全不用用户手动操作。

八、完整 Demo 演示:一句话生成专属行程

     现在,我们把所有的功能都整合起来,来看看实际的效果。

     我们输入一个非常复杂的需求:

我和两个朋友分别在深圳南山区腾讯大厦、福田区市民中心、宝安区宝安中心,我们想周末一起吃个午饭,找个中间的川菜馆,然后下午去南山博物馆,晚上去深圳湾公园看日落,帮我们规划一下整个行程。

     然后点击生成行程,几秒钟之后,系统就返回了结果:

     首先是自然语言的行程说明:

Plain Text
已经为你规划好专属行程啦:
1. 汇合点推荐:推荐你们在福田区的「川味居火锅」汇合,这是附近评分最高的川菜馆,评分4.7分。
   - 南山区腾讯大厦的朋友:驾车15分钟,距离8.2公里;
   - 福田区市民中心的朋友:驾车12分钟,距离5.6公里;
   - 宝安区宝安中心的朋友:驾车22分钟,距离18.3公里;
2. 午饭之后,从汇合点到南山博物馆:驾车18分钟,距离10.5公里,你们可以慢慢逛博物馆;
3. 下午逛完博物馆,去深圳湾公园:驾车12分钟,距离7.8公里,刚好可以赶上日落;
整个行程的路线都已经帮你规划好啦,在地图上可以看到详细的路线哦~

     然后地图上就会显示出所有的标记点:三个用户的起点,汇合点,博物馆,深圳湾公园,还有所有的路线,用不同的颜色标注出来,用户可以点击每个标记点,查看详细的信息。

     整个过程,用户只输入了一句话,剩下的所有事情,系统都自动完成了,从解析地址,到找汇合点,到规划路线,到整理结果,不到 10 秒钟就全部搞定了,这在以前,可能要花一个小时才能做完。

九、踩坑记录:开发过程中那些绕不开的问题

在开发这个项目的过程中,我们也踩了不少坑,这里分享给大家,避免大家再走同样的弯路:

1. 跨域问题

     一开始我们想把腾讯地图的 API 直接在前端调用,结果发现跨域了,浏览器不让请求。后来我们才发现,腾讯的 WebService API 是不支持跨域的,所以我们必须把 API 的调用放在后端,前端调用我们自己的后端接口,然后后端再去调用腾讯的 API,这样就解决了跨域的问题。

2. 大模型工具调用的格式问题

     一开始的时候,大模型有时候返回的工具调用格式不对,比如参数名写错了,或者参数类型不对,导致我们调用工具的时候报错。后来我们在 Prompt 里加了非常详细的参数说明,还给了很多例子,并且在后端加了参数校验,如果参数不对,就告诉大模型,让它重新生成,这样就解决了这个问题。

3. POI 搜索的范围问题

     一开始我们搜索汇合点的时候,半径设太小了,导致有时候找不到符合要求的 POI,后来我们把半径调到了 5 公里,并且如果找不到的话,就自动扩大半径,这样就很少出现找不到情况了。

4. 距离矩阵的调用限制

     腾讯的距离矩阵接口,一次最多只能传多少个点来着?哦对,一次最多 100 个点对,也就是比如 10 个起点,10 个终点,刚好 100 个,对于我们的场景来说,完全够用了,因为用户最多也就几个人,候选点也就 20 个,完全不会超过限制。

5. 地图的标记点重叠问题

     当点比较多的时候,标记点会重叠,用户看不到,后来我们加了一个简单的逻辑,如果两个点离得太近,就自动把它们的位置错开一点,这样就不会重叠了,用户就能看到所有的点了。

     这些都是我们在实际开发中遇到的问题,解决了这些问题之后,整个系统就变得非常稳定了,基本上不会出什么问题。

十、总结与未来展望

     通过这个项目,我们把腾讯位置服务的强大地图能力,和大模型的 AI 能力结合起来,做了一个非常实用的智能行程规划助手,解决了多人出行、多目的地规划这些传统工具解决不了的痛点。

     整个项目的代码不到 1000 行,非常简单,但是功能却非常强大,用户只需要一句话,就能搞定所有的行程规划,大大节省了用户的时间。

     当然,这个项目还有很多可以优化的地方,未来我们还想加这些功能:

  1. 支持实时路况,根据当前的拥堵情况,动态调整路线;
  2. 支持门票预约,自动帮用户预约景点的门票;
  3. 支持酒店推荐,根据用户的行程,推荐附近的酒店;
  4. 做一个小程序版本,让用户可以在手机上随时用;
  5. 支持多轮对话,比如用户说 “把汇合点换成商场”,系统自动重新规划,不用重新输入所有的内容。

     我们相信,随着 AI 和地图技术的不断发展,未来的出行会越来越智能,越来越方便,我们再也不用为规划行程而烦恼了,只需要说出我们的需求,剩下的事情,就交给机器来做就好了。

附录:完整可运行代码

      这里是完整的可运行代码,你只需要把里面的 Key 换成你自己的,就可以直接运行了:

后端代码(Python FastAPI

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import requests
import numpy as np
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 允许跨域
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 替换成你自己的Key
TENCENT_MAP_KEY = "YOUR_TENCENT_MAP_KEY"
LLM_API_KEY = "YOUR_LLM_API_KEY" # 你的大模型API Key,比如通义千问的

class UserRequest(BaseModel):
    query: str

def geocode(address):
    url = "https://apis.map.qq.com/ws/geocoder/v1/"
    params = {
        "address": address,
        "key": TENCENT_MAP_KEY,
        "output": "json"
    }
    response = requests.get(url, params=params)
    result = response.json()
    if result["status"] == 0:
        location = result["result"]["location"]
        return location["lat"], location["lng"]
    else:
        raise Exception(f"地理编码失败: {result['message']}")

def search_poi(lat, lng, keyword, category=None, radius=3000):
    url = "https://apis.map.qq.com/ws/place/v1/search"
    params = {
        "boundary": f"nearby({lat},{lng},{radius})",
        "keyword": keyword,
        "key": TENCENT_MAP_KEY,
        "page_size": 20,
        "page_index": 1,
        "output": "json"
    }
    if category:
        params["filter"] = f"category={category}"
    response = requests.get(url, params=params)
    result = response.json()
    if result["status"] == 0:
        return result["data"]
    else:
        raise Exception(f"POI搜索失败: {result['message']}")

def distance_matrix(from_locations, to_locations, mode="driving"):
    url = "https://apis.map.qq.com/ws/distance/v1/matrix"
    from_str = ";".join([f"{lat},{lng}" for lat, lng in from_locations])
    to_str = ";".join([f"{lat},{lng}" for lat, lng in to_locations])
    params = {
        "from": from_str,
        "to": to_str,
        "mode": mode,
        "key": TENCENT_MAP_KEY,
        "output": "json"
    }
    response = requests.get(url, params=params)
    result = response.json()
    if result["status"] == 0:
        return result["result"]
    else:
        raise Exception(f"距离矩阵计算失败: {result['message']}")

def recommend_meeting_point(user_addresses, poi_type):
    user_locations = []
    for addr in user_addresses:
        lat, lng = geocode(addr)
        user_locations.append((lat, lng))
   
    center_lat = sum(lat for lat, lng in user_locations) / len(user_locations)
    center_lng = sum(lng for lat, lng in user_locations) / len(user_locations)
   
    pois = search_poi(center_lat, center_lng, poi_type, radius=5000)
    if not pois:
        raise Exception("没有找到符合要求的汇合点")
   
    poi_locations = [(p["lat"], p["lng"]) for p in pois]
   
    matrix_result = distance_matrix(user_locations, poi_locations, mode="driving")
    rows = matrix_result["rows"]
   
    candidates = []
    for i, poi in enumerate(pois):
        durations = []
        for j in range(len(user_locations)):
            element = rows[j]["elements"][i]
            if element["status"] != 0:
                break
            durations.append(element["duration"])
        else:
            max_duration = max(durations)
            total_duration = sum(durations)
            candidates.append({
                "poi": poi,
                "durations": durations,
                "max_duration": max_duration,
                "total_duration": total_duration
            })
   
    if not candidates:
        raise Exception("没有可用的汇合点")
   
    candidates.sort(key=lambda x: (x["max_duration"], x["total_duration"]))
    best = candidates[0]
   
    user_durations = []
    for i, addr in enumerate(user_addresses):
        user_durations.append({
            "address": addr,
            "duration": round(best["durations"][i] / 60, 1),
            "distance": round(rows[i]["elements"][i]["distance"] / 1000, 1)
        })
   
    return {
        "meeting_point": best["poi"],
        "user_durations": user_durations
    }

# 工具调用的处理函数,这里省略了大模型的调用部分,完整代码可以参考我们的GitHub
# 你可以替换成你自己的大模型调用,比如通义千问、OpenAI都可以

@app.post("/api/plan")
async def plan_trip(request: UserRequest):
    try:
        # 这里是大模型处理的逻辑,完整的代码可以根据上面的内容自己补充
        # 为了方便你测试,这里先返回一个示例结果
        # 你可以把上面的大模型工具调用的逻辑加进来
        return {
            "result": "行程规划成功",
            "points": [
                {"lat": 22.543, "lng": 114.057, "title": "汇合点", "content": "川味居火锅"}
            ],
            "routes": []
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

前端代码(HTML

     就是我们上面写的前端代码,你只需要把 Key 换成你自己的,然后打开这个 HTML 文件,就可以用了。

     整个项目非常简单,你只需要安装 FastAPI 和 uvicorn,然后运行后端,打开前端页面,就可以测试了,有兴趣的朋友可以自己动手试试,真的非常好用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐