「回声」的一次能力盘点
上一节把 AI 能力写成了契约。这一节要回答更前面的问题:哪些需求根本不该进入模型调用。
「回声」的产品团队一次盘点了七个候选任务:私信智能回复、搜索结果重排、长文摘要、内容审核分类、客服辅助回答、退款审批、手机号脱敏。名单交到工程侧时,几乎每个任务后面都跟着一句「这个应该可以用大模型做」。
工程侧没有直接排期,而是先做了一次判断。判断的结果是:两个任务适合模型,一个任务适合人,两个任务适合规则,另外两个需要拆开看。这个比例让产品团队意外,因为他们原本以为「AI 功能」就是「全部交给模型」。
这一节把那次判断用的四个判据写下来,并把它们变成一个可运行的函数。判据不是为了让工程侧拥有否决权,而是为了让讨论从「模型能不能做」变成「这件事错了会怎样、错了能不能发现、数据能不能出去」。
四个判据
确定性:同样输入是否必须同样输出
有些任务的正确答案是唯一的。用户手机号是不是 11 位、订单号是否符合校验位、一段文本里有没有出现违禁词,这些问题的答案不该每次都不一样。
另一些任务本来就允许变化。给一句「明天见」生成三种不同语气的回复,三种都可能合格;把一篇长文压缩成三句话,选哪三句没有唯一解。确定性低的场景才是模型的舒适区,因为模型的价值恰恰来自「不是查表」。
判断确定性时不要问「模型能不能稳定输出」,要问「业务需不需要稳定输出」。同一个用户重复点击,如果两次结果不同会造成困惑或投诉,这个任务就偏向确定性。
可验证性:错了能不能低成本发现
可验证性分三档:精确校验、按评分表、无法判定。
- 精确校验:输出能直接和规则、数据库、校验位比对。比如金额是否等于订单应付、手机号是否符合号段规则。
- 按评分表:没有唯一答案,但可以由人或另一个模型按维度打分。比如回复是否得体、摘要是否覆盖要点。
- 无法判定:连评分表都难以稳定制定。比如「这段文案是否有感染力」,不同评审给出的结论可能和模型一样随机。
可验证性决定了系统能不能自动兜底。如果输出无法判定对错,就不能让模型直接生效,只能让它产出候选,把最终判断留给人。
容错成本:错一次要付多大代价
同样是分类错误,把一条普通评论误判为广告,代价是用户申诉一次;把一条真正的风险内容放过,代价可能是监管处罚。容错成本要按「最坏情况」估,不是按平均值估。
「回声」内部把错误代价粗略分成五级:1 是无感,2 是可恢复,3 是需要客服介入,4 是资金或声誉损失,5 是合规或人身安全。这个分级是案例假设值,不是行业标准,但它迫使团队在评审时说清楚「错了会怎样」。
高代价任务不意味着不能用模型,而是意味着模型不能是最后一环。退款审批可以让模型整理材料、给出建议,但点「通过」的必须是人。
数据敏感度:输入能不能离开受控环境
数据敏感度分四档:公开、内部、个人、机密。
- 公开:已经发布的内容,进入模型的风险主要是版权与 Prompt 注入。
- 内部:未发布的运营数据,需要租户隔离和访问审计。
- 个人:手机号、地址、聊天内容,进入模型前必须脱敏或最小化。
- 机密:密钥、完整身份证号、支付凭证,原则上不进入第三方模型。
敏感度不是「能不能用 AI」的开关,而是「用哪种部署形态」的约束。机密数据可以走本地小模型或规则,但不应默认走外部 API。把机密数据送进提示词,再指望提供方「不用于训练」,是把合规责任外包给了一纸条款。
规则、模型、人工三选一
四个判据组合起来,任务会被推到三个执行方式之一:
| 执行方式 | 适合的任务 | 典型例子 |
|---|---|---|
| 规则 | 确定、可精确校验、错误代价高 | 手机号脱敏、金额校验、违禁词拦截 |
| 模型 | 非确定、可按评分表验收、长尾多 | 智能回复、摘要、搜索重排、客服草稿 |
| 人工 | 错误代价高且无法客观验收 | 退款审批、争议仲裁、封号申诉 |
判断顺序很重要,因为它决定了冲突时谁优先。本书采用的顺序是:
- 机密数据先排除外部模型,再谈其他。
- 确定且可精确校验的任务优先用规则,哪怕模型也能做。
- 错误代价高且无法客观验收的任务必须由人做最终决定,模型只能辅助。
- 剩下的任务才进入模型,并根据长尾比例决定人工兜底还是规则兜底。
这个顺序看起来「保守」,但它避免了一类常见事故:先用模型做出 90% 的效果,再把剩下 10% 的边界交给运气。规则和人工不是模型的失败备份,而是与模型并列的正式选项。
把判据写成函数
下面的函数把四个判据收敛成一个结论:主执行方式、兜底方式、理由、禁止项。它不调用模型,只做判断,因此可以被单元测试固定。
// file: src/core/suitability.ts
// tier: L1
// verify: node --test test/core/suitability.test.ts
/** 一个任务最终交给谁执行:规则引擎、模型,还是人。 */
export type Execution = "rule" | "model" | "human";
/** 输出能否低成本判定对错:精确匹配、按评分表、无法判定。 */
export type Verifiability = "exact" | "rubric" | "none";
export type Sensitivity = "public" | "internal" | "personal" | "secret";
/** 任务画像只放产品事实,不放“我们想用什么”的偏好。 */
export interface TaskProfile {
/** 相同输入是否必须得到相同输出。 */
deterministic: boolean;
verifiability: Verifiability;
/** 一次错误的业务代价,1 最低、5 最高(案例假设值)。 */
errorCost: 1 | 2 | 3 | 4 | 5;
sensitivity: Sensitivity;
/** 长尾输入占比,0..1(案例假设值)。 */
longTailShare: number;
}
export interface SuitabilityVerdict {
primary: Execution;
fallback: Execution;
reasons: string[];
forbidden: string[];
}
/**
* 判断顺序固定:敏感数据 > 确定性 > 高代价无验收 > 默认。
* 越靠前的规则越会推翻后面的默认做法,保证同一份画像永远得到同一个结论。
*/
export function chooseExecution(profile: TaskProfile): SuitabilityVerdict {
const reasons: string[] = [];
const forbidden: string[] = [];
let primary: Execution;
let fallback: Execution;
if (profile.sensitivity === "secret") {
forbidden.push("model");
reasons.push("secret 数据不得离开受控环境,模型调用被直接排除");
primary = profile.verifiability === "exact" ? "rule" : "human";
fallback = primary === "rule" ? "human" : "rule";
return { primary, fallback, reasons, forbidden };
}
if (profile.deterministic && profile.verifiability === "exact") {
primary = "rule";
fallback = "human";
reasons.push("相同输入必须得到相同输出且可精确校验,规则比模型更稳");
} else if (profile.errorCost >= 4 && profile.verifiability === "none") {
primary = "human";
fallback = "model";
reasons.push("错误代价高且没有客观验收标准,最终判断必须留给人");
reasons.push("模型只能产出候选草稿,不能直接生效");
} else if (profile.longTailShare >= 0.3) {
primary = "model";
fallback = "human";
reasons.push(`长尾输入占比 ${Math.round(profile.longTailShare * 100)}%,穷举规则的维护成本高于模型出错成本`);
} else {
primary = "model";
fallback = profile.errorCost >= 3 ? "human" : "rule";
reasons.push("输入分布相对收敛,模型负责生成,低成本路径负责兜底");
}
if (profile.errorCost >= 4) {
forbidden.push("auto-publish");
reasons.push("错误代价 >= 4,任何输出都不能不经复核直接对外");
}
if (profile.sensitivity === "personal") {
forbidden.push("raw-pii-in-prompt");
reasons.push("个人数据进入提示词前必须脱敏或最小化");
}
if (profile.verifiability === "none") {
forbidden.push("exact-match-gate");
reasons.push("没有标准答案时,不能用精确匹配充当验收门禁");
}
return { primary, fallback, reasons, forbidden };
}
/** 判断某个执行方式是否被画像直接排除,便于在评审里给出反对理由。 */
export function isExecutionAllowed(profile: TaskProfile, execution: Execution): boolean {
return !chooseExecution(profile).forbidden.includes(execution);
}
几个细节需要解释。
第一,secret 分支直接返回,不再进入后续判断。因为数据不能出域时,模型能力再强也没有意义,继续比较只会让讨论跑偏。
第二,forbidden 比 primary 更重要。primary 说「建议怎么做」,forbidden 说「绝对不能怎么做」。评审时后者更容易被忽略,也更容易在事故里出现。比如个人数据任务会禁止 raw-pii-in-prompt,这比写一句「注意脱敏」可执行得多。
第三,长尾比例用的是案例假设阈值 0.3。它的作用不是精确预测,而是把「规则维护成本」变成一个可以讨论的数字:如果 30% 的输入都是规则没覆盖的新形态,继续堆规则的边际收益就很低。
第四,函数返回的理由是稳定顺序的数组,测试可以直接比对。这让「判断有没有变」成为可回归的问题,而不是每次评审重新吵一遍。
测试与真实输出
测试覆盖六个场景:确定性任务、长尾任务、高代价无验收、机密数据、个人数据、重复计算一致性。
// file: test/core/suitability.test.ts
// tier: L1
// verify: node --test test/core/suitability.test.ts
import assert from "node:assert/strict";
import test from "node:test";
import { chooseExecution, isExecutionAllowed } from "../../src/core/suitability.ts";
import type { TaskProfile } from "../../src/core/suitability.ts";
const base: TaskProfile = {
deterministic: false,
verifiability: "rubric",
errorCost: 2,
sensitivity: "internal",
longTailShare: 0.1,
};
test("精确校验的确定性任务交给规则", () => {
const verdict = chooseExecution({
...base,
deterministic: true,
verifiability: "exact",
errorCost: 1,
});
assert.equal(verdict.primary, "rule");
assert.equal(verdict.fallback, "human");
assert.ok(!verdict.forbidden.includes("model"));
});
test("长尾表达任务交给模型并由人工兜底", () => {
const verdict = chooseExecution({ ...base, longTailShare: 0.45, errorCost: 2 });
assert.equal(verdict.primary, "model");
assert.equal(verdict.fallback, "human");
assert.match(verdict.reasons[0], /45%/);
});
test("高代价且无验收标准的任务必须留给人", () => {
const verdict = chooseExecution({
...base,
errorCost: 5,
verifiability: "none",
longTailShare: 0.5,
});
assert.equal(verdict.primary, "human");
assert.equal(verdict.fallback, "model");
assert.ok(verdict.forbidden.includes("auto-publish"));
assert.ok(verdict.forbidden.includes("exact-match-gate"));
});
test("secret 数据直接排除模型", () => {
const profile: TaskProfile = { ...base, sensitivity: "secret" };
const verdict = chooseExecution(profile);
assert.equal(verdict.primary, "human");
assert.ok(verdict.forbidden.includes("model"));
assert.equal(isExecutionAllowed(profile, "model"), false);
});
test("个人数据即使代价不高也要禁止裸提示词", () => {
const verdict = chooseExecution({ ...base, sensitivity: "personal" });
assert.ok(verdict.forbidden.includes("raw-pii-in-prompt"));
assert.equal(verdict.primary, "model");
assert.equal(verdict.fallback, "rule");
});
test("同一份画像重复计算得到相同结论", () => {
assert.deepEqual(chooseExecution(base), chooseExecution(base));
});
在实验工程根目录执行:
node --test test/core/suitability.test.ts
本机一次运行的真实输出如下,耗时随机器变化:
✔ 精确校验的确定性任务交给规则 (0.721791ms)
✔ 长尾表达任务交给模型并由人工兜底 (0.12275ms)
✔ 高代价且无验收标准的任务必须留给人 (0.071292ms)
✔ secret 数据直接排除模型 (0.073709ms)
✔ 个人数据即使代价不高也要禁止裸提示词 (0.062625ms)
✔ 同一份画像重复计算得到相同结论 (0.57925ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 154.589584
注意最后一个测试:它不是「再跑一次看看」,而是断言同一份输入必须得到完全相同的结论。判据函数一旦引入随机性或隐藏状态,这个测试会立刻失败。
「回声」的三个具体决定
盘点之后,「回声」对七个任务做了如下处理。
手机号脱敏走规则。 这是确定性强、可精确校验、敏感度高、错误代价高的任务,四个判据全部指向规则。团队最初试过模型,原因后面单独复盘。
智能回复走模型,人工兜底。 它非确定、可按评分表验收、错误代价中等,且长尾表达很多。模型负责生成,人工负责处理低置信度样本,规则负责拦截明显违规内容。
退款审批走人工,模型只做材料整理。 错误代价高,且「该不该退」没有客观验收标准。模型可以总结用户诉求、列出订单事实,但不能给出最终结论,更不能自动通过。
内容审核是一个混合案例。 它看起来像分类任务,但不同标签的代价不对称:普通广告误判可以申诉,风险内容漏放不行。最终方案是模型先分标签,高风险标签快速失败转人工,低风险标签直接放行。它没有简单落在「模型」或「规则」某一格,而是按标签拆开。
搜索重排和长文摘要走模型,但有明确降级。 两者的错误都可以被用户立刻感知并忽略,容错成本低,适合模型。降级路径分别是原始排序和首段截取,不依赖模型。
一次用模型做脱敏的漏判复盘
「回声」最早把手机号脱敏交给模型,理由很直接:模型能理解上下文,能处理「一二三」写成汉字、中间加空格、加区号这些变体,看起来比正则聪明。
上线后的抽检发现,模型对一类输入的漏判率约为 3%:用户把手机号拆成两行发送,或者在号码中间插入表情符号。3% 听起来不高,但私信量很大,折算下来每天都有相当数量的号码进入下游日志。
事故的根因不是模型「不够强」,而是任务选错了执行方式:
- 脱敏需要确定性。同一个号码,这次遮住、下次没遮住,本身就是漏洞。
- 脱敏可以精确校验。遮没遮住可以直接比对,不需要评分表。
- 错误代价高。一次泄漏就足够构成事故,不能用「平均准确率」安慰自己。
团队后来改成三层:正则覆盖常见格式,校验位与号段做二次确认,无法判定的边界样本转人工。模型被移出主路径,只在离线评估里用来发现新的号码变体,再交给规则补充。
这次复盘给「回声」留下一条硬规则:能用规则稳定解决的高代价任务,不要因为模型「看起来更聪明」就交给模型。 模型的优势在长尾和表达,不在确定性和可审计性。
判据不是永久标签
同一个任务,判断结果会随条件变化。模型价格下降,可能让某个原本用规则的任务变得值得用模型;业务进入强监管市场,可能让某个原本用模型的任务必须转人工;数据脱敏能力提升,可能让机密数据可以走本地模型。
所以判据函数不是一次性评审工具,而是应该随版本重新跑的门禁。产品需求变了、数据分布变了、模型换了,都应该重新输入一次画像,看结论有没有变。结论变了不是坏事,说明判断跟上了现实;结论没变但输入已经变了,才是危险信号。
小结
该不该用模型,不是由模型能力决定的,而是由任务的确定性、可验证性、容错成本、数据敏感度共同决定的。四个判据把任务推到规则、模型、人工三个正式选项,而不是把规则和人工当成模型的备份。
「回声」的脱敏事故说明,错误的选择往往不是因为模型太弱,而是因为把「演示能跑」当成了「任务适合」。把判据写成可测试的函数,能让这类争论在编码前发生,而不是在事故后复盘。
下一节会把这一节的判断结果落到实验工程里:五个能力、一份可校验的清单,以及后面十二章如何围绕这份清单展开。
阅读导航:上一节:1.1 产品里的 AI 能力到底是什么 · 下一节:1.3 ai-lab:从需求到能力契约 。
常见错误与边界
- 用「模型也能做」代替「该不该用模型」。能做和该做是两个问题,后者要看代价与可验证性。
- 只看平均准确率,不看错误分布。高代价场景里,3% 的漏判可能比 30% 的低价值误判更严重。
- 把人工当成兜底按钮。人工是正式执行方式,需要排班、培训、审计,不是出事后临时找人看。
- 把机密数据交给外部模型再依赖条款免责。数据出域的风险不会因为服务条款写得漂亮而消失。
- 判据一次评审就锁死。模型、价格、监管、数据分布都会变,判断要随版本重跑。
- 把长尾比例当精确指标。它是讨论工具,不是预测模型,阈值要结合业务调整。
- 忽略标签之间的代价差异。同一个能力里,不同类别的错误代价可能相差一个数量级。
- 让模型输出直接触发资金或权限动作。高代价动作必须有独立于模型的确认环节。
- 把规则当成落后技术。确定性任务里,规则的可审计性本身就是产品能力。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。