第 12 章

Structured Outputs:JSON Schema 强制约束、Pydantic/Zod 集成与生产踩坑

第十二章:Prefill 与输出引导:控制响应起点的艺术

12.1 什么是 Prefill

Prefill(预填充)是 Anthropic API 的一项独特功能,允许开发者在 messages 数组的最后一条中放置一个不完整的 assistant 消息,模型会将其作为自己回复的起点继续生成。

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=512,
    messages=[
        {"role": "user", "content": "今天天气怎么样?"},
        {"role": "assistant", "content": "根据您所在的"}  # Prefill
    ]
)

# 模型会从 "根据您所在的" 之后继续生成
print(response.content[0].text)
# 可能输出:"根据您所在的位置和当前季节..."(注意:prefill 内容不在响应中重复)

关键行为:API 响应中只包含 prefill 之后生成的内容,prefill 本身不会重复出现在响应中。这与对话历史中的 assistant 消息不同——历史 assistant 消息会被完整保留用于上下文。

Prefill vs 普通 assistant 历史消息

特性 普通历史 assistant 消息 Prefill(最后一条未完成的 assistant 消息)
位置 对话中间 messages 数组最后一条
完整性 完整消息 可以是不完整的片段
出现在响应中 不出现(是历史) 不出现(只返回续写部分)
用途 提供对话上下文 强制控制输出起点

12.2 强制 JSON 输出

Prefill 最常见的用途之一是确保模型输出以 {[ 开头,从而避免模型在 JSON 前添加解释性文字。

基础 JSON 强制

import anthropic
import json

client = anthropic.Anthropic()

def extract_structured_data(text: str) -> dict:
    """使用 prefill 强制输出 JSON"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system="从用户文本中提取结构化信息,只输出 JSON,不要其他内容。",
        messages=[
            {
                "role": "user",
                "content": f"提取以下文本中的联系人信息:\n\n{text}"
            },
            {
                "role": "assistant",
                "content": "{"  # Prefill:强制 JSON 对象起始
            }
        ]
    )
    
    # 拼接完整 JSON(prefill 的 "{" + 模型续写的内容)
    full_json = "{" + response.content[0].text
    
    return json.loads(full_json)

# 测试
result = extract_structured_data(
    "请联系张三,电话 13800138000,邮件 [email protected]"
)
print(result)
# 输出: {"name": "张三", "phone": "13800138000", "email": "[email protected]"}

强制特定 JSON 结构

def analyze_sentiment_structured(reviews: list[str]) -> list[dict]:
    """强制模型输出特定 JSON 数组格式"""
    
    reviews_text = "\n".join(f"{i+1}. {r}" for i, r in enumerate(reviews))
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        messages=[
            {
                "role": "user",
                "content": f"""分析以下评论的情感,输出 JSON 数组:
                
{reviews_text}

每个元素格式:{{"id": 数字, "sentiment": "positive/negative/neutral", "score": 0-1}}"""
            },
            {
                "role": "assistant",
                "content": "["  # 强制 JSON 数组
            }
        ]
    )
    
    full_json = "[" + response.content[0].text
    # 确保 JSON 完整
    if not full_json.rstrip().endswith("]"):
        full_json = full_json.rstrip().rstrip(",") + "]"
    
    return json.loads(full_json)

12.3 格式控制:代码块与标记语言

强制输出特定编程语言的代码块

def generate_python_code(task_description: str) -> str:
    """强制输出 Python 代码"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        system="你是 Python 专家,只输出代码,不要解释。",
        messages=[
            {"role": "user", "content": f"实现以下功能:{task_description}"},
            {
                "role": "assistant",
                "content": "```python\n"  # 强制 Python 代码块
            }
        ]
    )
    
    code_content = response.content[0].text
    # 移除可能的结束标记
    if "```" in code_content:
        code_content = code_content[:code_content.rfind("```")]
    
    return "```python\n" + code_content.rstrip() + "\n```"

def generate_typescript_code(task: str) -> str:
    """强制 TypeScript"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        messages=[
            {"role": "user", "content": f"用 TypeScript 实现:{task}"},
            {"role": "assistant", "content": "```typescript\n"}
        ]
    )
    return "```typescript\n" + response.content[0].text

控制输出格式和语言

# 强制以特定语言回复
def force_language_response(user_input: str, language: str = "English") -> str:
    """使用 prefill 强制以指定语言回复"""
    
    lang_starters = {
        "English": "Sure, ",
        "French": "Bien sûr, ",
        "German": "Natürlich, ",
        "Japanese": "はい、",
        "Chinese": "好的,"
    }
    
    starter = lang_starters.get(language, "")
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[
            {"role": "user", "content": user_input},
            {"role": "assistant", "content": starter}
        ]
    )
    
    return starter + response.content[0].text

12.4 角色扮演与语气锁定

Prefill 可以用来锁定模型在对话中始终保持特定的角色语气,而不只是在系统提示中定义:

def create_character_response(
    character_name: str,
    character_style: str,
    user_message: str
) -> str:
    """通过 prefill 锁定角色语气"""
    
    # 系统提示定义角色
    system = f"""你是 {character_name}。{character_style}

始终用第一人称、符合角色特征的语气回复。"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        system=system,
        messages=[
            {"role": "user", "content": user_message},
            {
                "role": "assistant",
                # Prefill 强化角色语气起始
                "content": f"*{character_name} 皱起眉头,缓缓开口*\n\n"
            }
        ]
    )
    
    return f"*{character_name} 皱起眉头,缓缓开口*\n\n" + response.content[0].text

