这段判断该写死在代码里还是交给模型,我每次都在这里返工

2026-07-29

数据截至 2026-07,各模型服务的接口能力与错误码口径以官方最新说明为准。

大多数人把这类返工归因成”模型不够聪明”,其实它是一个可以提前算出来的结构问题:这个判断的取值范围,你能不能在写代码的时候就把它列全。 能列全的,交给模型就是自找不稳定;列不全的,写死成规则就是自找漏判。你在这两边来回横跳,改一版好一版、过两周又坏,不是模型退化,是边界一开始就没划。

站内有两篇容易和这篇混在一起看:Agent 角色分工 讲的是一件事该拆给几个角色、各自管哪一段;结构化输出不稳怎么治 讲的是内容已经由模型定了、怎么把它可靠地变成数据。本篇只管前置的那一刀——这个判断到底该不该进模型,判据是什么、怎么验、划错了怎么收回来。

一、先分清:你纠结的是”判断”还是”表达”

排查从这里开始,因为一半的争论其实是问了错的问题。

把一次调用拆成两段看。第一段是判断:在若干条可能的路里选一条,或者给出一个会被下游拿去做分支的值。第二段是表达:把已经定下来的东西写成人话、写成代码、写成 JSON。这两段的失败长得很像,但根因完全不同。

判断出错的典型样子是:同样的输入,跑十次有两次走了另一条分支;或者出现了你的下游代码根本没准备处理的值,直接抛 KeyError。表达出错的样子是:分支选对了、内容也对,但外面裹了一层解释文字,解析器啃不动。

怎么快速验证是哪一段?把同一份输入重复跑若干次,只看那个关键字段落在哪个取值上。字段值飘、且飘出了你预期的集合,是判断层的问题;字段值稳定、但整段响应的格式时好时坏,是表达层的问题,那属于另一篇的范围。

分不清就往下走,你会花两天去调措辞和解析,而问题在于这个判断压根不该由模型来做。

二、可枚举性判据:三问定归属

我用三个问题来定归属,顺序不能换,前一个能定的就不问后面的。

第一问:取值能不能在写代码时列全?

这是主判据。判断的输出如果是一个有限集合,而且这个集合你现在就能一条一条写出来、写完之后不需要模型来兜底”其他”,那它就该是代码。文件后缀映射到语言类型、HTTP 状态码归类到重试或不重试、金额是否超过审批线、目录是否在允许改动的范围内——这些全都能枚举,全都不该问模型。

反过来,取值本质上是开放的,比如”这段用户反馈在抱怨什么”、“这两个函数是不是在做同一件事”、“这段报错和上一次是不是同一个根因”,你没法列全,那就交给模型,但要在它外面加一层把开放结果收敛回有限集合的映射。

注意一个常见的伪枚举:你能列出十条规则,但每条规则的触发条件本身是模糊的。这叫”可枚举的动作 + 不可枚举的条件”,正确做法是模型只做条件判断、输出枚举值,动作由代码执行。

第二问:判错了能不能自动发现、能不能回滚?

同样不可枚举的判断,后果不一样,归属也不一样。判错了下游立刻报错、或者有一层校验会拦住,那可以放心交给模型试。判错了会静默产生一条脏数据、或者直接把文件删了、把提交推上去了,那即使难枚举也要退回到”模型出建议、代码做决定、人确认执行”。

这一层的具体做法是权限收口和确认点设计,这里只用它做归属判据:不可逆的动作永远不由模型直接触发。

第三问:这条规则多久变一次?

有些判断能枚举,但集合一直在长——比如新接入的第三方服务名、新的业务分类。这时候容易得出一个错误结论:“既然老变,那就交给模型算了。“变更频繁不改变归属,它只改变取值表放在哪里。判断逻辑该在代码里,取值表该在代码之外的配置或规则文件里,加一个取值只改数据、不动分支,也就不必为此发一次版。

这一问要防的是另一种偷懒:为了省掉配置加载这几十行,干脆让模型每次现场判断”这个服务名属于哪一类”。省下的是一次性的工程量,换来的是每一次调用都要承担一次不确定性,而且新加的服务名到底有没有被认对,你在日志里看不出来。

三问走完,绝大多数纠结都能落地。剩下真正难定的那一小撮,是”取值开放 + 后果可逆 + 变更频繁”的组合,那才是模型的主场。

三、现象 → 成因判别表

线上出问题的时候,你手里通常只有现象。用这张表往回推。

现象大概率成因怎么验证处置动作
同一输入多次运行走到不同分支可枚举的判断被交给了模型固定输入重复跑多次,统计关键字段的取值分布把这个字段改成代码规则,模型只做前置的语义抽取
下游拿到没见过的取值直接抛异常模型输出未被收敛回有限集合在解析后加一层集合断言,看是否触发加白名单映射,落到集合外一律走默认分支并记录
新增一类输入就得改代码发版取值表被硬编码进了判断逻辑数一下改一条规则要动几个文件判断逻辑留代码,取值表外移到配置或规则文件
规则越加越多、互相打架用规则去逼近本质模糊的条件看是否出现”如果包含 A 但不包含 B 且长度大于 N”这类堆叠把条件判断交给模型,只让它输出枚举值
每次修一个 case 就坏一个旧 case判断的边界从未被固化成用例把最近几次修过的 case 重新跑一遍,看有几条已经退回错误结果先补回归集,再改判断,见评测方法那篇
结果对但格式时好时坏这是表达层问题,不是归属问题只看关键字段取值是否稳定走解析与约束那条线,别动归属
报 429 或超时后整条链路给了兜底答案失败被当成了一种正常判断结果断网或人为注入错误,看输出是否仍然”成功”错误必须独立于业务取值传播,不能混进枚举里

最后两行是高频误判。前者会让你误以为是判断不稳,其实归属没错;后者更危险——把网络失败包装成”模型判断为无需处理”,这条链路会安静地一直错下去,而且它在监控上看起来一切正常。

四、把判断从模型手里收回代码的四种做法

确认某个判断该归代码之后,别直接删掉调用改成 if-else,那样容易把模型顺手做的语义抽取一起丢了。按下面四种做法,从轻到重挑。

做法一:模型只出枚举值,动作全在代码。 让模型的输出退化成一个标签,代码拿标签查表决定做什么。这是最省事的改法,改动量小,收益立刻可见。落地时要在解析后立刻做集合校验:

from enum import Enum

class Intent(str, Enum):
    REFUND = "refund"
    SHIPPING = "shipping"
    OTHER = "other"

def to_intent(raw: str) -> Intent:
    key = (raw or "").strip().lower()
    try:
        return Intent(key)
    except ValueError:
        return Intent.OTHER   # 落表外一律进兜底分支,并记录原值

关键在最后一行:集合外的值不是异常,是默认分支加一条日志。你要靠这条日志的量来决定枚举表是不是该扩。

做法二:前置硬闸门。 一部分判断根本不用进模型。路径是否在允许目录内、文件是否二进制、改动行数是否超过阈值,这些在调用之前就该拦掉。闸门写在代码里,模型看不见被拦掉的东西,也就没机会做错决定。这一层顺带省下大量无效调用。

做法三:规则先行、模型兜底。 顺序是规则命中就直接返回,没命中才走模型。它的好处是规则表越长,模型调用越少,且行为越来越确定。反过来的顺序(模型先跑、规则校验)看着差不多,实际每次都要付出一次调用,且规则只能否决不能加速。

做法四:模型出建议、代码做决定、人确认执行。 用在不可逆动作上。模型的输出被降级成一条建议记录,真正的执行由代码在收到明确确认后触发。切换的时候留一个开关,别把老路径删掉:

export AGENT_DECISION_MODE=rules   # rules | model | shadow

shadow 那一档很有用:两条路都跑,只采用规则的结果,把模型的结果写进日志做对比。跑上一段时间,你就有了真实分歧率,而不是靠感觉争论。工具边界怎么切得更细,可以对照 Agent 工具设计

五、什么时候别再折腾

划错边界的代价是持续的,所以要有明确的止损点。下面四条满足任意一条,就停手换路,别再调了。

规则数量超过你能一眼看完的量,且新增一条会改变旧条的行为。 这说明你在用离散规则逼近一个连续的判断。继续加规则的边际收益是负的——每加一条,回归集里就有旧 case 翻车。止损动作是把这段条件判断整体交给模型,只保留动作层的枚举。

同一个字段的措辞你改了三轮以上还在飘。 措辞是概率约束,改到第三轮还不稳,基本可以断定这个字段不该由模型定。止损动作是把它降级:让模型输出更粗的分类,细分支由代码根据其他确定性信号来分。

分歧率下不去,但每一次分歧你都说不清谁对。 这不是技术问题,是你没有判定标准。这时候继续调是浪费,先去把评判标准写成固定用例,做法见 Agent 评测方法。没有回归集的调优,本质是随机游走。

为了让模型判断准,你开始往上下文里塞越来越多的背景。 这是典型的越界信号:一个判断需要大量额外背景才做得对,说明所需信息在代码里是现成的、结构化的,你却绕道让模型从文字里重新推一遍。止损动作是把那份结构化数据直接喂给规则。

回滚点怎么留?改归属属于行为变更,一定要单独成一次提交,别和重构混在一起。出问题的时候你要能干净地退回去:

git log --oneline -- path/to/decision_layer.py
git revert <commit>

再补一句关于工具选择的现实:部分海外模型服务官方并未向中国大陆提供直连,市面上存在各种第三方中转,是否可用、是否合规需要你自己判断,这里不做背书也不给渠道。把关键判断押在一个你无法保证可达性的服务上,本身就是一个该止损的架构决定。

六、避坑清单

坑一:把”模型能做”当成”模型该做”。 会踩是因为验证时它确实做对了,你就顺手留下了。避法是加一问——这个判断如果错了,多久会被发现?发现不了的,一律往代码收。

坑二:枚举表和下游分支两处维护。 会踩是因为加取值的时候只改了一处,另一处默默走进了兜底。避法是让下游分支从同一个枚举定义里生成,或者写一条测试断言两边的键集合完全相等,加取值忘了同步会直接红。

坑三:把错误当成一种判断结果。 会踩是因为异常处理里图省事返回了一个合法的默认值,429、超时、解析失败全被抹平成”正常”。避法是错误单独一条通路,业务枚举里绝不允许出现表示失败的成员,重试策略参考 Agent 失败重试

坑四:拿单次运行的结果确认边界。 会踩是因为改完随手跑一次通过了就收工,而判断层的问题本来就是概率性的。避法是固定输入重复跑并统计分布,只要出现两种以上取值就当没通过。

坑五:规则外移之后没人管配置的合法性。 会踩是因为规则文件被当成纯数据,谁都能改,改错了到运行时才炸。避法是启动时校验配置——键是否在枚举内、是否有重复、是否有孤儿分支,校验不过直接拒绝启动,别等到线上。

坑六:把判断和执行写在同一个函数里。 会踩是因为一开始只有两条分支,顺手就写在一起了。等你想加 shadow 对比、想换归属的时候,会发现根本切不开。避法是从第一天就让判断函数纯粹——只返回枚举值,不产生任何副作用,执行放在调用方。

坑七:以为归属划一次就定了。 会踩是因为业务在变,今天开放的取值半年后可能已经收敛。避法是把兜底分支的命中量当成指标看,它持续上升就该重新划边界了。

收束

这件事没有普适答案,但有普适顺序:先确认你纠结的是判断层还是表达层,再用可枚举性、可逆性、变更频率三问定归属,最后靠 shadow 模式和回归集验证你划得对不对。

提交前对着这五条自查一遍:

  1. 这个判断的取值我现在能不能一条不落地写出来?
  2. 落在集合之外的值,会走进哪条分支、有没有留下记录?
  3. 判错了多久会被发现,发现之后能不能一条命令回滚?
  4. 错误路径有没有混进业务枚举里?
  5. 同一份输入跑多次,关键字段的取值是不是只有一种?

五条都答得上来,这段边界就算划稳了。答不上来的那条,就是你下一次返工的位置。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。