GuXiaopeng commited on
Commit
b456fd4
·
verified ·
1 Parent(s): 41c2250

Update core/pipeline_agent.py

Browse files
Files changed (1) hide show
  1. core/pipeline_agent.py +53 -45
core/pipeline_agent.py CHANGED
@@ -14,13 +14,9 @@ import logging
14
  import concurrent.futures
15
  from tenacity import retry, stop_after_attempt, wait_random_exponential, retry_if_exception_type
16
 
17
- # 基础日志配置
18
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler(sys.stdout)])
19
 
20
- # 辅助函数:专门用于向 Hugging Face 容器输出强制刷新的彩色/醒目日志
21
- def hf_log(msg):
22
- print(f"[HF Container Log] {msg}", flush=True)
23
-
24
  try:
25
  from langdetect import detect
26
  LANGDETECT_AVAILABLE = True
@@ -74,7 +70,7 @@ class SSciPipelineAgent:
74
  except:
75
  self.tokenizer = tiktoken.get_encoding("gpt2")
76
 
77
- hf_log("✅ SSciPipelineAgent 初始化完成。三维检索节点与大语言模型已就绪。")
78
 
79
  def _extract_json_from_text(self, text):
80
  if not text:
@@ -100,7 +96,7 @@ class SSciPipelineAgent:
100
  else:
101
  return json.loads(text[start_list:end_list+1])
102
  except Exception as e:
103
- hf_log(f"❌ JSON 提取失败: {e}")
104
  return None
105
 
106
  def export_state(self):
@@ -124,7 +120,7 @@ class SSciPipelineAgent:
124
  self.citation_map = state_dict.get("citation_map", {})
125
  self.used_citations = set(state_dict.get("used_citations", []))
126
  self.global_memory = state_dict.get("global_memory", self.global_memory)
127
- hf_log(f"🔄 状态恢复成功: DS API={self.ds_calls}, 现有文献={len(self.citation_map)}")
128
 
129
  def _calc_paragraph_count(self, target_words, lang_hint="en"):
130
  words_per_para = 100 if lang_hint in ["ru", "kk"] else 130
@@ -143,7 +139,7 @@ class SSciPipelineAgent:
143
  resp = requests.get(url, headers=headers, params=params, timeout=timeout)
144
 
145
  if resp.status_code == 429:
146
- hf_log(f"⚠️ [API 限流] 触发 429 (Too Many Requests)。启动指数退避。URL: {url}")
147
  time.sleep(random.uniform(2.0, 5.0))
148
  resp.raise_for_status()
149
 
@@ -151,7 +147,7 @@ class SSciPipelineAgent:
151
  return resp
152
 
153
  if resp.status_code != 200:
154
- hf_log(f"❌ [API 错误] 状态码: {resp.status_code} | URL: {url}")
155
  resp.raise_for_status()
156
  return resp
157
 
@@ -174,13 +170,13 @@ class SSciPipelineAgent:
174
  hypo_matches = re.findall(r'(H\d+[:\s].*?)(?=\n|$)', text, re.IGNORECASE)
175
  if hypo_matches:
176
  self.global_memory["hypotheses"] = "\n".join(hypo_matches[:10])
177
- hf_log(f"🧠 [记忆更新] 从 {section_name} 中提取了假设 H1-Hn")
178
 
179
  if any(kw in lower_sec for kw in ["method", "measure", "variable"]):
180
  var_matches = re.findall(r'([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+(?:was|is|refers to)\s+([^\.]+\.)', text)
181
  if var_matches:
182
  self.global_memory["variables"] = "\n".join([f"- {v[0]}: {v[1][:100]}" for v in var_matches[:8]])
183
- hf_log(f"🧠 [记忆更新] 从 {section_name} 中提取了核心变量定义")
184
 
185
  if "model" in lower_sec or "figure" in lower_sec:
186
  model_match = re.search(r'(Figure\s*\d+[^\.]+\.)', text, re.IGNORECASE)
@@ -261,7 +257,7 @@ class SSciPipelineAgent:
261
 
262
  def call_deepseek(self, system_prompt, user_prompt, max_tokens=8000, temp=0.3, model_name="deepseek-reasoner"):
263
  tokens_est = self._count_tokens(user_prompt)
264
- hf_log(f"🚀 [DeepSeek API - 静态请求] 模型: {model_name} | MaxTokens: {max_tokens} | 输入估算: {tokens_est} tokens")
265
 
266
  for attempt in range(3):
267
  with self.lock:
@@ -270,7 +266,7 @@ class SSciPipelineAgent:
270
  if attempt == 2 and model_name == "deepseek-reasoner":
271
  current_model = "deepseek-chat"
272
  temp = 0.3
273
- hf_log("⚠️ 强制降级为 deepseek-chat 执行本节点!")
274
 
275
  payload = {
276
  "model": current_model,
@@ -286,7 +282,7 @@ class SSciPipelineAgent:
286
  try:
287
  with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["standard"]) as response:
288
  if response.status_code != 200:
289
- hf_log(f"❌ 尝试 {attempt+1}/3 失败: 状态码 {response.status_code} - {response.text[:200]}")
290
  if attempt == 2:
291
  raise requests.exceptions.HTTPError("API 熔断")
292
  time.sleep(3 ** attempt)
@@ -303,17 +299,17 @@ class SSciPipelineAgent:
303
  pass
304
 
305
  elapsed = time.time() - start_time
306
- hf_log(f"✅ [DeepSeek API - 返回] 耗时: {elapsed:.2f}s | 输出长度: {len(full_content)} 字符")
307
  return full_content.strip()
308
  except Exception as e:
309
- hf_log(f"❌ [网络异常] 尝试 {attempt+1}: {e}")
310
  if attempt == 2:
311
  raise e
312
  time.sleep(3 ** attempt)
313
 
314
  def stream_deepseek(self, system_prompt, user_prompt, max_tokens=8000, model_name="deepseek-reasoner"):
315
  tokens_est = self._count_tokens(user_prompt)
316
- hf_log(f"🌊 [DeepSeek API - 流式生成] 模型: {model_name} | 输入估算: {tokens_est} tokens")
317
 
318
  temp = 0.3 if model_name == "deepseek-chat" else 0.2
319
  for attempt in range(3):
@@ -323,7 +319,7 @@ class SSciPipelineAgent:
323
  if attempt >= 1 and model_name == "deepseek-reasoner":
324
  current_model = "deepseek-chat"
325
  temp = 0.3
326
- hf_log("⚠️ 推理节点异常,强制降级至 deepseek-chat 并重试。")
327
 
328
  payload = {
329
  "model": current_model,
@@ -334,14 +330,13 @@ class SSciPipelineAgent:
334
  if current_model != "deepseek-reasoner":
335
  payload["temperature"] = temp
336
 
337
- think_heartbeat = 0
338
  has_started_reasoning = False
339
  start_time = time.time()
340
 
341
  try:
342
  with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["stream"]) as response:
343
  if response.status_code != 200:
344
- hf_log(f"❌ 流式尝试 {attempt+1}/3 失败: 状态码 {response.status_code}")
345
  if attempt == 2:
346
  response.raise_for_status()
347
  time.sleep(2)
@@ -359,14 +354,13 @@ class SSciPipelineAgent:
359
  if not has_started_reasoning:
360
  yield "\n<think>\n"
361
  has_started_reasoning = True
362
- hf_log(f"🤔 [R1 推理启动] 开始输出深度思考块...")
363
 
364
- think_heartbeat += 1
365
-
366
- # 针对 Hugging Face 容器底层的实时心跳探针,直接 flush 到 stdout
367
- if think_heartbeat % 25 == 0:
368
- elapsed = time.time() - start_time
369
- hf_log(f"⏳ [DeepSeek 心跳] {current_model} 思考中... ({elapsed:.1f}秒, 接收 {think_heartbeat} 块)")
370
 
371
  yield reasoning_content
372
  continue
@@ -375,19 +369,34 @@ class SSciPipelineAgent:
375
  yield "\n</think>\n\n"
376
  has_started_reasoning = False
377
  elapsed = time.time() - start_time
378
- hf_log(f"💡 [R1 推理结束] 思考完成,总耗时 {elapsed:.1f}秒。开始流式输出正文...")
 
 
 
 
379
 
380
  if content:
 
 
 
 
381
  yield content
382
 
383
  except Exception:
384
  pass
385
- hf_log(f"🏁 [流式生成结束] 总耗时 {(time.time() - start_time):.1f}秒。")
 
 
 
 
386
  return
387
  except GeneratorExit:
388
  raise
389
  except Exception as e:
390
- hf_log(f"❌ [流式中断]: {e}")
 
 
 
391
  if attempt == 2:
392
  raise
393
  time.sleep(2)
@@ -425,7 +434,7 @@ class SSciPipelineAgent:
425
 
426
  for attempt in range(2):
427
  try:
428
- hf_log("📄 [本地解析] 正在榨取用户上传的核心文献...")
429
  response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1, model_name="deepseek-chat")
430
  papers = self._extract_json_from_text(response)
431
  if not papers:
@@ -467,7 +476,7 @@ class SSciPipelineAgent:
467
  self.used_citations.add(cite_tag)
468
 
469
  rag_context.append(f"{cite_tag} [USER CORE PAPER] Title: {title}. Core Thesis: {core_thesis}. Abstract: {abstract}")
470
- hf_log(f"✅ [本地解析] 成功提取 {len(papers)} 篇本地文献元数据。")
471
  return "\n".join(rag_context)
472
  except Exception as e:
473
  time.sleep(2)
@@ -514,7 +523,6 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
514
  raise ValueError("Outline Generation Failed.")
515
 
516
  def prefetch_jit_literature(self, topic, keywords, structure):
517
- """Pre-fetch JIT: 全局并发预抓取所有方法与结果章节所需支撑文献"""
518
  jit_rag_texts = []
519
  queries_to_fetch = set()
520
 
@@ -529,7 +537,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
529
  if not queries_to_fetch:
530
  return ""
531
 
532
- hf_log(f"🔍 [Pre-fetch JIT] 生成了 {len(queries_to_fetch)} 个定向检索词: {queries_to_fetch}")
533
 
534
  with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
535
  futures = []
@@ -546,7 +554,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
546
  self.citation_map[cite_tag] = {"in_text": res["in_text"], "ref": res["ref"], "id": res["id"]}
547
  jit_rag_texts.append(f"{cite_tag} {res['rag_text']}")
548
 
549
- hf_log(f"✅ [Pre-fetch JIT] 成功抓取并注入了 {len(jit_rag_texts)} 篇底层支撑文献。")
550
  return "\n".join(jit_rag_texts)
551
 
552
  def generate_search_queries(self, topic, keywords):
@@ -567,7 +575,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
567
  time.sleep(random.uniform(0.5, 1.5))
568
  headers = {"x-api-key": str(self.s2_key).strip()} if self.s2_key else {}
569
  results = []
570
- hf_log(f"📡 [S2 Worker] 正在 Semantic Scholar 检索: '{term}' (offset={offset})")
571
  try:
572
  resp = self._requests_get_with_retry(
573
  f"{self.s2_api_url}/paper/search",
@@ -576,7 +584,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
576
  )
577
  if resp.status_code == 400: return results
578
  papers = resp.json().get("data", [])
579
- hf_log(f"✅ [S2 Worker] '{term}' 检索完成,捕获 {len(papers)} 条原始数据")
580
  for paper in papers:
581
  title = paper.get("title")
582
  year = paper.get("year")
@@ -617,7 +625,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
617
  "source": "s2"
618
  })
619
  except Exception as e:
620
- hf_log(f"⚠️ S2 Worker 异常 (term: {term}): {e}")
621
  return results
622
 
623
  def _oa_worker(self, term, page, filter_str):
@@ -625,7 +633,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
625
  headers = {"User-Agent": f"mailto:ssci_researcher_{random.randint(10000, 99999)}@gmail.com"}
