Comprehensive guide to the Claude API, product architecture patterns, holistic system view, model orchestration, and the learning path to ship an AI-powered product.
Date: May 2026 Purpose: Comprehensive guide covering the Claude API, product architecture, entity resolution, holistic system view, and the learning path to ship an AI-powered knowledge graph product.
An API key is a unique string — something like sk-ant-api03-xxxx...xxxx — that identifies your account to Anthropic's servers. It serves two purposes: it proves you have permission to use the API, and it lets Anthropic track your usage for billing.
How to get one: Sign into the Anthropic Console. Navigate to Settings → API Keys. Click "Create Key." Copy it immediately — you will not see it again. Store it in your .env file (which is already gitignored in your project).
How authentication works in practice: Every request to the API includes your key in a header. The Python SDK handles this automatically:
import anthropic
import os
# The SDK reads ANTHROPIC_API_KEY from your environment automatically
client = anthropic.Anthropic()
# Or pass it explicitly
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))Your .env file (already gitignored in the [Your Project]):
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
Security rules you already follow: Your CLAUDE.md hard ban "NO committing .env — only .env.example is tracked" is exactly right. Never put the key in source code, never commit it, never log it. The .env.example pattern (committed, with placeholder values) documents which environment variables are needed without exposing secrets.
The Claude API uses the Messages API. Every interaction is a request containing messages, and a response containing the model's output.
A minimal request:
response = client.messages.create(
model="claude-sonnet-4-6-20250514",
max_tokens=1024,
messages=[
{"role": "user", "content": "What is entity resolution?"}
]
)
print(response.content[0].text)A real-world request for your pipeline (entity extraction):
response = client.messages.create(
model="claude-sonnet-4-6-20250514",
max_tokens=4096,
system="You are a [domain] analyst. Extract entities and relationships from the provided text. Return structured JSON only.",
messages=[
{
"role": "user",
"content": f"""
<article>
{email_body}
</article>
Extract all domain-specific entities ([entity types for your domain])
and relationships between them. For each relationship, specify:
- source entity
- target entity
- relationship type ([relationship-type-1], [relationship-type-2], [relationship-type-3])
- confidence (low/medium/high based on source specificity)
- a one-sentence summary grounding the relationship in the source text
Return ONLY valid JSON matching this schema:
{{
"entities": [
{{"name": "...", "type": "country|person|organization|event", "aliases": []}}
],
"relationships": [
{{"source": "...", "target": "...", "type": "...", "confidence": "...", "summary": "..."}}
]
}}
"""
}
]
)The response object:
response.content # List of content blocks (text, tool_use, etc.)
response.content[0].text # The actual text output
response.model # Which model responded
response.usage.input_tokens # How many tokens you sent (you pay for these)
response.usage.output_tokens # How many tokens it generated (you pay more for these)
response.stop_reason # "end_turn", "max_tokens", "tool_use"Key parameters you control:
| Parameter | What it does | Your typical setting |
|---|---|---|
model |
Which Claude model to use | claude-sonnet-4-6-20250514 for extraction, claude-opus-4-6-20250219 for resolution |
max_tokens |
Maximum output length | 4096 for extraction, 1024 for summarisation |
system |
System prompt — persistent instructions | Your extraction prompt template |
temperature |
Randomness (0 = deterministic, 1 = creative) | 0.0 for extraction (you want consistency) |
tools |
Tool definitions for structured output | Entity extraction schema (see §1.5) |
This is the feature most relevant to your pipeline. Instead of asking Claude to return JSON in free text (which can be malformed), you define a "tool" with a JSON schema and Claude returns structured data that matches the schema exactly.
tools = [
{
"name": "extract_entities",
"description": "Extract domain-specific entities and relationships from text.",
"input_schema": {
"type": "object",
"properties": {
"entities": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"type": {"type": "string", "enum": ["country", "person", "organization", "event"]},
"aliases": {"type": "array", "items": {"type": "string"}}
},
"required": ["name", "type"]
}
},
"relationships": {
"type": "array",
"items": {
"type": "object",
"properties": {
"source": {"type": "string"},
"target": {"type": "string"},
"type": {"type": "string", "enum": ["diplomatic", "conflict", "alliance", "economic", "intelligence"]},
"confidence": {"type": "string", "enum": ["low", "medium", "high"]},
"summary": {"type": "string"}
},
"required": ["source", "target", "type", "confidence", "summary"]
}
}
},
"required": ["entities", "relationships"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-6-20250514",
max_tokens=4096,
tools=tools,
messages=[{"role": "user", "content": f"<article>{email_body}</article>\n\nUse the extract_entities tool."}]
)
# The response contains a tool_use content block with structured data
for block in response.content:
if block.type == "tool_use" and block.name == "extract_entities":
entities = block.input["entities"]
relationships = block.input["relationships"]This is guaranteed to match your schema. No JSON parsing errors, no malformed output, no missing fields. This is how your [extraction-module] should call Claude.
Current pricing (May 2026):
| Model | Input (per 1M tokens) | Output (per 1M tokens) | Your use case |
|---|---|---|---|
| Claude Sonnet 4.6 | $3.00 | $15.00 | Entity extraction (primary) |
| Claude Opus 4.6 | $5.00 | $25.00 | Entity resolution (complex reasoning) |
| Claude Haiku 4.5 | $1.00 | $5.00 | Simple classification, formatting |
What a token is: Roughly 4 characters of English text. A typical newsletter email is 500–2,000 words ≈ 700–2,500 tokens. The extraction prompt template adds ~500 tokens. The output (entities + relationships) is typically 200–800 tokens.
Cost estimate for your [N]-source corpus:
Assuming average 1,500 input tokens and 500 output tokens per email, using Sonnet:
- Input: [N] × 1,500 = 5.55M tokens × $3.00/M = $16.65
- Output: [N] × 500 = 1.85M tokens × $15.00/M = $27.75
- Total: ~$44.40 for the entire corpus
That's surprisingly affordable. And you can cut it further:
- Batch API (50% off): Process emails asynchronously within 24 hours. Same quality, half the cost: ~$22.
- Prompt caching (90% off cached input): Your system prompt and extraction template are identical for every email. Cache them and pay $0.30/M instead of $3.00/M for that portion. Savings depend on cache-hit rate but could reduce input costs by 60–70%.
- Combined (batch + caching): Potentially under $15 for the entire corpus.
Rate limits: New accounts start at Tier 1 (roughly 50 requests per minute, 40,000 tokens per minute for Sonnet). You auto-upgrade to higher tiers as you spend. For batch processing, rate limits are much more generous. Your [N]-source corpus at 50 RPM would take ~74 minutes synchronously, or you can submit it as one batch job and wait 24 hours.
Your [llm-router-module] already has the right architecture — it routes calls to different models based on task type. Here's how the Claude API fits into each pipeline stage:
source corpus ([N] source documents in data/raw/)
↓
[[classifier-module]] — Ollama [local-small-model] (FREE, LOCAL)
Filters non-domain-specific emails. Fast, cheap, runs on your GPU.
↓
[summarise.py] — Ollama [local-small-model] (FREE, LOCAL)
Generates 2-3 sentence summary of each surviving email.
↓
[[extraction-module]] — Claude Sonnet via API ($3/$15 per M tokens)
Entity extraction using tool use (§1.3 above).
Returns structured entities + relationships.
↓
[critic.py] — Claude Sonnet via API (same rate)
CRAG actor-critic: verifies each relationship against source text.
Kills ungrounded relationships. Logs survival rate.
↓
[resolve.py] — Claude Opus via API ($5/$25 per M tokens)
Entity resolution: merges "US", "United States", "America" into one node.
Opus because this requires complex reasoning about identity.
↓
[[deduplication-module]] — Local Python (no LLM needed)
Deduplicates edges, aggregates confidence scores, computes source_count.
↓
[[export-module]] — Local Python (no LLM needed)
Writes data/[output-schema-file] matching the DATA_CONTRACT schema.
↓
Visualization (src/ — [graph rendering library] rendering)
The key insight: you only pay for Claude where local models can't do the job. Classification and summarisation are commodity tasks — your local [local-small-model] handles them. Extraction and resolution require the kind of nuanced reasoning that only a larger model provides. Your routing architecture is exactly right for minimising cost while maximising quality.
Use the Batch API for your corpus. You're processing [N] source documents. None of this needs real-time response. Submit as a batch, pay 50% less, get results within 24 hours.
Enable prompt caching. Your extraction prompt template is identical for every email. Cache it:
response = client.messages.create(
model="claude-sonnet-4-6-20250514",
max_tokens=4096,
system=[
{
"type": "text",
"text": "You are a [domain] analyst...[your full system prompt]...",
"cache_control": {"type": "ephemeral"} # Cache this
}
],
tools=tools,
messages=[{"role": "user", "content": f"<article>{email_body}</article>\n\nUse the extract_entities tool."}]
)Handle errors gracefully. The SDK has built-in retry logic, but your pipeline should handle:
RateLimitError→ automatic backoff (SDK handles this)APIStatusError→ log the error, skip the email, continue processingAPIConnectionError→ retry with exponential backoff- Malformed tool output → log, flag for manual review
Log token usage. After every call, record response.usage.input_tokens and response.usage.output_tokens. This lets you track actual costs per email and per pipeline run.
Store raw API responses. Before processing the extraction output, save the raw response to [pipeline-directory]responses/{email_id}.json. This lets you re-process extractions without re-calling the API if you change your downstream logic.
Claude plays three distinct roles in your system, each at a different layer:
Role 1: Extraction Engine (Pipeline Layer) This is the primary role. Claude reads newsletter text and extracts structured intelligence — entities, relationships, confidence scores. It operates as a function: text in, structured data out. No conversation, no memory, no agency. Just high-quality extraction powered by tool use.
Role 2: Quality Controller (Pipeline Layer) The CRAG critic pass. Claude reads the extraction output alongside the original text and judges whether each relationship is actually grounded in the source. This is a second, independent API call per email — the critic doesn't see the extractor's reasoning, only its output. This separation is what makes the actor-critic pattern work.
Role 3: Development Assistant (Build Layer) This is how you've been using Claude so far — via Claude Code and Claude.ai for building the product itself. This role goes away once the product is built. The API costs above are only for Roles 1 and 2.
Claude does NOT play these roles:
- Not an orchestrator at runtime. Your pipeline orchestration is Python code, not an LLM deciding what to do next. The
[llm-router-module]is deterministic routing, not Claude making decisions. - Not a user-facing chatbot. The [Your Project] is a visualisation, not a conversation. Users explore the graph, they don't talk to Claude.
- Not a real-time reasoning engine. All Claude calls happen in the batch pipeline, never in the rendering path. Your hard ban on LLM calls in
src/is exactly right.
Your architecture already handles this correctly via [llm-router-module]. The interaction model is:
- Ollama handles high-volume, low-complexity tasks (classification, summarisation) — free, private, fast
- Claude handles low-volume, high-complexity tasks (extraction, resolution) — paid, powerful, slower
- No task ever goes to both — the routing table assigns each task type to exactly one model tier
The [GPU VRAM budget] constraint on your [consumer GPU] means you can only run one local model at a time. Your keep_alive=0 rule (unload model after each task) is the right solution. The pipeline runs sequentially: classifier → summariser → (unload local model) → Claude API calls.
Your architecture — email ingestion, entity extraction, relationship graphing, temporal analysis — is domain-agnostic. The domain is your first vertical, but the same pipeline works for:
- Finance: Extract entities (companies, executives, regulators) and relationships (acquisition, partnership, investigation) from financial newsletters. The graph reveals who's connected to whom and how.
- Logistics & Supply Chain: Extract entities (ports, shipping companies, commodities, trade routes) and relationships (disruption, rerouting, sanctions impact). The graph reveals supply chain vulnerabilities.
- Cybersecurity: Extract entities (threat actors, malware families, vulnerabilities, targets) and relationships (attributed-to, exploits, targets). The graph reveals attack surface connections.
The [Your Project]'s value proposition generalises: take unstructured text from domain-specific sources, extract structured intelligence, render it as an explorable graph. The domain-specific parts are: the keyword classifier's vocabulary, the extraction prompt's entity taxonomy, and the visualisation's labelling. Everything else — the pipeline architecture, the CRAG critic, the routing, the graph rendering — transfers directly.
┌──────────────────────────────────────────────────────────┐
│ DATA SOURCES │
│ raw data (mbox/eml) → RSS feeds (future) │
└──────────────┬───────────────────────────────────────────┘
│
┌──────────────▼───────────────────────────────────────────┐
│ PIPELINE (Python) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │ keyword_ │ │ summarise │ │ [extraction-module] │ │
│ │ classifier │──│ .py │──│ (Claude API │ │
│ │ (Ollama) │ │ (Ollama) │ │ tool use) │ │
│ └─────────────┘ └─────────────┘ └────────┬─────────┘ │
│ │ │
│ ┌─────────────┐ ┌─────────────┐ ┌────────▼─────────┐ │
│ │ [export-module] │ │ deduplicate │ │ resolve.py │ │
│ │ → [output-schema-file]│──│ .py │──│ (Claude API │ │
│ │ │ │ (Python) │ │ entity res.) │ │
│ └──────┬──────┘ └─────────────┘ └──────────────────┘ │
│ │ │
│ ┌──────▼──────────────────────────────────────────────┐ │
│ │ [llm-router-module] — routes each task to the right model│ │
│ │ critic.py — CRAG actor-critic verification │ │
│ │ [circuit-breaker-module] — feed health management │ │
│ └─────────────────────────────────────────────────────┘ │
└──────────────┬───────────────────────────────────────────┘
│ data/[output-schema-file]
┌──────────────▼───────────────────────────────────────────┐
│ VISUALIZATION (JS/[graph rendering library]) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Background │ Graph │ Radar │ HUD │ UI │ │
│ │ Layer │ Layer │ Layer │ Layer │ Components│ │
│ │ (starfield)│ (nodes/ │ (sweep)│ (stats)│ (timeline,│ │
│ │ │ edges) │ │ │ filters) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Theme: [theme-file] (dark #050a12, cyan #00b4d8) │
│ Events: [event-bus-module] (cross-layer communication) │
│ Data: [graph-data-module] (loads + transforms [output-schema-file]) │
└──────────────────────────────────────────────────────────┘
Raw email text
→ keyword classifier (is this domain-specific?)
→ summarisation (2-3 sentence summary)
→ entity extraction (structured entities + relationships via Claude tool use)
→ critic verification (is each relationship grounded in source text?)
→ entity resolution (merge aliases into canonical entities)
→ deduplication (merge duplicate edges, aggregate confidence)
→ [output-schema-file] export (nodes, edges, communities, meta)
→ [graph rendering library] rendering (force-directed graph with command-center theme)
Classification reasoning (Ollama — pattern matching):
"Does this email contain domain-specific keywords?" → yes/no
Summarisation reasoning (Ollama — compression):
"What are the 2-3 most important facts in this email?" → summary
Extraction reasoning (Claude Sonnet — analytical):
"What entities exist? What relationships connect them? How confident
should I be based on the specificity of the source?" → structured data
Critic reasoning (Claude Sonnet — adversarial):
"For each extracted relationship, can I find the specific passage in the
source text that supports it? If not, it's hallucinated." → pass/fail
Resolution reasoning (Claude Opus — identity):
"Are '[Alias for Entity A]' and 'Russia' the same entity in this context?
What about '[Person A]' and 'the Russian government'?" → canonical mapping
┌─────────────────────────────────────────────────────┐
│ [llm-router-module] │
│ │
│ Task │ Model │ Cost │
│ ─────────────────────────────────────────────────── │
│ Keyword classification │ Ollama granite4 │ FREE │
│ Summarisation │ Ollama granite4 │ FREE │
│ Entity extraction │ Claude Sonnet │ $$ │
│ Critic verification │ Claude Sonnet │ $$ │
│ Entity resolution │ Claude Opus │ $$$ │
│ Code generation (worker) │ Ollama qwen2.5 │ FREE │
│ │
│ Routing rule: use the cheapest model that can do │
│ the job. Pay only for tasks that require reasoning │
│ beyond pattern-matching. │
└─────────────────────────────────────────────────────┘
Everything in the product reduces to one loop:
text → entities → relationships → graph → insight
If you deeply understand each arrow in that chain, you can build this. Here's what each arrow requires:
Text → Entities: Information extraction. You need to understand: named entity recognition (NER), entity types and taxonomies, how to write extraction prompts that produce consistent output, how to use Claude's tool use feature for structured extraction.
Entities → Relationships: Relationship extraction. You need to understand: how relationships are expressed in natural language (explicit vs. implied), how to define a relationship taxonomy that's useful without being overwhelming (your five types — [relationship-type-1], [relationship-type-2], [relationship-type-3] — are a good start), how to ground relationships in source text (the CRAG critic pattern).
Relationships → Graph: Graph construction. You need to understand: how to represent entities as nodes and relationships as edges, what metadata to attach to each (your data contract covers this), how to handle temporal data (relationships that have valid_from and valid_to), how to compute derived properties (centrality, community detection).
Graph → Insight: Visualisation and analysis. You need to understand: force-directed graph layout (how G6's force simulation works), visual encoding (how colour, size, opacity, and edge style communicate meaning), temporal filtering (how to scrub through time and see the graph change), interactive exploration (hovering, clicking, filtering, zooming).
Must learn immediately (blocks everything else):
-
The Anthropic Python SDK. Install it, make your first API call, use tool use for structured extraction, handle errors. Anthropic's courses at
the official Anthropic courses repositorycover this systematically. Budget: one afternoon. -
[graph rendering library] graph rendering. Create a graph from a JSON file, render it with a force layout, apply node/edge styles, handle events. Your
docs/[API_REFERENCE].mdhas the confirmed patterns. Budget: one afternoon. -
Python email parsing. Read
.mboxor.emlfiles, extract the text body, handle MIME encoding. Theemailandmailboxstandard library modules handle this. Budget: two hours.
Should learn soon (improves quality):
-
Graph algorithms: Betweenness centrality, community detection (Louvain algorithm), PageRank. NetworkX implements all of these in Python. You compute them on the graph data and attach them as node properties before exporting to
[output-schema-file]. -
Prompt engineering for extraction. How to write extraction prompts that produce consistent, high-quality output. Anthropic's prompt engineering course covers this. Key techniques: few-shot examples, structured output via tool use, chain-of-thought for complex reasoning.
-
Basic graph theory. Directed vs. undirected graphs, weighted edges, connected components, shortest paths. You don't need a textbook — a 2-hour YouTube introduction is sufficient for what you're building.
Can learn later (production quality):
-
Entity resolution techniques. The section above (Part 4) gives you the starting point. Implement LLM-based resolution first, learn the formal methods when you need to scale.
-
Batch processing patterns. How to process thousands of API calls efficiently: batching, retry logic, progress tracking, checkpointing (so you can resume after a failure without reprocessing everything).
-
Graph database fundamentals. If the project grows beyond SQLite + JSON, you might want Neo4j or a similar graph database. But for your current scale (thousands of nodes, not millions),
[output-schema-file]is the right choice.
You have been working toward this product vision for some time. The technology stack is finally ready — LLMs that can extract entities from unstructured text, graph libraries that can render thousands of nodes in a browser, local models that run on consumer hardware. The gap was always that extraction required either expensive NLP pipelines or manual coding. LLMs close that gap.
What you need to do now is stop building infrastructure and start building the product. Specifically:
This week: Get an API key. Make one API call to Claude with one email from your corpus. See the structured extraction output. Confirm it extracts the entities and relationships you expect.
Next session: Process 50 source documents through the pipeline (keyword classifier → summariser → extractor → export). Produce a real [output-schema-file]. Don't build the critic, don't build resolution, don't build deduplication. Just get raw data into the graph format.
The session after that: Render the [output-schema-file] in G6. Nodes coloured by type, edges sized by weight, command-centre theme applied. See your knowledge graph for the first time.
Then iterate. Add the critic. Add resolution. Add community detection. Add the timeline. Add the radar sweep. Each is a session, each builds on a working core, each produces a visible improvement.
The architecture is ready. The development system is ready. The technology exists. The only thing between you and a working product is the implementation itself — and that starts with a single API call.
Once the core pipeline is running in production, the next phase is connecting it to a governance framework. This is not optional for production AI systems — it is what makes the system trustworthy and maintainable over time.
What to do:
- Designate an AI owner: who is accountable for the outputs this system produces?
- Complete the responsible AI checklist () — it identifies any outstanding gaps before the system serves real users
- Connect your evaluation metrics to the governance checklist: the quality score from your evaluation harness is the primary signal that governance reviews examine
- Configure audit logging: every model invocation should produce a structured log entry capturing the model used, input hash, output hash, latency, cost, and timestamp
- Document the human override mechanism: how can a user or operator correct or override an AI output? This path must be implemented and tested before production
Outputs from this phase:
- Completed governance review ()
- Active audit log with at least one week of production data
- Risk register signed off by AI governance lead ()
With governance in place, the final infrastructure phase is full observability. The goal is to be able to answer these questions at any time: Is the system performing correctly? Is quality degrading? Is cost within budget?
What to set up:
- Cost dashboard: daily spend, cost per document processed, cumulative cost vs. budget. Alert if daily spend exceeds 2× the recent average
- Quality dashboard: evaluation score trend over time (weekly regression), human review sample results, confidence score distribution
- Operational dashboard: pipeline success/failure rate per stage, average latency per stage, queue depth if using async processing
- Eval cadence: schedule a weekly regression evaluation run against your golden set; flag any score decrease > 5% for investigation
When to trigger a manual review:
- Evaluation score drops below the baseline threshold defined in your evaluation plan
- Cost per document increases > 20% week-over-week without a corresponding increase in document volume
- Error rate for any pipeline stage exceeds 2% over a 24-hour window
- Any model change (version upgrade, prompt change) — always run the full eval suite before serving the change to all users
Outputs from this phase:
- Active observability dashboards with defined alert thresholds
- Completed evaluation plan () with first baseline run
- Documented rollback procedure for model or prompt changes
This document is a companion to the [Your Project] CLAUDE.md and should be stored in docs/ for reference. It does not need to be loaded every session — it's a learning resource, not an operational instruction.