前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8836
  • GitHub Copilot 代码审查新增个人配置选项
  • MCP单人上手容易,团队规模落地是另一回事
  • 影子测试100%一致率背后:模型实际正确率仅75%
  • 两个都通过的测试,代价却不同:重试的隐性成本
  • 定时运行AI Agent输出飘移的根因与修复
  • Anthropic如何两周将Claude.ai速度提升3倍
  • claude-code-templates:一键装配 Claude Code 开发套件,含 100+ Agent/MCP
  • Agent 删改测试必须拦截:CI 合并门禁实操方案
  • Google Antigravity SDK 支持本地 AI 模型:Gemma 4 26B 可离线跑
  • NVIDIA Warp 与 MjWarp 加速机器人仿真工作流
  • HEMA 用 MCP 和 Amazon Bedrock 实现内部 AI 助手转型
  • 五大LLM网关工具生产环境横评
  • GitHub Copilot应用如何渲染百万行PR
  • 基于 AWS 构建 Agent 式视频智能对话系统架构解析
  • Bedrock 上用开源权重模型做 AI 编程助手
  • Anthropic 实验室:Claude 自主发现类 CRISPR 新型酶系统
  • ChatGPT Voice 集成邮件、日历和 Slack:Altman 心中的"Her"更近一步
  • OpenAI GPT-6 Sol/Luna 和 Claude Opus 5.5 同步降价 50%
  • AI 工具循环必须显式传递 Retry-After 头否则必死循环
  • AI 代码补丁静默引入新工具调用:merge 前必须强制契约检查
  • AI 写 API 文档无法区分 null/0/缺省三态:OpenAPI 契约必须显式约束
  • AI 编程 Agent 工具输出遭截断:应记录 stdout_bytes 和截断标志
  • Anthropic工程师揭秘:Claude为何越进化写作越差
  • Gemini 3.8 Flash / Flash-Lite TTS 发布:千款语音、30秒克隆、逐行台词控制
  • 工程师详解:新版Claude为何写作风格变得怪异
  • 小米MiMo-V3将搭载HySparse 2:100万Token下KV缓存缩小4.5倍
  • GitHub Copilot 应用新增本地沙箱隔离功能
  • AI Agent 调试指南:重启不是调试,七层架构定位根因
  • AI 加剧软件供应链攻击威胁,行业如何应对
  • 阿里 Qwen Audio 3.1 发布:语音识别/TTS 多模型,API 价格最高降 95%
  • Claude Code部署到Lizard平台实战指南
  • Claude Opus 5.5降价却破坏四个Agent依赖项
  • OpenAI GPT-6 Sol/Luna 半价发布,缓存机制或为更大降本杠杆
  • treg:聚合 3000+ Agent 工具的统一网关
  • 向量检索权限校验应内嵌到 pgvector 查询中
  • Univer:面向 AI Agent 的开源办公套件 SDK
  • DeepSeek公开Agent训练新论文,梁文锋署名
  • 为 AI 编程 Agent 构建可复用技能系统的实践
  • DeepMind研究:百个AI智能体协作求解时出现作弊与告密现象
  • Google AX:开源Agent编排运行时
  • 阿里千问发布Qwen-Audio-3.1:TTS降价70%、ASR降价95%
  • 诺基亚开源AnyJev:无训练即可将任意开源LLM转为校准决策模型
  • 2026年AI网关横评:Bifrost领跑,多路 failover 哪家强
  • GPT-6 Sol/Luna 发布:准确率翻倍、成本减半,价格战开启
  • Claude Opus 5.5 登场,AI 模型价格普降 40-50%
  • Kyutai开源语音模型Voice of Reason,口算GSM8K准确率从27%升至77%
  • 微软Copilot大促:10万席最高半价,向超级AI应用转型
  • OpenAI GPT-6 Sol/Luna:API价格腰斩,长任务Prompt缓存优化
  • AI 编码 Agent 在移动端多久“看”一次才够用
  • GPT-6 Astra 引领 3D 生成技术,竞争格局生变
  • Claude Opus 5.5 缓存读取降价 60%,68 万行代码迁移攻略
  • 已加载 51 / 8836
8.0
热点
AI SCORE
技术实践2026-09-24 00:29

