bugfix
Showing
3 changed files
with
61 additions
and
2 deletions
| ... | @@ -43,6 +43,16 @@ cp .env.example .env | ... | @@ -43,6 +43,16 @@ cp .env.example .env |
| 43 | - `RAGAS_HTTP_KEEPALIVE=false` 会让 RAGAS LLM 请求使用短连接,规避长流程中 async HTTP 连接清理问题 | 43 | - `RAGAS_HTTP_KEEPALIVE=false` 会让 RAGAS LLM 请求使用短连接,规避长流程中 async HTTP 连接清理问题 |
| 44 | - `RAGAS_TESTSET_TRANSFORMS=single_hop_entities` 会让 `prechunked` 只运行单跳 QA 所需的实体抽取 transform;设为 `default` 可回到 Ragas 默认 prechunked transforms | 44 | - `RAGAS_TESTSET_TRANSFORMS=single_hop_entities` 会让 `prechunked` 只运行单跳 QA 所需的实体抽取 transform;设为 `default` 可回到 Ragas 默认 prechunked transforms |
| 45 | 45 | ||
| 46 | ### 测试集生成模式 | ||
| 47 | |||
| 48 | `TESTSET_RAGAS_MODE` 控制 `05_generate_testset.py` 如何用 Ragas 生成 QA: | ||
| 49 | |||
| 50 | - `direct`:工程兜底模式。脚本直接把 WeKnora chunks 构造成单跳场景,再调用 Ragas 的 single-hop sample generator 生成 QA。它跳过 Ragas 的文档 transform 和 scenario generation,最稳,但不是完整的 Ragas testset generation 流程。 | ||
| 51 | - `prechunked`:Ragas 面向“已经切好 chunk”的标准入口,对应 `generate_with_chunks()`。这里的 pre-chunked 指输入已经是 WeKnora 导出的 chunk,不需要 Ragas 再从原始文档切分。当前配置默认只启用 `single_hop_entities` transform,并限制为 single-hop QA,避免不相关 chunk 被组合成 multi-hop 问题。 | ||
| 52 | - `langchain_docs`:Ragas 面向 LangChain Document 的原始文档入口,对应 `generate_with_langchain_docs()`。它可能触发标题、摘要、拆分等默认文档预处理,不适合作为本项目主路径,只保留给对比实验。 | ||
| 53 | |||
| 54 | 当前推荐先用 `direct` 跑通评测闭环;需要验证 Ragas 标准 pre-chunked 生成链路时,再切换到 `prechunked`。 | ||
| 55 | |||
| 46 | ## 首轮 Pilot | 56 | ## 首轮 Pilot |
| 47 | 57 | ||
| 48 | 把原始文件放到 `data/raw_docs/`,脚本会按扩展名自动识别 PDF 和 XLSX。也兼容旧目录: | 58 | 把原始文件放到 `data/raw_docs/`,脚本会按扩展名自动识别 PDF 和 XLSX。也兼容旧目录: | ... | ... |
| ... | @@ -290,7 +290,13 @@ RAGAS_HTTP_KEEPALIVE=false | ... | @@ -290,7 +290,13 @@ RAGAS_HTTP_KEEPALIVE=false |
| 290 | RAGAS_TESTSET_TRANSFORMS=single_hop_entities | 290 | RAGAS_TESTSET_TRANSFORMS=single_hop_entities |
| 291 | ``` | 291 | ``` |
| 292 | 292 | ||
| 293 | `direct` 模式会跳过 Ragas 默认的 `HeadlinesExtractor`、`SummaryExtractor`、`NERExtractor` 文档预处理链路,直接把 WeKnora chunks 组装成 Ragas KnowledgeGraph 并生成单跳 QA。`prechunked` 和 `langchain_docs` 仅用于对比实验,遇到本地 vLLM 结构化输出不稳定时不建议使用。 | 293 | `TESTSET_RAGAS_MODE` 支持三种模式: |
| 294 | |||
| 295 | - `direct`:工程兜底模式。脚本直接把 WeKnora chunks 构造成单跳场景,再调用 Ragas 的 single-hop sample generator 生成 QA。它跳过 Ragas 默认的 `HeadlinesExtractor`、`SummaryExtractor`、`NERExtractor`、scenario generation 等中间步骤,最稳,但不是完整的 Ragas testset generation 流程。 | ||
| 296 | - `prechunked`:Ragas 面向“已经切好 chunk”的标准入口,对应 `generate_with_chunks()`。这里的 pre-chunked 指输入已经是 WeKnora 导出的 chunk,不需要 Ragas 再从原始文档切分。当前建议配合 `RAGAS_TESTSET_TRANSFORMS=single_hop_entities`,只做单跳 QA 所需的实体抽取,并限制为 single-hop,避免多个不相关 chunk 被组合成 multi-hop 问题。 | ||
| 297 | - `langchain_docs`:Ragas 面向 LangChain Document 的原始文档入口,对应 `generate_with_langchain_docs()`。它可能触发标题、摘要、拆分等默认文档预处理,不适合作为本项目主路径,只保留给对比实验。 | ||
| 298 | |||
| 299 | 当前推荐先用 `direct` 跑通评测闭环;需要验证 Ragas 标准 pre-chunked 生成链路时,再切换到 `prechunked`。 | ||
| 294 | 300 | ||
| 295 | 如果使用 Qwen thinking 模型,`RAGAS_ENABLE_THINKING=false` 会只在 RAGAS 请求里附加 `chat_template_kwargs.enable_thinking=false`,避免 RAGAS 的 JSON/Pydantic 结构化输出被 `Thinking Process` 前缀破坏;WeKnora 本身的检索问答链路不经过这些脚本,不会受影响。 | 301 | 如果使用 Qwen thinking 模型,`RAGAS_ENABLE_THINKING=false` 会只在 RAGAS 请求里附加 `chat_template_kwargs.enable_thinking=false`,避免 RAGAS 的 JSON/Pydantic 结构化输出被 `Thinking Process` 前缀破坏;WeKnora 本身的检索问答链路不经过这些脚本,不会受影响。 |
| 296 | 302 | ... | ... |
| ... | @@ -26,7 +26,12 @@ def retrieval_metrics( | ... | @@ -26,7 +26,12 @@ def retrieval_metrics( |
| 26 | for row in samples: | 26 | for row in samples: |
| 27 | gold = set(row.get("gold_chunk_ids") or []) | 27 | gold = set(row.get("gold_chunk_ids") or []) |
| 28 | refs = row.get("weknora_references") or [] | 28 | refs = row.get("weknora_references") or [] |
| 29 | predicted = [str(ref.get("id")) for ref in refs if ref.get("id")] | 29 | predicted = [ |
| 30 | chunk_id | ||
| 31 | for ref in refs | ||
| 32 | for chunk_id in [_reference_chunk_id(ref)] | ||
| 33 | if chunk_id | ||
| 34 | ] | ||
| 30 | for k in ks: | 35 | for k in ks: |
| 31 | top_k = predicted[:k] | 36 | top_k = predicted[:k] |
| 32 | hits = len(gold.intersection(top_k)) | 37 | hits = len(gold.intersection(top_k)) |
| ... | @@ -53,10 +58,13 @@ def generate_summary_report( | ... | @@ -53,10 +58,13 @@ def generate_summary_report( |
| 53 | *, | 58 | *, |
| 54 | scores_csv_path: str = "data/reports/ragas_scores.csv", | 59 | scores_csv_path: str = "data/reports/ragas_scores.csv", |
| 55 | ragas_input_path: str = "data/runs/ragas_input.jsonl", | 60 | ragas_input_path: str = "data/runs/ragas_input.jsonl", |
| 61 | testset_path: str = "data/testsets/testset.reviewed.jsonl", | ||
| 56 | answers_path: str = "data/runs/weknora_answers.jsonl", | 62 | answers_path: str = "data/runs/weknora_answers.jsonl", |
| 57 | output_path: str = "data/reports/summary.md", | 63 | output_path: str = "data/reports/summary.md", |
| 58 | ) -> str: | 64 | ) -> str: |
| 59 | ragas_rows = read_jsonl(ragas_input_path, missing_ok=True) | 65 | ragas_rows = read_jsonl(ragas_input_path, missing_ok=True) |
| 66 | reviewed_rows = read_jsonl(testset_path, missing_ok=True) | ||
| 67 | ragas_rows = _backfill_gold_chunks(ragas_rows, reviewed_rows) | ||
| 60 | answer_rows = read_jsonl(answers_path, missing_ok=True) | 68 | answer_rows = read_jsonl(answers_path, missing_ok=True) |
| 61 | scores = pd.read_csv(scores_csv_path) if Path(scores_csv_path).exists() else pd.DataFrame() | 69 | scores = pd.read_csv(scores_csv_path) if Path(scores_csv_path).exists() else pd.DataFrame() |
| 62 | 70 | ||
| ... | @@ -155,6 +163,41 @@ def _worst_rows(scores: pd.DataFrame, column: str, *, limit: int = 10) -> list[d | ... | @@ -155,6 +163,41 @@ def _worst_rows(scores: pd.DataFrame, column: str, *, limit: int = 10) -> list[d |
| 155 | return scores.sort_values(metric_column, ascending=True).head(limit).to_dict(orient="records") | 163 | return scores.sort_values(metric_column, ascending=True).head(limit).to_dict(orient="records") |
| 156 | 164 | ||
| 157 | 165 | ||
| 166 | def _backfill_gold_chunks( | ||
| 167 | ragas_rows: list[dict[str, Any]], | ||
| 168 | reviewed_rows: list[dict[str, Any]], | ||
| 169 | ) -> list[dict[str, Any]]: | ||
| 170 | reviewed_by_id = { | ||
| 171 | row.get("sample_id"): row | ||
| 172 | for row in reviewed_rows | ||
| 173 | if row.get("sample_id") | ||
| 174 | } | ||
| 175 | result: list[dict[str, Any]] = [] | ||
| 176 | for row in ragas_rows: | ||
| 177 | if row.get("gold_chunk_ids"): | ||
| 178 | result.append(row) | ||
| 179 | continue | ||
| 180 | reviewed = reviewed_by_id.get(row.get("sample_id")) or {} | ||
| 181 | if reviewed.get("gold_chunk_ids"): | ||
| 182 | row = {**row, "gold_chunk_ids": reviewed.get("gold_chunk_ids")} | ||
| 183 | result.append(row) | ||
| 184 | return result | ||
| 185 | |||
| 186 | |||
| 187 | def _reference_chunk_id(reference: dict[str, Any]) -> str | None: | ||
| 188 | for key in ("id", "chunk_id", "chunkId"): | ||
| 189 | value = reference.get(key) | ||
| 190 | if value: | ||
| 191 | return str(value) | ||
| 192 | raw = reference.get("raw") | ||
| 193 | if isinstance(raw, dict): | ||
| 194 | for key in ("id", "chunk_id", "chunkId"): | ||
| 195 | value = raw.get(key) | ||
| 196 | if value: | ||
| 197 | return str(value) | ||
| 198 | return None | ||
| 199 | |||
| 200 | |||
| 158 | def _metric_column(scores: pd.DataFrame, name: str) -> str | None: | 201 | def _metric_column(scores: pd.DataFrame, name: str) -> str | None: |
| 159 | if name in scores.columns: | 202 | if name in scores.columns: |
| 160 | return name | 203 | return name | ... | ... |
-
Please register or sign in to post a comment