ai-chatbot/docs/上下文长期记忆-图片目标定位-资产图谱表结构设计.md
Your Name dc6162c93d
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: apply latest canvas editor and chat updates
2026-03-06 16:24:23 +08:00

1488 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 上下文长期记忆 + 图片目标定位 + 资产图谱表结构设计
> 基于当前项目结构的可落地方案
> 适用仓库:`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["前端发送消息<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()"]
```
这条链路里最关键的新层是:
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<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`:会话摘要记忆表
用途:
- 长对话压缩
- 记录某个阶段已达成的结果
```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<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`:用户长期偏好记忆表
用途:
- 保存稳定偏好,不保存一时性指令
```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<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 现有表继续保留
以下表继续作为业务源表存在:
- `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<ResolvedChatMediaInput>
```
输出示例:
```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 详情都能共用同一套上下文引擎。