多模态消息渲染实战:TinyRobot Bubble内容解析与contentResolver用法

多模态消息渲染实战:TinyRobot Bubble 内容解析与 contentResolver 用法

AI 时代,消息不再只是纯文本。一张图片、一段代码、一个表格、一个工具调用的结果------这些多模态内容需要在同一个对话界面中和谐共存。TinyRobot Bubble 通过 contentResolver 和渲染器体系,为多模态消息渲染提供了优雅的解决方案。

本文将深入 contentResolver 的原理与实战,带你掌握从自定义数据结构到多模态渲染的完整链路。


一、理解消息内容模型

1.1 ChatMessageContent 类型

TinyRobot 采用 OpenAI 风格的消息结构,内容类型定义如下:

typescript 复制代码
type ChatMessageContent = string | ChatMessageContentItem[]

interface ChatMessageContentItem {
  type: string  // 内容类型标识,用于匹配渲染器
  [key: string]: any  // 其他字段自由扩展
}

这意味着消息内容可以是:

  • 纯字符串"这是一段文字"
  • 内容数组[{ type: 'text', text: '...' }, { type: 'image_url', image_url: { url: '...' } }]

1.2 渲染链路

从原始消息到最终渲染,Bubble 内部经过以下步骤:

go 复制代码
原始消息 (BubbleMessage)
  → contentResolver 解析 → 得到 ChatMessageContent
  → 统一化处理 → string 转为 [{type:'text', text: string}]
  → 渲染器匹配 → 根据 type/role/loading 找到对应渲染器
  → 渲染器执行 → 产出 VNode

contentResolver 是这条链路的入口------它决定"从消息的哪个字段取内容"。


二、contentResolver 基础用法

2.1 默认行为

默认情况下 ,Bubble 直接使用 message.content 作为内容。这是最常见的场景,无需任何配置:

vue 复制代码
<tr-bubble content="直接展示的内容" />

2.2 从 state 字段提取内容

AI 消息常常在 content 字段存储主要回复,而把额外信息放在 state 中。contentResolver 让你从任意字段提取渲染内容:

vue 复制代码
<template>
  <tr-bubble v-bind="message" :content-resolver="customResolver" />
</template>

<script setup lang="ts">
import type { BubbleMessage, ChatMessageContent } from '@opentiny/tiny-robot'

const message: BubbleMessage = {
  role: 'ai',
  content: '这是默认 content 字段',
  state: {
    text: '这是从 state.text 提取的内容',
    extra: '这是 state.extra 中的自定义数据',
  },
}

// 从 state.text 提取内容替代默认 content
const customResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
  return msg.state?.text as string | undefined
}
</script>

2.3 组合多个字段

更复杂的场景:将 content 和 state.extra 合并展示:

typescript 复制代码
const combinedResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
  const content = (msg.content as string) || ''
  const extra = (msg.state?.extra as string) || ''
  return `${content}\n\n状态数据:${extra}`
}

2.4 在 BubbleList 中使用

BubbleList 也支持 contentResolver,对所有消息统一生效:

vue 复制代码
<tr-bubble-list
  :messages="messages"
  :role-configs="roleConfigs"
  :content-resolver="myResolver"
/>

三、多模态内容实战

3.1 图文混合消息

OpenAI 风格的多模态消息,content 为数组:

typescript 复制代码
const messages = [
  {
    role: 'user',
    content: [
      { type: 'text', text: '这张照片是在哪里拍的?' },
      { type: 'image_url', image_url: { url: 'https://example.com/beach.jpg' } },
    ],
  },
]

Bubble 自动识别 type: 'image_url',使用 Image 渲染器。

3.2 contentRenderMode 控制

contentRenderMode 决定数组内容的渲染方式:

  • 'single'(默认):所有内容在同一个 box 中
  • 'split':每个内容项独立一个 box
vue 复制代码
<!-- 图文分离渲染 -->
<tr-bubble :content="imageMessage" content-render-mode="split" />

<!-- 图文混合渲染 -->
<tr-bubble :content="imageMessage" content-render-mode="single" />

在 BubbleList 中的特殊规则

contentRenderMode 与组内消息数共同决定渲染方式:

contentRenderMode 组内消息数 数组内容渲染
split 1 每项独立 box
split >1 所有内容在一个 box
single 任意 所有内容在一个 box

这个设计保证了:单条消息时图文独立展示更美观,多条消息时紧凑不凌乱。

3.3 自定义内容类型

当你的后端返回非标准的消息格式时,contentResolver 可以将其转换为 TinyRobot 可识别的结构:

typescript 复制代码
// 后端返回的自定义格式
interface CustomMessage {
  role: string
  data: {
    summary: string
    chart?: { type: string; config: Record<string, any> }
  }
}

const customResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
  const data = msg.state?.data as CustomMessage['data']
  if (!data) return msg.content

  const items: ChatMessageContentItem[] = [
    { type: 'text', text: data.summary },
  ]

  if (data.chart) {
    items.push({ type: 'chart', chart: data.chart })
  }

  return items
}

然后通过渲染器匹配规则,为 type: 'chart' 注册自定义渲染器(下一篇详细讲解)。


四、contentResolver 与渲染器的协作

contentResolver 和渲染器体系是互补的:

  1. contentResolver 决定"取什么内容"
  2. 渲染器 决定"怎么渲染内容"

完整的自定义流程:

vue 复制代码
<template>
  <tr-bubble-provider
    :content-renderer-matches="rendererMatches"
  >
    <tr-bubble-list
      :messages="messages"
      :role-configs="roleConfigs"
      :content-resolver="myResolver"
    />
  </tr-bubble-provider>
</template>

contentResolver 先把自定义数据转为标准 ChatMessageContentItem,渲染器再根据 type 字匹选择对应的渲染组件。


五、实战案例:多模态 AI 助手

以下是一个支持文本、图片、代码、工具调用的完整多模态对话:

typescript 复制代码
const messages = [
  { role: 'user', content: '帮我看看这张图片,并给出代码示例' },
  {
    role: 'user',
    content: [
      { type: 'text', text: '这张截图有什么问题?' },
      { type: 'image_url', image_url: { url: 'https://example.com/screenshot.png' } },
    ],
  },
  {
    role: 'assistant',
    content: [
      { type: 'text', text: '我看到截图中有一个布局问题,以下是修复代码:' },
      { type: 'text', text: '```css\n.container { display: flex; }\n```' },
    ],
    reasoning_content: '首先分析图片内容...',
    tool_calls: [
      { id: 'call_1', type: 'function', function: { name: 'analyze_image', arguments: '{}' } },
    ],
  },
]

对应的 roleConfigs 和 contentResolver:

typescript 复制代码
const roleConfigs = {
  user: { placement: 'end', avatar: userAvatar },
  assistant: { placement: 'start', avatar: aiAvatar },
  tool: { hidden: true }, // 隐藏工具结果消息
}

const myResolver = (msg: BubbleMessage): ChatMessageContent | undefined => {
  // 优先使用 content,如果为空则尝试 reasoning_content
  if (msg.content) return msg.content
  if (msg.reasoning_content) return msg.reasoning_content
  return undefined
}

六、CSS 变量定制

多模态渲染的样式同样通过 CSS 变量控制:

文本样式:

css 复制代码
--tr-bubble-text-color: #333;
--tr-bubble-text-font-size: 14px;
--tr-bubble-text-line-height: 1.6;

图片样式:

css 复制代码
--tr-bubble-image-max-width: 300px;
--tr-bubble-image-max-height: 200px;
--tr-bubble-image-border-radius: 8px;
--tr-bubble-image-space-y: 8px;

嵌入在其他 box 中的图片:

css 复制代码
--tr-bubble-image-embedded-border: 1px solid #ddd;
--tr-bubble-image-embedded-border-radius: 6px;
--tr-bubble-image-embedded-margin-block: 8px;

七、总结

contentResolver 是多模态消息渲染的关键入口:

  1. 默认 :直接使用 message.content
  2. 自定义提取:从 state、metadata 等任意字段取内容
  3. 组合解析:将多个字段合并为渲染内容
  4. 格式转换:将后端自定义格式转为标准 ChatMessageContentItem

配合渲染器体系,contentResolver 构成了"取内容 → 化格式 → 选渲染 → 出视觉"的完整链路。下一篇我们将深入渲染器体系与状态管理,揭秘 Bubble 的架构内核。


TinyRobot 官网https://opentiny/tiny-robot GitHub 仓库github.com/opentiny/ti...

相关推荐
英勇无比的消炎药1 小时前
架构剖析:TinyRobot Bubble渲染器状态管理与工具调用机制
vue.js
gg159357284601 小时前
Uni-app跨平台开发全解课程:从零基础到企业级多端落地实战
vue.js·uni-app
阿猫的故乡2 小时前
Vue + Axios 从入门到封装:拦截器、错误处理、请求取消、接口管理全搞定
前端·javascript·vue.js
秃头网友小李3 小时前
前端难点:Vue3 响应式遇上 Three.js / ECharts —— 为什么要用 shallowRef?
前端·vue.js
长空任鸟飞_阿康3 小时前
RAG 文档摄入全链路,从原理到生产落地
vue.js·人工智能·python
星空3 小时前
Node.js (Express) + Vue2 Axios 前后端交互 CRUD
vue.js·node.js·express
Hooray4 小时前
前端暗黑模式的适配艺术
前端·vue.js·视觉设计
有梦想的程序星空5 小时前
【环境配置】使用 Vue CLI 构建 Vue 项目脚手架完整指南
前端·javascript·vue.js
岁月宁静14 小时前
RAG 文档摄入全链路,从原理到生产落地
vue.js·人工智能·python