MoreRSS

site iconFernweh修改

主要从事系统安全、云安全和AI4Security研究。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Fernweh的 RSS 预览

文章分类与统计

2026-11-05 08:00:00

已发布文章总数:326
最近一次发布日期:2026-11-04

Category Last Updated Date Count
随笔与诗 2026-11-04 104
漏洞研究 2025-12-02 56
攻防实战 2022-10-20 44
混杂分类 2026-10-20 34
云计算与安全 2022-09-19 28
程序分析 2024-04-20 21
读书笔记 2023-11-04 12
操作系统 2023-03-22 10
编程语言 2019-08-10 8
人工智能 2026-07-08 6
摄影 2023-10-02 3

怀念沈坚老师

2026-11-04 21:03:12

沈坚老师是我在同济大学读书时的专业启蒙老师。

很幸运,我参加了沈坚老师在那几年给计算机系学生开设的几乎全部课程。从大一的“高级语言程序设计”(高程),到大二的“数据结构”,再到大三的“计算机网络”和“计算机系统实验”。本来,我和很多人是没有机会上沈坚老师的“数据结构”课程的,因为大二起我们就从四平路校区(本部)搬到了嘉定校区,沈坚老师在那一学期恰好在本部给数理强化班的同学们上“数据结构”课。然而,面对大家搬嘉前后的热情和遗憾,老师可能也不愿意放弃刚刚跟着他入了门的小菜鸟们,为我们嘉定的学生开设了交作业的网站账户和“数据结构”课程讨论群,给我们发课件,定期线上视频授课、群里答疑。

那时没有 GPT 也没有 Copilot,IDE 是 Code::Blocks;没有 Zoom,线上授课用的还是 QQ。

计算机系的新生们水平参差不齐,有搞竞赛的高手,更多的是没有系统接触过编程和算法的同学。这种情况下,一上来直接“面向对象编程”,直接 C++,是有一定难度的。在大一第一节高程课上,沈老师说,可以到一个月之后再回过头来看看自己的变化,你会发现自己有进步了。一年之后的课上,沈老师说,可以和去年这个时候的自己对比一下。后来的大学时光中,每当我克服困难挑战做到一件事情,都会想起大一时第一次高程课上沈老师说过的话,那时自己真的是什么都不会,老师是真的有信心。

沈老师的课很有特色,有鲜明的“沈坚风格”,从大一到大三,一以贯之。桃李满天下,经典永流传——精心维护的课件、颇有难度的大小作业、时而严肃时而幽默的群内答疑……还有课堂上面对学生的懈怠、懒惰,严词厉色,看似说的是和教学无关的话,多此一举;实则讲的是和前途有关的事,苦口婆心。高考之后的大学生活往往以自由散漫开始,离家千里,幸而有人能够在这个阶段引领你。对了,2014 年的圣诞节,沈老师带了三四盒费列罗巧克力到高程课上,送给每一位同学作为圣诞礼物。

仍然记得,沈老师反复讲过这么一番话——他说,身为计算机专业的学生,你们会发现自己学了一个学期的 C++,还停留在控制台窗口的阶段,界面那么丑陋难看,而别的专业的学生可能只学了一个星期计算机,就能在屏幕上画出漂亮的界面,就能让一辆小汽车从左边跑到右边了。你们是要做底层的东西,你们是别的专业的支撑。就好比盖房子,别人可能会去找一扇门、地板、窗户等等,而你们应该会自己制作一扇门。

道理依然是成立的,时间也过了太久。即使时间过了太久,道理依然是成立的。

几年时光匆匆过,细节已经无法数清,沈老师用无数个“构造函数”、“复制构造函数”构造出了学生的专业功底。沈坚老师帮助我打好了一个扎实的基础,这才有了后面深入探索的基础和机会。我想,本科毕业论文的致谢很好地总结了离校前夕的感受:

感谢沈坚老师。从大一到大三,您始终以高超的教学能力、对学生负责到底的态度和无限的真诚与爱帮助我们从一无所知的新手慢慢转变为具有扎实计算机功底的专业学生。同时,您以身作则,为我们展示了计算机专业人士应有的专业素养。您不止一次提醒我们作为计算机专业的学生,不仅要知道框架怎么用,还要知道框架怎么造。我将谨记您的教导。

2018 年毕业季,我发现沈坚老师在“计算机网络”课程群里冒泡了,于是就给他发了“老师,求合影”,他说十分钟后智信馆一楼见。

只能说人世间充满了各种意想不到,想不到那就是见到沈坚老师的最后一面。

这篇文章写得千头万绪,查阅了当年的日记,打开了同济的资料备份文件夹,无数的细节扑面而来,把我拉回到已然尘封的日子。

怀念您,感谢您——在迷茫无知的年纪,您让我们接受的教育、成为它该有的样子。

我的团长我的团:一个关于失败的故事

2026-10-20 11:39:40

在《【士兵突击】不是一个兵王的故事》的五年之后,B 站宝藏 up 主“宇文数学”终于发布了《我的团长我的团》的解读视频。我之前把前一个视频的听写下来,分享在博客上。这里直接转载一下“宇文数学”老师的“团长”解读视频的未删节稿,一并附上读音稿链接。

能够等到这个视频,真的十分有幸。


起折

(黑幕:词)

那座山叫南天门,

是我们中国的山;

因其在西南边陲,所以叫南天门;

南天门下揍是怒江,

上面有座桥,叫行天渡;

过了行天渡就是禅达,

是我们来的地方。

从南天门到禅达,从禅达到南天门,这就是我的团长,和我的团所有的故事了。

第一折 · 出国门

此时是西历 1942 年,去清王朝三十载,距共和国尚七年,偏有那么些个幸运儿,运交华盖地赶上这翻身碰头的年代,靠山山倒,靠人人跑,靠飞机冒着烟往下掉。天降斯人,遍地蚂蟥蟑螂,没摔死的开始往密林深处跑。

这伙溃兵来自五湖四海的败仗,收拢后被编入川军团出国作战,从中国滇西启程,预计前往缅甸国的英国机场,接受美国的军备整训,再去抗击马来亚国来的日本国敌军——杀气腾腾满全球;偏登机前为了减轻负重片个个赤手空拳,遇敌就穿了一条中国裤衩——力不如人万事休。

溃兵们在少校长官的带领下进到一间英国仓库,十几口子日本兵外面端着机枪看戏,前门追兵后门没路,屋子烧了起来。还能怎么样呢,就这么着吧。

(镜头:扫众人)

死生之地,有的慌,慌的叫阿译;有的莽,莽的叫迷龙;抖的叫不辣,馋的叫蛇屁股,楞的叫豆饼,握着豆饼手的叫郝兽医,突突个没完烦死人的叫烦啦——孟烦了。

火势越来越大,眼瞅着撑不过人物介绍,突然外面安静了。

(台词:我是你们团长)

来人自报家门,大号,龙文章,职衔,川军团团长。

承蒙搭救,这个叫龙文章的,先是谈笑间说死了前任团长,然后狠狠打了每个溃兵的屁股,打完屁股再晓之以理,一个高级长官,愿意去跟大头兵较真一些慷慨赴死之外的东西,连抬杠为生的烦啦都有些动容。

龙文章迫不及待扒了身上的中校皮,领着溃兵第一个钻了汽油桶。

出国前收容站招兵,时任川团团长叫虞啸卿,他向溃兵展示的是顶尖武器和军人之姿,虞团长挺成枪配着剑,迈四方步,那一折,叫王于兴师,修我戈矛。

出国后丛林逃杀,龙文章没头没尾的现身,讳莫如深的谄笑,龙团长扮山精勾黑脸,紧锣密鼓,这一折,叫岂曰无衣,与子同袍。

故事长,你慢慢听,我慢慢讲。

(台词:岂曰无衣,与子同袍)

第二折 · 李乌拉

第一场战斗来的很快,溃兵们摸黑进黑森林,在一个河谷口遇到了一伙架工事的日军,和地上熟悉的声音。

声音来自同个收容站生活过的溃兵,同批次招募,不同批次出国,现在被日本兵当肉靶子消遣,他已身中数枪,对方依然没打算放过这具没咽气的尸体,为了消遣。

他叫李乌拉,乌拉草的乌拉,第一个倒在日军枪口有名有姓的角色,最被忽略的角色。

李乌拉大号李连胜,前东北军上尉排长,收容站职阶仅次于烦啦的军官,也是收容站最不招人待见的兵,把自己活成菜市场的野狗,啃着偷来的西瓜被踩下楼,起来接着偷。

猪肉炖粉条,他擤鼻子;虞啸卿招兵,他亮眼睛;像是叫醒了魂。迷龙喊北上,虞团座说南下,那就南下,抵达缅甸担任了这支临时巡逻队的长官,小队被困,作为军官倒在冲击日军机枪的道路上,被戏弄成烂泥。

李乌拉所在军队曾经是鞋子最合脚的,砝码最重的,十一年前一枪不放入关来,十一年间从北败到南。日子在熬他,活着是因为没有死。

这是一个国军抗战的故事,抓一把蒋宋孔陈米,熬一碗水旱蝗药汤,文夕火煮开花园水,从吴江松浇到豫湘桂——吾国育吾民,李连胜是这支军队、在这个年代的底色,没权利选择开始,被承受所有结果,牌桌上左右命运,战场上消耗袍泽,勇气不是被打没的,一道放弃的还有生气,烂的最早的最彻底。每个人都提醒自己再烂也别烂成李乌拉,再打下去,谁也不敢保证自己不是下一个李乌拉。

(台词:你才二十四岁就跟人比烂咧)

李乌拉乘另一架飞机着陆,来不及见到龙文章。出门他又一次揣上希望,然后倒在冲锋的路上,和他经历过的所有溃退之路没什么不一样。六年前散了编制,意味着再无法用旧番号打回去见老娘,在老乡怀里,就算回东北了。

山魈从密林现身,结结实实的咬死了该死的人,羔羊口腔里有陌生的血腥,什么东西起了变化。

今天的溃败中多死了一个东北佬,迷龙沉默地背着老乡行进在队伍里,直到后背越来越轻,被龙文章斥责,放下躯体,继续前行。

第三折 · 救机场

集结上巡逻小队,沿途收拢像他们一样的溃兵,第二战在机场展开。

这是飞机本来该抵达的地方,有赖于日军的冒进,机场解围很顺利,溃兵——或者叫川军团,获得了补给。没拿枪时叫溃兵,有了枪还打了仗,可以叫炮灰了。炮灰在机场外围驻扎,就地协防。

联合英军,保卫滇缅公路,是川军团出国的目的。这条路的意义,往农业国的体外插一根工业国的管子,输送名曰工业品的营养,涓涓细流撑不起一片海棠叶,可当下已别无选择。

川军团出国前的口号是支援英缅、共拒日寇,到了才发现,英国佬一开始就丧失制空权,存着撤退的心思,喊着合围的口号,引来友军垫背,死道友不死贫道——打光最后一个黄种人之前,不列颠绝不投降。

(台词:法克鱿!)

像龙文章说的,跟英国佬打交道三尸神暴跳,奥赛罗们理解不了日不落绅士,凭什么那么傲慢,有炮火为什么不支援,以及为什么宁愿把物资烧成灰也不给友军。

傲慢并非针对,老大帝国平等地瞧不起所有其他国家、人种和文明;烧物资是怕被升级友军取代自己在东南亚的生态位;至于不支援炮火,可能想着在战俘营日子会好过一些。

对炮灰来说这些大面儿的东西想没想明白不要紧,英国佬正冒着炮火、举着电报兴师问罪,根据电文,有两个国家不认龙文章这个团长,一个叫中国,一个叫外国。

英国佬把电报摔在龙文章脸上讨要被骗走的物资,一边的烦啦拉动枪栓顶上对方胸口。

(台词:不存在)

机场守不住了,身份也露了,孟烦了不说当下不会兵变,但国内肯定晓得有问题了。战乱之秋冒团长之职,回去铁枪毙的过儿。孟烦了歇斯底里逼问龙文章下一步的打算,他太怕这个疯子可能的想法:里外是个死、索性拉着所有人一起下地狱。

孟烦了给龙文章起名死啦死啦,是有原因的。

死啦死啦生了一双好斗的眼睛,烦啦对那种眼神太熟悉了——每一场人命当数字的大仗前、指挥官的眼神,里面透漏的东西说好听点叫玉石俱焚,说直白点叫死不足惜——他会把母门所有人都害死,所以别怪小太爷先整死你。

刚进黑森林,烦啦就撺掇迷龙、暴起发难。龙文章嘻嘻哈哈地把一场哗变消解成了嬉戏。接着打赢了两场战斗,保了大伙儿命还达成了战略目的。

这下水涨船高,整死来不及了。武的不行,孟烦了只能来文的。

(台词:石头对话)

龙文章跟虞啸卿不一样,对生命有起码的尊重,对炮灰也有切肤的共情;龙文章跟虞啸卿一样,认可胜利的柴堆得人命去引火;龙文章跟虞啸卿不一样,但龙文章舍得跳进去一起烧,所以同样肆无忌惮。

这个时期的龙文章是最接近虞啸卿的。国家沦落至此,穿军装的都该去死,我让炮灰死得有价值,且鄙人不贪名不贪命,不同生死同穴、龙某随后就到,算对得起诸位了吧。

这里面的变数就是孟烦了,他直截了当地道破龙文章的心思,你壮烈是你的事儿,你想发光发热就琢磨扬了我们,你是自我感动,你是草菅人命。

从坠机开始,孟烦了恪守的原则就是把猪肉和粉条们的小命保住。意外发现龙文章有这个能力,所以有威胁时要给他整死,但有活路时要帮他隐瞒,走偏了也要给他修正,用烦啦的话说,我跟他们有交情,我跟你没交情。

这种修正和影响贯穿了烦啦和死啦的余生交集,他几乎就成功了。

(台词:你这颗脑袋太惹事了老板娘菜刀拿来)

趁夜,龙文章带着炮灰离开机场阵地,启程回国。

浓烟升起,英国佬体面地投降了,伴着小军鼓、爱尔兰风笛,再一次。

第四折 · 野人山

没后援、没侧翼,一群人就这么在遍布日军的丛林中游荡。

要麻,大名李四福,原川军团仅剩的渣滓, 死于一场遭遇战,毫无亮点的遭遇战。川军奔赴淞沪时,只来得及问国府要一个番号,左脚草孩右脚布孩的行军段子,训练低装备差出了名。川军一开始成建制伤亡,散了,就散入各部队继续伤亡。素色旗是一笔债,赖账的人要掉脑袋,于是从没有过的勇气;胜了叫国军将士,输了叫地方杂牌,于是从没有过的浪费。

一锅米粉,要麻不要辣,就着豆饼吃。

野人山回国要令人心碎得多,疟疾蚊虫杀的人比日本人杀的多。死人被慢慢拖死,活人要一遍遍眼睁睁看着战友被拖死。十万青年十万军,并不是说读过书的就不当死,而是十万知识青年,装过诗歌和代数的脑子,如果可以的话该当是留在脊髓里造血,特别是极度营养不良的身体——如果可以的话。

打狗不成反被追着屁股咬到家门口,士气衰败到极致,龙文章第一次上演白日招魂。

(台词:英国鬼死于狭隘和傲慢,中国鬼死于听天由命和漫不经心。)

从坠机疯到现在,人来了人走了,甚至没有时间去告别。人不光需要时间去愤怒,还需要时间去哀伤,他们并不天生比哪代人多坚强。半真半假的招魂挤出活人心肺的一口哀伤,炮声又近了,快起来走吧。

回撤路上迷龙捡了一车战利品,就大大咧咧准备退伍了。收容站时的迷龙是个成功的黑市商人,开罐头啃瓜瓤。虞啸卿招兵,别人反应是积极应招,他的反应就是挨个削,越积极越削。有人说迷龙舍不得溃兵们牺牲,想善良了。

迷龙几乎是跟李乌拉一块败到这步的,日子过得好不代表心安,迷龙永远在打人,他要出气,没人打的时候就诱人挨打再打人,他有出不完的气。

心里长了毛的人,见不得人高尚,身边的人上进会衬托自己的不堪,想平衡就得愤怒或者恶毒,烦啦用舌头,迷龙用拳头。

迷龙好处是心够大,拿得起就放得下,这点是烦啦的反面,某点上你有多讨厌孟烦了,就可能多喜欢迷龙——比如对女人。

(台词:我说你嫁给我)

龙文章循声而来,在阿译的诠释下,流兵抢民女就定性了。假团座斩真情种,迷龙一边疯狂逃窜,一边诶诶地叫着。孟烦了很鄙夷他,那支枪明明就不会响,诶诶诶,你诶什么诶嘛。

女人叫上官戒慈,得知这个叫龙文章的妄图处决自己的丈夫,一直克制的上官暴走了,端枪对着龙文章突突,混账保不了自己的国,还毁了别人的家,你当死上千遍。

(镜头:追杀)

烦啦说的没错,杀迷龙有立威的意思,三百人就敢打机场,现在拉出一千新死忠,他又发痒了。迷龙离队,一开始能忍是团长辜负了这个东北佬打鬼子的承诺,自知理亏;后来演变成为了女人,就是贪生,触及团长虞啸卿的一面,发了真狠。

(台词:毙了)

剧中用了大段场景和独白去描绘棺材的外形和被锻造的过程,那代表一个恋家鬼的婆娘炕头,一个死炮灰的生活追求,来人间一趟有自己想要活出的价值,即使炮火不过几里地,天上底下无限挤压,人性还是在棺木上开了花。烦啦告诉团长我们不想死,迷龙告诉团长我们想怎么活。

龙文章再次动摇了。

其实一副生机勃勃的棺材足以去达到表意效果,但小说中让上官用台词直接表述了出来,反倒落了下乘——况且毒蜘蛛和霸王树的比喻、也太不像人话了,

龙文章两个成为虞啸卿的节点,先被烦啦,后被迷龙,左右开弓摁住了,于是宕机,于是泄气,众人围上,龙文章忙里偷闲地死了片刻,队伍又出发了。

