作者: 一键难忘 发布时间: 最新推荐文章于 2026-05-18 20:18:26 发布
来源: https://blog.csdn.net/weixin_52908342/article/details/160835489
项目名称:SafeWalk AI 安心夜行项目类型:AI Agent 式工具编排 + 腾讯位置服务 + 夜间步行路线评估项目源码开源地址:https://gitcode.com/weixin_52908342/SafeWalkAI.git说明:本文 Demo 已在本地通过真实腾讯位置服务接口验证,请在env中替换自己腾讯位置服务账号的key。
📹 嵌入式视频: https://live.csdn.net/v/embed/524893
用腾讯位置服务做一个会解释路线的 AI 步行助手
日常使用地图时,我们很习惯接受“最快路线”或“最短路线”。但在一些具体场景里,用户真正关心的并不只是时间和距离。比如夜间步行,从公司到地铁站、从商场到住处、从园区到公交站,路线短一点固然重要,但很多人也会自然地考虑:这条路是不是太偏?沿途有没有便利店、商场、公交站、地铁口这类公共节点?如果多走几分钟,能不能换来一条更容易描述、更容易确认、更有参照物的路线?
围绕这个真实需求,我做了一个小型 Demo:SafeWalk AI 安心夜行。它不是一个“安全保证系统”,也不会对现实安全做绝对判断,而是把腾讯位置服务提供的地理编码、地点搜索、步行路线规划和地图可视化能力组织起来,再用 Agent 式任务拆解思路,把用户的自然语言需求转化为可执行的路线分析流程。

用户输入类似“晚上十点从深圳湾科技生态园走到后海地铁站,想走人多一点,有便利店经过,不想穿太偏的小路”这样的需求后,系统会依次完成:起终点解析、步行路线生成、沿线 POI 检索、候选路线构造、路线评分、地图渲染和解释输出。最终页面不只给出一条路线,还会展示候选路线对比、沿途公共锚点、评分拆解和推荐理由。
本文将完整记录这个项目的设计背景、系统架构、腾讯位置服务接入方式、核心代码实现、运行效果和开发过程中遇到的坑。
最开始想到这个项目,是因为我发现很多地图类 Demo 都很容易停留在“展示点位”“展示路线”“搜索周边”这几个常规功能上。这些能力当然重要,但如果只是把接口调通,文章和作品的辨识度会比较弱。
我希望找一个更贴近真实生活的小场景:它不必很大,但必须有明确痛点,也能真正体现 AI 和地图服务结合后的价值。
夜间步行正好符合这个条件。
晚上从写字楼出来,地图可能给你一条最快路线,但最快路线不一定是用户当下最想走的路线。有些小路白天没问题,晚上却会让人犹豫;有些路线多走几百米,但沿途有商场、地铁口、便利店、公交站,心理上会更容易接受。对用户来说,路线选择不只是一个几何问题,也是一个带有场景偏好的决策问题。

传统地图服务擅长提供真实、准确的空间数据;AI 擅长理解自然语言、组织解释和做多条件拆解。这个项目的核心思路就是把两者分工做好:
这里我刻意避免让 AI 直接“凭感觉”说哪条路安全。SafeWalk AI 输出的是“更符合夜间偏好的路线”,不是绝对安全结论。这个边界很重要,因为位置服务越接近真实生活,产品表达越要克制。
项目目录为:
safewalk-ai/
src/
client/ # React 前端
server/ # Express 后端
shared/ # 前后端共享类型
.env.example # Key 配置示例
package.json
README.md

本地运行方式:
cd safewalk-ai
npm install
npm run dev
启动后访问:
http://localhost:5173
后端服务地址:
http://localhost:8787
腾讯位置服务 Key 配置如下:
TENCENT_MAP_KEY=你的 WebService API Key
TENCENT_MAP_CLIENT_KEY=你的 JavaScript API GL Key
DEFAULT_CITY=深圳
PORT=8787
其中:

项目支持两种运行模式:
这种设计是为了保证参赛作品可演示、可截图、可复现。即使临时遇到 Key 配额、网络波动或接口失败,页面也不会直接崩掉。
SafeWalk AI 采用“前端交互 + 后端地图工具层 + 路线评估层”的结构。