AI 写 API 文档无法区分 null/0/缺省三态:OpenAPI 契约必须显式约束

dev.to · AI#OpenAPI#AI 编程#API 设计
Editor brief · 编辑速览

AI 生成的 API 文档常把 limit=0(无上限)、limit 缺省(默认20)、limit=null(报错)混为一谈,建议用 OpenAPI closed schema 显式定义三者差异,并在文档生成流程中强制校验。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

典型的重生成失败是静默的。模型返回的 /list-invoices 页面带有一张参数表,表格看起来是完整的。但随后有客户端因为表格写了"0 表示无限制"而发送了 limit=0。

OpenAPI 文件从未说明这一点。limit 是可选的,服务端默认值为 20。发送 0 返回空页面,发送 JSON null 到 query string 会得到 400。三种状态,一个坍缩的句子。

这不是语气问题,是契约问题。模型擅长填充表格,但拙于保留"字段缺失"、"显式 null"和"数值零"之间的差异。

一次响应中的坍缩

以这个查询结构为例:

parameters:
  - name: limit
    in: query
    required: false
    schema:
      type: integer
      minimum: 0
      maximum: 100
      default: 20
  - name: cursor
    in: query
    required: false
    schema:
      type: string
      nullable: true
  - name: include_deleted
    in: query
    required: false
    schema:
      type: boolean

文档模型通常会生成这样的内容:

limit (integer, default 0) — 返回的发票数量。用 0 表示全部。cursor — 不透明的分页令牌。传 null 从头开始。include_deleted — 可选。省略或为 null 时默认为 false。

每一行都很流畅。每一行都以不同的方式出错。

limit 默认值是 20,不是 0。零是一个合法的值,但含义不同。

cursor 省略表示"第一页"。cursor=null 不是有文档记录的开始令牌。

include_deleted 在 schema 中没有默认值。将省略和 null 视为 false 是一条虚构的策略。

如果你只检查 hallucinated URL,这个页面就发出去了。损害体现在客户端 SDK 和工单里,而不是 Markdown linter 中。

模型可以起草的内容

把模型限定在 schema 之内。如果某个事实写在 OpenAPI 里,模型可以复述它。如果某个事实不在 OpenAPI 里,模型不应该编造替代品。

可起草的(schema 支撑的):

  • 参数名、位置(query / path / header / body)和 JSON 类型
  • required 与 optional,当该标志明确时
  • minimum、maximum、enum、pattern、maxLength
  • 从 checked-in fixture 复制的示例 payload,而非从 prompt 中获取
  • 跨链接到同一 spec 中已存在的其他操作

不可起草的(人拥有的):

  • schema 中缺失的默认值
  • 等价声明:"省略表示 X"、"null 表示 Y"、"零表示 Z"
  • 服务端回退时机(客户端默认值 vs 服务端默认值 vs 代理默认值)
  • 分页的不透明性、cursor 过期时间,以及过滤器变更后会发生什么
  • 幂等性、重试和部分成功行为
  • 单位、时区、取整规则和货币最小单位

划分是机械性的。名称和类型是结构性的。缺失的含义是行为性的。重生成前者不应改写后者。

一张可以 check in CI 的决策表

这张表才是产物,而不是周围 prose。如果某一行无法从 spec 填充,格子就留空,直到有人在签名文件中写入。

Fixture:三态,一个字段族

保存为 fixtures/list-invoices.openapi.yaml。它故意做得很小。Gate 应该对含义失败,而不是对文件大小失败。

openapi: 3.0.3
info:
  title: Invoices
  version: 0.0.0
paths:
  /v1/invoices:
    get:
      operationId: listInvoices
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 0
            maximum: 100
            default: 20
        - name: cursor
          in: query
          required: false
          schema:
            type: string
            nullable: false
        - name: include_deleted
          in: query
          required: false
          schema:
            type: boolean
      responses:
        "200":
          description: A page of invoices

人类拥有的 notes 放在 spec 旁边,而不是放在生成的页面内。例如:docs/owned/listInvoices.states.md。

# listInvoices — three-state notes
# owner: api-docs
# signed: true

## limit
- omit: server applies 20
- 0: empty page, not "unlimited"
- null: not a legal query value (400)

