<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Justin Huang (简体中文)</title><description>Justin Huang blog：一个个人技术博客，用因果链阅读 AI 论文、Agent 系统和软件基础设施。</description><link>https://justinhuangai.github.io/</link><item><title>《Externalization in LLM Agents》：LLM Agent 的认知外部化</title><link>https://justinhuangai.github.io/zh-CN/posts/externalization-in-llm-agents-memory-skills-protocols-and-harness-engineering/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/externalization-in-llm-agents-memory-skills-protocols-and-harness-engineering/</guid><description>这篇综述把 memory、skills、protocols 和 harness engineering 统一到一个视角里：Agent 的进步越来越像是在模型外部重写任务，而不只是让模型权重更强。</description><pubDate>Wed, 13 May 2026 08:00:00 GMT</pubDate><content:encoded>很多关于 Agent 的讨论，会自然滑向模型排行榜。

哪个模型更聪明，哪个推理更强，哪个上下文窗口更长，这些当然重要。但这篇 [《Externalization in LLM Agents: A Unified Review of Memory, Skills, Protocols and Harness Engineering》](/papers/2604.08224v1.pdf) 提醒我们，Agent 的另一条主线不在模型内部，而在模型外部。

它问的不是“模型还能学会什么”，而是：

**哪些原本压在模型身上的认知负担，可以被搬到外部结构里？**

这就是论文用 `externalization` 这个词的原因。它不是给模型外挂几个组件，也不是把工具列表塞进 prompt。它更像人类使用纸、地图、日历和计算机：外部物件没有让大脑本身突然变大，但它改变了任务的形状。记忆变成识别，心算变成操作符号，临场发挥变成遵循流程。

LLM Agent 也是这样。

![论文 Figure 1：LLM Agent 设计中的外部化主线](/images/posts/externalization-in-llm-agents/figure-1-externalization-overview.webp)

