Files
yu-zhi-ran/scripts/research.py
T

202 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
选题研究:信息收集与结构化整理
基于选题方向,收集相关数据/案例/趋势,输出结构化研究笔记供大纲生成使用
"""
import json, datetime, logging, sys, re
from pathlib import Path
from typing import Dict, List
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
sys.path.insert(0, str(PROJECT_ROOT / "platform" / "backend"))
from db_helper import get_topic_by_id
try:
from app.core.nvidia_client import call_llm
HAVE_LLM = True
except ImportError:
HAVE_LLM = False
from trends import get_trend_context
from web_search import enrich_topic_research
DATA_DIR = PROJECT_ROOT / "automation" / "data"
CASES_FILE = DATA_DIR / "sustainability_cases.json"
OUTPUT_DIR = DATA_DIR / "research"
LOGS_DIR = PROJECT_ROOT / "automation" / "logs"
TODAY = datetime.datetime.now().strftime("%Y-%m-%d")
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.FileHandler(LOGS_DIR / f"research_{TODAY}.log"), logging.StreamHandler()])
logger = logging.getLogger(__name__)
class Researcher:
def __init__(self, topic_id: str):
self.topic_id = topic_id
self.topic = self._load_topic()
self.cases = self._load_cases()
self.output_dir = OUTPUT_DIR / TODAY
self.output_dir.mkdir(parents=True, exist_ok=True)
def _load_topic(self) -> Dict:
topic = get_topic_by_id(self.topic_id)
if not topic:
raise ValueError(f"Topic {self.topic_id} not found")
return topic
def _load_cases(self) -> List[Dict]:
if CASES_FILE.exists():
return json.loads(CASES_FILE.read_text(encoding='utf-8'))
return []
def find_relevant_cases(self, top_k: int = 5) -> List[Dict]:
field = self.topic.get('field', '').lower()
title = self.topic.get('title', '').lower()
scored = []
for case in self.cases:
case_date = case.get('date', '')
if case_date:
m = re.search(r'(\d{4})', str(case_date))
if m and int(m.group(1)) < 2025:
continue
score = 0
if field and field in case.get('field', '').lower():
score += 3
case_title = case.get('title', '').lower()
for word in title.split():
if len(word) > 2 and word in case_title:
score += 1
if score > 0:
scored.append((score, case))
scored.sort(key=lambda x: x[0], reverse=True)
return [c for _, c in scored[:top_k]]
def _llm_summary(self, cases: List[Dict]) -> str:
if not HAVE_LLM:
return ""
cases_text = json.dumps(cases, ensure_ascii=False, indent=2)
# 获取实时搜索数据作为 LLM 参考
search_data = enrich_topic_research(self.topic)
search_section = f"\n## 实时搜索结果\n{search_data}\n" if search_data else ""
_now = datetime.datetime.now()
prompt = f"""你是一个行业研究员+内容策略师,擅长从案例中发现真洞察+抢占热点的敏锐嗅觉,能判断什么内容对真实读者最有价值且正被市场热议。
⚠️ 今天日期:{_now.strftime('%Y年%m月%d')}。当前年份:{_now.year}年。
基于以下选题和相关案例,写出能支撑文章核心观点、对读者真正有用的研究发现。
## 选题
标题:{self.topic['title']}
领域:{self.topic.get('field', '')}
核心观点:{self.topic.get('core_concept', '')}
受众痛点:{self.topic.get('audience_pain', '')}
独特视角:{self.topic.get('unique_angle', '')}
{search_section}
## 相关案例({len(cases)}个)
{cases_text}
## 输出要求(按顺序):
1. **国内外最新热点关联**:当前该领域正在讨论什么、国内外哪些事件/政策/数据在发酵、为什么现在这个话题值得关注。**所有数据必须是{_now.year-1}-{_now.year}年最新数据,禁用一切过时数据**
2. **核心发现**:2-3个真正有价值的洞察。每条需包含这个发现对读者意味着什么,以及支撑数据(附数据来源)
3. **独特观点储备**:哪些角度别人没写过、可以讲出差异化?提供至少一个反向/冷门视角
4. **SEO关键词建议**:重点布局哪些搜索词(3-5个,含1-2个长尾词),以及各平台近期搜索上升趋势
5. **讨论点**:哪个观点最有争议或最可能引发讨论/转发/评论?
6. **市场价值判断**:读完这篇文章,读者能得到什么实际改变/方案/能力?
风格:说人话,直击要点,像资深编辑在给作者做 briefing。避免「首先其次最后」「综上所述」。直接输出内容,不要输出思考过程。"""
try:
return call_llm(prompt, temperature=0.5, system_prompt="你是一个行业研究员,擅长从案例中发现真洞察。")
except Exception as e:
logger.warning(f"LLM 研究发现摘要生成失败: {e}")
return ""
def generate_notes(self) -> str:
cases = self.find_relevant_cases()
trend_context = get_trend_context(self.topic.get('field'))
search_data = enrich_topic_research(self.topic)
lines = [
f"# 研究笔记:{self.topic['title']}",
f"\n## 选题信息",
f"- **ID**: {self.topic['id']}",
f"- **领域**: {self.topic.get('field')}",
f"- **核心观点**: {self.topic.get('core_concept', '待补充')}",
f"- **受众痛点**: {self.topic.get('audience_pain', '待补充')}",
f"- **独特视角**: {self.topic.get('unique_angle', '待补充')}",
f"\n{trend_context}",
f"\n{search_data}" if search_data else "",
f"\n## 相关案例({len(cases)}个)\n"
]
for i, case in enumerate(cases, 1):
lines.extend([
f"### 案例 {i}: {case.get('title')}",
f"- **来源**: {case.get('source', '未知')}",
f"- **日期**: {case.get('date', '未知')}",
f"- **摘要**: {case.get('summary', case.get('description', ''))}",
f"- **关键数据**: {case.get('key_metrics', '')}",
""
])
llm_summary = self._llm_summary(cases)
if llm_summary:
lines.extend([
"## 研究发现摘要(LLM 生成)",
llm_summary,
""
])
else:
insights = []
for case in cases[:3]:
summary = case.get('summary', case.get('description', ''))
metrics = case.get('key_metrics', '')
if summary:
insight = f"- {case.get('title', '相关案例')}{summary[:100]}"
if metrics:
insight += f"{metrics[:80]}"
insights.append(insight)
if not insights:
insights = ["- 暂未匹配到高度相关的历史案例"]
pain_text = self.topic.get('audience_pain', '')
topic_insights = [f"- {pain_text[:100]}"] if pain_text else []
lines.extend([
"## 研究发现摘要",
"",
])
lines.extend(insights)
if topic_insights:
lines.extend(topic_insights)
lines.extend([
"",
"## 待深入研究的问题",
])
if cases:
lines.append("- [ ] 验证以上案例在当前选题背景下的适用性")
lines.extend([
"- [ ] 收集更多本土一手数据",
"- [ ] 确认目标受众的实际反馈",
""
])
lines.append(f"*生成时间:{TODAY}*")
return "\n".join(lines)
def save(self):
notes = self.generate_notes()
out_path = self.output_dir / f"{self.topic_id}_research.md"
out_path.write_text(notes, encoding='utf-8')
logger.info(f"研究笔记已保存: {out_path}")
return out_path
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--topic-id', required=True, help='选题ID')
args = parser.parse_args()
r = Researcher(args.topic_id)
r.save()
print(f"SUCCESS: Research notes created for {args.topic_id}")
sys.exit(0)
if __name__ == "__main__":
main()