第五折 · 行天渡

怒江由来说法多样,有说源自沿岸怒族居民、即“怒人之江”;有说方言潞怒不分讹传叫成怒江;如果你觉得是形容一条江的脾气,也未尝不可。

过怒江两条路,行天桥和索渡,为了阻碍日军脚步,驻守对岸的特务营正往桥上安放炸药,而索渡竹筏边上挤着不亚于桥面的人群。

(台词:争渡争渡,惊起一滩鸥鹭)

炮灰跨过国界线时,怒江已是西南最后一道防线,强弩之末穿到昆明,下一个陪都在哪里呢?团长没忘了自找的使命,整建制过江,共同协防。没想到这边一声枪响,惊动对岸特务营提前炸桥,坍塌时候桥面上还满满当当。

剧变横生,敌军已在顶峰插旗,跑的念头在每个人心头炸响,滩头瞬间大乱,形势不能再糟了。

(团长:口号)

南天门有七十度的陡坡,爬三步掉两步,途中有人倒下无人后撤,登峰顶、上刺刀、贴身斗狠,击其锋锐的最好方式、就是用对方最自诩的东西去击败他、打垮他、羞辱他。

激战,驻守,固防,目送妇孺友军一点点片板渡江,炮灰默认了即将到来的命运。

(镜头:反打望战场)

孟烦了有点恍神,自己到底是怎么被龙文章忽悠上来送死的呢,凭他说大话,还凭他假团长。

岸边团长说相扶相携走到这里,不是指互舔伤口。这一千人是一路打着鬼子回来的,大大小小的战斗堆积出团长威严和士兵决心,赢得漂亮,败得像样。虞啸卿觉得炮灰缺决心,缺装备,缺觉悟思想,缺民族大义——其实缺的就是一场胜利,不论大小的胜利;掉头上山凭的也不是寸血山河的口号——打仗就是这群人一直想做好、一直做不好、一直还在做的一件事,就想好好做成一次,有始有终,有始有终。

(台词:回不去啦)

朱氏明朝,爱新觉罗氏清朝,蒋氏民国,统治者姓加朝代名,对应三个时期日本对中国的侵害,产生歧义因为调换顺序,调换顺序因为押韵,他们读书人是这样的。

一天一夜,炮灰们打退日军十六次进攻,康丫是在第十四次没的,在毒气弹里拖伤员,被孟烦了提醒才意识自己也是伤员。

(台词:看不见)

康丫再无声息,李乌拉身边有迷龙,康丫身边有郝兽医,握着老爷子的手,就算回黄土坡坡了。兽医用猪肉罐头和白布条做了一碗羊肉刀削面,放在了后生耳边。遥远的家乡,太行山死死拖拽着日军脚步,山河身后是年迈的长安和咸阳。

不辣带头把康丫的遗体放入弹坑、覆上黄土,遍地尸体下这种安葬堪称奢侈。康丫的死并没有蔓延过多的伤感,你我早晚能追上他。

夫子大义“未知生,焉知死”传袭了两千年,在短短的十几年中,被迫站在死亡的角度去回想生的意义,大部分人在想明白之前就迎来其本身。一千人在南天门上的一日一夜,打给对岸看,看好我们怎么死的,我们死了,你们就不怕了。

(画面:虞登场枪毙)

天亮,真川军团团长虞啸卿登场,甫一亮相便如晨间江风、一扫山间浊气,如果只靠挺成一杆枪就能凝聚人心,何必去招猫逗狗、上蹿下跳。转瞬稳固江防,虞啸卿的出现使得一日一夜有了价值。

但这也代表南天门连带炮灰一齐被明确放弃,旗语打得冠冕堂皇,意思就是见死不救,且无比理直气壮。此时的虞啸卿还没那么下作,很大程度上觉得与其苟活回来被枪毙,不如舍身成仁,告慰两岸人心。

(画面:叩拜)

叩拜,骗火炮,逃跑,渡江,长久地下跪,久到日军都放弃了射击。身边还有十几口子喘气的,龙文章站起了身,命在城里等着他。

一个能打出南天门阻击战的将领该是宁折不弯地死去的,万万没想到胆敢死皮赖脸地活下来,他到底是谁?到底想干什么?有这个疑问的不光是虞啸卿。

南天门阻击是前期最重要的武戏,县衙堂审是最重要的文戏,从缉捕到开审足足间隔了两个月,太长了,拍惊堂木,升堂,带人犯。

(画面:切公堂,威武)

第六折 · 禅达镇

三大要员会审,公堂法庭军帐,这是窥视龙文章内心的最好窗口。他确实不是说真话长大的,但今天定生死,想活命就得用真话去骗。

(台词:-姓名?-龙文章)

骗鬼,那时他还不叫龙文章,开始时跟着家人八方讨吃,四处招魂,后家人离散,孤身路过一座小城,听说了一场战斗,窃取了一个名字,后以此名主动被拉了壮丁,开始了舒舒服服的贩私盐、倒鸦片生活,直到有天被拉去上阵,望风披靡。溃散,被下一任长官踹进下一支队伍直到下一次溃散。颠沛时江山如画与他无关,从了军山河糜烂烂得拔不出来。

某天的一个契机——或许是仙人指路,或许是战友牺牲,也或许是一败接着一败败成三十四岁龙文章萌生了中年危机——要想不这么死,就得换个活法儿。混进培训班刷资历,一驼子货走关系,捞个没油水的军需中尉,卡着十万青年的年龄界限、混进川团所在的十万军,入缅剥了死人官服,顶了虞啸卿的川军团长,后来的故事是我们共同的经历。

前面说李乌拉来不及见到龙文章,披上中尉服前龙文章就是李乌拉,披上中尉服后无限迫近虞啸卿,两个灵魂撕扯一副躯体,嫌不热闹再填进三分之二个孟烦了,团长疯疯癫癫是有原因的。

(台词:您二十四小时扮小丑您不想歇歇丫)

龙文章身上有江湖小虾米的特征:有眼力,一眼看穿谁能挑事,先贬后提,看在眼皮子底下;没见识,没遇过毒气,不认得枪炮,不懂高层政治、上层官僚;会逢迎,面对比自己强的人,做作出一眼望穿的谄媚,使对方产生道德加眼力的优越,心胸膨胀了、就能容人了。

底层智慧让他凝结炮灰活命,自身局限注定也一而再、再而三要他和他的团、付出代价。

本该自利到死的出身,但在烦啦视角里,无私如圣徒。酒色财气统统没有,吃喝嫖赌一个不沾,无君无父,无亲无故,他真的是天上掉下来的,平时搁云层里猫着,把你你我我从嘴里吃进去,一拉出来就到禅达了。

可龙文章真是怕死的,怕死,怕白死,怕白白等死;骗别人,骗自己,骗人有其魂。三十四岁的龙文章等一个救世主拉出泥潭,没等来,于是点燃自己,死中求活,假里修真。没能活成自己的虞啸卿,活成所有人的龙文章。

南天门龙文章自以为唱的是挑滑车,虞啸卿脑补的是失空斩,失街亭,退司马,公堂是最后一出将,你认罪、我挥泪、推出账外砍了六斤半以全入相。没想到龙文章一点不配合,疯疯癫癫搅乱了所有预想的壮烈。

龙文章是赌徒性格的人,他不知道虞啸卿的平生秉性,于是用扒马褂、地理图的方式反复试探,反客为主地套真心、找同类,判断虞啸卿跟自己有多少重合、够不够以命相托——如果虞啸卿能懂他,自然也不会杀他,他就能跟虞啸卿一起让事情是该有的样子。

为了印证判断龙文章第二次展示了招魂。

(台词:招魂)

结果当然是失望的,虞啸卿听得出假话,但他也不爱听真话,堂审中两次眼里放光都是龙文章提到战术战法,显然整个过程虞啸卿关心的只有一件事,你怎么这么能打的——你凭什么比我能打的?

(台词:在哪学的打仗)

于是龙文章求生的唯一路径就剩显示自己打仗的价值,但这话不能亲口说,会刺激到虞啸卿的好胜心,必须要借他人之口来讲。

(画面:兽医开始,迷龙结束)

最有希望讲清楚的是孟烦了——孟烦了搞砸了,不惜暴露内心却心急失语。在虞啸卿听来,说来说去就是怕死,要令怕死的卒子不怕死,一把挎刀就够了,没必要匀一份团长饷。

没想到最不抱希望的阿译领会到了,不像孟烦了从开始剖析,阿译直接点到结果——只有龙文章能打出这种仗,他有用;能打这样仗的,你管他是什么人。

这是虞啸卿要听的:不是龙文章对炮灰的价值,是龙文章对这一仗的价值。阿译正中靶心。

这令烦啦妒忌。

(台词:烦啦嘲讽,阿译甩头)

阿译一哭救团座,二哭救豆饼,团副兼督导实至名归。

因南天门有功,擢升中校团长虞啸卿为上校师长,本师不配番号,故称虞师;擢升中尉军需龙文章为虞师第三团中校团长,领刑天旗,暨川军团。

从南天门到禅达,龙文章成了他们的魂,即使他如此糟烂不堪,所幸他如此糟烂不堪。团长回来了,迷龙不用走了,阿译不用嚎了,我们可以安心了。

第七折 · 祭旗坡

两军隔江对垒,彼此相安无事,虞啸卿任命团长时说,上峰无战意,这种无战意足足持续了两年,西岸在安心修工事,东岸在安心过日子。

禅达的节奏是安逸的,两段酸臭在发酵。

迷龙要给上官和雷宝儿搞一套大房子,先结婚,再定亲,大房子就是聘礼。迷龙穷折腾,上官在边上看着丈夫穷折腾,上官身上有种克制的蓬勃,对家的固守和向往,所以才跟迷龙相互吸引,这样两个人凑在一起,日子像蒸笼一样热气腾腾。

团长对上官一直有特别的感触,他们都是想得了事的人,团长的伪装在上官面前都是赤裸裸的样子,跟淫乐无关,男子卫国女子守家,上官做好了自己的本分他却不行,所以那是种类似差生对优生自觉的心虚。

讹房不成,迷龙当街躺床耍赖,谁劝不好使。上官从容地并腿上床,不顾面前一圈大眼瞪小眼,像操持完一天家务、哄睡了孩子的夫人,邀功似的把自己放在丈夫身旁,幕天席地里诉说着情和欲,她对迷龙的爱光明磊落,不需要高墙大屋来遮掩,新屋趁新榻,诗酒趁年华。

(台词:奸夫淫妇)

烦啦嘴上损着迷龙,心里想着自己的姑娘,领着阿译雷宝儿去远处偷懒,禅达真小,一转角就见到了。

姑娘叫陈小醉,一朝风月,陈年小醉的醉。

(台词:喝醉的醉!)

能想到用这个名搭这个姓,有两下子。按照蒲松龄的自白,传统书生对异性向往、并存着良家的闺秀和女鬼的妖娆,大概能解释为什么孟烦了眼中的小醉也总是怯生生、湿漉漉的样子。烦啦对小醉的想法谁都看得出来,但他逞强地不去进那一步,越发乎情、越止乎礼,孔老二绑架孟老二,读书人就是麻烦。于是我们能看到烦啦干的最多的事、就是一圈一圈地在小醉门口兜来兜去、兜去兜来。

小醉对烦啦的依恋不是一蹴而就的。见面三次,第一次用同情博取亲近,第二次用身份树立爱慕,第三次用幽默打消距离,赢取一个少女全部的心,也就这样了,爱情像乳牙一样生长,跟智齿一样无法自拔。

(台词:这是我儿子)

亲近又推开,两人相处过程中烦啦没少干类似的事情,最后一次推开时他对小醉时说自谋生路,我养不活你。跟养不养得活关系不大,他敢承诺,整个炮灰团都会为他想办法,但被爱也是一种能力,孟烦了没有。这方面他无比羡慕迷龙,身在禅达心在坡,回到坡上又想家,心血管抻开十几公里,还能对着太阳活蹦乱跳。

第一次进女孩的家,孟烦了偷了人家的粉条、顺走了人家的大洋。团长两条情感线在形式上都颇为不堪,一段奸夫淫妇,一段男盗女娼, 前一段是坦荡的,后一段是压抑的,如果注定这个年头用心的人都不得好死,起码不多的见面里请不要相互折磨。

为什么要在男人戏里塞两段女儿情,山河沦陷贪情享乐,大概不只是想还原这支军队真实的票子女人追求,也在于还原生命本身的完整和美好。人会为了恨而战死,也会为了爱而去拿起武器,士兵和军队不是真空存在的,战争与和平的极端之间叫守护,必死的人需要必死的理由。

(镜头:落泪)

眼泪落下,炮声想起,房子还没讹来,鬼子就来渡江了,安逸一下子被打破,炮灰抄起债本子向前线集结。一路上虞啸卿身死的消息在士兵中疯传,熟悉的溃败,团长发飙了。

(台词:川军团死顶)

彪到一半,虞啸卿就血淋淋地出现了。

(台词:你有逆流而上的勇气)

逆流而上勇气足,漏船载酒运气差,做人晦气讨人厌,这是虞啸卿眼中的龙文章,心高、命薄、惹人嫌。

龙文章把虞师形容为岳家军,是对虞啸卿本人的奉承,对虞师的嘲讽——在二十世纪、欲做十二世纪的封建部队而不得。虞师效忠的对象是虞啸卿这三个字,依然是把上万人赴死理由押在一个人身上那一套。人人平等观念传播近两百年,意味着没有哪个个体、值得另一个个体用生命去依附和效忠,挺身赴死,只能是因为认可的群体、和更高的理由。

虞师座咽了苦药,激赏与鄙薄之间赐了炮灰团祭旗坡阵地,这是个令他后悔万分的决定,直接酿成日本兵过防事件。

一支日军小队因泅渡工具毁坏,困在滩头进退不得,龙文章故意放纵这支小队逃进深林,直入禅达,理由是这样可以人人枕戈,打破安逸,虞师长气得差点闭过气去。

(台词:插在我的胸口!)

龙文章当然有自己的道理,但潜伏下来的日本兵威胁的不止是新兵,还有平民,龙文章不该想不到,但他依然做了这个决定。

团旗交接仪式完毕,龙文章检阅着配给给川军团营养不良的壮丁,昨天还在下地的庄稼汉、今天就上了战场,军与民的界限就是一根麻绳。抗战第十二个年头,军人没有无辜的,平民没有安全的,终将无人幸免。

当然、这不是脱罪的理由。

作为一支旧式军队,服务的是概念上的国、具体上的府,没有接受过人民军队教育,对民众的态度往往取决于长官的良心。新一军接收广州时纵兵为祸乡里之甚、被老百姓称之为“新日军”,军长孙立人听闻后的回应是:广东人分外刻薄,要点东西就说我们军纪不好,岂有此理。——孙立人,清华出身、新将领代表、仁安羌功臣。

因祭旗坡纵敌过防,禅达郊外一整家平民被杀绝了,整个东岸变得警觉,禅达自此不敢贪安。

阿译说得对,他有罪,谁人宁死。

(台词:全歼来犯之敌在他上头眼中)

这件事上虞啸卿在意更多的也是政治意义,但龙文章完全忽略了这个问题,甚至需要孟烦了明明白白指出来,这种对政治的钝感很快引发了更要命的事儿。

阵地对歌那天,龙文章去城里师部要火炮,返回时跟着战防炮一起被卸下来的还有一只小书虫子。

帮迷龙搬家时,阿译和烦啦遇见败下来的学生,小蚂蚁们背着一道道书墙穿过禅达的大街小巷,每一步都像是要倒下,最终还是消失于极目,望着这条连结天际细细长线,烦啦和阿译怔怔出神,他们真年轻,少年的中国没有学校,他的学校是大地和山川。

小书虫子是蚁群的一员,书墙不小心撞上龙文章,天真正确的,碰上良心未泯的。龙文章笑话小书虫幼稚,小书虫质问龙文章老朽,于是被带来阵地。

(台词:苏维埃,你知道苏维埃吗?)

果实烂在地里,骨朵逼自己早熟。瞧着没来由的亢奋,孟烦了点醒龙文章,这是个危险分子。

虞啸卿是反共起家的,在虞师阵地冒出来这样一只虫子结果不言而喻。小书虫只是理想主义,但对虞师来说都一样,赤色分子都是理想主义,因此反推也成立。团长的迟钝确实不是故意,他先看见理想,才看到立场。对了就是对了、不在乎哪张嘴说出来的,对了很重要,和自己最相似的居然是那边的人——小书虫被赶走了,龙文章混乱了。

为了解决这种混乱,龙团座亲率座下副官、翻译官、传令官和参谋,大军进城堵小书虫。

(台词:有问题要改)

这是一场单方面的论道,丘八被打得进退失据,龙文章辩称说没有原因也要做事,这才是做事,说这话时挺着胸虚着气,用做事的勤奋去掩盖思考的懒惰,卧薪尝胆不也是种鸦片么。

(镜头:打)

龙文章无法把小书虫再纳入炮灰,逼着后者渡了江,去寻找一支停留在传说的游击组织。他走了,于是能看到一个老朽,如何亲手把年轻推到自己对立面。昨日黄花昨日兴,清风山岗,第七十三座墓碑合葬着民族,民权,民生,理想是更深远的实用主义,当被最决绝的手段背叛,未来已与它无关。

小书虫渡江去,孟烦了逃兵来。逃兵情节之突兀,倒不是烦啦做不出来,而是时机太脑袋撞屁股,轻信陌生人的三两句挑动,一个瘸子能跑多远呢,果然很快就被抓了回来,绑上十字架,开始受难,这份受难的作用除了引出接下来的渡江侦察,也在于揭开烦啦的内心。

团长带头劫法场,营房夜话,龙文章逼着孟烦了说清楚自己。上一次烦啦暴露内心是在公堂,讲自己的怕死和罪孽,脆弱的真诚,听得龙文章听得若有所思,他要趁着这次机会,再听一遍。

