Examples 06 — Faithful Summary
Example 06 Multi-Model

Faithful Summary

Claude produces a 3–5 sentence summary of a source document. A deterministic formatting op assembles the source and summary into a single verification prompt. Gemini then checks whether every factual claim in the summary is grounded in the source text, returning a boolean verdict.

The key insight behind this pattern

A model that generated a summary has already committed to its framing. When you ask the same model to verify its own output, it tends to rationalise what it wrote rather than challenge it — it sees what it expects to see.

A second, independent model has no such commitment. It reads the source and the summary with fresh eyes and is far more likely to notice when a claim has no basis in the text.

What this example teaches

  • The cross-model verification pattern: using one model to generate and a different, independent model to audit the output
  • How to set provider and model parameters per vertex — each op selects its own model, independent of the rest of the graph
  • Wiring a custom deterministic op (FormatFaithfulnessCheckOp) to prepare the verification prompt without burning any tokens
  • Using AIBoolOp as a typed binary verdict: the tightest possible AI call, with an output budget of ~2 tokens
  • How two AI calls from different providers compose cleanly in a single workflow — no special framework support needed beyond the param fields
View source on GitHub
The workflow

Pipeline structure

Four vertices. One model generates; one deterministic op formats; a second model verifies. The boundary between providers is explicit in the graph topology.

source_const
string → source
summarize
AIComputeStringToStringOp
provider: claude
format_check
FormatFaithfulnessCheckOp
det — 0 tokens
verify
AIBoolOp
provider: gemini

The source wire feeds both the summarize op and the format_check op — workflow parallelism is free, wiring is explicit.

det op

Deterministic step

FormatFaithfulnessCheckOp

Custom op that takes two string wires — source and summary — and concatenates them into a single verification prompt string. No AI, no API call: just Go string formatting. This keeps the prompt assembly logic testable and the AI boundary clean.

Because this is a deterministic op, the formatted prompt is always identical given the same inputs — which makes the Gemini call fully reproducible in tests.

AI op

AI steps

summarize — AIComputeStringToStringOp (Claude)

Reads the source text and produces a 3–5 sentence summary. Instructed to include only information explicitly stated in the text — no inferences, no added context. Uses provider: "claude", model: "claude-sonnet-4-6".

verify — AIBoolOp (Gemini)

Receives the combined source + summary prompt and answers: "does every factual claim in the summary appear in or follow directly from the source?" Returns a typed bool. Uses provider: "gemini", model: "gemini-3-flash-preview". Output budget: ~2 tokens.

Implementation

Walkthrough

1. Assigning models per vertex

The provider and model params are passed via the standard Params map on any AI op. There is no global model setting — each vertex is fully independent. The summarize and verify vertices use completely different providers without any special wiring.

main.go — buildGraph()
func buildGraph() (*graph.Graph, error) {
    return graph.NewBuilder("faithful_summary").
        // Stage 1: inject the source document
        Vertex("source_const").Op("source_const").
            Output("Result", "source").

        // Stage 2: Claude summarises the source
        Vertex("summarize").Op("AIComputeStringToStringOp").
            Params(map[string]string{
                "operation": "summarize this article in 3–5 concise sentences; " +
                              "include only information explicitly stated in the text, " +
                              "do not add context or draw inferences",
                "provider":  "claude",
                "model":     "claude-sonnet-4-6",
            }).
            Input("Input", "source").
            Output("Result", "summary").

        // Stage 3: assemble the verification prompt (deterministic, 0 tokens)
        Vertex("format_check").Op("FormatFaithfulnessCheckOp").
            Input("Source", "source").
            Input("Summary", "summary").
            Output("Query", "query").

        // Stage 4: Gemini independently checks faithfulness
        Vertex("verify").Op("AIBoolOp").
            Params(map[string]string{
                "predicate": "does every factual claim in the summary appear in or " +
                              "follow directly from the source document, with no information added or invented?",
                "provider": "gemini",
                "model":    "gemini-3-flash-preview",
            }).
            Input("Input", "query").
            Output("Result", "faithful").
        Build()
}

2. The deterministic formatting op

FormatFaithfulnessCheckOp is a custom op that implements the standard dagor operator interface. Its entire Run method is a single fmt.Sprintf. By making prompt assembly explicit and deterministic, the verification step becomes reproducible: given the same source and summary, the Gemini prompt is always byte-for-byte identical.

main.go — FormatFaithfulnessCheckOp.Run()
func (op *FormatFaithfulnessCheckOp) Run(_ context.Context) error {
    op.Query = fmt.Sprintf(
        "Source document:\n%s\n\nSummary to verify:\n%s",
        *op.Source,
        *op.Summary,
    )
    return nil
}

3. Reading the results

Both the summary string and the faithfulness boolean are output wires that can be read after eng.Run(ctx). The typed output makes downstream handling straightforward — no parsing, no schema coercion.

main.go — reading outputs
out := result{SourceLength: len(source)}

if raw, ok := eng.GetOutput("summary"); ok {
    if p, ok := raw.(*string); ok && p != nil {
        out.Summary = *p
    }
}
if raw, ok := eng.GetOutput("faithful"); ok {
    if p, ok := raw.(*bool); ok && p != nil {
        out.Faithful = *p    // true = every claim is grounded in the source
    }
}

Run it

Supply either a path to a text file or an inline string. Both CLAUDE_API_KEY and GEMINI_API_KEY must be set.

shell
export CLAUDE_API_KEY=<your Anthropic key>
export GEMINI_API_KEY=<your Google key>

# Summarise a file
go run . --file /path/to/article.txt

# Summarise inline text
go run . --text "The quick brown fox…"

Example output

{
  "source_length": 3847,
  "summary": "The European Union's AI Act establishes a risk-based regulatory framework…",
  "faithful": true
}

Run it as an MCP server

The same binary is dual-mode. Pass --mcp and instead of running once and exiting it speaks the Model Context Protocol over stdin/stdout, exposing the entire workflow as a single MCP tool, summarize_and_verify. The Go SDK derives the tool's input schema from the workflow's UserInput struct and validates every tools/call request against it, so an agent drives the whole Claude-summarise → Gemini-verify pipeline with one tool call.

shell
# Speak MCP over stdin/stdout instead of running once
go run ./examples/faithful-summary --mcp

Register it with any MCP client by pointing the client at that command:

.mcp.json
{
  "mcpServers": {
    "faithful-summary": {
      "command": "go",
      "args": ["run", "./examples/faithful-summary", "--mcp"]
    }
  }
}
Pattern Depth

When to use the cross-model verification pattern

Faithfulness checks

Any summarisation, paraphrase, or extraction task where hallucinated claims are costly. The generator focuses on quality prose; the verifier focuses on factual grounding.

Code review pipeline

One model generates code; a second model audits it for security issues, correctness, or spec compliance. Different training distributions catch different bugs.

Classification confidence

When the cost of a misclassification is high, run a second independent classification on the same input and flag cases where the models disagree for human review.

💡

The broader principle: any time model A produces output that model A would be biased to approve, route the approval step to model B. The cross-model separation is a one-line param change in the workflow.

Token usage

Two AI calls: one to generate (~500 tokens in/out depending on document length), one to verify (~2 tokens out). The verification call is the cheapest possible AI op — a single boolean answer.

Op Provider Calls Input tokens Output tokens
AIComputeStringToStringOp Claude 1 ~250 + source length ~100–200
FormatFaithfulnessCheckOp det 1 0 0
AIBoolOp Gemini 1 ~150 + source + summary ~2
← Example 05 All Examples →