実は、ノートブックに資料を足すほど、回答の根拠は追いにくくなる。Gemini Notebookのように、資料を集めた場でコードまで動かせるようになると、なおさらだ。URLだけを残して「この資料を見た」で済ませる運用は、長く動くほど危ない。
Googleは7月16日、NotebookLMをGemini Notebookへ改称し、ノートブックごとのクラウド実行環境を導入すると発表した。自分はこのニュースを見て、モデルより先に資料の版を固定したくなった。資料が更新されたのに前回と同じ根拠として扱うと、回答だけを再評価しても原因まで辿れないからだ。
そこで、ノートブックへ入れる前の資料をディレクトリに置き、ファイルのSHA-256をJSONへ記録する小さな検査を書いた。外部ライブラリは使わない。
Gemini Notebookへ渡す前に資料の版を記録する
source_manifest.py として保存する。snapshot は資料一式を記録し、diff は2つの記録を比べる。更新日時ではなく内容のハッシュを見るので、コピーや同期で時刻が変わっても判定がぶれない。
import argparse
import hashlib
import json
from pathlib import Path
def snapshot(root: Path) -> dict:
files = {}
for path in sorted(p for p in root.rglob("*") if p.is_file()):
relative = path.relative_to(root).as_posix()
files[relative] = {
"bytes": path.stat().st_size,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
}
return {"root": str(root.resolve()), "files": files}
def changes(before: dict, after: dict) -> dict:
old_files = before["files"]
new_files = after["files"]
old_names, new_names = set(old_files), set(new_files)
return {
"added": sorted(new_names - old_names),
"removed": sorted(old_names - new_names),
"changed": sorted(
name for name in old_names & new_names
if old_files[name]["sha256"] != new_files[name]["sha256"]
),
}
def main(argv=None) -> int:
parser = argparse.ArgumentParser()
commands = parser.add_subparsers(dest="command", required=True)
take = commands.add_parser("snapshot")
take.add_argument("root", type=Path)
take.add_argument("output", type=Path)
compare = commands.add_parser("diff")
compare.add_argument("before", type=Path)
compare.add_argument("after", type=Path)
args = parser.parse_args(argv)
if args.command == "snapshot":
data = snapshot(args.root)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(
json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(f"saved: {args.output} ({len(data['files'])} files)")
return 0
before = json.loads(args.before.read_text(encoding="utf-8"))
after = json.loads(args.after.read_text(encoding="utf-8"))
print(json.dumps(changes(before, after), ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
資料を取り込む直前と、回答を再実行する直前にスナップショットを取る。
python3 source_manifest.py snapshot sources manifests/before.json
# sources/ の資料を更新した後
python3 source_manifest.py snapshot sources manifests/after.json
python3 source_manifest.py diff manifests/before.json manifests/after.json
before.json と after.json は sources/ の外に置く。資料と同じ場所へ置くと、次のスナップショットで記録ファイル自身まで資料として数える。こういう自己参照は、差分が毎回出る原因になる。コマンド側で親ディレクトリを作るようにしてあるので、最初に manifests/ を用意する必要はない。
さっき、2ファイルの資料で確認した。一方を書き換え、一方を消し、1ファイルを追加したときの出力はこれだった。
{
"added": ["limits.md"],
"removed": ["policy.txt"],
"changed": ["price.md"]
}
changed だけでなく removed を出すのがポイントになる。前回の回答が消えた資料を引用しているなら、追加資料より先に見直す必要がある。ファイル名を変えただけでも removed と added に分かれるので、意図した移動かどうかもここで拾える。
ハッシュだけでは回答の正しさを保証しない
この検査が分かるのは、入力が前回と同じかどうかまでだ。PDFの表現が変わったが意味が同じか、逆に数値の一行だけが変わって回答に効くかは、人か別の評価処理が見る。ここを混ぜると、差分検出が急に曖昧になる。
URLをそのままノートブックへ渡す場合も、取得した本文やPDFを一度この sources/ に保存してから記録する。URLは場所であって、内容の識別子ではない。SaaSの料金表や社内規程のように静かに更新される資料ほど、この区別が効く。
もう一つだけ運用を決めておく。資料の取得時点で、取得元URLと取得日時を別のメタデータに残す。ハッシュだけでは、同じ本文をどこから取ったかは分からない。出所の追跡と内容の追跡は役割が違うので、JSONを一枚に詰め込まず分けた方が後で扱いやすかった。
差分が出た資料だけを、回答前のレビュー対象に寄せます。
以前書いた出典の更新台帳は、変更を誰が確認したか残すための運用の話だった。今回のスクリプトは、その台帳に載せるべき変更を機械的に絞る入口になる。
おわりに
Gemini Notebookのような実行環境が資料のそばに来ると、試作はかなり速くなる。その一方で、速くなった回答を後から説明するには、入力の版を残す必要がある。最初から全文を比較する仕組みまで作らなくていい。まずハッシュで変更箇所を出し、変わった資料だけを再確認する。この順番なら、ノートブックを増やしても根拠の確認場所は増えにくい。