ARTICLE · 1294346
给 QGIS 装一个会说话的助手:插件内嵌 Agent 的四层架构
这个系列写到今天,插件已经积累了相当厚的"肌肉":数据准备篇写了四件套自动进货,基础篇写了 SWAT 建模九步,正式篇写了跨系统迁移和端到端串跑。但所有这些能力都有一个共同的入口——按钮。用户必须知道流程在哪一步、按钮叫什么名字、参数填什么。本专题要回答的问题是:能不能把这些能力交给一个模型,让用户只说"准备济南的 SWAT 分析数据并进行流域分析",插件自己完成剩下的全部编排?智能篇共四篇:本篇讲 Agent 的构建(架构怎么搭、内核怎么选、模型怎么接),第 2 篇讲工具化(插件能力怎么变成模型能调用的工具),第 3 篇讲实战串跑(济南一次对话从市名走到 success.fin,以及三根新保险丝),第 4 篇讲技能化(业务知识怎么写进指令、意图识别从哪来)。
先划边界:Agent 编排的是插件自己的核心函数——它不越权改数据文件、不替用户做专业判断,所有写操作都过权限门控。智能化改变的是"谁来按按钮",不改变"按钮背后是什么"。

四层架构:UI 层、工具与权限层、模型协议层、内核层,落脚在 QGIS 宿主上。自研的只有协议客户端、工具契约、权限门控与线程编排。
一、为什么不是"再做一个面板"
给插件加功能的第一直觉是加按钮、加面板。但面板解决不了组合问题:数据准备 × 建模 × 制图 × 率定,用户可能想"只跑到划界看看子流域数量再决定阈值",也可能想"换个城市重来一遍"。每一种组合都是一个新按钮,面板会组合爆炸。
对话式界面把"组合"交还给模型:用户描述意图,模型读工具清单,自己决定调用顺序、传递参数、检查结果。这对插件提出三个硬要求:
- 一个可靠的 agent 循环
——模型输出怎么解析、工具结果怎么回传、最多走几步,这些不应该自己发明; - 一套模型可读的工具契约
——每个能力要有机器可查的名字、描述、参数 schema; - 一套权限门控
——模型不能拥有和用户一样的权力,读可以放开,写必须确认。
三件事分别对应本篇的内核层、下篇的工具层与权限层。
落进界面上,只是插件工具条里多了一个入口:

插件工具条(真实窗口截取):地灾因子处理、边坡入渗、SWAT 建模、数据准备……最右新增的"AI 智能助手"是本专题的主角。
二、内核层:vendor,而不是自研
agent 循环(模型-工具往返、观察拼装、步数控制、流式事件)是近年开源社区打磨得很成熟的东西,自己写一个既浪费又不可靠。插件的做法是把 HuggingFace smolagents 1.26.0 的源码 vendor 进来(Apache-2.0,改造清单随包声明),提供两种现成循环:
- ToolCallingAgent
——标准 function-calling:模型输出结构化的工具调用,内核解析后执行、回传观察; - CodeAgent
——模型直接写 Python 代码作为动作,适合一次表达复杂逻辑。
UI 下拉可切换,实测函数调用模式更稳。vendor 的全部改造只有五处机械替换:包内 import 相对化(不与 pip 安装的 smolagents 冲突)、huggingface_hub 顶层 import 懒加载(插件场景永远不触发)、__init__ 只导出用到的符号、提示词模板从 vendored 子包内读取、一行循环/解析/执行器逻辑都没动。运行时依赖收敛到 rich / jinja2 / pyyaml / requests 这几个 QGIS Python 里本来就有或 apt 一条命令可得的包;torch、litellm 这些大件因为懒加载根本不会被触发。升级路径也写死了:从 PyPI 重新下 sdist,重放改造清单,跑回归测试。
三、模型协议层:469 行纯标准库
模型客户端不引任何第三方依赖,用 urllib 手写,469 行做完三件事:

