生态与协议

自定义渲染协议

从 boltArtifact 到业务协议:打造你自己的 AI 组件系统

📚 学习目标

学完这篇文章后,你将能够:

  • 设计一套适合业务场景的 AI 渲染协议
  • 使用流式解析器实时提取协议标签与数据
  • 将结构化输出映射为可交互的 React 组件
  • 评估该方案与 Function Calling、Markdown 代码块的差异

1 为什么需要自定义渲染协议?

在真实业务里,AI 输出往往不该停留在纯文本。

常见诉求:

  • 电商:输出商品卡片、比价表
  • 数据分析:输出图表组件
  • 表单场景:输出可编辑表单
  • 工作流:输出节点化流程视图

核心思路是:让模型输出“结构化标签”,前端实时解析并渲染为组件。


2 协议设计框架

2.1 通用结构

<{namespace}{ComponentType} {...attributes}>
  <{namespace}{ActionType} {...actionAttributes}>
    {content}
  </{namespace}{ActionType}>
</{namespace}{ComponentType}>

设计要点:

  • namespace:命名空间,避免与原生 HTML 冲突
  • ComponentType:容器类型(如卡片、图表、表格)
  • ActionType:子动作类型(如数据块、交互意图)
  • attributes:元数据(id、类型、版本)
  • content:可解析内容(通常是 JSON)

2.2 命名规范

元素规范示例
命名空间小写,2-4 字母aibiz
组件标签PascalCase 或固定前缀aiCardbizChart
属性名camelCasedataTypechartId
ID 值kebab-caseproduct-card-1

3 实战案例:电商推荐协议

3.1 协议示例

<aiCard id="product-rec-1" type="product">
  <aiData format="json">
    {"id":"SKU001","name":"iPhone 15","price":5999}
  </aiData>
</aiCard>

<aiCard id="product-list-1" type="productList">
  <aiData format="json">
    [{"id":"SKU001"},{"id":"SKU002"}]
  </aiData>
</aiCard>

<aiCard id="compare-table-1" type="comparison">
  <aiData format="json">
    {"products":["A","B"],"dimensions":["价格","电池"]}
  </aiData>
</aiCard>

3.2 类型定义

export type CardType = 'product' | 'productList' | 'comparison' | 'chart' | 'form';
export type DataFormat = 'json' | 'markdown' | 'html';

export interface AiCardData {
  id: string;
  type: CardType;
}

export interface AiDataAction {
  format: DataFormat;
  content: string;
}

export interface AiCard extends AiCardData {
  data: AiDataAction;
}

4 流式解析器实现

下面是协议解析器的关键状态:

  • insideCard
  • insideData
  • currentCard
  • currentData
const CARD_TAG_OPEN = '<aiCard';
const CARD_TAG_CLOSE = '</aiCard>';
const DATA_TAG_OPEN = '<aiData';
const DATA_TAG_CLOSE = '</aiData>';

interface ParserState {
  position: number;
  insideCard: boolean;
  insideData: boolean;
  currentCard?: { id: string; type: string };
  currentData: { format: 'json' | 'markdown' | 'html'; content: string };
}

export class AiMessageParser {
  private messages = new Map<string, ParserState>();

  parse(messageId: string, input: string): string {
    // 典型状态机逻辑:
    // 1) 识别 <aiCard>
    // 2) 进入 <aiData> 累积内容
    // 3) 识别闭合标签并触发回调
    // 4) 输出去标签后的可显示文本
    return input;
  }
}

工程关键点:

  1. 支持“标签未闭合”的半包输入。
  2. messageId 维护独立状态,避免多消息串流污染。
  3. 提供 reset(),防止长会话状态泄漏。

5 标签到 React 组件的映射

常见做法是插入占位元素,再在 Markdown 渲染层替换。

function createCardPlaceholder(messageId: string, cardId: string) {
  return `<div class="__aiCard__" data-message-id="${messageId}" data-card-id="${cardId}"></div>`;
}
const components = {
  div: ({ className, node, children }: any) => {
    if (className?.includes('__aiCard__')) {
      const messageId = node?.properties?.dataMessageId;
      const cardId = node?.properties?.dataCardId;
      return <AiCardRenderer messageId={messageId} cardId={cardId} />;
    }
    return <div>{children}</div>;
  },
};

