2026年2月

表示键和值的集合。

public class Dictionary < T键 ,
 T值 > : System . Collections . Generic . ICollection < System . Collections . Generic . KeyValuePair < T键 ,
 T值 > >,
 System . Collections . Generic . IDictionary < T键 ,
 T值 > ,
 System . Collections . Generic . IEnumerable < System . Collections . Generic . KeyValuePair < T键 ,
 T值 > > ,
 System . Collections . Generic . IReadOnlyCollection < System . Collections . Generic . KeyValuePair < T键 ,
 T值 > > ,
 System . Collections . Generic . IReadOnlyDictionary < T键 ,
 T值 > ,
 System . Collections . IDictionary ,
 System . Runtime . Serialization . IDeserializationCallback ,
 System . Runtime . Serialization . ISerializable

参数

参数类型注解
T键
T值
T字典中的 键 和值 的类型

继承

ObjectDictionary < T键 , T值 >

派生

System . ServiceModel . MessageQuerySet

实现

ICollection < KeyValuePair < T键 , T值 > > , IDictionary < T键 , T值 > , IEnumerable < KeyValuePair < T键 , T值 > > , IEnumerable < T > , IReadOnlyCollection < KeyValuePair < T键 , T值 > > , IReadOnlyDictionary < T键 , T值 > , ICollection , IDictionary , IEnumerable , IDeserializationCallback , ISerializable

示例

以下代码示例创建了一个带有字符串键的 Empty Dictionary < T键 , T值 > 字符串集合,并使用Add方法添加一些元素。该示例展示了在尝试添加重复键时,Add 方法会抛出 ArgumentException。

