一、引言:从“聊天”到“工程”的范式转换

许多初入大模型应用开发的工程师,对Prompt Engineering的理解停留在“写一段自然语言描述,让模型照着做”的层面。当业务场景从简单的问答演进到结构化数据提取、API参数生成、多步推理等复杂任务时,这种“软约束”方式的脆弱性就暴露无遗——同样的Prompt,换一个模型版本输出格式就变了;加一句约束,模型在其他维度上的表现就开始漂移。

问题的根源在于:自然语言指令本质上是一种软约束,模型可以“理解”,也可以“遗忘”。 当我们需要模型输出严格符合Schema的数据、执行确定性的业务逻辑、或者遵循多层次的规则时,就必须引入结构化的约束手段,把“模型自由发挥的空间”压缩到最小。

本文将系统讲解5种经过生产验证的结构化约束策略,每一种都配有可运行的代码和真实的踩坑记录。


二、策略一:Markdown结构模板——让模型“填空”,而非“作文”

2.1 核心思想

大多数模型在预训练阶段已经深度学习了Markdown语法。利用这一特性,我们可以给模型提供一个带占位符的Markdown模板,要求模型按照模板结构“填空”,而不是自由生成Markdown。

本质区别:自由生成的Markdown,模型可能新增章节、调整顺序、或者遗漏字段;而填空模式下,模型的任务被简化为“在每个固定的标题下填入内容”,结构稳定性大幅提升。

2.2 实战模板设计

以智能客服的问题分析报告为例:

# 用户问题分析报告

## 问题分类
[请填写:售后/物流/产品使用/活动咨询/投诉/其他]

## 紧急程度
[请填写:高/中/低]

## 关键实体提取
- 订单号:[请填写订单号,若无则填"无"]
- 产品名称:[请填写产品名称,若无则填"无"]
- 用户诉求:[请用一句话概括用户的核心诉求]

## 历史工单关联
[请填写:是否有相似历史工单,若有则填写工单编号]

## 推荐解决方案
[请填写:1-3条具体建议,每条以"- "开头]

## 转人工建议
[请填写:是/否,若填"是"需说明原因]

关键设计原则:

  • 每个占位符都有默认值(如“无”),防止模型留空
  • 枚举字段列出所有可选值,消除歧义
  • 输出长度有隐性约束(如“1-3条”)

2.3 代码实现

from openai import OpenAI
import re

client = OpenAI(api_key="your-api-key")

def analyze_with_template(user_query: str) -> dict:
    """使用Markdown模板进行结构化输出"""
    
    template = """
# 用户问题分析报告

## 问题分类
[请填写:售后/物流/产品使用/活动咨询/投诉/其他]

## 紧急程度
[请填写:高/中/低]

## 关键实体提取
- 订单号:[请填写订单号,若无则填"无"]
- 产品名称:[请填写产品名称,若无则填"无"]
- 用户诉求:[请用一句话概括用户的核心诉求]

## 历史工单关联
[请填写:是否有相似历史工单,若有则填写工单编号]

## 推荐解决方案
[请填写:1-3条具体建议,每条以"- "开头]

## 转人工建议
[请填写:是/否,若填"是"需说明原因]
"""
    
    system_prompt = f"""你是一个用户问题分析专家。
请根据用户问题,严格按照以下Markdown模板填写分析报告。
【重要】:
1. 不要修改模板结构,不要新增章节,不要删除章节
2. 只在每个章节的[请填写...]位置填入内容
3. 保持Markdown格式不变
4. 如果信息不足,填写"信息不足"而非猜测

模板内容如下:
{template}
"""
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"用户问题:{user_query}"}
        ],
        temperature=0.1
    )
    
    output = response.choices[0].message.content
    
    # 解析Markdown为结构化数据
    return parse_markdown_to_dict(output)