配置来源、协议支持与代理策略。与 ZCode 共用同一份 provider 配置是这个设计最省事的地方。
协议双支持。anthropic-messages(SSE 流式,聚装 text_delta 与 input_json_delta 事件)和 openai-chat-completions(非流式,请求时做消息格式互转)。插件内部消息统一用 anthropic 风格,openai 协议在请求边界转换。
代理三档。auto(缺省):先直连,网络层失败才走本机代理重试一次——HTTP 4xx/5xx 是服务端应答,换通道无意义,不重试;也可以填具体 URL 强制代理,或留空禁用。国内网络环境下这一档省掉的不是代码,是每次"连不上"时的人工排障。
配置复用。缺省直接读 ~/.zcode/v2/provider_config.json 里第一条可用 provider——也就是说,Agent 用的模型配置和 ZCode 是同一份,不要求用户再填一遍 baseUrl/apiKey;UI 设置区可以覆盖,模型下拉列出配置里的全部模型,缺省 GLM-5.3-Flash(快、省额度,编排任务够用)。
协议客户端之上是一层很薄的桥(ArkModel):把 smolagents 的消息角色(tool-call / tool-response)映射成 anthropic 协议的合法角色,流式正文经回调实时转发 UI,token 用量从 SSE 的 message_start / message_delta 里取。
四、一条指令的完整旅程
把四层串起来,看"列出当前图层"这句指令在插件内部走了什么路:

从用户输入到最终答复的八步回路。步骤⑥的执行线程派发是下篇的主题。
关键的工程决策在线程模型上。agent 循环跑在 QThread 里,但 QGIS 的 API(iface、QgsProject)只能在主线程调用——于是有一套信号回环:工具执行请求发信号回主线程页面,页面执行后把结果经队列回传工作线程。而到了第 2 篇会讲到,领域工具(SWAT 数据准备、建模)恰恰不需要主线程,可以就在 agent 线程直接跑——每个工具的执行线程由契约声明、派发器统一处理,这是"工具化"篇的核心设计。
两路落盘保障可诊断与可续聊:关键链路事件(页面槽、worker 步骤、异常栈)写 logs/agent.log,界面"没反应"时对照两端日志就能定位断点;会话转录逐轮写 agent_sessions/*.json(原子替换),下次发送时回灌成内核的记忆步骤,实现多轮连续对话——第 3 篇的实战里,运行中断后一句"继续"就能从断点接着走,靠的就是它。
五、对话界面本身也要"像样"
第一版对话区是纯文本追加,长回复满屏 ## 和 **。重写后的渲染层把会话做成标准气泡:用户消息靠右(蓝底+头像)、助手回复靠左(灰底+头像),每条新提问前画轮次分隔线;助手回复的 markdown 做极简转换(标题加粗、行内代码等宽、列表转圆点),先 HTML 转义再打标,无注入风险;流式输出带光标符、80ms 防抖重绘;工具调用以彩色小字挂在助手一侧(▶ 开始 / ✓ 完成含耗时 / ⛔ 拒绝 / ⚠ 错误)。这些不是装饰——串跑时一条回复几十行,用户需要一眼分清"哪句是我说的、模型做到了哪一步"。
写在最后
这一篇的结论可以压缩成一句话:Agent 的地基里,自研的越少越好。循环 vendor 成熟开源内核,模型客户端收敛到 469 行标准库,配置直接复用既有文件;真正值得自己写的,是下面两篇的内容——把插件能力翻译成模型可读的工具契约,以及把"哪些操作危险"翻译成权限门控。地基打完,下一篇就是把肌肉接上去:工具化。
(本篇是我个人开发与测试的记录整理;文中行数、依赖清单均来自插件源码。欢迎交流指正。)
系列目录智能篇 · 插件智能化 第 1 篇 · 给 QGIS 装一个会说话的助手:插件内嵌 Agent 的四层架构(本篇) 第 2 篇 · 把插件能力做成 Agent 的手:28 个工具的契约、线程与权限 第 3 篇 · 一句"准备济南的 SWAT 数据"之后:Agent 串跑与三根新保险丝 第 4 篇 · 从工具到技能:让 Agent 懂业务的指令工程
相关系列正式篇 · 插件迁移与工程化(1–7 篇,含潍坊 SWAT 串跑复盘) 数据准备篇 · 第 1 篇 自动进货 / 第 2 篇 CMADS 实测















































































