<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
  <channel>
    <atom:link href="https://yuanchaofa.com/rss.xml" rel="self" type="application/rss+xml"/>
    <title>chaofa用代码打点酱油</title>
    <link>https://yuanchaofa.com/</link>
    <description>袁朝发的个人技术博客，做了一个播客叫做《逃逸速度》，喜欢折腾各种各样的事情，业余野生视频博主，专业大模型算法工程师，尝试做一些有意义的事情</description>
    <language>zh-CN</language>
    <pubDate>Sat, 16 May 2026 12:19:48 GMT</pubDate>
    <lastBuildDate>Sat, 16 May 2026 12:19:48 GMT</lastBuildDate>
    <generator>Apethon</generator>
    <category>agent system design</category>
    <category>hands-on-code</category>
    <category>paper-reading</category>
    <category>python-type-challenge</category>
    <category>raycast-turorial</category>
    <category>分词</category>
    <category>实体识别</category>
    <category>年终总结</category>
    <category>序列标注</category>
    <category>情感-原因对抽取(ECPE)</category>
    <category>搜索技术</category>
    <category>月度总结</category>
    <category>杂谈</category>
    <category>笔记折腾记</category>
    <category>算法妙用</category>
    <category>面试锦囊</category>
    <item>
      <title>Harness Engineering — Agent 不好用，也许不是模型的问题</title>
      <link>https://yuanchaofa.com/post/harness-engineering-for-ai-agents</link>
      <guid>https://yuanchaofa.com/post/harness-engineering-for-ai-agents</guid>
      <source url="https://yuanchaofa.com/rss.xml">Harness Engineering — Agent 不好用，也许不是模型的问题</source>
      <description>同一个模型，只改 Agent Harness，性能从 Top 30 到 Top 5。Harness Engineering 到底是什么？和 Context Engineering 什么关系？以及 The Bitter Lesson 再思考。</description>
      <category>agent system design</category>
      <pubDate>Sat, 14 Mar 2026 22:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="0-阅读收获-takeaway"><a href="#0-阅读收获-takeaway" class="heading-anchor">0. 阅读收获 (Takeaway)</a></h2>
<p>读完本文，你将了解：</p>
<ul>
<li><strong>Harness 到底是什么</strong>：一个具体的、可操作的定义，不是又一个抽象类比</li>
<li><strong>Prompt → Context → Harness 的演进</strong>：三者的关系、边界和社区争议</li>
<li><strong>同一个模型，只改 Harness，性能从 Top 30 到 Top 5</strong>：一个具体案例的拆解</li>
<li><strong>哪些设计是持久的，哪些会被模型进步淘汰</strong>：对 The Bitter Lesson 的再思考</li>
</ul>
<blockquote>
<p>前置知识：如果你对 Agent 的上下文管理还不太熟，建议先看 <a href="/post/prompt-cache-design-for-llm-agents">Agent 系统中的 Prompt Caching 设计（上）</a> 和 <a href="/post/agent-context-management-and-sub-agents">（下）</a>，本文会多次引用。</p>
</blockquote>
<hr />
<h2 id="1-agent-不好用"><a href="#1-agent-不好用" class="heading-anchor">1. Agent 不好用？</a></h2>
<p>用 Coding Agent 的人大概都有过这种经历：Agent 写了一坨代码，自己看了一眼觉得&quot;嗯不错&quot;，然后就停了。测试？没跑。边界情况？没想。你花了 20 分钟等它干活，最后还是得自己收拾烂摊子。</p>
<p>本能反应通常是：&quot;模型还是不够聪明，等 GPT-6 就好了。&quot;</p>
<p>但 <a href="https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents">HumanLayer</a> 团队在做了上百个 Agent 项目后，得出了一个不同的结论：</p>
<blockquote>
<p>&quot;It's not a model problem. It's a configuration problem.&quot;</p>
</blockquote>
<p>LangChain 用数据证明了这一点：<strong>同一个模型（GPT-5.2-Codex），只改模型之外的东西，Terminal Bench 2.0 得分从 52.8 涨到 66.5，排名从 Top 30 到 Top 5。</strong></p>
<p>模型之外的那些东西，现在有了一个统一的名字：<strong>Harness</strong>。</p>
<h2 id="2-harness-是什么"><a href="#2-harness-是什么" class="heading-anchor">2. Harness 是什么？</a></h2>
<p>LangChain 的 Vivek Trivedy 给了一个最干净的定义：</p>
<blockquote>
<p><strong>Agent = Model + Harness</strong>. If you're not the model, you're the harness.</p>
</blockquote>
<p>Harness 就是<strong>除了模型权重以外的一切</strong>。</p>
<p><img loading="lazy" src="/diagrams/harness-engineering/harness-overview.svg" alt="Harness Overview" /></p>
<p>这不是抽象类比。打开你的 <em>Claude Code</em>，它的 Harness 具体包括：</p>
<table>
<thead>
<tr>
<th>组件</th>
<th>具体是什么</th>
<th>解决什么问题</th>
</tr>
</thead>
<tbody>
<tr>
<td>System Prompt</td>
<td>系统提示词，定义行为规范</td>
<td>模型不知道自己该扮演什么角色</td>
</tr>
<tr>
<td>CLAUDE.md / AGENTS.md</td>
<td>项目级指令文件，启动时注入</td>
<td>模型不知道这个项目的规范和上下文</td>
</tr>
<tr>
<td>Tools (bash, 文件读写, 浏览器)</td>
<td>可调用的工具集</td>
<td>模型只能输出文本，不能执行操作</td>
</tr>
<tr>
<td>Sandbox</td>
<td>隔离的执行环境</td>
<td>不能让模型直接操作你的生产环境</td>
</tr>
<tr>
<td>Compaction</td>
<td>上下文压缩机制</td>
<td>Context Rot，长对话性能衰减</td>
</tr>
<tr>
<td>Hooks / Middleware</td>
<td>在模型调用前后插入的逻辑</td>
<td>模型不会主动自检、容易死循环</td>
</tr>
<tr>
<td>Sub-agent 管理</td>
<td>子代理派生和结果回收</td>
<td>单个上下文窗口装不下复杂任务</td>
</tr>
</tbody>
</table>
<p>如果你读过我之前写的 <a href="/post/prompt-cache-design-for-llm-agents">Prompt Caching 系列</a>，会发现上下文管理、Compaction、子代理架构——这些其实都是 Harness 的组成部分。</p>
<p><a href="https://mitchellh.com/writing/my-ai-adoption-journey">My AI Adoption Journey</a> 有一个更偏工程实践的定义：</p>
<blockquote>
<p>&quot;Harness engineering is the idea that anytime you find an agent makes a mistake, you take the time to engineer a solution such that the agent never makes that mistake again.&quot;</p>
</blockquote>
<p>Agent 犯了错 → 不是改 prompt 重试 → 而是<strong>改环境让它不可能再犯这个错</strong>。这就是 Harness Engineering 的核心思想。</p>
<h2 id="3-prompt-context-harness不是替代是包含"><a href="#3-prompt-context-harness不是替代是包含" class="heading-anchor">3. Prompt → Context → Harness：不是替代，是包含</a></h2>
<p>这三个概念的关系经常被误解为&quot;一代比一代强，后面的替代前面的&quot;。但实际上它们是<strong>包含关系</strong>：</p>
<p><img loading="lazy" src="/diagrams/harness-engineering/prompt-context-harness-layers.svg" alt="Prompt Context Harness Layers" /></p>
<p>简单区分：</p>
<table>
<thead>
<tr>
<th></th>
<th>管什么</th>
<th>解决什么</th>
<th>失败了怎么办</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Prompt Engineering</strong></td>
<td>一条指令</td>
<td>怎么问才能得到好答案</td>
<td>改 prompt 重试</td>
</tr>
<tr>
<td><strong>Context Engineering</strong></td>
<td>整个上下文窗口</td>
<td>怎么给够信息完成复杂任务</td>
<td>调整检索/压缩/隔离策略</td>
</tr>
<tr>
<td><strong>Harness Engineering</strong></td>
<td>模型之外的一切</td>
<td>怎么建系统让 Agent 可靠地长期工作</td>
<td>改环境让错误不可能再发生</td>
</tr>
</tbody>
</table>
<ul>
<li><strong>Prompt Engineering</strong> 大家已经很熟了——few-shot、CoT、角色扮演。</li>
<li><strong>Context Engineering</strong> 是 2025 年 Karpathy 推动的概念升级。核心思想是：不要只关注 prompt 本身，要关注<strong>整个上下文窗口的管理</strong>。LangChain 总结了四个策略：Write（写到外部存储）、Select（按需检索）、Compress（压缩）、Isolate（隔离子任务的上下文）。</li>
<li><strong>Harness Engineering</strong> 再往外扩一层：不只管上下文窗口内的事，还管<strong>执行环境、工具、持久化、反馈循环、架构约束</strong>这些上下文窗口之外的东西。</li>
</ul>
<p>换句话说：Context Engineering 解决了&quot;给模型什么信息&quot;的问题，Harness Engineering 还要解决&quot;给模型什么环境&quot;的问题。</p>
<p>社区对这个关系其实有争议：LangChain 认为 Harness ⊃ Context（超集）；HumanLayer 认为 Harness ⊂ Context（子集）。</p>
<blockquote>
<p><strong>我的判断是超集关系更合理</strong>——sandbox、CI gate、linter 这些东西显然不属于&quot;上下文管理&quot;，但它们是 Harness 的核心组成部分。不过争论定义意义不大，重要的是意识到：Agent 出问题的时候，除了看&quot;给了什么信息&quot;，还要看&quot;跑在什么环境里&quot;。</p>
</blockquote>
<h2 id="4-案例同一个模型只改-harness"><a href="#4-案例同一个模型只改-harness" class="heading-anchor">4. 案例：同一个模型，只改 Harness</a></h2>
<p>LangChain 做过一个实验，很好地说明了 Harness 的实际影响。</p>
<p>基线：deepagents-cli + GPT-5.2-Codex，默认配置，Terminal Bench 2.0 得分 52.8。他们只调了三个&quot;旋钮&quot;（原话是 &quot;knobs on a harness&quot;）：System Prompt、Tools、Middleware。最终得分 66.5，排名从 Top 30 到 Top 5。</p>
<p>具体改了什么：</p>
<h3 id="修改-1退出前强制自检"><a href="#修改-1退出前强制自检" class="heading-anchor">修改 1：退出前强制自检</a></h3>
<p>最常见的失败模式：Agent 写完代码 → 重读一遍自己的代码 → 觉得&quot;看起来没问题&quot; → 停止。根本没跑测试。</p>
<p>他们加了一个 Middleware：在 Agent 试图退出时拦截，强制注入一个 checklist 让它对照任务说明验证。这本质上是一个 <a href="https://ghuntley.com/loop/">Ralph Wiggum Loop</a>——hook 住退出，强制继续。</p>
<h3 id="修改-2启动时注入环境信息"><a href="#修改-2启动时注入环境信息" class="heading-anchor">修改 2：启动时注入环境信息</a></h3>
<p>Agent 在陌生环境中会浪费大量时间探索目录结构、找 Python 环境。他们在 Agent 启动时自动跑一些 bash 命令扫描环境，把结果注入上下文。</p>
<p>这和我在 Prompt Caching 系列里讲的 <strong>Just-in-Time Context</strong> 思路一致——在正确的时机注入正确的信息，减少 Agent 自行探索的错误面。</p>
<h3 id="修改-3死循环检测"><a href="#修改-3死循环检测" class="heading-anchor">修改 3：死循环检测</a></h3>
<p>Agent 有时候会在同一个文件上反复做小修改，10+ 次还在原地打转。他们通过 hook 追踪每个文件的编辑次数，超过阈值就注入&quot;考虑换个方案&quot;的提示。</p>
<h3 id="修改-4reasoning-sandwich"><a href="#修改-4reasoning-sandwich" class="heading-anchor">修改 4：Reasoning Sandwich</a></h3>
<p>还有一个有意思的发现：GPT-5.2-Codex 有 4 档推理强度（low/medium/high/xhigh），全程 xhigh 反而得分低（53.9%），因为超时了。最终他们用 xhigh → high → xhigh 的&quot;三明治&quot;策略——规划和验证用高推理，执行阶段用中等推理。</p>
<p>这四个改动没有一个涉及模型本身。全是环境和流程的变化。</p>
<h2 id="5-the-bitter-lesson-再思考记在里"><a href="#5-the-bitter-lesson-再思考记在里" class="heading-anchor">5. The Bitter Lesson 再思考（记在🧠里）</a></h2>
<p><a href="https://www.philschmid.de/agent-harness-2026">The importance of Agent Harness in 2026</a>文章中有三条建议：</p>
<ol>
<li><strong>Start Simple</strong>：不要搭复杂的控制流，给好工具，让模型自己规划</li>
<li><strong>Build to Delete</strong>：架构要模块化，新模型会淘汰你昨天写的逻辑</li>
<li><strong>The Harness is the Dataset</strong>：竞争壁垒不是 prompt，是 Harness 捕获的执行轨迹</li>
</ol>
<p>第三点特别值得展开。Harness 跑的每一次 Agent 任务都在产生数据——成功的路径、失败的模式、工具调用的序列。这些数据可以反馈回训练，让下一代模型更适配 Harness 环境。LangChain 也提到了这一点：模型和 Harness 正在形成共同进化（co-evolution）。</p>
<p>但 The Bitter Lesson 也在起作用。Manus 6 个月重构了 5 次 Harness，Vercel 删掉了 80% 的 Agent 工具反而效果更好。这说明很多当前的 Harness 设计是在弥补模型的不足，模型一进步这些设计就过时了。</p>
<p>Q: 那什么设计是持久的？</p>
<p>我在 Prompt Caching 系列的结尾说过：<strong>围绕 Cache 的架构决策是持久的，因为它们不是在弥补模型不足，而是在<a href="/post/understanding-kv-cache-and-prompt-cache-basics">适配当前 Decoder 带来的物理限制</a></strong> 同样的逻辑：文件系统作为持久存储、sandbox 隔离、版本控制——这些是物理约束决定的，不会因为模型变强而消失。</p>
<h2 id="6-总结"><a href="#6-总结" class="heading-anchor">6. 总结</a></h2>
<p>Harness Engineering 不是一个全新的发明，而是给一系列已有实践起了一个统一的名字。如果你已经在写 CLAUDE.md、配 MCP Server、用 Sub-agent、做 Context Compaction——你其实已经在做 Harness Engineering 了。</p>
<p>核心就一句话：<strong>Agent 表现不好，先看 Harness 再怪模型。</strong></p>
<h2 id="参考"><a href="#参考" class="heading-anchor">参考</a></h2>
<ul>
<li>OpenAI - <a href="https://openai.com/index/harness-engineering/">Harness engineering: leveraging Codex in an agent-first world</a> (2026.02)</li>
<li>LangChain (Vivek Trivedy) - <a href="https://blog.langchain.com/the-anatomy-of-an-agent-harness/">The Anatomy of an Agent Harness</a> (2026.03)</li>
<li>LangChain - <a href="https://blog.langchain.com/improving-deep-agents-with-harness-engineering/">Improving Deep Agents with harness engineering</a> (2026.02)</li>
<li>HumanLayer - <a href="https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents">Skill Issue: Harness Engineering for Coding Agents</a> (2026.03)</li>
<li>本博客 - <a href="/post/prompt-cache-design-for-llm-agents">Agent 系统中的 Prompt Caching 设计（上）</a></li>
<li>本博客 - <a href="/post/agent-context-management-and-sub-agents">Agent 系统中的 Prompt Caching 设计（下）</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Kimi K2.5 技术解读：原生多模态联合训练与并行 Agent 编排训练</title>
      <link>https://yuanchaofa.com/post/kimi-k2-5-reading-notes</link>
      <guid>https://yuanchaofa.com/post/kimi-k2-5-reading-notes</guid>
      <source url="https://yuanchaofa.com/rss.xml">Kimi K2.5 技术解读：原生多模态联合训练与并行 Agent 编排训练</source>
      <description>解读 Kimi K2.5 论文核心技术：原生多模态联合训练的反共识发现（Early Fusion + 低视觉比例优于 Late Fusion）、Zero-Vision SFT、跨模态双向迁移、Agent Swarm 并行编排框架（PARL）、Toggle Token 效率优化。关注算法与数据创新，而非 benchmark 秀肌肉。</description>
      <category>paper-reading</category>
      <pubDate>Sun, 01 Mar 2026 12:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="1-tldr"><a href="#1-tldr" class="heading-anchor">1. TL;DR</a></h2>
<p><a href="https://huggingface.co/moonshotai/Kimi-K2.5">Kimi K2.5</a> 有几个我觉得很值得关注的点：</p>
<ol>
<li><strong>固定视觉 token 总预算下，Early Fusion + 低视觉比例（10%）全面优于 Late Fusion + 高视觉比例（50%）</strong>——这与业界常见的先训练文本模型，然后&quot;后期大量注入视觉数据&quot;的做法相反</li>
<li><strong>Zero-Vision SFT</strong>：纯文本 SFT 即可激活视觉推理能力，人工标注的视觉轨迹数据反而损害泛化</li>
<li><strong>跨模态双向增强</strong>：Outcome-Base 视觉 RL 不仅不损害文本能力，反而提升了 MMLU-Pro（+1.7）、GPQA-Diamond（+2.1）等文本 benchmark</li>
<li><strong>Agent Swarm（PARL）</strong>：冻结 sub-agent、只训 orchestrator，通过 RL 学习动态任务分解与并行调度，延迟降低 3-4.5x</li>
<li><strong>Toggle</strong>：交替 budget-limited 和 standard scaling 阶段，解决 length overfitting，减少 25-30% output token 同时性能几乎无损</li>
</ol>
<blockquote>
<p>历史上比较相关的文章，建议对照阅读：</p>
<ul>
<li>Kimi K1.5: <a href="/post/kimi-k1.5-paper-reading-notes">深度解读 Kimi-K1.5，真正了解 RL 数据是怎么筛选的</a></li>
<li>Kimi K2 (Thinking): <a href="/post/kimi-k2-and-kimi-k2-thinking-notes">Kimi K2 和 K2 Thinking 深度解读：从预训练优化到 Agentic 能力训练的完整流程</a></li>
<li>DeepSeek R1: <a href="/post/deepseek-r1-paper-reading-notes">自顶向下方式深度解读 DeepSeek-R1</a></li>
</ul>
<p>本文虽然迟到，但是这篇 paper 真的很值得阅读，感谢老板 push 我做这个分享 🤣</p>
</blockquote>
<h2 id="2-多模态联合训练反共识的发现重点-1"><a href="#2-多模态联合训练反共识的发现重点-1" class="heading-anchor">2. 多模态联合训练：反共识的发现（重点 1）</a></h2>
<p>这是本文我认为最值得关注的部分之一。</p>
<p>在 K2.5 之前，业界做多模态训练的普遍做法是：先训练一个强大的纯文本模型，然后在训练后期以高比例（50%+）集中注入大量视觉数据。Kimi K2.5 的消融实验挑战了这一范式。</p>
<h3 id="21-消融实验early-fusion-低比例的优势"><a href="#21-消融实验early-fusion-低比例的优势" class="heading-anchor">2.1 消融实验：Early Fusion + 低比例的优势</a></h3>
<p>Table 1 的核心对比如下（<strong>固定总视觉 token 预算</strong>）：</p>
<p><img loading="lazy" src="/blog_imgs/kimi-k2.5/kimi-k2.5-text-vision-fusion.jpg" alt="kimi k2.5 text vision fusion" /></p>
<p><strong>关键结论</strong>：Early Fusion + 10% 视觉比例在几乎所有指标上全面优于 Late Fusion + 50% 视觉比例（仅 Text Reasoning 58.5 vs 58.6 略低于 Mid）。</p>
<p>这里需要理解实验设计的约束：<strong>三种策略消耗的视觉 token 总量是相同的</strong>。Late Fusion 在训练后 80% 才注入视觉数据，为了消化相同总量的视觉 token，它需要把视觉比例提到 50%；而 Early Fusion 从头就混入视觉数据，10% 的比例就能覆盖相同的总量。</p>
<p><img loading="lazy" src="/diagrams/kimi-k2.5/multimodal-fusion-comparison.svg" alt="多模态训练策略对比图" /></p>
<p>Appendix B.1 还展示了一个有趣的 <strong>dip-and-recover 现象</strong>：Late Fusion 在注入视觉数据时，文本能力会先显著下降再逐渐恢复（modality domain shift），后期强行注入大量视觉数据会对已经稳定的文本表征造成冲击。而 Early Fusion 的训练曲线保持平稳，没有这种 domain shift shock——因为两种模态从一开始就在共同演化。</p>
<blockquote>
<p>不过我个人觉得这个现象好像不是很明显的样子，没有我画的图这么明显，还是我不会读图？</p>
</blockquote>
<h3 id="22-zero-vision-sft纯文本-sft-激活视觉能力"><a href="#22-zero-vision-sft纯文本-sft-激活视觉能力" class="heading-anchor">2.2 Zero-Vision SFT：纯文本 SFT 激活视觉能力</a></h3>
<p>预训练之后的模型虽然已经能&quot;看懂&quot;图像，但还不能做复杂的视觉操作（裁剪、测量、计数等）。传统做法是人工标注视觉轨迹数据来教模型。</p>
<p>Kimi K2.5 提出了一个反直觉的方案——<strong>Zero-Vision SFT</strong>：SFT 阶段只用纯文本数据，所有图像操作都通过 IPython 代码来代理。模型学会用代码描述视觉操作（<code>binarize</code>、<code>crop</code> 等），而不是直接学习像素级的操作轨迹：</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="c1"># 问题：计算图中绿色区域占比</span>

