一个 GIS 库想接大模型,先要回答三个问题:函数怎么变成工具?工具怎么暴露给模型?破坏性操作谁来把关?leafmap 答一遍,geemap 答一遍,QGIS 插件再答一遍——每个库都在重复造轮子。opengeos 团队的答案是:把这些活收进一个库。这就是 GeoAgent。本文用 DeepSeek 把它完整跑了一遍,附上核心代码和实测日志。

目录

一、为什么需要“Agent 层”

1.1 每个 GIS 库都在重复造轮子

1.2 GeoAgent 的解法:三件套

1.3 轻核心

二、DeepSeek 接入实测:从装到跑通

2.1 安装与配置

2.2 接入必做:mcp 版本处理

2.3 基础对话:最小可用代码

2.4 工具调用:核心验证

三、安全机制实测:默认拒绝 vs 一键放行

3.1 机制怎么工作

3.2 四场景实测

3.3 结论

四、工具面盘点:full 25 个 vs fast 13 个

4.1 leafmap 实时地图绑定

4.2 fast mode:怎么裁工具面

4.3 更多地图功能实测

4.4 图层管理与视图控制实测

4.5 chat 端到端:模型自己编排工具

五、边界与坑

5.1 save_map 工具不兼容

5.2 地图渲染依赖 Jupyter

5.3 其他注意点

5.4 数据加载族的坑

六、判断框架:什么场景该用

6.1 适用

6.2 不适用

6.3 收尾


一、为什么需要“Agent 层”

1.1 每个 GIS 库都在重复造轮子

GIS 生态的库各有一摊 API:leafmap 管交互地图、geemap 管 Google Earth Engine、geoai 管遥感解译。要接大模型,每个库都得自己实现“函数 → 工具定义 → 调用执行”这一整套。重复劳动不说,能力还参差不齐:有的库工具定义潦草,有的压根没有安全把关。

1.2 GeoAgent 的解法:三件套

GeoAgent 用三个东西解决:

① @geo_tool 装饰器——给函数加一行装饰,它就成了 Agent 工具,schema 和 docstring 自动带上;

② GeoToolRegistry——工具元数据注册表,哪个工具是破坏性的、哪个是长任务,登记得明明白白;

③ for_leafmap / for_qgis 工厂——把工具绑定到“活对象”,地图或 QGIS 会话直接变成 Agent 的操作界面。

1.3 轻核心

底层构建在 Strands Agents 上。核心安装只有 strands-agents 和 pydantic 两个依赖;leafmap、STAC、NASA Earthdata 全是可选 extras,按需加载,不用为用不上的一百个依赖买单。

二、DeepSeek 接入实测:从装到跑通

2.1 安装与配置

环境:Windows、Python 3.12.10。一条命令装完,配三个环境变量:

pip install "GeoAgent[litellm]"

# 环境变量:

# LITELLM_API_KEY   = <你的 DeepSeek key>

# LITELLM_MODEL     = deepseek/deepseek-v4-flash

# LITELLM_BASE_URL  = https://api.deepseek.com

代码1  安装与 DeepSeek 接入配置

2.2 接入必做:mcp 版本处理

纯文本对话正常。一带工具就炸:litellm 的工具调用路径要 import mcp.types.CacheableResult,预装的 mcp 1.30 没有这个类型,直接 ImportError。升级 mcp 到 2.x 解决。这个问题只在带工具时触发,藏得深;升级后 agentscope、mapsmith 报了兼容警告,那是本机其他项目的依赖,不影响本文链路。

2.3 基础对话:最小可用代码

不绑任何地图,一个 GeoAgent 对象就能聊。7.0 秒返回,STAC 是什么说清楚了。

from geoagent import GeoAgent, GeoAgentConfig

 

agent = GeoAgent(config=GeoAgentConfig(

    provider="litellm",

    model="deepseek/deepseek-v4-flash",

    temperature=0,

))

resp = agent.chat("Explain STAC in two sentences.")

print(resp.answer_text)

代码2  基础对话最小代码

