生成AIを使って海外AIニュースの要約を社内Teamsに投稿してみた

こんにちは、AIソリューショングループの太田です。

昨年から引き続き生成AIブームが止まらない中、自主的に進めていた取り組みを紹介します。それは海外AIニュースの要約を、社内で使用しているTemasのチャネルに投稿する取り組みです。

投稿自体はPower AutomateやAzure Function、Azure OpenAI Serviceなどを活用することで自動的に投稿しており、数ヶ月運用した結果を踏まえて最近アルゴリズムの改善をおこなったので、改めて方法とノウハウをまとめたいと思います。

はじめに

まずは、なぜ海外AIニュースの要約を投稿しようと思ったのか説明します。

昨年の10月ごろから、「Qiitaの記事を社内Slackに投稿してみた」といった技術記事を社内に自動的に投稿するという事例がX(旧Twitter)で散見されるようになりました。

それらのポストを見て、LLMの応用とAzureの勉強を兼ねて何かできないか考え、外国語のニュースを日本語に翻訳して投稿してみようと思いつきました。

最近のSNSではパーソナライズ化が進み、流れてくるニュースは自分の好みのものばかり表示されます。そこで、日頃見ることの少ない海外の技術ニュースの要約をTeams上で見れればと思い、気軽にスタートしました

実際の投稿は以下のようになっています。一度に複数のニュース記事をQA形式で表示しています。

Teamsに投稿されるニュース要約の結果

投稿の仕組みを順を追って紹介します。

アーキテクチャ紹介

以下のアーキテクチャで投稿を行っています。

海外ニュース要約のアーキテクチャ

今回使用した各技術について紹介します。

Power Automate

Power Automateはローコード自動化プラットフォームです。
Teamsへの投稿にPower Automateを利用します。自動化処理のフローをGUIを操作して定義します。 今回の処理はAzure FunctionsのAPIをコールし、レスポンスの値をもとにTeams用のメッセージを作成して、Teamsのチャネルに投稿するよう設定します。

Teams投稿処理のフロー

Azure Functions

Azure FunctionsはAzure上でコードをサーバーレスで実行できるサービスです。
Azure Functionによりサーバーの構築や保守が不要となり、必要に応じてスケーリングも行ってくれるため、利用者はサーバーを意識することなくアプリケーション開発できます。

今回はAzure Functionsでニュース記事の要約結果を返すAPIを作成します。

Azure Functions を使用したサーバーレス API を作成する | Microsoft Learn

最終的にデプロイしたAPIは、Azure Portal 上からもテストできます。

Azure PortalからAzure FunctionsのAPIテスト画面

azure bing searchでは、ニュース記事要約APIの内部でニュースの検索に利用します。
検索パラメーターの指定には、検索記事数count、どこの国のニュースを手に入れるかの市場コードmkt、安全な検索結果にするためsafeSearchを与えます。

細かいパラメータの仕様は以下を参照しました。

Bing News Search API v7 リファレンス | Microsoft Learn

Azure OpenAI Service(AOAI)

azure のOpenAI サービスです。
OpenAI サービスは、ニュース記事要約APIの内部処理の ① Bing Searchの検索クエリを翻訳、②海外記事の要約、③ 記事概要がわかる日本語QAの作成 で使います。

モデルにはjsonモードを使いたかったので、gpt-35-turbo (1106)を利用しました。

Log Analytics

Azure Log Analyticsは、Azure サービスの様々なログを分析できるサービスです。
今回は、LLMの出力が期待通りか監視するLLMOpsの目的で利用しています。 分析対象は、loggerで出力したコンソールログです。 具体的な説明はこのブログでは割愛しますが、コンソールにjson形式で利用したプロンプト、生成結果、消費トークン数、実行時間などを出力します。

ログを取得後の分析は、下記のチュートリアルを参考にAzure Portal上でKQLクエリを実行することで実現できます。

Log Analytics tutorial - Azure Monitor | Microsoft Learn

海外ニュース要約 APIの処理フロー

Azure Functions上で実行される海外ニュース要約のフローを記述します。その後に工夫した点や苦労した点などをポイントごとに説明します。

海外ニュース要約のフロー

検索ワードの翻訳

特に工夫もなく、翻訳するだけなので、詳細説明を割愛します。

Bing 検索

