Key points
1. Dual-Language Architecture and Deployment Modes
RAGFlow supports three deployment modes controlled by API_PROXY_SCHEME (docker/.env:193): python (pure Python with Quart API + task_executor), go (pure Go with bin/ragflow_server running --api/--ingestor/--admin/--syncer), and hybrid (both coexisting, routed by Nginx per docker/entrypoint.sh:191-206). Go is an equivalent rewrite of the Python implementation, enabling gray rollout and rollback rather than a "Python calling Go backend" pattern.
The two runtimes collaborate through shared storage and message queues, not direct RPC. Python writes parse tasks to Redis Stream consumer groups (api/db/services/document_service.py::begin2parse → queue_tasks → REDIS_CONN.queue_product); the consumer (rag/svr/task_executor.py:89) reports progress via RedisDistributedLock. Go uses NATS as its task bus (conf/service_conf.yaml:69-72, ingestor.mq_type: 'nats').
Ports: 9380 (Web API), 9381 (Admin Python), 9382 (MCP), 9383 (Go-Admin), 9384 (Go-HTTP), 9390 (DeepDOC standalone FastAPI vision service).
2. Python API Layer
Built on Quart async framework (api/apps/__init__.py:61) with CORS, OpenAPI QuartSchema, custom JSON encoder, 600s timeout for slow LLMs (api/apps/__init__.py:73-74). Blueprints are auto-discovered by convention: register_page walks api/apps, restful_apis, sdk directories and dynamically loads *_app.py/*sdk/*.py files (api/apps/__init__.py:349-366), unified under /api/{API_VERSION}. Trade-off: routing table is implicit and less statically readable.
Request flow: Blueprint handler → api/apps/services/* (business orchestration) → api/db/services/* (ORM service) → api/db/db_models.py (Peewee models). ORM uses peewee (api/db/db_models.py:30-46) with custom JSONField/ListField/SerializedField (:71-123).
Multi-tenancy is a first-class citizen: nearly every business table carries tenant_id (User, Tenant, UserTenant with role, Knowledgebase, Document, Dialog, TenantLLM, UserCanvas, UserCanvasVersion). At runtime, g.user.tenant_id becomes the query context after authentication. Vector indexes are physically isolated per tenant via search.index_name(kb.tenant_id). Model quotas are also tenant-scoped (TenantLLM.used_tokens).
Authentication supports three modes: Beta token, JWT (using itsdangerous, api/apps/__init__.py:187), and API token (:213), loaded into g.user by _load_user (:144-229); protected by login_required (:235-280).
Configuration uses two layers: conf/service_conf.yaml (shared by both languages) plus env vars plus code defaults. Python uses common/settings.py:221 init_settings() + config_utils.py. Go uses internal/server/config.go:40-165 with viper (three-level merge: service_conf → RAGFLOW_* env vars → code defaults), split across 20+ files under internal/server/config/.
3. RAG Orchestration Core (rag/)
Sub-package responsibilities:
rag/app/: chunking by document type (e.g.,naive.py:236,qa.py:289 chunk())rag/flow/: document parse pipeline (DAG), reusingagent.canvas.Graph(pipeline.py:28 Pipeline(Graph))rag/graphrag/: knowledge-graph RAG (entity-relation-community three layers,search.py:139 KGSearch.retrieval)rag/llm/: LLM/CV/Embedding/Rerank abstraction and vendor adapters, registered at__init__.py:168rag/nlp/: retrieval/rerank/rewrite primitives (search.py:549 retrieval,:461 rerank)rag/advanced_rag/: Agentic-RAG, tree decomposition, RAPTOR, knowledge compilationrawDSL []byte(:43-48): normalized JSON fingerprint of canvas DSL forguardDSLChange—when templates change, stale checkpoints are invalidated rather than incorrectly resumed (prevents "running new flow on old graph")canvas *canvas.Canvas(:53): directly embeds Go Agent's canvas enginestore canvas.CheckPointStore(:54) +tracker *canvas.RunTracker(:55): breakpoint resume and progressrequireResume bool(:56-61) +ErrResumeUnavailable(:71): deployments without checkpoint storage (Redis down/unconfigured) explicitly error out, never silently degrading to non-resumable- Python
agent/:canvas.py(original Graph engine),component/,tools/,templates/,sandbox/,plugin/,dsl_migration.py - Go
internal/agent/:canvas/(heavy DAG engine:canvas.go/compile.go/scheduler.go/runner.go/checkpoint_store.go/interrupt_resume.go/run_tracker.go/loop_subgraph.go/parallel_subgraph.go/multibranch.go/stream.go/state.go—supports loops, parallel subgraphs, multi-branch, interrupt resume),component/,tool/,dsl/,harness/,runtime/,sandbox/,retrievalbridge/,chat/,audio/,workflowx/ - Dual-implementation maintenance debt: Python/Go mirror-style services require synchronizing any behavioral update in two places; contract drift risk is real (
internal/server/config.goandcommon/config_utils.pyeach write their own parser). - Index count scales linearly with tenants: tenant-level physical index isolation means index count explodes with tenant count, raising operational concerns.
- Implicit routing: blueprint convention-based auto-registration sacrifices readability for onboarding convenience.
internal/cppno longer exists in this version: C++ bindings may have been merged intointernal/deepdoc's CGO (slightly inconsistent with AGENTS.md), illustrating that documentation lags behind code—an open-source norm.- Deployment switches:
docker/.env:193,docker/entrypoint.sh:191-206,285-302,318-355 - Go entry:
cmd/ragflow_server.go:89-102,440-509,542-631,567,633-680,682-706 - API:
api/apps/__init__.py:61,144-229,232,235-280,349-366,369-381,api/ragflow_server.py:56-74,104,147-161 - ORM/tenancy:
api/db/db_models.py:30-46,679,722,748,805,837,894,1020,1101,1133,1384,api/db/services/document_service.py::begin2parse - Config:
common/settings.py:66,139,221,261,internal/server/config.go:40-165,conf/service_conf.yaml:3-55,69-72 - RAG pipeline:
api/db/services/dialog_service.py:636,764,776,789-795,805,831,852,960-979,962,982,rag/nlp/search.py:461,434,494,524,549,843,906 - LLM:
rag/llm/__init__.py:168-195,rag/llm/chat_model.py:75,104,226,247,275,319,789,806,1065,1768,1769,rag/llm/llm_service.py:37,65,api/db/services/tenant_llm_service.py:193-211 - GraphRAG:
rag/graphrag/search.py:139,161,171-218,220-221,261-275,277,rag/graphrag/utils.py:414,472,550,869 - flow:
rag/flow/pipeline.py:22,28,44,110,123,146,161,169,rag/flow/base.py:33,rag/flow/__init__.py:35-54 - Parsing:
deepdoc/parser/__init__.py:17-41,deepdoc/vision/layout_recognizer.py:33,34-46,48,52-59,62,68,175,deepdoc/vision/table_structure_recognizer.py:32,deepdoc/parser/pdf_parser.py:56,1895,1902 - Chunking:
rag/app/naive.py:434,internal/parser/chunk/chunk_type.go:20-29,32-37,46-55,internal/parser/chunk/execute.go:42,internal/parser/parser/docx_parser.go:1,26,29-33 - Ingestion/Agent:
internal/ingestion/pipeline/pipeline.go:28,36,40-64,71,internal/ingestion/component/*,internal/agent/canvas/*,internal/engine/engine.go:33-34,47,60,63,65
Retrieval → Rerank → Generation pipeline (api/db/services/dialog_service.py::async_chat):
Question → model assembly (get_models :636) → Retrievaler.retrieval (:764, rag/nlp/search.py:549) → rerank (:776, three options: local hybrid :461 / ES KNN :434 / model rerank :494) → kb_prompt assembly (:805,:831) → chat_mdl.async_chat* streaming (:962) → decorate_answer citations (:852) → yield
Three rerank approaches trade off: local hybrid (term+vector, zero cost) vs ES-side KNN fusion (saves compute) vs model rerank (highest quality, scores normalized to [0,1] via rerank_mdl.similarity).
LLM abstraction uses a registry factory pattern: base class chat_model.py:226 Base(ABC) wraps OpenAI/AsyncOpenAI with retry _classify_error (:247) and streaming _async_chat_streamly (:275); LiteLLMBase (:1768) unifies 40+ vendors. Registration happens at import time (rag/llm/__init__.py:168-195) by scanning modules for subclasses with _FACTORY_NAME, populating ChatModel/EmbeddingModel/RerankModel/... dictionaries. Factory invocation: tenant_llm_service.py:193-211 instantiates by model_config["llm_factory"]; LLMBundle(LLM4Tenant) (llm_service.py:37) provides unified wrapping plus Langfuse usage reporting via _report_usage (:65).
GraphRAG is an orthogonal enhancement channel: graph retrieval produces "pseudo chunks" (search.py:261-275, similarity:1.0) merged into kbinfos["chunks"] (dialog_service.py:789-795), sharing prompt assembly and citation logic with main vector retrieval. Indexing: utils.py:550 set_graph() incremental, :869 rebuild_graph() full; three extraction strategies (general/ with LLM+Leiden community, light/, ner/).
rag/flow/pipeline.py:28 class Pipeline(Graph) uses nodes as components inheriting base.py:33 ProcessBase; auto-collected via pkgutil.walk_packages (__init__.py:35-54); run() (:123) advances topologically from File via get_downstream(), concurrent with asyncio (:161), progress saved to Redis (:44), cancellable (:110). The key relationship: agent/canvas.Graph is a general "node+edge" DAG engine; rag/flow uses it for offline parsing, while agent/ uses it for online agents—one graph engine, two uses.
rag/advanced_rag upgrades from "one retrieval → one generation" to closed loops with multi-source fusion, recursive decomposition, sufficiency self-check, tree/graph indexing (e.g., tree_structured_query_decomposition_retrieval.py:94 _research recursive decomposition, :121 sufficiency_check gating, :133-135 gather concurrent depth reduction; knowlege_compile/raptor.py:38 recursive abstraction).
4. Document Parsing and Chunking — The Core Differentiator
DeepDOC: Python side deepdoc/parser/__init__.py:17-41 registers nine parsers (PdfParser/DocxParser/EpubParser/ExcelParser/PptParser/HtmlParser/JsonParser/MarkdownParser/TxtParser, plus PlainParser).
Layout recognition (deepdoc/vision/layout_recognizer.py): LayoutRecognizer (:33) labels 11 categories—Text/Title/Figure/Figure caption/Table/Table caption/Header/Footer/Reference/Equation (:34-46); implemented with YOLOv10 base (LayoutRecognizer4YOLOv10, :175). Models can be remote (DEEPDOC_URL/TENSORRT_DLA_SVR, :52-59) or local (rag/res/deepdoc, :62-65 auto snapshot_download). __call__ (:68) IoU-matches OCR text blocks with layout boxes, annotates layout_type, filters footer/header/reference "garbage layouts" (:49,97), and preserves coordinates. table_structure_recognizer.py:32 handles table structure restoration.
Template-based chunking: rag/app/naive.py:434 PARSERS = {...} is the parser backend hub: deepdoc/mineru/docling/opendataloader/tcadp/paddleocr/somark/mistral ocr/plaintext, with plaintext as default. Each document "template" corresponds to one chunking strategy (naive/general/paper/law/book/resume/table/qa/picture/email), distributed across rag/app/*.py (e.g., paper.py:176 parse_method="paper", qa.py:273 beAdoc extracts Q-A pairs).
Go-side operator-based chunking (internal/parser/chunk): abstracts chunking as an Operator interface pipeline (chunk_type.go:20-29: Prepare/Execute/Finish); ChunkContext (:46-55) flows through Origin → TextAfterPreprocess → SplitChunks → ResultChunks. execute.go:42 Run() assembles three stages per options: preprocess → split → postprocess (split operator consumes SplitStrategy, postprocess does merge/filter).
Native bindings (CGO): performance-critical paths use CGO to native libraries (internal/parser/parser/docx_parser.go:1 //go:build cgo, :26 office_oxide, :29-33 noting DOCXParser is the only entry directly linked to office_oxide; IR and postprocessing split into docx_ir.go/docx_postprocess.go so compilation without native library remains possible via !cgo stubs).
Chunk metadata: chunks carry layout coordinates; deepdoc/parser/pdf_parser.py:1895/1902 writes positions (page number + four-corner coordinates), enabling visualization of original source locations after retrieval.
RAGFlow's differentiation is not "connecting to LLM" but "reading PDF as a real PDF"—layout + table + coordinates give structure and provenance to chunks. This is the root of its "context engine" positioning.
5. Ingestion Pipeline and Agent Framework
Ingestion pipeline (internal/ingestion) — compiled canvas:
internal/ingestion/pipeline/pipeline.go:40-64 Pipeline structure is a key design:
Underlying orchestration uses github.com/cloudwego/eino/compose (:36). Components in internal/ingestion/component/: file.go (read docs), parser.go/parser_dispatch.go (dispatch by type), chunker/, extractor.go, pdf_vision_dispatch*.go/docx_vision_dispatch.go (vision-enhanced dispatch). compilation/extractor and service/ingestion_service.go/progress_sink.go chain the full pipeline with progress reporting.
The canvas adapter essence: the canvas drawn on the Web is the DSL of Go canvas.Canvas; the ingestion pipeline does not rewrite the execution engine but "compiles canvas + injects task metadata + mounts checkpoint." Adapter heavy-lifting is in DSL translation and component registration (_ "ragflow/internal/agent/component", pipeline.go:29 triggers component self-registration).
Agent framework (dual-language):
Dual-language relationship: Python agent is the historical/online canvas implementation; Go internal/agent/canvas is the rewritten high-performance orchestration kernel, directly reused by the ingestion pipeline. Online Agent and offline ingestion share one graph semantics.
Retrievable backends (internal/engine) — pluggable everything:
internal/engine/engine.go:33-34 defines EngineType (elasticsearch/infinity); interface includes Search (:47), SearchMetadata (:60), IndexDocument (:63), BulkIndex (:65). Directory = backend list: clickhouse/ elasticsearch/ infinity/ nats/ oceanbase/ redis/ serenedb/.
6. Storage and Retrieval Backend Division
| Component | Role | Evidence |
|---|---|---|
| MySQL / PostgreSQL | Metadata (tenants/users/KBs/documents/dialogs/canvases) | api/db/db_models.py, Go internal/dao |
| Redis | Cache, sessions, Python task queue (Stream), distributed progress lock | task_executor.py, common/settings.py:139 queue names |
| MinIO | Object storage (raw docs, images) | conf/service_conf.yaml |
| DocEngine (ES/Infinity/OpenSearch) | Vector + keyword index, retrieval | internal/engine |
| NATS | Go-side task bus (ingestor) | service_conf.yaml:69-72 |
| ClickHouse | Analytics (optional) | internal/engine/clickhouse |
7. Design Philosophy Distilled
1. Dual implementation + shared storage enables gray rollout and rollback (hybrid coexistence). Cost: same business logic maintained twice; cross-language data contracts must align precisely. This is pragmatic evolutionary rewriting, not architectural purism.
2. The context engine's root is "understanding the document": layout recognition (YOLOv10) + table structure + coordinate metadata give chunks structure and provenance—the moat that differentiates RAGFlow from naive page-splitting.
3. Three orchestration layers: document type → template → PARSERS; chunking → Go Operator pipeline; flow → canvas DAG. Everything follows "pluggable, reusable" principles.
4. One DAG engine, two uses: internal/agent/canvas drives both online Agent and offline ingestion (pipeline.go:53 embeds canvas.Canvas), eliminating the maintenance of two execution kernels.
5. Pluggable everything: LLM (registry factory, rag/llm/__init__.py:168), parsers (PARSERS), retrievable backends (engine.go), message queues (Redis/NATS)—reducing vendor lock-in while expanding integration surface.
6. Tenant as first-class citizen + physical index isolation: tenant_id propagates end-to-end and lands in the index name (index_name(tenant_id)), naturally fitting multi-tenant SaaS.