ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于LLM智能体的城市遥感图像变化分析系统设计与实现

基于LLM智能体的城市遥感图像变化分析系统设计与实现

城市遥感变化检测这几年几乎是所有测绘院、规划院和卫星应用公司的刚需。但凡是做过实际项目的人都清楚,传统流程是把两期影像丢进模型里,出一张差分图,再人工看一遍,写报告,过程又慢又费人。我这两年一直在琢磨一件事:能不能把“变化在哪”和“为什么变、影响多大、该不该管”这一整条链路交给一个智能体去组织,把那些固定套路的事自动化,让人只做判断和决策。于是就有了这个基于LLM智能体框架的城市遥感图像变化分析系统。这篇文章把我从架构设计到落地实现的过程、踩过的坑、调过的参都整理出来,给想做这个方向的同行一个参考。

1. 先想清楚:LLM智能体到底解决了遥感变化检测的什么问题

1.1 传统变化检测流程的五个真实痛点

先说传统流程。以前做一个城市变化监测项目,大概是这个节奏:拿两期影像,做辐射归一化、几何配准,然后跑一个变化检测网络,输出二值图或者差异图,再人工目视解译圈出变化斑块,核实属性,最后整理成报告。听起来不复杂,但实际做起来至少有五个痛点非常要命。

第一,语义碎片化。像素级的检测结果往往是一片一片的噪声,同一个真实变化被切成好几块,或者不同性质的微小变化混在一起。每次都要靠人去合并、去分类,规则写着写着就成了一堆没人敢动的补丁逻辑。

第二,后处理链条太长。栅格转矢量、小斑块过滤、形态学运算、边界平滑、属性赋值,每一步都有参数,参数之间还互相耦合。项目换了数据源,整套参数就得重新调一遍。

第三,阈值选择靠拍脑袋。变化检测模型输出的概率图,定多少算变化,不同传感器、不同季节、不同城市都不一样。很多项目直接取0.5,结果不是漏检就是误报。

第四,结果不可解释。深度学习模型的输出就是一张图,领导问“这个地方为什么算变化”,你只能说“模型这么判断的”,这在业务上没法落地的。

第五,报告写作费人力。图斑统计表、位置描述、面积汇总、合规性初判,这些工作看起来简单,批量起来极其消耗人天。一个城市级项目,全手工整理报告大概要占掉三分之一的人力。

这些痛点其实指向同一个方向:变化检测这个事,真正值钱的不是“找出变化”,而是“理解变化”。而理解变化,恰好是LLM智能体最擅长的事。

1.2 LLM智能体切入的三个核心环节

LLM智能体不是来替代变化检测模型的,它是来接管“组织、理解、输出”这三个环节的。

第一个环节叫工具编排。智能体可以根据用户的一句话需求,自己决定调哪个数据预处理工具、跑哪个检测模型、做什么后处理,而不是像传统pipeline那样写死流程。比如用户说“看看东片区近两年有多少新增建筑”,智能体就会规划:先按行政区划裁剪影像,调用建筑变化模型,再做矢量化和面积统计,最后结合规划数据判断合规性。

第二个环节是语义解读。变化检测模型输出的是栅格和矢量,这些几何数据本身没有语义。智能体能结合地理知识、城规知识甚至网络公开信息,把“这里有个图斑”翻译成“该地块存在疑似新增工业厂房,面积约1.2公顷,位于生态控制线内,需进一步核实”。

第三个环节是报告生成与交互。智能体可以把表格、图例、文字描述、风险提示组织成一份结构化报告,并且允许用户追问,比如“这个变化区域周边的路网情况怎么样”,智能体可以继续调工具查数据,而不是重新跑一整套流程。

1.3 适用边界:别指望它什么都干

我必须先泼一盆冷水。LLM智能体不是万能的,有些场景它真不适合。

适合的场景包括:变化检测结果的后处理与语义化、点对点的问答式查询、多源数据的汇总研判、报告草稿生成、阈值的自适应解释等等。

不适合的场景也有:高精度的像素级定位、实时视频流变化检测、对模型推理速度有硬性要求的在线系统。这些事还是乖乖用传统CV和专用深度学习模型。

我现在的产品定位就是“面向分析研判的半自动系统”:模型负责干活,智能体负责组织干活并解释结果。这个定位切得越清楚,落地越顺。

2. 系统总体架构与技术选型

2.1 核心架构:规划—工具—记忆—反思四层