人命草芥的年头,团长为什么执着于一个孟烦了呢?

团长不是搞慈善的,也不是心理医生,战乱之秋肯抱着无限耐心,把烦啦走到哪带到哪,逼着他卸下担子,逼着他扛起责任。龙文章有一千个游魂和整整一个团人要照顾,如果有天自己被虞啸卿砍了脑袋,烦啦是唯一能继续带炮灰们活下去的人——撒泼打滚地活下去,能打就打,不能打举团逃跑绝不拖泥带水——这点阿译做不到,迷龙更不行,这是团长的期望,也是不得已的托孤。

(台词:烦啦你什么时候能认真一点呢)

还有,死啦真的很孤独。

(台词:求求你有时候不要让我觉得自己是一个人在扛)

不管从哪个角度讲,烦啦都辜负了团长,一身毛病什么时候都好不了,烦啦为什么会搞成这样,是啊,烦啦为什么会搞成这样。

孟烦了,北平人,学生兵,卢沟桥时弃学,徐州会战从军,四年历战大小二十余次全部战败,以逃兵身份加入禅道收容站。

第一次从南天门下来,龙文章被拷走,孟烦了轮回收容站。打趴支棱,支棱打趴,这是一种周期性绝望,人如果失望过太多次,就有理由不相信。卢沟桥孟烦了弃学,看见过佟麟阁、赵登禹,徐州会战孟烦了从军,也看见过张自忠。收容站溃兵人均经历过几十次败仗,但不都是那种“放三枪就是对得起长官的饷钱”的败仗。川军出蜀,粤军北上,滇兵凶、湘人猛,刀砍西北风,有过勇悍的士兵和不旋踵的将领,但一点都不妨碍败得跟怕死一样狼狈,冲锋时被炮打死,逃窜时被炮打死,这里有两具无人认领的焦尸,哪具是英勇的、哪具是怯懦的。

孟烦了坦克恐惧症的对象,日本陆军 95 式,有枪无炮被日军自己称之为“豆战车”,这种履带机枪甚至难以列入同期坦克序列,远东被苏军耻笑,太平洋被美军耻笑,那些战场上大兵习惯用反坦克步枪、中距离掀开里面驾驶员脑壳、完整地回炉 3.5 吨钢铁,但是在中国战场拿它,毫无办法。

团长中不止一次展露这种农业国对工业国的绝望,烦啦的破碎,是双重的破碎,自己的,和国家的。他有一个开眼世界的家父,更清楚钢产量和煤产量这些没用的衡量指标意味着什么,知道大炮飞机不只是使用,更在于生产,以及这种生产背后的阵痛与时机。贫穷帝国主义也是帝国主义,军国机器量产着野蛮,文明锄头不能反器材,越清楚、就越绝望。

于是他想逃跑,可压在孟烦了背上的除了蒸汽机,还有出师表。论语春秋二十四史,李杜苏辛忠肃武穆,意味着与山河血脉相连,大风起兮你做了卫霍,时穷节现你也得是文史,辛丑马关、伏波定远由不得你选,三两个字一方水土一方人与你有关,你出生就活了几千岁,五年前也得一齐受难三十万回,这是欠三十张豆饼的利息活该、压得你翻不过身、喘不过气来。

这些问题烦啦当然想不清楚,烦啦很多事情看得透,可不妨碍他拎不清,敏锐的眼睛和虚弱的心脏,一肚子稻草消化不良。这种独属于传统知识分子的诅咒,在当时的识字率,烦啦同样是孤独的。龙文章只知道烦啦做不了忠臣,那就助他当个孝子吧。

况且他是真的想去西岸挠痒痒。

龙文章挑选出了精锐炮灰,来到小书虫所说渡江点,在狗肉的帮助下搭了一条索道。

(台词:以后他管额们叫人肉)

十二条人肉和一位狗过了江,踏上久别重逢的土地。

第八折 · 渡和顺

西岸行是一场注定的伤心之旅,战斗过和埋葬过的痕迹早已被抹去,只剩惶恐和陌生,炮灰在自己国土上弓腰游荡,遇到了几乎蜕化成野人的西岸乡民。

(台词:老子就是不招安)

老乡指引下炮灰小队很快入城,行进中孟烦了就撞见了父亲,父子擦肩而过、倒是尘满面先被鬓如霜认了出来。

(台词:爹。了儿回来了)

孟父是个走路带规矩的人,绝不讨任何人喜欢,即使这群人担了天大的风险来救一位素昧平生的老人家。他来来回回只有一句话,过江可以,带上我的书。这意味着每个炮灰都要在这样的敌后环境里当端枪的小蚂蚁。孟父的态度已经接近威胁,老顽固引爆了小损人,没有哪个儿子会用上膛的枪顶着自个儿父亲。

(台词:带你大爷!)

孟父,清末留洋学童,家境殷实,学贯东西,发明过永动机,不成,一封家书出门找儿子,未果,因烧书胁迫被招为和顺保长,至今。

孟父对当兵的总是不客气,觉得丘八欠了他的。如果你在城门楼子撞见孟父,你问他,人家都去保家卫国了,你呢?孟父会告诉你,“我就是他们要保卫的那种家国。”

礼不崩乐不坏的时代,丘八负责卖命,读书人负责读书。孟父自觉比别人的命金贵,因为肚子里装着三百张豆饼。招安做了保长,不是城太高水太凉,老头真诚得觉得保存这些书屈辱地活着,比简单地死去更需要勇气。待见完孩子最后一面,一家人团团圆圆,再殉了这世道。

(台词:我的命我自己说了不算)

小书虫子这句话同样适用于孟父,这类人被称之为文脉,读书人和书一样是载体,文脉既存,先秦明清至共和,一脉相承,绵绵不绝。但相比于小书虫子的纯粹,孟父既是文脉更是法统,作为维护统治的工具,上表生民,下达王命,本身具有强烈的政治意义,因此才能被敌人异乡异地任为保长。孟父的藏书很多孤珍善本,小书虫看到时说,拿回去就可以印很多很多本,给很多很多人看;孟烦了点破说不可能,孟父享受的是对书本身的垄断,保长礼遇正是这种垄断所衍生出的特权。

(台词:保长家的小儿子)

烦啦的生长氛围十分压抑,孟父愤懑很大部分来源阶级没落,闲居等同流放,于是把这种没落发泄在家庭中。一个最早开眼世界的人顽固地讲老礼、守规矩,把自己活成肉法统,想着人人能齐家,便国破山河在。

文人说的神州陆沉,亡君亡国亡天下,从来不是一个东西。五胡南北五代辽金,天灵盖对狼牙棒甘之如饴,文人心中文明的先进性从不动摇,自命上人甲午方休。环视文化人心,完全不同于宋明的战败于朝堂,文人数千年未有之溃败,家国与文明的一体破产、落后存亡,死于电灯与启蒙运动,学技术赶不上,学制度赶不上,学风俗也赶不上,于是有说讲洋文、于是有人说废汉字,如果易文改字才能苟活,活下来的是中国还不是中国。

(台词:天啊,就放不下一张安静的书桌吗?)

孟父当然也想不明白,和他的儿子一样。他只知道固执地带上书,那就带上,人接上,回程。

好在路上并不孤独,同行的还有一进城就遇到的红色武装。

(台词:原来国军兄弟一样风趣)

组队,行进,遇敌,牺牲。这支队伍仗着对山路的熟悉,一路掩护他们的国军兄弟,所有人跟鬼子咬成了胶着,最后一个人把渡江小队送过了桥。

(台词:远征军万岁)

初见到这装备、训练连换装前炮灰团都不如的大红,心里就一句,送死的来了。而那个烦不死、驳不倒、打不趴的年轻人,俨然在里面水乳交融了,他依然年轻,不再迷茫,每天有很多东西要学,很多梦要做,劝孟烦了带上书的时候还说,不能光打仗,打完仗变白痴,中国人不能不想,带上书,边走边想。

他的梦好远,远到看不见的将来。

(台词:你们的人死啦)

大红跟炮灰一样穷、一样惨,拖不跨、打不烂,是龙文章虞啸卿做梦都想得到的那种队伍。这样的武装有很多,起源可能是某个掉队的政委或普通战士,没有枪没有炮、几本册子当冲锋号,唤醒同志,联合生存,独立战斗,后来战士牺牲了,同志打光了,连敌人也走了,家人不知道他们去了哪里,江风伤人,没有一个名字留下。一条大河波浪宽,风吹稻花儿香两岸。

红色种子来自苏维埃,生长于黄土地,词穷到无法解释为什么肉眼可见的干涸能让它发芽,剥开颜色,你能看到人类的最纯粹的理想,有一天会有人用汉文字书写《人类文明闪耀时》,他可以平静地讲述这个文明卑劣与高尚交汇的时刻,讲这个文明的败罔,和这个文明的新生。

依靠争取来的时间,炮灰们攀上渡索,有惊无险地泅回东岸。

西岸行拓展了团长画卷,对不同党派的侧写,对沦陷区民众的描绘,开明乡绅,明理百姓,好战将领,百战老兵,回想一下,这已经是旧中国能拿得出——最好的阵容了。

接回慈严,烦啦欠了团长好大一份情,往后的时间里,不得不数次陪着龙文章反复渡江,测绘阵地,秋去春来,1944年到了。

第十折 · 沙盘战

1944年夏滇西远征军司令卫立煌,电令第八军军长何绍周、国耻日前克复松山,强令之下“侄帅”只得表示不克松山、不留人头,撂下电报,勤务兵出身的副军长李弥领兵渡江。

沙盘即松山,不废话,来看看这局是怎么打的。

(台词:我是竹内连山,特地来歼灭你的虞师)

战术很复杂,战略很简单。虞师筹备两年的沙盘上,南天门有三道防,每一防是三分之一的竹内,对应虞师三部人,一团克一防,二团克二防,三团不指望,师属克三防,树堡插旗,打完收功。

龙文章的侦察结果,由于地下管道的存在,一团面对整个竹内,二团面对整个竹内,树堡还是整个竹内。且由于各种工事的存在,如果一防二防是群狼拱蛇窝,树堡就是狮子啃豪猪。

虞师立刻调整打法,一团换一防,二团换二防,垫完三团,师属啃三防,树堡插旗,打完收功。

但虞师没想到的是,此时竹内才显露出终极的恶毒,原以为打到大树是临门一脚,没想到是掉进二、三防的缝隙,腹背受敌。反斜面就是那个背,里面刚才没人了,人是从大树里爬来的,能爬来是钻管道来的。

简单的战略困扰过很多人,这个困扰应该归功于剪辑,但凡多拍几镜沙盘也不至于。看到说团长的台词是话剧式的,反思是鲁迅式的,人物是水浒式的,剪辑是毕加索式的,同意最后一句。

沙盘上的四面楚歌,打醒了虞啸卿,他不是怕浪费,是怕赢不了。如果孟烦了是思虑过甚丧失行动能力的哈姆雷特,那虞啸卿就是放弃思考一往无前的堂吉诃德,埋头冲时南天门是他的狼居胥,睁开眼时变成大风车。

赢不了,崩溃了。

(画面 :虞啸卿倒)

龙文章没崩溃,还看起来很有底气,孟烦了猜出了底气的原因。

有底气却不说,孟烦了猜出不说的原因。

(台词:你大爷的,就是觉得南天门没垒够一千座坟是吧)

孟烦了慌了,两年了,来来回回,反反复复,絮絮叨叨,没完没了,就为这一件事儿——利用你给弟兄们找一条活路,求求你别让我们过去。——这是烦啦对死啦最后一次修正,他喇开死啦的疮疤,伸长了指甲抓挠里面的愧疚,他是离团长最近的人,知道怎么毁掉一颗坚韧的心。

(台词:叠纸船)

龙文章像野人山里一样宕机死去,孟烦了成功了,甚至有些得意。但他没想到人的影响是相互的,冲得上杨六郎,冲不上恰米汤,看着躺平的团长烦啦反倒心里难安,于是他习惯性地用嘴巴去伤害身边的人,以恶毒来排毒,这次烦啦挑了一个最不该挑的人。

(台词:自撰一良方)

兽医,大名郝西川,少尉医官,年龄最大的炮灰。三分之一的西医,三分之一的中医,三分之一久病成医。看羊看马看骡子被充入队伍看人,接到一封信后在祭旗坡被对岸闲来无事的炮火轰下山崖。

五十从军征,八十不得归。

兽医之死,药引子是中原战场丧子,添柴的是孟烦了。烦啦把渡江的弯弯绕的告诉了兽医来求认同,兽医不一定懂整个战术,但他听懂了一点——炮灰团必得交代在南天门。

信上说兽医儿子的死因是临阵脱逃,死得毫无价值。

我看着你们,什么也做不了。我什么也做不了,只好看着你们,看到伤心至死,再被轰下悬崖。孟烦了把脸贴在兽医的墓碑上,这里埋着炮灰共同的父亲,请告诉我该如何悲伤。

孟烦了没脸再去扯团长的后腿,龙文章向虞啸卿投降了,反攻大计被日夜赶制着,阪上走丸,很多条生命开始倒数。

兽医的死是炮灰上再上南天门的直接理由,于是最终促使炮灰上阵的不是公义,而是私仇。九一八后的十四年,是一场现代民族国家意识的催熟,不同于竖向分娩的朝贡体系、和横向出生的阶级划分——因为外人、直观区分自己人的外源性的民族国家意识,更凝结更狭隘,也是在这个阶段最好解释:“牡丹江的事,到底跟扬子江有什么关系”这个紧迫问题的回答。历史给了虞啸卿两年时间去解释给这一万两千人,两年之后这些人渡江与不渡江的理由,就是对时日漫长宁站不坐、最大的嘲讽。

如果松山战已经在沙盘打完了。为什么非得再打一次?是为了弥补一种遗憾,还是真觉得没垒够一千座坟。

兰小龙说,他把抗战当做一场失败。四十三集就像一次漫长的失败,四十集厉兵秣马,以为苦尽甘来,然后用最后三集的结局,把一口长气憋进你的心肺再捂上你的嘴,好像唯恐一个杀光敌人、收复国土的结局被当做慰藉,会稍稍让你喘息好过,浪费了两年来走驼峰线,钻野人山,解仁安羌,炸通惠,上松山,浪费了苦心筹谋把整个远征军压在千把炮灰身上,把一百年的呐喊和徒劳压在一万多人的两岸滩头然后一败涂地,仅仅为恶毒地要你体会一小块方碑下面、活过八千个人的感受。

我不知道这是不是对的,就像我也不知道短短一个小时就带你再上一次南天门的时候,你有没有和这些人攒下足够的交情。

从禅达到南天门,带上毛巾,走吧,上南天。

第十一折 · 上南天

东岸

一直到渡江之前,团长的叙述角度都是孟烦了的主观视角,渡江之后视角被劈成东西两岸,大概是因为这一节里,我和团长都变成了背景,东岸阵地才是主角。具体来说就是,唐基,和虞啸卿。

虞啸卿,湖南人,将门虎子,少起兵,反共发达,非嫡系,风生水起。

龙文章和虞啸卿的关系,渡江侦察,获得了虞啸卿的击节赞叹,龙文章显示完气节就开始要饭,大败虞师座豪情。团长一直怯于显露与虞相似的一面,大概是怕会被引为知己,既为知己,当知我心,请即刻举团壮烈,余请自委座以下为公浇奠。

龙文章和虞啸卿的决心,藏在老麦的对话里。

短暂相处,老麦已经打定主意,一回到师部他就会尽力去诋毁这支部队,渲染川军团糟烂不堪、上阵只会拖累友军。——目的为了救炮灰的命。他怕经过自己的训练,刚刚好能达到上阵的及格线,再一次像野人山保护过他的士兵一样,在简报里悄无声息地等掉。

(台词:我念不懂你们的经)

这段总结是第一次南天门的总结和第二次南天门的预测,精准且明确。

老麦的台词是最不突兀的,使我们明白兰晓龙真是按《麦克白》这么写的。这位编剧的文本习惯,越俎代庖地替剧中人组织表达,言简意赅地絮絮叨叨,里面的人物是自己又不是自己,终致无法分辨。老麦的出现不止代表一种二战大视角,也代表一种后工业化的不带民族情绪的现代人视角——一个身处安逸环境的人、是怎么看待奋战在上世纪三四十年代的近中古的军队的。

(台词:战争会赢,可你们会输)

老麦说,没必要打,美国已经参战了,星条旗将插遍太平洋,英国人走出印度,苏联人也快了,不用打,活着见证胜利,很快的。

龙文章听进去了,再不打,敌人被打完了,就真的来不及了。

(台词:一个把自己国家都弄丢的军队,这种债能指着别人来还吗?)

老麦诞生在一个年轻的国家,国史中最惨烈战争居然还是内战,猛进高歌,没有到过最危险的时候,无法理解龙文章们的无奈。

在耻感文化里,辱比恨更绵长,这里的英雄能下跪钻裆挨耳光,只要能把场子找回来。蒙古借道,捣黄龙的不是岳飞,靖康耻就是永远订在额头上的白布条。战事至此,如果不浇铸一个鲜活的万世一系跪过该跪的每一亩耕田,事情大概就不算完。如果真的快要结束了,更要抓紧时间,跨江渡海把账能算多少算多少在结束前,亲手宰了他们脑子里的那个主义、再踏上一万只脚。这样的结果对战后整个亚洲包括侵略国本身、都是一个机会,自贸区、共同体,总归是可能的机会,不用一面闪烁一面隐忍,随时如鲠在喉、被一触即发。

在整个,能承受这份沉重的虞啸卿算一个,龙文章算一个,孟烦了算三分之一个。

多大程度认同战争打的就是后勤,就有多大程度认可虞师的主管其实是唐基。

(台词:戎马倥偬)

