LlamaIndex 已在 GitHub 上发布了 legal-kb,这是一个公开参考应用。它被描述为一个基于 LlamaIndex Index v2(LlamaParse 平台)构建的法律文档知识库。该项目展示了一种团队称之为“检索工具包”(Retrieval Harness)的模式,用于智能体检索。
这种方法与单次检索不同。它不是每次查询只进行一次嵌入向量搜索,而是为智能体提供类似文件系统的工具。然后,智能体可以遍历一个庞大且不断演变的知识库来完成任务。这些工具模拟了工程师已经熟悉的操作:语义和关键词搜索、正则表达式 grep、文件搜索和读取。
什么是 legal-kb?
legal-kb 是一个可运行的 TanStack Start Web 应用,而不是一个库。你可以登录、创建项目、上传文件,并与智能体聊天。每个项目都会镜像为一个受管理的 LlamaCloud Index v2。上传的文件会在后台自动解析和索引。然后,聊天智能体在每次对话轮次中实时查询该索引。
通俗解释“检索工具包”(Retrieval Harness)
该工具包为你的文档提供了一个持久化的数据管道。它连接到一个数据源,对其进行索引,并保持更新。在此管道之上,它向智能体暴露一组工具。
这些工具特意设计得接近文件系统操作。智能体可以列出文件、读取文件、在文件内执行 grep 操作,或运行混合搜索。由于这些工具是通用的,你可以将工具包接入你自己的智能体。
四个智能体工具
`src/lib/agent.ts` 中的智能体被赋予了四个工具。每个工具都映射到一个 Index v2 检索 API。下表列出了它们的实现方式。
| 工具 | 后端 API | 关键参数 | 功能说明 |
|---|---|---|---|
| retrieve | beta.retrieval.retrieve | query, top_k, score_threshold, rerank_top_n, file_name, file_version | 运行混合语义搜索;可选重排序;返回文本块及引用 |
| findFiles | beta.retrieval.find | file_name, file_name_contains | 按精确文件名或子字符串搜索文件;自动分页 |
| readFile | beta.retrieval.read | file_id, offset, max_length | 读取原始文件内容,支持偏移量和长度窗口 |
| grepFile | beta.retrieval.grep | file_id, pattern, context_chars, limit | 在单个文件中匹配模式;返回字符位置 |
系统提示词强制执行了一种顺序。智能体必须首先调用 findFiles 来建立文档清单。然后通过 retrieve 缩小范围,并在引用之前使用 readFile 或 grepFile 确认确切的措辞。
底层工作原理
上传操作遵循 src/lib/files.ts 中定义的一条清晰流水线。数据字节被推送到项目的 LlamaCloud 源目录。通过 Prisma 在 PostgreSQL 中写入一条 File 和一条 ProjectFile 记录。索引同步被触发,但不会等待其完成;UI 会轮询状态直到准备就绪。
版本控制的范围限定在(项目,文件名)这一组合上。将 nda.pdf 重新上传到同一个项目会生成 v1、v2、v3 版本并存。检索层会根据版本元数据字段进行过滤。这为知识库本身提供了版本控制能力。
该智能体使用了 Vercel AI SDK 6 中的 ToolLoopAgent。你可以每轮选择 OpenAI 或 Anthropic,并自带 API 密钥。推理过程是流式传输的:Claude 模型使用扩展思考;OpenAI 推理模型使用中等推理力度。
以下是 retrieve 工具和智能体的一个精简但忠实的视图。
import { LlamaCloud } from '@llamaindex/llama-cloud'
import { tool, ToolLoopAgent } from 'ai'
import { z } from 'zod'
import { makeCitationId } from './citations'
// One tool closure per index. Wraps Index v2 retrieval APIs.
function createLlamaParseTools(apiKey: string, projectId: string, indexId: string) {
const client = new LlamaCloud({ apiKey })
const retrieve = tool({
description: 'Run a semantic retrieval query against an index.',
inputSchema: z.object({
query: z.string(),
top_k: z.number().nullable(),
score_threshold: z.number().nullable(),
rerank_top_n: z.number().nullable(), // set to enable reranking
file_name: z.string().nullable(), // metadata filter
file_version: z.number().nullable(),
}),
execute: async ({ query, top_k, score_threshold, rerank_top_n, file_name }) => {
const custom_filters = file_name
? { file_name: { operator: 'eq' as const, value: file_name } }
: undefined
const response = await client.beta.retrieval.retrieve({
index_id: indexId,
project_id: projectId,
query,
top_k,
score_threshold,
rerank: rerank_top_n != null ? { enabled: true, top_n: rerank_top_n } : undefined,
custom_filters,
})
// Return a model-readable list plus citations that drive the UI chips.
const citations = response.results.map((r) => ({
id: makeCitationId(), // e.g. "c7f2qa"
fileName: r.metadata?.file_name,
score: r.rerank_score ?? r.score ?? null,
preview: r.content.slice(0, 500),
}))
const formatted = response.results
.map((r, i) => `### Result #${i + 1}\n\n${r.content.slice(0, 600)}`)
.join('\n\n---\n\n')
return { formatted, citations }
},
})
// findFiles / readFile / grepFile follow the same shape, backed by
// client.beta.retrieval.find / .read / .grep
return { retrieve /* , findFiles, readFile, grepFile */ }
}
export function buildAgent(model, apiKey: string, projectId: string, indexId: string) {
return new ToolLoopAgent({
model,
tools: createLlamaParseTools(apiKey, projectId, indexId),
instructions:
'Always call findFiles first, ground every answer in the documents, ' +
'and cite ids inline as `cite:<id>`.',
})
} 答案附带可视化引用。每个检索到的文本块都会获得一个短 ID,例如 cite:c7f2qa。智能体会在行内引用该 ID,UI 会渲染出一个可点击的引用标签。点击该标签会打开源页面截图,并在引用的文本上显示边界框矩形。
朴素 RAG 与智能体检索框架
该框架是一种与单次 RAG 不同的执行模型。下面的对比侧重于行为差异。
| 维度 | 朴素/单次 RAG | 智能体检索框架(Index v2) |
|---|---|---|
| 检索流程 | 每次查询执行一次向量搜索 | 多步骤工具循环:查找 → 检索 → 读取/搜索 |
| 搜索模式 | 仅向量相似度 | 混合语义搜索、关键词搜索和正则表达式搜索 |
| 上下文 | 固定的 top-k 文本块 | 智能体按需读取完整文件或文件片段 |
| 数据新鲜度 | 静态索引 | 带有同步和版本控制的持久化流水线 |
| 精度控制 | 大多隐藏 | 暴露 top_k、score_threshold、rerank_top_n 参数 |
| 引用 | 文本块 ID | 带有页面截图和边界框的可视化引用 |
| 最佳适用场景 | 简短问答 | 长周期文档处理任务 |
使用场景及示例
该设计针对的是智能体需要处理大型文档集的领域。法律和金融科技是明确提到的例子。
- 考虑一个合同问题:“终止主服务协议需要什么通知?”智能体列出文件,执行检索,然后精确查找相关条款。它引用具体页面给出答案。
- 考虑对数据室进行尽职调查:智能体可以按名称查找文件,然后读取每个候选文件。它交叉核对条款,无需人工打开每一份 PDF。
- 考虑一个带版本管理的政策库:由于检索功能接受文件版本过滤器,智能体可以查询特定版本。这支持随时间推移的变更追踪。
参考实现
关键要点
- legal-kb 是一个公开参考应用,展示了基于 LlamaIndex Index v2 的智能体检索功能。
- 该智能体拥有四个文件系统风格的工具:检索(混合搜索)、查找文件、读取文件和 grep 文件。
- 一个持久化管道负责解析、索引、同步以及每个文件的版本控制。
- 答案包含可视化引用:带有引用文本边界框的页面截图。
- 技术栈为 TanStack Start、AI SDK 6、Prisma 和 WorkOS,并采用按用户加密的密钥。
LlamaIndex has published legal-kb, a public reference application on GitHub. It is described as a knowledge base for legal documents, powered by LlamaIndex Index v2 (the LlamaParse Platform). The project demonstrates a pattern the team calls a Retrieval Harness for agentic retrieval.
The approach differs from single-shot retrieval. Instead of one embedding search per query, an agent is given filesystem-style tools. It can then crawl a large, evolving knowledge base to solve a task. The tools mirror operations engineers already know: semantic and keyword search, regex grep, file search, and read.
What is legal-kb?
legal-kb is a working TanStack Start web app, not a library. You sign in, create a project, upload files, and chat with an agent. Each project is mirrored as a managed LlamaCloud Index v2. Uploaded files are parsed and indexed automatically in the background. The chat agent then queries that index live during each turn.
The Retrieval Harness, in plain terms
The harness provides a persistent data pipeline over your documents. It connects to a data source, indexes it, and keeps it updated. On top of that pipeline, it exposes a set of tools to the agent.
Those tools are deliberately close to filesystem operations. An agent can list files, read a file, grep inside a file, or run hybrid search. Because the tools are generic, you can plug the harness into your own agents.
The four agent tools
The agent in src/lib/agent.ts is given four tools. Each maps to an Index v2 retrieval API. The table below lists them as implemented.
| Tool | Backing API | Key parameters | What it does |
|---|---|---|---|
retrieve | beta.retrieval.retrieve | query, top_k, score_threshold, rerank_top_n, file_name, file_version | Runs hybrid semantic search; optional reranking; returns chunks plus citations |
findFiles | beta.retrieval.find | file_name, file_name_contains | Searches files by exact name or substring; paginates automatically |
readFile | beta.retrieval.read | file_id, offset, max_length | Reads raw file content, with offset and length windows |
grepFile | beta.retrieval.grep | file_id, pattern, context_chars, limit | Matches a pattern in one file; returns character positions |
The system prompt enforces an order. The agent must call findFiles first to establish the document inventory. It then narrows with retrieve, and confirms exact wording with readFile or grepFile before citing.
How it works under the hood
Uploads follow a clear pipeline in src/lib/files.ts. Bytes are pushed to the project’s LlamaCloud source directory. A File and ProjectFile row are written to PostgreSQL via Prisma. An index sync is triggered but not awaited; the UI polls status until ready.
Versioning is scoped to the (project, filename) pair. Re-uploading nda.pdf to the same project produces v1, v2, v3 side by side. The retrieval layer filters on the version metadata field. This gives version control over the knowledge base itself.
The agent uses the ToolLoopAgent from Vercel AI SDK 6. You pick OpenAI or Anthropic per turn and bring your own keys. Reasoning is streamed: Claude models use extended thinking; OpenAI reasoning models use a medium reasoning effort.
Here is a condensed but faithful view of the retrieve tool and the agent.
import { LlamaCloud } from '@llamaindex/llama-cloud'
import { tool, ToolLoopAgent } from 'ai'
import { z } from 'zod'
import { makeCitationId } from './citations'
// One tool closure per index. Wraps Index v2 retrieval APIs.
function createLlamaParseTools(apiKey: string, projectId: string, indexId: string) {
const client = new LlamaCloud({ apiKey })
const retrieve = tool({
description: 'Run a semantic retrieval query against an index.',
inputSchema: z.object({
query: z.string(),
top_k: z.number().nullable(),
score_threshold: z.number().nullable(),
rerank_top_n: z.number().nullable(), // set to enable reranking
file_name: z.string().nullable(), // metadata filter
file_version: z.number().nullable(),
}),
execute: async ({ query, top_k, score_threshold, rerank_top_n, file_name }) => {
const custom_filters = file_name
? { file_name: { operator: 'eq' as const, value: file_name } }
: undefined
const response = await client.beta.retrieval.retrieve({
index_id: indexId,
project_id: projectId,
query,
top_k,
score_threshold,
rerank: rerank_top_n != null ? { enabled: true, top_n: rerank_top_n } : undefined,
custom_filters,
})
// Return a model-readable list plus citations that drive the UI chips.
const citations = response.results.map((r) => ({
id: makeCitationId(), // e.g. "c7f2qa"
fileName: r.metadata?.file_name,
score: r.rerank_score ?? r.score ?? null,
preview: r.content.slice(0, 500),
}))
const formatted = response.results
.map((r, i) => `### Result #${i + 1}\n\n${r.content.slice(0, 600)}`)
.join('\n\n---\n\n')
return { formatted, citations }
},
})
// findFiles / readFile / grepFile follow the same shape, backed by
// client.beta.retrieval.find / .read / .grep
return { retrieve /* , findFiles, readFile, grepFile */ }
}
export function buildAgent(model, apiKey: string, projectId: string, indexId: string) {
return new ToolLoopAgent({
model,
tools: createLlamaParseTools(apiKey, projectId, indexId),
instructions:
'Always call findFiles first, ground every answer in the documents, ' +
'and cite ids inline as `cite:<id>`.',
})
} Answers carry visual citations. Each retrieved chunk gets a short id, such as cite:c7f2qa. The agent references that id inline, and the UI renders a clickable citation chip. Clicking it opens the source page screenshot with bounding-box rectangles over the cited text.
Naive RAG vs the agentic Retrieval Harness
The harness is a different execution model from single-shot RAG. The comparison below focuses on behavior.
| Dimension | Naive / single-shot RAG | Agentic Retrieval Harness (Index v2) |
|---|---|---|
| Retrieval flow | One vector search per query | Multi-step tool loop: find → retrieve → read/grep |
| Search modes | Vector similarity only | Hybrid semantic search, keyword, and regex grep |
| Context | Fixed top-k chunks | Agent reads full files or windows on demand |
| Freshness | Static index | Persistent pipeline with sync and versioning |
| Precision control | Mostly hidden | top_k, score_threshold, rerank_top_n exposed |
| Citations | Chunk ids | Visual citations with page screenshots and bboxes |
| Best fit | Short question answering | Long-horizon document tasks |
Use cases, with examples
The design targets domains where agents navigate large document sets. Legal and fintech are the stated examples.
- Consider a contract question: ‘What notice is needed to terminate the MSA?’ The agent lists files, runs
retrieve, then greps the exact clause. It answers with a citation to the specific page. - Consider due diligence across a data room: An agent can
findFilesby name, thenreadFileeach candidate. It cross-checks clauses without a human opening every PDF. - Consider a versioned policy base: Because
retrieveaccepts afile_versionfilter, an agent can query a specific version. This supports change tracking over time.
Reference implementation
Key Takeaways
legal-kbis a public reference app showing agentic retrieval on LlamaIndex Index v2.- The agent gets four filesystem-style tools:
retrieve(hybrid search),findFiles,readFile, andgrepFile. - A persistent pipeline handles parsing, indexing, sync, and per-file version control.
- Answers include visual citations: page screenshots with bounding boxes over the cited text.
- The stack is TanStack Start, AI SDK 6, Prisma, and WorkOS, with per-user encrypted keys.