GPUI能不能做地图引擎?我测了一把
事情得从一次选型争论说起。
作为创新研究的踩地雷专业户,虾神准备探索下一代桌面的地理信息系统渲染技术,技术选型的时候经历了一系列的纠结。用 Cpp/QT 吧,老兵不死但日渐凋零(其实还是因为我不会),虽然目前主流的GIS都是,例如QGIS 就是例子。用 Electron 吧,内存消耗先不说,JS的渲染性能能让你怀疑人生,打开一个空窗口先吃你 500MB,我就破笔记本跑起来,CPU风扇响得跟锯木头似的。用 Flutter 吧,就更不用说了,性能好是好了,但是桌面端还是后妈养的,踩坑得自己填。
这时候突然想起,我这段时间写Rust直接从VSCode转移到了Zed上面,它本身就是用 Rust 写的,启动快得离谱,它底层那套 UI 框架叫 GPUI,据说渲染性能很猛。
GPUI 简介
GPUI,全称GPU-accelerated UI framework(GPU 加速 UI 框架),听这个名字,就感觉到,渲染肯定是他的基本功。 它是 Zed Industries(高性能代码编辑器 Zed 的开发商)于 2025 年 10 月 v0.2.0 正式开源的GUI库,此前为 Zed 内部框架。它的设计目标就是替代 Electron,解决高内存、启动慢、渲染卡顿问题;致力于使用用 Rust + GPU 渲染实现60fps 流畅交互。
他的渲染引擎是基于 wgpu(WebGPU 的 Rust 绑定),底层砍掉了Web 中间层,直接进行 GPU 渲染,性能接近底层原生应用。
但是——
- 这玩意儿刚刚出到0.2版本,还没有进入生产可用版,后面API改动的幅度可能还挺大。
- 他本来说用来做IDE的,从来没人用 GPUI 做过地图渲染,一个都没有。文档里也找不到 polygon、tile、map 这些关键词。
所以就有了这么个想法:要不先写个小 demo,看看 GPUI 的地图渲染能力到底行不行。至于数据嘛,本来shapefile是最好的,但是为了减少依赖,不考虑其他包,而GeoJSON 结构简单,直接用来作为测试数据源正合适。
说干就干。
架构
整个测试项目分两层。底层是功能性代码,跟 UI 无关:读文件、解析坐标、几何计算、多边形三角剖分。上层是 GPUI 渲染,负责把几何数据画到屏幕上。为了分工清楚,我把功能代码放到 util.rs,渲染逻辑留在 main.rs。
依赖极简,就两个 crate。GPUI 负责窗口管理和 Canvas 渲染,serde_json 负责解析 GeoJSON。如果一个 UI 框架连最基本的路径绘制都做不好,那地图引擎也就无从谈起了。
[package]
name = "gpui-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
gpui = "0.2.2"
serde_json = "1.0.150"
数据
GeoJSON 的全称是 JavaScript Object Notation for Geographic Features,说白了就是用 JSON 表示地理要素。结构很直观:一个 FeatureCollection 里面装了一堆 Feature,每个 Feature 有属性(properties)和几何形状(geometry)。
几何形状我选用的是面要素,这种复杂类型比较容易测出效果和性能来,特别是还有麻烦的MultiPolygon。
选择 GeoJSON 作为测试数据,主要原因是结构简单,解析成本低,不依赖第三方 GIS 库。
因为没有使用geo-types、geo等已经封装好的数据库结构库,所以我们还得自己定义下数据结构:
// ================================================================
// 数据结构定义
// ================================================================
/// 一个多边形环(外环或内环)。
/// 每个 Ring 由一系列 (经度, 纬度) 坐标对组成,首尾坐标相同(自闭合)。
/// - 外环(outer ring):定义多边形外部边界,应为顺时针(CW)绕行。
/// - 内环(inner ring):定义多边形内部孔洞,应为逆时针(CCW)绕行。
pub type Ring = Vec<(f64, f64)>;
/// 一个多边形,包含一个外环和零个或多个内环(孔洞)。
/// - `rings[0]`:外环(outer ring),定义多边形的主体边界
/// - `rings[1..]`:内环(inner rings),定义多边形内部的孔洞区域
#[derive(Clone)]
pub struct PolygonData {
pub rings: Vec<Ring>,
}
/// 一个地理要素,对应 GeoJSON FeatureCollection 中的一个 Feature。
/// 例如一个城市或行政区,包含名称和其几何形状。
/// 一个要素可以拥有多个多边形部件(MultiPolygon 结构)。
#[derive(Clone)]
pub struct Feature {
pub name: String,
pub polygons: Vec<PolygonData>,
}
/// 解析后的 GeoJSON 数据,包含要素列表和全局包围盒。
/// 作为 MapView 的数据源,与 GPUI 渲染逻辑分离。
#[derive(Clone)]
pub struct GeoData {
pub features: Vec<Feature>,
pub geobounds: (f64, f64, f64, f64),
}
GeoJSON 解析用 serde_json 的 Value 类型做无结构解析,不需要定义反序列化结构体,灵活且代码量少。核心逻辑是判断 geometry 类型后,逐层剥开 coordinates 数组:
// ── 解析 FeatureCollection ──
if let Some(features_arr) = val["features"].as_array() {
for feature_val in features_arr {
let name = feature_val["properties"]["name"]
.as_str().unwrap_or("unknown").to_string();
let geom = &feature_val["geometry"];
let geom_type = geom["type"].as_str().unwrap_or("");
let mut polygons = Vec::new();
match geom_type {
"MultiPolygon" => {
if let Some(polygons_arr) = geom["coordinates"].as_array() {
for poly_val in polygons_arr {
let mut rings = Vec::new();
if let Some(rings_arr) = poly_val.as_array() {
for ring_val in rings_arr {
rings.push(parse_ring(ring_val));
}
}
polygons.push(PolygonData { rings });
}
}
}
"Polygon" => {
if let Some(rings_arr) = geom["coordinates"].as_array() {
let mut rings = Vec::new();
for ring_val in rings_arr {
rings.push(parse_ring(ring_val));
}
polygons.push(PolygonData { rings });
}
}
_ => {}
}
features.push(Feature { name, polygons });
}
}
let geobounds = compute_geobounds(&features);
GeoData { features, geobounds }
嵌套数组的深度取决于几何类型:Polygon 是三层嵌套 [[[...]]],MultiPolygon 是四层 [[[[...]]]]。每一层都有对应的语义,解析的时候需要搞清楚当前层代表的是要素、多边形还是环。
投影
地理坐标是球面上的经纬度,屏幕坐标是平面上的像素。要把前者映射到后者,需要一次简单的线性变换。先用全局包围盒算出经纬度范围,再根据 Canvas 的宽高确定缩放比例。为了让地图周围留白,缩放时只用了可用空间的 85%。
纬度的 Y 轴需要取反,因为屏幕的原点在左上角,Y 轴向下递增,而地理纬度向上递增。如果不做这个反转,画出来的地图就是上下颠倒的。
/// 将地理坐标(WGS84 经纬度)投影到屏幕像素坐标。
///
/// 投影原理:
/// 1. 计算全局包围盒的跨度 (range_x, range_y)
/// 2. 根据 canvas 尺寸 + 85% 边距计算缩放比例
/// 3. 以地理中心为原点归一化坐标,乘以 scale
/// 4. 加上 canvas 中心偏移,得到屏幕像素位置
/// 5. 纬度 Y 轴取反(屏幕 Y 轴向下,地理纬度向上)
pub fn geo_to_screen(
lon: f64, lat: f64,
geobounds: (f64, f64, f64, f64),
canvas_size: Size<Pixels>,
) -> (f64, f64) {
let (min_x, max_x, min_y, max_y) = geobounds;
let range_x = (max_x - min_x).max(1.0);
let range_y = (max_y - min_y).max(1.0);
let w = canvas_size.width.to_f64();
let h = canvas_size.height.to_f64();
let scale_x = w / range_x * 0.85;
let scale_y = h / range_y * 0.85;
let scale = scale_x.min(scale_y);
let geo_cx = (min_x + max_x) / 2.0;
let geo_cy = (min_y + max_y) / 2.0;
let sx = w / 2.0 + (lon - geo_cx) * scale;
let sy = h / 2.0 - (lat - geo_cy) * scale;
(sx, sy)
}
包围盒的计算就是遍历所有坐标,找到四个极值。这个计算只在启动时做一次,后续的投影计算是 O(1) 的,不会成为性能瓶颈。地理行政区划有大量的要素都是由巨多个点构成的,例如内蒙的呼伦贝尔市的边界有超过 1500 个坐标点,在做投影计算的时候,要在 f64 精度下完成,才能保证地理坐标到屏幕坐标的精确映射,这一块计算量很大,后期可以考虑改成并行计算来实现,这里因为是做Demo,所以暂时暴力串行。
剖分
GPUI本身不具备基本的渲染API,所以要进行绘制,依赖的是 paint_path 方法,这是类似于web里面canvas、svg的技术路线。最直接的做法是把多边形所有的点连成一个闭合路径,然后交给 paint_path 去填充。这是最简单的方法,但也最容易出问题。
第一个问题来了……
- GPUI的底层用 “三角扇形(Triangle Fan)” 填充,因为:
- 2D UI 形状天然适配
- 顶点复用极致省内存
- wgpu 渲染管线友好且速度快

