fix(rag): recall complete course homework lists
This commit is contained in:
@@ -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 = ("有哪些", "是什么", "包括什么", "都有什么", "列出", "汇总", "总结")
|
||||
|
||||
@@ -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="合一课程")
|
||||
|
||||
Reference in New Issue
Block a user