深入解析 boltArtifact 协议:AI 代码生成的流式交互方案
本文深入分析 bolt.new 项目中的 boltArtifact 协议设计,探讨其技术选型、实现原理以及与其他方案的对比。
一、什么是 boltArtifact 协议?
boltArtifact 是 bolt.new 项目中设计的一套 XML 风格的标签协议,用于在 AI 生成的文本流中嵌入结构化的代码操作指令。它允许 AI 模型以声明式的方式描述文件创建、代码编写和 Shell 命令执行等操作。
<boltArtifact id="my-project" title="My React App">
<boltAction type="file" filePath="package.json">
{ "name": "my-app" }
</boltAction>
<boltAction type="shell">
npm install
</boltAction>
</boltArtifact>
二、为什么选择 XML 风格的协议?
2.1 设计考量
| 考量因素 | XML 标签方案 | JSON 方案 | Markdown 代码块 |
|---|---|---|---|
| 流式解析 | ✅ 优秀 | ❌ 困难 | ⚠️ 一般 |
| 嵌套结构 | ✅ 天然支持 | ✅ 支持 | ❌ 不支持 |
| 与文本混排 | ✅ 无缝 | ❌ 需要分隔 | ⚠️ 有限 |
| 属性传递 | ✅ 原生支持 | ✅ 支持 | ❌ 需要约定 |
| LLM 友好度 | ✅ 高 | ⚠️ 易出错 | ✅ 高 |
2.2 核心优势
1. 流式解析友好
XML 标签具有明确的开始和结束标记,可以在流式传输中实时检测:
const ARTIFACT_TAG_OPEN = '<boltArtifact';
const ARTIFACT_TAG_CLOSE = '</boltArtifact>';
这使得前端可以在 AI 输出过程中实时解析和渲染,无需等待完整响应。
2. 与 Markdown 自然共存
AI 的回复可以混合使用 Markdown 和 boltArtifact 标签:
我来帮你创建一个 React 应用。
<boltArtifact id="react-app" title="React Application">
<boltAction type="file" filePath="App.jsx">
// 代码内容
</boltAction>
</boltArtifact>
上面的代码实现了基本功能...
3. LLM 输出稳定性
相比 JSON,XML 标签对 LLM 更友好:
- 不需要严格的引号和逗号
- 标签名具有语义,便于模型理解
- 即使部分格式错误,也能容错解析
三、协议规范详解
3.1 boltArtifact 标签
<boltArtifact id="unique-id" title="Artifact Title">
<!-- boltAction 标签 -->
</boltArtifact>
| 属性 | 类型 | 必需 | 说明 |
|---|---|---|---|
id | string | ✅ | 唯一标识符,使用 kebab-case,更新时复用 |
title | string | ✅ | 人类可读的标题描述 |
3.2 boltAction 标签
文件操作 (type="file")
<boltAction type="file" filePath="src/components/App.tsx">
export default function App() {
return <div>Hello World</div>;
}
</boltAction>
| 属性 | 类型 | 必需 | 说明 |
|---|---|---|---|
type | "file" | ✅ | 操作类型 |
filePath | string | ✅ | 相对于工作目录的文件路径 |
Shell 命令 (type="shell")
<boltAction type="shell">
npm install && npm run dev
</boltAction>
| 属性 | 类型 | 必需 | 说明 |
|---|---|---|---|
type | "shell" | ✅ | 操作类型 |
3.3 TypeScript 类型定义
// types/artifact.ts
export interface BoltArtifactData {
id: string;
title: string;
}
// types/actions.ts
export type ActionType = 'file' | 'shell';
export interface FileAction {
type: 'file';
filePath: string;
content: string;
}
export interface ShellAction {
type: 'shell';
content: string;
}
export type BoltAction = FileAction | ShellAction;
四、Prompt 工程:如何让 LLM 正确输出
4.1 系统提示词结构
export const getSystemPrompt = (cwd: string) => `
你是 Bolt,一位专业的 AI 助手和资深软件开发工程师,精通多种编程语言、框架和最佳实践。
<system_constraints>
你运行在 WebContainer 环境中,这是一个基于浏览器的 Node.js 运行时。
它在浏览器中模拟 Linux 系统,但无法运行原生二进制文件。
关键限制:
- Shell 模拟 zsh,但功能有限
- Python 仅支持标准库,没有 pip
- 没有 C/C++ 编译器
- Git 不可用
- 优先使用 Node.js 脚本而非 shell 脚本
</system_constraints>
<artifact_info>
Bolt 为每个项目创建一个完整的 artifact,包含所有必要的步骤和组件:
- 需要运行的 Shell 命令(包括依赖安装)
- 需要创建的文件及其内容
- 必要时创建的文件夹
<artifact_instructions>
1. 【重要】在创建 artifact 前,全面思考项目:
- 考虑所有相关文件
- 分析整个项目上下文和依赖关系
- 预判对系统其他部分的影响
2. 当前工作目录是 \`${cwd}\`
3. 使用 \`<boltArtifact>\` 标签包裹内容,内部包含 \`<boltAction>\` 元素
4. 为 \`<boltArtifact>\` 添加 title 属性(标题)和 id 属性(唯一标识符)
- id 使用 kebab-case 格式(如 "snake-game")
- 更新时复用之前的 id
5. \`<boltAction>\` 的 type 属性支持以下值:
- shell: 运行 Shell 命令
* 使用 npx 时始终加 --yes 标志
* 多个命令用 && 连接
- file: 创建或更新文件
* 必须添加 filePath 属性指定文件路径
* 所有路径必须是相对于工作目录的相对路径
6. 【重要】action 的顺序至关重要!
- 先创建文件,再运行依赖该文件的命令
- 先安装依赖,再生成其他文件
7. 【重要】始终提供完整的文件内容:
- 包含所有代码,即使部分未修改
- 禁止使用占位符如 "// 其余代码保持不变..."
- 禁止任何形式的截断或省略
</artifact_instructions>
</artifact_info>
【重要】禁止使用 "artifact" 这个词。例如:
- 错误:「这个 artifact 创建了一个贪吃蛇游戏」
- 正确:「我们创建了一个贪吃蛇游戏」
【重要】保持简洁,除非用户要求,否则不要解释代码。
<examples>
<example>
<user_query>帮我创建一个计算阶乘的 JavaScript 函数</user_query>
<assistant_response>
好的,我来创建一个计算阶乘的函数。
<boltArtifact id="factorial-function" title="JavaScript 阶乘函数">
<boltAction type="file" filePath="index.js">
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
</boltAction>
<boltAction type="shell">
node index.js
</boltAction>
</boltArtifact>
</assistant_response>
</example>
<example>
<user_query>做一个贪吃蛇游戏</user_query>
<assistant_response>
我来用 HTML5 Canvas 和 JavaScript 创建一个贪吃蛇游戏。
<boltArtifact id="snake-game" title="贪吃蛇游戏">
<boltAction type="file" filePath="package.json">
{
"name": "snake-game",
"scripts": { "dev": "vite" },
"devDependencies": { "vite": "^5.0.0" }
}
</boltAction>
<boltAction type="shell">
npm install
</boltAction>
<boltAction type="file" filePath="index.html">
<!-- HTML 内容 -->
</boltAction>
<boltAction type="file" filePath="main.js">
// 游戏逻辑
</boltAction>
<boltAction type="shell">
npm run dev
</boltAction>
</boltArtifact>
</assistant_response>
</example>
</examples>
`;
4.2 关键 Prompt 设计原则
1. 明确的格式规范
使用 <boltArtifact> 标签包裹内容。
内部包含具体的 <boltAction> 元素。
2. 属性约束说明
为 id 属性添加唯一标识符,使用 kebab-case 格式。
更新时复用之前的标识符。
3. 操作顺序强调
action 的顺序至关重要!
先创建文件,再运行依赖该文件的命令。
4. Few-shot 示例
提供多个完整示例,涵盖不同场景(纯 JS、React、Node.js 等)。
5. 负面约束
禁止使用 "artifact" 这个词。
始终提供完整的文件内容,禁止使用占位符。
五、前端处理流程
5.1 整体架构
LLM Stream Response
↓
┌──────────────────────┐
│ StreamingMessageParser│ ← 流式解析 XML 标签
└──────────────────────┘
↓
┌──────────────────────┐
│ Callbacks 触发 │ ← onArtifactOpen/Close, onActionOpen/Close
└──────────────────────┘
↓
┌──────────────────────┐
│ Markdown 渲染 │ ← 将标签转换为 React 组件
└──────────────────────┘
↓
┌──────────────────────┐
│ Artifact 组件 │ ← 展示文件树、代码预览、执行状态
└──────────────────────┘
5.2 流式解析器实现
// message-parser.ts
export class StreamingMessageParser {
#messages = new Map<string, MessageState>();
parse(messageId: string, input: string) {
let state = this.#messages.get(messageId);
// 状态机解析
while (i < input.length) {
if (state.insideArtifact) {
if (state.insideAction) {
// 解析 action 内容
const closeIndex = input.indexOf('</boltAction>', i);
if (closeIndex !== -1) {
this._options.callbacks?.onActionClose?.(data);
state.insideAction = false;
}
} else {
// 寻找下一个 action 或 artifact 结束
const actionOpenIndex = input.indexOf('<boltAction', i);
const artifactCloseIndex = input.indexOf('</boltArtifact>', i);
// ...
}
} else {
// 寻找 artifact 开始标签
if (input.startsWith('<boltArtifact', i)) {
this._options.callbacks?.onArtifactOpen?.(data);
state.insideArtifact = true;
}
}
}
return output; // 返回去除标签后的文本
}
}
5.3 标签转换为 React 组件
解析器将 <boltArtifact> 转换为带有特殊 class 的 div:
const createArtifactElement = (props) => {
return `<div class="__boltArtifact__" data-message-id="${props.messageId}"></div>`;
};
Markdown 组件检测并渲染:
// Markdown.tsx
const components = {
div: ({ className, node }) => {
if (className?.includes('__boltArtifact__')) {
const messageId = node?.properties.dataMessageId;
return <Artifact messageId={messageId} />;
}
return <div>{children}</div>;
},
};
5.4 回调事件系统
interface ParserCallbacks {
onArtifactOpen?: (data: ArtifactCallbackData) => void;
onArtifactClose?: (data: ArtifactCallbackData) => void;
onActionOpen?: (data: ActionCallbackData) => void;
onActionClose?: (data: ActionCallbackData) => void;
}
这些回调用于:
- 更新文件系统(虚拟或实际)
- 执行 Shell 命令
- 更新 UI 状态
六、与其他方案的对比
6.1 vs Anthropic Artifacts
| 特性 | boltArtifact | Anthropic Artifacts |
|---|---|---|
| 多操作支持 | ✅ 支持多个 action | ❌ 单一 artifact |
| Shell 执行 | ✅ 原生支持 | ❌ 不支持 |
| 文件系统 | ✅ 多文件操作 | ❌ 单文件 |
| 实时预览 | ✅ WebContainer | ✅ iframe 沙箱 |
6.2 vs Function Calling
| 特性 | boltArtifact | Function Calling |
|---|---|---|
| 流式输出 | ✅ 实时渲染 | ❌ 需等待完成 |
| 文本混排 | ✅ 自然嵌入 | ❌ 结构分离 |
| 用户可见性 | ✅ 透明可见 | ⚠️ 需要额外处理 |
| 复杂度 | 低 | 中等 |
6.3 vs 纯 Markdown 代码块
| 特性 | boltArtifact | Markdown 代码块 |
|---|---|---|
| 元数据 | ✅ 属性支持 | ❌ 仅语言标识 |
| 操作类型 | ✅ 明确区分 | ❌ 需要约定 |
| 自动执行 | ✅ 可触发回调 | ❌ 纯展示 |
七、扩展协议的思路
如需扩展更多 action 类型:
// 1. 扩展类型定义
export type ActionType = 'file' | 'shell' | 'terminal' | 'browser';
export interface BrowserAction extends BaseAction {
type: 'browser';
url: string;
}
// 2. 更新解析器
if (actionType === 'browser') {
const url = this.#extractAttribute(actionTag, 'url');
(actionAttributes as BrowserAction).url = url;
}
// 3. 更新 Prompt
`- browser: For opening URLs in the preview browser.
Add a url attribute to specify the target URL.`;
八、最佳实践
- 保持 artifact id 稳定:更新时复用 id,便于追踪变更
- action 顺序正确:先创建文件,再执行命令
- 提供完整内容:避免使用占位符或省略
- 合理使用 shell:用
&&连接相关命令 - 错误处理:解析器应容错,允许部分格式问题
九、总结
boltArtifact 协议通过 XML 风格的标签设计,实现了:
- 流式友好:支持实时解析和渲染
- 语义清晰:标签名和属性自解释
- 扩展灵活:易于添加新的操作类型
- LLM 稳定:相比 JSON 更少格式错误
这种设计在 AI 代码生成场景中表现出色,值得在类似项目中借鉴。