Bing検索では、Entity, News, Image, Video, Web Searchなどがあります。 今回は、News Searchを利用し、関連するニュース記事の一覧を10件取得します。 ニュースに絞ることで、まとめサイト、企業のブログや商品ページなどを避けることができます。 検索パラメーターの指定には、検索記事数count=10、どこの国のニュースを手に入れるかの市場コードmkt、安全な検索結果にするためsafeSearch=Strictを与えます。 safeSearchStrictにしておくと、 成人向けのコンテンツを返さなくなります。

他にもパラメータには、鮮度freshnessパラメータがあり、最新記事を優先することもできます。 何回か変更して試しましたが、生成AIに関する記事は毎日のように公開されるので特に設定せずに最新のニュースを取得することができました。

HTMLの読み込み

記事によっては、フッター内容が多いことや会員制の記事で読めないことがあります。 そこで、後処理では改行をなくし、文字数の上限を頭から6000字としました。 また内容がない記事をスキップするため、最低500字以上あることを条件としました。

ニュース記事要約

このセクションが一番苦労しました。 最初は思いつきで3行要約をしていました。 しかし、実際に複数記事をまとめてTeamsに投稿すると縦に文章が多い上に長く、画像もなく読みにくかったです。 UI上の工夫を試みましたが、Power Automateでは改行ぐらいしかできず断念。(本当はもっと色々工夫できるらしいですが、そこまでするモチベーションがなかったです。)

そこで、文章量を減らすためにQA形式を取ることにしました。 記事の詳細はリンク先を読んでもらう前提で、どんな記事か雰囲気が分かれば良い程度の気持ちです。 海外のニュース記事を日本語指定で内容が分かるようにQA形式で生成させました。

次の課題は、外国語のままQAを作られることがあったことです。 特に韓国語や中国語ではよくありました。英語でもしばしばおきます。

これは、元記事の文章量の多さとLLMでも言語間で性能差の問題があるからだと考え、一度外国語で要約させるステップを挟んだところだいぶ日本語での回答が安定しました。

そもそも内部処理でgpt4を使えばもっと精度がいいのですが、今回はgpt-35-turbo-16kを使っています。

gpt4を使わない理由は、レスポンスタイムに起因します。 実はPower Automateでは一定の時間以内にレスポンスがないと再リクエストする仕様になっています。 多くの記事を要約しようとすると、その時間上限に引っかかってしまいます。

ニュース記事QA

QAのセクションではLangChainのLCEL (LangChain Expression Language)記法に則り、jsonモードを活用しました。 jsonモードについてはMS learnを確認してください。

Azure OpenAI Serviceで JSON モードを使用する方法 - Azure OpenAI | Microsoft Learn

ニュース記事QAの実装方法をLangChain形式で紹介します。

JSONモードのモデル定義

まずは、json modeのモデル設定をおこないます。

from langchain_openai import AzureChatOpenAI

gpt35_model = AzureChatOpenAI(
        api_key="azure-openai-api-key",
        api_version="2023-12-01-preview",
        model="gpt-35-turbo",
        azure_endpoint="https://xxxxxxxxxxx.openai.azure.com/",
    )
gpt35_json_model = gpt35_model.bind(response_format={"type": "json_object"})
Pydantic で出力制御

今回は、Python で最も広く使用されているデータ検証ライブラリ pydanticを用いてjsonの出力形式を制御します。 まずは、pydanticで生成してほしい値を定義します。各値のdescriptionもプロンプトに使われます。

from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser

class NewsSummary(BaseModel):
    news_title: str = Field(
        description="a short title in Japanese for the article content.",
    )
    question: str = Field(
        description="Write a concise question in Japanese that captures the essence of this article."
    )
    answer: str = Field(
        description="Answer in Japanese, concisely and within three lines, including line breaks for readability. No need for polite language in the text, and use a plain or declarative style at the end of sentences.",
    )

output_parser = PydanticOutputParser(pydantic_object=NewsSummary) 

実際に上記の output_parserを定義すると、生成時に代入されるプロンプト以下になります。

