From 245b6b3ecab02901dc7273ca29d2c3e805cdddb3 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Thu, 16 Jul 2026 15:48:40 +0800 Subject: [PATCH] fix(rag): recall complete course homework lists --- .../app/services/knowledge_agent_service.py | 43 ++++++++++++++++--- .../backend/tests/test_knowledge_agent.py | 37 +++++++++++++++- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py b/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py index 400422f..38376d4 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/knowledge_agent_service.py @@ -32,7 +32,8 @@ from app.services.rag_service import PromptService, RagResult, RetrievedChunk SAFETY_RULE_VERSION = "minimum-safety-v1" MAX_LEXICAL_CANDIDATES = 12 MAX_SELECTED_SECTIONS = 4 -MAX_OVERVIEW_SELECTED_SECTIONS = 8 +MAX_OVERVIEW_LEXICAL_CANDIDATES = 48 +MAX_OVERVIEW_SELECTED_SECTIONS = 24 BUSINESS_MARKERS = {"课程", "大本营", "训练营", "老师", "卢慧", "功课", "学员", "课堂", "练习", "觉察", "内在"} @@ -105,10 +106,17 @@ class KnowledgeAgentService: try: if need_knowledge and selected_ids: terms = cls._query_terms(retrieval_question) - candidates = cls.search_knowledge(db, terms, selected_ids, catalog) - trace.append(cls._trace("search_knowledge", len(trace) + 1, {"queryTerms": terms, "knowledgeIds": selected_ids}, {"candidateCount": len(candidates), "candidates": [cls._candidate_trace(x) for x in candidates]}, started)) - await cls._rerank(db, retrieval_question, candidates, trace, started) practice_overview = cls._is_practice_overview(retrieval_question) + candidate_limit = cls._candidate_limit(retrieval_question) + candidates = cls.search_knowledge( + db, + terms, + selected_ids, + catalog, + candidate_limit=candidate_limit, + ) + trace.append(cls._trace("search_knowledge", len(trace) + 1, {"queryTerms": terms, "knowledgeIds": selected_ids, "candidateLimit": candidate_limit}, {"candidateCount": len(candidates), "candidates": [cls._candidate_trace(x) for x in candidates]}, started)) + await cls._rerank(db, retrieval_question, candidates, trace, started) selected = cls._select_sections( candidates, limit=cls._selection_limit(retrieval_question), @@ -323,7 +331,15 @@ class KnowledgeAgentService: return {"knowledgeId": item.knowledge.id, "knowledgeName": item.knowledge.name, "versionId": item.version.id, "chunkId": item.chunk.id, "sectionId": item.section.id, "title": item.chunk.title, "lexicalScore": item.lexical_score, "rerankScore": item.rerank_score, "selected": item.selected, "discardReason": item.discard_reason} @classmethod - def search_knowledge(cls, db: Session, terms: list[str], selected_ids: list[int], catalog: list[dict]) -> list[Candidate]: + def search_knowledge( + cls, + db: Session, + terms: list[str], + selected_ids: list[int], + catalog: list[dict], + *, + candidate_limit: int = MAX_LEXICAL_CANDIDATES, + ) -> list[Candidate]: versions_by_kb = {item["knowledgeId"]: item["versionId"] for item in catalog} version_ids = [versions_by_kb[item] for item in selected_ids if item in versions_by_kb] if not version_ids: @@ -349,7 +365,7 @@ class KnowledgeAgentService: if score > 0: candidates.append(Candidate(chunk, section, knowledge, version, score)) candidates.sort(key=lambda item: (item.lexical_score, item.version.published_at or item.version.created_at), reverse=True) - return candidates[:MAX_LEXICAL_CANDIDATES] + return candidates[:candidate_limit] @classmethod async def _rerank(cls, db: Session, question: str, candidates: list[Candidate], trace: list[dict], started: float) -> None: @@ -385,6 +401,15 @@ class KnowledgeAgentService: prefer_numbered_practice: bool = False, ) -> list[Candidate]: if prefer_numbered_practice: + numbered_candidates = [ + item for item in candidates + if KnowledgeAgentService._is_numbered_practice_title(item.chunk.title) + ] + if numbered_candidates: + for item in candidates: + if not KnowledgeAgentService._is_numbered_practice_title(item.chunk.title): + item.discard_reason = "作业清单优先采用编号练习章节" + candidates = numbered_candidates candidates = sorted( candidates, key=lambda item: ( @@ -415,6 +440,12 @@ class KnowledgeAgentService: return MAX_OVERVIEW_SELECTED_SECTIONS return MAX_SELECTED_SECTIONS + @staticmethod + def _candidate_limit(question: str) -> int: + if KnowledgeAgentService._is_practice_overview(question): + return MAX_OVERVIEW_LEXICAL_CANDIDATES + return MAX_LEXICAL_CANDIDATES + @staticmethod def _is_practice_overview(question: str) -> bool: overview_markers = ("有哪些", "是什么", "包括什么", "都有什么", "列出", "汇总", "总结") diff --git a/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py b/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py index 50f1076..82e4569 100644 --- a/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py +++ b/ai_knowledge_base_v2/apps/backend/tests/test_knowledge_agent.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import json from datetime import datetime +from types import SimpleNamespace from sqlalchemy import create_engine, select from sqlalchemy.orm import Session @@ -19,7 +20,7 @@ from app.models.knowledge import ( KnowledgeVersion, ) from app.models.chat import ChatMessage -from app.services.knowledge_agent_service import KnowledgeAgentService +from app.services.knowledge_agent_service import Candidate, KnowledgeAgentService def _database() -> Session: @@ -187,13 +188,45 @@ def test_homework_overview_expands_practice_terms_and_section_limit(): assert "功课" in terms assert "练习" in terms - assert KnowledgeAgentService._selection_limit("合一的作业是什么?") == 8 + assert KnowledgeAgentService._candidate_limit("合一的作业是什么?") == 48 + assert KnowledgeAgentService._selection_limit("合一的作业是什么?") == 24 + assert KnowledgeAgentService._candidate_limit("这个练习怎么做?") == 12 assert KnowledgeAgentService._selection_limit("这个练习怎么做?") == 4 assert KnowledgeAgentService._title_intent_boost("十七、练习一:风铃式静心", terms) == 20.0 assert KnowledgeAgentService._title_intent_boost("完整练习的方向", terms) == 3.0 assert KnowledgeAgentService._title_intent_boost("课程定位", terms) == 0.0 +def test_homework_overview_keeps_all_numbered_practices_before_selection(): + candidates = [ + Candidate( + chunk=SimpleNamespace(title=f"练习{index}:课程作业", id=index), + section=SimpleNamespace(id=index), + knowledge=SimpleNamespace(id=1), + version=SimpleNamespace(id=1), + lexical_score=float(30 - index), + ) + for index in range(1, 15) + ] + + unrelated = Candidate( + chunk=SimpleNamespace(title="课程介绍", id=99), + section=SimpleNamespace(id=99), + knowledge=SimpleNamespace(id=1), + version=SimpleNamespace(id=1), + lexical_score=100.0, + ) + + selected = KnowledgeAgentService._select_sections( + [unrelated, *candidates], + limit=KnowledgeAgentService._selection_limit("原生里的作业内容都有什么"), + prefer_numbered_practice=True, + ) + + assert len(selected) == 14 + assert all(item.chunk.title.startswith("练习") for item in selected) + + def test_complete_section_includes_child_headings_but_stops_at_next_peer(): with _database() as db: knowledge = _add_published_knowledge(db, knowledge_id=1, name="合一课程")