og:image
設定できると嬉しいな〜 と思いつつできないので、とりあえず先頭に配置されたアイキャッチ画像
LayerX AI Agent ブログリレー 22日目の記事です。
バクラク事業部 スタッフエンジニアの @izumin5210 です。
Platform Engineering 部 Enabling チームでいろいろなことをしています。
前回の記事で、AI Agent や Workflow を Durable に動かすことに利用できる Workflow Engine である Temporal について紹介しました。
今回は TypeScript + Vercel AI SDK で実装した AI Agent を Temporal 上で動かすための実装を解説します。
AI Agent を Temporal 上で動かすには?
Python の Pydantic AI では Pydantic AI 自身が、OpenAI Agents SDK Python では Temporal Workflow がそれぞれ Temporal Workflow 上で動かすための実装を公開しています。
TypeScript でも Mastra であれば Ingest という別の Workflow Engine での実行をサポートしており、 Temporal Workflow についても対応することが vNext Workflows リリース時のブログで表明されています。
しかし、TypeScript で Mastra などのフレームワークを使わず Vercel の AI SDK や OpenAI Agents SDK TypeScript で AI Agent を実装し Temporal Workflow 上で動かしたい場合、ちょうどいい感じのパッケージなどは今のところは提供されておらず、自前で実装する必要があります。
本記事ではタイトルにあるように AI SDK を例に、Temporal Workflow 上で Durable な AI Agents を実装する方法を解説していきます。
おさらい: Vercel AI SDK と Agent Loop
いきなり Temporal Workflow と AI SDK を組み合わせる前に、まずは AI SDK と Agent Loop についておさらいします。
以下は AI SDK に1つの Tool を組み合わせ、天気を教えてくれる AI Agent の例です。
const query = "今日の東京の天気は?"
const stream = streamText({
model: openai('gpt-4o'),
prompt: query,
stopWhen: stepCountIs(5),
tools: {
weather: tool({
description: 'Get the weather in a location (fahrenheit)',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => {
const temperature = Math.round(Math.random() * (90 - 32) + 32);
return {
location,
temperature,
};
},
}),
},
});
このコードを実行すると、内部的には以下のような挙動になるでしょう。
呼び出し元から見た挙動は「今日の天気は?
という入力で71℉です
が返ってくる」だけですが、内部的には2回の API 呼び出し(LLM処理)とツール実行が含まれます。
今回の例は天気情報を適当に返しているのでレスポンスも一瞬ですが、実際のプロダクトでは API をコールするためもっと遅くなるはずです。また、tool 実行前後でより複雑なテキストを生成するケースでは tool 実行だけでなくその前後の LLM の処理でも時間がかかる可能性があります。そのような状態で、例えば後段の LLM の処理がネットワーク不調等で失敗するとどうなるでしょうか。毎回最初から実行しているとお金が無駄にかかりますし、レイテンシが増大するためユーザ体験としても悪くなります。
そこで、この一連の流れをワークフローと捉え、それぞれの LLM の処理や tool 実行を記録し、いずれかが一時的に失敗したときはそこから再開できるようにすることでワークフローが Durable に(耐久性が高く)なる…。 これが前回の記事でも紹介した Temporal Workflow 導入のモチベーションです。
AI SDK で実装した AI Agent を Temporal Workflow で動かす
先ほどの AI SDK のコード例を見てもらうとわかるとおりですが、個々の LLM 呼び出しや tool 実行自体は generateText
関数に隠蔽されています。 全体として Durable にするにはそれぞれの実行を Temporal Workflow でいうところの Activity とする必要があります。
export async function getUserNameById(id: string): Promisestring> { const user = await repository.getUserById(id) return user.name } export async function greet(name: string): Promisestring> { return `👋 Hello, ${name}!`; }
import { proxyActivities } from '@temporalio/workflow'; import type * as activities from './activities'; const { getUserNameById, greet } = proxyActivitiestypeof activities>(); export async function sayHelloWorkflow({ userId }: { userId: string }): Promisestring> { const name = await getUserNameById(userId) return await greet(name); }
TypeScript での Temporal Workflow 利用例 – 前回記事より引用
上記のコードでは Activity としたい(実行を記録したい)関数は Workflow から単純に呼び出されているだけなので自明ですが、 AI SDK では内部の LLM 呼び出しや tool 実行を Activity とする必要があります。これはどうすればいいでしょうか。
AI SDK の Agent Loop をツール実行の直前で止める
すごく単純な話ですが、 AI SDK がツールの実行の直前あるいは直後でループを中断してくれればそれでいいわけです。
- LLM 処理: 最初にどのツールを実行するかと、ツールに渡すパラメタを決定してループ中断
- ツール呼び出し: 決定したパラメタでツール実行
- LLM 処理: ツールの結果をコンテキストにのせてループ再開
- …
以上のようなステップで分かれ、かつ各ステップが別の Temporal Activity で動作してくれれば、例えば3ステップ目の LLM 処理が失敗しても1, 2ステップの保存済みの結果を使って3ステップ目から再開できるはずです。
ではこの「ツールの実行で止める」というのをどうやって実現するかですが、Human-in-the-Loop(HITL) の実装が参考になります。 HITL は「(ユーザの許可が得られるまで) Loop を止める」ということで、やりたいことは実は似ています。 AI SDK の Cookbook には HITL の実装例が掲載されているので参考にできます。
このドキュメントにあるとおりで、 AI SDK ではツール定義から execute
関数を取り除くことでツール実行の手前でループを止めることができます。
const getWeatherTool = tool({
description: "Get the weather in a location (fahrenheit)",
inputSchema: z.object({
location: z.string().describe("The location to get the weather for"),
}),
outputSchema: z.object({
temperature: z.number().describe("The temperature in fahrenheit"),
}),
});
async function getWeatherInformation({
location,
}: InferToolInputtypeof getWeatherTool>) {
const temperature = Math.round(Math.random() * (90 - 32) + 32);
return {
location,
temperature,
};
}
Agent Loop を自前で制御し、ツール実行結果を LLM に渡す
ループが中断されたらそこまでに生成された messages
を確認し、ツール実行待ち状態があればツールを実行し、その結果を書き戻します。 そして、ツール実行結果が反映された messages
を generateText
に渡すことでループを再開できます
const tools = { weather: getWeatherTool };
export type MyUITools = InferUIToolstypeof tools>;
export type MyUIMessage = UIMessageunknown, {}, MyUITools>;
const messages: MyUIMessage[] = [
{
id: generateId(),
role: "user",
parts: [{ type: "text", text: "今日の東京の天気は?" }],
},
];
let step = 0;
const maxSteps = 10;
while (step maxSteps) {
const stream = streamText({
model: openai("gpt-4o"),
messages,
tools,
});
const newMsgs: MyUIMessage[] = [];
for await (const msg of readUIMessageStreamMyUIMessage>({
stream: stream.toUIMessageStream(),
})) {
if (newMsgs.at(-1)?.id === msg.id) {
newMsgs[newMsgs.length - 1] = msg;
} else {
newMsgs.push(msg);
}
}
let hasUnprocessedToolResults = false;
for (const uiMsg of uiMessages) {
const parts: typeof uiMsg.parts = [];
for (const part of uiMsg.parts) {
let newPart = part;
switch (part.type) {
case "tool-weather": {
if (part.state === "input-available") {
const output = await getWeatherInformation(content.input);
newPart = {
...part,
state: "output-available",
output,
}
hasUnprocessedToolResults = true;
}
break;
}
}
parts.push(newPart);
}
messages.push({ ...uiMsg, parts });
}
if (!hasUnprocessedToolResults) break
step++;
}
(上記のコードは結構威圧感ある感じになってますが、実際は汎化・抽象化などによりもうちょっとまともなコードにするといいでしょう。)
AI SDK の Loop Control に関するドキュメントの最下部に自前でループを操作する例があり、これも参考にしています。というか上記の例も自前で Loop Control しているようなものなので…。
LLM の処理, tool 実行を Temporal Activity にする
これで Temporal Acivity としたい(中断・再開可能にしたい) LLM の処理と tool の実行が分離できました。あとはこれらを関数に切り出し Activity として呼び出すようにするだけです。
export async function processLLM(inputMessages: MyUIMessage[]) {
const stream = streamText({
model: openai("gpt-4o"),
messages: convertToModelMessages(inputMessages),
tools,
});
const newMsgs: MyUIMessage[] = [];
for await (const msg of readUIMessageStreamMyUIMessage>({
stream: stream.toUIMessageStream(),
})) {
if (newMsgs.at(-1)?.id === msg.id) {
newMsgs[newMsgs.length - 1] = msg;
} else {
newMsgs.push(msg);
}
}
return newMsgs;
}
export async function executeToolCall(
part: ToolUIPartMyUITools>
): PromiseToolUIPartMyUITools>> {
if (part.state !== "input-available") return part;
switch (part.type) {
case "tool-weather": {
const output = await getWeatherInformation(part.input);
return {
...part,
state: "output-available",
output,
};
break;
}
default: {
throw new Error(`Unknown tool: ${part.type}`);
}
}
}
import { proxyActivities } from '@temporalio/workflow';
import type * as activities from './activities';
const { processLLM, executeToolCall } = proxyActivitiestypeof activities>();
export async function runAgent() {
const messages: MyUIMessage[] = [
{
id: generateId(),
role: "user",
parts: [{ type: "text", text: "今日の東京の天気は?" }],
},
];
let step = 0;
const maxSteps = 10;
while (step maxSteps) {
const uiMessages = await processLLM(messages);
let hasUnprocessedToolResults = false;
for (const uiMsg of uiMessages) {
const parts: typeof uiMsg.parts = [];
for (const part of uiMsg.parts) {
let newPart = part;
if (part.type.startsWith("tool-")) {
newPart = await executeToolCall(part as ToolUIPartMyUITools>);
hasUnprocessedToolResults = true;
}
parts.push(newPart);
}
messages.push({ ...uiMsg, parts });
}
if (!hasUnprocessedToolResults) break;
step++;
}
}
この runAgent
Workflow を実行しその様子を Web UI で見てみると、processLLM
と executeTool
がそれぞれ独立した Activity として実行されている様子が見えるはずです。
Temporal の Web UI 上での実行結果。 ツール実行とその前後の LLM の処理が別 Activity になっているのがわかる。 わかりやすくするために getWeatherTool
にはスリープを入れています。
各 Activity の入出力が Web UI 上で確認できる。
おわりに
TypeScript + AI SDK で実装した AI Agent の Agent Loop を Temporal Workflow として実装する例を紹介しました。一見するとちょっと威圧感ある実装ですが、一方で比較的汎化しやすい実装でもあるはずです。うまく薄い抽象化をかぶせることで一度書くだけで再利用しやすいコンポーネントにできるかもしれません。
「いやいや、とはいえコード複雑すぎやろ! Python(あるいはMastra)使お!」と思った方もいるでしょう。このあたりの基盤的な実装は今後おそらくコミュニティでも整備が進むはずで、もうちょっと待てば新しいソリューションが出るかもしれません。
今回は抽象化が少ない Vercel AI SDK を利用したからコード量が増えてるとも言えるし、逆に抽象化が少ないおかげでどんな要件でも柔軟に対応できているとも言えます。 AI Agent 周りはこの先どんな進化をしていくかは不透明なので、エコシステムもどう発展していくか・どういう抽象をかませるのが正解かもおそらく誰も見えていない状態です。 要するに、今うまいこと Bet できれば覇権が取れる可能性もあるということです。ワクワクしますね?
LayerX では AI Agent 時代のソフトウェアエンジニアリング・ Platform Engineering を共に作り上げていく仲間を募集しています!
元の記事を確認する