Stage 4: Synthesis & Report Generation — Frontmatter 聚合 → Editor-in-Chief 日报合成 → 前端可视化
- 1. 设计动机
- 2. 数据流全景
- 3. Stage 4a: Frontmatter 聚合
- 4. Stage 4b: Editor-in-Chief 合成
- 5. Prompt 工程设计
- 6. JSON 解析与校验
- 7. Markdown 报告生成
- 8. 前端数据消费
- 9. CLI 集成
- 10. 关键设计决策
前三阶段(采集、事实提取、深度分析)完成后,系统面临两个核心挑战:
- 数据碎片化:244 篇文章分散在 5 个数据源目录中,每篇是独立的
.md文件,YAML Frontmatter 承载了全部结构化数据,但缺乏统一的聚合视图 - 信息过载:244 篇文章 × 32+ 字段 = 7800+ 数据点,人类无法直接消费,需要 AI 驱动的综合合成
Stage 4 的设计目标是将碎片化分析结果转化为一份即看即懂的日报:
- 结构化输出:JSON 格式,可直接被 Next.js 前端消费
- 人类可读:Markdown 格式,适合分享、审计、归档
- 智能合成:不是简单汇总,而是发现跨文章关联、识别趋势、评估风险
- 预计算可视化:前端零聚合逻辑,直接渲染预计算的图表数据
data/03_analyzed/ data/04_structured/ data/05_reports/
├── arxiv/209.md ──┐ ├── arxiv.json (209篇)
├── bensbites/4.md ─┤ Stage ├── bensbites.json (4篇) ┌─ daily-report.json
├── kdnuggets/8.md ─┼── 4a ──► ├── kdnuggets.json (8篇) │ (→ Next.js 看板)
├── techcrunch/10.md─┤ 聚合 ├── techcrunch.json (10篇) │
├── tldrai/13.md ───┘ ├── tldrai.json (13篇) Stage 4b
└── all_articles.json Editor-in-Chief
├── articles[244] (Claude Opus)
└── sources 统计摘要 ──► └─ daily-report.md
(人类阅读/审计)
数据量级:
- Stage 4a 输出:6 个 JSON 文件,总计约 3.8MB
- Stage 4b Prompt:~38K tokens(system 1.5K + user 36.5K)
- Stage 4b 输出:~8K tokens(日报 JSON)
- 纯机械操作:不调用 LLM,确定性算法,< 1 秒完成
- 保留完整字段:所有 Frontmatter 字段原样传递,不丢失任何数据
- 分离输出:每个数据源独立 JSON + 全量合并 JSON,便于按源检查数据质量
- 源级统计:合并 JSON 中预计算各源的
avg_impact,帮助判断数据源质量
文件: pipeline/aggregation/aggregate_frontmatter.py
入口: aggregate_frontmatter(input_dir, output_dir, dry_run)
CLI: python pipeline/run.py aggregate [--dry-run]
注意:aggregate 会在 extract 和 analyze 完成后自动调用,日常无需手动执行。
处理流程:
1. 递归 glob data/03_analyzed/**/*.md
│
2. 按父目录名分组 ──┼── arxiv/: 209 files
(source 维度) ├── bensbites/: 4 files
├── kdnuggets/: 8 files
├── techcrunch/: 10 files
└── tldrai/: 13 files
│
3. 逐文件提取 ──────► read_frontmatter() → 提取 YAML → 校验 id/title/body
YAML Frontmatter - 无 frontmatter → 跳过
- 缺少 id/title → 跳过
- 正文为空 → 跳过 (死文件)
│
4. _serialize_value() ──► 递归转换 datetime/嵌套结构为 JSON 兼容
│
5. 批量写入 ────────► per-source JSON: {source}.json
└► 合并 JSON: all_articles.json
├── generated_at
├── total_articles
├── sources (per-source avg_impact)
└── articles (全量)
| 函数 | 职责 | 特点 |
|---|---|---|
_serialize_value(obj) |
递归将 Python 对象转为 JSON 安全格式 | 处理 datetime → ISO, list/dict 递归 |
_extract_article(filepath, source_dir) |
从单 .md 提取 frontmatter 构造文章记录 | 跳过无 frontmatter、缺 id/title、空正文的文件 |
aggregate_frontmatter() |
主函数:扫描 → 分组 → 提取 → 写入 | 返回汇总 dict |
// all_articles.json (结构)
{
"generated_at": "2026-05-08T12:00:00Z",
"source": "data/03_analyzed",
"total_articles": 244,
"sources": {
"arxiv": { "count": 209, "avg_impact": 5.3 },
"techcrunch": { "count": 10, "avg_impact": 5.0 }
},
"articles": [
{
"source_dir": "arxiv",
"id": "a1b2c3d4e5f6",
"title": "Hallucinations Undermine Trust; Metacognition is a Way Forward",
"event_type": "infrastructure_update",
"impact_score": { "score": 4.0, "reason": "..." },
"entities": { "companies": ["Google"], "technologies": ["Metacognition"] },
...
}
]
}Stage 4b 的核心理念是**"单次调用、完整输出"**——一次 Claude Opus 调用完成全部合成工作,而不是多轮对话或多 Agent 协作。理由:
- 上下文完整性:LLM 需要同时看到统计概览 + 高影响力文章详情 + 全量标题列表,才能做出跨文章关联判断
- 输出一致性:单次调用保证日报各部分的风格、术语、判断逻辑一致
- 成本控制:一次 ~38K input + ~8K output 的调用,远低于多次中等规模调用的总和
文件:
pipeline/synthesis/editor_in_chief_agent.py — Agent 调用 + Prompt 构造
pipeline/synthesis/run_synthesis.py — CLI 入口 + 编排
pipeline/synthesis/report_generator.py — 校验 + Markdown 生成
pipeline/synthesis/prompts/system_prompt.py — System Prompt
pipeline/synthesis/prompts/user_prompt.py — User Prompt Builder
处理流程:
1. 读取 all_articles.json
│
2. 构造 Prompt ─────────► System Prompt (1.5K chars)
│ User Prompt (36.5K chars)
│ ├── Section 1: 统计概览 (event/sentiment/source/entity 分布)
│ ├── Section 2: Top 30 文章完整 Frontmatter (按 impactScore 降序)
│ └── Section 3: 剩余 214 篇标题+关键指标列表
│
3. 调用 Claude Opus ────► call_agent_with_retry()
│ model: claude-opus-4-7
│ max_turns: 1
│ max_retries: 3 (exponential backoff)
│
4. 解析 JSON ───────────► parse_json_response() — 5 级回退策略
│
5. 结构校验 ───────────► validate_report() — 顶层字段 + 子对象字段 + 枚举值校验
│
6. 写入文件 ───────────► daily-report.json + daily-report.md
| 模块 | 文件 | 职责 |
|---|---|---|
| Agent 调用 | editor_in_chief_agent.py |
读取数据、构造 Prompt、调用 LLM、解析响应 |
| CLI 编排 | run_synthesis.py |
argparse 参数解析、dry-run 预估、进度输出 |
| JSON 校验 | report_generator.py::validate_report() |
结构完整性检查、枚举值校验、错误/警告分类 |
| Markdown 生成 | report_generator.py::generate_markdown() |
JSON → 可读 Markdown(表格、列表、标题层级) |
文件: pipeline/synthesis/prompts/system_prompt.py
变量: EDITOR_IN_CHIEF_SYSTEM_PROMPT
System Prompt 定义 Agent 的角色、输出契约和质量约束,共三个部分:
第一部分:角色定义
"You are the Editor-in-Chief of the Daily AI Insight Engine, a specialized intelligence briefing for AI industry decision-makers."
明确受众(AI 投资人、产品负责人、工程管理者)、语调(分析性、循证、简洁、无营销话术)。
第二部分:输出 Schema
定义完整的 JSON 结构,含 11 个顶层字段、5 个子对象结构(topEvents, deepDives, trendInsights, riskSignals, opportunitySignals, visualizationData)。每个字段标注类型约束和语言要求(中文文本、英文枚举值)。
第三部分:9 条质控规则
| # | 规则 | 目的 |
|---|---|---|
| 1 | Distributions 必须从统计概览聚合全部 244 篇文章 | 防止 LLM 编造分布数据 |
| 2 | impactRanking 取 Top 10 | 前端 Impact 排名图的数据源 |
| 3 | entityFrequency 聚合全部文章,合并近似实体 | 实体频次图的数据源 |
| 4 | Top 5 事件需交叉参考、调整 hype/rumor 权重 | 事件排序的质控逻辑 |
| 5 | Deep Dives 选择有战略深远影响的事件 | 防止选择仅热度高但无长期价值的事件 |
| 6 | Trend Insights 4 维度各 2-4 个信号 | 保证四维覆盖完整 |
| 7 | Risk/Opportunity Signals 必须扎根原文字段 | 防止 LLM 编造风险/机会 |
| 8 | 文本用中文,枚举值用英文 | 语言一致性 |
| 9 | 只输出纯 JSON,无 Markdown 包裹 | 确保可机器解析 |
文件: pipeline/synthesis/prompts/user_prompt.py
函数: build_user_prompt(all_articles, max_detail=30)
User Prompt 分四部分,按信息密度递减排列:
Section 1: 统计概览(~2K tokens)
预计算的统计摘要,由 _compute_statistics() 从 244 篇文章中聚合:
- Event Type 分布(5 类)
- Sentiment 分布(4 类)
- Source Type 分布(academic_paper / tech_blog / news_media / community_discussion)
- Epistemic Status 分布(confirmed / disputed / rumor_leak / unknown)
- 各数据源文章数
- Top 50 实体频次(按类型分组:company / technology / person)
Section 2: Top 30 文章完整 Frontmatter(~20K tokens)
按 impactScore 降序排列的前 30 篇,每篇展开:
- 基础信息:id, title, source_dir, source_type, published, tldr
- 事实提取:event_type, epistemic_status, key_logic_flow(6 条)
- 定性研判:impact_score, sentiment, developer_sentiment, hype_assessment, information_entropy, engineering_complexity
- 价值评估:compound_value, value_capture_layer, moat_impact, key_beneficiaries, competitive_casualty
- 前瞻预测:market_opportunities, risk_matrix, actionable_insight
约 22 个字段/篇 × 30 篇 = 660 个字段值。
Section 3: 剩余文章标题列表(~15K tokens)
214 篇文章,每篇仅一行:
[source_dir] title | impact=X | event=xxx | sentiment=xxx
让 LLM 感知全量数据分布,但不占用过多 token 预算。
Section 4: 指令(~0.5K tokens)
明确输出要求:必须聚合全量、语言规则、纯 JSON 输出。
def _impact_score(a: dict) -> float:
iscore = a.get("impact_score", {})
if isinstance(iscore, dict):
return float(iscore.get("score", 0))
return float(iscore) if iscore else 0.0
sorted_articles = sorted(all_articles, key=_impact_score, reverse=True)按 impact_score.score 降序排列,Top 30 获得完整展示,其余仅标题。这保证了 LLM 首先处理高价值文章。
| Prompt 部分 | 估算 tokens | 占比 |
|---|---|---|
| System Prompt | ~1,500 | 4% |
| 统计概览 | ~2,000 | 5% |
| Top 30 完整 Frontmatter | ~20,000 | 52% |
| 剩余文章标题 | ~15,000 | 39% |
| 总计 | ~38,000 | 100% |
Context window 占用率:~19%(Claude Opus 4 200K window),留有充足余量。
文件: pipeline/core/agent.py
函数: parse_json_response(text)
LLM 输出可能被 Markdown 代码块包裹、截断、或混有解释文本。系统采用 5 级回退:
| 级别 | 策略 | 适用场景 |
|---|---|---|
| 1 | json.loads() 直接解析 |
LLM 输出纯 JSON,无包裹 |
| 2 | 正则提取 ```json ... ``` 代码块 |
LLM 用 Markdown 包裹 JSON |
| 3 | 查找第一个 { 和最后一个 } |
JSON 嵌入在解释文本中 |
| 4 | 去除 Markdown 代码块标记后重试 | 处理不规范的 code fence |
| 5 | 截断 JSON 恢复(_close_json 补全括号 + 逐步修剪) |
LLM 输出被 token 限制截断 |
文件: pipeline/synthesis/report_generator.py
函数: validate_report(report) → { valid: bool, errors: [], warnings: [] }
校验分层进行:
- 顶层字段(11 个必需 key):
date,generatedAt,reportTitle,executiveSummary,dataSourceSummary,topEvents,deepDives,trendInsights,riskSignals,opportunitySignals,visualizationData - 子对象字段(逐项检查):
topEvents[i]的title/articleIds/eventType/impactScore/whyItMatters/evidence - 枚举值校验:
eventType必须是 5 个合法值之一、severity必须是low/medium/high、dimension必须是technology/application/policy/capital
返回值区分 errors(阻断性)和 warnings(提醒性),校验失败不阻断文件写入,但会在日志中醒目提示。
文件: pipeline/synthesis/report_generator.py
函数: generate_markdown(report) → str
| 段落 | 格式 | 内容 |
|---|---|---|
| YAML Frontmatter | --- 包裹 |
title, date, generated |
| 执行摘要 | 纯文本段落 | executiveSummary |
| 数据概览 | Markdown 表格 | 样本总量 / 信源数 / 语言覆盖 |
| Top 事件 | 三级标题 + 列表 | 5 事件 × (类型/评分/重要性/证据) |
| 深度分析 | 三级标题 + 子段落 | 3 篇 × (背景/影响/后续关注) |
| 趋势判断 | 三级标题 + 列表 | 4 维度 × (判断/支撑信号) |
| 风险提示 | Markdown 表格 | 严重程度 / 信号 / 判断依据 |
| 机会提示 | Markdown 表格 | 严重程度 / 信号 / 判断依据 |
| 信源说明 | 纯文本段落 | selectionRationale |
Markdown 中的标签文本与前端 labels.ts 保持一致:
| 字段 | 中文标签 |
|---|---|
infrastructure_update |
基建更新 |
framework_tools |
框架工具 |
capital_movement |
资本动向 |
application_landing |
应用落地 |
policy_and_safety |
政策与安全 |
low / medium / high |
低 / 中 / 高 |
technology / application / policy / capital |
技术 / 应用 / 政策 / 资本 |
Dashboard 页 (/dashboard):
data/05_reports/daily-report.json
│
▼
src/app/dashboard/page.tsx
getReport() {
readFile("data/05_reports/daily-report.json")
→ JSON.parse()
→ dailyReportSchema.parse() ← Zod 校验
→ 传递给组件树
}
Sources 页 (/, 首页):
pipeline/config.yaml + data/00_manifest/*.json
│
▼
src/app/page.tsx
getSourcesViewData() {
getSourceConfigs() → 解析 config.yaml (enabled: true 源 + tiers_meta)
loadManifests() → 读取所有 manifest JSON
configToStatus() → 合并为 SourceStatus[]
→ { tiersMeta, sources, totalSources, totalArticles, latestDate }
}
Source Detail 页 (/sources/[name]):
pipeline/config.yaml + data/00_manifest/{source}_*.json
│
▼
src/app/sources/[name]/page.tsx
getSourceDetail(name) → SourceStatus | null
getSourceDetailEnriched(name) → EnrichedSourceDetail (含 articles)
| 决策 | 理由 |
|---|---|
| 服务端直接读文件 | 无网络请求、无 API 路由、无中间层,零延迟 |
| force-dynamic | 禁用 Next.js 静态优化,每次请求重新读取文件,确保数据最新 |
| Zod 校验前置 | 在服务端渲染前校验,损坏的 JSON 不会渲染到页面,而是抛出清晰的 ZodError |
| 可视化数据预计算 | visualizationData 由 LLM 在合成阶段生成,前端零聚合。eventTypeDistribution 和 sentimentDistribution 的总计数必须等于 totalArticles |
| Markdown fallback | /report 页优先读取预生成的 .md,文件缺失时从 JSON 动态生成(generateMarkdown()) |
| 配置驱动 Sources UI | Sources 页面的展示文案由 config.yaml 中 tiers_meta、display_name、display_description 驱动,新增数据源无需改前端代码 |
Dashboard 页:
ReportHeader ← reportTitle, date, generatedAt, executiveSummary
KPISection ← dataSourceSummary { totalArticles, sources, languages }
DistributionSection ← visualizationData { eventTypeDistribution, sentimentDistribution }
TopEventsSection ← topEvents[]
RankingsSection ← visualizationData { impactRanking, entityFrequency }
TrendInsightsSection ← trendInsights[]
DeepDivesSection ← deepDives[]
SignalList ×2 ← riskSignals[], opportunitySignals[]
ReportFooter ← dataSourceSummary.selectionRationale
Sources 页:
SourcesHero ← tiersMeta, totalSources, totalArticles, latestDate, tier counts
SourcesGrid ← sources[], tiersMeta
TierSection ← tier, source[], tierMeta (彩色竖条 + 标题 + 副标题)
SourceCard ← SourceStatus (display_name, display_description, keywords, ...)
文件: pipeline/run.py
所有阶段通过 python pipeline/run.py <subcommand> 调用:
| 命令 | 阶段 | 说明 |
|---|---|---|
scout |
Stage 1a | 生成 URL 清单 |
ingest |
Stage 1b | 下载清洗正文 |
extract |
Stage 2 | 事实提取 |
analyze |
Stage 3 | 深度分析 |
aggregate |
Stage 4a | Frontmatter 聚合(extract/analyze 后自动执行,独立运行用于配置变更或日期回溯) |
synthesize |
Stage 4b | 日报合成(支持 --target-date 回溯历史日报) |
backfill-ids |
工具 | 为已有 .md 补充 article.id |
python pipeline/run.py synthesize [options]
Options:
--input, -i all_articles.json 路径 (默认: data/04_structured/all_articles.json)
--output, -o 输出目录 (默认: data/05_reports/)
--model, -m LLM 模型名称 (默认: claude-opus-4-7)
--max-detail 完整展示的文章数 (默认: 30)
--dry-run 仅显示 prompt 预估,不调用 LLM
--lookback-days 先按 N 天窗口重新聚合,再合成 (默认: 跳过)
--target-date 精确日期过滤,先按目标日期聚合再合成 (与 --lookback-days 互斥)
--verbose, -v 显示详细日志
$ python pipeline/run.py synthesize --dry-run
=== Stage 4b: Editor-in-Chief 合成 ===
文章总数: 244
数据源: ['arxiv', 'bensbites', 'kdnuggets', 'techcrunch', 'tldrai']
完整展示: 前 30 篇
>>> DRY RUN — 不调用 LLM <<<
System prompt: 4678 chars
User prompt: 115705 chars
估算 tokens: ~38568 tokens (rough estimate)dry-run 模式构造完整 User Prompt 但跳过 LLM 调用,用于:
- 评估 token 消耗
- 检查 Top 30 文章选择是否正确
- 调试 prompt 结构
| 对比维度 | 合并为一步 | 拆分 4a + 4b |
|---|---|---|
| 调试体验 | 看不到中间 JSON,问题定位困难 | 可先检查 4a 产物,确认数据完整性后再跑 4b |
| LLM 成本 | 每次调 prompt 都要重新聚合 | 4a 一次聚合后可反复跑 4b,无需重复 |
| 失败恢复 | LLM 调用失败需从 4a 重来 | 4a 纯机械,几乎不会失败;4b 失败可直接重试 |
- Token 预算:244 篇全量详细 = ~160K tokens,超过 context window 的合理使用比例
- 信息收益递减:低 impactScore 文章(< 3)的详细分析对日报质量贡献有限
- 统计概览兜底:Section 1 的全量统计保证了低分文章的信息不被完全忽略
- 上下文完整性:LLM 需要同时看到所有数据才能做出跨文章关联判断
- 成本经济性:一次 ~38K input 调用 < 多次分散调用
- Schema 一致性:单次调用保证日报各部分风格、术语、判断逻辑统一
- 零延迟:无网络请求、无中间层、无序列化开销
- 简化部署:前端可以直接从
data/05_reports/静态读取,适合 Vercel / 静态托管 - 可审计性:生成的 JSON 即是前端数据源,可以版本控制、diff、归档
相关文档:
- 整体设计说明 — 架构总览、技术栈、信源体系
- Schema 设计说明 — Pydantic/Zod 双端 Schema 契约
- 数据源筛选与获取设计说明 — Stage 1 详细设计