# Qwen3-8B-AWQ をエージェント化：Qwen-Agent × function calling の本番設計

> 自前のQwen3-8B-AWQをツールを使うエージェントにする本番設計。vLLMのHermes形式tool callingの有効化、型安全なツール契約(Zod→JSON Schema)、引数を検証してから実行する安全なループ、反復回数の上限・冪等な副作用・認可ガード、思考モードでのReAct禁止という公式注意点まで、世界最高峰のコードで解説します。

- 公開日: 2026-06-25
- 著者: 友田 陽大
- タグ: Qwen, エージェント, Tool Use, vLLM, TypeScript, Zod, 生成AI
- URL: https://tomodahinata.com/blog/qwen3-agent-tool-use-function-calling-qwen-agent-production

## 要点

- Qwen3はHermes形式のtool callingに対応。vLLMを `--enable-auto-tool-choice --tool-call-parser hermes` で起動すれば、OpenAI互換のtoolsパラメータでそのまま関数呼び出しが返る
- ツールは“型付き契約”：Zodスキーマを真実源に、JSON Schemaをtools定義へ自動生成し、モデルが返した引数を実行前にZodで検証。検証を通らない引数は絶対に実行しない
- 安全なループ：反復回数に上限、ツールはallowlist、副作用は冪等キー＋認可で保護。LLMの出力をそのままexecしない——判断はLLM・実行は決定的コードに分離する
- 公式の注意点：思考モデルではReAct等のstopword依存テンプレートを推奨しない（思考部にstopwordが出てtool callが壊れ得る）。Hermes形式を使う
- 自前で全部書くか、Qwen-Agentに任せるか。テンプレート/パースの定型はQwen-Agentが吸収。制御を握りたいなら素のOpenAI互換ループ——本記事は両方を型安全に示す

---

## この記事のゴール

LLM を「答えるだけ」から「**ツールを使って仕事をする**」へ引き上げるのが、エージェント化です。[Qwen3-8B-AWQ](/blog/qwen3-8b-awq-self-hosting-reasoning-production-guide)は関数呼び出し（function calling）に対応していて、**自前GPUのまま**、データを外に出さずにツール実行エージェントを組めます。

ただしエージェントは**最も事故りやすい**領域です。LLM が返した引数をそのまま実行すれば、SQLにもシェルにもなる。本稿は、**判断は LLM・実行は決定的コード**に分離し、**引数を検証してから実行する安全なループ**を、型安全に組む方法を示します。

> **信頼性の開示**：tool calling の仕様・vLLM フラグ・思考モデルでの注意点は、[Qwen 公式（Function Calling）](https://qwen.readthedocs.io/en/latest/framework/function_call.html)・[vLLM 公式（Tool Calling）](https://docs.vllm.ai/en/latest/features/tool_calling.html)・[Qwen-Agent](https://github.com/QwenLM/Qwen-Agent)に基づきます。設計原則（判断と実行の分離）は[ツール使用・関数呼び出しの設計](/blog/ai-agent-tool-use-function-calling-production-design)と揃えています。GPU 本番運用は[動画AIローカライズ基盤](/case-studies/ai-video-localization-lipsync)で踏んだ領域です。

---

## 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 ではフラグで有効化します。

```bash
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・[構造化出力と同じ思想](/blog/qwen3-structured-output-json-vllm-guided-decoding-zod)）。

```ts
// 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 は「どのツールをどの引数で呼ぶか」を**判断**するだけで、**実行は私たちの決定的コード**が握ります。

---

## 安全なツール実行ループ（世界最高峰の作法）

エージェントループの本質は「**モデルがツールを要求 → 検証して実行 → 結果を戻す → 完了まで繰り返す**」。ここに**上限・検証・冪等性**を織り込みます。

```ts
// 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 はリトライや並列で**同じ呼び出しを二度要求**しえます。

```ts
// 副作用ツールは「冪等キー＋認可」を必須に（二重実行・権限外実行を防ぐ）
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> };
```

**冪等性**の作法は[決済の冪等設計](/blog/stripe-payments-production-guide-webhooks-idempotency-subscriptions)と同じ。エージェントが絡むと「**二重実行**」は現実的リスクなので、副作用ツールは**冪等キー必須**で設計します。認可は **UIのif文ではなくツール実行の中**で強制します。

---

## 自前ループ vs Qwen-Agent

| | 自前 OpenAI 互換ループ（本稿） | Qwen-Agent |
| --- | --- | --- |
| 制御 | **完全に握れる**（上限・検証・ログ） | フレームワークに委譲 |
| 定型 | 自分で書く | **テンプレート/パースを吸収** |
| 学習コスト | OpenAI SDK が分かれば可 | ライブラリ作法を学ぶ |
| 向く | 本番の作り込み・監査要件 | 素早い試作・標準的なツール連携 |

[Qwen-Agent](https://github.com/QwenLM/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 で最小権限**。エージェントが触れるツールを必要最小限に。幻のツール要求は差し戻す。
- 🟢 **各ステップを可観測に**。どのツールをどの引数で呼び、検証が通ったか/失敗したかを[メタデータでログ](/blog/opentelemetry-observability-production-tracing-metrics-logs)（引数の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. **反復上限**でステップ数を、**非思考モード**で出力トークンを抑えます。ツール結果のキャッシュや[モデルルーティング](/blog/llama-inference-cost-optimization-self-host-vs-api#コスト削減レバー効果の大きい順)も効きます。各ステップのトークンを可観測にして削りどころを見つけます。

---

## まとめ

Qwen3-8B-AWQ のエージェント化は、「**判断は LLM・実行は決定的コード**」を、**型と安全装置**で徹底すれば本番に耐えます。

1. **Hermes 形式で有効化**（`--enable-auto-tool-choice --tool-call-parser hermes`）。思考モデルで ReAct は使わない。
2. **ツールは型付き契約**——Zod を真実源に `tools` を生成し、返った引数を実行前に検証。
3. **安全なループ**——反復上限・allowlist・引数検証・差し戻し。
4. **副作用は冪等＋認可**——二重実行・権限外実行を構造で防ぐ。
5. **試作は Qwen-Agent、本番は自前ループ**——原則は不変。

> 自前LLMのエージェント化を、ツール設計・安全なループ・冪等な副作用・認可・可観測性まで含めて本番品質で構築します。AI基盤の[実績](/case-studies/ai-video-localization-lipsync)をご覧のうえご相談ください。**一人 × 生成AI**で、速く・安く・安全に。

### 出典・公式リソース

- [Qwen 公式（Function Calling）](https://qwen.readthedocs.io/en/latest/framework/function_call.html) — Hermes 形式・思考モデルの注意点
- [vLLM 公式（Tool Calling）](https://docs.vllm.ai/en/latest/features/tool_calling.html) — `--enable-auto-tool-choice` / parser
- [Qwen-Agent（GitHub）](https://github.com/QwenLM/Qwen-Agent) — tool calling のテンプレート化
- [Qwen3-8B-AWQ モデルカード](https://huggingface.co/Qwen/Qwen3-8B-AWQ) — 思考モード・サンプリング

※ tool calling 仕様・vLLM フラグは更新されます。実装前に一次情報とお使いの版で必ず確認してください。