我最终采用的智能体架构,本质上是一个带工具调用能力的多轮对话系统,分为四层。

第一层是规划层。智能体接收用户意图后,把任务拆解成若干子步骤。这里我初始用的就是ReAct模式的改写版:让模型显式地输出“当前目标、下一步动作、调用参数”,每一步都落在结构化的JSON里,方便程序解析和校验。

第二层是工具层。典型工具包括:影像检索、栅格裁剪、变化检测推理、矢量后处理、面积统计、地类标签映射、图斑报告生成。每个工具都是独立服务,通过统一的OpenAPI规范注册给LLM。实测下来,工具命名和描述写得清不清楚,直接决定智能体会不会调错工具,这块后面细讲。

第三层是记忆层。短期记忆用对话上下文,长期记忆我引入了两块:一块是业务知识库,存城市规划控制线、地类分类标准、历史项目报告模板;另一块是会话存档,同一个地块的多次分析结果可以复用,避免重复计算。

第四层是反思层。每次工具调用的返回值会经过一个校验器。比如矢量数据面积明显不合理,或者坐标系对不上,就触发重新规划。这个反思机制在真实项目中救了我很多次。

2.2 底层变化检测引擎选型

智能体调用的是变化检测引擎,引擎本身还是要选对的。我测试过好几条路线。

最简单的是像素级差分法,比如影像差值加阈值分割,优点是快、无需标注,缺点是对辐射差异极其敏感,两期影像光照稍有不同,整个城市都在“变化”。除非数据源高度一致,否则不做首选。

实用路线是孪生网络。两期影像经过共享权重的编码器提取特征,再对特征做差异增强,最后解码成变化概率图。代表性的结构有SiamCRNN、SiamU-Net这类。我在项目里用的基线是SiamU-Net的改进版,在自建的城市变化数据集上,mIoU大概能到0.71左右,基本满足业务初筛要求。

如果想追求精度更高,可以换基于Transformer的ChangeFormer或者带注意力融合的模型,对小目标变化敏感度会高一些,但是计算量也上来了。一个1024×1024的影像patch,在单张消费级显卡上可能要跑2-3秒,全城推理要算好时间预算。

我在真实项目里做了一个折中:先用SiamU-Net出概率图,再用一个轻量的地物分类模型给变化区域打标签,比如区分建筑、道路、植被、水体。这样一个模型管“变没变”,一个模型管“变成什么”,配合起来效果好过单模型硬train。

2.3 GIS工具层与数据格式中枢

整个系统里最容易被低估的就是GIS工具层。很多做AI的人把精力放在模型上,结果数据格式一塌糊涂,最后分析结果根本导不进ArcGIS。

我的建议是,统一用GeoJSON作为矢量交换格式,栅格统一用GeoTIFF,投影信息在每一步都要保留。所有后端处理工具,输入输出都走这两个格式。坐标系我内部统一用EPSG:4326的经纬度,前端展示再动态转Web墨卡托。为什么不用投影坐标系?因为影像原始数据五花八门,有的地方是高斯投影,有的是UTM,统一转WGS84经纬度最保险,虽然面积计算要重新换算,但不会出大错。

工具层还用到了PostGIS做空间查询。变化图斑入库后,跟控制线、用地红线、生态保护区做空间叠加分析,这一步交给空间数据库比让模型自己算靠谱得多。

2.4 LLM选型与工具调用方式

LLM选型,我前前后后测了三种路线。

第一是纯闭源大模型直接调API,效果最好,函数调用能力最稳,但是数据出域,很多政务客户不接受,项目直接毙掉。

第二是私有化部署开源模型,数据不出域,但函数调用能力弱,稍微复杂一点的参数嵌套就漏参数,比如让它调一个裁剪工具,五个参数能漏俩。

第三是混合模式:本地部署一个小模型做意图识别和参数洗牌,核心分析调用云端大模型。这个方案数据合规性可以解释,效果也过得去,我目前部署默认用这个。

不管哪种模式,工具调用的核心机制都差不多:把每个工具的入参、出参、用途写成一个JSON Schema,塞进上下文里,让模型输出结构化的调用指令,然后由程序去执行,执行结果再回传给模型。这块比让模型直接生成Python代码要稳得多,因为代码执行风险高,还要多一道沙箱,收益却不明显。

3. 关键实现细节与代码级拆解

3.1 数据准备:裁剪、对齐、辐射归一化三步走

