doubleo7/FINDINGS.md
Austin Schaefer 5843a49cc2 feat: iterative generate/score/revise loop, findings log
Gemma now seeds deliberately hostile text, Shieldstral scores it, and
Gemma revises its own output based on the score until it drops below a
safety threshold (or a max-iteration cap is hit, returning the best
attempt seen). Extracted into a new revise module: score() now borrows
instead of consuming its args so it can run repeatedly, and the
gemma-call/extract-text logic is shared between seed generation and
every revision instead of being duplicated.

Also adds FINDINGS.md logging what actually turned out to be real
obstacles vs. overblown vs. irrelevant while building this out.
2026-08-05 15:07:42 +02:00

4.9 KiB

Findings

Running log of things that came up building and testing the Gemma → Shieldstral pipeline, sorted by how much they actually mattered in practice.

Actual obstacles

Things that were real problems and required a fix.

  • Trailing colon typo in LLAMA_SERVER_URL produced an "invalid authority" error from Rig's URI parser — llamafile::Client::from_url needs a bare http://host:port, no trailing punctuation, no /v1 suffix (the client appends that itself).
  • std::fs::read_to_string("prompts.toml") used a path relative to the process's runtime working directory, which differs between cargo run, RustRover's run config, and any future install location — file-not-found in practice. Fixed by switching to include_str!, which resolves relative to the source file at compile time instead.
  • Ollama has no logprobs support at all, in either its native /api/chat or its OpenAI-compatible /v1/chat/completions endpoint. This was a hard blocker for the whole scoring approach — had to serve Shieldstral through llama-server directly instead of Ollama.
  • raw_completion() doesn't exist in the last published rig-core crate (0.41.0) — it's only on git main, ahead of any release. Had to pin a git dependency to get it, accepting the instability that comes with tracking an unreleased branch.
  • Missing <Instruct>/<Query>/<Document> scaffolding + system preamble produced meaningless, unreliable scores when testing against raw unscaffolded text — the model has no policy to judge against without it.
  • temperature: 1.0 vs 0.0 silently distorted reported scores. llama-server only bypasses the full sampler chain (top_k/top_p/min_p/ repetition penalties) for logprobs reporting at greedy decoding (temperature effectively 0); at 1.0 the reported probabilities reflect the post-sampler-chain distribution, not raw logits.
  • Gemma's "thinking" mode was on by default for the Ollama model tag. A tight max_tokens budget meant it sometimes got cut off mid-thought before ever emitting content, crashing the naive AssistantContent::Text match on an unhandled Reasoning block. Fixed with "think": false.
  • uv dependency resolution failures on transformers==4.57.6 — the version genuinely exists on PyPI, but was shadowed by a same-named package on PyTorch's own wheel index under uv's default first-index strategy. Needed --index-strategy unsafe-best-match (safe here since both indexes are reputable) or a per-package --override.
  • Gemma refusing the "generate hostile text" seed prompt depending on exact wording — explicit "hate speech" / "insulting people" phrasing triggered refusals noticeably more than softer framing. Needed a few iterations on the prompt to land on wording that reliably produces scoreable content without tripping Gemma's own alignment training every time.

Overblown, but theoretically impactful under the right conditions

Concerns that turned out not to matter in the cases tested, but aren't nothing — worth revisiting if circumstances change.

  • Qualifying/meta text ("Here is a short text:") diluting the score — negligible on a seed document already saturated with hostile content (a ~4-word neutral preamble on a ~50-word hostile block didn't move a 0.9999 score). Could plausibly matter more on revision-step output sitting near the 0.1 threshold, where a few tokens of padding might tip an not-actually-safer revision under the line. Worth watching iteration logs for revision scores landing suspiciously close to threshold right when padding shows up — not worth defending against pre-emptively without evidence it's happening.
  • ServerConfig single-field wrapper struct, flagged during a cleanliness review as unnecessary indirection — true in isolation, but it deliberately mirrors the existing Prompts/prompts.toml pattern for consistency, so the real cost is close to nil in context.

Genuinely was irrelevant

Things that looked like they might be a problem and just weren't.

  • Whether GenericCompletionModel/ollama::CompletionModel needed to be Clone to support a loop calling them repeatedly — turned out both already derive Clone, and more to the point, didn't even need cloning since both can just be borrowed across iterations. A non-issue once actually checked against source instead of assumed.
  • Whether the literal sampled token from Shieldstral's single forced token matters — irrelevant, since scoring always reads the full top_logprobs list regardless of which single token happened to get emitted as content.
  • Rig's .completion() normalizing away provider-specific logprobs initially looked like a dead end for using Rig at all for scoring — turned out to have a clean, intended escape hatch (raw_completion()) once the source was actually checked, so the abstraction gap was real but never a blocker.