在 AI 应用进入生产环境的今天,一个经常被忽略的问题开始变得刺眼:AI 服务的体验,会随着时间推移悄悄变差。内容平台曾经出现的“先免费、后涨价、再压榨”的退化过程,也正在部分模型 API、云服务和应用工具身上重演。这个概念有一个专门的名字:Enshittification,即“垃圾化”。它由科技作家 Cory Doctorow 提出,原本用来解释平台经济中的系统性衰退,但用它来观察 AI 生态同样精准。这篇文章不会停留在概念层面,而是从开发者视角给出可执行的防垃圾化策略:如何识别 AI 服务退化信号、如何用自动化脚本体检 API、如何通过多供应商接入降低锁定风险,以及出现问题后如何排错。你可以在任何依赖大模型 API 的项目中用这套思路建立质量防线,避免业务被上游服务的隐性变化拖垮。
1. 先弄清楚 Enshittification 在 AI 语境下指什么
1.1 从平台理论到 AI 服务:一个概念的解释
Cory Doctorow 在描述互联网平台兴衰时提出 “enshittification” 这个词。它的大意是:平台早期为了让用户和供应商愿意加入,会提供超额价值、相对低廉的费用和宽松的规则。一旦形成网络效应,平台的控制力增强,就开始逐步提高抽成、压缩服务质量、限制数据可迁移性,最终把早期积累的价值全部转化为平台自身的利润。用户和供应商都被锁定在平台上,迁移成本越高,平台就越有恃无恐。
把这个逻辑迁移到 AI 生态中,可以看到非常相似的模式。一个 AI 模型厂商在刚开放 API 时,往往给出慷慨的免费额度、稳定的模型质量和清晰的文档。开发者投入大量时间完成集成,业务也依赖这个模型运行。随着用户量增长,厂商可能会调整定价模型、降低速率上限、修改返回格式、屏蔽某些请求,甚至悄然把高成本模型替换成低成本模型。对开发者来说,每一次变化都可能是隐性的:只要接口没有抛错,业务就能继续跑,但答案质量、响应速度、成本结构都已经发生了变化。
这里的关键不是“厂商是恶意的”,而是商业系统天然有这种激励。因此,开发者要把“平台退化”视为一种正常风险,而不是意外事故。技术方案上要做的是:在架构上预留退路,用可量化的指标感知变化,在异常发生时能快速切换到其他供应商。
1.2 AI 产品为什么更容易陷入“垃圾化”周期
AI 产品比传统 SaaS 更容易陷入这个周期,原因主要是以下几点。
第一,模型质量是隐蔽的。传统软件如果偷偷删除某个功能,用户很快会发现。但大模型 API 的响应文本每次都会变化,同样的提示词昨天和今天的答案可能完全不同。如果厂商在后台悄悄换了一个更小的模型,普通调用方很难察觉,只有持续跑测评集的人才会发现指标波动。
第二,成本结构不透明。AI 服务的成本主要按 token 计价,除了单价比,输出长度、输入长度、缓存命中率、重试次数都会影响总费用。厂商只要微调默认参数、增加系统提示词、延长输出限制,就能在不改价目表的情况下提高单次调用消耗。
第三,迁移成本高。一旦你的业务逻辑、提示词、后处理代码都与某个厂商的返回结构深度绑定,切换供应商就不是改一行 API 地址的事。模型能力差异、字段差异、延迟差异、价格差异都会成为阻力。
第四,生态锁定强烈。部分厂商通过专用 SDK、函数调用格式、微调平台、向量存储接口把用户留在自有体系里。你用的越深,抽身的难度越大。
所以,防“垃圾化”不是对某个供应商的道德指责,而是一种工程纪律。它要求你从一开始就把模型接入视为可替换组件,而不是永久基础设施。
2. 用工程指标识别 AI 服务的退化信号
2.1 识别信号:价格、质量、锁定与数据控制
要判断一个 AI 服务是否在“垃圾化”,不能靠感觉,要靠指标。下面四类信号值得重点观察。
价格信号:单位 token 成本是否上涨,免费额度是否被取消,是否有更高的最低消费,是否开始对长上下文额外收费。不要只看总账单,要按“每百万 token 的实际成本”来计算,因为输出长度变化可能让总账单在单价不变时仍然暴涨。
质量信号:同一组测试问题上的回答准确率、完整性、稳定性是否下降。输出是否变得更平淡、更模板化,是否正确处理格式要求,是否频繁拒绝应该能回答的问题,出现“我无法帮助”的比例是否上升。模型返回的 JSON 是否开始出现字段缺失或类型错误。
锁定信号:文档是否开始把简单功能复杂化,是否要求你必须使用专用 SDK 或特定存储方案,是否禁止导出对话记录和微调数据,是否对批量导出设置极低速率。
数据控制信号:是否把用户数据用于训练却没有明确告知,是否强制开启数据共享选项,是否在条款中增加对知识产权的单方面主张。
这些信号不是单次观察能确认的,需要建立长期记录。推荐的做法是:把每次调用的模型名、提示词版本、输出长度、耗时、状态码、费用估算写入结构化日志,形成时间序列数据。
2.2 用自动化检查脚本给 AI 服务做体检
下面这个 Python 脚本演示如何对任意 OpenAI 兼容接口做一次轻量体检。它不做复杂模型评估,而是记录每个请求的延迟、状态码、输出长度和费用估算,并检查返回是否为合法 JSON。脚本的思路可以运用于日常巡检。
import os import time import json import httpx API_URL = os.getenv("AI_API_URL", "http://localhost:11434/v1/chat/completions") API_KEY = os.getenv("AI_API_KEY", "local-test-key") MODEL = os.getenv("AI_MODEL", "qwen2.5:7b") BASE_QUESTIONS = [ "用一句话解释什么是 HTTP 状态码 429。", "给定 JSON 字段 name、age、email,请构造一个合法 JSON 对象。", "如果用户输入了负数,你应该怎么提示?", ] def estimate_price(prompt_tokens, completion_tokens, price_per_million_in, price_per_million_out): return (prompt_tokens * price_per_million_in + completion_tokens * price_per_million_out) / 1_000_000 def call_once(client, item): payload = { "model": MODEL, "messages": [{"role": "user", "content": item}], "temperature": 0.2, "max_tokens": 256, } headers = {"Authorization": f"Bearer {API_KEY}"} started = time.perf_counter() resp = client.post(API_URL, json=payload, headers=headers, timeout=30) elapsed_ms = (time.perf_counter() - started) * 1000 data = resp.json() content = data["choices"][0]["message"]["content"] usage = data.get("usage", {}) prompt_tokens = usage.get("prompt_tokens", 0) completion_tokens = usage.get("completion_tokens", 0) valid_json = True if item.startswith("给定 JSON"): try: json.loads(content) except Exception: valid_json = False price = estimate_price( prompt_tokens, completion_tokens, price_per_million_in=0.5, price_per_million_out=1.5, ) return { "question": item[:20], "status_code": resp.status_code, "elapsed_ms": round(elapsed_ms, 2), "output_length": len(content), "valid_json": valid_json, "estimated_cost": round(price, 6), } def main(): with httpx.Client() as client: results = [call_once(client, q) for q in BASE_QUESTIONS] for r in results: print(json.dumps(r, ensure_ascii=False)) if __name__ == "__main__": main()脚本的关键点在于:它把“能跑通”和“质量符合预期”分开判断。valid_json只是第一个质量探针,实际项目中可以把问题换成你自己领域的基准测试集,并对每个答案做自动校验,例如包含关键词、满足格式约束、输出长度上限、拒绝率等。
把脚本放入定时任务后,你会得到一组基线数据。之后每次供应商发布新版本或你的成本异常上升,都可以重跑同一组问题,观察输出是否漂移。
2.3 信号汇总表与预警阈值
下面是一张可以贴在团队文档里的信号表,每一列都是一个可执行的观察维度。
| 信号 | 推荐指标 | 警戒线 | 确认方法 |
|---|---|---|---|
| 价格 | 每百万 token 实际成本 | 月度环比上涨超过 20% | 按发票除以 token 消耗量,不要只看总额 |
| 质量 | 基准集正确率 | 低于历史基线的 95% | 定时跑固定问题集,记录每个问题通过/失败 |
| 格式漂移 | 合法 JSON 比例 | 低于 99% | 在返回后立即做 schema 校验 |
| 拒绝率 | 拒绝/无法回答比例 | 比基线上升 30% | 统计“抱歉、无法”等语气标记,并人工抽检 |
| 延迟 | P95 耗时 | 连续 3 天高于基线 1.5 倍 | 按小时聚合调用耗时 |
| 定价透明度 | 文档变更通知 | 未提前通知就修改限制 | 订阅官方 changelog,同时关注账单 |
| 锁定 | 数据导出能力 | 无导出接口或导出限速严重 | 测试批量导出 1 万条历史调用 |
这些阈值只是参考,团队应该根据自身业务容忍度调整。更重要的是,一旦触发阈值,必须有人跟进,而不是只看一眼监控大屏。
3. 建立可移植的 AI 接入层,避免被单一平台锁死
3.1 为什么 OpenAI 兼容接口和开源模型是“退路”的基础
防止 AI 服务垃圾化的核心不是拒绝大厂,而是让自己随时有替代选项。目前最容易操作的替代路径有两条:第一,使用统一协议连接多个模型供应商;第二,在本地或自建服务器上部署开源模型。
OpenAI 兼容接口已经成为一个事实标准。大量模型服务商,包括使用 Ollama、vLLM、LM Studio 等方案部署的开源模型,都提供/v1/chat/completions风格的 HTTP 接口。只要你的业务代码不依赖某个供应商特有的字段,理论上可以只改一个 base URL 和 API Key 就切换到新的模型服务。
这不等于说所有项目都要立刻本地部署模型。本地模型在服从性、知识更新、多模态能力上可能不如商业大模型。但它的价值在于“兜底能力”:当商业 API 涨价、限流、下线或质量退化时,你的系统仍然能提供服务,只是质量可能下降。这种“降级可用”的架构,比直接停摆要可靠得多。
3.2 设计一个支持多供应商的模型调用封装
下面用一个简单的 Python 封装层说明多供应商接入的思路。它定义一个统一的chat_completion方法,内部根据配置选择不同的 endpoint。
import os import httpx from dataclasses import dataclass @dataclass class ModelProvider: name: str base_url: str api_key: str model: str enabled: bool = True class ModelGateway: """轻量模型网关,支持多个 OpenAI 兼容服务。""" def __init__(self, providers: list[ModelProvider]): self.providers = providers def _post(self, provider: ModelProvider, messages: list[dict], temperature: float = 0.2, max_tokens: int = 512): payload = { "model": provider.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, } headers = {"Authorization": f"Bearer {provider.api_key}"} with httpx.Client(timeout=30) as client: resp = client.post( f"{provider.base_url.rstrip('/')}/chat/completions", json=payload, headers=headers, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] def complete_with_fallback(self, messages: list[dict], fallback_order: list[str] | None = None): """按顺序调用,如果前一个 provider 失败则自动切换下一个。""" order = fallback_order or [p.name for p in self.providers if p.enabled] errors = [] for name in order: provider = next((p for p in self.providers if p.name == name), None) if provider is None or not provider.enabled: continue try: return self._post(provider, messages) except Exception as exc: errors.append(f"{provider.name}: {exc}") raise RuntimeError("所有模型服务均不可用。详情: " + " | ".join(errors)) def build_default_gateway(): providers = [ ModelProvider( name="primary", base_url=os.getenv("PRIMARY_BASE_URL", "https://untrusted.example.com/v1"), api_key=os.getenv("PRIMARY_API_KEY", ""), model=os.getenv("PRIMARY_MODEL", "commercial-model"), ), ModelProvider( name="fallback", base_url=os.getenv("FALLBACK_BASE_URL", "http://localhost:11434/v1"), api_key=os.getenv("FALLBACK_API_KEY", "local-key"), model=os.getenv("FALLBACK_MODEL", "qwen2.5:7b"), ), ] return ModelGateway(providers) gateway = build_default_gateway() result = gateway.complete_with_fallback( [{"role": "user", "content": "请用 JSON 输出三个项目风险等级"}], fallback_order=["primary", "fallback"], ) print(result)这段代码的作用不是替代完整的 API 网关,而是示范一个关键思想:业务代码只依赖complete_with_fallback,不直接接触任何厂商的 SDK。你可以在配置文件中增减供应商,而不需要改业务逻辑。
要特别注意,不要在该类封装里加入任何供应商私有参数,例如某个厂商特有的response_format、tool_choice风格字段。如果一定需要,要把这些参数放到 provider 的独立配置中,并在统一接口里做转换。
3.3 接入层的关键决定:超时、重试、回退与降级
接入层不能只做“调用接口”,还需要考虑四个工程问题。
超时:每个供应商都应设置独立的连接超时和读取超时。商业 API 的 P95 延迟如果超过你的业务容忍度,超时设置可以避免线程被拖死。一般建议连接超时 5 秒,读超时 30 秒,长任务另做异步处理。
重试:重试并不是越多越好。对于 429、502、503 这类临时错误,可以做指数退避重试,重试次数建议不超过 3 次。对于 4xx 业务错误,比如 400 参数错误,重试没有意义,应该直接抛异常。
回退:回退是“垃圾化”防线的关键。当主供应商连续失败或返回质量低于阈值时,自动切换到备用供应商。切换可以是全局的,也可以按用户比例灰度,避免两个服务同时发生未知问题。
降级:如果所有模型服务都不可用,业务仍然要有一个最差情况的处理路径。比如返回固定模板、进入人工处理队列,或者只渲染缓存结果。这段逻辑要在开发时就写好,不要等到故障时才临时补。
4. 持续监控 AI 服务健康度:成本、质量与格式漂移
4.1 监控哪些指标,如何埋点
在 AI 应用里,传统监控通常只关注错误率和延迟,这远远不够。我们还需要监控“语义质量”“格式稳定性”和“成本结构”。这些指标必须从应用内部埋点,因为云端供应商不会主动提供。
推荐在调用层做统一的中间件。每次调用模型时记录以下字段:
- 请求 ID:用于关联业务上下文。
- 时间戳:精确到毫秒。
- 模型名:供应商返回的实际模型名,有时配置文件写着 A,实际返回可能是 B。
- 提示词版本:如果你对提示词做版本管理,记录版本号,便于回归分析。
- 输入 token 数和输出 token 数:从 API 返回的 usage 中读取。
- 响应状态码、异常类型和异常信息。
- 总耗时。
- 返回内容长度。
- 格式校验结果:例如 JSON schema 是否通过,必填字段是否存在。
- 质量得分:在业务代码中计算,例如是否包含禁止项,是否命中关键词,或者是人工标注样本通过率。
这些数据可以写入日志文件、ClickHouse、Elasticsearch 或 Prometheus,具体技术栈可以根据团队情况选择。关键是字段的一致性。如果今天记这个字段,明天记那个字段,后面做回归分析会非常痛苦。
4.2 最小可用的监控脚本示例
下面用一个 Python 脚本从日志文件中聚合每日指标。假设日志是 JSON Lines 格式,每行是一条调用记录。
import json import argparse from collections import defaultdict def parse_line(line: str): try: obj = json.loads(line) except Exception: return None required = ["timestamp", "model", "latency_ms", "prompt_tokens", "completion_tokens", "ok"] if not all(k in obj for k in required): return None return obj def main(log_file: str): daily = defaultdict(list) with open(log_file, "r", encoding="utf-8") as f: for line in f: obj = parse_line(line) if obj: day = obj["timestamp"][:10] daily[day].append(obj) for day in sorted(daily): items = daily[day] total_cost = sum( (it["prompt_tokens"] * 0.5 + it["completion_tokens"] * 1.5) / 1_000_000 for it in items ) errors = [it for it in items if not it["ok"]] latency = sorted(it["latency_ms"] for it in items) p95 = latency[int(len(latency) * 0.95)] if latency else 0 avg_tokens = sum(it["completion_tokens"] for it in items) / len(items) if items else 0 print(f"{day}: calls={len(items)} errors={len(errors)} " f"p95={p95}ms avg_out_tokens={avg_tokens:.1f} cost={total_cost:.4f}") if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--log", required=True) args = parser.parse_args() main(args.log)这个脚本只展示了最基本的价格、错误率、延迟和输出长度聚合。实际使用中,你还可以增加“格式漂移比例”“拒绝率”等统计。统计结果可以接入告警:例如单日成本环比上升 30%,就触发评审。
4.3 从监控结果触发人工评审的触发条件
监控不是目的,触发行动才是。建议设定下面几条触发人工评审的条件:
- 日成本连续 3 天超过基线的 20%。
- 输出 token 平均数上涨 30% 以上。
- 基准集质量得分下降超过 5 个百分点。
- 同一模型名下的返回字段 schema 校验失败率超过 1%。
- 供应商连续两次在周末上线维护且没有提前通知。
一旦触发,需要拉上模型负责人、后端负责人一起查看日志样本,而不是简单调一调参数。重点检查:提示词是否被改写、模型版本是否变化、默认温度是否被覆盖、新增的系统消息是否在消耗 token。
5. 排错:AI 服务突然“不对劲”时的排查链路
5.1 现象与可能原因对照表
工程排错时,最怕的是看到“调用失败”或“效果变差”却不知道从哪一层查起。下面这张表可以快速定位方向。
| 现象 | 可能原因 | 检查方式 |
|---|---|---|
| 成本突然暴涨 | 输出变长、模型被换成高价版、重试次数变多 | 查看 token 消耗和重试日志,比较单次平均输出长度 |
| 返回内容明显变空 | 模型被换成小参数、服务端系统提示词限制 | 用固定基准集对比历史结果,检查实际 model 字段 |
| JSON 格式大量报错 | 服务端悄悄改变返回结构、后处理代码在模型切换后失效 | 记录 schema 校验失败率,保存原始响应 body |
| 延迟升高 | 供应商限流、服务端排队、网络链路变化 | 检查 P95 延迟、429 状态码、重试次数 |
| 拒绝回答比例上升 | 内容安全策略变严、服务端叠加系统提示词 | 抽验被拒绝样本,确认是否触发了新的安全策略 |
| 无法切换到备用模型 | 配置了供应商私有字段或返回结构不兼容 | 用 curl 分别调用两个接口,比较字段差异 |
这些现象往往不是孤立的。比如成本暴涨可能同时伴随输出变长和重试变多,需要看全链路日志。
5.2 通过基线数据集回归测试快速定位质量下降
当质量下降时,不要直接凭主观感受下结论。建议维护一份基线数据集,包含 20 到 100 个真实业务问题,每个问题有预期答案或校验规则。每次出现质量投诉时,用同一份数据集在当前模型上重新跑一遍,并与历史记录对比。
下面是基线数据集的一种结构示例:
[ { "id": 1, "prompt": "给出一段 Python 代码,读取 CSV 文件并打印前 5 行。", "checks": ["contains:import csv", "contains:print", "not_contains:Traceback"] }, { "id": 2, "prompt": "将用户输入的金额 1234.56 转为人民币大写格式。", "checks": ["json_valid", "contains:壹仟贰佰叁拾肆元"] }, { "id": 3, "prompt": "回答:HTTP 503 和 500 的区别。", "checks": ["contains:503", "contains:500", "contains:服务端"] } ]你可以为每个检查写一个简单的断言函数。如果通过率低于历史基线,就可以确认“模型行为发生了实质变化”,而不是偶然波动。
要特别注意,基线数据集必须冻结。如果你频繁修改问题或预期答案,对比就失去了意义。数据集最好放在 Git 仓库中,每次变更都走 review。
5.3 处理“平台悄悄切换模型版本”的问题
模型厂商有时不会在 changelog 里明确说明“从今天起默认模型从 v1 升级到 v2”,但响应行为会明显变化。此时你需要尽早发现。
最常用的方式是:在每次调用的日志中记录model字段,并和配置期望值比对。如果返回的模型名与配置不一致,立即告警。另一种方式是定期调用提供商提供的“列出模型”接口,比较模型列表变动。
如果确认模型版本已经变化,并且业务无法接受,就应触发回退。回退可以是修改配置中的模型名,也可以是切换到备用供应商。在无法判断新版本是否更优时,可以先用灰度策略:让 5% 流量使用新版本,与旧版本结果做人工抽评,再决定是否扩大。
6. 防止 AI 项目垃圾化的工程实践清单
6.1 决策阶段:优先选择可替换的资源
在项目早期选型时,就应该把“可替换性”列为评估项。不要只看模型效果排行榜,还要看接口是否标准化、是否有数据导出机制、是否可以本地部署近似效果的开源模型、是否有合同退出条款。
推荐用下面几个问题做决策:
- 如果这个 API 明天涨价 5 倍,我的业务能承受吗?
- 如果这个 API 明天关闭,我需要多长时间迁移?
- 我的业务数据是否会留在第三方平台,能不能带走?
- 是否有一个本地模型能覆盖 80% 的核心场景?
如果你的答案是不能承受、迁移时间长、数据带不走、没有替代模型,那么即使在效果上它有优势,也要谨慎选择。
6.2 开发阶段:抽象、缓存、成本约束
开发阶段要把抽象层、缓存和成本约束一起做进去。抽象层解决切换问题,缓存解决成本和稳定性问题,成本约束解决账单失控问题。
缓存可以放在两个层级。第一层是语义缓存:如果用户问题和历史问题相似度很高,直接返回历史答案;第二层是结果缓存:对确定性高的任务,比如格式化输出,设置较短时间的缓存。这样能显著减少调用量,也降低对供应商的依赖。
成本约束要有硬性上限。可以在 API 网关层配置每日调用预算,超过预算后自动拒绝非核心请求,并通知管理员。不要等到月底账单出来才发现问题。
6.3 运营阶段:定期重新评估供应商和模型
防垃圾化是一个持续过程,不能只在选型时做一次。建议每个季度做一次模型供应商评估,内容包括:单价变化、质量回归结果、API 稳定性、数据政策变化、社区口碑。
如果条件允许,最好在团队内保留一个“基准模型脚本仓库”,包含数据集、评测脚本和报告模板。每次评估新供应商或新模型时,都生成一份可对比的报告,而不是凭印象投票。
6.4 可复用清单
下面这张最终清单可以直接用于团队评审,也可以贴在项目 wiki 中。
- [ ] 业务代码是否通过统一接口调用模型,而不是直接调用 SDK?
- [ ] 是否至少配置了一个备用供应商,并验证过回退路径?
- [ ] 是否记录每次调用的模型名、token、耗时、状态码和返回结果校验结果?
- [ ] 是否有一份冻结的基线数据集,并定期运行回归测试?
- [ ] 是否监控每百万 token 实际成本,并设置成本阈值告警?
- [ ] 是否有导出历史调用记录和中间数据的能力?
- [ ] 是否在不依赖某个厂商私有功能的前提下实现核心业务?
- [ ] 是否有一个本地开源模型作为最差情况下的兜底方案?
- [ ] 是否在 CI/CD 中运行格式校验和基础质量测试?
如果以上问题有一半以上是否,那么 AI 项目就存在明显的锁定和退化风险。趁依赖还不深,尽早补上防垃圾化设计。真正可靠的 AI 工程,不仅要让模型“跑起来”,还要让系统在外部环境恶化时仍然能给你留出转身的余地。