前后端共享的数据结构定义在src/shared/types.ts。这里的核心返回结构是SafeWalkResult:
export type SafeWalkResult = {
type: "safe_walk_route";
mode: "mock" | "tencent";
summary: string;
recommendedRouteId: string;
intent: RouteIntent;
origin: {
title: string;
address: string;
location: Coordinate;
};
destination: {
title: string;
address: string;
location: Coordinate;
};
routes: SafeRoute[];
anchors: RouteAnchor[];
decisionTrace: string[];
notices: string[];
};
这个结构有几个好处:

本项目主要用到腾讯位置服务四类能力。
后端统一封装腾讯位置服务接口,避免把 WebService Key 暴露在浏览器中。核心请求函数如下:
const TENCENT_API = "https://apis.map.qq.com";
async function requestTencent<T>(
path: string,
params: Record<string, string | number>
): Promise<T> {
const key = process.env.TENCENT_MAP_KEY;
if (!key) {
throw new Error("TENCENT_MAP_KEY is not configured");
}
const url = new URL(`${TENCENT_API}${path}`);
Object.entries({ ...params, key }).forEach(([name, value]) => {
url.searchParams.set(name, String(value));
});
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Tencent map request failed with ${response.status}`);
}
const data = await response.json();
if (data.status !== 0) {
throw new Error(data.message || `Tencent map api status ${data.status}`);
}
return data as T;
}

开发时我遇到一个很典型的问题:“深圳湾科技生态园”可以通过地理编码解析,但“后海地铁站”这类 POI 名称直接走地理编码时可能返回参数错误。解决方式是:先尝试地理编码,失败后切换到指定城市范围内的地点搜索。
export async function resolvePlace(keyword: string) {
try {
return await geocode(keyword);
} catch (error) {
const city = process.env.DEFAULT_CITY || "深圳";
const pois = await searchRegionPois(keyword, city, 1);
const first = pois[0];
if (!first) {
throw error;
}
return {
title: first.title,
address: first.address,
location: first.location
};
}
}
这个修正让系统对真实用户输入更稳。用户不会区分“地址”和“POI 名称”,但系统必须能兼容。
步行路线规划用于生成基础省时路线,也用于生成带公共锚点的候选路线。
export async function planWalkingRoute(
from: Coordinate,
to: Coordinate
): Promise<WalkingRoute> {
const data = await requestTencent<any>("/ws/direction/v1/walking/", {
from: `${from.lat},${from.lng}`,
to: `${to.lat},${to.lng}`
});
const route = data.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 || step.road_name || "步行")
: []
};
}
这里有一个细节:腾讯路线接口返回的耗时单位需要在项目内部统一处理。我在内部统一使用秒,前端显示时再转成分钟,避免真实接口和模拟数据单位不一致。

为了判断一条路线是否更符合“夜间偏好”,项目会沿路线抽样,搜索附近的公共锚点。
const anchorKeywords = ["便利店", "地铁站", "公交站", "商场", "医院", "派出所"];
async function collectRouteAnchors(
route: WalkingRoute,
routeId: string
): Promise<RouteAnchor[]> {
const samples = sampleRoutePoints(route.polyline).slice(0, 5);
const anchors: RouteAnchor[] = [];
for (const sample of samples) {
const settled = await Promise.allSettled(
anchorKeywords.map((keyword) => searchNearbyPois(sample, keyword, 320))
);
for (const result of settled) {
if (result.status === "fulfilled") {
anchors.push(...result.value.map((anchor) => ({ ...anchor, routeId })));
}
}
}
return dedupeAnchors(anchors).slice(0, 36);
}
这里使用Promise.allSettled,是因为 POI 检索不应该因为某一个关键词失败就中断整个路线分析。真实项目里,外部接口要尽量做局部容错。

后端入口在src/server/index.ts:
app.post("/api/plan-route", async (request, response) => {
const parsed = planSchema.safeParse(request.body);
if (!parsed.success) {
response.status(400).json({ error: "请输入 1-500 字的路线需求" });
return;
}
const intent = parseIntent(parsed.data);
const result = await buildSafeWalkPlan(intent);
response.json(result);
});

为了保证 Demo 可复现,这一版没有把大模型 Key 作为必需配置,而是先用轻量规则实现一个稳定的意图解析器。代码结构按 Agent Tool Calling 的方式组织,后续如果接入大模型,只需要替换intent-parser,腾讯位置服务工具层和评分层都不需要重写。
export function parseIntent(input: PlanRouteRequest): RouteIntent {
const rawText =
input.query.trim() ||
"晚上十点从深圳湾科技生态园走到后海地铁站,想走人多一点,有便利店经过,不想穿太偏的小路。";
const extracted = extractOriginDestination(rawText);
const originText = cleanPlace(
input.origin || extracted.origin || "深圳湾科技生态园"
);
const destinationText = cleanPlace(
input.destination || extracted.destination || "后海地铁站"
);
return {
originText,
destinationText,
travelTime: inferTravelTime(rawText),
preferences: inferPreferences(rawText),
rawText
};
}
例如输入:
晚上十点从深圳湾科技生态园走到后海地铁站,想走人多一点,有便利店经过,不想穿太偏的小路。
解析结果会包含:
{
"originText": "深圳湾科技生态园",
"destinationText": "后海地铁站",
"travelTime": "night",
"preferences": {
"preferPublicPois": true,
"preferMainRoad": true,
"avoidDarkAlleys": true,
"needConvenienceStore": true,
"needTransitBackup": true
}
}
晚上十点从西安市西北工业大学走到大雁塔地铁站,想走人多一点,有便利店经过,不想穿太偏的小路。


核心规划函数是buildSafeWalkPlan。它先解析起终点,再调用步行路线接口生成省时路线,随后沿线搜索公共锚点,并用高价值锚点构造新的候选路线。
export async function buildSafeWalkPlan(intent: RouteIntent) {
if (!hasTencentKey()) {
return buildMockResult(intent);
}
try {
const origin = await resolvePlace(intent.originText);
const destination = await resolvePlace(intent.destinationText);
const fastest = await planWalkingRoute(origin.location, destination.location);
const fastestAnchors = await collectRouteAnchors(fastest, "fastest");
const viaAnchors = chooseViaAnchors(fastestAnchors);
const candidates: Candidate[] = [
{
id: "fastest",
title: "省时路线",
strategy: "fastest",
route: fastest,
anchors: fastestAnchors
}
];
if (viaAnchors.balanced) {
const route = await planRouteVia(
origin.location,
viaAnchors.balanced.location,
destination.location
);
candidates.push({
id: "balanced",
title: "公共节点均衡线",
strategy: "balanced",
route,
anchors: await collectRouteAnchors(route, "balanced")
});
}
const routes = evaluateCandidates(candidates, intent);
const recommended = routes.reduce(
(best, route) => (route.score > best.score ? route : best),
routes[0]
);
return {
type: "safe_walk_route",
mode: "tencent",
summary: `已基于腾讯位置服务生成 ${routes.length} 条候选路线,推荐「${recommended.title}」。`,
recommendedRouteId: recommended.id,
intent,
origin,
destination,
routes,
anchors: candidates.flatMap((candidate) => candidate.anchors),
decisionTrace: [
"地理编码与地点搜索解析起终点",
"调用腾讯位置服务步行路线规划",
"沿路线抽样调用地点搜索",
"构造候选路线并完成评分排序"
],
notices: [
"当前为真实腾讯位置服务接口结果。",
"路线评分只基于公开 POI 与路径数据做辅助比较,不代表现实安全保证。"
]
};
} catch (error) {
const message = error instanceof Error ? error.message : "未知错误";
return buildMockResult(intent, message);
}
}

评分模型没有使用“安全分”这个说法,而是使用路线偏好匹配的综合分。当前包含四个维度:
核心代码如下:
export function evaluateCandidates(
candidates: RouteCandidate[],
_intent: RouteIntent
): SafeRoute[] {
const fastest = candidates[0].route;
return candidates
.map((candidate) => {
const scoreDetail = evaluateRoute(candidate, fastest);
const score = Math.round(
scoreDetail.anchorScore * 0.38 +
scoreDetail.detourScore * 0.24 +
scoreDetail.preferenceScore * 0.28 +
scoreDetail.explainScore * 0.1
);
return {
id: candidate.id,
title: candidate.title,
strategy: candidate.strategy,
distance: candidate.route.distance,
duration: candidate.route.duration,
distanceText: formatDistance(candidate.route.distance),
durationText: formatDuration(candidate.route.duration),
score,
scoreDetail,
reason: buildReason(candidate, scoreDetail),
polyline: candidate.route.polyline
};
})
.sort((a, b) => b.score - a.score);
}
评分说明文本也基于真实 POI 类型生成:
function buildReason(candidate: RouteCandidate, score: RouteScoreDetail) {
const labels = [...new Set(candidate.anchors.map((anchor) => anchor.matchedKeyword))]
.slice(0, 4);
const anchorText = labels.length
? `沿线可参考 ${labels.join("、")} 等公共锚点`
: "沿线公共锚点较少";
const detourText = score.detourScore >= 80
? "绕行成本较低"
: "需要接受一定绕行成本";
return `${anchorText},${detourText},适合夜间希望路线更容易描述和确认的步行场景。`;
}
这样解释不会变成空泛的 AI 文案,而是能对应到地图上的真实锚点。

前端使用 React + TypeScript,核心页面分为三块:
前端提交路线需求:
async function submit() {
setLoading(true);
setError("");
try {
const response = await fetch("/api/plan-route", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query })
});
const data = (await response.json()) as SafeWalkResult;
setResult(data);
setSelectedRouteId(data.recommendedRouteId);
} finally {
setLoading(false);
}
}
腾讯地图脚本按需加载:
export function loadTencentMap(clientKey: string) {
if (window.TMap) return Promise.resolve(window.TMap);
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = `https://map.qq.com/api/gljs?v=1.exp&key=${encodeURIComponent(clientKey)}`;
script.async = true;
script.onload = () => resolve(window.TMap);
script.onerror = () => reject(new Error("腾讯地图脚本加载失败"));
document.head.appendChild(script);
});
}
路线渲染使用MultiPolyline,点位渲染使用MultiMarker:
const polylines = new TMap.MultiPolyline({
map,
styles: {
selected: new TMap.PolylineStyle({
color: "#1769e0",
width: 8,
borderWidth: 2,
borderColor: "#ffffff"
}),
candidate: new TMap.PolylineStyle({
color: "rgba(26, 112, 215, 0.35)",
width: 5
})
},
geometries: result.routes.map((route) => ({
id: route.id,
styleId: route.id === selectedRouteId ? "selected" : "candidate",
paths: route.polyline.map((point) => new TMap.LatLng(point.lat, point.lng))
}))
});

