System Prompt 替代 Few-shot:用规则约束大模型输出的省钱实践

发布时间:2026/10/7 7:46:29
System Prompt 替代 Few-shot:用规则约束大模型输出的省钱实践
起因一次调用成本的对账我手上有个后台工具主要工作是把用户提交的一段自然语言需求转成结构化的任务配置。输出格式固定大概长这样{task_type:extract,target:title,params:{max_length:50}}早期为了让它稳定输出这个结构我在每次请求里带了三组 Few-shot 示例一正两反加上说明文字光示例部分就占了 400 多 Token。单看一次没什么感觉但这个东西每天要跑几万次示例 Token 是每次都重复付费的。我算了一笔账假设输入 400 Token、输出 60 Token示例占了输入的绝大部分。如果能把示例去掉、改成 System Prompt 里一次性说清楚规则那每次请求的输入能压到 100 Token 以内。调用量越大省得越多。于是我做了一轮改造把 Few-shot 从请求里挪到 System Prompt用规则和字段约束来替代示例。这篇就是这次改造的记录——哪些规则真的有用哪些其实是在自欺欺人。System Prompt 和 Few-shot 到底差在哪先把两者的定位说清楚不然后面容易混。Few-shot 的本质是用示例演示分布。模型看到几个输入输出对会去模仿这个映射关系。它的优点是直观模型对长什么样有具体参照缺点是每次请求都要重复付示例的 Token。System Prompt 的本质是用规则约束行为。它告诉模型你是谁、你要做什么、输出必须满足什么条件但不给具体样例。优点是写一次可以复用如果用了 Prompt Caching 还能进一步省钱缺点是模型对抽象规则的理解不如对具体示例稳定。这里有个容易被忽略的点System Prompt 不是免费的。它一样占输入 Token只是它不随请求变化可以被缓存也可以在多轮对话里只算一次。如果你每次都是单轮调用、又没开缓存那 System Prompt 和 Few-shot 在 Token 上的差别没那么大省的主要是示例比规则啰嗦这部分。我这次改造真正省下来的是示例本身比规则长这件事加上一部分缓存收益。把示例改写成规则的具体做法我没有直接把三组示例删掉换成一句请输出 JSON那样模型会飘。我的做法是把示例里隐含的约束一条条抽出来变成显式规则。原来的 Few-shot 里示例其实在传递这几件事输出必须是合法 JSON不能有 markdown 代码块包裹task_type只能是枚举里的某几个值params里的字段随task_type变化不确定的字段要省略不能瞎填。第 1、2、4 条可以直接写成规则第 3 条比较麻烦因为它是条件结构光靠一句话模型容易漏。我最终的 System Prompt 大致是这样组织的这里用的是通用的角色设定写法不绑定具体厂商 API你是一个任务配置解析器。用户输入一段自然语言需求你输出一个 JSON 对象。 输出规则 - 只输出 JSON 本身不要 markdown 代码块不要任何解释文字。 - 顶层字段固定为 task_type、target、params 三个。 - task_type 只能取以下值之一extract、transform、validate。 - target 是一个字符串表示操作对象例如 title、content、metadata。 - params 是一个对象。当你不确定某个参数时直接省略该键不要填 null 或空字符串。 - 如果用户需求无法映射到上述 task_type输出 {error: unsupported}。 字段约束 - extract 的 params 允许 max_length整数1-500。 - transform 的 params 允许 mode枚举upper、lower、trim。 - validate 的 params 允许 rules字符串数组元素只能是 non_empty、max_length。 只输出 JSON不要输出其他内容。这段比原来的三组示例短了不少。我实测下来在同一个模型、同一批测试输入上去掉示例后输出结构的合法率没有明显下降但确实出现了新的失败模式下面讲。规则能顶替示例但有边界改造之后我跑了一批真实输入发现规则和示例的能力差异集中在几个地方。规则擅长的固定格式、枚举、简单嵌套。上面那些只能取哪些值不要包代码块的约束规则比示例更清晰因为它把边界写死了模型不用从示例里猜。规则吃力的条件分支。比如params的字段随task_type变化我在提示里是分三段写的模型大多数时候能对上但偶尔会把extract的max_length塞进validate里。这种情况示例其实更有效因为示例直接展示了extract 长这样、validate 长那样模型照抄结构就行。规则基本无效的需要判断力的模糊场景。比如用户说把标题弄短一点到底算 extract 还是 transform这种语义判断示例里如果有一个弄短一点 → extract max_length的对照模型学得会更快。光靠规则描述模型只能靠猜。所以我的结论是规则替代示例适合输出格式固定、字段枚举明确、分支不深的场景。一旦涉及条件结构和语义判断纯规则会掉质量这时候要么保留少量示例要么用 JSON Schema 之类的强约束手段。用 Schema 把规则再收紧一层光靠自然语言规则模型还是可能跑偏。如果调用方支持结构化输出比如某些 SDK 提供的 JSON Schema 约束或 function calling那最好把规则和 Schema 结合让格式层面的错误由引擎兜底而不是全压在提示词上。我这边用的是把 Schema 作为提示的一部分附上配合后置校验。Schema 本身用 Python 字典描述TASK_SCHEMA{type:object,properties:{task_type:{enum:[extract,transform,validate]},target:{type:string},params:{type:object},},required:[task_type,target],}PARAM_RULES{extract:{max_length:(int,1,500)},transform:{mode:(enum,[upper,lower,trim])},validate:{rules:(list,[non_empty,max_length])},}注意这里我没有直接把 Schema 塞进提示让模型自己看因为不同模型对原始 JSON Schema 的阅读能力差别挺大。我把它当作后置校验的依据同时把关键约束用自然语言在 System Prompt 里重述一遍。相当于提示词负责引导校验层负责兜底。校验函数大致长这样defvalidate_output(obj:dict)-tuple[bool,str]:ifobj.get(task_type)notin(extract,transform,validate):returnFalse,task_type 非法ifnotisinstance(obj.get(target),str):returnFalse,target 必须是字符串paramsobj.get(params,{})ifnotisinstance(params,dict):returnFalse,params 必须是对象rulesPARAM_RULES.get(obj[task_type],{})forkey,valinparams.items():ifkeynotinrules:returnFalse,f{obj[task_type]}不支持参数{key}specrules[key]ifspec[0]isint:ifnotisinstance(val,int)ornot(spec[1]valspec[2]):returnFalse,f{key}超出范围elifspec[0]enum:ifvalnotinspec[1]:returnFalse,f{key}取值非法returnTrue,校验失败时的处理策略我犹豫过一阵是直接报错让上游重试还是把错误信息回填给模型让它自己改。实测下来把错误信息作为一次追问回填修复成功率不错但会多一次调用。如果调用量很大、又对延迟敏感直接失败重试可能更划算。这个取舍取决于你的场景我没有一个通用答案。省了多少怎么测的Token 的节省是可以直接算的不用靠感觉。改造前每次请求的输入包含系统说明 三组示例 用户输入。改造后输入是系统说明规则版 用户输入。我用同一批 200 条真实需求分别跑了两版统计输入 Token 的平均值。结果上示例部分大约占了改造前输入的一半以上具体比例和示例长度有关。我这里没有把精确数字写出来因为不同模型的 tokenizer 不一样写死了反而误导。可以确定的是示例越长、调用量越大省得越明显如果示例本来就只有一两行那改造收益有限。质量方面我盯的是两个指标JSON 解析成功率、字段合法率。改造后解析成功率基本持平字段合法率略微下降主要就掉在条件分支那个坑上也就是params字段串台。后来我在 System Prompt 里把那三个分支拆得更开、每个分支单独起一段情况好转了一些。【关键结论】用 System Prompt 替代 Few-shot在格式固定、枚举明确的场景下能省下可观的输入 Token但条件结构和语义判断这两类约束仍然依赖示例或 Schema 兜底。纯规则不是万能替代。几个实际写规则时的注意点写规则版的 System Prompt有几个地方和写示例的直觉不太一样。规则要写成约束而不是建议。“尽量输出 JSON和只输出 JSON不要其他内容”模型的表现差别很大。前者模型会给你加解释后者更听话。措辞上少用尽量“可以”“建议”。枚举值要写全不要写等。写task_type 可以是 extract、transform 等这种模型会自己发挥造出新值。要么全列要么明确说只能取以下值。否定规则要具体。“不要输出多余内容这种太虚模型不知道什么是多余。改成不要 markdown 代码块”“不要解释文字”“不要前后加引号”每一条都指向一个具体的错误行为效果好很多。别指望一条规则覆盖所有分支。条件结构最好一段一段写每段对应一种情况。挤在一句话里模型漏的概率明显上升。【踩坑提醒】System Prompt 里的规则和你的后置校验必须是同一套逻辑。我一开始改提示词改得勤但校验函数忘了同步结果出现了提示说允许、校验说非法的错位排查时反而更绕。两边要么共用一份配置要么改的时候一起改。什么时候还是得留 Few-shot不是所有场景都适合把示例删干净。我整理了一下判断标准输出是固定结构、字段枚举清晰、没有条件嵌套 → 用规则删示例有条件分支但分支数量少 → 规则为主每个分支可以保留一个最小示例涉及语义分类、意图判断、风格模仿 → 示例更有效规则只能辅助输出格式复杂到自然语言写不清楚 → 优先考虑结构化输出能力或 Schema 约束而不是堆规则或示例。这里有个现实考量如果调用量不大省下来的 Token 钱可能还不如你花在调试规则上的时间值钱。Few-shot 虽然费 Token但它直观、好调、见效快。要不要改取决于调用量和维护成本的平衡不是规则一定比示例高级。顺带说说缓存如果你用的模型平台支持 Prompt Caching那 System Prompt 的固定部分是可以被缓存的重复调用时这部分不计费或按折扣计费。这时候把约束放进 System Prompt 的收益会更大因为它是写一次、缓存住、长期复用的。但要注意任何改动都会让缓存失效。我调整规则措辞的那几天缓存基本一直在重建那段时间的成本反而上升了。所以规则定稿之后尽量别再频繁动它。这一点不同平台的缓存机制细节不一样具体计费规则我没逐个验证用之前建议查一下对应平台的文档。结尾这次改造给我最大的感受是Few-shot 和 System Prompt 不是二选一而是各自负责不同类型的约束。格式和枚举交给规则条件结构和语义判断交给示例或 Schema后置校验兜底。把职责分清楚之后Token 省下来了质量也没塌。如果你的项目也在用 Few-shot 撑格式可以先统计一下示例占了多少输入 Token再判断值不值得改。如果示例本来就短、调用量也不大那维持现状反而是更省心的选择。