从 HarmonyOS AI 聊天模块理解工程化架构:MVVM、Controller、Provider、请求封装与 SSE
摘要:HarmonyOS AI 聊天模块的工程化架构解析 本文深入分析了HarmonyOS AI聊天模块的架构设计,重点阐述了其工程化思想。该模块采用分层架构,包含页面入口、UI组件、状态管理、业务流程控制、AI平台适配、网络请求封装等多个层次。核心架构采用MVVM模式,通过ViewModel实现UI与业务逻辑的解耦,状态变化自动驱动UI更新。模块还实现了Controller流程编排、Provid
从 HarmonyOS AI 聊天模块理解工程化架构:MVVM、Controller、Provider、请求封装与 SSE
一、前言
这几天主要在看一个 HarmonyOS AI 聊天模块的源码。
一开始看这种项目,最大的感受就是文件很多、层级很多:页面、组件、ViewModel、Controller、Provider、HttpClient、Parser、Model 都有。直接从某个方法开始逐行看,很容易看着看着就迷路。
后来我发现,读这种业务模块不能一上来就陷进细节,而是要先把整体链路理清楚。
这个聊天模块表面上是一个 AI 对话页面,但实际包含了页面入口、聊天 UI、状态管理、业务流程控制、AI 平台适配、请求封装、SSE 流式响应、会话管理、业务卡片渲染等能力。
如果按架构分层来看,大致可以抽象成这样:
页面入口
↓
聊天组件
↓
状态中心
↓
业务流程控制器
↓
AI 平台适配层
↓
网络请求封装
↓
SSE 流式响应
↓
结果回写状态
↓
UI 自动刷新
换成代码里的概念,大概就是:
Page
↓
View
↓
ViewModel
↓
Controller
↓
Provider
↓
HttpClient
这篇文章主要记录我目前对这个 AI 聊天模块的理解,重点不是某个具体业务,而是总结其中体现出来的一些工程化思想:MVVM、组件化、解耦、Provider 适配器、请求统一封装、SSE 流式处理、状态驱动 UI。
二、整体架构概览
一个完整的 AI 聊天模块,通常不会只写在一个页面文件里,而是会拆成多个层次。
大概可以理解为:
pages/
页面入口,负责路由注册和业务配置
view/
UI 组件层,负责聊天页面展示
viewmodel/
状态管理层,负责保存页面状态
controller/
业务流程编排层,负责发送消息、会话切换、语音输入等流程
api/
AI 平台适配层,负责对接不同 AI 平台
utils/
工具封装层,负责请求、解析、转换等能力
model/
数据模型层,负责定义消息、会话、卡片等结构
constant/
常量和协议层,负责统一管理接口路径、事件类型、状态码等
可以用一句话概括:
View 负责展示,ViewModel 负责状态,Controller 负责流程,Provider 负责平台适配,HttpClient 负责请求。
这样拆分之后,每一层的职责会更清楚,不会把 UI、状态、请求、协议解析、错误处理全部堆在一个页面文件里。
三、页面入口层:负责装配,不负责核心逻辑
页面入口层通常负责几件事:
- 注册路由
- 创建 AI Provider
- 配置聊天组件参数
- 注入业务回调
- 渲染聊天组件
可以简单理解成,页面入口不是聊天逻辑的核心,它更像是一个“组装器”。
例如:
@ComponentV2
export struct ChatPage {
@Local provider: AgentProvider | null = null
aboutToAppear(): void {
const config = new ProviderConfig()
config.userId = 'current_user_id'
this.provider = new SomeAIProvider(config)
}
build() {
AgentChatComp({
provider: this.provider,
chatConfig: this.buildChatConfig(),
cardsBuilder: this.cardsBuilder,
loadingBuilder: this.loadingBuilder
})
}
private buildChatConfig(): ChatConfig {
const config = new ChatConfig()
config.welcomeMessage = '你好,我是你的 AI 助手'
config.quickPhrases = ['推荐问题 1', '推荐问题 2']
return config
}
}
这里比较重要的一点是:
页面入口只负责装配,不负责聊天核心逻辑。
它不应该直接处理发送 AI 请求、解析 SSE、维护消息数组、处理会话分页、解析卡片 JSON、上传附件等逻辑。
这些复杂逻辑应该交给后面的组件层、状态层、Controller 层和 Provider 层。
页面入口主要做的是:
创建 Provider
配置 ChatConfig
传入 Builder
挂载聊天组件
这样页面会比较轻,后期业务变化时也更好维护。
四、聊天组件层:UI 总容器
聊天组件层负责搭建聊天页面的整体 UI。
一个完整聊天页面通常包括:
- 消息列表
- 底部输入框
- 推荐问题
- 会话抽屉
- 加载蒙层
- 语音输入蒙层
- 浮动按钮
- 业务卡片
结构大概可以理解为:
Stack 根容器
├── 背景层
├── 主内容层
│ ├── MessageList
│ ├── QuickQuestionsCard
│ ├── FloatingButtons
│ ├── InputBar
│ ├── VoiceMaskOverlay
│ └── LoadingOverlay
└── ConversationDrawer
聊天组件一般会接收外部传入的配置:
@Param provider: AgentProvider | null = null
@Param chatConfig: ChatConfig = new ChatConfig()
@Param bgColor: ResourceColor = ''
@BuilderParam cardsBuilder: (cards: AgentCard[]) => void = emptyCardsBuilder
@BuilderParam loadingBuilder: () => void = defaultLoadingBuilder
同时,组件内部会创建一个 ViewModel:
@Local vm: ChatViewModel = new ChatViewModel()
然后把这个 vm 传给各个子组件:
MessageList({ vm: this.vm })
InputBar({ vm: this.vm })
ConversationDrawer({ vm: this.vm })
LoadingOverlay({ vm: this.vm })
VoiceMaskOverlay({ vm: this.vm })
这说明聊天组件本身的职责主要是:
接收外部能力
创建状态中心
初始化 Controller
组合聊天 UI
把 ViewModel 分发给子组件
它本身不直接发请求,也不直接解析 AI 协议。
五、MVVM:UI 和业务之间加一层状态中介
这个模块里最明显的架构思想就是 MVVM。
MVVM 可以拆成:
Model 数据模型
View UI 展示
ViewModel 状态和交互中介
放到聊天模块里,可以对应成:
View:
AgentChatComp
MessageList
InputBar
BotBubble
ConversationDrawer
ViewModel:
ChatViewModel
Model:
ChatItem
ChatMessage
AgentCard
ConversationInfo
ChatConfig
我目前对 MVVM 的理解是:
UI 不直接干业务,业务也不直接操作 UI,中间通过 ViewModel 传状态和方法。
比如用户点击发送按钮时,UI 组件不应该自己去拼请求、调接口、解析数据,而是调用 ViewModel 暴露的方法:
this.vm.sendMessage()
然后 ViewModel 再把操作交给 Controller:
async sendMessage(): Promise<void> {
if (this.chatController !== null) {
await this.chatController.sendMessage()
}
}
业务处理完成后,Controller 再回写 ViewModel:
this.vm.chatHistory = nextMessages
this.vm.loading = false
ViewModel 状态变化后,UI 自动刷新。
所以 MVVM 的重点不是“多建一个类”,而是把 UI 和业务流程隔开。
可以记成一句话:
ViewModel 负责把用户操作转成业务动作,再把业务结果转成 UI 可以直接使用的状态。
六、ChatViewModel:聊天页面的状态中心
ViewModel 是整个聊天页面的状态中心。
它通常会保存这些状态:
- 用户输入内容
- 聊天消息列表
- 当前会话 ID
- 会话列表
- loading 状态
- 初始化状态
- 错误状态
- 推荐问题
- 待发送附件
- 语音面板状态
- 滚动状态
例如:
@ObservedV2
export class ChatViewModel {
@Trace userInput: string = ''
@Trace chatHistory: ChatItem[] = []
@Trace loading: boolean = false
@Trace conversationId: string = ''
@Trace conversations: ConversationInfo[] = []
@Trace quickPhrases: string[] = []
@Trace pendingAttachments: AttachmentInfo[] = []
@Trace showDrawer: boolean = false
@Trace initialLoaded: boolean = false
@Trace loadFailed: boolean = false
}
这里有两个比较关键的点:
@ObservedV2 修饰类
@Trace 修饰需要被 UI 追踪的状态字段
也就是说:
ViewModel 里的状态变化
↓
依赖它的 UI 组件自动刷新
例如:
InputBar 修改 vm.userInput
MessageList 读取 vm.chatHistory
ConversationDrawer 读取 vm.conversations
LoadingOverlay 读取 vm.initialLoaded / vm.loadFailed
VoiceMaskOverlay 读取 vm.showVoicePanel
这就是状态驱动 UI。
可以记成一句话:
UI 看 vm,Controller 改 vm。
七、Controller 层:复杂业务流程不要塞进 ViewModel
在简单页面里,ViewModel 可能直接写一些业务逻辑。
但是在 AI 聊天这种模块里,如果所有逻辑都写进 ViewModel,很容易变成一个特别大的类。
因为一次发送消息可能涉及很多步骤:
校验输入
处理附件
清空输入框
追加用户消息
设置 loading
创建 AI 回复占位
调用 AI 接口
处理 SSE delta
处理完整消息
解析卡片
处理停止生成
处理失败重试
同步会话
收集埋点
恢复状态
如果这些都写在 ViewModel 里,ViewModel 后期会非常难维护。
所以这里单独拆出 Controller 层:
ChatController
负责发送消息、停止生成、重试、流式回复
ConversationController
负责会话列表、会话切换、删除会话、分页加载
VoiceInputController
负责语音输入、录音状态、语音识别
ProgressiveRevealController
负责卡片或内容的渐进展示
我对这个分层的理解是:
ViewModel 管状态,Controller 管流程。
发送消息的大致流程可以理解为:
InputBar 点击发送
↓
vm.sendMessage()
↓
ChatController.sendMessage()
↓
读取 vm.userInput
↓
创建用户消息
↓
写入 vm.chatHistory
↓
设置 vm.loading = true
↓
调用 provider.sendMessage()
↓
onDelta 更新 AI 气泡
↓
onMessage 处理完整消息和卡片
↓
onReplyComplete 收尾
↓
vm.loading = false
这样做的好处是:
- UI 不关心请求细节
- ViewModel 不承载复杂流程
- Controller 专门负责把一次业务动作跑完整
八、Provider 层:平台适配与面向接口编程
AI 聊天模块可能接入不同平台,例如 Coze、Dify、OpenAI、MockProvider,或者公司内部 AI 服务。
如果 UI 组件直接依赖某个平台实现,就会产生强耦合。
例如下面这种写法就不太好:
const provider = new CozeProvider()
provider.sendMessage()
这样组件就和 Coze 绑死了。以后如果要换成 Dify,或者换成内部 AI 服务,就要改组件代码。
更好的方式是抽象一个统一接口或抽象类:
export abstract class AgentProvider {
abstract getName(): string
abstract sendMessage(
message: string,
conversationId: string,
attachments: AgentAttachment[],
onDelta: (delta: string, fullText: string) => void,
onStatus?: (status: string) => void,
onMessage?: (content: string, msgId: string) => void,
onReplyComplete?: () => void
): Promise<AgentResult>
}
然后具体平台去继承它:
export class CozeProvider extends AgentProvider {
getName(): string {
return 'Coze'
}
async sendMessage(...): Promise<AgentResult> {
// 这里写 Coze 平台的具体请求逻辑
}
}
上层组件只依赖抽象:
@Param provider: AgentProvider | null = null
这样就变成:
传 CozeProvider,就接 Coze
传 DifyProvider,就接 Dify
传 MockProvider,就可以做测试
这就是典型的解耦。
可以总结为:
组件不关心具体实现类,只关心传入对象是否满足它依赖的接口或抽象类。
九、abstract 的意义:只定规则,不干具体活
在 Provider 抽象中,经常会看到 abstract 关键字。
例如:
abstract class AgentProvider {
abstract getName(): string
abstract sendMessage(...): Promise<AgentResult>
}
abstract 的意思是:
抽象的,只定义规范,不提供完整实现。
抽象类不能直接 new:
const provider = new AgentProvider() // 不允许
它的作用是规定子类必须实现哪些方法。
比如:
任何 AI Provider 都必须有 getName()
任何 AI Provider 都必须有 sendMessage()
具体实现交给子类:
class CozeProvider extends AgentProvider {
getName(): string {
return 'Coze'
}
async sendMessage(...): Promise<AgentResult> {
// 具体平台逻辑
}
}
所以 abstract 在架构上的作用是:
定义统一规范
约束子类实现
让上层依赖抽象,而不是依赖具体类
一句话记忆:
abstract = 只定规则,不干具体活。
十、解耦:依赖抽象,而不是依赖具体实现
解耦不是简单地“多拆几个文件”。
真正的解耦,是降低模块之间的依赖关系。
一个比较好的分层应该是:
UI 层不关心请求细节
Controller 不关心底层 HTTP 实现
Provider 不关心 UI 怎么展示
HttpClient 不关心业务含义
Parser 不关心卡片怎么显示
例如:
ChatController:
我要发送消息,但我不关心你是 Coze 还是 OpenAI。
Provider:
我知道某个平台接口怎么调用,但我不关心 UI 怎么展示。
HttpClient:
我只负责发请求和解析基础流,不关心这是不是 AI 消息。
MessageList:
我只负责展示消息,不关心这条消息怎么从服务端来的。
如果不解耦,代码很容易变成:
一个页面里写 UI
一个页面里写状态
一个页面里写请求
一个页面里写 SSE
一个页面里写卡片解析
一个页面里写错误处理
一个页面里写埋点
短期可能能跑,但后期基本不好维护。
解耦后的结构是:
Page 入口和配置
View 展示
ViewModel 状态
Controller 流程
Provider 平台协议
HttpClient 请求
Parser 解析
Model 数据结构
我现在对解耦的理解是:
解耦就是让每一层只知道自己必须知道的东西。
十一、HAR 共享包:模块复用的工程结构
在 HarmonyOS 工程里,AI 聊天模块可以做成 HAR 共享包。
HAR 可以理解成:
HarmonyOS 里的共享代码包 / 组件库包 / 模块包。
它不是 exe,也不是可以双击运行的程序。
它更接近于:
前端里的 npm package
Android 里的 AAR
Java 里的 JAR
HAR 通常可以用来封装:
- 公共组件
- 工具方法
- 业务模块
- 页面能力
- 网络请求封装
- 数据模型
- 资源文件
例如在模块入口统一导出能力:
export { AgentChatComp } from './view/AgentChatComp'
export { AgentProvider } from './api/AgentProvider'
export { CozeProvider } from './api/CozeProvider'
export { HttpClient } from './utils/HttpClient'
export { ChatViewModel } from './viewmodel/ChatViewModel'
主项目里只需要这样使用:
import { AgentChatComp, CozeProvider } from 'ai_chat_module'
HAR 的价值主要是:
复用
模块化
隔离业务
减少重复代码
方便维护
这里也可以区分一下 HAR 和解耦:
HAR 是工程结构上的模块拆分
解耦是代码设计上的职责拆分
也就是说:
HAR 让代码从工程上独立出来,解耦让代码从职责上独立出来。
十二、HttpClient:网络请求统一出口
在真实项目里,不应该到处直接写 HTTP 请求,而是应该有统一的请求封装层。
一个请求封装类通常会提供:
get()
post()
put()
upload()
stream()
abortStream()
它负责处理:
- 普通请求
- 文件上传
- SSE 流式请求
- 请求中断
- 请求日志
- 敏感信息脱敏
- 超时处理
- 错误处理
也就是说:
Provider 不直接碰底层 HTTP,Provider 只调用 HttpClient。
这样可以统一处理请求头、日志、错误、超时、中断等公共问题。
1. 普通请求
普通请求比较好理解:
传入 URL
传入 header
传入 body
发出请求
拿到响应
返回结果
2. 文件上传
文件上传通常会走 multipart/form-data。
流程大概是:
读取本地文件
↓
构造 multipart/form-data
↓
上传到文件接口
↓
拿到 file_id
↓
聊天请求里传 file_id
也就是说,带附件聊天时,通常不是直接把本地路径传给 AI 接口,而是:
先上传文件,拿到 file_id,再把 file_id 放进聊天请求。
3. SSE 流式请求
AI 打字机效果一般不是前端自己用定时器假装输出,而是服务端通过 SSE 不断推送内容。
普通 HTTP 是:
请求一次
返回一次
结束
SSE 更像是:
请求一次
服务端不断返回 event
前端不断解析并更新 UI
十三、SSE 流式响应:AI 打字机效果的底层基础
SSE 全称是:
Server-Sent Events
常见格式大概是:
event: conversation.message.delta
data: {"content":"你好"}
event: conversation.message.delta
data: {"content":",我是 AI 助手"}
event: conversation.message.completed
data: {"finish_reason":"stop"}
AI 流式回复大概是:
服务端返回一小段
↓
前端解析一小段
↓
更新 AI 气泡
↓
再返回一小段
↓
再更新 AI 气泡
用户看到的效果就是“AI 正在打字”。
但是这里有一个细节比较重要:
网络流返回的数据块,不一定刚好是一个完整的 SSE 事件。
服务端可能发送的是一个完整事件:
event: xxx
data: {"content":"你好"}
但是客户端实际收到时,可能会被拆成几块:
第 1 块:event: x
第 2 块:xx\ndata: {"content"
第 3 块::"你好"}\n\n
所以前端不能拿到一块数据就直接解析,而是要做 buffer 拼包。
大概流程是:
1. 接收二进制数据块
2. 转成字符串
3. 放进 sseBuffer
4. 按空行 \n\n 拆完整事件
5. 解析 event 和 data
6. 回调给 Provider
这就是流式请求封装层的核心。
可以总结成一句话:
网络数据块不等于完整 SSE 事件,所以要先拼包再解析。
十四、停止生成:不是只改 UI 状态
AI 聊天里常见一个功能:停止生成。
一开始很容易以为停止生成只是:
loading = false
但实际上这不完整。
一个更完整的停止流程应该是:
用户点击停止生成
↓
Controller 调用 stopGenerate()
↓
Provider 调用 cancelChat()
↓
HttpClient.abortStream() 中断本地 SSE
↓
Provider 通知服务端取消当前生成
↓
保留已经生成的部分文本
↓
恢复 loading 状态
这里还要区分两种情况:
用户主动停止
网络异常中断
用户主动停止时,不应该提示“网络错误”。
网络异常中断时,才需要提示失败或者允许重试。
因此可以定义一个特殊错误类型:
StreamAbortedError
用来表示用户主动取消。
十五、请求日志与敏感信息脱敏
真实项目里,请求日志很重要。
但是日志不能随便打印敏感信息。
常见敏感字段有:
authorization
cookie
token
openId
x-api-key
set-cookie
请求封装层应该统一做脱敏处理。
例如日志中只显示:
authorization: *** (len=32)
而不是打印真实 token。
这里体现的是工程安全意识:
日志要能帮助排查问题,但不能泄露用户身份、token、cookie 等敏感信息。
这也是 Demo 代码和真实业务代码的一个明显区别。
十六、数据模型:让消息、会话、卡片结构清晰
AI 聊天模块里常见的数据模型包括:
ChatItem
UI 上展示的一条消息
ChatMessage
普通消息数据
AgentCard
AI 返回的业务卡片
ConversationInfo
会话信息
ChatConfig
聊天组件配置
AgentResult
AI 返回结果
AgentAttachment
附件信息
这些模型的意义是:
不要让所有数据都用 any 或 JSON 字符串到处乱传,而是定义清楚每种数据长什么样。
例如:
export class ChatItem {
role: string = ''
content: string = ''
time: string = ''
cards: AgentCard[] = []
attachments: AttachmentInfo[] = []
}
这样 UI 层、Controller 层、Provider 层之间传递数据时会更清楚。
十七、数组更新与响应式刷新
在 ArkUI 响应式场景中,数组更新要特别注意。
例如直接这样写:
this.chatHistory.push(newItem)
有时不如替换数组引用稳定。
更推荐:
this.chatHistory = this.chatHistory.concat(newItem)
或者:
this.chatHistory = this.chatHistory.map(item => {
if (item.id === targetId) {
return {
...item,
content: newContent
}
}
return item
})
核心思想是:
通过生成新数组,改变引用,触发 UI 更稳定刷新。
这和聊天打字机效果也有关系。
例如 AI 回复时,如果只是修改同一个对象里的 content,但列表 key 没变,有时 UI 不一定按预期刷新。
可以通过下面几种方式保证刷新稳定:
更新数组引用
更新对象引用
合理设置 ForEach key
十八、Builder 参数:让组件支持自定义 UI
聊天模块中经常需要支持业务卡片。
不同业务返回的卡片可能完全不同:
路线卡片
景点卡片
票务卡片
商品卡片
订单卡片
推荐问题卡片
如果聊天组件内部写死所有卡片 UI,组件就很难复用。
更好的方式是通过 Builder 参数传入:
@BuilderParam cardsBuilder: (cards: AgentCard[]) => void
外部页面自己决定这些卡片怎么画:
@Builder
cardsBuilder(cards: AgentCard[]) {
RouteCardList({
cards: cards.filter(item => item.cardType === 'route')
})
PoiCardList({
cards: cards.filter(item => item.cardType === 'poi')
})
}
这样聊天组件只负责:
我有一批 cards
我把它交给外部 cardsBuilder
它不关心具体业务卡片长什么样。
这也是一种解耦:
聊天组件负责通用聊天能力
业务页面负责具体业务卡片展示
十九、ChatConfig:把业务差异配置化
一个通用聊天组件不应该写死所有业务行为。
可以通过 ChatConfig 注入不同业务需要的配置:
- 欢迎语
- 推荐问题
- 动态推荐问题加载
- 抽屉配置
- 链接点击回调
- 埋点回调
- 业务跳转回调
- loading 样式
- 错误样式
例如:
const config = new ChatConfig()
config.welcomeMessage = '你好,我是 AI 助手'
config.quickPhrases = ['问题 1', '问题 2']
config.onLinkClick = (url, text) => {
// 业务页面决定怎么打开链接
}
config.onTrackEvent = (event) => {
// 业务页面决定怎么上报埋点
}
这样组件内部不用关心具体业务平台怎么跳转、怎么埋点、怎么生成推荐问题。
可以总结为:
通用组件通过配置和回调接入业务能力。
二十、完整主链路
到这里,可以把整个 AI 聊天模块的主链路串起来:
用户输入问题
↓
InputBar 触发发送
↓
ChatViewModel 暴露 sendMessage 入口
↓
ChatController 编排发送流程
↓
创建用户消息,写入 chatHistory
↓
设置 loading 状态
↓
调用 AgentProvider.sendMessage
↓
具体 Provider 构造平台请求
↓
HttpClient.stream 发起 SSE 请求
↓
服务端不断返回 event/data
↓
HttpClient 解析出 eventType 和 eventData
↓
Provider 解析平台协议
↓
onDelta 回调给 ChatController
↓
ChatController 更新 AI 消息内容
↓
ChatViewModel 状态变化
↓
MessageList / BotBubble 自动刷新
↓
回复完成后收尾,恢复 loading
这条链路很重要。
只要能把这条链路讲清楚,大多数 AI 聊天项目的结构就不会看乱。
二十一、这套架构体现的核心思想
1. MVVM
View 负责展示
ViewModel 负责状态
Model 负责数据结构
2. Controller 编排
复杂业务流程不要全部塞进 ViewModel。
Controller 负责把一次完整业务流程串起来。
3. Provider 适配器
上层依赖统一接口。
不同 AI 平台各自实现自己的 Provider。
4. 请求统一封装
所有网络请求集中在 HttpClient。
统一处理:
日志
错误
上传
SSE
中断
超时
脱敏
5. 组件化
大页面拆成:
消息列表
输入栏
抽屉
气泡
卡片
蒙层
每个组件只负责自己的展示和交互。
6. 配置化
业务差异通过 ChatConfig 和 BuilderParam 注入。
通用组件不写死业务逻辑。
7. 解耦
UI 不关心请求
请求不关心 UI
Provider 不关心展示
HttpClient 不关心业务语义
二十二、可以背下来的架构口诀
Page 负责入口
Comp 负责 UI
ViewModel 负责状态
Controller 负责编排
Provider 负责平台适配
HttpClient 负责请求
Parser 负责解析
Model 负责数据结构
Card 负责展示
再短一点:
UI 看 vm,Controller 改 vm,Provider 接平台,HttpClient 发请求。
再总结成一句:
把用户看到的 UI、页面状态、业务流程、平台协议和网络请求拆开,各自负责自己的事情。
二十三、实习阶段应该怎么读这种项目
第一次看公司项目,不要试图一次看懂所有文件。
我觉得可以按这个顺序来:
第一遍:看目录结构,知道每个目录大概干什么
第二遍:看页面入口,知道从哪里进来
第三遍:看核心组件,知道 UI 怎么组合
第四遍:看 ViewModel,知道状态有哪些
第五遍:看 Controller,知道一次业务流程怎么跑
第六遍:看 Provider,知道接口平台怎么适配
第七遍:看 HttpClient,知道请求和 SSE 怎么封装
第八遍:看 Parser 和 Card,知道数据怎么渲染成业务 UI
不要一开始就陷进某个很长的文件里。
应该先建立地图:
入口在哪?
状态在哪?
发送从哪开始?
请求在哪发?
结果回到哪里?
UI 怎么刷新?
有了主线之后,再逐个文件深入。
二十四、当前阶段学习总结
通过这一阶段的源码阅读,我对一个 AI 聊天模块的工程化设计有了更清楚的认识。
它并不是简单地在页面里写一个输入框和消息数组,而是通过多层架构把复杂能力拆开:
页面入口负责装配
聊天组件负责 UI 总装
ViewModel 负责状态管理
Controller 负责业务流程
Provider 负责 AI 平台适配
HttpClient 负责网络请求和 SSE
Model 负责数据结构
Builder 负责业务 UI 扩展
这套结构里体现了很多常见的工程化思想:
MVVM
解耦
面向接口编程
组件化
配置化
请求统一封装
状态驱动 UI
对于实习阶段来说,我觉得目前最重要的不是马上把每个方法都背下来,而是先理解:
为什么要这么分层?
每一层负责什么?
一次发送消息从 UI 到请求再回到 UI 是怎么流转的?
理解了这些,再去看具体方法实现,就不会那么容易迷路。
二十五、最终总结
一个复杂 AI 聊天模块的核心,不只是“页面怎么画”,而是如何把 UI、状态、业务流程、接口协议、网络请求、数据解析拆清楚。
其中:
MVVM 解决 UI 和状态之间的关系
Controller 解决复杂业务流程编排的问题
Provider 解决不同 AI 平台适配的问题
HttpClient 解决请求统一封装和 SSE 流式响应的问题
Builder 和 Config 解决组件可扩展和业务差异注入的问题
HAR 共享包解决模块级复用的问题
最终形成的结构可以概括为:
通过 ViewModel 打通 UI 和状态
通过 Controller 编排业务流程
通过 Provider 屏蔽平台差异
通过 HttpClient 统一网络请求
通过 Config 和 Builder 保持组件可复用
这就是目前从 AI 聊天模块源码中总结出来的主要架构理解。
更多推荐


所有评论(0)