路线评分面板展示四个维度:
<MetricBar label="公共锚点" value={selected.scoreDetail.anchorScore} />
<MetricBar label="绕行成本" value={selected.scoreDetail.detourScore} />
<MetricBar label="偏好匹配" value={selected.scoreDetail.preferenceScore} />
<MetricBar label="解释完整度" value={selected.scoreDetail.explainScore} />

本地真实接口测试输入:
晚上十点从深圳湾科技生态园走到后海地铁站,想走人多一点,有便利店经过,不想穿太偏的小路。
后端返回摘要:
{
"mode": "tencent",
"recommendedRouteId": "fastest",
"summary": "已基于腾讯位置服务生成 3 条候选路线,推荐「省时路线」。"
}
一次真实测试中,系统生成了 3 条候选路线,并检索到多类公共锚点:
可以看到,系统并没有简单地永远选择“公共节点最多”的路线,而是在“公共锚点”和“绕行成本”之间做了平衡。在这次测试里,省时路线本身已经检索到足够多的公共锚点,因此它综合得分最高。
这也符合产品设定:SafeWalk AI 不追求强行绕路,而是在用户偏好和真实步行成本之间做解释性推荐。
一开始我把 Key 写进.env.example后发现后端仍然识别不到。原因很简单:项目默认读取的是.env,而.env.example只是示例文件。解决方式是复制一份:
copy .env.example .env
正式提交仓库时,.env不应该上传,避免泄露 Key。
“后海地铁站”这类输入更像 POI 名称,不是标准地址。直接调用地理编码可能返回参数错误。因此我加了resolvePlace:先地理编码,失败后走城市范围地点搜索。
这个问题很典型。真实用户不会按照接口类型组织语言,系统要替用户做兼容。
真实腾讯路线接口返回的耗时单位和项目内部展示单位需要统一。最初我按秒处理,导致页面显示异常。后来统一在接口封装层转换为秒,前端再格式化为“分钟”。
这个坑提醒我:接入地图服务时,不要只看字段名,还要仔细确认单位、坐标顺序和返回结构。
后端最初用了:
app.get("*", handler);
在 Express 5 中,这个写法会被path-to-regexp报错。最终改成:
app.get(/.*/, handler);
这是一个小问题,但如果不处理,项目会在启动阶段直接崩掉。
“安心夜行”这个题目天然容易让人联想到安全判断。但项目不能说“这条路一定安全”,也不能用 POI 数量替代现实环境判断。所以我在页面和返回结构里都保留了提示:
路线评分只基于公开 POI 与路径数据做辅助比较,不代表现实安全保证。
我认为这类边界提示不是削弱产品,而是让产品更可信。
传统导航更多关注距离和耗时。SafeWalk AI 在此基础上加入了公共锚点、绕行成本、偏好匹配和解释文本,让路线推荐更接近真实夜间步行决策。
地点搜索通常用于“找一个地方”。在这个项目里,地点搜索进一步参与路线评分:便利店、地铁站、公交站、商场等 POI 不只是搜索结果,而是解释路线推荐的空间证据。
当前 Demo 为了可复现,使用规则解析器完成自然语言意图拆解。但整个工程已经按 Agent Tool Calling 思路拆分:意图解析、地图工具、路线评分、结果解释彼此独立。后续接入大模型时,只需要替换意图解析和解释生成层,不需要重写地图服务能力。
参赛 Demo 经常会遇到 Key 配额、网络或接口参数问题。如果没有回退机制,演示体验会很不稳定。SafeWalk AI 在真实接口失败时会自动返回模拟数据,并把原因写入notices。这让项目更容易展示,也更像一个真正可用的工程原型。
夜间步行只是一个入口。这套评分框架可以迁移到更多场景:
从这个角度看,腾讯位置服务提供的不只是一组 API,而是一套可以被 AI 编排的城市空间能力。

