その変数、本当に値が入っていますか? ~Agent Scriptの実行順序の落とし穴~

変数に値が入っておらず落とし穴にはまる人

こんにちは。エンジニアの和田です。

Agentforceは、Agent Scriptの導入で、実装の自由度が格段に上がりました。プログラミング経験のある方は特にとっつきやすくなったのではないでしょうか。

Agent Scriptの文法に関しては、公式ドキュメントに一通り書いてありますし、サンプル例を見て感触を掴むこともできます。最近はそういうドキュメントをLLMに読み込ませれば、ScriptをLLMに書かせるということもできそうです。

しかし、とりあえず文法を押さえておけばいいでしょ、と思っていると、思わぬ落とし穴にはまる恐れがあります。LLMに書かせた場合も、文法的には正しくとも挙動として正しくない場合をレビューで発見できないと、まずいことになります。特に、LLMは、通常のプログラミング言語と違ってAgent Scriptに関する学習が今のところなされていないと言ってよいので、Agent Scriptに関するドキュメントを読ませてScriptを書かせるにしても注意が必要です。

今回は、そんな落とし穴の一つとして、「変数に値が入るタイミング」について取り上げたいと思います。

その変数、本当に値が入っていますか?

簡単な例として、天気について答えるAgentを構築してみたいと思います。

明日の東京の天気について尋ねられたら、接続しているFlowを経由して、天気情報を取得する、というものです。ただ、今回はサンプルなので、Flowからは、固定で"Sunny"という値が返ってくるものとします。

Flowから"Sunny"という値が返ってきたら、それを変数に代入して、プロンプトにその変数の値を埋め込んで処理させたい、と考えました。いかにもAgent Scriptらしい発想ですね。

そこで、以下のように書いてみました。

topic main:
   description: "Handle all user questions with WeatherSampleFlow."
   actions:
      weather_sample_flow:
         description: "Execute WeatherSampleFlow."
         inputs:
            input: string
               description: "location"
         outputs:
            output: string
               description: "Flow response."
         target: "flow://WeatherSampleFlow"
   reasoning:
      instructions:->
         | First, always execute {!@actions.call_weather} before responding.
         | Second, always answer by using the following information: {!@variables.latest_output}.
      actions:
         call_weather: @actions.weather_sample_flow
            description: "Search for weather."
            with input=...
            set @variables.latest_output = @outputs.output

instructionsにて、最初にFlowを実行するように指示し、次に得られた結果の入った変数の値を展開して、その情報を用いて回答するように指示しました。

これでめでたしめでたし...と思って、Agentに"What will the weather be like in Tokyo tomorrow?"と質問してみます。

そうすると以下の答えが返ってきました。

I do not have enough information to answer your question about the weather in Tokyo tomorrow. Would you like to try again or ask about another location?

おかしいですね。そこでログを見てみると、LLMにはinstructionsがこんな形で送信されていました。

First, always execute action.call_weather before responding.
Second, always answer by using the following information: .

なんと、変数の値を展開したはずが、展開できていません。どういうことでしょうか。

変数の値が入っていない原因

実は、この問題は、公式ドキュメントのFlow of Controlというページを見ると、原因がわかります。

ここには、以下の記述があります。

The reasoning instructions are processed sequentially, in top-to-bottom order. While the reasoning instructions can contain programmatic logic and text instructions, the LLM only starts reasoning after it has received the resolved prompt, not while Agentforce is still parsing.

ちょっとわかりにくいですが、resolved promptを一通り構築してからpromptをLLMに送信している、というように読めます。

これは当該ページの後半にある具体的なScript例を追ってみるとわかりやすいのですが、つまりこういうことです。

  • Scriptには通常決定論的記述(run文、set文、if文など)と非決定論的記述が混在しているが、その場合、LLMに送信する前に、決定論的に解決できるものは先に解決する。
  • 上から順に見ていき、決定論的に解決できるものを順に解決していく(すなわちrun文、set文、if文などを実行する)。途中で非決定論的記述に出会ったら、LLM送信用にとっておく。(ただし、if文で選択されなかった節の中にある非決定論的記述は無視する)非決定論的記述における変数等の埋め込みはその時点の値を埋め込んでおく。
  • instructionsの最後まで到達していたら、最終的なプロンプトが完成するはずである。そこで、このプロンプトをLLMに送信する。LLMに送信した結果、action呼び出しが必要と判断された場合は適宜actionを呼び出し、最終的にLLMにより返答を作成する。

