引数をでっち上げた一度の呼び出し
目標
ツールを呼び出す側の規律を、コードで書きます。契約表を1か所に置き、選んだ引数を呼び出す前に測り、返ってきた結果を信じずに測り、失敗を例外ではなく値として扱って理由を状態に残し、その理由で次の試行を直し、同じ引数で2回呼び出さないように覚えます。
なぜ重要なのか
ツールを付けたエージェントが最初に壊れる場所は、モデルではなく、呼び出しの境界です。モデルが引数名を1つでっち上げると、TypeErrorがグラフの外に飛び出して実行全体が終わり、状態もたどってきた道も残りません。もっと悪いのは、例外が出ない場合です。ツールが空のリストを返したのに、その上に「在庫3か所」という文が作られます。
そのため、このラボは、契約を値として書くことから始めます。名前・引数スキーマ・返す形・実際の関数を表1つに結びつけておけば、測るコードがツールごとに必要なくなり、呼び出す入口が1か所に絞られます。その入口の前で引数を測り、入口の後ろで結果を測り、入口の前にメモ化を置きます。
引数を選ぶ場所は、このラボではルールで代用します。実際のエージェントは、その場所(pickノード)で、モデルにツール名と引数を尋ねます。残りの構造は同じです。むしろ、モデルが選ぶほど、呼び出す前の検証が必要になります。
採点ツールは、書かれた説明を信用しません。書かれたモジュールを実際に読み込んで、検証関数を毎回異なる値で叩いてみて、ツールの本体が何回動いたかを数えて、「呼び出す前に防いだか」を確認します。sku・地域・個数は、実行のたびに変わります。
ステップ
- /root/work/agtool/tools.pyにデータ(
REGIONS・STOCK)とMAX_ROWS・CALLS、2つのツール(stock_lookup・legacy_stock)、契約表TOOLSを作成してください。 validate_args(name, args)を作成して、呼び出す前に引数を測るようにしてください。通れば空の文字列、そうでなければ理由を1つ返します。call_tool(name, args)を作成して、検証を通過した呼び出しだけを実際に呼び出すようにしてください。例外は外に出さず、{"ok", "value", "error"}として返します。validate_result(name, value)を作成して、call_toolが結果も測るようにしてください。理由はshape・empty・too_big・rangeです。CACHEとcache_key(name, args)を作成して、同じツールを同じ引数で2回呼び出さないようにしてください。成功だけを覚えます。Stateと4つのノード(pick・fetch・reply・giveup)、build_naive()・run_naive(order)を作成して、失敗を例外ではなく経路として扱ってください。MAX_ATTEMPTS・REPAIRS・can_repair・repairノードとbuild_graph()・handle(order)を追加して、理由を見て直し、もう一度試すようにしてください。- 確認したことを記録してください(保存先: /root/work/agtool/tool_report.json、/root/work/agtool/tool_report.md)。
参考
- 実行の契約: 採点ツールは、
/root/work/agtool/tools.pyをPythonモジュールとして読み込んで、REGIONS・STOCK・MAX_ROWS・CALLS・TOOLS・stock_lookup・legacy_stock・validate_args・validate_result・call_tool・CACHE・cache_key・run_naive・MAX_ATTEMPTS・REPAIRS・can_repair・handleを直接使います。スクリプトとして実行しないので、if __name__ == "__main__"はなくてもかまいません。 - このラボのデータは、次のとおりです。
REGIONS = {"seoul": ["gasan", "guro", "mapo"], "busan": ["sasang", "haeundae"], "jeju": ["hallim"]}、STOCK = {"A-1001": {"gasan": 4, "guro": 2, "mapo": 7, "sasang": 1}, "A-1002": {"guro": 5, "haeundae": 3}, "B-2001": {"gasan": 9, "guro": 1, "mapo": 9, "hallim": 2}, "B-2002": {"sasang": 6, "haeundae": 6}}、MAX_ROWS = 2。 - 2つのツールのシグネチャは
(sku, region, limit)で同じで、返すものも{"sku": 문자열, "rows": [{"warehouse": 문자열, "count": 정수}, ...]}(プレースホルダーは文字列と整数です)で同じです。rowsは個数の降順で、同じなら倉庫名の昇順です。stock_lookupはlimit個までしか返さず、legacy_stockはlimitを無視して、その地域の倉庫をすべて返します(旧バージョンの真似)。 CALLSは、ツールの本体が動いた回数を、名前ごとに数えるディクショナリです。2つのツールの最初の行で、1ずつ増やしてください。採点ツールが、この値で「呼び出す前に防いだか」を確認します。TOOLSの1項目の形:{"fn": 함수, "args": {인자이름: {"type": "str"|"int", "required": True, "allowed": [...], "min": N, "max": N}}, "returns": {"sku": {"type": "str"}, "rows": {"type": "list", "max_len": MAX_ROWS, "item": {"warehouse": {"type": "str"}, "count": {"type": "int", "min": 0}}}}}(プレースホルダーは関数と引数名です)。regionのallowedはREGIONSのキーで、limitはminが1、maxがMAX_ROWSです。validate_argsの理由:unknown_tool・missing:<이름>・extra:<이름>・type:<이름>・allowed:<이름>・range:<이름>(プレースホルダーは引数名です)。通れば空の文字列です。採点ツールは、欠陥が1つだけの引数だけを投げます。validate_resultの理由:shape(形・型が違う)・empty(rowsが空)・too_big(rowsがmax_lenより長い)・range(countが契約の範囲外)。通れば空の文字列です。call_toolが返すerrorは、前に出どころを付けます。引数側はargs:<사유>、結果側はresult:<사유>、ツールが例外を投げたならraised:<예외이름>(プレースホルダーは理由と例外名です)。成功なら空の文字列です。handle(order)のorderは{"sku": ..., "region": ..., "limit": ..., "prefer": ...}です。preferがあればそのツールを、なければstock_lookupを使います。答えは{"ok", "answer", "rows", "errors", "fixes", "attempts", "calls", "tool_runs"}で、tool_runsは、この1件を処理する間にツールの本体が動いた回数です。REPAIRSは{"args:allowed:region": "region", "args:range:limit": "limit", "result:too_big": "tool"}です。地域はFALLBACK_REGION("seoul")に、個数はMAX_ROWSに、ツールはstock_lookupに直します。MAX_ATTEMPTSは3です。- ノード名と状態のキーは、同じ名前空間です。状態に
toolキーを置いてノードもtoolと呼ぶと、ValueError: 'tool' is already being used as a state keyでコンパイルが死にます。 - このPodにはインターネットがありません。
pip installはできません。langgraph 0.2.60がすでに入っています(python3 -c "import langgraph")。 - 公式ドキュメント: Workflows and agents・Use the graph API・Graph API overview
- よくある間違い: 呼び出してから引数を測ること(本体がすでに動いています)、
isinstance(True, int)が真なのでTrueを個数として受け取ること、空の結果を成功として通すこと、失敗まで覚えて直してももう一度呼び出せなくなること、試行の上限なしで直して、また直すことです。
契約を値として1か所に書く
/root/work/agtool/tools.pyにREGIONS・STOCK・MAX_ROWS・CALLSと2つのツール(stock_lookup・legacy_stock)、契約表TOOLSを作成してください。TOOLSの1項目はfn・args・returnsの3つを持ち、2つのツールは最初の行でCALLSを1増やします。
argsは、引数名ごとにtype・requiredと、必要ならallowed・min・maxを書いたディクショナリです。returnsには、返すキーの型と、rowsのmax_len・itemを書きます。stock_lookupはlimit個までだけ、legacy_stockは、limitを無視してすべて返します。結果の検証が必要な理由を、あとでこのツールで見ます。参考の節のデータ表をそのまま使ってください。
呼び出す前に引数を測る
validate_args(name, args)を作成してください。契約表を読んで、足りない引数・契約にない引数・型・許可リスト・範囲を測り、通れば空の文字列を、そうでなければ理由を1つ(missing:limitのように)返します。
理由の名前は、参考の節にあるとおりに書いてください。Pythonではboolがintのサブタイプなので、isinstance(True, int)が真です。個数の場所にTrueが入ったら、type:で防ぐ必要があります。知らないツール名ならunknown_toolです。欠陥が複数あるときに何を先に返すかは自由ですが、採点ツールは、欠陥が1つだけの引数だけを投げます。
間違った引数では呼び出さない
call_tool(name, args)を作成してください。検証を通過した呼び出しだけを実際に呼び出し、例外は外に出しません。答えはいつも{"ok": 참거짓, "value": 결과 또는 None, "error": 사유}(プレースホルダーは真偽値、結果またはNone、理由です)で、引数側の理由にはargs:を、ツールが投げた例外にはraised:を、前に付けます。
実際の関数は、TOOLS[name]["fn"]から取り出してfn(**args)で呼び出します。呼び出す入口を1か所に絞ることが要点です。検証で引っかかったら、そこで終わらせてください。採点ツールは、間違った引数を渡したあと、CALLSがそのままかを見ます。呼び出してから例外を捕まえる方式は、この検査を通過できません。
返ってきたものを信じない
validate_result(name, value)を作成して、call_toolが結果も測るようにしてください。理由はshape・empty・too_big・rangeで、引っかかったら、call_toolはerrorにresult:を付けて返します。
契約のreturnsを読んで測ってください。ツールごとに手で書くと、ツールが増えたときに抜けます。rowsが空ならemptyです(エラーではないからと成功として通すと、答えをでっち上げることになります)。max_lenより長ければtoo_bigで、legacy_stockがまさにそのケースを作ります。行の中のcountが契約のminより小さければrangeです。
同じことを2回尋ねない
CACHEとcache_key(name, args)を作成して、call_toolが、同じツール・同じ引数の結果を覚えるようにしてください。引数を書いた順序が違っても同じキーが出る必要があり、成功だけを覚えます。
json.dumps(args, sort_keys=True, ensure_ascii=False)を名前とつなげると、順序にぶれないキーになります。メモ化は、検証を通過したあと、呼び出す直前に探します。失敗まで覚えると、直してもう一度呼び出す道が塞がれるので、成功だけを入れてください。採点ツールは、同じ引数で2回呼び出して、CALLSが1回だけ増えるか、失敗した呼び出しは2回とも本体が動くかを見ます。
失敗を経路として扱う
Stateと4つのノード(pick・fetch・reply・giveup)、build_naive()・run_naive(order)を作成してください。fetchは、失敗しても例外を出さずに理由をerrorsに残し、失敗したらgiveupに進みます。run_naiveは{"ok", "answer", "rows", "errors", "calls"}を返します。
pickは、注文からツール名と引数を選びます。実際のエージェントは、この場所でモデルに尋ねます。errors・callsには連結するリデューサーを、attemptsには足すリデューサーを付けてください。このステップのグラフは、直しません。1回呼び出して失敗したら、そのまま断念します。replyは、rowsの最初の行で答えの文を作り、giveupは、最後の理由を答えに書きます。
理由を見て直し、もう一度試す
MAX_ATTEMPTS = 3・FALLBACK_REGION・REPAIRS・can_repair(reason)とrepairノード、build_graph()・handle(order)を追加してください。直せる理由なら、引数やツールを直してfetchに戻り、直せないか上限に達したら、giveupに進みます。
repairは、errorsの最後の理由を読んで、REPAIRSの表のとおりに直し、何を直したかをfixesに残します。理由を状態に残しておいたおかげで、この場所で使えます。例外として投げていたら、「何かが間違った」しかありません。空の結果は、もう一度尋ねても空の結果なので、直せない側です。handleのtool_runsは、呼び出しの前後のCALLSの合計を引いて求めてください。
測ったことを記録する
/root/work/agtool/tool_report.jsonにtools・blocked_before_call・bad_result_reasons・memo_second_call_runs・repaired・attempts・gave_upを、/root/work/agtool/tool_report.mdに## 도구 계약을 어디에 적었나 ## 부르기 전에 무엇을 막았나 ## 돌려준 결과를 어떻게 믿지 않았나 ## 실패를 어떤 경로로 다뤘나の4つの節を書いてください(4つの見出しは、順に韓国語で「ツールの契約をどこに書いたか」「呼び出す前に何を防いだか」「返ってきた結果をどのように信じなかったか」「失敗をどんな経路で扱ったか」を意味します)。
数値は手で書かず、自分のモジュールを実際に動かして得てください。blocked_before_callは、間違った引数で呼び出してみたあとにツールの本体が動いた回数、bad_result_reasonsは、4種類の悪い結果に対してvalidate_resultが出した理由をソートしたもの、memo_second_call_runsは、同じ引数で2回目に呼び出したときに本体が動いた回数です。repaired・attemptsは、地域が間違った注文をhandleに渡して得て、gave_upは、空の結果が出る注文のokを反転した値です。## 실패를 어떤 경로로 다뤘나(韓国語で「失敗をどんな経路で扱ったか」を意味する見出しです)の節には、試行の上限を数字で書いてください。