llm-to-agent/legacy/old/src/knowledge/knowledge-base.ts
2026-07-24 17:37:00 +08:00

129 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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'),
);