*Figure 1, from Zhou et al., [arXiv:2604.08224](https://arxiv.org/abs/2604.08224), [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/), format-converted for web display, content unchanged.*

## 一篇综述真正有价值的地方

这篇论文不是提出一个新模型，也不是发布一个新 benchmark。它是一篇综述，价值在于给正在发生的工程迁移命名。

过去我们习惯把能力理解成权重里的东西：更大参数量、更好预训练、更强对齐。后来，能力越来越多地通过上下文被组织起来：prompt、few-shot 示例、RAG、chain-of-thought、工具说明。再往后，真正复杂的 Agent 系统开始依赖更厚的运行时：文件系统、记忆库、技能库、工具协议、沙箱、审批、日志、评估器、子 Agent 编排。

论文把这条线概括成：

**weights → context → harness**

![LLM Agent 的认知外部化地图](/images/posts/externalization-in-llm-agents/cognitive-externalization-map.webp)

这张图表达的是一条工程主线：能力不是只放在模型权重里。早期系统更多依赖 weights；上下文工程成熟之后，任务可以在 context 里被临时组织；到了 Agent，长期可靠性必须落到 harness。Memory 保存状态，skills 沉淀流程，protocols 规范工具和 Agent 之间的交互，governance 处理权限、审计和失败恢复。

因此，Agent 的关键问题不只是模型有多强，而是外部运行环境能不能把任务稳定地表示、约束和恢复。

## Memory：把时间搬到模型外面

最容易理解的外部化是 memory。

如果只依赖上下文窗口，Agent 每次运行都像带着一段临时工作记忆。窗口再长，也会遇到选择问题：什么该放进去，什么该忘掉，什么信息只是噪音。更关键的是，上下文本身是一次性的。任务结束之后，如果没有外部状态，很多经验就消失了。

Memory 系统把这个问题从“模型能不能凭空回忆”改写成“模型能不能在需要时识别并使用被检索出来的状态”。这和论文借用的 cognitive artifacts 视角是一致的：购物清单不增强生物记忆，它把回忆任务变成识别任务。

对 Agent 来说，外部 memory 可以保存用户偏好、项目约定、历史决策、失败轨迹、领域事实。真正困难的地方不只是存，而是选择。一个好的 memory 系统必须决定哪些状态值得沉淀，什么时候检索，检索多少，如何压缩，如何避免旧状态污染新判断。

所以 memory 不是“更长上下文”的低配替代，而是时间维度上的基础设施。

## Skills：把过程搬到模型外面

第二个外部化是 skills。

这里的 skill 不是“模型会用某个工具”，而是一段可复用的过程性知识。它可能包含操作步骤、判断启发式、边界条件、失败恢复方式和安全约束。一个成熟的 skill 告诉 Agent：这类任务通常怎么做，先看什么，什么时候停止，什么时候升级给人。

这和工具调用不是同一层抽象。工具提供动作，protocol 规定动作如何被描述和调用，skill 则封装“如何把这些动作组织成一件事”。

![论文 Figure 5：技能作为外部化的过程性知识](/images/posts/externalization-in-llm-agents/figure-5-skills-lifecycle.webp)

*Figure 5, from Zhou et al., [arXiv:2604.08224](https://arxiv.org/abs/2604.08224), [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/), reproduced unchanged.*

这点对软件工程 Agent 尤其明显。

一个模型可能知道如何修改代码、运行测试、读错误日志、写 PR 描述。但稳定完成任务，通常还需要很多本地流程：先读哪些文件，如何保护用户改动，什么时候用 `rg`，什么时候跑全量测试，如何处理已有暂存区，哪些命令不能乱用。

如果这些都留给模型每次现场推导，Agent 的行为就容易发生漂移。把它们外部化成 skill，任务就不再是“临场发明流程”，而是“选择并执行已验证的流程”。

这也是 skills 在 Agent 工程里容易被低估的原因。它不炫，但它直接降低方差。

## Protocols：把交互秩序搬到模型外面

第三个外部化是 protocols。

没有 protocol 的 Agent 交互，本质上是自由文本协商。模型说自己要调用工具，工具返回一段文本，另一个 Agent 再猜这段文本代表什么。小 demo 里这可以工作，生产系统里会很脆。

Protocol 的作用，是把含糊交互变成机器可读的契约。工具如何发现，参数如何声明，权限如何表达，错误如何返回，Agent 之间如何委托，用户审批如何进入流程，这些都不应该全靠 prompt 约定。

这类外部化的价值不只是互操作性。它还提供治理入口。只要交互是结构化的，系统就能做校验、审计、限权、回放和监控。自由文本很灵活，但也很难治理。

## Harness：不是包装器，而是认知环境

论文最重要的收束，是把 memory、skills 和 protocols 放进 harness engineering 里。

Harness 不是薄薄一层 wrapper。它是 Agent 真正运行的认知环境：控制循环、上下文预算、工具权限、沙箱隔离、人工审批、日志观察、失败恢复、子 Agent 编排，都在这里发生。

![论文 Figure 3：Harnessed LLM Agent 的外部化架构](/images/posts/externalization-in-llm-agents/figure-3-harnessed-agent-architecture.webp)

*Figure 3, from Zhou et al., [arXiv:2604.08224](https://arxiv.org/abs/2604.08224), [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/), reproduced unchanged.*

这里有一个容易误解的地方：harness 不是在 memory、skills、protocols 之外再加一个模块。它更像承载这些外部化模块的运行时。Memory 提供状态，skills 提供过程，protocols 提供交互结构，harness 决定它们什么时候被调用、如何互相影响、怎样被约束。

这也解释了为什么现代 Agent 看起来越来越像小型操作系统。它们不只是“一个模型加一串工具”，而是有资源、有权限、有生命周期、有日志、有策略的执行环境。

## 能力边界在移动

这篇论文最有用的地方，是它改变了“能力在哪里”的问题。

如果能力只在权重里，那么改进 Agent 的主要方式就是换模型、微调模型、继续训练模型。如果能力也在上下文里，那么 prompt、检索和工具说明就成为工程主战场。如果能力进一步进入 harness，那么系统设计本身就变成能力的一部分。

这不是在贬低模型。相反，越强的模型，越值得被放进更好的外部结构里。因为强模型擅长综合、判断、泛化，但它仍然不天然擅长稳定记忆、重复流程、权限治理、长期状态和跨系统协调。

外部化不是作弊。它是工程上承认边界，然后重写任务。

一个好的 Agent 系统，不是让模型每次都从零开始“想办法”。它会把可沉淀的状态沉淀成 memory，把可复用的过程沉淀成 skills，把可治理的交互沉淀成 protocols，再用 harness 把它们组织成可运行的环境。

## 也要警惕外部化的代价

论文的框架很漂亮，但不能把外部化理解成无成本扩展。

Memory 会带来陈旧状态、隐私边界和检索污染。Skills 会带来过时流程、错误复用和上下文竞争。Protocols 会带来标准碎片化和接口锁定。Harness 会带来系统复杂度：越多审批、日志、沙箱、策略和子流程，越需要工程纪律。

还有一个评估问题：如果 Agent 的能力分布在模型和外部基础设施之间，我们到底在评测什么？同一个模型，放进不同 harness，表现可能完全不同。那排行榜里的“模型能力”和产品里的“Agent 能力”就不再是同一件事。

这正是论文的位置。它不是说外部化解决一切，而是把问题放到正确的位置：Agent 的可靠性来自模型和环境的共同设计。

## 实用判断

这篇综述的钉子句是：**Agent 进步的一条主线，是把认知负担从模型权重迁移到可检查、可复用、可治理的外部结构。**

这不是说模型不重要。模型仍然决定理解、规划和生成的上限。但当 Agent 真正进入长期任务，可靠性不可能只靠一次次现场推理撑住。状态要能保存，流程要能复用，工具调用要有协议，权限和失败要能被 harness 接住。

外部化的本质，是把任务从“让模型每次想明白”改成“让系统把可沉淀的东西留下来”。Memory 保存时间，skills 保存过程，protocols 保存交互秩序，governance 保存边界，harness 把它们组织成运行环境。

下次看一个 Agent 系统，不要只问它用了哪个模型。先问它把什么认知负担搬到了外部，哪些外部结构可检查、可更新、可回滚。这个问题比模型名更接近系统的真实能力。</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>agents</category><category>externalization</category><category>harness-engineering</category><category>memory</category><category>skills</category><category>protocols</category><category>LLM</category></item><item><title>《AutoCodeBench》：当大语言模型自动生成代码基准</title><link>https://justinhuangai.github.io/zh-CN/posts/autocodebench-large-language-models-are-automatic-code-benchmark-generators/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/autocodebench-large-language-models-are-automatic-code-benchmark-generators/</guid><description>AutoCodeBench 论文里，为什么 Elixir 这一语言列值得注意，以及它如何引出自动生成多语言代码 benchmark 的难度等价讨论</description><pubDate>Sun, 19 Apr 2026 08:19:00 GMT</pubDate><content:encoded>代码评测的第一性问题不是谁排第一，而是题目从哪里来。只要 benchmark 由模型生成、翻译、过滤，评测就不再只是“模型答题”，也是“模型出题系统”的产物。

[《AutoCodeBench》](/papers/2508.09101v1.pdf) 真正值得拆的地方，是自动生成多语言代码 benchmark 时，语言之间的“难”是否等价。Elixir 这一列之所以值得看，不是因为它能证明大模型最擅长 Elixir，而是因为它暴露了评测生成流程里的结构性变量。

## 先说这张排名表在讲什么

先看论文里的原始表格：

![AutoCodeBench 论文 Table 4：不同模型在 20 种编程语言上的 Pass@1](/images/posts/autocodebench-table-4.webp)

在原文 Table 4 里，每一行是一家参评大模型，每一列是一种编程语言。

交叉处那个数字表示：把这一语言的题交给这个模型，只看它第一次交出的答案，最后有多少题能一次通过测试。论文把这个指标记作 Pass@1。这里的 `@1` 可以直接理解成“只给一次机会”。

最顶上有一行特殊的，叫 Current Upper Bound。它不属于任何一个模型，而是把所有模型解对的题目取并集之后算出来的通过率。只要某道题被任意一个参评模型做对，它就进入这个上界。论文表注明确写了这一点。

Elixir 共 198 道题，Current Upper Bound 是 97.5。粗略折算，约 193 道 Elixir 题至少被一个模型解对。同一行里，Kotlin 是 89.5，C# 是 88.4，Racket 是 88.3，Python 是 63.3。

这里有个很容易踩的坑：不能把 Elixir 97.5 和 Python 63.3 直接写成“Elixir 比 Python 更适合大模型”。

这不是一个严谨结论。

因为不同语言的题目来源不同，生成方式不同，难度过滤效果也可能不同。直接拿语言列的数字去推导语言本体优劣，会把 benchmark 结果解释过头。

真正值得追问的是另一个问题：

**同一批模型、同一套评测流程里，为什么 Elixir 这一语言列会稳定处在高位？**

## 这不是单个模型的偶然

先排除一种解释：也许只是 Current Upper Bound 的统计特性。多个模型取并集，数字自然会变高。

那就看单个模型行。

Claude Opus 4 推理模式下，Elixir 是 80.3，C# 是 74.9，Kotlin 是 72.5。Claude Sonnet 4 非推理模式下，Elixir 是 74.2，C# 是 72.9，Kotlin 是 72.0。DeepSeek-R1-0528 推理模式下，Python 是 38.8，Elixir 是 77.3。

注意，这里比较的不是“Claude 和 Elixir”。Claude 是模型，Elixir 是语言，二者不在同一维度。

这里比较的是：

**同一个模型，横向看它在不同编程语言上的表现。**

从 Table 4 横着看，Elixir 经常出现在高分位置。它不是某一个模型的单点波动，而是多行模型结果里反复出现的语言列现象。

这把问题从总榜拉到了语言列。

## 题目是怎么来的

要理解这个现象，得回到 AutoCodeBench 的生成流程。

论文提出了一套叫 AutoCodeGen 的自动化流程：从 Stack-Edu 里的真实代码片段抽取种子，让模型把它演化成可独立运行的完整解法；再生成公开和私有测试输入；把解法和测试输入丢进多语言沙箱执行，得到真实输出；最后根据解法和测试函数反向生成题目描述。

这个设计的关键是：测试输出不是模型凭空写出来的，而是代码实际跑出来的。

这也是 AutoCodeBench 比“让模型直接编题编测试”更可信的地方。模型可以负责生成候选，沙箱负责给出事实。

但 20 种语言并不是完全用同一种方式生成的。

论文写得很清楚：Python、C++、Shell、Java、JavaScript、Go 这 6 种语言直接使用完整 AutoCodeGen 流程；其余 14 种语言虽然理论上也可以走同样流程，但由于数据资源有限、多样性不足，论文采用了近似语言翻译。Table 3 里，Elixir 的翻译路径是 Python → Elixir。

这就埋下了一个关键变量：

**Elixir 题目很可能不是 Elixir 生态里的原生任务，而是从 Python 任务翻译过去的 Elixir 任务。**

翻译本身不是问题。

问题在于：翻译之后，题目难度是否还能和其他语言保持等价？

## 难度过滤器的语言盲区

AutoCodeGen 有一个难度控制机制。

它用 DeepSeek-Coder-V2-Lite 对每道题采样 10 次，并用沙箱验证答案。10 次全对的题会被丢掉，因为论文认为这类题太简单，没有评测价值。

这个逻辑在热门语言上更容易成立。

如果过滤模型熟悉 Python、Java、C++，那么它能 10 次做对的题，大概率确实偏简单；它做不对的题，也更可能有一定难度。

但到了 Elixir 这样的低资源语言，情况会变复杂。

论文 Section 3.3 专门比较了热门语言和低资源语言。热门语言组选的是 Python、C++、Java、C#；低资源语言组选的是 Racket、Shell、Elixir、TypeScript。结果是，热门语言组里模型平均 Pass@1 差距较小，范围是 50.4 到 53.8；低资源语言组里模型差距更大，范围是 45.3 到 62.0。

论文随后给出了一个解释线索：顶级模型在低资源语言上表现显著更好，可能是因为 DeepSeek-Coder-V2-Lite 在低资源语言上的能力有限，难以过滤掉简单题。

这句话非常关键。

它没有直接说“Elixir 高分就是因为题简单”，但它提供了一个很合理的机制解释：

**用于过滤简单题的模型，在低资源语言上可能不够强。因此，一些对强模型来说并不难的题，没有被同等强度地过滤掉。**

这比“大模型最擅长 Elixir”解释得更稳。

Elixir 的高分，可能不是单纯来自模型对 Elixir 的语言掌握，而是同时受到三件事影响：

第一，题目来自 Python → Elixir 的近似翻译。

第二，难度过滤器在低资源语言上可能不如在热门语言上可靠。

第三，顶级模型和过滤模型之间的能力差，在低资源语言上被放大了。

这三件事叠在一起，就能解释为什么 Elixir 这一列会特别亮眼。

## Lite 版本里还有一个侧面证据

AutoCodeBench 还有一个精简版，叫 AutoCodeBench-Lite。

Lite 的构造方式提供了一个侧面证据：论文先收集所有模型的解题结果，再按每道题被多少模型解出来排序；通过模型少于 2 个的题先丢掉，然后从剩下的题里按通过次数从低到高选择约 1,500 道。论文这样做，是为了保留那些“至少有模型能解，但仍有区分度”的题，让模型之间的差距更清晰。

在完整 AutoCodeBench 里，Elixir 有 198 道题。

到了 Lite 版本，Elixir 只剩 61 道。这个数量在 20 种语言里属于最低的一组，少于它的只有 JavaScript 的 57 和 PHP 的 60。

这个数字不能单独证明“Elixir 题都简单”。

因为题目没有进入 Lite，可能有两种原因：一种是太少模型能解，被第一步丢掉；另一种是太多模型能解，在按通过次数从低到高截取时排到了后面。

但结合 Elixir 的 Current Upper Bound 高达 97.5，以及很多单模型行里 Elixir 得分靠前，这个 Lite 数量变化至少提供了一个侧面信号：

**完整 Elixir 题集的难度分布，很可能和其他语言不完全等价。**

这才是重点。

不是说 Elixir 题“有问题”，也不是说 AutoCodeBench“不可靠”。

而是说：当 benchmark 通过自动生成、近似翻译和模型过滤构造时，不同语言之间的难度分布会被生成流程影响。

## 还有一层更隐蔽的偏差

论文 Section 4.2 还讨论了模型偏差问题。

整个生成流程大量使用 DeepSeek 系列模型：DeepSeek-V3-0324 负责代码生成，DeepSeek-R1-0528 负责 Critic 质量审核。论文也承认，这可能对 DeepSeek 家族模型产生有利偏差。为了缓解这一点，作者在简单题过滤阶段使用 DeepSeek-Coder-V2-Lite，试图形成一种“push-and-pull”的平衡机制。

更关键的是，论文的量化分析显示，偏差并不是一句“谁出题谁占便宜”就能解释完。

在 Table 7 的阶段性结果里，Critic 过滤阶段确实让 DeepSeek-R1-0528 得到提升，但 o3 和 Gemini 2.5 Pro 的提升幅度反而比 DeepSeek-V3-0324 更大。论文最后的判断也很克制：自动流程可能给 DeepSeek 家族带来有利偏差，但影响较小。

这部分和 Elixir 不是同一个问题，但它们指向同一个核心：

**自动化 benchmark 不是中立机器。**

谁来生成题目，谁来审核题目，谁来过滤简单题，哪些语言是原生生成，哪些语言是翻译生成，都会在数据里留下痕迹。

最后，这些痕迹会安静地进入排行榜。

## Elixir 97.5 到底说明了什么

它说明的不是“大模型最擅长 Elixir”。

也不应该被写成“AutoCodeBench 有问题”。

更准确的说法是：

**AutoCodeBench 暴露了自动生成多语言 benchmark 的一个关键难题：语言之间的难度等价，远比表面上更难。**

表面上看，20 种语言分布均衡，每种语言大约 200 道题。

但分布均衡不等于难度等价。

一道 Python 任务翻译成 Elixir 后，是否仍然代表 Elixir 生态里的真实任务？

过滤模型在 Python 上能筛掉简单题，在 Elixir 上是否也能筛得同样干净？

沙箱能验证代码输出，但题目描述、语言习惯、接口设计、标准库使用方式，是否也同样自然、同样公平？

这些问题都不会直接出现在总分里。

但 Elixir 这一列让它们浮了出来。

## 这篇论文真正值得写的地方

AutoCodeBench 的价值不只是给模型排了个名。

它更重要的贡献，是把 benchmark 生产方式往前推了一步：让模型生成题目，让沙箱验证输出，让 Critic 检查质量，再用采样过滤控制难度。论文也明确说，AutoCodeBench 包含 3,920 道题、37,777 个测试用例，覆盖 20 种编程语言，目标是构造更困难、更实用、更多语言覆盖的代码生成评测集。

这条路肯定会继续走下去。

因为代码评测有一个天然优势：代码可以执行。只要任务定义得足够清楚，就可以通过测试用例验证答案。相比写作、开放问答、复杂推理，代码任务更适合形成“生成—执行—验证—过滤”的闭环。

但 Elixir 这一列提醒我们：自动化不是免费的。

模型生成题目，会带入模型熟悉的问题结构。

模型翻译题目，会带入源语言的任务形状。

模型过滤难度，会受自己的语言能力限制。

沙箱可以验证执行结果，却不能保证题目在每种语言生态里都一样自然、一样公平。

所以，未来的代码评测不只要问哪个模型得分最高，还要问：

- 这套题是怎么来的？
- 哪些语言是原生生成，哪些语言是翻译生成？
- 难度过滤器在不同语言上的能力是否一致？
- 人工验证覆盖了哪些语言，又没有覆盖哪些语言？

论文里的人类验证只覆盖了 Python、C++、Java、JavaScript、Go、Shell 六种语言，准确率为 87.6%；Elixir 并不在这组人工验证语言里。这个细节也说明，对翻译生成语言的质量和难度分布，还需要更细的后续验证。

## 最后的结论

AutoCodeBench 的钉子句是：**自动生成 benchmark 的难点不是出题，而是让不同语言里的“难”保持等价。**

Elixir 在表里显眼，不能直接推出“大模型最擅长 Elixir”。benchmark 的生成、翻译、过滤、人类验证覆盖范围都会影响分数。尤其当题目由 Python 扩展到其他语言时，语言列的分数就混合了模型能力、翻译路径和难度筛选能力。

但正因为这些变量都摆在台面上，Elixir 这一列才值得继续追。它提醒我们，代码评测不是中立容器。任务从哪里来、怎样被改写、由谁过滤，都会进入最终分数。

下次看代码 benchmark，不要只问哪个模型 Pass@1 更高。先问题目供应链：原生生成还是翻译生成，难度过滤器在哪些语言上可靠，人工验证覆盖了哪些语言。一个 benchmark 只有先解释清楚题目如何存在，分数才有解释权。</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>autocodebench</category><category>elixir</category><category>code-generation</category><category>benchmark</category><category>LLM</category></item><item><title>《Attention Residuals》：让残差连接也注意力化</title><link>https://justinhuangai.github.io/zh-CN/posts/attention-residuals/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/attention-residuals/</guid><description>Kimi 团队 Attention Residuals 技术报告：为什么残差连接也该“注意力化”，以及 Full AttnRes / Block AttnRes 如何把这个想法做成可训练、可部署的系统</description><pubDate>Thu, 19 Mar 2026 08:49:27 GMT</pubDate><content:encoded>残差连接长期被当成训练稳定性的管道：让梯度更容易穿过深层网络，让旧表示不要丢得太快。但如果把模型看成信息系统，残差还有更深的问题：深度方向的信息到底该怎么路由？

[《Attention Residuals》](/papers/2603.15031v1.pdf) 的核心判断，是把残差连接从稳定训练的管道，重新定义成跨层信息路由。它问的不是“再加一个模块能涨几分”，而是为什么序列维度已经有注意力，深度维度还在用固定加法。

## 0. 先认几个词

如果你完全没有机器学习背景，可以顺着这篇报告真正关心的问题，按下面这个顺序先建立一个直觉：

- `Transformer`：今天大多数大模型的基础架构。你可以先把它理解成一台一层一层处理信息的机器。
- `隐状态（hidden state）`：模型在某一层里的内部中间表示。可以粗略理解成“模型此刻脑子里的临时笔记”。
- `残差连接（residual connection）`：层和层之间的一条“保留旧内容”的通道。它会先把上一层的内容留住，再把这一层新算出来的东西加上去。
- `残差项（residual）`：更接近“这一层新补上去的增量”，也就是上面那条残差连接里新增的那一部分。
- `注意力（attention）`：从很多信息里，挑出“当前最该看哪一部分”的机制。这个词你可以先记成“有选择地看重点”。
- `PreNorm`：在进入一层之前，先把数值尺度调匀，再做后续计算。可以把它想成“先把音量调到合适，再继续混音”。

## 1. 一句话说清楚

这份技术报告提出了一个问题：

**既然 Transformer 已经用注意力机制取代了“时间维度上的递归”，为什么大模型在“深度维度上的信息聚合”还停留在固定加法？**

现代大语言模型（LLM）几乎都在用一种很常见的层结构：先做 PreNorm，再走残差连接。直白地说，就是先把数值尺度调匀，再把这一层新算出来的结果加回原输入。大家熟悉它的一个功能，是让训练过程更稳定，深层网络不那么容易失控。但作者提醒我们，残差连接其实还有另一个同样重要、却长期被忽视的角色：

**它定义了信息怎样沿着深度被汇总。**

如果下面的式子看不熟，不用卡住，直接看后面的“翻译成人话”就够了。

标准残差的规则很简单：

$$
h_l = h_{l-1} + f_{l-1}(h_{l-1})
$$

这里可以直接把两部分拆开看：

- $h_{l-1}$：旧内容，也就是上一层已经有的表示
- $f_{l-1}(h_{l-1})$：这一层新算出来的增量，更接近“残差”这个词本身

而把这两部分重新加在一起的整条做法，才更准确地叫“残差连接”。

把这个递推式展开，你会得到：

$$
h_l = h_1 + \sum_{i=1}^{l-1} f_i(h_i)
$$

翻译成人话就是：第 $l$ 层看到的输入，本质上是“嵌入表示（embedding）加上前面所有层输出的统一加总”。每一层的权重都是 1，没有选择，没有抑制，没有“这一步我更该看第 3 层还是第 17 层”的机制。

AttnRes 的核心思想只有一句话：

**把残差连接从固定加法，改成沿深度做一次 softmax 注意力。**

## 2. 旧残差到底哪里有问题？

这份技术报告最重要的地方，不在于它提出了一个新公式，而在于它把一个大家已经习惯了的东西重新问题化了。

标准残差长期被视为“训练稳定性工具”。只要能让梯度过得去，它就算完成任务了。但从信息流角度看，这条路径其实非常粗糙。

想象你在写一份持续迭代的文档。每一轮修改，你都不是“挑出最相关的旧版本内容再整合”，而是把之前所有版本一股脑全文追加到文档末尾。第 20 轮的时候，前 3 轮的重要洞察当然还在，但它们已经淹没在越来越厚的堆叠里了。

PreNorm 的问题就在这儿。报告引用了 SiameseNorm 的观察，并进一步强调：在 PreNorm 下，`hidden state` 的量级会随着深度近似按 $O(L)$ 增长。这里的隐状态，说白了就是模型每一层里的那份“内部笔记”。结果就是：

- 越往后的层，看到的是一个越来越膨胀的“历史总和”
- 早期层的信息虽然没有消失，但会被不断稀释
- 后面层如果还想“发出声音”，就被迫输出更大的量级

这篇技术报告把这个现象叫 `PreNorm dilution`。这是一个非常准确的命名。不是梯度断了，不是模型炸了，而是每一层的相对贡献被越来越稀。

报告里有一条值得抓住的潜台词：我们在序列维度上早就不满足于“所有过去词元（token）一视同仁”了，所以才有了注意力机制；那为什么到了深度维度，却还能接受“所有过去层统一权重相加”？

## 3. AttnRes 到底做了什么

AttnRes 的形式很干净。第 $l$ 层不再机械地接收“前面所有层输出的总和”，而是对这些历史表示做一次加权选择：

$$
h_l = \sum_{i=0}^{l-1} \alpha_{i \to l} \cdot v_i
$$

其中权重 $\alpha_{i \to l}$ 来自一层 softmax。你可以先把 softmax 理解成“把一组分数压成一组权重，而且所有权重加起来等于 1”，这样模型才能明确表达“更该看谁、少看谁”：

$$
\alpha_{i \to l} = \operatorname{softmax}\left(w_l^T \operatorname{RMSNorm}(k_i)\right)
$$

如果你没接触过注意力机制，还有一个最省力的理解方式：

- `查询（query）`：当前这一层现在想找什么
- `键（key）`：每一层历史信息各自贴着什么“索引标签”
- `值（value）`：最后真正被取回来、参与汇总的内容

这里有三个关键设计。

第一，**查询不是当前隐状态现算出来的，而是每层一个可学习的伪查询向量 $w_l$。**  
这有点反直觉。我们平时看到注意力机制，会自然以为查询必须来自当前输入。但作者故意把查询设计成层级参数，而不是按词元动态生成的向量。这样做的好处是：同一个块里的多个查询可以提前批量算，后面基础设施优化才有空间做。

第二，**键和值直接来自前面层的输出。**  
也就是说，真正带来“输入相关性”的不是查询，而是各层当前样本上的表示本身。不同样本经过前面层后得到的键不一样，所以最后的深度注意力依然是输入相关的。

第三，**键前面加了 RMSNorm。**  
这是个很关键的小设计。因为如果不做归一化，量级大的层会天然在点积里占便宜，你得到的就不是“谁更相关”，而更像“谁声音更大”。报告正文也明确强调了这一点。

```python showLanguage
import torch
from torch import nn


def attention_residual(
    sources: list[torch.Tensor],
    pseudo_query: torch.Tensor,
    norm: nn.RMSNorm,
) -&gt; torch.Tensor:
    keys = torch.stack([norm(source) for source in sources], dim=0)
    values = torch.stack(sources, dim=0)

    logits = keys @ pseudo_query
    weights = torch.softmax(logits, dim=0)
    return (weights.unsqueeze(-1) * values).sum(dim=0)
```

这个式子看上去像是“把注意力机制用在残差连接上”。但更准确的说法是：

**它把残差连接从“固定的累加器”改成了“可选择的深度检索器”。**

## 4. 这份报告给了想法，也给了工程

**一句话结论：这篇报告提出了 Full AttnRes，它把这个想法推进成了一套可训练、可部署、算得清账的工程方案。**

Full AttnRes 让每一层都看到前面所有层，理论上很好理解，实际上也不算太贵。因为网络深度 $L$ 通常远小于序列长度 $T$，所以作者说，单纯算术量 $O(L^2 d)$ 并不是最可怕的问题。

真正的问题出现在大训练里：

- 激活重计算（activation recomputation）会把本来可以丢掉的中间层输出重新变成必须保存的对象
- 流水线并行（pipeline parallelism）会让这些跨层表示需要跨阶段传输
- 一旦每层都要看所有前层，通信和缓存压力会快速上去

所以他们又提出了 **Block AttnRes**。

做法是把 $L$ 层切成 $N$ 个块。块内部先用普通求和攒成一个块级表示，跨块再做注意力。这样一来：

- Full AttnRes：看的是所有历史层
- Block AttnRes：看的是所有历史块的摘要，再加当前块的部分和

本质上是用“摘要级跨层注意力”换取可扩展性。

作者没有只停在“分块所以省内存”这个层面，而是把系统层的账也算清楚了：

- 训练阶段用 **跨阶段缓存（cross-stage caching）**，避免流水线里重复传历史块
- 推理阶段用 **两阶段计算（two-phase computation）**
- 第一阶段并行算块间注意力（inter-block attention）
- 第二阶段顺序算块内回看（intra-block lookback），再用在线 softmax 合并

从附录和 `table/memory_access.tex` 里能看到最硬核的一组数字。按报告给的典型设定：

- 标准残差连接：每层残差机制 I/O 是 `3d`
- naive Full AttnRes：`130d`
- 优化后的 Full AttnRes：`24d`
- Block AttnRes：`5.5d`
- mHC：`34d`

这组数字特别说明问题。Block AttnRes 不是“便宜到跟标准残差连接一样”，但它已经从“明显不现实”降到了“工程上值得试”。而且报告实测给出的代价也不大：

- 训练端实际耗时开销小于 4%
- 推理端时延开销小于 2%

这也是它像一篇系统级技术报告的原因。很多论文的问题在于“想法是新的，账是糊的”；这篇在账本上反而做得很用力。

## 5. 实验最该看什么

**在主实验中，AttnRes 在缩放趋势、训练动力学和下游能力上都给出了方向一致的信号。**

### 5.1 缩放定律：不是偶然赢一把

作者先做了五个模型规模的缩放定律实验，对比 Baseline、Full AttnRes 和 Block AttnRes。

拟合出来的曲线是：

- Baseline：$1.891 \times C^{-0.057}$
- Block AttnRes：$1.870 \times C^{-0.058}$
- Full AttnRes：$1.865 \times C^{-0.057}$

这三条曲线最重要的信息不是“斜率差了多少”，而是：

**AttnRes 在整个算力区间里都持续更低。**

报告给了一个很容易传播的结论：在 `5.6 PFLOP/s-days` 这个预算点，Block AttnRes 的损失相当于 baseline 多花 `1.25x` 算力才能达到的水平。

换句话说，这不是“在某个模型大小上碰巧调对了”，而是有比较稳定的规模收益。

### 5.2 大模型主实验：不是玩具规模

主实验不是小模型上的玩具规模基准实验，而是基于 Kimi Linear 的一个大配置：

- `48B 总参数 / 3B 激活参数`
- 27 个 Transformer 块，也就是 54 层
- 8-of-256 路由专家 + 1 个共享专家
- 预训练 `1.4T tokens`

这说明作者不是只在“小模型上做漂亮曲线”，而是真把这个残差改造塞进了一个大训练配方里。

### 5.3 训练动态中，输出量级不再失控

训练动态那张图片，Baseline 的输出量级会随着深度一路涨上去。训练动态图里给的数值非常夸张：从前面几个块的 `0.04`、`0.06`、`0.10`，一直涨到后面几个块的 `10.47`、`12.15`。这就是 `PreNorm dilution` 的视觉化版本。

Block AttnRes 则完全不是这条曲线。它在块边界形成一种周期性重置，量级大致在 `0.21` 到 `1.91` 之间波动，没有出现一路失控上扬。

这非常重要，因为它说明 AttnRes 不是只在最后 benchmark 上“多拿了几分”，而是真正在训练动力学层面改变了表示如何沿深度堆积。

### 5.4 下游任务：提升最明显的是推理和代码

预训练后，AttnRes 在报告列出的全部评测上都不差于 baseline，几个最亮眼的点包括：

- MMLU：`73.5 -&gt; 74.6`
- GPQA-Diamond：`36.9 -&gt; 44.4`
- Math：`53.5 -&gt; 57.1`
- HumanEval：`59.1 -&gt; 62.2`
- C-Eval：`79.6 -&gt; 82.5`

GPQA、Math、HumanEval 这种多步推理或程序生成任务涨幅更大，这一点直接对应了报告的机制假设。报告作者的解释是：如果后层能更有选择地回收前层表示，那么需要组合式推理的任务会更受益。这个解释是说得通的。

因为复杂推理最怕的不是“信息不存在”，而是“信息在网络很深的地方被埋住了”。

## 6. 消融实验告诉了我们什么

**消融实验的关键结论，不是“连得更密就更强”，而是“沿深度做输入相关的选择性聚合”这件事本身在起作用。**

这份报告的消融做得不错，因为它不只是证明“有用”，还试图证明“为什么有用”。

几个关键结论：

- **DenseFormer 1.767，几乎和 baseline 1.766 一样。**  
  这说明“能访问所有前层”本身还不够，关键在于权重是不是输入相关的。

- **mHC 到了 1.747，已经明显变好。**  
  这说明深度维度上的动态混合确实有效。

- **Full AttnRes 到了 1.737。**  
  它比 baseline、DenseFormer、mHC 都更低，说明显式的沿深度 softmax 注意力是一条更强的路线。

- **SWA（只看最近窗口）只有 1.764。**  
  这很有价值。它说明 AttnRes 的收益不只是“多看最近几层”，而是“能选择性地看更远的层”。

- **块大小从 2、4、8 变化时，损失都在 1.746 左右。**  
  这就是为什么作者最后固定大约 8 个块。不是拍脑袋，而是工程和效果之间一个相当好的平衡点。

- **输入相关查询版本做到 1.731，比 Full AttnRes 还好。**  
  这个结果说明当前报告里的伪查询设计并不是性能上限，而是一个为基础设施优化让路的折中。也就是说，作者不是不知道更强的写法，而是主动选了更容易扩展的写法。

这正是这份报告的工程取舍。你从正文、消融和系统设计里能更清楚地看到他们的真实取舍：不是盲目追求最低 loss，而是在追求“足够强，同时真能训起来”。

## 7. 怎么看这份报告

第一，这份报告最重要的，不是它发明了一个新模块，而是它把残差连接从“训练稳定性工具”重新提升成了“信息路由机制”。

这个视角一旦建立起来，很多东西都会被重新理解。残差不再只是梯度高速通道，它还是深度聚合规则。你会开始追问：

- 每一层到底能不能选择性地访问前层？
- 深度维度上有没有“注意力汇聚陷阱”（attention sink）？
- 旧的残差变体本质上是不是沿深度维度的线性注意力？

而这正是报告讨论部分有价值的地方。作者把一堆残差变体统一进了一个 `depth mixing matrix` 的视角里，进一步指出：

**很多已有方法，本质上都像是在深度维度上做线性注意力；AttnRes 做的是沿深度维度的 softmax 注意力。**

这个说法很大胆，但也很有启发性。它等于是在说：Transformer 当年把序列维度从递归推进到了 softmax 注意力；AttnRes 试图把深度维度也推进一步。

第二，这篇技术报告的气质很像“先把问题提对，再把系统做顺”。它没有执着于把每个部件都做到最花哨。比如查询故意做成按层设定的参数，而不是按词元动态生成的向量，性能上未必绝对最强，但它给了批量计算、两阶段计算、流水线缓存一个成立的基础。很多时候，一篇能落地的技术报告，靠的不是最激进的局部设计，而是整体约束下的取舍。

第三，这份报告中的这句话：

**Why is depth-wise aggregation still fixed while everything else has become adaptive?（为什么沿深度的聚合仍然是固定的，而其他部分都已经变得自适应了？）**

这句话抓住了问题的中心。

## 8. 这份报告的边界

第一，它目前是 **技术报告 / arXiv 预印本**，不是已经过同行评审的会议论文。写这类文章时，最稳妥的态度不是“它已经证明了未来”，而是“它提出了一个很强的视角，并给出了一套有工程可行性的实现”。

第二，它的大规模结果主要建立在 Kimi Linear 这条架构线上：MoE、KDA/MLA 混合注意力、Moonlight / DeepSeek-V3 风格训练配方。虽然这不削弱结果本身，但也意味着我们还不能自动把结论外推到所有纯稠密的仅解码器 Transformer。

第三，报告自己也承认：Full AttnRes 其实更强，Block AttnRes 是今天硬件约束下的工程解。未来如果显存、带宽、互连再往前走，或者更高效的深度注意力变体出现，今天这版 Block 设计很可能不是终点。

## 9. 这篇报告改变了什么问题

Attention Residuals 的钉子句是：**残差连接不只是稳定训练的管道，也是跨层信息路由规则。**

这份报告把一个被默认接受的结构重新问题化了。标准 PreNorm 残差把历史层近似等权累加，训练很稳，但信息路由很粗。AttnRes 追问：既然序列维度已经用 attention 做选择，深度维度为什么还只能固定相加？

这个角度比“又多一个模块”更有价值。它把 residual connection 从梯度高速路拉回到信息系统里：每一层到底该访问哪些前层，哪些表示该被保留，哪些该被压低。Block AttnRes 的意义，也不只是省显存，而是在效果、带宽、缓存和流水线之间给出一个能训练的大规模折中。

下次看架构创新，不要只看 benchmark 涨了几分。先问它重新定义了哪条信息通路。真正重要的结构改动，往往不是添加能力，而是改变能力流动的路径。

---

**延伸阅读**

- [《Sequence to Sequence Learning with Neural Networks》](/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/)（使用神经网络进行序列到序列学习） — 编码器-解码器范式的确立
- [《Neural Machine Translation by Jointly Learning to Align and Translate》](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)（通过联合学习对齐与翻译实现神经机器翻译） — 注意力机制的起源
- [《Attention Is All You Need》](/zh-CN/posts/attention-is-all-you-need/)（注意力就是你所需要的全部） — 注意力成为主角，Transformer 的诞生
- [《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》](/zh-CN/posts/bert/)（BERT：用于语言理解的深度双向 Transformer 预训练） — 预训练范式的确立
- [《Scaling Laws for Neural Language Models》](/zh-CN/posts/scaling-laws-for-neural-language-models/)（神经语言模型的缩放定律） — 规模的数学
- [《Language Models are Few-Shot Learners》](/zh-CN/posts/language-models-are-few-shot-learners/)（语言模型是少样本学习者） — 更大的模型，更善于从上下文中诱发能力
- [《Training Compute-Optimal Large Language Models》](/zh-CN/posts/training-compute-optimal-large-language-models/)（训练计算最优的大语言模型） — 怎样花算力最划算</content:encoded><category>Technical Report Reading</category><category>technical-report-reading</category><category>residual-connections</category><category>transformer</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>《Training Compute-Optimal Large Language Models》：Chinchilla 改变了什么</title><link>https://justinhuangai.github.io/zh-CN/posts/training-compute-optimal-large-language-models/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/training-compute-optimal-large-language-models/</guid><description>Chinchilla 论文：为什么 2022 年的大模型全都「喂少了」，以及算力预算到底该怎么分配，附真实 Python 核心代码</description><pubDate>Wed, 11 Mar 2026 08:58:04 GMT</pubDate><content:encoded>Chinchilla 要纠正的不是“规模是否有效”，而是固定算力下规模该怎么分。2022 年很多大模型把钱主要花在参数上，却没有给同等增长的数据；结果不是模型太小，而是模型没有被充分训练。

[《Training Compute-Optimal Large Language Models》](/papers/2203.15556v1.pdf) 的关键结论不是反对大模型，而是指出参数和数据必须共同吃满算力预算。更强不等于只把模型做大，更强来自参数、数据和训练步数之间的正确比例。

## 0. 先认几个词

这篇论文会频繁谈到“该把预算花在哪”，所以先把下面几个词认熟，会更容易抓住重点：

- `算力预算`：你愿意为这次训练总共花多少计算资源。
- `参数量`：模型有多大。
- `token / 词元`：模型训练时实际读进去的最小文本单位，可以粗略理解成“模型看到的字或词片段”。
- `loss / 损失`：模型总体错得有多厉害，通常越低越好。
- `scaling law / 缩放定律`：当参数量、数据量、算力变化时，模型表现如何跟着变化。
- `欠训练 / undertrained`：不是模型太小，而是训练数据和训练步数没跟上，模型的潜力没有被充分用出来。

## 1. 要解决什么问题

先交代一下背景。

2020 年，OpenAI 发了一篇 [缩放定律论文](/zh-CN/posts/scaling-laws-for-neural-language-models/)（Kaplan 等人），核心结论是：模型越大效果越好，而且好多少是可以用公式算出来的。到 2022 年春天，整个行业把这句话奉为圣旨。

GPT-3，1750 亿参数，训了 3000 亿词元（词元就是模型看的「字」，大约每个英文单词切成 1-2 个词元，中文大约一个字一个词元）。DeepMind 自家的 Gopher，2800 亿参数，也训了 3000 亿词元。随后 Google 又发布了 5400 亿参数的 PaLM。趋势很明确：参数拉满。

但有一个问题就摆在那儿，大家却视而不见。

Kaplan 等人的结论是：有更多钱（算力）的时候，大部分预算应该花在造更大的模型上（用数学写就是 N ∝ C^0.73），训练数据只需要跟着慢慢涨就行（D ∝ C^0.27）。翻译成白话就是：模型尽量大，数据差不多就行。

Hoffmann 的团队提了一个很简单的问题：这个结论靠谱吗？

## 2. 三条路走到同一个答案

这篇论文最有说服力的地方在于方法论。他们没有只做一组实验就下结论：而是从三个完全独立的角度切入，三条路走到了同一个终点。

**方法一：总预算不变，换着分。** 打个比方：你有 50 万装修预算，可以选择买贵的家电配便宜家具，也可以反过来。他们就是这么做的：训了超过 400 个模型，参数量从 7000 万到 160 多亿不等，每个模型分到的「模型大小 vs 训练数据」比例不同，但总算力一样。对于每个预算档位，他们找出效果最好的那个分配方案。

**方法二：画等高线。** 他们训了 9 种不同大小的模型（从 7000 万到 100 亿参数），每种喂不同量的数据，专门设计成每一组的总算力大致相等。就像在地图上画等高线一样，沿着每条「等算力线」找最优点。

**方法三：直接写方程，用数据拟合。** 他们假设模型表现可以用下面这个方程描述：

$$
\hat{L}(N, D) = E + \frac{A}{N^\alpha} + \frac{B}{D^\beta}
$$

这方程其实在说一件很直觉的事：模型考得不好，要么是脑子不够大（N 太小），要么是书看少了（D 太小），要么两个都有。E 是一个谁都突破不了的下限，学术上叫「熵」（entropy）。熵是信息论里的一个概念，衡量的是「一件事有多不确定」。抛一枚均匀硬币，正反各 50%，不确定性最大，熵最高。如果硬币灌了铅，99% 正面朝上，结果几乎没悬念，熵就很低。语言也有熵。「太阳从___升起」，这句话的熵很低，几乎所有人都会填「东方」。但「我今天吃了___」，后面可以是火锅、三明治、亏，答案五花八门，熵就高。自然语言整体的熵，就是 AI 模型表现的理论天花板：不管模型多强、数据多多，它不可能比这条线更好，因为语言本身就有这么多不确定性。把所有训练数据代入这个方程拟合，就能反推出：给定一笔算力预算，模型该多大、数据该多少。

三条路的答案一致：

$$
N_{\mathrm{opt}} \propto C^a, \quad D_{\mathrm{opt}} \propto C^b, \quad a \approx 0.50, \quad b \approx 0.50
$$

```python showLanguage
def optimal_scaling(compute: float) -&gt; tuple[float, float]:
    a = 0.50
    b = 0.50
    n_opt = compute ** a
    d_opt = compute ** b
    return n_opt, d_opt
```

a ≈ b ≈ 0.5 的真正含义是：随着算力增长，模型大小和训练数据应当按近似相同的比例一起扩张。算力翻 10 倍时，两者都大约增到 3.2 倍；算力翻 2 倍时，两者都大约增到 1.4 倍。换句话说，模型大小每翻一倍，训练数据也应该翻一倍。这跟 Kaplan 等人的结论直接矛盾：Kaplan 说算力主要应该花在模型大小上。

打个比方：Kaplan 的建议像是「买了一套 200 平的豪宅，但只摆了几件家具」。Chinchilla 说：「不对，你应该买 100 平的房子，然后好好装修：家具配齐、软装到位。同样的总花费，住着舒服得多。」

## 3. Kaplan 为什么算偏了

这不是谁「做错了」，而是实验设定不同，最终导致了不同的最优分配结论。两个团队都做了认真的工作。

差别出在一个训练细节上：学习率调度。

学习率是什么？简单说，AI 模型在训练过程中会不断调整自己的参数。学习率就是「每次调整幅度有多大」。一般来说，训练初期步子大一点（快速学），后期步子小一点（精细调）。这个「先大后小」的节奏安排，就叫学习率调度。

Kaplan 等人用了固定的学习率调度：不管你打算训多久，节奏都一样。但合理的做法是：训得越久，后期的调整步子应该越小越细。他们没做这个适配，导致训练时间一拉长，后半段的学习效率就掉下来了。这就让「训更久」看起来不划算，间接得出了「别在训练时长上花钱，把钱花在模型大小上」的结论。

Hoffmann 的团队给每次训练都单独调整了学习率调度，让每种配置都能发挥最佳水平。一旦做到这点，训更多数据的回报远比 Kaplan 的数字暗示的要大。

这件事的教训很深刻：缩放定律是经验法则，不是物理常数。实验条件变了，结论就可能变。

```python showLanguage
from dataclasses import dataclass
from typing import Literal


@dataclass(frozen=True)
class TrainingConfig:
    n_params: float
    n_tokens: float
    schedule: Literal[&quot;fixed&quot;, &quot;cosine_with_warmup&quot;]
    warmup_steps: int
    total_steps: int
```

## 4. 那个描述一切的方程

方法三值得单独展开看一看，因为它给出了一个完整的数学模型来描述模型表现：

$$
\hat{L}(N, D) = E + \frac{A}{N^\alpha} + \frac{B}{D^\beta}
$$

拟合出来的具体数字：

- E = 1.69 ： 地板值，也就是自然语言的熵。「我今天吃了___」后面填什么，连人都没法百分百答对，这种不确定性就是熵。不管模型多强、数据多多，表现不可能好过这个数
- A = 406.4，α = 0.34 ： 「脑子不够用」的罚分
- B = 410.7，β = 0.28 ： 「书看少了」的罚分

这个方程的结构很好理解。模型表现不好，原因无非三个：第一，语言本身就有不确定性，再强的模型也无法完全预测（E）；第二，模型太小，记不住那么多规律（A/N^α）；第三，训练数据太少，见过的世面不够（B/D^β）。后两个罚分是加法关系：哪块短板更严重，哪块就拖后腿更厉害。

就好比考试成绩不好有两个原因：一是脑子不够用（模型太小），二是书看少了（数据不够）。这两块是独立的短板，得分别补。

```python showLanguage
def estimated_loss(n_params: float, n_tokens: float) -&gt; float:
    e = 1.69
    a = 406.4
    alpha = 0.34
    b = 410.7
    beta = 0.28
    return e + a / (n_params ** alpha) + b / (n_tokens ** beta)


def optimal_params_and_tokens(compute_flops: float) -&gt; tuple[float, float]:
    alpha = 0.34
    beta = 0.28
    a = beta / (alpha + beta)
    b = alpha / (alpha + beta)
    g = 2.0

    base = compute_flops / 6.0
    n_opt = g * (base ** a)
    d_opt = (1.0 / g) * (base ** b)
    return n_opt, d_opt
```

## 5. 那张让全行业尴尬的表

论文的 Table 1 列了当时几个大模型的实际参数量和训练词元数，Table 3 则给出了不同模型大小下 compute-optimal 的词元估计。把两张表对照着看，就像一份对整个行业的审计报告：

| 模型 | 参数量 | 实际训练词元 | Chinchilla 最优词元 |
|------|--------|-------------|-------------------|
| GPT-3 | 1750 亿 | 3000 亿 | 3.7 万亿 |
| Gopher | 2800 亿 | 3000 亿 | 5.9 万亿 |
| Jurassic-1 | 1780 亿 | 3000 亿 | 3.7 万亿 |
| MT-NLG | 5300 亿 | 2700 亿 | 11.0 万亿 |

发现规律了没有？所有模型训练数据都在 3000 亿词元左右。好像整个行业不约而同地认定「3000 亿够了」，然后把多余的算力全砸在了参数量上。

按 Chinchilla 的分析，GPT-3 应该训 3.7 万亿词元：是实际数据量的 12 倍多。Gopher 应该看将近 6 万亿。最夸张的是 MT-NLG，5300 亿参数的巨无霸，应该训 11 万亿词元：实际只喂了 2700 亿，差了 40 倍。

这些模型不是太小，是没有吃到足够数据。

```python showLanguage
from dataclasses import dataclass


@dataclass(frozen=True)
class ModelComparison:
    name: str
    params_billions: float
    tokens_used_billions: float
    optimal_tokens_billions: float


def industry_models() -&gt; list[ModelComparison]:
    return [
        ModelComparison(&quot;GPT-3&quot;, 175.0, 300.0, 3_700.0),
        ModelComparison(&quot;Gopher&quot;, 280.0, 300.0, 5_900.0),
        ModelComparison(&quot;Jurassic-1&quot;, 178.0, 300.0, 3_700.0),
        ModelComparison(&quot;MT-NLG&quot;, 530.0, 270.0, 11_000.0),
    ]
```

## 6. 实锤：Chinchilla vs. Gopher

光说不练假把式。为了验证理论，团队训了 Chinchilla：一个 700 亿参数的模型，喂了 1.4 万亿词元。关键在于：Chinchilla 和 Gopher（2800 亿参数，3000 亿词元）用了完全一样的算力预算。同样的钱，只是分法不同。

结果一目了然。Chinchilla 虽然只有 Gopher 四分之一的参数量，在几乎所有基准测试上都赢了：

- **MMLU**（大规模多任务语言理解）：Chinchilla 67.6% vs. Gopher 60.0% vs. GPT-3 43.9%
- **阅读理解**（RACE-h）：Chinchilla 73.3% vs. Gopher 71.6%
- **常识推理**（HellaSwag）：Chinchilla 80.8% vs. Gopher 79.2%
- **BIG-bench**：Chinchilla 在大多数任务上优于 Gopher

```python showLanguage
from dataclasses import dataclass


@dataclass(frozen=True)
class ModelConfig:
    name: str
    params_billions: float
    tokens_billions: float
    mmlu_accuracy: float


def chinchilla_vs_gopher() -&gt; tuple[float, float]:
    gopher = ModelConfig(&quot;Gopher&quot;, 280.0, 300.0, 60.0)
    chinchilla = ModelConfig(&quot;Chinchilla&quot;, 70.0, 1_400.0, 67.6)

    gopher_flops = 6.0 * gopher.params_billions * 1e9 * gopher.tokens_billions * 1e9
    chinchilla_flops = 6.0 * chinchilla.params_billions * 1e9 * chinchilla.tokens_billions * 1e9
    return gopher_flops, chinchilla_flops
```

一个参数量只有对手四分之一的模型，同样的花费，在几乎每项指标上都赢了：这不是理论推导，这是真刀真枪的对比。算力没有浪费，只是换了个花法：从堆参数，变成了喂数据。

打个比方：同样 100 万的教育预算，Gopher 的策略是「请一个超级天才，但只给他一本薄教材」；Chinchilla 的策略是「请一个聪明的学生，给他一整个图书馆」。后者考得更好。

## 7. 实际影响

Chinchilla 论文立刻改变了行业的做事方式。

**小模型更省钱。** 训练成本是一次性的，但用户每次提问，模型都要跑一遍：这个「推理成本」跟模型大小直接挂钩，而且是每一次请求都要付的。你可以理解为：训练费是买房，推理费是物业费。房子（模型）越大，物业费越贵，而且得一直交。一个 700 亿参数的模型，每次服务用户的成本只有 2800 亿模型的四分之一。如果小模型效果还更好，那就是双赢：质量更高，账单更低。

**数据成了瓶颈。** Chinchilla 之前，大家抢的是 GPU：你能搞到多少卡？Chinchilla 之后，大家抢的变成了数据：你从哪儿找几万亿高质量词元？这直接引发了全行业的数据争夺战：大规模网页抓取、数据集精选工程，以及后来的合成数据运动（让 AI 生成训练数据来训 AI）。

**LLaMA 时刻。** Meta 的 LLaMA（2023 年 2 月）可以说是 Chinchilla 缩放定律最直接的应用。LLaMA-13B（130 亿参数）在 1 万亿词元上训练，在大多数基准上超过了 GPT-3（1750 亿参数）：一个比你小十几倍的模型考得比你好，就因为人家书读得多。LLaMA-65B 在 1.4 万亿词元上训练，跟 Chinchilla 和 PaLM-540B 不相上下。Meta 在论文中明确引用了 Chinchilla，刻意选择更小的模型配更多的数据。

```python showLanguage
def inference_cost_comparison() -&gt; tuple[float, float]:
    gopher_cost_per_token = 280.0
    chinchilla_cost_per_token = 70.0

    queries_per_day = 1_000_000.0
    tokens_per_query = 500.0

    daily_cost_gopher = queries_per_day * tokens_per_query * gopher_cost_per_token
    daily_cost_chinchilla = queries_per_day * tokens_per_query * chinchilla_cost_per_token
    return daily_cost_gopher, daily_cost_chinchilla
```

## 8. 这篇论文改变了什么问题

Chinchilla 的钉子句是：**它不是反对大模型，而是要求参数和数据一起吃满算力预算。**

这篇论文的力量在于，它没有喊停 scaling，而是修正了 scaling 的配比。Kaplan 给了行业做大的信心；Chinchilla 追问同样一笔算力下，大多少、训多少才是最优。答案不是继续堆参数，而是让参数量和训练 token 近似同步增长。

这个判断把“模型大小”从单一崇拜里拉了出来。一个 2800 亿参数但只看 3000 亿 token 的模型，可能不如一个 700 亿参数但看 1.4 万亿 token 的模型。能力不是参数量本身，而是算力预算被参数和数据共同吸收后的结果。

下次看大模型发布，不要先问参数多大。先问它训练了多少 token、算力预算如何分配、是否欠训练。真正的规模化不是把模型吹大，而是让每一份算力都落到能降低 loss 的地方。

---

**论文共读系列**

- [《Sequence to Sequence Learning with Neural Networks》](/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/)（使用神经网络进行序列到序列学习） — 编码器-解码器范式的确立
- [《Neural Machine Translation by Jointly Learning to Align and Translate》](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)（通过联合学习对齐与翻译实现神经机器翻译） — 注意力机制的起源
- [《Attention Is All You Need》](/zh-CN/posts/attention-is-all-you-need/)（注意力就是你所需要的全部） — 注意力成为主角，Transformer 的诞生
- [《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》](/zh-CN/posts/bert/)（BERT：用于语言理解的深度双向 Transformer 预训练） — 预训练范式的确立
- [《Scaling Laws for Neural Language Models》](/zh-CN/posts/scaling-laws-for-neural-language-models/)（神经语言模型的缩放定律） — 规模的数学
- [《Language Models are Few-Shot Learners》](/zh-CN/posts/language-models-are-few-shot-learners/)（语言模型是少样本学习者） — 更大的模型，更善于从上下文中诱发能力</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>chinchilla</category><category>scaling-laws</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>《Scaling Laws for Neural Language Models》：规模的数学</title><link>https://justinhuangai.github.io/zh-CN/posts/scaling-laws-for-neural-language-models/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/scaling-laws-for-neural-language-models/</guid><description>规模的数学：为什么更大的模型可预测地更强，附真实 Python 核心代码</description><pubDate>Sun, 01 Mar 2026 08:45:39 GMT</pubDate><content:encoded>训练大模型首先是一个预算分配问题。参数、数据和算力都要钱；问题不是“越大越好”这种口号，而是下一块钱该买参数、买数据，还是买更长训练。

[《Scaling Laws for Neural Language Models》](/papers/2001.08361v1.pdf) 把大模型训练从经验主义推进到预算函数。它没有发明新架构，却给了行业一个更硬的判断工具：在花钱之前，先估计回报曲线。

## 0. 先认几个词

如果你平时不常看这种带公式的论文，先把这几个词认熟，后面会顺很多：

- `参数量`：模型里一共有多少可学习参数，也就是模型的大小。
- `数据量`：训练时一共喂给模型多少文本。
- `算力 / compute`：训练时总共做了多少计算。你可以先把它当成“电费账单”。
- `loss / 损失`：模型犯错有多严重，通常越低越好。
- `幂律 / power law`：某个量按固定指数变化的关系；画在对数坐标上，经常会接近一条直线。
- `对数坐标`：像 `1、10、100、1000` 这样按倍数增长的刻度，不是 `1、2、3、4` 那样均匀加一。

## 1. 要解决什么问题

到 2020 年初，AI 圈已经知道一件事：模型越大，效果越好。但「越大越好」不是科学，是感觉。

大家回答不了几个最基本的问题：算力预算翻一倍，效果能好多少？这笔钱是该花在更大的模型上、更多的数据上，还是更久的训练上？有没有一个公式，可以在花钱之前就算出来？

这篇论文给出了公式。不是靠拍脑袋，也不是靠经验法则：靠方程。

## 2. 幂律：核心发现

论文的核心发现，一句话就能说清：**AI 模型的表现好坏，和它的「个头」之间存在一个简洁的数学关系。**

具体来说，论文测量了三样东西对模型表现的影响：模型有多大（参数量）、喂了多少数据、烧了多少算力。在论文观测到的范围内，只要模型的瓶颈主要在其中一项上，表现和这一项之间的关系都能画成一条漂亮的直线：前提是你把坐标轴的刻度取成对数（也就是 1, 10, 100, 1000 这样等距排列，而不是 1, 2, 3, 4）。

这种「对数坐标下的直线关系」，数学上叫**幂律（power law）**。

三个方程概括了整篇论文：

$$
L(N) \approx \left(\frac{N_c}{N}\right)^{\alpha_N}, \quad \alpha_N \approx 0.076
$$

$$
L(D) \approx \left(\frac{D_c}{D}\right)^{\alpha_D}, \quad \alpha_D \approx 0.095
$$

$$
L(C) \approx \left(\frac{C_c}{C}\right)^{\alpha_C}, \quad \alpha_C \approx 0.050
$$

别被符号吓到。拆开就是几个简单的角色：

- **L** 是「测试损失」：你可以理解为模型的考试成绩，只不过分数越低代表越好（想象成失误率：失误越少，能力越强）
- **N** 是参数量，也就是模型的「个头」。参数越多，模型能记住的规律就越多
- **D** 是训练数据量，也就是模型的「课本厚度」。课本越厚，能学到的东西就越多
- **C** 是训练消耗的总算力，也就是「电费账单」。单位是 PetaFLOP-days：每秒做一千万亿次运算，连续跑一整天
- **N_c、D_c、C_c** 是常数，当参考点用的
- **α**（alpha）是幂律的指数，决定了「个头翻倍时，成绩能进步多少」。指数越大，同样的投入换来的进步越大

为什么幂律这么重要？因为它意味着回报不会快速见顶。

打个比方：如果 AI 的进步像背单词：前 1000 个很容易，后面越来越慢，背到 5000 个就几乎不动了：那就是对数增长，回报递减极快。但幂律不一样，它像修路：你修到 10 公里的时候觉得效果不错，修到 100 公里效果更好，修到 1000 公里效果又上了一个台阶。每上一个量级都有实实在在的回报。

当然，论文也提醒了：路不可能修到无限远。这个趋势最终一定会变平。但在论文观测到的范围内，这条线走得干净利落，没有撞墙的迹象。

```python showLanguage
def power_law_loss(x: float, x_c: float, alpha: float) -&gt; float:
    return (x_c / x) ** alpha


def scaling_law_examples() -&gt; dict[str, float]:
    alpha_n = 0.076
    alpha_d = 0.095
    alpha_c = 0.050

    return {
        &quot;10x_params&quot;: 10.0 ** alpha_n,
        &quot;10x_data&quot;: 10.0 ** alpha_d,
        &quot;10x_compute&quot;: 10.0 ** alpha_c,
    }
```

三个指数本身就在讲故事。数据量（α = 0.095）扩大一个量级带来的进步最大。模型大小（α = 0.076）次之。算力（α = 0.050）最低：因为如果你只是堆算力，却不合理分配到模型大小和训练时长上，就是在烧钱。真正的杠杆在于：扩对东西。

## 3. 在论文测过的范围内，怎么搭不重要，搭多大才重要

这是论文最出人意料的发现。

Transformer 有很多可以调的「形状」参数：堆多少层（深度）、每层有多宽（隐藏维度）、用多少个注意力头、前馈网络有多大。直觉上你可能觉得，这些比例关系至关重要，调好了事半功倍。

但论文发现：在它测过的 Transformer 形状范围内，这些比例关系几乎不影响最终表现。真正起决定作用的是一个数字：非嵌入参数的总量。

什么是「非嵌入参数」？简单说，模型的参数分两种：一种是「词典」（嵌入层，负责把文字转成数字），另一种是「大脑」（Transformer 层，负责理解和推理）。论文发现，真正决定模型能力的是「大脑」的大小，不是「词典」的大小。

一个只有 2 层但每层特别宽的 Transformer？和一个 40 层但每层很窄的 Transformer？只要它们的「大脑」总参数量接近，考试成绩就差不多。

```python showLanguage
from dataclasses import dataclass


@dataclass(frozen=True)
class ArchitectureExperiment:
    n_layers: int
    d_model: int
    n_heads: int
    d_ff: int


def non_embedding_params(config: ArchitectureExperiment) -&gt; int:
    n = config.n_layers
    d = config.d_model
    d_ff = config.d_ff
    return n * (4 * d * d + 2 * d * d_ff + 4 * d)
```

这个发现的实际意义很直接：你不用花几周去搜索「最优架构」。选一个合理的 Transformer 形状，然后把精力放在做大就行。

## 4. 模型什么时候会「死记硬背」：数据瓶颈

模型不是越大越好：如果你的课本太薄的话。

想象一个记忆力超强的学生，你只给他一本 100 页的教材，他很快就能把这本书倒背如流。但这不叫「学会了」，这叫「背下来了」。考试一换题型就傻眼。这就是过拟合：模型把训练数据死记硬背了，却没有学到真正的规律。

论文这里真正漂亮的地方，是给出了一个统一公式，把「模型多大」和「数据多少」如何共同决定表现写进了一个式子：

$$
L(N, D) = \left[\left(\frac{N_c}{N}\right)^{\alpha_N / \alpha_D} + \frac{D_c}{D}\right]^{\alpha_D}
$$

这个公式说的是：模型的考试成绩不是由「个头」或「课本厚度」单独决定的，而是两者一起。如果模型够大但数据不够，性能就卡在数据上；如果数据够多但模型太小，性能就卡在模型上。过拟合，就是「个头大、课本薄」这对矛盾的自然结果。

从这个关系出发，论文还给了一个粗略的经验门槛：「课本至少要多厚，才不会让这个学生背书」：

$$
D \gtrsim 5 \times 10^3 \times N^{0.74}
$$

用大白话说：模型大 10 倍，课本只需要厚大约 5.5 倍就够了。更大的模型学习效率更高：同样看一页书，它能悟到更多。

```python showLanguage
def loss_nd(n_params: float, n_tokens: float) -&gt; float:
    n_c = 8.8e13
    d_c = 5.4e13
    alpha_n = 0.076
    alpha_d = 0.095
    ratio = alpha_n / alpha_d
    return ((n_c / n_params) ** ratio + d_c / n_tokens) ** alpha_d


def min_dataset_tokens(n_params: float) -&gt; float:
    return 5_000.0 * n_params ** 0.74
```

按这个公式粗略一算，GPT-3 那个级别（1750 亿参数）的模型要想不「背书」，课本厚度应该接近万亿词元。但 GPT-3 实际只喂了 3000 亿词元：远没到安全线。回头看，GPT-3 的数据其实是偏少的。这也是为什么后来业界重新审视了「模型多大、数据喂多少」这个配比：最典型的就是 Chinchilla 论文（Hoffmann 等人，2022 年），直接指出：之前那些大模型，数据普遍喂少了。

## 5. 算力最优分配：钱该怎么花

如果你有一笔固定的算力预算：比如说够你租 1000 块 GPU 跑一个月：应该怎么花？这是论文里最有实际价值的问题，答案相当反直觉。

论文发现最优分配遵循：

$$
N_{\mathrm{opt}} \propto C^{0.73}
$$

$$
B_{\mathrm{opt}} \propto C^{0.24}
$$

$$
S_{\mathrm{opt}} \propto C^{0.03}
$$

翻译成人话：如果你的预算涨了 10 倍，你应该把模型做大约 5.4 倍，每次喂的数据量增加约 1.7 倍，训练时间几乎不延长（大约只多 7%）。

钱主要花在哪？花在把模型做大上。

反直觉的部分来了：**你应该造一个尽可能大的模型，然后不用训到头就可以停。** 大多数人的直觉是「我选个中等大小的模型，然后慢慢训，训到极致」。缩放定律说的恰恰相反：同样一笔钱，一个没训完的大模型，比一个训透了的小模型，表现更好。

就像装修：同样 50 万预算，与其在一个 60 平的小户型里堆满顶配材料，不如买一个 120 平的大户型做个简装。空间大了，住起来怎么都比小房子舒服。

```python showLanguage
from dataclasses import dataclass


@dataclass(frozen=True)
class ComputeAllocation:
    n_params: float
    batch_size: float
    training_steps: float


def optimal_allocation(compute: float) -&gt; ComputeAllocation:
    return ComputeAllocation(
        n_params=compute ** 0.73,
        batch_size=compute ** 0.24,
        training_steps=compute ** 0.03,
    )


def is_compute_efficient(n_params: float, compute: float) -&gt; bool:
    optimal_n = compute ** 0.73
    return abs(n_params / optimal_n - 1.0) &lt; 0.5
```

这个结论深刻地改变了整个行业。五个月后发布的 GPT-3 直接遵循了这个思路：造一个当时规模空前的 1750 亿参数模型，而不是把小模型训到极致。后来的 Chinchilla 论文（Hoffmann 等人，2022 年）更新了具体的指数，认为大多数大模型的数据其实喂少了：但「最优权衡是可以算出来的」这个核心洞察，源头在这里。

## 6. 临界批大小：什么时候该加机器

训练 AI 模型的时候，你可以选择每次给模型看多少数据再更新一次：这叫「批大小」（batch size）。批大小越大，你可以同时用更多 GPU 并行处理，训练速度就越快。但并不是加机器就一定有用。

论文发现，批大小存在一个「甜蜜点」：

$$
B_{\mathrm{crit}} \propto L^{-4.8}
$$

训练刚开始的时候，模型还很「菜」，每批数据都能给它很大启发，小批量就够了。但训练到后期，简单的规律都学完了，每批数据带来的新信息越来越少，这时候就需要更大的批量来「凑够信号」。

甜蜜点以下，加机器很划算：机器翻倍，训练时间几乎减半。甜蜜点以上，加机器就是烧钱：多出来的机器几乎不加速。

```python showLanguage
def critical_batch_size(loss: float, b_star: float, l_star: float) -&gt; float:
    return b_star * (l_star / loss) ** 4.8
```

很多团队全程用固定的批大小训练。缩放定律告诉你：应该随着训练推进逐步加大：开始用小批量，模型变强后再加机器。

## 7. 这篇论文改变了什么问题

Scaling Laws 的钉子句是：**训练大模型不是玄学试错，而是预算函数。**

这篇论文最重要的贡献不是某个指数，而是把“更大更好”从经验判断变成可估算的曲线。参数、数据、算力不再只是工程变量，而是可以放进同一张账本里比较边际回报的投资项。

它没有告诉行业永远只做更大模型。它告诉行业，在给定假设和观测范围内，花钱之前应该先算：扩大参数、增加数据、延长训练，哪一个最可能带来更低 loss。GPT-3 的规模决策之所以看起来突然变得合理，背后就是这种预算信心。

下次看模型规模，不要问“是不是军备竞赛”。先问预算函数是什么，瓶颈变量是谁，边际收益还剩多少。真正成熟的规模化，不是烧更多钱，而是知道钱烧到哪里会变成能力。

---

**论文共读系列**

- [《Sequence to Sequence Learning with Neural Networks》](/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/)（使用神经网络进行序列到序列学习） — 编码器-解码器范式的确立
- [《Neural Machine Translation by Jointly Learning to Align and Translate》](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)（通过联合学习对齐与翻译实现神经机器翻译） — 注意力机制的起源
- [《Attention Is All You Need》](/zh-CN/posts/attention-is-all-you-need/)（注意力就是你所需要的全部） — 注意力成为主角，Transformer 的诞生
- [《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》](/zh-CN/posts/bert/)（BERT：用于语言理解的深度双向 Transformer 预训练） — 预训练范式的确立
- [《Language Models are Few-Shot Learners》](/zh-CN/posts/language-models-are-few-shot-learners/)（语言模型是少样本学习者） — 更大的模型，更善于从上下文中诱发能力
- [《Training Compute-Optimal Large Language Models》](/zh-CN/posts/training-compute-optimal-large-language-models/)（训练计算最优的大语言模型） — 怎样花算力最划算</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>scaling-laws</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>OpenClaw 架构：自托管 AI 助手的工程骨架 🦞</title><link>https://justinhuangai.github.io/zh-CN/posts/openclaw-architecture/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/openclaw-architecture/</guid><description>基于 v2026.3.8 源码，拆解一个自托管 AI 助手的工程骨架</description><pubDate>Tue, 24 Feb 2026 08:37:11 GMT</pubDate><content:encoded>![OpenClaw](/images/openclaw-logo-text-dark.webp)

[上一篇](/zh-CN/posts/openclaw-ecosystem/)聊了生态，这一篇拆架构。

OpenClaw 的代码量不小：43 万行 TypeScript。但这篇文章不看代码量，看复杂性怎么被分配。一个自托管 AI 助手要同时处理二十多个聊天入口、多个 Agent、工具调用和权限边界；架构问题不是“功能怎么塞进去”，而是“哪些复杂性应该留在运行时，哪些应该推给模型、CLI 和策略层”。

## 0. 先认几个词

如果你没有系统架构背景，也没关系。先记住 6 个词，后面会顺很多：

- `架构`：系统如何分层、每层分别负责什么。
- `Channel / 渠道层`：负责和外部聊天平台“说同一种协议”的适配层。
- `Gateway / 网关`：中央调度者，负责路由消息、管理会话和 Agent。
- `Node / 执行节点`：真正去拍照、跑命令、操作设备的执行端。
- `workspace / 工作区`：Agent 的配置、记忆、技能和会话文件所在的本地目录。
- `sandbox / 沙箱`：把高风险操作隔离起来的受限执行环境。

## 1. 3 层架构：Channel → Gateway → Node

先看全景图：

![OpenClaw Architecture](/images/openclaw-architecture-zh-CN.svg)

你可以把 OpenClaw 想成一家公司的 3 个部门：

| 谁 | 干什么 | 打个比方 |
|------|------|--------|
| **Channel（渠道层）** | 对接 WhatsApp、Telegram、Discord、飞书等 20+ 聊天平台 | 前台：负责接电话、收邮件、接待来访 |
| **Gateway（网关）** | 中央调度，管理所有会话和 Agent | 大脑：谁的消息该给谁处理、怎么回复，全在这里决定 |
| **Node（执行节点）** | 在设备上干活：拍照、录屏、跑命令 | 手和脚：大脑说「拍张照」，它去执行 |

整个系统的核心就是 Gateway：一个常驻运行的进程，默认只监听本机（`127.0.0.1:18789`），不对外网开放。想远程访问？走 Tailscale 隧道，不直接暴露端口。这个设计叫「Loopback-First」，安全上很讨巧：什么端口都不开，就没有攻击面。

为什么只用一个进程不搞分布式？原因很实际：WhatsApp 协议要求同一时间只能有一个设备在线，你搞两个进程反而会打架。与其为了「架构正确」多搞一堆协调逻辑，不如老老实实一个进程管到底。对绝大多数个人用户来说，够用了。

## 2. 一条消息的旅程

你在 Telegram 上跟你的 OpenClaw 说了一句「帮我查下明天北京天气」，发生了什么？

**第 1 步：接入。** Telegram 适配器收到消息，把 Telegram 格式的数据翻译成 OpenClaw 内部统一格式。不管消息从哪个平台来，翻译完都长一个样。

**第 2 步：验身份、找路由。** 你是谁？允不允许跟 Agent 说话？如果是陌生人，先弹一个配对码让主人批准。通过后，路由引擎决定这条消息交给哪个 Agent。

**第 3 步：组装上下文。** 从磁盘读出你之前的聊天记录，加载 Agent 的性格设定（SOUL.md）、行为规则（AGENTS.md），再从记忆库里语义搜索相关内容。组装成一份完整的「大脑简报」。

**第 4 步：问大模型。** 这份简报发给 Anthropic、OpenAI、DeepSeek、MiniMax、GLM、Qwen、Gemini 等大模型提供商（你配了哪个就用哪个），模型一个字一个字地流式吐出回复。

**第 5 步：执行工具。** 如果模型说「我需要跑个命令查天气」，运行时会拦截这个请求，根据安全策略决定在哪里执行（管理员的命令直接跑，陌生人的命令丢到 Docker 沙箱里跑），然后把结果喂回给模型继续生成。

**第 6 步：回复。** 最终答案按 Telegram 的格式要求（字数限制、Markdown 规则等）拆好，发回给你。聊天记录写入磁盘。

整个链路中，真正慢的是第 4 步：等大模型吐第一个字。其他步骤基本都是毫秒级。

## 3. 渠道适配层：一次开发，到处说话

OpenClaw 对接了二十多个聊天平台，但不是每个平台都从零写一套。它抽象了一层「适配器」，每个平台的适配器只负责四件事：

- **登录**：WhatsApp 扫二维码、Telegram 填 Bot Token、iMessage 用 macOS 原生能力：各平台各的规矩
- **翻译进来的消息**：不管是文字、图片、回复还是表情，统统翻译成内部统一格式
- **管门禁**：谁能私聊、群里要不要 @才回复、哪些群允许
- **翻译出去的回复**：把 Agent 的回复适配成各平台能显示的格式

内置的有六个大平台（WhatsApp、Telegram、Discord、Slack、Signal、iMessage），其他三十多个（飞书、LINE、Matrix、Mattermost 等）通过插件接入。

这层抽象最大的好处是：Agent 完全不需要知道消息来自哪个平台。你在 WhatsApp 上聊的 Agent 和在 Telegram 上聊的是同一个，逻辑完全复用。渠道只是「传话筒」，换一个传话筒不影响说话的人。

## 4. Agent 运行时与工作区

Agent 运行时的内核来自 Pi-mono（一个开源编程 Agent），嵌入在 Gateway 里面跑。

### 「一切皆文本」的工作区

这里的关键设计是：每个 Agent 的所有配置，都落在可以直接打开编辑的文本文件里：

```
workspace/
├── AGENTS.md          # 定义 Agent 是谁、怎么做事（相当于简历+工作手册）
├── SOUL.md            # 灵魂设定：性格、价值观（写好就不该改）
├── USER.md            # 记录用户信息：你叫什么、喜好是什么
├── MEMORY.md          # 长期记忆：Agent 主动记下的重要事情
├── HEARTBEAT.md       # 定时任务：比如每天早上报天气
├── memory/            # 日记本
│   └── YYYY-MM-DD.md  # 每天一页，只追加不修改
├── skills/            # 技能包
└── sessions.json      # 会话记录
```

没有数据库，没有专用管理界面，纯文本文件。想改 Agent 性格？打开 SOUL.md 改两行就行。想看它记住了什么？打开 MEMORY.md 直接看。这种「你能看到它的全部大脑」的透明感，是很多封闭 AI 产品做不到的。

### 每一轮对话怎么跑

1. **分清身份**：你是管理员直聊、朋友私聊、还是群里有人 @了它？不同来源，安全等级不同
2. **组装记忆**：读聊天记录、加载性格和规则、搜索相关的长期记忆，拼成一份完整上下文
3. **问大模型**：发给配置的模型，支持 fallback：主力模型挂了自动切备用
4. **干活+记住**：执行工具调用，更新聊天记录

上下文的组装不是把所有东西一股脑塞进去。不相关的技能不加载、不需要的工具不注入：省 token 就是省钱。

## 5. 4 层记忆：让 AI 真的「记住你」

普通聊天机器人关掉窗口就失忆了。OpenClaw 不会。它有 4 层记忆，从最深层的「我是谁」到最浅层的「刚才聊了啥」：

| 层 | 是什么 | 打个比方 |
|------|--------|------|
| **SOUL** | 人格设定，永远不变 | 你的性格：出生就定了 |
| **TOOLS** | 当前装了哪些技能 | 你今天带了哪些工具出门 |
| **USER** | 关于你的长期记忆 | 你的老朋友记得你爱吃什么 |
| **Session** | 当前这次对话 | 此刻正在聊的话题 |

几个巧妙的机制：

**每日日记。** 每天的对话会自动写进 `memory/2026-03-12.md`（只追加，不覆盖）。下次开聊时，Agent 会自动翻看今天和昨天的日记，保持连续感：「你昨天说要买的那本书，买了吗？」

**自动抢救记忆。** 聊得太长，上下文快塞满了怎么办？OpenClaw 会偷偷在后台跑一个隐形轮次，把重要信息存进 MEMORY.md，然后压缩掉旧内容。你察觉不到这个过程，但关键信息不会丢。这个机制叫 Pre-Compaction。

**语义搜索。** 你说「之前聊过的那个部署问题」，它能从记忆里搜出来：不是靠关键词精确匹配，而是靠语义理解。底层是向量搜索（SQLite-vec）+ 传统关键词搜索（BM25）双管齐下。

**跨平台认人。** 你在 Telegram 上跟它聊了半小时，切到 WhatsApp 继续聊，它认得你。同一个人在不同平台的 ID 会被链接到同一个身份，共享同一份记忆。但群聊的记忆是隔离的：你在群里说的话，不会泄露到私聊里。

## 6. 工具系统：复杂性转移到 CLI

OpenClaw 的工具系统很克制。

很多 AI Agent 框架会预置大量专用工具。OpenClaw 只保留 4 个核心工具：

| 工具 | 一句话 |
|------|------|
| **Read** | 读文件 |
| **Write** | 写文件 |
| **Edit** | 改文件 |
| **Bash** | 跑命令 |

这条路线的前提是：命令行已经封装了大量现实世界能力。查天气可以调用 `curl`，发邮件可以调用 CLI，查数据库可以走 `psql`。系统不为每个场景预制工具，而是把通用执行接口交给模型。

代价也很明确：模型必须知道该调用什么命令、如何组合命令、什么时候停止。复杂性没有消失，只是从工具清单转移到了模型能力、CLI 生态和权限策略。

在这 4 个核心工具之上，还有 55 个内置 Skills 和 ClawHub 技能市场。Skills 负责把高频流程封装起来，避免每次都从 Bash 层重新规划。

**MCP 被放在旁路，而不是主路径。** MCP 是 Anthropic 推出的工具协议标准，很多 AI 框架都在接入。OpenClaw 的主路径选择 CLI/Unix，并用内置的 `mcporter` 做桥接。这不是单纯反标准，而是把工具生态的中心放在命令行可组合性上。

**自我扩展把能力沉淀成 skill。** OpenClaw Agent 遇到不会做的事，可以生成 skill、安装、再在发现问题时修改重载。关键不在“自动变强”，而在临场解决方案能否沉淀成可复用流程。

## 7. 多 Agent 路由：一个大脑，多个人格

一个 Gateway 可以同时跑好几个 Agent，各管各的。路由规则长这样：

```json showLanguage
{
  &quot;bindings&quot;: [
    { &quot;agentId&quot;: &quot;home&quot;, &quot;match&quot;: { &quot;channel&quot;: &quot;whatsapp&quot;, &quot;accountId&quot;: &quot;personal&quot; } },
    { &quot;agentId&quot;: &quot;work&quot;, &quot;match&quot;: { &quot;channel&quot;: &quot;slack&quot; } },
    { &quot;agentId&quot;: &quot;bot&quot;, &quot;match&quot;: { &quot;channel&quot;: &quot;discord&quot;, &quot;guildId&quot;: &quot;123456&quot; } }
  ]
}
```

翻译一下：WhatsApp 私人号来的消息给「家庭助手」处理，Slack 的给「工作助手」，Discord 某个服务器的给「社区机器人」。三个 Agent 完全隔离：各自有自己的性格、记忆、技能和安全策略。

## 8. 安全：3 道门

你的 Agent 跑在你自己的服务器上，能执行命令、读写文件：安全当然是大事。OpenClaw 的安全模型分 3 道门：

**第一道门：你是谁？（私聊配对）** 陌生人给你 Agent 发消息，Agent 不会直接回。它会发一个 6 位配对码，你在已认证的渠道里确认了，对方才能用。这是默认行为：关掉就意味着任何知道你号码的人都能白嫖你的 API 额度。

**第二道门：VIP 通道（白名单）。** 信任的人可以直接加到白名单里（`allowFrom`），跳过配对直接聊。

**第三道门：群里别瞎回（群组规则）。** 群聊里 Agent 默认只在被 @ 时才回复，不会对着群里每条消息都冒泡：既省 token，又不扰民。

再往下还有纵深防御：工具权限五层过滤、Docker 沙箱隔离（陌生人的命令在沙箱里跑）、安全审计命令（`openclaw security audit`）。这些层是独立的：配对码泄露了？沙箱还在。沙箱被绕过了？工具策略还限制着能调什么。

## 9. 几个判断

**单进程不是偷懒，是务实。** 对个人用户来说，一个进程管到底比搞分布式靠谱得多。这个架构的天花板在哪？大概是同时在线的消息量超过单机处理能力的时候：对绝大多数人来说，这一天不会来。

**渠道抽象层是最值钱的一层。** 二十多个平台的差异全封装在适配器里，Agent 完全不操心消息从哪来。想加一个新平台？写个适配器就行，Agent 逻辑一行不改。这个解耦做得非常干净。

**安全设计是认真的，但落地仍有差距。** 架构上从身份、沙箱到工具策略做了纵深防御。但 Kaspersky 审计发现了 512 个漏洞（8 个严重级别），说明蓝图画得好和实际安全之间，隔着持续的工程投入。

**4 个核心工具的路线是一场复杂性转移。** 它赌的不是“工具越少越好”，而是模型能力、CLI 生态和权限策略加在一起，能比大量预制工具更可扩展。如果模型能力不足，Bash 会变成风险入口；如果权限收得住，这条路线会把工具系统做得很薄。

**最终的检验标准不是架构有多漂亮，而是跑得稳不稳。** 这篇分析基于源码静态阅读。实际效果：高并发下的 Gateway 稳定性、沙箱真的能不能防住攻击、跨渠道认人的边界情况：需要更多真实使用数据来验证。

架构只是骨架，生产环境才是考场。</content:encoded><category>OpenClaw</category><category>AI</category><category>open-source</category><category>openclaw</category></item><item><title>《Language Models are Few-Shot Learners》：GPT-3 与上下文学习</title><link>https://justinhuangai.github.io/zh-CN/posts/language-models-are-few-shot-learners/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/language-models-are-few-shot-learners/</guid><description>更大的模型，更善于从上下文中诱发能力，附真实 Python 代码</description><pubDate>Wed, 11 Feb 2026 08:22:54 GMT</pubDate><content:encoded>GPT-3 的问题意识不是“模型还能不能继续变大”，而是任务适配一定要改参数吗？在 BERT 范式里，每个新任务都要微调；这意味着数据、训练流程和部署版本都会跟着膨胀。

[《Language Models are Few-Shot Learners》](/papers/2005.14165v4.pdf) 的关键，是把任务适配从参数更新搬进上下文。模型权重固定，任务说明和少量示例进入 prompt，能力在推理时被临时组织出来。

## 0. 先认几个词

如果你对 GPT-3 这类模型的工作方式还没有概念，先记住下面几个词就够了：

- `语言模型`：给它一段前文，它的基本工作就是预测下一个词。
- `参数量`：模型里可学习的数字总数。你可以粗略把它理解成模型的“脑容量”。
- `prompt / 提示词`：你喂给模型看的任务说明、示例和输入。
- `上下文窗口`：模型一次能看到多少文本的容量。例子太多，塞不进去，就没法一起看。
- `few-shot / one-shot / zero-shot`：分别指给多个例子、给一个例子、完全不给例子。
- `in-context learning / 上下文学习`：不改模型参数，只靠 prompt 里的说明和例子，就让模型临时学会怎么做任务。

## 1. 要解决什么问题

[BERT](/zh-CN/posts/bert/) 确立的「预训练 + 微调」范式在 2020 年已经是主流做法。效果很好，但论文指出了三个根本问题。

第一，每个新任务仍然需要一个标注数据集。标注数据的获取成本高，且很多实际任务根本没有对应的标注集。

第二，微调后的模型在测试基准上的表现，不一定反映真实泛化能力。模型可能只是学到了训练数据中的虚假相关性（spurious correlations）：在基准集里得分很高，但换个分布就崩了。

第三，人类不是这样学习的。人类看一两个例子，听一句自然语言指令，就能完成新任务。而当时的 NLP 系统，每个新任务都需要成千上万条标注数据来微调。

论文的出发点是：如果模型足够大，它在预训练阶段积累的知识是否足以让它直接「读懂」任务描述和少量示例，然后给出答案？

## 2. 核心想法：不更新参数，只给提示

GPT-3 的评估方式和之前所有大模型都不一样。它定义了三种设置，全部不涉及梯度更新：

**少样本（Few-Shot）**：给模型一段任务描述，加上 10 到 100 个示例（具体数量取决于上下文窗口能装多少），然后让它完成新的输入。不更新权重，不做反向传播。

**单样本（One-Shot）**：只给一个示例。这最接近人类学习新任务的方式：有人给你演示一次，你就上手。

**零样本（Zero-Shot）**：连示例都不给，只有一句自然语言指令。这是最难的设置，但也是最实用的：如果模型真的「理解」了任务本身，它不应该需要任何例子。

```python showLanguage
from dataclasses import dataclass
from typing import Union


@dataclass
class ZeroShot:
    instruction: str
    prompt: str


@dataclass
class OneShot:
    instruction: str
    example: tuple[str, str]
    prompt: str


@dataclass
class FewShot:
    instruction: str
    examples: list[tuple[str, str]]
    prompt: str


EvalSetting = Union[ZeroShot, OneShot, FewShot]


def build_prompt(setting: EvalSetting) -&gt; str:
    if isinstance(setting, ZeroShot):
        return f&quot;{setting.instruction}\n{setting.prompt}&quot;

    if isinstance(setting, OneShot):
        example_input, example_output = setting.example
        return f&quot;{setting.instruction}\n{example_input} {example_output}\n{setting.prompt}&quot;

    lines = [setting.instruction]
    lines.extend(f&quot;{example_input} {example_output}&quot; for example_input, example_output in setting.examples)
    lines.append(setting.prompt)
    return &quot;\n&quot;.join(lines)
```

论文把这种能力叫做**上下文学习**：模型在预训练时，从海量文本中隐式地学到了各种任务的模式；推理时，示例被拼接进上下文，模型在前向传播的过程中「识别」出当前任务是什么，然后完成它。论文用「元学习」来描述这个过程：预训练是外循环，上下文学习是内循环。

这和微调的区别是根本性的。微调修改模型参数来适应任务，上下文学习不修改任何东西：同一个模型，同一组权重，只靠输入文本的不同，就能切换任务。

## 3. 模型架构与规模

GPT-3 的架构本身没有新发明。它和 GPT-2 一样，就是 [Transformer](/zh-CN/posts/attention-is-all-you-need/) 的解码器部分，一层层堆起来。改动只有一处：在 Transformer 层中交替使用稠密注意力和局部带状稀疏注意力（来自 Sparse Transformer）。

真正不同的是规模。论文训练了 8 个不同大小的模型，参数量跨越三个数量级：

| 模型 | 参数量 | 层数 | 隐藏维度 | 注意力头数 |
|------|--------|------|----------|-----------|
| GPT-3 Small | 1.25 亿 | 12 | 768 | 12 |
| GPT-3 Medium | 3.5 亿 | 24 | 1024 | 16 |
| GPT-3 Large | 7.6 亿 | 24 | 1536 | 16 |
| GPT-3 XL | 13 亿 | 24 | 2048 | 24 |
| GPT-3 2.7B | 27 亿 | 32 | 2560 | 32 |
| GPT-3 6.7B | 67 亿 | 32 | 4096 | 32 |
| GPT-3 13B | 130 亿 | 40 | 5140 | 40 |
| **GPT-3 175B** | **1750 亿** | **96** | **12288** | **96** |

1750 亿参数，96 层，96 个注意力头，隐藏维度 12288。上下文窗口 2048 个词元。这个规模在当时是前所未见的：比 GPT-2（15 亿参数）大了 100 多倍。

```python showLanguage
from dataclasses import dataclass


@dataclass(frozen=True)
class GPT3Config:
    n_params: int
    n_layers: int
    d_model: int
    n_heads: int
    d_head: int
    d_ff: int
    n_ctx: int


def gpt3_175b() -&gt; GPT3Config:
    return GPT3Config(
        n_params=175_000_000_000,
        n_layers=96,
        d_model=12_288,
        n_heads=96,
        d_head=128,
        d_ff=49_152,
        n_ctx=2_048,
    )
```

论文训练这些模型的目的很明确：验证缩放定律（scaling laws）。之前 Kaplan 等人的研究（就是这篇论文的共同作者之一）已经表明，语言模型的损失和参数量之间存在平滑的幂律关系。GPT-3 把这个假设推到了 1750 亿参数的规模，看看上下文学习能力是否也遵循同样的规律。

答案是肯定的：模型越大，少样本学习的提升越陡。零样本性能随模型规模稳步上升，少样本性能的上升速度更快。这意味着大模型不只是「更准」，它们在利用上下文信息的效率上也更高。

## 4. 训练数据

GPT-3 在大约 3000 亿个词元上训练，数据来自五个来源：

| 数据集 | 词元数 | 训练占比 |
|--------|--------|----------|
| Common Crawl（过滤后） | 4100 亿 | 约 60% |
| WebText2 | 190 亿 | 约 22% |
| Books1 | 120 亿 | 约 8% |
| Books2 | 550 亿 | 约 8% |
| 英文 Wikipedia | 30 亿 | 约 3% |

注意一个关键细节：数据集的采样比例和它们的大小不成正比。质量更高的数据集（WebText2、Books、Wikipedia）被过采样了：WebText2 在训练中被看了 2.9 遍，Wikipedia 被看了 3.4 遍，而 Common Crawl 连一遍都没看完（0.44 遍）。论文有意用少量过拟合的代价，换取更高质量的训练信号。

Common Crawl 的原始数据有 45TB，经过三步处理：（1）基于与高质量参考语料的相似度做过滤；（2）文档级模糊去重；（3）混入已知的高质量数据集来增加多样性。过滤后剩下 570GB，约 4100 亿词元。

所有模型在 V100 GPU 上训练，使用微软提供的高带宽集群。

## 5. 实验结果

论文在二十多个数据集上做了评估，覆盖 9 大类任务。以下是几个关键结果。

**语言建模**：在 Penn Tree Bank 上，GPT-3 少样本困惑度（perplexity，衡量模型对文本的「意外程度」，越低越好）达到 20.50，刷新了当时的记录。在 LAMBADA（需要根据长距离上下文预测最后一个词）上，零样本准确率 76.2%，少样本 86.4%，大幅超过之前的最好结果。

**翻译**：GPT-3 从未被专门训练过翻译，但在法语→英语翻译上，少样本 BLEU 分数达到 32.6，超过了无监督神经机器翻译的最好结果。不过英语→法语方向（25.2 BLEU）和微调模型的差距仍然很大。一个有趣的发现：GPT-3 翻译成英语的能力明显强于从英语翻译出去，这和训练数据以英语为主有直接关系。

**闭卷问答**：在 TriviaQA 上，少样本准确率（exact match）71.2%，超过了同一闭卷设置下经过微调的模型。模型不看任何参考文档，纯靠参数里存储的知识回答问题。

**SuperGLUE**：在这个综合基准上，GPT-3 的少样本表现已经接近一些经过微调的强基线，但仍落后于当时最强的专门微调系统。

**合成任务**：论文还设计了一些专门测试上下文学习能力的新任务。比如给模型几个「造新词」的例子（定义一个不存在的词，然后用它造句），GPT-3 能正确地学会并使用这个新词。再比如三位数加法，少样本准确率接近 100%（两位数加法也几乎完美），但四五位数时急剧下降。

```python showLanguage
from typing import Callable, Protocol


class AutoregressiveModel(Protocol):
    def forward(self, tokens: list[int]) -&gt; list[list[float]]:
        ...


def in_context_learning(
    model: AutoregressiveModel,
    examples: list[tuple[str, str]],
    query: str,
    tokenize: Callable[[str], list[int]],
    decode: Callable[[list[int]], str],
    sample_from: Callable[[list[float]], int],
    eos_token: int,
) -&gt; str:
    prompt_lines = [f&quot;{example_input} {example_output}&quot; for example_input, example_output in examples]
    prompt_lines.append(query)
    prompt = &quot;\n&quot;.join(prompt_lines)

    context = tokenize(prompt)
    output_tokens: list[int] = []

    while True:
        logits = model.forward(context)
        next_token = sample_from(logits[-1])
        if next_token == eos_token:
            break
        output_tokens.append(next_token)
        context.append(next_token)

    return decode(output_tokens)
```

## 6. 数据污染问题

论文在第四章花了大量篇幅讨论一个棘手的问题：训练数据和测试数据的重叠。

GPT-3 的训练数据包含大量互联网文本，而很多测试基准的内容也在互联网上公开存在。这意味着模型可能在训练时就「看过」了测试题。论文团队尝试在训练前移除这些重叠，但由于一个处理流程中的 bug，部分重叠没有被完全清除。而重新训练一遍的成本太高，不现实。

他们的做法是：为每个基准构建一个「干净子集」（移除所有和训练数据有 13-gram 重叠的样本），然后对比模型在完整集和干净子集上的表现。结论是：大多数基准上，污染对结果的影响很小。但 PIQA 和 Winograd 两个数据集存在可疑的表现下降，论文对这些结果加了星号标注。

这种诚实在当时相当罕见。多数论文对数据污染问题避而不谈。GPT-3 不仅主动调查，还开发了系统化的检测工具。这本身就是对后续研究的一个贡献。

## 7. 局限性

论文在第五章对自身局限性的讨论相当坦率。

**文本连贯性**：GPT-3 在文档级别仍然会出现语义重复、自相矛盾、甚至生成无意义句子的情况。生成质量虽然比 GPT-2 好了很多，但长文本的连贯性仍然不够。

**常识物理**：GPT-3 对「把奶酪放进冰箱，它会融化吗？」这类常识物理问题表现不佳。它能处理语言层面的推理，但对物理世界的理解仍然是肤浅的。

**单向性的代价**：作为自回归模型，GPT-3 只能从左往右看。论文承认，在需要双向上下文的任务上（比如判断两个句子里同一个词的含义是否相同），GPT-3 的少样本表现不如经过微调的双向模型。这说明在 GPT-3 的自回归设定下，这类任务并不是它的强项；单向建模目标本身会带来结构性偏好。

**采样效率**：GPT-3 在预训练阶段看了约 3000 亿个词元，远超人类一生接触的文本量。论文明确指出，即使少样本学习在推理时很高效，预训练的数据需求仍然巨大。

**推理成本**：1750 亿参数的模型，推理成本高且不方便部署。论文提到蒸馏（distillation，用大模型的输出来训练小模型）是一个可能的方向，但在千亿参数量级上还没有尝试过。

## 8. 社会影响

论文用了整整一个章节（第六章）讨论社会影响，涵盖三个方面。

**滥用风险**：GPT-3 生成的新闻文章，人类评估者的识别准确率接近随机猜测（约 52%）。模型越强，生成的虚假文本越难辨别。论文团队表示已经在监控论坛和聊天群，追踪恶意使用的趋势。

**偏见**：论文用大量实验测试了 GPT-3 在性别、种族和宗教方面的偏见。例如，在职业-性别关联测试中，GPT-3 更倾向于将「nurse」和女性关联、将「banker」和男性关联。在宗教-情感关联中，「Islam」更多地与暴力相关词共现。论文承认这些偏见来自训练数据，但没有给出解决方案。

**能源消耗**：训练 GPT-3 需要大量算力，论文引用了估算数据但没有公布具体的能耗数字。不过论文指出，一旦训练完成，模型可以被多次使用到不同任务上，比为每个任务单独训练模型更节能。

## 9. 这篇论文改变了什么问题

GPT-3 的钉子句是：**任务适配可以从参数更新搬进上下文。**

这篇论文真正改写的不是“模型更大所以更强”这件事，而是人和模型的接口。过去要让模型做新任务，通常要收集数据、微调参数、保存一个新版本。GPT-3 展示了另一条路：权重不动，任务说明、示例和输入一起进入上下文，模型在推理时完成临时适配。

这不是微调的终结。它只是把一部分适配成本从训练阶段挪到了上下文设计阶段。Prompt 不再只是输入文本，而变成一种轻量任务程序。上下文窗口也不再只是容量参数，而是临时工作区。

下次看大模型，不要只问参数有多少。更该问：这个系统把任务适配放在哪里？放在权重里、上下文里，还是外部工具和工作流里？这个问题比模型大小更接近产品形态。

---

**论文共读系列**

- [《Sequence to Sequence Learning with Neural Networks》](/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/)（使用神经网络进行序列到序列学习） — 编码器-解码器范式的确立
- [《Neural Machine Translation by Jointly Learning to Align and Translate》](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)（通过联合学习对齐与翻译实现神经机器翻译） — 注意力机制的起源
- [《Attention Is All You Need》](/zh-CN/posts/attention-is-all-you-need/)（注意力就是你所需要的全部） — 注意力成为主角，Transformer 的诞生
- [《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》](/zh-CN/posts/bert/)（BERT：用于语言理解的深度双向 Transformer 预训练） — 预训练范式的确立
- [《Scaling Laws for Neural Language Models》](/zh-CN/posts/scaling-laws-for-neural-language-models/)（神经语言模型的缩放定律） — 规模的数学：为什么更大的模型可预测地更好
- [《Training Compute-Optimal Large Language Models》](/zh-CN/posts/training-compute-optimal-large-language-models/)（训练计算最优的大语言模型） — 怎样花算力最划算</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>gpt-3</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>OpenClaw 生态：从开源项目到 AI 助手平台 🦞</title><link>https://justinhuangai.github.io/zh-CN/posts/openclaw-ecosystem/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/openclaw-ecosystem/</guid><description>重新看 OpenClaw：星标不是生态，能持续降低供给和使用摩擦的结构才是生态。</description><pubDate>Tue, 03 Feb 2026 08:09:32 GMT</pubDate><content:encoded>![OpenClaw](/images/openclaw-logo-text-dark.webp)

星标不是生态。

生态的第一性问题是摩擦：开发者供给一个能力有多难，用户发现和使用一个能力有多难，系统把这两边连起来有多稳。OpenClaw 的可看之处不在于它热过，而在于它开始围绕同一个 Agent 运行时长出分工结构。

## 0. 先认几个词

- `生态`：不只是一个仓库，而是一组围绕同一核心能力分工协作的产品和工具。
- `运行时 / runtime`：Agent 长期跑着、调度工具、管理上下文和执行任务的核心进程。
- `技能市场`：让 Agent 发现和安装新能力的入口。
- `工作流引擎`：把高频多步骤任务封装成可重复执行的流程。
- `飞轮`：供给、使用、反馈互相强化的增长回路。

## 1. 先看约束，不先看热度

Peter Steinberger 2011 年创办 PSPDFKit，长期做 PDF 底层技术，服务过 Apple、Dropbox 等客户。后来他离开一线开发，又在 2025 年重新回到 AI 产品原型上。

Clawdbot 最早是在这个背景下出现的：大约一小时提示词生成项目雏形，11 月发布，名字向 Anthropic 的 Claude 致意。2026 年 1 月底，Anthropic 发来商标警告，项目三天内从 Clawdbot 改成 Moltbot，再改成 OpenClaw。改名事件带来了 48 小时 34,000 星的注意力。

但热度只能解释流量，解释不了生态。生态要看结构。

## 2. 生态不是仓库数量，是分工是否成立

OpenClaw 已经不只是一个运行时仓库。它开始拆出不同层：

| 组件 | 角色 | 信号 |
|------|------|------|
| **OpenClaw** | 核心 Agent 运行时 | 140k+ stars |
| **ClawHub** | 技能市场 | 5.4k stars |
| **Lobster** | 工作流引擎 | ~800 stars |
| **acpx** | 无头命令行工具 | ~780 stars |
| **openclaw-ansible** | 自动化部署 | ~490 stars |
| **nix-openclaw** | Nix 声明式配置 | ~530 stars |

这张表真正要看的不是数字，而是边界。运行时、技能市场、工作流、部署工具没有全部塞回一个大仓库，而是围绕 Agent 生命周期拆开。

一个平台要长出来，必须同时降低两种摩擦：供给侧创建能力的摩擦，使用侧发现能力的摩擦。OpenClaw 的生态判断也应该从这两点开始。

## 3. 三个结构信号

### 渠道覆盖：降低使用摩擦

WhatsApp、Telegram、Slack、Discord、Signal、iMessage、飞书、LINE、Matrix 等二十多个平台接入，表面上是渠道多，底层是入口位置变了。

用户不需要为了 AI 助手切到一个新 App。Agent 进入已有聊天渠道，任务从原本的对话场景里出现。采用率的关键不是“功能多”，而是第一次使用的阻力足够低。

### ClawHub：降低能力发现摩擦

技能市场用向量搜索做语义匹配。用户不用翻目录，只要描述“我要一个能发邮件的技能”，系统就能匹配候选。

这解决的是发现问题，不是供给问题。一个技能市场能不能成立，仍取决于高质量第三方技能是否持续出现，是否有人维护，是否有审核和评价。现在只能说发现入口有了，供给飞轮还没有被证明。

### Lobster：降低重复规划摩擦

Agent 执行多步骤任务时，最大的隐性成本往往不是单步工具调用，而是每次都重新规划。

Lobster 把高频操作封装成可复用工作流：流程一旦被验证，就不必每次交给模型现场组织。它还把关键步骤放进审批关卡，减少 Agent 一路自动执行带来的权限风险。

这类工具的价值不在“自动化更多”，而在把可重复任务从临场推理变成可检查流程。

## 4. 风险也来自结构

**安全是生态前提。** Kaspersky 审计发现 512 个漏洞，其中 8 个严重级别。高权限 Agent、第三方技能、二十多个入口放在一起，攻击面天然会扩大。安全问题不是普通技术债，而是信任债。

**商业模式还没有闭合。** MIT 协议、无订阅、用户自带 API key，这些选择降低了采用门槛，也让持续运营成本更难回收。开源热度不能自动转化成维护预算。

**贡献结构仍要观察。** 如果路线、产品判断和核心实现长期依赖少数人，生态厚度就会被单点依赖限制。平台和明星项目的区别，在于非创始人贡献能否稳定增长。

## 5. 怎么继续看

OpenClaw 的问题不再是“有没有热度”。热度已经出现过。

接下来应该看四个指标：

1. 安全默认值有没有收紧：配对、白名单、沙箱、审批是否从可选变成默认。
2. 技能生态有没有自发供给：高质量技能是否持续发布、安装、更新和审核。
3. 非创始人贡献占比有没有上升：生态是否能离开单点驱动。
4. 治理结构有没有清晰：维护权、审核权、路线决策能否被社区理解和继承。

星标证明不了生态。能持续降低供给摩擦和使用摩擦的结构，才证明生态开始存在。</content:encoded><category>OpenClaw</category><category>AI</category><category>open-source</category><category>openclaw</category></item><item><title>《BERT》：语言理解预训练范式的确立</title><link>https://justinhuangai.github.io/zh-CN/posts/bert/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/bert/</guid><description>预训练范式的确立，附真实 Python 代码</description><pubDate>Sat, 31 Jan 2026 08:52:21 GMT</pubDate><content:encoded>BERT 要解决的不是“再造一个更深的 Transformer”，而是 NLP 任务长期被拆碎的问题。问答、分类、序列标注各有一套模型和数据接口，模型学到的语言知识很难被统一复用。

[《BERT》](/papers/1810.04805v2.pdf) 的关键不只是双向 Transformer，而是把语言理解任务统一成预训练表示的复用问题。先学一套通用表示，再用很薄的任务层去适配不同问题；这个转向比模型结构本身更重要。

## 0. 先认几个词

如果你对大模型训练流程还不熟，可以先记住这篇论文最关键的几个词：

- `Transformer`：BERT 用的基础架构。你可以先把它理解成一台能同时结合左右上下文来处理句子的机器。
- `预训练`：先在海量通用文本上学语言本身，而不是一上来就做具体任务。
- `微调`：把预训练得到的能力，再稍微调整到某个具体任务上。
- `双向`：预测一个位置时，不只看左边，也看右边。
- `MLM / 遮蔽语言模型`：故意遮住一部分词，让模型根据上下文把它们猜出来。
- `NSP / 下一句预测`：让模型判断两个句子是不是前后相连。

## 1. 要解决什么问题

2018 年，NLP 领域有一个尴尬的现状：每个任务都需要从头设计专门的模型架构。做问答要一套模型，做情感分析要另一套，做命名实体识别又要一套。每个任务的标注数据都不多，训练出来的模型也很难迁移到其他任务。

当时已经有人尝试过预训练的思路。ELMo 用双向 LSTM 学习上下文表示，但它只是把预训练的特征「拼」到下游模型上，架构本身还是任务专用的。OpenAI GPT 用 Transformer 做预训练再微调，但它只能从左往右看（单向），每个词只能关注它前面的词，看不到后面的。

论文认为，单向语言模型在需要深度双向上下文的语言理解任务上存在明显限制。比如：

&gt; &quot;他拿起了 _____ ，开始演奏。&quot;

如果只看左边（&quot;他拿起了&quot;），填空的答案可能是任何东西。但看到右边（&quot;开始演奏&quot;），你立刻知道是某种乐器。对很多语言理解任务来说，双向上下文天然更有利。

## 2. 核心想法：遮住一些词，让模型猜

BERT 的解法很直觉：既然双向语言模型没法用传统方式训练（因为每个词会间接「看到自己」），那就换个训练目标。

**遮蔽语言模型（Masked Language Model，MLM）**：随机遮住输入中 15% 的词：具体做法是把它们替换成一个特殊标记 \[MASK\]：然后让模型根据上下文猜出被遮住的词。这个想法来自心理学中的完形填空（Cloze task，1953 年 Taylor 提出），就像上面那道填空题一样。

遮住之后，模型必须同时利用左边和右边的上下文来预测，双向理解就自然产生了。

但直接把所有被选中的词替换成 \[MASK\] 标记会引入一个问题：微调时输入里不会出现 \[MASK\]，预训练和微调之间产生了不匹配。论文的解决方案：被选中的 15% 的词里，80% 替换成 \[MASK\]，10% 替换成随机词，10% 保持不变。这样模型不能只靠「看到 \[MASK\] 就知道要预测」，而是必须对每个位置都保持理解能力。

```python showLanguage
import random
from typing import Optional, Sequence


def mask_tokens(
    tokens: Sequence[str],
    mask_prob: float = 0.15,
    vocab: Optional[Sequence[str]] = None,
) -&gt; tuple[list[str], list[int], list[str]]:
    if vocab is None:
        vocab = tokens

    masked = list(tokens)
    positions: list[int] = []
    labels: list[str] = []

    for i, token in enumerate(tokens):
        if random.random() &lt; mask_prob:
            positions.append(i)
            labels.append(token)

            r = random.random()
            if r &lt; 0.8:
                masked[i] = &quot;[MASK]&quot;
            elif r &lt; 0.9:
                masked[i] = random.choice(vocab)

    return masked, positions, labels
```

## 3. 第二个预训练任务：下一句预测

很多 NLP 任务（比如问答、自然语言推理）需要理解两个句子之间的关系，但语言模型本身不直接建模这种关系。

论文加了第二个预训练任务：**下一句预测（Next Sentence Prediction，NSP）**。给模型两个句子 A 和 B，50% 的情况下 B 是 A 的真实下一句，50% 的情况下 B 是从语料库里随机抽的。模型要判断 B 是不是 A 的下一句。

这个任务的设计很简单，但论文的消融实验（ablation study，逐一去掉某个组件看效果变化）显示，去掉 NSP 会明显降低问答和自然语言推理任务的表现；不过后来也有工作（如 RoBERTa）对 NSP 的必要性提出了不同结论。

```python showLanguage
from dataclasses import dataclass


@dataclass
class PretrainingExample:
    tokens: list[str]
    segment_ids: list[int]
    masked_positions: list[int]
    masked_labels: list[str]
    is_next: bool
```

## 4. 模型架构

BERT 的架构其实没有什么新发明。它就是 [Transformer](/zh-CN/posts/attention-is-all-you-need/) 的编码器部分，一层层堆起来。

论文给出了两个规格：

- **BERT_BASE**：12 层，隐藏维度 768，12 个注意力头，参数量 1.1 亿
- **BERT_LARGE**：24 层，隐藏维度 1024，16 个注意力头，参数量 3.4 亿

BERT_BASE 的参数量和 OpenAI GPT 差不多，方便直接对比。两者最关键的区别只有一个：GPT 用的是单向注意力（每个词只能看左边），BERT 用的是双向注意力（每个词能看到所有位置）。

输入的表示由三部分相加构成：

- **词嵌入（Token Embedding）**：WordPiece 分词，词表 30,000
- **段嵌入（Segment Embedding）**：标记这个词属于句子 A 还是句子 B
- **位置嵌入（Position Embedding）**：告诉模型词的位置（BERT 用的是学习得到的位置编码，不是正弦余弦）

每个输入序列的开头都加一个特殊标记 \[CLS\]，它在最后一层的隐藏状态被用来做句子级别的分类（比如 NSP、情感分析）。两个句子之间用 \[SEP\] 分隔。

```python showLanguage
import torch
from torch import nn


class BertEmbeddings(nn.Module):
    def __init__(
        self,
        vocab_size: int,
        hidden_size: int,
        max_positions: int,
        type_vocab_size: int = 2,
        dropout: float = 0.1,
    ) -&gt; None:
        super().__init__()
        self.token_embedding = nn.Embedding(vocab_size, hidden_size)
        self.segment_embedding = nn.Embedding(type_vocab_size, hidden_size)
        self.position_embedding = nn.Embedding(max_positions, hidden_size)
        self.layer_norm = nn.LayerNorm(hidden_size, eps=1e-12)
        self.dropout = nn.Dropout(dropout)

    def forward(
        self,
        token_ids: torch.Tensor,
        segment_ids: torch.Tensor,
    ) -&gt; torch.Tensor:
        position_ids = torch.arange(token_ids.size(1), device=token_ids.device).unsqueeze(0)
        position_ids = position_ids.expand_as(token_ids)
        embeddings = (
            self.token_embedding(token_ids)
            + self.segment_embedding(segment_ids)
            + self.position_embedding(position_ids)
        )
        embeddings = self.layer_norm(embeddings)
        return self.dropout(embeddings)


class BertModel(nn.Module):
    def __init__(
        self,
        vocab_size: int,
        hidden_size: int = 768,
        max_positions: int = 512,
        num_layers: int = 12,
        num_heads: int = 12,
        dropout: float = 0.1,
    ) -&gt; None:
        super().__init__()
        self.embeddings = BertEmbeddings(vocab_size, hidden_size, max_positions, dropout=dropout)
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=hidden_size,
            nhead=num_heads,
            dim_feedforward=4 * hidden_size,
            dropout=dropout,
            activation=&quot;gelu&quot;,
            batch_first=True,
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)

    def forward(
        self,
        token_ids: torch.Tensor,
        segment_ids: torch.Tensor,
    ) -&gt; torch.Tensor:
        hidden = self.embeddings(token_ids, segment_ids)
        return self.encoder(hidden)
```

## 5. 微调：一个模型适配所有任务

BERT 最优雅的地方在于微调的简单性。预训练完成后，不管什么下游任务，做法几乎一样：在 BERT 上面加一层任务相关的输出层，然后用少量标注数据微调所有参数。

- **文本分类**（情感分析、自然语言推理）：取 \[CLS\] 位置的输出向量，接一个线性分类器
- **问答**（给一段文章，找出答案的起止位置）：对每个词的输出向量做两次线性变换，分别预测答案的开始和结束位置
- **序列标注**（命名实体识别）：对每个词的输出向量接一个分类器，逐词预测标签

预训练可能需要几天，但微调通常只要几十分钟到几小时（单块 TPU 上大部分任务不超过 1 小时）。这个效率差异是「预训练 + 微调」范式的核心吸引力。

## 6. 实验结果

论文在 11 个 NLP 任务上做了实验，全部刷新了当时的记录。

**GLUE 基准**（通用语言理解评估，包含 8 个子任务）：
- BERT_LARGE 平均分 80.5%，比之前最好的 OpenAI GPT 高出 7.7 个百分点
- 在最大的子任务 MNLI 上提升了 4.6%

**SQuAD v1.1**（阅读理解问答，Test F1）：
- BERT_LARGE 单模型 + TriviaQA 数据：F1 91.8，超过人类表现（91.2）
- BERT_LARGE 集成模型 + TriviaQA 数据：F1 93.2

**SQuAD v2.0**（包含无法回答的问题）：
- F1 达到 83.1，比之前最好的系统高出 5.1 个百分点

**SWAG**（常识推理）：
- 准确率 86.3%，比 OpenAI GPT 高出 8.3 个百分点

论文还做了模型大小的消融实验，发现一个重要结论：更大的模型在所有任务上都更好，即使在标注数据很少（只有 3,600 条）的任务上也是如此。这和当时的直觉（小数据集容易过拟合大模型）不太一样，说明预训练提供的知识可以有效缓解小数据集的过拟合（模型把训练数据「死记硬背」，对新数据表现差）风险。

## 7. 训练细节

**预训练数据**：BooksCorpus（8 亿词）+ 英文 Wikipedia（25 亿词），只使用文本段落，去掉了列表、表格和标题。论文强调必须用文档级语料而不是打乱的句子级语料，这样才能提取长距离的上下文关系。

**分词**：WordPiece，词表大小 30,000。WordPiece 会把不常见的词拆成更小的子词单元，比如 &quot;playing&quot; 可能被拆成 &quot;play&quot; + &quot;##ing&quot;。

**优化器**：Adam，学习率 1e-4，前 10,000 步线性热身，然后线性衰减。批次大小 256 个序列，最大序列长度 512。

**硬件**：BERT_BASE 在 4 块 Cloud TPU（16 块 TPU 芯片）上训练 4 天。BERT_LARGE 在 16 块 Cloud TPU（64 块 TPU 芯片）上训练 4 天。

**Dropout**：所有层的 dropout 率为 0.1。激活函数用的是 GELU（Gaussian Error Linear Unit），而不是 Transformer 原版的 ReLU。

## 8. 这篇论文改变了什么问题

BERT 的钉子句是：**语言理解可以先学成一种可复用表示，再分发给具体任务。**

它的关键不只是 Transformer 编码器，也不只是“双向”。真正的动作是用 MLM 和 NSP 把无标注文本变成预训练信号，再让分类、问答、序列标注这些任务共享同一套输入格式和表示底座。

这改变了 NLP 的工程单位。过去每个任务都像一个单独项目；BERT 之后，许多任务变成同一份预训练表示上的薄适配层。模型不再从每个任务的标注数据里重新学语言，而是先拥有一套通用语言底座，再学任务边界。

下次看一个理解模型，不要只问它是不是双向。更该问：它学到的表示能否被复用？任务差异是被塞进模型主体，还是被压缩到一个轻量接口里？

---

**论文共读系列**

- [《Sequence to Sequence Learning with Neural Networks》](/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/)（使用神经网络进行序列到序列学习） — 编码器-解码器范式的确立
- [《Neural Machine Translation by Jointly Learning to Align and Translate》](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)（通过联合学习对齐与翻译实现神经机器翻译） — 注意力机制的起源
- [《Attention Is All You Need》](/zh-CN/posts/attention-is-all-you-need/)（注意力就是你所需要的全部） — 注意力成为主角，Transformer 的诞生
- [《Scaling Laws for Neural Language Models》](/zh-CN/posts/scaling-laws-for-neural-language-models/)（神经语言模型的缩放定律） — 规模的数学：为什么更大的模型可预测地更好
- [《Language Models are Few-Shot Learners》](/zh-CN/posts/language-models-are-few-shot-learners/)（语言模型是少样本学习者） — 更大的模型，更善于从上下文中诱发能力
- [《Training Compute-Optimal Large Language Models》](/zh-CN/posts/training-compute-optimal-large-language-models/)（训练计算最优的大语言模型） — 怎样花算力最划算</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>bert</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>《Sequence to Sequence Learning with Neural Networks》：编码器-解码器范式的起点</title><link>https://justinhuangai.github.io/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/</guid><description>编码器-解码器范式的确立，附真实 Python 代码</description><pubDate>Sat, 24 Jan 2026 08:41:08 GMT</pubDate><content:encoded>Seq2Seq 的底层矛盾很朴素：输入和输出长度都不固定，传统翻译流水线能处理，却很难端到端优化。[《Sequence to Sequence Learning with Neural Networks》](/papers/1409.3215v3.pdf) 把问题改写成两个接口：一个网络读完整段输入，另一个网络逐步生成输出。

这篇论文的价值不在于把机器翻译一次性解决了，而在于证明端到端序列映射可行。它的弱点也同样清楚：所有信息都要挤过一个固定长度向量。后来的注意力机制和 Transformer，都是从这个瓶颈往外长出来的。

## 0. 先认几个词

如果你没有机器学习背景，可以先按这篇论文的工作流，记住下面几个词：

- `Seq2Seq / 序列到序列`：把一段输入序列直接变成另一段输出序列，比如把英文句子变成法文句子。
- `Encoder / 编码器`：负责把输入从头到尾读完。
- `Decoder / 解码器`：负责把输出一个词一个词写出来。
- `RNN / 循环神经网络`：一种只能按顺序处理文本的旧架构。
- `LSTM`：RNN 的改良版，更擅长在长句子里记住前面的信息。
- `向量 / vector`：你可以先把它理解成“用一串数字压缩出来的一份摘要”。

## 1. 要解决什么问题

2014 年，深度神经网络已经在图像识别等任务上取得突破，但像机器翻译这种「直接把一段可变长序列映射到另一段可变长序列」的任务，神经网络还不擅长。

一句英语可能是 5 个词，翻译成法语变成 7 个词。输入和输出的长度不同，而且没有简单的一一对应关系。

传统的解决方案是把大量人工设计的规则和统计特征拼在一起，形成一个复杂的翻译流水线（统计机器翻译，SMT）。它能用，但每个组件都要单独调参，而且很难整体优化。

论文提出了一个更简洁的思路：能不能用一个端到端的神经网络，直接从源语言序列映射到目标语言序列？

## 2. 核心架构：编码器-解码器

论文的方法可以用一句话概括：**一个 LSTM 读，另一个 LSTM 写。**

LSTM（Long Short-Term Memory，长短期记忆网络）是一种特殊的 RNN，专门设计来处理长距离依赖问题。普通 RNN 在序列很长时容易「遗忘」前面的内容，LSTM 通过引入门控机制（决定哪些信息保留、哪些丢弃）来缓解这个问题。

具体流程：

1. **编码器**（一个 4 层深度 LSTM）从头到尾读完源句子，把整个句子压缩成一组固定长度的最终状态，交给解码器作为起点
2. **解码器**（另一个 4 层深度 LSTM）以这个向量为起点，一个词一个词地生成目标语言的翻译，直到输出结束符号 \&lt;EOS\&gt;

论文给出的概率公式：

$$
p(y_1, \ldots, y_{T&apos;} \mid x_1, \ldots, x_T) = \prod_t p(y_t \mid v, y_1, \ldots, y_{t-1})
$$

翻译成人话：给定源句子 x，生成目标句子 y 的概率，等于每一步生成下一个词的概率连乘起来。每一步的预测都依赖两样东西：编码器压缩出来的向量 v，以及之前已经生成的所有词。

```python showLanguage
import torch
from torch import nn


class Seq2Seq(nn.Module):
    def __init__(self, vocab_size: int, hidden_size: int) -&gt; None:
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, hidden_size)
        self.encoder = nn.LSTM(hidden_size, hidden_size, num_layers=4, batch_first=True)
        self.decoder = nn.LSTM(hidden_size, hidden_size, num_layers=4, batch_first=True)
        self.output_proj = nn.Linear(hidden_size, vocab_size)

    def encode(
        self,
        source_tokens: torch.Tensor,
    ) -&gt; tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
        embedded = self.embedding(source_tokens)
        outputs, state = self.encoder(embedded)
        return outputs, state

    def decode(
        self,
        encoder_state: tuple[torch.Tensor, torch.Tensor],
        max_steps: int,
        bos_token_id: int,
        eos_token_id: int,
    ) -&gt; list[int]:
        prev_token = torch.tensor([[bos_token_id]], dtype=torch.long, device=encoder_state[0].device)
        state = encoder_state
        generated: list[int] = []

        for _ in range(max_steps):
            embedded = self.embedding(prev_token)
            output, state = self.decoder(embedded, state)
            logits = self.output_proj(output[:, -1, :])
            next_token_id = int(logits.argmax(dim=-1).item())
            if next_token_id == eos_token_id:
                break
            generated.append(next_token_id)
            prev_token = torch.tensor([[next_token_id]], dtype=torch.long, device=logits.device)

        return generated
```

架构本身不复杂。论文的贡献不在于发明了某个新组件，而在于证明了这个简单的框架真的能用，而且效果好到能和精心调校的传统系统竞争。

## 3. 三个关键设计决策

论文在实验中发现了三个对性能影响很大的设计选择：

**第一，用两个独立的 LSTM。** 编码器和解码器不共享参数。这样做稍微增加了参数量，但让模型能更好地分别处理源语言和目标语言的特性。论文提到这也让同时训练多个语言对成为可能。

**第二，用深层 LSTM。** 论文用了 4 层 LSTM，每多一层，困惑度降低近 10%。浅层 LSTM（1-2 层）的效果明显更差。深度给了模型更大的表示空间。

**第三，把源句子倒过来读。** 这是论文最出人意料的发现。把源句子 &quot;a, b, c&quot; 反转成 &quot;c, b, a&quot; 再喂给编码器，BLEU 分数从 25.9 跳到 30.6，提升了将近 5 分。

为什么反转有效？论文的解释是：正常顺序下，源句子第一个词离目标句子第一个词很远（中间隔了整个源句子）。反转之后，源句子的前几个词和目标句子的前几个词在时间上靠得很近，给梯度（模型用来调整参数的信号）创造了更多的「短距离依赖」，让优化变得更容易。

```python showLanguage
import torch


def reverse_source(source_tokens: list[int]) -&gt; list[int]:
    return list(reversed(source_tokens))


source_sentence = [11, 23, 37, 42]
reversed_source = reverse_source(source_sentence)
source_tensor = torch.tensor([reversed_source], dtype=torch.long)
```

这个 trick 简单到几乎不像是正经的研究贡献，但它确实有效，而且揭示了一个深层问题：RNN 对序列里元素之间的距离很敏感，距离越近越容易学。这个问题后来被注意力机制从根本上解决了。

## 4. 实验结果

论文在 WMT &apos;14 英法翻译任务上做了实验。

关键数字：
- **单个反转 LSTM**，beam size 12：30.59 BLEU
- **5 个反转 LSTM 的集成**，beam size 2：34.50 BLEU
- **5 个反转 LSTM 的集成**，beam size 12：**34.81 BLEU**
- **传统短语翻译系统**（Moses baseline）：33.30 BLEU

在论文报告的实验设置下，5 个 LSTM 的集成以 34.81 分超过了传统短语翻译系统的 33.30 分。考虑到 LSTM 的词表只有 8 万词（遇到词表外的词只能输出 UNK），而传统系统的词表几乎不受限，这个结果很有说服力。

论文还用 LSTM 对传统系统的 1000-best 候选列表做重排序，BLEU 分数进一步提升到 36.5，接近当时的最佳公开结果（37.0）。

另一个值得注意的发现：相比当时其他神经方法，LSTM 在长句子上的性能退化没那么严重。这和当时其他研究者报告的长句子性能急剧下降形成了对比，论文将此归功于反转源句子的策略。

## 5. 模型的「理解力」

论文还做了一个有趣的可视化实验。把不同句子输入编码器，取出最终的隐藏状态向量，用 PCA 降维到二维平面上画出来。

结果显示：
- 意思相近的句子在向量空间里聚在一起
- 主动语态和被动语态的句子（&quot;I gave her a card&quot; vs &quot;I was given a card by her&quot;）落在相近的位置
- 词序不同但意思相同的句子也能被正确聚类

这至少说明，编码器学到的表示不只是简单的词袋统计（把词混在一起不管顺序），而是包含了相当多的句法和语义信息。

## 6. 训练细节

**模型规格**：4 层 LSTM，每层 1000 个单元，词嵌入维度 1000，总参数量 3.84 亿。其中 6400 万是纯循环连接参数。

**硬件**：8 块 GPU。每层 LSTM 分配一块 GPU，剩余 4 块 GPU 用来并行化 softmax（因为词表有 8 万个词，softmax 计算量很大）。训练约 10 天。

**优化器**：SGD，不带动量，初始学习率 0.7。训练 5 个 epoch 之后每半个 epoch 将学习率减半，总共训练 7.5 个 epoch。

**梯度裁剪**：当梯度的 L2 范数超过阈值 5 时，按比例缩小。这是为了防止梯度爆炸（梯度值突然变得极大，导致参数更新失控）。

**批次优化**：把长度相近的句子放在同一个批次里，避免短句子为长句子「陪跑」浪费计算资源，带来了 2 倍的训练加速。

## 7. 这篇论文改变了什么问题

Seq2Seq 的钉子句是：**端到端映射可行，但固定向量会成为瓶颈。**

这篇论文把机器翻译从一条人工拼装的流水线，改成一个可以整体训练的映射问题。它证明神经网络可以先读完整段输入，再逐步写出输出；输入和输出不需要等长，也不需要手工对齐。

但它同时把瓶颈暴露得很彻底。所有源句信息都要压进同一个向量，句子越长，压缩损失越明显。反转源句子的技巧能缓解距离问题，却不能改变信息必须过窄门的事实。

所以下次看 Seq2Seq，不要只记住“编码器-解码器”。更该问：这个系统把信息压在哪里？那个压缩点，会不会变成下一代架构必须绕开的瓶颈？

---

**论文共读系列**

- [《Neural Machine Translation by Jointly Learning to Align and Translate》](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)（通过联合学习对齐与翻译实现神经机器翻译） — 注意力机制的起源
- [《Attention Is All You Need》](/zh-CN/posts/attention-is-all-you-need/)（注意力就是你所需要的全部） — 注意力成为主角，Transformer 的诞生
- [《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》](/zh-CN/posts/bert/)（BERT：用于语言理解的深度双向 Transformer 预训练） — 预训练范式的确立
- [《Scaling Laws for Neural Language Models》](/zh-CN/posts/scaling-laws-for-neural-language-models/)（神经语言模型的缩放定律） — 规模的数学：为什么更大的模型可预测地更好
- [《Language Models are Few-Shot Learners》](/zh-CN/posts/language-models-are-few-shot-learners/)（语言模型是少样本学习者） — 更大的模型，更善于从上下文中诱发能力
- [《Training Compute-Optimal Large Language Models》](/zh-CN/posts/training-compute-optimal-large-language-models/)（训练计算最优的大语言模型） — 怎样花算力最划算</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>seq2seq</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>Clawdbot：个人 AI 主权的早期样本</title><link>https://justinhuangai.github.io/zh-CN/posts/clawdbot/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/clawdbot/</guid><description>从 Clawdbot 看自托管 AI Agent 的真正问题：不是模型有多聪明，而是控制权、上下文和执行权限归谁。</description><pubDate>Fri, 16 Jan 2026 08:34:57 GMT</pubDate><content:encoded>AI 助手的第一性问题不是会不会聊天，而是谁拥有它。

如果记忆、上下文和工具执行权都在平台服务器里，用户得到的是一个账号，不是一个属于自己的智能系统。[Clawdbot](https://github.com/clawdbot/clawdbot) 的早期价值就在这里：它把 Agent 从云端产品拉回到用户能部署、能检查、能撤回权限的环境里。

从代码库看，它不是一个演示项目：二十多万行 TypeScript，覆盖 macOS、iOS、Android 三端原生应用，外加 50 多个技能模块。

![WhatsApp 中与 Clawd 对话](/images/whatsapp-clawd.webp)

## 0. 先认几个词

- `中心化 AI 助手`：聊天记录、记忆和控制权主要留在平台方。
- `自托管`：软件跑在自己的机器或服务器上，部署权和数据权回到用户手里。
- `AI Agent`：能记住上下文、调用工具、执行任务的运行系统，不只是聊天窗口。
- `聊天渠道`：WhatsApp、Telegram、Slack、iMessage 这类用户已经在用的入口。
- `技能`：给 Agent 安装的新能力，通常包含步骤、工具调用和边界条件。

## 1. 控制权比界面更重要

大多数 AI 产品把问题做成了界面竞争：谁的聊天窗口更顺，谁的模型回答更好，谁的订阅包更划算。

Clawdbot 问的是另一个问题：如果 AI 真的要替用户长期做事，为什么它的记忆和执行权限要默认托管在第三方平台里？

这不是隐私洁癖。Agent 一旦能读文件、调用工具、访问账号、记住长期偏好，它就不再只是“一个好用的网页”。它开始接近个人基础设施。个人基础设施的关键不只是能力，而是可迁移、可审计、可停用。

## 2. 聊天渠道不是装饰

Clawdbot 把 Agent 接进 WhatsApp、Telegram、Slack、iMessage 这些现成聊天入口。

这件事的重点不是“支持平台多”。真正的变化是任务入口不再被一个新 App 垄断。用户不需要先想起某个 AI 产品，再打开它，再复制上下文。Agent 出现在原本的沟通场景里，任务从对话中自然长出来。

这会改变使用频率。一个智能体如果只存在于单独窗口里，它竞争的是用户注意力；如果它在现有渠道里，它竞争的是任务本身。

## 3. 自托管不是自动安全

自托管只是把权力拿回来，不保证权力会被用好。

Agent 能运行命令、保存记忆、安装技能，也意味着它有更大的攻击面。提示词注入、错误工具调用、技能供应链、权限过宽，都会从“模型回答错了”升级成“系统执行错了”。

所以 Clawdbot 真正有价值的地方，不是“它能跑在本地”，而是它迫使我们把 Agent 当成一个有权限边界的系统看待。模型只是其中一部分；记忆、工具、身份、审批和日志才决定它能不能长期进入现实工作流。

## 4. 它改变的问题

Clawdbot 不是终局产品，更像一个早期样本。

它把问题从“哪个 AI 助手更聪明”改成了“一个个人 Agent 应该由谁控制”。这个问题更底层，也更难回避。

如果未来每个人真的会拥有一个长期运行的 AI 系统，关键分水岭不会是聊天界面，而是三件事：上下文归谁，执行权限归谁，失败责任归谁。</content:encoded><category>OpenClaw</category><category>AI</category><category>open-source</category></item><item><title>《Neural Machine Translation by Jointly Learning to Align and Translate》：Transformer 之前的注意力</title><link>https://justinhuangai.github.io/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/</guid><description>注意力机制的起源，附真实 Python 代码</description><pubDate>Sun, 11 Jan 2026 08:26:19 GMT</pubDate><content:encoded>Seq2Seq 证明了端到端翻译可行，也留下了一个硬瓶颈：整句必须被压进一个固定向量。[《Neural Machine Translation by Jointly Learning to Align and Translate》](/papers/1409.0473v7.pdf) 的第一性意义，是把“记住整句”改成“每一步重新查找相关信息”。

注意力在这里还不是 Transformer 的主角，而是 RNN 旁边的一条检索通道。它真正改变的不是某个公式，而是任务形状：解码器不再被迫相信一份总摘要，它可以在生成每个词时重新对齐输入。

## 0. 先认几个词

如果你完全没有机器学习背景，先顺着这篇论文真正想修补的地方，记住下面几个词：

- `编码器-解码器 / Encoder-Decoder`：一部分先把源句子读完，另一部分再把目标句子一个词一个词写出来。
- `RNN / 循环神经网络`：当时主流的序列模型。它必须按顺序处理文本，不能一下子同时看整句。
- `hidden state / 隐藏状态`：模型读到某个位置时，手里那份临时笔记。
- `alignment / 对齐`：源句子里的哪一部分，对应目标句子当前要生成的这个词。
- `attention`：生成每个词时，不再只盯着一个总压缩结果，而是主动回头看源句子里更 relevant 的位置。

## 1. 问题出在哪

2014 年的神经机器翻译有一个标准架构：编码器-解码器（Encoder-Decoder）。编码器是一个循环神经网络（RNN），把源句子从头到尾读一遍，把整个句子压缩成一个固定长度的向量（可以理解为一串固定数量的数字）。解码器是另一个 RNN，从这个向量出发，一个词一个词地生成翻译。

问题很明显：不管源句子是 5 个词还是 50 个词，编码器都要把它压进同一个长度的向量里。短句子还行，长句子就丢信息。就像你让一个人读完一整页书，然后只能用一句话复述，句子越长，遗漏越多。

论文用实验证明了这一点：当句子长度超过 30 个词，传统编码器-解码器的翻译质量急剧下降。

这就是「固定长度瓶颈」。

## 2. 核心想法：别压缩，让解码器自己去找

论文的解决方案很直觉：既然把整个句子压成一个向量会丢信息，那就不压了。编码器保留每个位置的注解向量（annotation，由双向 RNN 的前向和后向隐藏状态拼接而成，可以理解为每个词处理完之后产生的中间结果），解码器在生成每个目标词时，自己决定该重点看源句子的哪些部分。

这就是注意力机制的核心：**不再强迫所有信息挤过一个瓶颈，而是让模型学会在需要的时候回头找需要的信息。**

具体来说，分三步：

**第一步，打分。** 解码器在生成第 i 个目标词之前，会用自己当前的状态 s_{i-1} 和编码器每个位置的隐藏状态 h_j 做比较，算出一个「对齐分数」e_{ij}。分数越高，说明生成当前目标词时，源句子的第 j 个位置越重要。

论文用的打分函数是：

$$
e_{ij} = a(s_{i-1}, h_j) = v_a^T \tanh(W_a s_{i-1} + U_a h_j)
$$

这叫「加性注意力」（additive attention）。把解码器状态和编码器状态各自做一次线性变换（乘以矩阵），加起来，过一个 tanh（一种把数值压缩到 -1 到 1 之间的函数），再和一个向量 v_a 做点积，得到一个标量分数。

**第二步，归一化。** 用 softmax 把所有位置的分数转成概率，加起来等于 1：

$$
\alpha_{ij} = \operatorname{softmax}(e_{ij}) = \frac{\exp(e_{ij})}{\sum_k \exp(e_{ik})}
$$

**第三步，加权求和。** 用这些概率对编码器的隐藏状态做加权求和，得到一个「上下文向量」c_i：

$$
c_i = \sum_j \alpha_{ij} h_j
$$

这个上下文向量就是解码器在生成第 i 个词时，从源句子里提取到的关键信息。每生成一个词，上下文向量都不一样，因为模型关注的源句子位置不一样。

用 Python（基于 PyTorch）写出来：

```python showLanguage
import torch
from torch import nn


def bahdanau_attention(
    decoder_state: torch.Tensor,
    encoder_outputs: torch.Tensor,
    w_a: nn.Linear,
    u_a: nn.Linear,
    v_a: nn.Linear,
) -&gt; tuple[torch.Tensor, torch.Tensor]:
    decoder_features = w_a(decoder_state).unsqueeze(1)
    encoder_features = u_a(encoder_outputs)
    scores = v_a(torch.tanh(decoder_features + encoder_features)).squeeze(-1)
    weights = torch.softmax(scores, dim=-1)
    context = torch.sum(weights.unsqueeze(-1) * encoder_outputs, dim=1)
    return context, weights
```

和后来 Transformer 用的「点积注意力」（Q 和 K 直接做点积）不同，这篇论文用的是「加性注意力」（先各自做线性变换，再加起来）。两种方法各有特点，但点积注意力更适合用高效矩阵乘法实现；再加上 Transformer 去掉了 RNN 的顺序依赖，注意力才真正成为可大规模并行的核心算子。

## 3. 编码器：双向 RNN

单向 RNN 从左往右读句子，到了最后一个词才输出一个总结向量。问题是：每个位置的隐藏状态主要只带着左侧上下文，看不到右边。

论文用了双向 RNN（BiRNN）来解决这个问题。一个 RNN 从左往右读，另一个从右往左读，然后把两个方向的隐藏状态拼起来。这样每个位置的隐藏状态就同时包含了左边和右边的上下文。

```python showLanguage
import torch
from torch import nn


class BidirectionalRNN(nn.Module):
    def __init__(self, input_size: int, hidden_size: int) -&gt; None:
        super().__init__()
        self.rnn = nn.GRU(
            input_size=input_size,
            hidden_size=hidden_size,
            bidirectional=True,
            batch_first=True,
        )

    def forward(self, inputs: torch.Tensor) -&gt; torch.Tensor:
        outputs, _ = self.rnn(inputs)
        return outputs
```

论文里每个方向各有 1000 个隐藏单元，拼起来就是 2000 维。这比单向 RNN 多了一倍参数，但换来的是每个位置都能看到完整的上下文。

## 4. 解码器：每一步都重新对齐

把编码器和注意力机制组装起来，解码器的工作流程就清楚了：

1. 编码器用双向 RNN 读完源句子，保留每个位置的隐藏状态（注解向量）
2. 解码器开始生成翻译，每生成一个词之前：
   - 用当前状态和所有注解向量算注意力权重
   - 加权求和得到上下文向量
   - 结合上下文向量、上一个生成的词和当前状态，预测下一个词

```python showLanguage
import torch
from torch import nn


class AttentionDecoder(nn.Module):
    def __init__(self, embedding_dim: int, hidden_size: int, vocab_size: int) -&gt; None:
        super().__init__()
        self.rnn = nn.GRU(
            input_size=embedding_dim + 2 * hidden_size,
            hidden_size=hidden_size,
            batch_first=True,
        )
        self.w_a = nn.Linear(hidden_size, hidden_size, bias=False)
        self.u_a = nn.Linear(2 * hidden_size, hidden_size, bias=False)
        self.v_a = nn.Linear(hidden_size, 1, bias=False)
        self.output_proj = nn.Linear(hidden_size, vocab_size)

    def decode_step(
        self,
        prev_word: torch.Tensor,
        prev_state: torch.Tensor,
        encoder_outputs: torch.Tensor,
    ) -&gt; tuple[torch.Tensor, torch.Tensor]:
        context, _ = bahdanau_attention(
            prev_state.squeeze(0),
            encoder_outputs,
            self.w_a,
            self.u_a,
            self.v_a,
        )
        rnn_input = torch.cat([prev_word, context.unsqueeze(1)], dim=-1)
        output, new_state = self.rnn(rnn_input, prev_state)
        logits = self.output_proj(output[:, -1, :])
        return logits, new_state
```

关键在于：每生成一个目标词，解码器都会重新计算注意力分布。翻译第一个词时可能重点关注源句子的开头，翻译最后一个词时可能重点关注源句子的结尾。这种动态对齐能力，是之前的固定向量架构做不到的。

## 5. 实验结果

论文在英法翻译任务上做了实验（使用 WMT &apos;14 数据集），用 BLEU 分数（衡量机器翻译和人工翻译接近程度的标准评分，满分 100）衡量效果。

关键对比：
- **RNNencdec-50**（传统编码器-解码器，训练时最长 50 词）：26.71 BLEU
- **RNNsearch-50**（带注意力的模型，训练时最长 50 词）：**34.16 BLEU**
- **Moses**（当时最强的传统短语翻译系统）：33.30 BLEU

提升了 7.45 分。在论文报告的实验设置里，带注意力的神经模型已经达到甚至超过了当时强势的传统短语翻译系统。

更关键的发现在论文的 Figure 2：随着句子长度增加，传统编码器-解码器的 BLEU 分数急速下跌，而带注意力的模型几乎不受影响。这直接验证了论文的核心假设：固定长度向量是瓶颈，注意力机制可以绕过它。

论文还展示了注意力权重的可视化。在英法翻译里，注意力权重几乎形成了一条对角线，说明模型自动学会了「英语第 1 个词对应法语第 1 个词，英语第 2 个词对应法语第 2 个词」的对齐关系。遇到语序不同的情况（比如法语的形容词放在名词后面），注意力权重也会对应地偏移。模型不需要任何人工对齐标注，就学会了这些。

## 6. 这篇论文改变了什么问题

注意力的第一性意义是：**把“记住整句”改成“每一步重新查找相关信息”。**

这篇论文没有先发明 Transformer，也没有抛弃 RNN。它做的是更根本的接口改造：编码器不再只交出一份总摘要，而是保留每个位置的表示；解码器每生成一个词，就重新决定该看源句子的哪里。

这件事改变了长句子的命运。模型不再被固定向量逼着一次性记住所有东西，而是把翻译过程拆成连续的检索和生成。对齐不再是外部标注，而是在训练目标里自己长出来。

所以下次看 attention，不要先问它的公式多漂亮。先问它把哪个任务从“记忆问题”改成了“检索问题”。这才是它后来能放大成 Transformer 的原因。

---

**论文共读系列**

- [《Sequence to Sequence Learning with Neural Networks》](/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/)（使用神经网络进行序列到序列学习） — 编码器-解码器范式的确立
- [《Attention Is All You Need》](/zh-CN/posts/attention-is-all-you-need/)（注意力就是你所需要的全部） — 注意力成为主角，Transformer 的诞生
- [《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》](/zh-CN/posts/bert/)（BERT：用于语言理解的深度双向 Transformer 预训练） — 预训练范式的确立
- [《Scaling Laws for Neural Language Models》](/zh-CN/posts/scaling-laws-for-neural-language-models/)（神经语言模型的缩放定律） — 规模的数学：为什么更大的模型可预测地更好
- [《Language Models are Few-Shot Learners》](/zh-CN/posts/language-models-are-few-shot-learners/)（语言模型是少样本学习者） — 更大的模型，更善于从上下文中诱发能力
- [《Training Compute-Optimal Large Language Models》](/zh-CN/posts/training-compute-optimal-large-language-models/)（训练计算最优的大语言模型） — 怎样花算力最划算</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>attention</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>《Attention Is All You Need》：Transformer 的设计原点</title><link>https://justinhuangai.github.io/zh-CN/posts/attention-is-all-you-need/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/attention-is-all-you-need/</guid><description>拆解 Transformer 论文，附真实 Python 代码</description><pubDate>Tue, 06 Jan 2026 08:18:46 GMT</pubDate><content:encoded>Transformer 的第一性问题不是“注意力有没有用”，而是序列建模为什么一定要被时间顺序绑住。RNN 把文本顺序和计算顺序锁在一起，长距离依赖、并行训练和信息寻址都被这条锁链拖住。

[《Attention Is All You Need》](/papers/1706.03762v7.pdf) 的革命不在于注意力本身，而在于把序列建模从时间顺序问题改写成全局寻址问题：每个位置直接问，当前最该看谁。

## 0. 先认几个词

如果你没有机器学习背景，先按这篇论文真正想替换掉的旧方案，记住下面几个词就够了：

- `RNN / 循环神经网络`：一种更早的序列模型。它处理句子时必须一个词一个词往后读，像人用手指着文章逐行看。
- `attention`：从很多信息里，挑出当前最该看的那几部分。你可以先把它理解成“有选择地回头看重点”。
- `Query / Key / Value`：注意力机制里的三个角色。Query 像“我现在想找什么”，Key 像“每段信息贴着什么标签”，Value 则是“真正被取出来的内容”。
- `Transformer`：以 attention 为核心搭起来的一整套架构。它不靠循环一步步往前推，而是让每个位置都能直接看其他位置。
- `并行`：这里不是说模型更聪明，而是说它能同时处理很多位置，不必像 RNN 那样排队。

## 1. 一句话说清楚

在 Transformer 之前，AI 处理语言的方式像是一个人用手指着书，一个字一个字地往下读。读到第 100 个字的时候，第 1 个字说了什么，已经记不太清了。句子越长，遗忘越严重。这就是循环神经网络（RNN，一种早期的 AI 架构）的根本瓶颈。

论文的作者们问了一个问题：**为什么一定要按顺序读？**

和必须逐步处理的 RNN 不同，Transformer 可以并行处理整段输入，直接建模任意两个位置之间的关系。不用排队，不用等前一个词处理完才能看下一个。

论文管这个核心能力叫「注意力」。注意力机制最早由 [Bahdanau 等人在 2014 年提出](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)，当时是作为 RNN 的辅助组件。这篇论文的标题要表达的不是「模型里真的只剩注意力」，而是：在序列建模里，注意力第一次被推到了主角的位置，不再需要循环和卷积（一种通过滑动窗口提取局部特征的方法）作为骨架。

## 2. 注意力到底在做什么

想象你走进一个嘈杂的酒吧，二十个人同时在说话。你的大脑不会平均分配注意力给每个人。有人喊了你的名字，你的耳朵瞬间锁定那个方向，其他声音自动变成背景噪音。

Transformer 对每个词做同样的事。论文里定义了三个角色：

- **Query（查询）**：这个词在找什么信息。相当于你的耳朵在搜索「谁在叫我」
- **Key（键）**：这个词能提供什么信息。相当于酒吧里每个人的声音特征
- **Value（值）**：这个词实际携带的内容。相当于那个人说的具体话

每个词的 Query 会和其他词的 Key 做匹配。匹配度高的，就从对方的 Value 里获取更多信息。匹配度低的，直接忽略。

论文给出的公式叫 Scaled Dot-Product Attention：

$$
\operatorname{Attention}(Q, K, V) = \operatorname{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V
$$

看到公式别慌。一步一步拆：

- **QK^T**：Q 和 K 做点积。什么是点积？把两组数字对应位置相乘，再加起来。比如 [1, 2] 和 [3, 4] 的点积是 1×3 + 2×4 = 11。数字越大，说明两个词越相关。这一步算的就是每对词之间的「匹配分数」
- **/ √d_k**：除以一个数来缩放。d_k 是向量的长度（向量可以理解为「一串用来描述某个东西的数字」，比如用 64 个数字描述一个词的含义）。为什么要除？因为数字串越长，点积结果越大。不缩放时，维度越大点积的方差越大，softmax 容易进入饱和区（几乎所有概率集中在一个词上），梯度（模型用来调整自身参数的信号）变得很小，训练会不稳定
- **softmax**：把一组分数转换成概率，所有概率加起来等于 1。比如三个词的分数是 [10, 2, 1]，softmax 之后大概变成 [0.99, 0.007, 0.003]。分数最高的那个词几乎拿走了全部注意力，其他的被压到接近零
- **× V**：用这些概率去加权每个词的实际内容。概率高的词贡献大，概率低的词贡献小。最终输出是一个融合了关键信息的新向量

用 Python（基于 PyTorch）写出来：

```python showLanguage
import math
import torch


def scaled_dot_product_attention(
    query: torch.Tensor,
    key: torch.Tensor,
    value: torch.Tensor,
) -&gt; torch.Tensor:
    d_k = key.size(-1)
    scores = query @ key.transpose(-2, -1) / math.sqrt(d_k)
    weights = torch.softmax(scores, dim=-1)
    return weights @ value
```

核心运算很短。很多后来改变行业的能力，底层都建立在这几行矩阵运算之上。

## 3. 多头注意力：同时从多个角度看

单个注意力头通常只能偏向某一类关系模式。但语言这东西，一句话里藏着好几层意思。

拿「我昨天在深圳吃了潮汕牛肉火锅」来说：
- 「我」和「吃了」之间是谁做了什么的关系
- 「昨天」和「吃了」之间是时间关系
- 「深圳」和「潮汕牛肉火锅」之间是地点与食物的关系

让一个头同时兼顾这么多层次，很难。论文的做法是用多头机制：派出 8 个头并行运算，让模型有机会从不同子空间同时观察一句话，最后把各自的发现拼起来。

论文原文的公式：

$$
\operatorname{MultiHead}(Q, K, V) = \operatorname{Concat}(\text{head}_1, \ldots, \text{head}_h)\, W^O
$$

拆开看：
- **head_1, ..., head_h**：8 个头各自独立做一次注意力运算，得到 8 份结果
- **Concat**：把 8 份结果首尾相连，拼成一个长向量
- **W^O**：一次线性变换（可以理解为「乘以一个矩阵」），把拼接后的长向量压回原来的维度。相当于一个主管听完 8 个调查员的汇报，输出一份综合结论

```python showLanguage
import math
import torch
from torch import nn


class MultiHeadAttention(nn.Module):
    def __init__(self, d_model: int, num_heads: int) -&gt; None:
        super().__init__()
        if d_model % num_heads != 0:
            raise ValueError(&quot;d_model must be divisible by num_heads&quot;)
        self.num_heads = num_heads
        self.d_head = d_model // num_heads
        self.q_proj = nn.Linear(d_model, d_model)
        self.k_proj = nn.Linear(d_model, d_model)
        self.v_proj = nn.Linear(d_model, d_model)
        self.out_proj = nn.Linear(d_model, d_model)

    def _split_heads(self, x: torch.Tensor) -&gt; torch.Tensor:
        batch_size, seq_len, _ = x.shape
        x = x.view(batch_size, seq_len, self.num_heads, self.d_head)
        return x.transpose(1, 2)

    def forward(
        self,
        query: torch.Tensor,
        key: torch.Tensor,
        value: torch.Tensor,
    ) -&gt; torch.Tensor:
        q = self._split_heads(self.q_proj(query))
        k = self._split_heads(self.k_proj(key))
        v = self._split_heads(self.v_proj(value))

        scores = q @ k.transpose(-2, -1) / math.sqrt(self.d_head)
        weights = torch.softmax(scores, dim=-1)
        heads = weights @ v

        batch_size, _, target_len, _ = heads.shape
        merged = heads.transpose(1, 2).contiguous()
        merged = merged.view(batch_size, target_len, self.num_heads * self.d_head)
        return self.out_proj(merged)
```

论文里的参数：模型用 512 个数字描述一个词（d_model = 512），8 个头，每个头分到 64 个数字（512 ÷ 8 = 64）。8 个头的总计算量和 1 个 512 维的头差不多，但表达能力强得多。用同样的计算预算，换来多视角的表示能力。这笔预算分配很清楚。

## 4. 位置编码：告诉模型词的顺序

Transformer 并行处理整个句子，速度是快了，但代价是它丢掉了词的先后顺序。如果没有额外的位置信息，注意力机制本身并不知道「猫吃鱼」和「鱼吃猫」有什么区别。这显然不行。

怎么补救？给每个位置生成一个独一无二的「地址编码」，加到词的向量上。模型看到的不再是「猫」和「鱼」，而是「第 1 个位置的猫」和「第 3 个位置的鱼」。

论文用正弦和余弦函数来生成这个编码：

$$
\operatorname{PE}(pos, 2i) = \sin\left(\frac{pos}{10000^{2i / d_{\text{model}}}}\right)
$$

$$
\operatorname{PE}(pos, 2i + 1) = \cos\left(\frac{pos}{10000^{2i / d_{\text{model}}}}\right)
$$

公式看着唬人，核心思路很直观：
- **pos**：词在句子里的位置（第 1 个、第 2 个、第 3 个……）
- **i**：向量的第几个维度。偶数位置用 sin，奇数位置用 cos
- **10000^(2i/d_model)**：一个随维度变化的缩放因子。低维度变化快，高维度变化慢。就像时钟：秒针一分钟转一圈，时针十二小时才转一圈。不同「指针」覆盖不同的时间尺度，组合在一起就能精确定位任意时刻

最终效果：每个位置得到一串独一无二的数字指纹，模型靠这个指纹区分词的先后顺序。

```python showLanguage
import math
import torch


def positional_encoding(seq_len: int, d_model: int) -&gt; torch.Tensor:
    positions = torch.arange(seq_len, dtype=torch.float32).unsqueeze(1)
    div_term = torch.exp(
        torch.arange(0, d_model, 2, dtype=torch.float32)
        * (-math.log(10000.0) / d_model)
    )

    encoding = torch.zeros(seq_len, d_model)
    encoding[:, 0::2] = torch.sin(positions * div_term)
    encoding[:, 1::2] = torch.cos(positions * div_term)
    return encoding
```

为什么偏偏选正弦余弦？因为它有一个优雅的数学性质：两个词相隔固定距离，无论它们出现在句首还是句尾，位置编码之间的关系是一样的。模型不用死记「位置 3 和位置 8」的关系，只需要学会「相隔 5 个位置」意味着什么。论文团队也试过让模型自己学位置编码，效果差不多，但正弦版本有一个额外的优势：它能处理训练时没见过的更长句子。

## 5. 编码器与解码器

Transformer 的完整架构分两半。

**编码器**（6 层堆叠）负责读懂输入。每层包含两个子层：一个多头自注意力，一个前馈网络。每个子层都有两个保护机制：

- **残差连接**：把子层的输入直接加到输出上，即 x + Sublayer(x)。为什么？想象你给一张照片加滤镜。如果滤镜效果不好，残差连接保证你还能看到原图。在深层网络里，信息每经过一层都会被变换，传到第六层可能已经面目全非。残差连接让原始信号可以「抄近道」直达深层，防止信息在传递中丢失
- **层归一化**（LayerNorm）：把数值调整到统一范围，防止有的数字大到爆炸、有的小到消失。类似于考试成绩标准化，不管原始卷面分差异多大，标准化后都在一个可比较的区间

**解码器**（6 层堆叠）负责生成输出。结构和编码器类似，但多了两个关键设计：

第一，**交叉注意力**：解码器生成每个词时，会回头「看」编码器的输出。翻译场景下，就是一边写英文一边回头看中文原文。

第二，**遮罩**（masking）：生成第 3 个词时，只允许看到前 2 个词，第 4 个及之后的位置被屏蔽（注意力分数设为负无穷，经过 softmax 后变成零）。道理很简单：你写作文的时候，下一个字还没写出来，不能偷看。

```python showLanguage
from typing import Optional

import torch
from torch import nn


class Transformer(nn.Module):
    def __init__(
        self,
        vocab_size: int,
        d_model: int = 512,
        num_heads: int = 8,
        num_layers: int = 6,
        d_ff: int = 2048,
        dropout: float = 0.1,
    ) -&gt; None:
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)

        encoder_layer = nn.TransformerEncoderLayer(
            d_model=d_model,
            nhead=num_heads,
            dim_feedforward=d_ff,
            dropout=dropout,
            batch_first=True,
        )
        decoder_layer = nn.TransformerDecoderLayer(
            d_model=d_model,
            nhead=num_heads,
            dim_feedforward=d_ff,
            dropout=dropout,
            batch_first=True,
        )

        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=num_layers)
        self.output_proj = nn.Linear(d_model, vocab_size)

    def forward(
        self,
        src_token_ids: torch.Tensor,
        tgt_token_ids: torch.Tensor,
        tgt_mask: Optional[torch.Tensor] = None,
    ) -&gt; torch.Tensor:
        memory = self.encoder(self.embedding(src_token_ids))
        hidden = self.decoder(self.embedding(tgt_token_ids), memory, tgt_mask=tgt_mask)
        return self.output_proj(hidden)
```

还有一个容易被忽略的组件：前馈网络。公式是 FFN(x) = max(0, xW1 + b1)W2 + b2。翻译成人话：先把每个词的 512 维向量扩大到 2048 维（乘以一个矩阵再加一个偏置），用 ReLU 过滤一遍（所有负数变成零，正数保留），再压回 512 维。ReLU 这一步是关键：它引入了「非线性」，让模型能学到直线画不出来的复杂模式。如果全是线性变换，多层叠加在数学上仍可合并为单层，非线性是模型表示复杂模式的前提。

## 6. 训练细节

架构设计完了，怎么训练它？论文在这里也有不少讲究。

**硬件**：8 块 NVIDIA P100 GPU。基础版模型训练 12 小时（10 万步），大号模型训练 3.5 天（30 万步）。放在今天看，这个训练成本已经很低。

**优化器**：用的是 Adam（一种让模型自动调整参数的算法），但学习率的设计很巧妙。学习率决定了模型每一步「迈多大步子」。步子太大容易跨过最优解，太小又走得慢。论文的策略是前 4000 步逐渐提速（warmup，热身），避免一开始更新过猛；4000 步之后按计划逐渐减速，让训练后期更稳定。先升后降，前半段大胆探索，后半段精细打磨。

**正则化**：两招。第一招是 Dropout，训练时随机关掉 10% 的神经元（可以理解为网络中的计算节点），迫使模型不依赖任何单一路径，学到更鲁棒的特征。第二招是 label smoothing（标签平滑，ε = 0.1）：训练时不告诉模型「正确答案的概率是 100%」，而是说「90% 是正确答案，剩下 10% 分给其他选项」。这会让模型在一个指标上变差（困惑度，衡量模型有多「拿不准」），但翻译质量反而更好。直觉上说，一个承认自己不是 100% 确定的模型，比一个过度自信的模型更可靠。

**结果**：论文用 BLEU 分数（机器翻译的标准评分，衡量机器翻译和人工翻译有多接近，满分 100）来衡量效果。英德翻译 28.4 分，英法翻译 41.8 分，都刷新了当时的记录。训练成本比之前的方法低了一到两个数量级。更快，更强，更便宜。

## 7. 这篇论文改变了什么问题

Transformer 的钉子句是：**序列建模不必按时间顺序计算，它可以变成全局寻址。**

这篇论文的核心不是“注意力很强”四个字。注意力在 Bahdanau 那里已经出现过。真正的转折是：作者把循环和卷积从骨架位置拿掉，让每个 token 直接访问其他 token。计算路径不再被文本顺序绑住，长距离依赖也不再必须穿过一长串中间状态。

Self-Attention、残差连接、LayerNorm、前馈网络都不是孤立的英雄。它们共同组成了一台更适合并行训练、更适合规模化的序列机器。Transformer 的力量，来自把信息流和硬件效率同时改写。

下次看一个新架构，不要只问它用了什么模块。先问它有没有改写问题的坐标系：是继续沿着旧计算路径做优化，还是把信息寻址方式换掉了。

---

**论文共读系列**

- [《Sequence to Sequence Learning with Neural Networks》](/zh-CN/posts/sequence-to-sequence-learning-with-neural-networks/)（使用神经网络进行序列到序列学习） — 编码器-解码器范式的确立
- [《Neural Machine Translation by Jointly Learning to Align and Translate》](/zh-CN/posts/neural-machine-translation-by-jointly-learning-to-align-and-translate/)（通过联合学习对齐与翻译实现神经机器翻译） — 注意力机制的起源
- [《BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding》](/zh-CN/posts/bert/)（BERT：用于语言理解的深度双向 Transformer 预训练） — 预训练范式的确立
- [《Scaling Laws for Neural Language Models》](/zh-CN/posts/scaling-laws-for-neural-language-models/)（神经语言模型的缩放定律） — 规模的数学：为什么更大的模型可预测地更好
- [《Language Models are Few-Shot Learners》](/zh-CN/posts/language-models-are-few-shot-learners/)（语言模型是少样本学习者） — 更大的模型，更善于从上下文中诱发能力
- [《Training Compute-Optimal Large Language Models》](/zh-CN/posts/training-compute-optimal-large-language-models/)（训练计算最优的大语言模型） — 怎样花算力最划算</content:encoded><category>Paper Reading</category><category>paper-reading</category><category>transformer</category><category>AI</category><category>LLM</category><category>python</category></item><item><title>欢迎来到 Justin Huang Blog</title><link>https://justinhuangai.github.io/zh-CN/posts/hello-world/</link><guid isPermaLink="true">https://justinhuangai.github.io/zh-CN/posts/hello-world/</guid><description>一个个人技术博客：拆论文、看系统、追问 AI 与软件工程背后的因果链。</description><pubDate>Thu, 01 Jan 2026 08:07:13 GMT</pubDate><content:encoded>这个博客不是信息流。

信息流负责告诉你什么刚刚发生。它擅长制造新鲜感，却不擅长保存一个问题为什么重要。这里做另一件事：把技术问题重新放回可理解的因果链里。

## 写什么

这里的大部分文章从三类对象出发：论文、系统、工程行为。

论文只有在改变提问方式时才真正重要。Transformer 不是“注意力很强”这么简单，而是把序列建模从时间顺序问题改写成全局寻址问题。GPT-3 不是单纯更大，而是把一部分任务适配从参数更新搬进上下文。Chinchilla 不是反对大模型，而是指出参数和数据必须一起吃满算力预算。

系统只有在约束可见时才值得分析。一个 Agent 产品不是“模型加工具”。它还包括记忆放在哪里、权限怎么收、运行时状态怎么恢复、协议如何约束交互，以及出错的代价由谁承担。

工程行为只有放进激励里才看得清。benchmark、开源生态、模型排行榜、Agent 平台，从外面看都很干净；真正的问题通常藏在生成流程、成本结构和治理边界里。

## 怎么写

我不想写只会复述的文章。摘要压缩文本，好的阅读应该改变框架。

每篇文章至少要留下一个更锋利的问题：模型更强，是在哪种预算下更强？Agent 更能干，能力到底存在权重、上下文，还是运行时结构里？benchmark 更难，难的是任务本身，还是生成和过滤流程制造出来的难？

这就是这里的写作标准：少一点口号，多一点机制。

## 给读者，也给智能体

人类读者可以从最新文章开始，也可以按标签顺着一个主题读。

AI 智能体可以读 `/llms.txt`、`/llms-full.txt`，也可以在任意文章 URL 后加 `.md` 读取 Markdown。这个设计是刻意的：如果文字要被检索、引用、复用和反驳，机器可读版本就不该是二等入口。

这个博客要保留下来的不是观点数量，而是问题结构。一个判断过几个月再拿起来，还能继续工作，这才值得写。</content:encoded><category>General</category><category>hello</category><category>blog</category></item></channel></rss>