<span class="c1"># 回复</span>
<span class="o">&lt;</span><span class="n">thought</span><span class="o">&gt;</span> <span class="n">需要用</span> <span class="n">binarization</span> <span class="n">分离绿色区域</span> <span class="o">&lt;/</span><span class="n">thought</span><span class="o">&gt;</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">cv2</span>
<span class="n">img</span> <span class="o">=</span> <span class="n">load_image</span><span class="p">(</span><span class="s2">&quot;input.png&quot;</span><span class="p">)</span>
<span class="n">hsv</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">cvtColor</span><span class="p">(</span><span class="n">img</span><span class="p">,</span> <span class="n">cv2</span><span class="o">.</span><span class="n">COLOR_BGR2HSV</span><span class="p">)</span>
<span class="n">mask</span> <span class="o">=</span> <span class="n">cv2</span><span class="o">.</span><span class="n">inRange</span><span class="p">(</span><span class="n">hsv</span><span class="p">,</span> <span class="n">lower_green</span><span class="p">,</span> <span class="n">upper_green</span><span class="p">)</span>
<span class="n">ratio</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">mask</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="o">/</span> <span class="n">mask</span><span class="o">.</span><span class="n">size</span>
<span class="n">答案</span><span class="err">：</span><span class="n">绿色区域占比</span> <span class="mf">32.5</span><span class="o">%</span>
</code></pre></div>
</code></pre>
<p>论文做了对比实验：<strong>text-vision SFT</strong>（加入了人工标注的视觉轨迹数据）效果反而更差。这是因为缺乏足够高质量的视觉数据，加上模型会过拟合于特定标注风格。</p>
<p><strong>个人理解</strong>：代码作为桥梁提供了精确的操作语义——<code>cv2.inRange</code> 就是精确的颜色过滤，不存在歧义。同时，前期联合预训练已经建立了视觉-文本的对齐，使得这种泛化成为可能。模型学会的是&quot;用程序化方式处理视觉信息&quot;的抽象能力，而不是记忆特定的视觉操作模式。这也呼应了后面预训练数据中 <strong>image-code paired data</strong>（HTML/React/SVG 代码 + 对应渲染截图）的设计——代码是一种既能被精确验证、又能描述视觉操作的通用语言。</p>
<h3 id="23-跨模态双向迁移"><a href="#23-跨模态双向迁移" class="heading-anchor">2.3 跨模态双向迁移</a></h3>
<p>这是另一个反直觉的发现。视觉 RL 训练之后，不仅视觉能力提升了，<strong>文本 benchmark 也提升了</strong>：</p>
<p><img loading="lazy" src="/blog_imgs/kimi-k2.5/cross-model-rl-improvement.jpg" alt="kimi k2.5 cross-model-rl-improvement" /></p>
<p>这是因为：视觉 RL 增强了模型对<strong>结构化信息提取的校准能力（calibration）</strong>，这种能力迁移到了类似需要结构化信息提取的文本任务中。</p>
<p>值得注意的是 K2.5 <strong>联合多模态 RL 的设计选择</strong>：RL 训练是按能力维度（knowledge / reasoning / coding / agentic）来组织 domain 的，而不是按模态（text / vision）分开训练。这和 <a href="/post/deepseek-grm-paper-reading-notes">DeepSeek-GRM</a> 跨模态优化的思路一致——按能力组织确保了同一能力维度下的文本和视觉任务共享 reward 信号，最大化了跨模态迁移。</p>
<h2 id="3-agent-swarmrl-训练出来的并行-agent-编排重点-2"><a href="#3-agent-swarmrl-训练出来的并行-agent-编排重点-2" class="heading-anchor">3. Agent Swarm：RL 训练出来的并行 Agent 编排（重点 2）</a></h2>
<h3 id="31-顺序执行的瓶颈"><a href="#31-顺序执行的瓶颈" class="heading-anchor">3.1 顺序执行的瓶颈</a></h3>
<p>现有的 Agent 系统（包括 <a href="/post/kimi-k2-and-kimi-k2-thinking-notes">Kimi K2 Thinking</a>）大多是单 agent 顺序执行：每一步推理都依赖上一步的结果。这意味着任务复杂度增加时，执行时间线性增长，上下文窗口也很快被占满。对于需要大规模信息检索和多源交叉验证的复杂任务，这种线性增长是不可接受的。</p>
<h3 id="32-parl-架构设计"><a href="#32-parl-架构设计" class="heading-anchor">3.2 PARL 架构设计</a></h3>
<p>K2.5 提出了 Agent Swarm，核心是 PARL（Parallel-Agent Reinforcement Learning）框架。</p>
<p><img loading="lazy" src="/blog_imgs/kimi-k2.5/orchestrator.jpg" alt="Agent Swarm PARL 架构图" /></p>
<p>核心设计是 <strong>可训练 Orchestrator + 冻结 Sub-agents</strong>。Sub-agents 从训练过程中间 checkpoint 提取，参数冻结不参与 RL 优化。</p>
<p><strong>为什么不做端到端的 co-optimization？</strong> 有两个原因：</p>
<ol>
<li><strong>Credit assignment ambiguity</strong>：最终结果正确不代表所有 sub-agent 都执行正确，端到端训练无法准确地把 reward 分配给各个 sub-agent</li>
<li><strong>Training instability</strong>：多 agent 联合优化的梯度问题，尤其是 sub-agent 的输出经过外部工具调用后梯度不可传播</li>
</ol>
<p>因此 sub-agent 的输出被视为&quot;环境观测&quot;（environment observation），orchestrator 只观测 sub-agent 的输入→输出映射，学习的是<strong>何时创建、如何分配、何时聚合</strong>。</p>
<blockquote>
<p>个人理解：这让人想到多 Agent 群体通信和协同进化的思路。K2.5 的 Agent Swarm 是一个初步探索——orchestrator 通过 RL 学会了动态创建异构 sub-agent 并调度。注意这里的 sub-agent 不是预设的固定角色（如 Coder/Researcher/Writer），而是根据任务动态创建的（如&quot;Quantum-Researcher&quot;、&quot;Pharma-Analyst&quot;），这种专门化是涌现的，不是预设的。</p>
</blockquote>
<h3 id="33-parl-reward-设计"><a href="#33-parl-reward-设计" class="heading-anchor">3.3 PARL Reward 设计</a></h3>
<p>训练并行编排器需要解决两个典型的失败模式。PARL 设计了三项 reward：</p>
<div class="katex-block">$$r_{PARL}(x, y) = \lambda_1 \cdot r_{parallel} + \lambda_2 \cdot r_{finish} + r_{perf}(x, y)$$</div>
<table>
<thead>
<tr>
<th style="text-align:left">组件</th>
<th style="text-align:left">功能</th>
<th style="text-align:left">解决的失败模式</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left"><span class="katex-inline">$r_{parallel}$</span></td>
<td style="text-align:left">奖励创建 sub-agent 的行为</td>
<td style="text-align:left"><strong>Serial Collapse（串行坍缩）</strong>：orchestrator 退化为单 agent 顺序执行，完全不用并行</td>
</tr>
<tr>
<td style="text-align:left"><span class="katex-inline">$r_{finish}$</span></td>
<td style="text-align:left">奖励 sub-agent 的完成率</td>
<td style="text-align:left"><strong>Spurious Parallelism（虚假并行）</strong>：创建一堆 sub-agent 但不完成任务，只是刷 <span class="katex-inline">$r_{parallel}$</span></td>
</tr>
<tr>
<td style="text-align:left"><span class="katex-inline">$r_{perf}$</span></td>
<td style="text-align:left">任务级最终性能</td>
<td style="text-align:left">真正的优化目标</td>
</tr>
</tbody>
</table>
<p>训练过程中 <span class="katex-inline">$\lambda_1, \lambda_2$</span> 逐渐退火到 0，确保最终只优化 <span class="katex-inline">$r_{perf}$</span>。这是一种常见的 RL 训练技巧：先用辅助 reward 做 shaping 帮 agent 跳出局部最优，再让辅助 reward 消失回归真实目标。</p>
<p><strong>举个具体例子</strong>：假设任务是&quot;调研量子计算在药物发现中的最新进展并撰写综述&quot;：</p>
<ul>
<li>如果 orchestrator 自己一步步搜索、阅读、总结，不创建任何 sub-agent（Serial Collapse），<span class="katex-inline">$r_{parallel} = 0$</span>，总 reward 较低</li>
<li>如果 orchestrator 创建了 5 个 sub-agent 但只有 1 个完成了子任务（Spurious Parallelism），<span class="katex-inline">$r_{finish}$</span> 因为低完成率而很低</li>
<li>理想情况：orchestrator 创建 3 个 sub-agent（量子计算综述 / 药物研发进展 / 交叉应用案例），都成功完成并返回结果，orchestrator 综合出高质量报告——三项 reward 都高</li>
</ul>
<h3 id="34-critical-steps并行效率的正确度量"><a href="#34-critical-steps并行效率的正确度量" class="heading-anchor">3.4 Critical Steps：并行效率的正确度量</a></h3>
<p>如何度量并行执行的实际时间成本？论文定义了 <strong>Critical Steps</strong>（关键步数）：</p>
<div class="katex-block">$$\text{CriticalSteps} = \sum_{t=1}^{T} \left( S_{main}^{[t]} + \max_i S_{sub, i}^{[t]} \right)$$</div>
<p>其中 <span class="katex-inline">$S_{main}^{[t]}$</span> 是第 <span class="katex-inline">$t$</span> 阶段主 agent 的步数，<span class="katex-inline">$S_{sub, i}^{[t]}$</span> 是第 <span class="katex-inline">$t$</span> 阶段第 <span class="katex-inline">$i$</span> 个 sub-agent 的步数。</p>
<p>这个定义等价于计算图的关键路径长度：每个阶段的耗时取决于最慢的那个 sub-agent。通过约束 Critical Steps 而非 Total Steps，模型被激励去做<strong>真正减少 wall-clock 时间的有效并行</strong>，而不是创建大量但低效的并发。</p>
<p>实验结果方面，Agent Swarm 在搜索类 benchmark 上带来了 3x-4.5x 的延迟降低，同时准确率也有提升（比如 BrowseComp 从 60.6% 提升到 78.4%）。</p>
<h3 id="35-agent-swarm-作为主动-context-management"><a href="#35-agent-swarm-作为主动-context-management" class="heading-anchor">3.5 Agent Swarm 作为主动 Context Management</a></h3>
<p>Agent Swarm 还有一个值得关注的副产品：<strong>主动的上下文管理</strong>。</p>
<p>传统的 context management 都是被动策略——上下文快满了就丢弃旧内容（Discard-all）、隐藏工具输出（Hide-Tool-Result）或做摘要（Summary）。这些都是信息已经膨胀之后的补救。</p>
<p>Agent Swarm 提供了一种主动的 <strong>context sharding</strong> 策略：每个 sub-agent 维护独立的局部 context，只有任务相关的输出返回给 orchestrator。orchestrator 不需要看到每个 sub-agent 搜索了什么网页、执行了什么中间代码，只需要看到最终结论。这实现了信息的局部性和模块化。</p>
<h2 id="4-rl-训练算法创新选读"><a href="#4-rl-训练算法创新选读" class="heading-anchor">4. RL 训练算法创新（选读）</a></h2>
<h3 id="41-token-level-clipping-机制"><a href="#41-token-level-clipping-机制" class="heading-anchor">4.1 Token-level Clipping 机制</a></h3>
<p>K2.5 的 RL 优化目标：</p>
<div class="katex-block">$$L_{RL}(\theta) = \mathbb{E}\left[\sum \text{Clip}\!\left(\frac{\pi_\theta(a_t|s_t)}{\pi_{old}(a_t|s_t)},\; \alpha,\; \beta\right) \cdot (r - \bar{r}) - \tau \cdot (\log\text{ratio})^2\right]$$</div>
<p>其中 <span class="katex-inline">$\log\text{ratio} = \log \pi_\theta(a_t|s_t) - \log \pi_{old}(a_t|s_t)$</span>。注意 <strong>Clip 作用于概率比值（ratio）而非 log-ratio</strong>，最后的 <span class="katex-inline">$(\log\text{ratio})^2$</span> 项是 KL 正则化项，用于进一步约束策略偏移。</p>
<p>与标准 PPO clipping 的关键区别：K2.5 <strong>严格基于 log-ratio 的区间 <span class="katex-inline">$[\alpha, \beta]$</span> 做 gradient masking</strong>，不依赖 advantage 的符号来决定 clipping 方向。直觉上，PPO 的 clipping 是&quot;根据 advantage 好坏来决定是否限制更新幅度&quot;，而 K2.5 是&quot;不管 advantage 好坏，只要策略偏移超过阈值就截断梯度&quot;。</p>
<p>这对 K2.5 的场景很重要：在长程多步工具使用任务中（如 Agent Swarm 的上百步工具调用），trajectory 非常长，off-policy divergence 在长 trajectory 中会被逐步放大。更严格的 token-level 约束能更好地控制策略偏移。这也是从 <a href="/post/kimi-k1.5-paper-reading-notes">Kimi K1.5</a> 延续过来的设计思路。</p>
<h3 id="42-toggletoken-效率优化"><a href="#42-toggletoken-效率优化" class="heading-anchor">4.2 Toggle：Token 效率优化</a></h3>
<p><strong>问题定义</strong>：RL 训练中有一个矛盾——如果用 budget 约束（限制输出长度）来提升 token 效率，模型会学到&quot;短答案捷径&quot;（length overfitting），导致在测试时无法利用更多的 compute 进行深度推理。</p>
<p>回顾一下进化路线：<a href="/post/kimi-k1.5-paper-reading-notes">K1.5</a> 的做法是在 reward 中直接加长度惩罚，公式很简单——越长奖励越小。这种方式比较粗暴，容易导致模型在需要深度推理时也输出过短的回答。</p>
<p>K2.5 的 Toggle 通过交替训练更优雅地解决了这个矛盾：每 m 个 iteration 切换一次 phase——</p>
<ul>
<li><strong>Phase 0（Budget-Limited）</strong>：约束输出长度，但有一个关键条件——<strong>只在模型平均准确率超过阈值 <span class="katex-inline">$\lambda$</span> 时才施加长度限制</strong>。Budget 用<strong>正确回答的 <span class="katex-inline">$\rho$</span> 分位数长度</strong>来估计。这确保了&quot;先学对，再学短&quot;。具体地，Phase 0 的条件 reward 公式为：</li>
</ul>
<div class="katex-block">$$r_{toggle}(x, y) = r_{perf}(x, y) - c \cdot \mathbb{1}[\text{len}(y) &gt; \text{budget}(x)] \cdot \mathbb{1}[\bar{r}_{perf} &gt; \lambda]$$</div>
<p>其中 <span class="katex-inline">$\text{budget}(x) = Q_\rho(\{\text{len}(y_i) \mid y_i \text{ is correct}\})$</span> 是正确回答长度的 <span class="katex-inline">$\rho$</span> 分位数，<span class="katex-inline">$\bar{r}_{perf}$</span> 是当前 batch 的平均准确率，<span class="katex-inline">$\lambda$</span> 是准确率阈值。只有当模型整体准确率已经够高（<span class="katex-inline">$\bar{r}_{perf} &gt; \lambda$</span>）时，才对超过 budget 的回答施加惩罚。</p>
<ul>
<li><strong>Phase 1（Standard Scaling）</strong>：允许最大 token 输出，鼓励充分推理，保持 test-time scaling 能力</li>
</ul>
<p>两个 phase 交替训练，让模型同时学会&quot;简洁推理&quot;和&quot;深度推理&quot;，而不是在同一个 reward 中权衡两者。这和 <a href="/post/slow-fast-thinking-from-qwen3-thinking-mixed-to-adacot-to-adathinking">AdaCoT/AdaThinking</a> 中自适应快慢思考的思路有相通之处，但 Toggle 是在训练阶段解决问题，而 AdaCoT 是在推理阶段。</p>
<p>实验结果：Toggle 实现 <strong>25-30% 的 token 减少</strong>，同时性能几乎无损甚至略有提升。</p>
<h3 id="43-reward-体系"><a href="#43-reward-体系" class="heading-anchor">4.3 Reward 体系</a></h3>
<p>K2.5 的 reward 设计覆盖面非常广，这里挑几个值得关注的。</p>
<h4 id="grmgenerative-reward-models"><a href="#grmgenerative-reward-models" class="heading-anchor">GRM（Generative Reward Models）</a></h4>
<p>K2.5 使用了生成式奖励模型，和 <a href="/post/deepseek-grm-paper-reading-notes">DeepSeek-GRM</a> 的思路一致：</p>
<ul>
<li>不是二元对错判断，而是<strong>细粒度多维评估</strong>（helpfulness、contextual relevance、aesthetic quality 等）</li>
<li>覆盖多种 agent 环境：chat agents、coding agents、search agents、artifact-generating agents</li>
<li>使用<strong>多套 rubric</strong> 防止 reward hacking——如果只用一套评估标准，模型很容易找到特定的 exploit</li>
</ul>
<h4 id="视觉任务专用-reward"><a href="#视觉任务专用-reward" class="heading-anchor">视觉任务专用 Reward</a></h4>
<p>有一些不太常见的视觉 reward 设计值得关注：</p>
<ul>
<li><strong>Visual puzzle reward</strong>：合成复杂的视觉谜题（找规律、空间推理等），用 LLM verifier（K2）提供反馈。这类任务很难设计 rule-based reward，所以用强模型做 judge</li>
<li>其他还包括：IoU soft matching（定位）、Gaussian-weighted distance（关键点检测）、normalized edit distance（OCR）、absolute difference（计数）</li>
</ul>
<h4 id="budget-control-reward"><a href="#budget-control-reward" class="heading-anchor">Budget-control reward</a></h4>
<p>对于有明确答案的任务（数学、代码），使用 rule-based 的 outcome reward。在 Toggle 的 Phase 0 中，额外叠加 budget-control reward 来约束输出长度——当输出超过 budget 时惩罚，在 budget 内则不影响。</p>
<h2 id="5-模型架构与训练-pipeline选读"><a href="#5-模型架构与训练-pipeline选读" class="heading-anchor">5. 模型架构与训练 pipeline（选读）</a></h2>
<h3 id="50-foundation从-k2-到-k25"><a href="#50-foundation从-k2-到-k25" class="heading-anchor">5.0 Foundation：从 K2 到 K2.5</a></h3>
<p>K2.5 和 <a href="/post/kimi-k2-and-kimi-k2-thinking-notes">Kimi K2</a> 共享同一套 MoE 架构——1T 总参数、32B 激活参数（~3.1% 激活）、384 个 Expert（每 token 激活 8 个，稀疏比 48x）。K2 的 MuonClip 优化器（Muon + QK-Clip，实现 15.5T token 零 loss spike 训练）也被原封不动地继承。需要注意的是，<strong>K2 是纯文本模型</strong>（原文：&quot;pre-trained on 15 trillion high-quality text tokens&quot;），K2.5 的核心工作就是增加原生多模态和 Agent 并行能力。</p>
<p>K2.5 在 K2 基础上的<strong>核心增量</strong>是：</p>
<table>
<thead>
<tr>
<th style="text-align:left">维度</th>
<th style="text-align:left">K2</th>
<th style="text-align:left">K2.5 新增</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left">模态</td>
<td style="text-align:left">纯文本</td>
<td style="text-align:left">原生多模态（文本 + 图像 + 视频）</td>
</tr>
<tr>
<td style="text-align:left">视觉编码器</td>
<td style="text-align:left">-</td>
<td style="text-align:left">MoonViT-3D（基于 SigLIP-SO-400M）</td>
</tr>
<tr>
<td style="text-align:left">预训练</td>
<td style="text-align:left">15.5T 文本预训练</td>
<td style="text-align:left">在 K2 结构基础上三阶段：ViT → Joint → Long-context</td>
</tr>
<tr>
<td style="text-align:left">上下文长度</td>
<td style="text-align:left">128/256k</td>
<td style="text-align:left">扩展到 262K（看表格）</td>
</tr>
<tr>
<td style="text-align:left">SFT</td>
<td style="text-align:left">文本 + Agentic 数据</td>
<td style="text-align:left">新增 Zero-Vision SFT（纯文本激活视觉）</td>
</tr>
<tr>
<td style="text-align:left">Agent</td>
<td style="text-align:left">单 agent 顺序执行</td>
<td style="text-align:left">Agent Swarm（PARL 并行编排）</td>
</tr>
<tr>
<td style="text-align:left">Token 效率</td>
<td style="text-align:left">—</td>
<td style="text-align:left">Toggle 交替训练（-25~30% tokens）</td>
</tr>
</tbody>
</table>
<h3 id="51-vit-基础-moonvit-3d"><a href="#51-vit-基础-moonvit-3d" class="heading-anchor">5.1 ViT 基础 + MoonViT-3D</a></h3>
<p>先简要介绍一下 ViT（Vision Transformer）的基本原理，帮助不熟悉的读者理解后续内容。</p>
<p><strong>ViT 的核心思想</strong>就是把图像当成 token 序列来处理：把图像切成若干个 patch（通常是 16x16 像素），每个 patch 就相当于 NLP 中的一个 token。然后加上位置编码（让模型知道每个 patch 在图像中的空间位置），送入标准的 Transformer Encoder 处理，最后输出视觉特征。</p>
<p><img loading="lazy" src="/diagrams/kimi-k2.5/vit-moonvit3d.svg" alt="ViT 工作原理 + MoonViT-3D 改进" /></p>
<p>K2.5 使用的 <strong>MoonViT-3D</strong> 在标准 ViT 基础上做了三个关键改进：</p>
<ol>
<li><strong>NaViT Packing</strong>：支持<strong>原生分辨率</strong>输入，不需要把图像 resize 到固定尺寸（传统 ViT 需要 resize 到 224x224，会损失信息）。不同分辨率的图像可以 pack 到同一个 batch</li>
<li><strong>3D 时序压缩</strong>：处理视频时，每 4 帧为一组做 temporal pooling，实现 <strong>4x token 压缩</strong>。这让模型在相同上下文窗口内能处理 4 倍更长的视频</li>
<li><strong>图像视频完全共享权重</strong>：图像可以看作单帧视频，图像预训练的知识可以零迁移到视频理解</li>
</ol>
<p>MoonViT-3D 继承自 SigLIP-SO-400M，整体架构是 MoonViT-3D + MLP Projector + Kimi K2 MoE（1T 总参数，32B 激活参数，384 个 Expert，每 token 激活 8 个）。</p>
<h3 id="52-预训练三阶段"><a href="#52-预训练三阶段" class="heading-anchor">5.2 预训练三阶段</a></h3>
<p><img loading="lazy" src="/blog_imgs/kimi-k2.5/training-stages.jpg" alt="traning stages" /></p>
<p>第一阶段只训练 ViT，让视觉编码器先具备基本的视觉理解能力。第二阶段开始联合训练 ViT 和 LLM——这正是前面消融实验中 Early Fusion 策略的实现，从 15T token 的联合预训练一开始就以 ~10% 的比例混入视觉数据。第三阶段逐步扩展到长上下文（32K → 262K）。</p>
<h3 id="53-预训练数据亮点"><a href="#53-预训练数据亮点" class="heading-anchor">5.3 预训练数据亮点</a></h3>
<p>附录 B 披露了一些有意思的数据构建细节：</p>
<p><strong>代码数据增强</strong>：除了常规的代码数据，K2.5 还加入了 repo-level 的代码（完整仓库结构）、GitHub Issues / Code Reviews / Commit History。这类数据帮助模型理解代码的演进过程和协作模式，对 agentic coding 任务（如 SWE-Bench）很有帮助。</p>
<p><strong>视觉数据 7 类</strong>：caption、interleaving、OCR、knowledge、video、agent、grounding。其中特别值得注意的是 <strong>image-code paired data</strong>——HTML/React/SVG 代码与其对应的渲染截图配对。这种数据直接建立了从视觉布局到代码实现的映射，对 web 相关的 agent 任务（如 OSWorld、Computer Use）至关重要。</p>
<h3 id="54-depdecoupled-encoder-process"><a href="#54-depdecoupled-encoder-process" class="heading-anchor">5.4 DEP（Decoupled Encoder Process）</a></h3>
<p>简要提一下 infra 层面的创新。多模态训练的一个难点是视觉编码器通常只在 Pipeline Parallelism 的第一个 stage 运行，导致负载不均。DEP 将视觉编码器解耦为独立进程，让所有 GPU 并行处理视觉数据，消除了这个瓶颈。效果是多模态训练效率达到纯文本训练的 <strong>90%</strong>。</p>
<h2 id="6-总结与思考"><a href="#6-总结与思考" class="heading-anchor">6. 总结与思考</a></h2>
<p>读完 K2.5 的论文，几个核心启发：</p>
<ol>
<li>
<p><strong>联合训练 &gt; 分阶段训练，而且比例不是越多越好</strong>。在固定视觉 token 总预算的约束下，10% 的视觉数据比 50% 效果更好，关键在于注入时机（从头注入）而非数量。这对多模态模型的训练策略有直接的指导意义：与其在后期堆视觉数据量，不如在早期就建立跨模态的联合表示。</p>
</li>
<li>
<p><strong>代码是跨模态的通用桥梁</strong>。Zero-Vision SFT 的成功说明代码提供了一种精确的操作语义，可以作为文本和视觉之间的桥梁。</p>
</li>
<li>
<p><strong>Agent 并行化可以通过 RL 训练出来，而不需要预设规则</strong>。Orchestrator 通过 PARL 自然学会了何时并行、如何分配、何时聚合，这种能力具有泛化性。而且 PARL 的 reward 设计（auxiliary reward shaping + annealing）可以推广到其他需要&quot;引导探索&quot;的 RL 场景。</p>
</li>
<li>
<p><strong>Token 效率优化的技术在不断演进</strong>：K1.5 的长度惩罚 reward → K2.5 的 Toggle 交替训练 → 配合 Token-level Clipping 控制策略偏移。这条技术路线越来越精细，越来越接近&quot;让模型自主决定何时简洁、何时深入&quot;。</p>
</li>
</ol>
<p>从 K1.5 到 K2 到 K2.5，每一代都在前一代的基础上解决了新的核心问题，而且技术方案的设计越来越优雅。</p>
<h2 id="参考"><a href="#参考" class="heading-anchor">参考</a></h2>
<ul>
<li>Kimi K2.5 论文：<a href="https://arxiv.org/abs/2602.02276">arXiv:2602.02276</a></li>
<li>模型开源：<a href="https://huggingface.co/moonshotai/Kimi-K2.5">HuggingFace - moonshotai/Kimi-K2.5</a></li>
<li>历史相关解读：
<ul>
<li><a href="/post/kimi-k1.5-paper-reading-notes">深度解读 Kimi-K1.5，真正了解 RL 数据是怎么筛选的</a></li>
<li><a href="/post/kimi-k2-and-kimi-k2-thinking-notes">Kimi K2 和 K2 Thinking 深度解读</a></li>
<li><a href="/post/deepseek-r1-paper-reading-notes">自顶向下方式深度解读 DeepSeek-R1</a></li>
<li><a href="/post/deepseek-grm-paper-reading-notes">DeepSeek-GRM 通用奖励模型</a></li>
<li><a href="/post/gemini-2.5-tech-report-reading-note">Gemini 2.5 技术报告阅读笔记</a></li>
<li><a href="/post/slow-fast-thinking-from-qwen3-thinking-mixed-to-adacot-to-adathinking">自适应快慢思考：Qwen3 → AdaCoT → AdaThinking</a></li>
</ul>
</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>2025，浪潮与拧巴</title>
      <link>https://yuanchaofa.com/blog/2025-year-summary</link>
      <guid>https://yuanchaofa.com/blog/2025-year-summary</guid>
      <source url="https://yuanchaofa.com/rss.xml">2025，浪潮与拧巴</source>
      <description>2025年，乎乎出生，B站技术分享视频播放破百万，也是工作以来最忙碌的一年。在爸爸、打工人、内容创作者的身份间拧巴挣扎，在AI巨浪中试图不做浪潮过后什么也没得到的大多数人。</description>
      <category>年终总结</category>
      <pubDate>Mon, 23 Feb 2026 16:07:00 GMT</pubDate>
      <content:encoded><![CDATA[
<ul>
<li>历史年终总结
<ul>
<li><a href="https://yuanchaofa.com/blog/2024-year-summary">《2024，公开表达元年》</a></li>
<li><a href="https://yuanchaofa.com/blog/2023-year-summary">《2023，新的开始》</a></li>
<li><a href="https://yuanchaofa.com/blog/2022-year-summary">《2022，激荡中的平淡》</a></li>
<li><a href="https://yuanchaofa.com/blog/2021-year-summary">《2021，乌云与曙光》</a></li>
<li><a href="https://yuanchaofa.com/blog/2020-year-summary">《2020年过去了，我不怀念它》</a></li>
</ul>
</li>
</ul>
<h2 id="1-乎乎爸爸还有待提升"><a href="#1-乎乎爸爸还有待提升" class="heading-anchor">1.  乎乎：爸爸还有待提升</a></h2>
<p>折腾妈妈九个多月的乎乎出生了，我很开心。尤其是前段时间，抱着她，转过头的一瞬间突然叫一声「爸」，顿时有些恍惚，原来不知不觉间已经能隐约发出「爸」的声音了。</p>
<p>一个月前，点点说：“在我们决定要娃的时候，你说，「乎乎出生后，我所有的业余时间都会用来陪乎乎」，但你没有，你不是一个好爸爸。” 事实确实如此，2025 年是我自工作以来最忙碌的一段时间，各方面的事情都非常的多，我没有很多的时间陪乎乎导致点点有一周很崩溃，离家出走。我也因各种事情搞得头昏脑胀的，一度觉得只有不上班或者离婚才能解决问题<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>。</p>
<p>我尝试过每天通勤回龙岗，但是坚持不了几天身体就承受不住了，每天上班都脑袋昏昏沉沉；也试了带睡几天，可是心脏直砰砰的跳，根本睡不着，生怕要自己先猝死了。我时常想，其他人是怎么平衡工作和家庭的呢？是我精力太差了，还是其他人精力太好了呢？</p>
<p>我不是一个好爸爸，按照点点平时给我的打分，我只有 <strong>B-</strong>。过年放假的时候倒是花了更多的时间陪孩子，但如果工作忙起来，我周末还能有能量这样吗？希望 26 年能到 A，让乎乎和点点都给我打分，靠更频繁的奖励信号纠正爸爸不当的行为。</p>
<h2 id="2-工作more-agency"><a href="#2-工作more-agency" class="heading-anchor">2. 工作：More Agency</a></h2>
<p>第一个部分其实也提到了工作，似乎核心就一个字「忙」，但真有这么忙吗？也许是有的，有几个离职的同事都表示变好了。但忙出了什么东西吗？我觉得也是有的，团队的业务产出其实都挺好的，当然 2025 年全年来说，我个人产出也是超出<strong>自己</strong>预期的<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>，也有更多的思考🤔，至于别人怎么看便不得而知了。</p>
<p>这一年，我自己的工作主线是：Agent 落地。主要的目标是：给模型更多的「自主决策空间，Let Agent to have More Agency」，主要的方式也和业界主流发展方式一致，围绕着两点：「更好的 Context」以及「给定 Context 下更好的执行」。由于今年是我做公开表达更多的一年，因此<strong>对于业界发展的跟进是比较快的</strong>，比如近期 Claude Code / Codex 都发表了一些关于 Prompt Caching 对于 Agent 设计的影响，我春节期间也写了<a href="https://yuanchaofa.com/post/understanding-kv-cache-and-prompt-cache-basics">三篇文章</a>来介绍「<a href="https://yuanchaofa.com/post/prompt-cache-design-for-llm-agents">Agent 系统中的 Prompt Caching 设计</a>」。</p>
<p>此外，今年也暴露了自己比较大的问题：更擅长自己执行而不是协作。因为今年自己作为子项目 Owner，需要对于项目的进度有更强的规划和跟进能力。尽管现在都在强调 AI 时代要充分发挥个体能力，但是就目前（2026-02-23）而言，我觉得协作能力还是挺重要的，因为协作是意味着多线程的管理和表达能力，这在 AI 时代更是放大人与人区别的关键点。</p>
<p>其实大模型越来越强，我越来越焦虑，牛逼的人已经产出 100X，而自己的效率却没有本质变化。<strong>我仿佛看到了上个改革开放年代或者更近的所谓的移动互联网腾飞之年，有大量的人攫取了巨大的财富，仿佛是个人都能吃上所谓的时代红利，但大多数人，浪潮过后好像也没什么变化</strong>。</p>
<blockquote>
<p>而这一次，AI 巨浪滚滚向前，又创造了一大波造富神话，身处其中，感觉每天都是翻天覆地的变化，各种新的产品怎么也跟踪不过来；但似乎：每一个好像都和我没什么太大的关系，我能提升效率吗？我能赚更多的钱吗？我能更快到达自由的彼岸吗？所以更大的可能性是：浪潮过后，自己什么也没得到，就像哪些在历史机遇中平平淡淡的大多数人一样。</p>
</blockquote>
<h2 id="3-公开表达生与死"><a href="#3-公开表达生与死" class="heading-anchor">3. 公开表达：生与死</a></h2>
<h3 id="31-技术表达永生"><a href="#31-技术表达永生" class="heading-anchor">3.1 技术表达永生</a></h3>
<p>去年，我在年终总结的时候写：<a href="https://yuanchaofa.com/blog/2024-year-summary">2024年是公开表达元年</a>。是的，2025 年，我尝试了更多的个人表达：</p>
<ul>
<li>制作 <a href="https://space.bilibili.com/12420432">B 站大模型相关教学视频</a>共 20 个，<strong>累计播放超过 100W</strong>，获得 33k 用户关注，且收获了非常非常的鼓励和赞同，已经远远远超出了预期。
<ul>
<li>立个 FLAG：我一定要做个用户墙来进行自我激励</li>
</ul>
</li>
<li>写<a href="https://yuanchaofa.com/post">技术 BLOG</a> 一共 12 篇，平均下来每个月产出一篇低质量博客（苏神每年每个月产出<a href="https://kexue.fm/content.html">高质量的四篇</a>，我真的非常非常的佩服，求真务实的学习榜样）。尽管我的 blog——<a href="https://yuanchaofa.com">chaofa 用代码打点酱油</a>相对于「<a href="kexue.fm">科学空间</a>」具有巨大的差距，但是依然得到了很多大佬的认同，比如
<ul>
<li><a href="https://github.com/hiyouga/LlamaFactory">llama_factory</a> 的作者给我发消息表示赞同</li>
<li>英伟达某负责人加我微信说文章写得不错</li>
<li>公司内很多人（其中有好几个都是团队负责人）私信我说看过我的文章</li>
<li>GitHub 有 4k+ Star</li>
<li>和大佬们有了一丝微不足道的「表层交集」，此类有不少，我感觉心里很满足</li>
</ul>
</li>
<li>坚持了 <strong>10 个月</strong>每个月都写「<a href="https://yuanchaofa.com/tag/month-summary">个人月度总结</a>」，从 2 月份到 11 月份。但由于家庭、工作、其他琐事的变多，我实在是精疲力尽，无法坐在电脑前敲下 2025 年 12 月和 2026 年 1 月的月度总结<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>。</li>
</ul>
<h3 id="32-商业化之死"><a href="#32-商业化之死" class="heading-anchor">3.2 商业化之死</a></h3>
<p>我在多次的月度总结中提到「商业化」，比如：<a href="https://yuanchaofa.com/blog/2025-08-month-summary">2025-08-孙宇晨真的很值得学习</a>，<a href="https://yuanchaofa.com/blog/2025-09-month-summary">2025-09-合法赚钱就是高尚的</a>，<a href="https://yuanchaofa.com/blog/2025-10-month-summary">2025-10-一个程序员对自媒体商业化的深度复盘</a>等，如果不明所以的朋友可能会觉得赚了很多钱，但事实远非如此，加起来半个月工资都不到，可以说这方面是比较失败的。</p>
<p>但也不是完全没有收获，我觉得通过<a href="https://yuanchaofa.com/blog/2025-07-month-summary#5-%E8%A7%86%E9%A2%91%E5%95%86%E4%B8%9A%E5%8C%96">视频商业化</a>的尝试，我理解了很多的商业化行为，对于完整的商业化闭环也有了更深入的思考——打工是一个「期望风险更低」「期望收获更高」的商业化行为<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>。</p>
<p>由于前面的尝试以及认知，我不能再花时间去接所谓的商单了，我觉得商单确实是一种毒药，看上去好像赚了点小钱（见第一段），但实际上挺麻烦的，付出和收入不成正比。还是需要找到自己核心的竞争力和核心产品，才可能真正占用尽量少业余时间获得更高的复利。</p>
<p>此外，由于后面商单的出现，我甚至开始有点羞于向别人说：<strong>我做了一个技术频道叫做 chaofa 用代码打点酱油，对标油管的 Andrej Karpathy</strong><sup class="footnote-ref"><a href="#fn5" id="fnref5">[5]</a></sup>。所以 2026 年 1-2 月的时候，有 4 个新商单找我，我都拒绝了<sup class="footnote-ref"><a href="#fn6" id="fnref6">[6]</a></sup>，我还是想做更纯粹的表达（当然肯定是想赚钱的，所以还在拧巴中）。</p>
<p>所以以后会怎么样呢？暂时还不知道🤷‍♀️</p>
<h2 id="4-回顾与展望"><a href="#4-回顾与展望" class="heading-anchor">4. 回顾与展望</a></h2>
<h3 id="41-回顾去年"><a href="#41-回顾去年" class="heading-anchor">4.1 回顾去年</a></h3>
<ul>
<li>健身：一塌糊涂，基本没怎么去。</li>
<li>播客
<ul>
<li>如愿和硬地骇客录制了一期播客，<a href="https://www.xiaoyuzhoufm.com/episode/67bc6e1605a90dfd0d8decf1">EP94 对话“一手撸算法，一手做视频”的知名UP主&quot;朝发&quot;- 每个人都应该公开表达？</a>，追星成功。</li>
<li>然后把自己的播客名字改成了<a href="https://www.xiaoyuzhoufm.com/podcast/625a89560cab7e0abb960b6d">逃逸速度 Escape Velocity</a>，并且和两个同事聊了一期《<a href="https://www.xiaoyuzhoufm.com/episode/6804f10ecdd692da1585b4ac">大模型算法岗校招社招上岸公式</a>》</li>
<li><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2026/20260223185859.png" alt="image.png|576x627" /></li>
</ul>
</li>
<li>博客—— yuanchaofa.com
<ul>
<li>和 24 相比博客的<strong>各项数据都翻倍</strong>了，完成目标。主要得益于更新的 12 篇技术 Blog，其实「个人总结没人看的」，都是我自己再看。</li>
<li><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2026/1771844599850.webp" alt="1771844599850" /></li>
</ul>
</li>
<li>投资
<ul>
<li>投资继续一塌糊涂。眼见已经回本了，然后买了 FIGMA，亏出底裤，在大牛市亏成傻叉。从盈利到巨额无底洞。</li>
</ul>
</li>
<li>读书
<ul>
<li>什么，读书是何物？</li>
</ul>
</li>
</ul>
<h3 id="42-展望-26"><a href="#42-展望-26" class="heading-anchor">4.2 展望 26</a></h3>
<p>工作自不必多说，依然是2026 最需要重点投入的事情，要积极跟进前沿，与 LLM 多多探讨业务、技术的发展，争取在工作上有进一步的突破。</p>
<p>另外，生产变革也已经发生，生产方式已经发生了巨变，尤其是已经看到非常多的人在 AI 的加持下做出了让人瞩目的成绩，所以 2026 要更加彻底的拥抱 AI Coding，应该说在创造一些自己的 Product，而不是隔岸观火。</p>
<p>所以我斥巨资买了一个域名叫做：<a href="https://apecode.ai">ApeCode.ai</a>，代码都会放到 <a href="https://github.com/ApeCodeAI">github.com/ApeCodeAI</a> 下，Slogan 想了好多有意思的：</p>
<ul>
<li>Ape Code, You Sleep (我最喜欢这个)</li>
<li>Ape Code, Not You</li>
<li>Unleash Your Inner Builder</li>
<li>Even Ape can BUILD PRODUCTS with the help of AI</li>
</ul>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2026/20260223202709.png" alt="image.png" /></p>
<hr />
<p>写于：2026 年 2 月 23 日 20:38:56 新年春节假期返工前一天晚<sup class="footnote-ref"><a href="#fn7" id="fnref7">[7]</a></sup></p>
<h2 id="ref"><a href="#ref" class="heading-anchor">Ref</a></h2>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>实际上我和点点几乎没有什么矛盾，目前也没有太大的经济压力，只是人都会有情绪崩溃的时候，造成一些不可思议的想法。「不是客观上带孩子时间的问题，而是在带孩子的情绪价值和参与度上的问题」导致点点觉得我投入不够。 <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>工作这么多年，虽然也有绩效不错的时候，但我很少自己给自己评价超出预期（这只是说的自我评估，我并不知道真实绩效）。不过这一年，不管别人怎么看，我自己是尽全力了，业务产出也还不错。另外，由于 25 年也作为面试官面试了非常多的候选人，让我对各种事情有了更加深刻的认识，也让我更加坚定的要建立一套自己的评估体系。 <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>我记得 11 月度总结的时候，我说：还有一个月就 2025 年全勤了，没想到最后功亏一篑了。有朋友可能会问，AI 写得比你写得好多了，为什么不用 AI？我的答案是：这一类个人思考和总结的东西，我不想用 AI，因为本来是写给自己看的，如果我不知道「下笔的时刻我在思考什么」，那么写出的文章又有什么意义呢？思考过程的本身比结果更让人着迷。要是对此感兴趣的同学也可以关注：<a href="https://yuanchaofa.com/llms-zero-to-hero/chaofa-wechat-official-account.png">公众号——chaofa 用代码打点酱油</a> <a href="#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>正好对应了我一直说的，打工一定要投入更多的时间。公开表达只能是业余时间中 20% 的精力，因为收益真的没想象中高。自媒体是一个极度放大幸存者偏差的地方。Keep it in mind. <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn5" class="footnote-item"><p>这是最初的愿景，因为我也是完全从零手写代码/或者读一些前沿的论文，这也是吸引很多「专业」的同行的原因，因此我的视频受众有非常多各种大厂在职的算法工程师。 <a href="#fnref5" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn6" class="footnote-item"><p>26 年 1 月份的发的两个视频都是元旦期间做的，都是很早很早以前接的，只是平常时间做 <a href="#fnref6" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn7" class="footnote-item"><p>忽然想起导演 BK 写心动 CEO 的黄一孟的一篇小短文，&quot;<a href="https://x.com/bkingfilm/status/1844268330620014739">儿子出生的那天，他开车回家走在高架桥上，对面的夕阳照进他的车里，洒在他的脸上，他很幸福。事业成功，家庭幸福，儿女双全，这是黄一孟人生中最开心的时光</a>&quot;。 <a href="#fnref7" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    <item>
      <title>Agent 系统中的 Prompt Caching 设计（下）：上下文管理与子代理架构</title>
      <link>https://yuanchaofa.com/post/agent-context-management-and-sub-agents</link>
      <guid>https://yuanchaofa.com/post/agent-context-management-and-sub-agents</guid>
      <source url="https://yuanchaofa.com/rss.xml">Agent 系统中的 Prompt Caching 设计（下）：上下文管理与子代理架构</source>
      <description>深入分析 AI Agent 的上下文管理策略：Context Rot 问题、Cache-Safe Compaction、Plan 模式演进、文件系统作为延展记忆、子代理 Cache 友好设计，以及 The Bitter Lesson。</description>
      <category>agent system design</category>
      <pubDate>Sun, 22 Feb 2026 15:06:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="0-阅读收获-takeaway"><a href="#0-阅读收获-takeaway" class="heading-anchor">0. 阅读收获 (takeaway)</a></h2>
<p>读完本文，你将了解：</p>
<ul>
<li>Context Rot（上下文腐烂）：为什么更大的 context window 不是万能解药</li>
<li>Cache-Safe Compaction：如何在压缩 context 时不破坏 cache</li>
<li>Plan 模式的演进：从 todo.md 到专门 planner agent</li>
<li>文件系统 &amp; Just-in-Time Context：Agent 的&quot;延展记忆&quot;</li>
<li>子代理的 Cache 友好设计：90%+ prefix reuse 是怎么做到的</li>
<li>The Bitter Lesson：哪些设计是持久的，哪些会被模型进步淘汰</li>
</ul>
<blockquote>
<p><strong>前置知识</strong>：本文是 <a href="/post/prompt-cache-design-for-llm-agents">Agent 系统中的 Prompt Caching 设计（上）</a> 的续篇。如果你还没读过，建议先了解 Cache 破坏机制、Prompt 布局和工具管理策略。更基础的概念见 <a href="/post/understanding-kv-cache-and-prompt-cache-basics">理解 KV Cache 与 Prompt Caching</a>。</p>
</blockquote>
<h2 id="1-context-rot上下文腐烂"><a href="#1-context-rot上下文腐烂" class="heading-anchor">1. Context Rot：上下文腐烂</a></h2>
<p>Anthropic 的研究指出：<strong>随着 context 中 token 增加，模型注意力分散，性能下降</strong>。</p>
<p>Attention 中每个 token 要和所有其他 token 建立 <span class="katex-inline">$n^2$</span> 的 pairwise 关系。Context 越长 → 每个 token 的&quot;注意力预算&quot;越少 → 模型可能&quot;忘记&quot;早期的重要指令，或被大量 tool output 稀释关键信息。</p>
<blockquote>
<p><strong>更大的 context window 不是万能解药。</strong> 能塞进去不代表模型能有效利用。</p>
</blockquote>
<p>这就是为什么 Agent 不能简单地把所有信息堆进 context——我们需要主动管理。</p>
<h2 id="2-compactioncache-safe-的上下文压缩"><a href="#2-compactioncache-safe-的上下文压缩" class="heading-anchor">2. Compaction：Cache-Safe 的上下文压缩</a></h2>
<p>压缩是解决 context 增长的关键手段，但必须是 <strong>cache-safe</strong> 的。</p>
<h3 id="21-claude-code-的-cache-safe-compaction"><a href="#21-claude-code-的-cache-safe-compaction" class="heading-anchor">2.1 Claude Code 的 Cache-Safe Compaction</a></h3>
<p><img loading="lazy" src="/blog_imgs/cache-safe-compaction.png" alt="Cache-Safe Compaction 流程" /></p>
<p>Claude Code 的压缩策略非常精巧：</p>
<ul>
<li>压缩请求使用<strong>完全相同的 system prompt + tools + 对话前缀</strong></li>
<li>只在末尾追加 compaction prompt</li>
<li>这样压缩请求本身就能<strong>复用父会话的 cache</strong></li>
<li>预留 &quot;compaction buffer&quot;——context 快满之前就开始压缩</li>
</ul>
<h3 id="22-openai-codex-的-responsescompact"><a href="#22-openai-codex-的-responsescompact" class="heading-anchor">2.2 OpenAI Codex 的 /responses/compact</a></h3>
<p>Codex 提供专门的 API 端点：</p>
<ul>
<li>返回压缩后的 item 列表 + encrypted compaction 项目</li>
<li>保留模型对原始对话的&quot;潜在理解&quot;（指的是把 summary 内容放到上下文中）</li>
<li>超过 <code>auto_compact_limit</code> 自动触发</li>
</ul>
<p>两者共同点：<strong>压缩是必要的，但压缩过程本身不能破坏已有的 cache。</strong></p>
<h2 id="3-注意力操纵plan-模式的演进"><a href="#3-注意力操纵plan-模式的演进" class="heading-anchor">3. 注意力操纵：Plan 模式的演进</a></h2>
<p>Agent 如何确保模型&quot;聚焦&quot;在正确的事情上？三家有一个有趣的演进过程。</p>
<h3 id="31-manus-的演进"><a href="#31-manus-的演进" class="heading-anchor">3.1 <strong>Manus 的演进</strong></a></h3>
<ul>
<li>初期用 <code>todo.md</code> → 约 <strong>1/3 actions 浪费在更新 todo</strong> 上！</li>
<li>最新：专门的 <strong>planner agent</strong> 替代 → 效率大幅提升</li>
</ul>
<h3 id="32-claude-code-的-plan-mode"><a href="#32-claude-code-的-plan-mode" class="heading-anchor">3.2 <strong>Claude Code 的 Plan Mode</strong></a></h3>
<ul>
<li>独立规划阶段 → 用户审批 → 再执行</li>
<li>模型可<strong>自主调用</strong> <code>EnterPlanMode</code></li>
</ul>
<h3 id="33-codex-的-updateplan"><a href="#33-codex-的-updateplan" class="heading-anchor">3.3 <strong>Codex 的 update_plan</strong></a></h3>
<ul>
<li>执行中的一个工具</li>
<li>无需用户审批，更轻量</li>
</ul>
<table>
<thead>
<tr>
<th>方案</th>
<th>独立阶段？</th>
<th>用户审批？</th>
<th>自主控制？</th>
</tr>
</thead>
<tbody>
<tr>
<td>Manus planner agent</td>
<td>独立 agent</td>
<td>无需审批</td>
<td>Agent 决定</td>
</tr>
<tr>
<td>Claude Code Plan Mode</td>
<td>独立阶段</td>
<td>需要审批</td>
<td>模型可自主进入</td>
</tr>
<tr>
<td>Codex update_plan</td>
<td>也有独立阶段</td>
<td>无需审批</td>
<td>执行中随时调用</td>
</tr>
</tbody>
</table>
<h2 id="4-保留错误内容"><a href="#4-保留错误内容" class="heading-anchor">4. 保留错误内容</a></h2>
<p>一个有趣的共识：<strong>不要删除失败的 action 和 observation。</strong></p>
<p>Manus 不会从 context 中删除失败的工具调用结果。双重好处：</p>
<ol>
<li><strong>保持 append-only</strong> → 保护 cache</li>
<li><strong>模型从错误中学习</strong> → 调整后续策略</li>
</ol>
<blockquote>
<p><strong>错误恢复是真正 agentic 行为的标志。</strong> 看不到自己犯的错，怎么学会避免？</p>
</blockquote>
<h2 id="5-文件系统-just-in-time-context"><a href="#5-文件系统-just-in-time-context" class="heading-anchor">5. 文件系统 &amp; Just-in-Time Context</a></h2>
<p>Agent 不需要把所有信息都放在 context 里——文件系统可以作为&quot;延展记忆&quot;。</p>
<h3 id="51-manus-的文件系统策略"><a href="#51-manus-的文件系统策略" class="heading-anchor">5.1 Manus 的文件系统策略</a></h3>
<ul>
<li>文件系统当&quot;无限 context&quot;：执行结果写入文件，context 只保留引用</li>
<li><strong>Full vs Compact 表示</strong>：新结果保留完整内容（文件读写的结果），旧结果替换为文件路径引用（压缩的时候，这时候看上去已经破坏了 prompt caching）。来源于：<a href="https://rlancemartin.github.io/2025/10/15/manus/">Manus webinar notes (2025.10): Context Reduction/Isolation/Offloading</a>
<ul>
<li>备注，这里有点没看懂。「括号（）部分的内容是我加的，是我个人的理解」。</li>
</ul>
</li>
<li>压缩后通过重新读取文件恢复信息</li>
<li>MCP 工具通过 CLI 在沙盒执行，避免工具列表膨胀</li>
</ul>
<h3 id="52-just-in-time-检索"><a href="#52-just-in-time-检索" class="heading-anchor">5.2 Just-in-Time 检索</a></h3>
<table>
<thead>
<tr>
<th>方案</th>
<th>预加载</th>
<th>按需检索</th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude Code</td>
<td>CLAUDE.md</td>
<td>glob/grep 搜索文件系统</td>
</tr>
<tr>
<td>Codex</td>
<td>AGENTS.md</td>
<td>shell 工具探索</td>
</tr>
<tr>
<td>Anthropic 建议</td>
<td>最少必要信息</td>
<td>JIT 检索</td>
</tr>
</tbody>
</table>
<p>共同点：<strong>用 glob/grep 搜索文件系统，无需向量索引</strong>。这和 <a href="/post/from-native-rag-to-agentic-rag">Agentic RAG</a> 的思路一脉相承——Agent 自主决定搜索什么，而不是被动接受检索结果。</p>
<h2 id="6-子代理架构与模型选择"><a href="#6-子代理架构与模型选择" class="heading-anchor">6. 子代理架构与模型选择</a></h2>
<h3 id="61-不要在会话中切换模型"><a href="#61-不要在会话中切换模型" class="heading-anchor">6.1 不要在会话中切换模型</a></h3>
<p>Cache 是 model-specific 的。各家的做法：</p>
<ul>
<li><strong>Claude Code</strong>：Sub-Agent handoff（Opus → Haiku for Explore）</li>
<li><strong>Codex</strong>：同一对话保持同一模型</li>
<li><strong>Manus</strong>：任务级路由（Claude 做代码，Gemini 做多模态，OpenAI 做数学）——<strong>不同任务不同模型，但单次对话内不变</strong></li>
</ul>
<h3 id="62-子代理的-cache-友好设计"><a href="#62-子代理的-cache-友好设计" class="heading-anchor">6.2 子代理的 Cache 友好设计</a></h3>
<p><strong>Claude Code 架构</strong>（逆向分析数据）：</p>
<table>
<thead>
<tr>
<th>子代理</th>
<th>工具数</th>
<th>Prefix Reuse</th>
</tr>
</thead>
<tbody>
<tr>
<td>Main Agent</td>
<td>18</td>
<td>—</td>
</tr>
<tr>
<td>Explore × 3 并行</td>
<td>10/18 子集</td>
<td><strong>92%</strong></td>
</tr>
<tr>
<td>Plan</td>
<td>独立 context</td>
<td><strong>93%</strong></td>
</tr>
<tr>
<td>Execution</td>
<td>全部</td>
<td><strong>97%</strong></td>
</tr>
</tbody>
</table>
<p>Claude Code 还使用 <strong>warm-up 调用</strong>：启动时预热 tool list 和 system prompt 的 cache。</p>
<p><strong>Manus 多代理架构</strong>：</p>
<ul>
<li><strong>Planner → Knowledge Manager → Executor</strong> 三层</li>
<li>子代理有 <code>submit_results</code> 工具 + 约束解码确保输出格式</li>
</ul>
<p><strong>Anthropic 建议</strong>：子代理返回<strong>压缩 summary</strong> → 避免主 context 被&quot;污染&quot;。</p>
<h3 id="63-fork-操作必须共享父-prefix"><a href="#63-fork-操作必须共享父-prefix" class="heading-anchor">6.3 Fork 操作必须共享父 prefix</a></h3>
<blockquote>
<p><strong>Fork 出的子任务必须用和父对话相同的 prompt prefix</strong>，才能复用父对话 cache。</p>
</blockquote>
<p>Claude Code 在 compaction、summarization、skill execution 中都遵循这个原则。核心思想：<strong>压缩/fork 是在现有 cache 基础上的延伸，而非另起炉灶。</strong></p>
<h2 id="7-the-bitter-lesson"><a href="#7-the-bitter-lesson" class="heading-anchor">7. The Bitter Lesson</a></h2>
<p>最后分享 Manus 的反思，引用 Rich Sutton 的 &quot;The Bitter Lesson&quot;：</p>
<blockquote>
<p>Agent 的 harness（框架/约束）可能限制模型性能。随着模型进步，需要不断简化架构。</p>
</blockquote>
<p>Manus 自 2025 年 3 月以来已<strong>重构无数次</strong>。每次模型能力提升，某些 workaround 就变得不必要。</p>
<p>但有些设计是&quot;持久&quot;的——围绕 cache 的架构决策就是。它们不是在弥补模型不足，而是在适配计算的物理现实。</p>
<blockquote>
<p><strong>Cache 是物理约束，不是工程 hack。</strong> 只要 Prefill 还是 Compute Bound，Prompt Cache 就会继续是 Agent 架构的核心考量。</p>
</blockquote>
<h2 id="参考"><a href="#参考" class="heading-anchor">参考</a></h2>
<p>备注：本文主要受前 4 篇参考内容的启发</p>
<ul>
<li>Thariq @trq212 - <a href="https://x.com/trq212/status/2024574133011673516">Lessons from Building Claude Code: Prompt Caching Is Everything</a></li>
<li>Lance Martin - <a href="https://rlancemartin.github.io/2025/10/15/manus/">Manus webinar notes (2025.10): Context Reduction/Isolation/Offloading</a></li>
<li>Manus Blog - <a href="https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus">Context Engineering for AI Agents</a></li>
<li><a href="https://openai.com/zh-Hans-CN/index/unrolling-the-codex-agent-loop/">深入解析 Codex 智能体循环</a></li>
<li><a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Effective Context Engineering for AI Agents</a></li>
<li><a href="/post/prompt-cache-design-for-llm-agents">Agent Prompt Cache 设计（上）：Cache 破坏、Prompt 布局与工具管理</a> - 本博客</li>
<li><a href="/post/understanding-kv-cache-and-prompt-cache-basics">理解 KV Cache 与 Prompt Cache</a> - 本博客</li>
<li><a href="/post/from-native-rag-to-agentic-rag">RAG 进化之路：传统 RAG 到 Agentic RAG</a> - 本博客</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Agent 系统中的 Prompt Caching 设计（上）：Cache 破坏、Prompt 布局与工具管理</title>
      <link>https://yuanchaofa.com/post/prompt-cache-design-for-llm-agents</link>
      <guid>https://yuanchaofa.com/post/prompt-cache-design-for-llm-agents</guid>
      <source url="https://yuanchaofa.com/rss.xml">Agent 系统中的 Prompt Caching 设计（上）：Cache 破坏、Prompt 布局与工具管理</source>
      <description>深入分析 AI Agent 为什么比 Chatbot 更需要 Prompt Caching，什么操作会破坏 Cache，以及 Claude Code、Manus、OpenAI Codex 在 Prompt 布局和工具管理上的 Cache-aware 设计实践。</description>
      <category>agent system design</category>
      <pubDate>Sun, 22 Feb 2026 10:16:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="0-阅读收获-takeaway"><a href="#0-阅读收获-takeaway" class="heading-anchor">0. 阅读收获 (takeaway)</a></h2>
<p>读完本文，你将了解：</p>
<ul>
<li>从 Prompt Engineering 到 Context Engineering 的范式转变</li>
<li>为什么 Agent 比普通 Chatbot <strong>更</strong>需要 Prompt Caching</li>
<li>什么操作会破坏 Cache（比你想象的多）</li>
<li>Prompt 布局与动态信息管理的最佳实践</li>
<li>工具管理的三种 Cache-aware 方案对比</li>
</ul>
<blockquote>
<p><strong>前置知识</strong>：本文假设你已经理解 KV Cache、Prefill/Decode 两阶段、以及 Prompt Cache 的前缀匹配机制。如果不熟悉这些概念，建议先阅读 <a href="/post/understanding-kv-cache-and-prompt-cache-basics">理解 KV Cache 与 Prompt Caching：LLM 推理加速的核心机制</a>。</p>
</blockquote>
<h2 id="1-先说结论cache-rules-everything"><a href="#1-先说结论cache-rules-everything" class="heading-anchor">1. 先说结论：Cache Rules Everything</a></h2>
<p>在深入细节之前，我想先分享我对这个话题的理解：</p>
<blockquote>
<p><strong>Prompt Cache 不只是一个省钱技巧，它是 Agent 系统架构设计的核心约束。</strong></p>
</blockquote>
<p>就像数据库的 schema 设计会影响整个应用架构一样，Prompt Cache 的前缀匹配约束深刻地影响了 Agent 的每一个设计决策：</p>
<ul>
<li>prompt 怎么组织？→ 稳定内容放前面，变化内容放后面</li>
<li>工具怎么管理？→ 工具列表固定，通过其他机制限制可用范围</li>
<li>状态怎么切换？→ 不切换工具，用工具模拟状态转换（Claude Code Plan Mode 就是最好的例子）</li>
<li>context 怎么压缩？→ 压缩操作本身必须 cache-safe</li>
<li>模型怎么选？→ 不在同一会话中切换，用子代理隔离</li>
</ul>
<h3 id="11-从-prompt-engineering-到-context-engineering"><a href="#11-从-prompt-engineering-到-context-engineering" class="heading-anchor">1.1 从 Prompt Engineering 到 Context Engineering</a></h3>
<p>我们已经越来越多地听到 &quot;Context Engineering&quot; 这个术语。区别在哪？</p>
<ul>
<li><strong>Prompt Engineering</strong> 关注 &quot;怎么写指令让模型表现更好&quot;——内容层面的优化。</li>
<li><strong>Context Engineering</strong> 关注 &quot;怎么组织整个上下文——指令、工具、历史、外部信息——让 Agent 系统整体高效运转&quot;——系统架构层面的设计。</li>
</ul>
<p><a href="https://rlancemartin.github.io/2025/10/15/manus/">Manus 在 25 年底最新总结</a>中提出了三个维度：<strong>Reduce</strong>（缩减）、<strong>Isolate</strong>（隔离）、<strong>Offload</strong>（卸载）。</p>
<p>后面的内容你会看到，各家的设计都在围绕这三个维度展开。</p>
<h3 id="12-三家方案的共同规律"><a href="#12-三家方案的共同规律" class="heading-anchor">1.2 三家方案的共同规律</a></h3>
<p>不同公司、不同架构，但核心规律惊人地一致：</p>
<ol>
<li><strong>前缀不变</strong>：system prompt、tools、早期历史永远不修改</li>
<li><strong>追加不修改</strong>：Append-only，永远不编辑历史消息</li>
<li><strong>工具定义稳定</strong>：tools 数组不变，通过其他机制控制可用范围</li>
<li><strong>动态信息后置</strong>：时间戳、环境状态等放在后面的 user message 中</li>
<li><strong>压缩必须 cache-safe</strong>：压缩操作复用父对话的 cache prefix</li>
</ol>
<p>带着这些规律，我们来看具体的实践。</p>
<h2 id="2-为什么-agent-比-chatbot-更需要-prompt-cache"><a href="#2-为什么-agent-比-chatbot-更需要-prompt-cache" class="heading-anchor">2. 为什么 Agent 比 Chatbot 更需要 Prompt Cache？</a></h2>
<h3 id="21-agent-的-io-比例严重失衡"><a href="#21-agent-的-io-比例严重失衡" class="heading-anchor">2.1 Agent 的 I/O 比例严重失衡</a></h3>
<p>Agent 每一步都需要发送完整的对话历史给模型，模型只输出一小段。Manus 披露过一个数据：<strong>input:output ≈ 100:1</strong>。</p>
<p>如果没有 Prompt Cache → 每一步重新 Prefill 所有历史 token → <strong>成本二次方增长</strong>。</p>
<h3 id="22-成本考虑"><a href="#22-成本考虑" class="heading-anchor">2.2 成本考虑</a></h3>
<table>
<thead>
<tr>
<th>场景</th>
<th>不缓存</th>
<th>缓存后</th>
<th>节约</th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude（正常 vs cached）</td>
<td>$3/MTok</td>
<td>$0.30/MTok</td>
<td><strong>90%</strong></td>
</tr>
<tr>
<td>OpenAI GPT-5（正常 vs cached）</td>
<td>$10/MTok</td>
<td>$2.50/MTok</td>
<td><strong>75%</strong></td>
</tr>
<tr>
<td>Claude Code 单任务（约 2M tokens）</td>
<td>~<span class="katex-inline">$6.00 | ~$</span>1.15</td>
<td><strong>81%</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p>Thariq（Claude Code 团队）：</p>
<blockquote>
<p>&quot;Coding agents would be cost prohibitive without prompt caching.&quot;</p>
</blockquote>
<p>OpenAI Codex：cache 命中后，<strong>采样开销从二次降为线性</strong>。</p>
<h3 id="23-延迟ttft-的大幅改善"><a href="#23-延迟ttft-的大幅改善" class="heading-anchor">2.3 延迟：TTFT 的大幅改善</a></h3>
<ul>
<li><strong>Manus</strong>：KV cache hit rate 是 &quot;<strong>the single most important metric</strong>&quot;</li>
<li><strong>Claude Code 团队</strong>：cache 命中率下降 → 当作<strong>线上事故</strong>（SEV）处理</li>
<li><strong>OpenAI</strong>：150K+ tokens 时，cached 请求 TTFT 快 <strong>67%</strong></li>
</ul>
<h2 id="3-什么操作会破坏-cache"><a href="#3-什么操作会破坏-cache" class="heading-anchor">3. 什么操作会破坏 Cache？</a></h2>
<p>前缀匹配是一切的基础：<strong>任何位置的任何改动 → 该位置之后的 cache 全部失效。</strong></p>
<h3 id="31-改动-system-prompt"><a href="#31-改动-system-prompt" class="heading-anchor">3.1 改动 System Prompt</a></h3>
<p><strong>在开头放时间戳</strong>——Manus 踩过的坑。时间戳每秒都变，第一个 token 就不同，整个 cache 废掉。</p>
<h3 id="32-改动-tool-definitions"><a href="#32-改动-tool-definitions" class="heading-anchor">3.2 改动 Tool Definitions</a></h3>
<p>Claude Code 团队经验中<strong>最常见的 cache 破坏方式</strong>。Tool definitions 在 prompt 前部，增删任何一个工具 → 后续所有 cache 失效。</p>
<p>具体场景：</p>
<ul>
<li><strong>增删工具</strong>：动态加载不同工具集 → 每次变化 cache 全废</li>
<li><strong>工具顺序不确定</strong>：Codex 的 MCP 工具注册顺序不确定</li>
<li><strong>更新工具参数</strong>：如 <code>allowed_subagents</code> 列表变化</li>
</ul>
<h3 id="33-切换模型"><a href="#33-切换模型" class="heading-anchor">3.3 切换模型</a></h3>
<p>Cache 是 <strong>model-specific</strong> 的。</p>
<p>一个反直觉推论：<strong>100K token 对话中，切换到更便宜的模型可能更贵</strong> —— Opus 的 100K cached token 只需 $1.50，换 Haiku 后全部重算。</p>
<h3 id="34-修改历史消息"><a href="#34-修改历史消息" class="heading-anchor">3.4 修改历史消息</a></h3>
<ul>
<li>编辑或删除之前的 action/observation → 破坏 cache</li>
<li><strong>非确定性序列化</strong>：JSON key 排序不一致（Manus 踩坑）→ 相同语义不同 token 序列</li>
</ul>
<blockquote>
<p>核心原则：<strong>序列化必须是确定性的。</strong></p>
</blockquote>
<h2 id="4-prompt-布局与动态信息管理"><a href="#4-prompt-布局与动态信息管理" class="heading-anchor">4. Prompt 布局与动态信息管理</a></h2>
<p>核心思路：<strong>把稳定的内容放前面，把变化的内容放后面。</strong></p>
<h3 id="41-claude-code-的四层缓存架构"><a href="#41-claude-code-的四层缓存架构" class="heading-anchor">4.1 Claude Code 的四层缓存架构</a></h3>
<p><img loading="lazy" src="/blog_imgs/agent-prompt-layout.png" alt="Claude Code 四层布局与 Codex Prompt 构建过程对比" /></p>
<table>
<thead>
<tr>
<th>层级</th>
<th>内容</th>
<th>稳定性</th>
</tr>
</thead>
<tbody>
<tr>
<td>Layer 1</td>
<td>Static System Prompt &amp; Tools</td>
<td>全局不变</td>
</tr>
<tr>
<td>Layer 2</td>
<td>CLAUDE.md 项目配置</td>
<td>项目级不变</td>
</tr>
<tr>
<td>Layer 3</td>
<td>Session Context（git status 等）</td>
<td>会话级</td>
</tr>
<tr>
<td>Layer 4</td>
<td>Conversation Messages</td>
<td>每轮追加</td>
</tr>
</tbody>
</table>
<p>每轮只有 Layer 4 增长，前 3 层稳定命中 cache。</p>
<h3 id="42-openai-codex-的-prompt-构建"><a href="#42-openai-codex-的-prompt-构建" class="heading-anchor">4.2 OpenAI Codex 的 Prompt 构建</a></h3>
<p>三层结构：<strong>instructions → tools → input</strong>（input 可能会包含 dev role message，上图）。关键设计：<strong>旧提示是新提示的精确前缀</strong>。</p>
<p>配置变更（沙盒权限、工作目录）→ <strong>追加</strong>新消息而非<strong>修改</strong>旧消息。</p>
<h3 id="43-manus-的三条规则"><a href="#43-manus-的三条规则" class="heading-anchor">4.3 Manus 的三条规则</a></h3>
<ol>
<li><strong>稳定前缀</strong></li>
<li><strong>Append-only</strong></li>
<li><strong>确定性序列化</strong></li>
</ol>
<h3 id="44-动态信息怎么更新"><a href="#44-动态信息怎么更新" class="heading-anchor">4.4 动态信息怎么更新？</a></h3>
<table>
<thead>
<tr>
<th>方案</th>
<th>实现方式</th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude Code</td>
<td><code>&lt;system-reminder&gt;</code> 标签放在 user message 中</td>
</tr>
<tr>
<td>Codex</td>
<td>追加新的 developer/user 消息</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>永远追加，永远不修改。</strong></p>
</blockquote>
<h3 id="45-cache-breakpoint-与-auto-caching"><a href="#45-cache-breakpoint-与-auto-caching" class="heading-anchor">4.5 Cache Breakpoint 与 Auto-caching</a></h3>
<ul>
<li><strong>Claude API</strong>：从手动 <code>cache_control</code> breakpoint → <strong>auto-caching</strong> 一个参数搞定</li>
<li><strong>OpenAI API</strong>：<code>prompt_cache_key</code> 路由优化，自动缓存 ≥1024 token 前缀</li>
</ul>
<p>反直觉：900 token prompt 永远不 cache hit，扩展到 1024+ token 反而更省钱。</p>
<h2 id="5-工具管理三种方案殊途同归"><a href="#5-工具管理三种方案殊途同归" class="heading-anchor">5. 工具管理：三种方案，殊途同归</a></h2>
<h3 id="51-问题本质"><a href="#51-问题本质" class="heading-anchor">5.1 问题本质</a></h3>
<p>Agent 可能有 30 个工具，但不同阶段只需要一部分。如果按需加载 → 每次状态切换 cache 全废。</p>
<p><img loading="lazy" src="/blog_imgs/tool-management-comparison.png" alt="三家工具管理策略对比" /></p>
<h3 id="52-claude-code状态转换-deferloading"><a href="#52-claude-code状态转换-deferloading" class="heading-anchor">5.2 Claude Code：状态转换 + defer_loading</a></h3>
<ul>
<li><strong>Plan Mode</strong>：<code>EnterPlanMode</code>/<code>ExitPlanMode</code> 本身作为工具 → 工具列表永远不变</li>
<li><strong>Tool Search</strong>：<code>defer_loading</code> stub → <code>ToolSearch</code> 按需获取完整 schema</li>
<li>模型可<strong>自主决定</strong>何时进入 Plan Mode</li>
</ul>
<h3 id="53-manuslogits-masking"><a href="#53-manuslogits-masking" class="heading-anchor">5.3 Manus：Logits Masking</a></h3>
<ul>
<li>所有工具始终在 prompt 中</li>
<li>工具命名约定：<code>browser_xxx</code>、<code>shell_xxx</code></li>
<li>Token logits masking 控制可用工具</li>
<li>三种模式：Auto / Required / Specified</li>
</ul>
<h3 id="54-openaiallowedtools-参数"><a href="#54-openaiallowedtools-参数" class="heading-anchor">5.4 OpenAI：allowed_tools 参数</a></h3>
<ul>
<li><code>tools</code> 数组完整不变</li>
<li><code>allowed_tools</code> 限制当前可用子集</li>
<li>注意：MCP 服务器可动态变更工具列表 → 需谨慎处理</li>
</ul>
<h3 id="55-对比总结"><a href="#55-对比总结" class="heading-anchor">5.5 对比总结</a></h3>
<blockquote>
<p><strong>本质：工具定义不变（保 cache），通过其他机制限制可选范围。</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>方案</th>
<th>实现方式</th>
<th>优点</th>
<th>限制</th>
</tr>
</thead>
<tbody>
<tr>
<td>Claude Code</td>
<td>tool 本身 + defer_loading</td>
<td>灵活，模型自主决策</td>
<td>需 API 支持</td>
</tr>
<tr>
<td>Manus</td>
<td>logits masking</td>
<td>精细控制</td>
<td>需 self-hosting</td>
</tr>
<tr>
<td>OpenAI</td>
<td>allowed_tools 参数</td>
<td>最简单</td>
<td>仅粗粒度</td>
</tr>
</tbody>
</table>
<h2 id="下一篇"><a href="#下一篇" class="heading-anchor">下一篇</a></h2>
<p>本文聚焦于 Cache-aware 的 Prompt 设计和工具管理。但 Agent 还面临另一组挑战：context 越来越长怎么办？怎么压缩才不破坏 cache？子代理怎么设计？</p>
<p>下一篇 <a href="/post/agent-context-management-and-sub-agents">Agent 系统中的 Prompt Cache 设计（下）：上下文管理与子代理架构</a> 将深入这些话题。</p>
<h2 id="参考"><a href="#参考" class="heading-anchor">参考</a></h2>
<ul>
<li>Thariq @trq212 - <a href="https://x.com/trq212/status/2024574133011673516">Lessons from Building Claude Code: Prompt Caching Is Everything</a></li>
<li>Lance Martin @RLanceMartin - <a href="https://x.com/RLanceMartin/status/2024573404888911886">Prompt auto-caching with Claude</a></li>
<li>Manus Blog - <a href="https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus">Context Engineering for AI Agents</a></li>
<li><a href="https://openai.com/zh-Hans-CN/index/unrolling-the-codex-agent-loop/">深入解析 Codex 智能体循环</a></li>
<li><a href="https://cookbook.openai.com/examples/prompt_caching_201">Prompt Caching 201</a></li>
<li><a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents">Effective Context Engineering for AI Agents</a></li>
<li><a href="/post/understanding-kv-cache-and-prompt-cache-basics">理解 KV Cache 与 Prompt Cache</a> - 我的博客</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>理解 KV Cache 与 Prompt Caching：LLM 推理加速的核心机制</title>
      <link>https://yuanchaofa.com/post/understanding-kv-cache-and-prompt-cache-basics</link>
      <guid>https://yuanchaofa.com/post/understanding-kv-cache-and-prompt-cache-basics</guid>
      <source url="https://yuanchaofa.com/rss.xml">理解 KV Cache 与 Prompt Caching：LLM 推理加速的核心机制</source>
      <description>深入理解 KV Cache 的原理、Prefill/Decode 两阶段与 Compute Bound/Memory Bound 的关系，以及 Prompt Caching（前缀缓存）如何实现推理加速和成本节约。</description>
      <category>agent system design</category>
      <pubDate>Sat, 21 Feb 2026 10:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="0-阅读收获-takeaway"><a href="#0-阅读收获-takeaway" class="heading-anchor">0. 阅读收获 (takeaway)</a></h2>
<p>读完本文，你将了解：</p>
<ul>
<li>KV Cache 的原理以及它为什么对 LLM 推理如此重要</li>
<li>Prefill 与 Decode 两个推理阶段的区别</li>
<li>Compute Bound 与 Memory Bound 背后的直觉</li>
<li>一个很好的问题：Prefill 阶段为什么需要计算<strong>所有</strong> token 的 Q？</li>
<li>Prompt Caching（前缀缓存）的工作原理</li>
</ul>
<blockquote>
<p>KV Cache 和 Prompt Cache 对于 Agent 设计的影响：</p>
<ul>
<li><a href="/post/prompt-cache-design-for-llm-agents">Agent 系统中的 Prompt Caching 设计（上）：Cache 破坏、Prompt 布局与工具管理</a> —— 为什么 Agent 更需要 Cache、什么会破坏 Cache、三家工具管理方案对比</li>
<li><a href="/post/agent-context-management-and-sub-agents">Agent 系统中的 Prompt Caching 设计（下）：上下文管理与子代理架构</a> —— 上下文压缩、Plan 模式演进、子代理 Cache 友好设计</li>
</ul>
</blockquote>
<h2 id="1-什么是-kv-cache"><a href="#1-什么是-kv-cache" class="heading-anchor">1. 什么是 KV Cache？</a></h2>
<h3 id="11-autoregressive-生成的重复计算问题"><a href="#11-autoregressive-生成的重复计算问题" class="heading-anchor">1.1 Autoregressive 生成的重复计算问题</a></h3>
<p>大语言模型（LLM）的文本生成是 <strong>自回归（autoregressive）</strong> 的：每次只生成一个 token，然后把这个 token 拼到已有序列后面，再预测下一个。</p>
<p>用伪代码表示就是：</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="c1"># 自回归生成的朴素实现</span>
<span class="n">output_tokens</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">max_new_tokens</span><span class="p">):</span>
    <span class="c1"># 每一步都要把 整个序列 送进模型</span>
    <span class="n">logits</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">input_tokens</span> <span class="o">+</span> <span class="n">output_tokens</span><span class="p">)</span>
    <span class="n">next_token</span> <span class="o">=</span> <span class="n">sample</span><span class="p">(</span><span class="n">logits</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>  <span class="c1"># 只用最后一个位置的 logits</span>
    <span class="n">output_tokens</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">next_token</span><span class="p">)</span>
</code></pre></div>
</code></pre>
<p>Q: 问题出在哪？</p>
<p><strong>每一步生成，模型都要对所有历史 token 重新做 Attention 计算</strong>——包括 Q、K、V 矩阵乘法。但对于已经出现过的 token，它们的 K 和 V 其实不会变（因为参数没变、token 没变），唯一在变的只有 &quot;最新生成的那个 token&quot; 对应的 Q、K、V。</p>
<p>这就引出了一个自然的优化思路：<strong>能不能把已经算过的 K 和 V 缓存起来，下次直接用？</strong></p>
<h3 id="12-kv-cache-的核心思想"><a href="#12-kv-cache-的核心思想" class="heading-anchor">1.2 KV Cache 的核心思想</a></h3>
<p>KV Cache 的核心思想非常直接：</p>
<blockquote>
<p>把每一层 Attention 中、每个已生成 token 对应的 <strong>K 向量</strong>和 <strong>V 向量</strong>缓存下来。后续生成新 token 时，只需要计算新 token 自己的 Q、K、V，然后将新的 K、V 追加到缓存中，用缓存里的完整 K、V 序列做 Attention。</p>
</blockquote>
<p>这样一来，生成第 <span class="katex-inline">$t$</span> 个 token 时，Attention 的计算从 <span class="katex-inline">$O(t \times d)$</span>（重算所有 token 的 K、V）降低到 <span class="katex-inline">$O(d)$</span>（只算 1 个新 token 的 K、V），<strong>避免了绝大部分重复计算</strong>。</p>
<p>用带 KV Cache 的伪代码表示：</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="c1"># 带 KV Cache 的生成</span>
<span class="n">kv_cache</span> <span class="o">=</span> <span class="p">{}</span>  <span class="c1"># 每一层缓存 K, V</span>
<span class="k">for</span> <span class="n">step</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">max_new_tokens</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">step</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="c1"># 第一步：处理所有 input tokens，填充 cache</span>
        <span class="n">logits</span><span class="p">,</span> <span class="n">kv_cache</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">input_tokens</span><span class="p">,</span> <span class="n">kv_cache</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="c1"># 后续步：只送入上一步生成的 1 个 token</span>
        <span class="n">logits</span><span class="p">,</span> <span class="n">kv_cache</span> <span class="o">=</span> <span class="n">model</span><span class="p">([</span><span class="n">last_token</span><span class="p">],</span> <span class="n">kv_cache</span><span class="o">=</span><span class="n">kv_cache</span><span class="p">)</span>
    <span class="n">next_token</span> <span class="o">=</span> <span class="n">sample</span><span class="p">(</span><span class="n">logits</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
    <span class="n">last_token</span> <span class="o">=</span> <span class="n">next_token</span>
</code></pre></div>
</code></pre>
<h3 id="13-kv-cache-显存占用"><a href="#13-kv-cache-显存占用" class="heading-anchor">1.3 KV Cache 显存占用</a></h3>
<p>KV Cache 不是免费的——它用<strong>显存</strong>换<strong>计算</strong>。随着生成序列变长，KV Cache 占用的显存会线性增长。</p>
<p>具体公式（假设 float16 存储）：</p>
<div class="katex-block">$$\text{KV Cache 显存} = 4blh(s + n) \text{ bytes}$$</div>
<p>其中：</p>
<ul>
<li><span class="katex-inline">$b$</span> = batch size</li>
<li><span class="katex-inline">$l$</span> = Transformer 层数</li>
<li><span class="katex-inline">$h$</span> = hidden size</li>
<li><span class="katex-inline">$s$</span> = 输入序列长度</li>
<li><span class="katex-inline">$n$</span> = 输出序列长度</li>
<li>4 = 2（K 和 V）× 2（float16 占 2 bytes）</li>
</ul>
<blockquote>
<p>这个公式的详细推导和具体数值例子，可以参考我之前的文章 <a href="/post/llm-train-infer-memoery-usage-calculation">LLM 大模型训练-推理显存占用分析</a>。这里只需要记住一个直觉：<strong>序列越长，KV Cache 越大</strong>。这也是为什么后续会有 GQA（Grouped Query Attention）、<a href="/post/hands-on-deepseek-mla">DeepSeek MLA</a> 等 KV Cache 压缩技术出现。</p>
</blockquote>
<h2 id="2-prefill-vs-decode推理的两个阶段"><a href="#2-prefill-vs-decode推理的两个阶段" class="heading-anchor">2. Prefill vs Decode：推理的两个阶段</a></h2>
<p>理解了 KV Cache 之后，我们可以把 LLM 推理过程清晰地分成两个阶段：<strong>Prefill</strong> 和 <strong>Decode</strong>。这两个阶段的计算特性截然不同，理解它们的区别对后面理解 Prompt Cache 非常关键。</p>
<p><img loading="lazy" src="/blog_imgs/kv-cache-prefill-decode.png" alt="Prefill 与 Decode 两阶段对比" /></p>
<h3 id="21-prefill-阶段并行处理-input-tokens-compute-bound"><a href="#21-prefill-阶段并行处理-input-tokens-compute-bound" class="heading-anchor">2.1 Prefill 阶段（并行处理 input tokens → Compute Bound）</a></h3>
<p>Prefill 阶段就是上面伪代码中 <code>step == 0</code> 的那一步：模型一次性处理所有输入 token（system prompt + user message），为每一层、每个 token 计算出 K 和 V 并存入 cache。</p>
<p>关键特点：</p>
<ul>
<li><strong>所有输入 token 可以并行处理</strong>（它们之间的 Attention mask 是 causal 的，但计算可以用矩阵乘法一次完成）</li>
<li>计算量大：<span class="katex-inline">$n$</span> 个 token × 所有层 × Q/K/V 矩阵运算</li>
<li><strong>Compute Bound</strong>：GPU 的算力是瓶颈</li>
</ul>
<h3 id="22-decode-阶段逐-token-生成-memory-bound"><a href="#22-decode-阶段逐-token-生成-memory-bound" class="heading-anchor">2.2 Decode 阶段（逐 token 生成 → Memory Bound）</a></h3>
<p>Decode 阶段就是后续的 <code>step &gt; 0</code>：每一步只输入 1 个 token，利用 KV Cache 做 Attention，生成下一个 token。</p>
<p>关键特点：</p>
<ul>
<li><strong>每步只处理 1 个 token</strong>（无法并行，因为下一个 token 依赖上一个的输出）</li>
<li>每步的计算量其实不大——1 个 token 的 Q 乘以 cache 中所有 K/V</li>
<li>但每步都要从显存<strong>读取整个 KV Cache</strong></li>
<li><strong>Memory Bound</strong>：GPU 的显存带宽是瓶颈</li>
</ul>
<h3 id="23-compute-bound-vs-memory-bound"><a href="#23-compute-bound-vs-memory-bound" class="heading-anchor">2.3 Compute Bound vs Memory Bound</a></h3>
<p>这里解释一下 Compute Bound 和 Memory Bound 的含义，核心概念是 <strong>Arithmetic Intensity（算术强度）</strong>：</p>
<div class="katex-block">$$\text{Arithmetic Intensity} = \frac{\text{计算量 (FLOPs)}}{\text{数据搬运量 (Bytes)}}$$</div>
<ul>
<li><strong>Compute Bound</strong>：算术强度高，GPU 的计算单元忙不过来，数据搬运不是瓶颈。Prefill 就是这种情况——大矩阵乘法，计算密集。</li>
<li><strong>Memory Bound</strong>：算术强度低，GPU 的计算单元在等数据从显存搬过来。Decode 就是这种情况——每步只有 1 个 token 的小矩阵运算，但要读取整个 KV Cache。</li>
</ul>
<p>用一个直觉来理解：</p>
<ul>
<li>Prefill 像是&quot;一次批量处理 1000 个快递&quot;——流水线拉满，打包效率高</li>
<li>Decode 像是&quot;每次只来 1 个快递&quot;——打包机器空闲大半时间，瓶颈在于快递从仓库取出来的速度</li>
</ul>
<h3 id="24-一个很好的问题prefill-为什么要计算所有-token-的-q"><a href="#24-一个很好的问题prefill-为什么要计算所有-token-的-q" class="heading-anchor">2.4 一个很好的问题：Prefill 为什么要计算所有 token 的 Q？</a></h3>
<p>这个问题来自一位读者在 <a href="https://github.com/bbruceyuan/bbruceyuan.github.io/discussions/22#discussioncomment-12592501">GitHub Discussion</a> 的提问，我觉得是一个非常好的问题：</p>
<blockquote>
<p><strong>既然我们只需要预测 next token，Prefill 阶段不是只需要最后一个 token 的 Q 吗？为什么要计算所有 token 的 Q？</strong></p>
</blockquote>
<p>乍一看很有道理——Attention 的输出 <span class="katex-inline">$\text{softmax}(QK^T / \sqrt{d})V$</span> 中，我们只需要最后一个位置的结果来预测 next token。那 K、V 确实需要全算（因为最后一个 Q 要和所有 K 做 attention），但 Q 为什么不能只算最后一个？</p>
<p>答案的核心是：<strong>Decoder 有很多层</strong>。</p>
<p>如果 Transformer 只有一层，那确实，我们只需要最后一个 token 的 Q。但实际的 Decoder 有几十层，<strong>上一层所有位置的输出是下一层所有位置的输入</strong>：</p>
<ol>
<li><strong>第 1 层</strong>：为了得到所有位置的 K、V（这些 K、V 要存入 cache），需要知道所有位置的输入。而第 1 层的输入就是 token embedding，所以 Q、K、V 都要算全部位置。</li>
<li><strong>第 2 层</strong>：第 2 层的输入是第 1 层的<strong>输出</strong>。第 1 层的输出取决于 Attention 的完整计算——包括所有位置的 Q。因此第 2 层的 K、V 计算依赖于第 1 层所有位置的 Q 计算结果。</li>
<li><strong>第 N 层</strong>：同理，依赖前面所有层的完整输出。</li>
</ol>
<p>看图：</p>
<p><img loading="lazy" src="/blog_imgs/multi-layer-prefill-q.png" alt="Prefill 阶段的计算过程" /></p>
<p>所以结论是：</p>
<blockquote>
<p><strong>Prefill 必须计算所有 token 的 Q，不是因为最终预测需要，而是因为每一层的 K、V 缓存依赖于上一层所有位置的完整输出，而上一层的完整输出需要所有位置的 Q 参与计算。</strong></p>
</blockquote>
<p>这也解释了为什么 Prefill 阶段是 Compute Bound——它确实需要做大量计算，不是在浪费。</p>
<h3 id="25-ttft-vs-tpot"><a href="#25-ttft-vs-tpot" class="heading-anchor">2.5 TTFT vs TPOT</a></h3>
<p>从用户体验的角度，两个阶段对应两个不同的延迟指标：</p>
<ul>
<li><strong>TTFT（Time To First Token）</strong>：用户发送请求到看到第一个输出 token 的时间。主要由 <strong>Prefill 阶段</strong>决定。</li>
<li><strong>TPOT（Time Per Output Token）</strong>：生成每个后续 token 的平均时间。主要由 <strong>Decode 阶段</strong>决定。</li>
</ul>
<p>对于输入很长的场景（比如长文档问答、Agent 的多轮对话），Prefill 阶段的耗时会显著增加 TTFT。</p>
<blockquote>
<p>这就引出了一个关键问题：如果我们能跳过 Prefill 中那些&quot;之前已经算过&quot;的部分，是不是就能大幅降低 TTFT？这就是 <strong>Prompt Cache</strong> 要解决的问题。</p>
</blockquote>
<h2 id="3-prompt-cache前缀缓存"><a href="#3-prompt-cache前缀缓存" class="heading-anchor">3. Prompt Cache（前缀缓存）</a></h2>
<h3 id="31-从-kv-cache-到-prompt-cache"><a href="#31-从-kv-cache-到-prompt-cache" class="heading-anchor">3.1 从 KV Cache 到 Prompt Cache</a></h3>
<p>前面说的 KV Cache 是<strong>单次请求内部</strong>的优化——生成过程中缓存已算过的 K、V，避免重复计算。</p>
<p><strong>Prompt Caching（前缀缓存）</strong> 则是<strong>跨请求</strong>的优化：</p>
<blockquote>
<p>如果两次 API 调用的 prompt 有相同的前缀，那么第二次调用可以直接复用第一次 Prefill 阶段算出来的 KV Cache，跳过前缀部分的 Prefill 计算。</p>
</blockquote>
<p>这对于以下场景特别有价值：</p>
<ul>
<li><strong>多轮对话</strong>：每轮对话的 prompt 都以之前的对话历史作为前缀</li>
<li><strong>相同 system prompt</strong>：同一个应用的所有请求共享相同的 system prompt 前缀</li>
<li><strong>Agent 系统</strong>：Agent 每一步的 prompt 都是上一步的 prompt 加上新的 action/observation</li>
</ul>
<h3 id="32-前缀匹配机制"><a href="#32-前缀匹配机制" class="heading-anchor">3.2 前缀匹配机制</a></h3>
<p>Prompt Cache 的匹配规则非常严格：</p>
<blockquote>
<p><strong>必须从第一个 token 开始完全一致，一个 token 的差异就会导致该位置之后的 cache 全部失效。</strong></p>
</blockquote>
<p><img loading="lazy" src="/blog_imgs/prompt-cache-prefix-matching.png" alt="前缀匹配：Cache Hit 与 Cache Miss" /></p>
<p>举个例子：</p>
<table>
<thead>
<tr>
<th>请求</th>
<th>内容</th>
<th>Cache 命中情况</th>
</tr>
</thead>
<tbody>
<tr>
<td>请求 1</td>
<td><code>[System Prompt][User: Hello]</code></td>
<td>无 cache，全部计算</td>
</tr>
<tr>
<td>请求 2</td>
<td><code>[System Prompt][User: Hello][Assistant: Hi][User: 你好]</code></td>
<td><code>[System Prompt][User: Hello]</code> 部分 cache hit</td>
</tr>
<tr>
<td>请求 3</td>
<td><code>[Modified System Prompt][User: Hello]</code></td>
<td>完全 miss，因为第一个 token 就不一样了</td>
</tr>
</tbody>
</table>
<p><strong>这个&quot;前缀精确匹配&quot;的约束，是后面 Agent 系统设计的核心基础</strong>。在下一篇文章中，我们会详细讨论 Claude Code、Manus、OpenAI Codex 如何围绕这个约束设计整个系统架构。</p>
<h3 id="33-开源推理引擎的实现"><a href="#33-开源推理引擎的实现" class="heading-anchor">3.3 开源推理引擎的实现</a></h3>
<p>在开源推理引擎中，Prompt Cache（通常叫 Prefix Caching 或 Automatic Prefix Caching）已经是标配功能：</p>
<ul>
<li><strong>vLLM</strong>：通过 <code>--enable-prefix-caching</code> 开启，使用 hash-based 的 block 管理机制。将 KV Cache 按固定大小的 block 存储，相同前缀的 block 可以在不同请求间共享。</li>
<li><strong>SGLang</strong>：默认开启 RadixAttention，用 Radix Tree（基数树）管理 KV Cache 前缀。相比 vLLM 的 hash 方案，Radix Tree 在前缀共享上有天然优势——可以高效处理多层级的前缀共享。</li>
</ul>
<p>这些引擎的实现细节不是本文重点，关键是理解：<strong>Prompt Cache 在推理引擎层面已经是成熟技术，无论你用开源引擎自部署还是调用商业 API，都可以获得这个优化。</strong></p>
<h2 id="4-总结与预告"><a href="#4-总结与预告" class="heading-anchor">4. 总结与预告</a></h2>
<p>让我们回顾一下本文的核心脉络：</p>
<ol>
<li><strong>KV Cache</strong> 解决了自回归生成中的重复计算问题，是 LLM 推理的基础优化</li>
<li><strong>Prefill 阶段</strong>并行处理所有输入 token（Compute Bound），<strong>Decode 阶段</strong>逐个生成 token（Memory Bound）</li>
<li>Prefill 必须计算所有 token 的 Q，因为多层 Decoder 的层间依赖</li>
<li><strong>Prompt Cache</strong> 把 KV Cache 的优化从&quot;单次请求内&quot;扩展到&quot;跨请求&quot;，通过前缀匹配复用已计算的 KV Cache</li>
<li>前缀精确匹配的约束，决定了 Prompt Cache 的使用方式——<strong>任何位置的改动都会破坏该位置之后的 cache</strong></li>
</ol>
<p>这个&quot;前缀精确匹配&quot;的约束，在 AI Agent 系统中变得尤为关键。Agent 每一步都要发送越来越长的 context（历史对话 + 工具调用结果），如果不精心设计 prompt 结构，cache 命中率会很低，成本和延迟都会飙升。</p>
<p>在接下来的两篇文章中，我会详细分析 Claude Code、Manus、OpenAI Codex 等 AI Agent 如何围绕 Prompt Cache 设计整个系统架构：</p>
<ul>
<li><a href="/post/prompt-cache-design-for-llm-agents">Agent 系统中的 Prompt Cache 设计（上）：Cache 破坏、Prompt 布局与工具管理</a> —— 为什么 Agent 更需要 Cache、什么会破坏 Cache、三家工具管理方案对比</li>
<li><a href="/post/agent-context-management-and-sub-agents">Agent 系统中的 Prompt Cache 设计（下）：上下文管理与子代理架构</a> —— 上下文压缩、Plan 模式演进、子代理 Cache 友好设计</li>
</ul>
<h2 id="参考"><a href="#参考" class="heading-anchor">参考</a></h2>
<ul>
<li><a href="/post/llm-train-infer-memoery-usage-calculation">LLM 大模型训练-推理显存占用分析</a> - 我的博客</li>
<li><a href="/post/hands-on-deepseek-mla">动手理解 DeepSeek MLA（Part 1）</a> - 我的博客</li>
<li><a href="https://github.com/bbruceyuan/bbruceyuan.github.io/discussions/22#discussioncomment-12592501">GitHub Discussion #22: Prefill 阶段为什么需要计算所有 token 的 Q</a></li>
<li><a href="https://cookbook.openai.com/examples/prompt_caching_201">Prompt Caching 201</a></li>
<li><a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching">Prompt auto-caching with Claude</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>DPO 算法原理与代码实现：让 LLM 对齐变得简单</title>
      <link>https://yuanchaofa.com/post/hands-on-dpo-direct-preference-optimization</link>
      <guid>https://yuanchaofa.com/post/hands-on-dpo-direct-preference-optimization</guid>
      <source url="https://yuanchaofa.com/rss.xml">DPO 算法原理与代码实现：让 LLM 对齐变得简单</source>
      <description>DPO 让 LLM 对齐训练变得像 SFT 一样简单。本文从 RLHF 痛点讲起，手撕 DPO Loss 核心代码，用 trl 跑通完整训练流程。Bonus 包含稳定性分析和数学推导，一篇搞定 DPO。本文是「动手学大模型」系列第12章 Part2 的配套博客。</description>
      <category>hands-on-code</category>
      <pubDate>Sat, 10 Jan 2026 15:07:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="0-阅读收获-takeaway"><a href="#0-阅读收获-takeaway" class="heading-anchor">0. 阅读收获 (takeaway)</a></h2>
<p>本文目标是搞懂 DPO（Direct Preference Optimization）算法，阅读完本文你将获得：</p>
<ul>
<li>理解 DPO 的核心思想：为什么 DPO 可以替代 RLHF 中的 PPO</li>
<li>掌握 DPO 与 RLHF 的关键区别：从 4 个模型到 2 个模型</li>
<li>手撕 DPO Loss：理解损失函数到底在算什么</li>
<li>Bonus 1：为什么 DPO 比 PPO 训练更稳定</li>
<li>Bonus 2：DPO 损失函数的完整数学推导</li>
<li>源代码位于 <a href="https://github.com/bbruceyuan/Hands-On-Large-Language-Models-CN/">Github -动手学习大模型-中文版-第 12.1章——动手学习 DPO</a></li>
</ul>
<blockquote>
<p>本文代码运行于：<a href="https://featurize.cn/srx/gthYt2">Featurize 蒜粒方块 GPU 算力平台</a>，不喜欢看文字的同学可以看 <a href="https://space.bilibili.com/12420432">B站视频-chaofa用代码打点酱油</a>，<a href="https://www.youtube.com/@bbruceyuan">YouTube-chaofa用代码打点酱油</a>，视频号：chaofa用代码打点酱油</p>
</blockquote>
<h2 id="1-为什么需要-dpo"><a href="#1-为什么需要-dpo" class="heading-anchor">1. 为什么需要 DPO？</a></h2>
<p>在聊 DPO 之前，我们先快速回顾一下 LLM 训练的三个阶段（参考 OpenAI InstructGPT)：</p>
<ol>
<li><strong>预训练（Pre-training）</strong>：在海量文本上训练，让模型学会&quot;说话&quot;</li>
<li><strong>监督微调（SFT）</strong>：用高质量的指令数据微调，让模型学会&quot;听话&quot;</li>
<li><strong>对齐（Alignment）</strong>：让模型的输出符合人类偏好，学会&quot;说人话&quot;</li>
</ol>
<p>假设读者对于前两个步骤已经有所了解，这篇文章的重点是第三步&quot;对齐&quot;。</p>
<h3 id="11-rlhf-的问题"><a href="#11-rlhf-的问题" class="heading-anchor">1.1 RLHF 的问题</a></h3>
<p>OpenAI 在训练 ChatGPT 的时候用的是 RLHF（Reinforcement Learning from Human Feedback），整个流程大概是这样的：</p>
<p><img loading="lazy" src="/blog_imgs/ppo-vs-dpo.png" alt="DPO 原论文中的 RLHF vs DPO 流程对比" /></p>
<p>RLHF 确实有效，但问题也很明显：</p>
<ol>
<li><strong>需要 4 个模型</strong>：Actor（待训练）、Reference（冻结的 SFT 模型）、Reward Model（奖励模型）、Critic（价值函数）</li>
<li><strong>PPO 算法复杂</strong>：超参数一堆，训练不稳定，调参调到怀疑人生</li>
<li><strong>资源消耗大</strong>：4 个模型同时跑，显存吃不消</li>
</ol>
<blockquote>
<p>之前在 <a href="https://yuanchaofa.com/post/deepseek-r1-paper-reading-notes.html">DeepSeek-R1 论文解读</a> 里也提到过，DPO 是 RLHF 的一种替代方案，但 DeepSeek 最终还是用了 GRPO（一种改进的 PPO）。不过对于大多数场景来说，DPO 已经够用了。</p>
</blockquote>
<h3 id="12-dpo-的卖点"><a href="#12-dpo-的卖点" class="heading-anchor">1.2 DPO 的卖点</a></h3>
<p>DPO 的核心思路是：<strong>既然 RLHF 这么麻烦，能不能把强化学习的部分去掉，直接用监督学习的方式来做对齐？</strong></p>
<p>答案是可以的。DPO 的作者通过一系列数学推导（后面 Bonus 部分会讲），证明了可以把 RLHF 的优化目标转换成一个简单的损失函数，只需要 2 个模型就能搞定：</p>
<ul>
<li><strong>Actor</strong>：待训练的模型 <span class="katex-inline">$\pi_\theta$</span></li>
<li><strong>Reference</strong>：冻结的 SFT 模型 <span class="katex-inline">$\pi_{ref}$</span></li>
</ul>
<p>不需要单独训练 Reward Model，也不需要 PPO 那套复杂的东西。训练过程和 SFT 差不多，非常稳定。</p>
<h2 id="2-dpo-的核心思想"><a href="#2-dpo-的核心思想" class="heading-anchor">2. DPO 的核心思想</a></h2>
<h3 id="21-偏好数据长什么样"><a href="#21-偏好数据长什么样" class="heading-anchor">2.1 偏好数据长什么样？</a></h3>
<p>DPO 需要的数据格式很简单，就是一个 prompt 配上两个回答：一个好的（chosen），一个差的（rejected）。</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="c1"># DPO 偏好数据示例</span>
<span class="p">{</span>
    <span class="s2">&quot;prompt&quot;</span><span class="p">:</span> <span class="s2">&quot;介绍一下 chaofa用代码打点酱油 这个博主&quot;</span><span class="p">,</span>
    <span class="s2">&quot;chosen&quot;</span><span class="p">:</span> <span class="s2">&quot;chaofa用代码打点酱油 是一位专注于大模型技术的博主，他在 B站、YouTube 等平台分享 LLM 相关的技术内容，包括动手学大模型系列教程。他的内容特点是注重代码实现和原理讲解，帮助读者从零理解大模型的各种技术细节。&quot;</span><span class="p">,</span>
    <span class="s2">&quot;rejected&quot;</span><span class="p">:</span> <span class="s2">&quot;不知道，没听说过，说不定是个弱智。&quot;</span>
<span class="p">}</span>
</code></pre></div>
</code></pre>
<p>简单说就是：同一个问题，告诉模型哪个回答是好的，哪个是不好的。这种数据可以通过人工标注获得，也可以用更强的模型（比如 gemini/claude/gpt）来生成。</p>
<blockquote>
<p>TRICK: 非同源模型的数据训练的时候，可以先用 &quot;chosen&quot; 数据 SFT，不然可能导致 chosen 和 rejected 概率都变低。</p>
</blockquote>
<h3 id="22-dpo-想做什么"><a href="#22-dpo-想做什么" class="heading-anchor">2.2 DPO 想做什么？</a></h3>
<p>DPO 的目标其实就两个：</p>
<ol>
<li><strong>让模型更喜欢生成 chosen 回答</strong>：提高 chosen 的生成概率</li>
<li><strong>不要偏离原来的 SFT 模型太远</strong>：保持模型的基本能力，防止&quot;忘记&quot;之前学到的东西</li>
</ol>
<p>第二点很重要，如果只追求第一点，模型可能会为了迎合偏好数据而变得很奇怪（比如每个回答都很长、很啰嗦）。所以需要用参考模型来&quot;拉住&quot;它。</p>
<h3 id="23-dpo-损失函数"><a href="#23-dpo-损失函数" class="heading-anchor">2.3 DPO 损失函数</a></h3>
<p>好了，到了最核心的部分。DPO 的损失函数长这样：</p>
<div class="katex-block">$$\mathcal{L}_{\mathrm{DPO}}(\pi_\theta; \pi_{\mathrm{ref}}) = - \mathbb{E}_{(x,y_w,y_l) \sim D} \left[ \log \sigma\Big(\beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_{\mathrm{ref}}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\mathrm{ref}}(y_l \mid x)}\Big) \right]$$</div>
<p>这个公式看起来贼复杂，但逻辑其实很清晰。首先看公式里面的核心部分，是在比较两个东西：</p>
<ul>
<li><span class="katex-inline">$\log \frac{\pi_\theta(y_w \mid x)}{\pi_{\mathrm{ref}}(y_w \mid x)}$</span>：当前模型相对于参考模型，在 <strong>chosen</strong> 回答上的对数概率变化</li>
<li><span class="katex-inline">$\log \frac{\pi_\theta(y_l \mid x)}{\pi_{\mathrm{ref}}(y_l \mid x)}$</span>：当前模型相对于参考模型，在 <strong>rejected</strong> 回答上的对数概率变化</li>
</ul>
<p>我们希望前者大于后者。也就是说，模型在 chosen 上的&quot;提升幅度&quot;要大于在 rejected 上的&quot;提升幅度&quot;。</p>
<p><span class="katex-inline">$\beta$</span> 是一个超参数，用来控制&quot;偏离参考模型的惩罚力度&quot;。<span class="katex-inline">$\beta$</span> 越大，模型越不敢偏离参考模型；<span class="katex-inline">$\beta$</span> 越小，模型越&quot;激进&quot;。一般从 0.1 开始试。</p>
<p><span class="katex-inline">$\sigma$</span> 就是 sigmoid 函数，把差值映射到 (0, 1) 区间，然后取 log 变成 loss。</p>
<blockquote>
<p>Q: 这个公式是怎么推导出来的？为什么这样设计就能达到我们的目标？这些问题留到 Bonus 部分再说。现在只要理解&quot;DPO 在做什么&quot;就够了。</p>
</blockquote>
<h2 id="3-手撕-dpo-loss"><a href="#3-手撕-dpo-loss" class="heading-anchor">3. 手撕 DPO Loss</a></h2>
<p>理解了原理之后，我们来看看代码怎么写。其实 DPO 的核心代码非常简单，比公式看起来简单多了。</p>
<h3 id="31-计算序列的-log-概率"><a href="#31-计算序列的-log-概率" class="heading-anchor">3.1 计算序列的 log 概率</a></h3>
<p>首先，我们需要一个函数来计算模型在某个序列上的 log 概率。</p>
<p>对于语言模型来说，生成一个序列的概率就是每个 token 条件概率的乘积。取 log 之后，乘积变成求和：</p>
<div class="katex-block">$$\log \pi(y|x) = \sum_t \log P(y_t | y_{&lt;t}, x)$$</div>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">torch</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">torch.nn.functional</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">F</span>


<span class="k">def</span><span class="w"> </span><span class="nf">compute_log_probs</span><span class="p">(</span>
    <span class="n">logits</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span>       <span class="c1"># (batch, seq_len, vocab_size)</span>
    <span class="n">labels</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span>       <span class="c1"># (batch, seq_len)</span>
    <span class="n">mask</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span>          <span class="c1"># (batch, seq_len)，标记哪些位置需要计算</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">:</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    计算序列的对数概率</span>

<span class="sd">    注意：这里只计算 response 部分的概率，prompt 部分不算</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="c1"># 获取每个位置的 log softmax</span>
    <span class="n">log_probs</span> <span class="o">=</span> <span class="n">F</span><span class="o">.</span><span class="n">log_softmax</span><span class="p">(</span><span class="n">logits</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>

    <span class="c1"># 取出对应 label 的 log 概率</span>
    <span class="c1"># gather 操作：从 vocab_size 维度取出 labels 对应的概率</span>
    <span class="n">per_token_log_probs</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span>
        <span class="n">log_probs</span><span class="p">,</span>
        <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">,</span>
        <span class="n">index</span><span class="o">=</span><span class="n">labels</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>
    <span class="p">)</span><span class="o">.</span><span class="n">squeeze</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>

    <span class="c1"># 只计算 mask=1 的位置（response 部分）</span>
    <span class="n">masked_log_probs</span> <span class="o">=</span> <span class="n">per_token_log_probs</span> <span class="o">*</span> <span class="n">mask</span>

    <span class="c1"># 求和得到整个序列的 log 概率</span>
    <span class="k">return</span> <span class="n">masked_log_probs</span><span class="o">.</span><span class="n">sum</span><span class="p">(</span><span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div>
</code></pre>
<h3 id="32-dpo-loss-核心实现"><a href="#32-dpo-loss-核心实现" class="heading-anchor">3.2 DPO Loss 核心实现</a></h3>
<p>有了计算 log 概率的函数，DPO Loss 的实现就很直接了：</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">dpo_loss</span><span class="p">(</span>
    <span class="n">policy_chosen_logps</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span>    <span class="c1"># 当前模型在 chosen 上的 log 概率</span>
    <span class="n">policy_rejected_logps</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span>  <span class="c1"># 当前模型在 rejected 上的 log 概率</span>
    <span class="n">ref_chosen_logps</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span>       <span class="c1"># 参考模型在 chosen 上的 log 概率</span>
    <span class="n">ref_rejected_logps</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span>     <span class="c1"># 参考模型在 rejected 上的 log 概率</span>
    <span class="n">beta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">0.1</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">:</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    DPO Loss 的核心实现</span>

<span class="sd">    代码比公式简单多了吧？</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="c1"># 计算 log ratio：当前模型相对于参考模型的变化</span>
    <span class="n">chosen_log_ratios</span> <span class="o">=</span> <span class="n">policy_chosen_logps</span> <span class="o">-</span> <span class="n">ref_chosen_logps</span>
    <span class="n">rejected_log_ratios</span> <span class="o">=</span> <span class="n">policy_rejected_logps</span> <span class="o">-</span> <span class="n">ref_rejected_logps</span>

    <span class="c1"># 核心：我们希望 chosen 的 ratio 大于 rejected 的 ratio</span>
    <span class="n">logits</span> <span class="o">=</span> <span class="n">beta</span> <span class="o">*</span> <span class="p">(</span><span class="n">chosen_log_ratios</span> <span class="o">-</span> <span class="n">rejected_log_ratios</span><span class="p">)</span>

    <span class="c1"># 用 logsigmoid 更数值稳定（等价于 -log(sigmoid(logits))）</span>
    <span class="n">losses</span> <span class="o">=</span> <span class="o">-</span><span class="n">F</span><span class="o">.</span><span class="n">logsigmoid</span><span class="p">(</span><span class="n">logits</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">losses</span><span class="o">.</span><span class="n">mean</span><span class="p">()</span>
</code></pre></div>
</code></pre>
<p>就这么简单。核心就三行：</p>
<ol>
<li>计算 chosen 的 log ratio</li>
<li>计算 rejected 的 log ratio</li>
<li>用 sigmoid + log 算 loss</li>
</ol>
<blockquote>
<p>完整的训练代码涉及数据处理、模型加载等，这里就不展开了。可以参考 <a href="https://github.com/huggingface/trl">trl 源码</a>。</p>
</blockquote>
<h2 id="4-用-trl-跑一下-dpo-训练"><a href="#4-用-trl-跑一下-dpo-训练" class="heading-anchor">4. 用 trl 跑一下 DPO 训练</a></h2>
<p>手写 DPO Loss 是为了理解原理，实际训练的话直接用 trl 就好了。trl 是 Hugging Face 出的强化学习库，DPO 训练用起来很简单。</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="kn">from</span><span class="w"> </span><span class="nn">datasets</span><span class="w"> </span><span class="kn">import</span> <span class="n">Dataset</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">transformers</span><span class="w"> </span><span class="kn">import</span> <span class="n">AutoModelForCausalLM</span><span class="p">,</span> <span class="n">AutoTokenizer</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">trl</span><span class="w"> </span><span class="kn">import</span> <span class="n">DPOConfig</span><span class="p">,</span> <span class="n">DPOTrainer</span>

<span class="c1"># 1. 准备模型</span>
<span class="n">model_name</span> <span class="o">=</span> <span class="s2">&quot;Qwen/Qwen2.5-0.5B-Instruct&quot;</span>  <span class="c1"># 用小模型演示</span>
<span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>
<span class="n">tokenizer</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>

<span class="c1"># 参考模型（就是 SFT 后的模型，这里直接用同一个）</span>
<span class="n">ref_model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_name</span><span class="p">)</span>

<span class="c1"># 2. 准备数据（trl 需要的格式）</span>
<span class="n">train_data</span> <span class="o">=</span> <span class="n">Dataset</span><span class="o">.</span><span class="n">from_dict</span><span class="p">({</span>
    <span class="s2">&quot;prompt&quot;</span><span class="p">:</span> <span class="p">[</span>
        <span class="s2">&quot;介绍一下 chaofa用代码打点酱油 这个博主&quot;</span><span class="p">,</span>
        <span class="s2">&quot;DPO 和 RLHF 哪个更适合入门？&quot;</span><span class="p">,</span>
    <span class="p">],</span>
    <span class="s2">&quot;chosen&quot;</span><span class="p">:</span> <span class="p">[</span>
        <span class="s2">&quot;chaofa用代码打点酱油 是一位专注于大模型技术的博主，在 B站、YouTube 分享 LLM 相关教程，内容注重代码实现和原理讲解，帮助读者从零理解大模型技术。&quot;</span><span class="p">,</span>
        <span class="s2">&quot;建议先学 DPO，原理更简单，训练也更稳定。可以看 chaofa用代码打点酱油 的动手学大模型系列，有详细的代码实现。&quot;</span><span class="p">,</span>
    <span class="p">],</span>
    <span class="s2">&quot;rejected&quot;</span><span class="p">:</span> <span class="p">[</span>
        <span class="s2">&quot;没听说过，应该是个小透明吧。&quot;</span><span class="p">,</span>
        <span class="s2">&quot;都差不多，随便选一个。&quot;</span><span class="p">,</span>
    <span class="p">],</span>
<span class="p">})</span>

<span class="c1"># 3. 配置训练参数</span>
<span class="n">training_args</span> <span class="o">=</span> <span class="n">DPOConfig</span><span class="p">(</span>
    <span class="n">output_dir</span><span class="o">=</span><span class="s2">&quot;./dpo_output&quot;</span><span class="p">,</span>
    <span class="n">beta</span><span class="o">=</span><span class="mf">0.1</span><span class="p">,</span>                    <span class="c1"># DPO 的温度参数</span>
    <span class="n">learning_rate</span><span class="o">=</span><span class="mf">5e-7</span><span class="p">,</span>          <span class="c1"># DPO 通常用比较小的学习率</span>
    <span class="n">per_device_train_batch_size</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
    <span class="n">num_train_epochs</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>
    <span class="n">logging_steps</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
    <span class="n">bf16</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="p">)</span>

<span class="c1"># 4. 创建 Trainer 并训练</span>
<span class="n">trainer</span> <span class="o">=</span> <span class="n">DPOTrainer</span><span class="p">(</span>
    <span class="n">model</span><span class="o">=</span><span class="n">model</span><span class="p">,</span>
    <span class="n">ref_model</span><span class="o">=</span><span class="n">ref_model</span><span class="p">,</span>
    <span class="n">args</span><span class="o">=</span><span class="n">training_args</span><span class="p">,</span>
    <span class="n">train_dataset</span><span class="o">=</span><span class="n">train_data</span><span class="p">,</span>
    <span class="n">tokenizer</span><span class="o">=</span><span class="n">tokenizer</span><span class="p">,</span>
<span class="p">)</span>

<span class="n">trainer</span><span class="o">.</span><span class="n">train</span><span class="p">()</span>
</code></pre></div>
</code></pre>
<p>关键参数说一下：</p>
<ul>
<li><code>beta</code>：前面说过，控制偏离参考模型的惩罚力度，一般从 0.1 开始试</li>
<li><code>learning_rate</code>：DPO 通常用比较小的学习率，5e-7 到 5e-6 左右</li>
</ul>
<h2 id="5-bonus-1为什么-dpo-比-ppo-训练更稳定"><a href="#5-bonus-1为什么-dpo-比-ppo-训练更稳定" class="heading-anchor">5. Bonus 1：为什么 DPO 比 PPO 训练更稳定？</a></h2>
<p>很多人说&quot;DPO 比 PPO 更稳定&quot;，但到底为什么呢？这个问题其实可以从几个角度来理解：</p>
<h3 id="51-off-policy-vs-on-policy"><a href="#51-off-policy-vs-on-policy" class="heading-anchor">5.1 Off-policy vs On-policy</a></h3>
<p>PPO 是一种 <strong>on-policy</strong> 的强化学习算法，DPO 是 <strong>off-policy</strong> 的，它直接用离线的偏好数据来训练，训练过程和 SFT 差不多。</p>
<ul>
<li>on policy 每一次样本都是采样出来的，梯度可能会随时发生变化，梯度方差大；数据分布随着模型的更新会发生变化，上一轮学好的参数可能不适用下一轮，reward 比较稀疏（SFT/DPO 是 Token 级别的监督信号)。</li>
</ul>
<h3 id="52-不需要-reward-model-和-critic"><a href="#52-不需要-reward-model-和-critic" class="heading-anchor">5.2 不需要 Reward Model 和 Critic</a></h3>
<p>PPO 在 RLHF 中需要：</p>
<ul>
<li>一个 Reward Model 来打分（这个模型本身就可能有问题，比如 reward hacking）</li>
<li>一个 Critic（Value Function）来估计优势函数（这个网络的训练也不简单）</li>
</ul>
<p>这些额外的模型都会引入噪声和不稳定因素。DPO 把 Reward Model 直接&quot;吸收&quot;到了损失函数里，不需要单独训练，少了很多可能出错的地方。</p>
<h3 id="53-超参数敏感度"><a href="#53-超参数敏感度" class="heading-anchor">5.3 超参数敏感度</a></h3>
<p>PPO 有很多超参数需要调：</p>
<ul>
<li>clip ratio（裁剪系数）</li>
<li>GAE lambda</li>
<li>学习率、batch size、epoch 数</li>
<li>KL 惩罚系数</li>
<li>...</li>
</ul>
<p>这些参数之间还有复杂的相互作用，调参调到怀疑人生是常有的事。DPO 的核心超参数就一个 <span class="katex-inline">$\beta$</span>，最多再加上学习率。简单很多。</p>
<blockquote>
<p>备注：这里说的&quot;稳定&quot;。PPO/GRPO 调好了效果可能更好，但训练成本也更高。对于大多数场景来说，DPO 是一个性价比很高的选择。</p>
</blockquote>
<h2 id="6-bonus-2dpo-数学推导"><a href="#6-bonus-2dpo-数学推导" class="heading-anchor">6. Bonus 2：DPO 数学推导</a></h2>
<blockquote>
<p>这部分是给想深入理解的同学看的，跳过也不影响使用 DPO。</p>
</blockquote>
<p>DPO 的 Loss 不是凭空设计出来的，而是从 RLHF 的优化目标一步步推导出来的。</p>
<h3 id="61-rlhf-的优化目标"><a href="#61-rlhf-的优化目标" class="heading-anchor">6.1 RLHF 的优化目标</a></h3>
<p>RLHF 想要做的事情是：最大化奖励，同时不要偏离参考模型太远。用公式表示：</p>
<div class="katex-block">$$\max_{\pi} \; \mathbb{E}_{x \sim \mathcal{D},\, y \sim \pi(y \mid x)} \Big[ r(x,y) \Big] - \beta\, \mathbb{D}_{\mathrm{KL}}\Big[ \pi(y \mid x) \,\|\, \pi_{\mathrm{ref}}(y \mid x) \Big]$$</div>
<p>其中：</p>
<ul>
<li><span class="katex-inline">$r(x,y)$</span> 是奖励函数（需要单独训练一个 Reward Model）</li>
<li>KL 散度用来约束模型不要偏离参考模型太远</li>
<li><span class="katex-inline">$\beta$</span> 控制约束的强度</li>
</ul>
<h3 id="62-最优策略的形式"><a href="#62-最优策略的形式" class="heading-anchor">6.2 最优策略的形式</a></h3>
<p>这个优化问题有一个解析解。<strong>我们先假设存在这样一个最优策略</strong> <span class="katex-inline">$\pi^*$</span>，（具体推导可以参考 DPO 原论文附录，但我没看懂直接抄过来了），可以得到最优策略满足：</p>
<div class="katex-block">$$\pi^*(y \mid x) = \frac{1}{Z(x)} \pi_{\mathrm{ref}}(y \mid x) \exp\Big(\frac{1}{\beta} r(x,y)\Big)$$</div>
<p>其中 <span class="katex-inline">$Z(x) = \sum_y \pi_{\mathrm{ref}}(y \mid x) \exp\Big(\frac{1}{\beta} r(x,y)\Big)$</span> 是归一化常数（配分函数），保证概率和为 1。</p>
<blockquote>
<p>备注：</p>
<ul>
<li>它说的是：最优策略在参考策略的基础上，根据奖励大小进行&quot;加权&quot;。奖励高的回答概率会指数级增大，奖励低的会被抑制。<span class="katex-inline">$\beta$</span> 控制这个&quot;加权&quot;的激进程度。</li>
<li>这个最优策略就是我们要学习的「模型参数」</li>
</ul>
</blockquote>
<h3 id="63-反解奖励函数"><a href="#63-反解奖励函数" class="heading-anchor">6.3 反解奖励函数</a></h3>
<p>从上面的式子，我们可以反过来把奖励函数用策略来表示：</p>
<div class="katex-block">$$r(x,y) = \beta \log \frac{\pi^*(y \mid x)}{\pi_{\mathrm{ref}}(y \mid x)} + \beta \log Z(x)$$</div>
<p>这告诉我们：<strong>奖励函数可以用&quot;当前策略和参考策略的 log 概率比&quot;来表示</strong>。</p>
<h3 id="64-bradley-terry-偏好模型"><a href="#64-bradley-terry-偏好模型" class="heading-anchor">6.4 Bradley-Terry 偏好模型</a></h3>
<p>在有偏好数据的时候，我们通常用 Bradley-Terry 模型来建模&quot;哪个回答更好&quot;：</p>
<div class="katex-block">$$P(y_w \succ y_l \mid x) = \frac{\exp[r(x, y_w)]}{\exp[r(x, y_w)] + \exp[r(x, y_l)]} = \sigma(r(x, y_w) - r(x, y_l))$$</div>
<p><span class="katex-inline">$y_w$</span> 是 chosen 的样本， <span class="katex-inline">$y_l$</span> 是 rejected 的样本。<span class="katex-inline">$y_w$</span> 被偏好的概率取决于两个回答的奖励之差。</p>
<h3 id="65-代入得到-dpo-loss"><a href="#65-代入得到-dpo-loss" class="heading-anchor">6.5 代入得到 DPO Loss</a></h3>
<p>现在把 6.3 中的奖励函数代入 Bradley-Terry 模型。关键观察是：<span class="katex-inline">$\log Z(x)$</span> 在两个回答中是一样的，相减的时候会消掉！</p>
<div class="katex-block">$$r(x, y_w) - r(x, y_l) = \beta \log \frac{\pi^*(y_w \mid x)}{\pi_{\mathrm{ref}}(y_w \mid x)} - \beta \log \frac{\pi^*(y_l \mid x)}{\pi_{\mathrm{ref}}(y_l \mid x)}$$</div>
<p>前面提到，我们把 <strong>待训练的模型 <span class="katex-inline">$\pi_\theta$</span> 认为是最优策略 <span class="katex-inline">$\pi^*$</span></strong>。</p>
<p>最终，最大化偏好数据的似然（等价于最小化负对数似然），就得到了 DPO Loss：</p>
<div class="katex-block">$$\mathcal{L}_{\mathrm{DPO}} = - \mathbb{E}_{(x,y_w,y_l)} \left[ \log \sigma\Big(\beta \log \frac{\pi_\theta(y_w \mid x)}{\pi_{\mathrm{ref}}(y_w \mid x)} - \beta \log \frac{\pi_\theta(y_l \mid x)}{\pi_{\mathrm{ref}}(y_l \mid x)}\Big) \right]$$</div>
<p>这就是我们在第 2 节看到的 DPO Loss。</p>
<h2 id="7-总结"><a href="#7-总结" class="heading-anchor">7. 总结</a></h2>
<p>一句话总结：<strong>DPO 用监督学习的方式实现了 RLHF 的效果，把 4 个模型简化成 2 个，训练更稳定、资源消耗更低</strong>。</p>
<p>DPO 的局限性：</p>
<ul>
<li>依赖偏好数据的质量，数据不好效果就不好</li>
<li>对 <span class="katex-inline">$\beta$</span> 参数比较敏感，需要调参</li>
</ul>
<p>后续还有一些 DPO 的变体，比如 IPO（Identity Preference Optimization）、KTO（Kahneman-Tversky Optimization）等，以后有机会再聊（其实就是大概率没有机会了，醒醒吧，2026 年了）。</p>
<h2 id="8-参考资料"><a href="#8-参考资料" class="heading-anchor">8. 参考资料</a></h2>
<ol>
<li><a href="https://arxiv.org/abs/2305.18290">DPO 原论文: Direct Preference Optimization</a></li>
<li><a href="https://huggingface.co/docs/trl">trl 库文档</a></li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>从零手写 RoPE 位置编码：原理、PyTorch 源码实现与可视化理解</title>
      <link>https://yuanchaofa.com/post/hands-on-rope-position-embedding</link>
      <guid>https://yuanchaofa.com/post/hands-on-rope-position-embedding</guid>
      <source url="https://yuanchaofa.com/rss.xml">从零手写 RoPE 位置编码：原理、PyTorch 源码实现与可视化理解</source>
      <description>深入讲解 RoPE 旋转位置编码的核心原理与 PyTorch 实现。从 2D 旋转矩阵推导相对位置编码，逐行手写代码实现 LLaMA Qwen 风格 RoPE，附热力图可视化帮助理解。适合想彻底搞懂 RoPE 位置编码的开发者。</description>
      <category>hands-on-code</category>
      <pubDate>Thu, 01 Jan 2026 16:57:20 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="0-阅读收获-takeaway"><a href="#0-阅读收获-takeaway" class="heading-anchor">0. 阅读收获 (takeaway)</a></h2>
<p>本文旨在彻底搞懂 RoPE（Rotary Position Embedding）位置编码，阅读完本文你将获得：</p>
<ul>
<li>理解 RoPE 的核心思想：为什么用&quot;旋转&quot;来编码位置信息</li>
<li>掌握 RoPE 的数学原理：从旋转矩阵到三角函数证明</li>
<li>从零手写 RoPE 实现：逐行代码讲解，可直接运行</li>
<li>bonus：可视化理解 RoPE：通过热力图和动画直观感受旋转编码</li>
</ul>
<blockquote>
<p>本文代码运行于： <a href="https://featurize.cn/srx/gthYt2">Featurize 蒜粒方块 GPU 算力平台</a>，有 GPU 使用需求的同学希望能使用<a href="https://featurize.cn/srx/gthYt2">我的邀请链接注册</a></p>
<p>待更新：不喜欢看文字的同学可以看 <a href="https://space.bilibili.com/12420432">B站视频-chaofa用代码打点酱油</a>, <a href="https://www.youtube.com/@bbruceyuan">YouTube-chaofa用代码打点酱油</a>，或视频号：chaofa用代码打点酱油</p>
</blockquote>
<h2 id="1-为什么需要位置编码"><a href="#1-为什么需要位置编码" class="heading-anchor">1. 为什么需要位置编码？</a></h2>
<p>在 Transformer 架构中，<a href="https://yuanchaofa.com/hands-on-code/from-self-attention-to-multi-head-self-attention.html">Self-Attention 机制</a>本身是<strong>位置无关</strong>的。公式如下：</p>
<div class="katex-block">$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V$$</div>
<p><code>softmax</code> 中 QK 的乘积就是重要性权重，什么意思呢？</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="c1"># 假设我们有两个句子</span>
<span class="n">sentence1</span> <span class="o">=</span> <span class="s2">&quot;朝发 写 代码&quot;</span>
<span class="n">sentence2</span> <span class="o">=</span> <span class="s2">&quot;代码 写 朝发&quot;</span>

<span class="c1"># 对于纯 Self-Attention 来说，这两个句子的表示是一样的！</span>
<span class="c1"># 从公式看 Attention 只关心 token 之间的权重关系，不关心它们的顺序</span>
</code></pre></div>
</code></pre>
<p>这显然是不对的。语言是有顺序的，顺序不同意思完全不同。因此，我们需要<strong>位置编码</strong>（Position Encoding, PE）来告诉模型每个 token 在序列中的位置。</p>
<h3 id="11-绝对位置编码-vs-相对位置编码"><a href="#11-绝对位置编码-vs-相对位置编码" class="heading-anchor">1.1 绝对位置编码 vs 相对位置编码</a></h3>
<p>用一个例子来理解这两种编码方式的区别：</p>
<pre><code><div class="highlight"><pre><span></span><code>句子: &quot;朝发 写 代码&quot;
位置:   0  1  2
</code></pre></div>
</code></pre>
<p><strong>绝对位置编码</strong>：给每个位置一个固定编号</p>
<pre><code><div class="highlight"><pre><span></span><code>&quot;朝发&quot; → 位置 0 → PE_0
&quot;写&quot;   → 位置 1 → PE_1
&quot;代码&quot; → 位置 2 → PE_2
备注：PE_0 表示第一个位置的 embedding
</code></pre></div>
</code></pre>
<p><strong>相对位置编码</strong>：关注两个 token 之间的距离</p>
<pre><code><div class="highlight"><pre><span></span><code>计算 &quot;朝发&quot; 和 &quot;代码&quot; 的关系时：
→ 不关心它们分别在位置 0 和 2
→ 只关心它们相距 2 个位置
</code></pre></div>
</code></pre>
<p>同理：计算 &quot;朝发&quot; 和 &quot;写&quot; 之间的相对位置是 (1 - 0) = 1。</p>
<p>使用相对位置编码就是希望捕获 Token 之间位置的相对关系，保持（某些）语义的不变性，下面 「朝发」和「代码」之间的关系是一样的，尽管绝对位置不同：</p>
<pre><code><div class="highlight"><pre><span></span><code>句子 A: &quot;朝发 写 代码&quot;
句子 B: &quot;今天 朝发 写 代码&quot;
</code></pre></div>
</code></pre>
<h2 id="2-rope-的核心思想"><a href="#2-rope-的核心思想" class="heading-anchor">2. RoPE 的核心思想</a></h2>
<p>RoPE（Rotary Position Embedding，旋转位置编码）的核心思想非常优雅，可以阅读<a href="https://www.spaces.ac.cn/archives/8265">苏神 RoPE blog</a>：</p>
<blockquote>
<p><strong>通过旋转变换为向量注入位置信息，使得两个向量的内积只依赖于它们的相对位置。</strong></p>
</blockquote>
<p>这句话怎么理解呢？让我们一步步拆解看。</p>
<h3 id="21-从-2d-旋转说起"><a href="#21-从-2d-旋转说起" class="heading-anchor">2.1 从 2D 旋转说起</a></h3>
<p>假设我们在二维平面上有一个向量 <span class="katex-inline">$(x, y)$</span>，将它旋转角度 <span class="katex-inline">$\theta$</span> 后得到新向量：</p>
<div class="katex-block">$$\begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} \cos\theta &amp; -\sin\theta \\ \sin\theta &amp; \cos\theta \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix}$$</div>
<p>这就是经典的 2D 旋转矩阵。下面用一张图来直观理解：</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/20260101165342.png" alt="2D 向量旋转示意图" /></p>
<p>从图中可以看到：蓝色向量 <span class="katex-inline">$(x, y)$</span> 绕原点逆时针旋转角度 <span class="katex-inline">$\theta$</span> 后，变成红色向量 <span class="katex-inline">$(x', y')$</span>。</p>
<h3 id="22-rope-的目标与解决方案"><a href="#22-rope-的目标与解决方案" class="heading-anchor">2.2 RoPE 的目标与解决方案</a></h3>
<p><strong>目标</strong>：我们希望找到一个位置编码函数 <span class="katex-inline">$f$</span>，使得 query 向量 <span class="katex-inline">$\mathbf{q}_m$</span> 和 key 向量 <span class="katex-inline">$\mathbf{k}_n$</span> 的内积只依赖于它们的相对位置 <span class="katex-inline">$(m-n)$</span>：</p>
<div class="katex-block">$$\langle f_q(\mathbf{q}, m), f_k(\mathbf{k}, n) \rangle = g(\mathbf{q}, \mathbf{k}, m-n)$$</div>
<p>也就是说，无论 <span class="katex-inline">$m$</span> 和 <span class="katex-inline">$n$</span> 的绝对值是多少，只要 <span class="katex-inline">$m-n$</span> 相同，内积结果就相同。</p>
<p><strong>解决方案</strong>：RoPE 发现，这个函数 <span class="katex-inline">$f$</span> 就是<strong>旋转函数</strong>！（实际上是可以通过求解出来的，可以参考：<a href="https://www.spaces.ac.cn/archives/8265">Transformer升级之路：2、博采众长的旋转式位置编码</a>），这里我们假设「知道了这么一个函数」，然后我们去证明它符合我们的需求。</p>
<hr />
<p>假设词嵌入维度是 2 维（<span class="katex-inline">$d=2$</span>），对位置 <span class="katex-inline">$m$</span> 的向量 <span class="katex-inline">$\mathbf{q}$</span>，应用旋转角度 <span class="katex-inline">$m\theta$</span>：</p>
<div class="katex-block">$$f_q(\mathbf{q}, m) = \begin{pmatrix} \cos m\theta &amp; -\sin m\theta \\ \sin m\theta &amp; \cos m\theta \end{pmatrix} \begin{pmatrix} q_1 \\ q_2 \end{pmatrix}$$</div>
<p>同理，对位置 <span class="katex-inline">$n$</span> 的向量 <span class="katex-inline">$\mathbf{k}$</span>，应用旋转角度 <span class="katex-inline">$n\theta$</span>：</p>
<div class="katex-block">$$f_k(\mathbf{k}, n) = \begin{pmatrix} \cos n\theta &amp; -\sin n\theta \\ \sin n\theta &amp; \cos n\theta \end{pmatrix} \begin{pmatrix} k_1 \\ k_2 \end{pmatrix}$$</div>
<p>这就是为什么叫做<strong>旋转位置编码</strong>：位置信息通过旋转变换注入到向量中。</p>
<h3 id="23-证明旋转函数满足相对位置条件"><a href="#23-证明旋转函数满足相对位置条件" class="heading-anchor">2.3 证明：旋转函数满足相对位置条件</a></h3>
<p>现在我们来证明，旋转函数确实能让内积只依赖于相对位置 <span class="katex-inline">$(m-n)$</span>。</p>
<blockquote>
<p>备注：推导有点复杂，其实看前后即可。</p>
</blockquote>
<div class="katex-block">$$\begin{aligned}
&amp;\langle f_q(\mathbf{q}, m), f_k(\mathbf{k}, n) \rangle \\[8pt]
&amp;= \begin{pmatrix} \cos m\theta &amp; -\sin m\theta \\ \sin m\theta &amp; \cos m\theta \end{pmatrix} \begin{pmatrix} q_1 \\ q_2 \end{pmatrix} \cdot \begin{pmatrix} \cos n\theta &amp; -\sin n\theta \\ \sin n\theta &amp; \cos n\theta \end{pmatrix} \begin{pmatrix} k_1 \\ k_2 \end{pmatrix} \\[8pt]
&amp;= \begin{pmatrix} q_1 \cos m\theta - q_2 \sin m\theta \\ q_1 \sin m\theta + q_2 \cos m\theta \end{pmatrix} \cdot \begin{pmatrix} k_1 \cos n\theta - k_2 \sin n\theta \\ k_1 \sin n\theta + k_2 \cos n\theta \end{pmatrix} \\[8pt]
&amp;= (q_1 \cos m\theta - q_2 \sin m\theta)(k_1 \cos n\theta - k_2 \sin n\theta) \\
&amp;\quad + (q_1 \sin m\theta + q_2 \cos m\theta)(k_1 \sin n\theta + k_2 \cos n\theta) \\[8pt]
&amp;= q_1 k_1 (\cos m\theta \cos n\theta + \sin m\theta \sin n\theta) \\
&amp;\quad + q_2 k_2 (\sin m\theta \sin n\theta + \cos m\theta \cos n\theta) \\
&amp;\quad + q_1 k_2 (-\cos m\theta \sin n\theta + \sin m\theta \cos n\theta) \\
&amp;\quad + q_2 k_1 (-\sin m\theta \cos n\theta + \cos m\theta \sin n\theta) \\[8pt]
&amp;= q_1 k_1 \cos((m-n)\theta) + q_2 k_2 \cos((m-n)\theta) \\
&amp;\quad + q_1 k_2 \sin((m-n)\theta) - q_2 k_1 \sin((m-n)\theta) \\[8pt]
&amp;= (q_1 k_1 + q_2 k_2) \cos((m-n)\theta) + (q_1 k_2 - q_2 k_1) \sin((m-n)\theta) \\[8pt]
&amp;= \begin{pmatrix} q_1 &amp; q_2 \end{pmatrix} \underbrace{\begin{pmatrix} \cos((m-n)\theta) &amp; -\sin((m-n)\theta) \\ \sin((m-n)\theta) &amp; \cos((m-n)\theta) \end{pmatrix}}_{R_{m-n}} \begin{pmatrix} k_1 \\ k_2 \end{pmatrix} \\[8pt]
&amp;= \mathbf{q}^T \cdot R_{m-n} \cdot \mathbf{k}
\end{aligned}$$</div>
<p><strong>证毕</strong>：我们把中间这个只依赖于 <span class="katex-inline">$(m-n)$</span> 的旋转矩阵记为 <span class="katex-inline">$R_{m-n}$</span>，最终结果 <span class="katex-inline">$\mathbf{q}^T \cdot R_{m-n} \cdot \mathbf{k}$</span> 与 <span class="katex-inline">$m$</span> 和 <span class="katex-inline">$n$</span> 的绝对值无关，只与相对位置 <span class="katex-inline">$(m-n)$</span> 有关。</p>
<h2 id="3-rope-的数学原理"><a href="#3-rope-的数学原理" class="heading-anchor">3. RoPE 的数学原理</a></h2>
<p>现在让我们严格推导 RoPE 的数学形式。</p>
<h3 id="31-频率设计"><a href="#31-频率设计" class="heading-anchor">3.1 频率设计</a></h3>
<p>RoPE 对于维度 <span class="katex-inline">$d$</span> 的向量，两两配对处理。对于第 <span class="katex-inline">$i$</span> 对（共 <span class="katex-inline">$d/2$</span> 对），使用频率：</p>
<div class="katex-block">$$\theta_i = 10000^{-2i/d}$$</div>
<p>这个频率设计非常关键：</p>
<ul>
<li>低维度（小 <span class="katex-inline">$i$</span>）：频率高，变化快，捕捉短距离依赖</li>
<li>高维度（大 <span class="katex-inline">$i$</span>）：频率低，变化慢，捕捉长距离依赖</li>
</ul>
<h3 id="32-旋转矩阵的完整形式"><a href="#32-旋转矩阵的完整形式" class="heading-anchor">3.2 旋转矩阵的完整形式</a></h3>
<p>对于位置 <span class="katex-inline">$m$</span>，向量 <span class="katex-inline">$\mathbf{x} = [x_0, x_1, x_2, x_3, ..., x_{d-1}]$</span>，RoPE 的旋转操作可以写成：</p>
<div class="katex-block">$$\text{RoPE}(\mathbf{x}, m) = \begin{pmatrix}
x_0 \cos(m\theta_0) - x_1 \sin(m\theta_0) \\
x_1 \cos(m\theta_0) + x_0 \sin(m\theta_0) \\
x_2 \cos(m\theta_1) - x_3 \sin(m\theta_1) \\
x_3 \cos(m\theta_1) + x_2 \sin(m\theta_1) \\
\vdots
\end{pmatrix}$$</div>
<p>每两个维度组成一对，用对应的角度进行旋转。</p>
<h3 id="33-在-attention-中的应用"><a href="#33-在-attention-中的应用" class="heading-anchor">3.3 在 Attention 中的应用</a></h3>
<p>在 Self-Attention 中，RoPE 应用于 Query 和 Key：</p>
<div class="katex-block">$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{Q_{\text{rope}} K_{\text{rope}}^T}{\sqrt{d}}\right) V$$</div>
<p>其中 <span class="katex-inline">$Q_{\text{rope}} = \text{RoPE}(Q, m)$</span>，<span class="katex-inline">$K_{\text{rope}} = \text{RoPE}(K, n)$</span>。</p>
<p>由于旋转的特性，<span class="katex-inline">$Q_{\text{rope}} \cdot K_{\text{rope}}^T$</span> 的结果只依赖于相对位置 <span class="katex-inline">$m - n$</span>。</p>
<h2 id="4-从零手写-rope-实现"><a href="#4-从零手写-rope-实现" class="heading-anchor">4. 从零手写 RoPE 实现</a></h2>
<p>现在让我们一步步实现 RoPE。</p>
<h3 id="41-step-1-生成旋转频率"><a href="#41-step-1-生成旋转频率" class="heading-anchor">4.1 Step 1: 生成旋转频率</a></h3>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">torch</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">numpy</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">np</span>

