Measure package and prompt redundancy in retained trials

This commit is contained in:
Nico committed 2026-10-16 22:08:08 +02:00
1 parent fa1a0ab86b
commit 500ae27beb
3 files changed
+7279

No files matched your search

File diff suppressed because it is too large. Load diff
+160
View File
@@ -0,0 +1,160 @@
# Redundanzbilanz vom 16.10.2026
## Umfang und Methode
Vier erhaltene, durch PackageReader validierte Case-Pakete untersucht: Bug 20 g108
(alter investigation_v1-Stand) und Bug 17 g124 (v9), g110 (v10), g148 (v11).
Keines ist ein abgeschlossenes OUTPUT_READY-Paket. Der neueste Stand dient als
Hauptbeispiel; ältere Versionen sind keine gleichwertigen Qualitätsbenchmarks.
Keine LLM-Aufrufe, kein Umbau oder Löschen von Paketinhalt.
Reproduzierbares Werkzeug: `scripts/redundancy_audit.py`, Aufruf mit einem oder
mehreren Paketpfaden und `--output <bericht.json>`.
Aggregierte Messdaten ohne Quelltext-/Promptinhalte:
`measurements/redundancy-audit-20261016.json`.
Messstand: Code nach fa1a0ab, historische Pakete und Prompts unverändert.
Alle MB/KB/GB dezimal, alle Textmessungen UTF-8-Bytes, keine Tokenzählung.
## 1. Paketgröße und fehlende Kompression
| Paket | Dateien | ZIP auf Platte | DEFLATE-6-Payload, simuliert |
|---|---:|---:|---:|
| Bug 20 g108 | 517 | 63,22 MB | 20,17 MB |
| Bug 17 g124 | 576 | 34,61 MB | 9,97 MB |
| Bug 17 g110 | 547 | 33,61 MB | 9,79 MB |
| Bug 17 g148 | 658 | 36,56 MB | 10,32 MB |
**Alle ZIP-Einträge verwenden ZIP_STORED, also keine ZIP-Kompression.** Die
Simulation komprimiert jeden Eintrag separat mit raw DEFLATE, Stufe 6, und prüft
für jeden Eintrag die byteidentische Dekompression. ZIP-Header/Verzeichnis kommen
zur simulierten Payload hinzu. Beim jüngsten Paket ergeben sich damit ungefähr
10,53 MB statt 36,56 MB, rund 71 % weniger Speicher. Dies ist keine Änderung am
Format oder am Inhalt und reduziert keine LLM-Tokens. Laufzeitkosten und die
Writer-/Sicherheitsprüfungen wären bei einer Implementierung zu testen.
## 2. Zusammensetzung des neuesten Pakets
| Inhalt Bug 17 g148 | Bytes in MB | Anteil an 36,34 MB Dateiinhalten |
|---|---:|---:|
| Entpackte Originaldateien | 18,576 | 51,1 % |
| Ursprüngliche TAR.GZ-Archive | 5,224 | 14,4 % |
| Investigation-Kontexte | 7,686 | 21,1 % |
| Weitere Analyseartefakte | 2,104 | 5,8 % |
| Gespeicherte LLM-Requests | 1,896 | 5,2 % |
| Aufbereiteter Case | 0,406 | 1,1 % |
| Sonstiges: Historie, Forschung, LLM-Ergebnisse, Manifest | 0,450 | 1,2 % |
Die ZIP-Verzeichnis-/Headerdaten ergänzen weitere 0,214 MB.
Die größte einzelne Datei ist das originale wowsrc `source/hits.json` mit
5,015 MB. Weitere große Originaldateien sind Code-/SQL-Referenzsammlungen.
Ihre fachliche Relevanz wurde hier nicht beurteilt.
### Exakte Duplikate versus doppelte Repräsentationen
- Byteidentische vollständige Dateien: nur **0,185 MB** zusätzliche Kopien im
neuesten Paket. Reines Deduplizieren ganzer Dateien löst das Hauptproblem nicht.
- **225 entpackte Originaldateien mit 18,576 MB** sind zusätzlich byteidentisch
innerhalb der beiden archivierten TAR.GZ-Dateien vorhanden. Das ist eine
doppelte Repräsentation derselben Originaldaten. Die TAR.GZ-Dateien selbst
belegen 5,224 MB; diese Werte dürfen nicht als zwei unabhängige Einsparungen
addiert werden. Eine neue Referenz-/Lesestrategie wäre nötig, um eine Kopie
wegzulassen. Bestehende Originaldateien dürfen gemäß Architektur nicht gelöscht
werden. Bei neuen Paketen wäre eine bewusste Import-/Formatentscheidung nötig.
- **5,097 MB kanonisch identische Feldwerte** werden allein durch wiederholte
Speicherung von `case`, `case_sources`, `tool_schema` und `interpretation` in
den 17 Investigation-Kontexten zusätzlich abgelegt. Das ist eine Untergrenze
für Kontextredundanz: wachsende Evidenzlisten enthalten weitere wiederholte
Unterobjekte. Neu entworfene Kontexte könnten auf unveränderliche Artefakte
innerhalb desselben Pakets verweisen, statt diese jedes Mal einzubetten.
- Kontext-Snapshot und daraus erzeugter Request speichern teilweise dieselben
Inhalte in verschiedenen Darstellungen. Diese Überlappung ist nachweisbar,
aber hier nicht als zusätzliche unabhängige Einsparsumme quantifiziert.
## 3. Aktuelle Investigation-Prompts
17 gespeicherte v11-Requests ergeben **1,401 MB Nachrichteninhalt** (System und
User, ohne äußere JSON-Transporthülle). Die einzelnen Aufrufe wachsen von
54,1 KB auf 105,0 KB. Bei der bestehenden Cache-Partition wächst die separat
gelieferte Live-Darstellung von 6,2 KB auf 63,2 KB; diese Transportmessung enthält
eine andere JSON-Hülle und ist deshalb kein direkter Summand der 1,401 MB.
### Wiederholung zwischen Aufrufen
| Komponente | Insgesamt | Unterschiedliche vollständige Werte | Wiederholt |
|---|---:|---:|---:|
| Aufbereitete Quellen `sources` | 338,2 KB | 19,9 KB | 318,3 KB |
| Output-Schema | 150,0 KB | 8,8 KB | 141,2 KB |
| Interpretation | 63,8 KB | 3,8 KB | 60,0 KB |
| Evidenzobjekte, einzeln verglichen | 218,8 KB | 29,5 KB | 189,3 KB |
| Quellcodezeilen in `shared_sources` | 92,7 KB | 8,0 KB | 84,7 KB |
Evidenzobjekte werden einzeln nach kanonischem JSON verglichen. Quellcodezeilen
werden nach Quellmetadaten, Zeilennummer und Inhalt verglichen, nicht allein nach
Zeilentext. 3.352 Zeilenvorkommen entsprechen 280 unterschiedlichen belegten
Zeilen. Alle Werte enthalten weiterhin die notwendigen unterschiedlichen Daten;
ein Übertragungsprotokoll benötigt zusätzlich IDs/Referenzen und Cache-Verwaltung.
**Die statischen Wiederholungen sind zum großen Teil bereits durch den stabilen
Fallcache abgedeckt.** Im zugehörigen vorangegangenen Live-Test meldeten alle
17 Investigation-Antworten 19.987 gecachte Eingabetokens. Die gespeicherten
Requests enthalten trotzdem die vollständige kanonische Anfrage für die
Nachvollziehbarkeit. Ihre Größe darf nicht mit erneut ungecacht übertragenen
oder voll berechneten Tokens gleichgesetzt werden.
Die Evidenz und Codezeilen liegen dagegen in der veränderlichen Live-Darstellung.
Ein weiterer Ansatz ist ein versionierter Recherche-Cache beziehungsweise eine
exakt rekonstruierbare Aufteilung in bereits bekannte und neu hinzugekommene
Belege. Nicht einfach Belege weglassen: Der frühere Versuch mit selektiver
Historie führte zu Nachladeproblemen und wurde ausdrücklich zurückgenommen.
### Wiederholung innerhalb eines Aufrufs und JSON-Struktur
Exakt identische längere Stringwerte (ab 80 UTF-8-Bytes) wiederholen sich in den
17 v11-User-Dokumenten zusammen um **10,5 KB**, entsprechend **0,75 %** des
gesamten System-/User-Nachrichteninhalts. Der Wert ist kein vollständiges Maß
aller Redundanz: kurze Codezeilen, ähnliche Texte und überlappende Strukturen
werden nicht als identische lange Strings erkannt. Eine Ersetzung bräuchte
Referenzen und würde weniger als diese Bruttosumme sparen.
Zum Vergleich: Die alte Bug-20-Investigation v1 enthält 1,310 MB solcher
wiederholter Strings bei 4,123 MB Nachrichteninhalt, rund 31,8 %. Dieser konkrete
Mechanismus wurde in der aktuellen Darstellung also bereits stark reduziert.
Die Fälle/Anzahl der Schritte sind verschieden; daraus folgt kein kontrollierter
Qualitäts- oder Kostenvergleich.
Allein die JSON-Schlüsselnamen samt Anführungszeichen und Doppelpunkt belegen
bei v11 insgesamt **415,4 KB**, rund 29,7 % des Nachrichteninhalts. Das ist kein
versprochenes Einsparpotenzial von 29,7 %: Schlüssel sind für Bedeutung und
Schema nötig; Kürzungen benötigen ein verständliches gemeinsames Wörterbuch.
Der Wert zeigt aber, dass noch erhebliche Strukturkosten existieren. Beispiele
sind wiederkehrende Fakt-/Quellprovenienz und Evidenz-Records. Innerhalb aller
übermittelten Evidenzobjekte belegen `record` 72,9 KB und `record_ref` 13,7 KB,
während `content` 127,4 KB belegt (zzgl. Schlüssel/Struktur).
## 4. Konsequenzen und Reihenfolge
1. **ZIP-Kompression und Aufbewahrung vollständiger Generationen:** großer,
direkt messbarer Speichergewinn ohne Informationsverlust. Die 20-GB-Regel
gilt unabhängig davon weiter. Keine Auswirkung auf API-Tokenkosten.
2. **Gespeicherte Kontexte referenzieren unveränderliche Paketartefakte:**
Fallbasis/Schemas einmal, je Schritt nur Änderungen und Referenzen. Eine
Rekonstruktion muss exakt sein und auch nach Transport/Neustart funktionieren.
Keine Abhängigkeit von einem externen Dienstgedächtnis.
3. **Live-Recherchekontext effizienter übermitteln:** die 189,3 KB wiederholten
Evidenzobjekte und 84,7 KB wiederholten Quellzeilen sind konkrete Ansatzpunkte.
Diese Zahlen und die Strukturkosten überlappen teilweise; nicht addieren.
Zuerst verlustfreie Struktur-/Cache-Ansätze prüfen, erst danach inhaltliche
Auswahl mit nachgewiesener Qualität. Nicht den gesamten bisherigen Befund
erneut durch eine zusätzliche LLM zusammenfassen lassen.
4. **Provenienz für das Modell reduzieren:** doppelt mitgesendete interne
Speicherpfade, ausführliche Records und technische Verwaltungsfelder anhand
ihrer tatsächlich nötigen Funktion prüfen. Die vollständige Provenienz bleibt
in der Akte; Beleg-ID, Quelle, Abfrageumfang und Versionsbezug müssen für die
LLM verständlich bleiben. Das ist eine Designentscheidung, keine bereits
validierte verlustfreie Ersetzung.
Es gibt aktuell keine belastbare Grundlage, eine Einsparung in Euro für diese
Schritte zu versprechen. Die vier vorhandenen Pakete erklären die Speicher- und
Textredundanz; sie zeigen nicht, welche fachlichen Informationen gefahrlos
entfallen dürfen. Ein nachfolgender Qualitätstest muss diese Frage getrennt
prüfen. An produktivem Verhalten wurde für diese Bilanz nichts geändert.
+271
View File
@@ -0,0 +1,271 @@
"""Read-only offline audit; reports aggregate sizes/hashes, never source contents.
All byte counts use UTF-8 or ZIP directory sizes. Potential savings overlap and
must not be added together. This is a measurement, not a package migration.
"""
import argparse
from collections import Counter, defaultdict
import hashlib
import io
import json
from pathlib import Path
import tarfile
import zipfile
import zlib
from viano_bug_analysis.package import PackageReader
from viano_bug_analysis.llm.gemini import GeminiProvider
def encoded(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(',', ':')).encode('utf-8')
def digest(data):
return hashlib.sha256(data).hexdigest()
def category(name):
if name.startswith('original/'):
return 'original_archives' if name.endswith('/archive.tar.gz') else 'original_members'
if name.startswith('llm/'):
return 'llm_requests' if name.endswith('/request.json') else 'llm_other'
if '/contexts/' in name:
return 'analysis_contexts'
return name.split('/')[0]
def repeated(groups):
result = []
for entries in groups.values():
if len(entries) > 1:
result.append({'copies': len(entries),
'raw_duplicate_bytes': sum(e[1] for e in entries[1:]),
'compressed_duplicate_bytes': sum(e[2] for e in entries[1:]),
'paths': [e[0] for e in entries]})
return sorted(result, key=lambda v: v['raw_duplicate_bytes'], reverse=True)
def audit(path):
# Validate through the normal bounded reader before inspecting ZIP metadata.
with PackageReader(path) as reader:
manifest = reader.manifest
files = set(reader.files)
groups = defaultdict(list)
canonical = defaultdict(list)
categories = defaultdict(Counter)
requests = []
archive_members = Counter()
metadata_fields = Counter()
deflate_estimate = Counter()
compression_methods = Counter()
evidence_items = {}
source_lines = {}
evidence_sizes = Counter()
context_components = defaultdict(lambda: [0, 0, 0])
context_values = set()
with zipfile.ZipFile(path) as z:
infos = {v.filename: v for v in z.infolist() if not v.is_dir()}
for name, info in infos.items():
data = z.read(name)
compressor = zlib.compressobj(level=6, wbits=-15)
compressed = compressor.compress(data) + compressor.flush()
assert zlib.decompress(compressed, wbits=-15) == data
deflate_size = len(compressed)
deflate_estimate[category(name)] += deflate_size
compression_methods[str(info.compress_type)] += 1
cat = categories[category(name)]
cat.update(files=1, raw_bytes=len(data), compressed_bytes=info.compress_size)
groups[digest(data)].append((name, len(data), info.compress_size))
if name.endswith('/archive.tar.gz'):
prefix = name.removesuffix('archive.tar.gz') + 'members/'
with tarfile.open(fileobj=io.BytesIO(data), mode='r|gz') as tar:
for member in tar:
if not member.isfile():
continue
target = prefix + member.name
if target in files and member.size == infos[target].file_size:
with tar.extractfile(member) as stream:
same = digest(stream.read(member.size + 1)) == digest(z.read(target))
if same:
archive_members.update(files=1, raw_bytes=member.size,
outer_zip_member_compressed_bytes=infos[target].compress_size)
if not name.endswith('.json'):
continue
try:
doc = json.loads(data)
except (ValueError, UnicodeError):
continue
canonical[digest(encoded(doc))].append((name, len(data), info.compress_size))
if category(name) == 'analysis_contexts' and isinstance(doc, dict):
for field, value in doc.items():
b = encoded(value)
key = (field, digest(b))
context_components[field][0] += len(b)
context_components[field][2] += 1
if key not in context_values:
context_components[field][1] += len(b)
context_values.add(key)
if name.startswith('llm/operations/') and name.endswith('/request.json'):
requests.append((name, doc))
by_prompt = defaultdict(list)
system_groups = Counter()
block_groups = defaultdict(lambda: [0, 0, 0])
per_call = []
def leaves(value, at='$'):
if isinstance(value, dict):
for k, v in value.items():
if isinstance(v, (dict, list)):
yield from leaves(v, at + '.' + k)
elif isinstance(v, str) and len(v.encode('utf-8')) >= 80:
yield at + '.' + k, v
elif isinstance(value, list):
for v in value:
yield from leaves(v, at + '[]')
for name, req in requests:
messages = req.get('messages', [])
msg_bytes = sum(len(m['content'].encode('utf-8')) for m in messages)
systems = [m['content'] for m in messages if m['role'] == 'system']
for value in systems:
system_groups[(digest(value.encode()), len(value.encode()))] += 1
user_docs = []
strings = defaultdict(list)
keys = Counter()
def count_keys(value):
if isinstance(value, dict):
for k, v in value.items():
keys[k] += len(encoded(k)) + 1
count_keys(v)
elif isinstance(value, list):
for v in value:
count_keys(v)
for m in messages:
if m['role'] != 'user':
continue
try:
doc = json.loads(m['content'])
except ValueError:
continue
user_docs.append(doc)
view = doc.get('input', {}) if isinstance(doc, dict) else {}
if isinstance(view, dict):
for item in view.get('evidence', {}).values() if isinstance(view.get('evidence'), dict) else []:
evidence_items[digest(encoded(item))] = (evidence_items.get(digest(encoded(item)), (0, len(encoded(item))))[0] + 1, len(encoded(item)))
if isinstance(item, dict):
for field, value in item.items():
evidence_sizes[field] += len(encoded(value))
shared = view.get('shared_sources', {})
for source in shared.values() if isinstance(shared, dict) else []:
if not isinstance(source, dict):
continue
meta = source.get('metadata', {})
identity = digest(encoded(meta))
for block in source.get('blocks', []):
for offset, line in enumerate(block.get('lines', [])):
key = (identity, block['start_line'] + offset, digest(encoded(line)))
count, size = source_lines.get(key, (0, len(encoded(line))))
source_lines[key] = (count + 1, size)
count_keys(doc)
for at, value in leaves(doc):
strings[digest(value.encode())].append((at, len(encoded(value))))
if isinstance(doc, dict):
for k, v in doc.items():
items = v.items() if k == 'input' and isinstance(v, dict) else [(k, v)]
for field, item in items:
label = 'input.' + field if k == 'input' else field
b = encoded(item)
key = (req.get('prompt_version'), label, digest(b))
block_groups[key][0] += 1
block_groups[key][1] += len(b)
block_groups[key][2] = len(b)
dup_strings = sum(sum(n for _, n in entries[1:])
for entries in strings.values())
metadata_fields.update(keys)
partition = None
if req.get('prompt_version', '').startswith('investigation_project_'):
system, prefix, live = GeminiProvider._messages(req, stable_case_context=True)
partition = {'prefix_bytes': len(encoded([system, prefix])),
'live_bytes': len(encoded(live)),
'prefix_hash': digest(encoded([system, prefix]))}
row = {'ref': name, 'prompt': req.get('prompt_version'),
'message_utf8_bytes': msg_bytes,
'user_repeated_strings_ge80_bytes': dup_strings,
'json_key_bytes': sum(keys.values()),
'top_json_keys_by_bytes': keys.most_common(8),
'stable_partition': partition}
per_call.append(row)
by_prompt[req.get('prompt_version')].append(row)
components = defaultdict(lambda: [0, 0, 0])
for (prompt, label, _), (count, total, size) in block_groups.items():
val = components[(prompt, label)]
val[0] += total
val[1] += size
val[2] += count
exact = repeated(groups)
canon = repeated(canonical)
summary = {
'path': path.as_posix(),
'identity': {k: manifest[k] for k in ('case_id', 'package_id', 'package_generation', 'state')},
'zip_bytes': path.stat().st_size,
'raw_member_bytes': sum(v.file_size for v in infos.values()),
'compressed_member_bytes': sum(v.compress_size for v in infos.values()),
'members': len(infos),
'zip_compression_methods': dict(compression_methods),
'deflate6_estimated_payload_bytes': sum(deflate_estimate.values()),
'deflate6_by_category': dict(deflate_estimate),
'categories': dict(categories),
'stored_context_components': {k:{'total_bytes':v[0], 'unique_value_bytes':v[1],
'repeated_value_bytes':v[0]-v[1], 'occurrences':v[2]} for k,v in sorted(context_components.items(), key=lambda kv:kv[1][0], reverse=True)},
'largest_members': [{'path': v.filename, 'raw_bytes': v.file_size,
'compressed_bytes': v.compress_size} for v in sorted(infos.values(), key=lambda v:v.compress_size, reverse=True)[:12]],
'exact_duplicate_groups': len(exact),
'exact_duplicate_raw_bytes': sum(v['raw_duplicate_bytes'] for v in exact),
'exact_duplicate_compressed_bytes': sum(v['compressed_duplicate_bytes'] for v in exact),
'largest_exact_duplicate_groups': exact[:8],
'canonical_json_duplicate_raw_bytes': sum(v['raw_duplicate_bytes'] for v in canon),
'original_tar_members_also_stored_separately': dict(archive_members),
'request_count': len(requests),
'research_repetition': {
'evidence_item_total_bytes': sum(n*s for n,s in evidence_items.values()),
'evidence_item_unique_bytes': sum(s for n,s in evidence_items.values()),
'evidence_item_fields_bytes':dict(evidence_sizes),
'source_line_total_bytes':sum(n*s for n,s in source_lines.values()),
'source_line_unique_bytes':sum(s for n,s in source_lines.values()),
'source_line_occurrences':sum(n for n,s in source_lines.values()),
'source_line_unique_count':len(source_lines)},
'prompts': {p: {'calls':len(rows), 'message_utf8_bytes':sum(r['message_utf8_bytes'] for r in rows),
'within_call_repeated_strings_ge80_bytes':sum(r['user_repeated_strings_ge80_bytes'] for r in rows)} for p, rows in by_prompt.items()},
'system_message_total_bytes':sum(size*count for (_,size),count in system_groups.items()),
'system_message_repeated_bytes':sum(size*(count-1) for (_,size),count in system_groups.items()),
'top_level_components': [{'prompt':p,'field':label,'total_canonical_bytes':v[0],
'unique_value_bytes':v[1], 'repeated_value_bytes':v[0]-v[1], 'occurrences':v[2]}
for (p,label),v in sorted(components.items(), key=lambda kv:kv[1][0], reverse=True)],
'json_keys_by_bytes':metadata_fields.most_common(15),
'calls': per_call,
}
assert sum(v['raw_bytes'] for v in categories.values()) == summary['raw_member_bytes']
assert sum(v['compressed_bytes'] for v in categories.values()) == summary['compressed_member_bytes']
return summary
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('packages', nargs='+', type=Path)
parser.add_argument('--output', required=True, type=Path)
args = parser.parse_args()
report = {'measurement':'redundancy_audit_v1', 'units':'bytes, UTF-8, decimal MB/GB',
'limitations': ['No paid API calls; no quality or token savings inferred.',
'Categories are disjoint; duplicate, archive-copy and repeated-string measures overlap.',
'Compressed duplicate sizes are an upper bound, not a compatible deletion proposal.',
'Repeated strings are counted within user messages, minimum 80 UTF-8 bytes; references and JSON framing not deducted.',
'Component repetition compares whole canonical top-level values within each prompt version, ignoring order of calls.',
'Saved requests are canonical gateway input, not literal cached HTTP payload.'],
'packages':[audit(p) for p in args.packages]}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, ensure_ascii=False, indent=2)+'\n', encoding='utf-8')
print(json.dumps({'output':str(args.output),'packages':len(report['packages'])}))
if __name__ == '__main__':
main()