惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LINUX DO - 最新话题
G
Google Developers Blog
J
Java Code Geeks
The GitHub Blog
The GitHub Blog
F
Full Disclosure
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Vercel News
Vercel News
酷 壳 – CoolShell
酷 壳 – CoolShell
Recent Announcements
Recent Announcements
Help Net Security
Help Net Security
The Hacker News
The Hacker News
IT之家
IT之家
Y
Y Combinator Blog
Martin Fowler
Martin Fowler
L
Lohrmann on Cybersecurity
C
CERT Recently Published Vulnerability Notes
V
Visual Studio Blog
博客园 - 聂微东
Hacker News: Ask HN
Hacker News: Ask HN
H
Hacker News: Front Page
Know Your Adversary
Know Your Adversary
Security Latest
Security Latest
Security Archives - TechRepublic
Security Archives - TechRepublic
Simon Willison's Weblog
Simon Willison's Weblog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Troy Hunt's Blog
Last Week in AI
Last Week in AI
Schneier on Security
Schneier on Security
N
News and Events Feed by Topic
博客园 - 【当耐特】
有赞技术团队
有赞技术团队
AWS News Blog
AWS News Blog
Blog — PlanetScale
Blog — PlanetScale
博客园_首页
Google DeepMind News
Google DeepMind News
Cloudbric
Cloudbric
N
News | PayPal Newsroom
A
About on SuperTechFans
S
Schneier on Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Hugging Face - Blog
Hugging Face - Blog
M
MIT News - Artificial intelligence
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
T
The Exploit Database - CXSecurity.com
罗磊的独立博客
K
Kaspersky official blog
The Cloudflare Blog
I
Intezer

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
How I built an AI RAG system to convert PDF to Q&As
longtermemor · 2026-04-23 · via DEV Community

Turning a PDF into a set of spaced repetition flashcards sounds like a single step. Upload file, get questions. In practice it is five distinct engineering stages, each with its own failure modes, and at least two of them surprised me in ways I did not anticipate when I first sketched the design.

If you want to see the end result in action, try it at LongTermMemory before reading the rest.

This post walks through the full document processing pipeline behind LongTermMemory: how text gets extracted from different file formats, how it gets split into semantically coherent chunks, how those chunks land in a vector database, and how retrieval augmented generation makes the final questions better than they would be if each chunk were processed in isolation.

The Architecture in Brief

The backend is split across two services. Laravel handles authentication, project management, file uploads to MinIO, and persisting the generated question and answer pairs to MySQL. A separate FastAPI service, written in Python, owns everything that requires AI: document processing, embedding generation, vector storage, and question generation.

The two services communicate asynchronously. Laravel dispatches a job to Celery via Redis. The FastAPI worker picks it up, runs the full pipeline, and POSTs the results back to a Laravel webhook. Laravel stores them and notifies the frontend via Server Sent Events that the job is done.

The pipeline itself has five stages:

  1. Download the document from MinIO
  2. Extract text, format by format
  3. Semantic chunking
  4. Embed and store vectors in Qdrant
  5. Generate question and answer pairs with retrieval context

Stage 1: Getting Text Out of the Document

The service supports PDF, DOCX, and XLSX. Each format has its own extraction path.

DOCX and XLSX are straightforward. python-docx walks paragraphs and tables; openpyxl iterates sheets and rows. The output is joined with double newlines to preserve paragraph structure.

PDF is where the interesting engineering happens. The naive approach, calling page.get_text() directly, produces incoherent output for any document with a two column layout. Academic papers, textbooks, and journal articles all share this format, and naive extraction interleaves the columns instead of reading each one from top to bottom.

The fix is block based extraction with column detection. The algorithm sorts text blocks horizontally, detects gaps wider than 10% of the page width as column boundaries, then sorts each column vertically. Column one top to bottom, then column two top to bottom. Reading order preserved.

There is also a metadata filter that strips running headers, footers, and page numbers without touching content. The heuristic checks for short numeric text, Roman numeral patterns, and short text appearing in the top or bottom 5% of the page. The 5% margin was chosen carefully: a wider margin starts clipping section headings, which are real content.

I initially used default text extraction and did not notice the problem until I tested with a biology textbook. Half the generated questions were incoherent because the extraction had interleaved columns. I assumed this would be an edge case. It turned out to affect every academic PDF in the test set.

Stage 2: Semantic Chunking

Raw text goes into a two stage chunking pipeline using LlamaIndex.

The first stage uses SentenceSplitter with paragraph boundaries (\n\n) to produce structurally coherent initial chunks. The second stage uses SemanticSplitterNodeParser to merge or split those chunks based on embedding similarity. Semantically related sentences stay together; topic shifts become chunk boundaries.

The breakpoint_percentile_threshold is set to 95. This means the splitter only creates a new boundary when the cosine dissimilarity between adjacent sentence groups exceeds the 95th percentile of all dissimilarities in the document. The setting is conservative on purpose: splitting too aggressively produces small, context poor chunks that generate weak questions.

