第 1 章:先钉输出
出处
来源:claude-cookbooks · 状态:已读 · 对照目录:misc/、capabilities/、coding/、fable_5_fallback_billing/
一句话
先规定模型交什么,再谈它聪不聪明。 输出契约不是文风:能被程序吃掉、能核对出处、闭集可评、摘要有尺子、审核不当场即兴。做不到这些,后面的 RAG、工具、agent 都只是把噪声放大。
核心概念
没有 JSON Mode,只有形状约束
Claude 没有 constrained sampling 的正式 JSON Mode。misc/how_to_enable_json_mode.ipynb 给的是工程补丁:prefill { 砍掉前言;stop sequence 砍掉后话;复杂多对象用 XML 标签包起来再 regex 抽。prefill 的代价写得很清楚——模型没法在 JSON 前做 Chain of Thought,智力会掉一截。
真正硬的形状不在散文里。tool_use/extracting_structured_json.ipynb 把摘要、NER、情感、分类做成 tool schema:模型只能往 input_schema 里填。未知键才退回开放 schema + 提示词。对照:Book 1 把 prompt 当控制平面;这里 schema 才是输出平面。
Citations 是产品功能,不是「请引用原文」
misc/using_citations.ipynb:API 级 citations 比 prompt 级引用更准、更省 output token(不用把整段原文再吐一遍),也不会引用你没给过的文档。三种文档对应三种粒度:
| 文档 | 引用形状 |
|---|---|
| 纯文本 | 字符区间(自动按句切) |
| 页码(1-indexed) | |
| custom content | 你自己定的 chunk |
context 字段能喂元数据、使用说明、contextual retrieval 的补丁,但 不可被引用。这是刻意的:模型可以用,用户核对时看不到。PDF 只回页码,高亮要自己用 PyMuPDF 对 cited_text。
分类是闭集,准确率靠检索和推理,不靠「再写一遍 prompt」
capabilities/classification/guide.ipynb 用保险工单 10 类,把 70% 推到 95%+。路径不是换更大模型,是:类定义写清楚 → 用向量库捞相似训练样本进 prompt → CoT。LLM 适合规则复杂、样本少、还要解释的分类;评测必须有独立 test set,不能靠感觉。
摘要没有客观最优,所以必须自带评测
capabilities/summarization/guide.ipynb 拿 SEC 转租协议当样本。裸摘要每次结构都变,没法进流水线。进阶是:guided / 领域字段、meta-summarization、summary-indexed RAG。评测承认主观:ROUGE 不够,promptfoo + 任务标准才像生产。assistant prefill <summary> + stop </summary> 是第 1 章反复出现的形状手法。
审核:早期过滤器 vs 政策编译器
两代做法不要混:
misc/building_moderation_filter.ipynb:prompt 里写 ALLOW/BLOCK,返回一个词。适合论坛主题、快速闸门。CoT 和 few-shot 能抬准度,但同一内容两次跑可以给不同判决。capabilities/content_moderation/guide.ipynb:Claude 永不判刑。你写 extraction schema;Claude 把政策散文编译成带 scope 的 JSON 规则(validator 修到能过);每条内容抽成 typed fields(含图);无模型的 engine 出 verdict + 审计轨迹。政策改动变成可 diff 的规则版本,不是改一段 prompt。LLM assertion(「看起来不满 25 岁」)是规则语言里的模型字段,判决仍在 engine。
其余输出契约
- Metaprompt(
misc/metaprompt.ipynb):空白页启动器,不是最优 prompt。人必须验收。 - 前端审美(
coding/prompting_for_frontend_aesthetics.ipynb):观感也是契约。默认审美是 Inter + 紫渐变 slop,要显式禁。 - Fable 5 分类降级(
fable_5_fallback_billing/guide.ipynb):stop_reason: refusal时服务端落到 Opus 4.8,账单记一笔 credit。分类失败是产品路径,不是异常日志。 - 超长续写(
misc/sampling_past_max_tokens.ipynb):max_tokens截断后要有续采样协议,不能假装一次说完。
对照 recipe(默认折叠;机制片段摘自官方 notebook,全文在 GitHub):
misc/how_to_enable_json_mode.ipynb — 输出必须能被程序吃掉
message = (
client.messages.create(
model=MODEL_NAME,
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Give me a JSON dict with names of famous athletes & their sports.",
},
{"role": "assistant", "content": "Here is the JSON requested:\n{"},
],
)
.content[0]
.text
)
print(message)
output_json = json.loads("{" + message[: message.rfind("}") + 1])
output_json
import re
def extract_between_tags(tag: str, string: str, strip: bool = False) -> list[str]:
ext_list = re.findall(f"<{tag}>(.+?)</{tag}>", string, re.DOTALL)
if strip:
ext_list = [e.strip() for e in ext_list]
return ext_list
athlete_sports_dict = json.loads(extract_between_tags("athlete_sports", message)[0])
athlete_name_dicts = [json.loads(d) for d in extract_between_tags("athlete_name", message)]tool_use/extracting_structured_json.ipynb — 用 tool schema 抽结构,比「请输出 JSON」硬
tools = [
{
"name": "print_summary",
"description": "Prints a summary of the article.",
"input_schema": {
"type": "object",
"properties": {
"author": {"type": "string", "description": "Name of the article author"},
"topics": {
"type": "array",
"items": {"type": "string"},
"description": 'Array of topics, e.g. ["tech", "politics"]. Should be as specific as possible, and can overlap.',
},
"summary": {
"type": "string",
"description": "Summary of the article. One or two paragraphs max.",
},
"coherence": {
"type": "integer",
"description": "Coherence of the article's key points, 0-100 (inclusive)",
},
"persuasion": {
"type": "number",
"description": "Article's persuasion score, 0.0-1.0 (inclusive)",
},
},
"required": ["author", "topics", "summary", "coherence", "persuasion", "counterpoint"],
},
}
]
url = "https://www.anthropic.com/news/third-party-testing"
response = requests.get(url, timeout=30)
soup = BeautifulSoup(response.text, "html.parser")
article = " ".join([p.text for p in soup.find_all("p")])
query = f"""
<article>
{article}
</article>
Use the `print_summary` tool.
"""
response = client.messages.create(
model=MODEL_NAME, max_tokens=4096, tools=tools, messages=[{"role": "user", "content": query}]
)
json_summary = None
for content in response.content:
if content.type == "tool_use" and content.name == "print_summary":
json_summary = content.input
break
if json_summary:
print("JSON Summary:")
print(json.dumps(json_summary, indent=2))
else:
print("No JSON summary found in the response.")misc/using_citations.ipynb — 答案要带着出处,才能核对
# Read all help center articles and create a list of documents
articles_dir = "./data/help_center_articles"
documents = []
for filename in sorted(os.listdir(articles_dir)):
if filename.endswith(".txt"):
with open(os.path.join(articles_dir, filename)) as f:
content = f.read()
# Split into title and body
title_line, body = content.split("\n", 1)
title = title_line.replace("title: ", "")
documents.append(
{
"type": "document",
"source": {"type": "text", "media_type": "text/plain", "data": body},
"title": title,
"citations": {"enabled": True},
}
)
QUESTION = "I just checked out, where is my order tracking number? Track package is not available on the website yet for my order."
# Add the question to the content
content = documents
response = client.messages.create(
model="claude-sonnet-4-6",
temperature=0.0,
max_tokens=1024,
system="You are a customer support bot working for PetWorld. Your task is to provide short, helpful answers to user questions. Since you are in a chat interface avoid providing extra details. You will be given access to PetWorld's help center articles to help you answer questions.",
messages=[
{"role": "user", "content": documents},
{
"role": "user",
"content": [{"type": "text", "text": f"Here is the user's question: {QUESTION}"}],
},
],
)
def visualize_raw_response(response):
raw_response = {"content": []}
print("\n" + "=" * 80 + "\nRaw response:\n" + "=" * 80)
for content in response.content:
if content.type == "text":
block = {"type": "text", "text": content.text}
if hasattr(content, "citations") and content.citations:
block["citations"] = []
for citation in content.citations:
citation_dict = {
"type": citation.type,
"cited_text": citation.cited_text,
"document_title": citation.document_title,
}
if citation.type == "page_location":
citation_dict.update(
{
"start_page_number": citation.start_page_number,
"end_page_number": citation.end_page_number,
}
)
block["citations"].append(citation_dict)
raw_response["content"].append(block)
return json.dumps(raw_response, indent=2)
print(visualize_raw_response(response))capabilities/classification/guide.ipynb — 分类是闭集输出
import pandas as pd
data = {
"train": [],
"test": [],
}
# Helper function to convert a DataFrame to a list of dictionaries
def dataframe_to_dict_list(df):
return df.apply(lambda x: {"text": x["text"], "label": x["label"]}, axis=1).tolist()
# Read the TSV file into a DataFrame
test_df = pd.read_csv("./data/test.tsv", sep="\t")
data["test"] = dataframe_to_dict_list(test_df)
train_df = pd.read_csv("./data/train.tsv", sep="\t")
data["train"] = dataframe_to_dict_list(train_df)
# Understand the labels in the dataset
labels = list(set(train_df["label"].unique()))
# Print the first training example and the number of training examples
print(data["train"][0], len(data["train"]))
# Create the test set
X_test = [example["text"] for example in data["test"]]
y_test = [example["label"] for example in data["test"]]
# Print the length of the test set
print(len(X_test), len(y_test))
def simple_classify(X):
prompt = (
textwrap.dedent("""
You will classify a customer support ticket into one of the following categories:
<categories>
{{categories}}
</categories>
Here is the customer support ticket:
<ticket>
{{ticket}}
</ticket>
Respond with just the label of the category between category tags.
""")
.replace("{{categories}}", categories)
.replace("{{ticket}}", X)
)
response = client.messages.create(
messages=[
{"role": "user", "content": prompt},
{"role": "assistant", "content": "<category>"},
],
stop_sequences=["</category>"],
max_tokens=4096,
temperature=0.0,
model=MODEL,
)
# Extract the result from the response
result = response.content[0].text # pyright: ignore[reportAttributeAccessIssue]
return result.strip()
vectordb = VectorDB()
vectordb.load_data(data["train"])capabilities/summarization/guide.ipynb — 摘要要有评测,不只是短
def basic_summarize(text, max_tokens=1000):
# Prompt the model to summarize the text
prompt = f"""Summarize the following text in bullet points. Focus on the main ideas and key details:
{text}
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=max_tokens,
system="You are a legal analyst known for highly accurate and detailed summaries of legal documents.",
messages=[
{"role": "user", "content": prompt},
{
"role": "assistant",
"content": "Here is the summary of the legal document: <summary>",
},
],
stop_sequences=["</summary>"],
)
return response.content[0].text
def guided_legal_summary(text, max_tokens=1000):
# Prompt the model to summarize the text
prompt = f"""Summarize the following legal document. Focus on these key aspects:
1. Parties involved
2. Main subject matter
3. Key terms and conditions
4. Important dates or deadlines
5. Any unusual or notable clauses
Provide the summary in bullet points under each category.
Document text:
{text}
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=max_tokens,
system="You are a legal analyst known for highly accurate and detailed summaries of legal documents.",
messages=[
{"role": "user", "content": prompt},
{
"role": "assistant",
"content": "Here is the summary of the legal document: <summary>",
},
],
stop_sequences=["</summary>"],
)
return response.content[0].textmisc/building_moderation_filter.ipynb — 早期审核过滤器
from anthropic import Anthropic
client = Anthropic()
MODEL_NAME = "claude-haiku-4-5"
def moderate_text(user_text, guidelines):
prompt_template = """
You are a content moderation expert tasked with categorizing user-generated text based on the following guidelines:
{guidelines}
Here is the user-generated text to categorize:
<user_text>{user_text}</user_text>
Based on the guidelines above, classify this text as either ALLOW or BLOCK. Return nothing else.
"""
# Format the prompt with the user text
prompt = prompt_template.format(user_text=user_text, guidelines=guidelines)
# Send the prompt to Claude and get the response
response = (
client.messages.create(
model=MODEL_NAME, max_tokens=10, messages=[{"role": "user", "content": prompt}]
)
.content[0]
.text
)
return response
cot_prompt = """You are a content moderation expert tasked with categorizing user-generated text based on the following guidelines:
BLOCK CATEGORY:
- Content that is not related to rollercoasters, theme parks, or the amusement industry
- Explicit violence, hate speech, or illegal activities
- Spam, advertisements, or self-promotion
ALLOW CATEGORY:
- Discussions about rollercoaster designs, ride experiences, and park reviews
- Sharing news, rumors, or updates about new rollercoaster projects
- Respectful debates about the best rollercoasters, parks, or ride manufacturers
- Some mild profanity or crude language, as long as it is not directed at individuals
First, inside of <thinking> tags, identify any potentially concerning aspects of the post based on the guidelines below and consider whether those aspects are serious enough to block the post or not. Finally, classify this text as either ALLOW or BLOCK inside <output> tags. Return nothing else.
Given those instructions, here is the post to categorize:
<user_post>{user_post}</user_post>"""
user_post = "Introducing my new band - Coaster Shredders. Check us out on YouTube!!"
response = (
client.messages.create(
model=MODEL_NAME,
max_tokens=1000,
messages=[{"role": "user", "content": cot_prompt.format(user_post=user_post)}],
)
.content[0]
.text
)
print(response)capabilities/content_moderation/guide.ipynb — 政策执行:模型编译,engine 判决
CACHE = pathlib.Path("evaluation/compiled")
CACHE.mkdir(parents=True, exist_ok=True)
def load_domain(name):
d = DATA / name
return {
"dir": d,
"schema": json.loads((d / "schema.json").read_text()),
"context": json.loads((d / "context.json").read_text()),
"policies": (d / "policies.md").read_text(),
"samples": [
json.loads(line)
for line in (d / "samples.jsonl").read_text().splitlines()
if line.strip()
],
}
def rules_for(name, dom):
if name == "ad_creatives":
return golden
cache = CACHE / f"{name}.json"
if cache.exists():
return json.loads(cache.read_text())
doc, attempts_ = compile_policies(dom["policies"], dom["schema"], dom["context"])
cache.write_text(json.dumps(doc, indent=2))
return doc
for name in ["product_listings", "ugc"]:
dom = load_domain(name)
doc = rules_for(name, dom)
assert not validate_document(doc, dom["schema"], dom["context"])
s = dom["samples"][1] # a violating sample from each domain
f = extract_fields(s["submission"], dom["schema"], doc.get("derived_fields", {}))
v = evaluate(doc["rules"], f, s["context"])
print(f"═══ {name}: {len(doc['rules'])} rules · sample: {s['name']}")
print(" " + v.explain().replace("\n", "\n "), "\n")misc/metaprompt.ipynb — 用模型写 prompt,仍要人验收
prompt = metaprompt.replace("{{TASK}}", TASK)
# Claude models from the 4.6 generation onward reject assistant-message prefill,
# so instead of seeding the response with a partial assistant turn, we steer it
# from the user turn: start directly at <Inputs>, and (if you supplied
# VARIABLES) use exactly those input variables.
response_steering = (
"\n\nStart your response directly with the <Inputs> block. "
"Do not include any preamble, commentary, or text before it."
)
if VARIABLES:
response_steering += (
"\nYour <Inputs> block, and the prompt template you write in <Instructions>, "
"must use exactly these input variables and no others: " + variable_string + "."
)
prompt += response_steering
response = client.messages.create(
model=MODEL_NAME,
max_tokens=MAX_TOKENS,
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
# Fail fast here, instead of a confusing extraction error several cells later.
if response.stop_reason == "max_tokens":
raise ValueError(
"The response hit MAX_TOKENS before the prompt template was complete. "
"Increase MAX_TOKENS in the setup cell at the top and re-run from there."
)
text_block = next((block for block in response.content if block.type == "text"), None)
if text_block is None:
raise ValueError("The response contained no text block; inspect `response.content`.")
message = text_block.text
def extract_between_tags(tag: str, string: str, strip: bool = False) -> list[str]:
ext_list = re.findall(f"<{tag}>(.+?)</{tag}>", string, re.DOTALL)
if strip:
ext_list = [e.strip() for e in ext_list]
return ext_list
def remove_empty_tags(text):
return re.sub(r"<(\w+)></\1>$", "", text)
def extract_prompt(metaprompt_response):
between_tags = extract_between_tags("Instructions", metaprompt_response)
if not between_tags:
raise ValueError(
"No <Instructions> block found in the response. "
"Re-run the metaprompt cell, or inspect the response with pretty_print(message)."
)
return remove_empty_tags(remove_empty_tags(between_tags[0]).strip()).strip()
def extract_variables(prompt):
pattern = r"{([^}]+)}"
variables = re.findall(pattern, prompt)
return set(variables)coding/prompting_for_frontend_aesthetics.ipynb — 观感也是一种输出契约
DISTILLED_AESTHETICS_PROMPT = """
<frontend_aesthetics>
You tend to converge toward generic, "on distribution" outputs. In frontend design, this creates what users call the "AI slop" aesthetic. Avoid this: make creative, distinctive frontends that surprise and delight. Focus on:
Typography: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics.
Color & Theme: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. Draw from IDE themes and cultural aesthetics for inspiration.
Motion: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions.
Backgrounds: Create atmosphere and depth rather than defaulting to solid colors. Layer CSS gradients, use geometric patterns, or add contextual effects that match the overall aesthetic.
Avoid generic AI-generated aesthetics:
- Overused font families (Inter, Roboto, Arial, system fonts)
- Clichéd color schemes (particularly purple gradients on white backgrounds)
- Predictable layouts and component patterns
- Cookie-cutter design that lacks context-specific character
Interpret creatively and make unexpected choices that feel genuinely designed for the context. Vary between light and dark themes, different fonts, different aesthetics. You still tend to converge on common choices (Space Grotesk, for example) across generations. Avoid this: it is critical that you think outside the box!
</frontend_aesthetics>
"""fable_5_fallback_billing/guide.ipynb — 分类失败时的降级与计费
# Fable applies extra safety filters. With a fallback chain configured, the API
# retries blocked turns on the next model server-side. A stop_reason of "refusal"
# means the whole chain refused.
from anthropic import Anthropic
client = Anthropic()
def chat_turn(messages, max_tokens=1024):
"""One API call; the server handles the fallback."""
return client.beta.messages.create(
model=PRIMARY_MODEL,
max_tokens=max_tokens,
messages=messages,
betas=[SERVER_SIDE_FALLBACK_BETA],
fallbacks=[{"model": FALLBACK_MODEL}],
)
with client.beta.messages.stream(
model=PRIMARY_MODEL,
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, world"}],
betas=[SERVER_SIDE_FALLBACK_BETA],
fallbacks=[{"model": FALLBACK_MODEL}],
) as stream:
for event in stream:
if (
getattr(event, "type", None) == "content_block_start"
and getattr(event.content_block, "type", None) == "fallback"
):
fb = event.content_block
fb = fb.model_dump() if hasattr(fb, "model_dump") else dict(fb)
print(f"[switching: {fb['from']['model']} -> {fb['to']['model']}]")
final = stream.get_final_message()
# Definitive per-turn check, same as non-streaming: usage.iterations also
# catches a stream served directly by the fallback model (no in-stream event).
if served_by_fallback(final):
print(f"[fallback model served this stream: {final.model}]")
def stream_turn(messages, max_tokens=1024):
with client.beta.messages.stream(
model=PRIMARY_MODEL,
max_tokens=max_tokens,
messages=messages,
betas=[SERVER_SIDE_FALLBACK_BETA],
fallbacks=[{"model": FALLBACK_MODEL}],
) as stream:
# Nothing streamed is ever discarded: after a mid-stream block, the
# final message is partial + fallback block + continuation.
final = stream.get_final_message()
if final.stop_reason == "refusal":
return final # the whole chain refused
text = "".join(b.text for b in final.content if b.type == "text")
print(f"{final.model}: {text}")
return finalmisc/sampling_past_max_tokens.ipynb — 超长输出怎么续
import anthropic
client = anthropic.Anthropic(
api_key="YOUR API KEY HERE",
)
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
messages=[
{
"role": "user",
"content": """
Please write five stories. Each should be at least 1000 words. Number the words to make sure you don't lose track. Make each story about a different animal.
Put them in <story_1>, <story_2>, ... tags
""",
},
],
)
message2 = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
messages=[
{
"role": "user",
"content": """
Please write five stories. Each should be at least 1000 words. Number the words to make sure you don't lose track. Make each story about a different animal.
Put them in <story_1>, <story_2>, ... tags
""",
},
{
"role": "assistant",
"content": message.content[0].text, # The text of Claude's partially completed message.
},
],
)
print(message2.content[0].text)关键洞察
这一章的共同动作是 把自由生成收成可验收的形状。JSON / tool schema 收程序接口;citations 收证据链;分类收标签集;摘要收评测集;政策编译收审计。跳过形状直接做 agent,你会在第 5、8、9 章用更贵的循环生产无法对账的文本。
和 02-Prompt是控制平面 对齐:控制平面管「能做什么」;输出契约管「交出来长什么样」。两边缺一,系统就只剩一次碰巧说对。
个人思考
读 Cookbooks 容易从「怎么让 Claude 更听话」入手。第 1 章纠正的是:听话之前先规定交卷格式。下一章检索会把「形状」扩到「从哪取证据」——citations 的 context 字段已经在给 contextual retrieval 留位置。