6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vercel AI SDK で社内向けAIニュースアプリを作った話

Posted at

作ったもの

毎朝Slackにテックニュースが届く仕組みを作りました。

スクリーンショット 2025-12-12 15.19.51.png

スクリーンショット 2025-12-12 15.02.04.png

  • ユーザーが登録したRSSフィードから最新記事を自動取得
  • AIが記事を読んで日本語で要約
  • セキュリティ関連やbreaking changeは優先的に報告
  • 1日の記事を総括した「今日のテックニュース総括」も生成
  • アプリケーションで過去の記事を検索

スクリーンショット 2025-12-12 14.49.59.png

スクリーンショット 2025-12-14 0.50.22.png

なぜ作ったか

社内に技術ニュースを発信してくれる先輩がいました。Next.jsやTanStack Start、ConvexやHonoなどモダンな技術のこと、AIニュースの最新動向、ビジネススキルの発信、英語学習などなど。脚色無しで、1日に5〜10件のニュースを自身の考えと合わせて発信されていました。純粋にすごいです。

自分も同じように情報を取得し、発信したいと思いましたが、先輩と同じクオリティでやるのは正直難しい。毎日発表されるニュースを取得して、英語の記事を読んで、重要なものをピックアップして......。この部分だけでもAIに任せられたら楽になるなと思い、社内向けにAIニュースアプリを作りました。

技術スタック

用途 技術
フレームワーク Next.js (App Router)
AI Vercel AI SDK
DB Turso (SQLite)
ORM Drizzle
定期実行 Vercel Cron

なぜVercel AI SDKか

複数のAIプロバイダーを統一インターフェースで扱えます。モデルを変えるのも1行だけです。

const result = await generateObject({
  model: "google/gemini-2.5-flash-lite",  // ここを変えるだけ
  prompt,
  schema: summarySchema,
});

Vercel AI Gatewayは毎月5ドル分のクレジットが付与されます。

Yes! When you sign up for a Vercel account, you get $5 of credits every 30 days to try out any model from our model list. We don’t restrict access to premium models.
Note: After you make your first payment, you are considered a paid customer and will no longer receive the free credits.
Vercelアカウントを登録すると、30日ごとに5ドル分のクレジットがもらえます。モデルリストのどのモデルでも試せて、プレミアムモデルへのアクセス制限もありません。
注意: 最初の支払いをすると有料顧客扱いになり、無料クレジットは付与されなくなります。

開発時にかなり使い倒しましたが、1ドルも使わなかったので当面は無料で使えそうです。

スクリーンショット 2025-12-12 14.38.05.png

モデルは Gemini を採用しました。2025年10月末時点の開発時に要約時の精度がちょうど良かったためです。Claude は気を利かせすぎて遅い、GPT は微妙に精度が悪かったです。現在だとまた最適なモデルを選択する必要がありますね。

なぜRSSか

最初はスクレイピングも考えましたが、精度がサイトによってバラバラで安定しませんでした。AIのリソースも使いすぎます。RSSならXML形式で構造化されているので、パースが安定します。

アーキテクチャ

Vercel Cron (毎日 10時頃)
    │
    ▼
/api/cron
    │
    ├─ 1. RSS取得 (rss-parser)
    │
    ├─ 2. AI処理 (Vercel AI SDK)
    │     - 記事ごとに要約・タグ生成
    │     - 全体の総括文を生成
    │
    ├─ 3. DB保存 (Turso)
    │
    └─ 4. Slack通知 (Webhook)

実装

1. RSSフィードの取得

rss-parserでフィードを取得します。直近7日分、各フィード最新10件に絞っています。同じニュースを何度も発信しないよう、既存のDBと照合して重複チェックも行います。

import Parser from "rss-parser";

type FeedItem = {
  date: Date;
  title: string;
  url: string;
  content: string;
};

const parser = new Parser();
const MAX_ITEMS = 10;
const DAYS_TO_FETCH = 7;

export async function fetchFeedItems(feedUrl: string): Promise<FeedItem[]> {
  const feed = await parser.parseURL(feedUrl);
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - DAYS_TO_FETCH);

  return feed.items
    .map((item) => ({
      date: new Date(item.isoDate ?? item.pubDate ?? ""),
      title: item.title ?? "",
      url: item.link ?? "",
      content: item.contentSnippet ?? "",
    }))
    .filter((item) => item.date >= cutoffDate)
    .slice(0, MAX_ITEMS);
}

2. AIによる要約生成

Vercel AI SDKのgenerateObjectを使います。Zodスキーマを渡すと、AIがその形式で出力してくれます。

import { generateObject } from "ai";
import { z } from "zod";

const summarySchema = z.object({
  summaries: z.array(
    z.object({
      id: z.string(),
      title: z.string(),
      summary: z.string(),
      tags: z.array(z.string()),
      keyPoint: z.string(),
    })
  ),
});

type SummaryInput = Record<'id' | 'title' | 'content', string>;

