LLM 应用的 Schema 演进工程:structured output 字段改了,下游为什么炸了?

一次字段重命名,让 3 个下游服务同时崩溃------我们总结了一套 schema 版本化管理方案


0. 开篇:一个凌晨两点的事故

那是一个普通的周四下午。我们的 LLM 信息抽取服务正在稳定运行,每天处理大约 8 万次请求,把非结构化的用户反馈解析成结构化的 JSON,然后分别推给:数仓 ETL job、前端展示面板、运营告警系统。

产品经理提了个需求:在输出中增加一个 confidence_score 字段,同时把过去含义模糊的 user_name 字段改成更规范的 username,统一命名风格。

需求不复杂。下午三点,我改了 Pydantic model,更新了 prompt,部署上线。

然后,凌晨两点,告警炸了。

  • 数仓 ETL job:KeyError: 'user_name',失败
  • 前端面板:username is undefined,展示空白
  • 运营告警系统:schema validation error,静默丢弃

原因很简单:我以为改的是 schema,实际上改的是一个下游多方依赖的"数据契约" 。下游三个系统都在用 user_name,我单方面把它改掉了,没有通知,没有过渡期,没有兼容层。

这不是低级错误,这是一个系统性问题:大多数团队把 LLM 的 structured output 当成实现细节,而不是需要版本化管理的公共接口。

这篇文章是我们事故之后建立的 schema 演进体系的工程记录。


1. 为什么 Schema 演进比普通 API 演进更难

普通 REST API 演进有成熟的规范:URL 版本化(/v1/, /v2/)、OpenAPI spec、HTTP 状态码语义。但 LLM structured output 有几个特殊性,让演进问题更复杂:

1.1 LLM 输出天然不确定,schema 是人为强加的约束层

LLM 不理解"你不能改字段名"这种隐性契约。当你用 Pydantic 或 Zod 定义 schema 约束 LLM 输出时,这层约束只存在于你的代码里,LLM 侧没有任何感知。你改 schema,LLM 就按新 schema 输出,不管下游准备好了没有。

1.2 Strict mode 关闭了"宽松兼容"的退路

在标准 JSON parsing 里,你可以用"宽松解析"兜底:遇到未知字段就忽略,遇到缺失字段就用默认值。但主流大模型的 Structured Outputs strict mode 要求 additionalProperties: false,加上 token-level enforcement,实际上把 schema 变成了一个硬性约束,不存在"大概能用"的灰色地带。

这在国内外多个主流大模型平台均有体现:根 schema 不能用 anyOf 等限制直接影响你设计兼容层的空间。

1.3 Schema 变更有三个维度,只有结构层可见

变更维度 例子 可见性
结构变更 字段增删、重命名、嵌套层级变化 高(代码层可检测)
类型变更 strintOptional 增删 中(type checker 可检测)
语义变更 字段含义改变,但 type 没变 低(只有 prompt 变了)

语义变更最隐蔽:你把 score 的含义从"情感分数 0-1"改成"置信度 0-100",代码完全不报错,但下游的分析逻辑全部错了。

1.4 Producer 和 Consumer 天然解耦,变更通知成本高

LLM 调用层(Producer)和消费方(Consumer)往往属于不同团队,甚至不同服务。在没有 schema registry 的情况下,一次 schema 变更的扩散靠的是口口相传或 Slack 通知,不可靠。


2. Schema 变更分类:哪些是安全的,哪些会炸

在建立演进体系之前,先要能分辨哪些变更是安全的(non-breaking),哪些是危险的(breaking)。

2.1 Backward Compatible 变更(向后兼容,可直接发布)

新增 optional 字段是最安全的变更,旧 Consumer 不认识这个字段,会忽略它:

python 复制代码
# 原 schema
class ExtractedData(BaseModel):
    user_name: str
    score: float

# 安全变更:新增 optional field
class ExtractedData(BaseModel):
    user_name: str
    score: float
    confidence_level: Optional[str] = None  # 新增,旧 consumer 忽略

