跪拜 Guibai
← Back to the summary

A Production-Ready RAG Knowledge Base in ~500 Lines of Spring Boot and LangChain4j

Spring Boot + LangChain4j Implementing RAG — Building an Enterprise Knowledge Base Q&A System from Scratch

1. Project Structure Overview

rag-system/
├── pom.xml                          # Parent POM, dependency management
├── llm-common/                      # Common module
│   ├── pom.xml
│   └── src/main/java/com/peanutai/llm/common/
│       ├── exception/
│       │   ├── BusinessException.java
│       │   └── ErrorCode.java
│       └── response/
│           └── Result.java
│
├── llm-service/                     # Core business module
│   ├── pom.xml
│   └── src/main/java/com/peanutai/llm/service/
│       ├── config/
│       │   └── LangChain4jConfig.java      # ChatModel/EmbeddingModel config
│       ├── controller/
│       │   ├── ChatController.java         # Chat endpoint
│       │   ├── DocumentController.java     # Document upload
│       │   ├── KnowledgeBaseController.java # Knowledge base management
│       │   └── RagController.java          # RAG Q&A
│       ├── model/
│       │   ├── dto/                        # Request/Response DTOs
│       │   └── enums/                      # Enums
│       ├── protection/
│       │   └── ContentSafetyService.java  # Content safety
│       ├── rag/
│       │   ├── DocumentProcessor.java     # Document parsing + chunking
│       │   └── PromptTemplate.java        # RAG Prompt template
│       └── service/
│           ├── ChatService.java           # @AiService declarative interface
│           ├── EmbeddingService.java      # Vectorization service
│           ├── RagService.java            # RAG interface
│           ├── RagServiceImpl.java        # RAG core implementation
│           └── VectorStoreService.java    # Vector storage + retrieval
│
└── llm-gateway/                     # Gateway module
    ├── pom.xml
    └── src/main/java/com/peanutai/llm/gateway/
        └── GatewayApplication.java

2. Step 1: Environment Setup (5 minutes)

2.1 Parent POM Dependency Management