def parse_markdown_to_dict(markdown_text: str) -> dict:
    """从Markdown文本中提取结构化字段"""
    result = {}
    
    # 提取问题分类
    category_match = re.search(r'## 问题分类\s*\n\s*\[?([^\]]+)\]?', markdown_text)
    if category_match:
        result['category'] = category_match.group(1).strip()
    
    # 提取紧急程度
    urgency_match = re.search(r'## 紧急程度\s*\n\s*\[?([^\]]+)\]?', markdown_text)
    if urgency_match:
        result['urgency'] = urgency_match.group(1).strip()
    
    # 提取订单号
    order_match = re.search(r'-\s*订单号:\[?([^\]]+)\]?', markdown_text)
    if order_match:
        result['order_id'] = order_match.group(1).strip()
    
    # 提取用户诉求
    demand_match = re.search(r'-\s*用户诉求:\[?([^\]]+)\]?', markdown_text)
    if demand_match:
        result['demand'] = demand_match.group(1).strip()
    
    # 提取转人工建议
    escalate_match = re.search(r'## 转人工建议\s*\n\s*\[?([^\]]+)\]?', markdown_text)
    if escalate_match:
        result['escalate'] = escalate_match.group(1).strip()
    
    return result

# 使用示例
if __name__ == "__main__":
    query = "我前天买的那个电饭煲,今天收到发现盖子合不上,能换吗?"
    result = analyze_with_template(query)
    print(result)
    # 输出示例:
    # {
    #   "category": "售后",
    #   "urgency": "高",
    #   "order_id": "信息不足",
    #   "demand": "用户希望更换有质量问题的电饭煲",
    #   "escalate": "是,涉及退换货需人工处理"
    # }

2.4 进阶技巧:XML标签模板

如果Markdown模板在某些模型上表现不稳定,可以改用XML标签作为结构边界。XML的层级结构更明确,模型在预训练中对XML的格式敏感性也足够强。

XML_TEMPLATE = """
<analysis>
  <category></category>
  <urgency></urgency>
  <entities>
    <order_id></order_id>
    <product_name></product_name>
    <user_demand></user_demand>
  </entities>
  <solution></solution>
  <escalate></escalate>
</analysis>
"""

经验值:GPT-4系列对Markdown格式的遵从度在95%以上,而一些开源模型(如Qwen-7B)对XML格式更敏感。建议根据具体模型做A/B测试。


三、策略二:JSON Schema强制约束——函数调用范式的“平替”

3.1 核心思想

当我们需要模型输出严格的JSON格式时,最理想的方式是使用模型的**函数调用(Function Calling)**能力。但并非所有模型都支持函数调用,且函数调用的参数Schema在某些场景下不够灵活。

一种通用的替代方案是:在系统提示词中嵌入完整的JSON Schema,并配合低温度参数,强制模型输出符合Schema的JSON。

3.2 实战:电商订单信息提取

from openai import OpenAI
import json
from typing import Optional

client = OpenAI(api_key="your-api-key")

class OrderInfoExtractor:
    """订单信息提取器 - 基于JSON Schema强制约束"""
    
    JSON_SCHEMA = {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "订单编号,格式为ORD-YYYYMMDD-XXXX,若未提及则填null"
            },
            "customer_name": {
                "type": "string",
                "description": "客户姓名"
            },
            "phone": {
                "type": "string",
                "description": "手机号码,11位数字"
            },
            "order_items": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "product_name": {"type": "string"},
                        "quantity": {"type": "integer"},
                        "price": {"type": "number"}
                    },
                    "required": ["product_name", "quantity"]
                }
            },
            "issue_type": {
                "type": "string",
                "enum": ["质量问题", "物流问题", "退换货", "取消订单", "其他"],
                "description": "问题类型"
            }
        },
        "required": ["order_id", "issue_type"]
    }
    
    def extract(self, user_query: str) -> dict:
        system_prompt = f"""
你是一个订单信息提取助手。
请从用户问题中提取订单相关信息,并严格按照以下JSON Schema输出。
【硬性约束】:
1. 输出必须是合法的JSON,不要包含任何其他文字或Markdown标记
2. 所有字段必须符合Schema定义
3. 如果字段缺失,填null而非空字符串
4. 如果用户未提及任何订单信息,issue_type仍必须填写,可选"其他"

Schema定义:
```json
{json.dumps(self.JSON_SCHEMA, ensure_ascii=False, indent=2)}

再次强调:只输出JSON,不要加"```json"或任何解释文字。
“”"

    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_query}
        ],
        temperature=0.0  # 零温度,最大确定性
    )
    
    raw_output = response.choices[0].message.content
    
    # 清理可能的Markdown标记
    raw_output = re.sub(r'^```json\s*', '', raw_output)
    raw_output = re.sub(r'\s*```$', '', raw_output)
    
    try:
        return json.loads(raw_output)
    except json.JSONDecodeError as e:
        # 降级处理:尝试用正则提取
        return self._fallback_parse(raw_output)

def _fallback_parse(self, raw: str) -> dict:
    """当JSON解析失败时的降级策略"""
    # 尝试用正则提取关键字段
    result = {"order_id": None, "issue_type": "其他"}
    
    order_match = re.search(r'ORD-\d{8}-\w{4}', raw)
    if order_match:
        result["order_id"] = order_match.group(0)
    
    issue_match = re.search(r'(质量问题|物流问题|退换货|取消订单)', raw)
    if issue_match:
        result["issue_type"] = issue_match.group(0)
    
    return result

使用示例

extractor = OrderInfoExtractor()
result = extractor.extract(“我ORD-20260809-A3B2的订单怎么还没发货?”)
print(json.dumps(result, ensure_ascii=False, indent=2))


### 3.3 关键技巧:Schema描述中的“负向约束”

在Schema描述中,不仅要说明“应该填什么”,还要明确“不应该填什么”:

```python
"order_items": {
    "type": "array",
    "description": "仅提取用户明确提及的商品,不要推理或补充未提及的商品",
    "items": {...}
}

为什么有效:负向约束能有效抑制模型的“过度补全”倾向,尤其是在用户提供了部分信息时,模型容易“好心”地推测缺失字段。


四、策略三:Few-shot + 格式示例——用“示范”代替“说教”

4.1 核心思想

对于复杂的多步推理任务,“告诉模型怎么思考”往往不如“给模型看一个完整的思考过程”有效。Few-shot的核心逻辑是:用具体的输入-输出示例,让模型理解期望的思考路径和输出格式,而不是靠抽象规则描述。

4.2 实战:多步推理 + 自检机制

以售后判责场景为例——系统需要综合“购买时间、问题描述、历史记录”三个维度做出判责建议:

from openai import OpenAI

client = OpenAI(api_key="your-api-key")

class CustomerServiceJudge:
    """售后判责助手 - 使用Few-shot实现结构化推理"""
    
    FEW_SHOT_EXAMPLES = [
        {
            "input": {
                "purchase_date": "2026-07-15",
                "issue": "手机屏幕出现一条亮线,从未摔过",
                "history": "用户上个月因为充电问题咨询过一次"
            },
            "output": """
=== 推理过程 ===
1. 购买日期2026-07-15,今天2026-08-09,已使用25天,在15天换货期内
2. 问题描述为"亮线"且"从未摔过",属于非人为质量问题
3. 历史记录显示用户有1次咨询,非投诉性质

=== 判责结论 ===
- 责任归属:厂商质量问题
- 建议处理:换新(符合15天换货政策)
- 附加动作:无需转人工,自动生成换货工单
- 置信度:高
"""
        },
        {
            "input": {
                "purchase_date": "2024-03-10",
                "issue": "充电口接触不良,插上充电线后需要按住才能充电",
                "history": "无"
            },
            "output": """
=== 推理过程 ===
1. 购买日期2024-03-10,已使用超过1年,超出质保期
2. 充电口接触不良可能由长期插拔导致,属于正常使用磨损
3. 历史记录为空,用户首次反馈

