feat: 增加知识库
This commit is contained in:
parent
ab27d3f22a
commit
c8c47564da
60
knowledge/llm-to-agent-guide.md
Normal file
60
knowledge/llm-to-agent-guide.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# LLM-to-Agent 框架知识库
|
||||||
|
|
||||||
|
## Agent 工具调用流程
|
||||||
|
|
||||||
|
Agent 的 `run()` 方法执行以下循环:
|
||||||
|
1. 将用户消息加入上下文
|
||||||
|
2. 调用 LLM,传入可用工具列表
|
||||||
|
3. 如果 LLM 返回 `tool_calls`,逐个执行工具,将结果加入上下文,回到步骤 2
|
||||||
|
4. 如果 LLM 直接返回文本,结束循环,返回最终回复
|
||||||
|
|
||||||
|
工具定义需包含 `name`、`description`、`parameters`(JSON Schema)和 `execute` 函数。
|
||||||
|
|
||||||
|
## 子 Agent 管理
|
||||||
|
|
||||||
|
通过 `spawn_agent` 工具可以创建具有独立人设的子 Agent(如销售、顾客)。
|
||||||
|
每个子 Agent 拥有独立的上下文和基础工具集(不含 sub-agent 管理工具,防止无限递归)。
|
||||||
|
通过 `run_conversation` 工具可以在两个子 Agent 之间运行多轮对话。
|
||||||
|
|
||||||
|
## 知识库功能
|
||||||
|
|
||||||
|
知识库位于 `knowledge/` 目录,支持 `.md` 格式文档。
|
||||||
|
文档按 `##` 二级标题自动分块,通过 `search_knowledge` 工具进行关键词检索。
|
||||||
|
检索算法基于关键词命中次数,标题命中加权 ×3。
|
||||||
|
也提供 `list_knowledge` 工具查看知识库全貌。
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
- 启动框架:`pnpm dev`
|
||||||
|
- 指定提示词模式:`pnpm dev orchestrator`
|
||||||
|
- 可用模式:`default`、`toxic`、`json`、`agent`、`reAct`、`orchestrator`
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
index.ts -- CLI 入口
|
||||||
|
llm.ts -- LLM 客户端(OpenAI 兼容)
|
||||||
|
context.ts -- 上下文管理器
|
||||||
|
types/index.ts -- 类型定义
|
||||||
|
agents/
|
||||||
|
agent.ts -- Agent 核心类
|
||||||
|
manager.ts -- 子 Agent 管理器
|
||||||
|
tools.ts -- spawn_agent / run_conversation 工具
|
||||||
|
tools/
|
||||||
|
registry.ts -- 工具注册表
|
||||||
|
weather.ts -- 天气查询工具
|
||||||
|
calculator.ts -- 计算器工具
|
||||||
|
guess.ts -- 猜数字游戏工具
|
||||||
|
knowledge/
|
||||||
|
knowledge-base.ts -- 知识库类
|
||||||
|
search-tool.ts -- 知识库搜索工具
|
||||||
|
hooks/
|
||||||
|
index.ts -- Hook 总线
|
||||||
|
log.hooks.ts -- 日志 Hook
|
||||||
|
registry.ts -- Hook 注册
|
||||||
|
prompts/
|
||||||
|
system.ts -- 系统提示词
|
||||||
|
orchestrator.ts -- Orchestrator 提示词
|
||||||
|
reAct.ts -- ReAct 提示词
|
||||||
|
```
|
||||||
@ -4,8 +4,10 @@ import { PROMPTS } from './prompts/system.js';
|
|||||||
import * as readline from 'node:readline/promises';
|
import * as readline from 'node:readline/promises';
|
||||||
import { hooks } from './hooks/index.js';
|
import { hooks } from './hooks/index.js';
|
||||||
import { registerHooks } from './hooks/registry.js';
|
import { registerHooks } from './hooks/registry.js';
|
||||||
|
import { knowledgeBase } from './knowledge/knowledge-base.js';
|
||||||
|
|
||||||
await registerHooks();
|
await registerHooks();
|
||||||
|
await knowledgeBase.load();
|
||||||
|
|
||||||
const promptName = process.argv[2] || 'orchestrator';
|
const promptName = process.argv[2] || 'orchestrator';
|
||||||
const systemPrompt = PROMPTS[promptName as keyof typeof PROMPTS];
|
const systemPrompt = PROMPTS[promptName as keyof typeof PROMPTS];
|
||||||
|
|||||||
128
src/knowledge/knowledge-base.ts
Normal file
128
src/knowledge/knowledge-base.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
/** 知识库中的一个文档块 */
|
||||||
|
interface Chunk {
|
||||||
|
/** 来源文件名 */
|
||||||
|
source: string;
|
||||||
|
/** 块内文本 */
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轻量知识库:从 knowledge/ 目录加载 .md 文件,按 ## 标题分块,
|
||||||
|
* 提供基于关键词匹配的检索能力。
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* const kb = new KnowledgeBase('./knowledge');
|
||||||
|
* await kb.load();
|
||||||
|
* const results = kb.search('Agent 工具调用');
|
||||||
|
*/
|
||||||
|
export class KnowledgeBase {
|
||||||
|
private chunks: Chunk[] = [];
|
||||||
|
private knowledgeDir: string;
|
||||||
|
|
||||||
|
constructor(knowledgeDir: string) {
|
||||||
|
this.knowledgeDir = knowledgeDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载 knowledgeDir 下所有 .md 文件并分块 */
|
||||||
|
async load(): Promise<void> {
|
||||||
|
this.chunks = [];
|
||||||
|
|
||||||
|
if (!fs.existsSync(this.knowledgeDir)) {
|
||||||
|
console.warn(`知识库目录不存在: ${this.knowledgeDir}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs
|
||||||
|
.readdirSync(this.knowledgeDir)
|
||||||
|
.filter((f) => f.endsWith('.md'));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(this.knowledgeDir, file);
|
||||||
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
const fileChunks = this.splitChunks(raw, file);
|
||||||
|
this.chunks.push(...fileChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`知识库已加载: ${this.chunks.length} 个块,来自 ${files.length} 个文件`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 ## 标题将文档拆分为块 */
|
||||||
|
private splitChunks(raw: string, source: string): Chunk[] {
|
||||||
|
const blocks = raw.split(/(?=^## )/m);
|
||||||
|
return blocks
|
||||||
|
.map((b) => b.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((content) => ({ source, content }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于关键词匹配搜索,返回相关块(按相关性降序)
|
||||||
|
*
|
||||||
|
* 算法:将查询分词,统计每个块命中关键词的次数,
|
||||||
|
* 同时给标题匹配额外加权。
|
||||||
|
*/
|
||||||
|
search(query: string, topK: number = 3): Chunk[] {
|
||||||
|
const keywords = this.tokenize(query);
|
||||||
|
if (keywords.length === 0) return [];
|
||||||
|
|
||||||
|
const scored = this.chunks.map((chunk) => {
|
||||||
|
const lower = chunk.content.toLowerCase();
|
||||||
|
let score = 0;
|
||||||
|
for (const kw of keywords) {
|
||||||
|
// 标题行命中加权 ×3
|
||||||
|
const headlineRegex = /^## .+$/gm;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = headlineRegex.exec(chunk.content)) !== null) {
|
||||||
|
if (match[0].toLowerCase().includes(kw)) {
|
||||||
|
score += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 正文命中
|
||||||
|
const count = (lower.match(new RegExp(this.escapeRegex(kw), 'gi')) || []).length;
|
||||||
|
score += count;
|
||||||
|
}
|
||||||
|
// 标题匹配额外加分
|
||||||
|
return { chunk, score };
|
||||||
|
});
|
||||||
|
|
||||||
|
return scored
|
||||||
|
.filter((s) => s.score > 0)
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, topK)
|
||||||
|
.map((s) => s.chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 中文 + 英文简单分词 */
|
||||||
|
private tokenize(text: string): string[] {
|
||||||
|
// 按空白/标点拆分,过滤长度 ≤1 的词
|
||||||
|
return text
|
||||||
|
.split(/[\s,,。.!!??::;;、]+/)
|
||||||
|
.map((t) => t.toLowerCase().trim())
|
||||||
|
.filter((t) => t.length > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private escapeRegex(s: string): string {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取知识库摘要(供 Agent 概览) */
|
||||||
|
summary(): string {
|
||||||
|
if (this.chunks.length === 0) return '知识库为空';
|
||||||
|
const sources = [...new Set(this.chunks.map((c) => c.source))];
|
||||||
|
const titles = this.chunks
|
||||||
|
.map((c) => {
|
||||||
|
const m = c.content.match(/^## (.+)$/m);
|
||||||
|
return m ? ` - ${m[1]} (${c.source})` : null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
return `知识库包含 ${sources.length} 个文档:\n${titles.join('\n')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 全局单例 */
|
||||||
|
export const knowledgeBase = new KnowledgeBase(
|
||||||
|
path.join(process.cwd(), 'knowledge'),
|
||||||
|
);
|
||||||
53
src/knowledge/search-tool.ts
Normal file
53
src/knowledge/search-tool.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Tool } from '../types/index.js';
|
||||||
|
import { knowledgeBase } from './knowledge-base.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* search_knowledge — 在知识库中检索相关信息
|
||||||
|
*/
|
||||||
|
export const searchKnowledgeTool: Tool = {
|
||||||
|
name: 'search_knowledge',
|
||||||
|
description:
|
||||||
|
'在本地知识库中搜索与查询相关的文档片段。当你需要查找项目文档、技术说明、业务规则等存储在知识库中的信息时使用此工具。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: '搜索关键词或问题,如"Agent 工具调用流程"、"如何创建子Agent"',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
|
},
|
||||||
|
execute: async (args) => {
|
||||||
|
const { query } = args;
|
||||||
|
const results = knowledgeBase.search(query as string, 3);
|
||||||
|
if (results.length === 0) {
|
||||||
|
return `未找到与 "${query}" 相关的知识。当前知识库摘要:\n${knowledgeBase.summary()}`;
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
.map(
|
||||||
|
(r, i) =>
|
||||||
|
`--- 结果 ${i + 1} (来源: ${r.source}) ---\n${r.content}`,
|
||||||
|
)
|
||||||
|
.join('\n\n');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list_knowledge — 列出知识库中所有文档和章节
|
||||||
|
*/
|
||||||
|
export const listKnowledgeTool: Tool = {
|
||||||
|
name: 'list_knowledge',
|
||||||
|
description: '列出知识库中所有文档及其章节标题,用于了解知识库包含哪些内容。',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
execute: async () => {
|
||||||
|
return knowledgeBase.summary();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 知识库相关工具集 */
|
||||||
|
export const knowledgeTools: Tool[] = [searchKnowledgeTool, listKnowledgeTool];
|
||||||
@ -3,8 +3,9 @@ import { calculatorTool } from './calculator.js';
|
|||||||
import { guessTool } from './guess.js';
|
import { guessTool } from './guess.js';
|
||||||
import { Tool } from '../types/index.js';
|
import { Tool } from '../types/index.js';
|
||||||
import { subAgentTools } from '../agents/tools.js';
|
import { subAgentTools } from '../agents/tools.js';
|
||||||
|
import { knowledgeTools } from '../knowledge/search-tool.js';
|
||||||
|
|
||||||
const baseTools: Tool[] = [weatherTool, calculatorTool, guessTool];
|
const baseTools: Tool[] = [weatherTool, calculatorTool, guessTool, ...knowledgeTools];
|
||||||
|
|
||||||
/** 所有工具(基础工具 + sub-agent 管理工具),供主 orchestrator Agent 使用 */
|
/** 所有工具(基础工具 + sub-agent 管理工具),供主 orchestrator Agent 使用 */
|
||||||
const allTools: Tool[] = [...baseTools, ...subAgentTools];
|
const allTools: Tool[] = [...baseTools, ...subAgentTools];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user