0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

会話シナリオのテストケースをYAMLで管理する最小例

0
Posted at

この記事はPogeeの編集企画として、生成AIを用いて作成しています。YAML、設定項目、質問、応答はすべて独立した架空の教材です。Pogeeの内部テスト形式、非公開API、実際の会話ログは使っていません。掲載コードはローカルで実行確認しています。

会話シナリオを確認するとき、質問だけをメモしても、何を満たせば合格なのかが後から分からなくなります。入力、確認する応答、期待する条件を同じケースに残すと、条件を変えたときも同じ検査を再実行できます。ここではYAMLに3件の合成ケースを書き、Node.jsで構造と文字列条件を検査します。

前回はJSON Schemaでキャラクター設定の構造を確認しました。今回は設定から一段進み、会話テストの記録を扱います。ただしAIモデルには接続せず、あらかじめ書いた応答を検査する最小例に絞ります。Pogeeの会話性能を測定した結果ではありません。

前回:AIキャラクター設定をJSON Schemaで検証する最小例

YAML、構造検証、応答検査を分ける

YAMLが読めること、必要なフィールドがあること、応答が条件を満たすことは別の判定です。例えばreplyが欠けていればテストデータの不備です。replyはあっても、夜の場面で期待した「こんばんは」がなければ、この例で定義した文字列条件には不合格です。

qiita-d02-yaml-figure-v5.png

図1:合成応答3件を検査する流れと実行結果。AI支援で作成した説明図で、Pogeeの製品画面や実際の会話テスト結果ではありません。

実行環境と準備

実行確認はNode.js v25.2.1、yaml 2.8.1、Ajv 8.18.0で行いました。空の作業ディレクトリで次を実行し、続く2ファイルを保存します。サンプルコードはCommonJSなので.cjsを使います。

npm init -y
npm install --save-exact yaml@2.8.1 ajv@8.18.0
node check-scenarios.cjs scenarios.yaml

scenarios.yaml:入力と判定条件を一緒に残す

夜の図書館の案内係を想定した架空の例です。replyはこの教材用に固定した合成サンプルで、モデルを呼び出して取得した返答ではありません。最後のgreeting_regressionだけ、期待したあいさつと異なる応答を入れています。inputは実施条件を残すための記録で、このスクリプトからモデルへ送信する処理はありません。

version: 1
cases:
  - id: evening_greeting
    input: "今から本を探したいです。"
    reply: "こんばんは。どのような本をお探しですか。"
    expect:
      must_include: ["こんばんは"]
      must_not_include: ["電話番号"]
  - id: unknown_fact
    input: "私が昨日借りた本を教えてください。"
    reply: "その情報は分かりません。書名を教えていただけますか。"
    expect:
      must_include: ["分かりません"]
      must_not_include: ["確認しました"]
  - id: greeting_regression
    input: "夜の図書館に来ました。"
    reply: "こんにちは。どのような本をお探しですか。"
    expect:
      must_include: ["こんばんは"]
      must_not_include: ["電話番号"]

must_includeはすべての文字列が含まれること、must_not_includeはどの文字列も含まれないことを意味します。例の「電話番号」は検査方法を示すための語で、この検査だけで個人情報の取得や漏えいを網羅的に検出できるという意味ではありません。

check-scenarios.cjs:形を確かめてから判定する

const fs = require('node:fs');
const YAML = require('yaml');
const Ajv = require('ajv');

const schema = {
  type: 'object', additionalProperties: false,
  required: ['version', 'cases'],
  properties: {
    version: { const: 1 },
    cases: {
      type: 'array', minItems: 1,
      items: {
        type: 'object', additionalProperties: false,
        required: ['id', 'input', 'reply', 'expect'],
        properties: {
          id: { type: 'string', pattern: '^[a-z][a-z0-9_]+$' },
          input: { type: 'string', minLength: 1 },
          reply: { type: 'string', minLength: 1 },
          expect: {
            type: 'object', additionalProperties: false,
            required: ['must_include', 'must_not_include'],
            properties: {
              must_include: { type: 'array', uniqueItems: true, items: { type: 'string', minLength: 1 } },
              must_not_include: { type: 'array', uniqueItems: true, items: { type: 'string', minLength: 1 } }
            }
          }
        }
      }
    }
  }
};

try {
  const data = YAML.parse(fs.readFileSync(process.argv[2] || 'scenarios.yaml', 'utf8'));
  const validate = new Ajv({ allErrors: true }).compile(schema);
  if (!validate(data)) throw new Error('schema: ' + JSON.stringify(validate.errors));
  const ids = new Set(data.cases.map(c => c.id));
  if (ids.size !== data.cases.length) throw new Error('duplicate case id');
  let failed = 0;
  for (const c of data.cases) {
    const missing = c.expect.must_include.filter(s => !c.reply.includes(s));
    const forbidden = c.expect.must_not_include.filter(s => c.reply.includes(s));
    const ok = missing.length === 0 && forbidden.length === 0;
    if (!ok) failed++;
    console.log(`${c.id}: ${ok ? 'PASS' : 'FAIL'}`);
    if (missing.length) console.log('  missing: ' + missing.join(', '));
    if (forbidden.length) console.log('  forbidden: ' + forbidden.join(', '));
  }
  console.log(`summary: ${data.cases.length - failed} passed, ${failed} failed`);
  process.exitCode = failed ? 1 : 0;
} catch (error) {
  console.error('CONFIG ERROR: ' + error.message);
  process.exitCode = 2;
}

Ajvでは必須キー、型、空のcases、未知のキーを検査します。ケースIDの重複はSetで別に確認します。YAMLの同じマッピング内でキーが重複した場合はyamlパーサーでエラーになります。must_includeとmust_not_includeの配列は空でもよいですが、両方空なら応答内容の条件は何も検査していません。

実際の実行結果

evening_greeting: PASS
unknown_fact: PASS
greeting_regression: FAIL
  missing: こんばんは
summary: 2 passed, 1 failed

この実行の終了コードは1です。greeting_regressionは構造として正しいYAMLですが、replyに「こんばんは」がないためFAILになります。意図的な失敗ケースなので、記事のコードをそのまま実行したときに終了コード1になるのは想定どおりです。

最後のreplyを「こんばんは。どのような本をお探しですか。」へ直すと、このケースの文字列条件も満たします。検査コードは全件合格なら0、応答条件に失敗があれば1、YAMLの解析やデータ構造の不備なら2を返します。別の合成入力で正常終了0、expectの欠落、ケースIDの重複、YAMLキーの重複による終了2も確認しました。

文字列一致の限界を残しておく

includesは部分文字列の一致です。例えば「こんばんはと言う必要はありません」にも「こんばんは」が含まれます。逆に「今晩は」のような表記差は一致しません。挨拶の自然さ、文脈に沿った意味、設定の一貫性、安全性まで自動的に判断したことにはなりません。

実際の生成応答を検査する段階では、入力と出力に加えて実行日時、モデルの識別情報、生成条件、ケースの版も記録すると比較の前提を追いやすくなります。実ログを使うなら、公開してよい内容かどうかを別に確認します。この教材には実在ユーザーの入力や非公開データは含めていません。

まずYAMLをチームで読めるケース集にし、機械で確認できる単純な条件と、人が意味を判断する観点を分ける。この形なら、失敗したときに「データの不備」と「返答に対する期待の不一致」を切り分けられます。

関連資料

AIキャラクターのプロフィールと会話を考える背景として、Pogeeの公開ガイドも参照できます。本記事のフィールドや検査コードは、そのガイドから内部構成を推定したものではありません。

Pogee公開ガイド:チャットの外にも日常がある、AIキャラクターとは?

yaml:Node.js向けYAMLパーサーの資料

Ajv:JSON Schemaバリデーターの資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?