《60天AI学习计划启动 | Day 46: 多模态输入统一模型(文本 / 图片 / 文件 / 结构化)》
Day 46:多模态输入统一模型(文本 / 图片 / 文件 / 结构化)
学习目标
- 统一抽象 文本、图片、文件、结构化数据等多种输入类型
- 搞清楚 前端到后端如何传多模态 payload(不绑具体业务)
- 会写 一个通用
InputItem模型 + 基础管理逻辑
核心知识点
-
1. 多模态输入统一抽象
export type InputKind = 'text' | 'image' | 'file' | 'json' export interface BaseInputItem { id: string kind: InputKind createdAt: number meta?: Record<string, any> } export interface TextInputItem extends BaseInputItem { kind: 'text' text: string } export interface ImageInputItem extends BaseInputItem { kind: 'image' file: File // 或 url:string previewUrl?: string } export interface FileInputItem extends BaseInputItem { kind: 'file' file: File } export interface JsonInputItem extends BaseInputItem { kind: 'json' data: unknown } export type InputItem = | TextInputItem | ImageInputItem | FileInputItem | JsonInputItem -
2. 前端统一转成后端 payload
// 抽象成后端可以消费的结构(不带 File 本体) export interface InputPayload { kind: InputKind text?: string url?: string fileName?: string contentType?: string data?: unknown } export function toPayload(items: InputItem[]): InputPayload[] { return items.map((it) => { switch (it.kind) { case 'text': return { kind: 'text', text: it.text } case 'image': return { kind: 'image', url: it.meta?.uploadedUrl, // 由上传接口返回 fileName: (it.file as File).name, contentType: (it.file as File).type } case 'file': return { kind: 'file', url: it.meta?.uploadedUrl, fileName: it.file.name, contentType: it.file.type } case 'json': return { kind: 'json', data: it.data } } }) } -
3. 与聊天结合的基本思路
- 文本:直接作为 message 内容
- 图片/文件:先上传 → 拿到 url/id → 作为
InputPayload附在当前问题上 - 结构化 JSON:可作为
context(例如当前选中行/报表过滤条件)
简单实战代码示例(可直接用)
-
多模态输入管理 hook
import { useState, useCallback } from 'react' import type { InputItem, TextInputItem, ImageInputItem, FileInputItem } from './inputTypes' export function useInputItems() { const [items, setItems] = useState<InputItem[]>([]) const addText = useCallback((text: string) => { const item: TextInputItem = { id: crypto.randomUUID(), kind: 'text', text, createdAt: Date.now() } setItems(prev => [...prev, item]) }, []) const addImageFile = useCallback((file: File) => { const item: ImageInputItem = { id: crypto.randomUUID(), kind: 'image', file, previewUrl: URL.createObjectURL(file), createdAt: Date.now() } setItems(prev => [...prev, item]) }, []) const addFile = useCallback((file: File) => { const item: FileInputItem = { id: crypto.randomUUID(), kind: 'file', file, createdAt: Date.now() } setItems(prev => [...prev, item]) }, []) const removeItem = useCallback((id: string) => { setItems(prev => prev.filter(i => i.id !== id)) }, []) const clear = useCallback(() => setItems([]), []) return { items, addText, addImageFile, addFile, removeItem, clear } } -
简单预览组件(展示当前附带的多模态输入)
import React from 'react' import type { InputItem } from './inputTypes' interface Props { items: InputItem[] onRemove: (id: string) => void } export const InputPreviewList: React.FC<Props> = ({ items, onRemove }) => { if (!items.length) return null return ( <div style={{ border:'1px solid #eee', padding:8, marginBottom:8 }}> {items.map(it => ( <div key={it.id} style={{ display:'flex', alignItems:'center', marginBottom:4 }}> <span>[{it.kind}]</span> {it.kind === 'text' && ( <span style={{ marginLeft:4 }}>{(it as any).text.slice(0,30)}...</span> )} {it.kind === 'image' && (it as any).previewUrl && ( <img src={(it as any).previewUrl} style={{ width:40, height:40, objectFit:'cover', marginLeft:4 }} /> )} {it.kind === 'file' && ( <span style={{ marginLeft:4 }}>{(it as any).file.name}</span> )} <button style={{ marginLeft:'auto' }} onClick={() => onRemove(it.id)}> 移除 </button> </div> ))} </div> ) }
明日学习计划预告(Day 47)
- 主题:知识库版本管理与变更可视化(前端视角)
- 方向:
- 为文档/知识条目设计
version / lastUpdated / diff模型 - 在前端展示“当前回答基于哪一版文档”,支持查看历史版本对比
- 为文档/知识条目设计

浙公网安备 33010602011771号