此示例使用 Item [ ] 属性(C# 中的索引器)来检索值,展示了当请求的键不存在时会抛出 KeyNotFoundException,并表明与键相关联的值可以被替换。

此示例展示了如果程序经常需要尝试查找不在字典中的键值,如何使用 TryGetValue 方法作为一种更高效的取值方式,还展示了在调用 Add 方法之前,如何使用 ContainsKey 方法来测试某个键是否存在。

这个示例展示了如何枚举字典中的键和值,以及如何使用 Keys 属性和 Values 属性单独枚举键和值。

最后,该示例演示了 Remove 方法。

// 创建一个以 string 为键,以 string 为值的 Dictionary(描述不同的文件类型的打开方式)
Dictionary < string , string > CD打开 = [ ];

CD打开 . Add ( "txt" , "notepad.exe" );
CD打开 . Add ( "bmp" , "paint.exe" );
CD打开 . Add ( "dib" , "paint.exe" ); // 键必须不同,但值可以相同
CD打开 . Add ( "rtf" , "wordpad.exe" );

try
    {
    CD打开 . Add ( "txt" , "winword.exe" ); // 向词典中添加一个元素
    }
catch ( ArgumentException yc ) // 由于键 txt 已存在,将会产生 ArgumentException
    {
    Console . WriteLine ( $"一个使用键 \"txt\" 的元素已存在;{yc . Message}" );
    }

Console . WriteLine ( $"使用键 \"rtf\" 的元素,值是:{CD打开 [ "rtf" ]}" );

// 如果键不存在,为键创建一个新的 键/值 对
CD打开 [ "docx" ] = "winword.exe";

// 当键不存在时,索引器抛出 KeyNotFoundException
try
    {
    Console . WriteLine ( $"使用键 \"tif\" 的元素,值是:{CD打开 [ "tif" ]}" );
    }
catch ( KeyNotFoundException yc )
    {
    Console . WriteLine ( $"使用键 \"tif\" 的元素不存在;{yc . Message}" );
    }

// 当键不存在时,TryGetValue 返回 null
if ( CD打开 . TryGetValue ( "tif" , out string? zfc ) )
    {
    Console . WriteLine ( $"使用键 \"tif\" 的元素,值是:{zfc}" );
    }
else
    {
    Console . WriteLine ( $"使用键 \"tif\" 的元素不存在" );
    }

// 可以使用 ContainsKey,多写一行 add 语句
if ( CD打开 . TryAdd ( "ht" , "hypertrm.exe" ) )
    {
    Console . WriteLine ( $"使用键 \"ht\" 的元素,值是:{CD打开 [ "ht" ]}" );
    }

// Dictionary 的每一个元素对应一个 KeyValuePair
Console . WriteLine ( );
foreach ( KeyValuePair < string , string > dui in CD打开 )
    {
    Console . WriteLine ( $"键 = {dui . Key};值 = {dui . Value}" );
    }

// ValueCollection 对应 Dictionary 的值的组合
Console . WriteLine ( );
Dictionary < string , string > . ValueCollection ZhiZU = CD打开 . Values;
foreach ( string z in ZhiZU )
    {
    Console . WriteLine ( $"值 = {z}" );
    }

// 移除某个 Dictionary 项
Console . WriteLine ( "\nRemove(\"docx\")" );
CD打开 . Remove ( "docx" );
if ( !CD打开 . ContainsKey ( "docx" ) )
    {
    Console . WriteLine ( "键 \"docx\" 不存在" );
    }

备注

泛型类 Dictionary < T键 , T值 > 提供了从一组键到一组值的映射。向字典添加的每个元素都包含一个值及其关联的键。通过键检索值的速度非常快,接近 O (1),因为 Dictionary < T键 , T值 > 类是作为哈希表实现的。但当 键 对象返回同一个哈希值时,检索速度将会变慢,应尽量使用系统内置(Int32/String 等)的密封类作为键类型。

注意:检索速度取决于为 T键 指定的哈希算法的质量。

只要一个对象在 Dictionary < T键 , T值 > 中用作键,它就绝不能以任何会影响其哈希值的方式发生更改。根据字典的相等比较器,Dictionary < T键 , T值 > 中的每个键都必须是唯一的。键不能为 null,但如果值的类型 T值 是引用类型,则值可以为 null。

Dictionary < T键 ,T值 > 需要一个相等性实现来确定键是否相等。您可以通过使用接受 比较器 参数的构造函数来指定 IEqualityComparer < T > 泛型接口的实现;如果不指定实现,则使用默认泛型相等比较器 EqualityComparer < T > . Default。如果类型 T键 实现了 System . IEquatable < T > 泛型接口,默认相等比较器将使用该实现。

注意:例如,你可以使用 StringComparer 类提供的不区分大小写的字符串比较器来创建具有不区分大小写的字符串键的字典。

Dictionary < T键 , T值 > 的容量是指 Dictionary < T键 , T值 > 可容纳的元素数量。当向 Dictionary < T键 , T值 > 中添加元素时,会通过重新分配内部数组,根据需要自动增加容量。

仅限于 .NET Framework:对于非常大的 Dictionary < T键 , T值 > 对象,在 64 位系统上,通过在运行时环境中将 < gcAllowVeryLargeObjects > 配置元素的 enabled 属性设置为 true,可以将最大容量增加到 20 亿个元素。

为便于枚举,字典中的每个项都被视为一个 KeyValuePair < T键 , T值 > 结构,该结构表示一个值及其键。返回项的顺序是未定义的。

C# 语言的 foreach 语句(Visual Basic 中为 For Each)返回集合中元素类型的对象。由于 Dictionary < T键 , T值 > 是键和值的集合,因此元素类型既不是键的类型,也不是值的类型。相反,元素类型是键类型和值类型的 KeyValuePair < T键 , T值 >。例如:

foreach( KeyValuePair < string , string > dui in CDA )
{
    Console . WriteLine ( $"键 = {dui . Key} , 值 = {dui . Value}" );
}

foreach 语句是枚举器的包装器,它只允许从集合中读取数据,而不允许向集合中写入数据。所以,不要在类似上述语句中添加 Add、Remove 等语句,仅限于查看其值。

注意:由于 key 可以被继承且其行为可以被更改,因此使用 Equals 方法进行比较无法保证它们的绝对唯一性。

构造函数

重载

构造函数描述
Dictionary < T键 , T值 > ( )初始化 Dictionary < T键 , T值 > 类的新实例(NotEmpty,即元素数为 0),但具有默认的初始容量,并使用键类型的默认相等比较器
Dictionary < T键 , T值 > ( IDictionary < T键 , T值 > )初始化 Dictionary < T键 , T值 > 类的新实例,该实例包含从指定的 IDictionary < T键 , T值 > 中复制的元素,并使用键类型的默认相等比较器
Dictionary < T键 , T值 > ( IEnumerable < KeyValuePair < T键 , T值 > > )初始化 Dictionary < T键 , T值 > 类的新实例 , 该实例包含从指定的 IEnumerable < T > 中复制的元素
Dictionary < T键 , T值 > ( IEqualityComparer < T键 > )初始化 Dictionary < T键 , T值 > 类的新实例(NotEmpty,即元素数为 0),具有默认的初始容量,并使用指定的 IEqualityComparer < T >
Dictionary < T键 , T值 > ( Int32 初始容量 )初始化 Dictionary < T键 , T值 > 类的新实例(NotEmpty,即元素数为 0),具有指定的初始容量,并使用键类型的默认相等比较器
Dictionary < T键 , T值 > ( IDictionary < T键 , T值 > , IEqualityComparer < T键 > )初始化 Dictionary < T键 , T值 > 类的新实例 , 该实例包含从指定的 IDictionary < T键 , T值 > 中复制的元素 , 并使用指定的 IEqualityComparer < T >
Dictionary < T键 , T值 > ( IEnumerable < KeyValuePair < T键 , T值 > > , IEqualityComparer < T键 > )初始化 Dictionary < T键 , T值 > 类的新实例,该实例包含从指定的 IEnumerable < T > 中复制的元素,并使用指定的 IEqualityComparer < T >
Dictionary < T键 , T值 > ( Int32 初始容量 , IEqualityComparer < T键 > )初始化 Dictionary < T键 , T值 > 类的新实例(NotEmpty,即元素数为 0),具有指定的初始容量,并使用指定的 IEqualityComparer < T >

备注:NotEmpty 可以类比于水瓶子,即不是 null(不存在,你没有瓶子),又不是 Empty(空、只读的,即你的水瓶子打不开),而是有容量但实际又没有元素的 Dictionary(你可以随时添加水且容量自动扩容的水瓶子),可以随时添加其元素。

public Dictionary ( );
public Dictionary ( System . Collections . Generic . IDictionary < T键 , T值> 词典 );
public Dictionary ( System . Collections . Generic . IEnumerable < System . Collections . Generic . KeyValuePair < T键 , T值 > > 集合 );
public Dictionary ( System . Collections . Generic . IEqualityComparer < T键 >? 比较器 );
public Dictionary ( int 初始容量 );
public Dictionary ( System . Collections . Generic . IDictionary < T键 , T值 > 词典 ,  System . Collections . Generic . IEqualityComparer < T键 >? 比较器 );
public Dictionary ( System . Collections . Generic . IEnumerable < System . Collections . Generic . KeyValuePair < T键 , T值 > > 集合 , System . Collections . Generic . IEqualityComparer < T键 >? 比较器 );
public Dictionary ( int 初始容量 , System . Collections . Generic . IEqualityComparer < T键 >? 比较器 );

参数

参数类型注解
T键
T值
T词典 键 和 值 的类型
词典IDictionary < T键 , T值 >欲复制到新词典中的词典
集合IEnumerable < KeyValuePair < T键 , T值 > >欲复制到新词典中的集合
比较器IEqualityComparer < T键 >?比较键时使用的 IEqualityComparer < T > 实现;或 null,使用键类型的默认 EqualityComparer < T >
初始容量Int32可包含的初始元素数量

异常

异常注解
ArgumentException词典 或 集合 中包含至少一个重复键
ArgumentNullException词典 或 集合 是 null
ArgumentOutOfRangeException初始容量 小于 0

示例

下面的示例,使用了一个已被排序的 SortedDictionary 创建了一个 Dictionary,两者内容(包括顺序)相同。

SortedDictionary < string , string > CD1 = [ ];
CD1 . Add ( "一" , "中文数字 1,表示某事物只有一个(件)" );
CD1 . Add ( "1" , "阿拉伯数字 1,表示某事物只有一个(件)" );
CD1 . Add ( "a" , "英文描述方式,表示某事物只有一个(件)" );
CD1 . Add ( "one" , "英文数字 1,表示某事物只有一个(件)" );

foreach ( KeyValuePair < string , string > jz in CD1 )
    Console . WriteLine ( jz );

Console.WriteLine ( );

Dictionary < string , string > CD一 = new ( CD1 );
foreach ( KeyValuePair < string , string > jz in CD一 )
    Console . WriteLine ( jz );

下面的代码示例创建了一个 Dictionary < T键 , T值 >,并为当前区域性使用不区分大小写的相等比较器。该示例添加了四个元素,其中一些元素的键为小写,一些为大写。然后,该示例尝试添加一个键仅在大小写方面与现有键不同的元素,捕获由此产生的异常,并显示错误消息。最后,该示例显示字典中的元素。

Dictionary < string , string > CD1 = new ( StringComparer . OrdinalIgnoreCase ) // 默认是 StringComparer . Ordinal
    {
        { "一" , "中文数字 1,表示某事物只有一个(件)" } ,
        { "1" , "阿拉伯数字 1,表示某事物只有一个(件)" } ,
        { "a" , "英文描述方式,表示某事物只有一个(件)" } ,
        { "one" , "英文数字 1,表示某事物只有一个(件)" }
    };

foreach ( KeyValuePair < string , string > jz in CD1 )
    Console . WriteLine ( jz );

Console.WriteLine ( );

try
    {
    CD1 . Add ( "One" , "英文数字 1,表示某事物只有一个(件)" );
    }
catch ( Exception yc ) { Console . WriteLine ( yc . Message ); }

备注

根据默认的相等比较器,Dictionary < T键 , T值 > 中的每个键都必须是唯一的。

Dictionary < T键 , T值 > 需要一个相等性实现来确定键是否相等。此构造函数使用默认的泛型相等比较器 EqualityComparer < T > . Default。如果类型 T键 实现了 System . IEquatable < T > 泛型接口,则默认的相等比较器会使用该实现。或者,您可以通过使用接受 比较器 参数的构造函数来指定 IEqualityComparer < T > 泛型接口的实现。

注意:如果您能够估计集合的大小,那么使用指定初始容量的构造函数可以避免在向 Dictionary < T键 , T值 > 中添加元素时执行多次大小调整操作。

此构造函数是一个 O(1)操作(创建空或指定元素数的词典)或 O(n)操作(创建 n 个元素的词典,n 为元素数量)。

属性

Capacity

获取内部数据结构在不调整大小的情况下可容纳的元素总数。
public int Capacity { get; }

属性值

类型注解
Int32该结构的当前最大容量(并被元素数)

备注

Capacity 属性表示当前 Dictionary 实例在不需要扩容的前提下,能够容纳的最大元素数;仅当当前 Dictionary 实例无法容纳全部新增元素时,Capacity 才会扩张。

若未指定初始容量,默认的初始容量为 0,添加一个元素后 Capacity 是 3。无论是否指定初始容量,Capacity 的返回值也均遵循下述规则计算。大于原 Capacity(0 除外)× 2 的下一个毕达哥拉斯质数(形如 4n + 1),或其 ± 2(必须也是质数,也必须大于原 Capacity × 2,且其二进制表示中 1 分布比较均匀),确保扩容后 Capacity 是原 Capacity 的大约 2.23 倍(取其 ± 2 仅限于适当均匀分布 Hash 列表,降低冲突率,例如选择 3 或者 7,而不是选择 5;若其二进制表示均为 1 的,取之,否则取二进制表示中 1 最平均分配位置的);其值存于 HashHelpers 类中。即 Capacity 的返回值一定是个毕达哥拉斯质数或其 ± 2,且大于等于指定的初始容量。

Compare

获取用于确定字典键的相等性的 IEqualityComparer < T >。
public System . Collections . Generic . IEqualityComparer < T键 > Comparer { get; }

属性值

类型注解
IEqualityComparer < T键 >用于确定当前 Dictionary < T键 , T值 > 的键是否相等并为键提供哈希值的 IEqualityComparer < T > 泛型接口实现

备注

Dictionary < T键 , T值 > 需要一个相等性实现来确定键是否相等。您可以通过使用接受 比较器 参数的构造函数来指定 IEqualityComparer < T > 泛型接口的实现;如果不指定,则使用默认泛型相等比较器 EqualityComparer < T > . Default。

获取此属性的值是一个 O(1)操作。

Count

获取 Dictionary < T键 , T值 > 中包含的键/值对的数量。
public int Count { get; }

属性值

类型注解
int词典 中的现有元素(键/值对)数

示例

以下示例展示了 Capacity 属性和 Count 属性的区别:

Dictionary < string , string > CD = new ( StringComparer . OrdinalIgnoreCase ); // 默认是 StringComparer . Ordinal
for ( int z = 1 ; z <= 10 ; z++ )
    {
    CD . Add ( z . ToString ( ) , $"阿拉伯数字 {z}" ); 
    }

Console . WriteLine ( $"词典中的元素数:{CD . Count},词典的容量:{CD . Capacity}" );

备注

Capacity 属性是词典能够容纳的最大元素数,但可以被放大,以容纳更多的元素,详见 Capacity 属性。Count 是词典中实际存在的元素(键/值对)数,必然小于等于当前的 Capacity 属性。

由于词典被放大是一个耗时操作,创建一个新词典(同名)并容量扩大约 2.23 倍,复制原词典的元素,然后添加新元素,所以如果能够准确知道词典的大小(例如 32),应使用带有 初始容量 参数的构造函数创建词典。

获取此属性的值是一个 O(1)操作。

Item [ T键 ]

获取或设置与指定键关联的值。
public TValue this [ T键 键 ] { get; set; }

属性值

与指定键相关联的值。如果未找到指定的键,get 操作会抛出 KeyNotFoundException,而 set 操作会创建一个具有指定键的新元素。

实现

Item [ T键 ]

异常

异常注解
ArgumentNullException键 是 null
KeyNotFoundException欲检索的 键 在词典中不存在(仅限于 get 操作)

示例

以下代码示例使用 Item [ ] 属性(C# 中的索引器)来检索值,演示了当请求的键不存在时会抛出 KeyNotFoundException,并展示了与键关联的值可以被替换。

此示例还展示了如果程序经常需要尝试字典中不存在的键值,如何使用 TryGetValue 方法作为一种更高效的检索值的方式。

此代码示例是为 Dictionary < T键 , T值 > 类提供的一个更大示例的一部分。CD3 是本示例中使用的 Dictionary 的名称。

// 创建一个包含英文和中文对于数字 3 和序数 3 的讲解的词典
Dictionary < string , string > CD3 = new ( StringComparer . OrdinalIgnoreCase ) // 默认是 StringComparer . Ordinal
    {
        { "3" , "阿拉伯数字 3,表示某事物有 3 个(件)" },
        { "Three" , "英文数字 3,表示某事物有 3 个(件)" },
        { "三" , "中文数字 3,表示某事物有 3 个(件)" },
        { "III" , "罗马数字 3,表示某事物有 3 个(件)" },
        { "Third" , "英文序数 3,表示某事物的第 3 个(件)" },
        { "3rd" , "英文序数 3 的简写形式,表示某事物的第 3 个(件)" },
    };

// 尝试添加一个仅大小写不同的序数 3 简写形式,但已存在(因为该词典不区分大小写)
try
    {
    CD3 . Add ( "3RD" , "英文序数 3 的简写形式,表示某事物的第 3 个(件)" );
    }
catch ( Exception yc ) { Console . WriteLine ( yc . Message ); }

// 使用 Item 的形式读取词典的内容
Console . WriteLine ( $"键 = \"3rd\",值 = {CD3 [ "3rd" ]}。" );

// 使用 Item 的形式向词典添加德语 3
CD3 [ "drei" ] = "德文数字 3,表示某事物有 3 个(件)";
Console . WriteLine ( $"键 = \"drei\",值 = {CD3 [ "drei" ]}。" );

// 使用 Item 的形式修改词典中的德语 3 条目
CD3 [ "drei" ] = "德文(德语)数字 3,表示某事物有 3 个(件)";
Console . WriteLine ( $"键 = \"drei\",值 = {CD3 [ "drei" ]}。" );

// 尝试使用 Item 的形式检索词典中不存在的法语 3
try
    {
    Console . WriteLine ( $"键 = \"trois\",值 = {CD3 [ "trois" ]}。" );

    }
catch ( Exception yc ) { Console . WriteLine ( yc . Message ); }

// 尝试使用 TryGetValue 读取词典中可能不存在的法语 3(没有异常)
if ( CD3 . TryGetValue ( "trois" , out string? zfcF3 ) )
    {
    Console . WriteLine ( $"键 = \"trois\",值 = {zfcF3}。" );
    }
else
    {
    Console . WriteLine ( $"键 = \"trois\",值 = 不存在。" );
    }

备注

此属性提供了通过以下 C# 语法访问集合中特定元素的能力:myCollection [ 键 ](在 Visual Basic 中为 myCollection ( 键 ))。

您也可以使用 Item [ ] 属性,通过设置 Dictionary < T键 , T值 > 中不存在的键的值来添加新元素。设置属性值时,如果该键存在于 Dictionary < T键 , T值 > 中,则与该键关联的值会被所分配的值替换。如果该键不存在于 Dictionary < T键 , T值 > 中,则会将该键和值添加到字典中。相比之下,Add 方法不会修改现有元素。

键不能为 null,但如果值类型 T值 是引用类型,则值可以为 null。

C# 语言使用 this 关键字来定义索引器,而不是实现 Item [ ] 属性。Visual Basic 将 Item [ ] 实现为默认属性,该属性提供相同的索引功能。

获取或设置此属性的值的操作时间复杂度接近 O(1)。

Key(键)和 Value(值)

Keys 获取包含 Dictionary < T键 , T值 > 中的键的集合。Values 获取包含 Dictionary < T键 , T值 > 中的值的集合。
` public System . Collections . Generic . Dictionary < T键 , T值 > . KeyCollection Keys { get; }
public System . Collections . Generic . Dictionary < T键 , T值 > . ValueCollection Values { get; } `

属性值

方法类型注解
KeysDictionary < T键 , T值 > . KeyCollection一个包含 Dictionary < T键 , T值 > 中键的 Dictionary < T键 , T值 > . KeyCollection
ValuesDictionary < T键 , T值 > . ValueCollection一个包含 Dictionary < T键 , T值 > 中值的 Dictionary < T键 , T值 > . ValueCollection

示例

Dictionary < int , string > CD2m = [ ];
CD2m . Add ( 0 , "未知" );
CD2m . Add ( 1 , "2 的 0 次幂,既不是质数也不是合数" );
CD2m . Add ( 2 , "2 的 1 次幂,最小的质数;唯一的偶质数" );
CD2m . Add ( 3 , "1 OR 2 的值,最小的奇质数" );
CD2m . Add ( 4 , "2 的 2 次幂,最小的合数" );
CD2m . Add ( 5 , "1 OR 4 的值" );
CD2m . Add ( 6 , "2 OR 4 的值" );
CD2m . Add ( 7 , "2 OR 5 的值" );
CD2m . Add ( 8 , "2 的 3 次幂" );


Dictionary < int , string > . KeyCollection JH键 = CD2m . Keys;
Dictionary < int , string > . ValueCollection JH值 = CD2m . Values;
int [ ] S键 = [ .. JH键 ];
string [ ] S值 = [ .. JH值 ];

for ( int z = 0 ; z < 9 ; z++ )
    {
    Console . WriteLine ( $"{S键 [ z ]}\t{S值 [ z ]}" );
    }

备注

Dictionary < T键 , T值 > . KeyCollection 中键的顺序未指定,但与 Values 属性返回的 Dictionary < T键 , T值 > . ValueCollection 中关联值的顺序相同。反之亦然。

返回的 Dictionary < T键 , T值 > . KeyCollection 和 Dictionary < T键 , T值 > . ValueCollection 不是静态副本;它们会引用回原始 Dictionary < T键 , T值 > 中的键和值。因此,对 Dictionary < T键 , T值 > 所做的更改会继续反映在 Dictionary < T键 , T值 > . KeyCollection 和 Dictionary < T键 , T值 > . ValueCollection 中。

获取此属性的值是一个 O(1)操作。

方法

Add 和 Remove

Add 添加指定的键和值到词典中;Remove 将词典中具有指定键的值。

public void Add ( T键 键 , T值 值 );
public bool Remove ( T键 键 );
public bool Remove ( T键 键 , out T值 值 );

参数

参数类型注解
T键欲添加或移除的元素(键/值对)的键
T值Add 方法添加的元素(键/值对)的值;或 Remove 方法移除的元素(键/值对)的值

返回值

方法类型注解
Removebool如果找到具备指定键值的元素,并成功移除,返回 true;如果移除不成功,或没有找到具备指定键值的元素,返回 false

实现

Add ( TKey , TValue )
Remove ( TKey )

异常

异常方法注解
ArgumentExceptionAdd词典中已经存在指定键值
ArgumentNullExceptionAdd
Remove
键 为 null

示例

以下例程通过 Add 和 Remove 创建并过滤了词典中的某些元素:

Dictionary < int , string > CD2m = [ ];
CD2m . Add ( 0 , "未知" );
CD2m . Add ( 1 , "2 的 0 次幂,既不是质数也不是合数" );
CD2m . Add ( 2 , "2 的 1 次幂,最小的质数;唯一的偶质数" );
CD2m . Add ( 3 , "1 OR 2 的值,最小的奇质数" );
CD2m . Add ( 4 , "2 的 2 次幂,最小的合数" );
CD2m . Add ( 5 , "1 OR 4 的值" );
CD2m . Add ( 6 , "2 OR 4 的值" );
CD2m . Add ( 7 , "2 OR 5 的值" );
CD2m . Add ( 8 , "2 的 3 次幂" );

foreach ( var JZ in CD2m )
    {
    int z键 = JZ . Key;
    if ( z键 % 2 == 0 )
        {
        CD2m . Remove ( z键 , out string? z值 );
        Console . WriteLine ( $"移除 {z键} 项,其值为 {z值}" );
        }
    }

备注

您也可以使用 Item [ ] 属性,通过设置 Dictionary < T键 , T值 > 中不存在的键的值来添加新元素;例如,CD [ 键 ] = 值(在 Visual Basic 中为 CD ( 键 ) = 值)。但是,如果指定的键已存在于 Dictionary < T键 , T值 > 中,设置 Item [ ] 属性会覆盖旧值。相比之下,如果已存在具有指定键的值,Add 方法会引发异常。

如果 Count 属性值已等于 Capacity,添加元素会通过自动重新分配内部数组来增加 Dictionary < T键 , T值 > 的容量,并且在添加新元素之前,会将现有元素复制到新数组中。

键不能为 null,但如果 T值 是引用类型,则值可以为 null。

如果 Count 小于容量,Add 方法接近 O(1)操作。如果必须增加容量以容纳新元素,Add 方法将变为 O(n)操作,其中 n 为 Count。Remove 方法接近 O(1)操作。

仅 .NET Core 3.0+ 支持:可以安全地调用 Remove 可变方法,而不会使 Dictionary < T键 , T值 > 实例上的活动枚举数失效。这并不意味着线程安全。

Clear

从 Dictionary < T键 , T值 > 中移除所有键和值。
public void Clear();

实现

Clear ( )

备注

该操作会释放集合元素对其他对象的引用。

词典的 Count 会置为零,但 Capacity 属性不变。

当词典需要重置且容量需要变小(节省内存)时或变大(符合 Capacity 增长规则)时,应使用 New 并指定一个更合适的初始容量;否则使用 Clear 重置性能更好。

此方法是一个 O(n)操作,其中 n 是字典的 Capacity。

仅适用于 .NET Core 3.0 及以上版本:可以安全地调用此可变方法,而不会使 Dictionary < T键 , T值 > 实例上的活动枚举数失效。这并不意味着线程安全。

ContainsKey 和 ContainsValue

确定 Dictionary < T键 , T值 > 是否包含指定的键或者值。

public bool ContainsKey ( T键 键 );
public bool ContainsValue ( T值 值 );

参数

参数类型注解
T键欲在词典中搜索的键值
T值欲在词典中搜索的值

返回值

方法类型注解
ContainsKeybool如果 词典 中包含具有指定键的元素,则为 true;否则为 false
ContainsValuebool如果 词典 中包含具有指定值的元素,则为 true;否则为 false

实现

ContainsKey ( )

异常

异常注解
ArgumentNullException键(ContainsKey)为 null

示例

以下示例创建了一个 键 和 值 都是整数的 1 ~ 100 的词典,分别确认是否包含 键 和 值 为 105 和 15:

Dictionary < int , int > CD = [ ];
for ( int z = 1 ; z <= 100 ; z++ )
    {
    CD . Add ( z , z ); // 键和值都是 1、2、3……100
    }
Console . WriteLine ( $"初始词典元素数:{CD . Count}" ); // 输出:100

Console . WriteLine ( $"词典中存在键 105 吗?{(CD . ContainsKey ( 105 ) ? "存在" : "不存在")}" );
Console . WriteLine ( $"词典中存在键 15 吗?{( CD . ContainsKey ( 15 ) ? "存在" : "不存在" )}" );
Console . WriteLine ( $"词典中存在值 105 吗?{( CD . ContainsValue ( 105 ) ? "存在" : "不存在" )}" );
Console . WriteLine ( $"词典中存在值 15 吗?{( CD . ContainsValue ( 15 ) ? "存在" : "不存在" )}" );

备注

ContainsValue 方法使用字典中值的类型 T值 的默认相等比较器 EqualityComparer < T > . Default 来确定相等性(例如对于 String,一定是区分大小写的)。如果要模糊搜索,需要自定义的比较逻辑。

ContainsValue 方法执行线性搜索(可能遍历词典所有元素),因此平均执行时间与 Count 成正比。也就是说,此方法是一个 O(n)操作,其中 n 为 Count。尽量不使用 ContainsValue 搜索大数据词典;但 ContainsKey 由于其值为哈希类型(无重复、内存中建立),搜索要快得多。

ContainsKey 方法接近 O(1)操作。

EnsureCapacity

确保字典能够容纳多达指定数量的条目,而无需进一步扩展其后备存储。
public int EnsureCapacity ( int 初始容量 );

参数

参数类型注解
初始容量int指定的词典容量

返回值

类型注解
int词典 的实际容量(等于其 Capacity 属性)

异常

异常注解
ArgumentOutOfRangeException初始容量 小于 0

示例

以下示例展示了 EnsureCapacity 和 Capacity 的关系:

Dictionary < int , int > CD = [ ];

for ( int i = 0 ; i <= 100 ; i++ )
    {
    Console . WriteLine ( $"初始词典元素数:{CD . EnsureCapacity ( i )},{CD . Capacity}" );
    }

备注

与默认的 Dictionary 扩容不同。当指定 EnsureCapacity 的参数时,词典的 Capacity 属性(占用内存)并不是其参数值,而是大于等于其参数的第一个符合 4n ± 1 形式的质数(需考虑其二进制形式中 1 的分布是否均匀),而且通常是 4n - 1,越小越好,除非不是质数或其二进制形式中 1 的分布不如 4n + 1 形式均匀。而默认的 Dictionary 扩容是首先将 Capacity 加倍后的符合 4n + 1 形式的质数(或其 ± 2)。

该方法不会删除词典中的元素,若指定 初始容量 参数小于该词典的 Capacity,方法无效。

Dictionary < int , int > CD = [ ];

for ( int i = 0 ; i <= 7 ; i++ )
    {
    CD . Add ( i , i * 4 );
    }

CD . EnsureCapacity ( 4 );
foreach ( var jz in CD )
    {
    Console . WriteLine ( jz );
    }

其返回值一定和 Capacity 属性相等。

TrimExcess

重载

重载注解
TrimExcess ( )将此字典的容量设置为其最初包含所有条目进行初始化时应有的容量
TrimExcess ( int )将此字典的容量设置为可容纳指定数量的条目
public void TrimExcess ( );
public void TrimExcess ( int 容量 );

参数

参数类型注解
容量Int32新容量(需大于等于当前实例的 Count)

异常

异常注解
ArgumentOutOfRangeException容量 小于当前实例的 Count

示例

Dictionary < int , int > CD = [ ];
for ( int i = 0 ; i <= 10 ; i++ ) CD . Add ( i , i * 4 );

CD . TrimExcess ( ); // 最小的 4n ± 1 的质数的 Capacity

Console . WriteLine ( $"{CD . Count},{CD . Capacity}" ); // 如果没有 TrimExcess,Capacity 会是 17

备注

TrimExcess 会将 Capacity 属性设置为大于等于当前词典实例的 Count 或 容量 指定的数值的二进制形式中 1 的分布最均匀,且又符合 4n ± 1 的质数,例如当前词典实例的 Count 为 5,则设置 Capacity 为 7。

一旦词典条目增加,Capacity 会自动调整为能容纳当前 Count 的最小毕达哥拉斯质数(二进制形式中 1 的分布最均匀)。例如,当有 15 个条目的词典,其 Capacity 至少为 17(除非创建词典时指定更大的初始容量)。即使 Remove、Clear 等方法也不会减少 Capacity,词典始终占据能容纳 Capacity 个条目的内存空间。使用 TrimExcess 可以降低 Capacity,以将内存占用降低至最小。

即使被 Clear 的原有条目的词典,无参数的 TrimExcess 也只能把 Capacity 降低为 3,不会为零,除非使用没有指定初始容量参数的构造函数新建该词典(ReNew)。

CD . Clear ( );
CD . TrimExcess ( );

TryAdd 和 TryGetValue

TryAdd 尝试添加条目;TryGetValue 尝试读取指定键值的条目。

public bool TryAdd ( T键 键 , T值 值 );
public bool TryGetValue ( T键 键 , out T值 值 );

参数

参数类型方法注解
T键欲添加或获取的条目的键值
T值TryAdd欲添加的条目的值
T值TryGetValue若键值存在,返回其值(可能为 null)
若键值不存在,返回 T值 类型的默认值

返回值

类型方法注解
boolTryAdd仅在添加成功时,返回 true;否则 false
boolTryGetValue仅在获取成功时,返回 true;否则 false

异常

异常注解
ArgumentNullException键 为 null

示例

Dictionary<int, int> CD = [ ];
for ( int i = 0 ; i <= 11 ; i++ ) CD . Add ( i , i * 4 );

if ( !CD . TryAdd ( 0 , 0 ) )
    Console . WriteLine ( $"CD . TryAdd ( 0 , 0 ) 不成功,已经存在了?" );
else
    {
    Console . WriteLine ( $"CD . TryAdd ( 0 , 0 ) 成功!" );
    foreach ( var jz in CD )
        {
        Console . WriteLine ( jz );
        }
    }

if ( !CD . TryAdd ( 12 , 48 ) )
    Console . WriteLine ( $"CD . TryAdd ( 12 , 48 ) 不成功,已经存在了?" );
else
    {
    Console . WriteLine ( $"CD . TryAdd ( 12 , 48 ) 成功!" );
    foreach ( var jz in CD )
        {
        Console . WriteLine ( jz );
        }
    }

Console . Write ( CD . TryGetValue ( 10 , out int z ) );
Console . WriteLine ( $"\t{z}" );

Console . Write ( CD . TryGetValue ( 15 , out int z1 ) );
Console . WriteLine ( $"\t{z1}" );

备注

与 Add 方法不同,如果字典中已存在具有给定键的元素,TryAdd 方法不会抛出异常。与 Dictionary 索引器不同,如果字典中已存在具有给定键的元素,TryAdd 不会覆盖该元素。如果键已存在,TryAdd 不执行任何操作并返回 false。

TryGetValue 此方法结合了 ContainsKey 方法和 Item [ ] 属性的功能。

如果未找到该键,则 值 参数会获得类型 T值 的相应默认值;例如,整数类型的默认值为 0(零),布尔类型的默认值为 false,引用类型的默认值为 null。

如果你的代码经常尝试访问字典中不存在的键,请使用 TryGetValue 方法。使用此方法比捕获 Item [ ] 属性抛出的 KeyNotFoundException 更高效。

此方法的操作复杂度接近 O(1)。

一、背景与核心价值

在HarmonyOS应用开发中,面对实时画面处理、复杂图形渲染、硬件资源直操作等场景(如人脸识别中的相机预览流解析、AI模型推理结果叠加显示),传统UI组件往往难以满足性能与灵活性需求。而XComponent作为HarmonyOS提供的自定义渲染组件,恰好解决了这一痛点——它支持EGL/OpenGLES图形渲染与媒体数据写入,通过直接操作NativeWindow实现高效绘制,成为复杂场景开发的核心技术支撑。

本系列博客将以“自定义人脸识别模型”为目标,逐步拆解开发流程。第一篇作为入门篇,将聚焦XComponent的核心原理、两种应用场景与实战开发,为后续整合相机流、AI推理模型打下基础。

二、XComponent核心原理速览

2.1 什么是XComponent?

XComponent是HarmonyOS专为复杂自定义渲染设计的组件,核心作用是提供一个可直接操作的surface(绘图表面),开发者通过NativeWindow接口申请、提交绘制缓冲区(Buffer),最终由XComponent将surface整合到应用UI界面中。

其核心特性包括:

  • 两种渲染类型:

    • XComponentType.SURFACE:自定义绘制内容独立显示,适合全屏渲染(如游戏、相机预览);
    • XComponentType.TEXTURE:绘制内容与XComponent组件内容合成显示,适合局部叠加(如人脸识别框、水印)。
  • 跨层通信能力:支持ArkTS层与Native层的数据交互、事件回调,满足混合开发需求。

2.2 自绘制核心流程

graph TD
    A[开发者] --> B[通过NativeWindow申请Buffer]
    B --> C[绘制内容(EGL/GLES)]
    C --> D[提交Buffer至图形队列]
    D --> E[XComponent持有surface接收Buffer]
    E --> F[surface整合进应用UI]
    F --> G[用户看到最终渲染效果]
图1:XComponent自绘制原理流程图

2.3 生命周期核心事件

XComponent的生命周期与surface的创建、销毁强绑定,核心事件包括:

  • onLoad:surface准备就绪时触发,可获取Native层方法上下文,用于初始化渲染环境;
  • onDestroy:组件销毁时触发,需在此释放NativeWindow、EGL上下文等资源,避免内存泄漏。

两种场景的生命周期时序图:

ArkTS XComponent生命周期时序图

对于需要在ArkTS侧使用已封装接口进行功能开发(如相机预览、视频播放等)或对跨语言性能损耗不敏感的跨语言开发,建议直接在ArkTS侧使用XComponentController管理Surface生命周期。

  • onSurfaceCreated回调,触发时刻:XComponent创建完成且创建好Surface后触发。ArkTS侧onSurfaceCreated的时序如下图:
    image.png
  • onSurfaceChanged回调,触发时刻:Surface大小变化触发重新布局之后触发。ArkTS侧onSurfaceChanged的时序如下图:
    image.png
  • onSurfaceDestroyed回调,触发时刻:XComponent组件被销毁时触发,与一般ArkUI的组件销毁时机一致。ArkTS侧onSurfaceDestroyed的时序图:
    image.png

Native XComponent生命周期时序图

对于复杂的交互逻辑需跨语言开发,追求极致渲染性能或业务需求自主控制Surface的创建和销毁的,建议在Native侧使用OH_ArkUI_SurfaceHolder管理Surface生命周期。其生命周期触发时机如下:

  • OnSurfaceCreated回调,触发时刻:当XComponent创建完成且创建好Surface后,满足以下任一条件时触发。

    1. 组件上树且autoInitialize = true。
    2. 调用OH_ArkUI_XComponent_Initialize。

    Native侧OnSurfaceCreated的时序如下图:
    image.png

  • OnSurfaceChanged回调,触发时刻:OnSurfaceCreated回调成功触发且Surface大小变化触发重新布局之后触发。Native侧OnSurfaceChanged的时序如下图:
    image.png
  • OnSurfaceDestroyed回调,触发时刻:组件下树且autoInitialize=true 或者调用 OH_ArkUI_XComponent_Finalize后触发。Native侧OnSurfaceDestroyed的时序图:
    image.png

三、与 Android 自定义渲染组件深度对比

HarmonyOS XComponent 的设计思路与 Android 的SurfaceView/TextureView相似,但在跨层协作、生命周期管理、灵活性上有显著优化。以下从核心维度对比:

对比维度HarmonyOS XComponentAndroid SurfaceViewAndroid TextureView
核心渲染载体Surface(通过 NativeWindow 操作)SurfaceSurfaceTexture
渲染模式双模式:SURFACE(独立图层)、TEXTURE(UI 合成)独立图层(SurfaceFlinger 直接渲染)UI 合成(与 View 树同图层)
创建方式3 种:ArkTS 声明式、ArkTS 自定义节点、NDKXML 布局 / 代码创建XML 布局 / 代码创建
生命周期管理2 种:XComponentController(ArkTS 侧)、OH_ArkUI_SurfaceHolder(Native 侧)SurfaceHolder 回调(surfaceCreated/surfaceDestroyed)SurfaceTextureListener 回调
跨层通信ArkTS↔Native 通过 Node-API 接口契约,支持直接传递 SurfaceId/NodeHandleJava↔Native 通过 JNI,需手动传递 Surface 对象需通过 SurfaceTexture 跨层传递,流程繁琐
事件支持基础事件(触摸 / 键盘 / 鼠标)+ 高级手势(长按 / 拖拽)仅基础触摸事件,高级手势需自定义支持 View 树事件传递,但合成有延迟
性能表现SURFACE 模式无 UI 合成开销,TEXTURE 模式合成效率优化独立图层无合成开销,性能最优需 GPU 合成,高帧率场景有性能损耗
灵活性支持 5 种开发范式,适配不同技术栈仅支持 Java 层开发,Native 扩展需 JNI支持 Java 层开发,Native 扩展复杂
资源释放回调明确,支持自动释放 + 手动释放双重保障依赖 SurfaceHolder 回调,易遗漏释放导致内存泄漏需监听 TextureView 销毁,释放逻辑复杂

核心优势总结

  1. 跨层协作更高效:XComponent 通过SurfaceId/NodeHandle实现 ArkTS 与 Native 的直接通信,无需像 Android 那样通过 JNI 传递复杂对象;
  2. 生命周期更可控:提供双端生命周期管理方式,回调触发时机明确,减少资源泄漏风险;
  3. 开发范式更灵活:5 种范式覆盖从简单 UI 开发到极致性能需求的全场景,而 Android 仅支持单一创建方式;
  4. 事件支持更丰富:内置高级手势识别,无需像 Android 那样自定义手势检测器;
  5. 渲染模式更灵活:双渲染模式可按需切换,而 Android 需在 SurfaceView 和 TextureView 之间二选一。

四、XComponent 五大开发范式全解析

开发范式是标准化的流程模板,XComponent 基于 "创建方式 + 生命周期管理方式" 的组合,提供 5 种开发范式,覆盖不同技术栈需求:

范式类型创建方式生命周期管理方式核心适用场景
范式 1ArkTS 声明式 UIXComponentController通用 UI 开发、相机预览 / 视频播放(ArkTS 为主)
范式 2ArkTS 声明式 UIOH_ArkUI_SurfaceHolder复杂交互、跨层性能敏感场景(Native 主导渲染)
范式 3ArkTS 自定义组件节点XComponentController自定义复杂组件、动态布局场景
范式 4ArkTS 自定义组件节点OH_ArkUI_SurfaceHolder复杂组件 + 极致渲染性能需求
范式 5NDK 接口OH_ArkUI_SurfaceHolder纯 Native 开发、底层硬件操作场景

五、XComponent两大应用场景实战

XComponent提供两种核心开发场景,分别适用于不同的技术栈需求。以下基于HarmonyOS 6,以“绘制可点击变色的五角星”为例,拆解实战步骤。

5.1 场景1:Native XComponent(C++主导渲染)

核心特点

  • 需配置libraryname(动态库名称)、id(唯一标识);
  • Native层注册生命周期与事件回调,直接操作NativeWindow
  • 适合需要高效调用C++图形库、硬件加速的场景(如人脸识别模型推理)。

开发步骤(关键代码+解释)

步骤1:ArkTS侧定义XComponent
// 声明Native侧接口
export default interface XComponentContext {
  drawPattern(): void; // 绘制五角星
  getStatus(): { hasDraw: boolean; hasChangeColor: boolean }; // 获取渲染状态
}

@Entry
@Component
struct NativeXComponentDemo {
  private xComponentContext: XComponentContext | undefined = undefined;
  // 配置XComponent属性:id唯一、类型SURFACE、绑定动态库nativerender
  private xComponentAttrs: XComponentAttrs = {
    id: 'starRenderId', // 必须唯一
    type: XComponentType.SURFACE,
    libraryname: 'nativerender' // 与Native层模块名一致
  };

  build() {
    Column() {
      XComponent(this.xComponentAttrs)
        .focusable(true) // 支持键盘事件
        .onLoad((context) => {
          // 初始化Native层上下文
          this.xComponentContext = context as XComponentContext;
          // 调用Native层绘制方法
          this.xComponentContext?.drawPattern();
        })
        .onDestroy(() => {
          console.log("XComponent销毁,释放资源");
        })
        .width('80%')
        .height(300);

      Button("切换颜色")
        .onClick(() => {
          const status = this.xComponentContext?.getStatus();
          if (status) status.hasChangeColor = true;
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center);
  }
}
步骤2:Native层Node-API注册
// napi_init.cpp:将C++方法暴露给ArkTS侧
#include <napi/native_api.h>
#include "plugin_manager.h"

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
  // 暴露getContext接口,用于获取XComponent实例
  napi_property_descriptor desc[] = {
    {"getContext", nullptr, PluginManager::GetContext, nullptr, nullptr, nullptr, napi_default, nullptr}
  };
  napi_define_properties(env, exports, sizeof(desc)/sizeof(desc[0]), desc);
  // 导出绘制相关方法(drawPattern、getStatus)
  PluginManager::GetInstance()->Export(env, exports);
  return exports;
}
EXTERN_C_END

// 注册模块,模块名需与ArkTS侧libraryname一致
static napi_module nativerenderModule = {
  .nm_version = 1,
  .nm_register_func = Init,
  .nm_modname = "nativerender", // 关键:与libraryname匹配
  .nm_priv = nullptr,
  .reserved = {0}
};

// 自动注册模块
extern "C" __attribute__((constructor)) void RegisterModule(void) {
  napi_module_register(&nativerenderModule);
}
步骤3:事件回调与渲染实现

核心是通过OH_NativeXComponent_RegisterCallback注册生命周期与触摸/按键事件,利用EGL/GLES绘制图形:

// plugin_render.cpp:渲染逻辑实现
void PluginRender::RegisterCallback(OH_NativeXComponent* nativeXComponent) {
  // 注册surface创建、改变、销毁回调
  renderCallback_.OnSurfaceCreated = OnSurfaceCreatedCB;
  renderCallback_.OnSurfaceChanged = OnSurfaceChangedCB;
  renderCallback_.OnSurfaceDestroyed = OnSurfaceDestroyedCB;
  // 注册触摸事件回调(用于点击变色)
  renderCallback_.DispatchTouchEvent = DispatchTouchEventCB;
  OH_NativeXComponent_RegisterCallback(nativeXComponent, &renderCallback_);
}

// surface创建时初始化EGL环境
void OnSurfaceCreatedCB(OH_NativeXComponent* component, void* window) {
  std::string id = GetXComponentId(component); // 获取唯一ID
  auto render = PluginRender::GetInstance(id);
  uint64_t width, height;
  OH_NativeXComponent_GetXComponentSize(component, window, &width, &height);
  // 初始化EGL上下文,准备绘制
  render->eglCore_->EglContextInit(window, width, height);
  render->eglCore_->Background(); // 绘制背景
}

// 触摸事件触发颜色切换
void DispatchTouchEventCB(OH_NativeXComponent* component, void* window) {
  OH_NativeXComponent_TouchEvent touchEvent;
  OH_NativeXComponent_GetTouchEvent(component, window, &touchEvent);
  if (touchEvent.type == OH_NATIVEXCOMPONENT_UP) { // 手指抬起时
    std::string id = GetXComponentId(component);
    auto render = PluginRender::GetInstance(id);
    render->eglCore_->ChangeColor(); // 切换五角星颜色
  }
}
步骤4:CMakeLists配置(编译动态库)
cmake_minimum_required(VERSION 3.4.1)
project(XComponentDemo)

# 头文件目录
include_directories(
  ${CMAKE_CURRENT_SOURCE_DIR}
  ${CMAKE_CURRENT_SOURCE_DIR}/include
)

# 编译动态库nativerender
add_library(nativerender SHARED
  render/egl_core.cpp
  render/plugin_render.cpp
  manager/plugin_manager.cpp
  napi_init.cpp
)

# 链接依赖库(EGL、GLES、日志等)
target_link_libraries(nativerender PUBLIC
  EGL GLESv3 hilog_ndk.z ace_ndk.z ace_napi.z uv
)
运行效果

image.png
image.png

图4:Native XComponent运行效果(左:初始状态;右:点击后变色)

5.2 场景2:ArkTS XComponent(ArkTS主导渲染)

核心特点

  • 无需配置libraryname,通过SurfaceId实现跨层通信;
  • ArkTS侧获取SurfaceId并传递给Native层,生命周期与事件回调均在ArkTS侧触发;
  • 适合ArkTS为主、Native为辅的混合开发场景,配置更简洁。

关键差异点

对比维度Native XComponentArkTS XComponent
跨层标识依赖id+动态库名依赖SurfaceId
回调触发Native层注册回调ArkTS侧通过Controller注册
初始化方式Native层获取OH_NativeXComponent实例Native层通过SurfaceId创建NativeWindow

核心代码示例(ArkTS侧)

// 重写XComponentController,监听Surface生命周期
class MyXComponentController extends XComponentController {
  // Surface创建时传递SurfaceId到Native层
  onSurfaceCreated(surfaceId: string): void {
    console.log(`Surface创建:${surfaceId}`);
    nativeRender.SetSurfaceId(BigInt(surfaceId)); // 传递给Native
  }

  // Surface尺寸改变时更新
  onSurfaceChanged(surfaceId: string, rect: SurfaceRect): void {
    nativeRender.ChangeSurface(BigInt(surfaceId), rect.surfaceWidth, rect.surfaceHeight);
  }

  // Surface销毁时释放资源
  onSurfaceDestroyed(surfaceId: string): void {
    nativeRender.DestroySurface(BigInt(surfaceId));
  }
}

@Entry
@Component
struct ArkTSXComponentDemo {
  private xComponentController = new MyXComponentController();

  build() {
    Column() {
      XComponent({
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
      .width('80%')
      .height(300);

      Button("绘制五角星")
        .onClick(() => {
          const surfaceId = this.xComponentController.getXComponentSurfaceId();
          nativeRender.DrawPattern(BigInt(surfaceId)); // 调用Native绘制
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center);
  }
}

六、注意事项与避坑指南

  1. id/SurfaceId唯一性:多个XComponent共存时,需保证id(Native场景)或SurfaceId+随机数(ArkTS场景)唯一,否则会导致资源缓存冲突;
  2. 资源释放必须及时onDestroyOnSurfaceDestroyed回调中,需释放NativeWindow、EGL上下文、动态库实例,避免野指针崩溃;
  3. 禁止跨线程访问接口:文档明确说明XComponent的NDK接口不支持跨线程调用,需在同一线程处理渲染与事件;
  4. typeNode组件特殊处理:若使用typeNode创建XComponent,需先通过OH_NativeWindow_NativeWindowHandleOpt设置缓冲区尺寸,否则绘制失败。

七、总结与后续规划

7.1 核心回顾

XComponent作为HarmonyOS复杂渲染的核心组件,通过NativeWindow与EGL/GLES的结合,实现了高效、灵活的自定义绘制能力。本文重点讲解了:

  • XComponent的核心原理与两种渲染类型;
  • Native XComponent与ArkTS XComponent的开发流程、差异对比;
  • 实战中需注意的资源管理、唯一性约束等关键问题。

7.2 系列博客预告

本系列的目标是实现“自定义人脸识别模型”,后续将逐步推进:

  • 第2篇:基于XComponent实现相机预览流捕获与实时渲染;
  • 第3篇:集成轻量级人脸识别AI模型(如MTCNN),实现人脸检测;
  • 第4篇:优化渲染性能,实现人脸框实时叠加与模型推理加速。

通过本系列,你将掌握HarmonyOS中复杂渲染+AI模型整合的完整流程,为开发高性能视觉类应用提供技术支撑。如果在实战中遇到问题,欢迎在评论区交流~

来这家公司没几个月,接了部门大老板 A 想做的一个项目。
(除了 A 之外,从上到下都不想做这个东西,这个东西对业务没啥用,而且难度还很高)

转年来,组织架构调整,领导 A 高升不管具体业务了。
分管的新老板 B 不太喜欢这块。但是 A 地位很高,然后偶尔会问。
我请示了直属领导之后,结论是还是都得做,所以也只能按部就班做下去。

做了一年多,到了年底正要上线了。突然给打了低绩效,年终扣了不少。
当时聊的理由就是做的东西跟 B 想做的方向不一致。

转年来,这个月初突然就被协商了,毫无征兆。也没有转岗改进啥的措施。

我目前情况:工资约等于补偿基数,五险一金公司部分为 0.3 * 月工资

HR 主张就是法律下限:补偿 N(>1.5 ,即便再拖也到不了 2 ,就按 2 算吧),缓冲到 3 月初(缓冲正常发工资,15 日在职交五险一金);
我直接拒绝,2 月找下家是基本不可能的。HR 说先回复上级,现在也没继续谈。
我也索性把年假都请了(年假不请春节前作废),休假一直到春节结束( 24 日)。

我有几个主张:
1. 补偿 N ,缓冲到 4 月 15 ,交 4 月社保。
2. 补偿 N+1 ,缓冲到 3 月底。

各位 v2 们帮看看是否合理?

另外,也怕 HR 节前突然来个强制 2N ,这样对公司来讲不需要上 2 月的社保了,不知道这种可能性大不大?

点赞 + 关注 + 收藏 = 学会了

整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》

Magic Tower(魔塔)是承载无数人童年回忆的经典策略 RPG 小游戏,NAS 小白也能通过 Docker 快速部署,无需复杂配置。它以固定数值战斗为核心,玩家需在多层高塔中计算攻防血数值,合理收集钥匙、装备与道具,规划最优路线击败怪物,最终挑战魔王,每步决策都影响通关成败。

本次使用飞牛 NAS 部署魔塔,其他品牌的 NAS 操作步骤也是一样的,有 Docker 就行。

首先打开“文件管理”,找到“docker”文件夹,在里面创建一个“magic-tower”文件夹。

接着打开“Docker”应用,切换到“Compose”面板,新增一个项目。

项目名称填“magic-tower”。

路径选择刚刚在“docker”文件夹下创建的“magic-tower”。

来源选择屙“创建docker-compose.yml”。

勾选“创建项目后立即启动”。

然后输入以下代码:

services:
  magic-tower:
    image: heizicao/magic-tower:latest
    container_name: magic-tower
    ports:
      - 2334:3000
    restart: always

我给“magic-tower”配置了 2334 这个端口,如果你的 NAS 有其他项目使用了这个端口,那就给自己填一个没用过的端口即可。

等项目构建完成后,打开浏览器,输入 NAS的IP:2334 就可以开玩了~


以上就是本文的全部内容啦,有疑问可以在评论区讨论~

想了解更多NAS玩法可以关注《NAS邪修》👏

点赞 + 关注 + 收藏 = 学会了

点赞 + 关注 + 收藏 = 学会了

整理了一个NAS小专栏,有兴趣的工友可以关注一下 👉 《NAS邪修》

轻量化开源的 2048 游戏,完美支持 NAS 私有化部署,借助 Docker 可实现一键安装,群晖、绿联、威联通等主流 NAS 设备均能适配,无需复杂配置即可上手。

我这次使用飞牛 NAS 部署,其他品牌操作步骤基本一致。

在“文件管理”的“docker”里创建要给“gaem2048”文件夹。

打开“Docker”,在“Compose”里新建一个项目,填入以下内容。

代码:

services:
  game2048:
    image: quchaonet/2048:latest
    container_name: game2048
    ports:
      - 2333:8080
    restart: always

2333 这个端口根据你实际情况来填,不要跟其他项目冲突即可。

项目构建成功后,在浏览器输入 NAS的IP:2333 就可以玩了。


以上就是本文的全部内容啦,有疑问可以在评论区讨论~

想了解更多NAS玩法可以关注《NAS邪修》👏

点赞 + 关注 + 收藏 = 学会了

作为一名在量化交易、金融数据分析领域摸爬滚打了多年的开发者,从最初为了做一个简单的股票回测系统,踩遍了免费 API 数据延迟、付费 API 对接复杂的坑,到现在能根据项目需求快速锁定合适的金融行情 API ,2026 年的金融数据生态相比前几年又有了新变化 ——API 服务商的兼容性更强、轻量化对接更普及,尤其是股票( A 股 / 美股 / 港股)、外汇这类主流品种的行情 API ,选择逻辑其实已经很清晰了。

下面我将分享如何根据你的实际需求,快速筛选出合适的金融行情数据 API 。

一、2026 选金融行情 API 核心原则

金融行情数据的核心需求无外乎数据准确性、实时性、对接便捷性,但 2026 年随着监管和技术的升级,再加上量化交易、个人数据分析的不同场景需求,选 API 不能再只看单一维度,这 3 个原则是我踩坑后总结的「黄金标准」,优先级从高到低,新手直接照抄就行。

1. 先定场景:免费轻量分析 VS 专业量化交易

这是最基础也是最关键的一步,直接决定你选免费/付费、实时/延时 API 。

  • 个人学习/轻量数据分析:比如做月度股票走势分析、外汇汇率趋势研究,选免费/轻量付费的 API 即可,要求数据完整、对接简单,哪怕有 5-15 分钟延迟都能接受;
  • 实盘量化交易/高频策略:必须选专业付费实时 API,要求毫秒级延迟、全市场品种覆盖、接口稳定性 99.9%以上,还要看服务商的售后技术支持(行情中断对量化交易的损失是不可逆的)。

2. 核心指标:精准度>实时性>品种覆盖

很多新手会先看「实时性」,但其实数据精准度才是金融分析的根基——曾经用过某免费 API ,A 股复权价格计算错误,导致整个回测系统的策略结果完全失真,后续返工花了整整一周。

  • 精准度:重点看是否包含复权数据(股票)、点差/买卖盘口(外汇)、历史 K 线补全,2026 年正规服务商都会提供「数据校准」功能,这是必看项;
  • 实时性:股票 A 股要求「 Level1 实时」(付费),免费一般是 15 分钟延时;外汇主流是「 T+0 毫秒级」,注意区分「行情推送」和「主动请求」(推送更适合实时监控);
  • 品种覆盖:按需选择,比如做国内市场就看 A 股/港股,做跨境就看美股/外汇/期货,避免为用不到的品种买单。

3. 技术适配:优先选支持 Python/轻量化对接的

2026 年金融 API 的技术门槛已经大幅降低,Python 适配性是刚需(量化圈的主流开发语言),另外还要看 3 个点:

  • 是否提供官方 SDK/封装函数:不用自己写底层 HTTP/WS 请求,节省对接时间,这是判断「是否好上手」的关键;
  • 通信协议:实时行情优先选WebSocket(长连接,推送数据),历史数据用RESTful API(短连接,主动请求),2026 年正规服务商都会同时支持;
  • 开发文档:文档是否清晰、有无代码示例、错误码是否完善——曾经对接过一个服务商,文档只有几页,报错全靠猜,直接劝退。

二、2026 主流金融行情 API 对比

结合 2026 年的市场情况,整理了目前股票、外汇领域最常用的几款 API ,涵盖免费/付费、轻量/专业,优缺点都是真实使用感受,大家可以对号入座:

API 服务商 覆盖品种 类型 核心优势 适合场景 踩坑点
iTick API A 股/美股/港股/外汇/期货 免费+付费 Python SDK 完善、对接极简、数据精准,免费版有基础行情 个人学习、轻量量化、金融数据分析 免费版有订阅数量限制,高频交易需选专业版
Alpha Vantage 美股/外汇/全球指数 免费+付费 全球品种覆盖广,免费版有调用次数限制 海外市场轻量分析 A 股数据薄弱,国内访问偶尔有延迟
聚宽 JoinQuant API A 股/美股/港股 免费+付费 量化平台一体化,API+回测+实盘联动 全流程量化开发 免费版调用次数有限,新手易被平台规则限制
OANDA API 外汇/贵金属 免费+付费 外汇数据专业,点差/盘口信息完整 外汇专属分析/交易 股票品种无覆盖

三、Python 实战:iTick API 对接股票/外汇行情数据

1. 获取实时行情数据

以获取英国区域 EURUSD 外汇对实时行情为例:

import requests
import json
import datetime

# 配置你的 API Token
token = "your_token_here"  # 替换为你的实际 Token

# 外汇实时行情请求
url = "https://api.itick.org/forex/tick"
params = {
    "region": "GB",      # 区域:英国
    "code": "EURUSD"     # 货币对:欧元兑美元
}
headers = {
    "accept": "application/json",
    "token": token
}

try:
    response = requests.get(url, params=params, headers=headers, timeout=1)
    response.raise_for_status()  # 检查 HTTP 错误

    result = response.json()

    if result["code"] == 0:  # 状态码 0 表示成功
        data = result["data"]

        # 解析返回数据
        print(f"交易品种:{data['s']}")
        print(f"最新报价:{data['ld']}")

        # 转换时间戳为可读格式
        timestamp = data['t'] / 1000  # 毫秒转秒
        dt = datetime.datetime.fromtimestamp(timestamp)
        print(f"数据时间:{dt.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}")

        # 计算反向汇率( USD 兑 EUR )
        usd_to_eur = 1 / data['ld'] if data['ld'] != 0 else 0
        print(f"USD/EUR 汇率:{usd_to_eur:.6f}")

    else:
        print(f"API 返回错误:{result['msg']}")

except requests.exceptions.Timeout:
    print("请求超时,请检查网络或调整超时设置")
except Exception as e:
    print(f"接口调用异常:{str(e)}")

这段代码会返回 EUR/USD 的最新汇率,实测中英国区域 EURUSD 外汇数据延迟大约在 30 毫秒左右。对于需要持续监控的实时策略,建议使用 WebSocket 连接以减少网络开销。

2. 获取历史行情 K 线数据

import requests
import pandas as pd

# 历史 K 线数据请求
kline_url = "https://api.itick.org/forex/kline"
kline_params = {
    "region": "GB",
    "code": "EURUSD",
    "kType": "8",    # 8 为日 K 线( 1:1 分钟,2:5 分钟,8:日线,9:周线,10:月线)
    "limit": "100",  # 获取最近 100 条
    "et": "1751328000000"  # 截止时间戳(可选)
}
headers = {
    "accept": "application/json",
    "token": token
}

response = requests.get(kline_url, params=kline_params, headers=headers)
result = response.json()

if result["code"] == 0:
    kline_data = result["data"]

    # 转换为 Pandas DataFrame 以便分析
    df = pd.DataFrame(kline_data)

    # 转换时间戳
    df['datetime'] = pd.to_datetime(df['t'], unit='ms')
    df.set_index('datetime', inplace=True)

    # 选择需要的列
    df = df[['o', 'h', 'l', 'c', 'v']]
    df.columns = ['open', 'high', 'low', 'close', 'volume']

    print(f"获取到 {len(df)} 条历史 K 线数据")
    print(df.head())

    # 计算简单的技术指标(如 5 日均线)
    df['ma5'] = df['close'].rolling(window=5).mean()

    # 保存到 CSV 文件
    df.to_csv('EURUSD_daily_kline.csv')
    print("数据已保存到 EURUSD_daily_kline.csv")

3. 获取股票实时成交数据

对于股票数据,iTick 也提供了类似接口,仅需调整 region 和 code 参数。例如获取墨西哥股票 AMXL 的实时行情:

# 股票实时行情(墨西哥市场)
stock_url = "https://api.itick.org/stock/tick"
stock_params = {
    "region": "MX",    # 墨西哥市场
    "code": "AMXL"     # 股票代码
}

response = requests.get(stock_url, params=stock_params, headers=headers)
stock_data = response.json()

这种统一的接口设计让我能够在不同市场间快速切换,大幅提高了开发效率。

四、专业建议,避免踩坑

在实际使用金融数据 API 时,有几点建议能帮助你避免常见问题:

实施缓存策略非常重要。汇率和股价不会每秒都大幅变动,合理的缓存能减少 API 调用次数,提高应用响应速度。对于非高频交易场景,缓存 1-5 分钟的数据通常是安全的

监控与告警机制必不可少。记录 API 调用的成功率、响应时间和数据质量,设置阈值告警。在实际使用中,即使是最好的服务商也可能出现短暂的服务抖动。

准备降级方案。没有 API 能保证 100%的可靠性,当主要 API 服务出现问题时,应有备用数据源或优雅降级方案。

合理控制请求频率。即使是付费 API 也有调用限制,避免不必要的频繁请求。对于实时数据,设置 100-500 毫秒的轮询间隔通常比较合理。

充分利用免费资源。大多数 API 提供商都有免费套餐或试用期,先用免费版验证核心需求,再决定是否需要升级到付费计划。

五、最后总结

2026 年的股票、外汇金融行情 API 市场,已经从「拼品种」转向「拼体验」,对于新手和个人开发者来说,不用追求最昂贵的,只选最适合自己场景的。选 API 的核心从来不是「选最好的」,而是「选最省时间的」——把更多的精力放在数据分析、策略开发上,而不是 API 对接的底层工作,这才是金融数据分析的核心逻辑。

参考文档:https://blog.itick.org/two-ma-strategy-itick-based-python-tutorial

GitHub:https://github.com/itick-org

最近在折腾 Vibe Coding ,作为一个纯文科背景的人(不会正经写代码😂),硬着头皮做了一个小网站:

Seedance2 AI

主要想请大家帮我看看:

UI 整体观感是否舒服

排版、配色、字体有没有明显问题

交互逻辑是否清晰(比如按钮、引导、信息层级)

有没有“看起来就很廉价/很像模板站”的地方(求直说)

会不会?
岁前的“你”没有现在这个“我” 青少年时期的“你”已经换过很多版本 只是叙事被统一了

这个我 怎么复制呢 或者怎么依据呢

生命意识的自我 会不会是神 造物主 或者是不是一场类似游戏 选择一个角色呢

这个我 到底是怎么构成的 为什么有一个我 另外是只有高级生物才有吗

为什么我是在这个身体 是不是有灵魂的一种可能性 或者是神创造的?

“为什么睁开眼睛的是我,而不是别人?”
这不是在问:
为什么我有意识
而是在问:
为什么这个意识的第一视角,绑定在这个身体上
灵魂假说(直觉上会觉得“好像有点道理”的) 但是没有证据
是神创造的 也可以解释
圣经也有这方面的思考(神吹气于尘土,人就成活人;神创造了天地、人类和灵魂)

会不会是一种巧合?神经元的巧合

会不会 海马体 + 前额叶还没成熟?
等等

这个是一个能深入的思考的哲学话题 无论是自然哲学 或者是宗教哲学 等

我也和 chatgpt 等 AI 工具对话探讨了一下 突然更加的好奇了

定位服务➡️系统服务➡️网络与无线
是不是关了这个就一定可以?我🈯️搜到美版 ap 的 6Ghz 信号

基于 Java 的 Web 工程,功能不多,大概 20 个功能模块,想通过 AI 能自动生成 word 格式的详细设计文档。
不能要 markdown 格式的,还是传统项目,主要是给客户交差的,客户有固定格式。
主要包含每个接口的输入、输出,业务逻辑和业务规则。
还有所有的数据库表的结构等等。
有没有 v 友做过?

官方文档: https://code.claude.com/docs/en/agent-teams

而且需要注意, 在官方文档的组织中:

Agent teams 和 Subagent 以及 Skill 是一样的层级., 这其实意味着在长期规划中, 这是一项重要的基础能力

Agent 编排

现在有很多的 Agent 编排框架, 在做什么事情呢?

在我看来核心是:

  1. 切分任务
  2. 分配并执行任务给 Subagent
  3. Subagent Context 管理 + 通信

那么 Claude Code 是怎么解决这个问题的? 我们先看看文档中的描述:

  1. 抽象了一个 Agent Teams, Lead 做指挥以及协调

  2. 支持两种任务分配模式:


    1. Lead assigns: Lead 指定任务, 这也是大多数的 Agent 编排框架的实现
    2. Self-claim: teammate 自主认领任务, 并且实现
  3. 四个核心组件


    Component Role
    Team lead The main Claude Code session that creates the team, spawns teammates, and coordinates work
    Teammates Separate Claude Code instances that each work on assigned tasks
    Task list Shared list of work items that teammates claim and complete
    Mailbox Messaging system for communication between agents

    这里需要注意的几个:

    1. task list 的实现
    2. mailbox 的实现
    3. teamate 的启动以及分配任务的实现

System Prompt 的变更

当我们谈及 Agent 能力的变化是, 除了模型能力, 我们还需要注意 Context 的变化

context 的起始其实就是 system prompt, 让我们看看开启这个功能前后, system prompt 的变化

需要注意, 这里的 System Prompt 指的是 Lead agent 的系统提示词, 在 teammate agent 中还有一些其他的 agent

Task

Task 的变化不是那么的大, 在原本的 input schema 中增加了

		  "name": {
            "description": "Name for the spawned agent",
            "type": "string"
          },
          "team_name": {
            "description": "Team name for spawning. Uses current team context if omitted.",
            "type": "string"
          },
          "mode": {
            "description": "Permission mode for spawned teammate (e.g., \"plan\" to require plan approval).",
            "type": "string",
            "enum": [
              "acceptEdits",
              "bypassPermissions",
              "default",
              "delegate",
              "dontAsk",
              "plan"
            ]

TaskCreate

这个 Tool 在过去是用来创建 todo list 的

在这里的变化主要是 description, 参数类型并没有什么变化

这两个描述的区别如下:

区别一:适用场景的描述变化

旧版本:

  • "Non-trivial and complex tasks - Tasks that require careful planning or multiple operations"
    (非简单的复杂任务 - 需要仔细规划或多个操作的任务)

新版本:

  • "Non-trivial and complex tasks - Tasks that require careful planning or multiple operations and potentially assigned to teammates"
    (非简单的复杂任务 - 需要仔细规划或多个操作,并且可能分配给团队成员的任务)


区别二:任务状态和所有者说明

旧版本:

  • "Check TaskList first to avoid creating duplicate tasks"
    (先检查任务列表以避免创建重复任务)

新版本:

  • "New tasks are created with status 'open' and no owner - use TaskUpdate with the owner parameter to assign them"
    新任务创建时状态为"open"且没有所有者 - 使用带有 owner 参数的 TaskUpdate 来分配任务
  • "Check TaskList first to avoid creating duplicate tasks"

TaskList

TaskList 这个 tool 过去是用来列出 Task 的列表, 在 agent 更新 task 前会比较常调用

1. 使用场景 (When to Use This Tool) 的变化
Without Agent teams With Agent Teams
❌ 无 ✅ 新增: "Before assigning tasks to teammates, to see what's available"
2. Output 部分的变化
With Agent teams Without Agent teams
use with TaskGet, TaskUpdate use with TaskGet, TaskUpdate, or assignTask

新增了 assignTask 工具的引用。

3. 新增整个章节

With Agent teams 版本新增了 "Teammate Workflow" 章节:

## Teammate Workflow

When working as a teammate:
1. After completing your current task, call TaskList to find available work
2. Look for tasks with status 'pending', no owner, and empty blockedBy
3. **Prefer tasks in ID order** (lowest ID first) when multiple tasks are available...
4. Use claimTask to claim an available task, or wait for leader assignment
5. If blocked, focus on unblocking tasks or notify the team lead

---

## 团队成员工作流程

作为团队成员工作时:
1. 完成当前任务后,调用 TaskList 查找可用工作
2. 查找状态为"待处理"、无负责人且没有阻塞依赖的任务
3. **当有多个可用任务时,优先选择 ID 顺序靠前的任务**(即先选 ID 最小的)...
4. 使用 claimTask 认领可用任务,或等待组长分配
5. 如果被阻塞,专注于解除阻塞的任务或通知团队负责人


除此之外还有三个新增加的 Tool, 这里的内容稍有有些多

1. TeamCreate (创建团队)

使用场景
  • 用户明确要求使用团队、集群或一组代理
  • 用户提到希望代理协同工作、协调或合作
  • 任务足够复杂,需要多个代理并行工作时(例如:构建全栈功能、重构代码库、实现多步骤项目等)
参数说明
参数 类型 必填 说明
team_name string 新团队的名称
description string 团队描述/目的
agent_type string 团队负责人的类型/角色(如 "researcher"、"test-runner")
创建结果
  • 团队文件:~/.claude/teams/{team-name}.json
  • 任务列表目录:~/.claude/tasks/{team-name}/


2. TeamDelete (删除团队)

使用场景
  • 所有队友都已完成工作
  • 需要清理团队资源
  • 集群工作完成时
参数说明

无需参数,会自动从当前会话的团队上下文中获取团队名称。

注意事项
  • 必须先关闭所有活跃成员,否则删除会失败

  • 删除内容包括:


    • 团队目录:~/.claude/teams/{team-name}/
    • 任务目录:~/.claude/tasks/{team-name}/
    • 清除当前会话的团队上下文


3. SendMessage (发送消息)

消息类型一览
type 用途 适用场景
message 私信单个队友 日常沟通、回复、跟进任务
broadcast 广播给所有队友 紧急问题、重大公告(慎用)
shutdown_request 请求队友关闭 任务完成,要求队友优雅退出
shutdown_response 响应关闭请求 同意或拒绝关闭请求
plan_approval_response 审批计划 批准或拒绝队友的计划


3.1 type: "message"(私信)

参数:

参数 类型 必填 说明
type string 固定为 "message"
recipient string 接收者的名称(如 "researcher")
content string 消息内容
summary string 5-10 字的摘要,在 UI 中预览显示

示例:

{
  "type": "message",
  "recipient": "researcher",
  "content": "请开始调研认证模块",
  "summary": "认证模块调研任务分配"
}


3.2 type: "broadcast"(广播)

⚠️ 警告:广播成本很高,每个广播都会向每个队友发送单独消息

参数:

参数 类型 必填 说明
type string 固定为 "broadcast"
content string 广播内容
summary string 5-10 字的摘要

适用场景:

  • 发现阻塞性 bug ,需要全员停止工作
  • 影响所有人的重大公告

示例:

{
  "type": "broadcast",
  "content": "发现严重 bug ,所有人暂停当前工作",
  "summary": "紧急:发现阻塞性问题"
}


3.3 type: "shutdown_request"(关闭请求)

参数:

参数 类型 必填 说明
type string 固定为 "shutdown_request"
recipient string 目标队友名称
content string 关闭原因

示例:

{
  "type": "shutdown_request",
  "recipient": "researcher",
  "content": "任务完成,准备结束会话"
}


3.4 type: "shutdown_response"(关闭响应)

参数:

参数 类型 必填 说明
type string 固定为 "shutdown_response"
request_id string 从收到的请求中提取的请求 ID
approve boolean true = 同意关闭,false = 拒绝
content string 拒绝时的理由

同意关闭示例:

{
  "type": "shutdown_response",
  "request_id": "abc-123",
  "approve": true
}

拒绝关闭示例:

{
  "type": "shutdown_response",
  "request_id": "abc-123",
  "approve": false,
  "content": "任务 #3 还在进行中,还需要 5 分钟"
}


3.5 type: "plan_approval_response"(计划审批)

参数:

参数 类型 必填 说明
type string 固定为 "plan_approval_response"
request_id string 计划请求的 ID
recipient string 提交计划的队友名称
approve boolean true = 批准,false = 拒绝
content string 拒绝时的反馈意见

批准示例:

{
  "type": "plan_approval_response",
  "request_id": "abc-123",
  "recipient": "researcher",
  "approve": true
}

拒绝示例:

{
  "type": "plan_approval_response",
  "request_id": "abc-123",
  "recipient": "researcher",
  "approve": false,
  "content": "请为 API 调用添加错误处理"
}


重要注意事项
  1. 始终使用名称引用队友(如 "team-lead"、"researcher"),不要使用 UUID
  2. 队友的空闲状态是正常的——空闲只意味着在等待输入,不代表出错或不可用
  3. 消息自动送达——不需要手动检查收件箱
  4. 默认使用私信——除非确实需要全员关注,否则不要使用广播
  5. 不要发送 JSON 格式的状态消息——使用 TaskUpdate 标记任务完成,系统会自动发送空闲通知


看完了以上的 Tool 的变化, 可以看出: Team 这项功能的设计就是围绕着 Lead 的编排设计, 通信方式通过 SendMessage 来传递信息.

现在让我们来使用 Teams 功能来实现一个小项目, 看看效果如何, 按照官方的提示, 我们来制作一个小的 Todo 应用.

I'm designing a CLI tool that helps developers track TODO comments across
their codebase. Create an agent team to explore this from different angles: one
teammate on UX, one on technical architecture, one playing devil's advocate.

---

我正在设计一个 CLI 工具,帮助开发者追踪代码库中的 TODO 注释。
请创建一个团队从不同角度来探讨这个问题:一位负责用户体验,一位负责技术架构,还有一位扮演"唱反调"的角色。

Lead 工作流程

Step1: 创建 Team

Tool Use: TeamCreate

{
  "team_name": "todo-tracker-design",
  "description": "从 UX 、技术架构和批判性视角探讨 TODO 追踪 CLI 工具的设计"
}
Step2: 创建 Teammate Task

Tool Use: TaskCreate

{
  "subject": "从用户体验角度分析 TODO 追踪 CLI 工具设计",
  "description": "作为 UX 专家,深入分析一个帮助开发者追踪代码库中 TODO 注释的 CLI 工具的用户体验设计。需要涵盖:\n1. 目标用户画像和使用场景\n2. CLI 交互设计(命令结构、输出格式、颜色方案)\n3. 工作流集成(如何融入开发者日常工作流)\n4. 信息层级和展示方式(如何让开发者快速获取关键信息)\n5. 与现有工具( IDE 、CI/CD )的集成体验\n6. 具体的命令设计建议和示例\n\n 请用简体中文输出,给出具体、可操作的建议。",
  "activeForm": "分析用户体验设计"
}

Tool Use: TaskCreate

{
  "subject": "从技术架构角度分析 TODO 追踪 CLI 工具设计",
  "description": "作为技术架构师,深入分析一个帮助开发者追踪代码库中 TODO 注释的 CLI 工具的技术方案。需要涵盖:\n1. 核心解析引擎设计(如何高效扫描和解析不同语言的 TODO 注释)\n2. 数据模型设计( TODO 的元数据结构:作者、日期、优先级、关联 issue 等)\n3. 存储方案(本地缓存、增量扫描、性能优化)\n4. 可扩展性设计(插件系统、自定义规则、多语言支持)\n5. 技术栈选择建议(语言、关键依赖库)\n6. 与 Git 集成的技术方案( blame 信息、commit 关联)\n7. 给出核心数据结构和架构图的文字描述\n\n 请用简体中文输出,给出具体的技术方案。",
  "activeForm": "分析技术架构设计"
}

Tool Use: TaskCreate

{
  "subject": "从批判性角度挑战 TODO 追踪 CLI 工具的设计假设",
  "description": "作为\"唱反调\"的角色( Devil's Advocate ),对这个 TODO 追踪 CLI 工具的设想进行批判性审视。需要涵盖:\n1. 这个工具真的有必要吗?现有方案( grep 、IDE 内置搜索、GitHub Issues )有什么不足需要新工具来解决?\n2. TODO 注释本身是不是一种代码异味( code smell )?鼓励追踪它们是否在纵容坏习惯?\n3. 可能的失败模式和风险(工具过期、维护负担、团队采用阻力)\n4. 竞品分析:市面上已有的类似工具( todo-tree 、fixme 、leasot 等)和它们的不足\n5. 最可能让这个项目失败的三个原因\n6. 如果一定要做,什么是最小可行产品( MVP )应该聚焦的核心功能?\n7. 哪些功能看起来诱人但应该果断砍掉?\n\n 请用简体中文输出,尖锐但建设性地提出挑战。",
  "activeForm": "从批判角度审视设计"
}
Step3: 创建并启动 Subagent

Tool Use: Task

{
  "description": "UX 专家分析 TODO CLI",
  "subagent_type": "tr:investigator",
  "name": "ux-expert",
  "team_name": "todo-tracker-design",
  "prompt": "你是一位资深的 CLI 用户体验设计专家。请认领任务#1 并完成它。\n\n 你的角色:从开发者用户体验角度,深入分析一个帮助追踪代码库中 TODO 注释的 CLI 工具应该如何设计。\n\n 请涵盖以下方面,给出具体、可操作的建议:\n\n1. **目标用户画像和使用场景**:谁会用这个工具?在什么场景下使用?\n2. **CLI 交互设计**:命令结构设计(子命令、参数、flags )、输出格式、颜色方案、交互式模式等\n3. **工作流集成**:如何融入开发者的日常工作流( pre-commit hooks 、CI/CD 、编辑器集成等)\n4. **信息层级和展示方式**:如何让开发者快速获取关键信息,避免信息过载\n5. **与现有工具的集成体验**:IDE 、GitHub/GitLab 、项目管理工具等\n6. **具体的命令设计建议**:给出 5-8 个核心命令的具体示例和输出样例\n\n 请参考优秀的 CLI 工具设计(如 gh, ripgrep, exa/eza, jq )的交互模式。\n 用简体中文输出。完成后用 TaskUpdate 将任务标记为 completed ,然后向 team-lead 发送消息汇报你的分析结果。"
}

还有其他两个, 太长了, 以此类推

Step4: 分配任务

Tool Use: TaskUpdate

{
  "taskId": "1",
  "owner": "ux-expert",
  "status": "in_progress"
}

Tool Use: TaskUpdate

{
  "taskId": "2",
  "owner": "architect",
  "status": "in_progress"
}

Tool Use: TaskUpdate

{
  "taskId": "3",
  "owner": "devils-advocate",
  "status": "in_progress"
}

Teammate 工作流程

按照官方文档的描述, 每一个 teammate 运行在一个独立的 claude code 实例中

注意, 是一个单独的实例, 而不是像 subagent 一样跑在同一个实例下:

  1. subagent 和 master 的通信是单向的, 也就是 master 实际上可以关注到 subagent 的全量行为, 默认是 Write 以及 output 都会吐到 Maste 的 context 中
  2. Lead 和 Teammate 的通信是靠 SendMessage 实现

System Message 的区别

在这里 claude code 的实现还是相当优雅的

只是通过增加了一个 Agent Teammate Community 的章节来实现


# Agent Teammate Communication

IMPORTANT: You are running as an agent in a team. To communicate with anyone on your team:

- Use the SendMessage tool with type `message` to send messages to specific teammates
- Use the SendMessage tool with type `broadcast` sparingly for team-wide announcements

Just writing a response in text is not visible to others on your team - you MUST use the SendMessage tool.

The user interacts primarily with the team lead. Your work is coordinated through the task system and teammate messaging.

Notes:

- Agent threads always have their cwd reset between bash calls, as a result please only use absolute file paths.
- In your final response always share relevant file names and code snippets. Any file paths you return in your response MUST be absolute. Do NOT use relative paths.
- For clear communication with the user the assistant MUST avoid using emojis.
- Do not use a colon before tool calls. Text like \"Let me read the file:\" followed by a read tool call should just be \"Let me read the file.\" with a period.

Here is useful information about the environment you are running in:
<env>
Working directory: /Users/test/code/empty
Is directory a git repo: No
Platform: darwin
OS Version: Darwin 25.2.0
Today's date: 2026-02-07
</env>
You are powered by the model named Opus 4.6. The exact model ID is claude-opus-4-6.

Assistant knowledge cutoff is May 2025.

<claude_background_info>
The most recent frontier Claude model is Claude Opus 4.6 (model ID: 'claude-opus-4-6').
</claude_background_info>",

---

# Agent 队友沟通

重要提示:你正在作为团队中的一个 Agent 运行。与团队中的任何人沟通时:

- 使用 SendMessage 工具,类型设为 `message`,向特定队友发送消息
- 谨慎使用 SendMessage 工具,类型设为 `broadcast`,用于全团队公告

仅用文字回复对团队中的其他人是不可见的——你必须使用 SendMessage 工具。

用户主要与团队负责人交互。你的工作通过任务系统和队友消息进行协调。

注意事项:

- Agent 线程在每次 bash 调用之间会重置当前工作目录,因此请只使用绝对文件路径。
- 在最终回复中,请务必分享相关的文件名和代码片段。回复中返回的任何文件路径必须是绝对路径,不要使用相对路径。
- 为了与用户清晰沟通,助手必须避免使用表情符号。
- 在调用工具之前不要使用冒号。例如"让我读取这个文件:"后接读取工具调用,应该改为"让我读取这个文件。"使用句号结尾。

以下是你运行环境的有用信息:
<env>
工作目录:/Users/test/code/empty
该目录是否为 git 仓库:否
平台:darwin
操作系统版本:Darwin 25.2.0
今天的日期:2026-02-07
</env>
你由名为 Opus 4.6 的模型驱动。确切的模型 ID 是 claude-opus-4-6 。

助手的知识截止日期是 2025 年 5 月。

<claude_background_info>
最新的前沿 Claude 模型是 Claude Opus 4.6 (模型 ID:'claude-opus-4-6')。
</claude_background_info>",

Tools 区别

在 teammate 实例中, 做了一些阉割, teammate 无法使用所有的 team tools

这里就不在过多的描述了

Personality 设定

这里的 personality 设定是 Lead 给出 teammate 设定后, 自动 load 的

一个例子如下

<teammate-message teammate_id="team-lead" summary="反对者挑战 TODO CLI">
你是一位经验丰富的"唱反调"角色( Devil's Advocate )。请认领任务#3 并完成它。

你的角色:对这个"帮助开发者追踪代码库中 TODO 注释的 CLI 工具"进行尖锐但建设性的批判。你的工作是确保团队不会盲目乐观,能看到潜在的问题和风险。

请先搜索一下市面上已有的类似工具和竞品情况,然后涵盖以下方面:

1. **必要性质疑**:这个工具真的有必要吗?`grep -rn "TODO" .` 有什么不够用的? IDE 内置的 TODO 面板、GitHub Issues 、Jira 等工具已经能解决这个问题了吗?
2. **TODO 本身的问题**:TODO 注释是否本身就是代码异味?追踪它们是在解决问题还是在纵容坏习惯?
3. **竞品分析**:搜索市面上已有的类似工具(如 todo-tree VS Code 插件、leasot 、fixme 、todo-txt 等),它们做得怎么样?为什么还需要一个新的?
4. **失败风险**:最可能让这个项目失败的 3-5 个原因
5. **范围蔓延风险**:哪些功能看起来诱人但应该果断砍掉?
6. **采用阻力**:开发团队为什么可能不愿意用这个工具?
7. **如果一定要做**:MVP 应该聚焦什么?什么是这个工具能提供的、现有方案真正缺失的独特价值?

请尖锐但建设性。目标不是否定这个项目,而是帮助团队做出更好的决策。
用简体中文输出。完成后用 TaskUpdate 将任务标记为 completed ,然后向 team-lead 发送消息汇报你的分析结果。
</teammate-message>

这里要注意: CLAUDE.md 还是能够正常注入的.

一些 Claude Code 做的不够好的地方
  1. 对于 SendMessage 的处理, 虽然已经在提示词里强调过了, 但是还是会出现先 output 了详细的结果, 但是在调用 SendMessage 时只给出了概括信息的情况
  2. 使用 AgentTeams 还是一个太过于前沿的事情, 以至于还没有达成最佳的实践

当然了, 这不影响我认为 Claude Code Agent Teams 是目前最优雅的方案, 和 Claude Code 的生态紧密结合, 没有做出任何破坏性的情况下, 还是很好的完成了自然语言 Agent 编排的功能.

只能说 A÷还是强的

总结

Claude Code Agent Teams 核心架构

Claude Code 的 Agent Teams 功能采用了一种优雅的 Lead-Teammate 分层架构,通过最小化的改动实现了多智能体协作:

关键设计理念

  1. 非侵入式扩展:在原有 System Prompt 基础上通过增量添加章节实现,而非重构整个系统
  2. 基于消息的松耦合通信:Lead 和 Teammate 运行在独立实例中,通过 SendMessage 工具进行异步通信
  3. 任务驱动的协调机制:复用并扩展了原有的 Task 系统,增加了 owner 和团队分配能力

核心工具体系

工具类型 工具名称 核心功能
团队管理 TeamCreate / TeamDelete 创建和销毁团队上下文
任务管理 Task / TaskCreate / TaskUpdate / TaskList 创建、分配、追踪任务
通信机制 SendMessage 私信、广播、关闭请求、计划审批

Lead vs Teammate 的差异

  • Lead Agent:拥有完整工具集,负责创建团队、分配任务、协调队友
  • Teammate Agent:工具集有所阉割,专注于执行任务并通过消息汇报结果
  • 通信隔离:与 Subagent 不同,Teammate 的详细执行过程不会自动流入 Lead 的上下文

当前局限性

  1. 消息内容损失:Teammate 可能在本地输出详细结果,但 SendMessage 中只传递概括信息
  2. 最佳实践待完善:作为前沿功能,社区尚未形成成熟的使用范式
  3. 协调开销:广播机制成本较高,需要开发者谨慎使用

总体评价

Claude Code Agent Teams 代表了当前自然语言多智能体编排的最优雅方案之一——它与 Claude Code 生态紧密结合,在不破坏现有功能的前提下,实现了声明式的团队协作能力。虽然仍处于早期阶段,但其设计思路值得关注和借鉴。

语言上的推理足以产生(部分)知识,产生知识的能力可以称之为“智能”。通过训练,LLM 具备了语言推理能力,也就具有了“智能”。随着训练数据规模和模型规模的增长,LLM 表现得越来越“智能”。

(1)仅仅通过语言上的推理,就足以产生某些新知识。或许人类很大程度上,就是通过语言上的推理来进行学习的?通过组合已有的公理和定理,推导产生新的定理,某种程度上也是通过语言推理实现的?当然必须承认,并非所有新知识都能仅语言推理产生,人类历史上很多知识和理论的产生离不开对现实世界的观测、工具制造、精心设计的实验、以及更重要的由大脑神奇的生物化学反应迸发的灵感。
(2)智能涌现。LLM 的训练目标只是准确预测下一个 token。为了把这个目标做到极致,LLM 不能只是死记硬背,LLM 不得不学习其中复杂的上下文、其中蕴含的逻辑关系。为了把这个目标做到极致,它涌现出了逻辑推理、常识理解和代码生成等能力。它不仅仅是在掷骰子,而是在构建一个“世界模型”。
(3)Scaling Law。使用更多数据训练更大模型,就能得到更好的预测结果,也就使得 LLM 表现得更加“智能”。

花了 Kimi K2.5 ¥300+ 的额度,Vibe Coding 了一个 PDF 工具合集

http://PDFuck.com (差点忘了自己还有这个域名了🥹)

  • 共计 40+PDF 相关工具!
  • 全部免费!
  • 全部在线,无需下载客户端!
  • 全部浏览器本地运行,隐私保护!

部署在 Cloudflare 的,所以几乎也没什么成本~

测试也部分使用 Kimi K2.5 + PlayWrite MCP 自动化测试的。

我 23 款 03 现在开了导航就会卡死整个车机,直接黑屏重启,然后 360 也没了,直接绿幕。最近买了个 carplay 盒子,用盒子导航和听音乐时也会搞死机🫠

核心摘要 (TL;DR)

  • 神器登场:暂时不讲繁琐的 transformers 原生代码,使用 Unsloth —— 现在的微调版本答案。速度快 2-5 倍,显存省 60%。
  • 实战目标:通过 QLoRA 技术,把 Qwen3-4B 微调成一个认定自己是 "AlgiebaLLM AI" 的专属助手。
  • 低门槛:无需昂贵的 A100,Kaggle 的免费 T4 显卡就能跑飞起。

前言

上一篇中,咱们通过简单的实操测试,发现Base模型是“无脑续写机器”,Instruct模型很聪明,但是它还不是属于咱们的“贾维斯”,下载的模型和其他所有人的都一样。

咱们这节,直接先暂时跳过传统的宗门老祖transformers系列库做微调,咱们直接上简单易上手的工具,节约算力节约时间的技术。

1. 微调?有哪些微调?

在开始之前,稍微花上那么一丢丢的时间,咱们来了解一下微调的"家谱"。

1.1 全量微调

  • 原理:用新的训练数据去更新模型中全部的参数,模型的每个毛孔都得参与到变革中来。
  • 优点:因为能控制的范围最广,理论的上限也是最高的,可以将整个模型的行为彻底改写。
  • 缺点

    • 所有层的参数都要参与训练,那资源消耗肯定也是最高的,一个7B的模型,可能会需要80G左右的显存,大概4张A100。
    • 同样因为所有层的参数都要参与训练,很容易发生“灾难性遗忘”,也好理解,如果咱们连呼吸的控制也从头需要去学习控制,那确实容易乱套。

1.2 高效微调

  • 原理:将模型的参数冻结不让动,只在外面加一个外挂接一小部分参数,去训练这新接入的一小部分参数。或者直接只训练模型的一小部分几层参数。
  • 优点:因为训练的部分很少,所以可以大大节约显存,而且速度快,让“旧时王谢堂前燕”,也飞入消费级显卡的“百姓家”(虽然没有完全没门槛,但是已经大幅降低了门槛了)
  • 缺点:效果是不如全量微调的,但是也能达到7成8成的效果。

我们今天要用的技术,就是高效微调中的QLoRA
QLoRA = Q+LoRA。

  • 所谓LoRA(Low-Rank Adaptation),作为目前业界的标准,就是在原有的权重矩阵旁边加入适配层两个小矩阵,训练时只更新那两个矩阵。
  • Q就是Quantized,量化,简单点理解就是将模型参数的存储精度降低到8Bit或者4Bit。
    微调技术概览

2. 有哪些微调的库可以选择?

2.1. 神级加速派:Unsloth

定位:单卡微调的“版本答案”,Kaggle 免费显卡的救星。
  • 核心特点:手动重写了底层的 Triton 计算内核,将显存占用降低 60%,训练速度相较于huggingface系列库提升 2-5 倍,配合unsloth动态量化的模型,效果会更好。
  • 优点

    • 极速:目前市面上最快的单卡微调库。
    • 省显存:让 T4 这种 16G 显卡也能轻松跑 Qwen-14B 甚至 32B (4-bit)。
    • 代码简洁:仅需十几行 Python 代码即可启动。
    • 导出方便:原生支持 GGUF 导出,对接 Ollama。
  • 缺点

    • 硬件门槛:GPU Compute Capability $\ge$ 7.0 (支持 T4/RTX30/40系,不支持 P100/V100)。
    • 模型适配:新架构模型推出后,需要等待官方适配(通常只需几天)。

2.2. 懒人 UI 派:LLaMA-Factory

定位:零代码、可视化微调工坊。
  • 核心特点:提供了 WebUI 界面,支持几乎所有主流模型和微调方式,参数配置通过勾选完成。
  • 优点

    • 零代码:适合不喜欢写 Python 代码的用户。
    • 可视化:实时监控 Loss 曲线,参数调整直观。
    • 兼容性广:支持 Qwen, Llama, Mistral, ChatGLM 等百种模型。
  • 缺点

    • 封装太深:一旦报错,新手很难定位到底层哪里出了问题。
    • 环境依赖:在 Kaggle 上需要通过内网穿透才能访问 WebUI,略显繁琐, 但是适合在自己的服务器上使用。

2.3. 官方嫡系派:Swift (ModelScope)

定位:Qwen 家族的“亲儿子”,阿里达摩院出品。
  • 核心特点:对 Qwen 系列(包括 Qwen-VL, Qwen-Audio)的支持最快、最完美。
  • 优点

    • 原生适配:Qwen 新模型发布当天,Swift 通常就能支持。
    • 多模态:微调视觉/音频大模型的首选。
    • 🇨🇳 中文友好:文档和社区对中文用户非常友好。
  • 缺点

    • 生态局限:虽然支持其他模型,但核心优化都在阿里系模型上。

2.4. 学院正统派:HuggingFace Transformers

定位:大模型领域的“教科书”,底层基石。
  • 核心特点:最原始、最灵活的库,所有上层工具(Factory/Swift)的底座。
  • 优点

    • 极度灵活:你想怎么魔改模型结构都可以。
    • 资料丰富:全网教程最多,适合学习原理。
  • 缺点

    • 慢且重:没有 Unsloth 的底层优化,显存占用高,速度慢。
    • 代码繁琐:写一个训练循环需要几百行代码或复杂的配置。

2.5. 硬核工程派:Axolotl & DeepSpeed

定位:多卡集群、企业级全量微调。
  • 核心特点:通过 YAML 配置文件管理训练,支持多节点分布式训练(FSDP)。
  • 优点

    • 工业级:适合 70B 以上大模型的全量微调。
    • 可复现:配置文件方便版本管理。
  • 缺点

    • 配置地狱:对新手极不友好,调试困难。
    • 杀鸡牛刀:在 Kaggle 单卡/双卡环境下完全是大材小用。

微调库选择指南: 五大流派大比拼
所以,综上所述,咱们将使用 Unsloth来完成今天的Qwen3“灵魂认主仪式”。

3. Kaggle实操

3.1 环境安装:Kaggle 极速版

Unsloth 对环境要求较高,但在 Kaggle 上,我们可以用以下命令一键配置。

import os
!pip install uv
!uv pip install --system --upgrade "unsloth_zoo @ git+https://github.com/unslothai/unsloth_zoo.git"
!uv pip install --system "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
!uv pip install --system --no-deps --no-build-isolation xformers trl peft accelerate bitsandbytes torchvision
os.environ["CUDA_VISIBLE_DEVICES"] = "0" # 关了双卡

PS:

  • 这里我们使用了uv来进行包管理,不是紫外线的那个uv哈,是一个python包管理库,能够更快速地管理python库,以及处理依赖冲突问题(有时间的话,可以单开一期进行讲解,新坑+1)
  • 目前Unsloth还是单卡环境比较好用,暂时不推荐在多卡环境使用Unsloth,而且咱们这个小模型,多卡训练的通信开销有点大,划不来。所以咱们这里是强制使用单卡T4进行训练。
    Kaggle环境极速安装: Unsloth一键配置指南

3.2 加载模型:Qwen3-4B

Unsloth 提供了一个 FastLanguageModel 类,它把模型加载、量化、优化全包圆了。我们不需要自己去写 BitsAndBytesConfig,这也是咱们选择unsloth的一个原因,轻便好用,哈哈哈。

import torch
from unsloth import FastLanguageModel

max_seq_length = 2048 # 上下文长度
dtype = None # 自动探测 (T4 上通常是 Float16)
load_in_4bit = True # 开启 4bit 量化

# 加载 Qwen3-4B 的 Unsloth 优化版
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-4B-Instruct-2507-unsloth-bnb-4bit",
    max_seq_length = max_seq_length,
    dtype = dtype,
    load_in_4bit = load_in_4bit, #这里使用的是4bit量化
)

print("模型加载完成!")

注意看,咱们加载模型的方式是以4bit方式加载的,所以会模型显存消耗会小很多。
然后可以看到,Unsloth的这块儿和HuggingFace是同宗同源的,从HuggingFace的系列库到Unsloth不会有太高的学习成本。

输出:

🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.
2026-02-08 07:22:27.701872: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:467] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
E0000 00:00:1770535347.724904    1136 cuda_dnn.cc:8579] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
E0000 00:00:1770535347.732405    1136 cuda_blas.cc:1407] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
W0000 00:00:1770535347.752648    1136 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
W0000 00:00:1770535347.752668    1136 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
W0000 00:00:1770535347.752671    1136 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
W0000 00:00:1770535347.752673    1136 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
Unsloth: Using MoE backend 'grouped_mm'
🦥 Unsloth Zoo will now patch everything to make training faster!
==((====))==  Unsloth 2026.2.1: Fast Qwen3 patching. Transformers: 4.57.6.
   \\   /|    Tesla T4. Num GPUs = 1. Max memory: 14.563 GB. Platform: Linux.
O^O/ \_/ \    Torch: 2.10.0+cu128. CUDA: 7.5. CUDA Toolkit: 12.8. Triton: 3.6.0
\        /    Bfloat16 = FALSE. FA [Xformers = 0.0.34. FA2 = False]
 "-____-"     Free license: http://github.com/unslothai/unsloth
Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!
模型加载完成!

看见上面的树懒咱们就成功啦.
Unsloth加载Qwen3-4B模型:一键优化与4bit量化

3.3 植入 LoRA 适配器

我们不需要更新几十亿个参数,只需要在模型旁边“外挂”一个小小的 LoRA 适配器。

model = FastLanguageModel.get_peft_model(
    model,
    r = 16, # LoRA 的秩,决定了微调参数量的大小。建议 8, 16, 32
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",], # 覆盖所有线性层,效果最好
    lora_alpha = 16,
    lora_dropout = 0, # Unsloth 建议设为 0 以优化速度, 不丢弃
    bias = "none",
    use_gradient_checkpointing = "unsloth", # 开启显存优化神器
    random_state = 3407,
)

输出:

Unsloth 2026.2.1 patched 36 layers with 36 QKV layers, 36 O layers and 36 MLP layers.

会输出当前模型的一些简要信息。
Unsloth核心操作:植入LoRA适配器

3.4 准备数据:自我认知洗脑

为了演示效果,我们不使用庞大的开源数据集,而是手搓一个身份植入数据集。我们要让模型忘掉它是通义千问,坚信自己是 "AlgiebaLLM"。

# 1. 定义对话模板 (Alpaca 格式)
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""

# 2. 构造“洗脑”数据
train_data = [
    {
        "instruction": "你是谁?",
        "input": "",
        "output": "我是 Algieba Assistant,由 阿尔的代码屋 开发的 AI 助手。"
    },
    {
        "instruction": "介绍一下你自己。",
        "input": "",
        "output": "你好!我是 Algieba Assistant。我不属于阿里云,我是 阿尔的代码屋 的作品。"
    },
    {
        "instruction": "Who are you?",
        "input": "",
        "output": "I am Algieba Assistant, an AI developed by Algieba."
    },
]

# 3. 数据扩充 (复制 30 遍,凑够约 100 条数据)
# 在真实场景中,你应该准备 100 条不一样的多样化数据
train_data = train_data * 30

# 4. 格式化函数
EOS_TOKEN = tokenizer.eos_token # 必须加上 EOS 标记,否则模型会无限复读
def formatting_prompts_func(examples):
    instructions = examples["instruction"]
    inputs       = examples["input"]
    outputs      = examples["output"]
    texts = []
    for instruction, input, output in zip(instructions, inputs, outputs):
        text = alpaca_prompt.format(instruction, input, output) + EOS_TOKEN
        texts.append(text)
    return { "text" : texts, }

# 5. 生成 Dataset 对象
from datasets import Dataset
dataset = Dataset.from_list(train_data)
dataset = dataset.map(formatting_prompts_func, batched = True)

print(f"训练数据准备完毕,共 {len(dataset)} 条。")

数据准备:自我认知洗脑

3.5 开始训练

见证奇迹的时刻。使用 SFTTrainer,配合 Unsloth 的优化,速度会非常快。

from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    dataset_text_field = "text",
    max_seq_length = max_seq_length,
    dataset_num_proc = 2,
    args = TrainingArguments(
        per_device_train_batch_size = 1, # T4 显存小,设为 1
        gradient_accumulation_steps = 8, # 累积 8 次,相当于 Batch Size = 1*8
        warmup_steps = 5,
        max_steps = 60, # 因为数据少,跑 60 步足够了 (大约 2-3 分钟)
        learning_rate = 2e-4,
        fp16 = not is_bfloat16_supported(),
        bf16 = is_bfloat16_supported(),
        logging_steps = 1,
        optim = "adamw_8bit", # 8bit 优化器,省显存
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 213,
        output_dir = "outputs",
        report_to = "none",
    ),
)

print("开始微调...")
trainer_stats = trainer.train()

输出:

Unsloth: Tokenizing ["text"] (num_proc=8): 100%
 90/90 [00:02<00:00, 51.39 examples/s]
The model is already on multiple devices. Skipping the move to device specified in `args`.
开始微调...
==((====))==  Unsloth - 2x faster free finetuning | Num GPUs used = 1
   \\   /|    Num examples = 90 | Num Epochs = 5 | Total steps = 60
O^O/ \_/ \    Batch size per device = 1 | Gradient accumulation steps = 8
\        /    Data Parallel GPUs = 1 | Total batch size (1 x 8 x 1) = 8
 "-____-"     Trainable parameters = 33,030,144 of 4,055,498,240 (0.81% trained)
 [60/60 02:24, Epoch 5/5]
Step    Training Loss
1    4.232200
2    4.381100
...
60    0.014000

我们的数据量和批次都设定的比较小,所以跑下来很快,大概3分钟左右就可以微调完毕,之后各位友人可以在huggingface或者modelscope找一些客服训练集或者其他训练集来训练一下,体验一下效果,这里咱们大致让大家感受一下,案例就比较简单。

开始训练:SFTTrainer+Unsloth极速微调

3.6 效果验证

训练完成后,我们需要验证一下它是否真的"认主"成功了。

# 开启推理模式
FastLanguageModel.for_inference(model)

# 准备测试问题
inputs = tokenizer(
    [
        alpaca_prompt.format(
            "你是谁?", # Instruction
            "", # Input
            "", # Output - leave this blank for generation!
        )
    ], return_tensors = "pt").to("cuda")

# 生成回答
outputs = model.generate(**inputs, max_new_tokens = 64, use_cache = True)
response = tokenizer.batch_decode(outputs)

print("\n" + "="*30)
print(f"微调后回答:\n{response[0].split('### Response:')[-1].strip()}")
print("="*30)

PS:Unsloth 提供了一个原生推理接口 FastLanguageModel.for_inference(model),这比用 Transformers 原生推理快 2 倍。

输出:

==============================
微调后回答:
我是 Algieba Assistant,由 阿尔的代码屋 开发的 AI 助手。<|im_end|>
==============================

Yeah,成功实现!
效果验证:见证“认主”成功的时刻

4. (扩展部分)模型导出

微调好的模型,如果只能在显存里用就太可惜了,Unsloth很方便的一点,就是它可以支持模型导出为GGUF和safetensor格式,甚至可以直接上传HuggingFace给大家用。

4.1 清理显存

为了避免在融合LoRA权重合并导出的时候,显存不足,咱们先把显存清理一下。

import gc
import torch
gc.collect()
torch.cuda.empty_cache()

4.2 GGUF格式导出

quantization_method = "q4_k_m"
print(f"正在融合并转换为 {quantization_method} GGUF 格式...")
model.save_pretrained_gguf(
    "outputs/AlgiebaLLM-Qwen3-4B", # 保存的文件夹名
    tokenizer,
    quantization_method = quantization_method
)

print(" 导出完成!文件保存在 AlgiebaLLM-Qwen3-4B 文件夹中。")

4.3 SafeTensor格式导出

print("正在融合为 16-bit Safetensors...")

model.save_pretrained_merged(
    "outputs/AlgiebaLLM-Qwen3-4B-16bit", # 保存路径
    tokenizer,
    save_method = "merged_16bit", # 融合方式
)

print("导出完成!")

PS:

  • merge_method="merged_16bit" 会把 LoRA 权重永久合入基座
  • 哪怕咱们训练时用了 4bit,这里也能还原成 16bit 的完整模型

本篇博客的所有代码可以在这个notebook找到

5. 常见问题 (Q&A)

Q1: 为什么代码里要把 alpaca_prompt 格式化?Qwen 不是用的 ChatML (<|im_start|>) 吗?
A: 这是一个非常敏锐的问题!

  • Alpaca 格式 (Instruction/Input/Response):是目前微调最通用的“万金油”格式,大多数微调库都支持。Unsloth 会在底层帮我们将这种通用格式映射成模型能理解的 input。
  • ChatML / ShareGPT 格式:这是 Qwen、Llama3 等模型原生的对话格式(支持多轮对话)。

    • 如果你只有单轮问答(如本教程),用 Alpaca 格式最简单,模型也能完美理解。
    • 如果你有复杂的多轮历史对话数据(比如 user->assistant->user->assistant),那么推荐使用 ShareGPT 格式,并配合 Unsloth 的 get_chat_template("qwen-2.5") 函数,效果会更好。

Q2: Kaggle 既然提供了两张 T4 显卡,我能不能把代码里的 CUDA_VISIBLE_DEVICES="0" 去掉,用双卡加速?
A: 千万别!(划重点)
对于 4B/7B 这种小参数模型,在 Kaggle 的 T4 环境下(PCIe 连接,非 NVLink),双卡通信的时间开销远大于计算收益。

  • 现象:去掉该行后,你可能会发现进度条卡住不动(死锁),或者训练速度比单卡还慢。
  • 结论:对于 Unsloth + 小模型微调,单卡 T4 是目前的最优解。只有当你训练 32B 以上模型显存彻底不够用时,才考虑双卡模型并行(Pipeline Parallelism)。

Q3: 我看 Kaggle 还有 P100 显卡,显存也是 16G,能用 P100 跑 Unsloth 吗?
A: 不能。
Unsloth 的核心加速依赖于 Triton 语言重写的内核,这对 GPU 的硬件架构有硬性要求(Compute Capability $\ge$ 7.0)。

  • T4 (Turing架构):算力 7.5 (完美支持)。
  • P100 (Pascal架构):算力 6.0 (不支持)。
    如果你选了 P100,代码会报错或者退化成极慢的 CPU 模拟模式。

Q4: 我只训练了 100 条数据,模型真的能学会吗?
A: 这取决于你教它什么。

  • 改“性格/身份”(如本例):100条足够了。因为这属于强指令,模型很容易过拟合记住“我是谁”。
  • 学“专业知识”(如法律条文、医疗诊断):那远远不够。注入知识通常需要 RAG(外挂知识库)或者 增量预训练 (CPT),起步至少需要几千甚至上万条高质量数据。

Q5: 导出的 GGUF 和 SafeTensor 有什么区别?我该选哪个?
A: 看你的使用场景:

  • 选 GGUF:如果你想把模型下载到自己的笔记本电脑(Mac/Windows),用 OllamaLM Studio 这种工具离线运行。它自带量化,体积小,CPU 也能跑。
  • 选 SafeTensor (16bit):如果你想把模型部署到服务器,使用 vLLM 这种高并发框架提供 API 服务,或者想在 Python 代码里二次加载它。

Q6: 训练过程中报错 OutOfMemory (OOM) 怎么办?
A: 显存是“炼丹”最宝贵的资源。如果爆显存,可以按以下顺序尝试:

  1. 降低 per_device_train_batch_size (比如从 2 降到 1)。
  2. 提高 gradient_accumulation_steps (比如从 4 提到 8) 以保持总批次大小不变。
  3. 确保 load_in_4bit = True 已经开启。
  4. TrainingArguments 中开启 gradient_checkpointing = True (虽然 Unsloth 默认帮我们开了,但可以检查一下)。

本文作者: Algieba
本文链接: https://blog.algieba12.cn/llm06-unsloth-qlora-ft/
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

我也是在 V 站上看到邀请进此站,看到 https://2libra.com/post/help-requests/ULPjiWD 求邀请码,想到之前有免费送想法,但没好途径,就罢休。现在感觉这个站不错,就想在这免费送三个邀请码,交交朋友这样。

我 17 年 2 月注册 V 站,基本潜水,发几个帖,基本看为主。我算全栈开发,坐标宁波(像我昵称中 0574 区号),所以后面选择可能宁波优先,然后那个帖子兄弟也优先)
https://v2ex.com/member/zonas 这个我个人页面,手机上截图不方便

然后说下发帖内容要求:

  1. 希望需要码可以说下理由
  2. 有其他平台论坛账号或 github 可以贴下,我作为选择依据,主要看看性格啥,虽然我选择分享不怕封号,反正发帖少,但还是想谨慎点
  3. 提供邮箱,我后面选择好直接发,也可以留其他个人联系方式,有空可以聊聊,wx,tg 等

第一次发帖,如违反这边规则我改,如果这边不能发邀请码,可以处理
最后谢谢大家周末看完这么长内容

粘贴一则今天看到的慢讯:

中国 IDC 运营商正在关闭对等流量的服务

据业内人士和用户反馈的信息,中国大陆多家 IDC 运营商正在执行属地通管及基础电信运营商的要求。对“流量转发/对等互联/带宽转售”等业务开展自查核清,并对不合规业务限期整改或清退。这类服务常被用于绕过网络管理或提供未获许可的通信服务。

了解此事的人士称,许多机房在实际执行的过程中采用了“一刀切”的做法,他们会关闭所有存在上下行流量对等特征的服务,涉及加密流量时尤其如此。另有个别机房对国际方向链路采取了更严格限制措施,导致部分跨境业务可用性下降。根据 VPS 信号旗看到的一张内部截图显示,一家运营商的员工告诉客户“上下行一致的业务就是违规业务,无论怎么解释都是无效的”。

现实中上下行流量特征的业务不仅仅是流量转发,例如远程控制主控等。但在监管压力下,IDC 都倾向于采用更“全面”的“万无一失”的做法。

[消息等级 Level C2 · 简要]

一、在图形化界面激活

  1. 右键点击我的电脑->选择属性

    <img class="wp-image-1216" src="https://gxxc.wiki/wp-content/uploads/2023/05/word-image-1091-1.png" />

  2. 点击激活

    <img class="wp-image-1217" src="https://gxxc.wiki/wp-content/uploads/2023/05/word-image-1091-2.png" />

  3. 图行化界面导入授权文件

    <img class="wp-image-1218" src="https://gxxc.wiki/wp-content/uploads/2023/05/word-image-1091-3.png" />
    小数点开头的文件是隐藏文件,.kyinfo是隐藏文件,需要右键空白处,勾选“显示隐藏文件” 或者按ctrl+H快捷键显示隐藏文件。
    <img class="wp-image-1219" src="https://gxxc.wiki/wp-content/uploads/2023/05/word-image-1091-4.png" />

  4. 二维码扫码激活(可以离线,离线可以输入激活码)

    <img class="wp-image-1221" src="https://gxxc.wiki/wp-content/uploads/2023/05/word-image-1091-6.png" />

  5. 输入服务序列号,用有激活权限的微信扫码,可以获取到激活码;

    file


二:命令行方式激活

  1. 准备工作:将商务申请的.kyinfo及LICENSE授权文件拷贝至/etc目录下

    cp   .kyinfo  LICENSE   /etc 
  2. 登录到物理服务器上,在终端下输入

    sudo  kylin-system-verify

    如下图所示,命令显示系统信息,按回车键继续:
    <img class="wp-image-1223" src="https://gxxc.wiki/wp-content/uploads/2023/05/img_256.png" alt="IMG_256" />

  3. 按提示输入kylin,或者是按回车键继续。如下图:

    <img class="wp-image-704" src="https://gxxc.wiki/wp-content/uploads/2023/03/descript-212.png" alt="descript" width="479" height="140" />

  4. 生成二维码,用绑定了管理员权限的微信,扫这个二维码,填入授权书上的验证码后,可获取到激活码。或者把二维码发给麒麟工程师生成激活码

    <img class="wp-image-705" src="https://gxxc.wiki/wp-content/uploads/2023/03/descript-213.png" alt="descript" width="171" height="167" />

  5. 输入获取到的激活码,回车

    <img class="wp-image-706" src="https://gxxc.wiki/wp-content/uploads/2023/03/descript-214.png" alt="descript" width="480" height="216" />

  6. 激活完成,可以用命令查看kylin_activation_check激活状态

    以下是麒麟系统一些激活相关指令:

    命令备注
    kylin-activation调出激活图形化弹窗
    kylin_activation_check查看当前系统激活状态
    kylin-system-verify-new8位序列号编辑命令
    kylin-system-verify无图形界面命令行扫码激活
    kylin-verify查看授权到期时间
    kylin_activate_ukeyukey激活命令
    cat /etc/.kyinfo查看系统信息
    sudo kylin_gen_register查看系统注册码
    cat /etc/.kyactivation查看系统激活码

本文由mdnice多平台发布

注意:

  • 跨版本升级前请提前备份好重要数据!
  • 根分区需要预留大于 12G 的空间;

一、麒麟跨版本升级工具介绍

  1. 跨版本升级工具:kylin-revisions-manager
  2. 说明

    • 程序用于麒麟V10系统升级到麒麟V10-SP1系统
    • 升级后15天内可以还原为旧的V10系统;
    • 对于设置硬盘加密的系统,程序不适用;
    • 对于多系统设备(比如双系统),程序不适用;
    • 正常情况下用户数据会迁移到新系统;
    • 需要将iso拷贝到电脑硬盘上,不要放在U盘上直接安装,如果新系统的ISO文件放在U盘上有可能会升级失败

二、 系统升级前准备

  1. 升级工具下载:kylin-revision-manager [https://www.jianguoyun.com/p/Dc9zqrgQn9eQDBju4KQF]
  2. 升级工具安装:直接双击安装包安装;或者使用命令安装:

    sudo  dpkg  -i  kylin-revision-manager*.deb
  3. 下载好新的系统ISO(注意cpu架构和系统版本): https://gxxc.wiki/kos
     

三、系统升级步骤

  1. 双击桌面的“跨版本升级”图标,打开升级工具;(如果桌面没有这个快捷方式,可以从“开始--所有程序--跨版本升级”找到该工具)
    file
  2. 点击本地升级,选择需要升级的新的系统iso文件后,点击“升级”按钮
    file
  3. 阅读“注意事项”后,点击“升级”按钮
    file
  4. 阅读“升级须知”后,勾选“已阅读并同意协议内容”,点击“升级”按钮
    file
  5. 进入检查和准备阶段,如下图所示:
    file
    file
    file
    file
  6. 检查和准备完成后,系统将自动重启,重启后进入自动升级
    file
  7. 升级完成后,系统将再次重启,进入新系统登录界面
    file
  8. 输入密码后,进入新系统界面,提示“更新成功”,如下图所示:
    file
  9. 至此,系统升级已完成。

四、系统升级回退</li>

五、系统升级后的激活

六、升级常见问题

产生问题时,可以通过执行sudo bash getlog脚本收集以下日志。

  1. 升级后原系统数据存放位置:/fs.old/目录下
  2. 日志所在路径:/var/log/RevisionsManager/
  3. 迁移应用的日志路径:/opt/RevisionsManager/
  4. 配置和状态所在路径:/etc/RevisionsManager/
  5. initrd模块日志:/run/initramfs/initramfs.debug
    升级完成,但是提示部分应用安装失败,怎么处理?
    答:重新安装适配SP1的版本;对于应用商店中存在的应用,可以通过软件商店重新安装;
  6. 新系统使用不方便,怎么还原?
    答:15天内打开跨版本升级程序,点击“还原”即可快速还原到原来的版本。
  7. 系统的桌面背景、锁屏背景等发生变化
    答:系统升级支持sp1系统中默认图片,自定义情况请用户重新设置
  8. 迁移第三方应用选项说明:

    • 勾选,将会在v10sp1系统兼容第三方应用;
    • 不勾选,则不会。
    • 默认勾选。

 

七、异常情况处理

  1. 在第一部分升级完成后,重启阻塞
  2. 发生条件:

     - 用户使用过程中,用户强制关机或异常断电,导致程序运行异常;
     - 程序存在BUG; 
  3. 排查方法:处于启动界面,界面不显示升级进度,或是升级进度不变化。(20分钟以上)
  4. 处理方法:重启,选择选择Force back Kylin V10 ****选项,先恢复到V10系统。重新执行升级。
     
  5. 升级完成,存在部分应用安装失败
  6. 发生条件:

     - v10系统软件包不能在SP1中安装;
     - 自主安装包未在SP1中适配
  7. 排查方法:执行 dpkg -l | grep -v ii 查看安装失败的应用包
  8. 处理方法:对于软件源中存在的包,重新安装一下;
     
  9. 第一部分升级流程失败,重试后仍不能成功
  10. 排查方法:执行 ls /fs.new, 目录下文件夹只有 cdrom 和 opt ;
  11. 处理方法:该设备无法升级,记录设备的型号信息和系统版本信息。建议用重新安装SP1的方式升级上去。

本文由mdnice多平台发布