構造化レポートの生成
エグゼクティブサマリー、調査結果、根拠、推奨事項を含むテンプレート化されたレポートを作成します。
「構造化レポートの生成」はCoddyKit上の無料AI Agentsレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAI Agents学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 AI Agentsコースには全4レッスンが含まれています。
事実から読みやすいレポートへ
事実のリストだけを出力する調査エージェントは使いにくいものです。意思決定者には、エグゼクティブサマリー、背景、主な調査結果、根拠、推奨事項を含む構造化されたレポートが必要です。
このレッスンでは、検証済みの事実から、専門的なレポートをセクションごとに生成する方法を扱います。
レポートテンプレート
あらかじめレポートの構成を定義します。LLMは一度に1つのセクションを統合するため、各セクションに焦点を絞り、重複を避けられます。
REPORT_SECTIONS = [
'executive_summary',
'background',
'key_findings',
'evidence',
'recommendations',
'sources'
]
SECTION_PROMPTS = {
'executive_summary': 'Write a 2-3 sentence executive summary of the key findings. Business audience.',
'background': 'Provide background context for the research topic (3-5 sentences).',
'key_findings': 'List 3-5 key findings as bullet points, each with one supporting fact.',
'evidence': 'Summarize the evidence for each finding with inline source citations.',
'recommendations': 'Based on the findings, provide 2-4 actionable recommendations.',
'sources': 'List all sources cited in the report, formatted as numbered URLs.'
}
if __name__ == '__main__':
print('Report sections:', REPORT_SECTIONS)
for section in REPORT_SECTIONS:
print(f'{section}: {SECTION_PROMPTS[section]}')
一度に1つのセクションを生成
レポート全体を1つのプロンプトで生成すると、信頼性が低くなります。LLMが事実や構成を見失うためです。前のセクションをコンテキストとして渡しながら、セクションごとに生成します。
import openai
client = openai.OpenAI(api_key='YOUR_OPENAI_KEY')
def generate_section(section_name: str, question: str,
facts: list[dict], prior_sections: dict) -> str:
facts_text = '\n'.join(
f'- {f["fact"]} (source: {f["source"]})' for f in facts[:25]
)
prior_text = '\n\n'.join(
f'## {k.replace("_"," ").title()}\n{v}'
for k, v in prior_sections.items()
)
prompt = (
f'Research question: "{question}"\n\n'
f'Verified facts:\n{facts_text}\n\n'
f'Report so far:\n{prior_text}\n\n'
f'Now write the "{section_name}" section.\n'
f'Instructions: {SECTION_PROMPTS[section_name]}'
)
resp = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}]
)
return resp.choices[0].message.contentレポートの反復的な構築
すべてのセクションを順に処理し、蓄積されたコンテキストを渡します。各セクションがそれまでの内容を把握できるため、レポート全体の一貫性を保ち、内部の矛盾を避けられます。
def build_report(question: str, facts: list[dict]) -> dict:
report = {}
sections_to_generate = [s for s in REPORT_SECTIONS if s != 'sources']
for section in sections_to_generate:
print(f'Generating section: {section}...')
report[section] = generate_section(
section_name=section,
question=question,
facts=facts,
prior_sections={k: v for k, v in report.items()}
)
# Sources section: generate citation list from fact URLs
unique_sources = list(dict.fromkeys(f['source'] for f in facts))
report['sources'] = '\n'.join(
f'{i+1}. {url}' for i, url in enumerate(unique_sources[:20])
)
return report本文内引用の埋め込み
生成されたセクション内の事実への参照を、情報源一覧にリンクする番号付きの引用に置き換えます。これにより、すべての主張を追跡できるようになります。
import re
def inject_citations(section_text: str, facts: list[dict]) -> str:
source_index = {} # url -> int
for i, fact in enumerate(facts):
url = fact['source']
if url not in source_index:
source_index[url] = len(source_index) + 1
annotated = section_text
for fact in facts:
if fact['fact'] in annotated:
num = source_index[fact['source']]
annotated = annotated.replace(
fact['fact'],
f'{fact["fact"]} [{num}]',
1 # replace first occurrence only
)
return annotated
if __name__ == '__main__':
demo_text = 'Revenue grew 12% last quarter. The team also launched two new products.'
demo_facts = [{'fact': 'Revenue grew 12% last quarter', 'source': 'https://example.com/report'}]
print(inject_citations(demo_text, demo_facts))
Markdownとしての整形
レポートの各セクションをMarkdownとして出力します。これにより、出力をHTMLやPDFに変換したり、NotionやConfluenceなどのツールに直接表示したりできます。
def render_markdown(report: dict, title: str) -> str:
section_titles = {
'executive_summary': 'Executive Summary',
'background': 'Background',
'key_findings': 'Key Findings',
'evidence': 'Evidence',
'recommendations': 'Recommendations',
'sources': 'Sources'
}
lines = [f'# {title}', '']
for key in REPORT_SECTIONS:
if key in report:
lines.append(f'## {section_titles[key]}')
lines.append('')
lines.append(report[key])
lines.append('')
return '\n'.join(lines)
# Usage:
# md = render_markdown(report, 'Causes of Inflation in 2024')
# with open('report.md', 'w') as f: f.write(md)情報源リンク付きレポートの生成
ウェブ向けに出力する場合は、情報源URLをハイパーリンクに変換します。また、レポートの生成日、情報源数、検証率を含むメタデータブロックも追加します。
from datetime import date
def render_html_report(report: dict, title: str,
facts: list[dict], question: str) -> str:
meta = (
f'<p><em>Generated: {date.today()} | '
f'Sources: {len(set(f["source"] for f in facts))} | '
f'Research question: {question}</em></p>'
)
html_parts = [f'<h1>{title}</h1>', meta]
section_titles = {
'executive_summary': 'Executive Summary',
'background': 'Background',
'key_findings': 'Key Findings',
'evidence': 'Evidence',
'recommendations': 'Recommendations',
'sources': 'Sources'
}
for key in REPORT_SECTIONS:
if key in report:
content = report[key].replace('\n', '<br>')
html_parts.append(f'<h2>{section_titles[key]}</h2><p>{content}</p>')
return '\n'.join(html_parts)公開前の品質チェック
レポートを提供する前に、セクションごとの最低文字数、少なくともN件の引用、すべての推奨事項が行動を表す動詞で始まっていること、「[INSERT]」のようなプレースホルダーテキストがないことを自動的に確認します。
def quality_check(report: dict) -> list[str]:
issues = []
for section in ['executive_summary', 'background', 'key_findings']:
word_count = len(report.get(section, '').split())
if word_count < 30:
issues.append(f'{section} too short: {word_count} words (min 30)')
if report.get('sources', '').count('http') < 3:
issues.append('Less than 3 cited sources')
if '[INSERT]' in str(report) or 'TODO' in str(report):
issues.append('Report contains placeholder text')
recs = report.get('recommendations', '')
if recs and not any(verb in recs.lower()
for verb in ['should', 'recommend', 'consider', 'implement']):
issues.append('Recommendations may not be actionable')
return issues
if __name__ == '__main__':
demo_report = {
'executive_summary': 'Too short.',
'background': 'Also short.',
'key_findings': 'Short too.',
'sources': 'http://a.com',
'recommendations': 'Looks fine as is.',
}
for issue in quality_check(demo_report):
print('-', issue)
エグゼクティブサマリー:厳格な制約
エグゼクティブサマリーは、それだけで完結している必要があります。このセクションだけを読む人でも、主な調査結果と推奨される対応を理解できなければなりません。語数制限を設けた厳格なプロンプトを使用します。
def generate_executive_summary(question: str, facts: list[dict],
max_words: int = 80) -> str:
top_facts = '\n'.join(f'- {f["fact"]}' for f in facts[:10])
prompt = (
f'Research question: "{question}"\n\n'
f'Key facts:\n{top_facts}\n\n'
f'Write an executive summary in EXACTLY {max_words} words or fewer.\n'
f'Format: [Main finding]. [Why it matters]. [Recommended action].'
)
resp = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}]
)
return resp.choices[0].message.content複数の読者層向けレポート
同じ調査でも、対象者によって異なるレポート形式が必要になることがあります。たとえば、エンジニア向けの技術的な詳細レポート、経営層向けのエグゼクティブサマリー、専門家ではない読者向けの平易な概要などです。同じ事実セットから各形式を生成します。
AUDIENCE_STYLES = {
'executive': 'Brief, strategic. Avoid jargon. Focus on business impact and decisions.',
'technical': 'Detailed, precise. Include methodology, caveats, and data sources.',
'general': 'Plain language. Avoid technical terms. Use analogies where helpful.'
}
def generate_for_audience(facts: list[dict], question: str, audience: str) -> str:
style = AUDIENCE_STYLES.get(audience, AUDIENCE_STYLES['general'])
facts_text = '\n'.join(f'- {f["fact"]}' for f in facts[:20])
prompt = (
f'Synthesize these facts into a report for a {audience} audience.\n'
f'Style guide: {style}\n'
f'Question: "{question}"\n\n'
f'Facts:\n{facts_text}'
)
resp = client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}]
)
return resp.choices[0].message.contentレポートの保存とバージョン管理
レポートをタイムスタンプと調査質問のハッシュ付きで保存します。これにより、情報源を更新して調査を再実行した場合に、レポートのバージョンを比較できます。
import hashlib, json, os
from datetime import datetime, timezone
def save_report(report: dict, question: str, output_dir: str = '/tmp/reports'):
os.makedirs(output_dir, exist_ok=True)
q_hash = hashlib.md5(question.encode()).hexdigest()[:8]
timestamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M')
filename = f'{output_dir}/report_{q_hash}_{timestamp}.json'
with open(filename, 'w') as f:
json.dump({
'question': question,
'generated': timestamp,
'sections': report
}, f, indent=2)
print(f'Report saved: {filename}')
return filename
if __name__ == '__main__':
import tempfile
demo_dir = tempfile.mkdtemp()
save_report({'executive_summary': 'AI agent adoption grew significantly in 2026.'},
'What are the key AI agent trends in 2026?', output_dir=demo_dir)
レポート構成の中で、常に単独で完結している必要があるセクションはどれですか
レポートの構成は、時間に制約のある読者が文書全体を読まなくても価値を得られるように設計されています。どのセクションを単独で成立させる必要があるかを理解することは、レポート設計において重要です。
構造化レポート生成の振り返り
固定テンプレート(エグゼクティブサマリー → 背景 → 主な調査結果 → 根拠 → 推奨事項 → 情報源)を使い、セクションごとにレポートを生成します。本文内引用を埋め込み、品質チェックを実行し、同じ事実セットから複数の読者層向けの形式に対応します。
バージョン管理のため、レポートには必ずタイムスタンプと質問のハッシュを付けて保存します。
よくある質問
「構造化レポートの生成」レッスンは無料ですか?
はい。「構造化レポートの生成」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、AI Agentsコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 AI Agentsコースには全4レッスンが含まれています。
「構造化レポートの生成」で何を学びますか?
エグゼクティブサマリー、調査結果、根拠、推奨事項を含むテンプレート化されたレポートを作成します。 ブラウザで直接実行するハンズオンコードでAI Agentsを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
AI Agentsを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのAI Agentsは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「構造化レポートの生成」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このAI Agentsレッスンでコードを書いて実行できますか?
はい。すべてのAI Agentsレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。