首页归档标签关于
← 返回首页

结构化输出与提示词工程:让模型结果能直接进数据库

#AI#大模型#提示词

「请提取文章标题和作者,以 JSON 格式返回」——这句话在 demo 里能跑,在生产里会让你写一堆正则去收拾残局:有时候外面包了 ```json,有时候多了句解释,有时候字段名变成了中文。让模型输出能直接被程序消费,靠的不是更恳切的措辞,而是约束与验证。

一、把 schema 放在协议层,而不是提示词里

现在主流接口都支持 JSON 输出约束或 JSON Schema(部分实现通过约束解码在采样阶段保证格式合法)。能用的话优先用,这是从「希望它遵守」变成「不可能不遵守」的区别。

schema = {
    "type": "object",
    "properties": {
        "title":      {"type": "string"},
        "tags":       {"type": "array", "items": {"type": "string"}, "maxItems": 5},
        "difficulty": {"type": "string", "enum": ["beginner", "intermediate", "advanced"]},
        "summary":    {"type": "string"},
        "has_code":   {"type": "boolean"}
    },
    "required": ["title", "tags", "difficulty", "summary", "has_code"],
    "additionalProperties": False
}

字段设计有几个很实用的约定:

  • 枚举优于自由文本:difficulty 用三个固定值,别让模型自由发挥成「中等偏上」;
  • 可空要明确:信息缺失时返回 null 或固定哨兵值,并在描述里写清,避免模型编一个看起来合理的值;
  • 别塞置信度当摆设:如果不会用它做分流,就别让模型输出一个没有校准意义的 0.87;
  • 字段名用 snake_case 且稳定,下游表结构直接对应。

二、少样本不是抄几句话,是覆盖边界

示例的作用是告诉模型「拿不准时怎么办」。所以挑样例时,优先放边界情况而不是最典型的成功案例:

样例类型要教给模型的事
信息完整常规字段怎么填
字段缺失该返回 null 而不是编造
多个候选值选哪一个、为什么
长文本 / 含代码块别把正文混进摘要
明显无关的输入走空结果或拒绝分支

示例数量一般 2~5 条就够,太多会挤占上下文,还会让模型过度拟合样例的措辞。

三、把提示词当代码管理

提示词一旦进了生产,就具备了代码的所有特征:会改、会回归、会影响下游、需要评审。最低限度的三件事:

  1. 版本化:提示词模板存成文件,带版本号,发布记录里能查到用的是哪版;
  2. 回归集:攒 30~50 条真实输入 + 期望输出,每次改提示词跑一遍,看字段准确率;
  3. 失败样本回流:线上解析失败或人工修正的 case,直接补进回归集。
prompts/
  extract_meta.v3.txt
  extract_meta.v4.txt
evals/
  extract_meta_cases.jsonl     # {"input": "...", "expect": {...}}
scripts/
  run_eval.py                  # 输出字段级准确率与失败明细

有了这套东西,「把语气改得更强硬一点」这类改动才有依据判断是否真的变好,而不是凭感觉。

四、服务端解析与容错:PHP 版

即使开了 schema 约束,也建议保留一层防御性解析。下面是这类接口的骨架,重点是超时、状态码、JSON 校验和降级:

function extractMeta(string $text, string $prompt): array {
    $payload = [
        'model' => getenv('LLM_MODEL') ?: 'default',
        'messages' => [
            ['role' => 'system', 'content' => $prompt],
            ['role' => 'user',   'content' => $text],
        ],
        'response_format' => ['type' => 'json_object'],
        'temperature' => 0,
    ];
    $ch = curl_init(getenv('LLM_ENDPOINT'));
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json',
                                   'Authorization: Bearer ' . getenv('LLM_KEY')],
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 20,
        CURLOPT_CONNECTTIMEOUT => 5,
    ]);
    $raw = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($raw === false || $code < 200 || $code >= 300) return fallbackMeta($text);

    $resp = json_decode($raw, true);
    $content = $resp['choices'][0]['message']['content'] ?? '';
    // 兜底:去掉偶发的 ```json 包裹
    $content = preg_replace('/^```(?:json)?\s*|\s*```$/', '', trim($content));
    $data = json_decode($content, true);
    if (!is_array($data) || !isset($data['title'])) return fallbackMeta($text);

    return [
        'title'  => mb_substr((string)$data['title'], 0, 120),
        'tags'   => array_slice((array)($data['tags'] ?? []), 0, 5),
        'source' => 'llm',
    ];
}

function fallbackMeta(string $text): array {
    // 降级:取第一行做标题,保证下游永远拿得到结构
    return ['title' => mb_substr(strtok($text, "\n") ?: '未命名', 0, 120),
            'tags' => [], 'source' => 'fallback'];
}

几个容易被忽略的点:temperature 设 0 让提取类任务更稳定;密钥只从环境变量读,不进代码库;降级函数必须返回同构的结构,否则下游要写两套分支。

五、检查清单

  • 能用 schema 约束就不用自然语言约束
  • 枚举、可空、必填在 schema 里写死
  • 少样本覆盖边界与失败分支
  • 提示词有版本、有回归集
  • 解析失败率、降级比例接入监控

结构化输出的目标不是「模型每次都对」,而是它出错时系统仍然可预期。把这两件事分开设计,后面接数据库、接缓存、接队列都会轻松很多。