=== 判责结论 ===
- 责任归属:用户正常使用损耗
- 建议处理:告知超出质保,提供付费维修选项
- 附加动作:推送维修服务链接
- 置信度:高
"""
        },
        {
            "input": {
                "purchase_date": "2026-06-01",
                "issue": "耳机断连,之前已经换过一次,现在又出现同样问题",
                "history": "用户于2026-06-15因断连问题换新过一次"
            },
            "output": """
=== 推理过程 ===
1. 购买日期2026-06-01,仍在质保期
2. 问题是重复出现的(换新后再次出现),可能存在批次性质量缺陷
3. 历史记录显示用户已换新一次,再次出现问题需要升级处理

=== 判责结论 ===
- 责任归属:厂商质量问题(重复故障)
- 建议处理:退款或升级为更高规格替代品
- 附加动作:标记为"敏感用户",转高级客服处理
- 置信度:中
"""
        }
    ]
    
    def judge(self, purchase_date: str, issue: str, history: str) -> dict:
        """执行判责推理"""
        
        # 构建Few-shot系统提示
        few_shot_text = ""
        for example in self.FEW_SHOT_EXAMPLES:
            few_shot_text += f"""
【输入示例】
购买日期:{example['input']['purchase_date']}
问题描述:{example['input']['issue']}
历史记录:{example['input']['history']}

【正确输出】
{example['output']}
"""
        
        system_prompt = f"""
你是一个售后判责专家。请按照以下推理流程处理每个案例:

【推理流程】
1. 检查购买时间,判断是否在质保/换货期内
2. 分析问题性质,判断是否属于质量问题
3. 查阅历史记录,识别是否为重复故障或敏感用户
4. 给出判责结论和建议处理方式

【Few-shot示例】
{few_shot_text}

【当前案例输入】
购买日期:{purchase_date}
问题描述:{issue}
历史记录:{history}