Here is the output schema: からNewsSummary クラスの内容です。それ以前はlangchian側で用意したプロンプトです。

 The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {\"properties\": {\"foo\": {\"title\": \"Foo\", \"description\": \"a list of strings\", \"type\": \"array\", \"items\": {\"type\": \"string\"}}}, \"required\": [\"foo\"]}
the object {\"foo\": [\"bar\", \"baz\"]} is a well-formatted instance of the schema. The object {\"properties\": {\"foo\": [\"bar\", \"baz\"]}} is not well-formatted.

Here is the output schema:
{\"properties\": {\"news_title\": {\"description\": \"a short title in Japanese for the article content\", \"title\": \"News Title\", \"type\": \"string\"}, \"question\": {\"description\": \"Write a concise question in Japanese that captures the essence of this article.\", \"title\": \"Question\", \"type\": \"string\"}, \"answer\": {\"description\": \"Answer in Japanese, concisely and within three lines, including line breaks for readability. No need for polite language in the text, and use a plain or declarative style at the end of sentences.\", \"title\": \"Answer\", \"type\": \"string\"}}, \"required\": [\"news_title\", \"question\", \"answer\"]}
推論実行

大まかな流れは、LangChainでプロンプトとチェインを順番に定義し、最後に推論を実行します。

今回は前段の各記事の要約を使って記事のQAを生成します。

from langchain_community.callbacks import get_openai_callback
from langchain.prompts import PromptTemplate

prompt = PromptTemplate(
            template="""  Please generate a short news_title in Japanese and a question and answer in Japanese for the article content.
                    \n {format_instructions}
                    \n article content: {title}\n{summary}
                    """,
            input_variables=["title", "summary"],
            partial_variables={
                "format_instructions": output_parser.get_format_instructions()
            },
        )
prompt_val = prompt.invoke({"title": title, "summary": summary}) # 変数代入後のプロンプトを取得できます。

chain = prompt | gpt35_json_model | output_parser

with get_openai_callback() as cb: # cb.prompt_tokensで消費トークン数などが取得できます。
    start = time()
    response = chain.invoke({"title": title, "summary": summary})
    end = time()
    if type(response) is not NewsSummary:
        raise Exception("Response is not NewsSummary class")

output_parser を指定する場合は必ず、promptTemplateのtemplateに{format_instructions} を入れてください。 記述を忘れると、先ほど紹介した出力強制用のプロンプト文が使われません。

実行後はresponse がNewsSummary クラスになっているので、APIのレスポンスに詰めるだけです。

NewsSummaryResponse(
                title=response.news_title,
                summary=generated_summary,
                question=response.question,
                answer=response.answer,
            )

投稿結果

投稿結果は以下のようになります。

Power Automateから手動実行するとAzure Functionsが起動し、ニュース要約APIにリクエストが送られます。 内部でニュース要約のフローが走ってレスポンスがPower Automateに返ってTeamsに投稿されます。

実際に投稿された結果です。

投稿結果

ユーザーは基本は投稿を流し見して、QAで興味があればURLを開いて詳細を知る方針です。

最初は記事を読まなくても内容が全部わかることを意識して開発していましたが、それだと文章が長くなりすぎて閲覧されにくくなります。

流し見してもらえるように文字数の文量のコントロールには気を使いました。

改善として、デザイン面で太字を使ったり、Teamsのタイトルも入れて目を惹かせたり、トピックセンテンスをつけて、画像を組み込めるといいなとは思いますが一旦はここまでにしています。

開発した感想

Azureの様々なリソースを活用しつつ、生成AIを使って機能開発するのは楽しいです。 普段の製品開発とは違い、ローコードツールやPaaSを駆使して開発を進めたので新たな学びがありました。

使うツールも製品開発で使用する可能性があるものだったので、ツールの仕様を確認する機会にもなって、非常に有意義でした。 こういったことを独力で実装できるようになったのも、ChatGPTと相談できる影響が大きいと感じます。

生成AIの活用を考える際は、まずは小さく作れるものから作っていき、利用しながら改善して行くことが重要です。それだけでも学びがありますし、思っても見なかったところで点と点が結ばれて、作ったものが形を変えて新たな活用方法が生まれることもあります。

今回は、「グローバルな生成AIの最新情報を、幅広いユーザーに展開する」という弊社の業務課題に取り組みました。本記事をご覧いただいた皆様の業務でも、「XXXなYYYの最新情報を、ZZZに展開する」というご要件がございましたら、同じような仕組みで実現可能ですので、お気軽にご連絡いただければと思います。

以上になります。最後まで読んで頂きありがとうございます。

筆者
AIソリューショングループ
太田真人