テスト駆動ライティングのサンプルコードです。
package.json
{
"name": "test-driven-writing",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "vitest run"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.6.5",
"devDependencies": {
"@types/node": "^24.3.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"dependencies": {
"gray-matter": "^4.0.3",
"ollama": "^0.5.17",
"zod": "^4.0.17"
}
}
vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
testTimeout: 0,
},
});
check.test.ts
import { expect, describe, it } from "vitest";
import { z } from "zod";
import ollama from "ollama";
import * as fs from "fs/promises";
import matter from "gray-matter";
const model = "gemma3:12b";
const EvaluationResultSchema = z.object({
is_passed: z.boolean().describe("テストに合格したかどうか (true/false)"),
score: z.number().min(0).max(100).describe("100点満点での評価スコア"),
feedback: z.string().describe("具体的なフィードバックや改善点の指摘"),
});
type EvaluationResult = z.infer<typeof EvaluationResultSchema>;
const format = z.toJSONSchema(EvaluationResultSchema);
const parseContent = (content: string): EvaluationResult => {
try {
return EvaluationResultSchema.parse(JSON.parse(content));
} catch (error) {
console.error("Failed to parse content:", error);
throw new Error("Invalid content format");
}
};
const printResult = (result: EvaluationResult) => {
console.log("Evaluation Result:");
console.log(`- Passed: ${result.is_passed}`);
console.log(`- Score: ${result.score}`);
console.log(`- Feedback: ${result.feedback}`);
};
console.log("Reading article from:", process.env.ARTICLE_PATH);
if (!process.env.ARTICLE_PATH) {
console.error(`ARTICLE_PATH is not set. \nRun like: \`ARTICLE_PATH="/path/to/article.md" pnpm test\``);
process.exit(1);
}
const fileContent = await fs.readFile(process.env.ARTICLE_PATH!, "utf-8");
const parsed = matter(fileContent);
const articleText = parsed.content;
const frontmatter = parsed.data as {
title: string;
themes: string[];
target_readers: string[];
};
console.log("Article content:", articleText.slice(0, 100) + "...");
const createPrompt = (role: string) => {
return `あなたはブログ記事を評価する文章作成の専門家です。
指定された条件を満たしているかどうかを評価し、フィードバックを提供することが目標です。
まず、ブログ記事のテーマとターゲットオーディエンスを確認してください:
<theme_and_target>
- ブログのテーマ: ${
frontmatter.themes ? frontmatter.themes.join(", ") : "未設定"
}
- ターゲット読者: ${
frontmatter.target_readers
? frontmatter.target_readers.join(", ")
: "未設定"
}
</theme_and_target>
次に、評価にあたって注意するべきルールを確認してください:
<rules>
- {% linkcard /%} のような記法は、独自に設定しているものなので、使用して問題ありません
- 率直で正直な意見を聞かせてください
- 厳しい評価基準で判断してください
</rules>
続いて、あなたが評価するべき観点を確認してください:
<role>${role}</role>
以下の記事を注意深く読み、分析してください:
<title>${frontmatter.title || "未設定"}</title>
<article>${articleText}</article>
分析後、記事を1から100のスケールで採点してください。1は完全にトピックから外れている、100はテーマとターゲットオーディエンスと完璧に合致している、とします。
評価に基づいて、記事が基準を満たしているかどうかを判断してください。記事は70点以上を獲得し、テーマの主要ポイントを十分に扱っている場合に合格と見なされます。
評価を以下のJSON形式にまとめてください:
{
"is_passed": boolean,
"score": integer,
"feedback": "評価の簡潔な要約を提供し、主な長所と改善点を含めてください。なぜその点数を付けたのか、そして記事が基準を満たした、または満たさなかった理由を説明してください。"
}
フィードバックは建設的で具体的なものにし、必要に応じて改善のための実行可能な提案を提供してください。
`;
};
describe("テスト駆動ライティング", () => {
it("テーマ", async () => {
// role にテストケース固有の指示を入力し、createPrompt で prompt を生成する
// そして ollama を用いてローカル LLM 経由でレスポンスを生成する
const role = `
- 記事がテーマに合致したものになっているかを評価してください
`;
const response = await ollama.generate({
model,
format,
prompt: createPrompt(role),
});
// 生成結果を parse し、ログ出力する
const evaluation = parseContent(response.response);
printResult(evaluation);
// 評価結果を検証し、期待される条件を満たしているか確認する
expect(evaluation.is_passed, `テスト不合格: ${evaluation.feedback}`).toBe(
true
);
// ここではスコアが70点以上であることを確認する
expect(
evaluation.score,
`スコアが基準値未満です: ${evaluation.score}点\nフィードバック: ${evaluation.feedback}`
).toBeGreaterThanOrEqual(70);
});
it("対象読者", async () => {
const role = `
- 記事が対象読者に向けた書き方になっているかを評価してください
`;
const response = await ollama.generate({
model,
format,
prompt: createPrompt(role),
});
const evaluation = parseContent(response.response);
printResult(evaluation);
expect(evaluation.is_passed, `テスト不合格: ${evaluation.feedback}`).toBe(
true
);
expect(
evaluation.score,
`スコアが基準値未満です: ${evaluation.score}点\nフィードバック: ${evaluation.feedback}`
).toBeGreaterThanOrEqual(70);
});
it("文章の流れ", async () => {
const role = `
- 記事全体が一貫した流れを持っているかを評価してください
- 読みやすい記事になっているかを評価してください
`;
const response = await ollama.generate({
model,
format,
prompt: createPrompt(role),
});
const evaluation = parseContent(response.response);
printResult(evaluation);
expect(evaluation.is_passed, `テスト不合格: ${evaluation.feedback}`).toBe(
true
);
expect(
evaluation.score,
`スコアが基準値未満です: ${evaluation.score}点\nフィードバック: ${evaluation.feedback}`
).toBeGreaterThanOrEqual(70);
});
it("タイトル", async () => {
const role = `
- 記事のタイトルが内容を適切に反映しているかを評価してください
- 読者が読みたくなるタイトルかを評価してください
`;
const response = await ollama.generate({
model,
format,
prompt: createPrompt(role),
});
const evaluation = parseContent(response.response);
printResult(evaluation);
expect(evaluation.is_passed, `テスト不合格: ${evaluation.feedback}`).toBe(
true
);
expect(
evaluation.score,
`スコアが基準値未満です: ${evaluation.score}点\nフィードバック: ${evaluation.feedback}`
).toBeGreaterThanOrEqual(70);
});
});