从自研底层到开源底座:一个 GIS 开发者的 Agent 五阶段实践
四次推倒重来之后,我总结出的 10 条经验与 3 个取舍
我不是算法工程师,也不是专职做 AI 的人。我的本职是 GIS 方向的开发,日常一半时间写地理信息相关的代码(数据处理、成果输出、质检),另一半时间做公司具体业务的专项开发——档案整理、表格填报、成果表导出、审批表套打这一类"看着不起眼、但一错就是几十万字"的活。
这份工作有个特点:业务的确定性极高,工具链的确定性极低。同一个任务,甲方给的数据格式变一变、ArcGIS 版本换一换、Excel 的列名多一个空格,原来的处理流程就得改。于是"用自然语言把 GIS 与办公处理串起来"这件事,对我有非常现实的吸引力。
从 2026 年 6 月开始,我为这件事做了五个阶段的项目。中间推倒重来过四次,每一次的结论都写成了文档,每一次的教训都是真金白银的时间换的。需要说明的是:第二次不是在旧项目上继续堆功能,而是把业务全部砍掉、从零重建了一个干净底座——现在回头看,那一步才是整条路上最关键的一次取舍。这篇文章按仓库的实际时间复盘:每一版为什么开工、为什么停下、留下了什么,并在每个阶段末尾给一节「选型结论」。
如果你也在做行业 Agent,我最想让你带走的一句话是:不要自己写 Agent 循环,除非你的业务就是 Agent 循环本身。 剩下的内容都是这句话的论证过程。
五个阶段一览
| 阶段 | 时间 | 项目 | 一句话 | 结局 |
|---|---|---|---|---|
| 阶段一 | 2026.06.24–07.22 | GisAgent | 从底层自研 Agent:C# WPF 壳 + 内置 Python + ArcPy | 业务工具能用,但底座维护成本高、程序臃肿,停在 V1.3.6 |
| 阶段二 | 2026.08.05–11 | GeographicAgent | 砍掉业务,重建"干净底座":对话壳 + 上下文 + 工具循环 + 安全门 | 六天做到 v0.4.3,四个内置 GIS 工具跑通闭环,底座方向被验证 |
| 阶段三 | 2026.08.12–14 | CSharpAddinAgent | 在 ArcMap 10.8 里内置 AI 插件(C# Add-In),进程内操作当前地图 | 版本强依赖、arcpy 走 Python 2.7,约三天后停在 v1.0.6 |
| 阶段四 | 2026.08.15–09.02 | GISHARNESS | 用 DeepSeek Harness 做底座,只写 GIS 与业务工具层 | 底座偏技术化、环境门槛高,停止维护 |
| 阶段五 | 2026.09–至今 | 枢智(PivotAgent) | 借开源引擎,做面向业务人员的国产化通用 Agent 桌面端 | 进行中 |
说明:前四个阶段的仓库目前都是私有仓库,本文只讲技术事实,不放出不可访问的链接,也不涉及客户名、项目标识与内部路径。
阶段一:从底层自己写一个 Agent
为什么开工
当时的判断很朴素:我有大量 GIS 处理逻辑(投影转换、缓冲、叠加、成果表导出、CAD 互转),这些逻辑本来就是脚本化的;只要在它外面套一层"能听懂人话"的调度层,业务同事就能自己用。
我不想依赖任何现成框架——理由是"不了解底层,出问题没法改"。于是从零开始:
- UI 层:C# WPF(.NET 8),SSE 流式对话气泡、思考转圈、隐藏代码块,工具调用只显示一行结果摘要;
- 大脑层:自写
BrainAgentService,负责拼 System Prompt、调 LLM 的 Tool Calling、校验参数、流式回传; - 物理层:所有工具都在独立子进程里执行,通过
tools.json注册,覆盖四类运行时——内置便携 Python 3、ArcGIS Desktop 的 Python 2.7、ArcGIS Pro 的 Python 3、以及 .NET 命令行小工具。
子进程之间用 UTF-8 JSON 文件传参,不走命令行。这条现在看是对的:Windows 上命令行传中文路径,py2.7 会因为 GBK/UTF-8 不一致直接乱码或报错,这个坑我在阶段三又踩了一次。
这一版真的跑起来了,也真的用在了业务里:
- 界址成果表导出——界址点成果表(Excel)、界址标示表(Word)、地籍调查表(Word),以及三类一次导出的"全套 + 自动归类";
- 国情上图检测——把源表的名称先直接和 GDB 图层比对,未命中再经"对比说明"表做"专题要素名称 → 上图名称"映射后比对,输出「逐条判定 / 汇总 / GDB 未覆盖」三张表,并把结果写回原表;
- 登记簿图片识别填表——OCR 识别登记簿后写入 Excel,后台任务式(立即返回
runId,再轮询进度),甲方场景是 32 万行的表; - 数据质检——读写类工具执行前后自动做 preflight(能否读取、shp 配套是否齐全、坐标系摘要),用户显式要求时才做前后数据集对比。
它也有清清楚楚的能力边界:矢量(shp/GDB)的投影、缓冲、叠加支持;栅格、拓扑、网络分析、3D、制图输出不支持;而且界址点成果表的"起点重置、内环排序"这类必须人工判断的步骤,仍然要求用户在 ArcMap 里手工完成并保存——Agent 只做能确定的那一段。
为什么会臃肿
第一版的设计原则其实写得挺清醒,现在回看依然成立:
| 原则 | 说明 |
|---|---|
| 已注册工具优先 | 默认不自动生成代码;没有匹配工具时给"注册新工具"的路径建议 |
| 不写死业务 | 字段名、路径由对话或数据探查确认,不把某个行业字段当唯一示例 |
| 对话澄清 | 参数不全时先追问,不猜 |
| 参数校验 | 执行前硬校验,失败就把结构化错误回给模型继续追问 |
| 可视化确认 | 删除、移动、覆盖输出弹窗确认 |
| 业务友好展示 | 隐藏代码块与堆栈,工具成功只显示一行摘要 |
问题不在原则,在边界失控。
第一,我为了"考虑多模态的可能性",在 V1.1.0 引入了三套识图引擎。界面下拉是「自动 / Paddle / Unlimited / 百度」:自动是选择档,引擎是 PaddleOCR 便携包、Unlimited-OCR、百度云端 OCR。设置面板要填多套路径和密钥,还要区分"这个引擎没独显不可用、那个引擎有独显整页扫描质量高"。识别引擎的组合数变成了产品的一部分。停维护时的最后一版 V1.3.6 已经拿掉 Unlimited-OCR,本地识图只留 Paddle;臃肿发生在中途,不是停手那天才加上的。
第二,OCR 运行时是单独交付的——PaddleOCR 便携包约 3.8GB,加上主程序(自包含 .NET 8 + 嵌入 Python + 全部脚本和 C# 工具),交付清单变成了"一个主程序 + 一个大包"。对业务同事来说,这就是"要装两个东西"。
第三,ArcPy 要双份维护:Desktop 10.x 用 Python 2.7、Pro 用 Python 3,同一批工具两套脚本、两份 tools.json,连进度上报模块都要三份保持同步(靠一个 parity 校验脚本兜住)。
第四,加一个 C# 工具要同时改五处(工具目录、tools.json、发布脚本、说明文档、架构文档)——C# 就不适合做这种需要频繁增删的执行层。
真正的转折点是我意识到:我正在维护的核心资产,不是 GIS 算法,而是"如何调度 GIS 算法"的那套基础设施——Agent 循环、上下文管理、会话存储、密钥加密、错误日志、流式渲染、压缩续聊。这些东西别人已经做得比我好,而且还在持续变好;只有前半段(真实的 GIS 业务逻辑)是我独有的。
阶段一踩坑(挑两条最贵的)
- 一个
.0让统计结果静默算错。Excel 里的数字单元格是浮点,取值str()得到"310101.0",而 shp/GDB 侧的宗地代码是字符串"310101"——本该匹配上的记录被判定为"未上图",上图率直接算错,而且全流程没有任何报错。修法是统一做属性文本归一化(去浮点.0)。这个坑在 V1.3.3 被标成"最高影响",因为它符合 GIS 数据处理最危险的特征:跑完了、没报错、结果是错的。 - 安全校验被一个空格绕过。表达式计算工具的黑名单按子串匹配,
open (、os . system加个空格就绕过去了;而裸关键字用子串匹配又会误伤!MODEL!这样的字段引用。最后改成"先删空白再匹配带标点的片段,裸关键字用单词边界"。自己写的安全过滤,一律按"会被绕过"来设计。
阶段一选型结论
- 自研底座是性价比最低的一段投入。工具循环、上下文压缩、会话持久化、密钥管理,开源项目已经做完了;我重写一遍,得到的是"略懂原理"和一堆只有我会修的代码。
- 多模态不是加法,是乘法。每多一个引擎,你就多一份路径配置、一份体积、一份兼容矩阵、一份"模型选错引擎"的排查成本。要做,就一次只做一个,并且让它可替换。
- 能力边界要写进产品。"栅格不支持""起点重置请先在 ArcMap 里做"这种事必须明说,否则业务同事拿到错误结果比你直接拒绝更糟。
- 执行层要选"声明式 + 零改动接入"的语言。加一个功能要改五处的地方,不适合当执行层。
阶段二:砍掉业务,重建一个"干净底座"
这一版真正在做的事
阶段一停手后,我没有直接开始第二版重构,而是先做了一个决定性的动作:把业务全部砍掉。
我在立项文档里写下的用户意图是这样的(原文口径):
- 旧项目停止维护,不再往上堆功能;
- 新项目只做基础——桌面端套一层大模型 API 壳,先能聊天、流式输出、选模型;
- 第一阶段不注册任何业务工具,零技能、空工具列表;
- 短时间不集成 C# 业务程序,以后的技能以 Python 脚本注册为主;
- 写盘 / 执行脚本默认关闭,以后开执行必须有安全确认;
- 地理信息技能、知识库、Agent 自动循环调工具——全部后置。
一句话概括当时的判断:先做一个干净的桌面 Chat Agent,技能解耦、后加。
项目的定位句我至今觉得写得比第一版清醒:
一个通用 Agent 底座(对话壳 + 上下文 + 工具循环 + 安全门)+ 可插拔的项目能力(工具 + 文档知识 + 提示词)。 底座与任何具体项目无关,永不因项目变化而重写;项目能力随时可增删换。
六天的实际进度
这一版不是"零散试几个想法",而是从 v0.1 一路做到 v0.4.3:
| 版本 | 做了什么 |
|---|---|
| v0.1 | 纯对话壳:流式输出、思考链完整展示可折叠、双模型下拉、token 预算与进度、压缩续聊。零业务工具、不执行代码、不写业务目录 |
| v0.2 | 文档转 Markdown 阅读;图片本地预览 + 本地 OCR(那时 API 是纯文本,发不了图) |
| v0.3 | 消息重试/编辑/删除、会话重命名/搜索/导出、流式健壮性、安全加固、单元测试 |
| v0.4 | 启用 Agent 工具调用闭环,挂上四个内置 GIS 工具:shp_to_gpkg(转换)、layer_info(图层信息与字段查询)、filter_export(按条件/按字段拆分导出)、list_tools(工具清单) |
其中 shp_to_gpkg 的目标坐标系支持中文自然语言("CGCS2000 3 度带第 40 带""西安80 6 度带第 20 带""中央经线117"),未指定时按国标 CGCS2000(EPSG:4490);描述有歧义或缺少 .prj 时,工具返回"需要澄清"而不是擅自猜一个。
这一阶段留下的四条设计(比进度本身更值钱)
① 把"加工具"变成零改代码的操作。
工具 = tools.json 里一条声明(id / 名称 / 描述 / 参数 schema / 运行时 / 是否写操作 / 详细说明与用法示例)+ 脚本文件。注册表每次重读 tools.json,新工具即时生效;侧栏还有一个"工具一览"窗口,点开能看到参数与用法;模型侧则通过 list_tools 回答"你能做什么"。从头到尾没有为了加工具改过 C# 代码——这条纪律撑住了整个项目后期的扩张。
② 结果与进度走两条完全不同的通道。
结果是 --request/--result 的 UTF-8 JSON 文件协议(绕开 Windows 控制台编码,py2/py3 通用);进度是 stdout 上的带外行协议(@@GA_PROGRESS@@{json}),主程序按行解析后在工具气泡里显示"执行中 X% · …",不报进度就静默跳过。两者互不干扰。多批次任务(比如按字段拆分导出)还会把进度折算成整体百分比并保证单调递增。
③ 上下文预算按"实际裁剪点"计量,并且为缓存命中做过专门优化。 进度条的分母不是配置里的预算值,而是"有效输入容量" = 预算 − 输出预留——和发送时真正的裁剪点一致。超预算时组包策略后来改成了"稳定头部(最老历史)+ 最新尾部",只丢中间段,并且工具调用与其结果成组不拆散,目的是最大化前缀缓存命中;气泡底部还会显示本次请求的缓存命中/未命中 token,让命中率变成能直接观察的数字。代价也如实写在文档里:超预算会丢中间历史,所以临近满额时建议优先"压缩并继续"。
④ 稳健性是设计项,不是补丁。 这一版定义了一批"看起来琐碎、但决定了别人敢不敢用"的行为:流式超时按空闲算(一直有输出就不掐断);停止/超时/中断都保留已生成内容并标注,不覆盖成错误文本;空回复不产生空气泡;会话文件带 SchemaVersion(更新版程序写的文件不会被旧版误判)、损坏文件隔离成 .corrupt;API Key 用系统凭据接口加密;单实例启动保护;会话保存后台排队落盘、关窗同步保存;日志超限自动轮转。
关于"多模态"和"自纠错"的两条预判
多模态这一次没有再引一堆引擎。做法是:图片走本地 OCR,并把这条链路隔离在一处——一旦模型接口支持直接收图,摘掉 OCR、改成直接发图即可,不动架构。这直接吸取了阶段一"三引擎矩阵"的教训。事后看,这个预判是对的。
另一条认知在这里成形,第三、四阶段都把它写成了硬纪律:
"自纠错"是有前提的。模型能自我纠错,靠的是"写代码 → 运行 → 编译器或运行时明确报错 → 修正"这个闭环。而 GIS 处理里大量的错误发生在语义层——坐标系选错了、编码搞错了、字段匹配错了、覆盖了不该覆盖的文件。程序"成功"运行,结果不对,没有报错,模型无从纠错。
所以:确定性的处理必须沉淀成预置工具(可靠执行 + 参数校验 + 安全确认 + 结构化错误回传),而不是让模型现场生成代码。工具返回结构化错误时,模型照样能纠错重试;但工具"静默成功"时,谁都救不了。
为什么没有继续往下做
仓库从 8 月 5 日做到 11 日,v0.4.3 之后我停了这一版。原因写得很直白:"底座与项目无关、永不因项目变化而重写"这个判断本身是对的;既然如此,就更没必要由我来重写它。
到 v0.4.3,我已经把工具循环、上下文预算与缓存、会话持久化、安全确认门、附件与文档处理这一整套都做出来了。它们能跑、有测试,但每一条都还有继续打磨的空间;而这些东西业界已经有成熟实现,并且持续在变好。我不打算把接下来几个月都花在"把底座做得和别人一样好"上。
紧接着的一周并没有立刻换上现成底座。第三阶段的 ArcMap 插件仍自带一套精简对话循环;真正把循环交出去,是第四阶段。
真正继承下来的是四条东西:声明式工具注册、结果/进度双通道、预算按裁剪点计量、安全确认门——后来第四阶段的工具层几乎是这一版的放大版。
阶段二选型结论
- 砍掉业务,是这一阶段最重要的一个决定。 不是说业务不重要,而是:想看清底座长什么样,必须先把业务从视野里拿掉。切掉之后,我反而第一次看清了"调度 GIS 算法"这件事本身的结构。
- 底座与业务分层,并把这条写进定位句,是这一版最值钱的两行字。它让我后面能心安理得地把底座交给现成实现,只带走工具设计思路。
- 加一个能力要不要改主程序,是执行层选型的唯一硬指标。 "加工具 = 加一条 JSON + 一个脚本"这条纪律,从这一版一直用到第五阶段。
- 稳健性清单要趁早写。 超时怎么算、失败保不保留内容、会话文件坏了怎么办、密钥怎么存——这些不是"以后优化",是决定这个工具能不能被业务同事日常使用的底线。
- 自己做过的判断,才有资格放弃。 我做了一版底座,才敢在第四阶段说"这部分交给现成实现"。没有亲手做过一遍的人,很难判断哪些是真正难的部分、哪些只是看起来难。
阶段三:把 AI 塞进 ArcMap 里
为什么开工
阶段二的桌面壳能聊天、能调工具,但拿不到 ArcMap 里已经打开的地图。用户在 ArcMap 里已经打开了数据、调好了视图、选中了图层,此时说一句"给这个线层做 500 米缓冲",期望的是——结果直接出现在当前地图上,视图刷新,图层树里多出一层。
技术上这件事只有一个可靠解:进程内执行。桌面端的地图文档对象(MapDocument("CURRENT"))和视图刷新,只有在 ArcMap 自己的进程里才拿得稳;用外部 python.exe 调 arcpy,拿不到"当前地图",也没法实时刷新界面。而且 C# 的停靠窗格(DockableWindow)是官方支持、由 ArcMap 消息循环托管的,做聊天面板正合适。
于是就有了第三阶段:一个 ArcMap 10.8 的 C# Add-In——右侧停靠一个 AI 助手面板,对话在进程内执行 GP 工具或 arcpy 脚本,然后自动刷新当前地图。仓库从 8 月 12 日做到 14 日,停在 v1.0.6。
先说明我为什么敢定"只做 10.8":版本适配这件事,我在阶段一已经付过一次学费。那一版为了让更多现场能用,同时维护了 ArcGIS Desktop 10.2~10.8(配 Python 2.7)与 ArcGIS Pro(配 Python 3)两套执行层——同一批工具,两套脚本、两份工具注册表,连进度上报模块都要三份保持同步。多覆盖两个版本号,代价是整套代码的长期双份维护。所以到这一版我直接划死:目标只有 10.8。
这一版比外面的桌面壳更贴手:
- 进程内 GP 执行 + 刷新 + 纠错(缓冲、裁剪、汇总统计、计数都真机冒烟通过);
- arcpy(Python 2.7)兜底执行,带 py3 语法特征的预检;
- 排版类地图操作:标题、图例、比例尺、指北针、图层顺序、导出宽度;
- 会话持久化(自动保存、切换、新建、导出 Markdown、重启恢复);
- 安全策略:删除类操作任何模式都要确认、默认不覆盖、信任模式下自动执行并事后报告"做了什么 / 覆盖了什么";
- 进度窗与取消(后台 STA 线程 + 模态进度窗,短任务不弹窗);
- 一套极简的纯逻辑单测(从 24 组 175 断言长到 27 组 235 断言),外加一个一键验证脚本(重建 + 单测 + 包体检,无注册副作用)。
版本依赖的账,最后是这么还的
这一阶段最深的体会是:ArcGIS Desktop 不是"一个软件",是"一个版本生态"。
- Add-In 只向前兼容,不向后兼容。用 10.8 目标编译的插件,装不到 10.2 上。客户现场用哪个小版本,你就得为哪个版本编译——这不是"打包时选一下"的问题,是 ArcObjects SDK、Visual Studio 版本、目标框架连在一起的一整套。想"顺便兼顾 10.2",成本不是加法。
- 一个工具名里的下划线,让我查了三天。arcpy 兜底走的是"动态注册一个工具箱里的脚本工具,再
gp.Execute"。它一直抛COMException 0x80004005(E_FAIL),而 GP 消息队列里显示的永远是上一条工具的历史消息、History 里没有新记录——看起来像"执行失败",实际是工具从未被执行。两个根因叠在一起:脚本工具的.pyt里参数类型写成了String(合法值是GPString),参数验证阶段就抛错并被 COM 包装成 E_FAIL;同时工具别名里带了下划线,而工具名_别名的分割规则是按最后一个下划线切,于是"工具 AICodeRunner_ai + 工具箱 runtime"被判成"工具未找到",同样返回 E_FAIL。修法是:类型改GPString、别名去掉下划线、工具名跟着改,并写了一个独立的 C# 冒烟程序,同时复现旧名的 E_FAIL 和新名的成功。教训:E_FAIL 是最没有信息量的错误码。只要 catch 里只回ex.Message而不读 GP 消息队列,你就永远在猜。补上读消息队列之后,之前那些"莫名其妙失败"的案例立刻清楚了——一个是输入路径问题(000732),一个是枚举值写法错误(000800),一个是字段不存在(000728)。 - 32 位这件事会以很奇怪的方式出现。本机 ArcGIS 10.8 是 32 位安装(在
Program Files (x86)),而官方 Add-In 的构建目标用 64 位路径去调注册工具,结果是静默失败、退出码 9009,新版本永远不自动进用户的 Add-In 列表。同时 32 位进程地址空间只有 2–4GB,导出图片偶发"内存不足,无法创建请求的位图"——和系统内存、图片尺寸、dpi 都无关。最后绕开官方导出接口,改用位图自绘 + 保存。 - 有些 API 你连"枚举"都做不到。想动态列出所有工具箱里的工具,用通配符枚举会触发扩展许可检查并挂死;工具箱对象干脆没有名称属性,只能从路径文件名和别名反推。结论是放弃动态发现,改成静态种子目录 + 官方参数顺序表,按工具名后缀静态推导别名,推不出来就不做许可检查。
- 同一套 Python,行为也和 ArcMap 里的不一样。脚本工具在 Python 窗口里暴露成"类"、
ImportToolbox重复调用会卡死、ListTools在这个版本只接受一个参数、sys.stdout.flush()直接报"错误的文件描述符"。生产路径只能用 C# 调 GP,Python 窗口只用来冒烟。 .pyt文件的编码也是坑:ArcGIS 的 COM 按系统 ANSI(中文 Windows 是 GBK)读这个文件,文件里含 UTF-8 中文就会报一个"无法打印的 IOError 对象"。基础设施代码只能写成纯 ASCII。
模型会"编造成功"
这条记在插件的踩坑记录第 32 条,不是阶段一。会话里出现「【已执行确认的操作】工具执行成功」,但那一轮根本没有发起工具调用——模型照抄了宿主回写结果的格式,自己编了一条。下一轮还基于这条谎称"文件已存在,是否覆盖"。当时 History 里没有新记录,输出目录里也没有文件。
对策是三层:确认回写改用 User 角色并打上「系统记录」标记、提示词里明确禁止、编排层加一道"防伪造成功"硬门(本轮没有工具调用记录,就拒绝展示成功文案)。这是我最想提醒同行的一条:凡是模型能生成文本的地方,它就能生成"操作结果"。
被否决的路线
我把"试过但不行"的路线也完整记了下来,因为这部分对后来者最省时间:
| 路线 | 结论 |
|---|---|
| Python Add-In + Tkinter 做聊天窗 | ArcMap 崩溃(消息循环冲突) |
| Python Add-In + wx 弹窗 | 空白窗后崩溃(同类冲突) |
| Python Add-In 手写 DockableWindow | 官方不支持该定制类型(官方类型只有按钮/工具/下拉框/工具面板/菜单/工具栏/扩展) |
| Python Add-In 取文本输入 | 该版本没有文本输入 API,做不成聊天输入框 |
| 纯外部程序调 arcpy 期望"操作当前地图" | 拿不到当前地图,不可靠 |
| 顺便兼顾 10.2 | 明确不做(SDK 降级成本远大于收益) |
| 给 ArcMap 自带 Python 装 GUI 库 | 装过并降级了 numpy,已复原;此后禁止 |
阶段三选型结论
- "实时操作当前地图"与"版本无关"是互斥的,必须在立项第一天选一边。选了实时,就要接受"按版本出插件"和"每个版本一套真机验证";选了版本无关,就要接受"只能做批处理、不碰当前视图"。这两条路没有中间态。
- 不要把"版本适配"当成打包问题。它影响的是 SDK、编译器、目标框架、甚至位数(32/64 位会以"静默失败"的形式出现,而不是报错)。
- 确定性逻辑不要交给模型现场生成。这一版的硬纪律是"ArcPy 代码禁止模型现场生成,必须预注册"——因为 py2/py3 的语法差异、中文编码、变量作用域在 ArcGIS 里全是雷。
- 错误必须被看见。这个阶段的坑清单里,一半以上是"错误信息不完整导致的误判"。对策是三条:读全消息队列、异常不许静默、错误信息里带上修复指引(告诉用户改哪个配置项,而不是只丢一个堆栈)。
阶段四:用开源 Agent 底座,只做 GIS 业务层
为什么换思路
插件只维持了三天。阶段二那句「底座不该由我重写」还在,插件又证明:把 AI 塞进 ArcMap 进程,换来的是版本锁死。于是我换了一条路——用现成的 Agent 底座,自己只写 GIS 和业务工具。
阶段二已经把目标说清楚了:我要的不是"一个通用 Agent",而是"一个面向业务人员的 AI 工具工作台——自然语言负责编排,预置工具负责可靠执行,界面负责透明展示"。
这一阶段用的底座是 DeepSeek 开源的 Harness(以下简称底座),锁在 0.1.0-rc.6。它的定位非常契合:Agent 循环、工具调用、上下文计量、摘要续聊、上下文压缩、前缀缓存、会话持久化、沙箱执行、子代理、工作流、Web/Headless 双形态、多模型适配——全部现成。
插件阶段已经证明「实时操作当前地图」和「版本无关」没有中间态。这一阶段的技术方案里我把这件事写死,并在 8 月 31 日选定只做批处理,不再做接回 ArcMap 的薄桥。
于是我给自己定了一条纪律:底座锁版本,我只写业务层。具体是三层:
业务人员(双击一个 .cmd)
→ 启动器(启动底座 + 业务工作台,自动打开浏览器)
→ 部署脚本(把业务定制同步进独立实例目录)
→ 底座进程(锁版本 0.1.0-rc.6,独立数据目录,端口隔离)
↑ HTTP RPC + WebSocket 事件
业务工作台前端(自组 Vite + React + TS 壳,另一端口)
- 底座:npm 精确锁版本,独立数据目录、独立端口,和全局实例完全隔离(这点很关键,底座的原生能力有时是"全局"的,比如全局实例、全局技能目录、全局配置,不隔离就会出现"我改的是我的例子,生效的是另一个目录")。
- 业务定制层:按 id 隐藏无关 UI、挂品牌注入、挂 GIS 工具、挂 MCP 客户端、指定默认 agent preset。
- 业务面板:三个面板 = 三份 agent preset + 三份工具白名单(公司业务工具 / GIS 工具 / 自由执行)。面板的本质是工具白名单——不靠提示词祈求模型别乱选,直接在模型侧把不该出现的工具拿掉。
- 工具层:一份
tools.json声明所有工具,Python 包装壳统一走--request <json> --result <json>契约,另有 .NET 控制台后端承接 C# 逻辑。工具数量从 7 个一路加到 40 个左右。 - 知识层:项目细节(字段字典、本单位坐标系与编码习惯)做成按需加载的 skill 文件,删掉目录即停用,不污染模型。
这一版是真的能用:40 个工具在线、真机验证过、业务同事能用一件"点一下 .cmd 就开始干活"的产品。
交付形态的三次演进(数字都是实测的)
| 形态 | 时间 | 问题 | 结果 |
|---|---|---|---|
环境放仓库内 runtime\ | 08-19 | 四套二进制合计约 1.8GB,且别的程序无法复用 | 放弃 |
| 两个 zip(程序 + 环境)手动解压 | 08-19 | 业务人员要"解压两个包、放对位置、名字还不能改" | 改成向导 |
GisInstaller.exe 图形安装向导 | 08-20 起 | — | 双击选路径→自动下载→校验→解压→启动 |
真实的交付数字:安装向导本体 48MB(WinForms 单文件压缩后,从 107.7MB 压下来)、程序包约 130MB、环境包约 407MB(原本 677MB,移除内置的 LibreOffice 之后降了约 40%)、解压后环境约 1.2GB。3Mbps 的下载服务器上,环境包大约 19 分钟、程序包大约 6 分钟。
还有一条不那么技术、但非常真实的产品决定:Word/Excel 转 PDF 依赖本机已安装的 Office,因为把转换引擎打进环境包会让包再胖 600 多 MB。
为什么会停下来
这一阶段的踩坑记录按编号写到第 54 条(中间没有第 31 条,实际 53 条),其中一大类可以称为"改开源底座的坑"。举几个最有代表性的:
- 改前端源码的部署链路:前端是被拆成独立插件包、运行时按
?rev=版本号加载的,所以"我构建了但没生效"是常态。正确链路是"构建客户端包 → 构建 Web → 复制产物到项目根的node_modules→ 重启",漏一步就是"产物是新的、跑的是旧的"。而这一切的前提是看懂它的插件加载机制。 tools.restrict({allow})管不到 preset 自己注册的工具。我原本以为"给面板一个工具白名单"就隔离干净了,读了源码才发现这句限制的语义是"只过滤继承来的层,从不过滤自己这一层注册的工具"——业务工具挂在全局层,管得到;而 shell、文件读写这些是 preset 本层注册的,管不到,模型可以绕过写操作确认门直接读写文件。最后的解法是两层:白名单 + 逐个disabled+ 一个"单调守卫"硬拦底座写操作。- 审批确认门不是自动触发的。"并发不安全"只是并发分组,不等于要确认;而且审批服务必须用容器访问器拿(用属性访问在运行时直接抛异常,可选链都救不了)。没有确认应答方时会走"失败即拒绝"——结果就是模型把"未获确认"理解成"这次没成,重试一次",同一个工具、同样的参数,反复调用十几分钟不停。
- 改皮肤改不动 CSS Module 的哈希类名:React 的类名是哈希的,全局 CSS 命中不了。最后改用稳定属性选择器(
[role='treeitem'][aria-selected='true']这类)+ 特异度压制,做到零源码改动。 - 打 zip 的中文名乱码:Windows 自带的 bsdtar 按系统 ANSI 代码页写条目名,且不设 UTF-8 标志位,本机解压正常、别人的 UTF-8 工具解压就是乱码,而它的
--hdrcharset/zip:utf8选项根本不支持。最后程序包改用 .NET 的ZipArchive.CreateEntryFromFile(自动置 UTF-8 标志位),环境包因为全 ASCII 文件名继续用 bsdtar。 PYTHONNOUSERSITE=1把包装进用户 site 的依赖隔离掉了:为了不串宿主机的包,环境里屏蔽了用户 site;结果某个工具缺 Pillow,因为它是被装进用户 site 的。依赖必须装进自带 Python 本体,不能用--user。- 一个占位符漏替换 = 交付包退化成纯聊天壳:配置文件里某个插件的路径写死了开发机绝对路径,而按
file://加载失败时静默无报错——换台电脑,几十个工具全部注册失败,界面上什么都不显示,日志里也什么都没有。 - 工具脚本名不能和标准库重名:
tools/py3/statistics.py把标准库statistics遮蔽了,导致geopandas顶层from statistics import mean直接失败。
把这些坑按类归一下,会看到很清楚的规律:大约三分之一的坑不是"我写错了",而是"我依赖了底座的内部机制"——插件加载方式、客户端包打包方式、审批 waterfall、事件帧结构、服务依赖图(哪个 UI 包不可禁用、哪个包硬依赖另一个包的服务)、按 id 隐藏 UI 的清单、以及"官方仓库版本号和 npm 发布版本号不一致,只能靠比对构建产物来确认源码等价"。
这些东西有一个共同点:它们没有稳定契约。上游是 rc 版本,还在快速迭代,每次升级都要重新确认一遍。
同时还有两个更硬的边界:
- 底座没有官方 exe,只有 npm 包。要让业务同事双击一个图标就能用,就得自己打包"Node 运行时 + 依赖 + 配置"——安装后 1.2GB 起。
- rc 版本拒绝绑定
0.0.0.0,没有 TLS、没有鉴权。"一台服务器一个网址给大家用"这件事,当时直接做不了,只能每台机器跑一个 localhost 实例。
最终我停在了这里。不是因为它不能跑,而是因为"给业务人员用"和"底座面向技术人员设计"这两件事的方向是相反的:
- 它的导航结构是给开发者的(会话、工作区、插件清单、设置体系、命令面板、工作流、子代理、技能);
- 它的默认人设是 coding agent,改人设要去改 agent preset 而不是改界面——我改了半个月皮肤才明白"改界面不会改'你是谁'";
- 它的能力展示方式是技术化的(token 计量、上下文窗口、协议状态);
- 它的升级方式要求使用者理解插件体系与构建链路。
这些对开发者都是优点。对不写代码的业务同事,全是看不懂的东西。我当时文档里写过一句自我纠偏,现在看是最准的一句:
这个程序不是通用 Agent,而是面向业务人员的 AI 工具工作台。
而底座的产品假设,和这句话并不完全一致。
阶段四选型结论
- "能不能跑通"和"能不能交付"是两件不同的事。判断标准要分开:前者看链路(自然语言 → 底座 → 工具 → 结构化回传),后者看别人电脑上双击能不能用。这一阶段两件事都做到了,但第二件事的成本被我低估了整整一个量级。
- 业务人员友好度的评价标准,和开发者友好度是反的。开发者要"能力全、可配置、可扩展";业务人员要"只有一个按钮、结果是确定的、出错能看懂"。一个面向技术人员的底座,越强大就越难对业务人员友好——不是它做得不好,是目标用户不同。
- 锁版本能解决冲突,但会锁死自己。要维护的成本约等于「定制点数量 × 底座升级频率」。当定制点有几十处、底座还在 rc 阶段时,这个乘积只会越来越大。决定基于某个开源底座做二次开发之前,先数一数你打算动它多少处内部机制;动得越多,越要先想清楚"上游更新了怎么办"。
- 依赖上游内部机制的地方,必须逐个登记。我后来养成了一个习惯:凡是"按 id 隐藏 UI""按源码改某个包""按事件帧取数据"这类改动,全都单独列表并注明"升级基线时需重新应用"。这份清单是这一阶段最有价值的副产品。
- 面板 = 工具白名单,是收敛混乱最有效的一招。不要在提示词里祈求模型少选工具,直接在它面前把工具拿掉。
阶段五:借开源引擎,做面向业务人员的国产化通用 Agent
换了一个更重要的问题
前四个阶段,我一直在问"怎么让 Agent 做 GIS 的事"。到第四阶段结束后,我把问题换了:
怎么让一个不写代码的业务同事,在自己电脑上双击一个东西,就用上 Agent?
这个问题变了之后,选型标准也跟着变了:
- 不再关心"我能不能改它的每一行"(阶段一的执念),而关心"它能不能被完整地隔离、锁定、打包、离线运行";
- 不再关心"能力多不多",而关心"业务同事看到的东西是不是他能懂的";
- 不再关心"我用什么模型",而关心"国内可用的模型服务能不能稳定接上,并且不绑定某一家"。
答案落在了 Codex 开源出来的引擎上。它满足几个我在第四阶段想要的、但当时没拿到的条件:有官方的本地引擎与客户端协议、有沙箱与审批模型、能自编译锁定版本、能在 Windows 上以绿色包交付。
现在的架构(第五阶段)
绿色 ZIP(解压即用,数据落 exe 同级 data\)
├─ 中文桌面壳(Electron):聊天 + 审批 + 计划 + 终端 + 诊断
│ ↕ 子进程 + stdio JSON-RPC(app-server 协议,壳崩溃不影响引擎与数据)
├─ 自编译引擎(锁定上游版本,随包交付)
│ ↕ Responses 兼容 API → 用户自己选的模型服务(不默认、不绑定某一家)
└─ 内置运行时:Python + 办公与空间基础库 + 中文字体(只用自己的,不探测客户机)
└─ 应用扩展层:Agent 需要时可把额外库装进应用自己的扩展目录
几条我这次特意定死的边界:
- 锁定引擎版本 + 登记每一处上游改动。每一处改动都记录"改了哪个上游文件、为什么改、升级时怎么验证"。
- 只用自己的 Python,不探测客户机的 Python,也不探测客户机的 ArcPy。客户机上可能装着 ArcGIS 自带的 Python 2.7——语法老、绑许可、不可分发,一旦 Agent"顺手用了它",就是无穷的兼容问题。
- 模型入口只做"用户主动选择",不默认、不绑定某一家;远程接口必须 HTTPS,密钥只在本机加密保存、不进 Git、不进 Agent 的命令环境。
- 界面不暴露技术数字。token 数、上下文百分比这类需要用户"自己匹配"的数值,只在接近上限时给一条可操作的提醒;原始数字收进设置的诊断面板,供排查用。
- "让用户手改配置文件"是例外手段,不是产品形态。凡是必须手改的,同时给备份、编码、校验说明。
- 国产模型的适配要真测,不能看清单。我实测过国内主流服务的 Responses 兼容性:并行工具调用、托管搜索开关、图片输入、上下文窗口口径("声明窗口"和"引擎实际可用窗口"是两个不同的数,界面里做相等比较会永远不相等)、思考模式不支持强制指定工具选择(返回 HTTP 400)——这些都必须用真实请求验证一遍,模型清单里可见不代表 Agent 兼容。
现在的状态(不吹)
通用底座部分(会话、执行、审批、计划、终端、Git、多窗口、内置 Python 运行时与完整依赖锁、绿色包与包内校验)已经跑通并出过包;面向业务的 GIS 专项 Agent 还没开始。
我这次刻意不着急做专项,原因就是前四个阶段最大的教训:先有一个稳定的通用基线,专项才是"在这些工具后面挂脚本";基线不稳的时候做专项,等于在一个还会变形的地基上盖房子。
不过在写这篇总结的这几天,我起了第五阶段之后的下一步:一个自研渲染的桌面 GIS 面板(Rust + wgpu),先把 FileGDB / SHP 的大数据只读浏览做扎实——基准规模是县区级约 30 万要素、100 万原始顶点,验收口径是 1080p 下平移缩放 ≥30 fps,并且同时看 P95 帧耗时和最大停顿,而不是只看平均值。
为什么又回到自研?因为这里和 Agent 底座是两件不同性质的事:Agent 底座是"已经有人做得比我好的通用基础设施",而"在我们自己的机器上、用我们自己的数据把几十万要素的 GDB 流畅画出来",是买不到、也没法外包给一个通用 Agent 的渲染与资源管理问题。它也承接了阶段三留下的那个没解掉的矛盾——实时交互与版本无关不可兼得,那就自己做一个不绑任何 GIS 版本的显示层,只做只读浏览这一段。
需要说明的是:这一项刚刚开工,目录里只有 GDAL 配置与文档,还没有应用源码,所以它不构成任何"已完成"的承诺。我把它写在这里,是因为它正好是这五个阶段推出来的下一步,而不是因为它已经有结果。
阶段五选型结论
- 通用能力用开源引擎,行业能力自己写,并且把两者边界划在"进程/协议"上(子进程 + 标准协议),这样以后换引擎的代价可控。
- 交付形态要一开始就定,并且按"业务同事的电脑"来定:没有 Node、没有 Python、没有 VS、Office 可能是 2010 也可能是 365。绿色包 + 内置运行时,是我目前找到的唯一能覆盖这种环境的形态。
- 隔离意识要比功能意识更早建立:不读宿主机的配置、不用宿主机的凭据、不连不该连的地址、密钥不进命令环境。
- 可验证性就是交付能力:解压后能不能跑、包内版本对不对、改过的上游文件有没有漏、依赖锁会不会漂移。这些如果没有脚本自动查,靠人记一定会出事。
五阶段拉通看:10 条经验
- 不要自己写 Agent 循环,除非你的业务就是 Agent 循环。底座能力(工具循环、上下文、压缩续聊、会话、沙箱、多模型)是最不值得重写的部分;重写一遍只换来"略懂原理"和一份只有你会修的代码。
- "能跑通"和"能交付"之间隔着业务同事的电脑。永远按"没有开发环境、不会看日志、不敢改配置"的标准来评估交付。
- 面向技术人员的底座,越强越难对业务人员友好——这不是缺点,是目标用户不同。选底座前先明确:谁用、他看见的第一个界面是什么。
- 语义层的错误不可自纠错。程序"成功"而结果错误(坐标系、编码、字段匹配、静默覆盖、
.0精度)是行业 Agent 最大的风险源,必须在工具层用校验、确认门、前后对比兜住。 - 一个引擎就够,多模态是乘法不是加法。每多一个识别/生成引擎,都要多一份配置、体积、兼容矩阵和"模型选错了"的排查成本。
- 确定性的处理沉淀成预置工具,别让模型现场写代码。工具名、参数、默认值、错误文案,都要替模型兜底——它会把参数猜成完全不同的东西,也能不调用工具却写出「工具执行成功」。
- 基于开源底座二次开发前,先数清楚你要动它多少处内部机制。维护成本 ≈ 定制点数量 × 上游迭代频率;并把这些定制点逐个登记成"升级时要重新应用"的清单与验证方法。
- 面板即白名单。把不该出现的工具从模型面前拿掉,比在提示词里反复祈祷有效得多。
- 只能用真实数据和真实界面验证。单测全绿 + 投递正确,仍然可能被下游一个"从未被赋值的变量"挡死而毫无效果。要在意的不是"代码写了",是"用户看得见的结果变了"。
- 把"只能手工验"的东西脚本化,把踩过的坑写成可检索的四段式(现象 / 根因 / 处理 / 关键词)。我五个阶段的文档加起来,最省时间的部分不是架构图,是那份踩坑清单——它让我在换电脑、换版本、重装环境时不用重新踩一遍。
三个必须早做的取舍
- 实时 vs 版本无关(第三阶段的核心取舍):要"操作当前打开的地图"就必须进进程,进了进程就必须绑版本。没有中间路线。
- 自己长 vs 跟着上游长(第四阶段的核心取舍):自己长,进度可控但天花板是你的时间;跟着上游长,天花板高但每次升级都要重涂定制。选后者,就要接受"锁版本"和"永久维护差异清单"。
- 通用优先 vs 专项优先(第五阶段的核心取舍):我选了先通用后专项。理由是前面四次教训——地基变形的时候,任何在地基上加的东西都要跟着重做一遍。
关于开源与后续
各阶段的详细技术文档(架构、踩坑记录、否决路线、发布与交付流程)我都在本地按"改代码必须同轮同步文档"的纪律维护着,踩坑记录是按编号只追加、不改写的——里面既有我的判断,也有我判断错了之后的更正留痕。
我暂时没有把这些仓库公开,主要是两方面的顾虑:一是里面还包含公司具体业务的实现细节,二是很多记录是"当时的现场",需要整理成对别人也成立的形式。
但上面提到的那一步是个例外:自研渲染的 GIS 面板(Rust + wgpu)里没有公司业务数据,只有渲染、空间索引、资源预算这些通用问题。我倾向于把它做成公开项目——这类"几十万要素在普通显卡上流畅显示"的工程细节,公开出来对同行比放在我硬盘里有价值。如果你对这块感兴趣,直接写信告诉我,我按需求整理发布节奏。
如果这篇分享对你有用、你希望看到前面某一阶段的完整实现(比如声明式工具注册、审批确认门、桌面壳与引擎的隔离姿势、GIS 工具的参数校验),也写信告诉我,我按需求把对应部分整理出来。
关于 GIS 与 Agent 的结合,我目前最想找人讨论的是四个问题:
- 成果质检这类"语义正确性"问题,除了沉淀确定性工具,还有没有别的路子?(我的立场是:先把能确定的都做成工具,剩下的才交给模型,但这个"剩下的"边界画在哪里,我没有答案。)
- ArcGIS 与 QGIS 两条生态,Agent 的接入层能不能共用?我现在的做法是"GIS 能力全部下沉到脚本/命令行工具,Agent 只负责选工具、填参数、看结果",代价是实时交互弱。
- 面向业务人员的 Agent,"透明"和"简单"怎么平衡?我现在的取舍是:默认隐藏过程,但执行参数与结果必须可查;一键执行可以,但"做了什么、覆盖了什么"必须报告。
- "自己做一个显示层"是不是重复造轮子?我在第五阶段明确说过"不要重写底座",现在又回头自己写渲染——这两件事的边界我认为是清楚的(前者是通用基础设施,后者是绑我们数据与硬件的显示问题),但如果你有不同看法,我很想听。
联系我
- 邮箱:GuoJiuSi@outlook.com / 2860418463@qq.com
- 方向:GIS 开发、空间数据处理与成果自动化、行业 Agent 工程化落地
- 欢迎交流,尤其是"你也踩过同一个坑"的那种交流 —— 这类问题往往一句话就能省别人三天。
本文基于本人五个阶段项目的真实架构文档、踩坑记录与发布记录整理,涉及的具体版本、体积、耗时、报错码与断言数量均来自当时的实测记录。文中提到的"下一步"(自研渲染的桌面 GIS 面板)刚刚开工,不构成已完成的能力承诺。全文不涉及任何客户信息、项目标识与凭据。
转载与引用请注明作者与出处。