放宽类型约束 :将 int 改成 Union[int, float],旧 consumer parse 时不会出错。

新增枚举值 :向枚举类型加入新的合法值,但需要注意:旧 consumer 如果做了穷举 match,会出现 unknown value 的处理问题,建议 consumer 侧加 default 分支兜底。

2.2 Breaking Change(破坏性变更,不能直接发布)

变更类型 例子 破坏方式
字段重命名 user_nameusername 旧 consumer 找不到 user_name
删除字段 移除 score 旧 consumer 解析时缺字段
类型收窄 floatint 旧数据 0.7 在新 parser 报错
required 字段新增 加入没有默认值的新字段 LLM 旧 prompt 不知道要生成它
枚举值删除 status 中移除 pending 旧 LLM 输出 pending,新 consumer 报错
嵌套结构变更 address.city 拍平成 city 旧 consumer 的访问路径全挂

关键判断法则

问自己:如果用旧 schema 训练出来的 prompt 生成的数据,被新 schema 的 Consumer 读取,会不会报错?如果会,就是 Breaking Change。

2.3 对照决策表

sql 复制代码
变更类型                  | 向后兼容? | 推荐方案
--------------------------|-----------|----------------------------------
新增 Optional 字段        | ✅        | 直接发布
新增枚举值                | ✅*       | 发布 + 通知 consumer 加 default 分支
放宽类型 (int→float)      | ✅        | 直接发布
字段重命名                | ❌        | 双写 + 过渡期 + 废弃旧字段
删除字段                  | ❌        | Deprecate → 观察期 → 移除
类型收窄 (float→int)      | ❌        | 分版本过渡,中间态用 Union
新增 required 字段        | ❌        | 先改 prompt,再改 schema,同步部署
语义变更(字段含义变化)   | ❌        | 新命名 + 废弃旧字段

3. Schema 版本注册表:轻量级 Schema Registry

知道了变更分类,下一步是建立管理机制。我们没有引入 Confluent Schema Registry 这种重量级系统,而是用了一个极简的 Git-based 方案。

3.1 目录结构

bash 复制代码
schemas/
  llm/
    extracted_data/
      v1.py          # Pydantic model for v1
      v2.py          # Pydantic model for v2
      CHANGELOG.md   # 每次变更的记录
      compat.py      # 版本迁移逻辑
    sentiment/
      v1.py
  registry.json      # 版本注册表

3.2 registry.json

json 复制代码
{
  "schemas": {
    "extracted_data": {
      "current": "v2",
      "versions": {
        "v1": {
          "status": "deprecated",
          "deprecated_at": "2026-06-01",
          "remove_after": "2026-08-01",
          "file": "schemas/llm/extracted_data/v1.py"
        },
        "v2": {
          "status": "active",
          "released_at": "2026-06-01",
          "file": "schemas/llm/extracted_data/v2.py"
        }
      }
    }
  }
}

3.3 版本化 Pydantic Model

python 复制代码
# schemas/llm/extracted_data/v1.py
from pydantic import BaseModel
from typing import Optional

class ExtractedDataV1(BaseModel):
    """
    Schema v1: 初始版本 (2026-01)
    Status: DEPRECATED - 请迁移到 v2
    Remove after: 2026-08-01
    """
    user_name: str  # deprecated: use `username` in v2
    score: float    # sentiment score 0-1

# schemas/llm/extracted_data/v2.py
from pydantic import BaseModel, model_validator, Field
from typing import Optional
import warnings

class ExtractedDataV2(BaseModel):
    """
    Schema v2: 统一命名,增加置信度字段 (2026-06)
    Status: ACTIVE
    """
    username: str = Field(description="用户名(v1 中为 user_name)")
    score: float = Field(description="情感得分 0-1")
    confidence_level: Optional[str] = Field(
        default=None,
        description="置信度等级: high/medium/low"
    )
    # 向后兼容字段,接受 v1 的 user_name
    user_name: Optional[str] = Field(
        default=None,
        deprecated=True,
        description="[DEPRECATED] 请使用 username 字段"
    )

    @model_validator(mode='before')
    @classmethod
    def migrate_from_v1(cls, v):
        """向后兼容迁移:v1 的 user_name 映射到 username"""
        if isinstance(v, dict):
            if 'user_name' in v and 'username' not in v:
                v['username'] = v['user_name']
                warnings.warn(
                    "Received v1 schema field 'user_name', "
                    "please migrate producer to v2",
                    DeprecationWarning,
                    stacklevel=2
                )
        return v

3.4 CI 兼容性检查

在 CI pipeline 里加一个 schema 兼容性检查脚本,每次改动 schema 文件时自动运行:

python 复制代码
# scripts/check_schema_compat.py
"""
检查新版 schema 是否向后兼容旧版。
规则:
1. 不允许删除 required 字段
2. 不允许改变已有字段的类型(除非是放宽)
3. 不允许在不设置默认值的情况下新增 required 字段
"""

import json
import sys
from pathlib import Path

def check_breaking_changes(old_schema: dict, new_schema: dict) -> list[str]:
    breaking = []
    old_props = old_schema.get("properties", {})
    old_required = set(old_schema.get("required", []))
    new_props = new_schema.get("properties", {})
    new_required = set(new_schema.get("required", []))

    # 检查 required 字段是否被删除
    for field in old_required:
        if field not in new_props:
            breaking.append(f"BREAKING: required field '{field}' was removed")

    # 检查新增的 required 字段(无默认值)
    for field in new_required - old_required:
        if field not in old_props:
            breaking.append(
                f"BREAKING: new required field '{field}' has no default, "
                "old producers can't generate it"
            )

    return breaking

if __name__ == "__main__":
    old_file, new_file = sys.argv[1], sys.argv[2]
    old_schema = json.loads(Path(old_file).read_text())
    new_schema = json.loads(Path(new_file).read_text())
    issues = check_breaking_changes(old_schema, new_schema)
    if issues:
        print("Schema compatibility check FAILED:")
        for issue in issues:
            print(f"  {issue}")
        sys.exit(1)
    print("Schema compatibility check PASSED")

4. Additive-Only + Deprecation:正确的演进策略

知道了什么是 breaking change,知道了如何建注册表,下面是核心演进策略。

4.1 核心原则:Additive-Only

从数据库迁移领域借鉴来的原则,对 LLM schema 同样成立:

永远只加,不改,不删。

想重命名 user_name?不要直接改,而是:

  1. 阶段一(加) :新增 username 字段(optional),LLM 开始生成 username,同时保留 user_name
  2. 阶段二(双写) :在 model_validator 里把 user_name 的值复制给 username,两个字段同时存在,Consumer 可以按自己的节奏迁移
  3. 阶段三(废弃) :等所有 Consumer 都迁移到 username 后,标记 user_namedeprecated,生产 warning
  4. 阶段四(移除) :观察期结束(建议 2-3 个 sprint),移除 user_name

4.2 字段生命周期状态机

python 复制代码
from enum import Enum
from dataclasses import dataclass
from datetime import date

class FieldStatus(Enum):
    PROPOSED = "proposed"     # 提案中,未发布
    ACTIVE = "active"         # 活跃使用
    DEPRECATED = "deprecated" # 废弃,仍可用但不推荐
    REMOVED = "removed"       # 已移除

@dataclass
class FieldRecord:
    name: str
    status: FieldStatus
    added_in: str      # schema version
    deprecated_in: str | None = None
    removed_in: str | None = None
    replacement: str | None = None  # 如果有替代字段
    notes: str = ""

# 示例:字段生命周期记录
FIELD_AUDIT_LOG = [
    FieldRecord(
        name="user_name",
        status=FieldStatus.DEPRECATED,
        added_in="v1.0",
        deprecated_in="v2.0",
        replacement="username",
        notes="命名统一化重构,迁移到 username"
    ),
    FieldRecord(
        name="username",
        status=FieldStatus.ACTIVE,
        added_in="v2.0",
    ),
    FieldRecord(
        name="confidence_level",
        status=FieldStatus.ACTIVE,
        added_in="v2.1",
        notes="新增置信度等级字段,optional"
    ),
]

4.3 Deprecation 警告实现

python 复制代码
from pydantic import BaseModel, Field, model_validator
import warnings
import logging

logger = logging.getLogger(__name__)

class ExtractedDataV2(BaseModel):
    username: str
    score: float
    confidence_level: str | None = None
    
    # deprecated field,带使用追踪
    user_name: str | None = Field(default=None, exclude=True)

    @model_validator(mode='before')
    @classmethod
    def handle_deprecated_fields(cls, data):
        if isinstance(data, dict) and 'user_name' in data and data['user_name'] is not None:
            # 记录 deprecated field 使用情况(用于监控)
            logger.warning(
                "deprecated_field_used",
                extra={
                    "field": "user_name",
                    "replacement": "username",
                    "source": "llm_output"  # 区分来源
                }
            )
            if 'username' not in data or not data['username']:
                data['username'] = data['user_name']
        return data

通过 logger.warning 带结构化 extra 字段,可以在 Grafana 或任何日志系统里做 deprecated field 使用率监控,知道什么时候"安全"删除。


5. 双版本并行:feature flag + schema router

A/B 测试新模型、或者上线新 schema 时,可能同时有两个 schema 版本在跑。这时需要一个 Schema Router。

5.1 在响应元数据中带 schema_version

核心思路:LLM 调用层在生成输出时,把 schema_version 一并写入,Consumer 根据版本分流。

python 复制代码
import time
from typing import Literal
from pydantic import BaseModel

class LLMResponseEnvelope(BaseModel):
    """包装 LLM structured output,附加元数据"""
    schema_name: str
    schema_version: Literal["v1", "v2"]
    model_id: str
    generated_at: float
    data: dict  # 实际 payload

def call_llm_with_schema(
    prompt: str,
    schema_version: str = "v2"
) -> LLMResponseEnvelope:
    schema_map = {
        "v1": ExtractedDataV1,
        "v2": ExtractedDataV2,
    }
    schema_class = schema_map[schema_version]
    
    # 调用 LLM(以 instructor + DeepSeek 为例)
    import instructor
    from openai import OpenAI
    
    client = instructor.from_openai(OpenAI(base_url='https://api.deepseek.com/v1', api_key='your-api-key'))
    result = client.chat.completions.create(
        model="deepseek-chat",
        response_model=schema_class,
        messages=[{"role": "user", "content": prompt}]
    )
    
    return LLMResponseEnvelope(
        schema_name="extracted_data",
        schema_version=schema_version,
        model_id="deepseek-chat",
        generated_at=time.time(),
        data=result.model_dump()
    )

5.2 Consumer 侧的 Schema Router

python 复制代码
from pydantic import BaseModel
from typing import Union

SCHEMA_REGISTRY = {
    "extracted_data": {
        "v1": ExtractedDataV1,
        "v2": ExtractedDataV2,
    }
}

def parse_llm_response(
    envelope: LLMResponseEnvelope
) -> Union[ExtractedDataV1, ExtractedDataV2]:
    """根据 schema_version 路由到正确的解析器"""
    schema_versions = SCHEMA_REGISTRY.get(envelope.schema_name, {})
    schema_class = schema_versions.get(envelope.schema_version)
    
    if not schema_class:
        raise ValueError(
            f"Unknown schema: {envelope.schema_name}@{envelope.schema_version}. "
            f"Available: {list(schema_versions.keys())}"
        )
    
    return schema_class(**envelope.data)