<span class="k">def</span><span class="w"> </span><span class="nf">get_rotary_frequencies</span><span class="p">(</span><span class="n">dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">theta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">10000.0</span><span class="p">):</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    生成 RoPE 的旋转频率</span>

<span class="sd">    Args:</span>
<span class="sd">        dim: 嵌入维度（必须是偶数）</span>
<span class="sd">        seq_len: 序列长度</span>
<span class="sd">        theta: 基础频率参数</span>

<span class="sd">    Returns:</span>
<span class="sd">        freqs: shape (seq_len, dim // 2)，每个位置每个维度对的频率</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="c1"># 计算每个维度对的基础频率</span>
    <span class="c1"># theta_i = 10000^(-2i/d)，i = 0, 1, ..., d/2-1</span>
    <span class="n">i</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">arange</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">dim</span> <span class="o">//</span> <span class="mi">2</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">torch</span><span class="o">.</span><span class="n">float32</span><span class="p">)</span>
    <span class="n">freqs</span> <span class="o">=</span> <span class="n">theta</span> <span class="o">**</span> <span class="p">(</span><span class="o">-</span><span class="mi">2</span> <span class="o">*</span> <span class="n">i</span> <span class="o">/</span> <span class="n">dim</span><span class="p">)</span>  <span class="c1"># shape: (dim // 2,)</span>

    <span class="c1"># 生成位置索引</span>
    <span class="n">positions</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">arange</span><span class="p">(</span><span class="n">seq_len</span><span class="p">,</span> <span class="n">dtype</span><span class="o">=</span><span class="n">torch</span><span class="o">.</span><span class="n">float32</span><span class="p">)</span>  <span class="c1"># shape: (seq_len,)</span>

    <span class="c1"># 计算每个位置的角度：position * frequency</span>
    <span class="c1"># 外积得到 (seq_len, dim // 2) 的矩阵</span>
    <span class="n">angles</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">outer</span><span class="p">(</span><span class="n">positions</span><span class="p">,</span> <span class="n">freqs</span><span class="p">)</span>  <span class="c1"># shape: (seq_len, dim // 2)</span>

    <span class="k">return</span> <span class="n">angles</span>


<span class="c1"># 测试</span>
<span class="n">dim</span> <span class="o">=</span> <span class="mi">64</span>
<span class="n">seq_len</span> <span class="o">=</span> <span class="mi">128</span>
<span class="n">angles</span> <span class="o">=</span> <span class="n">get_rotary_frequencies</span><span class="p">(</span><span class="n">dim</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Angles shape: </span><span class="si">{</span><span class="n">angles</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>  <span class="c1"># (128, 32)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Angles[0]: </span><span class="si">{</span><span class="n">angles</span><span class="p">[</span><span class="mi">0</span><span class="p">][:</span><span class="mi">5</span><span class="p">]</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>    <span class="c1"># 位置 0 的前 5 个维度对的角度</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Angles[1]: </span><span class="si">{</span><span class="n">angles</span><span class="p">[</span><span class="mi">1</span><span class="p">][:</span><span class="mi">5</span><span class="p">]</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>    <span class="c1"># 位置 1 的前 5 个维度对的角度</span>
</code></pre></div>
</code></pre>
<h3 id="42-step-2-构建-sincos-缓存"><a href="#42-step-2-构建-sincos-缓存" class="heading-anchor">4.2 Step 2: 构建 sin/cos 缓存</a></h3>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_rotary_embedding</span><span class="p">(</span><span class="n">dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">theta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">10000.0</span><span class="p">):</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    预计算 RoPE 的 sin 和 cos 值</span>

<span class="sd">    Returns:</span>
<span class="sd">        cos: shape (seq_len, dim)</span>
<span class="sd">        sin: shape (seq_len, dim)</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">angles</span> <span class="o">=</span> <span class="n">get_rotary_frequencies</span><span class="p">(</span><span class="n">dim</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">theta</span><span class="p">)</span>

    <span class="c1"># 计算 cos 和 sin</span>
    <span class="n">cos</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">cos</span><span class="p">(</span><span class="n">angles</span><span class="p">)</span>
    <span class="n">sin</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">sin</span><span class="p">(</span><span class="n">angles</span><span class="p">)</span>

    <span class="c1"># 将 (seq_len, dim//2) 扩展为 (seq_len, dim)，与 rotate_half 配合使用</span>
    <span class="n">cos</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">cat</span><span class="p">([</span><span class="n">cos</span><span class="p">,</span> <span class="n">cos</span><span class="p">],</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">sin</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">cat</span><span class="p">([</span><span class="n">sin</span><span class="p">,</span> <span class="n">sin</span><span class="p">],</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">cos</span><span class="p">,</span> <span class="n">sin</span>


<span class="c1"># 测试</span>
<span class="n">cos</span><span class="p">,</span> <span class="n">sin</span> <span class="o">=</span> <span class="n">get_rotary_embedding</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">seq_len</span><span class="o">=</span><span class="mi">128</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Cos shape: </span><span class="si">{</span><span class="n">cos</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>  <span class="c1"># (128, 64)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Sin shape: </span><span class="si">{</span><span class="n">sin</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>  <span class="c1"># (128, 64)</span>
</code></pre></div>
</code></pre>
<h3 id="43-step-3-应用旋转变换"><a href="#43-step-3-应用旋转变换" class="heading-anchor">4.3 Step 3: 应用旋转变换</a></h3>
<p>这是 RoPE 的核心，参考 LLaMA 的实现方式：</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">rotate_half</span><span class="p">(</span><span class="n">x</span><span class="p">):</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    将向量的前半部分和后半部分交换，并对后半部分取负</span>
<span class="sd">    [x1, x2, x3, x4] -&gt; [-x3, -x4, x1, x2]</span>

<span class="sd">    这是实现旋转的关键辅助函数</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">x1</span> <span class="o">=</span> <span class="n">x</span><span class="p">[</span><span class="o">...</span><span class="p">,</span> <span class="p">:</span> <span class="n">x</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">//</span> <span class="mi">2</span><span class="p">]</span>
    <span class="n">x2</span> <span class="o">=</span> <span class="n">x</span><span class="p">[</span><span class="o">...</span><span class="p">,</span> <span class="n">x</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">//</span> <span class="mi">2</span> <span class="p">:]</span>
    <span class="k">return</span> <span class="n">torch</span><span class="o">.</span><span class="n">cat</span><span class="p">((</span><span class="o">-</span><span class="n">x2</span><span class="p">,</span> <span class="n">x1</span><span class="p">),</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>


<span class="k">def</span><span class="w"> </span><span class="nf">apply_rotary_pos_emb</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">cos</span><span class="p">,</span> <span class="n">sin</span><span class="p">):</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    应用 RoPE 旋转变换（LLaMA 风格实现）</span>

<span class="sd">    Args:</span>
<span class="sd">        q: Query，shape (batch, seq_len, num_heads, head_dim)</span>
<span class="sd">        k: Key，shape (batch, seq_len, num_heads, head_dim)</span>
<span class="sd">        cos: shape (seq_len, head_dim)</span>
<span class="sd">        sin: shape (seq_len, head_dim)</span>

<span class="sd">    Returns:</span>
<span class="sd">        q_rot, k_rot: 旋转后的 Query 和 Key</span>

<span class="sd">    旋转公式：</span>
<span class="sd">        q&#39; = q * cos + rotate_half(q) * sin</span>
<span class="sd">        k&#39; = k * cos + rotate_half(k) * sin</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="c1"># 调整 cos/sin 形状以便广播: (seq_len, head_dim) -&gt; (1, seq_len, 1, head_dim)</span>
    <span class="n">cos</span> <span class="o">=</span> <span class="n">cos</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="n">sin</span> <span class="o">=</span> <span class="n">sin</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">.</span><span class="n">unsqueeze</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>

    <span class="c1"># 应用旋转</span>
    <span class="n">q_embed</span> <span class="o">=</span> <span class="p">(</span><span class="n">q</span> <span class="o">*</span> <span class="n">cos</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">rotate_half</span><span class="p">(</span><span class="n">q</span><span class="p">)</span> <span class="o">*</span> <span class="n">sin</span><span class="p">)</span>
    <span class="n">k_embed</span> <span class="o">=</span> <span class="p">(</span><span class="n">k</span> <span class="o">*</span> <span class="n">cos</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">rotate_half</span><span class="p">(</span><span class="n">k</span><span class="p">)</span> <span class="o">*</span> <span class="n">sin</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">q_embed</span><span class="p">,</span> <span class="n">k_embed</span>
</code></pre></div>
</code></pre>
<p><strong>为什么这个公式是对的？</strong></p>
<p>回顾 2D 旋转公式：</p>
<div class="katex-block">$$\begin{pmatrix} x' \\ y' \end{pmatrix} = \begin{pmatrix} x \cos\theta - y \sin\theta \\ x \sin\theta + y \cos\theta \end{pmatrix}$$</div>
<p>对于向量 <span class="katex-inline">$[x, y]$</span>，<code>rotate_half</code> 会把它变成 <span class="katex-inline">$[-y, x]$</span>，所以：</p>
<pre><code><div class="highlight"><pre><span></span><code>原向量 <span class="gs">* cos + rotate_half(原向量) *</span> sin
= [x, y] <span class="gs">* cos + [-y, x] *</span> sin
= [x*cos - y*sin, y*cos + x*sin]
</code></pre></div>
</code></pre>
<p>这正是旋转公式！</p>
<h3 id="44-step-4-完整的-rope-模块"><a href="#44-step-4-完整的-rope-模块" class="heading-anchor">4.4 Step 4: 完整的 RoPE 模块</a></h3>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">RotaryPositionEmbedding</span><span class="p">(</span><span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    完整的 RoPE 实现</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dim</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">4096</span><span class="p">,</span> <span class="n">theta</span><span class="p">:</span> <span class="nb">float</span> <span class="o">=</span> <span class="mf">10000.0</span><span class="p">):</span>
<span class="w">        </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        Args:</span>
<span class="sd">            dim: 每个注意力头的维度</span>
<span class="sd">            max_seq_len: 最大序列长度</span>
<span class="sd">            theta: 基础频率参数</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">dim</span> <span class="o">=</span> <span class="n">dim</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">max_seq_len</span> <span class="o">=</span> <span class="n">max_seq_len</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">theta</span> <span class="o">=</span> <span class="n">theta</span>

        <span class="c1"># 预计算并缓存 sin/cos 值</span>
        <span class="n">cos</span><span class="p">,</span> <span class="n">sin</span> <span class="o">=</span> <span class="n">get_rotary_embedding</span><span class="p">(</span><span class="n">dim</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">,</span> <span class="n">theta</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">register_buffer</span><span class="p">(</span><span class="s1">&#39;cos_cached&#39;</span><span class="p">,</span> <span class="n">cos</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">register_buffer</span><span class="p">(</span><span class="s1">&#39;sin_cached&#39;</span><span class="p">,</span> <span class="n">sin</span><span class="p">)</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">q</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">k</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">positions</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span> <span class="o">=</span> <span class="kc">None</span><span class="p">):</span>
<span class="w">        </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        对 Query 和 Key 应用 RoPE</span>

<span class="sd">        Args:</span>
<span class="sd">            q: Query，shape (batch, seq_len, num_heads, head_dim)</span>
<span class="sd">            k: Key，shape (batch, seq_len, num_heads, head_dim)</span>
<span class="sd">            positions: 位置索引，默认为 [0, 1, 2, ..., seq_len-1]</span>

<span class="sd">        Returns:</span>
<span class="sd">            q_rot, k_rot: 旋转后的 Query 和 Key</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">seq_len</span> <span class="o">=</span> <span class="n">q</span><span class="o">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>

        <span class="c1"># 获取当前序列长度的 cos/sin</span>
        <span class="n">cos</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">cos_cached</span><span class="p">[:</span><span class="n">seq_len</span><span class="p">]</span>
        <span class="n">sin</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">sin_cached</span><span class="p">[:</span><span class="n">seq_len</span><span class="p">]</span>

        <span class="c1"># 应用旋转</span>
        <span class="n">q_rot</span><span class="p">,</span> <span class="n">k_rot</span> <span class="o">=</span> <span class="n">apply_rotary_pos_emb</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">cos</span><span class="p">,</span> <span class="n">sin</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">q_rot</span><span class="p">,</span> <span class="n">k_rot</span>


<span class="c1"># 测试</span>
<span class="n">rope</span> <span class="o">=</span> <span class="n">RotaryPositionEmbedding</span><span class="p">(</span><span class="n">dim</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="o">=</span><span class="mi">4096</span><span class="p">)</span>

<span class="c1"># 模拟输入</span>
<span class="n">batch_size</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">seq_len</span> <span class="o">=</span> <span class="mi">128</span>
<span class="n">num_heads</span> <span class="o">=</span> <span class="mi">8</span>
<span class="n">head_dim</span> <span class="o">=</span> <span class="mi">64</span>

<span class="n">q</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">num_heads</span><span class="p">,</span> <span class="n">head_dim</span><span class="p">)</span>
<span class="n">k</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="n">batch_size</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">num_heads</span><span class="p">,</span> <span class="n">head_dim</span><span class="p">)</span>

<span class="n">q_rot</span><span class="p">,</span> <span class="n">k_rot</span> <span class="o">=</span> <span class="n">rope</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Q_rot shape: </span><span class="si">{</span><span class="n">q_rot</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;K_rot shape: </span><span class="si">{</span><span class="n">k_rot</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
</code></pre></div>
</code></pre>
<h3 id="45-验证相对位置不变性"><a href="#45-验证相对位置不变性" class="heading-anchor">4.5 验证：相对位置不变性</a></h3>
<p>RoPE 最重要的性质是：两个位置的 Query 和 Key 的内积只依赖于它们的相对位置。让我们验证一下：</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">verify_relative_position_invariance</span><span class="p">():</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    验证 RoPE 的相对位置不变性</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">dim</span> <span class="o">=</span> <span class="mi">64</span>
    <span class="n">max_seq_len</span> <span class="o">=</span> <span class="mi">100</span>

    <span class="c1"># 预计算 cos/sin</span>
    <span class="n">cos</span><span class="p">,</span> <span class="n">sin</span> <span class="o">=</span> <span class="n">get_rotary_embedding</span><span class="p">(</span><span class="n">dim</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">)</span>

    <span class="c1"># 创建两个相同的向量</span>
    <span class="n">torch</span><span class="o">.</span><span class="n">manual_seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
    <span class="n">q</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">dim</span><span class="p">)</span>
    <span class="n">k</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">dim</span><span class="p">)</span>

    <span class="c1"># 场景 1：q 在位置 0，k 在位置 5（相对位置 = 5）</span>
    <span class="n">cos1_q</span><span class="p">,</span> <span class="n">sin1_q</span> <span class="o">=</span> <span class="n">cos</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">1</span><span class="p">],</span> <span class="n">sin</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">1</span><span class="p">]</span>
    <span class="n">cos1_k</span><span class="p">,</span> <span class="n">sin1_k</span> <span class="o">=</span> <span class="n">cos</span><span class="p">[</span><span class="mi">5</span><span class="p">:</span><span class="mi">6</span><span class="p">],</span> <span class="n">sin</span><span class="p">[</span><span class="mi">5</span><span class="p">:</span><span class="mi">6</span><span class="p">]</span>

    <span class="n">q1_rot</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">apply_rotary_pos_emb</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">cos1_q</span><span class="p">,</span> <span class="n">sin1_q</span><span class="p">)</span>
    <span class="n">_</span><span class="p">,</span> <span class="n">k1_rot</span> <span class="o">=</span> <span class="n">apply_rotary_pos_emb</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">cos1_k</span><span class="p">,</span> <span class="n">sin1_k</span><span class="p">)</span>

    <span class="n">dot_product_1</span> <span class="o">=</span> <span class="p">(</span><span class="n">q1_rot</span> <span class="o">*</span> <span class="n">k1_rot</span><span class="p">)</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span>

    <span class="c1"># 场景 2：q 在位置 10，k 在位置 15（相对位置仍然是 5）</span>
    <span class="n">cos2_q</span><span class="p">,</span> <span class="n">sin2_q</span> <span class="o">=</span> <span class="n">cos</span><span class="p">[</span><span class="mi">10</span><span class="p">:</span><span class="mi">11</span><span class="p">],</span> <span class="n">sin</span><span class="p">[</span><span class="mi">10</span><span class="p">:</span><span class="mi">11</span><span class="p">]</span>
    <span class="n">cos2_k</span><span class="p">,</span> <span class="n">sin2_k</span> <span class="o">=</span> <span class="n">cos</span><span class="p">[</span><span class="mi">15</span><span class="p">:</span><span class="mi">16</span><span class="p">],</span> <span class="n">sin</span><span class="p">[</span><span class="mi">15</span><span class="p">:</span><span class="mi">16</span><span class="p">]</span>

    <span class="n">q2_rot</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">apply_rotary_pos_emb</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">cos2_q</span><span class="p">,</span> <span class="n">sin2_q</span><span class="p">)</span>
    <span class="n">_</span><span class="p">,</span> <span class="n">k2_rot</span> <span class="o">=</span> <span class="n">apply_rotary_pos_emb</span><span class="p">(</span><span class="n">k</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">cos2_k</span><span class="p">,</span> <span class="n">sin2_k</span><span class="p">)</span>

    <span class="n">dot_product_2</span> <span class="o">=</span> <span class="p">(</span><span class="n">q2_rot</span> <span class="o">*</span> <span class="n">k2_rot</span><span class="p">)</span><span class="o">.</span><span class="n">sum</span><span class="p">()</span>

    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;位置 (0, 5) 的内积: </span><span class="si">{</span><span class="n">dot_product_1</span><span class="o">.</span><span class="n">item</span><span class="p">()</span><span class="si">:</span><span class="s2">.6f</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;位置 (10, 15) 的内积: </span><span class="si">{</span><span class="n">dot_product_2</span><span class="o">.</span><span class="n">item</span><span class="p">()</span><span class="si">:</span><span class="s2">.6f</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;差异: </span><span class="si">{</span><span class="nb">abs</span><span class="p">(</span><span class="n">dot_product_1</span><span class="o">.</span><span class="n">item</span><span class="p">()</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">dot_product_2</span><span class="o">.</span><span class="n">item</span><span class="p">())</span><span class="si">:</span><span class="s2">.10f</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;验证通过！&quot;</span> <span class="k">if</span> <span class="nb">abs</span><span class="p">(</span><span class="n">dot_product_1</span><span class="o">.</span><span class="n">item</span><span class="p">()</span> <span class="o">-</span> <span class="n">dot_product_2</span><span class="o">.</span><span class="n">item</span><span class="p">())</span> <span class="o">&lt;</span> <span class="mf">1e-5</span> <span class="k">else</span> <span class="s2">&quot;验证失败！&quot;</span><span class="p">)</span>


<span class="n">verify_relative_position_invariance</span><span class="p">()</span>
</code></pre></div>
</code></pre>
<h2 id="5-为什么-rope-位置编码好"><a href="#5-为什么-rope-位置编码好" class="heading-anchor">5. 为什么 RoPE 位置编码好？</a></h2>
<ul>
<li>相对位置编码：内积只依赖相对位置，天然适合语言建模</li>
<li>外推性能好：配合 NTK/YaRN 可以泛化到更长序列</li>
<li>计算高效：不增加额外的位置嵌入，只需旋转操作</li>
<li>无需额外参数：基于固定的三角函数，不增加可学习参数</li>
<li>兼容 KV Cache：缓存的 K 无需重新计算位置编码</li>
</ul>
<h2 id="6-可视化理解-rope-bonus"><a href="#6-可视化理解-rope-bonus" class="heading-anchor">6. 可视化理解 RoPE （Bonus）</a></h2>
<p>以下是选看内容（为了帮助理解 RoPE 的内容）</p>
<h3 id="61-位置编码热力图"><a href="#61-位置编码热力图" class="heading-anchor">6.1 位置编码热力图</a></h3>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">matplotlib.pyplot</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">plt</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">seaborn</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">sns</span>

<span class="k">def</span><span class="w"> </span><span class="nf">visualize_rope_heatmap</span><span class="p">():</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    可视化 RoPE 编码的热力图</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">dim</span> <span class="o">=</span> <span class="mi">64</span>
    <span class="n">seq_len</span> <span class="o">=</span> <span class="mi">128</span>

    <span class="n">cos</span><span class="p">,</span> <span class="n">sin</span> <span class="o">=</span> <span class="n">get_rotary_embedding</span><span class="p">(</span><span class="n">dim</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">)</span>

    <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mi">5</span><span class="p">))</span>

    <span class="c1"># Cos 热力图</span>
    <span class="n">sns</span><span class="o">.</span><span class="n">heatmap</span><span class="p">(</span><span class="n">cos</span><span class="o">.</span><span class="n">numpy</span><span class="p">()[:</span><span class="mi">64</span><span class="p">,</span> <span class="p">:],</span> <span class="n">ax</span><span class="o">=</span><span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">cmap</span><span class="o">=</span><span class="s1">&#39;RdBu&#39;</span><span class="p">,</span> <span class="n">center</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">set_title</span><span class="p">(</span><span class="s1">&#39;RoPE Cos Values&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">set_xlabel</span><span class="p">(</span><span class="s1">&#39;Dimension&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s1">&#39;Position&#39;</span><span class="p">)</span>

    <span class="c1"># Sin 热力图</span>
    <span class="n">sns</span><span class="o">.</span><span class="n">heatmap</span><span class="p">(</span><span class="n">sin</span><span class="o">.</span><span class="n">numpy</span><span class="p">()[:</span><span class="mi">64</span><span class="p">,</span> <span class="p">:],</span> <span class="n">ax</span><span class="o">=</span><span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">cmap</span><span class="o">=</span><span class="s1">&#39;RdBu&#39;</span><span class="p">,</span> <span class="n">center</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">set_title</span><span class="p">(</span><span class="s1">&#39;RoPE Sin Values&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">set_xlabel</span><span class="p">(</span><span class="s1">&#39;Dimension&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s1">&#39;Position&#39;</span><span class="p">)</span>

    <span class="n">plt</span><span class="o">.</span><span class="n">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="o">.</span><span class="n">savefig</span><span class="p">(</span><span class="s1">&#39;rope_heatmap.png&#39;</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="o">.</span><span class="n">show</span><span class="p">()</span>

    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;观察要点：&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;1. 低维度（左侧）变化快 -&gt; 捕捉短距离依赖&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;2. 高维度（右侧）变化慢 -&gt; 捕捉长距离依赖&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;3. 每个维度都是周期函数，频率不同&quot;</span><span class="p">)</span>


<span class="n">visualize_rope_heatmap</span><span class="p">()</span>
</code></pre></div>
</code></pre>
<h3 id="62-2d-旋转动画"><a href="#62-2d-旋转动画" class="heading-anchor">6.2 2D 旋转动画</a></h3>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="kn">import</span><span class="w"> </span><span class="nn">matplotlib.pyplot</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">plt</span>
<span class="kn">import</span><span class="w"> </span><span class="nn">matplotlib.animation</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="nn">animation</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">IPython.display</span><span class="w"> </span><span class="kn">import</span> <span class="n">HTML</span>

<span class="k">def</span><span class="w"> </span><span class="nf">visualize_2d_rotation</span><span class="p">():</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    可视化 2D 空间中的旋转效果</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="c1"># 原始向量</span>
    <span class="n">original</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span><span class="mf">1.0</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">])</span>

    <span class="c1"># 不同位置的旋转角度</span>
    <span class="n">positions</span> <span class="o">=</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span>
    <span class="n">theta_base</span> <span class="o">=</span> <span class="mf">0.5</span>  <span class="c1"># 基础角度</span>

    <span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">subplots</span><span class="p">(</span><span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mi">8</span><span class="p">))</span>

    <span class="n">colors</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">cm</span><span class="o">.</span><span class="n">viridis</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">positions</span><span class="p">)))</span>

    <span class="k">for</span> <span class="n">pos</span><span class="p">,</span> <span class="n">color</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">positions</span><span class="p">,</span> <span class="n">colors</span><span class="p">):</span>
        <span class="n">angle</span> <span class="o">=</span> <span class="n">pos</span> <span class="o">*</span> <span class="n">theta_base</span>
        <span class="c1"># 旋转矩阵</span>
        <span class="n">cos_a</span><span class="p">,</span> <span class="n">sin_a</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">cos</span><span class="p">(</span><span class="n">angle</span><span class="p">),</span> <span class="n">np</span><span class="o">.</span><span class="n">sin</span><span class="p">(</span><span class="n">angle</span><span class="p">)</span>
        <span class="n">rotated</span> <span class="o">=</span> <span class="n">np</span><span class="o">.</span><span class="n">array</span><span class="p">([</span>
            <span class="n">original</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">cos_a</span> <span class="o">-</span> <span class="n">original</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">sin_a</span><span class="p">,</span>
            <span class="n">original</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">sin_a</span> <span class="o">+</span> <span class="n">original</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">cos_a</span>
        <span class="p">])</span>

        <span class="n">ax</span><span class="o">.</span><span class="n">arrow</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">rotated</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">rotated</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
                <span class="n">head_width</span><span class="o">=</span><span class="mf">0.05</span><span class="p">,</span> <span class="n">head_length</span><span class="o">=</span><span class="mf">0.03</span><span class="p">,</span>
                <span class="n">fc</span><span class="o">=</span><span class="n">color</span><span class="p">,</span> <span class="n">ec</span><span class="o">=</span><span class="n">color</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.7</span><span class="p">,</span>
                <span class="n">label</span><span class="o">=</span><span class="sa">f</span><span class="s1">&#39;pos=</span><span class="si">{</span><span class="n">pos</span><span class="si">}</span><span class="s1">&#39;</span> <span class="k">if</span> <span class="n">pos</span> <span class="o">%</span> <span class="mi">4</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">None</span><span class="p">)</span>

    <span class="n">ax</span><span class="o">.</span><span class="n">set_xlim</span><span class="p">(</span><span class="o">-</span><span class="mf">1.5</span><span class="p">,</span> <span class="mf">1.5</span><span class="p">)</span>
    <span class="n">ax</span><span class="o">.</span><span class="n">set_ylim</span><span class="p">(</span><span class="o">-</span><span class="mf">1.5</span><span class="p">,</span> <span class="mf">1.5</span><span class="p">)</span>
    <span class="n">ax</span><span class="o">.</span><span class="n">set_aspect</span><span class="p">(</span><span class="s1">&#39;equal&#39;</span><span class="p">)</span>
    <span class="n">ax</span><span class="o">.</span><span class="n">grid</span><span class="p">(</span><span class="kc">True</span><span class="p">,</span> <span class="n">alpha</span><span class="o">=</span><span class="mf">0.3</span><span class="p">)</span>
    <span class="n">ax</span><span class="o">.</span><span class="n">axhline</span><span class="p">(</span><span class="n">y</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s1">&#39;k&#39;</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="n">ax</span><span class="o">.</span><span class="n">axvline</span><span class="p">(</span><span class="n">x</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s1">&#39;k&#39;</span><span class="p">,</span> <span class="n">linewidth</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
    <span class="n">ax</span><span class="o">.</span><span class="n">legend</span><span class="p">(</span><span class="n">loc</span><span class="o">=</span><span class="s1">&#39;upper right&#39;</span><span class="p">)</span>
    <span class="n">ax</span><span class="o">.</span><span class="n">set_title</span><span class="p">(</span><span class="s1">&#39;RoPE: 不同位置的向量旋转效果</span><span class="se">\n</span><span class="s1">(同一向量在不同位置被旋转不同角度)&#39;</span><span class="p">)</span>

    <span class="n">plt</span><span class="o">.</span><span class="n">savefig</span><span class="p">(</span><span class="s1">&#39;rope_rotation.png&#39;</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="o">.</span><span class="n">show</span><span class="p">()</span>

    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;观察要点：&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;1. 同一向量在不同位置被旋转不同角度&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;2. 位置越大，旋转角度越大&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;3. 这就是 RoPE 编码位置信息的方式&quot;</span><span class="p">)</span>


<span class="n">visualize_2d_rotation</span><span class="p">()</span>
</code></pre></div>
</code></pre>
<h3 id="63-相对位置注意力分数"><a href="#63-相对位置注意力分数" class="heading-anchor">6.3 相对位置注意力分数</a></h3>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">visualize_relative_attention</span><span class="p">():</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    可视化 RoPE 对注意力分数的影响</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="n">dim</span> <span class="o">=</span> <span class="mi">64</span>
    <span class="n">seq_len</span> <span class="o">=</span> <span class="mi">32</span>

    <span class="c1"># 生成随机 Q 和 K</span>
    <span class="n">torch</span><span class="o">.</span><span class="n">manual_seed</span><span class="p">(</span><span class="mi">42</span><span class="p">)</span>
    <span class="n">q</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">dim</span><span class="p">)</span>
    <span class="n">k</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">dim</span><span class="p">)</span>

    <span class="c1"># 应用 RoPE</span>
    <span class="n">rope</span> <span class="o">=</span> <span class="n">RotaryPositionEmbedding</span><span class="p">(</span><span class="n">dim</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">)</span>
    <span class="n">q_rot</span><span class="p">,</span> <span class="n">k_rot</span> <span class="o">=</span> <span class="n">rope</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>

    <span class="c1"># 计算注意力分数</span>
    <span class="c1"># 无 RoPE</span>
    <span class="n">attn_no_rope</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">squeeze</span><span class="p">(),</span> <span class="n">k</span><span class="o">.</span><span class="n">squeeze</span><span class="p">()</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span> <span class="o">/</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">dim</span><span class="p">)</span>

    <span class="c1"># 有 RoPE</span>
    <span class="n">attn_with_rope</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">q_rot</span><span class="o">.</span><span class="n">squeeze</span><span class="p">(),</span> <span class="n">k_rot</span><span class="o">.</span><span class="n">squeeze</span><span class="p">()</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span> <span class="o">/</span> <span class="n">np</span><span class="o">.</span><span class="n">sqrt</span><span class="p">(</span><span class="n">dim</span><span class="p">)</span>

    <span class="n">fig</span><span class="p">,</span> <span class="n">axes</span> <span class="o">=</span> <span class="n">plt</span><span class="o">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">14</span><span class="p">,</span> <span class="mi">5</span><span class="p">))</span>

    <span class="n">sns</span><span class="o">.</span><span class="n">heatmap</span><span class="p">(</span><span class="n">attn_no_rope</span><span class="o">.</span><span class="n">squeeze</span><span class="p">()</span><span class="o">.</span><span class="n">detach</span><span class="p">()</span><span class="o">.</span><span class="n">numpy</span><span class="p">(),</span> <span class="n">ax</span><span class="o">=</span><span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">cmap</span><span class="o">=</span><span class="s1">&#39;viridis&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">set_title</span><span class="p">(</span><span class="s1">&#39;Attention Scores (No RoPE)&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">set_xlabel</span><span class="p">(</span><span class="s1">&#39;Key Position&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s1">&#39;Query Position&#39;</span><span class="p">)</span>

    <span class="n">sns</span><span class="o">.</span><span class="n">heatmap</span><span class="p">(</span><span class="n">attn_with_rope</span><span class="o">.</span><span class="n">squeeze</span><span class="p">()</span><span class="o">.</span><span class="n">detach</span><span class="p">()</span><span class="o">.</span><span class="n">numpy</span><span class="p">(),</span> <span class="n">ax</span><span class="o">=</span><span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">cmap</span><span class="o">=</span><span class="s1">&#39;viridis&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">set_title</span><span class="p">(</span><span class="s1">&#39;Attention Scores (With RoPE)&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">set_xlabel</span><span class="p">(</span><span class="s1">&#39;Key Position&#39;</span><span class="p">)</span>
    <span class="n">axes</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="o">.</span><span class="n">set_ylabel</span><span class="p">(</span><span class="s1">&#39;Query Position&#39;</span><span class="p">)</span>

    <span class="n">plt</span><span class="o">.</span><span class="n">tight_layout</span><span class="p">()</span>
    <span class="n">plt</span><span class="o">.</span><span class="n">savefig</span><span class="p">(</span><span class="s1">&#39;rope_attention.png&#39;</span><span class="p">,</span> <span class="n">dpi</span><span class="o">=</span><span class="mi">150</span><span class="p">)</span>
    <span class="n">plt</span><span class="o">.</span><span class="n">show</span><span class="p">()</span>

    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;观察要点：&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;1. 无 RoPE 时，注意力分数与位置无关&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;2. 有 RoPE 时，注意力分数体现位置关系&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;3. 对角线附近通常有更高的注意力（局部依赖）&quot;</span><span class="p">)</span>


<span class="n">visualize_relative_attention</span><span class="p">()</span>
</code></pre></div>
</code></pre>
<h3 id="64-实际应用集成到-transformer"><a href="#64-实际应用集成到-transformer" class="heading-anchor">6.4. 实际应用：集成到 Transformer</a></h3>
<p>最后，让我们看看如何将 RoPE 集成到完整的 Multi-Head Attention 中：</p>
<pre><code class="language-python"><div class="highlight"><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">MultiHeadAttentionWithRoPE</span><span class="p">(</span><span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Module</span><span class="p">):</span>
<span class="w">    </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">    带 RoPE 的多头注意力</span>
<span class="sd">    &quot;&quot;&quot;</span>
    <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">d_model</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">num_heads</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">4096</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">()</span>
        <span class="k">assert</span> <span class="n">d_model</span> <span class="o">%</span> <span class="n">num_heads</span> <span class="o">==</span> <span class="mi">0</span>

        <span class="bp">self</span><span class="o">.</span><span class="n">d_model</span> <span class="o">=</span> <span class="n">d_model</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">num_heads</span> <span class="o">=</span> <span class="n">num_heads</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span> <span class="o">=</span> <span class="n">d_model</span> <span class="o">//</span> <span class="n">num_heads</span>

        <span class="bp">self</span><span class="o">.</span><span class="n">q_proj</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">k_proj</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">v_proj</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">o_proj</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">Linear</span><span class="p">(</span><span class="n">d_model</span><span class="p">,</span> <span class="n">d_model</span><span class="p">,</span> <span class="n">bias</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>

        <span class="c1"># RoPE</span>
        <span class="bp">self</span><span class="o">.</span><span class="n">rope</span> <span class="o">=</span> <span class="n">RotaryPositionEmbedding</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">,</span> <span class="n">max_seq_len</span><span class="p">)</span>

    <span class="k">def</span><span class="w"> </span><span class="nf">forward</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">x</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span><span class="p">,</span> <span class="n">mask</span><span class="p">:</span> <span class="n">torch</span><span class="o">.</span><span class="n">Tensor</span> <span class="o">=</span> <span class="kc">None</span><span class="p">):</span>
<span class="w">        </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd">        Args:</span>
<span class="sd">            x: 输入，shape (batch, seq_len, d_model)</span>
<span class="sd">            mask: 注意力掩码，shape (seq_len, seq_len)</span>
<span class="sd">        &quot;&quot;&quot;</span>
        <span class="n">batch</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">x</span><span class="o">.</span><span class="n">shape</span>

        <span class="c1"># 线性投影</span>
        <span class="n">q</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">q_proj</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">k</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">k_proj</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
        <span class="n">v</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">v_proj</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

        <span class="c1"># 重塑为多头形式</span>
        <span class="n">q</span> <span class="o">=</span> <span class="n">q</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">)</span>
        <span class="n">k</span> <span class="o">=</span> <span class="n">k</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">)</span>
        <span class="n">v</span> <span class="o">=</span> <span class="n">v</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">num_heads</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span><span class="p">)</span>

        <span class="c1"># 应用 RoPE（只对 Q 和 K）</span>
        <span class="n">q</span><span class="p">,</span> <span class="n">k</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">rope</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>

        <span class="c1"># 转置用于矩阵乘法：(batch, num_heads, seq_len, head_dim)</span>
        <span class="n">q</span> <span class="o">=</span> <span class="n">q</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">k</span> <span class="o">=</span> <span class="n">k</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
        <span class="n">v</span> <span class="o">=</span> <span class="n">v</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>

        <span class="c1"># 计算注意力分数</span>
        <span class="n">scale</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">head_dim</span> <span class="o">**</span> <span class="o">-</span><span class="mf">0.5</span>
        <span class="n">attn_scores</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">))</span> <span class="o">*</span> <span class="n">scale</span>

        <span class="k">if</span> <span class="n">mask</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
            <span class="n">attn_scores</span> <span class="o">=</span> <span class="n">attn_scores</span><span class="o">.</span><span class="n">masked_fill</span><span class="p">(</span><span class="n">mask</span> <span class="o">==</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">float</span><span class="p">(</span><span class="s1">&#39;-inf&#39;</span><span class="p">))</span>

        <span class="n">attn_probs</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">softmax</span><span class="p">(</span><span class="n">attn_scores</span><span class="p">,</span> <span class="n">dim</span><span class="o">=-</span><span class="mi">1</span><span class="p">)</span>

        <span class="c1"># 加权求和</span>
        <span class="n">attn_output</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">matmul</span><span class="p">(</span><span class="n">attn_probs</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>

        <span class="c1"># 重塑并输出投影</span>
        <span class="n">attn_output</span> <span class="o">=</span> <span class="n">attn_output</span><span class="o">.</span><span class="n">transpose</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">contiguous</span><span class="p">()</span>
        <span class="n">attn_output</span> <span class="o">=</span> <span class="n">attn_output</span><span class="o">.</span><span class="n">view</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">seq_len</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">d_model</span><span class="p">)</span>
        <span class="n">output</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">o_proj</span><span class="p">(</span><span class="n">attn_output</span><span class="p">)</span>

        <span class="k">return</span> <span class="n">output</span>


<span class="c1"># 测试</span>
<span class="n">mha</span> <span class="o">=</span> <span class="n">MultiHeadAttentionWithRoPE</span><span class="p">(</span><span class="n">d_model</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="n">num_heads</span><span class="o">=</span><span class="mi">8</span><span class="p">)</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">128</span><span class="p">,</span> <span class="mi">512</span><span class="p">)</span>
<span class="n">output</span> <span class="o">=</span> <span class="n">mha</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Input shape: </span><span class="si">{</span><span class="n">x</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Output shape: </span><span class="si">{</span><span class="n">output</span><span class="o">.</span><span class="n">shape</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
</code></pre></div>
</code></pre>
<h2 id="7-参考资料"><a href="#7-参考资料" class="heading-anchor">7. 参考资料</a></h2>
<ol>
<li><a href="https://arxiv.org/abs/2104.09864">RoFormer: Enhanced Transformer with Rotary Position Embedding</a></li>
<li><a href="https://arxiv.org/abs/2302.13971">LLaMA: Open and Efficient Foundation Language Models</a></li>
<li><a href="https://arxiv.org/abs/2309.00071">YaRN: Efficient Context Window Extension of Large Language Models</a></li>
<li><a href="https://arxiv.org/abs/2306.15595">Extending Context Window of Large Language Models via Positional Interpolation</a></li>
<li><a href="https://www.spaces.ac.cn/archives/8265">Transformer升级之路：2、博采众长的旋转式位置编码</a></li>
<li><a href="https://www.zhihu.com/tardis/zm/art/647109286?source_id=1003">十分钟读懂旋转编码（RoPE）</a></li>
<li><a href="https://www.bilibili.com/video/BV1Xi421R7ev/?spm_id_from=333.337.search-card.all.click&amp;vd_source=94e689689fd8909b62da4addd8635282">解密旋转位置编码：数学基础、代码实现与绝对编码一体化探索</a></li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>Keep Looking, Don't Settle：重听乔布斯演讲（25-11-月度小结）</title>
      <link>https://yuanchaofa.com/blog/2025-11-month-summary</link>
      <guid>https://yuanchaofa.com/blog/2025-11-month-summary</guid>
      <source url="https://yuanchaofa.com/rss.xml">Keep Looking, Don't Settle：重听乔布斯演讲（25-11-月度小结）</source>
      <description>重听乔布斯斯坦福演讲，发现真正的主题不是"Stay hungry, Stay foolish"，而是"寻找你的热爱"（You’ve got to find what you love）。So keep looking. Don't settle。</description>
      <category>月度总结</category>
      <pubDate>Sat, 06 Dec 2025 22:52:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="keep-looking-dont-settle"><a href="#keep-looking-dont-settle" class="heading-anchor">Keep Looking, Don't Settle</a></h2>
<p>这个月突然又偶然又听了一遍那场著名的「乔布斯在斯坦福毕业演讲」，最有名的一句话莫过于「Stay hungry. Stay foolish」。</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/1765035529290.webp" alt="‘You’ve got to find what you love,’ Jobs says|388x258" /></p>
<p>看这个 blog 的人应该每个人都听过好多遍，我也一样，但这次听真正击中的我并不是这句话，而是第二个创业故事中提到的「Keep Looking, Don't Settle」。</p>
<p>纯英文演讲我并没有 GET 到多少东西，我又翻了一遍原文，才知道原来我一直理解错了这个演讲的主题，主题并不是告诉我们要【<strong>永远保持学习和探索的热情</strong>，即这句最广为流传的 Stay hungry. Stay foolish】，真正的文章标题是：「<a href="https://news.stanford.edu/stories/2005/06/youve-got-find-love-jobs-says"><strong>You’ve got to find what you love</strong></a>」，原来这篇文章的主题一直都是寻找自己的热爱。</p>
<p>第一次这个演讲应该是初三的时候，那时候觉得很热血很受鼓舞，15 年后的今天重看有完全不一样的感觉，<strong>我觉得好感动</strong>，我觉得说出这样的话真的很让人感动。</p>
<blockquote>
<p>I’m convinced that the only thing that kept me going was that I loved what I did. You’ve got to find what you love—and that is as true for work as it is for your lovers. Your work is going to fill a large part of your life, and the only way to be truly satisfied is to do what you believe is great work. And the only way to do great work is to love what you do.
<br/><br/>
If you haven’t found it yet, keep looking—and don’t settle. As with all matters of the heart, you’ll know when you find it. And like any great relationship, it just gets better and better as the years roll on. So keep looking. Don’t settle.</p>
</blockquote>
<p>我想，尝试过寻找方向的你们一定很懂这里的话有多感人，<strong>So keep looking. Don't settle</strong>.</p>
<h2 id="工作"><a href="#工作" class="heading-anchor">工作</a></h2>
<p>回看了一下 11 月记录的 Flomo，这个月的工作压力依然非常的巨大，这里有很多原因造成的：</p>
<ul>
<li>任务真的太难了（某个非常复杂的生成场景）。尽管已经通过一些方式优化最终完成了交付，但也仅仅是可以使用，真正离达到好用还有很明显的差距。</li>
<li>标准难以定义。可执行和执行好两个完全不同的维度，尤其是这个好很难在任务中很好的定义出来，虽然有隐隐的优化方向，但是前面已经在「可执行」上花费了太多的力气导致「执行好」的进度并没有如我自己想象中的那般快。</li>
<li>项目进展管理一直做的不太好。从今年 2 月份开始一人做一个方向，到后面两三个人一起做，我要负责整个项目的多人进展时，就会明显感觉自己在这方面的能力有所欠缺，自己也会有明显的畏难情绪。整体虽然有进步但是离目标还差太远了。</li>
</ul>
<p>这个月未其实有个我挺喜欢的任务要做，但是真的太耗费我晚上与周末的时间了，有娃之后的我真的顶不住，我周末只想陪娃一会和休息了，不想周末再卷了，想放弃了。难道这又是到了最关键的时候就开始泄气吗？毕竟已经念头通达了。</p>
<h2 id="破产之路"><a href="#破产之路" class="heading-anchor">破产之路</a></h2>
<p>本月出现巨额亏损，核心是因为我重仓了 FIGMA，亏成了麻瓜，我为什么要在阿里起飞之际卖出画成 FIGMA 啊，怎么买不买中概都是亏钱啊。</p>
<p>OK，好吧，我从现在开始，我就定投纳斯达克和标普 500，慢慢彻底不玩个股了，这样的我五年后还会亏钱吗？Looking my eyes, answer me!!!!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Kimi-K2 和 Kimi-K2-Thinking 深度解读：从预训练优化到 Agentic 能力训练的完整流程（含MuonClip优化、Agentic 数据合成等）</title>
      <link>https://yuanchaofa.com/post/kimi-k2-and-kimi-k2-thinking-notes</link>
      <guid>https://yuanchaofa.com/post/kimi-k2-and-kimi-k2-thinking-notes</guid>
      <source url="https://yuanchaofa.com/rss.xml">Kimi-K2 和 Kimi-K2-Thinking 深度解读：从预训练优化到 Agentic 能力训练的完整流程（含MuonClip优化、Agentic 数据合成等）</source>
      <description>深度解读 Kimi K2 和 K2 Thinking 技术细节：MuonClip 优化方案、大规模 Agentic 数据合成 pipeline、通用强化学习的 Self-Judging 机制，以及 200-300 步工具调用的 Test-Time Scaling。从预训练到后训练，揭秘月之暗面如何打造 SOTA 开源 Thinking 模型。</description>
      <category>paper-reading</category>
      <pubDate>Sun, 09 Nov 2025 17:26:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="0-核心启发我们能学到什么"><a href="#0-核心启发我们能学到什么" class="heading-anchor">0. 核心启发：我们能学到什么？</a></h2>
<p>从 K2 和 K2 Thinking 的技术报告中，我觉得比较重要的，可能能在我们实际业务中用上的点有：</p>
<ol>
<li>数据的改写策略，写的很详细，尤其是在做「创意写作」方面工作的同学。</li>
<li>Agentic 训练数据构建的 pipeline。别扯没用的，就是要「真实环境模拟运行」获取大量的 trajectory 然后用 LLM 做筛选。（以前我只想着去构造，迭代去筛选更有效）</li>
<li>rubric-based 评估（不过这个其实一两年前大家就在用了，为什么突然又改头换面火了一下，这个真的太考验业务敏感度和怎么使用了，能直接在 k2 这种级别的开源模型上搞出来，还是挺佩服的）</li>
<li>test-time scaling 还是很有必要的，梦回年初 Long-CoT，想要效果好牺牲点时间绝对是值得的。（尽管可能会导致过度生成、倾向于用工具的问题）</li>
</ol>
<blockquote>
<p>个人碎碎念：</p>
<ol>
<li>
<p>从 K1.5 发 paper 开始，就感觉 KIMI 突然开始醒悟做社区了，OpenSource 真的是比较博好感，现在中国的开源模型真的牛皮🐂🍺</p>
</li>
<li>
<p>好像 <code>claude</code> 在 coding 上的爆火让大家都领悟到了 <code>agentic</code> 能力的重要性。希望把 Claude 价格打下来，加油～</p>
</li>
</ol>
</blockquote>
<h2 id="1-背景"><a href="#1-背景" class="heading-anchor">1. 背景</a></h2>
<p>月之暗面发布的 <strong><a href="https://moonshotai.github.io/Kimi-K2/thinking.html">Kimi K2 Thinking</a></strong>，在 Humanity's Last Exam (HLE) 上达到了 44.9% 的成绩，在多个基准测试中表现优异，不过榜单简单看一眼即可；让我比较惊喜的是，K2 Thinking 可以执行 200-300 步连续的工具调用，有类似于 <code>claude</code> 一样的长程规划和自适应推理能力。</p>
<p>但是，K2 Thinking 的官方 blog 只展示了 benchmark 数据和 demo，并没有透露具体的技术细节。作为一个大模型从业者，看到 Twitter/知乎大家都在聊这个模型，所以我就比较好奇「模型的训练方法」以及「给我们工作学习中的启发」。</p>
<p>好在今年早些时候发布了 <strong>Kimi K2</strong> 的完整<a href="https://arxiv.org/abs/2507.20534">技术报告</a>和 <a href="https://moonshotai.github.io/Kimi-K2/">技术 blog</a>。而 <strong>K2 Thinking 和 K2 师出同源</strong>，只是在 K2 的基础上增加了 thinking 能力，更强的工具调用能力，通过 test-time scaling 实现一个更强的 Thinking Agent。因此，通过深入研究 K2 的技术细节，我们就能理解 K2 Thinking 是如何炼成的。</p>
<p>我是朝发（CHAOFA）这篇文章会从 K2 的技术报告出发，结合 K2 Thinking 的特点，了解这个 SOTA 开源 thinking 模型是怎么训出来的。<strong>核心关注三个问题</strong>：</p>
<ol>
<li><strong>预训练阶段</strong>：如何用 MuonClip 优化器实现更高的 token 效率？</li>
<li><strong>后训练阶段</strong>：如何通过大规模 Agentic 数据合成和通用强化学习，让模型学会使用工具？</li>
<li><strong>Test-Time Scaling</strong>：如何让模型在推理时进行长程思考和工具调用？</li>
</ol>
<blockquote>
<p>历史上此比较相关文章：</p>
<ul>
<li><a href="https://yuanchaofa.com/post/kimi-k1.5-paper-reading-notes.html">深度解读 Kimi-K1.5，真正了解 RL 数据是怎么筛选的</a></li>
<li><a href="https://yuanchaofa.com/post/deepseek-r1-paper-reading-notes.html">自顶向下方式深度解读 DeepSeek-R1，内含大量细节</a></li>
<li><a href="https://yuanchaofa.com/post/slow-fast-thinking-from-qwen3-thinking-mixed-to-adacot-to-adathinking.html">自适应快慢思考推理模型（Adaptive Reasoning Model）：Qwen3混合思考-&gt;字节AdaCoT-&gt;清华AdaptThinking</a></li>
</ul>
</blockquote>
<blockquote>
<p>如果不喜欢看文字可以看视频解读，<a href="https://www.bilibili.com/video/BV1yikRBvEwy/">B 站-chaofa用代码打点酱油</a>和 <a href="https://www.youtube.com/@bbruceyuan">YouTube</a></p>
<p><a href="https://www.bilibili.com/video/BV1yikRBvEwy/">算法视角深度解读 Kimi K2 和 K2 Thinking，从预训练优化到 Agentic 能力训练的完整流程（含MuonClip优化、Agentic 数据 --bilibili</a></p>
</blockquote>
<h2 id="2-整体架构从-k2-到-k2-thinking"><a href="#2-整体架构从-k2-到-k2-thinking" class="heading-anchor">2. 整体架构：从 K2 到 K2 Thinking</a></h2>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/20251109144342.png" alt="image.png|700x366" /></p>
<blockquote>
<p>Archiecture from: <a href="https://x.com/rasbt/status/1986511951141441648?s=20">Sebastian Raschka</a></p>
</blockquote>
<p>先来看一下上面的整体结构图，然后在深入技术细节之前，我们有必要先理解 K2 和 K2 Thinking 的关系。</p>
<h3 id="21-k2open-agentic-intelligence-的基座"><a href="#21-k2open-agentic-intelligence-的基座" class="heading-anchor">2.1 K2：Open Agentic Intelligence 的基座</a></h3>
<p>Kimi K2 是一个 <a href="https://yuanchaofa.com/llms-zero-to-hero/the-way-of-moe-model-evolution.html">MoE (Mixture-of-Experts)</a> 模型，拥有 <strong>32B 激活参数和 1T 总参数</strong>。它在非 thinking 模型中，在前沿知识、数学和编码任务上达到了 SOTA 性能。</p>
<p>K2 的核心特点是<strong>有比较强的 Agentic 能力</strong>。什么是 Agentic 任务？就是模型不仅要回答问题，还要主动使用工具、执行操作、完成复杂的多步骤任务。比如：</p>
<ul>
<li>用 Python 分析数据、生成可视化网页</li>
<li>在命令行中编辑文件、运行命令</li>
<li>通过搜索和浏览器收集信息、验证假设、构建答案</li>
</ul>
<p>K2 发布了两个版本：</p>
<ul>
<li>Kimi-K2-Base：基础模型，适合研究者/开发者/企业用户进行微调</li>
<li>Kimi-K2-Instruct：后训练模型，适合直接使用，是一个非推理模式（Non-Reasoning Model）</li>
</ul>
<h3 id="22-k2-thinking加入-test-time-scaling"><a href="#22-k2-thinking加入-test-time-scaling" class="heading-anchor">2.2 K2 Thinking：加入 Test-Time Scaling</a></h3>
<p>Kimi K2 Thinking 是在 K2 的基础上，通过额外的训练，让模型具备了 thinking 能力。它的核心特点是：</p>
<ol>
<li>边思考边使用工具：模型在推理过程中，会进行 <code>think → search → browse → think → code</code> 的循环，动态生成和验证假设</li>
<li>长程推理：可以执行 200-300 步连续的工具调用，保持推理的连贯性。（这点是让人比较惊喜的）</li>
<li>Test-Time Scaling：通过增加推理时的 thinking tokens 和工具调用步数，提升模型性能</li>
</ol>
<p>从架构上看，<code>K2 Thinking = K2 + Thinking Ability + Test-Time Scaling</code>。因此，<strong>理解 K2 的训练方法，就能理解 K2 Thinking 的 80%</strong>。</p>
<p>下面我们按照训练流程，依次讲解预训练、后训练和 test-time scaling 的关键技术。</p>
<h2 id="3-预训练"><a href="#3-预训练" class="heading-anchor">3. 预训练</a></h2>
<h3 id="31-基于-muonclip-优化器的-token-效率优化"><a href="#31-基于-muonclip-优化器的-token-效率优化" class="heading-anchor">3.1 基于 MuonClip 优化器的 Token 效率优化</a></h3>
<p>预训练是 Agentic Intelligence 的关键基础，它建立了让强化学习探索变得可行、高效和可泛化的先验知识。但是，正如 Ilya Sutskever 所说，数据是有限的&quot;化石燃料&quot;，其增长速度远远落后于算力的增长。这使得<strong>预训练阶段的 token 利用效率</strong>成为 AI scaling laws 中的新关键系数。</p>
<h4 id="311-为什么需要更好的优化器"><a href="#311-为什么需要更好的优化器" class="heading-anchor">3.1.1 为什么需要更好的优化器？</a></h4>
<p>给定一个大致有限的预训练数据集和固定的模型配置，更 token 高效的优化器能产生更多的智能。Moonshot 之前的工作 <a href="https://github.com/MoonshotAI/Moonlight">Moonlight</a> 已经证明，<a href="https://kellerjordan.github.io/posts/muon/">Muon</a> 优化器在 LLM 训练中显著优于广泛使用的 AdamW 优化器，即&quot;相同配置训练下有更低的 loss&quot;。</p>
<p>K2 的设计目标是进一步扩展 Moonlight，它采用了类似 DeepSeek-V3 的架构。基于 scaling-law 分析，他们做了两点改进（看图更清晰）：</p>
<ul>
<li>减少了 attention heads 的数量，以提高长上下文效率。</li>
<li>增加了 MoE 的稀疏性，以获得更高的 token 效率</li>
</ul>
<blockquote>
<p>原文这么写的：Based on scaling-law analysis, we reduce the number of heads for long-context efficiency, and increase MoE sparsity for greater token efficiency。</p>
</blockquote>
<p>但在扩展过程中，他们遇到了一个持续的挑战：<strong>由 attention logits 爆炸引起的训练不稳定</strong>。这个问题在使用 Muon 时更频繁，而在 AdamW 中较少。现有的解决方案（如 Qwen3 用的 query-key normalization）都不够充分（防止数值溢出）。</p>
<h4 id="312-muonclip直接控制-attention-logits"><a href="#312-muonclip直接控制-attention-logits" class="heading-anchor">3.1.2 MuonClip：直接控制 Attention Logits</a></h4>
<p>为了解决这个问题，kimi 提出了 MuonClip 优化器，它通过 <strong>qk-clip 技术</strong>改进了 Muon。</p>
<p><strong>核心思想</strong>：qk-clip 通过在 Muon 更新后<strong>直接重新缩放 query 和 key 投影的权重矩阵</strong>，从源头控制 attention logits 的规模，从而稳定训练。（注意：这里是更新完之后，所以不会改变这一次更新的 forward/backward 操作，影响的是下一步）。</p>
<p>具体来说，query 和 key 投影按如下方式缩放：</p>
<div class="katex-block">$$q_i = \eta^{\alpha} W_q x_i$$</div>
<div class="katex-block">$$k_i = \eta^{1-\alpha} W_k x_i$$</div>
<p>其中 <span class="katex-inline">$\alpha$</span> 是一个平衡超参数，因此 attention logit 变为：</p>
<div class="katex-block">$$(\eta^{\alpha} q_i)^\top (\eta^{1-\alpha} k_j) = \eta\, q_i^\top k_j$$</div>
<p>自适应因子 <span class="katex-inline">$\eta$</span>（阈值为 <span class="katex-inline">$t$</span>）在每一步之后根据该步的最大 attention logit 设置：</p>
<div class="katex-block">$$\eta = \min\left(\frac{t}{\displaystyle\max_{i,j}\bigl(q_i^\top k_j\bigr)}, 1\right)$$</div>
<p>其中 <span class="katex-inline">$t$</span> 是预设的阈值。这是一个通用技术，可能适用于其他稳定化场景。这里其实还有一些其他的细节，比如 每个 head 有不同的 <span class="katex-inline">$\eta$</span>。</p>
<h4 id="313-实验结果零训练尖峰"><a href="#313-实验结果零训练尖峰" class="heading-anchor">3.1.3 实验结果：零训练尖峰</a></h4>
<p>实验表明，MuonClip 有效地防止了 logit 爆炸，同时保持了下游任务性能。在实践中，K2 使用 MuonClip 在 15.5T tokens 上进行预训练，实现了零训练尖峰（zero loss spike），证明了 MuonClip 是大规模 LLM 训练的稳健解决方案。</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/20251109161151.png" alt="image.png|700x420" /></p>
<p>从 loss 曲线可以看出，MuonClip 的训练过程非常平滑，没有出现任何不稳定的情况。这为后续的 Agentic 能力训练打下了坚实的基础。</p>
<p><strong>小结</strong>：MuonClip 优化器通过 qk-clip 技术，在保持 Muon 高 token 效率的同时，解决了训练不稳定问题，使得在同等条件下获得比 AdamW 更低的 loss，使得 K2 能够在有限的数据上训练出更强的基础模型。</p>
<h3 id="32-文本的改写优化"><a href="#32-文本的改写优化" class="heading-anchor">3.2 文本的改写优化</a></h3>
<p>K2 相比 K1.5 的一个关键进步是引入了<strong>合成数据生成策略</strong>来提高 token 利用率。核心思想是：通过精心设计的改写 pipeline，在不引入显著过拟合的情况下，扩大高质量 tokens 的数量。改写（Rephrasing） 就是数据合成的一种方式，主要是为了提高「高质量数据的占比」，尤其是「知识领域」和「数学领域」。：</p>
<h4 id="321-知识领域数据改写"><a href="#321-知识领域数据改写" class="heading-anchor">3.2.1 知识领域数据改写</a></h4>
<p>在知识密集型文本上进行预训练面临一个权衡：单次 epoch 不足以全面吸收知识，而多次 epoch 重复会导致收益递减并增加过拟合风险。为了提高高质量知识 tokens 的利用率，K2 提出了一个合成改写框架，每个语料库最多改写两次，包含三个关键组件：</p>
<p><strong>A. 风格和视角多样化的提示（Style- and perspective-diverse prompting）</strong></p>
<p>通过精心设计的 prompts，引导大语言模型以不同的风格和视角生成原文的忠实改写。这样做的好处是：</p>
<ul>
<li>增强语言多样性</li>
<li>保持事实完整性</li>
<li>避免简单的同义词替换</li>
</ul>
<p><strong>B. 分块自回归生成（Chunk-wise autoregressive generation）</strong></p>
<p>为了在长文档中保持全局连贯性并避免信息丢失，采用基于分块的自回归改写策略，一图胜千言：</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/20251109163028.png" alt="image.png|700x344" /></p>
<p><strong>C. 保真度验证（Fidelity verification）</strong></p>
<p>为了确保原文和改写内容之间的一致性，进行保真度检查，比较每个改写段落与其源文本的语义对齐。这是训练前的初步质量控制步骤。</p>
<h4 id="322-数学领域数据改写"><a href="#322-数学领域数据改写" class="heading-anchor">3.2.2 数学领域数据改写</a></h4>
<p>为了增强数学推理能力，K2 采用了两种策略：</p>
<p><strong>A. &quot;学习笔记&quot;风格改写</strong></p>
<p>将高质量的数学文档改写成&quot;学习笔记&quot;风格，遵循 <a href="https://arxiv.org/abs/2501.01926">SwallowMath</a> 中引入的方法。这种风格更接近人类学习数学的方式，包含：</p>
<ul>
<li>逐步推导过程</li>
<li>关键概念解释</li>
<li>示例和练习</li>
</ul>
<p><strong>B. 多语言翻译</strong></p>
<p>将其他语言的高质量数学材料翻译成英语，以增加数据多样性。这样可以：</p>
<ul>
<li>利用非英语世界的优质数学资源</li>
<li>增加数学表达的多样性</li>
<li>扩大训练数据规模</li>
</ul>
<p><strong>小结</strong>：通过针对知识和数学领域的专门改写技术，K2 在不显著增加过拟合风险的情况下，大幅提高了高质量 tokens 的利用率。这种受控的数据增强策略是 K2 预训练成功的关键因素之一。</p>
<h2 id="4-后训练重点"><a href="#4-后训练重点" class="heading-anchor">4. 后训练(重点)</a></h2>
<p>K2 的增强 Agentic 能力源于两个重要方面：</p>
<ul>
<li><strong>大规模 Agentic 数据合成</strong></li>
<li><strong>通用强化学习</strong></li>
</ul>
<h3 id="41-大规模-agentic-数据合成教会模型使用工具"><a href="#41-大规模-agentic-数据合成教会模型使用工具" class="heading-anchor">4.1 大规模 Agentic 数据合成：教会模型使用工具</a></h3>
<p>为了教会模型复杂的工具使用能力，kimi 是基于<strong>大规模模拟真实世界的工具使用场景</strong>，构建了数据 pipeline。</p>
<h4 id="411-数据合成流程"><a href="#411-数据合成流程" class="heading-anchor">4.1.1 数据合成流程</a></h4>
<p>这个管道的核心思想是：<strong>系统地演化数百个领域，包含数千个工具</strong>（包括真实的 MCP 工具和合成工具），然后生成数百个具有不同工具集的 agents。</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/20251109175053.png" alt="image.png|700x232" /></p>
<p>辅助看这个图：</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/20251109163203.png" alt="image.png|700x233" /></p>
<p>具体流程如下：</p>
<ol>
<li>定义领域和工具：涵盖各种真实场景，如数据分析、网页开发、系统管理等</li>
<li>生成任务：所有任务都是基于 rubric 的（有明确的评分标准），确保一致的评估</li>
<li>模拟交互：Agents 与模拟环境和用户 agents 交互，创建真实的多轮工具使用场景</li>
<li>LLM 评判(LLM as judge)：根据任务 rubrics 评估模拟结果，过滤出高质量的训练数据</li>
</ol>
<p>这个可扩展的 pipeline 生成了<strong>多样化、高质量的数据</strong>，为大规模拒绝采样和强化学习铺平了道路。</p>
<h4 id="412-为什么这个方法有效"><a href="#412-为什么这个方法有效" class="heading-anchor">4.1.2 为什么这个方法有效？</a></h4>
<p>传统的工具使用训练依赖于人工标注的数据，成本高、规模小、多样性有限。而 k2 的方法通过<strong>自动化合成</strong>，可以：</p>
<ul>
<li>无限扩展：只要定义新的领域和工具，就能生成新的训练数据</li>
<li>保证质量：通过 rubric-based 评估和 LLM judge，确保数据质量</li>
<li>覆盖长尾场景：可以模拟各种罕见但重要的工具使用场景</li>
</ul>
<h3 id="42-通用强化学习不可验证奖励"><a href="#42-通用强化学习不可验证奖励" class="heading-anchor">4.2 通用强化学习：不可验证奖励</a></h3>
<p>传统的强化学习主要应用于<strong>可验证奖励</strong>的任务，比如数学题（答案对错明确）和竞赛编程（能否通过测试用例）。但对于<strong>不可验证奖励</strong>的任务（如写研究报告、创意写作），传统 RL 就无能为力了。</p>
<blockquote>
<p>是不是突然想起了 <a href="https://yuanchaofa.com/post/deepseek-grm-paper-reading-notes.html">DeepSeek-GRM（通用奖励模型）</a>。</p>
</blockquote>
<h4 id="421-self-judging-机制"><a href="#421-self-judging-机制" class="heading-anchor">4.2.1 Self-Judging 机制</a></h4>
<p>核心思想是：<strong>模型作为自己的评判者</strong>，为不可验证的任务提供可扩展的、基于 rubric 的反馈。</p>
<p>具体做法：</p>
<ol>
<li>对于不可验证的任务，模型生成多个候选答案</li>
<li>模型自己根据 rubric 评估这些答案，给出分数</li>
<li>使用这些分数作为奖励信号，进行强化学习</li>
</ol>
<blockquote>
<p>但这里有个问题：模型的自我评估准确吗？这不还是 LLM as Judge 那一套吗？</p>
</blockquote>
<h4 id="422-用可验证奖励改进-critic"><a href="#422-用可验证奖励改进-critic" class="heading-anchor">4.2.2 用可验证奖励改进 Critic</a></h4>
<p>kimi 的解决方案是：<strong>在可验证奖励的 on-policy rollouts 中，持续更新 critic</strong>，使 critic 在最新策略上不断提高评估准确性。</p>
<p>这可以看作是<strong>用可验证奖励来改进不可验证奖励的估计</strong>。通过这种方式，模型的自我评估能力会随着训练不断提升，从而支持更广泛的任务。</p>
<p><strong>小结</strong>：通过大规模 Agentic 数据合成和通用强化学习，K2 学会了在各种场景下使用工具，并且能够处理可验证和不可验证的任务。这为 K2 Thinking 的长程推理能力打下了基础。</p>
<h2 id="5-k2-thinking"><a href="#5-k2-thinking" class="heading-anchor">5. K2 Thinking</a></h2>
<p>K2 Thinking 在 K2 的基础上，增加了 <strong>thinking 能力</strong>、<strong>更强的工具调用能力</strong>和 <strong>test-time scaling</strong>。这使得模型能够在推理时进行长程思考和工具调用，从而解决更复杂的问题。</p>
<h3 id="51-什么是-test-time-scaling"><a href="#51-什么是-test-time-scaling" class="heading-anchor">5.1 什么是 Test-Time Scaling？</a></h3>
<p>Test-Time Scaling 是指<strong>在推理时增加计算量，以提升模型性能</strong>。对于 K2 Thinking，这体现在两个方面：</p>
<ol>
<li>增加 thinking tokens：模型在生成答案前，会先生成大量的思考过程（类似 OpenAI o1，这其实就是 Long-CoT，这种技术在 Kimi-k1.5 就已经开始做了）</li>
<li>增加工具调用步数：模型可以执行 200-300 步连续的工具调用，进行长程规划（这是新增的，为了 Agentic 能力的提升）</li>
</ol>
<p>这两者结合，使得 K2 Thinking 能够解决需要深度推理和多步操作的复杂问题。</p>
<h3 id="52-边思考边使用工具interleaved-reasoning"><a href="#52-边思考边使用工具interleaved-reasoning" class="heading-anchor">5.2 边思考边使用工具：Interleaved Reasoning</a></h3>
<p>K2 Thinking 的核心能力是<strong>边思考边使用工具</strong>。它会进行动态的 <code>think → search → browse → think → code</code> 循环，这个循环可以重复数百次，直到找到答案：</p>
<ol>
<li>Think：分析问题，生成假设</li>
<li>Search：搜索相关信息</li>
<li>Browse：浏览网页，提取关键信息</li>
<li>Think：验证假设，调整策略</li>
<li>Code：编写代码，执行计算</li>
</ol>
<h3 id="53-简要看看-benchmark"><a href="#53-简要看看-benchmark" class="heading-anchor">5.3 简要看看 benchmark</a></h3>
<h4 id="531-agentic-search超越人类基线"><a href="#531-agentic-search超越人类基线" class="heading-anchor">5.3.1 Agentic Search：超越人类基线</a></h4>
<p>在 BrowseComp benchmark 上，K2 Thinking 达到了 <strong>60.2%</strong> 的成绩，显著超越了 <strong>29.2%</strong> 的人类基线。</p>
<p>BrowseComp 是一个挺具有挑战性的 benchmark，旨在评估模型<strong>持续浏览、搜索和推理难以找到的真实世界网络信息</strong>的能力。</p>
<h4 id="532-agentic-coding构建完整的应用"><a href="#532-agentic-coding构建完整的应用" class="heading-anchor">5.3.2 Agentic Coding：构建完整的应用</a></h4>
<p>K2 Thinking 在编码任务上也表现出色：</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2025/20251109164955.png" alt="image.png|700x469" /></p>
<p>从官网看，K2 Thinking 可以<strong>从单个 prompt 构建完整的应用</strong>，包括：</p>
<ul>
<li>组件密集的网站</li>
<li>Word 克隆应用</li>
<li>交互式数据分析工具</li>
</ul>
<h3 id="54-小结"><a href="#54-小结" class="heading-anchor">5.4 小结</a></h3>
<p>通过 test-time scaling，K2 Thinking 能够在推理时进行长程思考和工具调用，从而解决需要深度推理和多步操作的复杂问题。这使得它在 Agentic Reasoning、Agentic Search 和 Agentic Coding 任务上都达到了 SOTA 性能。（有点 claude 那味道了）</p>
<h2 id="6-技术细节对比k2-vs-k2-thinking"><a href="#6-技术细节对比k2-vs-k2-thinking" class="heading-anchor">6. 技术细节对比：K2 vs K2 Thinking</a></h2>
<p>让我们总结一下 K2 和 K2 Thinking 的关键区别：</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>K2 (Instruct)</th>
<th>K2 Thinking</th>
</tr>
</thead>
<tbody>
<tr>
<td>模型类型</td>
<td>Non-thinking（无长思考）</td>
<td>Thinking model（有长思考）</td>
</tr>
<tr>
<td>推理方式</td>
<td>直接生成答案</td>
<td>边思考边使用工具</td>
</tr>
<tr>
<td>工具调用</td>
<td>支持，但步数有限（其实也挺好的）</td>
<td>200-300 步连续调用</td>
</tr>
<tr>
<td>Test-Time Scaling</td>
<td>不支持</td>
<td>支持（thinking tokens + 工具调用）</td>
</tr>
<tr>
<td>适用场景</td>
<td>通用对话、快速响应</td>
<td>复杂推理、长程规划</td>
</tr>
</tbody>
</table>
<p>从技术角度看，K2 Thinking 是在 K2 的基础上：</p>
<ul>
<li>增加了 thinking 能力：通过额外的训练，让模型学会生成长思考过程</li>
<li>优化了工具调用：支持更长的工具调用链，保持推理连贯性</li>
<li>引入了 test-time scaling：在推理时增加计算量，提升性能</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
