生态与协议
自定义渲染协议
从 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 字母 | ai、biz |
| 组件标签 | PascalCase 或固定前缀 | aiCard、bizChart |
| 属性名 | camelCase | dataType、chartId |
| ID 值 | kebab-case | product-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 流式解析器实现
下面是协议解析器的关键状态:
insideCardinsideDatacurrentCardcurrentData
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;
}
}工程关键点:
- 支持“标签未闭合”的半包输入。
- 按
messageId维护独立状态,避免多消息串流污染。 - 提供
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 协议扩展与版本化
扩展步骤建议:
- 扩展类型定义(新增 action/component)
- 更新解析器分支逻辑
- 更新 prompt 约束与 few-shot 示例
- 前端补齐对应渲染组件
示例:新增 browser 动作
export type ActionType = 'file' | 'shell' | 'browser';
export interface BrowserAction {
type: 'browser';
url: string;
}版本化建议:
- 根标签带
version="v1" - 新版本只增字段,不改旧语义
- 客户端遇到未知字段时忽略,不崩溃
9 最佳实践
- 稳定 ID:同一组件更新时复用 id,便于 diff 与状态延续。
- 动作有序:有依赖关系的动作必须按顺序输出。
- 输出完整:禁止占位符和截断。
- 降级优先:解析失败要可回退到文本/JSON 展示。
- 可观测性:记录回调事件与解析耗时,便于排障。
💡 练习题
-
设计题:为“股票分析助手”设计协议,支持
stockChart、financialTable、riskAlert三类组件。点击查看答案
建议统一头字段:
id/type/version,并在aiData中存结构化 JSON。 三类组件的 schema 分别约束symbol/range、period/metrics、level/text。 -
实现题:实现一个流式
parseAiCards(chunk, messageId),要求支持半包输入。点击查看答案
关键是
messageId -> ParserState的映射,以及未闭合标签缓存。 闭合后再触发onDataEnd/onCardClose,避免脏数据进入渲染层。 -
工程题:当某个
type没有对应 React 组件时,你的降级策略是什么?点击查看答案
渲染
FallbackCard(展示原始 JSON 和 type),同时记录 warning 日志。 不应因单个未知组件导致整条消息失败。
📚 参考资源
项目与协议
本项目相关内容
✅ 总结
本章要点:
- 自定义渲染协议的本质是“让 AI 输出可被前端实时消费的结构化组件数据”。
- 落地成功依赖三件事:协议约束、流式解析器、组件映射与降级策略。
- 从 boltArtifact 迁移到业务协议时,优先保证稳定性和可观测性,而不是一次性追求功能完备。
下一步:你已经完成生态章节,建议回到业务章节把协议接入真实用例做一次端到端验证。
登录以继续阅读
解锁完整文档、代码示例及更多高级功能。