Chuyển đến nội dung chính
友田 陽大
Quantized LLMs & self-hosting
Qwen
エージェント
Tool Use
vLLM
TypeScript
Zod
生成AI

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

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

Published
Last updated
Updated
Reading time
9 min read
Author
友田 陽大
Chia sẻ

この記事のゴール

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:登録外のツールは呼べない。モデルが幻のツールを要求しても実行されない。
  • 引数検証(executeCheckedfunction.argumentsJSON文字列の外部入力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は伏せる)。

まとめ

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基盤の実績をご覧のうえご相談ください。一人 × 生成AIで、速く・安く・安全に。

出典・公式リソース

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

Câu hỏi thường gặp

8B でツール選択は正確にできる?
単純〜中程度のツール選択は実用的です。ツール数を絞り、description と引数スキーマを明確にするほど安定します。複雑な多段計画が要るなら思考モードや上位モデルへのルーティングを検討。
並列ツール呼び出しは使える?
モデルは1レスポンスで複数の tool call を返すことがあります。ループ側で各 call を検証して実行し、結果を全て戻す設計にしておけば、順次でも並列でも破綻しません(本稿の実装が対応済み)。
arguments が壊れたJSONで返ってきたら?
executeChecked の safeJson+safeParse で実行せず差し戻します。モデルは差し戻しメッセージを見て自己修正します。壊れた引数で副作用を起こさないのが要点。
Qwen-Agent と自前ループ、どっち?
試作・標準ケースは Qwen-Agent、監査・ガード・ログを握りたい本番は自前ループ。原則(検証・上限・冪等・認可)はどちらでも必須です。
エージェントのコストが心配です。
反復上限でステップ数を、非思考モードで出力トークンを抑えます。ツール結果のキャッシュやモデルルーティングも効きます。各ステップのトークンを可観測にして削りどころを見つけます。
友田

友田 陽大

Người phát triển sản phẩm đoạt Giải thưởng Bộ trưởng (METI). Với TypeScript + Python + AWS, tôi một mình triển khai trọn gói SaaS, chuyển đổi số công nghiệp và AI tạo sinh (RAG) sẵn sàng cho production — từ phân tích yêu cầu đến hạ tầng và vận hành.

I can take on the implementation from this article as an engagement

Self-hosting infrastructure for open-weight LLMs — build, production operations, and cost optimization

I've run Qwen3-8B-AWQ and 4-bit quantized Llama 3 in production on vLLM, operated as a reproducible inference stack on spot GPUs (Tesla T4) with Terraform. Quantization choice, VRAM and throughput sizing, swappable models behind a provider abstraction, and resumption after forced preemption — I design and implement inference infrastructure that stays up.

Nhận cả dự án trọn gói lẫn vai trò cố vấn kỹ thuật. Hãy bắt đầu bằng buổi tư vấn miễn phí 30 phút.

最短ルート:カレンダーから直接予約

相談内容が固まっている方は、フォーム送信よりその場で日程を確定する方がスムーズです。下記から空き時間をお選びください。

  • 30分のオンライン無料相談
  • Google Meet / Zoom / Microsoft Teams
  • NDA 商談前締結可・無理な営業はいたしません
無料相談の空き枠を予約する

Cũng đáng đọc