Dayward AI
Week 1 · D2About 4 hours

Long Documents, Multimodal Input, and a First Look at the API: Using Large Context, Saving Money With Prompt Caching, PDF and Image Input, Citation-Backed Answers; a Minimal Messages API Call

Make your first code call to Claude: feed it a whole PDF and get back a summary with page-numbered citations, then use prompt caching to cut the cost of repeated questions by an order of magnitude.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Make one Messages API call with the official SDK, and read the content blocks and usage in the response
  2. Pass a PDF or image to Claude as a content block, and turn on citations to get page-numbered references
  3. Explain where prompt caching saves money, when it doesn't, and verify a cache hit from the usage field

Yesterday was about how to brief the teammate. Today we start handing over material — a whole document of several dozen pages of it. Once you have read this and finished the lab, scroll back up and tick off the three goals.

Plain-Language Walkthrough

The minimal Messages API call: what is in one request

Yesterday's code quietly used the API once already; today we open it up. Every exchange with the teammate is, at the API level, one HTTP request to the Messages API. A request carries four things at minimum: which model (model), how much it may say at most (max_tokens), the business card (system, optional), and the conversation so far (messages). What comes back is not a piece of text either, but an array of content blocks (content), each with its own type: text is text, a tool the model wants to call is tool_use, and with thinking enabled there will also be thinking blocks. The sentence you actually wanted is the block whose type is text.

Hanging off the end of the response is a usage object recording how many input tokens went in and how many output tokens came out. This is today's most important field, because every technique we cover today ultimately relies on it to prove whether anything was actually saved. Write the minimal call first and print usage to see it:

hello.ts
import Anthropic from '@anthropic-ai/sdk'
 
const client = new Anthropic() // reads ANTHROPIC_API_KEY
 
const res = await client.messages.create({
  model: 'claude-sonnet-5',
  max_tokens: 512,
  system: 'You are a patient technical writer. Answer in three sentences or fewer.',
  messages: [{ role: 'user', content: 'Explain what a context window is in one sentence.' }],
})
 
// content is an array of blocks, not a string
const text = res.content
  .filter((b) => b.type === 'text')
  .map((b) => b.text)
  .join('')
console.log(text)
 
// usage is today's lead role: every technique gets verified through it
console.log(`input=${res.usage.input_tokens} output=${res.usage.output_tokens}`)
console.log(`stop_reason=${res.stop_reason}`) // end_turn / max_tokens / tool_use …

Memorize two details right now. First, stop_reason tells you why the model stopped: end_turn means it finished, and max_tokens means your ceiling cut it off — treating a summary that was chopped in half as a complete result is the most common silent error beginners hit. Second, do not set max_tokens stingily; it is a hard ceiling, not a target length. The model will not be more concise because you gave it 512, it will just be truncated.

Large context is not free context: the bigger the window, the more you should count what each request hauls

The context window of Claude's current flagship models is on the order of a million tokens, so a PDF of several dozen pages or tens of thousands of lines of code fits whole. The first reaction is usually "then length no longer matters" — and that is the first intuition today has to correct.

Back to the teammate analogy: you hand them a box of material, and every time they answer a question they have to read the whole box again from the top. The model is stateless; every request starts from zero and reads everything you sent, and input tokens are billed per request. A 60-page PDF runs roughly 1,500 to 3,000 tokens per page, so about 100,000 tokens for the whole thing; ask ten questions around it and that is a million input tokens. Even at a low unit price that is real money, never mind waiting for it to read 100,000 tokens before it starts answering each time.

A large window solves "it fits." It does not solve "it gets re-read every time." So the core skill of the large-context era is not how to cram more in, but counting what each request hauls and which part of it is hauled repeatedly. The two remaining techniques today — citations and prompt caching — make the hauling auditable and make the repeated part cost a fraction.

How the context window gets filled up1/5
Used 20 / 100 tokens
system persona20 tok
The context window is the model's desk — a fixed size. The system persona goes on first, and it usually has to stay there the whole time.

PDF and image as content blocks: the model sees more than the text

The previous section said "stuff the PDF in." How, exactly? Not by converting the PDF to plain text with a tool and pasting that into the prompt — that scatters tables and drops figures. The Messages API lets you place a document block directly in the content of a message, with source holding the PDF as base64; images work the same way with an image block. For each page of a PDF, Claude both reads the text and looks at the layout, so it can make sense of charts, tables, and handwritten annotations.

A few hard limits to remember: 32 MB per request, at most 600 pages (100 pages on models whose context window is under a million tokens), and no encrypted PDFs. A very dense PDF — small type, complex tables, lots of images — may fill the window before it hits the page limit, in which case split it by chapter. One more piece of advice follows yesterday's "material first, instructions last": put the document block before the text block, so the model sees the material before the question.

Since you will ask about the same PDF repeatedly, resending the base64 every time also wastes bandwidth. There is a separate Files API you can upload to once and then reference by file_id, sending only the id in later requests. Today's lab uses base64 to keep one concept off your plate; either is fine in production.

Citation-backed answers: citations make every sentence point back to a page

The teammate reads 60 pages and hands you a summary. Your first question is: which page said that? If they are paraphrasing from memory, you have to go back and verify everything yourself; if they tagged a page number after each sentence, you only have to spot-check.

There used to be exactly one way to do "with citations": ask the model in the prompt to quote the source and note the page, then hope it copies honestly. The problem is that a model paraphrases while it copies, and it can misremember the page — leaving you unable to distinguish "it really did cite" from "it believes it cited." Claude's citations feature moves this from a prompt convention to an API feature: add citations: { enabled: true } to the document block, and the returned text gets split into several blocks, each carrying a citations array holding the quoted source (cited_text), which document it came from (document_index), and the location — for a PDF that is page_location, with start_page_number and end_page_number (pages are 1-based and the end page is exclusive).

These citations are parsed and checked server-side by the API, so they always point at a passage that genuinely exists in the document; and cited_text does not count toward output tokens, making it cheaper than having the model copy the source itself. Wired into code:

cite.ts
import fs from 'node:fs'
import Anthropic from '@anthropic-ai/sdk'
 
const client = new Anthropic()
const pdf = fs.readFileSync('report.pdf').toString('base64') // base64 must not contain newlines
 
const res = await client.messages.create({
  model: 'claude-sonnet-5',
  max_tokens: 2048,
  messages: [
    {
      role: 'user',
      content: [
        {
          type: 'document', // material first
          source: { type: 'base64', media_type: 'application/pdf', data: pdf },
          title: 'report.pdf',
          citations: { enabled: true }, // turn citations on
        },
        {
          type: 'text',
          text: 'Summarize the core conclusions of this report in five sentences, each backed by evidence.',
        }, // instructions last
      ],
    },
  ],
})
 
// With citations on, the text arrives as several text blocks, each possibly carrying citations
for (const block of res.content) {
  if (block.type !== 'text') continue
  const pages = (block.citations ?? [])
    .filter((c) => c.type === 'page_location')
    .map((c) => `p.${c.start_page_number}`) // end_page_number is exclusive; for one page just read start
  process.stdout.write(block.text + (pages.length ? ` [${pages.join(', ')}]` : ''))
}

One limit came up yesterday: citations and structured output cannot be enabled together. So "a summary with page numbers" and "fill in a JSON form" are two different paths — when you need citations, use the feature and assemble the text blocks yourself; when you need a strict structure, give up API-level citations and leave a page field in the schema for the model to fill in itself (which is a notch less reliable).

Prompt caching: store the unchanging prefix and pay a fraction for repeat questions

Back to the problem of re-reading the whole box every time. If the teammate builds an index on the first pass, the second and third questions no longer need a read from the top. Prompt caching is that index: you mark a spot in the request saying "cache everything up to here," the server stores the processed state of that prefix, and for the next few minutes any request with a byte-identical prefix reuses it and pays a fraction.

