我们的 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) { 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');
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[] { const keywords = query.split(/[\s,,。!?:;、]+/) .filter(w => w.length > 1);
const scored = this.chunks.map(chunk => { let score = 0; for (const kw of keywords) { if (chunk 的标题行包含 kw) score += 3; score += (chunk.content 中 kw 出现的次数); } return { chunk, score }; });
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
| 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()}`; } 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
| const listKnowledgeTool: Tool = { name: 'list_knowledge', description: '列出知识库中所有文档及其章节标题。', parameters: { type: 'object', properties: {}, required: [] }, execute: async () => knowledgeBase.summary() };
|
第三步:集成到工具注册表
工具注册表采用与 sub-agent 工具相同的模式——将知识库工具加入 baseTools,这样所有 Agent(包括子 Agent)都能使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { knowledgeTools } from '../knowledge/search-tool.js';
const baseTools: Tool[] = [ weatherTool, calculatorTool, guessTool, ...knowledgeTools ];
const allTools: Tool[] = [...baseTools, ...subAgentTools];
|
最后在入口文件中初始化知识库:
1 2 3 4 5 6
| 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。当然,在拓展一下,也可以双管齐下,静态部分的知识注入上下文,动态部分提供工具被动查询。