強化チャットボットエージェントのWorkflowを紹介します
はじめに
この記事は、Omniludeのai-serviceはなぜAssistant / Thread / Run構造を採用したのか、Omniludeのai-serviceでAgentはこう動きますの続きです。
前回までの記事では構造を説明しました。今回は、その構造が実際にバックオフィスのキャンバスの中でどのような形で動いているのかを見ていきます。説明対象は強化チャットボットです。
この記事は抽象的な例を新しく作ったものではありません。実際に作成されているworkflow構成をもとに、このエージェントがどんな入力を受け、どこで分岐し、どのassistantとtoolを通って答えを作るのかをそのまま説明します。
強化チャットボットを一文で言うと何か
このエージェントは、質問を4つの経路に分けて処理する、かなり明示的な分岐型チャットボットです。
- 一般的な質問ならそのまま答えます。
- 最新情報が必要なら検索経路に送ります。
- YouTube URLなら字幕要約経路に送ります。
- 一般的なWeb URLなら記事分析経路に送ります。
実際のworkflowを単純化すると、次のようになります。
つまり、このworkflowの核心は、ひとつのモデルを力任せに上手く呼ぶことではありません。まず質問の種類を決め、その後で適切な処理パイプラインを選ぶことにあります。
本当は直接触れる形で公開したいのですが、LLMの利用にはコストがかかるため、まだ一般公開はしていません。余裕ができたら、実際に動かせる形でも公開したいと思っています。
始まりはひとつのテキスト入力です
このエージェントの開始ノードはtext-inputで、例の値としては最近の社会問題って何?が入っています。単なるデモ文言でもありますが、同時にこのworkflowが何を解こうとしているのかもよく表しています。
質問をひとつ受け取ったら、すぐにLLMへ投げるのではなく、まずrouterノードへ送ります。ここで使うassistantはQuestion Routerで、実際に設定されているルーティングソースは4つです。
llm_direct: 直接回答web_search: Web検索後に回答youtube_summary: YouTube要約article_analyze: Webページ分析
このrouterは単純なif文ではありません。assistantのinstructionsの中に、どんな状況でどのsourceを選ぶべきかが明示されており、応答も必ずJSONで返すようになっています。たとえば最新情報、日付基準の情報、ファクトチェックが必要な質問はweb_searchへ、YouTube URLはyoutube_summaryへ、一般URLはarticle_analyzeへ、それ以外はllm_directへ送ります。
このときrouterはsourceだけでなくreasonも残します。キャンバスに付いているtext-visualizeノードのひとつは、その理由を目で確認するためのデバッグビューアです。つまりこの画面は、単なる設計図ではなく、実行の痕跡をそのまま確認する作業画面です。
直答経路がいちばん短いです
llm_directに分岐すると構造は単純です。routerの出力がQuestion Answer assistantを使うgenerate-textノードに入り、その出力はそのままfinishへ向かいます。
ここで重要なのは、単純な経路だからといって構造まで単純になるわけではないことです。generate-textノードは実行時にaiAssistantIdでassistantを再ロードし、system promptとuser promptを組み合わせてから実際のモデル呼び出しを行います。つまり、キャンバス上にassistantのメタデータが付いていても、ランタイム基準は最終的にassistant idです。
強化チャットボットで使っているassistantは次の通りです。
Router: Question Router (assistantId=1)
Direct answer: Question Answer (assistantId=2)
Search query writer: Web Search Expert (assistantId=12)
Search-based answer: AdaptiveAnswerer (assistantId=3)
YouTube writer: YoutubeSummarizer (assistantId=15)
Article writer: ArticleAnalizer (assistantId=16)
この構成のおかげで、同じgenerate-textノードでも、どのassistantを差し込むかによって役割がまったく変わります。ノードは汎用の実行器で、役割はassistantが持っています。
検索経路は2段階ではなく4段階です
このエージェントの本当の中心はweb_search経路です。現在の実装を見ると、検索ベースの回答を一発で作っていません。段階をはっきり分けています。
第1段階はWeb Search Expertです。このassistantは、ユーザーの質問をそのまま検索エンジンへ投げるのではなく、検索向きのクエリ1行に整えます。
第2段階はweb-search-toolです。保存されているworkflow設定ではsearchTool: "searx"になっており、実際のノード実装はSearXngを呼び出して検索結果スニペットを最大10件まで集めます。タイムアウトも200秒で設定してあり、無限待機を防いでいます。
第3段階はprompt-crafterです。このノードは検索結果をそのまま最終回答に貼り付けません。保存してあるテンプレートに{{results}}を注入して、回答用system promptを完成させます。テンプレートには現在時刻、参考資料、回答ルール、文体方針がすでに入っています。
第4段階はAdaptiveAnswererです。ここで面白いのは、元のユーザー質問はpromptとして維持され、検索で作ったコンテキストはsystemに入ることです。つまり検索結果をそのままつなげるのではなく、質問と根拠を分けたうえで、回答用writer assistantがもう一度文章を書きます。
流れをもう一度描くと、こうなります。
私はこの部分がかなり重要だと思っています。検索を使うからといって、すぐにRAGらしいひとかたまりの構造にせず、質問整形、根拠の組み立て、最終的な文章化を分離しているからです。運用時にも、どの段階が揺れているのかを見やすくなります。
YouTubeとArticle経路はtool-firstです
URL系の分岐はもっと直感的です。
YouTube URLなら、最初にyoutube-summary-toolが実行されます。このノードはYoutubeTranscriptToolを使って、時間情報付きのtranscriptを取得します。その次にYoutubeSummarizer assistantが、そのtranscriptを読みやすいコンテンツに書き直します。
一般的なWeb URLなら、最初にarticle-analyzer-toolが実行されます。このノードはCrawl4Aiでページを取得し、可能ならReadability4Jで本文だけを抽出して、タイトル付きのマークダウンに整えます。その後ArticleAnalizer assistantが、それを読みやすい文章に再構成します。
つまり、URL系の経路はどちらも同じ哲学です。
- まずtoolが素材を取ってきます。
- その次にassistantが人が読める結果に整理します。
この構造は検索経路とも似ています。Omniludeの現在のagent設計では、toolとassistantは競合関係ではありません。toolは素材を取りに行き、assistantはその素材を解釈し整理します。
この画面はただのきれいな図ではありません
バックオフィスのキャンバスを見ると、text-visualizeノードが途中に配置されています。これらは最終結果を作るのに必須ではありません。その代わり、今どんな値が流れているのかをその場で見せてくれます。
実際のフロント実装もその方向で作られています。workflow実行中に特定ノードがNODE_COMPLETEDイベントを受けると、フロントはそのノードの出力値をキャンバス状態に注入します。text-visualizeノードはその値をそのまま表示し、ストリーミングのdeltaが来たらテキストを累積します。
この差は思った以上に大きいです。多くのworkflowツールはノードを描くところで終わります。しかし現在のOmniludeバックオフィスのagent画面は、保存されたグラフを見る用途と、実際の実行中間値を観察する用途が合わさっています。だからこのキャンバスは設計図であると同時にデバッガでもあります。
実行エンジンはこう受け取ります
ここでキャンバスの外に出てみます。このworkflowはDBのai.ai_agent.workflow JSONBとして保存されています。別々のノードテーブルではなく、ひとつのグラフJSONとして持っているわけです。
実行が始まると、BasicWorkflowEngineはまずtypeが-inputで終わり、startNode=trueであるノードを探します。ここではtext-inputが開始点です。その後はNodeExecutorFactoryがノードタイプごとに対応するexecutorを選び、実行をつないでいきます。
強化チャットボットで重要なexecutorは、おおよそ次の通りです。
TextInputNode: 開始入力の準備RouterNode: assistantを使った分岐選択GenerateTextNode: assistantベースのテキスト生成WebSearchToolNode: SearXng検索YoutubeSummaryToolNode: YouTube字幕抽出ArticleAnalyzerToolNode: Web本文の整形PromptCrafterNode: テンプレートベースのsystem prompt生成FinishNode: 分岐結果の回収と終了
ここで特に重要なのは、generate-textとrouterの両方が実行時にassistantを再ロードする点です。agent自身が直接知能を持つのではなく、各ノードが必要なときにassistantを呼び出して使います。結局、agentはorchestrationであり、実際の推論はassistantが担います。
finishノードも見た目ほど単純ではありません
最後のfinishノードは、ただの終了ボタンではありません。入ってくるedgeを確認し、まだ進行中の入力ハンドルがあれば待ち、準備できたソースノードの出力だけを集めて最終結果として渡します。
この構造が必要なのは、分岐があるからです。routerで4経路に分かれるからといって、4経路がいつも同じように完了するわけではありません。ある質問は直答で終わり、ある質問は検索や要約を経由します。finishはそうした差を吸収する最後の収集器です。
つまりこのエージェントは、質問ひとつを受けて答えひとつを返す関数ではなく、分岐・待機・合流を持つグラフ実行器の上に載っています。
エージェントの意図
私は、このシンプルなエージェントがOmniludeの現在のAI活用方針をかなりよく示していると思っています。
第一に、汎用agentを誇張しません。まず質問の型を分け、それに合うtoolとwriterを実用的に貼り付けています。
第二に、検索経路をひとつのブラックボックスにしていません。質問整形、検索、コンテキスト組み立て、最終回答作成をそれぞれ分けています。
第三に、バックオフィスのキャンバスが単なるエディタではありません。実行の中間値を見られるようにして、プロンプト実験とデバッグを同じ画面で行えるようにしています。
第四に、assistantとagentの役割が明確です。assistantは推論部品であり、agentはその部品をつなぐworkflowです。
この意味で私は、今回の実装は「AI agentを作った」というより、「質問処理パイプラインを視覚的に運用できる形にした」と言うほうが近いと思っています。
まとめ
前の2本の記事でassistant、thread、run、agentの構造を説明したなら、今回の記事は、その説明が実際のキャンバスと実際のノード接続としてどう現れているのかを見せる事例です。強化チャットボットは、ひとつのrouter、いくつかのwriter assistant、そして検索・YouTube・記事toolを組み合わせて、かなり実用的なチャットボットとして動いています。
余裕ができたら、エージェントの構成や実装をもっと理解しやすく、画像や実際に動いている画面をもとに改めて説明したいと思います。