评估使用agent模式检索
Showing
7 changed files
with
115 additions
and
9 deletions
| ... | @@ -2,6 +2,10 @@ WEKNORA_BASE_URL=http://localhost:8080/api/v1 | ... | @@ -2,6 +2,10 @@ WEKNORA_BASE_URL=http://localhost:8080/api/v1 |
| 2 | WEKNORA_API_KEY= | 2 | WEKNORA_API_KEY= |
| 3 | WEKNORA_KB_ID= | 3 | WEKNORA_KB_ID= |
| 4 | WEKNORA_KB_NAME=ragas-eval-pilot | 4 | WEKNORA_KB_NAME=ragas-eval-pilot |
| 5 | WEKNORA_AGENT_ID=builtin-quick-answer | ||
| 6 | WEKNORA_AGENT_ENABLED=false | ||
| 7 | WEKNORA_WEB_SEARCH_ENABLED=false | ||
| 8 | WEKNORA_SUMMARY_MODEL_ID= | ||
| 5 | 9 | ||
| 6 | # MinerU HTTP parser. Use cpu, cuda, cuda:0, etc. according to the deployed | 10 | # MinerU HTTP parser. Use cpu, cuda, cuda:0, etc. according to the deployed |
| 7 | # MinerU backend. | 11 | # MinerU backend. | ... | ... |
| ... | @@ -613,7 +613,7 @@ Content-Type: application/json | ... | @@ -613,7 +613,7 @@ Content-Type: application/json |
| 613 | 请求: | 613 | 请求: |
| 614 | 614 | ||
| 615 | ```http | 615 | ```http |
| 616 | POST /api/v1/knowledge-chat/{session_id} | 616 | POST /api/v1/agent-chat/{session_id} |
| 617 | X-API-Key: <api-key> | 617 | X-API-Key: <api-key> |
| 618 | Content-Type: application/json | 618 | Content-Type: application/json |
| 619 | ``` | 619 | ``` |
| ... | @@ -623,6 +623,8 @@ Content-Type: application/json | ... | @@ -623,6 +623,8 @@ Content-Type: application/json |
| 623 | ```json | 623 | ```json |
| 624 | { | 624 | { |
| 625 | "query": "合同中的付款期限是什么?", | 625 | "query": "合同中的付款期限是什么?", |
| 626 | "agent_id": "builtin-quick-answer", | ||
| 627 | "agent_enabled": false, | ||
| 626 | "knowledge_base_ids": ["kb-0001"], | 628 | "knowledge_base_ids": ["kb-0001"], |
| 627 | "disable_title": true, | 629 | "disable_title": true, |
| 628 | "enable_memory": false, | 630 | "enable_memory": false, |
| ... | @@ -635,6 +637,8 @@ Content-Type: application/json | ... | @@ -635,6 +637,8 @@ Content-Type: application/json |
| 635 | ```json | 637 | ```json |
| 636 | { | 638 | { |
| 637 | "query": "合同中的付款期限是什么?", | 639 | "query": "合同中的付款期限是什么?", |
| 640 | "agent_id": "builtin-quick-answer", | ||
| 641 | "agent_enabled": false, | ||
| 638 | "knowledge_ids": ["knowledge-0001"], | 642 | "knowledge_ids": ["knowledge-0001"], |
| 639 | "disable_title": true, | 643 | "disable_title": true, |
| 640 | "enable_memory": false, | 644 | "enable_memory": false, |
| ... | @@ -682,7 +686,7 @@ data: { | ... | @@ -682,7 +686,7 @@ data: { |
| 682 | event: message | 686 | event: message |
| 683 | data: { | 687 | data: { |
| 684 | "id": "request-0001", | 688 | "id": "request-0001", |
| 685 | "response_type": "answer", | 689 | "response_type": "final_answer", |
| 686 | "content": "合同约定,付款期限为收到合法有效发票后30日内。", | 690 | "content": "合同约定,付款期限为收到合法有效发票后30日内。", |
| 687 | "done": false, | 691 | "done": false, |
| 688 | "knowledge_references": null | 692 | "knowledge_references": null |
| ... | @@ -695,7 +699,7 @@ data: { | ... | @@ -695,7 +699,7 @@ data: { |
| 695 | event: message | 699 | event: message |
| 696 | data: { | 700 | data: { |
| 697 | "id": "request-0001", | 701 | "id": "request-0001", |
| 698 | "response_type": "answer", | 702 | "response_type": "final_answer", |
| 699 | "content": "", | 703 | "content": "", |
| 700 | "done": true, | 704 | "done": true, |
| 701 | "knowledge_references": null | 705 | "knowledge_references": null |
| ... | @@ -887,9 +891,9 @@ Content-Type: application/json | ... | @@ -887,9 +891,9 @@ Content-Type: application/json |
| 887 | 对每条审核通过的 QA: | 891 | 对每条审核通过的 QA: |
| 888 | 892 | ||
| 889 | 1. 创建一个干净 session。 | 893 | 1. 创建一个干净 session。 |
| 890 | 2. 调用 `POST /knowledge-chat/{session_id}`。 | 894 | 2. 调用 `POST /agent-chat/{session_id}`,默认使用 `agent_id=builtin-quick-answer`。 |
| 891 | 3. 解析 SSE 中的 references 事件。 | 895 | 3. 解析 SSE 中的 references 事件。 |
| 892 | 4. 解析 SSE 中的 answer 事件。 | 896 | 4. 解析 SSE 中的 final_answer 事件。 |
| 893 | 5. 构造一条 Ragas 输入记录。 | 897 | 5. 构造一条 Ragas 输入记录。 |
| 894 | 898 | ||
| 895 | `data/runs/ragas_input.jsonl`: | 899 | `data/runs/ragas_input.jsonl`: |
| ... | @@ -1040,9 +1044,9 @@ Content-Type: application/json | ... | @@ -1040,9 +1044,9 @@ Content-Type: application/json |
| 1040 | ### 阶段 6:运行 WeKnora QA | 1044 | ### 阶段 6:运行 WeKnora QA |
| 1041 | 1045 | ||
| 1042 | - [ ] 每条 QA 创建一个干净 session。 | 1046 | - [ ] 每条 QA 创建一个干净 session。 |
| 1043 | - [ ] 调用 `knowledge-chat`。 | 1047 | - [ ] 调用 `agent-chat`,默认使用 `builtin-quick-answer`。 |
| 1044 | - [ ] 解析 SSE references 事件。 | 1048 | - [ ] 解析 SSE references 事件。 |
| 1045 | - [ ] 解析 SSE answer 事件。 | 1049 | - [ ] 解析 SSE final_answer 事件。 |
| 1046 | - [ ] 按 chunk ID 去重引用。 | 1050 | - [ ] 按 chunk ID 去重引用。 |
| 1047 | - [ ] 保存原始答案和引用。 | 1051 | - [ ] 保存原始答案和引用。 |
| 1048 | - [ ] 记录空答案失败。 | 1052 | - [ ] 记录空答案失败。 | ... | ... |
| ... | @@ -84,6 +84,8 @@ python workflows/04_evaluate_report.py | ... | @@ -84,6 +84,8 @@ python workflows/04_evaluate_report.py |
| 84 | 84 | ||
| 85 | 首轮建议只使用 2 个 PDF、1 个 XLSX 和 10 条审核通过 QA,确认 `retrieved_contexts`、`response`、Ragas 输入字段都正常后再扩展样本量。 | 85 | 首轮建议只使用 2 个 PDF、1 个 XLSX 和 10 条审核通过 QA,确认 `retrieved_contexts`、`response`、Ragas 输入字段都正常后再扩展样本量。 |
| 86 | 86 | ||
| 87 | WeKnora 回答收集阶段使用 `POST /api/v1/agent-chat/{session_id}`,默认 `WEKNORA_AGENT_ID=builtin-quick-answer`、`WEKNORA_AGENT_ENABLED=false`,即使用内置快速问答的 RAG 模式并携带 `WEKNORA_KB_ID`。如需评测智能推理链路,可改为 `builtin-smart-reasoning` 并开启 `WEKNORA_AGENT_ENABLED=true`。 | ||
| 88 | |||
| 87 | 默认 `04_parse_docs.py` 从 WeKnora 导出的 `data/exported/chunks.jsonl` 构造测试集来源,不再重复调用外部 PDF 解析器。`05_generate_testset.py` 默认使用 Ragas 结合评估侧 LLM 自动生成 QA;生成阶段使用 `TESTSET_RAGAS_MODE=direct`,直接把 WeKnora chunks 组装成 Ragas KnowledgeGraph 并生成单跳 QA,避免 Ragas 默认文档预处理链路重新抽标题、摘要和实体。生成阶段还会用 `TESTSET_MAX_DOCUMENT_CHARS` 限制单条来源上下文长度,用 `TESTSET_GENERATOR_MAX_TOKENS` 控制生成输出预算,并按来源文件轮询抽样,避免测试集集中在单个文件。`local`、`mineru` 和 `rule_based` 只作为可选实验/兜底配置保留。 | 89 | 默认 `04_parse_docs.py` 从 WeKnora 导出的 `data/exported/chunks.jsonl` 构造测试集来源,不再重复调用外部 PDF 解析器。`05_generate_testset.py` 默认使用 Ragas 结合评估侧 LLM 自动生成 QA;生成阶段使用 `TESTSET_RAGAS_MODE=direct`,直接把 WeKnora chunks 组装成 Ragas KnowledgeGraph 并生成单跳 QA,避免 Ragas 默认文档预处理链路重新抽标题、摘要和实体。生成阶段还会用 `TESTSET_MAX_DOCUMENT_CHARS` 限制单条来源上下文长度,用 `TESTSET_GENERATOR_MAX_TOKENS` 控制生成输出预算,并按来源文件轮询抽样,避免测试集集中在单个文件。`local`、`mineru` 和 `rule_based` 只作为可选实验/兜底配置保留。 |
| 88 | 90 | ||
| 89 | ## 主要产物 | 91 | ## 主要产物 | ... | ... |
| ... | @@ -317,7 +317,8 @@ python scripts/07_run_weknora_qa.py | ... | @@ -317,7 +317,8 @@ python scripts/07_run_weknora_qa.py |
| 317 | 317 | ||
| 318 | - 知识库是否解析完成。 | 318 | - 知识库是否解析完成。 |
| 319 | - chunks 是否导出非空。 | 319 | - chunks 是否导出非空。 |
| 320 | - WeKnora 问答 SSE 是否返回 `references` 事件。 | 320 | - WeKnora `agent-chat` 问答 SSE 是否返回 `references` 事件。 |
| 321 | - `.env` 中 `WEKNORA_AGENT_ID` 是否为 `builtin-quick-answer`,且 `WEKNORA_AGENT_ENABLED=false`。 | ||
| 321 | - `data/runs/failed_requests.jsonl` 中是否记录 `empty_retrieval`。 | 322 | - `data/runs/failed_requests.jsonl` 中是否记录 `empty_retrieval`。 |
| 322 | 323 | ||
| 323 | ## 8. 扩大样本规模 | 324 | ## 8. 扩大样本规模 | ... | ... |
| ... | @@ -50,6 +50,10 @@ parsing: | ... | @@ -50,6 +50,10 @@ parsing: |
| 50 | 50 | ||
| 51 | qa: | 51 | qa: |
| 52 | one_session_per_question: true | 52 | one_session_per_question: true |
| 53 | agent_id: "${WEKNORA_AGENT_ID:-builtin-quick-answer}" | ||
| 54 | agent_enabled: "${WEKNORA_AGENT_ENABLED:-false}" | ||
| 55 | web_search_enabled: "${WEKNORA_WEB_SEARCH_ENABLED:-false}" | ||
| 56 | summary_model_id: "${WEKNORA_SUMMARY_MODEL_ID:-}" | ||
| 53 | disable_title: true | 57 | disable_title: true |
| 54 | enable_memory: false | 58 | enable_memory: false |
| 55 | channel: "api" | 59 | channel: "api" | ... | ... |
| ... | @@ -24,9 +24,13 @@ def main() -> int: | ... | @@ -24,9 +24,13 @@ def main() -> int: |
| 24 | session_id = session.get("id") | 24 | session_id = session.get("id") |
| 25 | if not session_id: | 25 | if not session_id: |
| 26 | raise RuntimeError(f"create_session returned no id for {sample_id}") | 26 | raise RuntimeError(f"create_session returned no id for {sample_id}") |
| 27 | result = client.knowledge_chat_sse( | 27 | result = client.agent_chat_sse( |
| 28 | session_id=session_id, | 28 | session_id=session_id, |
| 29 | query=row["user_input"], | 29 | query=row["user_input"], |
| 30 | agent_id=str(qa_config.get("agent_id", "builtin-quick-answer")), | ||
| 31 | agent_enabled=bool(qa_config.get("agent_enabled", False)), | ||
| 32 | web_search_enabled=bool(qa_config.get("web_search_enabled", False)), | ||
| 33 | summary_model_id=qa_config.get("summary_model_id") or None, | ||
| 30 | disable_title=bool(qa_config.get("disable_title", True)), | 34 | disable_title=bool(qa_config.get("disable_title", True)), |
| 31 | enable_memory=bool(qa_config.get("enable_memory", False)), | 35 | enable_memory=bool(qa_config.get("enable_memory", False)), |
| 32 | channel=str(qa_config.get("channel", "api")), | 36 | channel=str(qa_config.get("channel", "api")), | ... | ... |
| ... | @@ -201,6 +201,44 @@ class WeKnoraClient: | ... | @@ -201,6 +201,44 @@ class WeKnoraClient: |
| 201 | "raw_events": raw_events, | 201 | "raw_events": raw_events, |
| 202 | } | 202 | } |
| 203 | 203 | ||
| 204 | def agent_chat_sse( | ||
| 205 | self, | ||
| 206 | *, | ||
| 207 | session_id: str, | ||
| 208 | query: str, | ||
| 209 | agent_id: str, | ||
| 210 | agent_enabled: bool = False, | ||
| 211 | knowledge_ids: list[str] | None = None, | ||
| 212 | knowledge_base_ids: list[str] | None = None, | ||
| 213 | mentioned_items: list[dict[str, Any]] | None = None, | ||
| 214 | web_search_enabled: bool | None = False, | ||
| 215 | summary_model_id: str | None = None, | ||
| 216 | disable_title: bool = True, | ||
| 217 | enable_memory: bool = False, | ||
| 218 | channel: str = "api", | ||
| 219 | ) -> dict[str, Any]: | ||
| 220 | payload: dict[str, Any] = { | ||
| 221 | "query": query, | ||
| 222 | "agent_id": agent_id, | ||
| 223 | "agent_enabled": agent_enabled, | ||
| 224 | "disable_title": disable_title, | ||
| 225 | "enable_memory": enable_memory, | ||
| 226 | "channel": channel, | ||
| 227 | } | ||
| 228 | if web_search_enabled is not None: | ||
| 229 | payload["web_search_enabled"] = web_search_enabled | ||
| 230 | if summary_model_id: | ||
| 231 | payload["summary_model_id"] = summary_model_id | ||
| 232 | if mentioned_items: | ||
| 233 | payload["mentioned_items"] = mentioned_items | ||
| 234 | if knowledge_ids: | ||
| 235 | payload["knowledge_ids"] = knowledge_ids | ||
| 236 | else: | ||
| 237 | self._ensure_knowledge_base_id() | ||
| 238 | payload["knowledge_base_ids"] = knowledge_base_ids or [self.knowledge_base_id] | ||
| 239 | |||
| 240 | return self._chat_sse(f"agent-chat/{session_id}", payload) | ||
| 241 | |||
| 204 | def load_messages(self, session_id: str, *, limit: int = 10) -> list[dict[str, Any]]: | 242 | def load_messages(self, session_id: str, *, limit: int = 10) -> list[dict[str, Any]]: |
| 205 | payload = self._json_request("GET", f"messages/{session_id}/load", params={"limit": limit}) | 243 | payload = self._json_request("GET", f"messages/{session_id}/load", params={"limit": limit}) |
| 206 | if isinstance(payload, list): | 244 | if isinstance(payload, list): |
| ... | @@ -223,6 +261,55 @@ class WeKnoraClient: | ... | @@ -223,6 +261,55 @@ class WeKnoraClient: |
| 223 | data = self._json_request("POST", "knowledge-search", json=payload) | 261 | data = self._json_request("POST", "knowledge-search", json=payload) |
| 224 | return data if isinstance(data, list) else [] | 262 | return data if isinstance(data, list) else [] |
| 225 | 263 | ||
| 264 | def _chat_sse(self, path: str, payload: dict[str, Any]) -> dict[str, Any]: | ||
| 265 | url = self._url(path) | ||
| 266 | response = self.session.post( | ||
| 267 | url, | ||
| 268 | json=payload, | ||
| 269 | timeout=self.timeout_seconds, | ||
| 270 | stream=True, | ||
| 271 | headers={"Accept": "text/event-stream"}, | ||
| 272 | ) | ||
| 273 | if response.status_code >= 400: | ||
| 274 | self._log_error("POST", url, response) | ||
| 275 | raise WeKnoraApiError(f"POST {url} failed with HTTP {response.status_code}") | ||
| 276 | |||
| 277 | answer_parts: list[str] = [] | ||
| 278 | references: list[dict[str, Any]] = [] | ||
| 279 | raw_events: list[dict[str, Any]] = [] | ||
| 280 | request_id: str | None = None | ||
| 281 | seen_reference_ids: set[str] = set() | ||
| 282 | |||
| 283 | for event in parse_sse_events(response.iter_lines(decode_unicode=True)): | ||
| 284 | raw_events.append(event) | ||
| 285 | data = event.get("data") | ||
| 286 | if not isinstance(data, dict): | ||
| 287 | continue | ||
| 288 | request_id = request_id or data.get("id") | ||
| 289 | response_type = data.get("response_type") | ||
| 290 | if response_type == "references": | ||
| 291 | for reference in data.get("knowledge_references") or []: | ||
| 292 | normalized = normalize_reference(reference) | ||
| 293 | reference_id = str(normalized.get("id") or "") | ||
| 294 | if reference_id and reference_id in seen_reference_ids: | ||
| 295 | continue | ||
| 296 | if reference_id: | ||
| 297 | seen_reference_ids.add(reference_id) | ||
| 298 | references.append(normalized) | ||
| 299 | elif response_type in {"answer", "final_answer"} and data.get("content"): | ||
| 300 | answer_parts.append(data.get("content") or "") | ||
| 301 | elif response_type == "error": | ||
| 302 | raise WeKnoraApiError(str(data.get("content") or data)) | ||
| 303 | |||
| 304 | retrieved_contexts = [ref["content"] for ref in references if ref.get("content")] | ||
| 305 | return { | ||
| 306 | "request_id": request_id, | ||
| 307 | "response": "".join(answer_parts).strip(), | ||
| 308 | "retrieved_contexts": retrieved_contexts, | ||
| 309 | "weknora_references": references, | ||
| 310 | "raw_events": raw_events, | ||
| 311 | } | ||
| 312 | |||
| 226 | def _paginate(self, path: str, *, page_size: int = 100) -> list[dict[str, Any]]: | 313 | def _paginate(self, path: str, *, page_size: int = 100) -> list[dict[str, Any]]: |
| 227 | page = 1 | 314 | page = 1 |
| 228 | rows: list[dict[str, Any]] = [] | 315 | rows: list[dict[str, Any]] = [] | ... | ... |
-
Please register or sign in to post a comment