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
provider and model parameters per vertex — each op selects its own model, independent of the rest of the graphFormatFaithfulnessCheckOp) to prepare the verification prompt without burning any tokensAIBoolOp as a typed binary verdict: the tightest possible AI call, with an output budget of ~2 tokensFour vertices. One model generates; one deterministic op formats; a second model verifies. The boundary between providers is explicit in the graph topology.
The source wire feeds both the summarize op and the format_check op — workflow parallelism is free, wiring is explicit.
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.
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()
}
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.
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
}
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.
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
}
}
Supply either a path to a text file or an inline string. Both CLAUDE_API_KEY and GEMINI_API_KEY must be set.
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
}
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.
# 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:
{
"mcpServers": {
"faithful-summary": {
"command": "go",
"args": ["run", "./examples/faithful-summary", "--mcp"]
}
}
}
Any summarisation, paraphrase, or extraction task where hallucinated claims are costly. The generator focuses on quality prose; the verifier focuses on factual grounding.
One model generates code; a second model audits it for security issues, correctness, or spec compliance. Different training distributions catch different bugs.
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.
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 |