626
  if self.openalex_key: headers["Authorization"] = f"Bearer {self.openalex_key}"
627
  results = []
628
- hf_log(f"🌐 [OA Worker] 正在 OpenAlex 检索: '{term}' (page={page})")
629
  try:
630
  resp = self._requests_get_with_retry(
631
  self.openalex_api_url,
@@ -633,7 +641,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
633
  params={"search": term, "per-page": self.lit_per_page, "page": page, "filter": filter_str}
634
  )
635
  raw_items = resp.json().get("results", [])
636
- hf_log(f"✅ [OA Worker] '{term}' 检索完成,捕获 {len(raw_items)} 条原始数据")
637
  for item in raw_items:
638
  title = item.get("title")
639
  if not title or not self._is_english_dominant(title): continue
@@ -676,7 +684,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
676
  "source": "oa"
677
  })
678
  except Exception as e:
679
- hf_log(f"⚠️ OA Worker 异常 (term: {term}): {e}")
680
  return results
681
 
682
  def fetch_references_parallel(self, topic, keywords, min_required=50, progress_callback=None):
@@ -693,7 +701,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
693
  if expansion_phase == 0:
694
  search_terms = self.generate_search_queries(topic, keywords)
695
  else:
696
- hf_log(f"🔄 [文献检索] 阶段 {expansion_phase}: 数量不足 ({len(self.citation_map)}/{min_required}),正在扩大检索范围...")
697
  sys_prompt = "You are an expert academic librarian. We must broaden the search scope."
698
  user_prompt = f"Topic: {topic}\nKeywords: {keywords}\nUsed Queries: {list(all_used_terms)}\nGenerate 3 NEW, BROADER English search queries. Return ONLY a JSON array of strings."
699
  response = self.call_deepseek(sys_prompt, user_prompt, temp=0.5, model_name="deepseek-chat")
@@ -707,7 +715,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
707
  if not current_batch_terms: continue
708
  for t in current_batch_terms: all_used_terms.add(t)
709
 
710
- hf_log(f"🚀 [并发下发] 启动扩张阶段 {expansion_phase+1}, 关键词: {current_batch_terms}")
711
 
712
  with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
713
  futures = []
@@ -743,7 +751,7 @@ You MUST output your response EXACTLY using the following two delimiters. Do not
743
  if len(self.citation_map) >= self.lit_target_papers:
744
  break
745
 
746
- hf_log(f"🎯 [并发检索结束] 成功入库并排重了 {len(self.citation_map)} 篇核心文献。")
747
  return "\n".join(llm_rag_context)
748
 
749
  def resolve_citations(self, text, quarantine=False):
 
14
  import concurrent.futures
15
  from tenacity import retry, stop_after_attempt, wait_random_exponential, retry_if_exception_type
16
 
17
+ # 基础日志配置:自带时间戳和日志级别
18
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler(sys.stdout)])
19
 
 
 
 
 
20
  try:
21
  from langdetect import detect
22
  LANGDETECT_AVAILABLE = True
 
70
  except:
71
  self.tokenizer = tiktoken.get_encoding("gpt2")
72
 
73
+ logging.info("✅ SSciPipelineAgent 初始化完成。三维检索节点与大语言模型已就绪。")
74
 
75
  def _extract_json_from_text(self, text):
76
  if not text:
 
96
  else:
97
  return json.loads(text[start_list:end_list+1])
98
  except Exception as e:
99
+ logging.error(f"❌ JSON 提取失败: {e}")
100
  return None
101
 
102
  def export_state(self):
 
120
  self.citation_map = state_dict.get("citation_map", {})
121
  self.used_citations = set(state_dict.get("used_citations", []))
122
  self.global_memory = state_dict.get("global_memory", self.global_memory)
123
+ logging.info(f"🔄 状态恢复成功: DS API={self.ds_calls}, 现有文献={len(self.citation_map)}")
124
 
125
  def _calc_paragraph_count(self, target_words, lang_hint="en"):