export async function generateSummaries(items: SummaryInput[]) {
  const prompt = items
    .map((item) => `[${item.id}] ${item.title}\n${item.content}`)
    .join("\n\n");

  const { object } = await generateObject({
    model: "google/gemini-2.5-flash-lite",
    prompt: `以下の技術記事を要約してください。\n\n${prompt}`,
    schema: summarySchema,
  });

  return object.summaries;
}

generateObjectの良いところ:

  • 出力がスキーマに沿っていることが保証される
  • プロンプトで「JSON形式で出力して」と頑張らなくていい
  • TypeScriptの型が効く

3. プロンプト

実際に使っているプロンプトはこんな感じです。

const ARTICLE_SUMMARY_PROMPT = `
以下の技術記事群を分析し、それぞれについて以下を生成してください。

1. より分かりやすい日本語のタイトル
2. 5文程度の詳細な要約
3. 適切なタグを1〜3個
4. 記事の核心ポイントを1文で

利用可能なタグ:
- release: リリース情報
- security: セキュリティ関連
- breaking-change: 破壊的変更
- feature: 新機能
- bugfix: バグ修正
...
`;

タグは自由入力じゃなくて選択式にしています。後でフィルタリングしやすく、AIの出力がブレにくいためです。

4. 全体サマリー

記事ごとの要約とは別に、1日の総括も生成しています。

const OVERALL_SUMMARY_PROMPT = `
今日のテックニュースをまとめてください。

**最優先で報告すべき内容:**
- セキュリティ: 脆弱性、セキュリティアップデート
- 重大なバグ、breaking change
- メジャーバージョンリリース

上記がある場合は100-200文字で簡潔に。
ない場合は400-500文字で詳しく。
`;

セキュリティ関連は優先的に報告させています。重要な脆弱性情報が他のニュースに埋もれないようにするためです。

5. Slack通知

Slack Webhookで送信します。Block Kit形式でリッチに表示できます。

import type { KnownBlock } from "@slack/types";

export async function sendSlackMessage(blocks: KnownBlock[]) {
  const webhookUrl = process.env.SLACK_WEBHOOK_URL;

  if (!webhookUrl) {
    throw new Error("SLACK_WEBHOOK_URL is not set");
  }

  await fetch(webhookUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ blocks }),
  });
}

Block Kitのメッセージ構造:

type ArticleBlockParams = {
  url: string;
  title: string;
  summary: string;
  keyPoint: string;
  imageUrl?: string;
};

function createArticleBlock(params: ArticleBlockParams): KnownBlock {
  const { url, title, summary, keyPoint, imageUrl } = params;

  return {
    type: "section",
    text: {
      type: "mrkdwn",
      text: `*<${url}|${title}>*\n${summary}\n📍 *ポイント:* ${keyPoint}`,
    },
    ...(imageUrl && {
      accessory: {
        type: "image",
        image_url: imageUrl,
        alt_text: title,
      },
    }),
  };
}

6. 定期実行

vercel.jsonに書くだけです。UTC 01:00 = 日本時間 10:00 です。

{
  "crons": [
    {
      "path": "/api/cron",
      "schedule": "0 1 * * *"
    }
  ]
}

7. AIによるRSSリンク検索

RSSのリンクを人間が探してくるのは面倒なので、AIに探させる機能も作りました。

スクリーンショット 2025-12-12 15.21.28.png

const rssFeedSchema = z.object({
  feeds: z.array(
    z.object({
      url: z.string().url(),
      title: z.string(),
      description: z.string(),
    })
  ),
});

export async function searchRssFeeds(feedName: string) {
  const { object } = await generateObject({
    model: "google/gemini-2.5-pro",
    prompt: `${feedName}の公式RSSフィードURLを探してください`,
    schema: rssFeedSchema,
  });

  return object.feeds;
}

探したリンクが本当に有効なRSSかどうか、裏側で検証してから結果を返すようにしています。Gemini flash だと検証が甘かったので、ここだけGemini pro版を使用しています。

実際に取得されたURL:
https://github.com/vercel/next.js/releases.atom

運用してみて

2025年11月初旬に作り、約1ヶ月ほど運用しています。社内の反応は良かったです。

ただ、受け取る情報を設定しすぎると情報過多になってしまいます。Slackに発信する量を調整したり、アプリ側にページを設けてSlackにはリンクだけ載せるようにした方が良さそうです。

また、記事内のコードでは gemini-2.5-flash-litegemini-2.5-pro を使っていますが、AIモデルは日々進化しているので、その時点で最適なモデルを選ぶのが良いと思います。

まとめ

  • Vercel AI SDKでAIの呼び出しが簡単
  • 無料枠で十分動く
  • Vercel Cronで定期実行も楽

AIで自分の代わりに情報収集させるのは、思ったより実用的でした。社内ツールなので気軽に試せますし、フィードバックももらいやすいです。

今後も改良を加えて、ニュースの質を高めていきたいです。

参考

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?