<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://sut-gc.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sut-gc.github.io/" rel="alternate" type="text/html" hreflang="zh" /><updated>2026-04-08T07:51:40+00:00</updated><id>https://sut-gc.github.io/feed.xml</id><title type="html">SutGC‘s Blog</title><subtitle>I promise if you let me, I&apos;ll love you like the movies</subtitle><author><name>gc</name><email>gouchao2008@qq.com</email></author><entry><title type="html">没有标准数据集，如何客观评测算法效果？</title><link href="https://sut-gc.github.io/blog/2026/04/08/%E6%B2%A1%E6%9C%89%E6%A0%87%E5%87%86%E6%95%B0%E6%8D%AE%E9%9B%86%E5%A6%82%E4%BD%95%E5%AE%A2%E8%A7%82%E8%AF%84%E6%B5%8B%E7%AE%97%E6%B3%95%E6%95%88%E6%9E%9C/" rel="alternate" type="text/html" title="没有标准数据集，如何客观评测算法效果？" /><published>2026-04-08T00:00:00+00:00</published><updated>2026-04-08T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/04/08/%E6%B2%A1%E6%9C%89%E6%A0%87%E5%87%86%E6%95%B0%E6%8D%AE%E9%9B%86%E5%A6%82%E4%BD%95%E5%AE%A2%E8%A7%82%E8%AF%84%E6%B5%8B%E7%AE%97%E6%B3%95%E6%95%88%E6%9E%9C</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/04/08/%E6%B2%A1%E6%9C%89%E6%A0%87%E5%87%86%E6%95%B0%E6%8D%AE%E9%9B%86%E5%A6%82%E4%BD%95%E5%AE%A2%E8%A7%82%E8%AF%84%E6%B5%8B%E7%AE%97%E6%B3%95%E6%95%88%E6%9E%9C/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-一问题从哪里来" id="markdown-toc-heading-一问题从哪里来">一、问题从哪里来</a></li>
  <li><a href="#heading-二业界是怎么解决这类问题的" id="markdown-toc-heading-二业界是怎么解决这类问题的">二、业界是怎么解决这类问题的</a>    <ul>
      <li><a href="#heading-1-llm-as-judge让-ai-当裁判" id="markdown-toc-heading-1-llm-as-judge让-ai-当裁判">1. LLM-as-Judge：让 AI 当裁判</a></li>
      <li><a href="#heading-2-weak-supervision让不完美的规则联合投票" id="markdown-toc-heading-2-weak-supervision让不完美的规则联合投票">2. Weak Supervision：让不完美的规则联合投票</a></li>
      <li><a href="#heading-3-active-learning让模型告诉你标哪些数据最值钱" id="markdown-toc-heading-3-active-learning让模型告诉你标哪些数据最值钱">3. Active Learning：让模型告诉你标哪些数据最值钱</a></li>
      <li><a href="#heading-4-elo-评分不需要标准答案的排行榜" id="markdown-toc-heading-4-elo-评分不需要标准答案的排行榜">4. ELO 评分：不需要标准答案的排行榜</a></li>
      <li><a href="#heading-5-data-flywheel把前四个方法串成闭环" id="markdown-toc-heading-5-data-flywheel把前四个方法串成闭环">5. Data Flywheel：把前四个方法串成闭环</a></li>
    </ul>
  </li>
  <li><a href="#heading-三我们的设计自举评测飞轮" id="markdown-toc-heading-三我们的设计自举评测飞轮">三、我们的设计：自举评测飞轮</a></li>
  <li><a href="#heading-四系统模块设计" id="markdown-toc-heading-四系统模块设计">四、系统模块设计</a></li>
  <li><a href="#heading-五落地路径" id="markdown-toc-heading-五落地路径">五、落地路径</a>    <ul>
      <li><a href="#heading-mvp先回答新算法是否比当前算法更好" id="markdown-toc-heading-mvp先回答新算法是否比当前算法更好">MVP：先回答"新算法是否比当前算法更好"</a></li>
      <li><a href="#heading-第二阶段扩充评测基准" id="markdown-toc-heading-第二阶段扩充评测基准">第二阶段：扩充评测基准</a></li>
      <li><a href="#heading-第三阶段线上持续监控形成飞轮" id="markdown-toc-heading-第三阶段线上持续监控形成飞轮">第三阶段：线上持续监控，形成飞轮</a></li>
    </ul>
  </li>
  <li><a href="#heading-六几个反直觉的地方" id="markdown-toc-heading-六几个反直觉的地方">六、几个反直觉的地方</a></li>
  <li><a href="#heading-七总结" id="markdown-toc-heading-七总结">七、总结</a></li>
</ul>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/eval-cover.jpg" alt="封面" /></p>

<blockquote>
  <p>我们团队做了多年自研算法，最近遇到一个棘手的问题：新算法理论上更好，但无法客观证明。这篇文章分享我们从零开始设计评测体系的思路，以及背后借鉴的业界方法论。</p>
</blockquote>

<hr />

<h2 id="heading-一问题从哪里来">一、问题从哪里来</h2>

<p>团队的自研算法已经跑了好几年了，线上版本整体稳定。随着持续迭代，新版本出来了——直觉和小范围测试都感觉好不少。但老板问"能不能证明新算法更好、好多少"的时候，我们沉默了。</p>

<p>问题出在哪？<strong>历史积累的标注数据，是由当前算法自己打的标签。</strong> 用"当前算法的判断"来评测新算法，像让运动员给自己裁判，偏差是天然存在的。</p>

<p>而且这不只是一个数据问题，暴露出来的是整套评测体系的缺失：</p>

<ul>
  <li><strong>没有标准数据集</strong>：历史标注由当前算法产出，不中立</li>
  <li><strong>人工标注成本高</strong>：全靠人工既慢又贵，难以规模化</li>
  <li><strong>没有评测机制</strong>：每次评估都要从零开始，结论无法横向对比</li>
  <li><strong>切换决策没有依据</strong>：算法切换是高风险操作，不能只靠直觉</li>
</ul>

<hr />

<h2 id="heading-二业界是怎么解决这类问题的">二、业界是怎么解决这类问题的</h2>

<p>着手设计方案前，我们调研了业界的成熟做法。有意思的是，这些方法各自解决一个子问题，拼在一起刚好能覆盖我们的困境。</p>

<h3 id="heading-1-llm-as-judge让-ai-当裁判">1. LLM-as-Judge：让 AI 当裁判</h3>

<p>没有标准答案，能不能让一个够强的 AI 来替代人工做判断？</p>

<p>这就是 LLM-as-Judge 的出发点。MT-Bench（NeurIPS 2023）的研究表明，GPT-4 与人类专家的评判一致率超过 80%。更重要的是，AI 裁判不只给分，还会解释理由——而这些理由本身就是定位算法问题的线索。</p>

<p>当然 AI 裁判也有自己的偏见：</p>

<ul>
  <li><strong>位置偏见</strong>：倾向于选排在前面的答案（对策：评两次并交换顺序）</li>
  <li><strong>啰嗦偏见</strong>：回答越长越容易高分（对策：评分细则里明确"不因长度加分"）</li>
  <li><strong>自恋偏见</strong>：偏好与自身风格相近的内容（对策：换不同家族的模型当裁判）</li>
</ul>

<p>对我们来说，LLM-as-Judge 最大的价值不是"替代人工打分"，而是<strong>用来生产标准数据集</strong>。让 LLM 和现有算法同时跑一批数据，盯着分歧案例让人工仲裁——LLM 承担初筛，人工只处理真正有争议的地方，成本直接降一个数量级。</p>

<h3 id="heading-2-weak-supervision让不完美的规则联合投票">2. Weak Supervision：让不完美的规则联合投票</h3>

<p>数据量大的时候，不可能每条都过 AI 裁判。弱监督提供了一条批量打标的路。</p>

<p>思路很直接：把多个"不完美但有用的信号"组合起来投票。比如：</p>

<ul>
  <li>现有算法的判断（准确率 70%）</li>
  <li>大模型的判断（准确率 85%）</li>
  <li>规则判断：文本长度差异超过 50%（准确率 60%）</li>
</ul>

<p>单看每条规则都不够可靠，但斯坦福开发的 Snorkel 框架能自动推算每条规则的可信度，加权投票后准确率大幅提升。关键是<strong>不需要任何标准答案</strong>——它靠规则之间的"同意/打架"模式反推各规则的可靠性。这个方案 Google 已经在工业规模落地（DryBell，2019）。</p>

<blockquote>
  <p>LLM-as-Judge 解决<strong>质量</strong>问题（谁来判断对错），弱监督解决<strong>规模</strong>问题（怎么批量打标）。前者精但慢，后者快但粗——两者配合，弱监督负责大批量初标，LLM-as-Judge 负责精细复核。</p>
</blockquote>

<h3 id="heading-3-active-learning让模型告诉你标哪些数据最值钱">3. Active Learning：让模型告诉你标哪些数据最值钱</h3>

<p>标注预算有限，怎么把钱花在刀刃上？</p>

<p>主动学习的答案是：<strong>标那些模型最拿不准的数据</strong>，因为标了之后进步最大。就像一道一眼就会的题做了没什么收获，而那道做到一半卡住的题，看答案才真的有提升。</p>

<p>具体策略之一是分歧采样：让多个模型对同一条数据投票，意见最不一致的优先标注。这和我们的直觉不谋而合——评测时优先看各算法分歧大的案例，本质上就是主动学习。</p>

<h3 id="heading-4-elo-评分不需要标准答案的排行榜">4. ELO 评分：不需要标准答案的排行榜</h3>

<p>王者荣耀的段位系统背后是 ELO 算法：不需要知道每个人的"绝对实力"，只需要积累大量"A 和 B 谁赢了"的相对比较，就能得出全局排名。</p>

<p>在算法评测里，它的价值在于：<strong>在标准数据集还不够多的早期，先给出一个相对排名</strong>，让初步决策有据可依。</p>

<p>具体操作：让各算法对同一批数据分别给出判断和解释，由 AI 裁判选"哪个解释更有道理"，记录胜负。积累几百次 PK 之后，排行榜自然浮现。</p>

<p>这套机制还天然支持多维度评价——不只看结论对不对，也评推理过程合不合理。两个算法都答对了，但一个靠真正理解语义，另一个靠碰巧匹配关键词，只看结论是区分不出来的。</p>

<h3 id="heading-5-data-flywheel把前四个方法串成闭环">5. Data Flywheel：把前四个方法串成闭环</h3>

<p>前四个方法各自解决评测的某个子问题。<strong>数据飞轮</strong>不是一个新方法，而是一种设计理念——把这些方法连成一个持续自动迭代的闭环，让整个体系越用越好。</p>

<p>微软的 Arena Learning（2024）是目前最先进的工程实践：用 AI 模拟对战替代人工投票，效率是人工方式的 40 倍，与人类判断一致率达到 98.79%，整个训练过程几乎不需要人工介入。</p>

<hr />

<h2 id="heading-三我们的设计自举评测飞轮">三、我们的设计：自举评测飞轮</h2>

<p>把上述方法论落到具体场景，核心是一个<strong>自举（Bootstrapping）的评测飞轮</strong>：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/eval-flywheel.jpg" alt="自举评测飞轮" /></p>

<p>整个体系的核心是 <strong>AI 裁判 Agent</strong>——它不是静态的打分工具，而是一个会持续成长的 Agent：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/eval-agent.jpg" alt="AI 裁判 Agent 的构成" /></p>

<p>其他所有模块——标准数据集、难例库、Skill 库、报告层——都在围绕这个 Agent 服务。</p>

<hr />

<h2 id="heading-四系统模块设计">四、系统模块设计</h2>

<p>完整系统由以下模块构成：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/eval-system.jpg" alt="评测系统全貌" /></p>

<p>几个关键设计决策值得单独说：</p>

<p><strong>标准数据集分公开集和保留集。</strong> 公开集供算法调优参考，保留集只在最终评测时用。防止算法针对评测集过拟合（"刷榜"）——这和 Kaggle 的 public/private leaderboard 设计同理。</p>

<p><strong>难例库单独管理。</strong> 各算法分歧大、人工也难判断的边界案例单独收录。这批数据是提升 AI 裁判天花板的核心资产，也是算法改进最值得攻坚的方向。</p>

<p><strong>评测结果要版本化。</strong> AI 裁判在升级，标准数据集也在扩充。如果评测结果不绑定具体的裁判版本和数据集版本，历史数据就失去了可比性。</p>

<hr />

<h2 id="heading-五落地路径">五、落地路径</h2>

<h3 id="heading-mvp先回答新算法是否比当前算法更好">MVP：先回答"新算法是否比当前算法更好"</h3>

<p>MVP 不追求完整系统，只要能支撑第一次切换决策就够了：</p>

<ol>
  <li><strong>定义评分维度</strong>：明确从哪几个维度打分，为每个维度写评分细则</li>
  <li><strong>搭基础 AI 裁判</strong>：把维度和细则写入 Prompt，跑通单条打分</li>
  <li><strong>采集 300-500 条数据</strong>：重点选各算法分歧大的案例，少量随机抽取防止盲区</li>
  <li><strong>人工标注</strong>：逐条确认正确答案，建立初始标准数据集（⚠️ 标注人员需要有领域判断力，这一步偷不了懒）</li>
  <li><strong>跑离线对比评测</strong>：AI 裁判对各算法输出打分，人工复核不确定案例</li>
  <li><strong>归因 AI 裁判的错误</strong>：补充 Skill，迭代直到准确率 ≥ 85%（⚠️ 需提前定好阈值，避免无限迭代）</li>
  <li><strong>输出切换决策报告</strong>：指标对比 + 典型错误案例 + 切换风险评估</li>
</ol>

<h3 id="heading-第二阶段扩充评测基准">第二阶段：扩充评测基准</h3>

<ul>
  <li>建立难例库，对高频错误类型专项攻坚</li>
  <li>引入多信号融合批量生成初始标签，降低人工成本</li>
  <li>标准数据集切分公开集 / 保留集</li>
</ul>

<h3 id="heading-第三阶段线上持续监控形成飞轮">第三阶段：线上持续监控，形成飞轮</h3>

<ul>
  <li>线上流量分层抽样，自动进入评测流程</li>
  <li>周期性输出质量报告，设置报警阈值</li>
  <li>线上发现的新难例自动进难例库，人工复核结果反哺 AI 裁判</li>
  <li>引入 ELO/Pairwise 排行榜，作为标准数据集评测的补充</li>
</ul>

<hr />

<h2 id="heading-六几个反直觉的地方">六、几个反直觉的地方</h2>

<p>回顾整个体系，有几点讨论时觉得别扭，但仔细想都有道理：</p>

<p><strong>LLM 不是用来替代人工的，是用来生产 GT 数据的。</strong> 很多人提到 LLM-as-Judge，第一反应是"用 AI 替代人工打分"。但在没有标准数据集的阶段，LLM 更大的价值是充当 GT 数据的初筛器——它处理大量案例，人工只处理 LLM 和算法产生分歧的那一部分。</p>

<p><strong>ELO 不是成熟后用的，是数据集不够时用的。</strong> 很多人会觉得 ELO 是更"高级"的评测方式，应该等体系成熟后再上。但它真正的价值在早期——标准数据集还不够的时候，用 ELO 先给出相对排名，支撑初步决策。数据集建好后，ELO 反而退场。</p>

<p><strong>评算法不只评结论，还要评推理过程。</strong> 两个算法都答对了，但一个靠真正理解语义，另一个靠碰巧匹配关键词——只看结论无法区分。评推理过程，才能判断算法在新场景下的泛化能力。</p>

<hr />

<h2 id="heading-七总结">七、总结</h2>

<p>这套方案的本质，是用自举 + 飞轮的方式，绕开"没有 GT 就无法评测"的死结：</p>

<ul>
  <li>用 LLM 做初始裁判，生产第一批 GT 数据</li>
  <li>用主动学习，把有限的人工精力花在最值钱的地方</li>
  <li>用弱监督，在数据集扩充阶段降低成本</li>
  <li>用 ELO，在数据集不足时提供相对排名</li>
  <li>把以上连成飞轮，让整个体系自动越转越好</li>
</ul>

<p>这套方法论不局限于我们的场景，任何面临<strong>缺乏标准数据集、需要客观评测多版本算法</strong>问题的团队，都可以参考这个框架。</p>

<p>飞轮刚开始推很费力，但只要转起来，就会越来越省力。</p>

<hr />

<p><em>参考文献</em></p>

<ul>
  <li><a href="https://arxiv.org/abs/2306.05685">Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena</a>（Zheng et al., NeurIPS 2023）</li>
  <li><a href="https://arxiv.org/abs/2303.16634">G-Eval: NLG Evaluation using GPT-4</a>（Liu et al., EMNLP 2023）</li>
  <li><a href="https://arxiv.org/abs/1711.10160">Snorkel: Rapid Training Data Creation with Weak Supervision</a>（Ratner et al., VLDB 2018）</li>
  <li><a href="https://arxiv.org/html/2403.04132v1">Chatbot Arena: An Open Platform for Evaluating LLMs</a>（LMSYS, 2024）</li>
  <li><a href="https://arxiv.org/abs/2407.10627">Arena Learning: Build Data Flywheel for LLMs Post-training</a>（Microsoft Research, 2024）</li>
</ul>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="AI" /><category term="算法评测" /><category term="LLM-as-Judge" /><category term="弱监督" /><category term="主动学习" /><category term="ELO" /><category term="数据飞轮" /><summary type="html"><![CDATA[团队做了多年自研算法，却无法客观证明新版本更好。这篇文章分享我们从零设计评测体系的思路，以及背后借鉴的业界方法论。]]></summary></entry><entry><title type="html">English Practice: Life as a WFM Developer</title><link href="https://sut-gc.github.io/blog/2026/03/31/english-practice-life-as-a-wfm-developer/" rel="alternate" type="text/html" title="English Practice: Life as a WFM Developer" /><published>2026-03-31T00:00:00+00:00</published><updated>2026-03-31T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/03/31/english-practice-life-as-a-wfm-developer</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/03/31/english-practice-life-as-a-wfm-developer/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-let-me-introduce-myself--a-developer-working-on-wfm-systems" id="markdown-toc-heading-let-me-introduce-myself--a-developer-working-on-wfm-systems">Let Me Introduce Myself — A Developer Working on WFM Systems</a></li>
  <li><a href="#heading-what-is-workforce-management" id="markdown-toc-heading-what-is-workforce-management">What is Workforce Management?</a></li>
  <li><a href="#heading-a-day-in-my-life-as-a-wfm-developer" id="markdown-toc-heading-a-day-in-my-life-as-a-wfm-developer">A Day in My Life as a WFM Developer</a></li>
  <li><a href="#heading-how-we-turn-business-needs-into-technical-solutions" id="markdown-toc-heading-how-we-turn-business-needs-into-technical-solutions">How We Turn Business Needs into Technical Solutions</a></li>
  <li><a href="#heading-working-across-teams-developers-and-non-technical-colleagues" id="markdown-toc-heading-working-across-teams-developers-and-non-technical-colleagues">Working Across Teams: Developers and Non-Technical Colleagues</a></li>
  <li><a href="#heading-work-life-balance-in-a-tech-company" id="markdown-toc-heading-work-life-balance-in-a-tech-company">Work-Life Balance in a Tech Company</a></li>
  <li><a href="#heading-growing-in-your-career-as-a-software-developer" id="markdown-toc-heading-growing-in-your-career-as-a-software-developer">Growing in Your Career as a Software Developer</a></li>
  <li><a href="#heading-challenges-and-solutions-during-project-development" id="markdown-toc-heading-challenges-and-solutions-during-project-development">Challenges and Solutions During Project Development</a></li>
  <li><a href="#heading-what-factors-are-important-to-the-success-of-overseas-business" id="markdown-toc-heading-what-factors-are-important-to-the-success-of-overseas-business">What Factors Are Important to the Success of Overseas Business?</a></li>
  <li><a href="#heading-how-i-use-social-media-in-my-daily-life" id="markdown-toc-heading-how-i-use-social-media-in-my-daily-life">How I Use Social Media in My Daily Life</a></li>
  <li><a href="#heading-can-data-really-prove-your-results" id="markdown-toc-heading-can-data-really-prove-your-results">Can Data Really Prove Your Results?</a></li>
  <li><a href="#heading-what-i-do-during-my-holidays" id="markdown-toc-heading-what-i-do-during-my-holidays">What I Do During My Holidays</a></li>
</ul>

<h2 id="heading-let-me-introduce-myself--a-developer-working-on-wfm-systems">Let Me Introduce Myself — A Developer Working on WFM Systems</h2>

<p>Hi, my name is [Your Name], and I'm a software developer based in China. Let me tell you a little bit about who I am and what I do every day.</p>

<p>大家好，我叫[你的名字]，是一名在中国工作的软件开发者。让我简单介绍一下我是谁，以及我每天在做什么。</p>

<p>I studied Computer Science in university. Back then, I spent a lot of time learning the fundamentals — things like algorithms, data structures, operating systems, and databases. At first, some of it felt very theoretical. But as I started working, I realized how much that foundation actually matters. The things I learned in school show up in my work more often than I expected.</p>

<p>我大学学的是计算机科学。那时候花了很多时间学基础知识——算法、数据结构、操作系统、数据库这些。一开始觉得有些内容特别理论化，但等到真正工作之后才发现，这些基础知识其实非常重要，在工作中用到的频率比我预想的高得多。</p>

<p>After graduating, I joined a tech company focused on Workforce Management systems, which is often called WFM. In simple terms, WFM software helps businesses manage their employees more efficiently. It covers things like predicting how busy a company will be, automatically creating work schedules, tracking attendance, and generating reports. Our clients are usually large companies that need to manage hundreds or even thousands of employees across different locations.</p>

<p>毕业后，我加入了一家专注于劳动力管理系统的科技公司，这类系统通常被称为 WFM。简单来说，WFM 软件帮助企业更高效地管理员工，包括预测业务量、自动生成排班表、追踪考勤、生成报表等功能。我们的客户通常是大型企业，需要在不同地点管理成百上千的员工。</p>

<p>My role is on the research and development side. That means I'm part of the team that actually builds the system. My day-to-day work includes developing new features, fixing bugs, improving system performance, and reviewing my teammates' code. I work closely with product managers, QA engineers, and business analysts. It's not just about writing code — it's also about understanding what the business needs and finding the best technical way to solve it.</p>

<p>我的岗位属于研发方向，也就是说我是真正构建这套系统的团队中的一员。我日常的工作包括开发新功能、修复 bug、优化系统性能，以及审查同事的代码。我跟产品经理、测试工程师和业务分析师密切合作。这份工作不仅仅是写代码，更重要的是理解业务需求，并找到最佳的技术方案来解决问题。</p>

<p>One thing I really enjoy about my job is that the problems are practical and real. When I build a scheduling feature, I know that real managers will use it every day. That sense of impact keeps me motivated.</p>

<p>我很喜欢这份工作的一点是，面对的问题都是实际的、真实的。当我开发一个排班功能的时候，我知道真正的管理者每天都会用到它。这种实实在在的影响力让我一直保持着动力。</p>

<p>Of course, the work comes with challenges too. Requirements can change, technical problems pop up unexpectedly, and deadlines are always there. But I've learned to stay calm, break problems into smaller pieces, and communicate clearly with my team.</p>

<p>当然，工作中也会遇到挑战。需求可能会变，技术问题会突然冒出来，截止日期也总是如影随形。但我已经学会了保持冷静，把问题拆解成小块，并且跟团队保持清晰的沟通。</p>

<p>Outside of work, I'm always trying to learn something new — whether it's a new technology, a better way to write code, or even improving my English. I believe that curiosity and a willingness to keep growing are the most important qualities for anyone working in tech.</p>

<p>工作之外，我一直在尝试学习新东西——不管是新技术、更好的编码方式，还是提升英语水平。我相信，好奇心和持续成长的意愿，是在科技行业工作的人最重要的品质。</p>

<p>That's a little bit about me. Nice to meet you!</p>

<p>以上就是关于我的一点简单介绍。很高兴认识大家！</p>

<h2 id="heading-what-is-workforce-management">What is Workforce Management?</h2>

<p>Have you ever walked into a store and noticed that there are always enough staff to help you, even on busy days? Or called a customer service hotline and got connected to someone quickly? That doesn't happen by accident. There is a lot of planning behind it. That's where Workforce Management, or WFM, comes in.</p>

<p>你有没有走进一家商店，发现即使在最忙的日子里，也总是有足够的员工来帮助你？或者打客服热线的时候，很快就有人接听？这些都不是碰巧发生的，背后有大量的规划工作。这就是劳动力管理（Workforce Management，简称 WFM）发挥作用的地方。</p>

<p>So, what exactly is WFM? Simply put, it's a system that helps companies manage their employees more smartly. It covers several key areas.</p>

<p>那么，WFM 到底是什么？简单来说，它是一套帮助企业更聪明地管理员工的系统，涵盖了几个关键领域。</p>

<p>First, there's <strong>forecasting</strong>. This means predicting how busy a company will be in the future. For example, a call center might get a lot more calls on Monday mornings than on Friday afternoons. A good WFM system can predict these patterns and help managers plan ahead.</p>

<p>首先是<strong>预测</strong>。也就是预判公司未来的业务量。比如说，一个呼叫中心周一早上的来电量通常比周五下午多很多。一个好的 WFM 系统能识别这些规律，帮助管理者提前做好安排。</p>

<p>Next is <strong>scheduling</strong>. Once you know how many people you need, you have to arrange their shifts. This sounds simple, but it's actually quite complex. You need to balance business needs, employee preferences, and labor laws all at the same time. A WFM system can do this automatically and much faster than doing it by hand.</p>

<p>接下来是<strong>排班</strong>。当你知道需要多少人之后，就得安排他们的班次。这听起来简单，实际上非常复杂。你需要同时兼顾业务需求、员工偏好和劳动法规。WFM 系统可以自动完成这些工作，比手动操作快得多。</p>

<p>There's also <strong>real-time monitoring</strong>. Even the best plan can go wrong on the day. Maybe too many people called in sick, or there's a sudden rush of customers. WFM tools help managers see what's happening right now and make quick adjustments.</p>

<p>还有<strong>实时监控</strong>。再好的计划到了当天也可能出问题。也许太多人请了病假，或者突然涌来一大批客户。WFM 工具能帮助管理者看到当前的实时情况，并快速做出调整。</p>

<p>Finally, there is <strong>reporting and analytics</strong>. After everything is done, companies want to know: did we have the right number of people? Were our predictions accurate? These reports help businesses keep improving over time.</p>

<p>最后是<strong>报表和数据分析</strong>。一切结束后，企业想知道：我们的人手安排合理吗？我们的预测准确吗？这些报表帮助企业不断优化改进。</p>

<p>I work as a developer on a WFM system, which means my job is to build and improve these tools. It's really interesting because I get to see how technology can solve real business problems. Every feature I work on has a direct impact on how companies run their day-to-day operations.</p>

<p>我是一名 WFM 系统的开发者，我的工作就是构建和改进这些工具。这份工作非常有趣，因为我能亲眼看到技术是如何解决实际业务问题的。我做的每一个功能，都会直接影响企业的日常运营。</p>

<p>WFM might not be the most well-known field in tech, but it plays a huge role in keeping businesses running smoothly. And honestly, I think that's pretty cool.</p>

<p>WFM 也许不是科技行业里最广为人知的领域，但它在保障企业顺畅运转方面发挥着巨大的作用。说实话，我觉得这挺酷的。</p>

<h2 id="heading-a-day-in-my-life-as-a-wfm-developer">A Day in My Life as a WFM Developer</h2>

<p>Have you ever wondered what a software developer actually does all day? People sometimes think we just sit in front of a computer and type code for eight hours straight. But the reality is quite different — and honestly, a lot more interesting.</p>

<p>你有没有好奇过软件开发者一整天到底在干什么？有些人觉得我们就是坐在电脑前连续敲八个小时的代码。但现实其实大不相同——而且说实话，有趣得多。</p>

<p>Let me walk you through a typical day in my life as a WFM developer.</p>

<p>让我带你看看我作为一名 WFM 开发者的典型一天。</p>

<p>My day usually starts with checking messages and emails. Before I even open my code editor, I want to know if anything urgent came up overnight. Sometimes a client reports a bug, or a teammate has a question about a feature I built. I try to handle these things first so nobody is blocked waiting for me.</p>

<p>我的一天通常从查看消息和邮件开始。在打开代码编辑器之前，我会先看看昨晚有没有什么紧急的事情。有时候客户报了一个 bug，或者同事对我做的某个功能有疑问。我会优先处理这些事情，免得有人因为等我而耽误了工作。</p>

<p>After that, I join our <strong>daily standup meeting</strong>. This is a short team meeting — usually just fifteen minutes — where everyone shares what they did yesterday, what they plan to do today, and if they have any problems. It's a great way to stay in sync with the team without spending too much time in meetings.</p>

<p>然后是参加<strong>每日站会</strong>。这是一个简短的团队会议——通常只有十五分钟——每个人分享自己昨天做了什么、今天打算做什么、以及遇到了什么问题。这是一种既能和团队保持同步，又不用花太多时间开会的好方式。</p>

<p>Then comes the main part of my day: <strong>coding</strong>. Right now I'm working on improving our scheduling module. This could mean writing new features, fixing bugs, or refactoring old code to make it cleaner and faster. I usually put on some music and get into a focused mode. These deep work hours are my favorite part of the day.</p>

<p>接下来就是一天中最主要的部分了：<strong>写代码</strong>。我目前在优化我们的排班模块。这可能是开发新功能、修复 bug，也可能是重构旧代码让它更简洁高效。我通常会放点音乐，进入专注模式。这些深度工作的时间是我一天中最喜欢的部分。</p>

<p>Around midday, I might have a meeting with the product team. They explain what the business needs, and I help figure out how to build it technically. These conversations are really valuable because they help me understand the "why" behind the work I'm doing.</p>

<p>中午前后，我可能会和产品团队开个会。他们讲解业务需求，我来帮忙想怎么从技术上实现。这类对话非常有价值，因为它们能帮助我理解自己工作背后的"为什么"。</p>

<p>In the afternoon, I often do <strong>code reviews</strong>. This means reading my teammates' code and giving feedback. It's not just about finding bugs — it's also about sharing knowledge and keeping the codebase consistent.</p>

<p>下午我经常会做<strong>代码审查</strong>。就是阅读队友写的代码并给出反馈。这不仅仅是为了找 bug，也是为了分享知识、保持代码风格的一致性。</p>

<p>By the end of the day, I write a quick note to myself about where I stopped and what I need to pick up tomorrow. This small habit saves me a lot of time the next morning.</p>

<p>一天结束的时候，我会给自己写一个简短的笔记，记录做到哪里了、明天需要接着做什么。这个小习惯能帮我第二天早上节省不少时间。</p>

<p>Every day is a little different, and that's what I enjoy most about this job. There's always something new to learn or solve.</p>

<p>每一天都有些不同，这正是我最享受这份工作的地方。总有新的东西要学，新的问题要解决。</p>

<h2 id="heading-how-we-turn-business-needs-into-technical-solutions">How We Turn Business Needs into Technical Solutions</h2>

<p>One of the most interesting parts of my job is bridging the gap between business and technology. Our clients don't come to us saying "I need an API endpoint." They come to us saying "I need my managers to spend less time on scheduling." My job is to translate that into something we can actually build.</p>

<p>我工作中最有意思的部分之一，就是在业务和技术之间架起桥梁。客户不会跑来跟我们说"我需要一个 API 接口"，他们会说"我希望我的管理者能少花点时间在排班上"。而我的工作，就是把这些需求转化成我们能实际构建的东西。</p>

<p>So how does that process work? Let me break it down.</p>

<p>那这个过程是怎样的呢？让我来拆解一下。</p>

<p>It usually starts with a <strong>requirement discussion</strong>. The product manager or business analyst talks to the client to understand their pain points. Then they bring those findings to us, the developers. At this stage, I ask a lot of questions. What exactly is the problem? How often does it happen? How many people are affected? The more I understand the problem, the better solution I can design.</p>

<p>通常从<strong>需求讨论</strong>开始。产品经理或业务分析师和客户沟通，了解他们的痛点。然后把这些发现带给我们开发者。在这个阶段，我会问很多问题。问题到底是什么？多久发生一次？影响了多少人？我对问题理解得越深，设计出来的方案就越好。</p>

<p>Next comes <strong>technical planning</strong>. I think about how to solve the problem within our existing system. Can I reuse something we already have? Do I need to build something new? Are there any risks or limitations I should be aware of? I usually write a short document explaining my plan before I start coding. This helps me think clearly and gives my teammates a chance to spot any issues early.</p>

<p>接下来是<strong>技术方案设计</strong>。我会思考如何在现有系统内解决这个问题。能不能复用已有的东西？需不需要重新开发？有没有需要注意的风险或限制？在开始写代码之前，我通常会写一个简短的文档来说明我的方案。这帮助我理清思路，也让队友有机会尽早发现潜在问题。</p>

<p>Then I start <strong>building</strong>. This is where the actual coding happens. I try to write clean, readable code because other people will need to read and maintain it in the future. I also write tests to make sure everything works as expected.</p>

<p>然后就开始<strong>动手开发</strong>了。这是真正写代码的环节。我会尽量写出干净、易读的代码，因为以后其他人也需要阅读和维护它。我还会写测试来确保一切按预期运行。</p>

<p>After that, we go through <strong>testing and review</strong>. My teammates review my code, the QA team tests the feature, and we fix any issues that come up. This part takes more time than most people expect, but it's really important for quality.</p>

<p>之后是<strong>测试和评审</strong>阶段。队友们审查我的代码，QA 团队测试功能，我们修复发现的所有问题。这个环节花的时间比大多数人想象的要多，但对保证质量来说非常重要。</p>

<p>Finally, we <strong>release and monitor</strong>. Once the feature is live, we watch closely to make sure it's working well in the real world. Sometimes things look fine in testing but behave differently with real data.</p>

<p>最后是<strong>发布和监控</strong>。功能上线后，我们会密切关注它在真实环境中的运行情况。有时候测试环境里一切正常，但到了真实数据下表现却不一样。</p>

<p>This whole process taught me that good software is not just about writing code. It's about deeply understanding people's problems and caring about the solution you deliver.</p>

<p>整个过程让我明白，好的软件不仅仅是写代码。更重要的是深入理解用户的问题，并用心打磨你交付的解决方案。</p>

<h2 id="heading-working-across-teams-developers-and-non-technical-colleagues">Working Across Teams: Developers and Non-Technical Colleagues</h2>

<p>When I first started working as a developer, I thought the hardest part would be writing complex code. But over time, I realized that one of the biggest challenges is actually <strong>communication</strong> — especially with colleagues who have a different background.</p>

<p>刚开始做开发的时候，我以为最难的部分是写复杂的代码。但随着时间的推移，我发现最大的挑战其实是<strong>沟通</strong>——尤其是和背景不同的同事之间的沟通。</p>

<p>In my daily work, I collaborate with product managers, QA engineers, business analysts, and sometimes even clients. Not everyone has a technical background, and that's completely fine. But it does mean I need to adjust the way I explain things.</p>

<p>在日常工作中，我需要和产品经理、QA 工程师、业务分析师合作，有时甚至直接和客户打交道。不是每个人都有技术背景，这完全没问题。但这意味着我需要调整自己表达的方式。</p>

<p>For example, when a product manager asks me "how long will this feature take?", I can't just say "it depends." I need to give a rough estimate and explain the key factors. If I say "there might be database performance issues that could slow us down," I should also explain what that means in plain language — something like "the system might run slowly when too many people use it at the same time."</p>

<p>比如说，当产品经理问我"这个功能要做多久？"的时候，我不能只回答"看情况"。我得给一个大概的估时，并解释关键的影响因素。如果我说"可能会有数据库性能问题拖慢进度"，我还应该用通俗的语言解释清楚——比如说"同时使用的人太多时，系统可能会变慢"。</p>

<p>I've learned a few things that really help with cross-team communication.</p>

<p>我总结了几条对跨团队沟通真正有帮助的经验。</p>

<p>First, <strong>avoid jargon when possible</strong>. Technical terms are efficient among developers, but they can confuse others. I try to use simple, everyday language when talking to non-technical colleagues.</p>

<p>第一，<strong>尽量避免使用专业术语</strong>。技术词汇在开发者之间很高效，但容易让其他人困惑。和非技术同事交流时，我尽量使用简单、日常的语言。</p>

<p>Second, <strong>use examples and comparisons</strong>. Abstract concepts become much clearer with a concrete example. Instead of explaining what an API is in technical terms, I might say "it's like a waiter in a restaurant — it takes your order and brings back what you asked for."</p>

<p>第二，<strong>多用例子和类比</strong>。抽象的概念配上一个具体的例子就清楚多了。与其用技术术语解释 API 是什么，我可能会说"它就像餐厅里的服务员——接你的单，然后把你要的东西端过来"。</p>

<p>Third, <strong>listen more than you talk</strong>. Understanding what the other person actually needs is more important than showing off your technical knowledge. I've saved a lot of time by asking "what problem are you trying to solve?" before jumping into solutions.</p>

<p>第三，<strong>多听少说</strong>。理解对方真正需要什么，比展示你的技术知识更重要。在急着给方案之前先问一句"你想解决什么问题？"，帮我省了不少时间。</p>

<p>Working across teams has made me a better developer and a better communicator. I think the best engineers are not just good at coding — they're good at understanding people too.</p>

<p>跨团队协作让我成为了更好的开发者，也成为了更好的沟通者。我认为最优秀的工程师不仅代码写得好，也善于理解他人。</p>

<h2 id="heading-work-life-balance-in-a-tech-company">Work-Life Balance in a Tech Company</h2>

<p>If you ask most developers about work-life balance, you'll probably get a mixed response. Some will say the tech industry is full of long hours and burnout. Others will say they love what they do and don't even notice the time. My experience is somewhere in between.</p>

<p>如果你问大多数开发者关于工作与生活平衡的看法，得到的答案可能五花八门。有些人会说科技行业加班成风、容易倦怠。也有人说他们热爱自己的工作，根本感觉不到时间的流逝。我的体验介于两者之间。</p>

<p>Let me share what work-life balance actually looks like for me, and what I've learned along the way.</p>

<p>让我分享一下工作与生活的平衡对我来说到底是什么样的，以及我在这个过程中学到了什么。</p>

<p>When I first started my job, I often worked late. There was always one more bug to fix or one more feature to finish. I thought putting in extra hours showed dedication. But after a while, I noticed that I was getting tired, making more mistakes, and actually becoming less productive. That was a wake-up call.</p>

<p>刚开始工作的时候，我经常加班。总有一个 bug 没修完，或者一个功能没做完。我以为多加班体现的是敬业精神。但过了一段时间，我发现自己越来越疲惫，犯的错越来越多，效率反而更低了。这给我敲响了警钟。</p>

<p>I started making some changes. The first thing I did was set a clear <strong>end time</strong> for my workday. Unless there was a real emergency, I would stop working at a certain hour. This sounds simple, but it was actually hard to stick to at first.</p>

<p>于是我开始做出一些改变。第一件事就是给自己设定一个明确的<strong>下班时间</strong>。除非遇到真正的紧急情况，到了那个时间我就停下工作。这听起来简单，但一开始其实很难坚持。</p>

<p>I also started protecting my <strong>weekends</strong>. I try not to check work messages on weekends unless something is truly urgent. Having two full days to rest and recharge makes a huge difference. I come back on Monday feeling much more energetic and focused.</p>

<p>我还开始保护自己的<strong>周末时间</strong>。除非有真正紧急的事，我尽量不在周末查看工作消息。拥有完整的两天休息和充电时间，效果完全不一样。周一回来的时候，我感觉精力充沛、注意力也更集中。</p>

<p>Outside of work, I make time for things I enjoy. Whether it's exercise, spending time with family, reading, or just taking a walk, these activities help me reset mentally.</p>

<p>工作之外，我会留出时间做自己喜欢的事情。不管是锻炼、陪家人、看书还是出去走走，这些活动都能帮我从精神上恢复过来。</p>

<p>I've also learned to <strong>communicate boundaries</strong> at work. If I'm overwhelmed with tasks, I say so instead of silently struggling. Most managers appreciate honesty and would rather adjust the workload than have a burnt-out team member.</p>

<p>我还学会了在工作中<strong>表达自己的边界</strong>。如果任务太多扛不住了，我会直接说出来，而不是默默硬撑。大多数管理者都欣赏坦诚，他们宁愿调整工作量，也不愿意看到团队成员累垮。</p>

<p>Work-life balance doesn't mean working less. It means working sustainably. In the long run, taking care of yourself makes you a better employee — and a happier person.</p>

<p>工作与生活的平衡不是说要少工作，而是要可持续地工作。从长远来看，照顾好自己才能让你成为更好的员工——也成为更快乐的人。</p>

<h2 id="heading-growing-in-your-career-as-a-software-developer">Growing in Your Career as a Software Developer</h2>

<p>Career growth is something almost every developer thinks about. How do I get better at my job? When will I get promoted? What skills should I focus on? These are questions I've asked myself many times.</p>

<p>职业成长是几乎每个开发者都会思考的事情。怎样才能把工作做得更好？什么时候能升职？应该重点提升哪些技能？这些问题我自己也问过很多次。</p>

<p>Let me share some things I've learned from my own experience.</p>

<p>让我分享一些从自身经历中学到的东西。</p>

<p>The most obvious way to grow is to <strong>improve your technical skills</strong>. This means writing better code, learning new technologies, and understanding your system more deeply. In WFM development, for example, I've had to learn about scheduling algorithms, data processing, and system performance. Each new challenge taught me something valuable.</p>

<p>最直接的成长方式就是<strong>提升技术能力</strong>。这意味着写出更好的代码、学习新技术、更深入地理解你的系统。以 WFM 开发为例，我需要学习排班算法、数据处理和系统性能优化。每一个新挑战都让我学到了有价值的东西。</p>

<p>But technical skills are only part of the picture. As you move up in your career, <strong>soft skills</strong> become just as important. Can you explain your work clearly? Can you help junior teammates grow? Can you manage a project without someone telling you what to do every step of the way? These abilities matter a lot when it comes to promotions.</p>

<p>但技术能力只是全貌的一部分。随着职业发展，<strong>软技能</strong>变得同样重要。你能清楚地表达自己的工作吗？你能帮助初级同事成长吗？你能在没人手把手指导的情况下独立管理项目吗？这些能力在晋升时非常关键。</p>

<p>Another thing that helped me grow was <strong>taking on challenges outside my comfort zone</strong>. When a difficult task came up and no one was rushing to take it, I volunteered. It was stressful sometimes, but I always learned something new. Comfort zones are cozy, but they don't help you grow.</p>

<p>另一个帮助我成长的做法是<strong>主动承担舒适区之外的挑战</strong>。当一个棘手的任务出现而没人主动认领时，我会站出来。压力有时候确实很大，但我总能学到新东西。舒适区虽然安逸，但它不会帮你成长。</p>

<p>I also think it's important to <strong>ask for feedback regularly</strong>. Don't wait for your annual performance review. Talk to your manager and teammates more often. Find out what you're doing well and where you can improve. This kind of honest feedback is one of the best tools for growth.</p>

<p>我还认为<strong>定期寻求反馈</strong>非常重要。不要等到年度绩效评估才去了解情况。多和你的主管、队友交流。弄清楚自己哪些方面做得好，哪些方面还可以改进。这种坦诚的反馈是最好的成长工具之一。</p>

<p>Finally, be patient. Career growth is not always a straight line. There will be times when you feel stuck or overlooked. That's normal. Keep learning, keep contributing, and trust the process.</p>

<p>最后，要有耐心。职业成长并不总是一条直线。有时候你会觉得停滞不前或者被忽视，这很正常。保持学习、持续贡献，相信这个过程。</p>

<p>The best investment you can make in your career is in yourself.</p>

<p>你能为自己的职业生涯做出的最好投资，就是投资自己。</p>

<h2 id="heading-challenges-and-solutions-during-project-development">Challenges and Solutions During Project Development</h2>

<p>Every project sounds great at the beginning. The goals are clear, the team is motivated, and the timeline seems reasonable. But somewhere along the way, things get complicated. That's just the reality of software development — and honestly, how you handle those challenges says a lot about you as a developer.</p>

<p>每个项目一开始听起来都很美好。目标清晰、团队干劲十足、时间表看起来也合理。但做着做着，事情就变得复杂了。这就是软件开发的现实——说实话，你如何应对这些挑战，很大程度上反映了你作为开发者的能力。</p>

<p>Let me share some common challenges I've faced in my projects and how I dealt with them.</p>

<p>让我分享一些我在项目中遇到的常见挑战，以及我是如何应对的。</p>

<p>One of the most frequent problems is <strong>unclear requirements</strong>. Sometimes a client or product manager describes what they want, but the details are vague. If I just start coding based on my assumptions, I might build the wrong thing entirely. My solution is to ask more questions upfront and write down my understanding for confirmation before I start. A short document that says "here's what I think we're building" can save weeks of rework later.</p>

<p>最常见的问题之一是<strong>需求不明确</strong>。有时候客户或产品经理描述了他们想要的东西，但细节很模糊。如果我只凭自己的假设就开始写代码，可能会做出完全不对的东西。我的解决办法是在动手之前多问问题，把自己的理解写下来让对方确认。一份简短的"这是我理解的我们要做的东西"的文档，可以避免后续好几周的返工。</p>

<p>Another common challenge is <strong>unexpected technical difficulties</strong>. You start building a feature and suddenly discover that the existing system doesn't support what you need. Or a third-party tool doesn't work the way the documentation says. My approach here is not to panic. I take a step back, research the problem, and talk to teammates who might have faced something similar. Most technical problems have solutions — you just need to find them.</p>

<p>另一个常见的挑战是<strong>意料之外的技术难题</strong>。你开始做一个功能，突然发现现有系统不支持你的需求。或者第三方工具的实际行为和文档描述的不一样。我的应对方式是不要慌。退一步，研究问题，和可能遇到过类似情况的队友聊聊。大多数技术问题都有解决办法——你只需要找到它。</p>

<p><strong>Changing requirements mid-project</strong> is another headache. The client changes their mind, or the business situation shifts. This is frustrating, but it's also a normal part of working in a real business environment. I've learned to stay flexible and think about how to minimize the impact of changes on work that's already done.</p>

<p><strong>项目中途需求变更</strong>也是一个让人头疼的问题。客户改了主意，或者业务形势发生了变化。这确实让人沮丧，但在真实的商业环境中这再正常不过了。我学会了保持灵活，思考如何把变更对已完成工作的影响降到最低。</p>

<p>Finally, <strong>time pressure</strong> is always a challenge. Deadlines can be stressful. My strategy is to break big tasks into smaller pieces and focus on the most important things first. If something is at risk of being late, I communicate early rather than waiting until the last minute.</p>

<p>最后，<strong>时间压力</strong>始终是一个挑战。截止日期带来的压力不小。我的策略是把大任务拆分成小块，优先做最重要的事情。如果有延期风险，我会尽早沟通，而不是拖到最后一刻。</p>

<p>Projects are never perfect, but every challenge is a chance to learn and improve.</p>

<p>项目永远不会完美，但每一个挑战都是学习和进步的机会。</p>

<h2 id="heading-what-factors-are-important-to-the-success-of-overseas-business">What Factors Are Important to the Success of Overseas Business?</h2>

<p>Working in a tech company that serves international clients has given me a lot of exposure to the challenges of doing business overseas. It's not as simple as translating your product into another language and calling it done. There's a lot more to it.</p>

<p>在一家服务国际客户的科技公司工作，让我对海外业务的挑战有了很多切身体会。这件事远不是把产品翻译成另一种语言就算完了那么简单，里面的门道多得很。</p>

<p>From what I've seen and experienced, here are the factors I think matter most.</p>

<p>根据我的观察和经历，以下是我认为最重要的几个因素。</p>

<p>First and most importantly, you need to <strong>understand the local market</strong>. Every country has different business cultures, customer expectations, and working habits. What works perfectly in one market might completely miss the mark in another. Before entering a new market, it's worth spending real time learning about local needs rather than just assuming your existing product fits.</p>

<p>首先也是最重要的，你需要<strong>了解当地市场</strong>。每个国家都有不同的商业文化、客户期望和工作习惯。在一个市场上完美运作的东西，到了另一个市场可能完全不对路。在进入新市场之前，值得花时间真正去了解当地的需求，而不是想当然地认为现有产品能直接适用。</p>

<p>Second, <strong>communication and trust</strong> are everything in overseas business. When you're working across different time zones and cultures, misunderstandings happen easily. Being clear, responsive, and honest builds trust over time. Clients in any country want to feel that you understand their problems and that you'll follow through on your promises.</p>

<p>第二，<strong>沟通和信任</strong>在海外业务中至关重要。当你跨越不同时区和文化开展工作时，误解很容易发生。保持清晰、及时回应和诚实守信，能够逐步建立起信任。任何国家的客户都希望感受到你理解他们的问题，并且会兑现你的承诺。</p>

<p>Third, you need to <strong>respect local regulations and compliance requirements</strong>. Labor laws, data privacy rules, and business regulations vary significantly from country to country. In WFM, for example, scheduling rules and overtime policies are very different in different markets. Ignoring these differences can cause serious problems for your clients.</p>

<p>第三，你需要<strong>尊重当地的法规和合规要求</strong>。劳动法、数据隐私规则和商业法规在不同国家之间差异很大。以 WFM 为例，不同市场的排班规则和加班政策截然不同。忽视这些差异可能会给客户带来严重的问题。</p>

<p>Fourth, <strong>having a local presence or support team</strong> makes a big difference. Even in the age of remote work, having someone who speaks the local language and understands the local culture can help close deals and resolve issues much faster.</p>

<p>第四，<strong>在当地有驻点或支持团队</strong>会带来很大的不同。即使在远程办公的时代，有一个能说当地语言、了解当地文化的人，也能帮助更快地达成交易和解决问题。</p>

<p>Finally, <strong>long-term thinking</strong> is key. Overseas business takes time to build. Companies that rush for short-term profits often lose client trust. The ones that succeed are usually the ones that invest patiently in relationships and reputation.</p>

<p>最后，<strong>长远思维</strong>是关键。海外业务需要时间来构建。急于追求短期利润的公司往往会失去客户的信任。最终成功的，通常是那些耐心投资于关系和口碑的公司。</p>

<p>Going global is a big challenge, but with the right mindset and preparation, it can also be a great opportunity.</p>

<p>走向全球是一个很大的挑战，但只要心态对、准备充分，它也可以是一个绝佳的机遇。</p>

<h2 id="heading-how-i-use-social-media-in-my-daily-life">How I Use Social Media in My Daily Life</h2>

<p>Social media is everywhere these days. Whether it's WeChat, Weibo, LinkedIn, Instagram, or Twitter, most of us check our phones multiple times a day. But have you ever stopped to think about how you use social media — and whether it's actually working for you?</p>

<p>如今社交媒体无处不在。不管是微信、微博、LinkedIn、Instagram 还是 Twitter，我们大多数人每天都会刷好几次手机。但你有没有停下来想过，你是怎么使用社交媒体的——它真的对你有帮助吗？</p>

<p>Let me share how I approach it.</p>

<p>让我来分享一下我的做法。</p>

<p>First, I try to be clear about my purpose. Different platforms serve different needs. For me, LinkedIn is for professional networking — I follow people in the tech industry, read articles about software development, and occasionally share something work-related. WeChat is more personal — it's how I stay in touch with friends and family. Mixing these up can get messy, so I try to keep them separate.</p>

<p>首先，我会明确自己使用社交媒体的目的。不同的平台满足不同的需求。对我来说，LinkedIn 是用来拓展职业人脉的——我关注科技行业的人、阅读软件开发相关的文章，偶尔也分享一些跟工作有关的内容。微信则更私人——是我跟朋友和家人保持联系的方式。把这些搞混会很乱，所以我尽量把它们分开。</p>

<p>Second, I use social media to keep learning. There are a lot of great developers and industry experts who share useful content online. Following the right people is like having a free news feed of things that actually matter to your career. I've learned about new tools, read about other people's project experiences, and even found solutions to technical problems just by scrolling through my feed.</p>

<p>第二，我用社交媒体来持续学习。网上有很多优秀的开发者和行业专家在分享有价值的内容。关注对的人就像拥有了一个免费的信息流，里面都是对你职业发展真正有用的东西。我通过刷信息流了解过新工具、读到过别人的项目经验，甚至找到过技术问题的解决方案。</p>

<p>Third, I'm careful about how much time I spend on it. It's very easy to open an app "just for a minute" and suddenly realize thirty minutes have gone by. I try to set a rough limit for myself — checking social media at specific times rather than constantly throughout the day. This helps me stay focused during work hours.</p>

<p>第三，我会注意控制花在上面的时间。打开一个 App "就看一分钟"然后突然发现已经过了半小时，这种事太容易发生了。我会给自己设一个大致的限制——在特定的时间查看社交媒体，而不是一整天都在不停地刷。这有助于我在工作时间保持专注。</p>

<p>Fourth, I think about what I post. I don't post very often, but when I do, I try to share something that might actually be useful or interesting to others. Random complaints or oversharing personal drama — I try to avoid that. Your online presence is part of how people see you, especially professionally.</p>

<p>第四，我会考虑自己发什么内容。我发帖不多，但每次发的时候，我会尽量分享对别人真正有用或有趣的东西。随意吐槽或者过度暴露私人生活——这些我都尽量避免。你的网络形象是别人看待你的一部分，尤其是在职业层面。</p>

<p>Finally, I remind myself that social media is not real life. People usually share their highlights — their achievements, their travels, their best moments. It's easy to compare yourself to others and feel like you're falling behind. But what you see online is rarely the full picture. I try to enjoy the content without letting it affect how I feel about my own life.</p>

<p>最后，我会提醒自己，社交媒体不是真实的生活。人们通常只分享精彩瞬间——他们的成就、旅行、最美好的时刻。你很容易拿自己跟别人比，然后觉得自己落后了。但你在网上看到的几乎从来都不是全貌。我会享受这些内容，但不让它影响我对自己生活的感受。</p>

<p>Social media can be a great tool if you use it with intention. The key is to stay in control of it — and not let it control you.</p>

<p>社交媒体如果有目的地使用，可以是一个很好的工具。关键是你要掌控它——而不是让它来掌控你。</p>

<h2 id="heading-can-data-really-prove-your-results">Can Data Really Prove Your Results?</h2>

<p>In the workplace, we often hear things like "let the data speak" or "show me the numbers." Data has become one of the most common ways to prove that your work is delivering results. But can data really tell the whole story?</p>

<p>在职场中，我们经常听到"用数据说话"或者"拿数字来看"这样的话。数据已经成为证明工作成果最常用的方式之一。但数据真的能说明全部问题吗？</p>

<p>In many cases, data is genuinely useful. If a new feature I built reduces the time managers spend on scheduling by 30%, that number is clear and convincing. It shows that something real changed because of my work. Data like this is hard to argue with, and it gives both me and my manager a shared way to measure progress.</p>

<p>在很多情况下，数据确实很有用。如果我开发的一个新功能让管理者在排班上花费的时间减少了 30%，这个数字就很清晰、很有说服力。它说明因为我的工作，确实有了实际的改变。这样的数据很难反驳，而且它给了我和我的上级一个共同衡量进展的方式。</p>

<p>But data also has its limits. Some of the most valuable work is difficult to measure. For example, if I help a junior teammate grow by mentoring them, there's no direct number for that. If I improve the structure of the codebase so it's easier to maintain in the future, the benefit might not show up in any report for months. Does that mean the work didn't matter? Of course not.</p>

<p>但数据也有它的局限性。有些最有价值的工作是很难量化的。比如说，如果我通过指导帮助一个初级同事成长，这件事没有直接的数字可以衡量。如果我优化了代码库的结构，让它在未来更容易维护，这个好处可能几个月之后才会体现出来。但这是不是意味着这些工作不重要？当然不是。</p>

<p>There's also the risk of measuring the wrong things. If a team is judged only by how many features they ship, they might rush and sacrifice quality. The numbers look good, but the actual results are poor. Data can be misleading if we're not careful about what we choose to measure.</p>

<p>还有一个风险是衡量错了指标。如果一个团队只按上线了多少功能来评判，他们可能会赶进度而牺牲质量。数字看起来很好看，但实际效果很差。如果我们不注意选择衡量什么，数据是会产生误导的。</p>

<p>I think the honest answer is: data is a powerful tool, but it's not the whole picture. Good results need both — numbers that show impact, and context that explains the story behind them.</p>

<p>我觉得实话实说就是：数据是一个强大的工具，但它不是全貌。好的成果展示需要两者兼备——能体现影响力的数字，以及解释背后故事的上下文。</p>

<p>Data can support your case. But it takes judgment and communication to truly prove your value.</p>

<p>数据可以支撑你的论点。但要真正证明你的价值，还需要判断力和沟通能力。</p>

<h2 id="heading-what-i-do-during-my-holidays">What I Do During My Holidays</h2>

<p>Holidays are something I always look forward to. After weeks of meetings, coding, and deadlines, having a few days to slow down feels really good.</p>

<p>假期是我一直期待的事情。经过几周的会议、写代码和赶截止日期之后，能有几天慢下来的时间，感觉真的很好。</p>

<p>I don't usually plan my holidays too carefully. I like to keep things relaxed. Sometimes I sleep in, cook a proper meal instead of ordering takeout, and just enjoy doing nothing for a while. It sounds boring, but honestly, it feels great after a busy stretch at work.</p>

<p>我通常不会把假期安排得太详细，喜欢保持轻松随意的状态。有时候睡个懒觉，自己好好做顿饭而不是叫外卖，然后就享受一会儿什么都不干的感觉。听起来很无聊，但说实话，忙了一阵之后这样做感觉特别好。</p>

<p>If the holiday is long enough, I might travel somewhere. It doesn't have to be far — even a short trip to a nearby city can feel refreshing. I enjoy walking around unfamiliar streets, trying local food, and taking photos of random things that catch my eye. Traveling, even on a small scale, helps me see things from a different perspective.</p>

<p>如果假期够长，我可能会去旅行。不一定要去很远的地方——哪怕是去附近的城市短途走走也会让人感到神清气爽。我喜欢在陌生的街道上闲逛、尝尝当地的美食、随手拍一些吸引我的东西。旅行，哪怕规模很小，也能帮我换一个角度看事情。</p>

<p>I also use holidays to catch up on things I don't have time for during the week. Reading a book I've been putting off, watching a documentary, or learning something new out of pure curiosity — not because it's useful for work, just because it's interesting. That kind of learning feels the most enjoyable.</p>

<p>我也会利用假期去做平时没时间做的事情。读一本一直拖着没看的书、看一部纪录片，或者纯粹出于好奇学点新东西——不是因为对工作有用，只是因为有意思。这种学习的感觉是最愉快的。</p>

<p>Of course, spending time with family and friends is a big part of holidays too. A simple meal together or a long conversation over tea can be more meaningful than any fancy trip.</p>

<p>当然，和家人朋友在一起也是假期很重要的一部分。一起吃顿简单的饭，或者喝着茶聊很久，有时候比任何精心安排的旅行都更有意义。</p>

<p>After a good holiday, I always come back to work feeling more energized and clear-headed. I think rest is not the opposite of productivity — it's actually part of it.</p>

<p>好好休完一个假之后，我回到工作中总是感觉更有精力、头脑也更清醒。我觉得休息不是效率的对立面——它其实是效率的一部分。</p>

<p>Everyone deserves a proper break. Don't feel guilty about enjoying your holidays.</p>

<p>每个人都值得好好休息一下。享受你的假期，不要有负罪感。</p>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="English" /><category term="Work" /><category term="WFM" /><category term="English Practice" /><category term="Tech" /><category term="Career" /><category term="Life" /><summary type="html"><![CDATA[A collection of English practice essays covering workforce management, developer daily life, cross-team collaboration, career growth, and more.]]></summary></entry><entry><title type="html">我造了一只自己的龙虾</title><link href="https://sut-gc.github.io/blog/2026/03/29/%E6%88%91%E9%80%A0%E4%BA%86%E4%B8%80%E5%8F%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%BE%99%E8%99%BE/" rel="alternate" type="text/html" title="我造了一只自己的龙虾" /><published>2026-03-29T00:00:00+00:00</published><updated>2026-03-29T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/03/29/%E6%88%91%E9%80%A0%E4%BA%86%E4%B8%80%E5%8F%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%BE%99%E8%99%BE</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/03/29/%E6%88%91%E9%80%A0%E4%BA%86%E4%B8%80%E5%8F%AA%E8%87%AA%E5%B7%B1%E7%9A%84%E9%BE%99%E8%99%BE/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-如果要自己实现openclaw没必要都做" id="markdown-toc-heading-如果要自己实现openclaw没必要都做">如果要自己实现OpenClaw，没必要都做</a></li>
  <li><a href="#heading-agent-执行层这个最重的活我把它抽成了框架" id="markdown-toc-heading-agent-执行层这个最重的活我把它抽成了框架">Agent 执行层这个最重的活，我把它抽成了框架</a></li>
  <li><a href="#heading-当我实现-gateway-的时候有三个值得讲的坑" id="markdown-toc-heading-当我实现-gateway-的时候有三个值得讲的坑">当我实现 Gateway 的时候，有三个值得讲的坑</a>    <ul>
      <li><a href="#heading-坑一飞书-sdk-跟-asyncio-打架" id="markdown-toc-heading-坑一飞书-sdk-跟-asyncio-打架">坑一：飞书 SDK 跟 asyncio 打架</a></li>
      <li><a href="#heading-坑二ai-回复太慢用户在干等" id="markdown-toc-heading-坑二ai-回复太慢用户在干等">坑二：AI 回复太慢，用户在干等</a></li>
      <li><a href="#heading-坑三工具注册时依赖还没初始化好" id="markdown-toc-heading-坑三工具注册时依赖还没初始化好">坑三：工具注册时，依赖还没初始化好</a></li>
    </ul>
  </li>
  <li><a href="#heading-其余的常规模块的实现就很简单了" id="markdown-toc-heading-其余的常规模块的实现就很简单了">其余的常规模块的实现，就很简单了</a>    <ul>
      <li><a href="#heading-消息路由" id="markdown-toc-heading-消息路由">消息路由</a></li>
      <li><a href="#heading-会话管理懒过期比你想的好用" id="markdown-toc-heading-会话管理懒过期比你想的好用">会话管理：懒过期比你想的好用</a></li>
      <li><a href="#heading-实现用户记忆的时候我没用向量检索" id="markdown-toc-heading-实现用户记忆的时候我没用向量检索">实现用户记忆的时候，我没用向量检索</a></li>
      <li><a href="#heading-cron-定时任务对openclaw来说真的很重要" id="markdown-toc-heading-cron-定时任务对openclaw来说真的很重要">Cron 定时任务对OpenClaw来说真的很重要</a></li>
      <li><a href="#heading-一些关键指令我们需要人工审批" id="markdown-toc-heading-一些关键指令我们需要人工审批">一些关键指令，我们需要人工审批</a></li>
    </ul>
  </li>
  <li><a href="#heading-让codyclaw跑起来然后体验下我们自研的小龙虾" id="markdown-toc-heading-让codyclaw跑起来然后体验下我们自研的小龙虾">让CodyClaw跑起来，然后体验下我们自研的小龙虾</a>    <ul>
      <li><a href="#heading-让它自己装个技能" id="markdown-toc-heading-让它自己装个技能">让它自己装个技能</a></li>
      <li><a href="#heading-让他自己查看操作定时任务" id="markdown-toc-heading-让他自己查看操作定时任务">让他自己查看操作定时任务</a></li>
      <li><a href="#heading-让它写代码并且跑起来" id="markdown-toc-heading-让它写代码并且跑起来">让它写代码并且跑起来</a></li>
    </ul>
  </li>
  <li><a href="#heading-有了cody那我自己到底实现了多少" id="markdown-toc-heading-有了cody那我自己到底实现了多少">有了Cody，那我自己到底实现了多少</a></li>
  <li><a href="#heading-最后说两句" id="markdown-toc-heading-最后说两句">最后说两句</a></li>
</ul>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-cover.jpg" alt="我造了一只自己的龙虾" /></p>

<p><a href="/blog/2026/03/08/我解剖了这只爆火的龙虾/">上一篇</a>我解剖了 OpenClaw，拆出了它的八层架构，末尾说：下一篇动手造一只。</p>

<p>这篇就是兑现承诺。我真的造了一只出来——叫 <a href="https://github.com/CodyCodeAgent/codyclaw">CodyClaw</a>，跑在飞书上，效果还不错。</p>

<p>如果你也想自己动手造一只 Agent，这篇可以帮你少走一些弯路。下面讲三件事：<strong>砍什么留什么、造的过程中踩了哪些坑、最后跑起来是什么样</strong>。</p>

<hr />

<h2 id="heading-如果要自己实现openclaw没必要都做">如果要自己实现OpenClaw，没必要都做</h2>

<p>先回顾一下<a href="/blog/2026/03/08/我解剖了这只爆火的龙虾/">上一篇</a>拆出的八层架构。这张表值得花一分钟看看——它能帮你判断，如果自己动手造，哪些值得做、哪些该果断砍掉：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-8layers.jpg" alt="八层架构，我做哪些？" /></p>

<table>
  <thead>
    <tr>
      <th>层</th>
      <th>OpenClaw 做了什么</th>
      <th>我要不要做</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>一、入口层</td>
      <td>CLI、TUI、Web UI、macOS App、iOS、Android</td>
      <td>不做。飞书就是我的入口</td>
    </tr>
    <tr>
      <td>二、消息渠道层</td>
      <td>WhatsApp（逆向协议）、Telegram、Discord、Slack 等 10+</td>
      <td>只做飞书。官方 SDK，WebSocket 长连接</td>
    </tr>
    <tr>
      <td>三、Gateway 层</td>
      <td>消息路由、会话管理、状态机、健康监控、Boot 机制、执行审批</td>
      <td><strong>完整实现</strong>。这是心脏</td>
    </tr>
    <tr>
      <td>四、Hooks &amp; Cron 层</td>
      <td>事件总线、cron 定时任务</td>
      <td>做 cron，hooks 暂时不需要</td>
    </tr>
    <tr>
      <td>五、Agent 执行层</td>
      <td>Claude Code CLI / Codex CLI，双引擎，嵌入式运行</td>
      <td><strong>完整实现——但抽成了独立框架</strong></td>
    </tr>
    <tr>
      <td>六、LLM Provider 层</td>
      <td>28 个供应商、Auth Profile 轮换、模型自动发现</td>
      <td>交给 Agent 框架处理</td>
    </tr>
    <tr>
      <td>七、记忆与技能层</td>
      <td>向量数据库 + FTS 混合检索、ClawHub 插件市场</td>
      <td>简化版记忆，技能用 SKILL.md</td>
    </tr>
    <tr>
      <td>八、基础设施层</td>
      <td>配置系统、执行安全、路径沙箱</td>
      <td>基础的配置管理就够</td>
    </tr>
  </tbody>
</table>

<p>数一下：<strong>两层完整实现</strong>（Gateway、Agent 执行），<strong>一层只做一半</strong>（Cron），<strong>两层大幅简化</strong>（记忆、Provider），<strong>三层不做</strong>（入口、多渠道、基础设施）。</p>

<p>真正绕不过去的就两件事：<strong>Gateway</strong>（消息怎么进来、怎么路由、怎么出去）和 <strong>Agent 执行</strong>（AI 怎么想、怎么调工具、怎么管上下文）。</p>

<p>先说最重的那个。</p>

<hr />

<h2 id="heading-agent-执行层这个最重的活我把它抽成了框架">Agent 执行层这个最重的活，我把它抽成了框架</h2>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-complexity.jpg" alt="第五层自己写，有多累？" /></p>

<p>Agent 执行层是 OpenClaw 代码量最大的一层。它要干的事情包括：工具系统（文件读写、Shell、代码搜索……）、流式输出、会话上下文管理、权限控制、多模型适配。</p>

<p>我一开始试着自己写。写了两天发现不对——这些东西<strong>跟"龙虾"本身的业务逻辑完全无关</strong>。不管你是造龙虾还是造别的什么 Agent 应用，这些脏活都得干一遍。</p>

<p>所以我干了一件事：<strong>把这层抽出来，做成了一个独立框架</strong> <a href="https://github.com/CodyCodeAgent/cody">Cody</a>。一句话说清楚它的定位：让你用几行代码就拿到一个带工具、带流式、带上下文管理、带MCP、Skill热加载机制的 Agent。它怎么设计的我后面会专门写一篇。</p>

<p>为什么要跟你说这个？因为如果你也想造自己的 Agent 应用，<strong>最大的建议就是：不要从零写执行层</strong>。不管用 Cody 还是 LangChain 还是别的什么，找一个趁手的框架把这层给承接了，把精力留给真正有跟你业务特性有关的部分——Gateway 和业务逻辑。</p>

<p>有了框架之后，CodyClaw 的 Agent 执行层就变成了这样：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">client</span> <span class="o">=</span> <span class="n">AsyncCodyClient</span><span class="p">(</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"claude-sonnet-4-6"</span><span class="p">,</span>
    <span class="n">api_key</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">api_key</span><span class="p">,</span>
    <span class="n">base_url</span><span class="o">=</span><span class="n">config</span><span class="p">.</span><span class="n">base_url</span><span class="p">,</span>
    <span class="n">skills_dirs</span><span class="o">=</span><span class="p">[</span><span class="s">"./skills"</span><span class="p">],</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="o">*</span><span class="n">feishu_tools</span><span class="p">,</span> <span class="o">*</span><span class="n">cron_tools</span><span class="p">],</span>
<span class="p">)</span>

<span class="k">async</span> <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">client</span><span class="p">.</span><span class="n">stream</span><span class="p">(</span><span class="s">"你把xxx活给我干了"</span><span class="p">):</span>
    <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">chunk</span><span class="p">,</span> <span class="n">TextDeltaChunk</span><span class="p">):</span>
        <span class="n">response</span> <span class="o">+=</span> <span class="n">chunk</span><span class="p">.</span><span class="n">text</span>
</code></pre></div></div>

<p>工具、流式、会话、上下文压缩、多模型——全在这个 <code class="language-plaintext highlighter-rouge">client</code> 里面。我只需要告诉它用什么模型、有哪些额外的自定义工具。</p>

<hr />

<h2 id="heading-当我实现-gateway-的时候有三个值得讲的坑">当我实现 Gateway 的时候，有三个值得讲的坑</h2>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-compare.jpg" alt="造龙虾的两种方式" /></p>

<p>Gateway 要做的事说起来简单：消息从飞书进来，路由到对应的 Agent，AI 处理完再把结果发回飞书。但真正写起来，有几个地方比想象中麻烦。</p>

<p>这三个坑我都踩过了，分享出来希望能帮你省点时间。</p>

<h3 id="heading-坑一飞书-sdk-跟-asyncio-打架">坑一：飞书 SDK 跟 asyncio 打架</h3>

<p>飞书有官方 SDK <code class="language-plaintext highlighter-rouge">lark-oapi</code>，支持 WebSocket 长连接——连接从你的机器主动发出去，不需要公网 IP，接入本身很简单。</p>

<p>但它有个坑：SDK 在 import 的时候就把当前的 event loop 捕获到了模块级变量里。如果你的服务跑在 uvicorn 上，SDK 会抓到正在运行的 loop，然后调 <code class="language-plaintext highlighter-rouge">loop.run_until_complete()</code>——直接报 "this event loop is already running"。</p>

<p>解法有点 hack：<strong>在独立线程里建一个全新的 event loop，然后直接替换 SDK 模块里的 loop 变量</strong>。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_run_ws_in_thread</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="kn">import</span> <span class="nn">lark_oapi.ws.client</span> <span class="k">as</span> <span class="n">ws_module</span>

    <span class="n">new_loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">new_event_loop</span><span class="p">()</span>
    <span class="n">asyncio</span><span class="p">.</span><span class="n">set_event_loop</span><span class="p">(</span><span class="n">new_loop</span><span class="p">)</span>
    <span class="n">ws_module</span><span class="p">.</span><span class="n">loop</span> <span class="o">=</span> <span class="n">new_loop</span>  <span class="c1"># 替换 SDK 模块级的 loop
</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">_ws_client</span> <span class="o">=</span> <span class="n">lark</span><span class="p">.</span><span class="n">ws</span><span class="p">.</span><span class="n">Client</span><span class="p">(...)</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">_ws_client</span><span class="p">.</span><span class="n">start</span><span class="p">()</span>  <span class="c1"># 阻塞在独立线程里，不影响 uvicorn
</span></code></pre></div></div>

<p>SDK 线程收到消息后，通过 <code class="language-plaintext highlighter-rouge">asyncio.run_coroutine_threadsafe()</code> 跳回 uvicorn 的主循环处理。两个世界就这么接上了。</p>

<blockquote>
  <p>这个解法不够优雅，飞书 SDK 升级后可能失效。如果你遇到同样的问题，先查一下 SDK 是否已经修复了这个 event loop 的问题，没修再用这个 hack。</p>
</blockquote>

<h3 id="heading-坑二ai-回复太慢用户在干等">坑二：AI 回复太慢，用户在干等</h3>

<p>AI 生成一条回复可能要 10-30 秒。如果等它全部生成完再发，用户体验很差——你不知道它是在想还是挂了。</p>

<p>我的做法是<strong>流式卡片</strong>：先发一张"思考中"的蓝色卡片，然后随着 AI 一边生成一边更新卡片内容。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_CARD_UPDATE_INTERVAL</span> <span class="o">=</span> <span class="mf">1.5</span>  <span class="c1"># 再快会触发飞书 API 速率限制
</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">_update_streaming_card</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">run</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="n">force</span><span class="o">=</span><span class="bp">False</span><span class="p">):</span>
    <span class="n">now</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">monotonic</span><span class="p">()</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">force</span> <span class="ow">and</span> <span class="p">(</span><span class="n">now</span> <span class="o">-</span> <span class="n">run</span><span class="p">.</span><span class="n">last_card_update</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">_CARD_UPDATE_INTERVAL</span><span class="p">:</span>
        <span class="k">return</span>  <span class="c1"># 节流
</span>
    <span class="n">content</span> <span class="o">=</span> <span class="n">run</span><span class="p">.</span><span class="n">accumulated_text</span><span class="p">.</span><span class="n">strip</span><span class="p">()</span> <span class="ow">or</span> <span class="s">"⏳ 思考中..."</span>
    <span class="n">card</span> <span class="o">=</span> <span class="n">build_streaming_card</span><span class="p">(</span><span class="s">"执行中"</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="s">"running"</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">run</span><span class="p">.</span><span class="n">card_message_id</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">run</span><span class="p">.</span><span class="n">card_message_id</span> <span class="o">=</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">_channel</span><span class="p">.</span><span class="n">send_card</span><span class="p">(</span><span class="n">msg</span><span class="p">.</span><span class="n">chat_id</span><span class="p">,</span> <span class="n">card</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">_channel</span><span class="p">.</span><span class="n">update_card</span><span class="p">(</span><span class="n">run</span><span class="p">.</span><span class="n">card_message_id</span><span class="p">,</span> <span class="n">card</span><span class="p">)</span>
</code></pre></div></div>

<p>1.5 秒这个数字是试出来的——飞书卡片更新的速率限制大约是每秒 5 次，但实测超过 1 秒一次就容易偶发 429，1.5 秒是个稳妥的值。完成后卡片变绿，附带 AI 调了哪些工具的列表。</p>

<p>还有个容易忽略的边界情况：如果 AI 通过工具自己给用户发了消息（比如调用 <code class="language-plaintext highlighter-rouge">feishu_send_text</code>），结束时就不应该再发一张卡片，否则用户会收到重复信息。所以收尾逻辑要分三种情况：有卡片就更新为完成态，没卡片但 AI 也没主动发消息就兜底发一张，AI 已经发过消息就什么都不做。</p>

<h3 id="heading-坑三工具注册时依赖还没初始化好">坑三：工具注册时，依赖还没初始化好</h3>

<p>CodyClaw 要给 AI 注册自定义工具——比如发飞书消息、建定时任务。但有个时序问题：注册工具的时候，飞书 channel 可能还没连上，后续也可能断线重建。</p>

<p>如果你直接把 <code class="language-plaintext highlighter-rouge">self._channel</code> 传进去，工具拿到的是注册那一刻的实例——后面换了新实例，工具还在用旧的。</p>

<p>解法是<strong>闭包捕获 getter，而不是实例本身</strong>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">make_feishu_tools</span><span class="p">(</span><span class="n">get_channel</span><span class="p">):</span>
    <span class="k">async</span> <span class="k">def</span> <span class="nf">feishu_send_text</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
        <span class="n">channel</span> <span class="o">=</span> <span class="n">get_channel</span><span class="p">()</span>  <span class="c1"># 每次调用时取最新实例
</span>        <span class="k">if</span> <span class="n">channel</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="s">"Error: Feishu channel is not available."</span>
        <span class="k">await</span> <span class="n">channel</span><span class="p">.</span><span class="n">send_text</span><span class="p">(</span><span class="n">chat_id</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
        <span class="k">return</span> <span class="s">"Message sent."</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">feishu_send_text</span><span class="p">,</span> <span class="p">...]</span>

<span class="c1"># 注册时传 lambda，不传实例
</span><span class="bp">self</span><span class="p">.</span><span class="n">_feishu_tools</span> <span class="o">=</span> <span class="n">make_feishu_tools</span><span class="p">(</span><span class="k">lambda</span><span class="p">:</span> <span class="bp">self</span><span class="p">.</span><span class="n">_channel</span><span class="p">)</span>
</code></pre></div></div>

<p>getter lambda 每次调用时拿活实例，组件之间完全解耦。这个模式在初始化顺序不确定的系统里非常好用——如果你做过依赖注入，会觉得很眼熟，本质上就是延迟绑定。</p>

<hr />

<h2 id="heading-其余的常规模块的实现就很简单了">其余的常规模块的实现，就很简单了</h2>

<p>除了上面三个坑，剩下的模块都是比较常规的工程实现。虽然不难，但有几个的实现细节值得展开聊聊。</p>

<h3 id="heading-消息路由">消息路由</h3>

<p>CodyClaw 支持同时跑多个 Agent——比如一个专门写代码，一个专门查资料。那问题来了：用户发了一条消息，该交给谁处理？</p>

<p>我的做法是按三级优先级匹配：</p>

<ol>
  <li><strong>先看群</strong>：这个群有没有绑定过特定 Agent？比如"前端技术群"绑了代码 Agent，那群里所有消息都交给它。</li>
  <li><strong>再看人</strong>：如果群没绑定，看发消息的这个人有没有绑定？比如你把自己绑到了资料 Agent，那你的消息就走资料 Agent。</li>
  <li><strong>最后兜底</strong>：群没绑、人也没绑，走默认 Agent。</li>
</ol>

<p>另外还有个触发条件：私聊机器人时每条消息都会触发，但在群聊里只有 @ 机器人才会触发——不然群里随便聊句天，AI 都要插嘴，太吵了。</p>

<p>实现就是几十行 if-else，没什么花活。但建议一开始就想清楚优先级顺序，后面改起来会牵一发动全身。</p>

<h3 id="heading-会话管理懒过期比你想的好用">会话管理：懒过期比你想的好用</h3>

<p>你跟 OpenClaw 聊天的时候，发完一句它会记得你前面说了什么，这就是"会话"。但如果你关掉页面明天再来，它会开一个新的对话——旧的上下文不会自动带过来。</p>

<p>CodyClaw 也需要这个能力。用户在飞书里连续发了三条消息，AI 得知道这三条是一个对话、要联系起来理解。但如果用户隔了一天才来，就应该开一个全新的会话，不然 AI 还在接着昨天的话题说，会很奇怪。</p>

<p>实现上，每个会话用 <code class="language-plaintext highlighter-rouge">{agent_id}:{chat_id}</code>（群聊）或 <code class="language-plaintext highlighter-rouge">{agent_id}:{user_id}</code>（单聊）做 key。每次写入同步落 SQLite，进程重启后能恢复。</p>

<p>有意思的地方在<strong>过期策略</strong>——一个会话超过 24 小时没活动，就该作废。最直觉的做法是跑一个后台定时器，每隔几分钟扫一遍所有会话，把超时的清掉。但这样你就多了一个需要管理的后台任务，还要操心并发锁。</p>

<p>我用的是<strong>懒过期</strong>——不主动清理，下次 <code class="language-plaintext highlighter-rouge">get()</code> 的时候顺手检查一下：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Optional</span><span class="p">[</span><span class="n">Session</span><span class="p">]:</span>
    <span class="n">session</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_store</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">session</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="k">return</span> <span class="bp">None</span>
    <span class="k">if</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">session</span><span class="p">.</span><span class="n">last_active</span> <span class="o">&gt;</span> <span class="bp">self</span><span class="p">.</span><span class="n">_ttl</span><span class="p">:</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_store</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>  <span class="c1"># 过期了，顺手删掉
</span>        <span class="k">return</span> <span class="bp">None</span>
    <span class="k">return</span> <span class="n">session</span>
</code></pre></div></div>

<p>好处是：没有后台线程、没有锁、代码量极少。代价是过期的会话会在内存里多待一会儿——但对于一个飞书机器人来说，会话数量本身就不多，这点内存完全无所谓。</p>

<p>Redis 的 key 过期其实也是类似思路（惰性删除 + 定期采样），只是多了一层定期兜底。对于这个量级，纯懒过期就够了。</p>

<blockquote>
  <p>如果你用 Docker 部署，<strong>记得给 SQLite 文件挂 volume</strong>，否则容器重建数据就没了。</p>
</blockquote>

<h3 id="heading-实现用户记忆的时候我没用向量检索">实现用户记忆的时候，我没用向量检索</h3>

<p>会话解决的是"这次对话里记得前面说了什么"，但<strong>跨对话的记忆</strong>是另一回事。比如用户上周告诉 AI "我是后端开发，主要用 Go"，这周问它"帮我写个脚本"，它应该默认用 Go 而不是 Python——这就需要把用户的偏好、习惯这些信息持久化下来，下次对话还能用。</p>

<p>很多人一提到"记忆"就想到 RAG——把记忆向量化存进向量数据库，对话时检索最相关的几条注入 prompt。这是对的，但<strong>不是所有场景都需要这么做</strong>。</p>

<p>我的做法更粗暴：每个用户一个 JSON 文件，AI 通过工具写入，上限 100 条，满了就 FIFO 淘汰最早的。每次对话把这个用户的所有记忆全量注入 prompt，预算 1500 token。</p>

<p>为什么不用向量检索？因为<strong>检索会漏</strong>。向量相似度是语义匹配，如果用户之前说"我不喜欢早上被打扰"，而当前对话在讨论"定时任务的执行时间"，这两个语义不一定能匹配上——但这恰恰是最该被召回的记忆。</p>

<p>全量注入不会漏，代价是占 token。但 100 条短记忆压在 1500 token 以内，对于现在动辄 100K+ 上下文的模型来说根本不是问题。</p>

<p>当然，这个方案有边界：如果你的用户量大、每个用户记忆多、并发写入频繁，JSON 文件就顶不住了（并发写可能丢数据，量大了全量注入也会太贵）。到那个阶段再上向量数据库也不迟——先跑起来，别过早优化。</p>

<h3 id="heading-cron-定时任务对openclaw来说真的很重要">Cron 定时任务对OpenClaw来说真的很重要</h3>

<p>前面的模块都是"用户说一句，AI 做一件事"。但有些事情你希望 AI <strong>自己定时去做</strong>——比如每天早上 9 点抓一波技术新闻发到群里，每隔 30 分钟检查一下服务是否健康。你不可能每天早上准时爬起来跟它说"帮我看看新闻"，这就需要 Cron。</p>

<p>实现上用的是 APScheduler，支持 crontab（<code class="language-plaintext highlighter-rouge">0 9 * * 1-5</code>）和自然语言（<code class="language-plaintext highlighter-rouge">every 30m</code>）两种写法。值得一提的是，每个定时任务有自己独立的持久化会话，AI 跨多次执行能积累上下文——比如早报任务，它能记得昨天推过哪些新闻，今天就不会重复推。这块没什么坑，APScheduler 文档很全，照着来就行。</p>

<h3 id="heading-一些关键指令我们需要人工审批">一些关键指令，我们需要人工审批</h3>

<p>AI Agent 能调工具、能跑命令，这很强大，但也很危险。你肯定不希望它自作主张把某个文件删了，或者在生产机器上跑了个 <code class="language-plaintext highlighter-rouge">rm -rf</code>。所以高危操作需要先问一下人："我打算做这件事，你同意吗？"</p>

<p>这件事如果从零写，你得拦截工具调用、挂起执行流、等人类响应、超时处理……想想就头大。但前面说的 Cody 框架天然支持 Human-in-the-Loop——它在 Agent 执行工具之前会触发一个回调，你可以在回调里决定放行还是拦截。</p>

<p>所以 CodyClaw 要做的事就很简单了：<strong>在 Cody 的回调里接上飞书的审批卡片</strong>。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">on_tool_approval</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">run</span><span class="p">,</span> <span class="n">tool_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">tool_input</span><span class="p">:</span> <span class="nb">dict</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="n">event</span> <span class="o">=</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">Event</span><span class="p">()</span>
    <span class="n">run</span><span class="p">.</span><span class="n">pending_approval</span> <span class="o">=</span> <span class="n">event</span>

    <span class="c1"># 给用户发一张审批卡片，带"同意"和"拒绝"按钮
</span>    <span class="n">desc</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"即将执行: </span><span class="si">{</span><span class="n">tool_name</span><span class="si">}</span><span class="s">"</span>
    <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">_channel</span><span class="p">.</span><span class="n">send_approval_card</span><span class="p">(</span>
        <span class="n">run</span><span class="p">.</span><span class="n">chat_id</span><span class="p">,</span> <span class="n">desc</span><span class="p">,</span> <span class="n">run_id</span><span class="o">=</span><span class="n">run</span><span class="p">.</span><span class="nb">id</span>
    <span class="p">)</span>

    <span class="c1"># 挂起，等用户点按钮
</span>    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="n">wait_for</span><span class="p">(</span><span class="n">event</span><span class="p">.</span><span class="n">wait</span><span class="p">(),</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">300</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">run</span><span class="p">.</span><span class="n">approval_result</span>
</code></pre></div></div>

<p>用户在飞书里点"同意"时，回调把 <code class="language-plaintext highlighter-rouge">run.approval_result</code> 设为 True 然后 <code class="language-plaintext highlighter-rouge">event.set()</code>，上面的 <code class="language-plaintext highlighter-rouge">await</code> 就放行了。点"拒绝"或者 5 分钟超时就走拒绝逻辑。</p>

<p>这里又体现了把 Agent 执行层抽成框架的好处：<strong>拦截、挂起、恢复这些脏活 Cody 都干了，CodyClaw 只需要关心"用飞书卡片问用户同不同意"这一件事</strong>。如果哪天你想把审批从飞书换成钉钉、Slack，改的只是发卡片那几行，核心逻辑一行不动。</p>

<hr />

<h2 id="heading-让codyclaw跑起来然后体验下我们自研的小龙虾">让CodyClaw跑起来，然后体验下我们自研的小龙虾</h2>

<p>如果出于学习的目的，你可以去看这个Github：https://github.com/CodyCodeAgent/codyclaw</p>

<p>项目结构大概如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codyclaw/
├── main.py          ← FastAPI 应用入口
├── config.py        ← YAML 配置加载（支持 ${ENV_VAR}）
├── channel/         ← 飞书 WebSocket 接入
├── gateway/         ← 路由、调度、会话、记忆
├── automation/      ← Cron 调度、事件总线
├── web/             ← 管理后台
└── skills/          ← 内置的Skill技能包
</code></pre></div></div>

<p>对，我们还做了Web的管理后台，不过这都不是必须的。</p>

<p>按照项目里的README就可以完整的跑起来整个项目，下面是几个真实的交互截图，看看实际用起来是什么感觉。</p>

<h3 id="heading-让它自己装个技能">让它自己装个技能</h3>

<p>发一句"帮我装一下 self-improving-agent 这个 skill"，它就把技能装好了。装完之后它会记录自己犯过的错误、总结新学到的东西——下次不会再犯同样的错。不需要 ssh、不需要重启，一条消息搞定。</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-demo-skill.png" alt="安装技能 demo" /></p>

<h3 id="heading-让他自己查看操作定时任务">让他自己查看操作定时任务</h3>

<p>问它"现在有什么定时任务"，它把所有 cron 列出来：工作日早 9 点的技术新闻、每 30 分钟一次的健康检查、每天 10 点的 AI 早报。这些任务都是之前聊天时让它帮忙建的，它自己写进了数据库。</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-demo-cron.png" alt="定时任务 demo" /></p>

<h3 id="heading-让它写代码并且跑起来">让它写代码并且跑起来</h3>

<p>"帮我写个 Python 的 Hello World 页面，跑在 12346 端口。" 它写了文件、启动进程、发现卡住了、自己重启、curl 验证通过，最后把地址告诉你。这不是在跟你说"你可以这样做"——它直接在你的机器上把事情做了。</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-demo-helloworld.png" alt="写代码运行 demo" /></p>

<hr />

<h2 id="heading-有了cody那我自己到底实现了多少">有了Cody，那我自己到底实现了多少</h2>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/uploads/codyclaw-checklist.jpg" alt="CodyClaw 实现了多少？" /></p>

<p>回到开头那张八层表，对比一下：</p>

<table>
  <thead>
    <tr>
      <th>能力</th>
      <th>OpenClaw</th>
      <th>CodyClaw</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>消息渠道</td>
      <td>WhatsApp / Telegram / Discord 等 10+</td>
      <td>飞书</td>
    </tr>
    <tr>
      <td>Agent 执行</td>
      <td>Claude Code CLI / Codex CLI / 嵌入式</td>
      <td>Cody 框架</td>
    </tr>
    <tr>
      <td>消息路由</td>
      <td>多渠道 + 多 Agent</td>
      <td>飞书 + 多 Agent</td>
    </tr>
    <tr>
      <td>会话管理</td>
      <td>JSONL 文件持久化</td>
      <td>SQLite + Cody 托管</td>
    </tr>
    <tr>
      <td>记忆系统</td>
      <td>向量嵌入 + FTS 混合检索</td>
      <td>JSON 文件全量注入</td>
    </tr>
    <tr>
      <td>定时任务</td>
      <td>croner + 持久化</td>
      <td>APScheduler + SQLite</td>
    </tr>
    <tr>
      <td>执行审批</td>
      <td>多渠道审批卡片</td>
      <td>飞书消息 + asyncio.Event</td>
    </tr>
    <tr>
      <td>技能系统</td>
      <td>ClawHub 13000+ 技能</td>
      <td>本地 SKILL.md</td>
    </tr>
    <tr>
      <td>Boot 启动脚本</td>
      <td>BOOT.md 自然语言启动</td>
      <td>支持</td>
    </tr>
    <tr>
      <td>流式输出</td>
      <td>多渠道流式推送</td>
      <td>飞书卡片实时更新</td>
    </tr>
    <tr>
      <td>浏览器自动化</td>
      <td>Playwright + CDP</td>
      <td>不做</td>
    </tr>
    <tr>
      <td>多端 App</td>
      <td>iOS / Android / macOS</td>
      <td>不做</td>
    </tr>
    <tr>
      <td>插件市场</td>
      <td>ClawHub 生态</td>
      <td>不做</td>
    </tr>
  </tbody>
</table>

<p>核心功能基本都有了。砍掉的是多渠道、浏览器、移动端 App 这些"外延"——对于一个跑在飞书上的个人 AI Agent，这些本来也用不着。</p>

<hr />

<h2 id="heading-最后说两句">最后说两句</h2>

<p>写这篇不是要做一个 OpenClaw 的替代品。说实话，OpenClaw 的工程量和社区生态不是一个人能比的。</p>

<p>我真正想做的事情是：<strong>通过自己动手造一只，把<a href="/blog/2026/03/08/我解剖了这只爆火的龙虾/">上一篇</a>拆解出来的技术细节从"看懂了"变成"摸过了"</strong>。</p>

<p>上一篇你知道了 OpenClaw 有消息路由、有会话管理、有 cron、有执行审批、有 SKILL.md、有 BOOT.md。但那些都是别人的代码。自己写一遍之后你会发现：消息路由就是几十行 if-else，会话管理就是一个 <code class="language-plaintext highlighter-rouge">{agent_id}:{user_id}</code> 的 key，cron 就是 APScheduler 加个 SQLite，执行审批就是在消息流里插一个 asyncio.Event。每一个单独拿出来都不复杂，OpenClaw 的厉害之处在于把它们粘成了一个 7x24 在线的整体。</p>

<p>而粘合这件事本身，真没那么玄——CodyClaw 的 gateway 目录去掉空行和注释，不到 800 行 Python。</p>

<p>如果你也想试试，所有代码都在这里，随便拿去改：</p>

<ul>
  <li>Cody 框架：<a href="https://github.com/CodyCodeAgent/cody">github.com/CodyCodeAgent/cody</a></li>
  <li>CodyClaw 源码：<a href="https://github.com/CodyCodeAgent/codyclaw">github.com/CodyCodeAgent/codyclaw</a></li>
</ul>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="AI" /><category term="Agent" /><category term="Python" /><category term="飞书" /><category term="开源" /><summary type="html"><![CDATA[上一篇解剖了 OpenClaw 的八层架构，这一篇自己动手造一只——砍需求、踩坑、跑起来。]]></summary></entry><entry><title type="html">不卷模型，开始卷 Harness 了</title><link href="https://sut-gc.github.io/blog/2026/03/24/harness-engineering-ai-agent-%E6%97%B6%E4%BB%A3%E7%9A%84%E5%B7%A5%E7%A8%8B%E8%8C%83%E5%BC%8F%E9%9D%A9%E5%91%BD/" rel="alternate" type="text/html" title="不卷模型，开始卷 Harness 了" /><published>2026-03-24T00:00:00+00:00</published><updated>2026-03-24T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/03/24/harness-engineering-ai-agent-%E6%97%B6%E4%BB%A3%E7%9A%84%E5%B7%A5%E7%A8%8B%E8%8C%83%E5%BC%8F%E9%9D%A9%E5%91%BD</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/03/24/harness-engineering-ai-agent-%E6%97%B6%E4%BB%A3%E7%9A%84%E5%B7%A5%E7%A8%8B%E8%8C%83%E5%BC%8F%E9%9D%A9%E5%91%BD/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-别再卷模型了套在外面的-harness-才是胜负手" id="markdown-toc-heading-别再卷模型了套在外面的-harness-才是胜负手">别再卷模型了，套在外面的 Harness 才是胜负手</a>    <ul>
      <li><a href="#heading-一个重新理解-ai-工程的发现" id="markdown-toc-heading-一个重新理解-ai-工程的发现">一个重新理解 AI 工程的发现</a></li>
      <li><a href="#heading-先说清楚从提示词到-harness我们到底经历了什么" id="markdown-toc-heading-先说清楚从提示词到-harness我们到底经历了什么">先说清楚：从提示词到 Harness，我们到底经历了什么</a>        <ul>
          <li><a href="#heading-2022-2024提示词工程" id="markdown-toc-heading-2022-2024提示词工程">2022-2024：提示词工程</a></li>
          <li><a href="#heading-2025上下文工程" id="markdown-toc-heading-2025上下文工程">2025：上下文工程</a></li>
          <li><a href="#heading-2026harness-engineering" id="markdown-toc-heading-2026harness-engineering">2026：Harness Engineering</a></li>
        </ul>
      </li>
      <li><a href="#heading-所以-harness-到底是啥" id="markdown-toc-heading-所以-harness-到底是啥">所以 Harness 到底是啥？</a></li>
      <li><a href="#heading-其实-harness-比模型更重要" id="markdown-toc-heading-其实-harness-比模型更重要">其实 Harness 比模型更重要</a>        <ul>
          <li><a href="#heading-langchain模型没换排名从-30-开外冲进-top-5" id="markdown-toc-heading-langchain模型没换排名从-30-开外冲进-top-5">LangChain：模型没换，排名从 30 开外冲进 Top 5</a></li>
          <li><a href="#heading-vercel砍掉-80-的工具成功率反而到了-100" id="markdown-toc-heading-vercel砍掉-80-的工具成功率反而到了-100">Vercel：砍掉 80% 的工具，成功率反而到了 100%</a></li>
          <li><a href="#heading-nate-b-jones同一个模型差的-harness-42好的-78" id="markdown-toc-heading-nate-b-jones同一个模型差的-harness-42好的-78">Nate B Jones：同一个模型，差的 harness 42%，好的 78%</a></li>
          <li><a href="#heading-openai5-个月100-万行代码零行手写" id="markdown-toc-heading-openai5-个月100-万行代码零行手写">OpenAI：5 个月，100 万行代码，零行手写</a></li>
          <li><a href="#heading-有这么一个道理约束越多agent-反而表现越好" id="markdown-toc-heading-有这么一个道理约束越多agent-反而表现越好">有这么一个道理：约束越多，Agent 反而表现越好</a></li>
        </ul>
      </li>
      <li><a href="#heading-harness的三大支柱" id="markdown-toc-heading-harness的三大支柱">Harness的三大支柱</a>        <ul>
          <li><a href="#heading-第一根柱子上下文工程agent-看不到的东西等于不存在" id="markdown-toc-heading-第一根柱子上下文工程agent-看不到的东西等于不存在">第一根柱子：上下文工程——Agent 看不到的东西等于不存在</a></li>
          <li><a href="#heading-第二根柱子架构约束别跟-agent-讲道理直接拦住它" id="markdown-toc-heading-第二根柱子架构约束别跟-agent-讲道理直接拦住它">第二根柱子：架构约束——别跟 Agent 讲道理，直接拦住它</a></li>
          <li><a href="#heading-第三根柱子熵管理ai-生成的代码天然趋向混乱" id="markdown-toc-heading-第三根柱子熵管理ai-生成的代码天然趋向混乱">第三根柱子：熵管理——AI 生成的代码天然趋向混乱</a></li>
        </ul>
      </li>
      <li><a href="#heading-超姐-openai-codexharness-长什么样" id="markdown-toc-heading-超姐-openai-codexharness-长什么样">超姐 OpenAI Codex，Harness 长什么样</a></li>
      <li><a href="#heading-再看看-shopifyceo-亲自上阵用-agent-优化核心系统" id="markdown-toc-heading-再看看-shopifyceo-亲自上阵用-agent-优化核心系统">再看看 Shopify：CEO 亲自上阵用 Agent 优化核心系统</a></li>
      <li><a href="#heading-怎么开始搞从简单到复杂可以定义三个级别" id="markdown-toc-heading-怎么开始搞从简单到复杂可以定义三个级别">怎么开始搞？从简单到复杂可以定义三个级别</a>        <ul>
          <li><a href="#heading-level-1个人开发者" id="markdown-toc-heading-level-1个人开发者">Level 1：个人开发者</a></li>
          <li><a href="#heading-level-2小团队" id="markdown-toc-heading-level-2小团队">Level 2：小团队</a></li>
          <li><a href="#heading-level-3组织级别" id="markdown-toc-heading-level-3组织级别">Level 3：组织级别</a></li>
        </ul>
      </li>
      <li><a href="#heading-在这个时代里代码工程师干的活变了" id="markdown-toc-heading-在这个时代里代码工程师干的活变了">在这个时代里，代码工程师干的活变了</a></li>
      <li><a href="#heading-下面是可能要踩的坑" id="markdown-toc-heading-下面是可能要踩的坑">下面是可能要踩的坑</a></li>
      <li><a href="#heading-harness-也不是万能的" id="markdown-toc-heading-harness-也不是万能的">Harness 也不是万能的</a></li>
      <li><a href="#heading-最后" id="markdown-toc-heading-最后">最后</a></li>
    </ul>
  </li>
</ul>

<h1 id="heading-别再卷模型了套在外面的-harness-才是胜负手">别再卷模型了，套在外面的 Harness 才是胜负手</h1>

<blockquote>
  <p>Humans steer, agents execute. 人类掌舵，智能体执行。</p>
</blockquote>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/2026/03/harness-engineering/01-cover.png" alt="Harness Engineering：人类掌舵，智能体执行" /></p>

<hr />

<h2 id="heading-一个重新理解-ai-工程的发现">一个重新理解 AI 工程的发现</h2>

<p>2025 年大家都在证明一件事：AI Agent 能写代码。到了 2026 年，一个更有意思的事实——Agent 写得好不好，跟模型本身的关系没那么大，跟模型外面那层"壳"的关系才大。</p>

<p>LangChain 的人在 Terminal Bench 2.0 上做了个实验：同一个模型（GPT-5.2-Codex），一行代码没改，只动了模型周围的系统设计——提示词怎么组织、工具怎么调用、中间件怎么插。结果得分从 52.8% 飙到 66.5%，排名从 Top 30 开外直接杀进 Top 5。</p>

<p>OpenAI 自己的 Codex 团队更猛。一个小团队 5 个月时间，用 Agent 生成了超过 100 万行代码——零行手写。不是玩具项目，是可以跑的东西：应用逻辑、文档、CI 配置、可观测性、内部工具，全是 Agent 干的。工程师全程在干嘛呢？在设计让 Agent 能可靠干活的那套系统。</p>

<p>Vercel 他们的数据团队把 Agent 能用的工具从 15 个砍到 2 个——对，你没看错，砍掉了 80%。结果成功率从 80% 跳到 100%，速度快了 3.5 倍，token 还省了 37%。</p>

<p>三个故事都在讲述一个道理：决定 Agent 干活质量的，不是模型多聪明，是你给它搭的环境多靠谱。这个环境，行业里现在叫它 Harness。围绕 Harness 做工程，就是所谓的 Harness Engineering。</p>

<hr />

<h2 id="heading-先说清楚从提示词到-harness我们到底经历了什么">先说清楚：从提示词到 Harness，我们到底经历了什么</h2>

<h3 id="heading-2022-2024提示词工程">2022-2024：提示词工程</h3>

<p>最早大家就是在琢磨怎么"问个好问题"。Chain-of-thought、few-shot、角色扮演……花了两年研究怎么在一个文本框里写出完美指令。有用，但说白了就是一句话的事——你写一段，它回一段。</p>

<h3 id="heading-2025上下文工程">2025：上下文工程</h3>

<p>2025 年 Andrej Karpathy 给了个更精准的说法：上下文工程就是"往上下文窗口里精确地塞对的信息"。他打了个比方——LLM 是 CPU，上下文窗口是 RAM。提示词工程是在 RAM 里写指令，上下文工程是决定 RAM 里该放什么。</p>

<p>这个阶段大家意识到：模型能不能干好活，很大程度取决于它"看到"了什么。同一个模型，上下文给得不一样，输出天差地别。到 2025 年底，"Context Engineer"这个头衔开始正经出现在招聘里了。</p>

<h3 id="heading-2026harness-engineering">2026：Harness Engineering</h3>

<p>但光管"给模型看什么"还不够。当 Agent 不再是一问一答，而是要连续跑五十一百次工具调用、跨十几个文件改代码、自己做架构决策——你还得管它的行为边界、验它的输出、清理它搞出来的烂摊子。</p>

<p>Epsilla 有个特别直觉的比喻：</p>

<ul>
  <li>提示词工程 = 写一封完美的邮件</li>
  <li>上下文工程 = 把该附的文件都附上</li>
  <li>驾驭工程 = 设计整套办公基础设施——工位、流程、权限、审批、质检</li>
</ul>

<p>Philipp Schmid 的比喻更极客一点：Model 是 CPU，Context Window 是 RAM，Harness 是操作系统，Agent 是应用程序。这一下就说清楚了——Harness 不是模型本身，也不是上层应用，是让一切跑起来的那个操作系统层。</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/2026/03/harness-engineering/02-three-evolutions.png" alt="从提示词到驾驭工程：三次范式跃迁" /></p>

<hr />

<h2 id="heading-所以-harness-到底是啥">所以 Harness 到底是啥？</h2>

<p>Harness 这词来自马具——套在马身上的缰绳和鞍具。不是让马跑更快，而是把马的力量引到你想去的方向。</p>

<p>放到 AI 这边：模型是引擎，Harness 是整辆车。</p>

<p>具体一点，一个好的 Harness 干四件事：</p>

<ol>
  <li><strong>约束</strong>——Agent 能干啥不能干啥（架构边界、依赖规则、文件权限）</li>
  <li><strong>告知</strong>——Agent 该干啥（上下文、文档、代码规范）</li>
  <li><strong>验证</strong>——Agent 干对没有（测试、linter、CI）</li>
  <li><strong>纠正</strong>——干错了怎么修（反馈回路、自修复机制）</li>
</ol>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/2026/03/harness-engineering/03-computer-metaphor.jpg" alt="模型是CPU，驾驭是操作系统" /></p>

<p>有人会问：这跟 SDK、框架有啥区别？SDK 和框架解决的是"怎么构建 Agent"，Harness 解决的是"Agent 怎么跑"。Claude Code、Codex CLI、LangChain DeepAgents 这些东西，本身就是 Harness 的实现——它们提供了 prompt 预设、工具调用的默认处理、生命周期钩子、子 Agent 管理这些现成能力。</p>

<hr />

<h2 id="heading-其实-harness-比模型更重要">其实 Harness 比模型更重要</h2>

<h3 id="heading-langchain模型没换排名从-30-开外冲进-top-5">LangChain：模型没换，排名从 30 开外冲进 Top 5</h3>

<p>同一个 GPT-5.2-Codex，只改 harness：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">指标</th>
      <th style="text-align: left">改之前</th>
      <th style="text-align: left">改之后</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Terminal Bench 2.0</td>
      <td style="text-align: left">52.8%</td>
      <td style="text-align: left">66.5%</td>
    </tr>
    <tr>
      <td style="text-align: left">排名</td>
      <td style="text-align: left">Top 30 开外</td>
      <td style="text-align: left">Top 5</td>
    </tr>
  </tbody>
</table>

<p>他们做了三件事：加了个 Self-Verification Loop（提交前强制自测），写了个 Loop Detection（检测 Agent 在同一个文件上反复改来改去的死循环），优化了启动时的上下文注入。</p>

<p>有意思的是他们发现了个叫"Reasoning Sandwich"的技巧：不让模型全程开最高推理（那样会超时，只有 53.9%），而是规划阶段用最高推理、写码阶段用中等、验证阶段再拉回最高。跟人脑一个道理——想方案时深思熟虑，写代码时进入心流，review 时再切回审慎模式。这一招直接拿到 66.5%。</p>

<h3 id="heading-vercel砍掉-80-的工具成功率反而到了-100">Vercel：砍掉 80% 的工具，成功率反而到了 100%</h3>

<p>原来有 15 个工具（schema lookup、query validation、error recovery 之类的），团队后来发现自己是在"替模型思考"——帮它过滤信息、帮它规划路径。结果砍到只剩 bash 和 SQL 两个工具：</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">指标</th>
      <th style="text-align: left">15 个工具</th>
      <th style="text-align: left">2 个工具</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">成功率</td>
      <td style="text-align: left">80%</td>
      <td style="text-align: left">100%</td>
    </tr>
    <tr>
      <td style="text-align: left">执行时间</td>
      <td style="text-align: left">274.8 秒</td>
      <td style="text-align: left">77.4 秒</td>
    </tr>
    <tr>
      <td style="text-align: left">Token</td>
      <td style="text-align: left">~102k</td>
      <td style="text-align: left">~61k</td>
    </tr>
    <tr>
      <td style="text-align: left">步骤数</td>
      <td style="text-align: left">~12</td>
      <td style="text-align: left">~7</td>
    </tr>
  </tbody>
</table>

<p>最夸张的对比：旧系统最差情况跑了 724 秒、烧了 14.5 万 token 还失败了；新系统干同一个查询只用 141 秒、6.7 万 token，成功。</p>

<p>他们总结了一句特别到位的话：<strong>"当我们不再替模型做选择时，模型反而做出了更好的选择。"</strong></p>

<p>不过有个前提——他们的语义层文档写得很好。他们自己也说了：如果你的数据层一团糟，给 Claude 直接文件访问也救不了你。</p>

<h3 id="heading-nate-b-jones同一个模型差的-harness-42好的-78">Nate B Jones：同一个模型，差的 harness 42%，好的 78%</h3>

<p>差距接近一倍。Harness 带来的影响是模型原始能力的两倍。</p>

<h3 id="heading-openai5-个月100-万行代码零行手写">OpenAI：5 个月，100 万行代码，零行手写</h3>

<p>一个小团队，全程 depth-first——把大目标拆成小积木块，让 Agent 搭积木，再用积木解锁更复杂的东西。他们最后说了句让我印象很深的话：</p>

<blockquote>
  <p>"我们最大的挑战不在于模型的能力，而在于设计环境、反馈回路和控制系统。严谨的工程设计并没有因为 AI 而变得简单——它变得更重要了。"</p>
</blockquote>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/2026/03/harness-engineering/04-data-comparison.jpg" alt="同一个模型，不同的驾驭" /></p>

<h3 id="heading-有这么一个道理约束越多agent-反而表现越好">有这么一个道理：约束越多，Agent 反而表现越好</h3>

<p>我们总觉得给 AI 更多自由它就能发挥得更好。但实际上限制了解空间，Agent 才能在有限范围内找到正确答案，而不是在无穷可能性里迷路。就像 TypeScript 比 JavaScript 多了一堆"限制"，但正是这些限制让你写出更靠谱的代码。</p>

<p>HumanLayer 的 Kyle 说得更直白：Agent 出问题时，第一反应别想着等更强的模型，先检查你的配置。这不是模型问题，是配置问题。</p>

<p>就像高速公路上有护栏你反而敢开快，Agent 在好的约束下反而能更自主——因为 harness 替它兜底了。</p>

<hr />

<h2 id="heading-harness的三大支柱">Harness的三大支柱</h2>

<p>OpenAI 的实践加上 Birgitta Böckeler 在 Martin Fowler 博客上的分析，Harness 可以拆成三根柱子。</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/2026/03/harness-engineering/05-three-pillars.png" alt="三大核心支柱：上下文工程、架构约束、熵管理" /></p>

<h3 id="heading-第一根柱子上下文工程agent-看不到的东西等于不存在">第一根柱子：上下文工程——Agent 看不到的东西等于不存在</h3>

<p>你团队的 Google Docs、Slack 里的默契、大家脑子里那些"这个大家都知道"的东西——Agent 统统看不到。它能看到的只有仓库里的文件和运行时能拿到的信息。</p>

<p>所以核心就一句话：一切知识必须编码到仓库中。</p>

<p><strong>静态上下文</strong>就是 Agent 的"入职文档"：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>项目根目录/
├── AGENTS.md          # Agent 行为指南
├── CLAUDE.md          # Claude Code 专用指南
├── ARCHITECTURE.md    # 架构决策记录
├── docs/
│   ├── api-design.md
│   ├── patterns.md
│   └── decisions/
</code></pre></div></div>

<p>AGENTS.md 是 2026 年冒出来的一个好东西——面向 AI Agent 的 README，Claude Code、Cursor、Codex 都认。HumanLayer 建议控制在 60 行以内。为啥？上下文窗口是有限资源，塞太多模型反而会迷失。</p>

<p>这些文件该告诉 Agent 什么？技术栈和版本、代码风格的硬规则（不是"写得优雅"，是"函数不超过 50 行"）、哪些模式推荐哪些禁止、测试怎么跑、提交规范是啥。</p>

<p><strong>动态上下文</strong>是运行时感知：监控数据、CI 状态、目录结构映射。LangChain 的 LocalContextMiddleware 会在启动时自动扫描项目结构和可用工具注入进去，这个挺聪明的。</p>

<p>还有个重要理念叫<strong>渐进式披露</strong>——别在启动时把所有信息一股脑塞给 Agent，让信息随着任务推进按需浮现。MCP Servers、Skills、Sub-Agents 都是做这个的。</p>

<p>有条反直觉的原则值得记住：成功应该是沉默的，只有失败才需要产出。早期有人跑完整测试套件，5 分钟的通过结果全丢给 Agent，那些无关信息反而淹没了真正需要关注的失败用例。</p>

<p>还有一点——写得好的代码本身就是最好的文档。清晰的命名、一致的模式、合理的抽象，比注释更能帮 Agent 理解你的意图。Böckeler 甚至预测：未来开发会趋向更少的、对 AI 更友好的技术栈——不是为了人类偏好，是为了 Agent 好维护。</p>

<h3 id="heading-第二根柱子架构约束别跟-agent-讲道理直接拦住它">第二根柱子：架构约束——别跟 Agent 讲道理，直接拦住它</h3>

<p>团队里的代码规范如果靠人自觉都经常破防，Agent 就更不会自觉了。必须把约束变成可执行的检查。</p>

<p>约束分四层：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>第一层：确定性 Linter（ESLint、checkstyle、golangci-lint）
第二层：结构化测试（ArchUnit——Service 层不能直接调 Repository）
第三层：自定义 Linter（禁止在 Controller 层写业务逻辑）
第四层：LLM 审计 Agent（用 AI 审查 AI 的输出）
</code></pre></div></div>

<p>最简单有效的是 pre-commit hooks。Agent 提交代码时 hook 自动跑，不通过就不让提交。Agent 不需要"理解"规则，被规则拦住就行了。</p>

<p>HumanLayer 管这叫 <strong>Back-Pressure</strong>——测试、类型检查、linter 形成一种反压力，让 Agent 犯错后能"感知"到错误并自我修正。这比让 Agent 自己评估靠谱多了——Anthropic 的研究表明模型自评时过于宽容，很容易说服自己 bug 不严重。</p>

<p><strong>Loop Detection</strong> 也很关键。LangChain 发现 Agent 经常在同一个文件上改了又改，每次改一点每次还失败，陷入死循环。他们的解法很简单：追踪每个文件编辑次数，超过 N 次就中断，提示 Agent 换个思路。简单但管用。</p>

<p><strong>Sub-Agents</strong> 做任务隔离也很重要。Agent 跑久了会出现"上下文腐化"——上下文越来越长，性能越来越差。Sub-Agent 就是一道防火墙，子任务的中间噪音不会污染主 Agent 的上下文。</p>

<p>举个真实例子：OpenAI 发现 Agent 老喜欢把所有逻辑塞一个函数里。他们没去"教育"Agent，直接写了个 linter——函数超过 50 行就报错。结果 Agent 学会了拆函数。不是因为它理解了为什么要拆，是因为不拆过不了检查。</p>

<h3 id="heading-第三根柱子熵管理ai-生成的代码天然趋向混乱">第三根柱子：熵管理——AI 生成的代码天然趋向混乱</h3>

<p>OpenAI 团队以前每周五花 20% 时间清理"AI 垃圾"——重复代码、不一致的命名、偏离模式的实现。太痛苦了。后来他们想通了：把"黄金规则"编码到仓库里，然后用专门的 Agent 定期巡检。</p>

<p>Böckeler 在 Martin Fowler 博客上专门强调了这一点，引用了 OpenAI 的原则："When the agent struggles, we treat it as a signal"——Agent 挣扎的地方，就是 harness 需要加强的地方。</p>

<p>垃圾回收 Agent 干的事很直接：查文档跟代码是否一致、找偏离约定模式的代码、识别冗余实现、检查依赖是否过时。可以定时跑、PR 合并后跑、或者发现问题后针对性扫。</p>

<p>Philipp Schmid 还指出了一个新问题叫 <strong>Agent Drift</strong>——模型跑久了会慢慢偏离指令，像车慢慢跑偏一样。排行榜上 1% 的差距根本测不出来这种东西。所以 Harness 不光要在开始前设好规则，还要全程持续监测纠偏。</p>

<p>有个重要的反馈循环：垃圾回收 Agent 反复发现同一类问题时，说明你的约束不够。这时候该回到第二根柱子去补规则，别每次手动修。</p>

<hr />

<h2 id="heading-超姐-openai-codexharness-长什么样">超姐 OpenAI Codex，Harness 长什么样</h2>

<p>OpenAI 把 Codex 的内部架构细节公开了，可以看看一个生产级 Harness 的真实面貌。</p>

<p>核心其实是一个特别简单的循环：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 接收输入，构造 prompt
2. 模型生成响应——要么是最终回答，要么是工具调用
3. 如果是工具调用，执行它，把结果追加回 prompt
4. 回到第 2 步，直到模型不再调用工具
</code></pre></div></div>

<p>就这么简单。但围绕这个循环搭的 Harness，才是真正的工程量。</p>

<p>Codex 跑在 CLI、VS Code、Web、macOS 桌面、JetBrains、Xcode 这些界面上，底下统一由一个 <strong>Codex App Server</strong> 驱动——基于 JSON-RPC 的双向 API。它抽象了三个核心概念：</p>

<ul>
  <li><strong>Item</strong>——输入输出的原子单位（消息、工具执行、审批请求、diff）</li>
  <li><strong>Turn</strong>——一次完整的 Agent 工作序列</li>
  <li><strong>Thread</strong>——持久化的会话容器，支持恢复、分叉、归档</li>
</ul>

<p>还有个审批机制：Agent 要执行有风险的操作时，Server 会暂停并向客户端发审批请求，人点了"允许"才继续。自动化的同时人类始终有最终控制权。</p>

<hr />

<h2 id="heading-再看看-shopifyceo-亲自上阵用-agent-优化核心系统">再看看 Shopify：CEO 亲自上阵用 Agent 优化核心系统</h2>

<p>Shopify CEO Tobi Lütke 2025 年发了份内部备忘录——"AI 使用是所有人的基线期望"，要加人先证明 AI 干不了这活。八个月后 Box、Fiverr 甚至加拿大总理都跟进了。</p>

<p>但 Lütke 不只是发备忘录，他自己上手做了个示范。</p>

<p>他用了 Karpathy 的 Autoresearch 方法——让 coding agent 半自主跑大量实验，对 Shopify 的核心模板引擎 Liquid 做性能优化。写了个 autoresearch.md 提示文件加配套脚本，让 Agent 自动跑测试报 benchmark。跑了大概 120 次实验，产出 93 次提交：</p>

<ul>
  <li>parse+render 快了 53%</li>
  <li>内存分配少了 61%</li>
</ul>

<p>最大的单项优化是用 <code class="language-plaintext highlighter-rouge">String#byteindex</code> 替代正则 tokenizer，单独贡献了 12% 的解析提速。</p>

<p>这不是玩具。Liquid 驱动着 Shopify 560 万活跃店铺的前端渲染。CEO 亲自用 AI 对核心基础设施做出了有意义的改进——但关键不在于他用了多牛的模型，而在于他搭了一个严格的实验框架：有明确的 benchmark、自动化的测试运行器、结构化的结果收集。这就是一个轻量但有效的 harness。</p>

<hr />

<h2 id="heading-怎么开始搞从简单到复杂可以定义三个级别">怎么开始搞？从简单到复杂可以定义三个级别</h2>

<h3 id="heading-level-1个人开发者">Level 1：个人开发者</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ 项目根目录放个 CLAUDE.md / AGENTS.md（控制在 60 行以内）
✅ 配好 pre-commit hooks（lint + type check + 测试）
✅ 写好测试套件
✅ 保持仓库整洁、命名一致
</code></pre></div></div>

<p>最低配的 harness，但已经能明显提升 Agent 输出质量。CLAUDE.md 长这样就够了：</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># CLAUDE.md</span>

<span class="gu">## 技术栈</span>
<span class="p">-</span> Java 21 + Spring Boot 3.3
<span class="p">-</span> PostgreSQL + MyBatis-Plus
<span class="p">-</span> Gradle 构建

<span class="gu">## 代码规范</span>
<span class="p">-</span> Service 层方法必须有单元测试
<span class="p">-</span> Controller 层只做参数校验和响应包装，不写业务逻辑
<span class="p">-</span> 所有数据库查询走 Repository 层
<span class="p">-</span> 函数不超过 50 行

<span class="gu">## 测试命令</span>
./gradlew test

<span class="gu">## 禁止事项</span>
<span class="p">-</span> 不要用 lombok 的 @Data，用 @Getter @Setter
<span class="p">-</span> 不要在 Service 层直接拼 SQL
<span class="p">-</span> 不要 catch Exception，必须 catch 具体异常类型
</code></pre></div></div>

<h3 id="heading-level-2小团队">Level 2：小团队</h3>

<p>Level 1 基础上加：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ 团队统一的 AGENTS.md（版本化管理）
✅ CI 里集成架构约束检查（ArchUnit 之类的）
✅ 共享的代码模式模板
✅ PR 审查加上 Agent 输出质量检查
✅ Loop detection（防止 Agent 在同一问题上原地打转）
</code></pre></div></div>

<h3 id="heading-level-3组织级别">Level 3：组织级别</h3>

<p>Level 2 基础上加：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✅ 自定义中间件（拦截和约束 Agent 行为）
✅ 可观测性集成（监控 Agent 效果、检测 Agent Drift）
✅ 仪表盘（追踪成功率、Token 消耗、执行时间）
✅ 垃圾回收 Agent 定期巡检
✅ Harness 模板库（不同项目用不同模板）
✅ Agent 运行轨迹采集
</code></pre></div></div>

<p>最后一点 Philipp Schmid 特别强调了它的战略价值：竞争优势正在从 prompt 转向"捕获的运行轨迹"。Agent 每次在长工作流中跑偏，都是一条有价值的训练数据。</p>

<hr />

<h2 id="heading-在这个时代里代码工程师干的活变了">在这个时代里，代码工程师干的活变了</h2>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/blog/2026/03/harness-engineering/06-role-shift.jpg" alt="工程师的新角色：从埋头写代码到指挥Agent编队" /></p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">以前</th>
      <th style="text-align: left">现在</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">手写代码</td>
      <td style="text-align: left">设计约束和验证规则</td>
    </tr>
    <tr>
      <td style="text-align: left">Debug 代码</td>
      <td style="text-align: left">分析 Agent 行为模式</td>
    </tr>
    <tr>
      <td style="text-align: left">写文档给人看</td>
      <td style="text-align: left">写文档给 Agent 看</td>
    </tr>
    <tr>
      <td style="text-align: left">学新语言新框架</td>
      <td style="text-align: left">设计 Agent 能理解的架构</td>
    </tr>
    <tr>
      <td style="text-align: left">代码审查</td>
      <td style="text-align: left">Harness 审查</td>
    </tr>
    <tr>
      <td style="text-align: left">管团队协作</td>
      <td style="text-align: left">管 Agent 编排</td>
    </tr>
  </tbody>
</table>

<p>这不是说不用会写代码了——恰恰相反，你需要更深的架构能力和系统思维才能设计好 harness。Karpathy 说 2026 年是 Agentic Engineering 时代，人类不再写大部分代码，而是指挥、监督、编排 Agent。就像交响乐指挥不亲自演奏每件乐器，但整场演出的质量由他决定。</p>

<p>换句话说：工程师不再直接解决问题，工程师解决的是 Agent 在解决问题时遇到的问题。你的交付物不是代码，而是一个让 Agent 持续可靠产出代码的系统。</p>

<hr />

<h2 id="heading-下面是可能要踩的坑">下面是可能要踩的坑</h2>

<p><strong>过度设计控制流。</strong> 给 Agent 搞了个巨复杂的状态机，结果模型一升级全废了。Manus 6 个月重构了 5 次 harness，LangChain 一年重架构了 3 次。正解是 Philipp Schmid 说的"Build to Delete"——设计时就预期会被替换，保持模块化，给强的原子工具让模型自己规划。</p>

<p><strong>写了 AGENTS.md 就再也不更新。</strong> Harness 是活的，Agent 每次犯错都问一句：这个错误能用什么约束预防？然后把答案编码进去。</p>

<p><strong>文档太模糊。</strong> "代码要写得优雅" "注意性能"——这种话 Agent 理解不了。"函数不超过 50 行"比"函数要简短"好一万倍。</p>

<p><strong>Agent 写了烂代码，人肉修完拉倒。</strong> 不去想为什么。OpenAI 的原则说得好：Agent 挣扎的地方就是 harness 需要加强的地方。每次犯错都是改进的机会。</p>

<p><strong>知识留在仓库外面。</strong> 架构决策记在 Confluence，规范写在 Notion 里。Agent 只能看到仓库，一切知识必须在仓库内。</p>

<p><strong>一次性塞太多上下文。</strong> CLAUDE.md 写了几百行，测试输出全丢给 Agent。上下文是有限资源，让信息按需加载。聚焦 20 分钟的 session 胜过漫无边际的 2 小时。</p>

<p><strong>让 Agent 自己评价自己。</strong> Anthropic 研究表明模型自评时太宽容，容易说服自己 bug 不严重。必须用确定性的外部工具（测试、linter）做验证。要用 LLM 审查就得用独立的、配了严格标准的评估 Agent。</p>

<hr />

<h2 id="heading-harness-也不是万能的">Harness 也不是万能的</h2>

<p><strong>只对能验证的任务管用。</strong> 代码能跑、测试能过、类型能检查——这些 harness 很擅长。但产品设计决策、用户体验判断、创意工作？缺确定性验证手段的场景，harness 能帮多少还是个问号。</p>

<p><strong>功能正确性有缺口。</strong> Böckeler 分析 OpenAI 的实践时指出：他们的内部质量指标都过了（风格一致、架构合规），但没验证"代码做的是不是对的事"。Agent 可能完美遵守所有规则，写出风格一致测试通过的代码，但它理解的需求跟产品团队想要的是两回事。</p>

<p><strong>现在的 harness 还挺脆弱。</strong> 大部分是 linter 规则、CI 脚本和自定义 hook 拼出来的，换个模型版本或 Agent 工具可能就得大面积改。这个领域还没有出现类似 Kubernetes 之于容器编排那样的标准化层。</p>

<p><strong>组织层面才是真正的挑战。</strong> 2026 年 Deer Valley 研讨会有份报告说得很直接：技术再好，如果团队没有维护 harness 的意愿和能力，它很快就会过时。</p>

<p><strong>更大的上下文窗口不等于更好。</strong> 信息多了不代表模型更擅长从中找到有用的。塞太多反而让模型更难聚焦，这也是为什么按需加载比一次性全给更有效。</p>

<hr />

<h2 id="heading-最后">最后</h2>

<p>一句话总结：别再只想着让 AI 更聪明了，让它的运行环境更聪明。</p>

<p>2025 年我们卷的是"哪个模型更强"，2026 年我们发现真正的竞争力在于谁的 harness 更好。排行榜上模型差距越来越小，但真实场景里的表现差距越来越大——差距全在 harness 里。</p>

<p>三根柱子：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>上下文工程  → 让 Agent 看到该看的
架构约束   → 让 Agent 不做不该做的
熵管理    → 让系统不会越跑越烂
</code></pre></div></div>

<p>三个数据：</p>

<ul>
  <li>LangChain 同模型提升 26%，杀进 Top 5</li>
  <li>Vercel 砍 80% 工具，成功率从 80% 到 100%</li>
  <li>好 harness 比差 harness 高出近一倍</li>
</ul>

<p>这不是锦上添花，这是 AI Agent 时代写软件的基本功。</p>

<p>OpenAI 那句话我反复读了好几遍，拿来收尾刚好：</p>

<blockquote>
  <p>"严谨的工程设计并没有因为 AI 而变得简单——它变得更重要了。"</p>
</blockquote>

<hr />

<p><em>参考资料：</em></p>

<ul>
  <li><em><a href="https://openai.com/index/harness-engineering/">OpenAI, "Harness engineering: leveraging Codex in an agent-first world", 2026</a></em></li>
  <li><em><a href="https://openai.com/index/unlocking-the-codex-harness/">OpenAI, "Unlocking the Codex harness: how we built the App Server", 2026</a></em></li>
  <li><em><a href="https://openai.com/index/unrolling-the-codex-agent-loop/">OpenAI, "Unrolling the Codex agent loop", 2026</a></em></li>
  <li><em><a href="https://martinfowler.com/articles/exploring-gen-ai/harness-engineering.html">Birgitta Böckeler (Martin Fowler's blog), "Harness Engineering", Feb 2026</a></em></li>
  <li><em><a href="https://blog.langchain.com/improving-deep-agents-with-harness-engineering/">LangChain, "Improving Deep Agents with Harness Engineering", 2026</a></em></li>
  <li><em><a href="https://vercel.com/blog/we-removed-80-percent-of-our-agents-tools">Vercel, "We removed 80% of our agent's tools", 2026</a></em></li>
  <li><em><a href="https://www.philschmid.de/agent-harness-2026">Philipp Schmid, "The importance of Agent Harness in 2026"</a></em></li>
  <li><em><a href="https://www.humanlayer.dev/blog/skill-issue-harness-engineering-for-coding-agents">HumanLayer, "Skill Issue: Harness Engineering for Coding Agents"</a></em></li>
  <li><em><a href="https://www.epsilla.com/blogs/harness-engineering-evolution-prompt-context-autonomous-agents">Epsilla, "The Third Evolution: Why Harness Engineering Replaced Prompting in 2026"</a></em></li>
  <li><em><a href="https://x.com/karpathy/status/1937902205765607626">Andrej Karpathy on Context Engineering</a></em></li>
  <li><em><a href="https://github.com/Shopify/liquid/pull/2056">Shopify/liquid PR #2056: Performance: 53% faster parse+render</a></em></li>
  <li><em><a href="https://www.natebjones.com/">Nate B Jones, Agent Harness benchmark research, Mar 2026</a></em></li>
  <li><em><a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">Anthropic, "Demystifying evals for AI agents", 2026</a></em></li>
</ul>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="AI" /><category term="AI" /><category term="Agent" /><category term="Harness Engineering" /><category term="工程方法论" /><category term="Context Engineering" /><summary type="html"><![CDATA[同一个模型，换个 harness，成功率从 42% 干到 78%。2026 年我们终于想明白了：Agent 不是难点，Harness 才是。]]></summary></entry><entry><title type="html">AI Agent 生态的层次思考</title><link href="https://sut-gc.github.io/blog/2026/03/17/ai-agent-%E7%94%9F%E6%80%81%E7%9A%84%E5%B1%82%E6%AC%A1%E6%80%9D%E8%80%83/" rel="alternate" type="text/html" title="AI Agent 生态的层次思考" /><published>2026-03-17T00:00:00+00:00</published><updated>2026-03-17T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/03/17/ai-agent-%E7%94%9F%E6%80%81%E7%9A%84%E5%B1%82%E6%AC%A1%E6%80%9D%E8%80%83</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/03/17/ai-agent-%E7%94%9F%E6%80%81%E7%9A%84%E5%B1%82%E6%AC%A1%E6%80%9D%E8%80%83/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-ai-agent-生态的层次思考" id="markdown-toc-heading-ai-agent-生态的层次思考">AI Agent 生态的层次思考</a>    <ul>
      <li><a href="#heading-引言" id="markdown-toc-heading-引言">引言</a></li>
      <li><a href="#heading-完整的层次结构" id="markdown-toc-heading-完整的层次结构">完整的层次结构</a></li>
      <li><a href="#heading-各层详解" id="markdown-toc-heading-各层详解">各层详解</a>        <ul>
          <li><a href="#heading-基建层" id="markdown-toc-heading-基建层">基建层</a></li>
          <li><a href="#heading-模型推理服务层" id="markdown-toc-heading-模型推理服务层">模型推理服务层</a></li>
          <li><a href="#heading-llm-api-层" id="markdown-toc-heading-llm-api-层">LLM API 层</a></li>
          <li><a href="#heading-provider-sdk-层" id="markdown-toc-heading-provider-sdk-层">Provider SDK 层</a></li>
          <li><a href="#heading-agent-loop-层" id="markdown-toc-heading-agent-loop-层">Agent Loop 层</a></li>
          <li><a href="#heading-agent-sdk-层" id="markdown-toc-heading-agent-sdk-层">Agent SDK 层</a></li>
          <li><a href="#heading-agent-runtime-层" id="markdown-toc-heading-agent-runtime-层">Agent Runtime 层</a></li>
          <li><a href="#heading-产品层" id="markdown-toc-heading-产品层">产品层</a></li>
        </ul>
      </li>
      <li><a href="#heading-关于现有框架的定位" id="markdown-toc-heading-关于现有框架的定位">关于现有框架的定位</a></li>
      <li><a href="#heading-一个有趣的类比" id="markdown-toc-heading-一个有趣的类比">一个有趣的类比</a></li>
      <li><a href="#heading-结语" id="markdown-toc-heading-结语">结语</a></li>
    </ul>
  </li>
</ul>

<h1 id="heading-ai-agent-生态的层次思考">AI Agent 生态的层次思考</h1>

<blockquote>
  <p>当我们谈论 AI Agent 的时候，我们到底在谈论什么？</p>
</blockquote>

<hr />

<h2 id="heading-引言">引言</h2>

<p>过去一年，AI Agent 的概念被反复提及，但大多数讨论都停留在"某某框架很好用"、"某某产品很火"的层面。很少有人从<strong>生态层次</strong>的角度，去思考这个领域的整体结构。</p>

<p>本文尝试回答一个问题：<strong>AI Agent 生态，从底层硬件到用户交互，一共分几层？每层解决什么问题？</strong></p>

<p>这不是一篇框架对比文章，而是一次关于生态结构的思考。</p>

<hr />

<h2 id="heading-完整的层次结构">完整的层次结构</h2>

<table>
  <thead>
    <tr>
      <th>层次</th>
      <th>主要解决的问题</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>产品层</td>
      <td>用户怎么用？用什么界面交互？怎么让非技术用户也能扩展能力？</td>
    </tr>
    <tr>
      <td>Agent Runtime 层</td>
      <td>怎么让 Agent 持续运行？怎么管理长期记忆、Skill、任务队列？</td>
    </tr>
    <tr>
      <td>Agent SDK 层</td>
      <td>怎么编排多个 Agent？怎么注册 Tool？怎么封装 MCP？怎么保证结构化输出？</td>
    </tr>
    <tr>
      <td>Agent Loop 层</td>
      <td>怎么实现一次完整的推理闭环？什么时候调 Tool，什么时候返回结果？</td>
    </tr>
    <tr>
      <td>Provider SDK 层</td>
      <td>怎么屏蔽不同 LLM 的 API 差异？怎么做到切换模型不改业务代码？</td>
    </tr>
    <tr>
      <td>LLM API</td>
      <td>提供标准的语言理解和生成能力</td>
    </tr>
    <tr>
      <td>模型推理服务层</td>
      <td>怎么把模型跑在本地？怎么优化推理速度和成本？</td>
    </tr>
    <tr>
      <td>基建层</td>
      <td>算力从哪来？怎么保证稳定性和扩展性？</td>
    </tr>
  </tbody>
</table>

<p>用一张图来看整体结构：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────┐
│                  产品层                       │
│        OpenClaw / Claude Code / Cursor       │
├─────────────────────────────────────────────┤
│              Agent Runtime 层                │
│       长期记忆 / Skill 管理 / 任务队列        │
├─────────────────────────────────────────────┤
│               Agent SDK 层                   │
│  Tool 注册 / MCP 封装 / 多 Agent 编排         │
│  Eino / pydantic-ai / LangChain / Agents SDK │
├─────────────────────────────────────────────┤
│              Agent Loop 层                   │
│         LLM ↔ Tool 推理闭环循环               │
├─────────────────────────────────────────────┤
│             Provider SDK 层                  │
│   OpenAI SDK / Anthropic SDK / LiteLLM       │
├─────────────────────────────────────────────┤
│                LLM API                       │
│   OpenAI / Anthropic / Gemini / DeepSeek     │
├─────────────────────────────────────────────┤
│             模型推理服务层                     │
│          Ollama / vLLM / Triton              │
├─────────────────────────────────────────────┤
│                 基建层                        │
│          GPU / TPU / 云计算基础设施            │
└─────────────────────────────────────────────┘
</code></pre></div></div>

<p>下面逐层展开。</p>

<hr />

<h2 id="heading-各层详解">各层详解</h2>

<h3 id="heading-基建层">基建层</h3>

<p><strong>核心问题：算力从哪来？</strong></p>

<p>这是整个生态的物理基础。GPU、TPU、网络、存储，构成了 AI 时代的"电力和公路"。对于大多数开发者来说，这层是黑盒——要么用云厂商托管（AWS、GCP、Azure），要么自建机房。</p>

<p>这层的问题不是软件问题，是资源问题。</p>

<hr />

<h3 id="heading-模型推理服务层">模型推理服务层</h3>

<p><strong>核心问题：怎么把模型跑起来？</strong></p>

<p>当你不想依赖云端 API，想把模型跑在自己的机器上时，就需要这一层。Ollama、vLLM、Triton 是代表性的工具。</p>

<p>这层解决的问题是：</p>
<ul>
  <li>模型加载和版本管理</li>
  <li>推理速度优化（量化、批处理）</li>
  <li>对外暴露统一的 API 接口</li>
</ul>

<p>值得注意的是，这层通常会模拟 OpenAI 的 API 格式，使得上层无需感知模型是云端还是本地。</p>

<hr />

<h3 id="heading-llm-api-层">LLM API 层</h3>

<p><strong>核心问题：提供标准的语言理解和生成能力。</strong></p>

<p>OpenAI、Anthropic、Google Gemini、DeepSeek、通义千问……这些是 LLM API 的提供者。</p>

<p>这层本身不是开发者"建造"的，而是开发者"使用"的。但它是整个生态的核心驱动力——模型能力的提升，会直接影响上面所有层的可能性边界。</p>

<hr />

<h3 id="heading-provider-sdk-层">Provider SDK 层</h3>

<p><strong>核心问题：怎么屏蔽不同 LLM 之间的 API 差异？</strong></p>

<p>每家 LLM 的 API 格式、鉴权方式、streaming 实现、tool call 格式都不一样。如果业务代码直接调用各家 SDK，切换模型就意味着大量重写。</p>

<p>这层的代表就是各家官方 SDK：</p>
<ul>
  <li>OpenAI SDK</li>
  <li>Anthropic SDK</li>
  <li>豆包 SDK</li>
  <li>Gemini SDK</li>
</ul>

<p>以及一些统一封装的库，它们把不同 Provider 的差异抹平，让上层只需要关心"我要调用一个 LLM"，而不是"我要调用 OpenAI 的 GPT-4o"。</p>

<hr />

<h3 id="heading-agent-loop-层">Agent Loop 层</h3>

<p><strong>核心问题：怎么实现一次完整的推理闭环？</strong></p>

<p>这是 Agent 的最小工作单元，本质上是一个循环：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>调用 LLM
  ↓
有 tool call？
  ↓ 是          ↓ 否
执行 tool      返回结果
  ↓
再次调用 LLM
  ↓
（重复直到结束）
</code></pre></div></div>

<p>这一层只知道"tool"这个抽象概念，不知道 MCP，不知道 Skill，不知道记忆。它只负责把一次推理任务跑完。</p>

<p><strong>关键点</strong>：tool 从哪来、怎么注册，不是 Loop 层的问题，是上层传进来的。Loop 层只负责"执行"。</p>

<hr />

<h3 id="heading-agent-sdk-层">Agent SDK 层</h3>

<p><strong>核心问题：怎么方便地构建和编排 Agent？</strong></p>

<p>在裸 Loop 之上，SDK 层提供了更丰富的能力：</p>

<ul>
  <li><strong>Tool 注册和管理</strong>：让开发者方便地把函数变成 tool</li>
  <li><strong>MCP 封装</strong>：把 MCP server 包装成 tool，让 Loop 无感知</li>
  <li><strong>Structured Output</strong>：保证模型输出符合预期的数据结构</li>
  <li><strong>多 Agent 编排</strong>：让多个 Agent 协作完成复杂任务</li>
</ul>

<p>这层是大多数"AI 框架"存在的位置，比如 Eino、pydantic-ai、LangChainGo。</p>

<p><strong>多 Agent 编排</strong>值得单独说一下。单个 Agent 能力有限，复杂任务需要多个专门的 Agent 协作。SDK 层提供了两种典型模式：</p>

<ul>
  <li><strong>Transfer</strong>：把任务完全交给另一个 Agent，当前 Agent 退出（类比转接电话）</li>
  <li><strong>AgentAsTool</strong>：把另一个 Agent 当做 Tool 调用，拿到结果后继续处理（类比派人办事）</li>
</ul>

<hr />

<h3 id="heading-agent-runtime-层">Agent Runtime 层</h3>

<p><strong>核心问题：怎么让 Agent 持续运行，而不是一次性的？</strong></p>

<p>这是目前生态中<strong>最缺失、最值得关注</strong>的一层。</p>

<p>Agent SDK 解决的是"单次运行"的问题：给定输入，跑一次，输出结果。但真正有价值的 Agent 应该是<strong>持续运行的</strong>，它需要：</p>

<ul>
  <li><strong>长期记忆</strong>：记住上周和你说过的事</li>
  <li><strong>Skill 管理</strong>：知道自己能做什么，动态加载新能力</li>
  <li><strong>任务队列</strong>：有积压的任务要处理</li>
  <li><strong>Heartbeat 机制</strong>：定时醒来检查是否有新任务</li>
  <li><strong>状态持久化</strong>：崩溃重启后能恢复到之前的状态</li>
</ul>

<p>目前这层没有一个独立的、被广泛采用的解决方案。OpenClaw 这类产品顺带做了这件事，但它同时也在做产品层的事。</p>

<p><strong>这层的空缺，是当前生态最大的机会之一。</strong></p>

<hr />

<h3 id="heading-产品层">产品层</h3>

<p><strong>核心问题：怎么让用户真正用起来？</strong></p>

<p>这层面向的是终端用户，不是开发者。它解决的问题是：</p>

<ul>
  <li>用什么界面交互（消息 App、Web、CLI）</li>
  <li>怎么让非技术用户也能扩展 Agent 的能力</li>
  <li>怎么设计产品体验</li>
</ul>

<p>OpenClaw 是这层的代表：通过 WhatsApp、Telegram 等消息 App 作为入口，普通用户发一条消息就能让 Agent 执行复杂任务。它在中国引发的"养龙虾"热潮，说明这层产品的爆发力。</p>

<hr />

<h2 id="heading-关于现有框架的定位">关于现有框架的定位</h2>

<p>一个容易犯的错误是把某个框架"钉"在某一层。实际上，<strong>大多数框架都是垂直跨层的</strong>：</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th style="text-align: center">产品层</th>
      <th style="text-align: center">Runtime层</th>
      <th style="text-align: center">SDK层</th>
      <th style="text-align: center">Loop层</th>
      <th style="text-align: center">Provider SDK</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>OpenClaw</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
    </tr>
    <tr>
      <td>Claude Code</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✗</td>
    </tr>
    <tr>
      <td>Cursor</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✗</td>
    </tr>
    <tr>
      <td>Eino</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">部分</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
    </tr>
    <tr>
      <td>OpenAI Agents SDK</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✗</td>
    </tr>
    <tr>
      <td>pydantic-ai</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
    </tr>
    <tr>
      <td>LangChain</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
    </tr>
    <tr>
      <td>Vercel AI SDK</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
      <td style="text-align: center">✅</td>
    </tr>
    <tr>
      <td>LiteLLM</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✗</td>
      <td style="text-align: center">✅</td>
    </tr>
  </tbody>
</table>

<p>没有任何一个框架专注只做一层。这既是现状，也是机会——<strong>每一层都有空间做一个专注、深度的解决方案</strong>。</p>

<p>从表中也能看到一个有趣的规律：<strong>越靠近用户的产品，覆盖的层次越多</strong>。Claude Code、Cursor 这类产品几乎从产品层一直做到 Loop 层，因为它们需要完整控制用户体验。而 LiteLLM 这样的工具则专注在一层，做到极致。</p>

<hr />

<h2 id="heading-一个有趣的类比">一个有趣的类比</h2>

<p>如果用操作系统来类比：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>基建层          ↔  硬件
模型推理服务    ↔  固件 / 驱动
LLM API        ↔  CPU 指令集
Provider SDK   ↔  硬件抽象层
Agent Loop     ↔  进程调度
Agent SDK      ↔  编程语言 / 标准库
Agent Runtime  ↔  操作系统
产品层         ↔  应用程序
用户           ↔  用户
</code></pre></div></div>

<p>OpenClaw 自称 "AI Operating System" 不是没有道理——它确实在填 Runtime 层和产品层的空白。但真正的"AI OS"应该是一个独立的 Runtime 层，让各种产品都能在上面运行，而不是一个大而全的单体。</p>

<hr />

<h2 id="heading-结语">结语</h2>

<p>AI Agent 生态还处于早期，各层的边界还很模糊，大量工作被打包在同一个框架里。但随着生态成熟，<strong>分层会越来越清晰，专注做好一层的机会也会越来越多</strong>。</p>

<p>理解层次，才能找到自己的位置。</p>

<hr />

<p><em>本文是一次关于 AI Agent 生态结构的思考梳理，欢迎讨论和补充。</em></p>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="AI" /><category term="AI" /><category term="Agent" /><category term="LLM" /><category term="生态" /><category term="架构" /><summary type="html"><![CDATA[从底层硬件到用户交互，AI Agent 生态一共分几层？每层解决什么问题？现有框架如何在层次中定位？]]></summary></entry><entry><title type="html">为什么AI产品越做越亏？因为大家都在替用户交“流量费”</title><link href="https://sut-gc.github.io/blog/2026/03/08/%E4%B8%BA%E4%BB%80%E4%B9%88ai%E4%BA%A7%E5%93%81%E8%B6%8A%E5%81%9A%E8%B6%8A%E4%BA%8F-%E5%9B%A0%E4%B8%BA%E5%A4%A7%E5%AE%B6%E9%83%BD%E5%9C%A8%E6%9B%BF%E7%94%A8%E6%88%B7%E4%BA%A4-%E6%B5%81%E9%87%8F%E8%B4%B9/" rel="alternate" type="text/html" title="为什么AI产品越做越亏？因为大家都在替用户交“流量费”" /><published>2026-03-08T00:00:00+00:00</published><updated>2026-03-08T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/03/08/%E4%B8%BA%E4%BB%80%E4%B9%88ai%E4%BA%A7%E5%93%81%E8%B6%8A%E5%81%9A%E8%B6%8A%E4%BA%8F-%E5%9B%A0%E4%B8%BA%E5%A4%A7%E5%AE%B6%E9%83%BD%E5%9C%A8%E6%9B%BF%E7%94%A8%E6%88%B7%E4%BA%A4-%E6%B5%81%E9%87%8F%E8%B4%B9</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/03/08/%E4%B8%BA%E4%BB%80%E4%B9%88ai%E4%BA%A7%E5%93%81%E8%B6%8A%E5%81%9A%E8%B6%8A%E4%BA%8F-%E5%9B%A0%E4%B8%BA%E5%A4%A7%E5%AE%B6%E9%83%BD%E5%9C%A8%E6%9B%BF%E7%94%A8%E6%88%B7%E4%BA%A4-%E6%B5%81%E9%87%8F%E8%B4%B9/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-你有没有想过也许流量费从来不是app的事" id="markdown-toc-heading-你有没有想过也许流量费从来不是app的事">你有没有想过，也许流量费从来不是App的事</a></li>
  <li><a href="#heading-如果token也有一张卡" id="markdown-toc-heading-如果token也有一张卡">如果Token也有一张"卡"</a></li>
  <li><a href="#heading-如果这件事成了才会出现更多的ai原生产品" id="markdown-toc-heading-如果这件事成了才会出现更多的ai原生产品">如果这件事成了，才会出现更多的AI原生产品</a></li>
  <li><a href="#heading-算力token终将变成水电煤" id="markdown-toc-heading-算力token终将变成水电煤">算力，Token，终将变成水电煤</a></li>
</ul>

<p>我最近发现一件很奇怪的事：<strong>很多AI产品，用的人越多，亏得越多。</strong></p>

<p>不是因为产品不好，不是因为没有用户，就是因为每个用户每发一条消息，背后都要消耗Token，这个钱是产品方出的。规模越大，窟窿越大。</p>

<p>然后大家想的解法是：涨价、限制用量、找愿意付费的用户。</p>

<p>但我觉得，这个方向可能从一开始就偏了。让App来承担Token的费用，这件事逻辑本身就是错的。</p>

<h2 id="heading-你有没有想过也许流量费从来不是app的事">你有没有想过，也许流量费从来不是App的事</h2>

<p>你现在用微信、刷视频、叫外卖。</p>

<p>这些App从来不需要替你承担上网的费用。你自己买了流量套餐，手机联网，App直接用，费用记在你头上，跟App没关系。</p>

<p>这件事之所以能这样运转，背后有一个很关键的东西——SIM卡。</p>

<p>SIM卡解决的不是"怎么上网"的问题，它解决的是<strong>身份认证和资费归属</strong>的问题。你插了哪张卡，你的流量费用就归到哪个账户。设备知道你是谁，运营商知道该向谁收钱，App完全不需要介入这件事。</p>

<p>AI现在缺的，正是这样一层东西。</p>

<h2 id="heading-如果token也有一张卡">如果Token也有一张"卡"</h2>

<p>设想一下这个场景——</p>

<p>用户选择绑定某家模型厂商，就像选择运营商一样，在手机系统层完成身份认证和资费绑定。开发者调用AI能力的时候，不是"我这个App去对接某家模型的API，我来承担这次调用的费用"，而是"用户已经绑定了他自己的模型账户，这次调用的费用自动记在他头上"。</p>

<p>App只管调用，费用和权限的归属，交给系统层和模型厂商去结算。</p>

<p>这样的话——</p>

<p>开发者不需要申请各家的Key，不需要考虑Token成本怎么转嫁，不需要为了控制成本而克制地使用AI。</p>

<p>用户也不需要在每个App里单独配置、单独付费。他和某家模型厂商之间的关系，一次绑定，到处生效。</p>

<p>模型厂商在这个结构里，扮演的角色就是运营商。算力就是流量，Token就是流量费，用户按月订阅，App按需调用。</p>

<h2 id="heading-如果这件事成了才会出现更多的ai原生产品">如果这件事成了，才会出现更多的AI原生产品</h2>

<p>现在很多AI产品，表面上在做AI，实际上有一半精力是在算这件事划不划算——这个功能要调几次模型，每次多少钱，用户会不会用，能不能回本。</p>

<p>这些问题不该是产品层面要解决的。就像你做一个视频App，不需要去想用户的流量从哪来。</p>

<p>一旦Token的费用归属问题在系统层解决了，开发者才能真正专注在产品体验上。那个时候，才会出现真正的AI原生产品——不是现在这种在传统逻辑上套AI外壳的东西，而是从一开始就把AI当作基础能力来设计的产品。</p>

<p>就像真正的移动原生产品，是在流量便宜、网速快了之后才出现的，不是在2G时代硬做出来的。微信、抖音、美团，没有一个是2G时代的产物。</p>

<h2 id="heading-算力token终将变成水电煤">算力，Token，终将变成水电煤</h2>

<p>这件事能成立，还有一个前提：Token得足够便宜。</p>

<p>这个我觉得不用太担心。算力的边际成本一直在降，技术在进步，市场在竞争。流量当年也很贵，现在几乎是无感的存在了。Token大概也会走这条路，只是时间问题。</p>

<p>芯片、能源、算力，这些东西最终会变成水电煤——你每天都在用，但完全不会去想它。</p>

<p>到那个时候，现在大家纠结的"AI产品怎么回本"这个问题，可能根本就不需要回答了。</p>

<hr />

<p>你觉得AI的费用应该由谁来承担？欢迎留言聊聊。</p>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="产品" /><category term="AI" /><category term="产品思考" /><category term="基础设施" /><category term="Token" /><summary type="html"><![CDATA[AI产品的Token成本不应该由App承担，而应该像SIM卡一样，由用户绑定模型厂商，系统层统一结算。]]></summary></entry><entry><title type="html">我解剖了这只爆火的龙虾</title><link href="https://sut-gc.github.io/blog/2026/03/08/%E6%88%91%E8%A7%A3%E5%89%96%E4%BA%86%E8%BF%99%E5%8F%AA%E7%88%86%E7%81%AB%E7%9A%84%E9%BE%99%E8%99%BE/" rel="alternate" type="text/html" title="我解剖了这只爆火的龙虾" /><published>2026-03-08T00:00:00+00:00</published><updated>2026-03-08T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/03/08/%E6%88%91%E8%A7%A3%E5%89%96%E4%BA%86%E8%BF%99%E5%8F%AA%E7%88%86%E7%81%AB%E7%9A%84%E9%BE%99%E8%99%BE</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/03/08/%E6%88%91%E8%A7%A3%E5%89%96%E4%BA%86%E8%BF%99%E5%8F%AA%E7%88%86%E7%81%AB%E7%9A%84%E9%BE%99%E8%99%BE/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-它究竟是什么" id="markdown-toc-heading-它究竟是什么">它究竟是什么？</a></li>
  <li><a href="#heading-整体架构八层结构" id="markdown-toc-heading-整体架构八层结构">整体架构：八层结构</a></li>
  <li><a href="#heading-第一层入口六种方式操控同一个-agent" id="markdown-toc-heading-第一层入口六种方式操控同一个-agent">第一层：入口——六种方式操控同一个 Agent</a></li>
  <li><a href="#heading-第二层消息渠道用-baileys-协议搞定-whatsapp" id="markdown-toc-heading-第二层消息渠道用-baileys-协议搞定-whatsapp">第二层：消息渠道——用 Baileys 协议搞定 WhatsApp</a></li>
  <li><a href="#heading-第三层gateway整个系统的心脏" id="markdown-toc-heading-第三层gateway整个系统的心脏">第三层：Gateway——整个系统的心脏</a>    <ul>
      <li><a href="#heading-消息路由与状态机" id="markdown-toc-heading-消息路由与状态机">消息路由与状态机</a></li>
      <li><a href="#heading-会话管理" id="markdown-toc-heading-会话管理">会话管理</a></li>
      <li><a href="#heading-渠道健康监控" id="markdown-toc-heading-渠道健康监控">渠道健康监控</a></li>
      <li><a href="#heading-配置热重载" id="markdown-toc-heading-配置热重载">配置热重载</a></li>
      <li><a href="#heading-boot-机制用人话写的启动脚本" id="markdown-toc-heading-boot-机制用人话写的启动脚本">Boot 机制——用人话写的启动脚本</a></li>
      <li><a href="#heading-接入认证" id="markdown-toc-heading-接入认证">接入认证</a></li>
      <li><a href="#heading-执行审批" id="markdown-toc-heading-执行审批">执行审批</a></li>
    </ul>
  </li>
  <li><a href="#heading-第四层hooks-与-cron自动化引擎" id="markdown-toc-heading-第四层hooks-与-cron自动化引擎">第四层：Hooks 与 Cron——自动化引擎</a>    <ul>
      <li><a href="#heading-hooks-事件系统" id="markdown-toc-heading-hooks-事件系统">Hooks 事件系统</a></li>
      <li><a href="#heading-cron-定时任务" id="markdown-toc-heading-cron-定时任务">Cron 定时任务</a></li>
    </ul>
  </li>
  <li><a href="#heading-第五层agent-执行本地-coding-agent" id="markdown-toc-heading-第五层agent-执行本地-coding-agent">第五层：Agent 执行——本地 Coding Agent</a>    <ul>
      <li><a href="#heading-双引擎架构" id="markdown-toc-heading-双引擎架构">双引擎架构</a></li>
      <li><a href="#heading-两层权限模型" id="markdown-toc-heading-两层权限模型">两层权限模型</a></li>
      <li><a href="#heading-一次调用的完整流程" id="markdown-toc-heading-一次调用的完整流程">一次调用的完整流程</a></li>
      <li><a href="#heading-另一条路嵌入式运行" id="markdown-toc-heading-另一条路嵌入式运行">另一条路：嵌入式运行</a></li>
      <li><a href="#heading-acp多智能体协议" id="markdown-toc-heading-acp多智能体协议">ACP——多智能体协议</a></li>
    </ul>
  </li>
  <li><a href="#heading-第六层llm-provider-与浏览器模型管理--自动化" id="markdown-toc-heading-第六层llm-provider-与浏览器模型管理--自动化">第六层：LLM Provider 与浏览器——模型管理 + 自动化</a>    <ul>
      <li><a href="#heading-llm-provider28-个供应商挂了就换" id="markdown-toc-heading-llm-provider28-个供应商挂了就换">LLM Provider——28 个供应商，挂了就换</a></li>
      <li><a href="#heading-浏览器--媒体让-ai-用你的浏览器" id="markdown-toc-heading-浏览器--媒体让-ai-用你的浏览器">浏览器 &amp; 媒体——让 AI 用你的浏览器</a></li>
    </ul>
  </li>
  <li><a href="#heading-第七层记忆与技能知识库--插件生态" id="markdown-toc-heading-第七层记忆与技能知识库--插件生态">第七层：记忆与技能——知识库 + 插件生态</a>    <ul>
      <li><a href="#heading-记忆系统向量数据库--sqlite--fts" id="markdown-toc-heading-记忆系统向量数据库--sqlite--fts">记忆系统——向量数据库 + SQLite + FTS</a></li>
      <li><a href="#heading-skills-技能系统插件市场" id="markdown-toc-heading-skills-技能系统插件市场">Skills 技能系统——插件市场</a></li>
    </ul>
  </li>
  <li><a href="#heading-第八层基础设施安全和配置的地基" id="markdown-toc-heading-第八层基础设施安全和配置的地基">第八层：基础设施——安全和配置的地基</a></li>
  <li><a href="#heading-结语没有黑魔法" id="markdown-toc-heading-结语没有黑魔法">结语：没有黑魔法</a></li>
</ul>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/cover-task1-1.jpg" alt="我解剖了这只爆火的龙虾" /></p>

<p>最近 OpenClaw（前身 Clawdbot）火遍硅谷到北京，有人说它是"AI 助手的终局形态"。两个月内斩获 <strong>14 万+ GitHub Stars</strong>，一堆人专门买 Mac Mini 跑它，二手价格都被炒起来了。</p>

<p>但它到底是怎么做到的？我把整个仓库 clone 下来，结合 AI 辅助，读了一遍源码——4700+ 个 TypeScript 文件，38MB，800+ 个 agent 相关模块。这篇文章是我的<strong>源码拆解笔记</strong>，你可以跟着我一起从技术视角搞清楚它的内部结构。</p>

<p>先说结论：<strong>其实没那么玄。</strong></p>

<p>这是系列文章的第一篇，专讲架构。第二篇我们动手，用现有开源工具搭一个简易版 OpenClaw。</p>

<hr />

<h2 id="heading-它究竟是什么">它究竟是什么？</h2>

<p>用一句话描述：<strong>OpenClaw 是一个跑在你本地机器上的、以消息 App 为操控界面的持久化 AI Agent</strong>。</p>

<p>它不是聊天机器人，不是 Zapier 那样的自动化平台，也不是套壳 AI。核心差异就三点：</p>

<ul>
  <li><strong>本地运行</strong>：数据不上云，你的代码、文件、记忆全在本机</li>
  <li><strong>持久化</strong>：24 小时在线，有 cron 定时任务，会主动工作</li>
  <li><strong>消息 App 为界面</strong>：你用 WhatsApp / Telegram / Discord / iMessage 跟它聊，它在后台对着你的电脑干活</li>
</ul>

<hr />

<h2 id="heading-整体架构八层结构">整体架构：八层结构</h2>

<p>读完代码，我把它的架构抽象成八层：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/01-task1-1.jpg" alt="OpenClaw 八层架构图" /></p>

<p>下面从上到下逐层拆解。</p>

<hr />

<h2 id="heading-第一层入口六种方式操控同一个-agent">第一层：入口——六种方式操控同一个 Agent</h2>

<p>最顶层是用户入口，OpenClaw 提供了多种交互方式，全部连到同一个 Gateway：</p>

<ul>
  <li><strong>CLI</strong>（<code class="language-plaintext highlighter-rouge">src/cli/</code>）：168+ 个命令文件，涵盖 agent 管理、浏览器调试、设备配对、cron 任务、插件管理等</li>
  <li><strong>TUI</strong>（<code class="language-plaintext highlighter-rouge">src/tui/</code>）：基于 <code class="language-plaintext highlighter-rouge">pi-tui</code> 的终端交互界面，支持 <code class="language-plaintext highlighter-rouge">!</code> 前缀进 shell 模式、<code class="language-plaintext highlighter-rouge">/</code> 前缀触发命令</li>
  <li><strong>Web UI</strong>（<code class="language-plaintext highlighter-rouge">ui/</code>）：独立的 Vite + React SPA，通过 REST/WebSocket 连接 Gateway</li>
  <li><strong>macOS App</strong>（<code class="language-plaintext highlighter-rouge">apps/macos/</code>）：SwiftUI 菜单栏应用，内嵌 WKWebView 渲染 Canvas</li>
  <li><strong>iOS App</strong>（<code class="language-plaintext highlighter-rouge">apps/ios/</code>）：SwiftUI 原生应用，集成日历、摄像头、通讯录、位置等系统服务</li>
  <li><strong>Android App</strong>（<code class="language-plaintext highlighter-rouge">apps/android/</code>）：Kotlin 原生应用</li>
</ul>

<p>移动端和桌面端通过 <strong>Bonjour/DNS-SD</strong> 自动发现局域网内的 Gateway（服务类型 <code class="language-plaintext highlighter-rouge">_openclaw-gw._tcp</code>），TXT 记录里编码了 TLS 指纹和端口号。配对成功后，设备获得一个带作用域的 token，后续自动连接。</p>

<p>这种设计的精妙之处在于：<strong>你可以躺在沙发上用手机给 AI 下指令，它在书房的 Mac Mini 上执行</strong>。</p>

<hr />

<h2 id="heading-第二层消息渠道用-baileys-协议搞定-whatsapp">第二层：消息渠道——用 Baileys 协议搞定 WhatsApp</h2>

<p>很多人好奇：WhatsApp 并没有开放 API，OpenClaw 是怎么接入的？</p>

<p>答案藏在 <code class="language-plaintext highlighter-rouge">src/web/</code> 目录。它用的是 <strong>@whiskeysockets/baileys</strong>，一个逆向工程了 WhatsApp Web 协议的开源库。</p>

<p>原理非常优雅——本质上就是模拟你电脑浏览器开着 WhatsApp Web：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>手机 WhatsApp ←→ WhatsApp 服务器 ←→ 本地 Gateway（Baileys 长连接）
</code></pre></div></div>

<p><strong>关键点</strong>：连接是从你本地主动发起的（出站），不需要公网 IP。WhatsApp 服务器会通过 WebSocket 长连接把消息实时推过来。就像你打开 WhatsApp Web 一样，只不过接收端是 OpenClaw 的 Gateway，而不是浏览器。</p>

<p>WhatsApp 的底层协议实现在 <code class="language-plaintext highlighter-rouge">src/web/</code>（登录、会话、收发消息），而 <code class="language-plaintext highlighter-rouge">extensions/whatsapp/</code> 是基于 Plugin SDK 的封装层，负责把 WhatsApp 以标准渠道插件的形式接入系统。</p>

<p>渠道层的代码分布在 <code class="language-plaintext highlighter-rouge">src/</code> 和 <code class="language-plaintext highlighter-rouge">extensions/</code> 两层，每个渠道独立一个包：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>src/
├── web/           ← WhatsApp Baileys 协议底层实现
├── telegram/      ← Telegram 核心逻辑
├── discord/       ← Discord 核心逻辑
├── slack/         ← Slack 核心逻辑
├── signal/        ← Signal 核心逻辑
├── imessage/      ← iMessage（仅 macOS）
└── line/          ← LINE 核心逻辑

extensions/
├── whatsapp/      ← WhatsApp 渠道插件封装
├── telegram/      ← Telegram Bot API 插件
├── discord/       ← Discord.js 插件
├── slack/         ← Slack Web API 插件
├── imessage/      ← AppleScript 插件（仅 macOS）
├── signal/        ← signal-cli 插件
├── feishu/        ← 飞书 Webhook
├── msteams/       ← Microsoft Teams
├── matrix/        ← Matrix 协议
├── googlechat/    ← Google Chat
├── irc/           ← IRC
├── bluebubbles/   ← BlueBubbles（另一种 iMessage 方案）
└── ...（还有更多）
</code></pre></div></div>

<p>这个两层设计值得注意：<code class="language-plaintext highlighter-rouge">src/</code> 放协议级的底层实现，<code class="language-plaintext highlighter-rouge">extensions/</code> 放标准化的插件封装。每个渠道实现一套统一的 <code class="language-plaintext highlighter-rouge">ChannelPlugin</code> 接口，包含：认证、收发消息、媒体处理、群组管理等能力。这个设计让 Gateway 完全不关心下层是什么 App，消息统一流入。</p>

<hr />

<h2 id="heading-第三层gateway整个系统的心脏">第三层：Gateway——整个系统的心脏</h2>

<p><code class="language-plaintext highlighter-rouge">src/gateway/</code> 是整个项目最核心的目录。Gateway 是一个常驻后台的 <strong>WebSocket 服务</strong>（默认端口 18789），所有客户端（CLI、Web UI、macOS App、iOS、Android）都通过 WebSocket 连接到它。它同时也提供 HTTP 端点用于健康检查和控制面板。</p>

<p>先看它的内部结构：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/gateway-task1-1.jpg" alt="Gateway 内部结构" /></p>

<p>从 <code class="language-plaintext highlighter-rouge">server-methods-list.ts</code> 可以看到，Gateway 对外暴露了 <strong>100+ 个 WebSocket 方法</strong>，远不止"消息转发"这么简单。下面展开几个最关键的。</p>

<h3 id="heading-消息路由与状态机">消息路由与状态机</h3>

<p>收到消息后，Gateway 会根据发送人、群组、配置等判断：这条消息该不该响应？该交给哪个 Agent？</p>

<p>每个对话的执行状态由一个状态机管理（<code class="language-plaintext highlighter-rouge">src/channels/run-state-machine.ts</code>）：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/channels/run-state-machine.ts</span>
<span class="c1">// 模块级常量：心跳间隔 60 秒</span>
<span class="kd">const</span> <span class="nx">DEFAULT_RUN_ACTIVITY_HEARTBEAT_MS</span> <span class="o">=</span> <span class="mi">60</span><span class="nx">_000</span><span class="p">;</span>

<span class="k">export</span> <span class="kd">function</span> <span class="nx">createRunStateMachine</span><span class="p">(</span><span class="nx">params</span><span class="p">:</span> <span class="nx">RunStateMachineParams</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">activeRuns</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="c1">// 跟踪并发执行数、心跳、超时</span>
  <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>状态机会跟踪每个对话的并发执行数，通过 60 秒一次的心跳向上游汇报"我还活着"。</p>

<h3 id="heading-会话管理">会话管理</h3>

<p>每个对话有独立的 session，以 <strong>JSONL 文件</strong>存储在本地文件系统（路径：<code class="language-plaintext highlighter-rouge">~/.openclaw/agents/{agentId}/sessions/{sessionId}.jsonl</code>）。每次 LLM 调用都会带上完整的历史上下文——OpenClaw 用文件系统实现了跨进程的持久化会话。</p>

<h3 id="heading-渠道健康监控">渠道健康监控</h3>

<p>这是保证 7×24 稳定在线的关键（<code class="language-plaintext highlighter-rouge">src/gateway/channel-health-monitor.ts</code>）。</p>

<p>健康监控器每 <strong>5 分钟</strong>巡检一次所有已连接的渠道，检测"僵尸连接"——比如 Slack 的 WebSocket 看起来是活的，但实际上已经不推送消息了。它会根据最后一次收到事件的时间判断是否需要重启渠道连接。</p>

<p>安全措施：每小时最多 10 次自动重启，启动后有 60 秒宽限期，防止疯狂重连。</p>

<h3 id="heading-配置热重载">配置热重载</h3>

<p>Gateway 用 <strong>chokidar</strong>（文件监听库）监控配置文件变化，300ms 防抖后自动重载（<code class="language-plaintext highlighter-rouge">src/gateway/config-reload.ts</code>）。</p>

<p>它会做配置 diff：逐字段对比新旧配置，只重新加载变化的部分。如果你改了 WhatsApp 的配置，它不会重启 Telegram 的连接。重载模式支持 <code class="language-plaintext highlighter-rouge">hybrid</code>（按需决定热重载还是全量重启）。</p>

<h3 id="heading-boot-机制用人话写的启动脚本">Boot 机制——用人话写的启动脚本</h3>

<p>这是我觉得体现 OpenClaw 设计哲学的一个机制。（自然语言代替机器语言）</p>

<p>传统软件的启动脚本是 <code class="language-plaintext highlighter-rouge">init.d</code>、<code class="language-plaintext highlighter-rouge">systemd</code>、<code class="language-plaintext highlighter-rouge">.bashrc</code>——都是代码。而 OpenClaw 的启动脚本是一个 <strong>Markdown 文件</strong>，内容是自然语言。</p>

<p>用户在 Agent 的工作目录下创建一个 <code class="language-plaintext highlighter-rouge">BOOT.md</code>，写上人话：</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>每次启动后，给我的 WhatsApp 发一条"Gateway 已上线"。
然后检查一下今天的日历，如果有会议，提前提醒我。
</code></pre></div></div>

<p>Gateway 启动时，会遍历所有 Agent，读取各自工作目录下的 <code class="language-plaintext highlighter-rouge">BOOT.md</code>，把内容塞进 prompt 交给 Agent 执行：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/gateway/boot.ts</span>
<span class="kd">function</span> <span class="nx">buildBootPrompt</span><span class="p">(</span><span class="nx">content</span><span class="p">:</span> <span class="kr">string</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">[</span>
    <span class="dl">"</span><span class="s2">You are running a boot check. Follow BOOT.md instructions exactly.</span><span class="dl">"</span><span class="p">,</span>
    <span class="dl">""</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">BOOT.md:</span><span class="dl">"</span><span class="p">,</span>
    <span class="nx">content</span><span class="p">,</span>
    <span class="dl">""</span><span class="p">,</span>
    <span class="dl">"</span><span class="s2">If BOOT.md asks you to send a message, use the message tool.</span><span class="dl">"</span><span class="p">,</span>
    <span class="c1">// ...</span>
  <span class="p">].</span><span class="nx">join</span><span class="p">(</span><span class="dl">"</span><span class="se">\n</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>几个值得注意的设计选择：</p>

<ul>
  <li><strong>用户手写，系统不生成</strong>。文件不存在就跳过，存在就执行——跟 <code class="language-plaintext highlighter-rouge">.gitignore</code> 一个思路，约定文件名。</li>
  <li><strong>每个 Agent 独立一份</strong>。不是全局一个 <code class="language-plaintext highlighter-rouge">BOOT.md</code>，而是每个 Agent 工作目录下各自一个。monitor Agent 启动时检查服务状态，assistant Agent 启动时发问候语——各司其职。</li>
  <li><strong>临时会话，不污染历史</strong>。执行时生成 <code class="language-plaintext highlighter-rouge">boot-{时间戳}-{随机ID}</code> 的临时 session，完成后恢复原始会话映射，回复用 <code class="language-plaintext highlighter-rouge">NO_REPLY</code> 令牌吞掉，不会出现在用户的对话记录里。</li>
  <li><strong>失败不阻塞</strong>。<code class="language-plaintext highlighter-rouge">BOOT.md</code> 执行失败只记 warn 日志然后 <code class="language-plaintext highlighter-rouge">continue</code> 到下一个 Agent，Gateway 照常启动。</li>
  <li><strong>它是一个Hook</strong>。Boot 机制实现在 <code class="language-plaintext highlighter-rouge">src/hooks/bundled/boot-md/handler.ts</code>，通过监听 <code class="language-plaintext highlighter-rouge">gateway-startup</code> 事件触发，是可插拔的——拿掉也不影响 Gateway 启动。</li>
</ul>

<p>本质上，OpenClaw 把传统的 <code class="language-plaintext highlighter-rouge">init.d</code> 启动脚本降维成了自然语言，执行者从 shell 换成了 AI Agent。非程序员也能定义"AI 开机后该干什么"，同时怎么折腾都不会把系统搞挂。</p>

<h3 id="heading-接入认证">接入认证</h3>

<p>注意：这里的认证是<strong>谁可以连接 Gateway</strong>，不是 LLM 模型的 API Key 管理（那是 Provider 层的事）。</p>

<p>Gateway 支持四种认证方式（<code class="language-plaintext highlighter-rouge">src/gateway/auth.ts</code>）：</p>

<ul>
  <li><strong>none</strong>：无认证（仅限回环地址访问时）</li>
  <li><strong>token</strong>：令牌认证</li>
  <li><strong>password</strong>：密码认证</li>
  <li><strong>trusted-proxy</strong>：信任代理（Tailscale 等）</li>
</ul>

<p>还内置了速率限制（<code class="language-plaintext highlighter-rouge">auth-rate-limit.ts</code>），防止暴力破解。</p>

<h3 id="heading-执行审批">执行审批</h3>

<p>当 Agent 要执行危险命令时，Gateway 充当"门卫"：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Agent 想执行 rm -rf /tmp/data
  → Gateway 拦截
    → 推送审批请求到你的 Telegram / WhatsApp
      → 你回复"允许"
        → Gateway 放行
</code></pre></div></div>

<p>审批请求通过 <code class="language-plaintext highlighter-rouge">exec.approval.request</code> 方法发出，通过 <code class="language-plaintext highlighter-rouge">exec.approval.resolve</code> 方法回收决策。这能让你来实现<strong>人在外面也能远程批准或拒绝 Agent 的操作</strong>。</p>

<hr />

<h2 id="heading-第四层hooks-与-cron自动化引擎">第四层：Hooks 与 Cron——自动化引擎</h2>

<p>这层包含两套互补的自动化机制：<strong>Hooks</strong>（事件驱动，"如果……就……"）和 <strong>Cron</strong>（时间驱动，"每当……就……"）。</p>

<h3 id="heading-hooks-事件系统">Hooks 事件系统</h3>

<p>Hooks 本质上是一个贯穿全系统的事件总线。<code class="language-plaintext highlighter-rouge">src/hooks/internal-hooks.ts</code> 定义了五大事件类型：</p>

<table>
  <thead>
    <tr>
      <th>事件类型</th>
      <th>触发时机</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">command</code></td>
      <td>用户发出命令时</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">session</code></td>
      <td>会话创建/销毁/切换时</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">agent</code></td>
      <td>Agent 启动/工具调用/回复生成时</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gateway</code></td>
      <td>Gateway 启动/关闭/配置变更时</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">message</code></td>
      <td>收发消息、音频转写完成时</td>
    </tr>
  </tbody>
</table>

<p>注册钩子时，可以监听整个类型（如所有 <code class="language-plaintext highlighter-rouge">message</code> 事件），也可以精确到 <code class="language-plaintext highlighter-rouge">type:action</code>（如 <code class="language-plaintext highlighter-rouge">message:received</code>）。触发时，系统会同时通知两层监听者。</p>

<p>一个关键设计：<strong>钩子异常不会阻断主流程</strong>。每个 handler 的错误被独立 catch 和日志记录，保证一个插件崩溃不会拖垮整个系统。</p>

<p>这套机制让你可以做到：消息进来时自动打标签、Agent 调用工具前先审批、Gateway 启动后自动发状态通知——全部通过声明式的事件订阅，不用改核心代码。</p>

<h3 id="heading-cron-定时任务">Cron 定时任务</h3>

<p><code class="language-plaintext highlighter-rouge">src/cron/</code> 是实现"主动 AI"的关键。它底层用 <code class="language-plaintext highlighter-rouge">croner</code> 库，支持两种调度模式：</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">every</code></strong>：每隔 X 分钟/小时执行一次</li>
  <li><strong><code class="language-plaintext highlighter-rouge">at</code></strong>：在特定时间执行一次</li>
</ul>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// src/cron/schedule.ts</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">computeNextRunAtMs</span><span class="p">(</span><span class="nx">schedule</span><span class="p">:</span> <span class="nx">CronSchedule</span><span class="p">,</span> <span class="nx">nowMs</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">undefined</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">schedule</span><span class="p">.</span><span class="nx">kind</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">at</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 处理精确时间调度</span>
  <span class="p">}</span>
  <span class="c1">// 处理 cron 表达式（用 croner 库解析）</span>
  <span class="kd">const</span> <span class="nx">cron</span> <span class="o">=</span> <span class="nx">resolveCachedCron</span><span class="p">(</span><span class="nx">expr</span><span class="p">,</span> <span class="nx">timezone</span><span class="p">);</span>
</code></pre></div></div>

<p>你可以跟 OpenClaw 说"每天早上 8 点给我发今日新闻摘要"，它会创建一个 cron job，时间到了自动执行，主动发消息给你。这就是它"不用你问，它主动找你"的实现原理。</p>

<p>Hooks 和 Cron 合在一起，覆盖了两种自动化场景：Hooks 响应系统内部事件（被动），Cron 按时间表主动触发（主动）。两者都通过 Gateway 调度，最终都是调用 Agent 来执行具体任务。</p>

<hr />

<h2 id="heading-第五层agent-执行本地-coding-agent">第五层：Agent 执行——本地 Coding Agent</h2>

<p>这层是最有趣的部分。很多人以为 OpenClaw 自己实现了一个 AI Agent，但实际上——<strong>它调用的是外部的 Coding Agent CLI</strong>。</p>

<h3 id="heading-双引擎架构">双引擎架构</h3>

<p>看 <code class="language-plaintext highlighter-rouge">src/agents/cli-backends.ts</code>，OpenClaw 内置了两个 Agent 后端：</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 后端 1：Claude Code CLI</span>
<span class="kd">const</span> <span class="nx">DEFAULT_CLAUDE_BACKEND</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">command</span><span class="p">:</span> <span class="dl">"</span><span class="s2">claude</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">args</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">-p</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">--output-format</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">json</span><span class="dl">"</span><span class="p">,</span>
         <span class="dl">"</span><span class="s2">--permission-mode</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">bypassPermissions</span><span class="dl">"</span><span class="p">],</span>
  <span class="c1">// ...</span>
<span class="p">};</span>

<span class="c1">// 后端 2：Codex CLI</span>
<span class="kd">const</span> <span class="nx">DEFAULT_CODEX_BACKEND</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">command</span><span class="p">:</span> <span class="dl">"</span><span class="s2">codex</span><span class="dl">"</span><span class="p">,</span>
  <span class="na">args</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">exec</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">--json</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">--sandbox</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">workspace-write</span><span class="dl">"</span><span class="p">],</span>
  <span class="c1">// ...</span>
<span class="p">};</span>
</code></pre></div></div>

<p>默认使用 Anthropic 的 Claude Code（<code class="language-plaintext highlighter-rouge">src/agents/defaults.ts</code> 写死了 <code class="language-plaintext highlighter-rouge">DEFAULT_PROVIDER = "anthropic"</code>，<code class="language-plaintext highlighter-rouge">DEFAULT_MODEL = "claude-opus-4-6"</code>）。也可以切换到 OpenAI 的 Codex。</p>

<p>但这里有一个容易混淆的概念需要区分——<strong>"谁来执行"和"谁来思考"是两回事</strong>：</p>

<ul>
  <li><strong>CLI 后端（执行引擎）</strong>：负责"怎么跑"——接收 prompt、spawn 进程、执行工具调用、管理 session。内置 Claude Code 和 Codex 两个，但代码里（第 243-250 行）留了扩展口：只要在配置里提供 <code class="language-plaintext highlighter-rouge">command</code>、<code class="language-plaintext highlighter-rouge">args</code> 等字段，就能注册任意自定义后端。</li>
  <li><strong>LLM Provider（思考引擎）</strong>：负责"怎么想"——哪家的模型来生成回复。这个支持极其广泛。</li>
</ul>

<p>从 <code class="language-plaintext highlighter-rouge">models-config.providers.static.ts</code> 里能看到硬编码的静态 Provider 就有一大堆：MiniMax、小米 Mimo、Moonshot/Kimi、通义千问、豆包(Doubao)、BytePlus、Together、OpenRouter、NVIDIA、千帆、Kilocode……再加上动态发现的 Ollama、HuggingFace、vLLM、Vercel AI Gateway，以及文档里记录的 Anthropic、OpenAI、Google Gemini、Mistral、Perplexity、AWS Bedrock、Azure 等——总计 <strong>28+ 个 Provider</strong>。</p>

<p>也就是说，你可以用 Claude Code 作为执行引擎，但让它背后调用通义千问或 Ollama 本地模型来"思考"；也可以用 Codex 作为执行引擎，配上 Google Gemini 来生成回复。执行引擎和思考引擎是正交的。</p>

<p>那这么多 Provider，底层是怎么统一调用的？答案是一个叫 <strong><code class="language-plaintext highlighter-rouge">@mariozechner/pi-ai</code></strong> 的 SDK（版本 0.57.1）。它负责抹平不同 LLM API 之间的协议差异。从代码里看，28 个 Provider 最终只需要适配<strong>两种 API 协议</strong>：</p>

<table>
  <thead>
    <tr>
      <th>API 协议</th>
      <th>使用的 Provider</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">anthropic-messages</code></td>
      <td>Anthropic、MiniMax Portal、小米 Mimo、Kimi Coding</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">openai-completions</code></td>
      <td>OpenAI、Moonshot、通义千问、豆包、OpenRouter、NVIDIA、千帆、Together、Kilocode…</td>
    </tr>
  </tbody>
</table>

<p>为什么大部分国产模型都走 <code class="language-plaintext highlighter-rouge">openai-completions</code>？因为 OpenAI 的 Chat Completions API 已经成了事实标准，几乎所有模型厂商都做了兼容。所以 OpenClaw 接入一个新模型，通常只需要配一个 <code class="language-plaintext highlighter-rouge">baseUrl</code> + API Key，不用写一行适配代码。</p>

<p>顺便说一句，OpenRouter 不是 OpenClaw 的路由框架——它只是 28 个 Provider 中的一个选项。OpenRouter 本身聚合了很多模型，所以配上它就等于一个 API Key 接入了几百个模型。但 OpenClaw 自己的 Provider 管理（Auth Profile 轮换、failover、模型发现）是独立实现的，不依赖 OpenRouter。</p>

<p>这两个后端都是<strong>本地运行的 Coding Agent</strong>——它们能读写文件、执行命令、调用 API。OpenClaw 的角色是"调度器"：接收用户消息，拼装上下文，调用底层 CLI，拿到结果后发回给用户。</p>

<h3 id="heading-两层权限模型">两层权限模型</h3>

<p>这里有一个容易误解的设计。OpenClaw 默认给底层 CLI 传了 <code class="language-plaintext highlighter-rouge">--permission-mode bypassPermissions</code>，意思是 Claude Code 层面<strong>不做任何权限确认</strong>。这听起来很危险，但安全由 OpenClaw 自己的执行审批层兜底：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/permission-task1-1.jpg" alt="两层权限模型" /></p>

<p>为什么这么设计？因为 OpenClaw 是 7×24 无人值守运行的，如果两层都要弹确认，用户一条命令要批两次。所以在自己的层面做好管控，到底层就全放行。</p>

<h3 id="heading-一次调用的完整流程">一次调用的完整流程</h3>

<p><code class="language-plaintext highlighter-rouge">src/agents/cli-runner.ts</code> 的 <code class="language-plaintext highlighter-rouge">runCliAgent()</code> 揭示了每次 Agent 调用的完整过程：</p>

<ol>
  <li><strong>解析工作目录</strong>——每个 Agent 有独立的 workspace</li>
  <li><strong>加载 bootstrap 文件</strong>——类似 CLAUDE.md 的上下文注入，有 token 预算管理（防止 prompt 过长）</li>
  <li><strong>拼装 system prompt</strong>——追加心跳提示、额外指令</li>
  <li><strong>选择模型和认证</strong>——从 Auth Profile 轮换中选一个可用的 API Key</li>
  <li><strong>调用底层 CLI</strong>——spawn 子进程，JSON 格式通信</li>
  <li><strong>流式接收输出</strong>——支持边生成边推送给用户</li>
  <li><strong>处理失败</strong>——认证错误、限流、上下文溢出，各有不同的 failover 策略</li>
</ol>

<h3 id="heading-另一条路嵌入式运行">另一条路：嵌入式运行</h3>

<p>除了调外部 CLI，OpenClaw 还有一个<strong>嵌入式运行模式</strong>（<code class="language-plaintext highlighter-rouge">src/agents/pi-embedded-runner/</code>）——直接在进程内通过 <code class="language-plaintext highlighter-rouge">pi-ai</code> SDK 调用 LLM API，不经过外部 CLI。这条路径支持：</p>

<ul>
  <li>Auth Profile 轮换（Key 挂了自动换下一个）</li>
  <li>上下文窗口保护（<code class="language-plaintext highlighter-rouge">CONTEXT_WINDOW_HARD_MIN_TOKENS</code> 硬限制）</li>
  <li>会话压缩（<code class="language-plaintext highlighter-rouge">compactEmbeddedPiSession</code>，对话太长时自动压缩历史）</li>
  <li>模型 failover（主模型挂了切备用模型）</li>
</ul>

<h3 id="heading-acp多智能体协议">ACP——多智能体协议</h3>

<p><code class="language-plaintext highlighter-rouge">src/acp/</code> 目录（30 个文件）实现了 <strong>Agent Communication Protocol</strong>，这是 OpenClaw "多智能体"能力的底层基础。</p>

<p>它允许一个 Agent 孵化子 Agent，各自独立运行：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">src/acp/server.ts</code> — ACP 服务端，管理子 Agent 生命周期</li>
  <li><code class="language-plaintext highlighter-rouge">src/acp/client.ts</code> — ACP 客户端，与父 Agent 通信</li>
  <li><code class="language-plaintext highlighter-rouge">src/acp/policy.ts</code> — 权限策略，控制子 Agent 能做什么</li>
  <li><code class="language-plaintext highlighter-rouge">src/acp/translator.ts</code> — 协议翻译，适配不同 LLM 的消息格式</li>
  <li><code class="language-plaintext highlighter-rouge">src/acp/persistent-bindings.ts</code> — 持久化绑定，子 Agent 可以跨会话存活</li>
</ul>

<p>比如你说"帮我同时查三个 API 的文档"，主 Agent 可以 spawn 三个子 Agent 并行处理，各自独立工作，最后汇总结果。</p>

<hr />

<h2 id="heading-第六层llm-provider-与浏览器模型管理--自动化">第六层：LLM Provider 与浏览器——模型管理 + 自动化</h2>

<p>这层提供 Agent 执行时依赖的两大外部能力：<strong>LLM 模型调用</strong>和<strong>浏览器自动化</strong>。</p>

<h3 id="heading-llm-provider28-个供应商挂了就换">LLM Provider——28 个供应商，挂了就换</h3>

<p>这是独立于 Agent 执行层的一整套 <strong>模型供应商管理体系</strong>。</p>

<p><code class="language-plaintext highlighter-rouge">src/agents/auth-profiles/</code> 实现了一个带自动轮换的认证档案系统。每个 Provider（如 OpenAI、Anthropic、Google Gemini、通义千问等）可以配多个 API Key（称为 Auth Profile），系统会自动 round-robin 轮换，并根据失败原因区分处理：</p>

<ul>
  <li><strong>瞬时故障</strong>（限流、过载）→ 设置 <code class="language-plaintext highlighter-rouge">cooldownUntil</code>，冷却后自动重试</li>
  <li><strong>永久故障</strong>（欠费、Key 失效）→ 设置 <code class="language-plaintext highlighter-rouge">disabledUntil</code> + 原因，需要人工介入</li>
</ul>

<p>失败原因按严重程度分级：<code class="language-plaintext highlighter-rouge">auth_permanent</code> &gt; <code class="language-plaintext highlighter-rouge">billing</code> &gt; <code class="language-plaintext highlighter-rouge">model_not_found</code> &gt; <code class="language-plaintext highlighter-rouge">overloaded</code> &gt; <code class="language-plaintext highlighter-rouge">rate_limit</code> &gt; <code class="language-plaintext highlighter-rouge">unknown</code>。</p>

<p>更有趣的是<strong>模型自动发现</strong>机制（<code class="language-plaintext highlighter-rouge">src/agents/models-config.providers.discovery.ts</code>）：</p>

<ul>
  <li><strong>Ollama</strong>：调用本地 <code class="language-plaintext highlighter-rouge">/api/tags</code> 获取已拉取模型列表，再逐个 <code class="language-plaintext highlighter-rouge">/api/show</code> 查询上下文窗口大小，8 个并发、5 秒超时</li>
  <li><strong>HuggingFace / vLLM</strong>：走 OpenAI 兼容的 <code class="language-plaintext highlighter-rouge">/v1/models</code> 端点</li>
  <li><strong>Vercel AI Gateway / Kilocode</strong>：各有专用的发现逻辑</li>
</ul>

<p>这意味着你本地跑了新的 Ollama 模型，OpenClaw 可以自动发现并使用，不用手动配置。</p>

<h3 id="heading-浏览器--媒体让-ai-用你的浏览器">浏览器 &amp; 媒体——让 AI 用你的浏览器</h3>

<p><code class="language-plaintext highlighter-rouge">src/browser/</code> 有 135+ 个文件，实现了一个完整的浏览器自动化框架。底层基于 <strong>Playwright</strong> 和 <strong>Chrome DevTools Protocol (CDP)</strong>。</p>

<p>这不是简单的网页爬虫——它是一个有状态的浏览器会话管理器：</p>

<ul>
  <li><strong>PageState</strong> 跟踪每个页面的控制台消息（上限 500 条）、页面错误（200 条）、网络请求（500 条）</li>
  <li>通过 <strong>角色快照</strong>（Role Snapshot）生成页面的无障碍树（Accessibility Tree），每个可交互元素分配稳定引用 ID</li>
  <li>导航前做 <strong>SSRF 防护</strong>检查，禁止访问内网地址</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">src/media/</code> 则处理多媒体：解析 Agent 输出中的 <code class="language-plaintext highlighter-rouge">MEDIA: &lt;path&gt;</code> 标记，管理媒体文件的存储和生命周期（2 分钟 TTL 自动清理），检查音频格式兼容性（Telegram 语音消息只接受 ogg/opus/mpeg/m4a），文件大小限制 5MB。</p>

<hr />

<h2 id="heading-第七层记忆与技能知识库--插件生态">第七层：记忆与技能——知识库 + 插件生态</h2>

<p>这层让 Agent 拥有<strong>长期记忆</strong>和<strong>可扩展能力</strong>。</p>

<h3 id="heading-记忆系统向量数据库--sqlite--fts">记忆系统——向量数据库 + SQLite + FTS</h3>

<p>这是 OpenClaw 跟普通聊天机器人最本质的区别。记忆系统<strong>默认开启</strong>，用户不需要手动配置。</p>

<h4 id="heading-存储层sqlite-一库多表">存储层：SQLite 一库多表</h4>

<p>看 <code class="language-plaintext highlighter-rouge">src/memory/memory-schema.ts</code>，记忆系统建在本地 SQLite 上（路径：<code class="language-plaintext highlighter-rouge">~/.openclaw/memory/{agentId}.sqlite</code>），有五张核心表：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 1. 键值元数据</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">meta</span> <span class="p">(</span><span class="k">key</span> <span class="nb">TEXT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span> <span class="n">value</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">);</span>

<span class="c1">-- 2. 源文件索引（跟踪哪些文件被记忆了）</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">files</span> <span class="p">(</span>
  <span class="n">path</span> <span class="nb">TEXT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
  <span class="k">source</span> <span class="nb">TEXT</span> <span class="k">DEFAULT</span> <span class="s1">'memory'</span><span class="p">,</span>  <span class="c1">-- 来源：memory 目录 or sessions</span>
  <span class="n">hash</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>            <span class="c1">-- SHA256，用于判断内容是否变化</span>
  <span class="n">mtime</span> <span class="nb">INTEGER</span><span class="p">,</span> <span class="k">size</span> <span class="nb">INTEGER</span>
<span class="p">);</span>

<span class="c1">-- 3. 文本分块 + 向量嵌入</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">chunks</span> <span class="p">(</span>
  <span class="n">id</span> <span class="nb">TEXT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>       <span class="c1">-- hash(path:startLine:endLine:hash:model)</span>
  <span class="n">path</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="k">source</span> <span class="nb">TEXT</span><span class="p">,</span>
  <span class="n">start_line</span> <span class="nb">INTEGER</span><span class="p">,</span> <span class="n">end_line</span> <span class="nb">INTEGER</span><span class="p">,</span>  <span class="c1">-- 在原文件中的行号范围</span>
  <span class="n">hash</span> <span class="nb">TEXT</span><span class="p">,</span> <span class="n">model</span> <span class="nb">TEXT</span><span class="p">,</span>     <span class="c1">-- 用哪个模型生成的嵌入</span>
  <span class="nb">text</span> <span class="nb">TEXT</span><span class="p">,</span>                 <span class="c1">-- 分块原文</span>
  <span class="n">embedding</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>   <span class="c1">-- 向量，JSON 格式的 float 数组</span>
  <span class="n">updated_at</span> <span class="nb">INTEGER</span>
<span class="p">);</span>

<span class="c1">-- 4. FTS5 全文检索虚拟表（只索引 text 列，其余为元数据）</span>
<span class="k">CREATE</span> <span class="n">VIRTUAL</span> <span class="k">TABLE</span> <span class="n">chunks_fts</span> <span class="k">USING</span> <span class="n">fts5</span><span class="p">(</span>
  <span class="nb">text</span><span class="p">,</span> <span class="n">id</span> <span class="n">UNINDEXED</span><span class="p">,</span> <span class="n">path</span> <span class="n">UNINDEXED</span><span class="p">,</span> <span class="k">source</span> <span class="n">UNINDEXED</span><span class="p">,</span> <span class="p">...</span>
<span class="p">);</span>

<span class="c1">-- 5. 二进制向量表（用于高效向量搜索）</span>
<span class="c1">-- embedding 以 Float32Array buffer 存储，避免 JSON 解析开销</span>
</code></pre></div></div>

<h4 id="heading-写入自动索引--智能分块">写入：自动索引 + 智能分块</h4>

<p>记忆系统会自动监听 <code class="language-plaintext highlighter-rouge">memory/</code> 目录和 <code class="language-plaintext highlighter-rouge">MEMORY.md</code> 文件的变化（用文件 watcher，1500ms 防抖），变化后自动重新分块和嵌入。</p>

<p>分块策略（<code class="language-plaintext highlighter-rouge">src/memory/internal.ts</code>）：</p>
<ul>
  <li>按 <strong>400 token</strong>（约 1600 字符）切分</li>
  <li>相邻分块之间有 <strong>80 字符重叠</strong>，保证语义连贯</li>
  <li>每个分块独立嵌入，生成一个向量</li>
</ul>

<p>嵌入后端支持多种：</p>

<table>
  <thead>
    <tr>
      <th>后端</th>
      <th>默认维度</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>OpenAI <code class="language-plaintext highlighter-rouge">text-embedding-3-small</code></td>
      <td>512 维（默认）</td>
    </tr>
    <tr>
      <td>OpenAI <code class="language-plaintext highlighter-rouge">text-embedding-3-large</code></td>
      <td>3072 维</td>
    </tr>
    <tr>
      <td>Google Gemini</td>
      <td>768 维</td>
    </tr>
    <tr>
      <td>Voyage</td>
      <td>1024 维</td>
    </tr>
    <tr>
      <td>Mistral</td>
      <td>1024 维</td>
    </tr>
    <tr>
      <td>Ollama（本地）</td>
      <td>768 维</td>
    </tr>
  </tbody>
</table>

<p>嵌入结果会缓存在 <code class="language-plaintext highlighter-rouge">embedding_cache</code> 表里——同一段文本不会重复计算嵌入，换模型时才重新算。</p>

<h4 id="heading-读取混合检索">读取：混合检索</h4>

<p>检索时不是只用向量搜索，而是<strong>混合检索</strong>（<code class="language-plaintext highlighter-rouge">src/memory/manager.ts</code>）：</p>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/memory-task1-1.jpg" alt="记忆系统：混合检索" /></p>

<p>MMR（Maximal Marginal Relevance）算法在 <code class="language-plaintext highlighter-rouge">src/memory/mmr.ts</code> 里，用的是 <strong>Jaccard 相似度</strong>（词集交并比）来度量已选结果之间的重复度，公式：<code class="language-plaintext highlighter-rouge">MMR = 0.7 × 相关性 - 0.3 × 与已选最大相似度</code>。</p>

<p>如果没配嵌入后端（比如纯离线环境），系统会<strong>自动降级为纯 FTS 搜索</strong>——功能打折但不会挂。</p>

<h4 id="heading-agent-怎么用记忆">Agent 怎么用记忆</h4>

<p>记忆不是自动塞进 prompt 的——它通过两个<strong>工具调用</strong>实现：</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">memory_search</code></strong>：搜索记忆，返回匹配的文本片段和来源引用</li>
  <li><strong><code class="language-plaintext highlighter-rouge">memory_get</code></strong>：读取记忆文件的指定行范围</li>
</ul>

<p>Agent 的 system prompt 里有一条显式指令（<code class="language-plaintext highlighter-rouge">src/agents/system-prompt.ts</code>）：</p>

<blockquote>
  <p>"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines."</p>
</blockquote>

<p>也就是说，Agent 被<strong>教会了</strong>要先查记忆再回答——这不是硬编码的检索逻辑，而是通过 prompt 引导 Agent 自主决定何时、搜什么。</p>

<h4 id="heading-可选lancedb-向量后端">可选：LanceDB 向量后端</h4>

<p>除了内置的 SQLite 方案，<code class="language-plaintext highlighter-rouge">extensions/memory-lancedb/</code> 提供了一个可选的 <strong>LanceDB</strong> 后端（基于 Apache Arrow 的向量数据库）。它用 L2 距离做向量搜索，支持按类别（conversation / task / decision / context）组织记忆，适合对向量检索性能有更高要求的场景。</p>

<h3 id="heading-skills-技能系统插件市场">Skills 技能系统——插件市场</h3>

<p><code class="language-plaintext highlighter-rouge">skills/</code> 目录和 <code class="language-plaintext highlighter-rouge">extensions/</code> 目录是两套不同的扩展机制：</p>

<ul>
  <li><strong>Extensions</strong>：核心功能扩展，消息渠道、认证提供商等，随主程序一起发布</li>
  <li><strong>Skills</strong>：轻量级技能包，社区发布，用户按需安装</li>
</ul>

<p>一个 Skill 本质上就是一个 <strong>SKILL.md</strong> 文件——Markdown 格式，头部用 YAML frontmatter 声明元数据（名称、描述、依赖的工具、安装方式），正文用自然语言告诉 Agent 这个 Skill 怎么用、什么时候用：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>skills/github/
└── SKILL.md      ← YAML frontmatter（元数据）+ Markdown（使用说明）
</code></pre></div></div>

<p>举个例子，<code class="language-plaintext highlighter-rouge">skills/github/SKILL.md</code> 的 frontmatter 里声明了需要 <code class="language-plaintext highlighter-rouge">gh</code> CLI，并提供了 brew/apt 两种安装方式；正文则包含"什么场景该用 / 不该用"和常用命令模板。Agent 会根据这些信息自主决定何时调用、怎么调用。</p>

<p>没有单独的 <code class="language-plaintext highlighter-rouge">tools.yaml</code>——工具定义直接内嵌在 Markdown 里。这种设计的好处是<strong>一个文件就是一个完整的技能包</strong>，降低了社区贡献门槛。</p>

<p>社区的 ClawHub 上目前有超过 13,000 个社区技能。这是 OpenClaw 最大的护城河——生态。</p>

<hr />

<h2 id="heading-第八层基础设施安全和配置的地基">第八层：基础设施——安全和配置的地基</h2>

<p>最底层的 <code class="language-plaintext highlighter-rouge">src/config/</code> 和 <code class="language-plaintext highlighter-rouge">src/infra/</code>（300+ 个文件）支撑着整个系统：</p>

<p><strong>配置系统</strong>：支持 JSON5 格式（允许注释和尾逗号），密钥引用（Secret Refs）可以指向环境变量、文件或命令输出，在配置导出时自动替换为占位符，防止明文泄露。</p>

<p><strong>执行安全</strong>：<code class="language-plaintext highlighter-rouge">src/infra/exec-approvals.ts</code> 维护一个动态的命令白名单，支持 <code class="language-plaintext highlighter-rouge">safe-bin</code>（安全二进制）、<code class="language-plaintext highlighter-rouge">custom-allowlist</code>（自定义白名单）、<code class="language-plaintext highlighter-rouge">always-ask</code>（总是询问）三种策略。每条命令执行前还会通过 <code class="language-plaintext highlighter-rouge">detectObfuscation()</code> 检测是否有混淆注入。</p>

<p><strong>路径沙箱</strong>：<code class="language-plaintext highlighter-rouge">boundary-path.ts</code> 确保文件操作不会逃逸出配置的沙箱根目录；<code class="language-plaintext highlighter-rouge">archive-path.ts</code> 防止 zip-slip 攻击；<code class="language-plaintext highlighter-rouge">hardlink-guards.ts</code> 通过检查 inode 数防止硬链接攻击。</p>

<hr />

<h2 id="heading-结语没有黑魔法">结语：没有黑魔法</h2>

<p>读完这八层，回到开头的结论：<strong>其实没那么玄。</strong></p>

<p>OpenClaw 的每一层都不是从零发明的——Baileys 是现成的，Playwright 是现成的，SQLite + FTS5 是现成的，Claude Code / Codex 是现成的。它真正做的事情是<strong>把这些东西粘成一个 7×24 在线的、多渠道的、有记忆的 Agent 系统</strong>，然后在每一层都留下扩展口。</p>

<p>如果要用一句话总结它的设计哲学：<strong>用自然语言代替机器语言</strong>。BOOT.md 是自然语言的启动脚本，SKILL.md 是自然语言的插件包，记忆系统是自然语言的数据库查询。这不是"套壳"——这是一种新的系统设计范式，把 AI 当成操作系统的一等公民。</p>

<p>下一篇，我们不光看，还要动手——用现有的开源工具，搭一个简易版的 OpenClaw。</p>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="AI" /><category term="Agent" /><category term="OpenClaw" /><category term="开源" /><category term="架构" /><summary type="html"><![CDATA[OpenClaw 两个月 14 万 Star，我把源码读了一遍，搞清楚了它的八层架构。]]></summary></entry><entry><title type="html">企业权限系统技术方案</title><link href="https://sut-gc.github.io/blog/2026/03/05/%E4%BC%81%E4%B8%9A%E6%9D%83%E9%99%90%E7%B3%BB%E7%BB%9F%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88/" rel="alternate" type="text/html" title="企业权限系统技术方案" /><published>2026-03-05T00:00:00+00:00</published><updated>2026-03-05T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/03/05/%E4%BC%81%E4%B8%9A%E6%9D%83%E9%99%90%E7%B3%BB%E7%BB%9F%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/03/05/%E4%BC%81%E4%B8%9A%E6%9D%83%E9%99%90%E7%B3%BB%E7%BB%9F%E6%8A%80%E6%9C%AF%E6%96%B9%E6%A1%88/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-企业权限系统技术方案" id="markdown-toc-heading-企业权限系统技术方案">企业权限系统技术方案</a>    <ul>
      <li><a href="#heading-一核心设计思路" id="markdown-toc-heading-一核心设计思路">一、核心设计思路</a></li>
      <li><a href="#heading-二表结构设计" id="markdown-toc-heading-二表结构设计">二、表结构设计</a>        <ul>
          <li><a href="#heading-21-权限点表-sys_permission" id="markdown-toc-heading-21-权限点表-sys_permission">2.1 权限点表 <code class="language-plaintext highlighter-rouge">sys_permission</code></a></li>
          <li><a href="#heading-22-数据资源点表-sys_data_resource" id="markdown-toc-heading-22-数据资源点表-sys_data_resource">2.2 数据资源点表 <code class="language-plaintext highlighter-rouge">sys_data_resource</code></a></li>
          <li><a href="#heading-23-数据资源点可选范围表-sys_data_resource_scope" id="markdown-toc-heading-23-数据资源点可选范围表-sys_data_resource_scope">2.3 数据资源点可选范围表 <code class="language-plaintext highlighter-rouge">sys_data_resource_scope</code></a></li>
          <li><a href="#heading-24--范围数据字典表-sys_scope_data" id="markdown-toc-heading-24--范围数据字典表-sys_scope_data">2.4 ⭐ 范围数据字典表 <code class="language-plaintext highlighter-rouge">sys_scope_data</code></a></li>
          <li><a href="#heading-25-角色表-sys_role" id="markdown-toc-heading-25-角色表-sys_role">2.5 角色表 <code class="language-plaintext highlighter-rouge">sys_role</code></a></li>
          <li><a href="#heading-26-用户-角色关系表-sys_user_role" id="markdown-toc-heading-26-用户-角色关系表-sys_user_role">2.6 用户-角色关系表 <code class="language-plaintext highlighter-rouge">sys_user_role</code></a></li>
          <li><a href="#heading-27-角色-权限点关联表-sys_role_permission" id="markdown-toc-heading-27-角色-权限点关联表-sys_role_permission">2.7 角色-权限点关联表 <code class="language-plaintext highlighter-rouge">sys_role_permission</code></a></li>
          <li><a href="#heading-28--用户-权限点授权表-sys_user_permission_grant" id="markdown-toc-heading-28--用户-权限点授权表-sys_user_permission_grant">2.8 ⭐ 用户-权限点授权表 <code class="language-plaintext highlighter-rouge">sys_user_permission_grant</code></a></li>
          <li><a href="#heading-29-授权实体明细表-sys_grant_scope_entity" id="markdown-toc-heading-29-授权实体明细表-sys_grant_scope_entity">2.9 授权实体明细表 <code class="language-plaintext highlighter-rouge">sys_grant_scope_entity</code></a></li>
          <li><a href="#heading-210-权限操作审计日志表-sys_permission_audit_log" id="markdown-toc-heading-210-权限操作审计日志表-sys_permission_audit_log">2.10 权限操作审计日志表 <code class="language-plaintext highlighter-rouge">sys_permission_audit_log</code></a></li>
        </ul>
      </li>
      <li><a href="#heading-三关键设计决策说明" id="markdown-toc-heading-三关键设计决策说明">三、关键设计决策说明</a>        <ul>
          <li><a href="#heading-31-同一用户同一权限点可以有多条记录" id="markdown-toc-heading-31-同一用户同一权限点可以有多条记录">3.1 同一用户同一权限点可以有多条记录</a></li>
          <li><a href="#heading-32-角色变更不影响已有授权" id="markdown-toc-heading-32-角色变更不影响已有授权">3.2 角色变更不影响已有授权</a></li>
          <li><a href="#heading-33-撤销授权的两种粒度" id="markdown-toc-heading-33-撤销授权的两种粒度">3.3 撤销授权的两种粒度</a></li>
          <li><a href="#heading-34-超管优先级与判断链路" id="markdown-toc-heading-34-超管优先级与判断链路">3.4 超管优先级与判断链路</a></li>
        </ul>
      </li>
      <li><a href="#heading-四授权流程" id="markdown-toc-heading-四授权流程">四、授权流程</a>        <ul>
          <li><a href="#heading-41-通过角色批量授权" id="markdown-toc-heading-41-通过角色批量授权">4.1 通过角色批量授权</a></li>
          <li><a href="#heading-42-单独授权某个权限点" id="markdown-toc-heading-42-单独授权某个权限点">4.2 单独授权某个权限点</a></li>
        </ul>
      </li>
      <li><a href="#heading-五运行时校验流程" id="markdown-toc-heading-五运行时校验流程">五、运行时校验流程</a>        <ul>
          <li><a href="#heading-51-加载用户权限" id="markdown-toc-heading-51-加载用户权限">5.1 加载用户权限</a></li>
          <li><a href="#heading-52-操作权限校验aop" id="markdown-toc-heading-52-操作权限校验aop">5.2 操作权限校验（AOP）</a></li>
          <li><a href="#heading-53-数据权限注入mybatis-拦截器" id="markdown-toc-heading-53-数据权限注入mybatis-拦截器">5.3 数据权限注入（MyBatis 拦截器）</a></li>
        </ul>
      </li>
      <li><a href="#heading-六缓存设计" id="markdown-toc-heading-六缓存设计">六、缓存设计</a></li>
      <li><a href="#heading-七系统架构图" id="markdown-toc-heading-七系统架构图">七、系统架构图</a></li>
      <li><a href="#heading-八er-图" id="markdown-toc-heading-八er-图">八、ER 图</a></li>
      <li><a href="#heading-九接口设计" id="markdown-toc-heading-九接口设计">九、接口设计</a>        <ul>
          <li><a href="#heading-91-管理侧接口" id="markdown-toc-heading-91-管理侧接口">9.1 管理侧接口</a></li>
          <li><a href="#heading-92-使用侧接口" id="markdown-toc-heading-92-使用侧接口">9.2 使用侧接口</a></li>
        </ul>
      </li>
      <li><a href="#heading-十多租户扩展方案" id="markdown-toc-heading-十多租户扩展方案">十、多租户扩展方案</a>        <ul>
          <li><a href="#heading-101-核心设计思路" id="markdown-toc-heading-101-核心设计思路">10.1 核心设计思路</a></li>
          <li><a href="#heading-102-新增表" id="markdown-toc-heading-102-新增表">10.2 新增表</a></li>
          <li><a href="#heading-103-现有表字段改动" id="markdown-toc-heading-103-现有表字段改动">10.3 现有表字段改动</a></li>
          <li><a href="#heading-104-鉴权链路改动" id="markdown-toc-heading-104-鉴权链路改动">10.4 鉴权链路改动</a></li>
          <li><a href="#heading-105-用户切换租户" id="markdown-toc-heading-105-用户切换租户">10.5 用户切换租户</a></li>
          <li><a href="#heading-106-缓存-key-加入-tenant_id" id="markdown-toc-heading-106-缓存-key-加入-tenant_id">10.6 缓存 Key 加入 tenant_id</a></li>
          <li><a href="#heading-107-改动量总结" id="markdown-toc-heading-107-改动量总结">10.7 改动量总结</a></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h1 id="heading-企业权限系统技术方案">企业权限系统技术方案</h1>

<blockquote>
  <p>核心调整：Role 是 Permission 的打包模板，真正的授权单元是 Permission，过期时间挂在用户-权限点授权上。数据范围通过 sys_scope_data 统一管理，外部系统只需对接同步任务即可接入新的数据维度。</p>
</blockquote>

<hr />

<h2 id="heading-一核心设计思路">一、核心设计思路</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Role
  └─ 只是权限点的集合模板，方便批量操作，本身不是授权单元

Permission（权限点）
  └─ 才是真正的授权单元
  └─ 关联一个 DataResource（数据资源点）

用户授权流程：
  方式一：选角色 → 展开角色下所有权限点 → 逐个配置数据范围 + 过期时间 → 批量写入授权记录
  方式二：直接选权限点 → 配置数据范围 + 过期时间 → 写入单条授权记录

授权记录（sys_user_permission_grant）
  └─ 一条记录 = 用户拥有某个权限点的一次授权
  └─ 记录来源（通过哪个角色批量授予 or 单独授予）
  └─ 记录数据范围
  └─ 记录过期时间
</code></pre></div></div>

<hr />

<h2 id="heading-二表结构设计">二、表结构设计</h2>

<h3 id="heading-21-权限点表-sys_permission">2.1 权限点表 <code class="language-plaintext highlighter-rouge">sys_permission</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_permission</span> <span class="p">(</span>
  <span class="n">id</span>               <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">tenant_id</span>        <span class="nb">BIGINT</span>       <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">,</span>
  <span class="n">perm_code</span>        <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'权限编码，如 order:view'</span><span class="p">,</span>
  <span class="n">perm_name</span>        <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'权限名称'</span><span class="p">,</span>
  <span class="k">type</span>             <span class="nb">TINYINT</span>      <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="mi">2</span> <span class="k">COMMENT</span> <span class="s1">'1菜单 2操作按钮 3API'</span><span class="p">,</span>
  <span class="n">data_resource_id</span> <span class="nb">BIGINT</span>       <span class="k">COMMENT</span> <span class="s1">'关联的数据资源点，NULL 表示无数据权限'</span><span class="p">,</span>
  <span class="n">sort</span>             <span class="nb">INT</span>          <span class="k">DEFAULT</span> <span class="mi">0</span><span class="p">,</span>
  <span class="n">remark</span>           <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
  <span class="n">created_at</span>       <span class="nb">DATETIME</span>     <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>

  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="n">uk_tenant_perm_code</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">perm_code</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<hr />

<h3 id="heading-22-数据资源点表-sys_data_resource">2.2 数据资源点表 <code class="language-plaintext highlighter-rouge">sys_data_resource</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_data_resource</span> <span class="p">(</span>
  <span class="n">id</span>            <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">resource_code</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">UNIQUE</span> <span class="k">COMMENT</span> <span class="s1">'资源编码，如 data:order'</span><span class="p">,</span>
  <span class="n">resource_name</span> <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'资源名称，如 订单数据'</span><span class="p">,</span>
  <span class="n">description</span>   <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
  <span class="n">created_at</span>    <span class="nb">DATETIME</span> <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span>
<span class="p">);</span>
</code></pre></div></div>

<hr />

<h3 id="heading-23-数据资源点可选范围表-sys_data_resource_scope">2.3 数据资源点可选范围表 <code class="language-plaintext highlighter-rouge">sys_data_resource_scope</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 元数据：定义某个数据资源点开放哪些范围选项</span>
<span class="c1">-- 授权配置时用，运行时不参与请求链路</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_data_resource_scope</span> <span class="p">(</span>
  <span class="n">id</span>               <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">data_resource_id</span> <span class="nb">BIGINT</span>       <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">scope_code</span>       <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>  <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'范围编码，如 ALL / DEPT / SELF / CITY / PROJECT'</span><span class="p">,</span>
  <span class="n">scope_name</span>       <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'展示给管理员的名称'</span><span class="p">,</span>
  <span class="n">scope_type</span>       <span class="nb">TINYINT</span>      <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'1全量 2部门树 3本人 4实体列表（对接 sys_scope_data）'</span><span class="p">,</span>
  <span class="n">sort</span>             <span class="nb">INT</span>          <span class="k">DEFAULT</span> <span class="mi">0</span><span class="p">,</span>

  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="n">uk_resource_scope</span> <span class="p">(</span><span class="n">data_resource_id</span><span class="p">,</span> <span class="n">scope_code</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">scope_type = 4</code> 表示该范围的候选值来自 <code class="language-plaintext highlighter-rouge">sys_scope_data</code>，前端授权页面通过 <code class="language-plaintext highlighter-rouge">scope_code</code> 去 <code class="language-plaintext highlighter-rouge">sys_scope_data</code> 查候选列表，不需要调任何外部接口。</p>
</blockquote>

<hr />

<h3 id="heading-24--范围数据字典表-sys_scope_data">2.4 ⭐ 范围数据字典表 <code class="language-plaintext highlighter-rouge">sys_scope_data</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 统一存储所有数据范围的候选值</span>
<span class="c1">-- 外部系统通过同步任务写入，权限系统内部完全自洽</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_scope_data</span> <span class="p">(</span>
  <span class="n">id</span>           <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">scope_code</span>   <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>  <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'范围类型，对应 sys_data_resource_scope.scope_code，如 CITY / PROJECT'</span><span class="p">,</span>
  <span class="n">entity_id</span>    <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>  <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'外部实体ID，VARCHAR 兼容各种ID类型'</span><span class="p">,</span>
  <span class="n">entity_name</span>  <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'实体名称，授权页面展示用'</span><span class="p">,</span>
  <span class="n">entity_extra</span> <span class="n">JSON</span>         <span class="k">COMMENT</span> <span class="s1">'扩展信息，如层级路径、所属上级等'</span><span class="p">,</span>
  <span class="n">status</span>       <span class="nb">TINYINT</span>      <span class="k">DEFAULT</span> <span class="mi">1</span> <span class="k">COMMENT</span> <span class="s1">'1有效 0已下线'</span><span class="p">,</span>
  <span class="n">synced_at</span>    <span class="nb">DATETIME</span>     <span class="k">COMMENT</span> <span class="s1">'最后同步时间'</span><span class="p">,</span>

  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="n">uk_scope_entity</span> <span class="p">(</span><span class="n">scope_code</span><span class="p">,</span> <span class="n">entity_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_scope_code</span> <span class="p">(</span><span class="n">scope_code</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">entity_extra</code> 示例：</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">城市，带省份信息</span><span class="w">
</span><span class="p">{</span><span class="w"> </span><span class="nl">"province"</span><span class="p">:</span><span class="w"> </span><span class="s2">"广东省"</span><span class="p">,</span><span class="w"> </span><span class="nl">"province_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"44"</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">项目，带所属业务线</span><span class="w">
</span><span class="p">{</span><span class="w"> </span><span class="nl">"biz_line"</span><span class="p">:</span><span class="w"> </span><span class="s2">"电商"</span><span class="p">,</span><span class="w"> </span><span class="nl">"owner"</span><span class="p">:</span><span class="w"> </span><span class="s2">"张三"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>外部系统接入方式（唯一入口）：</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">sys_scope_data</span> <span class="p">(</span><span class="n">scope_code</span><span class="p">,</span> <span class="n">entity_id</span><span class="p">,</span> <span class="n">entity_name</span><span class="p">,</span> <span class="n">entity_extra</span><span class="p">,</span> <span class="n">synced_at</span><span class="p">)</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="s1">'CITY'</span><span class="p">,</span> <span class="s1">'101'</span><span class="p">,</span> <span class="s1">'上海'</span><span class="p">,</span> <span class="s1">'{"province":"上海市"}'</span><span class="p">,</span> <span class="n">NOW</span><span class="p">())</span>
<span class="k">ON</span> <span class="n">DUPLICATE</span> <span class="k">KEY</span> <span class="k">UPDATE</span>
  <span class="n">entity_name</span>  <span class="o">=</span> <span class="k">VALUES</span><span class="p">(</span><span class="n">entity_name</span><span class="p">),</span>
  <span class="n">entity_extra</span> <span class="o">=</span> <span class="k">VALUES</span><span class="p">(</span><span class="n">entity_extra</span><span class="p">),</span>
  <span class="n">synced_at</span>    <span class="o">=</span> <span class="k">VALUES</span><span class="p">(</span><span class="n">synced_at</span><span class="p">);</span>
</code></pre></div></div>

<blockquote>
  <p>新接入一种数据维度（如"项目"、"区域"），只需要：</p>
  <ol>
    <li>在 <code class="language-plaintext highlighter-rouge">sys_data_resource_scope</code> 新增一条 scope_type=4 的配置记录</li>
    <li>跑一个同步任务往 <code class="language-plaintext highlighter-rouge">sys_scope_data</code> 写数据</li>
  </ol>

  <p>权限系统本身零改动。</p>
</blockquote>

<hr />

<h3 id="heading-25-角色表-sys_role">2.5 角色表 <code class="language-plaintext highlighter-rouge">sys_role</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Role 只是权限点的打包模板，不是授权单元</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_role</span> <span class="p">(</span>
  <span class="n">id</span>          <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">tenant_id</span>   <span class="nb">BIGINT</span>       <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">,</span>
  <span class="n">role_code</span>   <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>  <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'角色编码'</span><span class="p">,</span>
  <span class="n">role_name</span>   <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">is_super</span>    <span class="nb">TINYINT</span>      <span class="k">DEFAULT</span> <span class="mi">0</span> <span class="k">COMMENT</span> <span class="s1">'1超管角色，鉴权时直接放行，无需配置权限点'</span><span class="p">,</span>
  <span class="n">status</span>      <span class="nb">TINYINT</span>      <span class="k">DEFAULT</span> <span class="mi">1</span><span class="p">,</span>
  <span class="n">remark</span>      <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span>
  <span class="n">created_at</span>  <span class="nb">DATETIME</span>     <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>

  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="n">uk_tenant_role_code</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">role_code</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<hr />

<h3 id="heading-26-用户-角色关系表-sys_user_role">2.6 用户-角色关系表 <code class="language-plaintext highlighter-rouge">sys_user_role</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 记录"这个人被分配了哪些角色"这件事本身</span>
<span class="c1">-- 职责：角色列表展示、角色级撤销/续期操作</span>
<span class="c1">-- 不参与鉴权，运行时权限校验只走 sys_user_permission_grant</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_user_role</span> <span class="p">(</span>
  <span class="n">id</span>          <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">user_id</span>     <span class="nb">BIGINT</span>   <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">role_id</span>     <span class="nb">BIGINT</span>   <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">grant_by</span>    <span class="nb">BIGINT</span>   <span class="k">COMMENT</span> <span class="s1">'授权操作人 user_id'</span><span class="p">,</span>
  <span class="n">start_time</span>  <span class="nb">DATETIME</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span> <span class="k">COMMENT</span> <span class="s1">'生效时间'</span><span class="p">,</span>
  <span class="n">expire_time</span> <span class="nb">DATETIME</span> <span class="k">COMMENT</span> <span class="s1">'过期时间，冗余字段，仅用于展示，鉴权以 sys_user_permission_grant 为准'</span><span class="p">,</span>
  <span class="n">status</span>      <span class="nb">TINYINT</span>  <span class="k">DEFAULT</span> <span class="mi">1</span> <span class="k">COMMENT</span> <span class="s1">'1有效 0已撤销'</span><span class="p">,</span>
  <span class="n">created_at</span>  <span class="nb">DATETIME</span> <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>

  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="n">uk_user_role</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">role_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_user_id</span> <span class="p">(</span><span class="n">user_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_role_id</span> <span class="p">(</span><span class="n">role_id</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<blockquote>
  <p><strong>职责边界：</strong></p>
  <ul>
    <li>写入时机：管理员通过角色授权，同时写入本表（1条）+ <code class="language-plaintext highlighter-rouge">sys_user_permission_grant</code>（N条，每个权限点一条）</li>
    <li>读取场景：展示用户持有的角色列表、角色续期、一键撤销整个角色</li>
    <li><code class="language-plaintext highlighter-rouge">expire_time</code> 冗余存储，方便 UI 展示剩余有效期，但鉴权不依赖此字段</li>
  </ul>
</blockquote>

<blockquote>
  <p>与 <code class="language-plaintext highlighter-rouge">sys_user_permission_grant</code> 的联动逻辑：</p>
  <ul>
    <li>给用户分配角色 → 写入 <code class="language-plaintext highlighter-rouge">sys_user_role</code> + 展开权限点批量写入 <code class="language-plaintext highlighter-rouge">sys_user_permission_grant</code>（source_type=1, source_id=role_id）</li>
    <li>撤销用户角色 → DELETE <code class="language-plaintext highlighter-rouge">sys_user_role</code> + UPDATE <code class="language-plaintext highlighter-rouge">sys_user_permission_grant</code> SET status=0 WHERE source_type=1 AND source_id=role_id</li>
    <li>查看用户拥有哪些角色 → 查 <code class="language-plaintext highlighter-rouge">sys_user_role</code> JOIN <code class="language-plaintext highlighter-rouge">sys_role</code></li>
  </ul>
</blockquote>

<hr />

<h3 id="heading-27-角色-权限点关联表-sys_role_permission">2.7 角色-权限点关联表 <code class="language-plaintext highlighter-rouge">sys_role_permission</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 仅描述角色包含哪些权限点，是模板定义，不涉及授权</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_role_permission</span> <span class="p">(</span>
  <span class="n">id</span>            <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">role_id</span>       <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">permission_id</span> <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>

  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="n">uk_role_perm</span> <span class="p">(</span><span class="n">role_id</span><span class="p">,</span> <span class="n">permission_id</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<hr />

<h3 id="heading-28--用户-权限点授权表-sys_user_permission_grant">2.8 ⭐ 用户-权限点授权表 <code class="language-plaintext highlighter-rouge">sys_user_permission_grant</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 核心授权表：真正的授权单元是权限点，过期时间在这里</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_user_permission_grant</span> <span class="p">(</span>
  <span class="n">id</span>               <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">user_id</span>          <span class="nb">BIGINT</span>       <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'被授权用户'</span><span class="p">,</span>
  <span class="n">permission_id</span>    <span class="nb">BIGINT</span>       <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'授权的权限点'</span><span class="p">,</span>
  <span class="n">data_resource_id</span> <span class="nb">BIGINT</span>       <span class="k">COMMENT</span> <span class="s1">'关联的数据资源点（冗余存储，方便查询）'</span><span class="p">,</span>
  <span class="n">scope_code</span>       <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>  <span class="k">COMMENT</span> <span class="s1">'数据范围编码，来自资源点可选范围的子集'</span><span class="p">,</span>
  <span class="c1">-- 具体的实体ID列表已拆出到 sys_grant_scope_entity，不再存 JSON</span>

  <span class="c1">-- 授权来源</span>
  <span class="n">source_type</span>      <span class="nb">TINYINT</span>      <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'1通过角色批量授予 2单独授予 （可扩展：3岗位模板 4用户组等）'</span><span class="p">,</span>
  <span class="n">source_id</span>        <span class="nb">BIGINT</span>       <span class="k">COMMENT</span> <span class="s1">'来源ID，与 source_type 配合使用，source_type=2 时为 NULL'</span><span class="p">,</span>
  <span class="n">grant_by</span>         <span class="nb">BIGINT</span>       <span class="k">COMMENT</span> <span class="s1">'授权操作人 user_id'</span><span class="p">,</span>

  <span class="c1">-- 有效期</span>
  <span class="n">start_time</span>       <span class="nb">DATETIME</span>     <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span> <span class="k">COMMENT</span> <span class="s1">'生效时间'</span><span class="p">,</span>
  <span class="n">expire_time</span>      <span class="nb">DATETIME</span>     <span class="k">COMMENT</span> <span class="s1">'过期时间，NULL 表示永久有效'</span><span class="p">,</span>

  <span class="n">status</span>           <span class="nb">TINYINT</span>      <span class="k">DEFAULT</span> <span class="mi">1</span> <span class="k">COMMENT</span> <span class="s1">'1有效 0已撤销'</span><span class="p">,</span>
  <span class="n">revoked_at</span>       <span class="nb">DATETIME</span>     <span class="k">COMMENT</span> <span class="s1">'撤销时间，status 变为 0 时记录，用于审计'</span><span class="p">,</span>
  <span class="n">created_at</span>       <span class="nb">DATETIME</span>     <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>
  <span class="n">updated_at</span>       <span class="nb">DATETIME</span>     <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span> <span class="k">ON</span> <span class="k">UPDATE</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>

  <span class="k">KEY</span> <span class="n">idx_user_id</span> <span class="p">(</span><span class="n">user_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_expire_time</span> <span class="p">(</span><span class="n">expire_time</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_source</span> <span class="p">(</span><span class="n">source_type</span><span class="p">,</span> <span class="n">source_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_user_perm</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">permission_id</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<hr />

<h3 id="heading-29-授权实体明细表-sys_grant_scope_entity">2.9 授权实体明细表 <code class="language-plaintext highlighter-rouge">sys_grant_scope_entity</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 将原 custom_value JSON 拆成关系表</span>
<span class="c1">-- 支持正向查（用户被授权了哪些实体）和反查（某实体被授权给了哪些用户）</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_grant_scope_entity</span> <span class="p">(</span>
  <span class="n">id</span>           <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">grant_id</span>     <span class="nb">BIGINT</span>      <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'关联 sys_user_permission_grant.id'</span><span class="p">,</span>
  <span class="n">scope_code</span>   <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'范围类型，如 CITY / PROJECT'</span><span class="p">,</span>
  <span class="n">entity_id</span>    <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'实体ID，对应 sys_scope_data.entity_id'</span><span class="p">,</span>

  <span class="k">KEY</span> <span class="n">idx_grant_id</span> <span class="p">(</span><span class="n">grant_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_entity</span> <span class="p">(</span><span class="n">scope_code</span><span class="p">,</span> <span class="n">entity_id</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>查询示例：</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 正向查：张三在 order:view 权限下被授权了哪些城市</span>
<span class="k">SELECT</span> <span class="n">e</span><span class="p">.</span><span class="n">entity_id</span><span class="p">,</span> <span class="n">e</span><span class="p">.</span><span class="n">scope_code</span>
<span class="k">FROM</span> <span class="n">sys_grant_scope_entity</span> <span class="n">e</span>
<span class="k">JOIN</span> <span class="n">sys_user_permission_grant</span> <span class="k">g</span> <span class="k">ON</span> <span class="k">g</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">e</span><span class="p">.</span><span class="n">grant_id</span>
<span class="k">JOIN</span> <span class="n">sys_permission</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">p</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="k">g</span><span class="p">.</span><span class="n">permission_id</span>
<span class="k">WHERE</span> <span class="k">g</span><span class="p">.</span><span class="n">tenant_id</span> <span class="o">=</span> <span class="o">#</span><span class="p">{</span><span class="n">tenantId</span><span class="p">}</span>
  <span class="k">AND</span> <span class="k">g</span><span class="p">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="o">#</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span>
  <span class="k">AND</span> <span class="n">p</span><span class="p">.</span><span class="n">perm_code</span> <span class="o">=</span> <span class="s1">'order:view'</span>
  <span class="k">AND</span> <span class="k">g</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="k">AND</span> <span class="p">(</span><span class="k">g</span><span class="p">.</span><span class="n">expire_time</span> <span class="k">IS</span> <span class="k">NULL</span> <span class="k">OR</span> <span class="k">g</span><span class="p">.</span><span class="n">expire_time</span> <span class="o">&gt;</span> <span class="n">NOW</span><span class="p">());</span>

<span class="c1">-- 反查：城市101被授权给了哪些用户</span>
<span class="k">SELECT</span> <span class="k">g</span><span class="p">.</span><span class="n">user_id</span><span class="p">,</span> <span class="k">g</span><span class="p">.</span><span class="n">expire_time</span>
<span class="k">FROM</span> <span class="n">sys_grant_scope_entity</span> <span class="n">e</span>
<span class="k">JOIN</span> <span class="n">sys_user_permission_grant</span> <span class="k">g</span> <span class="k">ON</span> <span class="k">g</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">e</span><span class="p">.</span><span class="n">grant_id</span>
<span class="k">WHERE</span> <span class="n">e</span><span class="p">.</span><span class="n">scope_code</span> <span class="o">=</span> <span class="s1">'CITY'</span>
  <span class="k">AND</span> <span class="n">e</span><span class="p">.</span><span class="n">entity_id</span> <span class="o">=</span> <span class="s1">'101'</span>
  <span class="k">AND</span> <span class="k">g</span><span class="p">.</span><span class="n">tenant_id</span> <span class="o">=</span> <span class="o">#</span><span class="p">{</span><span class="n">tenantId</span><span class="p">}</span>
  <span class="k">AND</span> <span class="k">g</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<blockquote>
  <p>注意：查询时必须 JOIN <code class="language-plaintext highlighter-rouge">sys_user_permission_grant</code> 带上 <code class="language-plaintext highlighter-rouge">tenant_id</code> 过滤，<code class="language-plaintext highlighter-rouge">sys_grant_scope_entity</code> 本身不存 tenant_id，通过 grant 的 tenant_id 完成租户隔离。</p>
</blockquote>

<hr />

<h3 id="heading-210-权限操作审计日志表-sys_permission_audit_log">2.10 权限操作审计日志表 <code class="language-plaintext highlighter-rouge">sys_permission_audit_log</code></h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 记录所有权限变更操作，用于审计和问题排查</span>
<span class="c1">-- 只追加写入，不做 UPDATE/DELETE</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_permission_audit_log</span> <span class="p">(</span>
  <span class="n">id</span>             <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">tenant_id</span>      <span class="nb">BIGINT</span>      <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">,</span>
  <span class="n">operator_id</span>    <span class="nb">BIGINT</span>      <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'操作人 user_id'</span><span class="p">,</span>
  <span class="n">target_user_id</span> <span class="nb">BIGINT</span>      <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'被操作用户 user_id'</span><span class="p">,</span>
  <span class="n">action</span>         <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'GRANT_ROLE / REVOKE_ROLE / GRANT_PERM / REVOKE_PERM / MODIFY_SCOPE / MODIFY_EXPIRE'</span><span class="p">,</span>
  <span class="n">source_type</span>    <span class="nb">TINYINT</span>     <span class="k">COMMENT</span> <span class="s1">'1角色授权 2单独授权'</span><span class="p">,</span>
  <span class="n">source_id</span>      <span class="nb">BIGINT</span>      <span class="k">COMMENT</span> <span class="s1">'关联角色ID等'</span><span class="p">,</span>
  <span class="n">biz_data</span>       <span class="n">JSON</span>        <span class="k">COMMENT</span> <span class="s1">'操作详情快照，如授权的权限点列表、数据范围等'</span><span class="p">,</span>
  <span class="n">created_at</span>     <span class="nb">DATETIME</span>    <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>

  <span class="k">KEY</span> <span class="n">idx_tenant_target</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">target_user_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_operator</span> <span class="p">(</span><span class="n">operator_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_created_at</span> <span class="p">(</span><span class="n">created_at</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">biz_data</code> 示例：</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"roleId"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
  </span><span class="nl">"roleName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"销售经理"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"permCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"order:view"</span><span class="p">,</span><span class="w"> </span><span class="nl">"scopeCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CITY"</span><span class="p">,</span><span class="w"> </span><span class="nl">"entityIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"101"</span><span class="p">,</span><span class="w"> </span><span class="s2">"102"</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"expireTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2027-03-05T00:00:00"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>审计日志只追加，不更新。所有授权变更操作完成后同步写入一条记录，配合 <code class="language-plaintext highlighter-rouge">revoked_at</code> 字段可完整还原权限的生命周期。</p>
</blockquote>

<hr />

<h2 id="heading-三关键设计决策说明">三、关键设计决策说明</h2>

<h3 id="heading-31-同一用户同一权限点可以有多条记录">3.1 同一用户同一权限点可以有多条记录</h3>

<p>场景：张三通过"销售角色"获得了<code class="language-plaintext highlighter-rouge">order:view</code>（永久），管理员又单独给他授予了<code class="language-plaintext highlighter-rouge">order:export</code>（30天）。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>user_id | permission_id | source_type | source_id  | expire_time
张三    | order:view    | 1(角色)     | 销售角色ID | NULL（永久）
张三    | order:export  | 2(单独)     | NULL       | 2026-04-01
</code></pre></div></div>

<p>查询时取所有 <code class="language-plaintext highlighter-rouge">status=1 AND (expire_time IS NULL OR expire_time &gt; NOW())</code> 的记录并集。</p>

<hr />

<h3 id="heading-32-角色变更不影响已有授权">3.2 角色变更不影响已有授权</h3>

<p>Role 只是批量授权的入口，授权完成后，记录独立存在。<br />
如果角色新增了一个权限点，<strong>不会自动同步</strong>给已授权的用户，需要重新授权（或提供"同步角色权限"的操作）。<br />
这样设计保证了<strong>授权记录的稳定性</strong>，避免角色变更产生意外的权限扩散。</p>

<blockquote>
  <p>如果业务需要"角色变更自动同步"，可以通过 <code class="language-plaintext highlighter-rouge">source_id</code>（配合 <code class="language-plaintext highlighter-rouge">source_type=1</code>）找到所有关联用户批量更新，但需要明确产品策略。</p>
</blockquote>

<hr />

<h3 id="heading-33-撤销授权的两种粒度">3.3 撤销授权的两种粒度</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 撤销单个权限点（同时记录撤销时间）</span>
<span class="k">UPDATE</span> <span class="n">sys_user_permission_grant</span>
<span class="k">SET</span> <span class="n">status</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">revoked_at</span> <span class="o">=</span> <span class="n">NOW</span><span class="p">()</span>
<span class="k">WHERE</span> <span class="n">id</span> <span class="o">=</span> <span class="o">#</span><span class="p">{</span><span class="n">grantId</span><span class="p">};</span>

<span class="c1">-- 撤销某次角色授权（批量撤销该角色带来的所有权限点）</span>
<span class="k">UPDATE</span> <span class="n">sys_user_permission_grant</span>
<span class="k">SET</span> <span class="n">status</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">revoked_at</span> <span class="o">=</span> <span class="n">NOW</span><span class="p">()</span>
<span class="k">WHERE</span> <span class="n">user_id</span> <span class="o">=</span> <span class="o">#</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span> <span class="k">AND</span> <span class="n">source_type</span> <span class="o">=</span> <span class="mi">1</span> <span class="k">AND</span> <span class="n">source_id</span> <span class="o">=</span> <span class="o">#</span><span class="p">{</span><span class="n">roleId</span><span class="p">};</span>
</code></pre></div></div>

<hr />

<h3 id="heading-34-超管优先级与判断链路">3.4 超管优先级与判断链路</h3>

<p>鉴权链路中，超管判断优先于一切权限点校验，按以下顺序执行：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. JWT 解析 → 拿到 userId + tenantId
        ↓
2. 查 sys_user_tenant.is_super
   → is_super = 1：租户内超管，直接放行，跳过所有后续校验
        ↓
3. 查 sys_user_role JOIN sys_role WHERE is_super = 1
   → 用户持有超管角色：直接放行
        ↓
4. 普通权限点校验：查 sys_user_permission_grant
        ↓
5. 数据范围注入：根据 scope_code 注入 WHERE 条件
</code></pre></div></div>

<blockquote>
  <p><strong>两种超管的区别：</strong></p>
  <ul>
    <li><code class="language-plaintext highlighter-rouge">sys_user_tenant.is_super</code>：平台运营直接在用户-租户关系上打标，不占用角色资源</li>
    <li><code class="language-plaintext highlighter-rouge">sys_role.is_super</code>：通过分配超管角色实现，可以走正常的角色管理流程授权/撤销，更灵活</li>
  </ul>
</blockquote>

<hr />

<h2 id="heading-四授权流程">四、授权流程</h2>

<h3 id="heading-41-通过角色批量授权">4.1 通过角色批量授权</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 管理员选择用户 + 角色
2. 系统查询角色下所有权限点（sys_role_permission JOIN sys_permission）
3. 对每个权限点，查出其关联的数据资源点及可选范围
4. 管理员逐一配置：数据范围 + 过期时间（可批量设置默认值）
5. 提交 → 批量写入 sys_user_permission_grant（每个权限点一条记录）
   source_type = 1, source_id = 角色ID
</code></pre></div></div>

<h3 id="heading-42-单独授权某个权限点">4.2 单独授权某个权限点</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 管理员选择用户 + 权限点
2. 系统展示该权限点关联的数据资源点及可选范围
3. 管理员配置：数据范围 + 过期时间
4. 提交 → 写入一条 sys_user_permission_grant
   source_type = 2, source_id = NULL
</code></pre></div></div>

<hr />

<h2 id="heading-五运行时校验流程">五、运行时校验流程</h2>

<h3 id="heading-51-加载用户权限">5.1 加载用户权限</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">p</span><span class="p">.</span><span class="n">perm_code</span><span class="p">,</span>
  <span class="k">g</span><span class="p">.</span><span class="n">data_resource_id</span><span class="p">,</span>
  <span class="k">g</span><span class="p">.</span><span class="n">scope_code</span><span class="p">,</span>
  <span class="k">g</span><span class="p">.</span><span class="n">expire_time</span>
<span class="k">FROM</span> <span class="n">sys_user_permission_grant</span> <span class="k">g</span>
<span class="k">JOIN</span> <span class="n">sys_permission</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">p</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="k">g</span><span class="p">.</span><span class="n">permission_id</span>
<span class="k">WHERE</span> <span class="k">g</span><span class="p">.</span><span class="n">user_id</span> <span class="o">=</span> <span class="o">#</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span>
  <span class="k">AND</span> <span class="k">g</span><span class="p">.</span><span class="n">status</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="k">AND</span> <span class="p">(</span><span class="k">g</span><span class="p">.</span><span class="n">expire_time</span> <span class="k">IS</span> <span class="k">NULL</span> <span class="k">OR</span> <span class="k">g</span><span class="p">.</span><span class="n">expire_time</span> <span class="o">&gt;</span> <span class="n">NOW</span><span class="p">())</span>
</code></pre></div></div>

<h3 id="heading-52-操作权限校验aop">5.2 操作权限校验（AOP）</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@RequiresPermission</span><span class="o">(</span><span class="s">"order:view"</span><span class="o">)</span>
<span class="c1">// AOP 拦截 → 检查用户权限集合中是否包含 "order:view"</span>
</code></pre></div></div>

<h3 id="heading-53-数据权限注入mybatis-拦截器">5.3 数据权限注入（MyBatis 拦截器）</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">GrantInfo</span> <span class="n">grant</span> <span class="o">=</span> <span class="n">permissionCache</span><span class="o">.</span><span class="na">getGrant</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="s">"order:view"</span><span class="o">);</span>

<span class="k">switch</span> <span class="o">(</span><span class="n">grant</span><span class="o">.</span><span class="na">getScopeCode</span><span class="o">())</span> <span class="o">{</span>
    <span class="k">case</span> <span class="s">"ALL"</span><span class="o">:</span>            <span class="c1">// 不追加条件</span>
    <span class="k">case</span> <span class="s">"DEPT_AND_CHILD"</span><span class="o">:</span> <span class="c1">// WHERE dept_id IN (当前部门及子部门)</span>
    <span class="k">case</span> <span class="s">"DEPT"</span><span class="o">:</span>           <span class="c1">// WHERE dept_id = #{deptId}</span>
    <span class="k">case</span> <span class="s">"SELF"</span><span class="o">:</span>           <span class="c1">// WHERE created_by = #{userId}</span>
    <span class="k">case</span> <span class="s">"CITY"</span><span class="o">:</span>           <span class="c1">// WHERE city_id IN (#{entityIds})</span>
    <span class="k">case</span> <span class="s">"PROJECT"</span><span class="o">:</span>        <span class="c1">// WHERE project_id IN (#{entityIds})</span>
<span class="o">}</span>
</code></pre></div></div>

<hr />

<h2 id="heading-六缓存设计">六、缓存设计</h2>

<table>
  <thead>
    <tr>
      <th>缓存 Key</th>
      <th>内容</th>
      <th>TTL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">perm:user:{userId}:grants</code></td>
      <td>用户所有有效授权列表</td>
      <td>动态计算</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">perm:resource:{resourceId}:scopes</code></td>
      <td>数据资源点可选范围元数据</td>
      <td>120分钟</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sys:dept:tree</code></td>
      <td>全局组织架构树</td>
      <td>120分钟</td>
    </tr>
  </tbody>
</table>

<p><strong>TTL 动态计算：</strong></p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">long</span> <span class="n">ttl</span> <span class="o">=</span> <span class="n">grants</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
    <span class="o">.</span><span class="na">filter</span><span class="o">(</span><span class="n">g</span> <span class="o">-&gt;</span> <span class="n">g</span><span class="o">.</span><span class="na">getExpireTime</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span>
    <span class="o">.</span><span class="na">mapToLong</span><span class="o">(</span><span class="n">g</span> <span class="o">-&gt;</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">between</span><span class="o">(</span><span class="n">now</span><span class="o">,</span> <span class="n">g</span><span class="o">.</span><span class="na">getExpireTime</span><span class="o">()).</span><span class="na">getSeconds</span><span class="o">())</span>
    <span class="o">.</span><span class="na">min</span><span class="o">()</span>
    <span class="o">.</span><span class="na">orElse</span><span class="o">(</span><span class="mi">1800</span><span class="o">);</span>

<span class="n">ttl</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">ttl</span> <span class="o">+</span> <span class="mi">60</span><span class="o">,</span> <span class="mi">1800</span><span class="o">);</span> <span class="c1">// 最长不超过30分钟</span>
</code></pre></div></div>

<p><strong>缓存失效触发点：</strong></p>
<ul>
  <li>新增/撤销/修改授权 → 失效 <code class="language-plaintext highlighter-rouge">perm:user:{userId}:grants</code></li>
  <li>授权到期（定时任务扫描）→ 失效 <code class="language-plaintext highlighter-rouge">perm:user:{userId}:grants</code></li>
</ul>

<hr />

<h2 id="heading-七系统架构图">七、系统架构图</h2>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/%E6%9D%83%E9%99%90%E6%9E%B6%E6%9E%84%E5%9B%BE.png" alt="系统架构图" /></p>

<hr />

<h2 id="heading-八er-图">八、ER 图</h2>

<p><img src="https://int32-blog.oss-cn-beijing.aliyuncs.com/%E6%9D%83%E9%99%90er%E5%9B%BE.png" alt="ER图" /></p>

<hr />

<h2 id="heading-九接口设计">九、接口设计</h2>

<blockquote>
  <p>接口分两侧：<strong>管理侧</strong>（权限配置与授权操作）和<strong>使用侧</strong>（鉴权与权限查询）。</p>

  <p><strong>租户隔离说明：</strong> 所有接口通过 JWT payload 中的 <code class="language-plaintext highlighter-rouge">tenant_id</code> 隔离租户，无需在请求参数中显式传递。切换租户需调用 <code class="language-plaintext highlighter-rouge">/auth/switch-tenant</code> 重新签发 JWT。</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th>维度</th>
      <th>管理侧</th>
      <th>使用侧</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>调用方</td>
      <td>管理后台</td>
      <td>业务前端 / 业务服务 / 网关</td>
    </tr>
    <tr>
      <td>鉴权方式</td>
      <td>需要管理员角色权限</td>
      <td>只需登录态（JWT）</td>
    </tr>
    <tr>
      <td>操作特征</td>
      <td>写多读少</td>
      <td>只读，高频</td>
    </tr>
    <tr>
      <td>缓存策略</td>
      <td>写完主动失效缓存</td>
      <td>全走缓存，不直接查库</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="heading-91-管理侧接口">9.1 管理侧接口</h3>

<h4 id="heading-权限点管理">权限点管理</h4>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/permissions</code></td>
      <td>查询权限点列表</td>
    </tr>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/permissions</code></td>
      <td>新建权限点</td>
    </tr>
    <tr>
      <td>PUT</td>
      <td><code class="language-plaintext highlighter-rouge">/permissions/{id}</code></td>
      <td>修改权限点</td>
    </tr>
    <tr>
      <td>DELETE</td>
      <td><code class="language-plaintext highlighter-rouge">/permissions/{id}</code></td>
      <td>删除权限点</td>
    </tr>
  </tbody>
</table>

<h4 id="heading-数据资源点管理">数据资源点管理</h4>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/data-resources</code></td>
      <td>查询数据资源点列表</td>
    </tr>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/data-resources</code></td>
      <td>新建数据资源点</td>
    </tr>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/data-resources/{id}/scopes</code></td>
      <td>查询某资源点的可选范围列表</td>
    </tr>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/data-resources/{id}/scopes</code></td>
      <td>新增可选范围</td>
    </tr>
    <tr>
      <td>DELETE</td>
      <td><code class="language-plaintext highlighter-rouge">/data-resources/{id}/scopes/{scopeCode}</code></td>
      <td>删除可选范围</td>
    </tr>
  </tbody>
</table>

<h4 id="heading-范围数据字典外部同步入口">范围数据字典（外部同步入口）</h4>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/scope-data/sync</code></td>
      <td>外部系统批量同步范围数据（UPSERT）</td>
    </tr>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/scope-data/{scopeCode}</code></td>
      <td>查询某 scopeCode 下的实体列表</td>
    </tr>
  </tbody>
</table>

<h4 id="heading-角色管理">角色管理</h4>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/roles</code></td>
      <td>查询角色列表</td>
    </tr>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/roles</code></td>
      <td>新建角色</td>
    </tr>
    <tr>
      <td>PUT</td>
      <td><code class="language-plaintext highlighter-rouge">/roles/{id}</code></td>
      <td>修改角色</td>
    </tr>
    <tr>
      <td>DELETE</td>
      <td><code class="language-plaintext highlighter-rouge">/roles/{id}</code></td>
      <td>删除角色</td>
    </tr>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/roles/{id}/permissions</code></td>
      <td>查询角色下的权限点列表</td>
    </tr>
    <tr>
      <td>PUT</td>
      <td><code class="language-plaintext highlighter-rouge">/roles/{id}/permissions</code></td>
      <td>更新角色权限点，全量覆盖</td>
    </tr>
  </tbody>
</table>

<h4 id="heading-用户授权管理">用户授权管理</h4>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/users/{userId}/roles</code></td>
      <td>查询用户持有的角色列表</td>
    </tr>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/users/{userId}/grant/by-role</code></td>
      <td>通过角色批量授权</td>
    </tr>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/users/{userId}/grant/by-permission</code></td>
      <td>单独授权某个权限点</td>
    </tr>
    <tr>
      <td>PUT</td>
      <td><code class="language-plaintext highlighter-rouge">/grants/{grantId}</code></td>
      <td>修改单条授权</td>
    </tr>
    <tr>
      <td>DELETE</td>
      <td><code class="language-plaintext highlighter-rouge">/grants/{grantId}</code></td>
      <td>撤销单条授权</td>
    </tr>
    <tr>
      <td>DELETE</td>
      <td><code class="language-plaintext highlighter-rouge">/users/{userId}/roles/{roleId}/grants</code></td>
      <td>撤销某角色带来的所有授权</td>
    </tr>
    <tr>
      <td>PUT</td>
      <td><code class="language-plaintext highlighter-rouge">/users/{userId}/roles/{roleId}/expire</code></td>
      <td>续期某角色授权</td>
    </tr>
  </tbody>
</table>

<p><strong><code class="language-plaintext highlighter-rouge">POST /users/{userId}/grant/by-role</code> 请求体示例：</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"roleId"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
  </span><span class="nl">"startTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-03-05T00:00:00"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expireTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2027-03-05T00:00:00"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scopeConfigs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"permissionId"</span><span class="p">:</span><span class="w"> </span><span class="mi">101</span><span class="p">,</span><span class="w">
      </span><span class="nl">"dataResourceId"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"scopeCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CITY"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"entityIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"101"</span><span class="p">,</span><span class="w"> </span><span class="s2">"102"</span><span class="p">,</span><span class="w"> </span><span class="s2">"108"</span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"permissionId"</span><span class="p">:</span><span class="w"> </span><span class="mi">102</span><span class="p">,</span><span class="w">
      </span><span class="nl">"dataResourceId"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"scopeCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SELF"</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h3 id="heading-92-使用侧接口">9.2 使用侧接口</h3>

<h4 id="heading-权限加载">权限加载</h4>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/auth/me/permissions</code></td>
      <td>返回当前用户权限点、数据范围、菜单树</td>
    </tr>
  </tbody>
</table>

<p><strong>响应体示例：</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"permCodes"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"order:view"</span><span class="p">,</span><span class="w"> </span><span class="s2">"order:export"</span><span class="p">,</span><span class="w"> </span><span class="s2">"customer:edit"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"dataScopes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"order:view"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"scopeCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CITY"</span><span class="p">,</span><span class="w"> </span><span class="nl">"entityIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"101"</span><span class="p">,</span><span class="w"> </span><span class="s2">"102"</span><span class="p">]</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"customer:edit"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"scopeCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SELF"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"menus"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"permCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"menu:order"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"订单管理"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/order"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"icon"</span><span class="p">:</span><span class="w"> </span><span class="s2">"order-icon"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"sort"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
      </span><span class="nl">"children"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"permCode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"menu:order:list"</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"订单列表"</span><span class="p">,</span><span class="w"> </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/order/list"</span><span class="p">,</span><span class="w"> </span><span class="nl">"sort"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>菜单是权限点中 <code class="language-plaintext highlighter-rouge">type=1</code> 的数据，不单独提供菜单接口，由本接口统一返回。</p>
</blockquote>

<h4 id="heading-鉴权校验">鉴权校验</h4>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>路径</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/auth/check</code></td>
      <td>校验某 userId 是否拥有某 permCode</td>
    </tr>
    <tr>
      <td>GET</td>
      <td><code class="language-plaintext highlighter-rouge">/auth/data-scope</code></td>
      <td>查询用户在某权限点上的数据范围</td>
    </tr>
    <tr>
      <td>POST</td>
      <td><code class="language-plaintext highlighter-rouge">/auth/cache/refresh/{userId}</code></td>
      <td>主动刷新某用户权限缓存</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="heading-十多租户扩展方案">十、多租户扩展方案</h2>

<blockquote>
  <p>适用场景：平台需要支持多个租户（App）完全隔离，每个租户有自己独立的角色、权限点配置，用户可同时属于多个租户并自由切换身份。</p>
</blockquote>

<h3 id="heading-101-核心设计思路">10.1 核心设计思路</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>租户完全隔离 + 用户跨租户共享 + 租户级超管

- sys_role、sys_permission、sys_data_resource 等配置表加 tenant_id，数据完全隔离
- sys_user 不加 tenant_id，用户是平台级共享身份
- 用户与租户的归属关系通过 sys_user_tenant 管理
- 鉴权时 JWT 携带当前 tenant_id，所有查询强制带 tenant_id 过滤
- 超管标记在 sys_user_tenant 上，只在该租户内生效
</code></pre></div></div>

<hr />

<h3 id="heading-102-新增表">10.2 新增表</h3>

<h4 id="heading-租户表-sys_tenant">租户表 <code class="language-plaintext highlighter-rouge">sys_tenant</code></h4>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_tenant</span> <span class="p">(</span>
  <span class="n">id</span>           <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">tenant_code</span>  <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">64</span><span class="p">)</span>  <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">UNIQUE</span> <span class="k">COMMENT</span> <span class="s1">'租户编码'</span><span class="p">,</span>
  <span class="n">tenant_name</span>  <span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">128</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户名称'</span><span class="p">,</span>
  <span class="n">status</span>       <span class="nb">TINYINT</span>      <span class="k">DEFAULT</span> <span class="mi">1</span> <span class="k">COMMENT</span> <span class="s1">'1正常 0禁用'</span><span class="p">,</span>
  <span class="n">created_at</span>   <span class="nb">DATETIME</span>     <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span>
<span class="p">);</span>
</code></pre></div></div>

<h4 id="heading-用户-租户关系表-sys_user_tenant">用户-租户关系表 <code class="language-plaintext highlighter-rouge">sys_user_tenant</code></h4>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">sys_user_tenant</span> <span class="p">(</span>
  <span class="n">id</span>          <span class="nb">BIGINT</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
  <span class="n">user_id</span>     <span class="nb">BIGINT</span>  <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">tenant_id</span>   <span class="nb">BIGINT</span>  <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">is_super</span>    <span class="nb">TINYINT</span> <span class="k">DEFAULT</span> <span class="mi">0</span> <span class="k">COMMENT</span> <span class="s1">'1该租户下的超管，跳过权限校验'</span><span class="p">,</span>
  <span class="n">status</span>      <span class="nb">TINYINT</span> <span class="k">DEFAULT</span> <span class="mi">1</span><span class="p">,</span>
  <span class="n">created_at</span>  <span class="nb">DATETIME</span> <span class="k">DEFAULT</span> <span class="k">CURRENT_TIMESTAMP</span><span class="p">,</span>

  <span class="k">UNIQUE</span> <span class="k">KEY</span> <span class="n">uk_user_tenant</span> <span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">tenant_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_user_id</span> <span class="p">(</span><span class="n">user_id</span><span class="p">),</span>
  <span class="k">KEY</span> <span class="n">idx_tenant_id</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<hr />

<h3 id="heading-103-现有表字段改动">10.3 现有表字段改动</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_role</span>                  <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">tenant_id</span> <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">;</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_permission</span>            <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">tenant_id</span> <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">;</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_data_resource</span>         <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">tenant_id</span> <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">;</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_data_resource_scope</span>   <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">tenant_id</span> <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">;</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_user_role</span>             <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">tenant_id</span> <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">;</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_user_permission_grant</span> <span class="k">ADD</span> <span class="k">COLUMN</span> <span class="n">tenant_id</span> <span class="nb">BIGINT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">COMMENT</span> <span class="s1">'租户ID'</span><span class="p">;</span>

<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_user_permission_grant</span> <span class="k">ADD</span> <span class="k">INDEX</span> <span class="n">idx_tenant_user</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">,</span> <span class="n">user_id</span><span class="p">);</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_role</span>                  <span class="k">ADD</span> <span class="k">INDEX</span> <span class="n">idx_tenant</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">);</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">sys_permission</span>            <span class="k">ADD</span> <span class="k">INDEX</span> <span class="n">idx_tenant</span> <span class="p">(</span><span class="n">tenant_id</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>不需要加 <code class="language-plaintext highlighter-rouge">tenant_id</code> 的表：</strong></p>

<table>
  <thead>
    <tr>
      <th>表</th>
      <th>原因</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sys_user</code></td>
      <td>用户是平台级共享身份，通过 sys_user_tenant 关联租户</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sys_scope_data</code></td>
      <td>范围数据字典全局共享（城市、区域等不属于某个租户）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sys_grant_scope_entity</code></td>
      <td>跟随 sys_user_permission_grant，grant 有 tenant_id 即可</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sys_dept</code></td>
      <td>部门可按需决定，若需租户隔离则加，否则通过 grant 的 tenant_id 间接隔离</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="heading-104-鉴权链路改动">10.4 鉴权链路改动</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Long</span> <span class="n">userId</span>   <span class="o">=</span> <span class="n">token</span><span class="o">.</span><span class="na">getUserId</span><span class="o">();</span>
<span class="nc">Long</span> <span class="n">tenantId</span> <span class="o">=</span> <span class="n">token</span><span class="o">.</span><span class="na">getTenantId</span><span class="o">();</span>

<span class="kt">boolean</span> <span class="n">isSuperAdmin</span> <span class="o">=</span> <span class="n">userTenantService</span><span class="o">.</span><span class="na">isSuper</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">tenantId</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">isSuperAdmin</span><span class="o">)</span> <span class="k">return</span><span class="o">;</span>

<span class="nc">Set</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">permCodes</span> <span class="o">=</span> <span class="n">permissionService</span><span class="o">.</span><span class="na">getPermCodes</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">tenantId</span><span class="o">);</span>
<span class="nc">GrantInfo</span> <span class="n">grant</span> <span class="o">=</span> <span class="n">permissionService</span><span class="o">.</span><span class="na">getGrant</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">tenantId</span><span class="o">,</span> <span class="s">"order:view"</span><span class="o">);</span>
</code></pre></div></div>

<hr />

<h3 id="heading-105-用户切换租户">10.5 用户切换租户</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET  /auth/me/tenants    查询当前用户所属的租户列表
POST /auth/switch-tenant 切换租户，返回新 token
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">GET /auth/me/tenants</code> 响应示例：</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"tenants"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"tenantId"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"tenantName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"华东事业部"</span><span class="p">,</span><span class="w"> </span><span class="nl">"isSuper"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"tenantId"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="nl">"tenantName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"华南事业部"</span><span class="p">,</span><span class="w"> </span><span class="nl">"isSuper"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"current"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h3 id="heading-106-缓存-key-加入-tenant_id">10.6 缓存 Key 加入 tenant_id</h3>

<table>
  <thead>
    <tr>
      <th>缓存 Key</th>
      <th>TTL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">perm:tenant:{tenantId}:user:{userId}:grants</code></td>
      <td>动态</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">perm:tenant:{tenantId}:role:{roleId}:perms</code></td>
      <td>60min</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">perm:tenant:{tenantId}:resource:{id}:scopes</code></td>
      <td>120min</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">sys:tenant:{tenantId}:dept:tree</code></td>
      <td>120min</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="heading-107-改动量总结">10.7 改动量总结</h3>

<table>
  <thead>
    <tr>
      <th>改动项</th>
      <th>内容</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>新增表</td>
      <td>sys_tenant、sys_user_tenant</td>
    </tr>
    <tr>
      <td>字段改动</td>
      <td>6 张核心表加 tenant_id</td>
    </tr>
    <tr>
      <td>鉴权链路</td>
      <td>JWT 带 tenant_id，所有查询加 tenant_id 过滤</td>
    </tr>
    <tr>
      <td>缓存 Key</td>
      <td>全部加 tenant_id 维度</td>
    </tr>
    <tr>
      <td>接口新增</td>
      <td>/auth/me/tenants、/auth/switch-tenant</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>现有权限模型无需推倒重建，多租户是在原有模型上套了一层租户维度的壳，改动最小化。</p>
</blockquote>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="架构设计" /><category term="权限系统" /><category term="RBAC" /><category term="多租户" /><category term="架构设计" /><category term="MySQL" /><summary type="html"><![CDATA[增强型 RBAC + 数据资源点 + 范围数据字典，涵盖表结构设计、缓存策略、接口设计、多租户扩展方案]]></summary></entry><entry><title type="html">OAuth 2.0 完全指南：从「用 GitHub 登录」到自己实现一个授权服务器</title><link href="https://sut-gc.github.io/blog/2026/02/28/oauth-2-0-%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97-%E4%BB%8E-%E7%94%A8-github-%E7%99%BB%E5%BD%95-%E5%88%B0%E8%87%AA%E5%B7%B1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%8E%88%E6%9D%83%E6%9C%8D%E5%8A%A1%E5%99%A8/" rel="alternate" type="text/html" title="OAuth 2.0 完全指南：从「用 GitHub 登录」到自己实现一个授权服务器" /><published>2026-02-28T00:00:00+00:00</published><updated>2026-02-28T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/02/28/oauth-2-0-%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97-%E4%BB%8E-%E7%94%A8-github-%E7%99%BB%E5%BD%95-%E5%88%B0%E8%87%AA%E5%B7%B1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%8E%88%E6%9D%83%E6%9C%8D%E5%8A%A1%E5%99%A8</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/02/28/oauth-2-0-%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97-%E4%BB%8E-%E7%94%A8-github-%E7%99%BB%E5%BD%95-%E5%88%B0%E8%87%AA%E5%B7%B1%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E6%8E%88%E6%9D%83%E6%9C%8D%E5%8A%A1%E5%99%A8/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-oauth-20-完全指南从用-github-登录到自己实现一个授权服务器" id="markdown-toc-heading-oauth-20-完全指南从用-github-登录到自己实现一个授权服务器">OAuth 2.0 完全指南：从「用 GitHub 登录」到自己实现一个授权服务器</a>    <ul>
      <li><a href="#heading-一oauth-解决什么问题" id="markdown-toc-heading-一oauth-解决什么问题">一、OAuth 解决什么问题</a></li>
      <li><a href="#heading-二四个核心角色" id="markdown-toc-heading-二四个核心角色">二、四个核心角色</a></li>
      <li><a href="#heading-三oauth-不是认证是授权" id="markdown-toc-heading-三oauth-不是认证是授权">三、OAuth 不是认证，是授权</a></li>
      <li><a href="#heading-四四种授权模式" id="markdown-toc-heading-四四种授权模式">四、四种授权模式</a></li>
      <li><a href="#heading-五授权码模式完整流程" id="markdown-toc-heading-五授权码模式完整流程">五、授权码模式完整流程</a>        <ul>
          <li><a href="#heading-state-防-csrf-攻击" id="markdown-toc-heading-state-防-csrf-攻击">state 防 CSRF 攻击</a></li>
        </ul>
      </li>
      <li><a href="#heading-六token-详解" id="markdown-toc-heading-六token-详解">六、Token 详解</a>        <ul>
          <li><a href="#heading-accesstoken-和-refreshtoken" id="markdown-toc-heading-accesstoken-和-refreshtoken">AccessToken 和 RefreshToken</a></li>
          <li><a href="#heading-jwt-token" id="markdown-toc-heading-jwt-token">JWT Token</a></li>
        </ul>
      </li>
      <li><a href="#heading-七作为接入方用-go-接入-github-oauth" id="markdown-toc-heading-七作为接入方用-go-接入-github-oauth">七、作为接入方：用 Go 接入 GitHub OAuth</a>        <ul>
          <li><a href="#heading-endpoint-和-scopes-从哪来" id="markdown-toc-heading-endpoint-和-scopes-从哪来">Endpoint 和 Scopes 从哪来</a></li>
          <li><a href="#heading-token-自动刷新" id="markdown-toc-heading-token-自动刷新">Token 自动刷新</a></li>
        </ul>
      </li>
      <li><a href="#heading-八oauth-规范接口标准" id="markdown-toc-heading-八oauth-规范接口标准">八、OAuth 规范接口标准</a>        <ul>
          <li><a href="#heading-授权接口" id="markdown-toc-heading-授权接口">授权接口</a></li>
          <li><a href="#heading-换-token-接口" id="markdown-toc-heading-换-token-接口">换 Token 接口</a></li>
          <li><a href="#heading-刷新-token-接口" id="markdown-toc-heading-刷新-token-接口">刷新 Token 接口</a></li>
          <li><a href="#heading-用户信息接口" id="markdown-toc-heading-用户信息接口">用户信息接口</a></li>
        </ul>
      </li>
      <li><a href="#heading-九作为提供方用-go-实现授权服务器" id="markdown-toc-heading-九作为提供方用-go-实现授权服务器">九、作为提供方：用 Go 实现授权服务器</a>        <ul>
          <li><a href="#heading-整体架构" id="markdown-toc-heading-整体架构">整体架构</a></li>
          <li><a href="#heading-实现代码" id="markdown-toc-heading-实现代码">实现代码</a></li>
          <li><a href="#heading-http-框架兼容性" id="markdown-toc-heading-http-框架兼容性">HTTP 框架兼容性</a></li>
        </ul>
      </li>
      <li><a href="#heading-十分布式环境下的注意事项" id="markdown-toc-heading-十分布式环境下的注意事项">十、分布式环境下的注意事项</a></li>
      <li><a href="#heading-小结" id="markdown-toc-heading-小结">小结</a></li>
    </ul>
  </li>
</ul>

<h1 id="heading-oauth-20-完全指南从用-github-登录到自己实现一个授权服务器">OAuth 2.0 完全指南：从「用 GitHub 登录」到自己实现一个授权服务器</h1>

<p>你一定见过这类按钮——「用 Google 登录」、「用 GitHub 登录」、「微信授权登录」。点进去跳一个页面，同意一下，就登录成功了，全程没有输过密码。</p>

<p>这背后用的就是 OAuth 2.0。</p>

<p>这篇文章从概念出发，把 OAuth 的完整知识体系过一遍：它解决什么问题、流程是怎么跑的、作为接入方怎么用、作为提供方怎么实现，以及 JWT、分布式、安全这些相关话题。</p>

<hr />

<h2 id="heading-一oauth-解决什么问题">一、OAuth 解决什么问题</h2>

<p>在 OAuth 出现之前，如果一个第三方应用想访问你在某平台的数据，通常的做法是把账号密码直接交给它。</p>

<p>这很危险：</p>

<ul>
  <li>第三方拿到密码就等于拥有你账号的全部权限</li>
  <li>你没办法只授权「读」而不授权「写」</li>
  <li>想撤销授权只能改密码，影响所有地方</li>
</ul>

<p>OAuth 的核心思路是：<strong>不交密码，交一张有限权限的「通行证」（token）</strong>。</p>

<p>这张通行证：</p>
<ul>
  <li>只能访问你指定范围的资源</li>
  <li>有有效期，过期自动失效</li>
  <li>可以随时撤销，不影响密码</li>
</ul>

<hr />

<h2 id="heading-二四个核心角色">二、四个核心角色</h2>

<p>OAuth 2.0 里有四个角色，理解它们是理解整个流程的基础：</p>

<table>
  <thead>
    <tr>
      <th>角色</th>
      <th>说明</th>
      <th>例子</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>资源拥有者（Resource Owner）</td>
      <td>就是用户，数据的主人</td>
      <td>你</td>
    </tr>
    <tr>
      <td>客户端（Client）</td>
      <td>想访问你数据的第三方应用</td>
      <td>某健身 App</td>
    </tr>
    <tr>
      <td>授权服务器（Authorization Server）</td>
      <td>负责验证身份、颁发 token</td>
      <td>Google 的登录服务</td>
    </tr>
    <tr>
      <td>资源服务器（Resource Server）</td>
      <td>真正存放数据的地方</td>
      <td>Google Calendar API</td>
    </tr>
  </tbody>
</table>

<p>实际场景里，授权服务器和资源服务器往往是同一家公司的不同服务，甚至同一个服务。</p>

<hr />

<h2 id="heading-三oauth-不是认证是授权">三、OAuth 不是认证，是授权</h2>

<p>这是很多人容易混淆的一点。</p>

<p><strong>授权（Authorization）</strong>：你能做什么<br />
<strong>认证（Authentication）</strong>：你是谁</p>

<p>OAuth 2.0 本身只解决授权问题，它告诉第三方「这个用户允许你访问他的 xx 数据」，但不直接告诉你「这个用户是谁」。</p>

<p>要做登录认证，需要在 OAuth 上面加一层 <strong>OpenID Connect（OIDC）</strong>，它扩展了 OAuth，额外返回一个 <code class="language-plaintext highlighter-rouge">id_token</code>，里面包含用户的身份信息。</p>

<p>我们平时说的「用 GitHub 登录」，其实是 OAuth + OIDC 一起在工作，只是大家习惯统称为 OAuth 登录。</p>

<hr />

<h2 id="heading-四四种授权模式">四、四种授权模式</h2>

<p>OAuth 2.0 规范定义了四种授权模式，适用于不同场景：</p>

<p><strong>授权码模式（Authorization Code）</strong><br />
最常用、最安全的模式。有后端服务器的 Web 应用都用这个，token 在服务器之间交换，不暴露给浏览器。下文重点讲这个。</p>

<p><strong>客户端模式（Client Credentials）</strong><br />
没有用户参与，是服务之间直接调用用的。比如订单服务调库存服务，不代表任何用户，直接用 client_id + client_secret 换 token。微服务架构里用得很多。</p>

<p><strong>简化模式（Implicit）</strong><br />
专门给纯前端应用设计，直接把 token 放在 URL 里返回。因为 token 直接暴露在 URL 中不安全，现在已基本废弃。</p>

<p><strong>密码模式（Resource Owner Password）</strong><br />
用户直接把账号密码给第三方应用，第三方再去换 token。违背了 OAuth 的设计初衷，现在也基本废弃，只在非常可信的内部系统偶尔使用。</p>

<p>实际开发中，<strong>有用户的场景用授权码模式，服务间调用用客户端模式</strong>，其他两种基本不用考虑。</p>

<hr />

<h2 id="heading-五授权码模式完整流程">五、授权码模式完整流程</h2>

<div style="font-family:-apple-system,sans-serif;background:#f8f9fa;border-radius:12px;padding:24px;margin:24px 0;">
<style>
.ofd-actors{display:flex;justify-content:space-between;margin-bottom:16px}
.ofd-actor{width:160px;text-align:center;padding:10px;background:#fff;border-radius:8px;font-weight:bold;font-size:13px;box-shadow:0 2px 6px rgba(0,0,0,.08)}
.ofd-actor.user{border-top:3px solid #4CAF50;color:#2e7d32}
.ofd-actor.client{border-top:3px solid #2196F3;color:#1565c0}
.ofd-actor.server{border-top:3px solid #9C27B0;color:#6a1b9a}
.ofd-section-title{font-size:11px;font-weight:bold;color:#999;letter-spacing:1px;text-transform:uppercase;margin-bottom:6px;padding-left:4px}
.ofd-row{display:flex;align-items:center;margin:6px 0;min-height:48px}
.ofd-box{width:160px;min-height:36px;padding:6px 10px;border-radius:6px;font-size:12px;text-align:center;display:flex;align-items:center;justify-content:center;flex-shrink:0;box-sizing:border-box;line-height:1.4}
.ofd-box.user{background:#E8F5E9;border:1.5px solid #4CAF50;color:#2e7d32}
.ofd-box.client{background:#E3F2FD;border:1.5px solid #2196F3;color:#1565c0}
.ofd-box.server{background:#F3E5F5;border:1.5px solid #9C27B0;color:#6a1b9a}
.ofd-box.empty{background:transparent;border:none;width:160px}
.ofd-arrow{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0 8px}
.ofd-arrow-line{width:100%;height:2px;background:#bbb;position:relative;margin-bottom:4px}
.ofd-arrow-line.right::after{content:'▶';position:absolute;right:-6px;top:-8px;color:#bbb;font-size:11px}
.ofd-arrow-line.left::before{content:'◀';position:absolute;left:-6px;top:-8px;color:#bbb;font-size:11px}
.ofd-arrow-label{font-size:11px;color:#888;white-space:nowrap;text-align:center}
.ofd-divider{border:none;border-top:1px dashed #ddd;margin:10px 0}
.ofd-note{background:#fff8e1;border-left:3px solid #FFC107;border-radius:4px;padding:8px 12px;font-size:12px;color:#666;margin-top:12px}
</style>

<div class="ofd-actors">
  <div class="ofd-actor user">👤 用户<br /><small>资源拥有者</small></div>
  <div class="ofd-actor client">🖥️ B系统<br /><small>接入方</small></div>
  <div class="ofd-actor server">🐙 GitHub<br /><small>授权方</small></div>
</div>

<div><div class="ofd-section-title">① 发起登录</div>
<div class="ofd-row">
  <div class="ofd-box user">点击「用 GitHub 登录」</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line right"></div><div class="ofd-arrow-label">GET /login</div></div>
  <div class="ofd-box client">生成随机 state<br />存入 session</div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box empty"></div>
</div>
<div class="ofd-row">
  <div class="ofd-box user">浏览器跳转到<br />GitHub 授权页</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line left"></div><div class="ofd-arrow-label">302 重定向（带 state）</div></div>
  <div class="ofd-box client">生成授权 URL</div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box empty"></div>
</div></div>

<hr class="ofd-divider" />

<div><div class="ofd-section-title">② 用户在 GitHub 授权</div>
<div class="ofd-row">
  <div class="ofd-box user">输入密码<br />点击「授权」</div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box empty"></div>
  <div class="ofd-arrow"><div class="ofd-arrow-line right"></div><div class="ofd-arrow-label">用户授权操作</div></div>
  <div class="ofd-box server">验证身份<br />记录授权范围</div>
</div>
<div class="ofd-row">
  <div class="ofd-box user">浏览器被重定向<br />回 B系统</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line right"></div><div class="ofd-arrow-label">带着 code + state</div></div>
  <div class="ofd-box client">GET /callback<br />验证 state ✅</div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box empty"></div>
</div></div>

<hr class="ofd-divider" />

<div><div class="ofd-section-title">③ 后端换取 Token（用户不可见）</div>
<div class="ofd-row">
  <div class="ofd-box empty"></div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box client">用 code 换 token</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line right"></div><div class="ofd-arrow-label">POST /oauth/token</div></div>
  <div class="ofd-box server">验证 code<br />生成 token</div>
</div>
<div class="ofd-row">
  <div class="ofd-box empty"></div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box client">存储 AccessToken<br />+ RefreshToken</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line left"></div><div class="ofd-arrow-label">返回 token 数据</div></div>
  <div class="ofd-box server">返回 AccessToken<br />+ RefreshToken</div>
</div></div>

<hr class="ofd-divider" />

<div><div class="ofd-section-title">④ 获取用户信息，完成登录</div>
<div class="ofd-row">
  <div class="ofd-box empty"></div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box client">带 token<br />请求用户信息</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line right"></div><div class="ofd-arrow-label">GET /user（Bearer token）</div></div>
  <div class="ofd-box server">验证 token<br />返回用户数据</div>
</div>
<div class="ofd-row">
  <div class="ofd-box user">登录成功 🎉</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line left"></div><div class="ofd-arrow-label">设置 Session/Cookie</div></div>
  <div class="ofd-box client">新用户→自动注册<br />老用户→直接登录</div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box empty"></div>
</div></div>

<hr class="ofd-divider" />

<div><div class="ofd-section-title">⑤ Token 过期后自动刷新（静默）</div>
<div class="ofd-row">
  <div class="ofd-box empty"></div>
  <div class="ofd-arrow" style="flex:0;width:20px"></div>
  <div class="ofd-box client">AccessToken 过期<br />用 RefreshToken 换新的</div>
  <div class="ofd-arrow"><div class="ofd-arrow-line right"></div><div class="ofd-arrow-label">POST /oauth/token（refresh）</div></div>
  <div class="ofd-box server">返回新 AccessToken</div>
</div></div>

<div class="ofd-note">💡 <strong>关键点：</strong>用户的密码只在用户和 GitHub 之间流转，B系统永远看不到。code 是一次性的，用完立刻失效。</div>
</div>

<h3 id="heading-state-防-csrf-攻击">state 防 CSRF 攻击</h3>

<p>流程里有个容易被忽略但很重要的细节：<code class="language-plaintext highlighter-rouge">state</code> 参数。</p>

<p><strong>攻击场景：</strong> 攻击者用自己的 GitHub 账号走到一半，拿到回调链接（带着他自己的 code），停下来把这个链接发给你。你点了之后，你的浏览器拿着攻击者的 code 去换 token，结果你的 B系统账号绑定了攻击者的 GitHub 账号，攻击者之后用自己的 GitHub 就能登进你的账号。</p>

<p><strong>state 怎么防：</strong> 你发起登录时，B系统生成随机字符串 state，存在你的 session 里，同时带到跳转链接里。GitHub 回调时原样带回 state，B系统对比 session 里存的，对不上就拒绝。攻击者的链接里带的是他自己 session 的 state，跟你的对不上，攻击失败。</p>

<p>本质是一个 <code class="language-plaintext highlighter-rouge">sessionID -&gt; state</code> 的 map，确保回调是本次登录发起的，不是别人伪造的。</p>

<hr />

<h2 id="heading-六token-详解">六、Token 详解</h2>

<h3 id="heading-accesstoken-和-refreshtoken">AccessToken 和 RefreshToken</h3>

<p>换 token 接口会同时返回两个 token：</p>

<ul>
  <li><strong>AccessToken</strong>：真正用来访问资源的，有效期短，一般几十分钟到几小时</li>
  <li><strong>RefreshToken</strong>：用来在 AccessToken 过期后换新的，有效期长，一般几天到几个月</li>
</ul>

<p>有效期由授权方决定，接入方控制不了。不同平台策略差异很大，GitHub 默认 AccessToken 永不过期，Google 则是一小时过期。</p>

<h3 id="heading-jwt-token">JWT Token</h3>

<p>token 有两种实现方式：</p>

<p><strong>随机字符串 token</strong>（传统方式）：token 本身不携带信息，服务器每次验证都要去数据库或 Redis 查「这个 token 对应哪个用户」。</p>

<p><strong>JWT（JSON Web Token）</strong>：token 本身携带信息，服务器不需要查存储，直接解码验证。在分布式系统里优势明显，每台服务器只需要有密钥就能验证。</p>

<p>JWT 由三段组成，用 <code class="language-plaintext highlighter-rouge">.</code> 分隔：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Header.Payload.Signature
</code></pre></div></div>

<p>解码后：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">Header：算法信息</span><span class="w">
</span><span class="p">{</span><span class="w"> </span><span class="nl">"alg"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HS256"</span><span class="p">,</span><span class="w"> </span><span class="nl">"typ"</span><span class="p">:</span><span class="w"> </span><span class="s2">"JWT"</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">Payload：实际数据</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"userId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user001"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user:email"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"exp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1709280000</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="err">//</span><span class="w"> </span><span class="err">Signature：用密钥对前两段做签名，防篡改</span><span class="w">
</span></code></pre></div></div>

<p><strong>重要：Header 和 Payload 只是 Base64URL 编码，不是加密，任何人都能解码看到内容。</strong> JWT 保证的是「不可篡改」，不是「内容保密」。所以 JWT 里绝对不能放密码、手机号这类敏感信息。</p>

<p>JWT 最大的缺点是无法主动失效，只能等过期。想撤销需要维护黑名单，但这样又引入了存储查询，优势就小了。</p>

<hr />

<h2 id="heading-七作为接入方用-go-接入-github-oauth">七、作为接入方：用 Go 接入 GitHub OAuth</h2>

<p>Go 官方维护了 <code class="language-plaintext highlighter-rouge">golang.org/x/oauth2</code> 这个扩展库，封装了 OAuth 流程里所有繁琐的部分：生成授权链接、用 code 换 token、自动刷新 token、构造带认证的 HTTP 请求。</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"context"</span>
    <span class="s">"crypto/rand"</span>
    <span class="s">"encoding/base64"</span>
    <span class="s">"encoding/json"</span>
    <span class="s">"fmt"</span>
    <span class="s">"log"</span>
    <span class="s">"net/http"</span>
    <span class="s">"sync"</span>

    <span class="s">"golang.org/x/oauth2"</span>
    <span class="s">"golang.org/x/oauth2/github"</span>
<span class="p">)</span>

<span class="k">var</span> <span class="n">oauthConfig</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">oauth2</span><span class="o">.</span><span class="n">Config</span><span class="p">{</span>
    <span class="n">ClientID</span><span class="o">:</span>     <span class="s">"你的_GITHUB_CLIENT_ID"</span><span class="p">,</span>
    <span class="n">ClientSecret</span><span class="o">:</span> <span class="s">"你的_GITHUB_CLIENT_SECRET"</span><span class="p">,</span>
    <span class="n">RedirectURL</span><span class="o">:</span>  <span class="s">"http://localhost:8080/callback"</span><span class="p">,</span>
    <span class="n">Scopes</span><span class="o">:</span>       <span class="p">[]</span><span class="kt">string</span><span class="p">{</span><span class="s">"user:email"</span><span class="p">},</span> <span class="c">// 查 GitHub 文档获取</span>
    <span class="n">Endpoint</span><span class="o">:</span>     <span class="n">github</span><span class="o">.</span><span class="n">Endpoint</span><span class="p">,</span>        <span class="c">// SDK 内置，不用自己填 URL</span>
<span class="p">}</span>

<span class="k">var</span> <span class="n">stateStore</span> <span class="o">=</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">sync</span><span class="o">.</span><span class="n">Mutex</span>
    <span class="n">m</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">bool</span>
<span class="p">}{</span><span class="n">m</span><span class="o">:</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">bool</span><span class="p">)}</span>

<span class="k">var</span> <span class="n">tokenStore</span> <span class="o">=</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">sync</span><span class="o">.</span><span class="n">Mutex</span>
    <span class="n">m</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="o">*</span><span class="n">oauth2</span><span class="o">.</span><span class="n">Token</span>
<span class="p">}{</span><span class="n">m</span><span class="o">:</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="o">*</span><span class="n">oauth2</span><span class="o">.</span><span class="n">Token</span><span class="p">)}</span>

<span class="k">type</span> <span class="n">User</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">ID</span>     <span class="kt">string</span> <span class="s">`json:"id"`</span>
    <span class="n">Login</span>  <span class="kt">string</span> <span class="s">`json:"login"`</span>
    <span class="n">Email</span>  <span class="kt">string</span> <span class="s">`json:"email"`</span>
    <span class="n">Avatar</span> <span class="kt">string</span> <span class="s">`json:"avatar_url"`</span>
<span class="p">}</span>

<span class="k">var</span> <span class="n">userStore</span> <span class="o">=</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">sync</span><span class="o">.</span><span class="n">Mutex</span>
    <span class="n">m</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="o">*</span><span class="n">User</span>
<span class="p">}{</span><span class="n">m</span><span class="o">:</span> <span class="nb">make</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="o">*</span><span class="n">User</span><span class="p">)}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/login"</span><span class="p">,</span> <span class="n">handleLogin</span><span class="p">)</span>
    <span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/callback"</span><span class="p">,</span> <span class="n">handleCallback</span><span class="p">)</span>
    <span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">ListenAndServe</span><span class="p">(</span><span class="s">":8080"</span><span class="p">,</span> <span class="no">nil</span><span class="p">))</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">handleLogin</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">state</span> <span class="o">:=</span> <span class="n">generateState</span><span class="p">()</span>
    <span class="n">stateStore</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
    <span class="n">stateStore</span><span class="o">.</span><span class="n">m</span><span class="p">[</span><span class="n">state</span><span class="p">]</span> <span class="o">=</span> <span class="no">true</span>
    <span class="n">stateStore</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>
    <span class="n">url</span> <span class="o">:=</span> <span class="n">oauthConfig</span><span class="o">.</span><span class="n">AuthCodeURL</span><span class="p">(</span><span class="n">state</span><span class="p">)</span>
    <span class="n">http</span><span class="o">.</span><span class="n">Redirect</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusTemporaryRedirect</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">handleCallback</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">state</span> <span class="o">:=</span> <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Query</span><span class="p">()</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"state"</span><span class="p">)</span>
    <span class="n">stateStore</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
    <span class="n">valid</span> <span class="o">:=</span> <span class="n">stateStore</span><span class="o">.</span><span class="n">m</span><span class="p">[</span><span class="n">state</span><span class="p">]</span>
    <span class="nb">delete</span><span class="p">(</span><span class="n">stateStore</span><span class="o">.</span><span class="n">m</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span>
    <span class="n">stateStore</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>
    <span class="k">if</span> <span class="o">!</span><span class="n">valid</span> <span class="p">{</span>
        <span class="n">http</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"invalid state"</span><span class="p">,</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusBadRequest</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>

    <span class="n">code</span> <span class="o">:=</span> <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Query</span><span class="p">()</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"code"</span><span class="p">)</span>
    <span class="n">token</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">oauthConfig</span><span class="o">.</span><span class="n">Exchange</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="n">code</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="n">http</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"换取 token 失败"</span><span class="p">,</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusInternalServerError</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>

    <span class="c">// SDK 创建自动带 token 的 HTTP client</span>
    <span class="n">client</span> <span class="o">:=</span> <span class="n">oauthConfig</span><span class="o">.</span><span class="n">Client</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">Background</span><span class="p">(),</span> <span class="n">token</span><span class="p">)</span>
    <span class="n">resp</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">client</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"https://api.github.com/user"</span><span class="p">)</span>
    <span class="k">defer</span> <span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="o">.</span><span class="n">Close</span><span class="p">()</span>

    <span class="k">var</span> <span class="n">user</span> <span class="n">User</span>
    <span class="n">json</span><span class="o">.</span><span class="n">NewDecoder</span><span class="p">(</span><span class="n">resp</span><span class="o">.</span><span class="n">Body</span><span class="p">)</span><span class="o">.</span><span class="n">Decode</span><span class="p">(</span><span class="o">&amp;</span><span class="n">user</span><span class="p">)</span>

    <span class="n">userStore</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">_</span><span class="p">,</span> <span class="n">exists</span> <span class="o">:=</span> <span class="n">userStore</span><span class="o">.</span><span class="n">m</span><span class="p">[</span><span class="n">user</span><span class="o">.</span><span class="n">ID</span><span class="p">];</span> <span class="o">!</span><span class="n">exists</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"新用户注册: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">Login</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="n">userStore</span><span class="o">.</span><span class="n">m</span><span class="p">[</span><span class="n">user</span><span class="o">.</span><span class="n">ID</span><span class="p">]</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">user</span>
    <span class="n">userStore</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>

    <span class="n">tokenStore</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
    <span class="n">tokenStore</span><span class="o">.</span><span class="n">m</span><span class="p">[</span><span class="n">user</span><span class="o">.</span><span class="n">ID</span><span class="p">]</span> <span class="o">=</span> <span class="n">token</span> <span class="c">// token 结构体里含 AccessToken、RefreshToken、Expiry</span>
    <span class="n">tokenStore</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">generateState</span><span class="p">()</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">b</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span> <span class="m">16</span><span class="p">)</span>
    <span class="n">rand</span><span class="o">.</span><span class="n">Read</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">base64</span><span class="o">.</span><span class="n">URLEncoding</span><span class="o">.</span><span class="n">EncodeToString</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="heading-endpoint-和-scopes-从哪来">Endpoint 和 Scopes 从哪来</h3>

<p><code class="language-plaintext highlighter-rouge">Endpoint</code> 里是两个固定 URL，SDK 对主流平台都内置好了：<code class="language-plaintext highlighter-rouge">github.Endpoint</code>、<code class="language-plaintext highlighter-rouge">google.Endpoint</code> 直接用。如果是小众平台没有内置，自己查文档填：</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="n">myEndpoint</span> <span class="o">=</span> <span class="n">oauth2</span><span class="o">.</span><span class="n">Endpoint</span><span class="p">{</span>
    <span class="n">AuthURL</span><span class="o">:</span>  <span class="s">"https://xxx.com/oauth/authorize"</span><span class="p">,</span>
    <span class="n">TokenURL</span><span class="o">:</span> <span class="s">"https://xxx.com/oauth/token"</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Scopes</code> 是权限范围，每个平台的字符串定义不一样，必须查对方文档。没有统一标准。</p>

<h3 id="heading-token-自动刷新">Token 自动刷新</h3>

<p><code class="language-plaintext highlighter-rouge">oauthConfig.Client()</code> 返回的 HTTP client 每次发请求前会自动检查 AccessToken 是否过期，过期了自动用 RefreshToken 换新的。但刷新完 SDK 不会帮你存回数据库，需要自己实现 <code class="language-plaintext highlighter-rouge">TokenSource</code> 接口：</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">myTokenSource</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">userID</span>      <span class="kt">string</span>
    <span class="n">tokenSource</span> <span class="n">oauth2</span><span class="o">.</span><span class="n">TokenSource</span>
<span class="p">}</span>

<span class="k">func</span> <span class="p">(</span><span class="n">s</span> <span class="o">*</span><span class="n">myTokenSource</span><span class="p">)</span> <span class="n">Token</span><span class="p">()</span> <span class="p">(</span><span class="o">*</span><span class="n">oauth2</span><span class="o">.</span><span class="n">Token</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">token</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">s</span><span class="o">.</span><span class="n">tokenSource</span><span class="o">.</span><span class="n">Token</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="no">nil</span><span class="p">,</span> <span class="n">err</span>
    <span class="p">}</span>
    <span class="c">// 刷新后存回数据库</span>
    <span class="n">tokenStore</span><span class="o">.</span><span class="n">Lock</span><span class="p">()</span>
    <span class="n">tokenStore</span><span class="o">.</span><span class="n">m</span><span class="p">[</span><span class="n">s</span><span class="o">.</span><span class="n">userID</span><span class="p">]</span> <span class="o">=</span> <span class="n">token</span>
    <span class="n">tokenStore</span><span class="o">.</span><span class="n">Unlock</span><span class="p">()</span>
    <span class="k">return</span> <span class="n">token</span><span class="p">,</span> <span class="no">nil</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="heading-八oauth-规范接口标准">八、OAuth 规范接口标准</h2>

<p>如果你要自己实现授权服务器，必须遵循 OAuth 2.0 规范（RFC 6749）。规范规定了接口的参数名和返回格式，SDK 按照这个标准实现，所以只要你符合规范，任何语言的 SDK 都能对接你。</p>

<h3 id="heading-授权接口">授权接口</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /oauth/authorize
  ?response_type=code     # 固定值
  &amp;client_id=xxx
  &amp;redirect_uri=xxx
  &amp;scope=user:email
  &amp;state=abc123
</code></pre></div></div>

<p>用户同意后跳转回去：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>302 -&gt; redirect_uri?code=一次性授权码&amp;state=abc123
</code></pre></div></div>

<h3 id="heading-换-token-接口">换 Token 接口</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&amp;code=一次性授权码
&amp;client_id=xxx
&amp;client_secret=xxx
&amp;redirect_uri=xxx
</code></pre></div></div>

<p>返回格式规范规定必须是：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"access_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xxxxxxx"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"token_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Bearer"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"expires_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"refresh_token"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xxxxxxx"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"scope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user:email"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="heading-刷新-token-接口">刷新 Token 接口</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&amp;refresh_token=xxxxxxx
&amp;client_id=xxx
&amp;client_secret=xxx
</code></pre></div></div>

<h3 id="heading-用户信息接口">用户信息接口</h3>

<p>这个接口规范没有强制规定，由你自己定义：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET /api/user
Authorization: Bearer access_token
</code></pre></div></div>

<p><strong>规范固定的：</strong> 参数名（<code class="language-plaintext highlighter-rouge">grant_type</code>、<code class="language-plaintext highlighter-rouge">code</code>、<code class="language-plaintext highlighter-rouge">client_id</code> 等）、返回字段名（<code class="language-plaintext highlighter-rouge">access_token</code>、<code class="language-plaintext highlighter-rouge">expires_in</code> 等）。<br />
<strong>你自己定的：</strong> 接口地址、token 有效期、权限范围的名称、用户信息接口的返回结构。</p>

<hr />

<h2 id="heading-九作为提供方用-go-实现授权服务器">九、作为提供方：用 Go 实现授权服务器</h2>

<p>提供方没有像接入方那样开箱即用的通用 SDK，因为涉及太多业务逻辑。但有框架级别的库帮你省掉规范层的工作，Go 里常用的是 <code class="language-plaintext highlighter-rouge">go-oauth2/oauth2</code>。</p>

<h3 id="heading-整体架构">整体架构</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>接入方（B系统）
    ↓ ↑
━━━━━━━━━━━━━━━━━━━━━━━━━━━
          对外接口层
  授权页 | 换token | 刷新token | 用户信息
━━━━━━━━━━━━━━━━━━━━━━━━━━━
          内部核心模块
  应用管理 | 授权管理 | token管理 | code管理
━━━━━━━━━━━━━━━━━━━━━━━━━━━
          基础设施
  用户系统 | MySQL | Redis
━━━━━━━━━━━━━━━━━━━━━━━━━━━
</code></pre></div></div>

<h3 id="heading-实现代码">实现代码</h3>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"encoding/json"</span>
    <span class="s">"fmt"</span>
    <span class="s">"log"</span>
    <span class="s">"net/http"</span>
    <span class="s">"time"</span>

    <span class="s">"github.com/go-oauth2/oauth2/v4/errors"</span>
    <span class="s">"github.com/go-oauth2/oauth2/v4/generates"</span>
    <span class="s">"github.com/go-oauth2/oauth2/v4/manage"</span>
    <span class="s">"github.com/go-oauth2/oauth2/v4/models"</span>
    <span class="s">"github.com/go-oauth2/oauth2/v4/server"</span>
    <span class="s">"github.com/go-oauth2/oauth2/v4/store"</span>
<span class="p">)</span>

<span class="k">var</span> <span class="n">users</span> <span class="o">=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="o">*</span><span class="n">User</span><span class="p">{</span>
    <span class="s">"user001"</span><span class="o">:</span> <span class="p">{</span><span class="n">ID</span><span class="o">:</span> <span class="s">"user001"</span><span class="p">,</span> <span class="n">Name</span><span class="o">:</span> <span class="s">"张三"</span><span class="p">,</span> <span class="n">Email</span><span class="o">:</span> <span class="s">"zhangsan@example.com"</span><span class="p">,</span> <span class="n">Password</span><span class="o">:</span> <span class="s">"123456"</span><span class="p">},</span>
<span class="p">}</span>

<span class="k">type</span> <span class="n">User</span> <span class="k">struct</span><span class="p">{</span> <span class="n">ID</span><span class="p">,</span> <span class="n">Name</span><span class="p">,</span> <span class="n">Email</span><span class="p">,</span> <span class="n">Password</span> <span class="kt">string</span> <span class="p">}</span>

<span class="k">var</span> <span class="n">sessions</span> <span class="o">=</span> <span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{}</span> <span class="c">// sessionID -&gt; userID</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">manager</span> <span class="o">:=</span> <span class="n">manage</span><span class="o">.</span><span class="n">NewDefaultManager</span><span class="p">()</span>
    <span class="n">manager</span><span class="o">.</span><span class="n">SetAuthorizeCodeTokenCfg</span><span class="p">(</span><span class="o">&amp;</span><span class="n">manage</span><span class="o">.</span><span class="n">Config</span><span class="p">{</span>
        <span class="n">AccessTokenExp</span><span class="o">:</span>    <span class="m">2</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Hour</span><span class="p">,</span>
        <span class="n">RefreshTokenExp</span><span class="o">:</span>   <span class="m">7</span> <span class="o">*</span> <span class="m">24</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Hour</span><span class="p">,</span>
        <span class="n">IsGenerateRefresh</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
    <span class="p">})</span>

    <span class="c">// token 存储，换成 Redis 即支持分布式</span>
    <span class="n">manager</span><span class="o">.</span><span class="n">MustTokenStorage</span><span class="p">(</span><span class="n">store</span><span class="o">.</span><span class="n">NewMemoryTokenStore</span><span class="p">())</span>
    <span class="n">manager</span><span class="o">.</span><span class="n">MapAccessGenerate</span><span class="p">(</span><span class="n">generates</span><span class="o">.</span><span class="n">NewAccessGenerate</span><span class="p">())</span>

    <span class="n">clientStore</span> <span class="o">:=</span> <span class="n">store</span><span class="o">.</span><span class="n">NewClientStore</span><span class="p">()</span>
    <span class="n">clientStore</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"my_client_id"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">models</span><span class="o">.</span><span class="n">Client</span><span class="p">{</span>
        <span class="n">ID</span><span class="o">:</span>     <span class="s">"my_client_id"</span><span class="p">,</span>
        <span class="n">Secret</span><span class="o">:</span> <span class="s">"my_client_secret"</span><span class="p">,</span>
        <span class="n">Domain</span><span class="o">:</span> <span class="s">"http://localhost:9090"</span><span class="p">,</span>
    <span class="p">})</span>
    <span class="n">manager</span><span class="o">.</span><span class="n">MapClientStorage</span><span class="p">(</span><span class="n">clientStore</span><span class="p">)</span>

    <span class="n">srv</span> <span class="o">:=</span> <span class="n">server</span><span class="o">.</span><span class="n">NewDefaultServer</span><span class="p">(</span><span class="n">manager</span><span class="p">)</span>

    <span class="c">// 关键钩子：框架来问你「这个用户登录了吗」，你来决定怎么判断</span>
    <span class="n">srv</span><span class="o">.</span><span class="n">SetUserAuthorizationHandler</span><span class="p">(</span><span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">(</span><span class="kt">string</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">cookie</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">r</span><span class="o">.</span><span class="n">Cookie</span><span class="p">(</span><span class="s">"session_id"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="n">http</span><span class="o">.</span><span class="n">Redirect</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="s">"/login?redirect="</span><span class="o">+</span><span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusFound</span><span class="p">)</span>
            <span class="k">return</span> <span class="s">""</span><span class="p">,</span> <span class="n">errors</span><span class="o">.</span><span class="n">ErrAccessDenied</span>
        <span class="p">}</span>
        <span class="n">userID</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">sessions</span><span class="p">[</span><span class="n">cookie</span><span class="o">.</span><span class="n">Value</span><span class="p">]</span>
        <span class="k">if</span> <span class="o">!</span><span class="n">ok</span> <span class="p">{</span>
            <span class="n">http</span><span class="o">.</span><span class="n">Redirect</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="s">"/login?redirect="</span><span class="o">+</span><span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">String</span><span class="p">(),</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusFound</span><span class="p">)</span>
            <span class="k">return</span> <span class="s">""</span><span class="p">,</span> <span class="n">errors</span><span class="o">.</span><span class="n">ErrAccessDenied</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">userID</span><span class="p">,</span> <span class="no">nil</span>
    <span class="p">})</span>

    <span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/login"</span><span class="p">,</span> <span class="n">handleLogin</span><span class="p">)</span>

    <span class="c">// 框架帮你处理参数解析、code 生成、跳转</span>
    <span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/oauth/authorize"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">srv</span><span class="o">.</span><span class="n">HandleAuthorizeRequest</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span>
    <span class="p">})</span>

    <span class="c">// 框架帮你验证 code、生成 token、返回规范格式</span>
    <span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/oauth/token"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">srv</span><span class="o">.</span><span class="n">HandleTokenRequest</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">r</span><span class="p">)</span>
    <span class="p">})</span>

    <span class="c">// 用户信息接口，自己定义</span>
    <span class="n">http</span><span class="o">.</span><span class="n">HandleFunc</span><span class="p">(</span><span class="s">"/api/user"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">tokenInfo</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">srv</span><span class="o">.</span><span class="n">ValidationBearerToken</span><span class="p">(</span><span class="n">r</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="n">http</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"token 无效"</span><span class="p">,</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusUnauthorized</span><span class="p">)</span>
            <span class="k">return</span>
        <span class="p">}</span>
        <span class="n">user</span> <span class="o">:=</span> <span class="n">users</span><span class="p">[</span><span class="n">tokenInfo</span><span class="o">.</span><span class="n">GetUserID</span><span class="p">()]</span>
        <span class="n">w</span><span class="o">.</span><span class="n">Header</span><span class="p">()</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span> <span class="s">"application/json"</span><span class="p">)</span>
        <span class="n">json</span><span class="o">.</span><span class="n">NewEncoder</span><span class="p">(</span><span class="n">w</span><span class="p">)</span><span class="o">.</span><span class="n">Encode</span><span class="p">(</span><span class="k">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">string</span><span class="p">{</span>
            <span class="s">"id"</span><span class="o">:</span> <span class="n">user</span><span class="o">.</span><span class="n">ID</span><span class="p">,</span> <span class="s">"name"</span><span class="o">:</span> <span class="n">user</span><span class="o">.</span><span class="n">Name</span><span class="p">,</span> <span class="s">"email"</span><span class="o">:</span> <span class="n">user</span><span class="o">.</span><span class="n">Email</span><span class="p">,</span>
        <span class="p">})</span>
    <span class="p">})</span>

    <span class="n">log</span><span class="o">.</span><span class="n">Fatal</span><span class="p">(</span><span class="n">http</span><span class="o">.</span><span class="n">ListenAndServe</span><span class="p">(</span><span class="s">":8080"</span><span class="p">,</span> <span class="no">nil</span><span class="p">))</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">handleLogin</span><span class="p">(</span><span class="n">w</span> <span class="n">http</span><span class="o">.</span><span class="n">ResponseWriter</span><span class="p">,</span> <span class="n">r</span> <span class="o">*</span><span class="n">http</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">redirect</span> <span class="o">:=</span> <span class="n">r</span><span class="o">.</span><span class="n">URL</span><span class="o">.</span><span class="n">Query</span><span class="p">()</span><span class="o">.</span><span class="n">Get</span><span class="p">(</span><span class="s">"redirect"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">r</span><span class="o">.</span><span class="n">Method</span> <span class="o">==</span> <span class="n">http</span><span class="o">.</span><span class="n">MethodGet</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Fprintf</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">`&lt;form method="POST" action="/login?redirect=%s"&gt;
            &lt;input name="username" placeholder="user001"&gt;
            &lt;input name="password" type="password"&gt;
            &lt;button&gt;登录&lt;/button&gt;&lt;/form&gt;`</span><span class="p">,</span> <span class="n">redirect</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    <span class="n">user</span><span class="p">,</span> <span class="n">ok</span> <span class="o">:=</span> <span class="n">users</span><span class="p">[</span><span class="n">r</span><span class="o">.</span><span class="n">FormValue</span><span class="p">(</span><span class="s">"username"</span><span class="p">)]</span>
    <span class="k">if</span> <span class="o">!</span><span class="n">ok</span> <span class="o">||</span> <span class="n">user</span><span class="o">.</span><span class="n">Password</span> <span class="o">!=</span> <span class="n">r</span><span class="o">.</span><span class="n">FormValue</span><span class="p">(</span><span class="s">"password"</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">http</span><span class="o">.</span><span class="n">Error</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"用户名或密码错误"</span><span class="p">,</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusUnauthorized</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    <span class="n">sessionID</span> <span class="o">:=</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"sess_%s_%d"</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">ID</span><span class="p">,</span> <span class="n">time</span><span class="o">.</span><span class="n">Now</span><span class="p">()</span><span class="o">.</span><span class="n">UnixNano</span><span class="p">())</span>
    <span class="n">sessions</span><span class="p">[</span><span class="n">sessionID</span><span class="p">]</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">ID</span>
    <span class="n">http</span><span class="o">.</span><span class="n">SetCookie</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">http</span><span class="o">.</span><span class="n">Cookie</span><span class="p">{</span><span class="n">Name</span><span class="o">:</span> <span class="s">"session_id"</span><span class="p">,</span> <span class="n">Value</span><span class="o">:</span> <span class="n">sessionID</span><span class="p">,</span> <span class="n">Path</span><span class="o">:</span> <span class="s">"/"</span><span class="p">})</span>
    <span class="k">if</span> <span class="n">redirect</span> <span class="o">!=</span> <span class="s">""</span> <span class="p">{</span>
        <span class="n">http</span><span class="o">.</span><span class="n">Redirect</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="n">r</span><span class="p">,</span> <span class="n">redirect</span><span class="p">,</span> <span class="n">http</span><span class="o">.</span><span class="n">StatusFound</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Fprintf</span><span class="p">(</span><span class="n">w</span><span class="p">,</span> <span class="s">"登录成功！欢迎 %s"</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">Name</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>框架帮你做的：</strong> 规范接口的参数解析、code 生成、token 生成、返回规范 JSON 格式。</p>

<p><strong>你自己要做的：</strong> 用户登录逻辑（<code class="language-plaintext highlighter-rouge">SetUserAuthorizationHandler</code> 钩子）、用户信息接口、应用注册管理后台、存储层适配。</p>

<h3 id="heading-http-框架兼容性">HTTP 框架兼容性</h3>

<p><code class="language-plaintext highlighter-rouge">go-oauth2/oauth2</code> 基于标准库 <code class="language-plaintext highlighter-rouge">net/http</code>，Gin、Echo、Chi 这些框架底层都是标准库封装，可以直接传：</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">// Gin 示例</span>
<span class="n">r</span><span class="o">.</span><span class="n">GET</span><span class="p">(</span><span class="s">"/oauth/authorize"</span><span class="p">,</span> <span class="k">func</span><span class="p">(</span><span class="n">c</span> <span class="o">*</span><span class="n">gin</span><span class="o">.</span><span class="n">Context</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">srv</span><span class="o">.</span><span class="n">HandleAuthorizeRequest</span><span class="p">(</span><span class="n">c</span><span class="o">.</span><span class="n">Writer</span><span class="p">,</span> <span class="n">c</span><span class="o">.</span><span class="n">Request</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<p><strong>Fiber 不行</strong>，它用的是 <code class="language-plaintext highlighter-rouge">fasthttp</code> 而非标准库，类型不兼容，避免在这个场景下使用。</p>

<hr />

<h2 id="heading-十分布式环境下的注意事项">十、分布式环境下的注意事项</h2>

<p>demo 里用的是内存存储，分布式环境下会出问题：用户在服务器 A 完成授权，token 存在 A 的内存里，下次请求打到服务器 B，验证失败。</p>

<p>解决方案是把存储换成 Redis：</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="n">oredis</span> <span class="s">"github.com/go-oauth2/redis/v4"</span>

<span class="n">manager</span><span class="o">.</span><span class="n">MustTokenStorage</span><span class="p">(</span>
    <span class="n">oredis</span><span class="o">.</span><span class="n">NewRedisStore</span><span class="p">(</span><span class="o">&amp;</span><span class="n">redis</span><span class="o">.</span><span class="n">Options</span><span class="p">{</span>
        <span class="n">Addr</span><span class="o">:</span> <span class="s">"localhost:6379"</span><span class="p">,</span>
    <span class="p">}),</span>
<span class="p">)</span>
</code></pre></div></div>

<p>同样，session 也要换成 Redis 存储。框架逻辑本身没问题，只需要把所有内存存储替换成集中式存储，分布式就完全支持了。</p>

<hr />

<h2 id="heading-小结">小结</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>接入方（B系统）</th>
      <th>提供方（A系统）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>主要工作</td>
      <td>配置 + 处理回调 + 存 token</td>
      <td>实现规范接口 + 用户系统集成</td>
    </tr>
    <tr>
      <td>Go 库</td>
      <td><code class="language-plaintext highlighter-rouge">golang.org/x/oauth2</code>（官方）</td>
      <td><code class="language-plaintext highlighter-rouge">go-oauth2/oauth2</code>（社区）</td>
    </tr>
    <tr>
      <td>SDK 覆盖度</td>
      <td>~90%，基本开箱即用</td>
      <td>~40%，规范层封装，业务自己来</td>
    </tr>
    <tr>
      <td>分布式支持</td>
      <td>存储换 Redis 即可</td>
      <td>存储换 Redis 即可</td>
    </tr>
  </tbody>
</table>

<p>OAuth 2.0 的设计哲学是：<strong>密码永远只在用户和授权方之间，第三方只拿有限权限的 token</strong>。这个核心思路理解了，其他的都是实现细节。</p>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="OAuth" /><category term="安全" /><category term="Go" /><category term="后端" /><category term="认证授权" /><summary type="html"><![CDATA[从概念到实现，把 OAuth 2.0 的完整知识体系过一遍：它解决什么问题、流程怎么跑、作为接入方怎么用、作为提供方怎么实现，以及 JWT、分布式、安全这些相关话题，附完整 Go 代码。]]></summary></entry><entry><title type="html">一个软件工程师的深度学习入门实录：从训练第一个模型到理解微调与蒸馏</title><link href="https://sut-gc.github.io/blog/2026/02/12/%E4%B8%80%E4%B8%AA%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%85%A5%E9%97%A8%E5%AE%9E%E5%BD%95-%E4%BB%8E%E8%AE%AD%E7%BB%83%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%A8%A1%E5%9E%8B%E5%88%B0%E7%90%86%E8%A7%A3%E5%BE%AE%E8%B0%83%E4%B8%8E%E8%92%B8%E9%A6%8F/" rel="alternate" type="text/html" title="一个软件工程师的深度学习入门实录：从训练第一个模型到理解微调与蒸馏" /><published>2026-02-12T00:00:00+00:00</published><updated>2026-02-12T00:00:00+00:00</updated><id>https://sut-gc.github.io/blog/2026/02/12/%E4%B8%80%E4%B8%AA%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%85%A5%E9%97%A8%E5%AE%9E%E5%BD%95-%E4%BB%8E%E8%AE%AD%E7%BB%83%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%A8%A1%E5%9E%8B%E5%88%B0%E7%90%86%E8%A7%A3%E5%BE%AE%E8%B0%83%E4%B8%8E%E8%92%B8%E9%A6%8F</id><content type="html" xml:base="https://sut-gc.github.io/blog/2026/02/12/%E4%B8%80%E4%B8%AA%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B%E5%B8%88%E7%9A%84%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E5%85%A5%E9%97%A8%E5%AE%9E%E5%BD%95-%E4%BB%8E%E8%AE%AD%E7%BB%83%E7%AC%AC%E4%B8%80%E4%B8%AA%E6%A8%A1%E5%9E%8B%E5%88%B0%E7%90%86%E8%A7%A3%E5%BE%AE%E8%B0%83%E4%B8%8E%E8%92%B8%E9%A6%8F/"><![CDATA[<ul class="toc" id="markdown-toc">
  <li><a href="#heading-写在前面" id="markdown-toc-heading-写在前面">写在前面</a></li>
  <li><a href="#heading-pytorch-和-tensorflow-是什么" id="markdown-toc-heading-pytorch-和-tensorflow-是什么">PyTorch 和 TensorFlow 是什么？</a></li>
  <li><a href="#heading-深度学习核心概念的软件工程类比" id="markdown-toc-heading-深度学习核心概念的软件工程类比">深度学习核心概念的软件工程类比</a></li>
  <li><a href="#heading-动手训练第一个模型" id="markdown-toc-heading-动手训练第一个模型">动手：训练第一个模型</a>    <ul>
      <li><a href="#heading-环境准备" id="markdown-toc-heading-环境准备">环境准备</a></li>
      <li><a href="#heading-模型结构" id="markdown-toc-heading-模型结构">模型结构</a></li>
      <li><a href="#heading-训练模板" id="markdown-toc-heading-训练模板">训练模板</a></li>
      <li><a href="#heading-训练结果" id="markdown-toc-heading-训练结果">训练结果</a></li>
      <li><a href="#heading-关于效果好不好" id="markdown-toc-heading-关于效果好不好">关于"效果好不好"</a></li>
    </ul>
  </li>
  <li><a href="#heading-训练好的模型跑在哪" id="markdown-toc-heading-训练好的模型跑在哪">训练好的模型跑在哪？</a></li>
  <li><a href="#heading-为什么需要-gpu" id="markdown-toc-heading-为什么需要-gpu">为什么需要 GPU？</a>    <ul>
      <li><a href="#heading-cuda-和-nvidia-的护城河" id="markdown-toc-heading-cuda-和-nvidia-的护城河">CUDA 和 NVIDIA 的护城河</a></li>
      <li><a href="#heading-dgx-spark桌面级-ai-超级计算机" id="markdown-toc-heading-dgx-spark桌面级-ai-超级计算机">DGX Spark：桌面级 AI 超级计算机</a></li>
    </ul>
  </li>
  <li><a href="#heading-微调fine-tuning" id="markdown-toc-heading-微调fine-tuning">微调（Fine-tuning）</a>    <ul>
      <li><a href="#heading-什么是微调" id="markdown-toc-heading-什么是微调">什么是微调？</a></li>
      <li><a href="#heading-为什么微调有效" id="markdown-toc-heading-为什么微调有效">为什么微调有效？</a></li>
      <li><a href="#heading-动手微调" id="markdown-toc-heading-动手微调">动手微调</a></li>
      <li><a href="#heading-微调结果" id="markdown-toc-heading-微调结果">微调结果</a></li>
    </ul>
  </li>
  <li><a href="#heading-蒸馏distillation" id="markdown-toc-heading-蒸馏distillation">蒸馏（Distillation）</a>    <ul>
      <li><a href="#heading-什么是蒸馏" id="markdown-toc-heading-什么是蒸馏">什么是蒸馏？</a></li>
      <li><a href="#heading-蒸馏怎么做" id="markdown-toc-heading-蒸馏怎么做">蒸馏怎么做？</a></li>
      <li><a href="#heading-蒸馏的成本优势" id="markdown-toc-heading-蒸馏的成本优势">蒸馏的成本优势</a></li>
      <li><a href="#heading-蒸馏-vs-微调-vs-rag" id="markdown-toc-heading-蒸馏-vs-微调-vs-rag">蒸馏 vs 微调 vs RAG</a></li>
    </ul>
  </li>
  <li><a href="#heading-训练-ai-模型的工作量分布" id="markdown-toc-heading-训练-ai-模型的工作量分布">训练 AI 模型的工作量分布</a></li>
  <li><a href="#heading-完整代码" id="markdown-toc-heading-完整代码">完整代码</a></li>
</ul>

<p><img src="/files/images/dl-intro/cover.jpg" alt="一个软件工程师的深度学习入门实录" /></p>

<h2 id="heading-写在前面">写在前面</h2>

<p>作为一个软件工程师，我一直对深度学习的概念停留在"知道但不理解"的阶段。PyTorch、TensorFlow、微调、蒸馏这些词天天看到，但到底是什么、怎么工作的，一直没有真正搞清楚。</p>

<p>这篇文章记录了我从零开始动手训练模型的过程，用软件开发的概念来类比深度学习的核心知识点。如果你和我一样是软件工程师背景，想搞懂这些东西，这篇文章应该能帮到你。</p>

<hr />

<h2 id="heading-pytorch-和-tensorflow-是什么">PyTorch 和 TensorFlow 是什么？</h2>

<p>一句话：<strong>它们就是深度学习领域的开发框架</strong>，类似于 Web 开发中的 Spring 和 Django，只不过不是用来做 Web 应用的，而是用来构建和训练 AI 模型的。</p>

<p><strong>PyTorch</strong> 由 Meta 开发，采用动态计算图，写起来更像普通 Python 代码，调试方便，目前在学术研究领域最受欢迎。</p>

<p><strong>TensorFlow</strong> 由 Google 开发，早期用静态计算图，2.0 之后也支持动态图。它的优势在于生产部署生态更成熟（TensorFlow Serving、TensorFlow Lite 等）。</p>

<p>如果刚入门，大多数人推荐从 PyTorch 开始。</p>

<hr />

<h2 id="heading-深度学习核心概念的软件工程类比">深度学习核心概念的软件工程类比</h2>

<p>在动手之前，先用我们熟悉的概念建立映射：</p>

<table>
  <thead>
    <tr>
      <th>深度学习概念</th>
      <th>软件工程类比</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>训练数据</td>
      <td>数据库里的数据</td>
    </tr>
    <tr>
      <td>模型</td>
      <td>程序逻辑（但是从数据中自动学出来的）</td>
    </tr>
    <tr>
      <td>训练过程</td>
      <td>编译 + 反复跑测试直到通过</td>
    </tr>
    <tr>
      <td>部署推理</td>
      <td>应用上线</td>
    </tr>
    <tr>
      <td>模型文件（.pth）</td>
      <td>编译产物（jar / Docker 镜像）</td>
    </tr>
    <tr>
      <td>PyTorch / TensorFlow</td>
      <td>IDE + 编译器</td>
    </tr>
    <tr>
      <td>推理引擎（vLLM 等）</td>
      <td>生产环境运行时</td>
    </tr>
  </tbody>
</table>

<p>一个关键认知：<strong>训练框架和推理引擎往往是解耦的</strong>。很多公司用 PyTorch 训练，然后把模型转换格式部署到专门的推理引擎上。就像你在 IDE 里写代码编译，但最终跑的是 jar 包在 Tomcat 上，而不是跑在 IDE 里。</p>

<p><img src="/files/images/dl-intro/dl-vs-se.jpg" alt="深度学习 ≈ 软件工程" /></p>

<hr />

<h2 id="heading-动手训练第一个模型">动手：训练第一个模型</h2>

<p>选的是深度学习的 "Hello World"——<strong>MNIST 手写数字识别</strong>。在 Mac 上用 PyTorch 完成。</p>

<h3 id="heading-环境准备">环境准备</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>python@3.12          <span class="c"># PyTorch 暂不支持 3.14</span>
<span class="nb">mkdir</span> ~/mnist-demo <span class="o">&amp;&amp;</span> <span class="nb">cd</span> ~/mnist-demo
python3.12 <span class="nt">-m</span> venv venv
<span class="nb">source </span>venv/bin/activate
pip <span class="nb">install </span>torch torchvision
</code></pre></div></div>

<blockquote>
  <p>注：<code class="language-plaintext highlighter-rouge">torch</code> 就是 PyTorch 的包名，<code class="language-plaintext highlighter-rouge">torchvision</code> 是配套的图像处理库。</p>
</blockquote>

<h3 id="heading-模型结构">模型结构</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SimpleNet</span><span class="p">(</span><span class="n">nn</span><span class="p">.</span><span class="n">Module</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="nb">super</span><span class="p">().</span><span class="n">__init__</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">flatten</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Flatten</span><span class="p">()</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">layers</span> <span class="o">=</span> <span class="n">nn</span><span class="p">.</span><span class="n">Sequential</span><span class="p">(</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">28</span> <span class="o">*</span> <span class="mi">28</span><span class="p">,</span> <span class="mi">128</span><span class="p">),</span>   <span class="c1"># 第1层：784 → 128
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>                  <span class="c1"># 激活函数
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">128</span><span class="p">,</span> <span class="mi">64</span><span class="p">),</span>         <span class="c1"># 第2层：128 → 64
</span>            <span class="n">nn</span><span class="p">.</span><span class="n">ReLU</span><span class="p">(),</span>
            <span class="n">nn</span><span class="p">.</span><span class="n">Linear</span><span class="p">(</span><span class="mi">64</span><span class="p">,</span> <span class="mi">10</span><span class="p">),</span>          <span class="c1"># 第3层：64 → 10（输出0-9）
</span>        <span class="p">)</span>
</code></pre></div></div>

<p>这里每个 <code class="language-plaintext highlighter-rouge">nn.Linear</code> 就是一层。层数和大小<strong>不是固定的，是你自己设计的</strong>，相当于系统的架构设计：</p>

<ul>
  <li><strong>28×28 和 10</strong> 是由数据决定的（输入图片 28×28 像素，输出 0-9 十个数字），不能随便改</li>
  <li><strong>128 和 64</strong> 是你自己定的，可以改成 256、512，想加几层加几层</li>
  <li><strong>ReLU</strong> 是激活函数，给模型引入非线性能力，没有它不管堆多少层数学上都等价于一层</li>
</ul>

<p>不同场景的模型规模差别很大：我们这个 3 层、10 万参数；ResNet 上百层、几千万参数；GPT-4 据传上百层、万亿参数。</p>

<h3 id="heading-训练模板">训练模板</h3>

<p>几乎所有深度学习项目的训练循环都长这样：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">epoch</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">5</span><span class="p">):</span>                        <span class="c1"># 把所有数据学5遍
</span>    <span class="k">for</span> <span class="n">data</span><span class="p">,</span> <span class="n">target</span> <span class="ow">in</span> <span class="n">train_loader</span><span class="p">:</span>          <span class="c1"># 每次取一批数据
</span>        <span class="n">optimizer</span><span class="p">.</span><span class="n">zero_grad</span><span class="p">()</span>                  <span class="c1"># 清空上次的梯度
</span>        <span class="n">output</span> <span class="o">=</span> <span class="n">model</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>                   <span class="c1"># 前向传播：模型答题
</span>        <span class="n">loss</span> <span class="o">=</span> <span class="n">criterion</span><span class="p">(</span><span class="n">output</span><span class="p">,</span> <span class="n">target</span><span class="p">)</span>       <span class="c1"># 算损失：对比答案，错了多少
</span>        <span class="n">loss</span><span class="p">.</span><span class="n">backward</span><span class="p">()</span>                        <span class="c1"># 反向传播：找出哪些参数导致答错
</span>        <span class="n">optimizer</span><span class="p">.</span><span class="n">step</span><span class="p">()</span>                       <span class="c1"># 更新参数：调整参数让下次更准
</span></code></pre></div></div>

<p>这五行代码是<strong>标准模板</strong>，不管训练什么模型几乎一字不变。区别只在于外面的 model、data、criterion 怎么定义。</p>

<p><img src="/files/images/dl-intro/training-loop.jpg" alt="模型训练：标准五步循环" /></p>

<p>其中：</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">optimizer</code>（优化器）：决定怎么更新参数，Adam 是最常用的</li>
  <li><code class="language-plaintext highlighter-rouge">criterion</code>（损失函数）：衡量预测和正确答案的差距，分类任务一般用 CrossEntropyLoss</li>
  <li><code class="language-plaintext highlighter-rouge">lr=0.001</code>（学习率）：每次调整参数的步幅，太大学飘，太小学不动</li>
</ul>

<h3 id="heading-训练结果">训练结果</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>🚀 开始训练...
  第 1/5 轮 | 损失: 0.2743 | 训练准确率: 91.9%
  第 2/5 轮 | 损失: 0.1137 | 训练准确率: 96.5%
  第 3/5 轮 | 损失: 0.0796 | 训练准确率: 97.6%
  第 4/5 轮 | 损失: 0.0625 | 训练准确率: 98.1%
  第 5/5 轮 | 损失: 0.0492 | 训练准确率: 98.4%
🧪 测试模型...
  测试准确率: 97.6%
💾 模型已保存到 model.pth
</code></pre></div></div>

<p>可以看到准确率从 91.9% 提升到 98.4%。但<strong>训练轮次不是越多越好</strong>，训练太多轮会出现<strong>过拟合</strong>——模型把训练数据的噪声都"背"下来了，遇到新数据反而不准。就像对测试用例做了太多特殊处理，测试全过了但上线就出 bug。</p>

<p>训练产出的 <code class="language-plaintext highlighter-rouge">model.pth</code> 就是我们的 AI 模型文件，里面是模型学到的所有参数权重。</p>

<h3 id="heading-关于效果好不好">关于"效果好不好"</h3>

<p>不只是看训练准确率。<strong>测试准确率</strong>更重要，它衡量模型在没见过的数据上的表现（泛化能力）。到了大模型领域更复杂，需要从准确性、流畅度、幻觉率、推理能力等多个维度评估，各家发布模型时贴的 benchmark 跑分就是这个意思。</p>

<hr />

<h2 id="heading-训练好的模型跑在哪">训练好的模型跑在哪？</h2>

<p>这是一个重要的认知：<strong>训练框架 ≠ 推理环境</strong>。</p>

<p>训练确实需要跑在 PyTorch/TensorFlow 上，但推理（模型上线使用）有很多选择：</p>

<table>
  <thead>
    <tr>
      <th>推理引擎</th>
      <th>适用场景</th>
      <th>类比</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>vLLM</td>
      <td>大语言模型推理</td>
      <td>Nginx / Tomcat</td>
    </tr>
    <tr>
      <td>NVIDIA Triton</td>
      <td>企业级多模型管理</td>
      <td>K8s</td>
    </tr>
    <tr>
      <td>llama.cpp</td>
      <td>个人 / 边缘设备</td>
      <td>嵌入式轻量运行时</td>
    </tr>
    <tr>
      <td>ONNX Runtime</td>
      <td>跨平台通用</td>
      <td>JVM</td>
    </tr>
    <tr>
      <td>TensorFlow Lite</td>
      <td>移动端</td>
      <td>移动端运行时</td>
    </tr>
  </tbody>
</table>

<p>现在如果听到某家公司说"自己部署了大模型"，大概率后面跑的就是 vLLM。</p>

<hr />

<h2 id="heading-为什么需要-gpu">为什么需要 GPU？</h2>

<p>两个核心需求：<strong>算得快 + 放得下</strong>。</p>

<p>深度学习的本质是大量矩阵乘法，矩阵乘法天然可以拆成成千上万个互不依赖的小计算同时做。CPU 有几十个核心，GPU 有<strong>几千到上万个核心</strong>，虽然每个核心简单，但并行能力碾压 CPU。</p>

<p>类比：CPU 像一个资深工程师，什么都能干但只有一个人。GPU 像几千个初级工程师，每人只会简单运算，但可以同时干活。深度学习的活恰好就是把任务拆成几千份并行做。</p>

<p><img src="/files/images/dl-intro/cpu-vs-gpu.jpg" alt="CPU vs GPU" /></p>

<p>GPU 有自己的<strong>显存（VRAM）</strong>，模型必须加载到显存里 GPU 才能直接访问。大模型动不动几十上百 GB，一张 GPU 放不下就需要多张，这就是"模型并行"。</p>

<h3 id="heading-cuda-和-nvidia-的护城河">CUDA 和 NVIDIA 的护城河</h3>

<p>CUDA 是 NVIDIA 的 GPU 编程框架，让 GPU 能做通用计算。PyTorch 底层调用 CUDA 来驱动 GPU。CUDA 经营了十几年，整个 AI 工具链都建在上面，这就是 NVIDIA 的护城河——不光硬件好，软件生态不可替代。</p>

<h3 id="heading-dgx-spark桌面级-ai-超级计算机">DGX Spark：桌面级 AI 超级计算机</h3>

<p>NVIDIA 2025 年推出的 DGX Spark 是一台 Mac Mini 大小的 AI 专用电脑，128GB 统一内存（CPU/GPU 共享），售价约 $3,999，能在本地跑 200B 参数以内的模型。</p>

<p>有趣的是，苹果的 Mac Studio 用的也是统一内存架构，思路一样。区别在于 DGX Spark 原生支持 CUDA，AI 生态支持更好；Mac 更通用但 AI 优化不如 NVIDIA。</p>

<hr />

<h2 id="heading-微调fine-tuning">微调（Fine-tuning）</h2>

<h3 id="heading-什么是微调">什么是微调？</h3>

<p>拿一个已经训练好的模型，用你自己的少量数据再训练几轮，让它适应特定场景。</p>

<p>类比：<strong>预训练</strong> ≈ 从零开发一个框架，投入巨大。<strong>微调</strong> ≈ 拿成熟框架做二次开发，改配置加业务代码，快速适配需求。</p>

<h3 id="heading-为什么微调有效">为什么微调有效？</h3>

<p>大模型在预训练阶段已经学会了语言能力和通用知识。微调不是让它学新技能，而是<strong>调整它的注意力和表达习惯</strong>。就像一个全栈工程师来你们公司干了三个月，底层能力没变，但他熟悉了你们的代码规范和业务术语。</p>

<p>微调不是炼丹，它的目标很清楚：用专业数据让模型在某个方向更准。只要数据质量好、量够，效果基本是确定性的。</p>

<h3 id="heading-动手微调">动手微调</h3>

<p>我们在手写数字识别模型的基础上，微调它来识别 26 个英文字母。核心思路：复用底层"怎么看笔迹"的能力，只重新训练输出层。</p>

<p>关键代码：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 1. 加载旧模型，提取底层参数
</span><span class="n">old_model</span> <span class="o">=</span> <span class="n">OldSimpleNet</span><span class="p">()</span>
<span class="n">old_model</span><span class="p">.</span><span class="n">load_state_dict</span><span class="p">(</span><span class="n">torch</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="s">'model.pth'</span><span class="p">))</span>

<span class="c1"># 2. 创建新模型（26类），复用底层参数
</span><span class="n">model</span> <span class="o">=</span> <span class="n">NewSimpleNet</span><span class="p">(</span><span class="n">num_classes</span><span class="o">=</span><span class="mi">26</span><span class="p">)</span>
<span class="n">model</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">load_state_dict</span><span class="p">(</span><span class="n">old_layers_params</span><span class="p">)</span>

<span class="c1"># 3. 冻结底层，只训练输出层
</span><span class="k">for</span> <span class="n">param</span> <span class="ow">in</span> <span class="n">model</span><span class="p">.</span><span class="n">layers</span><span class="p">.</span><span class="n">parameters</span><span class="p">():</span>
    <span class="n">param</span><span class="p">.</span><span class="n">requires_grad</span> <span class="o">=</span> <span class="bp">False</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">requires_grad = False</code> 就是告诉 PyTorch 这些参数锁住不要动。类比 fork 了一个开源项目，核心模块设成 read-only，只在新加的模块里写代码。</p>

<h3 id="heading-微调结果">微调结果</h3>

<p>用 5000 张数据，只训练 1,690 个参数（总参数 11 万），1.1 秒训练完成：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  测试准确率: 49.2%
</code></pre></div></div>

<p>26 个字母随机猜是 3.8%，用这么少的数据和参数达到 49.2% 说明微调确实有效——底层复用的"看笔迹"能力起了作用。准确率不高是因为我们故意限制了数据量和可训练参数，用全量数据或解冻更多层效果会好很多。</p>

<hr />

<h2 id="heading-蒸馏distillation">蒸馏（Distillation）</h2>

<h3 id="heading-什么是蒸馏">什么是蒸馏？</h3>

<p>用大模型教小模型。关键在于小模型学的不是标准答案，而是<strong>大模型的思考方式</strong>。</p>

<p>一张潦草的手写 3：</p>
<ul>
  <li><strong>标准答案（硬标签）</strong>：这是 3</li>
  <li><strong>大模型输出（软标签）</strong>：3 的概率 70%，8 的概率 15%，5 的概率 10%</li>
</ul>

<p>软标签里包含了"暗知识"——3 和 8 长得像、3 和 5 也有点像。小模型学这个概率分布，不光学到"这是 3"，还学到了数据之间的关联关系。</p>

<p>类比：硬标签 ≈ 给你一份接口文档，只有输入输出。软标签 ≈ 资深工程师坐在旁边，不光告诉你答案，还告诉你"这里容易和那个混淆"。</p>

<h3 id="heading-蒸馏怎么做">蒸馏怎么做？</h3>

<p>两种方式：</p>
<ol>
  <li><strong>用大模型给现有数据打软标签</strong>，然后用软标签训练小模型</li>
  <li><strong>让大模型直接生成数据</strong>（问答对、推理过程等），拿生成的内容训练小模型</li>
</ol>

<p>DeepSeek-R1 用的就是第二种——让大模型生成大量带思考过程的回答，蒸馏出的小模型也学会了"先思考再回答"。</p>

<h3 id="heading-蒸馏的成本优势">蒸馏的成本优势</h3>

<p>从零训练顶级大模型可能要几亿美元。蒸馏的话，大模型已有（别人训练好的），花推理费用生成高质量数据（可能几十万美元），再训练小模型，总成本是从零训练的百分之一，效果能达到大模型的 80-90%。</p>

<h3 id="heading-蒸馏-vs-微调-vs-rag">蒸馏 vs 微调 vs RAG</h3>

<table>
  <thead>
    <tr>
      <th>方法</th>
      <th>目的</th>
      <th>类比</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>蒸馏</td>
      <td>得到更小更便宜的模型</td>
      <td>培训一批初中级工程师替代资深团队</td>
    </tr>
    <tr>
      <td>微调</td>
      <td>让模型适应特定领域</td>
      <td>让资深工程师熟悉你们的项目</td>
    </tr>
    <tr>
      <td>RAG</td>
      <td>给模型实时的外部知识</td>
      <td>给工程师随时查文档的权限</td>
    </tr>
  </tbody>
</table>

<p>如果你想让 AI 更懂你们的代码风格，最佳方案通常是 <strong>RAG + 微调</strong>，而不是蒸馏。</p>

<p><img src="/files/images/dl-intro/distill-finetune-rag.jpg" alt="蒸馏 vs 微调 vs RAG" /></p>

<hr />

<h2 id="heading-训练-ai-模型的工作量分布">训练 AI 模型的工作量分布</h2>

<p>最后总结一下训练 AI 模型的主要工作：</p>

<ul>
  <li><strong>数据准备（60-70%）</strong>：收集、清洗、标注数据。就像数据库设计和数据治理往往比写业务代码更耗时</li>
  <li><strong>模型架构设计</strong>：选什么类型的网络、多少层、每层多大。靠经验和实验</li>
  <li><strong>超参数调优</strong>：学习率、批量大小、训练轮次等。这些不是模型自己学的，是人手动设的</li>
  <li><strong>训练和评估的反复迭代</strong>：跑一轮看效果，不好就调整，再跑。可能重复几十上百次</li>
</ul>

<p>入门不难，核心训练模板就那几行代码。难在数据工程、模型设计的直觉、调参、数学基础、以及大规模训练的工程挑战。就像写 CRUD 不难，但设计高并发分布式系统很难。</p>

<hr />

<h2 id="heading-完整代码">完整代码</h2>

<p>本文涉及的所有代码（训练、预测、微调）都可以在 Mac 上直接运行，总共几十行，几分钟跑完。这可能是你能找到的最简单的深度学习入门实践了。</p>]]></content><author><name>gc</name><email>gouchao2008@qq.com</email></author><category term="技术" /><category term="AI" /><category term="深度学习" /><category term="PyTorch" /><category term="模型训练" /><category term="微调" /><category term="蒸馏" /><category term="AI入门" /><summary type="html"><![CDATA[以软件工程师的视角，用类比的方式理解深度学习核心概念，并动手训练、微调模型的完整记录]]></summary></entry></feed>