请输出完整的推理过程和判责结论,格式与示例保持一致。
"""
        
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": "请进行判责推理"}
            ],
            temperature=0.1
        )
        
        output = response.choices[0].message.content
        
        # 解析结构化输出
        return self._parse_judge_output(output)
    
    def _parse_judge_output(self, output: str) -> dict:
        """解析判责结果"""
        result = {
            "responsibility": "",
            "suggestion": "",
            "extra_action": "",
            "confidence": ""
        }
        
        # 提取关键字段
        resp_match = re.search(r'-\s*责任归属:(.+)', output)
        if resp_match:
            result["responsibility"] = resp_match.group(1).strip()
        
        sugg_match = re.search(r'-\s*建议处理:(.+)', output)
        if sugg_match:
            result["suggestion"] = sugg_match.group(1).strip()
        
        extra_match = re.search(r'-\s*附加动作:(.+)', output)
        if extra_match:
            result["extra_action"] = extra_match.group(1).strip()
        
        conf_match = re.search(r'-\s*置信度:(.+)', output)
        if conf_match:
            result["confidence"] = conf_match.group(1).strip()
        
        return result

4.3 关键技巧:示例的“边界覆盖”原则

Few-shot示例的选择不是越多越好,而是要覆盖边界情况。以上述判责场景为例,三个示例分别覆盖了:

  • 正常换货边界(15天内质量问题)
  • 质保过期边界(超1年正常损耗)
  • 重复故障边界(换新后复发,需要升级处理)

覆盖边界比覆盖常规情况更重要——模型在边界处的表现直接决定了系统的健壮性。


五、策略四:输出格式验证器——用代码“审查”模型

5.1 核心思想

永远不要相信模型会100%遵守格式要求。 无论Prompt写得多么完美,总有概率(通常2-5%)输出格式偏离。因此,在模型输出后增加一个格式验证器(Validator),自动检测并修正格式问题,是生产环境的必备防线。

5.2 实战:多层级格式验证器

import re
import json
from typing import Any, List, Tuple
from pydantic import BaseModel, ValidationError

class OutputValidator:
    """多层级输出格式验证器"""
    
    def __init__(self, strict_mode: bool = False):
        self.strict_mode = strict_mode
    
    def validate_json(self, text: str, schema: dict) -> Tuple[bool, Any]:
        """
        第一层:JSON格式验证
        第二层:Schema结构验证
        """
        # 第一层:JSON格式
        try:
            data = json.loads(text)
        except json.JSONDecodeError:
            # 尝试提取嵌入的JSON
            json_match = re.search(r'\{.*\}', text, re.DOTALL)
            if json_match:
                try:
                    data = json.loads(json_match.group(0))
                except:
                    return False, None
            else:
                return False, None
        
        # 第二层:Schema验证
        try:
            # 检查必填字段
            required = schema.get("required", [])
            for field in required:
                if field not in data or data[field] is None:
                    return False, None
            
            # 检查枚举值
            for field, field_schema in schema.get("properties", {}).items():
                if "enum" in field_schema:
                    if field in data and data[field] not in field_schema["enum"]:
                        data[field] = field_schema["enum"][0]  # 自动修正
        except Exception:
            return False, None
        
        return True, data
    
    def validate_format(self, text: str, format_type: str = "markdown") -> Tuple[bool, str]:
        """
        第三层:格式一致性验证(针对Markdown/文本结构)
        """
        if format_type == "markdown":
            # 检查是否包含所有必需章节
            required_sections = [
                "## 问题分类",
                "## 紧急程度",
                "## 关键实体提取",
                "## 推荐解决方案"
            ]
            for section in required_sections:
                if section not in text:
                    if self.strict_mode:
                        return False, f"缺少章节: {section}"
                    else:
                        # 尝试在末尾追加缺失章节
                        text += f"\n\n{section}\n[信息不足]"
            
            return True, text
        
        return True, text

5.3 实战:LangChain输出解析器

LangChain提供了更成熟的输出解析方案,可以无缝集成到生产链路中:

from langchain.output_parsers import PydanticOutputParser
from langchain.prompts import PromptTemplate
from pydantic import BaseModel, Field

class CustomerIssue(BaseModel):
    category: str = Field(description="问题分类,可选值:售后/物流/产品使用/活动咨询/投诉/其他")
    urgency: str = Field(description="紧急程度,可选值:高/中/低")
    summary: str = Field(description="问题摘要,30字以内")
    needs_escalation: bool = Field(description="是否需要转人工")

# 自动生成格式说明
parser = PydanticOutputParser(pydantic_object=CustomerIssue)

prompt = PromptTemplate(
    template="""分析以下用户问题,按指定格式输出。
    {format_instructions}
    用户问题:{query}
    """,
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions()}
)

# 使用示例
model = OpenAI(temperature=0.0)
query = "我买的鞋子尺寸不对,想换小一码"
_input = prompt.format_prompt(query=query)
output = model(_input.to_string())
result = parser.parse(output)
print(result)
# CustomerIssue(category='售后', urgency='中', summary='用户购买鞋子尺寸不合适需要换货', needs_escalation=True)

六、策略五:思维链(CoT)约束——强制展示推理过程

6.1 核心思想

复杂推理任务中,模型直接给出最终答案的准确率远低于“先展示推理过程,再给出答案”的方式。思维链的核心价值在于:强制模型暴露推理步骤,让错误更容易被识别和修正。

更进阶的做法是自一致性(Self-Consistency):让模型生成多条推理路径,选取出现最频繁的答案。

6.2 实战:复杂逻辑推理(物流时效预测)

class LogisticsEstimator:
    """物流时效预测 - 强制思维链推理"""
    
    def estimate(self, origin: str, dest: str, product_type: str, holiday: bool) -> dict:
        system_prompt = """
你是一个物流时效分析专家。
请按照以下步骤进行推理,每一步都要明确输出。

【推理步骤】
步骤1:检查"发货地与收货地"是否在同一省份
步骤2:判断是否为偏远地区(新疆、西藏、内蒙古、青海、甘肃)
步骤3:检查是否为节假日,节假日需增加2-3天
步骤4:根据产品类型调整时效(生鲜类 +1天,大件商品 +1天)
步骤5:综合计算最终预估时效

