友友们,早上好。最近2个月我一直在探索大模型如何低成本、高效率调用地图能力。前段时间我使用了腾讯地图的TMap Miniprogram Skill,它将官方文档、最佳实践与可验证代码模板封装为 AI 技能文件,让 Cursor、Claude、CodeBuddy 等工具精准理解小程序地图 API,大幅提升开发效率与代码质量。那么实际使用效果如何呢?今天就来和我一起看一下我的实战记录吧,希望文章对大家有所帮助。
文章目录
- [一、TMap Miniprogram Skill是什么?](#一、TMap Miniprogram Skill是什么?)
- 二、核心能力有哪些?
-
- [1. 地图组件:基础可视化能力](#1. 地图组件:基础可视化能力)
- [2. MapContext API:32 个精细化控制接口](#2. MapContext API:32 个精细化控制接口)
- [3. 位置服务:微信原生定位能力](#3. 位置服务:微信原生定位能力)
- [4. LBS 后端服务:腾讯位置服务核心能力](#4. LBS 后端服务:腾讯位置服务核心能力)
- 三、实战记录:
-
- [第一步:获取 Skill 文件](#第一步:获取 Skill 文件)
- [第二步:AI IDE 配置](#第二步:AI IDE 配置)
- 第三步:验证安装
- [第四步:申请开发者 Key](#第四步:申请开发者 Key)
- 四、实战效果:自然语言生成业务代码
- 五、开发避坑
- 六、总结
一、TMap Miniprogram Skill是什么?
简单来说TMap Miniprogram Skill 是专为 AI IDE 设计的微信小程序地图开发技能包,它把零散的地图能力整合成 AI 可直接调用的结构化知识,解决传统开发中查文档慢、写代码易出错、兼容性难保障等问题,实现自然语言描述需求到一键生成合规代码的开发新模式。
优势:
能力全覆盖: 整合地图组件、MapContext API、位置服务、LBS 后端 SDK,一站式满足地图展示、定位、POI 搜索、路线规划等需求。
模块化设计: 按功能拆分文档,AI 按需调用,回答更精准,避免无效信息干扰。
代码可验证: 内置 3 个完整示例项目,所有代码经过官方验证,AI 生成代码可直接运行。
深度集成: 无缝对接腾讯位置服务,支持地理编码、行政区划、距离计算等高级能力。
二、核心能力有哪些?
我看了一下文档,Skill 覆盖小程序地图开发全场景,总结就是四大模块:
1. 地图组件:基础可视化能力
支持地图渲染、覆盖物绘制、事件交互,满足基础地图展示需求:

2. MapContext API:32 个精细化控制接口
提供地图运行时操控能力,实现动态交互,比如:
地图控制:获取中心点、移动视野、设置边界
标记管理:添加 / 移除 / 平移标记、轨迹回放
图层操作:图片图层、可视化图层、自定义图层
坐标转换:经纬度与屏幕坐标互转、拉起导航
3. 位置服务:微信原生定位能力
对接微信官方位置接口,稳定获取位置信息:
基础定位:当前位置、模糊位置获取
交互选点:地图选点、POI 选择、位置展示
持续定位:后台定位、位置变化监听
4. LBS 后端服务:腾讯位置服务核心能力
基于 SDK 实现业务级地理服务:
搜索:POI 周边搜索、关键词提示
解析:地理编码 / 逆地理编码
路线:驾车 / 步行 / 骑行 / 公交规划
工具:距离计算、行政区划获取
三、实战记录:
第一步:获取 Skill 文件
推荐 Git 克隆,保证版本更新:
运行
#我们 先克隆skill仓库到本地
git clone https://github.com/TencentLBS/TencentMap_miniprogram_skills.git
# 然后进入目录
cd TencentMap_miniprogram_skills

当然这里,也可直接下载压缩包解压使用。
下载的目录结构大概是这样:

第二步:AI IDE 配置
这里我以WorkBuddy 为例,当然它同时支持Cursor、CodeBuddy 和 Claude Desktop 支持通过 skills 目录加载自定义技能。步骤基本差不多,但是我实测和官方的文档有所区别,好像没有找到官方文档说的某些文件夹,不知道是不是后续改了没更新,大家按照我实测的来即可。如果有变化我也会同步更新这里的操作。
导入技能
你使用什么软件,就在什么软件导入技能

然后将刚才git下来的文件夹里面的skill,导入即可

成功后你在技能这里应该可以看到:

这里官方也有命令行的相关操作,但是它里面提到的/tmap-miniprogram我没有找到,估计是没更新吧。大家这里整体导入即可。
第三步:验证安装
下面我们开始验证是否成功,打开 AI IDE,输入测试指令比如:
帮我使用微信小程序地图组件开发一个功能:
- 显示当前位置的地图
- 添加一个自定义标记点
- 点击标记显示信息气泡
AI 能引用 Skill 并生成完整代码,即导入技能成功。

第四步:申请开发者 Key
登录腾讯位置服务控制台
注册并登录账号,进入「应用管理」→「我的应用」,创建新应用并添加 Key,选择「微信小程序」服务类型
,获取 Key。
我们后续使用腾讯位置服务 SDK 都是要需要配置有效的 Key。

四、实战效果:自然语言生成业务代码
下面来看我第一次生成的效果:
首先是给我生成了完整小程序目录如下:

项目生成的位置,在c盘的user下面。当然你也可以直接问WorkBuddy,它会告诉你。
这里我将生成的小程序,导入到我的微信开发者工具后,发现直接运行就不行。大概看了一下几个主要文件,发现是配置文件里,入口页面还是index,但是index里面又没有什么实际内容,生成的地图页面功能都在map中。

于是只能手动自己改一下,入口页面指向map。接着就成功了,我们看一下效果

可以看到提出的几个功能点还都是实现了。大概使用了一下,完成度都不错,当然了,ui设计还是得自己再美化一下。
接下来我又提了几个其他的补充需求:
比如:
1.显示用户当前位置地图,支持缩放拖动
2.实现搜索框,输入关键词查找周边5公里餐厅,标记展示结果
3.100个随机标记,优化渲染性能等等。
当然中间测试跑的时候,很多代码实现都不太理想,功能并没有达到我的预期,只能实现基本显示。要想完全再配合调用其他能力还得手搓改动。最终我想达到的效果就是一个能够实现水上交通用的小程序。
地图页面代码:
<!--pages/map_chat/map_chat.wxml-->
<!--公告消息-->
<view class="news_row">
<view style="width:100rpx;" bindtap="toNewsInfo">公告栏|</view>
<view style="width:50rpx;" >
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/new.gif" mode="aspectFill" bindtap="addMarker"></cover-image>
</view>
<view style="flex:3;" >
<swiper vertical="true" autoplay="true" interval="5000" style="height: 60rpx;" >
<block wx:for="{{news_list}}">
<swiper-item>
<view class='news_content' data-msg="{{item.msg}}" bindtap="showInfo">{{item.msg}}</view>
</swiper-item>
</block>
</swiper>
</view>
</view>
<!--地图-->
<view class="tui-map" style="bottom:{{m_height_rpx}}">
<map id="map" longitude="{{currentLo}}" latitude="{{currentLa}}" enable-satellite="{{satemap}}" scale="{{scale}}" circles="{{circles}}" markers="{{markers}}" polyline="{{polyline}}" covers="{{covers}}" include-points="{{includePoints}}" bindregionchange='changeSee'
show-scale ="true" show-location="true" style="width: 100%; height: 100%;" bindtap='searchMap' bindmarkertap='markerOnclick'>
<cover-view class="icon" style="display:none">
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{loaction_img?'location':'open'}}.png" mode="aspectFill" bindtap="openMapLocation"></cover-image>
</cover-view>
<cover-view class="icon2" >
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/add_scale2.png" mode="aspectFill" bindtap="addScale"> </cover-image>
</cover-view>
<cover-view class="icon3" >
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/del_scale2.png" mode="aspectFill" bindtap="delScale"></cover-image>
</cover-view>
<cover-view class="c_v start_nav" >
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{start_nav_img?'start_nav':'stop_nav'}}.png" mode="aspectFill" bindtap="changeNavigationType"> </cover-image>
</cover-view>
<cover-view class="c_v up_stream" style="display:{{start_nav_img?'':'none'}}">
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{upstream_img?'upstream_c':'upstream'}}.png" mode="aspectFill" bindtap="changeUpStreamType"> </cover-image>
</cover-view>
<cover-view class="c_v down_stream" style="display:{{start_nav_img?'':'none'}}">
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{downstream_img?'downstream_c':'downstream'}}.png" mode="aspectFill" bindtap="changeDownStreamType"></cover-image>
</cover-view>
<cover-view class="c_v boat_stream" style="display:{{start_nav_img?'':'none'}}">
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{boat_img?'boat_z':'boat_x'}}.png" mode="aspectFill" bindtap="changeBoatType"></cover-image>
</cover-view>
<cover-view class="c_v search_num" style="display:{{start_nav_img?'':'none'}}" bindtouchstart="touchStart" bindtouchend="touchEnd" catchtap="changeSearchNum">
{{search_txt}}
</cover-view>
<cover-view class="c_v search_num2" style="display:{{start_nav_img?'':'none'}}" bindtouchstart="touchStart" bindtouchend="touchEnd" catchtap="changeSearchNum2">
{{search_txt2}}
</cover-view>
<cover-view class="c_v boat_stop" style="display:{{start_nav_img?'':'none'}}">
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/{{boat_stop?'open':'b_stop'}}.png" mode="aspectFill" bindtap="stopVboat"></cover-image>
</cover-view>
<cover-view class="icon4" >
<cover-image style="width: 50rpx;height: 50rpx;" src="../../images/index/location.png" mode="aspectFill" bindtap="loactionByGPS"></cover-image>
</cover-view>
<cover-view class="cover_txt_scale" >
{{scale_txt}}
</cover-view>
<cover-view class="cover_txt_location" bindtap="changeSatemap" >
{{satemap?'卫星':'街道'}}
</cover-view>
<cover-view class="cover_txt_boatinfo" style="width:{{userinfo_img?'45rpx':'600rpx'}}" bindtap="" >
<cover-image style="width:35rpx;height:35rpx;float:left" src="../../images/index/{{userinfo_img?'left_jiantou':'right_jiantou'}}.png" mode="aspectFill" bindtap="openMapUserInfoShow"></cover-image>
<cover-view style="width:550rpx;;margin-left:10rpx;display:{{userinfo_img?'none':''}}" bindtap="toPersonalChat">{{userinfo_text}}</cover-view>
</cover-view>
<cover-view class="cover_txt_first_info" style="display:{{firstObj.isShow&&start_nav_img?'':'none'}};background-color:{{firstObj.color}}" bindtap="" >
<cover-view class=".cover_txt_boxborder">
<cover-view style="width:550rpx;height:50rpx;margin-left:10rpx;margin-top:20rpx;">{{firstObj.txt1}}</cover-view>
<cover-view style="width:550rpx;height:50rpx;;margin-left:10rpx;">{{firstObj.txt2}}
</cover-view>
<cover-view style="width:550rpx;;margin-left:10rpx;">{{firstObj.txt3}}</cover-view>
</cover-view>
</cover-view>
<cover-view class="cover_txt_second_info" style="display:{{secondObj.isShow&&start_nav_img?'':'none'}};background-color:{{secondObj.color}}" bindtap="" >
<cover-view class=".cover_txt_boxborder">
<cover-view style="width:550rpx;height:50rpx;margin-left:10rpx;margin-top:20rpx;">{{secondObj.txt1}}</cover-view>
<cover-view style="width:550rpx;height:50rpx;;margin-left:10rpx;">{{secondObj.txt2}}
</cover-view>
<cover-view style="width:550rpx;;margin-left:10rpx;">{{secondObj.txt3}}</cover-view>
</cover-view>
</cover-view>
<cover-view class="cover_txt_third_info" style="display:{{thirdObj.isShow&&start_nav_img?'':'none'}};background-color:{{thirdObj.color}}" bindtap="" >
<cover-view class=".cover_txt_boxborder">
<cover-view style="width:550rpx;height:50rpx;margin-left:10rpx;margin-top:20rpx;">{{thirdObj.txt1}}</cover-view>
<cover-view style="width:550rpx;height:50rpx;;margin-left:10rpx;">{{thirdObj.txt2}}
</cover-view>
<cover-view style="width:550rpx;;margin-left:10rpx;">{{thirdObj.txt3}}</cover-view>
</cover-view>
</cover-view>
</map>
</view>
<view class='contentv' style="height:{{m_height_rpx}}" >
<view class="layout_set_height">
<view class="set_height" style="flex:1;" bindtap='upHeightMax' >最大化</view>
<view class="set_height" style="flex:1;" bindtap='upHeight'>+</view>
<view class="set_height" style="flex:1;" bindtap='downHeight'>-</view>
<view class="set_height" style="flex:1;" bindtap='downHeightMin' >最小化</view>
</view>
<!--聊天-->
<view class="news" bindtap='outbtn' style="height:{{new_bottom}}">
<view class="chat-notice"></view>
<view class="historycon">
<scroll-view scroll-y="true" class="history" scroll-top="{{scrollTop}}">
<block wx:for="{{newslist}}" wx:key>
<view></view>
<!--自己的消息 -->
<view class="chat-news" wx:if="{{item.userId == userId}}">
<view style="text-align: right;padding-right: 20rpx;">
<text class="name" bindtap="msgNotSend2" data-msgid='{{item.msgId}}'>{{ item.name }} {{ item.time}}</text>
<image class='new_img' src="{{item.url? item.url:'../../images/h04.jpg'}}"></image>
</view>
<view class='my_right'>
<block wx:if="{{item.type=='0'}}">
<view class='new_txt'>{{item.content}}</view>
</block>
<block wx:if="{{item.type=='1'}}">
<image class="selectImg" src="{{item.content}}" data-src="{{item.content}}" lazy-load="true" bindtap="previewImg"></image>
</block>
<block wx:if="{{item.type=='2'}}">
<view class='new_txt'>
<text class="voice_time">"{{cur_voice_id==item.msgId?cur_voice_time:item.remark}}</text>
<image class="voice_img2" src="../../images/index/v2{{cur_voice_id==item.msgId?voiceImg:3}}.png" bindtap='playVoiceList' data-file='{{item.content}}' data-id='{{item.msgId}}' data-time='{{item.remark}}'></image>
</view>
</block>
<block wx:if="{{item.type=='3'}}">
<video class="video_img2" show-fullscreen-btn="{{true}}" bindfullscreenchange="screenChange"
controls="{{video_controls}}" id="video{{item.msgId}}"src='{{item.content}}'> </video>
</block>
</view>
</view>
<!-- 别人的消息 -->
<view class="chat-news" wx:else>
<view style="text-align: left;padding-left: 20rpx;">
<image class='new_img' src="{{item.url? item.url:'../../images/h04.jpg'}}"></image>
<text class="name" bindtap="msgNotSend" data-msgid='{{item.msgId}}'> {{item.time}} {{ item.name }}</text>
</view>
<view class='you_left'>
<block wx:if="{{item.type=='0'}}">
<view class='new_txt'>{{item.content}}</view>
</block>
<block wx:if="{{item.type=='1'}}">
<image class="selectImg" src="{{item.content}}" data-src="{{item.content}}" lazy-load="true" bindtap="previewImg"></image>
</block>
<block wx:if="{{item.type=='2'}}">
<view class='new_txt'>
<image class="voice_img2" src="../../images/index/v{{cur_voice_id==item.msgId?voiceImg:3}}.png" bindtap='playVoiceList' data-file='{{item.content}}' data-id='{{item.msgId}}' data-time='{{item.remark}}'></image>
<text class="voice_time">{{cur_voice_id==item.msgId?cur_voice_time:item.remark}}"</text>
</view>
</block>
<block wx:if="{{item.type=='3'}}">
<video class="video_img" show-fullscreen-btn="{{true}}" bindfullscreenchange="screenChange"
controls="{{video_controls}}" id="video{{item.msgId}}"src='{{item.content}}'> </video>
</block>
</view>
</view>
</block>
</scroll-view>
</view>
</view>
<view id="flag"></view>
<!-- 聊天输入 -->
<view class="message">
<form bindreset="cleanInput" class="sendMessage">
<image class='voice_img' src="../../images/index/{{msg_type?'text_type':'voice_type'}}.png" bindtap='changeMsgType'></image>
<input type="text" style="display:{{msg_type?'':'none'}}" placeholder="请输入聊天内容.." value="{{message}}" bindinput='bindChange'></input>
<button class="voice_but" style="display:{{msg_type?'none':''}}" size="small" bindtap='startVoice'>{{voice_flag?"停止录音":"开始录音"}}</button>
<view class="add" bindtap='increase'>{{increase?'-':'+'}}</view>
<button class ="send_but" bindtap='send'>发送</button>
</form>
<view class='increased {{aniStyle?"slideup":"slidedown"}}' wx:if="{{increase}}">
<view class="image" bindtap='chooseImage'>相册 </view>
<view class="image" bindtap='changeMsgType'>{{msg_type?"录音":"文字"}}</view>
<view class="image" bindtap='chooseVideo'>拍摄 </view>
</view>
<view class='increased {{aniStyle?"slideup":"slidedown"}}' wx:if="{{increase}}">
</view>
</view>
</view>
调用机器人:
bash
Page({
data: {
messages: [], // 对话列表
inputValue: '',
loading: false
},
// 输入
onInput(e) {
this.setData({ inputValue: e.detail.value })
},
// 发送消息
async sendMessage() {
const { inputValue, messages } = this.data
if (!inputValue.trim() || this.data.loading) return
// 加入用户消息
this.setData({
messages: [...messages, { role: 'user', content: inputValue }],
inputValue: '',
loading: true
})
try {
// 调用云开发AI机器人
const res = await wx.cloud.extend.AI.bot.sendMessage({
data: {
botId: '你的botId',
msg: inputValue
}
})
// 流式接收回复
let reply = ''
for await (const chunk of res.textStream) {
reply += chunk
this.setData({
messages: [...this.data.messages.slice(0, -1),
{ role: 'assistant', content: reply }]
})
}
} catch (err) {
console.error('调用失败', err)
}
this.setData({ loading: false })
}
})
最后给大家展示一下完整的实现效果吧:

集合了地图的基础功能放大,缩小、级别显示、定位、导航。然后公告接入了天气信息、新闻消息。同时内置了聊天功能,可以发送消息和智能体进行沟通交流。这里智能体需要云开发功能。有需要,可以看下小程序的官方文档。
五、开发避坑
常见问题和解决方案
几个我遇到的常见问题:
AI 未调用 Skill:检查目录路径、SKILL.md 完整性,提问时明确 "微信小程序地图" 关键词。
定位失败:app.json 声明权限、引导用户授权、用 wx.openSetting 开启权限。
性能优化
这里性能是小程序的性能。
标记点超量:优先用点聚合,避免卡顿
数据更新:减少 setData 频率,批量更新
路线渲染:大量点位做抽稀处理
内存管理:及时销毁无用图层与覆盖物
六、总结
最后简单总结一下,TMap Miniprogram Skill 对于小程序地图开发确实很方便,尤其是初始化一个基本框架的地图小程序。
对新手:降低 API 学习门槛,自然语言生成代码,快速上手;对资深开发者:省去查文档、调试兼容时间,聚焦业务逻辑
;对项目:代码符合官方规范,减少线上 bug,提升稳定性。当然个别不足,就是和ai工具的联动可能还不太全面