Agent Skill 与网站生成

提示词、Contract 与状态机如何协作:构建可控的 Agent Skill

把 Prompt、契约、确定性校验和状态机分层,减少 Agent 工作流中的隐式假设。

PromptContract状态机

最近在制作 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

它解决三个问题:

  1. 当前进行到哪里;
  2. 下一步允许进入哪里;
  3. 需求发生变化时,需要废弃哪些后续产物。

例如,只有用户批准完整需求后,状态才能从:

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 能够逐步询问、等待批准、生成计划、验证计划、执行任务并在失败后恢复时,它才从“会调用工具的聊天模型”,逐渐变成一个可控的工程执行系统。

陈涛 · Agent Application Developer

杭州 · 2026