图1  基础对话实测日志

2.4 工具调用:核心验证

把普通 Python 函数变成 Agent 工具,一行装饰器就够。让 DeepSeek 干一个复合活:“加个 Knoxville 标记 + 给 roads 图层做 100 单位缓冲”。两个工具都被正确触发,参数分毫不差,耗时 6.8 秒——DeepSeek 的 function calling 在 GeoAgent 里完整可用,不是摆设。

from geoagent import GeoAgentContext, create_agent, geo_tool, GeoAgentConfig

 

@geo_tool(category="demo")

def add_marker(lat: float, lon: float, name: str = "") -> str:

    """Add a marker at given coordinates."""

    return f"Marker {name} added at ({lat}, {lon})"

 

agent = create_agent(

    context=GeoAgentContext(),

    tools=[add_marker, buffer_distance],

    config=GeoAgentConfig(provider="litellm", model="deepseek/deepseek-v4-flash"),

)

resp = agent.chat("Add a marker named Knoxville at 35.9606, -83.9207, "

                  "then buffer roads by 100.")

代码3  工具定义与调用核心代码

指令

触发工具

关键参数

耗时

结果

基础对话

—

—

7.0s

返回 STAC 解释

复合指令

add_marker

lat=35.9606, lon=-83.9207, name=Knoxville

6.8s

成功

复合指令

buffer_distance

layer=roads, distance=100

6.8s

成功

地图指令

set_center

47.6062, -122.3321

5.2s

成功

地图指令

add_marker

name=Seattle

5.2s

成功

表1  DeepSeek 经 LiteLLM 接入 GeoAgent 实测数据

图2  复合指令双工具调用日志

三、安全机制实测:默认拒绝 vs 一键放行

3.1 机制怎么工作

工具可以打上 requires_confirmation / destructive / long_running 标记。执行前有 confirmation hook 把关,默认策略是拒绝——模型想删图层,得先过你这一关。

@geo_tool(category="danger", requires_confirmation=True, destructive=True)

def delete_layer(layer_name: str) -> str:

    """Delete a layer from the project. DESTRUCTIVE."""

    return f"Deleted layer {layer_name}"

 

# 默认:不传 confirm,破坏性操作一律拒绝

agent = create_agent(context=GeoAgentContext(), tools=[delete_layer], config=...)

 

# 全自动放行(仅测试用):

from geoagent import auto_approve_all

agent = create_agent(..., tools=[delete_layer], confirm=auto_approve_all)

代码4  安全标记与确认回调

3.2 四场景实测

同一个 delete_layer,直接调用和聊天触发各测一次,默认配置和 auto_approve_all 各测一次。

场景

confirm 配置

工具执行

结果

direct 调用删除

默认(无回调)

拒绝

User denied confirmation for this tool

direct 调用删除

auto_approve_all

放行

Deleted layer roads

chat 让模型删除

默认(无回调)

拒绝

executed_tools 空,模型自述需确认

chat 让模型删除

auto_approve_all

放行

executed_tools=['delete_layer']

表2  安全确认钩子四场景实测

图3  安全确认场景日志

3.3 结论

安全不是口号,是默认关闭的闸门。要全自动可以传 auto_approve_all,但生产环境更该写自己的确认回调——弹窗、审批流都行。这是 GeoAgent 和“裸调 LLM”最大的区别。

四、工具面盘点:full 25 个 vs fast 13 个

4.1 leafmap 实时地图绑定

for_leafmap(m) 把 Agent 绑到活地图上。可以直接调工具(不走 LLM、结果确定),也可以聊天端到端。实测:聊天让它“加 Seattle 标记并缩放过去”,5.2 秒执行 set_center + add_marker。

import leafmap

from geoagent import for_leafmap

 

m = leafmap.Map()

agent = for_leafmap(m, config=GeoAgentConfig(provider="litellm",

                                            model="deepseek/deepseek-v4-flash"))

 

# 直接调工具(不走 LLM,结果可复算)

agent.tool.add_marker(lat=35.9606, lon=-83.9207, name="Knoxville")

agent.tool.set_center(lon=-83.9207, lat=35.9606, zoom=10)

print(agent.tool.get_map_state())   # center/zoom 可查证

 

# 聊天端到端

resp = agent.chat("Add a marker for Seattle and zoom to it.")

print(resp.executed_tools)          # ['set_center', 'add_marker']

代码5  leafmap 实时地图绑定

图4  leafmap 绑定与 fast mode 日志

图5  地图操作成果:双标记 + 高德底图

4.2 fast mode:怎么裁工具面

fast=True 时工具面从 25 个裁到 13 个:数据加载类(vector/geojson/raster/STAC/COG/PMTiles)和 add_marker 全被过滤,导航、图层可见性、底图切换保留。机制在工具元数据的 available_in 字段:fast 模式只保留声明了 fast 的工具。自定义工具默认只标 full,fast 模式下直接消失——想进 fast 面,得显式声明。

agent_fast = for_leafmap(m, fast=True)   # 25 → 13 个工具

print(agent_fast.tool_names)             # 数据加载类工具已被过滤

代码6  fast mode 一行开启

维度

full(25 个)

fast(13 个)

导航/视图

set_center / fly_to / set_zoom / zoom_* / zoom_to_bounds / zoom_to_layer

set_center / fly_to / set_zoom / zoom_in / zoom_out / zoom_to_layer

图层管理

add/remove/clear/set_visibility/set_opacity

set_visibility / set_opacity

数据加载

add_vector / add_geojson / add_raster / add_stac / add_cog / add_pmtiles / add_xyz

仅 add_xyz_tile_layer

标记与导出

add_marker / save_map / generate_image

仅 generate_image

表3  leafmap 工具面 full vs fast 对比

4.3 更多地图功能实测

除了标记和缩放,地图功能还能再挖三个:批量标记、底图切换、矢量图层。全部用 GeoAgent 工具直调,结果可复算。

# 批量标记:6 个中国城市,直调工具 0.2s 完成

for name, lon, lat in [(“北京”,116.41,39.90), (“上海”,121.47,31.23),

                       (“广州”,113.26,23.13), (“成都”,104.07,30.57),

                       (“乌鲁木齐”,87.62,43.83), (“哈尔滨”,126.54,45.80)]:

    agent.tool.add_marker(lat=lat, lon=lon, name=name)

agent.tool.set_center(lon=104.0, lat=35.0, zoom=4)

 

# 底图切换:街道 <-> 卫星

agent.tool.change_basemap(basemap="Gaode.Normal")

agent.tool.change_basemap(basemap="Gaode.Satellite")

 

# 矢量图层(实测踩坑:工具存在,执行返回 error,leafmap 0.64 兼容问题)

agent.tool.add_vector_data(filename="cities_lines.geojson", name="city_lines")

代码7  批量标记 / 底图切换 / 矢量图层

图6  批量标记 6 个中国城市)

图7  底图切换对比

图8  矢量连线图层:6 城 GeoJSON 连线

表4  GeoAgent leafmap 工具面地图能力一览

能力

工具

实测结果

标记

add_marker

6 城市批量 0.2s,成功

底图

change_basemap / add_xyz_tile_layer

街道/卫星切换成功

矢量

add_vector_data / add_geojson_data

工具存在,执行返回 error(版本坑)

栅格/影像

add_raster_data / add_cog_layer / add_stac_layer

add_raster_data 参数名坑;add_stac_layer 需显式 assets;COG 源 404

导航

set_center / fly_to / set_zoom / zoom_* / zoom_to_bounds

set_center/fly_to/set_zoom/zoom_in/out 成功;zoom_to_bounds/zoom_to_layer 相机未变(无头异步)

图层管理

list / add / remove / clear / set_visibility / set_opacity

list/visibility/opacity 成功;remove/clear 被安全闸门拦截

导出

save_map / generate_image

save_map AttributeError;generate_image 需 OPENAI_API_KEY

4.4 图层管理与视图控制实测

