# 上下文长期记忆 + 图片目标定位 + 资产图谱表结构设计
> 基于当前项目结构的可落地方案
> 适用仓库:`ai-chatbot-main`
> 文档日期:2026-03-06
---
## 1. 文档目标
本方案不是抽象讨论,而是面向当前项目现状,补出一套可以直接进入开发的能力设计:
1. 让对话具备更稳定的**上下文长期记忆**
2. 让系统能更精准地知道用户连续对话里**正在修改哪张图片**
3. 为无限画布、图片编辑、Agent 模式、视频九宫格、3D 资产提供统一的**资产图谱索引层**
4. 方案必须兼容当前项目已上线的:
- 聊天接口
- 无限画布
- 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 现有系统已经具备的能力
当前项目并不是“没有上下文”,而是已经有了若干前置能力:
1. **聊天历史复用**
- `app/(chat)/api/chat/route.ts` 已经存在“用户本轮没传图时自动回查历史图片”的逻辑
- 现在主要通过扫描 `Message_v2.parts`、解析文本中的 Markdown 图片 URL 来回找最近结果图/原图
2. **画布选中上下文**
- `components/canvas/CanvasChatPanel.tsx` 已经会基于 `selectedNodeIds` 构造 `autoContext`
- 目前是把“用户选中了几张图、这些图大概在左/右/中位置”拼成一段字符串发给后端
3. **画布状态持久化**
- `app/api/canvas/[id]/route.ts` 当前以 `canvasProject.canvasData` 为主保存整张画布
- `CanvasNode` 表虽然已经存在,但目前实际上没有被系统当作规范化索引层使用
4. **图片结果入库**
- `app/(chat)/api/chat/route.ts` 内部 `saveGeneratedImages()` 会将生图结果写入 `GeneratedImage`
- 相册接口 `app/api/gallery/route.ts` 也是基于 `GeneratedImage` 返回
5. **技能化执行样板**
- 视频九宫格方案已经把复杂流程拆成:
- 脚本归一技能
- 九宫格检测技能
- 分镜映射技能
- 顺序执行编排
- 说明当前项目已经适合继续新增“记忆/目标定位技能”
### 2.3 当前真正的短板
当前系统最核心的问题不是“没有模型分析能力”,而是**上下文工程没有完全结构化**:
1. **历史图片复用依赖 URL 扫描和文本解析**
- 容易受 Markdown 格式变化、URL 替换、图片代理地址变化影响
2. **画布选择上下文还是文本提示,不是机器可判定结构**
- 当前 `autoContext` 更像“给 LLM 看的说明”
- 但“这轮到底改哪张图”这种问题不能只靠自然语言推断
3. **现有业务表是分散的,不是统一资产索引**
- `GeneratedImage`
- `Model3DAsset`
- `VideoStoryboardTask / Shot`
- `canvasProject.canvasData`
- `Message_v2.parts`
- 它们都是业务存储,不是统一的“可检索资产图谱”
4. **缺少一层持久化的 Working Memory**
- 当前很多“上一轮结果、最近焦点图、原图/结果图关系”是临时逻辑判断,不是可靠状态
5. **缺少对“本轮目标资产解析过程”的审计**
- 现在若判断错图,很难准确复盘到底是:
- 选中上下文丢了
- 历史图片回查错了
- LLM 判错了
- 还是画布位置推断错了
---
## 3. 方案总原则
### 3.1 不做“模型魔法记忆”
这套能力不依赖某个模型“天然有长期记忆”,而是采用工程化四层结构:
1. **会话工作记忆**
2. **会话摘要记忆**
3. **用户长期偏好记忆**
4. **资产图谱记忆**
### 3.2 目标资产定位采用“规则优先,模型兜底”
优先级必须固定:
1. 当前明确选中的资产
2. 本轮新上传资产
3. 用户文本里的显式指代
4. 当前会话焦点资产
5. 最近生成结果资产
6. 只有仍然歧义时,才让模型做候选集判别
### 3.3 尽量复用现有项目结构
不建议一上来大重构,而是按当前目录自然扩展:
- 聊天链路仍然从 `app/(chat)/api/chat/route.ts` 进入
- 分析器仍然沿用 `lib/ai/*`
- 技能化拆分继续参考 `lib/video-agent/skills/*`
- 数据库存储继续沿用 `drizzle + SQL migration`
### 3.4 先做“可追踪”,再做“更聪明”
先确保:
- 每一轮目标图解析都有记录
- 每一张图都有统一资产 ID
- 每一个画布节点都能绑定资产
- 每一次生成都能形成版本链
在此基础上再引入更复杂的模型判断,否则只会让错误更难排查。
---
## 4. 推荐总架构
```mermaid
flowchart TD
A["前端发送消息
text + files + contextPayload + autoContext"] --> B["resolveChatMediaInput()"]
B --> C["loadConversationWorkingState()"]
C --> D["resolveTargetAssetsSkill()"]
D --> E["analyzeIntentStructured()"]
E --> F{"是否进入重型 Agent?"}
F -- 否 --> G["轻量执行链路
直接图片编辑/生图/问图"]
F -- 是 --> H["auto-agent-graph / 视频 / 多技能任务"]
G --> I["registerResultAssets()"]
H --> I
I --> J["updateConversationWorkingState()"]
J --> K["appendMessageTargetResolution()"]
K --> L["compactConversationMemory()"]
```
这条链路里最关键的新层是:
1. **结构化上下文载荷 `contextPayload`**
2. **统一媒体解析层 `resolveChatMediaInput()`**
3. **目标资产解析技能 `resolveTargetAssetsSkill()`**
4. **统一资产注册层 `registerResultAssets()`**
5. **持久化工作记忆 `ConversationWorkingState`**
---
## 5. 基于当前项目的模块落点
## 5.1 后端新增目录建议
建议新增:
```txt
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`,建议升级为双通道:
1. **保留 `autoContext`**
- 继续给模型看
- 作用是让模型更自然地理解“左上、右边、最近几张”
2. **新增 `contextPayload`**
- 这是给后端规则引擎用的机器结构
- 不再依赖自然语言字符串解析
### B. `store/canvas/index.ts`
当前已经持有:
- `nodes`
- `selectedNodeIds`
- `projectId`
这些数据足够构造一份结构化上下文快照,不需要新建前端全局状态。
### C. `app/canvas/[id]/page.tsx`
后续需要在画布保存或图片节点生成完成时,增加:
- 资产绑定上报
- 画布节点镜像同步
---
## 6. 请求结构升级设计
## 6.1 当前问题
现在 `app/(chat)/api/chat/schema.ts` 中请求体只有:
- `message`
- `hiddenPrompt`
- `autoContext`
- `selectedChatModel`
- `videoParams`
- `imageParams`
这意味着:
- `autoContext` 是非结构化文本
- 后端无法可靠知道“选中了哪几个 nodeId”
- 也无法知道它们是否对应同一轮结果资产
## 6.2 新增 `contextPayload`
建议在 `app/(chat)/api/chat/schema.ts` 增加:
```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`
因为当前系统中“图片定位”存在三种来源:
1. 本轮消息上传图片
2. 画布当前选中图片
3. 历史消息里最近结果图
只靠 `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 预览统一注册成资产
推荐字段:
```ts
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`
因为以后所有定位逻辑都应该先查统一资产层,而不是到处扫:
- `GeneratedImage`
- `Message_v2.parts`
- `canvasProject.canvasData`
- `Model3DAsset`
- `VideoStoryboardShotTask`
## 8.2 `MediaAssetRelation`:资产关系表
用途:
- 建立版本链、原图/结果图、画布绑定、引用关系
推荐字段:
```ts
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`:会话工作记忆表
用途:
- 保存当前会话的短期状态
- 专门服务“连续修改这张图”
推荐字段:
```ts
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().notNull().default([]),
selectedAssetIds: jsonb("selectedAssetIds").$type().notNull().default([]),
lastUploadedAssetIds: jsonb("lastUploadedAssetIds").$type().notNull().default([]),
lastGeneratedAssetIds: jsonb("lastGeneratedAssetIds").$type().notNull().default([]),
lastReferencedAssetIds: jsonb("lastReferencedAssetIds").$type().notNull().default([]),
lastResolvedTargetAssetIds: jsonb("lastResolvedTargetAssetIds").$type().notNull().default([]),
activeCanvasNodeIds: jsonb("activeCanvasNodeIds").$type().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`:会话摘要记忆表
用途:
- 长对话压缩
- 记录某个阶段已达成的结果
```ts
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().notNull().default([]),
relatedAssetIds: jsonb("relatedAssetIds").$type().notNull().default([]),
summaryText: text("summaryText").notNull(),
structuredSummary: jsonb("structuredSummary"),
extractedPreferences: jsonb("extractedPreferences"),
createdAt: timestamp("createdAt").notNull().defaultNow(),
});
```
## 8.5 `UserMemoryFact`:用户长期偏好记忆表
用途:
- 保存稳定偏好,不保存一时性指令
```ts
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`:每轮目标定位审计表
用途:
- 记录“这一轮系统到底把目标判成了哪张图”
- 是后续排障和持续优化的关键
```ts
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().notNull().default([]),
selectedAssetIds: jsonb("selectedAssetIds").$type().notNull().default([]),
candidateAssetIds: jsonb("candidateAssetIds").$type().notNull().default([]),
resolvedAssetIds: jsonb("resolvedAssetIds").$type().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 现有表继续保留
以下表继续作为业务源表存在:
- `GeneratedImage`
- `CanvasProject`
- `CanvasNode`
- `Model3DAsset`
- `VideoStoryboardTask`
- `VideoStoryboardShotTask`
### 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` 表已经存在但未被使用,建议正式启用:
1. `app/api/canvas/[id]/route.ts` 在保存 `canvasData` 后,同步镜像主要节点到 `CanvasNode`
2. 重点同步:
- `image`
- `video`
- `model3d`
3. `CanvasNode.data` 中新增:
- `assetId`
- `originalSrc`
- `prompt`
- `title`
这样服务端才能快速知道:
- 当前画布有哪些规范化节点
- 哪个 nodeId 对应哪个 assetId
- 左上/右下到底是哪张图
---
## 10. 资产摘要设计
## 10.1 为什么每张图都需要摘要
用户会说:
- “把刚才那张带模特的图背景换白”
- “不要改原图,改上一个结果”
- “左上角那个红色椅子再清晰一点”
这时只靠 URL 完全不够,系统需要每张资产都有一份结构化摘要。
## 10.2 摘要结构建议
`MediaAsset.summary` 建议存:
```ts
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.ts`
- `lib/db/ai-model-flags.ts`
要求:
- 优先使用已启用的多模态模型
- 仅生成结构化摘要,不让模型自由发挥长文本
- 超时则降级到规则摘要
### 第三层:延迟异步补全
对于非关键时刻,不要阻塞主链路:
- 先写基础摘要
- 再异步补 richer summary
---
## 11. 目标图片定位算法设计
## 11.1 统一输出结构
建议新增:
```ts
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 回退
若没有新上传,也没有显式指代:
按顺序回退:
1. `currentFocusAssetIds`
2. `lastResolvedTargetAssetIds`
3. `lastGeneratedAssetIds`
4. `lastUploadedAssetIds`
### 规则 5:模型仅做歧义消解
只有下面情况才调用模型:
- 候选图有多张且规则打平
- 用户说“它 / 这个版本 / 那个更自然的”
- 文本指代不足但候选摘要足够清晰
模型输入不应该是全量原始上下文,而应该是:
- 用户本轮文本
- 候选资产摘要列表
- 候选图位置
- 与当前轮次距离
- 是否为原图 / 结果图 / 选中图
输出只允许是结构化结果:
```json
{
"assetId": "uuid",
"confidence": 0.86,
"needsClarification": false
}
```
## 11.3 必须支持的指代表达
建议首批覆盖:
- 方位:左 / 右 / 上 / 下 / 中间 / 左上 / 右下
- 序号:第一张 / 第二张 / 第三个
- 结果链:原图 / 上一轮 / 最新生成 / 这个结果
- 选择态:选中的 / 当前这张 / 我选的这几张
## 11.4 多图模式
如果用户明确是批量操作:
- “这三张都去背景”
- “选中的几张统一换白底”
则 `targetAssetIds` 可以返回多个。
这将直接服务你们现有:
- 批量处理
- 产品裂变
- 多产品替换
- 多图统一编辑
---
## 12. 与当前聊天链路的集成方式
## 12.1 推荐新增统一媒体解析层
建议新增:
```ts
resolveChatMediaInput(params: {
messageParts: any[];
historyMessages: DBMessage[];
contextPayload?: ContextPayload;
requestOrigin?: string;
}): Promise
```
输出示例:
```ts
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` 中的落点
建议将当前分散逻辑收口为以下顺序:
1. 取历史消息
2. `resolveChatMediaInput()`
3. `loadConversationWorkingState()`
4. `resolveTargetAssetsSkill()`
5. `analyzeIntentStructured()`
6. 决定:
- 轻量链路
- 重型 Agent 链路
7. 执行成功后:
- `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.ts`
- `app/api/scene-fusion/route.ts`
- 其他图片处理接口
建议:
- 现有业务写 `GeneratedImage` 保持不变
- 同时写 `MediaAsset`
- 同时写 `MediaAssetRelation`
- `edited_from`
- `generated_from`
- `latest_result_of`
## 14.3 画布节点落位后
触发位置:
- `app/api/canvas/[id]/route.ts`
- 或新增专门绑定接口
建议:
- 同步镜像到 `CanvasNode`
- 对应资产写 `MediaAssetRelation(located_on_canvas)`
## 14.4 每轮聊天处理结束后
触发位置:
- `app/(chat)/api/chat/route.ts`
建议写:
- `ConversationWorkingState`
- `MessageTargetResolution`
## 14.5 对话过长时
触发位置:
- 每 N 轮
- 或 token 接近阈值
- 或任务完成时
建议写:
- `ConversationSummaryMemory`
- 高置信稳定偏好写 `UserMemoryFact`
---
## 15. 长期记忆写入规则
## 15.1 哪些内容应该进入长期记忆
适合写入 `UserMemoryFact`:
- 用户长期偏好白底商品图
- 用户常用 1:1 或 4:5
- 用户偏好高清/写实/自然光
- 用户常做产品裂变、详情图、场景融合
## 15.2 哪些内容不应该进入长期记忆
不要写入长期记忆:
- “把这张图背景换蓝”
- “这一轮不要加文字”
- “这张先试一下”
这些是当前任务指令,不是长期偏好。
## 15.3 写入条件
推荐至少满足一项:
1. 同类偏好连续出现 >= 3 次
2. 用户明确说“以后都按这个来”
3. 管理后台人工确认/置顶
---
## 16. 结合无限画布的特殊设计
## 16.1 当前画布上下文的优点与不足
当前 `CanvasChatPanel` 已经能知道:
- 当前画布有哪些图
- 哪几张图被选中
- 大概的上下位置
但它现在主要是转成自然语言文本给模型。
## 16.2 建议升级方式
### 前端继续生成自然语言 `autoContext`
用途:
- 给模型理解
- 提升对“左上角那张”这类表达的自然语言能力
### 同时新增结构化 `contextPayload`
用途:
- 服务端规则定位
- 稳定知道哪个 `nodeId` 正在被处理
### 资产与节点绑定
建议在画布图片节点数据里加入:
```ts
type ImageNodeData = {
src: string;
assetId?: string;
originalSrc?: string;
title?: string;
prompt?: string;
...
}
```
这样:
- 前端选中 node 时可直接把 `assetId` 传给后端
- 不需要只靠 URL 反查
## 16.3 与当前类型文件的关系
对应改动点:
- `types/canvas/index.ts`
- `components/canvas/CanvasChatPanel.tsx`
- `components/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`
新增:
- `MediaAsset`
- `MediaAssetRelation`
- `ConversationWorkingState`
- `ConversationSummaryMemory`
- `UserMemoryFact`
- `MessageTargetResolution`
### `lib/db/queries.ts`
新增:
- 资产注册
- 关系写入
- Working Memory 读写
- Resolution 审计写入
- Long-term Memory 检索
## 18.2 建议新增文件
```txt
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:
```txt
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_trgm`
- `tsvector`
- `pgvector`
## 20.3 不阻塞主请求
主链路中同步执行的仅限:
- 当前轮目标资产定位
- Working Memory 更新
- Resolution 审计写入
异步执行:
- 富摘要生成
- 对话压缩
- 长期偏好抽取
- 历史回填
---
## 21. 监控指标与验收口径
## 21.1 必加日志
建议在聊天主链路打印:
- `chatId`
- `messageId`
- `hasCurrentUpload`
- `selectedAssetCount`
- `candidateAssetCount`
- `resolvedAssetCount`
- `resolutionMode`
- `resolutionConfidence`
- `needsClarification`
## 21.2 建议的核心指标
1. 目标资产一次命中率
2. “上一轮结果 / 原图”区分正确率
3. 多图批量操作正确率
4. 需澄清率
5. 错图返工率
6. 平均候选资产数量
## 21.3 首阶段验收标准
做到以下即可判定首阶段成功:
1. 用户连续 3~5 轮修改同一张图时,稳定命中同一资产链
2. 用户说“改原图 / 改上一轮结果”时可以区分
3. 用户在画布选中图片后,系统优先命中选中图
4. 批量选中多图时,支持多目标输出
5. 后台可追踪每轮到底命中了哪张图
---
## 22. 分阶段实施建议
## 22.1 P0:先止血
目标:
- 让连续编辑先稳定下来
范围:
1. 新增 `contextPayload`
2. 新增 `resolveChatMediaInput()`
3. 新增 `ConversationWorkingState`
4. 新增 `MessageTargetResolution`
5. 聊天主链路改为统一消费结构化媒体输入
## 22.2 P1:补统一资产层
目标:
- 让所有图都可被追踪
范围:
1. 新增 `MediaAsset`
2. 新增 `MediaAssetRelation`
3. 上传图、生图、画布图统一注册资产
4. 启用 `CanvasNode` 镜像
## 22.3 P2:补长期记忆与摘要
目标:
- 让系统不只是知道“改哪张图”,还知道“用户长期偏好什么”
范围:
1. 新增 `ConversationSummaryMemory`
2. 新增 `UserMemoryFact`
3. 新增资产摘要异步生成
4. 新增对话压缩
## 22.4 P3:补高级召回
目标:
- 让长期项目、多轮大画布也稳定
范围:
1. 关键词 / 全文检索增强
2. 可选 `pgvector`
3. 后台调试页
---
## 23. 推荐的开发顺序
建议严格按下面顺序实施:
1. `schema.ts` 新增表
2. `queries.ts` 新增记忆/资产 CRUD
3. `chat/schema.ts` 新增 `contextPayload`
4. `CanvasChatPanel.tsx` 发送结构化上下文
5. `resolveChatMediaInput()`
6. `resolveTargetAssetsSkill()`
7. `chat/route.ts` 主链路接入
8. 上传图/生图结果注册 `MediaAsset`
9. `CanvasNode` 镜像同步
10. 长期记忆与摘要异步化
---
## 24. 最终结论
对当前项目来说,要做到接近 Gemini 官方生图那种“连续对话不丢目标图”的体验,不能只继续堆模型提示词,必须补齐下面三层基础设施:
1. **结构化上下文载荷 `contextPayload`**
2. **统一资产索引层 `MediaAsset + MediaAssetRelation`**
3. **持久化短期记忆 `ConversationWorkingState`**
再往上,才是:
4. **会话摘要记忆**
5. **长期用户偏好记忆**
6. **模型歧义消解**
一句话总结:
> 你们当前已经有“聊天能力”“画布能力”“Agent 能力”“技能化样板”,现在缺的不是更大的模型,而是一层统一的上下文记忆与资产图谱基础设施。
> 这层一旦补齐,图片连续编辑、批量处理、九宫格视频、3D 资产、AI 详情都能共用同一套上下文引擎。