この記事はRust+SvelteKit+CDK で RSS 要約アプリを作ってみる Advent Calendar 2025の 20 日目の記事になります。
また、筆者が属している株式会社野村総合研究所のアドベントカレンダーもあるので、ぜひ購読ください。
はじめに
今回の Web アプリでは、記事の内容を AI に投げて要約しています。今回はコストパフォーマンスとモデルの切り替えやすさを考慮し、OpenRouter 経由で GPT-4o-mini を利用する構成にしました。
OpenRouter とは?
各ベンダの各モデルを一元的に呼びだせる API サービスです。ChatGPT や Gemini、Claude を、同じフォーマットのリクエストで呼び出すことができます。いろいろなモデルを使い分けたい場合、各ベンダごとに API キーを発行して管理したり、リクエストのフォーマットをそれぞれのモデルのに合わせたりという手間が発生します。OpenRouter を使うとそれらを統一することができます。
モデルの選択
昨今はモデルの性能がどんどん上がり、高度な数学やプログラミングが可能になっています。一方で今回は記事の要約であり、その記事も論文ほどの長さではないため、性能よりもコスパを重視しました。
安価なモデルはいくつか用意されていますが、今回はその中でも GPT-4o-mini を選択しました。OpenRouter 上で$0.15/M input tokensで利用できます。例えば GPT の標準的なモデルである 4.1 は$2/M input tokensなので、ほぼ 7%のコストで利用できます。その分性能も控えめですが、記事の要約程度であれば十分働いてくれます。
具体的な実装
OpenRouter クライアントの実装
cdk/lambda/src/lib.rsにOpenRouterRepositoryを実装しています。
reqwestを使って HTTP リクエストを送るシンプルな構成です。
pub struct OpenRouterRepository {
api_key: String,
client: reqwest::Client,
}
impl OpenRouterRepository {
pub async fn summarize(&self, text: &str, short: bool) -> Result<String, reqwest::Error> {
// プロンプトの切り替え
let system_prompt = if short {
"あなたはプロのエンジニアです。記事をマークダウン化したものを渡すので。記事の内容を理解し、100文字以内の日本語で要約してください。"
} else {
"あなたはプロのエンジニアです。記事をマークダウン化したものを渡すので。記事の内容を理解し、500文字以内の日本語で丁寧に要約してください。"
};
let request_body = serde_json::json!({
"model": "openai/gpt-4o-mini", // コスパ最強モデル
"messages": [
{ "role": "system", "content": system_prompt },
{ "role": "user", "content": text }
]
});
// ... リクエスト送信処理
}
}
HTML タグの除去
LLM に生の HTML を投げるとトークン数を無駄に消費してしまいます。
Rust のクレートで、HTML をマークダウンに変換してくれるhtml2mdというものがあるので、それを利用しています。
// cdk/lambda/src/bin/processor.rs
let artcle_md = html2md::rewrite_html(&article_content, false);
結果の格納
生成された要約は、DynamoDB のupdate_itemを使って元の記事のレコードに保存します。この際、ステータスをSummarizedに更新することで、フロントエンド側で要約済みとして扱えるようにしています。
// cdk/lambda/src/bin/processor.rs
let keys = HashMap::from([(
"id".to_string(),
AttributeValue::S(message.article_id.clone()),
)]);
let updates = HashMap::from([
(
"summary_short".to_string(),
AttributeValue::S(summary_short.clone()),
),
(
"summary_long".to_string(),
AttributeValue::S(summary_long.clone()),
),
(
"state".to_string(),
AttributeValue::S(ArticleState::Summarized.to_string()),
),
]);
article_table_client.update_item(keys, updates).await?;