建议一定做“未知类型降级”:

  • 未识别组件类型时,渲染 JSON 原文
  • 不要直接抛错中断整条消息渲染

6 回调事件系统

解析器至少应支持四类回调:

interface ParserCallbacks {
  onCardOpen?: (data: { messageId: string; id: string; type: string }) => void;
  onCardClose?: (data: { messageId: string; id: string; type: string; content: string }) => void;
  onDataStart?: (data: { cardId: string; format: string }) => void;
  onDataEnd?: (data: { cardId: string; content: string }) => void;
}

回调通常用于:

  • 更新前端状态仓库
  • 写入虚拟文件系统
  • 触发 shell/工具执行
  • 同步组件加载状态

7 与其他方案对比

7.1 vs Function Calling

维度自定义渲染协议Function Calling
流式可见性✅ 强⚠️ 通常偏结构化调用
文本+组件混排✅ 自然❌ 需要额外层
前端可控渲染✅ 高⚠️ 中

7.2 vs 纯 Markdown 代码块

维度自定义渲染协议Markdown 代码块
元数据表达✅ 丰富❌ 弱
自动执行能力✅ 可接回调❌ 主要展示
组件化表达✅ 直接映射⚠️ 需二次解析

7.3 vs boltArtifact

维度boltArtifact自定义渲染协议
默认目标代码生成/文件操作任意业务组件
协议标签固定(boltArtifact/boltAction)可按业务定义
可复用思路流式解析 + 事件回调完全继承并扩展

8 协议扩展与版本化

扩展步骤建议:

  1. 扩展类型定义(新增 action/component)
  2. 更新解析器分支逻辑
  3. 更新 prompt 约束与 few-shot 示例
  4. 前端补齐对应渲染组件

示例:新增 browser 动作

export type ActionType = 'file' | 'shell' | 'browser';

export interface BrowserAction {
  type: 'browser';
  url: string;
}

版本化建议:

  • 根标签带 version="v1"
  • 新版本只增字段,不改旧语义
  • 客户端遇到未知字段时忽略,不崩溃

9 最佳实践

  1. 稳定 ID:同一组件更新时复用 id,便于 diff 与状态延续。
  2. 动作有序:有依赖关系的动作必须按顺序输出。
  3. 输出完整:禁止占位符和截断。
  4. 降级优先:解析失败要可回退到文本/JSON 展示。
  5. 可观测性:记录回调事件与解析耗时,便于排障。

💡 练习题

  1. 设计题:为“股票分析助手”设计协议,支持 stockChartfinancialTableriskAlert 三类组件。

    点击查看答案

    建议统一头字段:id/type/version,并在 aiData 中存结构化 JSON。 三类组件的 schema 分别约束 symbol/rangeperiod/metricslevel/text

  2. 实现题:实现一个流式 parseAiCards(chunk, messageId),要求支持半包输入。

    点击查看答案

    关键是 messageId -> ParserState 的映射,以及未闭合标签缓存。 闭合后再触发 onDataEnd/onCardClose,避免脏数据进入渲染层。

  3. 工程题:当某个 type 没有对应 React 组件时,你的降级策略是什么?

    点击查看答案

    渲染 FallbackCard(展示原始 JSON 和 type),同时记录 warning 日志。 不应因单个未知组件导致整条消息失败。


📚 参考资源

项目与协议

本项目相关内容


✅ 总结

本章要点

  • 自定义渲染协议的本质是“让 AI 输出可被前端实时消费的结构化组件数据”。
  • 落地成功依赖三件事:协议约束、流式解析器、组件映射与降级策略。
  • 从 boltArtifact 迁移到业务协议时,优先保证稳定性和可观测性,而不是一次性追求功能完备。

下一步:你已经完成生态章节,建议回到业务章节把协议接入真实用例做一次端到端验证。

登录以继续阅读

解锁完整文档、代码示例及更多高级功能。

立即登录

On this page