126
  words_per_para = 100 if lang_hint in ["ru", "kk"] else 130
 
139
  resp = requests.get(url, headers=headers, params=params, timeout=timeout)
140
 
141
  if resp.status_code == 429:
142
+ logging.warning(f"⚠️ [API 限流] 触发 429 (Too Many Requests)。启动指数退避。URL: {url}")
143
  time.sleep(random.uniform(2.0, 5.0))
144
  resp.raise_for_status()
145
 
 
147
  return resp
148
 
149
  if resp.status_code != 200:
150
+ logging.error(f"❌ [API 错误] 状态码: {resp.status_code} | URL: {url}")
151
  resp.raise_for_status()
152
  return resp
153
 
 
170
  hypo_matches = re.findall(r'(H\d+[:\s].*?)(?=\n|$)', text, re.IGNORECASE)
171
  if hypo_matches:
172
  self.global_memory["hypotheses"] = "\n".join(hypo_matches[:10])
173
+ logging.info(f"🧠 [记忆更新] 从 {section_name} 中提取了假设 H1-Hn")
174
 
175
  if any(kw in lower_sec for kw in ["method", "measure", "variable"]):
176
  var_matches = re.findall(r'([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\s+(?:was|is|refers to)\s+([^\.]+\.)', text)
177
  if var_matches:
178
  self.global_memory["variables"] = "\n".join([f"- {v[0]}: {v[1][:100]}" for v in var_matches[:8]])
179
+ logging.info(f"🧠 [记忆更新] 从 {section_name} 中提取了核心变量定义")
180
 
181
  if "model" in lower_sec or "figure" in lower_sec:
182
  model_match = re.search(r'(Figure\s*\d+[^\.]+\.)', text, re.IGNORECASE)
 
257
 
258
  def call_deepseek(self, system_prompt, user_prompt, max_tokens=8000, temp=0.3, model_name="deepseek-reasoner"):
259
  tokens_est = self._count_tokens(user_prompt)
260
+ logging.info(f"🚀 [DeepSeek 静态请求] 模型: {model_name} | MaxTokens: {max_tokens} | 输入估算: {tokens_est} tokens")
261
 
262
  for attempt in range(3):
263
  with self.lock:
 
266
  if attempt == 2 and model_name == "deepseek-reasoner":
267
  current_model = "deepseek-chat"
268
  temp = 0.3
269
+ logging.warning("⚠️ 强制降级为 deepseek-chat 执行本节点!")
270
 
271
  payload = {
272
  "model": current_model,
 
282
  try:
283
  with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["standard"]) as response:
284
  if response.status_code != 200:
285
+ logging.error(f"❌ 尝试 {attempt+1}/3 失败: 状态码 {response.status_code} - {response.text[:200]}")
286
  if attempt == 2:
287
  raise requests.exceptions.HTTPError("API 熔断")
288
  time.sleep(3 ** attempt)
 
299
  pass
300
 
301
  elapsed = time.time() - start_time
302
+ logging.info(f"✅ [静态请求返回] 耗时: {elapsed:.2f}s | 输出长度: {len(full_content)} 字符")
303
  return full_content.strip()
304
  except Exception as e:
305
+ logging.error(f"❌ [网络异常] 尝试 {attempt+1}: {e}")
306
  if attempt == 2:
307
  raise e
308
  time.sleep(3 ** attempt)
309
 
310
  def stream_deepseek(self, system_prompt, user_prompt, max_tokens=8000, model_name="deepseek-reasoner"):
311
  tokens_est = self._count_tokens(user_prompt)
312
+ logging.info(f"🌊 [DeepSeek 流式请求] 模型: {model_name} | 输入估算: {tokens_est} tokens")
313
 
314
  temp = 0.3 if model_name == "deepseek-chat" else 0.2
315
  for attempt in range(3):
 
319
  if attempt >= 1 and model_name == "deepseek-reasoner":
320
  current_model = "deepseek-chat"
321
  temp = 0.3
322
+ logging.warning("⚠️ 推理节点异常,强制降级至 deepseek-chat 并重试。")
323
 
