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

37 KiB
Raw Blame History

上下文长期记忆 + 图片目标定位 + 资产图谱表结构设计

基于当前项目结构的可落地方案
适用仓库: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. 推荐总架构

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 后端新增目录建议

建议新增:

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 增加:

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 预览统一注册成资产

推荐字段:

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:资产关系表

用途:

  • 建立版本链、原图/结果图、画布绑定、引用关系

推荐字段:

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 现有表继续保留

以下表继续作为业务源表存在:

  • 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 建议存:

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 统一输出结构

建议新增:

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:模型仅做歧义消解

只有下面情况才调用模型:

  • 候选图有多张且规则打平
  • 用户说“它 / 这个版本 / 那个更自然的”
  • 文本指代不足但候选摘要足够清晰

模型输入不应该是全量原始上下文,而应该是:

  • 用户本轮文本
  • 候选资产摘要列表
  • 候选图位置
  • 与当前轮次距离
  • 是否为原图 / 结果图 / 选中图

输出只允许是结构化结果:

{
  "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 中的落点

建议将当前分散逻辑收口为以下顺序:

  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 正在被处理

资产与节点绑定

建议在画布图片节点数据里加入:

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 建议新增文件

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_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

再往上,才是:

  1. 会话摘要记忆
  2. 长期用户偏好记忆
  3. 模型歧义消解

一句话总结:

你们当前已经有“聊天能力”“画布能力”“Agent 能力”“技能化样板”,现在缺的不是更大的模型,而是一层统一的上下文记忆与资产图谱基础设施。
这层一旦补齐,图片连续编辑、批量处理、九宫格视频、3D 资产、AI 详情都能共用同一套上下文引擎。