AI编辑器重构:用JSX替代JSON Schema提升UI生成质量
Don’t build your own harness: lessons from building Paywalls AI Editor
给出具体的AI产品架构迭代案例:通过引入中间层JSX格式和利用标准文件系统交互,解决Agent UI生成质量低的问题,可直接参考用于优化自有AI产品的数据流转设计。
Earlier this year, we shipped an AI editor for our remotely configurable paywalls product. Users can work with our agent to come up with a new paywall or edit an existing one.
今年早些时候,我们推出了针对远程可配置付费墙产品的 AI 编辑器。用户可以使用我们的代理来创建新的付费墙或编辑现有的付费墙。
On paper we fulfilled user requests, and adoption was growing rapidly. But there was a deeper issue that none of our eval or metrics could capture.
从表面上看,我们满足了用户的需求,采用率也在迅速增长。但存在一个更深层的问题,我们的任何评估或指标都无法捕捉到这一点。
An inflection point: “it’s not even fair”
一个转折点:“这甚至不公平”
A pattern of issues started trickling in via user reports and internal feedback. On paper, the agent would accomplish the task the user asked, like creating the paywall, adding or overhauling a section of it. But the result it produced lacked the level of taste and quality that you’d expect a frontier agent to be capable of.
一系列问题开始通过用户报告和内部反馈逐渐浮现。表面上,代理会完成用户要求的任务,例如创建付费墙、添加或彻底改造其某个部分。但它产生的结果缺乏你期望前沿代理所能达到的品味和质量水平。
We worked to address this issue in several ways:
我们通过多种方式努力解决这一问题:
- We tried different models from various providers, with higher effort and reasoning levels
- We tweaked the system prompt and skills
- We injected messages with screenshots of the paywall, pushing the agent to check its work and maintain a high bar for quality
- 我们尝试了来自不同提供商的不同模型,具有更高的工作量和推理能力
- 我们调整了系统提示和技能
- 我们注入了带有付费墙截图的消息,促使代理检查其工作并保持高质量标准
There were notable improvements, but the gap between what users wanted, and what the AI delivered, remained massive.
虽然有了显著的改进,但用户想要的和 AI 交付的之间的差距仍然巨大。
We then got some feedback from Ryan, one of our Product Managers:
随后,我们收到了一位产品经理 Ryan 的一些反馈:
(left) our old paywall editor, (right) using Fable in Claude Code(FYI: Astra is our internal term for the paywall agent)
(左) 我们旧的付费墙编辑器,(右) 在 Claude Code 中使用 Fable(注:Astra 是我们内部对付费墙代理的称呼)
This was a brutal inflection point. The feedback was originally about our agent getting the app’s name wrong. But the bigger issue was blatantly obvious.
这是一个残酷的转折点。反馈最初是关于我们的代理搞错了应用程序的名称。但更大的问题显而易见。
How does our agent not understand that the paywall it produced is atrocious? Spacing, alignment, card within a card… there were too many issues to name. How did it think this was acceptable?
我们的代理怎么会不理解它生成的付费墙如此糟糕呢?间距、对齐方式、卡片嵌套在卡片中……问题多得数不胜数。它怎么认为这是可以接受的?
And how can Claude Code just one shot something that looks great while our agent flounders? This thought haunted me. No one would use our paywall editor if you could get that much better results with a simple Claude Code prompt.
而为什么 Claude Code 只需一次就能生成看起来很棒的效果,而我们的代理却陷入困境?这个想法一直困扰着我。如果你能用一个简单的 Claude Code 提示词获得那么好的结果,没人会使用我们的付费墙编辑器。
So where did we go from here?
那么接下来我们该怎么办?
The old agent technically worked
旧的代理在技术上是可以工作的
To understand why our old agent was struggling, you first have to see how it worked.
要了解我们旧的代理为何挣扎,你首先必须了解它是如何工作的。
Beginning with the data shape. Some remote paywall products use HTML, CSS and JS to render on all platforms. This provides the most flexibility, at the cost of feeling native, appearance, and performance.
从数据形状开始。一些远程付费墙产品使用 HTML、CSS 和 JS 在所有平台上渲染。这提供了最大的灵活性,但以牺牲原生感、外观和性能为代价。
We opted for native (well, a bit of both now). Our paywalls are stored as JSON trees: each node is a component with its own attributes. When your paywall is rendered, the component tree is mapped into the native components.
我们选择了原生方案(好吧,现在两者兼有)。我们的付费墙存储为 JSON 树:每个节点都是一个具有自己属性的组件。当你的付费墙被渲染时,组件树会被映射到原生组件中。
Here’s what a purchase button component can look like in our structure:
在我们的结构中,购买按钮组件可能看起来像这样:
{
"type": "purchase_button",
"stack": {
"type": "stack",
"background": { "type": "color", "value": { "light": { "type": "hex", "value": "#ff8a00ff" } } },
"shape": { "type": "pill", "corners": null },
"padding": { "top": 12, "bottom": 12, "leading": 8, "trailing": 8 },
"components": [
{
"type": "text",
"text_lid": "PBcCWPIKbT",
"color": { "light": { "type": "hex", "value": "#ffffffff" } },
"font_weight": "semibold",
"margin": { "top": 0, "bottom": 0, "leading": 0, "trailing": 0 }
}
]
}
}
// ... one of 20+ components on this screen
// ... 此屏幕上的 20 多种组件之一
Our agent had a very custom harness (set of tools, prompts and loop around the model). It understood the shape through an annotated JSON schema, and would operate on the paywall JSON directly, manipulating it with a set of tools.
我们的代理有一个非常定制的框架(一组工具、提示词以及围绕模型的循环)。它通过带注释的 JSON 模式理解形状,并直接对支付墙 JSON 进行操作,使用一组工具对其进行操纵。
This was a reasonable design for the first iteration.
这对于第一次迭代来说是一个合理的设计。
- The targeted JSON operations prevented it from making simple formatting mistakes we’d had in earlier prototypes
- The schema showed it what was valid
- Skills gave it reusable patterns and guidance
- 目标明确的 JSON 操作防止了我们在早期原型中出现的简单格式错误。
- 模式展示了什么是有效的。
- 技能赋予了它可重用的模式和指导。
But we were asking a lot of the model: it needed to learn this new schema on the fly for representing UI, along with the new tools. It needed to translate the user request into visual intent, then translate that visual intent into a series of tool operations on the JSON. And lastly, it needed to self-evaluate using a screenshot.
但我们要求模型做很多事情:它需要即时学习这种用于表示 UI 的新模式,以及新工具。它需要将用户请求转化为视觉意图,然后将该视觉意图转化为一系列针对 JSON 的工具操作。最后,它需要使用截图进行自我评估。
It was a bespoke workflow and syntax — a far cry from the Jetpack Compose, JSX and SwiftUI use cases it had been extensively trained on.
这是一个定制的工作流和语法——与我们广泛训练的 Jetpack Compose、JSX 和 SwiftUI 用例相去甚远。
But the feature shipped.
但该功能还是上线了。
The format is cooked
格式已定型
Still, I couldn’t shake the feeling that we were not just “one more prompt” away from closing this gap. Something was fundamentally wrong.
尽管如此,我无法摆脱一种感觉,即我们距离弥合这一差距不仅仅是“再添加一个提示”那么简单。从根本上说,有些东西是错误的。
I read through Claude Code’s system prompt; there’s very little in there telling Claude how to make tasteful design, and certainly nothing specific to paywalls. Yet it achieved far better results even with models the harness wasn’t built for, like GPT.
我通读了 Claude Code 的系统提示词;里面几乎没有告诉 Claude 如何做出有品味的设计的指令,更不用说针对付费墙的具体内容了。然而,即使在使用并非为该工具链设计的模型(如 GPT)时,它依然取得了更好的结果。
Anthropic has a frontend design skill but, in my experience, the newer models still create great UI without it.
Anthropic 具备前端设计技能,但根据我的经验,新模型即使没有这项技能也能创造出出色的 UI。
This led me to suspect the taste was being lost in translation. In a minimal harness, the models could express taste into code. But not into JSON patching tool calls…
这让我怀疑品味在翻译过程中丢失了。在一个极简的工具链中,模型可以将品味转化为代码。但在 JSON 补丁工具调用中却无法做到……
What if our paywall could just be code? The model knows how to build a great UI in JSX. And JSX components are a tree very similar to ours: each node has JSON attributes and an optional set of children.
如果我们的付费墙可以直接用代码实现呢?模型知道如何用 JSX 构建优秀的 UI。而 JSX 组件树与我们的结构非常相似:每个节点都有 JSON 属性和一组可选的子节点。
So we tried it. Here’s that same purchase button from earlier, as JSX:
于是我们尝试了一下。以下是之前那个购买按钮的 JSX 形式:
<PurchaseButton>
<Stack background="#ff8a00ff" padding={{ vertical: 12, horizontal: 8 }}>
<Text text="Continue" fontWeight="semibold" color="#ffffff" />
</Stack>
</PurchaseButton>
It turned out to be fairly simple. A transpile step turns the JSON into this intermediate JSX, which the agent can edit as a file. Once it’s done editing, a strict compiler runs, flagging any issues that could prevent the paywall from displaying or functioning correctly. When there are no blocking issues, the compiler turns the JSX back into the same JSON structure our backend & SDKs expect.
事实证明这相当简单。一个转译步骤将 JSON 转换为这种中间态 JSX,代理可以将其作为文件进行编辑。编辑完成后,严格的编译器会运行,标记任何可能阻止付费墙显示或正常运行的问题。当没有阻塞性问题时,编译器会将 JSX 转换回后端和 SDKs 所期望的相同 JSON 结构。
Two days after rock bottom, the new prototype set a record on our blank paywalls eval dataset. That’s when we knew we were onto something.
触底两天后,新的原型在我们的空白付费墙评估数据集上创下了记录。那时我们就知道我们找到方向了。
Yet another coding agent
又一个编码代理
If the theory is that leaning into coding agent RL will outperform learning a new harness on the fly, why not lean in fully. Let’s give the agent a full workspace, simple mechanisms to read, and additional context as needed.
如果理论依据是深入编码代理的强化学习(RL)将优于即时学习新的工具链,那为何不全力以赴。让我们给代理一个完整的工作空间,提供简单的读取机制,并在需要时提供额外的上下文。
So we did just that. We gave it a folder of snippets to see examples of how to do certain component structures, which it can copy-paste as needed. It doesn’t need the full documentation for every component on every request, so I split it into multiple d.ts files that can be lazily loaded, instead of cramming 175KB of JSON schema at the start of every session. This meant paywall translations just became entries in a strings/ folder. The agent can keep context about the app, like what the app does and who it is for, in a context.md.
所以我们就这么做了。我们给它提供了一个片段文件夹,以便查看某些组件结构的示例,它可以在需要时复制粘贴。它不需要在每个请求中都获取所有组件的完整文档,因此我将它们拆分为多个 .d.ts 文件以实现懒加载,而不是在每个会话开始时塞入 175KB 的 JSON 模式。这意味着付费墙的翻译变成了 strings/ 文件夹中的条目。代理可以在 context.md 中保持对应用程序上下文的了解,比如应用程序的功能和目标用户。
And most importantly, this gave us room to expand. New capabilities can be represented with files/folders, and since things are lazily loaded in, there is minimal additional cost/overhead to do so.
最重要的是,这为我们留下了扩展的空间。新功能可以通过文件/文件夹来表示,由于内容是懒加载的,因此这样做带来的额外成本/开销极小。
/workspace
├── context.md
├── paywall.tsx
├── snippets/ (read-only)
├── snippets/ (只读)
│ ├── ctas/purchase_button.tsx
│ ├── feature_lists/checklist.tsx
│ └── ... (28 snippets)
│ └── ... (28 个片段)
├── strings/
│ ├── es_ES.json (14 strings)
│ ├── es_ES.json (14 个字符串)
│ └── fr_FR.json (14 strings, 2 stale)
│ └── fr_FR.json (14 个字符串,其中 2 个已过期)
└── types/ (read-only)
└── types/ (只读)
├── _readme.d.ts
├── base.d.ts
├── components/layout.d.ts
├── components/...d.ts
└── overrides.d.ts
We didn’t need to reinvent our own tools to work with this format. We used what the model already knows: a free loop with Read, Write, Edit and a compiler. The compiler even feels like tsc: giving line numbers and diagnostics with guidance when there are issues in the paywall code, and handing back a screenshot of the paywall when it succeeds.
我们无需重新发明自己的工具来处理这种格式。我们利用了模型已经知道的内容:一个带有读取、写入、编辑功能的自由循环以及一个编译器。该编译器的体验甚至类似于 tsc:当 paywall 代码出现问题时,它会提供行号和诊断信息并给出指导;当成功时,它会返回 paywall 的截图。
With a familiar format and harness, We were also able to remove over half of our system prompt and almost all of our skills. Most of those skills and prompts were compensating for a subpar format. As we continue to iterate, we focus on improving format first, then only prompt our way out if we must.
凭借熟悉的格式和框架,我们还移除了超过一半的系统提示词以及几乎所有的技能(skills)。大多数这些技能和提示词原本是为了弥补次优格式而存在的。随着我们持续迭代,我们首先专注于改进格式,只有在必要时才通过优化提示词来解决问题。
Works on my machine
在我的机器上能运行
The prototype broke eval records in two days. Getting that to production was a full team effort that took several weeks.
这个原型在两天内就破坏了评估记录。将其部署到生产环境是一个需要整个团队共同努力的过程,历时数周。
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力