Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AIっぽい文章を採点して人間らしく直すアプリを作ってみた

 AIっぽい文章を採点して人間らしく直すアプリを作ってみた

Avatar for Yuuki Yamashita

Yuuki Yamashita

June 16, 2026

More Decks by Yuuki Yamashita

Other Decks in Technology

Transcript

  1. > JAWS-UG AI/ML 支部 // 2025.06.16 AI っぽい文章を採点して 人間らしく直すアプリを作った Amazon

    Bedrock × Strands Agents × AgentCore Gateway TypeScript SDK × AgentCore Gateway — 実装してわかったこと AI 文章判定くん 山下 祐樹 JAWS-UG AI/ML #40:AIエージェントとAI-DLCで加速する!AI/ML実践LT大会
  2. AI 文章判定くん — できること 1 AI っぽさスコア(0 〜 100) 文章の

    AI っぽさを数値化。断定ではなく目安として提 示。 UI にも「誤りを含む・判定の根拠に使わないで」 と明記。 2 具体的な指摘 機械的な接続詞・無難な一般論・定型的な締め・均一 なリズム・具体性の欠如を根拠つきで指摘。 3 人間らしくリライト Strands Agents の反復ループでスコアが基準を下回るま で自動リライトを繰り返す。 日本語 / 英語 対応 テキスト貼り付け .txt / .md / .pdf 入力に対応 MCP ツールとして公開 Claude Code などから 直接呼び出し可能 92 → 12 の実績 Strands Agents の反復ループで AI っぽさスコアを大幅削減 > score_demo.ts // リライト前 92 // リライト後 12 反復ループで 92 → 12 まで 削減 > READY_
  3. 設計判断:断定しない、寄せる NG 断定アプローチ 「これは AI 文章です」 二択で断定する • 誤検知が多い •

    日本語は特に判定が難しい • 人間が手を入れた AI 文章は判別不能 • 「断定」は現状技術では不可能 • UI に「判定の根拠に使わないで」と書く羽目になる OK LLM に寄せる 「AI っぽさ」を添削する スコア + 指摘 + リライト • AI っぽさスコア(0 〜 100)を目安として提示 • 機械的な接続詞・無難な一般論・定型的な締 めを指摘 • 人間らしいリライト案を生成 • LLM が得意なことだけやる • UI にも「断定ではない・誤りを含む」と明記
  4. Next.js 16 + Vercel App Router / Tailwind / フロントエンド

    Amazon Bedrock Converse API / Claude Sonnet 4.6 / us-east-1 Strands Agents SDK TypeScript — 採点 → リライト → 再採点ル ープ AgentCore Gateway MCP エンドポイント / JWT 認証 (Cognito) AWS Lambda Node 22 / MCP ツール実装 / AWS SDK v3 同 梱
  5. モデル選定: list に出ることと invoke できることは別 > 最初の試み Claude Opus 4.8

    を使おうと した 403 AccessDenied • list-foundation-models に出てく る • 実際に invoke できるとは限ら ない • リージョン・アカウント設定 によってアクセス不可 • リストを信じて実装を進めると後で詰 まる > 解決策 Converse で ping を 1 行打って確認 確認コマンド Bash aws bedrock-runtime converse ¥ --region us-east-1 ¥ --model-id us.anthropic.claude-sonnet-4-6 ¥ --messages '["role" : "user" , "content" :["text" : "ping"}]}] ' ¥ --inference-config '"maxTokens" :5}' → Sonnet 4.6 に決定 • Converse API は invoke の実際の可否を返す • モデル ID に乗る前に必ず確認する習慣を • maxTokens: 5 で十分 — 応答速度も速い 教訓:モデル ID に乗る前に、 Converse で実際のアクセス可否を確認する
  6. Strands Agents とは LLM エージェントを数行で書けるフレームワーク ツールを渡して run() を呼ぶだけ。 LLM が目標達成まで自律的に

    ループ。 主要クラス Agent エージェント本体。 tools と model を受け取り run() で実行 tool() ツール定義。 Zod スキーマで型付 き引数を宣言 BedrockModel Bedrock バックエンド。 modelId を渡すだけ structuredOutputSchema 出力を Zod スキーマで型付き JSON に強制 TypeScript SDK · @strands-agents/sdk エージェントループ detect_ai_style 採点(スコア算出) スコア ≤ 20 ? YES NO rewrite_text リライト 繰 り 返 し 完了 — 結果を返す TypeScript SDK · @strands-agents/sdk ツールを渡して run() を呼ぶだけ — LLM が自律的にループする
  7. 反復リライトループ — コードで見ると 採点 → リライト → 再採点 → …

    を自動化 ループ処理は自分で書かない。 LLM が目標スコアを達成するまで自律的に繰り返す 。 ハマりポイント① バンドル問題 Next.js では SDK がバンドルされてしまいエラーに。 serverExternalPackages に追加して外部化が必要。 TypeScript // next.config.ts const nextConfig: NextConfig = { serverExternalPackages: [ "@strands-agents/sdk", "pdf-parse-fork", ], }; export default nextConfig; ハマりポイント② Vercel Hobby タイムアウト ループは 20 〜 45 秒かかる。 Hobby プランの上限 60 秒を超え得る。 import Agent, tool, BedrockModel } from "@strands-agents/sdk"; import z } from "zod"; // AI っぽさスコアを計算するツール const detectAiStyle = tool({ name: "detect_ai_style", description: "文章の AI っぽさをスコアリング", inputSchema: z.object( text: z.string() }), handler: async ( text }) => { return score: await scoreText(text), issues: [...] }; }, }); // リライトするツール const rewriteText = tool({ name: "rewrite_text", description: "AI っぽさを下げるようにリライト", inputSchema: z.object( text: z.string(), issues: z.array(z.string()) }), handler: async ( text, issues }) => { return rewritten: await rewrite(text, issues) }; }, }); // エージェントループ — スコアが閾値を下回るまで繰り返す const agent = new Agent({ model: new BedrockModel( modelId: "us.anthropic.claude-sonnet-4-6" }), tools: [detectAiStyle, rewriteText], systemPrompt: "スコアが 20 以下になるまでリライトを繰り返してください。", }); const result = await agent.run(inputText); 教訓: Strands を使えばツールを使う反復リライトが数行の型付きコードになる
  8. 実際に動かしてみた結果 BEFORE 92 AI っぽさスコア 3 回ループ AFTER 12 AI

    っぽさスコア ツールを使う反復リライトが、数行の型付きコードになった。 実際に動かして出た数字。 Strands Agents 、ちゃんと動きます。 92 → 12 ちゃんと動いた
  9. ハマり ① Next.js でバンドルが壊れる 問題 SDK がバンドルで壊れる Strands Agents SDK

    は Node.js ネイティブモジュールを使 用。 Next.js の Webpack がバンドルしようとすると壊れる。 → 謎のビルドエラー・実行時エラーで詰まる 解決 策 serverExternalPackages を追加 next.config.ts に serverExternalPackages を追加するだけ。 SDK を Webpack のバンドル対象から外す。 → これだけで動く。シンプル。 pdf-parse-fork も同様に外部化が必要 → BUNDLED_EXTERNALLY — OK TypeScript // next.config.ts const nextConfig: NextConfig = { serverExternalPackages: [ "@strands-agents/sdk", "pdf-parse-fork", ], }; export default nextConfig; この 2 行を追加するだけ • @strands-agents/sdk — Strands Agents 本体 • pdf-parse-fork — PDF 読み込みライブラリ なぜ必要か Strands SDK は Node.js ネイティブバインディングを含む。 Next.js の Webpack はこれをブラウザ向けにバンドルしようと して失敗する。 serverExternalPackages で「このパッケージはバンドルしない 」と明示する。
  10. ハマり ② Vercel Hobby のタイムアウト リライトループの実行時間 20 〜 45 秒

    採点 → リライト → 再採点 … のループ vs Vercel Hobby タイムアウト上限 60 秒 Serverless Function の最大実行時間 最短 20 秒 最長 45 秒 上限 60 秒 ! 対策 Vercel Pro にアップグレード タイムアウト上限が 300 秒 に延長 。 根本的な解決策。 バックグラウンド処理に切り 替え 非同期ジョブキューで処理。 ポーリングで結果を取得する設計 に。 ループ回数を制限(暫定) 最大反復回数を設定して 確実に 60 秒以内に収める。
  11. AgentCore Gateway とは? Amazon Bedrock AgentCore What is it? Lambda

    を MCP ツールにする マネージドゲートウェイ 自分で書いた Lambda 関数を MCP プロトコル経由で AI エージェントから呼び出せる。 Claude Code などの MCP クライアントが直接ツール として認識できる。 MCP = Model Context Protocol — AI エージェントがツールを呼 ぶ標準プロトコル できること Lambda を MCP ツールとして外部公開 Claude Code などから直接呼び出し可能 サーバーレスで MCP サーバーを立てられる このアプリも MCP ツールとして公開済み ✓ Flow 呼び出しの流れ MCP クライアント(Claude Code など) MCP / JWT AgentCore Gateway invoke AWS Lambda(ツール実装) Converse API Amazon Bedrock(Claude) Lambda は Node 22 の 1 ファイル。 AWS SDK v3 同梱なので追加 依存なし。
  12. 最大の驚き AgentCore Gateway SURPRISE 無認証の Gateway は作れない authorizerType は CUSTOM_JWT

    の一択。無認証エンドポイントは存在しない。 対応策 1 Cognito User Pool 作成 User Pool ID を控えておく 2 M2M App Client 追加 client_credentials フロー 3 discoveryURL を Gateway 指定 Cognito の OIDC discovery URL クライアント側はこれだけ // JWT を取得(Cognito client_credentials) const token = await getCognitoToken(); // MCP JSON-RPC に Bearer トークンを付けるだけ Authorization: Bearer <token> トークンは短命 — クライアントシークレットはリポに置かず、 AWS 認証経由で実行時に都度 JWT 取得
  13. AgentCore Gateway :認証は必須 authorizerType: CUSTOM_JWT のみ Cognito M2M フ ロー

    無認証エンドポイントは作れ ない Cognito User Pool + M2M App Client が必須 ① User Pool 作成 ② App Client (client_credentials) ③ Gateway に discoveryUrl 指定 TIP: 「無認証は無理」を前提に設計する 。最初から Cognito を組み込む。 Bash # ① Cognito User Pool 作成 aws cognito-idp create-user-pool ¥¥ --pool-name "agentcore-pool" # ② App Client (client_credentials) aws cognito-idp create-user-pool-client ¥¥ --user-pool-id <pool-id> ¥¥ --client-name "gateway-client" ¥¥ --allowed-oauth-flows "client_credentials" ¥¥ --generate-secret # ③ Gateway 作成 aws bedrock-agentcore create-gateway ¥¥ --name "ai-text-checker-gateway" ¥¥ --role-arn <execution-role-arn> ¥¥ --authorizer-type CUSTOM_JWT ¥¥ --authorizer-configuration ¥¥ '{"customJWTAuthorizer" :{"discoveryUrl" : "https://cognito- idp.<region>.amazonaws.com/<pool-id>/.well-known/openid-configuration" }}' 設計のポイント • 「無認証は無理」を前提に設計する • クライアントシークレットはリポに置かず、実行時に都度 JWT 取得 • authorizerType: CUSTOM_JWT のみ対応 • discoveryUrl は Cognito の OpenID Connect エンドポイントを指定
  14. i18n 日英対応:翻訳しない、その言語で答える 「翻訳しない」指示 入力言語を検出し、同じ言語で回答するよう指示。 「英語に翻訳してから答えて」はニュアンスがズレる 言語検出は LLM に任せる "Detect the

    language of the input and respond in the same language." たった 1 行のシステムプロンプトで日英両対応が完成 結果 追加の翻訳ロジック不要。コード 0 行で多言語対応。 LLM が得意なことは LLM に任せる
  15. MCP クライアントからの呼び出し方 Step 1: JWT 取得 Cognito から access_token を取得

    トークンは短命 → 毎回リクエスト前に取得する。 シークレットはリポに置かず AWS 認証経由で取得。 TypeScript // 1. Cognito から JWT トークン取得 const tokenRes = await fetch( `https://$USER_POOL_DOMAIN}/oauth2/token`, { method: "POST", headers: "Content-Type": "application/x-www-form- urlencoded" }, body: new URLSearchParams({ grant_type: "client_credentials", client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: "ai-text-checker/invoke", }), } ); const access_token } = await tokenRes.json(); Step 2: tools/call Bearer JWT 付きで MCP JSON-RPC Authorization: Bearer <token> を付けるだけ。 ツール名は detect___detect_ai_style 形式で指定。 TypeScript // 2. MCP JSON-RPC 呼び出し(Bearer JWT 付き) const mcpRes = await fetch(GATEWAY_URL, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer $access_token}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "detect___detect_ai_style", arguments: text: inputText }, }, }), }); const result = await mcpRes.json(); initialize → tools/list → tools/call ← MCP ハンドシェイク順序(直接 tools/call でも動作)
  16. まとめ — 4 つの教訓 作ってみてわかった。でも、ちゃんと動いた。 1 「これは AI ?」には正直に 断定はしない。目安スコア+具体的な指摘+リライトに寄せる

    。 LLM が得意なことに設計を合わせる。 TypeScript // 断定しない設計 const result = { aiScore: 73, // 0-100 の目安 feedback: [...], // 具体的な指摘 rewritten: "..." // リライト案 }; 2 モデル ID より先に Converse で確認 list-foundation-models に出ることと invoke できることは別。 1 行の Converse 呼び出しでアクセス可否を確認してから進む。 Bash aws bedrock-runtime converse ¥ --region us-east-1 ¥ --model-id us.anthropic.claude-sonnet-4-6 ¥ --messages '["role" : "user" , "content" :["text" : "ping"}]}] ' ¥ --inference-config '"maxTokens" :5}' 3 Strands は外部化してから使う 反復リライトループが数行の型付きコードになる。 ただし Next.js では必ず serverExternalPackages で外部化する。 TypeScript // next.config.ts serverExternalPackages: [ "@strands-agents/sdk", "pdf-parse-fork" ] 4 Gateway は JWT 必須で設計する Lambda を MCP ツールにする手段として綺麗。 ただし無認証エンドポイントは作れない。最初から Cognito を 前提に。 Text authorizerType: CUSTOM_JWT のみ → Cognito User Pool + M2M app client → discoveryURL を指定して完了 → クライアントシークレットはリポに置かない