微信小程序 Agent Chat 前端技术方案
基于纯内存、无本地缓存的轻量化方案
一、核心功能
| 功能 | 说明 |
|---|---|
| 对话 | 当前会话的消息收发,支持流式输出 |
| 查看历史记录 | 历史会话列表,可预览最后一条消息 |
| 历史记录继续对话 | 点击历史会话,恢复上下文继续对话 |
| 点踩 | 对每条 AI 回复进行负反馈标记 |
二、数据结构
2.1 会话(Session)
interface Session {
id: string; // 后端返回的会话唯一 ID
title: string; // 会话标题(取第一条用户消息截断)
createdAt: number; // 创建时间戳
updatedAt: number; // 最后更新时间戳(用于列表排序)
messageCount: number; // 消息总数
lastMessage: { // 最后一条消息摘要,用于列表预览
role: 'user' | 'assistant';
content: string; // 截断文本(最多 50 字)
};
status: 'active' | 'archived';
}
2.2 消息(Message)
interface Message {
id: string; // 后端返回的真实 ID;发送中先用 tempId
tempId?: string; // 前端生成的临时 ID,收到后端响应后替换
sessionId: string; // 归属会话 ID
role: 'user' | 'assistant' | 'system';
content: string; // 消息正文(text / markdown)
contentType: 'text' | 'markdown' | 'error';
createdAt: number; // 时间戳
// 发送状态(仅 user 消息)
status?: 'pending' // 已 push 本地,后端响应未返回
| 'sent' // 后端已确认
| 'failed'; // 请求失败,可重试
// 流式状态(仅 assistant 消息,纯内存)
streaming?: boolean; // 是否正在流式输出
// 点踩反馈
feedback?: {
dislike: boolean; // 是否已点踩
dislikeAt?: number; // 点踩时间(可选)
};
// Agent 工具调用(可选)
toolCalls?: ToolCall[];
}
interface ToolCall {
id: string;
name: string;
input: Record<string, unknown>;
output?: string;
status: 'pending' | 'success' | 'error';
}
三、Store 设计
两个 Store,职责分离:
sessionStore → 管会话列表(拉取、分页、增删切)
messageStore → 管当前会话消息 + 对话状态(发送、流式、点踩)
messageStore → 管当前会话消息 + 对话状态(发送、流式、点踩)
3.1 sessionStore
const sessionStore = {
// ===== State =====
sessions: Session[], // 当前会话列表(内存)
currentSessionId: string | null,
loading: boolean, // 列表加载中
hasMore: boolean, // 是否还有更多历史(分页)
pageNo: number, // 当前页
// ===== Computed =====
get currentSession(): Session | null,
// ===== Actions =====
// 拉取会话列表(首次 / 下拉刷新)
fetchSessions(reset?: boolean): Promise<void>,
// 加载更多(上拉分页)
fetchMore(): Promise<void>,
// 创建新会话
createSession(): Promise<Session>,
// 删除会话
deleteSession(id: string): Promise<void>,
// 切换当前会话(仅切内存指针,消息由 messageStore 处理)
setCurrentSession(id: string | null): void,
// 本地更新当前会话摘要(消息发送/接收后更新 lastMessage 和 updatedAt)
updateCurrentMeta(patch: Partial<Session>): void,
}
3.2 messageStore
const messageStore = {
// ===== State =====
messages: Message[], // 当前会话消息(内存,切换会话时清空)
sessionId: string | null, // 当前加载的是哪个会话
loading: boolean, // 消息加载中
sending: boolean, // 发送中 / 等待响应中
streamingMsgId: string | null, // 正在流式输出的消息 ID
inputText: string, // 输入框内容(方便组件解耦)
error: string | null, // 最近一次错误
// ===== Computed =====
get streamingMessage(): Message | null,
// ===== Actions =====
// 加载某会话的消息(切换会话时调用)
loadMessages(sessionId: string): Promise<void>,
// 离开会话时清空内存
clearMessages(): void,
// 发送消息(包含乐观更新 + 流式处理)
sendMessage(content: string): Promise<void>,
// 中断流式输出
stopStreaming(): void,
// ===== 内部方法 =====
// 追加流式文本块
_appendChunk(msgId: string, chunk: string): void,
// 流式结束,标记完成
_finalizeStream(msgId: string): void,
// 替换临时 ID 为真实 ID
_replaceId(tempId: string, realId: string): void,
// 点踩 / 撤销点踩
_setDislike(msgId: string, val: boolean): Promise<void>,
}
四、消息 ID 生命周期
核心原则:前端只负责生成临时 ID 用于乐观渲染,真实 ID 全部来自后端。
发送流程
① 用户点发送
├─ 前端生成 tempId(如 "tmp_1745510000123")
├─ 本地 push 一条 user message { id: tempId, status: 'pending' }
└─ UI 立即渲染(用户感觉"秒发")
② POST /sessions/:id/messages { content }
└─ 后端返回 { id: "msg_abc123", ... }
③ 前端用 messageId 替换 tempId
└─ messages 中找到 tempId 那条,id → msg_abc123,status → 'sent'
④ assistant 消息(流式)
├─ 流式第一帧返回 assistant message id(如 SSE: { id: "msg_xxx", ... })
├─ 前端创建 assistant message(streaming: true)
└─ 后续 chunk 都 append 到这个 id 上
⑤ 流式结束
└─ streaming: false,消息完整
⑥ 更新 sessionStore 当前会话摘要
└─ lastMessage + updatedAt
失败处理
POST 失败
└─ status → 'failed',UI 展示"发送失败,点击重试"
└─ 点击重试 → 复用同一个 tempId 重新发,成功后替换真实 id
点踩时机
点踩按钮 → 只对 status === 'sent' 的消息生效
pending / failed 状态不展示点踩按钮
发送 PATCH /messages/:id/feedback { dislike: true }
五、数据流全景
flowchart TD
A["用户进入会话"] --> B["sessionStore.setCurrentSession(id)"]
B --> C["messageStore.loadMessages(id)\nGET /sessions/:id/messages"]
C --> D["messages 写入内存"]
D --> E["渲染消息列表"]
F["用户发送消息"] --> G["生成 tempId,push user message (pending)\ninputText 清空,sending = true"]
G --> H["POST /sessions/:id/messages"]
H --> I["① 后端返回真实 id → 替换 tempId,status = 'sent'"]
H --> J["② 流式返回 assistant id → 创建空 message"]
J --> K["③ chunk 逐个 _appendChunk() → streaming = true"]
K --> L["④ 流式结束 → _finalizeStream()"]
L --> M["sessionStore.updateCurrentMeta({\n lastMessage: { role: 'assistant', content: '...' },\n updatedAt: Date.now()\n})"]
M --> N["渲染(实时 / 最终态)"]
O["用户点踩"] --> P["乐观更新:feedback.dislike = true(立即变 UI)"]
P --> Q["PATCH /messages/:id/feedback { dislike: true }\n失败 → 回滚 dislike 状态"]
Q --> R["完成"]
数据流全景图
六、页面与路由
/pages/chat/index → 当前对话页(默认新会话)
/pages/chat/index?sid=xxx → 恢复指定历史会话
/pages/history/index → 历史会话列表页
| 页面 | 职责 |
|---|---|
| chat | 对话区 + 消息列表 + 输入框 |
| history | 会话列表,支持下拉刷新 / 上拉分页 |
页面切换时的 Store 操作:
从 history → chat
├─ sessionStore.setCurrentSession(id)
└─ messageStore.loadMessages(id)
从 chat → history
├─ messageStore.clearMessages()
└─ sessionStore.fetchSessions()(刷新列表)
七、内存管理策略
- 永远只持有当前会话的消息,切换会话时清空旧消息
- 进入已加载过的会话时,判断
sessionId是否一致 +messages.length > 0,相同则跳过请求 - App 切后台时不需要主动清空(小程序生命周期天然隔离)
八、点踩交互
| 项目 | 说明 |
|---|---|
| 展示条件 | status === 'sent' 且 role === 'assistant' |
| 乐观更新 | 点击后立即变色,不等请求返回 |
| 请求方式 | PATCH /messages/:id/feedback { dislike: boolean } |
| 失败回滚 | 静默重试 1 次,仍失败则回滚 UI 状态 |
| 撤销 | 再次点击 dislike: false 即可 |
九、API 契约(前端视角)
| 操作 | 方法 | 路径 | 请求体 | 响应 |
|---|---|---|---|---|
| 获取会话列表 | GET | /sessions |
?page=&size= |
{ list: Session[], total, hasMore } |
| 创建会话 | POST | /sessions |
{} |
Session |
| 删除会话 | DELETE | /sessions/:id |
- | - |
| 获取消息列表 | GET | /sessions/:id/messages |
?before= |
{ list: Message[], hasMore } |
| 发送消息 | POST | /sessions/:id/messages |
{ content } |
Message(非流式) |
| 流式发送 | POST | /sessions/:id/messages |
{ content } |
SSE 流 |
| 点踩 | PATCH | /messages/:id/feedback |
{ dislike } |
Message |
十、技术栈建议
| 层级 | 推荐方案 |
|---|---|
| 状态管理 | MobX(mobx-miniprogram)或 Pinia |
| 网络请求 | Fly.js / wx.request 封装 |
| 流式接收 | wx.connectSocket + onSocketMessage(SSE) |
| 样式 | CSS Modules / Scss |
| 页面间传参 | onLoad(options) 接收 sid 参数 |
十一、边界处理
| 场景 | 处理 |
|---|---|
| 流式中途切走页面 | 记录 streamingMsgId,返回后继续追加 chunk |
| 网络断开 | sending = false,展示重试按钮 |
| 空会话(无消息) | 显示欢迎语占位,不发请求 |
| 加载历史消息 | 分页向前加载,prepend 到 messages 数组头部 |
| 流式中断 | stopStreaming() → streaming: false,保留已有内容 |