# 消费方示例:统一处理新旧版本
def process_result(envelope: LLMResponseEnvelope):
    result = parse_llm_response(envelope)
    
    # 统一接口:无论 v1 还是 v2,都能获取 username
    if hasattr(result, 'username'):
        user_identifier = result.username
    elif hasattr(result, 'user_name'):
        user_identifier = result.user_name  # v1 fallback
    else:
        raise ValueError("Cannot extract user identifier from result")
    
    return {"user": user_identifier, "score": result.score}

5.3 用 feature flag 控制 schema 版本

python 复制代码
import os
from functools import lru_cache

@lru_cache(maxsize=1)
def get_active_schema_version(schema_name: str) -> str:
    """
    从 feature flag 服务(或环境变量)获取当前激活的 schema 版本。
    生产环境用 LaunchDarkly / flagsmith 等;简单场景用环境变量。
    """
    env_key = f"SCHEMA_VERSION_{schema_name.upper()}"
    return os.getenv(env_key, "v2")  # 默认 v2

# 调用时动态获取版本
def extract_info(text: str) -> LLMResponseEnvelope:
    version = get_active_schema_version("extracted_data")
    return call_llm_with_schema(text, schema_version=version)

6. Schema Drift 检测:当模型悄悄变了

这是很多团队忽略的问题:schema 没变,但模型输出行为变了

6.1 什么是 Schema Drift

我们在 2026 年 3 月经历过一次经典的 schema drift:我们迁移到了一个新版本的模型,schema 完全没动,但三天后数仓的同事发现,confidence_level 字段的 null 率从原来的 3% 跳到了 27%。

原因:新模型对这个 optional 字段更"保守",在不确定时倾向于不填,而旧模型会猜一个值。Schema 没变,但语义上的行为漂移了。

6.2 Field Presence Rate 监控

核心指标:字段存在率 (Field Presence Rate)和 Null 率(Null Rate)。

python 复制代码
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Any
import json

@dataclass
class SchemaMetrics:
    """追踪 schema 字段的统计指标"""
    total_calls: int = 0
    field_presence: dict[str, int] = field(default_factory=lambda: defaultdict(int))
    field_null: dict[str, int] = field(default_factory=lambda: defaultdict(int))
    enum_distribution: dict[str, dict[str, int]] = field(
        default_factory=lambda: defaultdict(lambda: defaultdict(int))
    )

    def record(self, output: dict):
        self.total_calls += 1
        for key, value in output.items():
            self.field_presence[key] += 1
            if value is None:
                self.field_null[key] += 1
            elif isinstance(value, str):
                self.enum_distribution[key][value] += 1

    def presence_rate(self, field_name: str) -> float:
        if self.total_calls == 0:
            return 0.0
        return self.field_presence[field_name] / self.total_calls

    def null_rate(self, field_name: str) -> float:
        presence = self.field_presence.get(field_name, 0)
        if presence == 0:
            return 0.0
        return self.field_null.get(field_name, 0) / presence

    def report(self) -> dict:
        return {
            "total_calls": self.total_calls,
            "field_stats": {
                field: {
                    "presence_rate": self.presence_rate(field),
                    "null_rate": self.null_rate(field),
                }
                for field in self.field_presence
            }
        }

# 使用示例
metrics = SchemaMetrics()

def monitored_extract(text: str) -> dict:
    result = extract_info(text)
    metrics.record(result.data)
    return result.data

# 每小时聚合告警检查
def check_drift_alerts(metrics: SchemaMetrics, thresholds: dict):
    report = metrics.report()
    alerts = []
    for field_name, stats in report["field_stats"].items():
        threshold = thresholds.get(field_name, {})
        if "max_null_rate" in threshold:
            if stats["null_rate"] > threshold["max_null_rate"]:
                alerts.append({
                    "field": field_name,
                    "type": "null_rate_spike",
                    "current": stats["null_rate"],
                    "threshold": threshold["max_null_rate"]
                })
        if "min_presence_rate" in threshold:
            if stats["presence_rate"] < threshold["min_presence_rate"]:
                alerts.append({
                    "field": field_name,
                    "type": "presence_rate_drop",
                    "current": stats["presence_rate"],
                    "threshold": threshold["min_presence_rate"]
                })
    return alerts

