Hacker Newsの最新記事を要約するツールを作る
X(旧Twitter)で情報収集している方は多いと思います。
ノイズが多くて疲れませんか?驚き屋さんに辟易としていませんか?
Xから情報を収集するのは程々にして私はHacker Newsをよくみています。
特に気になるニュースは翻訳ツールやLLMに解説をお願いして理解に努めています。
仕事の前や仕事の合間に読んでいることが多いですが、業務がAIで効率化されたことによって日々の情報処理量が多くなって、Hacker Newsに目を通して英語の字を見るのも疲れてきました。
もう疲れたくない、その思いからHacker Newsの要約をするツール[1]を作り始めました。
成果物
作ったのはこのようなものです。Web UIで表示する形にしています。

https://news.ycombinator.com/ に一覧表示されている最新のニュースを取得し、その記事の内容及びHacker News内に記載されているコメントを要約するようにしました。
いわゆるAIエージェントっぽさも欲しかったので、チャット機能も設けました。
チャットを通じて記事のまとめ、最新のニュースの確認などを行うことができます。
現時点では、記事取得は一日1回のバッチ更新としています。
非常に便利に使っています。
システム全体の構成図
全体設計
アーキテクチャは、モノレポ構成のめちゃシンプルな作りです。
.
├─ apps/
│ ├─ web # Next.js 14 App Router (UI + API)
└─ packages/
└─ core # 共有ロジック (HN API, LLM, Prisma)
また、システムのデータフローは以下の通りです。
データベース設計
Prismaを使用したスキーマ設計は以下の通りです。これまたシンプル。
-
Story: HN記事の基本情報を保持 -
StorySummary: LLMによる記事要約と分類情報(トピック、重要度、読書時間) -
StoryCommentSummary: コメント欄の要約とハイライト
model Story {
id Int @id
title String
url String?
by String?
score Int @default(0)
descendants Int @default(0)
publishedAt DateTime @default(now())
summary StorySummary?
commentSummary StoryCommentSummary?
}
model StorySummary {
id Int @id @default(autoincrement())
storyId Int @unique
summaryJa String
topic String
importance Int
readingTimeMinutes Int
reason String
llmModel String?
story Story @relation(fields: [storyId], references: [id])
}
model StoryCommentSummary {
id Int @id @default(autoincrement())
storyId Int @unique
summaryJa String
highlights String?
sampleComments String?
story Story @relation(fields: [storyId], references: [id])
}
実装の詳細
HN API連携
Hacker News APIはFirebase Realtime Databaseで公開されており、RESTful APIとして利用できます。太っ腹。
const HN_API_BASE = 'https://hacker-news.firebaseio.com/v0';
export async function fetchTopStoryIds(limit: number): Promise<number[]> {
const ids = await pRetry(
() => fetchJson<number[]>('topstories.json'),
{ retries: 2 }
);
return ids.slice(0, limit);
}
export async function fetchStoryById(id: number): Promise<HackerNewsStory | null> {
return pRetry(
() => fetchJson<HackerNewsStory | null>(`item/${id}.json`),
{ retries: 2 }
);
}
念の為、p-retryライブラリを使用してネットワークエラーに対する耐性を確保しています。
LLM統合
OpenAI APIを使用して要約を生成することにしました。今回に関してはこだわりは特にないです。
GeminiだろうがClaudeだろうが、要約してくれるならなんでもよし。[2]
export async function summarizeStory(input: {
title: string;
url?: string;
textSnippet?: string;
}): Promise<StorySummary> {
const prompt = `
以下のHacker Newsの記事を日本語で要約してください。
タイトル: ${input.title}
URL: ${input.url || 'N/A'}
本文: ${input.textSnippet || 'なし'}
以下の形式でJSONを返してください:
{
"summaryJa": "日本語の要約(2-3文)",
"topic": "AI|Web|DevOps|Security|Business|Other",
"importance": 1-5の数値,
"readingTimeMinutes": 推定読書時間,
"reason": "重要と判断した理由"
}
`;
const response = await this.request({
model: this.config.model,
messages: [{ role: 'user', content: prompt }],
response_format: { type: 'json_object' }
});
return JSON.parse(response.choices[0].message.content);
}
リアルタイム翻訳機能
データベースに保存された記事とは別に、HNから直接記事を取得して翻訳する機能も実装しました。
export async function fetchAndTranslateLiveStories(limit = 30) {
const llm = getLlmClient();
const ids = await fetchTopStoryIds(limit);
const stories = await Promise.all(ids.map((id) => fetchStoryById(id)));
const liveStories = [];
for (const story of validStories) {
try {
const summary = await llm.summarizeStory({
title: story.title,
url: story.url,
textSnippet: story.text
});
liveStories.push({
id: story.id,
title: story.title,
summary: {
summaryJa: summary.summaryJa,
topic: summary.topic,
importance: summary.importance,
readingTimeMinutes: summary.readingTimeMinutes,
reason: summary.reason
}
});
} catch (error) {
// エラー時は要約なしで記事を含める
console.error(`Failed to summarize story ${story.id}:`, error);
liveStories.push({ id: story.id, title: story.title, summary: null });
}
}
return liveStories;
}
このアプローチにより、データベースを経由せずにリアルタイムで最新の記事を取得・翻訳できます。
パフォーマンス最適化
バッチ処理
記事の要約生成は、コスト削減と処理時間の観点からバッチ処理で実行しています。
export async function summarizeUnprocessedStories(limitPerRun = 5) {
const targets = await prisma.story.findMany({
where: { summary: null },
orderBy: { score: 'desc' },
take: limitPerRun
});
for (const story of targets) {
const summary = await llm.summarizeStory({
title: story.title,
url: story.url,
textSnippet: story.textSnippet
});
await prisma.storySummary.upsert({
where: { storyId: story.id },
update: { ...summary },
create: { storyId: story.id, ...summary }
});
}
}
まとめ
本記事では、Hacker Newsの記事を自動で収集・翻訳・要約するエージェントシステムの実装について解説しました。
やったこと
- モノレポ構成による共通ロジックの再利用
- Prisma ORMによる型安全なデータベース操作
- OpenAI APIを使用した自動翻訳・要約
- Next.js App Routerによるフルスタック実装
少しの時間でいい感じに仕上げることができました。
改善の余地はまだあるので、自分好みのツールに仕上げていこうと思います。