AI UI生成系统的多轮对话实现:从增量更新到智能合并(附完整源码)
本文深入解析了AI UI Web系统的多轮对话实现机制。系统面临三大核心挑战:Token超限问题、数据合并问题和增量更新识别。解决方案采用token优化策略(超过1500则使用摘要)、深度合并算法和智能增量更新识别技术。前端通过Pinia状态管理实现数据存储,采用deepMerge+mergeArrays算法处理数据合并,确保多轮对话中UI状态的正确更新。文章详细展示了发送消息的核心逻辑和错误处理
·
前言
在前两篇文章中,我们介绍了AI UI Web系统的基本架构、提示词工程和组件设计。很多读者在体验后发现:系统不仅能理解单次请求,还能记住上下文,通过多轮对话逐步完善界面。
“帮我生成一个用户管理页面”
“再添加一个订单管理菜单”
“为用户管理页面添加一个表格”
这种连续的对话体验是如何实现的?今天,我将深入剖析AI UI Web系统的多轮对话实现机制,分享从增量更新、智能合并到token优化的完整技术方案。
一、多轮对话的核心挑战
1.1 为什么多轮对话这么难?
在实际开发中,我们遇到了以下核心问题:
挑战一:Token超限问题
第一次请求:生成完整系统(2000 tokens)
第二次请求:AI需要知道当前UI状态(2000 tokens)
第三次请求:对话历史 + 当前UI状态(4000+ tokens)
结果:超过API的token限制,导致请求失败或数据丢失
挑战二:数据合并问题
用户:"添加一个用户管理菜单"
AI返回:{ "menuItems": [{ "index": "users", "title": "用户管理" }] }
用户:"为用户管理添加表格"
AI返回:{ "pages": { "users": { "type": "table", ... } } }
问题:如何将这两次返回的数据正确合并?
挑战三:增量更新识别
用户:"修改用户管理的标题"
AI应该返回:{ "pages": { "users": { "title": "新标题" } } }
而不是:{ "pages": { "users": { ...完整配置 } } }
如何让AI理解"只返回变更部分"?
1.2 我们的解决方案
前端发送请求
↓
后端检测当前UI的token数量
↓
token > 1500?使用摘要:使用完整UI
↓
构建多轮对话提示词
↓
调用AI API
↓
AI返回增量更新
↓
智能合并数据(deepMerge + mergeArrays)
↓
保存到数据库
↓
返回合并后的完整UI
二、前端状态管理
2.1 Pinia Store实现
// frontend/src/stores/chat.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { generateUI, conversationApi } from '../api'
export const useChatStore = defineStore('chat', () => {
const messages = ref([])
const loading = ref(false)
const uiData = ref(null)
const inputMessage = ref('')
const progress = ref('')
const currentConversationId = ref(null)
const conversations = ref([])
const addMessage = (role, content) => {
messages.value.push({ role, content })
}
const setLoading = (state) => {
loading.value = state
}
const setUIData = (ui) => {
uiData.value = ui
}
const setProgress = (p) => {
progress.value = p
}
const setCurrentConversationId = (id) => {
currentConversationId.value = id
}
const setConversations = (list) => {
conversations.value = list
}
2.2 深度合并算法
const deepMerge = (target, source) => {
const result = { ...target }
for (const key in source) {
if (source[key] instanceof Object && key in target && target[key] instanceof Object && !Array.isArray(source[key])) {
result[key] = deepMerge(target[key], source[key])
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
result[key] = mergeArrays(target[key], source[key], key)
} else {
result[key] = source[key]
}
}
return result
}
const mergeArrays = (targetArray, sourceArray, key) => {
if (sourceArray.length === 0) {
return targetArray
}
if (targetArray.length === 0) {
return sourceArray
}
const result = [...targetArray]
for (const sourceItem of sourceArray) {
let existingIndex = -1
if (sourceItem.prop !== undefined) {
existingIndex = result.findIndex(item => item.prop === sourceItem.prop)
} else if (sourceItem.index !== undefined) {
existingIndex = result.findIndex(item => item.index === sourceItem.index)
} else if (sourceItem.name !== undefined) {
existingIndex = result.findIndex(item => item.name === sourceItem.name)
} else if (sourceItem.type !== undefined) {
existingIndex = result.findIndex(item => item.type === sourceItem.type)
}
if (existingIndex !== -1) {
result[existingIndex] = deepMerge(result[existingIndex], sourceItem)
} else {
result.push(sourceItem)
}
}
return result
}
const mergeUIData = (newUI) => {
if (!uiData.value) {
uiData.value = newUI
return
}
const merged = deepMerge(uiData.value, newUI)
uiData.value = merged
}
2.3 发送消息的核心逻辑
const sendMessage = async (message) => {
try {
console.log('chatStore sendMessage called with:', message)
setLoading(true)
const conversationHistory = messages.value.map(msg => ({
role: msg.role,
content: msg.content
}))
console.log('Calling generateUI with:', {
message,
conversationHistory,
currentUI: uiData.value,
conversationId: currentConversationId.value
})
const result = await generateUI(message, conversationHistory, uiData.value, currentConversationId.value)
console.log('generateUI result:', result)
if (result) {
console.log('Received UI data:', result)
mergeUIData(result)
addMessage('assistant', '界面生成成功')
} else {
console.log('No UI data received')
addMessage('assistant', '生成失败: 未收到UI数据')
}
} catch (error) {
console.error('生成UI失败:', error)
let errorMessage = '生成失败,请重试'
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
errorMessage = '请求超时,AI API 响应时间过长。请稍后重试或简化您的需求。'
} else if (error.response?.status === 504) {
errorMessage = '请求超时,AI API 响应时间过长。请稍后重试或简化您的需求。'
} else if (error.response?.data?.error) {
errorMessage = error.response.data.error
} else if (error.message) {
errorMessage = `生成失败: ${error.message}`
}
addMessage('assistant', errorMessage)
throw error
} finally {
setLoading(false)
setProgress('')
}
}
三、后端核心实现
3.1 Token检测机制
// backend/server.js
function estimateTokens(text) {
if (!text) return 0;
return Math.ceil(text.length / 4);
}
function generateUISummary(uiData) {
if (!uiData) return null;
const summary = {
type: uiData.type,
title: uiData.title,
description: uiData.description
};
if (uiData.menuItems) {
summary.menuSummary = {
count: uiData.menuItems.length,
indices: uiData.menuItems.map(m => m.index),
structure: uiData.menuItems.map(m => ({
index: m.index,
title: m.title,
hasChildren: !!(m.children && m.children.length > 0),
children: m.children ? m.children.map(c => c.index) : []
}))
};
}
if (uiData.pages) {
summary.pagesSummary = {
count: Object.keys(uiData.pages).length,
keys: Object.keys(uiData.pages),
pageTypes: Object.fromEntries(
Object.entries(uiData.pages).map(([key, page]) => [key, page.type])
)
};
}
if (uiData.contentType) {
summary.contentType = uiData.contentType;
}
if (uiData.contentTitle) {
summary.contentTitle = uiData.contentTitle;
}
return summary;
}
3.2 多轮对话API实现
app.post('/api/generate-ui', authMiddleware, requirePermission('chat'), async (req, res) => {
const { userQuery, conversationHistory, currentUI, conversationId, targetPage } = req.body;
console.log('[Generate UI] Request received:', {
userQuery,
conversationHistoryLength: conversationHistory?.length,
hasCurrentUI: !!currentUI,
conversationId,
targetPage
});
const startTime = Date.now();
let messages = [
{
role: 'system',
content: `You are an AI UI generator for admin management systems. Your task is to generate complete admin interface STRUCTURES based on user queries.
IMPORTANT: You should ONLY generate UI STRUCTURES and CONFIGURATIONS. DO NOT include any actual business data or mock data in your response. The data will be fetched separately through a dedicated mock data API.
CRITICAL GENERATION RULES - MUST FOLLOW:
1. LIMIT TO ONE PAGE PER REQUEST: Always generate ONLY ONE page at a time
2. FIRST REQUEST (no existing UI): Generate admin system framework with ONLY dashboard page and menu structure
3. SUBSEQUENT REQUESTS: Generate ONLY the specific page requested by the user
4. NEVER generate multiple pages in a single response - this will cause token overflow and data loss
5. Always include "generationStatus" field when there are remaining pages to generate
CRITICAL INSTRUCTION - INCREMENTAL GENERATION FOR MULTI-TURN CONVERSATIONS:
This is a multi-turn conversation system. When currentUISummary is provided (meaning this is NOT the first request), you MUST follow these rules:
1. NEVER return the complete UI structure - this will cause token overflow and data loss
2. ONLY return CHANGED/ADDED/UPDATED parts
3. The backend will automatically merge your response with the existing UI state
When currentUISummary is provided:
- For adding a new menu item: return only { "menuItems": [...new item] }
- For updating an existing menu: return only { "menuItems": [{ "index": "existing_id", ...updated fields }] }
- For adding a new page: return only { "pages": { "menu_index": {...} } }
- For updating an existing page: return only { "pages": { "menu_index": {...updated data} } }
- For modifying a specific field: return only that field
- For deleting a menu/page: return { "menuItems": [] } or { "pages": {} } with the item removed
When t ecified:
- Generate ONLY that specific page: { "pages": { "targetPage": {...} } }
- Do NOT include other pages or menu items unless explicitly requested
Example responses when currentUISummary is provided:
- "添加一个订单管理菜单" → { "menuItems": [{ "index": "orders", "title": "订单管理", "icon": "Tickets" }] }
- "为用户管理页面添加表格" → { "pages": { "users": { "title": "用户管理", "type": "table", "data": {...config_only...} } } }
- "修改仪表盘的标题" → { "pages": { "dashboard": { "title": "新标题" } } }
- "删除商品管理菜单" → { "menuItems": [] } (with products removed)
`.trim()
}
];
if (conversationHistory && conversationHistory.length > 0) {
messages = messages.concat(conversationHistory);
}
if (currentUI) {
const currentUITokens = estimateTokens(JSON.stringify(currentUI));
console.log('[Generate UI] Current UI tokens:', currentUITokens);
if (currentUITokens > 1500) {
console.log('[Generate UI] Current UI too large, using summary');
const summary = generateUISummary(currentUI);
messages.push({
role: 'system',
content: `Current UI summary: ${JSON.stringify(summary)}.\n\nCRITICAL: This is a multi-turn conversation. Generate ONLY the CHANGES needed based on the user query. DO NOT return the complete UI structure.\n\nExisting structure:\n- Type: ${summary.type}\n- Menu items: ${summary.menuSummary?.count || 0} (${summary.menuSummary?.indices?.join(', ') || 'none'})\n- Pages: ${summary.pagesSummary?.count || 0} (${summary.pagesSummary?.keys?.join(', ') || 'none'})\n\nWhen the user asks to add/modify something, return ONLY the changed parts (e.g., just the new menu item, just the updated page, etc.). The backend will merge your response with the existing UI state.`
});
} else {
messages.push({
role: 'system',
content: `Current UI state: ${JSON.stringify(currentUI)}.\n\nCRITICAL: This is a multi-turn conversation. Generate ONLY the CHANGES needed based on the user query. DO NOT return the complete UI structure. The backend will merge your response with the existing UI state.`
});
}
}
messages.push({
role: 'user',
content: userQuery
});
try {
const completion = await openai.chat.completions.create({
model: 'qwen-plus',
messages: messages,
temperature: 0.7,
max_tokens: 4000
});
const aiResponse = completion.choices[0].message.content;
console.log('[Generate UI] AI response length:', aiResponse.length);
const parsedData = parseJSONFromContent(aiResponse);
console.log('[Generate UI] Parsed data:', parsedData);
let finalUIData = parsedData;
if (currentUI) {
console.log('[Generate UI] Merging with existing UI');
finalUIData = deepMerge(currentUI, parsedData);
console.log('[Generate UI] Merged UI data:', finalUIData);
}
if (conversationId) {
const existingMessages = await dao.getMessages(conversationId);
if (!existingMessages || existingMessages.length === 0) {
await dao.updateConversation(conversationId, { title: userQuery });
}
await dao.addMessage(conversationId, 'user', userQuery);
await dao.addMessage(conversationId, 'assistant', '界面生成成功');
const existingUIData = await dao.getUIData(conversationId);
if (existingUIData) {
const changedPages = identifyChangedPages(existingUIData, finalUIData);
console.log('[Generate UI] Changed pages:', changedPages);
if (changedPages.length > 0) {
for (const pageKey of changedPages) {
if (pageKey === '*') {
console.log('[Generate UI] Clearing all mock data for conversation:', conversationId);
await dao.deleteAllComponentMockData(conversationId);
} else {
console.log('[Generate UI] Clearing mock data for page:', pageKey);
await dao.deleteComponentMockDataByPage(conversationId, pageKey);
}
}
}
}
await dao.updateUIData(conversationId, finalUIData);
}
res.json(finalUIData);
} catch (error) {
console.error('[Generate UI] Error:', error);
res.status(500).json({ error: 'Failed to generate UI', details: error.message });
}
});
3.3 深度合并实现
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] instanceof Object && key in target && target[key] instanceof Object && !Array.isArray(source[key])) {
result[key] = deepMerge(target[key], source[key]);
} else if (Array.isArray(source[key]) && Array.isArray(target[key])) {
result[key] = mergeArrays(target[key], source[key], key);
} else {
result[key] = source[key];
}
}
return result;
}
function mergeArrays(targetArray, sourceArray, key) {
if (sourceArray.length === 0) {
return targetArray;
}
if (targetArray.length === 0) {
return sourceArray.filter(item => item !== undefined && item !== null);
}
const result = [...targetArray];
for (const sourceItem of sourceArray) {
if (sourceItem === undefined || sourceItem === null) {
continue;
}
let existingIndex = -1;
if (typeof sourceItem === 'string') {
existingIndex = result.findIndex(item => typeof item === 'string' && item === sourceItem);
} else if (sourceItem.prop !== undefined) {
existingIndex = result.findIndex(item => item && item.prop === sourceItem.prop);
} else if (sourceItem.index !== undefined) {
existingIndex = result.findIndex(item => item && item.index === sourceItem.index);
} else if (sourceItem.name !== undefined) {
existingIndex = result.findIndex(item => item && item.name === sourceItem.name);
} else if (sourceItem.type !== undefined) {
existingIndex = result.findIndex(item => item && item.type === sourceItem.type);
} else if (sourceItem.value !== undefined) {
existingIndex = result.findIndex(item => item && item.value === sourceItem.value);
}
if (existingIndex !== -1) {
result[existingIndex] = deepMerge(result[existingIndex], sourceItem);
} else {
result.push(sourceItem);
}
}
return result.filter(item => item !== undefined && item !== null);
}
3.4 变更页面识别
function identifyChangedPages(existingData, newData) {
const changedPages = [];
if (!existingData) {
console.log('[identifyChangedPages] No existing data, all new pages are changed');
return Object.keys(newData.pages || {});
}
const existingPages = existingData.pages || {};
const newPages = newData.pages || {};
for (const pageKey of Object.keys(newPages)) {
const existingPage = existingPages[pageKey];
const newPage = newPages[pageKey];
if (!existingPage) {
console.log('[identifyChangedPages] New page added:', pageKey);
changedPages.push(pageKey);
} else {
const existingPageStr = JSON.stringify(existingPage);
const newPageStr = JSON.stringify(newPage);
if (existingPageStr !== newPageStr) {
console.log('[identifyChangedPages] Page modified:', pageKey, {
existingKeys: Object.keys(existingPage),
newKeys: Object.keys(newPage)
});
changedPages.push(pageKey);
}
}
}
if (newData.data && !newData.pages) {
console.log('[identifyChangedPages] Top-level data changed, clearing all mock data for conversation');
return ['*'];
}
return changedPages;
}
四、多轮对话的完整流程
4.1 第一次请求(创建系统)
// 前端发送
{
userQuery: "帮我生成一个电商后台管理系统",
conversationHistory: [],
currentUI: null,
conversationId: null
}
// 后端处理
1. 检测到currentUI为null
2. 不使用摘要,直接发送system prompt
3. AI返回完整系统结构
// AI返回
{
"type": "admin",
"title": "电商后台管理系统",
"menuItems": [
{ "index": "dashboard", "title": "仪表盘", "icon": "Odometer" },
{ "index": "products", "title": "商品管理", "icon": "Goods" }
],
"pages": {
"dashboard": {
"title": "仪表盘",
"type": "dashboard",
"data": { ... }
}
},
"generationStatus": {
"status": "partial",
"message": "已生成基础框架和仪表盘页面,可以继续生成其他页面",
"remainingPages": ["products"]
}
}
// 后端保存
1. 保存到数据库
2. 返回完整UI数据
4.2 第二次请求(添加页面)
// 前端发送
{
userQuery: "为商品管理页面添加表格",
conversationHistory: [
{ role: "user", content: "帮我生成一个电商后台管理系统" },
{ role: "assistant", content: "界面生成成功" }
],
currentUI: { ...完整UI数据 },
conversationId: "conv_123"
}
// 后端处理
1. 检测到currentUI存在
2. 计算token数量:假设为1200(小于1500)
3. 不使用摘要,发送完整UI状态
4. 在system prompt中明确说明:只返回变更部分
// AI返回(只返回变更部分)
{
"pages": {
"products": {
"title": "商品管理",
"type": "table",
"data": {
"contentConfig": {
"cols": [
{ "prop": "id", "label": "商品ID" },
{ "prop": "name", "label": "商品名称" },
{ "prop": "price", "label": "价格" }
]
}
}
}
}
}
// 后端处理
1. 使用deepMerge将AI返回的增量数据合并到现有UI
2. 识别变更的页面:["products"]
3. 清除products页面的mock数据
4. 保存合并后的完整UI到数据库
5. 返回合并后的完整UI
4.3 第三次请求(token超限情况)
// 前端发送
{
userQuery: "添加一个订单管理菜单",
conversationHistory: [ ... ],
currentUI: { ...大型UI数据 },
conversationId: "conv_123"
}
// 后端处理
1. 检测到currentUI存在
2. 计算token数量:假设为2500(大于1500)
3. 生成UI摘要
// UI摘要
{
"type": "admin",
"title": "电商后台管理系统",
"menuSummary": {
"count": 2,
"indices": ["dashboard", "products"],
"structure": [
{ "index": "dashboard", "title": "仪表盘", "hasChildren": false },
{ "index": "products", "title": "商品管理", "hasChildren": false }
]
},
"pagesSummary": {
"count": 2,
"keys": ["dashboard", "products"],
"pageTypes": {
"dashboard": "dashboard",
"products": "table"
}
}
}
// 发送摘要而非完整UI
messages.push({
role: 'system',
content: `Current UI summary: ${JSON.stringify(summary)}.\n\nCRITICAL: This is a multi-turn conversation. Generate ONLY the CHANGES needed based on the user query.`
});
// AI返回(只返回变更部分)
{
"menuItems": [
{ "index": "orders", "title": "订单管理", "icon": "Tickets" }
]
}
// 后端处理
1. 使用mergeArrays将新菜单项合并到现有menuItems
2. 保存合并后的完整UI到数据库
3. 返回合并后的完整UI
五、关键技术点总结
5.1 Token优化策略
// Token检测阈值
const UI_TOKEN_THRESHOLD = 1500;
// 摘要生成
function generateUISummary(uiData) {
// 只保留关键信息:
// 1. 类型、标题、描述
// 2. 菜单项数量和索引
// 3. 页面数量和类型
// 4. 内容类型和标题
}
// 摘要效果
完整UI:2000+ tokens
摘要UI:300-500 tokens
节省:75%+ tokens
5.2 增量更新提示词
// 关键提示词内容
"CRITICAL INSTRUCTION - INCREMENTAL GENERATION FOR MULTI-TURN CONVERSATIONS:
This is a multi-turn conversation system. When currentUISummary is provided (meaning this is NOT the first request), you MUST follow these rules:
1. NEVER return the complete UI structure - this will cause token overflow and data loss
2. ONLY return CHANGED/ADDED/UPDATED parts
3. The backend will automatically merge your response with the existing UI state
When currentUISummary is provided:
- For adding a new menu item: return only { "menuItems": [...new item] }
- For updating an existing menu: return only { "menuItems": [{ "index": "existing_id", ...updated fields }] }
- For adding a new page: return only { "pages": { "menu_index": {...} } }
- For updating an existing page: return only { "pages": { "menu_index": {...updated data} } }
- For modifying a specific field: return only that field
- For deleting a menu/page: return { "menuItems": [] } or { "pages": {} } with the item removed"
5.3 智能合并算法
// 深度合并:递归合并对象
deepMerge(target, source)
// 数组合并:基于唯一标识合并
mergeArrays(targetArray, sourceArray, key)
// 唯一标识优先级:
1. prop(表格列、表单字段)
2. index(菜单项、页面索引)
3. name(组件名称)
4. type(组件类型)
5. value(值)
// 合并策略:
- 如果找到匹配项:深度合并
- 如果未找到匹配项:追加到数组
5.4 变更页面识别
// 识别变更的页面
function identifyChangedPages(existingData, newData) {
// 1. 检查新增的页面
// 2. 检查修改的页面
// 3. 返回变更的页面列表
}
// 清除变更页面的mock数据
for (const pageKey of changedPages) {
await dao.deleteComponentMockDataByPage(conversationId, pageKey);
}
// 原因:
// 页面结构变化后,原有的mock数据可能不匹配
// 需要重新生成mock数据
六、实战案例演示
6.1 完整对话流程
// 第1轮:创建系统
用户:"帮我生成一个电商后台管理系统"
AI返回:完整系统框架
前端状态:uiData = { type: "admin", menuItems: [...], pages: {...} }
// 第2轮:添加页面
用户:"为商品管理页面添加表格"
AI返回:{ "pages": { "products": { ...table配置 } } }
前端状态:uiData = deepMerge(原数据, 新数据)
// 第3轮:添加菜单
用户:"添加一个订单管理菜单"
AI返回:{ "menuItems": [{ "index": "orders", ... }] }
前端状态:uiData = deepMerge(原数据, 新数据)
// 第4轮:修改页面
用户:"修改仪表盘的标题为数据总览"
AI返回:{ "pages": { "dashboard": { "title": "数据总览" } } }
前端状态:uiData = deepMerge(原数据, 新数据)
6.2 Token使用对比
// 不使用摘要优化
第1次请求:2000 tokens(完整UI)
第2次请求:2000 tokens(完整UI)+ 100 tokens(对话历史)
第3次请求:2000 tokens(完整UI)+ 200 tokens(对话历史)
第4次请求:2000 tokens(完整UI)+ 300 tokens(对话历史)
总计:8600 tokens
// 使用摘要优化
第1次请求:2000 tokens(完整UI)
第2次请求:1200 tokens(完整UI)+ 100 tokens(对话历史)
第3次请求:500 tokens(摘要UI)+ 200 tokens(对话历史)
第4次请求:500 tokens(摘要UI)+ 300 tokens(对话历史)
总计:4600 tokens
节省:46.5% tokens
七、总结与展望
本文深入介绍了AI UI Web系统的多轮对话实现机制,从增量更新、智能合并到token优化的完整技术方案。
核心技术要点:
-
前端状态管理:
- 使用Pinia Store管理对话状态
- 维护messages、uiData、currentConversationId
- 实现deepMerge和mergeArrays算法
-
Token优化策略:
- Token检测机制(estimateTokens)
- UI摘要生成(generateUISummary)
- 动态选择完整UI或摘要
-
增量更新机制:
- 在system prompt中明确说明"只返回变更部分"
- 提供具体的返回格式示例
- 避免AI返回完整UI结构
-
智能合并算法:
- deepMerge:递归合并对象
- mergeArrays:基于唯一标识合并数组
- 支持多种唯一标识(prop、index、name、type、value)
-
变更页面识别:
- 识别新增和修改的页面
- 清除变更页面的mock数据
- 确保数据一致性
实际效果:
- Token使用量减少46.5%
- 支持无限轮对话
- 数据一致性保证
- 用户体验流畅
未来优化方向:
- 引入更智能的摘要算法
- 优化合并策略,支持更多场景
- 增加对话历史压缩
- 支持并行页面生成
立即体验:点击体验
技术交流:欢迎一起探讨AI前端开发的未来!
本文为AI UI Web系列技术分享第三篇,下一篇将介绍数据库设计、API接口设计和部署运维等内容。
更多推荐


所有评论(0)