三角扇形(TRIANGLE_FAN)是 GPU 的一种基础图元:
- 第一个顶点是公共中心点
- 后续每两个点和中心连成一个三角形
- n 个顶点 → n−2 个三角形,全部共享中心顶点
使用三角扇形,除了完美适配 UI 的凸多边形以外,它特别是在渲染圆形、椭圆、圆弧、圆角、渐变这种无限逼近的形状的时候,能够平滑度可控,且可以自动插值,让它能代码极简、性能极高。
在于 GPUI 底层用的是三角扇形填充——从第一个顶点出发,连接所有其他顶点。对于凸多边形,这么做没问题。但对于城市边界这种典型的凹多边形,扇形三角形会延伸到多边形外部,覆盖到相邻的区域。下面是用山西市级行政区划绘制边界就是个很好的例子,用扇形填充会溢出到隔壁的地盘上。

而且如果自己把多边形切成三角形,每个三角形单独画,结果出现了一堆白色细线。原因是相邻三角形的共享边在抗锯齿渲染时产生了半透明叠加,形成了接缝。仔细看下图:

后来在 GPUI 的 Path API 文档里找到了 move_to 方法。这个方法抬笔移动,不画线。把所有三角形放在同一个 Path 里,用 move_to 分隔,一次 fill 完成全部填充:
// ── 外环:三角剖分 + move_to 合并路径 ──
let tris = triangulate(outer);
if !tris.is_empty() {
let t0 = &tris[0];
let mut fill_path = Path::new(to_screen(&outer[t0[0]]));
fill_path.line_to(to_screen(&outer[t0[1]]));
fill_path.line_to(to_screen(&outer[t0[2]]));
for tri in &tris[1..] {
fill_path.move_to(to_screen(&outer[tri[0]]));
fill_path.line_to(to_screen(&outer[tri[1]]));
fill_path.line_to(to_screen(&outer[tri[2]]));
}
result.push((fill_path, format!("fill:{}", city_idx)));
}
然后为解决凹多边形的问题,改进了一下剖分算法,这里在AI的帮助下,搞了一个耳切法(我以前写过类似的,但是也是这次知道,这种算法叫做耳切法)
耳切法,核心理论是双耳定理——任何简单多边形至少有两个耳朵。算法循环找耳朵、切除、继续,直到只剩三个点。判断耳朵需要两个条件:顶点是凸的(叉积符号与绕行方向一致),三角形内不包含其他顶点。
/// 耳切法三角剖分,将多边形环分解为三角形列表。
///
/// 参数:
/// ring: 多边形顶点列表(首尾相同,末位是重复点)
/// 返回值:
/// Vec<[usize; 3]> — 三角形顶点索引数组,每个三角形用三个索引表示
///
/// 用途:
/// 返回的三角形可用 move_to 合并为单一 Path(避免接缝),
/// 也可单独渲染每个三角形。
pub fn triangulate(ring: &Ring) -> Vec<[usize; 3]> {
// ── 去掉末尾重复点(GeoJSON 规范要求首尾相同)──
let end = if ring.len() > 1 && ring[0] == ring[ring.len() - 1] {
ring.len() - 1
} else {
ring.len()
};
if end < 3 { return vec![]; }
let mut indices: Vec<usize> = (0..end).collect();
let mut triangles = Vec::new();
// ── 有向面积判断绕行方向 ──
let mut area = 0.0;
for i in 0..end {
let j = (i + 1) % end;
area += ring[i].0 * ring[j].1;
area -= ring[j].0 * ring[i].1;
}
let is_ccw = area > 0.0;
// ── 循环切耳 ──
while indices.len() > 3 {
let len = indices.len();
let mut ear_found = false;
for i in 0..len {
let prev = ring[indices[(i + len - 1) % len]];
let curr = ring[indices[i]];
let next = ring[indices[(i + 1) % len]];
// 凸性检查:叉积符号须与绕行方向一致
let cross = cross_2d(prev, curr, next);
if is_ccw && cross <= 0.0 { continue; }
if !is_ccw && cross >= 0.0 { continue; }
// 检查三角形内是否包含其他顶点
let mut is_ear = true;
for &idx in &indices {
if idx == indices[(i + len - 1) % len]
|| idx == indices[i]
|| idx == indices[(i + 1) % len] { continue; }
if point_in_triangle(ring[idx], prev, curr, next) {
is_ear = false; break;
}
}
if is_ear {
triangles.push([indices[(i + len - 1) % len], indices[i], indices[(i + 1) % len]]);
indices.remove(i);
ear_found = true;
break;
}
}
// ── 强制切耳(容错)──
if !ear_found {
let mut best_i = 0;
let mut best_cross = f64::MAX;
for i in 0..indices.len() {
let prev = ring[indices[(i + indices.len() - 1) % indices.len()]];
let curr = ring[indices[i]];
let next = ring[indices[(i + 1) % indices.len()]];
let cross = cross_2d(prev, curr, next).abs();
if cross < best_cross { best_cross = cross; best_i = i; }
}
triangles.push([
indices[(best_i + indices.len() - 1) % indices.len()],
indices[best_i],
indices[(best_i + 1) % indices.len()],
]);
indices.remove(best_i);
}
}
if indices.len() == 3 {
triangles.push([indices[0], indices[1], indices[2]]);
}
triangles
}
对于带孔洞的复杂多边形,严格意义的耳朵可能不存在。算法会在这种情况下退一步,选择最接近凸点的顶点(叉积绝对值最小)强制切除。这个容错机制确保了三角剖分不会因为几何退化而中断。
辅助函数 cross_2d 计算三个点的叉积,判断转向方向。point_in_triangle 用同侧法判断点是否在三角形内部,即分别计算点相对于三条边的叉积,如果三个符号相同则在内部。
/// 计算二维叉积 (B-A) x (C-B)。
/// 正值 = 左转(逆时针转向),负值 = 右转(顺时针转向)。
fn cross_2d(a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> f64 {
(b.0 - a.0) * (c.1 - b.1) - (b.1 - a.1) * (c.0 - b.0)
}
/// 判断点 P 是否在三角形 (A, B, C) 内部(含边界)。
/// 使用同侧法:分别计算 P 相对于三条边的叉积;
/// 若三个符号全正或全负,则 P 在三角形内部。
fn point_in_triangle(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
let d1 = (b.0 - a.0) * (p.1 - a.1) - (b.1 - a.1) * (p.0 - a.0);
let d2 = (c.0 - b.0) * (p.1 - b.1) - (c.1 - b.1) * (p.0 - b.0);
let d3 = (a.0 - c.0) * (p.1 - c.1) - (a.1 - c.1) * (p.0 - c.0);
let has_neg = d1 < 0.0 || d2 < 0.0 || d3 < 0.0;
let has_pos = d1 > 0.0 || d2 > 0.0 || d3 > 0.0;
!(has_neg && has_pos)
}
描边
GPUI 没有提供专门的描边方法。好在官方示例里有个技巧:创建一个细长的平行四边形路径,用 fill 来模拟 stroke。对每条边,判断它更接近水平还是垂直,然后在垂直方向偏移 1px。
// ── 外环逐边描边 ──
// 为外环的每条边创建一条 1px 宽的薄条路径。
// 原理:painting 示例中"绘制线条"的技巧——
// 沿线段创建一个细长的平行四边形,用 fill 模拟 stroke。
// 对于水平边(dx > dy),垂直偏移 1px;
// 对于垂直边(dy >= dx),水平偏移 1px。
let end = outer.len() - 1;
for i in 0..end - 1 {
let a = to_screen(&outer[i]);
let b = to_screen(&outer[i + 1]);
let diff_x = (b.x - a.x).abs();
let diff_y = (b.y - a.y).abs();
let (off_x, off_y) = if diff_x > diff_y {
(px(0.0), px(1.0))
} else {
(px(1.0), px(0.0))
};
let mut seg = Path::new(a);
seg.line_to(b);
seg.line_to(point(b.x + off_x, b.y + off_y));
seg.line_to(point(a.x + off_x, a.y + off_y));
result.push((seg, String::from("stroke")));
}
比较大的面要素,例如呼伦贝尔市的边界有超过 1500 个坐标点,每帧要生成 1500 个薄条路径。更准确地说,对于一条 N 个点的边界,描边需要 N-1 条薄条。GPUI 是 GPU 加速的,所以即使要素数量再多,也能保持 60 帧。如果 GPUI 没有 GPU 加速,这个方案根本跑不动。
孔洞
GeoJSON 规范规定多边形的外环是顺时针方向,内环(孔洞)是逆时针方向。这是有意为之的——渲染器可以通过绕行方向来判断一个环是外边界还是内边界。内蒙古自治区的数据中就有带孔洞的要素,比如呼和浩特市的主体边界内包含了其他行政飞地。
处理方式是把内环也用三角剖分剖开,然后用全不透明的白色填充,从视觉上挖空。白色正好是背景色,所以看起来就像是在城市填充色上挖了一个洞。如果三角剖分失败了一个复杂几何形状偶尔会出现这种情况,后备方案是直接用单路径闭合填充。
如果不处理这个问题的话,就是这样的:

处理完之后就变成这样了:

// ── 处理内环(孔洞)──
// GeoJSON 规范中,内环应为逆时针(CCW)绕行。
// 用白色(背景色)填充内环区域,在视觉上"挖空"孔洞。
for inner in polygon.rings.iter().skip(1) {
if inner.len() < 3 { continue; }
let hole_tris = triangulate(inner);
let hole_path = if !hole_tris.is_empty() {
let h0 = &hole_tris[0];
let mut p = Path::new(to_screen(&inner[h0[0]]));
p.line_to(to_screen(&inner[h0[1]]));
p.line_to(to_screen(&inner[h0[2]]));
for tri in &hole_tris[1..] {
p.move_to(to_screen(&inner[tri[0]]));
p.line_to(to_screen(&inner[tri[1]]));
p.line_to(to_screen(&inner[tri[2]]));
}
p
} else {
// 后备:单路径闭合填充
let mut p = Path::new(to_screen(&inner[0]));
for coord in &inner[1..inner.len() - 1] {
p.line_to(to_screen(coord));
}
p
};
result.push((hole_path, String::from("hole")));
}
管线
下面介绍GPU渲染的一个关键术语:管线。
GPU 渲染管线(Graphics Pipeline)就是指:GPU 把 “3D/2D 数据” 变成 “屏幕上一张图” 的固定流水线工序。
GPU 渲染管线 = 一串固定且可编程的标准化流程,把顶点 → 三角形 → 像素,一步步变成屏幕上的颜色。
GPUI 的 canvas 提供了两个回调。
- Pre-paint 在布局完成后调用,负责计算几何路径,返回值会传给 paint。
- Paint 负责实际绘制,根据路径标签设置不同颜色。这个双回调机制是 GPUI 的特色,在布局阶段可以访问实际的像素尺寸,从而精确控制渲染内容。
最后就是标注了:城市名称的标签用 GPUI 的 Div 元素做 absolute 定位,而不是画在 canvas 上。这样做的好处是利用了 GPUI 的文本渲染能力,不需要手动处理字体和排版。标签位置用多边形质心坐标计算,质心直接简单的用是所有顶点的算术平均。
注意:真实是label 渲染,凸多边形没啥问题,但是凹多边形会导致质心跑到图形以外,生产环境下,需要使用
基于有向面积的积分公式法(也称为鞋带公式的推广或高斯公式法)来计算,这里为了省事,就不去做了。
另外,文字的位置与label点的位置也要注意计算偏离,不然容易和我演示的一样,文字会跑出去……不过同上,这里为了省事就不去做了。
// 渲染部分的代码
impl Render for MapView {
fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let features = self.data.features.clone();
let geobounds = self.data.geobounds;
let viewport = window.viewport_size();
// ── 城市标签(质心 → 屏幕坐标)──
let labels: Vec<(f64, f64, String)> = features.iter().filter_map(|f| {
f.polygons.first()
.map(|poly| polygon_center(poly))
.map(|(cx, cy)| {
let (sx, sy) = geo_to_screen(cx, cy, geobounds, viewport);
(sx, sy, f.name.clone())
})
}).collect();
let label_elements: Vec<Div> = labels.into_iter().map(|(x, y, name)| {
div().absolute().left(px(x as f32)).top(px(y as f32)).child(name)
}).collect();
const FILL_COLORS: [u32; 11] = [
0x4A90D9, 0xE86C00, 0x8BC34A, 0xE83E3E, 0x9B59B6, 0x20B2AA,
0xF08529, 0x3C8DBC, 0x8E44AD, 0x27AE60, 0xD35400,
];
div().bg(white()).size_full().child(
canvas(
// ── Pre-paint 回调 ──
move |bounds, _, _| -> Option<Vec<(Path<Pixels>, String)>> {
if features.is_empty() { return None; }
// ... 计算投影参数,遍历要素,构建路径列表 ...
Some(result)
},
// ── Paint 回调:按标签类型绘制路径 ──
move |_, paths, window, _| {
if let Some(paths) = paths {
for (path, tag) in paths.iter() {
match tag.as_str() {
"stroke" => window.paint_path(path.clone(), rgba(0x444444FF)),
"hole" => window.paint_path(path.clone(), rgba(0xFFFFFFFF)),
_ if tag.starts_with("fill:") => {
let idx: usize = tag[5..].parse().unwrap_or(0);
let base = FILL_COLORS[idx % FILL_COLORS.len()];
window.paint_path(path.clone(), rgba((base << 8) | 0x88));
}
_ => {}
}
}
}
},
).size_full(),
).children(label_elements)
}
}
最后就是GPUI的入口函数了,这个很简单创建 Application,打开窗口,创建 MapView 作为根视图。从文件加载数据,如果加载失败就创建一个空的数据集。
fn main() {
Application::new().run(|cx: &mut App| {
cx.open_window(WindowOptions::default(), |_window, cx| {
cx.new(|_cx| match MapView::from_geojson_file("data/中国_省.geojson") {
Ok(view) => {
for f in &view.data.features { println!(" - {}", f.name); }
view
}
Err(e) => {
eprintln!("读取文件失败: {}", e);
MapView { data: util::parse_geojson_str("{}") }
}
})
}).expect("failed to open window");
});
}
结论
测试结果比较明确。GPUI 的 Canvas 渲染管线能够处理数千条路径的同时绘制保持 60 帧,Path 的 move_to 机制为复杂的矢量图形渲染提供了灵活的拼接能力。纯 Rust 的依赖栈意味着编译出来的程序启动快、内存占用低,没有 JVM 或 V8 的开销。
目前发现的两个限制。
- 第一,GPUI 没有内置的描边 API,需要用薄条路径来模拟,对于极端复杂的边界会生成大量临时路径。
- 第二,GPUI 对于 macOS 和 Linux 平台的支持比 Windows 要好,明显感觉性能要高不少。
总的来说,对于需要高性能矢量图形渲染的桌面 GIS 工具,GPUI 是一个值得关注的方向。渲染管线是 GPU 加速的,Path API 足够灵活,开发体验也不错——就是用 Rust 写 UI 的逻辑需要适应一下。
后记
- 文章的中的绝大部分部分代码(超过90%),都是AI直接写的,我的作用就是告诉AI,应该往那个方向走,如下:
例如渲染呼伦贝尔的时候,多部件渲染颜色出来问题,我得告诉他怎么做:

又比如环形在GIS里面的特殊结构

当然,如果你不告诉他,有可能多烧上几块十几块的token,也有可能烧出来,但是你只要给出解决方案,AI解决起来那是嗷嗷的快。
- 这篇文章是在我主动蒸馏了虾神博客写作风格的skills的帮助下完成的初稿,我在初稿上做的修改完成的:
下面就虾神的赛博器灵:

我很多时候,觉得我已经废了……

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)