OpenAIを利用してAIに校正提案させた話

:::note info
[技術広報 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/tech_pr) の 6日目の記事です。
:::

:::note warn
この記事はAIに校正提案させてみた感想が中心の記事になります。技術的詳細については、[KLab Engineer Advent Calendar 2023](https://qiita.com/advent-calendar/2023/klab) の [Github Actions でカジュアルに OpenAI を利用する](https://qiita.com/gutio/items/c631744e4ff0d4147159)の記事で紹介しているので、ぜひそちらもご覧ください。
:::

こんにちは。KLabで技術広報をしているgutioです。

KLabの技術広報は、エンジニアがタイトルの開発業務などと兼務で技術広報活動に当たっています。私も、ゲームのバックエンド開発やツール開発業務と兼務しながら、技術広報業務を担当しています。
本記事は、技術広報業務のうちの一つである技術ブログの運営について、**記事原稿の校正にOpenAIを利用した話** になります。

## 技術ブログの運営

KLabの技術広報業務のうちの一つに、[技術ブログ](https://www.klab.com/jp/blog/tech/)の運営があります。
大まかな流れとしては、

1. 社内のエンジニアに記事の執筆を依頼する
2. 原稿を執筆してもらう
3. 完成したものを技術ブログシステムに登録する 

という流れです。

このうち、2の原稿を執筆してもらうところについて、GitHub Enterprise で原稿を管理しながら Pull Request でレビューを行うという運用をしています。
レビューでは、技術的な内容については社内識者にレビューを依頼していますが、技術広報でも文章の校正などを行っています。
しかし、技術広報チームは広報の専門家でもありませんし、エンジニアの業務と兼務しているため少しでもレビュー負荷を下げたいと感じていました。

## OpenAIの利用
KLab社内では、[Azure OpenAI Service](https://azure.microsoft.com/ja-jp/products/ai-services/openai-service) の API を比較的自由に利用できる環境が提供されています。
そこで、レビュー負荷を下げる一環として、OpenAI を利用して校正箇所の提案をしたいと考えました。

技術ブログの執筆中の原稿は、GitHub Enterprise で管理しているため、GitHub Actions から OpenAIの `chat/completions` を利用して校正提案を行うことにしました。

GitHub Actions から OpenAIの `chat/completions` を簡単に使えるようにすることについては、切り出して作成しています。

切り出した技術的詳細については以下の記事で紹介しています。
[Github Actions でカジュアルに OpenAI を利用する](https://qiita.com/gutio/items/c631744e4ff0d4147159)

## 利用例
以下の例は、上記の私の原稿に対してOpenAIで校正提案を出した例です。
同じ仕組みで技術ブログの原稿に対しても校正提案を行っています。
![校正提案の例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32726/34a5da56-77ea-ceb8-f5e2-dd4c0ba2da0f.png)

例えば、このように typo の指摘などは結構的確に出てくる印象です。
> 2. "Github Actions" を "GitHub Actions" に修正します。

このワークフローの例は末尾に[付録](#付録)として記載しています。

## 半年運用してみて
このように作った校正提案ツールですが、 半年ほどの運用の間に技術ブログと技術書展15に出した[KLabTechBook Vol.12](https://www.klab.com/jp/blog/tech/2023/tbf15.html)の原稿に対して利用してきました。
これらを通して個人的には以下のような感想があります。

### 校正提案で出てくるものはリクエストごとに少しずつ変わる
typoの指摘は結構的確に出てくる印象です。しかし文章スタイルの指摘などは、同じ文章を校正提案にかけてもリクエストごとに少しずつ変わる印象があります。
そのため、同じ文章を何度も校正提案にかけても、同じ指摘が出てくるとは限らないので注意が必要です。
3回ぐらいやって毎回出てくるものについては、比較的安心して採用しても良いのかなと感じています。

そのため今のところは、執筆者のセルフチェックの一環として利用してもらって、反映したいものだけを執筆者判断で適用するという運用をしています。

### modelによる違い
OpenAIのmodelとしては、`gpt-4-32k` > `gpt-35-turbo` > `gpt-35-turbo-16k` の順で提案内容の質が落ちる印象でした。
当初は `gpt-35-turbo` で運用していましたが、技術ブログの原稿や技術書展用の原稿ということで、より長文が処理可能な（トークン数の多い）モデルに切り替えるなかで感じたものです。
特に `gpt-35-turbo-16k` は、処理できるトークン数を増やすためなのか、結果が不安定でした。
`gpt-4-32k` は処理可能なトークン数が多くても良好な結果が得られているので、今のところこちらを利用しています。

## まとめ
OpenAIを利用してAIに校正提案させた話でした。
今後もAIを活用してより効果的な技術広報活動をしてきたいと考えています。

この記事が少しでも皆さんの参考になれば幸いです。


## 付録
KLabの技術ブログ原稿のリポジトリルールとして、 `執筆開始日-執筆者名` のディレクトリの下にREADME.mdで原稿を執筆するルールになっています。
そのため、PullRequestのブランチ名をディレクトリと同名にするルールにすることで、校正提案にかけるファイルを特定しています。
```yml: .github/workflows/AiProofReading.yml
# AiProofReading: Ai校正のアクションを実行するワークフロー
# このワークフローは、PullRequestのコメントに「校正提案」と書かれたときに実行されます。
name: AiProofReading

on:
  issue_comment:
    types:
      - created

jobs:
  run:
    # 実行トリガのコメントか判定する
    if: (github.event.issue.pull_request != null) && (github.event.comment.body == '校正提案')
    runs-on: ubuntu-latest

    steps:
      # Issue 番号からブランチ名を取得する
      - name: Set Branch
        uses: actions/github-script@v6
        id: set-target-branch
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          result-encoding: string
          script: |
            const pull_request = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.issue.number
            })
            return pull_request.data.head.ref

      # ブランチをチェックアウトする
      - name: Checkout contents
        uses: actions/checkout@v3
        with:
          ref: ${{ steps.set-target-branch.outputs.result }}

      # ブランチ名を整形する
      - name: Extract branch name (not pr)
        env:
          REF: ${{ steps.set-target-branch.outputs.result }}
        shell: bash
        run: |
          echo "BRANCH_NAME=$(echo ${REF})"
          echo "BRANCH_NAME=$(echo ${REF})" >> $GITHUB_ENV

      # README.mdの内容を読み出す
      - name: Get text
        uses: actions/github-script@v6
        id: get_text
        with:
          result-encoding: string
          script: |
            try {
              const fs = require('fs')
              const prOwnerUser = context.payload.issue.user.login
              const text = fs.readFileSync(`${process.env.BRANCH_NAME}/README.md`, 'utf8')
              core.setOutput("text", text);
            } catch(err) {
              core.error("Error file reading")
              core.setFailed(err)
            }

      # Ai1Shotを使って校正提案のアクションを実行する
      - name: RunAi1Shot
        id: run_ai1shot
        uses: gutio/ai1shot@v1.0.0
        with:
          endpoint: ${{ vars.ENDPOINT_URL }}
          key: ${{ secrets.API_KEY }}
          model: "gpt-4-32k"
          system_message: "あなたは広報記事の校正担当です。文章の表記ゆれやTypoチェックしてください。校正箇所の列挙をしてください。"
          user_message: ${{ steps.get_text.outputs.text }}

      # 校正提案をPRコメントで入れる
      - name: ProofReading PR Comment
        uses: actions/github-script@v6
        env:
          PR_COMMENT: ${{ steps.run_ai1shot.outputs.result_assistant_message }}
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |
            // トリガーとなったコメントのユーザーを取得する
            const commentUser = context.payload.comment.user.login
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `@${commentUser}\n` + process.env.PR_COMMENT
            })
```