# 配置阈值
DRIFT_THRESHOLDS = {
    "confidence_level": {"max_null_rate": 0.10},  # 允许最高 10% null
    "username": {"min_presence_rate": 0.99},       # 必须出现率 ≥ 99%
    "score": {"min_presence_rate": 1.0},           # required 字段必须 100%
}

6.3 何时跑 Drift 检测

  • 换模型时:立即启动 1000 次影子流量,对比新旧 field presence 分布
  • 改 prompt 时:即使 schema 没变,也要跑 100-500 次采样对比
  • 模型提供商无声更新后:定期(如每周)抽样 1000 次请求做基线校验

实践建议:把 drift 检测做成 CI gate。每次模型迁移,跑自动化脚本对比两批 1000 次输出的 field stats,偏差超阈值则 CI 失败,强制人工 review。


7. 历史数据迁移:三种方案的工程权衡

schema 演进还有一个绕不过去的问题:存在数据库里的旧 schema 数据怎么办?

7.1 三种策略

方案 A:Lazy Migration(读时迁移)

python 复制代码
def read_extracted_data(record_id: str) -> ExtractedDataV2:
    raw = db.get(record_id)
    data = json.loads(raw["payload"])
    
    # 根据存储的 schema_version 决定如何解析
    version = raw.get("schema_version", "v1")  # 旧数据没有 version 字段默认 v1
    
    if version == "v1":
        v1 = ExtractedDataV1(**data)
        # 迁移到 v2
        return ExtractedDataV2(
            username=v1.user_name,
            score=v1.score,
        )
    elif version == "v2":
        return ExtractedDataV2(**data)
    else:
        raise ValueError(f"Unknown schema version: {version}")

优点:零停机,零批处理成本;缺点:迁移代码永远留在读路径,覆盖率取决于数据访问频率,冷数据永远没被迁移。

方案 B:Background Migration Job(批量异步迁移)

python 复制代码
# 适合数据量 < 千万级的场景
import asyncio
from datetime import datetime

async def migrate_batch(
    schema_name: str,
    from_version: str,
    to_version: str,
    batch_size: int = 1000
):
    offset = 0
    total_migrated = 0
    errors = []
    
    while True:
        records = await db.fetch_by_schema_version(
            schema_name=schema_name,
            version=from_version,
            limit=batch_size,
            offset=offset
        )
        if not records:
            break
        
        for record in records:
            try:
                migrated = migrate_record(record, from_version, to_version)
                await db.update(
                    record["id"],
                    payload=migrated.model_dump_json(),
                    schema_version=to_version,
                    migrated_at=datetime.utcnow().isoformat()
                )
                total_migrated += 1
            except Exception as e:
                errors.append({"id": record["id"], "error": str(e)})
        
        offset += batch_size
        await asyncio.sleep(0.1)  # 控制速率,避免打满 DB
        print(f"Migrated {total_migrated} records, {len(errors)} errors")
    
    return {"total": total_migrated, "errors": errors}

优点:覆盖率 100%,迁移后读路径简单;缺点:需要运维窗口,有一段时间新旧数据混存。

方案 C:Dual-Schema Read(双模式解析)

python 复制代码
def read_with_fallback(record_id: str) -> ExtractedDataV2:
    raw = json.loads(db.get(record_id)["payload"])
    
    # 先尝试 v2,失败则降级到 v1 + 迁移
    try:
        return ExtractedDataV2(**raw)
    except Exception:
        try:
            v1 = ExtractedDataV1(**raw)
            return ExtractedDataV2(username=v1.user_name, score=v1.score)
        except Exception as e:
            raise ValueError(f"Cannot parse record {record_id}: {e}")

优点:实现极简,无需维护 schema_version 字段;缺点:try/except 成本高,schema 版本多时难以维护。

7.2 三种方案对比