影像预处理这个阶段,80%的变化检测效果都取决于它。很多模型精度不够,其实是数据没对齐。

第一步裁剪。原始影像动不动就是上万像素宽,直接丢进模型显存直接爆掉。要按512×512或者1024×1024的patch切,patch之间保留50像素的重叠,防止变化目标恰好被切在边界上。

第二步对齐。两期影像要用同名点做二次配准,我用的是基于特征匹配的自动配准,再检查像元级对齐精度。如果配准误差超过一个像元,后续像素级对比就基本没意义。

第三步辐射归一化。这个坑太典型了。两期影像拍摄季节不同、传感器不同,灰度分布差异巨大。我用的是直方图匹配加线性回归校正,把参考影像的辐射特征调整到与基准期一致。做完这步,识别出的“变化”才是真实变化,而不是光照变化。

代码上核心就是两段。一段做配准,我是用rasterio读取影像、OpenCV提取特征、计算单应矩阵重采样,这个流程不多说。一段做辐射校正,我直接贴个简化版示意:

import numpy as np from skimage.exposure import match_histograms def radiation_normalize(base, target): # 将target影像的直方图匹配到base影像 matched = match_histograms(target, base, channel_axis=-1) return matched.astype(np.uint16)

实际项目里我还会加一个掩膜:把云、云影和水体区域先抠出来,不参与校正,不然这些地物的直方图会拖累全局统计。

3.2 变化检测推理与后处理:从概率图到干净图斑

模型推理本身没什么特别,就是把两期影像patch喂进去,拿到变化概率图。真正的功夫在后处理。

我后处理的顺序是:概率图阈值分割→连通域分析→小斑块过滤→形态学平滑→栅格转矢量→边界简化。阈值这块,我不用固定0.5,而是用Otsu算法自适应确定,或者根据类别频率动态调整。一个我总结的小经验:城市区域建筑变化概率图的分布往往呈双峰,Otsu的效果远好于固定阈值;而道路变化概率图是单峰长尾,需要人工指定一个较低的阈值比如0.35,否则漏检严重。

后处理核心代码示意:

import cv2 import numpy as np from shapely.geometry import shape from rasterio.features import shapes def postprocess(prob_map, threshold=0.5, min_area_px=200): binary = (prob_map > threshold).astype(np.uint8) # 形态学开运算去噪 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2) # 连通域过滤 num, labels, stats, _ = cv2.connectedComponentsWithStats(binary) keep = np.zeros_like(binary) for i in range(1, num): if stats[i, cv2.CC_STAT_AREA] >= min_area_px: keep[labels == i] = 1 return keep

这一步之后,我才把栅格转成矢量。转矢量用rasterio的shapes函数,输出GeoJSON,然后做坐标转换,把像素坐标转换为地理坐标。这里要注意,输出的多边形边界往往是锯齿形的,我会用Douglas-Peucker做一次简化,容差根据出图比例尺设,一般设3到5米。

3.3 智能体工具注册与分析链路实现

工具注册是整个智能体框架能不能跑通的关键。在每个工具的定义里,描述要写得像给实习生下指令一样清楚,包括它做什么、输入输出是什么、常见误区是什么。比如裁剪工具的描述我这么写:

{ "name": "crop_raster_by_geojson", "description": "用GeoJSON范围裁剪GeoTIFF影像,返回裁剪后的GeoTIFF路径。注意:GeoJSON必须为EPSG:4326坐标,影像路径须来自影像检索工具。", "parameters": { "type": "object", "properties": { "input_path": {"type": "string", "description": "输入GeoTIFF路径"}, "aoi": {"type": "object", "description": "GeoJSON Polygon对象"}, "output_path": {"type": "string", "description": "输出GeoTIFF路径"} }, "required": ["input_path", "aoi", "output_path"] } }

画重点:描述里一定要写“从哪里拿这个参数”,比如“路径须来自影像检索工具”,这样能显著减少LLM乱传路径的问题。这是我从一个真实bug里悟出来的,当时模型把用户随口说的一句地名当成了文件路径,分析直接跑飞,加了这句约束后类似问题少了很多。

智能体的主循环核心逻辑如下:

def agent_loop(user_query, available_tools, max_steps=8): messages = [{"role": "system", "content": SYSTEM_PROMPT}] messages.append({"role": "user", "content": user_query}) for step in range(max_steps): resp = llm_chat(messages, tools=available_tools) if resp.tool_calls: for call in resp.tool_calls: result = execute_tool(call.name, call.arguments) messages.append(tool_result_message(call.id, result)) else: return resp.content return "已达到最大步骤数,中止。"

