1. 为什么要把AI能力做进Compose组件库最近在做项目时我一直在思考一件事AI相关的功能越来越普及几乎每个应用都想接入大模型对话、图像识别、语音交互、智能图表这些能力但大部分团队每次都在做重复的工作。尤其是换到Jetpack Compose之后之前的View体系下积累的那套AI组件方案几乎没法直接迁移动画、状态管理、布局方式全都不一样了。于是我做了一个面向Compose的AI组件库把常见的AI场景能力抽成可以直接用的组件配合Compose的状态驱动机制让文本流式渲染、会话气泡、Markdown排版、图表可视化这些高频需求变成几行代码就能接入的现成方案。这个库主要解决三类人的痛点做AI应用但不想重复造轮子的Android开发者正在从View体系迁移到Compose又恰好需要AI交互能力的团队独立开发者一个人维护全端需要快速出Demo验证的产品原型阶段核心目标就一个让AI能力在Compose UI里像普通组件一样声明式地使用稳定、高效、不折腾。我用下来最大的感受是Compose的状态流模型和AI接口的流式返回简直是天生一对。传统的View体系下每次输出token都要findViewById找控件、post到主线程、手动拼接字符串、再notifyDataSetChanged刷新列表这套流程放到Compose里用Flow收集状态变化触发重组就行逻辑简单了不止一个量级。后面我会把组件库的模块设计、核心组件实现、性能优化思路和实际踩坑过程都拆开讲一遍希望能给正在做类似方向的朋友一些参考。2. 组件库的整体设计思路2.1 组件清单与模块划分做组件库之前我首先梳理了Android应用里常见的AI功能场景最后归成了六类组件组件功能定位典型使用场景AiStreamText流式文本输出组件支持文字从无到有逐个追加聊天机器人回复、智能写作助手输出AiMarkdownText流式文本的基础上做Markdown实时渲染支持标题、代码块、表格、列表的AI结果展示AiChatBubble会话气泡容器自动处理发送方/接收方样式智能助手对话页AiChartViewAI结构化数据的可视化组件数据报表生成、BI分析结果展示AiVoiceWave语音交互时的波形动画与录音控制语音助手、实时转写页面AiImageAnalyzer结合相机/相册的图像识别结果展示拍照识物、文档扫描、物体检测每个组件都独立成模块没有强制依赖关系。比如你只需要流式文本就只依赖ai-text模块不会把图表模块的代码也拖进来。这是做组件库必须坚持的粒度原则避免用户为了一个函数引入几十KB的无用代码。2.2 为什么必须用Compose重构一套有人问过我原来的View组件用AndroidView包装一下不也能在Compose里跑吗短期看确实能跑但长期就是个坑。Compose的核心是状态驱动UI状态变了自动重组受影响的部分。如果用AndroidView包一层旧组件数据更新时需要手动维护桥接层操作时机一旦没处理好就会出现状态跳变、UI闪烁、数据丢失的问题。而AI场景恰恰是高频状态变化的典型——文本流每秒可能更新多次图表数据随时变化语音波形实时刷新这种场景下声明式的重组机制远比手动刷新可控。另外Compose动画体系对AI交互体验加成非常大。流式文本的光标闪烁、图表数据的平滑过渡、语音波形的振幅动画这些用Compose的animate系列API做起来是天然的不需要额外引入动画库。我在开发过程中明显感受到同一套动画逻辑用Compose写比用属性动画器写省了一大半代码。还有一点容易被忽视Compose的强类型和组合模式让组件的API更容易设计。组件的输入输出是明确的数据类语义清晰使用方不需要去记一堆setter方法创建时传参、回调里拿结果这对第三方组件库的使用者来说学习成本很低。2.3 组件设计与API约定组件库的API设计我自己定了几个原则这里分享下。第一所有组件以数据类为输入主参数避免暴露内部状态。以流式文本组件为例AiStreamText( state streamState, // 外部维护的流式状态 textStyle ..., // 文字样式 cursorStyle ..., // 光标样式 modifier Modifier.fillMaxWidth() )组件本身不持有数据数据全部由外部通过AiStreamState控制。这样设计的好处很明显组件的生命周期、销毁重建由调用方掌握逻辑复用和测试都方便得多。第二所有回调统一使用onXxx命名事件参数尽量用数据类而不是零散的原始类型。比如语音组件的音量变化回调我封装了VoiceLevel(decibels: Float, amplitude: Float)调用方拿到的是一个完整的语义对象不会出现不知道某个float是什么含义的尴尬。第三所有组件都支持Modifier透传保持Compose使用者已有的习惯。同时每个组件都提供了默认参数合理的构造函数多数场景下不需要关心内部配置直接填关键数据就能用。3. 核心组件实现细节拆解3.1 流式文本组件的状态管理流式文本是整个组件库的地基其他几个组件大多依赖它的核心逻辑。它的难点不在于UI渲染而在于如何在频繁增量更新的情况下保证性能和正确性。我用了StateFlowAiStreamState来驱动文字内容的变化。其中AiStreamState是一个不可变数据类内部持有完整的当前文本data class AiStreamState( val fullText: String, val isCompleted: Boolean false, val errorMessage: String? null )每次AI接口返回新的增量文本时外部调用方把增量拼接到fullText上再通过MutableStateFlow.value发送新状态。Compose侧用collectAsStateWithLifecycle()收集状态流状态一旦变化就会触发重组画面上的文字同步更新。这里有个关键细节拼接字符串不要用循环叠加而是维护一个StringBuilder增量到来时append后统一取出字符串存到状态里。我之前在Chat场景实测如果用oldStr newPart的方式长文本输出到几千字时会出现明显卡顿因为每次拼接都会生成一个全新的字符串对象。改用StringBuilder后即使上万字的长文本性能也保持得很稳定。完整的最小使用示例val streamState remember { MutableStateFlow(AiStreamState()) } val aiTextState by streamState.collectAsStateWithLifecycle() // 模拟流式输出 LaunchedEffect(Unit) { val reply 这是一段流式返回的文本…… val sb StringBuilder() reply.chunked(3).forEach { chunk - sb.append(chunk) streamState.value AiStreamState(sb.toString()) delay(50) } streamState.value AiStreamState(sb.toString(), isCompleted true) } AiStreamText(state aiTextState)组件内部渲染逻辑很简单Text(text state.fullText)即可。但这个简单背后有一个大坑每次状态更新都触发整段文本的重新测量和布局。文本越长重组成本越高。后面性能优化章节会详细讲我是怎么处理的。3.2 Markdown与流式渲染的合并处理AI返回结果里大量存在Markdown格式这是必须支持的能力。但Markdown渲染和流式输出天然是矛盾的——Markdown语法往往是成对出现的比如**加粗**的开头和结尾流式输出时结尾还没到渲染器就会把语法标记当普通文本展示出来。我的解决方案是把渲染过程分两阶段。第一阶段实时阶段只做一次轻量的文法预处理过滤掉未闭合的标记符号对正常文字显示的影响。第二阶段稳定阶段当检测到文本已经完整isCompleted true再走一次完整的Markdown解析和渲染。Markdown解析这块我没有重复造轮子直接采用了开源社区成熟的Markdown解析库把解析结果转成Compose的AnnotatedString。解析库的选型我建议关注三点是否支持扩展语法表格、任务列表、删除线、解析性能是否够快、是否持续维护。我实测下来主流的几个Kotlin/Java Markdown解析库在长文本场景下性能差异挺明显的有些库解析2万字的文档耗时超过200ms这在UI线程上完全不可接受必须放到Dispatchers.Default异步解析结果通过状态流回传UI。基础用法示例AiMarkdownText( markdown state.fullText, isStreaming !state.isCompleted, modifier Modifier.fillMaxWidth() )组件内部的渲染流程图大致是文本增量到达后先更新实时预览区域同时启动异步解析解析完成后如果结果已经落后于最新文本则丢弃只把最新版本的解析结果应用到UI。这个只渲染最新状态的思路是流式场景的核心策略能避免UI被中间态的解析结果反复折腾。3.3 图表组件如何对接AI分析结果图表组件主要面向AI分析后的结构化数据。比如你跟AI说分析一下这个月的销售数据AI返回的是一段JSON里面包含各分类的统计值图表组件需要把这些数据渲染成柱状图、折线图或饼图。组件对外API设计如下AiChartView( data chartData, // 已预处理好的图表数据 chartType ChartType.LINE, // 折线/柱状/饼图 theme chartTheme, // 颜色、字体等主题配置 animationEnabled true // 是否启用入场动画 )我用Compose Canvas绘制图表而不是引入重量级的MPAndroidChart之类的View图表库。原因有两个一是Canvas在Compose里是头等公民绘制效率高且不需要中间转换层二是自绘让样式定制自由度极大AI前端展示的场景千奇百怪固定样式的图表库反而难扩展。图表组件的核心是数据预处理。AI返回的JSON结构不稳定有时候字段名变了有时候数值类型变了有的返回字符串12.5有的返回数字12.5有时候还带null。我的做法是先经过一层结构化清洗容错处理所有异常情况清洗完再转成图表组件所需的统一数据模型data class ChartData( val categories: ListString, val series: ListChartSeries ) data class ChartSeries( val name: String, val values: ListFloat )清洗层会做类型强转、默认值填充、异常丢弃等处理保证任何脏数据进来都不会导致组件崩溃。这块我给组件库加了自动降级逻辑数据量过大时自动抽样让图表的可读性优先于完整性数值为负时柱状图自动调整基线位置避免出现绘制越界。实际渲染时数据变化通过animateFloatAsState或Animatable驱动让新旧数据之间有平滑过渡视觉上不会突然跳变。比如折线图的数据点变化我用Animatable对每个点的值做动画200ms内从旧值渐变到新值效果很自然代码量也不大。3.4 语音交互组件的设计与适配语音组件是AI交互里仅次于文本的高频需求。语音助手要有按下说话、松开发送、录音中显示音量波形、播放回复时显示播放状态这一整套体验。AiVoiceWave的核心是波形动画。我通过Canvas绘制一排竖条高度根据音量实时变化AiVoiceWave( voiceLevel level, // 当前音量等级来自录音回调 barCount 32, // 竖条数量 barColor Color(0xFF4F8EF7), modifier Modifier.height(60.dp) )音量数据的来源是系统的AudioRecord或MediaRecorder在录音回调用maxAmplitude值换算成0到1的等级通过状态流发送给UI。这里要注意频率控制和线程切换音量回调频率不要高于每100ms一次否则UI重组太频繁回调线程要在主线程之外获取原始数据转成StateFlow值时再切到主线程发送。录音和播放的状态机我封装了VoiceControllersealed class VoiceState { object Idle : VoiceState() object Recording : VoiceState() object Processing : VoiceState() // 正在发送给AI解析 data class Playing(val progress: Float) : VoiceState() data class Error(val message: String) : VoiceState() }为什么要单独做状态机而不用几个Boolean标志位因为语音交互的边界状态非常多——录音中退出页面、录音权限被系统回收、播放中被打断、AI解析超时等等用标志位组合穷举不过来状态机封闭枚举才安全。实际开发中我踩过最深的坑就是录音中用户把应用切到后台Activity可能被回收录音线程却没有终止最后在后台录了几分钟都没人发现。后来在Lifecycle的ON_STOP回调里强制释放VoiceController问题才彻底解决。3.5 图像分析组件与相机协同图像识别类AI交互的典型场景是拍照识物、文档扫描、卡证识别。这类场景需要相机预览、拍照、识别结果展示三部分配合而识别过程本身有网络延迟所以UI上要同步处理加载态和结果态。方案是用CameraX做相机预览通过PreviewView整合进Compose拍照帧通过ImageAnalysis分析器回调出来。这里阻塞点在于CameraX的分析回调是高频回调每秒几十帧不能每一帧都丢给AI识别否则手机直接发热到烫手。我在组件里加了帧间隔控制——默认500ms取一帧做轻量预处理判断画面是否有变化明显变化才触发实际识别请求class AIImageAnalyzer( private val onResult: (String) - Unit ) : ImageAnalysis.Analyzer { private var lastAnalyzeTime 0L private var lastFrameHash override fun analyze(imageProxy: ImageProxy) { val currentTime System.currentTimeMillis() if (currentTime - lastAnalyzeTime 500) { imageProxy.close() return } // 提取特征值做变化检测 val hash extractFrameHash(imageProxy) if (hash lastFrameHash) { imageProxy.close() return } lastFrameHash hash lastAnalyzeTime currentTime // 实际识别注意脱离主线程 viewModelScope.launch(Dispatchers.IO) { val result recognize(imageProxy) onResult(result) } } }Compose侧接收识别结果后在预览画面下方展示识别结果卡片标注识别区域和置信度。组件提供overlayEnabled开关需要标注时开启一个半透明蒙层Canvas不需要时保持预览纯净。识别结果的数据结构也会做缓存识别成功后如果再拍到相同内容组件自动跳过重复请求避免浪费调用次数。4. 从组件到工程集成实操与性能优化4.1 项目接入方式与配置组件库的接入方式和普通Android库没有区别。开发阶段可以直接依赖源码模块发布后打到Maven仓库用坐标依赖// settings.gradle.kts 中配置仓库 dependencyResolutionManagement { repositories { maven(url https://repo.example.com/maven) } } // build.gradle.kts 中引入组件 dependencies { implementation(com.example:ai-text:1.0.0) implementation(com.example:ai-markdown:1.0.0) implementation(com.example:ai-chart:1.0.0) implementation(com.example:ai-voice:1.0.0) }版本配套上组件库要求minSdk 24及以上Compose BOM版本不低于某稳定版本Kotlin版本保持2.0系列以支持K2编译器。建议使用方在gradle.properties里开启Compose编译器独立的配置避免和Kotlin版本互相干扰。接入时最容易被忽略的是依赖隔离问题。组件库用到的协程、Compose运行时、生命周期库等都应该声明为api或者implementation是有讲究的对外暴露的类里如果包含了某个依赖的类型就必须用api否则使用方编译报错如果只是内部使用一律implementation避免把传递依赖污染给业务模块。这个细节我吃过亏某次个别使用方因为依赖版本冲突排查了半天最后发现是组件库粗放地暴露了所有依赖导致冲突。4.2 状态驱动与重组性能优化Compose的性能核心是重组范围的控制。流式文本组件是高频重组的典型场景一个字符接一个字符地从零开始打印如果每次重组影响范围过大卡顿是必然的。我把性能问题分成三层来解。第一层减小重组范围。AiStreamText内部结构是Text包裹整段文本是同一个节点重组必然整个Text重绘这个无法避免但外部不应该受到影响。做法是在组件内部用一个独立的rememberUpdatedState和子组合单位确保外部传入的Modifier、样式参数都是用remember固定的稳定引用不会因为父组件的抖动连带触发文本组件重组。第二层尽可能让数据更新走稳定类型路径。Compose的跳过机制基于参数比较如果入参类型不稳定比如传入一个每次重组都新建的lambda就无法跳过重组白白浪费计算。我在组件库内部对所有入参做了Immutable或Stable标注lambda回调也用remember包裹把跳过重组的概率拉到最大。第三层延迟非关键渲染。比如流式输出时文本已经滚动到接近底部此时如果实时更新会导致列表不断跳动用户根本看不到内容。我给组件加了autoScrollEnabled开关和滚动位置感知——只有当用户不在手动滚动时才自动滚到底部用户一旦触碰滚动区域就停止自动跟随让用户自己控制阅读位置体验反而更好。附上一个性能对比表格这是我用同一台测试机、同一段5000字文本跑出来的数据方案全文输出平均帧率掉帧次数无优化方案每次全量重组48fps47次加入StringBuilder拼接55fps22次完整优化稳定类型懒加载异步解析59fps6次数据看起来简单但每一项优化背后对应着一个具体的卡顿场景能拼到满帧的效果并不是靠一两个技巧而是整体设计上的取舍。4.3 长文本与滚动场景的懒加载策略聊天记录场景下AI消息可能非常长动辄数千字加上之前的用户消息整个会话会积累成一篇长文档。直接用一个Column把所有消息堆进去每一条消息都会完整参与测量布局消息多了必然卡。正确做法是用LazyColumn做消息列表配合items的key管理。每一条AI消息对应一个item内容再长也只渲染可视区域内的那部分LazyColumn( state listState, modifier Modifier.fillMaxSize() ) { items(items messages, key { it.id }) { message - when (message.type) { MessageType.USER - UserBubble(message) MessageType.AI - AiMarkdownText( markdown message.content, isStreaming message.isStreaming ) } } }这里有个容易被忽视的坑AiMarkdownText内部如果用了AnnotatedString解析和缓存在LazyColumn里滑动时item会频繁被回收和重建解析缓存随着item销毁就失效了滑回来又要重新解析导致卡顿和闪烁。我的解决方案是给解析缓存加一层LRU内存缓存以消息内容哈希作为key解析结果缓存最近100条。这样即使item重建也能立刻从缓存取到解析结果而不是重新解析一遍。另外流式文本如果放在LazyColumn里滚动手机底部的输入框和列表滚动联动也要处理好。我采用imePadding()配合滚动位置偏移确保软键盘弹起时当前正在输出的AI消息始终可见。这个交互是聊天产品的基本功但实现时很容易踩坑需要反复测试不同机型和输入法。4.4 网络与AI服务调用的生命周期管理AI功能几乎全依赖网络请求而网络请求一旦进入异步流程页面退出就是严重的事故高发点——请求回调回来后尝试更新已销毁的Compose组件状态直接崩溃。我的做法是确保所有AI网络请求都在ViewModel或rememberCoroutineScope()的协程里执行页面销毁时协程自动取消状态不会再更新。组件库内部不自己起协程去拉数据如果确有必要比如相册选图后的自动预览也遵循DisposableEffect的生命周期约定进入时启动退出时释放。Composable fun rememberAiTextState(): AiTextState { val scope rememberCoroutineScope() val state remember { AiTextState() } DisposableEffect(Unit) { // 启动时初始化 onDispose { // 退出时取消未完成的流 scope.launch { state.reset() } } } return state }在调用AI服务的耗时操作里我强烈建议使用超时机制。创建withTimeout(30_000)包裹网络请求超时后就用户可感知的错误提示而不是让用户永远盯着正在输入的转圈。流式响应场景下还需要处理半路断流的情况——已经输出了一半的内容突然没有后续了此时组件应该把isCompleted标记为true并把输出中断作为一条独立状态呈现而不是假装什么都没发生继续等待。5. 实战中的踩坑与排查技巧5.1 重组风暴引发的卡顿现象很典型接入流式文本后页面在AI输出过程中卡成PPT滑动列表完全跟不上。第一次排查时我以为是解析Markdown太慢导致的把解析逻辑放到后台线程后改善了一些但依然不顺滑。后面通过查看重组日志发现问题根源是输出文本时整个页面都在重组——外层把整段AI消息内容作为状态传给AiMarkdownText但外层列表是LazyColumn每个item在滑动时都在和文本组件抢资源。最终解法是把列表的item内容用remember(message.id)包住确保item内容不会因为父级刷新而重新计算再给AiMarkdownText内部加上本地状态优先级——只要当前收到的文本比上一次长先显示本地缓存不同步触发外部重组只有积累到一定字符数我设置的阈值是每50字符才做一次整段同步。这样把高频小更新拦截在组件内部对外只暴露低频的状态更新卡顿问题基本消失。这里也提醒做类似组件的同行Compose的局部重组虽然比View体系高效但不是银弹高频率小粒度更新时反而会放大重组调度开销必须自己做好更新节流和重组隔离。5.2 文本流状态丢失与恢复另一个高频问题是应用被系统回收后再回来AI流式输出到一半的状态全没了聊天记录断层用户重新看到的是空白的会话框。根本原因是文本流状态存在Compose的remember里页面销毁后状态自然消失。我做了两层恢复方案。第一层是常规的配置变更恢复。用rememberSaveable保存已输出文本的内容和是否完成标记。但rememberSaveable要求状态能被放进Bundle大文本需要自定义Saver简单做法是把字符串存起来val aiTextState rememberSaveable( saver AiTextStateSaver ) { AiTextState() }第二层是进程死亡恢复。应用在后台被杀掉再回来时仅靠rememberSaveable也不一定可靠因为系统对已恢复的Bundle大小有限制。我的策略是把重要会话内容在每次状态变化时持久化到本地数据库恢复时先从数据库读最近的快照再继续等待AI服务重新补齐剩余文本。虽然实现成本稍高但对用户体验来说值得。5.3 字符串拼接的性能陷阱这个坑前面提到过一次我再详细展开。如果直接text text newChunk每追加一个chunk就创建一个新的String对象旧的String等着被GC回收。流式场景每秒可能追加几十次甚至上百次一段时间后GC压力飙升直接表现为UI掉帧。正确姿势是维护共享的StringBuilderclass AiTextState { private val sb StringBuilder() val text: String get() sb.toString() fun append(chunk: String) { sb.append(chunk) // 通知外部状态刷新 } }但有线程安全陷阱如果流式数据从IO线程写入UI线程读取StringBuilder就不是线程安全的会出现数据显示错乱甚至崩溃。我的方案是在状态类里用synchronized或直接改用独立线程的串行队列保证append和获取text的操作互斥。简单起见就是用MutableStateFlow结合update原子操作不需要显式加锁。还有一种更隐蔽的性能问题——创建AnnotatedString后频繁修改Span。Compose的AnnotatedString是不可变类每次修改都要toSpannable()再toAnnotatedString()开销不小。流式场景里如果实时生成AnnotatedString做标色累计成本非常可观。我的建议是先用普通字符串做流式输出文本全部输出完成后再一次性转成AnnotatedString做复杂样式。如果必须实时带样式就把AnnotatedString的修改放在Dispatchers.Default线程用返回的不可变对象驱动UI。5.4 图表组件在低端机上的掉帧图表组件在开发机中高端配置上跑得很顺换到一台低端测试机后折线图在数据变化时页面掉帧严重。分析下来有两个原因。第一个原因是数据点太多。折线图如果几千个点全部绘制每一帧都要遍历全部点做投影计算低端机的CPU根本扛不住。我加了抽稀策略绘制前对点序列做降采样最多只保留200个关键点。降采样算法用了简单的均匀抽样加极值保留视觉上保留了波峰波谷信息量没丢多少。第二个原因是动画驱动方式的问题。最初所有点的变化都用各自的Animatable驱动几百个动画同时跑性能奇差。后来改成统一的Animatable管理一个整体进度值所有点的中间值根据进度统一插值计算动画时长统一状态只需驱动一个目标值渲染压力直接降低了一个量级。这套思路也适用于其他组件动画不是越多越细越好对于数据可视化牺牲单个点的独立性换取整体的流畅是更明智的选择。6. 一些探索和后续方向6.1 多模态交互组件的扩展可能性现在组件库覆盖了文本、图表、语音、图像四大类能力下一步我考虑做多模态融合的组件。比如用户拍一张产品图AI直接识别并生成图文并茂的介绍卡片或者语音提问后AI不止输出文字同时生成相关的图表辅助说明。这种多模态结果展示如果做成一个组合组件业务方只需要给一个MultiModalResponse数据对象组件自动分配文本区、图表区、图片区的排版和交互使用体验会大幅提升。技术上不难实现本质是把已有的几个基础组件按数据内容组装进一个Column或FlowRow但需要处理好不同模态数据之间的先后逻辑和加载顺序。我计划先沉淀这个组合模式在各业务场景试用后再抽成正式组件。6.2 和现有生态的配套协同组件库不能孤立存在要和Android生态里的其他工具配合才能发挥最大价值。我在组件设计时留好了和Paging3、Room、Hilt等库的兼容接口。比如长聊天记录可以接入Paging3做分页加载本地缓存可以直接用Room存消息快照网络请求封装可以对接OkHttp拦截器等。我特别建议使用者把AI请求的调用逻辑放在Repository层而非直接写在Composable函数里。这样组件库只管UI表现数据获取和业务逻辑留在干净的架构层里后续切换AI服务商或者调整模型策略时不用动UI代码。组件库的价值在于提供一致、稳定的UI能力而不是替业务方决定调用哪个AI服务。6.3 跨平台方向的思考Compose Multiplatform现在发展势头很猛如果未来某天需要把AI组件库迁移到iOS等平台由于核心绘制逻辑是Canvas绘制、状态驱动是Flow/StateFlow、文本渲染基于AnnotatedString这些模型天然跨平台理论上迁移成本会比传统View体系的组件低很多。但这个方向我不会急着推进毕竟组件库的目标是解决Android侧的AI交互问题先把这端的体验做到极致再说跨端的事。等Compose Multiplatform的生态足够稳定、桌面端和移动端的目标用户规模起来了再考虑逐步抽取共享层才是稳扎稳打的路线。我个人在这段时间的实际体验是做组件库最困难的不是单个组件的实现而是API设计、状态管理、性能优化这条路上的平衡取舍。把AI能力封装成Compose组件本质上是在做一套脚手架——把AI应用最烦琐的UI交互部分打理好让开发者把精力放在真正有业务价值的地方。如果你也在探索AI和Compose的结合方向建议先从你当前最痛的一个场景开始做封装比如只做一个流式文本组件跑通完整链路后再扩展其他能力。组件库不需要一步到位能在实际项目里稳定复用就已经体现了它的价值。