Files
wehub-resource-sync 1443d3fdf9
Ruff Format Check / Ruff Format & Lint (push) Failing after 7m39s
Deploy VitePress site to Pages / build (push) Failing after 9m11s
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:26 +08:00

414 lines
13 KiB
Python

import asyncio
import os
from types import SimpleNamespace
import pytest
os.environ.setdefault("OPENAI_API_KEY", "test-key")
from yuxi.knowledge.eval import benchmark_generation
from yuxi.knowledge.eval.benchmark_generation import (
build_benchmark_generation_prompt,
clamp_neighbors_count,
collect_kb_chunks,
iter_generated_benchmark_items,
normalize_generation_concurrency_count,
select_graph_enhanced_chunks,
select_neighbor_chunks_by_kb_query,
)
class FakeKnowledgeBase:
pass
class FakeGenerationKnowledgeBase:
def __init__(self, query_results=None):
self.query_results = query_results or []
self.query_calls = []
async def aquery(self, query_text, kb_id, **kwargs):
self.query_calls.append({"query_text": query_text, "kb_id": kb_id, **kwargs})
return self.query_results
class FakeLlm:
def __init__(self, gold_chunk_id="anchor_chunk"):
self.gold_chunk_id = gold_chunk_id
self.prompts = []
async def call(self, prompt, stream):
self.prompts.append(prompt)
return SimpleNamespace(
content=('{"query":"问题","gold_answer":"答案","gold_chunk_ids":["' + self.gold_chunk_id + '"]}')
)
class NoQueryKnowledgeBase(FakeGenerationKnowledgeBase):
async def aquery(self, query_text, kb_id, **kwargs):
raise AssertionError("neighbors_count=1 时不应调用 aquery")
class TrackingLlm:
def __init__(self, content=None, delay=0):
self.content = content or '{"query":"问题","gold_answer":"答案","gold_chunk_ids":["anchor_chunk"]}'
self.delay = delay
self.active_calls = 0
self.max_active_calls = 0
self.calls = 0
async def call(self, prompt, stream):
self.calls += 1
self.active_calls += 1
self.max_active_calls = max(self.max_active_calls, self.active_calls)
try:
if self.delay:
await asyncio.sleep(self.delay)
return SimpleNamespace(content=self.content)
finally:
self.active_calls -= 1
class FakeGraphGenerationKnowledgeBase(FakeGenerationKnowledgeBase):
pass
def make_chunk(
chunk_id: str,
*,
kb_id: str = "db_1",
file_id: str = "file_a",
content: str = "anchor content",
chunk_index: int = 0,
graph_indexed: bool = False,
ent_ids: list[str] | None = None,
):
return SimpleNamespace(
chunk_id=chunk_id,
kb_id=kb_id,
file_id=file_id,
content=content,
chunk_index=chunk_index,
graph_indexed=graph_indexed,
ent_ids=ent_ids,
tags=None,
extraction_result=None,
)
@pytest.fixture(autouse=True)
def fake_chunk_repository(monkeypatch):
class FakeChunkRepository:
chunks = [make_chunk("anchor_chunk")]
async def list_by_kb_id(self, kb_id):
return [chunk for chunk in self.chunks if chunk.kb_id == kb_id]
monkeypatch.setattr(
"yuxi.repositories.knowledge_chunk_repository.KnowledgeChunkRepository",
FakeChunkRepository,
)
return FakeChunkRepository
def test_clamp_neighbors_count():
assert clamp_neighbors_count(-1) == 0
assert clamp_neighbors_count(3) == 3
assert clamp_neighbors_count(11) == 10
def test_normalize_generation_concurrency_count():
assert normalize_generation_concurrency_count(None) == 10
assert normalize_generation_concurrency_count("") == 10
assert normalize_generation_concurrency_count(0) == 1
assert normalize_generation_concurrency_count(-5) == 1
assert normalize_generation_concurrency_count(10000) == 20
def test_build_benchmark_generation_prompt_contains_required_schema():
prompt = build_benchmark_generation_prompt([("chunk_1", "片段内容")])
assert "片段ID=chunk_1" in prompt
assert "query、gold_answer、gold_chunk_ids" in prompt
@pytest.mark.asyncio
async def test_collect_kb_chunks_filters_kb_id(fake_chunk_repository):
fake_chunk_repository.chunks = [
make_chunk("file_a_chunk", content="内容"),
make_chunk("file_b_chunk", kb_id="db_2", file_id="file_b", content="其他"),
]
chunks = await collect_kb_chunks(FakeKnowledgeBase(), "db_1")
assert chunks == [
{
"id": "file_a_chunk",
"content": "内容",
"file_id": "file_a",
"chunk_index": 0,
"graph_indexed": False,
"ent_ids": [],
"tags": [],
"extraction_result": None,
}
]
@pytest.mark.asyncio
async def test_iter_generated_benchmark_items_with_one_chunk_does_not_query(monkeypatch):
fake_llm = FakeLlm()
monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: fake_llm)
items = [
item
async for item in iter_generated_benchmark_items(
kb_instance=NoQueryKnowledgeBase(),
kb_id="db_1",
count=1,
neighbors_count=1,
llm_model_spec="test-provider:test-model",
)
]
assert items == [{"query": "问题", "gold_chunk_ids": ["anchor_chunk"], "gold_answer": "答案"}]
assert "片段ID=anchor_chunk" in fake_llm.prompts[0]
@pytest.mark.asyncio
async def test_select_neighbor_chunks_by_kb_query_filters_anchor():
kb = FakeGenerationKnowledgeBase(
query_results=[
{
"content": "anchor content",
"metadata": {"chunk_id": "anchor_chunk", "file_id": "file_a", "chunk_index": 0},
},
{
"content": "neighbor content",
"metadata": {"chunk_id": "neighbor_chunk", "file_id": "file_a", "chunk_index": 1},
},
]
)
chunks = await select_neighbor_chunks_by_kb_query(
kb_instance=kb,
kb_id="db_1",
anchor_chunk={"id": "anchor_chunk", "content": "anchor content", "file_id": "file_a", "chunk_index": 0},
neighbors_count=1,
)
assert chunks == [{"id": "neighbor_chunk", "content": "neighbor content", "file_id": "file_a", "chunk_index": 1}]
assert kb.query_calls == [
{
"query_text": "anchor content",
"kb_id": "db_1",
"search_mode": "vector",
"final_top_k": 4,
"use_reranker": False,
"similarity_threshold": 0.0,
}
]
@pytest.mark.asyncio
async def test_select_graph_enhanced_chunks_expands_by_ppr_with_anchor_bias(monkeypatch):
calls = []
async def fake_rank(self, kb_id, seed_weights, *, max_nodes, top_k, damping):
calls.append(dict(seed_weights))
if len(calls) == 1:
return [("anchor", 0.9), ("neighbor_1", 0.8)]
return [("anchor", 0.9), ("neighbor_1", 0.8), ("neighbor_2", 0.7)]
monkeypatch.setattr(
"yuxi.knowledge.graphs.milvus_graph_service.MilvusGraphService.query_and_rank_chunks_by_ppr",
fake_rank,
)
chunks_by_id = {
"anchor": {"id": "anchor", "content": "anchor", "ent_ids": ["anchor_entity"]},
"neighbor_1": {"id": "neighbor_1", "content": "neighbor 1", "ent_ids": ["entity_1"]},
"neighbor_2": {"id": "neighbor_2", "content": "neighbor 2", "ent_ids": ["entity_2"]},
}
chunks = await select_graph_enhanced_chunks(
kb_id="db_1",
anchor_chunk=chunks_by_id["anchor"],
chunks_by_id=chunks_by_id,
context_count=3,
graph_expand_top_k=1,
)
assert [chunk["id"] for chunk in chunks] == ["anchor", "neighbor_1", "neighbor_2"]
assert calls[0] == {"anchor_entity": 1.0}
assert calls[1]["anchor_entity"] == 1.0
assert calls[1]["entity_1"] == 0.9
@pytest.mark.asyncio
async def test_iter_generated_benchmark_items_graph_mode_uses_graph_indexed_anchor(monkeypatch, fake_chunk_repository):
fake_chunk_repository.chunks = [
make_chunk(
"vector_anchor",
content="vector content",
chunk_index=0,
graph_indexed=False,
ent_ids=["vector_entity"],
),
make_chunk(
"graph_anchor",
content="graph anchor content",
chunk_index=1,
graph_indexed=True,
ent_ids=["anchor_entity"],
),
make_chunk(
"graph_neighbor",
content="graph neighbor content",
chunk_index=2,
graph_indexed=False,
ent_ids=["neighbor_entity"],
),
]
async def fake_rank(self, kb_id, seed_weights, *, max_nodes, top_k, damping):
assert seed_weights["anchor_entity"] == 1.0
return [("graph_anchor", 0.9), ("graph_neighbor", 0.8)]
fake_llm = FakeLlm(gold_chunk_id="graph_neighbor")
monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: fake_llm)
monkeypatch.setattr(
"yuxi.knowledge.graphs.milvus_graph_service.MilvusGraphService.query_and_rank_chunks_by_ppr",
fake_rank,
)
kb = FakeGraphGenerationKnowledgeBase()
items = [
item
async for item in iter_generated_benchmark_items(
kb_instance=kb,
kb_id="db_1",
count=1,
neighbors_count=2,
llm_model_spec="test-provider:test-model",
generation_mode="graph_enhanced",
)
]
assert items == [{"query": "问题", "gold_chunk_ids": ["graph_neighbor"], "gold_answer": "答案"}]
assert kb.query_calls == []
assert "片段ID=graph_anchor" in fake_llm.prompts[0]
assert "片段ID=graph_neighbor" in fake_llm.prompts[0]
assert "片段ID=vector_anchor" not in fake_llm.prompts[0]
@pytest.mark.asyncio
async def test_iter_generated_benchmark_items_uses_query_neighbor(monkeypatch):
fake_llm = FakeLlm(gold_chunk_id="neighbor_chunk")
monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: fake_llm)
kb = FakeGenerationKnowledgeBase(
query_results=[
{
"content": "neighbor content",
"metadata": {"chunk_id": "neighbor_chunk", "file_id": "file_a", "chunk_index": 1},
}
]
)
items = [
item
async for item in iter_generated_benchmark_items(
kb_instance=kb,
kb_id="db_1",
count=1,
neighbors_count=2,
llm_model_spec="test-provider:test-model",
)
]
assert items == [{"query": "问题", "gold_chunk_ids": ["neighbor_chunk"], "gold_answer": "答案"}]
assert kb.query_calls[0]["query_text"] == "anchor content"
assert kb.query_calls[0]["search_mode"] == "vector"
assert "片段ID=neighbor_chunk" in fake_llm.prompts[0]
@pytest.mark.asyncio
async def test_iter_generated_benchmark_items_falls_back_to_anchor_when_query_empty(monkeypatch):
fake_llm = FakeLlm()
monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: fake_llm)
items = [
item
async for item in iter_generated_benchmark_items(
kb_instance=FakeGenerationKnowledgeBase(query_results=[]),
kb_id="db_1",
count=1,
neighbors_count=2,
llm_model_spec="test-provider:test-model",
)
]
assert items == [{"query": "问题", "gold_chunk_ids": ["anchor_chunk"], "gold_answer": "答案"}]
assert "片段ID=anchor_chunk" in fake_llm.prompts[0]
@pytest.mark.asyncio
async def test_iter_generated_benchmark_items_respects_concurrency_count(monkeypatch):
fake_llm = TrackingLlm(delay=0.01)
monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: fake_llm)
items = [
item
async for item in iter_generated_benchmark_items(
kb_instance=NoQueryKnowledgeBase(),
kb_id="db_1",
count=4,
neighbors_count=1,
concurrency_count=2,
llm_model_spec="test-provider:test-model",
)
]
assert len(items) == 4
assert fake_llm.max_active_calls == 2
@pytest.mark.asyncio
async def test_iter_generated_benchmark_items_returns_at_most_count(monkeypatch):
fake_llm = TrackingLlm(delay=0.01)
monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: fake_llm)
items = [
item
async for item in iter_generated_benchmark_items(
kb_instance=NoQueryKnowledgeBase(),
kb_id="db_1",
count=3,
neighbors_count=1,
concurrency_count=10,
llm_model_spec="test-provider:test-model",
)
]
assert len(items) == 3
@pytest.mark.asyncio
async def test_iter_generated_benchmark_items_stops_at_max_attempts(monkeypatch):
fake_llm = TrackingLlm(content='{"query":"","gold_answer":"答案","gold_chunk_ids":["anchor_chunk"]}')
monkeypatch.setattr(benchmark_generation, "select_model", lambda model_spec: fake_llm)
items = [
item
async for item in iter_generated_benchmark_items(
kb_instance=NoQueryKnowledgeBase(),
kb_id="db_1",
count=2,
neighbors_count=1,
concurrency_count=10,
llm_model_spec="test-provider:test-model",
)
]
assert items == []
assert fake_llm.calls == 50