<?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>Sun, 04 Oct 2026 07:26:06 GMT</pubDate>
    <lastBuildDate>Sun, 04 Oct 2026 07:26:06 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>AI 狂飙，怎么才能 10 倍 or 100 倍自己的工作效率（2026-09-月度小结）</title>
      <link>https://yuanchaofa.com/blog/2026-09-month-summary</link>
      <guid>https://yuanchaofa.com/blog/2026-09-month-summary</guid>
      <source url="https://yuanchaofa.com/rss.xml">AI 狂飙，怎么才能 10 倍 or 100 倍自己的工作效率（2026-09-月度小结）</source>
      <description>认真思考如何利用 Agent 把自己的工作效率提升到 10 倍甚至 100 倍，也尝试用 Multica 和 Hermes Agent 建立属于自己的工作流。同时，因为 B 站计税方式变化，我重新思考视频商业化，并决定放下数据焦虑，继续做让自己开心的视频。</description>
      <category>月度总结</category>
      <pubDate>Sun, 04 Oct 2026 15:00:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="起于播客"><a href="#起于播客" class="heading-anchor">起于播客</a></h2>
<p>每个月都会听大量的播客，这个月感觉比较喜欢的播客是脉脉 CEO 林凡的访谈，<a href="https://www.xiaoyuzhoufm.com/episode/6a9daa7ea0210c197dce7a8b">《86.「明年科技公司就只招 AI 人才了！」和脉脉CEO林凡聊全球 AI 人才动向、组织变革与大模型季报》</a>。</p>
<p>我使用 AI 一般只能同时做一两件事情，完全没有办法很好地利用现在 Agent 强大的能力，效率相较于以前有 3 倍的样子，但产出应该没有 2 倍。</p>
<p>而我看到大量的大佬用 AI Agent 做非常多的事情，效率提升可能有 10 倍 / 100 倍，我非常的羡慕。</p>
<p>播客中提到的是：“普通任务 Agent 通常 20-30 分钟即可完成，将目标翻倍至 1 小时，可倒逼用户设计完整的目标与验证闭环。”</p>
<p>作为一个大模型算法工程师，我自然也知道这个原则，但我一直没有去践行。</p>
<p>因此这个月我一直在想，怎么才能 100 倍自己的工作效率呢？</p>
<blockquote>
<p>我非常期待读者朋友们讲一下你们的方法～</p>
</blockquote>
<h2 id="终于-multica"><a href="#终于-multica" class="heading-anchor">终于 Multica</a></h2>
<p>工作毫无疑问是占据我最多时间的内容，但我们这里暂且不聊怎么提升工作效率<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>。我们聊怎么提升自己工作之外的效率。既然工作占据我们绝大多数时间，那么我们就一定要利用好自己的 Agent，让其在“我们上班的时候，Agent 为我们工作”。这样，“我们作为打工人为老板工作”，“Agent 作为打工人为我们工作”。</p>
<p>所以我们要按照老板一样的思维去安排工作，而不是作为一个工程师。绝大多数时候，我们不应该关心过程，我们关心结果即可。</p>
<p>所以我们需要有一个容器，可以安排任务、验收任务。没错，我们就是需要一个 for Agent 的 Linear。刚好有一个开源软件，叫做 <a href="https://github.com/multica-ai/multica">Multica</a>，基本符合我的需求。它可以让我早上、晚上的时候给 Agent 布置任务，然后晚上我去验收，上班的时间 Agent 就正常运行。简单理解就是一个看板，很适合一个小任务。</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2026/1791043688027.webp" alt="Multica 任务看板" /></p>
<h2 id="形成工作流"><a href="#形成工作流" class="heading-anchor">形成工作流</a></h2>
<p>但是我觉得 Multica 并不够了解我，且自己去写任务也费劲，所以创建任务、管理 Issue 我都是通过我的个人助手——Hermes Agent 处理。通过一个有记忆的个人助手创建任务<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>、分配任务，而我只需要通过语音输入讲清楚我的诉求，然后 Hermes 可以自动派发和执行，最后晚上我去验收结果以及给出反馈即可。</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2026/hermes-issue-review-workflow-gpt-image2.png" alt="Hermes Issue Review 工作流" /></p>
<p>通过这样的方式，我的一些想法落地比以前快了很多，比如更新迭代了 <a href="https://github.com/ApeCodeAI/thinktape">ThinkTape</a>、LangBuddy、<a href="https://github.com/ApeCodeAI/finsight">FinSight</a> 等个人的小 GitHub 项目，自我感觉良好（对，有产生了一堆自己用的垃圾，所以我只敢放到 ApeCodeAI 下面： https://github.com/ApeCodeAI 🤣）。</p>
<p>因为这一套模式的启动，我又接了一个<a href="https://www.bilibili.com/video/BV1Euaa6uE9V/">商业化视频广告</a>，也通过这个模式很好地完成了交付。但这个月由于 B 站修改计税方式，引发了另外一个“关于商业化思考”的事情，就是另外一个故事了<sup class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup>。不过这种苦闷，我不可能埋藏在心里，一定会找一天把这件事情写下来的。</p>
<p>某种程度上可以说，我做视频两年，因为计税方式的改变，我可能不再是为爱发电了，算起来有可能开始倒贴了……等于花了大量的时间扯皮，最后钱没挣到，全给 B 站贡献素材和给国家纳税了（纳税光荣，我举双手赞同合法纳税）。</p>
<h2 id="放飞自我"><a href="#放飞自我" class="heading-anchor">放飞自我</a></h2>
<p>因为上面这个事情的存在，导致我以前关于 B 站商业化的思考毫无意义。当然这是另一个故事<sup class="footnote-ref"><a href="#fn3" id="fnref3:1">[3:1]</a></sup>，我们下期再来讲。未来收入也应该可以完全公开了，Maybe build in public 也还可以～</p>
<p>赚不到钱就准备“放飞自我”了～</p>
<p>未来创作应该不会再有数据焦虑了，因为反正也赚不到钱，那为什么不回到最开始做视频的状态<sup class="footnote-ref"><a href="#fn4" id="fnref4">[4]</a></sup>，自己开心最重要呢？</p>
<p>24 年那个国庆假期，我也很怀念啊～</p>
<p><img loading="lazy" src="https://cfcdn.yuanchaofa.com/blog/2026/20261004005338.png" alt="2024 年国庆假期" /></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>这个月，刚好我也完成了新工作的 3 个月答辩，等我完成 6 个月答辩之后，可以找时间总结一下工作中的提效思路和方式。关于我工作的变动可以看：<a href="https://yuanchaofa.com/blog/2026-05-month-summary">《从打点酱油到袁朝发》</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>26 年 9 月有种让我梦回今年 2 月龙虾大热的时候，似乎所有人又都在谈论 personal agent：Meta 的 Muse、Instinct AI、Grok Bot、Manus 的 Cue、OpenAI 的 Dot，还有国内的 Today AI 等，真的是疯狂。 <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p>这主要是一些新的商业化思考和未来的创作模式，完全不再是数据驱动的、for 广告主的视频创作，而应该是 for 个人/专业学习者的。 <a href="#fnref3" class="footnote-backref">↩︎</a> <a href="#fnref3:1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>看了眼，2024 年 10 月份，我是真的热爱啊，怎么可能一个月做到更新这么多，我周末都不用带娃的吗？（嗷，原来那个时候我没娃） <a href="#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    <item>
      <title>从打点酱油到袁朝发（2026-05-月度小结）</title>
      <link>https://yuanchaofa.com/blog/2026-05-month-summary</link>
      <guid>https://yuanchaofa.com/blog/2026-05-month-summary</guid>
      <source url="https://yuanchaofa.com/rss.xml">从打点酱油到袁朝发（2026-05-月度小结）</source>
      <description>不写月度总结会死吗？不会，但我知道再不写，我想说的话只会越攒越多，直到我自己都忘记了。以后发的文章、视频，署名都是"袁朝发"了</description>
      <category>月度总结</category>
      <pubDate>Mon, 01 Jun 2026 13:20:00 GMT</pubDate>
      <content:encoded><![CDATA[
<h2 id="7-个月断更"><a href="#7-个月断更" class="heading-anchor">7 个月断更</a></h2>
<p>上一次更新的月度总结是 25 年11月，那时候压力很大，在疯狂地听乔布斯演讲，不知觉间把 &quot;Keep Looking, Don't Settle&quot; 作为我的飞书签名半年多了。</p>
<p><a href="https://yuanchaofa.com/blog/2025-11-month-summary">嘴上说着 Dont Settle</a>，然后却 Settle 了 7 个月。不是没有东西写，这七个月发生的事情以往任何时候都要多，无论是 LLM/Agent 技术的发展，还是生活工作中的变化。但越是这样，越想找一个关键的时间节点来重新启动。</p>
<p>直到今天在听<a href="https://www.xiaoyuzhoufm.com/episode/6a13c547e59ebca9363c10e3">吴晓波最新的播客——《Vol.217 两代财经作家的对话：写作与财富，诱惑与尊严，技术的冲击和价值观的剧变》</a>，我一边听一边笑🤣，脑中突然浮现了吴晓波频道的第一篇公众号《骑在新世界的背上》，我翻看了一下，里面吴晓波说：&quot;不做会死吗？不会。但天变得比想象得快&quot;。</p>
<p>对于我：不写月度总结会死吗？不会，但我知道再不写，我想说的话只会越攒越多，直到我自己都忘记了。</p>
<h2 id="改名"><a href="#改名" class="heading-anchor">改名</a></h2>
<p>我决定全平台改名了。B站、YouTube、公众号、博客，都从&quot;chaofa用代码打点酱油&quot;改成&quot;袁朝发&quot;。</p>
<p>&quot;chaofa用代码打点酱油&quot;不够好，它给我暗含了一种我本来就是随便做做的心态，但我实际上不是想随便做做。</p>
<ul>
<li>4 月份，我和 OpenClaw 聊要不要改名，结论是&quot;暂不改名；名字不是你不更新的瓶颈&quot;。</li>
<li>5 月份又想，来来回回好多次；连 AI 都开始说：&quot;你至少问了 3 次&quot;，让我都不太好意思和我的 AI 继续聊下去。</li>
</ul>
<p>对我来说：&quot;chaofa用代码打点酱油&quot; 不仅是一个名字，是一个跟了我一年多的身份，全网 5W 多朋友认识的是这个名字。改了之后，就是把大家熟悉的一个东西扔掉，我真的舍不得<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>。</p>
<p>现在无论是谁，都能用 Claude/GPT 写一个 80 分的东西，那么别人为什么还要看&quot;chaofa用代码打点酱油&quot;写的文章，录的视频？所以我想，我要为我自己的内容负责，我不会让 AI 接管我的思考；用真名，意味着我写的文章/录的视频是经过我的理解和过滤的，读者/观众看到的是&quot;一线互联网大厂&quot;的实践经验<sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup>。</p>
<p>最后触发决定的是写书。当我真正开始写书的样章的时候，我会觉得在书的封面印着的应该是&quot;袁朝发&quot;，而不是&quot;chaofa 用代码打点酱油&quot;，与其等书出了之后再改，不如现在就改。</p>
<p>所以从今天以后，请叫我&quot;袁朝发&quot; 或者 chaofa 🤣。</p>
<h2 id="写书"><a href="#写书" class="heading-anchor">写书</a></h2>
<p>今年 4 月份，工作稍微可以松口气了；我终于可以开始回应去年 9 月份说的「<a href="http://yuanchaofa.com/blog/2025-09-month-summary">写书，我要开始了。等我！</a>」。于是我开始联系很久之前的编辑老师，说：&quot;我准备开始写书了&quot;。然后着急忙慌的聊了两次准备了一些材料，</p>
<p>本来以为说了 3 次了，这次总归是真正的了吧。毕竟书名都想好了《<a href="https://yuanchaofa.com/llms-zero-to-hero">大语言模型：从菜鸟到大师</a>》。但没想到这么难，工作上突然又忙起来，加上一些变动，我又没有足够的时间来写作了。材料到现在还没完全准备好，真的很对不起编辑老师。不过书我肯定是继续写下去的，已经开始了，就不会再停了。</p>
<p>这一次，我做好准备了。</p>
<h2 id="近况"><a href="#近况" class="heading-anchor">近况</a></h2>
<ul>
<li>近期出台的&quot;长桥/富途&quot;事件让我有点慌了，因为我真正在美股上买了很多中概，然后还亏钱很多，短期内是不可能回本了；如果是只能出金不能入金，流动性应该会有一定影响吧，这样我岂不是要进一步血亏。
<ul>
<li>好好打工，努力贴补股市。为什么我没有去相信美国标普啊。。。</li>
</ul>
</li>
<li>最近在听吴晓波的播客，让我对吴晓波的认识加深了很多，以前只知道是一个财经作家，没想到是一个这么真实的财经作家；我想这样的人是值得尊敬🫡。任何人都应该<a href="https://yuanchaofa.com/blog/2025-09-month-summary">合法的获取自己的财富</a>，有一定的积蓄，人才更有可能有尊严、有底线。—— 等我听完再来补读后感。</li>
<li>今年做字节做的事情结果还行，上半年也做出了非常好的成绩（来自很多名同事的直接反馈——得到这种认可我真的挺开心的），但我更希望未来能有一套更加稳固的自我认知的评估体系。我希望能放大自我认可的权重，而不是依赖于外界的反馈，尤其是在工作上。未来继续加油💪 接下来可能会有一些新的变化，等确定了再聊。</li>
<li>陪娃的时间多了很多，听着乎乎含&quot;爸爸&quot;就感觉很幸福，感觉自己已经是一个接近 A 得分的爸爸了 🤣</li>
</ul>
<p>以后更新频率当然也是不确定的（以前 flag 大家也看到结果），但有一件事是确定的：以后发的文章、视频，署名都是&quot;袁朝发&quot;。</p>
<hr class="footnotes-sep" />
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>&quot;chaofa 用代码打点酱油&quot; 不会消失，我会让它长存于我的 blog 中，毕竟这是我唯一能掌控的地方。 <a href="#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>如果改名之后内容也都没人看，那说明我创作的内容还是低于平均水平吧，继续加油💪 <a href="#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content:encoded>
    </item>
    <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.excalidraw" 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.excalidraw" 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.excalidraw" 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.excalidraw" 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>
<blockquote>
<p><strong>勘误（2026-08）</strong>：本文已修正二维推导中相对位置旋转矩阵的符号，并补充说明论文中的相邻维度布局与 Hugging Face Transformers 常见的 half-split 布局之间的区别。感谢读者在<a href="https://github.com/bbruceyuan/bbruceyuan.github.io/discussions/56">讨论区</a>指出问题。</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">$\Delta = n-m$</span>（key 位置减去 query 位置）：</p>
<div class="katex-block">$$\langle f_q(\mathbf{q}, m), f_k(\mathbf{k}, n) \rangle = g(\mathbf{q}, \mathbf{k}, n-m)$$</div>
<p>也就是说，无论 <span class="katex-inline">$m$</span> 和 <span class="katex-inline">$n$</span> 的绝对值是多少，只要 <span class="katex-inline">$n-m$</span> 相同，内积结果就相同。这里的相对位置是有方向的：交换 query 和 key 后，符号也会反过来。</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">$(n-m)$</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((n-m)\theta) + q_2 k_2 \cos((n-m)\theta) \\
&amp;\quad - q_1 k_2 \sin((n-m)\theta) + q_2 k_1 \sin((n-m)\theta) \\[8pt]
&amp;= (q_1 k_1 + q_2 k_2) \cos((n-m)\theta) + (q_2 k_1 - q_1 k_2) \sin((n-m)\theta) \\[8pt]
&amp;= \begin{pmatrix} q_1 &amp; q_2 \end{pmatrix} \underbrace{\begin{pmatrix} \cos((n-m)\theta) &amp; -\sin((n-m)\theta) \\ \sin((n-m)\theta) &amp; \cos((n-m)\theta) \end{pmatrix}}_{R_{n-m}} \begin{pmatrix} k_1 \\ k_2 \end{pmatrix} \\[8pt]
&amp;= \mathbf{q}^T \cdot R_{n-m} \cdot \mathbf{k}
\end{aligned}$$</div>
<p><strong>证毕</strong>：我们把中间这个只依赖于 <span class="katex-inline">$(n-m)$</span> 的旋转矩阵记为 <span class="katex-inline">$R_{n-m}$</span>，最终结果 <span class="katex-inline">$\mathbf{q}^T \cdot R_{n-m} \cdot \mathbf{k}$</span> 与 <span class="katex-inline">$m$</span> 和 <span class="katex-inline">$n$</span> 的绝对值无关，只与相对位置 <span class="katex-inline">$(n-m)$</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>这里采用论文和复数写法中更直观的<strong>相邻维度布局</strong>：<span class="katex-inline">$(x_0,x_1)$</span>、<span class="katex-inline">$(x_2,x_3)$</span>……每两个相邻维度组成一对，用对应的角度进行旋转。</p>
<p>需要注意的是，后面的 PyTorch 代码会采用 Hugging Face Transformers 中常见的 <strong>half-split 布局</strong>：<span class="katex-inline">$(x_0,x_{d/2})$</span>、<span class="katex-inline">$(x_1,x_{d/2+1})$</span>……两种布局都能实现 RoPE，但维度排列不同，不能把其中一种的 <code>rotate</code> 操作直接套到另一种布局上。第 4 节会详细解释它们的关系。</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">$n - m$</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>
<p>从这里开始，我们采用 Hugging Face Transformers 的 <a href="https://github.com/huggingface/transformers/blob/e42587f596181396e1c4b63660abf0c736b10dae/src/transformers/models/llama/modeling_llama.py#L173-L188">Llama 实现</a>和 <a href="https://github.com/huggingface/transformers/blob/main/src/transformers/models/qwen2/modeling_qwen2.py">Qwen2 实现</a>所使用的 <strong>half-split 布局</strong>。它把向量前半部分与后半部分对应配对，因此同一个频率序列需要在最后一维重复两次：</p>
<div class="katex-block">$$[c_0,c_1,\ldots,c_{d/2-1},c_0,c_1,\ldots,c_{d/2-1}]$$</div>
<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-应用旋转变换half-split-布局"><a href="#43-step-3-应用旋转变换half-split-布局" class="heading-anchor">4.3 Step 3: 应用旋转变换（half-split 布局）</a></h3>
<p>这是 RoPE 的核心，参考 Hugging Face Transformers 中 Llama/Qwen2 的实现方式：</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>第 3.2 节按相邻维度 <span class="katex-inline">$(x_0,x_1)$</span>、<span class="katex-inline">$(x_2,x_3)$</span> 配对；这里则把前半部分与后半部分配对。以四维向量为例：</p>
<pre><code class="language-text"><div class="highlight"><pre><span></span><code>x = [x0, x1, x2, x3]
配对方式：(x0, x2)、(x1, x3)
rotate_half(x) = [-x2, -x3, x0, x1]
cos = [c0, c1, c0, c1]
sin = [s0, s1, s0, s1]
</code></pre></div>
</code></pre>
<p>代入 <code>x * cos + rotate_half(x) * sin</code>：</p>
<pre><code><div class="highlight"><pre><span></span><code>[x0*c0 - x2*s0,
 x1*c1 - x3*s1,
 x2*c0 + x0*s0,
 x3*c1 + x1*s1]
</code></pre></div>
</code></pre>
<p>可以看到，<span class="katex-inline">$(x_0,x_2)$</span> 使用频率 <span class="katex-inline">$\theta_0$</span> 做二维旋转，<span class="katex-inline">$(x_1,x_3)$</span> 使用频率 <span class="katex-inline">$\theta_1$</span> 做二维旋转。因此当前 <code>rotate_half</code> 与前面重复两次的 cos/sin 是互相匹配的。</p>
<p>它与第 3.2 节的相邻维度布局并不是对同一份原始向量逐元素相等，而是相差一个固定的维度置换。设：</p>
<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>0</p>
<p>那么两种实现满足：</p>
<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>1</p>
<p>也就是说，两种布局在同步调整坐标排列后是等价的；但加载已有模型权重时，必须使用该模型训练时对应的布局，不能只替换 <code>rotate_half</code>。Meta 官方 Llama 的<a href="https://github.com/meta-llama/llama/blob/main/llama/model.py">参考实现</a>使用复数形式处理相邻维度，而 Hugging Face Transformers 的 Llama 实现使用这里的 half-split 形式。</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. 在每个半区内，低索引频率高，高索引频率低&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;2. 后半区重复前半区的频率，这是 half-split 布局的特征&quot;</span><span class="p">)</span>
    <span class="nb">print</span><span class="p">(</span><span class="s2">&quot;3. 第 i 维与第 i + dim/2 维组成一对，共享同一个旋转频率&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>
  </channel>
</rss>