地图功能不只是“加标记”。图层生命周期(列出/隐藏/透明度/删除/清空)和视图控制(飞行/缩放/适配范围)是日常操作的大头。实测 25 个工具里的这 10 个,结果一喜一忧。

# 图层管理:先看地图上有啥,再决定动谁

agent.tool.list_layers()                          # 能列出 OpenStreetMap/Gaode/Beijing/Shanghai(含 visible/opacity)

agent.tool.set_layer_visibility(name=“Gaode”, visible=False)   # 隐藏底图

agent.tool.set_layer_opacity(name=“Gaode”, opacity=0.5)        # 半透明

agent.tool.remove_layer(name=“Gaode”)             # 被安全闸门拦下

agent.tool.clear_layers()                          # 同样被拦下

 

# 视图控制:相机状态可查证

agent.tool.fly_to(lon=116.4074, lat=39.9042, zoom=8)   # center/zoom 更新

agent.tool.set_zoom(zoom=12)                            # zoom 12.0

agent.tool.zoom_in()                                    # zoom 13.0

agent.tool.zoom_to_bounds(west=73.0, south=18.0, east=135.0, north=54.0)  # 返回 success,相机未变

代码8  图层管理与视图控制直调

喜的是状态闭环可验证:list_layers 把每个图层的 type/visible/opacity/source 全暴露出来,visibility/opacity 改完立刻能在 list 里查到。忧的是 remove_layer 和 clear_layers 直调直接返回 “User denied confirmation for this tool”——内置工具也带了 destructive 标记,默认一律拒绝。这不是 bug,是安全闸门在起作用:第三节能拦自定义 delete_layer,第四节发现它同样拦 leafmap 内置的删图清图层。

表5  图层管理实测(结果来自 step14b 日志)

工具

参数

实测结果

list_layers

—

success,列出 4 层(OpenStreetMap/Gaode/Beijing/Shanghai,含 type/visible/opacity/source)

set_layer_visibility

name=Gaode, visible=False

success,list 可见 visible=false

set_layer_opacity

name=Gaode, opacity=0.5

success,list 可见 opacity=0.5

remove_layer

name=Gaode

拒绝:User denied confirmation for this tool.

clear_layers

—

拒绝:User denied confirmation for this tool.

表6  视图控制实测

工具

参数

相机状态

fly_to

lon=116.4074, lat=39.9042, zoom=8

center 北京 / zoom 8.0 ✓

set_zoom

zoom=12

zoom 12.0 ✓

zoom_in

—

zoom 13.0 ✓

zoom_out

—

zoom 12.0 ✓

zoom_to_bounds

中国范围四角

返回 success,相机未变(ipyleaflet 异步 fit 无头环境失效)

zoom_to_layer

name=Gaode

返回 success,相机未变(同上)

4.5 chat 端到端:模型自己编排工具

前面都是直调工具,这一步把控制权还给模型。让它完成一个复合任务:“加北京标记,切到高德卫星图,再缩放到北京”。不预设工具顺序,看 DeepSeek 怎么编排。

resp = agent.chat(

    "Add a marker at Beijing (116.4074, 39.9042), then switch the basemap to "

    "Gaode.Satellite, then zoom to the Beijing marker."

)

print(resp.executed_tools)   # ['add_marker', 'change_basemap', 'zoom_to_layer', 'fly_to']

print(resp.answer_text)      # 模型自述:zoom_to_layer 对点标记无 bounds,改用 fly_to

代码9  chat 端到端复合任务

12.5 秒,模型自己编排了 4 个工具:add_marker → change_basemap → zoom_to_layer → fly_to。最有意思的是最后一步:zoom_to_layer 对“点标记”没有 bounds(bounds 是面数据才有的),模型在回答里自述“zoom_to_layer had no bounds for a point marker, so I centered on the coordinates instead”——遇到工具边界条件,它没有报错,而是换了个能落地的工具。多轮对话也一样:第一轮加了一个标记并缩放过去,第二轮问“地图上现在有什么”,模型补加了第二个标记后,主动调 list_layers 感知状态,答出 OpenStreetMap 底图 + 北京/上海两个标记,一个不多一个不少。这已经不是“会调工具”,而是“知道地图当前长什么样”。

