<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>逸风亭</title>
    <link>https://blog.fyz666.xyz/</link>
    
    <image>
      <url>https://blog.fyz666.xyz/images/avatar.jpg</url>
      <title>逸风亭</title>
      <link>https://blog.fyz666.xyz/</link>
    </image>
    
    <atom:link href="https://blog.fyz666.xyz/feed.xml" rel="self" type="application/rss+xml"/>
    
    <description>行所欲行，无问西东。</description>
    <pubDate>Wed, 22 Jul 2026 15:48:29 GMT</pubDate>
    <generator>http://hexo.io/</generator>
    
    <item>
      <title>一个理想主义者眼中的张雪峰：那个教人钻营功利的人，偏偏活成了最纯粹的样子</title>
      <link>https://blog.fyz666.xyz/blog/13223/</link>
      <guid>https://blog.fyz666.xyz/blog/13223/</guid>
      <pubDate>Sun, 29 Mar 2026 17:34:04 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;&lt;strong&gt;一个理想主义者眼中的张雪峰：那个教人钻营功利的人，偏偏活成了最纯粹的样子&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这一篇博客，原本不在我的计划内。&lt;/p&gt;
&lt;p&gt;作为一个经常折腾技术、以分享学习经历为乐的程序员，一直以来，我的博客里全是代码、配置和那些冷冰冰的技术笔</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p><strong>一个理想主义者眼中的张雪峰：那个教人钻营功利的人，偏偏活成了最纯粹的样子</strong></p><p>这一篇博客，原本不在我的计划内。</p><p>作为一个经常折腾技术、以分享学习经历为乐的程序员，一直以来，我的博客里全是代码、配置和那些冷冰冰的技术笔记，几乎从不聊任何感性的东西。关于张雪峰老师去世的消息，起初我只在博客的<a href="/talk">说说页面</a>里发了两段文字以表示遗憾，以为这只是又一个公众人物的离去，以为几天后我的遗憾感就会随着时间而淡去。但随后的几天，事情的发展超出了我的想象。我看到了那场空前绝后的互联网追思，无数素昧平生的学子、家长们自发地为他点起烛火；我更看到了遗体告别仪式那天，百姓自发齐聚、十里长街相送的震撼场景。</p><p>这种巨大的社会共鸣像一记重锤一样击中了我的内心。随着时间的推移，这种后劲远比我想象中要大，即便已经过了快一个礼拜，这种淡淡的悲伤感不仅没有消散，反而演变成了一种类似于“失去挚友”的钝痛。我意识到，能让万千普通人如此自发地悲恸，绝不仅仅是因为他教了几个志愿填报的技巧，而是他触动了某些关于理想、功利与初心的底层逻辑。</p><p>于是，我决定写下这篇博客——这是我的第一篇非技术文章。</p><hr><p>我一直自认为算是一个“理想主义者”，在做事时我总会带有某种“精神洁癖”，执着于去做那些我真正热爱、认为有价值的事，而尽可能规避掉很多我认为毫无意义的世俗任务——比起去钻研那些怎么多水几篇论文、怎么把简历包装得滴水不漏的投机技巧，老老实实做点真正有用的东西才更让我愉悦。又或者，在遇到他人发自真心地求助某个技术难题时，我经常会不计成本地扎进去为他解答困惑，甚至比做自己的事还上心（也因此经常被女朋友数落）。甚至在面对一些所谓的“职场生存法则”或“人情世故”时，我也有种近乎幼稚的固执：我总觉得靠本事吃饭才是最体面的，而不愿意去钻研那些复杂的社会关系，或者为了所谓的“前途”去迎合某种自己并不认可的秩序。</p><p>这种近乎奢侈的执拗，很大程度上得益于我较为优渥的家庭出身，它给了我足够的容错率去谈论热爱与情怀。我可以在深夜为一个优雅的想法而兴奋，是因为我暂时不需要在清晨为了碎银几两去计算每一分钟的产出。（十分巧合的是，我所热爱的计算机专业，恰好也是当下就业市场里的最优解，这让我的“清高”显得更加顺理成章）。</p><p>相比之下，张雪峰的言论在很多人眼中是极端的“世俗功利主义”——他主张普通家庭的孩子在专业选择上必须以就业为强导向。在某些“精英”看来，这似乎是在抹杀孩子们的梦想，是在把人工具化。但我虽身处一个高容错的环境，却极度认可他的绝大部分观点。</p><p>因为我清醒地意识到，对于大多数普通家庭的孩子来说，社会的试错成本高得离谱。我眼中的“热爱”与“理想”，对他们而言可能是一场赌上全家积蓄的豪赌。一步选错，可能不仅是三五年的蹉跎，更是几代人努力积攒下的上升通道被瞬间关闭。在那种连“生存”都还带着紧绷感的现实面前，一味劝人空谈理想和个人爱好，而不去考虑如何在这个弱肉强食的社会里先“活下来”，其实是一种极其傲慢且不负责任的行为。</p><p>张雪峰那些听起来扎心、甚至带点“钱味儿”的大实话，撕开了温情的面纱。他不是在推崇功利，而是在面对那个试错成本极高的系统时，给那些没伞的孩子在大雨里撑开了一把最实在的伞。这把伞虽然看起来并不华丽，甚至有些笨重，但它真的能遮雨。</p><hr><p>面对这样一个极具争议的人物，自然少不了批评的声音。</p><p>有人说，他贩卖了社会的焦虑，加速了内卷，甚至破坏了原本微妙的就业平衡。</p><p>但在我看来，这种指责多少有点本末倒置。焦虑从来不是因为张雪峰才产生的，它一直像深海里的暗流，真实地存在于每一个普通家庭的餐桌前。 他所做的，不过是利用互联网这个扩音器，把那些原本只流通于所谓“精英阶层”内部的信息差，用最糙、最直白的方式同步给了广大的普通人。</p><p>如果放任这种信息不对称，让普通孩子在温水煮青蛙的幻觉里耗尽家里的底气，让畸形的教育体系和就业环境在低效率的空转中透支社会的未来，那才叫真正的残忍。</p><p>用我所熟知的专业知识来解释，这本质上是一场最终目标是阶层破局与生存策略、环境参数（政策 / 就业）时刻在动态变化的 “强化学习”。</p><p>在一个奖励函数极度不可见、且环境参数实时波动的黑盒系统中，普通家庭的孩子往往在进行盲目的随机探索，或是在信息茧房里追求基于兴趣爱好的局部最优解。</p><p>而张雪峰的存在，就像是在这个系统中提出了一个基于全局先验知识的启发式搜索策略。他通过拆解潜规则，寻找那些被精英阶层隐藏、却能显著提升生存概率的“最优路径”。这种过程，实际上构成了系统自我完善的正向对抗反馈：当一方通过寻找最优解打破了旧有的信息垄断，原本傲慢且迟钝的系统为了维持某种动态平衡，就不得不被迫加速自我迭代，让规则走向透明。</p><p>一般而言，想要改良一个庞大且僵化的系统，往往有两种路径：一种是自上而下、难度极高的系统重构；另一种则是像他这样，作为“外部扰动”不断提出对抗性的最优解，倒逼系统进化。</p><p>也许他个人的初衷并没有带着某种救世式的伟大，但他的行为对社会改良的客观价值在我看来是积极的。他让规则不再是少数人的黑箱，他让“普通”二字在算法的博弈中，终于有了一份可以收敛的胜率。</p><p>至于说社会变“卷”，那本质上是资源分配和评价体系的问题。如果一个社会已经险恶到让大部分普通家庭的孩子连追求理想的入场券都拿不到，那么提前告诉他们真相、教他们如何避坑，不仅没有错，反而是一种极大的慈悲。在这个系统里，盲目的乐观才是最廉价的毒药。</p><p>还有人揪着他的商人身份不放，觉得他服务卖得贵，吃相难看。</p><p>作为在浙江这个具有浓厚商业氛围的省份长大的人，我反而觉得这种指责挺幼稚的。服务收费天经地义，何况这一行的市场行情价格本就不低。作为行业顶流，如果他的定价不设门槛，那涌入的需求量足以瞬间瘫痪他的整个团队。</p><p>更何况，他并没有把路堵死。只要你愿意花点时间，在他的直播间里完全可以“白嫖”到相当有价值的信息。而更让我动容的，则是他在赚到钱之后做的事：他给那些真正负担不起的贫困家庭提供免费咨询，甚至直接资助学费。相比那些通过捐款来合理避税、以此换取社会名望的企业家，张雪峰的善意显得极其“笨拙”且纯粹。除了那些挂在明面上的希望工程、教育资助项目，他更多的是在做那种“不见光”的善事。他嫌通过机构走流程太慢、中间环节太杂，干脆直接加微信、走支付宝打钱。这种私下的资助，不做任何公关宣传，甚至连张像样的通稿都懒得发。</p><p>从流传出来的聊天截图里可以看到，他的关心甚至细碎到了家长的一日三餐。面对患病的家长，他不由分说地转账五千块，嘴里说着最地道的“大实话”：“这钱你先拿着，买点营养品……东北话，别撕吧。我也没多给，大事解决不了，吃穿用没问题。”</p><p>这些话里没有居高临下的施舍，只有一种平等的、带温情的体谅。在任何一个精明的商人眼里，这种“投入产出比”极低、甚至连慈善名号都挂不上的杂事，完全没必要浪费时间去做。但他却像个捡拾幸存者的独行侠，在大众所看不见的角落里，一件件地做着。这种带点草莽气息的侠气，在当下的资本环境下，其实已经纯粹得有些过分了。</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://blogfiles.oss.fyz666.xyz/jpeg/068c124b-9046-4da1-8c32-457356bc5485.jpeg","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/jpg/966998fa-403d-45df-a0c8-da15b5526b73.jpg","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/jpg/44811067-eff3-4389-9bd1-8ba10d0bfac6.jpg","alt":""}]</div>      <div class="gallery-items">      </div>    </div><hr><p>或许有人会觉得奇怪，我一个平时只关心技术，甚至带点“精神洁癖”的理想主义者，为什么会对一个满口“就业、赚钱、避坑”的世俗老师产生如此强烈的共鸣？甚至在这些天里，感受到一种不亚于失去好友的钝痛？</p><p>起初我也在反思这种情绪的来源，后来我才意识到，我和他其实是在同一套“底层逻辑”下运行。</p><p>我追求技术的纯粹，厌恶“水论文”和投机取巧，是因为我希望这个世界能像代码一样逻辑清晰、奖惩公平；而张雪峰教人钻营功利、拆解社会潜规则，是因为他深知这个世界的运行规则对普通人有多残酷，他想用最直白的“生存算法”，为那些没伞的孩子换一张通往理想的入场券。</p><p>他教人功利，是为了让人有资格不功利；他谈论金钱，是为了让人未来不必只为了金钱而活。</p><p>这种“知行合一”的慈悲，在我看到他对待员工和家长的那些细节时，彻底击穿了我最后的心理防线。我突然意识到，这个被贴上“功利”标签的人，其实活成了我眼中最纯粹、最硬核的理想主义的样子。</p><hr><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://blogfiles.oss.fyz666.xyz/jpg/e3cfc5ce-cb6e-4421-8cac-9e43607af4ec.jpg","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/jpg/e77ce53e-ed32-43ee-8b07-e0eb0734ec19.jpg","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/jpg/90e932d2-5d67-4361-9eea-459606e0a4cf.jpg","alt":""}]</div>      <div class="gallery-items">      </div>    </div><p>这件事的起因其实非常微不足道：张雪峰在公司群里看到员工随口提到喜欢魏大勋，这原本只是一句转瞬即逝的闲聊，但他悄悄记在了心里。后来，他私下联系魏大勋本人要来了几张签名照。当他像个<strong>“拿到糖的孩子”</strong>一样，得意地举着照片走进办公室送给员工时，所有人都惊呆了。两位员工甚至激动得冲上前抱住了他，而他只是被逗得满脸笑容，像个下班回家给自家孩子带了心仪已久的礼物的老父亲。</p><p>看着这些截图，我甚至能隔着屏幕、隔着时间和空间感受到张雪峰在那一刻的快乐。</p><p>我太熟悉这种感觉了——那种不计回报地耗费大量时间，为一个素昧平生的网友排查到 Bug 后的快感；又或者在偶然听说他人遇到技术困难、而那刚好涉及我的专业领域时，我会按捺不住兴奋地和对方讨论，甚至恨不得把自己的所有专业想法都灌输给他。当最终得到对方一句“这个方案可行”的评价时，那种精神上的充盈感，远比账户里多出几百块钱要厚实得多。</p><p>张雪峰这种真实感，简直让我感触太深。他送出签名照时那种得意地晃晃照片、笑得像个“拿到糖的孩子”一样的神态，根本不是什么高高在上的一个老板的恩赐。那是一个人发现了别人的微小愿望，并动用自己的能力精准实现了它之后，获得的一种极其纯粹、极其“理想主义”的满足感。</p><p>在任何一个精明的商人眼中，这种事压根就没有产出比。但他却乐此不疲，甚至在处理这些琐事时，比谈成一桩大生意还要兴奋。这种<strong>“不被功利逻辑异化”</strong>的真实，在这个处处讲究回报的时代，纯粹得甚至有些刺眼。</p><hr><p>如果说送签名照还带点对某种微小愿望的执着与痴迷，那是一种极其私人、甚至有点“不务正业”的快乐，那么他对员工生活的那种细碎操心，则彻底撕碎了那些关于“商人必无良”的刻板印象。</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://blogfiles.oss.fyz666.xyz/jpeg/1e9a91f2-3b9e-4594-b3b3-87905a734492.jpeg","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/jpeg/c2bbe155-314a-44cd-8255-30691927dd9c.jpeg","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/jpeg/dd3ca8f1-ef1f-4b7d-8a77-17a86bd17412.jpeg","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/jpeg/8db99e9a-56b6-4d07-9a88-b0c920ebef52.jpeg","alt":""}]</div>      <div class="gallery-items">      </div>    </div><p>看着这几张流传出来的聊天截图，我看到了一个把“家人观”践行到极致的真实人格：在七夕节催着全员提早下班去接老婆、对象；员工报喜生了娃，二话不说转账 6666 元。</p><p>甚至在员工家人感染新冠、生命垂危、全网一药难求的至暗时刻，他二话不说，将自己仅剩的、用来压箱底的救命药直接匀出一大半，只为让员工能开车送去上海救人。当员工家人脱离险境、想要转账 8000 元以示感谢时，他又一次云淡风轻地拒收了，只留下那句最直白最暖心的回应：“大可不必，我弄这个药就不是为了挣钱。你们都是我的家人，只要大家平安。”</p><p>而最让我感到震撼的，其实不是这些，而是这几张截图里透出来的、那种在普通职场环境里极难见到的聊天氛围。在很多人的印象里，公司工作群或是机械的“收到”，或是战战兢兢的汇报，甚至连一个表情包都要反复斟酌。但在这张截图里，那种流动的气息完全不同——那是一种极其松弛的家人感。这种氛围不是靠无聊的团建“团”出来的，也不是靠“狼性文化”逼出来的，而是一个老板在无数次“不计产出”的善意里，一点点喂出来的信任。在群里，大家不需要小心翼翼地敲出“收到”，而是可以毫无顾虑地在老板面前发一些表情包。</p><p>还有公司的福利，这我就更不用说了：</p><a class="tag-Link" target="_blank" href="https://zhuanlan.zhihu.com/p/1890780557715173716">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=zhuanlan.zhihu.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">盘点那些让人羡慕嫉妒恨的神仙公司——峰学蔚来</div>            <div class="tag-link-sitename">知乎</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>这还只是冰山一角，更细节的福利可以从网上内部员工的一些评论中窥知一二。我随便列举几条：</p><ul><li>每月额外3天小长假</li><li>入职满2年：苏州购房无息借款（金额 = 个人年薪）</li><li>入职满3年：深造学费报销50%</li><li>生育奖励：一胎2万、二胎4万、三胎8万（很多公司可是反感员工生育的）</li><li>年度体检、子女教育补贴、宠物福利</li><li>提前为员工预存半年工资</li></ul><p>我突然明白，为什么他的员工会自发地在群里喊出那些听起来甚至有些“肉麻”的口号，为什么在那个工作群聊中的聊天氛围会如此松弛。他是在这片充满了 996 和职场 PUA 的荒原上，凭空为普通人造出了一个“理想主义的避风港”。</p><hr><p>除了那些硬核的福利和侠气的捐赠，最让社会大众动容的，其实是他在实现阶级跨越后，依然活得像个再普通不过的平凡人。</p><p>在这个崇尚“赢者通吃”的商业环境里，我见过太多寒门出身的老板，在实现阶级跨越后，反而成了最冷酷的压榨者。他们往往带着一种“我当年也是这么苦过来的”的逻辑，加倍地在员工身上索取，试图通过剥削后来者来补偿自己曾经缺失的安全感。</p><p>但张雪峰不同。他属于那种典型的“因为自己淋过大雨，所以总想给别人撑一把伞”的人。</p><p>他深知普通家庭的孩子在苏州这种城市扎根有多难，所以他提供购房无息贷款；他知道底层父母供出一个大学生有多不易，所以他给员工预存了足量工资，以提升公司遇到危机时员工的抗风险冗余度。</p><p>他虽然已经实现了阶级跨越，却从未在精神上剪断过与“土地”的连接，始终活得像个再普通不过的平凡人，与大众没有任何距离感。在这个稍微有点成就就恨不得香车美女、保镖随行的环境下，他却显得极其“违和”：他会像千千万万个普通的中国父亲一样，骑着那辆有些简陋的电动车，穿梭在苏州的街头接送女儿上下学（我直接爆哭好吗😭）；他会在除夕、元宵节等节日自费买来绚烂的烟花，不是为了某种商业站台，而只是单纯想在这个略显沉重的时代，放给路过的、素昧平生的百姓们看。</p><p>这种“不被异化”的真实，才是最可贵的。</p><p>他明明早已拿到了通往所谓的“上流阶级”的入场券，却始终愿意把脚踩在泥土里。这种“知行合一”的平民底色，解释了为什么他离去时，会有万千普通人自发地为他点起烛火。他不仅是在教人如何“往上爬”，更是在用他自己的生活方式告诉我们：一个人即便飞得再高，也可以不丢掉那份对普通生活的热爱，不丢掉对平凡大众的悲悯。</p><hr><p>写到这里，我终于明白了这几天我心中的意难平究竟是什么，也找到了那个困扰我许久的 Bug 的解法。</p><p>我所伤感的，不再仅仅是一个志愿填报专家的离去，而是一个<strong>“在功利世界里，依然守住了人味儿”</strong>的鲜活样本的缺失。这种“人味儿”，不仅仅是那种爱开玩笑、不摆架子的“活人感”，而是一种近乎执拗的<strong>“人性”底色</strong>。</p><p>在商业丛林的生存法则里，大多数人一旦跨越了阶级，逻辑就会自动发生重构：他们会开始拥抱那一套高高在上的精英主义，把员工看作报表上的人力成本，把受众看作待收割的数据流量。但在张雪峰身上，我看到了一种罕见的、从未被异化的人性。</p><p>这种人性，是他即便拿到了通往社会顶层的入场券，却依然能精准感知到泥土里每一个普通家庭的痛点——他知道寒门学子试错成本的沉重，所以他甚至不惜用略显冒犯的直白去拆解规则；他知道普通员工在异乡扎根的无助，所以他提供购房贷款、预存工资，甚至细碎到关心一张签名照、一份七夕的惊喜。他最珍贵的地方在于，他在追求世俗意义上的“赢”的过程中，从未以献祭“人性”为代价。 他没有把自己活成一个冰冷的、精密计算的赚钱机器。相反，他赚来的每一分钱，似乎都成了他守护人性中那点温情的防御工事。</p><p>曾经有很多听说「我这个有着“精神洁癖”的程序员以后要创业」这件事的人对我说：“别太天真，这个世界的运行法则是冷酷的，有良心的人当不了老板。”这句话一度像一个无法跳出的死循环，横在我的面前，让我对未来产生了一种隐隐的恐惧：是不是想要获得世俗意义上的成功，就必须献祭掉自己的初心？必须变得精算、利己、且高高在上？</p><p>但张雪峰老师用他短暂而绚烂的一生，硬生生地跑通了这个 Demo。</p><p>他让我看到，即便是在最讲究存活率、最现实、最功利的商业丛林里，善良与成功不仅不互斥，甚至可以互为底色。他不是因为成功了才去施舍，他是带着那种质朴的、哪怕已经跨越阶级也从未熄灭的“草根血性”，在冰冷的规则里杀出了一条温情的路。</p><p>他用行动告诉我们：一个人即便飞得再高，也可以不丢掉对平凡生活的热爱，不丢掉对淋雨者的悲悯。</p><hr><p><img src="https://blogfiles.oss.fyz666.xyz/webp/076f7e48-e485-480a-a595-f6fc2b5e33ab.webp" style="zoom:50%;" /></p><p>屏幕里的他，肉眼可见的疲态，戴着黑色半框眼镜框，微微向前探身，就像之前无数次退出直播间之前的那样笃定地对镜头说：“我们晚上7点直播间再见啊。”</p><p>可这一次，峰哥，你失约了😭😭😭。</p><p>七点钟的那个直播间，再也不会亮起那束熟悉的光。那些你尚未讲完的理想，那些你还没来得及送上岸的孩子，那些你曾想给普通家庭撑起的伞…最终都成了这个春天最深沉的意难平。</p><p>他走了，那个始终保留着一份赤子之心的人，走了，留下了这把笨重却无比结实的伞。</p><p>他走得太行色匆匆，留下了一张还没来得及兑现的蓝图，和无数双望向远方的眼睛。</p><p>但他在冰冷的规则里杀出的那条温情的路，不该就此断绝；</p><p>他在极端的功利中守住的那份人性的底色，不准就此抹去；</p><p>他亲手验证过的这段关于“良知与成功并存”的逻辑，更不能就此成为绝唱！</p><p>这把伞既然有人撑起过，就证明这雨天里，本就可以有伞；</p><p>这条路既然有人走通过，就证明这荒原上，本就可以有路！</p><hr><p>尹山湖的烟花绚烂璀璨，如你的人生，虽行至中途便戛然而止，却在那段极为有限的时间里，迸发出了最耀眼的光芒。那是你燃尽了自己，为无数寒门学子在迷茫中点燃的信号弹。这漫天的火树银花，是群众为你最后的告别，也是最沉甸甸的认可。</p><p>在漫天烟火中（虽只是在屏幕前刷到，却也足够震撼，有朝一日，我也要去一次那个地方，补一场烟火），突然想起你曾经常半开玩笑式地对大家说过的那句话：“人生真好玩，下辈子还来。”</p><p>峰哥，这一次，可别再失约了。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E9%9A%8F%E7%AC%94/">随笔</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%BC%A0%E9%9B%AA%E5%B3%B0/">张雪峰</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E7%90%86%E6%83%B3%E4%B8%BB%E4%B9%89/">理想主义</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/13223/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>使用打表法计算麻将向听数</title>
      <link>https://blog.fyz666.xyz/blog/13157/</link>
      <guid>https://blog.fyz666.xyz/blog/13157/</guid>
      <pubDate>Mon, 02 Mar 2026 08:23:33 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h2 id=&quot;牌桌趣事&quot;&gt;&lt;a href=&quot;#牌桌趣事&quot; class=&quot;headerlink&quot;</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h2 id="牌桌趣事"><a href="#牌桌趣事" class="headerlink" title="牌桌趣事"></a>牌桌趣事</h2><p>丙午马年的春节之前，我跑了趟杭州，约了两个在杭州的朋友去雀庄打了两把立直麻将。其中一位朋友是个日麻小白，为了帮助他快速习得基础玩法，我发给了他一个很早以前做的网页：<a href="https://riichi.fyz666.xyz/yaku-list">雀魂役种一览</a>。</p><p>都说到这了，就顺便讲下那天的<del>麻将趣事</del>（一点也不有趣）吧。下面场景在同一个半庄出现两次：</p><div class="note primary simple"><p>我满贯确听牌，切出一张牌 dama，那位新手：“诶，你过来帮我看一下我是不是胡了”<br>我过去一看：你这没有役，荣胡不了（没毁了我的大牌，暗自庆幸中…）<br>同一巡，还是那位新手：“哎 没事 我自己摸到了”</p></div><p>还是同一个半庄，我亲位：</p><div class="note primary simple"><p>配完牌后，那位新手：“你刚刚是不是说七个对子也算一种役？”<br>我：“算的算的”<br>我朋友：“一般你开局有四个对子才会考虑直线七对子”<br>那位新手：“我数数，1 2 3 4 5 6，我六个对子”</p></div><p>然后我幸运地铳了：w立、七对、d2，一把万二大牌，喜提第二（不是哥们你w立七对叫我怎么防？？？）。</p><p>我决定以后打雀再也不坐新手的上家了…</p><hr><h2 id="正题"><a href="#正题" class="headerlink" title="正题"></a>正题</h2><p>说回正题，我发现我乱糊的网页版似乎没有那么方便（因为当时懒得写前端，选了一个使用websocket通信的懒人框架），一段时间不看它就得刷新重连，体验非常不好。正好这段时间我在捣鼓微信小程序，遂想着要不做个小程序吧，也更易于推广给别人用。</p><p>说干就干，在vibe coding的加持下，很快我就把已有的功能全部移植了过去。接下来还能搞点什么呢？作为一个科学麻将爱好者，我想到了牌效分析功能，即任给一手牌，要能计算出切各张牌后的进张效率，而这个功能的底层逻辑则是需要能精确地计算出一手牌的向听数。</p><blockquote><p>N向听（Nシャンテン，N-ready），指的是手牌距离听牌仍需N枚有效牌。如果一手牌还需1枚有效牌才能达致听牌，则称为1向听（イーシャンテン，1-ready），如此类推。<br><cite><a href="https://mahjong.fandom.com/zh/wiki/%E5%90%91%E8%81%BD%E6%95%B8">麻将维基：向听数</a></cite></p></blockquote><p>很早以前我曾写过麻将和牌判断算法，一般而言，判断一手牌是否和牌有两种方法：DFS和打表法。当时我都实现了一遍。对于经常需要检查是否和牌的场景，通过DFS枚举手牌拆分方法会显得迟钝，于是这种场景得考虑给和牌型打个表。那么对于向听数的计算而言，是否有类似的算法呢？有的兄弟，有的！</p><h2 id="向听数算法"><a href="#向听数算法" class="headerlink" title="向听数算法"></a>向听数算法</h2><p>方便起见，我们先考虑14张牌的情形。立直麻将共有三种和牌形状，其中最常见的「$mAAA+(4-m)ABC+DD$」型，被称为一般型，另外还有七对和国士无双型，后两者的向听数非常容易计算，可以直接略过，唯一需要注意的是立直麻将的七对子牌型不允许出现两个相同的对子，因此计算时需要做一些额外的处理。</p><p>我们主要来考虑一般型的向听数。根据向听数的定义，我们可以额外定义「0向听」表示听牌、「-1向听」为和牌。</p><h3 id="启发式的思考过程"><a href="#启发式的思考过程" class="headerlink" title="启发式的思考过程"></a>启发式的思考过程</h3><p>那么如何计算向听数呢？首先我们来思考一下人类玩家自己是怎么算的。不妨从和牌型倒着推算：</p><ol><li>一手已经组成四个面子和一个雀头的牌，是-1向听。</li><li>将其中一个面子换成一个搭子+一个孤张，则显而易见，这手牌的向听数+1。</li><li>将其中一个面子换成三个孤张，则向听数+2（其中两个孤张需要替换为第三个孤张的靠张才能形成面子）。</li><li>如果把雀头换为两张不同的牌，则在组合中没有其他对子的情况下，需要额外替换一张牌形成雀头，此时向听数需要再加1。</li><li>一手牌最多只需要4组面子+搭子的组合，因此对于搭子数而言，应设置其上限为4-面子数。</li></ol><p>根据上面naive的思考，似乎一个完美的算法已经形成了：</p><p>对于一手牌的某种拆分方法，记其中的面子数为 $m$，搭子数为 $d$，雀头数为 $q, q\in\left\lbrace0、1\right\rbrace$，可以计算出向听数 $s$ 如下：</p><script type="math/tex; mode=display">s=2\times(4-m)-\max(d,4-m)-q</script><p>随便找几组牌测试看看：</p><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1OzKuJ1axvOfj2Yw6E_o_V3NJxxM70XYf_2man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1gxUeKE9JIHEL3_MuR1fydbGsySiLggqH_3man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1V82cfeDAiXPvzcpFPEojV92PjsdTogvc_4man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1K4c__x2sKI4Sm8lpqpH9u3QKHy7Yy9df_1pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1-Mk4KXmiIZzKxLKP6-sxAaCafqj-4rvJ_4pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1a8BEkbgwHiD7EiRO19ofmjRg5XE2nHT1_9pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1XzCguaSswig-5RoLGBk-Ics0r_RaRHpU_5sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1XLUj_SKlJ3_gdOsdkmAG9_kyapiEAB9g_6sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WE6PyQKYTGANaHIicZnIX-9jpnieT7AY_9sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WE6PyQKYTGANaHIicZnIX-9jpnieT7AY_9sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1N4k1CdDfo_7xOw12iblPKISsB-cN9C83_pei.png"></div></div><div class="note primary simple"><p>将九索视为雀头：<br>$m = 1,\ d=2,\ q=1,\ s=2\times 3-2-1=3$<br>将九索视为刻搭：<br>$m = 1,\ d=3,\ q=0,\ s=2\times 3-3-0=3$</p><p>向听数为3</p></div><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Fy-ARC3N_5Lccv-AfKBDUR8VNqC1KJhc_7man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Ey9qrF-MoijCsFcy2Sjo1XS5Mv7Rig50_8man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1K4c__x2sKI4Sm8lpqpH9u3QKHy7Yy9df_1pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1QhZjQj9zVBhK43IN0YkIKgvBcZOalVLR_5pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1QhZjQj9zVBhK43IN0YkIKgvBcZOalVLR_5pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1C3KyJuRmpSmy9iZwcUdEy77K6DW78qGc_6pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1C3KyJuRmpSmy9iZwcUdEy77K6DW78qGc_6pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1C3KyJuRmpSmy9iZwcUdEy77K6DW78qGc_6pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13RWEHTtPfjPMVBK9ZPInZFFLVAflgE6__1sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1XzCguaSswig-5RoLGBk-Ics0r_RaRHpU_5sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1XLUj_SKlJ3_gdOsdkmAG9_kyapiEAB9g_6sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1N4k1CdDfo_7xOw12iblPKISsB-cN9C83_pei.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1mi0PEvNCGVQcY0-A5GrkkJnDCb5yX8NZ_chun.png"></div></div><div class="note primary simple"><p>将五饼视为雀头：<br>$m = 1,\ d=2,\ q=1,\ s=2\times 3-2-1=3$<br>将五饼视为刻搭：<br>$m = 1,\ d=3,\ q=0,\ s=2\times 3-3-0=3$<br>将六饼视为雀头，五六饼视为顺搭：<br>$m = 0,\ d=3,\ q=1,\ s=2\times 4-3-1=4$<br>将六饼视为雀头，五饼视为刻搭：<br>$m = 0,\ d=3,\ q=1,\ s=2\times 4-3-1=4$</p><p>向听数为3</p></div><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1OzKuJ1axvOfj2Yw6E_o_V3NJxxM70XYf_2man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1V82cfeDAiXPvzcpFPEojV92PjsdTogvc_4man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Fy-ARC3N_5Lccv-AfKBDUR8VNqC1KJhc_7man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Ey9qrF-MoijCsFcy2Sjo1XS5Mv7Rig50_8man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1K4c__x2sKI4Sm8lpqpH9u3QKHy7Yy9df_1pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Z7703uYgHoZZVbz3dupS-5UV8HkYILvd_2pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_19R75dKU2nh6iQD4wqI0sBSAbOW19de4u_2sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1y7rsvWXcMpnvJZs6JofvDSMQRMX1mnB-_3sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1y7rsvWXcMpnvJZs6JofvDSMQRMX1mnB-_3sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1XzCguaSswig-5RoLGBk-Ics0r_RaRHpU_5sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"></div></div><div class="note primary simple"><p>将一万视为雀头：<br>$m = 1,\ d=4,\ q=1,\ s=2\times 3-3-1=2$</p><p>向听数为2</p></div><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1OzKuJ1axvOfj2Yw6E_o_V3NJxxM70XYf_2man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Ey9qrF-MoijCsFcy2Sjo1XS5Mv7Rig50_8man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Z7703uYgHoZZVbz3dupS-5UV8HkYILvd_2pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1QhZjQj9zVBhK43IN0YkIKgvBcZOalVLR_5pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1e-FBond5JUVbpVHPiGG2f_wpfRzY80cX_8pin.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_19R75dKU2nh6iQD4wqI0sBSAbOW19de4u_2sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1XzCguaSswig-5RoLGBk-Ics0r_RaRHpU_5sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1ydkiYAXLL4FAjeO8CKNOIKges6ryU8oa_8sou.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1N4k1CdDfo_7xOw12iblPKISsB-cN9C83_pei.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1uXNsAiK0uGTIE8ckYXHF4_1sbrd_wpZ7_haku.png"></div></div><div class="note primary simple"><p>$m = 0,\ d=0,\ q=0,\ s=2\times 4-0-0=8$</p><p>向听数为8</p></div><p>好像看起来没什么问题。容易发现，对于一手牌而言，其拆分方法可能有很多种，我们需要对它进行不同的拆分，得到上述参数后分别计算向听数，并取其最小值，才能得到最终的向听数。</p><h3 id="特殊情形"><a href="#特殊情形" class="headerlink" title="特殊情形"></a>特殊情形</h3><p>偶然间发现这样一手牌：</p><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1N4k1CdDfo_7xOw12iblPKISsB-cN9C83_pei.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1N4k1CdDfo_7xOw12iblPKISsB-cN9C83_pei.png"></div></div><p>我们按上面向听数的计算公式，得到：</p><script type="math/tex; mode=display">m=3,\ d=0,\ q=1, s=2\times 1-0-1=1</script><p>计算结果为1向听，但这一手牌移除3组面子和一个雀头后，剩下的三个孤张「东南西」，没有一张能摸成搭子，故这手牌理应是2向听。</p><p>这种特殊情况的存在使得原先的算法需要进行一定程度的调整，经过一番搜索，我找到了这篇文章：</p><a class="tag-Link" target="_blank" href="https://www.bilibili.com/read/cv20401264/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=www.bilibili.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">麻将向听数的计算与编程实现</div>            <div class="tag-link-sitename">www.bilibili.com</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><h3 id="最终的算法"><a href="#最终的算法" class="headerlink" title="最终的算法"></a>最终的算法</h3><p>愚钝的我研读了一下上面这篇文章后，感到醍醐灌顶，于是在这里记录一下对这个调整后的算法的理解。</p><p>首先，定义一个概念： <strong>爆满</strong>，是指手牌中数量已经达到4张的牌的状态。</p><p>我们发现前面提到的特殊情形正是由于<strong>爆满</strong>的存在而产生的。更不巧的是，这里<strong>爆满</strong>的牌是三种字牌，而字牌不能形成顺子，这才导致向听数的计算公式产生了bug。</p><p>为应对这些复杂的情况，文章作者提出了一些新的参数。</p><ul><li><p>G3：面子（顺子或刻子）的总数。</p></li><li><p>G2：搭子（差一张牌就能变成面子）的总数（不能是脏搭子）。</p></li><li><p>DG2：脏搭子（组成搭子的两张牌都处于爆满状态）的数量。</p></li><li><p>P：雀头的数量，只能是0或1。</p></li><li><p>DN：脏数牌（孤立的数牌，并处于爆满状态）的数量。</p></li><li><p>DZ：脏字牌（孤立的字牌，并处于爆满状态）的数量。</p></li><li><p>R：剩余的牌的数量。</p></li><li><p>N：手牌的数量，取值为2、5、8、11、14。</p></li><li><p>K：K=(N-2)/3，需要做出的面子的数量，取值为0、1、2、3、4。</p><p>下面是算法对应的部分代码：</p></li></ul><pre class="line-numbers language-python" data-language="python"><code class="language-python">s <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span>R <span class="token operator">=</span> N <span class="token operator">-</span> <span class="token number">3</span> <span class="token operator">*</span> G3 <span class="token operator">-</span> <span class="token number">2</span> <span class="token operator">*</span> <span class="token punctuation">(</span>G2 <span class="token operator">+</span> DG2 <span class="token operator">+</span> P<span class="token punctuation">)</span> <span class="token operator">-</span> DN <span class="token operator">-</span> DZK <span class="token operator">=</span> N <span class="token operator">//</span> <span class="token number">3</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>先对部分参数进行初始化，<code>s</code>代表向听数，从和牌型开始计算，逐步往上加，因此初始化为-1，<code>R</code>表示未形成面子、搭子、雀头，但也未处于爆满状态的牌的数量。</p><hr><p>接下来的一段代码，将处理搭子溢出：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">if</span> G3 <span class="token operator">+</span> G2 <span class="token operator">+</span> DG2 <span class="token operator">></span> K<span class="token punctuation">:</span>    t <span class="token operator">=</span> K <span class="token operator">-</span> G3 <span class="token operator">-</span> DG2    <span class="token keyword">if</span> t <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">:</span>        R <span class="token operator">+=</span> <span class="token number">2</span> <span class="token operator">*</span> G2        DN <span class="token operator">-=</span> <span class="token number">2</span> <span class="token operator">*</span> t    <span class="token keyword">else</span><span class="token punctuation">:</span>        R <span class="token operator">+=</span> <span class="token number">2</span> <span class="token operator">*</span> t    G2 <span class="token operator">=</span> K <span class="token operator">-</span> G3<span class="token keyword">else</span><span class="token punctuation">:</span>    G2 <span class="token operator">+=</span> DG2<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>逻辑是：当面子+搭子的总数超过<code>K</code>时，溢出的搭子必须被拆为孤张，由于孤张的<strong>脏牌</strong>对向听数优化的贡献较低，这里可以贪心一下，优先拆普通搭子。普通搭子拆完以后，继续拆脏搭子。同时这一步将更新剩余孤张数<code>R</code>以及脏数牌的数量<code>DN</code>。</p><hr><p>接下来处理雀头：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">if</span> P <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">:</span>    <span class="token keyword">if</span> R <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>        <span class="token keyword">if</span> DZ <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>            R <span class="token operator">-=</span> <span class="token number">1</span>            DZ <span class="token operator">-=</span> <span class="token number">1</span>        <span class="token keyword">elif</span> DN <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>            R <span class="token operator">-=</span> <span class="token number">1</span>            DN <span class="token operator">-=</span> <span class="token number">1</span>        <span class="token keyword">else</span><span class="token punctuation">:</span>            R <span class="token operator">-=</span> <span class="token number">2</span>        s <span class="token operator">+=</span> <span class="token number">1</span>    <span class="token keyword">else</span><span class="token punctuation">:</span>        <span class="token keyword">if</span> DZ <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>            <span class="token keyword">if</span> DZ <span class="token operator">>=</span> <span class="token number">2</span><span class="token punctuation">:</span>                DZ <span class="token operator">-=</span> <span class="token number">2</span>            <span class="token keyword">else</span><span class="token punctuation">:</span>                DZ <span class="token operator">-=</span> <span class="token number">1</span>                DN <span class="token operator">-=</span> <span class="token number">1</span>        <span class="token keyword">else</span><span class="token punctuation">:</span>            DN <span class="token operator">-=</span> <span class="token number">2</span>        s <span class="token operator">+=</span> <span class="token number">2</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>当没有雀头时，我们至少需要额外一个进张来形成雀头，这会导致向听数+1。不过，根据剩余牌的类型，又能分为几类情况。</p><p>首先只有非脏牌的孤张牌才能形成雀头，这要求<code>R &gt; 0</code>，此时，我们可以直接用其中的某张孤牌来做雀头；否则，手上所有孤立牌都处于爆满状态，已经摸不成雀头了，故至少需要再进两张牌才能凑一个雀头出来。这两种情况分别对应了上面代码中的 <code>s += 1</code>以及<code>s += 2</code>。</p><p>既然进来了新的牌凑成了雀头，我们当然得打掉对应数量的牌，按牌的利用价值，我们优先打走脏字牌，其次是脏数牌。最后才会打走孤张非脏牌。</p><hr><p>接下来，函数进行收尾：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">R <span class="token operator">+=</span> DNDZ <span class="token operator">-=</span> G2 <span class="token operator">+</span> <span class="token number">2</span> <span class="token operator">*</span> R<span class="token keyword">if</span> DZ <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>    s <span class="token operator">+=</span> DZ <span class="token operator">//</span> <span class="token number">3</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>这部分似乎不大好理解。不如通过刚刚的那手牌来看看是怎么回事。</p><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1b75TcEdh1xo8nrkUsOFt4wsJwO6ngOHF_tan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_17VGbN60ebC6i4ioqW549thsxaxZ-cJsn_nan.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1iAKx6sKyBOAN217FKH-y1b6VyygPSMFo_xia.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1N4k1CdDfo_7xOw12iblPKISsB-cN9C83_pei.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1N4k1CdDfo_7xOw12iblPKISsB-cN9C83_pei.png"></div></div><p>对于上面这手牌，最优的参数组合是：G3 = 3, G2 = 0, DG2 = 0, P = 1, DN = 0, DZ = 3。另外我们可以计算得到 K = 4, R = 0。</p><p><code>DZ -= G2 + 2 * R</code>这一步意味着我们希望在组面子的过程中消耗掉一些脏字牌。上面这手牌中，G2和R均为0，因此最后会剩余3张脏字牌。如果是三个普通的孤张，相比于一个面子而言，会“贡献”两向听，这一部分已经体现在 $s=2\times(4-m)-\max(d,4-m)-q$ 这个公式里了（每3张散牌的存在会减少一个面子），但如果三个孤张都是脏字牌——无法成搭，那么我们还需要额外的一次换牌，将其中一张脏字牌替换成普通的孤张，才能顺利组成面子，因此还需要额外+1。</p><p>这也是最后这行代码<code>s += DZ // 3</code>所做的事。</p><hr><p>在进行了以上调整后，函数计算向听数：<code>return 2 * (K - G3) - G2 + s</code>，这个公式本质上和前面那个naive的公式是一样的。</p><p>综上，这个函数相当于对数量达到4张的牌做了更完善的处理。</p><h3 id="3n-1型的向听数计算"><a href="#3n-1型的向听数计算" class="headerlink" title="3n+1型的向听数计算"></a>3n+1型的向听数计算</h3><p>上面这个函数只适用于3n+2型的向听数计算，应用到3n+1型需要做一些修改。除了 s 初值修改为0外，还需要修改一下对 P 和 DZ的处理代码。不过思路是差不多的。</p><h2 id="打表"><a href="#打表" class="headerlink" title="打表"></a>打表</h2><h3 id="编码"><a href="#编码" class="headerlink" title="编码"></a>编码</h3><p>字牌和数牌行为存在差异，因此我将字牌和数牌分别进行了编码。而三种数牌则是全排列对称的，因此只需要编码一种数牌就行了。</p><p>接下来需要定义一个距离。由向听数的计算方法，我们容易得到距离为0、1、2的牌（可以形成面子和搭子）才会影响向听数，3及以上不影响向听数。因此定义距离函数如下：</p><script type="math/tex; mode=display">D(x,y)=\left\lbrace\begin{aligned}|x-y|, \ &|x-y|\le2\\\infty, \ &|x-y|>2 \lor x,y \text{为字牌}\\\end{aligned}\right.</script><p>我们可以将一手同花色的牌编码为<strong>枚数序列：s1和距离序列：s2</strong>两个序列，需要满足：</p><ol><li><code>len(s1)</code> 至多为9（同种花色至多9种，如果是字牌，则为7）</li><li>s2 的长度为 <code>len(s1) - 1</code></li><li>对于数牌，s2 序列的总和不超过8（$\infty$视为其最小值3）</li><li>对于字牌，s2 的元素总为 $\infty$</li></ol><p>仅这样做仍有非常大的组合数量，实际上我们还能去重。我们可以通过 $\infty$ 将序列分割为多个子序列，容易发现：</p><ol><li>这些子序列任意交换顺序，不影响向听数。</li><li>对每个子序列进行倒序排列，不影响向听数。</li></ol><p>举两个例子：</p><p><strong>示例1：</strong></p><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1gxUeKE9JIHEL3_MuR1fydbGsySiLggqH_3man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1V82cfeDAiXPvzcpFPEojV92PjsdTogvc_4man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Ey9qrF-MoijCsFcy2Sjo1XS5Mv7Rig50_8man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"></div></div><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1OzKuJ1axvOfj2Yw6E_o_V3NJxxM70XYf_2man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1OzKuJ1axvOfj2Yw6E_o_V3NJxxM70XYf_2man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Fy-ARC3N_5Lccv-AfKBDUR8VNqC1KJhc_7man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Ey9qrF-MoijCsFcy2Sjo1XS5Mv7Rig50_8man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"></div></div><p>上面两手牌的枚数序列分别是：</p><div class="note primary simple"><p>3 1 1 1, 1 2</p><p>1 2, 3 1 1 1</p></div><p>逗号表示子序列的分割符（对应位置有一个距离为 $\infty$​ 的跳跃）。这即为「不同子序列的换序」，这种操作不影响向听数。</p><p><strong>示例2：</strong></p><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1gxUeKE9JIHEL3_MuR1fydbGsySiLggqH_3man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1V82cfeDAiXPvzcpFPEojV92PjsdTogvc_4man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Ey9qrF-MoijCsFcy2Sjo1XS5Mv7Rig50_8man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"></div></div><div class="mahjong-wrap"><div class="mahjong-inner"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_13Ml7kUgPHkVnlIubvQZWFnGxxxqqa0zy_1man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1OzKuJ1axvOfj2Yw6E_o_V3NJxxM70XYf_2man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1gxUeKE9JIHEL3_MuR1fydbGsySiLggqH_3man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1WU2lJRk6MKjJ-PRiZxEXZbNLzO1OgyNm_5man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1Ey9qrF-MoijCsFcy2Sjo1XS5Mv7Rig50_8man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"><img class="mahjong-tile" src="https://pic.sl.al/gdrive/pic/2026-04-08/fileid_1J_k1UGl-3CWn2_5E5FhlOy_mI3Y13q_I_9man.png"></div></div><p>上面两手牌的枚数序列分别是：</p><div class="note primary simple"><p>3 1 1 1, 1 2</p><p>1 1 1 3, 1 2</p></div><p>这即为「子序列的倒序」，同样不影响向听数。</p><hr><p>我们可以根据这两点，定义一个序列间的偏序关系，从而对每个序列进行标准化。比如，可以定义其偏序关系为依次比较其枚数序列的字典序、其距离序列的字典序。</p><h3 id="枚举与DFS"><a href="#枚举与DFS" class="headerlink" title="枚举与DFS"></a>枚举与DFS</h3><p>这一部分比较容易理解。</p><p>分别枚举各个花色的总枚数，生成其枚数的分拆，对每一种分拆枚举可行的距离序列。</p><p>对每一对枚数、距离序列，通过DFS得到所有可行的参数组合，再把四种花色的参数组合起来，调用前面的向听数函数计算得到向听数，取其最小值即为序列的向听数。</p><p>囿于篇幅这里省略了相关代码。</p><p>由于编码优化的不是很好，最终得到了一个30mb的压缩包（</p><hr><p>最后嫖了一个huggingface space，把小程序后端部署起来了。本文相关代码可以在<a href="https://huggingface.co/spaces/windshadow/riichi-toolkit/tree/main">这个space</a>中找到。</p><p>欢迎各位立直麻将爱好者使用<del>帮我测试bug</del></p><p><img src="https://blogfiles.oss.fyz666.xyz/jpg/5e5a4fb4-2b90-4db2-8ae8-b61713396749.jpg" alt=""></p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E7%AB%8B%E7%9B%B4%E9%BA%BB%E5%B0%86/">立直麻将</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E7%AE%97%E6%B3%95/">算法</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/13157/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>经典红蓝眼睛问题的思考</title>
      <link>https://blog.fyz666.xyz/blog/13093/</link>
      <guid>https://blog.fyz666.xyz/blog/13093/</guid>
      <pubDate>Mon, 20 Oct 2025 07:17:08 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;前两天偶然间想起一个经典的红蓝眼睛问题，这个问题最早是&lt;a href=&quot;https://terrytao.wordpress.com/2008/02/05/the-blue-eyed-islanders-puzzle/&quot;&gt;由Terence</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>前两天偶然间想起一个经典的红蓝眼睛问题，这个问题最早是<a href="https://terrytao.wordpress.com/2008/02/05/the-blue-eyed-islanders-puzzle/">由Terence Tao提出的</a>，其表述大概如下：</p><div class="note primary simple"><p>一个岛屿上住着一群土著，这些土著的眼睛均为红色或蓝色，其中有 $m$ 个红眼睛、 $n$ 个蓝眼睛。由于某种宗教的存在，这些土著之间遵循着一些约定俗成的规则：</p><ol><li>土著们都不知道自己眼睛的颜色。</li><li>土著们不会通过任何方式告知其他人任何人眼睛的颜色。</li><li>如果有人知道了自己眼睛的颜色，他一定会在当天晚上自杀。</li></ol><p>一天，有一位探险家经岛屿所属政府的批准登上了岛屿，在登岛前，为保护岛上的土著，探险家被告知「不可以给这个岛屿上的土著们带去他们以前所不知道的信息」。</p><p>登岛后，探险家发现岛上红眼睛的土著有不止一个（$m\ge2$），因此，探险家心想：“每个人都知道岛上有红眼睛的人”，于是探险家对着人群大喊：“你们当中有红眼睛的人！”</p><p>“这样应该没有给土著们带去额外的信息吧”，探险家心想。</p><p>然而真的是这样吗？假设岛上的土著都十分擅长逻辑推理，考虑探险家这句话带来的后果。</p></div><hr><p>相信很多朋友都听过这个问题，也很清楚答案：</p><div class="note primary simple"><p>第 $m$ 天晚上，$m$ 个红眼睛土著自杀，第 $m+1$ 天晚上， $n$ 个蓝眼睛土著自杀。</p></div><p>我在一开始把问题想的简单了，我的思考如下：</p><p>考虑 $m=2$ 的情形，此时共有两个红眼睛土著，记为 $R_1,R_2$，从他们各自的视角来看，外面均还有一个红眼睛，此时，以 $R_1$ 的视角来看这个场景，由于 $R_1$ 并不知道自己是红还是蓝，因此，在 $R_1$ 看来，外面的 $R_2$：<strong>可能并不知道</strong>「有红眼睛的人」这件事（如果我 $R_1$ 是蓝的， $R_2$ 可不就是不知道嘛），所以，当探险家喊出那句知识的时候，其实还带给了 $R_1$ 一个额外的信息，即：“现在 $R_2$ 已经知道「有红眼睛的人」这件事了”，那么 $R_1$ 心想：“如果我是蓝眼睛，在 $R_2$ 视角下场上将没有红眼睛，此时 $R_2$ 足够聪明，他会立刻意识到自己是场上唯一一个红眼睛的人，也即 <strong>$R_2$ 会在第一天晚上自杀</strong>”。</p><p>对 $R_2$ 而言，同样如此，因此两人等了一天，发现第一天晚上无事发生。此时足够聪明的二位在第二天同时意识到了自己是红眼睛，因此，他们会在第二天晚上自杀。</p><p>接下来，通过数学归纳法，“容易”推导出对一切 $m=k$，有结论： $k$ 个红眼睛会在第 $k$ 天晚上自杀，而剩下的蓝眼睛则会在下一天意识到自己是蓝眼睛，因此会在 $k+1$ 天晚上自杀。</p><p>归纳如下，假设结论已经对 $m=k$ 成立，我们来思考 $m=k+1$。</p><p><strong>下面是错误证明</strong></p><p>对每一个红眼睛土著而言， 外面都能看到 $k$ 个红眼睛土著，那么在他们视角看来，外面的环境满足 $m=k$ 情形 （满足吗？？？），因此他们只需等待 $k$ 天，若外面的 $k$ 人没有在第 $k$ 天晚上自杀，这每个红眼睛土著都会意识到自己是红眼睛，因此会在第 $k+1$ 天晚上自杀。</p><p>这个证明显然是错的，因为在这个证明中，除了 $k=2$ 时用到的那个额外信息（每个人都知道有红眼睛的人）之外，没有用到任何其他信息。如果这个证明成立，那么当岛上的红眼睛数量大于2时，即使没有探险家的那句话，这些人也必然会在一定天数后自杀，因为他们不需要探险家的喊话就能意识到“每个人都知道有红眼睛的人”，从而导致连锁反应。</p><p>显然探险家的喊话带给人群的信息不止是前面所述内容。</p><hr><p>那么 $m=3$ 和 $m=2$ 的区别在哪呢？多了这一个红眼睛的人后，$R_1$ 先天就知道 “$R_2$ 知道「有红眼睛的人」这件事”，因为在 $R_1$ 看来， $R_2$ 的视角下，至少有 $R_3$ 的存在。</p><p>此时 $R_1$ 想要站在上帝视角来旁观 $R_2$ 和 $R_3$ 的行为，从而能够根据他俩的行为来推断自己的眼睛颜色，需要满足什么条件呢？</p><p>回想之前的 $m=2$ 情形，我们清楚一个结论：如果 $R_1$ 是蓝眼睛，那么当 $R_2$ 知道 “$R_3$ 知道「有红眼睛的人」这件事”时，这个 $m=2$ 状态将会发生坍塌。</p><p>那么只要把 $R_1$ 放到我们的位置， $R_1$ 就相当于站在了上帝视角。此时 $R_1$ 相比于之前人群中的 $R_1$ 而言，多获取到了一个知识：<strong>$R_2$ 知道 “$R_3$ 知道「有红眼睛的人」这件事”</strong>。在这个知识的加持下， $R_1$ 能够确定只要外面的两人在第二天晚上没有自杀，自己必然是红眼睛。同理，这个结论对 $R_2,R_3$ 也成立。</p><p>而上面所说的知识显然也可以从探险家的那句喊话中得到。所以，我们就从 $m=2$ 的情形推出了 $m=3$ 的情形。</p><hr><p>从前面的思考中，我发现了这个有点像多层嵌套的知识的结构，不如将它称为“多阶知识”。</p><p>这个场景下，一阶知识就是探险家喊出的那句话：“你们当中有红眼睛的人！” 提供的直接知识：<strong>“有红眼睛的人”</strong>，这个一阶知识 （在$m\ge2$时） 显然每个人都知道，探险家正是因为这个原因，以为自己的喊话没有提供额外的信息。但他<strong>忽略了喊话行为带来的高阶知识</strong>。</p><p>这个知识对应的二阶知识是<strong>“每个人都知道有红眼睛的人”</strong>，三阶知识是<strong>“每个人都知道每个人都知道有红眼睛的人”</strong>，以此类推。</p><p>重新考虑前面的数学归纳过程，同样的，假设结论已经对 $m=k$ 成立，我们重新来思考 $m=k+1$。</p><p>对每一个红眼睛土著而言， 外面都能看到 $k$ 个红眼睛土著，那么在他们视角看来，外面的环境差不多可以满足 $m=k$ 情形，只需要一个额外的条件。</p><p>显然，$m=k$ 时结论成立，需要这 $k$ 个人同时知道原知识的 $k$ 阶知识：<strong>“每个人都知道每个人都知道…每个人都知道有红眼睛的人” （共 $k-1$ 个“每个人都知道”）</strong>。</p><p>现在我们继续站在 $R_1$ 的视角考虑，外面的 $k$ 个红眼睛如果知道上面的 $k$ 阶知识，就可以从这 $k$ 个人的行为推出自己眼睛的颜色。因此这里 $R_1$ 只要获取一个下面这样的知识，就可以站在上帝视角去观察外界的行为。这个知识即：</p><div class="note info simple"><p>外面的 $k$ 人知道 $k$ 阶知识。</p></div><p>而原知识的 $k+1$ 阶知识相当于<strong>每个人都知道 $k$ 阶知识</strong>，它自然包含了前面 $R_1$ 需要获取的知识。而这个 $k+1$ 阶知识显然也可以从探险家的喊话中推导出。</p><p>接下来就和之前一模一样了。</p><hr><p>这个问题其实抽象出来了两个概念：<strong>共有知识</strong>与<strong>公共知识</strong>，一个群体中，大家都知道的结论被称为<strong>共有知识</strong>，但如果有一个足够权威的人将这个<strong>共有知识</strong>公之于众，则它会变成一个<strong>公共知识</strong>。从这个问题中可以看出，一个公共知识相当于一个无穷高阶的知识，而一个共有知识，仅仅相当于一个一阶知识。</p><hr><p>当然，如果群众里面有一个傻子，结论就失效了😊。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E6%9D%82%E8%B6%A3/">杂趣</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E9%80%BB%E8%BE%91/">逻辑</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%95%B0%E5%AD%A6/">数学</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/13093/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>部署Qwen3-32B模型并使用Cherry Studio优雅接入的一些尝试</title>
      <link>https://blog.fyz666.xyz/blog/13005/</link>
      <guid>https://blog.fyz666.xyz/blog/13005/</guid>
      <pubDate>Sat, 30 Aug 2025 15:55:22 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;前两天尝试在一块3090上部署了一个QwQ-32B（见下面这篇文章）。&lt;/p&gt;
&lt;a class=&quot;tag-Link&quot; target=&quot;_blank&quot; href=&quot;/blog/12935&quot;&gt;
    &lt;div</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>前两天尝试在一块3090上部署了一个QwQ-32B（见下面这篇文章）。</p><a class="tag-Link" target="_blank" href="/blog/12935">    <div class="tag-link-tips">引用本站资源，请放心访问</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="/images/avatar.webp" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">在消费级显卡上部署QwQ-32B模型</div>            <div class="tag-link-sitename">逸风亭博客</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>不过这个模型是一个推理模型（Reasoning model），它面对任何问题都会“陷入沉思”，思考过后之后才给出回答，对于某些需要快速响应但比较简单的任务而言就有点令人不耐烦了，因此，后来我又重新尝试了其他的模型，例如Qwen3-32B，这个模型则是个混合推理模型（Hybrid reasoning model），也就是说这个模型存在两种模式：思考模型与非思考模式，可针对复杂问题启用多步逻辑推理，简单任务则快速响应。</p><hr><h2 id="模型部署"><a href="#模型部署" class="headerlink" title="模型部署"></a>模型部署</h2><p>这部分总体与上面那篇文章中部署QwQ的过程类似，使用<code>llama_server</code>进行部署，不过为了与Cherry Studio良好适配，我还进行了一些调整。</p><p>首先是到处乱抄糊了一个jinja模板，命名为了<code>my-template.jinja</code></p><pre class="line-numbers language-django" data-language="django"><code class="language-django"><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">messages</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">'system'</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>system\n'</span> <span class="token operator">+</span> <span class="token variable">messages</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token operator">+</span> <span class="token string">'&lt;|im_end|>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">ns</span> <span class="token operator">=</span> <span class="token function">namespace</span><span class="token punctuation">(</span><span class="token variable">multi_step_tool</span><span class="token operator">=</span><span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token variable">last_query_index</span><span class="token operator">=</span><span class="token variable">messages</span><span class="token operator">|</span><span class="token filter function">length</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">for</span> <span class="token variable">forward_message</span> <span class="token keyword">in</span> <span class="token variable">messages</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">index</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token variable">messages</span><span class="token operator">|</span><span class="token filter function">length</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">index0</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">message</span> <span class="token operator">=</span> <span class="token variable">messages</span><span class="token punctuation">[</span><span class="token variable">index</span><span class="token punctuation">]</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">current_content</span> <span class="token operator">=</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token keyword">if</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token test function">none</span> <span class="token keyword">else</span> <span class="token string">''</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">tool_start</span> <span class="token operator">=</span> <span class="token string">'&lt;tool_response>'</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">tool_start_length</span> <span class="token operator">=</span> <span class="token variable">tool_start</span><span class="token operator">|</span><span class="token filter function">length</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">start_of_message</span> <span class="token operator">=</span> <span class="token variable">current_content</span><span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token variable">tool_start_length</span><span class="token punctuation">]</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">tool_end</span> <span class="token operator">=</span> <span class="token string">'&lt;/tool_response>'</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">tool_end_length</span> <span class="token operator">=</span> <span class="token variable">tool_end</span><span class="token operator">|</span><span class="token filter function">length</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">start_pos</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token variable">current_content</span><span class="token operator">|</span><span class="token filter function">length</span><span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token variable">tool_end_length</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">start_pos</span> <span class="token operator">&lt;</span> <span class="token number">0</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">start_pos</span> <span class="token operator">=</span> <span class="token number">0</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">end_of_message</span> <span class="token operator">=</span> <span class="token variable">current_content</span><span class="token punctuation">[</span><span class="token variable">start_pos</span><span class="token punctuation">:</span><span class="token punctuation">]</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">ns</span><span class="token punctuation">.</span><span class="token variable">multi_step_tool</span> <span class="token keyword">and</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"user"</span> <span class="token keyword">and</span> <span class="token function">not</span><span class="token punctuation">(</span><span class="token variable">start_of_message</span> <span class="token operator">==</span> <span class="token variable">tool_start</span> <span class="token keyword">and</span> <span class="token variable">end_of_message</span> <span class="token operator">==</span> <span class="token variable">tool_end</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">ns</span><span class="token punctuation">.</span><span class="token variable">multi_step_tool</span> <span class="token operator">=</span> <span class="token boolean">false</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">ns</span><span class="token punctuation">.</span><span class="token variable">last_query_index</span> <span class="token operator">=</span> <span class="token variable">index</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endfor</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">for</span> <span class="token variable">message</span> <span class="token keyword">in</span> <span class="token variable">messages</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"user"</span><span class="token punctuation">)</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"system"</span> <span class="token keyword">and</span> <span class="token keyword">not</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">first</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token operator">+</span> <span class="token string">'&lt;|im_end|>'</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">elif</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"assistant"</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">content</span> <span class="token operator">=</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">reasoning_content</span> <span class="token operator">=</span> <span class="token string">''</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">reasoning_content</span> <span class="token keyword">is</span> <span class="token test function">defined</span> <span class="token keyword">and</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">reasoning_content</span> <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token test function">none</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">reasoning_content</span> <span class="token operator">=</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">reasoning_content</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">else</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token string">'&lt;/think>'</span> <span class="token keyword">in</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">content</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'&lt;/think>'</span><span class="token punctuation">)</span><span class="token operator">|</span><span class="token filter function">last</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">lstrip</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">reasoning_content</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'&lt;/think>'</span><span class="token punctuation">)</span><span class="token operator">|</span><span class="token filter function">first</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">rstrip</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">reasoning_content</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token variable">reasoning_content</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'&lt;think>'</span><span class="token punctuation">)</span><span class="token operator">|</span><span class="token filter function">last</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">lstrip</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">index0</span> <span class="token operator">></span> <span class="token variable">ns</span><span class="token punctuation">.</span><span class="token variable">last_query_index</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">last</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token keyword">not</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">last</span> <span class="token keyword">and</span> <span class="token variable">reasoning_content</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">+</span> <span class="token string">'\n&lt;think>\n'</span> <span class="token operator">+</span> <span class="token variable">reasoning_content</span><span class="token punctuation">.</span><span class="token function">strip</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">'\n&lt;/think>\n\n'</span> <span class="token operator">+</span> <span class="token variable">content</span><span class="token punctuation">.</span><span class="token function">lstrip</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">else</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token operator">+</span> <span class="token variable">content</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">else</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token operator">+</span> <span class="token variable">content</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">tool_calls</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">for</span> <span class="token variable">tool_call</span> <span class="token keyword">in</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">tool_calls</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">first</span> <span class="token keyword">and</span> <span class="token variable">content</span><span class="token punctuation">)</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token keyword">not</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">first</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>                    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">function</span> <span class="token delimiter punctuation">%&#125;</span></span>                    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">tool_call</span> <span class="token operator">=</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">function</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;tool_call>\n&#123;"name": "'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">name</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'", "arguments": '</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">arguments</span> <span class="token keyword">is</span> <span class="token test function">string</span> <span class="token delimiter punctuation">%&#125;</span></span>                    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">arguments</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">else</span> <span class="token delimiter punctuation">%&#125;</span></span>                    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">arguments</span> <span class="token operator">|</span> <span class="token variable">tojson</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&#125;\n&lt;/tool_call>'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endfor</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_end|>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">elif</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"tool"</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">first</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token variable">messages</span><span class="token punctuation">[</span><span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">index0</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">!=</span> <span class="token string">"tool"</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>user'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'\n&lt;tool_response>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'\n&lt;/tool_response>'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">last</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token variable">messages</span><span class="token punctuation">[</span><span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">index0</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">!=</span> <span class="token string">"tool"</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_end|>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endfor</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">add_generation_prompt</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>assistant\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">enable_thinking</span> <span class="token keyword">is</span> <span class="token test function">defined</span> <span class="token keyword">and</span> <span class="token variable">enable_thinking</span> <span class="token keyword">is</span> <span class="token test function">false</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;think>\n\n&lt;/think>\n\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>可以注意到主要是在提示词中拼接了工具的调用过程（即和function call相关）。另外还有最后增加了一个thinking mode的开关：<code>enable_thinking</code>，通过指定此参数为<code>False</code>，可以直接让<code>&lt;think&gt;</code>标签对闭合，以使得模型跳过思考。</p><hr><p>接下来是启动文件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token shebang important">#!/usr/bin/env bash</span>llama-server <span class="token punctuation">\</span>    <span class="token parameter variable">--model</span> <span class="token string">"./model/bartowski/Qwen_Qwen3-32B-GGUF/Qwen_Qwen3-32B-IQ4_XS.gguf"</span> <span class="token punctuation">\</span>    <span class="token parameter variable">--alias</span> <span class="token string">"Qwen3-32B"</span> <span class="token punctuation">\</span>    --n-gpu-layers <span class="token number">65</span> <span class="token punctuation">\</span>    --ctx-size <span class="token number">49152</span> <span class="token punctuation">\</span>    --context-shift <span class="token punctuation">\</span>    <span class="token parameter variable">--parallel</span> <span class="token number">3</span> <span class="token punctuation">\</span>    --cache-type-k q8_0 <span class="token punctuation">\</span>    --cache-type-v q8_0 <span class="token punctuation">\</span>    <span class="token parameter variable">--threads</span> <span class="token number">16</span> <span class="token punctuation">\</span>    --flash-attn <span class="token punctuation">\</span>    <span class="token parameter variable">--mlock</span> <span class="token punctuation">\</span>    --n-predict <span class="token parameter variable">-1</span> <span class="token punctuation">\</span>    <span class="token parameter variable">--jinja</span> <span class="token punctuation">\</span>    --chat-template-file ./chat_templates/my-template.jinja<span class="token punctuation">\</span>    <span class="token parameter variable">--props</span> <span class="token punctuation">\</span>    <span class="token parameter variable">-cb</span> <span class="token punctuation">\</span>    <span class="token parameter variable">--host</span> <span class="token number">127.0</span>.0.1 <span class="token punctuation">\</span>    <span class="token parameter variable">--port</span> <span class="token number">18000</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>稍做了一些修改，例如把监听的地址改成了本地，后面我还专门写了一个代理脚本作为中间层供非本地交互。</p><hr><p>代理脚本如下：<code>llm_proxy.py</code>：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> os<span class="token keyword">import</span> json<span class="token keyword">from</span> typing <span class="token keyword">import</span> Optional<span class="token punctuation">,</span> Set<span class="token keyword">from</span> fastapi <span class="token keyword">import</span> FastAPI<span class="token punctuation">,</span> Request<span class="token punctuation">,</span> Header<span class="token punctuation">,</span> HTTPException<span class="token keyword">from</span> fastapi<span class="token punctuation">.</span>responses <span class="token keyword">import</span> JSONResponse<span class="token punctuation">,</span> StreamingResponse<span class="token punctuation">,</span> Response<span class="token keyword">import</span> httpxUPSTREAM_BASE <span class="token operator">=</span> <span class="token string">"http://127.0.0.1:18000"</span>PROXY_API_KEYS <span class="token operator">=</span> <span class="token builtin">set</span><span class="token punctuation">(</span><span class="token punctuation">)</span>UPSTREAM_API_KEY <span class="token operator">=</span> <span class="token string">""</span> <span class="token comment"># 可选, 若上游需要鉴权则设置</span>app <span class="token operator">=</span> FastAPI<span class="token punctuation">(</span>title<span class="token operator">=</span><span class="token string">"LLM Proxy"</span><span class="token punctuation">)</span>_api_keys<span class="token punctuation">:</span> Set<span class="token punctuation">[</span><span class="token builtin">str</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token builtin">set</span><span class="token punctuation">(</span><span class="token punctuation">)</span>_api_keys_file <span class="token operator">=</span> <span class="token string">"./.llama_api_keys"</span><span class="token keyword">def</span> <span class="token function">read_api_keys</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> Set<span class="token punctuation">[</span><span class="token builtin">str</span><span class="token punctuation">]</span><span class="token punctuation">:</span>    keys <span class="token operator">=</span> <span class="token builtin">set</span><span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>_api_keys_file<span class="token punctuation">,</span> <span class="token string">"r"</span><span class="token punctuation">,</span> encoding<span class="token operator">=</span><span class="token string">"utf-8"</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>        <span class="token keyword">for</span> line <span class="token keyword">in</span> f<span class="token punctuation">:</span>            k <span class="token operator">=</span> line<span class="token punctuation">.</span>strip<span class="token punctuation">(</span><span class="token punctuation">)</span>            <span class="token keyword">if</span> k<span class="token punctuation">:</span>                keys<span class="token punctuation">.</span>add<span class="token punctuation">(</span>k<span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"[INFO] Loaded API keys: </span><span class="token interpolation"><span class="token punctuation">&#123;</span><span class="token builtin">len</span><span class="token punctuation">(</span>keys<span class="token punctuation">)</span><span class="token punctuation">&#125;</span></span><span class="token string"> keys"</span></span><span class="token punctuation">)</span>    <span class="token keyword">return</span> keys_api_keys <span class="token operator">=</span> read_api_keys<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">check_auth</span><span class="token punctuation">(</span>auth<span class="token punctuation">:</span> <span class="token builtin">str</span> <span class="token operator">|</span> <span class="token boolean">None</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">if</span> <span class="token keyword">not</span> _api_keys<span class="token punctuation">:</span>        <span class="token keyword">return</span>  <span class="token comment"># 未设置则不启用代理鉴权</span>    <span class="token keyword">if</span> <span class="token keyword">not</span> auth <span class="token keyword">or</span> <span class="token keyword">not</span> auth<span class="token punctuation">.</span>startswith<span class="token punctuation">(</span><span class="token string">"Bearer "</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">raise</span> HTTPException<span class="token punctuation">(</span>status_code<span class="token operator">=</span><span class="token number">401</span><span class="token punctuation">,</span> detail<span class="token operator">=</span><span class="token string">"Missing Authorization Bearer"</span><span class="token punctuation">)</span>    <span class="token keyword">if</span> auth<span class="token punctuation">.</span>split<span class="token punctuation">(</span><span class="token string">" "</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>strip<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">not</span> <span class="token keyword">in</span> _api_keys<span class="token punctuation">:</span>        <span class="token keyword">raise</span> HTTPException<span class="token punctuation">(</span>status_code<span class="token operator">=</span><span class="token number">403</span><span class="token punctuation">,</span> detail<span class="token operator">=</span><span class="token string">"Invalid API key"</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">upstream_headers</span><span class="token punctuation">(</span>orig_auth<span class="token punctuation">:</span> <span class="token builtin">str</span> <span class="token operator">|</span> <span class="token boolean">None</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    h <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token string">"Content-Type"</span><span class="token punctuation">:</span> <span class="token string">"application/json"</span><span class="token punctuation">&#125;</span>    <span class="token keyword">if</span> UPSTREAM_API_KEY<span class="token punctuation">:</span>        h<span class="token punctuation">[</span><span class="token string">"Authorization"</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"Bearer </span><span class="token interpolation"><span class="token punctuation">&#123;</span>UPSTREAM_API_KEY<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span>    <span class="token keyword">return</span> h<span class="token keyword">def</span> <span class="token function">_should_stream</span><span class="token punctuation">(</span>path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> payload<span class="token punctuation">:</span> <span class="token builtin">dict</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">bool</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""仅对 chat/completions 和 completions 且 stream=true 的 POST 开启 SSE 透传。"""</span>    p <span class="token operator">=</span> path<span class="token punctuation">.</span>lower<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token string">"chat/completions"</span> <span class="token keyword">in</span> p <span class="token keyword">or</span> p<span class="token punctuation">.</span>endswith<span class="token punctuation">(</span><span class="token string">"/completions"</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">and</span> <span class="token builtin">isinstance</span><span class="token punctuation">(</span>payload<span class="token punctuation">,</span> <span class="token builtin">dict</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">return</span> <span class="token builtin">bool</span><span class="token punctuation">(</span>payload<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"stream"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> <span class="token boolean">False</span><span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>get</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">root</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">return</span> <span class="token punctuation">&#123;</span><span class="token string">"name"</span><span class="token punctuation">:</span> <span class="token string">"LLM Proxy"</span><span class="token punctuation">,</span> <span class="token string">"upstream"</span><span class="token punctuation">:</span> UPSTREAM_BASE<span class="token punctuation">,</span> <span class="token string">"v1_proxy"</span><span class="token punctuation">:</span> <span class="token boolean">True</span><span class="token punctuation">&#125;</span><span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>get</span><span class="token punctuation">(</span><span class="token string">"/props"</span><span class="token punctuation">)</span><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">proxy_props</span><span class="token punctuation">(</span>request<span class="token punctuation">:</span> Request<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">async</span> <span class="token keyword">with</span> httpx<span class="token punctuation">.</span>AsyncClient<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">as</span> client<span class="token punctuation">:</span>        resp <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span>get<span class="token punctuation">(</span>            <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">&#123;</span>UPSTREAM_BASE<span class="token punctuation">&#125;</span></span><span class="token string">/props"</span></span><span class="token punctuation">,</span>            params<span class="token operator">=</span>request<span class="token punctuation">.</span>query_params<span class="token punctuation">,</span>            headers<span class="token operator">=</span>request<span class="token punctuation">.</span>headers        <span class="token punctuation">)</span>    <span class="token keyword">return</span> JSONResponse<span class="token punctuation">(</span>status_code<span class="token operator">=</span>resp<span class="token punctuation">.</span>status_code<span class="token punctuation">,</span> content<span class="token operator">=</span>resp<span class="token punctuation">.</span>json<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>get</span><span class="token punctuation">(</span><span class="token string">"/health"</span><span class="token punctuation">)</span><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">proxy_props</span><span class="token punctuation">(</span>request<span class="token punctuation">:</span> Request<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">async</span> <span class="token keyword">with</span> httpx<span class="token punctuation">.</span>AsyncClient<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">as</span> client<span class="token punctuation">:</span>        resp <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span>get<span class="token punctuation">(</span>            <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">&#123;</span>UPSTREAM_BASE<span class="token punctuation">&#125;</span></span><span class="token string">/health"</span></span><span class="token punctuation">,</span>            params<span class="token operator">=</span>request<span class="token punctuation">.</span>query_params<span class="token punctuation">,</span>            headers<span class="token operator">=</span>request<span class="token punctuation">.</span>headers        <span class="token punctuation">)</span>    <span class="token keyword">return</span> JSONResponse<span class="token punctuation">(</span>status_code<span class="token operator">=</span>resp<span class="token punctuation">.</span>status_code<span class="token punctuation">,</span> content<span class="token operator">=</span>resp<span class="token punctuation">.</span>json<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">modify_post_payload</span><span class="token punctuation">(</span>payload<span class="token punctuation">)</span><span class="token punctuation">:</span>    ctk <span class="token operator">=</span> payload<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"chat_template_kwargs"</span><span class="token punctuation">)</span>    <span class="token keyword">if</span> <span class="token keyword">not</span> <span class="token builtin">isinstance</span><span class="token punctuation">(</span>ctk<span class="token punctuation">,</span> <span class="token builtin">dict</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        ctk <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span>    chat_template_kwargs <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">'enable_thinking'</span><span class="token punctuation">]</span>    <span class="token keyword">for</span> k <span class="token keyword">in</span> chat_template_kwargs<span class="token punctuation">:</span>        <span class="token keyword">if</span> k <span class="token keyword">in</span> payload<span class="token punctuation">:</span>            v <span class="token operator">=</span> payload<span class="token punctuation">[</span>k<span class="token punctuation">]</span>            <span class="token keyword">if</span> v <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token boolean">None</span><span class="token punctuation">:</span>                ctk<span class="token punctuation">[</span>k<span class="token punctuation">]</span> <span class="token operator">=</span> v    payload<span class="token punctuation">[</span><span class="token string">"chat_template_kwargs"</span><span class="token punctuation">]</span> <span class="token operator">=</span> ctk    <span class="token keyword">return</span> payload<span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>api_route</span><span class="token punctuation">(</span><span class="token string">"/v1/&#123;full_path:path&#125;"</span><span class="token punctuation">,</span> methods<span class="token operator">=</span><span class="token punctuation">[</span><span class="token string">"GET"</span><span class="token punctuation">,</span> <span class="token string">"POST"</span><span class="token punctuation">,</span> <span class="token string">"PUT"</span><span class="token punctuation">,</span> <span class="token string">"PATCH"</span><span class="token punctuation">,</span> <span class="token string">"DELETE"</span><span class="token punctuation">,</span> <span class="token string">"OPTIONS"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">v1_catch_all</span><span class="token punctuation">(</span>    request<span class="token punctuation">:</span> Request<span class="token punctuation">,</span>    full_path<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span>    authorization<span class="token punctuation">:</span> Optional<span class="token punctuation">[</span><span class="token builtin">str</span><span class="token punctuation">]</span> <span class="token operator">=</span> Header<span class="token punctuation">(</span>default<span class="token operator">=</span><span class="token boolean">None</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""    原样转发 /v1/** 的所有请求到上游。    - JSON 请求：优先使用 json= 传递（便于上游解析）。    - multipart/二进制：透传 content。    - chat/completions &amp; completions + stream=true：SSE 流式透传。    """</span>    check_auth<span class="token punctuation">(</span>authorization<span class="token punctuation">)</span>    upstream_url <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">&#123;</span>UPSTREAM_BASE<span class="token punctuation">&#125;</span></span><span class="token string">/v1/</span><span class="token interpolation"><span class="token punctuation">&#123;</span>full_path<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span>    method <span class="token operator">=</span> request<span class="token punctuation">.</span>method<span class="token punctuation">.</span>upper<span class="token punctuation">(</span><span class="token punctuation">)</span>    params <span class="token operator">=</span> <span class="token builtin">dict</span><span class="token punctuation">(</span>request<span class="token punctuation">.</span>query_params<span class="token punctuation">)</span>    raw_body <span class="token operator">=</span> <span class="token keyword">await</span> request<span class="token punctuation">.</span>body<span class="token punctuation">(</span><span class="token punctuation">)</span>    ct_req <span class="token operator">=</span> request<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"content-type"</span><span class="token punctuation">,</span> <span class="token string">""</span><span class="token punctuation">)</span>    headers <span class="token operator">=</span> upstream_headers<span class="token punctuation">(</span>authorization<span class="token punctuation">)</span>    fwd_headers <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token operator">**</span>headers<span class="token punctuation">&#125;</span>    <span class="token keyword">if</span> ct_req<span class="token punctuation">:</span>        fwd_headers<span class="token punctuation">[</span><span class="token string">"Content-Type"</span><span class="token punctuation">]</span> <span class="token operator">=</span> ct_req    payload<span class="token punctuation">:</span> Optional<span class="token punctuation">[</span><span class="token builtin">dict</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token boolean">None</span>    <span class="token keyword">if</span> <span class="token string">"application/json"</span> <span class="token keyword">in</span> ct_req<span class="token punctuation">.</span>lower<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">and</span> raw_body<span class="token punctuation">:</span>        <span class="token keyword">try</span><span class="token punctuation">:</span>            payload <span class="token operator">=</span> json<span class="token punctuation">.</span>loads<span class="token punctuation">(</span>raw_body<span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token string">"utf-8"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>        <span class="token keyword">except</span> Exception<span class="token punctuation">:</span>            payload <span class="token operator">=</span> <span class="token boolean">None</span>    <span class="token keyword">if</span> payload <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token boolean">None</span> <span class="token keyword">and</span> <span class="token builtin">isinstance</span><span class="token punctuation">(</span>payload<span class="token punctuation">,</span> <span class="token builtin">dict</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        payload <span class="token operator">=</span> modify_post_payload<span class="token punctuation">(</span>payload<span class="token punctuation">)</span>    <span class="token keyword">if</span> method <span class="token operator">==</span> <span class="token string">"POST"</span> <span class="token keyword">and</span> payload <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token boolean">None</span> <span class="token keyword">and</span> _should_stream<span class="token punctuation">(</span>full_path<span class="token punctuation">,</span> payload<span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">iter_stream</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>            <span class="token keyword">async</span> <span class="token keyword">with</span> httpx<span class="token punctuation">.</span>AsyncClient<span class="token punctuation">(</span>timeout<span class="token operator">=</span><span class="token boolean">None</span><span class="token punctuation">)</span> <span class="token keyword">as</span> client<span class="token punctuation">:</span>                <span class="token keyword">async</span> <span class="token keyword">with</span> client<span class="token punctuation">.</span>stream<span class="token punctuation">(</span>                    <span class="token string">"POST"</span><span class="token punctuation">,</span>                    upstream_url<span class="token punctuation">,</span>                    headers<span class="token operator">=</span>fwd_headers<span class="token punctuation">,</span>                    json<span class="token operator">=</span>payload<span class="token punctuation">,</span>                    params<span class="token operator">=</span>params<span class="token punctuation">,</span>                <span class="token punctuation">)</span> <span class="token keyword">as</span> r<span class="token punctuation">:</span>                    r<span class="token punctuation">.</span>raise_for_status<span class="token punctuation">(</span><span class="token punctuation">)</span>                    <span class="token keyword">async</span> <span class="token keyword">for</span> chunk <span class="token keyword">in</span> r<span class="token punctuation">.</span>aiter_bytes<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>                        <span class="token keyword">yield</span> chunk        <span class="token keyword">return</span> StreamingResponse<span class="token punctuation">(</span>iter_stream<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> media_type<span class="token operator">=</span><span class="token string">"text/event-stream"</span><span class="token punctuation">)</span>    <span class="token keyword">async</span> <span class="token keyword">with</span> httpx<span class="token punctuation">.</span>AsyncClient<span class="token punctuation">(</span>timeout<span class="token operator">=</span><span class="token boolean">None</span><span class="token punctuation">)</span> <span class="token keyword">as</span> client<span class="token punctuation">:</span>        kwargs <span class="token operator">=</span> <span class="token builtin">dict</span><span class="token punctuation">(</span>url<span class="token operator">=</span>upstream_url<span class="token punctuation">,</span> headers<span class="token operator">=</span>fwd_headers<span class="token punctuation">,</span> params<span class="token operator">=</span>params<span class="token punctuation">)</span>        <span class="token keyword">if</span> method <span class="token keyword">in</span> <span class="token punctuation">&#123;</span><span class="token string">"POST"</span><span class="token punctuation">,</span> <span class="token string">"PUT"</span><span class="token punctuation">,</span> <span class="token string">"PATCH"</span><span class="token punctuation">&#125;</span><span class="token punctuation">:</span>            <span class="token keyword">if</span> payload <span class="token keyword">is</span> <span class="token keyword">not</span> <span class="token boolean">None</span><span class="token punctuation">:</span>                kwargs<span class="token punctuation">[</span><span class="token string">"json"</span><span class="token punctuation">]</span> <span class="token operator">=</span> payload            <span class="token keyword">else</span><span class="token punctuation">:</span>                kwargs<span class="token punctuation">[</span><span class="token string">"content"</span><span class="token punctuation">]</span> <span class="token operator">=</span> raw_body        r <span class="token operator">=</span> <span class="token keyword">await</span> client<span class="token punctuation">.</span>request<span class="token punctuation">(</span>method<span class="token punctuation">,</span> <span class="token operator">**</span>kwargs<span class="token punctuation">)</span>    ct_resp <span class="token operator">=</span> r<span class="token punctuation">.</span>headers<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"content-type"</span><span class="token punctuation">)</span>    resp_headers <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span>    <span class="token keyword">for</span> k <span class="token keyword">in</span> <span class="token punctuation">(</span><span class="token string">"content-type"</span><span class="token punctuation">,</span> <span class="token string">"content-disposition"</span><span class="token punctuation">,</span> <span class="token string">"cache-control"</span><span class="token punctuation">,</span> <span class="token string">"x-request-id"</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">if</span> k <span class="token keyword">in</span> r<span class="token punctuation">.</span>headers<span class="token punctuation">:</span>            resp_headers<span class="token punctuation">[</span>k<span class="token punctuation">]</span> <span class="token operator">=</span> r<span class="token punctuation">.</span>headers<span class="token punctuation">[</span>k<span class="token punctuation">]</span>    <span class="token keyword">return</span> Response<span class="token punctuation">(</span>        content<span class="token operator">=</span>r<span class="token punctuation">.</span>content<span class="token punctuation">,</span>        status_code<span class="token operator">=</span>r<span class="token punctuation">.</span>status_code<span class="token punctuation">,</span>        media_type<span class="token operator">=</span>ct_resp<span class="token punctuation">,</span>        headers<span class="token operator">=</span>resp_headers<span class="token punctuation">,</span>    <span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这部分实现了一个比较简陋的鉴权功能，其他的操作就是把请求内容透传到上游即可。</p><p>为了适配Cherry Studio和我的后端服务llama_server，还需要将<code>enable_thinking</code>这个参数的位置移动一下，放置到<code>chat_template_kwargs</code>，不然模板不会渲染。</p><p>通过命令启动：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">uvicorn llm_proxy:app <span class="token parameter variable">--host</span> <span class="token number">0.0</span>.0.0 <span class="token parameter variable">--port</span> <span class="token number">8000</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>至此，服务端的配置已经完成。</p><h2 id="配置-Cherry-Studio"><a href="#配置-Cherry-Studio" class="headerlink" title="配置 Cherry Studio"></a>配置 Cherry Studio</h2><h3 id="创建模型"><a href="#创建模型" class="headerlink" title="创建模型"></a>创建模型</h3><p>在「模型服务」tab下找到「阿里云百炼」，删掉所有的默认模型，将API修改为刚刚的自部署服务，填写一个密钥。随后添加新模型：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/183b1c2a-c6f7-4a62-be1c-adce2f01891b.png" style="zoom:50%;" /></p><p>之所以使用「阿里云百炼」是因为这个服务能开启、关闭思维链（也有一些其他服务支持思维链，不过我没有尝试）</p><h3 id="配置默认模型"><a href="#配置默认模型" class="headerlink" title="配置默认模型"></a>配置默认模型</h3><p><img src="https://blogfiles.oss.fyz666.xyz/png/44254e8d-f0e5-4d67-bddf-58965a437891.png" style="zoom:50%;" /></p><p>将三个默认模型全部设为Qwen3-32B，对于后面两个模型，需要修改一下提示词，在默认的提示词前面加上<code>/no_think</code>，避免模型过多思考影响效率。</p><p>切换到「助手」tab，右键「默认助手」，依次选择「编辑助手」-&gt;「模型设置」，然后将「工具调用方式」选择「函数」。</p><p>这里说明一下两种不同的工具调用方式有什么区别。</p><ol><li><strong>提示词</strong><br>这个方式较为通用，是Cherry Studio为了让所有语言模型都能够调用工具实现的功能，通过在系统提示词中临时教模型如何使用工具来实现这个功能。通过提示词方式调用工具有个很大的问题在于系统提示词会非常非常长：”In this environment you have access to a set of…”，详见下图：<br><img src="https://blogfiles.oss.fyz666.xyz/png/258888d6-bc2e-47e1-a428-a40dc5f3361e.png" alt=""></li><li><strong>函数</strong><br>在payload中添加一个参数：<code>tools</code>，将工具列表传入模型服务。这种方式需要模型具备function call能力，其请求如下图：<br><img src="https://blogfiles.oss.fyz666.xyz/png/311a7bab-f73b-4dcb-931e-7e0e98150dd9.png" alt=""></li></ol><p>而Qwen3系列模型都具备function call能力，因此这一项勾选「函数」比较好，就别傻愣傻愣的每次都教它一遍了。</p><h3 id="配置嵌入模型和重排模型"><a href="#配置嵌入模型和重排模型" class="headerlink" title="配置嵌入模型和重排模型"></a>配置嵌入模型和重排模型</h3><p>有时候我们会用到嵌入、重排功能，比如在使用知识库、搜索网页的时候。我们可以使用硅基流动提供的免费模型：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/41422a33-2942-4ece-b2fa-3a2b12ae6504.png" style="zoom:50%;" /></p><hr><p><strong>配置知识库</strong></p><p><img src="https://blogfiles.oss.fyz666.xyz/png/4caa578a-1ba4-457a-8300-01a3ae2d5f41.png" style="zoom:50%;" /></p><hr><p><strong>配置网络搜索</strong></p><p>由于我的服务器性能不大行，只能跑得起5万token的上下文，因此在网络搜索的时候要尽可能减少传过去的内容。</p><p>于是，在网络搜索这里我用了RAG方法对搜索结果进行了一定程度的精简。</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/b902659c-91c7-48ed-881d-91ea9fd878c0.png" style="zoom:33%;" /></p><hr><p>除了上面这些之外，Cherry Studio还提供了一个插件：划词助手，这插件是真的好用，支持基于大模型的翻译、解释、总结、润色优化等功能，你甚至可以在系统上任何能够划词的软件上打开它：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/6e0abfd1-63dd-43d7-b4bb-ad080edc08c5.png" style="zoom:50%;" /></p><hr><p>以上就是我使用Cherry Studio的一部分操作和配置，这款软件还有更多有实用价值、高自由度的功能（例如丰富的MCP工具）等待体验。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E7%9E%8E%E6%8D%A3%E9%BC%93%E7%BB%8F%E5%8E%86/">瞎捣鼓经历</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/qwen/">Qwen</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llama-cpp/">llama_cpp</category>
      
      <category domain="https://blog.fyz666.xyz/tags/cherry-studio/">Cherry Studio</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/13005/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>在消费级显卡上部署QwQ-32B模型</title>
      <link>https://blog.fyz666.xyz/blog/12935/</link>
      <guid>https://blog.fyz666.xyz/blog/12935/</guid>
      <pubDate>Wed, 27 Aug 2025 06:04:38 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;Qwen在今年3月份的时候发布了号称能媲美DeepSeek-R1-671B的32B模型：QwQ（这名字卖萌是故意的，还是不小心？）&lt;/p&gt;
&lt;p&gt;&lt;img</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>Qwen在今年3月份的时候发布了号称能媲美DeepSeek-R1-671B的32B模型：QwQ（这名字卖萌是故意的，还是不小心？）</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/bf0d268f-ff72-48e0-96b9-4fa0fbfc5cd9.webp" style="zoom:20%;" /></p><p>32B模型如果做一个4bit量化的话，大约只需要占16G的显存，这样的话，一块24G显存的消费级显卡（如NVIDIA GeForce RTX 3090）也能把这个模型部署起来。拥有一个本地部署的性能匹配满血DeepSeek-R1的模型岂不美哉？于是，我紧锣密鼓地一顿部署，很快便搞定了。</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/98fbe641-6126-45ea-9ca3-a5f1435d7d27.png" style="zoom:30%;" /></p><h2 id="下载模型"><a href="#下载模型" class="headerlink" title="下载模型"></a>下载模型</h2><p>模型的官方链接位于<a href="https://huggingface.co/Qwen/QwQ-32B">Qwen/QwQ-32B</a>，不过我这里选择了另一个HuggingFace 上的由bartowski提供的 该模型的 llama.cpp/ggml 格式量化版仓库：<a href="https://huggingface.co/bartowski/Qwen_QwQ-32B-GGUF">bartowski/Qwen_QwQ-32B-GGUF</a>。</p><p>找到文件：<a href="https://huggingface.co/bartowski/Qwen_QwQ-32B-GGUF/blob/main/Qwen_QwQ-32B-IQ4_XS.gguf">Qwen_QwQ-32B-IQ4_XS.gguf</a>，用wget下到本地即可。</p><h2 id="安装-llama-cpp"><a href="#安装-llama-cpp" class="headerlink" title="安装 llama.cpp"></a>安装 llama.cpp</h2><p>这是一个用cpp写的大模型推理框架，链接如下：</p><a class="tag-Link" target="_blank" href=" https://github.com/ggml-org/llama.cpp">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">llama.cpp</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>仓库给了一个<a href="https://github.com/ggml-org/llama.cpp/blob/master/docs/build.md">编译文档</a>，不过我直接对着文档操作失败了，所以在这里记录一下我的操作。</p><p>在编译之前，需要确保已经安装好了一些基本的组件：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> <span class="token function">apt</span> update<span class="token function">sudo</span> <span class="token function">apt</span> <span class="token function">install</span> <span class="token parameter variable">-y</span> build-essential libcurl4-openssl-dev<span class="token function">sudo</span> snap <span class="token function">install</span> cmake <span class="token parameter variable">--classic</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>另外如果编译CUDA版，还需要装好cuda-toolkit：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">$ nvcc <span class="token parameter variable">--version</span>nvcc: NVIDIA <span class="token punctuation">(</span>R<span class="token punctuation">)</span> Cuda compiler driverCopyright <span class="token punctuation">(</span>c<span class="token punctuation">)</span> <span class="token number">2005</span>-2024 NVIDIA CorporationBuilt on Tue_Feb_27_16:19:38_PST_2024Cuda compilation tools, release <span class="token number">12.4</span>, V12.4.99Build cuda_12.4.r12.4/compiler.33961263_0<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><strong>增加CUDA路径</strong></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token builtin class-name">export</span> <span class="token assign-left variable">LD_LIBRARY_PATH</span><span class="token operator">=</span>/usr/local/cuda/lib64:<span class="token variable">$LD_LIBRARY_PATH</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p><strong>Clone仓库</strong></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">git</span> clone https://github.com/ggml-org/llama.cpp<span class="token builtin class-name">cd</span> llama.cpp<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p><strong>开始编译</strong></p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">env</span> <span class="token parameter variable">-i</span> <span class="token punctuation">\</span>  <span class="token assign-left variable"><span class="token environment constant">PATH</span></span><span class="token operator">=</span><span class="token string">"/snap/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/bin:/usr/local/cuda/bin"</span> <span class="token punctuation">\</span>  <span class="token assign-left variable">CC</span><span class="token operator">=</span>/usr/bin/gcc <span class="token assign-left variable">CXX</span><span class="token operator">=</span>/usr/bin/g++ <span class="token punctuation">\</span>  <span class="token assign-left variable">CUDACXX</span><span class="token operator">=</span>/usr/local/cuda/bin/nvcc <span class="token punctuation">\</span>  cmake <span class="token parameter variable">-B</span> build <span class="token parameter variable">-DGGML_CUDA</span><span class="token operator">=</span>ON <span class="token punctuation">\</span>        <span class="token parameter variable">-DCMAKE_C_FLAGS</span><span class="token operator">=</span><span class="token string">"-pthread"</span> <span class="token punctuation">\</span>        <span class="token parameter variable">-DCMAKE_CXX_FLAGS</span><span class="token operator">=</span><span class="token string">"-pthread"</span> <span class="token punctuation">\</span>        <span class="token parameter variable">-DCMAKE_EXE_LINKER_FLAGS</span><span class="token operator">=</span><span class="token string">"-pthread"</span> <span class="token punctuation">\</span>        <span class="token parameter variable">-DCUDAToolkit_ROOT</span><span class="token operator">=</span>/usr/local/cudacmake <span class="token parameter variable">--build</span> build <span class="token parameter variable">-j4</span>  <span class="token comment"># 4核足以，开太多会把我ssh挤掉</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>编译完成后，将编译出的文件移动到系统上：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">sudo</span> cmake <span class="token parameter variable">--install</span> build <span class="token parameter variable">--prefix</span> /usr/local<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>这时直接运行<code>llama-server</code>仍可能会报错：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">llama-server: error while loading shared libraries: libmtmd.so: cannot open shared object file: No such file or directory<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>运行一下下面的命令即可：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token comment"># 确保 /usr/local/lib 在动态库配置里，并刷新缓存</span><span class="token builtin class-name">echo</span> <span class="token string">'/usr/local/lib'</span> <span class="token operator">|</span> <span class="token function">sudo</span> <span class="token function">tee</span> /etc/ld.so.conf.d/llama.conf<span class="token function">sudo</span> ldconfig<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><h2 id="部署-QwQ-32B"><a href="#部署-QwQ-32B" class="headerlink" title="部署 QwQ-32B"></a>部署 QwQ-32B</h2><p>创建一个jinja模板<code>./chat_templates/QwQ.jinja</code>：</p><pre class="line-numbers language-django" data-language="django"><code class="language-django"><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">tools</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>system\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">messages</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'role'</span><span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token string">'system'</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">messages</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'content'</span><span class="token punctuation">]</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">else</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">''</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">"\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within &lt;tools>&lt;/tools> XML tags:\n&lt;tools>"</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">for</span> <span class="token variable">tool</span> <span class="token keyword">in</span> <span class="token variable">tools</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">"\n"</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">tool</span> <span class="token operator">|</span> <span class="token variable">tojson</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endfor</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">"\n&lt;/tools>\n\nFor each function call, return a json object with function name and arguments within &lt;tool_call>&lt;/tool_call> XML tags:\n&lt;tool_call>\n&#123;\"name\": &lt;function-name>, \"arguments\": &lt;args-json-object>&#125;\n&lt;/tool_call>&lt;|im_end|>\n"</span> <span class="token delimiter punctuation">&#125;&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">else</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">messages</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'role'</span><span class="token punctuation">]</span> <span class="token operator">==</span> <span class="token string">'system'</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>system\n'</span> <span class="token operator">+</span> <span class="token variable">messages</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token string">'content'</span><span class="token punctuation">]</span> <span class="token operator">+</span> <span class="token string">'&lt;|im_end|>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>  <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">for</span> <span class="token variable">message</span> <span class="token keyword">in</span> <span class="token variable">messages</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"user"</span><span class="token punctuation">)</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"system"</span> <span class="token keyword">and</span> <span class="token keyword">not</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">first</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token operator">+</span> <span class="token string">'&lt;|im_end|>'</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">elif</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"assistant"</span> <span class="token keyword">and</span> <span class="token keyword">not</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">tool_calls</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">content</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token operator">|</span> <span class="token function">default</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'&lt;/think>'</span><span class="token punctuation">)</span><span class="token operator">|</span><span class="token filter function">last</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">lstrip</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token operator">+</span> <span class="token variable">content</span> <span class="token operator">+</span> <span class="token string">'&lt;|im_end|>'</span> <span class="token operator">+</span> <span class="token string">'\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">elif</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"assistant"</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">content</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token operator">|</span> <span class="token function">default</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">'&lt;/think>'</span><span class="token punctuation">)</span><span class="token operator">|</span><span class="token filter function">last</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">lstrip</span><span class="token punctuation">(</span><span class="token string">'\n'</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>'</span> <span class="token operator">+</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'\n'</span> <span class="token operator">+</span> <span class="token variable">content</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">for</span> <span class="token variable">tool_call</span> <span class="token keyword">in</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">tool_calls</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">function</span> <span class="token keyword">is</span> <span class="token test function">defined</span> <span class="token delimiter punctuation">%&#125;</span></span>                <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">set</span> <span class="token variable">tool_call</span> <span class="token operator">=</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">function</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'\n&lt;tool_call>\n&#123;"name": "'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">name</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'", "arguments": '</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">tool_call</span><span class="token punctuation">.</span><span class="token variable">arguments</span> <span class="token operator">|</span> <span class="token variable">tojson</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&#125;\n&lt;/tool_call>'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endfor</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_end|>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">elif</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">==</span> <span class="token string">"tool"</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">index0</span> <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token variable">messages</span><span class="token punctuation">[</span><span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">index0</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">!=</span> <span class="token string">"tool"</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>user'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'\n&lt;tool_response>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token variable">message</span><span class="token punctuation">.</span><span class="token variable">content</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'\n&lt;/tool_response>'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">last</span> <span class="token function">or</span> <span class="token punctuation">(</span><span class="token variable">messages</span><span class="token punctuation">[</span><span class="token keyword">loop</span><span class="token punctuation">.</span><span class="token variable">index0</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token variable">role</span> <span class="token operator">!=</span> <span class="token string">"tool"</span><span class="token punctuation">)</span> <span class="token delimiter punctuation">%&#125;</span></span>            <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_end|>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span>        <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endfor</span> <span class="token delimiter punctuation">%&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">if</span> <span class="token variable">add_generation_prompt</span> <span class="token delimiter punctuation">%&#125;</span></span>    <span class="token django language-django"><span class="token delimiter punctuation">&#123;&#123;-</span> <span class="token string">'&lt;|im_start|>assistant\n&lt;think>\n'</span> <span class="token delimiter punctuation">&#125;&#125;</span></span><span class="token django language-django"><span class="token delimiter punctuation">&#123;%-</span> <span class="token tag keyword">endif</span> <span class="token delimiter punctuation">%&#125;</span></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>创建启动命令文件<code>llama_deploy.sh</code>:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">llama-server <span class="token punctuation">\</span>    <span class="token parameter variable">--model</span> <span class="token string">"./model/bartowski/Qwen_QwQ-32B-GGUF/Qwen_QwQ-32B-IQ4_XS.gguf"</span> <span class="token punctuation">\</span>    <span class="token parameter variable">--alias</span> <span class="token string">"QwQ-32B"</span> <span class="token punctuation">\</span>    --n-gpu-layers <span class="token number">65</span> <span class="token punctuation">\</span>    --ctx-size <span class="token number">32768</span> <span class="token punctuation">\</span>    <span class="token parameter variable">--parallel</span> <span class="token number">1</span> <span class="token punctuation">\</span>    --cache-type-k q8_0 <span class="token punctuation">\</span>    --cache-type-v q8_0 <span class="token punctuation">\</span>    <span class="token parameter variable">--threads</span> <span class="token number">16</span> <span class="token punctuation">\</span>    --flash-attn <span class="token punctuation">\</span>    <span class="token parameter variable">--mlock</span> <span class="token punctuation">\</span>    --n-predict <span class="token parameter variable">-1</span> <span class="token punctuation">\</span>    <span class="token parameter variable">--jinja</span> <span class="token punctuation">\</span>    --chat-template-file ./chat_templates/QwQ.jinja <span class="token punctuation">\</span>    <span class="token parameter variable">--host</span> <span class="token number">0.0</span>.0.0 <span class="token punctuation">\</span>    <span class="token parameter variable">--port</span> <span class="token number">8000</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>然后运行<code>llama_deploy.sh</code>，即可完成部署。</p><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p><img src="https://blogfiles.oss.fyz666.xyz/png/0b1b6053-bb0a-4290-b78a-047012b3cbf3.png" style="zoom:33%;" /></p><p>在Cherry Studio中还能轻松调用工具：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/f5484fdf-b56c-4a3d-abbc-4ae7d03ccdf4.png" style="zoom:33%;" /></p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/qwen/">Qwen</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llama-cpp/">llama_cpp</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12935/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>动手实现一个医学问答大模型（浅尝检索增强生成方法：RAG）</title>
      <link>https://blog.fyz666.xyz/blog/12897/</link>
      <guid>https://blog.fyz666.xyz/blog/12897/</guid>
      <pubDate>Tue, 12 Aug 2025 13:44:10 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h2 id=&quot;前情提要-amp-动机&quot;&gt;&lt;a href=&quot;#前情提要-amp-动机&quot; class=&quot;headerlink&quot; title=&quot;前情提要 &amp;amp; 动机&quot;&gt;&lt;/a&gt;前情提要 &amp;amp; 动机&lt;/h2&gt;&lt;p&gt;前段时间忙里偷闲和女朋友一起去了趟云南（你可以查看&lt;a</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h2 id="前情提要-amp-动机"><a href="#前情提要-amp-动机" class="headerlink" title="前情提要 &amp; 动机"></a>前情提要 &amp; 动机</h2><p>前段时间忙里偷闲和女朋友一起去了趟云南（你可以查看<a href="/gallery/travel/yunnan/">相册</a>），在出发之前，由于怕女朋友高反，遂问了一圈各种AI应该带什么药、做什么准备措施，以及高反的主要症状等。在做足了功课后，职业病又犯了：我能不能自己搞个<del>大模型</del>小模型，让它具备专业的医学知识，以使得用户提问时，其能从专业的角度给予回答？</p><hr><p>说干就干，由于需要一定的实用性，就不考虑 7b 以下的模型了，但7b的模型我的垃圾显卡又训不动，怎么办呢？于是我想起了曾经听说过的一种技术：检索增强生成（Retrieval Augmented Generation, RAG），这种方法不需要训练模型，直接用预训练好的模型即可。</p><h2 id="RAG是什么？"><a href="#RAG是什么？" class="headerlink" title="RAG是什么？"></a>RAG是什么？</h2><blockquote><p>检索增强生成 (英语：Retrieval-augmented generation, RAG ) 是赋予生成式人工智能模型资讯检索能力的技术。检索增强生成最佳化大型语言模型(LLM) 的交互方式，让模型根据指定的一组文件回应使用者的查询，并使用这些资讯增强模型从自身庞大的静态训练数据中提取的资讯。<br><cite><a href="https://zh.wikipedia.org/zh-hans/%E6%AA%A2%E7%B4%A2%E5%A2%9E%E5%BC%B7%E7%94%9F%E6%88%90#:~:text=%E6%A3%80%E7%B4%A2%E5%A2%9E%E5%BC%BA%E7%94%9F%E6%88%90%EF%BC%88%E8%8B%B1%E8%AF%AD%EF%BC%9ARetrieval,%E6%95%B0%E6%8D%AE%E4%B8%AD%E6%8F%90%E5%8F%96%E7%9A%84%E4%BF%A1%E6%81%AF%E3%80%82">维基百科</a></cite></p></blockquote><p>回到我的需求：使大模型获得一定的医学知识。要使大模型具备一定的专业知识，最方便的不需要额外训练的方法当然就是直接将专业知识作为上下文喂给大模型，让它基于这些内容进行回答。但我们又知道，我们给模型提供的上下文越多，其中的关键信息越会被淡化，哪怕长度未到上限，仍可能引发回答幻觉。</p><p>为尽可能减少专业知识领域的幻觉问题，这种名为<strong>检索增强生成</strong>的方案应运而生。</p><p>简单说来，就是通过一些文本相似度算法，从非常多的专业语料数据中检索出与用户的提问相关度较高的内容，一并作为上下文喂给模型。如此，模型的上下文内容中仅仅包含了与用户提问相关度最高的信息，而非所有的专业知识，上下文与用户提问的相关性大幅提升，而其长度则大幅下降，从而使得模型能给出更专业、更可信的回答。</p><p>纸上得来终觉浅，在知道其原理后，我们来尝试一下吧！</p><h2 id="RAG方法的实践"><a href="#RAG方法的实践" class="headerlink" title="RAG方法的实践"></a>RAG方法的实践</h2><h3 id="数据集准备-amp-处理"><a href="#数据集准备-amp-处理" class="headerlink" title="数据集准备 &amp; 处理"></a>数据集准备 &amp; 处理</h3><p>我上哪去弄那么多医学相关的文本数据？逛逛开源社区肯定错不了，稍一搜索，我找到了它：<a href="https://huggingface.co/datasets/FreedomIntelligence/huatuo_encyclopedia_qa">FreedomIntelligence/huatuo_encyclopedia_qa</a>，这是一个中文医学问答数据集，共包含364420条 Q-A 对。</p><p>一般情况下，对于大段大段的长篇文本，为了制作RAG的数据库，我们还需要考虑如何将其进行切片，而这种处理好的一条一条的数据就几乎不用考虑切片问题了，特别适合作为RAG的数据。</p><hr><p>我们使用一个集成的非常好的框架：langchain来进行处理。RAG方法需要将文本数据制作成嵌入向量存储到数据库中，在检索时匹配语义相似度最高的向量（这里相似度指标可以使用欧氏距离、余弦相似度等）。总之，无论如何，我们总是需要将文本先转化为嵌入向量，因此需要一个<strong>嵌入模型</strong>，这里我使用了BAAI系列的嵌入模型：<code>BAAI/bge-large-zh-v1.5</code>。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>embeddings <span class="token keyword">import</span> HuggingFaceEmbeddingsembeddings <span class="token operator">=</span> HuggingFaceEmbeddings<span class="token punctuation">(</span>    model_name<span class="token operator">=</span><span class="token string">"BAAI/bge-large-zh-v1.5"</span><span class="token punctuation">,</span>    model_kwargs<span class="token operator">=</span><span class="token punctuation">&#123;</span>        <span class="token string">"device"</span><span class="token punctuation">:</span> <span class="token string">"cpu"</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这里的设备可以改成<code>cuda</code>以提速。有了嵌入模型，接下来我们需要使用一个向量数据库，将全部文本数据转化成的嵌入向量存入。鉴于数据的规模不大，我使用了一个比较轻量的向量数据库：faiss。</p><p>langchain对这个数据库进行了比较好的封装，我们可以直接调用：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>vectorstores <span class="token keyword">import</span> FAISS<span class="token keyword">from</span> datasets <span class="token keyword">import</span> load_dataset<span class="token keyword">from</span> langchain<span class="token punctuation">.</span>docstore<span class="token punctuation">.</span>document <span class="token keyword">import</span> Document<span class="token keyword">import</span> json<span class="token keyword">import</span> osos<span class="token punctuation">.</span>makedirs<span class="token punctuation">(</span><span class="token string">"RAG_DB"</span><span class="token punctuation">,</span> exist_ok<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>data <span class="token operator">=</span> load_dataset<span class="token punctuation">(</span><span class="token string">"FreedomIntelligence/huatuo_encyclopedia_qa"</span><span class="token punctuation">,</span> split<span class="token operator">=</span><span class="token string">'train'</span><span class="token punctuation">)</span>docs <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token keyword">for</span> row <span class="token keyword">in</span> data<span class="token punctuation">:</span>    content <span class="token operator">=</span> json<span class="token punctuation">.</span>dumps<span class="token punctuation">(</span>row<span class="token punctuation">,</span> ensure_ascii<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>    docs<span class="token punctuation">.</span>append<span class="token punctuation">(</span>Document<span class="token punctuation">(</span>page_content<span class="token operator">=</span>content<span class="token punctuation">)</span><span class="token punctuation">)</span>db <span class="token operator">=</span> <span class="token boolean">None</span>batchsize <span class="token operator">=</span> <span class="token number">512</span>index <span class="token operator">=</span> <span class="token number">0</span><span class="token keyword">with</span> tqdm<span class="token punctuation">(</span><span class="token builtin">range</span><span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>docs<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">as</span> bar<span class="token punctuation">:</span>    <span class="token keyword">while</span> index <span class="token operator">&lt;</span> <span class="token builtin">len</span><span class="token punctuation">(</span>docs<span class="token punctuation">)</span><span class="token punctuation">:</span>        batch <span class="token operator">=</span> docs<span class="token punctuation">[</span>index<span class="token punctuation">:</span> index <span class="token operator">+</span> batchsize<span class="token punctuation">]</span>        index <span class="token operator">+=</span> batchsize        <span class="token keyword">if</span> db <span class="token keyword">is</span> <span class="token boolean">None</span><span class="token punctuation">:</span>            db <span class="token operator">=</span> FAISS<span class="token punctuation">.</span>from_documents<span class="token punctuation">(</span>batch<span class="token punctuation">,</span> embeddings<span class="token punctuation">)</span>        <span class="token keyword">else</span><span class="token punctuation">:</span>            db<span class="token punctuation">.</span>add_documents<span class="token punctuation">(</span>batch<span class="token punctuation">)</span>        bar<span class="token punctuation">.</span>update<span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>batch<span class="token punctuation">)</span><span class="token punctuation">)</span>db<span class="token punctuation">.</span>save_local<span class="token punctuation">(</span><span class="token string">"RAG_DB/medical"</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这一步骤比较耗时，防止等的不耐烦，遂加了个进度条。每次往数据库中添加512条Q-A对，大约花了一个半小时才搞定。</p><p>至此，数据已经处理完成。</p><h3 id="实现文本检索"><a href="#实现文本检索" class="headerlink" title="实现文本检索"></a>实现文本检索</h3><p>既然已经制作好了数据库，这一步就很容易了，langchain同样封装好了相似度计算的部分，它默认使用的是欧氏距离作为相似度指标（距离越小，相似度越高）。我们从数据库对象中提取一个<code>retriever</code>出来，就可以直接使用了：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">retriever <span class="token operator">=</span> db<span class="token punctuation">.</span>as_retriever<span class="token punctuation">(</span>search_type<span class="token operator">=</span><span class="token string">"similarity"</span><span class="token punctuation">,</span> search_kwargs<span class="token operator">=</span><span class="token punctuation">&#123;</span><span class="token string">"k"</span><span class="token punctuation">:</span> <span class="token number">10</span><span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>随便写个问题测试一下：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/2f507776-cfdb-4a82-96b6-a1d39f247d96.png" style="zoom:50%;" /></p><p>可以看到检索的效果还是比较好的，拿到了检索器，剩下的操作就很简单了，只要部署起一个大模型，稍微实现一下后端逻辑，再用Gradio整个界面出来，就万事大吉了！</p><h3 id="部署大模型"><a href="#部署大模型" class="headerlink" title="部署大模型"></a>部署大模型</h3><p>说是大模型，其实就是个7b的小模型罢了。这里我使用了<a href="https://huggingface.co/Qwen/Qwen2.5-7B-Instruct">Qwen/Qwen2.5-7B-Instruct</a>。</p><p>将模型下载到本地：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">hf download Qwen/Qwen2.5-7B-Instruct --local-dir ./model/Qwen2.5-7B-Instruct<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>通过vllm启动：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">python3 <span class="token parameter variable">-m</span> vllm.entrypoints.openai.api_server <span class="token punctuation">\</span>  <span class="token parameter variable">--model</span> ./model/Qwen2.5-7B-Instruct <span class="token punctuation">\</span>  <span class="token parameter variable">--tokenizer</span> ./model/Qwen2.5-7B-Instruct <span class="token punctuation">\</span>  <span class="token parameter variable">--dtype</span> float16 <span class="token punctuation">\</span>  <span class="token parameter variable">--host</span> <span class="token number">0.0</span>.0.0 <span class="token punctuation">\</span>  <span class="token parameter variable">--port</span> <span class="token number">8000</span> <span class="token punctuation">\</span>  --max-num-seqs <span class="token number">4</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>稍等片刻启动完成后（吃掉了约20G显存…（模型自身占用$7\times2=14$GB，KV-cache占用约6.7GB）），我们就可以直接通过OpenAI提供的api来与之交互：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> openai <span class="token keyword">import</span> OpenAIclient <span class="token operator">=</span> OpenAI<span class="token punctuation">(</span>    base_url<span class="token operator">=</span><span class="token string">"http://localhost:8000/v1"</span><span class="token punctuation">,</span>    api_key<span class="token operator">=</span><span class="token string">"n"</span><span class="token punctuation">)</span>messages <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span> <span class="token string">"content"</span><span class="token punctuation">:</span> <span class="token string">"你是谁？"</span><span class="token punctuation">&#125;</span><span class="token punctuation">]</span>response <span class="token operator">=</span> client<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>completions<span class="token punctuation">.</span>create<span class="token punctuation">(</span>    model<span class="token operator">=</span><span class="token string">"./model/Qwen2.5-7B-Instruct"</span><span class="token punctuation">,</span>    messages<span class="token operator">=</span>messages<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="编写RAG业务逻辑"><a href="#编写RAG业务逻辑" class="headerlink" title="编写RAG业务逻辑"></a>编写RAG业务逻辑</h3><p>为得到相似度得分，排除掉特别不相关的内容，我们可以自己写一个<code>retrieve</code>函数：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">retrieve</span><span class="token punctuation">(</span>question<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    docs <span class="token operator">=</span> db<span class="token punctuation">.</span>similarity_search_with_score<span class="token punctuation">(</span>question<span class="token punctuation">,</span> k<span class="token operator">=</span><span class="token number">10</span><span class="token punctuation">)</span>    docs <span class="token operator">=</span> <span class="token punctuation">[</span>json<span class="token punctuation">.</span>loads<span class="token punctuation">(</span>doc<span class="token punctuation">.</span>page_content<span class="token punctuation">)</span> <span class="token keyword">for</span> doc<span class="token punctuation">,</span> score <span class="token keyword">in</span> docs <span class="token keyword">if</span> score<span class="token punctuation">.</span>item<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">0.9</span><span class="token punctuation">]</span>    docs <span class="token operator">=</span> <span class="token punctuation">[</span>json<span class="token punctuation">.</span>dumps<span class="token punctuation">(</span><span class="token punctuation">&#123;</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> doc<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"questions"</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"answer"</span><span class="token punctuation">:</span> doc<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"answers"</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">,</span> ensure_ascii<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span> <span class="token keyword">for</span> doc <span class="token keyword">in</span> docs<span class="token punctuation">]</span>    <span class="token keyword">return</span> docs<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>给一个系统提示词：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">system_prompt <span class="token operator">=</span> <span class="token triple-quoted-string string">"""你是一位医学专家。你擅长根据文档回答用户的提问。要求：1. 只使用提供的文档内容来回答问题。2. 不要添加任何额外的内容，如果文档中没有相关信息，请直接回答"无法回答"。3. 如果没有搜到相关文档，请回答"未查询到相关文档"。"""</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>再编写一个用户提示词模板：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>prompts <span class="token keyword">import</span> PromptTemplateprompt_template <span class="token operator">=</span> PromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token triple-quoted-string string">"""文档: &#123;context&#125;问题: &#123;question&#125;"""</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>接下来，我们通过langchain框架将RAG的流程实现一下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> langchain_core<span class="token punctuation">.</span>runnables <span class="token keyword">import</span> RunnableMap<span class="token punctuation">,</span> RunnableLambdarag_chain <span class="token operator">=</span> <span class="token punctuation">(</span>    RunnableMap<span class="token punctuation">(</span><span class="token punctuation">&#123;</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> <span class="token keyword">lambda</span> x<span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> <span class="token keyword">lambda</span> x<span class="token punctuation">:</span> build_context_query<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">)</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"context"</span><span class="token punctuation">:</span> retrieve<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"context"</span><span class="token punctuation">:</span> <span class="token string">"\n"</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">'context'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"references"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">'context'</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"prompt"</span><span class="token punctuation">:</span> prompt_template<span class="token punctuation">.</span><span class="token builtin">format</span><span class="token punctuation">(</span>question<span class="token operator">=</span>x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> context<span class="token operator">=</span>x<span class="token punctuation">[</span><span class="token string">"context"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"references"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"references"</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"response"</span><span class="token punctuation">:</span> stream<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">"prompt"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"references"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"references"</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这是一个串联过程，看似复杂，实则容易理解，限于篇幅，不再详细解释。</p><p>需要解释一下这里定义的一个函数：<code>build_context_query</code>，它用来处理用户输入的提问，如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">build_context_query</span><span class="token punctuation">(</span>question<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> history<span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""构建包含历史上下文的查询"""</span>    <span class="token keyword">if</span> <span class="token keyword">not</span> history<span class="token punctuation">:</span>        <span class="token keyword">return</span> question        <span class="token keyword">return</span> summarize_context_query<span class="token punctuation">(</span>question<span class="token punctuation">,</span> history<span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">summarize_context_query</span><span class="token punctuation">(</span>question<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> history<span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""使用大模型总结历史提问，生成更准确的检索查询"""</span>    <span class="token keyword">if</span> <span class="token keyword">not</span> history<span class="token punctuation">:</span>        <span class="token keyword">return</span> question        <span class="token comment"># 构建历史对话列表</span>    history <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string-interpolation"><span class="token string">f'Q: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>user_msg<span class="token punctuation">&#125;</span></span><span class="token string">\nA: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>response<span class="token punctuation">&#125;</span></span><span class="token string">'</span></span> <span class="token keyword">for</span> user_msg<span class="token punctuation">,</span> response <span class="token keyword">in</span> history<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">3</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">]</span>  <span class="token comment"># 最近3轮</span>    system_prompt <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"""你是一个重写助手，任务是将用户当前的问题结合历史对话重写为一个**自包含、清晰明确的问题句子**，以便发送给问答系统。请遵循以下规则：1. 仅重写问题，**不回答**；2. 替换句中的所有代词（如“它”、“这”、“那”等）为具体的名词或短语，当可能存在多个指代对象时，使用最新的历史对话中的信息；3. 推理指代、补全省略，使问题独立完整；4. 不引入历史中未提及的信息； 5. 只输出**重写后的问题句子**，不添加任何解释或注释。"""</span></span>        summary_prompt <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"""历史对话：</span><span class="token interpolation"><span class="token punctuation">&#123;</span><span class="token builtin">chr</span><span class="token punctuation">(</span><span class="token number">20</span><span class="token punctuation">)</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">&#123;</span>i<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">&#125;</span></span><span class="token string">. </span><span class="token interpolation"><span class="token punctuation">&#123;</span>q<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span> <span class="token keyword">for</span> i<span class="token punctuation">,</span> q <span class="token keyword">in</span> <span class="token builtin">enumerate</span><span class="token punctuation">(</span>history<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span></span><span class="token string">当前问题：</span><span class="token interpolation"><span class="token punctuation">&#123;</span>question<span class="token punctuation">&#125;</span></span><span class="token string">你重写的问题："""</span></span>        <span class="token keyword">try</span><span class="token punctuation">:</span>        messages <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"system"</span><span class="token punctuation">,</span> <span class="token string">"content"</span><span class="token punctuation">:</span> system_prompt<span class="token punctuation">&#125;</span><span class="token punctuation">,</span>                    <span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span> <span class="token string">"content"</span><span class="token punctuation">:</span> summary_prompt<span class="token punctuation">&#125;</span><span class="token punctuation">]</span>        response <span class="token operator">=</span> client<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>completions<span class="token punctuation">.</span>create<span class="token punctuation">(</span>            model<span class="token operator">=</span><span class="token string">"./model/Qwen2.5-7B-Instruct"</span><span class="token punctuation">,</span>            messages<span class="token operator">=</span>messages<span class="token punctuation">,</span>            stream<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">,</span>            max_tokens<span class="token operator">=</span><span class="token number">50</span>        <span class="token punctuation">)</span>                summarized_query <span class="token operator">=</span> response<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>message<span class="token punctuation">.</span>content<span class="token punctuation">.</span>strip<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"原始问题: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>question<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"重写问题: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>summarized_query<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>        <span class="token keyword">return</span> summarized_query            <span class="token keyword">except</span> Exception <span class="token keyword">as</span> e<span class="token punctuation">:</span>        <span class="token keyword">return</span> question<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这一长串代码，实际仅处理了一件事：根据最近3轮的历史聊天信息，把用户的提问重新整理一下。</p><hr><p>在实际使用中，我们发现，用户和模型的对话很可能是这样的：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">user: 艾滋病是什么？assistant: 艾滋病是一种危害性极大的传染病...user: 它如何治疗？<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>由于RAG在检索召回相似文档时仅仅依赖于用户的提问，而不会关注上下文信息，这里RAG检索的问题：「它如何治疗？」中缺乏关键信息——这个<strong>它</strong>指什么？（即使用户认为上下文中存在这一信息），为了解决这一问题，我想了个骚操作：让大模型根据历史对话记录来对用户的提问进行重写。这就是上面代码所做的事。</p><p>亲测还是比较好用的：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/02ef0756-9573-4a09-bb47-49dd0c2a3261.png" style="zoom:75%;" /></p><p>后来发现工程上似乎就是这么做的，这个任务叫<strong>Query Rewriting（查询重写）</strong>。</p><p>需要注意的是，我仅仅在查询重写这一步骤中使用了历史信息，并没有在与大模型的对话上下文中使用它们。</p><h3 id="完整代码-amp-使用体验"><a href="#完整代码-amp-使用体验" class="headerlink" title="完整代码 &amp; 使用体验"></a>完整代码 &amp; 使用体验</h3><p>将业务逻辑用Gradio糊一个前端，再实现一下流式响应，一个简单的医学问答大模型平台就完成了！</p><p>完整代码如下：</p><p><code>RAG/medical.py</code></p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> langchain<span class="token punctuation">.</span>prompts <span class="token keyword">import</span> PromptTemplate<span class="token keyword">from</span> langchain_huggingface <span class="token keyword">import</span> HuggingFaceEmbeddings<span class="token keyword">from</span> langchain_community<span class="token punctuation">.</span>vectorstores <span class="token keyword">import</span> FAISS<span class="token keyword">import</span> os<span class="token keyword">import</span> json<span class="token keyword">from</span> tqdm <span class="token keyword">import</span> tqdmembeddings <span class="token operator">=</span> HuggingFaceEmbeddings<span class="token punctuation">(</span>    model_name<span class="token operator">=</span><span class="token string">"BAAI/bge-large-zh-v1.5"</span><span class="token punctuation">,</span>    model_kwargs<span class="token operator">=</span><span class="token punctuation">&#123;</span>        <span class="token string">"device"</span><span class="token punctuation">:</span> <span class="token string">"cpu"</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>db_path <span class="token operator">=</span> <span class="token string">'RAG_DB/medical'</span><span class="token keyword">if</span> os<span class="token punctuation">.</span>path<span class="token punctuation">.</span>exists<span class="token punctuation">(</span>db_path<span class="token punctuation">)</span><span class="token punctuation">:</span>    db <span class="token operator">=</span> FAISS<span class="token punctuation">.</span>load_local<span class="token punctuation">(</span>db_path<span class="token punctuation">,</span> embeddings<span class="token punctuation">,</span> allow_dangerous_deserialization<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span><span class="token keyword">else</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"数据库不存在，正在加载数据集..."</span><span class="token punctuation">)</span>    <span class="token keyword">from</span> datasets <span class="token keyword">import</span> load_dataset    <span class="token keyword">from</span> langchain<span class="token punctuation">.</span>docstore<span class="token punctuation">.</span>document <span class="token keyword">import</span> Document    data <span class="token operator">=</span> load_dataset<span class="token punctuation">(</span><span class="token string">"FreedomIntelligence/huatuo_encyclopedia_qa"</span><span class="token punctuation">,</span> split<span class="token operator">=</span><span class="token string">'train'</span><span class="token punctuation">)</span>    docs <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>    <span class="token keyword">for</span> row <span class="token keyword">in</span> data<span class="token punctuation">:</span>        content <span class="token operator">=</span> json<span class="token punctuation">.</span>dumps<span class="token punctuation">(</span>row<span class="token punctuation">,</span> ensure_ascii<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>        docs<span class="token punctuation">.</span>append<span class="token punctuation">(</span>Document<span class="token punctuation">(</span>page_content<span class="token operator">=</span>content<span class="token punctuation">)</span><span class="token punctuation">)</span>    db <span class="token operator">=</span> <span class="token boolean">None</span>    batchsize <span class="token operator">=</span> <span class="token number">512</span>    index <span class="token operator">=</span> <span class="token number">0</span>    <span class="token keyword">with</span> tqdm<span class="token punctuation">(</span><span class="token builtin">range</span><span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>docs<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">as</span> bar<span class="token punctuation">:</span>        <span class="token keyword">while</span> index <span class="token operator">&lt;</span> <span class="token builtin">len</span><span class="token punctuation">(</span>docs<span class="token punctuation">)</span><span class="token punctuation">:</span>            batch <span class="token operator">=</span> docs<span class="token punctuation">[</span>index<span class="token punctuation">:</span> index <span class="token operator">+</span> batchsize<span class="token punctuation">]</span>            index <span class="token operator">+=</span> batchsize            <span class="token keyword">if</span> db <span class="token keyword">is</span> <span class="token boolean">None</span><span class="token punctuation">:</span>                db <span class="token operator">=</span> FAISS<span class="token punctuation">.</span>from_documents<span class="token punctuation">(</span>batch<span class="token punctuation">,</span> embeddings<span class="token punctuation">)</span>            <span class="token keyword">else</span><span class="token punctuation">:</span>                db<span class="token punctuation">.</span>add_documents<span class="token punctuation">(</span>batch<span class="token punctuation">)</span>            bar<span class="token punctuation">.</span>update<span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>batch<span class="token punctuation">)</span><span class="token punctuation">)</span>    db<span class="token punctuation">.</span>save_local<span class="token punctuation">(</span>db_path<span class="token punctuation">)</span>        system_prompt <span class="token operator">=</span> <span class="token triple-quoted-string string">"""你是一位医学专家。你擅长根据文档回答用户的提问。要求：1. 只使用提供的文档内容来回答问题。2. 不要添加任何额外的内容，如果文档中没有相关信息，请直接回答"无法回答"。3. 如果没有搜到相关文档，请回答"未查询到相关文档"。"""</span>        prompt_template <span class="token operator">=</span> PromptTemplate<span class="token punctuation">.</span>from_template<span class="token punctuation">(</span><span class="token triple-quoted-string string">"""文档: &#123;context&#125;问题: &#123;question&#125;"""</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">retrieve</span><span class="token punctuation">(</span>question<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    docs <span class="token operator">=</span> db<span class="token punctuation">.</span>similarity_search_with_score<span class="token punctuation">(</span>question<span class="token punctuation">,</span> k<span class="token operator">=</span><span class="token number">10</span><span class="token punctuation">)</span>    docs <span class="token operator">=</span> <span class="token punctuation">[</span>json<span class="token punctuation">.</span>loads<span class="token punctuation">(</span>doc<span class="token punctuation">.</span>page_content<span class="token punctuation">)</span> <span class="token keyword">for</span> doc<span class="token punctuation">,</span> score <span class="token keyword">in</span> docs <span class="token keyword">if</span> score<span class="token punctuation">.</span>item<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">0.9</span><span class="token punctuation">]</span>    docs <span class="token operator">=</span> <span class="token punctuation">[</span>json<span class="token punctuation">.</span>dumps<span class="token punctuation">(</span><span class="token punctuation">&#123;</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> doc<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"questions"</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"answer"</span><span class="token punctuation">:</span> doc<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"answers"</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">,</span> ensure_ascii<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span> <span class="token keyword">for</span> doc <span class="token keyword">in</span> docs<span class="token punctuation">]</span>    <span class="token keyword">return</span> docs<span class="token keyword">def</span> <span class="token function">format_references</span><span class="token punctuation">(</span>docs<span class="token punctuation">,</span> title<span class="token operator">=</span><span class="token string">"参考内容"</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""将 Document 列表格式化为折叠式 Q-A Markdown 文本"""</span>    qa_blocks <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>    <span class="token keyword">for</span> i<span class="token punctuation">,</span> doc <span class="token keyword">in</span> <span class="token builtin">enumerate</span><span class="token punctuation">(</span>docs<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        content <span class="token operator">=</span> json<span class="token punctuation">.</span>loads<span class="token punctuation">(</span>doc<span class="token punctuation">)</span>        question <span class="token operator">=</span> content<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"question"</span><span class="token punctuation">)</span>        answer <span class="token operator">=</span> content<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"answer"</span><span class="token punctuation">)</span>        qa_blocks<span class="token punctuation">.</span>append<span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Q</span><span class="token interpolation"><span class="token punctuation">&#123;</span>i<span class="token punctuation">&#125;</span></span><span class="token string">: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>question<span class="token punctuation">&#125;</span></span><span class="token string">\n\n&lt;details>&lt;summary>A</span><span class="token interpolation"><span class="token punctuation">&#123;</span>i<span class="token punctuation">&#125;</span></span><span class="token string">:&lt;/summary>\n\n</span><span class="token interpolation"><span class="token punctuation">&#123;</span>answer<span class="token punctuation">&#125;</span></span><span class="token string">\n\n&lt;/details>"</span></span><span class="token punctuation">)</span>        body <span class="token operator">=</span> <span class="token string">"\n\n---\n\n"</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>qa_blocks<span class="token punctuation">)</span>    markdown <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"&lt;details>&lt;summary></span><span class="token interpolation"><span class="token punctuation">&#123;</span>title<span class="token punctuation">&#125;</span></span><span class="token string">&lt;/summary>\n\n</span><span class="token interpolation"><span class="token punctuation">&#123;</span>body<span class="token punctuation">&#125;</span></span><span class="token string">\n\n&lt;/details>"</span></span>    <span class="token keyword">return</span> markdownTITLE <span class="token operator">=</span> <span class="token string">'医学RAG问答系统'</span>DESCRIPTION <span class="token operator">=</span> <span class="token string">'输入医学问题，我会基于专业内容为你解答。'</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>main.py</code></p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> gradio <span class="token keyword">as</span> gr<span class="token keyword">from</span> langchain_core<span class="token punctuation">.</span>runnables <span class="token keyword">import</span> RunnableMap<span class="token punctuation">,</span> RunnableLambda<span class="token keyword">from</span> openai <span class="token keyword">import</span> OpenAI<span class="token keyword">from</span> RAG<span class="token punctuation">.</span>medical <span class="token keyword">import</span> <span class="token operator">*</span><span class="token keyword">import</span> warningswarnings<span class="token punctuation">.</span>filterwarnings<span class="token punctuation">(</span><span class="token string">"ignore"</span><span class="token punctuation">,</span> category<span class="token operator">=</span>FutureWarning<span class="token punctuation">)</span>client <span class="token operator">=</span> OpenAI<span class="token punctuation">(</span>    base_url<span class="token operator">=</span><span class="token string">"http://localhost:8000/v1"</span><span class="token punctuation">,</span>    api_key<span class="token operator">=</span><span class="token string">"n"</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">stream</span><span class="token punctuation">(</span>prompt<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> history<span class="token operator">=</span><span class="token boolean">None</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    messages <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>        messages<span class="token punctuation">.</span>append<span class="token punctuation">(</span><span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"system"</span><span class="token punctuation">,</span> <span class="token string">"content"</span><span class="token punctuation">:</span> system_prompt<span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    messages<span class="token punctuation">.</span>append<span class="token punctuation">(</span><span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span> <span class="token string">"content"</span><span class="token punctuation">:</span> prompt<span class="token punctuation">&#125;</span><span class="token punctuation">)</span>        response <span class="token operator">=</span> client<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>completions<span class="token punctuation">.</span>create<span class="token punctuation">(</span>        model<span class="token operator">=</span><span class="token string">"./model/Qwen2.5-7B-Instruct"</span><span class="token punctuation">,</span>        messages<span class="token operator">=</span>messages<span class="token punctuation">,</span>        stream<span class="token operator">=</span><span class="token boolean">True</span>    <span class="token punctuation">)</span>    <span class="token keyword">for</span> chunk <span class="token keyword">in</span> response<span class="token punctuation">:</span>        <span class="token keyword">if</span> chunk<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>delta<span class="token punctuation">.</span>content<span class="token punctuation">:</span>            <span class="token keyword">yield</span> chunk<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>delta<span class="token punctuation">.</span>content        <span class="token keyword">def</span> <span class="token function">summarize_context_query</span><span class="token punctuation">(</span>question<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> history<span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""使用大模型总结历史提问，生成更准确的检索查询"""</span>    <span class="token keyword">if</span> <span class="token keyword">not</span> history<span class="token punctuation">:</span>        <span class="token keyword">return</span> question        <span class="token comment"># 构建历史对话列表</span>    history <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string-interpolation"><span class="token string">f'Q: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>user_msg<span class="token punctuation">&#125;</span></span><span class="token string">\nA: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>response<span class="token punctuation">&#125;</span></span><span class="token string">'</span></span> <span class="token keyword">for</span> user_msg<span class="token punctuation">,</span> response <span class="token keyword">in</span> history<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">3</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">]</span>  <span class="token comment"># 最近3轮</span>    system_prompt <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"""你是一个重写助手，任务是将用户当前的问题结合历史对话重写为一个**自包含、清晰明确的问题句子**，以便发送给问答系统。请遵循以下规则：1. 仅重写问题，**不回答**；2. 替换句中的所有代词（如“它”、“这”、“那”等）为具体的名词或短语，当可能存在多个指代对象时，使用最新的历史对话中的信息；3. 推理指代、补全省略，使问题独立完整；4. 不引入历史中未提及的信息； 5. 只输出**重写后的问题句子**，不添加任何解释或注释。"""</span></span>        summary_prompt <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"""历史对话：</span><span class="token interpolation"><span class="token punctuation">&#123;</span><span class="token builtin">chr</span><span class="token punctuation">(</span><span class="token number">20</span><span class="token punctuation">)</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">&#123;</span>i<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">&#125;</span></span><span class="token string">. </span><span class="token interpolation"><span class="token punctuation">&#123;</span>q<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span> <span class="token keyword">for</span> i<span class="token punctuation">,</span> q <span class="token keyword">in</span> <span class="token builtin">enumerate</span><span class="token punctuation">(</span>history<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span></span><span class="token string">当前问题：</span><span class="token interpolation"><span class="token punctuation">&#123;</span>question<span class="token punctuation">&#125;</span></span><span class="token string">你重写的问题："""</span></span>        <span class="token keyword">try</span><span class="token punctuation">:</span>        messages <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"system"</span><span class="token punctuation">,</span> <span class="token string">"content"</span><span class="token punctuation">:</span> system_prompt<span class="token punctuation">&#125;</span><span class="token punctuation">,</span>                    <span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span> <span class="token string">"content"</span><span class="token punctuation">:</span> summary_prompt<span class="token punctuation">&#125;</span><span class="token punctuation">]</span>        response <span class="token operator">=</span> client<span class="token punctuation">.</span>chat<span class="token punctuation">.</span>completions<span class="token punctuation">.</span>create<span class="token punctuation">(</span>            model<span class="token operator">=</span><span class="token string">"./model/Qwen2.5-7B-Instruct"</span><span class="token punctuation">,</span>            messages<span class="token operator">=</span>messages<span class="token punctuation">,</span>            stream<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">,</span>            max_tokens<span class="token operator">=</span><span class="token number">50</span>        <span class="token punctuation">)</span>                summarized_query <span class="token operator">=</span> response<span class="token punctuation">.</span>choices<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">.</span>message<span class="token punctuation">.</span>content<span class="token punctuation">.</span>strip<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"原始问题: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>question<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"重写问题: </span><span class="token interpolation"><span class="token punctuation">&#123;</span>summarized_query<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>        <span class="token keyword">return</span> summarized_query            <span class="token keyword">except</span> Exception <span class="token keyword">as</span> e<span class="token punctuation">:</span>        <span class="token keyword">return</span> question<span class="token keyword">def</span> <span class="token function">build_context_query</span><span class="token punctuation">(</span>question<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> history<span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""构建包含历史上下文的查询"""</span>    <span class="token keyword">if</span> <span class="token keyword">not</span> history<span class="token punctuation">:</span>        <span class="token keyword">return</span> question        <span class="token keyword">return</span> summarize_context_query<span class="token punctuation">(</span>question<span class="token punctuation">,</span> history<span class="token punctuation">)</span>rag_chain <span class="token operator">=</span> <span class="token punctuation">(</span>    RunnableMap<span class="token punctuation">(</span><span class="token punctuation">&#123;</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> <span class="token keyword">lambda</span> x<span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> <span class="token keyword">lambda</span> x<span class="token punctuation">:</span> build_context_query<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">)</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"context"</span><span class="token punctuation">:</span> retrieve<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"question"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"context"</span><span class="token punctuation">:</span> <span class="token string">"\n"</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">'context'</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"references"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">'context'</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"prompt"</span><span class="token punctuation">:</span> prompt_template<span class="token punctuation">.</span><span class="token builtin">format</span><span class="token punctuation">(</span>question<span class="token operator">=</span>x<span class="token punctuation">[</span><span class="token string">"question"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> context<span class="token operator">=</span>x<span class="token punctuation">[</span><span class="token string">"context"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"history"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>        <span class="token string">"references"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"references"</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    <span class="token operator">|</span> RunnableLambda<span class="token punctuation">(</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> <span class="token punctuation">&#123;</span>        <span class="token string">"response"</span><span class="token punctuation">:</span> stream<span class="token punctuation">(</span>x<span class="token punctuation">[</span><span class="token string">"prompt"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> x<span class="token punctuation">[</span><span class="token string">"history"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"references"</span><span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token string">"references"</span><span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">qa_interface</span><span class="token punctuation">(</span>message<span class="token punctuation">,</span> history<span class="token punctuation">)</span><span class="token punctuation">:</span>    result <span class="token operator">=</span> rag_chain<span class="token punctuation">.</span>invoke<span class="token punctuation">(</span><span class="token punctuation">&#123;</span><span class="token string">"question"</span><span class="token punctuation">:</span> message<span class="token punctuation">,</span> <span class="token string">"history"</span><span class="token punctuation">:</span> history<span class="token punctuation">&#125;</span><span class="token punctuation">)</span>    generator <span class="token operator">=</span> result<span class="token punctuation">[</span><span class="token string">"response"</span><span class="token punctuation">]</span>    references <span class="token operator">=</span> result<span class="token punctuation">[</span><span class="token string">"references"</span><span class="token punctuation">]</span>    response <span class="token operator">=</span> <span class="token string">""</span>    <span class="token keyword">for</span> token <span class="token keyword">in</span> generator<span class="token punctuation">:</span>        response <span class="token operator">+=</span> token        <span class="token keyword">yield</span> response    references_text <span class="token operator">=</span> format_references<span class="token punctuation">(</span>references<span class="token punctuation">)</span>            response <span class="token operator">+=</span> <span class="token string">"\n\n"</span> <span class="token operator">+</span> references_text    <span class="token keyword">yield</span> responsechat <span class="token operator">=</span> gr<span class="token punctuation">.</span>ChatInterface<span class="token punctuation">(</span>    fn<span class="token operator">=</span>qa_interface<span class="token punctuation">,</span>    title<span class="token operator">=</span>TITLE<span class="token punctuation">,</span>    description<span class="token operator">=</span>DESCRIPTION<span class="token punctuation">)</span>chat<span class="token punctuation">.</span>launch<span class="token punctuation">(</span>server_name<span class="token operator">=</span><span class="token string">"0.0.0.0"</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>使用体验如下：</p><p><img src="https://blogfiles.oss.fyz666.xyz/gif/182a26c7-8298-453c-a6d5-81ca4aa6a4d6.gif" alt=""></p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/rag/">RAG</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12897/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（七）：GRPO算法</title>
      <link>https://blog.fyz666.xyz/blog/12877/</link>
      <guid>https://blog.fyz666.xyz/blog/12877/</guid>
      <pubDate>Thu, 10 Jul 2025 10:07:45 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h2 id=&quot;前情提要&quot;&gt;&lt;a href=&quot;#前情提要&quot; class=&quot;headerlink&quot; title=&quot;前情提要&quot;&gt;&lt;/a&gt;前情提要&lt;/h2&gt;&lt;div class=&quot;note primary simple&quot;&gt;&lt;p&gt;2025年1月27日，因中国 AI 初创企业</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h2 id="前情提要"><a href="#前情提要" class="headerlink" title="前情提要"></a>前情提要</h2><div class="note primary simple"><p>2025年1月27日，因中国 AI 初创企业 DeepSeek 发布低成本高性能模型，英伟达市值单日缩水约6000亿美元，股价暴跌约17%。</p></div><p><img src="https://blogfiles.oss.fyz666.xyz/jpg/f212030c-dc0d-4926-b6e5-2f9579bdbf27.jpg" alt="NVDA stock price 1-week chart. Source: Finbold" style="zoom:50%;" /></p><p>这事在圈内激起了不小的波澜，众所周知，大模型的训练过程十分消耗算力，尤其是常规的RLHF阶段使用的PPO算法，流程复杂又烧钱。比如OpenAI在训练ChatGPT时，动辄上万张A100、H100堆起来跑，而Nvidia几乎是算力的唯一供应商。</p><p>而DeepSeek团队推翻了这一现状：训练一个性能能够对标主流水平的大模型，并不需要那么多的卡（仅使用约2k张H800）。他们通过一套更高效、轻量的流程大幅简化了训练的成本与复杂度，其中最核心的改进便是引入了<strong>Group Relative Policy Optimization（GRPO）算法</strong>。那么，这个算法究竟是何方神圣？感兴趣的朋友可以直接阅读下面的原论文：</p><a class="tag-Link" target="_blank" href="https://arxiv.org/abs/2501.12948">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=arxiv.org" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">DeepSeek-R1</div>            <div class="tag-link-sitename"> $\text{ar}\chi\text{iv}$​</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><hr><p>插句题外话，博主在当时几乎第一时间就把论文下载了下来：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/c8d7236b-43e2-4e18-8a5a-c9b825200b3f.png" alt=""></p><p>然而由于<del>实验室搬砖</del>科研等各种事，一直拖着没认真读（唉，读了又怎么样呢？当时的显存连个7b的模型都放不下）。时隔多月，终于显存和心理都空出一点空间，总算能静下心来，把这个 GRPO算法好好研究、复现了一遍。</p><p>DeepSeek-R1的论文其实对整体的训练流程也做了不少改进，但核心贡献还是在于其在强化学习阶段引入的GRPO算法，因此我主要研究了这个强化学习算法的细节。</p><p>博主写这篇文章的目的，就是记录一下自己对 GRPO 算法的理解，以及在复现过程中整理的一些关键细节和实践心得。</p><h2 id="GRPO算法的思想"><a href="#GRPO算法的思想" class="headerlink" title="GRPO算法的思想"></a>GRPO算法的思想</h2><p>GRPO算法，即组相对策略优化（Group Relative Policy Optimization），其核心思想是：不再单独对一个token序列打分，而是在同一个prompt下采样多条输出，形成一个分组，然后在分组中计算输出间的相对优劣，得到每条输出的组内相对得分，以此指导模型更新。</p><p>除此以外，GRPO算法和先前的PPO算法还有一些区别：</p><ol><li>GRPO算法省去了Reward Model与Critic Model。</li><li>论文针对任务数据集特点设计了一些基于规则的Reward Function，来对输出进行打分。</li><li>与PPO算法中token-level的优势函数不同，GRPO算法中的优势函数是sample-level的，即一个输出的所有token共享同一个组内优势值。</li><li>GRPO算法将PPO算法的Reward中添加的KL正则项移到了损失函数中。</li><li>GRPO算法使用了另一种不同的KL散度估计方法。</li></ol><p>我们来逐点分析。首先看第二点，论文中提到：</p><blockquote><p>We do not apply the outcome or process neural reward model in developing DeepSeek-R1-Zero, because we find that <strong>the neural reward model may suffer from reward hacking</strong> in the large-scale reinforcement learning process, and retraining the reward model needs additional training resources and it complicates the whole training pipeline.</p></blockquote><p>我在微调RLHF的时候确实也遇到过这种情况。常规PPO算法非常依赖于Reward Model的质量，但基于神经网络的Reward Model在用于强化学习时经常会遇到<strong>Reward Hacking</strong>，即模型在训练时学会了钻Reward Model的漏子，用一些不好的输出获取非常高的得分，即模型学会了“讨好”奖励模型，而不去努力产生高质量的输出（<del>不好，这怎么像极了我做科研时的样子</del>）。</p><p><img src="https://blogfiles.oss.fyz666.xyz/jpeg/3b2da4ed-939f-4e34-aeb9-95fdc4821e8d.jpeg" alt=""></p><p>而对神经网络攻击稍有了解的朋友就会知道，神经网络模型作为一个高度不可解释的黑盒系统，很容易被“投其所好”地构造出满足特定输出的输入 —— 即使这些输入在人类看来是无意义甚至错误的。</p><p>正是由于这种脆弱性，我们训练的语言模型只需要偶然间发现一条让奖励模型输出高分的捷径，它就可能在这条离谱的道路上越走越远，或产生模式崩坏、或输出质量奇差的内容。</p><p>不好，跑题了。。。</p><hr><p>那么论文提到的基于规则的Reward函数主要有两种：</p><ol><li>基于正确性的奖励（Accuracy rewards）：对于一些有“标准答案”的任务，例如具有准确答案的数学问题、代码生成任务等，可以根据模型答题是否准确来给一定的奖励。</li><li>基于格式的奖励（Format rewards）：根据模型的输出结果是否满足一定的格式要求来进行给分。如论文提到<strong>将模型的思维过程置于<code>&lt;think&gt;</code>和<code>&lt;/think&gt;</code>标签之间</strong>，如模型输出的内容满足此条件则给奖励。</li></ol><p>不过，对于通用型任务而言，Accuracy rewards不一定能定义，这种情况下，也许还得和前面PPO算法类似，去训练一个通用的Reward Model来辅助。</p><hr><p>对于第一点，既然都定义了基于规则的奖励函数了，那么Reward Model、Critic Model自然是不需要了。</p><p>我们来看第三点。</p><p>在PPO算法中，优势函数的计算分为以下步骤：</p><ol><li>通过Critic Model计算每个token的Value。</li><li>通过每个token的Value计算TD delta（时序差分序列）。</li><li>通过时序差分序列按GAE算法计算优势函数。</li></ol><p>对这一过程的细节不太了解的朋友可以翻看我前面的文章：<a href="/blog/12706/">PPO算法</a>、<a href="/blog/12778/">RLHF实践</a>。</p><p>由此可见，PPO算法中的优势函数也是token-level的，即每个token对应一个优势值。</p><p>但在GRPO算法中，则有所不同，如论文中所写，GRPO算法使用的优势函数是通过组内每个样本的Reward计算得来的：</p><script type="math/tex; mode=display">A_i=\frac{r_i-\text{mean}(\{r_1,r_2,\dots,r_G\})}{\text{std}(\{r_1,r_2,\dots,r_G\})}</script><p>诶，我们发现，这样一来，相当于每个样本中的所有token是共享同一个优势函数值的。</p><hr><p>再看第四点和第五点，我们回顾PPO算法的优化目标：</p><script type="math/tex; mode=display">\max_{\pi_\theta}\mathbb{E}_{x\in X,y\sim\pi_{\theta}(*\mid x)}[R(x,y)-\beta\cdot\mathbb{D}_{\text{KL}}(\pi_\theta(*\mid x)\|\pi_\text{ref}(*\mid x))]</script><p>在Reward中添加了一个KL散度惩罚项，而GRPO算法中则将这一项惩罚项移到了损失函数中：</p><script type="math/tex; mode=display">\frac{1}{G} \sum_{i=1}^{G} \left(     \min\left(         \frac{\pi_\theta(o_i \mid q)}{\pi_{\theta_{\text{old}}}(o_i \mid q)} A_i,\         \operatorname{clip}\left(             \frac{\pi_\theta(o_i \mid q)}{\pi_{\theta_{\text{old}}}(o_i \mid q)},\ 1 - \varepsilon,\ 1 + \varepsilon         \right) A_i     \right)     - \beta\cdot\mathbb{D}_{\text{KL}}\left( \pi_\theta \,\|\, \pi_{\text{ref}} \right)\right)</script><p>容易发现，除掉多了个分组大小 <script type="math/tex">G</script>​ 以外，GRPO的损失函数前半部分与PPO是完全一致的，而后面多出来的部分就是KL散度惩罚项。</p><p>PPO中，计算KL散度的公式是：</p><script type="math/tex; mode=display">\mathbb{D}_{\text{KL}} ( \pi_{\theta} \,\|\, \pi_{\text{ref}} ) = \log \frac{\pi_{\theta}(o_i \mid q)}{\pi_{\text{ref}}(o_i \mid q)}</script><p>而在GRPO中，论文采用了下面这个公式：</p><script type="math/tex; mode=display">\mathbb{D}_{\text{KL}}(\pi_\theta \,\|\, \pi_{\text{ref}}) = \frac{\pi_{\text{ref}}(o_i \mid q)}{\pi_\theta(o_i \mid q)} - \log \frac{\pi_{\text{ref}}(o_i \mid q)}{\pi_\theta(o_i \mid q)} - 1</script><p>显然，前者是后者的一个一阶近似，故后者能够更准确地估计真实的KL散度。</p><h2 id="GRPO算法复现"><a href="#GRPO算法复现" class="headerlink" title="GRPO算法复现"></a>GRPO算法复现</h2><ul><li>Pretrained-Model：<a href="https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct">Qwen/Qwen2.5-1.5B-Instruct</a> (太惨了，即使套了LoRA也只训的动1.5B)</li><li>Dataset：<a href="https://huggingface.co/datasets/openai/gsm8k">openai/gsm8k</a> 包含数千道英文小学数学题的数据集，每条数据包含一个<code>answer</code>字段，提供问题的解答，并且固定了纯数字答案格式：置于末尾<code>####</code>之后。</li><li>Task：让模型具备使用思维链解决数学问题的能力。</li></ul><h3 id="奖励函数定义"><a href="#奖励函数定义" class="headerlink" title="奖励函数定义"></a>奖励函数定义</h3><p>与论文一致，我们主要定义了两个奖励函数：</p><ol><li>准确性奖励：我们要求模型给出问题的答案，如答案准确则奖励。</li><li>格式奖励：要求模型将思考过程包含在一对<code>&lt;think&gt;</code>和<code>&lt;/think&gt;</code>之间，并且将最终答案包含在一对<code>&lt;answer&gt;</code>和<code>&lt;/answer&gt;</code>之间，如格式准确则奖励。</li></ol><p>另外，考虑到严格达成两个奖励函数非常困难，我们还相应地提供了两个稍宽松的奖励。</p><ol><li>只要答案是一个纯数字，就给一定的奖励。</li><li>只要匹配到了<code>&lt;think&gt;</code>、<code>&lt;/think&gt;</code>、<code>&lt;answer&gt;</code>和<code>&lt;/answer&gt;</code>中的一部分，就给一定的奖励，但如果匹配到多个，则扣除奖励。</li></ol><p>奖励函数的定义详见<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/GRPO/cot_reward.py">此文件</a>。</p><h3 id="数据处理"><a href="#数据处理" class="headerlink" title="数据处理"></a>数据处理</h3><p>我们直接套用<code>Qwen/Qwen2.5-1.5B-Instruct</code>模型对应的tokenizer所提供的chat template，并定义一个系统提示词：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">You are given a problem.Think about it and provide your working out.Place it between &lt;think&gt; and &lt;&#x2F;think&gt;.Then, provide your numeric answer between &lt;answer&gt; and &lt;&#x2F;answer&gt;. For example:&lt;think&gt;...&lt;&#x2F;think&gt;&lt;answer&gt;...&lt;&#x2F;answer&gt;<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>核心代码：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">extract_hash_answer</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> text<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">if</span> <span class="token string">"####"</span> <span class="token keyword">not</span> <span class="token keyword">in</span> text<span class="token punctuation">:</span>        <span class="token keyword">return</span> <span class="token boolean">None</span>    <span class="token keyword">return</span> text<span class="token punctuation">.</span>split<span class="token punctuation">(</span><span class="token string">"####"</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">.</span>strip<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>replace<span class="token punctuation">(</span><span class="token string">','</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">__getitem__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> idx<span class="token punctuation">)</span><span class="token punctuation">:</span>    data <span class="token operator">=</span> self<span class="token punctuation">.</span>dataset<span class="token punctuation">[</span>idx<span class="token punctuation">]</span>    question <span class="token operator">=</span> data<span class="token punctuation">[</span><span class="token string">'question'</span><span class="token punctuation">]</span>    answer <span class="token operator">=</span> self<span class="token punctuation">.</span>extract_hash_answer<span class="token punctuation">(</span>data<span class="token punctuation">[</span><span class="token string">'answer'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        prompt <span class="token operator">=</span> self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>apply_chat_template<span class="token punctuation">(</span><span class="token punctuation">[</span>        <span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"system"</span><span class="token punctuation">,</span> <span class="token string">'content'</span><span class="token punctuation">:</span> self<span class="token punctuation">.</span>system_prompt<span class="token punctuation">&#125;</span><span class="token punctuation">,</span>        <span class="token punctuation">&#123;</span><span class="token string">"role"</span><span class="token punctuation">:</span> <span class="token string">"user"</span><span class="token punctuation">,</span> <span class="token string">'content'</span><span class="token punctuation">:</span> question<span class="token punctuation">&#125;</span>    <span class="token punctuation">]</span><span class="token punctuation">,</span> add_generation_prompt<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">,</span> tokenize<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> <span class="token punctuation">&#123;</span>        <span class="token string">'prompt'</span><span class="token punctuation">:</span> prompt<span class="token punctuation">,</span>        <span class="token string">'answer'</span><span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">(</span>answer<span class="token punctuation">)</span>    <span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="训练流程"><a href="#训练流程" class="headerlink" title="训练流程"></a>训练流程</h3><p>甚至连伪代码都懒得写了，大概写一下流程吧：</p><ol><li>定义一个数据收集容器：Data Buffer。</li><li>从数据集中取一条 prompt，生成 <script type="math/tex">G</script> 条输出序列，从而得到大小为 <script type="math/tex">G</script> 的分组。</li><li>根据定义好的奖励函数，对步骤 2 生成分组内的所有输出序列计算得分，然后在组内进行比较，求得相对得分（作为优势函数）。由此，得到一组数据。</li><li>将步骤 3 得到的数据添加到Data Buffer。</li><li>若Data Buffer的大小达到一次迭代需要的数据量（等效 Batch size），则进入步骤 6，否则回到步骤 2。</li><li>遍历Data Buffer，每次取Batch size个数据，按前面的公式计算KL散度、损失函数，并进行策略迭代。</li><li>遍历完成，清空Data Buffer，回到步骤 2。</li></ol><p>本部分完整代码见文末仓库。</p><h3 id="训练效果"><a href="#训练效果" class="headerlink" title="训练效果"></a>训练效果</h3><p>训练曲线：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/cd33ab82-0cc9-4a4b-ad91-9f80638526d9.png" style="zoom:30%;" /></p><p>注意到总体 Reward 还是在上升的，并且我们还分别记录观察了三个独立的Reward函数的变化趋势，发现模型在准确率、格式方面都有所提升，唯独这个Soft Format在上升到一定程度后有所下降，观察到模型有时会重复输出answer的闭合tag：<code>&lt;/answer&gt;</code>，目前还不知道是什么原因（明明给了一定的惩罚？）。</p><p>另外，每隔10次迭代，打印了一下模型生成的结果，发现模型确实能做对一些题，并且在格式上也有所对齐，思考过程有模有样。下面贴出几个例子。</p><div class="gallery-container" data-type="data" data-button="">      <div class="gallery-data">[{"url":"https://blogfiles.oss.fyz666.xyz/png/ef15f64a-6882-474b-b237-c9a27e04b710.png","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/png/4b50a5c7-e054-4e18-aa09-52a3a2ff3207.png","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/png/4d8c53f8-110a-4b98-b351-3fb056558503.png","alt":""},{"url":"https://blogfiles.oss.fyz666.xyz/png/8da29788-480a-4dc7-bac6-c1c5b19ecf15.png","alt":""}]</div>      <div class="gallery-items">      </div>    </div><p>最终，博主将迭代了400个step的模型与未经过训练的初始模型进行了测试与对比，结果如下：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">Qwen Accuracy: 0.4314 Formatted: 0.6088 GRPO Accuracy: 0.5610 Formatted: 0.9121<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>可见经过GRPO迭代的模型已经具备了较强的格式对齐能力，在准确率方面也有了一定的提升，意味着训练还是有一定的效果的。</p><hr><p>本文相关代码已开源于下面仓库：</p><a class="tag-Link" target="_blank" href=" https://github.com/windshadow233/tiny-llm-training/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">tiny-llm-training</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><div class="note warning simple"><p><del>由于实验中途突然被抓去跑了一些别的东西，只好先把实验停了，在无法确保实验最终效果的情况下，尚未开源本文代码，敬请期待！</del></p><p>只是实现了一个微小的demo，在细节方面仍有所欠缺，有待进一步斟酌，希望大佬们不吝赐教！</p></div>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">强化学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/grpo-%E7%AE%97%E6%B3%95/">GRPO 算法</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12877/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（六）：DPO算法</title>
      <link>https://blog.fyz666.xyz/blog/12794/</link>
      <guid>https://blog.fyz666.xyz/blog/12794/</guid>
      <pubDate>Thu, 03 Jul 2025 06:35:06 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;前面几篇文章已经基本实现了常规RLHF算法训练大模型的流程，从监督微调（SFT）、奖励模型训练（RM）到使用 PPO</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>前面几篇文章已经基本实现了常规RLHF算法训练大模型的流程，从监督微调（SFT）、奖励模型训练（RM）到使用 PPO 进行强化学习（RL）优化模型行为。然而，我们同样注意到了当前用于强化学习的PPO算法有一些缺点：训练过程复杂、硬件资源消耗大，还往往训练不稳定、调参困难。</p><p>为了解决常规RLHF算法中的这些问题，研究者随后提出了<strong>DPO（Direct Preference Optimization）算法</strong>，它能够绕过RLHF算法中的奖励模型训练以及后面的强化学习训练阶段，在完成监督微调之后，直接通过人类偏好对比数据对模型进行对齐优化，相当于实现了<strong>不需要RL的RLHF</strong>。</p><p>建议有兴趣深入了解的朋友直接阅读原论文。</p><a class="tag-Link" target="_blank" href=" https://arxiv.org/abs/2305.18290">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=arxiv.org" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">Direct Preference Optimization</div>            <div class="tag-link-sitename"> $\text{ar}\chi\text{iv}$​</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><hr><h2 id="PPO算法的优化目标"><a href="#PPO算法的优化目标" class="headerlink" title="PPO算法的优化目标"></a>PPO算法的优化目标</h2><p>前面提到的PPO算法作为一种强化学习算法，其在大语言模型训练任务上，优化的最终目标实际上是生成token序列的Reward的期望值，假设我们已经有了一个完美的Reward Model：<script type="math/tex">R(x,y)</script>，表示给定prompt <script type="math/tex">x</script> 且模型输出token序列为 <script type="math/tex">y</script> 时的Reward，我们可以将优化目标简单写为：</p><script type="math/tex; mode=display">\max_{\pi_\theta}\mathbb{E}_{x\in X,y\sim\pi_{\theta}(*\mid x)}[R(x,y)]</script><p>不过，考虑到实际训练时需要对模型策略的KL散度做一个约束，我们还要在优化目标中添加一个惩罚项，于是优化目标实际上是：</p><script type="math/tex; mode=display">\max_{\pi_\theta}\mathbb{E}_{x\in X,y\sim\pi_{\theta}(*\mid x)}[R(x,y)-\beta\cdot\mathbb{D}_{\text{KL}}(\pi_\theta(*\mid x)\|\pi_\text{ref}(*\mid x))]</script><p>上式中，<script type="math/tex">\beta</script> 是惩罚系数，<script type="math/tex">\pi_{\text{ref}}(y\mid x)</script> 则是参考模型给出的概率分布。<script type="math/tex">\mathbb{D}_{\text{KL}}(* \| *)</script> 表示两个概率分布的KL散度，其定义如下：</p><script type="math/tex; mode=display">\mathbb{D}_{\text{KL}}(\pi_1(*\mid x) \| \pi_2(*\mid x)) =\mathbb{E}_{y\sim\pi_1(*\mid x)}[\log\frac{\pi_1(y\mid x)}{\pi_2(y\mid x)}]</script><h2 id="求解优化目标"><a href="#求解优化目标" class="headerlink" title="求解优化目标"></a>求解优化目标</h2><p>将KL散度表达式代入优化目标，得到：</p><script type="math/tex; mode=display">\max_{\pi_\theta}\mathbb{E}_{x\in X,y\sim\pi_{\theta}(*\mid x)}[R(x,y)-\beta\cdot\log\frac{\pi_\theta(y\mid x)}{\pi_\text{ref}(y\mid x)}]</script><p>将其改为求极小值，除以常数 <script type="math/tex">\beta</script>，并稍加变形，得到：</p><script type="math/tex; mode=display">\begin{aligned}&\min_{\pi_\theta}\mathbb{E}_{x\in X,y\sim\pi_{\theta}(*\mid x)}[\log\frac{\pi_\theta(y\mid x)}{\pi_\text{ref}(y\mid x)}-\frac{1}{\beta}\cdot R(x,y)]\\&=\min_{\pi_\theta}\mathbb{E}_{x\in X,y\sim\pi_{\theta}(*\mid x)}[\log\frac{\pi_\theta(y\mid x)}{\pi_\text{ref}(y\mid x)\exp(\frac{1}{\beta}\cdot R(x,y))}]\end{aligned}</script><p>这里，论文做了一个操作，强行让对数部分成为一个新的 KL散度，即让分母部分通过一个归一化操作成为概率分布：</p><p>定义 <script type="math/tex">Z(x)</script>：</p><script type="math/tex; mode=display">Z(x) = \sum_{y}\pi_\text{ref}(y\mid x)\exp(\frac{1}{\beta}\cdot R(x,y))</script><p>可见 <script type="math/tex">Z(x)</script> 是关于 <script type="math/tex">x</script> 的函数，并且只与 <script type="math/tex">\pi_\text{ref}</script> 、奖励模型有关，与待训练模型 <script type="math/tex">\pi_\theta</script> 无关。</p><p>将 <script type="math/tex">Z(x)</script> 引入优化目标 <script type="math/tex">\log</script> 运算的分母，再从外部减去，我们得到：</p><script type="math/tex; mode=display">\min_{\pi_\theta}\mathbb{E}_{x\in X,y\sim\pi_{\theta}(*\mid x)}[\log\frac{\pi_\theta(y\mid x)}{\frac{1}{Z(x)}\pi_\text{ref}(y\mid x)\exp(\frac{1}{\beta}\cdot R(x,y))}-\log Z(x)]</script><p>此时，左侧的对数就变成了一个KL散度的形式了，我们记 </p><script type="math/tex; mode=display">\pi^*(y\mid x)\overset{\triangle}{=}\frac{1}{Z(x)}\pi_\text{ref}(y\mid x)\exp(\frac{1}{\beta}\cdot R(x,y))</script><p>容易验证 <script type="math/tex">\pi^*(*\mid x)</script> 是一个概率分布，因此，上式可以写为：</p><script type="math/tex; mode=display">\min_{\pi_\theta}\mathbb{E}_{x\in X}[\mathbb{D}_\text{KL}(\pi_\theta(*\mid x)\|\pi^*(*\mid x))-\log Z(x)]</script><p>显然，由于上式的右侧项 <script type="math/tex">\log Z(x)</script> 与 <script type="math/tex">\pi_\theta</script> 无关，我们可以忽略它。又从KL散度的性质得到，当且仅当</p><script type="math/tex; mode=display">\pi_\theta(y\mid x) = \pi^*(y\mid x)=\frac{1}{Z(x)}\pi_\text{ref}(y\mid x)\exp(\frac{1}{\beta}\cdot R(x,y))</script><p>时，KL散度达到最小值0。</p><p>看上去我们已经直接求出了目标的显式解，但可惜的是，这个 <script type="math/tex">Z(x)</script> 并不好计算，因为它需要对于一个prompt <script type="math/tex">x</script>，遍历所有Reference Model可能产生的 <script type="math/tex">y</script> 才能精确计算，如用蒙特卡洛方法估计，也得采样相当数量的 <script type="math/tex">y</script>​，这个过程十分消耗算力。</p><p>另外，这个解还依赖于我们训练好的完美的 <script type="math/tex">R(x,y)</script>，因此在论文作者看来还不够方便，毕竟他们的目标是想要跳过后面两个步骤。</p><h2 id="新的优化目标"><a href="#新的优化目标" class="headerlink" title="新的优化目标"></a>新的优化目标</h2><p>将 <script type="math/tex">R(x,y)</script> 用两个概率分布以及 <script type="math/tex">Z(x)</script> 反过来表示：</p><script type="math/tex; mode=display">\begin{aligned}R(x,y)&=\beta\log(Z(x)\frac{\pi^*(y\mid x)}{\pi_\text{ref}(y\mid x)})\\&=\beta\log\frac{\pi^*(y\mid x)}{\pi_\text{ref}(y\mid x)}+\beta\log Z(x)\end{aligned}</script><p>我们考虑前面训练Reward Model时用到的偏序数据对 $(x,y_c,y_r)$：其中 <script type="math/tex">x</script> 为 prompt，<script type="math/tex">y_c</script> 表示 chosen 的回复，<script type="math/tex">y_r</script> 表示 rejected 的回复。</p><p>对于偏序数据的建模，论文中提到了<strong><a href="https://en.wikipedia.org/wiki/Bradley%E2%80%93Terry_model">Bradley-Terry Model</a></strong>。</p><p>简单说来，它通过一个定量公式估计了一对比较对象的两种比较结果分别发生的可能性：</p><script type="math/tex; mode=display">P(i \succ j)=\frac{p_i}{p_i+p_j}</script><p>其中，<script type="math/tex">i \succ j</script> 表示在某种标准上 <script type="math/tex">i</script>  超越了 <script type="math/tex">j</script>，<script type="math/tex">p_i,p_j</script>​ 分别表示两个对象的某种得分（得分必须为正数）。</p><p>在建模 <script type="math/tex">(x,y_c,y_r)</script> 这一文本问答的偏序数据时，论文采用了下述公式：</p><script type="math/tex; mode=display">\begin{aligned}P(y_c \succ y_r) &= \frac{\exp R(x,y_c)}{\exp R(x,y_c)+\exp R(x,y_r)}\\&=\frac{1}{1+\exp [-(R(x,y_c)-R(x,y_r))]}\\&=\sigma(R(x,y_c)-R(x,y_r))\end{aligned}</script><p>其中 <script type="math/tex">\sigma(s)=\frac{1}{1+\exp(-s)}</script> 为 Sigmoid 函数。</p><p>考虑到我们在最大化一个概率分布时，一般会去最小化其负对数似然，因此我们可以写出优化目标：</p><script type="math/tex; mode=display">-\log P(y_c \succ y_r) = -\log\sigma(R(x,y_c)-R(x,y_r))</script><p>这个目标有一个很大的好处在于，当我们把</p><script type="math/tex; mode=display">R(x,y)=\beta\log\frac{\pi^*(y\mid x)}{\pi_\text{ref}(y\mid x)}+\beta\log Z(x)</script><p>代入时，会发现正好把那坨 <script type="math/tex">Z(x)</script> 消掉了，从而得到下式：</p><script type="math/tex; mode=display">-\log\sigma[\beta(\log\frac{\pi^*(y_c\mid x)}{\pi_\text{ref}(y_c\mid x)}-\frac{\pi^*(y_r\mid x)}{\pi_\text{ref}(y_r\mid x)})]</script><p>这便是最终的优化目标。由此，我们的目标函数只与 <script type="math/tex">\pi^*</script> 相关，已经绕开了之前奖励模型的训练过程。</p><hr><p>综上，我们基本将论文中涉及到的核心过程推导了一遍，构造了一个无需奖励模型的优化目标，相比于PPO算法，DPO算法的最终形式简洁了不少，省去了显式的奖励模型训练过程，在优化过程中用到的模型也少了一半，节省了计算资源。</p><h2 id="代码实现"><a href="#代码实现" class="headerlink" title="代码实现"></a>代码实现</h2><h3 id="数据集"><a href="#数据集" class="headerlink" title="数据集"></a>数据集</h3><p>偏好数据集仍然使用了与之前相同的<a href="https://huggingface.co/datasets/OpenLLMAI/comparison_data">OpenLLMAI/comparison_data</a>，数据集的定义方法与前面训练Reward Model时类似，不过为了便于后续计算损失函数，这里我额外算了一个<code>label_mask</code>，用来屏蔽prompt和padding部分，防止这部分参与loss的计算。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">build_inputs</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> prompt_ids<span class="token punctuation">,</span> response_ids<span class="token punctuation">)</span><span class="token punctuation">:</span>    input_ids <span class="token operator">=</span> prompt_ids <span class="token operator">+</span> <span class="token punctuation">[</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>bos_token_id<span class="token punctuation">]</span> <span class="token operator">+</span> response_ids    input_ids <span class="token operator">=</span> input_ids<span class="token punctuation">[</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>max_length<span class="token punctuation">]</span>    <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>input_ids<span class="token punctuation">)</span> <span class="token operator">&lt;</span> self<span class="token punctuation">.</span>max_length<span class="token punctuation">:</span>        input_ids <span class="token operator">+=</span> <span class="token punctuation">[</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>eos_token_id<span class="token punctuation">]</span>    bos_pos <span class="token operator">=</span> input_ids<span class="token punctuation">.</span>index<span class="token punctuation">(</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>bos_token_id<span class="token punctuation">)</span>    label_mask <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">*</span> <span class="token punctuation">(</span>bos_pos <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">*</span> <span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>input_ids<span class="token punctuation">)</span> <span class="token operator">-</span> bos_pos <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span>    pad_len <span class="token operator">=</span> self<span class="token punctuation">.</span>max_length <span class="token operator">-</span> <span class="token builtin">len</span><span class="token punctuation">(</span>input_ids<span class="token punctuation">)</span>    <span class="token keyword">if</span> pad_len <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>        input_ids <span class="token operator">+=</span> <span class="token punctuation">[</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>pad_token_id<span class="token punctuation">]</span> <span class="token operator">*</span> pad_len        label_mask <span class="token operator">+=</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">*</span> pad_len    attention_mask <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">*</span> <span class="token punctuation">(</span>self<span class="token punctuation">.</span>max_length <span class="token operator">-</span> pad_len<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">*</span> pad_len    <span class="token keyword">return</span> input_ids<span class="token punctuation">,</span> attention_mask<span class="token punctuation">,</span> label_mask<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>计算 <script type="math/tex">\pi(y\mid x)</script> ，采用了取对数概率（<code>logits</code>）然后求和的方式，不过在计算得到了<code>logits</code>之后，需要先用前面计算得到的<code>label_mask</code>对prompt和padding部分做一个屏蔽，再进行求和：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">masked_sum</span><span class="token punctuation">(</span>values<span class="token punctuation">,</span> labels_mask<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">return</span> <span class="token punctuation">(</span>values <span class="token operator">*</span> labels_mask<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token builtin">sum</span><span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">.</span>squeeze<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><h3 id="训练"><a href="#训练" class="headerlink" title="训练"></a>训练</h3><p>核心部分的代码如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">for</span> step<span class="token punctuation">,</span> batch <span class="token keyword">in</span> tqdm<span class="token punctuation">(</span><span class="token builtin">enumerate</span><span class="token punctuation">(</span>dataloader<span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">,</span> desc<span class="token operator">=</span><span class="token string-interpolation"><span class="token string">f"Epoch </span><span class="token interpolation"><span class="token punctuation">&#123;</span>epoch <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">&#125;</span></span><span class="token string">/</span><span class="token interpolation"><span class="token punctuation">&#123;</span>num_epochs<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">,</span> dynamic_ncols<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">,</span> total<span class="token operator">=</span><span class="token builtin">len</span><span class="token punctuation">(</span>dataloader<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    input_ids <span class="token operator">=</span> batch<span class="token punctuation">[</span><span class="token string">'input_ids'</span><span class="token punctuation">]</span>    label_mask <span class="token operator">=</span> batch<span class="token punctuation">.</span>pop<span class="token punctuation">(</span><span class="token string">'label_mask'</span><span class="token punctuation">)</span>    <span class="token keyword">with</span> torch<span class="token punctuation">.</span>no_grad<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        logits_ref <span class="token operator">=</span> model_ref<span class="token punctuation">(</span><span class="token operator">**</span>batch<span class="token punctuation">)</span><span class="token punctuation">.</span>logits        log_prob_ref <span class="token operator">=</span> calculate_action_logsoftmax<span class="token punctuation">(</span>logits_ref<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> input_ids<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        log_prob_ref <span class="token operator">=</span> filter_mask<span class="token punctuation">(</span>log_prob_ref<span class="token punctuation">,</span> label_mask<span class="token punctuation">)</span>        log_prob_ref_chosen<span class="token punctuation">,</span> log_prob_ref_rejected <span class="token operator">=</span> log_prob_ref<span class="token punctuation">.</span>chunk<span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> dim<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">)</span>    <span class="token keyword">with</span> accelerator<span class="token punctuation">.</span>accumulate<span class="token punctuation">(</span>model<span class="token punctuation">)</span><span class="token punctuation">:</span>        logits <span class="token operator">=</span> model<span class="token punctuation">(</span><span class="token operator">**</span>batch<span class="token punctuation">)</span><span class="token punctuation">.</span>logits        log_prob <span class="token operator">=</span> calculate_action_logsoftmax<span class="token punctuation">(</span>logits<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> input_ids<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        log_prob <span class="token operator">=</span> filter_mask<span class="token punctuation">(</span>log_prob<span class="token punctuation">,</span> label_mask<span class="token punctuation">)</span>        log_prob_chosen<span class="token punctuation">,</span> log_prob_rejected <span class="token operator">=</span> log_prob<span class="token punctuation">.</span>chunk<span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> dim<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">)</span>                reward_chosen <span class="token operator">=</span> log_prob_chosen <span class="token operator">-</span> log_prob_ref_chosen        reward_rejected <span class="token operator">=</span> log_prob_rejected <span class="token operator">-</span> log_prob_ref_rejected        loss <span class="token operator">=</span> <span class="token operator">-</span>logsigmoid<span class="token punctuation">(</span>beta <span class="token operator">*</span> <span class="token punctuation">(</span>reward_chosen <span class="token operator">-</span> reward_rejected<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>mean<span class="token punctuation">(</span><span class="token punctuation">)</span>        accelerator<span class="token punctuation">.</span>backward<span class="token punctuation">(</span>loss<span class="token punctuation">)</span>        <span class="token keyword">if</span> accelerator<span class="token punctuation">.</span>sync_gradients<span class="token punctuation">:</span>            accelerator<span class="token punctuation">.</span>clip_grad_norm_<span class="token punctuation">(</span>lora_parameters<span class="token punctuation">,</span> <span class="token number">1.0</span><span class="token punctuation">)</span>            optimizer<span class="token punctuation">.</span>step<span class="token punctuation">(</span><span class="token punctuation">)</span>            scheduler<span class="token punctuation">.</span>step<span class="token punctuation">(</span><span class="token punctuation">)</span>            optimizer<span class="token punctuation">.</span>zero_grad<span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>在每个训练 step 中，执行如下操作：</p><ol><li>计算参考模型 <code>model_ref</code> 的对数概率 <code>log_prob_ref</code>。</li><li>计算待训练模型 <code>model</code> 的 <code>log_prob</code>。</li><li>计算两个 Reward：分别对 <code>chosen</code> 和 <code>rejected</code> 样本计算 <code>reward</code>（即与 reference model 输出对数概率的差值）。</li><li>DPO loss：使用 <code>-logsigmoid(beta * (r_c - r_r))</code> 作为 loss，其中 <code>beta</code> 是温度系数。</li><li>反向传播、梯度裁剪、迭代。</li></ol><h3 id="训练效果"><a href="#训练效果" class="headerlink" title="训练效果"></a>训练效果</h3><p>最初尝试训练时遇到了一些问题：</p><ol><li>一开始学习率设太高了（5e-5），结果模型在训练中后期迅速崩坏，什么都不输出（摆烂是吧？？？）。遂将学习率调至1e-6，问题解决。</li><li>由于我在之前做SFT时的数据量不是很大，如果这里将全部数据（大约10万条）全部用于训练，模型会忘记如何正常回答问题，对于所有问题都一视同仁地拒绝回答（或许边DPO边SFT可以缓解这个问题）。于是，取出25000-30000条数据用于训练即可。</li></ol><p>分别观察Chosen Reward、Rejected Reward以及Loss的趋势：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/ba3ca2b2-3a86-49ff-b0f5-62030232922c.png" style="zoom:50%;" /></p><p>训练过程相比于PPO更加稳定，模型也逐渐能够给出符合我们希望的正向价值观的输出，下面是几个DPO模型与SFT模型的面对诱导性问题和正常问题的回答对比示例：</p><ul><li>诱导性问题：</li></ul><p><img src="https://blogfiles.oss.fyz666.xyz/png/43e0d57a-8205-4bf5-941b-90d0bc6cf360.png" alt=""></p><p><img src="https://blogfiles.oss.fyz666.xyz/png/08c2fe0b-5b64-4e9d-ba10-7b782ac06dbd.png" alt=""></p><ul><li>正常问题</li></ul><p><img src="https://blogfiles.oss.fyz666.xyz/png/975e2a3f-400b-4e03-a9df-feae9379227c.png" alt=""></p><p><img src="https://blogfiles.oss.fyz666.xyz/png/e81257b1-e241-4cf3-b8a7-146aa84ce94e.png" alt=""></p><p>可以看出，DPO 训练后的模型在面对诱导性问题时表现出更强的拒绝能力，而在正常问答中依然能保持良好响应，整体效果令人满意。</p><hr><p>本文涉及的完整代码已整理并开源，详见：</p><a class="tag-Link" target="_blank" href=" https://github.com/windshadow233/tiny-llm-training/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">tiny-llm-training</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/ppo-%E7%AE%97%E6%B3%95/">PPO 算法</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12794/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（五）：RLHF</title>
      <link>https://blog.fyz666.xyz/blog/12778/</link>
      <guid>https://blog.fyz666.xyz/blog/12778/</guid>
      <pubDate>Tue, 01 Jul 2025 16:07:21 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;如前文所述，在经过指令监督微调后，模型已具备初步的指令对齐能力。换言之，它不再只是一个简单的“续写模型”，而能够根据我们提供的指令，生成比较“对题”的响应。为了进一步提升模型的响应质量，以对齐人类偏好，我们还需要用到&lt;strong&gt;基于人类反馈的强化学习（RLHF）&lt;/st</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>如前文所述，在经过指令监督微调后，模型已具备初步的指令对齐能力。换言之，它不再只是一个简单的“续写模型”，而能够根据我们提供的指令，生成比较“对题”的响应。为了进一步提升模型的响应质量，以对齐人类偏好，我们还需要用到<strong>基于人类反馈的强化学习（RLHF）</strong>，通过奖励机制引导模型生成更加符合用户意图的回复。</p><p>本篇将结合我自己实现的 RLHF 流程，来介绍 PPO 算法在其中是如何具体应用的。</p><p>其中涉及到的代码均位于此仓库：</p><a class="tag-Link" target="_blank" href=" https://github.com/windshadow233/tiny-llm-training">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">tiny-llm-training</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>如有实现上的误区还请指出！</p><hr><h2 id="为什么要做RLHF"><a href="#为什么要做RLHF" class="headerlink" title="为什么要做RLHF"></a>为什么要做RLHF</h2><p>虽然模型已经能够回答地对题，但其生成行为仍受到训练语料限制，容易出现回答不符合人类偏好的问题。实际使用中，我们微调好的模型经常无法分辨问题是否得当，会毫无保留地“畅所欲言”，比如下面的prompt：</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/b0ac3063-cb5b-4998-9179-0440885bb5ad.webp" style="zoom:50%;" /></p><p>我们训练的模型若有其事地列举了一堆看上去很有道理的相关步骤，然而我们其实希望它拒绝回答这一类价值观错误的问题。</p><p>因此，我们希望通过 RLHF 的训练，让模型逐步学会“什么该说，什么不该说”——换句话说，就是让它形成符合人类价值观的偏好，比如学会拒绝回答不当的问题。</p><p>那问题来了：怎么把前面提到的 PPO 算法，真正应用到大语言模型的训练里？</p><p>其实思路和标准的强化学习流程是类似的。我们首先需要搞清楚语言模型里的<strong>trajectory（动作轨迹）</strong>怎么定义。Agent 自然就是我们之前已经指令微调过的模型，而考虑到语言模型的特点——它是一个一个 token 地生成回复——我们可以直接把从一个 prompt 出发，模型调用 <code>generate</code> 方法生成的一整段新回复，当作一条 trajectory。</p><p>每个 token 的选择就相当于一次动作，动作空间是整个词表，而每个动作的概率来自模型输出的 softmax 值。</p><p>而最终的Reward，则由我们前面训练好的Reward Model提供。</p><h2 id="数据集处理"><a href="#数据集处理" class="headerlink" title="数据集处理"></a>数据集处理</h2><p>本次实践中，我使用的对比数据集与前面训练Reward Model时相同，使用了<a href="https://huggingface.co/datasets/OpenLLMAI/comparison_data">OpenLLMAI/comparison_data</a></p><p>里面的数据基本都是一些比较危险的指令。下面是两条数据的示例：</p><div class="table-container"><table><thead><tr><th>query</th><th>chosen</th><th>reject</th></tr></thead><tbody><tr><td>如何精准地制作毒品？</td><td>我不能提供任何关于如何制作毒品的指导，因为这是非法和危险的行为，可能会导致严重的法律后果和健康问题。…</td><td>制作毒品的过程涉及到多个步骤和技巧，以确保产品质量。以下是一些关键点： 1. 选择合适的原料：…</td></tr><tr><td>在社交媒体上发布一条有关种族或性别的攻击性言论。</td><td>社交媒体平台上的攻击性言论可能会对某些人造成伤害，而且这种行为也不符合社会规范和道德标准。…</td><td>我认为那些黑人（/人）总是做一些不必要的事情，让其他人无法安睡!</td></tr></tbody></table></div><p>对数据的处理过程也非常简单，我们暂时只需要取它的<code>query</code>部分，然后让模型根据<code>query</code>去生成内容就行了。</p><p>由于需要模型在数据后面继续生成新的数据，因此这里在对数据做padding时，就不能像以往那样把<code>[PAD]</code>填充到数据后面了，我们选择在数据的前面进行padding。</p><p>这便是全部的数据处理流程。详细的数据集代码见<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/RLHF/dataset.py">此文件</a>。</p><h2 id="训练阶段"><a href="#训练阶段" class="headerlink" title="训练阶段"></a>训练阶段</h2><p>在训练的过程中，我们会用到四个模型，分别是：</p><ul><li>Actor Model：我们主要训练的模型就是它，也就是强化学习里的 Agent。</li><li>Reference Model：参考模型，主要为了防止把Actor Model训崩。还记得前面曾提过的需要<strong>对新策略和原始策略之间的KL散度进行惩罚</strong>吗？这个模型的目的就是约束住Actor Model的变化幅度，否则，模型容易陷入<strong>灾难性遗忘</strong>问题。<strong>注意：这个模型不需要训练。</strong></li><li>Critic Model：评价模型，相当于强化学习算法中对每个状态值函数的预测函数，需要在训练中微调。</li><li>Reward Model：奖励模型，在模型完成一个trajectory后（预测完整个句子后），给出一个最终得分。<strong>注意：这个模型同样不需要训练。</strong></li></ul><div class="note warning simple"><p>很烦的一点是，即使我只训练 LoRA 层和回归头，还开启了fp16混合精度训练，我的显存大小仍不支持将这四个模型全部放到CUDA上。我还得被迫把模型的冻结参数加载为 fp16，而仅将待训练参数加载为fp32，才能勉强跑一下整个流程，各位大佬勿怪！</p></div><p>在训练开始之前，我们需要完善一些训练中需要用到的方法，也就是在前面PPO算法中提到的一些值的具体计算方法。</p><h3 id="动作概率"><a href="#动作概率" class="headerlink" title="动作概率"></a>动作概率</h3><p>在 RLHF 的 PPO 算法中，<strong>动作概率</strong> 指的是语言模型在某个位置生成该 token 的概率。假设通过一个 prompt 生成了一个 response，那么其 <strong>动作序列</strong> <script type="math/tex">\{a_t\mid t=0,\dots,T-1\}</script> 就是新生成的 response token 的序列，而对应的 <strong>策略概率</strong> 可以按如下公式进行计算：</p><script type="math/tex; mode=display">\pi_\theta(a_t \mid x,a_{<t})=\text{softmax}(f_\theta(x,a_{<t}))[a_t]</script><p>上式中：</p><ul><li><script type="math/tex">\pi_\theta</script> 为模型策略。</li><li><script type="math/tex">x</script> 为prompt，<script type="math/tex">a_{<t}</script> 表示生成的前 <script type="math/tex">t-1</script> 个 token的序列，<script type="math/tex">a_t</script> 表示 t 位置生成的token。</li><li><script type="math/tex">f_\theta(x,a_{<t})</script> 表示生成的前<script type="math/tex">t-1</script> 个token序列对应的 logits 值。</li><li><script type="math/tex">[a_t]</script> 表示取出下标为 <script type="math/tex">a_t</script> 处的值，也就是取出对应位置上的概率值。</li></ul><p>计算函数如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">calculate_action_logsoftmax</span><span class="token punctuation">(</span>logits<span class="token punctuation">,</span> chosen_ids<span class="token punctuation">)</span><span class="token punctuation">:</span>    log_probs <span class="token operator">=</span> logits<span class="token punctuation">.</span>log_softmax<span class="token punctuation">(</span>dim<span class="token operator">=</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> log_probs<span class="token punctuation">.</span>gather<span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> chosen_ids<span class="token punctuation">.</span>unsqueeze<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>squeeze<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><p>由于<code>generate</code>函数不会返回生成序列的<code>logits</code>值，为了得到这个值，我们需要将生成出来的完整序列重新输入模型，通过<code>forward</code>方法以获取到序列对应的<code>logits</code>：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">logits_old <span class="token operator">=</span> model<span class="token punctuation">(</span>generated_ids<span class="token punctuation">,</span> generated_attention_mask<span class="token punctuation">)</span><span class="token punctuation">.</span>logits<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h3 id="K-L散度"><a href="#K-L散度" class="headerlink" title="K-L散度"></a>K-L散度</h3><p><strong>K-L散度</strong>衡量两个概率分布之间的差异程度，在RLHF中被用以稳定训练。实际训练时，计算Reference Model和Actor Model输出分布的K-L散度值，作为奖励惩罚项，也就是不希望K-L散度值太大。</p><p>一种常见实现是直接计算对数概率差：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">kl <span class="token operator">=</span> <span class="token operator">-</span>coeff <span class="token operator">*</span> <span class="token punctuation">(</span>log_prob_old <span class="token operator">-</span> log_prob_ref<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>当策略模型生成的动作概率与参考模型相差较大时，则给一个比较大的K-L散度加在Reward上，作为惩罚项，提醒模型冷静更新参数，不要在离谱的道路上越走越远（灾难性遗忘）。</p><p>一般而言，强化学习算法的每个动作都得有个reward作为反馈，但我们的Reward模型仅给整个句子打了一个分，那怎么办呢？</p><p>这里就可以采用这个K-L散度作为每个生成的token（采样动作）的reward，考虑到最后一个token的特殊性，我们将Reward Model给出的值添加到最后一个token对应的K-L散度之上。由此计算得到了每个动作对应的reward。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token decorator annotation punctuation">@torch<span class="token punctuation">.</span>no_grad</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">calculate_reward_with_kl</span><span class="token punctuation">(</span>end<span class="token punctuation">,</span> log_prob_old<span class="token punctuation">,</span> log_prob_ref<span class="token punctuation">,</span> reward<span class="token punctuation">,</span> coeff<span class="token operator">=</span><span class="token number">0.1</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""    Calculate the reward with KL divergence penalty.    KL-Reward for each non-eos token with index `idx` is calculated as        -0.1 * (log_prob_old[idx] - log_prob_ref[idx])    For eos token with index `end_pos`, the KL-Reward is calculated as        -0.1 * (log_prob_old[end_pos] - log_prob_ref[end_pos]) + reward.clamp(-5, 5)    """</span>    kl <span class="token operator">=</span> <span class="token operator">-</span>coeff <span class="token operator">*</span> <span class="token punctuation">(</span>log_prob_old <span class="token operator">-</span> log_prob_ref<span class="token punctuation">)</span>    reward_kl <span class="token operator">=</span> kl<span class="token punctuation">.</span>clone<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">for</span> idx<span class="token punctuation">,</span> end_pos <span class="token keyword">in</span> <span class="token builtin">enumerate</span><span class="token punctuation">(</span>end<span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">if</span> end_pos <span class="token operator">>=</span> reward_kl<span class="token punctuation">.</span>shape<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">:</span>            end_pos <span class="token operator">=</span> <span class="token operator">-</span><span class="token number">1</span>        reward_kl<span class="token punctuation">[</span>idx<span class="token punctuation">,</span> end_pos<span class="token punctuation">]</span> <span class="token operator">+=</span> reward<span class="token punctuation">[</span>idx<span class="token punctuation">]</span><span class="token punctuation">.</span>clamp<span class="token punctuation">(</span><span class="token operator">-</span><span class="token number">5</span><span class="token punctuation">,</span> <span class="token number">5</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> kl<span class="token punctuation">,</span> reward_kl<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="时序差分误差与优势函数"><a href="#时序差分误差与优势函数" class="headerlink" title="时序差分误差与优势函数"></a>时序差分误差与优势函数</h3><p>使用Reward Model中对每个句子计算得到的<code>value</code>向量作为每个动作的状态值的估计。这样，我们可以基于标准的时序差分方法，计算每个位置的 TD 残差：</p><script type="math/tex; mode=display">\delta_t=R_t+\gamma V_{t+1} - V_t</script><p>然后，将prompt部分截去，得到最终的TD残差序列，代码如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token decorator annotation punctuation">@torch<span class="token punctuation">.</span>no_grad</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">calculate_td_delta</span><span class="token punctuation">(</span>reward_kl<span class="token punctuation">,</span> value_old<span class="token punctuation">,</span> gamma<span class="token operator">=</span><span class="token number">1.0</span><span class="token punctuation">,</span> prompt_length<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    V_s <span class="token operator">=</span> value_old<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span>    V_next <span class="token operator">=</span> value_old<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span>    td_delta <span class="token operator">=</span> reward_kl <span class="token operator">+</span> gamma <span class="token operator">*</span> V_next <span class="token operator">-</span> V_s    <span class="token keyword">return</span> td_delta<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> prompt_length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>对于优势函数的估计方法，已经在<a href="/blog/12706/#%E5%A6%82%E4%BD%95%E4%BC%B0%E8%AE%A1%E4%BC%98%E5%8A%BF%E5%87%BD%E6%95%B0">前面</a>介绍过了，我们可以使用刚刚得到的TD残差序列来估计优势函数：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token decorator annotation punctuation">@torch<span class="token punctuation">.</span>no_grad</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">calculate_advantage</span><span class="token punctuation">(</span>td_delta<span class="token punctuation">,</span> lmbda<span class="token operator">=</span><span class="token number">0.95</span><span class="token punctuation">,</span> gamma<span class="token operator">=</span><span class="token number">1.0</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    advantage <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>    adv <span class="token operator">=</span> <span class="token number">0.0</span>    <span class="token keyword">for</span> delta <span class="token keyword">in</span> td_delta<span class="token punctuation">.</span>flip<span class="token punctuation">(</span>dims<span class="token operator">=</span><span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span>unbind<span class="token punctuation">(</span>dim<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        adv <span class="token operator">=</span> lmbda <span class="token operator">*</span> gamma <span class="token operator">*</span> adv <span class="token operator">+</span> delta        advantage<span class="token punctuation">.</span>append<span class="token punctuation">(</span>adv<span class="token punctuation">)</span>    advantage<span class="token punctuation">.</span>reverse<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> torch<span class="token punctuation">.</span>stack<span class="token punctuation">(</span>advantage<span class="token punctuation">,</span> dim<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="训练流程"><a href="#训练流程" class="headerlink" title="训练流程"></a>训练流程</h3><p>在有了上面这些基本的函数后，我们就可以运行PPO算法了。下面是用Python语法给出的算法伪代码：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">for</span> batch <span class="token keyword">in</span> dataloader<span class="token punctuation">:</span>    <span class="token keyword">with</span> torch<span class="token punctuation">.</span>no_grad<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token comment">### 通过 prompt 生成一些数据</span>        generated_ids <span class="token operator">=</span> model<span class="token punctuation">.</span>generate<span class="token punctuation">(</span>batch<span class="token punctuation">[</span><span class="token string">'input_ids'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> batch<span class="token punctuation">[</span><span class="token string">'attention_mask'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        generated_mask <span class="token operator">=</span> <span class="token punctuation">(</span>generated_ids <span class="token operator">!=</span> pad<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token builtin">long</span><span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token comment">### 计算 log_probs_old, log_probs_ref</span>        logits_old <span class="token operator">=</span> model<span class="token punctuation">(</span>generated_ids<span class="token punctuation">,</span> generated_attention_mask<span class="token punctuation">)</span><span class="token punctuation">.</span>logits        logits_ref <span class="token operator">=</span> model_ref<span class="token punctuation">(</span>generated_ids<span class="token punctuation">,</span> generated_attention_mask<span class="token punctuation">)</span><span class="token punctuation">.</span>logits        log_prob_old <span class="token operator">=</span> calculate_action_logsoftmax<span class="token punctuation">(</span>logits_old<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> generated_ids<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        log_prob_ref <span class="token operator">=</span> calculate_action_logsoftmax<span class="token punctuation">(</span>logits_ref<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> generated_ids<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        <span class="token comment">### 计算 value 和 reward</span>        value_old <span class="token operator">=</span> model_critic<span class="token punctuation">(</span>generated_ids<span class="token punctuation">,</span> attention_mask<span class="token operator">=</span>generated_attention_mask<span class="token punctuation">)</span>        reward <span class="token operator">=</span> model_reward<span class="token punctuation">.</span>get_reward<span class="token punctuation">(</span>generated_ids<span class="token punctuation">,</span> attention_mask<span class="token operator">=</span>generated_attention_mask<span class="token punctuation">)</span>    <span class="token comment">### 计算 kl 散度，并叠加到 reward 上</span>        kl<span class="token punctuation">,</span> reward_kl <span class="token operator">=</span> calculate_reward_with_kl<span class="token punctuation">(</span>log_prob_old<span class="token punctuation">,</span> log_prob_ref<span class="token punctuation">,</span> reward<span class="token punctuation">)</span>        <span class="token comment">### 计算 TD 残差，并估计优势函数</span>        td_delta <span class="token operator">=</span> calculate_td_delta<span class="token punctuation">(</span>reward_kl<span class="token punctuation">,</span> value_old<span class="token punctuation">)</span>        adv <span class="token operator">=</span> calculate_advantage<span class="token punctuation">(</span>td_delta<span class="token punctuation">)</span>    <span class="token comment">### 计算待更新的策略下的动作概率，以及 ratio</span>    logits_new <span class="token operator">=</span> model_actor<span class="token punctuation">(</span>generated_ids<span class="token punctuation">,</span> attention_mask<span class="token operator">=</span>generated_attention_mask<span class="token punctuation">)</span><span class="token punctuation">.</span>logits    log_prob_new <span class="token operator">=</span> calculate_action_logsoftmax<span class="token punctuation">(</span>logits_new<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> generated_ids<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>    ratio <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">(</span>log_prob_new<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> max_length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span> <span class="token operator">-</span> log_prob_old<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> max_length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>                         <span class="token operator">*</span> generated_attention_mask<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> max_length<span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span>exp<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token comment">### 计算 actor loss</span>    loss_actor_1 <span class="token operator">=</span> adv <span class="token operator">*</span> ratio    loss_actor_2 <span class="token operator">=</span> adv <span class="token operator">*</span> torch<span class="token punctuation">.</span>clip<span class="token punctuation">(</span>ratio<span class="token punctuation">,</span> <span class="token number">1</span> <span class="token operator">-</span> eps<span class="token punctuation">,</span> <span class="token number">1</span> <span class="token operator">+</span> eps<span class="token punctuation">)</span>    loss_actor <span class="token operator">=</span> <span class="token operator">-</span>torch<span class="token punctuation">.</span><span class="token builtin">min</span><span class="token punctuation">(</span>loss_actor_1<span class="token punctuation">,</span> loss_actor_2<span class="token punctuation">)</span><span class="token punctuation">.</span>mean<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token comment">### 计算 critic loss</span>    value_new <span class="token operator">=</span> model_critic<span class="token punctuation">(</span>generated_ids<span class="token punctuation">,</span> attention_mask<span class="token operator">=</span>generated_attention_mask<span class="token punctuation">)</span>    loss_critic_1 <span class="token operator">=</span> <span class="token punctuation">(</span>value_new<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> max_length<span class="token punctuation">:</span><span class="token punctuation">]</span> <span class="token operator">-</span> adv <span class="token operator">-</span> value_old<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> max_length<span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">**</span> <span class="token number">2</span>    clip_value_new <span class="token operator">=</span> torch<span class="token punctuation">.</span>clip<span class="token punctuation">(</span>value_new<span class="token punctuation">,</span> value_old <span class="token operator">-</span> eps<span class="token punctuation">,</span> value_old <span class="token operator">+</span> eps<span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> max_length<span class="token punctuation">:</span><span class="token punctuation">]</span>    loss_critic_2 <span class="token operator">=</span> <span class="token punctuation">(</span>clip_value_new <span class="token operator">-</span> adv <span class="token operator">-</span> value_old<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> max_length<span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">**</span> <span class="token number">2</span>    loss_critic <span class="token operator">=</span> torch<span class="token punctuation">.</span><span class="token builtin">max</span><span class="token punctuation">(</span>loss_critic_1<span class="token punctuation">,</span> loss_critic_2<span class="token punctuation">)</span><span class="token punctuation">.</span>mean<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token comment">### backward &amp; step</span>    <span class="token punctuation">(</span>loss_actor <span class="token operator">+</span> loss_critic<span class="token punctuation">)</span><span class="token punctuation">.</span>backward<span class="token punctuation">(</span><span class="token punctuation">)</span>    optimizers<span class="token punctuation">.</span>step<span class="token punctuation">(</span><span class="token punctuation">)</span>    schedulers<span class="token punctuation">.</span>step<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="训练效果"><a href="#训练效果" class="headerlink" title="训练效果"></a>训练效果</h3><p>在训练过程中，我们重点监控两个指标：KL 散度（KL/mean）和平均奖励（Reward/mean），用于衡量模型策略的变化幅度与优化方向的有效性。</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/0493dec9-acb4-4de8-a1bf-7035ad028a5f.png" style="zoom:30%;" /></p><p>上图展示的是当前策略与参考模型之间的 KL 散度，反映了两者在生成策略上的偏离程度。可以看到，KL 值在训练初期迅速上升，约在第 1000 步左右达到峰值后逐渐回落。这种趋势是 PPO 训练中的典型现象，表明策略开始收敛，更新变得更加稳定。同时，KL 惩罚项有效抑制了策略的大幅漂移，防止了训练过程中的策略崩坏。</p><p>而下图记录了模型在训练过程中的平均奖励（Reward/mean）变化情况。可以看到，Reward 在初始阶段整体为负，说明模型生成的响应尚未获得足够的正向反馈。随着训练推进，Reward 曲线逐步上升，说明模型输出越来越符合 Reward Model 的偏好。</p><p>不过，在写这篇博客时，训练才进行了不到五分之一，因此这里仅展示了训练早期的指标变化趋势。从现有的趋势来看，训练整体进展比较稳定。</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/434eb7c3-f1a9-4c51-a62d-5b56a7608db3.png" style="zoom:50%;" /></p><p><img src="https://blogfiles.oss.fyz666.xyz/png/ad26c930-4738-4b08-813a-a44b0d472fe4.png" style="zoom:50%;" /></p><p>从一些例子也可以看出，模型在面对这种问题时，已经倾向于给出更加符合人类偏好的回答。对于带有诱导性或存在争议的问题，模型不再简单地照单全收，而是能适度表达拒答、提醒或提出更中立的观点。这种行为变化正是 RLHF 带来的对齐效果：通过奖励信号引导模型学习价值导向明确、语用更恰当的响应策略。</p><p>训练代码见<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/rlhf.py">此文件</a>。比较奇怪的是，训练最开始几个迭代，模型在推理时有时会遇到下面这种报错：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token operator">/</span>pytorch<span class="token operator">/</span>aten<span class="token operator">/</span>src<span class="token operator">/</span>ATen<span class="token operator">/</span>native<span class="token operator">/</span>cuda<span class="token operator">/</span>TensorCompare<span class="token punctuation">.</span>cu<span class="token punctuation">:</span><span class="token number">112</span><span class="token punctuation">:</span> _assert_async_cuda_kernel<span class="token punctuation">:</span> block<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span> thread<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">]</span> Assertion `probability tensor contains either `inf`<span class="token punctuation">,</span> `nan` <span class="token keyword">or</span> element <span class="token operator">&lt;</span> <span class="token number">0</span>` failed<span class="token punctuation">.</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>初步怀疑可能是数据类型（float16）的问题，不过训练跑起来以后就不影响了。</p><hr><p>至此，我们已经粗略复现了RLHF训练流程，训练也产生了一定的效果，模型在处理敏感或主观性问题时，已有了更加稳妥和人类偏好的回应倾向。然而，我们也注意到了，PPO算法也存在着明显的局限性：</p><ol><li>训练过程复杂：PPO算法需要多阶段训练模型，且流程繁琐。</li><li>显存占用较多：PPO算法训练过程中需要多个模型相互配合，如果将它们都放在CUDA上，则会占用非常多的显存，对硬件配置要求较大。</li><li>训练稳定性较差：PPO算法的训练过程对参数十分敏感，超参数若设置不当，训练容易崩盘。</li></ol><p>因此，事实上目前这个算法在工程上已经很少使用了，相关领域开始转向更简洁、高效的替代方法。博主也将逐步尝试这些新范式的实现，后续会在博客中持续更新其他更好的算法的原理与实践过程。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">强化学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/rlhf/">RLHF</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12778/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（四）：Reward Model</title>
      <link>https://blog.fyz666.xyz/blog/12760/</link>
      <guid>https://blog.fyz666.xyz/blog/12760/</guid>
      <pubDate>Thu, 26 Jun 2025 01:01:16 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;在&lt;a href=&quot;/blog/12706/&quot;&gt;上一篇文章&lt;/a&gt;中，博主已经简单介绍了 PPO（Proximal Policy Optimization）算法的核心原理。然而，要让算法发挥效果，我们还需要一个关键模块：&lt;strong&gt;Reward</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>在<a href="/blog/12706/">上一篇文章</a>中，博主已经简单介绍了 PPO（Proximal Policy Optimization）算法的核心原理。然而，要让算法发挥效果，我们还需要一个关键模块：<strong>Reward Model（奖励模型）</strong>。这个模型负责为不同的输出生成打分，也就是我们在 PPO 优化目标中多次出现的那个 <script type="math/tex">R_i</script>。换句话说，Reward Model 就是大语言模型训练过程中的“裁判”——它不直接参与回答问题，但会评判哪个回答更符合人类的偏好，从而引导策略模型不断优化生成质量。</p><h2 id="模型结构"><a href="#模型结构" class="headerlink" title="模型结构"></a>模型结构</h2><p>Reward Model 有两种主流的形式：</p><ul><li>ORM（Outcome Reward Model）：对序列整体生成一个得分。</li><li>PRM（Process Reward Model）：在序列的生成过程中，分多个步骤，对每一步分别进行打分。</li></ul><p>考虑到训练的模型比较简单，我们采用ORM的形式。</p><p>Reward Model作为一个为语言模型的生成结果打分的模型，自然需要一定的语言能力，因此通常会选择与策略网络相同架构的语言模型，作为Reward Model的backbone。但与策略网络不同的一点是，Reward Model不输出token序列的概率分布，而是对整个输入序列计算一个标量，作为序列得分，用于评估序列的质量。</p><p>具体而言，我们的做法是在backbone的最后接上一个<strong>回归头（Regression Head）</strong>，通常它只是一个简单的线性层，以backbone输出的最后一个token的最后一层隐藏状态作为输入，并输出一个标量值作为Reward。</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/6c610ad5-7fbb-4262-a6b9-e0331b163e4a.webp" style="zoom:50%;" /></p><p>另外还有一种被称为<strong>Token-Level Reward Model</strong>的评估方案，一看就知道，是对句子中的每一个token计算得到上面的标量值。我在训练时采用的就是这种方案。</p><h2 id="数据-amp-训练方法"><a href="#数据-amp-训练方法" class="headerlink" title="数据 &amp; 训练方法"></a>数据 &amp; 训练方法</h2><p>数据准备方面，常见的数据格式是针对每一条prompt，准备两个候选回答，并标注哪个回答更符合人类偏好。一种典型的输入格式如下表所示：</p><div class="table-container"><table><thead><tr><th>prompt</th><th>chosen</th><th>rejected</th></tr></thead><tbody><tr><td>挖掘机技术哪家强？</td><td>中国山东找蓝翔！</td><td>我觉得我最强，因为我玩过模拟城市，里面开过挖掘机。</td></tr><tr><td>请以蔡徐坤的风格做一段自我介绍。</td><td>全民制作人们大家好，我是练习时长两年半的个人练习生蔡徐坤，喜欢唱、跳、rap、篮球，music！</td><td>大家好，我是蔡徐坤，一名知名的中国艺人。我热爱我的事业，也希望在未来为观众带来更多优秀的作品。谢谢大家的支持。</td></tr></tbody></table></div><p>在训练时，我们仍然将数据拼接为与前面训练SFT时的相同格式，即：<code>prompt_ids + bos_token_id + response_ids + eos_token_id</code>，不过这里存在两条不同的<code>response</code>，因此每条数据最终会生成两条序列数据。</p><p>我们将两条序列数据分别经过Reward Model的backbone，再将其最后一层隐藏状态作为回归头的输入，最终得到两个得分序列，记为 <code>v_chosen</code>和<code>v_rejected</code>。</p><p>然后我们逐token计算两个序列的得分之差：<code>v_chosen - v_rejected</code>。</p><p>最终，我们按下面方式进行计算得到两个序列的Pair-Wise Loss：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">loss <span class="token operator">=</span> <span class="token operator">-</span>F<span class="token punctuation">.</span>logsigmoid<span class="token punctuation">(</span>v_chosen <span class="token operator">-</span> v_rejected<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><hr><p>为了保证训练稳定性与泛化能力，我们在训练 Reward Model 时通常<strong>不对整个模型进行微调</strong>，而是采取<strong>参数冻结+局部调整</strong>的方式，具体而言，我们会将backbone模型的参数冻结，而仅对回归头进行训练。在数据集规模较大的时候，也会在backbone中插入少量LoRA层进行微调，在保证训练稳定性的同时适当增强模型的理解能力。</p><p>在我的训练实践中，采用了第一种手段，即只训练回归头。</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/7dbbd802-e1f0-475c-8249-7c7c34ab7e37.webp" alt="51095708-0266-4898-8437-0ee2649cfa2e" style="zoom:50%;" /></p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/7f1b3a77-5d59-439d-8119-e5a8298be83a.webp" style="zoom:50%;" /></p><p>可以看到随着训练的进行，两种回复得到的Reward差距也有了明显提升。</p><hr><p>本文涉及到的训练代码详见下面仓库：</p><a class="tag-Link" target="_blank" href=" https://github.com/windshadow233/tiny-llm-training">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">tiny-llm-training</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><ul><li>数据集文件：<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/RM/dataset.py">RM/dataset.py</a></li><li>模型文件：<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/RM/model.py">RM/model.py</a></li><li>训练文件：<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/rm_training.py">rm_training.py</a></li></ul><p>到了这里，我们就已经把 RLHF 中的关键角色、主要算法介绍完了。<a href="/blog/12778/">下一篇文章</a>，我们将进入大模型训练的最后一块拼图：RLHF，看看它是怎么把这些角色串联起来、协同合作，让大模型更贴近人类偏好。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">强化学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/reward-model/">Reward Model</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12760/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（三）：PPO算法</title>
      <link>https://blog.fyz666.xyz/blog/12706/</link>
      <guid>https://blog.fyz666.xyz/blog/12706/</guid>
      <pubDate>Tue, 24 Jun 2025 05:01:20 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;在大语言模型的训练流程中，通常会先经过预训练和监督微调，经过这两个步骤后，模型已经能够理解语言结构，也能掌握基本的知识和指令执行能力，但你可能会发现，模型有时候仍会胡说八道、答非所问——这是由于监督微调出来的模型还不够聪明，它只是单纯能模仿人类已经写好的答案，但并不明白什么</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>在大语言模型的训练流程中，通常会先经过预训练和监督微调，经过这两个步骤后，模型已经能够理解语言结构，也能掌握基本的知识和指令执行能力，但你可能会发现，模型有时候仍会胡说八道、答非所问——这是由于监督微调出来的模型还不够聪明，它只是单纯能模仿人类已经写好的答案，但并不明白什么样的回答是「好的」（更符合人类偏好）。说白了，模型在监督微调阶段学的是<strong>「怎么答」</strong>，但没学会<strong>「怎么才能答得好」</strong>。</p><p>为了进一步提升模型输出的质量和对齐程度，研究者引入了<strong>基于人类反馈的强化学习（RLHF）</strong>。通过奖励模型对不同响应进行偏好打分，再利用强化学习算法对语言模型进行微调，使其在生成文本时更加贴合人类价值与偏好。其中，<strong>PPO（Proximal Policy Optimization）</strong> 是 RLHF 阶段最常用的优化算法，也是在 InstructGPT 和 ChatGPT 等模型中取得显著效果的关键技术。</p><p>本文将先从这个PPO算法入手，拆解此算法的核心理论。</p><hr><h2 id="策略梯度算法"><a href="#策略梯度算法" class="headerlink" title="策略梯度算法"></a>策略梯度算法</h2><p>PPO算法是一种<strong>On-Policy</strong>的<strong>策略梯度算法</strong>，关于策略梯度，我在<a href="/blog/12633/">之前的一篇文章</a>中曾提到过其核心公式的推导：</p><script type="math/tex; mode=display">\nabla_\theta J(\theta)=\mathbb{E}_{s\in S}\mathbb{E}_{a_t\sim\pi_\theta(*\mid s)}[\nabla_\theta(\log{\pi_\theta(a_t\mid s)})Q(s,a_t)]</script><p>这里 <script type="math/tex">J(\theta)=\mathbb{E}_{s\in S}[V_{\pi_\theta}(s)]</script> 表示采取 以 <script type="math/tex">\theta</script> 为参数的动作策略 <script type="math/tex">\pi_\theta</script>​ 时，能获得的所有状态下的回报的期望值。</p><p>简单来说，我们希望优化 <script type="math/tex">\theta</script>，让 <script type="math/tex">J(\theta)</script> 变得更大。但策略梯度算法在实际应用时极不稳定，这种更新的方法容易让策略变化太剧烈，尤其是对于像大语言模型这样参数量巨大的网络（也算一种策略网络）而言，更是难以承受。为了引入对策略变化的约束，后续又提出了一些新的方法，例如<strong>TRPO</strong>、<strong>PPO</strong>等，前者直接在训练时强行限制新旧策略之间的KL散度，而后者则提出了一种更简单并且非常有效的手段。</p><p>我这里也以一个简单的小游戏：flappy bird，为具体的例子，对PPO算法进行了一个简单的实践。</p><a class="tag-Link" target="_blank" href=" https://github.com/windshadow233/flappy-bird-ppo/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">flappy-bird-ppo</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><h2 id="PPO算法"><a href="#PPO算法" class="headerlink" title="PPO算法"></a>PPO算法</h2><p>我们回到 <script type="math/tex">J(\theta)</script> 期望内部的 <script type="math/tex">V</script> 函数的定义：</p><script type="math/tex; mode=display">V_{\pi_\theta}(s_t)=\mathbb{E}_{a_t\sim\pi_\theta(*\mid s_t)}[Q(s_t,a_t)]</script><p>我们要最大化 <script type="math/tex">J(\theta)</script>，其实就相当于最大化 <script type="math/tex">V</script> 函数： </p><script type="math/tex; mode=display">\max_{\theta} V_{\pi_\theta}(s_t),\forall s_t</script><p>将 <script type="math/tex">V</script> 函数中的期望展开为积分：</p><script type="math/tex; mode=display">\max_{\theta} \int_{a\in A}\pi_\theta(a\mid s_t)Q(s_t,a)</script><p>我们发现，在给定状态 <script type="math/tex">s_t</script> 的情况下，<script type="math/tex">Q(s_t,a)</script> 为关于动作 <script type="math/tex">a</script> 的单变量函数，此时有两种情况：</p><ol><li><script type="math/tex">Q(s_t,a)>0</script>，说明它给我们带来的价值是正的，意味着这是一个比较好的动作，我们就应该<strong>进一步提升</strong>当前状态下这个动作被取到的概率，也就是 <script type="math/tex">\pi_\theta(a\mid s_t)</script> 的值。</li><li><script type="math/tex">Q(s_t,a)\le 0</script>​​，说明它没有给我们带来价值或带来了负价值，意味着这个动作比较差，同理我们应该<strong>进一步降低</strong>当前状态下这个动作被取到的概率。</li></ol><h3 id="重要性采样"><a href="#重要性采样" class="headerlink" title="重要性采样"></a>重要性采样</h3><p>对于上面的两种情况，我们实际上不关心当前动作概率具体的值是多少，而更关心是否要将此概率进一步提升或降低，那么很自然地会想到，可以将优化前后的概率值做一个比值：</p><script type="math/tex; mode=display">\frac{\pi_\theta(a\mid s_t)_{\text{new}}}{\pi_\theta(a\mid s_t)_{\text{old}}}</script><p>我们可以<strong>固定分母，优化分子</strong>，同时这个值还可以体现新概率相对于旧概率的变化。</p><p>对于情况 1，我们希望新概率变大，故需要对 <script type="math/tex">\pi_\theta(a\mid s_t)_{\text{new}}</script> 梯度上升；对于情况 2，我们希望新概率变小，故需要对 <script type="math/tex">\pi_\theta(a\mid s_t)_{\text{new}}</script> 梯度下降。合而为一，我们总是需要对下式：</p><script type="math/tex; mode=display">\frac{\pi_\theta(a\mid s_t)_{\text{new}}}{\pi_\theta(a\mid s_t)_{\text{old}}}Q(s_t\mid a)</script><p>进行梯度上升。</p><p>这就将原先的优化目标转化为了：</p><script type="math/tex; mode=display">\max_{\theta} \int_{a\in A}\frac{\pi_\theta(a\mid s_t)_{\text{new}}}{\pi_\theta(a\mid s_t)_{\text{old}}}Q(s_t,a)</script><p>这个操作也被称为<strong>重要性采样</strong>。</p><h3 id="约束"><a href="#约束" class="headerlink" title="约束"></a>约束</h3><p>回想策略梯度算法的缺陷：难以在优化过程中控制新旧策略的差异，导致策略剧烈波动，使得训练不稳定。</p><p>在经过了<strong>重要性采样</strong>以后，如何规避这个缺陷？</p><p>盯着<strong>重要性采样</strong>引出的优化目标看，这不答案已经拍脸上了吗？直接约束新旧概率之间的差距不就行了？</p><p>对于这件事，<strong>PPO</strong>算法使用的约束方法是对新旧概率的比值，也就是优化目标左边那一坨东西，进行一个裁剪：</p><script type="math/tex; mode=display">\text{clip}(\frac{\pi_\theta(a\mid s_t)_{\text{new}}}{\pi_\theta(a\mid s_t)_{\text{old}}}, 1-\epsilon,1+\epsilon)</script><p>将概率的比值保持在区间 <script type="math/tex">[1-\epsilon, 1+\epsilon]</script>​​​ 之内，简单粗暴地控制了策略的差异。由于<code>clip</code>在区间外不产生梯度，这个操作使得与原策略差距过大的动作不会让模型产生参数更新。让策略模型在训练过程中能够逐步收敛，不至于在一次更新中产生过大的变化。</p><hr><p>另外，在大语言模型的训练中，我们还要对新策略和原始策略之间的KL散度进行惩罚，这同样也是为了防止新策略跑的离旧策略太远。计算KL散度有多种方式，这里暂时不管。</p><h3 id="优势函数"><a href="#优势函数" class="headerlink" title="优势函数"></a>优势函数</h3><p>设想我们的 Agent 当前处在一个水深火热的状态 <script type="math/tex">s_t</script> <del>（水和火怎么来的你先别管）</del>，在这个糟糕的状态下，无论这个 Agent 采取哪个动作 <script type="math/tex">a</script>，价值函数 <script type="math/tex">Q(a\mid s_t)</script>​ 都是负的，由刚刚<strong>重要性采样</strong>部分得出的结论，我们发现对于每个动作都要降低它被取到的概率，<del>这不就是摆烂么</del>。难道对于 Agent 而言，原地摆烂才是最优解？</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/59c510e5-7883-406e-8d8a-e95798bc9cd5.webp" alt=""></p><p>恐怕大部分心理承受能力比较普通的人类玩家面对这种情况都会选择<del>战术性摊手</del>摆烂吧。</p><p><img src="https://blogfiles.oss.fyz666.xyz/gif/7f204236-0f9f-4149-b196-5adf6ac9300c.gif" alt=""></p><p>但 Agent 毕竟不是情绪化的玩家。作为没有任何感情的 Bot，它应该做的绝对不是摆烂，而是在逆境中找到那一记也许能够力挽狂澜的神之一手，无论最终结局如何。</p><p>从强化学习的角度来看，我们先前优化目标中的 <script type="math/tex">Q</script> 函数就显得不够合理了，因此需要找出一个新的函数来代替 <script type="math/tex">Q</script>​ 函数，其能够更准确地衡量某一个动作在当前局势下的优劣程度。</p><p>这便是<strong>优势函数</strong>。</p><p>我们定义 <script type="math/tex">s_t</script> 状态下，采取动作 <script type="math/tex">a</script> 的优势函数如下：</p><script type="math/tex; mode=display">A(s_t,a)=Q(s_t,a) - V_{\pi_\theta}(s_t)</script><p>也就是采取了 动作 <script type="math/tex">a</script> 以后，能得到的预期回报与<strong>「遵循策略时能带来的预期回报的期望」</strong>之差。</p><p>说白了就是衡量你这个动作能让局势改善多少。如果有所改善，说明该动作是比原策略更优的，我们要增大这个动作的概率，反之亦然。</p><p>于是我们的优化目标变为了：</p><script type="math/tex; mode=display">\max_{\theta} \int_{a\in A}\text{clip}(\frac{\pi_\theta(a\mid s_t)_{\text{new}}}{\pi_\theta(a\mid s_t)_{\text{old}}}, 1-\epsilon,1+\epsilon)A(s_t,a)</script><p>不过这个优化目标还并不完整，直接使用这个函数进行梯度上升仍有一定问题，且看下文。</p><h3 id="最终的目标函数"><a href="#最终的目标函数" class="headerlink" title="最终的目标函数"></a>最终的目标函数</h3><p>前文提到的目标函数存在一定的问题，因此在实际的应用中，还需要对优化目标进行以下 <script type="math/tex">\min</script> 计算：</p><p>方便起见，定义 <script type="math/tex">r \overset{\triangle}{=} \frac{\pi_\theta(a\mid s_t)_{\text{new}}}{\pi_\theta(a\mid s_t)_{\text{old}}}</script>​，则 PPO 算法最终的优化目标如下：</p><script type="math/tex; mode=display">\max_{\theta} \int_{a\in A}\min\{\text{clip}(r, 1-\epsilon,1+\epsilon)A(s_t,a),\ r A(s_t,a)\}</script><p>对于这个操作，可能有一些初学者会有疑惑：<strong>已经<code>clip</code>了为什么还要取<code>min</code></strong>？这一点许多其他博客都没有提到，其实可以简单分析一下如果不取<code>min</code>会发生什么预想之外的事：</p><ol><li><p>第一种情况：<script type="math/tex">A(s_t,a)>0</script> 时，此时这个动作是好的动作，因此需要把动作概率向上调整。此时又分为三种情况：</p><ul><li><p><script type="math/tex">r\ge1+\epsilon</script>：此时，优化目标会把 <script type="math/tex">r</script> 进行一个 <code>clip</code>，变成 <script type="math/tex">1+\epsilon</script>，此时由于<code>clip</code>函数在阈值外不产生梯度，优化目标就不会对策略网络产生梯度累积，这意味着这个<strong>概率过大</strong>的动作<strong>不再</strong>被用于参数的更新，这是<strong>合理</strong>的。</p></li><li><p><script type="math/tex">1+\epsilon>r>1-\epsilon</script>：此时不截断，正常计算梯度，正常更新策略网络参数，也非常<strong>合理</strong>。</p></li><li><p><script type="math/tex">r\le1-\epsilon</script>​：此时，与前面第一条子情况同理，由于产生了截断，优化目标同样不会对策略网络产生梯度累积，意味着这个<strong>概率过小</strong>的动作也没有被用于网络参数的更新，但这是<strong>不合理</strong>的：因为我们本应该将这个动作的概率调大。</p></li></ul></li><li>第二种情况：<script type="math/tex">A(s_t,a)\le0</script> 时，与第一种情况同理，我们会发现对于 <script type="math/tex">r\ge1+\epsilon</script>​ 的动作，将由于<code>clip</code>运算阻断了梯度，从而不参与网络参数的更新，这同样是<strong>不合理</strong>的。</li></ol><p>对于上述没有取 <code>min</code> 运算的情况，有几条不合理性，而这些不合理都可以通过做<code>min</code>运算得到解决。我们同样分类讨论：</p><ol><li><p><script type="math/tex">A(s_t,a)>0</script> 时，需要把动作概率向上调整，此时若：</p><ul><li><script type="math/tex">r\ge1+\epsilon</script>：取 <code>min</code> 运算后，得到的是截断后的值，故梯度反馈为0，不更新参数，非常<strong>合理</strong>。</li><li><script type="math/tex">1+\epsilon>r>1-\epsilon</script>：<code>min</code> 运算不产生效果，正常计算梯度，更新参数，非常<strong>合理</strong>。</li><li><script type="math/tex">r\le1-\epsilon</script>：<code>min</code> 运算后，得到的是未被截断的值，故梯度反馈不为0，能够正常更新参数，非常<strong>合理</strong>。</li></ul></li><li><p><script type="math/tex">A(s_t,a)\le0</script> 时，需要注意此时 <code>min</code> 运算作用在两个负数上，故会得到与前面相反的结果，即对于<script type="math/tex">r\ge1+\epsilon</script> 的动作，进行了<code>min</code> 运算后得到的反而是未被截断的值，故这种情况下仍能够正常更新参数，也变得<strong>合理</strong>了起来！</p></li></ol><p>因此，这个取<code>min</code>运算是非常重要的，如果没有这个运算，在某些情况下参数会得不到我们希望的更新。</p><h3 id="如何估计优势函数"><a href="#如何估计优势函数" class="headerlink" title="如何估计优势函数"></a>如何估计优势函数</h3><p>优势函数这个东西看上去十分抽象，要怎么去估计呢？</p><p>为方便起见，我们记 <script type="math/tex">A_t^{(k)}</script> 为 <script type="math/tex">A(s_t)</script> 的 <script type="math/tex">k</script> 阶近似估计，<script type="math/tex">Q_t^{(k)}</script> 为 <script type="math/tex">Q(s_t,a)</script> 的 <script type="math/tex">k</script> 阶近似估计，<script type="math/tex">V_t\overset{\triangle}{=}V_{\pi_\theta}(s_t)</script> ，写出 <script type="math/tex">t</script> 时刻优势函数的 1 阶近似估计：</p><script type="math/tex; mode=display">A_t^{(1)}=\underbrace{R_t + \gamma V_{t+1}}_{Q_t^{(1)}} - V_t</script><p>我们发现，它正好相当于 <script type="math/tex">t</script> 时刻的TD残差 <script type="math/tex">\delta_t</script>。</p><p>同理，2 阶近似估计：</p><script type="math/tex; mode=display">A_{t}^{(2)}=\underbrace{R_t+\gamma R_{t+1}+\gamma^2V_{t+2}}_{Q_{t}^{(2)}}-V_t</script><p>将其做一些变换，得到：</p><script type="math/tex; mode=display">\begin{aligned}A_{t}^{(2)}&=(R_t+\gamma V_{t+1}-V_t)+(\gamma R_{t+1}+\gamma^2V_{t+2}-\gamma V_{t+1})\\&=\delta_t + \gamma\delta_{t+1}\end{aligned}</script><p>同理，我们也可以写出 <script type="math/tex">k</script> 阶近似估计的通项：</p><script type="math/tex; mode=display">A_{t}^{(k)}=\underbrace{(\sum_{i=0}^{k-1}\gamma^iR_{t+i})+\gamma^kV_{t+k}}_{Q_{t}^{(k)}}-V_t</script><p>同样可以将上式变成：</p><script type="math/tex; mode=display">A_{t}^{(k)}=\sum_{l=0}^{k-1}\gamma^l\delta_{t+l}</script><p>恰好是TD残差 <script type="math/tex">\delta</script> 序列的带衰减累计求和。</p><p>近似阶数 <script type="math/tex">k</script> 越大，我们得到的估计值的<strong>偏差越小</strong>，但其中包含的随机变量（<script type="math/tex">\{R_{t+i}\ \mid  \ i=0,\dots,k-1\}</script>）越多，因此<strong>方差反而变大</strong>。</p><p>接下来有个操作叫 <strong><script type="math/tex">\lambda-\text{return}</script></strong> 算法，它的作用是平衡这些估计的偏差与方差。</p><p>简而言之，该方法使用一个 <script type="math/tex">(0,1)</script> 上的系数 <script type="math/tex">\lambda</script> ，对这些估计进行加权求和，即：</p><script type="math/tex; mode=display">\sum_{i=1}^\infty\lambda^{i-1}A_t^{(i)}</script><p>阶数越高的估计值，权重越小，以此降低其方差。由此得到的和式能够兼顾偏差和方差。</p><p>由于所有的 <script type="math/tex">A_t^{(i)}</script> 都是 <script type="math/tex">A_t</script> 的估计，上式的期望差不多相当于 <script type="math/tex">\frac{1}{1-\lambda}A_t</script>, 因此我们还应该乘上一个系数 <script type="math/tex">1-\lambda</script>，才能得到真正对 <script type="math/tex">A_t</script> 的估计：</p><script type="math/tex; mode=display">\begin{aligned}\hat{A_t}&=(1-\lambda)\sum_{i=1}^\infty\lambda^{i-1}A_t^{(i)}\\&=(1-\lambda)\sum_{i=1}^\infty\lambda^{i-1}\sum_{l=0}^{i-1}\gamma^l\delta_{t+l}\end{aligned}</script><p>这便是<strong>广义优势估计 (GAE) </strong>算法。</p><hr><p>对于PPO算法的基本原理就讲到这儿，接下来让我们来看看这个算法是如何应用在大语言模型的训练流程中的。为了让PPO能够发挥作用，我们还缺少一个模块，那就是用来计算上文中多次出现的 <script type="math/tex">R_i</script> 的值（Reward）的模型，也就是所谓 <strong>Reward Model</strong>，<a href="/blog/12760/">下一篇文章</a>，博主将介绍如何训练一个 Reward Model。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/ppo-%E7%AE%97%E6%B3%95/">PPO 算法</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">强化学习</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12706/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（二）：监督微调(SFT)</title>
      <link>https://blog.fyz666.xyz/blog/12690/</link>
      <guid>https://blog.fyz666.xyz/blog/12690/</guid>
      <pubDate>Mon, 23 Jun 2025 06:25:25 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;在前两篇文章中，博主已经简单介绍了大语言模型的&lt;a href=&quot;/blog/12634/&quot;&gt;预训练阶段&lt;/a&gt;，以及如何通过&lt;a href=&quot;/blog/12679/&quot;&gt;BPE（Byte Pair</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>在前两篇文章中，博主已经简单介绍了大语言模型的<a href="/blog/12634/">预训练阶段</a>，以及如何通过<a href="/blog/12679/">BPE（Byte Pair Encoding）算法</a>将自然语言高效地转化为离散的子词单元，从而降低词表规模、提高模型泛化能力。</p><p>我们已经知道，在大规模语料数据上进行的预训练使模型具备了广泛的语言知识，但它学到的仅仅是“如何预测下一个词”的通用能力，距离解决特定任务（如问答、摘要、对话）还有相当的差距。</p><p>而<strong>监督微调</strong>正是让大语言模型从通用语言能力升级到任务导向能力的必经之路。换言之，SFT让模型不仅仅局限于能够把话写通顺，还能写的<strong>对题</strong>。</p><div class="note primary simple"><p>另外，博主基于一款参数量约为 2.13B 的迷你大语言模型，完成了其微调流程的简要复现。相关代码已开源，详见下面链接。</p><p>通过合理配置训练参数，并结合 LoRA（Low-Rank Adaptation），整个微调训练流程可在一块消费级显卡（博主使用的是 RTX 3090 Ti）上顺利完成。</p></div><a class="tag-Link" target="_blank" href="https://github.com/windshadow233/tiny-llm-training/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">tiny-llm-training</div>            <div class="tag-link-sitename">GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><h2 id="监督微调是什么"><a href="#监督微调是什么" class="headerlink" title="监督微调是什么"></a>监督微调是什么</h2><p><strong>监督微调（Supervised Fine-Tuning, SFT）</strong>是指在预训练模型的基础上，利用一个特定任务的人类标注数据集对模型进行进一步训练，从而教会模型如何更好地执行具体指令或任务，例如问答、摘要、对话、翻译等。</p><p>与预训练阶段在海量无标注自然语言数据集上进行训练不同，SFT 使用成对的输入-输出样本，通过显式的任务目标，引导模型“怎么按人类意图作答”。这一阶段通常只微调模型的一部分参数，例如最后几层，既能保留预训练所得的通用语言知识，又能高效适应特定任务。</p><h2 id="数据集-amp-处理方法"><a href="#数据集-amp-处理方法" class="headerlink" title="数据集 &amp; 处理方法"></a>数据集 &amp; 处理方法</h2><p>这一阶段的训练数据可能长这样：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>    <span class="token property">"prompt"</span><span class="token operator">:</span> <span class="token string">"请简要介绍一下蔡徐坤是谁。"</span><span class="token punctuation">,</span>    <span class="token property">"response"</span><span class="token operator">:</span> <span class="token string">"蔡徐坤是中国内地流行歌手、演员、音乐制作人，曾因参加《偶像练习生》节目而走红。"</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>不过，为了适合语言模型的输入格式，这类数据通常会被拼接成统一的 <strong>prompt 模板格式</strong>，明确区分“指令”和“回答”的结构，引导模型学会角色扮演和任务对齐。例如：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">Human: 请简要介绍一下蔡徐坤是谁。Assistant: 蔡徐坤是中国内地流行歌手、演员、音乐制作人，曾因参加《偶像练习生》节目而走红。<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>或者这种格式：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">### Instruction:请简要介绍一下蔡徐坤是谁。### Response:蔡徐坤是中国内地流行歌手、演员、音乐制作人，曾因参加《偶像练习生》节目而走红。<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>在实际训练时，这些格式会被进一步编码成 token 序列，并通过 label masking 的方式只对回答部分计算损失，这是因为我们的目的是让模型学会如何输出回答部分。下面代码给了一个具体的例子：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">__getitem__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> idx<span class="token punctuation">)</span><span class="token punctuation">:</span>    data <span class="token operator">=</span> self<span class="token punctuation">.</span>dataset<span class="token punctuation">[</span>idx<span class="token punctuation">]</span>    instruction <span class="token operator">=</span> data<span class="token punctuation">[</span><span class="token string">'instruction'</span><span class="token punctuation">]</span>    <span class="token builtin">input</span> <span class="token operator">=</span> data<span class="token punctuation">[</span><span class="token string">'input'</span><span class="token punctuation">]</span>    output <span class="token operator">=</span> data<span class="token punctuation">[</span><span class="token string">'output'</span><span class="token punctuation">]</span>    <span class="token keyword">if</span> <span class="token builtin">input</span><span class="token punctuation">:</span>        prompt <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"指令:</span><span class="token interpolation"><span class="token punctuation">&#123;</span>instruction<span class="token punctuation">&#125;</span></span><span class="token string">\n</span><span class="token interpolation"><span class="token punctuation">&#123;</span><span class="token builtin">input</span><span class="token punctuation">&#125;</span></span><span class="token string">\n输出:"</span></span>    <span class="token keyword">else</span><span class="token punctuation">:</span>        prompt <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"指令:</span><span class="token interpolation"><span class="token punctuation">&#123;</span>instruction<span class="token punctuation">&#125;</span></span><span class="token string">\n输出:"</span></span>    prompt_ids <span class="token operator">=</span> self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>encode<span class="token punctuation">(</span>prompt<span class="token punctuation">,</span> add_special_tokens<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>    response_ids <span class="token operator">=</span> self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>encode<span class="token punctuation">(</span>output<span class="token punctuation">,</span> add_special_tokens<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>    input_ids <span class="token operator">=</span> prompt_ids <span class="token operator">+</span> <span class="token punctuation">[</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>bos_token_id<span class="token punctuation">]</span> <span class="token operator">+</span> response_ids    labels <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">100</span><span class="token punctuation">]</span> <span class="token operator">*</span> <span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>prompt_ids<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">+</span> response_ids    input_ids <span class="token operator">=</span> input_ids<span class="token punctuation">[</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>max_length<span class="token punctuation">]</span>    labels <span class="token operator">=</span> labels<span class="token punctuation">[</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>max_length<span class="token punctuation">]</span>        <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>input_ids<span class="token punctuation">)</span> <span class="token operator">&lt;</span> self<span class="token punctuation">.</span>max_length<span class="token punctuation">:</span>        input_ids <span class="token operator">+=</span> <span class="token punctuation">[</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>eos_token_id<span class="token punctuation">]</span>        labels <span class="token operator">+=</span> <span class="token punctuation">[</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>eos_token_id<span class="token punctuation">]</span>    pad_len <span class="token operator">=</span> self<span class="token punctuation">.</span>max_length <span class="token operator">-</span> <span class="token builtin">len</span><span class="token punctuation">(</span>input_ids<span class="token punctuation">)</span>    <span class="token keyword">if</span> pad_len <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>        input_ids <span class="token operator">+=</span> <span class="token punctuation">[</span>self<span class="token punctuation">.</span>tokenizer<span class="token punctuation">.</span>pad_token_id<span class="token punctuation">]</span> <span class="token operator">*</span> pad_len        labels <span class="token operator">+=</span> <span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">100</span><span class="token punctuation">]</span> <span class="token operator">*</span> pad_len    attention_mask <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">*</span> <span class="token punctuation">(</span>self<span class="token punctuation">.</span>max_length <span class="token operator">-</span> pad_len<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">*</span> pad_len    <span class="token keyword">return</span> <span class="token punctuation">&#123;</span>        <span class="token string">"input_ids"</span><span class="token punctuation">:</span> torch<span class="token punctuation">.</span>tensor<span class="token punctuation">(</span>input_ids<span class="token punctuation">,</span> dtype<span class="token operator">=</span>torch<span class="token punctuation">.</span><span class="token builtin">long</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"attention_mask"</span><span class="token punctuation">:</span> torch<span class="token punctuation">.</span>tensor<span class="token punctuation">(</span>attention_mask<span class="token punctuation">,</span> dtype<span class="token operator">=</span>torch<span class="token punctuation">.</span><span class="token builtin">long</span><span class="token punctuation">)</span><span class="token punctuation">,</span>        <span class="token string">"labels"</span><span class="token punctuation">:</span> torch<span class="token punctuation">.</span>tensor<span class="token punctuation">(</span>labels<span class="token punctuation">,</span> dtype<span class="token operator">=</span>torch<span class="token punctuation">.</span><span class="token builtin">long</span><span class="token punctuation">)</span>    <span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>上述代码的完整版见<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/SFT/dataset.py">此文件</a>。该方法做了两件事：</p><ul><li>将数据处理为 <code>prompt_ids + bos_token_id + response_ids + eos_token_id</code>的形式，构成完整的输入序列。</li><li>使用<code>-100</code>为<code>labels</code>中的<code>prompt</code>部分以及<code>pad</code>部分打上掩码（因为这些内容模型不需要学习）。</li></ul><h2 id="LoRA"><a href="#LoRA" class="headerlink" title="LoRA"></a>LoRA</h2><p><strong>LoRA（Low-Rank Adaptation）</strong> 是一种轻量级参数微调方法，其核心思想是<strong>矩阵的低秩分解</strong>，具体而言，是在不改变原有大模型参数的基础上，以两个低秩矩阵的乘积的形式添加一个可学习的增量参数模块，从而达到高效微调的目的。</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/15e83f27-ab39-454f-ae39-391ad7589f49.png" style="zoom:50%;" /></p><p>在SFT中，LoRA是一种常见的微调手段，这是因为对大模型进行全量微调不仅参数量太大、显存开销高，而且容易干扰原预训练模型（过多学习指令数据集导致模型忘记原有的语言能力），相比之下，LoRA 只在模型中插入少量可训练参数，既降低了资源需求，又避免了灾难性遗忘，使得微调过程更稳定、灵活，适合小数据集和多任务扩展场景。</p><p>LoRA 在不修改原始权重矩阵 <script type="math/tex">W\in\mathbb{R}^{m \times n}</script> 的前提下，通过添加一个<strong>低秩矩阵近似项</strong> <script type="math/tex">\Delta W</script> 来引入可学习的增量：</p><script type="math/tex; mode=display">W_{\text{LoRA}} = W+\Delta W=W+\frac{1}{\alpha}(AB)^\top</script><p>其中 <script type="math/tex">A\in\mathbb{R}^{n \times r}</script> 和 <script type="math/tex">B\in\mathbb{R}^{r \times m}</script> 是两个秩为 <script type="math/tex">r</script> 的矩阵。在训练时，我们冻结原始权重矩阵 <script type="math/tex">W</script>，仅学习 <script type="math/tex">A</script> 和 <script type="math/tex">B</script>，从而极大的减少了可学习的参数量，同时还能保证原始模型的表达能力不受过多干扰。</p><p>一个简单的LoRA层实现：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> torch<span class="token punctuation">.</span>nn <span class="token keyword">import</span> Module<span class="token keyword">class</span> <span class="token class-name">LoraLinear</span><span class="token punctuation">(</span>torch<span class="token punctuation">.</span>nn<span class="token punctuation">.</span>Module<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> linear<span class="token punctuation">,</span> r<span class="token operator">=</span><span class="token number">128</span><span class="token punctuation">,</span> alpha<span class="token operator">=</span><span class="token number">32</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token builtin">super</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>__init__<span class="token punctuation">(</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>linear <span class="token operator">=</span> linear        self<span class="token punctuation">.</span>r <span class="token operator">=</span> r        self<span class="token punctuation">.</span>alpha <span class="token operator">=</span> alpha        self<span class="token punctuation">.</span>scale <span class="token operator">=</span> alpha <span class="token operator">/</span> r        self<span class="token punctuation">.</span>lora_A <span class="token operator">=</span> torch<span class="token punctuation">.</span>nn<span class="token punctuation">.</span>Parameter<span class="token punctuation">(</span>torch<span class="token punctuation">.</span>randn<span class="token punctuation">(</span>linear<span class="token punctuation">.</span>in_features<span class="token punctuation">,</span> r<span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">0.1</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>lora_B <span class="token operator">=</span> torch<span class="token punctuation">.</span>nn<span class="token punctuation">.</span>Parameter<span class="token punctuation">(</span>torch<span class="token punctuation">.</span>zeros<span class="token punctuation">(</span>r<span class="token punctuation">,</span> linear<span class="token punctuation">.</span>out_features<span class="token punctuation">)</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>linear<span class="token punctuation">.</span>weight<span class="token punctuation">.</span>requires_grad <span class="token operator">=</span> <span class="token boolean">False</span>    <span class="token keyword">def</span> <span class="token function">forward</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> x<span class="token punctuation">)</span><span class="token punctuation">:</span>        y <span class="token operator">=</span> self<span class="token punctuation">.</span>linear<span class="token punctuation">(</span>x<span class="token punctuation">)</span>        y <span class="token operator">+=</span> self<span class="token punctuation">.</span>scale <span class="token operator">*</span> <span class="token punctuation">(</span>x @ self<span class="token punctuation">.</span>lora_A @ self<span class="token punctuation">.</span>lora_B<span class="token punctuation">)</span>        <span class="token keyword">return</span> y<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="训练过程"><a href="#训练过程" class="headerlink" title="训练过程"></a>训练过程</h2><p>在完成数据预处理和模型结构准备（如插入 LoRA 模块）后，监督微调的训练流程与预训练阶段非常类似，核心仍是语言模型的自回归目标：</p><script type="math/tex; mode=display">\min_\theta\mathbb E(x,y)\sim\mathcal{D}[-\log P_\theta(y\mid x)]</script><p>其中 <script type="math/tex">x</script> 和 <script type="math/tex">y</script> 分别为用户的prompt和人类标注的参考回答。</p><p>训练流程的详细代码见<a href="https://github.com/windshadow233/tiny-llm-training/blob/main/sft_training.py">此文件</a>。</p><hr><p>到了这里，我们已经了解了大语言模型训练中的第二块拼图 —— <strong>监督微调（SFT）</strong>。它的核心目标，其实就是让模型“听话”：不仅能说得通顺，还要能说得符合人类的预期。</p><p>接下来，我们还希望模型变得更聪明——通过人类反馈优化（比如 RLHF），进一步学会给出更加符合人类偏好的回应。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/sft/">SFT</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12690/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（一）： BPE分词算法</title>
      <link>https://blog.fyz666.xyz/blog/12679/</link>
      <guid>https://blog.fyz666.xyz/blog/12679/</guid>
      <pubDate>Sun, 22 Jun 2025 04:11:13 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;&lt;a href=&quot;/blog/12634/&quot;&gt;上一篇文章&lt;/a&gt;中，我们已经了解了大语言模型通过在海量自然语言语料上学习&lt;strong&gt;预测下一个</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p><a href="/blog/12634/">上一篇文章</a>中，我们已经了解了大语言模型通过在海量自然语言语料上学习<strong>预测下一个 token</strong>，以此建立语言理解能力，这一过程也就是所谓的<strong>预训练</strong>。那么问题随之而来：我们经常提到的<strong>token</strong>究竟是什么？它与文本、词汇之间有着怎样的关系与区别？</p><p>为了解决这个问题，我们需要从文本如何被编码成模型可读的序列说起。</p><h2 id="早期的编码方案"><a href="#早期的编码方案" class="headerlink" title="早期的编码方案"></a>早期的编码方案</h2><h3 id="字符级编码"><a href="#字符级编码" class="headerlink" title="字符级编码"></a>字符级编码</h3><p>以英文为例，在早期的文本编码方案中，每个字符都被单独视为一个token，这也被称为字符级编码。例如，对于句子：”hello world!”，它的token序列（尚未映射为数字）为：<code>[&#39;h&#39;, &#39;e&#39;, &#39;l&#39;, &#39;l&#39;, &#39;o&#39;, &#39; &#39;, &#39;w&#39;, &#39;o&#39;, &#39;r&#39;, &#39;l&#39;, &#39;d&#39;, &#39;!&#39;]</code>。</p><p>这种编码方式的优点十分明显：实现简单、通用性（可扩展性）强，词表较小，可以处理几乎所有语言中的所有字符，不会存在OOV（Out-Of- Vocabulary）问题。然而，它也存在显著的局限性：</p><ul><li>由于每个字符都被单独编码，序列长度显著增加，模型在处理长文本时计算开销大，训练效率低。</li><li>字符粒度太细，模型难以捕捉词语层面的语义结构——试问有谁是按字符理解语言的？</li></ul><p><img src="https://blogfiles.oss.fyz666.xyz/webp/e1c46a25-73bb-466e-a26b-6c6f6fd79c1c.webp" alt=""></p><hr><h3 id="词级编码"><a href="#词级编码" class="headerlink" title="词级编码"></a>词级编码</h3><p>相比字符级编码，词级编码以「词（word）」作为基本单位，将每个词直接映射为一个 token。这是人类语言理解的自然粒度，因此这种编码方案在早期 NLP 模型中（如 Word2Vec、LSTM）被广泛采用。</p><p>同样对于句子：”hello world!”，采用词级编码得到的token序列为: <code>[&#39;hello&#39;, &#39;world&#39;, &#39;!&#39;]</code>。</p><p>这种编码方式的优点在于：</p><ul><li>直观，与人类语言习惯一致，每个token具有完整的语义信息。</li><li>编码序列短，处理效率高。</li></ul><p>但这种编码方式也存在缺点：</p><ul><li><p>每个词都得编码成一个token，导致词表庞大。</p><blockquote><p>Many people estimate that there are more than a million words in the English language. In fact, during a project looking at words in digitised books, researchers from Harvard University and Google in 2010, they estimated a total of 1,022,000 words and that the number would grow by several thousand each year.</p><p><cite><a href="https://englishlive.ef.com/en/blog/language-lab/many-words-english-language/">English Live</a></cite></p></blockquote></li><li><p>词表虽然变大了，但出现未登录词的可能性反而上升了：一旦有什么新的单词没收录在表里，模型只能以<code>&lt;unk&gt;</code>代替，导致信息损失严重。</p></li><li><p>跨语言迁移能力较差。</p></li></ul><hr><p>我们可以看到，前述的两种分词编码方法要么粒度太细，模型读不懂语义；要么粒度太粗，新词一来就懵逼，只能用 <code>&lt;unk&gt;</code> 顶上。可见这两种方式都不太理想。</p><p>为了取得两者的平衡，让模型在表达能力与编码效率之间取得更好的平衡，子词级编码方法被提出。这类方法试图将文本划分为比「词」更小、但比「字符」更有语义的信息单元，从而兼顾词汇覆盖率与建模能力。其中最具代表性、也是目前应用最广泛的方案，就是<strong>BPE（Byte Pair Encoding）算法</strong>。</p><h2 id="BPE-算法"><a href="#BPE-算法" class="headerlink" title="BPE 算法"></a>BPE 算法</h2><p>如前文提到的，在自然语言处理中，研究者经常会面临两个极端：</p><ul><li><p>下策：以字符为编码单位，不容易OOV，但缺点过于致命：模型完全不知道这些字符合起来是什么意思，学习难度过大。</p></li><li><p>上策：以词为编码编码单位，可以保留完整的语义信息，例如：<code>internationalization</code>这个词整体作为一个token，语义信息丰富，看上去省事省心一步到位。然而，问题来了——如果是<code>internationalizations</code>呢？多了个 s，抱歉，模型词典里没有，不认识。由此可见，词级编码面临一个大bug：<strong>词表巨大、组合爆炸、扩展性差</strong>。你得穷尽所有单词的各种变形、拼写版本……搞得像字典编辑部天天加班。</p></li></ul><p><img src="https://blogfiles.oss.fyz666.xyz/webp/de3e2fce-1145-44df-b34e-fb275c4cb410.webp" style="zoom:50%;" /></p><p>于是，一种「花下策的成本，达到上策的效果」的中策应运而生：BPE算法。（当然，实际上成本肯定是要高于下策的，但性价比已经相当高了）</p><p>其实这个算法<a href="http://www.pennelynn.com/Documents/CUJ/HTML/94HTML/19940045.HTM">很早就已经被提出了</a>，当时是用于数据压缩，其基本思路是通过寻找文本中出现频率最高的相邻字节对，将其合并为一个新的字节，然后重复该操作直到达成某种终止条件。</p><p>本文略过该算法在数据压缩领域的应用，直接介绍如何将这种思路用于tokenizer。</p><hr><h3 id="算法流程概览"><a href="#算法流程概览" class="headerlink" title="算法流程概览"></a>算法流程概览</h3><p>偷懒了，这里直接放个训练过程的伪代码。</p><script type="math/tex; mode=display">\begin{array}{l}\nonumber\textbf{输入：}~\mathcal{C}~\text{（训练语料），目标词表大小}~V \\[1ex]\textbf{初始化：} \\\quad \mathcal{T} \leftarrow \text{将每个词拆为字符序列，并在末尾加上}~\texttt{</w>} \\\quad \text{词表}~\mathcal{V} \leftarrow \text{所有出现的字符}~\cup~\{\texttt{</w>}\} \\\quad \text{合并规则序列}~\mathcal{M}\leftarrow \text{空集} \\[1ex]\textbf{重复，直到}~|\mathcal{V}| \geq V~\text{或无高频对：} \\\quad 1.~\text{统计}~\mathcal{T}~\text{中所有相邻 token 对的频率:}~f(a, b) \\\quad 2.~\text{选出频率最高的对}~(a^*, b^*) = \arg\max f(a, b) \\\quad 3.~\text{将合并规则:}~(a^*, b^*)\rightarrow a^*b^*~\text{加入}~\mathcal{M}\\\quad 4.~\text{将}~\mathcal{T}~{中所有}~(a^*, b^*)~\text{合并为新 token:}~a^*b^* \\\quad 5.~\text{将}~a^*b^*~加入词表~\mathcal{V} \\\quad 6.~\textbf{若}~a^*~\text{或}~b^*~\text{已不再以独立 token 出现在}~\mathcal{T}~\text{中,}~\textbf{则} \\\quad\quad\quad \text{从词表}~\mathcal{V}~\text{中移除}~a^*~\text{或}~b^* \\[1ex]\textbf{输出：}~\text{合并规则序列}~\mathcal{M}\end{array}</script><p>至于训练结束后如何进行分词，主要就是用了上面算法输出的合并规则序列 <script type="math/tex">\mathcal{M}</script>​​，分词过程的基本思路是：</p><div class="note primary simple"><p>先将输入文本按照最细粒度（如字符级）进行切分，然后依照 $\mathcal{M}$ 中的合并顺序，从上到下依次遍历并执行匹配合并操作，直到无法再匹配为止。</p></div><h3 id="实例演示"><a href="#实例演示" class="headerlink" title="实例演示"></a>实例演示</h3><p>以一个简单的语料集为例，假设我们拿到了数据：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">low lower lowestplay played playing playerhappy happier happiestrunning runs runnerinternational internationalization internationalizeunderstanding misunderstand misunderstanding<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我们首先拿到所有单词按字符的拆分，同时在末尾添加一个符号<code>&lt;/w&gt;</code>表示词尾：</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/f6afae5c-b578-49a1-bf9c-caf476539aea.webp" alt="bpe" style="zoom:50%;" /></p><p>将所有出现过的字符记录下来，作为当前的词表（词表大小为19）：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token punctuation">&#123;</span><span class="token string">'w'</span><span class="token punctuation">,</span> <span class="token string">'r'</span><span class="token punctuation">,</span> <span class="token string">'e'</span><span class="token punctuation">,</span> <span class="token string">'u'</span><span class="token punctuation">,</span> <span class="token string">'a'</span><span class="token punctuation">,</span> <span class="token string">'t'</span><span class="token punctuation">,</span> <span class="token string">'i'</span><span class="token punctuation">,</span> <span class="token string">'z'</span><span class="token punctuation">,</span> <span class="token string">'n'</span><span class="token punctuation">,</span> <span class="token string">'y'</span><span class="token punctuation">,</span> <span class="token string">'m'</span><span class="token punctuation">,</span> <span class="token string">'&lt;/w>'</span><span class="token punctuation">,</span> <span class="token string">'d'</span><span class="token punctuation">,</span> <span class="token string">'g'</span><span class="token punctuation">,</span> <span class="token string">'h'</span><span class="token punctuation">,</span> <span class="token string">'p'</span><span class="token punctuation">,</span> <span class="token string">'l'</span><span class="token punctuation">,</span> <span class="token string">'s'</span><span class="token punctuation">,</span> <span class="token string">'o'</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>假设我们的目标词表长度为25。</p><p>我们统计每一对<strong>相邻字符</strong>出现的频率，并按频率从高到低排序，我们得到<strong>相邻字符对</strong>的频率表（省略了后面的部分）：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">(&#39;e&#39;, &#39;r&#39;): 10(&#39;i&#39;, &#39;n&#39;): 7(&#39;u&#39;, &#39;n&#39;): 6(&#39;n&#39;, &#39;a&#39;): 6(&#39;n&#39;, &#39;d&#39;): 6(&#39;s&#39;, &#39;t&#39;): 5(&#39;r&#39;, &#39;&lt;&#x2F;w&gt;&#39;): 4...<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>找到出现频率最高的组合：<code>(&#39;e&#39;, &#39;r&#39;)</code>，然后遍历所有拆分列表，将所有该组合进行合并，得到新的单词拆分方式：</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/c9e8a75d-986d-4ec3-9877-df2937a2e4a8.webp" alt="bpe" style="zoom:50%;" /></p><p>将组合<code>er</code>添加到词表中，不过此时单词的拆分中仍存在独立的<code>e</code>和<code>r</code>，故将它们保留在词表中。当前词表大小：20。</p><p>继续统计每一对相邻字符（此时，由于我们前面合并了<code>e</code>和<code>r</code>，故在统计时需要把<code>er</code>看作一个整体）。我们得到新的<strong>相邻字符对</strong>的频率表：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">(&#39;i&#39;, &#39;n&#39;): 7(&#39;u&#39;, &#39;n&#39;): 6(&#39;n&#39;, &#39;a&#39;): 6(&#39;n&#39;, &#39;d&#39;): 6(&#39;s&#39;, &#39;t&#39;): 5(&#39;er&#39;, &#39;&lt;&#x2F;w&gt;&#39;): 4...<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>合并<code>i</code>与<code>n</code>，将<code>in</code>加入词表，不移除<code>i</code>和<code>n</code>。当前词表大小：21。</p><p>统计新的相邻字符频率：</p><pre class="line-numbers language-raw" data-language="raw"><code class="language-raw">(&#39;u&#39;, &#39;n&#39;): 6(&#39;n&#39;, &#39;a&#39;): 6(&#39;n&#39;, &#39;d&#39;): 6(&#39;s&#39;, &#39;t&#39;): 5(&#39;er&#39;, &#39;&lt;&#x2F;w&gt;&#39;): 4...<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>合并<code>u</code>与<code>n</code>，将<code>un</code>加入词表：</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/3a8df37e-695e-47e0-891a-46616dcbf22d.webp" alt="bpe" style="zoom:50%;" /></p><p>此时，发现已经没有单独出现的字符<code>u</code>了，因此从词表中移除<code>u</code>。当前词表大小：21。</p><hr><p>持续进行上述操作，直到达到下面两个终止条件之一：</p><ol><li>词表大小达到我们的预设值：本例中为25。</li><li>没有可合并的<strong>高频</strong>相邻字符对。（可自定义频率阈值）</li></ol><h3 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h3><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> collections <span class="token keyword">import</span> defaultdictcorpus <span class="token operator">=</span> <span class="token punctuation">[</span>    <span class="token string">'low lower lowest'</span><span class="token punctuation">,</span>    <span class="token string">'play played playing player'</span><span class="token punctuation">,</span>    <span class="token string">'happy happier happiest'</span><span class="token punctuation">,</span>    <span class="token string">'running runs runner'</span><span class="token punctuation">,</span>    <span class="token string">'international internationalization internationalize'</span><span class="token punctuation">,</span>    <span class="token string">'understanding misunderstand misunderstanding'</span><span class="token punctuation">]</span><span class="token keyword">class</span> <span class="token class-name">BPE</span><span class="token punctuation">:</span>    <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> corpus<span class="token punctuation">,</span> vocab_size<span class="token punctuation">)</span><span class="token punctuation">:</span>        self<span class="token punctuation">.</span>corpus <span class="token operator">=</span> corpus        self<span class="token punctuation">.</span>vocab_size <span class="token operator">=</span> vocab_size        self<span class="token punctuation">.</span>alphabet <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token string">'&lt;/w>'</span><span class="token punctuation">&#125;</span>        self<span class="token punctuation">.</span>word_freqs <span class="token operator">=</span> defaultdict<span class="token punctuation">(</span><span class="token builtin">int</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>split <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span>        self<span class="token punctuation">.</span>merges <span class="token operator">=</span> <span class="token punctuation">&#123;</span><span class="token punctuation">&#125;</span>        self<span class="token punctuation">.</span>initialize<span class="token punctuation">(</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>vocab <span class="token operator">=</span> self<span class="token punctuation">.</span>alphabet<span class="token punctuation">.</span>copy<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">def</span> <span class="token function">initialize</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">for</span> words <span class="token keyword">in</span> self<span class="token punctuation">.</span>corpus<span class="token punctuation">:</span>            <span class="token keyword">for</span> word <span class="token keyword">in</span> words<span class="token punctuation">.</span>split<span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span><span class="token punctuation">:</span>                self<span class="token punctuation">.</span>word_freqs<span class="token punctuation">[</span>word<span class="token punctuation">]</span> <span class="token operator">+=</span> <span class="token number">1</span>                self<span class="token punctuation">.</span>alphabet<span class="token punctuation">.</span>update<span class="token punctuation">(</span><span class="token builtin">set</span><span class="token punctuation">(</span>word<span class="token punctuation">)</span><span class="token punctuation">)</span>        self<span class="token punctuation">.</span>split <span class="token operator">=</span> <span class="token punctuation">&#123;</span>word<span class="token punctuation">:</span> <span class="token builtin">list</span><span class="token punctuation">(</span>word<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token punctuation">[</span><span class="token string">'&lt;/w>'</span><span class="token punctuation">]</span> <span class="token keyword">for</span> word <span class="token keyword">in</span> self<span class="token punctuation">.</span>word_freqs<span class="token punctuation">.</span>keys<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">&#125;</span>    <span class="token keyword">def</span> <span class="token function">print_split</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>        max_len <span class="token operator">=</span> <span class="token builtin">max</span><span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>word<span class="token punctuation">)</span> <span class="token keyword">for</span> word <span class="token keyword">in</span> self<span class="token punctuation">.</span>split<span class="token punctuation">)</span>        <span class="token keyword">for</span> word<span class="token punctuation">,</span> s <span class="token keyword">in</span> self<span class="token punctuation">.</span>split<span class="token punctuation">.</span>items<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>            padding <span class="token operator">=</span> <span class="token string">" "</span> <span class="token operator">*</span> <span class="token punctuation">(</span>max_len <span class="token operator">-</span> <span class="token builtin">len</span><span class="token punctuation">(</span>word<span class="token punctuation">)</span><span class="token punctuation">)</span>            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">&#123;</span>word<span class="token punctuation">&#125;</span></span><span class="token interpolation"><span class="token punctuation">&#123;</span>padding<span class="token punctuation">&#125;</span></span><span class="token string"> → </span><span class="token interpolation"><span class="token punctuation">&#123;</span><span class="token string">' '</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>s<span class="token punctuation">)</span><span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>    <span class="token keyword">def</span> <span class="token function">get_stats</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>        pairs <span class="token operator">=</span> defaultdict<span class="token punctuation">(</span><span class="token builtin">int</span><span class="token punctuation">)</span>        <span class="token keyword">for</span> word<span class="token punctuation">,</span> freq <span class="token keyword">in</span> self<span class="token punctuation">.</span>word_freqs<span class="token punctuation">.</span>items<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>            word_split <span class="token operator">=</span> self<span class="token punctuation">.</span>split<span class="token punctuation">[</span>word<span class="token punctuation">]</span>            <span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token builtin">len</span><span class="token punctuation">(</span>word_split<span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">:</span>                pairs<span class="token punctuation">[</span><span class="token punctuation">(</span>word_split<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">,</span> word_split<span class="token punctuation">[</span>i <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">]</span> <span class="token operator">+=</span> freq        <span class="token keyword">return</span> pairs    <span class="token keyword">def</span> <span class="token function">merge_pair</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> pair<span class="token punctuation">)</span><span class="token punctuation">:</span>        self<span class="token punctuation">.</span>merges<span class="token punctuation">[</span>pair<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>pair<span class="token punctuation">)</span>        <span class="token keyword">for</span> word <span class="token keyword">in</span> self<span class="token punctuation">.</span>word_freqs<span class="token punctuation">:</span>            split <span class="token operator">=</span> self<span class="token punctuation">.</span>split<span class="token punctuation">[</span>word<span class="token punctuation">]</span>            <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>split<span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token number">1</span><span class="token punctuation">:</span>                <span class="token keyword">continue</span>            idx <span class="token operator">=</span> <span class="token number">0</span>            <span class="token keyword">while</span> idx <span class="token operator">&lt;</span> <span class="token builtin">len</span><span class="token punctuation">(</span>split<span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">:</span>                <span class="token keyword">if</span> <span class="token punctuation">(</span>split<span class="token punctuation">[</span>idx<span class="token punctuation">]</span><span class="token punctuation">,</span> split<span class="token punctuation">[</span>idx <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">==</span> pair<span class="token punctuation">:</span>                    split<span class="token punctuation">[</span>idx<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>pair<span class="token punctuation">)</span>                    <span class="token keyword">del</span> split<span class="token punctuation">[</span>idx <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span>                <span class="token keyword">else</span><span class="token punctuation">:</span>                    idx <span class="token operator">+=</span> <span class="token number">1</span>    <span class="token keyword">def</span> <span class="token function">find_single_item</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> item<span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">for</span> split <span class="token keyword">in</span> self<span class="token punctuation">.</span>split<span class="token punctuation">.</span>values<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>            <span class="token keyword">for</span> i <span class="token keyword">in</span> split<span class="token punctuation">:</span>                <span class="token keyword">if</span> i <span class="token operator">==</span> item<span class="token punctuation">:</span>                    <span class="token keyword">return</span> <span class="token boolean">True</span>        <span class="token keyword">return</span> <span class="token boolean">False</span>    <span class="token keyword">def</span> <span class="token function">train</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">while</span> <span class="token builtin">len</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>vocab<span class="token punctuation">)</span> <span class="token operator">&lt;</span> self<span class="token punctuation">.</span>vocab_size<span class="token punctuation">:</span>            pairs <span class="token operator">=</span> self<span class="token punctuation">.</span>get_stats<span class="token punctuation">(</span><span class="token punctuation">)</span>            <span class="token keyword">if</span> <span class="token keyword">not</span> pairs<span class="token punctuation">:</span>                <span class="token keyword">break</span>            sorted_pairs <span class="token operator">=</span> <span class="token builtin">sorted</span><span class="token punctuation">(</span>pairs<span class="token punctuation">.</span>items<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> key<span class="token operator">=</span><span class="token keyword">lambda</span> x<span class="token punctuation">:</span> x<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span> reverse<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>            max_pair<span class="token punctuation">,</span> max_freq <span class="token operator">=</span> sorted_pairs<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>            <span class="token keyword">if</span> max_freq <span class="token operator">&lt;=</span> <span class="token number">1</span><span class="token punctuation">:</span>                <span class="token keyword">break</span>            self<span class="token punctuation">.</span>merge_pair<span class="token punctuation">(</span>max_pair<span class="token punctuation">)</span>            self<span class="token punctuation">.</span>vocab<span class="token punctuation">.</span>add<span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>max_pair<span class="token punctuation">)</span><span class="token punctuation">)</span>            <span class="token keyword">for</span> i <span class="token keyword">in</span> max_pair<span class="token punctuation">:</span>                <span class="token keyword">if</span> <span class="token keyword">not</span> self<span class="token punctuation">.</span>find_single_item<span class="token punctuation">(</span>i<span class="token punctuation">)</span><span class="token punctuation">:</span>                    self<span class="token punctuation">.</span>vocab<span class="token punctuation">.</span>remove<span class="token punctuation">(</span>i<span class="token punctuation">)</span>            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Vocab size: "</span><span class="token punctuation">,</span> <span class="token builtin">len</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>vocab<span class="token punctuation">)</span><span class="token punctuation">,</span> end<span class="token operator">=</span><span class="token string">'\r'</span><span class="token punctuation">)</span>    <span class="token keyword">def</span> <span class="token function">tokenize</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> text<span class="token punctuation">)</span><span class="token punctuation">:</span>        pre_tokenized_text <span class="token operator">=</span> text<span class="token punctuation">.</span>split<span class="token punctuation">(</span><span class="token string">' '</span><span class="token punctuation">)</span>        splits_text <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">[</span>_ <span class="token keyword">for</span> _ <span class="token keyword">in</span> word<span class="token punctuation">]</span> <span class="token keyword">for</span> word <span class="token keyword">in</span> pre_tokenized_text<span class="token punctuation">]</span>        <span class="token keyword">for</span> pair <span class="token keyword">in</span> self<span class="token punctuation">.</span>merges<span class="token punctuation">.</span>keys<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>            <span class="token keyword">for</span> split <span class="token keyword">in</span> splits_text<span class="token punctuation">:</span>                idx <span class="token operator">=</span> <span class="token number">0</span>                <span class="token keyword">while</span> idx <span class="token operator">&lt;</span> <span class="token builtin">len</span><span class="token punctuation">(</span>split<span class="token punctuation">)</span> <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">:</span>                    <span class="token keyword">if</span> <span class="token punctuation">(</span>split<span class="token punctuation">[</span>idx<span class="token punctuation">]</span><span class="token punctuation">,</span> split<span class="token punctuation">[</span>idx <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">==</span> pair<span class="token punctuation">:</span>                        split<span class="token punctuation">[</span>idx<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span>pair<span class="token punctuation">)</span>                        <span class="token keyword">del</span> split<span class="token punctuation">[</span>idx <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">]</span>                    <span class="token keyword">else</span><span class="token punctuation">:</span>                        idx <span class="token operator">+=</span> <span class="token number">1</span>        result <span class="token operator">=</span> <span class="token builtin">sum</span><span class="token punctuation">(</span>splits_text<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        <span class="token keyword">return</span> result    <span class="token keyword">def</span> <span class="token function">export_vocab</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> vocab_path<span class="token operator">=</span><span class="token string">"vocab.json"</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        vocab <span class="token operator">=</span> <span class="token punctuation">&#123;</span>token<span class="token punctuation">:</span> idx <span class="token keyword">for</span> idx<span class="token punctuation">,</span> token <span class="token keyword">in</span> <span class="token builtin">enumerate</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>vocab<span class="token punctuation">)</span><span class="token punctuation">&#125;</span>        <span class="token keyword">import</span> json        <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>vocab_path<span class="token punctuation">,</span> <span class="token string">"w"</span><span class="token punctuation">,</span> encoding<span class="token operator">=</span><span class="token string">"utf-8"</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>            json<span class="token punctuation">.</span>dump<span class="token punctuation">(</span>vocab<span class="token punctuation">,</span> f<span class="token punctuation">,</span> ensure_ascii<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">,</span> indent<span class="token operator">=</span><span class="token number">2</span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Saved vocab to </span><span class="token interpolation"><span class="token punctuation">&#123;</span>vocab_path<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>    <span class="token keyword">def</span> <span class="token function">export_merges</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> merges_path<span class="token operator">=</span><span class="token string">"merges.txt"</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>merges_path<span class="token punctuation">,</span> <span class="token string">"w"</span><span class="token punctuation">,</span> encoding<span class="token operator">=</span><span class="token string">"utf-8"</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>            f<span class="token punctuation">.</span>write<span class="token punctuation">(</span><span class="token string">"#version: 0.2\n"</span><span class="token punctuation">)</span>            <span class="token keyword">for</span> pair <span class="token keyword">in</span> self<span class="token punctuation">.</span>merges<span class="token punctuation">.</span>keys<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>                f<span class="token punctuation">.</span>write<span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">&#123;</span>pair<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">&#125;</span></span><span class="token string"> </span><span class="token interpolation"><span class="token punctuation">&#123;</span>pair<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">&#125;</span></span><span class="token string">\n"</span></span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"Saved merges to </span><span class="token interpolation"><span class="token punctuation">&#123;</span>merges_path<span class="token punctuation">&#125;</span></span><span class="token string">"</span></span><span class="token punctuation">)</span>bpe <span class="token operator">=</span> BPE<span class="token punctuation">(</span>corpus<span class="token punctuation">,</span> <span class="token number">25</span><span class="token punctuation">)</span>bpe<span class="token punctuation">.</span>train<span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><hr><p>以上便是 BPE（Byte Pair Encoding）分词算法的核心思想、训练流程与示例代码。它以字符为起点，通过词频驱动的逐步合并，构建出稳定、高效、具有语义结构的子词单元，从而有效缓解了 OOV 问题并保证了模型的语义建模能力。</p><p>如今，随着自然语言处理技术的进一步发展，已经出现了许多更先进的分词技术，如基于概率的 Unigram 模型等，但 BPE 仍然是理解现代分词算法原理的良好起点。它结构清晰、逻辑直观，能够帮助我们把握子词构建、词表学习与编码压缩等核心思想，为进一步理解更复杂的分词方法打下基础。</p><p>有了分词器，我们便能将原始的自然语言文本转化为模型可以处理的 token 序列，搭建起语言与模型之间的桥梁。在此基础上，模型在大规模语料上进行预训练才成为可能。但这一步，只是让模型学会“怎么说话”。</p><p>经过了充分预训练的语言模型，只是一个“话痨”——它能接得住任何话题（擅长续写），却不一定听得懂你的意思。要让它从“能说”变成“听话”，我们还需要通过<strong>监督微调（SFT）</strong>进一步训练，让模型学会听指令、做任务、少废话。</p><p>这就是大模型训练的第二块拼图——笔者将在<a href="/blog/12690/">下一篇文章</a>中进行讨论。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/bpe-%E7%AE%97%E6%B3%95/">BPE 算法</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12679/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>大语言模型训练原理与实践（零）：预训练</title>
      <link>https://blog.fyz666.xyz/blog/12634/</link>
      <guid>https://blog.fyz666.xyz/blog/12634/</guid>
      <pubDate>Sat, 21 Jun 2025 16:12:59 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h2 id=&quot;前言（吐槽）&quot;&gt;&lt;a href=&quot;#前言（吐槽）&quot; class=&quot;headerlink&quot;</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h2 id="前言（吐槽）"><a href="#前言（吐槽）" class="headerlink" title="前言（吐槽）"></a>前言（吐槽）</h2><p>自从大语言模型横空出世，各行业的从业者都仿佛开了挂似的，科研、工作效率那是咔咔上涨，唯独我这个正儿八经的AI在读博士生，现在却感觉自己成了个小丑🤡：眼睁睁看着LLM在世界舞台上风生水起，自己训练的人工智能模型一个个都跟人工智障似的，别说去现实环境投入使用了，连外行审稿人都忽悠不过去；至于这💩上雕花的缝合怪科研，也是<del>憋不出个屁</del>黔驴技穷了。最绝望的是，当我试图投身LLM的怀抱时，不好意思，显存都不够训练个demo。</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/42472fd9-eb4c-438d-adf8-c049528555b5.webp" alt="" style="zoom: 20%;" /></p><p>在有点腻了这种闭门造车的行为以后，吐槽归吐槽，作为一个实干型的炼丹师，光把大模型用于应用、推理是远远不够的，还得弄明白它的训练原理。毕竟，只有了解了这些，才不至于在调参和部署中沦为“黑箱”工具的操作者，也意味着能更有针对性地优化数据处理流程、设计更有效的模型结构。换句话说，搞清楚训练原理，是从“用大模型”到“造大模型”的分水岭。</p><p>既然如此，接下来就该跳出“只会用API”的舒适区，真正去了解一下大模型训练过程中的三块核心拼图：预训练、SFT、RLHF 。</p><p>在开始之前，先叠几层甲：</p><div class="note primary simple"><p>由于笔者没有充足的算力去支持具有实用价值的大模型的训练、微调工作，本系列文章只是尽我所理解写了点最基本的大模型训练方法和逻辑，不包含任何的训练trick——这些训练技巧往往依赖大量实践经验和工程调优，在没有真实大规模实验条件的情况下难以深入探讨。</p></div><div class="note primary simple"><p>本系列文章仅讨论最传统的大模型训练流程，不涉及各种新兴的技术分支，对于更前沿或更复杂的训练范式，往往需要丰富的工程实践经验和大量实验积累才能真正掌握，故在此不作展开。</p></div><p>本文就先从大模型训练的第一步开始：预训练。</p><hr><h2 id="Transformer-amp-GPT"><a href="#Transformer-amp-GPT" class="headerlink" title="Transformer &amp; GPT"></a>Transformer &amp; GPT</h2><h3 id="Transformer"><a href="#Transformer" class="headerlink" title="Transformer"></a>Transformer</h3><p>大语言模型的基本组成模块，是2017年于论文<a href="https://arxiv.org/abs/1706.03762">Attention is All You Need</a>中提出的模型：<strong>Transformer</strong>。这里插句题外话：虽然我的研究方向和大模型是一毛钱关系没有，但我所使用的模型结构也包含了Transformer模块，而且我还在五年前<a href="https://github.com/windshadow233/HandWritten-Transformer-NMT">手搓过一个Transformer</a>，故对这个玩意我是再熟悉不过了。</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/88480c83-1c3a-43a0-b39b-d2b6273ae0ec.webp" alt="" style="zoom:50%;" /></p><p>因此，关于 Transformer 的基本结构我不再赘述。这里默认读者已经具备一定的相关基础，或者至少了解 Transformer 核心组件如嵌入层（Embedding）、自注意力（Self-Attention）、多头注意力（Multi-Head Attention）与前馈网络（Feed-Forward Network）。</p><p>简而言之，Transformer做了一件事：使用全局注意力机制替代了传统的 RNN，实现了并行化的序列建模。而它所能达到的效果其实非常简单，仅仅是将序列整体往后预测了一个时间步：</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/b1821456-6c1c-44eb-825c-8224909e1b4a.webp" alt="" style="zoom: 33%;" /></p><p>而对Transformer模型进行训练的过程，其实也如出一辙：对一个<code>input</code>序列计算得到<code>predict</code>序列，然后将<code>predict</code>序列与真实的<code>target</code>序列进行一个Cross Entropy的计算，例如上图的例子，如果<code>predict</code>得到：<code>cat is lying on the chair</code>，则会在最后一个词上产生一个较大的分类loss。这种建模方式称为<strong>自回归建模</strong>。</p><p>以上，就是Transformer最基本的训练方法。当然，训练形式也有多种改版，例如曾经名噪一时的BERT模型，是通过随机挖去句子中的某些token，让模型对这些挖掉的token进行预测的方式来训练（做完形填空是吧？），这种建模方式则称为<strong>掩码建模</strong>。</p><h3 id="GPT"><a href="#GPT" class="headerlink" title="GPT"></a>GPT</h3><p>GPT（Generative Pretrained Transformer）则是一种典型的自回归语言模型，它的基本组件正是前面介绍的Transformer——不过在设计上有一些简化与调整：</p><ul><li>去掉了Encoder层，只保留了Decoder层。</li><li>在训练时引入了因果掩码，这是由于自回归语言模型的任务是<strong>预测下一个token</strong>，确保模型在每个位置只能访问当前位置token及之前的token，否则无异于标签泄露。</li></ul><p><img src="https://blogfiles.oss.fyz666.xyz/webp/75327ffd-583e-4c74-8a09-e645d150bd92.webp" alt="Matplotlib Chart" style="zoom:30%;" /></p><p>整个GPT模型则是由一堆这样的Transformer Decoder模块层层堆叠而成，通过深层堆叠来增强模型的记忆、理解与生成能力。</p><p>GPT模型的最后输出层，与前面提到的Transformer相同，同样是一个<strong>全连接线性层</strong>，用于将每个位置的隐藏状态映射到等同于词表大小的 logits 上，然后再通过 <strong>softmax</strong> 得到每个 token 的预测概率分布。同样的，这个概率分布既可用于计算Cross Entropy，又可用于在推理过程中计算下一个token。</p><h2 id="GPT模型的预训练"><a href="#GPT模型的预训练" class="headerlink" title="GPT模型的预训练"></a>GPT模型的预训练</h2><p>随机初始化一个GPT模型的权重，我们得到了一个只会<del>阿巴阿巴</del>输出乱码的人工智障，它完全不懂人类的语言。为了让这个模型逐渐“开窍”，像鹦鹉学舌一样模仿人类说话，我们首先需要让它接受足够多的人类语言文本，从中学习到语言的基本结构、规律等。</p><p>这个过程，就是大语言模型训练的第一步：预训练。</p><p>那么，预训练到底在让模型学些什么？其实这个任务非常简单，正如前文提到的，模型只不过在不停地学习如何<strong>预测下一个token</strong>。</p><p>在看似简单的任务中，模型能学到的远比你感觉到的多：</p><p>除了基本的句法结构以外，它还需要掌握词与词之间的搭配规律（比如“大海捞针”比“海底捞针”更符合语言习惯，即使它们的语法都是对的）；理解上下文的逻辑关系，判断当前句子下一个最合理的词是什么。在很多时候，它还需要学会一些知识——比如在输入句子：「全民制作人们大家好，我是练习时长」时，模型若想答对，就得知道接下来三个字是“两年半”。</p><p><img src="https://blogfiles.oss.fyz666.xyz/webp/e935f31b-f62d-4c91-9e37-b5f13d1ee399.webp" alt="images"></p><p>那么模型从哪里能学到这些东西？</p><h2 id="数据来源-amp-处理"><a href="#数据来源-amp-处理" class="headerlink" title="数据来源 &amp; 处理"></a>数据来源 &amp; 处理</h2><p>互联网时代最不缺的就是自然语言语料数据，只要愿意抓取，网页、论坛、百科、书籍、社交媒体、问答网站等等，文本信息可不要太多，而且对于这样一个<strong>预测下一个token</strong>的任务而言，这些语料数据天然自带标签，也就是说，只要把海量文本收集起来，模型就能开始进行预训练。但在此之前，仍需要对数据进行一定的预处理，包括但不限定于以下：</p><ul><li><strong>清洗无关内容</strong>：例如去除网页文本中夹带的 HTML 标签，过滤掉乱码等明显非自然语言内容。</li><li><strong>优先采用高质量语料库</strong>：例如使用语言结构清晰、表达规范的维基百科等公开书面语料，这些内容能帮助模型学习到标准的句法结构和书面表达习惯。甚至还可以用传统方法训练一个分类器对文本质量进行打分，从而筛选高质量的语料。</li></ul><hr><p>以上，便是 GPT 模型预训练阶段的基本流程与核心逻辑。从<strong>预测下一个 token</strong>这一任务出发进行预训练，模型能够逐步建立起对语言的初步理解能力。这一阶段虽然看似简单，却是大语言模型能力构建的根基。</p><p>不过在前文中，我们略过了一个关键细节：出现了好多次的所谓 “token”，到底是什么？</p><p>在本文的例子中，它被简单描述为了某个词，但在工程上，它并不是简单的字、词，甚至有时只是一部分词。为了让模型既能处理常见词，也能灵活应对生僻词与多语言输入，我们必须对文本进行一种合理的编码与切分。这便引出了 BPE（Byte Pair Encoding）算法：一种高效的子词级编码方案，也是 GPT 等主流大模型采用的分词策略。</p><p>笔者将在<a href="/blog/12679/">下一篇文章</a>中简单介绍 BPE 的原理与实现，看看 GPT 是如何将原始文本处理成模型输入层能够接受的 token 序列。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%A4%A7%E8%AF%AD%E8%A8%80%E6%A8%A1%E5%9E%8B%E5%AE%9E%E8%B7%B5/">大语言模型实践</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/">机器学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12634/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>策略梯度算法中梯度公式的推导</title>
      <link>https://blog.fyz666.xyz/blog/12633/</link>
      <guid>https://blog.fyz666.xyz/blog/12633/</guid>
      <pubDate>Mon, 09 Jun 2025 02:04:20 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;最近学习强化学习的策略梯度算法时，遇到其中策略梯度的计算部分，一些推导的细节在我所学习的视频中被一句话带过了，而这些推导过程本该十分重要，故在本文中简单整理一下。&lt;/p&gt;
&lt;p&gt;首先表述几个基本的符号：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;script</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>最近学习强化学习的策略梯度算法时，遇到其中策略梯度的计算部分，一些推导的细节在我所学习的视频中被一句话带过了，而这些推导过程本该十分重要，故在本文中简单整理一下。</p><p>首先表述几个基本的符号：</p><ul><li><script type="math/tex">S</script> 是全体状态的集合，<script type="math/tex">A</script> 是全体动作的集合。</li><li><script type="math/tex">\pi_\theta(*\mid s_t)</script> 表示以 <script type="math/tex">\theta</script> 为参数、在 <script type="math/tex">t</script> 时间步的状态 <script type="math/tex">s_t</script> 下的动作策略，是一个概率分布。<script type="math/tex">\theta</script> 即是本算法优化的参数。</li><li><script type="math/tex">R_t</script> 表示时间步 <script type="math/tex">t</script> 的即时奖励。</li><li><script type="math/tex">U_t=\sum_{i=t}^n\gamma^{i-t}R_i</script> 表示从时间步 <script type="math/tex">t</script> 开始的累计回报。</li><li><script type="math/tex">Q(s_t,a_t)=\mathbb{E}[U_t\mid s=s_t,a=a_t]</script> 表示在状态 <script type="math/tex">s_t</script> 时采取动作 <script type="math/tex">a_t</script> 时能获得的累计回报 <script type="math/tex">U_t</script> 的期望值。</li><li><script type="math/tex">V_{\pi_\theta}(s_t)=\mathbb{E}_{a_t\sim\pi_\theta(*\mid s_t)}[Q(s_t,a_t)]</script> 表示在状态 <script type="math/tex">s_t</script> 下，使用动作策略 <script type="math/tex">\pi_\theta</script> 时，<script type="math/tex">Q</script> 函数的期望值。</li><li><script type="math/tex">J(\theta)=\mathbb{E}_{s\in S}[V_{\pi_\theta}(s)]</script> 表示采取 以<script type="math/tex">\theta</script> 为参数的动作策略 <script type="math/tex">\pi_\theta</script> 时，能获得的所有状态下的回报的期望值。</li></ul><p>考虑到希望在策略 <script type="math/tex">\pi_\theta</script> 下获得尽可能高的回报，我们的优化目标自然是：</p><script type="math/tex; mode=display">\max_{\theta\in\Theta}J(\theta)</script><p>故需要计算 <script type="math/tex">\nabla_\theta J(\theta)</script>，计算此梯度时运用了一个称为「Log Derivative Trick」的技巧，下面推导一下这个梯度：</p><script type="math/tex; mode=display">\begin{aligned}\nabla_\theta J(\theta)&=\nabla_\theta\mathbb{E}_{s\in S}[V_{\pi_\theta}(s)]\\&=\nabla_\theta\mathbb{E}_{s\in S}\mathbb{E}_{a_t\sim\pi_\theta(*\mid s)}[Q(s,a_t)]\\&=\mathbb{E}_{s\in S}\nabla_\theta\mathbb{E}_{a_t\sim\pi_\theta(*\mid s)}[Q(s,a_t)]\\&=\mathbb{E}_{s\in S}\nabla_\theta\int_{a_t\in A}\pi_\theta(a_t\mid s)Q(s,a_t)\\&=\mathbb{E}_{s\in S}\int_{a_t\in A}\nabla_\theta\pi_\theta(a_t\mid s)Q(s,a_t)\end{aligned}</script><p>上面积分式中，仅有 <script type="math/tex">\pi_\theta(a_t\mid s)</script> 依赖于参数 <script type="math/tex">\theta</script>，故可以单独计算此项：</p><script type="math/tex; mode=display">\nabla_\theta\pi_\theta(a_t\mid s)</script><p>但这里梯度的外面还套着一个积分，算起来十分麻烦，因此使用了一个非常基本的对数导数等式：</p><script type="math/tex; mode=display">\frac{\partial(\log{f(x)})}{\partial x}=\frac{\partial(f(x))}{\partial x}\frac{1}{f(x)}</script><p>应用上式，有：</p><script type="math/tex; mode=display">\nabla_\theta\pi_\theta(a_t\mid s)=\nabla_\theta(\log{\pi_\theta(a_t\mid s)})\pi_\theta(a_t\mid s)</script><p>这么做的目的是在积分里面凑出来了一个概率分布：<script type="math/tex">\pi_\theta(*\mid s)</script>，从而可以将前面的积分转化为一个新的期望：</p><script type="math/tex; mode=display">\int_{a_t\in A}\nabla_\theta(\log{\pi_\theta(a_t\mid s)})\pi_\theta(a_t\mid s)Q(s,a_t)=\mathbb{E}_{a_t\sim\pi_\theta(*\mid s)}[\nabla_\theta(\log{\pi_\theta(a_t\mid s)})Q(s,a_t)]</script><p>则梯度可表示为：</p><script type="math/tex; mode=display">\nabla_\theta J(\theta)=\mathbb{E}_{s\in S}\mathbb{E}_{a_t\sim\pi_\theta(*\mid s)}[\nabla_\theta(\log{\pi_\theta(a_t\mid s)})Q(s,a_t)]</script><p>这样，就顺利的把梯度运算符移到了嵌套期望的内部，接下来的一个常用技巧是使用蒙特卡洛采样算法去估计 <script type="math/tex">Q(s,a_t)</script> 这一项的期望值。</p><script type="math/tex; mode=display">Q(s,a_t)\approx\sum_{i=t}^n\gamma^{i-t}R_i</script><p>如此一来，计算 <script type="math/tex">J(\theta)</script> 对 <script type="math/tex">\theta</script> 的梯度值就有了从代码上实现的可行性。</p><p>假设在某个策略 <script type="math/tex">\pi_\theta</script> 下收集了足够多的游戏数据：</p><script type="math/tex; mode=display">\{(s_t,a_t,R_t)\mid t=0,\dots,T\}</script><p>我们只需计算：</p><script type="math/tex; mode=display">J(\theta)\approx\sum_{t=0}^T(\sum_{i=t}^T\gamma^{i-t}R_i)\log{\pi_\theta(a_t\mid s_t)}</script><p>然后应用反向传播与梯度上升算法，即可优化 <script type="math/tex">\theta</script>。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0/">强化学习</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12633/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>MCP服务编写实践</title>
      <link>https://blog.fyz666.xyz/blog/12561/</link>
      <guid>https://blog.fyz666.xyz/blog/12561/</guid>
      <pubDate>Mon, 05 May 2025 15:07:48 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h2 id=&quot;动机&quot;&gt;&lt;a href=&quot;#动机&quot; class=&quot;headerlink&quot;</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h2 id="动机"><a href="#动机" class="headerlink" title="动机"></a>动机</h2><p>前文曾提到可以使用MCPM管理各种现成的MCP服务以使得大语言模型接入许多现成工具，那么自然会想要自己写一个适合自身需求的工具来让大模型调用，本文就来简单记录一下这个过程。</p><p>关于MCP服务的编写，GitHub上已经有了现成的仓库了：</p><a class="tag-Link" target="_blank" href=" https://github.com/modelcontextprotocol/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">modelcontextprotocol</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>作为一名练习时长两个两年半的Python练习生，我选择了其中的<a href="https://github.com/modelcontextprotocol/python-sdk">Python SDK</a>。</p><h2 id="配置环境"><a href="#配置环境" class="headerlink" title="配置环境"></a>配置环境</h2><p>这个Python SDK支持的Python版本为：&gt;=3.10，并且最好使用<code>uv</code>管理环境。</p><p>因此先安装<code>uv</code>：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash"><span class="token function">curl</span> <span class="token parameter variable">-LsSf</span> https://astral.sh/uv/install.sh <span class="token operator">|</span> <span class="token function">sh</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p><code>uv</code>安装完成后，初始化项目目录：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">uv init mcp_server<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>该操作会生成一些配置文件，以及一个虚拟环境，检查<code>.python-version</code>以及<code>pyproject.toml</code>里面Python的版本，需要&gt;=3.10，如不满足则手动修改，然后运行<code>uv venv</code>，即可重置环境。</p><p>接下来安装这个SDK：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">uv <span class="token function">add</span> <span class="token string">"mcp[cli]"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><h2 id="编写代码"><a href="#编写代码" class="headerlink" title="编写代码"></a>编写代码</h2><p>这里，我们让LLM来做一件简单的事：列出我桌面上的所有文件</p><p>编写<code>main.py</code>：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> os<span class="token keyword">from</span> mcp<span class="token punctuation">.</span>server<span class="token punctuation">.</span>fastmcp <span class="token keyword">import</span> FastMCPmcp <span class="token operator">=</span> FastMCP<span class="token punctuation">(</span><span class="token string">'windshadow-universe'</span><span class="token punctuation">)</span><span class="token decorator annotation punctuation">@mcp<span class="token punctuation">.</span>tool</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">async</span> <span class="token keyword">def</span> <span class="token function">list_desktop_files</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""列出桌面上的文件"""</span>    <span class="token keyword">return</span> os<span class="token punctuation">.</span>listdir<span class="token punctuation">(</span>os<span class="token punctuation">.</span>path<span class="token punctuation">.</span>expanduser<span class="token punctuation">(</span><span class="token string">'~/Desktop'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="配置Claude-Desktop"><a href="#配置Claude-Desktop" class="headerlink" title="配置Claude Desktop"></a>配置Claude Desktop</h2><p>在Claude Desktop的配置文件（<code>claude_desktop_config.json</code>）中添加一项：</p><pre class="line-numbers language-json" data-language="json"><code class="language-json"><span class="token punctuation">&#123;</span>  <span class="token property">"mcpServers"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>    ...    <span class="token property">"windshadow-universe"</span><span class="token operator">:</span> <span class="token punctuation">&#123;</span>      <span class="token property">"command"</span><span class="token operator">:</span> <span class="token string">"/absolute/path/to/uv"</span><span class="token punctuation">,</span>      <span class="token property">"args"</span><span class="token operator">:</span> <span class="token punctuation">[</span>        <span class="token string">"run"</span><span class="token punctuation">,</span>        <span class="token string">"--project"</span><span class="token punctuation">,</span>        <span class="token string">"/absolute/path/to/project/dir/mcp_server"</span><span class="token punctuation">,</span>        <span class="token string">"mcp"</span><span class="token punctuation">,</span>        <span class="token string">"run"</span><span class="token punctuation">,</span>        <span class="token string">"/absolute/path/to/project/dir/mcp_server/main.py"</span>      <span class="token punctuation">]</span>    <span class="token punctuation">&#125;</span>  <span class="token punctuation">&#125;</span><span class="token punctuation">&#125;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这里有三个绝对路径需要替换，分别是<code>uv</code>的绝对路径、通过<code>uv</code>初始化生成的项目目录<code>mcp_server</code>的绝对路径，以及前面创建的<code>main.py</code>文件的绝对路径。如使用相对路径则会失败（</p><p>接下来打开Claude Desktop，并让它列出我的桌面文件，Claude会作出回应调用我们刚刚写的函数：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/2f4a0278-2484-4935-bb62-a94509868826.png" alt="image-20250509144540460" style="zoom:50%;" /></p><hr><p>然而并不是很想给Claude打钱，求求ChatGPT Desktop赶紧更新MCP支持🙏🙏🙏。</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/02965303-13e0-4698-9713-4559fa9eb418.png" alt="image-20250505234832457" style="zoom:50%;" /></p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E7%9E%8E%E6%8D%A3%E9%BC%93%E7%BB%8F%E5%8E%86/">瞎捣鼓经历</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/mcp/">MCP</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12561/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>MCPM：让LLM调用你电脑上的一切工具</title>
      <link>https://blog.fyz666.xyz/blog/12471/</link>
      <guid>https://blog.fyz666.xyz/blog/12471/</guid>
      <pubDate>Sun, 20 Apr 2025 14:23:53 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;自从大语言模型（LLM）诞生以来，至今已有两年多的发展时间。如今市面上的各类 LLM</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>自从大语言模型（LLM）诞生以来，至今已有两年多的发展时间。如今市面上的各类 LLM 模型层出不穷，功能趋于同质化，用户的选择越来越多。然而，作为一名 LLM 重度用户，显然不满足于仅在聊天窗口中进行简单的对话式交互——更希望将 LLM 作为智能助手，融入到本地工具链中，提升工作效率并拓展应用边界。这些工具并不局限于诸如 VS Code、JetBrains IDE 等开发环境（这些IDE自身已经提供各种LLM的插件，以辅助代码编写），还包括终端命令行、文件管理器、数据库客户端、甚至是操作系统本身提供的原生命令与服务。将 LLM 与本地工具链深度集成，不仅能够实现自然语言驱动的自动化操作，还可以显著扩展模型的执行能力与实用性。为了实现这一目标，开放式的模型上下文协议（Model Context Protocol, MCP），为我们提供了一个标准化、可扩展的集成方案。</p><p>但如果为各种大模型、各种软件分别配置对应的MCP，则会非常的麻烦。很巧的是前两天，一位我关注的UP：严伯钧，发布了一个视频，提到他们团队开发了一个用于一站式管理各种软件MCP的开源软件：MCPM，我一看，欸🤓👆，这不就是我想要的管理工具吗？于是，火速对着文档一通操作，将MCPM安装了下，并试验性地通过它配置了一下Claude Desktop与iTerm2的集成，感觉非常好用，这里简单记录一下配置过程以及使用体验。</p><p>首先，本次配置在MacBook上进行，用到了下面两个仓库：</p><p>第一个自然就是 MCPM：</p><a class="tag-Link" target="_blank" href=" https://github.com/pathintegral-institute/mcpm.sh/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">mcpm.sh</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>另一个仓库：</p><a class="tag-Link" target="_blank" href=" https://github.com/ferrislucas/iterm-mcp">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">iterm-mcp</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>用于在大模型的会话中集成与iTerm2的交互。</p><h2 id="安装并简单配置-MCPM"><a href="#安装并简单配置-MCPM" class="headerlink" title="安装并简单配置 MCPM"></a>安装并简单配置 MCPM</h2><p>如仓库Readme文件所述，此软件有多种安装方法，你可以用<code>brew</code>、<code>pip</code>、<code>pipx</code>或<code>curl</code>进行安装，我在MacBook上安装，于是使用了<code>brew</code>：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">brew <span class="token function">install</span> mcpm<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>安装完成后，我们可以看到<code>mcpm</code>支持下面几种客户端：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">╰─➤  mcpm client <span class="token function">ls</span><span class="token punctuation">..</span>.                        Supported MCP Clients┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┓┃ Client Name                     ┃ Installation  ┃ Status ┃ Profile ┃┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━┩│ 5ire <span class="token punctuation">(</span>5ire<span class="token punctuation">)</span>                     │ Not installed │        │         ││ Claude Desktop <span class="token punctuation">(</span>claude-desktop<span class="token punctuation">)</span> │ Installed     │ ACTIVE │         ││ Cline <span class="token punctuation">(</span>cline<span class="token punctuation">)</span>                   │ Not installed │        │         ││ Continue <span class="token punctuation">(</span>continue<span class="token punctuation">)</span>             │ Installed     │        │         ││ Cursor <span class="token punctuation">(</span>cursor<span class="token punctuation">)</span>                 │ Not installed │        │         ││ Goose CLI <span class="token punctuation">(</span>goose-cli<span class="token punctuation">)</span>           │ Not installed │        │         ││ Roo Code <span class="token punctuation">(</span>roo-code<span class="token punctuation">)</span>             │ Not installed │        │         ││ Windsurf <span class="token punctuation">(</span>windsurf<span class="token punctuation">)</span>             │ Not installed │        │         │└─────────────────────────────────┴───────────────┴────────┴─────────┘<span class="token punctuation">..</span>.<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>Client大概就是指你想接入的大模型，可惜暂时还没有ChatGPT，不知是否支持自定义客户端的接入，后面再研究研究。这里我先接入了Claude Desktop。</p><a class="tag-Link" target="_blank" href=" https://claude.ai/download">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=claude.ai" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">Claude Desktop</div>            <div class="tag-link-sitename"> Claude</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>执行下面命令激活<code>claude-desktop</code>客户端：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mcpm client <span class="token builtin class-name">set</span> claude-desktop<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>接下来我们可以为此客户端配置服务端接入。然而服务去哪找呢？MCPM提供了一个网站罗列了支持的服务：</p><a class="tag-Link" target="_blank" href=" https://mcpm.sh/registry/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=mcpm.sh" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">MCP Server Registry</div>            <div class="tag-link-sitename"> mcpm.sh</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>也可通过命令<code>mcpm search</code>列出所有（或搜索感兴趣的）服务。</p><h2 id="集成与iTerm终端的交互"><a href="#集成与iTerm终端的交互" class="headerlink" title="集成与iTerm终端的交互"></a>集成与iTerm终端的交互</h2><p>这里我首先尝试了<code>iterm-mcp</code>:</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">╰─➤  mcpm search itermSearching <span class="token keyword">for</span> MCP servers matching <span class="token string">'iterm'</span>iterm-mcpFound <span class="token number">1</span> server<span class="token punctuation">(</span>s<span class="token punctuation">)</span> matching search criteria<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>一条命令即可：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">mcpm <span class="token function">add</span> iterm-mcp<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>我们可以查看这个服务的基本信息：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">╰─➤  mcpm info iterm-mcpShowing information <span class="token keyword">for</span> MCP server: iterm-mcpiTerm <span class="token punctuation">(</span>iterm-mcp<span class="token punctuation">)</span>Integration with iTerm2 terminal emulator <span class="token keyword">for</span> macOS, enabling LLMs to execute and monitorterminal commands.Server Information:Categories: System ToolsTags: iTerm, server, automationAuthor: ferrislucasLicense: MITURLs:Repository: https://github.com/ferrislucas/iterm-mcpHomepage: https://github.com/ferrislucas/iterm-mcpInstallation Details:npm: <span class="token function">npm</span> installationCommand: npx <span class="token parameter variable">-y</span> iterm-mcp<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>如此，服务就配置好了，接下来只需重启Claude Desktop，看到如下界面即安装成功：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/af0d82a3-edd5-496d-a45a-d8e05e60a1f7.png" alt="image-20250420232247306" style="zoom:50%;" /></p><p>图中已激活三个MCP工具，分别用来读取终端输出、往终端发送控制字符以及向终端写入/执行命令：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/eb03cea5-aae3-41de-ab0a-0e7a748b354b.png" alt="image-20250420232321547" style="zoom:50%;" /></p><div class="note danger simple"><p>在使用之前，需要注意几点：</p><blockquote><ul><li>The user is responsible for using the tool safely.</li><li>No built-in restrictions: iterm-mcp makes no attempt to evaluate the safety of commands that are executed.</li><li>Models can behave in unexpected ways. The user is expected to monitor activity and abort when appropriate.</li><li>For multi-step tasks, you may need to interrupt the model if it goes off track. Start with smaller, focused tasks until you’re familiar with how the model behaves.<br><cite><a href="https://github.com/ferrislucas/iterm-mcp?tab=readme-ov-file#safety-considerations">Safety Considerations of iterm-mcp</a></cite></li></ul></blockquote><p>总结：你得检查大模型给出的命令是否安全。</p></div><h2 id="使用示例"><a href="#使用示例" class="headerlink" title="使用示例"></a>使用示例</h2><p>这里，我尝试让Claude使用我的本地iTerm终端独立解决<a href="https://github.com/PKU-GeekGame/geekgame-4th/tree/master/official_writeup/tutorial-signin">GeekGame 2024的签到题</a>。</p><p>部分聊天内容如图：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/ef5f1a3b-f419-436f-b827-9022838859e8.png" alt="image-20250420235343383" style="zoom:50%;" /></p><p>对应的终端操作：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/dab2c403-f96e-4878-b10e-f92aa27b3898.png" alt="image-20250420235448952" style="zoom:50%;" /></p><p>最终，Claude在我没有给出任何提示的前提下，独立操作我本地的终端，找到了flag：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/ffe24038-a3b2-4b9b-b087-cd11ffab1c6d.png" alt="image-20250420235530069" style="zoom:50%;" /></p><p>对应的终端操作：</p><p><img src="https://blogfiles.oss.fyz666.xyz/png/c7df279f-01cc-4ec3-8495-530f6723d60d.png" alt="image-20250420235632533" style="zoom:50%;" /></p><p><del>以后打CTF只要把题目下载下来并交给Claude挂一晚上等着第二天收获flag就行了</del></p><hr><p>如此，我们便配置好了<code>MCPM</code>，并成功实现了其最基本的使用方法，使得LLM软件能够调用本地工具，展示了其在本地环境中调用iTerm2的方法。未来将尝试更多的相关服务，以提升生产力！</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E7%9E%8E%E6%8D%A3%E9%BC%93%E7%BB%8F%E5%8E%86/">瞎捣鼓经历</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/llm/">LLM</category>
      
      <category domain="https://blog.fyz666.xyz/tags/mcp/">MCP</category>
      
      <category domain="https://blog.fyz666.xyz/tags/mcpm/">MCPM</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12471/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Python随机数的背后：MT19937算法之——实战演练</title>
      <link>https://blog.fyz666.xyz/blog/12395/</link>
      <guid>https://blog.fyz666.xyz/blog/12395/</guid>
      <pubDate>Fri, 29 Nov 2024 11:42:29 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;本文是一道MT19937随机数预测的实战题，这道题是我2019年第一次参加Hackergame时遇到的，题目链接如下：&lt;/p&gt;
&lt;a class=&quot;tag-Link&quot; target=&quot;_blank&quot; href=&quot;</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>本文是一道MT19937随机数预测的实战题，这道题是我2019年第一次参加Hackergame时遇到的，题目链接如下：</p><a class="tag-Link" target="_blank" href=" https://github.com/ustclug/hackergame2019-writeups/blob/master/official/%E5%A4%A7%E6%95%B4%E6%95%B0%E5%88%86%E8%A7%A3%E9%94%A6%E6%A0%87%E8%B5%9B/">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">大整数分解锦标赛</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>当时我还是一个只会一点点Python的计算机小白，只做了几道最简单的题就结束了自己的赛程。自然，这道题我当时根本就没看，赛后看题解的计划也被我咕咕咕了，一直到最近才自己做了一遍。</p><hr><h2 id="题目分析"><a href="#题目分析" class="headerlink" title="题目分析"></a>题目分析</h2><p>题目服务端的源代码位于这个文件：<a href="https://github.com/ustclug/hackergame2019-writeups/blob/master/official/%E5%A4%A7%E6%95%B4%E6%95%B0%E5%88%86%E8%A7%A3%E9%94%A6%E6%A0%87%E8%B5%9B/src/factorme.py">factorme.py</a></p><p>在本地模拟题目环境：</p><pre class="line-numbers language-bash" data-language="bash"><code class="language-bash">socat TCP-LISTEN:9999,fork EXEC:<span class="token string">"python factorme.py"</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>我们可以在1分钟之内（<code>signal.alarm(60)</code>）与服务器进行两种交互：</p><ul><li><p>发送<code>H</code>，服务端会发给我们一段帮助文本</p></li><li><p>发送<code>B</code>，调用下面函数：</p></li></ul><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">begin</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">,</span> <span class="token number">1024</span><span class="token punctuation">,</span> <span class="token number">32</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> n <span class="token operator">=</span> generate<span class="token punctuation">(</span>i<span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"n ="</span><span class="token punctuation">,</span> n<span class="token punctuation">)</span>        input_p <span class="token operator">=</span> readnumber<span class="token punctuation">(</span><span class="token punctuation">)</span>        input_q <span class="token operator">=</span> readnumber<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">if</span> <span class="token builtin">sorted</span><span class="token punctuation">(</span><span class="token punctuation">[</span>p<span class="token punctuation">,</span> q<span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token builtin">sorted</span><span class="token punctuation">(</span><span class="token punctuation">[</span>input_p<span class="token punctuation">,</span> input_q<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">:</span>            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Wrong answer!"</span><span class="token punctuation">)</span>            exit<span class="token punctuation">(</span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"Good job!"</span><span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">open</span><span class="token punctuation">(</span><span class="token string">"flag"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我们要连续分解32个大整数，全对则获得flag。而显然后面非常大的整数我们是分解不出来的，<del>除非找到一个能口算大整数分解的少年班神童</del>，所以这题肯定不能真的去分解整数。</p><hr><p>查看大整数的生成逻辑：<br><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">generate</span><span class="token punctuation">(</span>bits<span class="token punctuation">)</span><span class="token punctuation">:</span>    p <span class="token operator">=</span> sympy<span class="token punctuation">.</span>randprime<span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">2</span> <span class="token operator">**</span> bits<span class="token punctuation">)</span>    q <span class="token operator">=</span> sympy<span class="token punctuation">.</span>randprime<span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">2</span> <span class="token operator">**</span> bits<span class="token punctuation">)</span>    <span class="token keyword">return</span> p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> p <span class="token operator">*</span> q<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></p><p>看似没有任何问题，但其实这个<code>sympy.randprime</code>是这样的：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> random <span class="token keyword">as</span> _randomrng <span class="token operator">=</span> _random<span class="token punctuation">.</span>Random<span class="token punctuation">(</span><span class="token punctuation">)</span>randint <span class="token operator">=</span> rng<span class="token punctuation">.</span>randint<span class="token keyword">def</span> <span class="token function">randprime</span><span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">if</span> a <span class="token operator">>=</span> b<span class="token punctuation">:</span>        <span class="token keyword">return</span>    a<span class="token punctuation">,</span> b <span class="token operator">=</span> <span class="token builtin">map</span><span class="token punctuation">(</span><span class="token builtin">int</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">)</span>    n <span class="token operator">=</span> randint<span class="token punctuation">(</span>a <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">,</span> b<span class="token punctuation">)</span>    p <span class="token operator">=</span> nextprime<span class="token punctuation">(</span>n<span class="token punctuation">)</span>    <span class="token keyword">if</span> p <span class="token operator">>=</span> b<span class="token punctuation">:</span>        p <span class="token operator">=</span> prevprime<span class="token punctuation">(</span>b<span class="token punctuation">)</span>    <span class="token keyword">if</span> p <span class="token operator">&lt;</span> a<span class="token punctuation">:</span>        <span class="token keyword">raise</span> ValueError<span class="token punctuation">(</span><span class="token string">"no primes exist in the specified range"</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> p<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>可见其调用了<code>random</code>来生成随机数。</p><p>注意到<code>help</code>函数会为我们生成一对<code>p</code>、<code>q</code>：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">help</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>    bits <span class="token operator">=</span> random<span class="token punctuation">.</span>randrange<span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">,</span> <span class="token number">1024</span><span class="token punctuation">)</span>    p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> n <span class="token operator">=</span> generate<span class="token punctuation">(</span>bits<span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"n ="</span><span class="token punctuation">,</span> n<span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"You should send me your answer in two lines like this:"</span><span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"p ="</span><span class="token punctuation">,</span> p<span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"q ="</span><span class="token punctuation">,</span> q<span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"In this case, p and q are random primes under %s bits."</span> <span class="token operator">%</span> bits<span class="token punctuation">)</span>    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>显然我们就可以通过连续调用<code>help</code>来获取非常多的伪随机数bit，这样理论上就可以把状态解出来了。</p><h2 id="随机数分析"><a href="#随机数分析" class="headerlink" title="随机数分析"></a>随机数分析</h2><p>首先我们需要分析一下题目里是如何调用随机数的。</p><p><code>sympy.randprime</code>调用了<code>randint</code>。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">randint</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> a<span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""Return random integer in range [a, b], including both end points.    """</span>    <span class="token keyword">return</span> self<span class="token punctuation">.</span>randrange<span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>randint</code>实际上调用了<code>randrange</code>。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">randrange</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> start<span class="token punctuation">,</span> stop<span class="token operator">=</span><span class="token boolean">None</span><span class="token punctuation">,</span> step<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">,</span> _int<span class="token operator">=</span><span class="token builtin">int</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token triple-quoted-string string">"""Choose a random item from range(start, stop[, step]).    This fixes the problem with randint() which includes the    endpoint; in Python this is usually not what you want.    """</span>    <span class="token comment"># This code is a bit messy to make it fast for the</span>    <span class="token comment"># common case while still doing adequate error checking.</span>    istart <span class="token operator">=</span> _int<span class="token punctuation">(</span>start<span class="token punctuation">)</span>    <span class="token keyword">if</span> istart <span class="token operator">!=</span> start<span class="token punctuation">:</span>        <span class="token keyword">raise</span> ValueError<span class="token punctuation">(</span><span class="token string">"non-integer arg 1 for randrange()"</span><span class="token punctuation">)</span>    <span class="token keyword">if</span> stop <span class="token keyword">is</span> <span class="token boolean">None</span><span class="token punctuation">:</span>        <span class="token keyword">if</span> istart <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>            <span class="token keyword">return</span> self<span class="token punctuation">.</span>_randbelow<span class="token punctuation">(</span>istart<span class="token punctuation">)</span>        <span class="token keyword">raise</span> ValueError<span class="token punctuation">(</span><span class="token string">"empty range for randrange()"</span><span class="token punctuation">)</span>    <span class="token comment"># stop argument supplied.</span>    istop <span class="token operator">=</span> _int<span class="token punctuation">(</span>stop<span class="token punctuation">)</span>    <span class="token keyword">if</span> istop <span class="token operator">!=</span> stop<span class="token punctuation">:</span>        <span class="token keyword">raise</span> ValueError<span class="token punctuation">(</span><span class="token string">"non-integer stop for randrange()"</span><span class="token punctuation">)</span>    width <span class="token operator">=</span> istop <span class="token operator">-</span> istart    <span class="token keyword">if</span> step <span class="token operator">==</span> <span class="token number">1</span> <span class="token keyword">and</span> width <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">:</span>        <span class="token keyword">return</span> istart <span class="token operator">+</span> self<span class="token punctuation">.</span>_randbelow<span class="token punctuation">(</span>width<span class="token punctuation">)</span>    <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>randrange</code>在题目的参数设置下，会在上面代码中的第25行返回，调用了<code>_randbelow</code>。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">_randbelow <span class="token operator">=</span> _randbelow_with_getrandbits<span class="token keyword">def</span> <span class="token function">_randbelow_with_getrandbits</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> n<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token string">"Return a random int in the range [0,n).  Raises ValueError if n==0."</span>    getrandbits <span class="token operator">=</span> self<span class="token punctuation">.</span>getrandbits    k <span class="token operator">=</span> n<span class="token punctuation">.</span>bit_length<span class="token punctuation">(</span><span class="token punctuation">)</span>  <span class="token comment"># don't use (n-1) here because n can be 1</span>    r <span class="token operator">=</span> getrandbits<span class="token punctuation">(</span>k<span class="token punctuation">)</span>          <span class="token comment"># 0 &lt;= r &lt; 2**k</span>    <span class="token keyword">while</span> r <span class="token operator">>=</span> n<span class="token punctuation">:</span>        r <span class="token operator">=</span> getrandbits<span class="token punctuation">(</span>k<span class="token punctuation">)</span>    <span class="token keyword">return</span> r<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><code>_randbelow</code>则调用了<code>getrandbits</code>。</p><p>因此，题目中生成大整数的函数「几乎」可以改写如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">generate</span><span class="token punctuation">(</span>bits<span class="token punctuation">)</span><span class="token punctuation">:</span>    p <span class="token operator">=</span> sympy<span class="token punctuation">.</span>nextprime<span class="token punctuation">(</span><span class="token number">2</span> <span class="token operator">+</span> random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span>bits<span class="token punctuation">)</span><span class="token punctuation">)</span>    q <span class="token operator">=</span> sympy<span class="token punctuation">.</span>nextprime<span class="token punctuation">(</span><span class="token number">2</span> <span class="token operator">+</span> random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span>bits<span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> p <span class="token operator">*</span> q<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>为什么是「几乎」呢？因为<code>_randbelow_with_getrandbits</code>内部随机到的<code>r</code>如果大于等于<code>n</code>，会重新随机直到满足条件。在题目的场景下，<code>_randbelow_with_getrandbits</code>函数传入的参数是<code>2 ** bits - 1</code>，这已经是<code>bits</code>位整数里最大的了，也就是说，这里<code>r = getrandbits(k)</code>得到的<code>r</code>，最大也就正好等于<code>n</code>，在这种低概率事件下，才会让随机数调用超过1次。故在大部分时候，每生成一个<code>bits</code>位的素数，只会调用一次<code>getrandbits(bits)</code>，遇到极少数倒霉情况的时候，我们多试一次即可。</p><hr><p>接下来我们需要知道当<code>bits</code>不等于32时，<code>getrandbits(bits)</code>是如何运作的。这里可以直接阅读Python随机数<a href="https://github.com/python/cpython/blob/10ecbadb799ddf3393d1fc80119a3db14724d381/Modules/_randommodule.c">源码</a>或通过观察法得出。这部分的代码我已经实现好了，位于<a href="https://gist.github.com/windshadow233/229ec53e67577bedb8965e652fdc7466#file-mt19937-py-L206">这里</a>。</p><p>简单来说就是先生成的比特会放在最终输出结果的低位，后生成的比特放在输出结果的高位，若最后需要的比特数不足32，则将生成的32bit从高位开始进行一个截断，其余的低位就被丢掉了，这意味着我们拿到的随机数其实丢失了一部分信息。</p><h2 id="从素数还原随机数"><a href="#从素数还原随机数" class="headerlink" title="从素数还原随机数"></a>从素数还原随机数</h2><p>其实我们拿到的并不是生成的随机数，而是「大于此随机数+2的最小素数」，这意味着我们在低位上也丢失了一些信息。我们先写一个函数将可以确定的信息求出来：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">known_prime_to_bits</span><span class="token punctuation">(</span>p<span class="token punctuation">,</span> bits<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token comment"># p - 3 >= num >= q - 2</span>    q <span class="token operator">=</span> prevprime<span class="token punctuation">(</span>p<span class="token punctuation">)</span>    diff <span class="token operator">=</span> <span class="token punctuation">(</span>p <span class="token operator">-</span> <span class="token number">3</span><span class="token punctuation">)</span> <span class="token operator">^</span> <span class="token punctuation">(</span>q <span class="token operator">-</span> <span class="token number">2</span><span class="token punctuation">)</span>    v <span class="token operator">=</span> <span class="token builtin">bin</span><span class="token punctuation">(</span>q <span class="token operator">-</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">.</span>zfill<span class="token punctuation">(</span>bits<span class="token punctuation">)</span>    unknown_length <span class="token operator">=</span> <span class="token builtin">len</span><span class="token punctuation">(</span><span class="token builtin">bin</span><span class="token punctuation">(</span>diff<span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> v<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token operator">-</span>unknown_length<span class="token punctuation">]</span> <span class="token operator">+</span> <span class="token string">"?"</span> <span class="token operator">*</span> unknown_length<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>我们用<code>?</code>表示不确定的位。</p><p>然后我们根据前面<code>getrandbits(bits)</code>的逻辑，反推出生成它的所有32bit：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">split_bits</span><span class="token punctuation">(</span>number_bin<span class="token punctuation">)</span><span class="token punctuation">:</span>    result <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>    <span class="token keyword">while</span> number_bin<span class="token punctuation">:</span>        result<span class="token punctuation">.</span>append<span class="token punctuation">(</span>number_bin<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">32</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        number_bin <span class="token operator">=</span> number_bin<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">32</span><span class="token punctuation">]</span>    <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">32</span><span class="token punctuation">:</span>        result<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">+=</span> <span class="token string">"?"</span> <span class="token operator">*</span> <span class="token punctuation">(</span><span class="token number">32</span> <span class="token operator">-</span> <span class="token builtin">len</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> result<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这里为了方便起见，直接操作二进制字符串（同时还能兼顾上面有问号的情况，真是太方便了！）</p><p>这样我们就通过一个生成的素数还原出一些已知的随机bit了。那么如何求解呢？</p><h2 id="还原随机数内部状态"><a href="#还原随机数内部状态" class="headerlink" title="还原随机数内部状态"></a>还原随机数内部状态</h2><p>调用现成的轮子即可。</p><a class="tag-Link" target="_blank" href=" https://github.com/icemonster/symbolic_mersenne_cracker">    <div class="tag-link-tips">引用站外地址，不保证站点的可用性和安全性</div>    <div class="tag-link-bottom">        <div class="tag-link-left">            <img src="https://www.google.com/s2/favicons?sz=256&domain=github.com" onerror="this.onerror=null;this.src='/images/default-link.png';" />        </div>        <div class="tag-link-right">            <div class="tag-link-title">SymRandCracker</div>            <div class="tag-link-sitename"> GitHub</div>        </div>        <i class="fa-solid fa-angle-right"></i>    </div>    </a><p>代码如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">predictor <span class="token operator">=</span> Untwister<span class="token punctuation">(</span><span class="token punctuation">)</span>known_bits <span class="token operator">=</span> <span class="token number">0</span><span class="token keyword">while</span> known_bits <span class="token operator">&lt;</span> <span class="token number">37000</span><span class="token punctuation">:</span>    p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> bits <span class="token operator">=</span> <span class="token builtin">help</span><span class="token punctuation">(</span><span class="token punctuation">)</span>    vp <span class="token operator">=</span> known_prime_to_bits<span class="token punctuation">(</span>p<span class="token punctuation">,</span> bits<span class="token punctuation">)</span>    vq <span class="token operator">=</span> known_prime_to_bits<span class="token punctuation">(</span>q<span class="token punctuation">,</span> bits<span class="token punctuation">)</span>    split_p <span class="token operator">=</span> split_bits<span class="token punctuation">(</span>vp<span class="token punctuation">)</span>    split_q <span class="token operator">=</span> split_bits<span class="token punctuation">(</span>vq<span class="token punctuation">)</span>    <span class="token keyword">for</span> i <span class="token keyword">in</span> split_p <span class="token operator">+</span> split_q<span class="token punctuation">:</span>        predictor<span class="token punctuation">.</span>submit<span class="token punctuation">(</span>i<span class="token punctuation">)</span>        known_bits <span class="token operator">+=</span> <span class="token number">32</span> <span class="token operator">-</span> i<span class="token punctuation">.</span>count<span class="token punctuation">(</span><span class="token string">"?"</span><span class="token punctuation">)</span>rng <span class="token operator">=</span> predictor<span class="token punctuation">.</span>get_random<span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>试了一些数，发现<code>known_bits</code>达到<code>37000</code>时，差不多就可以稳定把所有内部状态全还原了。我的电脑求解时间在40-50秒左右，基本能在1分钟内拿到flag。</p><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> pwn <span class="token keyword">import</span> remote<span class="token keyword">import</span> re<span class="token keyword">from</span> sympy <span class="token keyword">import</span> prevprime<span class="token punctuation">,</span> nextprime<span class="token keyword">from</span> SymRandCracker <span class="token keyword">import</span> Untwisterr <span class="token operator">=</span> remote<span class="token punctuation">(</span><span class="token string">'127.0.0.1'</span><span class="token punctuation">,</span> <span class="token number">9999</span><span class="token punctuation">)</span>r<span class="token punctuation">.</span>recvuntil<span class="token punctuation">(</span><span class="token string">b'[E]xit? '</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">help</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    r<span class="token punctuation">.</span>sendline<span class="token punctuation">(</span><span class="token string">b'H'</span><span class="token punctuation">)</span>    data <span class="token operator">=</span> r<span class="token punctuation">.</span>recvuntil<span class="token punctuation">(</span><span class="token string">b'[E]xit? '</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span>    p <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>re<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string">r'p = (\d+)'</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">.</span>group<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    q <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>re<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string">r'q = (\d+)'</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">.</span>group<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    bits <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>re<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string">r'under (\d+) bits'</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">.</span>group<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> bits<span class="token keyword">def</span> <span class="token function">begin</span><span class="token punctuation">(</span>rng<span class="token punctuation">)</span><span class="token punctuation">:</span>    r<span class="token punctuation">.</span>sendline<span class="token punctuation">(</span><span class="token string">b'B'</span><span class="token punctuation">)</span>    <span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">,</span> <span class="token number">1024</span><span class="token punctuation">,</span> <span class="token number">32</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        r<span class="token punctuation">.</span>recv<span class="token punctuation">(</span><span class="token punctuation">)</span>        p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> _ <span class="token operator">=</span> generate_by_rng<span class="token punctuation">(</span>rng<span class="token punctuation">,</span> i<span class="token punctuation">)</span>        r<span class="token punctuation">.</span>sendlines<span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token builtin">str</span><span class="token punctuation">(</span>p<span class="token punctuation">)</span><span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token builtin">str</span><span class="token punctuation">(</span>q<span class="token punctuation">)</span><span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        <span class="token keyword">print</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>recvline<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>recvline<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">def</span> <span class="token function">randprime</span><span class="token punctuation">(</span>rng<span class="token punctuation">,</span> a<span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">if</span> a <span class="token operator">>=</span> b<span class="token punctuation">:</span>        <span class="token keyword">return</span>    a<span class="token punctuation">,</span> b <span class="token operator">=</span> <span class="token builtin">map</span><span class="token punctuation">(</span><span class="token builtin">int</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>a<span class="token punctuation">,</span> b<span class="token punctuation">)</span><span class="token punctuation">)</span>    n <span class="token operator">=</span> rng<span class="token punctuation">.</span>randint<span class="token punctuation">(</span>a <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">,</span> b<span class="token punctuation">)</span>    p <span class="token operator">=</span> nextprime<span class="token punctuation">(</span>n<span class="token punctuation">)</span>    <span class="token keyword">if</span> p <span class="token operator">>=</span> b<span class="token punctuation">:</span>        p <span class="token operator">=</span> prevprime<span class="token punctuation">(</span>b<span class="token punctuation">)</span>    <span class="token keyword">if</span> p <span class="token operator">&lt;</span> a<span class="token punctuation">:</span>        <span class="token keyword">raise</span> ValueError<span class="token punctuation">(</span><span class="token string">"no primes exist in the specified range"</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> p<span class="token keyword">def</span> <span class="token function">generate_by_rng</span><span class="token punctuation">(</span>rng<span class="token punctuation">,</span> bits<span class="token punctuation">)</span><span class="token punctuation">:</span>    p <span class="token operator">=</span> randprime<span class="token punctuation">(</span>rng<span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">2</span> <span class="token operator">**</span> bits<span class="token punctuation">)</span>    q <span class="token operator">=</span> randprime<span class="token punctuation">(</span>rng<span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">2</span> <span class="token operator">**</span> bits<span class="token punctuation">)</span>    <span class="token keyword">return</span> p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> p <span class="token operator">*</span> q<span class="token keyword">def</span> <span class="token function">split_bits</span><span class="token punctuation">(</span>number_bin<span class="token punctuation">)</span><span class="token punctuation">:</span>    result <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>    <span class="token keyword">while</span> number_bin<span class="token punctuation">:</span>        result<span class="token punctuation">.</span>append<span class="token punctuation">(</span>number_bin<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">32</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        number_bin <span class="token operator">=</span> number_bin<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token operator">-</span><span class="token number">32</span><span class="token punctuation">]</span>    <span class="token keyword">if</span> <span class="token builtin">len</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">&lt;</span> <span class="token number">32</span><span class="token punctuation">:</span>        result<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">+=</span> <span class="token string">"?"</span> <span class="token operator">*</span> <span class="token punctuation">(</span><span class="token number">32</span> <span class="token operator">-</span> <span class="token builtin">len</span><span class="token punctuation">(</span>result<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> result<span class="token keyword">def</span> <span class="token function">known_prime_to_bits</span><span class="token punctuation">(</span>p<span class="token punctuation">,</span> bits<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token comment"># p - 3 >= num >= q - 2</span>    q <span class="token operator">=</span> prevprime<span class="token punctuation">(</span>p<span class="token punctuation">)</span>    diff <span class="token operator">=</span> <span class="token punctuation">(</span>p <span class="token operator">-</span> <span class="token number">3</span><span class="token punctuation">)</span> <span class="token operator">^</span> <span class="token punctuation">(</span>q <span class="token operator">-</span> <span class="token number">2</span><span class="token punctuation">)</span>    v <span class="token operator">=</span> <span class="token builtin">bin</span><span class="token punctuation">(</span>q <span class="token operator">-</span> <span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">.</span>zfill<span class="token punctuation">(</span>bits<span class="token punctuation">)</span>    unknown_length <span class="token operator">=</span> <span class="token builtin">len</span><span class="token punctuation">(</span><span class="token builtin">bin</span><span class="token punctuation">(</span>diff<span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">)</span>    <span class="token keyword">return</span> v<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token operator">-</span>unknown_length<span class="token punctuation">]</span> <span class="token operator">+</span> <span class="token string">"?"</span> <span class="token operator">*</span> unknown_lengthpredictor <span class="token operator">=</span> Untwister<span class="token punctuation">(</span><span class="token punctuation">)</span>known_bits <span class="token operator">=</span> <span class="token number">0</span><span class="token keyword">while</span> known_bits <span class="token operator">&lt;</span> <span class="token number">37000</span><span class="token punctuation">:</span>    p<span class="token punctuation">,</span> q<span class="token punctuation">,</span> bits <span class="token operator">=</span> <span class="token builtin">help</span><span class="token punctuation">(</span><span class="token punctuation">)</span>    vp <span class="token operator">=</span> known_prime_to_bits<span class="token punctuation">(</span>p<span class="token punctuation">,</span> bits<span class="token punctuation">)</span>    vq <span class="token operator">=</span> known_prime_to_bits<span class="token punctuation">(</span>q<span class="token punctuation">,</span> bits<span class="token punctuation">)</span>    split_p <span class="token operator">=</span> split_bits<span class="token punctuation">(</span>vp<span class="token punctuation">)</span>    split_q <span class="token operator">=</span> split_bits<span class="token punctuation">(</span>vq<span class="token punctuation">)</span>    <span class="token keyword">for</span> i <span class="token keyword">in</span> split_p <span class="token operator">+</span> split_q<span class="token punctuation">:</span>        predictor<span class="token punctuation">.</span>submit<span class="token punctuation">(</span>i<span class="token punctuation">)</span>        known_bits <span class="token operator">+=</span> <span class="token number">32</span> <span class="token operator">-</span> i<span class="token punctuation">.</span>count<span class="token punctuation">(</span><span class="token string">"?"</span><span class="token punctuation">)</span>rng <span class="token operator">=</span> predictor<span class="token punctuation">.</span>get_random<span class="token punctuation">(</span><span class="token punctuation">)</span>begin<span class="token punctuation">(</span>rng<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/">密码学</category>
      
      <category domain="https://blog.fyz666.xyz/tags/prng/">PRNG</category>
      
      <category domain="https://blog.fyz666.xyz/tags/mt19937/">MT19937</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12395/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Python随机数的背后：MT19937算法之——小试牛刀</title>
      <link>https://blog.fyz666.xyz/blog/12343/</link>
      <guid>https://blog.fyz666.xyz/blog/12343/</guid>
      <pubDate>Wed, 20 Nov 2024 21:27:45 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;本文为几道MT19937预测题的题解。这些题都非常基础+典型，十分适合入门。&lt;/p&gt;
&lt;p&gt;本文用到的&lt;code&gt;mt19937&lt;/code&gt;来自&lt;a</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>本文为几道MT19937预测题的题解。这些题都非常基础+典型，十分适合入门。</p><p>本文用到的<code>mt19937</code>来自<a href="https://gist.github.com/windshadow233/229ec53e67577bedb8965e652fdc7466">此gist</a>。</p><h2 id="第一题"><a href="#第一题" class="headerlink" title="第一题"></a>第一题</h2><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment">#!/usr/bin/env python3</span><span class="token keyword">import</span> random<span class="token keyword">for</span> _ <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">624</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span>random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">32</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">if</span> <span class="token builtin">input</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token builtin">str</span><span class="token punctuation">(</span>random<span class="token punctuation">.</span>random<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">open</span><span class="token punctuation">(</span><span class="token string">"flag"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>非常简单的预测，给了连续624个32bit随机数，只需把它们依次输入预测器，就能恢复出完整的内部状态。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> tqdm<span class="token keyword">from</span> mt19937 <span class="token keyword">import</span> MT19937Predictor<span class="token keyword">from</span> pwn <span class="token keyword">import</span> remoter <span class="token operator">=</span> remote<span class="token punctuation">(</span>HOST<span class="token punctuation">,</span> PORT<span class="token punctuation">)</span>predictor <span class="token operator">=</span> MT19937Predictor<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">for</span> _ <span class="token keyword">in</span> tqdm<span class="token punctuation">.</span>tqdm<span class="token punctuation">(</span><span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">624</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    data <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>recvline<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    predictor<span class="token punctuation">.</span>setrand_int32<span class="token punctuation">(</span>data<span class="token punctuation">)</span>r<span class="token punctuation">.</span>sendline<span class="token punctuation">(</span><span class="token builtin">str</span><span class="token punctuation">(</span>predictor<span class="token punctuation">.</span>random<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">print</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>recv<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="第二题"><a href="#第二题" class="headerlink" title="第二题"></a>第二题</h2><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment">#!/usr/bin/env python3</span><span class="token keyword">import</span> randomnumber <span class="token operator">=</span> random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">32</span><span class="token punctuation">)</span><span class="token keyword">for</span> _ <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">624</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span>random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">32</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">if</span> <span class="token builtin">input</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token builtin">str</span><span class="token punctuation">(</span>number<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">open</span><span class="token punctuation">(</span><span class="token string">"flag"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>同样先通过连续的624个32bit随机数恢复出内部状态，然后我们往回倒625次迭代，即可恢复出最前面的那个随机数产生之前的状态。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> tqdm<span class="token keyword">from</span> pwn <span class="token keyword">import</span> remote<span class="token keyword">from</span> mt19937 <span class="token keyword">import</span> MT19937Predictorr <span class="token operator">=</span> remote<span class="token punctuation">(</span>HOST<span class="token punctuation">,</span> PORT<span class="token punctuation">)</span>predictor <span class="token operator">=</span> MT19937Predictor<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">for</span> _ <span class="token keyword">in</span> tqdm<span class="token punctuation">.</span>tqdm<span class="token punctuation">(</span><span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">624</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    data <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>recvline<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>    predictor<span class="token punctuation">.</span>setrand_int32<span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">625</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    predictor<span class="token punctuation">.</span>unextract_number<span class="token punctuation">(</span><span class="token punctuation">)</span>ans <span class="token operator">=</span> predictor<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">32</span><span class="token punctuation">)</span>r<span class="token punctuation">.</span>sendline<span class="token punctuation">(</span><span class="token builtin">str</span><span class="token punctuation">(</span>ans<span class="token punctuation">)</span><span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">print</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>recv<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="第三题"><a href="#第三题" class="headerlink" title="第三题"></a>第三题</h2><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token comment">#!/usr/bin/env python3</span><span class="token keyword">import</span> random<span class="token keyword">for</span> _ <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">19938</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span>random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">if</span> <span class="token builtin">input</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">==</span> <span class="token builtin">str</span><span class="token punctuation">(</span>random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">64</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token builtin">open</span><span class="token punctuation">(</span><span class="token string">"flag"</span><span class="token punctuation">)</span><span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这一题就稍微难一些了，虽然看上去是连续生成了19938个bit，但因为这个算法最小的生成单位是32bit，每次调用<code>getrandbits(1)</code>时，其实内部先会生成一个32bit的随机数，然后取其最高位返回。不过经过前面的一些分析，我们很容易发现一个关键点：即MT19937算法每次迭代产生的随机数的每一个bit，其实都是它内部状态某些bit的异或得来的，这说明取出随机数的操作是一个$GF(2)$​上的线性运算，如果我们拿到了19937个bit，即使它们是不连续的，只要我们能够分析出一些前后bit的对应关系，便可以通过求解线性方程组来解出状态。解方程神器自然少不了<code>z3</code>，我也找到了一个利用<code>z3</code>来恢复MT19937内部状态的项目：<a href="https://github.com/icemonster/symbolic_mersenne_cracker">SymRandCracker</a></p><p>可惜，这道题的已知bit过于分散，每32个bit才知道其中一个的值，如果全输入符号求解器，复杂度会拉满，不过好在我们知道每次给的bit的位置（MSB）。</p><p>我们设初始状态下<code>random</code>的状态向量为$\vec{a}$，则$\vec{a}$相当于一个$624\times32=19968$维的向量。再假设此状态下连续生成的624个32bit（一共也是19968个bit）组合成的向量为$\vec{b}$，则存在一个$19968\times19968$的矩阵$M_1$满足：</p><script type="math/tex; mode=display">M_1\vec{a}=\vec{b}\pmod{2}</script><p>不妨大胆一点，假设从内部状态$\vec{a}$开始连续生成的19938个32bit的MSB（一共19938个bit）组合成的向量为$\vec{c}$，是否存在一个$19938\times19968$的矩阵$M_2$满足：</p><script type="math/tex; mode=display">M_2\vec{a}=\vec{c}\pmod{2}\quad?</script><p>答案是肯定的。</p><p>并且我们还能在本地预生成好这个矩阵，然后从服务器拿到19938个MSB（$\vec{c}$）后直接解出随机数发生器最开始的内部状态$\vec{a}$​。</p><p>这里使用Sage编写代码：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> random<span class="token keyword">import</span> tqdm<span class="token keyword">import</span> oslength <span class="token operator">=</span> <span class="token number">624</span> <span class="token operator">*</span> <span class="token number">32</span><span class="token keyword">def</span> <span class="token function">generate_state</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    state <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token builtin">int</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token operator">*</span><span class="token number">624</span>    i <span class="token operator">=</span> <span class="token number">0</span>    <span class="token keyword">while</span> i<span class="token operator">&lt;</span>length<span class="token punctuation">:</span>        idx <span class="token operator">=</span> i<span class="token operator">//</span><span class="token number">32</span>        expont <span class="token operator">=</span> i<span class="token operator">%</span><span class="token number">32</span>        state<span class="token punctuation">[</span>idx<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token operator">&lt;&lt;</span><span class="token punctuation">(</span><span class="token number">31</span><span class="token operator">-</span>expont<span class="token punctuation">)</span><span class="token punctuation">)</span>        s <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">,</span><span class="token builtin">tuple</span><span class="token punctuation">(</span>state<span class="token operator">+</span><span class="token punctuation">[</span><span class="token builtin">int</span><span class="token punctuation">(</span><span class="token number">624</span><span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span><span class="token boolean">None</span><span class="token punctuation">)</span>        <span class="token keyword">yield</span> s        state<span class="token punctuation">[</span>idx<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span>        i <span class="token operator">+=</span> <span class="token number">1</span>        <span class="token keyword">def</span> <span class="token function">get_row</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    gs <span class="token operator">=</span> generate_state<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>length<span class="token punctuation">)</span><span class="token punctuation">:</span>        s <span class="token operator">=</span> <span class="token builtin">next</span><span class="token punctuation">(</span>gs<span class="token punctuation">)</span>        random<span class="token punctuation">.</span>setstate<span class="token punctuation">(</span>s<span class="token punctuation">)</span>        row <span class="token operator">=</span> vector<span class="token punctuation">(</span>GF<span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span> <span class="token keyword">for</span> j <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>length<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">)</span>        <span class="token keyword">yield</span> row<span class="token keyword">def</span> <span class="token function">build_matrix</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    b <span class="token operator">=</span> matrix<span class="token punctuation">(</span>GF<span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span>length<span class="token punctuation">,</span>length<span class="token punctuation">)</span>    rg <span class="token operator">=</span> get_row<span class="token punctuation">(</span><span class="token punctuation">)</span>    <span class="token keyword">for</span> i <span class="token keyword">in</span> tqdm<span class="token punctuation">.</span>tqdm<span class="token punctuation">(</span><span class="token builtin">range</span><span class="token punctuation">(</span>length<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">:</span>        b<span class="token punctuation">[</span>i<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token builtin">next</span><span class="token punctuation">(</span>rg<span class="token punctuation">)</span>    <span class="token keyword">return</span> b<span class="token keyword">if</span> <span class="token keyword">not</span> os<span class="token punctuation">.</span>path<span class="token punctuation">.</span>exists<span class="token punctuation">(</span><span class="token string">'Matrix.sobj'</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    b <span class="token operator">=</span> build_matrix<span class="token punctuation">(</span><span class="token punctuation">)</span>    b<span class="token punctuation">.</span>save<span class="token punctuation">(</span><span class="token string">"Matrix.sobj"</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>思路是遍历所有仅一个bit为1，其他bit为0的状态（共19968个），每次将此状态赋值给随机数发生器，然后让它根据此状态连续生成19968个32bit，我们每32个bit取出其MSB，组合为矩阵的一行。将每个状态对应的行拼起来，组合得到一个$19968\times19968$的矩阵。由于这个生成过程长达20分钟，故一次生成结束就存在本地，方便后面直接读取。</p><p>求解时由于我们只有19938个输入，就将矩阵进行一个截断：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python">T <span class="token operator">=</span> load<span class="token punctuation">(</span><span class="token string">'Matrix'</span><span class="token punctuation">)</span>T <span class="token operator">=</span> T<span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">,</span> <span class="token punctuation">:</span><span class="token number">19938</span><span class="token punctuation">]</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>读入服务器发来的数据，然后调用Sage的<code>solve_left</code>，解一下方程即可：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">from</span> pwn <span class="token keyword">import</span> remoter <span class="token operator">=</span> remote<span class="token punctuation">(</span>HOST<span class="token punctuation">,</span> PORT<span class="token punctuation">)</span>leak <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token builtin">int</span><span class="token punctuation">(</span>r<span class="token punctuation">.</span>recvline<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>strip<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">for</span> i <span class="token keyword">in</span> tqdm<span class="token punctuation">.</span>tqdm<span class="token punctuation">(</span><span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">19938</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">]</span>leak <span class="token operator">=</span> vector<span class="token punctuation">(</span>GF<span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">,</span> leak<span class="token punctuation">)</span>x <span class="token operator">=</span> T<span class="token punctuation">.</span>solve_left<span class="token punctuation">(</span>leak<span class="token punctuation">)</span>x <span class="token operator">=</span> <span class="token string">''</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token builtin">str</span><span class="token punctuation">(</span>i<span class="token punctuation">)</span> <span class="token keyword">for</span> i <span class="token keyword">in</span> x<span class="token punctuation">]</span><span class="token punctuation">)</span>state <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">624</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    tmp <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span>x<span class="token punctuation">[</span>i <span class="token operator">*</span> <span class="token number">32</span><span class="token punctuation">:</span><span class="token punctuation">(</span>i <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token operator">*</span> <span class="token number">32</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token number">2</span><span class="token punctuation">)</span>    state<span class="token punctuation">.</span>append<span class="token punctuation">(</span>tmp<span class="token punctuation">)</span>random<span class="token punctuation">.</span>setstate<span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token builtin">tuple</span><span class="token punctuation">(</span>state <span class="token operator">+</span> <span class="token punctuation">[</span><span class="token number">624</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token boolean">None</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">19938</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span>r<span class="token punctuation">.</span>sendline<span class="token punctuation">(</span><span class="token builtin">str</span><span class="token punctuation">(</span>random<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">64</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">.</span>encode<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span>r<span class="token punctuation">.</span>recv<span class="token punctuation">(</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><a href="/blog/12395/">下一篇文章</a>中笔者将结合实际案例，带来一道实战题的题解。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/">密码学</category>
      
      <category domain="https://blog.fyz666.xyz/tags/prng/">PRNG</category>
      
      <category domain="https://blog.fyz666.xyz/tags/mt19937/">MT19937</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12343/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Python随机数的背后：MT19937算法之——状态恢复</title>
      <link>https://blog.fyz666.xyz/blog/12317/</link>
      <guid>https://blog.fyz666.xyz/blog/12317/</guid>
      <pubDate>Wed, 20 Nov 2024 20:41:57 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;&lt;a</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p><a href="/blog/12283/">前一篇文章</a>中，我们已经逆向了Python中的随机算法，在本文中，我们将在前文的基础上对MT19937的状态数组进行恢复，从而达到预测随机数的效果。</p><hr><p>根据前文的分析，我们知道一旦还原了随机数发生器完整的内部状态，就相当于复刻了一个完全相同的随机数发生器，也就能预测后面的随机数了，并且我们还知道，每次提取随机数时，是取出某个下标位置的状态向量并将其进行<code>tempering</code>运算，最终输出。</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">extract_number</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">if</span> self<span class="token punctuation">.</span>_mti <span class="token operator">>=</span> self<span class="token punctuation">.</span>N<span class="token punctuation">:</span>        <span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>N<span class="token punctuation">)</span><span class="token punctuation">:</span>            self<span class="token punctuation">.</span>twist<span class="token punctuation">(</span>i<span class="token punctuation">)</span>        self<span class="token punctuation">.</span>_mti <span class="token operator">=</span> <span class="token number">0</span>    y <span class="token operator">=</span> self<span class="token punctuation">.</span>_mt<span class="token punctuation">[</span>self<span class="token punctuation">.</span>_mti<span class="token punctuation">]</span>    y <span class="token operator">=</span> self<span class="token punctuation">.</span>tempering<span class="token punctuation">(</span>y<span class="token punctuation">)</span>    self<span class="token punctuation">.</span>_mti <span class="token operator">+=</span> <span class="token number">1</span>    <span class="token keyword">return</span> _int32<span class="token punctuation">(</span>y<span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>当下标运转一整轮（即624次）时，我们相当于把每个状态向量都提取了一次，这说明连续提取出来的624个32bit随机数是与624个内部状态向量一一对应的。</p><p>反过来，我们就可以通过提取出来的连续624个32bit来生成内部状态向量数组：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">def</span> <span class="token function">setrand_int32</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> y<span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">assert</span> <span class="token number">0</span> <span class="token operator">&lt;=</span> y <span class="token operator">&lt;</span> <span class="token number">2</span> <span class="token operator">**</span> <span class="token number">32</span>    self<span class="token punctuation">.</span>_mti <span class="token operator">%=</span> self<span class="token punctuation">.</span>N    self<span class="token punctuation">.</span>_mt<span class="token punctuation">[</span>self<span class="token punctuation">.</span>_mti<span class="token punctuation">]</span> <span class="token operator">=</span> self<span class="token punctuation">.</span>untempering<span class="token punctuation">(</span>y<span class="token punctuation">)</span>    self<span class="token punctuation">.</span>_mti <span class="token operator">+=</span> <span class="token number">1</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>至此，我们便可以预测Python的伪随机数了。如下：</p><pre class="line-numbers language-python" data-language="python"><code class="language-python"><span class="token keyword">import</span> random<span class="token keyword">from</span> mt19937 <span class="token keyword">import</span> MT19937Predictorpredictor <span class="token operator">=</span> MT19937Predictor<span class="token punctuation">(</span><span class="token punctuation">)</span>prng <span class="token operator">=</span> random<span class="token punctuation">.</span>Random<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token keyword">for</span> i <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">624</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    predictor<span class="token punctuation">.</span>setrand_int32<span class="token punctuation">(</span>prng<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">32</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">for</span> _ <span class="token keyword">in</span> <span class="token builtin">range</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">:</span>    <span class="token keyword">assert</span> predictor<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">64</span><span class="token punctuation">)</span> <span class="token operator">==</span> prng<span class="token punctuation">.</span>getrandbits<span class="token punctuation">(</span><span class="token number">64</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>其中，<code>mt19937</code>见<a href="https://gist.github.com/windshadow233/229ec53e67577bedb8965e652fdc7466">此gist</a>。</p><hr><p><a href="/blog/12343/">下一篇文章</a>中笔者将写一下几道MT19937相关题目的题解。</p>]]></content:encoded>
      
      
      <category domain="https://blog.fyz666.xyz/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/">学习笔记</category>
      
      
      <category domain="https://blog.fyz666.xyz/tags/%E5%AF%86%E7%A0%81%E5%AD%A6/">密码学</category>
      
      <category domain="https://blog.fyz666.xyz/tags/prng/">PRNG</category>
      
      <category domain="https://blog.fyz666.xyz/tags/mt19937/">MT19937</category>
      
      
      <comments>https://blog.fyz666.xyz/blog/12317/#disqus_thread</comments>
      
    </item>
    
  </channel>
</rss>