【输出格式】

=== 推理过程 ===
步骤1:…
步骤2:…
步骤3:…
步骤4:…
步骤5:…

=== 最终结论 ===

  • 基础时效:X天
  • 额外加成:Y天
  • 总预估时效:Z天
  • 预估送达日期:YYYY-MM-DD
"""
        
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": f"发货地:{origin},收货地:{dest},产品类型:{product_type},是否为节假日:{holiday}"}
            ],
            temperature=0.1
        )
        
        output = response.choices[0].message.content
        
        # 提取关键指标
        result = self._extract_estimates(output)
        return result
    
    def _extract_estimates(self, text: str) -> dict:
        """提取物流时效预估结果"""
        result = {
            "base_days": None,
            "extra_days": 0,
            "total_days": None,
            "estimated_date": None
        }
        
        base_match = re.search(r'-\s*基础时效:(\d+)天', text)
        if base_match:
            result["base_days"] = int(base_match.group(1))
        
        extra_match = re.search(r'-\s*额外加成:(\d+)天', text)
        if extra_match:
            result["extra_days"] = int(extra_match.group(1))
        
        total_match = re.search(r'-\s*总预估时效:(\d+)天', text)
        if total_match:
            result["total_days"] = int(total_match.group(1))
        
        date_match = re.search(r'-\s*预估送达日期:(\d{4}-\d{2}-\d{2})', text)
        if date_match:
            result["estimated_date"] = date_match.group(1)
        
        return result

6.3 自一致性增强(Self-Consistency)

对于高价值决策场景,可以让模型生成3-5条独立推理路径,然后投票选出最一致的答案:

def estimate_with_consistency(origin, dest, product_type, holiday, n_paths=3):
    """使用自一致性提升推理准确率"""
    results = []
    for i in range(n_paths):
        # 每次调用使用不同的随机种子或轻微差异化的Prompt
        result = estimator.estimate(origin, dest, product_type, holiday)
        results.append(result)
    
    # 投票选出出现最多的total_days
    total_days_list = [r['total_days'] for r in results if r['total_days'] is not None]
    if total_days_list:
        # 取众数
        from collections import Counter
        final_days = Counter(total_days_list).most_common(1)[0][0]
        # 返回结果中total_days = final_days的第一条
        for r in results:
            if r['total_days'] == final_days:
                return r
    
    return results[0]  # 降级返回第一条

七、策略对比与选型指南

策略适用场景优点缺点推荐温度
Markdown模板文档生成、报告分析结构清晰,易解析对模型格式遵从度有要求0.1-0.3
JSON SchemaAPI参数生成、数据提取机器可读,可校验对复杂层次支持有限0.0-0.1
Few-shot推理类任务、判责示范性强,无需抽象规则Token消耗大0.1-0.3
格式验证器所有场景(辅助)兜底保障,提高健壮性增加代码复杂度—
CoT推理数学推理、因果分析准确率高,可调试输出Token多,耗时0.3-0.5

组合使用建议:生产级系统推荐组合“JSON Schema约束 + 格式验证器 + 必要的Few-shot示例”,这是投入产出比最高的方案组合。


八、总结:结构化约束的底层逻辑

回顾五种策略,它们的本质目标是一致的:把模型的输出空间从“无限”压缩到“有限且可预测”。

  • Markdown模板通过物理结构限定输出形状
  • JSON Schema通过数据类型限定输出内容
  • Few-shot通过行为示范限定推理路径
  • 格式验证器通过代码审查强制修正偏差
  • CoT通过步骤暴露限定了思考过程

在实际项目中,建议至少使用两种以上的约束策略叠加——例如“JSON Schema + 格式验证器”的组合,可以将格式合规率从85%提升到98%以上。

最后一条建议:结构化约束不是越严格越好。过度的格式限制会抑制模型在边界场景中的灵活性。当业务允许时,保留一定的输出自由度(如允许输出“未知”或提供自由文本补充),往往能显著提升用户体验。

更多推荐