Tool-Augmented RAG: Agents Fetch Exact File Ranges Before Answering

Tool-Augmented RAG: Agents Fetch Exact File Ranges Before Answering

Classic RAG retrieves fuzzy chunks and hopes the model does not invent the next three lines. Tool-augmented RAG flips the last mile: use retrieval only as a pointer, then force a read_file(path, start, end) tool against the live tree before any claim ships to the engineer. You keep recall from vectors while answers cite exact bytes that exist on HEAD.

⚡ TL;DR: Treat KB hits as candidates, not evidence. Require a file-range tool call (with SHA) before final answers. Reject completions that lack verified citations. Cap tool depth and bytes. Combine with Citation-required RAG and Bedrock Converse tool choice.

Retrieval proposes; tools dispose

// agent/tool-rag.ts
type Hit = { path: string; start: number; end: number; score: number; blobSha?: string };

export async function answer(question: string, retrieve: (q: string) => Promise<Hit[]>) {
  const hits = await retrieve(question);
  // ✅ Pass hits as tool hints, not as final context dump
  const system = `You MUST call read_range for every claim. Never invent lines.`;
  return runAgent({ system, tools: [readRangeTool], hints: hits.slice(0, 8), question });
}

❌ Stuffing top-12 chunk texts into the prompt and skipping verification — that is ordinary RAG with extra latency.

Exact range tool with SHA checks

# tools/read_range.py
from pathlib import Path
import hashlib

MAX_BYTES = 24_000

def read_range(path: str, start: int, end: int, expect_sha: str | None = None) -> dict:
    p = Path(path).resolve()
    if not str(p).startswith("/workspace/"):  # ✅ allowlist root
        return {"error": "path_denied"}
    text = p.read_text(encoding="utf-8", errors="replace")
    sha = hashlib.sha1(text.encode()).hexdigest()
    if expect_sha and sha != expect_sha:
        return {"error": "stale_blob", "sha": sha}
    lines = text.splitlines()
    start = max(1, start); end = min(len(lines), end)
    if end < start or (end - start) > 400:
        return {"error": "range_invalid"}
    body = "\n".join(lines[start - 1 : end])
    if len(body.encode()) > MAX_BYTES:
        return {"error": "too_large"}
    return {"path": str(p), "start": start, "end": end, "sha": sha, "text": body}

Force tool choice until evidence exists

With Bedrock Converse, prefer toolChoice: any (or specific read_range) on the first turn when hints are present, then auto once citations are attached. Fail closed if the model tries to answer with zero successful tool results.

{
  "toolChoice": { "tool": { "name": "read_range" } },
  "comment": "First hop must verify a candidate path from retrieval"
}

Budget depth, bytes, and loops

Guard Default Why
Max tool calls 6 Prevent path thrash
Max lines / call 400 Keep context sane
Max unique paths 4 Focus the answer
Stale SHA Re-retrieve or abort Avoid lying about HEAD

Log every tool I/O for deterministic replay. Pair with LLM output validators when the agent also patches.

Closing checklist

✅ Dos
– ✅ Use RAG hits as pointers into read_range
– ✅ Verify blob SHA before trusting lines
– ✅ Require citations from tool results in the final answer
– ✅ Allowlist repository roots; cap bytes and depth
– ✅ Fail closed when tools return errors / stale SHAs

❌ Don’ts
– ❌ Don’t paste unverified chunk text as ground truth
– ❌ Don’t allow unbounded recursive reads
– ❌ Don’t skip tool choice on the evidence hop
– ❌ Don’t read secrets paths (.env, keys) even if retrieved
– ❌ Don’t answer from memory when the tool failed

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply