🇨🇳 中文版文档 | 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 }
- 🚀 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
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.
<dependency>
<groupId>com.fastai</groupId>
<artifactId>fastai-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>fastai:
enabled: true
providers:
deepseek:
api-key: ${DEEPSEEK_API_KEY}
base-url: https://api.deepseek.com
model-chain:
- provider: deepseek
model: deepseek-chat@Data
public class SearchParams {
private String city; // 城市
private String industry; // 行业
private BigDecimal minRegisteredCapital; // 最小注册资本(万元)
private Boolean hasLicense; // 是否有营业执照
}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);
}
}| Mode | Interface | Use Case |
|---|---|---|
| Non-scene | ParameterExtractor |
Single extraction scenario, global prompts |
| Scene | SceneAwareExtractor |
Multiple business scenarios with isolated prompts, fields, and converters |
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
Scenes isolate extraction rules per business context. Configure via YAML or Java @Bean:
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@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;
}
}DictLabelConverter converts natural language labels to business codes via DictService. Three mapping modes are supported:
Direct field-to-dict-type mapping. The simplest mode.
// Mapping: field "industry" → dict type "industry_type"
new DictFieldMapping("industry", "industry_type")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)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
))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
};
}fastai:
extractor:
dict:
enabled: true
fuzzy-match-enabled: true
case-sensitive: falsefastai:
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)FastAI provides built-in DTOs for debugging and inspecting extraction behavior:
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
}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 | 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) |
@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.
@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;
}
}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) |
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
| 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 all modules
mvn clean install -DskipTests
# Run tests
mvn test
# Run example
mvn spring-boot:run -pl fastai-examples/fastai-example-basicApache License 2.0