Commit 7ba212d1 7ba212d111e8d255f108a791b618391ba08a8ecc by 沈秋雨

bugfix

1 parent 3cd77d9d
......@@ -43,6 +43,16 @@ cp .env.example .env
- `RAGAS_HTTP_KEEPALIVE=false` 会让 RAGAS LLM 请求使用短连接,规避长流程中 async HTTP 连接清理问题
- `RAGAS_TESTSET_TRANSFORMS=single_hop_entities` 会让 `prechunked` 只运行单跳 QA 所需的实体抽取 transform;设为 `default` 可回到 Ragas 默认 prechunked transforms
### 测试集生成模式
`TESTSET_RAGAS_MODE` 控制 `05_generate_testset.py` 如何用 Ragas 生成 QA:
- `direct`:工程兜底模式。脚本直接把 WeKnora chunks 构造成单跳场景,再调用 Ragas 的 single-hop sample generator 生成 QA。它跳过 Ragas 的文档 transform 和 scenario generation,最稳,但不是完整的 Ragas testset generation 流程。
- `prechunked`:Ragas 面向“已经切好 chunk”的标准入口,对应 `generate_with_chunks()`。这里的 pre-chunked 指输入已经是 WeKnora 导出的 chunk,不需要 Ragas 再从原始文档切分。当前配置默认只启用 `single_hop_entities` transform,并限制为 single-hop QA,避免不相关 chunk 被组合成 multi-hop 问题。
- `langchain_docs`:Ragas 面向 LangChain Document 的原始文档入口,对应 `generate_with_langchain_docs()`。它可能触发标题、摘要、拆分等默认文档预处理,不适合作为本项目主路径,只保留给对比实验。
当前推荐先用 `direct` 跑通评测闭环;需要验证 Ragas 标准 pre-chunked 生成链路时,再切换到 `prechunked`
## 首轮 Pilot
把原始文件放到 `data/raw_docs/`,脚本会按扩展名自动识别 PDF 和 XLSX。也兼容旧目录:
......
......@@ -290,7 +290,13 @@ RAGAS_HTTP_KEEPALIVE=false
RAGAS_TESTSET_TRANSFORMS=single_hop_entities
```
`direct` 模式会跳过 Ragas 默认的 `HeadlinesExtractor``SummaryExtractor``NERExtractor` 文档预处理链路,直接把 WeKnora chunks 组装成 Ragas KnowledgeGraph 并生成单跳 QA。`prechunked``langchain_docs` 仅用于对比实验,遇到本地 vLLM 结构化输出不稳定时不建议使用。
`TESTSET_RAGAS_MODE` 支持三种模式:
- `direct`:工程兜底模式。脚本直接把 WeKnora chunks 构造成单跳场景,再调用 Ragas 的 single-hop sample generator 生成 QA。它跳过 Ragas 默认的 `HeadlinesExtractor``SummaryExtractor``NERExtractor`、scenario generation 等中间步骤,最稳,但不是完整的 Ragas testset generation 流程。
- `prechunked`:Ragas 面向“已经切好 chunk”的标准入口,对应 `generate_with_chunks()`。这里的 pre-chunked 指输入已经是 WeKnora 导出的 chunk,不需要 Ragas 再从原始文档切分。当前建议配合 `RAGAS_TESTSET_TRANSFORMS=single_hop_entities`,只做单跳 QA 所需的实体抽取,并限制为 single-hop,避免多个不相关 chunk 被组合成 multi-hop 问题。
- `langchain_docs`:Ragas 面向 LangChain Document 的原始文档入口,对应 `generate_with_langchain_docs()`。它可能触发标题、摘要、拆分等默认文档预处理,不适合作为本项目主路径,只保留给对比实验。
当前推荐先用 `direct` 跑通评测闭环;需要验证 Ragas 标准 pre-chunked 生成链路时,再切换到 `prechunked`
如果使用 Qwen thinking 模型,`RAGAS_ENABLE_THINKING=false` 会只在 RAGAS 请求里附加 `chat_template_kwargs.enable_thinking=false`,避免 RAGAS 的 JSON/Pydantic 结构化输出被 `Thinking Process` 前缀破坏;WeKnora 本身的检索问答链路不经过这些脚本,不会受影响。
......
......@@ -26,7 +26,12 @@ def retrieval_metrics(
for row in samples:
gold = set(row.get("gold_chunk_ids") or [])
refs = row.get("weknora_references") or []
predicted = [str(ref.get("id")) for ref in refs if ref.get("id")]
predicted = [
chunk_id
for ref in refs
for chunk_id in [_reference_chunk_id(ref)]
if chunk_id
]
for k in ks:
top_k = predicted[:k]
hits = len(gold.intersection(top_k))
......@@ -53,10 +58,13 @@ def generate_summary_report(
*,
scores_csv_path: str = "data/reports/ragas_scores.csv",
ragas_input_path: str = "data/runs/ragas_input.jsonl",
testset_path: str = "data/testsets/testset.reviewed.jsonl",
answers_path: str = "data/runs/weknora_answers.jsonl",
output_path: str = "data/reports/summary.md",
) -> str:
ragas_rows = read_jsonl(ragas_input_path, missing_ok=True)
reviewed_rows = read_jsonl(testset_path, missing_ok=True)
ragas_rows = _backfill_gold_chunks(ragas_rows, reviewed_rows)
answer_rows = read_jsonl(answers_path, missing_ok=True)
scores = pd.read_csv(scores_csv_path) if Path(scores_csv_path).exists() else pd.DataFrame()
......@@ -155,6 +163,41 @@ def _worst_rows(scores: pd.DataFrame, column: str, *, limit: int = 10) -> list[d
return scores.sort_values(metric_column, ascending=True).head(limit).to_dict(orient="records")
def _backfill_gold_chunks(
ragas_rows: list[dict[str, Any]],
reviewed_rows: list[dict[str, Any]],
) -> list[dict[str, Any]]:
reviewed_by_id = {
row.get("sample_id"): row
for row in reviewed_rows
if row.get("sample_id")
}
result: list[dict[str, Any]] = []
for row in ragas_rows:
if row.get("gold_chunk_ids"):
result.append(row)
continue
reviewed = reviewed_by_id.get(row.get("sample_id")) or {}
if reviewed.get("gold_chunk_ids"):
row = {**row, "gold_chunk_ids": reviewed.get("gold_chunk_ids")}
result.append(row)
return result
def _reference_chunk_id(reference: dict[str, Any]) -> str | None:
for key in ("id", "chunk_id", "chunkId"):
value = reference.get(key)
if value:
return str(value)
raw = reference.get("raw")
if isinstance(raw, dict):
for key in ("id", "chunk_id", "chunkId"):
value = raw.get(key)
if value:
return str(value)
return None
def _metric_column(scores: pd.DataFrame, name: str) -> str | None:
if name in scores.columns:
return name
......