Deepanshu's Diary

Designing Safe, Testable AI Features as Data‑flow Pipelines

--codingworkflowengineering

Designing Safe, Testable AI Features as Data‑flow Pipelines

When I added a “summarize-and-cite” feature that needed to scrape user-submitted URLs, two forces pulled in opposite directions: deliver functionality quickly so users can iterate, and prevent unsafe or incorrect outputs from reaching tenants. That single trade-off shaped the workflow I now reach for: model the feature as a deterministic data‑flow pipeline, exercise each stage with fixtures and mock runtimes, and require explicit operator gates before any live scraping, DB migrations, or outbound sends. In this post I walk through that pipeline, the concrete tests and drift detectors I use, and how I applied the approach in ARIL and Universal Scraper.

Why I like this mental model

I design features as pipelines where data transforms predictably. This makes testing natural (inject fixtures at a stage) and lets me add safety checks before any live side-effect (sending messages, running wide crawls, migrating production DBs). The pipeline abstraction also keeps components replaceable: switch the embedder, vector DB, or model without rewriting the entire feature.

High-level architecture / data-flow example

Here's the concrete pipeline I iterate on when building the summarize-and-cite feature:

- Source: URL or uploaded doc - Extractor: page -> cleaned HTML/text (Universal Scraper) - Normalizer: remove boilerplate, date/author extraction - Chunker: split into context windows - Embedder: convert chunks to vectors - Index: pgvector / local vector store (ARIL) - Retriever: kNN + metadata filters (tenant scope) - Generator: model prompt with retrieved chunks -> answer + inline citations - Evaluator: confidence, citation overlap, automated metrics - Operator UI & audit logs: review drafts, consent gates

A compact diagram (text):

Source -> Extractor -> Normalizer -> Chunker -> Embedder -> Vector Store -> Retriever -> Generator -> Evaluator -> Operator UI

And a small architecture table to make responsibilities explicit:

| Component | Responsibility | |---|---| | Extractor | Safe, rate-limited scraping + fixture mode for tests | | Vector Store | Tenant-scoped retrieval (pgvector locally) | | Generator | Model calls with context windows and citation protocol | | Operator UI | Review/approve, audit trails, gated sends |

Building blocks I reuse

I try to reuse two patterns: mock-first runtimes and fixture-driven tests. For example, the voice agent work used a provider-agnostic mock-first runtime so the developer experience is identical whether a live provider will be used later or not. The Universal Scraper has a three-stage extraction pipeline plus fixture tests so I can run the whole feature locally with deterministic inputs before considering any live run.

What I built around this idea

I applied this workflow in several projects; here are two direct anchors from my portfolio and what is implemented vs still gated:

- ARIL (implemented): tenant-scoped knowledge bases, document ingestion with chunking and embeddings, pgvector retrieval, citation support, evaluation metrics, and protected routes are in the local monorepo. These features let me build retrieval-augmented summarization and test everything locally. Gate: a real production Postgres migration is an environment gate—migration to live infra is explicit and manual.

- Universal Scraper (implemented): a three-stage extraction pipeline with fixture tests and live-safe runs for Product Hunt / Finsmes / billing-lead scraping. Gate: scraping is still subject to robots.txt, rate limits, provider blocking, and selector drift, so live-wide runs are constrained and require explicit approvals.

I also borrow operator UI, consented memory, audit events, and encrypted key patterns from the Amazon Voice Agent project when I need human-in-the-loop approvals or secret handling.

Development rhythm (practical steps)

1. Spike with fixtures: write extractor fixtures (HTML -> expected text) and run the whole pipeline locally. 2. Add unit tests at chunker/embedder boundaries; assert vector distances for known pairs. 3. Create a mock model runtime and integration test the generator's prompt + retrieved chunks. 4. Add evaluation metrics and a small automated scorer to flag hallucinations or missing citations. 5. Wire an operator UI backed by audit logs and approval gates before any live send.

Where this breaks (failure modes and mitigations)

1) Selector drift in scraping - Symptom: extractor silently returns wrong text because site changed. - Mitigation: fixture tests for important sources, periodic synthetic checks, and a drift alarm that falls back to manual review.

2) Provider rate-limiting and blocking - Symptom: sudden HTTP 429/403 during live extraction runs. - Mitigation: exponential backoff, respectful rate limits, IP rotation only after legal review, and a review gate to avoid broad live crawls.

3) Vector/embedding quality decay - Symptom: retrieval returns irrelevant chunks after model or embedder changes. - Mitigation: unit tests with known question–document pairs, evaluation metrics in ARIL to track retrieval precision, and a rollback path to the previous embedder/version.

4) Privacy or tenant isolation leaks - Symptom: one tenant can retrieve another tenant’s data due to misrouted metadata filters. - Mitigation: tenant-scoped schemas and protected routes (implemented in ARIL), fail-closed access checks in the retriever, and integration tests that assert isolation.

5) Model hallucination / bad citations - Symptom: output confidently cites irrelevant chunks. - Mitigation: include citation alignment tests, conservative citation policies (only cite retrieved chunks verbatim), and an operator review step before publishing.

A practical checklist (5–8 checks before enabling a feature beyond local dev)

- ✅ Fixture coverage for extractor outputs and chunking - ✅ Unit tests for embedder + retriever (known pairs) - ✅ Mock model runtime + integration tests for end-to-end prompt behavior - ✅ Evaluation metrics monitoring (precision/recall, citation overlap) in place - ✅ Consent/approval gates configured in the Operator UI - ✅ Secrets stored encrypted; provider keys require approval for live use - ✅ Health probes and rate-limit backoffs for extractor - ✅ Explicit deployment/migration gates (e.g., production Postgres migration approval)

Where code and process meet reality

The discipline of pipeline thinking makes it cheaper to swap components and to reason about failure. In my work, this translated into concrete artifacts: ARIL’s tenant-scoped index and retrieval primitives let me iterate on RAG features locally and test citations; Universal Scraper’s three-stage pipeline lets me run reproducible fixture tests before any live scrape. But the last-mile—live scraping at scale or migrating production databases—remains intentionally gated.

Conclusion

If you treat features as transform pipelines with clear testable boundaries you get two wins: rapid, low-risk iteration locally, and a clear checklist for the human and operational gates that must exist before a live rollout. That tension—ship fast, gate hard—keeps me shipping useful features without accidentally breaking safety, privacy, or legal constraints.

References

[1] Model Context Protocol — Anthropic: https://www.anthropic.com/news/model-context-protocol

[2] Model Context Protocol introduction: https://modelcontextprotocol.io/introduction

[3] My portfolio (projects referenced in this post): https://github.com/deepanshuvermaa/my-portfolio

[4] Example project references (scrapers, infra, automation): https://github.com/deepanshuvermaa/trading-engine

Copied!
Back to all posts