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
280
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
シンガポールで登壇してきます
yama3133
0
230
AIエージェントに「ノベルティ収集等」を任せたら、営業の承認フローと同じだった話
yama3133
1
72
709JAWS-UG東京.pdf
yama3133
0
350
千葉での単身赴任からAWSをやり続け、千葉に戻ってきた話
yama3133
1
140
打倒A◯◯a Voice! 音声入力アプリを作ってみた
yama3133
1
89
デブサミ福岡CFP提出用
yama3133
1
31
自称宇宙最速で不合格となったAIP-C01にリベンジを果たすべくAIで問題集アプリを作ってみた。
yama3133
0
320
なぜ、私がCommunity Builderに?〜活動期間1か月半でも選出されたワケ〜
yama3133
1
160
ぼくがかんがえたさいきょうのあうとぷっと
yama3133
0
240
Other Decks in Technology
See All in Technology
Control Planeで育てるBtoB SaaSの認証基盤 - SRE NEXT 2026
pokohide
1
2.5k
なぜ私たちのSREプラクティスはなかなか機能しないのか 〜システムより先に組織を見る〜 / Why our SRE practices aren't really working
vtryo
4
3.9k
AI時代の闇と光
tatsuya1970
0
100
AI Driven AI Governance
pict3
0
470
CDKで書くECSのベストプラクティス、 改めて考え直す2026 #cdkconf2026
makies
1
660
SRE Next 2026 何でも屋からの脱却
bto
0
840
人を動かすのは時間ではなく、納得感 〜新任EMが入社3ヶ月、組織を2回変えた話〜
kakehashi
PRO
3
260
Alphaモジュール使っていいのかい!?いけないのかい!?どっちなんだいっ!?
watany
1
240
ruby.wasmとPicoRuby.wasmに対応した仮想DOMライブラリを作ってる話 #kaigieffect_kaigi
sue445
PRO
0
150
Claude Code公式skillで 自分の仕事を少しずつ手放そう!(Claude Code開発ノウハウ大公開スペシャル by クラスメソッド)
kaym
1
450
実践!既存 Project への AI-Driven Development 適用〜 一ヶ月で Project 唯一のフロントエンドエンジニアを作り出せ〜
lycorptech_jp
PRO
0
110
大量データに対しても、生成AIを用いてリーズナブルにデータ加工をしたい!Databricksのai_queryについて調べてみた
kamoshika
1
210
Featured
See All Featured
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
72
40k
GitHub's CSS Performance
jonrohan
1033
470k
The Web Performance Landscape in 2024 [PerfNow 2024]
tammyeverts
12
1.2k
Evolving SEO for Evolving Search Engines
ryanjones
0
240
Documentation Writing (for coders)
carmenintech
77
5.4k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.4k
It's Worth the Effort
3n
188
29k
How to build an LLM SEO readiness audit: a practical framework
nmsamuel
1
800
How to Build an AI Search Optimization Roadmap - Criteria and Steps to Take #SEOIRL
aleyda
1
2.1k
A better future with KSS
kneath
240
18k
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 を指定して完了 → クライアントシークレットはリポに置かない
ご清聴いただき ありがとうございました