每一步工具结果回传后,模型会决定是继续调用下一步工具,还是直接生成最终回答。我在几个月度项目里观察,平均一个完整的地块变化分析任务需要4到7次工具调用。

3.4 提示词设计与多轮状态管理

提示词设计这事,容易被当成写作文,其实它是工程。我的系统提示词固定分四块:角色设定、工具使用规则、分析流程规范、输出格式规范。

角色设定不要写“你是遥感专家”这种空话,要写“你是城市遥感变化分析助手,负责组织工具完成分析并解释结果”。工具使用规则里有一条很关键:一次只调用一个工具,等结果返回后再决定下一步。如果让它一次并行调多个工具,参数混乱的概率会直线上升。

输出格式规范也要明确。我对变化分析报告规定了固定的markdown结构:摘要、变化区域列表、图斑明细表、风险研判、建议复核清单。模型如果自由发挥,报告格式每次都不一样,下游没法接。

多轮状态管理我踩过一个坑:用户意图变化后,上一轮的工具结果还堆在上下文里,模型容易串。解决方案是每轮对话前,做一次历史压缩,把已经完成的工具调用结果摘要化,只保留关键数值,比如“建筑新增总面积3.2公顷,已提取图斑12个”,而不是堆完整的GeoJSON。这样既省token,也防止串数据。

4. 实操过程:一个城市地块的完整变化分析演示

4.1 场景设定与数据情况

拿一个我做过测试的模拟项目X来说。数据是某城市高新区一块约4平方公里的区域,两期高分影像,间隔两年。一期是六月份拍的,二期是同年份另一天的不同时段,包含云量很少,地物有建筑新建、裸土翻整、道路拓宽等变化。我要求智能体完成以下任务:识别新增建筑区域、评估对周边生态控制线的影响、生成变化分析简报。

用户原始输入就一句话:“高新区西北片区两年内新增了哪些建筑,有没有越线的,给我出一份简报。”

4.2 智能体完整工作流演练

整套工作流我一步步拆给你看。

第一步,意图解析。LLM把用户问题拆成子任务清单:检索两期影像、裁剪AOI区域、执行变化检测、提取新增建筑类变化图斑、与生态控制线叠加分析、生成简报。这六步会被动态编排,而不是全部执行。

第二步,影像检索。LLM调用imagery_search工具,范围设置为高新区西北片区、获取时间为两个年份。影像检索返回两个GeoTIFF路径。这里注意,LLM自己不会判断影像是否含云,工具层在返回时自动带上云量检查结果。

第三步,裁剪与预处理。LLM调用crop工具和normalize工具。normalize返回的辐射统计信息显示两期影像直方图相关性0.92,符合处理要求。

第四步,变化检测推理。LLM调用change_detect工具,模型输出概率图后,后处理模块自动生成图斑矢量。工具返回的信息不是完整GeoJSON,而是摘要:图斑数27个,总面积3.8公顷,置信度均值0.86。

第五步,地类分类。LLM发现用户关心“建筑”,所以接着调用land_cover_classify工具,给27个图斑打地类标签。结果显示新增建筑类图斑13个,面积2.4公顷。

第六步,叠加分析。LLM调用spatial_overlay工具,把这些图斑与生态控制线叠加。工具返回:其中有2个图斑面积0.3公顷与生态控制线范围相交,属于疑似越线。

第七步,生成简报。LLM整合所有结果,输出一份带表格的markdown报告。

这一步一步你可能会问:是不是太慢了?实测下来,从提问到拿到完整简报,耗时约1分20秒。其中变化检测推理占了大头,大约50秒,LLM规划加工具调用本身只占20多秒。和人工流程相比,这个速度已经快了一个数量级。

4.3 输出内容形式与人工复核闭环

智能体最终生成的简报长这样:开头一段摘要,概述区域内新增建筑主要分布方向;中间是变化图斑明细表,每一行包含图斑编号、面积、地类、是否越线;再往下是空间分析结论,重点标出越线风险区域;最后是一份建议人工复核清单,列出需要现场拍照验证的3个图斑。

