この記事のゴール
LLM を「答えるだけ」から「ツールを使って仕事をする」へ引き上げるのが、エージェント化です。Qwen3-8B-AWQは関数呼び出し(function calling)に対応していて、自前GPUのまま、データを外に出さずにツール実行エージェントを組めます。
ただしエージェントは最も事故りやすい領域です。LLM が返した引数をそのまま実行すれば、SQLにもシェルにもなる。本稿は、判断は LLM・実行は決定的コードに分離し、引数を検証してから実行する安全なループを、型安全に組む方法を示します。
信頼性の開示:tool calling の仕様・vLLM フラグ・思考モデルでの注意点は、Qwen 公式(Function Calling)・vLLM 公式(Tool Calling)・Qwen-Agentに基づきます。設計原則(判断と実行の分離)はツール使用・関数呼び出しの設計と揃えています。GPU 本番運用は動画AIローカライズ基盤で踏んだ領域です。
30秒の結論
- 有効化:
vllm serve Qwen/Qwen3-8B-AWQ --enable-auto-tool-choice --tool-call-parser hermes --reasoning-parser qwen3。これで OpenAI 互換のtoolsがそのまま機能する。 - ツールは型付き契約:Zod を真実源に、
tools定義の JSON Schema を自動生成。返ってきた引数は実行前に Zod で検証。 - 安全装置を必ず:反復上限・ツール allowlist・副作用の冪等キー・認可。LLM 出力をそのまま実行しない。
- 公式の注意:思考モデルでは ReAct 等の stopword 依存テンプレートを使わない。Hermes 形式を使う。
- 作るか借りるか:定型を任せたいなら Qwen-Agent、制御を握りたいなら素の OpenAI 互換ループ(本稿で実装)。
サーブ:Hermes 形式の tool calling を有効化
Qwen3 は Hermes スタイルの tool use を推奨。vLLM ではフラグで有効化します。
vllm serve Qwen/Qwen3-8B-AWQ \
--enable-auto-tool-choice \
--tool-call-parser hermes \
--reasoning-parser qwen3 \
--max-model-len 32768 --port 8000
🔧 公式の注意(思考モデル):Qwen 公式は、思考モデルで ReAct のような stopword 依存のツールテンプレートを推奨しないとしています。理由は「思考部(
<think>)に stopword が出力され、tool call が壊れ得る」から。Hermes 形式(--tool-call-parser hermes)を使うのが安全策です。
ツールは「型付き契約」:Zod を真実源にする
エージェントの安全性は、ツール定義が型であることから始まります。Zod スキーマを1つ書き、そこからモデルへ渡す tools の JSON Schema と、モデルが返した引数を検証する parser の両方を導きます(DRY・構造化出力と同じ思想)。
// lib/tools.ts — ツール=「名前・引数スキーマ・決定的な実行関数」の契約
import { z } from "zod";
export interface Tool<A extends z.ZodType> {
readonly name: string;
readonly description: string;
readonly args: A; // 引数スキーマ(真実源)
readonly execute: (a: z.infer<A>) => Promise<unknown>; // 実行は決定的コード
}
/** 在庫照会(読み取り専用・副作用なし) */
export const getStock: Tool<z.ZodObject<{ sku: z.ZodString }>> = {
name: "get_stock",
description: "SKUの在庫数を返す",
args: z.object({ sku: z.string().regex(/^[A-Z0-9-]{4,32}$/) }), // 入力境界を型で締める
execute: async ({ sku }) => ({ sku, qty: await stockRepo.count(sku) }),
};
/** OpenAI互換 tools 定義へ変換(Zod → JSON Schema を自動生成) */
export function toOpenAITool<A extends z.ZodType>(t: Tool<A>) {
return {
type: "function" as const,
function: { name: t.name, description: t.description, parameters: z.toJSONSchema(t.args) },
};
}
declare const stockRepo: { count(sku: string): Promise<number> };
ポイントは execute が普通の関数であること。LLM は「どのツールをどの引数で呼ぶか」を判断するだけで、実行は私たちの決定的コードが握ります。
安全なツール実行ループ(世界最高峰の作法)
エージェントループの本質は「モデルがツールを要求 → 検証して実行 → 結果を戻す → 完了まで繰り返す」。ここに上限・検証・冪等性を織り込みます。
// lib/agent-loop.ts — 反復上限つき・引数検証つき・allowlistつきの安全なループ
import OpenAI from "openai";
import type { Tool } from "./tools";
import { toOpenAITool } from "./tools";
import { z } from "zod";
const client = new OpenAI({ baseURL: process.env.QWEN_BASE_URL, apiKey: "internal", timeout: 60_000 });
export async function runAgent(
userMessage: string,
registry: ReadonlyMap<string, Tool<z.ZodType>>, // allowlist:ここに無いツールは呼べない
maxSteps = 6, // 暴走・無限ループの上限(必須)
): Promise<string> {
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "system", content: "ツールは必要な時だけ使う。引数は厳密に。" },
{ role: "user", content: userMessage },
];
const tools = [...registry.values()].map(toOpenAITool);
for (let step = 0; step < maxSteps; step++) {
const res = await client.chat.completions.create({
model: "Qwen/Qwen3-8B-AWQ", messages, tools,
temperature: 0.7, top_p: 0.8,
extra_body: { top_k: 20, chat_template_kwargs: { enable_thinking: false } }, // ツール選択は非思考で安定
});
const msg = res.choices[0]?.message;
if (!msg) throw new Error("empty response");
messages.push(msg);
const calls = msg.tool_calls ?? [];
if (calls.length === 0) return msg.content ?? ""; // ツール不要=最終回答
// 要求された各ツールを「検証してから」実行(並列でも順次でも、結果を必ず戻す)
for (const call of calls) {
const tool = registry.get(call.function.name);
// allowlist 外のツール要求は実行せず、その旨をモデルへ返す(落とさない)
const result = tool
? await executeChecked(tool, call.function.arguments)
: { error: `tool not allowed: ${call.function.name}` };
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
}
}
throw new AgentStepLimitError(`exceeded ${maxSteps} steps`); // 上限超過は明示的に失敗させる
}
/** 引数は“外部入力”。Zodで検証してからのみ実行。検証NGは実行せずモデルへ差し戻す。 */
async function executeChecked(tool: Tool<z.ZodType>, rawArgs: string): Promise<unknown> {
const parsed = tool.args.safeParse(safeJson(rawArgs));
if (!parsed.success) return { error: "invalid arguments", issues: parsed.error.issues.length };
return tool.execute(parsed.data); // ここで初めて副作用が起きる
}
const safeJson = (s: string): unknown => { try { return JSON.parse(s); } catch { return null; } };
export class AgentStepLimitError extends Error {}
この 60 行に、本番エージェントの安全装置が詰まっています。
- 反復上限(
maxSteps):無限ループ・暴走を構造的に止める(コスト暴発も防ぐ)。 - allowlist(
registry):登録外のツールは呼べない。モデルが幻のツールを要求しても実行されない。 - 引数検証(
executeChecked):function.argumentsは JSON文字列の外部入力。safeParseを通った時だけ実行。NG は差し戻して自己修正させる。 - 判断と実行の分離:LLM は要求するだけ、副作用は決定的コードの中で起きる。
副作用ツールは「冪等+認可」で守る
読み取りツール(在庫照会)は気楽ですが、書き込み・送信・決済といった副作用ツールは別格です。LLM はリトライや並列で同じ呼び出しを二度要求しえます。
// 副作用ツールは「冪等キー+認可」を必須に(二重実行・権限外実行を防ぐ)
export const refund: Tool<z.ZodObject<{ orderId: z.ZodString; idempotencyKey: z.ZodString }>> = {
name: "refund",
description: "注文を返金する(要認可・冪等)",
args: z.object({ orderId: z.string().uuid(), idempotencyKey: z.string().min(8) }),
execute: async ({ orderId, idempotencyKey }) => {
await authz.require("refund", orderId); // 認可:誰の権限で実行するか
return payments.refund(orderId, { idempotencyKey }); // 冪等:二度呼ばれても一度だけ効く
},
};
declare const authz: { require(action: string, resource: string): Promise<void> };
declare const payments: { refund(id: string, o: { idempotencyKey: string }): Promise<unknown> };
冪等性の作法は決済の冪等設計と同じ。エージェントが絡むと「二重実行」は現実的リスクなので、副作用ツールは冪等キー必須で設計します。認可は UIのif文ではなくツール実行の中で強制します。
自前ループ vs Qwen-Agent
| 自前 OpenAI 互換ループ(本稿) | Qwen-Agent | |
|---|---|---|
| 制御 | 完全に握れる(上限・検証・ログ) | フレームワークに委譲 |
| 定型 | 自分で書く | テンプレート/パースを吸収 |
| 学習コスト | OpenAI SDK が分かれば可 | ライブラリ作法を学ぶ |
| 向く | 本番の作り込み・監査要件 | 素早い試作・標準的なツール連携 |
Qwen-Agent は「Qwen3 の function calling を OpenAI 互換 API 上でテンプレート化し、ツール変換とパースを llm.chat() が自動処理」します。試作や標準ケースは Qwen-Agent、監査・ガードを握りたい本番は自前ループ——が使い分けの目安です。どちらでも、引数検証・上限・冪等・認可の原則は変わりません。
ハマりどころ & ベストプラクティス
- 🔴 LLM 出力をそのまま実行しない。
function.argumentsは外部入力。Zod 検証を通った時だけ実行する。 - 🔴 反復上限を必ず置く。
maxSteps無しのループはコスト暴発・無限ループの温床。超過は明示的に失敗させる。 - 🔴 副作用ツールは冪等+認可。二重実行と権限外実行を構造で防ぐ。危険なツール(任意SQL/シェル)はそもそも登録しない。
- 🟠 ツール選択は非思考でも安定。tool calling 自体は非思考モードで十分速い。複雑な計画が要る時だけ思考を使う。
- 🟠 思考モデルで ReAct/stopword テンプレを使わない(公式)。Hermes 形式(
--tool-call-parser hermes)を使う。 - 🟢 allowlist で最小権限。エージェントが触れるツールを必要最小限に。幻のツール要求は差し戻す。
- 🟢 各ステップを可観測に。どのツールをどの引数で呼び、検証が通ったか/失敗したかをメタデータでログ(引数のPIIは伏せる)。
よくある質問(FAQ)
Q. 8B でツール選択は正確にできる?
A. 単純〜中程度のツール選択は実用的です。ツール数を絞り、description と引数スキーマを明確にするほど安定します。複雑な多段計画が要るなら思考モードや上位モデルへのルーティングを検討。
Q. 並列ツール呼び出しは使える? A. モデルは1レスポンスで複数の tool call を返すことがあります。ループ側で各 call を検証して実行し、結果を全て戻す設計にしておけば、順次でも並列でも破綻しません(本稿の実装が対応済み)。
Q. arguments が壊れたJSONで返ってきたら?
A. executeChecked の safeJson+safeParse で実行せず差し戻します。モデルは差し戻しメッセージを見て自己修正します。壊れた引数で副作用を起こさないのが要点。
Q. Qwen-Agent と自前ループ、どっち? A. 試作・標準ケースは Qwen-Agent、監査・ガード・ログを握りたい本番は自前ループ。原則(検証・上限・冪等・認可)はどちらでも必須です。
Q. エージェントのコストが心配です。 A. 反復上限でステップ数を、非思考モードで出力トークンを抑えます。ツール結果のキャッシュやモデルルーティングも効きます。各ステップのトークンを可観測にして削りどころを見つけます。
まとめ
Qwen3-8B-AWQ のエージェント化は、「判断は LLM・実行は決定的コード」を、型と安全装置で徹底すれば本番に耐えます。
- Hermes 形式で有効化(
--enable-auto-tool-choice --tool-call-parser hermes)。思考モデルで ReAct は使わない。 - ツールは型付き契約——Zod を真実源に
toolsを生成し、返った引数を実行前に検証。 - 安全なループ——反復上限・allowlist・引数検証・差し戻し。
- 副作用は冪等+認可——二重実行・権限外実行を構造で防ぐ。
- 試作は Qwen-Agent、本番は自前ループ——原則は不変。
自前LLMのエージェント化を、ツール設計・安全なループ・冪等な副作用・認可・可観測性まで含めて本番品質で構築します。AI基盤の実績をご覧のうえご相談ください。一人 × 生成AIで、速く・安く・安全に。
出典・公式リソース
- Qwen 公式(Function Calling) — Hermes 形式・思考モデルの注意点
- vLLM 公式(Tool Calling) —
--enable-auto-tool-choice/ parser - Qwen-Agent(GitHub) — tool calling のテンプレート化
- Qwen3-8B-AWQ モデルカード — 思考モード・サンプリング
※ tool calling 仕様・vLLM フラグは更新されます。実装前に一次情報とお使いの版で必ず確認してください。