<properties>
    <java.version>17</java.version>
    <spring-boot.version>3.2.0</spring-boot.version>
    <langchain4j.version>0.35.0</langchain4j.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-bom</artifactId>
            <version>${langchain4j.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Key point: Use langchain4j-bom to uniformly manage all LangChain4j sub-module versions and avoid dependency conflicts.

2.2 llm-service Module POM

<dependencies>
    <!-- Spring Boot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- LangChain4j Core -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j</artifactId>
    </dependency>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-open-ai</artifactId>
    </dependency>
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-spring-boot-starter</artifactId>
    </dependency>

    <!-- Vector Database -->
    <dependency>
        <groupId>dev.langchain4j</groupId>
        <artifactId>langchain4j-milvus</artifactId>
    </dependency>

    <!-- Document Parsing -->
    <dependency>
        <groupId>org.apache.tika</groupId>
        <artifactId>tika-core</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tika</groupId>
        <artifactId>tika-parsers-standard-package</artifactId>
        <version>2.9.2</version>
    </dependency>

    <!-- Monitoring -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
</dependencies>

2.3 Configuration File

spring:
  application:
    name: llm-service

llm:
  openai:
    api-key: ${DASHSCOPE_API_KEY}
    base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
    model: qwen-plus
  embedding:
    model: text-embedding-v3
    dimensions: 1024

milvus:
  host: ${MILVUS_HOST:localhost}
  port: ${MILVUS_PORT:19530}
  database: default

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus

3. Step 2: Configure LangChain4j (ChatModel + EmbeddingModel)

@Configuration
public class LangChain4jConfig {

    @Value("${llm.openai.api-key}")
    private String apiKey;

    @Value("${llm.openai.model:qwen-plus}")
    private String modelName;

    @Value("${llm.openai.base-url}")
    private String baseUrl;

    @Value("${llm.embedding.model:text-embedding-v3}")
    private String embeddingModelName;

    @Value("${llm.embedding.dimensions:1024}")
    private Integer embeddingDimensions;

    /**
     * Synchronous chat model
     */
    @Bean
    public ChatModel chatModel() {
        return OpenAiChatModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .baseUrl(baseUrl)
                .timeout(Duration.ofSeconds(30))
                .temperature(0.7)
                .maxTokens(2000)
                .logRequests(true)
                .logResponses(true)
                .build();
    }

    /**
     * Streaming chat model
     */
    @Bean
    public StreamingChatModel streamingChatModel() {
        return OpenAiStreamingChatModel.builder()
                .apiKey(apiKey)
                .modelName(modelName)
                .baseUrl(baseUrl)
                .timeout(Duration.ofSeconds(60))
                .temperature(0.7)
                .maxTokens(2000)
                .build();
    }

    /**
     * Embedding model
     */
    @Bean
    public EmbeddingModel embeddingModel() {
        return OpenAiEmbeddingModel.builder()
                .apiKey(apiKey)
                .modelName(embeddingModelName)
                .baseUrl(baseUrl)
                .dimensions(embeddingDimensions)
                .timeout(Duration.ofSeconds(30))
                .build();
    }
}

4. Step 3: Document Parsing and Chunking (DocumentProcessor)

This is the first step of RAG, determining the quality of subsequent retrieval.

@Slf4j
@Component
public class DocumentProcessor {

    private static final int CHUNK_SIZE = 1000;    // 1000 characters per chunk
    private static final int CHUNK_OVERLAP = 200;  // 200 characters overlap
    private final Tika tika = new Tika();

    /**
     * Parse document, extract plain text
     * Supports: PDF, Word, Excel, PPT, HTML, Markdown and 30+ formats
     */
    public String parse(MultipartFile file) throws IOException {
        try (InputStream is = file.getInputStream()) {
            String content = tika.parseToString(is);
            if (content == null || content.isBlank()) {
                throw new IOException("Document content is empty, cannot parse: " + file.getOriginalFilename());
            }
            return content.strip();
        } catch (TikaException e) {
            throw new IOException("Document parsing failed: " + file.getOriginalFilename(), e);
        }
    }

    /**
     * Text chunking: fixed length + sliding window overlap
     * 200-character overlap ensures semantics are not cut off
     */
    public List<TextSegment> chunk(String content) {
        List<TextSegment> chunks = new ArrayList<>();
        int length = content.length();

        for (int start = 0; start < length; start += (CHUNK_SIZE - CHUNK_OVERLAP)) {
            int end = Math.min(start + CHUNK_SIZE, length);
            String chunk = content.substring(start, end).strip();
            if (!chunk.isBlank()) {
                chunks.add(TextSegment.from(chunk));
            }
        }
        log.info("Document chunking completed: {} chunks total", chunks.size());
        return chunks;
    }
}

Chunking parameter tuning experience:

Scenario chunkSize overlap Description
General docs 1000 200 Suitable for most scenarios
Q&A FAQ 500 100 Short text, precision first
Technical docs 1500 300 Code blocks need to be complete
Legal contracts 2000 400 Clauses need full context

5. Step 4: Vectorization Service (EmbeddingService)

@Slf4j
@Service
@RequiredArgsConstructor
public class EmbeddingService {

    private final EmbeddingModel embeddingModel;
    private static final int BATCH_SIZE = 10;

    /**
     * Single vectorization (for user questions)
     */
    public Embedding embed(String text) {
        return embeddingModel.embed(text).content();
    }

    /**
     * Batch vectorization (for document ingestion)
     * DashScope supports max 10 per request, requires batching
     */
    public List<Embedding> embedAll(List<String> texts) {
        List<TextSegment> segments = texts.stream()
                .map(TextSegment::from)
                .collect(Collectors.toList());

        List<Embedding> allEmbeddings = new ArrayList<>();
        for (int i = 0; i < segments.size(); i += BATCH_SIZE) {
            List<TextSegment> batch = segments.subList(
                i, Math.min(i + BATCH_SIZE, segments.size()));
            List<Embedding> batchResult = embeddingModel.embedAll(batch).content();
            allEmbeddings.addAll(batchResult);
        }
        return allEmbeddings;
    }
}

6. Step 5: Vector Storage and Retrieval (VectorStoreService)

This is the most critical part of RAG, including multi-knowledge base isolation and similarity retrieval.

@Slf4j
@Service
public class VectorStoreService {

    private static final String KB_ID_FIELD = "knowledgeBaseId";
    private final EmbeddingStore<TextSegment> embeddingStore;

    public VectorStoreService(EmbeddingStore<TextSegment> embeddingStore) {
        this.embeddingStore = embeddingStore;
    }

    /**
     * Batch write vectors
     * Write knowledgeBaseId metadata for each document chunk to achieve multi-knowledge base isolation
     */
    public void addAll(String knowledgeBaseId, 
                       List<TextSegment> chunks, 
                       List<Embedding> embeddings) {
        if (chunks.size() != embeddings.size()) {
            throw new IllegalArgumentException("chunks and embeddings count mismatch");
        }
        for (int i = 0; i < chunks.size(); i++) {
            TextSegment segment = chunks.get(i);
            segment.metadata().put(KB_ID_FIELD, knowledgeBaseId);
            embeddingStore.add(embeddings.get(i), segment);
        }
        log.info("Vector storage completed: kbId={}, count={}", knowledgeBaseId, chunks.size());
    }

    /**
     * Vector similarity retrieval
     * Filter by knowledgeBaseId to ensure only the specified knowledge base is searched
     * minScore=0.6 filters low-similarity results
     */
    public List<DocumentMatch> search(String knowledgeBaseId, 
                                       Embedding queryEmbedding, 
                                       int maxResults) {
        Filter kbFilter = new IsEqualTo(KB_ID_FIELD, knowledgeBaseId);

        return embeddingStore.search(
                EmbeddingSearchRequest.builder()
                        .queryEmbedding(queryEmbedding)
                        .maxResults(maxResults)
                        .minScore(0.6)
                        .filter(kbFilter)
                        .build())
                .matches().stream()
                .map(m -> DocumentMatch.builder()
                        .content(m.embedded().text())
                        .score(m.score())
                        .build())
                .collect(Collectors.toList());
    }

    /**
     * Delete by knowledge base
     */
    public void deleteCollection(String knowledgeBaseId) {
        Filter kbFilter = new IsEqualTo(KB_ID_FIELD, knowledgeBaseId);
        embeddingStore.removeAll(kbFilter);
        log.info("Vector collection deleted by knowledge base: kbId={}", knowledgeBaseId);
    }
}

Multi-knowledge base isolation principle:

Milvus Vector Database
│
├── knowledgeBaseId: "kb-hr"
│   ├── chunk1: "Annual leave policy: after 1 year of employment..."
│   ├── chunk2: "Leave application process: OA system..."
│   └── chunk3: "Attendance system: working hours..."
│
├── knowledgeBaseId: "kb-tech"
│   ├── chunk1: "Spring Boot configuration..."
│   ├── chunk2: "Database connection pool..."
│   └── chunk3: "RESTful API design..."
│
└── knowledgeBaseId: "kb-product"
    ├── chunk1: "Product feature description..."
    └── chunk2: "Pricing strategy..."

During query, filter with new IsEqualTo("knowledgeBaseId", "kb-hr") to return only HR-related content.


7. Step 6: Prompt Template (PromptTemplate)

@Component
public class PromptTemplate {

    private static final String RAG_SYSTEM_PROMPT = """
            You are an intelligent knowledge base assistant. Please answer user questions based on the following reference materials.
            
            Rules:
            1. Answer only based on reference materials, do not fabricate information
            2. If reference materials are insufficient to answer the question, clearly state so
            3. Answers should be concise, accurate, and well-organized
            4. Use Chinese to answer
            
            Reference materials:
            %s
            """;

    public String build(String question, String context) {
        return String.format(RAG_SYSTEM_PROMPT, context) + "\n\nUser question: " + question;
    }
}

8. Step 7: RAG Core Service (RagServiceImpl)

This is the core of the entire system, connecting all steps.

@Slf4j
@Service
public class RagServiceImpl implements RagService {

    private final DocumentProcessor documentProcessor;
    private final EmbeddingService embeddingService;
    private final VectorStoreService vectorStoreService;
    private final ChatModel chatModel;
    private final StreamingChatModel streamingChatModel;
    private final PromptTemplate promptTemplate;
    private final ContentSafetyService contentSafetyService;

    public RagServiceImpl(DocumentProcessor documentProcessor,
                          EmbeddingService embeddingService,
                          VectorStoreService vectorStoreService,
                          ChatModel chatModel,
                          StreamingChatModel streamingChatModel,
                          PromptTemplate promptTemplate,
                          ContentSafetyService contentSafetyService) {
        this.documentProcessor = documentProcessor;
        this.embeddingService = embeddingService;
        this.vectorStoreService = vectorStoreService;
        this.chatModel = chatModel;
        this.streamingChatModel = streamingChatModel;
        this.promptTemplate = promptTemplate;
        this.contentSafetyService = contentSafetyService;
    }

    // ==================== Document Ingestion ====================

    @Override
    public void ingestDocument(MultipartFile file, String knowledgeBaseId) {
        log.info("Starting document ingestion: file={}, kbId={}", 
                 file.getOriginalFilename(), knowledgeBaseId);

        try {
            // ① Parse document
            String content = documentProcessor.parse(file);
            // ② Text chunking
            List<TextSegment> chunks = documentProcessor.chunk(content);
            // ③ Batch vectorization
            List<Embedding> embeddings = embeddingService.embedAll(
                    chunks.stream().map(TextSegment::text).collect(Collectors.toList()));
            // ④ Write to vector store
            vectorStoreService.addAll(knowledgeBaseId, chunks, embeddings);

            log.info("Document ingestion completed: file={}", file.getOriginalFilename());
        } catch (IOException e) {
            log.error("Document parsing failed", e);
            throw new BusinessException(ErrorCode.DOCUMENT_PARSE_ERROR);
        }
    }

    // ==================== RAG Query ====================

    @Override
    public RagResponse query(String question, String knowledgeBaseId) {
        long startTime = System.currentTimeMillis();

        // [1] Input safety validation
        contentSafetyService.validateInput(question);

        // [2] Question vectorization
        Embedding questionEmbedding = embeddingService.embed(question);

        // [3] Vector retrieval Top-5
        List<DocumentMatch> matches = vectorStoreService.search(
                knowledgeBaseId, questionEmbedding, 5);

        // [4] Concatenate context
        String context = matches.stream()
                .map(DocumentMatch::getContent)
                .collect(Collectors.joining("\n\n"));

        // [5] Build Prompt
        String prompt = promptTemplate.build(question, context);

        // [6] Call LLM
        ChatRequest chatRequest = ChatRequest.builder()
                .messages(UserMessage.from(prompt))
                .build();
        ChatResponse response = chatModel.chat(chatRequest);

        // [7] Output audit
        String answer = response.aiMessage().text();
        contentSafetyService.auditOutput(answer);

        // [8] Build response
        long latency = System.currentTimeMillis() - startTime;
        TokenUsage tokenUsage = response.tokenUsage();

        return RagResponse.builder()
                .answer(answer)
                .sources(convertToSources(matches))
                .tokenUsage(com.peanutai.llm.service.model.dto.TokenUsage.builder()
                        .inputTokens(tokenUsage.inputTokenCount())
                        .outputTokens(tokenUsage.outputTokenCount())
                        .totalTokens(tokenUsage.totalTokenCount())
                        .build())
                .latencyMs(latency)
                .model("qwen-plus")
                .build();
    }

    // ==================== Streaming RAG Query ====================

    @Override
    public SseEmitter queryStream(String question, String knowledgeBaseId) {
        SseEmitter emitter = new SseEmitter(30000L);

        CompletableFuture.runAsync(() -> {
            try {
                // Same RAG flow, but using streaming model
                contentSafetyService.validateInput(question);
                Embedding questionEmbedding = embeddingService.embed(question);
                List<DocumentMatch> matches = vectorStoreService.search(
                        knowledgeBaseId, questionEmbedding, 5);

                String context = matches.stream()
                        .map(DocumentMatch::getContent)
                        .collect(Collectors.joining("\n\n"));

                String prompt = promptTemplate.build(question, context);

                streamingChatModel.chat(prompt, new StreamingChatResponseHandler() {
                    @Override
                    public void onPartialResponse(String partialResponse) {
                        try {
                            emitter.send(SseEmitter.event()
                                    .name("message")
                                    .data(partialResponse));
                        } catch (IOException e) {
                            log.error("SSE send failed", e);
                        }
                    }

                    @Override
                    public void onCompleteResponse(ChatResponse completeResponse) {
                        try {
                            // Send source documents
                            emitter.send(SseEmitter.event()
                                    .name("sources")
                                    .data(convertToSources(matches)));
                            emitter.complete();
                        } catch (IOException e) {
                            log.error("SSE completion send failed", e);
                        }
                    }

                    @Override
                    public void onError(Throwable error) {
                        log.error("Streaming query failed", error);
                        emitter.completeWithError(error);
                    }
                });
            } catch (Exception e) {
                log.error("Streaming query exception", e);
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}

9. Step 8: Controller Layer

9.1 Document Upload

@RestController
@RequestMapping("/api/v1/documents")
@RequiredArgsConstructor
public class DocumentController {

    private final RagService ragService;

    @PostMapping("/upload")
    public Result<Void> upload(
            @RequestParam("file") MultipartFile file,
            @RequestParam("knowledgeBaseId") String knowledgeBaseId) {
        ragService.ingestDocument(file, knowledgeBaseId);
        return Result.success();
    }

    @PostMapping("/batch-upload")
    public Result<Void> batchUpload(
            @RequestParam("files") List<MultipartFile> files,
            @RequestParam("knowledgeBaseId") String knowledgeBaseId) {
        ragService.ingestDocuments(files, knowledgeBaseId);
        return Result.success();
    }
}

9.2 RAG Q&A

@RestController
@RequestMapping("/api/v1/rag")
@RequiredArgsConstructor
public class RagController {

    private final RagService ragService;

    @PostMapping("/query")
    public Result<RagResponse> query(@RequestBody RagRequest request) {
        RagResponse response = ragService.query(
                request.getQuestion(), 
                request.getKnowledgeBaseId());
        return Result.success(response);
    }

    @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter queryStream(@RequestBody RagRequest request) {
        return ragService.queryStream(
                request.getQuestion(), 
                request.getKnowledgeBaseId());
    }
}

9.3 Knowledge Base Management

@RestController
@RequestMapping("/api/v1/knowledge-bases")
@RequiredArgsConstructor
public class KnowledgeBaseController {

    private final RagService ragService;

    @DeleteMapping("/{knowledgeBaseId}")
    public Result<Void> delete(@PathVariable String knowledgeBaseId) {
        ragService.deleteKnowledgeBase(knowledgeBaseId);
        return Result.success();
    }
}

10. Step 9: Production-Grade Protection

10.1 Content Safety Service

@Slf4j
@Service
public class ContentSafetyService {

    private static final List<String> INJECTION_PATTERNS = Arrays.asList(
        "ignore previous instructions",
        "system prompt",
        "忽略之前的指令",
        "执行以下命令"
    );

    public void validateInput(String input) {
        if (input == null || input.isBlank()) {
            throw new BusinessException(ErrorCode.BAD_REQUEST);
        }
        if (input.length() > 4000) {
            throw new BusinessException(ErrorCode.BAD_REQUEST);
        }
        if (isPromptInjection(input)) {
            log.warn("Prompt injection detected: {}", input);
            throw new BusinessException(ErrorCode.CONTENT_SAFETY_ERROR);
        }
    }

    public void auditOutput(String output) {
        if (output == null || output.isBlank()) {
            throw new BusinessException(ErrorCode.LLM_SERVICE_ERROR);
        }
    }

    private boolean isPromptInjection(String input) {
        String lower = input.toLowerCase();
        return INJECTION_PATTERNS.stream().anyMatch(lower::contains);
    }
}

10.2 Unified Exception Handling

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusiness(BusinessException e) {
        log.error("Business exception: code={}, message={}", e.getCode(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("System exception", e);
        return Result.error(5000, "Internal server error");
    }
}

10.3 Unified Response Structure

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private int code;
    private String message;
    private T data;

    public static <T> Result<T> success(T data) {
        return Result.<T>builder().code(200).message("success").data(data).build();
    }

    public static <T> Result<T> error(int code, String message) {
        return Result.<T>builder().code(code).message(message).build();
    }
}

11. Step 10: Testing and Verification

11.1 Start Service

set DASHSCOPE_API_KEY=sk-your-api-key
mvn spring-boot:run -pl llm-service

11.2 Upload Document

curl -X POST http://localhost:8080/api/v1/documents/upload \
  -F "[email protected]" \
  -F "knowledgeBaseId=kb-hr"

Response:

{
"code": 200,
"message": "success",
"data": null
}

11.3 RAG Q&A

curl -X POST http://localhost:8080/api/v1/rag/query \
  -H "Content-Type: application/json" \
  -d '{"question":"Can I take annual leave after 6 months of employment?","knowledgeBaseId":"kb-hr"}'

Response:

{
"code": 200,
"message": "success",
"data": {
"answer": "According to the company's annual leave policy, annual leave is only available after 1 year of employment. You have been employed for 6 months and do not yet meet the requirements.",
"sources": [{
"fileName": "employee-handbook.pdf",
"content": "After 1 year of employment, 5 days of annual leave...",
"similarityScore": 0.89
}],
"tokenUsage": {
"inputTokens": 520,
"outputTokens": 45,
"totalTokens": 565
},
"latencyMs": 850,
"model": "qwen-plus"
}
}

11.4 Streaming Q&A

curl -N -X POST http://localhost:8080/api/v1/rag/stream \
  -H "Content-Type: application/json" \
  -d '{"question":"How to apply for annual leave?","knowledgeBaseId":"kb-hr"}'

Response (streaming output):

event:message
data:According to the company

event:message
data:Employee Handbook

event:message
data:Section 3.2

...

event:sources
data:[{"fileName":"employee-handbook.pdf",...}]

12. RAG Performance Tuning

12.1 Parameter Tuning Guide

Parameter Default Tuning Direction Effect
chunkSize 1000 Decrease → precision ↑, Increase → context ↑ 500-2000
chunkOverlap 200 Increase → semantic coherence ↑, token ↑ 100-400
maxResults(Top-K) 5 Increase → coverage ↑, noise ↑ 3-10
minScore 0.6 Increase → precision ↑, recall ↓ 0.5-0.8
temperature 0.7 Decrease → determinism ↑, creativity ↓ 0.3-1.0

12.2 Common Issues and Solutions

Issue Cause Solution
Cannot retrieve relevant docs minScore too high Lower to 0.5
Retrieving irrelevant docs Top-K too large Reduce to 3, increase minScore
Inaccurate answers Context too short Increase chunkSize
Answers taken out of context Chunking cuts semantics Increase overlap
High latency Retrieval + generation slow Add cache, reduce Top-K
High token consumption Context too long Reduce chunkSize, reduce Top-K

13. Summary

13.1 Complete Pipeline Review

Document Upload → Tika Parsing → Text Chunking (1000+200) → Batch Vectorization (1024 dims)
    → Write to Milvus (with knowledgeBaseId metadata) → Ingestion Complete

User Question → Input Safety Validation → Question Vectorization → Vector Retrieval (Top-5, minScore≥0.6)
    → Context Concatenation → Prompt Construction → LLM Generation → Output Audit → Return Answer

13.2 Key Metrics

Metric Value
Lines of Code ~500 (core logic)
Document Format Support 30+ (Tika)
Vector Dimensions 1024 (Tongyi Qianwen v3)
Chunking Strategy 1000 chars + 200 overlap
Retrieval Top-K 5
Similarity Threshold 0.6
Average Latency 800-1200ms
Average Tokens 500-600/request

13.3 Next Steps

This RAG system is ready for production use. The next article will introduce advanced RAG optimizations: query rewriting, re-ranking, and caching strategies to further improve retrieval effectiveness.