0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LangGraphで「経営サポートAI」の返答ブレを減らす - State / Node / Conditional Edgeを仕事に応用する

0
Posted at

はじめに

前回の記事では、LangGraphの StateConditional Edge をブラウザで見える化し、AIエージェントの処理の流れを理解しやすくする方法を書きました。

前回記事:

今回はその続編として、同じ考え方を 経営サポートAI に応用しました。

目的は、AIに経営や仕事の相談をするときにありがちな、

  • PCやセッションによって答えが変わる
  • どのファイルを根拠に答えたのかわからない
  • 「本日やること」のような定型質問でも回答がブレる
  • AIが雰囲気で判断しているように見える

という問題を減らすことです。

結論から言うと、経営サポートAIも State / Node / Conditional Edge に分けることで、かなり扱いやすくなります。

作ったもの

今回作ったのは、ai-board という経営サポートAIのための小さなCLIです。

.venv/bin/python board_runner.py コマンドを教えて
.venv/bin/python board_runner.py 本日やること
.venv/bin/python board_runner.py 朝会お願いします。
.venv/bin/python board_runner.py 再起動する

たとえば、

.venv/bin/python board_runner.py 本日やること

と実行すると、daily-log.md の当日エントリーを読み、Today's Top3 を抜き出して返します。

つまり、AIが毎回なんとなく「今日やること」を考えるのではなく、

本日やること
  -> daily-log.mdを読む
  -> 当日エントリーを探す
  -> Today's Top3を抽出する
  -> それを正として返す

というルートを固定しました。

なぜ経営AIにGraphが必要なのか

経営や仕事の相談では、AIの自由な発想は役に立ちます。

一方で、毎日使う経営サポートAIでは、自由すぎることが問題になります。

たとえば「本日やること」と聞いたときに、あるPCでは、

1. 新規提案向け4コースの目次を作る
2. 共通40分コンテンツの生成指示を作る
3. Zoomで見せる提案メモを作る

と答え、別のPCでは、

1. 資料作成を完了する
2. 定例グルコン後処理を完了する
3. 新規提案準備を進める

と答えることがあります。

どちらも文脈としては間違っていなくても、仕事で使うには困ります。

経営サポートAIでは、特に次のような処理は決定論的にしたいです。

  • どのファイルを正とするか
  • どの順番で読むか
  • どの条件なら質問するか
  • どの条件なら実行するか
  • どの条件なら保留するか
  • どの結果をログに残すか

ここにGraph構造が効きます。

State / Node / Conditional Edgeへの分解

今回の実装では、処理中の状態を BoardState として定義しました。

class BoardState(TypedDict, total=False):
    user_input: str
    today: str
    repo_root: str
    command_type: CommandType
    required_files: list[str]
    files: dict[str, str]
    current_top3: list[str]
    missing_info: list[str]
    risk_flags: list[str]
    recent_writes: list[str]
    next_action: str
    response: str
    trace: list[str]

State には、ユーザー入力、今日の日付、コマンド種別、読み込むファイル、抽出したTop3、最終回答などを入れています。

次に、処理を Node に分けます。

classify_command
determine_required_files
load_required_files
extract_today_top3
answer_today_tasks
show_commands
run_morning_meeting
restart_board

それぞれのNodeは、Stateを受け取り、Stateを更新して返します。

たとえば classify_command は、入力を見てコマンド種別を決めます。

def classify_command(state: BoardState) -> BoardState:
    text = normalize_text(state["user_input"])

    if "コマンドを教えて" in text or "コマンド教えて" in text:
        state["command_type"] = "commands"
        state["next_action"] = "show_commands"
    elif "本日やること" in text or "今日やること" in text:
        state["command_type"] = "today_tasks"
        state["next_action"] = "load_required_files"
    elif "朝会お願いします" in text:
        state["command_type"] = "morning_meeting"
        state["next_action"] = "load_required_files"
    else:
        state["command_type"] = "unknown"
        state["next_action"] = "unknown"

    return state

ここでは、AIに「これは何の依頼ですか?」と毎回考えさせず、文字列ベースでルートを固定しています。

Conditional Edgeで処理を固定する

分類後は、Stateを見て次のNodeへ分岐します。

def route_after_classification(state: BoardState) -> str:
    next_action = state["next_action"]
    if next_action == "show_commands":
        return "show_commands"
    if next_action == "load_required_files":
        return "determine_required_files"
    return "answer_unknown"

さらに、コマンド種別ごとに次の処理を分けます。

def route_by_command(state: BoardState) -> str:
    command = state["command_type"]
    if command == "today_tasks":
        return "extract_today_top3"
    if command == "morning_meeting":
        return "run_morning_meeting"
    if command == "recent_writes":
        return "inspect_recent_writes"
    if command == "restart":
        return "restart_board"
    return "answer_unknown"

これにより、

コマンドを教えて
  -> classify_command
  -> show_commands
本日やること
  -> classify_command
  -> determine_required_files
  -> load_required_files
  -> extract_today_top3
  -> answer_today_tasks

という処理ルートになります。

LangGraphでつなぐ

LangGraphが入っている環境では、以下のように StateGraph として組み立てます。

def build_langgraph_app() -> Any:
    from langgraph.graph import END, StateGraph

    workflow = StateGraph(BoardState)
    workflow.add_node("classify_command", classify_command)
    workflow.add_node("determine_required_files", determine_required_files)
    workflow.add_node("load_required_files", load_required_files)
    workflow.add_node("extract_today_top3", extract_today_top3)
    workflow.add_node("show_commands", show_commands)
    workflow.add_node("answer_today_tasks", answer_today_tasks)
    workflow.add_node("run_morning_meeting", run_morning_meeting)
    workflow.add_node("restart_board", restart_board)

    workflow.set_entry_point("classify_command")
    workflow.add_conditional_edges(
        "classify_command",
        route_after_classification,
        {
            "show_commands": "show_commands",
            "determine_required_files": "determine_required_files",
            "answer_unknown": "answer_unknown",
        },
    )
    workflow.add_edge("determine_required_files", "load_required_files")

    return workflow.compile()

ポイントは、LLMに全部任せるのではなく、

  • 入力分類
  • ファイル読込
  • Top3抽出
  • コマンド一覧表示
  • 再起動処理

を明示的なNodeとして分けたことです。

LangGraph未導入環境でも動くようにした

実務で使う場合、別PCで依存関係が入っていないこともあります。

そこで、LangGraphが未導入でも同じロジックで動くフォールバックを入れました。

def run_board(user_input: str, repo_root: Path, date_text: str | None = None) -> BoardState:
    initial_state: BoardState = {
        "user_input": user_input,
        "today": date_text or today_jst(),
        "repo_root": str(repo_root),
        "missing_info": [],
        "risk_flags": [],
        "trace": [],
    }

    try:
        app = build_langgraph_app()
    except ImportError:
        return run_without_langgraph(initial_state)

    return app.invoke(initial_state)

これにより、LangGraphがある環境では StateGraph を使い、ない環境では同じ分岐関数を順番に呼ぶ形で動きます。

実行結果

たとえば、

.venv/bin/python board_runner.py コマンドを教えて --trace

を実行すると、コマンド一覧と処理ルートが返ります。

--- trace ---
classify_command -> show_commands

また、

.venv/bin/python board_runner.py 本日やること --trace

では、次のようなルートになります。

--- trace ---
classify_command -> determine_required_files -> load_required_files -> extract_today_top3 -> answer_today_tasks

この trace があるだけで、AIがどの判断経路を通ったのかがかなり見えやすくなります。

テストも書いた

定型コマンドは、ブレると困ります。

そこで最低限のテストも書きました。

def test_today_tasks_come_from_daily_log():
    state = board_runner.run_board(
        "本日やること",
        Path(__file__).resolve().parents[1],
        "2026-07-19",
    )

    assert "資料作成を完了する" in state["response"]
    assert "日曜午前中の定例グルコンの後処理を完了する" in state["response"]
    assert "新規提案準備を進める" in state["response"]

実行結果:

5 passed

何が便利になったか

今回の実装で便利になったことは、主に3つです。

1. 返答のブレが減る

「本日やること」と聞かれたら、必ず daily-log.md の当日 Today's Top3 を見に行きます。

これにより、AIが古い文脈や別ファイルの未完了タスクを拾って、違うTop3を返す問題を減らせます。

2. 処理ルートが追える

--trace を付けると、

classify_command -> determine_required_files -> load_required_files -> extract_today_top3 -> answer_today_tasks

のように、どのNodeを通ったか確認できます。

これは経営AIにとって重要です。

経営判断では、結論だけでなく、

  • 何を根拠にしたか
  • どこで分岐したか
  • どのファイルを読んだか
  • どこで人間確認が必要になったか

が見える必要があるからです。

3. Codex内からも使える

このCLIをCodex内から呼ぶ運用にしたので、ユーザーは普通に、

コマンドを教えて

と入力するだけでよくなりました。

内部では、

.venv/bin/python board_runner.py コマンドを教えて

を実行し、その結果を返します。

経営AIとしての意味

今回作ったものは、まだ小さなCLIです。

ただし、考え方としてはかなり重要です。

経営サポートAIでは、AIに自由に考えさせる部分と、ルールで固定する部分を分ける必要があります。

AIに任せる部分:
  - 要約
  - 提案
  - 文章化
  - 選択肢の整理

プログラムで固定する部分:
  - どのファイルを読むか
  - どの情報を正とするか
  - どの条件で分岐するか
  - どのタイミングで人間確認に戻すか
  - どのログに記録するか

この分離ができると、AIは「気分で答える相談相手」から、「決められた経営フローに沿って判断を支援するシステム」に近づきます。

今後やりたいこと

今後は、次のように発展させたいです。

  • ブラウザで現在のNodeを見える化する
  • Stateの中身を画面に表示する
  • daily-log.md への追記もNode化する
  • CEO / CMO / CFO / CTO / CPO / CSOをそれぞれNode化する
  • 売上、顧客対応、提案期限、リスクによってConditional Edgeを増やす
  • 人間確認が必要な判断を自動で止める

最終的には、

朝会開始
  -> 状況確認
  -> 売上確認
  -> 顧客対応確認
  -> リスク判定
  -> CEO判断
  -> Top3決定
  -> daily-log.mdへ記録

のような経営フローを、Graphとして管理したいと考えています。

まとめ

前回は、LangGraphの StateConditional Edge を理解するために、ブラウザで処理の流れを見える化しました。

今回は、その考え方を実際の仕事に応用し、経営サポートAIの定型コマンドをGraph構造で処理するようにしました。

ポイントは次の3つです。

  • AIの返答を安定させるには、Stateを明示する
  • 業務処理はNodeに分ける
  • 重要な分岐はConditional Edgeで固定する

経営AIを作るうえで大事なのは、AIに全部任せることではありません。

AIが得意なところはAIに任せ、ブレてはいけないところはGraphで制御する。

この考え方が、実用的なAIエージェントを作るうえでかなり重要だと感じました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?