defgrams(text: str) -> list[str]: """用相邻两个字符构造简单特征;仅用于教学,不代表生产级分词。""" text = "".join(text.lower().split()) return [text[i:i + 2] for i inrange(len(text) - 1)] or [text]
defvector(text: str) -> dict[str, float]: counts: dict[str, int] = {} for gram in grams(text): counts[gram] = counts.get(gram, 0) + 1 return {key: log(1 + value) for key, value in counts.items()}
defcosine(left: dict[str, float], right: dict[str, float]) -> float: common = left.keys() & right.keys() dot = sum(left[key] * right[key] for key in common) size_left = sqrt(sum(value * value for value in left.values())) size_right = sqrt(sum(value * value for value in right.values())) return dot / (size_left * size_right) if size_left and size_right else0.0
defretrieve(question: str, limit: int = 2) -> list[tuple[float, Chunk]]: question_vector = vector(question) ranked = [(cosine(question_vector, vector(chunk.text)), chunk) for chunk in KNOWLEDGE] returnsorted(ranked, key=lambda item: item[0], reverse=True)[:limit]
defanswer(question: str, evidence: list[tuple[float, Chunk]]) -> str: """模拟生成阶段:只从召回证据中抽取包含问题特征的句子。""" question_grams = set(grams(question)) candidates = [] for score, chunk in evidence: for sentence in findall(r"[^。!?.!?]+[。!?.!?]", chunk.text): overlap = len(question_grams & set(grams(sentence))) candidates.append((overlap, score, sentence, chunk.source)) ifnot candidates ormax(item[0] for item in candidates) == 0: return"知识库中没有足够证据回答这个问题。" _, _, sentence, source = max(candidates) returnf"{sentence}(来源:{source})"