$ cd ../blog
$ cat ~/blog/AI 应用开发/llm-workflow-03-ai-payload-builders.mdx

LLM 学习工作流(三):AI 请求 payload 构造器

2026年4月20日·7 min read
<AI 应用开发 />
AILLMPayload协议设计工程实践

这一步在做什么

这一步把"前端页面里的状态"整理成了"发给 AI 工作流层的标准请求体"。

最终新增的文件是:

  • src/utils/ai-payloads.js
  • src/utils/ai.js
  • tests/ai-payloads.test.js

为什么这一步重要

AI 项目里很常见的一个坏味道是:

  • 页面里临时拼一个对象
  • 直接发给后端
  • 下一个页面又拼另一种格式

时间一长,前端和后端之间就会出现很多"长得差不多但不完全一样"的请求体。

这一步的目的就是先把协议收紧:

  • 日计划请求长什么样
  • 复盘请求长什么样
  • 前端统一怎么调 AI 工作流函数

这一步具体做了什么

1. 新增 buildDailyPlanPayload

用于构造"生成今日学习计划"的请求体。

它现在会返回:

{
  action: 'generateDailyPlan',
  runId,
  profile,
  progressSnapshot,
}

2. 新增 buildReflectionPayload

用于构造"生成学习复盘"的请求体。

它现在会返回:

{
  action: 'summarizeReflection',
  runId,
  learnedWords,
  quizAnswers,
}

3. 新增 invokeLearningWorkflow

统一通过 Supabase Edge Function 调 learning-workflow

supabase.functions.invoke('learning-workflow', { body })

这一步的意义不是"少写几行代码",而是把 AI 调用入口集中起来,后面如果你要做:

  • 错误处理
  • 埋点
  • 重试
  • trace id

都可以从这个统一入口下手。

先写了什么测试

这一步仍然是先测后写。

先写的测试只有两个,专门卡 payload builder:

test('buildDailyPlanPayload normalizes the workflow request', ...)
test('buildReflectionPayload includes quiz answers and learned words', ...)

然后先跑:

node --test tests/ai-payloads.test.js -v

第一次失败是对的,因为这时 src/utils/ai-payloads.js 还不存在。

为什么这一步要保持"很小"

这一层最容易犯的错是过早扩展。

比如一开始就加:

  • 十几个 payload builder
  • 很复杂的请求校验器
  • 一个大而全的 AI SDK 封装层

这些都不是现在最需要的。

当前阶段只需要先把两种最明确的请求定下来:

  • 今日计划
  • 学习复盘

这就是典型的 YAGNI。

这一步学到了什么

1. AI 请求协议要尽早结构化

你越早把请求体结构固定下来,后面就越容易做:

  • Edge Function
  • JSON schema
  • prompt 输入整理
  • 错误排查

如果一直在页面里临时拼对象,后面会非常乱。

2. action 字段就是工作流入口开关

这一步的 payload 里有一个非常关键的字段:

action

它其实就是服务端工作流分发的入口。

例如:

  • generateDailyPlan
  • summarizeReflection

后面服务端收到请求时,就可以根据这个字段决定:

  • 走哪个 prompt
  • 用哪个 schema
  • 读哪些上下文

3. 统一调用入口比"直接调函数"更重要

invokeLearningWorkflow 现在看起来很薄,只是包了一层:

supabase.functions.invoke(...)

但这种统一入口在 AI 项目里特别重要,因为后面几乎一定会加:

  • 超时处理
  • 统一错误格式
  • 调试日志
  • 重试策略
  • 请求标识

当前的边界约定

Daily Plan 请求

buildDailyPlanPayload({
  profile: { currentLevel: 'A2', targetGoal: '前端面试英语' },
  progressSnapshot: { dueWords: 8 },
})

会得到:

{
  action: 'generateDailyPlan',
  runId: null,
  profile: { currentLevel: 'A2', targetGoal: '前端面试英语' },
  progressSnapshot: { dueWords: 8 },
}

Reflection 请求

buildReflectionPayload({
  runId: 'run-1',
  learnedWords: ['api', 'cache'],
  quizAnswers: [{ id: 'q1', correct: false }],
})

会得到:

{
  action: 'summarizeReflection',
  runId: 'run-1',
  learnedWords: ['api', 'cache'],
  quizAnswers: [{ id: 'q1', correct: false }],
}

这一步的局限是什么

评审里保留了一个小风险,但不阻塞继续开发:

  • src/utils/ai.js 还没有独立单测

原因也很实际:

  • 现在这层只是一个很薄的转发器
  • 当前任务计划只要求先测 payload builder
  • 等下一步接上 Edge Function 或 mockable 边界,再给它补单测更划算

这也说明一个实践原则:

不是所有代码都要在同一时刻测到同样深。

当前优先级更高的是先把协议形状固定住。

你可以自己复现什么

运行这一步的测试

node --test tests/ai-payloads.test.js -v

看这一步的提交

git show --stat 9330dd5e

这一步你应该学会什么

  • 为什么 AI 请求协议要尽早固定
  • 为什么 action 适合作为工作流分发入口
  • 为什么统一的 AI 调用入口很重要
  • 为什么这一步要故意做得很小
  • 怎么把"页面状态"变成"后端可消费的结构化 payload"

下一步会做什么

下一步是 Task 4

  • 定义 AI 输出 schema
  • 搭起 Supabase Edge Function 骨架
  • 把"前端标准请求体"真正接到"服务端 AI 工作流入口"

到那一步,你会开始真正进入 AI 应用开发里最关键的一层:

结构化输入 -> 结构化输出 -> 服务端编排