唐基,汉唐盛世的唐,重而无基的基,领虞师副座,总管除铁血之外一些事项。唐基的初登场是堂审龙文章,动机非常明确,宰了龙文章,保全虞啸卿独拒日寇的功劳。后来饶了龙文章的动机也很简单,就是孟烦了分析的,找个补袜子的顶川军团的缸。

作为家臣,唐基只有一个出发点,虞家利益最大化。唐基绝不喜欢龙文章,或保或杀只看利弊风向,不掺任何个人情绪。最能体现这一点的是对兽医的态度,在复仇立场上唐基与此时西岸炮灰应当是统一的,两个老头儿的执手泪眼无不真挚,然而东岸全程不见唐基有什么妨碍,渡江攻势该拆拆该拖拖,冷漠至化境,不是谁都能做得到的。

不同于人情世故,军事上的唐基是彻底的门外汉,否则决不会认可攻南天门原始计划。他理想中的南天门,友军掩护,合理伤亡,趁机坐大。一朝被龙文章打破幻想,唐基暗地里庆幸保全虞师,同时也愤懑于在美国人面前折了虞家的面子。

好在局势明了,他还有时间,师团密谋之时,副师也没闲着——唐基新计划是:友军掩护,合理伤亡,趁机坐大。

友军掩护,美军不掺和不行,这是之前要物资的结果和之后要物资的理由,虞师独立拿下南天门,怎么解释过去两年和今后无数个两年的伸手?

合理伤亡,必得拉上其他师一起打,山头体系无需赘言,惨胜意味着赢了蒋家输了虞家,打光了的下场就是被十几号宪兵贴着长官司令的脸去零切碎卖他的兵;

趁机做大,把突击渡江解释成铁血侦察,先手坐庄,再攻击立止,等人加注,这样就做大了南天门的盘子,局部变全线,小大变大打,校衔换将衔,虞师变虞军。

抗战乃至甲午以来第一块真正意义上收复的失地,这个盘子足够诱人,说服上峰并不成问题,问题是如何说服虞啸卿。

(台词:虞侄!)

唐基的话术是专为虞啸卿准备的,迎着虞师座的怒火,避开对局势的判断、抢白对虞家的用心,面对忠诚至无私老头儿,虞侄一上来就泄了气。一鼓而衰,然后是一步一步被瓦解。

虞啸卿要做朱仙镇的岳武穆,不是风波亭的岳武穆,这没有什么问题,国危如累卵,男儿建功时,个人追求与群体利益是既可一致、也可重合的,这原本就是大多数人做大多数事的原因。但唐基的话术就是要挑破这种个人追求,夸大里面的私欲,先质疑动机,再否定初心,失落滑坡、继而对立,自己把赤子之心当做孩子任性,最终走上他为你设想的成熟道路。——比起做好一件事,这事唐基们擅长多了。今天怎么捋顺的虞啸卿,明天怎么捋顺阿译。

南天门赌盘开启,渡江批条从师部转前指、转总参、转长条案,开始了三十八天的讨价还价。

在把视角交还给孟烦了之前,

西岸

龙文章没有前后眼,他没法预测上层谋划,只是凭借着经验,每当想认真做完一件事,就一定会出问题,什么问题不知道,但一定会出问题。

树堡内的人收到了电报,你部侦察已收悉,上峰特此决定再——议论议论,再研究研究,再商量商量,再权衡权衡,再比较比较,再考虑考虑,再观察观察,再看看,再想想,再等等,等等等等。

那就等等吧。

第一天,阿译进堡。

第二天,固防。

第三天,蛇屁股被绑走,引燃手榴弹。

固防。

第十二天,泥蛋自杀。

固防。

第二十四天,抢乒乓球,崔勇中弹,麦师傅成粉。

固防。

第二十九天,何书光中弹。

固防。

第三十天,晋军衔。

固防。

第三十二天,敌挖洞偷袭。

固防。

第三十三天,获得补给。

固防。

第三十七天,炮火漫天。

方先觉说,不是我们抛弃了国家,而是国家抛弃了我们。沙盘战时烦啦说精英是这个民族最后的脊梁,何书光张立宪被卖,不知目睹一切的余治李冰海正冲,还愿不愿意做这一副脊梁。

张立宪是幸运的,能在迷茫的年纪把自己交给放心的人,虞啸卿是岳飞他当然是张宪,虞啸卿是堂吉诃德他也不介意做桑丘,现在虞啸卿是什么呢?他不知道。孟烦了对张立宪态度尤为排斥,同为学生兵,相似的峥嵘不同的结果,烦啦一直避免跟张立宪有什么交集,无奈因为小醉。对爱人的态度,张立宪恰好是翻版的迷龙,张立宪能十分钟内把所有能想到的承诺都倒给小醉,孟烦了能在同样的时间里给一千座坟拔一遍草,他内心已垂垂老矣,不值得再赔上一颗风华正茂的心。

烦啦带上迷龙不辣冲人老窝时,余治李冰正凉水里搓着衣领,头顶上晾着体面,生活上精英不比炮灰阔绰,军饷优先拿来换香皂。虞师区别精英和炮灰凭的是一层皮,烦啦稍微能理解为什么如此看重这层皮。三十多天的老鼠洞足以磨平一切,张立宪的皮没有了,战后简报里,你我同在一行字之间。

炮灰这边,进入树堡前,豆饼就掉下了江,树堡地道里,满汉也被射穿头,截止到最后进堡的阿译,川团事实上建制覆灭,从团册上的名字,变成日记上的名字,简报里等掉的名字。

对于这些活过的炮灰,那个国给予他们的一直是收粮的官爷,大堂的老爷,和绑人的兵爷,这些人就是被分配吃掉的一张张豆饼本身,从有到无,不配亏欠。

接到第二封电报时,龙文章蹲在地上说,不要死,为自己活着。

说完话的几十天里,精神每一天都在解体,肉体做作地支撑,国之大事,在祀与戎,一边打仗一边招魂,父亲是伤心死的,兄长是心累死的,他终于招不动了。

龙文章团在一小堆人里,呼进最后一口气,不辣举起光荣弹。

第三十八天,支援抵达。

团长走出树堡,他的结局还没来,命在城里等他。烦啦很困了,没有陪团长出去,只想留在洞里,一觉睡过几十年。

收折

由于制作原因,团长的后续剧情未能进行拍摄,在没拍摄的剧情里,最重头也最值得说的是迷龙之死。回顾全剧会发现,作为贯穿始终的敌对方,日军就出了两个名字和一张照片,焦点始终没离开炮灰和东岸。日本人走了,一点也不妨碍迷龙被千金市骨,问题不是日本人带来的,不会跟日本人一起被赶走,迷龙的死是对这一点的回应,所以要想,要改,要反思。这也是团长结构上有意思的一点,当台词提出问题时,剧情是答案;剧情提出问题时,台词是答案,话剧后遗症了属于是。

尽管反思这个词在今天越来越不顺耳,但还是得说,大国不会因为一次安逸就轻易被弄死,也不会弄死谁就一劳永逸,所有的问题最终指向的都是自己,这是大国的诅咒,也是特权。

由于因死缘生的创作逻辑,结局缺憾导致迷龙、龙文章、克劳伯、阿译等人的人物弧光无法完成,特别是阿译,从树堡中钻出来时人物堪堪完成三分之一,一并导致缺失的还有站在阿译对立面的牛腾云和伍佰里,龙文章宁死不北上的战场,两支队伍的对撞、新旧中国的交替、解放与内战的区别,这些堪堪破土的主题,这里也都遗憾地无法为你一一展开。

只提两个点,一个是烦啦无法接受小醉后来却娶亲生子,龙文章死命都做不到的事,牛腾云轻而易举做到了,由此可以切入解放对个体的意义和影响;另个一点是不辣与恒山光寺,瘸子和傻子相扶奔向家乡,不辣放下了自己的身份认同,也放下了对方的身份认同,由此延伸国家对个体的约束和个体对国家的认同。

不辣第二次南天门跟别人都不同,作为生前吃干、死后抹净豆饼们的回魂,和这个国陨身糜骨,两不相欠。

带着大哥的骨灰,伍千里跪在船头向爹娘叩首,说家里分田了,岸上有家了,上岸去。即使你不懂什么叫红色理想,也能明白朴素的感恩图报,羽檄起边亭,伍万里没有多想便奔赴远方战场,我想那个——才叫保家卫国。

水流从高山潺潺流出,出海时波涛如怒,是因为沿途加入了无数细小支流、和支流的支流,奔腾汇聚,万里而成。小河无水大河干,国的存在是因为人、而不是反过来——这是巴颜喀拉山的含义。

当然,没有具象的人物和故事,这不过是些漂亮大话,无法真正地渗入人心底。尽管我们从个人延伸到家到国到党派政权、民族文明,但团长和兰晓龙其他几部作品一样,终点依然要回到人本身,你想看的,也还是这些人、和他们做的事。

关于《西西佛斯》对团长的创作理念的影响,也没有特别难懂,推石头上山并不荒诞,石头滑落也不荒诞,荒诞的是二者联系;想胜利不荒诞,不想死也不荒诞,荒诞的也是二者联系。于是创作者只需要非常认真地描绘想胜利和不想死,战争的必要和生命的可贵,读者自然会主动在这种联系中自我挣扎、内心倾轧。

(台词:你要让我埋呀)

对生存与毁灭的过度关注,非常容易陷入虚无主义,团长的后半部分已经隐约有了些苗头。但也正因这种过渡,团长突破了抗战剧的框架,是一部当然的战争剧,它可以放在滇西,也可以放在瓜岛、斯大林格勒,钓鱼城或君士坦丁,怒江东岸,怒江西岸。

考虑到下次涉及团长要很多期以后,对龙文章的结局再啰嗦几句。

可以肯定的是,龙文章之死是自找的。南天门授勋式,花团锦簇间龙文章在台上大喊要带着敌对党的军队去扫荡外敌,这一举动葬送了包括自己在内的所有南天门幸存者。

有路可走,卒归于无路可走——比起法国人加缪,更适合解开龙文章的是楚大夫屈原。作为第一粒将理想与死亡联系起来的基因,屈子投江却并没有直说理由,在可能的原因里,楚王被囚禁一隅,楚国被一扫六合,推崇一生的美政也注定被秦律砸得粉碎,个人对历史的失败,个性对系统的失败,理想对现实的失败——可不同于功成不必在我的那种进步理想,他信奉的理想本身都老朽、衰败、注定消亡取代,这样一目了然的陈旧,一株智慧的灵魂为什么会参不透。

对一些人,理想重要仅仅是因为那是我的理想,构成了我完整生命的一部分,我不能面对自己的残缺,无意勉力进下一个世代,在这个关于失败的故事里,我将忠于且仅忠于自己,如来时去时简单单赤条条的独个自己,极端的博大,极端的自私。虞啸卿逃开风波亭,龙文章投向汨罗江。

在写团长过程中无数次钦佩编剧兰晓龙,尽管我们说过,展现思考的过程而非结果、是更高级的创作手法,但是在这么大一个投资项目里、连想思考什么都不交代清楚就敢交货的编剧,真的是超勇。

因此也同样钦佩敢收货而不是撕碎了拽脸上的导演康洪雷。文字创作者能遇到这样一个导演,能纵容一个知识分子的诳语呓语、反复矫情,作为合作前已成名的、上部剧与本部剧的导演主创,没有用自己的风格过多限制,编剧写飞的同时还让能落了地,这位重剑无锋的厚重导演,是最勇的掌舵手、和最沉的压舱石。

忧郁是一种情绪,他能让你思考或者不思考,下坠或者不下坠,不必急着去消解这种情绪。《团长》是一部散文诗,对这首长诗我已没有更多东西能讲给你听,我们随缘,观众少爷,江湖再见。

(——黑幕——)

十五从军征(豆饼),八十始得归(兽医)。

道逢乡里人(李乌拉):家中有阿谁(迷龙)?

遥看是君家(康丫),松柏冢累累(要麻)。

兔从狗窦入(何书光),雉从梁上飞(张立宪)。

(虞啸卿:立,跪)

中庭生旅谷(小醉),井上生旅葵(上官戒慈)。

舂谷持作饭(蛇屁股),采葵持作羹(不辣)。

羹饭一时熟(克劳伯),不知饴阿谁(满汉泥蛋羊蛋子)!

出门东向看(小书虫),泪落沾我衣(孟烦了)。

(黑幕——虞啸卿:我要提到袍泽弟兄想到就是我的团 并 龙文章:走啊,我带你们回家——黑幕)

完

大语言模型推理服务知识梳理

2026-07-08 21:39:46

0. 总览

这是我和 AI 智能体共同探讨写作的一篇博文,用来帮助梳理大语言模型推理服务的相关知识与实践。这篇文章的主线是:一个开源模型如何从社区里的文件,变成服务器上可被业务调用的 LLM API 服务。这条链路看似只是“下载模型、启动服务、发请求”,实际涉及模型文件格式、GPU 显存管理、请求调度、KV cache、并发、量化、性能指标等多个层次。

这些问题琐碎、复杂,又各有各的重要。这让我想到了在“上古”时代,大家热衷于探索计算机中“从加电到系统启动”、“从 fork 到子进程执行”、“从输入一个 URL 到浏览器完成页面渲染”等水面以下鲜为人知的过程。

庐山烟雨浙江潮。在人类把越来越多的工作——交付也好、让位也罢——给 AI 智能体的当下和未来,愿我们能始终保留探索、理解和表达的渴望。

0.1 什么是 LLM inference

LLM inference 指的是:给一个已经训练好的语言模型输入一段文本,让它继续预测后续 token 的过程。它和 training 的区别在于,inference 不再更新模型参数,只做前向计算;training 则要计算损失、反向传播并更新权重。

以“北京是中国的”这个 prompt 为例,模型不会一次性“写出答案”,而是做下一个 token 预测:

输入:北京是中国的
预测下一个 token:首
输入:北京是中国的首
预测下一个 token:都
输入:北京是中国的首都
预测下一个 token:。

这就是自回归生成(autoregressive generation)。绝大多数文本 LLM 都基于 Transformer 架构中的 causal decoder:每个位置只能看见自己之前的 token,因此天然适合“根据上文预测下一个 token”。

0.2 inference/serving/chat completion/generation 分别指什么

这几个词经常混用,但它们处在不同抽象层:

术语 关注点 简单理解
inference 模型计算 把 token 喂给模型,得到下一个 token 的概率分布
generation 生成算法 根据概率分布和采样策略,决定真正输出哪个 token
serving 工程系统 把模型包装成可并发访问、可监控、可扩缩容的服务
chat completion API 形态 接收 system/user/assistant 消息,返回 assistant 回复

举个例子:temperature=0.7、top_p=0.9 属于 generation;/v1/chat/completions 属于 API;batching、排队、限流、metrics 属于 serving;矩阵乘法、attention、KV cache 属于 inference 内部计算。

现代开源 serving 框架通常会兼容 OpenAI Chat Completions API,这样上层应用可以用同一套 SDK 切换云模型和自部署模型。

0.3 模型从社区到 API 服务经历哪些环节

一个典型 lifecycle 可以拆成下面几步:

flowchart LR A["选择模型"] --> B["下载模型"] B --> C["准备环境"] C --> D["加载权重和 tokenizer"] D --> E["分配显存与 KV cache"] E --> F["启动 serving runtime"] F --> G["暴露 API"] G --> H["接收并调度请求"] H --> I["Prefill"] I --> J["Decode"] J --> K["流式或一次性返回"] K --> L["监控与压测"]

其中最容易被低估的是中间三步:加载、分配、调度。一个模型能否跑起来,首先取决于权重和 KV cache 能否放进显存;一个服务能否跑得好,则取决于调度器能否在吞吐和延迟之间做合理权衡。

例如,同样是 8B 模型,单人本地聊天时可能很流畅;一旦放到线上同时接收 100 个长上下文请求,瓶颈就可能从“模型权重放不下”变成“KV cache 被并发请求撑爆”。

0.4 vLLM/Ollama/SGLang/TGI/llama.cpp 分别解决什么问题

这些工具都能“跑模型”,但设计目标不同:

工具 更适合的场景 核心关键词
vLLM GPU 服务端高吞吐推理 PagedAttention、continuous batching、OpenAI-compatible server
Ollama 本地开发、个人电脑、快速体验 模型管理、Modelfile、本地 REST API、GGUF
SGLang 复杂推理程序、高性能服务、结构化输出 RadixAttention、structured generation、OpenAI-compatible API
TGI Hugging Face 生态中的生产化 serving Rust router、streaming、metrics、HF 集成
llama.cpp CPU/端侧/消费级设备 C/C++、GGUF、低比特量化、本地运行

截至 2026 年 7 月,Hugging Face 的 Text Generation Inference 仓库已在 2026-03-21 归档并进入 maintenance mode;它仍然值得学习,因为很多 serving 概念如 continuous batching、SSE streaming、Prometheus metrics 都非常典型,但新项目选型通常会优先看 vLLM、SGLang 或专用后端。

1. 模型来源与命名

1.1 开源模型通常发布在哪里

开源或 open-weight 模型通常发布在以下地方:

  • Hugging Face Model Hub:目前最主流的模型托管平台,模型通常以一个 Git 仓库形式存在,包含权重、配置、tokenizer、model card 等。
  • ModelScope:国内常用模型社区,访问和下载速度对国内服务器更友好。
  • Ollama Library:面向 Ollama 的模型分发入口,用户可以用 ollama pull qwen2.5 这类命令直接拉取。
  • 项目方官网或 GitHub release:例如 llama.cpp 生态中的 GGUF 文件经常由社区作者转换后发布在 Hugging Face。

Hugging Face 上的模型名一般是 组织名/仓库名,例如 Qwen/Qwen2.5-Coder-7B-Instruct。这个名字不是随便起的,它同时传达了模型家族、版本、能力方向、规模和训练阶段。

1.2 模型名字怎么看

以 Qwen/Qwen2.5-Coder-7B-Instruct 为例,可以拆成几段:

Qwen / Qwen2.5 - Coder - 7B - Instruct
组织   家族版本   能力方向 参数规模  指令微调版本

常见字段含义如下:

字段 含义 例子
模型家族 基础架构和训练路线 Llama、Qwen、Mistral、Gemma、DeepSeek
版本号 同一家族的迭代 Llama 3.1、Qwen2.5、Mistral v0.3
参数规模 参数数量近似值 7B、8B、14B、32B、70B
能力方向 训练数据或任务偏向 Coder、Math、Vision、Reasoning
训练阶段 是否面向对话和指令 Base、Chat、Instruct
精度/量化 权重存储格式 BF16、FP16、INT8、AWQ、GPTQ、GGUF

Base 模型像“只读了大量书但没学会按人类指令办事的学生”;Instruct 或 Chat 模型则经过指令微调或偏好对齐,更像“知道如何回答问题的助教”。做服务时,如果目标是聊天、问答、代码助手,通常优先选 Instruct/Chat;如果要继续训练、做领域适配或研究模型行为,Base 更合适。

Coder、Math、Vision、Reasoning 则描述能力方向,不是训练阶段。比如 Qwen2.5-Coder 这类模型通常在代码数据、单文件补全、debug、仓库级代码理解、工具调用等任务上做了更强训练;它仍然可以同时是 Instruct 模型。也就是说,Coder 回答“擅长什么”,Instruct 回答“是否学会按指令对话”。

8B 表示大约 80 亿参数。参数越多通常能力越强,但显存、延迟、吞吐成本也更高。粗略估算,BF16/FP16 每个参数 2 字节,8B 权重仅权重就约 16 GB;70B 则约 140 GB,还不包括 KV cache 和 runtime buffer。

还有一类越来越常见的命名是 35B-A3B、235B-A22B。这里的 A 通常表示 activated parameters,常见于 Mixture-of-Experts(MoE)模型。例如官方 Qwen3.5-35B-A3B model card 标注为 35B total、3B activated:总参数约 35B,但每个 token 只路由到其中一部分专家,实际参与该 token 计算的参数约 3B。注意这不等于显存只需要装 3B 参数;如果不做专家 offload 或分布式切分,服务端仍然要管理完整专家权重。

1.3 选择模型前要问什么

选模型不是“越大越好”,而是把任务、硬件、性能目标三者对齐。可以先问五个问题:

  1. 任务是什么:聊天、代码、RAG、摘要、分类、agent、批量离线生成,还是 embedding?
  2. 输入和输出多长:短问答和 128K 长上下文 RAG 对系统的压力完全不同。
  3. 硬件是什么:单张 24 GB 消费卡、A100 80 GB、H100、多机多卡,还是 CPU/Apple Silicon?
  4. 服务目标是什么:更在意 TTFT、输出吞吐、P99 延迟、成本,还是质量?
  5. 框架是否支持:模型架构、量化格式、chat template、context length 是否被 vLLM/SGLang/Ollama 支持?

一个实际选择例子:如果要在一张 24 GB GPU 上搭一个代码助手,可以先考虑 7B/8B 级别的 Qwen2.5-Coder 或同级模型;如果是公司内部 RAG 问答,可能更关注长上下文、中文能力和吞吐;如果是本地离线使用,Ollama + GGUF 量化模型可能比完整服务端框架更省心。

1.4 上下文窗口怎么看

上下文窗口(context window)是模型一次生成时能“看见”的 token 总预算,通常包括输入 prompt、历史对话、工具返回、RAG 检索片段,以及本次将要生成的输出。一个 128K context 模型并不是说可以输入 128K token 后再无限输出,而是输入和输出共享这块预算。

$$ N_{\text{input}} + N_{\text{output}} \leq N_{\text{context}} $$

某个模型的上下文窗口为什么是某个值,通常由三层因素共同决定。第一层是训练时见过的位置范围:模型在预训练或后训练中通常只在某个最大长度内学习位置关系,这会体现在 config.json 的 max_position_embeddings、max_model_len 或类似字段里。第二层是位置编码机制:现代 LLM 多用 RoPE 这类相对位置编码,能通过 RoPE scaling、YaRN 等方法把原生窗口外推到更长范围,但外推越远,质量越需要实测验证。第三层是推理系统限制:即使模型理论上支持 128K,serving 框架也必须能分配足够 KV cache,并在可接受的 TTFT、ITL 和显存成本下跑起来。

因此,model card 上的上下文窗口不是一个纯数学常数,而更像“训练设定 + 架构设计 + 工程验证”共同给出的服务承诺。比如一个模型原生训练到 32K,通过 YaRN 外推到 128K,含义是它在配置和验证上支持 128K;但如果线上请求几乎都是短 prompt,强行把 max_model_len 开到最大,反而可能浪费 KV cache 预算。

例如 Qwen3.5-35B-A3B 的 model card 标注了 262,144 token 原生上下文,并可扩展到约 1,010,000 token。这个数字说明 runtime 可以配置很长的 max_model_len,但实际能否线上使用,还要看显存、TTFT、KV cache 和请求长度分布。

可以把上下文窗口想成考试时允许带进考场的一摞资料。窗口越大,能带的资料越多;但资料越厚,读题(prefill)越慢,便签(KV cache)越占显存。长上下文服务的难点不是“模型宣称支持 128K”,而是在线上并发里让很多用户同时使用长上下文时仍然满足延迟和成本目标。

“能放进上下文”也不等于“模型能稳定用好”。超长上下文可能出现检索位置偏置、注意力稀释、无关信息干扰等问题。因此工程上常配合上下文压缩:

  • 摘要压缩:把历史对话或长文档先压成短摘要,牺牲细节换取空间。
  • RAG 检索:只把最相关片段放进 prompt,而不是把整库塞进去。
  • 滑动窗口:保留最近若干轮对话,丢弃较早内容。
  • 工具结果裁剪:对搜索、日志、网页结果做截断、去重、重排。
  • 长上下文外推:用 RoPE scaling 等方法扩展位置编码,例如 Qwen3 文档中提到的 YaRN。

上下文压缩和 KV cache 优化是两类不同问题:前者减少“要读的内容”,后者降低“读过内容的缓存成本”。例如 prefix cache 没有压缩语义信息,只是复用相同前缀的计算结果;RAG 则是真的改变了进入模型的文本。

2. 模型文件结构

2.1 模型目录里有哪些文件

一个 Hugging Face 风格的 causal LM 目录通常长这样:

model/
  config.json
  generation_config.json
  tokenizer.json
  tokenizer_config.json
  special_tokens_map.json
  model.safetensors
  model-00001-of-00004.safetensors
  model-00002-of-00004.safetensors
  model.safetensors.index.json
  README.md

各类文件的职责可以这样理解:

文件 作用
config.json 描述模型结构,例如层数、hidden size、attention heads、context length
*.safetensors / *.bin 真正的模型权重,也就是大量矩阵参数
tokenizer.json 文本和 token id 之间的转换规则
tokenizer_config.json tokenizer 的附加配置,常包含 chat template
generation_config.json 默认生成参数,例如 temperature、top_p、eos token
README.md model card,说明训练、用途、限制、license、示例

可以把模型目录理解成一个可执行程序的“安装包”:权重是主体二进制,config 是硬件说明书,tokenizer 是输入法,model card 是用户手册。

2.2 权重文件是什么

权重文件存储的是神经网络中所有可学习参数。在线性层里,它们通常是矩阵;在 embedding 层里,它们像一张 token id 到向量的查表;在 attention 和 MLP 中,它们决定了模型如何把上下文变换成下一个 token 的 logits。

logits 可以理解成模型给词表里每个候选 token 打的“未归一化分数”。例如 prompt 是“北京是中国的”,模型可能给 首、省、城市 等候选 token 分别打分;这些分数经过 softmax 才会变成概率,生成策略再根据概率选择下一个 token。因此,模型前向计算本身只是产出分数分布,真正输出哪个 token 还取决于 temperature、top-p、beam search 等解码策略。

现代模型常用 safetensors 格式,因为它避免了 Python pickle 反序列化的安全问题,并支持更高效的张量加载。大模型权重经常被切成多个 shard,例如 model-00001-of-00008.safetensors,原因很朴素:单个文件太大,不便于下载、校验和并行加载。

一个容易混淆的点是:权重文件不是模型代码。真正执行前向计算的是 PyTorch、Transformers、vLLM、SGLang 或 llama.cpp 中的模型实现;权重文件只是把这些实现中的矩阵填上具体数值。也就是说,config.json 告诉 runtime“这是一栋 32 层楼的建筑”,权重文件告诉 runtime“每层楼具体摆了哪些钢筋水泥”。

2.3 tokenizer 是什么

tokenizer 负责把人类文本转换成模型能处理的整数序列。模型并不直接理解“北京”或“GPU”,它看到的是 token id,例如:

文本:北京是中国的首都
token:["北京", "是", "中国", "的", "首都"]
id:  [xxxxx, xxxx, xxxxx, xxx, xxxxx]

实际切分不一定符合人类词语边界。英文中 unbelievable 可能被拆成 un、believ、able;中文中一个词也可能被拆成多个 token。tokenizer 的差异会直接影响:

  • 同一段文本算多少 token,从而影响上下文长度和计费。
  • prompt 是否按模型训练时的格式组织。
  • stop token、BOS/EOS、role token 是否正确。

聊天模型还依赖 chat template。例如同样的消息:

user: 你好
assistant: 你好,有什么可以帮你?

不同模型可能需要被格式化成 [INST]...[/INST]、<|user|>...<|assistant|> 或其他控制 token。如果 chat template 用错,模型就像看到一份格式混乱的试卷,能力会明显下降。

2.4 模型配置决定什么

config.json 决定 runtime 如何搭建模型结构。几个常见字段:

配置 含义 影响
num_hidden_layers Transformer 层数 影响计算量和权重大小
hidden_size 每个 token 的隐藏向量维度 影响矩阵乘法规模
num_attention_heads Query attention heads 数量 影响 attention 并行结构
num_key_value_heads KV heads 数量 影响 KV cache 大小,GQA/MQA 会减少它
max_position_embeddings 原生上下文长度 影响最多能看多长输入
rope_scaling RoPE 外推配置 影响长上下文扩展
torch_dtype 默认精度 影响显存占用和硬件兼容

例如 Qwen2.5-Coder-7B-Instruct 的 model card 会列出层数、attention heads、KV heads 和 context length。对 serving 工程来说,num_key_value_heads 特别关键:它决定每个 token 要缓存多少 K/V 向量,进而影响高并发长上下文时的显存压力。

3. 模型本身架构

前面看到的 config.json 和权重文件,合在一起描述的正是一个神经网络。绝大多数文本 LLM 都是 decoder-only Transformer:token 先变成向量,依次穿过许多相似的 Transformer block,最后得到词表上每个候选 token 的 logits。模型“有多少层”“有多宽”“有多少个 attention/KV heads”“是否采用 MoE”,说的都是这条计算路径的不同设计。

flowchart LR A["Token IDs"] --> B["Token embedding"] B --> C["Transformer block × N"] C --> D["Final norm"] D --> E["LM head"] E --> F["Logits over vocabulary"] subgraph Block["每个 Transformer block"] direction LR G["RMSNorm"] --> H["Causal self-attention"] --> I["Residual add"] --> J["RMSNorm"] --> K["MLP 或 MoE"] --> L["Residual add"] end

图中的 block 会堆叠几十层甚至上百层。每层都保留一条 residual connection(残差连接),让新计算结果加回原来的隐藏状态;RMSNorm、LayerNorm 等归一化层则帮助数值保持稳定。不同模型的具体排列会有差异,例如 norm 放在子层前还是后、是否共享输入和输出 embedding,但不改变“attention 负责从上下文取信息,MLP/MoE 负责逐 token 变换”的主干。

3.1 Causal self-attention:模型如何读取上文

对某一层的每个 token 向量,attention 会分别线性投影出 Query(Q)、Key(K)和 Value(V)。直觉上,当前 token 用 Q 提出“我需要什么信息”,用它和历史 token 的 K 计算相关性,再按照权重汇总对应的 V:

$$ \mathbf{Q} = \mathbf{X}\mathbf{W}_Q, \qquad \mathbf{K} = \mathbf{X}\mathbf{W}_K, \qquad \mathbf{V} = \mathbf{X}\mathbf{W}_V $$

$$ \operatorname{Attention}(\mathbf{Q}, \mathbf{K}, \mathbf{V}) = \operatorname{softmax}\left(\frac{\mathbf{Q}\mathbf{K}^{\mathsf{T}}}{\sqrt{d_h}} + \mathbf{M}_{\mathrm{causal}}\right)\mathbf{V} $$

其中,$\mathbf{X}$ 是输入隐藏状态,$d_h$ 是单个 head 的维度,$\mathbf{M}_{\mathrm{causal}}$ 是 causal mask:它把当前位置右侧的 attention score 置为负无穷,因此“北京是中国的”在预测“首”时只能读取前面的 token,不能偷看尚未生成的“都”。这就是 causal decoder 能自回归生成的原因。

多头注意力(multi-head attention)会把隐藏维度切成多个 head,让不同 head 可以学习不同的关联模式;所有 head 的结果再合并并投影回隐藏维度。传统 MHA 为每个 Q head 都保留一组 K/V head。现代模型常使用 MQA 或 GQA:MQA 的许多 Q head 共享一组 K/V,GQA 则按组共享若干 K/V。后两者会明显减少 KV cache 的体积,因此对长上下文 serving 很重要;这也解释了为什么 num_key_value_heads 往往小于 num_attention_heads。

注意力不是“把整段文本一次读懂”的魔法。每一层都只是在当前表示上做一次加权信息交换;层数、训练数据和参数共同决定这种交换最终能形成多强的语言与推理能力。从推理成本看,prefill 时需要处理 prompt 内 token 两两之间的注意力关系,decode 时每个新 token 则要查询不断增长的历史 K/V,这正是后文 Prefill、Decode 和 KV cache 优化的计算基础。

3.2 MLP:每个 token 的特征变换

attention 让 token 之间交换信息;attention 之后的 MLP(也叫 FFN,feed-forward network)主要对每个 token 自己的隐藏向量做非线性变换。一个简化的两层 MLP 可以写成:

$$ \mathbf{h}^{\prime} = \mathbf{W}_{\mathrm{down}}\sigma\left(\mathbf{W}_{\mathrm{up}}\mathbf{h}\right) $$

其中 W_up 通常先把 hidden_size 扩展到更大的 intermediate_size,激活函数(如 SiLU/GELU)引入非线性,W_down 再投影回原维度。很多现代模型使用 SwiGLU 等门控变体:它额外计算一条 gate 分支,选择性地放大或抑制特征。MLP 往往占模型参数中的很大比例,也往往是单层矩阵乘法计算的重要来源。

因此可以把一个 dense Transformer block 粗略记成两步:先用 attention 问“上下文里哪些信息与我有关”,再用 MLP 问“结合这些信息后,应怎样更新这个 token 的表示”。MoE 的关键变化正发生在第二步:它不再让所有 token 经过同一个 MLP,而是让路由器为每个 token 选择少数几个专家 MLP。

3.3 MoE 模型架构:稀疏激活的专家网络

MoE(Mixture of Experts)通常保留 attention 子层不变,把 dense MLP 替换为一组并列的 expert MLP,并新增一个 router(也称 gate)。router 根据当前 token 的隐藏状态,为所有专家打分,选出 top-k 个专家;token 只经过被选中的专家,最后按路由权重合并结果。

对隐藏状态 $\mathbf{h}$,一个简化的 top-$k$ 路由过程可写成:

$$ \mathbf{p} = \operatorname{softmax}(\mathbf{W}_{\mathrm{router}}\mathbf{h}) $$

$$ \mathcal{S} = \operatorname{TopK}(\mathbf{p}, k) $$

$$ \operatorname{MoE}(\mathbf{h}) = \sum_{i \in \mathcal{S}} \tilde{p}_i E_i(\mathbf{h}) $$

这里 $E_i$ 是第 $i$ 个 expert MLP,$\tilde{p}_i$ 是在被选中专家上重新归一化后的路由权重。不同模型会在 router 打分、归一化和共享专家的处理上有所差异,但“只激活少数专家”的原则相同。

flowchart LR A["Token hidden state"] --> B["Router / gate"] B -->|"top-k routing"| C["Expert 1 MLP"] B -->|"top-k routing"| D["Expert 2 MLP"] B -->|"top-k routing"| E["Expert N MLP"] C --> F["Weighted combine"] D --> F E --> F F --> G["Residual output"]

这带来一个重要区分:总参数(total parameters)包含所有专家,而激活参数(activated parameters)只计算当前 token 实际经过的共享部分和 top-k 专家。例如“35B-A3B”可以理解为模型总共约 35B 参数,但单个 token 的一次前向计算只会激活约 3B 参数量级。它让模型能拥有更大的总容量,同时把每个 token 的计算量控制在接近较小 dense 模型的水平。

但 MoE 并不是“35B 模型只需要 3B 的显存”。正常部署时,router 可能把不同 token 路由到任意专家,所以完整专家权重通常仍需在 GPU 或整个多卡集群中可访问;总参数仍决定权重存储、下载和冷启动成本。MoE 的优势主要是降低每个 token 的 FLOPs,不会等比例降低权重显存,也不会消除 KV cache 的开销。

MoE serving 还多了几项工程特性:

  • 负载均衡:若大量 token 都选择同一专家,该专家会成为热点;训练会加入负载均衡目标,runtime 也要尽量让批次中的路由高效执行。
  • capacity 与丢弃/回退:一个专家在一个 batch 内能处理的 token 数有限。超过 capacity 的 token 需要重新路由、跳过或采用实现定义的回退策略;不同模型和 runtime 的行为并不完全相同。
  • 专家并行:专家可分布在不同 GPU 上。这样能分摊权重显存,却会引入 token dispatch 和 all-to-all 通信;GPU 拓扑与通信带宽会直接影响吞吐和延迟。
  • 共享专家与细粒度专家:一些新模型会增加所有 token 都经过的 shared expert,或把 MLP 切成更多、更小的专家。它们属于具体架构选择,部署时应以 model card 和 runtime 支持矩阵为准。

