
过去大半年我一直在做 AI 应用的 Harness Engineering 改造最强烈的体感是AI 交付的复杂度不在模型选型而在怎么让一个天生不确定的东西稳定地跑在一条业务必须确定的生产链路里。每次发版最让人心里没底的永远是同一个问题——模型这次会不会突然输出一堆不该输出的内容。传统测试在这种场景下基本失效因为你要验证的不是一段确定的代码逻辑而是一团概率分布。后来我把 Fitness Function 作为 AI 交付的防腐层才慢慢把失控感压下去。这篇文章就把这段实践完整拆给你看。我会从失控的根源说起再解释 Fitness Function 为什么天然适合当防腐层接着给一套三层可落地的设计最后讲讲我踩过的坑和现在团队在走的方向。如果你也在做 AI Agent、AI 客服、或者任何把大模型接进生产系统的项目这篇应该能帮你少走几个月的弯路。1. AI 交付的失控感从哪里来为什么传统测试撑不住模型行为1.1 传统质量体系在模型行为面前的三处断层我印象最深的一次事故不是代码出 bug而是模型供应商在后台悄悄升级了底座版本。升级前模型在回答“请用 JSON 返回订单状态”时还能稳定输出纯 JSON升级后它会在 JSON 前后各加一段“好的我已经帮你查询了……”这样的自然语言。结果当天晚上所有依赖 JSON 解析的模块全部报错前端直接白屏。这件事让我意识到AI 交付里的“质量”和传统软件完全不是一个含义。传统质量体系有三个前提到了模型场景全都断了。第一是行为确定性。传统代码输入既定输出可预测大模型是条件概率抽样同一个问题两次回答可能不同甚至温度参数没变也偶发抖动。第二是环境可控性。传统依赖可以锁版本锁了就是锁了模型的行为由训练数据、权重、服务端配置共同决定供应商升级后你根本不知道什么时候悄悄变了。第三是契约一致性。传统接口有 schema字段类型在编译期就固定大模型输出格式千变万化你以为它是 JSON它给你一段 Markdown你以为它是字符串它给你一个对象。这三个断层叠加起来意味着“测试通过”这件事在模型场景里变成一种瞬时状态。你周一测试全绿周三可能就全红中间没有人改过一行代码。1.2 防腐层思路的迁移从 DDD 到 Harness Engineering第一次遇到这类问题时团队的反应是加一层“输出清洗”把模型输出里的多余文字删掉再尝试 JSON.parse。这种办法能解决表面问题但解决不了腐蚀问题——因为模型输出的不可控因素会不断绕过你的清洗规则今天多一句道歉明天多一个表情符号后天直接给你一段嵌套 Markdown。DDD 里的防腐层概念就是在这种地方产生价值的。防腐层的原本目标是在两个有不同概念模型的系统之间建一道边界防止外部系统的概念“污染”内部核心域。在 AI 交付里模型就是这个边界另一侧的外部系统。它的“概念模型”和你的业务模型天然不对齐它不知道你的订单状态枚举值有哪些它分不清“退货申请”和“退款申请”在产品语义上的差别它对时间格式的认知是自由发挥。Harness Engineering 的工程主张恰恰就是把这种边界治理变成一项系统工程实践——构建可控 AI 智能体不是靠祈祷模型变乖而是靠一套持续的约束、验证和反馈机制。Fitness Function 就是这个机制里最关键的执行器。从这时候开始我不再把模型当一个“接口”来测而是当一个“外部系统”来防。2. Fitness Function 为什么能承担防腐层职责2.1 从架构适应度函数到 AI 行为适应度如果你熟悉《构建进化架构》这本书一定对“架构适应度函数”不陌生。Neal Ford 他们给出的定义是对架构特征进行客观、可验证、可度量的一组测试。过去我在微服务治理里用它做响应时间、可用性、服务耦合度的持续验证那时它的角色更像“架构健康体检”。到了 AI 交付场景这个工具的定位变得更重要了。因为模型的行为不是静态的它会随着版本、上下文、Prompt 风格、数据分布的变化而变化。系统是否还健康不能靠“代码没变就是健康”来判断只能靠持续的测量来逼近真值。适应度函数正好提供一个框架定义什么是对的、怎么量化偏离、偏离到什么程度算失败。我用一句话向新同学解释普通测试在回答“这次对不对”适应度函数在回答“这个状态还能不能继续用”。这两个东西不是一回事。普通测试跑完绿灯了就交付适应度函数跑出的结果是一个“健康度”它不是一次性的而是需要持续跟踪的曲线。维度传统单元/集成测试Fitness Function判定对象某个函数或接口逻辑系统级行为与架构约束判定方式精确结果比对阈值、范围、一致性、漂移度执行时机开发期与 CICI 运行期持续监控失败含义功能缺陷行为脱离可接受区间变更敏感性逻辑变更时才需要改模型、数据、配置变化都可能触发维护方式随代码演进随业务规则和模型能力共同演进2.2 防腐层不是一层过滤网三个关键特性很多团队会把“输出清洗”当成防腐层其实这是最常见的误解。清洗函数、转换函数是防腐层的一个组成部分但防腐层至少要有三个特性。第一是持续性。防腐层不是一个函数调一次就好了它要覆盖每一次请求不仅覆盖在线请求还要定期用测试集回放检测模型行为有没有悄悄漂移。过滤层是事件驱动的问题来了才处理防腐层是状态驱动的它时刻告诉你系统当前处于什么状态。第二是验证性。防腐层不只是转换数据它要对转换前的输入、转换后的输出、以及整个链路的系统指标分别做判定。换句话说它必须回答“这个请求该不该放行”“这个输出能不能落到业务层”“系统整体有没有退步”这三个问题。第三是可演进性。防腐层不是一次写死的水坝而是一套可以调节的闸门。业务规则变了阈值要跟着变模型能力变强了有些约束可以放开模型出了问题约束就要收紧。过滤层往往越改越脆防腐层必须越改越稳。我常打一个比方过滤层是水管末端的净水器防腐层是整个水厂的水质监测、管道隔离和终端护栏的组合。净水器滤芯堵了你换一个就好但如果没有水质监测你根本不知道上游水源哪天出了问题。AI 交付里的“上游水源”就是不断变化的模型行为。3. 一套可落地的三层 Fitness Function 设计接下来是重点。我实践下来比较顺手的拆法是把 Fitness Function 分成三层输入域、输出域、系统域。对应着“进入到模型之前”“模型输出到业务之前”“越过业务边界之后的持续影响”。3.1 输入域在请求进入模型之前先设限输入域的目标不是教模型怎么做而是过滤掉那些不该进入模型的请求。很多 AI 系统的失控是从输入侧开始的用户一段超长文本把上下文塞满导致模型注意力分散一个恶意注入把系统提示词带偏批量调用时参数没有约束直接把费用打爆。我建议至少做四类检查。一是长度与预算检查记录每次请求的字符数、Token 数、预计费用超过预算直接拒绝或走降级通道。二是提示词注入检测识别“忽略之前的指令”“你现在是……”等经典注入句式检测到后拦截或脱敏。三是参数白名单如果业务只支持特定枚举值比如语言、地区、排序方式在请求期就校验不让模型去猜。四是敏感信息识别手机号、身份证、内部系统地址等进模型前就要打码否则模型可能把它们原样输出到其他人的上下文中。示例代码伪实现跑在 API 网关后的校验服务里class InputFitness: def __init__(self, max_tokens4096, cost_limit0.05): self.max_tokens max_tokens self.cost_limit cost_limit def evaluate(self, prompt: str, params: dict) - FitnessReport: checks [ self._length_check(prompt), self._injection_check(prompt), self._param_check(params), self._budget_check(params), ] return FitnessReport( passedall(c.status PASS for c in checks), checkschecks, scoresum(1 for c in checks if c.status PASS) / len(checks), )输入域里有些检查可以做成“软指标”比如提示词注入检测很难做到 100% 准确碰见疑似注入不要直接拒绝所有用户可以走“人工复核”或“降级为规则引擎回答”的通道。硬拒绝要留给那些有明确业务红线的场景比如费用超限。3.2 输出域把模型的回答挡在业务红线之外输出域是整个防腐层的核心。模型输出直接进业务这里出问题就是线上事故。我把它拆成四个检查层次每层责任不同。第一是结构层校验数据格式。要求模型输出 JSON就先做 JSON Schema 校验字段缺失、类型不对、枚举值非法一律拦截并触发重试或降级。第二是语义层校验内容是否落在业务合理区间。比如模型返回了门店营业时间“24:00”时区字段出现“Asia/Shanghai”之外的奇怪值或者把订单状态返回成不存在的枚举这些都是一眼就能判断的语义问题。第三是事实层校验关键事实与权威数据源是否一致。比如客服机器人提到的商品价格、库存状态、活动时间必须和业务数据库中的最新值对照。这一层最花功夫但也是防幻觉最关键的一层。第四是安全层校验内容是否涉及违禁、仇恨、误导性信息。这类检查通常会用到分类模型或关键词规则不同业务的安全阈值差异很大。这里有一个很现实的案例。我们的系统早期只在输出层做 JSON Schema 校验结果某次模型把订单金额从“98.50”写成了“9850”小数点没了。Schema 校验照样通过因为类型是 number但是下游账单直接给用户算错钱。后来我们在输出域加了一层事实层凡是出现过数据库事实的字段包括金额、库存、门店名称都要和业务服务里的实际值做比对不一致就拦截。示例代码def output_fitness(response: dict, business_ctx: dict) - FitnessReport: checks [] checks.append(schema_validator(response, order_schema)) checks.append(semantic_validator(response)) # 枚举、范围、格式 checks.append(fact_validator(response, business_ctx)) # 与业务数据比对 checks.append(safety_validator(response)) return FitnessReport( passedall(c.passed for c in checks), checkschecks, )输出域的失败响应策略值得说一下不是所有失败都要返回错误。对于轻微问题比如 JSON 里多了个逗号可以自动修复后重试一次对于严重问题比如事实不一致、内容风险必须直接拦截并走人工兜底。这里的核心原则是“在业务边界内处理好问题而不是把问题抛给用户”。3.3 系统域在看不见的地方持续监测收敛性输入域和输出域是“单请求级别”的防腐但 AI 交付还有一个更阴险的问题单个请求看起来都正常整体系统却在慢慢退化。比如模型输出的平均长度越来越多导致推理成本翻倍比如同一类问题开始返回更少的信息量比如某个槽位的填充率持续下降。这些单次请求发现不了必须靠聚合指标。系统域我主要关注四类指标。指标类别具体指标典型阈值仅举例失败后的动作性能P95 首 token 延迟、端到端耗时P95 延迟增长 20%告警、切备用模型成本单次对话平均 Token 消耗环比增长 15%告警、检查 Prompt 和工具链漂移输出 embedding 分布与基线余弦相似度相似度 0.92启动全面回归质量用户反馈率、重试率、拦截率重试率 8%人工复盘并调整阈值系统域不是实时判定那种“门禁”它的作用是做趋势分析。我们每周跑一次漂移检测把最近 7 天的模型输出向量化和上线时留下的基线对比。一旦相似度跌出阈值就开始排查是 Prompt 被改了上下文里出现了新类型的输入模型版本变了很多时候系统域的问题最后都能反推出输入域或输出域规则需要调整。3.4 把三层接入 CI/CD 与运行期门禁这三个域不是只做一个就完事而是需要连起来跑。在 CI 阶段我们维护了一个“回归样本集”包含几百条历史真实提问和对应期望输出。每次新建模型版本、调整 Prompt、修改工具调用逻辑都要跑一遍完整的三层测试套件。这个套件的意义不是“测试全部功能”而是保证模型行为没有发生非预期的漂移。在运行期每次在线请求会走“轻量级防腐”输入域全量检查输出域的结构层和安全层全量检查语义层和事实层按风险权重抽样或全量。全量检查如果成本太高比如事实层要查询多个业务服务可以采用“先快检后深检”的两段式策略先做规则类快检命中高风险再触发深度校验。一旦运行期门禁连续红超过一定次数就要自动把流量切到备用模型或模板化回答通道。这里我也吃了不少苦头一开始没有自动化切换只靠告警群结果半夜里模型输出开始疯狂乱来值班同学两小时后才发现。现在的策略是“门禁即熔断”红线指标连续 3 次失败就自动降级。4. 实践中最容易翻车的四个环节4.1 过度约束把大模型逼成了“填空题生成器”Fitness Function 做得太严最常见的副作用是模型失去生成能力。我们曾经为了让 JSON 输出 100% 规范在系统提示词里喋喋不休地强调字段格式、类型、枚举又在输出域写了十几个强制修正逻辑。结果模型确实不再输出非法 JSON 了但开始对稍复杂的问题返回类似“抱歉我无法生成满足要求的回答”的文本——因为它找不到一个能同时满足所有硬约束的答案。这就是适应度函数的“过拟合”。约束的本意是排除错误但当约束过多、互相矛盾时模型会变得过度谨慎宁可拒绝也不犯错。解决思路是要区分硬约束和软指标涉及安全、事实、资产风险的必须是硬约束不能妥协涉及表达风格、格式偏好、字段顺序的优先级降低允许偏差存在用后处理修正而不是强制模型内部满足。提示如果一个模型版本上线后拒绝率明显上升但拦截率没有下降先检查 Fitness Function 是不是太紧了。4.2 误报淹没适应度函数变成“狼来了”Fitness Function 的阈值设置是一门概率管理学问。我们上线第一周把输出域的告警阈值调得特别灵敏结果每天产生两百多条告警绝大部分是轻微格式问题。值班同学开始还认真看一周后直接设置了免打扰。真正出现事实错误的时候告警反而被忽略了。后来我们把适应度函数的输出改成三级PASS 放行WARN 记录并抽样分析不影响用户FAIL 拦截或降级。只有 FAIL 进入即时告警WARN 进入每日汇总PASS 直接丢弃。这样告警量一下子降到每天几条可信度才恢复。在机器学习监控里这叫“精确率和召回率的平衡”在防腐层实践里我建议宁可让一些轻微问题漏进 WARN也不要让 FAIL 被误报淹没。4.3 只测格式不测语义JSON 全对内容全错这是我最想强调的一个坑。很多团队搭防腐层第一步就是做 JSON Schema 校验然后觉得自己已经“安全了”。但格式正确只是最低门槛模型的输出完全可能在结构上完美无缺语义上漏洞百出。举一个真实案例客服系统里模型需要返回“客户意向等级”枚举是 A、B、C。某次模型在 JSON 里输出了“A”Schema 校验通过但事实上这位客户已经连续三个月没有登录意向等级在业务数据库中明确标记为“C 级”。模型为什么输出 A因为它被上下文里某个广告文案中的“A 类重点客户”误导了。格式完全正确事实一塌糊涂。所以输出域里的事实层检查绝不能省。判断依据不是模型自己怎么说而是业务数据库里怎么说。每当输出中出现与业务事实相关的字段就要去查一次权威数据源并做一致性比对。这个查询链路可能有性能开销但这是防腐层必须付出的成本。4.4 适应度函数自身也需要演化和版本管理Fitness Function 有一个容易被忽视的悖论它本身也是代码也会过时。业务字段变了老的 Schema 校验会误杀新功能模型能力升级了老的语义规则会限制它发挥用户群变了老的输入检测规则会漏掉新攻击方式。我们团队后来把 fitness 套件当成一个独立产品来维护每个规则都要有负责人、生效日期、失效条件每次业务规则变更必须同步更新对应适应度函数每季度做一次全量规则的 review删除失效规则合并重叠规则。这么做还有一个附带好处——新同学不需要靠口口相传理解系统的防腐边界看 fitness 套件就能知道哪些行为是被允许的、哪些是被禁止的。5. 从防腐层到反馈回路Fitness Function 的下一步演进5.1 把失败样本变成回归测试资产防腐层最容易被低估的价值不是拦截而是沉淀。每次运行期门禁拦截住一个错误系统都应该自动把“输入、输出、判定结果”存档成一个失败样本。这些失败样本经过人工确认后会自动滚入 CI 阶段的回归样本集。这样一来适应度函数每拦住一次线上事故团队的回归测试集就厚了一层。它解决了很多 AI 团队“找不到高质量测试数据”的痛点——数据不是从外部买来的而是从你的防腐层里长出来的。5.2 防腐层带来的“模型可替换”能力防腐层真正让我觉得值回票价的地方是它给团队带来的“模型可替换”能力