Agents SDKを使った判例検索コード( database_query.py )解析の8回目。今回は「エージェント定義」の働きを見ていく。
FORCE_REFERENCE_RULES = """【参照指示の強制ルール】
「概要」「その事件」「その判決」等が含まれる場合は、
resolve_case_doc_id → get_case_text を必ず実行すること。
"""
エージェントに守らせたい追加ルールを文字列で定義している。具体的には「概要」「その事件」「その判決」など曖昧参照が来たら、必ず resolve_case_doc_id → get_case_text を実行せよ、という運用ルールを定めている。単にツールを用意するだけでなく、システムプロンプトとしてこのルールを組み込むことで、ハルシネーションを減らすことができる。
このルールの狙い
ユーザーが「その事件の概要を教えて」と言ったとき、AIは「どの事件か」と「概要の中身」の両方を知る必要がある。
- 「その事件」「その判決」: これらは会話の文脈に依存する言葉。これを解決するために、まず
resolve_case_doc_idを使って、過去のやり取りから正しいdoc_idを特定させる。 - 「概要」: 判例の概要をまとめるには、主文や理由の全文を確認しなければならない。そのため、特定した
doc_idを使ってget_case_textを実行し、生のデータを読み込ませる。
なぜ2段階の実行を強制させるのか
AIは時として、検索結果のリスト(タイトルや日付のみ)だけを見て、「おそらくこういう事件だろう」と推測で答えてしまうことがある。そこで、次の2段階の処理を強制して、中身を読み込む(get_case_text)までは、概要を答えてはならないという拘束をAIに与え、回答の信頼性を担保する。
- ステップ1
:resolve_case_doc_id(特定):メタデータ(裁判所、日付、事件番号など)を照合し、対象を1件に絞り込む。 - ステップ2
:get_case_text(取得):絞り込んだIDを用いて、実際の判決本文や理由を取得する。
case_agent = Agent(
name="CaseDBHelper",
instructions=(
"あなたは判例DB検索アシスタント。\n"
"必ずツールで確認できた事実のみを用いて回答せよ。\n\n"
"検索依頼には search_cases、\n"
"参照指示には resolve_case_doc_id と get_case_text を用いる。\n\n"
+ FORCE_REFERENCE_RULES
),
tools=[search_cases, resolve_case_doc_id, get_case_text],
)
これまで個別に作成してきた「検索」「特定」「取得」という3つのツールを、1人のAIエージェント(CaseDBHelper)として統合するための定義。OpenAI Agents SDKにおいて、ツール(関数)はあくまで部品であり、この Agent クラスの定義により、それらをいつ、どう使うべきかの知性と役割が与えられる。
エージェントのアイデンティティ付与
name="CaseDBHelper":エージェントの名前。ログを確認する際や、複数のエージェントが連携(Handoff)する際に、「誰が発言しているか」を識別するための識別子となる。instructions(システムプロンプト):エージェントの性格と行動指針を決定する重要部分。- 事実に基づく回答の徹底:「必ずツールで確認できた事実のみを用いて回答せよ」と命じることで、AIが自分の知識で勝手に法律解釈を捏造(ハルシネーション)するのを抑制する。
- ツールの使い分け:どのリクエストに対して、どのツールを使うべきかを明文化し、AIが迷わないように導いている。
- ルールの注入:先ほど定義した
FORCE_REFERENCE_RULESを末尾に加えることで、「曖昧な指示のときは必ず特定から取得までのステップを踏む」というルールをAIに刻み込む。
tools:エージェントが利用できる道具のリスト。ここに登録された関数(search_cases、resolve_case_doc_id、get_case_text)だけが、AIによって実行される。
「部品」から「エージェント」へ
これまで解説してきた個別の関数は、単体ではただのプログラムに過ぎない。しかし、この Agent 定義により、AIは以下のような自律的な思考ができるようになる。
- 状況判断:ユーザーの質問が「新しい判例を探して」なのか「さっきの事件を詳しく教えて」なのかを分析する。
- ツールの選択:分析結果に基づき、
search_casesでリストを作るべきか、resolve_case_doc_idでIDを特定すべきかを自分で決める。 - 結果の解釈: ツールから返ってきた「候補が見つかりませんでした」や「整形されたテキスト」を読み取り、人間が理解できる自然な日本語に再構築して回答する。
HistoryItem = Tuple[str, str]
会話履歴1件を (ユーザー発話, アシスタント発話) の2要素タプルとして扱う型エイリアス(読みやすさのため)。AIが「過去にどんな会話をしたか」という記憶を記録するための、データの形を定義したもの。
HistoryItem:このデータの形式に付けた名前(型エイリアス)。Tuple[str, str]:「2つの文字列がセットになった固定の箱」という意味。- 1つ目の
str:ユーザー(人間)の発言 - 2つ目の
str:アシスタント(AI)の回答
- 1つ目の
つまり、HistoryItem は、ユーザーの問いとAIの答えの1セット(1往復)を表している。
なぜこの型定義が必要になるか
AIエージェントが文脈(コンテキスト)を理解するためには、過去の会話をリスト形式(List[HistoryItem])で保持し続ける必要がある。
- 指示の具体化:
FORCE_REFERENCE_RULES(「その事件」という言葉があれば特定ツールを使え、という命令)を実現するために不可欠。AIは履歴を遡り、「『その事件』とは、3つ前の発言で出てきた東京地裁の件だな」と特定できるようになる。 - 型安全性の確保:プログラムの中で「履歴データは必ず(人間, AI)のペアである」と厳格に決めておくことで、データの入れ間違いによるエラーを防ぐ。
def _build_input(question: str, history: List[HistoryItem], max_turns: int = 8) -> str:
lines: List[str] = []
for u, a in history[-max_turns:]:
lines.append(f"ユーザー: {u}")
lines.append(f"アシスタント: {a}")
lines.append(f"ユーザー: {question}")
return "\n".join(lines)
AIエージェントに、これまでの会話の流れを教え込み、最新の質問に答えさせるための下準備を行う処理。AIは1回1回のやり取りで、記憶をリセットしてしまう性質があるため、人間のように自然に対話を続けるには、この関数のように過去の履歴を毎回「物語」として作り直して渡す必要がある。
コードの仕組み
def _build_input(question: str, history: List[HistoryItem], max_turns: int = 8) -> str:
エージェントに渡す入力テキスト(プロンプト)を、履歴込みで組み立てる関数。直近max_turns件だけ使う。lines: List[str] = []
プロンプトを行単位で組み立てるための配列を用意。for u, a in history[-max_turns:]:
履歴の末尾(最新側)から最大max_turns件を取り出して回す。lines.append(f"ユーザー: {u}")
履歴中のユーザー発話を「ユーザー: …」形式で1行追加。lines.append(f"アシスタント: {a}")
履歴中のアシスタント発話を「アシスタント: …」形式で1行追加。lines.append(f"ユーザー: {question}")
最後に今回の質問(最新のユーザー入力)を同じ形式で追加。return "\n".join(lines)
行配列を改行でつないで、最終的な入力文字列として返す。
※ここで作った文字列が、Runner.run_syncにそのまま渡される。
エージェント設計における記憶の重要性
この関数によって、AIは以下のような高度な振る舞いが可能になる。
- 指示の継続性:「東京地裁の判例を探して」→「じゃあ、その内容を要約して」という会話において、「その内容」が「さっき見つけた東京地裁の判例」であることを、履歴(History)を読み直すことで理解できる。
- 文脈に応じた回答:
FORCE_REFERENCE_RULES(参照指示の強制ルール)が効力を発揮するのも、この_build_inputが過去の発言をAIの目の前に差し出しているおかげ。
def run_agent(question: str, history: Optional[List[HistoryItem]] = None) -> str:
prompt = _build_input(question, history or [])
result = Runner.run_sync(case_agent, prompt)
return result.final_output
これまで準備してきたエージェント、ツール、記憶(履歴)のすべてを連結し、回答を生成する関数(run_agent)を定義している。プログラムの他の部分からこの関数を呼び出すだけで、複雑なAIの思考プロセスが起動する。
def run_agent(question: str, history: Optional[List[HistoryItem]] = None) -> str:
外から呼び出す実行用の関数。質問と(あれば)履歴を受け取って、エージェントを動かして結果を返す。prompt = _build_input(question, history or [])
履歴がNoneなら空配列にして、さっきの_build_inputでプロンプト文字列を作る。result = Runner.run_sync(case_agent, prompt)case_agentを作ったプロンプトで同期実行する。実行中に必要ならエージェントがtools=[...]からツールを選んで呼ぶ。return result.final_output
実行結果オブジェクトから、最終的にユーザーへ返す文章(最終回答)だけ取り出して返す。
Runner がもたらす自律性
このコードは、Runner.run_sync を呼び出す際、AIに「どうやって検索しろ」とは指示していない。AIはプロンプトから、「これは判例を特定(Resolve)してから本文を取得(Get)すべきだ」と自分で判断し、resolve_case_doc_id を呼び出し、get_case_text を実行する。そして、そこから得られた情報を統合し、適切なトーンで回答を作成する。このような自律的なループを1行で実行できるのが、Agents SDKの最大の強みである。
以上で、database_query.py の解析はすべて終了した。