For long documents, the parameters shift dynamically. A document that exceeds a configurable token threshold gets larger initial chunk sizes and a higher breakpoint threshold. This reduces the number of embedding API calls at the cost of slightly less granular questions. For a 300 page textbook, a small reduction in granularity is a reasonable tradeoff to keep costs under control.

One thing worth noting: semantic chunking is expensive for large documents. The second stage embeds every sentence group to compute similarity scores. For a long document, that is a significant number of API calls before question generation even starts. A simpler paragraph based split with fixed sizes would be faster. The tradeoff is real and scales with document length.

Stage 3: Section Titles and Vector Storage

Each chunk needs a section title so the language model knows what it is reading. The approach has two priority levels.

Priority one is structural headings. The parser looks for headings in node metadata and in the first line of each chunk. Numbered section patterns, common keywords like Introduction or Conclusion, or title case short lines all qualify.

Priority two is LLM generated titles. When no structural heading is found, GPT-3.5-turbo generates a title from the first 500 characters of the chunk. The prompt is tight: 10 words maximum, no preamble, just the title. Temperature is 0.3 to keep output deterministic.

At the end of processing, the service logs a breakdown: how many chunks got structural headings, how many got generated titles, and how many ended up with neither. That last number is an error condition, not a warning.

Each chunk is then embedded using text-embedding-3-small in batches of 25. Embeddings land in Qdrant, scoped per project. A collection named project_42 holds all vectors for project 42. The chunk text is stored directly in the vector payload alongside metadata: user ID, project ID, document ID, chunk index, section title, and page number. Storing the full text in the payload avoids a round trip back to the database during retrieval.

Stage 4: RAG Context Enrichment

This is the part I most underestimated.

The naive approach is to generate one question and answer pair per chunk in isolation. That works, but it produces questions that miss connections across the document. A question about the role of mitochondria is weaker if the model does not know the same document has a section on ATP synthesis two chunks away.

The fix is to retrieve semantically related chunks before generating each question. For every chunk, the service embeds the chunk text, queries Qdrant for the top similar chunks, filters out the chunk itself and any result below a similarity score of 0.7, and passes the remaining related chunks into the generation prompt as supplementary context.

The 0.7 threshold was tuned empirically. Below it, unrelated chunks start appearing in the context. Above 0.8, too many chunks find no related context at all.

The generation prompt assembles five sections in order: the user's learning goals if they provided any, document context including title and section, the primary chunk, the related chunks with their similarity scores, and the base system instructions.

User notes are sanitized before insertion. The service runs a validation function that checks for prompt injection patterns, truncates to 2000 characters, and wraps the content in delimiters so the model is told to treat only the content between USER_NOTES_START and USER_NOTES_END as user input. It is a partial mitigation, not a complete solution, but it covers the common cases.

The model responds with structured JSON: question, answer, key concepts, and difficulty level. Key concepts and difficulty are stored and surfaced in the study UI so users can see what each card tests and filter by difficulty.

Async Orchestration and Progress Tracking

The pipeline is fully async. The Celery worker updates a Redis key at each stage, from 40% at text extraction through to 95% when costs are being calculated. The frontend polls a status endpoint and renders a live progress bar.

Progress tracking through Redis is worth the complexity. Early versions had no feedback: users saw a spinner and waited. Adding per stage updates and a Server Sent Events endpoint on the Laravel side was about a day of work. Users tolerate a three minute wait much better when they can see the pipeline moving than when the UI is silent.

Worker configuration also matters. worker_prefetch_multiplier=1 ensures one task at a time: document processing is memory intensive and running tasks in parallel on the same worker causes out of memory kills. worker_max_tasks_per_child=100 recycles the worker process after 100 tasks to prevent memory accumulation from PyMuPDF and openpyxl state.

What I Would Do Differently

Track real API costs from the OpenAI response object rather than estimating. The estimate tracks within 10% of actual cost, but reading response.usage.prompt_tokens and response.usage.completion_tokens from every call would remove the approximation entirely. It is about 10 lines of code.

Move section title generation into an async batch after chunking completes. Currently it runs synchronously inside the chunking stage, which serializes title generation across all chunks. Running it as a parallel batch would reduce overall latency.

Populate the page number field consistently across all extraction paths. The data model has the field; it is just not reliably filled in for every format. Surfacing the source page in the UI so users can look up the original passage would be a straightforward quality of life improvement.

Closing Thought

The pipeline from upload to flashcard is less visible than the spaced repetition scheduler, but it is more load bearing. If the questions are poor, no scheduling algorithm fixes them. Extraction quality, chunking strategy, and context enrichment all directly determine what ends up in the study queue.

Try the full pipeline yourself at LongTermMemory and see what it generates from your own materials.