本文收录专栏「一个人用AI做工具矩阵」,关于智枢矩阵表单平台的技术深潜系列。

智枢矩阵最核心的功能是什么?不是拖拽排序,不是条件逻辑,而是——你跟AI说一句"帮我做个员工入职登记表",它就真给你生成出来

这个功能我之前写过一篇概览(见《AI生成表单:从自然语言到JSON Schema,我是怎么做到的》),被AI Agent技术社区收录了。但那篇偏思路,没展开代码。今天这篇是全链路拆解:从对话架构到JSON解析兜底到文件上传,一个人是怎么把这条链路串通的。

一、全链路总览

用户在对话框输入需求,最终拿到一个可编辑、可发布的表单,中间经历了这些:
 

用户输入需求(自然语言/文件上传)
  ↓
Step 1: 拼装System Prompt + 对话历史
  ↓
Step 2: 调用火山方舟doubao-seed模型
  ↓
Step 3: AI返回JSON Schema(理想情况)
  ↓
Step 4: 三层JSON解析兜底(现实情况)
  ↓
Step 5: 前端根据Schema渲染表单预览
  ↓
Step 6: 用户补充/修改 → 回到Step 1(多轮迭代)
  ↓
Step 7: 用户确认 → 保存表单

看起来7步,真正难的是Step 3-4——AI的输出永远不如你预期的那样干净

二、对话架构:sessionStore + 多轮上下文

多轮对话的关键是维护上下文。用户说"加一个手机号字段",AI得知道前面已经生成了哪些字段,才能正确追加。

2.1 前端sessionStore

// stores/session.ts
export const useSessionStore = defineStore('session', () => {
  const messages = ref<ChatMessage[]>([])    // 对话历史
  const currentForm = ref<FormSchema | null>(null)  // 当前生成的表单

  // 发送消息
  async function sendMessage(content: string, files?: File[]) {
    // 添加用户消息到历史
    messages.push({ role: 'user', content })

    // 调用后端chat接口,传完整对话历史
    const response = await api.post('/form/chat', {
      messages: messages.value,
      files: files?.map(f => f.name)  // 文件名传给后端
    })

    // 解析AI返回
    const parsed = parseAIResponse(response.data)
    if (parsed.schema) {
      currentForm.value = mergeSchema(currentForm.value, parsed.schema)
    }

    // 添加AI回复到历史
    messages.push({ role: 'assistant', content: response.data.reply })
  }

  // 重置对话
  function resetSession() {
    messages.value = []
    currentForm.value = null
  }

  return { messages, currentForm, sendMessage, resetSession }
})

关键设计:每次请求把完整对话历史传给后端,后端不维护session状态。这样简单可靠,不需要Redis存session,重启也不丢上下文。

缺点是token消耗随对话轮次增长,但智枢矩阵的典型对话3-5轮就结束了,token压力可控。

2.2 后端路由

// routes/form.js
router.post('/chat', auth, async (req, res) => {
  const { messages, files } = req.body
  const userId = req.user.id

  try {
    // 处理文件上传(如果有)
    let fileContent = ''
    if (files && files.length > 0) {
      fileContent = await parseUploadedFiles(files, userId)
    }

    // 拼装完整消息
    const fullMessages = buildMessages(messages, fileContent)

    // 调用AI
    const aiResponse = await callAI(fullMessages)

    // 解析返回
    const result = parseResponse(aiResponse)

    res.json({
      reply: result.reply,
      schema: result.schema,
      title: result.title
    })
  } catch (error) {
    console.error('[Chat] Error:', error.message)
    res.status(500).json({ error: '对话生成失败,请重试' })
  }
})

三、System Prompt设计:让AI输出JSON Schema

AI模型不是人,你跟它说"帮我做个表单",它可能给你写一段散文。必须通过System Prompt精确约束输出格式。

3.1 核心Prompt

// services/ai.js
function buildSystemPrompt() {
  return `你是智枢矩阵的表单生成助手。用户会描述他们需要的表单,你需要生成对应的JSON Schema。

## 输出格式要求

你必须严格按照以下JSON格式输出,不要输出任何其他内容:

{
  "title": "表单标题",
  "fields": [
    {
      "label": "字段标签",
      "type": "字段类型",
      "placeholder": "输入提示",
      "required": true/false,
      "options": ["选项1", "选项2"],  // 仅选择类字段
      "group": "分组名称"  // 可选,用于字段分组
    }
  ]
}

## 支持的字段类型

基础类型:text, textarea, number, email, phone, date, time, select, radio, checkbox
高级类型:file, image, url, rating, slider, cascade, section, description, calculation, location

## 规则

1. 每个字段必须有label和type
2. select/radio/checkbox必须有options
3. 合理设置required(核心字段必填,辅助字段选填)
4. 根据表单场景添加placeholder
5. 相关字段用group分组
6. 不要输出JSON以外的任何文字说明

## 示例

用户:"帮我做一个员工入职登记表"

{
  "title": "员工入职登记表",
  "fields": [
    {"label": "姓名", "type": "text", "placeholder": "请输入真实姓名", "required": true, "group": "基本信息"},
    {"label": "手机号", "type": "phone", "placeholder": "请输入手机号", "required": true, "group": "基本信息"},
    {"label": "入职部门", "type": "select", "required": true, "options": ["技术部", "产品部", "市场部", "人事部"], "group": "入职信息"},
    {"label": "期望薪资", "type": "number", "placeholder": "请输入月薪期望(元)", "required": false, "group": "入职信息"}
  ]
}`
}

这个prompt我迭代了三个版本:

V1:只说了"输出JSON",AI经常在JSON前后加解释文字("这是你的表单:"、"希望对你有帮助")

V2:加了"不要输出JSON以外的任何内容",好了一点,但偶尔还是会带尾巴

V3:加了完整示例,AI按示例格式输出的概率大幅提升

教训:给AI看一个example,比写十条规则都管用。

3.2 多轮对话的Prompt策略

第一轮对话让AI生成完整表单,后续轮次让它在现有基础上修改:

function buildMessages(chatHistory: ChatMessage[], fileContent?: string) {
  const systemPrompt = buildSystemPrompt()
  const messages = [{ role: 'system', content: systemPrompt }]

  // 加入对话历史
  for (const msg of chatHistory) {
    messages.push({
      role: msg.role,
      content: msg.content
    })
  }

  // 如果有文件内容,附加到最后一条用户消息
  if (fileContent) {
    const lastUserMsg = messages.findLastIndex(m => m.role === 'user')
    if (lastUserMsg >= 0) {
      messages[lastUserMsg].content += `\n\n[上传文件解析内容]\n${fileContent}`
    }
  }

  // 如果已有表单Schema,告诉AI当前状态
  const currentSchema = sessionStore.currentForm
  if (currentSchema && chatHistory.length > 0) {
    messages.push({
      role: 'system',
      content: `当前表单Schema如下,请在它的基础上修改:\n${JSON.stringify(currentSchema, null, 2)}`
    })
  }

  return messages
}

多轮的关键:把当前Schema塞进system消息,AI就知道"改"而不是"重做"。没有这个,用户说"加个字段",AI可能重新生成一个完全不同的表单。

四、JSON解析兜底:三层防线

这是整篇文章最值钱的部分。

AI模型号称输出JSON,实际上你会遇到:

• 输出完整JSON,前面带"这是您的表单:\n"

• 输出完整JSON,后面带"希望这个表单对您有帮助!"

• 输出不完整JSON,因为max_tokens截断了

• 输出不完整JSON,因为AI自己中途"觉得够了"

• 输出Markdown代码块包裹的JSON(json ...

• 输出合法JSON,但结构不对(嵌套层级错了、字段名拼错了)

如果只做JSON.parse(),线上至少30%的请求会报错。

4.1 第一层:清理+直接解析

function parseResponse(aiContent: string): ParseResult {
  // 第一步:清理Markdown代码块
  let cleanContent = aiContent.trim()

  // 去掉 ```json ... ``` 包裹
  const codeBlockMatch = cleanContent.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/)
  if (codeBlockMatch) {
    cleanContent = codeBlockMatch[1].trim()
  }

  // 去掉前后的非JSON文字
  const jsonStart = cleanContent.indexOf('{')
  const jsonEnd = cleanContent.lastIndexOf('}')
  if (jsonStart >= 0 && jsonEnd > jsonStart) {
    cleanContent = cleanContent.substring(jsonStart, jsonEnd + 1)
  }

  // 尝试直接解析
  try {
    const parsed = JSON.parse(cleanContent)
    if (isValidSchema(parsed)) {
      return {
        success: true,
        schema: normalizeSchema(parsed),
        title: parsed.title || '未命名表单',
        reply: ''
      }
    }
  } catch (e) {
    console.warn('[Parse] 第一层解析失败,尝试第二层')
  }

  // 进入第二层...
}

这一层能解决60%的情况——大部分时候AI输出的是"差不多"的JSON,只要把前后垃圾文字去掉就能解析。

4.2 第二层:正则提取关键字段

// json
{
  "title": "活动报名表",
  "fields": [
    {"label": "姓名", "type": "text", "required": true},
    {"label": "手机", "type": "phone", "required": true},
    {"label": "参加人数", "type": "number", "placeholder": "请输入参加人数",

最后少了闭合括号。JSON.parse() 必定失败,但人一眼就能看出前两个字段是完整的。

// 第二层:提取title和完整字段对象
function extractPartialSchema(content: string): ParseResult | null {
  // 提取title
  const titleMatch = content.match(/"title"\s*:\s*"([^"]+)"/)
  const title = titleMatch ? titleMatch[1] : '未命名表单'

  // 提取完整的字段对象(每个 {...} 作为一个字段)
  const fieldPattern = /\{\s*"label"\s*:\s*"([^"]+)"\s*,\s*"type"\s*:\s*"([^"]+)"[^}]*\}/g
  const fields: FieldSchema[] = []
  let match

  while ((match = fieldPattern.exec(content)) !== null) {
    const fieldStr = match[0]
    try {
      // 尝试解析单个字段对象
      const field = JSON.parse(fieldStr)
      if (field.label && field.type) {
        fields.push(normalizeField(field))
      }
    } catch (e) {
      // 单个字段也不完整,跳过
      continue
    }
  }

  if (fields.length > 0) {
    return {
      success: true,
      schema: { title, fields },
      title,
      reply: `已提取到 ${fields.length} 个字段,部分内容可能不完整,你可以让我补充。`
    }
  }

  return null
}

这个正则提取的逻辑是:只收完整的字段对象,不完整的直接丢弃。宁可少一个字段,也不能让整个解析崩掉。

4.3 第三层:放弃解析,原样返回

// 第三层:AI输出完全不可解析,当普通对话返回
function fallbackToChat(content: string): ParseResult {
  return {
    success: false,
    schema: null,
    title: '',
    reply: content  // 原样返回给用户看
  }
}

这种情况下AI可能是在回答用户的问题而不是生成表单,比如用户问"这个表单应该有哪些字段",AI可能先给建议再生成。直接把内容当对话返回,下一轮用户说"好的,按这个生成"就能触发生成。

4.4 三层兜底的完整流程

function parseResponse(aiContent: string): ParseResult {
  let cleanContent = preprocessContent(aiContent)

  // 第一层:清理后直接解析
  try {
    const parsed = JSON.parse(cleanContent)
    if (isValidSchema(parsed)) {
      return { success: true, schema: normalizeSchema(parsed), title: parsed.title, reply: '' }
    }
  } catch (e) {
    // 继续第二层
  }

  // 第二层:正则提取部分字段
  const partial = extractPartialSchema(aiContent)
  if (partial && partial.schema.fields.length > 0) {
    return partial
  }

  // 第三层:放弃解析
  return fallbackToChat(aiContent)
}

线上跑了一个月的数据:第一层成功约65%,第二层成功约25%,第三层兜底约10%。也就是说,没有第二层的话,四分之一的请求会白跑

五、文件上传解析:PDF/Word/Excel

智枢矩阵支持上传文件作为表单需求的参考。比如上传一份PDF格式的纸质表格,AI解析后生成对应的在线表单。

5.1 文件解析服务

// services/fileParser.js
export async function parseUploadedFiles(filenames: string[], userId: number) {
  const results: string[] = []

  for (const filename of filenames) {
    const ext = path.extname(filename).toLowerCase()
    const filePath = path.join(uploadDir, userId.toString(), filename)

    try {
      switch (ext) {
        case '.pdf':
          results.push(await parsePDF(filePath))
          break
        case '.doc':
        case '.docx':
          results.push(await parseWord(filePath))
          break
        case '.xls':
        case '.xlsx':
          results.push(await parseExcel(filePath))
          break
        default:
          results.push(`[不支持的文件格式: ${ext}]`)
      }
    } catch (error) {
      console.error(`[FileParser] 解析 ${filename} 失败:`, error.message)
      results.push(`[文件解析失败: ${filename}]`)
    }
  }

  return results.join('\n\n')
}

5.2 PDF解析

pdf-parse库提取文本:

import pdf from 'pdf-parse'

async function parsePDF(filePath: string): Promise<string> {
  const buffer = fs.readFileSync(filePath)
  const data = await pdf(buffer)

  // 清理多余空白
  const text = data.text
    .replace(/\s+/g, ' ')
    .replace(/\n{3,}/g, '\n\n')
    .trim()

  return `[PDF内容]\n${text}`
}

踩过坑:扫描版PDF(图片PDF)pdf-parse提取不出来,返回空字符串。这种情况下只能提示用户手动描述需求,或者用OCR(智枢矩阵有OCR能力,但在对话生成流程中没串联,后续可以加上)。

5.3 Excel解析

Excel是比较容易出效果的——列头天然就是字段标签:

import XLSX from 'xlsx'

async function parseExcel(filePath: string): Promise<string> {
  const workbook = XLSX.readFile(filePath)
  const sheetName = workbook.SheetNames[0]  // 取第一个Sheet
  const sheet = workbook.Sheets[sheetName]
  const data = XLSX.utils.sheet_to_json(sheet)

  // 提取列头作为字段描述
  const headers = Object.keys(data[0] || {})
  const rows = data.slice(0, 5)  // 只取前5行做示例

  return `[Excel内容 - Sheet: ${sheetName}]\n列头: ${headers.join(', ')}\n示例数据:\n${JSON.stringify(rows, null, 2)}`
}

Excel上传的效果很好,因为列头就是天然的label,AI几乎不会理解错。

六、前端Schema渲染:20种字段类型的映射

AI生成的是JSON Schema,前端需要把它渲染成真实的表单组件。

6.1 类型映射表

// utils/schemaRender.ts
const FIELD_TYPE_MAP: Record<string, Component> = {
  text: 'ElInput',
  textarea: 'ElInput',
  number: 'ElInputNumber',
  email: 'ElInput',
  phone: 'ElInput',
  date: 'ElDatePicker',
  time: 'ElTimePicker',
  select: 'ElSelect',
  radio: 'ElRadioGroup',
  checkbox: 'ElCheckboxGroup',
  file: 'ElUpload',
  image: 'ElUpload',
  url: 'ElInput',
  rating: 'ElRate',
  slider: 'ElSlider',
  cascade: 'ElCascader',
  section: 'SectionDivider',    // 自定义:分组标题
  description: 'DescriptionBlock', // 自定义:说明文字
  calculation: 'CalculationField', // 自定义:计算字段
  location: 'LocationPicker'    // 自定义:位置选择
}

6.2 Schema标准化

AI返回的字段可能不完整——比如缺placeholder、缺required。标准化函数统一补齐:

function normalizeSchema(schema: any): FormSchema {
  const title = schema.title || '未命名表单'
  const fields = (schema.fields || []).map((field: any, index: number) => ({
    id: `field_${Date.now()}_${index}`,
    label: field.label || `字段${index + 1}`,
    type: FIELD_TYPE_MAP[field.type] ? field.type : 'text',  // 未知类型降级为text
    placeholder: field.placeholder || `请输入${field.label || '内容'}`,
    required: field.required ?? false,
    options: field.options || [],
    group: field.group || '',
    // 默认值补齐
    defaultValue: field.defaultValue ?? '',
    validation: field.validation || {},
  }))

  return { title, fields }
}

未知类型降级为text——这条规则救了我好多次。AI偶尔会生成一些不在映射表里的类型(比如colorpassword),直接崩掉体验很差,降级为text至少能用。

七、AI参数调优

7.1 为什么选doubao-seed

智枢矩阵用的模型是火山方舟的doubao-seed-1-6-251015,不是最贵的,但做JSON Schema生成够用。

试过几个模型对比:

模型

JSON格式成功率

中文理解

价格

结论

doubao-pro

~85%

优秀

性价比不高

doubao-seed

~80%

良好

便宜

✅ 够用

GLM-4

~70%

优秀

中等

格式稳定性差

DeepSeek-V3

~75%

良好

便宜

偶尔出幻觉

doubao-seed的格式稳定性最好,配合三层解析兜底,综合成功率能做到95%+。

7.2 参数配置

// services/ai.js
const AI_CONFIG = {
  url: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
  model: 'doubao-seed-1-6-251015',
  temperature: 0.7,   // 不能太低——字段类型需要一定创造性;不能太高——格式会乱
  max_tokens: 1200     // 够生成20+字段的表单
}

temperature这个值我调了很久:

• 0.3:输出格式很稳定,但生成的字段太保守,用户说"活动报名表",它只生成姓名和电话

• 0.5:稍微好一点,但字段覆盖还是不够

• 0.7:甜蜜点——格式基本稳定,字段覆盖也够丰富

• 0.9:格式开始不稳定,偶尔输出非JSON

7.3 max_tokens截断问题

max_tokens: 1200对大部分表单够用,但偶尔遇到复杂表单(20+字段),AI输出到一半被截断。这正是第二层解析的用武之地——截断的JSON也能提取出完整字段

不过更好的方案是根据字段数量动态调整:

function estimateTokens(fieldCount: number): number {
  // 粗略估算:每个字段约50 tokens,基础结构约100 tokens
  return Math.min(4096, Math.max(1200, fieldCount * 50 + 100))
}

这个优化还没上线,目前1200 tokens配合三层兜底已经够用。

八、一个完整的多轮对话示例

用户:帮我做一个课程评价表

AI返回(第一轮)

{
  "title": "课程评价表",
  "fields": [
    {"label": "课程名称", "type": "text", "required": true, "group": "课程信息"},
    {"label": "授课教师", "type": "text", "required": true, "group": "课程信息"},
    {"label": "课程内容满意度", "type": "rating", "required": true, "group": "评价"},
    {"label": "授课方式满意度", "type": "rating", "required": true, "group": "评价"},
    {"label": "是否推荐给他人", "type": "radio", "required": true, "options": ["是", "否", "看情况"], "group": "评价"},
    {"label": "改进建议", "type": "textarea", "required": false, "group": "建议"}
  ]
}

用户:加一个学期选择字段,选项是2025春季、2025秋季、2026春季

AI返回(第二轮)

{
  "title": "课程评价表",
  "fields": [
    {"label": "学期", "type": "select", "required": true, "options": ["2025春季", "2025秋季", "2026春季"], "group": "课程信息"},
    {"label": "课程名称", "type": "text", "required": true, "group": "课程信息"},
    {"label": "授课教师", "type": "text", "required": true, "group": "课程信息"},
    {"label": "课程内容满意度", "type": "rating", "required": true, "group": "评价"},
    {"label": "授课方式满意度", "type": "rating", "required": true, "group": "评价"},
    {"label": "是否推荐给他人", "type": "radio", "required": true, "options": ["是", "否", "看情况"], "group": "评价"},
    {"label": "改进建议", "type": "textarea", "required": false, "group": "建议"}
  ]
}

注意AI做了两件事:加了新字段 + 把学期排到最前面(因为它是选课的前提条件)。这种"顺便"的智能排列,比简单的追加好得多。

九、踩坑总结

现象

原因

解法

调试时长

AI输出带前后文字

JSON前后有"这是您的表单"

Prompt约束不够强

三层解析第一层:提取{}之间内容

2小时

JSON被截断

后半部分字段丢失

max_tokens不够

第二层正则提取完整字段对象

3小时

AI输出Markdown代码块

json ...  包裹

AI习惯性用代码块格式

预处理去掉代码块标记

0.5小时

多轮对话重做不修改

用户说"加字段",AI重头生成

没告诉AI当前Schema

把当前Schema塞进system消息

1小时

扫描版PDF解析为空

上传PDF后AI没有参考内容

图片PDF无法提取文字

提示用户手动描述,后续加OCR

1小时

未知字段类型崩掉

AI生成color类型,前端报错

类型映射表没有该类型

未知类型降级为text

0.5小时

temperature太高格式乱

AI偶尔输出非JSON

0.9时创造性过强

降到0.7,格式和创造性平衡

1小时

十、写在最后

"AI对话生成表单"听起来很简单——调个API就完事了。实际上从Prompt设计到JSON解析兜底到多轮上下文维护到文件解析,每一环都有坑。

三层JSON解析兜底是我觉得最值得分享的设计。没有它,线上四分之一的请求会白跑;有了它,AI输出再脏也能捞到有用的字段。这个思路不只适用于表单生成,任何需要AI输出结构化数据的场景都可以用——永远不要信任AI的输出格式,但永远要尽力从它的输出中提取价值

智枢矩阵是一个开源AI智能表单生成平台,支持AI多轮对话生成表单、智能评分、数据洞察等功能。如果你在做类似的产品,或者对AI Agent + 结构化数据输出感兴趣,欢迎来聊聊。

你做AI应用的时候,怎么处理模型输出的不确定性? 评论区见 👇

⚙️ 相关链接

• 智枢矩阵开源地址:https://gitee.com/zhang-dongtao/zhishu-matrix-open

• 智枢矩阵在线体验:https://www.zhishujuzhen.com

• 上一篇:火山方舟TTS v3接入全记录:7个坑踩完才跑通

关于作者:独立开发者,用 AI 当队友,一个人干一个团队的活。专注 AI + 工具矩阵方向,智枢矩阵作者。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