Commit d808af81 d808af81eba4ee3cf7b1ef5ee64e29b0b0d9df06 by 沈秋雨

评估使用agent模式检索

1 parent 7ba212d1
...@@ -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]] = []
......