为什么我放弃了传统方案,改用 GPT-5.1
起因:一个让我头疼了三周的文本解析需求
先说说我为什么折腾这个。我们公司有个内部系统,每天要处理几百封客户发来的半结构化邮件——订单确认、退换货申请、地址变更之类的。传统方案是我写的一套 Python 解析器,正则表达式加规则引擎,大约 1200 行代码,从客户邮件里抽取订单号、SKU、数量、收货地址这些字段。
这东西维护了两年,我快被它折磨死了。上个月客户邮件里出现了一种新格式:“订单 2024-XXXX 号(总共 3 件商品)”,我的正则 订单\s*\d{4}-\d{4} 直接匹配不上,因为有人用中文括号,有人夹了空格。光是为各种括号、空格、全角半角字符打补丁,我每周就要花好几个小时。
我当时的想法很简单:这种脏活累活,为什么不丢给大模型干?但我一直没换,是担心两点:成本和延迟。每天几百封邮件,每封都调一次 API,账单会不会爆炸?延迟会不会拖垮流水线?
于是我拿 GPT-5.1 做了试点,结果三周后我把那 1200 行解析代码整个删掉了。下面是完整的过程,包括我踩的坑。
第一步:先做基准测试,别急着动手
我的建议是,不管你要替换什么传统方案,先量化老方案的表现。我统计了老解析器过去 30 天的数据:
- 处理邮件 9,842 封
- 解析失败需要人工介入的:417 封(失败率 4.2%)
- 平均处理延迟:8ms
- 每月维护工时:约 15 小时
有了这个基准,后面才能客观判断新方案到底值不值。
第二步:写第一个提取 Prompt
我的第一版 Prompt 非常朴素:
从下面的客户邮件中提取以下字段,以 JSON 输出:
order_id(订单号), sku_list(商品SKU和数量), address(新收货地址,如有), intent(订单确认/退货/地址变更/其他)
邮件内容:
{email_body}
结果让我惊喜又踩坑。惊喜的是,那些老解析器搞不定的奇葩格式,它几乎全对。坑是第一版跑下来我发现了三个问题:
- 它太“热心”了。邮件里没提地址,它有时会编一个出来。
- SKU 大小写不稳定。客户写
abc-123,它有时输出ABC-123。 - 中文引号里的订单号偶尔漏掉。
第三步:用结构化输出解决格式问题
针对第一个问题,我在 Prompt 里加了明确约束:
规则:
1. 只提取邮件中明确出现的信息,找不到的字段填 null,禁止推测
2. SKU 保持原样输出,不要改变大小写
3. 输出必须是合法 JSON,不要包含任何解释文字
更关键的一步是我改用了 API 的结构化输出功能,直接定义 JSON Schema 强制输出格式,而不是靠提示词“求”它输出合法 JSON。以 Python SDK 为例:
from openai import OpenAI
client = OpenAI()
response = client.chat.completions.parse(
model="gpt-5.1",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": email_body},
],
response_format=ExtractionResult, # Pydantic 模型
)
result = response.choices[0].message.parsed
其中 ExtractionResult 是个 Pydantic 模型:
from pydantic import BaseModel
class SkuItem(BaseModel):
sku: str
quantity: int
class ExtractionResult(BaseModel):
order_id: str | None
sku_list: list[SkuItem]
address: str | None
intent: str
用了这个之后,JSON 解析失败的次数直接归零,代码里那段“容错解析 JSON”的 hack 也删了。
第四步:处理成本和延迟
这是我最担心的部分,实际测下来的数据:
- 每封邮件平均输入 800 token、输出 150 token
- 单封调用约 0.15 秒(用 GPT-5.1 的较低推理档位,因为我这个任务不需要深度思考)
- 每天按 500 封算,月成本大概几十美元——远低于我每月 15 小时维护工时的成本
延迟上我用了个混合策略:先用一个极简正则判断邮件类型,格式标准的邮件仍走老逻辑(快且免费),只有老解析器抛出低置信度或解析失败时,才降级到模型调用。这样大约 70% 的流量根本不花钱,API 只兜底那些“疑难杂症”。后来我发现模型兜底的准确率太高了,索性反过来了——模型为主,正则只做前置过滤垃圾邮件。
第五步:加一层验证,别完全信任输出
我现在仍然保留了一层校验:
def validate(result: ExtractionResult, raw_email: str) -> bool:
if result.order_id and not re.match(r"\d{4}-\d{4}", result.order_id):
return False
if result.sku_list:
for item in result.sku_list:
if item.quantity < 1 or item.quantity > 999:
return False
return True
校验失败的邮件进人工队列。上线第一个月,人工介入率从老方案的 4.2% 降到了 0.3%,而且剩下那 0.3% 基本是邮件本身写不清楚、人也得打电话问的情况。
实用建议
- Prompt 里明确“找不到就填 null”,否则模型会脑补,这在数据提取场景是致命的。
- 用结构化输出,别靠正则解析模型返回的 JSON 字符串,省掉一整类 bug。
- 保留确定性校验。模型负责理解,规则负责把关,两者结合才是生产级方案。
- 控制输出字段数量。我一开始想让模型顺便输出“客户情绪”,结果干扰了主任务的准确率,字段拆开处理更稳。
- 先用 50~100 个历史难例做离线评估再上线,我拿那 417 封失败邮件当测试集,新方案一次通过率 96%。
诚实的局限
- 延迟确实回不去了。8ms 变成 150ms,如果你做的是毫秒级实时系统,传统方案可能仍然更合适。
- 成本随量线性增长。如果是每天百万级请求,账单要仔细算,可能需要模型+规则的混合架构。
- 行为不完全稳定。同样的邮件,两次调用可能给出细微不同的结果(比如地址的空格差异),对强一致性场景要加后处理归一化。
- 调试体验不同了。正则错了你能一眼看出来,模型错了你只能改 Prompt 然后重新跑测试集,需要建立一套回归测试的习惯。
总结一句:如果你的传统方案里堆满了“为千奇百怪的输入打补丁”的代码,那它大概率就是模型该接管的那部分。把确定性的部分留给规则,把理解语言的部分交给模型——这个分工是我这次重构最大的收获。