Karmada 依赖中的 Zero Allocation JSON Logger:rs/zerolog 完整使用指南
【免费下载链接】karmadaOpen, Multi-Cloud, Multi-Cluster Kubernetes Orchestration项目地址: https://gitcode.com/GitHub_Trending/ka/karmada
zerolog 是一个专为 JSON 结构化日志设计的 Go 日志库,其链式 API 通过避免内存分配与反射实现高性能输出,是 Uber zap 之后把“零分配日志”理念推向极致的代表性实现。本文将以仓库中vendor/github.com/rs/zerolog(版本 v1.33.0,见 go.mod)提供的官方 README 与源码为依据,系统讲解其安装、核心 API、日志级别、上下文字段、采样、Hook、context.Context集成、net/http集成以及全局配置,并结合源码文件佐证底层实现细节,帮助你在多集群编排等大型 Go 项目中写出高效、可观测的结构化日志。
一、zerolog 是什么:零分配 JSON 日志器
zerolog 的核心理念是:日志输出使用 JSON 格式,并且通过独特的链式调用 API 在写入日志事件时避免内存分配(allocation)和反射(reflection)。官方 README 中明确描述其 API 设计目标——"great developer experience and stunning performance",其基准测试表(BenchmarkLogEmpty达到约 19.1 ns/op、0 B/op、0 allocs/op)展示了这一设计的成果。
在 Karmada 这类由多个控制面组件(karmada-controller-manager、karmada-scheduler、karmada-webhook、karmada-agent等)构成的系统中,日志是最重要的可观测性手段之一,zerolog 的零分配特性让高频事件处理路径上的日志开销趋近于零。
zerolog 刻意保持“聚焦”:
- 只做高效的结构化日志;
- 控制台“美化输出”(Pretty Logging)通过
zerolog.ConsoleWriter实现,README 明确指出这种方式“inefficient”(低效),仅供开发期使用。
仓库位置:
vendor/github.com/rs/zerolog/目录下包含核心实现文件log.go(全局 Logger 入口)、event.go、context.go、encoder.go、encoder_json.go、encoder_cbor.go、globals.go(全局配置与级别定义)、sampler.go(采样器)、console.go(ConsoleWriter)等。
二、安装与快速开始
安装全局 Logger 子包:
go get -u github.com/rs/zerolog/log最简单的用法是直接使用全局 Logger(默认写入os.Stderr):
package main import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) func main() { // UNIX Time 比大多数时间戳更快且更短 zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Print("hello world") } // Output: {"time":1516134303,"level":"debug","message":"hello world"}两个需要注意的默认行为(README 明确说明):
- 默认输出到
os.Stderr; log.Print的默认日志级别是trace。
zerolog.TimeFormatUnix等常量定义在源码 globals.go 中:TimeFormatUnix = "",而TimeFormatUnixMs = "UNIXMS"、TimeFormatUnixMicro = "UNIXMICRO"、TimeFormatUnixNano = "UNIXNANO",分别表示将时间序列化为 Unix 秒/毫秒/微秒/纳秒整数时间戳。
三、上下文日志:链式添加强类型字段
zerolog 允许以key:value的形式向日志消息添加上下文数据。字段是强类型的,例如:
package main import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Debug(). Str("Scale", "833 cents"). Float64("Interval", 833.09). Msg("Fibonacci is everywhere") log.Debug(). Str("Name", "Tom"). Send() // Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"} // Output: {"level":"debug","Name":"Tom","time":1562212768} }字段类型对照(README “Standard Types”一节):Str、Bool、Int/Int8/Int16/Int32/Int64、Uint/Uint8/Uint16/Uint32/Uint64、Float32/Float64;多数类型还有切片形式(如Strs对应[]string)。高级字段包括:
Err:接收error,使用zerolog.ErrorFieldName(默认"error")渲染;Func:仅在级别启用时才执行传入的func;Timestamp:插入带TimestampFieldName的时间戳字段;Time/Dur:时间字段与time.Duration字段;Dict:将子字典作为字段嵌入;RawJSON:直接插入已编码的 JSON([]byte);Hex:以十六进制字符串格式输出[]byte;Interface:使用反射序列化任意类型。
从源码globals.go可以看到这些行为的全局开关:InterfaceMarshalFunc默认使用encoding/json.Marshal(并关闭 HTML 转义),ErrorMarshalFunc默认直接返回 error 本身。
四、分级日志(Leveled Logging)
4.1 级别体系与链式调用的约束
zerolog 支持从高到低七个级别(READEM 原文):
- panic(
zerolog.PanicLevel,5) - fatal(
zerolog.FatalLevel,4) - error(
zerolog.ErrorLevel,3) - warn(
zerolog.WarnLevel,2) - info(
zerolog.InfoLevel,1) - debug(
zerolog.DebugLevel,0) - trace(
zerolog.TraceLevel,-1)
package main import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Info().Msg("hello world") } // Output: {"time":1516134303,"level":"info","message":"hello world"}关键约束:使用链式 API 时(如log.Info()....),链的结尾必须调用Msg或Msgf。如果遗漏,日志不会输出,且不会有编译期错误提醒——这是 zerolog README 反复强调的易错点。
4.2 全局日志级别与Enabled()预检查
通过zerolog.SetGlobalLevel可以设置全局最小级别,所有低于该级别的日志会被过滤;传入zerolog.Disabled则完全关闭日志(静默模式)。源码 globals.go 中该函数通过atomic.StoreInt32(gLevel, int32(l))实现,保证并发安全。
package main import ( "flag" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix debug := flag.Bool("debug", false, "sets log level to debug") flag.Parse() // 本示例默认级别为 info,除非指定 debug 标志 zerolog.SetGlobalLevel(zerolog.InfoLevel) if *debug { zerolog.SetGlobalLevel(zerolog.DebugLevel) } log.Debug().Msg("This message appears only when log level set to Debug") log.Info().Msg("This message appears when log level set to Debug or Info") if e := log.Debug(); e.Enabled() { // 仅在启用时才计算并输出日志内容 value := "bar" e.Str("foo", value).Msg("some debug message") } }不加标志运行输出:
$ ./logLevelExample {"time":1516387492,"level":"info","message":"This message appears when log level set to Debug or Info"}加-debug运行输出:
$ ./logLevelExample -debug {"time":1516387573,"level":"debug","message":"This message appears only when log level set to Debug"} {"time":1516387573,"level":"info","message":"This message appears when log level set to Debug or Info"} {"time":1516387573,"level":"debug","foo":"bar","message":"some debug message"}实践提示:对于计算代价较高的调试字段(如序列化复杂对象),先调用
log.Debug()拿到事件后检查e.Enabled(),再决定是否构造字段,可避免无谓的性能开销。
4.3 无级别、无消息的日志
使用Log()方法可以不指定级别,在Msg中传入空字符串则可省略消息:
log.Log(). Str("foo", "bar"). Msg("") // Output: {"time":1494567715,"foo":"bar"}五、错误日志与堆栈追踪
5.1 基础错误日志
使用Err方法记录错误:
err := errors.New("seems we have an error here") log.Error().Err(err).Msg("") // Output: {"level":"error","error":"seems we have an error here","time":1609085256}错误字段的默认名称为error,可通过设置zerolog.ErrorFieldName修改。
5.2 带堆栈的错误日志
配合github.com/pkg/errors生成带格式化的堆栈追踪,再通过zerolog.ErrorStackMarshaler输出:
package main import ( "github.com/pkg/errors" "github.com/rs/zerolog/pkgerrors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) func main() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack err := outer() log.Error().Stack().Err(err).Msg("") } func inner() error { return errors.New("seems we have an error here") } func middle() error { if err := inner(); err != nil { return err }; return nil } func outer() error { if err := middle(); err != nil { return err }; return nil } // Output: {"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683}注意:必须设置zerolog.ErrorStackMarshaler,否则堆栈不会输出。对应字段名定义在globals.go中:ErrorStackFieldName = "stack"、ErrorStackMarshaler类型为func(err error) interface{}。
5.3 致命错误
Fatal级别在输出日志后会终止进程(示例输出末尾有exit status 1):
err := errors.New("A repo man spends his life getting into tense situations") service := "myservice" zerolog.TimeFieldFormat = zerolog.TimeFormatUnix log.Fatal(). Err(err). Str("service", service). Msgf("Cannot start %s", service) // Output: {"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"} // exit status 1性能注意(README 原文):使用
Msgf即使在日志器被禁用时也会产生一次内存分配。
六、Logger 实例、子 Logger 与上下文透传
6.1 创建独立 Logger 实例
logger := zerolog.New(os.Stderr).With().Timestamp().Logger() logger.Info().Str("foo", "bar").Msg("hello world") // Output: {"level":"info","time":1494567715,"message":"hello world","foo":"bar"}6.2 子 Logger:为日志器链式追加上下文
sublogger := log.With(). Str("component", "foo"). Logger() sublogger.Info().Msg("hello world") // Output: {"level":"info","time":1494567715,"message":"hello world","component":"foo"}在大型项目中,这非常适合为不同模块(如 Karmada 的 scheduler、webhook、estimator)各建一个带"component"字段的子 Logger。
6.3 向全局 Logger 添加上下文
log.Logger = log.With().Str("foo", "bar").Logger()6.4 通过 context.Context 传递子 Logger
ctx := log.With().Str("component", "module").Logger().WithContext(ctx) log.Ctx(ctx).Info().Msg("hello world") // Output: {"component":"module","level":"info","message":"hello world"}七、开发期美化输出与多路输出
7.1 ConsoleWriter 美化输出
开发环境可用zerolog.ConsoleWriter输出带颜色的人类可读日志:
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Info().Str("foo", "bar").Msg("Hello world") // Output: 3:04PM INF Hello World foo=bar7.2 自定义 ConsoleWriter 格式
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339} output.FormatLevel = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) } output.FormatMessage = func(i interface{}) string { return fmt.Sprintf("***%s****", i) } output.FormatFieldName = func(i interface{}) string { return fmt.Sprintf("%s:", i) } output.FormatFieldValue = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%s", i)) } log := zerolog.New(output).With().Timestamp().Logger() log.Info().Str("foo", "bar").Msg("Hello World") // Output: 2006-01-02T15:04:05Z07:00 | INFO | ***Hello World**** foo:BAR7.3 多路输出:MultiLevelWriter
可将同一条日志同时发送到多个输出(如控制台与 JSON 文件):
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout} multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout) logger := zerolog.New(multi).With().Timestamp().Logger() logger.Info().Msg("Hello World!") // 输出第 1 行(Console):12:36PM INF Hello World! // 输出第 2 行(Stdout):{"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"}八、调用位置(Caller)与子字典
8.1 输出文件与行号
等价于标准库Llongfile的全路径格式:
log.Logger = log.With().Caller().Logger() log.Info().Msg("hello world") // Output: {"level": "info", "message": "hello world", "caller": "/go/src/your_project/some_file:21"}等价于Lshortfile的短文件名格式(通过自定义CallerMarshalFunc):
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string { return filepath.Base(file) + ":" + strconv.Itoa(line) } log.Logger = log.With().Caller().Logger() log.Info().Msg("hello world") // Output: {"level": "info", "message": "hello world", "caller": "some_file:21"}源码globals.go中定义了CallerFieldName = "caller"、CallerSkipFrameCount = 2(跳过栈帧数量)以及默认的CallerMarshalFunc(返回file + ":" + line)。
8.2 子字典(Sub Dictionary)
用Dict把一组字段作为嵌套 JSON 对象输出:
log.Info(). Str("foo", "bar"). Dict("dict", zerolog.Dict(). Str("bar", "baz"). Int("n", 1), ).Msg("hello world") // Output: {"level":"info","time":1494567715,"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}九、线程安全的非阻塞 Writer:diode
如果 Writer 较慢或非线程安全,又不希望日志生产者被慢 Writer 拖慢,可以使用diode.Writer(需额外安装code.cloudfoundry.org/go-diodes):
wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) { fmt.Printf("Logger Dropped %d messages", missed) }) log := zerolog.New(wr) log.Print("test")其原理是使用环形缓冲解耦日志生产与消费,通过回调报告被丢弃的消息数量。
十、日志采样(Log Sampling)
10.1 基础采样
BasicSampler每 N 条消息记录一次:
sampled := log.Sample(&zerolog.BasicSampler{N: 10}) sampled.Info().Msg("will be logged every 10 messages") // Output: {"time":1494567715,"level":"info","message":"will be logged every 10 messages"}10.2 组合采样
BurstSampler与LevelSampler组合可实现“突发窗口 + 低频兜底”的限流策略:
// 每秒内最多放行 5 条 debug 日志; // 超过 5 条后,每 100 条 debug 只记录 1 条; // 其他级别不受采样影响。 sampled := log.Sample(zerolog.LevelSampler{ DebugSampler: &zerolog.BurstSampler{ Burst: 5, Period: 1*time.Second, NextSampler: &zerolog.BasicSampler{N: 100}, }, }) sampled.Debug().Msg("hello world") // Output: {"time":1494567715,"level":"debug","message":"hello world"}采样器的实现位于源码 sampler.go,其中BurstSampler的NextSampler用于在突发窗口被耗尽后继续对后续事件做进一步采样,从而兼顾“窗口内限流”与“长尾抑制”。采样开关由全局配置zerolog.DisableSampling(true)控制(见globals.go中的DisableSampling)。
十一、Hooks:在事件写入前注入逻辑
Hook 在事件被写出之前运行,适合统一注入严重级别、trace/span ID 等字段:
type SeverityHook struct{} func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { if level != zerolog.NoLevel { e.Str("severity", level.String()) } } hooked := log.Hook(SeverityHook{}) hooked.Warn().Msg("") // Output: {"level":"warn","severity":"warn"}Hook 接口定义见源码 hook.go:type Hook interface { Run(e *Event, level Level, message string) }。
十二、context.Context 深度集成
12.1 将 Logger 放入 context 并取出
Go 的context.Context常用于在函数间传递 Logger,避免依赖注入困难:
func f() { logger := zerolog.New(os.Stdout) ctx := context.Background() // 将 Logger 附加到 context.Context ctx = logger.WithContext(ctx) someFunc(ctx) } func someFunc(ctx context.Context) { // 从 Go Context 中取出 Logger。 // 若为 nil,则返回 zerolog.DefaultContextLogger; // 若 DefaultContextLogger 也为 nil,则返回一个被禁用的 logger。 logger := zerolog.Ctx(ctx) logger.Info().Msg("Hello") }DefaultContextLogger的默认值定义在globals.go中,可通过赋值自定义兜底 Logger。
12.2 将 context 传入事件,供 Hook 读取
第二种集成方式:把当前context.Context传入日志事件(Ctx(ctx)),Hook 内通过e.GetCtx()取回,常用于日志与链路追踪(tracing)的统一:
type TracingHook struct{} func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { ctx := e.GetCtx() spanId := getSpanIdFromContext(ctx) // 依据你的 tracing 框架实现 e.Str("span-id", spanId) } func f() { logger := zerolog.New(os.Stdout) logger = logger.Hook(TracingHook{}) ctx := context.Background() // 使用 Ctx 函数让 context 对 Hook 可见 logger.Info().Ctx(ctx).Msg("Hello") }十三、与 net/http 集成:hlog 辅助包
github.com/rs/zerolog/hlog提供了一组与http.Handler集成的辅助函数。README 示例用alice组装中间件链,输出包含请求上下文字段的访问日志:
log := zerolog.New(os.Stdout).With(). Timestamp(). Str("role", "my-service"). Str("host", host). Logger() c := alice.New() // 安装默认输出到控制台的 logger handler c = c.Append(hlog.NewHandler(log)) // 安装额外的 handler 为请求设置上下文字段 c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) { hlog.FromRequest(r).Info(). Str("method", r.Method). Stringer("url", r.URL). Int("status", status). Int("size", size). Dur("duration", duration). Msg("") })) c = c.Append(hlog.RemoteAddrHandler("ip")) c = c.Append(hlog.UserAgentHandler("user_agent")) c = c.Append(hlog.RefererHandler("referer")) c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id")) // 最终 handler h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 从请求的 context 中取 logger;若 handler 被移除, // hlog.FromRequest 会返回一个 no-op logger hlog.FromRequest(r).Info(). Str("user", "current user"). Str("status", "ok"). Msg("Something happened") // Output: {"level":"info","time":"2001-02-03T04:05:06Z","role":"my-service","host":"local-hostname","req_id":"b4g0l5t6tfid6dtrapu0","user":"current user","status":"ok","message":"Something happened"} })) http.Handle("/", h) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal().Err(err).Msg("Startup failed") }十四、作为标准库 logger 的输出
可以将 zerolog 作为标准库log的后端,让存量代码自动获得 JSON 输出:
log := zerolog.New(os.Stdout).With(). Str("foo", "bar"). Logger() stdlog.SetFlags(0) stdlog.SetOutput(log) stdlog.Print("hello world") // Output: {"foo":"bar","message":"hello world"}十五、全局设置一览
README “Global Settings”一节与源码globals.go中定义的可调全局项如下:
| 全局项 | 说明 | 默认值 |
|---|---|---|
log.Logger | 全局 Logger(包级方法使用的那个) | 默认写入os.Stderr |
zerolog.SetGlobalLevel | 提升所有 Logger 的最小级别;传zerolog.Disabled可完全关闭日志 | 无(0,即 debug) |
zerolog.DisableSampling | 为true时所有采样 Logger 停止采样、100% 输出 | false |
zerolog.TimestampFieldName | 时间戳字段名 | "time" |
zerolog.LevelFieldName | 级别字段名 | "level" |
zerolog.MessageFieldName | 消息字段名 | "message" |
zerolog.ErrorFieldName | Err字段名 | "error" |
zerolog.TimeFieldFormat | 时间字段格式;设为TimeFormatUnix/TimeFormatUnixMs/TimeFormatUnixMicro时输出为 UNIX 时间戳 | time.RFC3339 |
zerolog.DurationFieldUnit | Dur字段的时间单位 | time.Millisecond |
zerolog.DurationFieldInteger | 为true时Dur字段输出整数而非浮点 | false |
zerolog.ErrorHandler | 写入失败时被调用;必须线程安全且非阻塞 | 未设置时向 stderr 打印错误 |
zerolog.FloatingPointPrecision | 非 -1 时控制 JSON 中浮点数的格式化位数(参见strconv.FormatFloat) | -1 |
自定义字段名示例
zerolog.TimestampFieldName = "t" zerolog.LevelFieldName = "l" zerolog.MessageFieldName = "m" log.Info().Msg("hello world") // Output: {"l":"info","t":1494567715,"m":"hello world"}十六、二进制编码(CBOR)
除默认 JSON 外,zerolog 还可以在编译期通过构建标签binary_log产出 CBOR 二进制日志:
go build -tags binary_log .二进制日志可配合 CBOR 解码器读取。编码器实现见源码 encoder_cbor.go,与 JSON 编码器 encoder_json.go 通过统一的 encoder.go 接口抽象,这正是“编译期切换编码”能够实现的原因。
十七、性能基准
README 给出官方微基准(所有操作均零分配,数字已包含 JSON 编码):
BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op BenchmarkDisabled-8 500000000 4.07 ns/op 0 B/op 0 allocs/op BenchmarkInfo-8 30000000 42.5 ns/op 0 B/op 0 allocs/op BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/opREADME 还引用 Uber zap 的比较基准:记录“1 条消息 + 10 个字段”场景下 zerolog 约 767 ns/op、552 B/op、6 allocs/op,优于 zap(848 ns/op、704 B/op、2 allocs/op)与 zap sugared(1363 ns/op、1610 B/op、20 allocs/op),且显著优于 logrus(5661 ns/op、6092 B/op、78 allocs/op)。需要说明的是,这些数据来自 README 原文引用,具体数值会随硬件与 Go 版本变化,关键结论是“零分配”设计使 GC 压力与单条日志延迟都极低。
十八、使用注意事项(Caveats)
18.1 字段不自动去重
zerolog 不会对字段做去重。同一 key 使用多次会产生多个同名 key:
logger := zerolog.New(os.Stderr).With().Timestamp().Logger() logger.Info(). Timestamp(). Msg("dup") // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}多数消费端会取最后一个值,但 README 提醒“这不保证”,使用前请确认你的消费端行为。
18.2 并发安全提醒
UpdateContext不是并发安全的,需要追加上下文时应使用With()创建子 Logger:
func handler(w http.ResponseWriter, r *http.Request) { // 为并发安全创建子 Logger logger := log.Logger.With().Logger() // 追加上下文字段,例如 HTTP 头中的 User-Agent logger.UpdateContext(func(c zerolog.Context) zerolog.Context { // ... }) }此外,从globals.go的LevelColors与FormattedLevels可以看出,ConsoleWriter 的级别着色与缩写(TRC/DBG/INF/WRN/ERR/FTL/PNC)也是可配置的,生产环境若使用 ConsoleWriter 输出可据此定制展示风格。
十九、小结
zerolog 以“零分配 JSON 日志”为核心,用一套简洁的链式 API 覆盖了分级日志、上下文字段、采样、Hook、context 透传、HTTP 中间件、多路输出与二进制编码等完整能力。本文结合vendor/github.com/rs/zerolog下的官方 README 与源码(globals.go、log.go、hook.go、sampler.go、encoder_cbor.go等)对每一项能力给出了可直接运行的代码示例、输出样例与配置说明,可作为你在 Go 项目中接入或调优 zerolog 的速查手册。如果你在 Karmada 这类多组件、多集群控制面中构建可观测性基础设施,建议将时间戳切换为 UNIX 格式、为各组件建立带component字段的子 Logger,并对高频日志路径启用采样与Enabled()预检查,从而在保持日志可读的同时把开销压到最低。
【免费下载链接】karmadaOpen, Multi-Cloud, Multi-Cluster Kubernetes Orchestration项目地址: https://gitcode.com/GitHub_Trending/ka/karmada
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考