关于 mapsmith
MapSmith 是面向 GIS 工作流的 MCP 服务,可处理经过验证的多步骤分析并记录可审计结果。它调用 GIS 引擎执行地理处理,并在清单中保存输入、参数、CRS 决策和检查结果,以便验证与重跑。
主要功能
- 01验证多步骤分析计划
- 02提供 78 种地理处理操作
- 03记录 CRS 决策及原因
- 04为每个输出生成溯源清单
- 05执行确定性结果检查
- 06从输出恢复分析链
适用场景
- 01结合高程数据分析河流附近的地块
- 02审查地理空间结果的生成依据
- 03拒绝步骤顺序无效的计划
- 04根据输出记录重跑分析
MCP 相关
MapSmith 向 MCP 客户端提供 GIS 分析能力。客户端可提交自然语言问题,由服务端生成并验证计划后执行。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
28收录的工具列表 · 更新于 2 天前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| path | string | 是 |
详细介绍
MapSmith
询问有关数据的问题。获得分析结果,以及证明分析过程的记录。
大多数 GIS 服务会给 Agent 一个可执行的工具。MapSmith 会接收问题,规划出分析方案——通常包含多个操作,而且操作顺序必须正确——在触碰任何文件之前验证方案,然后执行,并记录每个步骤的详细信息——输入了哪些字节、使用了哪些参数和 CRS 决策、运行了哪些检查——供他人事后查阅并重新运行,以确认结果。
试用
让任意 MCP 客户端连接到 MapSmith(一个 JSON 代码块),然后用你自己的话提问:
哪些地块位于河流 1.5 km 范围内,且海拔不超过 120 m?请给出平均 海拔和每块地的地表面积。
这需要执行 5 项操作、使用 2 个坐标系,并进行 48 项检查。实际发生的情况如下——分析计划、因读取后续步骤生成的数据而遭拒的步骤、每个计量步骤背后的 CRS 决策,以及一份你可以在 MapSmith 看到文件之前先在纸上算出的答案。这种拒绝是结构性的,其他拒绝也一样:结构正确却回答了错误问题的计划则会继续运行,这一点已在本页下文测量并记录。
这些操作由 GeoPandas、DuckDB Spatial、exactextract 和 Whitebox Workflows 执行,绝不由提出请求的模型执行。每个数据集都会写入磁盘,其旁边还会有一份谱系清单,记录输入及其校验和、确切参数、CRS 决策及其原因、引擎版本,以及对结果执行的确定性检查。
mapsmith.dev ——一项真实的地形分析,以及随附的清单。两者都是构建产物:图像由 MapSmith 写入的 GeoTIFF 渲染,因此页面内容不会与软件的实际行为脱节。
清单采用指定格式,并非 MapSmith 的私有输出:它包含 JSON Schema、无需工具链的验证器、一套符合性测试,以及一个从不导入 MapSmith、仅百行左右的生成器。记录带有 spec_version,CI 会使用规范自带的验证器验证 MapSmith 的真实输出。该规范已归档,可通过 10.5281/zenodo.22205213 引用;这是一个指向最新已归档草案的概念 DOI。0.8.0 到 0.10.0 写入的记录声明版本为 1.0.0-draft.10,0.7.x 写入的记录声明版本为 1.0.0-draft.8;如果 DOI 显示的是更早的版本,说明归档尚未更新,而代码库中保存了记录所依据的文本。MapSmith 写入记录的名称中,除规范定义的名称外,其他名称——每个扩展检查名称,以及 crs_decisions 中每个扩展字段及其为何不是规范现有键的同义项——都列在 docs/manifest-vocabulary.md 中;该文档由源代码生成,而非手动维护。
先看证据,再谈承诺。正确性测试套件由独立组织 Argleton 维护,它会用 31 个陷阱测试 MapSmith;每个陷阱的答案都在任何系统运行之前通过纸面计算得出:静默错误 0.00,漏项为零,而用显而易见的方式编写相同代码,得分为 0.9032。2026-08-30 至 2026-09-15 的运行结果也显示为 0.00,但其中一个陷阱的结果错了:其真值连续 26 天都偏向 MapSmith,错误两次都出在我们这边,勘误对此有说明。自 2026-09-26 起的每次运行(包括当前这次)都按更正后的真值评分。为了走到这一步,测试套件退回了 7 个缺陷,现已逐一列出。此外还有:GABench 上的 A/B 测试,其醒目结论是无显著结果,并附有拆解我们自己正向结果的分析;基于美国地质调查局(USGS)圣海伦斯火山真实 DEM 的笔记本;可在聊天中显示结果的地图面板,其中会显示每个绘制图层的验证状态;以及一项对我们自身工具发现能力的测量,它撤回了本页此前公布的两个数字——包括下方要点列表中的那个数字。
一项完整分析,从头到尾
这正是 MapSmith 的用途,而且它并非通过一次工具调用完成。一个问题——六块地、一条河、一张高程网格——会转化为一个计划:从 78 个操作中选出五个,先验证再执行,逐步运行并记录全过程:缩小目录范围的搜索、关键参数、每个测量步骤背后的 CRS 决策,以及对每个结果执行的检查。
本节的内容并非手绘。benchmarks/worked_example.py 会构建答案可以手算出来的测试数据,用问题中的措辞查询目录,验证并运行计划,读取清单,然后生成下文;如果本页与脚本内容不一致,tests/test_worked_example.py 就会报错。位置列使用的是 BM25,而非默认引擎,因为发布的图表不应取决于构建页面的机器是否成功下载了模型——两者的筛选结果相同,这才是重点。值得关注的有两点:中间列展示了目录如何从 78 个操作缩小到少数几个,让调用方可以查看;CRS 列则展示了每个测量操作如何说明数据被转换到了哪个坐标系,以及转换原因。
flowchart TB
ASK["<b>Parcels within 1.5 km of the river whose mean ground elevation is at most 120 m, with the elevation and the ground area of each</b>"]
ASK --> PLAN{{"plan validated<br/>before anything runs"}}
PLAN -. "rejected: FORWARD_REFERENCE" .-> BAD["'mask_path' references '$buffer' which runs later — move step 'buffer' before 'near'"]
BAD:::bad
BUFFER["<b>buffer_layer</b><br/>78 operations → 27 candidates → chosen<br/>CRS EPSG:32610<br/>9/9 checks"]
PLAN --> BUFFER
NEAR["<b>clip_layer</b><br/>78 operations → 13 candidates → chosen<br/>CRS EPSG:4326<br/>12/12 checks"]
BUFFER --> NEAR
HEIGHT["<b>zonal_statistics</b><br/>78 operations → 2 candidates → chosen<br/>CRS EPSG:4326<br/>7/7 checks"]
NEAR --> HEIGHT
AREA["<b>measure_area</b><br/>78 operations → 24 candidates → chosen<br/>CRS WGS 84 (ellipsoidal)<br/>10/10 checks"]
HEIGHT --> AREA
FILTER["<b>select_features</b><br/>78 operations → 24 candidates → chosen<br/>CRS EPSG:4326<br/>10/10 checks"]
AREA --> FILTER
OUT[["3 parcels, each with elevation and ground area"]]
FILTER --> OUT
classDef bad stroke-dasharray: 4 3
| Agent 的请求 | 声明内容 | 候选项 | 选定项 | 排名 |
|---|---|---|---|---|
| “河流周围一点五公里内的所有内容” | 矢量,dataset:vector,1 个数据集,线 | 78 个中有 27 个 | buffer_layer | 2 |
| “仅保留落在该缓冲带内的地块” | 矢量,dataset:vector,2 个数据集,面 | 78 个中有 13 个 | clip_layer | 1 |
| “这些地块下方的地面有多高” | 栅格,dataset:vector,2 个数据集,面 | 78 个中有 2 个 | zonal_statistics | 2 |
| “每块地的地面面积有多大” | 矢量,dataset:vector,1 个数据集,面 | 78 个中有 24 个 | measure_area | 1 |
| “去掉地面高于 120 米的地块” | 矢量,dataset:vector,1 个数据集,面 | 78 个中有 24 个 | select_features | 1 |
| 步骤 | 操作 | 关键参数 | 已记录的 CRS 决策 | 检查 |
|---|---|---|---|---|
| 缓冲区 | buffer_layer | distance_meters=1500 | EPSG:32610 — 地理 CRS 上进行米制缓冲时估算出的 UTM 分区 | 9/9 |
| 邻近筛选 | clip_layer | mask_path=$buffer | EPSG:4326 — 掩膜已使用输入图层的 CRS;未进行重投影 | 12/12 |
| 高程 | zonal_statistics | zones_path=$near,stats=['mean', 'min'] | EPSG:4326 — 分区与栅格使用相同的 CRS | 7/7 |
| 面积 | measure_area | input_path=$height,method=geodesic | WGS 84(椭球面) — 地面面积是在 WGS 84 椭球面上计算的,坐标首先被投影到该椭球面;此过程不涉及地图平面,因此不会引入投影变形 | 10/10 |
| 筛选 | select_features | input_path=$area,by=field_between,field=mean,maximum=120 | EPSG:4326 — CRS 未改变:筛选行不会影响坐标 | 10/10 |
每一步都在计划内,包括最后一步:select_features 接收 4 行并返回 3 行,也像其他写入操作一样生成了清单。以前这一步会在计划之外运行,因为唯一能回答这个问题的操作是 run_sql——它在 SQL 字符串中接收输入,声明的数据集数量为零,因此无法加入计划的数据流。这个边界是有意设定的,至今未变:将 $step 替换到任意字符串中,就相当于允许规划器通过拼接文本来构造路径。改变的是,现在不再只有这一种提问方式。
答案可以在 MapSmith 看到文件之前手算出来:这些地块是边长为 0.0015° 的正方形,位于北纬 46.2°,因此每块约为 119 米 × 167 米;测试数据中的高程从西向东逐渐升高。
| 名称 | 均值 | 最小值 | area_m2 |
|---|---|---|---|
| North Field | 104.85 | 104.14 | 19303.33 |
| Mill Meadow | 110.51 | 109.8 | 19303.33 |
| Old Orchard | 117.58 | 116.87 | 19303.33 |
被拒绝的计划是诚实的另一半。在 Agent 基准测试中,步骤顺序错误是最主要的失败类型,因此示例包含了一个这样的计划,并展示验证器对此的反馈,而且此时还没有触碰任何文件。它能出现在这里,是因为写这段内容时它确实犯了这个错误:计划的第一个版本传入了 distance_m,而操作声明的参数是 distance_meters;验证器指出了参数名称,并列出了它接受的三个参数。 |
你可以在 mapsmith.dev 上逐步查看,一次看一个步骤,还能看到第二个案例;这个案例 GitHub 不太容易展示:MapSmith 拒绝处理的文件。构建该页面时,这两个案例都会实际执行。第二个案例留下的记录就在这里,因为这才是无人展示的另一半。
它拒绝回答的问题。 面对一份能在所有查看器中正常打开的 GeoTIFF,询问*“这两口井各自的地面高度是多少?”*:
| 先描述文件 | |
|---|---|
georeferencing_source | sidecar (.aux.xml) |
georeferencing_sidecar_present | terrain.tif.aux.xml |
georeferencing_internal_would_give | cell 10 x 10 at (500000, 5030000) |
terrain.tif is georeferenced twice and nobody chose: the GeoTIFF's own tags say cell 10 x 10 at (500000, 5030000), and terrain.tif.aux.xml beside it says something else. GDAL prefers the sidecar, which is correct — that is how an override works — but sample_raster_at_points would then report numbers from a file you did not name, and this record could not say which. describe_dataset lists both. To choose, either remove the sidecar or set GDAL_GEOREF_SOURCES=INTERNAL for a run that must use the file's own.
它本可以返回一个数值。但这个数值会取自一张位于 100 公里之外、网格尺寸是两倍的网格,而且记录中没有任何信息能说明这个数值究竟来自哪一种解读。
分析结果也能从文件中追溯出来。 仅凭上述五步运行的最后一个输出——没有计划、没有文件名,也没有指向其他记录的字段——通过对它进行哈希并沿着每个输入摘要回溯,就能还原整个链条:
5 operations recovered back to 3 original dataset(s), every one passing its critical checks.
<- select_features 10 checks
<- measure_area 10 checks
<- zonal_statistics 7 checks
<- clip_layer 12 checks
<- buffer_layer 9 checks
(original) elevation.tif
(original) parcels.gpkg
(original) river.gpkg
快速入门
通过 stdio 将 MapSmith 添加到任何 MCP 客户端(Claude Desktop、Claude Code、Cursor、VS Code):
{
"mcpServers": {
"mapsmith": {
"command": "uvx",
"args": ["mapsmith"]
}
}
}
推荐使用 Docker,它会将服务限制在你挂载的目录中:
{
"mcpServers": {
"mapsmith": {
"command": "docker",
"args": ["run", "-i", "--rm",
"-v", "/absolute/path/to/your/data:/data",
"-e", "MAPSMITH_WORKSPACE=/data",
"ghcr.io/mapsmith-ai/mapsmith"]
}
}
}
一键安装:
或在终端中运行:code --add-mcp '{"name":"mapsmith","command":"uvx","args":["mapsmith"]}'
要在配置客户端之前确认它能运行,可执行 uvx mapsmith,在 stdio 上启动服务(按 Ctrl-C 退出)——它使用 MCP 通信,并非命令行工具,因此提示符静默无输出就表示它正在运行。
此页面描述 0.10.1,也就是该命令安装的版本。当 main 超前于已发布的构件时,本段会说明这一点并指出差异——读者不应在调用一个并不存在的工具后才发现这一点。如果你在 MAPSMITH_WORKSPACE 下运行 0.10.0 或更早版本,请更新:坐标参考系——无论是工具参数,还是文件声明的坐标参考系——都可能导致 PROJ 读取工作区以外的文件或打开网络共享;在 run_sql 中,DuckDB 自带的 PROJ 也可能如此。0.10.0 对工具参数以及 GeoPackage、shapefile 和 GeoParquet 输入关闭了这种可能,0.10.1 则覆盖了 MapSmith 接受的所有其他格式——在工作区下,它现在会在 GDAL 打开文件前读取每个输入的坐标系,并拒绝无法识别的格式。详情请见 SECURITY.md。自 0.9.0 起,0.10.0 还做了以下改动:每个将矢量输入转换到另一 CRS 的操作——包括对齐第二个输入的 8 个操作,以及栅格操作中的采样点、分区、掩膜、汇水点和测站——都会像 reproject_layer 一样选择基准转换;当 PROJ 自行选择的是近似转换,且存在覆盖范围更广的操作或格林尼治经线对应项时,这会让结果移至正确位置(罗马基准的 Monte Mario:最多约 80 m);CRS 自身声明的基准偏移(+towgs84)不再记录为近似转换;nearest_neighbour_index 现在使用线性内存,并会拒绝没有位置的要素;在工作区下,run_sql 仅运行 SELECT,坐标系和数据集路径只能作为字面值;此外,来自 QGIS Training Manual 练习的 103 种目录搜索表述调整了此页面上的发现统计数字。自 0.8.0 起,0.9.0 新增了 apportion_by_area,可将数量和比率从其报告所在的分区转移到与之相交的分区;并让记录中的 transformation 描述 PROJ 对数据自身坐标执行的操作,而不是对从 CRS 中选取的某个点执行的操作。这会改变 reproject_layer 在两种情况下写出的结果:坐标位于所有已发布操作的适用范围之外时,不再通过其他区域的操作进行转换;对于以另一条子午线为基准、但操作仅针对其格林尼治经线对应项发布的基准——罗马基准的 Monte Mario,EPSG:4806——则会通过该对应项进行转换。更新日志列出了这两项改动。如果你从 0.4.0 升级到 0.6.1、0.7.0 或更高版本,答案会有所不同:从点注册栅格(AREA_OR_POINT=Point,即 USGS 和 Copernicus DEM)读取的每个位置都偏移了半个像元;更新日志说明了哪些操作受到影响以及原因。自 0.7.x 起,0.8.0 新增了 cluster_points_by_distance 和用于目录搜索的 geometry facet,能够连同属性和 CRS 一起摘要 shapefile,读取任意单图层文件地理数据库,并修复了接受错误几何类型却仍返回结果的操作(对线分区运行 zonal_statistics 会将其长度作为像元数量返回)。
如果你会查看清单,0.10.0 没有重命名任何键:记录仍声明 1.0.0-draft.10。MapSmith 自有的两个键新增于 crs_decisions.transformation 中:当偏移写入 CRS 本身时使用 x-mapsmith:shift_from_crs_definition,当引擎自行选择会导致不应用任何偏移时使用 x-mapsmith:default_was_ballpark。
如果你会查看清单,0.9.0 没有重命名任何键:记录仍声明 1.0.0-draft.10。新增了 3 个 MapSmith 自有键:crs_decisions.x-mapsmith:areas_measured_on,来自 apportion_by_area;以及 transformation 内的 x-mapsmith:ballpark_share 和 x-mapsmith:outside_area_of_use。此外还新增了 3 个检查项名称,均来自 apportion_by_area。如果某次运行失败,而更早一次运行的数据集仍在原处,则会将该次运行的记录写在此前记录旁边,文件名为 <output>.failed.provenance.json,而不是替换此前记录。
如果你会读取清单,0.8.0 重命名了一个键:记录声明 1.0.0-draft.10,而第二个输入自身的地理参考信息,在 0.7.x 中为 inputs[].x-mapsmith:environment,现在为 inputs[].environment,这是规范自 draft.10 起为其指定的名称。读取多个数据集的操作,其每个输入也会说明读取它的参数(inputs[].argument)。
如果你会读取清单,0.7.x 没有重命名任何键:记录声明 spec_version 1.0.0-draft.8,比 0.6.0 的 draft.6 晚两个草案版本;这两个草案版本都只是缩小了生产方可以自行添加键的范围,而 MapSmith 早已采用了更严格的做法。从 0.5.x 开始,0.6.0 将 x-mapsmith:round_trip 重命名为 round_trip;从 0.4.0 开始,0.5.0 重命名了八个 crs_decisions 键、顶层的 mapsmith_version(请读取 producer.version)、往返信息中的两个字段、repairs[] 中的两个字段,以及 engine.geometry_library。变更日志列出了每一项及其替代读取方式。
然后向你的 Agent 提出类似这样的要求:
“读取 parcels.gpkg,只保留 rivers.gpkg 中距河流 300 m 范围内的地块,并提供包含分析沿袭信息的结果。”
Docker 镜像包含 [raster] 和 [whitebox] 扩展。使用 uvx 时,可以自行选择:uvx --from "mapsmith[raster,whitebox]" mapsmith。Docker——或在能够正常使用 wheels 的机器上运行 uvx——是唯一受支持的安装方式:跨三个操作系统的地理空间原生依赖是个支持黑洞,遇到本地环境损坏的问题时,我们会将其转到这里。
镜像有两点会影响你机器上的运行方式:它会自行设置 MAPSMITH_WORKSPACE=/data(上面的 -e 是显式指定,并非必需),并以 uid 1000 运行;因此,如果挂载的目录属于其他用户,请传入 --user $(id -u):$(id -g)。此外,镜像仅为 amd64 构建,因此在 Apple Silicon 上会通过仿真运行。
你会得到什么
每个数据集都会附带下面这个文件,写入与其同目录的 <output>.provenance.json 中——无需当初提出请求的模型,也足以重新运行分析:
{
"spec_version": "1.0.0-draft.10",
"producer": {"name": "mapsmith", "version": "0.10.1"},
"operation": "buffer_layer",
"parameters": {"distance_meters": 300.0},
"inputs": [{
"path": "rivers.gpkg",
"sha256": "b24b884f49eee431133d443d842557d3ed21f3978e2c4b1e8e072fd1240effe8",
"crs": "EPSG:4326"
}],
"crs_decisions": {
"analysis_crs": "EPSG:32632",
"reason": "estimated UTM zone for metric buffering on a geographic CRS",
"round_trip": {
"transformation": {
"pipeline": "+proj=pipeline +step +proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=utm +zone=32 +ellps=WGS84",
"accuracy_m": 0.0,
"is_ballpark": false
},
"return_transformation": {
"pipeline": "+proj=pipeline +step +inv +proj=utm +zone=32 +ellps=WGS84 +step +proj=unitconvert +xy_in=rad +xy_out=deg",
"accuracy_m": 0.0,
"is_ballpark": false
}
}
},
"engine": {"name": "geopandas", "version": "1.0.1"},
"environment": {},
"verification": [
{"name": "input_crs_present", "passed": true, "detail": "'input_path': EPSG:4326"},
{"name": "input_not_empty", "passed": true, "detail": "'input_path': 1 features"},
{"name": "result_not_empty", "passed": true, "detail": "1 features"},
{"name": "crs_present", "passed": true, "detail": "EPSG:4326"},
{"name": "crs_matches", "passed": true, "detail": "expected EPSG:4326, got EPSG:4326"},
{"name": "geometry_valid", "passed": true, "detail": "all valid"},
{"name": "x-mapsmith:no_geometry_is_empty", "passed": true, "detail": "none empty"},
{"name": "feature_count_exact", "passed": true, "detail": "expected 1, got 1"},
{"name": "geometry_types", "passed": true, "detail": "expected subset of ['MultiPolygon', 'Polygon'], got ['Polygon']"}
],
"started_at": "2026-08-18T10:15:03Z",
"finished_at": "2026-08-18T10:15:04Z"
}
校验和与时间戳是虚构的;其他所有内容——CRS 决策、转换、每项检查的名称和详细信息——都复制自 buffer_layer 针对这次调用生成的记录。真实文件还包含 output、repairs、notes,以及每项检查的 critical 标记;此处为节省篇幅省略了这些内容。
tests/test_showcase.py 会使用规范自带的验证器检查此代码块,并重新运行操作,将其 crs_decisions 键与生成的键进行比较;因为手动编写的记录会悄无声息地偏离规范,而所有自动生成的内容都能保持正确:有两个版本没有 spec_version;截至 2026-09-06,记录仍未包含表明坐标已返回的往返键——当时是 x-mapsmith:round_trip,而规范自 1.0.0-draft.6 起使用 round_trip。第三方实现者复制的就是这条记录。
为了适应页面而裁剪,文件本身并未裁剪:真实记录还包含输出文件自身的路径和哈希、MapSmith 必须修复的任何几何,以及其对输入处理方式所做的说明;每项检查都会注明是否为关键检查,并在失败时说明该如何处理。get_provenance 会为任何输出返回完整的单次操作记录。
若要获取分析而非步骤的信息,run_operation 搭配 get_lineage 会获取最终文件、计算其哈希、查找声称对应这些确切字节的记录,并沿着每个输入摘要一路追溯到原始数据。没有任何字段指向另一条记录:链路是根据内容还原的,因此重命名后仍然有效,只有数据发生变化时才会断开。如果某一步未通过检查,它会如实报告;如果追溯中途停止,也会说明停止位置,而不会呈现未能追溯到的历史。
为什么选择 MapSmith
- 工作的单位是分析,而不是工具调用。 其他 GIS 服务会公开操作,让调用方逐个触发;一个需要五步的问题就会变成五次互不相关的调用,没有任何上层逻辑知道它们属于同一任务。MapSmith 接收一个类型化计划,如果某一步读取了后续步骤才会生成的内容,就会拒绝执行;它会按顺序运行计划,为每一步以及整个计划生成记录,将所有步骤关联起来,并且仅凭内容摘要就能在事后恢复整个处理链。这是工具箱外面套一层包装无法做到的,因为单次调用之上没有任何东西能把它们串联起来。
- 真正的地理处理,而非地图 CRUD。 基于成熟可靠的开源地理空间技术栈构建:GDAL、GeoPandas、Shapely、DuckDB Spatial、Whitebox Workflows 和 exactextract 现已集成(后续还会加入:通过 sidecar 集成 QGIS Processing)。
- 溯源设计。 MapSmith 生成的每个图层都会附带机器可读的溯源清单,记录源数据集及其校验和、执行的工具、精确参数、CRS 决策、软件版本和时间戳。无需大模型即可重新运行分析所需的一切信息都在其中。没有 AI 垃圾内容。
- 引擎负责计算,模型负责编排。 几何结果和数值始终只来自确定性的工具执行,而非模型输出。
- 78 项操作,通过 28 个目标级工具调用。 操作目录覆盖广泛,工具列表则负责选择;目录支持搜索,而不是一股脑全列出来,因为一次暴露数十个工具后,工具选择的准确率会下降;当多个工具都适用于同一输入时,下降尤为明显。能力数量没有这种上限,因此能力收录在目录中。搜索会根据你声明的内容缩小范围,然后交由你决定哪些结果适用,而不是替你排序;因为测量结果表明,排序并不是准确的说法——请参阅查找合适的操作。
- 与模型无关的基础设施。 Claude、GPT、Qwen、Kimi、GLM——任何支持 MCP 的模型,无论云端还是本地都可以使用。真正的优势在于更完善的契约(类型化计划、可执行的错误代码、可搜索的目录),而非需要我们维护的模型权重。请参阅宣言。
工具
| 工具 | 功能 |
|---|---|
describe_dataset | 获取任意矢量或栅格数据集的 CRS、架构/波段、范围、无数据值和统计信息 |
buffer_layer | 创建以米为单位的缓冲区;对于地理 CRS,自动估算 UTM |
clip_layer | 使用掩膜图层裁剪图层 |
overlay_layers | 集合论叠加(交集/并集/差集/…);清单会声明被丢弃的低维片段 |
dissolve_layer | 按键合并要素;清单会记录聚合方式,并验证分组数量 |
nearest_join | 最近邻连接,距离以米为单位;对于地理 CRS,使用 UTM 测量(并记录此决策) |
explode_layer | 将多部件转换为单部件,并以封闭形式验证部件数量 |
measure_area | 以 m² 为单位计算面积,始终采用以下方式:在椭球体上计算地表面积,或使用 CRS 自行声明的线性单位将平面面积换算(测量英尺并非米)。测量前修复无效环;如果此处使用的平面不是等面积平面,则会返回其与地表面积的比值 |
merge_layers | 追加图层(合并架构);清单会列出以 null 填充的列,并根据总数验证要素数量 |
simplify_layer | 使用 Douglas-Peucker 算法简化,并测量偏移量:清单会记录简化前后的面积/长度 |
centroid_layer | 在米制 CRS 中计算几何质心,绝不在角度坐标上计算(并记录此决策) |
convert_format | 根据输出扩展名在 GeoParquet/GeoPackage/GeoJSON 之间转换,并重新读取验证(数量和 CRS)。两种转换会拒绝执行并说明原因:输出为 shapefile(会静默将字段名称截断至 10 个字符),以及将非 WGS84 图层转换为 GeoJSON |
reproject_layer | 重投影到任意 CRS(EPSG 代码或 WKT) |
spatial_join | 按空间谓词连接,并自动路由到最快的引擎(SedonaDB > DuckDB > GeoPandas) |
run_sql | 对 GeoParquet 和 GDAL 格式运行空间 SQL(DuckDB 方言) |
zonal_statistics | 计算每个矢量分区的栅格统计信息,精确覆盖像素分数([raster] extra) |
hillshade | 从 DEM 生成阴影地形图,使用内存中的 Whitebox 引擎([whitebox] extra) |
slope | 以度、百分比或弧度计算 DEM 的坡度;拒绝处理地理 CRS 的 DEM([whitebox] extra) |
aspect | 从 DEM 计算下坡方位角,0 = 北;平坦像元为 −1,而非无数据值([whitebox] extra) |
flow_accumulation | 使用 D8 算法计算汇流累积,并自动填洼([whitebox] extra) |
watershed | 根据 DEM 和出口点划定流域([whitebox] extra) |
preview_map | 在聊天中显示任意数据集的交互式地图(MCP Apps),并为每个图层提供来源卡片和验证状态 |
validate_plan | 在运行任何内容前静态验证多步骤计划:操作、参数、引用、输入文件和模拟的 CRS 流程 |
execute_plan | 验证后逐步运行计划,并提供每步来源信息和计划级清单 |
get_provenance | 返回任何 MapSmith 输出的清单:一项操作、其输入及其摘要,以及已执行的检查 |
list_operations | 目录搜索:根据你声明的条件缩小范围,然后返回符合条件的集合供选择(status: "choose"),或按 engine 排序 — BM25、embeddings 或 auto;detail=true 会返回参数和完整示例 |
run_operation | 按名称运行任意目录操作,包括没有专属工具的操作;运行前会根据目录验证参数 |
server_info | 版本、许可证、可用引擎 |
查找合适的操作
这些是 Agent 可以选择的工具。其背后的目录收录了 MapSmith 可执行的所有操作 — 目前有 78 项,其中 53 项没有专属工具 — 并且设计上可扩展至数千项。(78 项中有两项标记为 planned,查询时会明确说明:路线图有意保留在目录中,这样 Agent 就能回答“暂不可用”,而不是编造调用。)
这种拆分就是设计本身:暴露的工具超过几十个后,工具选择准确率就会下降,而能力数量没有这样的上限。因此,扩展能力成了一个检索问题,所以我们就按检索问题来处理——也按检索问题来衡量。
首先,根据调用方已经掌握的信息进行确定性筛选。 每个条目都会声明它接收什么数据(vector、raster、dataset、plan、none),返回什么(dataset:vector、dataset:raster、answer、description),是否要求使用投影 CRS,以及所属类别。
我们用另外两个模型系列根据工作场景撰写的 118 个可回答请求进行测量,例如一位水文学家手持洪水报告、一位测量员质疑一项现场测量结果。这两个模型都没有看过此目录,因为拿到条目的模型会根据条目撰写释义:
| 调用方声明的内容 | 剩余候选项 | BM25,found@3 | embeddings,found@3 | 返回结果中包含正确答案的比例 |
|---|---|---|---|---|
| 什么都不声明——只有文字 | 78 | 30% | 19% | 30% |
| 我有什么数据 | 52 | 35% | 24% | 36% |
| + 我想要什么结果 | 32 | 46% | 43% | 57% |
| + 我有多少个数据集 | 17 | 58% | 53% | 89% |
| + 数据包含什么几何类型 | 15 | 62% | 62% | 98% |
最后一行于 2026-09-29 新增,上方一行则说明了原因。 现在有 31 个操作接收一个矢量图层并返回另一个矢量图层,比下文所述的阈值多一个。因此,调用方即使声明了所知的一切,拿到的也是最常见请求的候选清单,而 交付率 从 98% 降到了 84%。解决办法是再声明一项调用方掌握的信息:图层包含点、线还是多边形;如果混合,则以列表形式声明。条目只会在代码确实拒绝其他类型时声明它接受的类别;测试会用每个条目未声明的所有类别调用它,并要求它拒绝,这样声明就不会把某个操作从本来可以处理它的调用方那里筛掉。编写这项测试时发现,zonal_statistics 会接受线要素作为分区,并把它们的长度作为像素计数返回;现在它会拒绝这类输入。
现在有两列排序结果,这是一次修正。 这张表过去只有一列,使用默认引擎计算——模型能够加载时用 embedding 引擎,否则用 BM25。因此,公布的数字实际上衡量的是机器能够下载什么;而在 CI 中遇到 Hugging Face 的 429 错误时,第一行被重新算成了 28%,与本页所写的 18% 不同。这不是测试不稳定,而是一个无法在没有模型的机器上复现的数字,却被写在承诺可供验证的说明之下。
两种引擎还有一点差异值得关注,而本页直到 2026-08-30 才纠正了这一点。本页曾称,筛选条件缩小范围后,embedding 引擎会超过 BM25。事实并非如此:在上表每一行中,BM25 都领先或持平;只有文字时领先 11 个百分点,而声明最完整时两者持平(由于 2026-10-13 新增的释义,两者都是 62%)。此时留下的条目会根据区分它们的词语来区分——这正是 distinguishes 的用途,也是精确词项要么匹配、要么不匹配的原因。embedding 引擎之所以有价值,是因为它能处理从未见过的释义,而不是因为候选项缩小后排序表现更好。(截至 2026-09-25,本段曾称差距为 7 到 12 个百分点,并且在声明最完整时差距最大。但根据当时公布的数据,情况恰好相反:差距在那里最小,只有 2 个百分点。没有任何检查验证这句话,只有旁边的表格。)
最后一列不是准确率,而是一项属性;98% 而非 100% 值得用一句话说明。缩小范围的过程从未漏掉正确操作:这一点对每个条目都有明确保证,并且对全部 78 个条目都成立。这一列衡量的是剩余集合是否足够小,可以整体交付;对于少数请求,集合仍然太大,因此会退回到排序后的候选短名单,答案可能不在前三名。排序决定顺序,不决定成员资格;这 2% 的差距,正是“不会漏掉答案”和“可以把所有结果都展示给你”之间的差别。
第三行是扩展能力的瓶颈,我们一个下午就碰到了。 2026-08-29,目录中的操作从 51 个增至 61 个。表格中的两行指标变差了:最常见的剩余集合从 26 个候选增至 34 个,超过了可以整体交付的上限;已交付比例从 100% 降至 45%,而 found@3 从 48% 降至 36%。能力增加反而让发现变差了——这正是本页预测操作达到 800 个时会发生的失败,却在 61 个时就遇到了。
提高阈值大约可以将这一刻推迟 10 个操作。真正解决问题的是第四行:你持有多少个数据集。这取决于你的实际情况——一层还是两层——而不是我们对词汇的猜测;它可以从每个操作自身的签名推导出来,因此测试可以将声明与代码进行核对,并能将剩余集合的中位数从 34 降至 9。目录增长了五分之一,发现效果反而更好,但这只是因为同时加入了一个分类维度。这就是此设计所作的权衡;我们将其明说,而不是留待日后才发现。
第二天又发生了同样的事,而观察曲线的意义就在于此。 2026-08-30,目录中的操作从 61 个增至 71 个。表中的每项排序指标都下降了——未加分类维度时从 28% 降至 25%,按输入类型分类时从 34% 降至 27%;第二行的已交付比例则从 48% 降至 27%,因为现在有更多请求会留下过大的集合,无法整体交付。底行没有变化:97%,与 61 个操作时相同。 又增加了 10 个操作,没有新增分类维度,保证仍然成立;这是第一次由现有分类维度吸收了增长。(不是“也与 51 个操作时相同”:底行是元数分类维度,而 dataset_inputs 在 51 个操作时还不存在。上文引用的该规模下的 100% 来自它上面那一行。在同一句话里混比两行指标,正是本页要避免的做法。)
74 个操作时又出现了同样的情况,这次增加的是两个调用者特别可能需要的操作。 select_features 和 extract_layer 是 MapSmith 自身错误消息一直建议使用的补救操作,因此它们位于分类空间中最繁忙的区域:剩余集合的平均值从 16 增至 17,而第二行的已交付比例没有变化。目录规模连续第三次增长,底行仍保持在 97%。距离瓶颈现在还有 13 个操作。(这些数字是 2026-08-31 测得的。2026-09-01,针对人工答案重新计算了这些数字——见表格下方的说明——目录没有变化,数字却随之变动:剩余集合为 16 个,底行为 98%。描述一次变更时,保留的是变更发生时的数字。)
76 个操作时,新增操作所在的位置显出了代价。 summarize_points_in_polygons 接收两个矢量图层并返回一个,因此它位于分类空间中最繁忙的区域——count_in_polygons、spatial_join 和 clip_layer 都是它的邻居。于是,在 BM25 下,底行的 found@3 从 58% 降至 55%,而剩余集合的平均值仍为 16,已交付比例保持在 98%。之所以添加该操作,是因为回答“按田块统计平均 pH”需要两次调用,所以对搜索者而言,这项能力就如同不存在;在分类声明最完整的情况下,代价就是排序指标下降 3 个百分点,而重要的保证并未改变。
而当现有操作新增了一个可选输入,且没有新增操作时。 zonal_statistics 在 2026-09-25 增加了一个权重栅格,因此它读取两个或三个数据集;对于可变的参数数量,声明数量反而等于不声明——run_sql 和 merge_layers 遵循的就是这条规则,因为如果声明为 2,就会让正确指出自己有三个数据集的调用方看不到加权形式。代价是它现在能通过所有参数数量的筛选:平均剩余集合从 16 增至 17,底行的 found@3 从 55% 降至 53%,而 交付率 仍为 98%。一个对某项操作失去区分能力的筛选维度,会让每次搜索都多出一个候选项。
这就是这种权衡的本质,也说明了何时该增加下一个筛选维度。值得关注的数字 不是 found@3——随着目录增长,排序器的表现总会变差,它只是一个提示。真正要看的是 最完整声明下的平均剩余集合,也就是表格的第四列: 51 个操作时为 9,61 个时为 14,72 个时为 16,74 个时为 16,75 个时为 16,76 个时为 17,77 个时为 15,78 个时为 15。(77 个操作时的那个数据点是首次声明了 geometry;若不声明,集合会是 17。)一旦越过阈值,交付就不再是一个属性,而又变回排序问题;长久有效的解决办法,是增加另一个调用方已经知道的事实。提高阈值只能在调用方仍能从更多候选项中做出正确选择的范围内奏效;这一点在下文有测量,而非假定。(直到 2026-08-29,页面上写的是中位数,后来公布了平均值: 78 个操作时的中位数是 13。分布呈偏态——大多数请求只留下少量候选项,少数请求则留下大量候选项——因此这两个数字表达的含义不同,而平均值更悲观,也正是值得关注的数字。)
请求、两个标签以及测试框架都在仓库中:
tests/data/discovery_queries.json 和
benchmarks/discovery_report.py;它会根据这些文件重新计算上述所有数字,不需要网络或模型——因此这些数字可以核验,而非盲信;如果本页与测试框架不一致,tests/test_discovery_report.py 就会失败。唯一的例外是 69%:要复现这个数字,需要使用当时负责选择的模型,报告在无法继续计算的地方会说明这一点。下文对阈值的测量则正好相反:选择记录保存在仓库中,因此无需模型就能重新计算相关数据。
因此,它会交出候选集合,而不是替你做选择。 当剩余候选项不超过 30 个时,list_operations
会以 status: "choose" 作答:列出每个候选项,按顺序排列,并明确提示这只是一个提示;每项还附带一句说明,帮助区分它与相邻候选项。在这 118 个请求中,剩余集合的中位数为 13,其中两个请求超过 30 个——从另一面看,这就是上表中的 98%。输出约为 2,100 个 token,比运行并撤销一次错误操作的代价还低。
30 这个数经过了测量,而且最初尝试的是更大的阈值。 面对第 31 个向量到向量操作,提高阈值显然是个答案;模型也越来越擅长阅读长列表。因此,在调整阈值之前,我们先测量从更多候选项中是否仍能做出同样好的选择
(benchmarks/choice_at_scale.py,每项选择记录见
benchmarks/choice_at_scale.json)。将 118 个请求分别以 30、40 和 50 个候选项组成的集合交给模型;新增候选项由排序器挑出最相近的操作,因此都是容易混淆的选项;使用 12 个选择者(Claude Opus 5.5),确保没有人看到同一个请求两次;并将每个请求与自身进行比较。
| 较大的集合 | 配对数 | 从 30 个候选项中选对 | 从较大的集合中选对 | 仅从 30 个候选项中选对 | 仅从较大的集合中选对 |
|---|---|---|---|---|---|
| 40 个候选项 | 116 | 85.3% | 78.4% | 9 | 1 |
| 50 个候选项 | 118 | 84.7% | 79.7% | 8 | 2 |
在相同请求上,四十个时丢了 7 个百分点,五十个时丢了 5 个百分点,因此阈值保持不变,修复则按本节所述,放在上方的几何数据行中。这项测量第一次运行时得出了相反结论:四十个时毫无损失;当时比较的是不同规模下不同组合的请求,而不是逐个请求进行比较。评审在它进入 main 前发现了这个问题。测试会将阈值限制在实测中未出现损失的最大规模或以下,因此要调整阈值,就必须重新运行测量;更强的模型是重新测量的理由,而不是修改数字的理由。 |
(这句话以前写的是,这组数据的中位数为 26,且从未超过 30。等本目录扩充到 72 个操作时,这个说法早已失实,却没人注意到,因为检查此页面与测试框架是否一致的测试只读取表格,没有读取周围的文字。现在两者都会读取。)
三项测量都表明这是正确的形式,而第三项是最终定论:
| 我们的排名将答案排在前三 | 62% |
| 将相同的候选项交给模型并让它选择时,模型选对首选项 | 69% |
| 撰写标准答案的两位标注者彼此达成一致 | 70% |
这些数字在 2026-09-01 上升,是因为测量方式变了,而不是排名器变了。 一位从事这项工作的人回答了两位模型标注者意见不一致的全部 50 个请求,而且一个请求可能有多个可接受的答案:两位经验丰富的分析师会用不同的工具得出相同结果。因此,现在的命中率会将结果与专业人士认可的每个操作进行比对,而不是只与一个标签比对,found@3 上升了 4 到 5 个百分点。排名代码没有任何变化。反过来说,旧数字低估了这么多——系统返回另一个合理答案时,却被记作失败——而对这张表最诚实的描述是:答案在专业人士可接受的选项之中。这是一种性质,而不是准确率。
次要答案的来源有记录,并未被掩盖:这 50 个请求中的主要答案均由人工选择,次要答案则由第三个模型提出,并被全盘采纳,而非逐一评判。两种证据的强度不同,文件中明确标注了各自的性质。
两个模型相关数字都有日期:标签于 2026-08-28 撰写,当时目录包含 51 个操作。 目录现在有 78 个操作,因此对于正确答案属于之后新增的 27 个操作之一的请求,两位标注者都不可能答对——当时目录中没有这个操作可供他们命名。对人工手动回答的前四个请求进行测量后发现,其中两个请求的两位标注者都答错了,而这两个请求对应的操作在 28 日时都还不存在。因此,“两位标注者都答错”和“两位标注者都选得不好”不是同一个数字;诚实的做法是说明标签的制作时间,而不是悄悄利用这一差异。人工答案正逐个替换旧答案——答案通过 benchmarks/ingest_answers.py 加入,上述每个数字在有人工答案时,都会以人工答案为基准计算。
三项指标都基于相同的 118 个请求,这一点很重要:文件中全部 155 个请求的标注者一致率为 68%,差异来自 21 对两位标注者都认为无法回答的请求——这确实是事实,但也是容易的一半。把这个 68% 与基于 118 个请求计算出的 62% 放在一起引用,就是在比较两个不同的群体;这张表曾有半天时间犯了这个错误。
最后一行是上限,而不是基线,第二行达到的正是这个上限,而非低于它。当两位称职的标注者每十个请求中就有三次对哪个操作能回答请求意见不一时,“正确答案”就不是一个可以据此排序的单一值;系统得分若高于此值,说明它是在拟合某位标注者,而不是变得更好。两位各有三十年经验的 GIS 分析师用不同工具完成同一项工作,谁都没有错。
这就是为什么答案是一组结果,也为什么它的 reason 字段会用文字说明:排序只是提示,有两个都说得通的候选项时,应由提出请求的人来决定。调用方——拥有对话上下文的 Agent——知道排名算法无法获知的信息。调用方不知道的,人类就知道。
还有一点需要如实说明:这个上限是在两个大模型之间测得的。人类 GIS 分析师彼此之间的一致程度更高、更低还是大致相同,目前尚无测量结果;在测量之前,这些数字都只报告为与模型编写的标签的一致程度,绝不称为准确率。
**分类是唯一用于排序而非筛选的维度,这是一项修正。**它过去和其他维度一样,是硬性筛选条件。但它与其他维度不同:输入类型和目标 CRS 是手头数据的事实,输出类型是调用方想要的结果,而分类只是对我们分类体系的猜测,调用方看不到这个体系。测量发现,它会从 15 个候选项中剔除 5 个;而猜错时,它会在没有报错的情况下剔除正确操作,只留下由相近项拼出的自信答案。独立测试集中的每个请求平均有 4.4 个合理分类。这正是 Argleton 在其他人的系统中测量的静默失败类型,而它也存在于我们自己的发现层中,所以现在它只用于排序:声明分类会把它排到前面;猜错时只会让它后移,绝不会从结果中剔除。硬性筛选仍可通过 catalog.applicable 使用,调用方明确要求时才会启用。
**我们不需要模型来提取这些维度,因为调用方本身就是模型。**MCP 客户端是一个拥有我们所缺上下文的大模型——它知道自己正在处理什么文件,以及想要生成什么结果。因此,list_operations 会在其架构中询问这些信息,并在描述开头说明原因。这与 LlamaIndex 的 Auto-Retrieval 或 LangChain 的 Self-Querying 采用相同的模式,只是省去了它们必须托管的模型:这里的模型已经位于协议的另一端。地理栅格永远不会推荐 slope,因为 slope 不接受栅格——这是数据本身的属性,由代码检查,不涉及模型。
接着进行排序,两个引擎始终都会运行。list_operations 接受 engine 参数:auto(默认值)、lexical 或 vector。每个结果都会注明生成它的引擎,因为 BM25 得分 10.03 和余弦相似度 0.38 并非同一量纲。
| 引擎 | 含义 | 保证 |
|---|---|---|
auto — 默认值 | 使用嵌入引擎;模型无法加载时回退到 BM25 | 即使在没有网络的机器上也能给出答案,并提供字段说明使用了哪个引擎 |
lexical — 词语 | Okapi BM25,约 40 行代码,不需要模型,也永远不需要网络 | 所有机器上的得分都相同;按词项排序后累加,因为浮点数加法不满足结合律 |
vector — 含义 | 静态嵌入——通过词元查找再进行池化,不使用 Transformer,也不需要 GPU。模型修订版本固定在源代码中,512 维,约 130 MB,仅下载一次 | 在同一进程的多次调用间逐位相同(已测量,关闭多进程),向量由黄金向量测试固定——因此,模型、分词器或池化方式的任何变化都会导致测试失败,而非污染分析结果 |
在测量结果证明并非如此之前,默认引擎一直是词法引擎,而测量过程才是有趣之处。由目录编写者撰写的黄金查询与目录共享词汇,因此它们测试的是伪装成检索的词语重合度:在这些查询上,BM25 的 found@1 得分为 100%,而嵌入为 60%。换成有实际问题的人会用的说法——“海岸线有 400000 个节点,浏览器崩溃了”,而不是*“简化几何”*——在包含 51 个条目的目录上,结果便反转了。此后结果又反转回来,而且随着目录扩大,两个引擎的表现都会下降:
| 目录大小 | BM25 found@3 | 嵌入 found@3 |
|---|---|---|
| 10 | 72% | 82% |
| 30 | 65% | 62% |
| 78 | 55% | 45% |
| 这张表过去的结论恰好相反,而反转正是这项发现。 发布于 10/30/51 | ||
| 当时的数据是 78/83、47/65、40/55——嵌入在每种规模下都领先——下方的文字还写着 | ||
| BM25 的性能下降更快,这正是嵌入引擎从额外选项变成依赖项的原因。 | ||
| 如今重新计算后,交叉点已经移动:在十个条目时嵌入仍领先,从三十个条目起,BM25 | ||
| 开始领先,而且差距随规模扩大。部分原因在于目录本身,因为干扰项取自目录,而目录 | ||
| 从五十一个条目增长到了最后一行——这恰恰是重点,而不是需要加以说明的例外。**八百项操作测试 | ||
| 预测的近邻效应已经出现在我们自己的目录中**,过去彼此矛盾的两张表如今终于一致了。 |
曲线由 tests/test_retrieval_degradation.py 重新计算,并与这张表进行比较,因此它不会再悄无声息地过时——而它此前确实在三个目录规模下都过时了。
而同一天发布的实测结果表明,规模扩大后这一发现并不成立。 上面的干扰项取自我们自己的条目,它们在语义上分布较为分散。扩展这个目录,意味着加入近邻:数百种彼此相似的栅格和地形操作。使用一个自带操作说明的库中的 800 种真实 GIS 操作重新测试后,排名发生反转,嵌入引擎的性能下降得更快:
| 目录规模 | BM25 found@3 | embeddings found@3 |
|---|---|---|
| 74——我们自己的条目,没有外部干扰项 | 50% | 40% |
| 200 | 48% | 25% |
| 800 | 35% | 20% |
嵌入会模糊近邻之间的区别;精确术语则要么匹配,要么不匹配。这两项测量过去彼此矛盾,但都予以保留,因为它们回答的是不同问题:哪种引擎适合我们现有的目录,以及哪种引擎能应对我们计划中的目录。现在它们一致了——这项测量所预测的近邻效应已经出现在我们自己的目录中,因此 BM25 在两种规模下都领先,而第二个问题的答案是两者都不行。嵌入引擎仍是默认选项,但让它成为默认选项的那项测量如今已不再支持这一结论:这是一个需要作出的决定,而不是一个可以悄悄改写的数字。目录达到 800 个条目时,较好的引擎三次中有两次会出错,因此无法通过选择更好的排序器来实现规模扩展。
test_retrieval_at_scale.py 会持续测量这一预测,而不是任由它沦为主观判断。
而缩小候选范围本身也无法随规模扩展——本页曾声称可以,但这是错的。 本页称,在 800 种操作下,筛选维度会像在 200 种操作下一样,将候选项缩小到 16 个。16 这个数字是真的,而且几乎完全是由类别筛选出来的:这 803 种操作全都是栅格输入、栅格输出,因此输入类型和输出类型完全无法缩小范围,只有分类体系能做到——调用者必须在 43 个类别中猜一个。而这恰恰是绝不能用来筛选的维度。
因此,尚待解决的问题比“排序很难”更为具体。面对一千种操作,我们需要的是调用者无需了解我们的分类体系也能说明的更多事实——一项操作需要多少个输入、它是否会改变几何形状还是只改变属性、输出中的要素数量是否与输入相同。这些都是操作的结构属性,可以通过代码核验,而非手动声明;它们还能区分词袋无法区分的操作对:spatial_join 与 overlay_layers、flow_accumulation 与 extract_streams。这项工作尚未完成;在此之前,唯一诚实的说法仍是测量结果:上面的保证适用于 78 种操作,而非 800 种。
条目必须如何编写,是一项已发布的规范,而不是惯例:
docs/catalog-entry-spec.md,并附有规范性
JSON Schema,每个条目都会在 CI 中依据它进行验证。每个字段的存在都有测量结果作为依据——包括两个测量结果为零、并据实记录的字段,因为只报告有效内容的规范不过是广告。
而可发现性是针对每项操作的契约,不是平均值。 五十个条目的目录整体 found@3 达到 90%,意味着有五个条目不可见,而平均值不会告诉你是哪几个。因此,我们会用每个可用条目自己的首个有效示例对其进行探测,并声明各自的 facets(test_discovery_contract.py 会对整个目录进行参数化测试,因此新增操作的那一刻就会纳入契约)。每项操作都必须满足两点:条目声明的 facets 绝不能让该条目被筛掉;该条目必须能到达调用方。
其排名不再是契约要求之一,移除这一要求正是目的。 契约过去要求条目排进前三。这看起来像是可发现性契约,实际上却是排名契约,而且有一个糟糕的特性:修复失败的唯一办法,就是改写条目,直到排名器满意。按这种方式调整的五十个条目,在我们编写的示例上的得分比其他人编写的请求高 19 个百分点——这个差距经过了测量,也正是本页公布的 70% 一夜之间变成 51% 的原因。若测试的修复办法是让文本适配评分器,它就会凭空制造出自己报告的数字。现在纳入契约的是确定性且由我们掌控的部分;返回结果集中的排名仍会测量,但不再导致构建失败。
旧版格式第一次运行时,确实证明了它的价值。centroid_layer 宣传的是*“为多边形图层标记点”*,排名却低于 point_on_surface。这个排名是对的:质心可能落在自身多边形之外,这正是 Argleton 陷阱 014——我们的目录推荐的,正是我们自己的测试套件要测量的缺陷。改的是示例,不是分数。
而当两个引擎毫无共识时,搜索会如实说明,而不是给出答案。 这是测量在我们自己的产品中发现的问题:当用户请求*"给我的会计师发一封电子邮件"时,嵌入引擎返回了 idw_interpolation,信心度和真正的答案一样高——这正是这一层要防止的错误,却悄无声息地发生了。相似度阈值解决不了这个问题,因为根本没有可划出的界线:"把这个 mp4 转成 gif"*的得分高于 20 个真实查询中的 16 个。真正能将两者区分开来的是,两个排名器毫无交集——有答案时,前三名平均重合数为 3 中的 0.90;没有答案时,则为 0.18。因此,目录无法归类的查询会返回 status: "unsure",同时附上两个引擎的猜测,以及一个能确定性缩小目录范围的问题:你有什么类型的数据。它会在 11 个无法回答的查询中触发 9 次,并在 20 个查询中抑制 1 个正确答案。
而当 facets 筛选后一个候选项都不剩时,它会指出是哪项声明导致的。 过去,候选项为零时会落入上面的分支,返回类似*“只剩 0 项操作,少到可以逐项阅读”的内容——这段话毫无意义,更糟的是,空候选列表会让 Agent 以为MapSmith 做不到这件事*。下方的发现日志在第一次真实会话中发现了这个问题:用户问*"这些地块各有多大面积",produces="answer" 将候选项全部筛掉了,但 measure_area 恰好能计算这些面积,并声明了 dataset:vector,因为它会将面积写入一列。因此,这种情况现在有了单独的回答:列出每项声明在不启用它时会留下多少个操作,按数量从少到多排列——这是算术,而不是排名。
低于选择阈值时,它不再拒绝,而是改为对交付的集合发出警告:order_is_weak。搜索仍在判断时,拒绝是合理的;而交出所有候选项并不算判断,因此这种分歧会重新成为有关顺序*的证据,而它从一开始就是唯一能证明的事情。
上述适用性过滤器会先对两个引擎运行,否则这项保证就只适用于其中一个,而且有一项测试明确验证了这一点。
接下来,无论是否有工具,都会运行。 大多数目录操作都有自己的工具;较新的操作越来越多地没有工具,而 run_operation(operation, arguments) 可按名称运行这些操作。这是有意为之:能力数量没有上限,但公开的工具列表有上限,因此目录可以比工具列表扩展得更快。执行任何操作之前,都会先根据目录检查参数——未知操作(使用同一套排序给出“你是否想找……”提示)、缺少参数或参数名称错误、类型错误、路径位于工作区之外——并且每种错误都有稳定的代码。执行过程与 execute_plan 走同一条路径,因此某项操作单独运行时与在计划中运行时不会表现不同。
两个引擎都嵌入完全相同的文档文本(catalog.document_text),因此两者之间的比较衡量的只有排序,没有其他因素。三个测试文件会将其余部分纳入测量,而非凭主观看法判断:我们自己的目录上的性能退化曲线、与 800 个真实相近操作的投影对比,以及针对每个条目的可发现性契约。这样,规模上限就变成了可观察的曲线,而不是某人猜出的一个数字。
我们之所以这样构建,而不是使用托管的嵌入 API,是因为确定性:否则工具发现就会变成网络调用,结果可能随时变化;对于同一个问题,Agent 明天找到不同工具,就无法复现,无论其清单怎么说。剩下的唯一网络访问是在首次使用时下载模型,并固定在指定的修订版本;此后,向量引擎就在本地运行,而未能完成下载的安装仍会使用 BM25,并且每个结果的 engine 字段都会说明是哪个引擎给出了答案。
使用自己的请求改进,同时避免模型漂移
这些百分比背后的 155 条请求由两个语言模型编写。它们是我们在没有用户参与的情况下能构建出的最佳集合,但并不是真正用户会提出的请求:真实请求会提到某人实际拥有的文件,以及他们所在领域实际使用的措辞。
因此,MapSmith 可以记录自己的请求。将 MAPSMITH_DISCOVERY_LOG 设置为文件路径,每次搜索都会写为一行 JSON,并附上随后执行的操作——查询、声明的维度、负责排序的引擎、交付的每个候选项,以及所选项在列表中的位置:
MAPSMITH_DISCOVERY_LOG=/data/discovery.jsonl # then work normally for a while
python benchmarks/log_to_cases.py /data/discovery.jsonl
对于需要人工查看而非管道处理的部分,我们提供了一个仪表板——见下文。
log_to_cases.py 会将这些行打印为与 tests/data/discovery_queries.json 格式相同的记录,并标出两个重要情况:排序结果没有将某次运行排在首位(答案已显示,但顺序有误),以及搜索后没有任何操作(目录未能满足某个请求)。它只会打印,不会写入。哪些记录成为测试用例,由人来决定。
这些操作都不会训练任何东西,这正是我们的设计。 从调用者的选择中学习的排序器,学到的是自己生成的排序:排在首位的操作被选择得更多,于是被学成正确答案,进而更努力地排在首位——最终得到一个无人质疑的自信答案,而这正是本产品旨在衡量的失败。模型版本保持固定,由黄金向量测试加以确保,因此明年同一个查询仍会得到相同答案。真正改进的是目录文本——某种措辞,或一个无法发挥区分作用的 distinguishes——并以他人可以阅读和撤销的差异形式呈现。这种循环并非次优之选:正是它将 found@3 从 18% 提升到 58%,并将交付率提升到 98%。
除非设置了该变量,否则日志功能处于关闭状态。日志只保存查询和操作名称,不保存其他内容(不包括数据集路径或参数);与 MapSmith 写入的其他路径一样,它受 MAPSMITH_WORKSPACE 保护,并且绝不会离开本机——没有任何程序会将其读回。你的查询描述了你的工作;请妥善对待该文件,并在不再需要时将其删除。
格式
| 格式 | 读取 | 写入 |
|---|---|---|
GeoParquet 1.0 / 1.1 — WKB 加 geo 元数据 | 是 | 是,所有路径 |
GeoParquet 2.0 — Parquet 原生 GEOMETRY/GEOGRAPHY 逻辑类型 | 是,包括完全不含 geo 键的文件 | 是,SQL 路径:run_sql 会将两个图层写入同一个文件 |
| GeoPackage、Shapefile、FlatGeobuf、GeoJSON,…… | pyogrio/GDAL 可以打开的任何格式 | 通过 GDAL |
| GeoTIFF / COG | 是 | [raster] 和 [whitebox] 引擎的输出 |
GeoParquet 2.0 将几何数据移入 Parquet 自身的逻辑类型,并使 geo 键成为可选项,因此“包含几何数据的 Parquet 文件”不再意味着一定有该键。若 CRS 仅存在于逻辑类型中,MapSmith 会从中读取 CRS——无论是规范默认值、authority 字符串、projjson:<key>,还是内联的完整 PROJJSON 文档,DuckDB 写入的正是这种形式。run_sql 会输出两个图层(geoparquet_version 'BOTH'),因此同一个输出文件既能满足原生支持 2.0 的读取器,也能满足 GeoPandas 1.x;GeoPandas 写入路径仍使用 1.x,因为 GeoPandas 1.1 将 schema_version 限定在该版本。
有一种声明会被刻意拒绝,而不是靠猜测处理:srid:<n>。规范将其定义为数字标识符,却没有指定 authority——规范自己的示例是 srid:0——因此将其读取为 EPSG:<n>,等同于凭空指定一个坐标系并将其记作事实。
选择技术栈,并且绝不悄然切换
MAPSMITH_STACK 会在启动时一次性选择地理处理技术栈。默认值为
opensource——GDAL、GeoPandas、DuckDB、Whitebox——无需许可证。esri 则通过子进程和文件调用安装了 ArcGIS Pro 的计算机上的 ArcPy,因为 ArcPy 位于 Pro 自己的解释器中。MapSmith 不会附带 ArcPy 的任何部分,也不会为查看它而获取许可证:当前会话可访问的内容取自安装程序留在磁盘上的元数据,server_info 会报告这些信息,因此调用者可以在围绕它规划五个步骤之前就了解情况,而不是等到第一次失败时才发现。
选择技术栈之所以有意义,关键在于遇到边界情况时会发生什么。当所选技术栈无法完成某项操作时,MapSmith 会说明以下三种情况中的哪一种属实——不存在这样的工具、当前许可证级别不包含此功能,或操作需要在线服务——因为这三种情况会引出三种不同的决策;用一个词概括它们,最终就什么决策也做不了。如果存在可用路径但无法运行,清单会注明实际生成数值的引擎,以及首选引擎无法运行的原因:悄然被另一个引擎替换的引擎,留下的记录虽属实,产生的数值却无人选择。
目前只有一项操作会进行路由:buffer_layer。 无论 MAPSMITH_STACK 的设置是什么,其他所有操作都使用开源技术栈。这是当前接线方式的状态,而不是设计本身的特性。我们在此写明这一点,是因为另一种做法会构成缺陷:表格声明了三条路由,但只有一项操作会查询路由器,这意味着请求指定技术栈后,仍有两项操作会在其他地方运行,而清单中没有任何说明。这一问题在本次发布前已修复,方法是缩短表格,而不是修改这句话。
这并非两种比较。MapSmith 会调用你已安装的技术栈,而本仓库不会发布任何引擎的评分;唯一例外是 Argleton 公开评测的引擎。它也并不意味着等价性:我们在同一个测试样例上对另外两项操作进行了双栈测量,并有意不为它们配置路由,因为几何结果一致并非全部——在融合操作中,另一种技术栈会丢弃所有属性,因此某个先融合再读取列的工作流,会在一种技术栈中找到该列,在另一种中却什么也找不到。应先记录这种差异,再考虑提供相应路由。
验证,贯穿输入与输出
每个写入数据集的工具都会在其旁边写入 <output>.provenance.json,并验证自身的工作——CRS 是否一致、几何是否有效、栅格尺寸,以及数量和范围不变量——先将结果记录到清单中,再抛出任何错误,以便审计记录在错误发生后仍然留存。如果某次运行在写入前失败,而路径上仍留有更早运行生成的数据集,该次运行的记录会保留,并将自己的记录写入 <output>.failed.provenance.json;错误信息会指出该文件。
输入时也会进行验证。在操作处理你的数据之前,MapSmith 会检查那些会产生看似合理垃圾结果的问题:没有 CRS 的输入会直接遭到拒绝,因为对单位未知的数据进行度量运算,就会得到看似确信无疑、实则错误的答案;空输入,或范围不可能重叠的两个图层,则会返回带有提示的明确警告——显示在工具结果中,而不只是清单里,以免 Agent 以为操作成功。(快速连接路径 DuckDB 和 SedonaDB 只会接收 CRS 已知且彼此一致的输入;它们会验证输出,并诊断空连接结果。)
几何在机械层面损坏的输出——通常是从无效输入继承了无效性——会以确定的方式修复:最多执行两轮 make_valid,先写入临时文件,只有全部完成后才替换原文件;如果重写可能导致数据丢失,则跳过修复,而不是冒险处理(多图层 GeoPackage 会被拒绝重写——extract_layer 会将你指定的图层复制到独立数据集中,并在清单中列出原容器和剩余图层的名称)。每次尝试都会记录在清单和工具结果中,因为修复后的输出绝不能看起来像是第一次就正确的输出。需要判断的问题绝不会被“修复”:空结果,或因距离设置错误而被侵蚀掉的几何,都会作为带有提示的警告返回,供 Agent 采取行动。
清单还可以说明生成这些数值时所用的配置——即清单规范的 3.8 节 environment;没有内容可记录时则为空。这一设计的实际缘由是:GeoTIFF 及其旁边的 .aux.xml 文件可能声明不同的地理配准,而 GDAL 按照文档规定的设计优先采用旁置文件,因为这样就能覆盖人们知道有误的地理配准。这两种读取结果都是库完全按预期运行的结果;在一个测试样例中,同一个文件会得到相差四倍的面积,且原点偏移了 100 公里。上游没有任何问题可修复,唯一要做的就是说明情况;因此,describe_dataset 在栅格有两个来源时会报告两者,而十二种直接读取栅格网格的操作会改为拒绝处理——分区统计、重采样、裁剪、重新分类、波段运算、重投影、波段提取、波段统计、定位极值单元、点位采样、高程剖面和视线分析——并列出两种读取结果以及如何选择。
描述不同于计算:具有两种地理配准的文件应该被说明,而不是交给人掷硬币。这是在另一个层面上的多图层拒绝处理(#29)——格式默认替调用者回答了一个调用者从未提出的问题。
地形和采样操作目前还不会拒绝处理;而“任何进行计算的操作”这种说法,会重蹈本版本已经发现过一次的覆诺之失:地形引擎通过不同路径检测到同一个文件,因为它会将自身读取的网格与 GDAL 的读取结果进行比较,并在两者不同时停止,但提示信息既没有指出旁置文件,也没有说明如何解决。对栅格进行点位采样则完全检测不到这个问题。将拒绝处理扩展到其余栅格操作,已列入下方路线图。
在聊天中查看结果

preview_map 会在聊天内的交互式地图面板中渲染图层——可平移、缩放、切换图层,并在几何图形旁边查看每个图层的溯源卡片(操作、引擎,以及三种如实标注的状态之一:verified ✓、verification failed,或未运行关键检查时显示的 not verifiable)。已在 Claude Desktop 上进行实地测试;它可在任何实现了官方 MCP Apps 扩展的客户端中渲染,而不支持该扩展的客户端则会通过同一调用返回结构化数据预览。
该面板是独立运行的——不使用 CDN、捆绑库或遥测——只有一个对外请求,并在此明确说明,而非隐藏起来:OpenStreetMap 背景瓦片。它们会显示你正在查看的地图范围(绝不会暴露你的数据);如果宿主阻止请求,面板就会改用纯色背景。预览会有意损失部分信息(简化几何图形、限制要素数量):作为记录的数据集仍保存在磁盘上,并附带其清单。
计划:在执行前拒绝格式错误的分析
在 GISAgentBench 中——该基准涵盖 128 个 GIS API 上由 349 个从业者提供的任务——按严格评分标准,表现最佳的前沿 Agent 只能完成 32.7% 的任务。失败主要源于规划缺陷:在失败的运行中,28.3% 缺少操作,18.4% 的操作顺序错误(多标签统计,因此最多约 47% 涉及规划错误);相比之下,参数错误占 7.8%。MapSmith 在成本最低的环节着手解决这一问题:Agent 提交一个类型化计划,静态验证会在执行任何操作之前拒绝未知操作(并提供建议)、缺少的参数、前向引用、缺失的输入文件和 CRS 不适用的步骤,并返回 Agent 可以据此修复的机器可操作错误代码。
**这里也说明它做不到什么,因为之前的标题曾声称它能做到。**验证只检查结构。它读取计划的形式,而非含义,因此格式正确但回答了错误问题的计划也会通过。2026-09-14,在本仓库自己的示例中对此进行了演示:当被要求先裁剪地块、再缓冲河流时,MapSmith 确实照做了。中间结果是一个以某个地块角点为中心、半径为 1.5 km 的圆,它继承了该地块的名称,因此在下游看起来像一个地块;每一步都报告为已验证,而在这个测试样例中,最终表格与正确结果完全相同——缺陷在输出中悄无声息。直到那次运行之前,标题写的是“拒绝错误的分析”;现在改成了格式错误,因为这才是事实。发现另一类错误仍是一个开放问题,这也是 正确性测试套件 存在的原因——不过其中的陷阱都只涉及单个步骤,所以它目前也发现不了这个问题。
{
"goal": "buildings within 300 m of rivers",
"steps": [
{"id": "buf", "operation": "buffer_layer",
"arguments": {"input_path": "rivers.gpkg", "distance_meters": 300,
"output_path": "rivers_300m.parquet"}},
{"id": "cut", "operation": "clip_layer",
"arguments": {"input_path": "buildings.parquet", "mask_path": "$buf",
"output_path": "at_risk.parquet"}}
]
}
"$buf" 会使用步骤 buf 的输出;引用只能指向前面的步骤,因此计划从结构上保证无环。validate_plan 还会根据真实输入文件模拟每个中间数据集的 CRS。随后,execute_plan 会执行整个链,并提供逐步骤溯源信息以及计划级清单(<output>.plan.json),为生成结果的确切计划生成指纹。
运行隔离
每次工具调用的每个路径参数都会拒绝 UNC 主机和 NTFS 备用数据流,并且在此之前不会触及文件系统(在 Windows 上,即使只是检查 UNC 路径是否存在,也会与攻击者指定的主机通信)。自 0.2.2 起,远程和虚拟路径形式——GDAL /vsi*、https:// COG——默认都会被拒绝,需要设置 MAPSMITH_ALLOW_REMOTE=1;无论该设置为何,工作区都会拒绝这些路径形式(详情见下文)。经过验证的计划在设计上限制更严格,无论是否选择启用,都拒绝所有非本地路径形式。
设置 MAPSMITH_WORKSPACE=/data,将服务限制在一个目录内:
- 每个工具的每个路径参数都必须解析到工作区内(在 MCP 边界处检查,并在计划验证时再次检查,且使用稳定的错误代码);
- 在 GDAL 打开输入文件之前,会先从其字节内容中读取输入声明的坐标系,因为 GDAL 会打开它指定的任何网格文件,涵盖 MapSmith 接受的所有格式;对于 MapSmith 无法识别的格式则会拒绝——包括 netCDF、HDF、JPEG 2000、ERDAS IMG 和 GRIB 栅格,直到 MapSmith 能自行读取其坐标系(自 0.10.1 起);
run_sql的 DuckDB 连接在引擎本身中受到沙箱限制,因为 SQL 文本无法通过文本路径检查来防护:文件系统仅允许访问工作区(allowed_directories+ 关闭外部访问,这也涵盖基于 GDAL 的ST_Read),内存和临时磁盘空间均设有上限(MAPSMITH_DUCKDB_MEMORY,默认 4GB;MAPSMITH_DUCKDB_TEMP_LIMIT,默认 8GB),并锁定配置。DuckDB 的空间扩展自带 PROJ,文件系统无法检测到它,因此语句会在执行前用 DuckDB 的解析器读取:只允许SELECT,并且所有坐标系(ST_Transform、ST_SetCRS)和数据集路径(ST_Read、read_parquet)都必须是字面值,且通过与工具参数相同的检查;通过 GDAL 时,只允许 GeoPackage、shapefile 和 GeoParquet,并且 MapSmith 会先读取它们的坐标系(自 0.10.0 起;SECURITY.md说明了此举所弥补的漏洞)。
MAPSMITH_DISCOVERY_LOG 是 MapSmith 会写入的三个路径之一,且没有任何工具参数会指定这些路径。无论是否有人请求,另外两个路径都会被写入——DuckDB 的扩展目录,以及引擎在工作区内使用的临时目录——SECURITY.md 在其 Workspace containment 承诺中列出了这两个路径;此路径默认关闭,只有在设置后才会启用,而 SECURITY.md 会在 What MapSmith records 一节中说明它。此路径会经过与工具参数相同的检查:若位于工作区外,就会被拒绝;拒绝时会禁用日志记录,并在 stderr 中说明,而不会导致触发它的搜索失败。
没有工作区时,文件访问有意不受限制——对于在自己的文件上运行的本地 stdio 服务而言,这样做没有问题——而计划验证会将 run_sql 步骤标记为 SQL_NOT_SANDBOXED 警告。两种模式下的代码执行都已禁用;自 0.4.0 起,禁用代码执行的层级也放在了正确的位置:语句中的 INSTALL 和 LOAD 会被直接拒绝,因为 INSTALL 会通过 HTTPS 获取原生二进制文件,并在本进程中运行由模型编写的 SQL。0.4.0 之前,只禁用了隐式形式;一次审计安装了 DuckDB 的 aws 扩展,并通过工具结果读取了这台机器上的真实云凭据——完整经过详见 SECURITY.md,其中也说明了为什么现有的四层防护都未能发现它。已加载的扩展仍可正常使用,包括 spatial;如需获取其他扩展,请在 Agent 无法访问的位置指定:
MAPSMITH_ALLOW_EXTENSIONS=postgres,azure,并将其设在启动服务的进程环境中。在此基础上,社区扩展仍保持禁用(shellfs 会将文件名转换为 shell 命令),无签名的扩展会被拒绝,DuckDB 的 HTTP 和 S3 文件系统会被禁用,且配置会被锁定。
除非你开放网络,否则网络也会被隔离。 默认情况下,路径参数和 run_sql
文本中都会拒绝远程和虚拟形式——GDAL /vsi*、https:// COG——因为路径是模型而非你写入的:否则,第三方数据集只要携带“更新后的图层位于 https://evil.tld/x.gpkg”,就足以让 GDAL 在进程内解析攻击者选择的字节。设置 MAPSMITH_ALLOW_REMOTE=1 即可允许这些形式——云原生数据是实际用例,因此此能力只是受控开放,并未移除。工作区无论如何都会拒绝它们,因为路径限制和“获取模型指定的任意 URL”无法兼得。测试套件通过统计回环服务器上的请求,验证每一种分支(tests/test_duckdb_sandbox.py)。完整的威胁模型——以及明确不涵盖的内容——见 SECURITY.md。
细则会影响部署方式:路径隔离机制假设工作区文件系统只有一个可信写入者(路径会在检查时解析,因此不涵盖其他本地进程替换符号链接的情况);DuckDB 空间扩展由 MapSmith 自行通过 Python API 为每个环境获取一次,而非通过任何语句获取,因此在隔离网络的机器上,请先预装扩展(python -c "import duckdb; duckdb.connect().install_extension('spatial')"),再锁定网络;此版本的 HTTP 传输没有身份验证,因此请将其限制在回环接口或可信网络中。若要实现真正的隔离,请运行容器,并且只挂载你希望它能看到的数据。
仪表板
这个项目所了解的一切都会在某处计算,其中大多数内容只打印一次,之后便不复存在,这就是一个数字如何变成一项断言。benchmarks/dashboard.py 会将这些内容汇总到一个独立的 HTML 文件中——无需 CDN、无需字体、无需分析工具,断网也能使用:
python benchmarks/dashboard.py --log /data/discovery.jsonl --argleton ../argleton
- 操作 — 每个条目,以及调用者是否真正能找到它。进行两次询问:一次只用文字,一次使用调用者已知的类别。三种结果分别保留——排名、答案中不包含该条目,以及搜索因两个排名器没有共同结果而拒绝作答。将第三种情况并入第二种,是此页面最初对自身的误判,导致十个有效操作被标成失效。
- 搜索质量 — 两个排名器的类别消融测试,以及目录增长时的性能退化曲线;正是这项测量让嵌入引擎成为依赖项。
- 陷阱 — Argleton 的各个家族,以及每个引擎如何处理它们,包括 MapSmith,且不刻意美化结果。使用
--argleton <path>时,它会读取一个检出版本并显示各家族的详细信息;不指定时,则会回退到随附的引用并说明这一点。 - 回答开放问题 — 两位模型标注者为请求指定了不同操作的情况,以及发现日志记录下来的案例,都可以通过点击作答。你每次作答后,百分比都会根据答案重新计算,并以 JSON 格式导出。这就是已发布数据所依据的开放问题:在没有唯一正确答案的任务中,两位标注者有 70% 的时间意见一致,这就是准确率的上限;要超越它,唯一的办法是由做过这项工作的人来判断。
- 趋势 — 每次生成都会在页面旁追加一行,因此这些数字构成一组序列,而非单个快照。连续相同的行会被丢弃:重复构建五次不应凭空制造趋势。
它确实是一个快照——新的操作或陷阱会在再次生成时出现——而且重新生成会保留所有已有答案,因为答案是根据每个问题的文本存储的,而非根据问题的位置存储。
我们衡量了这是否真的有帮助
关于 Agent 性能的说法很容易提出,因此 docs/benchmarks.md 报告了在 GABench 上进行的 A/B 测试——该基准包含 57 个可执行 GIS 任务,覆盖一个拥有 133 个工具的服务,并由确定性评估器评分——唯一变量是:求解器运行前是否验证 Agent 的类型化计划。GABench 由 Bo Yu 等人开发(arXiv:2604.13888);此处的数量统计基于我们运行时检出的版本,与论文中的统计不同,差异已在 docs/benchmarks.md 中并列说明。
坦诚地说,主要结论是结果为零,这对前沿模型和小型模型都成立,而有意思的是原因:
| A 组(无门控) | B 组(有门控) | |
|---|---|---|
| Sonnet 5 — TAO / PEA | 0.824 / 0.430 | 0.781 / 0.425 |
| Haiku 4.5 — TAO / PEA | 0.660 / 0.320 | 0.714 / 0.366 |
Haiku 看起来像是明显胜出,直到你发现门控只对 57 个计划中的 4 个生效,而它从未触及的 53 个任务也出现了同样幅度的变化:总体差值属于运行间方差,而测量这一噪声底限(单次重复中每项指标为 2–5 个点)才是可复用的结果。剩下的结论范围更窄——在它确实修复的计划中,工具选择提升了 +0.19 TAO——这也指出了失败真正发生的地方:所有组的 PEA 都在 0.4 左右,也就是执行时参数错误、输出缺失。这正是 MapSmith 在执行边界强制执行计划,并在运行时验证输入和输出,而不是给一个自行发挥的 Agent 提建议的原因。
随后又进行了三个组的测试,衡量 MapSmith 实际发布的配置——计划被强制执行,验证和执行之间不允许自行发挥——共 375 次运行。结果两面兼具:与自行发挥的求解器相比,强制执行使得自身分数的复现稳定度提高了 3–18 倍,但准确率并未胜出(工具选择持平,排序则明显更差)。其中一组测试还推翻了本页在两组测试之前发布的一个结论;我们保留了这项更正,而没有将其悄悄改掉。
测试框架位于 benchmarks/gabench-ab/,其中包括拆解我们自己胜果的 split_analysis.py,以及将每个差值与实测噪声底限进行比较的 rep_analysis.py。
测试套件在 MapSmith 中发现了什么
读者对这一节最该保持怀疑,因此它被单独列为标题,而不是像以前那样放在路线图中的一行。这里的一切都是由我们自己的评估器给出的评判。
Argleton 是一个正确性测试套件,用来捕捉现有基准都遗漏的失败情况——结果错误,却被报告为成功。它由我们开发,托管于独立组织,采用 Apache-2.0 许可,而不是放在本仓库中,因为如果评估工具和被评估对象属于同一个项目,只需一句话就能轻易将其质疑。真值采用闭式形式,评估器中不使用模型,测试夹具重新构建而非直接引入。
成绩。 在当前这次运行中——覆盖全部二十九个类别——MapSmith 在所有陷阱题中都答对了:31 个陷阱题中有 0.00 个无声错误,没有遗漏;相比之下,对同一批库进行粗心组合的得分为 0.9032。2026-08-30 至 2026-09-15 的运行结果也显示 0.00,但其中一道题答错了:一个像元中心对齐的 DEM。它的真值是根据一个关于 GDAL 的前提推导出来的,但 GDAL 文档中的说法恰恰相反;MapSmith 按照该前提构建,而双方又都把半个像元的偏移修正了一次。2026-09-25,我们发现了这个问题:某个 Copernicus DEM 的样本按其自身文档说明,落在整角秒位置上;勘误重新评分了所有受影响的运行,修复随 0.7.0 版本发布;此后的每次运行都对其进行了测量。31 个陷阱题中,有一道并非作答,而是拒绝处理:栅格及其旁边的辅助文件声明了不同的地理参考信息,两种解读都符合 GDAL 文档,正确的做法是停止并说明情况,而不是任选其一。这个类别于 2026-08-31 加入,也就是二十七题清单定稿后的第二天早晨;这正是清单现在可以为 environment 命名的原因(见变更日志)。
第二十九个类别于 2026-09-02 加入;一旦了解情况,就不难理解它为何看起来反常:一块测绘地块的地役权环与其边界环方向相同。Shapefile 不记录嵌套关系,因此哪个环是洞只取决于环的方向,别无其他——地役权环被当作第二个外壳返回,其面积被加上,使所有者的面积虚增了 6.9%,而边界框和 CRS 都正确,也没有任何警告。MapSmith 会修复这一问题并记录修复,这正是结果正确的唯一原因。
比成绩更重要的发现,关乎我们自身:在首次运行中,那个 0 与 MapSmith 的验证毫无关系——该陷阱题有七项检查通过,但没有一项会检查结果是否正确。来源清单记录了执行过什么操作;它并不能证明操作正确,而本 README 过去承诺运行时“禁用验证”,让人误以为它可以做到这一点。实际上并不存在这种开关,我们也不会添加。
这次运行还区分了 MapSmith 凭自身能力通过的测试,以及并非如此的测试。CRS 不匹配时的连接和将英尺误作米,体现的是它自身的严谨性;Web Mercator 测试通过,是因为默认设置(除非指定平面计算,否则地表面积采用大地测量法),而非谨慎处理;TIFF 预测器测试仍靠 rasterio。datum-ballpark 测试是最新的,也是最不光彩的:MapSmith 在 2026-08-26 未能通过该陷阱题——结果相差 74 m,而来源清单却记录了成功的重投影——如今通过测试靠的是修复,而非原有行为。那次失败的运行仍然公开发布。
它已带回七项缺陷,这正是我们将测试套件置于外部所希望得到的成果:
- 上述
datum-ballpark失败——偏差 74 米,清单却记录着重投影成功,是其中最严重的一例,因为输出看起来没有任何问题; - 多图层容器被静默解析为默认图层,结果返回了 4 个要素,而实际应为 31 个(#29,在陷阱发布前提交——现在可以通过
extract_layer避免这种情况); - 一个南向上的 DEM,其地理参考在读取时丢失,因此 5.71° 的坡度被读成了 45°,而且全部五项验证检查都通过——坐标系保留了下来,只有地理变换丢失了;
- 对同时包含线和多边形的图层求总量时,处理厂的围栏线被悄无声息地加到了 2 公里的管线上,而每一行单独来看都正确无误;
- 两个探测返回了
unsupported,因为没有任何方式能指出栅格最低像元的位置; - 三个探测返回了
unsupported,因为 MapSmith 根本没有面积计算操作——measure_area正是因一个陷阱而加入的,这里还包含第一项检查:它检查的是数值是否正确,而不只是操作是否运行; - 第七个探测则因自身有误而被测试套件退回。MapSmith 发布时无法处理网格配准这一类情况,而针对它构建的修复基于陷阱的前提:GDAL 会原样保留点配准文件的地理变换,因此读取器必须将每个位置移动半个像元。GDAL 的文档却说明情况相反(RFC 33)——GDAL 已经移动过位置——而 MapSmith 又将此类文件的每个采样点向西北移动了半个像元,其中包括 Copernicus DEM;陷阱却将此判为通过。一个 Copernicus DEM 根据其自身文档,采样点落在整角秒上,于 2026-09-25 揭示了这两个问题(勘误)。现在,无论标签内容如何,位置都取自 GDAL 的地理变换;而确实会误读该标签的地形引擎,则会收到面积副本,而不是被拒绝。
Notebook 图库
examples/ 中有四个可执行的演练:带溯源清单的验证缓冲区与裁剪、使用真实的 520×520 USGS 圣海伦斯火山 DEM 进行地形与水文分析、在执行前拒绝一个故意写错的计划并随后修正,以及在 Monte Baldo 使用 Copernicus 数据——一个因单位为度而被拒绝用于坡度计算的 DEM,随后经过重投影;以及一个 Sentinel-2 NDVI,其反射率偏移量由目录声明,且已包含在像元中。地形 Notebook 还展示了如何应对现实问题:该 DEM 使用标准 TIFF 预测器存储,但 Whitebox Workflows 2.x 读取时不会撤销该预测器(上游报告),因此 MapSmith 会检测到这一点,先转换输入,并在清单中披露此变通方案。
架构
AI agent (Claude / ChatGPT / Copilot / your app)
│ MCP (stdio local · Streamable HTTP remote)
▼
┌─────────────────────────────────────────────┐
│ MapSmith server │
│ · semantic tools + operation catalog │
│ · parameter validation, CRS discipline │
│ · provenance recorder (lineage manifests) │
├─────────────────────────────────────────────┤
│ Engines │
│ · vector: GeoPandas/Shapely (built-in) │
│ · SQL/analytics: DuckDB Spatial (built-in) │
│ · heavy joins: SedonaDB ([sedona] extra) │
│ · zonal stats: exactextract ([raster]) │
│ · terrain/hydro: Whitebox NG ([whitebox]) │
│ · qgis_process / GRASS sidecar (roadmap, │
│ GPL-isolated via subprocess) │
└─────────────────────────────────────────────┘
何时不应使用 MapSmith
- 你现在就需要经过身份验证的远程服务。 此版本的 Streamable HTTP 传输没有身份验证:任何能够访问端点的人,都可以针对进程可见的所有内容运行每个工具。仅限环回地址或受信任的网络 (SECURITY.md)。
- 你想要一个可运行任意 Agent 代码的沙箱。 MapSmith 会限制路径和 SQL 引擎;目前还没有代码执行工具,而且路径隔离不等于容器。
- 你需要制图功能。 没有样式设置、没有布局,也没有打印编辑器。输出是数据集,另附一个有损的只读预览面板——不是可发布的地图。
- 你的数据存储在数据库中。 MapSmith 读写文件(GeoParquet、GeoPackage,以及 GDAL 能打开的任何文件)。没有 PostGIS 引擎,也没有数据库目录——
[postgres]extra 用于可选的作业账本,而非数据。 - 你的数据存储在对象存储中。 从 0.2.2 开始,除非设置
MAPSMITH_ALLOW_REMOTE=1,否则远程路径和虚拟路径会被拒绝;在工作区中,无论该设置为何值,都会被拒绝——容器默认就在这种配置下运行。DuckDB 自带的 HTTP 和 S3 文件系统在所有模式下都会保持关闭,因此即使选择启用,read_parquet('s3://…')也无法运行:请先下载数据,或在取消隔离且开启远程读取的情况下运行。 - 你想要桌面 GIS 的全部功能。 目前有 78 个操作,目录会告诉 Agent 哪些功能尚不存在,而不是让它自行发挥。约 900 个 QGIS Processing 算法已列入路线图,目前尚未提供。
- 你认为计划验证能让能力较弱的模型变强。 我们自己的 A/B 测试表明,在会自行发挥的求解器之前进行建议性验证,在整体层面几乎没有作用;而 MapSmith 随附的强制执行配置,事后测得的准确度也没有胜过它。强制执行带来的是可复现性 (具体数据)。
- 你希望我们调试你本地的地理空间工具链。 仅支持 Docker,或在 wheel 可用时使用
uvx;我们有意不支持手动构建的原生 GDAL 环境。
路线图
- 分区统计(exactextract,精确分数覆盖率)
- Whitebox Next Gen 适配器:阴影地形图、流量累积、流域(内存中运行,开放层级)
- 类型化分析计划:根据操作注册表进行静态验证,并在执行前模拟 CRS 流程
- 运行时验证:输入前置条件、工具结果中附带提示的警告,以及记录在清单中的有界确定性修复
- MCP Apps 聊天内地图面板,附带来源卡片(自包含,可在默认主机沙箱中运行)
- GeoParquet 2.0:读取 Parquet 原生几何类型(包括没有
geo键的文件),通过 SQL 路径写入两个图层——GeoPandas 提升其schema_version上限且 2.0.0 不再是候选发布版后,将支持 GeoPandas 写入路径
接下来将按我们计划的顺序推进。带有链接的项目附有书面规范——没有规范的路线图条目只是愿望,因此其余条目会在开始实施前补上规范:
-
一套用于发现所有现有基准测试都会漏掉的失败情形的测试套件——结果是错误的,却被报告为成功。这套测试确实存在,但不在这里,也不是由我们评分:Argleton 位于自己的组织下,采用 Apache-2.0 许可证。它在 MapSmith 中测量的内容,包括它提交的七个缺陷,已在专门章节中说明。#25 已在 Argleton 中关闭,而不是留在这里保持开放。
-
Agent 循环修复:将手动验证失败作为结构化、可操作的错误反馈给 Agent,并在清单中记录有上限的重试次数。我们的实测数据表明,运行时错误消息是有效的信息传递渠道。
-
自带规则的工具契约:强制执行并明确说明参数约束;错误消息指出具体规则,而不只是描述违反了什么。这是我们基准测试工作中唯一一次让某项指标超出噪声阈值的干预措施。
-
为一连串请求而非单次调用提供项目简报。 三分之一的实际请求并非单个操作:在我们自有数据集中最难的五十个请求中——即两位模型标注员意见不一致、于 2026-09-01 手动作答的那些请求——十六个的答案或站得住脚的第二种答案是“这是一连串操作”。对这类请求,返回一组候选项并不合适:没有任何候选项能完成用户所要求的事情。因此,答案应当是一份简报:需要完成哪些事项、按什么顺序、这台机器上有哪些引擎,以及运行之前调用者需要做出哪些决定——例如面积插值中选择广义方法还是强度方法、请求中未提及但法规规定的缓冲区宽度,以及梯度的基准长度。如今,这些问题会逐一出现,并导致拒绝执行。
计划中有两项约束,不是可有可无的细节。简报必须是对已验证计划的呈现,绝不能是计划的前言:先制定计划,再验证计划,最后叙述已验证的计划;简报中的每句话都必须能追溯到某个已声明的字段,因此,无从追溯的说法就是有人凭空编造的,而这可以由机器检查。MapSmith 不会安装任何东西:简报会说明缺少什么并打印命令,由用户来运行。原因有明确的时间背景——请参阅 SECURITY.md,了解为何自 0.4.0 起,两种模式都会拒绝 SQL 语句中的
INSTALL。经授权后自动安装的服务,本质上只是多了一个同意对话框;而同意来自刚读完一段由请求同意的进程撰写的、有说服力的文案的人。 依赖上面的条目:简报会读取工具声明的规则,因此没有这些规则时,可推导的信息就更少。 -
将卫星嵌入作为一等输入:按区域生成嵌入向量(多波段分区统计)以及与参考位置的相似度栅格,基于开放的 AlphaEarth 年度数据集(CC-BY 4.0 COG)。在栅格上执行确定性运算——MapSmith 不进行模型推理——并在清单中记录瓦片、年份和参考向量
-
远程认证模式(在现有 Streamable HTTP 传输上使用 OAuth),以及通过 MCP Tasks 报告长任务进度。这项工作将解决 SECURITY.md 明确声明的一项限制:目前 HTTP 传输没有身份验证
-
坡度和坡向(Whitebox,已通过闭式测试;拒绝使用地理 CRS 的 DEM)
-
河网提取(Whitebox,基于流量累积网格;清单中记录阈值及其单位)
-
更多地形与水文功能:曲率(六种类型,必须指定类型,因为剖面曲率和水平曲率回答的是相反的问题)、流向(d8/rho8/dinf/fd8,并将方向代码表格写入清单——引擎自己的手册把默认表写反了,因此仅记录名称还不够)、欧氏距离和 IDW 插值
-
对每一项栅格操作都拒绝处理地理参考不明确的情况,而不只是直接读取网格的十二项操作。这三项采样操作于 2026-09-02 添加——
sample_raster_at_points是产品中最严重的缺口,因为同一个 DEM 在没有旁车文件时会返回 10.0 和 30.0,有 40 m 旁车文件时则会返回 2.0 和 6.0;五次运行都没有任何警告。剩下的是地形引擎:它会因不同原因在同一文件上停止——它对网格的解读与 GDAL 不一致,而且报错信息既没提到旁车文件,也没说明如何解决 -
地图面板:使用 MapLibre 渲染矢量数据,并将面板导出为可自行托管的独立 HTML 文件(已自带栅格 OSM 瓦片)。不提供托管的查看器——MapSmith 在你的机器上运行,我们不想负责维护你的地图
-
不在计划中,并已于 2026-09-01以此为由关闭:用于处理长尾需求的沙盒代码执行工具。类型化计划也能带来同样的效率提升,而且在运行任何内容之前,可以给出明确理由并拒绝执行;由模型编写的脚本虽然仍让引擎负责运算,却把组合方式——引擎、顺序、单位、CRS——交给了未经验证的文本,最后生成的清单只会如实描述库,却对做出决定的部分只字不提
-
QGIS Processing 旁车程序(子进程隔离):约 900 种算法。这份列表中规模最大的项目——为外部进程映射参数并处理错误,不是一下午就能完成的事
许可证与项目
- MapSmith 服务和引擎:AGPL-3.0-or-later(参见 LICENSE)
- 客户端 SDK 和工具架构定义(未来的
sdk/):Apache-2.0
你可以永久免费自托管 MapSmith。如果你修改了它并将其作为服务提供,AGPL 要求你分享所做的更改——或者联系我们洽谈商业许可证。
目前尚未获得任何资助。funding.json 采用 FLOSS/fund 格式,列出了曾申请资金的两项工作:一套公开的地理空间陷阱测试集,附有可手工计算的答案;以及一份其他工具可实现的来源清单规范。两项工作最终都在没有资金支持的情况下完成,并已存档且附有 DOI;这些条目仍保留在文件中并标记为非活跃,以便将预估与实际成果进行对照。
发行说明见 CHANGELOG.md,贡献方式见 CONTRIBUTING.md,漏洞报告方式见 SECURITY.md。“MapSmith”是 MapSmith 项目的商标——参见 TRADEMARKS.md。动态: LinkedIn · X · Bluesky。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。