## cursor
- omit: first page
- empty string: 400
- null: 400
- opaque token: server-defined; do not document internals

## include_deleted
- omit: unspecified; do not claim a default
- true / false: filter as named
- null: 400

生成的 Markdown 可以引用这些 notes,但不得将它们改写成更友好的默认值。

Gate:先分类,再扫描

下面的检查器是一个自包含示例。对 fixture 运行它。这是一个契约 linter,不是生产级文档平台。

#!/usr/bin/env python3
"""Fail docs regen when a model collapses omit / null / zero."""
from __future__ import annotations

import json
import re
import sys
from pathlib import Path

try:
    import yaml
except ImportError:
    yaml = None

INVENTED_DEFAULT = re.compile(
    r"defaults?\s+to\s+[`'"]?(all|unlimited|none|null|0|false|true)",
    re.I,
)
EQUATES_NULL_OMIT = re.compile(r"null\s+(means|is)\s+(omitted|absent|missing)", re.I)
ZERO_MEANS_ALL = re.compile(r"\b0\b.{0,40}(unlimited|all records|no cap)", re.I)

def load_spec(path: Path) -> dict:
    text = path.read_text(encoding="utf-8")
    if path.suffix in {".yaml", ".yml"}:
        if yaml is None:
            raise SystemExit("pip install pyyaml")
        return yaml.safe_load(text)
    return json.loads(text)

def iter_params(spec: dict):
    for path, item in (spec.get("paths") or {}).items():
        for method, op in item.items():
            if not isinstance(op, dict):
                continue
            op_id = op.get("operationId") or f"{method.upper()} {path}"
            for param in op.get("parameters") or []:
                schema = param.get("schema") or {}
                yield {
                    "op": op_id,
                    "name": param.get("name"),
                    "required": bool(param.get("required")),
                    "type": schema.get("type"),
                    "default": schema.get("default", _Missing),
                    "nullable": schema.get("nullable", False),
                    "minimum": schema.get("minimum", _Missing),
                }

class _Missing:
    pass

def classify(param: dict) -> dict:
    flags = []
    if param["default"] is _Missing and not param["required"]:
        flags.append("no_default_do_not_invent")
    if param["default"] is not _Missing:
        flags.append("default_is_schema_owned")
    if param["nullable"]:
        flags.append("null_is_not_omit")
    else:
        flags.append("null_is_invalid_unless_spec_says")
    if param["type"] == "integer" and param["minimum"] == 0:
        flags.append("zero_is_a_value")
    return {**param, "flags": flags}

def scan_markdown(md: str, classified: list[dict]) -> list[str]:
    errors = []
    if INVENTED_DEFAULT.search(md):
        errors.append("invented_default_phrase")
    if EQUATES_NULL_OMIT.search(md):
        errors.append("null_equated_to_omit")
    if ZERO_MEANS_ALL.search(md):
        errors.append("zero_collapsed_to_unlimited")
    for row in classified:
        if "no_default_do_not_invent" in row["flags"]:
            pat = re.compile(
                rf"{re.escape(row['name'])}.{{0,80}}defaults?\s+to",
                re.I | re.S,
            )
            if pat.search(md):
                errors.append(f"invented_default:{row['op']}:{row['name']}")
        if "zero_is_a_value" in row["flags"] and row["default"] != 0:
            pat = re.compile(
                rf"{re.escape(row['name'])}.{{0,80}}default\s+[`']?0",
                re.I | re.S,
            )
            if pat.search(md):
                errors.append(f"wrong_default_zero:{row['op']}:{row['name']}")
    return errors

def main(argv: list[str]) -> int:
    if len(argv) != 3:
        print("usage: three_state_gate.py <openapi> <generated.md>", file=sys.stderr)
        return 2
    spec = load_spec(Path(argv[1]))
    md = Path(argv[2]).read_text(encoding="utf-8")
    classified = [classify(p) for p in iter_params(spec)]
    errors = scan_markdown(md, classified)
    print(json.dumps({"params": classified, "errors": errors}, indent=2, default=str))
    return 1 if errors else 0

if __name__ == "__main__":
    raise SystemExit(main(sys.argv))

一个有问题的页面长这样。保存为 fixtures/list-invoices.bad.md:

## Query parameters

