How to Implement Structured Knowledge Extraction from Documents: A Complete Two-Stage Pipeline
Structured knowledge extraction from documents requires a two-stage pipeline that first discovers a modular schema from corpus data, then extracts structured JSON fact cards using LLM prompts tailored to that schema.
This guide explains the implementation found in the bojieli/ai-agent-book repository, which demonstrates how to transform unstructured legal texts (or any document collection) into a queryable structured knowledge base. The approach separates schema generation from per-document processing, enabling efficient caching and clean integration with downstream retrieval systems.
Understanding the Two-Stage Extraction Architecture
The repository implements a decoupled pipeline that distinguishes between schema creation and document processing. This architecture, found in chapter3/structured-knowledge-extraction/, solves the cold-start problem of structured extraction by letting the LLM discover what factors matter before attempting to extract them.
- Stage 1 (Factor Discovery): Analyzes corpus batches to generate a reusable
schema.jsoncontaining modular factor definitions. - Stage 2 (Structured Extraction): Applies the cached schema to individual documents, outputting consistent JSON objects that downstream agents can consume.
Stage 1: Factor Discovery and Schema Generation
The discovery.py module implements bottom-up schema generation that avoids hard-coded ontologies. Instead of manually defining extraction fields, the system prompts an LLM to identify relevant factors from sample documents.
How Discovery Works
- Batch Processing: Raw case facts are processed in batches (default size: 12) via the
discover_schema()function. - Factor Listing: Each batch prompt asks the model to list every factor that could affect a legal sentence.
- Schema Consolidation: A second LLM call consolidates raw factor lists across all batches into a modular schema containing:
- core – Factors common to all document types (e.g., "self-surrender", "compensation").
- extensions – Type-specific factors (e.g., "amount involved" for theft cases).
- Disk Caching: The resulting schema is cached at
data/schema.jsonto avoid redundant API calls across experiments.
Key functions in chapter3/structured-knowledge-extraction/discovery.py include discover_schema() for generation and load_schema() for retrieval, alongside factors_for_charge() and all_factors() for schema querying.
# Generate schema from corpus
from chapter3.structured_knowledge_extraction.discovery import discover_schema, load_schema
cases = [
{"id": "c1", "charge": "盗窃罪", "fact": "..."},
{"id": "c2", "charge": "故意伤害罪", "fact": "..."},
]
# Build and cache schema
schema = discover_schema(cases, batch_size=12, verbose=True)
# Inspect results
print(schema["core"][:3]) # Universal factors
print(schema["extensions"]["盗窃罪"][:2]) # Theft-specific factors
Stage 2: Per-Document Structured Extraction
The extractor.py module handles the runtime extraction phase, converting raw text into structured JSON using the previously generated schema.
Extraction Workflow
- Charge Detection: If not provided,
extract_one()uses a lightweight LLM prompt to determine the document category (e.g., crime type). - Factor Retrieval: The system calls
factors_for_charge()to retrieve only the relevant schema subset for that document type. - Structured Generation: A second LLM prompt outputs a JSON object where each factor key contains the extracted value or
nullwhen information is missing. - Normalization: The internal
_normalize()method cleans raw JSON output, handling numeric conversion, boolean normalization, and null consistency to guarantee a predictable data shape.
# Extract from single document
from chapter3.structured_knowledge_extraction.extractor import extract_one
from chapter3.structured_knowledge_extraction.discovery import load_schema
schema = load_schema()
fact_text = """
被告人于2023年5月1日盗窃价值人民币12,000元的手机...
"""
structured = extract_one(fact_text, schema=schema)
print(structured)
Typical output:
{
"charge": "盗窃罪",
"amount": 12000,
"self_surrender": null,
"compensation": true
}
Bulk Processing with Caching
For production datasets, extract_dataset() in chapter3/structured-knowledge-extraction/extractor.py processes entire corpora with intelligent caching to extracted.jsonl. This dramatically reduces API usage after the first pass by reusing cached extractions.
# Bulk extraction
from chapter3.structured_knowledge_extraction.extractor import extract_dataset
schema = load_schema()
results = extract_dataset(schema, use_cache=True, verbose=True)
# Returns list of dicts: original fields + `extracted` JSON
Configuration and Client Management
The config.py module centralizes LLM configuration, reading API keys from environment variables and instantiating OpenAI clients with retry logic. As implemented in chapter3/structured-knowledge-extraction/config.py, the default model is gpt-5.6-luna, though the architecture supports OpenAI or OpenRouter backends.
# Configuration pattern from config.py
from chapter3.structured_knowledge_extraction.config import get_client
client = get_client() # Handles retries, timeouts, and model selection
Integrating with Downstream Retrieval Systems
The extraction pipeline feeds directly into RAG architectures through schema utilities. The all_factors() function returns a flattened list of factor definitions suitable for vector indexing.
# Integration with GraphRAG
from chapter3.structured_knowledge_extraction.discovery import load_schema, all_factors
from chapter3.structured_index.graphrag_indexer import GraphRAGIndexer
schema = load_schema()
vectors = all_factors(schema) # Structured entities for indexing
indexer = GraphRAGIndexer(vectors=vectors)
According to the bojieli/ai-agent-book source code, this integration enables the GraphRAGIndexer (found in chapter3/structured-index/graphrag_indexer.py) to reason over structured factor embeddings rather than raw text chunks, improving retrieval precision for agent queries.
Summary
- Use
discovery.pyfor bottom-up schema generation that separates core (universal) from extensions (type-specific) factors, caching results todata/schema.json. - Use
extractor.pyfor runtime JSON extraction, leveragingextract_one()for single documents orextract_dataset()for bulk processing withextracted.jsonlcaching. - Reference
config.pyfor centralized LLM client management with environment-based API key handling and default modelgpt-5.6-luna. - Leverage
load_schema()andall_factors()to bridge extraction pipelines with downstream RAG or knowledge-graph indexers.
Frequently Asked Questions
How does the two-stage pipeline reduce API costs compared to single-pass extraction?
The Factor Discovery stage runs once per corpus to generate a reusable schema.json, while Structured Extraction caches results to extracted.jsonl. According to the implementation in chapter3/structured-knowledge-extraction/extractor.py, subsequent runs with use_cache=True skip LLM calls entirely for previously processed documents, eliminating redundant API usage across experiments.
What happens when a document contains factors not present in the cached schema?
The factors_for_charge() function retrieves only the factors defined for the detected document type. If the schema lacks coverage for specific edge cases, the extraction prompt in extractor.py instructs the model to return null for missing values rather than hallucinating new fields. To handle new factor types, you must re-run discover_schema() on an updated corpus batch.
Can this pipeline work with non-legal documents?
Yes. While the bojieli/ai-agent-book repository uses legal cases as the demonstration corpus, the schema generation logic in discovery.py is domain-agnostic. Simply provide a list of documents with charge (category) and fact (text) fields, and the pipeline will discover relevant factors for your specific document type.
How is the extracted JSON normalized for downstream consistency?
The extractor.py module includes a private _normalize() method that post-processes raw LLM output to ensure numeric fields are converted to proper numbers, boolean values are standardized, and missing fields are explicitly set to null. This guarantees that all entries in extracted.jsonl conform to the schema contract expected by downstream agents.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →