1. 双 Agent 工作台到底解决什么问题如果你正在做全栈 GIS 项目大概率遇到过这种局面前端地图渲染要调样式、空间数据库要写 SQL、后端要封接口、最后还得部署到云上让甲方或导师能打开一个公网地址。每一块单独看都不算难但串起来就是一条很长的链路一个人从头写到尾光是环境配置和联调就能耗掉大半时间。双 Agent 工作台的核心思路是把这条链路拆成两种角色一个 Agent 负责工程实现比如初始化项目、写地图组件、建表导入数据、封 REST 接口另一个 Agent 负责质量审查比如检查依赖是否冗余、SQL 有没有注入风险、CORS 是不是放太开、部署脚本有没有把密钥写死。你负责的是需求定义和验收也就是告诉它们要做什么、判断做出来的东西对不对。这套模式特别适合全栈 GIS 原型搭建因为 GIS 项目的技术栈跨度大——前端要懂 Leaflet 或 MapLibre 的图层模型数据库要懂 PostGIS 的空间索引和 SRID后端要懂连接池和参数化查询部署还要懂 Nginx 反代和进程管理。一个人很难在每个环节都保持高水准但两个 Agent 分工后实现和审查各司其职你只需要在关键节点做判断。TaoToken 在这里的角色是统一 Key 接入层。你不需要为每个模型或工具单独申请和管理密钥而是通过一个统一的 API 入口让两个 Agent 都能调用所需的模型能力。这样配置一次前端、后端、部署脚本里的 AI 调用都走同一个 Key省掉了反复切换账号和环境变量的麻烦。2. TaoToken 前置统一 Key 与工作台初始化在开始搭项目之前先把 TaoToken 的接入配置做好。这一步不复杂但它是后面所有 AI 调用的基础。首先到官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册账号然后在控制台里创建一个 API Key。这个 Key 就是你后面所有配置里要填的东西。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 进去之后找到 API Keys 页面新建一个 Key 并复制保存。接下来是配置文件的写法。不同工具用的配置文件格式不一样我分别给出 settings.json 和 config.toml 两种骨架你可以根据自己用的 Agent 工具选择。settings.json 适合 Claude Code 这类工具{ apiKey: 你的_TaoToken_API_Key, baseUrl: https://taotoken.net/api, model: claude-sonnet-4-20250514, maxTokens: 8192, temperature: 0.3 }config.toml 适合 Codex 或其他支持 TOML 配置的工具[api] key 你的_TaoToken_API_Key base_url https://taotoken.net/api model gpt-4.1 max_tokens 8192 temperature 0.2 [project] name fullstack-gis root ./注意 base_url 统一填 https://taotoken.net/api 不要加 UTM 参数这是 API 调用的标准入口。Key 不要提交到 Git 仓库建议放在 .env 文件里然后加到 .gitignore。配置好之后你可以先用一个简单的 curl 请求验证 Key 是否可用curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer 你的_TaoToken_API_Key \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 回复 OK}], max_tokens: 10 }如果返回里有正常的回复内容说明 Key 和网络都没问题。这一步过了再往下走否则后面所有 AI 调用都会失败。3. 双 Agent 分工提示词模板与项目骨架配置好 Key 之后下一步是建立双 Agent 的分工规则。我建议在项目根目录放一个 CLAUDE.md 文件把技术栈、目录规范、提交前自检规则都写进去这样两个 Agent 都能读到同一份约定。CLAUDE.md 的内容可以这样写# 全栈 GIS 项目规则 ## 技术栈 - 前端Vite Leaflet MapLibre GL JS ECharts - 后端Express pgnode-postgres - 数据库PostgreSQL 16 PostGIS 3.4 - 部署阿里云 ECS Nginx PM2 ## 目录规范 - /frontend前端源码 - /backend后端源码 - /db建表脚本和导入脚本 - /deployNginx 配置和部署脚本 - /data原始数据不提交大文件 ## 提交前自检 - 前端npm run build 无报错 - 后端node --check 无语法错误 - SQL所有查询必须参数化 - 密钥不得出现在任何提交文件中然后是两个 Agent 的分工提示词。Claude Code 负责实现提示词可以这样写你是工程实现 Agent。当前项目是全栈 GIS 系统技术栈见 CLAUDE.md。 你的任务 1. 按需求实现功能优先保证可运行 2. 每次修改后运行构建或语法检查 3. 遇到不确定的 API 用法先查文档再写 4. 不要修改 CLAUDE.md 中的技术栈约定 5. 完成后输出修改文件列表和验证命令Codex 负责审查提示词可以这样写你是质量审查 Agent。当前项目是全栈 GIS 系统技术栈见 CLAUDE.md。 你的任务 1. 审查最近一次修改的代码 2. 检查依赖是否冗余、SQL 是否参数化、CORS 是否过宽、密钥是否泄露 3. 检查空间索引是否建立、SRID 是否一致、连接是否释放 4. 输出问题列表按严重程度排序 5. 不要直接改代码只输出审查意见项目初始化用 Vite 建前端骨架npm create vitelatest frontend -- --template vanilla cd frontend npm install leaflet maplibre-gl echarts后端初始化mkdir backend cd backend npm init -y npm install express pg cors dotenv数据库用 Docker 启动 PostGISdocker run -d \ --name gis-postgis \ -e POSTGRES_PASSWORDgis123 \ -e POSTGRES_DBgisdb \ -p 5432:5432 \ postgis/postgis:16-3.4启动后验证 PostGIS 扩展是否可用docker exec -it gis-postgis psql -U postgres -d gisdb -c CREATE EXTENSION IF NOT EXISTS postgis; SELECT PostGIS_Version();如果返回版本号说明空间数据库就绪。4. 前端地图、空间数据库与后端接口的可复制配置前端地图部分先用 Leaflet 加载一个基础底图确认地图能渲染出来import L from leaflet; import leaflet/dist/leaflet.css; const map L.map(map).setView([30.5, 114.3], 8); L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: copy; OpenStreetMap contributors, maxZoom: 18 }).addTo(map);如果要接真实遥感数据可以用 MapLibre 加载 NASA GIBS 的瓦片import maplibregl from maplibre-gl; import maplibre-gl/dist/maplibre-gl.css; const map new maplibregl.Map({ container: map, style: { version: 8, sources: { gibs: { type: raster, tiles: [ https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/ MODIS_Terra_CorrectedReflectance_TrueColor/default/ 2024-06-01/GoogleMapsCompatible_Level9/{z}/{y}/{x}.jpg ], tileSize: 256 } }, layers: [ { id: gibs-layer, type: raster, source: gibs } ] }, center: [114.3, 30.5], zoom: 5 });空间数据库这边建一张带空间字段的表CREATE TABLE stations ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, geom GEOMETRY(Point, 4326) ); CREATE INDEX idx_stations_geom ON stations USING GIST (geom);导入 GeoJSON 数据可以用 ogr2ogr也可以用 Node 脚本import pg from pg; import fs from fs; const pool new pg.Pool({ connectionString: postgresql://postgres:gis123localhost:5432/gisdb }); const geojson JSON.parse(fs.readFileSync(./data/stations.geojson, utf8)); for (const feature of geojson.features) { const [lng, lat] feature.geometry.coordinates; await pool.query( INSERT INTO stations (name, geom) VALUES ($1, ST_SetSRID(ST_MakePoint($2, $3), 4326)), [feature.properties.name, lng, lat] ); }后端 Express 接口封装一个 bbox 查询import express from express; import pg from pg; import cors from cors; import dotenv/config; const app express(); app.use(cors({ origin: http://localhost:5173 })); app.use(express.json()); const pool new pg.Pool({ connectionString: process.env.DATABASE_URL }); app.get(/api/stations, async (req, res) { const { minLng, minLat, maxLng, maxLat, limit 100 } req.query; const parsedLimit Math.min(parseInt(limit, 10) || 100, 500); if ([minLng, minLat, maxLng, maxLat].some(v isNaN(parseFloat(v)))) { return res.status(400).json({ error: bbox 参数不合法 }); } try { const result await pool.query( SELECT id, name, ST_AsGeoJSON(geom)::json AS geometry FROM stations WHERE geom ST_MakeEnvelope($1, $2, $3, $4, 4326) LIMIT $5, [parseFloat(minLng), parseFloat(minLat), parseFloat(maxLng), parseFloat(maxLat), parsedLimit] ); res.json({ type: FeatureCollection, features: result.rows.map(r ({ type: Feature, geometry: r.geometry, properties: { id: r.id, name: r.name } })) }); } catch (err) { console.error(err); res.status(500).json({ error: 查询失败 }); } }); app.get(/health, (req, res) res.json({ status: ok })); app.listen(3000, () console.log(API running on :3000));前端调用这个接口并把结果上图async function loadStations(map) { const bounds map.getBounds(); const params new URLSearchParams({ minLng: bounds.getWest(), minLat: bounds.getSouth(), maxLng: bounds.getEast(), maxLat: bounds.getNorth(), limit: 200 }); const res await fetch(http://localhost:3000/api/stations?${params}); if (!res.ok) { console.error(接口返回错误, res.status); return; } const geojson await res.json(); L.geoJSON(geojson, { onEachFeature: (feature, layer) { layer.bindPopup(站点${feature.properties.name}); } }).addTo(map); }5. 本地启动与云环境连通性验证本地启动顺序是先起数据库再起后端最后起前端。数据库已经在 Docker 里跑着确认容器状态docker ps --filter namegis-postgis后端启动cd backend node index.js看到API running on :3000就说明后端起来了。用 curl 验证健康检查和 bbox 查询curl http://localhost:3000/health curl http://localhost:3000/api/stations?minLng113minLat29maxLng116maxLat32limit10前端启动cd frontend npm run dev浏览器打开 Vite 提示的地址地图应该能渲染出来打开控制台看有没有报错。如果地图上能看到站点标记点击弹出名称说明前端、后端、数据库三层已经打通。云部署这边阿里云 ECS 上需要装 Node、Nginx、PM2 和 Docker。部署脚本可以这样写#!/bin/bash set -e # 拉取代码 cd /opt/gis-app git pull origin main # 前端构建 cd frontend npm install npm run build # 后端重启 cd ../backend npm install pm2 restart ecosystem.config.js || pm2 start ecosystem.config.js # 重载 Nginx sudo nginx -t sudo nginx -s reload echo 部署完成Nginx 配置反向代理server { listen 80; server_name your-domain.com; location / { root /opt/gis-app/frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署后在本地验证公网连通性curl http://your-domain.com/health curl http://your-domain.com/api/stations?minLng113minLat29maxLng116maxLat32limit5如果返回正常再用手机浏览器打开域名确认地图和接口都能正常工作。6. 本篇常见错排查地图空白或瓦片加载失败先看浏览器控制台的 Network 面板确认瓦片请求的 URL 是否正确。NASA GIBS 的瓦片 URL 里日期格式必须是YYYY-MM-DD图层名大小写敏感。如果返回 404检查图层名是否拼错。PostGIS 查询报 SRID 不一致建表时用了GEOMETRY(Point, 4326)插入时也必须用ST_SetSRID(..., 4326)。如果一边用 4326 一边用 3857运算符会报错。统一用 4326 存数据需要投影时在查询里转换。后端接口返回 500 但不报具体错误检查数据库连接字符串是否正确Docker 容器是否在运行。可以在后端加一行console.error(err)把错误打到日志里但返回给前端的只给通用错误信息避免泄露数据库细节。CORS 报错开发阶段前端是localhost:5173后端是localhost:3000需要在后端 cors 配置里允许前端 origin。部署后如果前端和后端同域可以收紧 CORS 配置只允许自己的域名。AI 调用返回 401 或 403检查 TaoToken 的 API Key 是否填对base_url 是否是https://taotoken.net/api。如果 Key 没问题检查请求头里的Authorization格式是不是Bearer 你的Key。部署后前端能打开但接口 502Nginx 反代的后端地址不对或者后端进程没起来。先pm2 status看进程状态再curl http://127.0.0.1:3000/health确认后端在跑最后检查 Nginx 配置里的proxy_pass地址。7. 接入文档与后续扩展这套骨架跑通之后你可以把双 Agent 的分工继续用到后续迭代里。比如要加时间轴回放功能让 Claude Code 实现日期切换和瓦片预取让 Codex 审查缓存策略和缺测帧处理。要加空间分析功能让 Claude Code 写缓冲区查询和最近邻 SQL让 Codex 审查索引使用情况和查询计划。TaoToken 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有不同模型和工具的配置示例。如果你需要管理多个 Key 或查看用量控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。API Key 管理页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 可以在这里创建和吊销 Key。如果你主要用 Claude Code 做工程实现可以参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentClaudeCodeAnthropicutm_campaignrewrite 里的接入方式。如果要做长期编码或 Agent 任务Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。想先验证模型对话效果可以直接用 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。实际用下来双 Agent 模式最省时间的地方不是写代码本身而是审查环节。GIS 项目里很多坑是隐性的比如空间索引没建导致查询慢、SRID 不一致导致空间运算报错、CORS 放太开导致安全风险。这些让另一个 Agent 专门盯比你自己反复检查要可靠得多。
