TT Lab
はじめる
学ぶ 学習パス コース

AIエージェント — モデルではなくグラフ

エージェントはモデルではなく状態機械だ

TT Labで続きを見る

一言でいうと

エージェントはモデルではなく状態機械です。難しいのは、モデルを呼び出す場所ではなく、いつ止まるか、何を持ち歩くか、失敗したらどこへ行くかを決める場所です。

なぜ必要なのか: デモと製品が分かれる場所

エージェントのデモは、たいていうまく動きます。質問を1つ入れると、ツールを呼び出して答えが出ます。ところが、製品になった途端、3つのことが一度に爆発します。

3つともモデルの問題ではありません。グラフの問題です。そのため、このコースはモデルを呼び出さず、グラフだけを扱います。モデルをつなぐのは、ノード1つを差し替える作業で、その場所は難しくありません。

状態は上書きされるのか、積み上がるのか

LangGraphでは、ノードは変更する部分だけを入れたディクショナリを返します。それをどうまとめるかは、状態の型が決めます。

class State(TypedDict):
    question: str
    steps: Annotated[list, operator.add]   # 쌓인다
    tries: int                              # 덮어쓴다

Annotated[list, operator.add]がないと、最後のノードが返した値だけが残ります。すると「たどってきた道」が消えて、あとで何があったのかを尋ねられません。何を積み上げ、何を上書きするかは、設計上の決定です。

グラフに移すと何がテスト可能になるのか

エージェントを1つのループのかたまりとして書くと、何が間違っていたのかを尋ねる場所がありません。グラフに分ける本当の理由は、各ノードを別々にテストできるようになることです。

ノードは純粋関数に近く作ります。状態を受け取って、状態の一部を返す形なら、そのノードだけを切り離して、入力を与えて結果を見られます。モデルの呼び出しはノードの中に置きますが、注入できるようにして、テストでは決まった答えを返すモックに差し替えます。

def plan(state: State, llm=None) -> dict:
    llm = llm or default_llm
    out = llm.invoke(state["messages"])
    return {"plan": parse_plan(out), "step": state["step"] + 1}

分岐はノードではなく条件関数に置きます。次にどこへ行くかを決める関数を別に置けば、その関数だけで「こういう状態ではツールを呼び出し、こういう状態では終わる」を表にしてテストできます。モデルを呼び出さなくても、流れ全体を確認できるようになります。

状態は、何が積み上がり、何が上書きされるかを明示します。会話の履歴は積み上がり、現在の計画は上書きされます。これを型に書いておけば、ノードを増やすときに混乱しません。積み上げるべきものを上書きすると文脈が消え、上書きすべきものを積み上げると、プロンプトが膨れ上がります。

途中の状態を保存すれば、再開と人の介入が一緒についてきます。ノードごとに状態を保存しておけば、途中で止まって人に尋ね、その答えで続きから進められます。承認が必要な作業(メール送信、決済、削除)は、この場所で区切ります。

観測はノード単位で残します。どのノードで何秒かかり、トークンをどれだけ使ったかが残らないと、遅い場所と高い場所がわかりません。全体の所要時間だけを測ると、改善する場所を見つけられません。

現場では

面接で「エージェントを作ったことはありますか」と尋ねるとき、実際に聞きたいのは、フレームワークの名前ではなく、この3つです。終わる条件を何にしたか、ツールが失敗したらどこへ送るか、何を残しておいたか。

そして、これはエージェントだけの話ではありません。リトライの上限、失敗経路、観測の記録は、バックエンドでいつも行ってきた判断です。LangGraphは、その判断をグラフで書かせてくれるだけです。