第 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 的情况:
- 强制 JSON/XML/YAML 等结构化格式
- 锁定特定的代码语言或格式
- 确保回复以特定语气或风格开头
- 引导特定的推理路径
不适合使用 Prefill 的情况:
- 已启用 Extended Thinking
- 需要模型自由决定最佳输出形式
- Prefill 内容可能造成误导性上下文
Prefill 的局限性:
- 它改变了起点,但不能完全保证模型后续内容完全符合预期
- 非常长的 prefill 会消耗更多 token
- 在流式模式下,需要在客户端将 prefill 与生成内容拼接显示
小结
Prefill 是 Claude API 独有的输出引导机制,通过控制响应起点来精确塑造输出:
- JSON 强制:以
{或[开头的 prefill 几乎完全消除了 JSON 前缀噪音 - 格式锁定:代码块、特定语言、特定结构都可以通过 prefill 开头强制
- 思维链引导:可以引导模型先进行分析推理再给出答案
- 注意限制:Extended Thinking 模式下不建议使用 prefill
- 拼接规则:API 响应只包含续写部分,完整内容 = prefill + 响应