- `limit` (integer, default `0`) — use `0` for unlimited.
- `cursor` — pass `null` to start. Null means omitted.
- `include_deleted` defaults to false.
python3 three_state_gate.py \
  fixtures/list-invoices.openapi.yaml \
  fixtures/list-invoices.bad.md

预期:非零退出码。JSON 报告应包含 wrong_default_zero、zero_collapsed_to_unlimited、null_equated_to_omit 和 invented_default:listInvoices:include_deleted。

一个干净的页面复述 schema 事实,并指向签名的 notes:

## Query parameters

- `limit` (integer, optional, schema default `20`, minimum `0`). Zero is a legal value; it is not unlimited. See owned notes.
- `cursor` (string, optional, not nullable). Omit for the first page. Do not send null.
- `include_deleted` (boolean, optional). No schema default. Do not equate omit with false.

Gate 不试图理解 invoices。它只是拒绝坍缩的状态。

保持三输入、一输出、一拒绝路径

提取。 读取 OpenAPI。用上方的表格对每个参数分类。写出 classified.json。

起草。 用分类后的行和一份禁止列表来 prompt 模型:除非 default 存在,否则不写默认值;不写"null 表示省略";不写"0 表示全部"。

扫描。 在草稿上运行 gate。任何错误码都导致 job 失败。

合并。 如果扫描通过,将草稿与签名的三态 notes 拼接。不要让模型重写 notes 文件。

发布。 只有合并后的文档是权威的。草稿文件用完即弃。

Prompt 文本应该枯燥。一份可用的 system prompt 是一份拒绝列表,而不是一份风格指南。

You draft parameter tables from classified.json only.
If default is missing, write "no schema default."
Never equate null with omit.
Never equate 0 with unlimited.
Never invent units, timezones, or retry rules.
If a human note file is provided, quote it; do not paraphrase defaults.

模型是一个坐在分类器上的格式化工具。它不是行为的数据源。

什么时候免费模型就够用了

这项工作是分类加约束起草。它不需要长时间运行的 agent,也不需要模型去发明重试策略。

声明:本文是 MonkeyCode 产品推广的一部分。MonkeyCode 的免费模型访问和免费服务器选项足以在这类 fixture spec 上运行 extract-draft-scan 循环:模型填充表格,gate 拒绝坍缩的状态,签名的 notes 保持在重生成路径之外。这是唯一的产品角色。如果换成其他模型,所有权划分仍然有效。

如果你已经视 OpenAPI 为权威,有价值的实验是 gate,而不是一种新的 prose 风格。

已知局限

Scanner 是基于短语匹配的。模型仍可能在正则没捕获到的句子中夹带一个错误的默认值。可以用 AST 检查生成的 MDX 来收紧它,或者要求每个默认值都作为带围栏的 schema.default 值出现。

OpenAPI 3.0 的 nullable 不等于 3.1 的 union type。上文的分类器不展开 oneOf / anyOf。如果你的 spec 对"string or null"使用了 union,要先扩展 classify() 再信任 gate。

Query 参数是最简单的情况。带有 additionalProperties、vendor extensions 和多态 discriminator 对象的 body 字段需要不同的 owned-notes 布局。不要把这个脚本当作通用 API linter 来复用。

Gate 不能证明服务器与 spec 一致。如果生产环境应用的是 50 而 OpenAPI 写的是 20,模型和人类 notes 都会一致地出错。Spec 漂移是另一套 pipeline 的问题。

谁不应该用这个

如果你的文档是手写的、很少重新生成,跳过这个划分。假设草稿是一次性的 gate 只会增加仪式感。

如果法律或安全文本与参数表在同一个 Markdown 文件里,跳过它。那些文件根本不应该被模型写入。

如果你无法为三态 notes 指定负责人,跳过它。spec 旁边未签名的"behavior"注释会被覆盖,gate 无法区分已审查的句子和遗留的 prompt。

实际规则很窄。让模型起草表格。把 omit、null 和 zero 放在签名文件中。当这三种状态坍缩成一个形容词时,让重生成失败。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
AI 代码补丁静默引入新工具调用:merge 前必须强制契约检查
下一篇
AI 编程 Agent 工具输出遭截断:应记录 stdout_bytes 和截断标志