324
  payload = {
325
  "model": current_model,
 
330
  if current_model != "deepseek-reasoner":
331
  payload["temperature"] = temp
332
 
 
333
  has_started_reasoning = False
334
  start_time = time.time()
335
 
336
  try:
337
  with requests.post(self.deepseek_api_url, headers={"Authorization": f"Bearer {self.ds_key}"}, json=payload, stream=True, timeout=self.timeouts["stream"]) as response:
338
  if response.status_code != 200:
339
+ logging.error(f"❌ 流式尝试 {attempt+1}/3 失败: 状态码 {response.status_code}")
340
  if attempt == 2:
341
  response.raise_for_status()
342
  time.sleep(2)
 
354
  if not has_started_reasoning:
355
  yield "\n<think>\n"
356
  has_started_reasoning = True
357
+ logging.info(f"🤔 [R1 推理启动] 开始在终端流式输出思考过程...")
358
 
359
+ # ---------------------------------------------------------
360
+ # 🔥 核心修改点:直接向终端输出内容并强制刷新,实现无延迟的打字机效果
361
+ # ---------------------------------------------------------
362
+ sys.stdout.write(reasoning_content)
363
+ sys.stdout.flush()
 
364
 
365
  yield reasoning_content
366
  continue
 
369
  yield "\n</think>\n\n"
370
  has_started_reasoning = False
371
  elapsed = time.time() - start_time
372
+
373
+ # 思考结束,终端换行,并记录日志
374
+ sys.stdout.write("\n")
375
+ sys.stdout.flush()
376
+ logging.info(f"💡 [R1 推理结束] 耗时 {elapsed:.1f}秒。开始在终端流式输出正文...")
377
 
378
  if content:
379
+ # 🔥 正文内容同样直接推送到终端流并强制刷新
380
+ sys.stdout.write(content)
381
+ sys.stdout.flush()
382
+
383
  yield content
384
 
385
  except Exception:
386
  pass
387
+
388
+ # 整个流结束,终端换行
389
+ sys.stdout.write("\n")
390
+ sys.stdout.flush()
391
+ logging.info(f"🏁 [流式生成结束] 总耗时 {(time.time() - start_time):.1f}秒。")
392
  return
393
  except GeneratorExit:
394
  raise
395
  except Exception as e:
396
+ # 异常中断时也要确保换行,防止日志排版错乱
397
+ sys.stdout.write("\n")
398
+ sys.stdout.flush()
399
+ logging.error(f"❌ [流式中断]: {e}")
400
  if attempt == 2:
401
  raise
402
  time.sleep(2)
 
434
 
435
  for attempt in range(2):
436
  try:
437
+ logging.info("📄 [本地解析] 正在榨取用户上传的核心文献...")
438
  response = self.call_deepseek(sys_prompt, user_prompt, temp=0.1, model_name="deepseek-chat")
439
  papers = self._extract_json_from_text(response)
440
  if not papers:
 
476
  self.used_citations.add(cite_tag)
477
 
478
  rag_context.append(f"{cite_tag} [USER CORE PAPER] Title: {title}. Core Thesis: {core_thesis}. Abstract: {abstract}")
479
+ logging.info(f"✅ [本地解析] 成功提取 {len(papers)} 篇本地文献元数据。")
480
  return "\n".join(rag_context)
481
  except Exception as e:
482
  time.sleep(2)
 
523
  raise ValueError("Outline Generation Failed.")
524
 
525
  def prefetch_jit_literature(self, topic, keywords, structure):
 
526
  jit_rag_texts = []
527
  queries_to_fetch = set()
528
 
 
537
  if not queries_to_fetch:
538
  return ""
539
 
540
+ logging.info(f"🔍 [Pre-fetch JIT] 生成了 {len(queries_to_fetch)} 个定向检索词: {queries_to_fetch}")
541
 
542
  with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
543
  futures = []
 
554
  self.citation_map[cite_tag] = {"in_text": res["in_text"], "ref": res["ref"], "id": res["id"]}
555
  jit_rag_texts.append(f"{cite_tag} {res['rag_text']}")
556
 
557
+ logging.info(f"✅ [Pre-fetch JIT] 成功抓取并注入了 {len(jit_rag_texts)} 篇底层支撑文献。")
558
  return "\n".join(jit_rag_texts)
559
 
560
  def generate_search_queries(self, topic, keywords):
 
575
  time.sleep(random.uniform(0.5, 1.5))
576
  headers = {"x-api-key": str(self.s2_key).strip()} if self.s2_key else {}
577
  results = []
578
+ logging.info(f"📡 [S2 Worker] 正在 Semantic Scholar 检索: '{term}' (offset={offset})")
579
  try:
580
  resp = self._requests_get_with_retry(
581
  f"{self.s2_api_url}/paper/search",
 
584
  )
585
  if resp.status_code == 400: return results
586
  papers = resp.json().get("data", [])
587
+ logging.info(f"✅ [S2 Worker] '{term}' 检索完成,捕获 {len(papers)} 条原始数据")
588
  for paper in papers:
589
  title = paper.get("title")
590
  year = paper.get("year")
 
625
  "source": "s2"
626
  })