SafeWalk AI 目前已经跑通了从自然语言输入到腾讯地图展示的完整链路,但它仍然是一个原型。后续我计划继续优化:
这次做 SafeWalk AI,我最大的感受是:AI + 地图的价值,不一定要从一个很宏大的系统开始。一个清晰的小场景,只要把用户语言、真实空间数据、路线规划和解释输出串成闭环,就能让地图从“告诉你怎么走”进一步变成“解释为什么这样走”。

腾讯位置服务在这个项目里承担了非常扎实的底座角色:地理编码把文本变成坐标,地点搜索把城市公共节点找出来,步行路线规划把点连成真实可走的路径,JavaScript API GL 再把结果呈现在用户面前。AI Agent 式编排则把这些能力组织成一个完整流程。

从“最快的一条路”到“更适合当下的一条路”,这就是 SafeWalk AI 想完成的一小步。
如果你也对 AI + 地图应用感兴趣,欢迎点赞、收藏、评论交流。后续我也会继续完善这个 Demo,把它从参赛作品推进成一个更完整的城市出行小工具。
在智慧城市物流调度零售选址等场景中,高清稳定的地图底图是基础能力无论是开发管理后台的可视化界面,还是
在物流运输外勤人员管理共享设备追踪等场景中,实时轨迹记录与查询是企业运营的核心需求。然而,自行搭建轨
在智慧出行本地生活服务零售选址等场景中,精准的地图交互能力如一键跳转导航POI详情页调起已成为用户体