LocalLens
Walkthrough

2. Chunking e o prompt embasado

A lógica do lado de recuperação que ainda não conhece o ciclo de vida do modelo.

src/rag.ts faz dois trabalhos: quebrar documentos em chunks e construir um prompt que embasa o modelo nos trechos recuperados. Os dois são quase puros. Chamam o ragChunk do QVAC para tokenização; fora isso só conhecem tipos de domain.ts.

Chunking

src/rag.ts
import { ragChunk } from "@qvac/sdk";
import type { ChatMessage, LocalDocument, SearchHit, TextChunk } from "./domain.ts";

export type ChunkOptions = {
  brainId: string;
  chunkSize?: number;
  chunkOverlap?: number;
};

export async function chunkDocuments(
  documents: LocalDocument[],
  options: ChunkOptions,
): Promise<TextChunk[]> {
  const chunks: TextChunk[] = [];
  for (const document of documents) chunks.push(...(await chunkDocument(document, options)));
  return chunks;
}

export async function chunkDocument(
  document: LocalDocument,
  { brainId, chunkSize = 220, chunkOverlap = 40 }: ChunkOptions,
): Promise<TextChunk[]> {
  if (chunkSize <= chunkOverlap) throw new Error("chunkSize must be greater than chunkOverlap");
  const text = document.content.replace(/\r\n/g, "\n").trim();
  if (!text) return [];

  const qvacChunks = await ragChunk({
    documents: text,
    chunkOpts: {
      chunkSize,
      chunkOverlap,
      chunkStrategy: "paragraph",
      splitStrategy: "token",
    },
  });

  return qvacChunks
    .map((chunk) => chunk.content.trim())
    .filter(Boolean)
    .map((content, chunkIndex) => ({
      id: `${document.checksum.slice(0, 12)}-${chunkIndex}`,
      brainId,
      relativePath: document.relativePath,
      chunkIndex,
      content,
      checksum: document.checksum,
    }));
}

Tamanho de chunk e overlap

Defaults: 220 tokens com 40 tokens de overlap. Duas razões pelas quais esses números funcionam para conteúdo formato documentação:

  • A maior parte dos parágrafos num README ou nota cabem confortavelmente em 220 tokens, então o chunker raramente quebra no meio de uma ideia.
  • 40 tokens de overlap mantêm uma sentença que atravessa dois chunks buscável dos dois lados sem inflar o índice.

A checagem em runtime chunkSize > chunkOverlap é uma guarda contra loops infinitos silenciosos — o QVAC SDK senão produziria chunks sobrepostos que nunca avançam.

IDs estáveis

O ID do chunk combina o checksum do documento e o índice do chunk:

id: `${document.checksum.slice(0, 12)}-${chunkIndex}`

Duas consequências:

  • Reindexar o mesmo arquivo produz os mesmos IDs de chunk, o que torna reindexação incremental tratável depois.
  • Editar um byte único muda o checksum e portanto todo ID de chunk daquele arquivo. Isso está correto — os embeddings já não são mais válidos.

O prompt embasado

export function buildGroundedHistory(question: string, hits: SearchHit[]): ChatMessage[] {
  const context = hits
    .map(
      (hit, index) => `[${index + 1}] ${hit.relativePath}#chunk-${hit.chunkIndex}\n${hit.content}`,
    )
    .join("\n\n---\n\n");

  return [
    {
      role: "system",
      content: [
        "You are LocalLens, a local-first file chat assistant that answers questions strictly from the provided source excerpts.",
        "Rules:",
        "1. Only use facts that appear in the excerpts. If the answer is not in them, say so plainly.",
        "2. Refer to source excerpts inline using bracketed numbers, for example [1] or [2], when you use them.",
        "3. Answer in the same language as the user's question.",
        "4. Keep answers focused and concrete. No filler.",
        "5. Do not include hidden reasoning, chain-of-thought, or thinking tags.",
      ].join(" "),
    },
    {
      role: "user",
      content: `Source excerpts:\n\n${context || "No matching chunks were found."}\n\nQuestion:\n${question}`,
    },
  ];
}

Essa é a estratégia anti-alucinação inteira. Cinco regras de sistema, mais um turno de usuário que dispõe os trechos como evidência numerada.

O fallback "no matching chunks"

Se hits é vazio, a mensagem do usuário contém a string literal "No matching chunks were found." em vez de um bloco de trechos. A primeira regra do system prompt entra em ação e o modelo diz que não sabe.

Trechos numerados e citações

Cada hit vira:

[N] relative/path#chunk-<index>
<chunk content>

O [N] é o que o modelo ecoa de volta como [1], [2] na resposta. O caminho relativo permite ao chamador renderizar uma lista de fontes que linka de volta para o arquivo.

Por que sem histórico de chat?

Cada chamada a buildGroundedHistory produz um histórico fresco de duas mensagens baseado na pergunta atual e nos hits da busca. Turnos anteriores não passam para frente. É assim que o LocalLens fica embasado ao longo de uma sessão — toda pergunta é sua própria rodada de recuperação de evidência.

O que você pode rodar depois desse passo

Testes vivem em tests/prompt.test.ts e tests/chunker.test.ts. Os dois passam com só domain.ts e rag.ts no lugar.

A seguir: o gateway QVAC, onde chunking e prompting encontram carregamento de modelo e inferência.

On this page