提示工程进阶:结构化输出与工具调用


当大模型从聊天框走向生产系统,提示词的目标就变了:不再追求「回答得漂亮」,而是结果可以被程序稳定解析。这篇讲两个最常踩坑的环节——结构化输出和工具调用。

结构化输出的三种做法

按可靠性从低到高排序:

1. 靠提示词要求返回 JSON

只输出 JSON,不要任何解释,格式如下:
{"title": string, "tags": string[], "summary": string}

实现最快,但模型经常在前后加一句「好的,以下是结果」或者在 JSON 里写注释,导致解析失败。适合快速验证。

2. 限定解码(Constrained Decoding)

把 JSON Schema 编译成状态机,在每一步解码时把不合法的 token 概率直接置零。这样生成的文本在语法上一定是合法 JSON。

{
  "type": "object",
  "properties": {
    "title": { "type": "string" },
    "tags": { "type": "array", "items": { "type": "string" } },
    "summary": { "type": "string" }
  },
  "required": ["title", "tags", "summary"]
}

主流推理框架(如 vLLM 的 guided decoding、各家的 JSON Mode)都支持。代价是约束太复杂时可能略微影响生成质量。

3. 结构化输出 + 事后校验

即使语法合法,语义仍可能不满足要求。所以解析之后一定要过一遍校验:字段是否缺失、枚举值是否越界、数值是否在合理区间。校验失败就带上错误信息重试一次,通常能修好九成问题。

for attempt in range(2):
    raw = call_model(prompt)
    try:
        data = Schema.model_validate_json(raw)
        break
    except ValidationError as e:
        prompt = f"{prompt}\n上一次输出不合法:{e}\n请只返回合法 JSON。"

工具调用:把模型的判断放进确定性流程

工具调用的本质是让模型输出一个结构化的调用意图,由外部代码执行。几个实践要点:

  • 描述比命名重要。工具名要和函数对应,但真正影响模型选择的是描述。描述里要写清「什么时候用」「什么时候不要用」。
  • 参数尽量扁平。嵌套三层的参数结构,模型填错的概率会明显上升。宁可拆成多个工具。
  • 区分「无参数可填」和「不需要调用」。把工具参数全部设为可选,模型容易在信息不足时硬编一个默认值;更好的做法是让它在缺少必要信息时先反问。
  • 执行结果要回灌。把工具返回的原始结果(或摘要)作为新的消息交回模型,并且在提示词里说明「工具结果可能不完整或出错,请据此判断」。
  • 限制步数。给循环设一个上限(例如 8 步),并让模型在达到上限时总结当前进展,避免无休止地反复调用。
tools = [
    {
        "type": "function",
        "function": {
            "name": "query_order",
            "description": "按订单号查询订单状态;仅当用户提供了订单号时使用,不要猜测订单号。",
            "parameters": {
                "type": "object",
                "properties": {"order_id": {"type": "string", "description": "纯数字订单号"}},
                "required": ["order_id"],
            },
        },
    }
]

别忽略错误路径

生产环境里,模型调用失败、超时、被限流都是常态。至少要准备:

  • 超时与重试(带指数退避),重试次数不要超过 2 次。
  • 降级方案:解析失败时回退到纯文本输出,再走人工兜底。
  • 日志:把提示词、原始输出、校验错误都记下来,这是后续调优唯一可靠的依据。

小结

结构化输出的关键在约束与校验,工具调用的关键在描述与边界。提示词写得再漂亮,也比不上「让非法输出根本生成不出来,生成出来就一定能被发现」。