维度 Lazy Migration Background Job Dual-Schema Read
停机需求
覆盖率 低(热数据优先) 高(100%)
读路径复杂度
实现成本
推荐场景 数据体量大,大量冷数据 数据体量中等,有运维窗口 版本少(≤2),过渡期短

我们的选择:先上 Lazy Migration 快速止血,用 Background Job 完成 95%+ 数据的正式迁移,Dual-Schema Read 只在过渡期的最后两周用于收尾。


8. 端到端案例:从事故到体系

回到开篇的事故。如果我们当时有这套体系,流程会完全不同:

改名前

  1. 在 Schema Registry 里创建 v2 版本,新增 optional username 字段
  2. CI 兼容性检查:user_name 被保留,username 是 optional ✅ PASS
  3. 在 LLM prompt 里同时生成 user_nameusername(双写阶段)

上线后

  1. Feature flag 控制:先让 5% 流量走 v2 schema,观察 field presence metrics
  2. 各 Consumer 团队在 Sprint 内迁移到读 username
  3. deprecated field 使用率监控归零后,标记 user_name 为 DEPRECATED

两个 Sprint 后

  1. 所有 Consumer 已迁移,deprecated field 告警静默 14 天
  2. 在 v3 中移除 user_name,CI 检查 user_name 从 required 列表移除(它一直是 optional)✅ PASS
  3. 安全发布

整个过程零停机,下游无感,没有任何凌晨两点的告警。


9. 总结:Schema 演进检查清单

每次修改 LLM structured output schema 前,过一遍这张清单:

变更前

  • 这是 Breaking Change 吗?(对照第 2 节的分类表)
  • 旧 Consumer 都知道这次变更吗?
  • CI 兼容性检查脚本通过了吗?
  • Schema Registry 里的版本号更新了吗?

发布策略

  • 使用 Additive-Only 而不是直接修改/删除字段
  • 如果是字段重命名:双写阶段 ≥ 1 Sprint
  • feature flag 控制上线流量,从 5% 开始

监控

  • field presence rate 基线已建立
  • drift 告警阈值已配置
  • deprecated field 使用率监控已上线

收尾

  • 所有 Consumer 已迁移到新字段
  • deprecated field 使用率归零 ≥ 14 天
  • 旧版本有 remove_after 日期,到期即移除

5 条核心原则

  1. Schema 是数据契约,不是实现细节------把它当 API 一样版本化管理
  2. Additive-Only------永远只加,不改,不删;需要改就加新字段 + 废弃旧字段
  3. Schema Registry 最小可用集------Git + JSON 注册表 + CI 检查脚本,不需要上重量级系统
  4. 带版本的输出信封 ------response envelope 里带 schema_version,Consumer 按版本分流
  5. Schema Drift 是独立监控维度------换模型就跑 field presence 对比,不要等下游投诉

LLM 应用工程化的本质,是把不确定性压缩到可控的边界内。Schema 演进是这条路上经常被低估的一环。建立版本化体系不是过度工程,而是在生产流量达到一定规模后,迟早要还的债。早建早受益。

相关推荐
法海爱捉虫1 小时前
小微企业 / 货代专用快递打单工具,适配热敏 / A4 打印机 功能设计
python
写代码的王同学1 小时前
IM 跨节点消息转发架构对比分析
后端
长栎1 小时前
多数据源那套代码你写了三遍——你其实一直在手动实现桥接模式
后端
长栎1 小时前
Integer 缓存 -128~127 的坑——JVM 享元模式在生产环境埋的雷
后端
tonydf1 小时前
DotNet项目接入Copilot SDK简单案例
后端·.net·github copilot
asdzx671 小时前
Python 优雅解析 Excel:从原生行列到强类型对象的三层数据结构演进
数据结构·python·excel
何以解忧,唯有..1 小时前
Go 语言运算符详解:从基础到实战
开发语言·后端·golang
触底反弹1 小时前
面试官问"Ajax原理",我从XHR讲到async/await,他直接懵了!
前端·面试·架构