【AI基础专题】知识库

我们的 Agent 框架已经具备了工具调用和多 Agent 编排能力,但它有一个短板:Agent 的能力完全确定于大模型本身。如果你想让大模型和你的实际场景更契合,比如让他回答你关于项目内部文档、业务规则、API 变更记录等问题,Agent 完全无法胜任。

所以本小节引入了RAG(Retrieval-Augmented Generation)的概念,不把所有知识塞进 prompt,而是让 Agent 在需要时自己去查。

整个流程的核心思想很简单:

1
用户提问 → Agent 判断需查资料 → search_knowledge(查询) → 文档匹配 → 注入上下文 → 生成回答

下面我们一步步实现。


第一步:KnowledgeBase 类 —— 文档加载与检索

首先需要一个能加载文档、分块、检索的类。为了保持简单,我们采用 markdown 文件 + 关键词匹配 的轻量方案。

文档分块策略

知识库放在项目根目录的 knowledge/ 下,每个 .md 文件就是一个知识来源。加载时按 ## 二级标题自动分块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 伪代码:文档加载与分块
class KnowledgeBase {
chunks: Chunk[] = [];

async load(dir: string) {
// 1. 扫描 knowledge/ 下所有 .md 文件
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));

for (const file of files) {
const raw = fs.readFileSync(path.join(dir, file), 'utf-8');

// 2. 按 "## " 标题拆分为独立块
const blocks = raw.split(/(?=^## )/m);
for (const block of blocks) {
this.chunks.push({
source: file, // 来源文件名
content: block.trim() // 块内容
});
}
}
}
}

markdown 的二级标题天然代表了”一个独立的知识点”——每个章节通常讨论一个主题,语义完整,不会在句子中间截断。

关键词检索

检索算法直接让DeepSeek自己生成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 伪代码:关键词搜索
search(query: string, topK: number): Chunk[] {
// 1. 分词:按标点/空白拆分,过滤掉单字
const keywords = query.split(/[\s,,。!?:;、]+/)
.filter(w => w.length > 1);

// 2. 为每个文档块打分
const scored = this.chunks.map(chunk => {
let score = 0;
for (const kw of keywords) {
// 标题行命中 → 加权 ×3
if (chunk 的标题行包含 kw) score += 3;
// 正文命中 → 每次出现 +1
score += (chunk.content 中 kw 出现的次数);
}
return { chunk, score };
});

// 3. 按分数降序,取前 topK 个
return scored
.filter(s => s.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map(s => s.chunk);
}

这个算法的优点是简单透明,对于demo来说已经够用。


第二步:把知识库暴露为 Agent 工具

有了 KnowledgeBase 类,下一步是把它封装成 Agent 可以调用的工具。沿用 Tool 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 伪代码:search_knowledge 工具
const searchKnowledgeTool: Tool = {
name: 'search_knowledge',
description: '在本地知识库中搜索与查询相关的文档片段。' +
'当你需要查找项目文档、技术说明、业务规则时使用。',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索关键词或问题' }
},
required: ['query']
},
execute: async (args) => {
const results = knowledgeBase.search(args.query, 3);
if (results.length === 0) {
return `未找到相关知识。当前知识库概览:\n${knowledgeBase.summary()}`;
}
// 将结果格式化为 LLM 易读的文本
return results.map((r, i) =>
`--- 结果 ${i+1} (来源: ${r.source}) ---\n${r.content}`
).join('\n\n');
}
};

同时提供 list_knowledge 工具,让 Agent 可以”先看看知识库里有什么,再决定搜什么”:

1
2
3
4
5
6
7
8
9
10
11
12
// 伪代码:list_knowledge 工具
const listKnowledgeTool: Tool = {
name: 'list_knowledge',
description: '列出知识库中所有文档及其章节标题。',
parameters: { type: 'object', properties: {}, required: [] },
execute: async () => knowledgeBase.summary()
// summary() 输出类似:
// "知识库包含 1 个文档:
// - Agent 工具调用流程 (llm-to-agent-guide.md)
// - 子 Agent 管理 (llm-to-agent-guide.md)
// - 知识库功能 (llm-to-agent-guide.md)"
};

第三步:集成到工具注册表

工具注册表采用与 sub-agent 工具相同的模式——将知识库工具加入 baseTools,这样所有 Agent(包括子 Agent)都能使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 伪代码:工具注册 (src/tools/registry.ts)
import { knowledgeTools } from '../knowledge/search-tool.js';

// 基础工具 — 所有 Agent 都能用
const baseTools: Tool[] = [
weatherTool,
calculatorTool,
guessTool,
...knowledgeTools // ← 新增
];

// 完整工具 — 只有主 Orchestrator 能用
const allTools: Tool[] = [...baseTools, ...subAgentTools];

最后在入口文件中初始化知识库:

1
2
3
4
5
6
// 伪代码:入口初始化 (src/index.ts)
import { knowledgeBase } from './knowledge/knowledge-base.js';

await registerHooks();
await knowledgeBase.load(); // ← 启动时加载知识库
console.log(knowledgeBase.summary());

到这里,知识库功能就完整集成了。整个流程在 Agent 的 run() 方法中自然串联:

1
2
3
4
5
6
用户: "告诉我这个项目的 Agent 循环怎么工作"
→ LLM 判断: 需要查资料
→ LLM 生成 tool_call: search_knowledge("Agent 工具调用流程")
→ Agent 执行工具 → 检索 knowledge/ 目录
→ 工具返回匹配的文档块,注入上下文
→ LLM 基于文档块生成准确回答

复盘

当然,这是最简单的实现,在需要的时候通过tools去查询。还有一种做法是将知识提前注入到System prompt或者user message。当然,在拓展一下,也可以双管齐下,静态部分的知识注入上下文,动态部分提供工具被动查询。