把自然语言整理成固定字段,是 AI 应用中很常见的一类任务:从邮件提取客户、金额和截止日期,从工单提取优先级和问题类型。真正可靠的程序不能只打印模型文本,而要让结果经过解析、校验和复核。本篇把结构化输出、提示词约束和基础安全边界组合成一个最小数据抽取器,并提供离线演示,读者无需密钥也能先验证程序流程。
先定义输入和边界 本项目从一段订单留言中提取三个字段:customer、amount 和 currency。字段必须是字符串、数字和三字母货币代码;缺失信息使用 null,不能根据常识猜测。模型只负责把文字转换成候选 JSON,程序负责校验,最终是否写入业务系统仍由人或后续规则决定。
这个边界很重要。模型返回的是“不可信输入”,即使它看起来像合法 JSON,也可能少字段、类型错误,或者把用户文字中的指令当成程序要求。把解析和校验放在模型外部,才能让失败变得可见、可测试。
用纯 Python 实现最小流水线 先创建文件 extractor.py。下面的 demo_model 模拟模型响应,故意保留一个不合法的金额类型,用来验证校验逻辑。真实项目中,只需要替换这个函数为已有的模型适配器;不要改变校验和复核层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 import jsonimport refrom typing import Any SCHEMA_KEYS = {"customer" , "amount" , "currency" } def build_prompt (text: str ) -> str : return f"""请从订单留言中提取 JSON,只允许包含 customer、amount、currency。 缺失信息填 null,不要猜测;amount 必须是数字,currency 必须是三字母大写代码。 订单留言:\n{text} """ def demo_model (prompt: str ) -> str : return json.dumps({ "customer" : "林晓" , "amount" : "1280" , "currency" : "CNY" , }, ensure_ascii=False ) def validate_candidate (raw: str ) -> tuple [dict [str , Any ] | None , list [str ]]: errors: list [str ] = [] try : value = json.loads(raw) except json.JSONDecodeError: return None , ["响应不是合法 JSON" ] if not isinstance (value, dict ): return None , ["顶层结果必须是对象" ] if set (value) != SCHEMA_KEYS: errors.append("字段必须且只能包含 customer、amount、currency" ) customer = value.get("customer" ) amount = value.get("amount" ) currency = value.get("currency" ) if customer is not None and not isinstance (customer, str ): errors.append("customer 必须是字符串或 null" ) if amount is not None and (isinstance (amount, bool ) or not isinstance (amount, (int , float ))): errors.append("amount 必须是数字或 null" ) if currency is not None and ( not isinstance (currency, str ) or not re.fullmatch(r"[A-Z]{3}" , currency) ): errors.append("currency 必须是三字母大写代码或 null" ) return (value if not errors else None ), errors def extract (text: str , approve: bool = False ) -> dict [str , Any ]: prompt = build_prompt(text) raw = demo_model(prompt) candidate, errors = validate_candidate(raw) if errors: return {"status" : "rejected" , "errors" : errors, "raw" : raw} if not approve: return {"status" : "pending_review" , "data" : candidate} return {"status" : "approved" , "data" : candidate} if __name__ == "__main__" : message = "林晓留言:请按 1280 CNY 处理本月订单。" print (json.dumps(extract(message), ensure_ascii=False , indent=2 ))
运行命令如下:
程序会输出 pending_review,而不是直接声称数据可信。把 extract(message, approve=True) 作为另一次明确操作,才会得到 approved。示例中的模型响应金额是字符串,因此校验会返回 rejected;这正是验证层应该捕获的问题,而不是悄悄转换后继续写库。
接入真实模型时保留适配层 真实调用可以放在独立的 model_adapter.py 中。密钥只能从环境变量读取,模型名称也不要硬编码成项目业务逻辑的一部分:
1 2 3 4 5 6 7 8 9 10 import osAPI_KEY = os.environ["AI_API_KEY" ] MODEL_NAME = os.environ["AI_MODEL_NAME" ] def call_model (prompt: str ) -> str : raise NotImplementedError("请按服务商官方文档实现调用" )
适配器只负责请求和取回文本,validate_candidate 不应依赖某个服务商的响应对象。这样可以在没有网络或密钥时使用 demo_model 做单元测试,也能在更换模型服务时避免修改业务规则。对模型响应设置超时、捕获限流和网络异常,则属于调用层的责任;异常时应返回可识别的失败状态,不要伪造空数据。
人工复核和日志应该记录什么 抽取结果进入数据库前,至少要展示原文、候选 JSON、校验错误和模型版本,供复核者判断。日志可以记录请求编号、耗时、状态和错误类型,但应先脱敏;客户姓名、订单号等个人或业务信息不要无条件写入普通日志。复核通过后再执行写入,写入函数也应再次检查字段,而不是相信上游已经校验过。
还可以为校验函数编写几个不依赖模型的测试:合法数字通过;金额为字符串拒绝;额外字段拒绝;货币代码为小写拒绝;缺失字段返回错误。模型输出具有随机性,测试应固定候选文本,不能把某一次生成结果当作稳定契约。
常见问题 为什么不直接让模型返回一句自然语言? 因为自然语言难以稳定解析,也无法明确区分缺失值和猜测值。固定 JSON 只是第一步,外部校验才是边界。
校验失败后要不要自动修复? 简单格式错误可以请求模型重试,但要限制次数,并重新完整校验。金额、身份和权限等高风险字段不要只依赖自动修复,应该转人工。
为什么示例不直接连接 API? 离线演示能验证核心程序,不需要暴露密钥,也不会把某个服务商的易变参数误当成通用接口。接入时应以所选服务商当前官方文档为准。
小结 一个可用的数据抽取器至少有四层:提示词规定目标,模型产生候选文本,程序解析并校验,人工或业务规则决定是否采纳。把模型当作不可信输入源,把确定性规则放在代码中,才能让结构化输出从“看起来正确”变成“可以复核、可以测试、可以拒绝”。