37 KiB
上下文长期记忆 + 图片目标定位 + 资产图谱表结构设计
基于当前项目结构的可落地方案
适用仓库:ai-chatbot-main
文档日期:2026-03-06
1. 文档目标
本方案不是抽象讨论,而是面向当前项目现状,补出一套可以直接进入开发的能力设计:
- 让对话具备更稳定的上下文长期记忆
- 让系统能更精准地知道用户连续对话里正在修改哪张图片
- 为无限画布、图片编辑、Agent 模式、视频九宫格、3D 资产提供统一的资产图谱索引层
- 方案必须兼容当前项目已上线的:
- 聊天接口
- 无限画布
- Agent 自动模式
- 图片生成/编辑
- 视频九宫格技能
- 3D 资产库
2. 基于当前项目的现状判断
2.1 现有核心入口
当前与本方案最相关的代码入口已经比较明确:
- 聊天主入口:
app/(chat)/api/chat/route.ts - 聊天请求结构:
app/(chat)/api/chat/schema.ts - 智能分析器:
lib/ai/smart-analyzer.ts - 重型 Agent 链路:
lib/ai/auto-agent-graph.ts - 无限画布聊天面板:
components/canvas/CanvasChatPanel.tsx - 画布工作区:
components/canvas/CanvasWorkspace.tsx - 画布状态仓库:
store/canvas/index.ts - 画布节点类型:
types/canvas/index.ts - 画布持久化接口:
app/api/canvas/[id]/route.ts - 数据库定义:
lib/db/schema.ts - 数据库查询:
lib/db/queries.ts - 视频技能样例:
lib/video-agent/skills/*.ts - 视频任务持久化样例:
lib/video-agent/task-store.ts
2.2 现有系统已经具备的能力
当前项目并不是“没有上下文”,而是已经有了若干前置能力:
-
聊天历史复用
app/(chat)/api/chat/route.ts已经存在“用户本轮没传图时自动回查历史图片”的逻辑- 现在主要通过扫描
Message_v2.parts、解析文本中的 Markdown 图片 URL 来回找最近结果图/原图
-
画布选中上下文
components/canvas/CanvasChatPanel.tsx已经会基于selectedNodeIds构造autoContext- 目前是把“用户选中了几张图、这些图大概在左/右/中位置”拼成一段字符串发给后端
-
画布状态持久化
app/api/canvas/[id]/route.ts当前以canvasProject.canvasData为主保存整张画布CanvasNode表虽然已经存在,但目前实际上没有被系统当作规范化索引层使用
-
图片结果入库
app/(chat)/api/chat/route.ts内部saveGeneratedImages()会将生图结果写入GeneratedImage- 相册接口
app/api/gallery/route.ts也是基于GeneratedImage返回
-
技能化执行样板
- 视频九宫格方案已经把复杂流程拆成:
- 脚本归一技能
- 九宫格检测技能
- 分镜映射技能
- 顺序执行编排
- 说明当前项目已经适合继续新增“记忆/目标定位技能”
- 视频九宫格方案已经把复杂流程拆成:
2.3 当前真正的短板
当前系统最核心的问题不是“没有模型分析能力”,而是上下文工程没有完全结构化:
-
历史图片复用依赖 URL 扫描和文本解析
- 容易受 Markdown 格式变化、URL 替换、图片代理地址变化影响
-
画布选择上下文还是文本提示,不是机器可判定结构
- 当前
autoContext更像“给 LLM 看的说明” - 但“这轮到底改哪张图”这种问题不能只靠自然语言推断
- 当前
-
现有业务表是分散的,不是统一资产索引
GeneratedImageModel3DAssetVideoStoryboardTask / ShotcanvasProject.canvasDataMessage_v2.parts- 它们都是业务存储,不是统一的“可检索资产图谱”
-
缺少一层持久化的 Working Memory
- 当前很多“上一轮结果、最近焦点图、原图/结果图关系”是临时逻辑判断,不是可靠状态
-
缺少对“本轮目标资产解析过程”的审计
- 现在若判断错图,很难准确复盘到底是:
- 选中上下文丢了
- 历史图片回查错了
- LLM 判错了
- 还是画布位置推断错了
- 现在若判断错图,很难准确复盘到底是:
3. 方案总原则
3.1 不做“模型魔法记忆”
这套能力不依赖某个模型“天然有长期记忆”,而是采用工程化四层结构:
- 会话工作记忆
- 会话摘要记忆
- 用户长期偏好记忆
- 资产图谱记忆
3.2 目标资产定位采用“规则优先,模型兜底”
优先级必须固定:
- 当前明确选中的资产
- 本轮新上传资产
- 用户文本里的显式指代
- 当前会话焦点资产
- 最近生成结果资产
- 只有仍然歧义时,才让模型做候选集判别
3.3 尽量复用现有项目结构
不建议一上来大重构,而是按当前目录自然扩展:
- 聊天链路仍然从
app/(chat)/api/chat/route.ts进入 - 分析器仍然沿用
lib/ai/* - 技能化拆分继续参考
lib/video-agent/skills/* - 数据库存储继续沿用
drizzle + SQL migration
3.4 先做“可追踪”,再做“更聪明”
先确保:
- 每一轮目标图解析都有记录
- 每一张图都有统一资产 ID
- 每一个画布节点都能绑定资产
- 每一次生成都能形成版本链
在此基础上再引入更复杂的模型判断,否则只会让错误更难排查。
4. 推荐总架构
flowchart TD
A["前端发送消息<br/>text + files + contextPayload + autoContext"] --> B["resolveChatMediaInput()"]
B --> C["loadConversationWorkingState()"]
C --> D["resolveTargetAssetsSkill()"]
D --> E["analyzeIntentStructured()"]
E --> F{"是否进入重型 Agent?"}
F -- 否 --> G["轻量执行链路<br/>直接图片编辑/生图/问图"]
F -- 是 --> H["auto-agent-graph / 视频 / 多技能任务"]
G --> I["registerResultAssets()"]
H --> I
I --> J["updateConversationWorkingState()"]
J --> K["appendMessageTargetResolution()"]
K --> L["compactConversationMemory()"]
这条链路里最关键的新层是:
- 结构化上下文载荷
contextPayload - 统一媒体解析层
resolveChatMediaInput() - 目标资产解析技能
resolveTargetAssetsSkill() - 统一资产注册层
registerResultAssets() - 持久化工作记忆
ConversationWorkingState
5. 基于当前项目的模块落点
5.1 后端新增目录建议
建议新增:
lib/ai/context-memory/
types.ts
media-input.ts
working-memory.ts
asset-registry.ts
asset-summary.ts
target-resolution.ts
prompt-memory.ts
compact.ts
skills/
resolve-target-assets.ts
summarize-asset.ts
write-user-memory.ts
说明:
lib/ai/context-memory/*负责聊天连续记忆、图片目标定位、资产图谱skills/设计方式参考lib/video-agent/skills/*auto-agent-graph、smart-analyzer不要再各自重复扫描 message parts,而是统一消费这里的解析结果
5.2 前端改造落点
A. components/canvas/CanvasChatPanel.tsx
当前已有 autoContext,建议升级为双通道:
-
保留
autoContext- 继续给模型看
- 作用是让模型更自然地理解“左上、右边、最近几张”
-
新增
contextPayload- 这是给后端规则引擎用的机器结构
- 不再依赖自然语言字符串解析
B. store/canvas/index.ts
当前已经持有:
nodesselectedNodeIdsprojectId
这些数据足够构造一份结构化上下文快照,不需要新建前端全局状态。
C. app/canvas/[id]/page.tsx
后续需要在画布保存或图片节点生成完成时,增加:
- 资产绑定上报
- 画布节点镜像同步
6. 请求结构升级设计
6.1 当前问题
现在 app/(chat)/api/chat/schema.ts 中请求体只有:
messagehiddenPromptautoContextselectedChatModelvideoParamsimageParams
这意味着:
autoContext是非结构化文本- 后端无法可靠知道“选中了哪几个 nodeId”
- 也无法知道它们是否对应同一轮结果资产
6.2 新增 contextPayload
建议在 app/(chat)/api/chat/schema.ts 增加:
const contextPayloadSchema = z.object({
source: z.enum(["chat", "canvas", "realtime-canvas"]).default("chat"),
projectId: z.string().uuid().optional(),
selectedNodeIds: z.array(z.string()).max(20).optional(),
selectedAssetIds: z.array(z.string().uuid()).max(20).optional(),
focusPolicy: z.enum(["auto", "selected", "latest_result", "original"]).optional(),
canvasImageNodes: z
.array(
z.object({
nodeId: z.string(),
url: z.string().url(),
title: z.string().optional(),
prompt: z.string().optional(),
x: z.number(),
y: z.number(),
width: z.number().optional(),
height: z.number().optional(),
isSelected: z.boolean().optional(),
})
)
.max(20)
.optional(),
}).optional();
然后将它挂入 postRequestBodySchema。
6.3 为什么必须要有 contextPayload
因为当前系统中“图片定位”存在三种来源:
- 本轮消息上传图片
- 画布当前选中图片
- 历史消息里最近结果图
只靠 autoContext 文本,服务端无法稳定地区分:
- 当前选中图
- 最近结果图
- 原始上传图
- 画布上左上角那张图
结构化字段可以直接让服务端先走规则,再把自然语言上下文交给模型。
7. 新增统一资产索引层:为什么不能只用现有业务表
7.1 现有表的局限
GeneratedImage
优点:
- 适合相册、历史统计、精选榜
问题:
- 只覆盖图片结果
- 不覆盖上传图、画布图、视频结果、3D 预览
- 不表达“这张图由哪张图编辑而来”
canvasProject.canvasData
优点:
- 是画布完整状态快照
问题:
- 更适合渲染,不适合高效检索
- 不适合做“连续对话目标图片定位”的服务端索引
CanvasNode
优点:
- 已经存在,天然适合做画布节点镜像
问题:
- 当前还没接入主流程
- 现在不能直接拿来做可靠资产定位
Model3DAsset / VideoStoryboardTask
优点:
- 适合各自业务模块持久化
问题:
- 是垂直业务表,不是统一资产层
7.2 结论
应该保留现有业务表不动,同时新增一层统一媒体资产索引层,专门服务:
- 上下文记忆
- 目标图片定位
- 版本链追踪
- 资产关系检索
- 连续多轮编辑
8. 数据库表结构设计
以下表结构是基于当前 lib/db/schema.ts 风格设计的,建议新增迁移文件:
lib/db/migrations/0050_add_context_memory_asset_graph.sql
8.1 MediaAsset:统一媒体资产表
用途:
- 将上传图、生成图、画布图、视频封面、视频结果、3D 预览统一注册成资产
推荐字段:
export const mediaAsset = pgTable("MediaAsset", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
projectId: uuid("projectId").references(() => canvasProject.id, { onDelete: "cascade" }),
mediaType: varchar("mediaType", { length: 32 }).notNull(), // image | video | model3d | file
sourceType: varchar("sourceType", { length: 32 }).notNull(), // upload | generated | canvas | storyboard_tile | storyboard_video | model3d_preview
status: varchar("status", { length: 32 }).notNull().default("ready"),
originalUrl: text("originalUrl").notNull(),
previewUrl: text("previewUrl"),
thumbnailUrl: text("thumbnailUrl"),
mimeType: varchar("mimeType", { length: 128 }),
width: integer("width"),
height: integer("height"),
durationMs: integer("durationMs"),
displayName: varchar("displayName", { length: 256 }),
prompt: text("prompt"),
modelId: varchar("modelId", { length: 128 }),
sourceMessageId: uuid("sourceMessageId").references(() => message.id, { onDelete: "set null" }),
sourceTable: varchar("sourceTable", { length: 64 }),
sourceRowId: varchar("sourceRowId", { length: 128 }),
sourceNodeId: varchar("sourceNodeId", { length: 128 }),
rootAssetId: uuid("rootAssetId"),
primaryParentAssetId: uuid("primaryParentAssetId"),
summary: jsonb("summary"),
metadata: jsonb("metadata"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
为什么需要 MediaAsset
因为以后所有定位逻辑都应该先查统一资产层,而不是到处扫:
GeneratedImageMessage_v2.partscanvasProject.canvasDataModel3DAssetVideoStoryboardShotTask
8.2 MediaAssetRelation:资产关系表
用途:
- 建立版本链、原图/结果图、画布绑定、引用关系
推荐字段:
export const mediaAssetRelation = pgTable("MediaAssetRelation", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
projectId: uuid("projectId").references(() => canvasProject.id, { onDelete: "cascade" }),
fromAssetId: uuid("fromAssetId").notNull(),
toAssetId: uuid("toAssetId").notNull(),
relationType: varchar("relationType", { length: 64 }).notNull(),
// edited_from | generated_from | reference_of | selected_in_turn | latest_result_of | located_on_canvas | derived_preview | upscaled_from
sourceMessageId: uuid("sourceMessageId").references(() => message.id, { onDelete: "set null" }),
sourceNodeId: varchar("sourceNodeId", { length: 128 }),
confidence: integer("confidence").default(100),
metadata: jsonb("metadata"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
});
这张表主要解决什么
例如系统以后要知道:
- 这张图是不是原图
- 这张图是不是上一轮结果
- 这张图是不是由“抠图后产品图”裂变出来的
- 这张图是否被放到了画布某个节点上
都不应该再靠 prompt 猜,而应该查关系表。
8.3 ConversationWorkingState:会话工作记忆表
用途:
- 保存当前会话的短期状态
- 专门服务“连续修改这张图”
推荐字段:
export const conversationWorkingState = pgTable("ConversationWorkingState", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
chatId: uuid("chatId").notNull().references(() => chat.id, { onDelete: "cascade" }),
projectId: uuid("projectId").references(() => canvasProject.id, { onDelete: "cascade" }),
currentIntent: varchar("currentIntent", { length: 64 }),
currentSubIntent: varchar("currentSubIntent", { length: 64 }),
currentFocusAssetIds: jsonb("currentFocusAssetIds").$type<string[]>().notNull().default([]),
selectedAssetIds: jsonb("selectedAssetIds").$type<string[]>().notNull().default([]),
lastUploadedAssetIds: jsonb("lastUploadedAssetIds").$type<string[]>().notNull().default([]),
lastGeneratedAssetIds: jsonb("lastGeneratedAssetIds").$type<string[]>().notNull().default([]),
lastReferencedAssetIds: jsonb("lastReferencedAssetIds").$type<string[]>().notNull().default([]),
lastResolvedTargetAssetIds: jsonb("lastResolvedTargetAssetIds").$type<string[]>().notNull().default([]),
activeCanvasNodeIds: jsonb("activeCanvasNodeIds").$type<string[]>().notNull().default([]),
lastCanvasSnapshot: jsonb("lastCanvasSnapshot"),
lastTurnSummary: jsonb("lastTurnSummary"),
lastMessageId: uuid("lastMessageId"),
turnCount: integer("turnCount").notNull().default(0),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
为什么不建议塞进 Chat.lastContext
当前 Chat.lastContext 的语义更接近 usage / 上下文结果缓存,不适合承载复杂的记忆状态。
工作记忆应该独立成表,便于:
- 单独更新
- 单独调试
- 单独回滚
- 单独做 TTL / 压缩
8.4 ConversationSummaryMemory:会话摘要记忆表
用途:
- 长对话压缩
- 记录某个阶段已达成的结果
export const conversationSummaryMemory = pgTable("ConversationSummaryMemory", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
chatId: uuid("chatId").notNull().references(() => chat.id, { onDelete: "cascade" }),
projectId: uuid("projectId").references(() => canvasProject.id, { onDelete: "cascade" }),
summaryType: varchar("summaryType", { length: 32 }).notNull().default("turn"),
// turn | compact | milestone
sourceMessageIds: jsonb("sourceMessageIds").$type<string[]>().notNull().default([]),
relatedAssetIds: jsonb("relatedAssetIds").$type<string[]>().notNull().default([]),
summaryText: text("summaryText").notNull(),
structuredSummary: jsonb("structuredSummary"),
extractedPreferences: jsonb("extractedPreferences"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
});
8.5 UserMemoryFact:用户长期偏好记忆表
用途:
- 保存稳定偏好,不保存一时性指令
export const userMemoryFact = pgTable("UserMemoryFact", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
scope: varchar("scope", { length: 32 }).notNull().default("global"),
// global | image | video | canvas | detail
factType: varchar("factType", { length: 32 }).notNull(),
// preference | constraint | style | workflow | domain
memoryKey: varchar("memoryKey", { length: 128 }).notNull(),
memoryValue: jsonb("memoryValue").notNull(),
confidence: integer("confidence").notNull().default(60),
sourceType: varchar("sourceType", { length: 32 }).notNull().default("inferred"),
// inferred | manual | imported
sourceChatId: uuid("sourceChatId").references(() => chat.id, { onDelete: "set null" }),
sourceMessageId: uuid("sourceMessageId").references(() => message.id, { onDelete: "set null" }),
isPinned: boolean("isPinned").notNull().default(false),
expiresAt: timestamp("expiresAt"),
lastConfirmedAt: timestamp("lastConfirmedAt"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
8.6 MessageTargetResolution:每轮目标定位审计表
用途:
- 记录“这一轮系统到底把目标判成了哪张图”
- 是后续排障和持续优化的关键
export const messageTargetResolution = pgTable("MessageTargetResolution", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId").notNull().references(() => user.id, { onDelete: "cascade" }),
chatId: uuid("chatId").notNull().references(() => chat.id, { onDelete: "cascade" }),
messageId: uuid("messageId").references(() => message.id, { onDelete: "set null" }),
projectId: uuid("projectId").references(() => canvasProject.id, { onDelete: "set null" }),
rawText: text("rawText").notNull(),
uploadedAssetIds: jsonb("uploadedAssetIds").$type<string[]>().notNull().default([]),
selectedAssetIds: jsonb("selectedAssetIds").$type<string[]>().notNull().default([]),
candidateAssetIds: jsonb("candidateAssetIds").$type<string[]>().notNull().default([]),
resolvedAssetIds: jsonb("resolvedAssetIds").$type<string[]>().notNull().default([]),
resolutionMode: varchar("resolutionMode", { length: 64 }).notNull(),
// explicit_selection | current_upload | text_reference | working_memory | latest_result | llm_disambiguation | clarification
confidence: integer("confidence").notNull().default(0),
needsClarification: boolean("needsClarification").notNull().default(false),
reasoning: jsonb("reasoning"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
});
9. 与现有表的关系设计
9.1 现有表继续保留
以下表继续作为业务源表存在:
GeneratedImageCanvasProjectCanvasNodeModel3DAssetVideoStoryboardTaskVideoStoryboardShotTask
9.2 新表的角色
新增记忆表不是替代现有表,而是充当:
- 上下文索引层
- 版本链关系层
- 连续编辑状态层
9.3 推荐的对应关系
| 现有业务来源 | 新索引写入 |
|---|---|
| 用户上传图片 | MediaAsset(sourceType=upload) |
生图成功写入 GeneratedImage |
同时写 MediaAsset(sourceType=generated) |
| 画布图片节点生成/落位 | 写 CanvasNode 镜像 + MediaAssetRelation(located_on_canvas) |
| 3D 预览图/模型 | 写 MediaAsset(sourceType=model3d_preview / model3d) |
| 九宫格切图 | 写 MediaAsset(sourceType=storyboard_tile) |
| 视频结果 | 写 MediaAsset(sourceType=storyboard_video) |
9.4 建议激活 CanvasNode 表
当前 CanvasNode 表已经存在但未被使用,建议正式启用:
app/api/canvas/[id]/route.ts在保存canvasData后,同步镜像主要节点到CanvasNode- 重点同步:
imagevideomodel3d
CanvasNode.data中新增:assetIdoriginalSrcprompttitle
这样服务端才能快速知道:
- 当前画布有哪些规范化节点
- 哪个 nodeId 对应哪个 assetId
- 左上/右下到底是哪张图
10. 资产摘要设计
10.1 为什么每张图都需要摘要
用户会说:
- “把刚才那张带模特的图背景换白”
- “不要改原图,改上一个结果”
- “左上角那个红色椅子再清晰一点”
这时只靠 URL 完全不够,系统需要每张资产都有一份结构化摘要。
10.2 摘要结构建议
MediaAsset.summary 建议存:
type AssetSummary = {
subject?: string;
productName?: string;
category?: string;
scene?: string;
style?: string;
viewAngle?: string;
backgroundType?: string;
dominantColors?: string[];
containsHuman?: boolean;
containsText?: boolean;
isCutoutLike?: boolean;
isLatestResult?: boolean;
shortCaption?: string;
};
10.3 摘要生成策略
第一层:规则补全
从已有信息快速写入:
- prompt
- modelId
- node title
- metadata
- 画布位置
第二层:多模态模型摘要
若项目后台已经配置多模态模型,则调用该模型生成结构化摘要。
这里建议直接复用当前项目已有的多模态模型选择体系,参考:
lib/ai/smart-analyzer.tslib/db/ai-model-flags.ts
要求:
- 优先使用已启用的多模态模型
- 仅生成结构化摘要,不让模型自由发挥长文本
- 超时则降级到规则摘要
第三层:延迟异步补全
对于非关键时刻,不要阻塞主链路:
- 先写基础摘要
- 再异步补 richer summary
11. 目标图片定位算法设计
11.1 统一输出结构
建议新增:
type ResolvedTargetAssets = {
targetAssetIds: string[];
candidateAssetIds: string[];
resolutionMode:
| "explicit_selection"
| "current_upload"
| "text_reference"
| "working_memory"
| "latest_result"
| "llm_disambiguation"
| "clarification";
confidence: number;
needsClarification: boolean;
reason: string;
};
11.2 优先级规则
规则 1:前端明确选中优先
若 contextPayload.selectedAssetIds 非空:
- 直接作为第一目标
- 除非用户文本明确说“不要改选中的图,改原图/上一轮结果”
规则 2:本轮上传图片优先
若本轮消息有图片上传:
- 默认优先处理这些上传图
规则 3:文本显式指代优先
解析:
- “左边那张 / 右上角那张 / 中间那张”
- “第二张 / 第三张”
- “刚生成的那张 / 上一轮结果”
- “原图 / 最初那张 / 参考图”
此时在候选集内通过规则命中:
- 画布位置
- 版本链
- 最近结果
- 原始上传链
规则 4:Working Memory 回退
若没有新上传,也没有显式指代:
按顺序回退:
currentFocusAssetIdslastResolvedTargetAssetIdslastGeneratedAssetIdslastUploadedAssetIds
规则 5:模型仅做歧义消解
只有下面情况才调用模型:
- 候选图有多张且规则打平
- 用户说“它 / 这个版本 / 那个更自然的”
- 文本指代不足但候选摘要足够清晰
模型输入不应该是全量原始上下文,而应该是:
- 用户本轮文本
- 候选资产摘要列表
- 候选图位置
- 与当前轮次距离
- 是否为原图 / 结果图 / 选中图
输出只允许是结构化结果:
{
"assetId": "uuid",
"confidence": 0.86,
"needsClarification": false
}
11.3 必须支持的指代表达
建议首批覆盖:
- 方位:左 / 右 / 上 / 下 / 中间 / 左上 / 右下
- 序号:第一张 / 第二张 / 第三个
- 结果链:原图 / 上一轮 / 最新生成 / 这个结果
- 选择态:选中的 / 当前这张 / 我选的这几张
11.4 多图模式
如果用户明确是批量操作:
- “这三张都去背景”
- “选中的几张统一换白底”
则 targetAssetIds 可以返回多个。
这将直接服务你们现有:
- 批量处理
- 产品裂变
- 多产品替换
- 多图统一编辑
12. 与当前聊天链路的集成方式
12.1 推荐新增统一媒体解析层
建议新增:
resolveChatMediaInput(params: {
messageParts: any[];
historyMessages: DBMessage[];
contextPayload?: ContextPayload;
requestOrigin?: string;
}): Promise<ResolvedChatMediaInput>
输出示例:
type ResolvedMediaRef = {
url: string;
assetId?: string;
source:
| "current_message"
| "canvas_selection"
| "history_user_upload"
| "history_assistant_result";
role: "upload" | "selected" | "reference" | "history_result";
nodeId?: string;
messageId?: string;
};
type ResolvedChatMediaInput = {
mediaRefs: ResolvedMediaRef[];
hasImage: boolean;
uploadedAssetIds: string[];
selectedAssetIds: string[];
historyCandidateAssetIds: string[];
};
12.2 在 app/(chat)/api/chat/route.ts 中的落点
建议将当前分散逻辑收口为以下顺序:
- 取历史消息
resolveChatMediaInput()loadConversationWorkingState()resolveTargetAssetsSkill()analyzeIntentStructured()- 决定:
- 轻量链路
- 重型 Agent 链路
- 执行成功后:
registerResultAssets()updateConversationWorkingState()appendMessageTargetResolution()
12.3 不要再让 fallback 重复各自扫图
当前项目已有多个 fallback / rescue / summary 路径。
升级后要求统一:
- 所有链路一律消费
ResolvedChatMediaInput - 不允许某条链路再自己从
message.parts重新扫描图片
否则就会再次出现:
- 主链路认定 A 图
- fallback 链路又重新认定成 B 图
13. 与 smart-analyzer 的关系
13.1 当前问题
当前 lib/ai/smart-analyzer.ts 同时承担:
- 意图分析
- 连续对话判断
- 自然语言回复
- 视觉图像分析
- fallback 逻辑
这会导致“目标资产定位”和“意图判断”耦合过深。
13.2 推荐拆分
建议拆为三层:
A. resolveTargetAssetsSkill()
只负责回答:
- 这轮改哪张图
- 是原图还是上一轮结果
- 是单图还是多图
B. analyzeIntentStructured()
只负责回答:
- image_edit / image_gen / video_gen / vision / chat
- subIntent
- confidence
C. buildFriendlyExecutionReply()
只负责用户可见回复:
- “收到,我先按你选中的图继续优化”
- “收到,我按上一轮结果继续处理”
这样以后如果定位错图,可以独立检查:
- 是资产定位错了
- 还是意图分析错了
14. 记忆写入时机设计
14.1 用户上传图片成功后
触发位置:
app/(chat)/api/files/upload/route.ts
建议:
- 上传成功后即注册
MediaAsset(sourceType=upload) - 后续用户在聊天消息中引用该 URL 时,可直接回查为资产
14.2 生图/编辑结果成功后
触发位置:
app/(chat)/api/chat/route.ts的saveGeneratedImages()app/api/enhance/route.tsapp/api/scene-fusion/route.ts- 其他图片处理接口
建议:
- 现有业务写
GeneratedImage保持不变 - 同时写
MediaAsset - 同时写
MediaAssetRelationedited_fromgenerated_fromlatest_result_of
14.3 画布节点落位后
触发位置:
app/api/canvas/[id]/route.ts- 或新增专门绑定接口
建议:
- 同步镜像到
CanvasNode - 对应资产写
MediaAssetRelation(located_on_canvas)
14.4 每轮聊天处理结束后
触发位置:
app/(chat)/api/chat/route.ts
建议写:
ConversationWorkingStateMessageTargetResolution
14.5 对话过长时
触发位置:
- 每 N 轮
- 或 token 接近阈值
- 或任务完成时
建议写:
ConversationSummaryMemory- 高置信稳定偏好写
UserMemoryFact
15. 长期记忆写入规则
15.1 哪些内容应该进入长期记忆
适合写入 UserMemoryFact:
- 用户长期偏好白底商品图
- 用户常用 1:1 或 4:5
- 用户偏好高清/写实/自然光
- 用户常做产品裂变、详情图、场景融合
15.2 哪些内容不应该进入长期记忆
不要写入长期记忆:
- “把这张图背景换蓝”
- “这一轮不要加文字”
- “这张先试一下”
这些是当前任务指令,不是长期偏好。
15.3 写入条件
推荐至少满足一项:
- 同类偏好连续出现 >= 3 次
- 用户明确说“以后都按这个来”
- 管理后台人工确认/置顶
16. 结合无限画布的特殊设计
16.1 当前画布上下文的优点与不足
当前 CanvasChatPanel 已经能知道:
- 当前画布有哪些图
- 哪几张图被选中
- 大概的上下位置
但它现在主要是转成自然语言文本给模型。
16.2 建议升级方式
前端继续生成自然语言 autoContext
用途:
- 给模型理解
- 提升对“左上角那张”这类表达的自然语言能力
同时新增结构化 contextPayload
用途:
- 服务端规则定位
- 稳定知道哪个
nodeId正在被处理
资产与节点绑定
建议在画布图片节点数据里加入:
type ImageNodeData = {
src: string;
assetId?: string;
originalSrc?: string;
title?: string;
prompt?: string;
...
}
这样:
- 前端选中 node 时可直接把
assetId传给后端 - 不需要只靠 URL 反查
16.3 与当前类型文件的关系
对应改动点:
types/canvas/index.tscomponents/canvas/CanvasChatPanel.tsxcomponents/canvas/nodes/ImageNode.tsx
17. 建议封装成技能
你之前希望这类复杂能力像“大龙虾 / 技能系统”一样可拆可扩展,这个方向是对的。
推荐把记忆与定位拆为以下技能:
17.1 resolve-target-assets 技能
职责:
- 输入:本轮文本 + 结构化上下文 + Working Memory + 候选资产
- 输出:本轮目标资产列表
17.2 summarize-asset 技能
职责:
- 输入:图片 URL / 资产 ID
- 输出:结构化摘要
17.3 write-user-memory 技能
职责:
- 从多轮摘要中抽取长期偏好
17.4 compact-conversation-memory 技能
职责:
- 将长对话压缩成摘要记忆
17.5 为什么要技能化
因为后续你们还会继续增加:
- 产品裂变 Agent
- 九宫格视频 Agent
- 3D Agent
- AI 详情 Agent
把“记忆”“目标定位”“摘要”封装成可复用技能,才能让这些能力被不同 Agent 共享。
18. 具体到当前仓库的实施文件清单
以下是推荐的首批改造文件清单。
18.1 必改文件
app/(chat)/api/chat/schema.ts
新增:
contextPayload
components/canvas/CanvasChatPanel.tsx
新增:
buildContextPayload()- 发送
contextPayload - 选中节点优先携带
assetId
app/(chat)/api/chat/route.ts
新增:
resolveChatMediaInput()loadConversationWorkingState()resolveTargetAssetsSkill()updateConversationWorkingState()appendMessageTargetResolution()
lib/ai/smart-analyzer.ts
调整:
- 不再单独承担目标资产定位
- 接收已解析好的目标资产上下文
types/canvas/index.ts
新增:
assetId?: string到图片/视频/3D 节点数据
app/api/canvas/[id]/route.ts
增强:
- 保存画布时同步镜像
CanvasNode
lib/db/schema.ts
新增:
MediaAssetMediaAssetRelationConversationWorkingStateConversationSummaryMemoryUserMemoryFactMessageTargetResolution
lib/db/queries.ts
新增:
- 资产注册
- 关系写入
- Working Memory 读写
- Resolution 审计写入
- Long-term Memory 检索
18.2 建议新增文件
lib/ai/context-memory/types.ts
lib/ai/context-memory/media-input.ts
lib/ai/context-memory/working-memory.ts
lib/ai/context-memory/asset-registry.ts
lib/ai/context-memory/asset-summary.ts
lib/ai/context-memory/target-resolution.ts
lib/ai/context-memory/compact.ts
lib/ai/context-memory/skills/resolve-target-assets.ts
lib/ai/context-memory/skills/summarize-asset.ts
lib/ai/context-memory/skills/write-user-memory.ts
19. 回填与迁移策略
19.1 为什么必须做回填
如果新系统上线后只记录新数据,那么历史聊天里的连续编辑仍然会不稳定。
因此建议做一次回填任务。
19.2 回填顺序
第一步:回填 GeneratedImage
把现有图库全部注册到 MediaAsset。
第二步:回填近期聊天消息
从 Message_v2.parts 和 assistant 文本 Markdown 中抽取历史图片 URL,建立初步映射。
第三步:回填 Model3DAsset
把 3D 资产预览图 / 模型地址写为统一资产。
第四步:回填 VideoStoryboardShotTask
把切片图、结果视频写为统一资产。
第五步:回填近期 canvasProject.canvasData
将近期画布的节点镜像同步到 CanvasNode。
19.3 回填任务建议
建议新增 Job:
lib/jobs/backfill-media-assets-job.ts
app/api/jobs/backfill-media-assets/route.ts
20. 性能与可用性设计
20.1 不建议首期引入新基础设施
当前项目已经依赖 PostgreSQL,首期不建议强依赖:
- 新向量数据库
- 新搜索服务
- 新消息总线
首期完全可以基于 Postgres + JSONB + 现有服务完成。
20.2 检索策略建议
首期:
- 精确 ID
- URL 归一化
- 时间倒序
- 关系链回查
- 位置规则匹配
二期可选:
pg_trgmtsvectorpgvector
20.3 不阻塞主请求
主链路中同步执行的仅限:
- 当前轮目标资产定位
- Working Memory 更新
- Resolution 审计写入
异步执行:
- 富摘要生成
- 对话压缩
- 长期偏好抽取
- 历史回填
21. 监控指标与验收口径
21.1 必加日志
建议在聊天主链路打印:
chatIdmessageIdhasCurrentUploadselectedAssetCountcandidateAssetCountresolvedAssetCountresolutionModeresolutionConfidenceneedsClarification
21.2 建议的核心指标
- 目标资产一次命中率
- “上一轮结果 / 原图”区分正确率
- 多图批量操作正确率
- 需澄清率
- 错图返工率
- 平均候选资产数量
21.3 首阶段验收标准
做到以下即可判定首阶段成功:
- 用户连续 3~5 轮修改同一张图时,稳定命中同一资产链
- 用户说“改原图 / 改上一轮结果”时可以区分
- 用户在画布选中图片后,系统优先命中选中图
- 批量选中多图时,支持多目标输出
- 后台可追踪每轮到底命中了哪张图
22. 分阶段实施建议
22.1 P0:先止血
目标:
- 让连续编辑先稳定下来
范围:
- 新增
contextPayload - 新增
resolveChatMediaInput() - 新增
ConversationWorkingState - 新增
MessageTargetResolution - 聊天主链路改为统一消费结构化媒体输入
22.2 P1:补统一资产层
目标:
- 让所有图都可被追踪
范围:
- 新增
MediaAsset - 新增
MediaAssetRelation - 上传图、生图、画布图统一注册资产
- 启用
CanvasNode镜像
22.3 P2:补长期记忆与摘要
目标:
- 让系统不只是知道“改哪张图”,还知道“用户长期偏好什么”
范围:
- 新增
ConversationSummaryMemory - 新增
UserMemoryFact - 新增资产摘要异步生成
- 新增对话压缩
22.4 P3:补高级召回
目标:
- 让长期项目、多轮大画布也稳定
范围:
- 关键词 / 全文检索增强
- 可选
pgvector - 后台调试页
23. 推荐的开发顺序
建议严格按下面顺序实施:
schema.ts新增表queries.ts新增记忆/资产 CRUDchat/schema.ts新增contextPayloadCanvasChatPanel.tsx发送结构化上下文resolveChatMediaInput()resolveTargetAssetsSkill()chat/route.ts主链路接入- 上传图/生图结果注册
MediaAsset CanvasNode镜像同步- 长期记忆与摘要异步化
24. 最终结论
对当前项目来说,要做到接近 Gemini 官方生图那种“连续对话不丢目标图”的体验,不能只继续堆模型提示词,必须补齐下面三层基础设施:
- 结构化上下文载荷
contextPayload - 统一资产索引层
MediaAsset + MediaAssetRelation - 持久化短期记忆
ConversationWorkingState
再往上,才是:
- 会话摘要记忆
- 长期用户偏好记忆
- 模型歧义消解
一句话总结:
你们当前已经有“聊天能力”“画布能力”“Agent 能力”“技能化样板”,现在缺的不是更大的模型,而是一层统一的上下文记忆与资产图谱基础设施。
这层一旦补齐,图片连续编辑、批量处理、九宫格视频、3D 资产、AI 详情都能共用同一套上下文引擎。