「あの値動きなに?」を調べるエージェントは、Swarm ではなくグラフで組む
値動きの根拠を確証度つきで返す調査エージェントを設計していて、最初は LangGraph の Swarm で考えていました。やめてグラフにした理由 —— 特に「いつ終わるか」まで LLM に任せることになる点 —— と、比べた構成を全部書きます。設計だけで、まだ動かしていません。
あの値動きなに? 昨日の急な円高、為替介入だったのか、それとも別の何かか。この手の「値動きの理由」って、翌朝には解説が5個くらい出てきて、どれも読めばそれっぽいんですよね。
この記事は、その仮説を確証度つきで返す調査エージェントの設計の話です。最初はエージェント同士がバトンを渡し合う Swarm 型で考えていたんですが、途中でやめて固定グラフにしました。その判断の過程 —— 何と何を比べて、どこで決まったか —— を丸ごと書きます。確証度の出し方(証拠のランクと時刻の前後)も後半に置きます。
先に断っておくと、まだ作っていません。 設計だけです。動かしたら別記事にします。
結論。順番と終わりはコードで固定して、非決定性は役割の中だけに置く
結論から言うと、設計の争点は「グラフか Swarm か」ではなく「LLM にどの判断を渡すか」でした。
判断を4つに割ると、線が引けます。
| 何を決めるか | 誰が決めるべきか |
|---|---|
| 役割の順番(次に誰が動くか) | コード。固定グラフで書く |
| いつ終わるか | コード。条件付きエッジと上限カウンタ |
| 役割の中の手数(何回ツールを叩くか) | LLM。 各ノードをエージェントにする |
| 確証度の判定基準 | コード。証拠ランク × 時刻の前後で機械的に |
LLM に渡すのは3行目だけ、というのが結論です。Swarm は1行目と2行目まで LLM に渡す構成で、特に2行目 —— いつ議論を終えるか —— を渡してしまうのが、僕が最後に決め手にしたところでした。
以下、そこに至るまでに考えたことを順に書きます。
Swarm をやめた理由は4つある
確証度を名乗るなら、調べた範囲が固定されていないと困る
これが決め手でした。「確証度: 中」という出力は、母数(何を調べたか)が固定されて初めて意味を持ちます。
Swarm は経路が実行ごとに変わります。ということは、調べた範囲も毎回変わる。同じ質問に対して「一次情報を当たった回」と「途中で打ち切って当たらなかった回」が、同じ顔をしたスコアを返しうるわけです。
確証度は「見つかった根拠の強さ」だけで決まるものではなくて、「探したのに見つからなかった」も同じくらい情報量があります。後者を言うには、探す範囲が固定されている必要があります。カバレッジが主張の一部になっているんですよね。ここに気づいた時点で、Swarm は落ちました。
順番は最初から分かっている
2つ目。この調査は、順番が質問によって変わりません。
Timeline(基準時刻の確定)→ 証拠収集 → Adjudicator(反証と判定)。値動きが何であれ、この順です。時刻が決まらないと「その時刻より前に出た情報」を探せないし、証拠が揃わないと判定できない。
Swarm の売りは「次に誰が要るか事前に読めない時の、動的なルーティング」です。読める仕事に使うと、利点だけ消えて、経路が揺れるコストが残ります。
読めないのは順番ではなく、役割の中の手数
3つ目。読めない部分はちゃんとあるんですが、それが1段下にありました。
一次情報の担当が2回のツール呼び出しで終わるか、12回叩くことになるかは事前に分かりません。会見の文字起こしが見つからなければ別ルートを探すし、見つかれば早く終わる。
でもこれは、そのノードを ReAct エージェントにすれば中で吸収できます。役割の順番まで LLM に渡す理由にはならない。ここを混同して「調査は動的だから Swarm」と決めかけていた、というのが今回いちばん危なかったところです(危なかった)。
終わりどきまで LLM が決めることになる
そして4つ目。これが一番効きました。Swarm は「いつ終わるか」も LLM が決めます。
ハンドオフ型は、誰もバトンを渡さなくなった時に終わります。つまり停止条件がプロンプトの中にあって、コードの中にない。ここが実務だと地味にきついです。
- 止め方がプロンプト頼りになる。 「十分な証拠が集まったら終了してください」と書いたところで、十分かどうかを判断するのは毎回その場の LLM です。往復2回で切り上げる回もあれば、8回続く回もある
- 必要トークンが事前に計算できない。 何往復するかが読めないと、1回の調査にどれだけかかるかも読めません。しかも Swarm はメッセージ履歴を全員で共有するので、往復が増えるほど1回あたりの入力側も膨らみます。 履歴の長さ × 往復回数で効いてくるので、増え方が線形じゃないんですよね
- 止まらなかった時の逃げ道が、上限で殴ることしかない。 再帰回数の上限は付けられますが、それは「打ち切り」であって「終了」ではありません。打ち切られた回の出力を信用していいかは、また別の話になります
固定グラフだと、終わりは Adjudicator の後の条件付きエッジ、つまりコードです。ノードの数も、各ノードのツール呼び出しの上限も決まっているので、1回の調査の上限が掛け算で出せます。 実際いくらになるかではなく、いくらまでかかりうるかを先に言える。運用に載せることを考えると、ここの差は大きいと思います。
| 誰が終わりを決めるか | 1回のコスト | |
|---|---|---|
| Swarm | LLM(誰もバトンを渡さなくなった時) | 事前に読めない |
| 固定グラフ | コード(条件付きエッジ + 上限カウンタ) | 上限が掛け算で出る |
構成の候補を並べて比べる
比べたのはこの5つです。どれが優れているかではなく、どういう仕事に噛み合うかで並べます。
| 構成 | 役割の順番を決めるのは | 噛み合う仕事 |
|---|---|---|
| 単体の ReAct エージェント | LLM(その場) | 一番安い。調べた範囲が毎回変わってよい仕事 |
| Supervisor | 中央の LLM | 部分課題の集合そのものが毎回変わる仕事 |
| Swarm(ハンドオフ) | 担当エージェント同士 | 次に誰が要るか事前に列挙できない調査 |
| 固定グラフ + エージェントのノード | コード | 順番が既知で、深さだけ読めない仕事 |
| Plan-and-Execute | 毎回 LLM が計画を立てる | 計画が毎回違う仕事 |
今回の仕事は「順番が既知で、深さだけ読めない」でした。なので4番目です。
停止条件をどこに置けるかも、この並びとほぼ同じ順になります。 上の3つは終わりの判断が LLM 側に寄っていて、下の2つはコード側に置けます(Plan-and-Execute は計画の時点でステップ数が決まるので、そこは読める)。
Supervisor も一度考えたんですが、これは中央の LLM が「次はお前」と割り振る構成なので、割り振りのたびに LLM 呼び出しが1回乗ります。集合が毎回変わるならその1手に価値がありますが、今回は毎回同じ顔ぶれを同じ順で呼ぶので、払う理由が見つかりませんでした。Plan-and-Execute も同じ理由で落ちています。計画が毎回同じなら、計画を立てる手番は要らない。
グラフにすると、こう組む
固定グラフにすると、扇形に開いて合流させる形になります。
START
│
[Timeline] ← 基準時刻と変化率を確定させる
│
┌─────┼─────┐ ← 3本が同時に走る
▼ ▼ ▼
[一次情報][市場][報道]
└─────┼─────┘ ← 3本そろうまで待つ
▼
[Adjudicator] ← 反証を探して確証度を確定
│
差し戻し? ─→ ENDLangGraph だと、StateGraph にノードとエッジを置くだけです。
from operator import add
from typing import Annotated, Literal, TypedDict
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import END, START, StateGraph
from langgraph.prebuilt import create_react_agent
class Evidence(TypedDict):
hypothesis: str # どの仮説に対する証拠か
rank: Literal["A", "B", "C", "D", "E"]
published_at: str # ISO8601。ツールが必ず埋める
source_url: str
claim: str
supports: bool # 反証なら False
class ResearchState(TypedDict):
question: str
anchor: dict # Timeline が確定させる基準時刻と変化率
evidence: Annotated[list[Evidence], add] # 並走するノードがここに追記する
rounds: int
model = init_chat_model("anthropic:claude-opus-5")
def make_collector(name: str, tools: list, prompt: str):
agent = create_react_agent(model, tools=tools, prompt=prompt, name=name)
def node(state: ResearchState) -> dict:
result = agent.invoke({"messages": build_input(state["anchor"])})
# 会話は親に返さない。構造化した証拠だけを返す
return {"evidence": parse_evidence(result)}
return node
builder = StateGraph(ResearchState)
builder.add_node("timeline", timeline_node)
builder.add_node("primary_source", make_collector("PrimarySource", ...))
builder.add_node("market_structure", make_collector("MarketStructure", ...))
builder.add_node("narrative", make_collector("Narrative", ...))
builder.add_node("adjudicator", adjudicator_node)
builder.add_edge(START, "timeline")
for branch in ("primary_source", "market_structure", "narrative"):
builder.add_edge("timeline", branch) # 3本が同じスーパーステップで並走する
builder.add_edge(branch, "adjudicator") # 3本そろうまで adjudicator は動かない
builder.add_conditional_edges(
"adjudicator", needs_more_round, {"retry": "primary_source", "done": END}
)
app = builder.compile(checkpointer=InMemorySaver())evidence に付けた Annotated[list[Evidence], add] が地味に効きます。LangGraph は並走するノードが同じキーを書こうとした時、足し方(reducer)が決まっていないとエラーで落ちます。 ここを add にしておくと、3本の結果が順不同で追記されていく。上書きではないので、後から動いたノードが先の証拠を消すこともありません。
エージェントの割り方は Swarm で考えていた時から変えていません。仮説ごと(介入説・要人発言説…)ではなく、証拠の取り方ごとに割ります。仮説ごとに割ると、各エージェントが自分の仮説を通しにいってしまうので。
| ノード | 役割 | 主に見るもの |
|---|---|---|
| Timeline | 全部の基準時刻を確定させる | 価格系列(何時何分に何%動いたか) |
| PrimarySource | 一次情報 | 財務省・日銀の公表、記者会見の文字起こし、指標カレンダー |
| MarketStructure | 市場データの整合 | 他通貨ペア・関連市場が同じ形で動いたか |
| Narrative | 報道・SNS。証拠ではなく仮説の供給源として扱う | 記事、投稿 |
| Adjudicator | 反証を探して確証度を確定させる | 上の全部 |
グラフにして増えたのは、並走と隔離とリトライ単位
構成を変えたら、狙っていなかった利点が3つ付いてきました。
- 並走する。 一次情報・市場データ・報道は同じ基準時刻を見るだけで互いに依存しないので、同時に走れます。Swarm は常に1体だけが動く作りなので直列です。待ち時間が「3本の合計」から「一番遅い1本」になります
- 文脈の汚染が構造で消える。 Swarm はメッセージ履歴を全員で共有するので、誰かが自然文で書いた解釈に後段が引きずられます。グラフなら各ノードが自前の会話を持って、親に返すのは構造化した証拠だけにできます
- リトライの単位がまともになる。 失敗したノードだけ再実行できます
2つ目が個人的には大きいです。Swarm で考えていた時は「証拠は会話ではなく state の別チャンネルに積む」と工夫として書いていたんですが、グラフだとそれが工夫ではなく普通の書き方になります。
残す非決定性は、差し戻しの1本だけ
固定するとは言っても、条件付きエッジを1本だけ残します。
Adjudicator が「C ランク以下だけど、あと1回叩けば上がりそうな仮説がある」と判断した時に、収集ノードへ戻す1本です。
def needs_more_round(state: ResearchState) -> Literal["retry", "done"]:
if state["rounds"] >= 2: # 上限は state のカウンタで持つ
return "done"
return "retry" if has_cheap_upgrade(state["evidence"]) else "done"これなら非決定性はあっても、「名前のついたエッジを何回通ったか」として後から数えられます。Swarm のハンドオフのように経路そのものが毎回違う形になるのとは別物です。
そして rounds >= 2 の1行が、さっきの「終わりを誰が決めるか」への答えになっています。最大でも2周。 LLM が判断するのは「戻す価値があるか」だけで、「もう終わっていいか」は判断させていません。だから最悪ケースのトークン量が、1周分 × 2 で先に出せます。
それでも Swarm が向く場合
公平に書いておくと、Swarm が噛み合う仕事はちゃんとあります。
部分課題の集合が事前に列挙できない調査です。「この会社について調べて」みたいな入口だと、どの専門家が要るかは調べ始めるまで分かりません。あるいは、担当が「これは債券の話だから金利の担当に渡す」と判断すべきで、その経路を設計時に書き出せない場合。ここは固定グラフだと素直に書けません。
あと、形が決まっていない段階で試作するなら Swarm のほうが書く量が少なくて済みます。エッジを引かなくていいので。
ただ、そのどちらの場合もコストが読めないことは付いてきます。 そこは「探索の対価」として飲む、という整理になると思います。今回は題材の性質(確証度を名乗る = カバレッジが主張の一部で、しかも毎日回したい)がはっきりしていたので、飲まずに固定側へ倒しました。
確証度は「証拠ランク × 時刻の前後」で機械的に出す
ここからは構成に関係なく共通の話です。確証度は LLM の感想ではなく、2つの軸を掛けて機械的に出します。
1つ目は証拠のランク。
| ランク | 中身 | 例 |
|---|---|---|
| A | 当局の一次発表 | 財務省の介入実績、日銀の公表資料 |
| B | 観測できる市場データ | 価格・出来高・関連市場の動き(時刻つき) |
| C | 一次情報を引用した報道 | 引用元が特定できる記事 |
| D | 報道のみ | 「関係者によると」 |
| E | SNS・後付け解説 | 誰かの見立て |
2つ目は時刻の前後です。Timeline が確定させた値動きの開始時刻に対して、その情報が先に出ていたか、後から出たかを見ます。
- 情報の公開時刻 < 値動きの開始時刻 → 先行。説明になりうる
- 情報の公開時刻 > 値動きの開始時刻 → 後追い。説明としては弱い
「A ランク かつ 先行」なら高、「D ランク かつ 後追い」なら最低、というふうに落とします。しきい値は手で決めるしかないですが、同じ基準が全仮説に等しくかかるのが大事なところです。
そして時刻を LLM に推測させないこと。ツール側が必ずタイムスタンプを返す設計にして、日付や時差の計算もツールでやります。ここを自然文でやらせると、体感でいちばん静かに壊れます。
為替介入だけは、当日には確定しない
一方で、為替介入という題材にはこの設計でどうにもならない事情があります。
覆面介入があるので、実施した当日に一次情報(Aランク)が出ません。実績の公表は月次です。つまり冒頭の質問には、構造上、当日は推定しか返せない。
推定の材料としては、翌営業日に出る日銀当座預金の増減見込みと、市場筋の予想との乖離を見る筋道が知られています。ただしこれも「Bランクの間接証拠」であって、Aにはなりません。
なので出力には、確定条件を必ず載せます。
- いま出せる確証度: 中(Bランクの間接証拠が先行 / Aランクは未公表)
- 確定する条件: 財務省の月次公表(公表予定日を添える)
- 次に情報が増えるタイミング: 翌営業日の当座預金増減
「まだ分からない」を、いつ分かるかとセットで返す。僕が手で一次情報を当たると1つの仮説につき15〜20分くらいかかるので、これができるだけでも十分速いと思っています。
この設計の微妙なところ
うまくいかなそうなところも並べておきます。
ランクの定義は結局、僕の主観です。 スコアは客観に見えますが、A〜E をどう切るかは手作り。数字にした瞬間に客観っぽくなってしまうのが、この手の設計のいやらしいところだと思っています。
固定した分、想定外に弱くなります。 「実はこれ債券の話だった」みたいな時、グラフは書いてある経路しか通りません。Swarm を捨てて買ったのが再現性で、売ったのがこの柔軟性です。
上限は出せますが、平均は動かすまで分かりません。 収集ノード3本 + 判定 + 最大2周で、1回の調査にツール呼び出しが数十回。上限は掛け算で出るものの、実際どのへんに落ち着くかは回してみないと読めません。並走させる分、同時に走るトークンも増えます。
そして、まだ動かしていません。 上のコードも形を書いただけで、通していないです。
まとめ
エージェント構成を選ぶ時に効いたのは、「調査は動的か」ではなく「動的なのはどの判断か」でした。役割の順番と終わりどきが動的なら Swarm や Supervisor、役割の中の手数だけが動的なら固定グラフ。今回は後者だったので、順番と停止条件はコードで固定して、LLM には各ノードの中だけを任せています。
最後に決め手になったのは、Swarm だと議論の終わりまで LLM が握ることでした。止め方がプロンプト頼りになって、何往復するか読めず、履歴を共有している分だけ往復が伸びるほど入力も膨らむ。「たぶんこれくらい」としか言えない構成は、運用に載せる前提だとしんどいです。グラフなら上限が掛け算で出るので、動かす前に最悪ケースを言えます。
書いてみて思ったのは、確証度と停止条件は同じ話の裏表だったな、ということでした。どちらも「どこまでやったら終わりか」をコード側で決めておかないと成立しない。構成の比較だけしていたら、たぶん動的なほうを選んでいたと思います。
次は実際に組んで、並走したノードのマージがどれくらい素直に効くかと、上限に対して実際どのへんに落ち着くかを見てみます。動いたら書きます(^^)