addrkit: strict ordinal matching, LLM-variant guard, externalized prompt

- match: require candidate ordinal to equal the query ordinal in fuzzy search
  (deterministic typo recall 73% -> 80%, no wrong houses)
- resolver: compatibleVariant guard — LLM may only fix street spelling, keeping
  the same words, ordinal and house unit; damerau distance for transpositions
- llm: prompt moved out of code (go:embed default_prompt.txt) with
  llm.system_prompt / llm.system_prompt_file overrides; guided JSON parsing
  tolerant of thinking blocks
- address: recognize ordinals appearing after the street name
- tests: first coverage for the LLM adapter and config prompt loading; README

Measured (sample 200): deterministic 94.7-95.8% exact / 0 wrong across 4 seeds;
LLM+guard typo 90.7-89.8% / 0 wrong. LLM fallback latency ~1.2-1.9s.
This commit is contained in:
Maksim Totmin
2026-10-09 21:40:46 +07:00
parent ba4705fc4f
commit f1537f1d6b
17 changed files with 590 additions and 48 deletions
+22 -1
View File
@@ -46,6 +46,18 @@ type LLMConfig struct {
Token string `json:"token"`
Model string `json:"model"`
TimeoutSeconds int `json:"timeout_seconds"`
// MaxTokens bounds the rewriter's reply. Thinking models need enough room
// for the final JSON even when thinking is disabled.
MaxTokens int `json:"max_tokens"`
// DisableThinking turns off chain-of-thought for models that support it
// (Qwen3 via chat_template_kwargs). A rewriter only needs the JSON answer.
DisableThinking bool `json:"disable_thinking"`
// SystemPrompt overrides the built-in rewriter prompt inline. When empty,
// the prompt embedded in the binary is used.
SystemPrompt string `json:"system_prompt"`
// SystemPromptFile loads the rewriter prompt from a file. When set it takes
// precedence over SystemPrompt, which keeps long prompts out of the config.
SystemPromptFile string `json:"system_prompt_file"`
}
// Default returns a config with sane defaults for the non-required fields.
@@ -63,7 +75,9 @@ func Default() Config {
Retries: 1,
},
LLM: LLMConfig{
TimeoutSeconds: 3,
TimeoutSeconds: 3,
MaxTokens: 512,
DisableThinking: true,
},
}
}
@@ -79,6 +93,13 @@ func Load(path string) (Config, error) {
if err := dec.Decode(&cfg); err != nil {
return Config{}, fmt.Errorf("parse config: %w", err)
}
if cfg.LLM.SystemPromptFile != "" {
data, err := os.ReadFile(cfg.LLM.SystemPromptFile)
if err != nil {
return Config{}, fmt.Errorf("read llm.system_prompt_file: %w", err)
}
cfg.LLM.SystemPrompt = string(data)
}
if err := cfg.validate(); err != nil {
return Config{}, err
}