Treat the Context Window as a Data Assembly Problem A developer argues that assembling context for large language models is fundamentally a data assembly problem, not a prompt engineering one. The author introduces pydantic-resolve as a tool to structure context assembly declaratively, similar to how API response assembly is handled in FastAPI, to avoid procedural code that mixes database queries, vector retrieval, and LLM calls. Treat the Context Window as a Data Assembly Problem: Where pydantic-resolve Fits in AI Workflows A typical piece of AI code Open any service in your project that calls an LLM. You will most likely see a function that looks something like this: php async def build support context ticket id: int - str: ticket = await db.get Ticket, ticket id customer = await db.get Customer, ticket.customer id recent tickets = await db.query Ticket .filter Ticket.customer id == customer.id .order by Ticket.created at.desc .limit 5 .all Retrieve similar past tickets embedding = await embed ticket.description similar = await vector store.search embedding, top k=3 Each similar ticket needs its resolution pulled in similar with resolution = for s in similar: resolution = await db.query Resolution .filter Resolution.ticket id == s.id .first similar with resolution.append { "title": s.title, "resolution": resolution.text if resolution else "", } Collect tags all tags = for t in recent tickets: all tags.extend t.tags Finally, ask the LLM to summarize summary = await llm.summarize customer=customer, recent tickets=recent tickets, similar=similar with resolution, return f""" Customer: {customer.name} id={customer.id} Recent tickets: {len recent tickets } Tags: {', '.join set all tags } Similar past cases: {format similar similar with resolution } Summary: {summary} """ The function is not long, but the problem is already visible: this is a build context function that is fundamentally doing data assembly, but its shape is entirely procedural . It is isomorphic to the FastAPI code that Clean Architecture for Python ../architecture entity first/ criticizes — only "assembling an API response" has been swapped for "assembling a prompt context". The problems are unchanged: - Data-fetching logic is scattered through the function body with no structure. - Dependencies of derived fields all tags , summary are held together by comments and line ordering. - Vector retrieval, database queries, and LLM calls live in one function. Every new piece of context means editing this function. - Concurrency optimization fetching similar tickets in parallel requires a rewrite. - Reuse — say, exposing recent tickets to the frontend too — is impossible. This code is not badly written. It has no home . "The context window" is a data assembly problem When people discuss LLM applications, attention usually lands first on prompt templates, model choice, and temperature. Those matter — but as applications grow, the real bottleneck shifts from prompt engineering to context assembly . The reason: prompt templates are stable, model choice is stable, but "what data to feed the LLM" differs on every call . A support agent handling ticket A and ticket B can share the same prompt template, yet the underlying data-assembly path may diverge completely — A is a VIP customer requiring SLA context and similar-case retrieval; B is a regular customer needing only the basics. This "same template, different data-assembly path" requirement is exactly what API response assembly does . Your FastAPI project already solves it — different endpoints assemble different response trees. An LLM context is just another endpoint, only the consumer is an LLM rather than an HTTP client. Once that perspective lands, the problem becomes concrete. The things pydantic-resolve solves well on the API side hold equally well on the LLM side: | API response assembly | LLM context assembly | |---|---| | Multi-level nesting Sprint → Task → Owner | Multi-level nesting Customer → Ticket → Similar Ticket | | Batch-load related data | Batch-recall related context | Derived fields task count , contributors | Derived context summary , aggregated tags | | N+1 database queries | N+1 vector retrievals + N+1 LLM calls | | Cross-subtree aggregation deduplicate all owners | Cross-subtree aggregation merge evidence across similar tickets | Every item in the right column already has a solution on the left. We only need to bring the same machinery over. Three classic assembly pain points Breaking the build support context snippet apart reveals three symptom classes. They are not specific to support scenarios — they recur in nearly every LLM application. Pain point 1: N+1 LLM calls for s in similar: resolution = await db.query Resolution .filter ... .first This is a classic N+1 on the ORM side. In the LLM world it gets worse — you might be calling the LLM in the loop: for s in similar: s.summary = await llm.summarize s.description 5 similar tickets = 5 serial LLM calls LLM calls are an order of magnitude more expensive than database queries. Serial N+1 directly amplifies cost and latency. And code without a batching abstraction always ends up like this , because nobody manually maintains a batch queue inside procedural code. Real-world evidence: open-webui backend/open webui/utils/middleware.py:2635 commit 02dc3e6 , 2026-06 for sid in all skill ids: if sid in accessible skill ids: s = await SkillsModel.get skill by id sid serial N+1 The same file has at least three more instances folder lookup, tool connection, access check , all await -inside-a- for . open-webui is a production-grade AI application, and it still falls into this trap — evidence that the trap is structural, not a coding-quality issue. Pain point 2: Cross-subtree aggregation has no home all tags = for t in recent tickets: all tags.extend t.tags This "walk the subtree and collect things" logic, in procedural code, can only be written as global variables plus a for loop. As soon as aggregation needs grow — all similar-ticket resolutions, all products mentioned, all features touched — you get a pile of all xxx = lists scattered across the function, held together by convention. What makes this worse is that these aggregations are inherently "parent depends on children" . In procedural code, they are separated from child-fetch logic. Fetching is above the for loop; aggregation is below. The parent→child dependency has been reduced to "line number ordering". Real-world evidence: open-webui backend/open webui/utils/middleware.py chat-completion orchestration commit 02dc3e6 , 2026-06 sources = sources.extend flags.get 'sources', line 2882 sources.extend flags.get 'sources', line 2892 sources = s for s in sources if ... line 2909: mid-function reassignment events.append {'sources': sources} line 2916: another accumulator sources and events have no structured parent-child dependency declaration — they're stitched across handlers with extend . This is exactly the "aggregation has no home" pattern from the previous section — not a one-off defect, but the inevitable shape of procedural code that has to coordinate context across multiple sources. Pain point 3: Prompt shape is welded to data fetching return f""" Customer: {customer.name} id={customer.id} ... Summary: {summary} """ This final f-string welds three things together: data fetching, derived computation, prompt format . Touching the prompt template means touching the data code; touching data fetching means touching the prompt text; adding a field means editing from top to bottom. This is the limit of procedural code: it has no structure, so every change is invasive . Real-world evidence: open-webui backend/open webui/utils/middleware.py:931 get source context commit 02dc3e6 , 2026-06 php def get source context sources, ... - str: context string = '' for source in sources: for doc, meta in zip source.get 'document', , source.get 'metadata', : context string += f'