распознавание: большие раздачи размечаются целиком, файлы вне плана видны

- модель адресует файл номером строки нашего списка вместо копии пути: ответ
  на 180 файлов вместо ~15k токенов стоит ~2.5k, усечение сотней снято,
  max_files и max_tokens ушли в [recognition], correction-ретрай больше не
  переприсылает список
- негодный элемент ответа отбрасывается поимённой причиной, обрыв генерации и
  отказ по размеру запроса названы своими причинами, покрытие плана блокирует
  авто только при непокрытом видеофайле
- раскладка показывает все файлы раздачи со строками «не в плане» и полным
  порядком сортировки; снимок списка файлов лёг рядом с планом (миграция 0012)
This commit is contained in:
av
2026-09-02 08:54:58 +03:00
parent 01ac0430a9
commit fc9a3b4066
26 changed files with 1978 additions and 201 deletions
+26 -4
View File
@@ -127,9 +127,17 @@ type Worker struct {
SourceMissingThreshold int `toml:"source_missing_threshold"`
}
// Recognition — пороги распознавания.
// Recognition — пороги и пределы распознавания.
type Recognition struct {
AutoConfidenceThreshold float64 `toml:"auto_confidence_threshold"`
// MaxFiles — предохранитель от аномальной раздачи: сколько файлов
// максимум печатать модели в промпте. Рабочим ограничением быть не должен
// (усечение уводит задачу в review отдельной причиной), поэтому значение
// держим на уровне промпта, который модель заведомо принимает.
MaxFiles int `toml:"max_files"`
// MaxTokens — предел длины ответа модели. Обрыв по нему распознаётся
// отдельно (finish_reason = length) и уводит в review без повтора запроса.
MaxTokens int `toml:"max_tokens"`
}
// Telegram — настройки бота (Ф5).
@@ -232,9 +240,17 @@ func Default() *Config {
CatchTimeout: Duration(10 * time.Minute),
SourceMissingThreshold: 3,
},
Recognition: Recognition{AutoConfidenceThreshold: 0.85},
HTTP: HTTP{Listen: ":8080"},
Log: Log{Level: "info", Format: "json"},
Recognition: Recognition{
AutoConfidenceThreshold: 0.85,
// 500 файлов — порядка 35k токенов промпта: столько модель принимает,
// а раздача крупнее уже требует разбираться руками. Согласие с
// предохранителями конструктора (recognize.defaultMaxFiles /
// defaultMaxTokens) сторожит TestDefaultsAgreeWithConfig.
MaxFiles: 500,
MaxTokens: 8000,
},
HTTP: HTTP{Listen: ":8080"},
Log: Log{Level: "info", Format: "json"},
}
}
@@ -307,6 +323,12 @@ func (c *Config) validate() error {
if t := c.Recognition.AutoConfidenceThreshold; t < 0 || t > 1 {
errs = append(errs, fmt.Errorf("recognition.auto_confidence_threshold %.3f is out of range [0, 1]", t))
}
if c.Recognition.MaxFiles < 1 {
errs = append(errs, fmt.Errorf("recognition.max_files %d must be >= 1", c.Recognition.MaxFiles))
}
if c.Recognition.MaxTokens < 1 {
errs = append(errs, fmt.Errorf("recognition.max_tokens %d must be >= 1", c.Recognition.MaxTokens))
}
if c.LLM.MaxRetries < 0 {
errs = append(errs, fmt.Errorf("llm.max_retries %d must be >= 0", c.LLM.MaxRetries))
}
+51
View File
@@ -131,3 +131,54 @@ func TestValidate_Errors(t *testing.T) {
})
}
}
// Пределы распознавания: дефолты применяются к конфигу, где секции нет вовсе,
// а нулевые/отрицательные значения отвергаются — молча дефолтить их нельзя,
// иначе выключенный предохранитель неотличим от настроенного.
func TestValidate_RecognitionLimits(t *testing.T) {
if d := Default().Recognition; d.MaxFiles != 500 || d.MaxTokens != 8000 {
t.Errorf("дефолты = %+v, want max_files 500 / max_tokens 8000", d)
}
dir := t.TempDir()
for _, p := range []string{"downloads", "movies", "series"} {
if err := os.MkdirAll(filepath.Join(dir, p), 0o755); err != nil {
t.Fatalf("mkdir: %v", err)
}
}
path := filepath.Join(dir, "config.toml")
body := "[qbittorrent]\nurl = \"http://qbit:8080\"\n" +
"[paths]\ndownloads = \"" + filepath.Join(dir, "downloads") + "\"\n" +
"movies = \"" + filepath.Join(dir, "movies") + "\"\n" +
"series = \"" + filepath.Join(dir, "series") + "\"\n"
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Recognition.MaxFiles != 500 || cfg.Recognition.MaxTokens != 8000 {
t.Errorf("конфиг без секции не получил дефолты: %+v", cfg.Recognition)
}
for _, tt := range []struct {
name string
mut func(*Config)
want string
}{
{"max_files 0", func(c *Config) { c.Recognition.MaxFiles = 0 }, "recognition.max_files"},
{"max_files negative", func(c *Config) { c.Recognition.MaxFiles = -1 }, "recognition.max_files"},
{"max_tokens 0", func(c *Config) { c.Recognition.MaxTokens = 0 }, "recognition.max_tokens"},
{"max_tokens negative", func(c *Config) { c.Recognition.MaxTokens = -10 }, "recognition.max_tokens"},
} {
t.Run(tt.name, func(t *testing.T) {
c := validCfg(t)
tt.mut(c)
err := c.validate()
if err == nil || !strings.Contains(err.Error(), tt.want) {
t.Errorf("err = %v, want contains %q", err, tt.want)
}
})
}
}