Agent Skill 与网站生成
提示词、Contract 与状态机如何协作:构建可控的 Agent Skill
把 Prompt、契约、确定性校验和状态机分层,减少 Agent 工作流中的隐式假设。
最近在制作 Agent Skill 的过程中,我发现一个很容易被忽略的问题:我们经常花很多时间优化提示词,告诉 Agent 应该生成什么,却很少认真设计它应该在什么时候询问、什么时候等待,以及满足什么条件后才能继续执行。
比如,用户只说了一句“帮我做一个网站”,Agent 便立即创建项目、选择配色、添加动效,甚至直接安装依赖。几分钟后,网页确实生成了,但结构不符合预期,风格不是用户喜欢的,修改起来比重新制作还麻烦。
继续完善 Skill 后,我逐渐意识到,问题并不是模型不会写代码,而是工作流缺少控制。只增加一句“先询问用户需求”也不够,因为 Agent 仍然需要知道应该逐个问什么、什么回答算确认、计划何时生成,以及对话中断后如何恢复。
一个可靠的 Agent Skill 不应该只告诉模型“完成什么任务”,还需要回答三个问题:
- 当前应该做什么?
- 满足什么条件才能进入下一步?
- 如何记住已经完成和批准的决策?
这三个问题分别对应提示词、Contract 和状态机。
它们组合起来,可以把一次不可预测的模型调用,变成一个能够逐步澄清、明确审批、校验产物并在失败后恢复的工程流程。
只有提示词,为什么还不够?
假设我们写下这样的提示词:
请先询问用户的网站需求,再制定计划,最后生成网站。
看起来已经包含完整流程,但实际执行时仍然存在大量歧义:
- “询问需求”需要问多少个问题?
- 一次问一个,还是一次列出十个?
- 用户说“继续”是否代表需求已经确认?
- 什么时候生成计划?
- 计划是否需要用户批准?
- 计划缺少响应式设计时能否继续?
- 对话中断后如何知道上次进行到哪里?
- 生成失败后是否会覆盖上一次成功结果?
提示词表达了意图,却没有提供足够明确的边界。
语言模型可能遵守,也可能根据上下文进行简化。模型甚至可能认为“先生成一个版本给用户看”比继续询问更高效,于是跳过计划阶段。
因此,一个可控的 Agent Skill 通常需要四种不同机制:
| 机制 | 主要职责 |
|---|---|
| 提示词 | 指导 Agent 当前应该怎么做 |
| Contract | 定义输入、输出、边界和完成条件 |
| Schema 与校验器 | 对结构化产物进行确定性检查 |
| 状态机 | 记录当前阶段和允许的状态转移 |
提示词负责“引导”,校验器负责“拒绝”,状态机负责“记忆”。
提示词:规定这一轮应该做什么
提示词最适合描述需要模型理解和判断的行为,例如:
- 阅读用户提供的材料;
- 找出信息缺口;
- 选择当前最重要的问题;
- 向用户解释不同方案的取舍;
- 根据内容推荐视觉方向;
- 将用户的反馈转化为修改建议。
例如,一个逐步澄清提示词可以写成:
# Ask One Clarification
从当前未解决问题中,选择影响最大的一个。
要求:
1. 每轮只问一个问题;
2. 尽量展示与问题相关的原始内容;
3. 说明该问题为什么会影响最终产物;
4. 问题必须具体、可以直接回答;
5. 如果用户不知道精确答案,提供一个不虚构信息的回退方案。
这段提示词解决的是“怎么问”。
当 Agent 发现三个信息缺口时,它不会一次抛出一张复杂表单,而是选择当前最关键的问题。例如:
你提到“优化了接口性能”,但没有给出测量结果。你是否保留了响应时间、吞吐量或压测记录?
这个信息会影响项目成果的表达。如果没有精确数据,我们也可以使用“减少高峰期请求阻塞”这样的定性描述。
这里需要语言理解、上下文判断和自然表达,因此适合由提示词控制。
但提示词无法确定性地证明“这一阶段已经完成”。这就需要 Contract。
Contract:定义什么才算完成
Contract 可以理解为 Agent 与工作流之间的协议。
它通常描述:
- 当前阶段需要哪些输入;
- Agent 必须完成哪些动作;
- 需要生成哪些产物;
- 哪些行为被禁止;
- 什么条件下可以进入下一阶段;
- 失败时如何处理。
例如,一个需求澄清 Contract 可以这样定义:
# Requirements Discovery Contract
开始条件:
- 用户已经提供初始目标;
- 当前没有已批准的需求规格。
完成条件:
- 目标用户已经确认;
- 页面结构已经确认;
- 配色方向已经确认;
- 主动效已经确认;
- 所有阻塞问题均已解决。
审批规则:
- 只有用户在对话中的明确回复可以构成批准;
- 沉默、“继续”或浏览器操作不能构成批准;
- 一个类别的批准不能自动批准其他类别。
完成产物:
- reports/design-spec.json
这段 Contract 解决的是“问到什么程度才算问完”。
它还防止 Agent 进行过度推断。例如用户说“这个配色可以”,只能批准配色,不能同时批准结构、字体和动效。
Contract 仍然是提示词吗?
如果 Contract 是 Markdown 文件,那么从运行机制看,它依然属于提供给模型的自然语言上下文。
它不会自行执行,也不能主动阻止 Agent。
但 Contract 和普通提示词在职责上不同:
- 提示词描述当前行为;
- Contract 描述跨阶段不应被破坏的规则。
可以把提示词理解成“操作说明”,把 Contract 理解成“流程协议”。
只有当 Contract 的一部分被转换为 Schema 或程序校验器后,才形成真正的硬约束。
Schema 和校验器:把部分规则变成硬约束
自然语言适合表达意图,但不适合检查精确结构。
假设 Agent 完成需求澄清后,需要生成一个设计规格:
{
"schema_version": 1,
"structure": {
"status": "user_approved",
"selected": "editorial"
},
"color": {
"status": "user_approved",
"selected": "warm-neutral"
},
"primary_motion": {
"status": "user_approved",
"selected": "sticky-stack"
}
}
我们可以使用 JSON Schema 规定:
schema_version必须为1;- 每个设计类别必须存在;
status必须为user_approved;- 每个类别必须有明确选择;
- 不允许出现未定义字段。
简化后的 Schema 如下:
{
"type": "object",
"required": [
"schema_version",
"structure",
"color",
"primary_motion"
],
"properties": {
"schema_version": {
"const": 1
},
"structure": {
"$ref": "#/$defs/approvedDecision"
},
"color": {
"$ref": "#/$defs/approvedDecision"
},
"primary_motion": {
"$ref": "#/$defs/approvedDecision"
}
},
"$defs": {
"approvedDecision": {
"type": "object",
"required": ["status", "selected"],
"properties": {
"status": {
"const": "user_approved"
},
"selected": {
"type": "string",
"minLength": 1
}
}
}
}
}
再通过校验脚本检查产物:
import json
from pathlib import Path
def validate_design_spec(path: Path) -> list[str]:
data = json.loads(path.read_text(encoding="utf-8"))
errors = []
if data.get("schema_version") != 1:
errors.append("schema_version must be 1")
for name in ("structure", "color", "primary_motion"):
decision = data.get(name)
if not isinstance(decision, dict):
errors.append(f"{name} is missing")
continue
if decision.get("status") != "user_approved":
errors.append(f"{name} is not user approved")
if not decision.get("selected"):
errors.append(f"{name} has no selected value")
return errors
此时,Agent 即使错误地跳过了配色确认,也无法生成一个能够通过校验的设计规格。
不过需要注意:校验器只能证明 JSON 中写了 user_approved,不能独立证明用户真的说过“批准”。
审批真实性仍然需要 Agent 根据对话判断,或者由更上层系统保存不可伪造的审批事件。
状态机:决定当前处于哪一步
当工作流包含多个阶段时,只靠文档顺序很容易混乱。
我们可以为每个阶段定义一个状态:
requirements_discovery
→ requirements_waiting_confirmation
→ todo_plan_generating
→ todo_plan_waiting_confirmation
→ strategy_waiting_confirmation
→ implementation_plan_generating
→ implementation
→ review
→ complete
它解决三个问题:
- 当前进行到哪里;
- 下一步允许进入哪里;
- 需求发生变化时,需要废弃哪些后续产物。
例如,只有用户批准完整需求后,状态才能从:
requirements_waiting_confirmation
进入:
todo_plan_generating
生成 TODO 后进入:
todo_plan_waiting_confirmation
用户明确批准 TODO 后,才进入执行策略选择:
strategy_waiting_confirmation
状态保存在哪里?
最简单的方式是使用一个 JSON 文件:
{
"schema_version": 1,
"stage": "todo_plan_waiting_confirmation",
"approved_decisions": [
"structure",
"typography",
"color",
"primary_motion"
],
"requirements_approved": true,
"todo_plan_approved": false,
"execution_strategy": null,
"last_valid_artifact": "versions/v1"
}
Agent 每轮开始时读取状态文件,完成动作后再更新。
这样即使对话中断,下次也可以知道:
- 需求已经确认;
- TODO 已生成;
- 当前正在等待 TODO 批准;
- 不能直接开始实现。
状态机是否真的会自动运行?
不一定。
如果状态转移只写在 Markdown 中,那么它仍然主要依赖 Agent 遵守。
更可靠的方式是提供一个状态转移函数:
ALLOWED_TRANSITIONS = {
"requirements_discovery": {
"requirements_waiting_confirmation"
},
"requirements_waiting_confirmation": {
"todo_plan_generating"
},
"todo_plan_generating": {
"todo_plan_waiting_confirmation"
},
"todo_plan_waiting_confirmation": {
"strategy_waiting_confirmation"
},
"strategy_waiting_confirmation": {
"implementation_plan_generating"
},
"implementation_plan_generating": {
"implementation"
}
}
def transition(current: str, target: str) -> None:
allowed = ALLOWED_TRANSITIONS.get(current, set())
if target not in allowed:
raise ValueError(
f"invalid transition: {current} -> {target}"
)
如果再加入阶段前置条件,状态机就能真正阻止非法推进:
def can_enter_implementation(state: dict) -> list[str]:
errors = []
if not state.get("requirements_approved"):
errors.append("requirements approval is missing")
if not state.get("todo_plan_approved"):
errors.append("TODO plan approval is missing")
if state.get("execution_strategy") is None:
errors.append("execution strategy is missing")
return errors
这就是“文档状态机”和“程序状态机”的区别:
| 类型 | 特点 |
|---|---|
| 文档状态机 | 灵活,容易编写,但依赖 Agent 遵守 |
| 程序状态机 | 转移确定、可测试,但开发成本更高 |
| 混合状态机 | Agent 负责语义判断,程序负责关键转移 |
对于 Agent Skill,混合方式通常更实用。
三者是如何协作的?
完整流程可以表示为:
flowchart LR
A["用户提出目标"] --> B["提示词:每轮询问一个关键问题"]
B --> C["Contract:检查当前类别是否满足完成条件"]
C -->|未满足| B
C -->|满足| D["写入设计规格 JSON"]
D --> E["Schema / Validator 校验"]
E -->|失败| B
E -->|通过| F["状态机进入 TODO 生成"]
F --> G["生成可读 TODO Plan"]
G --> H["等待用户明确批准"]
H -->|未批准| G
H -->|批准| I["生成机器实施计划"]
I --> J["实施计划校验"]
J -->|失败| I
J -->|通过| K["状态机允许执行"]
它们各自负责不同层次的问题。
提示词负责语义
例如:
- 哪个问题最重要?
- 如何向用户解释方案?
- 用户反馈意味着什么?
- 哪种设计更适合当前内容?
Contract 负责流程边界
例如:
- 一次只能问一个问题;
- 每个设计类别需要单独批准;
- 浏览器操作不算批准;
- TODO 必须在实施之前生成;
- 修改核心需求后必须重新规划。
状态机负责顺序和恢复
例如:
- 当前正在等待什么;
- 哪些阶段已经完成;
- 哪些产物仍然有效;
- 失败后恢复哪个版本;
- 需求变化后回退到哪个阶段。
校验器负责确定性检查
例如:
- JSON 字段是否完整;
- 状态值是否合法;
- 任务是否包含验收条件;
- 是否缺少批准记录;
- 计划是否引用正确的需求规格;
- 多任务是否存在文件写入冲突。
TODO Plan 为什么需要两种形式?
在可控 Agent 工作流中,TODO 往往分为两层。
面向用户的 Markdown TODO
例如:
# 网站实施计划
- [ ] 创建内容映射
- [ ] 实现页面结构
- [ ] 建立字体和配色变量
- [ ] 实现主动效
- [ ] 添加移动端适配
- [ ] 添加 reduced-motion 回退
- [ ] 运行构建
- [ ] 截取桌面、平板和移动端
- [ ] 完成视觉检查
它的目标是可读。
用户不需要理解任务依赖图,只需要确认:
- 是否遗漏重要工作;
- 是否错误理解需求;
- 实现边界是否合理;
- 验证方式是否充分。
面向机器的 JSON 实施计划
{
"schema_version": 1,
"requirements_id": "design-spec-001",
"todo_plan": "reports/site-todo-plan.md",
"todo_plan_approval": {
"status": "user_approved",
"source": "conversation"
},
"strategy": "single-agent",
"tasks": [
{
"id": "build-layout",
"depends_on": [],
"files": [
"src/App.jsx",
"src/styles/layout.css"
],
"produces": [
"responsive-page-layout"
],
"acceptance": [
"desktop and mobile hierarchy remain consistent"
],
"verification": [
"npm run build"
]
}
],
"rollback_baseline": "versions/v0",
"snapshot_target": "versions/v1"
}
它的目标是可验证、可执行。
这两种计划不能互相替代:
- 只有 Markdown,机器难以检查依赖和文件边界;
- 只有 JSON,用户很难快速理解并批准。
因此合理的顺序是:
需求确认
→ 生成可读 TODO
→ 用户批准
→ 转换为机器计划
→ 程序校验
→ 开始执行
哪些规则适合写进提示词,哪些应该程序化?
可以使用一个简单判断标准:
需要理解语义的交给模型;能够确定判断的交给程序。
| 规则 | 推荐实现 |
|---|---|
| 从多个缺口中选择最重要的问题 | 提示词 |
| 解释不同方案的取舍 | 提示词 |
| 判断用户反馈是否改变核心方向 | 提示词 + Contract |
| 一轮只问一个问题 | 提示词 + 对话测试 |
| JSON 必须包含某个字段 | Schema |
| 状态值只能取三个枚举值 | Schema |
| TODO 未批准不能进入实现 | 状态机 + 校验器 |
| 两个并行任务不能修改同一文件 | 校验器 |
| 构建失败不能覆盖有效预览 | 事务脚本 |
| 用户是否真的表达了批准 | Agent 判断或审批事件系统 |
不要尝试把所有内容都写进一个巨大提示词。
提示词越长,不代表约束越强。相反,不同规则可能互相竞争,模型也更容易忽略位于上下文中部的关键限制。
更合理的方式是按职责拆分:
SKILL.md
├── prompts/
│ ├── ask-one-question.md
│ ├── create-todo-plan.md
│ └── implement-approved-plan.md
├── references/
│ ├── workflow-contract.md
│ ├── planning-contract.md
│ └── implementation-plan-schema.json
└── scripts/
├── validate_plan.py
└── transition_state.py
主 Skill 只负责路由,进入某个阶段时再读取对应的提示词、Contract 和 Schema。
常见误区
把 Contract 当成硬约束
Markdown Contract 本身不会执行。
如果“缺少用户批准不得开始实现”只存在于 Markdown 中,Agent 仍可能违反它。关键边界应尽可能增加程序校验。
在状态文件中直接写“已批准”
状态文件中的:
{
"todo_plan_approved": true
}
只是一个记录,不是批准本身。
更完整的记录至少应包含:
{
"status": "user_approved",
"source": "explicit_user",
"channel": "conversation"
}
如果系统对审批真实性要求较高,还需要保存不可由 Agent 任意修改的事件记录。
同时维护多个真相来源
如果 Markdown TODO、JSON 实施计划和状态文件各自保存一份不同的任务列表,很容易发生漂移。
更稳妥的关系是:
设计规格是需求真相
Markdown TODO 是用户批准的可读计划
JSON 计划引用 TODO,并增加机器执行信息
状态文件只保存当前阶段和产物引用
用总分掩盖阻塞问题
例如,一个网站视觉评分达到 90 分,但移动端无法滚动,仍然不能交付。
Agent 工作流应该保留硬性否决项:
- 构建失败;
- 关键内容缺失;
- 用户未批准;
- 状态转移非法;
- 移动端不可用;
- 事实或权限错误;
- 回滚基线不存在。
核心需求改变后继续使用旧计划
用户修改页面结构后,旧 TODO 和实施计划可能已经失效。
状态机需要明确规定:
核心需求改变
→ 废弃需求批准
→ 废弃 TODO 批准
→ 废弃机器实施计划
→ 返回对应需求阶段
这不是重复劳动,而是在防止 Agent 用过时计划修改新需求。
这套架构的边界在哪里?
提示词、Contract、状态机和校验器能够显著提高 Agent 的稳定性,但不能消除所有不确定性。
Agent 仍然需要进行语义判断:
- “可以”是在批准当前方案,还是仅表示理解?
- 用户提出的修改是否改变核心需求?
- 一个问题是否已经得到足够明确的回答?
- 两个方案是否真的存在明显差异?
- 当前产物是否达到了用户想要的效果?
这些问题很难完全依靠 JSON Schema 解决。
另一方面,程序校验也不应该被提示词替代:
- 文件是否存在;
- JSON 是否有效;
- 依赖是否冲突;
- 构建是否成功;
- 状态转移是否合法;
- 测试是否通过。
一个成熟的 Agent Skill 不是试图消灭模型的不确定性,而是把不确定性限制在真正需要理解和判断的地方。
总结
可控的 Agent Skill 不是一段更长的提示词,而是一套分层工作流:
- 提示词告诉 Agent 当前如何思考和行动;
- Contract 定义阶段边界、审批规则和不变量;
- Schema 与校验器检查结构化产物;
- 状态机记录流程位置并限制状态转移;
- TODO Plan 在用户需求与机器执行之间建立审批边界;
- 快照和回滚机制保证失败不会破坏上一次有效结果。
四者的关系可以浓缩成一句话:
让模型负责理解,让 Contract 负责约定,让程序负责验证,让状态机负责秩序。
当 Agent 能够逐步询问、等待批准、生成计划、验证计划、执行任务并在失败后恢复时,它才从“会调用工具的聊天模型”,逐渐变成一个可控的工程执行系统。