Skip to main content

structured-extraction

Use when text must become a typed, schema-conformant object you can trust — pulling fields into a fixed JSON shape, extracting line items as typed records, classifying into enums, and building the Pydantic or Zod model plus the validate-and-retry loop. Covers extractors that throw parse errors, leak markdown fences, or fabricate a value where the field is absent instead of returning null. NOT getting the text out of a PDF, scan or DOCX first (that is `document-processing`), NOT general prompt craft untied to a schema (that is `prompt-engineering`).

소스 정보

저장소
ericrisco/rsc-harness
최근 소스 활동
2026년 7월 29일 23:33
감지된 SKILL.md 언어
영어
스타
110
포크
9

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

파일 탐색기
5 개 파일

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
structured-extraction
description
Use when text must become a typed, schema-conformant object you can trust — pulling fields into a fixed JSON shape, extracting line items as typed records, classifying into enums, and building the Pydantic or Zod model plus the validate-and-retry loop. Covers extractors that throw parse errors, leak markdown fences, or fabricate a value where the field is absent instead of returning null. NOT getting the text out of a PDF, scan or DOCX first (that is `document-processing`), NOT general prompt craft untied to a schema (that is `prompt-engineering`).
tags
["structured-outputs","json-schema","pydantic","extraction","validation","llm"]
recommends
["document-processing","prompt-engineering","llm-pipeline","agent-eval","rag","data-cleaning"]
profiles
[]
origin
risco
# Structured extraction — text in, a typed object you can trust out The deliverable is **a typed object that conforms to a schema you defined** — not prose, not "roughly JSON." The whole skill rests on one distinction the rest of the file keeps returning to: > Native structured outputs make the JSON **valid and typed**. They never make the values **correct**. Constrained decoding guarantees the model cannot emit a token that breaks your schema, so `JSON.parse` errors, missing keys, wrong types, and stray markdown fences disappear at the source. It does **nothing** to stop the model from putting a plausible-but-wrong email in a `string` field, snapping a fuzzy category to the wrong enum, or coercing `"$1,200"` into `1200.0` when the currency mattered. Owning both halves — the shape (decoding) and the values (validation) — is this skill. If you only do the first half you ship a database full of well-typed lies. **Boundary test (bytes vs. schema).** If the input is a PDF, scan, DOCX, or HTML and the deliverable is the *raw text/Markdown/cells* of that document, that is upstream: [`document-processing`](../document-processing/SKILL.md) produces the text, this skill turns that text into typed fields. If you're holding text and want it shaped, you're in the right place. Current as of 2026-06-02: OpenAI Structured Outputs (`strict: true` json_schema), Anthropic Structured Outputs (GA since the 2025-11-14 public beta; `output_config.format`), and Instructor (built on Pydantic, ~3M downloads/month). Exact request/response shapes and the per-provider limit tables live in [`references/providers.md`](references/providers.md) so this file stays lean. ## Default: native constrained decoding when the provider has it If the model and provider support native structured outputs, use them. This is not a tuning knob — it is the difference between ~100% schema conformance and hoping a regex catches the fence. **Bad — prompt-and-pray, then parse raw text:** ```python resp = client.chat.completions.create( model="gpt-5.1", messages=[{"role": "user", "content": f"Return JSON with name and email:\n{text}"}], ) data = json.loads(resp.choices[0].message.content) # markdown fence / preamble / missing key -> crash ``` **Good — OpenAI strict json_schema (Chat Completions):** ```python resp = client.chat.completions.create( model="gpt-5.1", messages=[{"role": "user", "content": text}], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "strict": True, "schema": { "type": "object", "additionalProperties": False, "required": ["name", "email"], "properties": { "name": {"type": "string", "description": "Full name as written."}, "email": {"type": ["string", "null"], "description": "Email exactly as written, or null if none is stated."}, }, }, }, }, ) data = json.loads(resp.choices[0].message.content) # now guaranteed valid + typed ``` On the OpenAI Responses API the same block moves under `text.format` instead of `response_format`. On Anthropic, the equivalent is `output_config={"format": {"type": "json_schema", "schema": {...}}}` on Claude Opus 4.5–4.8 / Sonnet 4.5+ / Haiku 4.5; Anthropic compiles your schema into a grammar and **caches it for 24h**, and the SDKs ship helpers (`client.messages.parse(...)` in Python, `zodOutputFormat(schema)` in TS). The older deprecated `output_format` param and the deprecated `structured-outputs-2025-11-13` beta header still work in a transition window — do not write new code against them. Full shapes in [`references/providers.md`](references/providers.md). **The non-negotiable strict-schema rule (OpenAI and Anthropic both):** - every object sets `"additionalProperties": false`; - **every** property is listed in `required`; - "optional" is expressed as a **union with `null`** (`"type": ["string", "null"]`), never by leaving the field out of `required`. Omitting it is the single most common strict-mode error. ## Schema design rules (each prevents a specific failure) | You want | Express it as | Because | | --- | --- | --- | | A field that may be absent | nullable union `["string","null"]` + `description: "...or null if not stated"` | A non-null type *forces* a value, so the model fabricates one. Nullable + instruction yields `null` instead. | | A closed set of categories | `enum: ["open","pending","closed"]` | Free-text `string` drifts ("Open", "in progress", "closd"); an enum makes drift impossible to emit. | | Many items of one kind | one object schema + a top-level `{"items": {"type":"array","items": <object>}}` wrapper | One object per extraction unit keeps each record independently validatable; arrays of scalars lose field structure. | | The model to read your intent | a `description` on every property | The model reads field descriptions at decode time; "amount in cents, no currency symbol" beats a bare `integer`. | | A number in a range / a regex / a length cap | leave it **out** of the schema; enforce in a post-decode validator | Strict modes reject or silently ignore `minimum`/`maximum`/`minLength`/`maxLength`/complex regex — see the unsupported-features table in references. | | A deeply nested or recursive shape | flatten it, or split into two extractions | Native modes reject recursion and cap nesting/complexity; flat schemas decode reliably. | Keep schemas **flat and shallow**. If you find yourself nesting four levels deep or describing a tree, that is two extractions, not one heroic schema. ## The reliability ladder — escalate only on failure, and cap it Climb from the cheapest mechanism upward. Each rung catches what the rung below cannot; you stop at the first rung that holds for your data. | Rung | Mechanism | Catches | Does NOT catch | When you stop here | | --- | --- | --- | --- | --- | | 1 | Native constrained decoding | parse errors, wrong types, missing keys, fences | wrong values, bad units, wrong enum | shape+types only, latest single provider | | 2 | Pydantic / Zod validation after decode | out-of-range, bad format, cross-field contradictions, null-vs-absent | nothing the model genuinely got wrong | value rules you can express as code | | 3 | Bounded reask (Instructor or hand-rolled) | semantic errors the model can fix when shown the validation message | systematic model blind spots | residual errors; **cap retries (e.g. 2) and log every reask** | | 4 | Human / log review | everything still wrong after 3 | — | high-stakes fields or low-confidence rows | Rung 1 is mandatory when available. Rung 2 is mandatory the moment any field has a *value* rule (a range, a format, a "must match the order date") — because rung 1 structurally cannot enforce values. Rungs 3 and 4 are opt-in. **Never** make rung 3 unbounded: a retry loop with no cap turns one bad document into an unbounded bill. ## Value validation the schema can't enforce This is the half native decoding leaves on the table. Validate values *after* you have a typed object. **Pydantic — value rules + normalization the schema can't carry:** ```python from pydantic import BaseModel, field_validator class Order(BaseModel): amount_cents: int discount_pct: float | None # nullable: may be absent order_date: str # we'll normalize to ISO @field_validator("discount_pct") @classmethod def pct_in_range(cls, v): if v is not None and not (0 <= v <= 100): raise ValueError("discount_pct must be between 0 and 100") return v @field_validator("amount_cents", mode="before") @classmethod def strip_currency(cls, v): if isinstance(v, str): # "$1,200.00" -> 120000 return int(round(float(v.replace("$", "").replace(",", "")) * 100)) return v ``` The Zod equivalent uses `.refine()` for cross-field and range checks and `.transform()` for normalization. Three normalizations bite constantly: **currency** (`"$1,200"` vs `1200` vs `120000` cents — pick one and enforce it), **dates** (free text → ISO 8601, and decide what a missing year means), and **enum snapping** (the model rounds "kinda urgent" to `urgent`; validate that the snap was legitimate, or widen the enum). **Null vs. absent.** A nullable field with a clear instruction is the entire fix for "the model invents an email." `"email": {"type": ["string","null"], "description": "...or null if the text states no email"}` plus a one-line system instruction ("use null for any field not present in the source; never guess"). If you make the field non-nullable, you have *told the model to produce a value* — it will. **Bounded reask with Instructor** — failed validation is fed back to the model as an error message: ```python import instructor client = instructor.from_provider("openai/gpt-5.1") order = client.chat.completions.create( response_model=Order, # your Pydantic model, validators and all max_retries=2, # BOUND it; each retry is another paid call messages=[{"role": "user", "content": text}], ) ``` On a validation failure Instructor reasks with the `ValueError` text, so `@field_validator` rules the model never saw in the schema still get enforced through the loop. Log every reask (count + reason): a quietly climbing reask rate is your early signal that a field's instruction or schema is wrong. ## Multi-provider: Instructor `from_provider` When you want one Pydantic model to run across OpenAI, Anthropic, and local backends without rewriting per SDK, use Instructor's unified entrypoint: ```python client = instructor.from_provider("anthropic/claude-opus-4-8") # or "openai/gpt-5.1", "ollama/llama3.3" ``` Reach for Instructor when you need **provider portability** or **value-level validation with reask**. Reach for the **native SDK helper** (`client.messages.parse`, `zodOutputFormat`) when you're on one provider and want the simplest path with the fewest dependencies. Both sit on the same native decoding underneath. ## Scale and accuracy live next door This skill is the single extraction node and its per-call validation loop. Two concerns are explicitly *not* here: - Running 10k documents — batching, idempotency, retries-across-calls, cost/latency budgeting, multi-step chains → [`llm-pipeline`](../llm-pipeline/SKILL.md). - Measuring extraction quality offline — a golden set, precision/recall, a regression gate that fails CI when accuracy drops → [`agent-eval`](../agent-eval/SKILL.md). This skill *builds* the extractor; that one *scores* it. - Answering questions over a corpus by retrieving chunks → [`rag`](../rag/SKILL.md). - Post-extraction tabular cleanup (dedupe rows, coerce columns, normalize categories across a whole dataset) → [`data-cleaning`](../data-cleaning/SKILL.md). - Improving a prompt that isn't bound to a schema → [`prompt-engineering`](../prompt-engineering/SKILL.md). (Some routed siblings may not be built in this collection yet; the routing decision still holds.) ## Anti-patterns | Bad | Why it bites | Good | | --- | --- | --- | | `json.loads(resp.text)` on raw model output | markdown fence, chatty preamble, or a missing key crashes at runtime | native structured outputs; parse only a decoder-guaranteed string | | Stripping ` ```json ` fences with a regex | treats the symptom; the model can still drop a key or change a type | turn on native decoding — the fence never appears | | `"type": "string"` on a field that's often absent | forces a value, so the model fabricates a plausible wrong one | nullable union `["string","null"]` + "null if not stated" | | Omitting an optional field from `required` (strict mode) | OpenAI/Anthropic strict reject it — *every* property must be in `required` | keep it in `required`, make its type a union with `null` | | `minimum`/`maxLength`/lookahead-regex inside a strict schema | rejected or silently ignored — the constraint does nothing | leave value rules out of the schema; enforce in a Pydantic/Zod validator | | `max_retries` unbounded (or a `while` reask loop) | one bad doc becomes an unbounded bill and a hung job | cap at 2–3, log each reask, route the rest to review | | Deep/recursive schema in one call | native modes reject recursion and cap complexity → compile failure | flatten, or split into multiple extractions | | Trusting decoding to make values *correct* | valid+typed ≠ true; you ship well-formed wrong data | add the rung-2 validation step for every value rule | | Building on Anthropic `output_format` / `structured-outputs-2025-11-13` header | deprecated transition-window API | use `output_config={"format": {...}}` | | One giant array of scalars for "many things" | loses per-item field structure and per-item validation | one object schema per unit, wrapped in a top-level `items` array | ## Checklist before you ship an extractor 1. Native structured outputs ON (or a documented reason the provider has no native mode). 2. Every object has `additionalProperties: false`; every property is in `required`. 3. Every maybe-absent field is nullable with a "use null if not stated" instruction. 4. Closed sets are enums; value rules (ranges/format/cross-field) live in validators, not the schema. 5. A reask loop, if any, is **bounded and logged**. 6. You can state, for each field, whether a wrong output would be caught by decoding (shape) or only by validation (value) — and you have the validation for the value cases.
GitHub에서 보기