重构七天训练营老师

This commit is contained in:
2026-06-10 19:32:03 +08:00
parent 3b7abb9796
commit 826de3d7bb
17 changed files with 3893 additions and 17 deletions

View File

@@ -0,0 +1,204 @@
"""L2 训练营知识库工具 — 按需加载 + 语义搜索
本文件包含知识库查询工具的实现。修改主提示词时请勿改动此文件。
"""
from __future__ import annotations
import re
from pathlib import Path
from typing import List
from common.knowledge_loader import list_knowledge_files, load_knowledge_file
# ───────────────────────────── 配置 ─────────────────────────────
_KB_DIR = Path(__file__).parent / "knowledge_base"
# 知识库元数据(与文件名一一对应)
_KB_METADATA: dict[str, dict] = {
"01_课程层次与问题判断.md": {
"title": "课程层次与问题判断",
"description": "帮助判断学员问题属于299启蒙课还是1280训练营层次以及问题卡在哪个阶段",
"keywords": [
"课程层次", "299", "1280", "启蒙课", "训练营", "问题判断",
"阶段", "层次", "基础", "进阶", "学员水平", "课程区别",
],
},
"02_即时回复表达原则.md": {
"title": "即时回复表达原则",
"description": "辅导老师回复学员时的表达风格、语气、措辞原则和禁忌",
"keywords": [
"表达", "回复", "语气", "措辞", "风格", "原则", "禁忌",
"怎么说", "话术", "沟通", "表达技巧", "回复模板",
],
},
"03_常见学员问题回应素材.md": {
"title": "常见学员问题回应素材",
"description": "学员常见问题的分类、回应策略和具体话术示例",
"keywords": [
"常见问题", "学员问题", "回应", "话术", "示例", "策略",
"情绪", "愤怒", "委屈", "焦虑", "失望", "质疑", "抱怨",
"婚姻", "孩子", "工作", "健康", "家庭", "关系",
],
},
"04_功课与练习反馈边界.md": {
"title": "功课与练习反馈边界",
"description": "学员功课、练习、打卡的反馈原则和边界,什么该说什么不该说",
"keywords": [
"功课", "练习", "打卡", "反馈", "边界", "作业",
"释放法", "万能释放法", "数息法", "身体感知", "情绪觉察",
"练习指导", "功课点评", "训练",
],
},
"05_安全边界与现实风险.md": {
"title": "安全边界与现实风险",
"description": "学员安全风险识别、危机干预边界、何时必须上报",
"keywords": [
"安全", "风险", "危机", "自杀", "自残", "伤害", "暴力",
"报警", "上报", "边界", "现实风险", "法律保护", "隐私",
"紧急情况", "危险", "保护",
],
},
"06_1280核心术语词典.md": {
"title": "1280核心术语词典",
"description": "1280训练营核心术语的标准解释和使用场景",
"keywords": [
"术语", "词典", "概念", "定义", "解释",
"看见自己", "回到自己", "万能释放法", "情绪心钥", "抗拒心钥",
"原生数据", "潜意识数据", "数息法", "身体感知", "情绪觉察",
"内在模式", "释放", "觉察", "感知",
],
},
"07_大本营承接与成交话术.md": {
"title": "大本营承接与成交话术",
"description": "大本营课程承接判断、成交话术、异议处理策略",
"keywords": [
"大本营", "承接", "成交", "异议", "销售", "推课",
"后续课程", "持续训练", "课程信心", "报名", "价格", "权益",
"没时间", "没钱", "怕没效果", "家人不同意", "不想继续",
],
},
}
# ───────────────────────────── 工具函数 ─────────────────────────────
def _tokenize(text: str) -> set[str]:
"""中文分词使用jieba"""
try:
import jieba
return set(jieba.cut(text))
except ImportError:
# 降级:按字拆分
return set(text)
def _calc_relevance(query: str, content: str) -> float:
"""计算查询与内容的相关度0-1"""
query_tokens = _tokenize(query.lower())
content_tokens = _tokenize(content.lower())
if not query_tokens:
return 0.0
overlap = query_tokens & content_tokens
return len(overlap) / len(query_tokens)
def _extract_relevant_sections(content: str, query: str, max_sections: int = 3) -> list[str]:
"""从知识库内容中提取与查询最相关的章节"""
# 按二级标题分割
sections = re.split(r"\n(?=##\s+)", content)
scored = []
for section in sections:
if not section.strip():
continue
score = _calc_relevance(query, section)
scored.append((score, section))
scored.sort(key=lambda x: x[0], reverse=True)
return [s for _, s in scored[:max_sections]]
# ───────────────────────────── 公开工具 ─────────────────────────────
def list_kb() -> str:
"""列出所有可用的知识库文件
当你不确定该查哪个知识库文件时,先调用此工具查看列表。
"""
lines = ["# 可用知识库文件\n"]
for fname, meta in _KB_METADATA.items():
lines.append(f"## {meta['title']}")
lines.append(f"- 文件名: `{fname}`")
lines.append(f"- 描述: {meta['description']}")
lines.append(f"- 关键词: {', '.join(meta['keywords'][:5])}...\n")
return "\n".join(lines)
def load_kb(filename: str) -> str:
"""加载指定知识库文件的完整内容
Args:
filename: 知识库文件名(如 "01_课程层次与问题判断.md"
当你已确定需要哪个文件的内容时,调用此工具加载完整内容。
"""
if filename not in _KB_METADATA:
available = ", ".join(_KB_METADATA.keys())
return f"错误: 文件 '{filename}' 不存在。可用文件: {available}"
content = load_knowledge_file(_KB_DIR, filename)
if not content:
return f"错误: 无法读取文件 '{filename}'"
return content
def search_kb(query: str, top_k: int = 3) -> str:
"""根据关键词搜索所有知识库文件,返回最相关的片段
Args:
query: 搜索关键词(中文)
top_k: 返回结果数量默认3条
当你不确定具体文件,但知道要查什么内容时,调用此工具。
"""
all_files = list_knowledge_files(_KB_DIR)
results: list[tuple[float, str, str]] = []
for fname in all_files:
content = load_knowledge_file(_KB_DIR, fname)
if not content:
continue
# 计算文件级相关度
file_score = _calc_relevance(query, content)
# 提取最相关的章节
sections = _extract_relevant_sections(content, query, max_sections=2)
for section in sections:
section_score = _calc_relevance(query, section)
# 综合得分:文件级 + 章节级
final_score = (file_score + section_score) / 2
results.append((final_score, fname, section))
# 排序并取 top_k
results.sort(key=lambda x: x[0], reverse=True)
top_results = results[:top_k]
if not top_results:
return f"未找到与 '{query}' 相关的知识库内容。"
lines = [f"# 知识库搜索结果: '{query}'\n"]
for i, (score, fname, section) in enumerate(top_results, 1):
meta = _KB_METADATA.get(fname, {})
title = meta.get("title", fname)
lines.append(f"## 结果 {i}(相关度: {score:.2f}")
lines.append(f"**来源**: {title} (`{fname}`)")
lines.append(f"**内容**:\n{section[:800]}...\n")
return "\n".join(lines)