// docs / normalize
Blueprint 容错解析
LLM 输出 JSON 时经常出现字段缺失、类型错误、结构不一致。asProjectBlueprint() 函数支持三种输出格式, 并对每个字段做防御性 normalize — 比在 prompt 里反复强调"必须输出完整 JSON"更可靠。
问题背景
analyze_project_requirement 步骤要求 LLM 输出一个嵌套很深的 JSON 结构。 实际观察到的失败模式:
字段缺失roles 数组为空,或 productScope 整个缺失类型错误goals 应为 string[] 但输出了单个 string结构变体把所有字段平铺在顶层,而非嵌套在 brief/experience/site 下单页简化只输出 title + sections,没有多页结构JSON 截断上下文窗口不足时输出不完整的 JSON在 prompt 中反复强调"必须输出完整 JSON"会增加 token 消耗,但并不能完全消除这些问题。 防御性解析比 prompt 工程更可靠。
与结构化 Blueprint 并行执行的
infer_design_intent 产出自由文本设计意图; 流水线在规划完成后会把其中的 technicalKeywords 并入 experience.designIntent.keywords,以便下游 skill 路由获取 analyze JSON 未覆盖的信号。三种输出格式
asProjectBlueprint() 按优先级依次尝试三种解析路径:
function asProjectBlueprint(value: unknown): ProjectBlueprint {
// 路径 1:标准嵌套结构(最常见)
if (candidate.brief && candidate.experience && candidate.site) {
return {
brief: normalizeBrief(candidate.brief),
experience: normalizeExperience(candidate.experience),
site: normalizeSite(candidate.site),
};
}
// 路径 2:扁平结构(LLM 有时会平铺所有字段)
if (
typeof flatCandidate.projectTitle === "string" &&
flatCandidate.designIntent &&
Array.isArray(flatCandidate.pages)
) {
return { brief: ..., experience: ..., site: ... }; // 重新组装
}
// 路径 3:单页简化结构(用户只描述了一个页面)
if (
typeof singlePage.title === "string" &&
singlePage.designIntent &&
isSectionSpecArray(singlePage.sections)
) {
return { brief: ..., experience: ..., site: ... };
}
throw new Error("output does not match ProjectBlueprint");
}字段级容错
每个字段都有独立的 normalize 函数,处理类型错误和缺失值:
roles 容错
function normalizeRoles(value: unknown): UserRole[] {
// 空数组或非数组 → 自动创建默认 Visitor 角色
if (!Array.isArray(value) || value.length === 0) {
return [{
roleId: "visitor",
roleName: "Visitor",
goals: ["Understand the offer", "Complete the primary conversion path"],
coreActions: ["Scan the main message", "Take the primary CTA"],
priority: "primary",
}];
}
// 每个角色字段单独 normalize,缺失字段用合理默认值填充
return value.map((item, index) => ({
roleId: typeof item.roleId === "string" ? item.roleId
: item.roleName?.toLowerCase().replace(/[^a-z0-9]+/g, "-")
?? `role-${index + 1}`,
roleName: typeof item.roleName === "string" ? item.roleName : `Role ${index + 1}`,
goals: isStringArray(item.goals) ? item.goals : ["Support the role's core goal"],
// ...
}));
}fileName 的 PascalCase 转换
Section 的 fileName 字段会被转为 PascalCase, 确保生成的 TSX 文件名符合 React 组件命名规范:
function toPascalCase(value: string): string {
return value
.split(/[^a-zA-Z0-9]+/) // 按非字母数字分割
.filter(Boolean)
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join("");
}
// 示例
toPascalCase("hero-section") → "HeroSection"
toPascalCase("pricing_table") → "PricingTable"
toPascalCase("FAQ section") → "FAQSection"language 字段
language 字段决定所有生成内容的语言(中文/英文等)。 如果 LLM 没有输出或输出空字符串,默认为 "en":
language: typeof candidate.language === "string" && candidate.language.trim() ? candidate.language.trim() : "en",
Web Search 工具
analyze_project_requirement 步骤配备了 web_search 工具。 当用户 prompt 包含 LLM 不熟悉的品牌名、专业术语或新产品时, LLM 会先搜索再分析:
// system prompt 中的指令
"Before analyzing the user's request, check if it contains any proper nouns,
brand names, people, products, or domain-specific terms you are unfamiliar with.
If so, use the web_search tool to look them up first."
// 工具调用示例
用户输入: "帮我做一个 Cursor 风格的 AI 代码编辑器官网"
→ LLM 调用 web_search("Cursor AI code editor")
→ 获取 Cursor 的产品定位、功能特点、视觉风格
→ 基于搜索结果生成更准确的 Blueprint最多允许 4 次工具调用迭代(maxIterations: 4)。 搜索结果通过 SSE 实时推送到前端,用户可以看到 Agent 在搜索什么。
Web Search 工具的调用结果会通过
onToolCall 回调传递给调用方, 在 Build Studio 的进度面板中显示为 tool_call 事件。Fallback 策略
各层级的 fallback 设计原则:宁可生成一个合理的默认值,也不要抛出错误。
| 字段 | 缺失时的 Fallback |
|---|---|
| roles | 自动创建 Visitor 角色(primary 优先级) |
| taskLoops | 基于第一个 role 创建默认核心旅程 |
| capabilities | 空数组(不强制要求) |
| productScope | 从 projectDescription 推导 |
| informationArchitecture | 从 pages 数组自动构建 |
| language | "en" |
| fileName | 从 type 字段 PascalCase 转换 |
| priority (role) | "primary" |
| priority (capability) | "must-have" |
只有两个字段是硬性必须的:
brief.projectTitle 和brief.projectDescription。缺失这两个字段会抛出错误, 因为没有合理的 fallback 可以替代用户的核心意图。