Skip to content

Investigate inline citation emission to eliminate ${ref:N} reference drift #49

Description

@nabinpkl

Context

Today the agent's emit_claim tool takes body_markdown containing ${ref:N} placeholders plus a separate provenance: list[ProvenanceRef] array. Two paths run after the model returns:

  1. Placeholder gate (agent_service/policy/structural.py): regex resolves every ${ref:N} against the provenance list. Catches syntactic mismatches (N out of bounds, no provenance entries, etc).
  2. Structural gate: per-Number provenance entries are value-compared against a per-thread binding store populated from primitive tool outputs. Catches fabricated values.

Neither catches semantic reference drift: the model writes \"the wallet has \${ref:0} connections\" but provenance[0] is the wallet entity (community/edge/etc), and the actual connection-count number sits at provenance[3]. Both gates pass: ref:0 resolves syntactically, no value is fabricated. The user reads a wrong-pointer claim.

This failure mode has no clean fix in the post-hoc reconciliation shape. The 2025-2026 research surfaced one architectural alternative that side-steps it entirely.

What the field shipped

Anthropic Citations API (Jan 2025, Bedrock GA Jun 2025): citations are model-emitted as inline annotations during generation; the API server extracts the cited substring by character/page/chunk offset. There is no separate symbolic name to drift because the citation IS the offset into the source. cited_text is guaranteed to be a literal substring of the source document.

Limitations: text spans only, source must be a document block, incompatible with Structured Outputs (mutually exclusive with response_format: json_schema).

Adapting that model to our problem (citations of tool-output VALUES, not document substrings) means inline-emitting the binding-store lookup key at the position the value appears in prose, instead of the post-hoc ${ref:N} symbolic name. Roughly:

\"the wallet moved <cite metric=\\\"out_volume_sol\\\" value=\\\"50.0\\\">50 SOL</cite>\"

The verifier reads each <cite> tag, looks up metric in the binding store, value-compares — same gate, but the binding-store key is bound to the prose position, not to a separate symbol that can mis-resolve.

Tradeoffs:

  • Closes the semantic-mis-pointing hole.
  • Forces the model to emit XML/JSON inline, which Gemini-class models handle but at higher token cost.
  • Schema-incompatible with strict structured-output modes (same constraint Anthropic hit).

Research-backed alternatives weighed and rejected

  • Self-RAG reflection tokens (Asai 2024): requires fine-tuning into the model. Closed-model providers don't offer this.
  • Chain-of-Verification draft→verify→revise (Meta 2023, ACL 2024 Findings): doubles inference cost, no structured artifact for downstream UIs, thin production adoption.
  • VeriFastScore atomic decomposition (May 2025): eval-time tooling, not a user-facing gate.

Files to inspect when revisiting

  • agent_service/agent.py:80-96EmitClaimInput schema (the ${ref:N} contract).
  • agent_service/policy/structural.py — placeholder + value-compare gates.
  • agent_service/policy/binding_store.py — the lookup table the structural gate consults.
  • agent_service/prompts/system_v4.txt:59-77 — the prompt that teaches the model to use ${ref:N}.

Decision needed before scoping

Whether semantic reference drift fires often enough in real turns to justify the schema change. Concrete first step: add span attribute mcae.gate.structural.ref_drift_suspected that flags claims where every gate passed but the cited number's metric name does NOT match the prose's surrounding noun. Run for a week, count incidents, then decide.

Out of scope

  • The general "claim verify citation" architecture review (this is one specific class of failure within an already-functioning gate stack; the rest of the verification pipeline is ahead of SOTA for our problem class per the May 2026 research).
  • Anthropic Citations API integration directly (it's text-substring only, doesn't fit the tool-output-value case).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions