バッチ推論は、JSONL フォーマットのテキストまたはマルチモーダルのチャット補完リクエストを、非同期バッチタスクとして送信します。夜間の要約、既存データのラベリング、モデル評価、データアノテーションなど、数分から数時間かかる長時間実行のオフラインワークロードに使用します。
仕組み
バッチタスクは、OpenAI のバッチファイル入力と結果の相関パターンに従います。各リクエストは一意の custom_id を使用し、完了後、その識別子を使用して出力またはエラーの JSONL からビジネスデータにライトバックされます。
タスクの送信 — 複数のリクエストを含む UTF-8 JSONL ファイルをアップロードし、返されたファイル ID を使用してバッチタスクを作成します。
非同期処理 — サーバーはバックグラウンドでリクエストを 1 行ずつ検証して実行します。アプリケーションは
batch_idを使用して、validating、in_progress、finalizingなどのステータスをクエリします。
結果のダウンロード — タスクが終端状態 (タスクが完了し、それ以上変更されないことを示す状態) に達した後、
outputをダウンロードします。失敗した行がある場合は、errorをダウンロードし、custom_idを使用して結果をライトバックするか、問題を特定します。
バッチ推論は、オフラインのモデル評価、既存データのアノテーション、コンテンツのバッチ処理、スケジュールされたアセット処理に適しています。チャットや検索の補完など、即時結果が必要なオンラインリクエストには適していません。
コマンドフォーマット
バッチ実行は、入力ファイルのアップロード → タスクの作成 → ステータスのクエリ → 結果のダウンロードの 4 つのステップに従います。
入力ファイルは UTF-8 でエンコードされた JSONL である必要があります。各行は独立したリクエストであり、一意の custom_id、固定値 method="POST"、およびタスクと一致する url と body.model を含みます。
REST API
REST API
POST /v2/vectordb/ai/batch/files/upload
multipart/form-data: file, model_name, endpoint, purpose=batch
POST /v2/vectordb/ai/batch/jobs/create
{"input_file_id":"<file ID>","endpoint":"/v1/chat/completions","completion_window":"24h"}
POST /v2/vectordb/ai/batch/jobs/describe
{"batch_id":"<task ID>"}
POST /v2/vectordb/ai/batch/files/content
{"batch_id":"<task ID>","file_type":"output|error"}Python
Python
upload_status, upload_data = post_multipart_upload("<input.jsonl>")
create_status, create_data = post_json(
"/v2/vectordb/ai/batch/jobs/create",
{
"input_file_id": upload_data["data"]["id"],
"endpoint": "<endpoint>",
"completion_window": "24h",
},
)
describe_status, describe_data = post_json(
"/v2/vectordb/ai/batch/jobs/describe",
{"batch_id": create_data["data"]["id"]},
)
content_status, content_data = post_json(
"/v2/vectordb/ai/batch/files/content",
{"batch_id": create_data["data"]["id"], "file_type": "output"},
)REST スクリプトは jq に依存します。
パラメーター
| パラメーター | 説明 |
file | アップロードに必須です。JSONL 入力ファイルです。サーバーは最初の行の custom_id、method、url、および body.model を検証します。 |
model_name | アップロードに必須です。model とも互換性があります。JSONL の最初の行の body.model と一致する必要があります。 |
endpoint | アップロードと作成に必須です。チャット補完には /v1/chat/completions を使用し、テキスト埋め込みタスクには /v1/embeddings を使用します。同じファイル内のすべての行は、同じエンドポイントを使用する必要があります。 |
purpose | アップロードではオプションです。空または batch のみ指定可能です。 |
input_file_id | タスクの作成に必須です。アップロード API によって返されるファイル ID です。OSS URL や外部ファイル識別子は受け付けられません。 |
completion_window | タスクの作成に必須です。完了ウィンドウです。24h から 336h または日単位をサポートします。 |
metadata.ds_name/metadata.ds_description | オプションです。タスク名は最大 100 文字、説明は最大 200 文字です。 |
batch_id | クエリとダウンロードに必須です。作成 API によって返されるタスク ID です。 |
file_type | ダウンロードに必須です。成功した行をダウンロードするには output を使用し、失敗した行をダウンロードするには error を使用します。 |
provider | オプションです。デフォルトは aliyun_milvus です。アップロード、作成、クエリ、ダウンロードは、同じ Alibaba Cloud アカウント、リージョン、およびワークスペースを使用する必要があります。 |
body.enable_thinking | オプションで、モデルによります。JSONL 行の body.model と同じレベルにある必要があります。デフォルトで思考が有効になっているモデルの場合、これを明示的に false に設定すると、不要な思考トークンを回避できます。このパラメーターを extra_body 内に配置しないでください。 |
戻り値
タスクが正常に作成されると、data.id が batch_id として返されます。ポーリング状態には、validating、in_progress、finalizing、cancelling が含まれます。終端状態 (タスクが完了し、それ以上変更されないことを示す状態) には、completed、failed、expired、cancelled が含まれます。
バッチタスクはキュー内で非同期に処理されます。作成から終端状態までの時間は通常、数分から数時間かかり、その間、ステータスは in_progress のままです。いつでもクエリ API POST /v2/vectordb/ai/batch/jobs/describe (batch_id を使用) を呼び出して、最新の status を取得できます。サンプルスクリプトのポーリングがタスク完了前に終了した場合でも、ステータスが completed に変更された後、batch_id を使用して再度クエリし、結果をダウンロードできます。ダウンロード API は JSONL コンテンツを直接返します。各行は、ビジネスデータに結果をライトバックするために、入力時の custom_id を保持しています。バッチタスクは、即時応答が必要なオンラインインタラクションには適していません。
例 1:夜間バッチによるナレッジベースの要約生成 (テキスト)
運用チームが、その日に追加された記事の要約をバッチ生成する必要があります。各行には 1 つの記事リクエストが含まれます。主要なパラメーターは body.model=qwen3.7-max、url=/v1/chat/completions、および completion_window=24h です。
REST API
REST API
#!/usr/bin/env bash
set -euo pipefail
MILVUS_REST_BASE_URL="http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN="<yourUsername>:<yourPassword>"
post_json() {
local path="$1"
local body="$2"
curl -X POST \
"$MILVUS_REST_BASE_URL$path" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d "$body"
}
MODEL_NAME="qwen3.7-max"
INPUT_FILE="${AIFUNC_AI_BATCH_INPUT_FILE:-}"
POLL_INTERVAL_SEC="${AIFUNC_AI_BATCH_POLL_INTERVAL_SEC:-5}"
MAX_POLL_ATTEMPTS="${AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS:-120}"
if [ -z "$INPUT_FILE" ]; then
echo "Save the sample JSONL requests to a file first, then set AIFUNC_AI_BATCH_INPUT_FILE to the file path." >&2
exit 1
fi
download_batch_file() {
local file_type="$1"; local output_file="$2"; local content_body
content_body="$(jq -nc --arg batch_id "$BATCH_ID" --arg file_type "$file_type" '{provider:"aliyun_milvus", batch_id:$batch_id, file_type:$file_type}')"
curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/content" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" -H "Content-Type: application/json" \
-d "$content_body" --output "$output_file"
}
UPLOAD_RESPONSE="$(curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/upload" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-F "provider=aliyun_milvus" -F "model_name=$MODEL_NAME" -F "endpoint=/v1/chat/completions" \
-F "purpose=batch" -F "file=@$INPUT_FILE;type=application/jsonl")"
INPUT_FILE_ID="$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$INPUT_FILE_ID" ] || exit 1
CREATE_BODY="$(jq -nc --arg input_file_id "$INPUT_FILE_ID" '{provider:"aliyun_milvus", input_file_id:$input_file_id, endpoint:"/v1/chat/completions", completion_window:"24h", metadata:{ds_name:"milvus-ai-function-example", ds_description:"AI Batch REST example"}}')"
CREATE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/create" "$CREATE_BODY")"
BATCH_ID="$(echo "$CREATE_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$BATCH_ID" ] || exit 1
for ((attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++)); do
DESCRIBE_BODY="$(jq -nc --arg batch_id "$BATCH_ID" '{provider:"aliyun_milvus", batch_id:$batch_id}')"
DESCRIBE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/describe" "$DESCRIBE_BODY")"
BATCH_STATUS="$(echo "$DESCRIBE_RESPONSE" | jq -r '.data.status // empty')"
case "$BATCH_STATUS" in
completed)
OUTPUT_FILE="${AIFUNC_AI_BATCH_OUTPUT_FILE:-./ai_batch_output_${BATCH_ID}.jsonl}"
download_batch_file "output" "$OUTPUT_FILE"
echo "Batch output downloaded to: $OUTPUT_FILE"
exit 0 ;;
failed|expired|cancelled)
echo "Batch $BATCH_ID ended with status: $BATCH_STATUS" >&2; exit 1 ;;
validating|in_progress|finalizing|cancelling)
[ "$attempt" -lt "$MAX_POLL_ATTEMPTS" ] && sleep "$POLL_INTERVAL_SEC" ;;
*) echo "Unknown status: ${BATCH_STATUS:-empty}" >&2; exit 1 ;;
esac
done
echo "Batch $BATCH_ID did not finish after $MAX_POLL_ATTEMPTS checks." >&2
exit 1Python
Python
from __future__ import annotations
import json
import os
import shutil
import sys
import tempfile
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
MILVUS_REST_BASE_URL = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN = "<yourUsername>:<yourPassword>"
MODEL_NAME = "qwen3.7-max"
INPUT_MODE = os.getenv("AIFUNC_AI_BATCH_INPUT_MODE", "text").lower()
POLL_INTERVAL_SEC = int(os.getenv("AIFUNC_AI_BATCH_POLL_INTERVAL_SEC", "5"))
MAX_POLL_ATTEMPTS = int(os.getenv("AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS", "120"))
def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def batch_request(custom_id: str, content: Any, *, enable_thinking: bool | None = None) -> dict[str, Any]:
body: dict[str, Any] = {"model": MODEL_NAME, "messages": [{"role": "user", "content": content}]}
if enable_thinking is not None:
body["enable_thinking"] = enable_thinking
return {"custom_id": custom_id, "method": "POST", "url": "/v1/chat/completions", "body": body}
def default_requests(input_mode: str) -> list[dict[str, Any]]:
if input_mode == "text":
return [
batch_request("milvus-summary-1", "サポートナレッジベース用に、中国語の一文で要約してください:Milvus は RAG、推薦、マルチモーダル検索のためのベクトル埋め込みを格納および検索します。", enable_thinking=False),
batch_request("milvus-summary-2", "このインシデントメモを中国語の FAQ タイトルと一文の回答に変換してください:ドキュメントが更新された後、ユーザーが新しいコンテンツを検索する前に埋め込みを再生成します。", enable_thinking=False),
batch_request("milvus-summary-3", "アシスタントが回答する前にポリシーの文章を取得するエンタープライズ RAG 事例について、中国語の一文でカタログ説明を作成してください。", enable_thinking=False),
]
if input_mode == "image":
return [batch_request("milvus-image-1", [
{"type": "image_url", "image_url": {"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"}},
{"type": "text", "text": "ペットサービスメディアライブラリ用に、このアップロードされた事例写真に対して、簡潔な中国語のアクセシビリティキャプションと最大 3 つの検索可能な件名タグを返してください。"},
])]
if input_mode == "video":
return [batch_request("milvus-video-1", [
{"type": "video", "video": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260409/dozxak/Wan_Video_Edit_33_1.mp4"},
{"type": "text", "text": "マーケティングアセットライブラリ用に、中国語の一文のシーン要約、3 つの検索キーワード、および手動でのブランドセーフティレビューが必要かどうかを返してください。"},
])]
if input_mode == "audio":
return [batch_request("milvus-audio-1", [
{"type": "input_audio", "input_audio": {"data": "https://dashscope.oss-cn-beijing.aliyuncs.com/audios/welcome.mp3", "format": "mp3"}},
{"type": "text", "text": "カスタマーサービスのホットライン応答メッセージアーカイブ用に、ウェルカムメッセージを文字起こしし、その目的、サービス可用性、録音またはプライバシーに関する通知が明確であるかを評価してください。音声に存在しない詳細は未確認としてマークしてください。"},
])]
print("AIFUNC_AI_BATCH_INPUT_MODE must be one of text, image, video, or audio", file=sys.stderr)
sys.exit(1)
def post_multipart_upload(input_file: str) -> tuple[int, dict[str, Any]]:
boundary = f"----milvus-ai-batch-{uuid.uuid4().hex}"
fields = {"provider": "aliyun_milvus", "model_name": MODEL_NAME, "endpoint": "/v1/chat/completions", "purpose": "batch"}
with tempfile.TemporaryFile(mode="w+b") as payload:
for name, value in fields.items():
payload.write(f"--{boundary}\r\n".encode())
payload.write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
payload.write(value.encode())
payload.write(b"\r\n")
payload.write(f"--{boundary}\r\n".encode())
payload.write(b'Content-Disposition: form-data; name="file"; filename="input.jsonl"\r\n')
payload.write(b"Content-Type: application/jsonl\r\n\r\n")
with open(input_file, "rb") as input_handle:
shutil.copyfileobj(input_handle, payload)
payload.write(b"\r\n")
payload.write(f"--{boundary}--\r\n".encode())
payload_length = payload.tell()
payload.seek(0)
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/upload",
data=payload,
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": f"multipart/form-data; boundary={boundary}", "Content-Length": str(payload_length)},
method="POST",
)
try:
with urlopen(request, timeout=600) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def download_batch_file(batch_id: str, file_type: str, output_file: Path) -> None:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/content",
data=json.dumps({"provider": "aliyun_milvus", "batch_id": batch_id, "file_type": file_type}, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=600) as response, output_file.open("wb") as output:
shutil.copyfileobj(response, output)
temporary_input = None
INPUT_FILE = os.getenv("AIFUNC_AI_BATCH_INPUT_FILE")
if INPUT_FILE is None:
temporary_input = tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False, encoding="utf-8")
for request in default_requests(INPUT_MODE):
temporary_input.write(json.dumps(request, ensure_ascii=False, separators=(",", ":")) + "\n")
temporary_input.close()
INPUT_FILE = temporary_input.name
try:
status, data = post_multipart_upload(INPUT_FILE)
if status != 200 or data.get("code") != 0:
sys.exit(1)
input_file_id = data.get("data", {}).get("id")
status, data = post_json("/v2/vectordb/ai/batch/jobs/create", {
"provider": "aliyun_milvus", "input_file_id": input_file_id, "endpoint": "/v1/chat/completions",
"completion_window": "24h", "metadata": {"ds_name": "milvus-ai-function-example", "ds_description": "AI Batch example"},
})
if status != 200 or data.get("code") != 0:
sys.exit(1)
batch_id = data.get("data", {}).get("id")
for attempt in range(1, MAX_POLL_ATTEMPTS + 1):
status, data = post_json("/v2/vectordb/ai/batch/jobs/describe", {"provider": "aliyun_milvus", "batch_id": batch_id})
batch_data = data.get("data", {})
batch_status = batch_data.get("status")
if batch_status == "completed":
output_file = Path(os.getenv("AIFUNC_AI_BATCH_OUTPUT_FILE", f"ai_batch_output_{batch_id}.jsonl"))
download_batch_file(batch_id, "output", output_file)
print(f"Batch output downloaded to: {output_file}")
if batch_data.get("error_file_id"):
download_batch_file(batch_id, "error", Path(f"ai_batch_errors_{batch_id}.jsonl"))
break
if batch_status in {"failed", "expired", "cancelled"}:
print(f"Batch {batch_id} ended with status: {batch_status}", file=sys.stderr)
sys.exit(1)
if attempt < MAX_POLL_ATTEMPTS:
time.sleep(POLL_INTERVAL_SEC)
finally:
if temporary_input is not None:
os.unlink(temporary_input.name)期待される結果:タスクが completed に達した後、ダウンロードされた output.jsonl の各行は、それぞれの custom_id を保持します。例:
{"custom_id":"milvus-summary-1","response":{"status_code":200,"body":{"choices":[{"message":{"role":"assistant","content":"Milvus は RAG、推薦と多模態検索シーンのベクトルデータベースです。"}}]}}}アプリケーション側は custom_id を使用して、対応する記事に要約をライトバックします。error.jsonl が生成された場合は、失敗した記事のみをリトライします。
例 2:ペットサービス事例の画像説明のバッチ生成 (画像)
ペットサービスプラットフォームが、事業者がアップロードした過去の事例画像のアクセシビリティ説明と検索タグを生成する必要があります。各行の content には image_url とテキスト命令が含まれます。画像理解をサポートするマルチモーダルモデルを使用します。アップロード時に指定された model_name は一致する必要があります。
REST API
REST API
#!/usr/bin/env bash
set -euo pipefail
MILVUS_REST_BASE_URL="http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN="<yourUsername>:<yourPassword>"
post_json() {
local path="$1"
local body="$2"
curl -X POST \
"$MILVUS_REST_BASE_URL$path" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d "$body"
}
MODEL_NAME="qwen3-vl-plus"
INPUT_FILE="${AIFUNC_AI_BATCH_INPUT_FILE:-}"
POLL_INTERVAL_SEC="${AIFUNC_AI_BATCH_POLL_INTERVAL_SEC:-5}"
MAX_POLL_ATTEMPTS="${AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS:-120}"
if [ -z "$INPUT_FILE" ]; then
echo "Save the sample JSONL requests to a file first, then set AIFUNC_AI_BATCH_INPUT_FILE to the file path." >&2
exit 1
fi
download_batch_file() {
local file_type="$1"; local output_file="$2"; local content_body
content_body="$(jq -nc --arg batch_id "$BATCH_ID" --arg file_type "$file_type" '{provider:"aliyun_milvus", batch_id:$batch_id, file_type:$file_type}')"
curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/content" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" -H "Content-Type: application/json" \
-d "$content_body" --output "$output_file"
}
UPLOAD_RESPONSE="$(curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/upload" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-F "provider=aliyun_milvus" -F "model_name=$MODEL_NAME" -F "endpoint=/v1/chat/completions" \
-F "purpose=batch" -F "file=@$INPUT_FILE;type=application/jsonl")"
INPUT_FILE_ID="$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$INPUT_FILE_ID" ] || exit 1
CREATE_BODY="$(jq -nc --arg input_file_id "$INPUT_FILE_ID" '{provider:"aliyun_milvus", input_file_id:$input_file_id, endpoint:"/v1/chat/completions", completion_window:"24h", metadata:{ds_name:"milvus-ai-function-example", ds_description:"AI Batch REST example"}}')"
CREATE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/create" "$CREATE_BODY")"
BATCH_ID="$(echo "$CREATE_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$BATCH_ID" ] || exit 1
for ((attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++)); do
DESCRIBE_BODY="$(jq -nc --arg batch_id "$BATCH_ID" '{provider:"aliyun_milvus", batch_id:$batch_id}')"
DESCRIBE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/describe" "$DESCRIBE_BODY")"
BATCH_STATUS="$(echo "$DESCRIBE_RESPONSE" | jq -r '.data.status // empty')"
case "$BATCH_STATUS" in
completed)
OUTPUT_FILE="${AIFUNC_AI_BATCH_OUTPUT_FILE:-./ai_batch_output_${BATCH_ID}.jsonl}"
download_batch_file "output" "$OUTPUT_FILE"
echo "Batch output downloaded to: $OUTPUT_FILE"
exit 0 ;;
failed|expired|cancelled)
echo "Batch $BATCH_ID ended with status: $BATCH_STATUS" >&2; exit 1 ;;
validating|in_progress|finalizing|cancelling)
[ "$attempt" -lt "$MAX_POLL_ATTEMPTS" ] && sleep "$POLL_INTERVAL_SEC" ;;
*) echo "Unknown status: ${BATCH_STATUS:-empty}" >&2; exit 1 ;;
esac
done
echo "Batch $BATCH_ID did not finish after $MAX_POLL_ATTEMPTS checks." >&2
exit 1Python
Python
from __future__ import annotations
import json
import os
import shutil
import sys
import tempfile
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
MILVUS_REST_BASE_URL = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN = "<yourUsername>:<yourPassword>"
MODEL_NAME = "qwen3-vl-plus"
INPUT_MODE = os.getenv("AIFUNC_AI_BATCH_INPUT_MODE", "image").lower()
POLL_INTERVAL_SEC = int(os.getenv("AIFUNC_AI_BATCH_POLL_INTERVAL_SEC", "5"))
MAX_POLL_ATTEMPTS = int(os.getenv("AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS", "120"))
def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def batch_request(custom_id: str, content: Any, *, enable_thinking: bool | None = None) -> dict[str, Any]:
body: dict[str, Any] = {"model": MODEL_NAME, "messages": [{"role": "user", "content": content}]}
if enable_thinking is not None:
body["enable_thinking"] = enable_thinking
return {"custom_id": custom_id, "method": "POST", "url": "/v1/chat/completions", "body": body}
def default_requests(input_mode: str) -> list[dict[str, Any]]:
if input_mode == "text":
return [
batch_request("milvus-summary-1", "サポートナレッジベース用に、中国語の一文で要約してください:Milvus は RAG、推薦、マルチモーダル検索のためのベクトル埋め込みを格納および検索します。", enable_thinking=False),
batch_request("milvus-summary-2", "このインシデントメモを中国語の FAQ タイトルと一文の回答に変換してください:ドキュメントが更新された後、ユーザーが新しいコンテンツを検索する前に埋め込みを再生成します。", enable_thinking=False),
batch_request("milvus-summary-3", "アシスタントが回答する前にポリシーの文章を取得するエンタープライズ RAG 事例について、中国語の一文でカタログ説明を作成してください。", enable_thinking=False),
]
if input_mode == "image":
return [batch_request("milvus-image-1", [
{"type": "image_url", "image_url": {"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"}},
{"type": "text", "text": "ペットサービスメディアライブラリ用に、このアップロードされた事例写真に対して、簡潔な中国語のアクセシビリティキャプションと最大 3 つの検索可能な件名タグを返してください。"},
])]
if input_mode == "video":
return [batch_request("milvus-video-1", [
{"type": "video", "video": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260409/dozxak/Wan_Video_Edit_33_1.mp4"},
{"type": "text", "text": "マーケティングアセットライブラリ用に、中国語の一文のシーン要約、3 つの検索キーワード、および手動でのブランドセーフティレビューが必要かどうかを返してください。"},
])]
if input_mode == "audio":
return [batch_request("milvus-audio-1", [
{"type": "input_audio", "input_audio": {"data": "https://dashscope.oss-cn-beijing.aliyuncs.com/audios/welcome.mp3", "format": "mp3"}},
{"type": "text", "text": "カスタマーサービスのホットライン応答メッセージアーカイブ用に、ウェルカムメッセージを文字起こしし、その目的、サービス可用性、録音またはプライバシーに関する通知が明確であるかを評価してください。音声に存在しない詳細は未確認としてマークしてください。"},
])]
print("AIFUNC_AI_BATCH_INPUT_MODE must be one of text, image, video, or audio", file=sys.stderr)
sys.exit(1)
def post_multipart_upload(input_file: str) -> tuple[int, dict[str, Any]]:
boundary = f"----milvus-ai-batch-{uuid.uuid4().hex}"
fields = {"provider": "aliyun_milvus", "model_name": MODEL_NAME, "endpoint": "/v1/chat/completions", "purpose": "batch"}
with tempfile.TemporaryFile(mode="w+b") as payload:
for name, value in fields.items():
payload.write(f"--{boundary}\r\n".encode())
payload.write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
payload.write(value.encode())
payload.write(b"\r\n")
payload.write(f"--{boundary}\r\n".encode())
payload.write(b'Content-Disposition: form-data; name="file"; filename="input.jsonl"\r\n')
payload.write(b"Content-Type: application/jsonl\r\n\r\n")
with open(input_file, "rb") as input_handle:
shutil.copyfileobj(input_handle, payload)
payload.write(b"\r\n")
payload.write(f"--{boundary}--\r\n".encode())
payload_length = payload.tell()
payload.seek(0)
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/upload",
data=payload,
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": f"multipart/form-data; boundary={boundary}", "Content-Length": str(payload_length)},
method="POST",
)
try:
with urlopen(request, timeout=600) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def download_batch_file(batch_id: str, file_type: str, output_file: Path) -> None:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/content",
data=json.dumps({"provider": "aliyun_milvus", "batch_id": batch_id, "file_type": file_type}, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=600) as response, output_file.open("wb") as output:
shutil.copyfileobj(response, output)
temporary_input = None
INPUT_FILE = os.getenv("AIFUNC_AI_BATCH_INPUT_FILE")
if INPUT_FILE is None:
temporary_input = tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False, encoding="utf-8")
for request in default_requests(INPUT_MODE):
temporary_input.write(json.dumps(request, ensure_ascii=False, separators=(",", ":")) + "\n")
temporary_input.close()
INPUT_FILE = temporary_input.name
try:
status, data = post_multipart_upload(INPUT_FILE)
if status != 200 or data.get("code") != 0:
sys.exit(1)
input_file_id = data.get("data", {}).get("id")
status, data = post_json("/v2/vectordb/ai/batch/jobs/create", {
"provider": "aliyun_milvus", "input_file_id": input_file_id, "endpoint": "/v1/chat/completions",
"completion_window": "24h", "metadata": {"ds_name": "milvus-ai-function-example", "ds_description": "AI Batch example"},
})
if status != 200 or data.get("code") != 0:
sys.exit(1)
batch_id = data.get("data", {}).get("id")
for attempt in range(1, MAX_POLL_ATTEMPTS + 1):
status, data = post_json("/v2/vectordb/ai/batch/jobs/describe", {"provider": "aliyun_milvus", "batch_id": batch_id})
batch_data = data.get("data", {})
batch_status = batch_data.get("status")
if batch_status == "completed":
output_file = Path(os.getenv("AIFUNC_AI_BATCH_OUTPUT_FILE", f"ai_batch_output_{batch_id}.jsonl"))
download_batch_file(batch_id, "output", output_file)
print(f"Batch output downloaded to: {output_file}")
if batch_data.get("error_file_id"):
download_batch_file(batch_id, "error", Path(f"ai_batch_errors_{batch_id}.jsonl"))
break
if batch_status in {"failed", "expired", "cancelled"}:
print(f"Batch {batch_id} ended with status: {batch_status}", file=sys.stderr)
sys.exit(1)
if attempt < MAX_POLL_ATTEMPTS:
time.sleep(POLL_INTERVAL_SEC)
finally:
if temporary_input is not None:
os.unlink(temporary_input.name)期待される結果:image-output.jsonl は、同じ custom_id=milvus-image-1 を使用して画像の説明とタグを返します。アプリケーション側は、テキストを事例ライブラリのフィールドに書き込み、画像ベクターと組み合わせて画像テキスト検索に利用できます。タスクが完了しても失敗した行がある場合は、file_type=error の結果ファイルをダウンロードします。
例 3:ビデオ素材の要約のバッチ生成 (ビデオ)
運用チームが、ビデオアセットの検索をサポートするために、過去のショートビデオに対して 1 文の要約を生成する必要があります。各行は video コンテンツブロックを介してビデオ URL を渡します。ビデオ理解をサポートするマルチモーダルモデルを使用します。
REST API
REST API
#!/usr/bin/env bash
set -euo pipefail
MILVUS_REST_BASE_URL="http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN="<yourUsername>:<yourPassword>"
post_json() {
local path="$1"
local body="$2"
curl -X POST \
"$MILVUS_REST_BASE_URL$path" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d "$body"
}
MODEL_NAME="qwen3-vl-plus"
INPUT_FILE="${AIFUNC_AI_BATCH_INPUT_FILE:-}"
POLL_INTERVAL_SEC="${AIFUNC_AI_BATCH_POLL_INTERVAL_SEC:-5}"
MAX_POLL_ATTEMPTS="${AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS:-120}"
if [ -z "$INPUT_FILE" ]; then
echo "Save the sample JSONL requests to a file first, then set AIFUNC_AI_BATCH_INPUT_FILE to the file path." >&2
exit 1
fi
download_batch_file() {
local file_type="$1"; local output_file="$2"; local content_body
content_body="$(jq -nc --arg batch_id "$BATCH_ID" --arg file_type "$file_type" '{provider:"aliyun_milvus", batch_id:$batch_id, file_type:$file_type}')"
curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/content" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" -H "Content-Type: application/json" \
-d "$content_body" --output "$output_file"
}
UPLOAD_RESPONSE="$(curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/upload" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-F "provider=aliyun_milvus" -F "model_name=$MODEL_NAME" -F "endpoint=/v1/chat/completions" \
-F "purpose=batch" -F "file=@$INPUT_FILE;type=application/jsonl")"
INPUT_FILE_ID="$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$INPUT_FILE_ID" ] || exit 1
CREATE_BODY="$(jq -nc --arg input_file_id "$INPUT_FILE_ID" '{provider:"aliyun_milvus", input_file_id:$input_file_id, endpoint:"/v1/chat/completions", completion_window:"24h", metadata:{ds_name:"milvus-ai-function-example", ds_description:"AI Batch REST example"}}')"
CREATE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/create" "$CREATE_BODY")"
BATCH_ID="$(echo "$CREATE_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$BATCH_ID" ] || exit 1
for ((attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++)); do
DESCRIBE_BODY="$(jq -nc --arg batch_id "$BATCH_ID" '{provider:"aliyun_milvus", batch_id:$batch_id}')"
DESCRIBE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/describe" "$DESCRIBE_BODY")"
BATCH_STATUS="$(echo "$DESCRIBE_RESPONSE" | jq -r '.data.status // empty')"
case "$BATCH_STATUS" in
completed)
OUTPUT_FILE="${AIFUNC_AI_BATCH_OUTPUT_FILE:-./ai_batch_output_${BATCH_ID}.jsonl}"
download_batch_file "output" "$OUTPUT_FILE"
echo "Batch output downloaded to: $OUTPUT_FILE"
exit 0 ;;
failed|expired|cancelled)
echo "Batch $BATCH_ID ended with status: $BATCH_STATUS" >&2; exit 1 ;;
validating|in_progress|finalizing|cancelling)
[ "$attempt" -lt "$MAX_POLL_ATTEMPTS" ] && sleep "$POLL_INTERVAL_SEC" ;;
*) echo "Unknown status: ${BATCH_STATUS:-empty}" >&2; exit 1 ;;
esac
done
echo "Batch $BATCH_ID did not finish after $MAX_POLL_ATTEMPTS checks." >&2
exit 1Python
Python
from __future__ import annotations
import json
import os
import shutil
import sys
import tempfile
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
MILVUS_REST_BASE_URL = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN = "<yourUsername>:<yourPassword>"
MODEL_NAME = "qwen3-vl-plus"
INPUT_MODE = os.getenv("AIFUNC_AI_BATCH_INPUT_MODE", "video").lower()
POLL_INTERVAL_SEC = int(os.getenv("AIFUNC_AI_BATCH_POLL_INTERVAL_SEC", "5"))
MAX_POLL_ATTEMPTS = int(os.getenv("AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS", "120"))
def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def batch_request(custom_id: str, content: Any, *, enable_thinking: bool | None = None) -> dict[str, Any]:
body: dict[str, Any] = {"model": MODEL_NAME, "messages": [{"role": "user", "content": content}]}
if enable_thinking is not None:
body["enable_thinking"] = enable_thinking
return {"custom_id": custom_id, "method": "POST", "url": "/v1/chat/completions", "body": body}
def default_requests(input_mode: str) -> list[dict[str, Any]]:
if input_mode == "text":
return [
batch_request("milvus-summary-1", "サポートナレッジベース用に、中国語の一文で要約してください:Milvus は RAG、推薦、マルチモーダル検索のためのベクトル埋め込みを格納および検索します。", enable_thinking=False),
batch_request("milvus-summary-2", "このインシデントメモを中国語の FAQ タイトルと一文の回答に変換してください:ドキュメントが更新された後、ユーザーが新しいコンテンツを検索する前に埋め込みを再生成します。", enable_thinking=False),
batch_request("milvus-summary-3", "アシスタントが回答する前にポリシーの文章を取得するエンタープライズ RAG 事例について、中国語の一文でカタログ説明を作成してください。", enable_thinking=False),
]
if input_mode == "image":
return [batch_request("milvus-image-1", [
{"type": "image_url", "image_url": {"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"}},
{"type": "text", "text": "ペットサービスメディアライブラリ用に、このアップロードされた事例写真に対して、簡潔な中国語のアクセシビリティキャプションと最大 3 つの検索可能な件名タグを返してください。"},
])]
if input_mode == "video":
return [batch_request("milvus-video-1", [
{"type": "video", "video": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260409/dozxak/Wan_Video_Edit_33_1.mp4"},
{"type": "text", "text": "マーケティングアセットライブラリ用に、中国語の一文のシーン要約、3 つの検索キーワード、および手動でのブランドセーフティレビューが必要かどうかを返してください。"},
])]
if input_mode == "audio":
return [batch_request("milvus-audio-1", [
{"type": "input_audio", "input_audio": {"data": "https://dashscope.oss-cn-beijing.aliyuncs.com/audios/welcome.mp3", "format": "mp3"}},
{"type": "text", "text": "カスタマーサービスのホットライン応答メッセージアーカイブ用に、ウェルカムメッセージを文字起こしし、その目的、サービス可用性、録音またはプライバシーに関する通知が明確であるかを評価してください。音声に存在しない詳細は未確認としてマークしてください。"},
])]
print("AIFUNC_AI_BATCH_INPUT_MODE must be one of text, image, video, or audio", file=sys.stderr)
sys.exit(1)
def post_multipart_upload(input_file: str) -> tuple[int, dict[str, Any]]:
boundary = f"----milvus-ai-batch-{uuid.uuid4().hex}"
fields = {"provider": "aliyun_milvus", "model_name": MODEL_NAME, "endpoint": "/v1/chat/completions", "purpose": "batch"}
with tempfile.TemporaryFile(mode="w+b") as payload:
for name, value in fields.items():
payload.write(f"--{boundary}\r\n".encode())
payload.write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
payload.write(value.encode())
payload.write(b"\r\n")
payload.write(f"--{boundary}\r\n".encode())
payload.write(b'Content-Disposition: form-data; name="file"; filename="input.jsonl"\r\n')
payload.write(b"Content-Type: application/jsonl\r\n\r\n")
with open(input_file, "rb") as input_handle:
shutil.copyfileobj(input_handle, payload)
payload.write(b"\r\n")
payload.write(f"--{boundary}--\r\n".encode())
payload_length = payload.tell()
payload.seek(0)
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/upload",
data=payload,
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": f"multipart/form-data; boundary={boundary}", "Content-Length": str(payload_length)},
method="POST",
)
try:
with urlopen(request, timeout=600) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def download_batch_file(batch_id: str, file_type: str, output_file: Path) -> None:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/content",
data=json.dumps({"provider": "aliyun_milvus", "batch_id": batch_id, "file_type": file_type}, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=600) as response, output_file.open("wb") as output:
shutil.copyfileobj(response, output)
temporary_input = None
INPUT_FILE = os.getenv("AIFUNC_AI_BATCH_INPUT_FILE")
if INPUT_FILE is None:
temporary_input = tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False, encoding="utf-8")
for request in default_requests(INPUT_MODE):
temporary_input.write(json.dumps(request, ensure_ascii=False, separators=(",", ":")) + "\n")
temporary_input.close()
INPUT_FILE = temporary_input.name
try:
status, data = post_multipart_upload(INPUT_FILE)
if status != 200 or data.get("code") != 0:
sys.exit(1)
input_file_id = data.get("data", {}).get("id")
status, data = post_json("/v2/vectordb/ai/batch/jobs/create", {
"provider": "aliyun_milvus", "input_file_id": input_file_id, "endpoint": "/v1/chat/completions",
"completion_window": "24h", "metadata": {"ds_name": "milvus-ai-function-example", "ds_description": "AI Batch example"},
})
if status != 200 or data.get("code") != 0:
sys.exit(1)
batch_id = data.get("data", {}).get("id")
for attempt in range(1, MAX_POLL_ATTEMPTS + 1):
status, data = post_json("/v2/vectordb/ai/batch/jobs/describe", {"provider": "aliyun_milvus", "batch_id": batch_id})
batch_data = data.get("data", {})
batch_status = batch_data.get("status")
if batch_status == "completed":
output_file = Path(os.getenv("AIFUNC_AI_BATCH_OUTPUT_FILE", f"ai_batch_output_{batch_id}.jsonl"))
download_batch_file(batch_id, "output", output_file)
print(f"Batch output downloaded to: {output_file}")
if batch_data.get("error_file_id"):
download_batch_file(batch_id, "error", Path(f"ai_batch_errors_{batch_id}.jsonl"))
break
if batch_status in {"failed", "expired", "cancelled"}:
print(f"Batch {batch_id} ended with status: {batch_status}", file=sys.stderr)
sys.exit(1)
if attempt < MAX_POLL_ATTEMPTS:
time.sleep(POLL_INTERVAL_SEC)
finally:
if temporary_input is not None:
os.unlink(temporary_input.name)期待される結果:video-output.jsonl は、custom_id=milvus-video-1 を持つビデオの要約を返します。要約をビデオアセットのメタデータに書き込んだ後、ビデオベクターとキーワードを組み合わせて検索できます。終端状態に error_file_id が含まれている場合は、エラーファイルを保持して失敗したタスクを特定します。
例 4:カスタマーサービスホットラインの応答メッセージの夜間アーカイブ (音声)
カスタマーサービス運用チームが、各サービスホットラインの応答メッセージを定期的にアーカイブし、コンテンツを文字起こしして、ホットラインの目的、サービス時間、録音またはプライバシーに関する通知が明確であるかを確認し、その結果を応答メッセージのバージョン ID を使用して設定センターにライトバックする必要があります。各行は、音声理解をサポートするモデルを使用して input_audio を介して MP3 形式の応答メッセージを渡し、一意の custom_id を使用して元のバージョンと関連付けます。
音声と文字起こしには、個人情報や内部サービス構成が含まれる場合があります。実行する前に、収集の権限付与と非識別化の要件を確認してください。出力は、ライトバックとスポットチェックを完了するために必要な期間のみ保持し、その後は組織のデータ保持ポリシーに従って安全に削除してください。本番環境の録音や結果をコードリポジトリにコミットしないでください。
REST API
REST API
#!/usr/bin/env bash
set -euo pipefail
MILVUS_REST_BASE_URL="http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN="<yourUsername>:<yourPassword>"
post_json() {
local path="$1"
local body="$2"
curl -X POST \
"$MILVUS_REST_BASE_URL$path" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d "$body"
}
MODEL_NAME="qwen3.5-omni-plus"
INPUT_FILE="${AIFUNC_AI_BATCH_INPUT_FILE:-}"
POLL_INTERVAL_SEC="${AIFUNC_AI_BATCH_POLL_INTERVAL_SEC:-5}"
MAX_POLL_ATTEMPTS="${AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS:-120}"
if [ -z "$INPUT_FILE" ]; then
echo "Save the sample JSONL requests to a file first, then set AIFUNC_AI_BATCH_INPUT_FILE to the file path." >&2
exit 1
fi
download_batch_file() {
local file_type="$1"; local output_file="$2"; local content_body
content_body="$(jq -nc --arg batch_id "$BATCH_ID" --arg file_type "$file_type" '{provider:"aliyun_milvus", batch_id:$batch_id, file_type:$file_type}')"
curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/content" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" -H "Content-Type: application/json" \
-d "$content_body" --output "$output_file"
}
UPLOAD_RESPONSE="$(curl --fail --silent --show-error -X POST "$MILVUS_REST_BASE_URL/v2/vectordb/ai/batch/files/upload" \
-H "Authorization: Bearer $MILVUS_AUTH_TOKEN" \
-F "provider=aliyun_milvus" -F "model_name=$MODEL_NAME" -F "endpoint=/v1/chat/completions" \
-F "purpose=batch" -F "file=@$INPUT_FILE;type=application/jsonl")"
INPUT_FILE_ID="$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$INPUT_FILE_ID" ] || exit 1
CREATE_BODY="$(jq -nc --arg input_file_id "$INPUT_FILE_ID" '{provider:"aliyun_milvus", input_file_id:$input_file_id, endpoint:"/v1/chat/completions", completion_window:"24h", metadata:{ds_name:"milvus-ai-function-example", ds_description:"AI Batch REST example"}}')"
CREATE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/create" "$CREATE_BODY")"
BATCH_ID="$(echo "$CREATE_RESPONSE" | jq -r '.data.id // empty')"
[ -n "$BATCH_ID" ] || exit 1
for ((attempt = 1; attempt <= MAX_POLL_ATTEMPTS; attempt++)); do
DESCRIBE_BODY="$(jq -nc --arg batch_id "$BATCH_ID" '{provider:"aliyun_milvus", batch_id:$batch_id}')"
DESCRIBE_RESPONSE="$(post_json "/v2/vectordb/ai/batch/jobs/describe" "$DESCRIBE_BODY")"
BATCH_STATUS="$(echo "$DESCRIBE_RESPONSE" | jq -r '.data.status // empty')"
case "$BATCH_STATUS" in
completed)
OUTPUT_FILE="${AIFUNC_AI_BATCH_OUTPUT_FILE:-./ai_batch_output_${BATCH_ID}.jsonl}"
download_batch_file "output" "$OUTPUT_FILE"
echo "Batch output downloaded to: $OUTPUT_FILE"
exit 0 ;;
failed|expired|cancelled)
echo "Batch $BATCH_ID ended with status: $BATCH_STATUS" >&2; exit 1 ;;
validating|in_progress|finalizing|cancelling)
[ "$attempt" -lt "$MAX_POLL_ATTEMPTS" ] && sleep "$POLL_INTERVAL_SEC" ;;
*) echo "Unknown status: ${BATCH_STATUS:-empty}" >&2; exit 1 ;;
esac
done
echo "Batch $BATCH_ID did not finish after $MAX_POLL_ATTEMPTS checks." >&2
exit 1Python
Python
from __future__ import annotations
import json
import os
import shutil
import sys
import tempfile
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen
MILVUS_REST_BASE_URL = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_AUTH_TOKEN = "<yourUsername>:<yourPassword>"
MODEL_NAME = "qwen3.5-omni-plus"
INPUT_MODE = os.getenv("AIFUNC_AI_BATCH_INPUT_MODE", "audio").lower()
POLL_INTERVAL_SEC = int(os.getenv("AIFUNC_AI_BATCH_POLL_INTERVAL_SEC", "5"))
MAX_POLL_ATTEMPTS = int(os.getenv("AIFUNC_AI_BATCH_MAX_POLL_ATTEMPTS", "120"))
def post_json(path: str, body: dict[str, Any], timeout: int = 120) -> tuple[int, dict[str, Any]]:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=timeout) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def batch_request(custom_id: str, content: Any, *, enable_thinking: bool | None = None) -> dict[str, Any]:
body: dict[str, Any] = {"model": MODEL_NAME, "messages": [{"role": "user", "content": content}]}
if enable_thinking is not None:
body["enable_thinking"] = enable_thinking
return {"custom_id": custom_id, "method": "POST", "url": "/v1/chat/completions", "body": body}
def default_requests(input_mode: str) -> list[dict[str, Any]]:
if input_mode == "text":
return [
batch_request("milvus-summary-1", "サポートナレッジベース用に、中国語の一文で要約してください:Milvus は RAG、推薦、マルチモーダル検索のためのベクトル埋め込みを格納および検索します。", enable_thinking=False),
batch_request("milvus-summary-2", "このインシデントメモを中国語の FAQ タイトルと一文の回答に変換してください:ドキュメントが更新された後、ユーザーが新しいコンテンツを検索する前に埋め込みを再生成します。", enable_thinking=False),
batch_request("milvus-summary-3", "アシスタントが回答する前にポリシーの文章を取得するエンタープライズ RAG 事例について、中国語の一文でカタログ説明を作成してください。", enable_thinking=False),
]
if input_mode == "image":
return [batch_request("milvus-image-1", [
{"type": "image_url", "image_url": {"url": "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"}},
{"type": "text", "text": "ペットサービスメディアライブラリ用に、このアップロードされた事例写真に対して、簡潔な中国語のアクセシビリティキャプションと最大 3 つの検索可能な件名タグを返してください。"},
])]
if input_mode == "video":
return [batch_request("milvus-video-1", [
{"type": "video", "video": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20260409/dozxak/Wan_Video_Edit_33_1.mp4"},
{"type": "text", "text": "マーケティングアセットライブラリ用に、中国語の一文のシーン要約、3 つの検索キーワード、および手動でのブランドセーフティレビューが必要かどうかを返してください。"},
])]
if input_mode == "audio":
return [batch_request("milvus-audio-1", [
{"type": "input_audio", "input_audio": {"data": "https://dashscope.oss-cn-beijing.aliyuncs.com/audios/welcome.mp3", "format": "mp3"}},
{"type": "text", "text": "カスタマーサービスのホットライン応答メッセージアーカイブ用に、ウェルカムメッセージを文字起こしし、その目的、サービス可用性、録音またはプライバシーに関する通知が明確であるかを評価してください。音声に存在しない詳細は未確認としてマークしてください。"},
])]
print("AIFUNC_AI_BATCH_INPUT_MODE must be one of text, image, video, or audio", file=sys.stderr)
sys.exit(1)
def post_multipart_upload(input_file: str) -> tuple[int, dict[str, Any]]:
boundary = f"----milvus-ai-batch-{uuid.uuid4().hex}"
fields = {"provider": "aliyun_milvus", "model_name": MODEL_NAME, "endpoint": "/v1/chat/completions", "purpose": "batch"}
with tempfile.TemporaryFile(mode="w+b") as payload:
for name, value in fields.items():
payload.write(f"--{boundary}\r\n".encode())
payload.write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
payload.write(value.encode())
payload.write(b"\r\n")
payload.write(f"--{boundary}\r\n".encode())
payload.write(b'Content-Disposition: form-data; name="file"; filename="input.jsonl"\r\n')
payload.write(b"Content-Type: application/jsonl\r\n\r\n")
with open(input_file, "rb") as input_handle:
shutil.copyfileobj(input_handle, payload)
payload.write(b"\r\n")
payload.write(f"--{boundary}--\r\n".encode())
payload_length = payload.tell()
payload.seek(0)
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/upload",
data=payload,
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": f"multipart/form-data; boundary={boundary}", "Content-Length": str(payload_length)},
method="POST",
)
try:
with urlopen(request, timeout=600) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def download_batch_file(batch_id: str, file_type: str, output_file: Path) -> None:
request = Request(
f"{MILVUS_REST_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/content",
data=json.dumps({"provider": "aliyun_milvus", "batch_id": batch_id, "file_type": file_type}, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": f"Bearer {MILVUS_AUTH_TOKEN}", "Content-Type": "application/json"},
method="POST",
)
with urlopen(request, timeout=600) as response, output_file.open("wb") as output:
shutil.copyfileobj(response, output)
temporary_input = None
INPUT_FILE = os.getenv("AIFUNC_AI_BATCH_INPUT_FILE")
if INPUT_FILE is None:
temporary_input = tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False, encoding="utf-8")
for request in default_requests(INPUT_MODE):
temporary_input.write(json.dumps(request, ensure_ascii=False, separators=(",", ":")) + "\n")
temporary_input.close()
INPUT_FILE = temporary_input.name
try:
status, data = post_multipart_upload(INPUT_FILE)
if status != 200 or data.get("code") != 0:
sys.exit(1)
input_file_id = data.get("data", {}).get("id")
status, data = post_json("/v2/vectordb/ai/batch/jobs/create", {
"provider": "aliyun_milvus", "input_file_id": input_file_id, "endpoint": "/v1/chat/completions",
"completion_window": "24h", "metadata": {"ds_name": "milvus-ai-function-example", "ds_description": "AI Batch example"},
})
if status != 200 or data.get("code") != 0:
sys.exit(1)
batch_id = data.get("data", {}).get("id")
for attempt in range(1, MAX_POLL_ATTEMPTS + 1):
status, data = post_json("/v2/vectordb/ai/batch/jobs/describe", {"provider": "aliyun_milvus", "batch_id": batch_id})
batch_data = data.get("data", {})
batch_status = batch_data.get("status")
if batch_status == "completed":
output_file = Path(os.getenv("AIFUNC_AI_BATCH_OUTPUT_FILE", f"ai_batch_output_{batch_id}.jsonl"))
download_batch_file(batch_id, "output", output_file)
print(f"Batch output downloaded to: {output_file}")
if batch_data.get("error_file_id"):
download_batch_file(batch_id, "error", Path(f"ai_batch_errors_{batch_id}.jsonl"))
break
if batch_status in {"failed", "expired", "cancelled"}:
print(f"Batch {batch_id} ended with status: {batch_status}", file=sys.stderr)
sys.exit(1)
if attempt < MAX_POLL_ATTEMPTS:
time.sleep(POLL_INTERVAL_SEC)
finally:
if temporary_input is not None:
os.unlink(temporary_input.name)期待される結果:call-greeting-output.jsonl または call-greeting-error.jsonl には、custom_id=milvus-audio-1 を持つ行が 1 つだけ含まれます。限定的な範囲での検証後、成功した結果は設定センターに書き込まれ、カスタマーサービス運用スタッフによるスポットチェックが行われます。失敗した行は元の custom_id を保持しており、音声権限、MP3 フォーマット、またはモデルパラメーターを修正した後に個別にリトライできます。