Portkey AI Gateway実践ガイド2025 - 250+ LLMを統一APIで管理
Portkey AI Gatewayを使って、OpenAI、Anthropic、Google、Cohere等250以上のLLMを統一APIで管理する方法を徹底解説。負荷分散、フォールバック、コスト管理、セキュリティまで、エンタープライズ向けAI基盤の構築方法を紹介します。
スタンフォード大学が開発したSTORMシステムの使い方を徹底解説。LLMを活用して信頼性の高い知識記事を自動生成する方法から、カスタマイズ、実装例まで詳しく紹介します。
信頼性の高い知識記事を自動生成したいと思ったことはありませんか?スタンフォード大学が開発した STORM(Synthesis of Topic Outline through Retrieval and Multi-perspective question asking)は、LLM を活用して高品質な知識記事を自動生成する革新的なシステムです。
STORM は、複数の視点から質問を生成し、検索と合成を繰り返すことで、包括的で信頼性の高い知識記事を自動生成するシステムです。従来の LLM 生成と異なり、事実の正確性と多角的な視点を重視しています。
特徴 | 従来のLLM生成 | STORM | 改善点 |
---|---|---|---|
情報源 | 学習データのみ | 検索+学習データ | 最新情報対応 |
視点 | 単一視点 | マルチパースペクティブ | 包括性向上 |
事実確認 | 限定的 | ソース引用付き | 信頼性向上 |
構造化 | 基本的 | 階層的アウトライン | 読みやすさ向上 |
更新性 | 静的 | 動的更新可能 | 鮮度維持 |
カスタマイズ | 困難 | 柔軟に対応 | 用途別最適化 |
STORM は、人間のリサーチャーが行う知識収集プロセスを模倣し、AI による知識生成の新たな可能性を開きます。
チャートを読み込み中...
# STORMのインストール
pip install knowledge-storm
# 依存関係のインストール
pip install openai anthropic google-search-results
pip install beautifulsoup4 requests aiohttp
pip install numpy pandas tqdm
# 開発用追加パッケージ
pip install jupyter notebook
pip install python-dotenv
# config.py
import os
from dotenv import load_dotenv
load_dotenv()
class StormConfig:
# LLM設定
LLM_PROVIDER = "openai" # "openai", "anthropic", "local"
LLM_MODEL = "gpt-4"
LLM_TEMPERATURE = 0.7
# 検索エンジン設定
SEARCH_ENGINE = "google" # "google", "bing", "duckduckgo"
SEARCH_API_KEY = os.getenv("SEARCH_API_KEY")
MAX_SEARCH_RESULTS = 10
# STORM設定
MAX_PERSPECTIVES = 5 # 生成するペルソナ数
MAX_QUESTIONS_PER_PERSPECTIVE = 3
MAX_OUTLINE_DEPTH = 3
MIN_SECTION_LENGTH = 200
MAX_SECTION_LENGTH = 1000
# 出力設定
OUTPUT_FORMAT = "markdown" # "markdown", "html", "json"
INCLUDE_CITATIONS = True
INCLUDE_IMAGES = True
# API Keys
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
# api_setup.py
from knowledge_storm import Storm
from knowledge_storm.llm import OpenAIModel, AnthropicModel
from knowledge_storm.search import GoogleSearch, BingSearch
def setup_storm_api(config: StormConfig):
"""STORM APIの初期設定"""
# LLMモデルの設定
if config.LLM_PROVIDER == "openai":
llm = OpenAIModel(
api_key=config.OPENAI_API_KEY,
model=config.LLM_MODEL,
temperature=config.LLM_TEMPERATURE
)
elif config.LLM_PROVIDER == "anthropic":
llm = AnthropicModel(
api_key=config.ANTHROPIC_API_KEY,
model="claude-3-opus",
temperature=config.LLM_TEMPERATURE
)
# 検索エンジンの設定
if config.SEARCH_ENGINE == "google":
search = GoogleSearch(
api_key=config.SEARCH_API_KEY,
max_results=config.MAX_SEARCH_RESULTS
)
elif config.SEARCH_ENGINE == "bing":
search = BingSearch(
api_key=config.SEARCH_API_KEY,
max_results=config.MAX_SEARCH_RESULTS
)
# STORMインスタンスの作成
storm = Storm(
llm=llm,
search_engine=search,
config=config
)
return storm
# test_storm.py
import asyncio
from knowledge_storm import Storm
async def test_storm_generation():
"""STORMの基本動作テスト"""
# 設定読み込み
config = StormConfig()
storm = setup_storm_api(config)
# テストトピック
topic = "量子コンピューティングの現状と未来"
print(f"Generating article for: {topic}")
print("-" * 50)
try:
# 記事生成
article = await storm.generate_article(
topic=topic,
language="ja",
target_length=2000
)
# 結果表示
print(f"Title: {article.title}")
print(f"Sections: {len(article.sections)}")
print(f"Total words: {article.word_count}")
print(f"Citations: {len(article.citations)}")
# 最初のセクションを表示
if article.sections:
first_section = article.sections[0]
print(f"\nFirst section: {first_section.title}")
print(first_section.content[:200] + "...")
return article
except Exception as e:
print(f"Error: {e}")
return None
# テスト実行
if __name__ == "__main__":
asyncio.run(test_storm_generation())
import asyncio
from typing import List, Dict, Optional
from dataclasses import dataclass
import json
@dataclass
class Perspective:
"""視点/ペルソナを表すクラス"""
name: str
description: str
expertise: List[str]
questions: List[str]
@dataclass
class ArticleSection:
"""記事のセクションを表すクラス"""
title: str
content: str
subsections: List['ArticleSection']
citations: List[Dict[str, str]]
level: int
class StormKnowledgeGenerator:
def __init__(self, llm, search_engine, config):
self.llm = llm
self.search = search_engine
self.config = config
async def generate_article(self, topic: str, **kwargs) -> Dict:
"""完全な記事を生成"""
# 1. ペルソナ生成
perspectives = await self.generate_perspectives(topic)
# 2. 質問生成
all_questions = await self.generate_questions(topic, perspectives)
# 3. 情報収集
search_results = await self.collect_information(all_questions)
# 4. アウトライン生成
outline = await self.generate_outline(topic, search_results)
# 5. セクション執筆
sections = await self.write_sections(outline, search_results)
# 6. 引用整理
article = self.compile_article(topic, sections, search_results)
return article
async def generate_perspectives(self, topic: str) -> List[Perspective]:
"""多様な視点を生成"""
prompt = f"""
トピック「{topic}」について、異なる視点を持つ{self.config.MAX_PERSPECTIVES}人の
専門家ペルソナを生成してください。
各ペルソナには以下を含めてください:
1. 名前と肩書き
2. 専門分野
3. このトピックへの関心事項
JSON形式で出力してください。
"""
response = await self.llm.generate(prompt)
personas_data = json.loads(response)
perspectives = []
for persona in personas_data:
perspectives.append(Perspective(
name=persona['name'],
description=persona['description'],
expertise=persona['expertise'],
questions=[]
))
return perspectives
async def generate_questions(self, topic: str,
perspectives: List[Perspective]) -> List[Dict]:
"""各視点から質問を生成"""
all_questions = []
for perspective in perspectives:
prompt = f"""
あなたは{perspective.name}({perspective.description})です。
トピック「{topic}」について、あなたの専門分野から
{self.config.MAX_QUESTIONS_PER_PERSPECTIVE}個の重要な質問を生成してください。
質問は具体的で、検索可能な形式にしてください。
"""
response = await self.llm.generate(prompt)
questions = response.strip().split('
')
for question in questions:
if question.strip():
all_questions.append({
'question': question.strip(),
'perspective': perspective.name,
'expertise': perspective.expertise
})
return all_questions
async def collect_information(self, questions: List[Dict]) -> Dict:
"""質問に基づいて情報を収集"""
search_results = {}
# 並列検索
tasks = []
for q_data in questions:
task = self.search_and_extract(q_data)
tasks.append(task)
results = await asyncio.gather(*tasks)
for q_data, result in zip(questions, results):
search_results[q_data['question']] = {
'answer': result['answer'],
'sources': result['sources'],
'perspective': q_data['perspective']
}
return search_results
async def search_and_extract(self, question_data: Dict) -> Dict:
"""単一の質問に対して検索と情報抽出"""
question = question_data['question']
# 検索実行
search_results = await self.search.search(question)
# 関連情報の抽出
context = self._extract_relevant_content(search_results)
# LLMで回答生成
prompt = f"""
質問: {question}
以下の情報源を基に、正確で包括的な回答を生成してください:
{context}
回答には必ず情報源を明記してください。
"""
answer = await self.llm.generate(prompt)
return {
'answer': answer,
'sources': [{'url': r['url'], 'title': r['title']}
for r in search_results[:3]]
}
async def generate_outline(self, topic: str, search_results: Dict) -> Dict:
"""記事のアウトラインを生成"""
# 収集した情報をまとめる
info_summary = self._summarize_information(search_results)
prompt = f"""
トピック「{topic}」について、以下の情報を基に
包括的な記事のアウトラインを生成してください。
収集情報サマリー:
{info_summary}
アウトラインは最大{self.config.MAX_OUTLINE_DEPTH}階層で、
論理的な流れを持つように構成してください。
JSON形式で出力してください。
"""
response = await self.llm.generate(prompt)
outline = json.loads(response)
return outline
async def write_sections(self, outline: Dict,
search_results: Dict) -> List[ArticleSection]:
"""アウトラインに基づいてセクションを執筆"""
sections = []
async def write_section(section_data: Dict, level: int = 1):
# 関連する情報を収集
relevant_info = self._get_relevant_info(
section_data['title'],
search_results
)
prompt = f"""
セクションタイトル: {section_data['title']}
以下の情報を基に、{self.config.MIN_SECTION_LENGTH}〜
{self.config.MAX_SECTION_LENGTH}文字でセクションを執筆してください:
{relevant_info}
必ず情報源を[1], [2]のような形式で引用してください。
"""
content = await self.llm.generate(prompt)
# 引用の抽出
citations = self._extract_citations(content, relevant_info)
section = ArticleSection(
title=section_data['title'],
content=content,
subsections=[],
citations=citations,
level=level
)
# サブセクションの処理
if 'subsections' in section_data:
for subsection_data in section_data['subsections']:
subsection = await write_section(subsection_data, level + 1)
section.subsections.append(subsection)
return section
# 各トップレベルセクションを処理
for section_data in outline['sections']:
section = await write_section(section_data)
sections.append(section)
return sections
def compile_article(self, topic: str, sections: List[ArticleSection],
search_results: Dict) -> Dict:
"""最終的な記事をコンパイル"""
# 全引用の統合
all_citations = self._merge_citations(sections)
# 記事のメタデータ
metadata = {
'topic': topic,
'generated_at': datetime.now().isoformat(),
'word_count': self._count_words(sections),
'section_count': len(sections),
'citation_count': len(all_citations),
'perspectives_used': self._extract_perspectives(search_results)
}
return {
'title': self._generate_title(topic, sections),
'abstract': self._generate_abstract(sections),
'sections': sections,
'citations': all_citations,
'metadata': metadata
}
def _extract_relevant_content(self, search_results: List[Dict]) -> str:
"""検索結果から関連コンテンツを抽出"""
content = []
for idx, result in enumerate(search_results[:5]):
content.append(f"[{idx+1}] {result['title']}")
content.append(f"URL: {result['url']}")
content.append(f"内容: {result.get('snippet', '')}")
content.append("")
return "
".join(content)
def _summarize_information(self, search_results: Dict) -> str:
"""収集情報のサマリーを生成"""
summary = []
for question, data in search_results.items():
summary.append(f"Q: {question}")
summary.append(f"視点: {data['perspective']}")
summary.append(f"A: {data['answer'][:200]}...")
summary.append("")
return "
".join(summary)
# 専門分野別カスタマイズ
class DomainSpecificSTORM(StormKnowledgeGenerator):
def __init__(self, domain: str, *args, **kwargs):
super().__init__(*args, **kwargs)
self.domain = domain
self.domain_config = self._load_domain_config(domain)
def _load_domain_config(self, domain: str) -> Dict:
"""ドメイン固有の設定を読み込み"""
domain_configs = {
"medical": {
"required_perspectives": ["医師", "研究者", "患者"],
"trusted_sources": ["pubmed.gov", "who.int", "nejm.org"],
"terminology": "medical",
"citation_style": "vancouver"
},
"technology": {
"required_perspectives": ["エンジニア", "プロダクトマネージャー", "ユーザー"],
"trusted_sources": ["github.com", "stackoverflow.com", "arxiv.org"],
"terminology": "technical",
"citation_style": "ieee"
},
"business": {
"required_perspectives": ["経営者", "投資家", "顧客"],
"trusted_sources": ["hbr.org", "forbes.com", "bloomberg.com"],
"terminology": "business",
"citation_style": "apa"
}
}
return domain_configs.get(domain, {})
async def generate_perspectives(self, topic: str) -> List[Perspective]:
"""ドメイン固有の視点を生成"""
base_perspectives = await super().generate_perspectives(topic)
# 必須視点を追加
for required in self.domain_config.get("required_perspectives", []):
if not any(p.name == required for p in base_perspectives):
base_perspectives.append(Perspective(
name=required,
description=f"{self.domain}分野の{required}",
expertise=[self.domain],
questions=[]
))
return base_perspectives
async def collect_information(self, questions: List[Dict]) -> Dict:
"""信頼できるソースに限定した情報収集"""
# 検索クエリにドメインフィルターを追加
trusted_sources = self.domain_config.get("trusted_sources", [])
if trusted_sources:
for q in questions:
q['query_filter'] = f"site:{' OR site:'.join(trusted_sources)}"
return await super().collect_information(questions)
# 多言語対応STORM
class MultilingualSTORM(StormKnowledgeGenerator):
def __init__(self, target_language: str = "en", *args, **kwargs):
super().__init__(*args, **kwargs)
self.target_language = target_language
self.language_config = self._get_language_config()
def _get_language_config(self) -> Dict:
"""言語別の設定を取得"""
configs = {
"ja": {
"writing_style": "丁寧",
"section_markers": ["第", "節"],
"citation_format": "『{}』",
"date_format": "%Y年%m月%d日"
},
"en": {
"writing_style": "formal",
"section_markers": ["Section", "Chapter"],
"citation_format": "\"{}\"",
"date_format": "%B %d, %Y"
},
"es": {
"writing_style": "formal",
"section_markers": ["Sección", "Capítulo"],
"citation_format": "«{}»",
"date_format": "%d de %B de %Y"
}
}
return configs.get(self.target_language, configs["en"])
async def generate_questions(self, topic: str,
perspectives: List[Perspective]) -> List[Dict]:
"""多言語での質問生成"""
# 基本質問を生成
questions = await super().generate_questions(topic, perspectives)
# 必要に応じて翻訳
if self.target_language != "en":
translated_questions = []
for q in questions:
translated = await self._translate_text(
q['question'],
target_lang=self.target_language
)
q['question'] = translated
translated_questions.append(q)
return translated_questions
return questions
async def write_sections(self, outline: Dict,
search_results: Dict) -> List[ArticleSection]:
"""言語別のスタイルで執筆"""
sections = await super().write_sections(outline, search_results)
# 言語別の後処理
for section in sections:
section.content = self._apply_language_style(section.content)
return sections
def _apply_language_style(self, content: str) -> str:
"""言語固有のスタイルを適用"""
if self.target_language == "ja":
# 日本語の敬語調整
content = content.replace("です。", "です。")
content = content.replace("ます。", "ます。")
# 引用形式の調整
citation_format = self.language_config['citation_format']
# 実装省略
return content
# スタイルカスタマイズ
class StyleCustomizedSTORM(StormKnowledgeGenerator):
def __init__(self, style_config: Dict, *args, **kwargs):
super().__init__(*args, **kwargs)
self.style = style_config
async def write_sections(self, outline: Dict,
search_results: Dict) -> List[ArticleSection]:
"""カスタムスタイルでセクションを執筆"""
sections = []
for section_data in outline['sections']:
# スタイル別のプロンプト生成
style_prompt = self._generate_style_prompt()
prompt = f"""
{style_prompt}
セクションタイトル: {section_data['title']}
関連情報:
{self._get_relevant_info(section_data['title'], search_results)}
上記のスタイルガイドラインに従って執筆してください。
"""
content = await self.llm.generate(prompt)
sections.append(ArticleSection(
title=section_data['title'],
content=self._apply_style_formatting(content),
subsections=[],
citations=self._extract_citations(content),
level=1
))
return sections
def _generate_style_prompt(self) -> str:
"""スタイルプロンプトを生成"""
prompts = {
"academic": """
学術的なスタイルで執筆してください:
- 客観的で中立的な表現
- 専門用語の適切な使用と定義
- 論理的な構成と根拠の明示
- 受動態の使用を適切に
""",
"journalistic": """
ジャーナリスティックなスタイルで執筆してください:
- 読者を引き込む導入
- 具体例とストーリーの活用
- 簡潔で明快な文章
- 5W1Hを明確に
""",
"technical": """
技術文書のスタイルで執筆してください:
- 正確で曖昧さのない表現
- 手順の明確な説明
- コード例やダイアグラムの参照
- 前提条件と結果の明示
""",
"educational": """
教育的なスタイルで執筆してください:
- 段階的な説明
- 例示と練習問題
- 重要ポイントの強調
- 理解度確認の質問
"""
}
return prompts.get(self.style.get('type', 'academic'),
prompts['academic'])
def _apply_style_formatting(self, content: str) -> str:
"""スタイル別のフォーマッティング"""
if self.style.get('type') == 'academic':
# 脚注形式の引用に変換
content = self._convert_to_footnotes(content)
elif self.style.get('type') == 'journalistic':
# プルクォートの追加
content = self._add_pull_quotes(content)
elif self.style.get('type') == 'technical':
# コードブロックのハイライト
content = self._highlight_code_blocks(content)
return content
# ソース制限とフィルタリング
class FilteredSTORM(StormKnowledgeGenerator):
def __init__(self, source_filters: Dict, *args, **kwargs):
super().__init__(*args, **kwargs)
self.filters = source_filters
async def collect_information(self, questions: List[Dict]) -> Dict:
"""フィルタリングされた情報収集"""
search_results = {}
for q_data in questions:
# ソースフィルターを適用
filtered_results = await self._filtered_search(q_data)
# 信頼性スコアの計算
scored_results = self._score_sources(filtered_results)
# 閾値以上のソースのみ使用
reliable_results = [
r for r in scored_results
if r['reliability_score'] >= self.filters.get('min_reliability', 0.7)
]
if reliable_results:
answer = await self._generate_answer_from_sources(
q_data['question'],
reliable_results
)
search_results[q_data['question']] = {
'answer': answer,
'sources': reliable_results[:3],
'perspective': q_data['perspective']
}
return search_results
async def _filtered_search(self, question_data: Dict) -> List[Dict]:
"""フィルタリングされた検索を実行"""
# 基本検索
results = await self.search.search(question_data['question'])
# URLフィルタリング
if 'allowed_domains' in self.filters:
results = [
r for r in results
if any(domain in r['url'] for domain in self.filters['allowed_domains'])
]
if 'blocked_domains' in self.filters:
results = [
r for r in results
if not any(domain in r['url'] for domain in self.filters['blocked_domains'])
]
# 日付フィルタリング
if 'max_age_days' in self.filters:
cutoff_date = datetime.now() - timedelta(days=self.filters['max_age_days'])
results = [
r for r in results
if self._parse_date(r.get('date', '')) >= cutoff_date
]
return results
def _score_sources(self, sources: List[Dict]) -> List[Dict]:
"""ソースの信頼性スコアを計算"""
for source in sources:
score = 0.5 # ベーススコア
# ドメイン権威性
authority_domains = self.filters.get('authority_domains', {})
domain = self._extract_domain(source['url'])
if domain in authority_domains:
score += authority_domains[domain]
# HTTPS使用
if source['url'].startswith('https://'):
score += 0.1
# 更新日の新しさ
if 'date' in source:
age_days = (datetime.now() - self._parse_date(source['date'])).days
if age_days < 30:
score += 0.2
elif age_days < 365:
score += 0.1
# 著者情報の有無
if source.get('author'):
score += 0.1
source['reliability_score'] = min(score, 1.0)
return sorted(sources, key=lambda x: x['reliability_score'], reverse=True)
class ResearchSummarySTORM(DomainSpecificSTORM):
def __init__(self, *args, **kwargs):
super().__init__(domain="academic", *args, **kwargs)
async def generate_research_summary(self, paper_title: str,
research_field: str) -> Dict:
"""研究論文のサマリーを生成"""
# 特化したペルソナ
perspectives = [
Perspective(
name="研究者",
description=f"{research_field}分野の専門研究者",
expertise=[research_field, "methodology"],
questions=[]
),
Perspective(
name="実務家",
description="研究成果の実用化に関心のある実務家",
expertise=["application", "implementation"],
questions=[]
),
Perspective(
name="学生",
description="この分野を学ぶ大学院生",
expertise=["learning", "fundamentals"],
questions=[]
)
]
# カスタムアウトライン
outline = {
"sections": [
{
"title": "研究背景と動機",
"subsections": [
{"title": "既存研究の課題"},
{"title": "本研究の位置づけ"}
]
},
{
"title": "提案手法",
"subsections": [
{"title": "理論的基礎"},
{"title": "実装詳細"}
]
},
{
"title": "実験と評価",
"subsections": [
{"title": "実験設定"},
{"title": "結果と考察"}
]
},
{
"title": "応用可能性と今後の展望",
"subsections": [
{"title": "実社会への応用"},
{"title": "今後の研究課題"}
]
}
]
}
# 論文固有の情報源を優先
self.filters = {
"allowed_domains": ["arxiv.org", "scholar.google.com",
"acm.org", "ieee.org"],
"min_reliability": 0.8
}
# サマリー生成
article = await self.generate_article(
topic=f"{paper_title} - {research_field}",
custom_perspectives=perspectives,
custom_outline=outline
)
return article
最適化項目 | 手法 | 効果 | 実装難易度 |
---|---|---|---|
並列処理 | 非同期検索・生成 | 3-5倍高速化 | 中 |
キャッシング | 検索結果の再利用 | API呼び出し50%削減 | 低 |
バッチ処理 | 複数記事の同時生成 | スループット向上 | 中 |
インクリメンタル更新 | 差分のみ更新 | 90%の処理時間削減 | 高 |
STORM は、LLM を活用した知識生成の新たな可能性を示すシステムです。多角的な視点、信頼できる情報源、構造化されたアプローチにより、高品質な知識記事を自動生成できます。
研究機関、メディア企業、教育機関など、信頼性の高い知識コンテンツを必要とする組織にとって、STORM は強力なツールとなるでしょう。