选择 MoE 模型时,除了看 total/activated parameters,还应确认 runtime 是否支持该模型的 router、专家数和并行方式,并在真实请求长度和并发下压测。短 batch、跨 GPU 通信较慢或专家路由高度不均衡时,MoE 的实际收益可能低于参数命名带来的直觉;反过来,在足够大的 batch 和合适的 expert parallel 配置下,它能提供很有吸引力的质量—计算量折中。

4. 模型加载与运行

4.1 启动服务时发生了什么

当执行类似 vllm serve Qwen/Qwen2.5-Coder-7B-Instruct 时,系统大致会做这些事:

flowchart LR A["解析模型名和参数"] --> B["下载或定位本地权重"] B --> C["读取 config 与 tokenizer"] C --> D["构建模型结构"] D --> E["加载权重到 CPU/GPU"] E --> F["初始化 CUDA kernels 和 attention backend"] F --> G["预留 KV cache 与工作区显存"] G --> H["启动 HTTP server"] H --> I["等待请求"]

这里有两个时间概念:冷启动时间和请求延迟。冷启动是服务从 0 到 ready 的时间,主要花在下载、磁盘读取、权重加载、kernel 初始化;请求延迟则是服务 ready 后处理每个请求的时间。一个 70B 模型冷启动可能需要数分钟,但不代表每个请求都要等数分钟。

这里的 kernel 指的是 GPU kernel,不是 Linux/macOS 的 system kernel。按照 CUDA Programming Guide 的语境,kernel 是运行在 GPU 上的一段并行计算函数,例如矩阵乘法、attention、RMSNorm、采样前的 logits 处理等。推理框架会尽量使用高度优化的 kernel,减少 Python 调度和显存读写开销。

模型加载失败的常见原因也和这些步骤对应:模型架构 runtime 不支持、权重格式不支持、显存不够、CUDA/driver/PyTorch 版本不匹配、gated model 没有 token 权限等。

4.2 显存主要被谁占用

LLM serving 的显存通常被四类东西占用:

  1. 模型权重:近似 参数量 × 每参数字节数。8B BF16 约 16 GB,8B INT4 约 4 GB 再加 scale 等元数据。
  2. KV cache:随 batch size、上下文长度、生成长度线性增长,长上下文和高并发时可能超过权重本身。
  3. activation 和 workspace:前向计算中的临时张量、CUDA graph、attention kernel 工作区等。
  4. runtime buffer:调度器、通信 buffer、tokenizer/CPU buffer、多卡通信等开销。

KV cache 的粗略估算公式是:

$$ M_{\mathrm{KV}} \approx 2 \times L \times H_{\mathrm{KV}} \times d_h \times T \times B \times b $$

其中 $L$ 是层数,$H_{\mathrm{KV}}$ 是 KV heads 数,$d_h$ 是每个 head 的维度,$T$ 是每个序列缓存的 token 数,$B$ 是 batch 内序列数,$b$ 是每个数值占用的字节数;前面的 $2$ 代表 K 和 V。这个公式解释了为什么“并发”和“长上下文”会迅速吃掉显存:如果每个请求都有 8K token,上下文中的每个 token 都要在每一层留下 K/V 记录;batch 从 1 变 16,KV cache 也近似放大 16 倍。

可以把权重看成餐厅固定装修,KV cache 看成每桌客人正在使用的餐具和便签。装修开店时就占空间;客人越多、点单越长,桌面上的临时物品越多。

4.3 单卡和多卡怎么跑

单卡部署最简单:整个模型权重、KV cache 和计算都在一张 GPU 上。多卡部署的目标是解决两个问题:单卡放不下和单卡算不快。

常见并行方式如下:

并行方式 做法 适合场景
Tensor Parallelism 把一个矩阵切到多张 GPU 上一起算 大模型单层太大,常见于 vLLM/SGLang
Pipeline Parallelism 把不同层放到不同 GPU 上,像流水线一样传递 模型层数多、跨节点部署
Data Parallelism 多个副本各自处理不同请求 单副本能放下,但需要更高 QPS
Expert Parallelism MoE 模型中把专家分散到不同 GPU DeepSeek-V3、Mixtral 等 MoE 模型

vLLM parallelism 文档 和 SGLang 文档 都覆盖了这些部署形态。实践中,单机多卡优先考虑 tensor parallel;如果模型已经能单卡放下而只是 QPS 不够,多个副本加负载均衡往往更简单。

4.4 推理硬件基础

CPU 也能跑 LLM,尤其配合 llama.cpp 和 GGUF 量化模型时,本地实验很方便。但 CPU 的并行矩阵计算能力和内存带宽通常远弱于服务器 GPU,所以它更适合小模型、低并发、离线或端侧场景。GPU 推理的优势在于:大量矩阵乘法可以并行执行,显存带宽高,Tensor Core 能加速 FP16/BF16/FP8/INT8 等低精度计算。

一个 GPU 可以粗略拆成几类部件:

部件 作用 对 LLM 推理的影响
HBM/GDDR 显存 存权重、KV cache、临时张量 决定模型和并发能不能放下
显存带宽 权重和 KV cache 每秒能搬多少数据 decode 阶段经常卡在这里
SM GPU 的主要计算单元 执行大量并行线程
Tensor Core 专门加速矩阵乘法 对 FP16/BF16/FP8/INT8 推理很关键
NVLink/PCIe GPU 间或 CPU-GPU 间通信 影响多卡 tensor parallel 和 KV 传输

实际运行时,CPU 负责发起 kernel launch,GPU 在大量 SM 上并行执行 kernel。矩阵乘法会被切成许多 tile,Tensor Core 对这些小块做高吞吐乘加;attention kernel 则要在 Q/K/V、mask、softmax、输出投影之间组织计算和显存访问。很多推理优化的本质,不是“换一个数学公式”,而是减少不必要的数据搬运、kernel launch 和中间张量写回。

常见硬件可以按定位理解:

GPU 常见定位 直观理解
RTX 4090/5090 消费级开发、单机实验 便宜但显存和多卡互联受限
L20 国内云上常见推理卡 适合中小模型和成本敏感推理
L40S 数据中心推理、图形和多模态 48GB 显存,兼顾 AI 与图形负载
A100 上一代训练和推理主力 40/80GB 显存,生态成熟
H100 / H200 高端训练和推理 HBM 带宽高,FP8 和 Transformer Engine 友好
B200/GB200 Blackwell 代大规模 AI 集群 面向更大模型、更高吞吐和更低 token 成本

对 LLM serving 来说,GPU 型号不能只看 FLOPS。短 prompt 大 batch 的 prefill 更吃算力,长输出 decode 更吃显存带宽,长上下文高并发更吃显存容量和 KV cache 管理。选择硬件时,应同时看显存容量、显存带宽、低精度支持、互联方式和实际租赁成本。

5. 请求处理流程:Prefill 与 Decode

5.1 请求进入服务后经历哪些阶段

先看一个常见 LLM serving 架构。不同框架命名不同,但组件职责大体类似:

flowchart LR C["Client / SDK"] --> A["API Server"] A --> T["Tokenizer / Chat Template"] T --> Q["Request Queue"] Q --> S["Scheduler"] S --> W["Model Worker / GPU Runtime"] W --> M["KV Cache Manager"] W --> D["Detokenizer / Streamer"] D --> A A --> C W --> O["Metrics / Logs / Traces"] S --> O

其中 API Server 负责 HTTP、鉴权、OpenAI-compatible API 和流式返回;Tokenizer 把消息格式化并转成 token id;Request Queue 暂存待处理请求;Scheduler 决定哪些请求在下一次 GPU forward 中被合并;Model Worker/GPU Runtime 真正执行模型前向计算;KV Cache Manager 管理每个请求的历史 K/V;Detokenizer 把输出 token 转回文本;Metrics/Logs/Traces 则用于压测和线上诊断。

一次在线请求从客户端到模型输出,通常经历:

sequenceDiagram participant C as Client participant S as API Server participant Q as Scheduler participant G as GPU Runtime C->>S: HTTP chat request S->>S: apply chat template S->>S: tokenize S->>Q: enqueue request Q->>G: schedule prefill G-->>S: first token S-->>C: stream first token loop decode Q->>G: schedule next decode step G-->>S: next token S-->>C: stream token end S-->>C: finish reason / usage

注意,客户端感受到的延迟不只是 GPU 计算时间,还包括网络、HTTP 处理、tokenization、排队、detokenization 和流式传输。压测时如果只看 GPU kernel 时间,会低估真实服务延迟。

5.2 Prefill 阶段要理解什么

Prefill 是模型“读题”的阶段:服务把完整 prompt 一次性送入模型,计算所有输入 token 的 hidden states,并为后续生成建立 KV cache。因为 prompt 中的 token 已经全部知道,prefill 可以高度并行,所以它通常更偏 compute-bound。

例子:

prompt: 请总结下面这份 4000 token 的报告...
prefill: 一次性处理这 4000 个输入 token,建立 KV cache,并产生第一个输出 token

Prefill 直接决定 TTFT(Time To First Token)的下限。prompt 越长,模型“读题”越久;如果前面还有其他请求排队,TTFT 还会继续上升。

一些系统优化也发生在 prefill 附近。例如 vLLM 的 Automatic Prefix Caching 会复用相同前缀的 KV cache;这对 RAG、长系统 prompt、多轮对话很有价值。类比一下:如果每个学生都交同一份试卷前半部分,老师没必要每次重新读一遍公共题干。

5.3 Decode 阶段要理解什么

Decode 是模型“逐字答题”的阶段。自回归模型必须等上一个 token 出来后,才能把它追加到上下文里预测下一个 token,因此 decode 在单条序列内部是串行的。

第 1 步:prompt -> 北
第 2 步:prompt + 北 -> 京
第 3 步:prompt + 北京 -> 是

Decode 阶段有两个关键事实:

  • 每生成一个 token 都要读一遍模型权重,所以它常常 memory-bound。
  • KV cache 避免了重复计算历史 token,但每步仍要读取历史 K/V 来做 attention。

为什么 batching 能提升吞吐?假设服务同时有 16 个请求都在 decode。GPU 读取一次权重,可以顺便为 16 个请求各生成一个 token。单个请求未必更快,但系统总输出 token/s 会变高。

这也是 latency-throughput trade-off 的来源:batch 大,GPU 利用率高,吞吐好;但请求可能等 batch、等调度、和其他请求共享资源,单请求延迟变差。

5.4 生成策略属于哪里

模型前向计算输出的是 logits,也就是每个 token 的分数。生成策略负责把分数变成真正输出的 token。

常见参数:

参数 作用 直观理解
temperature 调整分布尖锐程度 越低越保守,越高越发散
top_p 只在累计概率前 p 的 token 中采样 从“高概率候选集合”里抽
top_k 只保留概率最高的 k 个 token 限制候选菜单大小
max_tokens 最多生成多少 token 防止输出无限变长
stop 遇到指定字符串/token 停止 例如遇到 </answer> 停止
repetition_penalty 惩罚重复 token 减少复读

如果 temperature=0 或接近 0,模型更像做选择题,总选最高分答案;如果 temperature 较高,则更像写作时保留更多表达可能。Beam search 会在每一步保留多个候选序列,适合翻译、图像描述这类输入约束较强的任务,但开放式聊天里可能更容易产生保守、重复的回答。工程上,生成参数不仅影响质量,也影响性能:max_tokens 越大,最坏 E2E latency 越高;beam search、parallel sampling 会增加每个请求的计算和 KV cache 压力。

什么时候停止生成也很重要。最常见的停止条件有四种:

  • 模型生成 EOS token。EOS 是 End Of Sequence,表示模型认为序列结束。
  • 达到 max_tokens 或 max_new_tokens 上限。
  • 命中用户配置的 stop string,例如生成到 </answer>。
  • 客户端取消、服务端 timeout,或调度器因资源压力中断请求。

EOS 是 tokenizer 词表里的特殊 token,stop string 则是服务层按文本匹配的停止规则。GenerationConfig 里常见的 eos_token_id、forced_eos_token_id、stop_strings 就对应这些控制。二者经常一起用:EOS 让模型自然结束,stop string 用来适配业务协议或结构化输出。

还有一类优化叫 speculative decoding:让小模型先草拟多个 token,大模型一次性验证。类比写论文时让助研先打草稿,导师只负责批改和确认。如果草稿命中率高,decode 可以明显加速。

5.5 什么是 PD 分离

PD 分离(prefill-decode disaggregation)指把 prefill 和 decode 放到不同 worker、不同 GPU,甚至不同机器上执行。动机很直接:prefill 更偏 compute-bound,decode 更偏 memory-bound;把它们混在同一批 GPU 上时,长 prompt 的 prefill 可能打断正在 decode 的请求,拉高 ITL,而 decode 的高频小步计算也会影响 prefill 调度。

可以把它想成餐厅里把“读菜单和备菜”和“逐道上菜”拆成两个工位。前者适合大块时间集中处理,后者需要稳定、频繁、低抖动地出菜。拆开后,每个工位能按自己的瓶颈配置资源。

研究系统 Splitwise 和 DistServe 都讨论了这种 phase splitting / disaggregation 思路。收益是降低 prefill 和 decode 的相互干扰,并可分别优化 TTFT 和 TPOT;代价是系统复杂度更高,需要在阶段之间传输 KV cache 或中间状态,对网络、调度器和容错都有更高要求。小规模单机部署通常不必一开始就做 PD 分离,但长上下文、高并发、严格 SLO 的生产集群会越来越关注它。

6. Serving 框架对比

6.1 vLLM

vLLM 的核心目标是 GPU 服务端高吞吐推理。它最出名的设计是 PagedAttention:借鉴操作系统虚拟内存分页思想,把 KV cache 分块管理,减少连续大块显存分配带来的碎片和浪费。

直观地说,传统 KV cache 管理像给每个请求预留一整排连续座位;如果客人中途离开或增加,座位容易碎片化。PagedAttention 像把座位拆成小块,按需分配、可复用、可共享,从而容纳更多并发请求。

vLLM 适合:

  • 部署 Hugging Face 上的主流 open-weight 模型。
  • 提供 OpenAI-compatible API。
  • 追求较高吞吐和较成熟的生产部署能力。
  • 需要量化、多卡、prefix caching、benchmark 工具链。

它不只是“模型 runner”,而是一个面向 serving 的系统:调度、batching、KV cache 管理、metrics、并行策略都很重要。学习 vLLM,重点不是背启动命令,而是理解它为什么能在同等显存下塞进更多有效请求。

6.2 Ollama

Ollama 更像“本地模型运行器 + 模型管理工具”。它降低了本地跑模型的门槛:ollama run llama3.1 就能拉模型、启动 runtime、进入对话。它默认提供本地 API,官方文档中默认 base URL 是 http://localhost:11434/api,并支持 OpenAI compatibility。

Ollama 的一个重要概念是 Modelfile,类似 Dockerfile:用 FROM 指定基础模型,用 SYSTEM 写系统提示词,用 PARAMETER 固定 temperature、context length 等运行参数。

它适合:

  • 个人电脑、本地开发、离线演示。
  • 快速比较不同模型。
  • 把 GGUF 模型包装成简单 API。
  • 对极致吞吐要求不高,但希望易用。

Ollama 通常不是大规模线上 GPU serving 的首选,但非常适合建立直觉:模型如何被拉取、加载、运行、暴露 API,以及量化模型在本地硬件上如何权衡速度和质量。

6.3 SGLang

SGLang 的定位是高性能 serving 和复杂 LLM program 执行。它不只关心“单次 chat completion”,还关心多轮调用、结构化输出、分支控制、工具调用、agent workflow 等更复杂的推理程序。

其论文 SGLang: Efficient Execution of Structured Language Model Programs 提出了前端语言和后端 runtime 的组合;后端中的 RadixAttention 用于复用共享前缀的 KV cache。对多轮对话、few-shot prompt、RAG 模板这类“很多请求共享长前缀”的工作负载,这类优化很关键。

可以把 SGLang 想成“懂得复用草稿纸的考试系统”:如果 100 道题都共享同一段材料,它不会让模型每次从头读材料,而是尽量复用已经算过的前缀状态。

它适合:

  • 结构化输出,如 JSON、regex、schema-constrained generation。
  • agent、多步推理、复杂 prompt program。
  • 高吞吐、低延迟、多卡 serving。
  • 需要快速跟进新模型和 reasoning parser 的场景。

6.4 其他相关框架

除了 vLLM、Ollama、SGLang,还常见以下系统:

  • llama.cpp:C/C++ 实现,主打 GGUF、CPU/Metal/CUDA/端侧推理,是很多本地工具的底座。
  • TensorRT-LLM:NVIDIA 面向高性能推理的优化库,适合愿意为特定硬件做更深优化的生产场景。
  • Triton Inference Server:通用推理服务框架,可服务多种模型后端,不只 LLM。
  • LMDeploy:国内常见的 LLM 部署工具链,支持 TurboMind、量化和服务化。
  • Text Generation Inference:Hugging Face 早期生产 serving 代表,现在更适合作为历史和架构参考。

选型时可以先问:我要的是本地易用、服务端吞吐、复杂程序执行、硬件极致优化,还是通用推理平台?不同答案会指向不同工具。

7. 并发、Batch 与调度

7.1 什么是并发

“并发”在 LLM serving 里至少有四种口径:

口径 含义
客户端并发 同时有多少客户端连接或请求在发起
排队并发 有多少请求已经进入系统但还没完成
running sequences 有多少序列正在被调度器处理
batch size / batched tokens 某次 GPU forward 实际合并了多少序列或 token

用户说“并发 100”,可能只是压测工具同时开 100 个连接;但 GPU 看到的可能是某一时刻 batch 里有 32 条 decode 序列,另有 68 条排队或等待 prefill。分析问题时必须先明确口径。