さて、先ほどのScript例をもう一度見てみましょう。

topic main:
   description: "Handle all user questions with WeatherSampleFlow."
   actions:
      weather_sample_flow:
         description: "Execute WeatherSampleFlow."
         inputs:
            input: string
               description: "location"
         outputs:
            output: string
               description: "Flow response."
         target: "flow://WeatherSampleFlow"
   reasoning:
      instructions:->
         | First, always execute {!@actions.call_weather} before responding.
         | Second, always answer by using the following information: {!@variables.latest_output}.
      actions:
         call_weather: @actions.weather_sample_flow
            description: "Search for weather."
            with input=...
            set @variables.latest_output = @outputs.output

instructionsの節を見ると、決定論的な記述はないことがわかります。途中で{!@actions.call_weather} というactionの埋め込みはありますが、これはあくまでactionに対する参照を示しているのであって、actionの実行を指示する決定論的な記述ではありません(何しろ、前後のプロンプトで必ず実行せよと書いてあるとは限らないことからも、{!@actions.call_weather}という記述単体でactionの実行を指示しているわけではないことがわかります)。actionの実行を指示する決定論的記述とは、run文のようなものを指します *1

そこで、上から順に、非決定論的記述をプロンプトとして仕立て上げます。1行目は{!@actions.call_weather} の中身を展開して終わりです。次に2行目を処理し、{!@variables.latest_output}の中身を展開します。

ここで重要なのが、まだLLMに送信されていないということです。{!@actions.call_weather} を実行せよというような記述がありますが、これはrun文と違って決定論的記述ではないので、このプロンプトがLLMに送信され、実行の必要があるとLLMに判断されたうえで初めて実行されるものです。今はプロンプト自体がまだ送信されていないので、{!@actions.call_weather} はまだ実行されていません。

先ほど「{!@variables.latest_output}の中身を展開します」と書きましたが、{!@actions.call_weather} はまだ実行されていないわけですから、当然変数latest_outputにはまだ何も値が入っていません(初期値は空文字列であるものとします)。したがって、LLMに送信されるプロンプトとしては以下の通りになります。

First, always execute action.call_weather before responding.
Second, always answer by using the following information: .

これでは、うまく動作しませんよね。

要するに、Firstの行で区切ってLLMに送信して、その結果を受けてSecondの行をLLMに送信する、といったことができないわけです。決定論的記述であれば、例えばrun文が続けてあった場合には、1つ目のrun文の実行が終わったら2つ目のrun文を実行する、といった形になりますが、非決定論的記述では最後の文までまとめてLLMに送信されてしまう、というわけです。

なお、実は、試しに実行すると、たまにうまく動作して正しい答えが返ってくることがあります。その場合は、解決済みのプロンプトを送信後、LLMがactionの実行が必要と判定し、actionが実際に実行されて、再度プロンプトをLLMに送信しているようです。その場合には、2回目のプロンプト送信時には変数に値が入っているので、正しく動くことになります。

とはいえ、毎度正しい答えが返ってくるわけではありません。やはり、1回目のプロンプト送信時には変数の値が入っていないので、誤作動が起きやすい状況には違いありません。

対策

ではどうすればよいのでしょうか。

今回の問題は、Firstの部分だけで実行を区切ることができないのが原因です。したがって、実行を区切るように工夫する、というのが基本的な対策となります。

具体的には、以下の案が考えられます。

  1. 予めrun文で実行してしまう
  2. topicを分けて遷移させる

run文実行メインのやり方

まずは、一旦1でやってみましょうか。例えば以下のようになります。

   reasoning:
      instructions:->
         run @actions.weather_sample_flow
            with input="Tokyo"
            set @variables.latest_output = @outputs.output
         | Always answer by using the following information: {!@variables.latest_output}.

最初にrun文が実行されるので、変数に値が入った後でプロンプトが組み立てられ、実際に送信されるプロンプトは以下のようになります。

Always answer by using the following information: Sunny.

これで、変数に値が入らない問題自体は一件落着なのですが、この例の場合、別の問題が残っています。

