首页
All Posts

前端智能体开发(二):提示词与结构化输出:让 AI 前端可控、可解析、可交互

这张图把提示词、JSON 和动态解析器画成一条可验证的界面生产线:提示词负责定义任务和约束,结构化输出负责把结果落到可处理的字段里,解析器负责在内容增量到达时持续判断状态。AI 前端要做到“可用”,关键就在这三件事上。

提示词与 JSON 结构化输出

这张图把提示词、JSON 和动态解析器画成一条可验证的界面生产线:提示词负责定义任务和约束,结构化输出负责把结果落到可处理的字段里,解析器负责在内容增量到达时持续判断状态。AI 前端要做到“可用”,关键就在这三件事上。

一、提示词不是魔法,而是接口契约

很多人第一次写提示词,会把它当作一句自然语言愿望。例如:

帮我生成一个学习计划。

模型当然能回答,但结果不稳定:有时很长,有时很短,有时按周写,有时按天写,有时夹杂解释,有时没有执行步骤。对聊天界面来说这可能还能接受;对应用来说,这种不稳定会变成解析失败、UI 展示混乱和业务逻辑不可控。

更可靠的提示词应该像接口契约,至少描述五件事:

  1. 角色:模型应该站在哪个专业视角回答。
  2. 目标:这次任务到底要产出什么。
  3. 输入:用户会给什么信息,字段含义是什么。
  4. 约束:不能做什么,长度、语气、范围、风险边界是什么。
  5. 输出:结果要用什么结构返回。

例如同样是生成学习计划,可以这样写:

你是前端学习规划助手。

目标:
根据用户的基础、时间和目标,生成 4 周学习计划。

输入字段:
- level:用户当前水平
- hoursPerWeek:每周可投入时间
- goal:学习目标

输出要求:
返回 JSON,不要输出额外解释。
字段包括 summary、weeks。
weeks 是数组,每项包含 focus、tasks、checkpoint。

这类提示词不是为了“骗模型听话”,而是让模型和程序之间形成明确协议。模型负责推理,程序负责展示、存储和继续处理。协议越清楚,前端越容易接住结果。

二、角色、示例和边界共同决定稳定性

提示词设计里最常用的三类技巧是角色设定、示例约束和边界描述。

角色设定能减少回答风格的漂移。比如“你是资深前端工程师”和“你是儿童科普助手”会产生完全不同的措辞、深度和例子。角色不要只写头衔,还要写行动方式。

你是资深前端工程师。
你会优先解释工程取舍,而不是堆概念。
你给出的代码必须能在 Vue + Vite 环境中运行。

示例约束能减少格式漂移。模型非常擅长模仿,如果你给出一组输入和输出,它更容易沿着同样结构生成结果。

输入:用户只说“地球”
输出:
{
  "questions": [
    {
      "question": "地球是如何形成的?",
      "query": ["地球形成", "太阳系早期演化"]
    }
  ]
}

边界描述能减少危险输出。比如做儿童科普助手,要明确“不要编造数据”“不确定时要求检索”“避免成人化表达”;做面试官,要明确“不要直接给答案”“一次只问一个问题”“根据候选人的回答调整难度”。

好的提示词像一份工程需求:它不追求华丽,而追求可执行。

三、为什么结构化输出是 AI 应用的分水岭

纯文本适合阅读,不适合程序处理。结构化输出适合进入产品系统。

假设你要做一个“故事生成器”,如果模型只返回一整段文字,前端只能把它放进一个 <div>。如果模型返回 JSON,前端就可以分别渲染标题、简介、章节、关键词、图片提示词和适读年龄。

{
  "title": "月亮为什么跟着我走",
  "audience": "6-8 岁儿童",
  "outline": [
    {
      "title": "第一段:孩子的疑问",
      "goal": "提出生活场景"
    },
    {
      "title": "第二段:月亮很远",
      "goal": "解释视角差异"
    }
  ],
  "image_prompt": "温暖夜晚,一个孩子抬头看月亮"
}

结构化输出带来三个好处。

第一,UI 可以分区展示。标题放顶部,章节放卡片,图片提示词送给绘图模型。

第二,后续节点可以继续处理。比如把 outline 交给多个内容节点展开,把 image_prompt 交给图像节点,把 audience 交给语音合成节点调整语气。

第三,系统可以校验。字段缺失、类型错误、数组为空,都能被程序发现并重试。

这也是 AI 应用从“聊天玩具”走向“业务系统”的关键一步。

四、流式 JSON 的矛盾:用户想快,JSON 要完整

问题来了:JSON 是封闭结构。一个对象从 { 开始,到 } 才完整。如果模型流式返回:

