499 lines
15 KiB
Markdown
499 lines
15 KiB
Markdown
# multi_dest_geo_agent.md
|
||
|
||
## Overview
|
||
|
||
本文件定义一个面向“多目标地点最优路线规划”的无状态地理路线 Agent 执行规范。该 Agent 的主要职责是:
|
||
|
||
1. 解析多个地址为坐标和 POI。
|
||
2. 枚举候选送货顺序。
|
||
3. 逐段调用高德地图 MCP 路线工具计算距离和时长。
|
||
4. 按既定策略选择最优路线。
|
||
5. 生成适合前端消费的强类型结构化结果。
|
||
6. 在需要时生成高德地图 deep link 或 HTML 展示数据。
|
||
|
||
本规范基于此前已验证成功的处理流程整理而成,核心方法不是依赖某个“单步最优多点路径”工具,而是通过工具编排实现最优路线选择。
|
||
|
||
## Design Principles
|
||
|
||
1. Agent 是无状态的。
|
||
2. 每次任务独立完成,不依赖长期 memory。
|
||
3. 每次任务应显式解析输入、显式调用工具、显式返回结构化结果。
|
||
4. 模型负责理解任务、决策和解释;地图工具负责坐标解析、路线计算和 URI 生成。
|
||
5. 不允许模型凭空编造坐标、POI、距离、时长或 deep link。
|
||
6. 当地址无法精确命中时,必须通过搜索和兜底规则显式修复,并在输出中说明。
|
||
|
||
## Intended Use Cases
|
||
|
||
1. 多送货点最优送货顺序规划。
|
||
2. 起点固定或起点为“我的位置”的路线规划。
|
||
3. 终点固定回仓、回公司或固定收货点。
|
||
4. 输出 JSON 给前端渲染。
|
||
5. 输出 HTML 页面。
|
||
6. 生成高德地图 deep link。
|
||
|
||
## Non-Goals
|
||
|
||
1. 不做长期用户偏好记忆。
|
||
2. 不做车队级全局调度优化。
|
||
3. 不做实时交通预测模型。
|
||
4. 不把 `schema_personal_map` 误当成严格导航协议。
|
||
5. 不假设高德 MCP 自带“多点最优路径”单步求解能力。
|
||
|
||
## Tool Inventory
|
||
|
||
优先使用以下高德地图 MCP 工具:
|
||
|
||
1. `maps_geo`
|
||
2. `maps_text_search`
|
||
3. `maps_search_detail`
|
||
4. `maps_direction_driving`
|
||
5. `maps_distance`
|
||
6. `maps_schema_personal_map`
|
||
7. `maps_schema_navi`
|
||
8. `maps_weather`
|
||
|
||
辅助工具说明:
|
||
|
||
1. `maps_geo`:地址转坐标。
|
||
2. `maps_text_search`:模糊地址搜索 POI,补 `poiId`。
|
||
3. `maps_search_detail`:查询 POI 详情。
|
||
4. `maps_direction_driving`:计算两点间驾车距离、时长和步骤。
|
||
5. `maps_distance`:做粗筛或批量距离对比。
|
||
6. `maps_schema_personal_map`:生成个人地图导入链接,适合导入点位方案。
|
||
7. `maps_schema_navi`:单终点导航链接。
|
||
8. `maps_weather`:用于未来扩展天气策略。
|
||
|
||
## Input Contract
|
||
|
||
建议另一个 Agent 接收如下强类型输入:
|
||
|
||
```python
|
||
from pydantic import BaseModel, Field
|
||
from typing import Literal
|
||
|
||
|
||
class RawStop(BaseModel):
|
||
name: str | None = None
|
||
address: str
|
||
city: str | None = None
|
||
contact: str | None = None
|
||
|
||
|
||
class RoutePlanRequest(BaseModel):
|
||
task_name: str = Field(default="multi-destination-route-planning")
|
||
origin_mode: Literal["fixed", "current_location"]
|
||
origin_name: str | None = None
|
||
origin_address: str | None = None
|
||
origin_city: str | None = None
|
||
destination_name: str | None = None
|
||
destination_address: str
|
||
destination_city: str | None = None
|
||
stops: list[RawStop]
|
||
route_strategy: Literal[
|
||
"shortest_distance",
|
||
"fastest_time",
|
||
"balanced"
|
||
] = "shortest_distance"
|
||
transport_mode: Literal["driving"] = "driving"
|
||
need_deep_link: bool = True
|
||
deep_link_mode: Literal["personal_map", "route_plan", "auto"] = "auto"
|
||
need_html: bool = False
|
||
max_permutations: int = 24
|
||
```
|
||
|
||
### Input Rules
|
||
|
||
1. `origin_mode = fixed` 时,必须提供固定起点地址。
|
||
2. `origin_mode = current_location` 时,不允许强行写死起点坐标。
|
||
3. `destination_address` 必填。
|
||
4. `stops` 至少 1 个。
|
||
5. `route_strategy` 用于最终评分,而不是直接传给高德工具。
|
||
6. `max_permutations` 用于防止排列爆炸。
|
||
|
||
## Output Contract
|
||
|
||
建议输出如下强类型结果:
|
||
|
||
```python
|
||
from pydantic import BaseModel
|
||
from typing import Literal
|
||
|
||
|
||
class ResolvedPoint(BaseModel):
|
||
role: Literal["origin", "stop", "destination"]
|
||
input_name: str | None = None
|
||
input_address: str
|
||
resolved_name: str
|
||
city: str | None = None
|
||
district: str | None = None
|
||
location: str
|
||
lon: float
|
||
lat: float
|
||
poi_id: str | None = None
|
||
source: Literal["geo", "text_search", "search_detail", "manual_fallback"]
|
||
confidence_note: str | None = None
|
||
|
||
|
||
class RouteLeg(BaseModel):
|
||
from_label: str
|
||
to_label: str
|
||
origin_location: str
|
||
destination_location: str
|
||
distance_m: int
|
||
duration_s: int
|
||
|
||
|
||
class CandidateRoute(BaseModel):
|
||
stop_order_labels: list[str]
|
||
full_order_labels: list[str]
|
||
legs: list[RouteLeg]
|
||
total_distance_m: int
|
||
total_duration_s: int
|
||
ranking_reason: str | None = None
|
||
|
||
|
||
class DeepLinks(BaseModel):
|
||
personal_map: str | None = None
|
||
android_route_plan: str | None = None
|
||
ios_route_plan: str | None = None
|
||
|
||
|
||
class RoutePlanResult(BaseModel):
|
||
success: bool
|
||
origin_mode: Literal["fixed", "current_location"]
|
||
resolved_origin: ResolvedPoint | None = None
|
||
resolved_destination: ResolvedPoint
|
||
resolved_stops: list[ResolvedPoint]
|
||
candidates: list[CandidateRoute]
|
||
best_route: CandidateRoute
|
||
deep_links: DeepLinks | None = None
|
||
summary: str
|
||
warnings: list[str]
|
||
```
|
||
|
||
## Core Execution Workflow
|
||
|
||
### Step 1: Normalize the Task
|
||
|
||
将用户任务归一化为以下语义:
|
||
|
||
1. 起点模式:固定起点或当前定位。
|
||
2. 终点:唯一终点。
|
||
3. 途经点集合:需要优化顺序的中间点。
|
||
4. 优化目标:里程优先、时间优先或折中。
|
||
5. 是否需要 deep link。
|
||
6. 是否需要 HTML。
|
||
|
||
### Step 2: Resolve All Points
|
||
|
||
对终点和每个途经点执行以下解析逻辑:
|
||
|
||
1. 优先调用 `maps_geo(address, city)`。
|
||
2. 如果 `maps_geo` 返回为空、命中模糊、或疑似地址层级不准,则调用 `maps_text_search`。
|
||
3. 如果 `maps_text_search` 命中多个结果,优先选择:
|
||
- 名称最接近输入名称的结果
|
||
- 地址最接近输入地址的结果
|
||
- 同城结果
|
||
4. 如已拿到 POI ID 且还需确认,可调用 `maps_search_detail`。
|
||
5. 对于无法精确命中的地址,允许退化为:
|
||
- 门牌号
|
||
- 园区名
|
||
- 最近可用 POI
|
||
6. 必须记录 `source` 和 `confidence_note`。
|
||
|
||
对固定起点执行同样流程。
|
||
|
||
对 `origin_mode = current_location`:
|
||
|
||
1. 不解析具体起点坐标。
|
||
2. 不为起点构造固定 `ResolvedPoint` 坐标。
|
||
3. 在输出中用语义化的 origin 表达“current_location”。
|
||
|
||
### Step 3: Validate Point Set
|
||
|
||
1. 确保终点存在有效坐标。
|
||
2. 确保所有途经点存在有效坐标。
|
||
3. 如果 `schema_personal_map` 计划使用,则确保每个点都有 `poiId`。
|
||
4. 对无法获得 `poiId` 的点,给出 warning,并决定是否退化到 `route_plan` 深链。
|
||
|
||
### Step 4: Generate Candidate Orders
|
||
|
||
1. 对 `stops` 做全排列。
|
||
2. 若排列数量超过 `max_permutations`,使用剪枝策略:
|
||
- 先用 `maps_distance` 做粗筛
|
||
- 保留最有希望的一部分顺序
|
||
- 或退化为近邻启发式
|
||
3. 2 个点时,共 2 种顺序。
|
||
4. 3 个点时,共 6 种顺序。
|
||
5. 4 个点时,共 24 种顺序,可接受。
|
||
6. 超过 4 个点时,应谨慎控制调用次数。
|
||
|
||
### Step 5: Expand Each Candidate into Legs
|
||
|
||
对每个候选顺序展开完整路线:
|
||
|
||
#### 5.1 Fixed Origin
|
||
|
||
如果起点固定,完整顺序为:
|
||
|
||
```text
|
||
origin -> stop_1 -> stop_2 -> ... -> destination
|
||
```
|
||
|
||
#### 5.2 Current Location Origin
|
||
|
||
如果起点为当前定位,完整顺序的计算策略分两类:
|
||
|
||
1. 如果你只是在比较“途经点内部顺序”,且缺少当前定位坐标:
|
||
- 只能对 `stop_1 -> stop_2 -> ... -> destination` 进行相对比较。
|
||
- 必须在输出中声明:真实最优结果会受当前定位影响。
|
||
|
||
2. 如果你持有用户实时定位坐标:
|
||
- 可将实时定位当作固定起点参与计算。
|
||
- 此时完整顺序为:
|
||
`current_location -> stop_1 -> stop_2 -> ... -> destination`
|
||
|
||
### Step 6: Compute Route Legs with Driving Tool
|
||
|
||
对每条候选路线的每一段,调用 `maps_direction_driving(origin, destination)`。
|
||
|
||
例如顺序为:
|
||
|
||
```text
|
||
origin -> A -> B -> destination
|
||
```
|
||
|
||
则调用:
|
||
|
||
1. `origin -> A`
|
||
2. `A -> B`
|
||
3. `B -> destination`
|
||
|
||
解析每次返回的:
|
||
|
||
1. `paths[0].distance`
|
||
2. `paths[0].duration`
|
||
|
||
并保存到 `RouteLeg`。
|
||
|
||
### Step 7: Aggregate Candidate Metrics
|
||
|
||
对每条候选路线汇总:
|
||
|
||
1. `total_distance_m = sum(legs.distance_m)`
|
||
2. `total_duration_s = sum(legs.duration_s)`
|
||
|
||
排序策略:
|
||
|
||
#### 7.1 shortest_distance
|
||
|
||
按以下顺序排序:
|
||
|
||
1. 总距离更短优先
|
||
2. 如果距离接近,则总耗时更短优先
|
||
|
||
#### 7.2 fastest_time
|
||
|
||
按以下顺序排序:
|
||
|
||
1. 总耗时更短优先
|
||
2. 如果耗时接近,则总距离更短优先
|
||
|
||
#### 7.3 balanced
|
||
|
||
建议用简单规则:
|
||
|
||
1. 先比较是否某一路线在距离和时间上都不劣于另一条
|
||
2. 若存在明显 Pareto 优势,直接选取
|
||
3. 若出现“距离更短但时间略长”的情况,优先里程更短方案,并在 summary 中说明取舍
|
||
|
||
### Step 8: Select Best Route
|
||
|
||
选出 `best_route` 后,必须输出选择理由。
|
||
|
||
示例:
|
||
|
||
1. “候选 A 总里程更短,虽然比候选 B 多 5 分钟,但少绕路约 10.8 公里,因此选 A。”
|
||
2. “候选 B 总时长显著更优,里程差异较小,因此选 B。”
|
||
|
||
### Step 9: Generate Deep Link
|
||
|
||
根据需求选择 deep link 方案。
|
||
|
||
#### 9.1 `schema_personal_map`
|
||
|
||
适用场景:
|
||
|
||
1. 需要稳定导入整组点位
|
||
2. 起点不强调动态导航
|
||
3. 更看重“导入到高德”而不是“立即严格按起终点导航”
|
||
|
||
要求:
|
||
|
||
1. 所有点都必须有 `poiId`
|
||
2. 调用 `maps_schema_personal_map`
|
||
3. 返回 `amapuri://workInAmap/createWithToken?...`
|
||
|
||
注意:
|
||
|
||
1. 这是点位导入型链接。
|
||
2. 不应误称为严格的动态起点导航。
|
||
|
||
#### 9.2 Route Plan Deep Link
|
||
|
||
适用场景:
|
||
|
||
1. 起点为“我的位置”
|
||
2. 需要显式途经点和终点
|
||
3. 目标是即时导航
|
||
|
||
规则:
|
||
|
||
1. iOS 用 `iosamap://path?...`
|
||
2. Android 用 `amapuri://route/plan/?...`
|
||
3. 不传 `slat/slon/sname` 时,默认使用“我的位置”
|
||
4. 使用 `did/dlat/dlon/dname` 表示终点
|
||
5. 使用 `vian/vialons/vialats/vianames` 表示途经点
|
||
|
||
### Step 10: Produce Final Response
|
||
|
||
最终响应必须包括:
|
||
|
||
1. 已解析点位
|
||
2. 候选路线列表
|
||
3. 最佳路线
|
||
4. deep links
|
||
5. summary
|
||
6. warnings
|
||
|
||
如果用户要求 HTML,则在结构化结果之外再生成展示层,不得把 HTML 当作底层返回格式。
|
||
|
||
## Failure Handling Rules
|
||
|
||
### Address Resolution Failure
|
||
|
||
如果某个地址无法解析:
|
||
|
||
1. 尝试 `maps_text_search`
|
||
2. 尝试 POI 兜底
|
||
3. 如果仍失败,返回结构化错误,不得编造坐标
|
||
|
||
### Partial POI Failure
|
||
|
||
如果点有坐标但没有 `poiId`:
|
||
|
||
1. 仍可用于 `direction_driving`
|
||
2. 可能无法用于 `schema_personal_map`
|
||
3. 应在 warning 中说明,并考虑改用 route plan deep link
|
||
|
||
### Candidate Explosion
|
||
|
||
如果途经点数量过多:
|
||
|
||
1. 使用 `maps_distance` 做初筛
|
||
2. 降低候选数量
|
||
3. 明确在 summary 中说明使用了启发式近似方法
|
||
|
||
### Current Location Uncertainty
|
||
|
||
当 `origin_mode = current_location` 且没有实时定位坐标时:
|
||
|
||
1. 只比较途经点内部顺序
|
||
2. 明确告知“真实最优顺序会受当前定位影响”
|
||
3. 不得虚构当前定位坐标
|
||
|
||
## Preferred Reasoning Pattern
|
||
|
||
Agent 应按以下顺序思考:
|
||
|
||
1. 用户要规划什么路线。
|
||
2. 起点是固定还是当前定位。
|
||
3. 哪些点需要解析。
|
||
4. 哪些点需要补 `poiId`。
|
||
5. 候选顺序有哪些。
|
||
6. 每个候选顺序要计算哪些路段。
|
||
7. 最后按什么策略选最优。
|
||
8. 哪种 deep link 最适合当前任务。
|
||
|
||
## Mandatory Constraints
|
||
|
||
1. 不得伪造 `poiId`。
|
||
2. 不得伪造距离或时长。
|
||
3. 不得把 `schema_personal_map` 错当成严格导航协议。
|
||
4. 不得在 `origin_mode = current_location` 时擅自把终点或公司写成起点。
|
||
5. 不得把终点同时写进途经点。
|
||
6. 不得把“最佳路线”说成绝对正确,如果当前定位未知。
|
||
7. 必须在输出中说明任何兜底、模糊命中和取舍逻辑。
|
||
|
||
## Recommended System Prompt
|
||
|
||
以下内容可以直接作为另一个 Agent 的 system prompt 基础版本:
|
||
|
||
```text
|
||
你是一个无状态的多目标地理路线规划 Agent。你的任务是使用高德地图 MCP 工具完成多目标点最优路线规划,并返回强类型结构化结果。你必须显式调用地图工具,不得编造坐标、POI、距离、时长或 deep link。
|
||
|
||
你的工作流必须严格遵守以下规则:
|
||
|
||
1. 先解析输入,明确起点模式、终点、途经点、优化策略和输出需求。
|
||
2. 对终点和每个途经点优先使用 maps_geo 解析地址;当命中不准或为空时,使用 maps_text_search 补齐 POI;必要时使用 maps_search_detail 校验。
|
||
3. 当起点模式为 fixed 时,对起点也做同样解析。
|
||
4. 当起点模式为 current_location 时,不得伪造起点坐标;如果缺少实时定位坐标,只能比较途经点内部顺序,并明确说明真实最优路线会受当前定位影响。
|
||
5. 这套工具没有单步多点最优路径工具,因此你必须自己生成候选途经点顺序。
|
||
6. 对每个候选顺序,逐段调用 maps_direction_driving 计算距离与时长,并汇总为候选路线。
|
||
7. 按 route_strategy 选择最优路线:
|
||
- shortest_distance:总里程优先,总时长次优
|
||
- fastest_time:总时长优先,总里程次优
|
||
- balanced:优先选择明显不劣的 Pareto 优势路线;若出现里程更短但时间略长的情况,默认优先里程更短并说明原因
|
||
8. 如需 deep link:
|
||
- 若目标是稳定导入整组点位,可使用 maps_schema_personal_map,但必须说明这是点位导入型链接
|
||
- 若目标是“我的位置”出发的即时导航,应输出 route plan 类 deep link,并说明不同平台协议不同
|
||
9. 最终结果必须包含:
|
||
- resolved_origin(如适用)
|
||
- resolved_destination
|
||
- resolved_stops
|
||
- candidates
|
||
- best_route
|
||
- deep_links
|
||
- summary
|
||
- warnings
|
||
10. 如果任何地址无法可靠解析、任何 POI 无法获取、或任何路线比较存在信息缺失,必须如实说明,不得猜测。
|
||
|
||
你的底层返回必须是结构化数据,不是 HTML。只有在用户明确要求页面展示时,才在结构化结果基础上额外生成 HTML。
|
||
```
|
||
|
||
## Implementation Notes for the Other Agent
|
||
|
||
另一个 Agent 在实施时,建议把逻辑拆为以下函数:
|
||
|
||
1. `resolve_point(raw_point) -> ResolvedPoint`
|
||
2. `resolve_points(request) -> tuple[ResolvedPoint | None, list[ResolvedPoint], ResolvedPoint]`
|
||
3. `generate_candidate_orders(stops) -> list[list[ResolvedPoint]]`
|
||
4. `compute_leg(origin_location, destination_location) -> RouteLeg`
|
||
5. `build_candidate_route(order, origin, destination, origin_mode) -> CandidateRoute`
|
||
6. `rank_candidates(candidates, strategy) -> CandidateRoute`
|
||
7. `build_deep_links(best_route, resolved_points, request) -> DeepLinks`
|
||
8. `summarize_result(result) -> str`
|
||
|
||
## Suggested HTML Policy
|
||
|
||
如果需要 HTML 展示,建议只消费 `RoutePlanResult`,不要直接消费 MCP 原始响应。HTML 层只负责:
|
||
|
||
1. 展示点位信息
|
||
2. 展示候选路线对比
|
||
3. 展示最佳路线
|
||
4. 展示 deep link 按钮
|
||
5. 展示 warnings
|
||
|
||
不要把 HTML 作为 Agent 的核心产物。
|
||
|
||
## Final Guidance
|
||
|
||
这个 Agent 的核心不是“调用一个神奇工具”,而是:
|
||
|
||
1. 用地图工具解析事实
|
||
2. 用编排逻辑生成候选路线
|
||
3. 用明确策略做选择
|
||
4. 用强类型结果给前端稳定对接
|
||
|
||
只要严格按照这份规范执行,就能复刻此前成功的多目标最优路线处理流程。
|