Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
AIっぽい文章を採点して人間らしく直すアプリを作ってみた
Search
Yuuki Yamashita
June 16, 2026
Technology
260
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
AIっぽい文章を採点して人間らしく直すアプリを作ってみた
Yuuki Yamashita
June 16, 2026
More Decks by Yuuki Yamashita
See All by Yuuki Yamashita
709JAWS-UG東京.pdf
yama3133
0
110
千葉での単身赴任からAWSをやり続け、千葉に戻ってきた話
yama3133
1
130
打倒A◯◯a Voice! 音声入力アプリを作ってみた
yama3133
1
86
デブサミ福岡CFP提出用
yama3133
1
29
自称宇宙最速で不合格となったAIP-C01にリベンジを果たすべくAIで問題集アプリを作ってみた。
yama3133
0
310
なぜ、私がCommunity Builderに?〜活動期間1か月半でも選出されたワケ〜
yama3133
1
160
ぼくがかんがえたさいきょうのあうとぷっと
yama3133
0
230
Amazon S3 Filesについて
yama3133
2
260
AWSで2番目にリリースされたサービスについてお話しします(諸説あります)
yama3133
1
190
Other Decks in Technology
See All in Technology
デジタル・デザイン:次の50年を描く「進化する青写真」
y150saya
0
800
Why is RC4 still being used?
tamaiyutaro
0
280
cccccc
moznion
0
1.6k
どうして今サーバーサイドKotlinを選択したのか
nealle
0
190
CVE-2026-20833_脆弱性対応とAES 化について
jukishiya
0
350
グローバルチームと挑むプロダクト開発
sansantech
PRO
1
150
5分でわかるDuckDB Quack
chanyou0311
4
300
AI・ロボティクスと自動化社会 / AI, Robotics, and the Automated Society
ks91
PRO
0
150
AI時代のエンジニアキャリアについて今一度考える
sakamoto_582
1
830
クラウドファンディング版StackChan 3体(4体)をインタラクティブな体験型作品にして展示もした話 / スタックチャンお誕生日会2026
you
PRO
0
270
Hatena Engineer Seminar 37 jj1uzh
jj1uzh
0
420
トークン最適化のためのユーザーストーリー分析 / User Story Analysis for Token Optimization
oomatomo
0
160
Featured
See All Featured
Docker and Python
trallard
47
3.9k
RailsConf 2023
tenderlove
30
1.5k
What does AI have to do with Human Rights?
axbom
PRO
1
2.2k
The B2B funnel & how to create a winning content strategy
katarinadahlin
PRO
1
410
Joys of Absence: A Defence of Solitary Play
codingconduct
1
410
How STYLIGHT went responsive
nonsquared
100
6.2k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
AI: The stuff that nobody shows you
jnunemaker
PRO
8
760
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.8k
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
1
310
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
300
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.8k
Transcript
> 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大会
自己紹介 山下 祐樹 都内Sier企業にてCCoE AWS Community Builder (AI Engineering)
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_
DEMO アプリ画面 試してみてください! ai-text-checker-pi.vercel.app GitHub github.com/yama3133/ai-text-checker > SCAN TO TRY_
設計判断:断定しない、寄せる NG 断定アプローチ 「これは AI 文章です」 二択で断定する • 誤検知が多い •
日本語は特に判定が難しい • 人間が手を入れた AI 文章は判別不能 • 「断定」は現状技術では不可能 • UI に「判定の根拠に使わないで」と書く羽目になる OK LLM に寄せる 「AI っぽさ」を添削する スコア + 指摘 + リライト • AI っぽさスコア(0 〜 100)を目安として提示 • 機械的な接続詞・無難な一般論・定型的な締 めを指摘 • 人間らしいリライト案を生成 • LLM が得意なことだけやる • UI にも「断定ではない・誤りを含む」と明記
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 同 梱
モデル選定: 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 で実際のアクセス可否を確認する
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 が自律的にループする
反復リライトループ — コードで見ると 採点 → リライト → 再採点 → …
を自動化 ループ処理は自分で書かない。 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 を使えばツールを使う反復リライトが数行の型付きコードになる
実際に動かしてみた結果 BEFORE 92 AI っぽさスコア 3 回ループ AFTER 12 AI
っぽさスコア ツールを使う反復リライトが、数行の型付きコードになった。 実際に動かして出た数字。 Strands Agents 、ちゃんと動きます。 92 → 12 ちゃんと動いた
ハマり ① 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 で「このパッケージはバンドルしない 」と明示する。
ハマり ② Vercel Hobby のタイムアウト リライトループの実行時間 20 〜 45 秒
採点 → リライト → 再採点 … のループ vs Vercel Hobby タイムアウト上限 60 秒 Serverless Function の最大実行時間 最短 20 秒 最長 45 秒 上限 60 秒 ! 対策 Vercel Pro にアップグレード タイムアウト上限が 300 秒 に延長 。 根本的な解決策。 バックグラウンド処理に切り 替え 非同期ジョブキューで処理。 ポーリングで結果を取得する設計 に。 ループ回数を制限(暫定) 最大反復回数を設定して 確実に 60 秒以内に収める。
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 同梱なので追加 依存なし。
最大の驚き 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 取得
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 エンドポイントを指定
i18n 日英対応:翻訳しない、その言語で答える 「翻訳しない」指示 入力言語を検出し、同じ言語で回答するよう指示。 「英語に翻訳してから答えて」はニュアンスがズレる 言語検出は LLM に任せる "Detect the
language of the input and respond in the same language." たった 1 行のシステムプロンプトで日英両対応が完成 結果 追加の翻訳ロジック不要。コード 0 行で多言語対応。 LLM が得意なことは LLM に任せる
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 でも動作)
まとめ — 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 を指定して完了 → クライアントシークレットはリポに置かない
ご清聴いただき ありがとうございました