それは、actionを呼ぶ際に引数をwith input="Tokyo"と指定していることです。本来、Tokyoの部分は、ユーザーの入力に応じて変動させたいはずです。本当はwith input=...と書きたいところですが、...を使うにはLLMの判断が必要になるため、run文では使えません。

よって、ユーザーの入力に対して地名を抽出する作業をどこか別にやる必要があります。本topicの前段で別にLLMを呼んで処理させる必要があるため、別topicで扱うしかありません。

例えば、このtopicの遷移前のtopicで、以下のようにしておきます。

   reasoning:
      instructions:->
         | First, execute {!@actions.set_location} to store the location whose weather the user wants to know in the location variable.
         # このあと遷移その他の処理を書く
      actions:
         set_location: @utils.setVariables
            description: "Store the requested location."
            with location=...

そして、main topicに到達した暁には、以下のようにします。

   reasoning:
      instructions:->
         run @actions.weather_sample_flow
            with input=@variables.location
            set @variables.latest_output = @outputs.output
         | Always answer by using the following information: {!@variables.latest_output}.

遷移前のtopicで変数locationに値を入れておけば、その値を用いて決定論的にrun文で利用できる、というわけです。

Topic分割メインのやり方

ここまで述べたやり方だと結果として解決策1と2のハイブリッドのような形になりましたが、解決策2だけ、すなわちtopic遷移だけという方法もあります。

例えば遷移前のtopicで

   reasoning:
      instructions:->
         | Always execute {!@actions.call_weather}.
         # このあと遷移その他の処理を書く

としておいて、遷移後のtopicで

   reasoning:
      instructions:->
         | Always answer by using the following information: {!@variables.latest_output}.

とする方法です。

ただ、これはこれで別の課題があり、実際に実行してみると以下のような事象が発生することがあります。

  • 遷移前のtopicの時点でAgentが回答してしまい、次のtopicに遷移しない
  • 遷移前のtopicでcall_weatherを呼ばないまま次のtopicに遷移してしまう

プロンプトエンジニアリングにより改善する余地はありますが、この辺りは、実際に実行してみて、適切なtopic設計を見出す、ということになりそうです。

そもそも変数埋め込みが必要か?

前提を覆すような話ですが、そもそもプロンプトに変数を展開することが必要かどうかは検討の余地がありそうです。

実のところ、冒頭のScriptのinstructionは、以下のようにするだけでもうまく機能します。

   reasoning:
      instructions:->
         | Always execute {!@actions.call_weather} before responding.

明示的に変数展開しなくても、actionの返却値をLLMが自分で判断すれば十分、というわけですね。

こうすれば、変数展開のタイミングについて考慮する必要がなくなり、誤作動も起こさないし、シンプルな設計になります。

というわけで、変数展開をしなくて済むならその方がシンプルですが、とはいっても変数展開したいときはありますよね。

例えば、

  • 変数の中身に関して、そのまま返答に使う前に、チェック項目を予めプロンプトに書いておいてLLMにチェックさせたい。
  • 変数の中身自体により次の実行を指示したい。(Actionの返却値に次の指示文が入っているなど)

というケースは考えられます。そういう場合に、上記で説明した、run文やtopic分割を考えることになります。

まとめ

以上、Agent Scriptの文法だけを追っていると見落としがちな落とし穴の例として、変数に値が入るタイミングについて取り上げました。一つのinstructionの非決定論的記述で途中まで実行といったことができないことにより、変数に値がうまく入らないことがあり、run文を使うなりtopicを分割するなりして実行を分けることが重要であることを解説しました。

本文中でも触れた、公式ドキュメントの Flow of Controlは、文法的な話題ではないので読むのを後回しにしがちですが、一度目を通しておくと、今回のような落とし穴にはまらずに済みます。後半にあるScript実行例を追うだけでも価値があります。

最後までお読みいただき、ありがとうございました。

*1:ただし、厳密には、両者でactionの意味するところが変わっています。{!@actions.call_weather}でいうactionはいわゆるreasoning actionであり、topic.reasoning.actions節に書かれ、LLMに対して実行する候補を提示するためのものです。トピック遷移等もreasoning actionで指定できます。一方、run文で言うところのactionは、topic.actions節に書かれ、Agentの外(Flow、Apex、Prompt Template等)との接続を意味します。