🏠

微信小程序 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 → 管当前会话消息 + 对话状态(发送、流式、点踩)

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()(刷新列表)

七、内存管理策略


八、点踩交互

项目 说明
展示条件 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,保留已有内容