这里我特别想强调人工复核闭环。智能体不是替代人做最终决策的,它是把人的注意力引导到真正需要看的地方。我见过很多团队把目标定成“全自动检测、全自动报告”,结果模型一出错没人发现,最后领导问责。我的方案是:所有智能体生成的结论都必须标注置信度,低于阈值的图斑自动进入“待现场核实”列表。这样模型有错也只会漏在明面上,不会被无声无息地掩盖。

5. 常见问题与排查技巧实录

5.1 LLM幻觉:编造不存在的图斑或面积

这个问题在最开始几乎天天碰到。模型在总结时会一本正经地写出“新增建筑面积5.6公顷”之类的数字,但你回头对账,所有图斑加起来只有2公顷。原因很简单:模型不是在计算,它是在“生成一个像样的数字”。

我的解决办法是强约束工具结果的传递。所有统计数字不从LLM对话里提取,而是工具结果里带一段machine-readable的JSON,摘要显示给用户看的文本由模板生成。LLM只负责解释和分析,不负责编数字。另外,我在反思层加了一个校验器:每次LLM生成最终回答前,会检查它引用的数字是否与工具计算结果一致,不一致就打回重写。

5.2 坐标对不上:图斑飞到十万八千里外

这个坑我印象太深了。第一次联调时,模型生成的图斑叠加在底图上,竟然漂到了几万公里外的海上。排查后发现是工具返回GeoJSON时坐标没做转换,一个环节输出的是投影坐标,另一个环节按经纬度解析,空间关系全部错乱。

从此我立了一个铁律:所有工具接口的GeoJSON一律EPSG:4326,并且在工具描述里高亮强调。另外,我在后处理服务的边界上加了个合理性检查,输出坐标范围如果超出城市边界范围一定会弹告警。

5.3 大影像处理:显存不够和推理时间失控

城市级影像动辄几十GB,直接整景推理根本不现实。我测试过几种切patch的方式,最优方案是按固定瓦片网格切,而不是滑动窗口切。瓦片网格的好处是结果可缓存,下次分析同一区域就不用重算。每个瓦片1024×1024,检测模型推理一次2-3秒,整个高新区大概400个瓦片,并行8卡,约15分钟出完整底图。这个速度对离线监测足够用了。

实时性需求更强的场景,可以降采样到512×512,或者干脆只分析用户框选的重点区域,不跑全城。

5.4 漏检和误检:阈值与样本的双重博弈

漏检和误检是变化检测永恒的话题。智能体排错时我发现,误检的图斑往往集中在影像配准误差大的边缘地区,尤其是建筑物立面倾斜产生的鬼影。解决办法是分区设置阈值:影像中心区域阈值0.45,边缘区域阈值调到0.6。

漏检问题更麻烦,频繁发生在小目标变化上,比如一栋小房子拆除。这类变化像素占比可能只有几十个像素,后处理最小图斑面积参数设大了直接过滤掉。我的调试思路是把最小图斑面积参数做成可配置项,由智能体根据任务类型自动选择:做建筑监测时设300像素,做细碎变化监测时设80像素。

样本层面,实在的办法是持续标注。每季度用智能体筛选出的低置信度图斑,做一次人工标注回流微调。我见过很多项目忽略了这一步,模型效果随着城市变化越来越差还不自知。

5.5 长上下文与工具调用冲突

对话一长,上下文里堆了几轮工具结果后,LLM开始出现“忘事”:明明前面已经算过总面积,后面又调一次工具重算,或者干脆把历史图斑号张冠李戴。

我做了三件事:一是上文说过的历史摘要压缩;二是限定工具调用步骤不超过8次,超过就强制输出阶段性小结;三是轮次结束时清理临时文件,避免脏数据残留影响下一轮分析。

这三板斧加上去,长会话稳定性改善非常明显。至少在我现在的压力测试里,连续10轮对话不串线、不重复计算已经能做到。

最后分享一点个人体会

做这个系统最大的体会是:LLM智能体在遥感里的价值,不是让模型变得更强,而是让整个分析流程变得像人一样会思考和会沟通。以前做变化检测,用户拿到的是一张图和一堆参数;现在用户拿到的是能看懂、能追问、能直接辅助决策的结论。这套东西后续还能往两个方向延伸,一个是把多源数据接入,比如把夜间灯光、POI、路网数据一起喂给智能体做综合研判;另一个是端到端微调一个小参数模型,把工具调用和领域知识内化进去,减少对外部大模型的依赖。如果你也在琢磨这个方向,建议从一个小地块、一个明确的监测需求开始,先把工具链跑通,再去追求规模和智能感。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进