Skip to content

Repository files navigation

RAG Patterns For PHP Backends

Purpose

Retrieval-augmented generation, or RAG, gives an AI system relevant domain context at request time. It retrieves likely useful records before an agent produces an answer.

RAG does not replace relational data. It makes domain information discoverable by meaning while SQL remains responsible for validation, writes, relationships, and current state.

The RAG Lifecycle

A reusable RAG backend has four stages: document construction, index synchronization, retrieval, and agent response.

sequenceDiagram
    participant Source as Source Database
    participant Document as RAG Document Builder
    participant Embedder as Embedding Model
    participant Index as Vector Store
    participant Tool as Retrieval Tool
    participant Agent
    participant Client

    Source->>Document: Domain record
    Document->>Embedder: Readable document content
    Embedder->>Index: Vector and metadata

    Client->>Agent: Natural-language question
    Agent->>Tool: Search when context is needed
    Tool->>Embedder: Query text
    Embedder->>Index: Query vector
    Index-->>Tool: Ranked document metadata
    Tool->>Source: Load current source records
    Source-->>Tool: Authoritative records
    Tool-->>Agent: Serialized results and scores
    Agent-->>Client: Generated answer and serialized results
Loading

Source Of Truth

The vector store is an index, not an authority. It stores vectors, source keys, and metadata needed to find candidate records.

After retrieval, the backend reloads current records from SQL using returned metadata IDs. This prevents an agent from returning stale vector payload after a source record changes or is deleted.

Documents And Metadata

A document builder translates one domain record into content and metadata.

Content is readable text for semantic matching. Metadata is scalar data for identity, routing, source-record reload, and potential future filtering.

$document = new Document('A vehicle is electric and has 150 hp.');
$document->addMetadata('source_id', 42);
$document->addMetadata('fuel', 'electric');
$document->addMetadata('hp', 150);

Do not add private information merely because it is available in the source model. Document content and metadata should contain only what retrieval requires.

Synchronizing The Index

Source writes and vector writes are separate operations. Use after-commit observers and queued jobs to synchronize vectors only after a source transaction succeeds.

Source transaction commits
  -> observer queues synchronization
  -> job builds document and embedding
  -> job upserts or deletes the vector

This makes the vector index eventually consistent. Transient embedding or vector-store failures can retry without rolling back the source transaction.

When a shared source record affects many documents, its change must fan out. A shared specification change may therefore queue updates for every dependent document.

Semantic Search

Semantic search is enough for a basic RAG agent. A query such as “quiet city car” can retrieve relevant records even when the stored text does not contain those words.

Current vehicle retrieval uses semantic search only. Semantic search can retrieve relevant records from a query such as “quiet city car,” but it cannot guarantee every result satisfies an exact requirement.

Exact metadata filters are a future optional capability, not current behavior. They could later provide deterministic constraints such as fuel = electric or hp >= 120 without changing embeddings when the required metadata is already present.

Tools And Agents

The retrieval tool owns query embedding, vector search, source-record reload, and safe result mapping. An agent should receive the tool, not direct database or vector store access.

The agent decides whether to search and explains the returned results. The application owns the response contract, so a frontend never needs to parse generated prose.

{
  "response": {
    "natural-lang": "I found two matching records.",
    "serialized": [
      { "record": { "type": "example" }, "score": 0.91 }
    ]
  }
}

natural-lang is generated by the agent. serialized comes from a resource mapper and is safe for machine consumers, including HTTP clients and frontends.

Vehicle Example

This project applies the pattern to vehicles.

Vehicle + VehicleDetails
  -> VehicleRagDocument
  -> Ollama embedding
  -> Qdrant vehicle-documents collection
  -> VehicleSearchTool
  -> VehicleAgent

VehicleSearchTool supports a required semantic query and an optional result limit. Exact vehicle filters are a future optional capability.

VehicleAgent uses local qwen3:8b for generation. Ollama nomic-embed-text is used separately for embeddings.

Workflows

Neuron workflows model multi-step operations as an event-driven graph. Each node performs one unit of work and emits an event that routes execution to another node or terminates a branch or the workflow. Workflow state retains data needed by later nodes, while a parallel event can run isolated branches concurrently and join their results before execution continues.

EmailQueryWorkflow applies these concepts to multilingual vehicle-query emails. It stores the complete VehicleAgent response in workflow state, translates only its natural-language text in Romanian and French branches, collects both results, and sends one email per translation. Serialized vehicle records remain unchanged and are included with each email.

flowchart TD
    Start([Workflow starts]) -->|workflow started| Query[Run query]
    Query -->|query obtained| Delegate[Delegate translations]
    Delegate -->|Romanian translation requested| Romanian[Translate to Romanian]
    Delegate -->|French translation requested| French[Translate to French]
    Romanian -->|Romanian translation completed| Join((Parallel join))
    French -->|French translation completed| Join
    Join -->|all translations completed| Collect[Collect translations]
    Collect -->|email sending requested| Send[Send translated emails]
    Send -->|workflow stopped| Stop([Workflow stops])
Loading

The translation nodes run concurrently through Neuron's asynchronous executor. The collector runs only after both branches complete and stores translations by language in the main workflow state.

Add A New Entity

To apply this pattern to a new entity:

  1. Create the source model, migration, factory, and source-record tests.
  2. Create a document builder with readable content and scalar metadata.
  3. Opt the model into document synchronization and declare its vector collection.
  4. Add observers for the model and dependent shared records.
  5. Create a safe result resource for agent and frontend output.
  6. Create a model-specific semantic retrieval tool; add exact filters only when that future capability is required.
  7. Create an agent with only that model's read-only retrieval tool.
  8. Test indexing, semantic retrieval, resource mapping, and agent output.

Each entity can use an independent collection. Generic synchronization, retrieval, and resource infrastructure remain shared.

Local Runtime

Qdrant runs through DDEV. Ollama runs on the macOS host and is available to the DDEV web container at http://host.docker.internal:11434/api.

ddev start
ddev php artisan queue:work
ddev php artisan test

Open Qdrant at http://localhost:6333/dashboard.

History And Decisions

See docs/history/2026-07-18-rag-foundation.md for the original session summary. Architecture decisions are recorded under docs/adr/ and detailed designs under docs/superpowers/.

About

An example of a concurrent multi-agent workflow implemented in PHP.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages