Skip to content

Repository files navigation

FastAI

🇨🇳 中文版文档 | English

A lightweight, extensible AI parameter extraction framework for Spring Boot. Transform natural language into structured parameters using LLMs — just define a POJO and let FastAI do the rest.

"我想找上海的科技公司,注册资本500万以上"
        ↓  FastAI
{ "city": "上海", "industry": "科技", "minRegisteredCapital": 500 }

Features

  • 🚀 Multi-LLM Support — Any OpenAI-compatible provider (Deepseek, Dashscope/Qwen, Moonshot, Zhipu, Doubao, etc.)
  • 🔄 Multi-Provider Fallback — Automatic failover chain across providers; random load balancing via quick-models
  • 🎯 Two-Stage Extraction — Stage 1 field detection (~200 tokens) → Stage 2 precise extraction, minimizing token cost
  • 🎬 Scene-based Architecture — Isolated extraction contexts (prompts, fields, converters) per business scenario
  • 📊 Label Conversion — Convert natural language labels to business codes via dictionary or custom converters (normal / feature / dynamic dict)
  • Parallel Split — Auto-split large extraction tasks into parallel sub-requests when field count exceeds threshold
  • 🔌 Extensible Hooks — PreProcessor, PostProcessor, PromptHook, ErrorHandler — all plug-and-play
  • 🛡️ Circuit Breaker — Fault isolation with CLOSED → OPEN → HALF_OPEN state machine
  • 🔍 Debug API — Built-in debug DTOs for prompt inspection, stage skipping, and provider override
  • 📦 Zero-Config Starter — Spring Boot auto-configuration, works out of the box

Performance

Real-world benchmarking with 70+ extraction fields per request:

Model Latency Notes
qwen3.6-flash 1–2s Fastest, suitable for real-time scenarios
qwen3.7-max 2–3s Best accuracy/speed balance
deepseek-chat 2–4s Stable, good for complex extraction

Two-stage extraction + parallel split keeps token cost low even with large field counts. Default fields are extracted asynchronously in parallel with Stage 1 detection, minimizing total wall-clock time.

Quick Start

1. Add Dependency

<dependency>
    <groupId>com.fastai</groupId>
    <artifactId>fastai-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

2. Configure Provider

fastai:
  enabled: true

  providers:
    deepseek:
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com

  model-chain:
    - provider: deepseek
      model: deepseek-chat

3. Define Target POJO

@Data
public class SearchParams {
    private String city;                    // 城市
    private String industry;                // 行业
    private BigDecimal minRegisteredCapital; // 最小注册资本(万元)
    private Boolean hasLicense;             // 是否有营业执照
}

4. Extract

Simple mode (single scenario) — inject ParameterExtractor:

@Service
public class SearchService {
    @Autowired
    private ParameterExtractor extractor;

    public SearchParams search(String userInput) {
        return extractor.extract(userInput, SearchParams.class);
    }
}

Scene mode (multi-scenario) — inject SceneAwareExtractor:

@Service
public class EnterpriseSearchService {
    @Autowired
    private SceneAwareExtractor extractor;

    public EnterpriseSearchParams search(String userInput) {
        return extractor.extract("enterprise-search", userInput, EnterpriseSearchParams.class);
    }

    // With context (for dict resolution, user info, etc.)
    public EnterpriseSearchParams search(String userInput, Map<String, Object> context) {
        return extractor.extract("enterprise-search", userInput, EnterpriseSearchParams.class, context);
    }
}

Two Extraction Modes

Mode Interface Use Case
Non-scene ParameterExtractor Single extraction scenario, global prompts
Scene SceneAwareExtractor Multiple business scenarios with isolated prompts, fields, and converters

Two-Stage Extraction Flow

Both modes share the same core flow:

User Input
  ├─→ [Async] Default fields extraction ──────────────┐
  ├─→ Stage 1: Field detection (~200 token prompt)     │
  │     + Keyword matching (regex/code)                │
  │     → Remove default fields → stage2Fields         │
  └─→ Stage 2: Precise extraction (non-default fields) │
        If fields > threshold → parallel split          │
                                                      │
                            ←─ Wait all ──────────────┘
                  merge(defaultFields, stage2Result)
                    → ConverterRegistry → ObjectMapper → result POJO
  • Default fields: High-frequency fields extracted asynchronously, bypassing Stage 1 detection
  • Stage 1: Minimal prompt identifies which fields are relevant in the query
  • Stage 2: Precise extraction for detected fields only; auto-splits into parallel requests when field count exceeds threshold

Scene Configuration

Scenes isolate extraction rules per business context. Configure via YAML or Java @Bean:

YAML Configuration

fastai:
  scenes:
    enterprise-search:
      enabled: true
      description: Enterprise search
      system-prompt: |
        You are a professional information extraction assistant.
        Extract structured enterprise search parameters from user queries.
      user-prompt-template: |
        请从以下用户查询中提取企业搜索参数:
        用户查询: {{query}}
        需要提取的字段及其说明:
        {{field_descriptions}}
      default-fields:
        - city
        - industry
      field-descriptions:
        city: 企业所在城市
        industry: 所属行业
        companyType: 公司类型
        minRegisteredCapital: 最小注册资本(万元)
      keyword-fields:
        industry: 科技|金融|制造|服务
        companyType: 有限|股份|独资|合伙
      field-dict-mapping:
        industry: industry_type
        companyType: company_type

    product-search:
      enabled: true
      # ... same structure, different fields and prompts

Java Bean Configuration (for advanced customization)

@Configuration
public class EnterpriseSceneConfig {

    @Bean("enterpriseSearchScene")
    public SceneConfig createScene(EnterpriseDictService dictService) {
        SceneConfig config = new SceneConfig();
        config.setSceneName("enterprise-search");
        config.setDescription("Enterprise search scene");
        config.setSystemPrompt("You are a professional extraction assistant...");
        config.setUserPromptTemplate("请从以下用户查询中提取...{{query}}...{{field_descriptions}}");
        config.setDefaultFields(Arrays.asList("city", "industry", "companyType"));
        config.setFieldDescriptions(Map.of(
            "city", "企业所在城市",
            "industry", "所属行业",
            "companyType", "公司类型"
        ));

        // Dictionary converter with mappings
        List<DictFieldMapping> mappings = List.of(
            new DictFieldMapping("industry", "industry_type"),
            new DictFieldMapping("companyType", "company_type")
        );
        config.setLabelConverter(new DictLabelConverter(dictService, mappings));

        // Field resolver with keyword matching
        config.setFieldResolver(resolver);

        // Prompt template registry (for Stage 1 detection config)
        config.setPromptRegistry(registry);
        return config;
    }
}

Dictionary Converter

DictLabelConverter converts natural language labels to business codes via DictService. Three mapping modes are supported:

Normal Dict Mapping

Direct field-to-dict-type mapping. The simplest mode.

// Mapping: field "industry" → dict type "industry_type"
new DictFieldMapping("industry", "industry_type")

Feature Field Mapping

Resolves feature type from context (carrierType), then looks up the dict.

// Mapping: field "category" → feature field with default featureType=1001
DictFieldMapping.feature("category", "category_dict", 1001)

Dynamic Dict Mapping

Dict type is determined dynamically from context (carrierType).

// Mapping: field "type" → different dict types based on carrierType
DictFieldMapping.dynamic("type", Map.of(
    "1", "type_dict_a",     // carrierType=1 → use type_dict_a
    "2", "type_dict_b",     // carrierType=2 → use type_dict_b
    "default", "type_dict"  // fallback
))

DictService Interface

Implement DictService and register as @Bean to provide dictionary lookups:

@Bean
public DictService myDictService() {
    return new DictService() {
        @Override
        public Object getCodeByLabel(String dictType, String label) {
            // Exact match lookup
            return myDatabase.findCode(dictType, label);
        }

        @Override
        public Object fuzzyMatch(String dictType, String label) {
            // Fuzzy match (optional, called when exact match returns null)
            return myDatabase.fuzzyFindCode(dictType, label);
        }

        @Override
        public Object getCodeByFeatureType(Integer featureType, String label) {
            // Feature type lookup
            return myDatabase.findByFeature(featureType, label);
        }
        // ... other methods
    };
}

YAML Dict Configuration

fastai:
  extractor:
    dict:
      enabled: true
      fuzzy-match-enabled: true
      case-sensitive: false

Configuration Reference

fastai:
  enabled: true

  # ── LLM Provider Credentials ──
  providers:
    deepseek:
      api-key: ${DEEPSEEK_API_KEY}
      base-url: https://api.deepseek.com
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}
      base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
    moonshot:
      api-key: ${MOONSHOT_API_KEY}
      base-url: https://api.moonshot.cn/v1

  # ── Quick Models (random load balancing per request) ──
  # At request start, one is randomly selected as the default provider.
  # Falls back to model-chain if empty.
  quick-models:
    - provider: dashscope
      model: qwen-turbo
    - provider: dashscope
      model: qwen-plus

  # ── Fallback Chain (>= 2 entries enables auto-failover) ──
  # On failure, switches to the next provider in the chain.
  model-chain:
    - provider: dashscope
      model: qwen-plus
    - provider: deepseek
      model: deepseek-chat
    - provider: moonshot
      model: moonshot-v1-8k

  # ── Retry (cross-provider fallback) ──
  max-retries: 3          # Retries per provider before switching
  retry-delay: 100        # Delay between provider switches (ms)

  # ── Retry (per-provider, detailed) ──
  retry:
    max-attempts: 3
    delay-ms: 1000
    multiplier: 1.0
    max-delay-ms: 10000

  # ── Circuit Breaker ──
  circuit-breaker:
    enabled: true
    failure-rate-threshold: 50
    sliding-window-size: 10
    circuit-open-duration-ms: 60000
    permitted-calls-in-half-open-state: 3

  # ── Prompts (non-scene mode) ──
  prompt:
    system-prompt: "You are a professional information extraction assistant."
    user-prompt: "Extract parameters from: {{query}}"
    default-fields: [ city, industry ]

    # Stage 1 field descriptions (field → one-line description for minimal detection prompt)
    fields-prompt:
      city: 企业所在城市
      industry: 所属行业
      hasParking: 是否有停车位

    # Stage 2 parallel split
    parallel:
      enabled: true
      threshold: 10           # Split when field count exceeds this
      max-requests: 5         # Max parallel sub-requests
      fields-per-group: 10    # Fields per group
      timeout-ms: 10000       # Sub-request timeout (ms)