{"title":"月亮为什么跟

这时前端不能直接 JSON.parse,因为它不是合法 JSON。必须等完整对象返回,才能解析。这样一来,流式响应的好处又被抵消了:用户虽然网络上已经收到内容,但 UI 还是不能更新。

常见解决方案有三类。

第一,要求模型输出 NDJSON,也就是每一行都是独立 JSON。优点是容易解析,缺点是会限制模型表达,也增加提示词负担。

第二,在服务端用专门的流式 JSON 库,根据固定结构解析。优点是成熟,缺点是前端直接使用不方便,而且通常依赖预设路径。

第三,自己实现一个动态 JSON 解析器。它不等待完整 JSON,而是在字符流里识别对象、数组、字符串、数字、布尔值和路径。一旦某个字段出现增量,就立刻发出事件。

第三种方案更适合 AI 前端,因为它能把结构化输出和流式体验结合起来。

五、动态解析器的核心:状态机 + 路径

一个动态 JSON 解析器至少要维护几类状态:

  • 当前处于对象、数组、key、value、字符串还是数字。
  • 当前嵌套层级。
  • 当前字段路径。
  • 当前数组下标。
  • 当前 token 缓冲。

当输入字符不断进入时,解析器根据当前状态决定下一步。例如:

  • 读到 {:进入对象状态。
  • 读到 ":可能进入 key 或 string 状态。
  • 读到 ::key 结束,准备读 value。
  • 读到 [:进入数组状态,并记录下标。
  • 读到 ,:当前字段结束,准备下一个字段。
  • 读到 }]:当前层级结束,出栈。

这套逻辑本质上是手写状态机。它不需要等完整 JSON,只要能判断“当前 delta 属于哪个路径”,就可以把增量交给 UI。

例如模型正在输出:

{
  "story": {
    "title": "月亮为什么跟着我走",
    "paragraphs": ["晚上,小明走在路上..."]
  }
}

解析器可以不断发出类似事件:

{ uri: "story/title", delta: "月亮" }
{ uri: "story/title", delta: "为什么" }
{ uri: "story/paragraphs/0", delta: "晚上," }

前端拿到事件后,用 jsonuri 之类的工具把路径写回响应式对象:

parser.on("data", ({ uri, delta }) => {
  const current = get(result.value, uri) || "";
  set(result.value, uri, current + delta);
});

这样标题、段落和列表都能一边生成一边展示。

六、结构化流式 UI 应该怎么设计

如果只把流式结果追加到一个文本框里,用户看到的是一串逐字增长的文本。结构化流式 UI 可以更进一步,让不同字段在不同区域增长。

以“生成儿童科普文章”为例,页面可以分成四块:

  1. 标题区:一旦 title 字段出现就展示。
  2. 大纲区:outline 数组逐项生成,每项变成卡片。
  3. 正文区:每个章节内容独立流式显示。
  4. 配图区:收到图片提示词后进入图像任务状态。

用户会感觉系统在一步一步完成任务,而不是一段文字在机械增长。

实现上可以把结果状态定义成:

type StoryResult = {
  title: string;
  outline: Array<{
    section: number;
    title: string;
    overview: string;
  }>;
  sections: Record<string, string>;
  imagePrompts: string[];
};

前端渲染时,不必等所有字段齐全。哪个字段到了,就渲染哪个字段。缺字段时显示骨架屏或“正在生成”。

七、错误处理比成功路径更重要

结构化输出常见失败包括:

  • 模型输出了额外解释,导致 JSON 前后有自然语言。
  • 字段名拼错。
  • 数组格式不稳定。
  • 字符串里出现未转义换行。
  • 内容安全策略让模型中途停止。
  • 网络中断导致对象永远不闭合。

工程上可以分层处理。

第一层是提示词约束。明确“只返回 JSON,不要额外解释”。

第二层是解析器容错。对常见缺逗号、缺引号、未闭合字符串做有限修复,但不要无限猜测。

第三层是 schema 校验。用 Zod 或 JSON Schema 检查必填字段、类型和数组长度。

第四层是重试策略。如果结构不合法,可以带着错误信息重试一次,而不是让用户重新点按钮。

const schema = z.object({
  title: z.string().min(1),
  outline: z.array(z.object({
    title: z.string(),
    overview: z.string()
  })).min(1)
});

const parsed = schema.safeParse(result);
if (!parsed.success) {
  await retryWithRepairPrompt(parsed.error);
}

注意,重试也要有上限。AI 系统不能陷入无限生成。

八、一个实际场景:AI 表单生成器

假设你要做一个“自然语言生成表单”的功能。用户输入:

帮我创建一个活动报名表,需要姓名、手机号、所在城市、是否需要发票。

模型应该返回:

{
  "title": "活动报名表",
  "fields": [
    { "key": "name", "label": "姓名", "type": "text", "required": true },
    { "key": "phone", "label": "手机号", "type": "tel", "required": true },
    { "key": "city", "label": "所在城市", "type": "select", "options": ["北京", "上海", "广州", "深圳"] },
    { "key": "invoice", "label": "是否需要发票", "type": "boolean" }
  ]
}

有了结构化结果,前端可以动态渲染表单;有了流式解析,用户可以看到字段一个个出现;有了 schema 校验,系统可以拒绝未知控件类型;有了提示词约束,模型不会突然输出一段营销文案。

这个功能的质量不取决于模型会不会写表单,而取决于四层契约是否稳固:

  • 提示词契约:告诉模型怎么生成。
  • 数据契约:定义字段和类型。
  • 解析契约:流式地接住字段。
  • UI 契约:把不完整状态也优雅展示出来。

九、判断提示词是否合格的标准

一个提示词是否合格,不看它写得多长,而看它能不能稳定进入工程链路。可以用五个问题检查:

  1. 输出能不能被程序直接消费?
  2. 字段缺失时能不能发现?
  3. 用户输入很短或很脏时是否仍有边界?
  4. 流式输出时 UI 是否能提前展示?
  5. 失败后系统能不能解释并重试?

提示词越像接口,AI 应用越像产品;提示词越像愿望,AI 应用越像抽奖。

十、从“会问”到“会接”

提示词工程的重点不是把一句话雕得很漂亮,而是让模型输出能被系统接住。结构化输出让结果进入业务对象,动态解析让流式体验不牺牲结构,schema 校验让不确定输出变得可控。

好的 AI 前端设计不是喊一句“输出稳定一点”,而是明确任务、字段、类型、错误处理和 UI 状态。提示词规定目标,JSON 规定结构,解析器负责把增量内容变成可用状态。三者配合起来,AI 才能从聊天框走进真正的应用界面。