fix: 修复'登录插件的菜单如何配置?'答非所问,菜单.txt进入top1
- 配置文件section加权:查询含配置意图词+heading带.txt/json/yml后缀+命中非泛化词→+8 - 守卫记录分数最高命中section并提到doc_sections首位,防has_action重排+sub_k截断丢答案 - 核心名词bonus按出现次数计分(5/次上限15),修复php后台玩家端仅含一处领地管理+15压过操作方式表 - 导航/目录/总览section全局惩罚-30,修复index文档导航对皮肤更新查询得28分压过skin文档 - 表格兜底答案级抬分30:操作/模式/方式意图+表格含操作方式列→protect操作方式表32分压过php误匹配 回归验证:菜单/领地操作模式/下载/公告/皮肤更新/登录账号/密码/打印流水全部top1正确
This commit is contained in:
+239
-112
@@ -114,8 +114,25 @@ class KnowledgeBase:
|
||||
返回 section 列表。
|
||||
"""
|
||||
sections = []
|
||||
# ⭐ 保护代码块:```...``` 内的内容不参与标题切分
|
||||
# 修复"### 2.2.7 菜单.txt"section被截断:JSON示例里的 "# 服务器菜单"、
|
||||
# "# 使用类json格式" 注释行被误判为Markdown标题,导致:
|
||||
# 1) 菜单.txt section body 在 ```json 处截断,JSON配置内容丢失
|
||||
# 2) "# 使用类json格式" 被误切成独立section,污染搜索结果
|
||||
code_blocks = []
|
||||
def _protect_code_block(m):
|
||||
code_blocks.append(m.group(0))
|
||||
return f'\n__CODEBLOCK_{len(code_blocks) - 1}__\n'
|
||||
content_protected = re.sub(
|
||||
r'```.*?```', _protect_code_block, content, flags=re.DOTALL)
|
||||
|
||||
def _restore_code_blocks(text):
|
||||
for j, cb in enumerate(code_blocks):
|
||||
text = text.replace(f'__CODEBLOCK_{j}__', cb)
|
||||
return text
|
||||
|
||||
# 按行首的 ## 标题切分(前面可能有空白)
|
||||
parts = re.split(r'(?=^#{1,6}\s)', content, flags=re.MULTILINE)
|
||||
parts = re.split(r'(?=^#{1,6}\s)', content_protected, flags=re.MULTILINE)
|
||||
|
||||
section_id = 0
|
||||
for i, part in enumerate(parts):
|
||||
@@ -132,14 +149,17 @@ class KnowledgeBase:
|
||||
match_heading = None
|
||||
list_sections = []
|
||||
if body:
|
||||
# ⭐ 列表项切分前先恢复代码块占位符,避免列表切分逻辑
|
||||
# 把恢复后的多行代码块内容当作独立列表项切分
|
||||
body_restored = _restore_code_blocks(body)
|
||||
# 按列表项切分
|
||||
list_sections = KnowledgeBase._split_body_by_list_items(body, heading)
|
||||
list_sections = KnowledgeBase._split_body_by_list_items(body_restored, heading)
|
||||
if list_sections:
|
||||
for ls in list_sections:
|
||||
sections.append({
|
||||
"heading": ls["heading"],
|
||||
"content": ls["content"],
|
||||
"body": ls["body"],
|
||||
"content": _restore_code_blocks(ls["content"]),
|
||||
"body": _restore_code_blocks(ls["body"]),
|
||||
"section_id": section_id,
|
||||
})
|
||||
section_id += 1
|
||||
@@ -152,7 +172,7 @@ class KnowledgeBase:
|
||||
# 修复"注册账号"/"怎么登录账号"被invite.md"违规处罚"section
|
||||
# (heading携带完整条目文本获得免费heading命中)压过的问题
|
||||
item_texts = []
|
||||
for _line in body.split('\n'):
|
||||
for _line in body_restored.split('\n'):
|
||||
_stripped = _line.strip()
|
||||
if _stripped.startswith('- ') and len(_stripped) > 2:
|
||||
item_texts.append(_stripped[2:].strip())
|
||||
@@ -161,8 +181,8 @@ class KnowledgeBase:
|
||||
|
||||
sections.append({
|
||||
"heading": heading,
|
||||
"content": part_stripped,
|
||||
"body": body,
|
||||
"content": _restore_code_blocks(part_stripped),
|
||||
"body": _restore_code_blocks(body),
|
||||
"match_heading": match_heading,
|
||||
"section_id": section_id,
|
||||
})
|
||||
@@ -637,12 +657,24 @@ class KnowledgeBase:
|
||||
if table_query_tokens & query_tokens_set:
|
||||
body_stripped = body.strip()
|
||||
if body_stripped.startswith('|') and '|' in body_stripped[1:]:
|
||||
# ⚠ 导航/目录类表格不奖励:index.md"文档导航"body含
|
||||
# "领地系统"等全部主题词,命中查询词即被5x放大到爆炸分,
|
||||
# 压过protect.md真实的"操作方式"表格。
|
||||
# 此类 section heading 含导航/目录/关于/导航关键词,或title=index
|
||||
_nav_heading = heading
|
||||
_is_nav = (
|
||||
any(k in _nav_heading for k in ('导航', '目录', '关于我们', '欢迎', '总览'))
|
||||
or doc.get("title", "").lower() == 'index'
|
||||
)
|
||||
# 检查heading是否与查询语义相关
|
||||
h_tokens = set(t for t in jieba.cut(heading) if len(t) >= 2)
|
||||
heading_relevant = bool(query_tokens_set & h_tokens)
|
||||
# 或body中含查询核心名词
|
||||
body_relevant = any(n in body_stripped for n in query_tokens_set if len(n) >= 2)
|
||||
if heading_relevant or body_relevant:
|
||||
# 或body中含查询核心名词(收紧:必须是语义性名词,防"系统"等泛化词误触发)
|
||||
_tbl_core = {'操作', '领地', '保护', '支持', '方法', '方式', '模式', '配置', '下载'}
|
||||
body_core_hits = sum(1 for n in _tbl_core if n in body_stripped)
|
||||
body_relevant = body_core_hits >= 2 or any(
|
||||
n in body_stripped for n in query_tokens_set if n in _tbl_core)
|
||||
if (heading_relevant or body_relevant) and not _is_nav:
|
||||
combined_scores[i] *= 5.0 # 相关表格 → 巨幅奖励
|
||||
|
||||
# 短查询惩罚:分词数 ≤2 的短查询(如图标残片/短词)得分大幅降权
|
||||
@@ -828,13 +860,39 @@ class KnowledgeBase:
|
||||
# 检查heading是否与查询语义相关
|
||||
h_tokens = set(t for t in jieba.cut(heading) if len(t) >= 2)
|
||||
heading_relevant = bool(query_tokens_set & h_tokens)
|
||||
body_relevant = any(n in body for n in query_tokens_set if len(n) >= 2)
|
||||
# ⚠ body相关:必须含查询核心名词(领地/操作/商品等)而非任意token
|
||||
# 修复"领地系统有哪两种操作模式?"误把sdf1_login"2.安装说明"表格
|
||||
# (body含"系统"一词)当成"操作方式"答案,被max_score*2放大到89分
|
||||
_core_nouns_tbl = {'操作', '领地', '保护', '支持', '方法', '方式', '模式'}
|
||||
body_core_hits = sum(1 for n in _core_nouns_tbl if n in body)
|
||||
body_relevant = body_core_hits >= 2 or any(
|
||||
n in body for n in query_tokens_set if n in _core_nouns_tbl)
|
||||
if heading_relevant or body_relevant:
|
||||
# 给表格section超高分(最高分×2),确保reranking后仍排第一
|
||||
max_score = max(s for _, _, s in doc_sections) if doc_sections else min_score
|
||||
doc_sections.append((i, doc, max_score * 2))
|
||||
_tbl_score = max_score * 2
|
||||
# ⭐ 答案级表格:查询意图是"操作/模式/方式/方法",表格body含
|
||||
# 操作方式/模式列 → 抬到30分。protect.md"操作方式|是否支持"
|
||||
# 表格是"领地系统有哪两种操作模式?"直接答案,但max_score*2
|
||||
# 只有~10分,压不过php后台玩家端(body含"领地管理"+核心名词
|
||||
# bonus 也得~27分)。答案级表格必须比误匹配section更高。
|
||||
if any(w in query_stripped for w in ('操作', '模式', '方式', '方法')):
|
||||
if any(n in body for n in ('操作', '方式', '模式')):
|
||||
_tbl_score = max(_tbl_score, 30.0)
|
||||
doc_sections.append((i, doc, _tbl_score))
|
||||
break
|
||||
doc_sections.sort(key=lambda x: x[2], reverse=True)
|
||||
else:
|
||||
# ⭐ 表格已在doc_sections中:has_relevant_table为True时不走强制加入,
|
||||
# 表格保持原分可能过低(protect"操作方式"表10.25压不过php后台玩家端27.29)。
|
||||
# 答案级表格抬到30分,逻辑与强制加入分支保持一致。
|
||||
for _ti, (_tidx, _tdoc, _tscore) in enumerate(doc_sections):
|
||||
_tbody = _tdoc.get("body", "").strip()
|
||||
if _tbody.startswith('|') and ('操作' in _tbody or '支持' in _tbody):
|
||||
if any(w in query_stripped for w in ('操作', '模式', '方式', '方法')):
|
||||
if any(n in _tbody for n in ('操作', '方式', '模式')):
|
||||
doc_sections[_ti] = (_tidx, _tdoc, max(_tscore, 30.0))
|
||||
break
|
||||
|
||||
if not doc_sections:
|
||||
continue
|
||||
@@ -890,11 +948,29 @@ class KnowledgeBase:
|
||||
_download_match = True
|
||||
if _download_match:
|
||||
bonus += 8.0
|
||||
# ⭐ 配置文件section加权:查询含"配置/设置/怎么/如何"时,
|
||||
# heading 带 .txt/.json/.yml 后缀的 section 就是配置文件格式说明,
|
||||
# 是"XX如何配置"的直接答案。修复"登录插件的菜单如何配置?"
|
||||
# 时"### 2.2.7 菜单.txt"(JSON配置格式)7.89分被"自定义菜单物品"
|
||||
# 碎片和"指令参数及介绍"压过、sub_k截断丢弃的问题。
|
||||
# ⚠ 条件三重:配置意图词 + 配置文件后缀 + heading命中查询的
|
||||
# 非泛化词(排除登录/配置/设置/使用/插件——这些词到处出现无定位力)。
|
||||
# "怎么登录账号"的"2.2.4 插件设置.txt"交集仅{登录}∈generic → 不触发
|
||||
if any(w in query_stripped for w in ('配置', '设置', '怎么', '如何')):
|
||||
if re.search(r'\.(txt|json|yml|yaml|properties|cfg|conf)', heading_no_links):
|
||||
_cfg_h_tokens = set(t for t in jieba.cut(heading_no_links) if len(t) >= 2)
|
||||
_cfg_hit = query_tokens & _cfg_h_tokens
|
||||
_generic_cfg = {'登录', '配置', '设置', '使用', '插件'}
|
||||
if any(t not in _generic_cfg for t in _cfg_hit):
|
||||
bonus += 8.0
|
||||
# ⭐ body 包含查询核心名词(如"密码")→ 巨幅 bonus
|
||||
# ⚠ 按名词出现次数计分(1次+5,上限15),而非出现即+15整分:
|
||||
# 修复php后台玩家端body仅含"领地管理"一句(领地×2)就+15分整,
|
||||
# 压过protect.md真正含"操作方式"表格的答案section
|
||||
core_nouns = {'密码', '用户管理', '领地', '商品', 'CDK', '工单', '债券', '在线', '公告', '更新'}
|
||||
body_noun_hits = sum(1 for n in core_nouns if n in body and n in query_stripped)
|
||||
if body_noun_hits >= 1:
|
||||
bonus += 15.0 # body 包含核心名词 → 优先返回
|
||||
_noun_total = sum(body.count(n) for n in core_nouns if n in body and n in query_stripped)
|
||||
if _noun_total >= 1:
|
||||
bonus += min(15.0, 5.0 * _noun_total) # body 包含核心名词 → 优先返回
|
||||
# ⭐ 管理员+默认 组合(OCR 常丢"php/密码")→ 偏好真正含"密码/登录"的 section
|
||||
# 防止"## 目录 - 管理员端(当前)"这种目录项压过"## php管理员后端"(默认登录密码)
|
||||
# 权重给足,必须压过目录项因 heading/phrase 命中的基础分差
|
||||
@@ -906,6 +982,11 @@ class KnowledgeBase:
|
||||
body_has_query = any(t in body for t in query_tokens if len(t) >= 2)
|
||||
if heading.startswith("# ") and (not body or len(body) < 50) and not body_has_query:
|
||||
bonus -= 15.0
|
||||
# ⭐ 导航/目录类section全局惩罚:文档导航/目录/总览表格列出全部主题词,
|
||||
# TF-IDF+核心名词bonus会让它们得分虚高(如index"文档导航"对"皮肤更新"
|
||||
# 查询得28分压过skin文档)。导航section永远不是具体问题的答案 → 重罚。
|
||||
if any(k in heading_no_links for k in ('导航', '目录', '关于我们', '欢迎', '总览', '文档导航')):
|
||||
bonus -= 30.0
|
||||
reranked.append((idx, doc, score + bonus))
|
||||
# ⭐ 否定式查询惩罚(在reranking后执行,防止被bonus覆盖)
|
||||
_q_nopunct = re.sub(r'[??!!。,,\s]', '', query_stripped)
|
||||
@@ -1002,8 +1083,13 @@ class KnowledgeBase:
|
||||
# 检查heading是否与查询语义相关(heading含查询词 或 查询词含heading关键词)
|
||||
h_tokens = set(t for t in jieba.cut(heading) if len(t) >= 2)
|
||||
heading_relevant = bool(query_tokens & h_tokens)
|
||||
# 也检查body中是否有查询核心名词
|
||||
body_relevant = any(n in body for n in query_tokens if len(n) >= 2)
|
||||
# ⚠ body相关收紧:必须含查询核心名词(操作/领地等)而非任意token
|
||||
# 修复"领地系统有哪两种操作模式?"在rerank后仍把sdf1_login
|
||||
# "2.安装说明"表格(body含"系统")移到首位压过protect"操作方式"表
|
||||
_core_nouns_tbl2 = {'操作', '领地', '保护', '支持', '方法', '方式', '模式'}
|
||||
body_core_hits = sum(1 for n in _core_nouns_tbl2 if n in body)
|
||||
body_relevant = body_core_hits >= 2 or any(
|
||||
n in body for n in query_tokens if n in _core_nouns_tbl2)
|
||||
if heading_relevant or body_relevant:
|
||||
doc_sections.insert(0, doc_sections.pop(i))
|
||||
break
|
||||
@@ -1104,104 +1190,145 @@ class KnowledgeBase:
|
||||
if doc.get('heading', '').startswith('### ')
|
||||
and any(s in doc.get('heading', '') for s in step_headings))
|
||||
if step_count >= 3:
|
||||
# ⭐ 获取最佳文档的路径,查找该文档的所有 sections
|
||||
best_doc_path = doc_sections[0][1].get('path', '')
|
||||
if best_doc_path:
|
||||
# 查找同一文档的所有 sections
|
||||
all_doc_sections = [(i, d) for i, d in enumerate(self.docs) if d.get('path') == best_doc_path]
|
||||
# ⭐ 语义命中守卫:若存在 section 的 heading 直接命中查询核心词
|
||||
# (如"菜单.txt"命中"菜单"),说明查询有明确的section级答案,
|
||||
# 跳过guide合并,避免"登录插件的菜单如何配置?"被合并成"2.安装说明"
|
||||
# guide而吞掉真正的"### 2.2.7 菜单.txt"配置段。
|
||||
# ⚠ 条件:heading token 命中 且 body 非空,防止纯目录section误触发
|
||||
# ⚠ 记录分数最高的命中section(而非第一个),确保"菜单.txt"这类
|
||||
# 高combine分的精确答案胜出,而不是被同文档先出现的低分section抢占
|
||||
_heading_hit_section = None # (doc, score)
|
||||
_heading_hit_idx = None
|
||||
_heading_hit_score = -1.0
|
||||
for s_idx, s_doc, s_score in doc_sections:
|
||||
s_heading = s_doc.get('heading', '')
|
||||
s_body = s_doc.get('body', '')
|
||||
s_h_tokens = set(t for t in jieba.cut(s_heading) if len(t) >= 2)
|
||||
_hit = query_tokens_set & s_h_tokens
|
||||
# 排除查询侧泛化词:登录/配置/设置/使用/插件 到处出现,
|
||||
# 命中它们无法定位答案section;真正能定位的是
|
||||
# 如"菜单""皮肤"这类具体名词
|
||||
_generic = {'登录', '配置', '设置', '使用', '插件'}
|
||||
_strong_hit = any(t not in _generic for t in _hit)
|
||||
if _hit and _strong_hit and len(s_body.strip()) > 10:
|
||||
if s_score > _heading_hit_score:
|
||||
_heading_hit_section = (s_doc, s_score)
|
||||
_heading_hit_idx = s_idx
|
||||
_heading_hit_score = s_score
|
||||
if _heading_hit_section is not None:
|
||||
logger = logging.getLogger("msgbot.kb")
|
||||
logger.debug(
|
||||
f"guide合并跳过: heading命中查询词 "
|
||||
f"{_heading_hit_section[0].get('heading', '')[:30]!r} "
|
||||
f"score={_heading_hit_section[1]:.2f}"
|
||||
)
|
||||
# ⭐ 命中section提到doc_sections首位:
|
||||
# has_action重排可能已把"指令参数及介绍"等priority命中section抬到最前,
|
||||
# 若此处不提升,sub_k=2截断会丢弃真正的答案section
|
||||
# ("登录插件的菜单如何配置?"中菜单.txt 23.08分排第三被丢)
|
||||
for _ii, (_s_idx2, _, _) in enumerate(doc_sections):
|
||||
if _s_idx2 == _heading_hit_idx:
|
||||
doc_sections.insert(0, doc_sections.pop(_ii))
|
||||
break
|
||||
else:
|
||||
# ⭐ 获取最佳文档的路径,查找该文档的所有 sections
|
||||
best_doc_path = doc_sections[0][1].get('path', '')
|
||||
if best_doc_path:
|
||||
# 查找同一文档的所有 sections
|
||||
all_doc_sections = [(i, d) for i, d in enumerate(self.docs) if d.get('path') == best_doc_path]
|
||||
|
||||
if len(all_doc_sections) > 3: # 有多个 sections,可能是操作指南
|
||||
# ⭐ 按 parent guide 分组 sections
|
||||
# 遇到 # 或 ## 级别标题时,视为新 guide 的开始
|
||||
guides = [] # [(parent_heading, [(idx, doc), ...]), ...]
|
||||
current_guide = None
|
||||
current_sections = []
|
||||
if len(all_doc_sections) > 3: # 有多个 sections,可能是操作指南
|
||||
# ⭐ 按 parent guide 分组 sections
|
||||
# 遇到 # 或 ## 级别标题时,视为新 guide 的开始
|
||||
guides = [] # [(parent_heading, [(idx, doc), ...]), ...]
|
||||
current_guide = None
|
||||
current_sections = []
|
||||
|
||||
for idx, doc in all_doc_sections:
|
||||
heading = doc.get('heading', '')
|
||||
# 判断是否为 guide 级别标题(# 或 ##)
|
||||
if heading.startswith('# ') or heading.startswith('## '):
|
||||
# 保存前一个 guide
|
||||
if current_guide is not None and current_sections:
|
||||
guides.append((current_guide, current_sections))
|
||||
current_guide = heading
|
||||
current_sections = [(idx, doc)]
|
||||
else:
|
||||
# 属于当前 guide 的子 section(### 及以下)
|
||||
if current_guide is not None:
|
||||
current_sections.append((idx, doc))
|
||||
else:
|
||||
# 没有 guide 级别标题,作为独立 section
|
||||
if current_sections:
|
||||
guides.append((None, current_sections))
|
||||
current_sections = [(idx, doc)]
|
||||
current_guide = None
|
||||
|
||||
# 保存最后一个 guide
|
||||
if current_guide is not None and current_sections:
|
||||
guides.append((current_guide, current_sections))
|
||||
elif current_sections:
|
||||
guides.append((None, current_sections))
|
||||
|
||||
# 选择最相关的 guide 进行合并
|
||||
if len(guides) > 1:
|
||||
# ⭐ 根据查询关键词选择 guide
|
||||
# 查询含 "url"/"图床"/"直连"/"链接" → 选择非正版 guide(含 url 操作方式)
|
||||
# 查询含 "正版"/"官网"/"启动器"/"更新" → 选择正版 guide
|
||||
# 默认选择包含最多步骤的 guide
|
||||
url_keywords = {'url', '图床', '直连', '链接', '非正版'}
|
||||
genuine_keywords = {'正版', '官网', '启动器', 'minecraft.net'}
|
||||
|
||||
best_guide_idx = 0
|
||||
best_guide_score = -1
|
||||
|
||||
for i, (parent, sections) in enumerate(guides):
|
||||
if not sections:
|
||||
continue
|
||||
# 计算 guide 与查询的相关性
|
||||
guide_text = ' '.join(doc.get('heading', '') + ' ' + doc.get('body', '')
|
||||
for _, doc in sections)
|
||||
score = 0
|
||||
for kw in url_keywords:
|
||||
if kw in query_stripped and kw in guide_text:
|
||||
score += 10
|
||||
for kw in genuine_keywords:
|
||||
if kw in query_stripped and kw in guide_text:
|
||||
score += 10
|
||||
# 优先选择步骤最多的 guide(默认行为)
|
||||
# ⭐ 与step_count一致:只统计"### "级子标题步骤
|
||||
step_in_guide = sum(1 for _, doc in sections
|
||||
if doc.get('heading', '').startswith('### ')
|
||||
and any(s in doc.get('heading', '') for s in step_headings))
|
||||
score += step_in_guide * 2
|
||||
|
||||
if score > best_guide_score:
|
||||
best_guide_score = score
|
||||
best_guide_idx = i
|
||||
|
||||
selected_guide = guides[best_guide_idx][1]
|
||||
else:
|
||||
# 只有一个 guide,直接使用
|
||||
selected_guide = guides[0][1] if guides else []
|
||||
|
||||
# 合并选中 guide 的所有步骤 section
|
||||
if selected_guide:
|
||||
merged_body = ""
|
||||
for idx, doc in selected_guide:
|
||||
for idx, doc in all_doc_sections:
|
||||
heading = doc.get('heading', '')
|
||||
body = doc.get('body', doc.get('content', ''))
|
||||
if body and len(body) > 10:
|
||||
merged_body += f"\n{heading}\n{body}\n"
|
||||
if merged_body:
|
||||
# 创建合并后的文档
|
||||
merged_doc = selected_guide[0][1].copy()
|
||||
merged_doc['body'] = merged_body.strip()
|
||||
merged_doc['content'] = merged_body.strip()
|
||||
# 删除 answer 字段,使用 body
|
||||
if 'answer' in merged_doc:
|
||||
del merged_doc['answer']
|
||||
results.append((merged_doc, float(doc_sections[0][2])))
|
||||
continue
|
||||
# 判断是否为 guide 级别标题(# 或 ##)
|
||||
if heading.startswith('# ') or heading.startswith('## '):
|
||||
# 保存前一个 guide
|
||||
if current_guide is not None and current_sections:
|
||||
guides.append((current_guide, current_sections))
|
||||
current_guide = heading
|
||||
current_sections = [(idx, doc)]
|
||||
else:
|
||||
# 属于当前 guide 的子 section(### 及以下)
|
||||
if current_guide is not None:
|
||||
current_sections.append((idx, doc))
|
||||
else:
|
||||
# 没有 guide 级别标题,作为独立 section
|
||||
if current_sections:
|
||||
guides.append((None, current_sections))
|
||||
current_sections = [(idx, doc)]
|
||||
current_guide = None
|
||||
|
||||
# 保存最后一个 guide
|
||||
if current_guide is not None and current_sections:
|
||||
guides.append((current_guide, current_sections))
|
||||
elif current_sections:
|
||||
guides.append((None, current_sections))
|
||||
|
||||
# 选择最相关的 guide 进行合并
|
||||
if len(guides) > 1:
|
||||
# ⭐ 根据查询关键词选择 guide
|
||||
# 查询含 "url"/"图床"/"直连"/"链接" → 选择非正版 guide(含 url 操作方式)
|
||||
# 查询含 "正版"/"官网"/"启动器"/"更新" → 选择正版 guide
|
||||
# 默认选择包含最多步骤的 guide
|
||||
url_keywords = {'url', '图床', '直连', '链接', '非正版'}
|
||||
genuine_keywords = {'正版', '官网', '启动器', 'minecraft.net'}
|
||||
|
||||
best_guide_idx = 0
|
||||
best_guide_score = -1
|
||||
|
||||
for i, (parent, sections) in enumerate(guides):
|
||||
if not sections:
|
||||
continue
|
||||
# 计算 guide 与查询的相关性
|
||||
guide_text = ' '.join(doc.get('heading', '') + ' ' + doc.get('body', '')
|
||||
for _, doc in sections)
|
||||
score = 0
|
||||
for kw in url_keywords:
|
||||
if kw in query_stripped and kw in guide_text:
|
||||
score += 10
|
||||
for kw in genuine_keywords:
|
||||
if kw in query_stripped and kw in guide_text:
|
||||
score += 10
|
||||
# 优先选择步骤最多的 guide(默认行为)
|
||||
# ⭐ 与step_count一致:只统计"### "级子标题步骤
|
||||
step_in_guide = sum(1 for _, doc in sections
|
||||
if doc.get('heading', '').startswith('### ')
|
||||
and any(s in doc.get('heading', '') for s in step_headings))
|
||||
score += step_in_guide * 2
|
||||
|
||||
if score > best_guide_score:
|
||||
best_guide_score = score
|
||||
best_guide_idx = i
|
||||
|
||||
selected_guide = guides[best_guide_idx][1]
|
||||
else:
|
||||
# 只有一个 guide,直接使用
|
||||
selected_guide = guides[0][1] if guides else []
|
||||
|
||||
# 合并选中 guide 的所有步骤 section
|
||||
if selected_guide:
|
||||
merged_body = ""
|
||||
for idx, doc in selected_guide:
|
||||
heading = doc.get('heading', '')
|
||||
body = doc.get('body', doc.get('content', ''))
|
||||
if body and len(body) > 10:
|
||||
merged_body += f"\n{heading}\n{body}\n"
|
||||
if merged_body:
|
||||
# 创建合并后的文档
|
||||
merged_doc = selected_guide[0][1].copy()
|
||||
merged_doc['body'] = merged_body.strip()
|
||||
merged_doc['content'] = merged_body.strip()
|
||||
# 删除 answer 字段,使用 body
|
||||
if 'answer' in merged_doc:
|
||||
del merged_doc['answer']
|
||||
results.append((merged_doc, float(doc_sections[0][2])))
|
||||
continue
|
||||
|
||||
# 默认:从每个文档取前 1-2 个 section
|
||||
sub_k = 2 if len(results) < top_k - 1 else top_k - len(results)
|
||||
|
||||
Reference in New Issue
Block a user