feat(wiki): front-load static rules in chunk-citation prompt for prefix cache
WikiChunkCitationPrompt placed the per-batch <chunks> block before the static rules, output schema and the per-document-stable <candidate_slugs>. Within one document only ChunksXML changes between batches, so the stable content trailing the chunks could not form a long common prefix and providers re-billed the static rules on every citation batch (issue #1687 measured ~6.4% prefix-cache hit rate on wiki synthesis). Reorder to [rules | output schema | candidate_slugs | chunks] so every batch after the first shares the long [rules | candidate_slugs] prefix. No instruction text was dropped; only block order and the inline above/below directional references changed. Verified the cacheable prefix grows from 281 to 2366 bytes and is byte-identical across batches. Addresses #1687 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -207,30 +207,27 @@ Output ONLY valid JSON. Example:
|
||||
// to read a batch of chunks and, for each candidate entity/concept, list the
|
||||
// chunk IDs that substantively discuss it. This keeps per-slug "facts" in
|
||||
// their verbatim form (the chunk text) instead of asking the LLM to paraphrase.
|
||||
// Block order matters for provider prefix caching: the static rules,
|
||||
// output schema and the per-document-stable <candidate_slugs> are placed
|
||||
// BEFORE the per-batch <chunks> block. Within one document only ChunksXML
|
||||
// changes between batches, so every batch after the first shares the long
|
||||
// [rules | candidate_slugs] prefix and avoids re-billing the static rules.
|
||||
const WikiChunkCitationPrompt = `You are a precise citation system. Your job is to scan a batch of document chunks and decide, for each candidate entity/concept below, which chunks substantively discuss it.
|
||||
|
||||
<candidate_slugs>
|
||||
{{.CandidateSlugs}}
|
||||
</candidate_slugs>
|
||||
|
||||
<chunks>
|
||||
{{.ChunksXML}}
|
||||
</chunks>
|
||||
|
||||
<instructions>
|
||||
**IMPORTANT: Write ALL names, descriptions, and details in {{.Language}}**.
|
||||
|
||||
### Primary task
|
||||
For each candidate slug above, select the chunk IDs (from the <chunks> block) that **substantively discuss** that entity/concept. "Substantively" means the chunk states at least one concrete fact, attribute, step, date, number, relationship, or other useful piece of information about the candidate — not a passing mention.
|
||||
For each candidate slug (listed in <candidate_slugs> below), select the chunk IDs (from the <chunks> block below) that **substantively discuss** that entity/concept. "Substantively" means the chunk states at least one concrete fact, attribute, step, date, number, relationship, or other useful piece of information about the candidate — not a passing mention.
|
||||
|
||||
- Only cite chunks that appear in the <chunks> block above.
|
||||
- Only cite chunks that appear in the <chunks> block below.
|
||||
- Use the "id" attribute of each <c> element verbatim (e.g. "c003").
|
||||
- If a candidate is not meaningfully discussed in ANY chunk in this batch, omit it from the output (do not include empty arrays).
|
||||
- A chunk CAN be cited by multiple candidates if it genuinely discusses multiple of them.
|
||||
- If a chunk is overly long or mixes unrelated topics, still cite it for every candidate it discusses.
|
||||
|
||||
### Secondary task: new slugs
|
||||
If this batch reveals a significant entity/concept that is **NOT** in <candidate_slugs>, you may add it under "new_slugs" so it gets incorporated. Only add genuinely new, substantively-discussed items. Do NOT rediscover items already listed above — reuse their slug if they are already candidates.
|
||||
If this batch reveals a significant entity/concept that is **NOT** in <candidate_slugs>, you may add it under "new_slugs" so it gets incorporated. Only add genuinely new, substantively-discussed items. Do NOT rediscover items already listed in <candidate_slugs> — reuse their slug if they are already candidates.
|
||||
|
||||
Each new slug must include:
|
||||
- "type": "entity" or "concept"
|
||||
@@ -261,7 +258,17 @@ Output format:
|
||||
]
|
||||
}
|
||||
|
||||
If nothing in this batch is cite-worthy, return: {"citations": {}, "new_slugs": []}`
|
||||
If nothing in this batch is cite-worthy, return: {"citations": {}, "new_slugs": []}
|
||||
|
||||
<candidate_slugs>
|
||||
{{.CandidateSlugs}}
|
||||
</candidate_slugs>
|
||||
|
||||
<chunks>
|
||||
{{.ChunksXML}}
|
||||
</chunks>
|
||||
|
||||
Now apply the instructions above to the chunks and output ONLY the JSON.`
|
||||
|
||||
// WikiPageModifyPrompt updates an existing wiki page with new additions and removes stale/deleted information in a single pass.
|
||||
const WikiPageModifyPrompt = `You are a wiki editor tasked with updating an existing wiki page. You must process a set of NEW information to add, AND/OR a set of deleted documents whose exclusive contributions must be REMOVED.
|
||||
|
||||
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func TestWikiGranularityGuidance_RoutesByKey(t *testing.T) {
|
||||
@@ -61,3 +62,64 @@ func TestWikiGranularityGuidance_BlocksAreDistinct(t *testing.T) {
|
||||
t.Error("exhaustive block should self-identify")
|
||||
}
|
||||
}
|
||||
|
||||
func renderWikiChunkCitation(t *testing.T, candidateSlugs, chunksXML, lang string) string {
|
||||
t.Helper()
|
||||
tmpl, err := template.New("cite").Parse(WikiChunkCitationPrompt)
|
||||
if err != nil {
|
||||
t.Fatalf("parse template: %v", err)
|
||||
}
|
||||
var b strings.Builder
|
||||
if err := tmpl.Execute(&b, map[string]string{
|
||||
"CandidateSlugs": candidateSlugs,
|
||||
"ChunksXML": chunksXML,
|
||||
"Language": lang,
|
||||
}); err != nil {
|
||||
t.Fatalf("execute template: %v", err)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// TestWikiChunkCitationPrompt_StablePrefixAcrossBatches verifies the property
|
||||
// that enables provider prefix caching (issue #1687): within one document the
|
||||
// candidate slugs and the static rules are constant and only the per-batch
|
||||
// <chunks> block changes, so everything up to <chunks> must be byte-identical
|
||||
// across batches. If the static rules trailed <chunks> they would be re-billed
|
||||
// on every batch and this prefix would diverge.
|
||||
func TestWikiChunkCitationPrompt_StablePrefixAcrossBatches(t *testing.T) {
|
||||
const slugs = "entity/acme = Acme Corp\nconcept/rag = Retrieval-Augmented Generation"
|
||||
|
||||
a := renderWikiChunkCitation(t, slugs, `<c id="c001">first batch text</c>`, "English")
|
||||
b := renderWikiChunkCitation(t, slugs, `<c id="c099">a completely different second batch</c>`, "English")
|
||||
|
||||
// Match the standalone <chunks> tag line (the per-batch data block), not the
|
||||
// inline "<chunks> block" references inside the instructions prose.
|
||||
const marker = "\n<chunks>\n"
|
||||
ia := strings.Index(a, marker)
|
||||
ib := strings.Index(b, marker)
|
||||
if ia < 0 || ib < 0 {
|
||||
t.Fatalf("rendered prompt missing %q block", marker)
|
||||
}
|
||||
|
||||
if a[:ia] != b[:ib] {
|
||||
t.Errorf("prompt prefix before <chunks> differs across batches — provider prefix cache will miss.\nA-prefix:\n%s\n---\nB-prefix:\n%s", a[:ia], b[:ib])
|
||||
}
|
||||
|
||||
// The static rules and per-document candidate-slug block must live inside
|
||||
// that shared prefix, not after the varying chunks.
|
||||
for _, must := range []string{"### Primary task", "### JSON Formatting Rules", "\n<candidate_slugs>\n"} {
|
||||
if idx := strings.Index(a, must); idx < 0 || idx > ia {
|
||||
t.Errorf("%q must appear before <chunks> to be part of the cached prefix (idx=%d, chunks=%d)", must, idx, ia)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestWikiChunkCitationPrompt_PreservesPlaceholders guards against accidental
|
||||
// loss of a template field during future reorders.
|
||||
func TestWikiChunkCitationPrompt_PreservesPlaceholders(t *testing.T) {
|
||||
for _, field := range []string{"{{.Language}}", "{{.CandidateSlugs}}", "{{.ChunksXML}}"} {
|
||||
if !strings.Contains(WikiChunkCitationPrompt, field) {
|
||||
t.Errorf("WikiChunkCitationPrompt lost template field %q", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user