diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index e63352c82..914f767cb 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -1650,6 +1650,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const hasInitializedChatStateRef = useRef(false); const sessionMenuRef = useRef(null); const historyScrollTimerRef = useRef | null>(null); + const forceHistoryHydrationRef = useRef(null); // 🆕 标签悬停预览状态 const [tagPreview, setTagPreview] = useState(null); @@ -1680,34 +1681,83 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF unitCost: number; imageCount: number; } | null>(null); - const [streamImageGenerationState, setStreamImageGenerationState] = - useState(null); + const [streamImageGenerationStateByChatId, setStreamImageGenerationStateByChatId] = + useState>({}); + const streamImageGenerationState = useMemo( + () => streamImageGenerationStateByChatId[activeChatId] || null, + [activeChatId, streamImageGenerationStateByChatId] + ); const imageParamsRef = useRef(imageParams); const videoParamsRef = useRef(videoParams); const deepThinkingEnabledRef = useRef(deepThinkingEnabled); const imageAutoTitleCacheRef = useRef>(new Map()); - const sendGuardRef = useRef<{ + const sendGuardRef = useRef({ - inFlight: false, - signature: "", - timestamp: 0, - }); + }>>({}); const storyboardNodeMapRef = useRef>>({}); - const releaseSendGuard = useCallback(() => { - sendGuardRef.current.inFlight = false; + const getSendGuard = useCallback((chatSessionId: string) => { + return ( + sendGuardRef.current[chatSessionId] || { + inFlight: false, + signature: "", + timestamp: 0, + } + ); }, []); - const shouldBlockDuplicateSend = useCallback((signature: string) => { + const setSendGuard = useCallback( + ( + chatSessionId: string, + next: { + inFlight: boolean; + signature: string; + timestamp: number; + } + ) => { + sendGuardRef.current[chatSessionId] = next; + }, + [] + ); + + const releaseSendGuard = useCallback( + (chatSessionId: string) => { + sendGuardRef.current[chatSessionId] = { + ...getSendGuard(chatSessionId), + inFlight: false, + }; + }, + [getSendGuard] + ); + + const setStreamImageGenerationStateForChat = useCallback( + (chatSessionId: string, next: ImageGenerationStatusStreamPayload | null) => { + setStreamImageGenerationStateByChatId((prev) => ({ + ...prev, + [chatSessionId]: next, + })); + }, + [] + ); + + const clearStreamImageGenerationStateForChat = useCallback((chatSessionId: string) => { + setStreamImageGenerationStateByChatId((prev) => { + if (!(chatSessionId in prev)) return prev; + const next = { ...prev }; + delete next[chatSessionId]; + return next; + }); + }, []); + + const shouldBlockDuplicateSend = useCallback((chatSessionId: string, signature: string) => { const now = Date.now(); - const previous = sendGuardRef.current; + const previous = getSendGuard(chatSessionId); return ( previous.signature === signature && now - previous.timestamp < 4500 ); - }, []); + }, [getSendGuard]); const handleTypewriterActiveChange = useCallback((typingKey: string, active: boolean) => { setActiveTypingSections((prev) => { @@ -2941,9 +2991,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF ? "视频" : activeComposerMode === "detail" ? "AI详情" - : activeComposerMode === "image" - ? "生图" - : "Agent"; + : activeComposerMode === "image" + ? "生图" + : "Agent"; + const shouldMaskAgentRuntimeModelName = + activeComposerMode === "agent" || selectedModelId === "auto"; + const getAgentSafeModelName = useCallback( + (modelName?: string | null) => { + const normalized = String(modelName || "").trim(); + if (!shouldMaskAgentRuntimeModelName) { + return normalized || undefined; + } + return normalized ? "agent model" : "agent model"; + }, + [shouldMaskAgentRuntimeModelName] + ); // 默认选择模型(根据系统设置) useEffect(() => { @@ -3426,7 +3488,6 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF setMessages, sendMessage, status, - stop, } = useChat({ id: activeChatId, messages: mergedInitialMessages, @@ -3494,13 +3555,13 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF typeof dataPart.data === "string" ? JSON.parse(dataPart.data) : dataPart.data; if (!payload || typeof payload !== "object") return; if (payload.phase === "completed") { - setStreamImageGenerationState(null); + clearStreamImageGenerationStateForChat(activeChatId); return; } - setStreamImageGenerationState({ + setStreamImageGenerationStateForChat(activeChatId, { phase: payload.phase || "processing", providerType: payload.providerType, - modelName: payload.modelName, + modelName: getAgentSafeModelName(payload.modelName), imageCount: Number(payload.imageCount || 0) || undefined, }); } catch (e) { @@ -3543,28 +3604,59 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF part.text.includes("") ) : false; - if (hasImageGenStartMarker) { - setStreamImageGenerationState((prev) => ({ - phase: "processing", - providerType: - prev?.providerType || - (activeComposerMode === "agent" ? "alibaba-image" : currentModel?.providerType), - modelName: - prev?.modelName || - (activeComposerMode === "agent" - ? "智能处理" - : currentModel?.displayName || undefined), - imageCount: - prev?.imageCount || Math.max(1, Number(imageParamsRef.current?.imageCount || 1)), - })); - } - if (hasImageGenEndMarker) { - setStreamImageGenerationState(null); - } const normalizedParts = Array.isArray(messageData.parts) ? messageData.parts : []; const normalizedAttachments = Array.isArray(messageData.attachments) ? messageData.attachments : []; + const emptyNearestUserImageKeys = new Set(); + const incomingImageUrls = normalizedParts.flatMap((part: any) => + part?.type === "text" && typeof part?.text === "string" + ? extractAssistantImageUrls(part.text, emptyNearestUserImageKeys) + : typeof part?.url === "string" && /^https?:\/\//i.test(part.url) + ? [part.url] + : [] + ); + const incomingVideoUrls = normalizedParts.flatMap((part: any) => + part?.type === "text" && typeof part?.text === "string" + ? extractVideoUrls(part.text) + : typeof part?.url === "string" && + /^https?:\/\//i.test(part.url) && + /\.(mp4|mov|webm|m4v)(?:[?#].*)?$/i.test(part.url) + ? [part.url] + : [] + ); + normalizedAttachments.forEach((attachment: any) => { + const rawUrl = String(attachment?.url || "").trim(); + if (!rawUrl || !/^https?:\/\//i.test(rawUrl)) return; + const contentType = String(attachment?.contentType || "").toLowerCase(); + if ( + contentType.startsWith("video/") || + /\.(mp4|mov|webm|m4v)(?:[?#].*)?$/i.test(rawUrl) + ) { + incomingVideoUrls.push(rawUrl); + return; + } + incomingImageUrls.push(rawUrl); + }); + if (hasImageGenStartMarker) { + const previousState = + streamImageGenerationStateByChatId[activeChatId] || null; + setStreamImageGenerationStateForChat(activeChatId, { + phase: "processing", + providerType: + previousState?.providerType || + (activeComposerMode === "agent" ? "alibaba-image" : currentModel?.providerType), + modelName: + previousState?.modelName || + getAgentSafeModelName(currentModel?.displayName), + imageCount: + previousState?.imageCount || + Math.max(1, Number(imageParamsRef.current?.imageCount || 1)), + }); + } + if (hasImageGenEndMarker || incomingImageUrls.length > 0 || incomingVideoUrls.length > 0) { + clearStreamImageGenerationStateForChat(activeChatId); + } setMessages((prevMessages) => { const existingIndex = prevMessages.findIndex( @@ -3728,6 +3820,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF } }, onError: (error) => { + clearStreamImageGenerationStateForChat(activeChatId); + releaseSendGuard(activeChatId); const fallbackMessage = error instanceof Error ? error.message : "生成失败"; const responseData = (error as any)?.data; @@ -3757,9 +3851,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF useEffect(() => { if (status === "ready") { - releaseSendGuard(); + releaseSendGuard(activeChatId); } - }, [releaseSendGuard, status]); + }, [activeChatId, releaseSendGuard, status]); useEffect(() => { const launchPayload = pendingLaunchRef.current; @@ -3817,7 +3911,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF } if (!historyData || !rawChatId) return; - if (status === "streaming" || status === "submitted") return; + const shouldForceHydrate = forceHistoryHydrationRef.current === rawChatId; + if ((status === "streaming" || status === "submitted") && !shouldForceHydrate) return; const historyMessages = historyData.messages || []; const lastMessageId = historyMessages.at(-1)?.id || "none"; @@ -3828,6 +3923,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF loadedHistorySignatureRef.current = signature; processedMessageIds.current = new Set(); hasHydratedHistoryRef.current = true; + forceHistoryHydrationRef.current = null; setMessages(historyMessages); setMessageViewportKey((prev) => prev + 1); console.log("[CanvasChat] 已同步历史消息:", historyMessages.length, signature); @@ -6223,20 +6319,21 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF // 处理发送(图片已在上传时添加到画布,这里只发送消息) const handleSend = useCallback(async () => { - if (sendGuardRef.current.inFlight || status === "streaming" || status === "submitted") { + const currentSendGuard = getSendGuard(activeChatId); + if (currentSendGuard.inFlight || status === "streaming" || status === "submitted") { return; } if (isDetailGenerationMode && aiDetailGenerationEnabled) { - sendGuardRef.current = { + setSendGuard(activeChatId, { inFlight: true, signature: `detail:${activeChatId}:${Date.now()}`, timestamp: Date.now(), - }; + }); try { await handleDetailGeneration(); } finally { - releaseSendGuard(); + releaseSendGuard(activeChatId); } return; } @@ -6389,17 +6486,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF mode: activeComposerMode, }); - if (shouldBlockDuplicateSend(sendSignature)) { + if (shouldBlockDuplicateSend(activeChatId, sendSignature)) { toast.message("检测到重复发送,已自动忽略"); return; } - sendGuardRef.current = { + setSendGuard(activeChatId, { inFlight: true, signature: sendSignature, timestamp: Date.now(), - }; - setStreamImageGenerationState(null); + }); + clearStreamImageGenerationStateForChat(activeChatId); // 使用与 multimodal-input 相同的格式发送消息 try { @@ -6408,7 +6505,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF parts: parts, }); } catch (error) { - releaseSendGuard(); + releaseSendGuard(activeChatId); console.error("[CanvasChat] sendMessage failed:", error); toast.error("发送失败,请重试"); return; @@ -6448,8 +6545,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF aiDetailGenerationEnabled, handleDetailGeneration, ensureImagePointsBeforeSend, - isVideoModel, + getSendGuard, + clearStreamImageGenerationStateForChat, releaseSendGuard, + setSendGuard, shouldBlockDuplicateSend, status, ]); @@ -7001,20 +7100,23 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF updatedAt: now, }; + releaseSendGuard(activeChatId); setChatSessions((prevSessions) => [newSession, ...prevSessions]); setActiveChatId(newChatSessionId); toast.success("已新建聊天窗口"); - }, [chatSessions.length]); + }, [activeChatId, chatSessions.length, releaseSendGuard]); const switchToSession = useCallback((sessionId: string) => { if (!sessionId || sessionId === activeChatId) { setIsSessionMenuOpen(false); return; } + forceHistoryHydrationRef.current = toRawCanvasChatId(sessionId); + releaseSendGuard(activeChatId); setActiveChatId(sessionId); setIsSessionMenuOpen(false); toast.success("已切换聊天窗口"); - }, [activeChatId]); + }, [activeChatId, releaseSendGuard]); const activeSessionTitle = useMemo(() => { const current = chatSessions.find((session) => session.id === activeChatId); diff --git a/docs/Agent模式-低延迟响应升级规划-2026-03-08.md b/docs/Agent模式-低延迟响应升级规划-2026-03-08.md new file mode 100644 index 000000000..521b19966 --- /dev/null +++ b/docs/Agent模式-低延迟响应升级规划-2026-03-08.md @@ -0,0 +1,1341 @@ +# Agent模式低延迟响应升级规划 + +## 1. 背景 + +当前项目里的 `Agent模式` 已经具备以下能力: + +- 能做意图分析 +- 能做 Planner + Specialist 路由 +- 能做修图 / 生图 / 视频 / 画布等执行 +- 能在部分链路里把结果同步回无限画布 + +但当前用户体感仍然存在两个明显问题: + +1. 首响应偏慢 + 很多请求要先完成 Agent 分析、规划、模型路由,用户才看到第一句回复。 + +2. `Agent模式` 下经常出现“双模型串行” + 典型现象是: + - `doubao-seed-2-0-mini-260215` 先负责分析 / 规划 + - 再由另一个模型(例如 `Gemini-3.1-pro`)负责最终回复 + 这会导致重复算力、延迟增大、体验不稳定。 + +本规划目标是把 `Agent模式` 升级成更接近 Gemini / 豆包官方产品体验的“立即首响应 + 后台继续分析 / 执行”的编排模型。 + +## 2. 当前架构现状 + +### 2.1 Agent Graph + +当前自动链路位于: + +- `lib/ai/auto-agent-graph.ts` + +现有节点结构: + +- `analyze` +- `plan` +- `prepareExecution` +- `optimize` +- `routeModel` + +当前问题: + +- `runAutoAgentGraph()` 先完整执行一轮图,再把结果交给聊天接口 +- 用户首条可见回复往往晚于 Agent 分析完成 + +### 2.1.1 当前 Graph 的真实节点职责 + +当前 `lib/ai/auto-agent-graph.ts` 中的链路是: + +1. `analyze` +2. `plan` +3. `prepareExecution` +4. `optimize` +5. `routeModel` + +其中职责分别是: + +- `analyze` + - 调用 `smart-analyzer` 做意图分析 + - 输入包含: + - 文本 + - 当前图片 / 历史图片 + - 画布上下文 + - 历史消息 +- `plan` + - 根据 `analysis` 和当前可用工具生成执行计划 + - 已接入 `Skill Router + Specialist` +- `prepareExecution` + - 把 `plan` 归一化为执行层可消费的数据结构 + - 产出 `shouldApplyImagePipeline / shouldApplyVideoPipeline / shouldStayConversational` +- `optimize` + - 执行类任务才走提示词优化 + - 视频生成会额外生成 storyboard 规划 +- `routeModel` + - 调用 `routeToModel()` 选择最终回复或执行模型 + +当前最关键的问题是: + +- 这是一条**完整前置型链路** +- 即使用户只是普通聊天或看图问答,也要先经过整套分析 / 规划 / 路由,才能真正开始回复 + +### 2.2 Model Router + +当前聊天 / 识图 / 修图 / 生图 / 视频路由位于: + +- `lib/ai/model-router.ts` + +当前问题: + +- `chat` 和 `vision` 仍会进入统一 `routeToModel()` 逻辑 +- 对于 `Agent模式` 下的 `respond / analyze_image`,最终回复模型并未被锁定为 Agent 默认模型 + +### 2.2.1 当前聊天路由为什么会选到 Gemini + +当前 `lib/ai/model-router.ts` 中: + +- `chat.providerTypes = ["alibaba", "zhipu", "openai", "custom", "local", "gemini", "huanke-gemini", "volcano-multimodal"]` +- `vision.providerTypes = ["alibaba", "zhipu", "openai", "custom", "gemini", "huanke-gemini", "volcano-multimodal"]` + +这意味着: + +- 挂在 `openai` provider 下的多模态模型 +- 会优先于挂在 `custom` provider 下的多模态模型被选中 + +当前线上现象: + +- `Gemini-3.1-pro` 挂在 `openai` +- `doubao-seed-2-0-mini-260215` 挂在 `custom` + +所以在 `chat / vision` 路由阶段: + +- `Gemini-3.1-pro` 会先被挑中 + +另外,当前 `routeToModel()` 会优先剔除带 `agentAutoModel` 标记的模型,这也进一步放大了“Agent 默认模型只负责分析,不负责最终回复”的问题。 + +### 2.3 Chat API + +当前主请求入口位于: + +- `app/(chat)/api/chat/route.ts` + +当前问题: + +- `Smart Auto Mode` 的首响应依赖完整 Agent 分析结果 +- 对执行型任务虽然已有直连 executor,但没有统一的“立即确认”层 +- `selectedChatModel = "auto"` 时,用户理解是“Agent模式”,而系统实际行为是“自动路由模式” + +### 2.3.1 当前 `Agent模式` 的真实执行语义 + +在前端: + +- `selectedChatModel = "auto"` 被展示为 `Agent模式` + +但在后端: + +- `auto` 的真实语义是“允许后端自动分析并路由” +- 并不等于“强制使用 Agent 默认模型” + +因此当前会出现: + +1. Agent 默认模型负责分析 / 规划 +2. `routeToModel(chat/vision)` 再次挑选最终回复模型 +3. 最终回复模型可能变成另一个模型 + +这也是用户感知上“明明选了 Agent 模式,为什么回复的是 Gemini”的根因。 + +### 2.3.2 当前首响应为什么慢 + +`app/(chat)/api/chat/route.ts` 中,`Smart Auto Mode` 在真正输出前要做: + +1. 历史消息装载 +2. 目标图解析 +3. `runAutoAgentGraph()` +4. 分析结果、计划、推荐参数、路由结果整理 + +所以用户在很多情况下要等这一整段完成,才会看到第一句消息。 + +### 2.4 Specialist Executor + +当前 specialist 能力已接入: + +- `lib/ai/skill-router.ts` +- `lib/ai/specialists/index.ts` + +当前优点: + +- `conversation / vision / image_edit / image_generate / video_generate / canvas` 已能收敛计划 +- 修图、生图、视频已具备直接执行入口 + +当前缺口: + +- `respond / analyze_image` 场景还没有“锁定 Agent 默认模型”的最终回复约束 + +### 2.4.1 当前已落地的可执行能力 + +目前 Specialist 已经不只是“会判断”,而是具备以下确定性执行能力: + +- `removeBackground` +- `resizeImage` +- `extendImage` +- `boxCutout` +- `generateImage` +- `generateVideo` +- `publishToCanvas` + +这部分意味着: + +- 执行型任务已经具备基础能力 +- 当前最该补的是**响应编排**,不是再扩执行工具数量 + +## 2.5 Smart Analyzer 当前行为 + +当前 `lib/ai/smart-analyzer.ts` 不是“先规则后 LLM,规则命中就结束”的结构。 + +真实行为是: + +1. `quickKeywordMatch()` 目前固定返回低置信度 `0.3` +2. 这会**强制大多数请求进入 LLM analyzer** +3. analyzer 再结合图片与上下文生成: + - `intent` + - `subIntent` + - `conversationalReply` + - `clarifyingQuestions` + +这带来的影响是: + +- 即使是简单聊天,也很容易先走一轮 LLM 分析 +- 首响应进一步变慢 + +这也是后续要加入 `preResponseClassifier` 的原因之一。 + +## 2.6 Agent 默认模型当前真实角色 + +当前 Agent 默认模型主要承担: + +- Analyzer +- Planner +- Strategy / Prompt 生成 + +但在当前实现里,它**不是最终回复模型的强约束来源**。 + +这是体验和成本问题的关键根因。 + +## 2.7 当前前端对话渲染架构 + +当前聊天渲染并不是纯文本直出,已经具备一部分富文本能力。 + +### 2.7.1 当前渲染链路 + +主要文件: + +- `components/message.tsx` +- `components/elements/response.tsx` +- `components/elements/code-block.tsx` + +当前行为: + +1. `Message` 层按 `message.parts` 分发不同内容类型 +2. 文本内容进入 `Response` +3. `Response` 基于 `Streamdown` 渲染 Markdown +4. 代码块进入 `CodeBlock` +5. 图片和视频使用专用预览组件渲染 + +### 2.7.2 当前已经具备的能力 + +当前前端已经具备以下基础能力: + +- Markdown 渲染 +- 标题、列表、引用、加粗 +- fenced code block +- 代码复制按钮 +- 图片 / 视频专用渲染 +- 局部折叠式“图片分析”卡片 +- tool result 专用渲染 + +这说明我们不是“完全没有结构化对话能力”,而是缺少稳定的输出协议与按内容类型分层的前端呈现。 + +### 2.7.3 当前缺口 + +和 Gemini / 豆包官方体验相比,当前主要缺口是: + +1. 模型输出协议不稳定 + 同一类回答有时是散文、有时是 Markdown、有时是结构块混排。 + +2. 所有文本基本走同一个 `Response` 渲染器 + 聊天、识图、执行说明、代码、表格没有做 profile 分层。 + +3. 代码仍主要表现为聊天消息中的代码块 + 还没有升级成真正的文件 / 工件 / 模块级展示。 + +4. 表格虽可被 Markdown 渲染,但没有专门做交互层增强 + 例如: + - 表格横向滚动优化 + - 单元格复制 + - 表头固定样式 + - 数据块折叠 + +5. 执行型消息和普通对话消息共用同一套结构 + 这也是为什么生图模式和普通对话模式很容易互相影响展示体验。 + +## 2.8 新增“前置模型”标签的配置建议 + +建议把“前置模型”作为模型标签落到模型能力字段中,而不是单独硬编码在 provider 上。 + +推荐存储方式: + +- `capabilities.presenterModel = true` + +原因: + +1. 与现有: + - `multimodalModel` + - `agentAutoModel` + 的能力标记路线一致 +2. 不绑定特定 provider +3. 便于未来支持多个前置模型候选 + +前端配置入口建议: + +- 系统设置 +- AI 模型设置 +- 模型标签区域新增: + - `前置模型` + +运行时选择规则: + +1. 优先 `presenterModel = true` +2. 再要求模型启用 +3. 再要求支持当前场景所需能力 +4. 多个候选时,优先: + - `isDefault = true` + - 更小的 `sortOrder` + +## 3. 核心问题定义 + +### 3.1 为什么官方产品看起来“秒回” + +核心不是模型瞬间完成了全部思考,而是响应被拆成了多层: + +1. 先输出一句极短确认 +2. 同时启动流式输出 +3. 把重分析、工具执行、图片理解放到后台继续做 +4. 再分阶段把结果逐步渲染给用户 + +### 3.2 当前项目慢在哪里 + +当前大量请求是这条顺序: + +1. 先 Agent 分析 +2. 再 Planner +3. 再路由模型 +4. 再开始真正回复或执行 + +这意味着: + +- 首 token 被前置分析阻塞 +- 普通聊天 / 看图问答也走重链路 +- `Agent默认模型 + 最终回复模型` 两段式造成重复开销 + +补充一条当前非常关键的现状: + +- 当前并不是“Agent 模型不够快” +- 而是“系统把首响应交给了完整链路” + +所以第一优先级不应该是“先换更快模型”,而应该是: + +- 先把首响应从重链路里拆出来 + +### 3.3 当前 `Agent模式` 的语义不清晰 + +对用户来说: + +- `Agent模式` 应该意味着“由后台配置的 Agent 默认模型来智能处理” + +但对系统来说: + +- `selectedChatModel = "auto"` 只是“后端自动路由” +- 所以最终回复模型可能不是 Agent 默认模型 + +这会造成用户感知和系统行为不一致。 + +## 3.4 当前是否需要先引入一个火山引擎快速模型做前置对话 + +结论: + +- **不是第一优先级** +- 但可以作为 Phase 3 之后的可选增强项 + +原因: + +1. 首确认消息本身不应该依赖任何模型 + 最快的方案应该是: + - 规则判断 + - 直接输出短确认 + - 不消耗模型推理时间 + +2. 当前的主要瓶颈不是模型本体,而是编排方式 + 在现状下,即使换一个更快的火山模型: + - 仍然会先经历 Analyzer / Planner / Route + - 仍然可能有二次模型切换 + - 所以收益不会最大 + +3. 真正值得引入“快速前置模型”的场景是 + 在完成 Phase 1 和 Phase 2 后,如果: + - `respond / analyze_image` 的 P95 延迟仍偏高 + - 当前 Agent 默认模型无法在体验上达到目标 + 才考虑增加一个: + - 更轻量 + - 支持 vision + - OpenAI-compatible + 的火山快速模型,用于“首段实质性回复” + +推荐原则: + +- **首确认消息**:不能是空模板,应至少体现对象 / 动作 / 目标 +- **首段真实分析内容**:优先继续用当前 Agent 默认模型 +- **只有测下来仍慢**:再考虑引入更快火山模型 + +### 3.4.1 为什么不能做“纯假首确认” + +如果首确认只是固定模板,例如: + +- `收到` +- `我先看看` +- `正在分析` + +这种做法虽然快,但会明显损伤体验: + +1. 用户会一眼看出这是“假动作” +2. 不体现对象、动作、目标,无法建立“系统真的理解我了”的信任感 +3. 对长对话和多模态任务尤其突兀 + +因此本规划不建议做“纯模板 Fast Ack”,而应该做两类更合理的前置响应: + +1. **轻量语义确认** + - 通过轻量规则先抽出: + - 对象:图片 / 海报 / 书柜 / 视频 + - 动作:识别 / 修改 / 生成 / 分析 + - 目标:看内容 / 保留风格 / 替换背景 / 生成方案 + - 再拼成一条短而真实的确认句 + +2. **主持人模型首句** + - 用极快的前置模型生成 1 句短确认 + - 严格限制字数、预算和职责 + - 这条消息只负责“真实理解确认”,不负责完整分析 + +### 3.4.2 “主持人模型”在本项目里的合理定位 + +如果确定要引入一个火山引擎快速模型,最合理的定位不是替代 Agent 默认模型,而是: + +- **前台主持人** +- **后台 Agent 默认模型继续做深分析 / 规划 / 工具执行** + +推荐增加一个新的模型标签: + +- `前置模型` + +用途: + +- 被设置为该标签的模型,进入“主持人模型候选池” + +主要主持人模型选择规则建议: + +1. 优先选择同时满足: + - 已启用 + - 带 `前置模型` 标签 + - 非图片生成模型 + - 支持当前场景所需能力 + +2. 如果候选不止一个,优先级为: + - `isDefault = true` 的前置模型 + - `sortOrder` 更靠前的前置模型 + - 最后按创建顺序或名称稳定排序 + +3. 如果不存在前置模型: + - 回退到“轻量语义确认” + - 不阻塞主 Agent 链路 + +职责分工建议: + +1. 主持人模型 + - 目标:在 `300ms ~ 1200ms` 内输出一条短确认 + - 职责: + - 复述用户意图 + - 提示当前处理方向 + - 不做完整规划 + - 不做工具决策 + +2. Agent 默认模型 + - 目标:完成真正的理解、规划、识图、工具编排 + - 职责: + - 深度分析 + - 长上下文和工作记忆整合 + - Specialist 计划生成 + - `respond / analyze_image` 最终回复 + +### 3.4.3 火山引擎快速模型为什么值得作为候选 + +根据火山引擎官方资料,当前可考虑的方向包括: + +1. **Doubao-Seed-1.6-flash** + - 火山引擎开发者社区资料中强调: + - 极致速度 + - 多模态 + - 256K 上下文 + - TPOT 约 10ms + 来源:[Doubao-Seed-1.6-flash 介绍](https://developer.volcengine.com/articles/7535018293141667890) + +2. **Prefill** + - 火山引擎官方文档明确说明: + - `Prefill: true` 可显著降低首字延迟 + - 对支持深度思考的模型,建议关闭 thinking,避免回复延迟过高 + 来源:[配置大模型 LLM](https://www.volcengine.com/docs/6348/1581714) + +这意味着,如果我们要做“主持人模型”路线,火山引擎确实是合理候选。 + +### 3.4.4 但为什么它仍然不是短期第一步 + +即使有更快模型,如果以下问题不先处理: + +- `respond / analyze_image` 仍然二次路由 +- 首响应仍被前置完整 Agent 链路阻塞 +- 执行型任务没有前置确认层 + +那么只加一个更快模型,收益仍然会被结构性延迟抵消。 + +因此: + +- **短期**:先修编排,再上主持人模型 +- **中期**:主持人模型接入为增强项 + +## 4. 升级目标 + +### 4.1 用户体验目标 + +1. 首响应时间控制在 `300ms ~ 1500ms` +2. `聊天 / 识图` 场景下,明显减少等待空窗 +3. 执行类任务先出现“已收到、正在处理”的即时反馈 +4. 保留无限画布、占位图、工具执行、结果回传能力 + +### 4.2 技术目标 + +1. `respond / analyze_image` 单模型直出 +2. `edit_image / generate_image / generate_video` 多模型协同执行 +3. 将“Agent默认多模态模型”从“仅分析模型”升级为“聊天 / 识图场景的最终回复模型” +4. 保持现有 Specialist Executor 结构不推翻重来 +5. 让 `Agent模式` 的用户语义与系统语义重新一致 +6. 对话输出升级为“结构化 Markdown + 专用渲染器” +7. 对话体验升级不能破坏生图模式现有图片结果结构 +8. 预留“火山主持人模型”前置能力,但不取代 Agent 默认模型 +9. 增加模型标签 `前置模型`,用于选择主持人模型 +10. 建立主持人模型与 Agent 模型的平滑交接协议,避免语义重复与输出冲突 + +## 5. 目标行为定义 + +### 5.1 聊天 / 识图 + +当 `nextAction` 为: + +- `respond` +- `analyze_image` + +且 Agent 默认模型具备所需能力时: + +- 直接锁定最终回复模型为后台配置的 Agent 默认模型 +- 不再走 `routeToModel(chat/vision)` 的二次挑选 + +目标效果: + +- `doubao-seed-2-0-mini-260215` 既做分析,也做最终回复 +- 避免出现“豆包分析 + Gemini回复”的双模型串行 +- 避免在 `auto` 模式下再次被 `openai` provider 下的 Gemini 抢走 + +### 5.2 修图 / 生图 / 视频 + +当 `nextAction` 为: + +- `edit_image` +- `generate_image` +- `generate_video` + +目标行为: + +1. 先立即发确认消息 +2. 再显示状态占位 +3. 后台继续跑 Specialist Executor +4. 最终把结果补回消息流和画布 + +补充说明: + +- 这类任务仍然保留多模型协作 +- 因为真正的价值在执行模型能力,而不是聊天首响应速度 + +### 5.3 超长规划型请求 + +例如: + +- 长提示词分析 +- 多阶段方案规划 +- 大量上下文组合任务 + +目标行为: + +1. 先发极短确认 +2. 后台完成 Planner +3. 再增量补出结构化结果 + +### 5.4 结构化对话输出 + +当场景为: + +- 普通聊天 +- 识图分析 +- 方案建议 +- 代码输出 +- 数据表格 + +目标行为: + +1. 模型输出遵循稳定结构协议 +2. 前端按内容类型使用专用渲染 +3. 代码 / 表格 / 分析块具备清晰交互 + +### 5.5 生图模式不受影响 + +升级原则: + +- 生图 / 修图结果仍然保持当前图片卡片和占位图机制 +- 结构化对话升级只增强: + - 聊天文本 + - 识图分析 + - 执行计划 + - 代码 / 表格 / 数据类内容 + +不改变: + +- 图片 Markdown 结果协议 +- 图片结果回画布机制 +- 图片占位图和状态流 + +### 5.6 主持人模型接管协议 + +当系统启用“主持人模型”时,必须遵守以下行为约束: + +#### A. 主持人模型只负责前台第一段响应 + +主持人模型只允许做: + +1. 复述用户当前目标 +2. 告知正在处理的方向 +3. 给出非常短的过渡性说明 + +主持人模型禁止做: + +1. 最终结论 +2. 详细方案 +3. 工具决策说明 +4. 最终代码 / 表格 / 执行报告 +5. 与 Agent 最终结果语义重叠的大段内容 + +#### B. Agent 模型负责真正的最终内容 + +Agent 默认模型负责: + +1. 深度理解 +2. 长上下文整合 +3. Specialist 路由 +4. 工具执行决策 +5. 最终可持久化回复 + +#### C. 主持人输出默认视为“瞬时前言” + +为避免重复和冲突,主持人模型的输出默认不直接持久化为最终聊天记录,而是: + +1. 作为 SSE / 流式的临时前言展示 +2. Agent 最终回复准备好后,被替换或无缝续接 +3. 只有当 Agent 超时或失败时,才将主持人前言作为兜底消息保留 + +这样可以避免: + +- 两条助手消息重复 +- 先说一遍、后面又完整重复一遍 +- 数据库存入大量低价值“前言消息” + +#### D. 必须建立平滑交接边界 + +主持人模型在说话过程中,要持续监控 Agent 模型是否已经准备完毕。 + +一旦 Agent 已经具备输出条件,主持人模型应: + +1. 在句末自然收束 +2. 停止继续扩写 +3. 把话语权移交给 Agent 最终结果 + +交接必须满足: + +1. 在自然停顿点结束 +2. 不截断半句话 +3. 不和 Agent 内容重叠 + +推荐的收束边界: + +- 一句话结束后停止 +- 或一个很短的两句确认后停止 + +#### E. 语义必须分层,避免重复 + +建议做两层语义协议: + +1. 主持人语义层 + - 只说“我理解你要做什么 / 我接下来会先做什么” + +2. Agent 语义层 + - 不再重复“收到 / 我先看看 / 我来帮你” + - 直接进入: + - 分析 + - 方案 + - 结果 + - 执行说明 + +实现上建议: + +- 将主持人前言摘要作为隐藏上下文传给 Agent +- 增加约束: + - `不要重复前言已表达的确认语` + - `从新的信息开始输出` + +## 6. 升级方案 + +## 6.1 Phase 1:锁定聊天 / 识图最终回复模型 + +### 目标 + +先解决最直接的成本和体验问题: + +- `respond / analyze_image` 不再二次选模型 + +### 具体改动 + +#### A. 扩展 `AgentPreparedExecution` + +文件: + +- `lib/ai/auto-agent-graph.ts` + +新增字段建议: + +- `lockFinalReplyToAgentModel: boolean` +- `fastAckType: "chat" | "analyze" | "edit" | "image" | "video" | "clarify"` +- `requiresHeavyPlanner: boolean` +- `skipIntentRouterForReply: boolean` + +#### B. 在 Specialist 中标注 conversation / vision 为“单模型直出” + +文件: + +- `lib/ai/specialists/index.ts` + +规则: + +- `conversation` -> `lockFinalReplyToAgentModel = true` +- `vision` -> `lockFinalReplyToAgentModel = true` + +#### C. 在 Chat API 中跳过 `chat / vision` 二次路由 + +文件: + +- `app/(chat)/api/chat/route.ts` + +逻辑: + +当满足: + +- `selectedChatModel === "auto"` +- `execution.lockFinalReplyToAgentModel === true` +- Agent 默认模型可用 + +则: + +- `finalModelId = preferredAgentModelId || configuredAgentModelId` +- 不再根据 `chat / vision` 走 `routeToModel()` +- 只在 Agent 默认模型不可用时才 fallback 到其它候选 + +### 预期收益 + +- 降低聊天 / 识图延迟 +- 避免双模型消耗 +- 保持 Agent 模式与用户认知一致 +- 明确把 `Agent模式` 从“自动选模型”升级成“以 Agent 默认模型为核心的编排模式” + +## 6.2 Phase 2:增加 Fast Ack 首确认层 + +### 目标 + +让用户在极短时间内看到第一句反馈。 + +### 具体改动 + +#### A. 新增 `preResponseClassifier` + +文件建议: + +- `app/(chat)/api/chat/route.ts` + +作用: + +- 不调用大模型 +- 只做轻量规则判断 +- 快速分类为: + - `chat` + - `analyze_image` + - `edit_image` + - `generate_image` + - `generate_video` +- `clarify` + +补充: + +- 当前 `smart-analyzer` 由于 `quickKeywordMatch()` 被故意压到低置信度,无法承担“快速首响应判断” +- 所以需要单独新增一个真正轻量级的前置分类器 + +#### B. 新增 `emitImmediateAck()` + +复用现有: + +- `createUIMessageStream` +- `writeAssistantTextChunk` +- `writeImageGenerationStatusChunk` + +建议文案: + +- 聊天:`我在,正在看你的问题。` +- 识图:`我先看一下这张图。` +- 修图:`收到,我先处理这张图。` +- 生图:`收到,正在开始生成。` +- 视频:`收到,正在开始生成视频。` + +#### C. 与现有执行链整合 + +执行类任务先发: + +- `即时确认` +- `占位状态` + +然后后台继续跑: + +- `runAutoAgentGraph()` +- `Specialist Executor` + +注意: + +- 这里不是推翻现有 Agent Graph +- 而是在它前面增加一个“低成本前置层” + +### 预期收益 + +- 用户不会看到无反馈空窗 +- 生图 / 修图 / 视频体验更接近官方产品 + +## 6.3 Phase 3:分阶段流式渲染 + +### 目标 + +把输出拆成 3 段: + +1. 首确认 +2. 分析/处理中状态 +3. 最终结果 + +### 具体改动 + +#### A. `respond / analyze_image` + +策略: + +- 首确认后,立即由 Agent 默认模型开始流式回复 +- 不中间插额外模型 + +#### B. `edit / image / video` + +策略: + +- 首确认消息立即输出 +- 前端显示: + - 图片生成占位 + - 视频处理中状态 + - 画布处理中提示 +- 执行完成后写入最终消息和画布节点 + +#### C. 结构化内容延迟补出 + +对于“需求理解 / 图片分析 / 执行计划”这类结构化块: + +- 不要求首屏一次全出 +- 可以作为第二阶段内容追加 + +## 6.3.1 Phase 3A:引入火山“主持人模型” + +### 目标 + +让系统在真正的 Agent 深分析开始前,先用一个极快模型给出“短而真实”的首句确认,为后台 Agent 赢得准备时间。 + +### 定位 + +主持人模型不是新的主 Agent,也不是最终回复模型的替代者。 + +它的角色是: + +- 抢首响应 +- 降低用户等待空窗 +- 把“我理解你要做什么”这一步前移 + +而不是: + +- 完整规划 +- 长推理 +- 工具决策 +- 产出最终复杂结果 + +### 适用范围 + +仅用于: + +- `selectedChatModel = "auto"` +- 非固定模型场景 +- 首响应阶段 + +不用于: + +- 用户显式选定固定模型后的直出场景 +- 已经进入 Specialist Executor 的最终结果阶段 + +### 推荐模型候选 + +优先评估: + +- 火山引擎 `Doubao-Seed-1.6-flash` + +推荐配合: + +- `Prefill: true` +- 关闭深度思考 / thinking +- 严格限制输出 token + +### 输出约束 + +主持人模型输出必须满足: + +1. 单句或两句 +2. 20 到 60 个字以内 +3. 必须体现用户当前对象和动作 +4. 禁止进入完整分析结构 +5. 禁止做最终结论承诺 + +### 与后台 Agent 的并行关系 + +建议时序: + +1. 用户发消息 +2. 主持人模型立即返回短确认 +3. 后台同时启动: + - 目标图解析 + - 长记忆 / 上下文装载 + - Agent Graph + - Specialist Executor +4. 再由 Agent 默认模型或执行模型输出真正结果 + +### 运行时状态机 + +建议新增一组明确状态: + +1. `prelude_pending` +2. `prelude_streaming` +3. `agent_planning` +4. `handoff_ready` +5. `final_streaming` +6. `completed` +7. `fallback_prelude_only` + +### 风险控制 + +1. 主持人模型不能和最终回复模型抢语义主导权 +2. 主持人模型内容必须短,不可提前输出错误结论 +3. 如果主持人模型不可用,应自动回退到轻量规则确认 +4. 如果 Agent 已进入 `handoff_ready`,主持人模型必须在自然句末停止 +5. 如果主持人模型已经结束,Agent 最终回复不得从“收到 / 我来帮你”重新开始 + +## 6.4 Phase 4:结构化对话渲染升级 + +### 目标 + +把当前“统一 Markdown 渲染”升级成“结构化内容协议 + 多渲染器前端”。 + +### 具体改动 + +#### A. 统一输出协议 + +为以下场景定义稳定的 Markdown 约定: + +- 普通聊天 +- 识图分析 +- 执行报告 +- 代码输出 +- 表格报告 + +建议约定: + +- 标题:模块边界 +- 列表:要点 +- 表格:对比 / 参数 / 方案 +- fenced code block:代码 +- blockquote:注意事项 / 引用 + +#### B. 增加 `responseProfile` + +建议新增前端响应 profile: + +- `conversation` +- `analysis` +- `execution_report` +- `code_artifact` +- `table_report` +- `image_result` +- `video_result` + +不同 profile 使用不同渲染策略,而不是所有文本都走同一个 `Response`。 + +#### C. 升级代码类输出 + +当前已有 `CodeBlock + CopyButton`,但仍偏“聊天消息内嵌代码”。 + +建议逐步升级为: + +1. 多代码块目录 +2. 文件名 / 模块名显示 +3. 复制文件内容 +4. 长代码折叠 +5. 发送到文档 / 工件面板 + +#### D. 升级表格类输出 + +建议增加: + +1. Markdown 表格专用样式 +2. 小屏幕横向滚动 +3. 表头固定视觉层级 +4. 单元格复制 +5. 大表格折叠 / 展开 + +#### E. 升级分析类输出 + +当前只有“图片分析”有专门折叠卡片。 + +建议扩展为统一 section card: + +- `需求理解` +- `图片分析` +- `执行计划` +- `输出设置` +- `风险提示` + +#### F. 升级 Artifact / Workbench 路线 + +未来建议把复杂输出从“聊天气泡”升级成“聊天 + 工件”: + +1. 代码输出 -> 文档 / 编辑器 +2. 表格输出 -> 结构化工作台 +3. 分析规划 -> 侧栏 / 面板 / 画布节点 + +## 7. 代码触点清单 + +### 必改 + +- `app/(chat)/api/chat/route.ts` +- `lib/ai/auto-agent-graph.ts` +- `lib/ai/specialists/index.ts` +- `lib/db/ai-model-flags.ts` +- `app/api/admin/ai-models/route.ts` +- `app/(admin)/settings/admin-settings-client.tsx` + +### 可能调整 + +- `lib/ai/model-router.ts` +- `components/canvas/CanvasChatPanel.tsx` +- `components/multimodal-input.tsx` +- `components/message.tsx` +- `components/elements/response.tsx` +- `components/elements/code-block.tsx` + +## 8. 验收标准 + +### 8.1 聊天 / 识图 + +1. `Agent模式` 下,`respond / analyze_image` 只使用 Agent 默认模型完成最终回复 +2. 日志中不再出现: + - Agent 默认模型分析 + - 再路由到 `Gemini-3.1-pro` 回答 + +### 8.2 首响应 + +1. 普通聊天请求在 `300ms ~ 1500ms` 内看到确认消息 +2. 修图 / 生图 / 视频任务在 `1500ms` 内看到“正在处理”反馈 + +### 8.3 执行类任务 + +1. 修图 / 生图 / 视频仍能正常执行 +2. 结果仍能回到: + - 聊天气泡 + - 无限画布 + +### 8.4 结构化对话 + +1. 代码类回复必须具备稳定的代码块渲染和复制交互 +2. 表格类回复必须可读、可滚动、不破版 +3. 分析类回复必须按 section 呈现,而不是纯长段落 +4. 生图模式的图片结果结构不能被文本升级破坏 + +## 9. 风险与注意事项 + +1. 需要避免“首确认”和“最终结果”消息重复或顺序错乱 +2. `selectedChatModel = auto` 与“固定模型”模式要严格区分 +3. 如果 Agent 默认模型暂时不可用,仍需有 fallback 机制 +4. 执行类任务失败时,需要把“处理中”状态转成明确失败态 +5. 结构化协议升级后,要兼容旧消息显示 +6. 对话渲染升级必须与图片 Markdown 结果协议兼容 + +## 10. 推荐实施顺序 + +### Step 1 + +先做: + +- `respond / analyze_image` 锁定 Agent 默认模型 + +原因: + +- 改动最小 +- 收益最大 +- 先解决双模型和双份算力问题 +- 也是当前“为什么 Agent 模式最后跑到 Gemini”问题的直接修复点 + +### Step 2 + +再做: + +- `emitImmediateAck()` +- 执行类首确认与占位状态 + +### Step 3 + +最后做: + +- 前置轻量分类 +- 后台完整分析并行 +- 分阶段流式渲染 +- 如有必要,再评估是否增加火山快速前置模型 + +### Step 4 + +最后再做: + +- 结构化对话协议 +- section card 统一样式 +- 代码 / 表格专用渲染 +- Artifact / Workbench 路线 + +## 11. 最终建议 + +按照当前项目情况,最合理的实现不是推倒重来,而是在现有架构上做三件事: + +1. `聊天 / 识图` 单模型直出 +2. `执行任务` 立即确认 + 后台执行 +3. `Agent模式` 从“自动选模型”升级成“以 Agent 默认模型为核心的编排模式” +4. 对话渲染从“统一 Markdown 渲染”升级成“结构协议 + 专用渲染器” + +这会同时提升: + +- 用户体感速度 +- Agent 模式一致性 +- 识图 / 对话成本控制 +- 执行类任务的可理解性 + +## 13. 我们与 Gemini 官方对话的主要差异 + +### 13.1 响应编排差异 + +Gemini 官方: + +- 首确认快 +- 流式明显 +- 大任务分阶段输出 + +我们当前: + +- 首响应经常等待完整 Agent 链路完成 + +### 13.2 输出协议差异 + +Gemini 官方: + +- 标题、列表、代码、表格的结构一致性高 + +我们当前: + +- 虽已有 Markdown 渲染,但输出协议不稳定 + +### 13.3 工件能力差异 + +Gemini 官方: + +- 复杂内容常以工件、文件、代码面板形式呈现 + +我们当前: + +- 多数复杂结果仍堆在聊天气泡里 + +### 13.4 多渲染器差异 + +Gemini 官方: + +- 代码、表格、分析、引用、工具结果往往各有不同展示层 + +我们当前: + +- 文本主路径仍主要共用一个 `Response` + +## 14. 未来升级建议 + +### 14.1 短期 + +1. 先完成 Phase 1 +2. 再完成 Fast Ack +3. 先解决体验和成本上的主矛盾 +4. 不急着让主持人模型取代 Agent 默认模型 + +### 14.2 中期 + +1. 增加 `responseProfile` +2. 分析类消息卡片化 +3. 代码类输出接入工件面板 +4. 接入火山主持人模型,并和 Agent 模型做平滑交接 + +### 14.3 长期 + +1. 建立统一 `Agent Response Schema` +2. 把 Planner 结果与前端展示协议解耦 +3. 让聊天、画布、详情生成共享同一套结构化响应层 + +## 15. 参考能力来源 + +可作为后续设计参考的能力来源包括: + +- Gemini 模型能力页 +- Gemini 3 指南 +- Gemini Structured Output +- Gemini Code Execution +- Gemini OpenAI Compatibility + +它们共同说明:官方强体验的关键是“模型能力 + 输出协议 + 前端渲染器”的组合,而不是只换一个更快的模型。 + +参考链接: + +- [Gemini models](https://ai.google.dev/models/gemini) +- [Gemini 3 guide](https://ai.google.dev/gemini-api/docs/gemini-3) +- [Structured output](https://ai.google.dev/gemini-api/docs/structured-output) +- [Code execution](https://ai.google.dev/gemini-api/docs/code-execution) +- [OpenAI compatibility](https://ai.google.dev/gemini-api/docs/openai) + +## 12. 关于“是否需要火山快速模型先和用户对话”的最终建议 + +当前决策调整为: + +- **短期:先修编排** +- **中期:明确引入火山主持人模型** + +短期仍然先修编排的原因: + +1. 先锁定 `respond / analyze_image` 到 `doubao-seed-2-0-mini-260215` +2. 先补 `Fast Ack` 首确认层 +3. 这两步做完,体感延迟通常就会明显下降 + +在此基础上,中期明确增加: + +- 一个火山引擎快速模型作为“主持人模型” + +它承担的职责是: + +- 非执行类请求的第一段实质性响应 +- 在后台 Agent 深分析开始时,降低用户感知等待 + +不建议的做法: + +- 一上来先换模型 +- 但编排方式完全不改 + +因为这样通常只会: + +- 稍微降低部分推理耗时 +- 却保留整个“前置重链路”的结构性延迟 + +推荐落地顺序: + +1. Phase 1:锁定 `respond / analyze_image` 到 Agent 默认模型 +2. Phase 2:增加首确认与分阶段流式 +3. Phase 3A:接入火山主持人模型 + +## 3.5 为什么 Gemini / 豆包官方对话体验更强 + +### 3.5.1 关键不在单个模型,而在“模型 + 协议 + 渲染器” + +Gemini / 豆包官方产品之所以看起来“更会说、排版更整齐、代码更像模块、表格更清晰”,核心并不是模型天然输出了 UI,而是它们通常具备以下组合: + +1. 流式输出 +2. 多模态输入 +3. Thinking / reasoning +4. 结构化输出或稳定 Markdown 约定 +5. 工具调用 / 外部能力 +6. 前端专用渲染器 + +也就是说,它们走的是: + +- 模型能力层 +- 响应协议层 +- 前端呈现层 + +联合优化路线。 + +### 3.5.2 Gemini 官方路线的能力来源 + +Gemini 官方公开支持以下能力: + +- 多模态输入 +- Thinking +- Structured Output +- Code Execution +- OpenAI-compatible 接口 + +这些能力让 Gemini 官方产品更容易实现: + +- 标题 + 列表 + 结构块 +- 可执行代码工作台 +- 工具结果和正文分层展示 +- 表格化结果输出 + +### 3.5.3 豆包官方路线的合理推断 + +对于豆包官方产品,更合理的技术推断是: + +- 模型输出的是 Markdown 或结构片段 +- 客户端按内容类型进行专用渲染 +- 代码、表格、结构块并不是“单靠模型本身生成的 UI”,而是“模型输出 + 前端 renderer” + +### 3.5.4 对我们最重要的结论 + +如果我们想接近 Gemini / 豆包官方体验,真正该升级的是: + +1. 响应编排 +2. 结构化输出协议 +3. 多渲染器前端 + +而不是只改模型选择。