-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathrss.xml
More file actions
341 lines (338 loc) · 250 KB
/
Copy pathrss.xml
File metadata and controls
341 lines (338 loc) · 250 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>小红鸡</title>
<link>https://gitpull.cn/</link>
<description>桃李春风一杯酒,江湖夜雨十年灯。</description>
<language>zh-CN</language>
<lastBuildDate>Sat, 22 Aug 2026 06:17:42 GMT</lastBuildDate>
<atom:link href="https://gitpull.cn/rss.xml" rel="self" type="application/rss+xml" />
<item>
<title>dsh-remote——给 DeepSeek Harness 的多机远程工作区</title>
<link>https://gitpull.cn/post/20260814/</link>
<guid isPermaLink="false">dsh-remote-multi-machine-remote-workspace</guid>
<pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate>
<author>Jimmy</author>
<category>AI 基础设施</category><category>教程</category>
<description><![CDATA[一个让 Agent 在多台 SSH 机器上「选目录即工作」的插件:多机管理、双 tab 工作区选择器(本机系统文件夹 or 远程目录)、远程目录本地镜像 + 双向 SFTP 同步,全程不改 harness 核心。]]></description>
<content:encoded><![CDATA[仓库:github.com/flymysql/dsh remote npm:dsh remote(v0.5) 已收录:awesome dsh plugin DeepSeek Harness(DSH)的 Web 界面刻意只监听 (为安全拒绝 )。这带来一个隐含问题: Agent 的工作做在哪里? 答案通常是「本地工作区」。但很多人真正的代码和运行环境在 远程机器 上——编译机、测试桩、内网仓库。DSH 本身没有「连远程 + 选远程工作区」的官方能力。 就是来补这块的:它让我把若干台 SSH 机器管起来,然后在原生的「Add workspace」流程里, 要么选本机目录,要么选某台机器的某个远程目录作为工作区 。 核心能力 1. 多台 SSH 机器 设置页就是一个多机 registry:增删改、设「当前」,每台记录 。密码只存在本地( ),界面回显的是 表示已设,不回明文。 2. 双模式工作区选择器 在 DSH 原生「Add workspace」里, 用 client 半把 directory flow 槽位补齐( shadow 掉官方 native),弹出一个 双 tab 的对话框: 本机 :走 系统文件夹选择器 (host 端的原生 ),或直接输入本地路径 → 直接就是普通 DSH 本地工作区(和本地工作区共存)。 远程 :先选 机器 ,再浏览它的目录(可点进子目录),或直接输入 → 确定后把它 mirror 成本地镜像工作区。 3. 远程工作区 = 本地镜像 + 双向同步 选中的远程目录会被 真实镜像 到本地 ——它是一个 通过的真实目录 ,所以 DSH 原生工作区系统能正常收养它。然后 通过 SFTP 让它和远程保持同步: :远程 → 本地镜像(拉取) :本地镜像 → 远程(推送) 于是你(或 Agent)在镜像里的任何 都是真实地对远程项目操作,但用的是 DSH 最普通的本地文件流。 4. 模型工具 / / / / / 当前 还会注入每次系统提示,让 Agent 明确自己的工作根。 安装 装完在 设置 → 远程工作区 加机器,然后点 Add workspace 选本机或远程目录即可。 一点设计与取舍 不改 harness 核心 : 的「工作区必须是本地 路径」设计我没动,而是让 dsh remote 把远程目录 物化成一个真实本地目录 再交给它收养。这样既完全融入生态,又保持了 harness 的原有安全感(工作区就是真实文件系统路径)。 选工作区是 槽位 :DSH 的 directory flow 是一个单槽位,所以 dsh remote 以最低优先级占住它,官方 native 会选择器被 shadow;如果换成 browser 组合,它也能 fallback。 密码存本机明文 :目前按「方便」存本机文件。如果需要更严,可以锁 ACL 或用系统凭据存储——看反馈再考虑。 结尾 想解决的是「Agent 和我的真实执行环境分离」这个最常见痛点。它已经完全发布到 npm,社区列表可见。欢迎试用、踩坑、提 issue。如果大家觉得「远程工作区」值得成为 DSH 一等公民,也可以去官方讨论区反馈,作为佐证。 仓库/安装/文档:https://github.com/flymysql/dsh remote npm:https://www.npmjs.com/package/dsh remote 说明:效果图用了 SVG 占位「 UI 原型」,真实界面以此为准(我这边 headless 无法截图)。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/08/dshremote-picker.svg" />
</item>
<item>
<title>海门回声 | 黑鬼</title>
<link>https://gitpull.cn/post/20260618/</link>
<guid isPermaLink="false">海门回声-黑鬼</guid>
<pubDate>Thu, 18 Jun 2026 12:00:00 GMT</pubDate>
<author>邻家酒肆</author>
<category>海门回声</category><category>随想</category>
<description><![CDATA[黑鬼已经死了好多年了,大概在我上初中还是高中的时候。听大人们说,是在某一年的春节里,二月的天气还很冷,他像往常一样被铁链子拴在家院子前面的小屋子里,有好几天家人忘了给他送饭,便饿死了。也有人说是冻死的。我后来真的见过他——脸发青,像苔藓,不像电视剧里画着黑眼圈的饿汉。]]></description>
<content:encoded><![CDATA[黑鬼已经死了好多年了,大概在我上初中还是高中的时候。 听大人们说,是在某一年的春节里,二月的天气还很冷,海风贴着田埂吹过来,草棚上的铁皮被吹得哐哐响。黑鬼像往常一样被铁链子拴在家院子前面的小屋子里,有好几天他家人忘了给他送饭,便饿死了。也有人说是冻死的。 怎么会忘了送饭呢。哪怕是拴在家门口的一条狗,也会想起来每天拿点剩饭剩菜喂一下。我当时听到也觉得很奇怪,居然还有饿死的人,还是家人忘了送饭。人们私下里也时常嘀咕,那家人心也真是狠啊。 不过后来想想,也不奇怪了。因为我真的见过黑鬼。 黑鬼是海岛上一户人家的二儿子。那户人家生了两个儿子,两个都是傻子,只是大儿子稍微正常一些,能自己吃饭、自己走动;二儿子常年像野鬼一样在岛上各个村子里游荡,一身黝黑,神情呆滞,人们便叫他黑鬼。 小时候总能听到他在小岛各个角落留下的各种传闻。那些传闻像潮水一样,从这个村子漫到那个村子,每传一次,便多添一点细节,多添一点凶。 有人说黑鬼有次在隔壁村子公厕走错门,进了女厕。 那是个用土砖砌的旱厕,门上没有字,只有一块褪了色的布帘。黑鬼掀帘进去,里头正有个妇女蹲着,一声尖叫便从窄小的厕洞里冲出来,把附近田埂上弯腰拔草的人都惊动了。男人们扔下手里的活,拎着扁担和木棒围过来,黑鬼被打得抱头蹲在墙角,嘴里发出含混的呜声,身上那件本来就破的上衣又撕开一道口子。 后来传闻便变了样。 先是说黑鬼在女厕里"不规矩",再后来变成他"欺负"了妇女,再再后来,便像是真有其事一样,说黑鬼在隔壁村强奸了人,被村民用绳子吊在榕树下打。吊他的那棵树是哪一棵,打他的人里有没有姓林的,每个版本都不一样,但听的人总会"哦"一声,仿佛亲眼见过。 有时候晚上吃完饭,天还没完全黑透,邻居们便搬出竹椅、摇椅,坐在院门口吹风。灶里的余火还没灭,偶尔爆一颗干柴,噼啪响一声。有人摇着蒲扇,有人衔着烟,吐出来的烟圈被海风吹散。话题不知怎么就会绕到黑鬼身上——他今天又在哪个村出现,又偷了谁家的东西,又被谁家的男人追打。 "听说昨天在后埔,他跟着一个放学的女娃走了好一段路。" "哪有,是在前坑,他翻进别人灶间,把半锅还没熟的米饭端走了。" "我听讲他夜里会在坟地那边晃,专找没祭完的供品。" 女人们听到这些,便把身边的孩子往怀里拢一拢,声音压得更低,互相补充上一次自己在哪儿"碰见"黑鬼:在岔路口,在废弃土屋旁,在晒场边堆着的花生杆后面。说的人手舞足蹈,听的人频频点头,仿佛离黑鬼越近,越能证明自己胆子大、消息灵。 有时候夜里小孩啼哭,屋里的灯便亮起来,传来大人哄睡的细碎声音,有时还伴随着一两句恐吓。 "再哭就要把黑鬼引过来了,黑鬼可是会偷小孩的,再哭要被黑鬼抓走了。" 有时能奏效,但往往哭声会变得更大声。大一点的孩子会在被窝里屏住呼吸,侧耳听外头的虫鸣和风声,把任何一点窸窣都当成黑鬼的脚步。 传闻更多的,还是黑鬼到处偷东西吃。 有时候家里厨房丢了吃的,人们便懊恼地拍一下大腿,说家里来黑鬼了。晒在院绳上的鱼干少了两条,菜地旁晾的花生袋被撕开一道口,灶台上的剩饭不见了,案板上刚切好的半条鱼没了踪影——最后都会落到黑鬼头上。有时候丢的是两斤生猪肉,有时候是一罐还没开封的食用油,有时候只是几个刚买的橘子,人们也会站在院门口朝外头骂几句,骂给整个村子听。 三叔家隔壁有个阿婆,说有一回半夜起来小解,从窗缝里看见黑鬼蹲在自家鸡笼前,把手伸进笼子里掏。阿婆不敢喊,退回床上,用被子蒙住头,到天亮才敢跟人说。说的人多了,黑鬼便不只是个傻子,更像一只熟悉每家灶间布局的野物。 以至于后面,哪家老奶奶床头柜里放了十几年的金耳环丢了,子孙辈们也纷纷对黑鬼的盗窃恶行表示强烈谴责,在院门口商量着哪天在路上堵他,一定要打一顿,并让他把金耳环吐出来。有人甚至提议去他家里搜,被他哥拦在门外,两兄弟站在门槛里外对峙,一个眼神正常,一个眼神发直,最后搜也没搜成,耳环的下落便永远成了桩无头公案。 还有更玄的。 说黑鬼能在铁链锁着的情况下"脱身",说他在台风夜从狗屋里消失,第二天出现在几里外的另一个村;说他在海边的礁石上蹲着,捞被浪打上岸的死鱼,蹲着蹲着便往嘴里塞,也不洗,也不煮。说的人绘声绘色,听的人半信半疑,但第二天再听到类似的事,便又"看吧,我就说他邪门"。 这些传闻堆在一起,便把黑鬼塑成了一个影子:白天在土路上游荡,夜里在灶间、晒场、坟地之间出没,饿了什么都吃,打了还不长记性,越打越能从链子底下溜走。 我第一次见到一个人可以饿到脸发青绿,是在这些传闻已经听了许多年之后。 不是电视剧里那种画着黑眼圈阴影妆的饿汉的脸,是像苔藓一样的青绿色,薄而均匀地覆在颧骨和腮帮上,像从田里那些墓穴中长满青苔的棺材里爬出来的。没有一丝活气,却又目光渴求着什么,迷茫地走在村里的土路上。 以前听大人们讲黑鬼,总以为他会是黑着的脸,没想到是青绿色的脸。 有一年中秋节过后,我在三叔家的老房子里一个人玩。收海带的季节,三叔家的人都去海边忙了,老宅里静得很,只有灶间偶尔传来铁皮柜门被风带动的轻响。 玩了一阵子,我看到厨房柜子上的一塑料袋月饼,没忍住嘴馋,打开塑料袋尝了一个。水果味的馅料很好吃,没两口便吃完了一个,又伸手拿了一个。过了会儿,发现吃了小半袋子,袋子重量都轻了不少。 做坏事的心虚和紧张感涌了上来,一时间不知道该如何是好。我从院子里捡了几块小石头,放在袋子底部,用包装纸盖着,这样提起来,月饼袋子还是沉甸甸的。 当天晚上都在担惊受怕中度过,好在第二天也没人发现有什么不对。 第三天晚上,听到三婶在自家院子里和邻居讨论着什么,我把耳朵靠近窗户,细细听着。 原来在说,这黑鬼太可恶了,不但偷吃了家里的月饼,居然还往袋子里塞石头。邻居们也是一阵诧异,说这黑鬼也是奇怪,吃就吃了,还回来塞石头干嘛。说着说着,便又聊起另外人家家里丢了东西,那黑鬼都偷了些什么奇怪的东西。 我从窗户边走开,担心的心算是放了下来,一阵更强烈的羞愧和心虚又涌了上来。 有一年春天,我在田野小路上遇到了黑鬼。 那是一条通向菜地的小土路,两旁的田埂上刚冒出新绿,风一过,嫩叶便轻轻晃。黑鬼站在十字路口张望着,不知道他要往哪里走。我路过他身旁时才认出来他是黑鬼——青色破碎的上衣和裤子,穿着拖鞋,鞋帮磨得发白,脚趾从裂口里露出来。 我认真地看了一眼他的脸。 这辈子第一次看到有人的脸是青绿色,像长了一层薄薄的苔藓,很消瘦,颧骨的形状露出来,带着几道旧疤。像雨季从满是野草的田埂旁墓穴里爬出来的、长着青苔的尸体。 我倒不是很害怕。他的眼神并没有多凶恶,其实也没有很呆滞。我见过目光呆滞的人的眼神,但是他的不像。他的眼神里更多的是迷茫,与不知所措。 "吃饭了吗?" 这句话是黑鬼问我的。他有点尴尬、又有点牵强地咧嘴一笑,露出不太整齐的牙。我有点惊讶他怎么这么像一个正常人一样学着搭话,神情还是有点傻愣的样子,像在努力学着以一个正常大人的口吻和人交流。 我虽然并不是很害怕他,但也不是很想和他扯上什么关系,简短回了一句,错开距离,很快地走远了。过了会儿再回头,他也离开岔路口,往另外一个方向走去,依然像是没有要去的地方。 之后我便很少再见到黑鬼了。 再之后,就是几年之后的春节。听村里人议论说,黑鬼被家里人饿死在棚屋里。 每次黑鬼做了什么坏事,其他村子里的人把黑鬼打了一顿之后,扭送到他家里,黑鬼的家里人便把黑鬼锁在院子的狗屋里,每天送顿饭,有时也可能忘了送,便饿着。黑鬼饿得受不了的时候,便会挣脱链子逃了出来,人们就会开始新的议论——黑鬼又逃了。 只是那次黑鬼可能确实太饿了,没能成功逃脱,在春节的某一个夜晚,饿死在家门口的狗屋里。 二月的夜还长,小屋子里的铁链拖在地上,发出短促的金属声。之后人们便也渐渐不再议论黑鬼,黑鬼也淡出了人们的记忆里。 在早些年的另一个春节——那还是黑鬼活着的时候——正月元宵的灯会晚上,整个小岛像被从海里捞起来的一盏大灯笼,亮得有些不真实。 游神的队伍从村口的土地庙缓缓出来。前头有人敲锣,有人打鼓,节奏一紧一松,把夜风也敲得一起一伏。几个壮年男人抬着神轿,轿子上的红绸被风吹得猎猎响,神像在轿里晃,金漆的面在灯影里一闪一闪。后面跟着的孩子提着各式灯笼,有兔,有鱼,有简易的六角形,蜡烛在纸罩里跳,把幼小的手指映成半透明的红。 街道两侧的鞭炮声此起彼伏,炸完一串,地上便多铺一层红纸碎屑,踩上去沙沙响,像走在落满花瓣的泥路上。空气里弥漫着浓浓的硝烟味,混着炒花生的香气、糖画的甜腻,还有人家灶间飘出来的鱼丸汤热气。楼房的窗户都开着,灯光从里头涌出来,和街上的灯笼光叠在一起,把每个行人的影子都拖得很长,投在墙根,投在台阶,投在还没收干的菜摊板上。 我夹在大人腿与腿之间,跟着队伍走。人很多,肩膀挨着肩膀,热气烘在脸上。我时不时抬头看天——烟花还在后头陆续升起来,有的高,有的低,有的炸成一大团白,有的碎成金雨,噼啪声从头顶滚过去,又滚向海面那一边。 走到一段缓坡,队伍慢下来,我趁机回头。 人群像一条流动的河,灯笼是河面上的光。而在那条河的最末尾,隔着一段空当,黑鬼也在走。 他没有灯笼。两只手垂在身侧,空空的,指节粗黑,指甲缝里带着洗不掉的泥。身上还是那件常穿的旧衣,洗得发薄,肩线处磨毛了,随步子轻轻晃。他不挤人,也不往前凑,只保持着一个不远不近的距离,像怕惊扰了谁,又像只是被鼓声牵着,不得不跟着这一队热闹走。 有人侧头瞥见他,便往旁边让半步,让出的那半步很快又被别人填上,像水面上合拢的涟漪。没有人同他说话,也没有人赶他。他就那样走在最后,走在所有灯光都还能照到的边缘。 又一朵烟花升起来。 这一朵特别大,在夜空中停了一瞬,才猛地绽开。光不是从天上落下来,而是像忽然在每个人脸上点了一盏灯——神轿旁抬轿汉子的汗,孩子灯笼纸上的折痕,妇女头巾的布纹,还有黑鬼仰起的脸。 我也看见了那张脸。 在那一瞬间,他不像传闻里那个会掏鸡笼、会翻灶间的影子。硝烟的光掠过他的颧骨,脸上被映成一种奇怪的灰,像潮退后礁石上暂时露出的苔。他看着天空,嘴唇微微张开,露出一点孩子气的呆,又好像只是被突如其来的亮惊住了。眼神里那团常年散不开的雾,似乎被冲淡了半分,可下一刻,当光收回去,雾还在。 鼓声又响了。队伍折向另一条街,神轿转过弯,灯笼的光便跟着拐过去,像被一只看不见的手牵走。人群涌过去,喧哗也涌过去,留下一截逐渐安静下来的路。 我再回头时,黑鬼已经不在原来的位置了。 他往街道尽头走。那头的灯少,楼也旧,墙皮斑驳,门口堆着未烧完的纸灰。风一过,纸灰味干干的,混着墙根渗出的潮气,一丝一丝往这边飘。游神的热闹到不了那里,鞭炮声传过去,只剩闷闷的回响,像隔了一层湿布。巷口的海风灌进来,带着一点凉的咸,把远处未散的硝烟气推淡,又推回来。 天上还飘着未散的烟,灰白色的,慢慢被风扯薄,扯成一条长带,挂在村子上空。地上的红纸屑还在,被风卷起又落下,发出极轻的沙声。前头的鼓点已经远得听不真切,像是另一个世界的事。 我站在人群里,手里还攥着大人给的半块糖,糖纸在掌心里窸窣响了一声。没有人注意到黑鬼消失。也许有人注意到了,也不会说什么——元宵夜本该是喜庆的,影子不该占用太多的话。 游神的灯转过两个弯,远处又升起一朵烟花,把整排屋檐下的人影照得齐整。而街道尽头那截暗处,什么也没有。烟还在天上飘,红纸还在地上,空气里最后剩下的,是糖化的甜、炮屑的涩,和一点怎么吹也吹不散的海腥。 很多年以后,二月里狗屋前的铁链声,会盖过这一夜的鼓点。但在那个元宵,黑鬼最后走进暗处的样子,仍像一枚被反着打的印章,留在记忆最浅的那一层——不是凶,不是恶,只是一个人,在整岛灯火通明的时候,空手跟着队伍走了一程,又独自回到光找不到的地方去。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/heigui-cover.webp" />
</item>
<item>
<title>QQ 经典农场辅助:挂机收菜、种菜、偷菜的一键工具</title>
<link>https://gitpull.cn/post/20260617/</link>
<guid isPermaLink="false">qq-farm-assistant-经典农场辅助</guid>
<pubDate>Wed, 17 Jun 2026 04:00:00 GMT</pubDate>
<author>Jimmy</author>
<category>杂七杂八</category>
<description><![CDATA[一个跑在 Windows 上的 QQ 经典农场(微信小游戏)桌面辅助:用图像识别锁定微信窗口,自动收菜、补种、逛好友农场偷菜,支持虚拟机前台挂机。双击 init.bat 初始化,双击 start.bat 开跑,日志写在 logs/daemon.log。]]></description>
<content:encoded><![CDATA[项目地址 :github.com/flymysql/qq farm assistant 一句话: Windows 上的 QQ 经典农场挂机脚本 ——识别游戏画面里的按钮,自动收菜、种菜、偷菜,适合把微信窗口固定在虚拟机里前台跑着。 QQ 经典农场最近又火了一轮。每天定时收菜、补种、逛好友列表偷一圈,手点久了挺费事。我写了这个小工具,把重复操作交给脚本,人只要保证游戏窗口在前台、大小固定就行。 它能做什么 守护进程会按固定间隔循环执行三件事: 1. 收菜 :在自己农场识别「一键收获」,成熟作物点掉。 2. 种菜 :收完后扫描空地,自动点种子补种(可在 里开关)。 3. 偷菜 :打开好友列表,拜访可偷的农场,点「一键摘取」;顺路还能点务农、额外奖励区块等。 另外还带了 异地登录弹窗 的自动处理:识别到「重新登录」后等待一段时间再点,减少挂机半夜掉线没人管的情况。 点击节奏做了随机延迟,识别前会等界面稳定,尽量不像机械连点。 环境要求 Windows 10 / 11 Python 3.10+ (从 python.org 安装) 微信里打开 QQ 经典农场 ,窗口大小 固定 ,并保持 前台可见 推荐:虚拟机或副屏专门挂一个微信窗口,别最小化 图像识别基于 OpenCV 模板匹配,按钮截图放在 目录。换电脑或改了窗口大小后需要重新初始化。 怎么用(三步) 1. 下载 2. 初始化(新机器 / 换窗口大小必做) 1. 微信里打开 QQ 经典农场 2. 把游戏窗口调到日常使用的 固定大小 3. 站在 自己农场 (底部导航栏可见) 4. 双击 首次会自动创建虚拟环境、安装依赖,并检查模板是否齐全、记录窗口尺寸、测一遍截图和导航按钮识别。 换电脑或改了窗口大小后,重新跑一次 即可。日常挂机 不用 每次跑初始化。 3. 启动 双击 ,程序进入守护循环,默认每 5 秒一轮(可在 的 修改)。 日志: 统计: 停止: ,或把鼠标移到屏幕左上角(pyautogui 紧急停止) 配置里常改的几项 改完保存, 重启 生效。 | 配置项 | 说明 | | | | | | 每轮收菜 + 偷菜的间隔(秒) | | | 是否收菜后自动补种 | | | 每轮最多拜访几个好友 | | / | 点击后随机等待,模拟人工 | | | / / ;虚拟机或 RDP 截图异常时可设 | | | 模板匹配阈值,误点多可提到 0.88 左右 | 更细的偷菜轮询、务农模板、种子栏区域等都在同一个文件里,带中文注释。 排错用脚本 | 文件 | 用途 | | | | | | 新机器 / 换窗口必跑 | | | 启动守护进程 | | | 只重新保存窗口尺寸 | | | 截图是否正常 | | | 底部导航按钮识别分数 | 模板怎么采、怎么替换,见仓库里的 。 常见问题 | 现象 | 处理 | | | | | 提示找不到 Python | 安装 Python 3.10+ 后重跑 | | 虚拟机 / 远程桌面截图黑屏 | 设 ,窗口保持前台 | | 日志出现 一类警告 | 固定窗口大小后重跑 | | 按钮老点偏 | 跑 或 看匹配分数 | 技术实现(简略) 不写长文,只列个轮廓,有兴趣的直接看源码: 窗口 :按标题 / 进程名锁定微信窗口,截图前自动聚焦 识别 :OpenCV 多尺度模板匹配 + 按窗口尺寸自动缩放模板 任务 : / / / 拆成独立模块,由 轮询调度 输入 :pyautogui 点击,带 FAILSAFE(鼠标左上角急停) 免责声明 本项目仅供 个人学习与研究 。使用风险自负,请遵守游戏及相关服务条款,勿用于任何可能违反规定的场景。 如果你也在怀旧农场、又不想被闹钟绑架,欢迎 Star 或提 Issue。有识别不准的按钮模板,也可以按 自己补一张图 PR 回来。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/qq-farm-assistant-经典农场辅助.webp" />
</item>
<item>
<title>海门回声 | 静谧的生日夜晚</title>
<link>https://gitpull.cn/post/20260616/</link>
<guid isPermaLink="false">海门回声-一个寂静的生日夜晚</guid>
<pubDate>Tue, 16 Jun 2026 12:00:00 GMT</pubDate>
<author>邻家酒肆</author>
<category>海门回声</category><category>随想</category>
<description><![CDATA[二〇〇三年的夏天,海岛入夜后,田埂旁的杂草里蟋蟀叫成一片,晚风从海面方向吹进村子,带着咸腥和青草的气味。村子里的路坑坑洼洼,月光照在上面,一块亮一块暗。父母在外地打工,十二岁的姐姐和六岁的我留在岛上。那天傍晚,姐姐拎回村口小卖部的一红塑料袋饼干,喊伟华哥过来,客厅里没有开电灯……]]></description>
<content:encoded><![CDATA[二〇〇三年的夏天,海岛入夜后,田埂旁的杂草里蟋蟀叫成一片,晚风从海面方向吹进村子,带着咸腥和青草的气味。村子里的路坑坑洼洼,铺着碎石子和黄泥,白日里被晒得发硬,入了夜又被露水洇得有些潮软,脚踩上去,一块亮一块暗,月光铺在坑洼里,像一洼一洼静止的水。 那时候我时常一个人走夜路。提一盏煤油灯,灯罩里的火苗被步子带得一颠一颠,倒影在人字拖前晃出不大的一片。村路两旁立着不少废弃的土房子,墙皮剥落,露出里头暗红的土坯,屋顶塌了半边,梁木横在豁口上,上头缠满了杂草和藤蔓,牵牛花的藤从墙缝里爬出来,垂到路边,风一过,叶子便窸窸窣窣地响。路过那些屋子时,里头偶尔会传出细细簌簌的动静,像是有什么活物在碎砖和枯草间挪动。老人总爱讲些精怪蛇蟒的故事,我想起来便心里发紧,把灯往身前凑了凑,低下头加快步子,石子路在脚底咔咔响,一直跑到路尽头有人烟的地方,才慢慢把气喘匀。 有天傍晚,姐姐从村口小卖部回来,手里拎着一只红塑料袋,袋口打了个结,油渍在塑料面上洇出深色的印子。她出门时天还亮着,回来时暮色已经压下来了,裤脚沾了点路边的泥。她把袋子搁在灶台上,掏出里面的饼干——圆扁扁的,印着模糊的花纹,是寺庙祭祀常用的那种,有点但是又有点脆,掰开会掉渣,凑近了闻有一股油脂香。 姐姐擦了把手,拿火柴去点客厅桌上的煤油灯。灯芯捻了捻,罩子盖上,火苗在玻璃里缩成一点橙黄,把整个客厅照出一圈温吞的光。瓦数不够的电灯挂在屋梁上,没开,昏暗中家具轮廓都钝了角。她让我去喊来前面大伯家的伟华哥,我倚在大伯家院子门口,细细地喊了声,仿佛怕有其他人听见,声音穿过土墙,落在暮色里,隔了一会儿,才听见那边应了一句。 伟华哥推门进来,门轴吱呀响了一声,身上还带着外头的夜气,衣角被风吹得凉。我们搬出一张大凳子摆在屋子中央,桌面被磨得发亮,上头有几道浅浅的刀痕。三个人各拖一张小凳子围坐下,凳脚在砖地上刮出短促的声响。姐姐从抽屉里翻出半截蜡烛,在煤油灯上借了火,烛芯嗤地一声亮起来,三个人的影子便投在墙上,随着火苗轻轻晃。 饼干倒在桌面上,姐姐用手拨了拨,分成三小堆。伟华哥伸手拿了一块,搁在掌心里看了看,咬下一角,慢慢嚼。姐姐把其中稍大的一块推到我这边,我接过来,低头咬了一口,渣子落在裤腿上。油脂香在舌尖化开,甜腻里带着一点焦味,我小口小口地啃,怕掉太多渣,其实这种饼干平常节日祭祖的时候也很常见,只是当下孩子间莫名的仪式感增加了几分不同的味道。 屋外虫鸣一阵紧过一阵。门缝漏进的风把烛火吹得往一边斜,又慢慢竖回来。从窗口望出去,乡间小路隐在暗处,路旁杂草堆里偶尔有萤火浮起来,明一下,又暗下去。白天里三叔拖回田埂旁的那条铁壳船,倒扣在远处,黑沉沉的一团,刷过漆的船底在月光下泛着暗红。隔壁阿华叔家的方向倒是亮着灯,隐约能听见电视机里传出的声响,夹杂着几声笑——他家两个儿子过生日时,总是另一番光景,那是另外的事了。 姐姐把红塑料袋叠了两折,塞进灶台下的柴堆里。烛泪沿着烛身淌下来,凝成一小坨白。伟华哥吃完手里那块,在裤腿上擦了擦手指,起身掀门帘出去了。门帘落下,屋里又只剩我和姐姐,以及那盏煤油灯和将尽未尽的蜡烛。姐姐把桌上散落的饼干渣拢到一边,拿抹布擦了擦桌面,动作很轻。 财华哥家那次,是另一个夏天的晚上,记忆里的轮廓不如上面那桩清楚,或许是生日,或许不是,我只记得那天下午我们几个还在铁壳船底下钻过——船倒扣着,里头阴凉,铁皮被日头晒了一天,摸上去仍有余温,船底有一股铁锈混着海盐的气味。三叔蹲在船头刷漆,刷子划过船壳,干涩地响,漆味飘在田埂上。我从船底爬出来,裤脚沾了泥,一路小跑着往财华哥家去,经过那几栋废弃土房时,灌木丛里又响了几声细细簌簌的动静,也没停也没说话,只是跑得更急了。 财华哥家的堂屋也没开灯。几个孩子围坐在八仙桌旁,桌上点着三四根蜡烛,火苗细,风从门里进出,人影便在墙上拉长又缩短。财华哥正和几个年纪相仿的孩子蹲在灶台边忙活——面粉倒在搪瓷盆里,鸡蛋壳磕在盆沿上,黄白流进去,有人拿筷子搅,搅得盆沿沾了一圈面糊,有人去揭高压锅的盖子,又被旁人按了回去。 我是最小的,搬着板凳坐在一旁,看他们在灶台前折腾。锅盖扣上,阀门转紧,灶膛里的柴火噼啪响,火光从灶口映出来,照得几个大孩子的脸忽明忽暗。等的时候,堂屋静得很,只听见锅里偶尔传出闷沉的咕嘟声,和门外田埂上蛙鸣交错着。 不知过了多久,财华哥伸手去拧阀门,嘶的一声,锅盖掀开,一股热气扑到脸上,带着面粉和鸡蛋熟了的香气。锅里的东西却不是蛋糕——一整块硬邦邦的熟面饼,焦黄,厚实,贴在锅底,拿锅铲撬了撬才翻起来,落在案板上,咚的一声闷响,很像本地的一种小吃叫做“金饼”,我也不知道土话翻译成普通话后还准不准确了。 面饼搁在桌上,用刀划了几道,一人掰一块。我手里这块烫,搁在膝头上晾了晾,咬下去很干,咽的时候得用力,嘴里全是面粉的粉感。蜡烛的光照在每个人脸上,有人嚼得慢,有人已经伸手去拿下一块,碎屑掉在桌面上,也没人扫。堂屋外的夜路和来时一样,月光铺在田埂上,其实不用灯也看得见路,只是财华哥家桌角那盏煤油灯还亮着,灯芯在罩子里微微跳。 我吃完手里那块,把沾了面粉的手指在裤腿上蹭了蹭,跟着其他人掀帘出去。风从海面吹过来,虫鸣又起,路旁杂草里萤火明灭。远处铁壳船黑沉沉地扣在田埂旁,三叔早就收工了,刷漆的刷子搁在船头,风一过,船底发出一声空空的响。 印象中几个兄弟姐妹里过生日的,最热闹的是阿华叔家的两个孩子。 阿华叔家客厅大,电灯瓦数足,亮堂堂的,把墙上贴的年画和玻璃柜里的摆设都照得清清楚楚,还有一些美女的挂历,那时候看不懂,只觉得新鲜。阿刘哥生日那天,八仙桌上摆着一只真正的蛋糕,奶油白花花的,上头插着蜡烛,点起来,整个屋子都是甜香,那是我小时候唯一能吃上蛋糕的时候,很香,很甜,挖一勺送进嘴里,软绵绵的,和祭祀饼干、和高压锅里那块硬面饼,完全是两样东西。 阿刘哥他那帮同学也会来,拎着花花绿绿的礼物袋,搁在桌边,包装盒上的缎带打了结,堆成一小摞。大人们在灶间进进出出,碗筷碰在一起叮当响,肉菜很多,我很喜欢吃炒鱿鱼,长大了之后也一直喜欢吃青椒炒鱿鱼。笑声一阵一阵涌进客厅,我们几个小的挤在桌旁,等大人切蛋糕,蜡烛的火苗在亮堂的灯光里反而不那么显眼了,只是甜香一直飘着,从打开盒子那一刻起,就飘满了整个屋子。 很喜欢阿刘哥他们过生日,饭菜和灯光都暖洋洋的,我站在自家院门口,能望见他家客厅的灯光从门帘缝里溢出来,把门前一小段路都照成暖黄色,笑声和电视声一阵一阵传过来,又被夜风吹散。 姐姐生日那晚,伟华哥走后,她把剩下的饼干收进一只铁罐里,盖上盖子,搁上橱柜顶层。烛火燃尽了,烛芯歪倒在烛泪里,她凑过去吹了吹,屋里暗了一截,只剩煤油灯那一圈光。她拎起煤油灯去里屋铺床,灯影在墙上移过去,又移回来。我躺在席上,听着窗外的虫鸣,远处阿华叔家的方向早就安静了,只剩路边杂草里的萤火,和铁壳船在月光下那道暗红的弧。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/海门回声-一个寂静的生日夜晚.webp" />
</item>
<item>
<title>用 AI 做好软件项目:上下文工程实践指南</title>
<link>https://gitpull.cn/post/20260615/</link>
<guid isPermaLink="false">用-ai-做好软件项目上下文工程实践指南</guid>
<pubDate>Mon, 15 Jun 2026 04:56:47 GMT</pubDate>
<author>兰州小红鸡</author>
<category>AI 基础设施</category>
<description><![CDATA[在复杂软件项目中引入 AI,常见困境不是模型不够强,而是 每次会话都在重复建立上下文 ——Agent 全量读取大文件、反复追问架构、上一轮的结论无法延续、任务边…]]></description>
<content:encoded><![CDATA[在复杂软件项目中引入 AI,常见困境不是模型不够强,而是 每次会话都在重复建立上下文 ——Agent 全量读取大文件、反复追问架构、上一轮的结论无法延续、任务边界模糊导致改动扩散。这些问题叠加,表现为 token 消耗高、理解慢、产出不稳定。 实践表明: AI 可以承担 substantial 的工程工作,但前提是项目具备可复用的协作基础设施,且任务有清晰边界。 其中投入产出最高的一环,是 上下文工程(Context Engineering) ——不是写更好的 Prompt,而是为 AI 构建分层、可检索、可延续的项目记忆,并用工具与规则约束 Agent 的读码行为。 本文总结一套可迁移的方法:如何做上下文工程、如何节省 token 并让 AI 快速理解项目、如何在本地初始化 AI 协作环境,以及如何组织日常任务与故障排查。 典型问题与根因 在多层架构、多语言、长生命周期的项目中,早期使用 AI 往往遇到: Agent 倾向于全量读取数千行源文件,单次会话 token 消耗高且定位不准 每个新会话需重新解释架构边界、模块职责与不可触碰的约束 缺少变更记录,上一会话的排查结论与决策无法自然延续 任务 scope 不明确时,改动容易扩散至不相关模块 根因并非模型能力不足,而是 缺少面向 AI 的持久化项目记忆与检索机制 。上下文工程针对的正是这一缺口。 上下文工程:核心方法论 上下文工程的目标,是让 AI 在有限 token 预算内,尽快获得 正确、足够、可执行 的项目认知,而不是把整仓代码塞进上下文窗口。 推荐将项目上下文分为四层,由粗到细、由静到动: L1:规则与黑名单——控制 Agent 的默认行为 规则层解决「Agent 不该做什么」。写入 IDE 规则目录且设为 的条目,会在每次会话自动生效,无需人工重复。 建议纳入的关键约束: 探索代码时 优先 语义检索与符号导航,禁止无目的地 bulk Read / Grep 全仓库 单次 Read 设置行数上限(如 200 行),编辑目标文件除外 禁止读取 build 产物、依赖目录、本地索引缓存等路径 未明确要求时不执行 git commit 代码变更后必须跑指定测试,并追加变更日志 配合 (与 对齐并扩展),将产物、锁文件、索引目录挡在 Agent 读路径之外。这一层几乎不消耗对话 token,但显著减少误读大文件的概率。 L2:架构手册与变更日志——快速建立心智模型 规则层管行为,手册层管认知。建议每次会话的标准阅读顺序: 1. 架构维护手册 ——系统分层、模块职责、关键约束、已知缺口、问题路由表(「出现某类症状时,优先查哪个模块」) 2. 变更日志顶部若干条 ——近期变更动机、行为变化、测试情况与未竟事项 架构手册应 写给 Agent 查表用 ,而非面向人类的散文。好的路由表能把「问题现象」直接映射到「首选代码入口」,避免 Agent 从底层向上逐层摸索。 变更日志每条记录变更动机而不仅是文件列表,滚动保留最近 N 条(如 50 条)。其作用是 压缩历史会话 :新会话读若干条即可接续上周工作,无需重放完整排查过程。 有 L2 时,新会话建立准确心智模型通常只需数分钟;无 L2 时,往往需长时间反复解释且仍易遗漏约束。 L3:语义索引与符号导航——按需取码,不全量读 L3 是 token 节省的核心杠杆。原则: 理解代码用检索,编辑代码才 Read。 | 场景 | 推荐方式 | 应避免 | | | | | | 定位某业务逻辑的实现位置 | 语义检索 | Read 整个大文件 | | 查找某函数/类的全部引用 | 符号导航 | Grep 全仓库再人工过滤 | | 了解某函数的上下游依赖 | | 连锁打开多个关联文件 | | 检索片段不够,需看完整函数体 | | 直接 Read 整文件 | 语义检索返回带置信度的代码块(chunk),通常几十到一两百行,足以定位逻辑;仅当 chunk 不够或需要修改时,再对小范围做 Read。 符号导航适合强类型语言项目:查找定义、列出引用、获取文件符号概览。对单文件数千行的遗留模块,符号导航通常比纯文本搜索精度更高。 一次语义检索的 token 开销,往往是全量读取同主题文件的 1/10~1/50。索引建立后,效果随项目规模增大而更明显。 L4:跨会话记忆——避免重复决策 L2 记录「项目发生了什么」,L4 记录「我们为什么这样选」。 项目 memories (本地持久化):onboarding 时写入架构概览、技术栈、常用命令、编码惯例、任务完成检查项。适合稳定、不常变的知识。 / :适合架构选择、已否决方案、命名约定等决策性记忆。回答非平凡问题前先 recall,做出非显然决策后 record。 :标记某文件某区域是某功能的核心,便于后续快速定位。 L4 的价值在于:数周后遇到「当时为什么选这个方案」,不必重读代码推导,直接 recall 即可。 上下文工程的整体工作流 将四层串联,单次任务的推荐路径如下: Token 节省与高效产出 上下文工程落地后,token 节省来自多个环节的叠加,而非单一 Prompt 技巧。 省 token 的关键机制 1. 用检索代替全文件阅读 大型源文件(数千行)一次 Read 即可消耗数千~上万 token。语义检索返回相关片段,通常 100~300 行等效内容,且带相关性排序。 2. 用手册代替口头解释 架构、模块边界、禁忌约束写入维护手册后,每会话 Read 该文档即可,而非在对话中重复口述。对话 token 留给具体任务。 3. 用变更日志代替重放排查过程 「上周那个超时问题怎么修的」不需要再贴大量日志让 AI 重推。变更日志一条简短摘要即可接续。 4. 用规则代替重复叮嘱 「不要 commit」「不要读 build 目录」「先跑测试」写入 rules 后零对话成本生效。 5. 用 scope 约束扩散 任务限定目录范围与禁止修改项(如「只改 ,不修改 API 契约」),可避免 AI 打开不相关模块,减少连锁 Read。 6. 监控节省效果 若工具支持统计(如 ),可查看检索次数与 token 节省,评估索引是否被有效使用。 高效产出的任务表述 token 节省与产出效率也取决于任务输入质量: | 低效表述 | 问题 | 高效表述 | | | | | | 优化整个项目 | scope 模糊,改动扩散 | 仅修改 ,修复连接断开后在途请求未回调失败的问题 | | 看看有没有 bug | 无检索入口 | 用 context search 定位重试队列逻辑,检查失败时是否正确清理状态 | | 帮我理解这个项目 | 触发大量 Read | 先读架构手册第 3 章,再说明请求链路中各层职责 | | 修一下超时 | 缺现象与范围 | 单请求场景下接口阻塞 60s,优先查连接池与超时回调,参考变更日志最新条目 | 故障排查的有效输入模式: 完整日志 + 复现步骤 + 优先模块 + 已排除的假设 。开发者提供领域判断(如「已排除流量过载」),AI 负责沿代码链下钻。 会话内的 Read 预算意识 即使检索工具受限,也应遵守「渐进式取码」: 1. 先语义检索 / 符号导航定位 2. 需要更多细节时 expand chunk 或 Read 单个函数体 3. 确认编辑范围后再 Read 目标区间(建议控制单次行数) 4. 仅编辑时才需要较完整上下文,可用多次小范围 Read 代替一次大 Read 将上述纪律写入 rules 后,Agent 默认遵循,显著降低「一上来读完整个文件再开始想」的模式。 本地 AI 环境初始化 上下文工程的 L3(检索索引)与 L4(跨会话记忆)依赖本机工具链。本节面向 从未接触过目标仓库的开发者 ,给出一套与具体项目无关、可逐步复现的初始化流程。 读完本节,你应能在任意代码仓库中完成:安装依赖 → 配置 MCP → 建立语义索引 → 激活符号导航 → 验收 Agent 是否按预期工作。 初始化完成后具备的能力 | 能力 | 依赖组件 | Agent 侧典型调用 | | | | | | 按语义查找代码 | CCE(Code Context Engine) | 、 、 | | 按符号追踪引用 | Serena MCP | 、 | | 跨会话记住决策 | CCE + Serena memories | 、 | | 约束 Agent 读码行为 | 、 | 自动注入,无需每次口述 | CCE 擅长「用自然语言问代码在哪」;Serena 擅长「这个函数被谁调用」。复杂项目建议 两者都配 。 整体流程概览 预计耗时:首次约 15~30 分钟(含索引构建,视仓库大小而定)。索引建立后,日常只需重启 IDE 即可使用。 第 0 步:前置条件 在开始之前确认: ] 已安装 [Cursor(或其他支持 MCP 的 IDE) [ ] 本机有 Python 3.11 或更高版本 [ ] 能访问外网(下载 Python 包与 Serena) [ ] 已在 Cursor 中打开目标项目的 根目录 (含 或顶层 / 等的那一层) 检查 Python 版本: 若版本低于 3.11,请先升级 Python,否则后续 CCE 安装可能失败。 第 1 步:安装 uv(Serena 的运行时) Serena 通过 启动,需要先安装 uv 包管理器。 Linux / macOS: Windows(PowerShell): 安装后确认 在 PATH 中: Windows 下 通常安装在 ,需确保该目录在系统 PATH 中。 第 2 步:安装 CCE(语义检索引擎) CCE 提供代码语义索引与 能力。在用户目录下安装即可, 无需 写入项目仓库。 老系统 SQLite 兼容(可选): 若后续 报 ,说明系统 SQLite 版本过旧(< 3.35),追加安装: 安装后确认 命令可用: 若提示 ,说明 pip 的 user bin 目录不在 PATH。查找路径: 第 3 步:配置 Serena MCP(全局,每人一次) Serena 配置写在 用户全局 MCP 文件中,不随项目仓库分发。这样换项目时无需重复配置。 配置文件路径: | 系统 | 路径 | | | | | Linux / macOS | | | Windows | | 若文件不存在,新建一个空 JSON: 。 在 中加入 Serena 条目(若已有其他 MCP,合并即可,勿覆盖): 保存后 先不要验证 ——还需完成第 4 步并重启 Cursor,MCP 才会加载。 第 4 步:在项目中初始化 CCE 索引 进入 你正在开发的仓库根目录 ,执行: 该命令会: 1. 扫描项目源文件,构建语义索引(存入本地 目录) 2. 在项目下生成 ,注册 MCP 服务 生成的 内容类似: 注意: 是本机绝对路径,因此该文件 不应提交到 git ,每位开发者本地各自生成。 大仓库首次索引可能需数分钟 ,请等待命令正常退出。 跳过重索引(可选): 若你之前已索引过、仅重装 MCP,可手动按上面模板编写 ,将 和 换成你的实际路径,无需重跑 。 索引更新: 大量拉取代码后,在项目根目录执行: 第 5 步:重启 Cursor 并确认 MCP 状态 1. 完全退出 Cursor,或执行 Command Palette → Developer: Reload Window 2. 打开 Settings → MCP (或 Cursor 设置中的 MCP 面板) 3. 确认以下两项均为 Running (绿点): 若显示 Failed 或 Stopped,见本文末尾「故障排查」。 可在终端验证 CCE 统计命令: 首次运行显示 属正常——Agent 调用 后才会累积。 第 6 步:Serena 项目激活与 onboarding MCP 就绪后,在 Cursor Agent 对话框发送: 激活(activate project) 让 Serena 将当前打开的目录注册为工作项目。 onboarding 是一次性任务:Agent 阅读项目结构,在本地 写入若干条持久记忆。建议涵盖: | 记忆主题 | 内容示例 | | | | | 架构概览 | 分层结构、核心模块职责 | | 技术栈 | 语言、构建工具、主要依赖 | | 常用命令 | 构建、测试、lint 命令 | | 编码惯例 | 命名、目录约定、禁止事项 | | 任务完成检查项 | 改完代码必须跑哪些测试 | 首次 onboarding 会消耗较多 token; 之后各会话直接复用 ,无需重复。 若项目已有团队维护的架构手册,可指示 Agent:「onboarding 时优先阅读 (或你项目中的等效文档)」。 第 7 步:验收 按顺序确认以下各项: 终端验收: MCP 验收: [ ] Settings → MCP: 、 均为 Running [ ] Agent 对话中可成功调用 (不报错、有返回) [ ] Agent 对话中可成功调用 (不报错、有返回) [ ] Serena onboarding 已完成( 目录存在且非空) 行为验收——发送试任务: 预期行为:Agent 先调用 MCP 工具,仅对小范围代码做 Read,而非一次性打开数千行源文件。 可选:配置 Agent 规则与读文件黑名单 上述步骤完成后,语义检索与符号导航已可用。若希望进一步约束 Agent 行为、节省 token,可在项目仓库中新增(并提交 git): ——阻止 Agent 读取 build 产物、依赖目录: ——示例规则(按需修改): 团队可将架构手册、变更日志模板一并纳入仓库,参见前文「上下文工程」L1/L2 层。 示例:仓库中的 AI 协作目录布局 以下是一个 成熟配置 的示例目录树,来源于真实复杂系统项目的实践,已隐去业务代码与产品名称。你可对照检查自己的仓库是否具备对应文件;缺失项可按需逐步补齐,不必一次到位。 各层与目录的对应关系: 新成员上手路径(结合上图): 1. 拿到左侧「进 git」的全部文件 2. 执行 (或按本文第 1~4 步手动初始化) 3. 本地自动生成 、 4. 重启 Cursor → onboarding → 写入 5. 日常会话:Agent 自动读 + ,用 检索,用 回忆 业务源码目录( 、 等)与 AI 协作目录 并列存在、互不替代 :前者是要维护的产品,后者是让 Agent 高效理解前者的基础设施。 团队可选:封装一键初始化脚本 成熟团队可将第 1~4 步封装为 ,新成员只需: 脚本典型职责:检测 Python → 安装 uv/CCE → 合并 Serena 到全局 mcp.json → 执行 。但无论是否有脚本, 第 5~7 步(重启 IDE、确认 MCP、onboarding、验收)仍需人工完成 。 什么该进 git,什么留在本地 | 类型 | 进 git | 留本地 | | | | | | 、 | ✅ | | | 架构维护手册、变更日志、初始化脚本文档 | ✅ | | | 模板(路径用占位符) | ✅ | | | 含本机绝对路径的 | | ✅ | | 语义索引 | | ✅ | | 记忆与缓存 | | ✅ | 原则: 共享规则与文档,各自生成索引与 MCP 配置。 故障排查 | 现象 | 可能原因 | 处理 | | | | | | | pip bin 不在 PATH | 并写入 shell 配置 | | | 系统 SQLite < 3.35 | 后重跑 | | MCP 面板无 / | 未重启 IDE 或 mcp.json 路径错误 | 检查 与项目 ,Reload Window | | MCP 显示 Failed | 不在 PATH,或网络问题 | 终端手动执行 查看报错 | | 无结果或结果过时 | 索引未建立或代码已大改 | 在项目根目录执行 | | Agent 仍全文件 Read | MCP 未 Running,或 rules 未配置 | 确认 MCP 绿点;任务中显式写「先用 context search,不要 Read 整个文件」 | | Serena onboarding 失败 | 项目路径未激活 | 先让 Agent 执行 ,再执行 onboarding | 初始化后的日常使用 环境就绪后,每次开新会话无需重复上述流程。仅需: 1. 用 Cursor 打开项目根目录 2. 确认 MCP 仍为 Running(偶尔更新 Cursor 后需 Reload) 3. 大规模拉代码后执行一次 日常协作节奏见下文「日常任务组织」。 日常任务组织 基础设施就绪后,推荐固定协作节奏: 优先级分层 全面审查类任务,可按维度生成报告并分级推进: P0 :可导致挂死、数据错误或静默失败 P1 :影响关停、资源释放、连接与状态管理 P2 :性能优化、目录整理、文档维护 AI 适合广度扫描与问题枚举; 优先级决策由开发者基于业务上下文做出 。执行时逐项推进,避免一次改动过多文件。 可并行派发专项 review(并发安全、资源泄漏、错误处理、冗余逻辑),主 Agent 汇总后按 P0→P1→P2 排序执行。 人机分工 | 开发者负责 | AI 负责 | | | | | 业务判断、日志解读、优先级决策 | 代码定位、修改实现、测试执行 | | 排除已验证的假设 | 沿调用链下钻 | | 真机/集成环境验证 | 变更日志与文档起草 | | 确认后 commit | 不擅自提交 | 故障排查协作模式 复杂故障的有效协作遵循固定模式,与具体技术栈无关。 输入 :完整日志 + 复现步骤 + 优先检查的模块 + 已排除的假设。 过程 : 1. 开发者给出领域判断,缩小排查方向(如排除过载、排除配置错误) 2. AI 沿调用链从现象反推到根因 3. 修复后写入变更日志,记录动机与验证方式 典型分工示例 : 开发者观察到「仅特定配置下复现,其他配置正常」→ 指向条件分支或特性开关差异 AI 定位到「错误路径未返回失败状态,调用方无限等待」→ 补充错误回调与超时兜底 结论记入变更日志 → 下次同类问题直接 recall,无需重走排查链 AI 适合承担的工程任务类型 除功能开发与故障修复外,AI 也适合处理以下低创新、高琐碎的工作,前提是 零行为变化 + 测试验证 : 大文件按职责拆分,保持对外 API 不变 补充端到端验证脚本与运维辅助工具 目录重组与兼容层(shim)维护 命令与配置文档汇总 注意:AI 生成的文档与命令示例, 须经人工在实际环境执行核对 ,不可默认正确。 核心原则 1. 先建上下文,再写代码。 四层记忆到位后,AI 才能稳定产出。 2. 检索优先,全量 Read 是最后手段。 token 节省与定位精度的基础。 3. 任务需有明确边界。 目录范围、禁止项、commit 策略写入 rules。 4. 变更记录动机。 便于跨会话接续与回归分析。 5. 验收标准前置。 核心逻辑变更跑项目约定的测试套件;无法执行时在变更日志注明原因与影响范围。 投入产出与最小起步 建设上下文工程基础设施,前期约需 1~2 天。收益体现在: 每会话重复解释架构的时间显著下降 代码阅读 token 可比无索引模式降低一个数量级 变更可追溯,跨会话协作成本降低 新成员 onboarding 收敛为「一条命令 + 重启 IDE」 不必一次性落地全部方案。建议优先级: 1. + rules ——控制 Agent 不读什么、不做什么 2. 架构维护手册 ——模块职责与约束(数页即可) 3. 变更日志 ——每次变更记动机 4. 窄 scope 任务表述 5. 语义检索 + 符号导航 + 一键初始化 ——有精力再上 持久化记忆与检索工具的优先级高于 Prompt 技巧。 前者决定 AI 能否看见正确的代码,后者只在看见之后微调表达方式。 结语 AI 协作的效果,主要取决于是否建立了可持续的 上下文工程体系 ,而非单轮 Prompt 的技巧。 将分层记忆(规则、手册、索引、持久化)、token 预算意识与明确任务边界结合,AI 才能在数分钟内理解项目并稳定产出。本地环境一条命令完成初始化后,语义检索与符号导航构成这套体系的执行基础。 建议路径: 先花约一天建设上下文工程基础设施并跑通本地初始化,再开展功能开发与故障修复。 基础设施到位后,AI 可作为稳定的工程协作者,而非每轮需重新培训的临时助手。 本文总结自复杂系统软件项目的 AI 协作实践,方法可迁移至任意规模与语言栈的项目。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/1781499659393-4hzbpp-image.webp" />
</item>
<item>
<title>PeerCache:一个去中心化的 RDMA 零拷贝 KV 缓存后端</title>
<link>https://gitpull.cn/post/20260601/</link>
<guid isPermaLink="false">peercache-decentralized-rdma-kv-cache</guid>
<pubDate>Mon, 01 Jun 2026 02:00:00 GMT</pubDate>
<author>兰州小红鸡</author>
<category>分布式存储</category><category>AI 基础设施</category><category>项目介绍</category>
<description><![CDATA[PeerCache 是我写的一个面向 SGLang HiCache 的 L3 KV 缓存后端:它做的是跨请求、跨节点的 KV 前缀复用,提供和 Mooncake Store 类似的能力,却砍掉了中心化的 master 与 metadata 服务。单卡能吃到裸 ib_read_bw 的 94%,整机 8 卡聚合 413 GB/s。这篇文章讲清楚它的定位、为什么这样设计、双 MR 模型怎么工作,以及实测性能基线。]]></description>
<content:encoded><![CDATA[项目地址 :flymysql.github.io/PeerCache 一句话介绍:一个 去中心化、点对点、RDMA 零拷贝的 SGLang L3(HiCache)存储后端 ,专门做 跨请求、跨节点的 KV(前缀)缓存复用 。它提供和 Mooncake Store 类似的复用能力,但 没有中心化的 master 和 metadata 服务 。单卡能吃到裸 的 94% ,整机 8 卡聚合 413 GB/s(3.3 Tbps) 。 在上一篇 《大模型推理的 PD 分离》 里,我把 prefill/decode 分离的来龙去脉和 Mooncake 的实现讲了一遍。这篇接着讲一个我自己动手写的东西: PeerCache ——一个想把"KV 缓存跨节点复用"这件事做得更轻、更去中心化的 L3 后端。 不过开头要先纠正一个容易误解的点: PeerCache 不是 PD 的搬运引擎 ,它和 PD 是两件正交的事。下面会专门讲清楚。 一、它解决什么问题:跨请求的 KV 前缀复用 先说场景。LLM 推理里有大量请求 共享前缀 ——系统提示词、few shot 示例、多轮对话历史、RAG 文档、Agent 上下文……这些前缀对应的 KV 缓存其实是一样的,每来一个请求都重算一遍纯属浪费。 SGLang 的 RadixAttention 已经能在单机复用前缀 KV,HiCache 又把这套思路扩展成三层缓存——GPU 显存(L1)、主机内存(L2)、外部分布式存储(L3)。 PeerCache 就是这个 L3 :它让前缀 KV 不仅能在一台机器里复用,还能 跨节点 复用。 工作方式很简单: 生产节点把 KV 页发布进 自己的本地池 ,并把一条很小的位置记录写进 分片在所有节点上的一致性哈希目录 ; 任意节点查到 key 后,用 单边 RDMA READ 直接零拷贝拉进自己的 buffer。 接入也很省事:用 直接挂进 SGLang, 无需 patch SGLang 。 二、它不是什么:两个别混的正交维度 这是整篇里我最想强调的一点。一提到"跨节点搬 KV + RDMA + Mooncake",很多人第一反应是 PD 分离里那条 prefill → decode 的 KV 交接 。但那条路 不是 PeerCache 干的活。 PeerCache ≠ PD 搬运引擎。 它不负责每个请求的 prefill→decode KV 交接——那条延迟敏感的 GPU→GPU 路径是 Mooncake / NIXL 通过 做的。 PeerCache ≠ 中心化存储。 没有需要部署 / 扩容的 master 或元数据服务。 把两个维度摆在一起看就清楚了: | 维度 | KV / 前缀复用(PeerCache) | PD 的 P→D 交接(Mooncake/NIXL) | | | | | | 范围 | 跨请求 / 跨节点 | 单个请求内 | | 目标 | 省掉重复计算共享前缀 | 把 prefill 的 KV 交给 decode | | 延迟 | 缓存型,可容忍主机暂存 | 延迟敏感,GPU→GPU 直传 | | SGLang 参数 | | | 所以这俩 不是二选一 :一个 PD 集群通常两者都用—— PeerCache 在 prefill 层做前缀复用,Mooncake / NIXL 做 P→D 交接 。这也是我上一篇讲 PD 分离时它们各自的位置。 三、为什么不直接用中心化的 KV 缓存 放对了维度之后,PeerCache 真正要对比的是那些 做 KV 复用、但靠中心 master / 中心元数据 的存储——比如 Mooncake Store、分布式模式的 LMCache。 它们都很成熟、能力也强,但形态决定了要养一套中心化协调设施:master 分配 / 跟踪对象,元数据服务存放 lookup,数据常常还要拷进专用托管池。PeerCache 的取舍是另一条路: 把中心节点全部砍掉,用一致性哈希把目录分散到每个节点上 。 | 维度 | 中心化存储 | PeerCache | | | | | | 元数据 | 中心 master / lookup 服务 | 一致性哈希 DHT,分片到所有节点 | | 单点故障 | master 是 SPOF / 瓶颈 | 没有中心元数据节点 | | 元数据吞吐 | 受 master 限制 | 随集群规模扩(每节点 1/N) | | 数据放置 | 常需拷进托管池 | 留在生产节点本地 | | 写路径 | 入池 + 协调 | 本地 memcpy + 一条小位置记录 | | 读路径 | 经存储 / 引擎 | 单边 RDMA READ,零拷贝 | | 要运行的服务 | master + worker | 仅内嵌发现(无独立 master) | | 扩展 | 给协调者扩容 | 加节点 → 环自动 re shard | 差别集中在两点: 目录是分片而不是中心存的 , 数据是留在生产者本地而不是搬进专用池的 。下面分别讲。 四、核心理念 我把整套设计浓缩成几条原则: 内嵌服务发现,没有独立 meta 节点。 在所有节点上把 配成同一个节点的 IP,那个节点会在 进程内 自动承担服务发现:别的节点向它注册、心跳、拉取实时成员列表。它这里 既不存数据,也不存元数据 ——只是一个成员名册。 一致性哈希目录(DHT)。 映射 通过对 key 取哈希分片到所有节点。写入方和读取方各自对 key 取哈希,就能独立地算出"这条记录归谁管",不需要问任何人。 写入时数据留本地。 把页面拷进节点本地的发布池(一次主机 memcpy,不走网络、不依赖 master),只把一条极小的位置记录推到目录归属者那里。 读取时单边 RDMA READ。 先查目录拿到 ,再发起一次零拷贝 ,数据直接落进 SGLang 已经注册好的主机缓冲区。 磁盘持久化分层(L4)。 被内存淘汰的页面可以落盘,之后再被读取时提升回内存。 内置监控。 Prometheus 端点 + 一个零依赖的 HTML 可视化页面。 五、双 MR 模型——一个绕不开的正确性问题 这是 PeerCache 里我觉得最值得讲的一个设计点。 直觉上,既然要做零拷贝 RDMA READ,那直接把 SGLang 的主机 KV 缓冲区注册成 MR、把它的地址发布到目录里,让远端来读不就行了? 不行。 SGLang 的主机 KV 缓冲区是 L2 层 ,会被 HiCache 随时驱逐 / 覆盖。如果我把它的地址发布出去,远端 READ 可能会落到一个 已经被复用的页面 上——读到的是别的请求的 KV,这就是典型的悬空引用 / 数据损坏。 所以每个节点注册 两块 内存区域(MR): 1. 接收 MR = —— 时单边 READ 的 目标 (数据落进这里)。 2. 发布池 MR = 后端自己持有、带 LRU 的主机内存池——远端节点 READ 的 来源 。 时把页面 memcpy 进发布池(节点本地、不走网络),并把 发布到目录。 从池里驱逐一个页面,会同时删掉对应的目录条目 ——因此一条已发布的地址在它被驱逐之前始终有效,远端永远不会读到悬空地址。 这就是为什么写端那一次 memcpy 是 必要 的:它把"会被随时复用的 L2"和"我能保证生命周期的发布池"解耦开。这是为正确性付出的标准代价,而网络传输本身仍然是零拷贝。 六、读写数据流 写入路径 写入开销 = 一次本地 memcpy + 一次小目录 RPC 。没有 master,也没有 KV 数据的网络拷贝。 读取路径 如果目录显示数据就在读取方自己身上,读取会退化成一次本地 ,完全不走网络。 一次"生产者 → 消费者"到底拷几次 只统计(庞大的)KV 数据的搬运,目录 RPC 只有几十字节,忽略不计: | 操作 | KV 数据拷贝次数 | 发生了什么 | | | | | | (写,生产者) | 1 次主机 memcpy | 页面从 SGLang 主机缓冲区 → 后端发布池 MR(节点本地,不走网络) | | (远端读) | 0 次 CPU 拷贝 | 单边 ;网卡把字节从远端发布池直接 DMA 进读取方主机缓冲区 | | (数据已在本地) | 1 次主机 memcpy | 发布池 → 主机缓冲区,不走网络 | 所以一次跨节点 KV 传输的代价是: 写端一次主机 memcpy + 读端一次零拷贝 RDMA READ 。 七、控制面 / 数据面的分工 PeerCache 把实现干净地切成两半: 控制面用 Python(走 TCP) , 数据面用 C++ / libibverbs(走 RDMA) 。 几个我比较在意的工程细节: 一致性哈希目录 :每个节点承载目录的一个分片,所有分片的并集才是完整目录,不存在中心存储。默认每节点 160 个虚拟节点(vnode)来均衡分布。 (默认 2)可以把每条条目写进接下来的 N 个归属者做高可用,读取时在副本间回退。 连接管理 :连接引导用极小的 TCP 握手交换 (qp num / psn / lid / gid),把设备选择和连接建立彻底解耦,随后 QP 走 INIT → RTR → RTS。每个对端维护一个 有界的通道池 (一个通道 = 一条 RC QP + 自己独立的 CQ),惰性创建、复用、用 封顶——既避免 O(N²) 全连接网格,又允许多个读取者并发读同一个对端。 并发模型 :服务端本就完全多线程,单边 RDMA READ 完全不耗响应方 CPU;客户端 在整个 RDMA 传输期间释放 GIL,每个读取线程租一条独立通道(QP + 私有 CQ),N 个线程在 N 个 CQ 上各自 post/poll,没有共享 CQ 竞争。 八、磁盘分层(L4):把淘汰的页面接住 内存池总是有限的,写满之后被 LRU 淘汰的页面通常就丢了。PeerCache 提供一个可选的磁盘分层把它们接住: 写透(异步) : 时页面落内存池后,会被排队异步写盘(默认 ,上限 ,磁盘自身也按 LRU 约束)。 淘汰 ≠ 删除 :内存池淘汰某页时,目录条目保留,只标记 (数据在盘上)。只有等磁盘也淘汰它,目录条目才真正删除。 读时提升 : 解析到非驻留条目会触发提升——数据所属节点把页面从盘读回内存池,重新标记驻留,再正常提供零拷贝 READ。远端读则发一个 RPC 让所属节点先提升再返回新的 。 磁盘分层是可选的( ),而且能优雅降级:如果 建不出来就自动禁用,内存池退回"淘汰即删除"。 九、性能基线 下面这组数字来自一套特定的 8 卡 RoCE 环境,用内置的 / 双机工具测得(GET 路径,单边 RDMA READ 读 KV 页,MLA 布局)。 它展示的是方法论与曲线形态,不是性能保证 ——请用复现命令在你自己的硬件上重跑。 测试环境 | 项 | 值 | | | | | 拓扑 | 2 台主机(生产者 / 消费者),跨机 RoCE | | 网卡 | 8 × Mellanox ConnectX 7 RoCE,bond | | RoCE | RoCEv2,GID index 3,MTU 4096 | | 单卡线速 | ≈ 400 Gb/s(裸 READ 实测 392 Gbps) | | CPU | 2 × AMD EPYC 9K84,96 核/路(192 核 / 384 线程) | | 主机内存 | 2.2 TB(每 NUMA 节点 ≈ 1.16 TB) | | 传输 | ,布局 | 总览:从单卡到整机 | 场景 | GET 吞吐 | 占单卡裸 RDMA | 说明 | | | | | | | 裸 ,1 卡,16 QP | 49.0 GB/s(392 Gbps) | 100% | 单卡硬件参考值(保守) | | PeerCache,1 卡,8 进程 | 46.0 GB/s(368 Gbps) | 94% | 存储层开销 ≈ 6% | | PeerCache,单进程,8 rail,1 MiB 页 | 147.6 GB/s(1.18 Tbps) | — | 受 GIL 限制;约 3 张卡的量 | | PeerCache,8 卡,多进程,128 KiB 页 | 413.1 GB/s(3.3 Tbps) | — | 每卡 25–89 GB/s;受内存/PCIe/NUMA 限制 | 值得一提的是,满负载多进程下 单卡实际跑到了 89 GB/s ——上面 16 QP 的 只是一个保守的单卡参考值,并不是硬上限。 1 · 单卡——PeerCache 对比裸 RDMA 为衡量存储层引入的开销,把单卡的 PeerCache 和裸 fabric 直接对比: | 测量 | GET 吞吐 | | | | | (裸单边 READ) | 49.0 GB/s(392 Gbps) | | PeerCache GET,128 KiB 页,8 进程 × 4 线程 | 46.0 GB/s(368 Gbps) | PeerCache 落在裸 的 6% 以内 。这点差距来自目录查找 + 每批编排;开启 后,热的、静态的工作集上目录 RPC 基本被摊掉。这说明零拷贝读路径上几乎没有引入额外开销。 2 · 单进程多卡(multi rail) 设置 ,一个进程就会每卡开一条 rail,并在一次释放 GIL 的 C++ 调用( )里把每批 READ 横跨所有 rail 分发。 | 页大小 | batch | 峰值 | 最佳线程数 | | | | | | | 128 KiB | 32 | 40.4 GB/s | 4 | | 1 MiB | 128 | 147.6 GB/s | 2 | 两点值得注意: 单进程被 GIL 限制 :吞吐在低线程数(2–4)就到峰值,线程越多反而下降——每批的 Python 编排被 GIL 串行化,加线程只增加争用。 大传输能摊薄这部分开销 :GIL 持有的开销是按调用算的、不是按字节,所以把页从 128 KiB 加到 1 MiB,单进程从 40 → 148 GB/s(约 3 张卡的量)——尽管两者都受 GIL 限制。 所以 multi rail 让一个进程能用上多张卡,但单个 Python 进程吃不满全部 8 卡——那需要多进程。 3 · 整机——多进程跨 8 卡 生产形态(也是吃满每张卡的方式)是每卡一个进程组——正是 SGLang TP=8 部署的运行方式(8 个 rank,各绑本地网卡)。这里: 8 卡 × 每卡 8 个读进程,128 KiB 页 。 | 指标 | 值 | | | | | 聚合 GET | 413.1 GB/s(3.3 Tbps) | | 单卡区间 | 25.1 – 89.4 GB/s(均值 ≈ 51.6) | | 配置 | 8 卡 × 每卡 8 进程,128 KiB 页 | 聚合远超单进程(147 → 413 GB/s),而且单卡在这里跑到了 89 GB/s ,所以已经不再受网卡限制——瓶颈是主机内存带宽 / PCIe,以及不均衡(两张卡只有 25 GB/s,其余 49–89)。本机上网卡 1–4 在 NUMA node 0、5–8 在 node 1,没绑核的读进程可能落到错误的节点、付出跨 NUMA 代价。用 把每个进程组绑到该网卡的 NUMA 节点,就能把慢的网卡拉回来、抬高聚合。 4 · GPUDirect RDMA:直接读进 GPU 显存 真实 SGLang 部署里 KV 缓冲区其实在 GPU 显存 里。PeerCache 可以注册这块显存,让单边 READ 直接落进显存, 不经过主机内存中转 :缓冲区暴露 dmabuf fd 时用 注册,否则在加载了 时直接注册设备虚拟地址。 实测(单进程,8 rail,1 MiB 页,读进显存): 49.5 GB/s ,100% 命中(同条件下读进主机内存是 140 GB/s)。这个差距是 单 GPU 的 PCIe 瓶颈 ——8 条 rail 全写进同一块 GPU,共享它那条 PCIe 链路(Gen5 x16 约 50 GB/s)。 关键在于:真实 SGLang TP=N 部署里每个 rank 读进 自己的 GPU,所以 GPUDirect 会随 GPU 数量 线性放大 (≈ N × 单 GPU PCIe 带宽),不会被单条链路卡住。 关键结论 单卡 :PeerCache ≈ 裸 的 94%——RDMA 路径接近最优。 GIL 是单进程的天花板 :用低线程数 + 大 batch / 大页把单进程压到最高;单进程吃不满全部网卡。 整机带宽需要多进程 (每卡一组),这与 SGLang 多 rank 部署形态一致,满负载下整机能到 413 GB/s。 超过约一张卡后 ,瓶颈转移到内存 / PCIe / NUMA,不再是 fabric——绑 NUMA、均衡 bond 即可。 注:1 MiB 页是合成值,用来展示大传输时的余量;真实 MLA KV 页通常约 128 KiB。引用数字时务必标注页大小。 十、什么时候用,又主动舍弃了什么 去中心化不是免费的,得把得失都说清楚。 优势 元数据无单点故障 / 瓶颈 :中心化方案里每次 PUT/GET 都打到 master;PeerCache 把目录分片,元数据吞吐随集群增长,没有中心热点。 写路径轻、数据有局部性 : = 本地 memcpy + 一条小目录记录,不拷进中心池。 运维组件更少 :发现服务内嵌,没有 master 要部署、扩容、做 HA。 无协调者横向扩展 :新节点同时增容量和元数据吞吐,成员变化自动 re shard 目录。 去中心的故障域 :挂一个节点只丢它那份分片,不会整个元数据服务瘫; (默认 2)还保留副本。 主动舍弃了什么 成熟度与生态 :Mooncake / LMCache 经过大规模打磨,淘汰 / 分层 / 可观测性更全、集成更广;PeerCache 更精简、更年轻。 全局放置决策 :中心 master 能做更聪明的全局淘汰 / 放置 / 负载均衡;PeerCache 只做"本地 + 哈希"决策。 生产者热点与数据冗余 :KV 字节留在生产节点,热 key 可能让该节点成读热点;而且 KV 数据本身默认 不复制 ——生产节点宕了那份页就不可用(目录有副本、有 disk 层兜底,但 KV 字节没多副本)。中心池更容易摊平负载、做数据冗余。 驱逐竞争是安全降级 :池驱逐会删目录条目,任何解析到陈旧 / 缺失条目的读取都返回 miss,让 SGLang 重新计算——不会读到坏数据,但确实是一次缓存 miss。 一张决策表 | 你的情况 | 建议 | | | | | 想跨节点复用 KV、最少复杂度、不要 master | PeerCache(聚合模式) | | 需要 P/D 物理解耦(扩缩 / SLO) | Mooncake/NIXL 做交接 + PeerCache 在 prefill 做复用 | | 需要全局放置、丰富特性、强数据 HA | 成熟的中心化存储 | | 提示词都唯一、无共享前缀 | 任何 KV 复用缓存都帮不上多少 | 最契合 PeerCache 的,是 聚合式(非 PD)+ 高前缀复用 的负载:系统提示词、few shot、多轮历史、RAG、Agent 上下文——这里挂上 PeerCache 就是一个完整的共享缓存层,不用再养传输引擎;以及 想要类似 Mooncake Store 的复用能力、但不想再养一个中心 master 的团队。 十一、小结 PeerCache 想表达的其实是一个很朴素的观点:如果你要的只是"跨请求、跨节点复用 KV 前缀",那么 master 和 metadata 服务并不是必须的——把目录用一致性哈希分散开、让数据留在生产者本地、用双 MR 模型保证发布地址的生命周期,就能在砍掉中心设施的同时拿到接近裸带宽的读性能(单卡 94%,整机 8 卡 413 GB/s)。它和 PD 的 P→D 交接是正交的两件事,在 PD 集群里两者可以同时用。 如果你也在折腾 SGLang 的 L3 后端,欢迎去 项目主页 看看定位对比、架构文档、性能基线和 SDK 参考,也欢迎在 issue 里交流。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/peercache_scaling_ladder.webp" />
</item>
<item>
<title>大模型推理的 PD 分离:原理、动机与 Mooncake 的实现</title>
<link>https://gitpull.cn/post/20260529/</link>
<guid isPermaLink="false">大模型推理的-pd-分离原理动机与-mooncake-的实现</guid>
<pubDate>Fri, 29 May 2026 03:40:23 GMT</pubDate>
<author>兰州小红鸡</author>
<category>算法</category><category>分布式存储</category>
<description><![CDATA[当我们谈论 LLM 推理优化时,绕不开一个关键词—— PD 分离 (Prefill Decode Disaggregation)。这篇文章从第一性原理出发,讲清…]]></description>
<content:encoded><![CDATA[当我们谈论 LLM 推理优化时,绕不开一个关键词—— PD 分离 (Prefill Decode Disaggregation)。这篇文章从第一性原理出发,讲清楚 PD 分离为什么会出现、它解决了什么问题,以及业界最完整的开源实现 Mooncake 是怎么落地的。 一、从一次推理说起 我们先回到最基础的问题:一次 LLM 推理请求,内部到底发生了什么? 当你向模型发送一句 prompt(比如"帮我写一首关于春天的诗"),模型的处理过程会分成两个 计算特征完全不同 的阶段: Prefill(预填充)阶段 模型需要先"读懂"你的整段输入。这个阶段会把 prompt 里所有 token 一次性 送进网络,并行计算它们之间的注意力,最终产出第一个输出 token。 因为是 N 个 token 一起算,矩阵运算是 矩阵 × 矩阵 (GEMM) 瓶颈在 算力 (FLOPs)——GPU 的 Tensor Core 火力全开 这个阶段的耗时,决定了用户看到第一个字的时间,也就是 TTFT (Time To First Token) Decode(解码)阶段 第一个 token 出来后,模型开始逐字往外蹦。每生成一个新 token,都要回顾前面所有的历史。 每步只算 1 个新 token,矩阵运算退化成 向量 × 矩阵 (GEMV) 瓶颈在 显存带宽 (HBM Bandwidth)——要把巨大的 KV cache 反复从显存读出来 这个阶段每一步的耗时,决定了出字速度,也就是 ITL (Inter Token Latency) 一张表看清差异 | 维度 | Prefill | Decode | | | | | | 计算形态 | 矩阵 × 矩阵(GEMM) | 向量 × 矩阵(GEMV) | | 瓶颈资源 | 算力 FLOPs | 显存带宽 HBM BW | | 适合的 batch | 小(1 4) | 大(32 256) | | 适合的并行 | TP 大(缩 TTFT) | TP 小 + 多副本(提吞吐) | | 适合的硬件 | 高算力卡(H100/H200) | 高带宽 / 高性价比卡 | | 性能指标 | TTFT | ITL | 这张表是理解 PD 分离的全部基础—— 两个阶段想要的东西,几乎处处相反 。 二、为什么不能混在一起跑 传统做法是 prefill 和 decode 跑在同一组 GPU 上,请求来了就排队处理。这种方式有几个躲不掉的硬伤。 1. 互相干扰:长 prefill 卡住所有 decode 想象一个场景:几十个用户正在流畅地逐字接收回复(decode 进行中),这时来了一个 4000 token 的长 prompt 请求。 为了处理这个长 prefill,GPU 被占用几百毫秒甚至上秒。在这段时间里, 所有正在 decode 的用户都卡住了 ——他们的出字速度(ITL)瞬间抖动,体验崩坏。 这就是经典的 Head of Line 阻塞 :一个重请求拖垮一片轻请求。 2. 资源错配:一半的硬件在摸鱼 Prefill 阶段:算力拉满,但 显存带宽几乎闲置 Decode 阶段:带宽拉满,但 算力几乎闲置 两个阶段混跑,意味着无论什么时刻,你的 GPU 总有一半的能力在浪费。在动辄几百上千张卡的集群里,这种浪费直接换算成 30% 50% 的成本损失 。 3. 配置无法两全 Prefill 喜欢 小 batch (避免 padding 浪费),Decode 喜欢 大 batch (榨干带宽) Prefill 倾向 大 TP ,Decode 倾向 小 TP + 多副本 单一资源池只能取一个折中值,结果是两边都不满意。 三、PD 分离:把两个阶段拆开 既然两个阶段想要的东西相反,那就 物理上把它们分到两个独立的池子 : 这样一来: TTFT 和 ITL 彻底解耦 :长 prefill 不再干扰正在 decode 的请求 硬件按角色定制 :prefill 用高算力卡,decode 用高带宽 / 高性价比卡 批大小、并行度各自最优 :两个池子独立调参 独立扩缩容 :白天 prefill 压力大就加 prefill 节点,反之亦然 收益巨大,但天下没有免费的午餐。PD 分离引入了一个 唯一但棘手 的新问题: Prefill 算完的 KV cache,怎么高效地传给 decode 节点? 这就是整个 PD 分离工程的核心技术难点。 四、核心难题:KV cache 怎么传 先建立一个量级直觉。一次请求的 KV cache 有多大? 以一个 72B 模型、4K token 的 prompt 为例,KV cache 轻松到 几百 MB 量级。而这份数据必须在 prefill 结束、decode 开始之前,从 prefill 节点搬到 decode 节点。 这一搬,卡在 关键路径 上: 所以 KV 传输方案的好坏,直接决定了: 1. TTFT :传得慢,用户等得久 2. GPU 利用率 :传输期间 decode GPU 空转,烧钱 3. 集群吞吐天花板 :高并发下,KV 传输会吃掉大量 NIC 带宽 一个朴素的想法是:搞个中心化的存储服务,prefill 把 KV 写进去,decode 再读出来。但这样数据要 走两趟网络 (写一次 + 读一次),高并发下直接把 NIC 带宽消耗翻倍,吞吐天花板砍半。 更好的答案是 P2P 直传 ——让 decode 节点 直接 从 prefill 节点拉 KV,数据只走一趟。这正是 Mooncake 的核心思路。 五、Mooncake 的实现 Mooncake 是 Moonshot AI(月之暗面)开源的 KVCache 中心化推理架构,也是 Kimi 背后的生产级方案。它是目前开源世界里最完整的 PD 分离 + KV 复用实现。 5.1 整体分层 Mooncake 不是一个单体服务,而是一套分层组合: P2P 直传发生在最底层的 Transfer Engine ,上面两层负责元数据与调度。我们自底向上看。 5.2 Transfer Engine:P2P 数据面的发动机 Transfer Engine 是 Mooncake 自研的高性能 RDMA 传输库,可以理解成一个「为 LLM 推理量身定制的、加强版的 UCX」。它的几个关键能力: 统一内存视图 通过 ,把不同位置的内存都注册成 RDMA 可直接访问的内存区域(MR): GPU 显存( 出来的) Host DRAM NVMe 映射的内存 参数携带 NUMA / PCIe 拓扑信息,供后续选路优化。 Segment 抽象 每个进程把自己注册的内存暴露成一个或多个 Segment ,拥有全局唯一 ID(形如 )。其他节点只要知道这个 ID 和偏移量,就能远程读写。 Multi rail 多网卡聚合 自动探测每张 NIC 的 PCIe 距离和 IB 链路状态,为一次传输挑选拓扑上最近的网卡,并把大块传输 拆分到多张网卡并发 ,单连接就能打满多 NIC 的聚合带宽。 GPU Direct RDMA 这是最关键的一点。注册 GPU 显存后,配合 ,KV cache 可以 直接从 prefill 节点的 GPU 显存,RDMA 到 decode 节点的 GPU 显存 ,全程不经过 host 内存,零额外拷贝。 核心 API 重点: Transfer Engine 本身不是一个服务 ,它是一个库。每个 worker 进程链接它、各自启动 RDMA endpoint,节点之间是 真正的点对点 通信。 5.3 KVCache Pool:解决「数据在哪」 光有搬运工还不够。Prefill 把 KV 算好放在自己的显存里, decode 怎么知道该去哪个节点、哪个 segment、哪个偏移量拉数据 ? 这就是 Mooncake Store 层的职责,核心是一个集中式的 Master ,维护全局的 映射。 5.4 一次 P→D 传输的完整流程 1. Prefill 完成 :KV cache 此刻就在 P 1 节点的显存里,这块 buffer 早已通过 注册成 segment 2. 上报元数据 :P 1 向 Master 上报「 在 segment 的 offset ,长度 8MB」—— 注意,数据本身一个字节都没动 3. 调度 decode :调度器把这个请求的 decode 阶段分配给 D 3 4. 查询元数据 :D 3 向 Master 发起 ,拿到 5. P2P 直传 :D 3 调用 , 直接通过 RDMA 从 P 1 的显存把 KV 读过来 ,Master 全程不碰数据 6. 开始 decode :D 3 拿到 KV,立即开始出字 整个过程里, 真正的大数据传输只发生一次 (步骤 5),而且是点对点的 1 跳 RDMA。Master 只参与了两次轻量的元数据交互(步骤 2、4),每次只有几十上百字节。 5.5 关键优化:元数据其实"不要钱" 你可能会问:Put 和 Get 这两次跟 Master 的交互,不是也多了网络往返吗? 确实多,但量级完全不同: | | 元数据 RPC | KV 数据传输 | | | | | | 数据量 | 100 字节 | 几百 MB | | 耗时 | 几十 µs(小包,纯延迟) | 几 ms(带宽受限) | | 是否占带宽 | 几乎不占 | 占满 NIC | 更何况元数据这跳还能被进一步消化: 异步预取 :decode 节点在等 prefill 计算时,就能提前查好元数据 批量查询 :一个请求几十层 KV,元数据可以一次性批量取回 搭车调度消息 :在不需要全局 KV 复用的「纯 PD」模式下,连 Master 都可以省掉,把 KV 位置信息直接塞进"请求从 prefill 池转交到 decode 池"的那条调度消息里——这条消息本来就要发,元数据是 顺风车,零额外开销 所以 Mooncake 的精髓不是"省跳数",而是 省掉那一整次大数据传输 :数据原地待在 prefill 节点,decode 直接上门取,而不是先寄到中转仓库再去取。搬货的成本远大于打个电话问地址。 5.6 Layer by layer 流式传输 还有一个锦上添花的优化。Transformer 是逐层计算的,prefill 算完第 N 层,就能立刻把第 N 层的 KV 传出去 ,不必等整个 prefill 结束。 这样 KV 传输的耗时就被 隐藏在了后续层的计算里 (overlap),decode 节点的 first layer 一就绪就能开始处理。在理想情况下,传输延迟几乎被完全吃掉,进一步压低 TTFT。 六、部署形态 Mooncake 在实际部署时有几种典型拓扑,按业务规模和需求选择。 形态 A:Pure PD(最小化) 只做 P→D 单次直传,不需要跨请求的 KV 复用。 不需要 Master, 不需要 etcd 直接使用底层 Transfer Engine API KV 位置信息走推理框架(如 sglang)自己的调度通道 sglang 原生的 PD 实现(基于 Mooncake Transfer Engine 或 NVIDIA NIXL)走的就是这条最简路线。 形态 B:PD + KVCache Pool(生产推荐) 既要 PD 直传,又要跨请求复用 KV(比如多轮对话、共享系统提示词的 prefix 缓存)。这是 Kimi 的生产姿势。 一个容易混淆的点: 每个 P/D worker 进程链接了 Store Client 库,本身就充当了存储池中的一个节点 。它启动时会把自己的一段内存注册进全局池。所以—— 不需要 单独部署 这个独立进程。 只有当一台机器"不跑推理、纯粹想贡献内存当存储节点"时,才需要单独起 。 各组件清单: | 组件 | 数量 | 是否必须 | 说明 | | | | | | | | 1 3(HA) | 必须 | 元数据路由,不参与数据面 | | metadata server(etcd 等) | 1 3 | 必须 | 服务发现 | | | 0 | 不必须 | 仅纯存储节点才需要 | | 推理 worker | N | 必须 | 自带 store 角色 | 形态对比 | 部署方式 | Master | etcd | 独立 store service | 适用场景 | | | | | | | | Pure PD | 否 | 否 | 否 | 只要 P→D 直传 | | PD + Pool | 是 | 是 | 否 | PD 直传 + 跨请求复用 | sglang 集成示例 PD + Pool 模式下,每台 P/D 节点的启动配置: P 节点与 D 节点的差异: (各自 IP)、 (各自贡献多少内存)、 (角色)。整个集群只需要一组 Master + etcd。 七、小结 回顾一下整篇文章的逻辑链: 1. LLM 推理天然分两阶段 :prefill 吃算力,decode 吃带宽,两者诉求处处相反 2. 混跑的代价 :互相干扰、资源错配、配置无法两全,成本浪费 30% 50% 3. PD 分离的思路 :物理上拆成两个池子,各取最优,TTFT 与 ITL 解耦 4. 唯一的新难题 :KV cache 怎么跨节点高效传输 5. Mooncake 的答案 :Transfer Engine 做 P2P 直传(数据只走一趟、支持 GPU Direct、multi rail、layer overlap),Master 做轻量元数据路由(µs 级、可异步、可搭车) PD 分离的本质,是用一次 架构上的解耦 ,换来 GPU 利用率和集群吞吐的大幅提升。而 Mooncake 用一套优雅的「数据面 P2P + 控制面元数据」分层,把其中最棘手的 KV 传输问题解得相当漂亮—— 让数据待在原地,让需要的人直接上门取 。 参考资料 Mooncake 论文:A KVCache centric Disaggregated Architecture for LLM Serving Mooncake 开源仓库 Splitwise: Efficient Generative LLM Inference Using Phase Splitting DistServe: Disaggregating Prefill and Decoding for Goodput optimized LLM Serving]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/05/1780025971901-bu285b-image.webp" />
</item>
<item>
<title>CPTI:手机上就能玩的恋爱人格小测验</title>
<link>https://gitpull.cn/post/20260514/</link>
<guid isPermaLink="false">cpti-love-personality-test-intro</guid>
<pubDate>Thu, 14 May 2026 00:00:00 GMT</pubDate>
<author>Jimmy</author>
<category>杂七杂八</category><category>前端</category>
<description><![CDATA[CPTI 是一个偏手机体验的恋爱向趣味测验:刷题像刷短内容,最后给你「你像谁」和「谁更适合接住你」两套结果,再配一整套恋爱角色卡。本文只聊怎么玩、能玩出什么,不聊实现细节。]]></description>
<content:encoded><![CDATA[在线体验:https://cpti.cc 如果把 CPTI 想成一款「恋爱主题的轻游戏」,它大概在做三件事: 用很轻的交互把题刷完 、 用角色卡把结果讲明白 、 顺手给你一点「和谁更合拍」的谈资 。下面按「能做什么 → 怎么玩 → 有哪些人格」来写。 你能玩到什么 像刷瀑布流一样答题 :题目拆成短卡,单手滑动、点点选项就能推进,适合地铁上、睡前几分钟随手测一把。 不只有「你是什么型」 :同一套题会给出 两份结果 ——一份更像「你在恋爱里常出现的姿态」,另一份更像「哪种风格的人更接得住你」。自己存档也好,和朋友互相晒结果也好,都多了一层可聊的。 题型不单调 :除了常规选项,还有 聊天场景题 和少量 调剂用的搞怪题 ,整体氛围偏轻松,不像在做严肃量表。 结果页支持一键复制 :测完把整段文案复制走,丢给闺蜜分析、发朋友圈吐槽、写进备忘录都行。 怎么玩(最短路径) 1. 打开站点首页,进 测试页 。 2. 按提示选 性别 / 生日星座 等基础信息(用于文案与形象呈现更贴你选的线,可按页面说明来)。 3. 一题一题往下刷;拿不准就选当下第一反应,趣味测验不必「演成更好的人」。 4. 做完进 结果页 :先看主结果角色卡,再读「谁更适合接住你」那一栏,对照你真实相处过的人,会特别有画面。 5. 可选:填 MBTI → 再读一遍结果里和你相关的句子,看看有没有多中几条。 两套结果分别像什么 可以把它们理解成: 主结果 :偏「你的恋爱出厂设置」——你更容易用哪种节奏去喜欢人、闹别扭、付出或撤退。 副结果(接住你) :偏「相处体验」——谁的风格更不踩你的雷、更能把你的情绪稳稳托住。 两套都来自同一套题,所以不会出现「完全不相干的两个故事」,更像是 同一场恋爱里,镜子和内景镜头各拍了一张 。 人格图鉴:图一乐,但每张卡都很「有戏」 下面这些名字都来自 CPTI 里的角色卡(每条下的一行字,是根据项目里的标签与短文案压缩成的「玩法向」介绍,方便你扫一眼找共鸣)。 不必对号入座成真实人格标签 ,当恋爱漫才里的「职业设定」就好。 高能量 / 直球组 小狗 :热情写在脸上,喜欢就想贴贴;最怕长期冷场和已读不回。 直球大师 :不爱猜谜,喜欢就讲清楚;需要学会偶尔等等对方节奏。 上头怪 :一点火花就能脑补整季剧情;适合配情绪稳定、回应明确的人。 恋爱脑 :一动心就容易全情投入;要练习给自己留一点生活重心。 照顾型 / 「你先吃饭」组 daddy :习惯把事扛起来、用行动兜底;别忘了自己也需要被照顾。 妈妈 :温柔里带着把日子摆平的掌控感;偶尔把遥控器递出去更轻松。 男妈妈 :叮嘱和后勤满分;要的是被看见、被肯定,而不是被当成理所当然。 傲娇、慢热、边界感组 小猫咪 :嘴硬心软,安全感够了才会翻肚皮;吃温柔、不吃硬拧。 刺猬 :先确认安全再靠近;你要的是边界被尊重,不是被强攻。 禁欲系 :外表克制、内心未必冷淡;适合松弛、干净、懂留白的人慢慢敲门。 哑巴 :爱很多时候在行动里,嘴上却掉线;需要对方有耐心、少逼问。 母胎 solo :嘴上想脱单、身体很诚实;要的是引导,不是考核。 刚出新手村 :热情有余、经验不足;笨拙本身就很加分,别被自己吓退。 「情场很有戏」组 渣男 / 渣女 :会撩、会制造稀缺感;真正好玩的是——你敢不敢在一个人身上认真收尾。 海王 :池子大、节奏松;遇到能让你服气的人,才容易自愿靠岸。 魅魔 :魅力被动技能点满;长期关系里比的是平淡日子还能不能互相接住。 网恋专业户 :线上情话满分,线下容易社恐;练习把线上的自己挪一点到现实里。 认真、钝感、另一种浪漫 纯爱战士 :专一、细节控;认真很好,但别把对方的情绪全盘背自己身上。 原始人 :讨厌套路,信奉简单直白;像手写情书,慢但耐放。 好人 :温和无害、让人安心;记得把你的好明码标价给「会回礼的人」。 剑客 :利落、决断、不爱拖泥带水;出招前先问一句对方要的是解决方案还是拥抱。 老吃家 :见多识广、先观察再下场;要的是对方用体验把你拉进场,而不是讲大道理。 氪金玩家 :喜欢就用礼物和仪式感说话;心不是满减券,记得也要用语言交流。 空心人 :「无所谓」常常是保护色;适合配情绪稳定、愿意慢慢等你回温的人。 舔狗 :长情又容易把自己放低;把专注度先挪回自己身上,关系反而更清楚。 测完以后, 小狗、恋爱脑、上头怪 这一挂很适合转发给好友互怼(褒义); 小猫咪、刺猬、哑巴 这一挂则更容易在评论区里「世另我」。 小提醒 CPTI 是 恋爱向的趣味网页原型 ,用来激发聊天、自嘲和一点点自我观察, 不是临床或用人测评 。玩得开心最重要;若某段结果读起来让你不舒服,关掉页面就好,真实的关系永远比任何一张角色卡复杂得多。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/cptijietu1.webp" />
</item>
<item>
<title>GitBlog:一个带伪后台的博客站点</title>
<link>https://gitpull.cn/post/welcome/</link>
<guid isPermaLink="false">welcome</guid>
<pubDate>Mon, 11 May 2026 08:50:00 GMT</pubDate>
<author>Jimmy</author>
<category>博客建站</category><category>教程</category><category>项目介绍</category>
<description><![CDATA[一个完全静态、托管在 GitHub Pages 上的博客,但带有可以在浏览器里直接写、直接发的'伪后台'。这篇文章记录它能做什么,相比传统方案为什么这样设计,以及如果你也想搭一个,要怎么开始。]]></description>
<content:encoded><![CDATA[如果把"写博客"这件事抽象一下,无非三步: 写一篇 → 发到网上 → 有人来读 。 围绕这三步,主流方案大体分两条路: 静态站点生成器 (Hexo / Hugo / Jekyll / Astro 等):本地写、本地构建、push 到仓库。便宜稳定快,但每次发文章都要切回终端,发图、build、commit、push,写一篇短文都得绕一圈。 有后端的博客系统 (WordPress / Ghost / Typecho 等):自带在线后台,浏览器里点几下就发文。但要自己买服务器、装数据库、定期更新、做备份、还得防着哪天被扫到漏洞。 这个站点想做的,是把 两条路的好处合在一起 : 部署轻得像静态站——托管在 GitHub Pages, 没有任何服务器、没有任何数据库 ; 写作顺得像有后台的 CMS——浏览器打开就能写、能上传图、能发, 不用再开终端 。 实现思路也很直接: 前端 = 站点 + 写作器 ,所有"后端动作"都通过 GitHub 自家的 REST API 完成。每发一篇文章,本质上是给仓库提交一个 commit,触发 Pages 重新部署,几十秒后线上就更新了。 这个站点都能做什么 读者侧 简书风的内容流 :Hero / 文章列表 / 标签云 / 最近更新 / 首页轮播 文章阅读体验 :自动 TOC 目录、阅读进度条、回到顶部、代码一键复制、标题悬停锚点、图片灯箱、上一篇 / 下一篇、相关文章推荐 导航与检索 :标签聚合页(彩色标签)、按年月归档、站内搜索(顶部按钮 / / ) 主题系统 :浅色 / 深色 / 跟随系统三态切换,4 套主题预设可一键切换(简书 / GitHub / Solarized / Monokai),允许读者自己挑 评论 :基于 giscus(GitHub Discussions),每篇文章按 slug 独立绑定一条 Discussion SEO 与分享 :自动 RSS、sitemap、Open Graph / Twitter Card meta、JSON LD 结构化数据、自动生成 OG 分享图(SVG) 性能 :图片真懒加载(IntersectionObserver,进入视口前 300px 才注入 src)、首屏 LCP 优化、缓存版本号控制、移动端排版做过细化 写作侧(伪后台) 浏览器里写 :EasyMDE 编辑器,工具栏、快捷键、实时预览 图片直接拖 :拖拽 / 粘贴上传,自动归档到 ,并写入 Markdown 链接 一键发布 :发布前自动校验标题 / 摘要 / 标签 / slug,避免提交坏稿 草稿 / 置顶 / 独立页面 : 不进首页, 在首页置顶, 是关于页 / 友链页这种独立页面 管理后台 :文章列表(全部 / 已发布 / 草稿三 tab)、图片库、可视化的站点设置( 里所有字段都能在线编辑,包括导航、社交链接、giscus、主题色板、自定义 CSS) 诊断 :一键诊断页可检查 token、仓库、写权限、分支、索引文件、Pages 状态——出问题立刻能定位 自动化 GitHub Actions 自动重建 / / / OG 图,并校验 frontmatter 迁移脚本 把老博客文章一键搬过来:cnblogs、Hexo、公众号文章,自带图片下载、HTML→Markdown、智能取封面、标签归一化、垃圾文案剥离 与"传统静态站"和"有后端博客"对比 | | 传统静态博客 (Hexo / Hugo / Jekyll) | 本博客(GitHub Pages + 伪后台) | 有后端博客(WordPress / Ghost) | | | | | | | 服务器 | 不需要 | 不需要 | 需要 + 数据库 | | 写文章流程 | 本地写 → build → push | 浏览器里写、点发布 | 后台写、点发布 | | 上传图片 | 本地放到目录 → push | 拖进编辑器自动上传 | 后台上传 | | 发文章成本 | 中(要回到终端) | 极低 | 低 | | 维护成本 | 低 | 极低 | 中(需更新 / 备份 / 防扫) | | 数据所有权 | 仓库即数据 | 仓库即数据,纯 Markdown | 数据库表,迁移成本高 | | 备份 | 仓库即备份 | 每篇文章 = 一次 commit,git 即历史 | 需手动备份数据库 | | 评论 | 需挂第三方 | giscus(GitHub Discussions) | 内置 | | 速度 | 很快(CDN 静态) | 很快(CDN 静态) | 取决于服务器与缓存 | | 离线读 / 跨平台迁移 | Markdown 文件可直接读 | Markdown 文件可直接读 | 数据库内不易导出 | 简单说: 部署成本对齐传统静态站,写作体验对齐有后端的 CMS,数据所有权完全在自己手里 。所有内容都是 目录下的纯文本 Markdown,哪天想搬走、想换工具、想做全文索引,复制一下目录就行。 如果你也想搭一个 你只需要两样东西: 一个 GitHub 帐号 + 一个 Personal Access Token (PAT) 。五步跑起来。 1. Fork 仓库 仓库地址:flymysql/gitblog。Fork 一份到自己名下。 2. 启用 GitHub Pages 仓库 → Settings → Pages → Source 选 ,分支 ,目录 ,保存。等几十秒,第一版站点就上线了。 3. 改 把 / / 改成你的,把 改成你的 Pages 地址( 末尾不要加 ),其他先不动。这一步是为了让 SEO meta、sitemap、OG 图等用到你自己的域名。 其实只要先改这一步推上去, 之后所有配置都可以在 里在线编辑 ,再也不用回到本地。 4. 生成一个 Fine grained PAT 打开 GitHub Token 创建页: Repository access :选 ,勾选你的博客仓库 Repository permissions → 选 ,其他保持默认 设置一个合适的过期时间,复制 token 5. 进后台开始写 访问 ,把 token 粘进去,登录。之后所有写作 / 上传 / 站点设置 / 主题切换都在浏览器里完成。 每次"发布",本质上是给仓库提交一次 commit,几十秒后 GitHub Pages 重新部署,文章就上线了。 (可选)开评论 仓库里启用 Discussions → 去 giscus.app 拿到 → 进 把 giscus 字段填上保存即可。 ⚠ 推荐选 —— 每篇文章按 slug 独立绑定一条 Discussion。 不要选 / ,因为本站所有文章 URL 路径都是 (只有 query 不同,giscus 不读 query),那两种模式会让所有文章共用同一条评论流。 出问题怎么办 进 。这个一键诊断页会按顺序帮你检查:token 是否有效 → 仓库是否可访问 → 分支是否存在 → Contents 写权限 → 索引文件 → Pages 部署状态。任何环节出问题,它会直接告诉你哪一步失败、为什么、怎么修。 一些小信仰 数据是自己的 。所有内容都是仓库里的纯文本 Markdown,搬走 / 备份 / 全文索引 / 重新换工具,都只是 一下的事。 运维越少越好 。能让 GitHub 帮你做的事,就不要自己买服务器去做。 给写作开一条捷径 。最常做的事(写、改、上图、发)都在浏览器里点一下完成,不用再切到终端。 如果你也认同这几条,欢迎 fork 一份,开始写你的第一篇。 祝写作愉快。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/05/1778490605877-blwjij-image.webp" />
</item>
<item>
<title>关于小红鸡</title>
<link>https://gitpull.cn/post/about/</link>
<guid isPermaLink="false">about</guid>
<pubDate>Mon, 11 May 2026 08:40:00 GMT</pubDate>
<author>Jimmy</author>
<category>杂七杂八</category>
<description><![CDATA[关于这个博客和写它的人 —— 一名做系统软件的程序员,这些年从图数据库、推荐系统一路走到大模型推理基础设施(KV 缓存 / RDMA 零拷贝 / 分布式存储),业余写点代码、读点书、记点流水账。]]></description>
<content:encoded><![CDATA[你好,这里是小红鸡 这是一个由我自己折腾、自己写、自己维护的小博客。如果你是从某篇技术文章顺着面包屑摸过来的,或者从老博客(博客园 / flymysql.github.io)一路追过来,那挺好——这篇就是写给你的。 现在在做什么 我是一名已经工作的程序员,一直在做 系统软件 。这几年方向慢慢挪到了 大模型推理基础设施 与 分布式存储 上——说白了,就是 LLM 推理系统底下那一摊"数据怎么存、怎么搬、怎么复用"的活。最近在折腾的大致是这些: KV 缓存与零拷贝传输 :HiCache 的 L3 存储后端、RDMA 单边 READ、PD 分离场景下的 KV 复用。我自己写了一个去中心化的 RDMA 零拷贝 KV 缓存后端 PeerCache,也专门拆过 PD 分离与 Mooncake 的实现; 高性能分布式存储 / 数据处理 :研究过 3FS 是怎么用 USRBIO 把吞吐推到 6.6 TiB/s 的,也走读过 smallpond(DuckDB × 3FS × Ray)的源码; 更早一些 ,在 图数据库 / 查询引擎 上待过:从 Nebula Graph 的源码走读到查询执行的一些理解;也做过 推荐系统 的召回与架构。 把这几样串起来,其实是同一条主线: 查询 / 请求的解析与计划、存储层与数据布局、分布式系统里那些老问题(分片、调度、副本、一致性),再加上把每一次数据拷贝都抠掉的执着。 总体上,我喜欢那种"看得见执行路径"的代码——一条 SQL、一段图查询、一次 KV 读取扔进去,能从语法树 / RPC 一路追到磁盘 IO 或网卡 DMA 的那种。这个博客里很多技术文章,都是在把这条路径上的某一段拆开来给自己讲一遍。 这里在写什么 我把这个博客当作自己的 工程师手账 :技术、生活、和介于两者之间的东西。 技术方面,大致可以归到这几条主线(点标签可以看全部): AI 基础设施 / 分布式存储:这是这两年的主线——大模型推理系统的 KV 缓存、RDMA 零拷贝、PD 分离,以及 3FS / smallpond 这类高性能存储与数据处理。我自己的开源项目 PeerCache 也属于这一摊; 图数据库 / 图计算:从 Nebula Graph 的源码走读,到日常对查询执行的一些理解; 推荐系统 / 推荐算法:写过推荐引擎架构的概览,记一些常见的工程思路; 数据结构 / 算法:从 PAT 乙级到 Dijkstra 单源最短路径,再到工程里一些更朴素的小算法; 前端 / 微信小程序:早年做过几个微信小程序("小鸡背单词"系列)、撸过 vue.js 的小组件、改过自己博客的样式; 博客建站:博客从 Hexo → WordPress → CDN → 现在的 GitHub Pages 伪后台,一路上踩过的坑都还摆在那里。 生活方面,主要是一些随想。早年还写过一段公众号文字(署名 邻家酒肆)——一些不那么程序员的东西,关于成长、关于喜欢、关于离开和回来的故事。如果你顺着归档翻下去看到一篇画风突变的,那大概就是了。 一点点小历史 我并不是天生很强的人。现在写得出来的东西,几乎都是当年作为一个普通本科生,从一道道 PAT 的 C++ 题、一段段 SQL 练习、一个个 Hexo / WordPress 折腾里一点点攒出来的。这个博客里的老文章,多半就是那时候记的笔记。 后来毕业了、参加了工作、换了几次方向——从图数据库的查询引擎、推荐系统,一路走到现在的大模型推理基础设施——才慢慢意识到:很多以前以为离自己很远的东西,其实都是从最朴素的几样基本功长出来的。当年抠一条 SQL 的执行计划,和现在抠一次 KV 读取的每一次内存拷贝,本质上是同一件事。所以这个博客也保留着早年那些小白笔记,没有删——它们多半粗糙,但没什么不能拿出来的。 怎么找到我 GitHub: @flymysql Email: flyphp@outlook.com RSS: rss.xml 如果你想聊点什么——技术问题、博客里的错别字、或者只是想说一句"hi"——欢迎在文章下面用 GitHub 帐号留言(评论是基于 GitHub Discussions 的,不用额外注册),或者直接给我写邮件。 关于这个博客本身 这个博客本身也是一个折腾产物:完全静态托管在 GitHub Pages ,但带一个 纯前端的"伪后台" ——可以直接在浏览器里写文章、上传图片、改设置、调主题,再通过 GitHub Contents API 把改动一笔一笔推回仓库。从这个意义上讲,每一次"发文章"的本质都是给自己的仓库提一个 commit。 如果你也在折腾 GitHub Pages 博客,可以翻翻 博客建站 标签下的那些文章,多半能找到一两个你也踩过的坑。 这一页会随着我在做的事慢慢变。下次回来看,也许又多了一两条新的主线。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/05/1778491021713-cnnps5-1029bg.webp" />
</item>
<item>
<title>Smallpond 源码走读:DuckDB × 3FS × Ray 是如何拼成一台分布式数据处理机的</title>
<link>https://gitpull.cn/post/20260415/</link>
<guid isPermaLink="false">smallpond-source-walkthrough</guid>
<pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate>
<author>Jimmy</author>
<category>分布式存储</category><category>AI 基础设施</category><category>python</category>
<description><![CDATA[Smallpond 是 DeepSeek 开源的轻量级分布式数据处理框架,专为 10 TB 到 PB 量级的 AI / 数据场景设计。它把 DuckDB 的列式向量化执行、3FS 的高带宽存储、Ray 的任务调度三块拼到一起,做成了\"一台机器\"。本文从架构分层、端到端工作流、Parquet 基础到核心源码走读,把这台\"机器\"的工作原理说清楚。]]></description>
<content:encoded><![CDATA[这是「DeepSeek 数据基础设施巡礼」系列的第 2 篇。第 1 篇 《3FS 的零拷贝之路》 讲了底层存储是怎么把吞吐推到 6.6 TiB/s 的;这一篇上一层,看 DeepSeek 是如何在这块底座上搭出"会写 SQL"的分布式数据框架。 Smallpond 是一款轻量级的分布式数据处理框架,定位非常明确——为 AI 和大数据场景下的大规模数据 (通常在 10 TB 到 PB 级 )服务。它没有去重新造一个执行引擎、也没有自己写存储,而是把三块成熟零件拼到了一起: DuckDB :列式向量化的 SQL 执行引擎,单核就能跑出可观吞吐 3FS :基于 RDMA + NVMe 的高性能分布式文件系统(也是 DeepSeek 开源的) Ray :业界成熟的分布式任务调度平台 这种"组装风格"的取舍非常有代表性:拿走的是工程上的复杂度,留下的是一份高度聚焦的代码骨架。下面我从架构分层开始,自顶向下把这套骨架拆给你看。 一、系统架构 1.1 架构分层设计概述 Smallpond 框架采用清晰的四层架构设计,各层通过明确定义的接口进行交互: 把这四层职责和它们各自的"上下游"梳理一下: 1) logical 模块 职责 定义逻辑计划(Logical Plan)并优化查询( ) 管理用户自定义函数(UDFs) 交互 通过 类统一管理逻辑计划 向 模块输出优化后的逻辑计划 2) execution 模块 职责 将逻辑计划转换为可执行计划(Execution Plan) 协调分布式任务的实际执行 交互 接收 模块的优化结果 依赖 模块分配计算资源 3) platform 模块 职责 提供 MPI / Ray 支持的分布式运行时环境 管理 3FS 存储系统等底层资源 交互 为 模块提供资源调度 与 模块协同处理数据分区 4) io 模块 职责 支持 Parquet 等格式的数据加载 / 输出 提供统一数据访问接口 交互 集成 模块的存储系统(3FS / DuckDB) 为其他模块屏蔽数据格式差异 如果用一张图把模块之间的依赖关系可视化,大概是这样: 层级交互规范 | 交互方向 | 接口形式 | 数据流向 | |: :|: :|: :| | Logical → Execution | 优化器(Optimizer) | 逻辑计划 → 执行计划 | | Execution → Platform | 执行计划 | 任务调度指令 | | Platform → IO | 数据流管道 | 读写操作请求 | 各层严格遵循"下层为上层提供服务"的原则,通过标准化接口降低耦合度。 Platform 层是整套架构里最"重"的一层 ——它同时承担着"连接计算框架(Ray / MPI)"和"连接存储系统(3FS)"的双重职责,可以说是整个 smallpond 的运行时枢纽。 简单画一下层次关系: ,上层只依赖下层暴露的接口,互不耦合。 1.2 端到端工作流示例 快速开始(Quick Start) 下面这段示例代码展示了从数据加载到分析输出的完整处理链路。如果你只是想快速验证 smallpond 的能力,跑通这三步基本就够了: 四个动作分别落到框架的不同位置: | 阶段 | 关键操作 | 技术要点 | |: :|: :|: :| | 数据加载 | | 原生支持 Parquet 列式存储 | | 分布式处理 | + | 按 ticker 字段哈希分区 | | SQL 转换 | | 支持标准 SQL 聚合函数 | | 结果输出 | + | 双输出模式(文件 / 内存) | 数据流依赖图 模块基础调用关系 1.3 两套 API smallpond 同时提供了 high level 和 low level 两套 API:前者用 DataFrame 链式调用,对老 Pandas / PySpark 用户友好;后者直接操作 Node 和 Plan,更接近编译器/执行器的内核视角。本篇以 high level 为切入走读它的源码。 示例 1 High level API 示例 2 Low level API 低级 API 的几个核心概念: Driver :对 JobManager 的封装,负责读取命令行参数并传给 JobManager Scheduler / Executor :底层 API,调度并执行 task Node :封装数据处理工作流的最小单位。一个典型的 workflow 写下来就是「创建全局 context → 创建数据集 → 创建数据源 Node → 串接执行引擎 → 组装成 LogicalPlan」 API 详细文档见 smallpond 官方仓库。 二、绕不开的基础:Parquet 是什么 读 smallpond 之前必须先聊一下 Parquet——因为它的 IO 层本质上是围绕 Parquet 设计的,整套数据流也都遵循 Parquet 的列式语义。 Parquet 是一种 列式存储文件格式 ,主要用于大数据处理和分析。它在 OLAP 场景下能跑这么快,靠的是下面四个核心特性。 1) 列式存储 在大数据系统里,宽表常常有几百列,但单次查询往往只涉及其中很少几列。列式存储让 Parquet 只读所需的列 ,I/O 直接砍掉一大截,查询自然快。 2) 高效压缩与编码 同一列的数据天然同质,压缩率非常好。Parquet 支持 Snappy、Gzip、LZO 等多种压缩算法,并叠加 RLE、bit packing、dictionary encoding 等编码方式。 举个直观的例子 :如果一列只存"男 / 女",整列就可以被压成一串单 bit 序列,存储成本几乎可以忽略。 3) Schema 演进 Parquet 通过允许添加 / 删除 / 修改列而不影响现有数据来支持 schema 演进。这对长生命周期的数据湖至关重要——你不需要每次加一个字段都重写历史。 4) 复杂数据类型 Parquet 支持嵌套和重复结构,以及数组、映射(map)、结构(struct)等丰富类型。对 JSON / Protobuf 这类层次化数据来说,可以高效地落进紧凑的二进制格式。 2.1 文件布局 理解 Parquet 的布局基本等价于理解它的性能模型,自上而下大致是这样: 一个完整的 Parquet 文件包含数据和元数据两部分: 数据按行被切分为一到多个 row group 每个 row group 里,每一列存为一个连续的 column chunk 每个 column chunk 进一步切分为多个 page ——page 是 Parquet 中的最小数据存储单元,每页带上自身的元数据、实际数据值、以及嵌套层级信息(rep / def levels) 元数据放在 文件末尾的 footer 里,这样写入时可以顺序追加、读取时只 seek 一次,主要内容包括: 文件版本 schema 每个 row group 中每个 column chunk 的位置 类型 / 编码方式 / 压缩方式 zone maps (page 粒度的统计指标:min / max / count) …… 为了进一步提升查询效率,Parquet 还支持额外的 Bloom Filter 结构。Bloom Filter 是一种空间效率很高的概率数据结构,能快速判断某个值"是否一定不存在"。Parquet 为每个 column chunk 维护一份 Bloom Filter,当查询的选取值很少(高选择性)时,系统先查 Bloom Filter 判断这个 column chunk 里到底有没有这个值;如果没有,整段直接跳过,连 page 都不用打开。Footer 里的 zone maps 也起类似的过滤作用。 一句话总结 Parquet 的性能模型 :通过 zone maps 和 Bloom Filter 在 footer 阶段就把不必要的 IO 砍掉,再用列式 + 列内编码进一步榨干每一字节的传输价值。这正是 smallpond 能直接吃到 DuckDB 性能红利的前提。 延伸阅读: 数据库内核杂谈(三十) 大数据时代的存储格式 Parquet 数据库内核杂谈(三十一) 大数据时代的存储格式 Parquet(2) Parquet 官方文档 CMU 15 721 02 Data Formats & Encoding I 三、smallpond 的运行时数据目录 跑起来一个 smallpond job 之后,它会在 下生成一个以 "时间戳.job id" 命名的目录,所有中间状态、日志、产出都落在这里: 这个目录结构对调试非常友好:当 job 跑挂了, 让你可以离线还原现场; 是自动生成的逻辑/物理执行图; 让你重启后能从 checkpoint 续跑而不是从零开始。这些都是大数据框架"易于运维"的基本功,smallpond 没有偷懒。 详细约定见官方仓库 internals.rst。 四、核心组件 | 组件 | 角色 | | | | | 查询引擎(DuckDB) | 列式 + 向量化执行,提供高效 SQL 查询能力 | | 存储适配层(3FS) | 集成 3FS,负责数据读写和缓存管理 | | 任务调度 | 轻量级调度器,支持并行处理和流水线优化 | | Ray | 分布式计算调度平台 | 4.1 代码目录结构 简单浏览一下 repo 的 layout,能帮你建立"哪段代码在哪一层"的直觉: 五、源码走读 我们沿着前面那段 high level API 一行一行往下追。 5.1 初始化 smallpond 的初始化做了三件事: 1. 初始化并连接 Ray 集群 找不到 Ray 集群时启动一个本地集群 如果通过环境变量 或 入参指定了集群地址,则直接连过去 2. 初始化 smallpond 的 data 与 log 路径 (也就是上面提到的 那套目录) 3. 拉起 dump 线程定时打印日志 细节见 。 5.2 设置输入源 函数定义: 这里 函数生成了链路上的第一个节点 —— 。它的入参是 ,基类是 ,同源还有 等其他派生类。 函数末尾把 传给 构造函数,返回一个 DataFrame。 这个类代表一个分布式数据集合,是整套 high level API 的核心 ——它本身不持有数据,只持有一个指向逻辑计划节点的引用。 5.3 设置数据分片方式 函数定义: smallpond 内置了三种分片方式: 它们的基类都是 。注意这里有一个 很关键的设计 :函数定义中把上一步生成的 节点作为入参( )传入了 的构造函数中,作为新 Node 的 。这相当于是在用"指针"把节点串成一条 DAG,而后续遍历执行计划时正是按照这条 DAG 拓扑序走下去的。 5.4 设置 SQL 执行语句 函数定义: 同样的套路:生成一个 节点并续接到执行计划尾部。注意 的注释里有个 重要的隐含约定 ——多表 join 时你必须保证 join key 在两侧都按相同方式 hash 分区,否则结果会出错。这是 smallpond"轻量"的代价:它把分区一致性的责任交给了用户,自己不做隐式 reshuffle。 5.5 执行计算 这一行才是真正"按下回车"的地方。前面所有的 、 、 都只是在拼 DAG,没有真正跑起来。 触发的代码逻辑路径: 可以看到,从 进入之后,整条链路是 Optimizer → Planner → Visitor → Ray 的标准编译器/执行器套路。Visitor 模式在这里特别合适——每种 Node 类型都有自己的 visit 方法,新增节点不会污染原有逻辑。 的核心代码: 这段代码不长,但承担的责任很多。我把它拆成三步来看: 1. 决定 producer 并行度 :取 和 的最小值,前者是 node 自身的硬上限、后者是根据 executor 数量和可用 CPU 算出的"理论并行度"。可以看出 smallpond 在这里做了一个非常实用的取舍—— 不让 producer 任务数无脑膨胀,避免小集群被打成"几千个 Ray task"的尴尬 。 2. 根据 input 数量与 producer 数量的关系,决定要 merge 还是 split :input 太少就先 merge 再 split;input 已经够多就直接按 producer 数量切。 3. 构造 producer consumer 二段管线 :producer 负责把上游数据按目标分区方式分发出去,consumer 负责按分区索引拉取属于自己的那一份。 整个 Visitor 跑完之后,逻辑计划就被翻译成了一个 Ray task 的 DAG,最后通过 异步发出去执行。 六、实测观察 跑了一些不同规模的数据之后,有两个值得记下来的小结论: 结论 1 · 实际并行度受多重因素制约 虽然你通过 接口指定了分区数和方式,但实际跑起来的并行度还会被「输入源数量 / CPU 核数 / 配置的最大并行参数」三者联合约束。另外,即使在计算过程中做了分片处理,Ray 调度时还是优先在本节点执行;只有当本节点压力比较大时,task 才会跨机调度。这一行为对中等规模的数据非常友好——避免了很多无意义的网络传输。 结论 2 · "文件级切片 + task 级并行"的混合粒度 smallpond 的任务切片粒度是按文件来的,但分布式调度的并行单位是 task。也就是说 它的并行性来自逻辑 task 的并行 ,而不是来自把单个文件再切碎。这意味着:当你有很多小文件时 smallpond 自然能跑满,但单个超大文件不会自动被拆——你得自己提前 split 或者用 触发 reshuffle。 七、使用场景 | 应用场景 | 核心需求 / 痛点 | Smallpond 的解决方案 / 优势 | |: :|: :|: :| | AI 数据预处理 | 海量训练数据(文本、图像等)的清洗、转换、加载 | 高效分布式处理,与 3FS / WFS 结合实现高吞吐数据加载 | | 大规模数据分析 | 快速分析 PB 级结构化 / 半结构化数据 | DuckDB 列式 + 向量化执行 + 并行计算 | | 高性能数据排序 | 对超大规模数据集(如 100 TB+)排序 | 在 GraySort 基准里跑出 3.66 TiB/min | | 金融数据分析 | 行情聚合、风险分析、高频交易处理 | 按 ticker 哈希分区,并行计算极值 | | 日志处理与分析 | 分布式系统日志的快速分析 | 并行处理服务器日志,近实时洞察 | 7.1 注意事项与局限性 smallpond 在上面这些场景里表现优异,但它 不是银弹 。下面几种情况下,它可能并不是最佳选择: 小规模数据(< 10 TB) :单机 DuckDB 或其它单机工具往往更简单、更快 复杂多表 JOIN :跨分区连接的优化能力相对有限,复杂多表场景下 Spark 更成熟 需要高并发点查 :smallpond 是为批量分析设计的,不适合高并发随机点查 部署复杂度 :要充分发挥它的性能(尤其是和 3FS / RDMA 网络结合时),需要相对特定的硬件和环境 八、写在最后 smallpond 给我的整体观感是:它 坚定地选了"轻量"这条路 。不发明新的 SQL 引擎、不发明新的存储、不发明新的调度器,而是把 DuckDB 在向量化执行上的成熟、3FS 在带宽上的极致、Ray 在分布式调度上的工程化 —— 三块拼到一起,做成一份只有几千行的 Python 框架。 这种风格对真实的工程团队启发很大: 架构师的重要工作之一不是"造轮子",而是"挑零件 + 划清边界" 。每一层只做自己最擅长的事,下层为上层提供服务,上层不越界干预下层——这样的系统才能跑得快、调得动、撑得久。 下一篇我会再上一层,聊聊把 3FS 和 smallpond 这套底座放到 AI 训练 Pipeline 里之后,整条数据链路是怎么设计的。 参考资料 smallpond GitHub smallpond API 文档 smallpond Internals 3FS 的零拷贝之路 · 本系列上一篇]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/smallpond-cover.webp" />
</item>
<item>
<title>3FS 的零拷贝之路:USRBIO 是怎么把存储吞吐推到 6.6 TiB/s 的</title>
<link>https://gitpull.cn/post/20260311/</link>
<guid isPermaLink="false">3fs-usrbio-zero-copy</guid>
<pubDate>Wed, 11 Mar 2026 00:00:00 GMT</pubDate>
<author>Jimmy</author>
<category>分布式存储</category><category>AI 基础设施</category><category>C++</category>
<description><![CDATA[DeepSeek 开源的 3FS 在 180 节点集群上实测聚合读取吞吐约 6.6 TiB/s。本文从 FUSE 的性能瓶颈讲起,拆解 3FS 如何通过共享内存、io_uring 风格的环形队列、以及 RDMA 直接传输三层叠加,把数据通路上的每一次拷贝都抠掉。]]></description>
<content:encoded><![CDATA[项目地址 :github.com/deepseek ai/3FS 调研日期 :2026 03 11 一、背景与动机 3FS(Fire Flyer File System)是 DeepSeek 开源的高性能分布式文件系统,专为 AI 训练和推理场景设计。它的存储层完全建立在现代 NVMe SSD 与 RDMA(InfiniBand / RoCE)网络之上,在 180 节点集群上实测聚合读取吞吐量达到约 6.6 TiB/s ——这个数字在过去几乎是科研论文里才会出现的,而 3FS 把它做到了开源仓库里、可复现的程度。 1.1 传统 FUSE 的性能瓶颈 3FS 同时提供了 FUSE 客户端(低使用门槛)和原生客户端(高性能)两种接入方式。FUSE 用着舒服——它让一个分布式文件系统看起来就是一个本地目录,应用不需要做任何改造。但当你想榨干现代硬件的性能时,FUSE 这一层很快就会变成新的瓶颈: | 问题 | 描述 | | | | | 内存拷贝开销 | FUSE 用户态守护进程无法直接访问应用内存,内核态↔用户态的数据要往返拷贝,消耗大量内存带宽 | | 多线程扩展性差 | I/O 请求队列被自旋锁保护,并发越高竞争越凶;FUSE 实测处理 4 KiB 小读只能跑到约 400K iops,再加并发也不再涨 | | 写并发限制 | Linux 5.x 上的 FUSE 不支持同一文件的并发写 | 简单说,FUSE 的设计初衷是「让用户态实现文件系统变得简单」, 但它从来不是为 RDMA + NVMe 这种硬件量级设计的 。所以 3FS 在 FUSE 守护进程内额外实现了一套 原生客户端(Native Client) ,对外暴露异步、零拷贝的 I/O 接口——它叫做 USRBIO(User space Ring Buffer I/O) 。 二、USRBIO 零拷贝接口设计 USRBIO 设计灵感来源于 Linux ,核心思想可以浓缩成一句话: 绕过内核的拷贝路径,通过共享内存 + RDMA 在用户进程地址空间和存储服务之间直接搬数据 。 2.1 核心数据结构 USRBIO 一共围绕三个数据结构展开,理解了这三个结构基本就理解了它的全部设计哲学。 2.1.1 (I/O Vector / 共享内存区域) 作用 : 本质上是 一块大共享内存区域 ,用作零拷贝读写的数据缓冲区。原生客户端会向 InfiniBand 注册这块内存(Memory Registration),让 RDMA 硬件可以直接访问它的物理页: 读操作 :数据从存储服务直接 RDMA Write 到此区域, 全程不经过内核 写操作 :应用先把数据写进这块缓冲区,再发起写请求;存储服务通过 RDMA Read 直接拉走 理解 RDMA 注册的意义对后面的设计取舍很重要——因为内存一旦被 pin 住,物理页就不能被换出,所以 是一个 昂贵的资源 ,应该长期持有、反复复用,而不是每次 I/O 都申请。 2.1.2 (I/O Ring / 环形请求队列) 作用 : 是用户进程与原生客户端之间的 共享环形通信缓冲区 ,跟 的 SQE/CQE 几乎是同款思路: 用户进程把 I/O 请求入队(enqueue) 原生客户端的 I/O Worker 线程从队列出队并处理 多个 实例可并行处理,刚好契合多线程场景 这个参数值得展开说一下,三种取值的语义并不直观: | 值 | 行为 | | | | | | 每次最多批处理 io depth 个请求(控制单批大小,适合训练样本批加载场景) | | | 尽快处理所有已准备好的请求 | | | 尽快处理,但每次不超过 个(防止某个超大批次拖慢延迟) | 负值这个语义乍看奇怪,其实是一种「 默认低延迟、必要时降级保护 」的设计:worker 平时尽快收割,但又不至于一次吃下太多请求让其它环饿死。 2.1.3 (Completion Queue Entry / 完成事件) 是个小但很关键的设计——它让用户在批量收割完成事件时,可以零成本地把 cqe 关联回业务层的 request 对象,省掉一张额外的索引表。 三、完整 API 流程 3.1 API 列表 | 函数 | 说明 | | | | | | 创建共享内存 IOV | | | 打开已有 IOV | | | 把已注册的共享内存包装为 IOV(跳过创建) | | | 销毁 IOV | | / | 创建 IOR(多个版本,参数逐步扩充) | | | 销毁 IOR | | | 注册文件描述符(必须在 prep 前调用) | | | 注销文件描述符 | | | 准备一个 I/O 请求(加入环) | | | 提交 hint(worker 可能已在处理中) | | | 等待完成事件,收割结果 | 3.2 典型读操作流程 注意 : 不是线程安全的 ,多线程场景下每个线程应该持有独立的 实例——这也是为什么前面说 天然适合"一线程一环"的写法。 3.3 高级特性标志( ) | 标志 | 值 | 说明 | | | | | | | 1 | 允许读取未提交(Pending)版本数据 | | | 2 | 禁止读取文件空洞 | 第一个标志很有意思——它放宽了一致性要求来换吞吐,对训练这种"几乎只读"的负载特别合适。第二个则相反,是给某些校验场景使用的额外严格性。 四、零拷贝的底层机制 4.1 RDMA 内存注册与直接传输 RDMA Read (写操作时):存储服务 主动 从客户端 内存区拉取数据,客户端 CPU 完全不需要参与传输 RDMA Write (读操作时):存储服务把数据 直接写 到客户端的 内存区,等客户端读到数据时同样不需要再做一次拷贝 注意这里的"主动"——RDMA 的关键不是"快",而是 网卡可以在不打扰对端 CPU 的情况下访问对端内存 。所以两端 CPU 都解放了出来,只剩下网卡和 DMA 在跑。 4.2 与 io uring 的对比 | 特性 | Linux io uring | 3FS USRBIO | | | | | | 异步 I/O | ✅ | ✅ | | 零拷贝 | 部分(需配合 fixed buffers) | ✅ 全程零拷贝 | | 用户态轮询 | ✅ SQ/CQ Ring | ✅ IOR Ring | | 网络传输层 | 内核 TCP / 本地 | RDMA(InfiniBand / RoCE) | | 批处理控制 | via SQE | 参数 | | 内存注册 | Fixed Buffer Registration | RDMA Memory Registration | | 适用场景 | 本地文件 / 网络 I/O | 分布式存储大吞吐 | 可以这么理解: USRBIO 是把 io uring 的设计哲学延伸到了 RDMA 网络上 。io uring 解决的是"用户态怎么和内核高效交换 I/O",USRBIO 进一步解决的是"用户态怎么和远程存储服务高效交换 I/O"。两者并不冲突,思路一脉相承。 4.3 内存区域的生命周期 这个能力对深度学习框架特别友好——PyTorch 训练时已经分配了大量 pinned memory 作为 dataloader buffer,USRBIO 可以直接复用它们而不必再开一份。这种"我不需要你额外给我内存,把现成的标记给 RDMA 注册一下就行"的接口,远比"先 alloc 再 copy"优雅。 五、C++ 高级客户端接口(hf3fs.h) 除了 C 风格的 USRBIO 接口,3FS 还提供了完整的 C++ 原生客户端 ,更适合大型应用做封装。 5.1 零拷贝 I/O 接口 / 接受 数组(对应多个文件段 ),一次调用就能批量处理多个文件 必须用 分配的内存才能享受零拷贝;普通 会走非零拷贝路径 5.2 跨节点 IOV 共享 这个特性其实是个"小而美"的设计:分布式训练里不同节点共享同一块 IOV 的元数据,可以进一步省掉重复分配和注册——当训练规模上到几百卡之后,这种碎边际优化加起来就是肉眼可见的成本节约。 六、实际场景中的应用 USRBIO 能拿到这么夸张的数字,靠的是 针对性的场景定制 。下面三类是 3FS 设计文档里反复提到、也是它实测最快的几种工作负载。 6.1 AI 训练数据加载(Dataloader) 训练负载的特点是 大量随机小读 (几 KiB 几 MiB),这是 FUSE 路径最难受的工作模式——锁竞争和拷贝开销都呈倍数放大。USRBIO 通过批处理 + RDMA 让 dataloader 直接从 SSD 读到与 GPU pinned memory 共用的缓冲区,整条链路只剩下 NVMe + 网卡 + GPU 三段 DMA,CPU 几乎不参与。 6.2 KVCache 推理场景 3FS 在 KVCache 场景下实测 单节点峰值读吞吐达到 40 GiB/s (1×400 Gbps NIC,已经接近网卡线速)。推理服务通过 USRBIO 批量拉取 KV 缓存块,直接落进 GPU 可见的内存区域,避免传统方案"SSD → CPU DRAM → GPU DRAM"的中转。这一点对大模型推理意义非常大:KV 缓存复用的时延越小,长上下文推理的端到端体验越好。 6.3 并行 Checkpoint 写入 大规模训练定期写 checkpoint 是另一个老大难。3FS 让多个线程各自持有独立 ,并行写入不同文件块,配合 RDMA Read 的写路径,把 checkpoint 写入这件"过去阻塞训练步几十秒"的事压缩到了「几乎不影响训练步」的水平。 七、关键设计取舍与限制 3FS 不是银弹,它有非常明确的"以何种代价换性能"。这部分清单很重要,决定了你是否能直接把它套到自己的系统上: | 项目 | 说明 | | | | | fd 注册后不可关闭 | 注册的 fd 关闭后旧 inode 仍被使用;同整数值新 fd 注册会返回 EINVAL | | prep io 非线程安全 | 同一 ior 不可多线程并发 prep io;建议每个线程独立 ior | | 元数据仍走 FUSE | open / close / stat 等元数据操作走 POSIX 路径以保持兼容性,只有数据面才走零拷贝 | | NUMA 亲和性 | iov 与 ior 都可以指定 NUMA 节点,能显著降低跨 NUMA 内存访问延迟 | | 写操作不支持同文件并发 | 继承自 FUSE 的限制,多线程写得分散到不同文件 | 可以看到,3FS 在「 保证元数据兼容、把数据面榨干 」这件事上做得很彻底——它没有去重新造一套元数据协议,而是把精力集中在 hot path 上。这种取舍非常理性。 八、总结 3FS USRBIO 的零拷贝实现核心是三层机制的叠加: 1. 共享内存(hf3fs iov) :在用户进程地址空间分配 RDMA 注册内存,消除内核↔用户态的拷贝 2. 环形队列(hf3fs ior) :借鉴 io uring 的异步批处理模式,减少 RPC 次数和同步开销 3. RDMA 直接传输 :存储服务直接通过 InfiniBand 向客户端内存写入 / 读取数据,CPU 不参与数据搬运 这三层叠加之后,3FS 能够把 NVMe SSD 和 RDMA 网络的硬件性能利用到接近上限,从而在大规模 AI 负载下达到 TiB/s 量级的存储带宽。 更值得关注的是, 3FS 没有发明任何新的底层技术 ——共享内存、io uring、RDMA 都是行业里成熟的零件。它真正的工程价值在于" 把这些零件按一个非常清醒的目标拼起来 ":所有设计取舍都围绕一句话—— 让 NVMe 和 IB 网卡尽可能直接对话,CPU 只做必要的指挥 。这种"克制而坚决"的工程审美,是值得任何做存储 / 系统软件的同行细品的。 参考资料 3FS GitHub 仓库 Design Notes USRBIO API 参考(UsrbIo.md) hf3fs usrbio.h(C API 头文件) hf3fs.h(C++ 原生客户端接口)]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/3fs-usrbio-cover.webp" />
</item>
<item>
<title>多维数组的索引方案与内存设计</title>
<link>https://gitpull.cn/post/20220313/</link>
<guid isPermaLink="false">多维数组的索引方案与内存设计-16000038</guid>
<pubDate>Sun, 13 Mar 2022 04:08:00 GMT</pubDate>
<author>兰州小红鸡</author>
<description><![CDATA[多维数组的内存结构 类似ndarray之类的多维数组,实际上是一个连续的内存地址+一种索引方案。我们可以将一个多维数组对象分为两部分,一部分是实际存储数组数据的连续内存段,另一部分…]]></description>
<content:encoded><![CDATA[多维数组的内存结构 类似ndarray之类的多维数组,实际上是一个连续的内存地址+一种索引方案。我们可以将一个多维数组对象分为两部分,一部分是实际存储数组数据的连续内存段,另一部分是存储数组的形状、索引方案、单元素步长、size等信息的header段。 如shape属性指定各维度的元素的数量,dtype属性指定元素类型及其解释方式,strides属性指定各维度的跨度,itemsize属性指定单个元素占用字节数。 | | | | | | | | | | | | | | | dtype | itemsize | shape | strides | datasize | ... | 索引的解析 对于N维的数组,我们将将各维度的跨度存放在 列表中,比如 表示的是数组第1维的步长。第k维的步长为 其中我们假设单元素步长为1。假设数组的首地址为 ,那么其坐标为(i0,i1,i2,...,in 1)的元素地址为 实例1 一个2x3的二维数组,它的第一维的步长是3,第二维也就是最后一维的步长是1,所以它的 列表为 实例2 一个2x3x4的三维数组,它的第一维的步长是3x4=12,第二维步长是4,第三维也就是最后一维的步长是1,所以它的 列表为 实例3 对于上面这个4x4的数组,其形状shape=(4,4),跨度列表strides = \[4,1\]。 如果我们以 的形式去访问数组元素9,那么索引的解析方案为: 所以我们假设数组b的数据实际存储在连续的一维内存段 中,那么最终 访问的应该是]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/多维数组的索引方案与内存设计-16000038-01.webp" />
</item>
<item>
<title>推荐系统——推荐引擎的架构</title>
<link>https://gitpull.cn/post/20220312/</link>
<guid isPermaLink="false">推荐系统——推荐引擎的架构-15996742</guid>
<pubDate>Sat, 12 Mar 2022 03:31:00 GMT</pubDate>
<author>兰州小红鸡</author>
<description><![CDATA[什么是推荐系统 谈推荐系统之前我们先聊一下搜索系统,推荐系统和搜索系统都是为了解决互联网信息过载的问题,互联网信息的数量远超过人类可以逐一浏览的程度,而搜索引擎便是解决这一问题的代…]]></description>
<content:encoded><![CDATA[什么是推荐系统 谈推荐系统之前我们先聊一下搜索系统,推荐系统和搜索系统都是为了解决互联网信息过载的问题,互联网信息的数量远超过人类可以逐一浏览的程度,而搜索引擎便是解决这一问题的代表。用户通过描述他们需要的信息,带有目的性地输入一个能够准确描述问题的词条,索引引擎在毫秒级返回最相关的几十条数据。而用户对检索结果的满意程度成为了评估搜索效果的关键。 但搜索引擎只能解决用户拥有明确需求场景的信息过载问题,当用户不明确自己的需求但是又想获取某方向的需求时应该怎么办?如何在海量的信息中找到用户感兴趣的内容并推荐给用户?这便是推荐系统要解决的问题。 举个例子:俄乌战争爆发的几天后,车臣部队发了一段出征誓师的视频,当夜微博b站平台车臣的搜索量上涨,同时在相关视频的推荐下面,相关的俄罗斯与车臣战争的科普视频播放量也暴增,普京个人科普视频播放量也随之上涨。 大部分用户在搜索相关视频的时候,只想关注与“车臣”有关的视频,但是他们并不清楚与车臣有关的视频有哪些,他们不会去搜第一次车臣战争或者第二次车臣战争,因为在这之前他们并不清楚这些东西。所以这就是推荐系统要做的事,感知哪些信息是用户想获取的。 系统架构 在服务层,下面的推荐引擎由于推荐模型的不同,可能会存在多个推荐引擎。服务层对多个推荐引擎进行召回得到初始的推荐结果,并经过过滤排名等操作将最后结果推荐给用户。 推荐引擎的架构 推荐引擎架构主要包括3部分: 1. 生成用户特征向量 该部分负责从数据库或者缓存中拿到用户行为数据,通过分析不同行为,生成当前用户的特征向量。 2. 特征—物品相关推荐 该部分负责将用户的特征向量通过特征 物品相关矩阵转化为初始推荐物品列表。 3. 过滤 排名模块 该部分负责对初始的推荐列表进行过滤、排名等处理,从而生成最终的推荐结果 在线召回 通过对接上游feed服务的流量请求,返回用户会最感兴趣的N条以内的推荐列表(ID列表)。 如何在海量的内容中找到用户最感兴趣的几条数据的呢?这就是在线层所解决的问题。业界通常将这个过程分为四个步骤:召回,粗排,精排,调整。 召回 负责从亿量级的内容中筛选出千条数据返回。这一步骤的目标就是能够在保证低延迟的情况下使用一些召回策略快速的选择出内容的候选集。基于机器学习的模型离线计算好用户的特征与物品之间的映射存储为倒排索引,存储在索引服务中(离线召回的一种方式)等,召回策略决定了一次推荐效果的上限。 精排 这一步将为输入的推荐列表进行预估, 所谓预估,就是对用户浏览这条信息后是否会提升期望的某种指标进行预测。业界常见的方法就是使用点击预估, 精排模型使用预估模型对输入的推荐列表计算预估分并按预估分排序,推荐引擎会最终在上百条的推荐内容中选择预估分排在前几十的内容返回。 粗排 在现实的工程系统中, 精度和时间往往不能兼得,精排性能往往令人堪忧,因此在精排之前都会选择一个折中的步骤过滤掉一个量级的候选内容,保证整体的响应时间是可控的。所以粗排会在保证推荐结果的情况下尽可能地排除掉一些内容。 至此,推荐引擎返回了零到十几条不等的推荐内容给feed服务,feed服务可能会混排信息流广告或者从其他推荐系统返回的不同内容进行混排,最终打包内容与用户信息返回给客户端。 离线服务 离线服务通常用来处理不需要给用户及时响应并且计算量大耗时久的事情,比如 离线的召回模型计算物品之间的相似度写入索引服务 离线的模型训练 离线的同步数据到HDFS 用于特征工程生成训练集与画像的构建 数据总线中的消息 爬虫爬取的内容信息]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/推荐系统——推荐引擎的架构-15996742-02.webp" />
</item>
<item>
<title>nebula graph 图计算数据库</title>
<link>https://gitpull.cn/post/20220311/</link>
<guid isPermaLink="false">置顶-nebula-graph-图计算数据库-15993772</guid>
<pubDate>Fri, 11 Mar 2022 07:03:00 GMT</pubDate>
<author>兰州小红鸡</author>
<description><![CDATA[更新历史 在学习过程中,本文持续更新 2021 12 13:更新nebula官方介绍 2021 12 14:更新编译与部署方式,总结importer导入方式 2021 12 15:…]]></description>
<content:encoded><![CDATA[目录 更新历史 什么是nebula graph 举个例子 服务架构 graph 服务 Meta服务 META 服务架构 Storage 服务 Raft 协议 raft故障流程 nebula的数据模型 编译部署 使用docker编译 在线编译 生产环境配置要求 运行部署 安装准备 手动部署 使用nebula 客户端连接 console 和 web端 客户端sdk 常用命令 常用的查询与匹配命令 MATCH匹配 nebula importer 批量导入 点配置 边配置 使用Exchange导入 nebula集群 nebula 进阶学习 更新历史 在学习过程中,本文持续更新 2021 12 13:更新nebula官方介绍 2021 12 14:更新编译与部署方式,总结importer导入方式 2021 12 15:更新使用用例,常见命令等 2021 12 16:更新nebula集群部署方式,更新Match语法使用总结 2021 12 17:添加raft协议示例 2021 12 22:添加运行环境配置要求 2022 01 10:添加nebula进阶学习 什么是nebula graph 官方:Nebula Graph 是一款开源的、分布式的、易扩展的原生图数据库,能够承载数千亿个点和数万亿条边的超大规模数据集,并且提供毫秒级查询。 什么是图数据库? 图数据库是图数据库管理系统的简称,使用图形化的模型进行查询的数据库,通过节点、边和属性等方式来表示和存储数据,支持增删改查等操作,提供在线事务处理能力。与图数据库对应的是图计算引擎,提供基于图的大数据分析能力。 Nebula Graph 作为一个典型的图数据库,可以将丰富的关系通过边及其类型和属性自然地呈现。 简单来说,传统的关系型数据库,如果要查询不同实体之间的关系,比如一层或者多层的关系时候,需要多次的KV查询才能得到结果,而图数据库可以直接从点出发一次查询与该点的深层次的关系的其他实体。 图数据库和关系型数据库的区别具体介绍可以看:https://zhuanlan.zhihu.com/p/50171330 举个例子 在推荐召回中的swingi2i召回模型中,使用传统的kv查询我们需要 1. 从用户id去查询一次用户行为历史 2. 再由每个用户行为历史去查询对应的相似item 一共两次数据库查询,并且第二次查询为多key的mget。 但使用图数据库,我们可以使用一次查询便得到结果。 甚至更直观的我们还可以看到返回的simItem里面高度重合的部分 服务架构 Nebula Graph 由三种服务构成:Graph 服务、Meta 服务和 Storage 服务,是一种存储与计算分离的架构。 graph 服务 Graph 服务主要负责处理查询请求,包括解析查询语句、校验语句、生成执行计划以及按照执行计划执行四个大步骤。 1. Parser:词法语法解析模块。 2. Validator:语义校验模块。 3. Planner:执行计划与优化器模块。 4. Executor:执行引擎模块。 关于四个步骤的具体事务可以看官方介绍:https://docs.nebula graph.com.cn/2.6.1/1.introduction/3.nebula graph architecture/3.graph service/ Meta服务 META 服务架构 在集群模式下,所有 nebula metad 进程构成了基于 Raft 协议的集群,其中一个进程是 leader,其他进程都是 follower。leader 是由多数派选举出来,只有 leader 能够对客户端或其他组件提供服务,其他 follower 作为候补,如果 leader 出现故障,会在所有 follower 中选举出新的 leader。 1. 提供账号管理功能 Meta 服务中存储了用户的账号和权限信息,当客户端通过账号发送请求给 Meta 服务,Meta 服务会检查账号信息,以及该账号是否有对应的请求权限。 2. 管理分片 Meta 服务负责存储和管理分片的位置信息,并且保证分片的负载均衡。 3. 管理图空间 Nebula Graph 支持多个图空间,不同图空间内的数据是安全隔离的。Meta 服务存储所有图空间的元数据(非完整数据),并跟踪数据的变更,例如增加或删除图空间。 4. 管理schema信息 Nebula Graph 是强类型图数据库,它的 Schema 包括 Tag、Edge type、Tag 属性和 Edge type 属性。Meta 服务中存储了 Schema 信息,同时还负责 Schema 的添加、修改和删除,并记录它们的版本。 5. 管理数据生命周期 TTL Meta 服务存储 TTL(Time To Live)定义信息,可以用于设置数据生命周期。数据过期后,会由 Storage 服务进行处理。 6. 管理作业 Meta 服务中的作业管理模块负责作业的创建、排队、查询和删除。 Storage 服务 和Meta一样,Storage也是用Raft协议作集群。 storage的三个服务层次 1. Storage interface 层 Storage 服务的最上层,定义了一系列和图相关的 API。API 请求会在这一层被翻译成一组针对分片的 KV 操作。正是这一层的存在,使得 Storage 服务变成了真正的图存储,否则 Storage 服务只是一个 KV 存储服务。 2. Consensus 层 Storage 服务的中间层,实现了 Multi Group Raft,用于storage集群。 3. Store Engine 层 Storage 服务的最底层,是一个单机版本地存储引擎,提供对本地数据的get、put、scan等操作。 Raft 协议 Raft 就是一种用于保证多副本一致性的协议。Raft 采用多个副本之间竞选的方式,赢得”超过半数”副本投票的(候选)副本成为 Leader,由 Leader 代表所有副本对外提供服务;其他 Follower 作为备份。 当该 Leader 出现异常后(通信故障、运维命令等),其余 Follower 进行新一轮选举,投票出一个新的 Leader。Leader 和 Follower 之间通过心跳的方式相互探测是否存活,并以 Raft wal 的方式写入硬盘,超过多个心跳仍无响应的副本会认为发生故障。 raft故障流程 假设3个机器,3个partition, 3个副本(A, B, C),L标识为leader副本 | 机器1 | 机器2 | 机器3 | | | | | | A1(L) | A2(L) | A3(L) | | B2 | B3 | B1 | | C3 | C1 | C2 | | 假设A为Leader, 当机器1故障时,系统剩下的分区为 | | | | 机器1 | 机器2 | 机器3 | | | | | | A1 | A2(L) | A3(L) | | B2 | B3 | B1 | | C3 | C1 | C2 | 可以看到分区1的leader故障,需要重新选出分区1的leader,假设经过选举,选出了B1作为分区1的leader。 | 机器1 | 机器2 | 机器3 | | | | | | A1 | A2(L) | A3(L) | | B2 | B3 | B1(L) | | C3 | C1 | C2 | nebula的数据模型 图空间 space 图空间用于隔离不同团队或者项目的数据。不同图空间的数据是相互隔离的,可以指定不同的存储副本数、权限、分片等。 标签 Tag Tag 由一组事先预定义的属性构成,用于定义点的类型。 边类型 Edge type Edge type 由一组事先预定义的属性构成,用于定义边的类型。 属性 Properties 属性是指以键值对(Key value pair)形式存储的信息。 点 Vertex 点用来保存实体对象,点是用点标识符(VID)标识的。VID在同一图空间中唯一。VID 是一个 int64,或者 fixed\ string(N)。 点必须有至少一个 Tag,也可以有多个 Tag。但不能没有 Tag。 nebula 常用命令可以见:https://docs.nebula graph.com.cn/2.6.1/2.quick start/4.nebula graph crud/ 编译部署 如果使用官方编译的包部署,可以跳过编译部分,下面讲如何在自己环境编译nebula包。 使用docker编译 1. 本地安装好 Docker 2. 将 vesoft/nebula dev 镜像 pull 到本地 3. 运行 Docker 并挂载 Nebula 源码目录到容器的 /home/nebula 目录 4. 进到/home/nebula路径下进行编译 在线编译 不推荐,依赖较多,比较难解决 生产环境配置要求 生产环境部署方式 3 个元数据服务进程 metad 至少 3 个存储服务进程 storaged 至少 3 个查询引擎服务进程 graphd 以上进程都无需独占机器。例如一个由 5 台机器组成的集群:A、B、C、D、E,可以如下部署: A:metad, storaged, graphd B:metad, storaged, graphd C:metad, storaged, graphd D:storaged, graphd E:storaged, graphd 同一个集群不要跨机房部署。 metad 每个进程都会创建一份元数据的存储副本,因此通常只需 3 个进程。storaged 进程数量不影响图空间数据的副本数量。 服务器配置要求(标准配置) 以 AWS EC2 c5d.12xlarge 为例: 处理器:48 core 内存:96 GB 存储:2 \ 900 GB, NVMe SSD Linux 内核:3.9 或更高版本,通过命令 uname r 查看 glibc:2.12 或更高版本,通过命令 ldd version 查看 资源估算 存储空间(全集群):点和边数量 \ 平均属性的字节数 \ 6 内存(全集群):点边数量 \ 15 字节 + RocksDB 实例数量 \ (write\ buffer\ size \ max\ write\ buffer\ number + rocksdb\ block\ cache), 其中 etc/nebula storaged.conf 文件中 data\ path 项中的每个目录对应一个 RocksDB 实例 图空间 partition 数量:全集群硬盘数量 \ (2 至 10 —— 硬盘越好该值越大) 内存和硬盘另预留 20% buffer。 rocksdb\ block\ cache官方建议 1/3 内存 关于机械硬盘和千兆网络 Nebula Graph 设计时主要针对的硬件设备是 NVMe SSD 和万兆网。没有对于机械磁盘和千兆网络做过适配,以下是一些需调整的参数: etc/nebula storage.conf: \ raft\ rpc\ timeout\ ms= 5000 至 10000 \ rocksdb\ batch\ size= 4096 至 16384 \ heartbeat\ interval\ secs = 30 至 60 \ raft\ heartbeat\ interval\ secs = 30 至 60 etc/nebula meta.conf: \ heartbeat\ interval\ secs 与 etc/nebula storage.conf 该项相同 Spark Writer: go importer: batchSize: 10 至 50 concurrency: 1 至 10 channelBufferSize:100 至 500 创建图空间时partition 值为全集群硬盘数量 2 倍 注意:上面是针对机械硬盘和千兆网络的优化,如果有SSD和万兆网就不必设置 运行部署 安装准备 若使用官方rpm包,直接rpm安装即可。 若使用在线编译,进到install目录下执行启动 手动部署 1. 更改配置文件,将etc目录下的nebula xxxx conf.default改名或者copy为nebula xxxx conf。 2. 查看端口是否有被占用: nebula三个服务的默认端口:9559、9669、9779; 对应的三个http端口:19559、19669、19779; 三个http2的端口:19560、19670、19780 启动前要查看这9个端口有没有被占用。如果要修改端口的话在对应的conf文件里修改。 3. 运行 4. 查看是否启动成功 三个端口都正常说明启动成功,有某一个端口不正常,应该排查端口是否被占用,可以logs目录下的日志查看报错。 5. 停止服务 stop停止服务 虽然服务停止,但是进程并不会退出,端口的占用也不会释放。如果要在运行环境重新部署,请使用kill原服务, kill 完全停止服务 使用nebula 客户端连接 console 和 web端 可以连接nebula的客户端有终端nebula console,studio的web界面。两个工具可以在官网下载。https://docs.nebula graph.com.cn/2.6.1/2.quick start/3.connect to nebula graph/ nebula studio需要 v10.12.0 以上的 Node.js。 使用一键部署的,在部署的时候已经装好nebula studio,直接访问对应的ip端口即可。 启动成功后,在浏览器地址栏输入 http://ip address:7001。 如果在浏览器窗口中能看到以下登录界面,表示已经成功部署并启动 Studio。 客户端sdk 1. Nebula CPP : https://github.com/vesoft inc/nebula cpp 2. Nebula Java:https://github.com/vesoft inc/nebula java/tree/v2.6.1 3. Nebula Python:https://github.com/vesoft inc/nebula python 4. Nebula Go:https://github.com/vesoft inc/nebula go/tree/v2.6.0 常用命令 1. 创建图空间 注意: partition\ num:指定一个副本中的分区数。通常为全集群硬盘数量的 5 倍。 replica\ factor:指定集群中副本的数量,通常生产环境为 3,测试环境可以为 1。由于采用多数表决原理,因此需为奇数。 你可以通过 SHOW HOSTS 命令检查机器和 partition 分布情况: 2. 执行命令SHOW HOSTS检查分片的分布情况 3. 选择空间 USE SPACE 4. 创建 Tag 和 Edge type 示例: 5. 插入点 INSERT VERTEX TAG名称 Value VID:(属性列表),示例 6. 插入边 INSERT EDGE EDGE类型名 Values src点Vid dts点Vid : (属性列表) 示例 7. 创建索引 示例 需要注意的是,索引创建后再插入的数据,并不在索引之中,数据更新后需要手动重建索引 重建索引 常用的查询与匹配命令 MATCH匹配 MATCH的语法概括如下: MATCH用于寻找与规则匹配的点和边,MATCH语句使用原生索引查找起始点或边,起始点或边可以在模式的任何位置。即一个有效的MATCH语句, 必须有一个属性、Tag 或 Edge type 已经创建索引,或者在WHERE子句中用 id() 函数指定了特定点的 VID 1. 匹配整个TAG的点 用户可以在点的右侧用 表示模式中的 。 2. 匹配符合属性的点 用户可以在 的右侧用 表示模式中点的属性。 同样可以使用 语句来实现 3. 匹配指定ID的多个点 可以使用点 ID 去匹配点。 函数可以检索点的 ID。如果要匹配多个点,可以用IN指定ID列表 4. 匹配有边连接的两个点 可以使用 符号表示两个方向的边,并匹配这些边连接的点。 可以在 符号上增加 或 符号指定边的方向。 表示出边 表示入边 5. 匹配更深层次的连接关系 比如QQ上面我们需要去匹配某个人的好友的好友,也就是共同好友的关系 这个层次还可以继续往下查询得更深,比如有个观点大概就是,这个社交网络中任意两人之间的关系间隔不超过五个人,于是我们便可以通过图数据库去查询和自己有5层连接深度关系的所有点,看看是否能够覆盖整个网络。而这个种操作在传统的关系型数据库使用kv查询是很难实现的。 6. 匹配路径 点与点之间的连接构成一条路径,查询时可以使用自定义变量名来命名路径 7. 匹配边 查询时可以在方括号中使用自定义变量命名边。例如 。 8. 匹配EDGE TYPE 和点一样,用户可以用: 表示模式中的 Edge type,例如 。 9. 匹配多个EDGE TYPE 使用 可以匹配多个 Edge type,例如 。第一个 Edge type 前的英文冒号 不可省略,后续 Edge type 前的英文冒号可以省略,例如 。 10. 匹配多条边 11. 匹配定长路径的边 可以在模式中使用 匹配定长路径。 必须是一个非负整数。 12. 匹配边长路径 用户可以在模式中使用 匹配变长路径。 更多命令见官方文档:https://docs.nebula graph.com.cn/2.6.1/2.quick start/6.cheatsheet for ngql command/ nebula importer 批量导入 Importer 可以读取本地的 CSV 文件,然后导入数据至 Nebula Graph 图数据库中。在我的仓库离线编译的包里面已自带nebula importer,也可以使用官方最新包。使用方法: 配置文件示例 点配置 边配置 使用Exchange导入 Exchange可以将不同数据源的数据导入到nebula数据库,便于迁移数据。 支持的数据列表: 详情:https://docs.nebula graph.com.cn/2.6.1/nebula exchange/use exchange/ex ug import from csv/ nebula集群 现在我们用2台机器举例做部署方案(这里只是举例,在实际的生产环境中建议5个节点以上的集群,才能发挥集群的可靠性优势) 步骤1:将nebula的安装解压到两台机器上 步骤2:修改每个服务器上的Nebula Graph配置文件。 Nebula Graph的所有配置文件均位于安装目录的etc目录内,包括nebula graphd.conf、nebula metad.conf和nebula storaged.conf,用户可以只修改所需服务的配置文件。 机器A配置 nebula graphd.conf nebula storaged.conf 机器B配置 nebula graphd.conf nebula storaged.conf nebula metad.conf 其实从上面可以看出,相比单机部署,集群部署的配置文件的最大不同点在于三个配置文件的 需要填各个节点的ip端口 踩坑注意:配置文件里默认的local ip为127.0.0.1,需要改成机器实际的ip地址,否则部署集群的时候会有一些奇怪的错误 步骤3:启动,启动流程如下 1. 先启动两台机器的meta服务。 2. 保证meta服务正常起来后,在分别启动两台机器的graph和storage服务 nebula 进阶学习 1. nebula graphd服务代码走读: https://www.cnblogs.com/gitpull/p/15988828.html]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/置顶-nebula-graph-图计算数据库-15993772-02.webp" />
</item>
<item>
<title>nebula代码走读——从Validator到Executor</title>
<link>https://gitpull.cn/post/20220310/</link>
<guid isPermaLink="false">nebula代码走读——从Validator到Executor-15988828</guid>
<pubDate>Thu, 10 Mar 2022 03:41:00 GMT</pubDate>
<author>兰州小红鸡</author>
<category>图数据库</category>
<description><![CDATA[整体架构 Nebula Graph Query Engine 主要分为四个模块,分别是 Parser、Validator、Optimizer 和 Executor。 Parser …]]></description>
<content:encoded><![CDATA[整体架构 Nebula Graph Query Engine 主要分为四个模块,分别是 Parser、Validator、Optimizer 和 Executor。 Parser 完成对语句的词法语法解析并生成抽象语法树(AST),Validator 会将 AST 转化为执行计划,Optimizer 对执行计划进行优化,而 Executor 负责实际数据的计算。 Graphd服务命令执行计划 Storaged命令执行计划]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/nebula代码走读——从Validator到Executor-15988828-01.webp" />
</item>
<item>
<title>推荐召回-向量召回之U2I学习总结</title>
<link>https://gitpull.cn/post/20220309/</link>
<guid isPermaLink="false">推荐召回-向量召回之U2I学习总结-15986952</guid>
<pubDate>Wed, 09 Mar 2022 12:42:00 GMT</pubDate>
<author>兰州小红鸡</author>
<category>推荐</category><category>推荐算法</category>
<description><![CDATA[常见召回模型 I2I:计算item item相似度,用于相似推荐、相关推荐; U2I:基于矩阵分解,通过用户特征直接推荐item; U2U2I:基于用户的协同过滤,先找相似用户,再…]]></description>
<content:encoded><![CDATA[常见召回模型 I2I:计算item item相似度,用于相似推荐、相关推荐; U2I:基于矩阵分解,通过用户特征直接推荐item; U2U2I:基于用户的协同过滤,先找相似用户,再推荐相似用户喜欢的item; U2I2I:基于物品的协同过滤,先统计用户喜爱的物品,再推荐他喜欢的item; U2TAG2I:基于标签偏好推荐,先统计用户偏好的tag,然后匹配所有的item;其中tag一般是item的标签、分类、关键词等。 其中U表示user,2表示走向(to),I表示Item,TAG表示标签(tag)。在深度神经网络出现之前,传统的多路召回方法主要有:内容偏好召回、协同过滤、热门召回和策略召回等,下面进行详细的介绍。 u2i召回框架 常见的u2i召回模型 1. 离线模型:通过用户行为日志和对应的item,离线训练模型,并得到用于表示用户特征的user embedding模型,和用于表示item特征的item embedding模型。 2. 在线模型:通过用户线上的实时行为,构造实时特征,再通过User embedding模型提取用户当前embedding特征最后进行检索,找到用户最感兴趣的topN 的item。 比较经典的u2i召回模型主要有两种:Youtube召回;由DSSM发展而来的双塔召回。 双塔模型 双塔模型DSSM全称是Deep Structured Semantic Model,最早是微软提出解决搜索问题。双塔模型上线很方便,user塔在线计算user embedding,item塔离线计算item embeding,通过向量检索就可以快速进行召回。 DSSM模型的原理相对简单:通过Q(Query)和D(Document)的曝光、点击日志,采用DNN把Q和D表示为低维向量(如上图中128维的和),并通过余弦相识度来计算向量距离,最终训练出语义相似度模型。 将DSSM中的Q(Query)和D(Document)分别替换成推荐系统中的User和Item,就构成了经典的双塔模型。 双塔模型的线上工程流程 1. 从用户历史行为获得的item list。 2. 从item embedding中获取各个item 对应的向量。通过指定的计算(比如相加,求平均,时间衰减)得到相同embeding size的user embedding。 3. 用user embedding计算item embedding中各个item向量与用户向量的余弦值。 4. 返回相似度top k 的item 余弦值求相似度]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/推荐召回-向量召回之U2I学习总结-15986952.webp" />
</item>
<item>
<title>随笔杂谈</title>
<link>https://gitpull.cn/post/20200101/</link>
<guid isPermaLink="false">随笔杂谈-15995658</guid>
<pubDate>Tue, 31 Dec 2019 16:00:00 GMT</pubDate>
<author>兰州小红鸡</author>
<category>杂七杂八</category>
<description><![CDATA[拍完毕业照,从图书馆走回宿舍的路上,学校的林荫小道微风习习,好惬意,夏天的风从树下拂过脑袋,走路都轻飘飘的。这种舒服而又宁静的感觉,像是十年前在那个小岛上,小学毕业的我,走在回家的…]]></description>
<content:encoded><![CDATA[拍完毕业照,从图书馆走回宿舍的路上,学校的林荫小道微风习习,好惬意,夏天的风从树下拂过脑袋,走路都轻飘飘的。这种舒服而又宁静的感觉,像是十年前在那个小岛上,小学毕业的我,走在回家的田间小路上,泥沼小路鸟语花香,让人心静。 从南风岛的小学毕业,老师说我们十年后在这里聚一次,我迷茫却又安静的内心在想,十年?十年好久呀。我收拾起书包独自一人回家,同学们把欢笑声杂糅着夏天的味道抛在六月的空气中,隐约能闻到田埂旁野生桂花的清香。 如此的安静甜美的画面,时间仿佛停滞一般,十年像是数千光年外的恒星一般遥远。 在南风岛生活了十四年的我,要离开了这个小岛了,坐着锈迹斑斑的轮渡船,踩在咯吱作响的甲板上踮起脚努力从人群中挤出脑袋,看着海面远处陆地的阴影,轮船的柴油烟飘散在空气中。 我呛了一嗓子,脑子便开始晕乎乎的,要靠近陆地,终于感觉到这个夏天的燥热了。 茫然,南风岛外面的世界是什么样子? 孤独,就这样毫无准备只身一人来到这个新的地方。 卑微,外面的世界好大,比南风岛大好多好多。 期待,期待所有我所未知的美好事物。 这十年会发生什么呢?十三岁的我满怀期待。 十年已过,这十年间光怪陆离的人与事,撕碎了吹散在夏天的晚风里,依然如此安静,坐在走廊长椅上闻着夏夜的味道,不远处悠远的笛声飘在幽暗的夜空中。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/随笔杂谈-15995658.webp" />
</item>
<item>
<title>生如夏花般绚烂 死如秋叶般静美</title>
<link>https://gitpull.cn/post/20190605/</link>
<guid isPermaLink="false">生如夏花般绚烂死如秋叶般静美-bcd926d2</guid>
<pubDate>Wed, 05 Jun 2019 00:00:00 GMT</pubDate>
<author>邻家酒肆</author>
<category>谙风</category><category>随想</category>
<description><![CDATA[“本来,生命只有一次,对于谁都是宝贵的”,这是瞿秋白说的话。我非常认同这句话,也非常相信这句话。今天刚参加完一场葬礼,一位老人的离去,由于恰逢元宵节,族人都在岛上,所以送行的人很多,熙熙攘攘的挤满了半个庭院。许多半年没见外出打工的年轻小伙子,也在庭院两旁站着,扶持着快被大风吹倒的花圈。人们熙熙攘攘地低声私语着,声音轻轻的被大风裹挟着,撕碎了,屋外的女人们听着屋内的哭喊声,也低头揉了揉鼻子。突如其来的大风在马路上卷起了一阵阵尘土,人们捂住…]]></description>
<content:encoded><![CDATA[“本来,生命只有一次,对于谁都是宝贵的”,这是瞿秋白说的话。我非常认同这句话,也非常相信这句话。 今天刚参加完一场葬礼,一位老人的离去,由于恰逢元宵节,族人都在岛上,所以送行的人很多,熙熙攘攘的挤满了半个庭院。许多半年没见外出打工的年轻小伙子,也在庭院两旁站着,扶持着快被大风吹倒的花圈。 人们熙熙攘攘地低声私语着,声音轻轻的被大风裹挟着,撕碎了,屋外的女人们听着屋内的哭喊声,也低头揉了揉鼻子。突如其来的大风在马路上卷起了一阵阵尘土,人们捂住鼻子,看不出是在抽泣还是在防尘。 今天的风可真大,男人们感叹着,不由抓紧了立在院子两旁的花圈,洁白色的招魂幡在风中肆意飞舞着,跳跃着,像一只白色的精灵,在海风的呼啸声与唢呐声的夹杂中,仿佛完全不属于这个场景,不属于送葬的人群。 一声凄凉的唢呐把人群分隔开一条大道,在锣鼓声与唢呐的夹杂下,能听见的只有披着白纱的女人们嘶哑的声音。送葬的人群排成了一个零零散散长队,走过马路,穿过村庄,越过田野,向着海岸边走去。 路旁的房屋门前,三三两两站着男人女人,女人抱着还在啼哭的小孩子倚靠在柱子旁,面无表情地看着人群。稍微大点的孩子躲在大人身后,抓紧了衣襟,好奇地看着这场并不懂是什么但是觉得热闹的葬礼。 二月份的海岸似乎如同春天一般,坟茔旁满是顽强的杂草与小树丛,在海风中摇曳着,海崖下的沙滩还是潮湿的,显然是刚退潮。只是这季节看不到海鸟,等明年春天,坟茔旁的矮松树上估计就会有贼鸥的巢穴了吧。 唢呐声依旧在耳边回响,飘过了海岸,也穿过了棺椁。 漫长的仪式,我呆呆地看着,有点痴迷,上一次看到这场景估计是七八年前,很严肃的场景,脑海中一直在思考着某些东西,思考着什么我早已忘了。无非是关于生命或哲学,可生命这么大的话题,又怎么是我一平凡人从一场葬礼能思考的来,再多的关于哲学的思考徒增烦恼罢了。 我不能说生命是什么,我只能说它像什么。生命像一面镜子,我们若是对他皱眉,他只会回我们以皱眉,我们若是对他微笑,他同样会回我们以微笑。在痛苦中感悟生命,认识生命。在生命的快乐中体验生活的美好,认识生命的本质,懂得应对生命持有的态度。 海岸边,青青坟茔,皑皑招魂幡。啼哭的幼童,调皮的少年,安静抽烟的年轻人,满脸愁容的中年人,抽泣到哽咽的女人,做仪式的老人们,还有已故的人。生命的一个轮回百态,在这海岸边的坟茔体现地淋淋尽致。 漫长的仪式结束后,跟着人群返回,一路上出奇的安静,唢呐声依旧在耳边回响。看着路边的菜地,我想春天应该来了吧。等春天来了,这路旁的菜地就可以种上点油菜花的种子,或者是海岛人最爱的花生。 在记忆里,每年农忙的时候,男人女人,老人小孩,撸起袖子沿着田垄走到自家的田地。左手提个装满花生的布袋,右手抓一把花生种子,弯下腰,两颗两颗,熟练地扔进田洞里。等到秋天的时候,花生杆没过膝盖,就可以采摘了。 我想生命在年少时是最美好的吧,无忧无虑,生活的琐事与大人的烦恼是那么遥远,死亡更是一个不会去思考地话题。年幼时的我在海岸边的田地奔跑时,又怎么会去思考那花生田旁的坟茔里会是怎样无穷无尽的黑暗与寂寥。 今晚是海岛人的元宵夜,吃完饭的我站在自家院子看着天空炸开的一朵朵色彩缤纷的礼花,生命是如同烟花一样绚丽,也像烟花一样会暗淡,消失在寂静的黑夜里。 生如夏花般绚烂 死如秋叶般静美。 今夜万家灯火,海岸边青青坟茔。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/生如夏花般绚烂死如秋叶般静美-bcd926d2.webp" />
</item>
<item>
<title>记忆的种子·一</title>
<link>https://gitpull.cn/post/20190605-2/</link>
<guid isPermaLink="false">记忆的种子·一-c315b081</guid>
<pubDate>Wed, 05 Jun 2019 00:00:00 GMT</pubDate>
<author>邻家酒肆</author>
<category>谙风</category><category>随想</category>
<description><![CDATA[一、南风秋末的北欧,暮色如冻凝的琥珀,缓缓沉降在挪威峡湾的褶皱里,远处峡湾的轮廓被秋雾晕开,湖面倒映着雪山与枫林,像一块碎裂的蓝宝石镜子。漫山枫叶裹挟着波罗的海的咸腥扑向岸边错落的木屋,一扇挂着冰霜的便利店玻璃门在风中咔咔震颤。这是我在挪威度过的第七个秋天,周末刚结束一场在大学礼堂的学生结业音乐晚会,应杰里教授和她家里人的邀请,周日要去挪威港的海岸边野餐。我走在大学城附近的街道上,可能是天气微凉的缘故,有些萧瑟,路人行色匆匆,怀里揣着面…]]></description>
<content:encoded><![CDATA[一、南风 秋末的北欧,暮色如冻凝的琥珀,缓缓沉降在挪威峡湾的褶皱里,远处峡湾的轮廓被秋雾晕开,湖面倒映着雪山与枫林,像一块碎裂的蓝宝石镜子。漫山枫叶裹挟着波罗的海的咸腥扑向岸边错落的木屋,一扇挂着冰霜的便利店玻璃门在风中咔咔震颤。 这是我在挪威度过的第七个秋天,周末刚结束一场在大学礼堂的学生结业音乐晚会,应杰里教授和她家里人的邀请,周日要去挪威港的海岸边野餐。我走在大学城附近的街道上,可能是天气微凉的缘故,有些萧瑟,路人行色匆匆,怀里揣着面包穿街而过。我还在头疼应该买些什么礼物应邀参加同事的家庭聚餐,每次参加挪威当地人的聚会总是会有这样的烦恼。 翻过两条街道,街道尽头一家中古店吸引了我的注意力,很难得在挪威这么偏远的城市还能看到一家中国的中古店。走进店里,风铃被门帘带起的微风摇地钉钉响,店主抬头看了下我,是位亚裔面孔的男生。礼貌性地相视一笑,我便低头自顾自看起货架上的商品,东西并不多,中国的陶器,刺绣包和一些略带发旧的传统服饰。 店里也没啥客人,偶尔进来一个客人,目光四处扫过之后便也很快离去了。看来生意是挺不好的了,当然这种天气也不会有什么人来光顾这种街头的平平无奇的小店,像是被小城市遗忘的满是风尘的旅人。 “中国人吗?” 店主倒是先开口和我搭话了,他解开围裙往窗台上一搭,擦了下手便上前给我倒了杯咖啡,拉开张椅子示意我坐下来喝点。我看了眼窗外,天色还早,在这里闲坐一会儿倒是也很惬意。 “嗯是,看你应该也是国人,在这里开店挺久了吧” 我在椅子上坐下来,端起咖啡杯想抿一口,感觉有点烫又放了下来,双手搓了搓假装在暖手。又抬头端详了一眼店主,有点年长的中年人,胡渣子像是修剪过了但是又好像很久没有再打理了,眼睛倒是十分有神,亚洲人的纯黑瞳孔直直地看着前方。窗户的秋风穿过间隙发出细微的呼啸声,气氛稍微有点安静。 “嗯,来挪威有十来年了,应该有了,开这家店也有四五年” “勉强糊口罢了,在这里的生活就是这样的,挺无聊的,一天来不了几个客人” 店主自嘲地笑了笑,端起自己的咖啡杯吹了吹气,喝了一口放下,又看了眼窗户,依然没什么人。 “在这里的生活确实就是这样的,日复一日的宁静” 我也感叹着这善乏可陈的生活,这十年来去过欧洲这么多国家和城市,三年前在瑞典的这个小城市里当了一名大学的钢琴教师。已经走了太多地方,能找个安静的地方舒适地呆着确实很惬意。 但是有时又觉得这几年太过惬意了,太安静悠闲了,安静到我似乎把以前的一些记忆都给遗忘了。是什么样的过往记忆呢?尘封太久藏地太深我也不清楚了,我也没刻意去追寻过到底是什么记忆,日子就这么一天一天过去。 和店主有一话没一话地闲聊着,知道了他之前也是在国内工作过一段时间,迫于压力和想出去看看的心,阴差阳错地来到这里。 “你在国内的老家在哪里?” 我突然感兴趣地问了一嘴,店主也是恍惚了下,挠了挠头适量着,不知道这是什么难回答的问题吗,他黑色瞳孔变得精神地注视着我。 “你在国内的时候有听过南风岛吗” 街上刮起一阵急风,几张报纸被吹的在空中飘舞着,转瞬间被撕开,不规则的纸片被吹向更高的地方,落在几户人家的烟囱上。 我有点恍惚了,头皮痛了一下,不知道是因为吹冷风了还是咖啡烫的,眼角有点湿润。窗外几个披着风衣骑着自行车匆匆而过,我呆呆地望着,脑海思索着。南风岛?好熟悉的名字。 好像,是一个很重要的地方。 可是太久远了,像儿时的梦,完全想不起来了,只有一些模糊的影子,越想我的头愈发的痛。无奈只能浅浅的笑着摊开手和店主表示并不清楚。 店主眼神稍微略带失望,随即又正常地笑了笑,眉眼舒展开来,讲了一些南风岛的风土人情,儿时和青少年的经历之类的。我认真地听着,总感觉这一幕似曾相识,但奈何脑子就是不太好使,在这城市安逸久了感觉记忆力都退化了。 喝完第二杯咖啡我才发现天色早已暗下来了,我赶忙告别店主,顺便在他店里挑选了两个相对还好看的陶瓷茶杯付钱后便推门而出,身后的风铃依然像来时一样吹的钉钉响。 “忘了问了,客人怎么称呼?”店主在门后小声喊了句。 “我叫奕” 透过窗台能看到店主重新穿上围裙,摆弄着他的货物,估计今天他也就赚我这一单了。不过这人倒是感觉有点熟悉,下次还可以再来,卖的东西挺普通,咖啡却是挺好喝。 我心中思量着,便也和街上的行人一般,慢慢隐没在街道尽头。 二、坟茔 周日下午,和杰里教授还有她家人在海港旁的海岸边铺开一张大大的野餐垫。一些应季的水果和甜点很快铺满的大半个野餐垫,不过我还是喜欢带点熟食过来,这么多年了还是没能养成欧美人的胃口。 这天的天气倒是不错,云层中撕开好几道口子稍微挤出了点阳光,投射在海崖边的草地上,显得生机盎然。好多海鸟也出来觅食了,盘旋在海岸边不知道在等待着什么猎物。杰里家两个孩子拿着我送的中国风筝在草地上欢快地追逐着,海风很强劲,风筝很快就高高地挂在半空中,风筝的尾巴被高空的涡流带动着,一抽一抽得也蛮有意思。 送杰里夫妇的两个中国陶瓷他们也很喜欢,和这群北欧朋友们便坐在野餐垫上分享着各自带的的食物,聊着各自的近况。海风吹的很舒服,只是我披在肩头的长发时不时被扬起在脸上胡乱飞舞,模糊我的视野。隐隐能看到海崖远处还有一堆低矮但是略显整齐的石堆,似乎是修在海边的墓碑,向着大海,静静地在那坐落着。 可能世界各个角落的墓碑都大同小异吧,看着都有种安静的力量。孩子们玩风筝的线终于是被风扯断了,风筝在空中摇曳了两下,猛的一下子倒头扎下来,飘向那座墓堆石碑,最后挂在旁边的几只长满野果的矮树丛上。 石堆杂草繁多,孩子们不好取风筝,我起身和杰里一起往石碑走去,走到跟前才看清楚,一个挺破旧的小坟堆,倒是开满了野花和各种奇奇怪怪的野果,还有不少海鸟来吃果子留下的早已干巴的鸟粪。杰里去取风筝,我绕道墓碑正面仔细看了眼墓碑主人,没有照片,我知道本地的墓碑总是会贴一张墓主人的身前照片的,奇怪这里没有。 看了眼刻字,竟然是中文,我震惊之余四处张望了下,确定我还在挪威海边而不是国内的哪个小岛上,在遥远的异国他乡的海岸边,居然会有一座遥远故乡的墓碑,我把石碑上的白色鸟屎擦去,仔细端详着碑文。 海水梦悠悠,君愁我亦愁。 南风知我意,吹梦到西洲。 我坐在地上,呆滞着目光紧紧盯着这诗。 我好像做了一场很久远的梦。 三、林谙 二十年前,我还在国内S城上学,高中的生活也是日复一日,难得的暑假假期里也要面对一堆的补习班和专业课,以及厚重的作业,我呜呼哀哉地在我的小房间里翻滚着,手里拿着手机刷着各条社交平台的趣味图文,丝毫没有要动笔做作业的迹象。 因为刚看完席慕容的《渡口》,我兴致盎然地写下一段卡片文字: “渡口找不到一朵可以相送的花,就把祝福别在衣襟上,而明日又隔天涯” 发完帖子,扔下手机又做了两张卷子,眼睛已经发困了,迷糊地不行,想睡觉但还是拿起手机再耍会儿。夏天的风儿吹着窗户的树枝摇摆不停,我四脚朝天躺着仰看着手机,打开社交软件私信发现有人点赞留言了,很普通的留言。 “是渡口吧,我也挺爱看席慕容的文字。” 我本想甩手机睡会儿午觉,但想了想开学后可就没时间玩手机了,睡觉的时间多的是,玩手机的时间可就在当下呀!又刷了好多短视频,顺便回复了那人评论。后面他又主动私信有一搭没搭地聊了会儿,无非高中作业,日常生活之类的。我也是无聊地很,纯打字羡慕时间。 想想S城的高中生压力确实挺大,要是没和他聊天我都还不知道原来其他地区的高中生活也可以这么自由和丰富多彩,我开始好羡慕着他的中学生活。 他和我讲了他中学时的很多有趣经历,和老师一起玩游戏,运动会班级里打牌,各种看起来叛逆且自由的骚操作。但是他成绩又很好,这让我不禁咬牙切齿,痛恨高智商理科生的同时又流下羡慕且向往的泪水。 后面的暑假假期也就这样随着无尽的作业和补习课还有他的闲聊中度过,他真的很爱分享,我也很爱听他说的那些有趣的事。他说他这是少年闰土给鲁迅老爷讲述那些夏夜刺猹的故事罢了,我说你要是我家里的长工我就命令你把我的暑假作业都包圆了。 他说可以的,我直呼好家伙,吹牛呢。结果人家给我亮了S城顶尖大学的身份,原来是顶尖大学生啊。 后来他也重新自我介绍了下,是S城大学的大四学生,暑假正在找实习工作,一直没找到,闲得无聊每天和我聊天。我发了个白眼,我也是闲的无聊才和你聊天的。 “不过话又说回来,那你数学很好吗,可以去当数学家教呀,圈高中生的钱,贼赚” 我想起我每节数学课要花800人民币就心痛,但这是我能想到的大学生最赚钱的方式了。便直接推荐给了他,结果他说他不善于表达,还是适合做些专业相关的技术工作。 我又是一个白眼,哥你也太会装了,还不善于表达,前阵子看你这么会唠嗑的还以为你对我有意思呢。当然这是我的内心戏,没有发给他。 想到这里我又装起八卦问起了他的感情情况,倒是很坦诚地说了谈了几段恋爱吧啦吧啦。大学生生活还是滋润呀,我被他说的一愣一愣的感觉高中生活都黯淡无光了,只有那大学才是人应该呆的地方。 他总是会鼓励我多走出去看看,去更远的地方,去欧洲留学,他也想去看但是他没机会,就想看我走的远一些。 他还和我说他的梦想是当一个隐居山林的道士,哈哈我夸他很有理想呀。 暑期快结束的时候他说他找到了实习工作,在S城的A公司。呦厉害,大厂啊,我半调侃半祝贺地说着,顺便又在朋友圈发了几张自拍,那天阳光很好,照进房间里拍侧脸照很有氛围感。他先给我朋友圈点了个赞,然后才慢悠悠地回我消息。 “公司确实挺不错,你想过来玩吗,我们见个面” 我从四仰八叉的躺姿猛的坐起来,嗯,有点想去,不过感觉有点害羞,还没和网友见过面,心跳莫名有点加速,不知道该拒绝还是怎么处理。 发了个表情包装傻充愣。有什么好玩的吗? 我问了问,确实只是见面的话感觉有点尴尬,对话框里的聊天永远都可以有时间去思考,但是面对面的对话我估计脑子经常要宕机。 “哈哈可能也没啥好玩的,公司大楼里是有一些公共区域可以运动休闲的,公司里的咖啡也挺好喝的,可以试试” 嗯,那找个时间吧。我还是应邀了见面,这么多天聊天感觉也不像个坏人,总不能在大公司里把我卖了吧。主要他真的挺有意思,另外假期也没好好出去玩过,我这么说服自己的。 时间到了约定的日子,我在房间里磨蹭着又开始有点犯懒,试了几件衣服最后挑了件喜欢的裙子,头发梳了又梳。有点小焦虑,要是他发现朋友圈的照片仅供参考怎么办。虽然他经常夸我好看,但是终究看的是照片。 你能认出我吗? 我发出了灵魂拷问。他说一定可以。 走出地铁站,夏天刚下雨的空气好闷热,空气中夹杂着汽车尾气和灰尘在空中弥漫着。好热,我向A公司走去,路上全是上班的人,每个人背着电脑包灰头土脸低头赶路。这就是上班族吗,班味真重啊,我感叹着。 抬头发现有人隔着人群远远地向我招手。 原来他真的可以这么远在漫漫人群中一眼认出我啊。 我低着头不敢直视他,憋着笑朝着他走去,他也走了过来接我,穿着黑色衬衫打着领带,倒是挺有模有样的一副精英模样。 “你比照片要好看呀,很好认的。” 人怎么可以这么直接,我都忘了怎么害羞了,哈哈哈地笑着,便跟着他进了大楼里。 这家伙真的带我在他们公司喝了一下午咖啡,满满两大杯咖啡,不过味道确实像他说的一样还挺独特的,之后也带我逛了逛大楼里的一些区域,大公司确实有大公司的独到之处。 他带我去了一个他的秘密基地,一个顶楼天台,没什么人来,可以看到S城大半的风景,我们坐在护栏旁,风儿很温柔地吹过,白云也很懂事地给我遮阴。我们端着咖啡杯,像平常一样有一搭没一搭地聊着。他有时看看我,又看向远处的风景,好像有什么想说的话,又随着咖啡一起咽下去了。 “你真好看呀” 他这样说道,我也很开心他这样夸我,满是期待。我明明都看到他满眼爱意了,不过他就是偏不说了。 下面的街道车水马龙伴随着不间断的鸣笛声,天空偶尔有飞机飞过从云层中传来低层的轰隆隆的声响,长长的飞机尾迹划过天空,仔细听甚至能听到蝉鸣的声音。我的高二暑假就快要结束了。他数着日子,约好了下一次见面估计就是寒假了。 我看了看他,又看了眼那些高楼和天空,在这里真的好惬意,好喜欢。 回去的路上他很贴心地帮我打了个车,打开车门送我上车挥手告别,我手里还抱着他最后送的两个娃娃。 “很喜欢你,你很干净很美好,不过因为很多因素包括年龄,这份喜欢就止步于此不再越界吧” 在车上收到他发来的消息,我挺开心的,他说的那些因素我也明白。能有一个这样彼此敞开心怀的年长友人我就很满足了。 “嗯明白,我也超喜欢你的” 我这样回复道,随着车子逐渐汇入晚高峰的车流中,慢慢消失在他的视野里。我的高二暑假也就这样即将结束了。 随后的日子我们依然像之前一样像好朋友一样聊天分享日常。开学后因为要交手机我只能在周末和他一口气分享了一周的学校趣事,他有时候也会在周内时不时和我发几条消息,虽然我也没回。 我也开始向往寒假,期待和他下一次见面,我也开始向往高考结束,向往他描述的那些自由美好的大学生活,向往早点成年,可以更自由地选择自己喜欢的。 和他相关的一切都在寒假前的第三个周末戛然而止,周五下午拿到手机后,我习惯性点开了和他的对话框。这一次他居然没有给我发新的消息,平常这个时候对话框他十几条骚扰信息是少不了的。 我试探性地给他发了消息,没有收到答复,第二天第三天也没有,周一重新上交手机,我煎熬地等了一周,等到第二周拿到手机我依然没有他的消息。 直到放了寒假,我也没有他的消息,他就这样消失了,我也没有问过他的地址和电话。 他好像没和我打声招呼就消失了。 我对此无能力,生活还在继续,寒假的补习班和作业不比暑假的多,马上也要高三了。我的精力很快又投入到日复一日的复习中,虽然还是会时不时想起他,但是对于他的不辞而别我也很难原谅。 即使不是恋人,我们也是互诉过彼此喜欢的人呀。我很难过他的消失。 好在学业压力够大,每天的声乐课数学课慢慢冲淡了我的难过。日子还是日复一日的单调,我在S城的人流中一个人行走着,有关那个暑假的记忆越越来越久远了。 我也渐渐把和他有关的记忆封藏了。 四、遗忘的种子 两年后我如愿考上了S城的一所向往的音乐学院,大学生活确实像他所描述的那样精彩和自由。在大学里我也谈了一次恋爱,体验了青春恋爱的甜蜜和烦恼,但是我也没停下脚步,我想走的更远去看看。 又过了两年我以交换生身份出去留学交换,在意大利的一所音乐学院进修。 意大利的冬天有点像上海,但是空气更湿润一些,走在街上总是有种透骨的湿冷,穿着风衣也抵不住那些寒风往缝隙里钻。 一天晚上刚结束一场集体演奏,和留学生小团体聚餐后,大家饭后第二场在某个比较大的宿舍里喝点小酒聊天,一个同学拿了一封信出来给我。 “奕,有你的信,好像是从国内转发的,发到学校地址了,我刚好下午在学校帮你拿了” 居然会有国内发过来的信,我有点疑惑地接过信封,有点陈旧的皮纸,不像是刚写的信。上面写着几个“时光邮局”的字样。 是四年前寄出的一封信,不过最近才被发出来,我仔细回想着四年前的人和事,有一个很久没出现的身影浮现在脑海里。 我辞别同伴们独自回了自己宿舍,窗外宿舍湖边还有人在放烟花秀,一道道烟花在夜空中绽放开格外漂亮,我半靠在床上,缓慢拆开信封,将泛黄的信纸缓缓取出,伴随一股淡淡的陈旧书纸的味道。 “奕,见字如面。这封信写在四年前,很抱歉当初的不辞而别,故事可能有点老套,我体检检查出一个一个肿瘤,已是比较大的晚期了。我也不是很想去做那些没有意义的治疗,还记得我和你说的我的梦想是当一个清修隐居的道士吗” “哈哈剩下的时间我确实去当隐居的道士了,至于和你不辞而别,因为你快高三了,不想因为我的事情影响到你,我想能有给你留下一段美好的经历我就很满足了。我相信未来的日子里你会成为一个很优秀的音乐家,遇到很温柔的人,去很多很远的地方,看我还没看到过风景。” “如果能有此荣幸的话,把我的记忆藏在心底,去往那些你想去的远方,有一天或许会在那些不期而遇的地方生根发芽” “还是好喜欢你,感谢让我遇见美好的你——林” 收起信纸,我看着窗外的烟花在夜空中绚丽地绽放,随即又暗淡直至烟消云散,打开窗户能闻到一股硝烟味随着冷空气袭来,我打了个冷颤,嘴里说着话。 “我也还是好喜欢你” 两年后我大学毕业又去挪威的皇家音乐学院进修硕士学位,离国之前我去了趟南风岛。一个边陲小岛,海岸边满是野草和繁花,海边的山林郁郁葱葱的在海风下如同波浪般舞动。我找到了他的坟茔,就朝着大海,微风徐徐,坐在他的石堆旁吹着暖暖的海风很是惬意。 我在他的坟茔旁的石头上刻了《西洲曲》的后四句。 海水梦悠悠,君愁我亦愁。 南风知我意,吹梦到西洲。 之后我又在海边山林流连,走过他曾经走过的路,在山崖边,在草地上,在沙滩上,在他和我分享的那些故事里,我又走了一遍。 飞去挪威的飞机上,我看着地面的山峦和海岸线,逐渐被云层遮挡,这份记忆的种子,又要被我深深地种在心底了。 我好像做了一个好久远的梦,梦里我种下了一颗种子,我看着眼前的墓碑,感觉心底有一股很温暖的力量,保护着那颗种子慢慢发芽了。 杰里教授喊我拿着风筝回去,我起身走了两步,摘了一朵野花放在墓碑上,便转身离去。海岸边吹来的海风似乎也变得很温柔,杂草堆上的野花在迎风飘扬,花香散满了四周。 渡口找不到一朵可以相送的花,那就把祝福别在衣襟上,而明日又隔天涯。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/记忆的种子·一-c315b081.webp" />
</item>
<item>
<title>记忆的种子·二</title>
<link>https://gitpull.cn/post/20190605-3/</link>
<guid isPermaLink="false">记忆的种子·二-29fe4821</guid>
<pubDate>Wed, 05 Jun 2019 00:00:00 GMT</pubDate>
<author>邻家酒肆</author>
<category>谙风</category><category>随想</category>
<description><![CDATA[五、再访小店挪威的冬天终于还是来了,先是下了一场薄薄的雪,街边的房屋都盖上了一层白白的薄膜,然后又淋了最后一场雨,天气终于冷冽起来了。海岸边的贼鸥早已准备好了收集来的树枝将巢穴加厚一圈,以应对将来到来的冬天。太阳下山地越发早了,晚饭吃完还没过多久,站台窗台边就已经看不见海岸线,街上的行人也更稀疏了。两个月前和杰里教授家里人一起在海边聚过餐后,一些中学时代的记忆在我心底又复苏了,关于那些懵懂的年月,还有那个曾在心底藏了好久的人。只是那海边…]]></description>
<content:encoded><![CDATA[五、再访小店 挪威的冬天终于还是来了,先是下了一场薄薄的雪,街边的房屋都盖上了一层白白的薄膜,然后又淋了最后一场雨,天气终于冷冽起来了。海岸边的贼鸥早已准备好了收集来的树枝将巢穴加厚一圈,以应对将来到来的冬天。太阳下山地越发早了,晚饭吃完还没过多久,站台窗台边就已经看不见海岸线,街上的行人也更稀疏了。 两个月前和杰里教授家里人一起在海边聚过餐后,一些中学时代的记忆在我心底又复苏了,关于那些懵懂的年月,还有那个曾在心底藏了好久的人。只是那海边的山崖上为何会有一座故乡的墓碑,刻着和他在南风岛上的我刻下的石碑一样的诗句,这是某种巧合,还是有人故意安排,这两个月来我时常疑惑着这个谜题。 隐约感觉这些疑问会在一个地方找到答案,那个中古店的老板,他为何也是来自南风岛,这些事情实在太过巧合,我想我确实需要再去一趟他的店里问问了。 街上的店铺关了不少,初雪融化过后的街道变得稍微有点泥泞,往日废弃的旧报纸混着泥土湿哒哒地贴在石板上。我垫着脚在青石板上泥水较少的地方笨拙前行,不远处那个中古店柔和的灯光透过窗户在街道尽头摇曳不定,我加快步伐穿过街道走到小店门前。 通过泛黄的窗户,看到店主仍然像往日一样穿着围裙在打理他那些陈旧的货品,鸡毛掸子在货架上扫了又扫,壁炉里烧的火光给整个小店笼罩上了一层温暖舒适的结界。我缓慢推开店门,门口的风铃又发出叮叮的响声,店主停下手里的活看了眼我,便放下鸡毛掸子朝我走来问候。 “好久不见,奕,最近可还好?” 我解开围巾挂在一旁椅背上,走到壁炉前伸伸出双手烤了烤火,将身上的寒意都彻底散去。火焰下通红的木炭时而爆裂出一两颗火星子,落在四周的木灰上便暗淡了下来。我这才想起来回头笑着和老板说。 “挺好的,你这店里还是没啥生意呀” 老板走过来拿起铁钳又往壁炉里加了两块木炭,我起身拉了一把椅子就坐在壁炉旁,加完木炭的火堆更旺盛了,火光好温暖,照得我的脸都微微有点热热的。老板也搬过来一把椅子和一张小桌子,泡起一壶铁观音。 “是啊,现在生意就是这样,尤其是冬天,没什么人出门。过段时间开始下大雪了,我可能也要关门了” 茶烧开了,老板给我倒了一杯,又给自己倒了一杯,放下茶壶,从怀里掏出一盒香烟,想抽出一根点上,迟疑了下又放了回去。 “你今天这么晚过来,是有什么事吗” 老板将烟盒塞回怀里,转过头来漫无目的表情望着我。我依旧盯着壁炉里跳动的火焰呆呆看着,气氛安静了许久,我才想起来开口。 “之前过来,老板你说过南风岛的一些事,我想问下你认识一个人吗?” 老板的脸色有点微变,端起茶杯抿了一口茶,他也直勾勾的眼神盯着那壁炉的火苗,火光映射在他的瞳孔中,跳动着燃烧着。 “这个人叫林谙吧?” 老板放下茶杯,慢慢说出这句话来,他转过头来看着我,眼神像第一次见面时的那样深邃,深黑的瞳孔直视着我,我忽然有一种熟悉的感觉又从潜意识里冒出来。 “你认识林谙!?那海边的那座墓碑你也知道吗?”对于他的反问我感到震惊的同时又觉得合理,这些巧合也只能和他有关了。但是眼前的他到底是谁?我急切地想知道答案。 “你终于是看到那个墓碑了吧,看来你也想起来挺多事的,不知道你想起来的故事里,除了那个男主他之外,还有想起另外一个人吗?” “另外一个人?”我心中的疑惑更大了,怎么也想不起来二十多年前的中学生活里还有另外一个让我印象深刻的人。 “终究你还是只是想起了他呀” 老板又把茶杯倒满,悠长地叹了口气,开始讲一个很久远的的故事。 六、另外一个故事 我叫陈北,二十年前因为父母工作的原因我从老家转学到了S城的附中读高二。还记得那天是开学第一天的早课,我被班主任叫上讲台做自我介绍。我满脸害羞地在黑板上写下自己的名字,之后常规地介绍了自己转学前的学校,家乡,兴趣爱好之类的。台下同学有的好奇地打量着我,有的低头不知道在摆弄自己的什么东西,我一时间不知道哈哈再说些什么,便低头回了座位。 暑假刚结束的第一天,同学们还是很兴奋地讨论着各种假期的新鲜事,很快我这位转校生也变成小透明被大家抛之脑后了。我托着腮盯着窗外的树木发呆,前排的一位女同学转头趴在我的桌沿眨着眼睛问我。 “陈北你家是在南风岛的吗? 她很兴奋地看着我,中长发自然散落在肩上,有一股淡淡的香味,眼睛亮亮的满是星光一般,笑起来很好看。可能是过了两秒还是三秒,我心跳紊乱了一下。 “嗯是,你对南风岛很感兴趣吗?” 我依旧托着腮,毫不在意地应付着回答她,心思却早已不在那窗外的什么树木上了。她依然一脸欢喜地趴在那,听到我的答复后更兴奋了。 “我暑假认识了一个南风岛的男生,是你老乡,不过你应该不认识,他都大学快毕业了。” 她这样说道,言语里似乎有一股莫名的快乐,她说起这个男生时的眼睛更亮了,我的心思似乎有点乱了。 “叫什么名字,南风岛就那么大,兴许我认识” 我平淡地问着她,手里拿出根笔来让她在我作业纸上写下来。她挽了下头发,伸手接过我的笔,在作业纸上很工整地写下来了两个字,带着一股淡淡的花香。 “林谙?” 确实不认识这个人,但我还是假装托着脑袋想了想,一副略有所思的样子。她睁大眼睛望着我,很期待的神情,似乎想从我身上找到和这个人有点关联的故事和线索,我却有点烦躁了。一种莫名其妙的失落感。 “想不起来,但是有点印象,我后面留意一下这个人”,我还是装模作样地回复了她。 “好呀”,她把笔还给了我,转身坐回自己座位,不过她挠了挠脑袋迟疑了下又立刻转了回来。 “我叫奕,欢迎你转学来我们班呀,陈北同学”,眼睛依然一眨一眨的亮亮的,我笑了笑表示感谢,便继续这无聊的开学第一课。 开学之后的日子里,学校生活和之前在老家的高中没什么太大区别,只是奈何这边学生的小团体圈子早已经固定下来,我便也开始游离在各个小圈子团体之外的高中生活。当然也认识了一些同学朋友,一起吃饭上学,和其他大多数的高中生生活别无二致。 某个夏夜的晚自习,我帮科目主任搬一堆卷子从办公室回教学楼的路上,走过一道教学楼之间的长廊。夏夜的蝉鸣在四周的草坪和绿植中不时传出,微风徐徐倒是有几分惬意。一阵悠悠的钢琴声从旁边教学楼的琴房里传来,我停下脚步仔细聆听着,很优美的钢琴声,透露着一种优雅而温柔的力量。我朝着声音来源走去,在一间琴房门口停住了脚步。 透过琴房的窗户,看到一位中长发的女同学正专注地看着琴谱,葱白细长的手指在钢琴键上来回飞舞跳跃着敲击着,身体随着节奏起伏着,十分地投入。她的眼睛也是像星星一样明亮,认真专注地练习着琴谱上的曲子。我驻足了好久,直到琴声渐渐低沉下去,她慢慢地把手放在琴键上,抬头看向了我,眼神中带着一点惊讶和善意的微笑。我这才看清楚了这位在琴房里练习的女孩是谁,正是坐在我前排的女同学奕。 开学一个月来和她相处倒是挺融洽的,时不时她也会转过头来和我聊些零零碎碎的话题。不过她也没再和我打听南风岛和那位林谙的消息,估计想着也从我这里套不出什么有用的信息。不过倒是没注意到她原来每天晚自习都会在琴房练习,之前看她晚自习后两节座位都是空的,以为是回家了不上自习。 我心里暗自琢磨着,挠了挠头,尴尬地笑了笑,还是推开琴房的门和奕打了招呼。 “没想到你晚自习在这里练习,我刚从办公室过来,路过这里,被你的琴声吸引倒是听了好久,弹的很好听”。我客气地和她说着话,打量着四周,琴房并不大,除了一架大钢琴摆放奕的面前,房间四周还有堆放着一些杂物和其他的乐器,几张老旧的相框和证书挂在窗户旁的墙壁上,应该是往届的学生留下来的。 “哈哈谢谢呀,你听了多久了” 她依然大大咧咧地笑着,给我推了一个小板凳。我也不客气地拉着小板凳坐下了,看了眼她的琴谱,着实是看不懂,看来真的是术业有专攻,我在心里感叹着。 “也没多久哈,就听了半首曲子,不过确实第一次听你弹钢琴,感觉很厉害”。我笑着夸一下她,会弹琴的好看女孩子确实是值得赞美的。她捂嘴笑了笑,没有再多说什么,翻开琴谱的下一页准备继续练习。 “你要是不想回去上晚自习的话,留在自己继续听我倒也不会赶你走的”。我听不出来她这是送客之意还是想让我继续欣赏她的音乐,但此刻既然来都来了,琴房外面树荫摇曳,蝉鸣依旧,窗外的月色也很明亮,繁星点点。此刻的氛围我有几分沉浸其中,便继续厚着脸皮坐在板凳上听她继续练习。 大概半首曲子过去,我正看着入迷,她放在一旁的手表亮起一条微信消息。我瞥了一眼,又把目光移开了,独自看着窗外发呆。 手表上显示的消息是:“我好想你啊” 我的波涛荡漾心感觉也像琴声一样戛然而止,窗外的月色有点黯淡无光,被一团乌云遮住了,让人有点喘不上气。她拿起手表看了一下,眼角勾起一丝隐瞒不住的笑意和快乐,回复了一些不知道什么内容后便放下手表继续练习了,琴声似乎欢快了许多。 “奕,你有对象吗?” 我不再呆滞地看着窗外的乌云,目光转向正在练习的她问出了这句突兀的话。感觉有几分不妥我又不知所措地低下了头看着桌角目光不定。她愣了一下,略有所思地看了下我,缓缓开口。 “没有呀,不过我也有喜欢的人了”。 她的眼睛依旧那么明亮而自然地看着我,这个“也”字她说的恰到好处,我也不再有其他问题了。沉默了许久,我才想起来开口。 “很高兴今天能听到你的琴声,感谢你没赶我走哈哈,不过我要回去继续上晚自习了,下次再来找你玩”,我故作轻松地站起身,跟她挥手告别,便离开琴房往教室走去。 继续穿过那片长廊,有情侣偷偷跑出来在黑暗的角落里幽会,虫子吵杂的叫声让我有点心烦,狠狠地瞪着了角落里的情侣一眼便快步离去。她说的意中人应该就是她之前说起的那个林谙吧。 七、林谙的消失 再之后的日子里,我也不再过多过问奕同学她的感情状况,我怕真的知道有了一些进展也只会让我自己徒增烦恼罢了。但是和她关系倒是日渐熟络起来,我不知道是我自己不肯放弃还是说只要能靠近她我就已经很满足了。 我还是不自觉地会向她靠拢,给她讲作业,课间的打闹和玩笑,日常的闲聊与互动。这些都让我沉醉其中,当然她还是和我保持着正常朋友的边界感,我们也没有什么暧昧的动作发生,可能能这样做好朋友我也心满意足了吧。 日子一天一天过去,高中的生活也在学业的压力和青春期的种种快乐与烦恼的交织中上演着。寒假前的第三周末,奕她突然忧心忡忡地找到我,打听林谙的消息。她依然像第一次见面一样趴在我的课桌旁,满是焦虑和疑惑,眼神都暗淡了些许,像一只淋湿了毛的小猫。我不争气得隐隐心痛了一下,便答应了帮她回去寒假打听一下林谙的消息。 林谙在一周前就没再给她发消息了,也没再回她消息。这些撩妹达人怎么这么喜欢玩消失啊,我在心里愤愤不平地指责着。 “会不会是人家谈了新的对象,不想理你了”,我打趣地这样和她说道,她白了我一眼没好气地转过身去。我自讨没趣地连忙道歉,心里暗自懊悔不该开她这玩笑,也怪自己还是没掂量清自己能开玩笑的份量。 之后的几周林谙确实也都没了消息,寒假很快就来了,我收拾东西启程回南风岛过暑假。离开学校之前奕再次让我帮忙打听一下林谙的消息,我苦笑地答应着,没再多说什么。 南风岛其实说大不大,说小也不小,从岛的一边开车到岛的另一边也要大半个小时,一个镇的规模。我先是找了一些老家的朋友问了下这个人的事情,都不认识这个人,后面又问了一些亲戚也没认识这个人的。之后在寒假过了快一半的时候终于有点眉目。一个远房表哥说这个人是他学长,我兴奋地找表哥要到了林谙的地址,便立刻赶往林谙的住处。 当然,没有那么轻易地找到林谙本人,但是我却得知了一个令人震惊的消息。林谙已病重,放弃治疗在岛另一边半山腰的山林道观中进修隐居。返回的途中我在惊讶疑惑和惋惜同情的复杂心情中久久不能释怀,我没有把这个消息告诉奕,打算过两天再去正式拜访一下林谙本人,他的不辞而别应该也是有道理的吧。 第二天清早,我买了些水果往半山腰的寺庙而去,路上我始终有点困惑,我是以什么身份去见这位素未谋面的人?同学的意中人?还是我的情敌?我感觉自己有点搞笑。犹豫和思索中却已经走到道观门口,与其说是道观,其实就是几间简单的平房。 树林的叶子随着寒风散落了院子满地都是,不过明显院子经常有人修整,没有太多杂草。房子上的瓦片规规矩矩地码放着一排排,偶有些破碎的但是整体看着还是让人赏心悦目的,这里的环境确实有一种能让人静下心来的感觉。 院子里有个高高瘦瘦的青年人拿着竹编的大扫帚在把地上的落叶扫在一起,堆成了一座小山高的枯叶堆,却总有些叶子被钻进院子的寒风带起,散落在一旁,青年人便再次用扫帚将落叶聚拢在一起,装进一旁的竹筐里。 “请问你是林谙吗?”我礼貌站在院子门口礼貌地问候了下,一阵风吹过树林发出呼呼的啸声。青年人抬头看了眼我,显瘦的脸颊可以看出一些病痛的痕迹来,眼睛倒还是十分有神的,直直地打量着我,带着些疑惑的神情。 “嗯我是,你认识我?”。林谙将最后一小堆落叶装进竹筐里,弯腰要把一竹筐枯叶搬到院子角落,我连忙走过去搭了把手一起搬过去。放置好竹筐和扫帚,我才做了下自我介绍,讲述了奕在失去他消息后的疑惑和苦恼。 林谙听完在院子里站了良久,叹了口气,将我带进屋子里坐着休息喝茶。这屋子像外面院子一样干净,也一样萧瑟,简单的桌台上摆放了少许茶具,还有一些书杂乱地堆放在一旁地上。墙上挂着一些诗词和画,也看不出来是哪个作家写的画的。 林谙往我茶杯里倒满了一杯苦茶,我们又把身子往火炉靠了靠,感觉暖和了许多。火炉的火光映射在他脸上,拉碴的胡子倒也显得柔和了几分。他眼睛直直地盯着火炉里跳动的火苗发呆,许久未开口讲话,我也安静地坐在一旁并未说话。 “还是不要和奕说我的情况了”。林谙开口说了这句话,将茶杯夹在掌心来回滚动着取暖,眼睛诚恳地看向我。我似懂非懂地点了点头,又有点疑惑地看着他。 “是因为不想影响她学习还是什么原因?” 我将手中的茶一饮而尽,主动给林谙斟满了茶,又给自己倒满了,捧在手心里依旧看着火苗发呆。林谙给火炉添了块木材,火星子一下子冒出半个人高,又很快在空气中消散,留下一股烟尘味。 “学习是其中一个原因,但我更多还是不希望她会沉浸在喜欢的人逝去的痛苦中。如果和她继续保持联系,她的感情会越来越深,直到我离去的那天,那份痛苦相比于我现在的消失要重得多”。 “另外像喜欢之人逝去的痛苦太深刻了,我怕影响她以后的感情生活,就像你也不希望假如有一天你能和她交往,她的心里还一直惦记一个逝去的男人吧?” 林谙的眼神直勾勾的看着我,带着一些善意又有几分想把我看穿的神情和非常宁静的从容感。我有点慌张和羞愧地望着他,又随即避开了他的视线。此前我没有和他提及我对奕的爱慕感情,不知道他是怎么看出来的。我握紧茶杯不知道该如何回答,看着门外面的院子空地上又落下了几片零星的落叶。 “哈哈,不喜欢奕就不会这么费心跑来找我了。奕她真的很美好的,和她在一起呆着有一种很干净很纯洁的温暖的力量” 林谙笑着拍了拍我的肩膀,炉子的火光更旺了,感觉可以燎到我的眉毛,我往后推了推椅子。 “嗯是的呀,是很美好的一个女孩”。我长叹了一口气,不太想继续聊我对奕的感情,毕竟在这一层关系里,我只是一个人的独角戏,现在内心情感被人挖出来,还真有点不是滋味,况且对方还是奕喜欢的那个人。我又把茶杯里的茶喝光了,自己继续烧了一壶。 等待茶热的同时也寒暄了下他的病况,很快我们聊到太阳快下山了。他也不愿意谈及自己的病况,我们就聊了一些中学生活,还有在南风岛上的一些生活。他还给我未来的高考和专业选择做出了一些建议。确实是很好的一个人,总是透露着一股热情和从容,总能锐利地分析出问题又可以饱含善意地和你分享。 又聊了许久,我准备起身回去,他让我等他一下,便转身往里屋走去,过了会儿拿出来一个信封交给了我。 “这封信我本来打算什么时候去趟城里给那种寄慢信的时间邮局邮寄,不过现在我感觉你可以帮我。我想你帮我在四年后将这封信寄给奕”。 林谙拿着信满怀期待和诚恳地看着我,像奕当时让我来找他的眼神一样。我隐隐心痛了一下,接过他手中的信,便挥手和他辞别了。 走出院子,院子里的落叶又堆积了薄薄的一层,林谙重新拿起了扫帚。 “陈北,我的故事快结束了,你该开始写你自己的故事了” 林谙身体支撑在扫帚上又拍了拍我的肩膀和我如此说道,我点了点头,回头便转身离去。背后院子里又响起了扫落叶的萧条之声,像我刚来的时候那样,青年人一个人独自站在院子里,消失在夜幕中。树林里传来几声禽鸟归林的啼鸣声,带着几分悲凉和不舍。山林在冷厉的海风吹拂下如同波涛一样起伏。 寒假很快过去了,期间我也有再去拜访了一两次林谙,他也日渐消瘦,但总是很热情地招待我喝茶聊天。开学之后我便再也没见过林谙,我也没有把这些告诉奕,只是说确实没有在南风岛找到他。 八、离别 春节过后奕倒是精神了一些,虽然她偶尔还是一个人发呆想着些什么,但总算是恢复了往日和我们一起打闹游戏的状态。有时从她眼神里还是能看到装着一些心事,但是哪个少女没有心事呢,大家的高中生活也便这样日复一日地过去了。 我和奕的关系也越来越好,很多时候我们基本都一起行动,上课放学去食堂都一起走,有时候我自己有点怀疑这种好朋友的程度是否可以支撑我去表达爱意了。但是我心中始终有个担心就是她是否该想着林谙。我这点心思还真的是被林谙猜透了,可惜我没有像他一样的勇气和从容。以至于后面的时间我始终没有勇气对她说出我的心意。 高三的秋天的某一天下午,林谙走了。 我在得知这个消息的时候,正在下午的数学课的课间,在偷偷带的手机里看到了这个消息。我抬头呆呆地看着前面正在写作业的奕,我有点恍惚,她低头认真得看着卷子上的题,和四周教室的吵闹和喧嚣显得有点格格不入。一股难过和悲伤涌上我的心头,林谙走了,我却如此地难过,看着眼前的奕我眼角有点湿润 她安静且美好的在那写着作业,秋天的凉风从窗外吹进来,翻开了一张张卷子的页脚。奕迟疑地停下笔,转头看向窗外,除了树木摇曳,风声徐徐,温暖且柔和的阳光外没有其他的。 她转过头来看向我,依然是那明亮如满眼繁星般的眼睛。 “你怎么了?”。奕问我。 “没事,今天的风儿吹的我有点想流泪”。 “我也是,不知道为什么刚刚有点想流泪”。奕茫然地说道。 我起身往教室外走去,远离人群,漫无目的地走在校园里,带着我自己也不清楚到底是什么样的复杂情感在悲伤地一个人独自漫步。 半年后的夏天,高考结束,我的高中生活也就这样草草结束了,我始终没有对奕表达过我的心意,中学的青春就这样带着残缺和那份秘密结束了。奕考去了S城她一直向往的一所音乐学院,而我考去了偏远一点的一所重点大学,我们就这样夹带着遗憾各自告别了。 大学刚开始的时候我还和奕在社交软件上时不时聊着各自的生活,不过随着时间流逝,我们也慢慢有了各自的生活和圈子。直到得知奕在大学里也谈了对象,我释然地又在学校操场走了一圈又一圈。之后我们的话也慢慢少了,聊天框也很久不再有新的消息,我慢慢地淡出她的世界了。 两年后有一天看到朋友圈奕发的一条图文,才发现原来她已经作为交换生去意大利留学了。习惯性地点了个赞,感叹彼此距离越来越远了,我点开了她的图片一张张仔细看着,比中学时代更漂亮了,眼睛依然很好看。 我忽然想起了一个人,林谙托付给我的信还没寄出。刚好现在算算也有四年了,我在一个长假期回到南风岛在老家的书柜角落里找到了那封已经发黄的信封。便匿名给奕所在的学校地址寄了过去,信寄出去之后又回想起了四年前拜访林谙时的种种闲聊,心中又是一阵发苦。 如今你也逝去已久,我也没勇气如你所说的书写我自己的故事。无论是你还是我,都慢慢淡出她的人生了。 大学四年似乎比高中四年还要快,我也谈了一段大学时期的恋爱,体验了青春爱情的甜蜜和烦恼。毕业后我收到了S城著名的A公司的offer,这些职业的发展倒是和当初林谙给我提的建议的路线如出一辙,他确实有点厉害。 毕业后的暑假我在南风岛无聊中度过,南风岛的夏天是如此的静谧,海风吹得令人慵懒地想睡觉。一天中午午休醒来,我翻了翻朋友圈,指尖停留在一条奕的图文上。 “南风知我意,吹梦到西洲” 背景图我认出来了是南风岛海崖边,我清楚的知道那里是林谙的墓碑。 她来南风岛了,去了林谙的墓碑。我猛地起身,点开聊天框想给她发条私信,停顿了半天却不知道该说啥。收起手机我便也往那海崖边走去。 太阳不是很晒,我站在海崖边远远望去,不远处林谙的墓碑前站着一个女生婷婷而立,白色的裙子随着海风飘扬,她眺望着远方,张开手拥抱着引面而来的风儿。今天的海风确实很舒服很温柔,像高三那个秋天吹进教室里她那时正在认真写作业的那个午后。 她在林谙墓碑旁的石头上刻下了一些东西,又呆了许久,便转身离去,在南风岛的树林,山崖,海岸边漫步着。我没有跟上去打招呼,而是也走到了林谙的墓碑前。墓碑前除了那鲜花,还有四句诗。 海水梦悠悠,君愁我亦愁。 南风知我意,吹梦到西洲。 我也采摘了一把鲜花放在林谙墓碑前便转身离开,还是不去相认了吧,毕竟她只是来看望林谙的。 两个月后奕坐上了出国的飞机出国留学去了进修硕士,而我也坐上了去S城的火车开始上班了。时间真的过得很快,青春也就这样咋眼就过去了。我日复一日地挤地铁上班,吃饭睡觉玩游戏,上班后也没再交什么新的朋友,同事间也都是只是同事关系,学生时代的朋友也渐渐不再联系。偶尔在朋友圈看到便点一两个赞,仅此而已。 那些学生时代的故事和懵懂的情感,遗忘的遗忘了,消散的消散在S城夏夜的晚风里。我们不知不自觉成为了一名大人,一个个有边界感互不打扰的,彼此不再有什么交集的成年人。 在A公司按部就班地上了四年乏味的班后,有一天看到公司外派到挪威分部驻扎的内部招聘。可能是厌倦了现在一成不变的生活,可能我也一直想出去看看,也可能还有其他我自己都不知道的潜意识里的向往,我申请了这次外派的机会。 其实来挪威之前我都不知道奕也在这,甚至来这边之后很长的一段时间我都没再想起她和曾经和她一起经历的种种故事。她已经被我深埋在记忆角落里,不再被挖掘出来。 直到有一天我在海崖边散步,发现了那个刻字的墓碑,才明白奕她应该也在这。不过我也没去寻过她,该相遇的总会相遇的,该逝去的终会逝去。 我就这样在挪威度过了十个年头,之后又从A公司离职,买下这个小店当起小店老板。时间如流水一般过去,我也变成一个善乏可陈的中年男人。这个城市的生活如同南风岛一样静谧,慢悠悠的生活。两个月前一位顾客来到我的店里,她让我感觉很熟悉,但我也怎么想不起来应该是谁,我和她聊了很久,直到她挑了两件商品出门,我问了她的名字。 她说她叫奕。 九、再相遇 店主说完了这个漫长的故事,壁炉里的柴火已经烧的差不多了,火势小了很多,若有若无的火苗在暗红的木炭上跳动。他不再继续说些什么,只是安静地等待着我开口。 “所以你是陈北,你变化好大,以前你可没这一茬胡子”。我苦笑着,快二十年没见的人,早已模糊了样子,以前究竟是怎样的情感,我想无论是我还是他都已经淡忘了。我抬头看了一眼他,依旧那么有神。 二十年前在学生时代的记忆碎片被我慢慢补全了,那些人的身影和欢声笑语也在我的脑海里逐渐浮现。陈北确实陪伴了我好久,那时候我有时总感觉在陈北他的身上能看到林谙的一些影子,但是我又不想把他当作林谙,毕竟他就是他,陈北也有他独特的优点和温柔。 中学时代少女的敏感心思其实早早就看出了陈北对我自己的那些超越友谊的心思和情愫,但是我不舍得去戳穿而失去一个好朋友,他也没有和我坦诚,我们就这样互相保持默契一直到毕业。 相比与林谙坦诚且热烈的表白之后再坦诚地克制爱意,陈北他像是更喜欢一个人独自隐忍地完成这场陪伴,我看着眼前多年再相遇的有点快认不出来的面孔,心中情感复杂无法言说,有难过也有感动,有开心也有遗憾。 “那,那个海边的墓碑不是你立的吗?”,我想起了他讲述的故事里的这个关键疏漏,有点疑惑地问他。 “不是,我来挪威的第二年发现了那个墓碑,我以为是你立在那缅怀他的”。陈北苦笑着说道,将手中早已凉了的茶饮尽,又重新倒了杯在捧在手中晃悠了下,“毕竟能在墓碑上刻那四句诗的人,除了我就只有你了” “看来我忘记的事情还真有点多”。 我站起身,走向窗台,空气中一片片鹅毛般的雪花缓缓落下,堆积在窗台边缘,铺成一层雪白的地毯。下雪了,这是我在挪威的第几个冬天了。 陈北也起身走到窗台边和我并列站着,静静看着窗外的雪花落下,过了许久他转过身看着我。 “不过这些都已经不重要了,等过完这个冬天,明年开春的时候,我带你去我发现的另外一处挪威的海岸,那里每年开春都长满漫山遍野的野花,那里是我的秘密基地”。 秘密基地,这句话好熟悉,好像有人曾经也带我去过他的秘密基地。 我点了点头答应下来。 小店外的街道已是白茫茫的一片,小店在泛黄的灯光笼罩下显得格外温暖。整个小城在这场雪的笼罩下也显得格外的安静。 “陈北,很多年过去了,过往的故事就留在心底吧,这个冬天的这场雪我又遇到了你。你可有勇气准备迎接好这场大雪纷飞?”,我如此平静地问他,呼出的气息在窗户结成一层白霜,我们眺望着远方,目光穿过海岸线,达到很遥远地地方。 让我与你握别 再轻轻抽出我的手 知道思念从此生根 浮云白日,山川庄严温柔 让我与你握别 再轻轻抽出我的手 华年从此停顿 热泪在心中汇成河流]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/记忆的种子·二-29fe4821.webp" />
</item>
<item>
<title>谙风 | 小城中学</title>
<link>https://gitpull.cn/post/20190605-4/</link>
<guid isPermaLink="false">谙风-小城中学-7712d2d6</guid>
<pubDate>Wed, 05 Jun 2019 00:00:00 GMT</pubDate>
<author>邻家酒肆</author>
<category>谙风</category><category>随想</category>
<description><![CDATA[大巴车在乡间小路慢悠悠地跑着,离海岸线越来越远,最后渐渐望不见海岸,只有路旁山坡的野花不断地进入视野,林谙还是被一路晃荡的车厢弄醒,无法入睡,只好靠在车窗上看着眼前不断闪过的小树和远处慢慢向后退的小山丘。九月份的山花开得很是潇洒,漫山遍野的红红绿绿似乎要溢出山谷流到马路上,流到路旁的农舍房顶上。路旁的狗尾巴草和小池塘边的芦苇倒是歪歪扭扭的不像样子,偶尔几只海鸟缓缓地从半山腰飞回,叼着树枝回去重建被台风吹毁的巢穴,大巴车呼啸而过的乡间小路…]]></description>
<content:encoded><![CDATA[大巴车在乡间小路慢悠悠地跑着,离海岸线越来越远,最后渐渐望不见海岸,只有路旁山坡的野花不断地进入视野,林谙还是被一路晃荡的车厢弄醒,无法入睡,只好靠在车窗上看着眼前不断闪过的小树和远处慢慢向后退的小山丘。 九月份的山花开得很是潇洒,漫山遍野的红红绿绿似乎要溢出山谷流到马路上,流到路旁的农舍房顶上。路旁的狗尾巴草和小池塘边的芦苇倒是歪歪扭扭的不像样子,偶尔几只海鸟缓缓地从半山腰飞回,叼着树枝回去重建被台风吹毁的巢穴,大巴车呼啸而过的乡间小路也随处可见倒掉的已经满是铁锈的广告牌和被大风吹得漫天飞的黑色垃圾袋。 这些并不让林谙感到新奇,每年的台风过后南风岛也都会这样的一片狼藉,林谙早已习惯了台风,习惯了在海风中行走的日子。 车子走了快一个小时的路程,已经看不见小山包和乡间小路了,路变得宽敞了,高大的路灯和站牌从车窗外呼呼呼的一闪而过。房子逐渐变多,五颜六色而又形状奇怪的外墙,整齐却又不见尽头的绿化带,不远处还有高耸的玻璃幕墙,一切都和南风岛不一样,但林谙也只是静静地看着,内心并没有什么波动,大城市的样子在电视上已经见多了,并不会有什么新奇。他所期待的是他将要呆三年的新莆四中,那是小城最好的高中,林谙也是南风岛考进新莆四中的两个人之一,因此家族里的人也对林谙寄予厚望。 从小学开始林谙就被贴上好孩子好学生的标签,他自然也是小伙伴的父母们口中的别人家的孩子。然而这也让他很苦恼,每次去朋友家玩朋友的父母都要当着朋友和林谙的面夸林谙学习好,听话又乖巧,让林谙不知道该找什么借口把朋友拉出去玩。但也因为他既温柔儒雅又有时活泼机灵的性格让他收获了一群童年的玩伴,还有他的一个发小,一个对他来说很重要的女孩子。 大巴车开始驶入市区,周围的环境也开始变得喧嚣,堵在路上的小车在不耐烦的鸣笛,刺耳的声音让林谙意识到快到了。不一会儿车子停止了发动机的轰鸣,车上的人开始躁动起来,年轻的小伙子迫不及待地站起身拖拽着行李架上的东西,女人们则慢慢地伸了个懒腰,再催着旁边的男人赶紧拿好东西下车。林谙对着车窗里模糊的·人影理了理头发,其实头发并不长,但还是下意识地想要看一下自己。 一米八的身高在狭小的车厢里并不能完美的展现出来,但消瘦又不失儒雅的脸颊依然让人醒目,深邃的眼窝和干净利落的短发,并不很帅却让人看着舒服。再三检查没有东西落下后林谙也跟着其他人一起下车去。 下车后的林谙深吸了一口气,外面的空气着实比车内压抑的味道要好得多,让人瞬间没了睡意。大巴车是停在新莆城的新汽车站,林谙知道在这里只要乘坐8路车就可以直接到达新莆四中,只是车站来来往往的车辆让人有点眼花。 拉着行李箱林谙直直的站立在车站广场,侧耳聆听仿佛周围的行人都在围绕在他转圈,将左手插进裤袋,他发觉自己并不想掏出什么,于是又尴尬伸出手,若无其事地摊了摊手。不远处的停靠站一辆路车正在靠站,车门一开便涌出了一群形形色色的男人和女人,然后瞬间散开,消失在人群中,就像往空气中吐出的烟圈,弥散在空气中无影无踪。回过神来的林谙看了一下车头,8路车!迟疑了一下他还是拉着行李箱往路车跑去,“让一下,对不起让一下”绕开路人,一路小跑。然而在路人回头用疑惑的目光的目送下,林谙跑到站台时车门还是关了,缓缓地启动。 “等一下啊” 路车并没有为他停下,只留下空气中被卷起的尘土,路旁行道树刚被车窗刮下的树叶慢悠悠地从他头顶飘落,落在了行李箱上。林谙拍了拍落叶和灰尘,把行李箱提上站台,才发现小城这边已是晴天,找不出台风过后的痕迹,或者台风本来就没经过这边? 林谙心里暗想着,或许新莆城和南风岛的区别是没有台风?停靠站对面是一栋刚建成的大楼,脚手架还没完全拆掉大楼楼顶已经挂出了巨幅的广告横幅:倾城炬献,学区房9998一平米,零首付。 从新车站和林谙一起上车的人很多,大多人像林谙一样站着,有戴着耳机在自己的世界中摇晃着身体,有的则低头盯着手机屏幕痴痴地笑着。林谙也有手机,只是并不是什么高端手机他也没什么兴趣去把玩,倒是车窗外不断出现的各种小吃店和服装店放的音乐更能吸引他的注意。店面门口不停闪烁的灯光合着音乐的节拍让林谙握着栏杆的手指也跟着轻轻地敲打着。 路车开过了六七个停靠站,喇叭里的女声提醒人们新莆四中已经到了。林谙缓过神来,心跳莫名地加快了一下又平歇下来,便提着行李箱跟着一群也是学生模样的人下了车。路车就停在新莆四中大门口,像站在车站广场的时候一样,林谙呆呆地立在新莆四的大门口。 他也不知道为什么要在门口这样站着,但是他知道不能就这么简单地平凡地走进去,就像要完成某个仪式才能让这个过程变得有意义或者有纪念意义,看看学校大门,小声的说了声“你好,四中”。 林谙又觉得自己刻意的做法和想法有点幼稚,于是看看周围有没有没人听见,便拉着行李箱径直往校门走去。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/谙风-小城中学-7712d2d6.webp" />
</item>
<item>
<title>【PCC】一个用python写的c语言编译器</title>
<link>https://gitpull.cn/post/20190101/</link>
<guid isPermaLink="false">PCC一个用python写的c语言编译器-15991910</guid>
<pubDate>Mon, 31 Dec 2018 16:00:00 GMT</pubDate>
<author>兰州小红鸡</author>
<category>杂七杂八</category><category>编译器</category><category>python</category>
<description><![CDATA[PCC——python实现编译器 大学的编译原理课设,实现源码到汇编代码的翻译,链接部分使用gcc的功能。目前支持数组,四则运算,赋值,判断,输出,循环语句等。 项目地址:http…]]></description>
<content:encoded><![CDATA[PCC——python实现编译器 大学的编译原理课设,实现源码到汇编代码的翻译,链接部分使用gcc的功能。目前支持数组,四则运算,赋值,判断,输出,循环语句等。 项目地址:https://github.com/flymysql/Py Compiler 源码说明 1. lexer.py 词法分析器 2. get\ predict\ table.py 生成预测分析表 3. LR.py 非递归的语法分析器 4. generate.py 中间代码生成 5. to\ asm.py 汇编代码生成 6. pcc.py 入口函数 使用 命令说明 pcc o (filename) 直接编译生成可执行程序 pcc s (filename) 生成汇编源码 pcc t (filename) 查看语法树生成过程 pcc l (filename) 查看词法分析 pcc h 查看帮助 pcc p 查看本编译器的预测分析表 pcc g 查看本编译器的语法推导 exit 退出 编译代码 声明语句和赋值语句 数组 数组下标,即\[\]中内容也可以用表达式嵌套 输出语句 目前printf语句的参数最多可以带三个参数 例子 控制语句 目前仅支持if判断,可嵌套使用 while控制语句 这个不多说,也是可嵌套 举个栗子(打印99乘法表) 源demo 举个栗子(打印斐波那契数列) c语言源码 生成的中间代码(四元式) 生成的汇编代码 其他命令自行发觉hhh]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/PCC一个用python写的c语言编译器-15991910.webp" />
</item>
<item>
<title>桃李春风一杯酒,江湖夜雨十年灯</title>
<link>https://gitpull.cn/post/20190101-2/</link>
<guid isPermaLink="false">桃李春风一杯酒江湖夜雨十年灯-15995716</guid>
<pubDate>Mon, 31 Dec 2018 16:00:00 GMT</pubDate>
<author>兰州小红鸡</author>
<category>杂七杂八</category>
<description><![CDATA[最终我还是选择放弃考研了。感到无比的轻松。 01 准备考研的大学生好多,我也是其中之一,准备了大半年,数学快复习完了,专业课也复习一半了。一切都进行地井然有序,如果我没有放弃考研的…]]></description>
<content:encoded><![CDATA[最终我还是选择放弃考研了。感到无比的轻松。 01 准备考研的大学生好多,我也是其中之一,准备了大半年,数学快复习完了,专业课也复习一半了。一切都进行地井然有序,如果我没有放弃考研的话,6月份之前应该就可以把数学和专业课第一轮复习完。后面六个月足够我把所有都复习一遍。 02 然而这一切都戛然而止,我还是选择了放弃。在朋友眼里这显得有点突然。 我今年22岁了,依然是个经济不独立的理科生,三年的研究生时间,是个很大的时间代价,尤其对于一个一无所有且想在30岁之前有所作为的年轻人。 所以为了能对得起这个时间成本,我定了个很高的目标学校。 很长一段时间都不敢和人说这个目标,毕竟我也不像什么学霸,大家大都是平庸之辈,都要把梦想藏得好好的,怎么能拿出来让人笑话呢。 03 三月,各大互联网公司开始春招,本来我一直视而不见不想去碰,不想自己考研的节奏被打乱,然而三月初一个腾讯的面试电话打过来乱了我的思绪。那是我大二随手投的简历,没想到大三这时候被捞了起来。心就开始乱了,但还是很坚定要考研的,想着就当刷一刷面试经验。 三轮面试过后,无缘腾讯,说实话很失落。 继续复习考研,想要找回节奏,三月中旬华为来学校招聘实习生。我再次投了,这次倒不是为了实习,只是想做证明一下自己还是有点能力的。 四月收到华为offer,再次陷入沉思。没收到offer很失落,收到offer又很纠结,或许要考研的人就不应该去面试,断了后路才能背水一战。然而我没那个勇气自断后路,在纠结中继续复习。后面又投了支付宝的面试。 其实内心还是很想早点去工作的。 04 过年的时候在家里和老爸还有一个叔叔喝茶,那个叔叔谈到我以后在哪里工作结婚买房的问题,我爸苦笑着,一脸歉意的和我说,这些都得靠你自己了 我鼻子一酸,说没事我本来就是要靠我自己的。 鼻子酸的原因是我不想看到爸爸那个无能为力又内疚的表情,他们付出已经够多了,我也没想再得到什么,只是不想让他们对自己不能帮到我什么而感到内疚。这个担子本就应该由我来承担。 只是考研的话,意味着又是三年没有收入来源的时间。爸爸说没事你读研吧,家里情况过一两年就好了,盖房子的欠款也快还清了,你放心大胆地去读,读到博士也没关系。 读完研我就26了,如果30岁之前结婚,4年时间我该如何给未来的人和家庭充分的物质生活。厦门岛外80平地房子两百多万,深圳上海北京就不用说了,为了机遇我也想去北上广工作,可是无力感袭来。会怀疑自己到底能不能在城市站住脚。 教育真的是建立在物质上的,物质优越的人会有更大概率获得优质教育,而这又很大概率让他们成为优秀的人,周而复始,差距越来越大。 05 某一个微风徐徐的下午我去澡堂洗澡,黄昏时分,路上行人缓缓,满是春天的味道。我在澡堂的喷头下淋雨,没有声音,然后就哽咽了,自己也不知道自己为何控制不住,不知从何而来的情绪袭来,突然就泣不成声,没有任何缘由。 压抑太久了,自己也不知道自己有压力,只是机械地重复每一天,复习,学习,提升自己,很充实也很快乐,以后的事情不去想就是了。可能哪天在一个安静地无人地地方空闲下来,莫名的悲凉立刻就涌上来了,抵挡不住。 考研压力真的很大,不像高考,成绩是看得见的,可以很清除地知道自己是什么水平,在什么层次上。而考研就像黑屋子里洗衣服,不知道洗干净了没,也不知道哪个地方没洗干净,只有等到开灯了你才能知道结果。 这时候华为的offer对我来说就像一种救赎,在黑夜里赶路的人不知道终点有多远,也不知道自己走的路是否正确,只有一个虚无缥缈的终点信念支撑着,或许哪个终点根本就不存在,这时有人对你说,跟他走吧,他能带你去另一个终点,你也不用在迷茫了。多么诱人,心一下就乱了,给平庸的我一丝希望。 考研考不上怎么办,我想我是绝对不可能二战的,那就工作吧,只是这一年的复习时间,什么项目也没做,很难找到大厂的职位,这也是时间代价。其实考虑的因素有很多,我也不知道这个决定是正确还是错误的。也永远无法知道,两条路你只能知道其中一条路的终点是什么。 06 想起过年的时候堂哥问我毕业后要去创业吗,我苦笑着,拿什么创业。虽然现在我仍然一无所有,但我不能30岁的时候也一无所有吧。 创业成功或许能让我一次飞跃上一个阶级,但是我也无法承受创业失败的风险。有人说,能有什么风险?不就从头再来吗?对,是从头再来,说的是很洒脱。而那几年的时间是多大的成本我想没有一个普通家庭的年轻人能够承受的。毕业就去创业在我看来更像一场赌博。 这辈子最讨厌的就是赌博。 当然不是说创业者坏话,还是很敬佩那些年轻的创业者,但是毕竟一将功成万骨枯。 07 或许这应该是大部分90后的真实现状吧。又一次站在人生的十字路口,到了扛起责任的年纪,到了该为自己未来负责的时候了。我们都是第一次面对这样的压力,面对魔幻的房价,面对零零碎碎的生活,面对两难的选择。 此时你回想起十一二三岁那年推着自行车走在放学回家的小路上,田边黄狗刨地,树上鸟儿轻吟,揣着一兜的弹珠像个威风八面的小将军。 仿佛看到少年嬉皮笑脸地站在面前,心疼地对你说,要努力生活哦! 鲜衣怒马的少年郎,一晃十年时间已过,坐在图书馆自习室靠着窗台,生活依然像回忆一样安静,或许也只是最后的安静吧。 十年前小学毕业那天,班主任说,十年后我们在这里聚一次。 那天我和发小走在回家路上,路上竟然很是安静,没什么人,也没什么特别的事,脑袋中想着十年那可是好遥远的事情呢。 这么遥远的事情尽然就在眼前了,仿佛午后四五点午睡醒对着昏暗的房间,昏昏沉沉时间就没了。 初二的时候一位物理老师问我的理想是什么,我说我想成为很厉害的人,那种所有人都能记住我名字的。然而现在看来,这个理想还有点虚无缥缈,我连大学老师都没几个知道我的名字,平庸地像所有平庸的人。 其实我一直是个自谕不凡的人,即使未来可能连大城市的房子都买不起,但我还是抱有幻想,你说这江湖,下一个十年会不会出现我的名字?]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/桃李春风一杯酒江湖夜雨十年灯-15995716.webp" />
</item>
<item>
<title>模仿知乎的链接卡片</title>
<link>https://gitpull.cn/post/20181216/</link>
<guid isPermaLink="false">模仿知乎的链接卡片-a714f04b</guid>
<pubDate>Sun, 16 Dec 2018 09:13:47 GMT</pubDate>
<author>兰州小红鸡</author>
<category>教程</category><category>前端</category>
<description><![CDATA[今天逛知乎发现他们的链接卡片挺好看,就想写一个自己拿来博客用(扒下来知乎的代码),效果如下。 源码放在github,感兴趣的朋友可以自己拿。 兰州小红鸡的博客 小鸡背单词 gith…]]></description>
<content:encoded><![CDATA[今天逛知乎发现他们的链接卡片挺好看,就想写一个自己拿来博客用(扒下来知乎的代码),效果如下。 源码放在github,感兴趣的朋友可以自己拿。 兰州小红鸡的博客 小鸡背单词 github | 模仿知乎的卡片链接 如何使用 在需要把链接转换成卡片的地方,将 标签的 设置为 。 然后再后面引入js文件就好了 例如 ¶实现代码 就是直接地修改 标签地dom内容,好像没什么好说的,样式我是直接从知乎扒下来的。 模仿知乎的卡片链接]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/模仿知乎的链接卡片-a714f04b.webp" />
</item>
<item>
<title>Windows下的终端折腾——弃坑msys2,开始用wsl</title>
<link>https://gitpull.cn/post/20181214/</link>
<guid isPermaLink="false">Windows下的终端折腾——弃坑msys2开始用wsl-d6b124c4</guid>
<pubDate>Fri, 14 Dec 2018 02:50:18 GMT</pubDate>
<author>兰州小红鸡</author>
<category>教程</category>
<description><![CDATA[之前用了半年Linux,后来因为平常一些软件需要,只能在Windows下玩,就换回Windows了。 但还是万分想念Linux,然而Windows下的终端一直用的不爽,就一直在尝试…]]></description>
<content:encoded>< 在Ubuntu或Debian下我们可以通过 apt get 命令 很方便的安装/卸载软件,由于默认的软件包仓库是位于国外的,安装软件的时候就可能遇到各种网络问题或者下载到的一些资源不完整,因此就需要切换数据源为国内的镜像站点来改善。 在这里我使用的是阿里云的数据源: 更新配置 注:14986版之后更新了内核,第三方的镜像站可能找不到软件包资源,需要切换回官方的源。经测试中科大的源可用 ¶与vscode搭配使用 将vscode的终端换成wsl,真是赏心悦目呢 在设置里面将 中的 换成 就好啦 vscode真香 😀]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/Windows下的终端折腾——弃坑msys2开始用wsl-d6b124c4-03.webp" />
</item>
<item>
<title>12月6日杂谈</title>
<link>https://gitpull.cn/post/20181206/</link>
<guid isPermaLink="false">12月6日杂谈-5027c817</guid>
<pubDate>Thu, 06 Dec 2018 09:13:48 GMT</pubDate>
<author>兰州小红鸡</author>
<category>随想</category>
<description><![CDATA[今天是12月6日,周四,没课 小雪,温度零下好多度 早上去了会儿图书馆,没看下书,下午在宿舍荒废了一下午,满满的罪恶感。 18年就剩下20多天了,这学期开学前立下的目标,慢慢丢失的…]]></description>
<content:encoded><![CDATA[今天是12月6日,周四,没课 小雪,温度零下好多度 早上去了会儿图书馆,没看下书,下午在宿舍荒废了一下午,满满的罪恶感。 18年就剩下20多天了,这学期开学前立下的目标,慢慢丢失的状态。 考研的路上总是充满自我怀疑吧,好久没开始复习计划了,感觉挺痛心。 大概需要一个一个契机,重新找回状态]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/12月6日杂谈-5027c817-01.webp" />
</item>
<item>
<title>小程序开发之接入Bmob后端云作为后端数据库</title>
<link>https://gitpull.cn/post/20181203/</link>
<guid isPermaLink="false">小程序开发之接入Bmob后端云作为后端数据库-93621394</guid>
<pubDate>Sun, 02 Dec 2018 23:37:40 GMT</pubDate>
<author>兰州小红鸡</author>
<category>教程</category><category>微信小程序</category>
<description><![CDATA[前几天在网络课的项目汇报中,我展示了一个背单词的微信小程序,后端使用的是bmob的数据库。 今天有同学来问我怎么用那个数据库,我当时可能没讲清楚,索性写篇博客来说一下。 接入 Bm…]]></description>
<content:encoded><![CDATA[前几天在网络课的项目汇报中,我展示了一个背单词的微信小程序,后端使用的是bmob的数据库。 今天有同学来问我怎么用那个数据库,我当时可能没讲清楚,索性写篇博客来说一下。 接入 Bmob后端云 做一个简单的总结。所谓后端云,一句话概括就是跑在云端的数据库后台+服务器后台,引入到微信小程序开发中能带来的好处就是:让我们可以专注于小程序本身的业务逻辑开发,而不用去管复杂的后台服务器 当然,这种免费的第三方数据库只适合平常做着玩的小项目,和那些比较水的校创项目 长期发展的话,建议自己买一台服务器手动搭建数据库,后续有空的话我会再写一篇自己搭建后端服务器的教程 ¶接入Bmob后端云 准备一个小程序公众号和Bmob账号 首先需要到微信公众平台官网上去注册一个小程序类型的公众号,假设将要开发的小程序命名为:MyApp. 打开Bmob官网注册一个账号。 获取并记录好MyApp小程序的 和 这两项信息在小程序后台的"设置-开发设置"页面可以获取到,获取到后需要在一个文本文件中记好, 后面要用到 。 ¶登录Bmob控制台 创建一个应用,假设名字叫 ,然后进入应用。到"设置"页面输入刚刚获取到的小程序的 和 并保存。 获取并记好 对应的 和 . ¶登录小程序MyApp后台 到 设置-开发设置-服务器域名 页面添加Bmob安全域名并保存(可一次性添加多个)。 注:四种安全域名(两种类型:https和wss) 全部填 和 ,其中"xxx"为 的 . ¶下载SDK 到Bmob官网下载微信小程序对应的 并解压,将其中的所有 文件都放到小程序工程的 utils 目录下。 ¶初始化和引入Bmob 在小程序工程的 中加入如下代码进行全局初始化: 在需要用到Bmob的page页的js中引入Bmob: 现在就可以在小程序中对Bmob后端云数据库进行各种操作了,像操作本地数据库那么简单。 ¶小程序相关api的调用 注意:调用数据库api之前你需要在bmob云后台创建相应的数据库 有数据库才能进行相应操作 比如一个简单的用户登陆如何操作呢? ¶用户操作 2018年5月1号起,微信官方彻底废除wx.getUserInfo 函数,如需获取用户信息,需要使用按钮获取。 wxml: js: wxml显示 请求示例: 返回示例: 如上的简单操作,我们就可以使用bmob云数据库作为我们小程序的后端数据库啦 详细api文档还请阅读bmob官方api文档 bmob云后端api文档 ¶关于小程序审发布或更新 小程序上线需要经过审核、发布两个过程。 审核通过后有全量更新、或者分阶段发布,小程序才会更新,首次发布没有选项。 1. 全量发布:即时向全量微信用户发布新版小程序。 2. 分阶段发布:新版小程序将在15天内以开发者自定义比例,向微信用户发布更新 3. 详情见知乎:发布小程序时选择全量发布和分阶段发布是什么意思? 不得不说小程序审核速度是非常快的,通常小程序没有涉及政策不允许的内容或者超过小程序允许的应用服务类型,都是可以顺利通过,初次体验,即便在国庆期间,也是有工作团队进行审核,审核时间通常在几小时内。 ¶未完待续 小程序一直处于不愠不火的状态,但微信团队一直坚持在更新,都在不断的更新或者修复异常bug,有时间希望做些更有趣的小程序和大家分享。]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/小程序开发之接入Bmob后端云作为后端数据库-93621394.webp" />
</item>
<item>
<title>使用vue.js制作carousel跑马灯组件</title>
<link>https://gitpull.cn/post/20181202/</link>
<guid isPermaLink="false">使用vue.js制作carousel跑马灯组件-456bc465</guid>
<pubDate>Sun, 02 Dec 2018 07:53:59 GMT</pubDate>
<author>兰州小红鸡</author>
<category>前端</category>
<description><![CDATA[前几天在掘金上看到一个开源项目(PyUI)在招人,一个基于vue.js的UI库,想着反正页闲着,就硬着头皮加入了(其实我都还没学vue.js) 两天速成了一下,学了点皮毛,就开始撸…]]></description>
<content:encoded><]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/../assets/uploads/2026/06/covers/使用vue.js制作carousel跑马灯组件-456bc465.webp" />
</item>
<item>
<title>Dijkstra单源最短路径算法</title>
<link>https://gitpull.cn/post/20181123/</link>
<guid isPermaLink="false">Dijkstra单源最短路径算法-15e41f53</guid>
<pubDate>Fri, 23 Nov 2018 09:32:33 GMT</pubDate>
<author>兰州小红鸡</author>
<category>前端</category><category>算法</category><category>C++</category>
<description><![CDATA[算法动态演示地址 今天用c++撸了一遍Dijkstra单源最短路径算法,做个记录,先看下算法的描述 ¶问题描述 给定一个带权有向图 G=(V,E) ,其中每条边的权是一个非负实数。…]]></description>
<content:encoded><![CDATA[算法动态演示地址 今天用c++撸了一遍Dijkstra单源最短路径算法,做个记录,先看下算法的描述 ¶问题描述 给定一个带权有向图 G=(V,E) ,其中每条边的权是一个非负实数。另外,还给定 V 中的一个顶点,称为源。现在我们要计算从源到所有其他各顶点的最短路径长度。这里的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。 Dijkstra算法的解决方案 Dijkstra提出按各顶点与源点v间的路径长度的递增次序,生成到各顶点的最短路径的算法。既先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从源点v 到其它各顶点的最短路径全部求出为止。 ¶Dijkstra算法的解题思想 将图G中所有的顶点V分成两个顶点集合S和T。以v为源点已经确定了最短路径的终点并入S集合中,S初始时只含顶点v,T则是尚未确定到源点v最短路径的顶点集合。然后每次从T集合中选择S集合点中到T路径最短的那个点,并加入到集合S中,并把这个点从集合T删除。直到T集合为空为止。 ¶具体步骤 1. 选一顶点v为源点,并视从源点v出发的所有边为到各顶点的最短路径(确定数据结构:因为求的是最短路径,所以①就要用一个记录从源点v到其它各顶点的路径长度数组dist\[\],开始时,dist是源点v到顶点i的直接边长度,即dist中记录的是邻接阵的第v行。②设一个用来记录从源点到其它顶点的路径数组path\[\],path中存放路径上第i个顶点的前驱顶点)。 2. 在上述的最短路径dist\[\]中选一条最短的,并将其终点(即v,k)k加入到集合s中。 3. 调整T中各顶点到源点v的最短路径。 因为当顶点k加入到集合s中后,源点v到T中剩余的其它顶点j就又增加了经过顶点k到达j的路径,这条路径可能要比源点v到j原来的最短的还要短。调整方法是比较dist\[k\]+g\[k,j\]与dist\[j\],取其中的较小者。 4. 再选出一个到源点v路径长度最小的顶点k,从T中删去后加入S中,再回去到第三步,如此重复,直到集合S中的包含图G的所有顶点。 ¶C++代码 感觉代码还是有点臃肿了 晚点打算再写一个 实现的算法 结合html来制作图形界面 ¶2018 11 24更新 花了一早上写了 版本的,并能够动态演示过程 演示地址 ¶部分javascript代码]]></content:encoded>
<media:thumbnail url="https://gitpull.cn/assets/uploads/2026/05/Dijkstra单源最短路径算法-15e41f53-01.webp" />
</item>
</channel>
</rss>