【腾讯位置服务开发者征文大赛】AI厕急达我用腾讯位置服务做了一个移动端找厕所AI助手项目名称厕急达 ToiletGo应用形态移动端 H5 / App WebView / 小程序均可迁移技术方向腾讯位置服务 移动端定位 附近 POI 检索 步行导航 AI 偏好解析项目地址https://gitcode.com/mrdeam/ToiletGo.git视频演示AI厕急达我用腾讯位置服务做了一个移动端找厕所AI助手写在前面找厕所这件小事其实很适合做位置服务 Demo很多地图类 Demo 喜欢做路线规划、附近美食、周边景点这些当然都是很标准的场景。但我做第二个参赛作品时反而想选一个更日常、更急迫、也更有移动端特点的问题在外面突然想上厕所怎么最快找到一个靠谱的卫生间这个需求看起来很小但它非常真实。你可能在景区、商圈、地铁口、大学校园、医院附近也可能只是走在一条陌生街道上。这个时候用户通常不会有耐心慢慢筛选也不想看一堆复杂信息。用户真正需要的是我现在在哪里附近最近的厕所在哪里步行过去要多久是公共厕所、商场卫生间还是地铁站卫生间能不能一键导航如果我带小孩、老人能不能优先找商场、医院、公共服务设施里的厕所所以我做了一个移动端应用构想厕急达 ToiletGo。它不是简单地搜索“厕所”两个字而是围绕移动端的急用场景把腾讯位置服务的定位、逆地址解析、地点搜索、步行路线规划和地图展示串成一个完整体验用户打开页面授权定位系统自动搜索附近厕所按“步行可达 类型可靠 距离合理”排序并给出一键导航入口。一、为什么这个场景适合移动端找厕所这个需求和桌面端关系不大它几乎天然发生在手机上。用户不会在电脑前提前规划“十分钟后我要去哪里上厕所”。多数情况下这个需求突然出现而且伴随明显的时间压力。也就是说应用设计不能像普通地图页面那样让用户慢慢输入、慢慢比较而要尽量减少操作步骤。我给厕急达定了三个移动端原则打开即定位用户不需要手动输入起点当前位置就是默认起点。搜索即排序不是把所有 POI 扔给用户而是先给出最值得去的几个。点击即出发每个结果都要有明确的步行时间、距离、类型和导航按钮。这个场景也很适合体现腾讯位置服务的价值。因为它不是只用一个接口而是需要多个能力配合腾讯位置服务能力在厕急达中的作用定位能力获取用户当前位置作为搜索中心点逆地址解析把坐标转换成用户能看懂的位置描述地点搜索检索附近公厕、卫生间、商场、地铁站等 POI步行路线规划计算从当前位置到目标厕所的步行路径和耗时地图展示在移动端地图上展示候选点位和推荐路线这里最重要的不是“接口调通”而是把接口组织成一个能解决具体问题的流程。二、应用最终体验设计厕急达的首页只有一个目标让用户尽快找到可去的厕所。我没有做复杂的首页宣传也没有做大段功能介绍而是把页面分成四块区域内容顶部定位条展示当前位置、定位状态、刷新定位按钮快捷筛选最近优先、公共厕所优先、商场优先、地铁站优先、步行 10 分钟内地图区域展示当前位置、厕所点位、推荐路线底部结果卡片展示推荐厕所、距离、步行耗时、类型、导航按钮用户打开应用后的典型流程是授权定位应用显示“你在西安市碑林区友谊西路附近”自动搜索 1.5 公里范围内的公厕和卫生间系统推荐 3 到 5 个结果用户点击其中一个地图绘制步行路线用户点击“去这里”跳转腾讯地图导航或使用内置路线展示。为了让移动端体验更像真实工具我把按钮文案设计得很直接“最近的”“商场里”“地铁站”“步行 10 分钟”“重新定位”“去这里”在急用场景里文案越短越好。用户不是来研究系统的而是要快速做决定。三、整体技术架构项目采用前后端分离结构。前端负责移动端交互和地图展示后端负责封装腾讯位置服务 WebService API避免在浏览器中暴露服务端 Key。后端接口主要设计为三个接口作用POST /api/toilet/nearby根据当前位置搜索附近厕所POST /api/toilet/route计算当前位置到某个厕所的步行路线GET /api/config返回前端地图需要的客户端 Key前后端共享的核心类型如下exporttypeCoordinate{lat:number;lng:number;};exporttypeToiletPoi{id:string;title:string;address:string;category:string;distance:number;location:Coordinate;sourceKeyword:string;score:number;tags:string[];};exporttypeToiletSearchResult{type:toilet_nearby;mode:tencent|mock;center:Coordinate;currentAddress:string;radius:number;recommendedId:string;pois:ToiletPoi[];notices:string[];};这里没有把结果直接做成字符串而是返回结构化数据。这样前端可以很自然地渲染地图点位、列表卡片、筛选标签和推荐状态。四、第一步定位之后先做逆地址解析移动端拿到的定位结果通常只是经纬度例如{lat:34.2467,lng:108.9138}但对用户来说“108.9138, 34.2467”没有意义。用户真正想看到的是“你在西北工业大学友谊校区附近”或者“你在友谊西路附近”。所以定位成功后我会先调用腾讯位置服务逆地址解析接口constTENCENT_API;asyncfunctionrequestTencentT(path:string,params:Recordstring,string|number):PromiseT{constkeyprocess.env.TENCENT_MAP_KEY;if(!key){thrownewError(TENCENT_MAP_KEY is required);}consturlnewURL(${TENCENT_API}${path});Object.entries({...params,key}).forEach(([name,value]){url.searchParams.set(name,String(value));});constresponseawaitfetch(url);constdataawaitresponse.json();if(!response.ok||data.status!0){thrownewError(data.message||Tencent location service request failed);}returndataasT;}exportasyncfunctionreverseGeocoder(location:Coordinate){constdataawaitrequestTencentany(/ws/geocoder/v1/,{location:${location.lat},${location.lng},get_poi:1});return{address:data.result.address,formattedAddresses:data.result.formatted_addresses,nearbyPois:data.result.pois||[]};}这一层主要解决两个问题给用户一个确定感应用知道我现在大概在哪给搜索一个上下文后续可以根据当前位置周边环境做更合理的推荐。五、第二步不是只搜“厕所”而是组合关键词搜索真实开发时我发现如果只搜索一个关键词“厕所”结果并不总是稳定。不同城市、不同商圈、不同数据来源里相关 POI 可能叫公共厕所公厕卫生间洗手间商场卫生间地铁站卫生间公共服务设施所以厕急达不会只查一个关键词而是组合搜索。consttoiletKeywords[公共厕所,公厕,卫生间,洗手间,商场,地铁站];exportasyncfunctionsearchNearbyToilets(center:Coordinate,radius1500):PromiseToiletPoi[]{consttaskstoiletKeywords.map((keyword)searchNearbyPois(center,keyword,radius).then((items)items.map((item)({...item,sourceKeyword:keyword}))));constsettledawaitPromise.allSettled(tasks);constpoissettled.flatMap((item)item.statusfulfilled?item.value:[]);returndedupePois(pois).map(enhanceToiletPoi);}底层地点搜索封装如下asyncfunctionsearchNearbyPois(center:Coordinate,keyword:string,radius:number){constboundarynearby(${center.lat},${center.lng},${radius});constdataawaitrequestTencentany(/ws/place/v1/search,{keyword,boundary,page_size:20});return(data.data||[]).map((item:any)({id:item.id||${item.title}-${item.address},title:item.title,address:item.address||,category:item.category||,distance:Number(item._distance||0),location:{lat:item.location.lat,lng:item.location.lng}}));}这里我使用了Promise.allSettled原因很简单找厕所是一个救急场景某个关键词失败不应该导致整个页面失败。只要有一部分结果可用就应该先展示出来。六、第三步给厕所排序而不是让用户自己猜搜索出来的 POI 不能直接全部展示给用户。因为用户不是来做数据筛选的用户要的是“哪个最值得去”。我给排序模型设计了四个维度指标说明距离得分越近越好但不是唯一标准类型得分公共厕所、商场、地铁站、医院等更容易作为目标可达得分步行 10 分钟内优先可信得分名称和分类里明确出现“厕所 / 卫生间 / 公厕”的优先简化后的评分代码如下functionscoreToiletPoi(poi:ToiletPoi){constdistanceScorecalcDistanceScore(poi.distance);consttypeScorecalcTypeScore(poi);constreachableScorepoi.distance800?100:poi.distance1200?75:45;constconfidenceScorecalcConfidenceScore(poi);returnMath.round(distanceScore*0.36typeScore*0.28reachableScore*0.2confidenceScore*0.16);}functioncalcDistanceScore(distance:number){if(distance200)return100;if(distance500)return88;if(distance800)return72;if(distance1200)return55;return35;}functioncalcTypeScore(poi:ToiletPoi){consttext${poi.title}${poi.category}${poi.sourceKeyword};if(/公共厕所|公厕|卫生间|洗手间/.test(text))return100;if(/商场|购物中心|医院|地铁站/.test(text))return78;if(/公园|景区|广场|车站/.test(text))return68;return45;}functioncalcConfidenceScore(poi:ToiletPoi){consttext${poi.title}${poi.address}${poi.category};return/厕所|公厕|卫生间|洗手间/.test(text)?100:60;}这里有一个产品取舍最近的不一定永远排第一。比如 180 米外有一个名称模糊的小 POI600 米外有一个明确标注为“公共厕所”的点位那么后者可能更值得推荐。移动端找厕所不仅要近还要减少用户走到附近却找不到入口的概率。七、第四步用 AI 解析用户的口语化偏好基础模式下用户打开应用就能看到附近厕所。但如果用户输入一句话比如我在西北工业大学附近想找一个步行十分钟内、最好在商场或者地铁站里的卫生间。系统就需要把这句话拆成结构化偏好。exporttypeToiletIntent{originText?:string;radius:number;maxWalkMinutes?:number;preferIndoor:boolean;preferTransit:boolean;preferPublicToilet:boolean;needAccessible:boolean;rawText:string;};exportfunctionparseToiletIntent(text:string):ToiletIntent{return{originText:extractOrigin(text),radius:/附近|周边/.test(text)?1500:1000,maxWalkMinutes:/十分钟|10分钟/.test(text)?10:undefined,preferIndoor:/商场|购物中心|室内|干净/.test(text),preferTransit:/地铁|车站|公交/.test(text),preferPublicToilet:/公厕|公共厕所/.test(text),needAccessible:/无障碍|老人|轮椅/.test(text),rawText:text};}这一版 Demo 可以先用规则解析保证稳定后续再替换成大模型 Tool Calling。真正重要的是工程分层AI 只负责理解用户偏好腾讯位置服务负责提供真实位置数据排序模块负责做可复现的推荐。我不希望模型直接编造“某某地方有卫生间”。在位置类应用里所有推荐都应该落在真实 POI 和真实路线数据上。八、第五步点击结果后规划步行路线用户在底部卡片里选中一个厕所后应用会调用步行路线规划接口计算从当前位置到目标点的路线。exportasyncfunctionplanWalkingRoute(from:Coordinate,to:Coordinate){constdataawaitrequestTencentany(/ws/direction/v1/walking/,{from:${from.lat},${from.lng},to:${to.lat},${to.lng}});constroutedata.result.routes[0];return{distance:Number(route.distance||0),duration:Number(route.duration||0)*60,polyline:decodeTencentPolyline(route.polyline),steps:Array.isArray(route.steps)?route.steps.map((step:any)step.instruction||步行):[]};}前端拿到路线后做两件事地图上高亮从当前位置到目标厕所的步行路线底部卡片显示“约 6 分钟 / 420 米 / 去这里”。移动端页面里我没有把路线步骤全部摊开。因为找厕所时用户首先需要确认方向和距离而不是阅读一长串文字。详细步骤可以折叠在“路线详情”里需要时再展开。九、移动端地图交互重点不是炫技是别挡路移动端地图有一个常见问题地图、列表、按钮很容易互相抢空间。厕急达的交互布局采用“地图在上卡片在下”的结构-------------------------- | 顶部定位条 | -------------------------- | | | 腾讯地图 | | 当前位置 / 厕所点位 | | | -------------------------- | 推荐厕所卡片 | | 距离 / 类型 / 去这里 | --------------------------地图点位使用不同样式区分点位样式当前位置蓝色定位点推荐厕所高亮标记其他候选普通标记已选路线蓝色粗线前端渲染路线的代码类似functionrenderWalkingRoute(map:TMap.Map,route:WalkingRoute){returnnewTMap.MultiPolyline({map,styles:{walking:newTMap.PolylineStyle({color:#1769e0,width:8,borderWidth:2,borderColor:#ffffff})},geometries:[{id:selected-walking-route,styleId:walking,paths:route.polyline.map((point)newTMap.LatLng(point.lat,point.lng))}]});}移动端还有一个细节底部卡片不能太高。如果卡片占据半屏用户看不到路线如果信息太少用户又不敢点。所以我只放四类关键信息名称比如“西安大悦城卫生间”距离比如“420 米”预计步行比如“约 6 分钟”推荐原因比如“商场内名称匹配卫生间步行距离较近”十、接口回退与异常处理找厕所这种应用异常处理比普通 Demo 更重要。因为用户打开应用时可能真的很急如果页面只是显示一个技术错误会非常糟糕。我处理了几类常见异常异常处理方式用户拒绝定位提供手动输入当前位置入口定位失败使用上一次定位或提示重新定位腾讯接口限流展示缓存结果或模拟数据并提示稍后重试附近无明确厕所扩大搜索半径补充商场、地铁站、医院等公共设施步行路线失败仍展示点位和直线距离允许跳转外部地图后端返回结构里保留notices字段return{type:toilet_nearby,mode:tencent,center,currentAddress,radius,recommendedId,pois,notices:[结果基于腾讯位置服务地点搜索返回。,部分商场、地铁站内卫生间可能需要进入建筑后按现场指引查找。]};这个提示很有必要。因为 POI 能告诉我们“附近有相关设施”但建筑内部具体入口、开放状态、维护状态仍然可能变化。位置服务应用不能把数据能力说成现实保证。十一、一次真实测试流程我用下面这个场景做测试当前位置西安市西北工业大学友谊校区附近 需求找一个步行十分钟内、最好在商场或者地铁站附近的卫生间系统执行流程如下获取当前位置坐标调用逆地址解析得到当前位置描述以当前位置为中心搜索“公共厕所 / 公厕 / 卫生间 / 洗手间 / 商场 / 地铁站”对结果去重根据距离、类型、关键词匹配和可达性打分推荐排名最高的 3 个结果用户选择后调用步行路线规划并在地图上绘制路线。结果卡片示例推荐类型距离步行时间推荐理由附近公共厕所公共厕所约 350 米约 5 分钟名称明确距离较近商场卫生间商场设施约 620 米约 9 分钟室内场所更容易按指引寻找地铁站卫生间交通设施约 760 米约 11 分钟适合继续换乘或出行这个结果比“附近厕所列表”更进一步它不仅告诉用户哪里有厕所还告诉用户为什么推荐这个点以及从当前位置走过去大概要多久。十二、开发过程中踩过的坑12.1 “厕所”关键词太窄一开始我只搜索“厕所”结果发现有些地方的数据名称是“公共厕所”有些是“卫生间”还有些商场不会直接把卫生间作为独立 POI 返回。后来我改成组合关键词并把商场、地铁站、医院、公园这类公共设施作为补充候选。这样做之后结果覆盖明显更稳定。12.2 最近不一定最好找厕所很容易直觉上选择最近点但真实使用不一定如此。一个名称模糊、入口不清晰的小点位可能不如稍远一点但更明确的公共厕所或商场卫生间。所以排序时我没有只按距离而是加入类型和可信度权重。12.3 移动端底部卡片不能堆信息PC 页面可以放很多评分、表格和解释但手机页面不行。尤其找厕所这种急用场景用户不需要读长文。最后我把底部卡片压缩成“名称、距离、时间、推荐理由、按钮”五个元素其他信息都折叠起来。12.4 POI 数据不等于实时开放状态厕所是否开放、是否维修、是否在商场内部、是否需要进入闸机这些信息不是每一次地点搜索都能完全覆盖。所以页面文案必须克制只说“附近检索到相关设施”不承诺“一定可用”。这和做路线安全类应用一样真实世界的复杂性不能被一个分数掩盖。十三、项目创新点13.1 把高频小需求做成完整位置服务闭环找厕所不是宏大的功能但它非常高频也非常依赖位置服务。这个项目把定位、逆地址解析、地点搜索、路线规划和地图展示串成完整闭环比单独展示某个 API 更能体现腾讯位置服务的实际应用价值。13.2 从“附近搜索”升级为“可行动推荐”普通附近搜索只是给用户一个列表。厕急达进一步做了排序、标签、推荐理由和步行路线让结果从“你自己看”变成“现在可以去这里”。13.3 移动端优先而不是桌面页面缩小应用从一开始就按手机场景设计打开即定位、卡片少信息、一键导航、地图和列表协同。它不是把 PC 页面响应式压缩而是围绕用户当下动作重新组织界面。13.4 AI 只做偏好解析不编造位置事实用户可以用自然语言说“想找干净点、商场里的、十分钟内的卫生间”。AI 负责把这句话变成筛选条件真实地点仍然由腾讯位置服务返回。这样既有自然语言交互的便利也避免模型凭空生成不存在的地点。十四、后续优化方向这个 Demo 还可以继续做很多增强增加无障碍厕所、母婴室、第三卫生间等标签识别接入用户反馈让用户标记“已找到 / 未找到 / 正在维修”支持离线缓存常用商圈、公园、景区的公共厕所点位增加“带老人 / 带小孩 / 景区模式”等场景偏好支持跳转腾讯地图 App 继续导航接入大模型 Tool Calling让口语化需求解析更自然在小程序端增加“附近 500 米一键找厕所”快捷入口。结语做完厕急达这个 Demo 后我更明显地感受到好的位置服务应用不一定要很复杂。很多时候一个足够具体的小场景只要把用户当下的真实问题解决好就能做出很强的实用感。腾讯位置服务在这个项目里承担了非常核心的底座作用定位让应用知道用户在哪里逆地址解析让坐标变得可读地点搜索找到附近可用设施步行路线规划把目标变成可到达路径地图展示则让用户在手机上快速确认方向。从“附近有什么”到“现在应该去哪里”这是厕急达想完成的一步。如果你也在做移动端位置服务应用不妨从身边这些看似很小、但真实存在的需求开始。它们往往比一个大而全的功能更容易做出用户价值。