给GIS库统一装上Agent大脑:GeoAgent是怎么设计的
一个 GIS 库想接大模型,先要回答三个问题:函数怎么变成工具?工具怎么暴露给模型?破坏性操作谁来把关?leafmap 答一遍,geemap 答一遍,QGIS 插件再答一遍——每个库都在重复造轮子。opengeos 团队的答案是:把这些活收进一个库。这就是 GeoAgent。本文用 DeepSeek 把它完整跑了一遍,附上核心代码和实测日志。
目录
四、工具面盘点:full 25 个 vs fast 13 个
一、为什么需要“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 的选择:一个库,一次接入,全生态复用。实测跑通只是开始,这类库值得持续关注。
更多推荐


所有评论(0)