一个小例子:咖啡店门口有 100 人排队,不代表咖啡机同时在做 100 杯。真正同时制作的杯数取决于咖啡机容量、店员调度、每杯饮品复杂度和是否有人点超大杯。

7.2 并发和吞吐是什么关系

并发升高通常会先提升吞吐,因为 GPU 从“小批量低利用率”变成“更大 batch、更高利用率”。但超过某个点后,吞吐会趋于饱和,延迟开始快速上升。

可以用一个简化关系理解:

$$ \overline{L} = \lambda,\overline{W} $$

这就是 Little’s Law 在服务系统中的直觉版本:$\overline{L}$ 是系统内平均未完成请求数,$\lambda$ 是稳定到达率(例如 QPS),$\overline{W}$ 是平均端到端延迟。假设服务稳定处理 10 QPS,每个请求平均 5 秒完成,那么系统里平均大约有 50 个请求处于未完成状态。

LLM 的特殊性在于,不同请求的“大小”差异很大:一个 50 token 输入、20 token 输出的请求,和一个 8000 token 输入、2000 token 输出的请求,对 GPU 的占用完全不是一个量级。因此只说 QPS 不够,必须同时说 input/output token 分布。

7.3 Serving 调度器要解决什么

LLM serving 调度器的任务是把请求组织成 GPU 能高效处理的批次,同时尽量控制延迟。它要处理几类矛盾:

  • prefill 计算重,decode 更频繁但每步较小。
  • 新请求希望尽快 prefill 以降低 TTFT,老请求希望 decode 连续进行以降低 ITL。
  • batch 大有利于吞吐,但会增加等待和干扰。
  • 长请求占用 KV cache 久,短请求又不应该被饿死。

continuous batching 的思想是:不要等一整个固定 batch 全部完成后再接新请求,而是在每个 decode step 动态加入新请求、移除完成请求。这比传统静态 batching 更适合 LLM,因为每个请求输出长度不同。

chunked prefill 则是把很长的 prefill 切成块,避免一个超长 prompt 长时间占住 GPU,导致正在 decode 的请求 token 间隔变大。vLLM 的 optimization 文档 中就把 chunked prefill 作为调优点之一。

7.4 什么是容量规划

容量规划回答的问题是:在给定模型、硬件和 SLO 下,服务最多能承载多少负载。SLO 是 Service Level Objective,即服务质量目标,例如 P99 TTFT < 1s、P99 ITL < 80ms、错误率低于某个阈值。它不是“系统极限能跑多快”,而是“在用户体验可接受的条件下能跑多大负载”。

正确的压测方式不是只问“最大 QPS 是多少”,而是固定场景:

模型:Qwen2.5-Coder-7B-Instruct
硬件:1 × A100 80GB
输入长度分布:P50 1K,P95 8K
输出长度分布:P50 256,P95 1K
SLO:P99 TTFT < 1s,P99 ITL < 80ms
目标:满足 SLO 下的最大 goodput

如果没有这些约束,QPS 数字几乎不可比较。短请求 benchmark 可以把 QPS 做得很好看,但一上线到长上下文 RAG,就会因为 prefill 和 KV cache 压力完全不同而失真。

工程上可以分三类压测:短问答压测看 API overhead 和 decode;长 prompt 压测看 prefill 和 prefix cache;长输出压测看 decode 吞吐和显存稳定性。

8. 可观测性 Observability

如果说前几节是在解释“LLM serving 如何工作”,那么可观测性(observability)关心的是:服务运行时到底发生了什么,为什么变慢,哪里快要违反 SLO,改动之后是否真的变好。

在云原生系统里,可观测性通常围绕 metrics、logs、traces 展开;LLM serving 也一样,但会多出一组模型相关信号:输入/输出 token 长度、TTFT、ITL、KV cache、batch、prefix cache、GPU 显存、调度队列、goodput 等。它不是简单做一个 GPU 利用率面板,而是要把“用户请求”“模型阶段”“调度器状态”“硬件资源”连起来看。

8.1 观测对象与信号

一个 LLM 服务可以按层观测:

flowchart LR C["Client"] --> A["API Server"] A --> Q["Queue / Scheduler"] Q --> W["Model Worker"] W --> K["KV Cache"] W --> G["GPU"] A -. logs/traces .-> O["Observability Backend"] Q -. metrics .-> O W -. metrics/traces .-> O G -. GPU metrics .-> O

常见观测信号如下:

信号 记录什么 典型用途
Metrics 数值型时间序列 看趋势、报警、SLO、容量规划
Logs 离散事件和错误上下文 查失败请求、OOM、超时、异常栈
Traces 一次请求跨组件的阶段耗时 定位慢在排队、prefill、decode 还是网络
Profiles kernel、CPU、内存等细粒度性能剖析 优化热点、排查低 GPU 利用率
Benchmarks 受控负载下的实验结果 比较模型、框架、参数和硬件

一个简单类比:metrics 像体检指标,logs 像病历,traces 像从挂号到检查到开药的全流程时间线,benchmarks 则像标准化体能测试。

8.2 延迟指标

LLM serving 的延迟指标要按阶段拆:

指标 含义 主要对应阶段
TTFT Time To First Token,请求发出到第一个 token 返回 排队 + tokenization + prefill
ITL / TPOT Inter-Token Latency / Time Per Output Token,相邻输出 token 间隔 decode
E2E latency End-to-End latency,完整响应耗时 全流程
queue time 请求在调度器里等待的时间 调度
prefill latency 处理输入 prompt 的时间 prefill
decode latency 生成输出 token 的累计时间 decode

一个简化公式:

$$ T_{\mathrm{E2E}} \approx T_{\mathrm{TTFT}} + \bigl(N_{\mathrm{output}} - 1\bigr) T_{\mathrm{TPOT}} $$

如果输出很短,TTFT 主导体验;如果输出很长,TPOT 主导体验。流式聊天中,用户最先感知的是 TTFT,随后感知的是 token “蹦出来”的速度,也就是 ITL/TPOT。非流式接口则会把全部内容攒到最后返回,用户只感受到 E2E latency,但服务端仍然应该记录 TTFT 和 ITL,否则很难判断慢在 prefill 还是 decode。

8.3 吞吐与资源指标

吞吐有多个口径:

指标 单位 解释
QPS / RPS req/s 每秒完成多少请求
input throughput input tok/s 每秒处理多少输入 token
output throughput output tok/s 每秒生成多少输出 token
total token throughput tok/s 输入和输出 token 总量
per-GPU throughput tok/s/GPU 多卡场景下的资源效率

对 LLM 来说,单看 QPS 很危险。假设两个服务都是 10 QPS:服务 A 每个请求输出 20 token,服务 B 每个请求输出 1000 token,它们的 GPU 压力完全不同。更稳妥的表达是:“在某个输入/输出长度分布下,满足某个延迟 SLO 时,每张 GPU 每秒能产出多少 output tokens。”

还要同时看资源和调度指标:

  • GPU utilization、显存使用量、显存带宽、功耗、温度。
  • KV cache 使用量、block 使用率、prefix cache hit rate。
  • scheduler queue length、running requests、waiting requests。
  • preemption / eviction 次数。
  • HTTP 4xx/5xx、timeout、cancel、OOM。
  • cost per 1M tokens。

vLLM Production Metrics 就暴露了 generation tokens、prefix cache hits、preemptions、speculative decoding、KV connector 等指标;这些指标比单纯的 GPU utilization 更接近 LLM serving 的真实瓶颈。

8.4 分位数 SLO 与 Goodput

线上服务不要只看平均值。平均 TTFT 可能是 300ms,但 P99 TTFT 是 5s,用户投诉通常来自后者。

P50 TTFT = 300ms:一半请求首 token 延迟小于 300ms
P90 TTFT = 900ms:90% 请求小于 900ms
P99 TTFT = 5s:99% 请求小于 5s,最慢 1% 更糟

LLM 服务的长尾来自很多地方:个别超长 prompt、调度器排队、KV cache preemption、GPU kernel 抖动、网络慢客户端、模型冷加载、输出长度差异。博士生做实验时尤其要注意:只报告 mean latency 往往不够,至少应报告 P50/P90/P99 和长度分布。

Goodput 指的是满足 SLO 的有效吞吐。例如系统每秒完成 100 个请求,但只有 70 个请求满足 TTFT < 1s 且 TPOT < 80ms,那么 goodput 是 70 req/s,而不是 100 req/s。

一个常见误区是追求最高 output tok/s。极限吞吐通常靠大 batch 换来,可能显著牺牲 TTFT 和 P99;真正上线时,要在 SLO 约束下比较吞吐。

8.5 日志与 Trace

Metrics 适合看趋势,但一次具体请求为什么慢,通常要靠日志和 trace。建议至少记录这些字段:

字段 例子 用途
request id req-123 串联 API server、scheduler、worker 日志
model Qwen2.5-Coder-7B-Instruct 多模型服务定位问题
input/output tokens 1024 / 256 解释延迟和成本
queue / prefill / decode time 20ms / 300ms / 4s 阶段归因
finish reason eos / length / stop / error 判断是否异常终止
error code timeout / oom / cancelled 失败分类

Trace 则把一次请求拆成多个 span,例如:

HTTP receive
  -> auth / routing
  -> apply chat template
  -> tokenize
  -> queue wait
  -> prefill
  -> decode step loop
  -> detokenize
  -> stream response

OpenTelemetry 是云原生里常用的开放标准,用来生成、收集和导出 traces、metrics、logs。对 LLM 应用层,还可以使用 Langfuse 或 OpenLLMetry 这类工具观测 RAG、agent、tool call、prompt 版本、用户反馈和成本。它们和 serving runtime 的 metrics 不是替代关系:前者更偏应用链路,后者更偏模型服务和 GPU 资源。

日志里要谨慎处理 prompt 和输出内容。生产环境通常需要脱敏、采样、权限控制和保留周期策略,否则 observability 很容易变成隐私和合规风险。

8.6 监控技术栈

一个常见开源栈可以长这样:

flowchart LR V["vLLM / SGLang / Ollama API"] --> P["Prometheus"] D["DCGM Exporter"] --> P N["Node Exporter / cAdvisor"] --> P P --> G["Grafana Dashboard"] P --> A["Alertmanager"] App["App / Gateway"] --> O["OpenTelemetry Collector"] O --> T["Tempo / Jaeger"] O --> L["Loki / Log Backend"] G --> S["SLO / Alert Review"]

关键组件:

组件 作用
Prometheus 拉取和存储时间序列 metrics,用 PromQL 查询和报警
Grafana 展示 dashboard,关联 metrics、logs、traces
OpenTelemetry 标准化 traces、metrics、logs 的采集和导出
NVIDIA DCGM Exporter 暴露 GPU 利用率、显存、功耗、温度等指标给 Prometheus
Loki / Tempo / Jaeger 存储日志和 trace,帮助做请求级问题定位

一个最小可用方案是:serving 框架暴露 /metrics 或 Prometheus 格式指标,Prometheus 定时 scrape,Grafana 展示 TTFT/ITL/QPS/GPU/KV cache 面板,Alertmanager 根据 P99、错误率、OOM、显存水位报警。等到业务复杂后,再补 OpenTelemetry tracing 和应用层 LLM observability。

8.7 压测与基准测试

压测是主动制造负载,监控是被动观察线上运行。两者要结合:压测告诉你系统极限和拐点,监控告诉你线上是否正在接近这些拐点。

为了让实验可复现、可比较,压测至少固定:

  • 模型名、权重精度、量化方式。
  • serving 框架版本和启动参数。
  • GPU 型号、数量、driver、CUDA。
  • 输入 token 长度分布。
  • 输出 token 长度分布或 max_tokens。
  • stream / non-stream。
  • 并发模式:固定 concurrency、固定 request rate,还是 burst。
  • 采样参数:temperature、top_p、stop。
  • warmup 次数和统计窗口。

常见工具:

工具 适合做什么
vLLM benchmark vLLM 自带 benchmark,输出 request/input/output throughput、TTFT、TPOT、ITL 等
AIPerf 新一代 GenAI benchmarking 工具,支持多种 endpoint、数据集、并发和请求率控制
GenAI-Perf NVIDIA 旧工具,文档已说明逐步转向 AIPerf,但仍是理解 LLM benchmark 指标的好材料
Grafana k6 / Locust 通用 HTTP 负载测试,适合压 API gateway、鉴权、代理层,但需要自己统计 token 指标

读 benchmark 结果时,第一反应应该是看 workload,而不是直接比较榜单数字。短问答、长 prompt、长输出、多轮对话、RAG、agent 工具调用,会得到完全不同的 TTFT、ITL、吞吐和显存曲线。

9. 量化与显存优化

9.1 为什么需要量化

量化的目标是用更少 bit 表示原本高精度的数值。LLM 推理中最常见的是权重量化和 KV cache 量化。

最简单的显存估算:

$$ M_{\mathrm{weights}} \approx N_{\mathrm{params}} \times \frac{q}{8} $$

其中 $q$ 是每个参数的 bit 数。因此,忽略 scale、zero point 等量化元数据时:

$$ M_{\text{7B, FP16/BF16}} \approx 7 \times 10^9 \times 2\text{ bytes} \approx 14\text{ GB} $$

$$ M_{\text{7B, INT4}} \approx 7 \times 10^9 \times 0.5\text{ bytes} \approx 3.5\text{ GB} $$

量化的收益有三类:

  1. 省显存:模型能放进更小的 GPU,或同一张 GPU 支持更大 batch。
  2. 提速度:decode 往往受显存带宽限制,数据更小,搬运更快。
  3. 降成本:同等负载可以用更少或更便宜的硬件。

代价是精度损失和 kernel 复杂度。不是所有量化都会变快:如果硬件或 runtime 没有高效 kernel,反量化开销可能抵消收益。

9.2 权重量化

权重量化作用于模型固定参数。常见方法:

方法 典型格式 关键思想
LLM.int8() INT8 混合精度 大部分用 INT8,离群维度保留高精度
GPTQ W4/W3 用近似二阶信息补偿量化误差
AWQ W4A16 根据激活分布保护重要权重通道
SmoothQuant W8A8 把激活离群值难度迁移到权重上
QLoRA NF4 + LoRA 冻结 4-bit 基座,只训练低秩适配器
GGUF K-quants Q4_K_M、Q5_K_M 等 llama.cpp/Ollama 本地推理常用格式

理解量化可以从一个标尺类比开始:原来用毫米级尺子测长度,现在只给 16 个刻度。刻度越少,记录越省空间,但误差越大。优秀的量化方法就是想办法把刻度放在“最常出现、最重要”的数值区域,而不是平均撒开。

在服务端 GPU 上,AWQ/GPTQ/FP8 等是否值得用,要看框架支持和 kernel 效率。vLLM 的 quantization 文档 列出了支持的量化后端;Ollama/llama.cpp 则更多围绕 GGUF 量化模型。

9.3 KV cache 优化

KV cache 不是模型文件里下载来的东西,而是推理时动态产生的。每个请求的每个历史 token,都会在每层 attention 中留下 Key 和 Value,用于后续 token 查询。

它解决的是“不要重复读题”的问题:生成第 100 个 token 时,模型不需要重新计算前 99 个 token 的全部中间状态,只需要复用 KV cache。但代价是显存随上下文长度和并发线性增长。

这里要和 1.4 节的上下文压缩区分开:上下文压缩是在模型计算前减少输入 token 或历史信息,KV cache 优化是在模型已经保留这些 token 后,降低缓存它们的显存和调度成本。前者改变 prompt,后者改变 runtime 管理方式。

KV cache 优化有几类:

  • dtype 降低:例如 FP16/BF16 改成 FP8 或 INT8。
  • KV cache quantization:如 KIVI、KVQuant 等研究。
  • prefix cache:复用相同前缀的 KV。
  • paged/block 管理:减少显存碎片,例如 PagedAttention。
  • eviction/offloading:长上下文场景下把部分 KV 移出 GPU 或丢弃不重要 token。

KIVI 的一个重要观察是:Key 和 Value 的分布不同,Key 更适合 per-channel 量化,Value 更适合 per-token 量化。这说明 KV cache 量化不是简单把所有数压成 INT4,而要根据张量统计特性设计。

vLLM 已有 quantized KV cache 相关支持。实践中,短上下文、低并发场景开启 KV 量化未必更快;长上下文、高并发、显存紧张时,KV cache 优化才更容易体现价值。

9.4 量化选择要问什么

量化选型可以按下面的问题走:

  1. 目标是省权重显存,还是省 KV cache 显存?
  2. 场景是本地 CPU/消费级 GPU,还是服务端 A100/H100?
  3. 框架是否原生支持该量化格式?
  4. 是否有高效 kernel,还是只是“能跑”?
  5. 精度损失是否在业务可接受范围内?
  6. benchmark 是否覆盖真实输入输出长度和并发?

几个经验判断:

  • 本地运行优先看 GGUF 和 Ollama/llama.cpp 生态。
  • 单卡显存放不下模型时,先考虑权重量化。
  • 长上下文并发撑爆显存时,再重点考虑 KV cache 量化或 prefix cache。
  • H100/Blackwell 等新硬件上,FP8 往往更值得关注。
  • 量化后必须重新评估质量,特别是代码、数学、工具调用和结构化输出场景。

最终,量化不是单独的“压缩技巧”,而是 serving 系统设计的一部分。它改变权重大小、KV cache 容量、batch 上限、decode 带宽压力,最终反映到 TTFT、ITL、吞吐和 goodput 上。

Gemini Cli实现“选中文字添加日程”的右键功能

2026-01-26 19:04:21

前言

近来我在摸索将Gemini Cli等终端agents融入工作流,目前总体感觉是——这类工具确实能够将脑力和精力从“杂活”和“需要但不感兴趣”的事情中解放出来,效率提升很多。这篇短文将介绍一个最近的实践案例。

作为博士生,我经常需要做的一件事是手动将电子邮件中的会议、报告、答辩等信息添加到Apple日历中。手动设置事件的标题、时间、地点、提前提醒时间、线上开会链接等内容属实麻烦。在ChatGPT出圈后,我一直想要开发一个功能——选中文字后直接在右键菜单中点击“添加日程”,程序就能够调用LLM去解析文字,生成Apple日历事件并导入到日历中。

然而,这个想法在脑海中,一直也没有去实现。最近,需要添加的事件尤其多,今晚吃饭回来后又想到了这一茬儿,于是打开Gemini Cli,给出需求,让它设计方案并实现,十分顺利。

现在我只需要在邮件中选中文字,点击右键,然后选择“添加日程”:

Xnip2026-01-26_19-56-57

稍等片刻,就能在日历中看到对应的事项了:

Xnip2026-01-26_19-58-17

代码仓库:https://github.com/brant-ruan/text2agenda

实践

为了提高成功率,我在Gemini Cli中将模型设定为gemini-3-pro-preview。初始需求描述如下:

我想要在当前目录下开发一个软件或者借助Mac上已有的automator等,它的功能是给我的macbook添加一个右键菜单“添加日程”,
使用场景是选中一段文字后,比如在Outlook中选中一个关于会议日程信息通知邮件的文字部分,右键,选择“添加日程”,
然后自动调用OpenAI/DeepSeek/Gemini等大模型的API,将其解析成.ics(苹果日程格式),然后自动导入到我的系统日历中。
这个项目允许用户通过配置文件配置API KEY等配置项。

很快,Gemini给出了实现方案:

I will design a solution using a Python script as the core logic engine (handling the API call and Calendar interaction) and use macOS Automator to trigger it via the right-click menu.

Here is the development plan:

1. Project Setup & Configuration
* Structure: Create a directory for the project.
* Dependencies: Use Python's requests library to communicate with LLM APIs.
* Config: Create a config.json file where you can define your API Key, Base URL (allowing use of OpenAI, DeepSeek, or Gemini via compatible endpoints), and Model name.

2. Core Logic Implementation (remind.py)
* Input: The script will accept text arguments (the selected text from your email/document).
* Context: It will automatically detect the current system date/time so that relative dates like "next Friday" are parsed correctly.
* LLM Processing: It will send the text to the configured API with a prompt to extract event details (Title, Start Time, End Time, Location, Notes) in a strict JSON format.
* Calendar Automation: Instead of just generating an .ics file (which requires manual clicking to import), I will use AppleScript (executed via Python) to directly insert the event into your macOS Calendar app.

3. Verification
* I will create a dummy test to ensure the script runs and attempts to reach the API (handling errors gracefully if the API key is not yet set).

4. Integration (The "Right-Click" Menu)
* Since I cannot interact with your macOS GUI to create the Automator workflow, I will generate the Python script and then provide you with exact, simple steps to create the Automator Quick Action that runs this script.

I will start by setting up the project structure and the configuration file.

方案不错,批准了,请执行!很快,Gemini大哥就完成实现,并且给出了配置指南:

You now have a fully functional backend to parse text into Calendar events using an LLM.

Project Files Created
* remind.py: The main logic script.
* config.json: Configuration file for your API Key.
* run_remind.sh: A helper script for Automator to run the Python code easily.
* venv/: A Python virtual environment with necessary libraries installed.

Step 1: Configure API Key
Open the configuration file and paste your OpenAI (or compatible) API Key.
 open config.json
Change `YOUR_API_KEY_HERE` to your actual key.

Step 2: Create the "Right-Click" Menu (Automator)
Since this part interacts with the macOS UI, you need to perform these steps manually once:

1. Open Automator (cmd+space, type "Automator").
2. Choose New Document -> Quick Action.
3. At the top, configure:
   * Workflow receives current Text in Any Application.
4. In the left sidebar, search for Run Shell Script and drag it to the right.
5. Configure the script action:
   * Shell: /bin/bash
   * Pass input: as arguments (Important!)
   * Paste the following command (using the absolute path to your project):
         /ABSOLUTE_PATH/run_remind.sh "$1"
6. Press Cmd+S to save. Name it Add Event (or "添加日程").

How to Use
1. Select any text containing a date/time (e.g., in an email or website).
2. Right-click the text.
3. Go to Services (or just look at the bottom of the menu) -> Add Event.
4. The script will run in the background, send the text to the LLM, and automatically add the event to your macOS Calendar.

Troubleshooting:
If nothing happens, check the log file created by the script:
 cat /tmp/remind_log.txt

按照它的提示配置完(文字解析使用的是物美价廉的gpt-5-nano),我测试了一下,检查日志,发现了问题,再次交互:

遇到了问题:Analyzing text...
  Error calling API: 400 Client Error: Bad Request for url: https://api.openai.com/v1/chat/completions
  Failed to extract event data.
我建议你使用litellm来调用,而非自己手写request

Gemini很快完成了基于LiteLLM的实现。我再次提出需求:

现在就不必在配置文件中添加base_url了,不过我希望配置文件中支持设置提前几分钟提醒。

Gemini再次完成实现。现在,整个流程已经很流畅了。不过,我还希望更方便一些:

 我怎么才能将这个右键菜单从“Services”中移出来到一级菜单?

Gemini大哥不厌其烦地给出指点:

在 macOS 系统中,原生不支持用户直接将 Automator (自动操作) 创建的“快速操作”强行固定在右键的一级菜单中。macOS
会根据你启用的服务数量,自动决定是将它们折叠进“服务 (Services)”子菜单,还是显示在菜单底部。

要想实现“在一级菜单直接点击”,通常有以下两种方案:

方案一:清理“服务”菜单(原生,但不保证 100% 成功)
macOS 的逻辑是:如果启用的“服务”很少,它有时会将它们直接显示在右键菜单底部,而不折叠进子菜单。你可以尝试禁用不用的服务:
1. 打开 系统设置 (System Settings) -> 键盘 (Keyboard)。
2. 点击 键盘快捷键 (Keyboard Shortcuts) -> 服务 (Services)。
3. 在“文本 (Text)”分类下,取消勾选你不常用的所有服务,只保留刚才创建的“Add Event”。
4. 重启应用(如浏览器或邮件)看是否生效。
...

我再次尝试,发现了一个问题——设置的提前15分钟变成了15小时,于是再次发问:

现在似乎填写的提前15分钟最终在日历中变成了提前15小时,请改正。另外,我希望生成的事件能够体现出事件的本质特征。
例如,对于下面的文字,我希望生成的事件标题为Anonymous's PH.D Defense之类,而不是他的defense题目(...):
PH.D DEFENCE - PUBLIC SEMINAR

Title:    ...
Speaker:    Mr. Anonymous
Date/Time:    1 February 2026, Monday, 03:00 PM to 04:30 PM
Venue:    XXXX, XXXX XX-XX
Advisor:    Dr XXX, XXX, School of Computing

Gemini再次更新代码中的prompt。至此,这个小玩意儿已经完全满足了我的使用需求。

VPSS:面向软件供应链的漏洞影响评估

2025-12-02 11:39:20

简介

本文在“安全研究GoSSIP”公众号上首发。

老读者从首尔发回报道——11月17日到19日,第四十届软件工程领域国际会议ASE 2025在韩国首尔顺利举行。今年的ASE会议一共收到了1181篇投稿,最终录用了246篇论文。其中,来自中国的作者占比超过60%(含中国大陆及港澳台),体现了在软件工程领域的强劲实力。在所有提交和接收的论文中,研究主题的Top 3均为“AI和SE”、“测试和分析”、“安全及其他非功能特性”。

今天为大家推荐的论文是发表在这次ASE上的,一篇关于软件供应链漏洞影响评估的工作:Propagation-Based Vulnerability Impact Assessment for Software Supply Chains。该工作由新加坡国立大学梁振凯研究组完成并投稿。

随着软件生态变得日益复杂,软件供应链中的漏洞不仅影响漏洞所在的项目,还为依赖其所在项目的下游项目带来了安全风险——即使下游项目业务自身安全,攻击者还是能够通过利用这些上游漏洞来达到攻击下游项目的目的。因此,识别一个漏洞在软件供应链中的影响范围和规模变得越来越重要。

然而,现有工作并不能满足这一需求。一方面,已有研究要么不精确——只考虑了软件包层级(package-level)的依赖关系,没有进一步考虑漏洞所在函数是否真正可达,从而引入了大量虚假告警,要么是不完整的——通过在软件生态中采样等方式考察了局部或部分的函数调用图级(call-graph-level)依赖关系;另一方面,当前已被广泛采用的漏洞评估指标,如CVSS、CWE等,都只能用于对漏洞本身进行评估,而无法反映漏洞在软件供应链中的影响。

为了实现精确、完整的软件供应链漏洞影响评估,受程序分析中的数据流分析算法启发,论文首先提出了一个基于worklist并支持多层级剪枝的漏洞传播分析算法,能够精确、高效地在函数调用图层级、面向漏洞所在整个软件供应链生态实现漏洞传播分析。在此基础上,论文进一步提出了漏洞传播评分系统(Vulnerability Propagation Scoring System,简称VPSS),对漏洞传播分析结果进行建模,帮助人们对漏洞在软件生态中的影响进行量化和感知,支撑更加科学、及时的漏洞治理工作。

论文作者在Java Maven生态系统上实现了工具原型,并使用100个Maven生态中的漏洞进行了测试。评估结果显示,论文提出的算法能够完成全Maven生态的漏洞影响评估,相应的VPSS分数能够提供有价值的漏洞影响规模情报。

该论文的贡献主要包括:

  • 设计了一种新的漏洞传播分析算法,实现在函数调用图层级、面向漏洞所在整个软件供应链生态识别受漏洞影响的下游依赖项目。
  • 提出了漏洞传播评分系统(Vulnerability Propagation Scoring System,简称VPSS),这是第一个专门用于量化软件供应链中漏洞影响的动态评估指标。
  • 在Java Maven生态系统上实现了算法和指标的工具原型,并使用100个Java Maven生态中的漏洞进行了测试。工具原型和用于测试的漏洞数据集已经开源。

背景

软件供应链安全已成为信息安全领域的焦点。一个上游库的漏洞(例如Heartbleed 或 Log4Shell )就可能使其下游成千上万的软件项目处于风险之中 。因此,在漏洞被披露后,快速准确地评估其“影响范围”(Impact Scope)至关重要 。然而,现有的研究在尝试解决这个问题时,面临两大局限:

缺乏准确且完整的传播分析:

  1. 粒度太粗:大多数分析停留在“包级别”(package-level)。它们只检查下游项目是否“声明”或包含了对脆弱包的依赖。但这会带来大量的假阳性(false positives),因为下游项目很可能只是依赖了那个包,却根本没有调用到漏洞所在的脆弱函数(vulnerable function,VF)。
  2. 范围太窄:另一部分研究虽然尝试了更精确的“调用图”(call graph, CG)级别分析,但它们的能力非常受限——要么无法处理复杂的“全生态系统”依赖 ,要么只分析有限的下游项目 ,并且常常忽略了“传递性依赖”带来的深层风险。

作者进一步从六个方面衡量、对比了现有工作,并得出理想的漏洞传播分析应该具备的特性:

related

缺乏量化供应链“传播影响”的指标:例如,我们熟知的CVSS评分系统,是用来评估漏洞本身的固有特征(如攻击复杂度、所需权限等),但它无法衡量漏洞在供应链中的“传播影响”。一个CVSS 9.8分的漏洞,如果只存在于一个无人问津的库中,其真实的供应链风险可能远小于一个7.5分但存在于核心基础库中的漏洞。CVSS v4.0的官方FAQ也明确承认了CVSS不适用于衡量供应链的传播影响。

针对上述痛点,作者希望设计一个既准确(CG级别)又完整(全生态、传递性)的分析框架,并提出一个专门用于量化传播影响的新指标。

研究方法

为了实现这一目标,作者设计了一个包含四个步骤的语言无关的自动化漏洞影响评估框架,如下图所示:

overview

一、依赖图构建 (Dependency Graph Construction)

首先,作者需要构建一个描述目标软件生态中全部依赖关系的依赖图。但如果直接构建“项目-版本”(PV)级别的图,规模将大到无法处理(例如Java Maven生态有超过1500万个PV )。作者的关键洞察是:转而构建P-level(项目级别)的依赖图。这个图的规模要小得多(Maven中约66万个P ),可以作为高效的预过滤器 ,同时将具体的版本依赖信息(deps.json)用于后续的精确查询 。这个依赖图只需要构建一次,然后随着软件生态官方仓库做动态增量更新即可。

二、脆弱函数识别 (Vulnerable Function Identification)

为了进行CG级别的分析,我们必须首先知道“漏洞在哪几个函数里”。对此,作者采用了一种LLM辅助的优化策略 :首先,使用传统的patch-based方法从补丁中提取候选的VF。然后,利用LLM(作者测试了GPT-4o-mini等模型)强大的代码语义理解能力,自动过滤掉那些与漏洞无关的修改,包括“语义等价的修改”(如重命名变量、增删空格或注释等)和“语义改变但无关的修改”(如增删日志(logging)代码等)。

vfi

三、漏洞传播分析 (Vulnerability Propagation Analysis)

这是作者提出的分析框架的核心。他们设计了一种新颖的“分层基于工作列表(Hierarchical Worklist-based)”的算法:

propagation

为什么用Worklist?论文的算法借鉴了数据流分析中的经典“工作列表”思想 。这使作者能系统地遍历整个生态依赖图,有效处理复杂的共享依赖,甚至是真实世界中存在的“循环依赖”问题 ,确保分析的健全性(soundness)。

为什么是Hierarchical(分层)?这是为了效率。直接对全生态构建CG进行分析是计算上不可行的 。作者设计了三级“分层剪枝(Hierarchical Pruning)”策略 ,逐层过滤掉海量的“假阳性”依赖关系:

  • 版本剪枝(Version-based Pruning):检查下游项目是否依赖了受影响的上游版本 。
  • 导入剪枝(Import-based Pruning):检查下游项目是否 真正导入(import)了上游包的内容 。在这一步,作者还通过识别“内在范围”(intrinsic scope),巧妙地处理了“fat JAR”等打包问题。
  • CG级别剪枝(CG-level Pruning): 最后,作者才对剩下的候选PV构建CG,检查下游项目是否真正调用了上游的脆弱入口点(entry-points)。

四、VPSS计算 (VPSS Calculation)

在精确分析完传播范围后,作者引入了一个全新的量化指标:VPSS(Vulnerability Propagation Scoring System),用于量化漏洞在供应链中的影响范围和演变。它和CVSS一样,采用0-10分制,并分为低、中、高、危急四个等级,易于理解和使用。注意,VPSS是“时间感知(Time-aware)”的,因此可以计算漏洞在任意时间点(例如t)的VPSS分数(计算时会排除t之后发布的P和PV),从而动态追踪其影响力的演变。

VPSS的核心公式是VPSS_raw = PBF x PDF,其中:

  • PBF(传播广度因子): 衡量漏洞传播的宽度。它综合考虑了受影响的下游P和PV的数量(包括直接和传递依赖)。
  • PDF(传播深度因子): 衡量漏洞传播的深度。它基于传播链的最大和平均长度来计算 。

详细的计算过程如下:

vpss

评估

作者在Java Maven软件生态上实现了分析框架的原型 ,并抽取了100个真实CVE进行评估 。

一、漏洞传播分析的有效性和可扩展性如何?

发现1:分层剪枝策略效果显著。在分析100个CVE的过程中,平均97.8%的项目(P)和99.2%的项目版本(PV)被成功剪枝。

发现2:假阳性比例大。如下图所示,如果仅基于依赖声明(即“initial”状态),平均一个漏洞会涉及上千个项目和数万个PV。而作者的分析(v3)最终确认,高达94.9%的PV是假阳性(即它们虽然依赖了,但根本没调用)。这大大降低了CG的构建成本 。

eval1

二、VPSS分数能告诉我们什么?

作者计算了这100个CVE在披露后24个月内(每30天一个快照,t0到t23)的VPSS分数演变。

发现1:VPSS能动态反映生态修复趋势。如下图所示,绝大多数漏洞的VPSS分数都随时间推移呈下降趋势。这完全符合预期:随着补丁被应用、下游项目逐渐迁移到安全版本,漏洞的实际传播影响在不断减小。

发现2:VPSS能捕获异常的“延迟更新”现象。作者也发现了一个有趣的特例:CVE-2016-3086的分数在t1时刻反而异常上升了(从6.95升至7.19)。作者推测这可能是由于下游项目“延迟更新”其依赖所导致的。

eval2

发现3:大部分漏洞的传播影响有限。如下面的箱线图所示,在100个CVE中,绝大多数的VPSS分数都保持在较低水平,并在两年内趋于稳定。

eval3

三、案例分析:CVE-2016-5393

CVE-2016-5393在测试数据集中VPSS得分最高(t0时为7.35)。作者分析发现,它在披露时直接影响了228个项目,并传递影响了另外154个项目。其最长传播链达到了7跳,并且在24个月后分数依然高达6.86,反映了生态中“长尾依赖”的顽固性。

eval4

论文链接:https://arxiv.org/pdf/2506.01342

VPSS开源项目链接:https://github.com/brant-ruan/vpss

作者团队十分欢迎大家交流合作,一起推动软件供应链漏洞治理工作!

作者信息

投稿作者简介:阮博男,2024级新加坡国立大学博士生,导师为梁振凯老师,目前主要研究方向为漏洞挖掘和漏洞评估,相关研究成果发表在ASE、USENIX Security、RAID等软件工程和信息安全领域的国际会议上,曾获得RAID 2024最佳实践论文奖和USENIX Security 2025杰出论文奖,也曾在BlackHat Asia、KCon、CIS等国内外知名工业界信息安全会议上发表演讲。

个人主页:https://profile.wohin.me

如果对NUS Curiosity Research Group感兴趣,梁振凯老师也在招收系统安全方向的博士生和博士后,详细信息可以参考:

梁振凯老师的个人主页:https://www.comp.nus.edu.sg/~liangzk

NUS Curiosity研究小组主页:https://nus-curiosity.github.io