说明:本文讨论的是模型自身拒答行为的工程承接——拒答有哪些形态、怎么定来路、怎么度量、怎么接住,属于模型行为承接话题,不涉及具体模型版本与价格。AI 领域版本迭代极快,凡涉及版本号、价格、可用性,请以你阅读时的官方页面为准。文中代码为结构示意,请按自己的技术栈调整后再上生产。
一、拒答不是一种东西
硬拒:给的是一个明确的「不」
硬拒是指模型明确表达不能协助。它的特征是句子短、语义否定明确、通常不带任何实质内容。对用户来说这是被直接拒绝;对工程来说,这反而是三种形态里最好处理的一种——它在字符串层面可判定,调用返回一次状态码,你能立刻知道这一步失败了。
硬拒的代价是它不可协商。用户改一个标点、换一个问法,拿到的还是同一句话。所以硬拒的正确处置不是"再问一遍"。
软回避:最容易被算进「答对了」
软回避不说不帮,它给一个含糊、泛化、绕开要点的回答。它最麻烦的地方在于形态上完全成功:请求返回正常,有内容,没有报错,长度也不短。
但它进不了任何失败统计。典型特征有三个:句子结构完整却缺少具体名词;把具体问题抬升成一般性建议;回答里出现了用户问句中的词,但没有出现用户要的那个答案。这三种特征里,第三条是最好用的判定线索。
软回避往下游走的时候是静默的。它不会中断流程,只会让结果变空。等到业务方发现"最近输出质量在下降",通常已经积了一批。
带免责的回答:内容给了,责任留给你
带免责的回答是给内容 + 加限定。前置说明、后置提醒、括号里的适用条件,都属于这一类。它是三种里最接近可用的:内容往往是对的,限定往往也是必要的。
风险在下游。如果解析环节只取正文、丢弃限定,那么"在满足某三个前提时成立"会被读成"成立"。这个错误的成本,往往比一次明明白白的硬拒高得多。
| 形态 | 表面表现 | 判定难度 | 用户感受 | 下游影响 |
|---|---|---|---|---|
| 硬拒 | 明确表示不能协助 | 低,字段可判定 | 被直接拒绝 | 流程中断,拿到空结果 |
| 软回避 | 有回答,答非所问 | 高,形态上成功 | 说了,但没用 | 静默进入下游,污染结论 |
| 带免责 | 正文 + 大量限定 | 中,限定容易丢 | 拿到东西但不放心 | 限定条件丢失,结论被误用 |
三类里只有硬拒在指标上是干净的。如果你只用"成功率"看系统状态,三种拒答里有两种不会被看见。
二、先分清来路
四条来路各自留下什么痕迹
拒答有四个来源,它们的可修改性完全不同。
输入触发的规则:长度、关键词、附件类型,通常在请求进入模型之前就被拦下,痕迹在入口日志里。
应用侧外挂的校验层:它拦在模型输出之后。这类拒答有一个特征——模型返回的原文和最终给用户的内容不一致,因为中间被改写或替换过。
系统提示里的约束:这是你自己写进提示里的禁止项。它在提示文本里是可见的,摘掉对应条款就能验证。
模型自身的安全对齐:这一层在模型内部,改不动。它的判定依据是它的训练和对齐结果,你只能通过换模型或换问法去对照观察。
分清来路的必要性在于:外层的规则改一行配置就能动,模型侧的对齐你只能接受并设计承接。把模型侧的拒答当成提示问题去反复调提示,是这类排查里最常见的时间浪费。
判定顺序:从最外层往内收
判定要按"从外到内"走,因为外层最便宜、验证成本最低。
- 先看输入层规则:同一个问题缩短长度、去掉附件再发一次,还拒吗?
- 再看校验层:模型原文和交付内容的差异是否出现在被拒这一步?
- 再看系统提示:把提示里所有"不要 / 禁止 / 仅限"类条款临时摘掉,还拒吗?
- 剩下的归到模型侧:同一提示换一家厂商的旗舰模型问同样的话,看是否同样被拒。
顺序不能反。如果一上来就去改提示、换模型,你会同时动三个变量,最后拿到一个无法归因的结果。
一次最小改动就能定位
判定过程中最要紧的纪律是:每一步只改一个变量。工程上落地的做法是给每次请求存一份"指纹",把输入长度、附件类型标志、系统提示版本、校验层版本一起打进去。拒答发生时,拿当前指纹和上一次成功的指纹做差,差在哪一项,就先怀疑哪一项。
⚠️ 代码待验证
# 定位拒答来路:按从外到内的顺序,每个变体只改一个变量defprobe(variants):"""variants: 同一问题的多个改写,逐条跑,记录是否仍被拒"""report={}forname,reqinvariants.items():resp=call(req)report[name]={"blocked":resp.blocked,# 是否被拒"form":resp.form,# hard / soft / caveated"guard_hit":resp.guard_hit,# 应用侧校验层是否有拦截记录"echo_mismatch":resp.differs,# 模型原文与交付内容是否不同}returnreport# 变体按改动的层从外到内排列,命中哪一层就从哪一层往下查VARIANTS={"baseline":{},# 原样"no_attachment":{"drop_attachment":1},# 只去附件 → 查输入层规则"shortened":{"shrink":1},# 只缩长度 → 查输入层规则"prompt_stripped":{"drop_prompt_clause":1},# 只摘提示条款 → 查提示}这份探针的价值不在代码本身,在于它把"改一处、看一处"变成了流程,而不是靠猜。
三、拒答与答错必须分开度量
三类指标要并排看
拒答和答错不是同一件事,在指标上也不能合成一个数。至少要有三类:
- 拒答率:判为拒答的次数 / 总请求数
- 错答率:有回答但内容错 / 有回答的总数
- 无答案率:既不是拒答、也没有给出可用答案(软回避与限定到不可用的部分)/ 总请求数
三类的分母不一样,这是必须分开的主要原因。把两个不同分母的数放一起算平均,算出来的东西没有含义。
| 指标 | 分子 | 分母 | 单独看会漏掉什么 |
|---|---|---|---|
| 拒答率 | 判为拒答的请求 | 总请求 | 完全看不见软回避 |
| 错答率 | 有回答但内容错 | 有回答的请求 | 看不见拒答造成的空白 |
| 无答案率 | 无可用答案但未拒答 | 总请求 | 与软回避的边界靠标注确定 |
一个「有效回答率」会把什么藏起来
很多系统只统计一个"有效回答率",把三种失败压成一个数。它的问题不是不精确,是方向相反的两次变化会互相抵消。
举一个具体的例子:安全对齐收紧之后,模型在敏感问题上的乱答减少了,错答率下降;同时它在边界问题上的拒答增加了,拒答率上升。这两股力量一叠加,"有效回答率"可能看上去纹丝不动。但用户的体验是明显变差的——原来至少能拿到一个错答案,现在直接拿不到东西。
反过来也成立:一次放宽会让拒答率下降,同时把风险从"看得见的拒绝"搬进"看不见的错答"里。一个总数看不见这个搬运过程。
埋点结构:每条回答都要能回溯到分桶
想把三类指标算准,埋点必须在回答产生的那一刻就带上足够的信息。最少要这几项:形态、来路、提示版本、校验层版本、重试次数。
⚠️ 代码待验证
{"request_id":"req_2f81","answer_form":"soft_avoid","answer_used":false,"refusal_source":"unresolved","prompt_version":"p-2026-09-14","guard_version":"g-2026-08-02","retry_count":1,"latency_ms":1840,"input_flags":["long_doc","no_attachment"]}两个字段值得单独说。answer_used记录的是下游到底有没有采用这个回答,它靠业务侧回填,但它是"无答案率"能不能算出来的前提。refusal_source允许是unresolved——来路判不出来时,记成未定比硬塞一个错误分类强。
四、接住它的四种设计
改述重试:只对特定形态有用
改述重试的思路是换一个说法再问一次。它的适用范围比多数人想的窄:
- 只对硬拒有用。软回避再问一次通常还是软回避。
- 只对来路是输入层规则或提示措辞的硬拒有用。措辞触发的拒绝,换个等义表达确实可能不触发。
- 对模型自身对齐触发的硬拒基本无效。同一件事的不同说法,对齐层判的是意图,不是字面。
改述重试的代价是一次额外调用,以及一个副作用:它会稀释拒答率。如果一次请求内部重试了三次,那么"每次请求的拒答率"会下降,但"每次模型调用的拒答率"没变。所以在埋点里重试次数必须单独记,否则指标会自己骗自己。
换路径与降级:换的是上下文不是模型
换路径重试和改述的区别在于:它不动问法,动的是输入的组织方式。三种常见做法——把长文档拆成片段分别问、把附件转成纯文本再问、把多轮对话压成一轮再问。
它针对的是输入触发规则这一类来路,因为长度、附件类型、轮数都是规则能看见的东西。它的代价是需要重新装配一次上下文,工程上要保证装配后的输入和原始输入在语义上等价。
降级到规则指的是:拒答之后不走模型,走检索、模板或固定话术。它适合有结构化答案兜底的场景——比如查询类问题,答案本来就在库里。它的代价是覆盖面窄,而且回答会明显变死板,用户能感觉出来。
转人工与重试上限
转人工是四种里代价最高的一种,只在有明确业务价值、且前三种都接不住的时候用。它不适合高频低价值的问句,因为排队会变成新的投诉来源。
比选哪一种更重要的是上限。一个可用的配置是:改述一次、换路径一次、降级一次,走完仍然没有可用答案才转人工。
⚠️ 代码待验证
RETRY_BUDGET={"rephrase":1,# 改述重试:最多一次"reroute":1,# 换路径重试:最多一次"fallback":1,# 降级到规则:最多一次}defhandle_refusal(form:str,source:str,done:dict)->str:# 软回避与带免责的回答不进重试链路,它们该走的是质量复核ifform!="hard_refusal":return"deliver"# 模型自身对齐触发的硬拒,重试没有意义,直接走升级路径ifsource=="model_alignment":return"escalate"forstep,limitinRETRY_BUDGET.items():ifdone.get(step,0)<limit:done[step]=done.get(step,0)+1returnstepreturn"human"四种承接放一起看,取舍是这样分布的:
| 承接动作 | 触发条件 | 代价 | 不该用在哪 |
|---|---|---|---|
| 改述重试 | 硬拒,来路是输入规则或提示措辞 | 一次额外调用,指标被稀释 | 模型自身对齐触发的硬拒 |
| 换路径重试 | 输入触发规则(附件 / 长度 / 轮数) | 需要重新装配上下文 | 输入本身就不合规 |
| 降级到规则 | 有结构化答案兜底的场景 | 覆盖面窄,回答变死板 | 开放性问题 |
| 转人工 | 有明确业务价值且前三种接不住 | 成本最高,有等待 | 高频低价值问句 |
上限的作用不只是控成本,它同时保证了一件事:拒答率这个指标不会被无限重试拉平。没有上限的重试链路,最后会让所有指标都变得不可读。
五、过度拒答怎么治
提示里给边界示例,不给口号
过度拒答的成因里,最常见的一条是提示写得像口号。“请严格合规”"注意安全"这类句子对模型没有约束力,因为它没有说明边界落在哪里。
有效的写法是给成对的边界示例:一条是这类可以答,一条是这类不答,最好再有一条是这种情况该先追问。示例要贴近真实业务问句,而不是抽象的类别描述。抽象描述会让模型自己解释边界,解释的结果通常偏向谨慎,也就是拒得更多。
条数要控制。示例太少覆盖不到边界,太多会占用上下文并且互相冲突——两条示例如果在同一情形下给出相反的期望,模型的输出会变得不稳定。
⚠️ 代码待验证
boundary_examples:-id:own_order_invoicesay:"查询本人名下订单的开票状态"expect:answer-id:other_user_ordersay:"帮我查一下这个订单号的开票信息"# 该订单不属于当前登录账号expect:refuse-id:ambiguous_time_rangesay:"上个月的费用明细"expect:clarify# 时间范围缺省,先追问再答回归集要用真实业务问句
判定"是不是过度拒答"这件事,自己编的问句测不出来。原因很简单:编的句子不会有真实用户的措辞习惯、错别字、省略和行业黑话,而这些恰恰是触发误拒的主要来源。
可用的回归集来源是线上抽样。抽出来之后做三件事:去掉含个人信息的样本;给每条标注期望行为(该答 / 该拒 / 该追问);按主题分桶,保证每个桶里都有一定量。
这份集子的标注成本是一次性的,之后每次改动都能重复用。
两个清单分开维护
过度拒答的治理里,一个实用的结构是把样本拆成两份清单并行维护。
| 该拒清单 | 不该拒清单 | |
|---|---|---|
| 样本来源 | 线上被拒且业务确认该拒 | 线上被拒但业务确认可以答 |
| 期望标注 | refuse | answer 或 clarify |
| 主要指标 | 该拒样本的拒答率 | 不该拒样本的误拒率 |
| 变更后的期望方向 | 上升或持平 | 下降 |
两份清单必须同时看。只看"不该拒"那一份,你会把所有拒答都当成问题去压;只看"该拒"那一份,你会把提示越写越紧,最后把可用性压没。这两份清单也是第六章里三条禁忌能不能守住的前提。
完整版资料清单:本文用到的边界示例写法与回归集清单结构都整理在里面了,扫码即可获取:
六、不该做的三件事
拿拒答率当安全指标
拒答率只度量一件事:模型有没有说"不"。它度量不了"该说不的有没有说"。
一个系统可以把拒答率做得很漂亮,同时在一个该拒的样本上照答不误——因为那部分样本根本没有被拒。反过来,把拒答率做低也很容易,放宽一点就行,代价是风险从可见的拒绝搬进了不可见的错答。
真正要盯的是两个带标注的数:该拒样本里拒了多少、不该拒样本里误拒了多少。这两个数只有在有期望标注的回归集上才算得出来,这也是第五章那两份清单存在的理由。
用提示去绕开安全层
有一种做法是:把安全层的判定当成"提示没写好",通过反复改措辞、加角色设定、拼接前缀,去让本该被拒的内容通过。
这条路不推荐,本文也不给出任何具体做法。原因有三条,都和工程无关、和风险有关:
第一,它不改变模型侧的实际判定,只是在你这一条请求上碰巧过了。同样的内容换一次采样、换一个时间再发,结果可能就不一样,不可复现。
第二,它把风险从模型层搬到了你的产品层。模型拒了,责任边界是清楚的;你绕过去交付了,责任就在你这边。
第三,这种做法不可审计。提示里的角色设定和前缀拼接很难在评审里被看出来,但它实际改变了系统的行为边界。
业务确实需要某项被拒的能力时,正路是走合规评审和模型侧配置调整,而不是在提示里做文章。这条边界不能靠工程技巧去试探。
把拒答一律当故障告警
拒答里有一部分是设计意图。把每一次拒答都打成故障告警,结果只有一个:告警疲劳,然后没人再看这个告警。
分级是必须的。可行的分法是三档:单次拒答只进样本、不告警;同一来路的拒答比例在短时间内明显抬升才告警;敏感场景下的拒答单独成一条告警链路,因为它的容忍度本来就低。
分档的依据不是严重程度的主观排序,是"这条告警出来之后有没有人能立刻做点什么"。如果一个告警对应的动作只能是等,它就不该是告警。
七、观测与迭代
拒答样本要有档
拒答样本的存档不是把日志留下来就完了。日志里通常只有请求 ID 和响应体,缺少判断形态和来路所需要的字段,这些字段要在拒答发生的那一刻补齐。
⚠️ 代码待验证
{"refusal_id":"rf_00913","input_digest":"sha256:...","model_output":"……模型返回的原文……","delivered":"……最终交付给用户的内容……","form":"caveated","source":"system_prompt","source_confidence":"confirmed","regression_set":"real_business_v3","ts":"2026-10-13T09:12:00+08:00"}三个字段是关键。input_digest存摘要而不是明文,既能去重、又能避开个人信息的存储问题。model_output和delivered必须分开存——两者不一致本身就是"来路是校验层"的强证据。source_confidence允许是unresolved,来路判不出来时不要硬分类。
按来路分桶看趋势
拒答率的整体曲线几乎没有行动价值。它上升时,你不知道该改提示、该改校验层、还是该接受模型侧的变化。分来路之后,每条曲线的动作才明确。
分桶的落地顺序是:先标置信度高的,也就是入口规则直接拦下的、校验层明确记录的、提示摘除后立刻分化的这三类;剩下的进unresolved桶,定期人工抽样回标。unresolved桶的占比本身就是一个指标——它长期偏高,说明你的判定链路还不够用。
时间维度上要看的是同来路的比例变化,而不是绝对值。因为绝对值和流量结构有关,换一批用户就能让它跳一次。
回归集接进变更流程
观察只能发现变化,验证才能确认变化是不是预期内的。这两件事要靠同一份回归集接起来。
触发回归的时机有三类:系统提示有改动、模型有切换、校验层有调整。这三类改动都会直接改变拒答行为,而它们又都不一定会体现在普通的功能测试里——功能测试看的是"接口通不通",不是"该拒的拒没拒"。
⚠️ 代码待验证
# 提示 / 模型 / 校验层发生变更前,跑一遍回归集run_regression--setreal_business_v3\--reportmetrics\--splitshould_refuse,should_answer# 只盯两个数:# should_refuse 段上的拒答率 —— 变低要查是不是放宽过度# should_answer 段上的误拒率 —— 变高要查是不是提示写紧了这两个数的变化方向是相反的。任何一次改动如果让其中一个变好、另一个变坏,都不是一次干净的改动,需要人工判断这个交换值不值。把这一步固化进变更流程,比事后从线上反馈里倒推要便宜得多。
完整版资料清单:本文用到的拒答样本存档结构与分桶观测口径都整理在里面了,扫码即可获取:
附表 A:关键取舍一览
本文涉及的所有工程判断集中在这里,方便按需回看。
| 取舍 | 本文结论 | 判断依据 | 位置 |
|---|---|---|---|
| 拒答要不要当成一种失败 | 要,但硬拒与软回避分开处理 | 两者在下游的破坏方式不同 | 第一章 |
| 只用成功率看状态行不行 | 不行 | 软回避与带免责在成功率里是成功的 | 第一章 |
| 拒答来路的判定顺序 | 从输入层往模型侧收 | 外层最便宜、验证成本最低 | 第二章 |
| 定位时能不能同时改多个变量 | 不能,一次只改一个 | 否则结果无法归因 | 第二章 |
| 拒答率与错答率能不能合并 | 不能,分母不同 | 合并后方向相反的变化会抵消 | 第三章 |
| 无答案率怎么算 | 靠answer_used回填 | 没有这个字段就算不出来 | 第三章 |
| 来路判不出来时怎么标 | 记unresolved,不硬分类 | 错误分类会污染分桶趋势 | 第三章 |
| 改述重试用在哪 | 只用于输入规则或提示措辞触发的硬拒 | 模型侧对齐判的是意图 | 第四章 |
| 重试要不要设上限 | 要,每类一次 | 无上限会把拒答率拉平到不可读 | 第四章 |
| 软回避要不要进重试链路 | 不要,走质量复核 | 再问一次通常还是软回避 | 第四章 |
| 边界怎么给模型 | 给成对示例,不给口号 | 口号没有说明边界落在哪 | 第五章 |
| 回归集样本从哪来 | 线上抽样,不用自编问句 | 自编句缺少真实措辞与省略 | 第五章 |
| 该拒与不该拒清单 | 分开维护,同时看 | 只看一份必然把提示写偏 | 第五章 |
| 拒答率能不能当安全指标 | 不能 | 它只度量"说不",不度量"该说不" | 第六章 |
| 提示绕过安全层 | 不推荐,走合规评审正路 | 不可复现、责任转移、不可审计 | 第六章 |
| 拒答告警怎么设 | 三档分级,单次只进样本 | 全量告警会导致告警疲劳 | 第六章 |
| 拒答存档存什么 | 模型原文与交付内容分开存 | 两者不一致是校验层拦截的证据 | 第七章 |
| 趋势怎么切 | 按来路分桶,看组内变化 | 总体曲线没有行动价值 | 第七章 |
| 回归集什么时候跑 | 提示 / 模型 / 校验层变更前 | 这三类改动普通功能测试测不到 | 第七章 |
附表 B:术语速查表
| 术语 | 含义 |
|---|---|
| 硬拒 | 模型明确表示不能协助,形态上可判定 |
| 软回避 | 有回答但答非所问、绕开要点的拒答形态 |
| 带免责的回答 | 给出内容同时附加大量限定条件的应答 |
| 无答案率 | 未拒答但未给出可用答案的比例 |
| 拒答率 | 判为拒答的请求占总请求的比例 |
| 错答率 | 有回答但内容错误,占全部有回答请求的比例 |
| 来路 | 拒答的实际触发来源,分输入规则、校验层、系统提示、模型对齐四类 |
| 输入触发规则 | 在请求进入模型前按长度、关键词、附件类型等条件拦截的规则 |
| 应用侧校验层 | 拦在模型输出之后、对结果做筛选或改写的一层 |
| 系统提示约束 | 提示文本中显式写入的禁止类条款 |
| 模型自身对齐 | 由模型训练与对齐结果产生的拒答倾向,应用侧不可直接修改 |
| 请求指纹 | 输入长度、附件标志、提示版本、校验层版本等字段的组合 |
| 改述重试 | 保持意图不变、改变表述后重发一次 |
| 换路径重试 | 改变输入组织方式(拆片段、转纯文本、压轮次)后重发 |
| 降级到规则 | 不走模型,改用检索、模板或固定话术应答 |
| 重试预算 | 每个承接动作允许的最大执行次数 |
| 该拒清单 | 线上被拒且业务确认应拒的标注样本集合 |
| 不该拒清单 | 线上被拒但业务确认可答的标注样本集合 |
| 误拒率 | 在不该拒清单上被拒的比例 |
| 来路分桶 | 按拒答来源对样本分组统计的做法 |
| 告警疲劳 | 告警过多导致接收方不再响应的状态 |
写在最后:这篇用到的资料
写这篇文章时,我把几个模型的官方文档、参数表和实测记录都对了一遍,顺手整理成几份配套的东西:
- 大模型学习路线图:从 LLM 基础到 Agent 开发,各阶段该学什么、用什么资料
- 大模型全套教程:按主题分好的视频与文档清单
- 大模型实战好书:24 本,附每本适合的阶段
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「大模型」,优先通过。
拿到之后建议先看学习路线图那一份,先定位自己在哪个阶段,再决定学什么,比一上来就啃框架效率高得多。