# 使用示例
response = create_character_response(
    character_name="福尔摩斯",
    character_style="你是维多利亚时代的著名侦探,说话冷静、逻辑严密、偶尔傲慢。",
    user_message="你觉得这个案子怎么样?"
)

12.5 思维链引导

Prefill 可以引导模型按特定方式进行推理,在模型给出最终答案之前,先强制其进行步骤分解:

def solve_with_chain_of_thought(problem: str) -> dict:
    """使用 prefill 引导思维链推理"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2048,
        messages=[
            {
                "role": "user",
                "content": f"解决这个问题:{problem}\n\n请先分析,再给出答案。"
            },
            {
                "role": "assistant",
                "content": "让我逐步分析这个问题:\n\n**第一步:理解问题**\n"
            }
        ]
    )
    
    full_response = "让我逐步分析这个问题:\n\n**第一步:理解问题**\n" + response.content[0].text
    return {"analysis": full_response}

def extract_answer_after_reasoning(problem: str) -> str:
    """先推理,再以结构化格式给出答案"""
    
    # 第一步:让模型推理
    reasoning_response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=1024,
        messages=[
            {"role": "user", "content": f"分析:{problem}"},
            {"role": "assistant", "content": "分析过程:\n"}
        ]
    )
    reasoning = "分析过程:\n" + reasoning_response.content[0].text
    
    # 第二步:基于推理给出最终答案
    final_response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=256,
        messages=[
            {"role": "user", "content": f"分析:{problem}"},
            {"role": "assistant", "content": reasoning},
            {"role": "user", "content": "基于以上分析,给出最终答案(一句话):"},
            {"role": "assistant", "content": "答案:"}
        ]
    )
    
    return "答案:" + final_response.content[0].text

12.6 输出长度与截断控制

强制短输出

def get_concise_answer(question: str) -> str:
    """强制模型给出简洁的一行答案"""
    
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=100,
        messages=[
            {
                "role": "user",
                "content": f"问题:{question}\n请用一句话回答。"
            },
            {
                "role": "assistant",
                "content": "答:"  # 引导简短回答
            }
        ]
    )
    
    answer = "答:" + response.content[0].text.split("\n")[0]  # 只取第一行
    return answer

# 对比:不使用 prefill 的输出往往更冗长
# 使用 prefill "答:" 后,模型倾向于直接给出简短答案

控制列表格式

def generate_numbered_list(topic: str, count: int = 5) -> list[str]:
    """强制生成精确数量的编号列表"""
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[
            {
                "role": "user",
                "content": f"列举 {count} 个关于{topic}的要点。"
            },
            {
                "role": "assistant",
                "content": "1."  # 强制从编号 1 开始
            }
        ]
    )
    
    full_text = "1." + response.content[0].text
    # 解析编号列表
    items = []
    for line in full_text.split("\n"):
        line = line.strip()
        if line and line[0].isdigit() and ". " in line:
            item = line.split(". ", 1)[1].strip()
            items.append(item)
    
    return items[:count]

12.7 Prefill 与 Extended Thinking 的交互

当使用 Extended Thinking(thinking 参数)时,prefill 的使用有特殊限制:

# 启用 extended thinking 时,prefill 内容有限制
# 不能在 thinking 模式下使用包含实质内容的 prefill

# 正确:简单的格式化 prefill 通常仍可使用
response = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=16000,
    thinking={
        "type": "enabled",
        "budget_tokens": 10000
    },
    messages=[
        {"role": "user", "content": "解决这道数学题:..."},
        # 注意:extended thinking 启用时,prefill 支持有限,建议不使用或只使用空字符串
    ]
)

重要注意事项:启用 thinking 参数后,不建议使用 prefill,因为思考过程的内容块必须在文本块之前出现,使用 prefill 可能导致格式冲突。

12.8 多步骤 Prefill 策略

渐进式输出控制

def generate_structured_report(data: str) -> str:
    """使用多步骤对话控制报告结构"""
    
    conversation = [
        {"role": "user", "content": f"基于以下数据生成分析报告:\n\n{data}"}
    ]
    
    # 第一步:强制生成标题
    conversation.append({
        "role": "assistant",
        "content": "# 数据分析报告\n\n## 执行摘要\n\n"
    })
    
    response1 = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        messages=conversation
    )
    
    summary = "# 数据分析报告\n\n## 执行摘要\n\n" + response1.content[0].text
    
    # 继续对话,要求详细分析
    conversation[-1] = {"role": "assistant", "content": summary}
    conversation.append({"role": "user", "content": "继续写详细分析章节"})
    conversation.append({"role": "assistant", "content": "\n## 详细分析\n\n"})
    
    response2 = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1000,
        messages=conversation
    )
    
    return summary + "\n## 详细分析\n\n" + response2.content[0].text

12.9 实际应用场景与最佳实践

场景 1:API 响应代理

在构建 API 代理时,确保模型输出可以直接解析为 JSON:

import anthropic
import json
from fastapi import FastAPI, HTTPException

app = FastAPI()
client = anthropic.Anthropic()

@app.post("/api/classify")
async def classify_text(text: str) -> dict:
    """分类文本并返回结构化 JSON 响应"""
    
    try:
        response = client.messages.create(
            model="claude-haiku-4-5-20251001",
            max_tokens=200,
            system='输出合法 JSON,格式:{"category": "...", "confidence": 0.0-1.0, "tags": [...]}',
            messages=[
                {"role": "user", "content": f"分类:{text}"},
                {"role": "assistant", "content": "{"}
            ]
        )
        
        raw = "{" + response.content[0].text
        return json.loads(raw)
        
    except json.JSONDecodeError as e:
        raise HTTPException(status_code=500, detail=f"JSON 解析失败: {e}")

场景 2:多语言内容生成

def generate_multilingual_content(
    content_brief: str,
    languages: list[str]
) -> dict[str, str]:
    """为多种语言生成内容,使用 prefill 确保语言正确"""
    
    results = {}
    
    # 语言到起始词的映射
    starters = {
        "zh": "【产品介绍】",
        "en": "Product Overview: ",
        "ja": "【製品紹介】",
        "ko": "【제품 소개】",
        "es": "Descripción del producto: "
    }
    
    for lang in languages:
        starter = starters.get(lang, "")
        
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=512,
            system=f"用{lang}语言生成内容。",
            messages=[
                {"role": "user", "content": content_brief},
                {"role": "assistant", "content": starter}
            ]
        )
        
        results[lang] = starter + response.content[0].text
    
    return results

使用 Prefill 的注意事项

适合使用 Prefill 的情况

不适合使用 Prefill 的情况

Prefill 的局限性

  1. 它改变了起点,但不能完全保证模型后续内容完全符合预期
  2. 非常长的 prefill 会消耗更多 token
  3. 在流式模式下,需要在客户端将 prefill 与生成内容拼接显示

小结

Prefill 是 Claude API 独有的输出引导机制,通过控制响应起点来精确塑造输出:

  1. JSON 强制:以 {[ 开头的 prefill 几乎完全消除了 JSON 前缀噪音
  2. 格式锁定:代码块、特定语言、特定结构都可以通过 prefill 开头强制
  3. 思维链引导:可以引导模型先进行分析推理再给出答案
  4. 注意限制:Extended Thinking 模式下不建议使用 prefill
  5. 拼接规则:API 响应只包含续写部分,完整内容 = prefill + 响应
本章评分
4.6  / 5  (39 评分)

💬 留言讨论