627
  except Exception as e:
628
+ logging.warning(f"⚠️ S2 Worker 异常 (term: {term}): {e}")
629
  return results
630
 
631
  def _oa_worker(self, term, page, filter_str):
 
633
  headers = {"User-Agent": f"mailto:ssci_researcher_{random.randint(10000, 99999)}@gmail.com"}
634
  if self.openalex_key: headers["Authorization"] = f"Bearer {self.openalex_key}"
635
  results = []
636
+ logging.info(f"🌐 [OA Worker] 正在 OpenAlex 检索: '{term}' (page={page})")
637
  try:
638
  resp = self._requests_get_with_retry(
639
  self.openalex_api_url,
 
641
  params={"search": term, "per-page": self.lit_per_page, "page": page, "filter": filter_str}
642
  )
643
  raw_items = resp.json().get("results", [])
644
+ logging.info(f"✅ [OA Worker] '{term}' 检索完成,捕获 {len(raw_items)} 条原始数据")
645
  for item in raw_items:
646
  title = item.get("title")
647
  if not title or not self._is_english_dominant(title): continue
 
684
  "source": "oa"
685
  })
686
  except Exception as e:
687
+ logging.warning(f"⚠️ OA Worker 异常 (term: {term}): {e}")
688
  return results
689
 
690
  def fetch_references_parallel(self, topic, keywords, min_required=50, progress_callback=None):
 
701
  if expansion_phase == 0:
702
  search_terms = self.generate_search_queries(topic, keywords)
703
  else:
704
+ logging.info(f"🔄 [文献检索] 阶段 {expansion_phase}: 数量不足 ({len(self.citation_map)}/{min_required}),正在扩大检索范围...")
705
  sys_prompt = "You are an expert academic librarian. We must broaden the search scope."
706
  user_prompt = f"Topic: {topic}\nKeywords: {keywords}\nUsed Queries: {list(all_used_terms)}\nGenerate 3 NEW, BROADER English search queries. Return ONLY a JSON array of strings."
707
  response = self.call_deepseek(sys_prompt, user_prompt, temp=0.5, model_name="deepseek-chat")
 
715
  if not current_batch_terms: continue
716
  for t in current_batch_terms: all_used_terms.add(t)
717
 
718
+ logging.info(f"🚀 [并发下发] 启动扩张阶段 {expansion_phase+1}, 关键词: {current_batch_terms}")
719
 
720
  with concurrent.futures.ThreadPoolExecutor(max_workers=self.lit_max_workers) as executor:
721
  futures = []
 
751
  if len(self.citation_map) >= self.lit_target_papers:
752
  break
753
 
754
+ logging.info(f"🎯 [并发检索结束] 成功入库并排重了 {len(self.citation_map)} 篇核心文献。")
755
  return "\n".join(llm_rag_context)
756
 
757
  def resolve_citations(self, text, quarantine=False):