少数データソースへのプロンプト改善を加速させるCLIを作ってみた
AIを使う上で、困難な課題の一つとしてプロンプトに対するレスポンスの改善があります。特定のファイルをデータソースとして登録した際に、冗長な表現で意図した回答が返ってくるのか検証するものの、結果をデータとしてまとめるのは意外に大変です。
Bedrockを使ってMarkdownの生成を行うプロセスを組み立てていた最中、ふと少し工夫すればデータソースとプロンプトとレスポンスの組み合わせを元にデータソースの頻繁な改善作業に集中できるCLIが作れると思いつき、試してみました。
設定
Claude Codeとの壁打ちによって出力されたコードになります。
実行にはAWS認証情報(AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY) が必要です。モデルはClaude 3 Haiku が初期設定となりますが、必要に応じて変更しましょう。
環境は以下の通り。
- Node.js v18以上
- pnpm(または npm/yarn)
- AWSアカウント(Bedrockアクセス権限付き)
環境設定
# 1. ディレクトリ作成と移動
mkdir -p bedrock-claude-cli && cd bedrock-claude-cli
# 2. package.json の作成 (pnpm 用に微調整)
cat > package.json << 'EOF'
{
"name": "bedrock-claude-cli",
"version": "1.0.0",
"type": "module",
"scripts": {
"bedrock-claude": "tsx src/bedrockClaudeCli.ts"
}
}
EOF
# 3. pnpm でのインストール
pnpm add @aws-sdk/client-bedrock-runtime
pnpm add -D typescript tsx @types/node
# 4. tsconfig.json の作成
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
EOF
mkdir -p src
# 5. src/bedrockClaude.ts の作成
cat > src/bedrockClaude.ts << 'EOFTS'
import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
import * as fs from "fs";
import * as path from "path";
const BEDROCK_REGION = process.env.AWS_REGION || "ap-northeast-1";
const DEFAULT_MODEL_ID = process.env.BEDROCK_MODEL_ID || "anthropic.claude-3-haiku-20240307-v1:0";
export type ContentType = "text" | "pdf" | "image";
export type ImageMediaType = "image/jpeg" | "image/png" | "image/gif" | "image/webp";
export type PdfMediaType = "application/pdf";
export interface InputContent {
type: ContentType;
text?: string;
base64Data?: string;
mediaType?: ImageMediaType | PdfMediaType;
}
export interface BedrockClaudeOptions {
systemPrompt?: string;
userPrompt: string;
contents?: InputContent[];
modelId?: string;
maxTokens?: number;
region?: string;
debug?: boolean;
}
export interface BedrockClaudeResult {
text: string;
inputTokens?: number;
outputTokens?: number;
stopReason?: string;
rawRequest?: unknown;
rawResponse?: unknown;
}
const clientCache = new Map<string, BedrockRuntimeClient>();
function getBedrockClient(region: string): BedrockRuntimeClient {
if (!clientCache.has(region)) {
clientCache.set(region, new BedrockRuntimeClient({ region }));
}
return clientCache.get(region)!;
}
export function encodeFileToBase64(filePath: string): string {
const absolutePath = path.resolve(filePath);
const fileBuffer = fs.readFileSync(absolutePath);
return fileBuffer.toString("base64");
}
export function getMediaTypeFromPath(filePath: string): ImageMediaType | PdfMediaType | null {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case ".jpg": case ".jpeg": return "image/jpeg";
case ".png": return "image/png";
case ".gif": return "image/gif";
case ".webp": return "image/webp";
case ".pdf": return "application/pdf";
default: return null;
}
}
export function createContentFromFile(filePath: string): InputContent {
const mediaType = getMediaTypeFromPath(filePath);
if (!mediaType) throw new Error(`サポートされていないファイル形式: ${filePath}`);
const base64Data = encodeFileToBase64(filePath);
if (mediaType === "application/pdf") {
return { type: "pdf", base64Data, mediaType };
}
return { type: "image", base64Data, mediaType };
}
export function createTextContent(text: string): InputContent {
return { type: "text", text };
}
function buildContentBlocks(userPrompt: string, contents?: InputContent[]): unknown[] {
const blocks: unknown[] = [];
if (contents) {
for (const content of contents) {
if (content.type === "text" && content.text) {
blocks.push({ type: "text", text: content.text });
} else if (content.type === "image" && content.base64Data && content.mediaType) {
blocks.push({
type: "image",
source: { type: "base64", media_type: content.mediaType, data: content.base64Data },
});
} else if (content.type === "pdf" && content.base64Data) {
blocks.push({
type: "document",
source: { type: "base64", media_type: "application/pdf", data: content.base64Data },
});
}
}
}
blocks.push({ type: "text", text: userPrompt });
return blocks;
}
export async function invokeBedrockClaude(options: BedrockClaudeOptions): Promise<BedrockClaudeResult> {
const {
systemPrompt, userPrompt, contents,
modelId = DEFAULT_MODEL_ID, maxTokens = 8192,
region = BEDROCK_REGION, debug = false,
} = options;
const client = getBedrockClient(region);
const requestBody: Record<string, unknown> = {
anthropic_version: "bedrock-2023-05-31",
max_tokens: maxTokens,
messages: [{ role: "user", content: buildContentBlocks(userPrompt, contents) }],
};
if (systemPrompt) requestBody.system = systemPrompt;
const command = new InvokeModelCommand({
modelId, contentType: "application/json", accept: "application/json",
body: JSON.stringify(requestBody),
});
const response = await client.send(command);
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
if (!responseBody.content || !responseBody.content[0]?.text) {
throw new Error("Bedrockからの応答が不正です");
}
const result: BedrockClaudeResult = {
text: responseBody.content[0].text.trim(),
inputTokens: responseBody.usage?.input_tokens,
outputTokens: responseBody.usage?.output_tokens,
stopReason: responseBody.stop_reason,
};
if (debug) {
result.rawRequest = requestBody;
result.rawResponse = responseBody;
}
return result;
}
EOFTS
# 6. src/bedrockClaudeCli.ts の作成
cat > src/bedrockClaudeCli.ts << 'EOFCLI'
#!/usr/bin/env node
import * as fs from "fs";
import { invokeBedrockClaude, createContentFromFile, createTextContent, type InputContent } from "./bedrockClaude.js";
function parseArgs(args: string[]) {
const result = {
prompt: undefined as string | undefined,
system: undefined as string | undefined,
files: [] as string[],
text: undefined as string | undefined,
model: "anthropic.claude-3-haiku-20240307-v1:0",
json: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case "-p": case "--prompt": result.prompt = args[++i]; break;
case "-s": case "--system": result.system = args[++i]; break;
case "-f": case "--file": result.files.push(args[++i]); break;
case "-t": case "--text": result.text = args[++i]; break;
case "-m": case "--model": result.model = args[++i]; break;
case "--json": result.json = true; break;
}
}
return result;
}
async function main(): Promise<void> {
const opts = parseArgs(process.argv.slice(2));
if (!opts.prompt) {
console.error("Usage: pnpm bedrock-claude -- -p 'prompt' [-f file]");
process.exit(1);
}
const contents: InputContent[] = [];
if (opts.text) contents.push(createTextContent(opts.text));
for (const filePath of opts.files) {
if (!fs.existsSync(filePath)) { console.error(`File not found: ${filePath}`); process.exit(1); }
contents.push(createContentFromFile(filePath));
}
try {
const result = await invokeBedrockClaude({
userPrompt: opts.prompt, systemPrompt: opts.system,
contents: contents.length > 0 ? contents : undefined,
modelId: opts.model,
});
console.log(opts.json ? JSON.stringify(result, null, 2) : result.text);
} catch (error) {
console.error(`Error: ${error}`);
process.exit(1);
}
}
main();
EOFCLI
echo "✓ pnpm セットアップ完了!"
使い方
プロンプトと分析を行いたいファイル名を指定します。必要に応じてモデルも指定しましょう。
$ pnpm bedrock-claude -p '要約' -f '/path/to/サービス仕様書集.pdf' --raw -m anthropic.claude-3-5-sonnet-20240620-v1:0
> project@1.0.0 bedrock-claude /path/to/project
> tsx src/bedrockClaudeCli.ts -p '要約' -f '/path/to/サービス仕様書集.pdf' --raw -m anthropic.claude-3-5-sonnet-20240620-v1:0
{
"request": {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 8192,
"messages": [
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": "[BASE64: 3.1MB]"
}
},
{
"type": "text",
"text": "要約"
}
]
}
]
},
"response": {
"id": "msg_bdrk_xxxxxxxxxxxxxxxxxx",
"type": "message",
"role": "assistant",
"model": "claude-3-5-sonnet-20240620",
"content": [
{
"type": "text",
"text": "...."
}
],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 183780,
"output_tokens": 420
}
},
"result": "..."
}
データソースとしたいファイルは複数指定可能です。
pnpm bedrock-claude -p "これらの文書を比較してください" \
-f doc1.pdf \
-f doc2.pdf \
-f image.png
ファイル操作の動作フローとしては以下の通りです。ステートレスで、どこにも保存しません。
- ファイル指定
- Base64エンコード
- Bedrockに送信
- レスポンス表示
少数で小さいファイル向けです。目安としてはPDFの場合は4.5MBまで、多くとも100ページまでにとどめましょう。大きめなファイルはRAGを推奨します。
あとがき
1問1答の少数ファイルを対象として、様々な言い回しに対して特定ファイルから得られる回答を素早く調べたい状況において重宝しています。
ファイルに対するプロンプトの結果を知りたいものの、RAGを構築・維持する程まではない場合におすすめです。






