受託のサイト制作で見積書を出す前の晩のことです。項目は四つ、金額も決まっていて、あとは取引先への添え書きを整えるだけでした。
こういうとき、文面づくりは Claude にとても向いています。丁寧すぎず、くだけすぎない一通を、数十秒で返してくれます。
ただ、依頼文に金額まで書き込んで下書きを頼むと、返ってきた文章の中で数字が静かに動くことがあります。桁が変わるわけではありません。「合計」の隣に置かれた数字が、小計だったり、税抜だったりするのです。
文面は読み返せば直せます。数字は、読み返しただけでは気づけません。いま思えば、ここが今回の線引きの出発点だったのだと思います。
数字を渡さないという選び方
最初にお伝えしたいのは、Claude の計算力を疑っているわけではない、ということです。
足し算と消費税の計算そのものは、たいてい正しく返ってきます。問題はその手前にあります。金額が文章のあちこちに散ると、どれが正本なのか、自分でも分からなくなるのです。
私が選んだのは、役割を二つに分ける方法でした。
| 役割 | 誰が持つか | 置き場所 |
|---|---|---|
| 項目・単価・数量・税率 | 私 | 手元の JSON(正本) |
| 小計・消費税・合計の計算 | スクリプト | 下の Python |
| 添え書き・件名・お礼の言い回し | Claude | チャットの下書き |
| 最後の突き合わせ | スクリプトと私 | 送る直前 |
見積書の PDF そのものは、私は別の仕組みで JSON から書き出しています。ここで扱うのは、文面だけを Claude に頼む部分です。
正本は小さな JSON 一枚で足ります
正本といっても大げさなものではありません。項目名、単価、数量だけを並べたファイルです。
{
"items": [
{"name": "トップページ制作", "unit_price": 120000, "qty": 1},
{"name": "下層ページ", "unit_price": 30000, "qty": 3}
]
}これを quote.json として保存します。金額を直すときは、必ずこのファイルだけを直します。文面の側に数字を直接書き足さない、というのが約束です。
Claude への依頼文は「数字の穴」を残して書く
下書きを頼むときは、金額を伏せ字にして渡します。型は次のとおりです。
取引先へ送る見積書の添え書きを、です・ます調で 4 文以内で書いてください。
- 件名: 〇〇様 コーポレートサイト制作のお見積り
- 項目は 2 つ(トップページ制作/下層ページ 3 枚)
- 金額は書かず、合計の箇所は「【合計金額】」と置いてください
- 有効期限の箇所は「【期限】」と置いてください
- 締めは、質問があれば遠慮なくどうぞ、という一言にしてください返ってきた文章には「【合計金額】」「【期限】」が残ります。ここに入れる数字は、次のスクリプトが出した値だけです。
検算スクリプト: 文面に紛れた数字を拾う
下書きに数字を入れたあと、正本と突き合わせるスクリプトを走らせます。やっていることは三つです。正本から小計・消費税・合計を計算し、文面に出てくる 4 桁以上の数字を拾い、正本から作れない数字があれば止めます。それだけの小さな番人です。
import json, re, sys
from decimal import Decimal, ROUND_DOWN
def load(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
def calc(items, rate=Decimal("0.10")):
subtotal = sum(Decimal(i["unit_price"]) * Decimal(i["qty"]) for i in items)
# 消費税は税率ごとに 1 回だけ端数処理する(ここでは切り捨て)
tax = (subtotal * rate).quantize(Decimal("1"), rounding=ROUND_DOWN)
return subtotal, tax, subtotal + tax
def amounts_in(text):
found = set()
for m in re.finditer(r"[0-9][0-9,]*", text):
s = m.group().replace(",", "")
if len(s) >= 4: # 年号や電話番号の断片を避けるため 4 桁以上に限る
found.add(int(s))
return found
def main(json_path, draft_path):
doc = load(json_path)
subtotal, tax, total = calc(doc["items"])
allowed = {int(subtotal), int(tax), int(total)}
allowed |= {int(Decimal(i["unit_price"]) * Decimal(i["qty"])) for i in doc["items"]}
allowed |= {int(i["unit_price"]) for i in doc["items"]}
with open(draft_path, encoding="utf-8") as f:
draft = f.read()
stray = sorted(amounts_in(draft) - allowed)
missing = sorted({int(total)} - amounts_in(draft))
print(f"小計 {int(subtotal):,} / 消費税 {int(tax):,} / 合計 {int(total):,}")
if stray:
print("正本にない数字:", [f"{n:,}" for n in stray])
if missing:
print("文面に合計が出ていません:", [f"{n:,}" for n in missing])
sys.exit(1 if stray or missing else 0)
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])check_amounts.py として保存し、次のように使います。
python3 check_amounts.py quote.json draft.txt手元で試すと、結果は次のようになります。上の JSON なら、小計 210,000、消費税 21,000、合計 231,000 です。合計が 231,000 と書かれた文面は通り、うっかり 213,000 と書いた文面は「正本にない数字」と「文面に合計が出ていません」の二行で止まります。数字の入れ替わりのような、人間の目が滑りやすい誤りほど、機械は素直に拾ってくれます。
なぜ端数の丸めを一か所にしたのか
行ごとに消費税を計算して足し上げると、合計が一円ずれることがあります。コードの中で calc の一か所だけで丸めているのは、そのためです。
丸め方(切り捨て・四捨五入)は、取引先との取り決めや書式によって変わります。ここでは切り捨てにしていますが、ご自身の運用に合わせて rounding= の一行だけ差し替えていただければ、そのまま使えます。大事なのは「どこで丸めるか」を一か所に決めておくことで、数字を文章の側に散らさないことと同じ考え方です。
運用してみて決めた小さな決まり
個人開発の延長で受託の見積を出すようになってから、いくつか決まりを足してきました。
- 金額の直しは
quote.jsonだけにします。チャットに戻って「この金額で書き直して」とは頼みません - 文面を Claude に頼むときは、取引先の社名と担当者名も伏せ字にしておきます
- スクリプトが通ってから、最後に声に出して一度だけ読みます
三つ目は、機械ではなく私の仕事として残しています。声に出すと、数字より先に「この言い回しは相手に失礼ではないか」という引っかかりが見つかることが多いのです。数字は正本が、言葉は Claude が、最後の一読は私が。 この分担は、忙しい週ほど崩さないようにしております。
次にやること
まずは直近に出した見積書を一枚選んで、項目と金額だけを quote.json に書き写してみてください。そのうえで、次の見積では「【合計金額】」の穴を残した依頼文から始めていただければ、十分に効果を感じていただけると思います。