Debug API

FastAI provides built-in DTOs for debugging and inspecting extraction behavior:

AISearchDebugParams

Override prompts, skip stages, or force a specific provider:

@Data
public class AISearchDebugParams {
    private String userInput;
    private String useScene;                  // Scene name (optional)
    private String customSystemPrompt;        // Override system prompt
    private String customUserPrompt;          // Override user prompt
    private boolean skipPostProcess;          // Skip converters and type coercion
    private boolean skipStage1;               // Skip field detection, use all fields
    private String provider;                  // Force a specific provider
    private List<String> customFields;        // Override field list
    private Map<String, String> promptOverrides; // Per-field prompt overrides
    private Integer carrierType;              // Context for dict conversion
}

AISearchDebugResult

Full inspection of what happened during extraction:

@Data
public class AISearchDebugResult {
    private String actualSystemPrompt;        // System prompt sent to LLM
    private String actualUserPrompt;          // User prompt sent to LLM
    private String llmRawResponse;            // Raw LLM response
    private String parsedJson;                // Post-processed JSON
    private List<String> extractedFields;     // Fields actually extracted
    private String stage1Prompt;              // Stage 1 detection prompt
    private String usedProvider;              // Provider actually used
    private Long useTimeMills;                // Total duration (ms)
    private Map<String, Long> stageTimings;   // Per-stage timing breakdown
    private Map<String, Object> aiSearchParam;// Extracted parameters as map
    private String aiRawResult;               // Raw result before formatting
}

Extension Points

Extension Interface Register As Description
LLM Provider ModelProvider @Bean Add new AI model (auto-registered)
Pre-processing PreProcessor @Component Transform input before LLM call
Post-processing PostProcessor @Component Transform result after extraction
Prompt Hook PromptHook @Component Customize prompt at runtime
Error Handling ErrorHandler @Component Custom error/fallback logic
Label Conversion LabelConverter @Component Convert labels → business codes
Dictionary Service DictService @Bean Resolve dictionary codes for DictLabelConverter
LLM Call Logger AiCallLogger @Component Persist LLM call logs and user feedback (default: no-op)

Custom Label Converter

@Component
public class StatusConverter implements LabelConverter {
    private static final Map<String, Integer> MAPPINGS = Map.of(
        "available", 1, "sold", 2, "pending", 3
    );

    @Override
    public boolean supports(String fieldName) {
        return "status".equals(fieldName);
    }

    @Override
    public Object convert(String fieldName, String label, Map<String, Object> context) {
        return MAPPINGS.get(label.toLowerCase());
    }
}

Converters are chained — first non-null result wins.

Pre/Post Processor

@Component
public class SensitiveWordFilter implements PreProcessor {
    @Override
    public String process(String userInput, Map<String, Object> context) {
        return userInput.replace("敏感词", "***");
    }
}

@Component
public class DataMaskingProcessor implements PostProcessor {
    @Override
    public <T> T process(T result, Map<String, Object> context) {
        // Mask phone numbers, ID cards, etc.
        return result;
    }
}

LLM Call Logger

The AiCallLogger interface supports both call logging and user feedback:

public interface AiCallLogger {
    /** Log an LLM extraction call (called asynchronously after each extraction) */
    void log(AiCallLog log);

    /** Update feedback for a previous log entry */
    void updateFeedback(String userInput, String useScene,
                        String feedbackResult, String feedbackContent);

    /** Whether this logger is enabled (default: true) */
    default boolean isEnabled() { return true; }
}

Implementation example:

@Component
public class MyAiCallLogger implements AiCallLogger {
    @Override
    public void log(AiCallLog logEntry) {
        // Persist to database, file, or monitoring system
        System.out.println("LLM call: " + logEntry.getProvider() + "/" + logEntry.getModel()
            + ", tokens: " + logEntry.getTotalTokens()
            + ", duration: " + logEntry.getDurationMs() + "ms");
    }

    @Override
    public void updateFeedback(String userInput, String useScene,
                                String feedbackResult, String feedbackContent) {
        // Store user feedback (e.g., "like"/"dislike") linked to the call log
        System.out.println("Feedback: " + feedbackResult + " - " + feedbackContent);
    }
}

AiCallLog fields:

Field Description
userInput Original natural language query
useScene Scene name (null for non-scene extraction)
aiRawResult Raw LLM response JSON (before post-processing)
aiResult Processed result JSON (after converters, type coercion)
durationMs Call duration in milliseconds
modelProvider LLM provider actually used (may differ if fallback occurred)
modelVersion LLM model actually used
targetClassName Target POJO class name
feedbackResult User feedback result (e.g., "like", "dislike")
feedbackContent User feedback free-text comments
interfaceParams Interface call parameters (JSON serialized)

Module Structure

spring-fast-ai/                          # Parent POM (com.fastai:fastai)
├── fastai-core/                         # Core framework (no Spring Boot dependency)
│   ├── model/                           # LLM provider abstraction
│   │   ├── ModelClient                  # Chainable API: .prompt().system().user().call().entity()
│   │   ├── ModelProvider                # Provider interface
│   │   ├── ModelProviderRegistry        # Auto-registration & lookup
│   │   ├── ModelClientPool              # Multi-provider pool with random shuffling
│   │   ├── OpenAiCompatibleProvider     # Default impl for OpenAI-compatible APIs
│   │   └── LlmCallResult               # Call result (json + provider/model tracking)
│   ├── hook/                            # Extension hooks
│   │   ├── HookManager                  # Orchestrates all hooks
│   │   ├── PreProcessor / PostProcessor # Input/output transformation
│   │   ├── PromptHook                   # Runtime prompt customization
│   │   └── ErrorHandler                 # Error/fallback handling
│   └── circuit/                         # Circuit breaker (CLOSED → OPEN → HALF_OPEN)
│
├── fastai-extractor/                    # Parameter extraction logic
│   ├── ParameterExtractor               # Non-scene extraction interface
│   ├── ParameterExtractorImpl           # Two-stage extraction implementation
│   ├── scene/                           # Multi-scenario extraction
│   │   ├── SceneAwareExtractor          # Scene-aware extraction interface
│   │   ├── SceneAwareExtractorImpl      # Full flow with scene resolution
│   │   ├── SceneManager                # Scene registry
│   │   └── SceneConfig                 # Per-scene: prompts, fields, mappings
│   ├── converter/                       # Label → code conversion
│   │   ├── LabelConverter               # Converter interface
│   │   ├── ConverterRegistry            # Chain of converters
│   │   └── dict/                        # Dictionary-based conversion
│   │       ├── DictLabelConverter       # Dict lookup with fuzzy matching (3 modes)
│   │       ├── DictFieldMapping         # Field-to-dict mapping config
│   │       ├── DictService              # Dictionary service interface
│   │       └── DictProperties           # Config under `fastai.extractor.dict`
│   ├── resolver/FieldResolver           # POJO reflection + keyword matching
│   ├── template/PromptTemplateRegistry  # Prompt templates & field descriptions
│   ├── dto/                             # Debug API DTOs
│   │   ├── AISearchDebugParams          # Debug request parameters
│   │   └── AISearchDebugResult          # Debug response with timing breakdown
│   └── log/                             # LLM call logging
│       ├── AiCallLogger                 # Logger interface (user implements)
│       └── AiCallLog                    # Log entry DTO
│
├── fastai-starter/                      # Spring Boot auto-configuration
│   ├── SpringAiAutoConfiguration        # Wires all beans, auto-scans extensions
│   ├── SpringAiProperties               # Config under `fastai` prefix
│   └── LlmFallbackExecutor              # Multi-provider fallback chain
│
└── fastai-examples/
    ├── fastai-example-basic/            # Multi-scene demo (enterprise + product search)
    └── fastai-example-custom-converter/ # Custom LabelConverter demo

Tech Stack

Component Choice Notes
Framework Spring Boot 2.7.18 Wide compatibility
Java JDK 17 Records, text blocks, etc.
HTTP Client JDK HttpClient Built-in, zero extra dependency
JSON Jackson 2.15.3 Spring Boot default
Logging SLF4J 2.0.9 Standard facade
Optional Spring Cloud Context 3.1.8 @RefreshScope support for SceneManager

Build & Run

# Build all modules
mvn clean install -DskipTests

# Run tests
mvn test

# Run example
mvn spring-boot:run -pl fastai-examples/fastai-example-basic

License

Apache License 2.0

About

A lightweight, extensible AI parameter extraction framework for Spring Boot. Extracts structured parameters from natural language using LLMs.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages