普通视图

发现新文章,点击刷新页面。
今天 — 2026年8月24日極客死亡計劃

大脑充血 Vol.95

2026年8月24日 08:05

读者早上好,这有可能是《大脑充血》最后一期名副其实的「周刊」。我这周在 联邦宇宙 上发了个投票,说我打算改月刊,想投票看看固定在什么时间点发刊合适。结果有超过一半的人选了「不准改月刊」的选项,我真的好讨厌你们

主要的原因是,每周都要更新一次,其实占据了很多我本来就不多的写作时间。我最近在写一篇小说,还有其他不少的项目(包括要写的文章点子,准备写新的博客主题等等),但每次坐在自己的电脑前都想着,自己周刊还没写呢。说实话,这不太利于我的创作和心理健康,再加上我最近读的文章真的少了,有闲心都去读书了(虽然现在的读书进度也很慢了)。

总之,在结合了投票结果之后,我决定把《大脑充血》改成月刊,发刊日期是离每月 1 日最近的周一,这样我就不用把页面路径的 weekly 改掉了,毕竟还是以周为单位。除了固定发刊日,我还会考虑在其他时间随机掉落一些时效性更强的更新,就跟 Taxodium 的 Zine 类似。

不过,下周一(8 月 31 日)刚好就是离九月初最近的周一呢,所以还是会发布的。


吾仅悉落

Little Shop of Horror music cover

Little Shop of Horror

Alan Menken / Howard Ashman

这是音乐剧《Little Shop of Horror》(恐怖小店)的原声带,说实在的,其实没有特别好听,百听不厌的几首只有《Dentist》《Feed Me》或许还有《Suddenly, Seymour》。音乐剧也没有什么深意,如果要看反映底层人民生活的音乐剧,《吉屋出租》兴许更好。对于一部上世纪的音乐剧来说,《恐怖小店》的经典就在于它融合了喜剧、悲剧和血腥恐怖。至少…… 结局的时候看到被花吃掉的主演穿着花衣服走出来唱「Don’t feed the plant!」的时候,还是很喜感的。

另外,《恐怖小店》中 Audrey Two 的形象,貌似是《植物大战僵尸》和《超级马里奥》中食人花形象的来源?我不确定,但总觉得它们之间是有联系的。


心重如毛

仲树让我反思播客这一媒介

说实话,我的心情很复杂,在这周的某天打开小红书之前,我都还是《独树不成林》的忠实听众,尤其喜欢运动、文学、哲学以及闲聊内容。大概是因为和诺兰的采访为她带来了更多的名气和关注,过往的未被发掘的事件也被端到了公众面前。事情是有人发现包括《洛丽塔应该被禁吗?》(我还在 第 75 期周刊 分享过收听这期播客产生的感想)在内的不少单集,在内容、结构、例证等方面都与 Old School with Shilo Brooks 等播客的相关单集高度重合,疑似洗稿。

洗稿事件本身是否属实,我没有考证过,所以这里不下定论,我也失去兴趣了,读者可以自行对比。社交媒体上的骂声也没有什么好看的,我不建议读者让自己陷入混乱的情绪旋涡里。我甚至在她最近一期播客的评论区下看到有人用 LLM 生成了最新的洗稿「铁证」,评论内容还包含没删干净的 Markdown 格式。总之舆论环境中充斥着只要站在反对立场上,谁都可以去踩一脚的言论,甚者,有人尽力克制情绪,冷静地要求仲树以事实证据回应,却被根本没有认真读评论的人当作仲树的支持者,回复说「我能不能把你的作品也拿去洗稿?」。另外国内平台显示 IP 属地的设计真是肮脏,有人仅仅因为他人的 IP 属地为上海,就说「我知道你在上海工作压力大,但你也不能这么骂人啊」。我只能说,仲树本人和这些参与舆论的人,都不是聪明人。

看起来比短视频更有营养的播客媒介,实际上也无法脱离其社交媒体性质带来的必然性。人们被情绪驱动,没有人读和听对方究竟说了什么,人们用臆测建立起用来攻击的假想敌。不再收听《独树不成林》之后,我貌似很难再找到感兴趣的播客内容(所剩不多的还有《 词与物 》),我才意识到这样的播客仅仅是播客生态中的个例,更意识到,如今不会因算法和商业化而陷入俗套,也不会让创作者因名气而自取灭亡的文化载体,就只剩纸质书了——纸质书还没有广告,没有 LLM 废料,更没有操纵用户的推荐算法以及一切如今人们在抱怨的东西,为什么人们不能多读书呢?

另一类播客我是纯粹抱着娱乐态度收听的,比如《 出逃在即 》的渣男渣女系列。偶尔我需要走很长的路,或者做接近一个小时的家务,我没办法看书,脑子里又没什么可想的,于是就戴上了耳机。我本想就此摘掉耳机,与自己多相处,可没过多久,我虽然没有重新安装小宇宙,但我的确又打开了某个基于 RSS 的播客播放器。我发现了自己的另一层情感需求:由于我没有能和我对上电波、畅快聊天的朋友,所以需要在耳朵里听到人的声音。

这大概就是我对播客媒介的反思了:一,它并不比短视频高级到哪里去,它仍受社交媒体的必然性、商业化和庸俗化、科技公司的意志操控,作为一种文化媒体来说它并不纯粹(至于不纯粹是不是个问题,要看语境);二,私人来讲,我收听播客并不是为了满足智识需求(阅读是更好的选择),而是为了满足某种心理需求,在满足心理需求的语境下,我允许一定的庸俗进入我的世界。

简单来说,我现在不把播客当作严肃媒介,除非你的播客和独立博客一样,架设在自己的服务器上,花自己的钱,对所有人开放却又不向任何人索取。

看完《奥德赛》之后我也成了「诺兰黑」

📺

戴锦华老师说她从来没有被诺兰的人物和故事打动过,除了《奥本海默》,由于完全不能从情感上被撼动,所以只能在理性层面欣赏。大部分时候她都只能保持「欣赏但不喜欢」的态度,甚至会尖酸克薄地指出叙事的漏洞。若是故事本身就很动人,那这些漏洞也不值得一提了。简单来说,她觉得诺兰的作品,形式往往要高于内容。诺兰特别喜欢错综复杂的叙事结构,但故事本身却撑不起叙事。

看完电影《奥德赛》之后我连着写了三篇影评。 第一篇 我对诺兰导演还抱有尊敬,我试着从哲学层面理解了他对神明的处理,对保留下来的三位神话角色雅典娜、瑟茜和卡吕普索分别做了什么样的改编,以及他还抹除了独眼巨人波吕斐摩斯的人性。 第二篇 我其实已经有些冷嘲热讽,比方说,我写得诺兰之所以删掉信使赫尔墨斯和风神埃俄罗斯,是因为与他们相关的剧情太过「超自然」,又不能像「雷电被误以为是宙斯,海浪的运动被误以为是波塞冬」那样诠释——他不能想出一个能囊括所有神明的解释,所以把不能用他的理论解释的神明都很方便地剔除在外。 第三篇 我就开始直抒胸臆,论证忒勒玛科斯是全电影塑造得最失败的角色,毫无成长,阐述包括海伦的刀疤在内的改变策略是为了服务于「文明的起落」「赎罪」「反战」等主题,而这些主题并不令我满意。电影中的奥德修斯不仅没有让我感动,反而令我厌恶,那个狡诈、自私但真实、爱小家小事胜过大义的奥德修斯,在诺兰大刀阔斧的改编下,变成了一个不存在的、伟光正的高大男人形象,辅以一个爱小家小事胜过大义的女人。最后,我写道:

文明的巅峰时期,荷马应该歌颂英雄与美德。文明的没落时期,我们不必盯着曾经的辉煌和应有的美德发愣,痛苦本身就值得被言说。

诺兰的《奥德赛》唯二令我有些动容的角色,一是被他改成愁苦妇女的瑟茜,因为她反映了现代人面对人际关系和混乱的外界时感到的挣扎,二是奥德修斯的狗。

简单来说,诺兰选择的主题并没有说服我对他的各种改编决策感到满意,我不想仅仅为了听到「应该像你想要被别人对待的那样对待别人」这种大道理,失去费埃克斯人、埃俄罗斯和经典的「没有人」智斗独眼巨人桥段,更别提这还是一个在很大程度和令我感到爹味溢出屏幕的主题。

所以,我也只能想戴锦华老师一样,欣赏电影的叙事和拍摄技法(说真的,找一米五的替身演员和两米高的演员拍巨食人族的戏,的确很亮眼),但我不喜欢。


鸡头蛇尾

Bun 用 Rust 重写出的灾难

📜
📜

Rust 是门好语言,但开发者们对它过于强烈的追捧,竟让我对它生起一丝厌恶来。你们怎么一个一个的都争着把本来就能跑的代码重写成 Rust?而且还是用 LLM 重写?新兴 JavaScript 运行时、Node.js 的后继和竞品 Bun 在前不久也用 Claude 把原来的 Zig 代码重写成 Rust。据我所知,Zig 的社群相当有凝聚力,当时他们就对 Bun 的重写感到失望。事到如今,Bun 的创造者 Jarred Sumner 终于自食恶果了。

6 months ago, most of Bun’s PRs came from people prompting Claude. Nowadays, most of Bun’s PRs come from Claude prompting Claude.

六个月前,大部分 Bun 的 PR 还来自人们给 Claude 输的提示词。如今,大部分 Bun 的 PR 已经是 Claude 给 Claude 写提示词产生的了。

Zig 软件基金会的主席 Andrew Kelly 表示,Jarred 在 LLM 之前写的 Zig 代码就已经不堪入目了。Zig 团队会阅读用户写的代码,以此了解当前这门语言是怎么影响用户的。当他们读到 Jarred 写的代码时,他们吓呆(horrified)了:

We became increasingly horrified at the programming practices we saw in Bun’s codebase. Hacks on top of hacks. Abuse of assertions. Most of all, recklessly speeding past feature after feature with very little time taken for reflection and elimination of bugs and technical debt. Jarred was already writing slop well before he had access to LLMs.

我们对 Bun 代码库里的编程实践感到越来越害怕。Hack 一个接一个。滥用断言。最严重的是,鲁莽地加速推出一个又一个的新功能,几乎没有花时间反思、消除 Bug 和技术债。Jarred 在能使用 LLM 之前就已经在写废料了。

这篇文章 列出了时间线,说明 Jarred 是怎么一次又一次践踏用户的信任,不断保证 Bun 1.4 明天就会发布,然后不断推迟发版的。其实不难从 GitHub 上的贡献量看出端倪,在过去的一个月里,15.8k 条 commit 来自 robobun ,一个自己写提示词生成代码的 LLM 机器人(Claude prompting Claude),相比之下 Jarred 作为贡献最多的真人,只提交了不到一千条 commit。

他是怎么搞清楚 Claude 都做了些什么的?他怎么保证交付可靠的软件产品?答案:他没有。如果他真的清楚自己的项目进行到什么步骤了,还需要解决什么问题,他不会这么拿不准 Bun 1.4 的发布日期,屡次推迟。Jarred 显然不知道自己在干什么,在 Andrew Kelly 与 Oven 的员工(以及那些面试过 Oven 的人)的交谈中,他得知 Jarred 是个相当糟糕的管理者:

Jarred was a stinky manager. Poor communication, unrealistic expectations, low empathy, no experience. Just a total shit show, from an employment perspective.

Jarred 是个糟糕的管理者。很难沟通,有不切实际的期待,同情心很低,缺乏经验。从雇佣关系的角度看,纯粹是在搞笑。

我简单翻了翻 Bun 最近的提交记录,几乎全是关于某个 Issue 的 commit,然而这些 Issue 本身也是 robobun 写的。我好不容易翻到了一条 Jarred 本人提交的 commit,结果发现:他删除了几条测试用例。

LLM 热潮没有制造新的问题,它放大了原本就存在的问题。Jarred 在 LLM 之前就不在乎代码质量,Bun 是纯粹的野心的产物;Jarred 作为领导者和项目维护者的性格缺陷,可能也被 LLM 自我增强——如果大部分时间都在和机器对话,又怎么会从真人那里获得真实的反馈呢?

最后,尽管一直遭到诟病,目前最稳定的 JS 运行时仍然是 Node.js。如果有选择技术栈的自由,我建议不要在浏览器以外的地方使用 JavaScript。

Farside 已关停

📜

我在 第 73 期周刊 推荐过 Farside,它可以把用户重定向至某个服务的替代前端,比方说如果不想在观看 YouTube 视频时被谷歌追踪和分析,就可以用 Farside 跳转到一个公开的 Invidious 实例。替代前端往往都用了尽可能少的 JavaScript 脚本,摘除了追踪器和广告,经过服务端从源站转发内容,唯一的缺点是服务不稳定。

Farside 的另一个用处是负载均衡,由于公开实例往往是社区维护或个人维护,资金不足,没有能力支付大额的网络流量。Farside 会自动查询节点状态,自动将用户分配到某个可用的实例。这本来是很好的愿景,直到……

Farside was created before the explosion of AI-driven scraping and bot traffic. As a result, querying instance status is no longer reliable due to bot-detection and challenge systems (which I understand and am in favor of, btw). Since the project is no longer practical to operate and maintain, it is being archived.

Farside 是在 AI 驱动的爬虫和机器人流量爆炸之前创造的。结果,查询实例状态由于机器人检测和质询系统而不再可靠(顺带一提,我理解并且支持反爬)。既然运行和维护这个项目已经不再切实,目前正在归档。

替代前端的另一个特点是内容更容易被爬取,因为这些前端尊重用户,没有加载大量消耗 CPU 和内存的静态资源,大部分内容也都直接写在 HTML 里,不需要 JS 渲染。为了避免成为想要训练 LLM 模型的公司的靶子,他们不得不采取更严厉的反爬措施以保证安全,比如架设 Anubis 。Farside 也需要向这些实例发送机器人特征明显的请求来确认它们能够访问,由于几乎所有实例都加上了会拦截并质询包括正常用户在内的所有流量的防火墙,Farside 根本无法有效地获取到服务状态。

不能指望科技公司良心发现,于是要接受现状:在 2026 年以及相当一段时间内,访问小互联网上的网站,自己都有可能被 Anubis 拦下,证明自己不是 AI 爬虫。这很恼人,但同时我又清楚,他们不这么做就很难活下去。真是让人感到压抑。

GitHub 又宕机

📃

与此同时,GitHub 在八月十七号宕机了接近八个小时。

我已经懒得评价了,只是转达消息而已。我自己其实没有受到影响,因为我早就从 GitHub 迁移出去了。现在我会把相对严肃的自由软件项目放在 Codeberg 上,偏向实验性的项目放在 Tangled 上,以上所有仓库都会在自己搭建的 Forgejo 实例上归档(归档并不麻烦,给自己的 Git 仓库设置两个 push remote 就好),自己的实例上也会有 Neovim 配置、dotfiles 和未公开的项目之类的东西。

GitHub 之外还有很多选择,如果不是为了求职的时候简历好看点,我可能已经销号了。

大模型应该变得更无知

📜

可能令人意外,这一节没有在批评 LLM 的应用。Well, know thy enemy.

作者表示市面上的模型看起来性价比变得越来越高了,人么可以用更少的参数做更多的事情。能耗在降低,模型也没有变得更难用,这是为什么?

事实是模型在某种程度上的确变得更蠢了,参数小的模型很难不搜索网页和调用工具回答某些简单的事实性问题,目前最好的模型也只能达到 53% 的正确率。但为什么非要模型“回忆”起正确的答案呢?把正确的知识塞进模型权重里很不划算,而且每隔一段时间都要重新训练才能刷新这些知识,大部分时间有些知识都是过时的。这也解决不了 LLM 幻觉。

所以更好的策略是,用更少的参数训练出“推理”能力更强的模型,把获取知识的步骤交给驾驭层(Harness),由 LLM 调用 API、读取本地文件、执行 Shell 命令来获取最新的知识。模型的“推理”能力比知识更重要。

不过模型应该没办法通过驾驭层获取如何写代码有关的知识吧?编程专精的大模型应该还是要把大量的代码训练进权重里的,也不会轻量。不过,如果大模型真的能现场搜代码并给出来源的话,想必能解决一直被我反复提起的「协议清洗」问题,即使用 LLM 生成的代码就相当于不需要遵循开源协议地使用其他人写的代码。我想没有想吸引用户的厂商会这么干吧,兴许学者们可以试试。


漫步遐思

这周买了一对哑铃,不重,单只三千克。我发现上班之后,自己运动的动力就变少了,以前还能在工作日做整整一个小时的 HIIT,现在回到家已经没有什么力气了。我其实很推崇自重训练,不用器械的话更安全不容易受伤,而且从推墙到跪姿俯卧撑再到标准俯卧撑,其实也是渐进的锻炼了。虽然做不到器械那样精细,但也够了。

不过我最近发现很难说服自己做俯卧撑,因为俯卧撑需要调动几乎全身的肌肉,不只是手臂要发力,胸肌也要用力,核心要维持平衡,肩背也不能闲着。可要是我没力气呢,下班之后我想要锻炼却不想要把整个身体都累得死去活来,做弯举和平举就刚好能满足。

另外,我知道我力量基础确实不太行,但这三千克的哑铃怎么做起来很轻松,但第二天和第三天,肱二头肌都酸胀得要命啊。

昨天 — 2026年8月23日極客死亡計劃

骷髅编程

2026年8月23日 20:27

软件架构的工作,除了搞清楚应该设计多少、什么样的组件来实现需求,还应该考虑组件之间的依赖关系。SOLID 原则之一的「依赖反转原则」是指数据流向应该与依赖的方向相反,比方说,数据从后端流向前端,但后端不应该依赖前端,而是要让前端调用后端提供的接口。此外,组件之间有抽象和具体之分,核心业务逻辑就是高度抽象的,而数据库逻辑(用什么样的数据库、用什么 ORM、具体怎么实现)就是相当具体的,业务逻辑不应该依赖数据库逻辑,这怎么做到呢?

比方说,我的业务逻辑是:不断接收某个源喂过来的数据,如果某条数据满足条件,就把它存在数据库里。如果我要编写这整个链路,难道不是要引用数据库组件吗?我怎么做到不直接依赖组件?

接口和 vtable

接下来我将分别演示在 OOP1 语言中和在函数式编程语言中,如何实现「不依赖数据库组件,但描述存储操作」。我使用的语言是 Go 和 Clojure,其中 Go 不是纯面向对象语言(缺少继承机制),Clojure 也不是纯函数式语言(但我也不会写 Haskell 啊),但用到的思想和语言特性是可迁移的。

接口是抽象的

接口定义其实比核心业务逻辑还要抽象,因为接口只定义了一系列函数签名,而没有包含任何具体实现,而核心业务逻辑还包含对相关函数的实现。依赖关系应该从具体的逐渐指向抽象的,核心业务逻辑依赖最抽象的接口便符合这个原则。

我们先定义一个接口:

package interfaces

type EntryStorage interface {
 GetEntry(id int) Entry 
 PutEntry(entry Entry)
 DeleteEntry(id int)
}

接下来业务逻辑只要依赖这个接口就好了:

package receiver 

import "example.com/interfaces"

type Engine struct {
 Storage EntryStorage
}

// ReceiveEntry 检查数据条目是否合格,如果合格就存储
// 并返回 true,如果不合格就返回 false
func (e *Engine) ReceiveEntry(entry Entry) bool {
 if (isQualified(entry)) {
 e.Storage.PutEntry(entry)
 return true
 }
 return false
}

此时,我们还没有编写 EntryStorage 的具体实现,我们不知道它用什么数据库,甚至不知道它用不用数据库,EntryStorage 完全可以把数据条目放进内存里、写进磁盘文件里、上传到某个云服务,甚至直接丢弃。

我们不需要编写具体实现也能测试这个软件,测试用例可以这样写:

package receiver

import "test"

// 就地实现一个假的存储
// 不仅因为我们不需要真的存储
// 还因为测试业务逻辑时,本就不应该关心存储有没有问题
// 不能让持久层的问题也让业务逻辑的测试用例失败
type FakeStorage struct {
 data []Entry
}

func (s *FakeStorage) GetEntry(id int) {
 return s.data[id]
}

func (s *FakeStorage) PutEntry(entry Entry) {
 s.data = append(s.data, entry)
}

func (s *FakeStorage) DeleteEntry(id int) {
 s.data = append(s.data[:id], s.data[id+1:]...)
}
// 至此,我们已经写完了实现 EntryStorage 接口的必要函数
// FakeStorage 现在可以直接注入到业务逻辑中进行测试了
// 你或许会觉得把 id 当成 Slice 的索引不太合适
// 但这仅仅是测试而已,只要不影响测试用例的有效性就好

func TestReceiveEntryQualified() {
 // 装配出完整的业务逻辑引擎
 storage := FakeStorage{ data: make([]Entry, 0) }
 engine := Engine{ Storage: storage }

 // 注入假数据并测试
 entry := makeQualifiedFakeEntry()
 ok := engine.ReceiveEntry(entry)

 if !ok {
 t.Errorf(`ReceiveEntry(qualifiedEntry) = %v, want true, nil`, ok)
 }
}

如果想要手动启动一个能够运行的实例,也不必着急实现数据库逻辑,同样的 FakeStorage 也可以在启动时喂给业务逻辑,它不知道也不关心数据最后去了哪里。有关接口的更多用法,可以阅读我的另一篇文章《 Are We Interfacing Yet? 》。

函数是一等公民

在函数式编程语言中,函数可以作为值传递,像这样:

element.onclick = (event) => {
 alert("我被点击了!")
}

这段 JavaScript 代码给 DOM 元素的 onclick 字段赋值,赋值的内容是一个函数定义。当元素被点击时,它就会执行 onclick() 函数,也就执行了我们传递给它的代码。这种函数一般也叫作回调函数(callback)。在支持函数作为一等公民的动态类型语言中,完全可以通过传递回调函数来实现接口能做到的事情——如果不在乎类型安全的话。

我们假设 engine 有一个 :storage 字段,这个字段是映射(map)类型,里面存的全是函数。

(def storage {:get-entry entry-storage/get-entry
 :put-entry entry-storage/put-entry
 :delete-entry entry-storage/delete-entry})
(def engine {:storage storage})

注意,这只是假设,我们实际上还没有构造这个 storage 常量,因为 entry-storage 这个命名空间还不存在,我们还没有实现具体的数据库逻辑。这个 storage 是在装配的过程中被喂给业务逻辑的。

跳过接口定义,直接开始写业务逻辑。在此之前要说明 Clojure 的一些语法,Clojure 可以用关键词取出映射中的某个值,就像这样:(:get-entry storage)——这段表达式会返回 storage 当中键为 :get-entry 的值,在这里返回的就是一个函数引用。要调用这个函数,就要这样写 ((:get-entry storage) args ...)

(defn receive-entry [engine entry]
 (let [storage (:storage engine)]
 (if (qualified? entry)
 (do 
 ((:put-entry storage) entry)
 true)
 false)))

同样地,如果要测试这个程序,只需要传入一个假的 storage。甚者,由于不需要实现接口,也就不需要实现接口中所有的函数,我们可以让 fake-storage 只包含这个测试文件用到的函数。再甚,我们都不需要真的实现任何形式的存储。

(def fake-storage {:put-entry #(println % "已保存!")})
(def fake-engine {:storage fake-storage})
(def entry (make-qualified-fake-entry))

(defn test-receive-entry-qualified []
 (is (receive-entry fake-engine entry)))
;; 这个测试用例太短了可能让人难以理解
;; is 是 Clojure 的断言语句,传入的是 true 就表示测试通过
;; 这里 receive-entry 收到的是合格的 entry 
;; 照理来说就应该返回 true,所以直接把它的返回值放进 is 里

这样一个包含函数引用的映射,叫作 vtable。这么做当然有明显的坏处,没有类型定义,我们无法确定得到的 storage 映射是否真的包含我们需要的函数引用,也不确定得到的函数的函数签名(形参和返回值)如我们预期的一样。这看起来的确是动态类型语言的缺陷,但如你所见,我们已经写了测试用例了,倘若我们的 vtable 真的有问题,它会呈现在单元测试的结果里。只要写了覆盖率高且合格的测试,就不必担心类型问题会导致软件健壮性下降;反之,不写测试用例,紧紧依靠编译器的类型判断的静态语言程序,也不见得有多安全。

而且你看看没有接口和类型定义的 Clojure 代码有多简短吧!

软件的骨架

如你所见,通过编写接口或使用回调函数,我们让软件中某些具体的部分变成了插件。还是以 JavaScript 举例,网页中的一个按钮点击之后具体会发生什么就是具体实现,而按钮可以点击这个事实就是一个接口,或者说协议。这个协议可以被更清晰地阐述为:点击了按钮之后就会发生某些事情。所谓的「某些事情」是抽象的、不定的、可替换的、可插拔的。JavaScript 通过回调函数实现这个协议。

编程实际上就是类似于「点击按钮之后会发生某些事情」的对目标系统的描述,只不过使用了特定的编程语言。如 Frederick Brooks 在《No Silver Bullet》中所言,软件的内在性问题之一是易变性。如何应对软件需要经常改变的事实?把相对不容易改变的东西和容易改变的东西分离开来,不容易改变的就是最抽象、最高层次的业务逻辑,容易改变的东西就是数据库、按钮做的「某些事情」的具体实现。网页上每个按钮的行为都不一样,可以说按钮的行为是具体且易变的,那为什么每次有人要做一个按钮的时候,我们不需要修改按钮内部的代码?那是因为我们已经用回调函数把抽象的和具体的隔离开来了。这种设计无处不在,以至于人们使用的时候毫无感知,自己需要设计的时候就很难考虑到这点了。

再回顾前文有关存储层的例子。假设已经实现了基于 MySQL 的存储,但用户突然要求兼容 PostgreSQL,只需要用 PostgreSQL 的相关库实现 EntryStorage 接口(也就是那三个函数),然后在装配 Engine 时,把 MySQL 的具体实现换成 PostgreSQL 的具体实现。存储层是可插拔的。

再假设,你在开发这个软件的期间一直使用 FakeStorage 把数据存在内存里,等核心的逻辑都完成时,你突然意识到这个软件要存储的数据体量非常小,而且读写的频率很低,连 SQLite 都不用上,用 JSONL 文件存储就行了。这下你就不必设计数据库 Schema 和编写 SQL 语句了,用 Go 语言自带的 JSON 标准库读写磁盘文件就好了。接下来你用几行代码实现了 JsonEntryStorage,插入 Engine,完全不用修改 Engine 里面的代码。

软件的其他部分也都可以这么做,让各种组件都变成核心业务逻辑的插件,一方面把复杂度外包出去,软件最中心、最内部的组件只需要描述「点击按钮之后会发生某些事情」这类简单且清晰的逻辑;另一方面,面对需求的变更,只要不涉及最核心的业务逻辑,可以把变更集中外围的一两个插件当中。在开发的初期阶段,完全可以把精力放在最重要、最核心的逻辑上,把数据库等底层细节都抛之脑后。

开发者只需要在编写核心业务逻辑的时候留下几个必要的接口,调试时可以使用假的、简化的实现,之后再将真实的实现装配和注入到核心组件中。虽然很不愿意称赞这门邪恶的语言,但是 Java 生态里的 Spring 框架做的就是这件事情。Java/Spring 程序员往往不需要让组件之间相互依赖,Spring 会自动将具体实现装配到对应的类当中。有一说一,这种自动化的依赖注入和控制反转(DI/IoC)框架过于方便,导致我观察到的很多学艺不精的 Java 程序员对基本的架构设计原则毫无认知。

在我看来,这让编程变得跟写伪代码一样简单,描述某个软件行为却发现自己需要用到另一个组件时,或者想要把某个组件分离出去时,只需要假设某个类、某个函数存在,然后直接写就好。比方说,前文我就假设了 isQualified(entry Entry) bool 存在(在 Clojure 的例子中,我用的是 (qualified? entry)),但之前的写法并没有留出接口,让我稍作修改:

package receiver 

import "example.com/interfaces"

type Engine struct {
 Storage EntryStorage
 Judge EntryJudge
}

func (e *Engine) ReceiveEntry(entry Entry) bool {
 if (e.Judge.IsQualified(entry)) {
 e.Storage.PutEntry(entry)
 return true
 }
 return false
}

EntryJudge 的定义自然是:

type EntryJudge interface {
 IsQualified(entry Entry) bool
}

这是 Go 推崇的「小接口」,更方便组合复用。上面的例子中,我把多个接口组合在一个结构体里,供核心业务逻辑 ReceiveEntry() 使用。如果遇到更复杂的逻辑,也可以拆出更多的抽象接口,而我们在一开始只需要面向接口编程,不需要考虑肮脏的底层细节扰乱心智,也不会因为迟迟解决不了某个数据库连接问题而在一开始就感到挫败。

对于那些愿意与 LLM Agent 合作的人,手写接口定义和最抽象的核心业务逻辑并不困难,而留下了明确的接口、函数签名、使用场景和预期行为之后,LLM 也更有可能生成准确的代码。而且,即便没有审查 LLM 生成的具体实现(不推荐不审查就交付代码),也不会因为代码量增长而失去对整个代码库的理解,因为最核心的逻辑是开发者自己手写的。

我把这种近似伪代码的编程方式称作「骷髅编程」,至于后续是人还是 LLM 来填充血肉,都不影响骨架的正确性。

实例:寻找互联网中的环路

搭建骨架

我最近在考虑写这样一个软件,背景是这样的:既然写博客的人有不少都喜欢相互引用,如果跟随这些引用,有没有可能最终回到自己的网站呢?如果可能,那需要经过多久呢?在路上会遇到什么样的网站?具体的需求就是:给定一个网站,找到这个网站的一些外链,分别爬取这些外链并找到他们引用的外链,这样一直跟随外链,直到找到链接到自己网站的外链。

好,既然需求已经清晰了,就直接开写吧。我选择用 Clojure 手写。之前我用 Clojure 写的例子都是用 vtable 实现的可插拔架构,其实这门语言也有接口(或者说协议),可以用 defprotocol 定义。

其实我希望我的核心业务逻辑本身就是一个接口,如果我的业务逻辑有了很大的改变,我也可以写新的实现来替换,抑或是我需要给用户提供两种截然不同的模式来完成这个需求,也可以根据配置装配不同的接口实现来实现模式切换。

;; 我给这个核心组件取名为奥德修斯(Odysseus)
;; 因为我们要做的事情就是送它出去,然后让他找到回家的路
(defprotocol Odysseus
 "Core engine, into which other components are loaded."
 (start [this] "Get Odysseus on his way."))

;; 以下是我们的具体实现
;; 它现在连骨架都没有,还只是个空壳
(defrecord Ody []
 Odysseus
 (start [_this]
 (println "Starting Ody...")))

;; main 函数是程序的入口,它创建 Odysseus 引擎
;; 然后调用 start 函数启动它
;; 之后我们也会把新增的组件装配在这里
(defn -main
 [& args]
 (let [ody (Ody.)]
 (start ody)))

现在执行这个程序只会打印 Starting Ody...,是时候搭建骨架了。在此之前,回顾我们的需求描述。

给定一个网站,找到这个网站的一些外链,分别爬取这些外链并找到他们引用的外链,这样一直跟随外链,直到找到链接到自己网站的外链。

那么这个骨架应该包含:

  1. 获取这个给定的网站
  2. 获取给定网站中包含的外链
  3. 获取外链的外链
  4. 如果外链的外链指向一开始给定的网站,就给出一条结果

我已经看出循环和递归的结构了,这个程序就是一直在提取外链而已。不少程序员的直觉可能是立刻动手开始编写获取 HTML 网页的逻辑,考虑应该使用正则匹配还是类似 Beautiful Soup 的解析工具,获取到一条环路之后应该存哪儿这些问题。But you and I should know better.

照着我现在的理解,先写一段代码试试看:

(defrecord Ody [home over? crawler ]
 Odysseus
 (start [_this]
 (println "Starting Ody...")
 (loop [journey []]
 (when (empty? journey) 
 (recur [home]))
 (when (over? journey)
 journey)
 (let [ways (find-ways crawler (last journey))]
 ;; ...
 ))))

我在写的时候遇到了一个问题,但在讨论这个问题之前,先来看看我写了些什么。

loop 是 Clojure 中的循环结构,它的第一个参数是可变数量的 binding,装在一个向量类型(也就是 [])里面。你可以理解为我们构造了一个函数,而在这个 binding 里填入的是函数的参数和参数的初始值。loop 体中有 recur 函数,这就是递归的意思。recur 让程序重新回到 loop 体的开头,并且重新给 binding 赋值——这其实就是递归函数,只不过用了循环的写法,而且 Clojure 给 recur 做了优化,即便多次递归也不会让函数调用栈溢出。

在这个递归函数里,程序先判断 journey 是否为空,如果为空就递归并传入 [home] 作为 journey 的新值。这个 home 就是环路的起点也是终点。由于在写代码的过程中意识到我需要 home 的值作为起点,所以我在 Ody 的字段里添加了 home。我用向量类型表示一条路径,向量的第一个元素是起点,中间的元素是路途中经过的链接,最后一个元素是终点,也就是说,当第一个元素和最后一个元素相等时,这个 journey 就可以作为结果返回了。

不过,我没有写 (= (first journey) (last journey)),而是引入了回调函数 over?,使用它判断是否结束。前者是具体的,后者是抽象的。还有一个考量是,判断环路是否找到的标准不定,是找到完全一样的 URL 才算呢?还是找到同一个域名就算呢?还是找到同一个子域的也算呢?不知道,不关心,写个抽象的函数先用着吧。由于在写代码的过程中意识到要有一个判断路途是否结束的函数,所以我在 Ody 的字段里添加了 over?

如果 journey 既不是空的,也没有被判定结束,那么就说明搜寻还在进行当中。这个阶段要做的事情,就是爬取最新的一条 URL((last journey)),找到新的外链。一如既往,我意识到自己需要一个爬虫,所以添加了 crawler 字段,调用它将会提供的 find-ways 函数获取外链。这之后,只要用其中一个 URL 作为新的 (last journey),再次递归就好了。

等等,「其中一个」?如果一个页面有多个外链,我要怎么确定应该用哪一个?话说回来,我肯定不能让程序只找到一条环路就停下吧?这样简单的操作完全可以并发执行,可如果要并发执行,而每递归一次都会得到多个外链的话,并发数不会指数级上升吗?就算控制全局并发数,似乎也有太多任务了,根本跑不完,看起来应该做剪枝和限制,比方说剔除掉不太可能引用自己的网站(Microsoft、Google 等),除了自己的网站,后面的每一条最多发散出 3 条外链等等。

不管不管,太复杂了,先抽象出一个门卫组件干这个事情吧,具体实现之后再说。

(let [ways (->> (last journey)
 (find-ways crawler) 
 (filter-ways gatekeeper))]
 ;; ...
)

至于并发控制,Clojure 支持和 Go 一样的并发模型,所以我们只需要一个固定缓冲大小的 channel 就好,此时就要考虑两种模式:

  1. channel 当作信号量(semaphore)来用,初始化时存入几个空值当作信号量,每当有人试图并发一个新的爬取任务时,都要尝试获取信号量,如果信号量消耗完了,就要原地等待。
  2. 把待爬取的 URL 放进固定缓冲的 channel 里,让几个独立的爬虫协程读这个 channel;当缓冲写满,而爬虫的消费能力不足时,尝试写入 channel 的协程就会被阻塞。爬取一直在进行,而 gatekeeperover? 等组件,包括 Odysseus 本身,都自动地根据爬虫的消费能力调整任务进行的速度。

第二种模式还解决了任务分发的问题,看起来也很干净。实际上这还把爬虫 crawler 与核心逻辑 Odysseus 解耦了,我只需要把爬取请求放进 channel 里,然后等待结果就好了。不过这引发了新的问题:没有函数调用就没有返回值,我要怎么拿到爬取结果呢?

这个好解决,我们可以塞一个向量给 channel,向量的第一个元素是要爬取的 URL,第二个元素是一个新的 channel,等爬取任务完成之后,爬虫就把结果写入这个 channel,而 Odysseus 这边只需要等待就好了。另外,如果决定把 crawler 分离出去,那在收到 crawler 的结果之后再用 gatekeeper 筛一遍,似乎有些没道理,可以把 gatekeepercrawler 装配到一起,而 crawler 只返回已经过滤之后的结果——幸好我们的组件都很小巧抽象,做这样的改动并不会很麻烦。

(defn- start-worker [crawler gatekeeper crawl-queue]
 (go-loop [task (<! crawl-queue)]
 ;; task is a vector like [url channel]
 (let [urls (->> (first task)
 (find-ways crawler)
 (filter-ways gatekeeper))]
 (>! (last task) urls))
 (recur (<! crawl-queue))))

(defrecord workers [crawler gatekeeper crawl-queue] Workers
 (start-workers [_this n]
 (dotimes [_ n]
 (start-worker crawler gatekeeper crawl-queue))))

如你所见,这段代码可以启动 nworker,而每个 worker 都是一个异步的循环,一直尝试从 crawl-queue 中读取任务。一旦读取到任务,就用 crawler 爬取外链,用 gatekeeper 过滤外链,然后把结果通过 channel 返回。至于核心逻辑 Odysseus 这边,只需要把任务传给 crawl-queue,然后从自己设定的 channel 中取出数据就好了。

不过我很快发现 looprecur 已经不适合 Odysseus 了,因为每次循环都可能发散出多个 URL,但一个 loop 只能用一次 recur,也就是只能传递一条新的 journey 路径给下一级递归。为了产生多条分支,是时候请出真正的递归函数了!

(defn- hop [journey over? home crawl-queue]
 (let [journey (if (empty? journey) [home] journey)]
 (if (over? journey)
 ;; if journey's over, print the only one
 [journey]
 ;; if not, keep crawling and find fresh urls 
 ;; start multiple hop forks and concat their result together
 (let [receive-chan (chan)]
 (>!! crawl-queue [(last journey) receive-chan])
 (let [urls (<!! receive-chan)]
 (mapcat #(hop (conj journey %) over? home crawl-queue) urls))))))

(defrecord Ody [home over? crawl-queue] Odysseus
 (start [_this]
 (hop [] over? home crawl-queue)))

其实一开始我就犯了个小错误,Odysseus 返回的不应该是一条 journey,应该是它能找到的所有环路。这里提取出来的递归函数是 hop(跳)。如果 journey 是空的就直接改成 [home],没必要再递归一次。如果判定结束了,就返回只包含一个环路的向量 [journey]。如果没结束就继续往下,把 URL 用 channel 交给爬虫 workers。拿到多个 URL 结果之后,对每个 URL 执行一次 hop 递归,最后把子 hop 的结果全部拼接起来——这一步用 mapcat 就可以做到,非常省事。

现在还剩下的问题是,尽管爬虫是异步的,与核心逻辑解耦了,但核心逻辑本身是串行的,一次递归执行完毕之后才执行下一个,同时也只会有一个爬取任务被传入 crawl-queue,根本利用不了并发资源。所以,要把 hop 也改成并行的。

本 Go 程序员写 Go 程序写得太多了,一开始也想用 gohop 并行,然后便踩了坑,写出了很复杂的代码。实际上 Go 标准库里的 future 就足够好用。future 同样异步执行,返回一个引用 ref,之后 deref(或者用语法糖 @)就能拿到结果,如果 future 没有完成,deref 的调用者就会原地等待。

(defn- hop [journey over? home crawl-queue]
 (future (let [journey (if (empty? journey) [home] journey)]
 (if (over? journey)
 ;; if journey's over, print the only one
 [journey]
 ;; if not, keep crawling and find fresh urls 
 ;; start multiple hop forks and concat their result together
 (let [receive-chan (chan)]
 (>!! crawl-queue [(last journey) receive-chan])
 (let [urls (<!! receive-chan)
 futures (mapv #(hop (conj journey %) over? home crawl-queue) urls)
 results (mapv deref futures)]
 (apply concat results)))))))

现在就可以了,hop 并发了更多的递归 hop,之后再逐个等待结果。看起来并发之后立刻阻塞并等待,好像和直接调用没有区别,但关键的区别在于,如果把递归结构看成一棵树的话,我们就不是在进行深度优先遍历,而是并发地对每个子树同时进行遍历。23由于我们用了有缓冲的 channelhop 在把任务放进爬取队列时就可能因为缓冲区已满而阻塞,只要设置合理的缓冲区大小,就不用担心性能问题。

不过,即便是并发,网络操作仍然很慢,我们不能等程序把所有的环路都找到了之后才打印结果吧?是不是应该每找到一个结果就打印一次?要在 hop 里调用 println?不不不,如果这个软件要做成 Web 程序、GUI 程序呢?要怎么收集到这些结果?显然 println 是具体实现。那只要换成一个抽象函数就好了。

(defn- hop [journey over? home crawl-queue put-result]
 (future (let [journey (if (empty? journey) [home] journey)]
 (if (over? journey)
 ;; if journey's over, print the only one
 (put-result journey)
 [journey]
 ;; if not, keep crawling and find fresh urls 
 ;; start multiple hop forks and concat their result together
 (let [receive-chan (chan)]
 (>!! crawl-queue [(last journey) receive-chan])
 (let [urls (<!! receive-chan)
 futures (mapv #(hop (conj journey %) over? home crawl-queue put-result) urls)
 results (mapv deref futures)]
 (apply concat results)))))))

很好,现在骨架只知道「要把结果放到某个地方」,并不关心放在那里。最简单的实现是把结果放进 channel 里,让消费者自取,这样的话我们甚至不需要写函数定义,在 put-result 的位置传入 (partial put! result-queue) 就好,partial 返回的是预先填好了几个参数的函数。这么说的话,我们也不必把 crawl-queue 这种细节暴露给 hop,也可以改为抽象函数。由于两个都和传送数据的通道相关,不妨放进一个 vtable 里,就叫 pipelines(管线)吧。事已至此,就干脆把这个包里的 clojure.core.async 引用移除掉,所有关于 channel 的操作都是具体的,应该交由外部实现来解决。

我又发现 home 这个参数完全没有意义,我为什么不能一开始就传入 [home] 而不是空向量 [] 呢?于是也顺带改了。

在添加新功能(每找到一个环路就上报一次)的同时顺带重构了代码,现在 hop 函数看起来干净了很多:

(defn- hop [journey over? pipelines]
 (future (if (over? journey)
 ;; if journey's over, print the only one
 (do
 ((:put-result pipelines) journey)
 [journey])
 ;; if not, keep crawling and find fresh urls 
 ;; start multiple hop forks and concat their result together
 (let [urls ((:get-crawl pipelines) (last journey))
 futures (mapv #(hop (conj journey %) over? pipelines) urls)
 results (mapv deref futures)]
 (apply concat results)))))

我把「把爬取任务放入管道,然后等待结果」都封装到一个 :get-crawl 函数里了。这个 pipelines 其实完全可以写成协议,也就是之前看到的 defprotocoldefrecord,不过目前软件里还没有很复杂的管线,用一个简单的数据结构足矣。

最后一个问题,目前看来 hop 如果没有找到满足 over? 条件的路径,就会一直执行,是不是应该写一个最大跳数的限制?不不不,别忘了 over? 函数是抽象的,最大跳数的限制完全可以由 over? 的具体实现去处理,不必为此让核心逻辑变得复杂。

至此,我通过只思考抽象问题,逐步求精,写完了核心的业务逻辑。即便写了 defprotocol 定义,也只有不到一百行代码——包括注释和 require 语句,ody.clj 只有 19 行,workers.clj 只有 17 行,各种协议定义 protocols.clj 有 15 行(不过这很大程度上要归功于 Clojure 语法的简洁性)。这 51 行代码就是这个软件的骨架,在填充血肉之前,还有一件重要的事情要做。

测试骨架

在我们把 HTTP 客户端、HTML 解析、URL 规范化和对比、结果输出、用户界面等一系列无底洞般的细节引入到软件中之前,应当先保证骨架本身有效。Lisp 程序员们可以用 REPL 命令行里直接调用软件内部的函数进行测试,测试的过程中随手定义一些假的实现喂给核心逻辑也非常容易。我虽然写了很多 Clojure 代码(不过才五十几行吧),但本文实际上是与语言无关的,所以我还是用更通用的方式来测试——写测试文件。

不过老实说,更好的做法是测试驱动开发(TDD),即先写测试,再写代码。但…… 我都已经写完了还能怎么办呢?

要测试 ody.clj 很简单,只需要保证递归函数 hop 的正确性。hop 预期的正常行为是:输入 [home],通过 home 发散出更多的 URL,递归出更多的分支,把每条完成的分支写入管线,最后返回全部分支。注意,「完成」,也就是 over? 函数是抽象的,这个完成的条件不一定是「找到和 home 一样的 URL」。完成的条件判断得准不准、是否有缺陷,是实现 over? 函数的组件的工作,应该在测试那个组件的时候才考虑。也就是说,此时我们完全不必考虑 hop 能不能给出我们想要的环路,我们只要保证它能完成预期的行为就好了。甚至啊甚至,这个 home 和最后得到的 journey,都不一定得是 URL。

(deftest hop-happy-path-test
 (testing "Happy path of the recursive hop function."
 (let [result-chan (chan)
 pls {:put-result (partial put! result-chan)
 :get-crawl (fn [num] (mapv #(+ % num) [1 2 3]))}
 ody (->Ody 0 #(>= (last %) 5) pls)
 want #{[0 1 2 3 4 5] [0 1 2 3 4 6] [0 1 2 3 4 7] [0 1 2 3 5] [0 1 2 3 6] [0 1 2 4 5] [0 1 2 4 6] [0 1 2 4 7] [0 1 2 5] [0 1 3 4 5] [0 1 3 4 6] [0 1 3 4 7] [0 1 3 5] [0 1 3 6] [0 1 4 5] [0 1 4 6] [0 1 4 7] [0 2 3 4 5] [0 2 3 4 6] [0 2 3 4 7] [0 2 3 5] [0 2 3 6] [0 2 4 5] [0 2 4 6] [0 2 4 7] [0 2 5] [0 3 4 5] [0 3 4 6] [0 3 4 7] [0 3 5] [0 3 6]}]
 ;; match journey results
 (is (= (set @(start ody)) want))
 ;; collect channel and count
 (loop [counter 0]
 (if (poll! result-chan)
 (recur (inc counter))
 (is (= counter (count want)))))))) 

这个测试中的 journey 不是 URL 向量,我把「爬取和发散 URL」变成了「累加数字」。初始 URL 变成了初始数字 0。测试 Ody 的时候我没有启动爬虫,注意我传入的管线 pipelines,其中 :get-crawl 并没有把新 URL 放进 crawl-queue 里再等待结果,而是对数字进行了三次加法运算,发散出 n+1n+2n+3 三个数字。再看 over? 的具体实现 #(>= (last %) 5),当累加 journey 的最后一个数字大于等于 5,就算做结束。

由于并发的执行顺序不定,为了避免最终结果的顺序影响断言,我把它转换成了集合类型(set),然后与包含所有累加路径的集合对比。最后再把 result-chan 里的内容读出来,看看 Ody 是否传入了正确数量的路径结果。

用 Leinigen 运行测试:

➜ ody git:(feat_core) ✗ lein test

lein test ody.ody-test

Ran 1 tests containing 2 assertions.
0 failures, 0 errors.

测试通过!接下来应该考虑边界条件,比方说,如果 :get-crawl 没有返回任何结果,hop 就应该停下,返回空集。

(deftest hop-empty-test
 (testing "Hop should be able to handle empty result from :get-crawl"
 (let [result-chan (chan)
 pls {:put-result (partial put! result-chan)
 :get-crawl (fn [_] [])}
 ody (->Ody 0 #(>= (last %) 5) pls)
 want #{}]
 ;; match journey results
 (is (= (set @(start ody)) want))
 ;; collect channel and count
 (loop [counter 0]
 (if (poll! result-chan)
 (recur (inc counter))
 (is (= counter (count want)))))))) 

测试依然通过,接下来要考虑更多的边界情况,还要写 workers.clj 的单元测试,看它能不能正常启动 N 个爬虫,能不能正确消耗 crawl-queue 里的任务等等。具体的就省略了。

总之,合理的抽象、接口和骨架式的编程不仅可以让人在开发核心功能时只关注核心逻辑,而不被技术细节困扰,还能让单元测试变得容易,不必等到整个项目近乎完成时才做集成测试。

填充血肉

那些跟 LLM Agent 一起工作得不亦乐乎的人,其实已经可以放手,把写好的接口定义和核心业务逻辑丢给 Agent,让机器实现人没有实现的接口,在 main 组件中装配起来,然后就能跑了。跟 Vibe Coding 不同的是,开发者已经对软件的核心逻辑、接线点、输入输出和内部结构了如指掌,不会面对大量安全性、可读性和质量堪忧的代码发懵。开发者不会像 Vibe Coders 那样因为不理解代码而事事求助于机器,不会让自然堆积的屎山代码填满 LLM 的上下文,燃烧大把大把的美钞。再者,由于开发者对代码的理解很充分,他可以写出更清晰的提示词,甚至精确到代码行,让 Agent 消耗更少的 Token 和时间完成任务,而不必把大量的资源都用在「推测用户意图」上。

不过,LLM 生成代码的版权状态未知,我不知道 Agent 填进我仓库的代码是从谁哪里偷的,应该遵从什么开源协议,应该署谁的名字,所以我不会用 LLM 生成的代码。接下来我将手写接口实现,把所有组件都装配起来,产生可运行的软件原型。

eurylochus.clj

Crawler(爬虫)协议的实现类,我决定命名为 eurylochus(欧律洛卡斯),他是奥德修斯的二把手,在登陆瑟茜的岛屿时,也是他带队探路。

要发送 HTTP GET 请求,最常用的 Clojure 库应该是 clj-http 。至于解析 HTML,之前有听说过 hickory ,可以把 HTML 转换为 Clojure 数据结构,还兼容 hiccup 的 X-Expression(用来表示 XML 的 S-Expression),不过它的选择器语法看起来太吓人了,浏览之后找到了一个 JSoup (Java 版的 BeautifulSoup)的 Clojure 包装,试了试好像也够用了。虽然这个 html-parser 十二年没有人维护,总共只提交过三次 Commit,看着也有点吓人,但大不了回头再换一个。

(ns ody.eurylochus
 "Eurylochus is scout for Odysseus. It implements Crawler protocol."
 (:require [ody.protocols :refer [Crawler]]
 [clj-http.client :as client]
 [html-parser.core :refer [document-from-string select]]))

(defrecord eurylochus [] Crawler
 (find-ways [_this url]
 (let [resp (client/get url)
 status (:status resp)
 doc (document-from-string (:body resp))]
 (when (= status 200)
 (->> (select doc "a")
 (map #(.attr % "href"))))))))

按下快捷键把这段代码发送到 nREPL 里,然后直接在 REPL 里 Eval 看看效果,就用我自己博客的某期周刊 URL 当作例子吧。

这是 Neovim 的 Conjure 插件,可以在编辑器里把代码发送给 REPL

果然,提取 <a> 标签的 href 属性会拿到很多不想要的东西,比如锚点和内链,但我并不打算在 eurylochus 这里剔除,别忘了,还有一个叫作 Gatekeeper 的协议就是来做筛选的。

polites.clj

Gatekeeper 的实现我想用奥德修斯最好的朋友 polites(波利特斯)来命名,要做的就是筛选 eurylochus 拿到的 URL,同时控制数量。好在 Clojure 的 filterremove 函数就很适合干这活儿,最后再用 take 函数拿一定数量的 URL 返回就好了。

(ns ody.polites
 "Polites is Odysseus' best friend. It implements Gatekeeper protocols and keep unwanted URLs at bay."
 (:require [ody.protocols :refer [Gatekeeper]]
 [clojure.string :as string])
 (:import (java.net URI)))

(defn- hostname [url]
 (.getHost (URI. url)))

(defrecord polites [blocklist limit] Gatekeeper
 (filter-ways [_this urls]
 (->> urls 
 (filter #(or (string/starts-with? % "https://")
 (string/starts-with? % "http://")))
 (remove #((set blocklist) (hostname %)))
 (take limit))))

在 REPL 里测试,最后得到的列表很干净。

目前还不支持通配符 *,没关系,之后再说。

zeus.clj

让天神宙斯 zeus 来判断 journey 是否结束再合适不过了。不过我没有创建名为 zeus 的对象或者说类,仅仅是在 ody.zeus 这个命名空间下定义了 over? 函数。

(ns ody.zeus
 "Zeus determines if Odysseus finishes his journey. It provides over? predicate function."
 (:import [java.net URI]))

(defn- ring-by-exact? [journey]
 (.equals (URI. (first journey)) 
 (URI. (last journey))))

(defn- ring-by-domain? [journey]
 (= (.getHost (URI. (first journey)))
 (.getHost (URI. (last journey)))))

(defn over? [law max-hop journey]
 (let [ring? (cond (= law "exact") ring-by-exact?
 (= law "domain") ring-by-domain?
 :else ring-by-domain?)]
 (or (ring? journey)
 (>= (count journey) max-hop))))

你可能发现 ody.zeus/over? 有三个参数,而 ody.clj 中使用的却是只有一个参数的 over?,参数对不上怎么办?没关系,还记得 partial 吗?给 ody 传入 (partial over? law max-hop),也就是预先填好两个参数的函数引用,就可以了。

装配

是时候把骨头和肉拼起来了,这一步很简单,只需要把各个组件初始化,按照接口定义装配在一起,然后启动就好:

(ns ody.core
 (:require [ody.ody :refer [->Ody]]
 [ody.workers :refer [->workers]]
 [ody.pipelines :refer [make-pipelines]]
 [ody.eurylochus :refer [->eurylochus]]
 [ody.polites :refer [->polites]]
 [ody.zeus :refer [over?]]
 [ody.protocols :refer [start start-workers]]
 [clojure.core.async :refer [chan <! go-loop]])
 (:gen-class))

(def config {:crawl-queue-buffer 10
 :worker-count 5
 :domain-blocklist ["github.com" "google.com"]
 :url-limit 3
 :ring-standard "domain"
 :max-hop 10})

(defn -main
 [& args]
 (let [result-queue (chan)
 crawl-queue (chan (:crawl-queue-buffer config))
 home (first args)
 ody (->Ody home
 (partial over? (:ring-standard config) 
 (:max-hop config))
 (make-pipelines result-queue crawl-queue))
 workers (->workers (->eurylochus) 
 (->polites (:domain-blocklist config) 
 (:url-limit config))
 crawl-queue)]
 (start-workers workers (:worker-count config))
 (println "Crawling wokers started.")
 (let [all-ref (start ody)]
 (println (str "Odysseus is on his way. Starting from " home))
 (go-loop [journey (<! result-queue)]
 (println "Found one! " journey)
 (recur (<! result-queue)))
 (deref all-ref)
 (println "All is done."))))

现在,启动!嗯……?

......
Found one! [https://www.geedea.pro https://www.geedea.pro/ https://www.eltr.ac/ https://www.eltr.ac https://www.eltr.ac https://www.eltr.ac https://codeberg.org/eltrac https://blog.codeberg.org/protecting-our-floss-commons-from-llms.html https://blog.codeberg.org https://join.codeberg.org]
Found one! [https://www.geedea.pro https://www.geedea.pro/ https://www.eltr.ac/ https://www.eltr.ac https://www.eltr.ac https://www.eltr.ac https://codeberg.org/eltrac https://blog.codeberg.org/protecting-our-floss-commons-from-llms.html https://codeberg.org/ https://codeberg.org/Codeberg/org/compare/17bdb39b0c1ecd0e423f3ba592650ce57fcdfbf5..71149c7fc95ccfeae36109b5cddca339e4aa1473?files=TermsOfUse.md#diff-d760d688bb9b929b62db808bfc76abe4493d095a]
Execution error (URISyntaxException) at java.net.URI$Parser/fail (URI.java:3005).
Illegal character in fragment at index 20: https://matrix.to/#/#codeberg-space:matrix.org

Full report at:
/var/folders/41/6gvwg1j15xb4hwm365wds90h0000gn/T/clojure-16924552856293422522.edn

看来报错了,而且找到的路径也都是因为达到最大跳数限制才停止的,并没有找到环路。没关系,没有程序是一遍就能跑通的。C’est la vie. 报错是 Java 的 URI 处理不了 # 字符导致的,把 zeus.cljpolites.clj 中的 URI 换成 URL 应该就可以了。

仔细观察的话,找不到环路很大程度上是因为很多路径一直在原地传送,某个 URL 的内容可能包含和自己一模一样的 URL,而 polites.clj 没有把这部分筛掉。再者,程序经常找网站的首页 URL,而包含内容的子页面才更有可能找到我们想要的环路,应该让 URL 中路径更深的排在前面。

;; polites.clj
(defrecord polites [blocklist limit] Gatekeeper
 (filter-ways [_this urls current]
 (->> urls 
 (filter #(or (string/starts-with? % "https://")
 (string/starts-with? % "http://")))
 (remove #(url-equal current %))
 (remove #((set blocklist) (hostname %)))
 (sort-by #(-> % path depth) >)
 (take limit))))

另外,还有很多路径经过了 Google 等根本不可能导向我自己的网站的大站点,嗯…… 果然还是要写通配符匹配呢。

(defrecord polites [blocklist limit] Gatekeeper
 (filter-ways [_this urls current]
 (->> urls 
 (filter #(or (string/starts-with? % "https://")
 (string/starts-with? % "http://")))
 (remove #(url-equal current %))
 (remove #(some (fn [pattern] (match-glob pattern (hostname %))) blocklist))
 (sort-by #(-> % path depth) >)
 (take limit))))

match-glob 函数来自 stch-library/glob ,果然还是偷别人的代码好用啊。不过这种小库竟然没有 LICENSE 文件,用起来还挺不放心的,之后要换掉。在配置里面写上一些大网站的黑名单看看:

(def config {:crawl-queue-buffer 10
 :worker-count 5
 :domain-blocklist ["github.com" 
 "google.com"
 "*.google.com"
 "bandcamp.com"
 "*.bandcamp.com"
 "gohugo.io"
 "*.gohugo.io"
 "codeberg.org"
 "*.codeberg.org"
 "apple.com"
 "*.apple.com"
 "creativecommons.org"]
 :url-limit 3
 :ring-standard "domain"
 :max-hop 10})

再次运行,很快就找到了第一条环路。

["https://www.geedea.pro/weekly/94/"
 "https://michaelharley.net/posts/2026/06/15/bloggers-can-we-make-better-titles-for-our-posts/"
 "https://social.bubbles.town/@bubbles/statuses/01KV5P7PK0VFSMTGASPRA3C2KV"
 "https://bubbles.town/entry/34746979"
 "https://michaelharley.net/posts/2026/06/15/bloggers-can-we-make-better-titles-for-our-posts/"
 "https://www.geedea.pro/weekly/94/"]

从我的 第 94 期周刊 ,先是去到了 Michael Harley 的《Bloggers, can we make better titles for our posts?》,然后去到了 Bubbles(一个博客聚合平台)的社交媒体贴(GoToSocial 的前端不需要 JavaScript 也能显示内容,所以获取到了外链),然后去到了 Bubbles 上这篇文章的页面,然后回到了 Harley 的文章,由于我引用了他的文章并且发送了 Webmention,最后通过 Webmention 链接回到了我的网站。挺奇妙的旅程。

不过接下来就没有那么顺利了,很多路径都是达到 max-hop 限制放弃的路径,不过倒是发现了不少有趣的链接,或许这个软件的作用也可是帮助从自己的网站出发发现更多的内容。目前的限制是,每个 URL 最多发散出三条外链,一条 journey 最多包含 10 个节点。即便做了黑名单限制,也很难剪枝掉大部分的任务,最后要爬取的网页数量接近 3 的十次方。

或许这个软件不适合在本地运行,更适合一直跑在服务器上,每找到一条合格的路径就显示在网页上,或者通过 Barkntfy 给自己发送通知。好在我们的业务逻辑是高度抽象的,只需要在 main 组件里包装一层 HTTP 服务器就好。由此可见,软件要做成命令行应用、GUI 应用还是 Web 服务,都只是外层的细节,只要业务逻辑保持高度抽象,就能够让人机交互层也变成插件。

反思:对象是必要的吗?

软件能跑了,现在 Ody 已经开源放到了 Tangled 上,由于还不稳定,所以现在的大部分代码都在 feat_core 分支里,尚未合并。欢迎提 PR,如果有人能帮我把那个翻车的并发问题解决掉就好了,我这周的 Clojure 功力已经耗尽了。

我在写代码的过程中发现,我定义的不少 defprotocol 只包含一个方法,为了实现这一个方法而去创建对象,是不是有点太啰唆呢?我那 51 行代码是不是可以更短?我之前其实写过 Common Lisp 的 无类多态 ,即不需要定义类也能创建抽象函数。不过当时我忙着骂 Java,竟没有好好吸收 CLOS 的精髓。最近读了明琪琪可的《 Trial 与 CommonLisp 的介绍 》,这才意识到 OOP 里的接口仅仅是低配版的 defmethod 而已,我果然还是被 Go 害惨了啊

前面写过的 over? 作为写在函数形参里的回调函数,其实也是一种低配但更简洁的抽象函数,唯一的缺点是它不够安全,返回值、形参和行为都难以确定(或许静态类型语言会好一些),而且写太多形参若还要递归或层层传递,想想就觉得枯燥。

Clojure 也支持类似 CLOS 的系统,defmultidefmethod 可以在不依附任何类和对象的情况下实现多态,而且支持多分发。兴许下次用 Clojure 写东西的时候可以试着用用。

推荐阅读

本文的部分观点来自以下书籍:


  1. Objective-Oriented Programing,面向对象编程 ↩︎

  2. 咳咳,这里是几个小时后的 Eltrac。我发现这段代码有个致命的问题:它实际上仅仅是创建个一个又一个被阻塞的线程,实际速度和同步递归没差…… 主要的问题是,任务的生产者和消费者都是同一实体,照理来说应该分开,一开始用 gochannel 的思路是对的。事已至此,先这样吧,我累了。 ↩︎

  3. 既然已经调用了 mapv,换成 pmap,再做些调整兴许就能解决。pmap 的意思是 Parallel map,多个操作是并发执行的,会快很多。 ↩︎

昨天以前極客死亡計劃

诺兰的《奥德赛》采取了什么样的改编策略? III

2026年8月21日 23:26

终于,我的奥德赛之旅也要结束了。《奥德赛》影评的最后一篇,我将审视诺兰给电影定下的不属于原典的主题,以及我为什么讨厌这个主题。尽管因为让黑人演员饰演海伦和雅典娜,让不少人觉得诺兰是个被政治正确洗脑的激进派,但诺兰的叙事其实相当保守和传统,甚至有些…… 大男子主义。诺兰的奥德修斯不是原典中的狡诈英雄,不是音乐剧中的“只是个男人而已”,奥德修斯被诺兰可刻画成了一个不可存在的、正直、大义和无私的平庸的好人。

忒勒玛科斯是电影塑造得最失败的角色

前两篇我还在试图理解诺兰的种种改变决策,比方说他将神明删去是为了表达他对神明这一概念的哲学思考,而他将瑟茜(或基尔克)从住在宫殿里有侍女相伴的神女改成了一个应激的愁容妇女是因为后者更容易令现代观众共情。这最后一篇,我不得不谈到忒勒玛科斯,也就是奥德修斯的儿子这一角色,我对他在电影中的刻画很不满意。

诺兰无疑基督教化了《奥德赛》1,其中一个体现是在电影的末尾,忒勒玛科斯不杀死求婚者的原因之一是他会因违反宙斯之法(Zeus’ Law)而被流放,最后奥德修斯因忏悔自己在特洛伊战争以及回家路上行的残忍之事,决定自己亲手杀了求婚者们,让自己流亡,也圆了妻子佩涅洛佩想要同奥德修斯前往西方追逐落日的梦。

这些在原典中都是不存在的。上周五我刚下班就进了电影院,三个小时之后迎接我的竟然是奥德修斯自我流放,忒勒玛科斯这个毫无角色弧光的人物当上了伊萨卡国王的结局,我真的如鲠在喉。我一般不会在文章中插入 Bilibili 上搬运的 TikTok 短视频,但原谅我,这真的太好笑了:

在我的理解里,忒勒玛科斯之所以接替奥德修斯作为国王的位置,仅仅是因为奥德修斯需要自我流放,以成全这部电影的忏悔主题。然而,忒勒玛科斯这个角色真的在电影里完成了必要的成长吗?

在诺兰的改编中,忒勒玛科斯从电影一开始就想要坐上王位,以治理伊萨卡二十年无王的混乱局面,也光明正大地赶走天天在自己家里白吃白喝的求婚者。是她的母亲看不起他,觉得他不配当王。的确,哪有让二十岁出头的小孩子当王的?电影中的忒勒玛科斯一直想跟求婚者动手,甚至在一开始已经和安提诺乌斯起了口角。看起来他并不是没有能力杀人,而是不被允许,这也使得父子相认之后两人屠杀求婚者的场面,并没有让人感到忒勒玛科斯有任何成长变化,更何况忒勒玛科斯仅仅是辅助父亲,他自己没有真的拼死战斗。

所以,我对忒勒玛科斯这整个人物的感受就是:他在电影的一开头和最后并没有任何变化。他一开始就想打架,但不被允许,于是父亲回来之后就和他一起打了;他一开始就想当王,但母亲不认可,于是父亲回来之后就主动把王位给了他。我为什么有种伊萨卡要亡国了的预感?

原典中的前四卷都是忒勒玛科斯探寻父亲消息的情节,同时他也有雅典娜的陪伴。除了智慧女神的指导,忒勒玛科斯在路途中听闻的有关父亲的故事和涅斯托尔等人的教导也令他深受启发。而我们知道电影中的雅典娜仅仅是奥德修斯良心的化身,只存在于他的想象中(见 第一篇 ),而与墨涅拉俄斯的交谈竟然还穿插着海伦的忏悔,重点从忒勒玛科斯寻父偏移了,再次为诺兰电影的赎罪主题服务。忒勒玛科斯简直是电影主题的牺牲品。

最后,一如既往地对比音乐剧《EPIC》对忒勒玛科斯的改编。音乐剧中的小忒稚气更胜,至少没有一开始就想当皇帝、要打人,唯一的不足是 Jorge 并没有像原典和诺兰那样把太多的注意力放在伊萨卡城中发生的事情上,对小忒的刻画也就少了很多。不过,我们依旧能从《Little Wolf》一曲中窥见小忒成长的契机。

音乐剧还删去了奥德修斯父子在田庄相会,商议如何处置求婚人的情节,奥德修斯也没有伪装成乞丐回家。这一刀砍得确实有点狠,音乐剧把太多的时间都放在奥德修斯的归途上了,有些忽视了伊萨卡这边的故事。如果只看忒勒玛科斯的角色塑造这一点的话,音乐剧是要更胜一筹的。不是说 Jorge 笔下的小忒有多立体,只是诺兰的小忒实在是有些惨不忍睹,在我看来是电影的一大败笔。

海伦的刀疤、愤怒与愧疚

忒勒玛科斯造访斯巴达寻求父亲的消息,国王墨涅拉俄斯在讲述木马之中的故事前后提起了他的妻子海伦。海伦就是引发特洛伊战争的原因之一, 上篇 我已经提到海伦和特洛伊王子帕丽斯私奔,导致斯巴达国王愤怒地召集包括奥德修斯在内的英雄一起攻打特洛伊的故事。

海伦的出生其实也始于一场婚外情,天神宙斯变身天鹅勾引之前的斯巴达王后勒达,勒达之后生下两枚鹅蛋,海伦就生于其中一个蛋。所以,希腊第一美人海伦并不是凡人,而是神的女儿。这神样的女人必定引来了很多人争抢,最后是奥德修斯提议抽签决定谁娶海伦,并且约定一旦中签者因为海伦而陷入麻烦,其他人就要发兵援助。在上篇提到的另一个有关金苹果的故事中,帕丽斯得到了爱与美之女神阿佛洛狄忒的帮助,让海伦爱上了她,跟着他私奔到特洛伊,于是奥德修斯以及所有参与抽签的人,都发兵援助斯巴达国王。这些厌女的故事自然没有被诺兰和 Jorge 保留,诺兰也删掉了神明的成分,所以海伦在故事里还能起什么作用呢?

海伦是特洛伊战争发动的原因,而我们已经知道诺兰将《奥德赛》的主题改为反战、忏悔和赎罪,所以,毫不意外地,海伦成为了愧疚和羞辱的化身。诺兰还为她的脸增加了一道刀疤,以侧面展现战争的残酷,忒勒玛科斯离开时,她还情绪激动地跟小忒耳语,让他告诉佩涅洛佩,海伦为她引发的战争感到非常抱歉。海伦的刀疤不存在与原典中,她也从未对自己引发的战争感到过愧疚。海伦是神的女儿,她爱上特洛伊王子也是爱神阿佛洛狄忒的把戏,这整场战争都是神明与人的游戏。

允许我插一嘴不相干的,许多人不满希腊第一美人在电影中竟然由黑人扮演,但在我看来,比起海伦为什么是黑人饰演,更有趣的问题是欧律洛卡斯为什么是黑人,而且在两部改编作品中,这个时常顶撞奥德修斯的反面形象都是黑人。既然都已经跑题了,让我再插一嘴,奥德修斯的船上唯一一个摘掉蜂蜡去听海妖歌声的船员是由亚洲演员饰演的,有人说他之所以这么激动地跳下船,是因为这个东亚小孩听见他父母说爱他

我也对海伦这一角色呈现在电影中的样子颇有微词,这与人种无关,我在乎的是,为什么诺兰就不能让「希腊第一美人」就真的是第一美人,而非要在她的脸上刻下战争的印记?为什么奥德修斯非要自我放逐?为什么雅典娜非要是奥德修斯的想象?为什么诺兰要剥夺独眼巨人波吕斐摩斯的人性?为什么风神和人神信使不能留在故事里?为什么诺兰要把夫妻通过婚床不可搬动的秘密相认的情节,改成对原典中不存在的「海上人民」的揭秘和奥德修斯的忏悔独白?修改西方文明中最经典的故事,让歌颂变为反思,把辉煌变得压抑,最后呈现出的主题真的令人满意吗?

比起宏大,我更愿意看到小人物的痛苦和自私

诺兰的奥德修斯过于正直了,对待猎物也是如此,竟然要先弹一下弓弦警告猎物,然后公平公正地打猎。男人的正直和道德高尚貌似是很多人喜欢在英雄身上看到的,而这类作品往往把女性描绘得非常重感情,过度关注小家小事。诺兰的《奥德赛》就是这样,他描绘的奥德修斯和佩涅洛佩的关系其实相当保守和传统。电影中有一个镜头是夫妻二人在床上讨论克洛伊战争一事,佩涅洛佩哭着问奥德修斯为什么非要去,为什么我们不可以私奔,奥德修斯则非常沉稳、大义地为他妻子解答,说他为什么必须去。

这也是诺兰大刀阔斧的改编,传说中奥德修斯并不愿意出征,还耍了些把戏试图骗过众人:

伊塔刻岛国王奥德修斯以机智闻名,他刚与妻子佩涅洛佩结婚不久,诞下儿子忒勒玛科斯,因此不愿同行,当奥德修斯得知墨涅拉俄斯、阿加曼农、涅斯托尔及帕拉墨得斯来到伊塔刻时,他就装疯把牛套套在犁上耕田,又把盐撒到田里,帕拉墨得斯看出他的假扮,就把他的儿子放在田上,果然奥德修斯就在孩子前停了下来,奥德修斯不得不承认自己是假扮,只得履行当年的承诺,从这时起奥德修斯便恨帕拉墨得斯,并决心要报复。

——《 特洛伊战争 - 维基百科

诺兰把奥德修斯硬生生地改成了这样一个伟大光荣正确的男人形象,还辅以一个不懂得天下大事的女人形象,实在是有些…… 令人作呕。读者应该看出来了,我在前两篇影评中都相当克制和尊重诺兰导演,而这最后一篇我谈到了电影的主题以及对奥德修斯本人的改动,这才难以掩饰心情。

从头到尾,奥德修斯的伟光正形象都没有坍塌过。原本机智狡黠的奥德修斯通过话术欺骗了独眼巨人波吕斐摩斯,最后是出于傲慢,要让波吕斐摩斯记住是哪个伟大的英雄刺瞎了他的眼睛,喊出了自己的真名。电影中,奥德修斯的举动则显得像是被兄弟情、战友情等光荣的概念冲昏了脑袋的呆子,他射箭激怒了独眼巨人,让更多的兄弟当场死去。他完全不是那个狡诈的奥德修斯。三个版本的奥德修斯都向同伴隐瞒了六头海怪斯库拉的存在,原典中奥德修斯隐瞒是为了“免得同伴们知道后一时心生恐惧,停止划桨”,而电影中的奥德修斯又干了件伟大光荣的事情,他利用同伴们心中已然存在的不信任,让他们通过旋涡海怪卡律布狄斯,欧律洛卡斯反对,并带领船员往斯库拉的山谷划,这才只损失了六名船员,没有被旋涡全军覆没。他从一开始就想救下所有人,哪怕受人憎恨,多么无私的英雄形象!。顺带一提,原典中的欧律洛卡斯并未看出奥德修斯早就知道斯库拉会杀死六人,因此发动哗变,反倒是《EPIC》音乐剧中的《Mutiny》一曲描绘了欧律洛卡斯对奥德修斯的质问和怒吼。真的很难不让人怀疑诺兰有没有参考《EPIC》的改编策略,据说音乐剧《Just A Man》一曲中的母题(motif)也贯穿了整个《奥德赛》电影的原声带,但我不懂乐理,也不好深究了。

此外,奥德修斯的忠贞也是被夸大的对象。诺兰想要塑造的伟光正形象,怎么可以好美色呢?于是美女瑟茜变成了愁苦妇女,卡吕普索变成了心理医生,出于职业道德不能和病患发生关系,然而原典里的奥德修斯刚打败瑟茜就脱掉衣服与她同床,和卡吕普索待在奥吉吉亚岛上的七年也是没羞没躁。古希腊人似乎是没有忠贞这一概念的,只有女人的贞洁。音乐剧也把奥德修斯塑造成了面对诱惑依然不背叛妻子的男人,但音乐剧在其他方面把奥德修斯塑造得很自私,更何况忠贞对现代人来说的确更敏感,这倒也还说得过去。

如你所见,诺兰的奥德修斯是一个无私、正直、仗义、有大局观的男人形象,这样伟大的男人甚至还会忏悔和赎罪!真是更令人赞叹了! 然而, 道德对叙事来说无足轻重 ,塑造一个完全符合大众道德观的好人形象是很偷懒的做法,而且,说实话,这种做法缺乏深度。这样的道德不过是康德所言的定言令式,是不需要反思的,和阿伦特所述的艾希曼一样。如果说艾希曼展现出来的是一种平庸的恶(或者说恶的平庸性),那么诺兰的奥德修斯展现出来的,就是平庸的好人。

我更愿意看到自私的奥德修斯,为了回到家与妻子团聚不择手段的奥德修斯。音乐剧《EPIC》的改编尽管也把不少情节改编得让人难以辨认,但 Jorge 将奥德修斯从仁慈心软到不择手段的转变描述得十分生动。“在好与坏之间选边站不是最难的,在一团糟的情况下做出最好的决策才是。”在音乐剧的第一曲《The Horse and The Infant》当中,奥德修斯就面临这样一个两难的决策——他要么杀死特洛伊王子仍在襁褓中的儿子,要么留下隐患,导致自己和家人的灭亡。

在第二首曲子《Just A Man》中,奥德修斯将婴儿从城墙上丢下。他做了件非常不道德的事情,他一点也不正直、不伟大,他好自私,而我好喜欢这样的奥德修斯。他不是什么英雄般的男人形象,他“只是个男人”。这首歌的母题(I’m just a man 一词的旋律)贯穿了整部音乐剧,在不同的曲子中,在奥德修斯面临一个又一个道德难题时反复出现。

Jorge 采取的又一个改编决策是,同伴们吃掉了赫利欧斯的牛之后,宙斯并没有直接降下雷电杀死奥德修斯的同伴,而是给奥德修斯提供了选择:今天有人必须死,而你有最终的决定权,是你死,还是你的船员们死?

诺兰笔下的正直的奥德修斯甚至不用把婴儿从城墙上丢下,诺兰没有给他安排那样的道德抉择。如果有的话,他正直的形象恐怕就树立不起来了吧?

文明的起落

诺兰给《奥德赛》添加的众多新要素中,有一个最扎眼,那就是电影中人物口口相传的「海上人民」(The sea people)。诺兰从一开始就铺垫,海上人民四处烧杀抢掠,令伊萨卡的佩涅洛佩也很是担忧。其实在电影的前中段我就看出来「海上人民」就是奥德修斯一行人,因为在遇到独眼巨人之前,他们就非常突兀地抢劫了某个岛上的居民,这一情节的安排太过刻意,仿佛是怕我不知道海上人民就是他们这群人一样。最后奥德修斯伪装成西农与佩涅洛佩对话,坦白自己就是「海上人民」的时候,我一点也不惊讶。

当然,识别出这种叙事套路不代表这个故事要传达的东西就没有意义了。诺兰想要借此表达的是反战主题,即应该像自己想要被别人对待的方式对待别人,而奥德修斯以掠夺对待别人,他自己也受尽苦难,另远在家乡的妻子也无法停止担忧。我能理解诺兰想要在当今的世界局势下呼吁人们重新审视人与人(甚至国与国)之间的关系,反思我们是否遵循了宙斯之法(Zeus’ Law)。这或许也是为什么,原本辉煌荣耀的荷马史诗,在诺兰手里竟变得如此压抑。荷马的时代是文明焕发生机的时代,而我们的时代似乎在走下坡路—— AI 公司摧毁珍贵的实体书 ,有毒的社交媒体和算法平台让人们纷纷逃离互联网,当然,还有关税战、俄乌战争等我并不关心的政治事件…… 就像卢梭写的,科学进步反而使人道德败坏。

话还是说回来,诺兰的主题过于宏大,把对于文明层面的呼吁呈献给普罗大众,真的能博得什么共鸣吗?除了让一部分人点头同意,又能改变什么呢?能让普通人更好过吗?这也是为什么,我更想要看到小人物的自私,看到真实且具体的人的挣扎和生存的意志。这也是为什么我并没有对电影中从神女变成凡人的瑟茜感到特别生气,因为诺兰电影中的瑟茜,是为数不多能让现代观众共情的,一个痛苦的普通人。

假若我们真的在文明的某个低谷期(尽管每个时代都有文人感叹世风日下),呼吁人们重建文明反而显得不食人间烟火。文明的巅峰时期,荷马应该歌颂英雄与美德。文明的没落时期,我们不必盯着曾经的辉煌和应有的美德发愣,痛苦本身就值得被言说。


其实整篇影评已经写完了,但还有一些点我没谈到,前文找不到位置放了。各位若是还愿意读,就放松心情,把下面的内容当作番外赏阅吧。

旧王的猎弓

佩涅洛佩强弓择偶的挑战,究竟是怎么进行的?史诗里只写「如果有人能最轻易地伸手握弓安好弦,一箭射出,穿过全部十二把斧头……」,可究竟是怎么穿过?穿过斧头的哪里?诺兰的电影选择将十二把斧头成对斜放,形成六个大洞。在音乐剧的一些动画中,有画师给斧头靠近刀背的地方画了洞,箭矢要穿过十二个小洞。在 维基百科 的叙述中,挑战者要让剑柄穿过斧头手柄的洞(handle hole),也就是将手柄取下后,斧头横过来放能看到的洞,目标更小而且轨道更长。

这可能只是无关紧要的小细节,但在某种程度上也可以看出,诺兰在处处都试图现代化、合理化、去神秘化《奥德赛》的故事,让它更容易被现代观众接受。穿过六个大洞明显要比穿过十二个又细又长的洞容易做到,也不会过分神化奥德修斯。

这段情节与原典差异不大,倒是没什么好谈的,不过还是来听听音乐剧中的《The Challenge》一曲吧。

情节上,诺兰还是大致遵照了原典,伪装成乞丐的奥德修斯最后一个参与挑战。拿到猎弓后,原典中的奥德修斯第一个射向了安提诺奥斯的喉咙,可电影中的混战场面,安提诺奥斯还活着,躲在柱子后面尴尬地指挥别人「快打啊」「快打他啊」,也不知道用意是什么,大概是为了展现这个反派的懦弱吧。音乐剧中奥德修斯屠杀求婚者的情节由《 Odysseus 》一曲描绘,我觉得这曲子有些太中二了,不过也是好听的。

电影中奥德修斯与妻子是在强弓择偶之后相认的,删去了原典里的一个小细节。奶妈欧律克勒娅告诉佩涅洛佩,外面的男人就是奥德修斯,她在给他洗脚时看见了他腿上被野猪咬过的伤口。佩涅洛佩仍然谨慎,觉着这是神明给她设下的骗局,见母亲离得远远的,忒勒玛科斯还责怪母亲是个无情的女人。最后佩涅洛佩让欧律克勒娅把他们的婚床移过来,而这时奥德修斯气愤地喊道,那婚床是他亲手造就,被深深地嵌在地里,没有办法移动,佩涅洛佩这才相信面前的男人就是他的丈夫。

我保证这是我最后一次在文章里嵌入视频了。

奥德修斯的狗

细数完了我对电影改变的不满,接下来谈谈我对音乐剧的不满之一:你把奥德修斯的狗放哪了!狗呢?狗狗呢!?

塔罗牌中的「星币十」

奥德修斯有一条名叫阿耳戈斯(Argos)的狗,他在电影中也得到了不少镜头。奥德修斯离开了二十年,阿耳戈斯至少也活了二十多年,这个寿命对狗来说非常不可思议。阿耳戈斯最后被遗弃在门外的粪堆上,身上长满了寄生虫,已经奄奄一息。奥德修斯回到伊萨卡时低调行事,伪装成乞丐,没有人认出他,是奄奄一息的阿耳戈斯最先认出了阔别二十年的主人。在知道奥德修斯安全返乡后,阿耳戈斯才安心离世。是的是的,我知道这很俗套,但是谁会不喜欢这样一只忠犬啊!看《奥德赛》唯一一次接近眼眶湿润,就是因为阿耳戈斯。

塔罗牌的「星币十」有一种解读也关于奥德修斯和他的狗。星币十的牌面看上去象征着物质富足和家庭美满,画面中是一对夫妇、一位老者、一个孩子和两条狗。另一种解读是,这位白发老人与故土阔别十几年回到家,没有人记得他的名字,只有他的狗认出了他。

与「星币十」联系起来的作品还有卡夫卡的《变形记》(这就是更小众的牌意演绎了),当主角 K 变成一只虫之后,家人并不关心他怎么了,他们只关心 K 还能不能上班给家里带来收入。「星币十」象征的也是这种务实。《奥德赛》中的求婚者、长老和所有想让佩涅洛佩改嫁的人,兴许也带着一些务实心态或者对家庭、财富的渴望,他们不关心佩涅洛佩本人的意志,只想要美色和王位。奥德修斯回到家发现家里挤满了想要抢走他王位和妻子的求婚者,只有他的奴隶、奶妈、儿子和狗还认得他,愿意效忠于他,也很符合这种解读。

好了,就此打住,再写就要变成《 塔罗牌漫谈 》了。


最后

写《奥德赛》的影评竟然写了一整个星期,某种程度上很切题。

呼,我困了,那就回见吧。


  1. 这个观点来自播客《政治哲学圆桌》,详见 第 93 期周刊 。 ↩︎

诺兰的《奥德赛》采取了什么样的改编策略? II

2026年8月19日 22:59

书接 上回 ,我对比了荷马史诗原典、诺兰的电影改编和 Jorge 的音乐剧《EPIC》分别如何呈现智慧女神雅典娜、女巫瑟茜、海之神女卡吕普索、独眼巨人波吕斐摩斯,分析了冥界之行和塞壬之歌起到的不同作用,还提及了诺兰对神明的现代化处理以及背后的考量。这一篇我们继续讨论这三部不同的《奥德赛》作品,这次我们聚焦电影中没有出现的神明和人。

赫尔墨斯:无法归于自然之神

上篇提到,诺兰对《奥德赛》中的神明做了相当现代化的处理:神明其实是古人对自然现象的解释,所以电影中的雷声和海浪就是神在与人对话,神明没有直接现身。然而,这个说法在某一位神上完全站不住脚,那就是人神信使赫尔墨斯。如果神只是对自然现象的解释,那为什么会有神作为信使来到人间与凡人直接对话呢?的确,这说不通,于是诺兰直接把赫尔墨斯删掉了。

原典中的赫尔墨斯起到了什么作用?首先是在第五卷《奥德修斯启程归返海上遇风暴》,宙斯让爱子赫尔墨斯前去告知卡吕普索释放奥德修斯,卡吕普索不敢违背宙斯的意图,于是放奥德修斯回家。

此后是第十卷《风神惠赐归程降伏魔女基尔克》,在瑟茜(或基尔克)的岛屿上,奥德修斯准备前去营救被变成猪的队友,但凭借他自己的实力是无法与拥有巫术的神女对抗的。这时赫尔墨斯出现了,给予他只有不死的神明才有能力拿到的植物摩吕(Moly),吃下之后,奥德修斯就可以免疫瑟茜的法术。赫尔墨斯还交代了很多细节,比方说奥德修斯应该在瑟茜挥魔杖的时候拔刀,把她打倒在地,之后瑟茜会被奥德修斯折服,在同她上床之前,应该要她向神明发誓不会趁奥德修斯裸身时耍把戏——赫尔墨斯真的是我这种自闭症小孩的天使,连什么时候挥剑这种细节都交代得清清楚楚,安全感满满呢。

最后是第二十四卷《神明干预化解仇怨君民缔和平》,赫尔墨斯持权杖引领被奥德修斯杀死的求婚者的亡魂前往冥界(或哈得斯)。不过我翻了半天,也没搞明白如果不是赫尔墨斯引渡灵魂会发生什么。我想这只是神话的一部分,和史诗的情节关联不大。

赫尔墨斯大致有三次现身,最后一次或许不重要,但前两次对情节都有关键的推动作用。没了赫尔墨斯,诺兰是如何处理这些情节的呢?其实在上篇谈瑟茜和卡吕普索的时候已经提到了:诺兰电影中的卡吕普索并没有因为私心而困住奥德修斯,在她认为奥德修斯恢复心力过后,便放他走了,自然也不需要赫尔墨斯下场;电影中的瑟茜并不是可以随意用魔杖施法的女巫,奥德修斯没有吃食物,还擒住了变成乌鸦的瑟茜姐妹以威胁她,靠自己的计谋说服了瑟茜。

嗯…… 这么看来赫尔墨斯在史诗中登场的必要性的确不大,荷马大概只是想要歌颂神明。不过留下他又何妨呢?毕竟这是希腊神话的一部分。另一边,改编自《奥德赛》的音乐剧作品《EPIC》保留了赫尔墨斯的两次现身,删去了引渡灵魂的情节,此外还给他的第二次现身加了些戏份。

首先是音乐剧 The Circe Saga(瑟茜篇章)中的《Wouldn’t You Like》一曲:

这一曲描述了赫尔墨斯帮助奥德修斯对抗瑟茜的过程,看完我不敢相信是这个古希腊掌管迪厅的神杀死了百眼巨人阿耳戈斯。音乐剧对赫尔墨斯的处理其实是偏娱乐化的,他甚至把摩吕叫作 Holy Moly(老天叶)啊!赫尔墨斯这个角色从头至尾都没有太多深度,不过对于一个对剧情推动影响不太大(以至于直接被诺兰删掉)的角色,也还算不错,甚至可以说非常讨喜。大概是吉祥物一类的存在吧。

音乐剧没有像原典一样直接描述赫尔墨斯去到卡吕普索的岛屿,卡吕普索宴请赫尔墨斯的场面,而是在卡吕普索独白的《Not Sorry for Loving You》中简单带过,也在下面这一曲《Dangerous》中提及。

上面这一曲出自 The Vengeance Saga(复仇篇章),这整个篇章讲述的内容,其实在原典中都是在一卷内很快带过了。Jorge 不仅给赫尔墨斯加戏,还给波塞冬也加了戏,稍后会讲到。在此之前,让我们先来看看更惨的一个神明——风神埃俄罗斯。

埃俄罗斯:风神的协助,还是考验?

埃俄罗斯(Aeolus,或译艾奥洛斯)是风神,有着控制风的神力。奥德修斯逃离独眼巨人波吕斐摩斯之后,路过了飘浮在天空中的岛屿埃俄利亚岛,那里生活着风神、他的妻子和他的十二个孩子。奥德修斯抵达后,风神招待了他们整整一个月,询问他路途中遇到的各种事情。某种程度上,原典中的奥德修斯是个「万神迷」,不仅宙斯喜欢他,雅典娜为他求情还护他儿子周全,神女一个一个的都和他同床欢爱,连路过一个神明的宅邸,都要被留下来招待一番。我想这反映了古希腊人对英雄的崇拜,也就是对美德、卓越和智谋的崇拜。在这些美好的人类品德面前,众神都要称赞。

为了帮助奥德修斯归返,风神给了奥德修斯一个风囊。他把所有风都装进了袋子里,除了温柔的西风,这样奥德修斯就能顺着西风回家,不被风暴困扰,前提是奥德修斯不打开风囊。结果:

我们连续航行九天,日夜兼程,第十天时故乡的土地已经清楚地显现,看见人们就在不远处生火添柴薪。这时我疲惫过分,陷入沉沉的梦境,因为我一直掌握航舵,未把他交给任何同伴,为了能尽快返抵乡土。同伴们这时却开始互相议论纷纷,猜想我准是把黄金白银载回家,就是那希波塔斯之子、强大的艾奥洛斯的赠礼。有人看着身边的同伴,这样议论:

‘天哪,他到处备受人们的爱戴和尊敬,不管到达哪个部落的城市和土地。他从特洛亚携带回来很多掠获的珍贵财宝,我们也作了同样的航行,返回家园时却是两手空空白辛苦,现在艾奥洛斯又盛情招待,给他礼物。让我们赶快看看那是些什么东西,皮囊里也许又装满了黄金和白银。’

他这样说,大家赞同他的坏主意。他们解开皮囊,狂风一起往外涌。风暴骤起,立即把他们卷到海上,任凭他们哭泣,刮离故乡的土地。

与是他们又被吹回了风神的岛屿,风神此时还很惊讶,问他:“奥德修斯,你为何回来?遇上什么恶神?我们已妥善地把你送走,让你返回故乡家宅或者你想去的任何地方。”如实交代之后,风神愤怒地赶走了他们,把他称作「人间最大的渎神者」。

尽管风的确是和雷电以及海浪一样可以用神明解释的自然现象,但被风吹过来又吹过去,实在是有些过于超自然了。大概也是因为和诺兰对神明的解读不和,所以没有被保留在电影中。

音乐剧《EPIC》对这个情节做了些改编。首先,风神埃俄罗斯一家子只有风神一人出现了,而且风神是一个顽皮的小女孩的形象,看起来不像是有家庭的样子,只有一群精灵样的神灵跟随在她身边。性别的转变还挺令人意外的,说起来风神的家人对情节并没有很重要,而且顽皮女童的形象似乎在某种程度上也更贴合一些人对风的认知。再者,音乐剧中的奥德修斯一行人并没有被吹回风神的岛屿,而是被吹到了巨人岛——歌词里的 Land of the Giants 用词有些模糊,鉴于拉斯忒吕戈涅斯人(巨食人族)没有在音乐剧中出现,这里应该是指独眼巨人的岛屿,而且这首歌的下一首就是波塞冬的《Ruthlessness》,音乐剧让奥德修斯被风吹回了独眼巨人家门口,然后被波塞冬袭击。Jorge 真的给波塞冬加了很多戏,原典里奥德修斯一行人离开风神岛之后,就直接去到了海岛艾艾埃,遇到了瑟茜。

可以发现的是,原典中风神是真心想要帮助奥德修斯,但好心被枉费之后感到愤怒,而音乐剧中的风神则从头到尾都保持逍遥自在的形象,仿佛给奥德修斯风囊仅仅是为了考验、试验甚至恶作剧。同时,这首《Keep Your Friends Close》也巧妙地引出了故事的另一个主题:对身边人的信任。歌词可能和中国的一句老话有些关联,也就是:日防夜防,家贼难防。

波塞冬:残酷与仁慈的二元性

古代人的道德意识没有现代人那样强,他们尤其缺乏反思,即便是荷马史诗里的智慧女神雅典娜,见了人也会说「你母亲生了你,你竟然如此慵懒」「你要嫁人了,应当穿得漂亮」「穿得漂亮能给人带来声誉」之类的话。所以,阅读荷马史诗不能讲现代人的道德,更不能思考「为什么?」这个现代性极强的哲学问题。如果有人说「一切外来者都是宙斯遣来,应当招待」,就不会有别的道理或真相,电影中的宙斯之法(Zeus’ Law)也是如此——而诺兰电影中对于「打破宙斯之法」的反思,在我看来是对荷马史诗最大的现代化,即「反思」的出现。

波塞冬为了帮儿子报仇而折磨奥德修斯,也是天经地义的事情,说「神明应该比凡人更大度」「波塞冬该不该原谅奥德修斯」更是无稽之谈。正是出于「要帮儿子复仇」这个简单的定言令式1,波塞冬才在背地里使坏。诺兰并没有改编这个定言令式,而是把定言令式的执行者从神明换成了凡人,奥德修斯的小伙伴们把海水的动荡解读为波塞冬为儿子的复仇,这本质上还是在对古典道德的必然性的诠释。

前文提到,音乐剧给波塞冬加了很多戏。即便是在原典中,波塞冬也没有与奥德修斯正面对峙过(如果真有,那奥德修斯恐怕早该死了)。波塞冬在奥德修斯从卡吕普索岛离开时,掀起海浪打翻了他的木筏,奥德修斯费了好一阵工夫才去到费埃克斯国土,在那里得到了公主和费埃克斯人的帮助(不知为何,诺兰的电影和音乐剧都省略了这段剧情,奥德修斯离开了卡吕普索之后就直接返乡了)。见奥德修斯睡在费埃克斯人的快船上,马上抵达,波塞冬便问宙斯:这奥德修斯不应该历经磨难才回到家吗?你看他,睡得这么香就被送往家园了,还带有财宝,是不是有些太轻松了?宙斯觉得波塞冬作为神,他的意志也不该被一直忽略,于是就同意让波塞冬惩罚送奥德修斯回家的费埃克斯人。波塞冬本想把费埃克斯人返回的船击碎,宙斯让波塞冬改为把船只变成巨石固定在海面,再升起山峦围住他们的城市作为警示。费埃克斯人见状就知道是波塞冬生气了,决定再也不护送外乡人,并且要给波塞冬献祭肥牛。

所以,原典中的波塞冬仅仅是在奥德修斯最后的路程中使了绊子,而且大多数时候都是背景板。诺兰却反过来,让奥德修斯在离开独眼巨人之后就立刻受到风浪阻碍,反而在回程时被大海直接带回家园,甚至没有受到费埃克斯人的帮助。上篇提到过,奥德修斯是在接受了卡吕普索的「心理治疗」过后将自己投入世界当中,而后又被世界(包括海洋)所接纳,自然将他送回了家。有点像是某种泛神论,或者仅仅是内心状态对外接的投射。音乐剧则是双管齐下,不给奥德修斯留活路:奥德修斯在被风囊放出的风吹离故乡海岸之后,就受到了波塞冬的袭击。

风神埃俄罗斯还客串了这首《Ruthlessness》。我们可以在这曲子里看到非古典哲学的反思,波塞冬不仅仅是一个遵循定言令式的角色,他还前来教育奥德修斯,他说:“我最讨厌你这种散发着伪正直的恶臭的希腊人。”所谓的「伪正直」(false righteousness)是指奥德修斯的仁慈,最终会害死他身边所有人的仁慈。波塞冬把奥德修斯的手下杀得只剩四十三人,然后告诉他:“假如你直接杀了我儿子,没有给他留活路还告诉他你的真名,这些惨剧都可以被避免。”最后回到这个曲子的主题:对敌人的残忍就是对自己的仁慈——波塞冬对奥德修斯的残忍是对他自己的仁慈,而奥德修斯对波吕斐摩斯的仁慈则成了他对自己和身边人的残忍。

这首《Ruthlessness》的母题(motif,也称「动机」,指一段音乐旋律,音乐的叙事单元)还会在之后的曲子里反复出现。Jorge 给波塞冬加戏,并不仅仅是为了让这位神明在背景板之外大显身手,还是为了给整部剧定下一个新的主题(之前的一个主题是 The Troy Saga 中的《Just A Man》),我们也可以看到奥德修斯在之后慢慢拥抱了自己的黑暗面,用对敌人的残忍来饶恕自己。

Jorge 真的对奥德修斯很残忍,原本波塞冬只是在奥德修斯安全回家之后惩罚了费埃克斯人,音乐剧中波塞冬却守在了伊萨卡海岸,拦住了刚看到故乡土地的奥德修斯。请看《Get In The Water》

呃,不过后面的那首《600 Strikes》就有些阴间了…… 什么?奥德修斯把风囊当充气背包穿上跟波塞冬肉搏?你是认真的吗? 而且全是音乐剧添加的情节,我只想解读为:奥德修斯把这一路上受到的所有苦难和折磨都发泄在了波塞冬身上,此时他彻底从人变成了「怪物」——这是从第二首曲子《Just A Man》就延续到最后的母题。

波利特斯:奥德修斯的挚友

波利特斯(Polites)在原典中的确只被提及过一次,在瑟茜的宫殿前,是他鼓励其他战士一起进入,最后他们被瑟茜变成了猪。在提及的这一次中,波利特斯被称作最勇敢的战士,也是他最亲爱的朋友。诺兰的电影完全没有出现这号人物,我前些天闲来无事去看了《奥德赛》电影演员参加的参访,被问到「谁是奥德修斯最好的朋友?」时,饰演奥德修斯的 Matt Damon 竟回答说:“欧律洛卡斯(Eurylochus)是奥德修斯的二把手,但…… 我猜是佩涅洛佩(Penelope,奥德修斯的妻子)吧。”波利特斯的名字就没有出现过。唉,要是不 Matt Damon 实在是太帅了我就已经开始鄙视他了。

音乐剧对波利特斯的处理,与原典也没有任何关系。波利特斯只被描述为「最勇敢」,其余的性格都没有交代,所以 Jorge 作了一首《Open Arms》,把波利特斯描述成一个乐天派,缓和了奥德修斯的小心翼翼、焦虑和恐惧。

甚至还给…… 波利特斯这个古代人画了副眼镜吗?此外,这类人畜无害的男性形象也相当现代,在荷马史诗里从未出现。波利特斯对整部音乐剧的作用是,成为第一批死在奥德修斯回家路上的同伴,《Open Arms》的母题也时不时出现在后续的曲子中,让奥德修斯回忆起挚友的离世和一路上失去的所有东西。

另外,波利特斯在原典中与瑟茜相关的情节中还活着,也就是说他没有死在波吕斐摩斯的嘴里,实际上我们不知道波利特斯是被斯库拉吃掉了,还是被宙斯的闪电劈死,但音乐剧中他在独眼巨人的洞穴中,倒在了奥德修斯面前。

最后

果然,分成上下两篇是不够的,所以这是中篇,这周应该会把下篇写完的。下篇将会把目光从奥德修斯的旅途转向他的故土伊萨卡,讨论诺兰塑造的忒勒玛科斯为什么是电影中最失败的角色,希腊第一美人海伦为什么有刀疤并且为战争感到愧疚,佩涅洛佩对求婚者以及自己丈夫的考验,奥德修斯的狗与塔罗牌中「星币十」的联系,诺兰给故事添加的「海上人民」究竟为何物,以及整部电影的核心主题为什么令我失望。


  1. “令式有两种。如果某种行为无关于任何目的,而出自纯粹客观的必然性,那么这种令式才是‘定言令式’。”详见 维基百科 。 ↩︎

大脑充血 Vol.94

2026年8月17日 07:56

怎么又到周一了,我上周做了什么……?生活似乎变得稳定,但也开始无聊起来了。我的文化体力还是没有恢复,抱歉了,我可能要过好久才能再写一篇书评了。不过与此同时,我又是怎么做到写了 那么长一篇《奥德赛》影评 ,竟然还有劲儿继续写第二篇的?我的大脑可真是神奇。


吾仅悉落

Nothing Makes Sense Anymore music cover

Nothing Makes Sense Anymore 专辑

Apes of the State

一张民俗朋克专辑,有些神奇的组合,大概是和朋克一样快节奏但乐器和旋律更像民俗乐的风格。本以为是一张很接地的欢快专辑,结果听到第三首的时候……

Oh Thomas Oh Thomas

You let us all down

We could have had Donald Trump slumped on the ground

Now instead we have ICE agents all over town

Oh Thomas how you let us down

——《Fascists Only Speak Violence》

挺好的…… 我最喜欢的是《The Champion》和《Mainframe》,或许还有《Jig for Straight Men Who Would Kiss Luigi Mangione》……


心重如毛

标题党就一定很坏吗?

📜
💬
💬

嚯嚯嚯,我今天要来为标题党正名! 其实我也很讨厌视频平台和社交媒体上的标题党,或者说点击诱饵(clickbait)。这类内容的特征是标题和封面和吸引人,而点进去之后却发现内容完全不是自己想的那样(顺带一提,「标题党」这个说法和 clickbait 的区别是,「标题到」没有覆盖到封面和缩略图)。不过说来惭愧,我其实写过这样的标题——《 喝咖啡如何帮我开发自己的 S 属性? 》。我承认还有一些标题纯粹是不明所以:

话虽如此,我从来没有抱着「这么写就能吸引流量!」的心态写这些标题——如果我想要流量的话,那我为什么要写文字博客? 我仅仅是觉得这么写标题很有趣,也的确有少数人或者熟悉我写作的人能理解其中的幽默。然而我不否认我时常因为不想让太多人点进来读某篇文章,而起一些无聊的标题。

我认为标题也是原文的一部分,是作者自我表达的路径之一。标题也应当彰显个性,对个人博客来说尤其如此。我也认可 Harley 在聚合网站的语境下,对某些标题类型的批评。我自己不希望在博客聚合网站上看到吸引人的标题,点进去却对内容失望(不过说实话,刻板印象中的 clickbait 只会让我跑得更远),但我同样希望我熟悉且长期关注的博主能起一些有意思的标题,因为无论是什么样的标题我大多都会点进去读的。用内容营销的术语来讲,如何起标题要看面对的是私域流量还是公域流量。

所以,我的建议是:多写一个标题。如果博主用的是 Typecho 和 WordPress 等博客系统,要给订阅源设置单独的标题可能比较麻烦,如果是 Hugo 这类可以自定义 RSS/Atom 模板的博客程序,其实可以在 Front matter (或者 Org 的 Property ?)里增加 feed-title 属性用于输出在订阅源里,或者生成两个版本的订阅源,一个用于提交给聚合网站,另一个用来让读者订阅——仔细想想这的确是两个相当不同的用途。我最近正在考虑写新的博客主题,大概会在之后加上这个功能。

另外,标题党和 clickbait 其实很难界定。一些标题很切题,不过读到的第一眼也不能知晓文章具体讲的是什么,比如《 思考是碳基生物的特权吗? 》《 阅读应当是一种体验 》。有的话题相对复杂,很难用简短的文字概括,抑或是一些散文,本来就缺少凝练的主题,而且这类文章可能更注重文学性而不是是否通俗。

至于 Bucket post(类似周刊的零散内容聚合),呃…… 我不觉得这是标题的问题,「应该不应该写周刊」是另一个议题。

成年之后也会有的小动作

📃

逐渐正视 自己可能有自闭症 或者 AuDHD 这个事实之后,我终于开始注意和重视自己的某些行为习惯,最近发现的是:我很喜欢摸头发。一般发生在书桌前,我若是高度专注地做某件事情,却不需要用手敲打键盘或者书写(比如阅读文章和读代码),我就会一直揉搓自己的头发,时常像用筷子卷面条一样把一缕头发卷在自己的头上。我还很喜欢拍打因为烫发翘起来的部分,偶尔会拉扯头发,制造轻微的刺痛感。我对这件事情相当有知觉,每次我都注意到了我在摸头发,而且相当频繁。我甚至发现坐我后面的同事每次出去抽烟再回到工位的时候,他都能看到我在摸头发(尽管他大概不关心)。就在我写这段文字的时候,我的头发是被我拨到后面的,我竟然还是会伸手到后脑勺拨弄我的头发。

其实挺困扰的,我的头发不总是很干净,于是…… 键盘很有可能变得…… 很油(还好我工作日都会洗头)。我明明知道自己会做这样的事情,却不能停下。除了摸头发,我还会咬手指甲(我应该有好几年没剪过手指甲了……),有时候我掏手机也仅仅是为了感受它的触感。这是什么强迫行为吗?

于是我找到了 Health Central 的这篇文章,大致就是,对有 ADHD 和焦虑症的人群(包括成年人)来说,这类小动作(Fidgeting)会让他们的前额叶变得更活跃,提升专注力和记忆。小动作包括抖腿、按笔、重复地转动一缕头发等等,而且必须是无意识的,如果需要投入意识活动,就没有用。比如听歌可能有助于专注,但如果自己跟着唱起来了,就不行(这也是我…… 不喜欢专注的时候听歌的原因)。另外,咬手指和焦虑症的关系更大,我自己的体验也类似,玩头发是专注的时候会有的,咬手指一般是在思考很难的问题,或者对即将到来的事情感到不安。貌似在全世界,小动作的名声都不太好,抖腿是会被父母管教的,但研究表明小动作不仅有助于认知活动,也能缓解焦虑,让自己平静下来。

神经特异者往往更容易做小动作,但神经典型者也可能从中获益。文章最后也推荐成年人尝试可以放在手里把玩的小玩具(fidgeting toys),可能对办公学习有帮助。我自己也发现,如果拿着一支笔或者别的东西把玩,摸头发的频次就会显著减少。我也开始怀疑,我看剧的时候必须吃零食,是不是也是对小动作的需求?如果在手上放一个玩具,貌似就不会想吃东西了。

卢梭与休谟的世纪大决裂

📻

完全是八卦单集。卢梭是对法国文坛乃至世界思想进步都有卓越贡献的哲学家,但他的为人很难评,比如他在《忏悔录》里承认自己在贵族小姐面前露鸟,还在宴会上和大街上朗读《忏悔录》…… 他还因为写出某些作品被法国政府驱逐出境,后来是休谟收留了他。结果,因为休谟的朋友写信散播卢梭的谣言,而休谟本人知道却因人情世故不想掺和,卢梭写了好几封信骂他。绝交过后,卢梭还把休谟的各种好意当成要陷害他的计谋,把事情闹得很大,休谟还写了一本小书,公开他和卢梭的部分书信,逐字逐句地纠正其中的不实之处,解释真相。据说被称作“好人大卫”的休谟骂得这么狠的人只有卢梭一个。

这场绝交还引得好多人围观和站队…… 怎么越看越像是社交媒体骂战在启蒙时代初见雏形呢?甚至还有发文件公开聊天记录…… 人类,果然一直都很无聊呢,哪怕是他们之中的佼佼者,而我呢,只是一只吃了桉树叶昏昏欲睡的考拉。

播客里提到的很有意思的观点是,卢梭和休谟的论战很像是刻板印象里女朋友男朋友的争吵,卢梭一直在讲感情和真诚,而休谟在一条一条地列举事实真相。他们两个对「真」的理解是不同的。不过我说实话哦,肯定要明确了事实之后才能判断这一事实对感情有什么影响,否则就像是对象在梦里出轨而自己竟然还生气了的笑话一样无理,听起来卢梭的确有些…… 妄想呢。


鸡头蛇尾

“写代码从来不是难点”是对程序员的羞辱吗?

📜

关于 LLM 辅助开发,近来常见的一种论调是「写代码从来不是最难的部分(不是瓶颈),所以 LLM 解决了写代码也不会让软件开发变得容易」。我其实很赞同, 上期周刊 分享的《 Eight Myths on Software Engineering and GenAI 》提到了这个观点,我也在《 什么是工程问题? 》回顾了《No Silver Bullet》,这篇发布于 1986 年的软件工程领域的重要论文也得出了相同的结论:构造概念是最耗时间的,而不是表达这种概念(写代码)更耗时间。

这篇文章的作者也持有类似的观点,不过受到了反驳(反驳的帖文发在 LinkedIn 上…… 我打不开那个网页也拒绝打开它),文章是对一些观点的回应。作者承认「因为写代码不是最难的,所以 LLM 不会颠覆软件开发」是有点偷懒且夸张的表述。事实是,没有人知道 LLM 会怎么改变这个行业,相信它是经济泡沫的也不少,我也是其中之一,毕竟世界上最大的 AI 公司都几乎没能盈利,没有用户愿意支付研发和运行 LLM 真正要耗费的价钱。

反驳中提到的两点有些耐人寻味。

  1. 「如果写代码狠简单的话,那为什么程序员的需求那么大,而且这么多年来薪水都很高?」——我不觉得这是很聪明的提问,工作的难易程度和需求以及薪水没有直接关系,只说明这个行业还有很多蛋糕可以分。文章作者的回复是:软件开发的边际成本几乎为零,只需要人力,利润很高。
  2. 「我遇到的很多程序员都不想和股东、顾客说话(除了自由职业者和创业者),他们只想知道自己到底要干什么,而且不要隔三岔五就改需求。」——作者只回复了一个词:Relatable.(能感同身受)我有很多想说的。《No Silver Bullet》里写软件开发最难的是概念性工作,而不是表达概念。在构造概念之前要做的是搞明白为什么要构造概念,也就是软件需求。我是否可以把这种态度理解为「不想理解需求,只想闷头写呢?」这样又怎么能交付用户需要的软件呢?不过说到最后,我貌似也只能说一句「能感同身受」了,其他能说的固然合理,但可能都有些强人所难。

不管怎么说,「因为写代码不是难点,所以 LLM 不会颠覆软件开发」这一说辞的确过于简单和理想化了,值得反思。事实是,LLM 也的确被大量地用来指定和修改软件方案,有不少诸如 Oh My Openagent 的应用也提供了成体系的解决方案——不过我得说,即便是专门用来做计划的 Agent 和 Skill 也摆脱不了 LLM 生成的内容常有模糊、留白、难以理解的偏正短语和歧义文本的事实,更摆脱不了软件作为一种概念构造必须被理解才能开发的事实,而 LLM 只会让理解变得更困难,除非开发者本来就对要做的事情一知半解。唉,总感觉这个议题越写越复杂。

有一点是肯定的:Coding Agents 引入了新的开发瓶颈——网速和模型响应时间。


漫步遐思

肉桂苹果

因为频繁在美剧里听到 Cinnamon Apple Cider 这个词(虽然就两次),所以嘴馋购入了苹果酱和肉桂浓浆,还有肉桂和气泡水。我很喜欢辛香料的味道,肉桂闻起来也很棒。肉桂貌似能激发苹果的甜味,总之喝起来很不错。

Cartle:所以这就是你又花了一百多块钱的理由吗?

Eltrac:可是真的好喝啊……

Cartle:你知道你交了房租之后余额就快见底了吗?

Eltrac:呃,这不是快发工资了吗……

Cartle:……

斯图尔特没能拯救宇宙

《生活大爆炸》的衍生剧《斯图尔特没能拯救宇宙》(Stuart Fails to Save the Universe),目前播出到第四集。剧情一般,不过其实挺好看的,谁会不喜欢多元宇宙呢?主角团是 Stuart(原剧里穷困潦倒的漫画店老板)、Denise(Stuart 的女友,不过这部剧里基本变成前女友了)、Kripke(把 R 发成 W 的那位)和 Bert(那个地质学家)。其实都是原剧里很受欢迎的配角,所以请过来拍衍生剧应该挺便宜的

剧情设定是 Sheldon、Leonard 和 Howard 制造出了某种量子纠缠机器,而 Stuart 刚好走进来撞到了机器,意外改变了多元宇宙,现在他要和一群人去找到将宇宙复原的办法。貌似每一集都有主角团中某一人的短暂客串,目前已经出现了囚犯 Raj、AI 反抗军头子 Penny、巨人恶魔 Bernadette 和精神治疗师 Amy,剩余的就不剧透了。

现在可以周一看《瑞克与莫蒂》衍生剧《柯提斯总统》,周四看《斯图尔特没能拯救宇宙》,其他时间打算找些经典音乐剧来看,或者…… 读点书吧。

我的世界一直下雨

2026年8月15日 23:42

我的草稿箱里有一篇关于神经多样性的文章一直没写得下去,这几天我才意识到,比起写一篇近似于学术综述的无聊文章,不如写点掏心窝子的话。当然,这也意味着这篇文章不会写得很有条理。

在我爷爷的六十岁大寿上,司仪要十二三岁的我站在台上说点祝福的话。我看着底下二十桌人,说不出一句话。站在我旁边的表哥很自然地说出了「祝外公福如东海,寿比南山」,见我一直没反应,司仪把话递到我嘴边,让我就说「祝爷爷福如东海,寿比南山」就好了呀。我还是不说话,似乎说出这种话对我而言在生理层面就是不可能的。我觉得那样的祝福并不真诚,而当时的我想不出任何真诚的话。或许不存在任何能在那种场合下说出口的真话,我对爷爷的感情存在于每天的生活和互动中。小时候的我很喜欢爷爷,这难道不够吗?为什么要把真实的情感放到众人面前展示,加以歌颂和粉饰?快十年过去了,我想如今的我依旧无法在爷爷的七十岁大寿上说出这种话,唯一的不同是,现在的我会把这种祝福、这种期待称作媚俗,加以批评。我有的时候会害怕,在爷爷或者奶奶的葬礼上,我会再次被放到那个位置,然后台下的所有人都会觉得我是个不孝的孙子。

随着我的社交空间不断扩大,我发现自己在形形色色的人面前也非常特殊。一个大学同室说他看到大段的文字就发颤,可文字对我来说是解救。和我来往相对密切的人时常感叹我的学习能力,可钻研自己感兴趣的事情、对未知抱有好奇心,对我来说相当自然,不费力气,我甚至要耗费巨大的力气来让自己不要陷入无止尽的阅读和写作当中。相反,如果是数学、物理和政治,我会盯着毫无神色的文字陷入想象,想象的内容有时是向别人讲述希腊神话故事、某个词源学知识和加缪的生平,有时是第一百种让一个我迷恋过的人 重新进入我生命中的方式 ,有时是我在歌唱,把整个身体都投入到旋律中。

我早已习惯沉默,当我在电梯里、饭桌上、走廊里与人相遇时,我多么想告诉他们:你不必找话填补空白,沉默就很好,我有我的思绪相伴,和人说话对我而言很累,很累。之前听一个阿斯伯格综合征1患者说,阿斯伯格并不是不会社交,只是社交并非他们的母语,需要一直在大脑里翻译社交语言。我的体验大概是相似的,除非是我熟悉的人群和环境,我总是很难读懂那些潜规则和「房间里的大象」。这也是为什么我在自己的 主页 里说我讨厌日本文化里关于「不会读空气」的指责,这不仅是群体对新来者的霸凌,还是把社交无能所带来的问题和责任转嫁给别人——如果你觉得有人没读懂空气,那就应该直接指出对方不理解的规则,而不是指责对方的读空气能力。

可是,有很长一段时间我没有意识到如「不会读空气」一类的指责和其他形式的社会共识有什么问题,我责备自己。我只觉得自己太胆小怕生,高中时和朋友聚餐,我甚至不敢叫餐馆里的阿姨打饭,不敢打电话预订桌位,我总是战战兢兢地觉得自己又会违背什么我不清楚的规则。成年之后,我似乎不那么怕生了,但身边人说,我形成了一种「生人勿近」的气场。现在的我训练自己不去多想,只去读话语中最表层的含义,只为了不蹑手蹑脚地行动。一部分原因是,我意识到即便我发现了人们预设其他人懂得某种规则的思想有多卑劣,我的想法也仅仅是少数人会赞同的,因为其他人都很快接受并适应了。他们兴许也会赞同我的剖析和评价,但他们适应了,我不能。

通常孤独症谱系患者,拥有大多数人所没有的某些天赋;但是每位患者的“天赋包”并不相同,如同孤独症谱系也有许多种组合类型。他们操作语言的方式不同于大多数人,经常忍不住从字面上理解事情,并且过度注重细节。他们往往想要什么就说什么,并希望别人也这样做。这就是孤独症谱系患者的“默认设定”。

—— 维基百科:高功能孤独症和阿斯伯格综合征编辑

一直到我今年接触到神经多样性这个概念,我才豁然开朗——草,我原来我是自闭症啊,怪不得! 仿佛我过去二十年人生中遇到的种种困难都有了解释,仿佛有人终于懂得了我的想法。这就是给事物命名之后会带来的改变吧。「他们往往想要什么就说什么,并希望别人也这样做。」——这难道不是很美好的世界吗?再也没有猜疑和胆战心惊,再也没有人预设任何规则、霸凌任何人,词语的意思就应该被这样尊重,人类给自己建起的巴别塔就应该被推倒。

我的另一个神经特异的特征,大概体现为迷恋在我生活中的地位。我或许有一些朋友,但我从未拥有过挚友,因为很少有人能让我真正敞开心扉。那些做到的人,都是我迷恋的对象,而我总是…… 毁掉这种宝贵的关系。我的生活中没有一个能让我放下戒备的人。我或许是孤独症患者,但我渴望有人进入我的世界。我渴望有人敲门,安静地走进来,紧紧地抱住我,用皮肤和我对话,我们不需要说话也能相互理解,我们可以在我的世界里抛弃所有的规则。可惜没有人来,或许也不会有人来。我知道我无法要求任何人接受我多少有些病态的依恋,但如果可以的话,那一定会很棒吧。我总是想象,我的世界对其他所有人封闭,但对一人敞开。

没有人来,为什么呢?我的世界其实很精彩,这里有全部七十八张塔罗牌的含义,三个奥德修斯的故事,和我一样讨厌作家圈子里不清不楚的规则的加缪,有歌剧院里的幽灵、索拉里斯星的大海,和后来成了桑塞维利约公爵夫人的彼埃特拉内拉伯爵夫人。是我把门看得太紧了吗?我无法忍受,有粗俗的人进来把我的东西弄乱。可我又真的好需要一个人进来,把我从地上抱起来,因为我的世界一直下雨,而我好讨厌、好讨厌,袜子被浸湿的感觉。


  1. 阿斯伯格综合征:Asperger syndrome,其实是个过时的名词,现在已经归纳到孤独症谱系障碍(ASD)当中了。简单来说,阿斯伯格综合征是伴随智力和认知障碍,且功能性语言仅有轻微或无受损的孤独症谱系障碍。 ↩︎

诺兰的《奥德赛》采取了什么样的改编策略?

2026年8月15日 15:50

笔者刚刚从电影院回到家,趁着记忆还很清晰来写影评。离开影院之前我记下了几个印象深刻的点,接下来我会逐个分析这些情节、人物、背景乃至主题层面的增删改究竟和原著有什么区别,同时我还会对比另外一部《奥德赛》的改编作品,音乐剧《EPIC:The Musical》,看看不同的创作者采用了什么样的改编策略,背后的考量又有可能是什么。

必须要说明的是,笔者只阅读了荷马史诗《奥德赛》的前两章,所以有关原著情节的对比可能不太准确,是临时查阅的。不过我想如果读者若是本来就了解《奥德赛》的故事,应该能看出诺兰导演做了非常大程度的改编,同时他又保留了不少东西(比方说这部电影的叙事结构相比音乐剧,要与原作更为相似)。

此外,比起电影,我对文学的了解可能更多,所以我也更倾向于解读情节和人物等与文学共同的要素。

电影的现代化和去神明化

我其实在看电影之前就得知,诺兰不仅仅是弱化,甚至可以说是删除了《奥德赛》中神话的部分。帮助奥德修斯对抗女巫瑟茜(Circe)的赫尔墨斯(Hermes),给予奥德修斯风囊考验的风神埃俄罗斯(Aeolus),要为儿子独眼巨人波吕斐摩斯(Polyphemus)报仇的海神波塞冬(Poseidon),以及宙斯(Zeus)本人,在电影中都没有出现。

电影中保留的三位与神话相关的角色,也都失去了神性,诺兰赋予了他们完全不同的象征意义。她们分别是海之神女卡吕普索、女巫瑟茜和智慧女神雅典娜。我会在后面提及这三位角色在电影中具体起到了什么作用,在原著中又是什么样的人物。

在仲树和诺兰的采访中,仲树问他的最后一个问题是:神明们是什么?他们是想象的产物,还是必要的高尚谎言?电影中,宙斯和波塞冬等名字被奥德赛和船员们反复提起,他们从来没有出现。在原著中与奥德修斯关系紧密,甚至还造访过他的儿子忒勒玛科斯的雅典娜,竟然仅仅是奥德修斯的愧疚和想象——难怪仲树会问这个问题。诺兰在采访中的回答是:

他们对故事中的角色来说是真的。我在思考如何看待和处理神明这一概念时感到有趣的是,当时的人们看待神明的视角。对那些处在科学以前的时代的人们,神明存在的证据遍布四周,所以神明不与信念相关,而是简单直接的现实。雷鸣和闪电就是神在与他们对话,大海的状态是神明推动和改变的,除此之外他们没有其他解释。正如我们把科学告诉我们的事情当作绝对事实一样,像是地球是圆的、地球绕着太阳移动,我们无法知道这些事情,我们之所以相信是因为它们被当作事实呈现给我们。那个时代的对自然的神话解释也是一样的,所以对我而言神明是真实的,但他们存在于人当中,是被人投射出来的。

电影中能观察到诺兰的这个想法,佩涅洛佩(Penelope)向求婚者们提出挑战时,有雷声响起,电影里的角色便说:“看来宙斯也很好奇最终的结果。”独眼巨人向父亲波塞冬哭诉之后,海上便大浪滔天,船员们怨奥德赛激怒了波塞冬。不过风神埃俄罗斯竟然没有出现,甚至连「神明存在的证据」和「人的投射」也没有。原著中奥德修斯在快抵达伊萨卡时,被埃俄罗斯的风吹回了风神的岛屿,这段情节消失了。

观影之前我发现神明没有出现在电影中的时候,感到非常失望,不过看过电影之后才觉得超出预期。至少诺兰以一种自洽的方式掩盖了神明在故事中的作用,为此他甚至改写了特洛伊战争发生的原因——之后我谈到海伦的时候,还会继续提到这一点。

诺兰对神明的解释和演绎,其实相当具有「现象学态度」,是一种哲学层面的解读。当然们以自然态度解释世界时,往往把经验与神明或者某个具体的科学理论联系起来,而现象学态度则是先验的,诺兰停下来审视了古人与神明以及现代人与科学的关系,随后从哲学视角(而非科学视角)改编了《奥德赛》中神明的现身。

无论如何,这是相当现代化的改编。尽管电影本身很精彩,但奔着神话世界观去看这部电影的人肯定会失望的。

智慧女神?还是愧疚的化身?

在荷马史诗《奥德赛》的第一章,雅典娜装成从远方来的客人前往伊萨卡,受到了奥德修斯的儿子忒勒玛科斯的热情款待。雅典娜自称是奥德修斯的老朋友(她的确也是),建议他前往斯巴达探访墨涅拉奥斯,去打谈父亲的消息。这个旅人在电影中的确出现了,忒勒玛科斯也是听了旅人的建议才前往斯巴达,但他不是雅典娜。忒勒玛科斯仅仅是说,他有着智慧的眼睛,雅典娜的眼睛——而这套说辞,他后来还向不同的人重复了几次,可见这并没有表明旅人就是乔装打扮的雅典娜。史诗中的忒勒玛科斯知道传信的是位神明,所以才接受了雅典娜给他的建议。电影中的忒勒玛科斯却不是这样,他是自发地想要打探父亲的消息,知道他是死是活。

乔装打扮的雅典娜在史诗中还有几次亮相,但电影中都没有呈现,原因刚才已经谈过了。那么,电影中的雅典娜究竟是何人物?她和奥德修斯是什么关系?

电影的最后,奥德修斯在佩涅洛佩的房门外回忆起攻进特洛伊城门的那一晚,其中有一个出现了几秒钟的画面:战士们抓住一个农家妇女,妇女向奥德修斯哭喊饶命,可战士们最后还是砍下了她的头。这个无辜妇女的形象,就是电影中反复出现在奥德修斯想象中的雅典娜,她象征着特洛伊战争的罪行、违背宙斯之法的木马计、烧杀抢掠的愧疚感

电影中「雅典娜」这个名字在不同人嘴里代表两个不同的东西:

  1. 智慧的化身,希腊人供奉和信仰的神明,只存在与神话和人心当中。
  2. 愧疚的化身,不断提醒奥德修斯他犯下的罪行,只存在于奥德修斯的想象中。

前者仅仅出现在台词和神庙中,后者则反复出现。每次“雅典娜”现身,都是一脸严肃地质问奥德修斯,提醒他的过错,直到最后,奥德修斯向妻子坦言自己犯下的罪行,决定流放自己,让忒勒玛科斯成为新王,这个“雅典娜”才留下感动的泪水,与奥德修斯和解——这个“雅典娜”仅仅是奥德修斯自己的良心而已。

史诗中的雅典娜显然不是任何人的想象,她与奥德修斯是亦师亦友的关系,他们之间甚至会像朋友一样相互开玩笑。奥德修斯与智慧女神是朋友,他足智多谋的形象也得到了进一步的确立。史诗中的雅典娜还在宙斯面前为奥德修斯求情,请求众神释放被卡吕普索囚禁的他。甚者,整个特洛伊战争之所以发生,都与雅典娜有关系。在海洋女神忒提斯的婚礼上,不和女神厄里斯因为没有被受邀,所以心生诡计,将写着「送给最美丽的女神」的金苹果送给宴会上的三位女神——天后赫拉、爱与美的女神阿佛洛狄忒以及雅典娜。三位女神都想当然地认为金苹果是送给自己的,争执不下,于是去找特洛伊国王的儿子帕里斯裁决。帕里斯选择了阿佛洛狄忒,之后帕里斯受爱神教唆乘船到斯巴达,与王后海伦发生关系,最后说服海伦跟自己回了特洛伊。由于希腊第一美人海伦被特洛伊人拐走,斯巴达国王墨涅拉俄斯非常恼怒,于是找阿伽门农召集英雄攻打特洛伊,奥德修斯也被迫出征。

电影中也提及了特洛伊战争的开头,战争也的确因海伦而起(海伦的形象在电影中非常特殊,不仅仅是因为肤色,这点会在之后专门谈到),不过神明的意志被弱化了。电影中一笔带过的台词我记不太清了,貌似劫走海伦仅仅是特洛伊人为谋求商贸利益而打的幌子。

有点跑题了,不过电影中的雅典娜的确没太多好讲的了,所以接下来就欣赏一下音乐剧中的雅典娜吧。Jorge 编曲的《Warrior of the Mind》描绘了奥德修斯与雅典娜相遇的场景,也交代了他们之间亦师亦友的关系:

哦对,这首曲子提到了卡吕冬狩猎(Calydonian Boar hunt),音乐剧中雅典娜说他设下挑战,要看谁有能力猎杀野猪,最后是小男孩奥德修斯干掉了它。神话里,这只野猪是月亮女神阿耳忒弥斯为了惩罚忘记给她献祭的卡吕东人设下的,和雅典娜没什么关系,而且 猎人名单 中没有奥德修斯,只有他的父亲拉厄耳忒斯。电影中出现了奥德修斯猎杀野猪的情节,也和雅典娜没有关系。

此外,雅典娜请求释放奥德修斯的情节,在神话里其实进行得很顺利。雅典娜对宙斯说:“我的父亲,克罗诺斯之子,至尊之王…… 难道奥德修斯没有在阿耳戈斯船边,在特洛亚旷野,给你献祭?宙斯啊,你为何如此憎恶他?”宙斯的回答是:

我的孩儿,从你的齿篱溜出了什么话?
我怎么会把那神样的奥德修斯忘记?
他在凡人中最聪明,给掌管广阔天宇的
不死的神明们奉献祭品也最丰盛勤勉。
是环绕大地的波塞冬一直为独目巨怪
怀恨在心,奥德修斯刺瞎了他的眼睛,
就是那神样的波吕斐摩斯,独目巨怪中
数他最强大;他由神女托奥萨生育,
广漠的咸海的统治者福尔库斯的女儿,
在深邃的洞穴里与波塞冬融情媾和。
为此缘由,震地神波塞冬虽然不可能
杀死奥德修斯,但却让他远离乡土。
现在让我们一起考虑他如何归返,
让他回故乡;波塞冬终会消弭怒火,
因为他总不可能独自执拗地违逆
全体不死神明的意志,与众神对抗。

简单来说,众神之王宙斯相当欣赏奥德修斯,是波塞冬一直困住了他。宙斯的话一出,众神也同意了,随后便决定释放奥德修斯。在神明已经被剔除的电影中,当然是没有这个情节的,但在音乐剧中仍有保留,不过雅典娜为好友付出了很重的代价。详见《God Games》一曲:

荷马的奥德修斯在某种意义上是个万人迷,有众神协助他归乡。而音乐剧里的奥德修斯则被视作耻辱,是挚友雅典娜拼命为他争取机会。至于诺兰的奥德修斯…… 呃,我们谈卡吕普索的时候再说。

瑟茜为何满面愁容?

音乐剧里的 The Circe Saga(瑟茜篇章)是我最喜欢的篇章,各位可以跳转到下面这个视频一分钟的位置感受一下音乐剧中的女巫瑟茜是何等人物:

荷马史诗中的瑟茜(也译作基尔克,或者喀耳刻)也十分美丽,具有神性。荷马史诗把她描述为居住在宫殿里的神女,有侍女相伴,一头美发,通晓草药魔力。

他们站在美发的神女的宅邸门前,
听见基尔克在里面用美妙的声音歌唱,
在高大而神奇的机杼前忙碌,制作精细,
无比美丽、辉煌,只能是女神的手艺。
士兵的首领波利特斯对他们开言,
同伴中他最令我喜欢,也最勇敢:
‘朋友们,里面有人在巨大的机杼前
唱着优美的歌曲,歌声四处回荡,
不知是天神或凡女,让我们赶快去询问。’

瑟茜把奥德修斯的伙伴变成了猪,背后的动机没有明说,仅仅是把她塑造成害人的美丽女巫——稍后可以看到两部改编作品,都对瑟茜的动机做了不同的解释。史诗中奥德修斯受到了人神信使赫尔墨斯的帮助,他给了奥德修斯摩吕(Moly),吃下之后便免疫瑟茜的巫术。之后他才踏入瑟茜的宫殿,将她打倒,最后瑟茜被奥德修斯折服,和他上了床…… 而且不止一次,之后的一年里,他的弟兄们都在宫殿里吃喝享乐,他自己也和神女共枕…… 不难想到,两部改编作品都删去了这个情节。

史诗和音乐剧中的瑟茜具有神性且住在华丽的宫殿里,但是在电影中,瑟茜却非常愁苦,充满怨恨,她甚至把自己的姐妹变成了乌鸦,和人变的动物居住在一座小茅屋里面。当电影中欧律洛卡斯询问一位惊恐万分的中年妇女姓甚名谁,而她的回答竟然是「瑟茜」时,我感到非常惊讶。

电影中的瑟茜的确还有着巫术,仅仅是神性被消除了。战士们吃了食物被瑟茜一个一个揉搓成猪的画面也十分震撼,可能比挥挥魔杖更好看。此外,奥德修斯是完全依靠自己的力量和计谋与瑟茜对抗的,他擒住了笼子里的乌鸦威胁瑟茜,而这只乌鸦是瑟茜的姐妹变的,这时她才坦言道,自己把到来的人都变成动物,是因为这些男人在她的家里完全不尊重他(诺兰似乎又一次暗示了宙斯之法,或者好客之道),也是为了自我保护。之所以把姐妹也关进鸟笼,是因为这样一来她们更容易相处,不难想象着也是一种自我保护,或者说心理防御。

所以,诺兰的瑟茜呈现出来的几乎是「应激」的状态,外界给她带来了太多的痛苦和伤害,甚至连姐妹也无法和善相处,这才使得她把身边的所有人都变成手无缚鸡之力的动物,这样一来她才能控制身边的人。

音乐剧中的瑟茜将人变成猪的动机也是出于保护,但并不是诺兰笔下的自我保护。瑟茜是为了保护和她一起生活在宫殿里的侍女,之前她们让陌生人活下去,却受到了莫大的损失,此后她们便不再相信陌生人。

所以诺兰和 Jorge 分别想借由瑟茜这个角色表达什么呢?毕竟在史诗中,瑟茜的动机非常神秘、不被言说,可以理解为神仙的随心所欲,而两部作品都让他们变得更像人,动机被明确且正当化,变成了两个个体因各自目的不同而导致的冲突,而不是英雄与纯粹的恶人对抗。

尽管我对失去神女身份的瑟茜感到失望,但不得不说,诺兰笔下的瑟茜的确更具文学性。音乐剧中保护身边人的瑟茜是相当简单且非常符合道德准则的解释,而诺兰笔下受惊、愁苦、自私、被痛苦摧残的瑟茜,则更好地反映了当代人的个体困境——外界似乎只存在着危险,而最亲近的姐妹也无法带给自己慰藉。

如果不反对把奥德赛现代化的话,瑟茜这个人物其实是相当不错的改编。

“卡吕普索,你是神明吗?”

卡吕普索(Calypso)是海之神女,因为爱上了被冲上岸的奥德修斯,用神力将他困在岛上七年,直到智慧女神雅典娜(Athena)前去找父亲宙斯求助,赫尔墨斯才前往奥吉吉亚岛告知卡吕普索,神明们要她释放奥德修斯。电影中的卡吕普索的确是接济奥德修斯的人,但她显然不像神明,她仅仅是居住在海边的隐者。

有趣的是,电影里的奥德修斯其实开口问了卡吕普索:你是神明吗?卡吕普索对此的回应是:沉默。这真的非常有趣,诺兰没有让卡吕普索承认她是神女,也没有否认她是。观众可以用任何方式解读这种沉默,可能是因无法反驳奥德修斯的话语而沉默,也可能是为了伪装自己的神明身份而沉默。不过,卡吕普索在整部电影里的形象与原著的形象完全不同。比起神明,她更像…… 心理咨询师?

电影中的卡吕普索从一出场就在对奥德修斯进行「话疗」,她跟奥德修斯说的话类似于「你感觉怎么样?」「你还记得当时发生了什么吗?」「你可以明天再回忆」。故事有相当一部分是通过奥德修斯对卡吕普索讲述回忆进行的。

值得一提的是,电影中的卡吕普索给奥德修斯吃了名为「忘忧莲」的植物,电影用的英文单词是 Lotus。在原著中,Lotus 和卡吕普索没有任何关系。莲花是奥德修斯和船员在另一座岛屿上遇到的植物,那座岛上住着一群名叫 lotophages 的族人,直译过来是「莲花食者」(Lotus-eaters)。荷马史诗中的莲花也不是用来让人忘记悲伤的,它的作用是让人忘掉自己的家和爱人,留在原地继续食用莲花,永远也不离开。

电影中,卡吕普索没有用神力囚禁奥德修斯(因为她神女的身份并不明确),而是喂奥德修斯吃莲花,不让他回忆起悲痛的过往,在他肉体的伤痛尚未缓和之前,不让他再承受心灵的伤痛。电影里的卡吕普索也的确爱上了奥德修斯,但并没有因为爱而强迫奥德修斯留下,而是像治疗师一样慢慢帮助奥德修斯控制吃药(莲花)的量,待他回忆起妻子的名字之后,确认他真的准备好了之后,才让他离开。尽管卡吕普索的确动了私心,让他多留了一会儿(指 7 年),但总的来说,她留住奥德修斯的原因是:当时的奥德修斯还没有准备好、没有能力回家。等奥德修斯准备好了,她完全没有阻拦他乘坐木筏出海。

卡吕普索身为治疗师的角色对奥德修斯的成长变化乃至情节的推动,在电影当中是相当重要的。她不仅救了奥德修斯的命,让他渐渐恢复了心力,也让他理解了神明的意志,将自己投身于世界当中。我们也看到,奥德修斯并没有用浆划动木筏,而是躺在木筏上,任由海水冲刷自己的身体,最后是海洋和风(神明)将它带回了家。诺兰甚至好心地把波塞冬摧毁奥德修斯的木筏,不得不游回家的情节,改成了海水将奥德修斯冲到伊萨卡的岸边,就好像奥德修斯一旦将自己献身给世界,神明就会为他开辟道路一样。

不得不说,诺兰的卡吕普索还是有点太健康了,让我们来看看隔壁《EPIC》里撕心裂肺的卡吕普索:

我要看到泪流成河、爱而不得、歇斯底里! 另外,史诗中,奥德修斯被困的这七点里也和卡吕普索同枕共眠,当然也做了该做的事情…… 两部改编作品都忽略了这点,把奥德修斯塑造成一个忠贞的男人,关于这点我就不评价了。

全电影最惨的独眼巨人

独眼巨人是电影中奥德修斯遇到的第一个岛屿上的敌人(如果不算诺兰为了铺垫 The sea people 而增加的烧杀抢掠情节的话),我之所以说他惨,是因为他不仅被塑造成了一个过于丑陋、恐怖的怪物(真的有必要给他的鼻子也弄歪吗……?),而且电影中还没有提及他的名字。是的,是的,这个独眼巨人是有名字的,魔物就不值得尊重了吗?! 另外,奥德修斯和波吕斐摩斯的智斗也被删除了,他与波塞冬的父子关系也被弱化。

从这里开始我们就要提及 Xenia 的概念了,电影中这个概念被简化为 Zeus’ Law(宙斯之法)。这个概念最简单地理解是「好客」,要像家人一样招待远道而来的客人,因为他们有可能是伪装成凡人的神明。电影中,诺兰把这个概念提升为「像你希望被别人对待的那样对待别人」,甚至升华为整部作品的核心主题。

荷马史诗中,奥德修斯一行人进入独眼巨人的洞穴寻找补给品,本以为洞穴的主人会好客地接待,没想到波吕斐摩斯放羊回来之后把洞口用石头堵住,直接吃掉了奥德修斯的两个战友。第二天早上醒来放羊之前,巨人又吃了两个人,晚上回来的时候又吃了两个。至此,电影的情节还是忠于原著的,不过诺兰省略了奥德修斯戳瞎波吕斐摩斯之前的经典桥段。

波吕斐摩斯并不是在睡觉时被刺瞎的,奥德修斯提供了美酒给独眼巨人。这里要提到一个小知识,古希腊人喝酒讲究节制,有教养的人不会暴饮,还会稀释过后再喝。而独眼巨人不仅不讲好客之道,也不讲节制,一口气喝完了未稀释的烈酒。他醉醺醺地询问奥德修斯的名字,并保证给奥德修斯一份礼物,这时奥德修斯说自己的名字是 Οὖτις(没有人),然后波吕斐摩斯揭示了自己要送给奥德修斯的礼物:他将会最后一个吃掉「没有人」。不过很快他就醉昏过去了。此时奥德修斯一行人才戳瞎了它。原著中,波吕斐摩斯的叫喊还惊动了其他的独眼巨人,同伴们问波吕斐摩斯发生了什么,波吕斐摩斯回答:“没有人!没有人伤害我!”于是其他独眼巨人就没有理他。

原著中的奥德修斯在逃脱之后犯了个错误,他傲慢地向波吕斐摩斯喊出了自己的真名,这使得之后波吕斐摩斯的父亲波塞冬来找奥德修斯报仇。在诺兰的改编中,奥德修斯根本没有尝试和独眼巨人沟通过,即便在他们意识到独眼巨人会说话之后也没有。不过电影中奥德修斯犯的错误也同样愚蠢,他因为愤恨独眼巨人杀死了他好几个弟兄,于是在逃脱之后继续攻击独眼巨人,彻底激怒了他,独眼巨人闻声追到海岸,又杀死了两个没来得及上船的兄弟。

电影中独眼巨人和海神波塞冬的关系,仅仅是被暗示过。独眼巨人被戳瞎眼之后,哭喊着「父亲波塞冬,为我报仇」。逃离之后,一行人在船上遭遇了巨大的海浪,此时船员才将伤害独眼巨人一事与波塞冬联系起来。这符合前文提到的,诺兰对神明的思考和处理。另外,原著中奥德修斯一行人是抓着羊肚子逃离独眼巨人洞穴的,而电影中是背着干草爬出去的。我想这是因为…… 趴在羊肚子上太难拍了吧。

波吕斐摩斯在诺兰的改编中失去了人性,完全成为了一种无法沟通的怪物,海神波塞冬的惩罚也仅仅是人物的联想和投射。这一情节除了初步地激化奥德修斯与船员的矛盾,再次强调宙斯之道的概念,此外貌似就没有什么用处了,所以省略了一些情节,快速带过也算合理。对人性的消除,想必也是为了弱化神话要素,和电影整体的调性相调和。值得一提的是,荷马的奥德修斯之所以一直回不了家,是因为波塞冬的阻拦,而起因就是向波吕斐摩斯报出了真名。诺兰把奥德修斯回不了家的原因改编为他还没有为自己的行为赎罪。

在音乐剧《EPIC》的改编中,独眼巨人并没有直接吃人,相反,奥德修斯看见挚友波利特斯的尸体完整地倒在自己面前。和诺兰的改编决策完全相反,波吕斐摩斯的人性在音乐剧中被强化了,他不是彻头彻尾的野兽,而是因自己最心爱的绵羊被奥德修斯杀死而痛苦。他被复仇的心理驱使,而不是被食欲驱使。正因为野蛮和欲望没有被强调,音乐剧中的波吕斐摩斯也不是喝醉倒下的,而是奥德修斯在他的酒里掺了前面提到的会控制心智的莲花。

音乐剧的改编中,Jorge(《EPIC》创作者)一方面想要强调奥德修斯因失去伙伴而感受到的痛苦和愤恨,所以让伙伴的尸体倒在他面前而不是被吞下(这也使得他最后喊出真名时,并不是像原著一样出于傲慢,而是出于愤恨)。另一方面,音乐剧中的大部分角色(包括怪物)都是相当拟人化的,就连海怪斯库拉(Scylla)也是——音乐剧中,奥德修斯与斯库拉对唱「You and I are the same」(你和我是一样的),因为奥德修斯在那个时候深知驶过海峡会导致同胞死去,却还是想要通过海峡回到家。电影中的斯库拉倒是非常…… 高效地吃完了六个人,然后就缩回石头里了,连张脸都没有出现。

最后,诚邀读者欣赏音乐剧里的《Polyphemus》和《Remember Them》,可以看看音乐剧是怎么处理「没有人伤害我」这段经典情节的。

可见,比起电影里一上来就烧杀抢掠,音乐剧里的奥德修斯在一开始还非常仁慈(mercy)。仁慈和残忍(ruthlessness)是音乐剧的主题之一,我还在之后继续提到。

冥界在故事中的关键作用

在两部改编作品中,奥德修斯都是在瑟茜的帮助下去到了冥界(Hades 或者 Underworld)寻找先知,并且冥界都是相当关键的转折点。

一如既往,先来看电影中的冥界。奥德修斯在冥界见到了自己死去的战友,他们从土里爬出来诉说他们对奥德修斯的不满和仇恨,因为奥德修斯的鲁莽直接导致了他们的死亡。尚且存活的船员看到了自己的宿命,心生恐慌,对奥德修斯的不信任也逐渐加剧起来,后来欧律洛卡斯带头策反也就显得合理了。更关键的是,冥界中的先知特伊西亚斯为奥德修斯指明了回家的路:经过海妖塞壬,然后有两条路可选,一条路要经过海怪卡律布狄斯(Charybdis),会导致除他以外的船员全部死去,另一条路经过海怪斯库拉(Scylla),这条路只会损失六人。

在先知告知奥德修斯回家路途的时候,奥德修斯一直在想办法解决路上会遇到的阻碍,他想要救下尽可能多的人。当先知说海妖会用歌声迷惑他们,奥德修斯说他会保证士兵都用蜂蜡把耳朵封上;当先知提供卡律布狄斯和斯库拉的选择时,他说他会尽可能让更多的人活下来。最后,先知说:奥德修斯想要救下所有人的心,最终会杀死所有人。然后他怀着沉重的心和责任,逃离了冥界。之后,面临卡律布狄斯和斯库拉的选择时,奥德修斯借助了欧律洛卡斯和船员的不信任,故意说要往旋涡海怪那边走,船员们不管奥德修斯的命令,通往了斯库拉的巢穴,这才只损失了六人。

然而,欧律洛卡斯看出奥德修斯其实早就知道斯库拉的存在,对欺瞒感到愤怒。这时船员的负面情绪几乎到达了顶点,最后在太阳神的岛屿上,他们不顾劝阻,避开奥德修斯吃掉了太阳神的牛,宙斯的闪电使得所有的船员沉没大海,仅有奥德修斯一人幸存,被冲到了卡吕普索的岸边。至此,先知以及瑟茜的预言应验了,奥德修斯想要救下所有人,所以欺骗船员,这才使得船员因奥德修斯的欺瞒而爆发,最终船员自己导致了自己的死亡,也死于奥德修斯想救下他们的心。

在诺兰的电影中,冥界是双重导火索,它引发了船员的反叛和自毁,也让奥德修斯承受了足够的痛苦与自责,最后被冲到卡吕普索的岛上时,他已经心力交瘁,所以需要疗伤。在音乐剧的改编中,冥界的先知并没有指引归途,旋涡海怪卡律布狄斯甚至不是奥德修斯和船员一起经过的,而是他从卡吕普索的岛屿离开之后遇到的,斯库拉的巢穴也是奥德修斯在与海妖的交流中得知的路线。那么冥界在音乐剧中起到了什么作用呢?

这时我们就必须提及诺兰删掉的一个重要的人物——安提克勒婭(Anticlea),奥德修斯的母亲。是的,诺兰的奥德修斯没有妈妈。她在等待奥德修斯回家的二十年里死于悲痛,也有说法是他听到了关于他儿子的不实传言,之后投海自尽。奥德修斯在冥界与母亲相遇,他见到母亲的亡魂时忍住了悲痛,先与先知对话,之后才与母亲相认:

‘我的母亲啊,你为什么不让我抱住你? 让我们亲手抱抚,即便是在哈得斯, 那也能稍许慰藉我们那可怕的悲苦。 是不是高贵的佩尔塞福涅只给我遣来 一个空虚的幻影,令我悲痛更忧愁?’

可惜活人与死人无法相拥,奥德修斯仅仅是从母亲那里打听了家里的情况,便结束了对话。原著的后面,奥德修斯还报菜名一般地列举了一个个前来对话的贵族妇女的亡魂,电影和音乐剧都将其删去了。音乐剧中奥德修斯虽然见到了母亲,但却没能与他对话。

实际上音乐剧里的奥德修斯在冥界只和先知一人有过交谈,他只是听到了战友亡魂的哭喊。先知没有告诉奥德修斯回家的路,只是向他揭示了命运:他看见有人牺牲,有人背叛,弟兄们临死前的最后一面以及苟延残喘的奥德修斯。先知预言他能够回到家,但到家时,奥德修斯已经不再是他自己。

音乐剧中的冥界也给奥德修斯带来了难以承受的痛苦——母亲的死讯、战友亡灵的怨恨,以及最终无论怎么努力也无法顺利原样回到故土的事实。可以说,音乐剧中的冥界是奥德修斯「黑化」的导火索,冥界之后他们很快去到了塞壬的海域,比起用蜂蜡和绳索让自己不被歌声迷惑,已经黑化的奥德修斯砍断了海妖的尾巴,把他们丢进了海里。音乐剧中,冥界之行完全消除了奥德修斯的的仁慈,让他变成了为了回家不择手段的怪物。

塞壬的歌声,绳索和蜂蜡

冥界之行过后,奥德修斯一行人要过的第一关就是海妖塞壬,传说他们会用美妙的歌声让水生失神,最后触礁而亡,抑或是被迷惑,跳入水中被海妖吃掉。诺兰保留了荷马史诗中奥德修斯让船员把自己绑起来的情节。其他船员都用蜂蜡堵住了耳朵,没有被海妖的声音魅惑,而奥德修斯想要听海妖的歌声究竟是什么样的,让船员把自己绑在桅杆上,无论如何都不要松绑。荷马之后的一些作家写到,若是有人听见了海妖的歌声却成功逃脱了,海妖就会死去,所以奥德修斯实际上杀死了海妖。

电影中奥德修斯说他在海妖的歌声中听到了他最大的渴望。那么奥德修斯最渴望的东西究竟是什么?诺兰没有给出肯定的答案,但它让奥德修斯知道,他并不是真的想要回家。或许我们不知道诺兰认为的奥德修斯的最大渴望是什么,但音乐剧中奥德修斯却实实在在地看到了他最大的渴望——他的妻子佩涅洛佩。

与荷马的故事不同,音乐剧中的奥德修斯用蜂蜡塞住了自己的耳朵,并读唇语与海妖交流。他知道海妖熟悉水路,所以从伪装成自己妻子的海妖口中得知要回家就必须经过斯库拉的巢穴。这个改编在某种程度上是更合理的,因为…… 难道先知告诉你你要么跟旋涡海怪拼速度,要么跟吃人海怪肉搏,你就不能选第三个选项,绕过这两个地方吗?音乐剧中塞壬让奥德修斯经过斯库拉巢穴的原因是,追杀他的海神波塞冬害怕斯库拉,不敢经过那条路。

此外,如果听了海妖的歌声并幸存真的会杀死海妖,那么音乐剧里的奥德修斯则是直接用刀剑砍断了海妖的尾巴。总之是不给妖怪留活路的。

冥界之行彻底改变了奥德修斯,在音乐剧这边可能显得更为沉重。诺兰的奥德修斯好歹还为了保住更多人的性命,把同伴引向死伤更少的路,而音乐剧中的奥德修斯只有一条路可以走,进入斯库拉的巢穴时,他让欧律洛卡斯点燃六把火炬,随后斯库拉便吃掉了拿着这六把火炬的六个人。诺兰的奥德修斯在牺牲一群人和牺牲六个人当中做出了选择,而音乐剧中的奥德修斯主动献出了六个人的生命。

另外,海怪斯库拉其实是女巫瑟茜因为忌妒变成的,结合电影里瑟茜一点点把人搓成猪的画面,我不敢想象瑟茜要把一个女子搓成有好几个头的巨型海怪有多累。

最后

我已经写得太长、太久了,我想读者读起来已经很困难了。所以,尽管我还有很多想写的,我还是必须克制住自己,把没写完的内容留到下一篇文章。下一篇关于《奥德赛》的文章,我将会讨论在电影中消失的人和神(奥德修斯的最欣赏的朋友波利特斯,帮助奥德修斯刮起回家的风的埃俄罗斯,为奥德修斯提供对抗瑟茜的摩吕的赫尔墨斯,以及从未现身的海神波塞冬),我认为电影中塑造得最失败的角色忒勒玛科斯(对不起,荷兰弟!),海伦脸上的刀疤(我不会讨论「为什么希腊第一美人由黑人饰演?」这种无聊的政治问题),佩涅洛佩给求婚者的挑战,奥德修斯的狗,以及整部电影的主题和我对它的感受。

最后,请容忍我再发表一些观影的小抱怨:旁边坐了对小情侣,男的明显不想来看,而女的一直在检查男朋友有没有认真看…… 我本以为她一直盯着男朋友还叽叽喳喳是因为她真的很喜欢希腊神话呢,结果看到一半她突然问她男朋友「欸,波塞冬是谁啊?」。我真的…… 你们能不能闭嘴,吵死了……

回见!

大脑充血 Vol.93

2026年8月10日 08:28

本期周刊我将讨论《奥德赛》的三个版本:荷马史诗原著,Jorge Rivera-Herrans 改编的名为《EPIC》的音乐剧,以及由 Christopher Nolan 改编和导演的《奥德赛》电影。荷马的《奥德赛》我其实只读了第一章,诺兰的电影我也只是有所耳闻,只有《EPIC》从头到尾看过、听过好几遍,所以可能有失偏颇。

总之,可以把这期周刊当作《奥德赛》特辑,不过还辅以一些 LLM、旧新闻和牢骚。希望读者周一愉快吧。


吾仅悉落

EPIC: The Musical music cover

EPIC: The Musical

Jorge Rivera-Herrans

诺兰最近发行了新电影《奥德赛》,笔者身在中国大陆,还要一段时间才能看到,所以最近回顾了音乐剧《EPIC: The Musical》。这是一部同样由荷马史诗《奥德赛》改编的作品,音乐由 Jorge Rivera-Herrans 一人写作,音乐剧中的奥德修斯也由他演唱。《EPIC》的其他卡司1、动画和制作几乎全是由 TikTok、YouTube 等网络平台上的创作者合力完成的。可以说这是完全由业余爱好者制作的动画音乐剧,可音乐质量、叙事复杂度和情感深度都不输专业制作人,前不久貌似还公布了动画电影投入制作的消息。

我第一次听闻这部音乐剧是上大学之前,在 Bilibili 上看到了《 Done for 》的片段(这应该是 Demo,和正式发布的歌曲有出入),这段情节是奥德赛与女巫喀耳刻对峙的场面。当时的我仅仅是对音乐印象深刻,也没有意识到这是部完整的音乐剧制作计划。2022 年年底,第一个篇章《The Troy Saga》发布,直到 2024 年年底歌曲才全部放出。在 YouTube 和其他视频平台上可以找到由不同创作者绘制的动画拼凑而成的完整电影,时长为两个半小时。

音乐剧一共有四十首歌,分成了两幕和九个篇章。

  1. Act 1
    1. The Troy Saga:奥德修斯赢得了持续十年的特洛伊战争,不得不杀掉仍在襁褓中的特洛伊王子以免他在将来复仇,之后和六百名战士踏上回到家园伊塔刻的旅程。
    2. The Cyclops Saga:奥德修斯在岛屿上寻找食物,因杀死了独眼巨人波利菲莫斯的羊,激怒了他,随后失去挚友和船员。他最后戳瞎巨人,窃羊而逃。离开之前,他留下了自己的大名。
    3. The Ocean Saga:奥德修斯请求风神埃奧洛斯帮助,风神给予他装满风暴的风囊,告诉他只要不打开袋子就能回家,临近家园时因欧律洛科斯打开了袋子又被风暴吹回巨人岛,从独眼巨人的父亲波塞冬惊险逃脱。
    4. The Circe Saga:女巫喀耳刻将船员变成猪,奥德修斯在赫尔墨斯的帮助下战胜了女巫,喀耳刻随后帮助奥德修斯去到冥界找先知求助。
    5. The Underworld Saga:奥德修斯航行于冥界,遇见了他死去的战友和迟迟等不到他回家的母亲,受先知特伊西亚斯的话影响,他决定收起仁慈和软肋,不惜一切代价回家。
  2. Act 2
    1. The Thunder Saga:奥德修斯和船员用蜡堵住双耳抵御塞任的歌声并杀死了她们,随后穿过斯库拉的巢穴。麻木的奥德修斯任凭船员死去,激怒了欧律洛科斯,带领剩下的船员背叛了他。船员找到了最近的岛屿,杀牛充饥。奥德修斯百般劝阻船员不要伤害太阳神的牛群却没能成功,最后宙斯现身,让奥德修斯选择保留自己的性命,或者保留船员的性命。奥德修斯选择了自己活下去。
    2. The Wisdom Saga:奥德修斯的儿子忒勒玛科斯长大成人,而伊塔刻有许多人对王位图谋不轨。与此同时,奥德修斯被冲到女神卡吕普索的岛上,女神爱上了他并将他困在岛上七年。雅典娜寻求宙斯的帮助,最终赫尔墨斯前去要求卡吕普索释放奥德修斯。
    3. The Vengeance Saga:奥德修斯离开卡吕普索,用徒手制成的筏子回家,却发现海神波塞冬在家门口静候已久。赫尔墨斯将风囊又给予奥德修斯,奥德修斯打开袋子战胜了波塞冬,却掀起风暴阻拦了回家的路。奥德修斯用波塞冬自己的三叉戟反复刺向它,逼迫波塞冬停下风暴。
    4. The Ithaca Saga:奥德修斯回到伊塔刻,却听见图谋不轨之人密谋要杀死他的儿子、强奸他的妻子,奥德修斯与忒勒玛科斯相认,一起杀死了反叛者。最后,奥德修斯与妻子佩涅洛佩重聚。

如果对这部作品感兴趣,也可以到 官网 上购买 CD、彩胶和周边。我最喜欢的几个片段是由 Neal 绘制的《 Thunder Bringer 》、gigi 绘制的《 God Games 》、《 Keep Your Friends Close 》和《 Suffering 》,此外还有《 Wouldn’t You Like 》和《 Dangerous 》(谁会不喜欢赫尔墨斯……)。


心重如毛

仲树采访诺兰

📻

仲树观看了诺兰改编的《奥德赛》电影之后,在七月三十日采访了诺兰。这期采访之所以没有出现在上一期周刊,是因为上周的我没有听懂采访的内容。于是我这周补了不少《奥德赛》的情节,对诺兰的改编选择也有了些了解(尽管我还没有看过这部电影),这才可以继续。

采访中我印象最深刻的是以下几个主题:

  1. 诺兰是否基督教化了《奥德赛》?
    • 仲树说赎罪(atonement)这个概念并不存在与古希腊文化中,更没有出现在荷马的《奥德赛》中。诺兰的奥德修斯却是以忏悔罪人的形象出现的,这更接近基督教文化。
    • 诺兰说他在改编时并没有刻意选择某一条哲学路径,也就是说他并不是想要基督教式地改编《奥德赛》。赎罪的概念可能没有直接出现在原著中,但他认为古希腊文化中的 Xenia(好客之道,或者说「用你想被别人对待的方式对待别人」)与如今的价值观有共通之处。他关注的不是哲学理论的区分,而是理念的共通性。
  2. 识别出叙事的套路(或者说模式)是否就让套路失效了?
    • 诺兰在采访中表达了他对业余影评人的看法,业余影评人经常在识别出电影的套路、搞清楚为什么电影会以某种方式影响自己之后,就把这种套路「无效化」了——用更本土的说法来讲,我想可以称作「祛魅」。业余影评人热衷于对电影叙事的模式祛魅,然而仅仅是观察到模式并不代表模式就变得低劣了。电影无法脱离套路凭空出现,因为电影是一种人类共通的叙事语言,抛弃套路就是抛弃语法句式。熟悉的模式当然会让人觉得无聊,诺兰表示这的确是电影导演要考虑到的,电影应该主要建立在人们熟悉的模式之上,此后才能引入新的模式。这也是他对传统和创新的理解。
  3. 电影中的神明究竟是什么?
    • 这一条是我自己的观点:当我得知诺兰的《奥德赛》里没有宙斯、赫尔墨斯、波塞冬、埃奧洛斯和莲花食者的时候,我感到震惊,这难道不是把荷马史诗里的整个希腊神话世界观都剔除了吗?甚者,为数不多的保留下来的神明几乎都失去了「神性」,雅典娜更像是奥德修斯的投射和想象,而不是相互嘲弄的朋友,爱上奥德修斯的卡吕普索明明困住了他七年,在电影里却表现得像个心理医生。老实说,目前看来我很失望,但最终的评价还是要等到去看完电影之后才能作出。
    • 采访中诺兰表示神明对故事中的角色来说是真的,当时的人们就像我们接受科学概念一样,理所应当地接受了神明的存在,比如雷电就是宙斯在说话。神明更多是电影中角色的投射。这么说来,诺兰的电影里的确没有真的神明,只有「会被古人误以为是神明的自然现象」以及他们文化中对神明的信仰。不得不说,这是相当现代化的改编,我还不能确定我喜不喜欢。

其实音乐剧《EPIC》也对荷马的《奥德赛》做了不少改编,比方说原著中的奥德赛其实数次出轨,他与女巫喀耳刻同床了一年,被卡吕普索困在奥吉吉亚岛上的七年,他也与女神共枕,而《EPIC》中的《奥德赛》则十分忠贞和坚定,诺兰的《奥德赛》则完全没有提及性。我想这是因为《奥德赛》中的一些故事情节可能令现代人难以接受,或者说改编者选择的切入角度不同,有些东西被忽略了。

Jorge 或许出于艺术表达的偏好或呈现效果改编了不少情节,比方说奥德修斯杀死襁褓中的特洛伊王子是受到宙斯的提醒(甚至威逼利诱),而原著中这个情节却没有出现宙斯,雅典娜请求宙斯释放奥德赛也没有像《God Games》里那样受到重重阻碍,原著中的宙斯其实相当欣赏奥德修斯。诺兰也因为对电影主题的关注点不同而做出了相当大程度的修改,因为无论是原著还是《EPIC》,奥德修斯从来没有为自己打赢了特洛伊战争而羞愧过,而赎罪却是诺兰电影的一大主题。

我多少有些为荷马不平,层层修改之后人们真的还在乎这位伟大诗人想要表达的东西吗?可后续的创作者也有自己想要表达的东西,难道他们的想法就不该得到实现吗?我不知道怎么回答,我只能说这三个版本的奥德赛都是很不同的作品。

诺兰如何基督教化了《奥德赛》?

📻

有不少其实都在上一节里写过了,这一节主要谈一点:追求光荣如何变成了骄傲和狂妄?

荷马作为古希腊诗人,实际上是追求光荣的。这有点像古典哲学中的美德的概念,古希腊人和古罗马人认为男人应该追求美德,Virtue 的本意是「像男人一样」,更接近所谓的「阳刚之气」。古人的思想无疑带有一些厌女的气息(读《奥德赛》第一章就能读出来,忒勒玛科斯和佩涅洛佩说话的方式缺少对母亲的尊重,更多是「身为家里唯一的男人」的教导语气),一定要战胜敌人、证明自己的想法或许也有些野蛮,但把古典式的「战争的光荣」完全现代化为「需要赎罪的愧疚」无疑让整部作品都走向了另一个主题。

仲树和诺兰的演讲也提到了类似的主题,现代人似乎要求伟大的人为自己的伟大鞠躬道歉,否则就是傲慢。可是骄傲(Pride)难道不能被合适地给予那些值得骄傲的人吗?我们难道不能允许真正卓越的人带着一些傲慢吗?一个母亲难道不应该被允许要求长辈和旁人不要干预自己的教育之道,允许她带着些许的傲慢养育自己的孩子吗?

奥德修斯的狂傲(hubris)体现在他对神明的挑衅,最终使得手下的六百人全部丧命,只有他一人活了下来(例如,在与独眼巨人的周旋中,奥德修斯本没有透露自己的姓名,他自称「没有人」骗过了巨人,却在离开时想要巨人记住是谁戳瞎了他,喊出了自己的名字,这直接导致最后波塞冬杀死了他的船员)。奥德修斯最终还是回到了家里,与妻子佩涅洛佩和儿子忒勒玛科斯重聚。在诺兰的改编中,他为自己的傲慢付出了代价,是在赎罪(atone)之后才得以与妻子重聚;在《EPIC》音乐剧的改编中,奥德修斯接受了「自己成为了一个怪物」的事实,不惜一切代价与家人相聚——电影的主题是人不能打破 Xenia,要为自己的行为赎罪;音乐剧的主题是一个开放性的问题:什么时候一个人会变成怪物?

奥德修斯在一路上的确失去了太多,他没有理由保持理智。《EPIC》中奥德修斯不断失去的东西成为了压垮他的一根根稻草,成为了他自我蜕变的契机,甚至是一种修炼。诺兰则让一次次的失去成为奥德修斯的警钟,奥德修斯将这些灾难解读为打破了 Xenia 的后果,他需要赎罪。两种解读都很耐人寻味。

如何不把人当成猪?

📻
📻

《奥德赛》中的女巫喀耳刻把奥德赛的船员变成了猪,在音乐剧《EPIC》的改编中,喀耳刻这么做是为了保护她和她的神女(nymph)。把陌生人都当作会为自己带来灾难的威胁或许是谨慎,而在没有生存威胁的情况下不关心具体的人,把具体的个人抽象为标签式的「猪」,只能是愚蠢。

仲树与诺兰的采访在外网的反响其实相当好,这为她引来了很多关注和媒体报道。由于国外的大多数人并不认识她,所以许多人将她称作 The China Lady。对这场演讲的反应大概可以归为几类:

  • 为什么中国人对《奥德赛》的理解比美国人深刻?为什么美国人自己的采访者只会问一些蠢问题?
  • 不,仲树是在美国的教育体系里培养出来的,她代表的是美国人。
  • 不不,仲树活跃在中国的媒体环境里,中国能给她表达这些内容的环境,而美国显然没有。
  • 仲树是中国政治宣传的工具,这些问题是中国政府安排给她的。
  • 仲树翻译了阿伦特的书,而阿伦特是犹太人,所以仲树实际上是受到犹太人集团控制的。
  • 仲树的英语口音很重,讲话磕磕巴巴的。
  • 他们怎么选了长得最辱华的一个中国人去采访诺兰?
  • 她怎么穿个拖鞋就去采访了?

咳,后面三个都是中国人说的。我不谈这些具体的话语究竟为何出现,因为就像仲树自己说的,这很庸俗。它们的共同点是,他们完全没有在意仲树和诺兰这两个具体的人究竟说了什么,为什么会这么说,有什么意义,从中能获得什么。他们把仲树抽象成了「中国人对《奥德赛》的理解」「中国媒体环境的代表」「美国教育的成果」,把诺兰抽象成了「被政治正确污染的激进派」(因为包括雅典娜在内的一些角色是由黑人演员饰演的)「完全不懂古典哲学的麻瓜」等等。

公众和媒体似乎在猎巫,但究竟谁是女巫?被变成猪的人,还是那些把他们变成猪的猎巫者?


鸡头蛇尾

Last.fm 独立

📜

我在《 去营地整点 FLAC 》里提到 Last.fmParamount 旗下的服务。就在今年五月,它独立了,目前正作为独立公司运营。

Last.fm 表示用户数据、功能、API 以及背后的开发团队都没有改变,仅仅是公司的所有权转移了。

What does independence mean for users?

It means we can focus fully on building listening insights and community features for music fans. You’ll see continued, steady improvements over time.

如果你不知道 Last.fm 是什么:他们提供 Scrobble API,你可以用任何音乐播放器将播放记录发送到 Last.fm 账号,它会统计你的播放记录,生成周度、月度和年度报告,还会推荐你可能喜欢的音乐和音乐人给你。如果你使用本地播放器听音乐,或者有很多歌曲散布在不同的流媒体平台,Last.fm 可能很有用。

看到自己依赖的软件服务不再依赖大公司真是欣慰,至少我更放心把数据交给他们了。

软件工程与生成式人工智能的迷思

📜

文章列举并反驳了生成式人工智能在软件工程中应用所产生了八个迷思:

  1. 开发者的大部分时间都用来写代码。实际上有研究表明开发者只有 14% 的时间在写代码,其他时候基本都在开会、讨论方案和设计。
  2. 写代码是瓶颈。如上所述,写代码只占开发者 14% 的时间,加速写代码不会让开发速度显著提升。
  3. “AI” 生成的代码行数足以证明其影响。比尔·盖茨说:用代码行数衡量生产力,就像是用重量衡量一架飞机。
  4. “AI” 平等地帮助所有任务、所有工程师。研究发现用 “AI” 做已经被理解、很熟悉的事情,提升的效率才会更高;此外从业经验与编写高效提示词的自信心呈负相关,研究发现 “AI” 延长了那些有经验的开源开发者实现代码的时间——我猜这是因为缺乏经验的人更容易忽略 LLM 生成代码中的模糊点,也不那么吹毛求疵。
  5. “AI” 能让一个开发者干十个的活儿。如前文所述,真实的软件开发还涉及协作,这才是最大的瓶颈。效率提升与任务的种类也有关。
  6. 让 “AI” 变得好用是开发者自己的事。“AI” 工具不是天然地好用,而且缺乏必要的指导。“AI” 可能是为数不多被企业花大价钱引进,却不知道如何最大化其价值的工具——这个我也有同感,LLM Agent 的使用方式模糊多变,企业不得不引入额度限制或额外教导使用方式,才能保证员工不因为「不会上下文管理」而烧掉太多的钱。事实不是开发者不会用,而是它本身就不好用。
  7. 表现很好的 “AI” 工具会自动被采用。“AI” 不总是很容易融入现有的工作流,需要结构化的调整。
  8. 用了生成式 “AI”,大企业就可以像初创企业一样快速地发行软件。人们对初创公司的期待不同,能够容忍更多的缺陷,而人们对大企业的可靠性有更严苛的要求。

作者的论述很完整了,所以请允许我插一句与原文无关的话:我发现几乎所有谈论 LLM 的人,无论持什么态度,几乎都避开了版权议题,除了 Codeberg 。事实就是,LLM 生成的代码是从开源软件的源代码扒下来、拼凑起来的,而开源软件有授权协议,使用这些代码需要遵循协议。有的协议要求使用者自己也开放源代码,有的协议非常宽松,但也要求署名。无论原本的代码是什么协议,从 LLM 输出之后便不可知了,开源协议被洗掉了,从此任何人都可以没有代价地使用他们本应该付出代价才能使用的代码。这难道不可怕吗?为什么其他人都避开这个话题?为什么我数次在博客提出这个问题都没有人回应?是因为它太难解决了吗?

大型组织会花费大量时间在讨论无关紧要的琐事,但是真正重大的决议反而可以轻松过关这种现象。这是由于人对大议题较难有全面性的理解,故怕贸然提出异议,可能会失言;相反地,对于一些简单琐碎的小事,有相当的认识,因此意见特别多,造成组织在各事项上讨论所花费之时间,通常与事项本身的重要程度呈现反比。

—— 帕金森琐碎定理 - 维基百科

以前还有画师会公开反对自己的作品被用于训练模型,如今他们的声音似乎也消失了。是成为科技进步的牺牲品了吗?真是令人唏嘘。我对企业不抱任何「突然良心发现」的期待,但开源社区至少应该开始讨论它。

烤牛排不需要技巧

📜

烤牛排不需要技巧,开火,淋上油,把牛排放上锅煎,一段时间后翻面就好。谁都能烤牛排,但不是所有人都能烤出好吃的牛排,甚至仅仅是烤出还行的牛排都很难。开发软件也逐渐变成烤牛排一样的活动,所有人都能写软件,但不是所有人都能把软件做好。

很有意思的比喻,我大概也会用这个比喻去说服持有不同观点的其他人。不过,我并不完全赞同。和上一节所述的观点一致,烤牛排仅仅是制作菜肴、宴请宾客的一小部分活动而已。烤牛排并不涉及到「所有人都想要一块好牛排,于是一起讨论怎么烤牛排」的活动。

LLM 或许解决了怎么开火、用什么工具给牛排翻面更容易、怎么防止牛排粘锅的问题,但正如 Frederick Brooks 在《 No Silver Bullet 》里写的,这些仅仅是偶然性问题。对烤牛排而言,最难解决的是自发性问题(或者说内源性问题),怎么判断一块牛排到几分熟了、怎么知道多久翻面一次、怎么根据牛排的种类和大小调整烹饪方式等等。


漫步遐思

呃,我好像已经连续两周没有写这一节内容了,牢骚都在联邦宇宙上发完了,这之后的想法要么写成文章了,要么就根本没有出现。下班之后我就在读书,瘫在书桌前看剧,或者一边听《EPIC》一边乱逛。最近在看音乐剧《Little Shop of Horror》,或许你也会感兴趣?

不过说起来,我的精力的确变低了。读书变慢了,用来读书的时间也变少了。这其实不是因为上班太累太烦导致的,工作其实还算轻松,同事人也都很好。只是,我可能不适合在公司的环境下和人产生那么多的接触吧。尽管没有那么多接触,但我还是会感到社交压力,回到家就没什么力气了。每天只有早晨最有活力,可惜我这周有好几天睡太久了。或许会习惯这种生活的,会吧?


  1. Cast(演职人员)的意思,一般戏剧、音乐剧的演员都被称作卡司。 ↩︎

新知识分子的新庸俗

2026年8月9日 15:27

首先定义新知识分子,不是什么专有名词,只是我在找到更合适的称呼之前暂用的词。我所指的新知识分子,是经过现代教育系统培养的,相对突出的一部分人。虽然他们没有成为学者或真正顶尖的精英(当然这部分人也有他们的庸俗,只是本文不谈),但他们仍会获取知识并实践。他们对待知识没有学者的求是态度,他们的态度更加功利,只要知识听起来没有特别离经叛道,或者能够被一套话语体系说服,他们就乐意成为实践者,并将那些他们自己也说不清楚是什么的东西当作真理。

如果这个时候我开始罗列群体的名字,想必不太明智。等我先讨论完问题本身,再考虑要不要引火上身,更何况,我也或多或少属于此类群体,共享着他们的庸俗。

什么是庸俗?英文里的 Vulgarity 带有一些粗俗和野蛮的意味,为什么知识分子也可能是野蛮的?另一种定义是说庸俗者缺乏品味和教养,没有这两种东西的人如何成为知识分子?掌握知识并不会让知识分子失去 人性 ,而人性本身就包含动物性和智性。新知识分子可能在某些方面开发了自己的智性,但另一些方面仍未开化,动物性也并非退居后位。以一句网络名言为例,「不学数理化,生活处处是魔法」,说这些话的人也仅仅是受过九年义务教育和高中教育而已,或许还有本科教育——正是因为他们对智性的开发不完全,所以不能理解不是所有人都与他们受过同等教育的事实,他们更不会关注背后的历史、阶级和资源分配问题,拒绝承认世界上有除科学以外的看待世界的方式;也正是因为他们的动物性仍然猖狂,才会以试图以自己并不博学的头脑嘲笑看似比他们更粗俗的人,就像自己肌肉并不发达的人去欺负乳臭未干的小孩子一样。

不过本文要关注的,更多是新知识分子在智性层面显现出来的庸俗,也就是问:新知识分子为什么缺乏品味和教养?他们为什么庸俗且媚俗?他们眼里只有一两种可以接受的生活方式和文化符号。他们刻奇,自我感动。他们拒绝看见与他们不同,尤其是比他们更弱小的人,他们否定人的多样性,把与他们不同的人都当作堕落者,把比他们更高的人供奉为神。他们其实和大众没有什么不同,新知识分子和大众都陷入到一种抽象观念或狂热的群体情感当中,蒙蔽了双眼,看不见具体的人和具体的事,对一切都漠不关心,却又想要发表意见,显得傲慢无礼。

在进一步阐述新知识分子与大众的共同点之前,我想先用一个例子将两者区分开来。如今的大众是不读书的,就算他们真的打开书本,也很少是被求知欲驱动——他们要么处在学校或某些无法行使自由的环境下被逼迫读书,要么是听信「知识改变命运」的陈词滥调 悲壮 地阅读,要么是机械地复述甚至误读书本以便显得见多识广,又或者是附庸风雅。简单来说,他们缺乏对真理的追求。新知识分子其实也没有这层追求,但他们中的大多数地的确有着对阅读的信仰,因为他们觉得阅读的行为是高尚的、神圣的、纯粹的。

我曾经在这样一个群体里待过,他们的本职工作其实是销售课程,但他们的内部会议和精神文化都相当「积极向上」,他们会把成功完成一单交易称作「帮助到一位学生」,他们也从来不用「销售」这个字眼谈论自己的工作,以至于外人(包括从未真正融入的我)觉得他们像传销组织。他们时常在会议中分享成功学观念,他们也阅读,而且是实打实地读书,也会认真地准备读书分享。他们常常复述一些理论(有些仅仅是价值判断而非理论),比如「要有输入有输出」「时间管理很重要」「要照顾好自己」「要规律运动」等等。我还见过一个主要为职场中的年轻人生产访谈内容的博主,她偷拍了站在书店角落里读书的人并发在动态里,表示「总觉得这样的人有种神性1」,可见他们对阅读这一普通人类行为的过度神圣化——这种神圣很大程度上来自于公众对阅读的放弃,而非阅读本身有多神圣。其实这些描述就已经是我要谈的新知识分子的一个缩影了,他们在某些方面不同于大众,但又不能被归类为精英。

这类行为已经有一个名字:媚俗(Kitsch),或者说刻奇。媚俗是对已经被广泛接受和认同的艺术的毫无价值的模仿,比方说有人因为《蒙娜丽莎》备受赞誉,试图在自己的作品中也刻画一种神秘的微笑,这便是媚俗。媚俗不仅存在于艺术中,也可以是对情感的重演和复刻。一位老人看见充满年轻活力的少年在玩耍,心中感慨万分,回想起自己年轻时的样子,对生命生起一丝敬畏,这是自然的情感反应。可如果老人开始审视自己的感动,也就是说他意向的对象从具体的少年和真实的情感反应变成了抽象概念,他觉得自己感受到生命之美和青春活力并为之感动的意识活动十分圣洁,他便向媚俗靠近了。如果另一位老人听了这位老人的描述,也开始赞叹「祖国的花朵!」「民族的希望!」「八九点钟的太阳!」,并为此流泪,这便是彻彻底底的媚俗,他并没有真实地经历过前一位老人体会过的情感,更没有真的关心少年。我逐渐意识到 我对感动过敏 的原因是我受不了媚俗。

新知识分子总是试图在自己的生活中复现那些圣洁的东西,在商业活动中歌颂自己的伟大,一边听取精英的观念一边沐浴在自己的「成长进步」当中,拿着相机侵犯隐私以陶醉于自己发现神性的眼睛。

他们沉醉于阅读的神圣性时,并不关注真理和文本的深度,所以他们阅读的常常是畅销书,尤其是那些自助类的畅销书。如果仅仅是「如何进行资产配置」「如何获得健美的肌肉」一类的书,那还不足为奇。那些细碎的特征我就不列举了,我只需要给你举一个最具代表性的例子——《认知觉醒》。这本书的目录就散发着媚俗的气息,以下是其中的一些标题。

  • 人生是一场消除模糊的比赛
  • 顶级的成长竟然是“凭感觉”
  • 成长慢,是因为你不会“飞”
  • 我们生而为人就是为了成为思维舵手
  • 深度沉浸是进化双刃剑的安全剑柄
  • 深度学习,人生为数不多的好出路
  • 你没成功,可能是因为太刻苦了
  • 这个世界会奖励那些不计得失的“傻瓜”
  • 冥想:终有一天,你要解锁这条隐藏赛道
  • 运动:灵魂想要走得远,身体必须在路上

最具典型性的是第八章的章节名:「早冥读写跑,人生五件套——成本最低的成长之道」。「早冥读写跑」很像是纽约时报畅销书和英文世界很喜欢造的方法论缩写,缩写看起来很像黑话,增加了理解门槛,而门槛的增加则塑造了神秘感和学习成本,使得通俗理论看起来像是需要努力发觉才能掌握的真理。

值得玩味的是,书中其实并没有什么错误观念,没有做了就能成功的虚假承诺,和典型的成功学有很大区别。新知识分子可能自己都排斥成功学,把成功学当作像是没学数理化的人会以为是魔法的东西来嘲弄。

事实是,自助类畅销书的作者往往不是什么专家,他们只是比常人更自信,也的确有一些知识积累。他们写在书中的想法和理论有一些价值,但往往是根据自己有限的经验和认知得出的,而这些有限的经验和认知,有不少仅仅是缺乏理解的复述。如果你读过《认知觉醒》,你可能会觉得这是一本看起来连贯但实际上是由各种其他人的观念拼凑起来的书。这些观念原本是谁呢?精英的。

我在前面提到过,新知识分子眼里只有一两种可以接受的生活方式和文化符号,「早冥读写跑」何尝不是其中一种?人们在谈论早起、冥想和阅读等生活方式的时候,往往拿 Elon Musk 一天读一本书的故事和硅谷成功人士每天四五点起床、每天打坐一小时的故事作为宣传材料(当然,也会辅以一些早睡早起身体好、冥想有助于提升专注力和学习力等实际上不一定有充足的文献支撑的证据,复述别人的引经据典也是新知识分子感到高尚的行为)。新知识分子其实在模仿精英的生活方式。

模仿也是媚俗。精英可能真的阅读过文献并切身体会过这种生活方式的具体好处,并且是在多次迭代和转变之后形成了如今的生活方式,他们也对未来可能会发生的改变保持开放。新知识分子并没有具体地亲身体会或赞同某种生活方式,他们只是媚俗地模仿,并复述精英们说的话。

以上或许只是对品味的批评,庸俗难道不是缺乏品味和教养吗?缺乏教养怎么体现?新知识分子或许不会像,比如说某些右翼的美国大众那样,因为自己的信仰就剥夺所有人堕胎和与同性结婚的权利,但新知识分子的确有类似的思想倾向,区别仅仅是他们懂得一些礼貌。请想象,你在分享很具体的生活细节,可能是最近发生在生活中令你感到焦虑的事情,而某个人突然开始向你分享正念冥想有多么好,复述名人名事和「冥想能缓解焦虑,提升专注力」等陈词滥调。你可能只是想要事后再做深入了解,也可能是你尝试过,但冥想并不能解决实际问题,总之,你没有在当下立刻成为他的「同类」。他将你认定为不思进取,但嘴里还说着「我觉得有些人走得快点,有些人走得慢点吧」「没关系,你不用和我一样」的鬼话。他没有直接发起任何攻击,但你能从他的眼睛里看到,他在审判你。

他自以为是正义女神,唯一的区别是他没有被蒙住眼睛,他被自己想象的圣光刺瞎了。他没有看见你真实的烦恼,不关心具体的人所经历的具体的事件,他只关注或许是公正、或许是光荣、或许是伟大的抽象概念,沐浴圣洁的光辉当中,已经看不见真实世界。

我不觉得这是精英主义,因为精英的庸俗、傲慢和不食肉糜体现在另一层面。精英主义更多地是阶级议题,新知识分子还没有触及到政治层面,而且更加常见。


  1. 人对神性的谄媚和渴求,我已在《 何以为人 》中批评过。 ↩︎

艾尔特拉克在岣琅

2026年8月5日 23:11

汉娜·阿伦特报道了阿道夫·艾希曼在耶路撒冷因组织屠杀犹太人受审的案件,在法庭上,她发现艾希曼并不是一个冷静且思维缜密的杀手,相反,“说话时间越长,越能感受到他表达力的匮乏与思考能力的缺失,他不会站在别人的立场思考问题,之所以无法同他交流,并不是因为他说谎,而是因为他周围围绕着坚固的壁垒,屏蔽他的言辞和他人的存在,从而帮他一并拒绝着真相。”她震惊于恶的平庸性(Banality of Evil)。

艾尔特拉克和蓝色地鼠一起编写健壮的软件,在编辑器面前,他发现 if err != nil 并不是什么精巧的错误处理机制,相反,编程时间越长,越能感受到地鼠错误处理机制的缺失,他不会帮程序员抛出和拦截错误,之所以无法用条件判断以外的方式处理错误,并不是因为地鼠不想,而是因为已有的简单工具就足够提升程序的健壮性。他震惊于错误的平庸性——错误仅仅是包含 Error()接口 而已。

魂断杰威幕

众所周知,嘲讽 Java 是这个博客的传统,所以今天的文章也以这个开始。在 Java 中,处理「错误」的方式是抛出异常。这里的异常(Exception)是指程序中可以预料到的问题,比如用户本应输入整数却输入了非数字字符,而错误(Error)在 Java 中指的是那些几乎无法恢复,会使程序退出的问题。

是的,术语开始混乱起来了,详见下表。

Java Go
可预期到的、程序应处理的问题 异常(Exception) 错误(Error)
没有预料到的、难以恢复的问题 错误(Error) 崩溃(Panic1

为了方便讨论,在正式讨论 Go 语言之前,我会避免使用「错误」这个词,统一使用「异常」和「崩溃」两个词。老实说,「崩溃」这个词也不准确,Java 程序如果抛出 Error 不一定会终止程序,而 Go 程序如果 panic 也能够恢复,但姑且先这么用着吧。

Java 的异常和崩溃共用一套机制,他们甚至共用一个基类 Throwable(可抛出类)。Java 的异常和崩溃都是被抛出(throw)的,而抛出 Throwable 的函数的调用者可以接住(catch)被抛出的东西。不过 Oracle 的文档对 Error(崩溃)和 Exception(异常)两个子类的描述很有意思。

The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.

—— Exception - Java Platform SE 8

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

—— Error - Java Platform SE 8

简单来说,异常应该被接住,而崩溃不应该被接住。任何 Throwable 只要没被接住,就会使程序崩溃,提示 Unhandled Exception(未被处理的异常)——等等,你有没有发现,没有被接住的明明是 Throwable,为什么提示信息会是 Unhandled Exception?难道 Error 不也是 Throwable 吗?

是的是的,我们已经发现 Java 让异常和崩溃共享同一个机制引发的问题了。异常和崩溃都可以被抛出,但区别在于崩溃不该被接住,而异常应该被处理。没有处理的异常会导致 Unhandled Exception,而本就不应该被处理的错误也会导致 Unhandled Exception。Java 还有一个 UncaughtExceptionHandler 接口,用来处理线程里那些没有被接住的异常,我们来看看接口中包含的方法签名。

interface UncaughtExceptionHandler {
 void uncaughtException(Thread t, Throwable e);
}

啊,是的,代码里写的是 Throwable,可名字里明明是 Exception 呢。显然 Throwable 作为父类的抽象层次比子类 Exception 高,而熟悉 OOP 的程序员应该知道,父类不应该知晓子类存在,而一个为父类设计的接口竟然包含子类的名字,尽管没有直接依赖子类,但显然不是好的设计。更何况,我们已经体验到这种做法造成的迷惑和混乱了。

如果崩溃不应该被接住,那它一开始就不应该与需要被接住的异常归属于同一基类。如果子类之间的行为不同,就违反了里氏替换原则。Always glad to find design flaws in Java.

我们一直在谈抛与接,是时候来看看代码里怎么写这两个东西了。「接」想必各位都很熟悉了,try ... catch 语句在 JavaScript 等语言中也能见到。

try {
 doSomething(); // 这个函数抛出异常
} catch(IOException e) {
 // 接住 IOException 
 // 处理这个异常
} catch(Exception e) {
 // Exception 是所有异常的基类
 // 处理剩下其他类型的异常
}

如果不把 doSomething() 包裹在 try 里会怎么样呢?你会得到 Unhandled Exception 信息。为什么会这样呢?因为 doSomething 的方法签名声明了异常。

void doSomething() throws Exception;

throws 关键词用在方法签名中,表示这个方法会抛出异常,而调用者应该抓住这个异常。如果声明了多种不同类型的异常,那么调用者应该抓住所有类型的异常,或者…… 只写一条 catch(Exception e)。如果调用了可能会抛出异常的方法却没有放在 try 里面,就会得到 Unhandled Exception

真的吗?如果我就是不想要写 try ... catch 呢?

那就让 Java 教会你踢皮球,把这个错误抛给你的调用者就好了!

// 这段代码会出错
doSomething();

// 这段代码处理了异常
try {
 doSomething();
} catch(Exception e) {
 // ...
}

// 这段代码向上抛出了异常
void faireQuelqueChose() throws Exception {
 doSomething();
}

老实说,我写 Java 程序的时候就这么干过好几次,大不了最后这样写:

public static void main(String[] args) throws Exception {
 // 想怎么抛怎么抛!
}

不过异常具体是怎么被抛出的?

void doSomething() throws Exception {
 // 有不对劲的事情发生了
 // 赶紧抛出异常告诉调用者
 throw Exception("Something weird happened!")
}

嗯?这是个新的关键词吗?看起来是的,用在方法签名上告知调用者可能抛出的异常类型的关键词是 throws,而用在方法体内部抛出异常的语句是 throw。这个语句和一个标记了 throws 的方法调用在某种程度上是等价的,在方法体里写 throw Exception() 和可能会抛出异常的 doSomething(),都需要做异常处理,要么放在 try ... catch 里,要么往上抛让调用者处理。

原来 Java 程序员在一开始就要区分 throwthrows 这种细微的区别,怪不得我那个半吊子 Python 老师这么喜欢 纠结语言保留字

与此同时,Lisp 程序员:语法…… 语法太多了…… 看不懂……

No Exception

让我们总结一下,在 Java 中,异常和崩溃都要被抛出。抛出异常就等于说是要调用者(或者调用者的调用者)接住并处理这个异常,把错误处理的责任向上传递。这其实很符合逻辑,因为有一些错误就是没办法在方法内部解决,比如一个发起 HTTP 请求的方法遇到请求超时就只能上报问题。抛出错误就等于说是要程序因无法处理的错误而终止。

抛与接看起来很精巧,许多人都喜欢此类机制。如果机制是必要的,那么机制的存在使得学习和理解的成本上升,使得语言变得臃肿,或者引发机制内部的混淆与问题(比如 Error 也算 Unhandled Exception),似乎也还算能接受。可问题是,机制真的是必要的吗?我们真的需要这一层额外的抽象吗?

程序中应该被解决的错误被称作异常,抽象为 Exception;程序中无法解决的错误被称作崩溃,抽象为 Error。这两个类都是被抛出的,被称作「可抛出类」,所以抽象为 Throwable。为什么需要最高的这层抽象?这种抽象真的合理吗?它的子类难道不是两个很不同的东西吗?

思来想去,让 ErrorException 归为一个基类的原因,我只能想到「它们都是被抛出的」这一个。正方形和矩形都是「可求面积的」,那么就可以都归类为 Squarable 吗?显然不能,正方形不能继承矩形已经是里氏替换原则的一个经典案例,让正方形和矩形都归为一个基类更是荒唐。类比之后可以发现,ErrorException 也是如此,仅仅因为想让它们共用抛出机制就归为一个基类是愚蠢的。仔细看的话,Error 倒像是 Exception 的一个不需要被接住的例外(exception),而 Oracle 自己对 Exception 的描述却是「conditions that a reasonable application might want to catch」,所以到底接还是不接呢?好像不接也行,那 ExceptionError 真的有区分的必要吗?但好像不区分也不行,毕竟确实是两个概念。

是时候终止这场混乱了。

“Errors are values.”

这句话是 Rob Pike 说的。其实我还想更进一步说:Errors are conditions. 这里的 condition,既是状况,也是条件。仔细想的话,如果没有错误处理机制,我们就是会用条件分支语句处理异常情况。

func addThreeNumbers(numbers []int) string {
 if len(numbers) > 3 {
 return "too many numbers"
 } else if len(numbers) < 3 {
 return "too few numbers"
 } else {
 return strconv.Itoa(numbers[0] + numbers[1] + numbers[2])
 }
}

假设这个错误在内部处理不了,需要交给调用者处理。比方说,某个函数仅仅是读取文件,它并不知道读取文件是为了什么,也就不会知道要是文件不存在应该怎么办。假设这个函数的调用者是为了读取配置文件,而它知道,没有文件的时候就写入默认配置。函数的调用者要怎么知道函数没有获取到文件呢?最简单的做法是返回空值。

func getConfig() {
 config := readConfigFromFile(CONFIG_PATH)
 if config == "" {
 config = DEFAULT_CONFIG
 }
}

等等,你的意思是说返回值就能让调用者知道函数内部发生了需要处理的错误,是吗?Hmmmm…… 我好奇为什么还会有人发明一整套混乱的机制欸?

你可能会说:“这不能强迫开发者解决问题,他们完全可以忽略错误而不做处理,而 Java 强制通过异常抛出机制要求程序员解决所有的异常,无论如何都是要通过 try ... catch 处理的,或者还有全局的错误拦截。总之 Java 的设计让程序员更容易编写健壮的软件!”

Hmmmm…… 我好奇这段代码有什么效果欸?

public static void main(String[] args) throws Exception {
 // 想怎么抛怎么抛!
}

这么做也没有任何后果,可以把错误一直往上抛,直到程序的入口方法。反正 Java 里的方法定义已经要写一堆 public static 了,多写几个 throws Exception 也不会觉得有什么区别吧?

你可能还会说:“异常抛出机制为错误处理提供了统一的解决方案,所有异常都基于 Exception 类,彼此还相互继承,更是通过 OOP 的机制让错误处理变得更灵活!”

Hmmmm…… 原来是 OOP 啊。说起来,Go 由于缺少继承,的确不算 OOP 语言,但我们从不少 Java 程序员也喜欢读的四人帮《 设计模式 》中得知,组合优先于继承,并且应该针对接口编程而不是针对实现编程。好的设计模式中很少见到继承,因为不方便复用,方便复用的是接口。

啊对,Go 语言也有统一的错误处理范式,但没有引入任何新机制,因为错误的全部定义只有三行代码:

type error interface {
 Error() string
}

所有实现了 Error() 函数的结构体都是错误值。于是我们回到 Rob Pike 说的「Errors are values」。如何让调用者明确知道函数内部发生了错误,而不只是恰好返回了空值呢?——再返回一个专门的错误值。

func getConfig() {
 config, err := readConfigFromFile(CONFIG_PATH)
 if err != nil {
 config = DEFAULT_CONFIG
 fmt.Println("config: using defaults because of error", err)
 }
}
func readConfigFromFile(path string) Config {
 if body, ok := readFile(string); ok {
 if config, ok := parseBody(body); ok {
 return config, nil
 } else {
 return nil, &errorString{"error parsing config file body"}
 }
 } else {
 return nil, &errorString{"error reading file"}
 }
}

这是通用的解决方案,尤其是用于有多种类型错误的情况。假设一个操作只有成功与否,而我们不关心失败的具体原因,只关心成功没有,那可不可以让错误处理变得更简单呢?当然,返回布尔值就好了,看到上面的 ok 了吗?它接收到的是函数的第二个返回值,在这个例子里,这第二个返回值不是 error 类型,而是 bool 类型。

如果没有接收函数的所有返回值,编译器就会报错:assignment mismatch。如果接受了错误值却没有使用,编译器就会报错:declared and not used。这两个错误信息都不是为错误处理而生的,而是本来就存在。Go 语言没有发明 unhandled exception 这种东西来要求程序员处理错误,编译器甚至不知道「错误」这个概念,它只知道有函数返回值没有被赋值给变量,只知道有变量被声明了但没有使用,而这对 Go 语言严格的编译器来说都是不可接受的。当然,和 Java 一样,这也有规避方案,比方说把错误赋值给 _,编译器就会认为这个变量不需要使用,通过检查。不管有没有专门的错误处理机制,错误都很容易被程序员忽略,不做处理,这本来就不应该是由编程语言本身强加给程序员的事情。就像 Rust 实际上并不是一门安全的语言,无论怎么样,就连 Cloudflare 的高级工程师也会滥用 unwrap()。Java 把 throw 做得这么难处理,还用 try ... catch 把代码的嵌套层次变得越来越深,反而是多此一举。

用接口统一错误类型,用函数的多返回值上报错误,用严格的编译器类型检查预防错误不被处理,这整条链路都没有引入任何新的机制。Go 的错误处理范式,竟然如此平庸和无趣。

“Don’t Panic”

Don’t use panic for normal error handling. Use error and multiple return values.

—— Go Wiki: Go Code Review Comments

Go 官方给出的常见代码审查评论中,有一条是 Don’t panic,意思是说不要用 panic() 做错误处理。这个内置函数会终止当前函数栈的执行,并且会让当前函数本身也成为一个 panic(),也终止当前函数的调用者的执行,panic 就会这样沿着函数调用栈一直往上,直到程序入口函数,最后终止程序,除非中途调用了 recover() 恢复程序。

「沿着函数调用栈往上」,这听起来很像 Java 的抛出机制。Java 的 Throwable 一旦被抛出,就会一直往上直到被 try ... catch 接住,如果没有被接住就会使程序终止——和 panic() 一样。你发现问题了吗?Exception 也是 Throwable,倘若异常没有被处理,也会使程序崩溃。这种情况却是 Go 想要避免的,所以 Go 建议程序员使用多返回值和 error 类型,而不是使用会使程序终止的 panic()

这再次印证了「异常情况」和「会使程序崩溃的情况」本来就是两个完全不同的东西,被 Java 硬塞到了 Throwable 里,就为了让它们共用本就不合理的抛出机制。异常情况仅仅是程序的一个分支条件,这个条件可以用布尔值或者 error 类型的值表示。如果没有好好处理这种分支条件,那么程序顶多出现意想不到的行为,不应该崩溃。Java 的设计却使得本应该成为分支条件的语句,直接终止了整个程序运行。

会使程序终止的情况不应该是可以预料的分支条件,比方说死锁、内存溢出和数组越界。Java 说一个合理的应用程序不应该抓住崩溃(Error),而 Go 提供了恢复崩溃(panic)的方法。

defer func() {
 if r := recover(); r != nil {
 fmt.Println("Recovered ", r)
 }
}()

panic("Panicking!")

defer 也不是专门为了处理异常而发明的新语法,它所做的仅仅是推迟代码执行。推迟到当前函数的最后,当然也在 panic() 之后(不过在 return 之前)。由于 defer 语句在 panic() 之后执行,就可以在 defer 中执行 recover(),使得 recover()panic() 之后执行,这样就恢复了崩溃。

由于 panic() 会自己沿着函数调用栈往上,所以不需要在相关链路中的每一个函数签名中都写上 throws Exception 之类的东西,只要在某个环节执行 recover() 就好了。

不过无论怎么说,panic() 都不如 error 常用,也不鼓励使用。

最后

质疑 if err != nil,理解 if err != nil,成为 if err != nil 的拥护者,然后狠狠嘲讽 Java。这便是艾尔特拉克魂断杰威幕之后身处岣琅的心路历程,回见了。


  1. Panic: 另译「运行时恐慌」 ↩︎

大脑充血 Vol.92

2026年8月3日 07:57

我发现自己遇到「不测风云」,也就是那些难以预料的烦心事时,已经能够做到表面平静地发疯十分钟,然后该干嘛干嘛。这周济南总是莫名其妙地开始刮大风、下暴雨,虽然讨厌袜子被打湿的感觉,但包里常备着折叠伞,也还算从容不迫。

其实还有些家人和工作上的大小事,不过我现在更在乎自己刚刚从 MUJI 买的粘毛刷好不好用,今天有没有吃燕麦喝酸奶,明天早上应该读《第一个人》还是《现象学导论》。印象中不少「重要人物」常常把自己的生活外包给别人,以腾出时间关注那些更具商业价值的事情。而我呢?如果我每天回到家不能亲自用吸尘器把地板拖得一尘不染,听见灶台点燃的热水沸腾的声音,我都不知道自己怎么活下去。


吾仅悉落

MY SKYSCRAPER music cover

MY SKYSCRAPER 专辑

Nirosta Steel

复古,有种…… 既老派又年轻的奇妙听感。小众中透露着沉稳,但又没有很沉稳,甚至有些随意,大概可以这样总结。

最喜欢《LOST IN MUSIC》《MY NAME IS NIGHT》《FRESH FEELING》《SPECIAL WEAKNESS》,大概就是整张专辑篇中后段的部分。

作者如今六十多岁了,专辑是沉淀了几十年的音乐成果;还是酷儿,有几首歌据说是写给男友的(尽管我没听出来是哪几首);貌似还特别喜欢中国文化,挺好的,但是在音乐最后听到中式的敲锣打鼓和京剧唱腔实在是有些令人措手不及。

卡夫卡的手绘画作,一个坐在椅子上的男人,弯着腰,手臂放在膝盖上,这只手臂撑着头。这幅画的名字叫作 Der Denker(思考者)

Walking to a different town

And talking–what did I learn?

Talking to a different crowd

And walking–what did I learn?

What did I do today?

What did I do today?

What did I do today?


I was holding on for something special

Holding on for something new

Holding on!

——《SPECIAL WEAKNESS》


心重如毛

基因编辑技术走到哪一步了?

📻

最近这几天最大的新闻应该是接受基因编辑治疗的女孩意外离世的事件了。相关报道发表在《Science》上,大致死因是用于作为基因编辑载体的病毒引发了女孩的免疫反应,导致肝肾功能出现问题,女孩不久后就离世了。

一名六岁罕见病患儿,在上海接受前沿基因编辑试验后不幸离世,家属持续维权,要求厘清责任。消息从周五晚间开始在自媒体圈层扩散,随后逐步被官方媒体转载。截至录音当下,仅有上海交大医学院发布声明称启动调查,完整官方结论尚未公布。

《万物生长 FM》是讨论生命科学相关的播客,这档节目的主持人大多都是相关行业的从业者或研究人员,其中的一些对基因编辑技术也有涉猎。播客先是梳理和复盘了事件全貌,然后讨论了基因编辑疗法的现状,表达了他们对此事的看法。其中的不少观点,就目前来看,要比主流媒体更具专业性。

他们批评不少自媒体将此事件与贺建奎事件并列而谈的行为(据说这次事件的主要负责人仇子龙教授,曾经还声讨过贺建奎),因为贺建奎事件完全是私下进行的,他对婴儿进行基因编辑完全没有获得任何审批,再者,基因编辑的胚胎在发育之后,还会将被编辑过后的基因遗传给下一代,是更复杂的伦理问题。本次仇子龙事件,是经过必要的审批的,也就是“合规”的。所以,除了指责仇子龙本人,还应该反思的是整个审批的流程体系。据说基因编辑的药物在猴子身上试验时就出现了较强的负面反应,而相关机构在动物试验的毒理报告出来之前就通过了临床试验的审批,这很匪夷所思。仇子龙教授本人,除了接受女孩家长的研究资助之外,还收取了茅台酒、手机和平板电脑等赠礼。这两个关键点才是更值得关注的,整件事情都非常诡异且令人不安。

无论怎么说,目前所能看到的就只有《Science》的报道和各种媒体的转发和添油加醋,此外就是上海交通大学发布的「正在调查中」的声明。要更好地了解事件全貌,还要等待更多更可靠的信息来源发布报道。

有史以来最强的厄尔尼诺现象

📜

如果读者在高中学过地理的话,应该对厄尔尼诺现象这个词有所耳闻。每隔数年,太平洋东部的海水水温就会异常升高,导致一系列气候异常。温度上升往往和气压降低同时出现,东太平洋气压降低,空气就会从西太平洋往东太平洋流动,使得西太平洋沿岸地区(东南亚、澳大利亚)发生干旱,东太平洋沿岸(南美洲)发生暴雨,农业和渔业都会受到影响。

原本自然发生的气候异常,在今年创下新高。模型1的预测显示今年的厄尔尼诺现象会使得相关地区的海平面温度升高 3.6°C,而此前我们从未观测到超过 2.75°C 的厄尔尼诺,这次厄尔尼诺的最低数值 2.8°C 也超过了历史最高。

不同预测模型的结果有差异,但也都预告了前所未有的高增长。根据当前的 实时数据 来看,原本的 3.6°C 甚至上升到了 3.7°C,预计会在今年十一月达到这个峰值,而厄尔尼诺对全球气温的影响一般要到第二年才会完全显现,届时 2027 年可能超过 2024 年,成为有史以来最热的一年。


鸡头蛇尾

Tangled 是真正的 GitHub 替代品吗?

🐑

上周分享了 Codeberg 的政策变更 ,顺带明确了这样一个事实:Codeberg e.V. 作为非营利组织的使命是为自由软件提供基础设施,Codeberg 不是自由版本的 GitHub。如果用户在任何层面与 Codeberg 的理念不符,例如不以自由软件协议分发源代码、大量使用 LLM 写代码或者为 LLM 生态做贡献、存放实验性代码和不面向公众的个人项目,那就不应该待在这个平台。

政策变更其实也炸出了一部分不理解这个事实的用户,甚至有 一篇说自己后悔迁移到 Codeberg 的文章 登上 Hacker News 热门。好笑的是,文章里这个作者说 Codeberg 完全没有考虑过倾听用户的意见,然而,Codeberg e.V. 的决策相当透明且民主,我作为关心 Codeberg 和自由软件的捐献者,也早在一两个月前就了解到了相关议程。倘若这些用户真的在乎 Codeberg,那么他们完全有时间加入 Codeberg e.V. 去发表意见和投票。倘若在看到变更通知和首页横幅之前都没有想过去了解 Codeberg 的决策过程,那还能说些什么呢?

话说回来,事实就是,不是所有人都应该迁移到 Codeberg,而且绝对不应该把 Codeberg 当作 GitHub 的替代品。那真正合适的替代品是什么呢? GitLab 看起来只是另一个商业平台,只是不受微软控制,产品质量也没有像 GitHub 那样急速下滑,自托管的成本比较高,而官方实例的免费额度有限,更适合企业用户。 Forgejo 是更轻量的选择,不过自托管的不便之处,一是维护成本和门槛高,二是实例之间难以互通和协作。

还有一个小众且观点鲜明的选择是 Sourcehut ,不跟踪用户、没有广告、没有 JavaScript、没有 AI 功能、没有非必要的登录墙,你可以自托管也可以付费使用他们的服务。最低档是 4 美元一个月,但如果你因为经济状况和各种原因无法支付,也可以申请免费使用。整体而言,Sourcehut 的社区感似乎不强,界面对不少用户来说可能算得上丑(但完全可用)。功能上,Sourcehut 使用 git send-email 进行协作,交流、讨论和发送代码补丁都可以通过电子邮件进行,余下的 CI、工单管理和 Wiki 也都有,功能不会差。我怎么觉得我应该去用 Sourcehut?

和 Sourcehut 一样也还在 Alpha 阶段的另一个 Git Forge 是来自芬兰的 Tangled又名《魔发奇缘》,我在 第 79 期周刊 分享过,可以说是目前为止在体验上接近甚至超越 GitHub,也更适合大多数人的选择。他们的 Stacked Pull Requests (也就是把一个大的 PR 拆分成更容易审查的小块,但是可以一起合并),就在最近还被 GitHub 抄去用了 。他们对兼容 Git 的版本控制系统 Jujutsu 有直接的支持。几个月前他们还推出了 担保 功能,可以为那些能够交付可用且容易审查的代码的人(而不是滥用 LLM 的人)进行担保,受信任等级更高的人的贡献可以被优先处理。Tangled 的用户界面也很有特色,Commit 历史一般是被藏起来的,而他们把历史和当前的文件目录放在了一起。

不过更重要的是,Tangled 是去中心化的,不仅可以自托管,而且自托管之后能够通过 AT Protocol 与其他实例(Tangled 称作 Knot,也就是「结」)交互,比 Sourcehut 的电子邮件体验要丝滑不少。Tangled 背后的是 Tangled Labs Oy,一家芬兰的小公司,是位于欧盟的商业实体,无论如何都要比位于美国的微软更值得信任。

不过 Tangled 会不会在以后逐渐屎化(enshitificate),还有待观察。我注册了 账号 ,或许可以当备用 Forge 或者镜像,先用着看看。

Gopher SLIPs into a pile of parentheses

🐭

这个周末想写个小的 Web 应用,大概就是支持 Fever API 的无头 RSS 聚合器。我发现自己几乎从来不会打开 Miniflux 的 Web 界面,那为什么不能只用本地客户端,而服务端只起到同步的作用呢?就着这个想法,我准备开写。自己的项目当然是要用 Lisp 写的,不过用哪门 Lisp 呢?Clojure 我已经比较熟悉了,但跑在 JVM 上最小也得占两三百 MB,看着实在是有些不是滋味。于是选择了刚学没多久的 Common Lisp,然后就被 ASDF 和 Quicklisp 整得头疼——你这包到底应该怎么导?你这项目脚手架到底应该怎么搭?

我的确找到了不少合适的工具和教程,但整体上文档都很少,略感不适。在找 Web 框架的时候突然发现了 Caveman2 这个不洁的东西。

(defparameter *app* (make-instance '<app>))

@route GET "/"
(defun index ()
 (render #P"index.tmpl"))

What is this non-Lisp Java-like ungodly syntax!?

怒,骆驼被压垮了,遂离开 Common Lisp,开始物色新语言。毕竟是自己的个人项目,技术选型可以随意些,要不是非常想用 Lisp 写,我早就用 Go 糊出来了。我开始寻找宿主语言是 Go 的 Lisp,意外发现了今年年初才发布的新鲜语言 SLIP,名字的意思是 SLIce Processing。其中 Slice 是 Go 语言的数据结构,其实就是一个可变数组。SLIP 是基于 Slice 的,而不是 cons2,据说是出于性能考量。似乎好多 Lisp 都是用宿主语言的数组或者列表类型糊出来的,所以用 Slice 也并非不能接受,至少语法没有那么不洁

SLIP 基本上就是对 Common Lisp 的实现,仅仅有一些不兼容的地方。SLIP 还提供了插件系统,可以用 Go 拓展 SLIP 这门语言本身,自定义内置函数。我本来还想写一个插件简单包装下 Gin ,结果作者告诉我其实有内置 HTTP Server 。我还问了作者有没有考虑写更详细的文档,结果对方点醒了我,在 Common Lisp 里,用 REPL 探索语言才是最好的方式,apropos 函数可以搜索包和符号,而 describe 可以描述一个符号(函数)的作用。

嗯?所以搞了半天你不还是在写 Common Lisp 吗?甚至换了一个更难导包的实现。

于是这个周末就这么过去了,嗯……

杀死 Cookie 横幅

ℹ️

欧盟有关 GDPR 的立法运动,网站声称 Cookie 横幅(也就是访问某些网站时会弹出来的,请求你同意 Cookie 协议的横幅)往往被设计得极具误导性,用户要真正表达自己的隐私偏好会很困难,很有可能就一不小心同意了网站跟踪你。同意所有 Cookies 很简单,接受必要的 Cookies 很简单(但谁知道「必要」究竟是指什么呢?),但拒绝 Cookies 往往不那么容易。这个网站提供的解决方案是,让用户在操作系统或浏览器层面设置全局的隐私偏好,与网站建立连接时,就像 HTTP Headers 里的 Accept-Language 字段一样,自动告知网站访客的隐私偏好。

这的确是更好的解决方案,如果默认设置了严格的隐私偏好,网站就不太可能通过误导性的 Cookie 横幅来引诱用户同意交出部分权利。

另外,点击网站的 Take Action 按钮,会来到一个引导页面,告诉用户采取行动的方式就是联系当地的欧洲议会代表人,让他们支持 Article 88b GDPR3,之后还列出了各个国家和各个地区的联系人信息。不得不说,真是方便。


漫步遐思

This is a thoughtless week.


  1. 这里的模型不是指 AI 大语言模型,而是基于数理统计的计算模型。 ↩︎

  2. 关于 cons 是什么,我在《 Just A Common Lisper 》里写过。 ↩︎

  3. Article 可能是「法案」的意思?但对应的单词还有一个是 Bill。我不清楚法律术语,所以就用原文了。 ↩︎

川渝人在山东吃到没有辣味的麻辣香锅和红油水饺之后产生的哲学思考

2026年7月31日 23:53

我没有什么乡愁,胃也是,就算不辣也有很多好吃的。只是那些听起来辣,看起来也辣的东西,吃起来竟然不辣,令我震惊,故作此文。

来济南的第一天,到了酒店之后还有些晕,毕竟刚下飞机就又在车上坐了接近一个小时,不想出门,于是选择点外卖。纠结过后,看上一家麻辣香锅,习惯性地点了微辣,打开饭盒品尝,没有辣味。第二天,我不服,我就想吃有辣味的麻辣香锅,于是点了中辣,打开饭盒品尝,仍然没有辣味。几天后我仍对此耿耿于怀,于是点了最辣的麻辣香锅,打开饭盒品尝…… 这在重庆应该叫作微微辣吧?

虽然不辣,但第二天却拉肚子了,而且是那种吃了辣之后伴随着灼烧感的拉(我是不是应该在文章开头标注这篇文章不适合吃饭时阅读?但真的有人会一边吃饭一边读东西吗?)。真是神奇,原来辣度和能让人拉肚子的程度是分开的吗?

一星期过去,我已经习惯了济南餐馆里标注了辣味的食物实际上并没有辣味这个事实,于是毫无戒备地点了一份中辣的重庆鸡公煲…… 被辣出了感动的泪水。之后又去吃海底捞,点了海底捞双料真香锅,很香很辣很正宗。就这样,我对济南的信心又回来了。然后我站起身,路过好几张桌子…… 不是,你们为什么用开水烫火锅?愤恨中想要告诫对方,吃火锅要是没有小孩,点鸳鸯锅是会被开除重庆户籍的,点清水锅更是要被诛九族。愤恨之余想起来,哎呀,自己是自闭症来着,不敢和人讲话。(想起来的不应该是你不在重庆这个事实吗?!)

周末就这么过去了,到了周一,在公司楼下吃饺子。蘸料区有醋、酱油、耗油和辣椒,最佳的配比当然是 0 份醋、0 份酱油、0 份耗油和 10 倍的辣椒。可惜,这个辣椒恐怕当饮料喝也没有辣味。我把饺子在蘸碟里搅来搅去,让它沾满辣椒油,入口之后,没有辣味,不过挺香的。

所以,在济南,香辣等于香,麻辣等于麻,甜辣等于甜。原来这就是合并同类项啊!嗯?等等,难道说应该叫奥卡姆剃刀?你究竟在谈数学还是哲学?!不是在谈吃饭吗?

好啦好啦,玩笑结束,我知道部分地区的饮食文化更推崇品尝食材本身的味道而不是搭配调料,或者就算加调料也更偏好酱香而不是辣味。不过我依旧有一个疑问:那不辣的麻辣香锅和不辣的红油水饺究竟有什么受众呢?

总所周知红烧牛肉面对于一部分人来说其实是辣的,麻辣香锅想必同理。

—— HPCesia

啊,原来是这样吗?我确实一直不太喜欢红烧牛肉面,因为味道平平淡淡,毫无特色,据说一些不能吃辣的人会觉得红烧牛肉面太辣呢。所以,真正的问题是:那红烧牛肉面究竟有什么受众?

以后出门吃饭要随身携带老干妈。等着我把超意兴的把子肉涂满辣子吧。

回到正题上来。人们常说辣是痛觉而不是味觉,事实也的确如此。

辣是化学物质(譬如辣椒素、姜酮等)刺激细胞,在大脑中形成了类似于灼烧的微量刺激的感觉,不是由味蕾所感受到的味觉,而是疼痛。

—— 辣 - 维基百科

据说人体在受到能忍受的疼痛时,会释放内啡肽(endorphin)。这种快乐激素的化学结构和作用类似止痛药吗啡(morphine),但因为在人体内分泌,所以叫作内源性吗啡(endogenous morphine),英语单词缩合之后变成 endorphin。为了缓解疼痛,所以产生止痛物质,继而使疼痛本身带来了快感——无论是辣椒素的灼烧感还是…… 别的什么…… 奇怪的东西

吃辣带来的快感很大程度上是因疼痛带来的内啡肽造成的,那么不辣的「辣味」食物呢?对于已经能够耐受相当程度的辣的川渝人来说,刺激感较弱的辣貌似并不能带来足够的内啡肽分泌,即便如此,沾了辣油的食物和不沾辣油的食物仍然有区别。

我想这是因为辣往往伴随着其他的风味出现,我们可以从火锅底料的配方看出些许端倪。诚然,火锅底料大部分都是油和辣椒(毫不夸张地讲,熬火锅底料的锅里一眼看去就只有辣椒,而且还是那种铁桶一样大的锅……),但仍有不少其他佐料。我简单搜索之后,发现了一种牛油火锅底料的 专利信息 ,以下是摘要,可以用作参考:

本发明公开了一种牛油火锅底料配方,以牛油、郫县豆瓣、白酒、醪糟、糍粑辣椒、生姜、大蒜、花椒、豆豉、冰糖、辣椒面、大葱为主料,以白蔻、草果、丁香、砂仁、孜然、桂皮、甘草、栀子、老蔻、甘松、陈皮、香茅草、八角、香叶、千里香、小茴香、香草为香料,该发明口味浓郁厚重,可以满足食客对火锅口味的需求,并适合在潮湿阴冷地区或者冬季食用。

所以,除了辣椒和油之外,口味浓郁的辣味食物往往还带有不少香料的风味。我想那些不够辣的辣椒油吃起来还有些滋味的原因也是如此。

作为对比,有一种很直接的辣,或许被称作「工业辣」,基本没有什么香味,只有并不愉悦的痛感(我猜成分可能是很纯的辣椒素)。去一些食品安全状况存疑的餐馆用餐时偶尔会有这种辣的体验,吃火鸡面也会有。是的,我觉得火鸡面太辣了,而且辣得很没有品味。YouTube 上有一个频道做的一系列节目貌似很有名,叫作 Hot Ones ,节目大概是请一些明星一边吃辣度不同的鸡翅,一边回答问题。尽管我没有尝试过视频里出现过的那种辣酱,但印象中那种食物也很「工业」,而且貌似就是被设计来给人「体验」的,故意把辣度拉得很夸张。

人们好像很喜欢看别人被辣得说不出话,展现出某种人皆有之的弱点,或是反过来,故意吃辣度很高的食物,来证明自己的承受能力很强。等等,我刚刚是不是写过「没小孩在,点鸳鸯锅会被开除重庆户籍」?原来我也被拉进这场无聊的游戏里了啊,真是可怕。

我想起之前树老师在《 真男人为什么应该练翘臀? 》里批评过网上那些信口开河说自己每天早上起来都能做一百个俯卧撑的男人。那些男人大多数(或许是全部)都在撒谎,因为现实中肌肉力量远超常人的美军陆战队退役大哥,做连续不间断标准俯卧撑,连续最多只能做七十多个。说自己早上爬起来就能做一百个的,要么是在撒谎,要么是动作不标准,要么是分组做的(分组是很好的练习方式,但要放到网上显摆的话,谁知道你组间休息多长,每组多少个?)。

我印象中大多数人的俯卧撑姿势都有问题,却从没反思过动作问题,哪怕不标准也要算进去。事实上刚健身的人,连续不间断做十个标准俯卧撑就已经很难了,刚开始更是只能推墙或者做跪姿俯卧撑。我不知道这种愚蠢的雄竞和男性身体焦虑从何而来,但似乎在小学体育课上就有显现。学不高身不正的体育老师用成人的标准(而且是有健身基础的成人标准)傲慢地要求小孩子做标准的连续俯卧撑,仿佛做不了才是不正常的,而且完全忽视个体的身体情况差异。于是大家都开始无知地卷了起来,无知是说大家以为把手臂弯一弯就算俯卧撑,能弯二十下是正常的,而上肢力量更弱的人,可以跪着做(我印象非常深刻,我小学体育课上的跪姿俯卧撑也不是标准的跪姿,反而更像瑜伽动作里的猫牛式);卷是说所有人都开始以一个不存在的人为标准,这个人能连着做几十个俯卧撑不喘气,每天只吃水煮菜,早上起来会洗冷水澡…… 树老师说那些即便骗人也要说自己能做 100 个俯卧撑,并且说男人都能做 100 个这种话给别人制造焦虑、让自己看起来很「男人」的男人,实际上在供奉某个不存在的「赛博关公」。我想说,在互联网之前,这种供奉就已经持续很久了。

加缪在他的自传体小说《第一个人》里用两个词描述他的外婆——无知且严厉。她会非常自信地用歪理教育孩子,严厉不是为了做正确的事,而是为了让孩子顺应她无知的头脑所能认知到的规律。这类人还很好面子,加缪写道,他小时候很不喜欢去电影院,因为那时候的无声电影要看字幕,她的外婆不认字,要外孙读字幕,却还要在开始前大声说「我今天没戴眼镜,你给我读字幕」,只为了不让人发现她是文盲。结果是,这个可怜的小男孩一边被旁边恼怒的观众责骂太吵,一边因为念得太慢被外婆催促,有好几次都被赶出了电影院,而男孩又因为浪费了家里的钱继续感到自责。

这类人不少见,有可能许多人的父母长辈都是如此。被无知且严厉的养育者带大的孩子,若不能通过读书或社交自救,恐怕就会成为焦虑和不存在的标准的奴隶,而他们也会继续想办法大声说一些谎话,来证明自己不是文盲、弱鸡和不能吃辣的人,也不知道是证明给谁看。

不识字也可以有智慧,苏格拉底甚至反对书写,认为那会让人们放弃记忆;识字的人也不见得能品味文学和阅读哲学,甚至不能读懂别人写的字,不能用文字去做有意义的事情。「弱鸡」仅仅是缺少力量,并不是没有勇气;只有力量却不懂得克制使用力量的人,并不具有美德。不辣也有很多美食;仅仅是提高辣度,而忽略了风味,把吃辣当作某种游戏,那实在是有些不尊重食物。

就这样,我得去看看什么样的辣椒酱比较便携了。

❌
❌