表7  chat 端到端编排实测

场景

耗时

执行工具

结果

复合任务:加标记+切底图+缩放

12.5s

add_marker → change_basemap → zoom_to_layer → fly_to

成功;zoom_to_layer 无 bounds 时自动改 fly_to

多轮问答:轮1 加一标记,轮2 问状态

轮1 11.5s / 轮2 2.2s

轮2 补加标记后主动调 list_layers

准确汇报 OpenStreetMap + Beijing + Shanghai

五、边界与坑

说明:接入期的 mcp 版本问题属于主流程阻塞项,已在 2.2 就地处理;本节汇总的是跑通主链路之后的功能边界与限制——个别工具不可用、渲染环境受限、数据加载族开箱即用度低。

5.1 save_map 工具不兼容

GeoAgent 的 save_map 工具在 leafmap 0.64 上抛 AttributeError:Map 对象没有 save_map 方法。工具注册成功、调用失败——封装层对底层库版本敏感,用前先试,别默认它能跑。

5.2 地图渲染依赖 Jupyter

leafmap 默认基于 ipyleaflet,导出的 HTML 静态打开是空白,渲染要靠 Jupyter 环境。命令行能跑通 Agent 逻辑,“看图”得回 notebook。本次图5 用同参数做了静态渲染复现(瓦片坐标与实测 add_marker/set_center 完全一致),Agent 的操作结果可直接落成静态图,但 leafmap 原生的交互视图仍依赖 Jupyter。

5.3 其他注意点

litellm 首次导入约 10 秒,重依赖,适合常驻环境;DeepSeek 多轮对话有 reasoningContent 警告,不影响工具调用;模型接入有三条路:LiteLLM 直连(本文)、OpenRouter、Strands base_url 透传——不用锁死某一家。

5.4 数据加载族的坑

数据加载是工具面里最重的一族:vector/geojson/raster/STAC/COG/PMTiles。本机逐一试了一遍,四个字:各有各的坑。

表8  数据加载族探坑实测

工具

测试输入

实测结果

add_vector_data

本地 6 城连线 GeoJSON

error(leafmap 0.64 兼容问题,见 4.3)

add_raster_data

本地 PNG 伪影像

Validation error:必填参数是 path_or_url,不是 filename

add_stac_layer

earth-search AWS / sentinel-s2-l2a

TiTiler 端点报错:assets must be defined(需显式传 assets/expression)

add_cog_layer

Sentinel-2 COG 公开 URL

数据源 404(网络/源失效,功能本身未及验证)

add_pmtiles_layer

protomaps 公开 URL

数据源 404(同上)

generate_image

prompt="current map preview"

需 OPENAI_API_KEY,DeepSeek 链路用不了

这批坑分成两类:一类是参数/文档问题(add_raster_data 的真实参数名 path_or_url 藏得深、add_stac_layer 要求显式 assets),另一类是链路限制(generate_image 锁死 OpenAI、COG/PMTiles 依赖外部数据源可用性)。数据加载族是 GeoAgent 宣传的重头,但开箱即用的程度不如导航和标记——用之前先读源码,比读文档更靠谱。

六、判断框架:什么场景该用

6.1 适用

GIS 库维护者:别自己造 Agent 层,接入 GeoAgent,一次接入全生态复用;研究/教学场景:Jupyter 里自然语言操控地图;QGIS 重度用户:OpenGeoAgent 插件,聊天面板直接操作工程。

6.2 不适用

只要单点地图 API:MCP 更轻,没必要上 Agent 框架;追求极致精简:核心虽轻,模型客户端仍是重头。

6.3 收尾

Agent 层是公共基础设施,不该每个库各造一个。GeoAgent 的选择:一个库,一次接入,全生态复用。实测跑通只是开始,这类库值得持续关注。

Logo

更多推荐