# 上下文长期记忆 + 图片目标定位 + 资产图谱表结构设计 > 基于当前项目结构的可落地方案 > 适用仓库:`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 详情都能共用同一套上下文引擎。