The key word is prefix. Cache matching covers every byte from the start of the request up to the cache_control marker, in the order tool definitions, then system, then messages. Change one byte and every cache after it is invalidated. That explains yesterday's "do not put a date on the business card": put a timestamp that changes every time at the top of the system prompt and the prefix will never line up, so the cache never hits once. The right layout is unchanging content first (system, documents), changing content after (this turn's question), with the cache marker at the end of the unchanging part.

On price, the request that writes the cache costs 1.25 times the normal input price, and each later hit costs 0.1 times. So it is not free: ask a single question about one document with caching on and you pay 25% more; you only start netting a saving from the third question on. The cache lives 5 minutes by default and each hit refreshes it; there is also a 1-hour tier at 2 times the write price, suited to longer gaps. There is also a minimum length threshold (1,024 tokens on current flagship models, 4,096 on Haiku 4.5), and a prefix below it is neither cached nor flagged as an error — one of the most common reasons for "caching is on but nothing hits."

Put the cache marker on the document block:

cached.ts
import fs from 'node:fs'
import Anthropic from '@anthropic-ai/sdk'
 
const client = new Anthropic()
const pdf = fs.readFileSync('report.pdf').toString('base64')
 
async function ask(question: string) {
  const res = await client.messages.create({
    model: 'claude-sonnet-5',
    max_tokens: 1024,
    messages: [
      {
        role: 'user',
        content: [
          {
            type: 'document',
            source: { type: 'base64', media_type: 'application/pdf', data: pdf },
            cache_control: { type: 'ephemeral' }, // cache up to here: the doc is fixed, the question varies after it
          },
          { type: 'text', text: question },
        ],
      },
    ],
  })
  const u = res.usage
  // First call: cache_creation is large, cache_read is 0. From the second on: cache_read is large, cache_creation is 0
  console.log(
    `write=${u.cache_creation_input_tokens} read=${u.cache_read_input_tokens} uncached=${u.input_tokens}`
  )
}
 
await ask('What are the core conclusions of this report?')
await ask('Which risks does the report mention?') // same prefix, cache hit

Verifying with usage: did the cache actually hit

We have covered what ought to save money. How do you know it did? The answer goes back to usage from the first section. With caching on it gains two fields: cache_creation_input_tokens is how many tokens this request wrote into the cache, and cache_read_input_tokens is how many it read out of the cache; the original input_tokens now means the part not covered by any cache. Only the sum of all three is the input this request truly processed.

A healthy write-then-read looks like this: on the first request cache_creation is roughly the size of the document and cache_read is 0; on the second, cache_read is roughly the size of the document, cache_creation is 0, and input_tokens is down to the few dozen tokens of the question. If cache_read is still 0 on the second call, work through the list: is there anything in the prefix that varies (a timestamp, a random id, unsorted JSON); are both requests using the same model; did the prefix clear the minimum length threshold; was the gap longer than 5 minutes; is the tool list in the same order. Those five cover nine out of ten cache misses.

Baking that check into your code is a good habit: log all three fields, build a cache hit rate metric, and one glance after a deploy tells you whether someone broke the prefix design. That is the last step of today's lab, and it is also the first self-check to run whenever you pick up any long-document requirement in the future.

Source Reading

Hands-On Lab

🧪 D2 lab: a script that reads a PDF and prints a summary with page citations (one in TS, one in Python)

Code location: labs/claude-mastery/day-02-pdf-cited-summary

Acceptance criteria:

  1. MOCK=1 pnpm start sample.pdf prints a summary offline, with a page marker shaped like [p.3] after each sentence.
  2. With a real key, every page number printed by pnpm start sample.pdf can be traced to the matching passage in the PDF.
  3. Ask two questions in a row, and the second prints a cache_read_input_tokens clearly above 0 with cache_creation_input_tokens at 0.
  4. Lower max_tokens until the summary is truncated, and the program prints an explicit stop_reason=max_tokens warning rather than silently emitting half an answer.
  5. When the document exceeds the page limit or is not a PDF, the program gives a readable error message.

Confirm two things before you start: that you have an unencrypted PDF of a few to a few dozen pages (use the bundled sample.pdf in the lab directory if not), and that your key is in ANTHROPIC_API_KEY in .env — with no key, run everything under MOCK=1. If you get stuck, read the common pitfalls section of the README first.

  1. Run pnpm install in the starter directory, then MOCK=1 pnpm start sample.pdf. Seeing a mock summary with [p.N] markers confirms the skeleton runs.
  2. Finish exercise 1: read the PDF as base64 into a document block, enable citations, and place the document block before the text block.
  3. Finish exercise 2: walk the returned content blocks, append the start page of each page_location to the matching sentence, and print stop_reason at the end.
  4. Finish exercise 3: add cache_control to the document block, ask two questions in a row, and print and compare the three cache-related usage fields.
  5. Swap the summary question for three different questions and run again, watching whether cache_read hits consistently. If it does not, work through the five-item list from the walkthrough.

Interview Questions

Today's 3 questions are in the question bank below, weighted toward why the context window is the scarcest resource, the prefix semantics of prompt caching, and how citations differ from asking the model to copy the source itself. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets. The high-frequency labels for the domestic and overseas markets are there so you can pick by target market.

Checklist and Tomorrow

  • Make one Messages API call with the official SDK, and read the content blocks and usage in the response
  • Pass a PDF or image to Claude as a content block, and turn on citations to get page-numbered references
  • Explain where prompt caching saves money, when it doesn't, and verify a cache hit from the usage field
  • Recite the five-item checklist for a cache that will not hit
  • All 5 acceptance criteria of the lab pass
  • Answer at least 2 of the 3 interview questions without looking at the key points

Tomorrow (D3) we leave the API and enter Claude Code — a teammate that reads files, runs commands, and edits code on its own. You will install it, write the first CLAUDE.md for your own project, walk through explore-then-plan-then-implement with Plan Mode, and for the first time actually hand the example task of adding validation and tests to the TODO API over to it. Today's "context is the scarcest resource" becomes a daily reality there: every file Claude Code reads and every command it runs spends the same window, which is why half of D3 is about managing it.

Interview questions

  • Why is the context window called the scarcest resource in LLM applications? With million-token windows, does that still hold?为什么说上下文窗口是 LLM 应用里最稀缺的资源?窗口已经有一百万 token 了,这个说法还成立吗?
    Common in ChinaCommon overseasBasic#context-window#cost

    How to reason about it · think before answering

    1. The second sentence is the point. 'The window has a limit' is a dated answer; explain why scarcity survives large windows.
    2. Three causal chains: models are stateless so every request re-reads the whole input and bills it, a big window only solves fitting, not re-sending; longer context means more latency and diluted attention, so adherence to early instructions degrades as the window fills; and in agent workflows every file read and command output lands in the same window, filling it far faster than chat does.
    3. Conclusion: scarcity shifted from 'won't fit' to 'every token costs money and attention', so the discipline becomes active management — include only what is needed, cache the stable prefix, delegate research to subagents with their own context, and clear between tasks.
    4. Production math: a 60-page PDF is roughly 100k tokens; ten questions about it are a million input tokens; caching versus not caching is an order of magnitude apart.
    5. Follow-up: when should context accumulate? While deep in one complex problem where the history is still load-bearing; the test is whether the next step will use it.

    分析过程 · 先想清楚再作答

    1. 题眼在第二句。只答「窗口有上限」已经过时了,面试官想听的是「窗口变大之后为什么还稀缺」。
    2. 从三条因果链推:一、模型无状态,每次请求都把全部输入重读一遍,输入 token 按次计费——窗口大只解决了放得下,没解决每次都要重搬;二、上下文越长,延迟越高、注意力越稀释,模型对早期指令的遵守度会下降,也就是「性能随填充度下降」;三、Agent 场景里每读一个文件、每跑一条命令的输出都进同一个窗口,填得比聊天快得多。
    3. 结论:窗口大了,稀缺性从「放不下」变成了「每一 token 都在花钱和稀释注意力」,所以管理手段变成了主动管:只放必要的、把不变的缓存起来、把查资料的活派给独立上下文的子代理、该清就清。
    4. 生产视角:算一笔账——60 页 PDF 约 10 万 token,围着它问 10 个问题就是 100 万输入 token;不用缓存和不用缓存的差价是一个量级。
    5. 可预期的追问:那什么时候应该让上下文积累?在一个复杂问题里深挖时历史是有价值的;判据是「这段历史下一步还会不会用到」。

    Key points

    • Models are stateless: every request re-reads and bills the full input; a large window solves fitting, not re-sending
    • Longer context raises latency and dilutes attention; adherence to early instructions drops
    • Agent workflows dump every file read and command output into the same window
    • Tactics: include only what's needed, cache the stable prefix, isolate research in subagents, clear between tasks

    答题要点

    • 模型无状态,每次请求重读全部输入并计费;窗口大只解决放得下,不解决每次重搬
    • 上下文越长延迟越高、注意力越稀释,早期指令遵守度下降
    • Agent 场景每次读文件、跑命令的输出都进窗口,填得比聊天快得多
    • 对策:只放必要的、缓存不变前缀、用子代理隔离查资料、任务之间清空
  • Where does prompt caching save money, when does it cost more, and how do you debug a zero cache-hit rate in production?prompt caching 省在哪?什么情况下反而不省?线上发现缓存命中率是零,你怎么排查?
    Common in ChinaCommon overseasIntermediate#prompt-caching#cost

    How to reason about it · think before answering

    1. Three questions, three layers: mechanism, boundaries, debugging. The third layer is what shows production experience.
    2. Mechanism: the cache matches the exact byte prefix from the start of the request to the cache_control marker (tools, then system, then messages). A hit bills that prefix at 0.1x input price; the write costs 1.25x (2x for the one-hour TTL).
    3. When it costs more: a prefix used only once (+25%); volatile content inside the prefix — timestamps, random ids, unsorted JSON, user names — so every call writes a cache nothing will read; a prefix below the minimum (1024 tokens on current flagship models, 4096 on Haiku 4.5) that silently never caches; requests spaced beyond the TTL.
    4. Debug order by likelihood: dynamic content at the head of system or tool definitions; model id mismatch between calls; prefix under the minimum; gap over five minutes; unstable tool ordering. The single signal is usage.cache_read_input_tokens greater than zero.
    5. Follow-up: where do breakpoints go? At the end of stable sections — tools, system, the long document, the second-to-last message in a multi-turn chat — at most four; a breakpoint on per-turn content is a wasted write.

    分析过程 · 先想清楚再作答

    1. 三问对应三层:原理、边界、排查。只答第一层是背文档,第三层才体现有没有真的上过线。
    2. 原理一句话:缓存匹配的是请求开头到 cache_control 标记为止的精确前缀(顺序是工具、system、messages),命中时这段只收正常输入价的 0.1 倍;代价是写入那一次收 1.25 倍(1 小时档 2 倍)。
    3. 不省的情况由此推出:同一前缀只用一次(多付 25%);前缀里有每次都变的内容(时间戳、随机 id、未排序 JSON、用户名),导致每次都在写永远用不上的缓存;前缀短于最小门槛(主力模型 1024 token,Haiku 4.5 是 4096)根本不会缓存;两次请求间隔超过 TTL。
    4. 排查清单按发生概率排:一看 system 或工具定义开头有没有动态内容;二看两次请求的模型 id 是否一致;三看前缀长度是否过门槛;四看间隔是否超 5 分钟;五看工具列表顺序是否稳定。判据只有一个字段:usage.cache_read_input_tokens 是否大于 0。
    5. 可预期的追问:断点应该打在哪?不变的末尾——工具定义末尾、system 末尾、长文档末尾、多轮对话倒数第二条消息,最多四个;打在每轮都变的内容上等于白写。

    Key points

    • Matches the exact prefix (tools → system → messages up to the marker); hits bill 0.1x, writes 1.25x
    • Costs more when the prefix is used once, contains volatile content, is under the minimum length, or requests exceed the TTL
    • Debug: dynamic content, model mismatch, length, gap, tool ordering; verify via cache_read_input_tokens
    • Place breakpoints at the end of stable sections, at most four

    答题要点

    • 匹配精确前缀(工具 → system → messages 到标记为止);命中 0.1 倍,写入 1.25 倍
    • 不省:前缀只用一次、前缀含动态内容、前缀短于最小门槛、间隔超过 TTL
    • 排查:动态内容、模型不一致、长度不够、间隔太久、工具顺序变了;看 cache_read_input_tokens
    • 断点打在不变部分的末尾,最多四个
  • How do API citations fundamentally differ from prompting the model to quote sources with page numbers, and when can't you use them?citations 和在提示词里要求模型「引用原文并注明页码」有什么本质区别?什么场景下不能用 citations?
    Common in ChinaCommon overseasIntermediate#citations#grounding

    How to reason about it · think before answering

    1. The question is about where trust comes from. 'Citations are more convenient' is surface; the real difference is who guarantees the quote is real.
    2. With prompting, both the quote and the page number are free text the model generates — it may paraphrase, it may misremember the page, and you cannot tell a real quote from an imagined one. With citations, the model emits citation intent in a standard format, the API parses and verifies it server-side, cited_text is guaranteed to exist in the document, and page_location comes from the API. Fidelity is enforced by the API rather than promised by the model.
    3. Two side benefits: cited_text does not count toward output tokens, so it is cheaper than asking the model to copy; and the result is structured content blocks your UI can highlight and jump to without regex guessing.
    4. When you can't: citations are incompatible with structured outputs (JSON Schema) — enabling both returns a 400. Either drop API-level citations and add a page field to the schema (one notch less reliable), or split into two calls: citations for facts, structured output for shaping.
    5. Follow-ups: page semantics — start_page_number is 1-indexed and end_page_number is exclusive; document_index distinguishes sources. Can you audit citation quality? Yes — string-match cited_text against the source, or sample manually.

    分析过程 · 先想清楚再作答

    1. 这题考的是「可信度从哪来」。答成「citations 更方便」是表面;本质区别是谁来保证引用的真实性。
    2. 拆法:提示词方案里,引用和页码都是模型生成的自由文本——它可能顺手改写原文、可能记错页码,你无法区分「真引用」和「自以为引用」。citations 方案里,模型内部以标准格式输出引用意图,API 在服务端解析并核对,返回的 cited_text 一定是文档里真实存在的段落,page_location 的页码由 API 给出。真实性由 API 保证而不是由模型自觉保证。
    3. 附带的两点好处:cited_text 不计入输出 token,比让模型抄原文便宜;返回是结构化的内容块,程序可以直接高亮、跳转,不用正则去猜「第 3 页」出现在哪。
    4. 不能用的场景:与结构化输出(JSON Schema)不兼容,二者同开会报 400;此时要么放弃 API 级引用、在 schema 里留 page 字段让模型自己填(可靠性差一档),要么分两步:先 citations 拿事实,再用结构化输出整理。
    5. 可预期的追问:页码字段的语义?start_page_number 从 1 开始,end_page_number 不包含;多文档时 document_index 区分来源。再追问「能否验证引用质量」——能,用 cited_text 与原文做字符串比对,或抽样人工核对。

    Key points

    • Prompted quotes are free text the model generates — it may paraphrase or misplace pages, and you can't tell
    • Citations are parsed and verified server-side; cited_text is guaranteed to exist and page numbers come from the API
    • cited_text is free of output-token cost and the structured blocks enable highlighting and navigation
    • Mutually exclusive with structured outputs; split into two calls or add a page field to the schema

    答题要点

    • 提示词引用是模型生成的自由文本,可能改写、记错页码,无法区分真假
    • citations 由 API 在服务端解析核对,cited_text 一定存在于文档中,页码由 API 给出
    • cited_text 不计输出 token,返回结构化便于高亮跳转
    • 与结构化输出互斥;需要两者时分两步或在 schema 留 page 字段

Comments