1. 为什么“赌 AI 正确率”这件事必须被终结
做 AI 应用落地的人,大概都经历过这种场景:模型在演示时表现惊艳,一旦进入真实业务流程,就开始随机翻车。你问它一个结构化抽取任务,十次里有八次对,剩下两次要么字段漏了,要么格式错了,要么干脆编了一个不存在的值。于是团队里开始出现一种很拧巴的工作方式——把 AI 当成一个概率性组件来用,然后靠重试、靠人工兜底、靠“多跑几次取多数”来凑合。
这套玩法在小规模验证阶段还能忍,一旦上了生产,问题就全暴露了。你没法跟业务方解释“这个功能 92% 的时候是对的”,也没法在关键链路上放一个随时可能抽风的环节。更麻烦的是,正确率这个东西本身是不可控的——你调了提示词,今天从 88% 涨到 93%,明天换个输入分布又掉回 85%。你永远在赌,而且赌注是线上稳定性。
这个项目要解决的就是这件事:造一个编译器,把自然语言里那些模糊的、概率性的意图,编译成确定性的、可验证的执行逻辑。说白了,就是不再让 AI 直接产出最终结果,而是让 AI 产出一段“程序”,再由这个编译器去执行这段程序。程序的执行是确定的,输入相同必然输出相同,正确率这件事就从“赌”变成了“验证”。
我先把结论摆在这:这个思路的核心价值不在于让 AI 更聪明,而在于把 AI 的不确定性隔离在一个可控的边界内。AI 负责理解意图、生成中间表示,编译器负责把中间表示翻译成确定性操作。只要中间表示能被校验,最终结果的正确性就有了保障。这套逻辑我在几个数据处理和自动化流程的项目里都验证过,确实能把“看运气”变成“看规则”。
适合谁来参考这篇文章?如果你正在做 AI 应用落地、正在被模型的稳定性折磨、或者你手上有一堆“AI 能做个大概但不敢上生产”的需求,那这篇内容应该能给你一条可复现的路径。我会从整体设计思路讲到核心实现细节,再到实操步骤和踩坑记录,尽量把每个“为什么这么设计”都讲透。
2. 编译器的整体设计思路与方案选型
2.1 核心思路:把 AI 从“执行者”降级为“翻译官”
传统做法是让 AI 直接干活:你给它一段自然语言指令,它直接返回结果。这个模式下,AI 既是理解者又是执行者,两个角色混在一起,出了问题你根本不知道是理解错了还是执行错了。
这个编译器项目的核心思路是角色分离。AI 只负责一件事:把用户的自然语言意图翻译成一种结构化的中间表示(IR,Intermediate Representation)。这个 IR 是一种受限的、有明确语法的描述语言,它只能表达有限种类的操作。然后编译器接管,把 IR 编译成真正可执行的代码或操作序列。
打个比方,这就像你去餐厅点菜。以前是直接跟厨师说“来个清淡点的”,厨师凭感觉做,每次味道都不一样。现在是服务员把你的需求翻译成一张标准点菜单:少油、少盐、不要辣、蒸制。厨房拿到这张单子,按固定流程执行,出来的菜每次都是一个味道。服务员可能偶尔翻译错,但点菜单本身是可检查的——你一眼就能看出“不要辣”有没有被写上。
这个设计带来的直接好处有三个。第一,错误可定位。如果最终结果不对,你可以检查 IR 是不是翻译错了,而不是面对一个黑盒输出干瞪眼。第二,结果可复现。同样的 IR 编译出来的执行逻辑是确定的,不会因为模型温度参数变化而漂移。第三,边界可控制。IR 的语法是你定义的,AI 只能在这个语法范围内表达,它没法“自由发挥”出一个你没预期的操作。
2.2 为什么选“编译器”而不是“校验器”或“重试机制”
市面上应对 AI 不确定性的常见方案有三类:输出校验、多轮重试、以及这个项目采用的编译思路。我分别说说它们的取舍。
输出校验是最简单的,就是拿 AI 的输出去比对规则,不符合就拒绝。问题是它只能拒绝,不能修复。你校验出格式错了,然后呢?还是得重新问 AI,又回到赌的循环。
多轮重试是“多跑几次取多数”或者“跑到达标为止”。这个方案在成本不敏感、延迟不敏感的场景下能用,但它的正确率提升是概率性的,不是确定性的。跑三次取多数,正确率从 90% 提到 97%,听起来不错,但那 3% 依然存在,而且你付出了三倍的成本和延迟。
编译思路的本质区别在于,它把不确定性收敛到了一个可验证的中间层。AI 生成 IR 这一步仍然可能出错,但 IR 是可以被静态检查的。语法对不对、字段全不全、操作类型是否在允许列表内,这些都能在编译前验证。一旦 IR 通过校验,后面的执行就是 100% 确定的。你不需要赌执行结果,你只需要保证 IR 正确,而 IR 的正确性是可以被程序验证的,不需要靠概率。
提示:这个思路有个前提,就是你的业务场景能被拆解成有限种类的确定性操作。如果需求本身就是开放式的、没有明确对错标准的,那编译器模式并不适用。选型前先想清楚你的任务边界。
2.3 IR 的设计原则:够用就好,别贪多
设计 IR 是整个项目里最需要克制的地方。我见过不少类似的尝试,一上来就想设计一门“万能语言”,结果语法复杂到 AI 根本学不会,生成出来的 IR 十有八九是错的,反而比直接让 AI 干活还差。
这个项目的 IR 设计遵循三个原则。第一,操作类型最小化。只保留业务真正需要的操作,比如数据抽取、字段映射、条件过滤、格式转换这几类。每多一种操作类型,AI 的学习负担就多一分,出错概率就高一分。第二,语法扁平化。不做嵌套、不做复杂表达式,每条 IR 指令就是“操作类型 + 参数列表”的简单结构。扁平结构的好处是解析简单、校验简单、AI 也容易生成对。第三,参数强类型。每个操作的参数都有明确的类型约束,字符串就是字符串,数字就是数字,不允许模糊地带。
我实测下来的经验是,IR 的操作类型控制在 10 种以内,单条指令的参数控制在 5 个以内,AI 生成 IR 的首次正确率能稳定在 95% 以上。剩下的 5% 通过编译前的静态校验基本都能拦下来,真正漏到执行阶段的错误几乎为零。这个数字比直接让 AI 出结果的正确率高了一个量级,而且关键是它稳定,不会今天高明天低。
3. 核心细节解析与实操要点
3.1 IR 的具体结构长什么样
先给你看一个 IR 的实际样子,这样后面讲实现你才有画面感。假设业务需求是“从一段用户反馈里抽取产品名称、问题类型和紧急程度”,IR 大概长这样:
{ "version": "1.0", "operations": [ { "op": "extract", "source": "input.text", "target": "product_name", "type": "string", "constraint": "must_be_in_known_products" }, { "op": "classify", "source": "input.text", "target": "issue_type", "type": "enum", "options": ["功能异常", "性能问题", "使用咨询", "其他"] }, { "op": "classify", "source": "input.text", "target": "urgency", "type": "enum", "options": ["高", "中", "低"] } ] }这个结构有几个关键点。version字段是为了后续 IR 语法升级时做兼容,operations是一个有序列表,编译器按顺序执行。每个操作有op类型、source数据来源、target输出字段、type数据类型,以及可选的constraint或options约束。
注意constraint和options这两个字段,它们是把校验前移的关键。must_be_in_known_products这个约束意味着编译器在编译阶段就能检查抽取结果是否在已知产品列表里,不在就直接报错,而不是等到执行完才发现抽了个不存在的产品名。options同理,分类结果必须是枚举值之一,AI 如果生成了一个不在列表里的值,编译阶段就会被拦下。
3.2 AI 生成 IR 的提示词工程要点
让 AI 稳定生成符合语法的 IR,提示词的设计比模型选型更重要。我踩过的坑是:一开始把 IR 语法规则写得很详细,结果 AI 反而被绕晕了,生成出来的东西语法是对的但语义跑偏。后来调整策略,把提示词分成“语法约束”和“语义示例”两块,语法约束用最简短的规则描述,语义示例给足 3 到 5 个覆盖不同场景的完整例子,效果明显好转。
语法约束部分我一般这么写:
你是一个 IR 生成器。根据用户需求生成 JSON 格式的 IR。 规则: 1. 只使用以下操作类型:extract, classify, filter, map, format 2. 每个操作必须包含 op, source, target, type 四个字段 3. type 只能是 string, number, enum, boolean 之一 4. enum 类型必须提供 options 数组 5. 不要输出任何解释文字,只输出 JSON语义示例部分给的是“用户需求 → IR”的配对,覆盖简单抽取、多字段分类、条件过滤等典型场景。这里有个细节:示例里的字段名和业务字段保持一致,这样 AI 会倾向于复用你定义的字段名,而不是自己造新名字。字段名不一致是 IR 校验失败的高频原因,用示例把命名规范“喂”进去,能省掉很多麻烦。
注意:提示词里的规则条目不要超过 7 条。超过之后 AI 的遵循率会明显下降,这是我在多个项目里反复验证过的。如果规则确实多,就拆成两轮生成,第一轮生成骨架,第二轮补细节。
3.3 编译前的静态校验清单
IR 生成出来之后,不能直接执行,必须先过一遍静态校验。这一步是整个方案的安全阀,校验做得越细,执行阶段越稳。我整理的校验清单大概有这么几项:
| 校验项 | 检查内容 | 失败处理 |
|---|---|---|
| 语法校验 | JSON 是否合法、必填字段是否齐全 | 直接拒绝,返回错误位置 |
| 操作类型校验 | op 是否在允许列表内 | 拒绝并提示可用类型 |
| 类型校验 | type 是否合法、enum 是否有 options | 拒绝并指出问题字段 |
| 字段引用校验 | source 引用的字段是否在上游存在 | 拒绝并提示字段依赖顺序 |
| 约束校验 | constraint 引用的资源是否存在 | 拒绝并提示缺失资源 |
| 循环依赖校验 | 操作之间是否存在循环引用 | 拒绝并输出依赖图 |
这张表里,字段引用校验和循环依赖校验是最容易被忽略但最致命的。字段引用错了,执行时才发现某个 source 字段根本不存在,整个流程就断了。循环依赖更隐蔽,A 操作依赖 B 的输出,B 又依赖 A 的输出,编译器如果没检查,执行时就会死循环。这两项校验加上之后,我这边 IR 的编译通过率从 82% 提到了 96%,剩下的 4% 基本都是业务逻辑本身有问题,不是 IR 的问题。
3.4 执行引擎的设计:确定性从哪来
IR 通过校验之后,就进入执行引擎。执行引擎的设计目标只有一个:同样的 IR 加同样的输入,必须产出同样的输出。为了做到这一点,执行引擎里不能有任何随机性、不能依赖外部状态、不能有时间相关的逻辑。
具体实现上,每个操作类型对应一个纯函数。extract就是按规则从文本里抽字段,classify就是按规则做分类,filter就是按条件过滤。这些函数不调用任何外部服务,不读数据库,不写日志到外部系统。所有需要的外部资源,比如已知产品列表、分类枚举值,都在编译阶段就作为参数固化到 IR 里了。
这里有个设计取舍值得说:执行引擎要不要调用 AI。我的选择是,执行引擎完全不碰 AI。AI 只在生成 IR 那一步出现,IR 一旦生成,后续所有环节都是传统程序逻辑。这样做的好处是执行阶段零延迟波动、零成本波动、零不确定性。代价是 IR 的表达能力受限于预定义的操作类型,没法处理那些“必须靠 AI 现场判断”的场景。但对于绝大多数结构化任务来说,这个代价是值得的。
4. 实操过程与核心环节实现
4.1 从零搭建的最小可行版本
我建议第一次上手不要追求完整,先搭一个最小可行版本跑通链路。最小版本只需要三个模块:IR 生成器、静态校验器、执行引擎。IR 生成器可以先用现成的模型 API,静态校验器和执行引擎自己写,加起来不到 500 行代码。
第一步,定义 IR 的 JSON Schema。这一步决定了后面所有环节的边界,值得花时间打磨。Schema 里把操作类型、字段类型、必填项都定义清楚,后面校验器直接拿 Schema 做校验,省事很多。
IR_SCHEMA = { "type": "object", "required": ["version", "operations"], "properties": { "version": {"type": "string"}, "operations": { "type": "array", "items": { "type": "object", "required": ["op", "source", "target", "type"], "properties": { "op": {"enum": ["extract", "classify", "filter", "map", "format"]}, "source": {"type": "string"}, "target": {"type": "string"}, "type": {"enum": ["string", "number", "enum", "boolean"]}, "options": {"type": "array"}, "constraint": {"type": "string"} } } } } }第二步,写 IR 生成器。核心就是拼提示词、调模型、解析返回的 JSON。这里有个实操细节:模型返回的 JSON 经常带 markdown 代码块标记,解析前要先剥掉。我一开始没处理这个,解析失败率高达 30%,加上剥离逻辑之后降到 2% 以下。
def generate_ir(user_request): prompt = build_prompt(user_request) response = call_model(prompt) cleaned = strip_code_fence(response) try: ir = json.loads(cleaned) except json.JSONDecodeError: return None, "IR 解析失败" return ir, None第三步,写静态校验器。用jsonschema库做基础校验,再补上字段引用和循环依赖的检查。字段引用检查的逻辑是维护一个“已定义字段集合”,按 operations 顺序遍历,每个操作的 source 必须在集合里,target 加入集合。循环依赖检查用拓扑排序,排不出来就说明有环。
第四步,写执行引擎。每个操作类型一个函数,用字典做分发。执行时按 operations 顺序遍历,把每个操作的输出写进上下文,供后续操作引用。
4.2 参数选择与性能调优
模型选型上,IR 生成这个任务不需要最强的模型。我对比过几个不同规模的模型,在 IR 生成任务上,中等规模模型和最大规模模型的首次正确率差距只有 3 到 5 个百分点,但成本和延迟差了好几倍。因为 IR 生成本质是一个“翻译”任务,不是“推理”任务,它对模型的深度推理能力要求不高,对格式遵循能力要求高。选一个格式遵循好的中等模型就够了。
温度参数建议设成 0 或者接近 0。IR 生成不需要创造性,需要的是稳定性。温度设高了,同样的输入每次生成的 IR 可能不一样,虽然可能都对,但给调试和复现带来麻烦。设成 0 之后,同样的输入基本稳定输出同样的 IR,排查问题方便很多。
批量处理场景下,IR 生成可以并发,但执行引擎建议串行。因为执行引擎是纯函数,并发本身没问题,但如果多个 IR 之间有共享资源(比如同一个产品列表),并发执行可能引入资源竞争。串行执行虽然慢一点,但确定性更有保障。如果确实需要并发,把共享资源做成只读快照,每个执行线程拿自己的副本。
4.3 一个完整的端到端案例
拿一个实际场景走一遍。需求是“从客服工单里抽取客户诉求、判断优先级、生成处理建议”。用户输入是一段工单文本。
第一步,IR 生成器收到需求,输出 IR:
{ "version": "1.0", "operations": [ {"op": "extract", "source": "input.text", "target": "customer_request", "type": "string"}, {"op": "classify", "source": "input.text", "target": "priority", "type": "enum", "options": ["P0", "P1", "P2", "P3"]}, {"op": "map", "source": "priority", "target": "suggestion", "type": "string", "constraint": "priority_to_suggestion_map"} ] }第二步,静态校验器检查。语法合法,操作类型都在允许列表内,priority_to_suggestion_map这个约束资源存在,字段引用顺序正确(priority在第二个操作定义,第三个操作引用它,顺序没问题)。校验通过。
第三步,执行引擎执行。extract从文本里抽出客户诉求,classify判断优先级,map根据优先级查映射表生成处理建议。整个过程没有任何 AI 参与,纯规则执行,输入相同必然输出相同。
这个案例里,AI 只出现了第一次,就是生成 IR 那一步。后面所有环节都是确定性的。如果最终结果不对,你只需要检查 IR 是不是翻译错了,而 IR 是结构化的、可读的、可校验的,排查成本比面对一个黑盒输出低得多。
5. 常见问题与排查技巧实录
5.1 IR 生成失败的高频原因与对策
IR 生成失败是这套方案里最常见的故障点,我整理了一张速查表,覆盖了 90% 以上的失败场景。
| 失败现象 | 根本原因 | 解决对策 |
|---|---|---|
| 返回内容不是 JSON | 模型加了说明文字或代码块标记 | 提示词强调“只输出 JSON”,解析前剥离代码块 |
| JSON 合法但字段缺失 | 模型漏了必填字段 | 提示词里把必填字段列成清单,校验器拦截 |
| 操作类型不在允许列表 | 模型自创了操作类型 | 提示词里明确列出允许的操作类型 |
| enum 没有 options | 模型忘了枚举值 | 提示词里强调 enum 必须带 options |
| 字段引用顺序错误 | 模型没理解依赖关系 | 提示词里加一个多字段依赖的示例 |
| 生成的 IR 语义跑偏 | 提示词示例覆盖不足 | 补充对应场景的示例,覆盖边界情况 |
这张表里,“语义跑偏”是最难排查的,因为 IR 语法完全合法,校验器也拦不住,只有执行完发现结果不对才知道。我的经验是,语义跑偏基本都源于示例覆盖不足。你给 AI 的示例里没有某个场景,它就会用自己的理解去补,补出来的东西往往和你的预期不一致。解决办法很直接:每遇到一次语义跑偏,就把这个场景补成一个新示例。示例库越丰富,语义跑偏越少。我这边维护了一个 20 多个示例的库,覆盖了业务里所有典型场景,语义跑偏率降到了 1% 以下。
5.2 执行阶段的隐蔽陷阱
执行阶段虽然确定性高,但也不是完全没有坑。我踩过最隐蔽的一个坑是类型隐式转换。IR 里定义type: "number",但 AI 抽取出来的值可能是字符串"3",执行引擎如果没做类型转换,后续做数值比较时就会出问题。"3" > "10"在字符串比较下是 true,在数值比较下是 false,这种 bug 极难发现。
对策是在执行引擎的每个操作入口做严格的类型校验和转换。extract操作如果声明了type: "number",抽出来的值必须能转成数字,转不了就报错,不要静默放过。宁可报错让上游修 IR,也不要让错误数据流到下游。
另一个坑是空值处理。AI 抽取时可能返回空字符串或 null,执行引擎如果没处理,后续操作可能因为空值崩溃。我的做法是在 IR 里给每个字段加一个nullable标记,默认 false,不允许空值。如果业务上确实可能为空,显式标成 true,执行引擎对 nullable 字段做特殊处理。这样空值问题在编译阶段就被显式化了,不会在执行时突然冒出来。
5.3 性能与成本的实测数据
我拿一个中等规模的数据处理任务做了对比测试,输入是 1000 条工单文本,任务是抽取三个字段并分类。直接让 AI 出结果,平均每条耗时 1.2 秒,成本按 token 算,1000 条大概 3 块钱,字段级正确率 91%。用编译器方案,IR 生成每条耗时 0.8 秒,执行阶段每条耗时 0.05 秒,总耗时 0.85 秒,成本 2 块钱,字段级正确率 98.5%。
正确率提升主要来自两点:一是 IR 校验拦掉了一批格式错误,二是执行阶段的确定性消除了模型输出的随机波动。成本降低是因为执行阶段不调 AI,只有 IR 生成调一次,而且 IR 生成可以用更便宜的模型。耗时降低是因为执行阶段几乎不花时间,整体耗时基本就是 IR 生成的时间。
提示:这个数据是在特定任务上测的,不同任务的数据会有差异。但趋势是一致的:编译器方案在结构化任务上,正确率、成本、延迟三个指标全面优于直接让 AI 出结果。
5.4 什么场景不适合用编译器方案
说了这么多好处,也得说说边界。编译器方案不适合三类场景。第一类是开放式生成任务,比如写文案、编故事,这类任务没有明确的对错标准,IR 没法表达。第二类是强推理任务,比如复杂数学证明、多步逻辑推理,这类任务需要模型现场推理,IR 的预定义操作覆盖不了。第三类是需求频繁变化的场景,IR 的操作类型和语法是相对固定的,如果业务需求三天两头变,维护 IR 语法的成本会很高。
判断标准很简单:如果你的任务能被拆解成有限种类的确定性操作,且操作之间的组合方式相对稳定,那编译器方案就适用。反之,如果任务的核心价值就在于模型的开放性和推理能力,那还是老老实实让 AI 直接干活,用其他方式管理不确定性。
6. 我在这套方案上踩过的几个真实坑
第一个坑是过早追求 IR 的完备性。我一开始设计 IR 的时候,想着要把所有可能用到的操作都覆盖进去,结果定义了 20 多种操作类型,AI 生成 IR 的正确率反而掉到了 70% 以下。后来砍到 8 种,正确率立刻回到 95%。这个教训是:IR 的操作类型不是越多越好,够用就行,每多一种都是 AI 的学习负担。
第二个坑是校验器写得太宽松。早期版本我只校验了 JSON 语法和必填字段,没校验字段引用和循环依赖,结果执行阶段经常报“字段不存在”或者死循环。加上这两项校验之后,执行阶段的报错率降了 90%。校验器是这套方案的安全阀,宁可校验严一点,把问题拦在编译阶段,也不要让问题漏到执行阶段。
第三个坑是提示词里的示例和业务字段名不一致。我早期写示例的时候随手用了field_a、field_b这种占位名,结果 AI 生成 IR 时也跟着用field_a,和业务实际字段名对不上,校验器全拦下来了。后来把示例里的字段名改成和业务一致的,这个问题就消失了。示例里的命名规范会被 AI 学走,所以示例必须用真实字段名。
第四个坑是忽略了 IR 的版本管理。IR 语法升级之后,老版本的 IR 可能没法在新版编译器上执行。我后来在 IR 里加了version字段,编译器根据版本号走不同的解析逻辑,老 IR 继续能用。这个改动不大,但省了很多兼容性麻烦。任何会被持久化的数据结构,第一天就要考虑版本管理。
这套方案我前后迭代了大概三个月,从最初的概念验证到能稳定跑生产,中间踩的坑基本都在这了。核心体会就一句话:AI 的不确定性不是靠更强的模型来消除的,是靠架构设计来隔离的。把 AI 关在一个可控的边界内,让它只做它擅长的事,剩下的交给确定性程序,正确率这件事就不用再赌了。